Merge "androidfw: Add tests for compact entries/short offsets"
diff --git a/.clang-format b/.clang-format
index 03af56d..d60d33c 100644
--- a/.clang-format
+++ b/.clang-format
@@ -9,5 +9,17 @@
 ConstructorInitializerIndentWidth: 6
 ContinuationIndentWidth: 8
 IndentWidth: 4
+JavaImportGroups:
+- android
+- androidx
+- com.android
+- dalvik
+- libcore
+- com
+- junit
+- net
+- org
+- java
+- javax
 PenaltyBreakBeforeFirstCallParameter: 100000
 SpacesBeforeTrailingComments: 1
diff --git a/Android.bp b/Android.bp
index aa65486..0a14565 100644
--- a/Android.bp
+++ b/Android.bp
@@ -150,6 +150,9 @@
     visibility: [
         // DO NOT ADD ANY MORE ENTRIES TO THIS LIST
         "//external/robolectric-shadows:__subpackages__",
+        //This will eventually replace the item above, and serves the
+        //same purpose.
+        "//external/robolectric:__subpackages__",
         "//frameworks/layoutlib:__subpackages__",
     ],
 }
@@ -211,6 +214,7 @@
         "android.hardware.radio-V1.5-java",
         "android.hardware.radio-V1.6-java",
         "android.hardware.radio.data-V1-java",
+        "android.hardware.radio.ims-V1-java",
         "android.hardware.radio.messaging-V1-java",
         "android.hardware.radio.modem-V1-java",
         "android.hardware.radio.network-V2-java",
@@ -384,6 +388,7 @@
         "av-types-aidl-java",
         "tv_tuner_resource_manager_aidl_interface-java",
         "soundtrigger_middleware-aidl-java",
+        "modules-utils-binary-xml",
         "modules-utils-build",
         "modules-utils-preconditions",
         "modules-utils-statemachine",
diff --git a/GAME_MANAGER_OWNERS b/GAME_MANAGER_OWNERS
index 502a9e36..b65c43a 100644
--- a/GAME_MANAGER_OWNERS
+++ b/GAME_MANAGER_OWNERS
@@ -1,2 +1,3 @@
 lpy@google.com
-timvp@google.com
+chingtangyu@google.com
+xwxw@google.com
diff --git a/apct-tests/perftests/core/Android.bp b/apct-tests/perftests/core/Android.bp
index ab20fdb..9366ff2d 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",
     ],
@@ -60,4 +62,10 @@
 
     test_suites: ["device-tests"],
     certificate: "platform",
+
+    errorprone: {
+        javacflags: [
+            "-Xep:ReturnValueIgnored:WARN",
+        ],
+    },
 }
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/ReferencePerfTest.java b/apct-tests/perftests/core/src/android/libcore/ReferencePerfTest.java
index 2ef68ca..05a3e12 100644
--- a/apct-tests/perftests/core/src/android/libcore/ReferencePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/ReferencePerfTest.java
@@ -118,7 +118,7 @@
         int got = count.get();
         if (n != got) {
             throw new IllegalStateException(
-                    String.format("Only %i of %i objects finalized?", got, n));
+                    String.format("Only %d of %d objects finalized?", got, n));
         }
     }
 }
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/XmlSerializerPerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
new file mode 100644
index 0000000..412cb5a
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.libcore;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Xml;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.XmlObjectFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Compares various kinds of method invocation.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class XmlSerializerPerfTest {
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void timeFastSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeFastSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    private void runTest(XmlSerializer serializer, int depth) throws IOException {
+        File file = File.createTempFile(XmlSerializerPerfTest.class.getSimpleName(), "tmp");
+        try (OutputStream out = new FileOutputStream(file)) {
+            serializer.setOutput(out, StandardCharsets.UTF_8.name());
+            serializer.startDocument(null, true);
+            writeContent(serializer, depth);
+            serializer.endDocument();
+        }
+    }
+
+    private void writeContent(XmlSerializer serializer, int depth) throws IOException {
+        serializer.startTag(null, "tag");
+        serializer.attribute(null, "attribute", "value1");
+        if (depth > 0) {
+            writeContent(serializer, depth - 1);
+        }
+        serializer.endTag(null, "tag");
+    }
+
+}
diff --git a/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/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java
index 0802072..0cce6ad 100644
--- a/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/DisplayPerfTest.java
@@ -18,11 +18,11 @@
 
 import android.content.Context;
 import android.hardware.display.DisplayManager;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
 import android.provider.Settings;
 import android.view.Display;
 
-import androidx.benchmark.BenchmarkState;
-import androidx.benchmark.junit4.BenchmarkRule;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -38,7 +38,7 @@
     private static final float DELTA = 0.001f;
 
     @Rule
-    public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+    public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
     private DisplayManager mDisplayManager;
     private Context mContext;
@@ -51,7 +51,7 @@
 
     @Test
     public void testBrightnessChanges() throws Exception {
-        final BenchmarkState state = mBenchmarkRule.getState();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
diff --git a/apct-tests/perftests/core/src/android/util/XmlPerfTest.java b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
index e05bd2a..b83657b 100644
--- a/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
+++ b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
@@ -28,6 +28,8 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.HexDump;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java b/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
index 76656bd..a31184c 100644
--- a/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
+++ b/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
@@ -22,6 +22,9 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.FastDataInput;
+import com.android.modules.utils.FastDataOutput;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -68,7 +71,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             os.reset();
-            final FastDataOutput out = FastDataOutput.obtainUsing4ByteSequences(os);
+            final FastDataOutput out = ArtFastDataOutput.obtain(os);
             try {
                 doWrite(out);
                 out.flush();
@@ -84,7 +87,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             os.reset();
-            final FastDataOutput out = FastDataOutput.obtainUsing3ByteSequences(os);
+            final FastDataOutput out = FastDataOutput.obtain(os);
             try {
                 doWrite(out);
                 out.flush();
@@ -116,7 +119,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             is.reset();
-            final FastDataInput in = FastDataInput.obtainUsing4ByteSequences(is);
+            final FastDataInput in = ArtFastDataInput.obtain(is);
             try {
                 doRead(in);
             } finally {
@@ -131,7 +134,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             is.reset();
-            final FastDataInput in = FastDataInput.obtainUsing3ByteSequences(is);
+            final FastDataInput in = FastDataInput.obtain(is);
             try {
                 doRead(in);
             } finally {
diff --git a/apct-tests/perftests/multiuser/AndroidManifest.xml b/apct-tests/perftests/multiuser/AndroidManifest.xml
index 63e5983..5befa1f 100644
--- a/apct-tests/perftests/multiuser/AndroidManifest.xml
+++ b/apct-tests/perftests/multiuser/AndroidManifest.xml
@@ -35,4 +35,8 @@
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
             android:targetPackage="com.android.perftests.multiuser"/>
 
+    <queries>
+        <package android:name="perftests.multiuser.apps.dummyapp" />
+    </queries>
+
 </manifest>
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
index f844ba3..cc74a52 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
@@ -30,8 +30,8 @@
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 
@@ -84,7 +84,7 @@
 
     private static class TestWindow extends BaseIWindow {
         final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
-        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        final int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         final InsetsState mOutInsetsState = new InsetsState();
         final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0];
         final Rect mOutAttachedFrame = new Rect();
@@ -106,7 +106,7 @@
 
                 long startTime = SystemClock.elapsedRealtimeNanos();
                 session.addToDisplay(this, mLayoutParams, View.VISIBLE,
-                        Display.DEFAULT_DISPLAY, mRequestedVisibilities, inputChannel,
+                        Display.DEFAULT_DISPLAY, mRequestedVisibleTypes, inputChannel,
                         mOutInsetsState, mOutControls, mOutAttachedFrame, mOutSizeCompatScale);
                 final long elapsedTimeNsOfAdd = SystemClock.elapsedRealtimeNanos() - startTime;
                 state.addExtraResult("add", elapsedTimeNsOfAdd);
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 9ac3e41..9d363c8 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -80,6 +80,7 @@
 import android.os.RemoteCallback;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
@@ -619,7 +620,7 @@
         return blobInfos;
     }
 
-    private void deleteBlobInternal(long blobId, int callingUid) {
+    private void deleteBlobInternal(long blobId) {
         synchronized (mBlobsLock) {
             mBlobsMap.entrySet().removeIf(entry -> {
                 final BlobMetadata blobMetadata = entry.getValue();
@@ -1612,10 +1613,7 @@
         @Override
         @NonNull
         public List<BlobInfo> queryBlobsForUser(@UserIdInt int userId) {
-            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
-                throw new SecurityException("Only system uid is allowed to call "
-                        + "queryBlobsForUser()");
-            }
+            verifyCallerIsSystemUid("queryBlobsForUser");
 
             final int resolvedUserId = userId == USER_CURRENT
                     ? ActivityManager.getCurrentUser() : userId;
@@ -1629,13 +1627,9 @@
 
         @Override
         public void deleteBlob(long blobId) {
-            final int callingUid = Binder.getCallingUid();
-            if (callingUid != Process.SYSTEM_UID) {
-                throw new SecurityException("Only system uid is allowed to call "
-                        + "deleteBlob()");
-            }
+            verifyCallerIsSystemUid("deleteBlob");
 
-            deleteBlobInternal(blobId, callingUid);
+            deleteBlobInternal(blobId);
         }
 
         @Override
@@ -1716,6 +1710,18 @@
             return new BlobStoreManagerShellCommand(BlobStoreManagerService.this).exec(this,
                     in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args);
         }
+
+        /**
+         * Verify if the caller is an admin user's app with system uid
+         */
+        private void verifyCallerIsSystemUid(final String operation) {
+            if (UserHandle.getCallingAppId() != Process.SYSTEM_UID
+                    || !mContext.getSystemService(UserManager.class)
+                    .isUserAdmin(UserHandle.getCallingUserId())) {
+                throw new SecurityException("Only admin user's app with system uid"
+                        + "are allowed to call #" + operation);
+            }
+        }
     }
 
     static final class DumpArgs {
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 5a445d4..dade7c3 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -309,7 +309,7 @@
         /**
          * Callback method that is invoked by the system when the alarm time is reached.
          */
-        public void onAlarm();
+        void onAlarm();
     }
 
     final class ListenerWrapper extends IAlarmListener.Stub implements Runnable {
@@ -453,7 +453,7 @@
      * @see #RTC
      * @see #RTC_WAKEUP
      */
-    public void set(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+    public void set(@AlarmType int type, long triggerAtMillis, @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, operation, null, null,
                 (Handler) null, null, null);
     }
@@ -480,8 +480,8 @@
      * @param targetHandler {@link Handler} on which to execute the listener's onAlarm()
      *         callback, or {@code null} to run that callback on the main looper.
      */
-    public void set(@AlarmType int type, long triggerAtMillis, String tag, OnAlarmListener listener,
-            Handler targetHandler) {
+    public void set(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+            @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
         setImpl(type, triggerAtMillis, legacyExactLength(), 0, 0, null, listener, tag,
                 targetHandler, null, null);
     }
@@ -546,7 +546,7 @@
      * @see Intent#EXTRA_ALARM_COUNT
      */
     public void setRepeating(@AlarmType int type, long triggerAtMillis,
-            long intervalMillis, PendingIntent operation) {
+            long intervalMillis, @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, legacyExactLength(), intervalMillis, 0, operation,
                 null, null, (Handler) null, null, null);
     }
@@ -602,7 +602,7 @@
      * @see #RTC_WAKEUP
      */
     public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
-            PendingIntent operation) {
+            @NonNull PendingIntent operation) {
         setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, operation,
                 null, null, (Handler) null, null, null);
     }
@@ -625,12 +625,62 @@
      * @see #setWindow(int, long, long, PendingIntent)
      */
     public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
-            String tag, OnAlarmListener listener, Handler targetHandler) {
+            @Nullable String tag, @NonNull OnAlarmListener listener,
+            @Nullable Handler targetHandler) {
         setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
                 targetHandler, null, null);
     }
 
     /**
+     * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}.  Rather
+     * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+     * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+     * <p>
+     * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+     * invoked via the specified target Executor.
+     *
+     * <p>
+     * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of
+     * less than 10 minutes. The system will try its best to accommodate smaller windows if the
+     * alarm is supposed to fire in the near future, but there are no guarantees and the app should
+     * expect any window smaller than 10 minutes to get elongated to 10 minutes.
+     *
+     * @see #setWindow(int, long, long, PendingIntent)
+     */
+    public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+            @Nullable String tag, @NonNull Executor executor, @NonNull OnAlarmListener listener) {
+        setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+                executor, null, null);
+    }
+
+    /**
+     * Direct callback version of {@link #setWindow(int, long, long, PendingIntent)}.  Rather
+     * than supplying a PendingIntent to be sent when the alarm time is reached, this variant
+     * supplies an {@link OnAlarmListener} instance that will be invoked at that time.
+     * <p>
+     * The OnAlarmListener {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+     * invoked via the specified target Executor.
+     *
+     * <p>
+     * Note: Starting with API {@link Build.VERSION_CODES#S}, apps should not pass in a window of
+     * less than 10 minutes. The system will try its best to accommodate smaller windows if the
+     * alarm is supposed to fire in the near future, but there are no guarantees and the app should
+     * expect any window smaller than 10 minutes to get elongated to 10 minutes.
+     *
+     * @see #setWindow(int, long, long, PendingIntent)
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+    public void setWindow(@AlarmType int type, long windowStartMillis, long windowLengthMillis,
+            @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource,
+            @NonNull OnAlarmListener listener) {
+        setImpl(type, windowStartMillis, windowLengthMillis, 0, 0, null, listener, tag,
+                executor, workSource, null);
+    }
+
+    /**
      * Schedule an alarm that is prioritized by the system while the device is in power saving modes
      * such as battery saver and device idle (doze).
      *
@@ -725,7 +775,8 @@
      * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
      */
     @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
-    public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {
+    public void setExact(@AlarmType int type, long triggerAtMillis,
+            @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, (Handler) null,
                 null, null);
     }
@@ -756,8 +807,8 @@
      * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
      */
     @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
-    public void setExact(@AlarmType int type, long triggerAtMillis, String tag,
-            OnAlarmListener listener, Handler targetHandler) {
+    public void setExact(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+            @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
         setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, null, listener, tag,
                 targetHandler, null, null);
     }
@@ -767,8 +818,8 @@
      * the given time.
      * @hide
      */
-    public void setIdleUntil(@AlarmType int type, long triggerAtMillis, String tag,
-            OnAlarmListener listener, Handler targetHandler) {
+    public void setIdleUntil(@AlarmType int type, long triggerAtMillis, @Nullable String tag,
+            @NonNull OnAlarmListener listener, @Nullable Handler targetHandler) {
         setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_IDLE_UNTIL, null,
                 listener, tag, targetHandler, null, null);
     }
@@ -828,7 +879,7 @@
      * @see Manifest.permission#SCHEDULE_EXACT_ALARM SCHEDULE_EXACT_ALARM
      */
     @RequiresPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)
-    public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+    public void setAlarmClock(@NonNull AlarmClockInfo info, @NonNull PendingIntent operation) {
         setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,
                 null, null, (Handler) null, null, info);
     }
@@ -837,7 +888,8 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
     public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
-            long intervalMillis, PendingIntent operation, WorkSource workSource) {
+            long intervalMillis, @NonNull PendingIntent operation,
+            @Nullable WorkSource workSource) {
         setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, operation, null, null,
                 (Handler) null, workSource, null);
     }
@@ -854,8 +906,8 @@
      */
     @UnsupportedAppUsage
     public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
-            long intervalMillis, String tag, OnAlarmListener listener, Handler targetHandler,
-            WorkSource workSource) {
+            long intervalMillis, @Nullable String tag, @NonNull OnAlarmListener listener,
+            @Nullable Handler targetHandler, @Nullable WorkSource workSource) {
         setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag,
                 targetHandler, workSource, null);
     }
@@ -873,8 +925,8 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
     public void set(@AlarmType int type, long triggerAtMillis, long windowMillis,
-            long intervalMillis, OnAlarmListener listener, Handler targetHandler,
-            WorkSource workSource) {
+            long intervalMillis, @NonNull OnAlarmListener listener, @Nullable Handler targetHandler,
+            @Nullable WorkSource workSource) {
         setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, null,
                 targetHandler, workSource, null);
     }
@@ -1072,7 +1124,7 @@
      * @see Intent#EXTRA_ALARM_COUNT
      */
     public void setInexactRepeating(@AlarmType int type, long triggerAtMillis,
-            long intervalMillis, PendingIntent operation) {
+            long intervalMillis, @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, intervalMillis, 0, operation, null,
                 null, (Handler) null, null, null);
     }
@@ -1122,7 +1174,7 @@
      * @see #RTC_WAKEUP
      */
     public void setAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
-            PendingIntent operation) {
+            @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, WINDOW_HEURISTIC, 0, FLAG_ALLOW_WHILE_IDLE,
                 operation, null, null, (Handler) null, null, null);
     }
@@ -1195,12 +1247,46 @@
      */
     @RequiresPermission(value = Manifest.permission.SCHEDULE_EXACT_ALARM, conditional = true)
     public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
-            PendingIntent operation) {
+            @NonNull PendingIntent operation) {
         setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,
                 null, null, (Handler) null, null, null);
     }
 
     /**
+     * Like {@link #setExact(int, long, String, Executor, WorkSource, OnAlarmListener)}, but this
+     * alarm will be allowed to execute even when the system is in low-power idle modes.
+     *
+     * <p> See {@link #setExactAndAllowWhileIdle(int, long, PendingIntent)} for more details.
+     *
+     * @param type            type of alarm
+     * @param triggerAtMillis The exact time in milliseconds, that the alarm should be delivered,
+     *                        expressed in the appropriate clock's units (depending on the alarm
+     *                        type).
+     * @param listener        {@link OnAlarmListener} instance whose
+     *                        {@link OnAlarmListener#onAlarm() onAlarm()} method will be called when
+     *                        the alarm time is reached.
+     * @param executor        The {@link Executor} on which to execute the listener's onAlarm()
+     *                        callback.
+     * @param tag             Optional. A string tag used to identify this alarm in logs and
+     *                        battery-attribution.
+     * @param workSource      A {@link WorkSource} object to attribute this alarm to the app that
+     *                        requested this work.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {
+            Manifest.permission.UPDATE_DEVICE_STATS,
+            Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional = true)
+    public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,
+            @Nullable String tag, @NonNull Executor executor, @Nullable WorkSource workSource,
+            @NonNull OnAlarmListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, null, listener, tag,
+                executor, workSource, null);
+    }
+
+    /**
      * Remove any alarms with a matching {@link Intent}.
      * Any alarm, of any type, whose Intent matches this one (as defined by
      * {@link Intent#filterEquals}), will be canceled.
@@ -1210,7 +1296,7 @@
      *
      * @see #set
      */
-    public void cancel(PendingIntent operation) {
+    public void cancel(@NonNull PendingIntent operation) {
         if (operation == null) {
             final String msg = "cancel() called with a null PendingIntent";
             if (mTargetSdkVersion >= Build.VERSION_CODES.N) {
@@ -1233,7 +1319,7 @@
      *
      * @param listener OnAlarmListener instance that is the target of a currently-set alarm.
      */
-    public void cancel(OnAlarmListener listener) {
+    public void cancel(@NonNull OnAlarmListener listener) {
         if (listener == null) {
             throw new NullPointerException("cancel() called with a null OnAlarmListener");
         }
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index d281da0..a3390b7 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -30,6 +30,25 @@
  */
 interface IJobCallback {
     /**
+     * Immediate callback to the system after sending a data transfer download progress request
+     * signal; used to quickly detect ANR.
+     *
+     * @param jobId Unique integer used to identify this job.
+     * @param workId Unique integer used to identify a specific work item.
+     * @param transferredBytes How much data has been downloaded, in bytes.
+     */
+    void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+            long transferredBytes);
+    /**
+     * Immediate callback to the system after sending a data transfer upload progress request
+     * signal; used to quickly detect ANR.
+     *
+     * @param jobId Unique integer used to identify this job.
+     * @param workId Unique integer used to identify a specific work item.
+     * @param transferredBytes How much data has been uploaded, in bytes.
+     */
+    void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId, long transferredBytes);
+    /**
      * Immediate callback to the system after sending a start signal, used to quickly detect ANR.
      *
      * @param jobId Unique integer used to identify this job.
@@ -65,4 +84,24 @@
      */
     @UnsupportedAppUsage
     void jobFinished(int jobId, boolean reschedule);
+    /*
+     * Inform JobScheduler of a change in the estimated transfer payload.
+     *
+     * @param jobId Unique integer used to identify this job.
+     * @param item The particular JobWorkItem this progress is associated with, if any.
+     * @param downloadBytes How many bytes the app expects to download.
+     * @param uploadBytes How many bytes the app expects to upload.
+     */
+    void updateEstimatedNetworkBytes(int jobId, in JobWorkItem item,
+            long downloadBytes, long uploadBytes);
+    /*
+     * Update JobScheduler of how much data the job has successfully transferred.
+     *
+     * @param jobId Unique integer used to identify this job.
+     * @param item The particular JobWorkItem this progress is associated with, if any.
+     * @param transferredDownloadBytes The number of bytes that have successfully been downloaded.
+     * @param transferredUploadBytes The number of bytes that have successfully been uploaded.
+     */
+    void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
+            long transferredDownloadBytes, long transferredUploadBytes);
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
index 22ad252..2bb82bd 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobService.aidl
@@ -17,6 +17,7 @@
 package android.app.job;
 
 import android.app.job.JobParameters;
+import android.app.job.JobWorkItem;
 
 /**
  * Interface that the framework uses to communicate with application code that implements a
@@ -31,4 +32,8 @@
     /** Stop execution of application's job. */
     @UnsupportedAppUsage
     void stopJob(in JobParameters jobParams);
+    /** Update JS of how much data has been downloaded. */
+    void getTransferredDownloadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
+    /** Update JS of how much data has been uploaded. */
+    void getTransferredUploadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index ab0ac5a..4c849fe 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -32,6 +32,7 @@
 import android.annotation.RequiresPermission;
 import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
@@ -97,6 +98,15 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public static final long THROW_ON_INVALID_PRIORITY_VALUE = 140852299L;
 
+    /**
+     * Require that estimated network bytes are nonnegative.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public static final long REJECT_NEGATIVE_NETWORK_ESTIMATES = 253665015L;
+
     /** @hide */
     @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
             NETWORK_TYPE_NONE,
@@ -1890,11 +1900,13 @@
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
-            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS));
+            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS),
+                    Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES));
         }
 
         /** @hide */
-        public JobInfo build(boolean disallowPrefetchDeadlines) {
+        public JobInfo build(boolean disallowPrefetchDeadlines,
+                boolean rejectNegativeNetworkEstimates) {
             // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy
             // check that would ideally be phased out instead.
             if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
@@ -1903,7 +1915,7 @@
                         " setRequiresDeviceIdle is an error.");
             }
             JobInfo jobInfo = new JobInfo(this);
-            jobInfo.enforceValidity(disallowPrefetchDeadlines);
+            jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates);
             return jobInfo;
         }
 
@@ -1921,13 +1933,24 @@
     /**
      * @hide
      */
-    public final void enforceValidity(boolean disallowPrefetchDeadlines) {
+    public final void enforceValidity(boolean disallowPrefetchDeadlines,
+            boolean rejectNegativeNetworkEstimates) {
         // Check that network estimates require network type and are reasonable values.
         if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
                 && networkRequest == null) {
             throw new IllegalArgumentException(
                     "Can't provide estimated network usage without requiring a network");
         }
+        if (networkRequest != null && rejectNegativeNetworkEstimates) {
+            if (networkUploadBytes != NETWORK_BYTES_UNKNOWN && networkUploadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network upload bytes: " + networkUploadBytes);
+            }
+            if (networkDownloadBytes != NETWORK_BYTES_UNKNOWN && networkDownloadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network download bytes: " + networkDownloadBytes);
+            }
+        }
         final long estimatedTransfer;
         if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
             estimatedTransfer = networkDownloadBytes;
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index dfdb290..7448686 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -22,8 +22,11 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.ClipData;
 import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.PersistableBundle;
 
@@ -93,6 +96,16 @@
  */
 @SystemService(Context.JOB_SCHEDULER_SERVICE)
 public abstract class JobScheduler {
+    /**
+     * Whether to throw an exception when an app doesn't properly implement all the necessary
+     * data transfer APIs.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public static final long THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION = 255371817L;
+
     /** @hide */
     @IntDef(prefix = { "RESULT_" }, value = {
             RESULT_FAILURE,
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index d184d44..dabf728 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -16,7 +16,13 @@
 
 package android.app.job;
 
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Service;
+import android.compat.Compatibility;
 import android.content.Intent;
 import android.os.IBinder;
 
@@ -72,6 +78,28 @@
                 public boolean onStopJob(JobParameters params) {
                     return JobService.this.onStopJob(params);
                 }
+
+                @Override
+                @BytesLong
+                public long getTransferredDownloadBytes(@NonNull JobParameters params,
+                        @Nullable JobWorkItem item) {
+                    if (item == null) {
+                        return JobService.this.getTransferredDownloadBytes();
+                    } else {
+                        return JobService.this.getTransferredDownloadBytes(item);
+                    }
+                }
+
+                @Override
+                @BytesLong
+                public long getTransferredUploadBytes(@NonNull JobParameters params,
+                        @Nullable JobWorkItem item) {
+                    if (item == null) {
+                        return JobService.this.getTransferredUploadBytes();
+                    } else {
+                        return JobService.this.getTransferredUploadBytes(item);
+                    }
+                }
             };
         }
         return mEngine.getBinder();
@@ -171,4 +199,161 @@
      * to end the job entirely.  Regardless of the value returned, your job must stop executing.
      */
     public abstract boolean onStopJob(JobParameters params);
+
+    /**
+     * Update how much data this job will transfer. This method can
+     * be called multiple times within the first 30 seconds after
+     * {@link #onStartJob(JobParameters)} has been called. Only
+     * one call will be heeded after that time has passed.
+     *
+     * This method (or an overload) must be called within the first
+     * 30 seconds for a data transfer job if a payload size estimate
+     * was not provided at the time of scheduling.
+     *
+     * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+     */
+    public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+            @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+        mEngine.updateEstimatedNetworkBytes(params, null, downloadBytes, uploadBytes);
+    }
+
+    /**
+     * Update how much data will transfer for the JobWorkItem. This
+     * method can be called multiple times within the first 30 seconds
+     * after {@link #onStartJob(JobParameters)} has been called.
+     * Only one call will be heeded after that time has passed.
+     *
+     * This method (or an overload) must be called within the first
+     * 30 seconds for a data transfer job if a payload size estimate
+     * was not provided at the time of scheduling.
+     *
+     * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+     */
+    public final void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+            @NonNull JobWorkItem jobWorkItem,
+            @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+        mEngine.updateEstimatedNetworkBytes(params, jobWorkItem, downloadBytes, uploadBytes);
+    }
+
+    /**
+     * Tell JobScheduler how much data has successfully been transferred for the data transfer job.
+     */
+    public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+            @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+        mEngine.updateTransferredNetworkBytes(params, null,
+                transferredDownloadBytes, transferredUploadBytes);
+    }
+
+    /**
+     * Tell JobScheduler how much data has been transferred for the data transfer
+     * {@link JobWorkItem}.
+     */
+    public final void updateTransferredNetworkBytes(@NonNull JobParameters params,
+            @NonNull JobWorkItem item,
+            @BytesLong long transferredDownloadBytes, @BytesLong long transferredUploadBytes) {
+        mEngine.updateTransferredNetworkBytes(params, item,
+                transferredDownloadBytes, transferredUploadBytes);
+    }
+
+    /**
+     * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+     * will call this if the job has specified positive estimated download bytes and
+     * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+     * hasn't been called recently.
+     *
+     * <p>
+     * This must be implemented for all data transfer jobs.
+     *
+     * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+     * @see JobInfo#NETWORK_BYTES_UNKNOWN
+     */
+    // TODO(255371817): specify the actual time JS will wait for progress before requesting
+    @BytesLong
+    public long getTransferredDownloadBytes() {
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            // Regular jobs don't have to implement this and JobScheduler won't call this API for
+            // non-data transfer jobs.
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
+
+    /**
+     * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+     * will call this if the job has specified positive estimated upload bytes and
+     * {@link #updateTransferredNetworkBytes(JobParameters, long, long)}
+     * hasn't been called recently.
+     *
+     * <p>
+     * This must be implemented for all data transfer jobs.
+     *
+     * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long)
+     * @see JobInfo#NETWORK_BYTES_UNKNOWN
+     */
+    // TODO(255371817): specify the actual time JS will wait for progress before requesting
+    @BytesLong
+    public long getTransferredUploadBytes() {
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            // Regular jobs don't have to implement this and JobScheduler won't call this API for
+            // non-data transfer jobs.
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
+
+    /**
+     * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+     * will call this if the job has specified positive estimated download bytes and
+     * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+     * hasn't been called recently and the job has
+     * {@link JobWorkItem JobWorkItems} that have been
+     * {@link JobParameters#dequeueWork dequeued} but not
+     * {@link JobParameters#completeWork(JobWorkItem) completed}.
+     *
+     * <p>
+     * This must be implemented for all data transfer jobs.
+     *
+     * @see JobInfo#NETWORK_BYTES_UNKNOWN
+     */
+    // TODO(255371817): specify the actual time JS will wait for progress before requesting
+    @BytesLong
+    public long getTransferredDownloadBytes(@NonNull JobWorkItem item) {
+        if (item == null) {
+            return getTransferredDownloadBytes();
+        }
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            // Regular jobs don't have to implement this and JobScheduler won't call this API for
+            // non-data transfer jobs.
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
+
+    /**
+     * Get the number of bytes the app has successfully downloaded for this job. JobScheduler
+     * will call this if the job has specified positive estimated upload bytes and
+     * {@link #updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)}
+     * hasn't been called recently and the job has
+     * {@link JobWorkItem JobWorkItems} that have been
+     * {@link JobParameters#dequeueWork dequeued} but not
+     * {@link JobParameters#completeWork(JobWorkItem) completed}.
+     *
+     * <p>
+     * This must be implemented for all data transfer jobs.
+     *
+     * @see JobInfo#NETWORK_BYTES_UNKNOWN
+     */
+    // TODO(255371817): specify the actual time JS will wait for progress before requesting
+    @BytesLong
+    public long getTransferredUploadBytes(@NonNull JobWorkItem item) {
+        if (item == null) {
+            return getTransferredUploadBytes();
+        }
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            // Regular jobs don't have to implement this and JobScheduler won't call this API for
+            // non-data transfer jobs.
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
 }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 3d43d20..6c4b686 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -16,7 +16,13 @@
 
 package android.app.job;
 
+import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Service;
+import android.compat.Compatibility;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.IBinder;
@@ -25,6 +31,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.os.SomeArgs;
+
 import java.lang.ref.WeakReference;
 
 /**
@@ -51,6 +59,20 @@
      * Message that the client has completed execution of this job.
      */
     private static final int MSG_JOB_FINISHED = 2;
+    /**
+     * Message that will result in a call to
+     * {@link #getTransferredDownloadBytes(JobParameters, JobWorkItem)}.
+     */
+    private static final int MSG_GET_TRANSFERRED_DOWNLOAD_BYTES = 3;
+    /**
+     * Message that will result in a call to
+     * {@link #getTransferredUploadBytes(JobParameters, JobWorkItem)}.
+     */
+    private static final int MSG_GET_TRANSFERRED_UPLOAD_BYTES = 4;
+    /** Message that the client wants to update JobScheduler of the data transfer progress. */
+    private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
+    /** Message that the client wants to update JobScheduler of the estimated transfer size. */
+    private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
 
     private final IJobService mBinder;
 
@@ -68,6 +90,32 @@
         }
 
         @Override
+        public void getTransferredDownloadBytes(@NonNull JobParameters jobParams,
+                @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+            JobServiceEngine service = mService.get();
+            if (service != null) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = jobParams;
+                args.arg2 = jobWorkItem;
+                service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_DOWNLOAD_BYTES, args)
+                        .sendToTarget();
+            }
+        }
+
+        @Override
+        public void getTransferredUploadBytes(@NonNull JobParameters jobParams,
+                @Nullable JobWorkItem jobWorkItem) throws RemoteException {
+            JobServiceEngine service = mService.get();
+            if (service != null) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = jobParams;
+                args.arg2 = jobWorkItem;
+                service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_UPLOAD_BYTES, args)
+                        .sendToTarget();
+            }
+        }
+
+        @Override
         public void startJob(JobParameters jobParams) throws RemoteException {
             JobServiceEngine service = mService.get();
             if (service != null) {
@@ -98,9 +146,9 @@
 
         @Override
         public void handleMessage(Message msg) {
-            final JobParameters params = (JobParameters) msg.obj;
             switch (msg.what) {
-                case MSG_EXECUTE_JOB:
+                case MSG_EXECUTE_JOB: {
+                    final JobParameters params = (JobParameters) msg.obj;
                     try {
                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
                         ackStartMessage(params, workOngoing);
@@ -109,7 +157,9 @@
                         throw new RuntimeException(e);
                     }
                     break;
-                case MSG_STOP_JOB:
+                }
+                case MSG_STOP_JOB: {
+                    final JobParameters params = (JobParameters) msg.obj;
                     try {
                         boolean ret = JobServiceEngine.this.onStopJob(params);
                         ackStopMessage(params, ret);
@@ -118,7 +168,9 @@
                         throw new RuntimeException(e);
                     }
                     break;
-                case MSG_JOB_FINISHED:
+                }
+                case MSG_JOB_FINISHED: {
+                    final JobParameters params = (JobParameters) msg.obj;
                     final boolean needsReschedule = (msg.arg2 == 1);
                     IJobCallback callback = params.getCallback();
                     if (callback != null) {
@@ -132,19 +184,117 @@
                         Log.e(TAG, "finishJob() called for a nonexistent job id.");
                     }
                     break;
+                }
+                case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final JobParameters params = (JobParameters) args.arg1;
+                    final JobWorkItem item = (JobWorkItem) args.arg2;
+                    try {
+                        long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
+                        ackGetTransferredDownloadBytesMessage(params, item, ret);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
+                        throw new RuntimeException(e);
+                    }
+                    args.recycle();
+                    break;
+                }
+                case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final JobParameters params = (JobParameters) args.arg1;
+                    final JobWorkItem item = (JobWorkItem) args.arg2;
+                    try {
+                        long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
+                        ackGetTransferredUploadBytesMessage(params, item, ret);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
+                        throw new RuntimeException(e);
+                    }
+                    args.recycle();
+                    break;
+                }
+                case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final JobParameters params = (JobParameters) args.arg1;
+                    IJobCallback callback = params.getCallback();
+                    if (callback != null) {
+                        try {
+                            callback.updateTransferredNetworkBytes(params.getJobId(),
+                                    (JobWorkItem) args.arg2, args.argl1, args.argl2);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error updating data transfer progress to system:"
+                                    + " binder has gone away.");
+                        }
+                    } else {
+                        Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
+                    }
+                    args.recycle();
+                    break;
+                }
+                case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final JobParameters params = (JobParameters) args.arg1;
+                    IJobCallback callback = params.getCallback();
+                    if (callback != null) {
+                        try {
+                            callback.updateEstimatedNetworkBytes(params.getJobId(),
+                                    (JobWorkItem) args.arg2, args.argl1, args.argl2);
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error updating estimated transfer size to system:"
+                                    + " binder has gone away.");
+                        }
+                    } else {
+                        Log.e(TAG,
+                                "updateEstimatedNetworkBytes() called for a nonexistent job id.");
+                    }
+                    args.recycle();
+                    break;
+                }
                 default:
                     Log.e(TAG, "Unrecognised message received.");
                     break;
             }
         }
 
+        private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
+                @Nullable JobWorkItem item, long progress) {
+            final IJobCallback callback = params.getCallback();
+            final int jobId = params.getJobId();
+            final int workId = item == null ? -1 : item.getWorkId();
+            if (callback != null) {
+                try {
+                    callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "System unreachable for returning progress.");
+                }
+            } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Attempting to ack a job that has already been processed.");
+            }
+        }
+
+        private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
+                @Nullable JobWorkItem item, long progress) {
+            final IJobCallback callback = params.getCallback();
+            final int jobId = params.getJobId();
+            final int workId = item == null ? -1 : item.getWorkId();
+            if (callback != null) {
+                try {
+                    callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "System unreachable for returning progress.");
+                }
+            } else if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Attempting to ack a job that has already been processed.");
+            }
+        }
+
         private void ackStartMessage(JobParameters params, boolean workOngoing) {
             final IJobCallback callback = params.getCallback();
             final int jobId = params.getJobId();
             if (callback != null) {
                 try {
                     callback.acknowledgeStartMessage(jobId, workOngoing);
-                } catch(RemoteException e) {
+                } catch (RemoteException e) {
                     Log.e(TAG, "System unreachable for starting job.");
                 }
             } else {
@@ -213,4 +363,69 @@
         m.arg2 = needsReschedule ? 1 : 0;
         m.sendToTarget();
     }
+
+    /**
+     * Engine's request to get how much data has been downloaded.
+     *
+     * @see JobService#getTransferredDownloadBytes()
+     */
+    @BytesLong
+    public long getTransferredDownloadBytes(@NonNull JobParameters params,
+            @Nullable JobWorkItem item) {
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
+
+    /**
+     * Engine's request to get how much data has been uploaded.
+     *
+     * @see JobService#getTransferredUploadBytes()
+     */
+    @BytesLong
+    public long getTransferredUploadBytes(@NonNull JobParameters params,
+            @Nullable JobWorkItem item) {
+        if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
+            throw new RuntimeException("Not implemented. Must override in a subclass.");
+        }
+        return 0;
+    }
+
+    /**
+     * Call in to engine to report data transfer progress.
+     *
+     * @see JobService#updateTransferredNetworkBytes(JobParameters, long, long)
+     */
+    public void updateTransferredNetworkBytes(@NonNull JobParameters params,
+            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+        if (params == null) {
+            throw new NullPointerException("params");
+        }
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = params;
+        args.arg2 = item;
+        args.argl1 = downloadBytes;
+        args.argl2 = uploadBytes;
+        mHandler.obtainMessage(MSG_UPDATE_TRANSFERRED_NETWORK_BYTES, args).sendToTarget();
+    }
+
+    /**
+     * Call in to engine to report data transfer progress.
+     *
+     * @see JobService#updateEstimatedNetworkBytes(JobParameters, JobWorkItem, long, long)
+     */
+    public void updateEstimatedNetworkBytes(@NonNull JobParameters params,
+            @NonNull JobWorkItem item,
+            @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
+        if (params == null) {
+            throw new NullPointerException("params");
+        }
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = params;
+        args.arg2 = item;
+        args.argl1 = downloadBytes;
+        args.argl2 = uploadBytes;
+        mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
+    }
 }
\ No newline at end of file
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
index 372f9fa..32945e0 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
@@ -20,6 +20,7 @@
 
 import android.annotation.BytesLong;
 import android.annotation.Nullable;
+import android.compat.Compatibility;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
 import android.os.Build;
@@ -88,25 +89,11 @@
      */
     public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes,
             @BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) {
-        if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && minimumChunkBytes <= 0) {
-            throw new IllegalArgumentException("Minimum chunk size must be positive");
-        }
-        final long estimatedTransfer;
-        if (uploadBytes == NETWORK_BYTES_UNKNOWN) {
-            estimatedTransfer = downloadBytes;
-        } else {
-            estimatedTransfer = uploadBytes
-                    + (downloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : downloadBytes);
-        }
-        if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && estimatedTransfer != NETWORK_BYTES_UNKNOWN
-                && minimumChunkBytes > estimatedTransfer) {
-            throw new IllegalArgumentException(
-                    "Minimum chunk size can't be greater than estimated network usage");
-        }
         mIntent = intent;
         mNetworkDownloadBytes = downloadBytes;
         mNetworkUploadBytes = uploadBytes;
         mMinimumChunkBytes = minimumChunkBytes;
+        enforceValidity(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
     }
 
     /**
@@ -222,7 +209,17 @@
     /**
      * @hide
      */
-    public void enforceValidity() {
+    public void enforceValidity(boolean rejectNegativeNetworkEstimates) {
+        if (rejectNegativeNetworkEstimates) {
+            if (mNetworkUploadBytes != NETWORK_BYTES_UNKNOWN && mNetworkUploadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network upload bytes: " + mNetworkUploadBytes);
+            }
+            if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN && mNetworkDownloadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network download bytes: " + mNetworkDownloadBytes);
+            }
+        }
         final long estimatedTransfer;
         if (mNetworkUploadBytes == NETWORK_BYTES_UNKNOWN) {
             estimatedTransfer = mNetworkDownloadBytes;
diff --git a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
index 299ad66..4a3a6d9 100644
--- a/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
+++ b/apex/jobscheduler/framework/java/android/app/tare/EconomyManager.java
@@ -113,7 +113,9 @@
     /** @hide */
     public static final String KEY_AM_INITIAL_CONSUMPTION_LIMIT = "am_initial_consumption_limit";
     /** @hide */
-    public static final String KEY_AM_HARD_CONSUMPTION_LIMIT = "am_hard_consumption_limit";
+    public static final String KEY_AM_MIN_CONSUMPTION_LIMIT = "am_minimum_consumption_limit";
+    /** @hide */
+    public static final String KEY_AM_MAX_CONSUMPTION_LIMIT = "am_maximum_consumption_limit";
     // TODO: Add AlarmManager modifier keys
     /** @hide */
     public static final String KEY_AM_REWARD_TOP_ACTIVITY_INSTANT =
@@ -242,7 +244,9 @@
     /** @hide */
     public static final String KEY_JS_INITIAL_CONSUMPTION_LIMIT = "js_initial_consumption_limit";
     /** @hide */
-    public static final String KEY_JS_HARD_CONSUMPTION_LIMIT = "js_hard_consumption_limit";
+    public static final String KEY_JS_MIN_CONSUMPTION_LIMIT = "js_minimum_consumption_limit";
+    /** @hide */
+    public static final String KEY_JS_MAX_CONSUMPTION_LIMIT = "js_maximum_consumption_limit";
     // TODO: Add JobScheduler modifier keys
     /** @hide */
     public static final String KEY_JS_REWARD_APP_INSTALL_INSTANT =
@@ -371,7 +375,9 @@
     /** @hide */
     public static final long DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(2880);
     /** @hide */
-    public static final long DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000);
+    public static final long DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(1440);
+    /** @hide */
+    public static final long DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(15_000);
     // TODO: add AlarmManager modifier default values
     /** @hide */
     public static final long DEFAULT_AM_REWARD_TOP_ACTIVITY_INSTANT_CAKES = arcToCake(0);
@@ -478,8 +484,10 @@
     /** @hide */
     public static final long DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES = arcToCake(29_000);
     /** @hide */
-    // TODO: set hard limit based on device type (phone vs tablet vs etc) + battery size
-    public static final long DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000);
+    public static final long DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES = arcToCake(17_000);
+    /** @hide */
+    // TODO: set maximum limit based on device type (phone vs tablet vs etc) + battery size
+    public static final long DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES = arcToCake(250_000);
     // TODO: add JobScheduler modifier default values
     /** @hide */
     public static final long DEFAULT_JS_REWARD_APP_INSTALL_INSTANT_CAKES = arcToCake(408);
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index bd475e9..e3bd5ac 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -31,6 +31,7 @@
 import android.app.AlarmManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
@@ -740,8 +741,10 @@
         }
     };
 
-    private final BroadcastReceiver mIdleStartedDoneReceiver = new BroadcastReceiver() {
-        @Override public void onReceive(Context context, Intent intent) {
+    private final IIntentReceiver mIdleStartedDoneReceiver = new IIntentReceiver.Stub() {
+        @Override
+        public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+                boolean ordered, boolean sticky, int sendingUser) {
             // When coming out of a deep idle, we will add in some delay before we allow
             // the system to settle down and finish the maintenance window.  This is
             // to give a chance for any pending work to be scheduled.
@@ -1816,13 +1819,15 @@
                     }
                     if (deepChanged) {
                         incActiveIdleOps();
-                        getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL,
-                                null, mIdleStartedDoneReceiver, null, 0, null, null);
+                        mLocalActivityManager.broadcastIntentWithCallback(mIdleIntent,
+                                mIdleStartedDoneReceiver, null, UserHandle.USER_ALL,
+                                null, null, null);
                     }
                     if (lightChanged) {
                         incActiveIdleOps();
-                        getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL,
-                                null, mIdleStartedDoneReceiver, null, 0, null, null);
+                        mLocalActivityManager.broadcastIntentWithCallback(mLightIdleIntent,
+                                mIdleStartedDoneReceiver, null, UserHandle.USER_ALL,
+                                null, null, null);
                     }
                     // Always start with one active op for the message being sent here.
                     // Now we are done!
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index bedfa7f..29e730d 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -84,6 +84,7 @@
 import android.content.PermissionChecker;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserPackage;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.BatteryManager;
@@ -120,7 +121,6 @@
 import android.util.IntArray;
 import android.util.Log;
 import android.util.LongArrayQueue;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
@@ -402,7 +402,7 @@
             public long lastUsage;
         }
         /** Map of {package, user} -> {quotaInfo} */
-        private final ArrayMap<Pair<String, Integer>, QuotaInfo> mQuotaBuffer = new ArrayMap<>();
+        private final ArrayMap<UserPackage, QuotaInfo> mQuotaBuffer = new ArrayMap<>();
 
         private long mMaxDuration;
 
@@ -414,11 +414,11 @@
             if (quota <= 0) {
                 return;
             }
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            QuotaInfo currentQuotaInfo = mQuotaBuffer.get(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            QuotaInfo currentQuotaInfo = mQuotaBuffer.get(userPackage);
             if (currentQuotaInfo == null) {
                 currentQuotaInfo = new QuotaInfo();
-                mQuotaBuffer.put(packageUser, currentQuotaInfo);
+                mQuotaBuffer.put(userPackage, currentQuotaInfo);
             }
             currentQuotaInfo.remainingQuota = quota;
             currentQuotaInfo.expirationTime = nowElapsed + mMaxDuration;
@@ -426,8 +426,8 @@
 
         /** Returns if the supplied package has reserve quota to fire at the given time. */
         boolean hasQuota(String packageName, int userId, long triggerElapsed) {
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            final QuotaInfo quotaInfo = mQuotaBuffer.get(userPackage);
 
             return quotaInfo != null && quotaInfo.remainingQuota > 0
                     && triggerElapsed <= quotaInfo.expirationTime;
@@ -438,8 +438,8 @@
          * required.
          */
         void recordUsage(String packageName, int userId, long nowElapsed) {
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            final QuotaInfo quotaInfo = mQuotaBuffer.get(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            final QuotaInfo quotaInfo = mQuotaBuffer.get(userPackage);
 
             if (quotaInfo == null) {
                 Slog.wtf(TAG, "Temporary quota being consumed at " + nowElapsed
@@ -479,26 +479,26 @@
 
         void removeForUser(int userId) {
             for (int i = mQuotaBuffer.size() - 1; i >= 0; i--) {
-                final Pair<String, Integer> packageUserKey = mQuotaBuffer.keyAt(i);
-                if (packageUserKey.second == userId) {
+                final UserPackage userPackageKey = mQuotaBuffer.keyAt(i);
+                if (userPackageKey.userId == userId) {
                     mQuotaBuffer.removeAt(i);
                 }
             }
         }
 
         void removeForPackage(String packageName, int userId) {
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            mQuotaBuffer.remove(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            mQuotaBuffer.remove(userPackage);
         }
 
         void dump(IndentingPrintWriter pw, long nowElapsed) {
             pw.increaseIndent();
             for (int i = 0; i < mQuotaBuffer.size(); i++) {
-                final Pair<String, Integer> packageUser = mQuotaBuffer.keyAt(i);
+                final UserPackage userPackage = mQuotaBuffer.keyAt(i);
                 final QuotaInfo quotaInfo = mQuotaBuffer.valueAt(i);
-                pw.print(packageUser.first);
+                pw.print(userPackage.packageName);
                 pw.print(", u");
-                pw.print(packageUser.second);
+                pw.print(userPackage.userId);
                 pw.print(": ");
                 if (quotaInfo == null) {
                     pw.print("--");
@@ -522,8 +522,7 @@
      */
     @VisibleForTesting
     static class AppWakeupHistory {
-        private ArrayMap<Pair<String, Integer>, LongArrayQueue> mPackageHistory =
-                new ArrayMap<>();
+        private final ArrayMap<UserPackage, LongArrayQueue> mPackageHistory = new ArrayMap<>();
         private long mWindowSize;
 
         AppWakeupHistory(long windowSize) {
@@ -531,11 +530,11 @@
         }
 
         void recordAlarmForPackage(String packageName, int userId, long nowElapsed) {
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            LongArrayQueue history = mPackageHistory.get(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            LongArrayQueue history = mPackageHistory.get(userPackage);
             if (history == null) {
                 history = new LongArrayQueue();
-                mPackageHistory.put(packageUser, history);
+                mPackageHistory.put(userPackage, history);
             }
             if (history.size() == 0 || history.peekLast() < nowElapsed) {
                 history.addLast(nowElapsed);
@@ -545,16 +544,16 @@
 
         void removeForUser(int userId) {
             for (int i = mPackageHistory.size() - 1; i >= 0; i--) {
-                final Pair<String, Integer> packageUserKey = mPackageHistory.keyAt(i);
-                if (packageUserKey.second == userId) {
+                final UserPackage userPackageKey = mPackageHistory.keyAt(i);
+                if (userPackageKey.userId == userId) {
                     mPackageHistory.removeAt(i);
                 }
             }
         }
 
         void removeForPackage(String packageName, int userId) {
-            final Pair<String, Integer> packageUser = Pair.create(packageName, userId);
-            mPackageHistory.remove(packageUser);
+            final UserPackage userPackage = UserPackage.of(userId, packageName);
+            mPackageHistory.remove(userPackage);
         }
 
         private void snapToWindow(LongArrayQueue history) {
@@ -564,7 +563,7 @@
         }
 
         int getTotalWakeupsInWindow(String packageName, int userId) {
-            final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+            final LongArrayQueue history = mPackageHistory.get(UserPackage.of(userId, packageName));
             return (history == null) ? 0 : history.size();
         }
 
@@ -573,7 +572,7 @@
          *          (1=1st-last=the ultimate wakeup and 2=2nd-last=the penultimate wakeup)
          */
         long getNthLastWakeupForPackage(String packageName, int userId, int n) {
-            final LongArrayQueue history = mPackageHistory.get(Pair.create(packageName, userId));
+            final LongArrayQueue history = mPackageHistory.get(UserPackage.of(userId, packageName));
             if (history == null) {
                 return 0;
             }
@@ -584,11 +583,11 @@
         void dump(IndentingPrintWriter pw, long nowElapsed) {
             pw.increaseIndent();
             for (int i = 0; i < mPackageHistory.size(); i++) {
-                final Pair<String, Integer> packageUser = mPackageHistory.keyAt(i);
+                final UserPackage userPackage = mPackageHistory.keyAt(i);
                 final LongArrayQueue timestamps = mPackageHistory.valueAt(i);
-                pw.print(packageUser.first);
+                pw.print(userPackage.packageName);
                 pw.print(", u");
-                pw.print(packageUser.second);
+                pw.print(userPackage.userId);
                 pw.print(": ");
                 // limit dumping to a max of 100 values
                 final int lastIdx = Math.max(0, timestamps.size() - 100);
@@ -1501,13 +1500,13 @@
      *                       null indicates all
      * @return True if there was any reordering done to the current list.
      */
-    boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<Pair<String, Integer>> targetPackages) {
+    boolean reorderAlarmsBasedOnStandbyBuckets(ArraySet<UserPackage> targetPackages) {
         final long start = mStatLogger.getTime();
 
         final boolean changed = mAlarmStore.updateAlarmDeliveries(a -> {
-            final Pair<String, Integer> packageUser =
-                    Pair.create(a.sourcePackage, UserHandle.getUserId(a.creatorUid));
-            if (targetPackages != null && !targetPackages.contains(packageUser)) {
+            final UserPackage userPackage =
+                    UserPackage.of(UserHandle.getUserId(a.creatorUid), a.sourcePackage);
+            if (targetPackages != null && !targetPackages.contains(userPackage)) {
                 return false;
             }
             return adjustDeliveryTimeBasedOnBucketLocked(a);
@@ -1524,13 +1523,13 @@
      *                       null indicates all
      * @return True if there was any reordering done to the current list.
      */
-    boolean reorderAlarmsBasedOnTare(ArraySet<Pair<String, Integer>> targetPackages) {
+    boolean reorderAlarmsBasedOnTare(ArraySet<UserPackage> targetPackages) {
         final long start = mStatLogger.getTime();
 
         final boolean changed = mAlarmStore.updateAlarmDeliveries(a -> {
-            final Pair<String, Integer> packageUser =
-                    Pair.create(a.sourcePackage, UserHandle.getUserId(a.creatorUid));
-            if (targetPackages != null && !targetPackages.contains(packageUser)) {
+            final UserPackage userPackage =
+                    UserPackage.of(UserHandle.getUserId(a.creatorUid), a.sourcePackage);
+            if (targetPackages != null && !targetPackages.contains(userPackage)) {
                 return false;
             }
             return adjustDeliveryTimeBasedOnTareLocked(a);
@@ -4786,8 +4785,7 @@
                                     }
                                 }
                             }
-                            final ArraySet<Pair<String, Integer>> triggerPackages =
-                                    new ArraySet<>();
+                            final ArraySet<UserPackage> triggerPackages = new ArraySet<>();
                             final IntArray wakeupUids = new IntArray();
                             for (int i = 0; i < triggerList.size(); i++) {
                                 final Alarm a = triggerList.get(i);
@@ -4796,13 +4794,13 @@
                                 }
                                 if (mConstants.USE_TARE_POLICY) {
                                     if (!isExemptFromTare(a)) {
-                                        triggerPackages.add(Pair.create(
-                                                a.sourcePackage,
-                                                UserHandle.getUserId(a.creatorUid)));
+                                        triggerPackages.add(UserPackage.of(
+                                                UserHandle.getUserId(a.creatorUid),
+                                                a.sourcePackage));
                                     }
                                 } else if (!isExemptFromAppStandby(a)) {
-                                    triggerPackages.add(Pair.create(
-                                            a.sourcePackage, UserHandle.getUserId(a.creatorUid)));
+                                    triggerPackages.add(UserPackage.of(
+                                            UserHandle.getUserId(a.creatorUid), a.sourcePackage));
                                 }
                             }
                             if (wakeupUids.size() > 0 && mBatteryStatsInternal != null) {
@@ -4990,8 +4988,8 @@
                 case TEMPORARY_QUOTA_CHANGED:
                 case APP_STANDBY_BUCKET_CHANGED:
                     synchronized (mLock) {
-                        final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
-                        filterPackages.add(Pair.create((String) msg.obj, msg.arg1));
+                        final ArraySet<UserPackage> filterPackages = new ArraySet<>();
+                        filterPackages.add(UserPackage.of(msg.arg1, (String) msg.obj));
                         if (reorderAlarmsBasedOnStandbyBuckets(filterPackages)) {
                             rescheduleKernelAlarmsLocked();
                             updateNextAlarmClockLocked();
@@ -5004,8 +5002,8 @@
                         final int userId = msg.arg1;
                         final String packageName = (String) msg.obj;
 
-                        final ArraySet<Pair<String, Integer>> filterPackages = new ArraySet<>();
-                        filterPackages.add(Pair.create(packageName, userId));
+                        final ArraySet<UserPackage> filterPackages = new ArraySet<>();
+                        filterPackages.add(UserPackage.of(userId, packageName));
                         if (reorderAlarmsBasedOnTare(filterPackages)) {
                             rescheduleKernelAlarmsLocked();
                             updateNextAlarmClockLocked();
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..f9dd0b3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.job;
 
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
@@ -99,6 +101,18 @@
     static final String KEY_PKG_CONCURRENCY_LIMIT_REGULAR =
             CONFIG_KEY_PREFIX_CONCURRENCY + "pkg_concurrency_limit_regular";
     private static final int DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR = STANDARD_CONCURRENCY_LIMIT / 2;
+    @VisibleForTesting
+    static final String KEY_ENABLE_MAX_WAIT_TIME_BYPASS =
+            CONFIG_KEY_PREFIX_CONCURRENCY + "enable_max_wait_time_bypass";
+    private static final boolean DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS = true;
+    private static final String KEY_MAX_WAIT_EJ_MS =
+            CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_ej_ms";
+    @VisibleForTesting
+    static final long DEFAULT_MAX_WAIT_EJ_MS = 5 * MINUTE_IN_MILLIS;
+    private static final String KEY_MAX_WAIT_REGULAR_MS =
+            CONFIG_KEY_PREFIX_CONCURRENCY + "max_wait_regular_ms";
+    @VisibleForTesting
+    static final long DEFAULT_MAX_WAIT_REGULAR_MS = 30 * MINUTE_IN_MILLIS;
 
     /**
      * Set of possible execution types that a job can have. The actual type(s) of a job are based
@@ -313,6 +327,7 @@
     private final ArraySet<ContextAssignment> mRecycledIdle = new ArraySet<>();
     private final ArrayList<ContextAssignment> mRecycledPreferredUidOnly = new ArrayList<>();
     private final ArrayList<ContextAssignment> mRecycledStoppable = new ArrayList<>();
+    private final AssignmentInfo mRecycledAssignmentInfo = new AssignmentInfo();
 
     private final Pools.Pool<ContextAssignment> mContextAssignmentPool =
             new Pools.SimplePool<>(MAX_RETAINED_OBJECTS);
@@ -353,6 +368,20 @@
      */
     private int mPkgConcurrencyLimitRegular = DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR;
 
+    private boolean mMaxWaitTimeBypassEnabled = DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS;
+
+    /**
+     * The maximum time an expedited job would have to be potentially waiting for an available
+     * slot before we would consider creating a new slot for it.
+     */
+    private long mMaxWaitEjMs = DEFAULT_MAX_WAIT_EJ_MS;
+
+    /**
+     * The maximum time a regular job would have to be potentially waiting for an available
+     * slot before we would consider creating a new slot for it.
+     */
+    private long mMaxWaitRegularMs = DEFAULT_MAX_WAIT_REGULAR_MS;
+
     /** Current memory trim level. */
     private int mLastMemoryTrimLevel;
 
@@ -386,7 +415,7 @@
     @VisibleForTesting
     JobConcurrencyManager(JobSchedulerService service, Injector injector) {
         mService = service;
-        mLock = mService.mLock;
+        mLock = mService.getLock();
         mContext = service.getTestableContext();
         mInjector = injector;
 
@@ -460,14 +489,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 +584,7 @@
      * execution guarantee.
      */
     @GuardedBy("mLock")
-    boolean isJobLongRunningLocked(@NonNull JobStatus job) {
+    boolean isJobInOvertimeLocked(@NonNull JobStatus job) {
         if (!mRunningJobs.contains(job)) {
             return false;
         }
@@ -666,7 +695,8 @@
         }
 
         prepareForAssignmentDeterminationLocked(
-                mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+                mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+                mRecycledAssignmentInfo);
 
         if (DEBUG) {
             Slog.d(TAG, printAssignments("running jobs initial",
@@ -674,7 +704,8 @@
         }
 
         determineAssignmentsLocked(
-                mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+                mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+                mRecycledAssignmentInfo);
 
         if (DEBUG) {
             Slog.d(TAG, printAssignments("running jobs final",
@@ -686,7 +717,8 @@
         carryOutAssignmentChangesLocked(mRecycledChanged);
 
         cleanUpAfterAssignmentChangesLocked(
-                mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
+                mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable,
+                mRecycledAssignmentInfo);
 
         noteConcurrency();
     }
@@ -695,7 +727,8 @@
     @GuardedBy("mLock")
     void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
             final List<ContextAssignment> preferredUidOnly,
-            final List<ContextAssignment> stoppable) {
+            final List<ContextAssignment> stoppable,
+            final AssignmentInfo info) {
         final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
         final List<JobServiceContext> activeServices = mActiveServices;
 
@@ -709,6 +742,8 @@
         updateNonRunningPrioritiesLocked(pendingJobQueue, true);
 
         final int numRunningJobs = activeServices.size();
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+        long minPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE;
         for (int i = 0; i < numRunningJobs; ++i) {
             final JobServiceContext jsc = activeServices.get(i);
             final JobStatus js = jsc.getRunningJobLocked();
@@ -723,12 +758,18 @@
             if (js != null) {
                 mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType());
                 assignment.workType = jsc.getRunningJobWorkType();
+                if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+                    info.numRunningTopEj++;
+                }
             }
 
             assignment.preferredUid = jsc.getPreferredUid();
             if ((assignment.shouldStopJobReason = shouldStopRunningJobLocked(jsc)) != null) {
                 stoppable.add(assignment);
             } else {
+                assignment.timeUntilStoppableMs = jsc.getRemainingGuaranteedTimeMs(nowElapsed);
+                minPreferredUidOnlyWaitingTimeMs =
+                        Math.min(minPreferredUidOnlyWaitingTimeMs, assignment.timeUntilStoppableMs);
                 preferredUidOnly.add(assignment);
             }
         }
@@ -754,6 +795,11 @@
         }
 
         mWorkCountTracker.onCountDone();
+        // Set 0 if there were no preferred UID only contexts to indicate no waiting time due
+        // to such jobs.
+        info.minPreferredUidOnlyWaitingTimeMs =
+                minPreferredUidOnlyWaitingTimeMs == Long.MAX_VALUE
+                        ? 0 : minPreferredUidOnlyWaitingTimeMs;
     }
 
     @VisibleForTesting
@@ -761,12 +807,14 @@
     void determineAssignmentsLocked(final ArraySet<ContextAssignment> changed,
             final ArraySet<ContextAssignment> idle,
             final List<ContextAssignment> preferredUidOnly,
-            final List<ContextAssignment> stoppable) {
+            final List<ContextAssignment> stoppable,
+            @NonNull AssignmentInfo info) {
         final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
         final List<JobServiceContext> activeServices = mActiveServices;
         pendingJobQueue.resetIterator();
         JobStatus nextPending;
         int projectedRunningCount = activeServices.size();
+        long minChangedWaitingTimeMs = Long.MAX_VALUE;
         while ((nextPending = pendingJobQueue.next()) != null) {
             if (mRunningJobs.contains(nextPending)) {
                 // Should never happen.
@@ -785,6 +833,14 @@
                         + " to: " + nextPending);
             }
 
+            // Factoring minChangedWaitingTimeMs into the min waiting time effectively limits
+            // the number of additional contexts that are created due to long waiting times.
+            // By factoring it in, we imply that the new slot will be available for other
+            // pending jobs that could be designated as waiting too long, and those other jobs
+            // would only have to wait for the new slots to become available.
+            final long minWaitingTimeMs =
+                    Math.min(info.minPreferredUidOnlyWaitingTimeMs, minChangedWaitingTimeMs);
+
             // Find an available slot for nextPending. The context should be one of the following:
             // 1. Unused
             // 2. Its job should have used up its minimum execution guarantee so it
@@ -812,13 +868,6 @@
                 }
             }
             if (selectedContext == null && stoppable.size() > 0) {
-                int topEjCount = 0;
-                for (int r = mRunningJobs.size() - 1; r >= 0; --r) {
-                    JobStatus js = mRunningJobs.valueAt(r);
-                    if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
-                        topEjCount++;
-                    }
-                }
                 for (int s = stoppable.size() - 1; s >= 0; --s) {
                     final ContextAssignment assignment = stoppable.get(s);
                     final JobStatus runningJob = assignment.context.getRunningJobLocked();
@@ -833,12 +882,21 @@
                     //    app was on TOP, the app is still TOP, but there are too many TOP+EJs
                     //    running (because we don't want them to starve out other apps and the
                     //    current job has already run for the minimum guaranteed time).
+                    // 5. This new job could be waiting for too long for a slot to open up
                     boolean canReplace = isTopEj; // Case 1
                     if (!canReplace && !isInOverage) {
                         final int currentJobBias = mService.evaluateJobBiasLocked(runningJob);
                         canReplace = runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP // Case 2
                                 || currentJobBias < JobInfo.BIAS_TOP_APP // Case 3
-                                || topEjCount > .5 * mWorkTypeConfig.getMaxTotal(); // Case 4
+                                // Case 4
+                                || info.numRunningTopEj > .5 * mWorkTypeConfig.getMaxTotal();
+                    }
+                    if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5
+                        if (nextPending.shouldTreatAsExpeditedJob()) {
+                            canReplace = minWaitingTimeMs >= mMaxWaitEjMs;
+                        } else {
+                            canReplace = minWaitingTimeMs >= mMaxWaitRegularMs;
+                        }
                     }
                     if (canReplace) {
                         int replaceWorkType = mWorkCountTracker.canJobStart(allWorkTypes,
@@ -860,6 +918,7 @@
             }
             if (selectedContext == null && (!isInOverage || isTopEj)) {
                 int lowestBiasSeen = Integer.MAX_VALUE;
+                long newMinPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE;
                 for (int p = preferredUidOnly.size() - 1; p >= 0; --p) {
                     final ContextAssignment assignment = preferredUidOnly.get(p);
                     final JobStatus runningJob = assignment.context.getRunningJobLocked();
@@ -872,6 +931,13 @@
                     }
 
                     if (selectedContext == null || lowestBiasSeen > jobBias) {
+                        if (selectedContext != null) {
+                            // We're no longer using the previous context, so factor it into the
+                            // calculation.
+                            newMinPreferredUidOnlyWaitingTimeMs = Math.min(
+                                    newMinPreferredUidOnlyWaitingTimeMs,
+                                    selectedContext.timeUntilStoppableMs);
+                        }
                         // Step down the preemption threshold - wind up replacing
                         // the lowest-bias running job
                         lowestBiasSeen = jobBias;
@@ -880,11 +946,17 @@
                         assignment.preemptReasonCode = JobParameters.STOP_REASON_PREEMPT;
                         // In this case, we're just going to preempt a low bias job, we're not
                         // actually starting a job, so don't set startingJob to true.
+                    } else {
+                        // We're not going to use this context, so factor it into the calculation.
+                        newMinPreferredUidOnlyWaitingTimeMs = Math.min(
+                                newMinPreferredUidOnlyWaitingTimeMs,
+                                assignment.timeUntilStoppableMs);
                     }
                 }
                 if (selectedContext != null) {
                     selectedContext.newJob = nextPending;
                     preferredUidOnly.remove(selectedContext);
+                    info.minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs;
                 }
             }
             // Make sure to run EJs for the TOP app immediately.
@@ -901,6 +973,9 @@
                     selectedContext = null;
                 }
                 if (selectedContext == null) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Allowing additional context because EJ would wait too long");
+                    }
                     selectedContext = mContextAssignmentPool.acquire();
                     if (selectedContext == null) {
                         selectedContext = new ContextAssignment();
@@ -913,6 +988,35 @@
                     selectedContext.newWorkType =
                             (workType != WORK_TYPE_NONE) ? workType : WORK_TYPE_TOP;
                 }
+            } else if (selectedContext == null && mMaxWaitTimeBypassEnabled) {
+                final boolean wouldBeWaitingTooLong = nextPending.shouldTreatAsExpeditedJob()
+                        ? minWaitingTimeMs >= mMaxWaitEjMs
+                        : minWaitingTimeMs >= mMaxWaitRegularMs;
+                if (wouldBeWaitingTooLong) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Allowing additional context because job would wait too long");
+                    }
+                    selectedContext = mContextAssignmentPool.acquire();
+                    if (selectedContext == null) {
+                        selectedContext = new ContextAssignment();
+                    }
+                    selectedContext.context = mIdleContexts.size() > 0
+                            ? mIdleContexts.removeAt(mIdleContexts.size() - 1)
+                            : createNewJobServiceContext();
+                    selectedContext.newJob = nextPending;
+                    final int workType = mWorkCountTracker.canJobStart(allWorkTypes);
+                    if (workType != WORK_TYPE_NONE) {
+                        selectedContext.newWorkType = workType;
+                    } else {
+                        // Use the strongest work type possible for this job.
+                        for (int type = 1; type <= ALL_WORK_TYPES; type = type << 1) {
+                            if ((type & allWorkTypes) != 0) {
+                                selectedContext.newWorkType = type;
+                                break;
+                            }
+                        }
+                    }
+                }
             }
             final PackageStats packageStats = getPkgStatsLocked(
                     nextPending.getSourceUserId(), nextPending.getSourcePackageName());
@@ -923,6 +1027,8 @@
                 }
                 if (selectedContext.newJob != null) {
                     projectedRunningCount++;
+                    minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs,
+                            mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob));
                 }
                 packageStats.adjustStagedCount(true, nextPending.shouldTreatAsExpeditedJob());
             }
@@ -967,7 +1073,8 @@
     private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed,
             final ArraySet<ContextAssignment> idle,
             final List<ContextAssignment> preferredUidOnly,
-            final List<ContextAssignment> stoppable) {
+            final List<ContextAssignment> stoppable,
+            final AssignmentInfo assignmentInfo) {
         for (int s = stoppable.size() - 1; s >= 0; --s) {
             final ContextAssignment assignment = stoppable.get(s);
             assignment.clear();
@@ -988,6 +1095,7 @@
         idle.clear();
         stoppable.clear();
         preferredUidOnly.clear();
+        assignmentInfo.clear();
         mWorkCountTracker.resetStagingCount();
         mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
     }
@@ -1043,7 +1151,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 +1168,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();
@@ -1251,13 +1359,37 @@
         }
 
         final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
-        if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT || pendingJobQueue.size() == 0) {
+        if (pendingJobQueue.size() == 0) {
             worker.clearPreferredUid();
-            // We're over the limit (because the TOP app scheduled a lot of EJs). Don't start
-            // running anything new until we get back below the limit.
             noteConcurrency();
             return;
         }
+        if (mActiveServices.size() >= STANDARD_CONCURRENCY_LIMIT) {
+            final boolean respectConcurrencyLimit;
+            if (!mMaxWaitTimeBypassEnabled) {
+                respectConcurrencyLimit = true;
+            } else {
+                long minWaitingTimeMs = Long.MAX_VALUE;
+                final long nowElapsed = sElapsedRealtimeClock.millis();
+                for (int i = mActiveServices.size() - 1; i >= 0; --i) {
+                    minWaitingTimeMs = Math.min(minWaitingTimeMs,
+                            mActiveServices.get(i).getRemainingGuaranteedTimeMs(nowElapsed));
+                }
+                final boolean wouldBeWaitingTooLong =
+                        mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0
+                                ? minWaitingTimeMs >= mMaxWaitEjMs
+                                : minWaitingTimeMs >= mMaxWaitRegularMs;
+                respectConcurrencyLimit = !wouldBeWaitingTooLong;
+            }
+            if (respectConcurrencyLimit) {
+                worker.clearPreferredUid();
+                // We're over the limit (because the TOP app scheduled a lot of EJs), but we should
+                // be able to stop the other jobs soon so don't start running anything new until we
+                // get back below the limit.
+                noteConcurrency();
+                return;
+            }
+        }
 
         if (worker.getPreferredUid() != JobServiceContext.NO_PREFERRED_UID) {
             updateCounterConfigLocked();
@@ -1609,6 +1741,14 @@
         mPkgConcurrencyLimitRegular = Math.max(1, Math.min(STANDARD_CONCURRENCY_LIMIT,
                 properties.getInt(
                         KEY_PKG_CONCURRENCY_LIMIT_REGULAR, DEFAULT_PKG_CONCURRENCY_LIMIT_REGULAR)));
+
+        mMaxWaitTimeBypassEnabled = properties.getBoolean(
+                KEY_ENABLE_MAX_WAIT_TIME_BYPASS, DEFAULT_ENABLE_MAX_WAIT_TIME_BYPASS);
+        // EJ max wait must be in the range [0, infinity).
+        mMaxWaitEjMs = Math.max(0, properties.getLong(KEY_MAX_WAIT_EJ_MS, DEFAULT_MAX_WAIT_EJ_MS));
+        // Regular max wait must be in the range [EJ max wait, infinity).
+        mMaxWaitRegularMs = Math.max(mMaxWaitEjMs,
+                properties.getLong(KEY_MAX_WAIT_REGULAR_MS, DEFAULT_MAX_WAIT_REGULAR_MS));
     }
 
     @GuardedBy("mLock")
@@ -1622,6 +1762,9 @@
             pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println();
             pw.print(KEY_PKG_CONCURRENCY_LIMIT_EJ, mPkgConcurrencyLimitEj).println();
             pw.print(KEY_PKG_CONCURRENCY_LIMIT_REGULAR, mPkgConcurrencyLimitRegular).println();
+            pw.print(KEY_ENABLE_MAX_WAIT_TIME_BYPASS, mMaxWaitTimeBypassEnabled).println();
+            pw.print(KEY_MAX_WAIT_EJ_MS, mMaxWaitEjMs).println();
+            pw.print(KEY_MAX_WAIT_REGULAR_MS, mMaxWaitRegularMs).println();
             pw.println();
             CONFIG_LIMITS_SCREEN_ON.normal.dump(pw);
             pw.println();
@@ -2382,6 +2525,7 @@
         public int workType = WORK_TYPE_NONE;
         public String preemptReason;
         public int preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
+        public long timeUntilStoppableMs;
         public String shouldStopJobReason;
         public JobStatus newJob;
         public int newWorkType = WORK_TYPE_NONE;
@@ -2392,12 +2536,24 @@
             workType = WORK_TYPE_NONE;
             preemptReason = null;
             preemptReasonCode = JobParameters.STOP_REASON_UNDEFINED;
+            timeUntilStoppableMs = 0;
             shouldStopJobReason = null;
             newJob = null;
             newWorkType = WORK_TYPE_NONE;
         }
     }
 
+    @VisibleForTesting
+    static final class AssignmentInfo {
+        public long minPreferredUidOnlyWaitingTimeMs;
+        public int numRunningTopEj;
+
+        void clear() {
+            minPreferredUidOnlyWaitingTimeMs = 0;
+            numRunningTopEj = 0;
+        }
+    }
+
     // TESTING HELPERS
 
     @VisibleForTesting
@@ -2406,6 +2562,15 @@
         final PackageStats packageStats =
                 getPackageStatsForTesting(job.getSourceUserId(), job.getSourcePackageName());
         packageStats.adjustRunningCount(true, job.shouldTreatAsExpeditedJob());
+
+        final JobServiceContext context;
+        if (mIdleContexts.size() > 0) {
+            context = mIdleContexts.removeAt(mIdleContexts.size() - 1);
+        } else {
+            context = createNewJobServiceContext();
+        }
+        context.executeRunnableJob(job, mWorkCountTracker.canJobStart(getJobWorkTypes(job)));
+        mActiveServices.add(context);
     }
 
     @VisibleForTesting
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 bdd1fc54..e0d1a30 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -40,6 +40,8 @@
 import android.app.job.JobWorkItem;
 import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -58,6 +60,7 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatteryStatsInternal;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.LimitExceededException;
 import android.os.Looper;
@@ -166,7 +169,15 @@
     /** The number of the most recently completed jobs to keep track of for debugging purposes. */
     private static final int NUM_COMPLETED_JOB_HISTORY = 20;
 
-    @VisibleForTesting
+    /**
+     * Require the hosting job to specify a network constraint if the included
+     * {@link android.app.job.JobWorkItem} indicates network usage.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static Clock sSystemClock = Clock.systemUTC();
 
     private abstract static class MySimpleClock extends Clock {
@@ -421,6 +432,7 @@
                             break;
                         case Constants.KEY_MIN_LINEAR_BACKOFF_TIME_MS:
                         case Constants.KEY_MIN_EXP_BACKOFF_TIME_MS:
+                        case Constants.KEY_SYSTEM_STOP_TO_FAILURE_RATIO:
                             mConstants.updateBackoffConstantsLocked();
                             break;
                         case Constants.KEY_CONN_CONGESTION_DELAY_FRAC:
@@ -442,6 +454,10 @@
                                 runtimeUpdated = true;
                             }
                             break;
+                        case Constants.KEY_PERSIST_IN_SPLIT_FILES:
+                            mConstants.updatePersistingConstantsLocked();
+                            mJobs.setUseSplitFiles(mConstants.PERSIST_IN_SPLIT_FILES);
+                            break;
                         default:
                             if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
                                     && !concurrencyUpdated) {
@@ -498,6 +514,8 @@
 
         private static final String KEY_MIN_LINEAR_BACKOFF_TIME_MS = "min_linear_backoff_time_ms";
         private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms";
+        private static final String KEY_SYSTEM_STOP_TO_FAILURE_RATIO =
+                "system_stop_to_failure_ratio";
         private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
         private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
         private static final String KEY_CONN_USE_CELL_SIGNAL_STRENGTH =
@@ -523,12 +541,15 @@
         private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS =
                 "runtime_min_high_priority_guarantee_ms";
 
+        private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files";
+
         private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
         private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
         private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
         private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
         private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS;
         private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS;
+        private static final int DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO = 3;
         private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
         private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
         private static final boolean DEFAULT_CONN_USE_CELL_SIGNAL_STRENGTH = true;
@@ -548,6 +569,7 @@
         public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
         @VisibleForTesting
         static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS;
+        static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
         private static final boolean DEFAULT_USE_TARE_POLICY = false;
 
         /**
@@ -578,6 +600,11 @@
          * The minimum backoff time to allow for exponential backoff.
          */
         long MIN_EXP_BACKOFF_TIME_MS = DEFAULT_MIN_EXP_BACKOFF_TIME_MS;
+        /**
+         * The ratio to use to convert number of times a job was stopped by JobScheduler to an
+         * incremental failure in the backoff policy calculation.
+         */
+        int SYSTEM_STOP_TO_FAILURE_RATIO = DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO;
 
         /**
          * The fraction of a job's running window that must pass before we
@@ -658,6 +685,12 @@
                 DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS;
 
         /**
+         * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be
+         * saved in a single file.
+         */
+        public boolean PERSIST_IN_SPLIT_FILES = DEFAULT_PERSIST_IN_SPLIT_FILES;
+
+        /**
          * If true, use TARE policy for job limiting. If false, use quotas.
          */
         public boolean USE_TARE_POLICY = DEFAULT_USE_TARE_POLICY;
@@ -689,6 +722,9 @@
             MIN_EXP_BACKOFF_TIME_MS = DeviceConfig.getLong(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                     KEY_MIN_EXP_BACKOFF_TIME_MS,
                     DEFAULT_MIN_EXP_BACKOFF_TIME_MS);
+            SYSTEM_STOP_TO_FAILURE_RATIO = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_SYSTEM_STOP_TO_FAILURE_RATIO,
+                    DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO);
         }
 
         private void updateConnectivityConstantsLocked() {
@@ -712,6 +748,11 @@
                     DEFAULT_CONN_LOW_SIGNAL_STRENGTH_RELAX_FRAC);
         }
 
+        private void updatePersistingConstantsLocked() {
+            PERSIST_IN_SPLIT_FILES = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_PERSIST_IN_SPLIT_FILES, DEFAULT_PERSIST_IN_SPLIT_FILES);
+        }
+
         private void updatePrefetchConstantsLocked() {
             PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong(
                     DeviceConfig.NAMESPACE_JOB_SCHEDULER,
@@ -786,6 +827,7 @@
 
             pw.print(KEY_MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME_MS).println();
             pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println();
+            pw.print(KEY_SYSTEM_STOP_TO_FAILURE_RATIO, SYSTEM_STOP_TO_FAILURE_RATIO).println();
             pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
             pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
             pw.print(KEY_CONN_USE_CELL_SIGNAL_STRENGTH, CONN_USE_CELL_SIGNAL_STRENGTH).println();
@@ -811,6 +853,8 @@
             pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
                     .println();
 
+            pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println();
+
             pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY).println();
 
             pw.decreaseIndent();
@@ -1266,7 +1310,16 @@
                     jobStatus.getJob().isPrefetch(),
                     jobStatus.getJob().getPriority(),
                     jobStatus.getEffectivePriority(),
-                    jobStatus.getNumFailures());
+                    jobStatus.getNumPreviousAttempts(),
+                    jobStatus.getJob().getMaxExecutionDelayMillis(),
+                    /* isDeadlineConstraintSatisfied */ false,
+                    /* isCharging */ false,
+                    /* batteryNotLow */ false,
+                    /* storageNotLow */false,
+                    /* timingDelayConstraintSatisfied */ false,
+                    /* isDeviceIdle */ false,
+                    /* hasConnectivityConstraintSatisfied */ false,
+                    /* hasContentTriggerConstraintSatisfied */ false);
 
             // If the job is immediately ready to run, then we can just immediately
             // put it in the pending list and try to schedule it.  This is especially
@@ -1465,7 +1518,16 @@
                     cancelled.getJob().isPrefetch(),
                     cancelled.getJob().getPriority(),
                     cancelled.getEffectivePriority(),
-                    cancelled.getNumFailures());
+                    cancelled.getNumPreviousAttempts(),
+                    cancelled.getJob().getMaxExecutionDelayMillis(),
+                    cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE),
+                    cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+                    cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+                    cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+                    cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+                    cancelled.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+                    cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+                    cancelled.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
         }
         // If this is a replacement, bring in the new version of the job
         if (incomingJob != null) {
@@ -1809,12 +1871,13 @@
             Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
         }
         jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
-        final boolean update = mJobs.add(jobStatus);
+        final boolean update = lastJob != null;
+        mJobs.add(jobStatus);
         if (mReadyToRock) {
             for (int i = 0; i < mControllers.size(); i++) {
                 StateController controller = mControllers.get(i);
                 if (update) {
-                    controller.maybeStopTrackingJobLocked(jobStatus, null, true);
+                    controller.maybeStopTrackingJobLocked(jobStatus, null);
                 }
                 controller.maybeStartTrackingJobLocked(jobStatus, lastJob);
             }
@@ -1847,7 +1910,7 @@
         if (mReadyToRock) {
             for (int i = 0; i < mControllers.size(); i++) {
                 StateController controller = mControllers.get(i);
-                controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
+                controller.maybeStopTrackingJobLocked(jobStatus, incomingJob);
             }
         }
         return removed;
@@ -1859,10 +1922,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) {
@@ -1892,7 +1955,7 @@
      * Reschedules the given job based on the job's backoff policy. It doesn't make sense to
      * specify an override deadline on a failed job (the failed job will run even though it's not
      * ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any
-     * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed.
+     * ready job with {@link JobStatus#getNumPreviousAttempts()} > 0 will be executed.
      *
      * @param failureToReschedule Provided job status that we will reschedule.
      * @return A newly instantiated JobStatus with the same constraints as the last job except
@@ -1900,12 +1963,24 @@
      * @see #maybeQueueReadyJobsForExecutionLocked
      */
     @VisibleForTesting
-    JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
+    JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule,
+            int internalStopReason) {
         final long elapsedNowMillis = sElapsedRealtimeClock.millis();
         final JobInfo job = failureToReschedule.getJob();
 
         final long initialBackoffMillis = job.getInitialBackoffMillis();
-        final int backoffAttempts = failureToReschedule.getNumFailures() + 1;
+        int numFailures = failureToReschedule.getNumFailures();
+        int numSystemStops = failureToReschedule.getNumSystemStops();
+        // We should back off slowly if JobScheduler keeps stopping the job,
+        // but back off immediately if the issue appeared to be the app's fault.
+        if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH
+                || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT) {
+            numFailures++;
+        } else {
+            numSystemStops++;
+        }
+        final int backoffAttempts = Math.max(1,
+                numFailures + numSystemStops / mConstants.SYSTEM_STOP_TO_FAILURE_RATIO);
         long delayMillis;
 
         switch (job.getBackoffPolicy()) {
@@ -1932,7 +2007,7 @@
                 Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
         JobStatus newJob = new JobStatus(failureToReschedule,
                 elapsedNowMillis + delayMillis,
-                JobStatus.NO_LATEST_RUNTIME, backoffAttempts,
+                JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops,
                 failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
         if (job.isPeriodic()) {
             newJob.setOriginalLatestRunTimeElapsed(
@@ -2023,7 +2098,7 @@
                     + newLatestRuntimeElapsed);
             return new JobStatus(periodicToReschedule,
                     elapsedNow + period - flex, elapsedNow + period,
-                    0 /* backoffAttempt */,
+                    0 /* numFailures */, 0 /* numSystemStops */,
                     sSystemClock.millis() /* lastSuccessfulRunTime */,
                     periodicToReschedule.getLastFailedRunTime());
         }
@@ -2038,7 +2113,7 @@
         }
         return new JobStatus(periodicToReschedule,
                 newEarliestRunTimeElapsed, newLatestRuntimeElapsed,
-                0 /* backoffAttempt */,
+                0 /* numFailures */, 0 /* numSystemStops */,
                 sSystemClock.millis() /* lastSuccessfulRunTime */,
                 periodicToReschedule.getLastFailedRunTime());
     }
@@ -2082,7 +2157,7 @@
         // job so we can transfer any appropriate state over from the previous job when
         // we stop it.
         final JobStatus rescheduledJob = needsReschedule
-                ? getRescheduleJobForFailureLocked(jobStatus) : null;
+                ? getRescheduleJobForFailureLocked(jobStatus, debugStopReason) : null;
         if (rescheduledJob != null
                 && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT
                 || debugStopReason == JobParameters.INTERNAL_STOP_REASON_PREEMPT)) {
@@ -2144,11 +2219,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);
             }
         }
     }
@@ -2416,7 +2491,7 @@
                     shouldForceBatchJob =
                             mPrefetchController.getNextEstimatedLaunchTimeLocked(job)
                                     > relativelySoonCutoffTime;
-                } else if (job.getNumFailures() > 0) {
+                } else if (job.getNumPreviousAttempts() > 0) {
                     shouldForceBatchJob = false;
                 } else {
                     final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -3147,10 +3222,17 @@
             return canPersist;
         }
 
-        private void validateJobFlags(JobInfo job, int callingUid) {
+        private void validateJob(JobInfo job, int callingUid) {
+            validateJob(job, callingUid, null);
+        }
+
+        private void validateJob(JobInfo job, int callingUid, @Nullable JobWorkItem jobWorkItem) {
+            final boolean rejectNegativeNetworkEstimates = CompatChanges.isChangeEnabled(
+                            JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES, callingUid);
             job.enforceValidity(
                     CompatChanges.isChangeEnabled(
-                            JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid));
+                            JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid),
+                    rejectNegativeNetworkEstimates);
             if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
@@ -3164,6 +3246,26 @@
                             + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
                 }
             }
+            if (jobWorkItem != null) {
+                jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
+                if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN
+                        || jobWorkItem.getEstimatedNetworkUploadBytes()
+                        != JobInfo.NETWORK_BYTES_UNKNOWN
+                        || jobWorkItem.getMinimumNetworkChunkBytes()
+                        != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                    if (job.getRequiredNetwork() == null) {
+                        final String errorMsg = "JobWorkItem implies network usage"
+                                + " but job doesn't specify a network constraint";
+                        if (CompatChanges.isChangeEnabled(
+                                REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS,
+                                callingUid)) {
+                            throw new IllegalArgumentException(errorMsg);
+                        } else {
+                            Slog.e(TAG, errorMsg);
+                        }
+                    }
+                }
+            }
         }
 
         // IJobScheduler implementation
@@ -3184,7 +3286,7 @@
                 }
             }
 
-            validateJobFlags(job, uid);
+            validateJob(job, uid);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -3212,8 +3314,7 @@
                 throw new NullPointerException("work is null");
             }
 
-            work.enforceValidity();
-            validateJobFlags(job, uid);
+            validateJob(job, uid, work);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -3244,7 +3345,7 @@
                         + " not permitted to schedule jobs for other apps");
             }
 
-            validateJobFlags(job, callerUid);
+            validateJob(job, callerUid);
 
             final long ident = Binder.clearCallingIdentity();
             try {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index d6456f0..9aa6b1c 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -21,6 +21,7 @@
 import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
+import android.annotation.BytesLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.job.IJobCallback;
@@ -187,6 +188,18 @@
         public long mStoppedTime;
 
         @Override
+        public void acknowledgeGetTransferredDownloadBytesMessage(int jobId, int workId,
+                @BytesLong long transferredBytes) {
+            doAcknowledgeGetTransferredDownloadBytesMessage(this, jobId, workId, transferredBytes);
+        }
+
+        @Override
+        public void acknowledgeGetTransferredUploadBytesMessage(int jobId, int workId,
+                @BytesLong long transferredBytes) {
+            doAcknowledgeGetTransferredUploadBytesMessage(this, jobId, workId, transferredBytes);
+        }
+
+        @Override
         public void acknowledgeStartMessage(int jobId, boolean ongoing) {
             doAcknowledgeStartMessage(this, jobId, ongoing);
         }
@@ -210,6 +223,18 @@
         public void jobFinished(int jobId, boolean reschedule) {
             doJobFinished(this, jobId, reschedule);
         }
+
+        @Override
+        public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
+                long downloadBytes, long uploadBytes) {
+            doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+        }
+
+        @Override
+        public void updateTransferredNetworkBytes(int jobId, JobWorkItem item,
+                long downloadBytes, long uploadBytes) {
+            doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
+        }
     }
 
     JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -363,7 +388,16 @@
                     job.getJob().isPrefetch(),
                     job.getJob().getPriority(),
                     job.getEffectivePriority(),
-                    job.getNumFailures());
+                    job.getNumPreviousAttempts(),
+                    job.getJob().getMaxExecutionDelayMillis(),
+                    isDeadlineExpired,
+                    job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+                    job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+                    job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+                    job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+                    job.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+                    job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+                    job.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
             if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
                 // Use the context's ID to distinguish traces since there'll only be one job
                 // running per context.
@@ -454,6 +488,10 @@
         return mTimeoutElapsed;
     }
 
+    long getRemainingGuaranteedTimeMs(long nowElapsed) {
+        return Math.max(0, mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis - nowElapsed);
+    }
+
     boolean isWithinExecutionGuaranteeTime() {
         return sElapsedRealtimeClock.millis()
                 < mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
@@ -493,6 +531,16 @@
         }
     }
 
+    private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback jobCallback, int jobId,
+            int workId, @BytesLong long transferredBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+    }
+
+    private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback jobCallback, int jobId,
+            int workId, @BytesLong long transferredBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+    }
+
     void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
         doCallback(cb, reschedule, null);
     }
@@ -545,6 +593,16 @@
         }
     }
 
+    private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
+            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+    }
+
+    private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId,
+            @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
+        // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
+    }
+
     /**
      * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
      * we intend to send to the client - we stop sending work when the service is unbound so until
@@ -1032,7 +1090,16 @@
                 completedJob.getJob().isPrefetch(),
                 completedJob.getJob().getPriority(),
                 completedJob.getEffectivePriority(),
-                completedJob.getNumFailures());
+                completedJob.getNumPreviousAttempts(),
+                completedJob.getJob().getMaxExecutionDelayMillis(),
+                mParams.isOverrideDeadlineExpired(),
+                completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_CHARGING),
+                completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW),
+                completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW),
+                completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY),
+                completedJob.isConstraintSatisfied(JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE),
+                completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY),
+                completedJob.isConstraintSatisfied(JobStatus.CONSTRAINT_CONTENT_TRIGGER));
         if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
             Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
                     completedJob.getTag(), getId());
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index f731b8d..c2602f2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -40,14 +40,16 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SystemConfigFileCommitEventLogger;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.BitUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
 import com.android.server.job.controllers.JobStatus;
@@ -89,6 +91,8 @@
 
     /** Threshold to adjust how often we want to write to the db. */
     private static final long JOB_PERSIST_DELAY = 2000L;
+    private static final String JOB_FILE_SPLIT_PREFIX = "jobs_";
+    private static final int ALL_UIDS = -1;
 
     final Object mLock;
     final Object mWriteScheduleLock;    // used solely for invariants around write scheduling
@@ -105,13 +109,20 @@
     @GuardedBy("mWriteScheduleLock")
     private boolean mWriteInProgress;
 
+    @GuardedBy("mWriteScheduleLock")
+    private boolean mSplitFileMigrationNeeded;
+
     private static final Object sSingletonLock = new Object();
     private final SystemConfigFileCommitEventLogger mEventLogger;
     private final AtomicFile mJobsFile;
+    private final File mJobFileDirectory;
+    private final SparseBooleanArray mPendingJobWriteUids = new SparseBooleanArray();
     /** Handler backed by IoThread for writing to disk. */
     private final Handler mIoHandler = IoThread.getHandler();
     private static JobStore sSingleton;
 
+    private boolean mUseSplitFiles = JobSchedulerService.Constants.DEFAULT_PERSIST_IN_SPLIT_FILES;
+
     private JobStorePersistStats mPersistInfo = new JobStorePersistStats();
 
     /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
@@ -144,10 +155,10 @@
         mContext = context;
 
         File systemDir = new File(dataDir, "system");
-        File jobDir = new File(systemDir, "job");
-        jobDir.mkdirs();
+        mJobFileDirectory = new File(systemDir, "job");
+        mJobFileDirectory.mkdirs();
         mEventLogger = new SystemConfigFileCommitEventLogger("jobs");
-        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), mEventLogger);
+        mJobsFile = createJobFile(new File(mJobFileDirectory, "jobs.xml"));
 
         mJobSet = new JobSet();
 
@@ -162,12 +173,21 @@
         // an incorrect historical timestamp.  That's fine; at worst we'll reboot with
         // a *correct* timestamp, see a bunch of overdue jobs, and run them; then
         // settle into normal operation.
-        mXmlTimestamp = mJobsFile.getLastModifiedTime();
+        mXmlTimestamp = mJobsFile.exists()
+                ? mJobsFile.getLastModifiedTime() : mJobFileDirectory.lastModified();
         mRtcGood = (sSystemClock.millis() > mXmlTimestamp);
 
         readJobMapFromDisk(mJobSet, mRtcGood);
     }
 
+    private AtomicFile createJobFile(String baseName) {
+        return createJobFile(new File(mJobFileDirectory, baseName + ".xml"));
+    }
+
+    private AtomicFile createJobFile(File file) {
+        return new AtomicFile(file, mEventLogger);
+    }
+
     public boolean jobTimesInflatedValid() {
         return mRtcGood;
     }
@@ -194,7 +214,7 @@
                         convertRtcBoundsToElapsed(utcTimes, elapsedNow);
                 JobStatus newJob = new JobStatus(job,
                         elapsedRuntimes.first, elapsedRuntimes.second,
-                        0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime());
+                        0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime());
                 newJob.prepareLocked();
                 toAdd.add(newJob);
                 toRemove.add(job);
@@ -203,21 +223,20 @@
     }
 
     /**
-     * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
-     * it will be replaced.
+     * Add a job to the master list, persisting it if necessary.
+     * Similar jobs to the new job will not be removed.
+     *
      * @param jobStatus Job to add.
-     * @return Whether or not an equivalent JobStatus was replaced by this operation.
      */
-    public boolean add(JobStatus jobStatus) {
-        boolean replaced = mJobSet.remove(jobStatus);
+    public void add(JobStatus jobStatus) {
         mJobSet.add(jobStatus);
         if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
             maybeWriteStatusToDiskAsync();
         }
         if (DEBUG) {
             Slog.d(TAG, "Added job status to store: " + jobStatus);
         }
-        return replaced;
     }
 
     /**
@@ -226,6 +245,9 @@
     @VisibleForTesting
     public void addForTesting(JobStatus jobStatus) {
         mJobSet.add(jobStatus);
+        if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
+        }
     }
 
     boolean containsJob(JobStatus jobStatus) {
@@ -259,12 +281,24 @@
             return false;
         }
         if (removeFromPersisted && jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
             maybeWriteStatusToDiskAsync();
         }
         return removed;
     }
 
     /**
+     * Like {@link #remove(JobStatus, boolean)}, but doesn't schedule a disk write.
+     */
+    @VisibleForTesting
+    public void removeForTesting(JobStatus jobStatus) {
+        mJobSet.remove(jobStatus);
+        if (jobStatus.isPersisted()) {
+            mPendingJobWriteUids.put(jobStatus.getUid(), true);
+        }
+    }
+
+    /**
      * Remove the jobs of users not specified in the keepUserIds.
      * @param keepUserIds Array of User IDs whose jobs should be kept and not removed.
      */
@@ -275,6 +309,7 @@
     @VisibleForTesting
     public void clear() {
         mJobSet.clear();
+        mPendingJobWriteUids.put(ALL_UIDS, true);
         maybeWriteStatusToDiskAsync();
     }
 
@@ -284,6 +319,36 @@
     @VisibleForTesting
     public void clearForTesting() {
         mJobSet.clear();
+        mPendingJobWriteUids.put(ALL_UIDS, true);
+    }
+
+    void setUseSplitFiles(boolean useSplitFiles) {
+        synchronized (mLock) {
+            if (mUseSplitFiles != useSplitFiles) {
+                mUseSplitFiles = useSplitFiles;
+                migrateJobFilesAsync();
+            }
+        }
+    }
+
+    /**
+     * The same as above but does not schedule writing. This makes perf benchmarks more stable.
+     */
+    @VisibleForTesting
+    public void setUseSplitFilesForTesting(boolean useSplitFiles) {
+        final boolean changed;
+        synchronized (mLock) {
+            changed = mUseSplitFiles != useSplitFiles;
+            if (changed) {
+                mUseSplitFiles = useSplitFiles;
+                mPendingJobWriteUids.put(ALL_UIDS, true);
+            }
+        }
+        if (changed) {
+            synchronized (mWriteScheduleLock) {
+                mSplitFileMigrationNeeded = true;
+            }
+        }
     }
 
     /**
@@ -354,6 +419,16 @@
     private static final String XML_TAG_ONEOFF = "one-off";
     private static final String XML_TAG_EXTRAS = "extras";
 
+    private void migrateJobFilesAsync() {
+        synchronized (mLock) {
+            mPendingJobWriteUids.put(ALL_UIDS, true);
+        }
+        synchronized (mWriteScheduleLock) {
+            mSplitFileMigrationNeeded = true;
+            maybeWriteStatusToDiskAsync();
+        }
+    }
+
     /**
      * Every time the state changes we write all the jobs in one swath, instead of trying to
      * track incremental changes.
@@ -451,10 +526,38 @@
      * NOTE: This Runnable locks on mLock
      */
     private final Runnable mWriteRunnable = new Runnable() {
+        private final SparseArray<AtomicFile> mJobFiles = new SparseArray<>();
+        private final CopyConsumer mPersistedJobCopier = new CopyConsumer();
+
+        class CopyConsumer implements Consumer<JobStatus> {
+            private final SparseArray<List<JobStatus>> mJobStoreCopy = new SparseArray<>();
+            private boolean mCopyAllJobs;
+
+            private void prepare() {
+                mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS);
+            }
+
+            @Override
+            public void accept(JobStatus jobStatus) {
+                final int uid = mUseSplitFiles ? jobStatus.getUid() : ALL_UIDS;
+                if (jobStatus.isPersisted() && (mCopyAllJobs || mPendingJobWriteUids.get(uid))) {
+                    List<JobStatus> uidJobList = mJobStoreCopy.get(uid);
+                    if (uidJobList == null) {
+                        uidJobList = new ArrayList<>();
+                        mJobStoreCopy.put(uid, uidJobList);
+                    }
+                    uidJobList.add(new JobStatus(jobStatus));
+                }
+            }
+
+            private void reset() {
+                mJobStoreCopy.clear();
+            }
+        }
+
         @Override
         public void run() {
             final long startElapsed = sElapsedRealtimeClock.millis();
-            final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
             // Intentionally allow new scheduling of a write operation *before* we clone
             // the job set.  If we reset it to false after cloning, there's a window in
             // which no new write will be scheduled but mLock is not held, i.e. a new
@@ -471,31 +574,73 @@
                 }
                 mWriteInProgress = true;
             }
+            final boolean useSplitFiles;
             synchronized (mLock) {
                 // Clone the jobs so we can release the lock before writing.
-                mJobSet.forEachJob(null, (job) -> {
-                    if (job.isPersisted()) {
-                        storeCopy.add(new JobStatus(job));
-                    }
-                });
+                useSplitFiles = mUseSplitFiles;
+                mPersistedJobCopier.prepare();
+                mJobSet.forEachJob(null, mPersistedJobCopier);
+                mPendingJobWriteUids.clear();
             }
-            writeJobsMapImpl(storeCopy);
+            mPersistInfo.countAllJobsSaved = 0;
+            mPersistInfo.countSystemServerJobsSaved = 0;
+            mPersistInfo.countSystemSyncManagerJobsSaved = 0;
+            for (int i = mPersistedJobCopier.mJobStoreCopy.size() - 1; i >= 0; --i) {
+                AtomicFile file;
+                if (useSplitFiles) {
+                    final int uid = mPersistedJobCopier.mJobStoreCopy.keyAt(i);
+                    file = mJobFiles.get(uid);
+                    if (file == null) {
+                        file = createJobFile(JOB_FILE_SPLIT_PREFIX + uid);
+                        mJobFiles.put(uid, file);
+                    }
+                } else {
+                    file = mJobsFile;
+                }
+                if (DEBUG) {
+                    Slog.d(TAG, "Writing for " + mPersistedJobCopier.mJobStoreCopy.keyAt(i)
+                            + " to " + file.getBaseFile().getName() + ": "
+                            + mPersistedJobCopier.mJobStoreCopy.valueAt(i).size() + " jobs");
+                }
+                writeJobsMapImpl(file, mPersistedJobCopier.mJobStoreCopy.valueAt(i));
+            }
             if (DEBUG) {
                 Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis()
                         - startElapsed) + "ms");
             }
+            mPersistedJobCopier.reset();
+            if (!useSplitFiles) {
+                mJobFiles.clear();
+            }
+            // Update the last modified time of the directory to aid in RTC time verification
+            // (see the JobStore constructor).
+            mJobFileDirectory.setLastModified(sSystemClock.millis());
             synchronized (mWriteScheduleLock) {
+                if (mSplitFileMigrationNeeded) {
+                    final File[] files = mJobFileDirectory.listFiles();
+                    for (File file : files) {
+                        if (useSplitFiles) {
+                            if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                                // Delete the now unused file so there's no confusion in the future.
+                                file.delete();
+                            }
+                        } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                            // Delete the now unused file so there's no confusion in the future.
+                            file.delete();
+                        }
+                    }
+                }
                 mWriteInProgress = false;
                 mWriteScheduleLock.notifyAll();
             }
         }
 
-        private void writeJobsMapImpl(List<JobStatus> jobList) {
+        private void writeJobsMapImpl(@NonNull AtomicFile file, @NonNull List<JobStatus> jobList) {
             int numJobs = 0;
             int numSystemJobs = 0;
             int numSyncJobs = 0;
             mEventLogger.setStartTime(SystemClock.uptimeMillis());
-            try (FileOutputStream fos = mJobsFile.startWrite()) {
+            try (FileOutputStream fos = file.startWrite()) {
                 TypedXmlSerializer out = Xml.resolveSerializer(fos);
                 out.startDocument(null, true);
                 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
@@ -525,7 +670,7 @@
                 out.endTag(null, "job-info");
                 out.endDocument();
 
-                mJobsFile.finishWrite(fos);
+                file.finishWrite(fos);
             } catch (IOException e) {
                 if (DEBUG) {
                     Slog.v(TAG, "Error writing out job data.", e);
@@ -535,9 +680,9 @@
                     Slog.d(TAG, "Error persisting bundle.", e);
                 }
             } finally {
-                mPersistInfo.countAllJobsSaved = numJobs;
-                mPersistInfo.countSystemServerJobsSaved = numSystemJobs;
-                mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs;
+                mPersistInfo.countAllJobsSaved += numJobs;
+                mPersistInfo.countSystemServerJobsSaved += numSystemJobs;
+                mPersistInfo.countSystemSyncManagerJobsSaved += numSyncJobs;
             }
         }
 
@@ -602,9 +747,11 @@
          *       because currently store is not including everything (like, UIDs, bandwidth,
          *       signal strength etc. are lost).
          */
-        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
+        private void writeConstraintsToXml(TypedXmlSerializer out, JobStatus jobStatus)
+                throws IOException {
             out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
             if (jobStatus.hasConnectivityConstraint()) {
+                final JobInfo job = jobStatus.getJob();
                 final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
                 out.attribute(null, "net-capabilities-csv", intArrayToString(
                         network.getCapabilities()));
@@ -612,6 +759,18 @@
                         network.getForbiddenCapabilities()));
                 out.attribute(null, "net-transport-types-csv", intArrayToString(
                         network.getTransportTypes()));
+                if (job.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                    out.attributeLong(null, "estimated-download-bytes",
+                            job.getEstimatedNetworkDownloadBytes());
+                }
+                if (job.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                    out.attributeLong(null, "estimated-upload-bytes",
+                            job.getEstimatedNetworkUploadBytes());
+                }
+                if (job.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                    out.attributeLong(null, "minimum-network-chunk-bytes",
+                            job.getMinimumNetworkChunkBytes());
+                }
             }
             if (jobStatus.hasIdleConstraint()) {
                 out.attribute(null, "idle", Boolean.toString(true));
@@ -722,54 +881,87 @@
 
         @Override
         public void run() {
+            if (!mJobFileDirectory.isDirectory()) {
+                Slog.wtf(TAG, "jobs directory isn't a directory O.O");
+                mJobFileDirectory.mkdirs();
+                return;
+            }
+
             int numJobs = 0;
             int numSystemJobs = 0;
             int numSyncJobs = 0;
             List<JobStatus> jobs;
-            try (FileInputStream fis = mJobsFile.openRead()) {
-                synchronized (mLock) {
-                    jobs = readJobMapImpl(fis, rtcGood);
-                    if (jobs != null) {
-                        long now = sElapsedRealtimeClock.millis();
-                        for (int i=0; i<jobs.size(); i++) {
-                            JobStatus js = jobs.get(i);
-                            js.prepareLocked();
-                            js.enqueueTime = now;
-                            this.jobSet.add(js);
+            final File[] files;
+            try {
+                files = mJobFileDirectory.listFiles();
+            } catch (SecurityException e) {
+                Slog.wtf(TAG, "Not allowed to read job file directory", e);
+                return;
+            }
+            if (files == null) {
+                Slog.wtfStack(TAG, "Couldn't get job file list");
+                return;
+            }
+            boolean needFileMigration = false;
+            long now = sElapsedRealtimeClock.millis();
+            for (File file : files) {
+                final AtomicFile aFile = createJobFile(file);
+                try (FileInputStream fis = aFile.openRead()) {
+                    synchronized (mLock) {
+                        jobs = readJobMapImpl(fis, rtcGood);
+                        if (jobs != null) {
+                            for (int i = 0; i < jobs.size(); i++) {
+                                JobStatus js = jobs.get(i);
+                                js.prepareLocked();
+                                js.enqueueTime = now;
+                                this.jobSet.add(js);
 
-                            numJobs++;
-                            if (js.getUid() == Process.SYSTEM_UID) {
-                                numSystemJobs++;
-                                if (isSyncJob(js)) {
-                                    numSyncJobs++;
+                                numJobs++;
+                                if (js.getUid() == Process.SYSTEM_UID) {
+                                    numSystemJobs++;
+                                    if (isSyncJob(js)) {
+                                        numSyncJobs++;
+                                    }
                                 }
                             }
                         }
                     }
+                } catch (FileNotFoundException e) {
+                    // mJobFileDirectory.listFiles() gave us this file...why can't we find it???
+                    Slog.e(TAG, "Could not find jobs file: " + file.getName());
+                } catch (XmlPullParserException | IOException e) {
+                    Slog.wtf(TAG, "Error in " + file.getName(), e);
+                } catch (Exception e) {
+                    // Crashing at this point would result in a boot loop, so live with a general
+                    // Exception for system stability's sake.
+                    Slog.wtf(TAG, "Unexpected exception", e);
                 }
-            } catch (FileNotFoundException e) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
-                }
-            } catch (XmlPullParserException | IOException e) {
-                Slog.wtf(TAG, "Error jobstore xml.", e);
-            } catch (Exception e) {
-                // Crashing at this point would result in a boot loop, so live with a general
-                // Exception for system stability's sake.
-                Slog.wtf(TAG, "Unexpected exception", e);
-            } finally {
-                if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
-                    mPersistInfo.countAllJobsLoaded = numJobs;
-                    mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
-                    mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
+                if (mUseSplitFiles) {
+                    if (!file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                        // We're supposed to be using the split file architecture, but we still have
+                        // the old job file around. Fully migrate and remove the old file.
+                        needFileMigration = true;
+                    }
+                } else if (file.getName().startsWith(JOB_FILE_SPLIT_PREFIX)) {
+                    // We're supposed to be using the legacy single file architecture, but we still
+                    // have some job split files around. Fully migrate and remove the split files.
+                    needFileMigration = true;
                 }
             }
+            if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
+                mPersistInfo.countAllJobsLoaded = numJobs;
+                mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
+                mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
+            }
             Slog.i(TAG, "Read " + numJobs + " jobs");
+            if (needFileMigration) {
+                migrateJobFilesAsync();
+            }
         }
 
         private List<JobStatus> readJobMapImpl(InputStream fis, boolean rtcIsGood)
                 throws XmlPullParserException, IOException {
-            XmlPullParser parser = Xml.resolvePullParser(fis);
+            TypedXmlPullParser parser = Xml.resolvePullParser(fis);
 
             int eventType = parser.getEventType();
             while (eventType != XmlPullParser.START_TAG &&
@@ -829,7 +1021,7 @@
          *               will take the parser into the body of the job tag.
          * @return Newly instantiated job holding all the information we just read out of the xml tag.
          */
-        private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser,
+        private JobStatus restoreJobFromXml(boolean rtcIsGood, TypedXmlPullParser parser,
                 int schemaVersion) throws XmlPullParserException, IOException {
             JobInfo.Builder jobBuilder;
             int uid, sourceUserId;
@@ -1024,7 +1216,8 @@
                 // have a deadline. If a job is rescheduled (via jobFinished(true) or onStopJob()'s
                 // return value), the deadline is dropped. Periodic jobs require all constraints
                 // to be met, so there's no issue with their deadlines.
-                builtJob = jobBuilder.build(false);
+                // The same logic applies for other target SDK-based validation checks.
+                builtJob = jobBuilder.build(false, false);
             } catch (Exception e) {
                 Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e);
                 return null;
@@ -1074,7 +1267,7 @@
          * reading, but in order to avoid issues with OEM-defined flags, the accepted capabilities
          * are limited to that(maxNetCapabilityInR & maxTransportInR) defined in R.
          */
-        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser)
+        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, TypedXmlPullParser parser)
                 throws XmlPullParserException, IOException {
             String val;
             String netCapabilitiesLong = null;
@@ -1111,7 +1304,17 @@
                 for (int transport : stringToIntArray(netTransportTypesIntArray)) {
                     builder.addTransportType(transport);
                 }
-                jobBuilder.setRequiredNetwork(builder.build());
+                jobBuilder
+                        .setRequiredNetwork(builder.build())
+                        .setEstimatedNetworkBytes(
+                                parser.getAttributeLong(null,
+                                        "estimated-download-bytes", JobInfo.NETWORK_BYTES_UNKNOWN),
+                                parser.getAttributeLong(null,
+                                        "estimated-upload-bytes", JobInfo.NETWORK_BYTES_UNKNOWN))
+                        .setMinimumNetworkChunkBytes(
+                                parser.getAttributeLong(null,
+                                        "minimum-network-chunk-bytes",
+                                        JobInfo.NETWORK_BYTES_UNKNOWN));
             } else if (netCapabilitiesLong != null && netTransportTypesLong != null) {
                 // Format used on R- builds. Drop any unexpected capabilities and transports.
                 final NetworkRequest.Builder builder = new NetworkRequest.Builder()
@@ -1139,6 +1342,8 @@
                     }
                 }
                 jobBuilder.setRequiredNetwork(builder.build());
+                // Estimated bytes weren't persisted on R- builds, so no point querying for the
+                // attributes here.
             } else {
                 // Read legacy values
                 val = parser.getAttributeValue(null, "connectivity");
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/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
index 65d7121..ecee10a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -81,8 +81,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
     }
 
     @Override
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index d284a99..2ca3f8f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -133,7 +133,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
             mTrackedTasks.remove(taskStatus);
             mTopStartedJobs.remove(taskStatus);
@@ -143,7 +143,7 @@
     @Override
     public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
         if (!jobStatus.hasPowerConstraint()) {
-            maybeStopTrackingJobLocked(jobStatus, null, false);
+            maybeStopTrackingJobLocked(jobStatus, null);
         }
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
index 9b59560..b029e00 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
@@ -127,8 +127,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
     }
 
     @Override
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index d2dc2a7e..16dd1672 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -290,8 +290,7 @@
 
     @GuardedBy("mLock")
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) {
             ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid());
             if (jobs != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
index 83a756c..847a1bf 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
@@ -159,8 +159,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) {
             mTrackedTasks.remove(taskStatus);
             if (taskStatus.contentObserverJobInstance != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index abbe177..bdf72b6 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -225,8 +225,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
             mAllowInIdleJobs.remove(jobStatus);
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 547f94ba..4c17692 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -213,7 +213,7 @@
 
     @Override
     @GuardedBy("mLock")
-    public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob, boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) {
         if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) {
             mFlexibilityAlarmQueue.removeAlarmForKey(js);
             mFlexibilityTracker.remove(js);
@@ -342,10 +342,10 @@
             // There is no deadline and no estimated launch time.
             return NO_LIFECYCLE_END;
         }
-        if (js.getNumFailures() > 1) {
-            // Number of failures will not equal one as per restriction in JobStatus constructor.
+        // Increase the flex deadline for jobs rescheduled more than once.
+        if (js.getNumPreviousAttempts() > 1) {
             return earliest + Math.min(
-                    (long) Math.scalb(mRescheduledJobDeadline, js.getNumFailures() - 2),
+                    (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
                     mMaxRescheduledDeadline);
         }
         return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index 926cfc1..a25af71 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -76,8 +76,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) {
             mTrackedTasks.remove(taskStatus);
         }
@@ -86,7 +85,7 @@
     @Override
     public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
         if (!jobStatus.hasIdleConstraint()) {
-            maybeStopTrackingJobLocked(jobStatus, null, false);
+            maybeStopTrackingJobLocked(jobStatus, null);
         }
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 669234b..f6410ff 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -95,11 +95,11 @@
     static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
     static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
     static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
-    static final int CONSTRAINT_TIMING_DELAY = 1<<31;
-    static final int CONSTRAINT_DEADLINE = 1<<30;
-    static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
+    public static final int CONSTRAINT_TIMING_DELAY = 1 << 31;
+    public static final int CONSTRAINT_DEADLINE = 1 << 30;
+    public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
     static final int CONSTRAINT_TARE_WEALTH = 1 << 27; // Implicit constraint
-    static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
+    public static final int CONSTRAINT_CONTENT_TRIGGER = 1 << 26;
     static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
     static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
     static final int CONSTRAINT_PREFETCH = 1 << 23;
@@ -241,10 +241,22 @@
      */
     private long mOriginalLatestRunTimeElapsedMillis;
 
-    /** How many times this job has failed, used to compute back-off. */
+    /**
+     * How many times this job has failed to complete on its own
+     * (via {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} or because of
+     * a timeout).
+     * This count doesn't include most times JobScheduler decided to stop the job
+     * (via {@link android.app.job.JobService#onStopJob(JobParameters)}.
+     */
     private final int numFailures;
 
     /**
+     * The number of times JobScheduler has forced this job to stop due to reasons mostly outside
+     * of the app's control.
+     */
+    private final int mNumSystemStops;
+
+    /**
      * Which app standby bucket this job's app is in.  Updated when the app is moved to a
      * different bucket.
      */
@@ -488,6 +500,8 @@
      * @param tag A string associated with the job for debugging/logging purposes.
      * @param numFailures Count of how many times this job has requested a reschedule because
      *     its work was not yet finished.
+     * @param numSystemStops Count of how many times JobScheduler has forced this job to stop due to
+     *     factors mostly out of the app's control.
      * @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job
      *     is to be considered runnable
      * @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be
@@ -497,7 +511,7 @@
      * @param internalFlags Non-API property flags about this job
      */
     private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
-            int sourceUserId, int standbyBucket, String tag, int numFailures,
+            int sourceUserId, int standbyBucket, String tag, int numFailures, int numSystemStops,
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags,
             int dynamicConstraints) {
@@ -535,6 +549,7 @@
         this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
         this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
         this.numFailures = numFailures;
+        mNumSystemStops = numSystemStops;
 
         int requiredConstraints = job.getConstraintFlags();
         if (job.getRequiredNetwork() != null) {
@@ -576,7 +591,7 @@
         // Otherwise, every consecutive reschedule increases a jobs' flexibility deadline.
         if (!isRequestedExpeditedJob()
                 && satisfiesMinWindowException
-                && numFailures != 1
+                && (numFailures + numSystemStops) != 1
                 && lacksSomeFlexibleConstraints) {
             mNumRequiredFlexibleConstraints =
                     NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mPreferUnmetered ? 1 : 0);
@@ -612,9 +627,9 @@
             requestBuilder.setUids(
                     Collections.singleton(new Range<Integer>(this.sourceUid, this.sourceUid)));
             builder.setRequiredNetwork(requestBuilder.build());
-            // Don't perform prefetch-deadline check at this point. We've already passed the
+            // Don't perform validation checks at this point since we've already passed the
             // initial validation check.
-            job = builder.build(false);
+            job = builder.build(false, false);
         }
 
         updateMediaBackupExemptionStatus();
@@ -626,7 +641,7 @@
         this(jobStatus.getJob(), jobStatus.getUid(),
                 jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
                 jobStatus.getStandbyBucket(),
-                jobStatus.getSourceTag(), jobStatus.getNumFailures(),
+                jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(),
                 jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
                 jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
                 jobStatus.getInternalFlags(), jobStatus.mDynamicConstraints);
@@ -654,7 +669,7 @@
             int innerFlags, int dynamicConstraints) {
         this(job, callingUid, sourcePkgName, sourceUserId,
                 standbyBucket,
-                sourceTag, 0,
+                sourceTag, /* numFailures */ 0, /* numSystemStops */ 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints);
 
@@ -673,12 +688,13 @@
     /** Create a new job to be rescheduled with the provided parameters. */
     public JobStatus(JobStatus rescheduling,
             long newEarliestRuntimeElapsedMillis,
-            long newLatestRuntimeElapsedMillis, int backoffAttempt,
+            long newLatestRuntimeElapsedMillis, int numFailures, int numSystemStops,
             long lastSuccessfulRunTime, long lastFailedRunTime) {
         this(rescheduling.job, rescheduling.getUid(),
                 rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
                 rescheduling.getStandbyBucket(),
-                rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
+                rescheduling.getSourceTag(), numFailures, numSystemStops,
+                newEarliestRuntimeElapsedMillis,
                 newLatestRuntimeElapsedMillis,
                 lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags(),
                 rescheduling.mDynamicConstraints);
@@ -715,7 +731,7 @@
         int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
                 sourceUserId, elapsedNow);
         return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
-                standbyBucket, tag, 0,
+                standbyBucket, tag, /* numFailures */ 0, /* numSystemStops */ 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
                 /*innerFlags=*/ 0, /* dynamicConstraints */ 0);
@@ -868,10 +884,27 @@
         pw.print(job.getId());
     }
 
+    /**
+     * Returns the number of times the job stopped previously for reasons that appeared to be within
+     * the app's control.
+     */
     public int getNumFailures() {
         return numFailures;
     }
 
+    /**
+     * Returns the number of times the system stopped a previous execution of this job for reasons
+     * that were likely outside the app's control.
+     */
+    public int getNumSystemStops() {
+        return mNumSystemStops;
+    }
+
+    /** Returns the total number of times we've attempted to run this job in the past. */
+    public int getNumPreviousAttempts() {
+        return numFailures + mNumSystemStops;
+    }
+
     public ComponentName getServiceComponent() {
         return job.getService();
     }
@@ -1061,27 +1094,41 @@
 
     private void updateNetworkBytesLocked() {
         mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes();
+        if (mTotalNetworkDownloadBytes < 0) {
+            // Legacy apps may have provided invalid negative values. Ignore invalid values.
+            mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+        }
         mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes();
+        if (mTotalNetworkUploadBytes < 0) {
+            // Legacy apps may have provided invalid negative values. Ignore invalid values.
+            mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+        }
+        // Minimum network chunk bytes has had data validation since its introduction, so no
+        // need to do validation again.
         mMinimumNetworkChunkBytes = job.getMinimumNetworkChunkBytes();
 
         if (pendingWork != null) {
             for (int i = 0; i < pendingWork.size(); i++) {
-                if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
-                    // If any component of the job has unknown usage, we don't have a
-                    // complete picture of what data will be used, and we have to treat the
-                    // entire up/download as unknown.
-                    long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes();
-                    if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes();
+                if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN && downloadBytes > 0) {
+                    // If any component of the job has unknown usage, we won't have a
+                    // complete picture of what data will be used. However, we use what we are given
+                    // to get us as close to the complete picture as possible.
+                    if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
                         mTotalNetworkDownloadBytes += downloadBytes;
+                    } else {
+                        mTotalNetworkDownloadBytes = downloadBytes;
                     }
                 }
-                if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
-                    // If any component of the job has unknown usage, we don't have a
-                    // complete picture of what data will be used, and we have to treat the
-                    // entire up/download as unknown.
-                    long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes();
-                    if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes();
+                if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN && uploadBytes > 0) {
+                    // If any component of the job has unknown usage, we won't have a
+                    // complete picture of what data will be used. However, we use what we are given
+                    // to get us as close to the complete picture as possible.
+                    if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
                         mTotalNetworkUploadBytes += uploadBytes;
+                    } else {
+                        mTotalNetworkUploadBytes = uploadBytes;
                     }
                 }
                 final long chunkBytes = pendingWork.get(i).getMinimumNetworkChunkBytes();
@@ -1566,7 +1613,8 @@
         }
     }
 
-    boolean isConstraintSatisfied(int constraint) {
+    /** @return whether or not the @param constraint is satisfied */
+    public boolean isConstraintSatisfied(int constraint) {
         return (satisfiedConstraints&constraint) != 0;
     }
 
@@ -1857,6 +1905,10 @@
             sb.append(" failures=");
             sb.append(numFailures);
         }
+        if (mNumSystemStops != 0) {
+            sb.append(" system stops=");
+            sb.append(mNumSystemStops);
+        }
         if (isReady()) {
             sb.append(" READY");
         } else {
@@ -2382,6 +2434,9 @@
         if (numFailures != 0) {
             pw.print("Num failures: "); pw.println(numFailures);
         }
+        if (mNumSystemStops != 0) {
+            pw.print("Num system stops: "); pw.println(mNumSystemStops);
+        }
         if (mLastSuccessfulRunTime != 0) {
             pw.print("Last successful run: ");
             pw.println(formatTime(mLastSuccessfulRunTime));
@@ -2579,7 +2634,7 @@
         proto.write(JobStatusDumpProto.ORIGINAL_LATEST_RUNTIME_ELAPSED,
                 mOriginalLatestRunTimeElapsedMillis);
 
-        proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures);
+        proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures + mNumSystemStops);
         proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime);
         proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime);
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
deleted file mode 100644
index 78a77fe..0000000
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
+++ /dev/null
@@ -1,59 +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.
- */
-
-package com.android.server.job.controllers;
-
-import java.util.Objects;
-
-/** Wrapper class to represent a userId-pkgName combo. */
-final class Package {
-    public final String packageName;
-    public final int userId;
-
-    Package(int userId, String packageName) {
-        this.userId = userId;
-        this.packageName = packageName;
-    }
-
-    @Override
-    public String toString() {
-        return packageToString(userId, packageName);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (!(obj instanceof Package)) {
-            return false;
-        }
-        Package other = (Package) obj;
-        return userId == other.userId && Objects.equals(packageName, other.packageName);
-    }
-
-    @Override
-    public int hashCode() {
-        return packageName.hashCode() + userId;
-    }
-
-    /**
-     * Standardize the output of userId-packageName combo.
-     */
-    static String packageToString(int userId, String packageName) {
-        return "<" + userId + ">" + packageName;
-    }
-}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index e04cec3..c46ffd7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -21,7 +21,6 @@
 
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.JobSchedulerService.sSystemClock;
-import static com.android.server.job.controllers.Package.packageToString;
 
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.ElapsedRealtimeLong;
@@ -31,6 +30,7 @@
 import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
 import android.appwidget.AppWidgetManager;
 import android.content.Context;
+import android.content.pm.UserPackage;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -167,13 +167,12 @@
 
     @Override
     @GuardedBy("mLock")
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         final int userId = jobStatus.getSourceUserId();
         final String pkgName = jobStatus.getSourcePackageName();
         final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
         if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
-            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+            mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
         }
     }
 
@@ -187,7 +186,7 @@
         final int userId = UserHandle.getUserId(uid);
         mTrackedJobs.delete(userId, packageName);
         mEstimatedLaunchTimes.delete(userId, packageName);
-        mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
+        mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, packageName));
     }
 
     @Override
@@ -355,7 +354,7 @@
             @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
         final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
         if (jobs == null || jobs.size() == 0) {
-            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+            mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
             return;
         }
 
@@ -366,10 +365,10 @@
             // Set alarm to be notified when this crosses the threshold.
             final long timeToCrossThresholdMs =
                     nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
-            mThresholdAlarmListener.addAlarm(new Package(userId, pkgName),
+            mThresholdAlarmListener.addAlarm(UserPackage.of(userId, pkgName),
                     nowElapsed + timeToCrossThresholdMs);
         } else {
-            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+            mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
         }
     }
 
@@ -428,25 +427,25 @@
     }
 
     /** Track when apps will cross the "will run soon" threshold. */
-    private class ThresholdAlarmListener extends AlarmQueue<Package> {
+    private class ThresholdAlarmListener extends AlarmQueue<UserPackage> {
         private ThresholdAlarmListener(Context context, Looper looper) {
             super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
                     PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
         }
 
         @Override
-        protected boolean isForUser(@NonNull Package key, int userId) {
+        protected boolean isForUser(@NonNull UserPackage key, int userId) {
             return key.userId == userId;
         }
 
         @Override
-        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+        protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
             final ArraySet<JobStatus> changedJobs = new ArraySet<>();
             synchronized (mLock) {
                 final long now = sSystemClock.millis();
                 final long nowElapsed = sElapsedRealtimeClock.millis();
                 for (int i = 0; i < expired.size(); ++i) {
-                    Package p = expired.valueAt(i);
+                    UserPackage p = expired.valueAt(i);
                     if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
                         Slog.e(TAG, "Alarm expired for "
                                 + packageToString(p.userId, p.packageName) + " at the wrong time");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index bb8d175..d8206ad 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -28,7 +28,6 @@
 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
-import static com.android.server.job.controllers.Package.packageToString;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -44,6 +43,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
 import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.Looper;
@@ -532,7 +532,7 @@
      */
     private final SparseSetArray<String> mSystemInstallers = new SparseSetArray<>();
 
-    /** An app has reached its quota. The message should contain a {@link Package} object. */
+    /** An app has reached its quota. The message should contain a {@link UserPackage} object. */
     @VisibleForTesting
     static final int MSG_REACHED_QUOTA = 0;
     /** Drop any old timing sessions. */
@@ -542,7 +542,7 @@
     /** Process state for a UID has changed. */
     private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
     /**
-     * An app has reached its expedited job quota. The message should contain a {@link Package}
+     * An app has reached its expedited job quota. The message should contain a {@link UserPackage}
      * object.
      */
     @VisibleForTesting
@@ -673,15 +673,14 @@
 
     @Override
     @GuardedBy("mLock")
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
             unprepareFromExecutionLocked(jobStatus);
             final int userId = jobStatus.getSourceUserId();
             final String pkgName = jobStatus.getSourcePackageName();
             ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
             if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
-                mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+                mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
             }
         }
     }
@@ -747,7 +746,7 @@
         }
         mTimingEvents.delete(userId, packageName);
         mEJTimingSessions.delete(userId, packageName);
-        mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+        mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
         mExecutionStatsCache.delete(userId, packageName);
         mEJStats.delete(userId, packageName);
         mTopAppTrackers.delete(userId, packageName);
@@ -1726,7 +1725,7 @@
             // exempted.
             maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
         } else {
-            mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+            mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
         }
         return changedJobs;
     }
@@ -1765,7 +1764,7 @@
                     && isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
                 // TODO(141645789): we probably shouldn't cancel the alarm until we've verified
                 // that all jobs for the userId-package are within quota.
-                mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+                mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
             } else {
                 mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
             }
@@ -1815,7 +1814,7 @@
         if (jobs == null || jobs.size() == 0) {
             Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
                     + packageToString(userId, packageName) + " that has no jobs");
-            mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+            mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
             return;
         }
 
@@ -1839,7 +1838,7 @@
                         + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
                         + "ms in its quota.");
             }
-            mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+            mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
             mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
             return;
         }
@@ -1904,7 +1903,7 @@
                             + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
             inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
         }
-        mInQuotaAlarmQueue.addAlarm(new Package(userId, packageName), inQuotaTimeElapsed);
+        mInQuotaAlarmQueue.addAlarm(UserPackage.of(userId, packageName), inQuotaTimeElapsed);
     }
 
     private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed,
@@ -2099,7 +2098,7 @@
     }
 
     private final class Timer {
-        private final Package mPkg;
+        private final UserPackage mPkg;
         private final int mUid;
         private final boolean mRegularJobTimer;
 
@@ -2111,7 +2110,7 @@
         private long mDebitAdjustment;
 
         Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
-            mPkg = new Package(userId, packageName);
+            mPkg = UserPackage.of(userId, packageName);
             mUid = uid;
             mRegularJobTimer = regularJobTimer;
         }
@@ -2366,7 +2365,7 @@
     }
 
     private final class TopAppTimer {
-        private final Package mPkg;
+        private final UserPackage mPkg;
 
         // List of jobs currently running for this app that started when the app wasn't in the
         // foreground.
@@ -2374,7 +2373,7 @@
         private long mStartTimeElapsed;
 
         TopAppTimer(int userId, String packageName) {
-            mPkg = new Package(userId, packageName);
+            mPkg = UserPackage.of(userId, packageName);
         }
 
         private int calculateTimeChunks(final long nowElapsed) {
@@ -2657,7 +2656,7 @@
             synchronized (mLock) {
                 switch (msg.what) {
                     case MSG_REACHED_QUOTA: {
-                        Package pkg = (Package) msg.obj;
+                        UserPackage pkg = (UserPackage) msg.obj;
                         if (DEBUG) {
                             Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
                         }
@@ -2686,7 +2685,7 @@
                         break;
                     }
                     case MSG_REACHED_EJ_QUOTA: {
-                        Package pkg = (Package) msg.obj;
+                        UserPackage pkg = (UserPackage) msg.obj;
                         if (DEBUG) {
                             Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota.");
                         }
@@ -2888,21 +2887,21 @@
     }
 
     /** Track when UPTCs are expected to come back into quota. */
-    private class InQuotaAlarmQueue extends AlarmQueue<Package> {
+    private class InQuotaAlarmQueue extends AlarmQueue<UserPackage> {
         private InQuotaAlarmQueue(Context context, Looper looper) {
             super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false,
                     QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
         }
 
         @Override
-        protected boolean isForUser(@NonNull Package key, int userId) {
+        protected boolean isForUser(@NonNull UserPackage key, int userId) {
             return key.userId == userId;
         }
 
         @Override
-        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+        protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
             for (int i = 0; i < expired.size(); ++i) {
-                Package p = expired.valueAt(i);
+                UserPackage p = expired.valueAt(i);
                 mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName)
                         .sendToTarget();
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 8453e53..44ac798 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -85,8 +85,7 @@
     /**
      * Remove task - this will happen if the task is cancelled, completed, etc.
      */
-    public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate);
+    public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob);
 
     /**
      * Called when a new job is being created to reschedule an old failed job.
@@ -187,4 +186,11 @@
     /** Dump any internal constants the Controller may have. */
     public void dumpConstants(ProtoOutputStream proto) {
     }
+
+    /**
+     * Standardize the output of userId-packageName combo.
+     */
+    static String packageToString(int userId, String packageName) {
+        return "<" + userId + ">" + packageName;
+    }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
index 1ce0a7f6..11e2ff7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
@@ -70,8 +70,7 @@
     }
 
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) {
             mTrackedTasks.remove(taskStatus);
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
index b2ca3a0..cafb02d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TareController.java
@@ -383,8 +383,7 @@
 
     @Override
     @GuardedBy("mLock")
-    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         final int userId = jobStatus.getSourceUserId();
         final String pkgName = jobStatus.getSourcePackageName();
         if (!mTopStartedJobs.remove(jobStatus) && jobStatus.madeActive > 0) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
index b6361ce..fc60228 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
@@ -31,6 +31,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.expresslog.Counter;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.StateControllerProto;
 
@@ -79,7 +80,7 @@
     @Override
     public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
         if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
-            maybeStopTrackingJobLocked(job, null, false);
+            maybeStopTrackingJobLocked(job, null);
 
             // First: check the constraints now, because if they are already satisfied
             // then there is no need to track it.  This gives us a fast path for a common
@@ -88,6 +89,8 @@
             // will never be unsatisfied (our time base can not go backwards).
             final long nowElapsedMillis = sElapsedRealtimeClock.millis();
             if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) {
+                // We're intentionally excluding jobs whose deadlines have passed
+                // (mostly like deadlines of 0) when the job was scheduled.
                 return;
             } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job,
                     nowElapsedMillis)) {
@@ -134,8 +137,7 @@
      * tracking was the one our alarms were based off of.
      */
     @Override
-    public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob,
-            boolean forUpdate) {
+    public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob) {
         if (job.clearTrackingController(JobStatus.TRACKING_TIME)) {
             if (mTrackedJobs.remove(job)) {
                 checkExpiredDelaysAndResetAlarm();
@@ -159,6 +161,9 @@
                     // Scheduler.
                     mStateChangedListener.onRunJobNow(job);
                 }
+                mTrackedJobs.remove(job);
+                Counter.logIncrement(
+                        "job_scheduler.value_job_scheduler_job_deadline_expired_counter");
             } else if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
                 // This job's deadline is earlier than the current set alarm. Update the alarm.
                 setDeadlineExpiredAlarmLocked(job.getLatestRunTimeElapsed(),
@@ -170,8 +175,11 @@
                 && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) {
             // Since this is just the delay, we don't need to rush the Scheduler to run the job
             // immediately if the constraint is satisfied here.
-            if (!evaluateTimingDelayConstraint(job, nowElapsedMillis)
-                    && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+            if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) {
+                if (canStopTrackingJobLocked(job)) {
+                    mTrackedJobs.remove(job);
+                }
+            } else if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
                 // This job's delay is earlier than the current set alarm. Update the alarm.
                 setDelayExpiredAlarmLocked(job.getEarliestRunTime(),
                         mService.deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
@@ -229,6 +237,8 @@
                         // Scheduler.
                         mStateChangedListener.onRunJobNow(job);
                     }
+                    Counter.logIncrement(
+                            "job_scheduler.value_job_scheduler_job_deadline_expired_counter");
                     it.remove();
                 } else {  // Sorted by expiry time, so take the next one and stop.
                     if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
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/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index 7a13e3f..abc196f 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -36,6 +36,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.pm.UserPackage;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -824,7 +825,7 @@
     void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
         mScribe.discardLedgerLocked(userId, pkgName);
         mCurrentOngoingEvents.delete(userId, pkgName);
-        mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+        mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
     }
 
     @GuardedBy("mLock")
@@ -959,7 +960,7 @@
                 mCurrentOngoingEvents.get(userId, pkgName);
         if (ongoingEvents == null || mIrs.isVip(userId, pkgName)) {
             // No ongoing transactions. No reason to schedule
-            mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+            mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
             return;
         }
         mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
@@ -972,7 +973,7 @@
         if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
             if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
                 // Will never cross a threshold based on current events.
-                mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+                mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
                 return;
             }
             timeToThresholdMs = upperTimeMs;
@@ -980,7 +981,7 @@
             timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
                     ? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
         }
-        mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName),
+        mBalanceThresholdAlarmQueue.addAlarm(UserPackage.of(userId, pkgName),
                 SystemClock.elapsedRealtime() + timeToThresholdMs);
     }
 
@@ -1071,57 +1072,22 @@
 
     private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
 
-    private static final class Package {
-        public final String packageName;
-        public final int userId;
-
-        Package(int userId, String packageName) {
-            this.userId = userId;
-            this.packageName = packageName;
-        }
-
-        @Override
-        public String toString() {
-            return appToString(userId, packageName);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj == null) {
-                return false;
-            }
-            if (this == obj) {
-                return true;
-            }
-            if (obj instanceof Package) {
-                Package other = (Package) obj;
-                return userId == other.userId && Objects.equals(packageName, other.packageName);
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return packageName.hashCode() + userId;
-        }
-    }
-
     /** Track when apps will cross the closest affordability threshold (in both directions). */
-    private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> {
+    private class BalanceThresholdAlarmQueue extends AlarmQueue<UserPackage> {
         private BalanceThresholdAlarmQueue(Context context, Looper looper) {
             super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
                     15_000L);
         }
 
         @Override
-        protected boolean isForUser(@NonNull Package key, int userId) {
+        protected boolean isForUser(@NonNull UserPackage key, int userId) {
             return key.userId == userId;
         }
 
         @Override
-        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+        protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
             for (int i = 0; i < expired.size(); ++i) {
-                Package p = expired.valueAt(i);
+                UserPackage p = expired.valueAt(i);
                 mHandler.obtainMessage(
                         MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
                         .sendToTarget();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
index b426f16..46338fa 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
@@ -33,9 +33,10 @@
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES;
-import static android.app.tare.EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_EXEMPTED_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_MIN_SATIATED_BALANCE_OTHER_APP_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT_CAKES;
@@ -71,9 +72,10 @@
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_NONWAKEUP_CTP;
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE;
 import static android.app.tare.EconomyManager.KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP;
-import static android.app.tare.EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_AM_MAX_SATIATED_BALANCE;
+import static android.app.tare.EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP;
 import static android.app.tare.EconomyManager.KEY_AM_REWARD_NOTIFICATION_INTERACTION_INSTANT;
@@ -146,7 +148,8 @@
     private long mMinSatiatedBalanceOther;
     private long mMaxSatiatedBalance;
     private long mInitialSatiatedConsumptionLimit;
-    private long mHardSatiatedConsumptionLimit;
+    private long mMinSatiatedConsumptionLimit;
+    private long mMaxSatiatedConsumptionLimit;
 
     private final KeyValueListParser mParser = new KeyValueListParser(',');
     private final Injector mInjector;
@@ -199,8 +202,13 @@
     }
 
     @Override
-    long getHardSatiatedConsumptionLimit() {
-        return mHardSatiatedConsumptionLimit;
+    long getMinSatiatedConsumptionLimit() {
+        return mMinSatiatedConsumptionLimit;
+    }
+
+    @Override
+    long getMaxSatiatedConsumptionLimit() {
+        return mMaxSatiatedConsumptionLimit;
     }
 
     @NonNull
@@ -240,12 +248,15 @@
         mMaxSatiatedBalance = getConstantAsCake(mParser, properties,
             KEY_AM_MAX_SATIATED_BALANCE, DEFAULT_AM_MAX_SATIATED_BALANCE_CAKES,
             Math.max(arcToCake(1), mMinSatiatedBalanceExempted));
+        mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+                KEY_AM_MIN_CONSUMPTION_LIMIT, DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES,
+                arcToCake(1));
         mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
-            KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
-            arcToCake(1));
-        mHardSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
-            KEY_AM_HARD_CONSUMPTION_LIMIT, DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES,
-            mInitialSatiatedConsumptionLimit);
+                KEY_AM_INITIAL_CONSUMPTION_LIMIT, DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
+                mMinSatiatedConsumptionLimit);
+        mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+                KEY_AM_MAX_CONSUMPTION_LIMIT, DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES,
+                mInitialSatiatedConsumptionLimit);
 
         final long exactAllowWhileIdleWakeupBasePrice = getConstantAsCake(mParser, properties,
                 KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE,
@@ -396,9 +407,11 @@
         pw.decreaseIndent();
         pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println();
         pw.print("Consumption limits: [");
+        pw.print(cakeToString(mMinSatiatedConsumptionLimit));
+        pw.print(", ");
         pw.print(cakeToString(mInitialSatiatedConsumptionLimit));
         pw.print(", ");
-        pw.print(cakeToString(mHardSatiatedConsumptionLimit));
+        pw.print(cakeToString(mMaxSatiatedConsumptionLimit));
         pw.println("]");
 
         pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
index 66f7c35..7a96076 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/CompleteEconomicPolicy.java
@@ -43,7 +43,8 @@
     private int mEnabledEconomicPolicyIds = 0;
     private int[] mCostModifiers = EmptyArray.INT;
     private long mInitialConsumptionLimit;
-    private long mHardConsumptionLimit;
+    private long mMinConsumptionLimit;
+    private long mMaxConsumptionLimit;
 
     CompleteEconomicPolicy(@NonNull InternalResourceService irs) {
         this(irs, new CompleteInjector());
@@ -100,14 +101,17 @@
 
     private void updateLimits() {
         long initialConsumptionLimit = 0;
-        long hardConsumptionLimit = 0;
+        long minConsumptionLimit = 0;
+        long maxConsumptionLimit = 0;
         for (int i = 0; i < mEnabledEconomicPolicies.size(); ++i) {
             final EconomicPolicy economicPolicy = mEnabledEconomicPolicies.valueAt(i);
             initialConsumptionLimit += economicPolicy.getInitialSatiatedConsumptionLimit();
-            hardConsumptionLimit += economicPolicy.getHardSatiatedConsumptionLimit();
+            minConsumptionLimit += economicPolicy.getMinSatiatedConsumptionLimit();
+            maxConsumptionLimit += economicPolicy.getMaxSatiatedConsumptionLimit();
         }
         mInitialConsumptionLimit = initialConsumptionLimit;
-        mHardConsumptionLimit = hardConsumptionLimit;
+        mMinConsumptionLimit = minConsumptionLimit;
+        mMaxConsumptionLimit = maxConsumptionLimit;
     }
 
     @Override
@@ -134,8 +138,13 @@
     }
 
     @Override
-    long getHardSatiatedConsumptionLimit() {
-        return mHardConsumptionLimit;
+    long getMinSatiatedConsumptionLimit() {
+        return mMinConsumptionLimit;
+    }
+
+    @Override
+    long getMaxSatiatedConsumptionLimit() {
+        return mMaxConsumptionLimit;
     }
 
     @NonNull
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
index 008dcb8..b52f6f1 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java
@@ -232,15 +232,21 @@
      * Returns the maximum number of cakes that should be consumed during a full 100% discharge
      * cycle. This is the initial limit. The system may choose to increase the limit over time,
      * but the increased limit should never exceed the value returned from
-     * {@link #getHardSatiatedConsumptionLimit()}.
+     * {@link #getMaxSatiatedConsumptionLimit()}.
      */
     abstract long getInitialSatiatedConsumptionLimit();
 
     /**
-     * Returns the maximum number of cakes that should be consumed during a full 100% discharge
-     * cycle. This is the hard limit that should never be exceeded.
+     * Returns the minimum number of cakes that should be available for consumption during a full
+     * 100% discharge cycle.
      */
-    abstract long getHardSatiatedConsumptionLimit();
+    abstract long getMinSatiatedConsumptionLimit();
+
+    /**
+     * Returns the maximum number of cakes that should be available for consumption during a full
+     * 100% discharge cycle.
+     */
+    abstract long getMaxSatiatedConsumptionLimit();
 
     /** Return the set of modifiers that should apply to this policy's costs. */
     @NonNull
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index dd0a194..17b8746 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -670,7 +670,7 @@
         final long shortfall = (mCurrentBatteryLevel - QUANTITATIVE_EASING_BATTERY_THRESHOLD)
                 * currentConsumptionLimit / 100;
         final long newConsumptionLimit = Math.min(currentConsumptionLimit + shortfall,
-                mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit());
+                mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit());
         if (newConsumptionLimit != currentConsumptionLimit) {
             Slog.i(TAG, "Increasing consumption limit from " + cakeToString(currentConsumptionLimit)
                     + " to " + cakeToString(newConsumptionLimit));
@@ -701,6 +701,10 @@
             return;
         }
         final long totalDischargeMah = mAnalyst.getBatteryScreenOffDischargeMah();
+        if (totalDischargeMah == 0) {
+            Slog.i(TAG, "Total discharge was 0");
+            return;
+        }
         final long batteryCapacityMah = mBatteryManagerInternal.getBatteryFullCharge() / 1000;
         final long estimatedLifeHours = batteryCapacityMah * totalScreenOffDurationMs
                 / totalDischargeMah / HOUR_IN_MILLIS;
@@ -720,12 +724,12 @@
             // The stock is too low. We're doing pretty well. We can increase the stock slightly
             // to let apps do more work in the background.
             newConsumptionLimit = Math.min((long) (currentConsumptionLimit * 1.01),
-                    mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit());
+                    mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit());
         } else if (percentageOfTarget < 100) {
             // The stock is too high IMO. We're below the target. Decrease the stock to reduce
             // background work.
             newConsumptionLimit = Math.max((long) (currentConsumptionLimit * .98),
-                    mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
+                    mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit());
         } else {
             // The stock is just right.
             return;
@@ -957,9 +961,9 @@
             } else {
                 mScribe.loadFromDiskLocked();
                 if (mScribe.getSatiatedConsumptionLimitLocked()
-                        < mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
+                        < mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()
                         || mScribe.getSatiatedConsumptionLimitLocked()
-                        > mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+                        > mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) {
                     // Reset the consumption limit since several factors may have changed.
                     mScribe.setConsumptionLimitLocked(
                             mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
@@ -1442,17 +1446,16 @@
 
         private void updateEconomicPolicy() {
             synchronized (mLock) {
-                final long initialLimit =
-                        mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit();
-                final long hardLimit = mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit();
+                final long minLimit = mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit();
+                final long maxLimit = mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit();
                 final int oldEnabledPolicies = mCompleteEconomicPolicy.getEnabledPolicyIds();
                 mCompleteEconomicPolicy.tearDown();
                 mCompleteEconomicPolicy = new CompleteEconomicPolicy(InternalResourceService.this);
                 if (mIsEnabled && mBootPhase >= PHASE_THIRD_PARTY_APPS_CAN_START) {
                     mCompleteEconomicPolicy.setup(getAllDeviceConfigProperties());
-                    if (initialLimit != mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()
-                            || hardLimit
-                            != mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()) {
+                    if (minLimit != mCompleteEconomicPolicy.getMinSatiatedConsumptionLimit()
+                            || maxLimit
+                            != mCompleteEconomicPolicy.getMaxSatiatedConsumptionLimit()) {
                         // Reset the consumption limit since several factors may have changed.
                         mScribe.setConsumptionLimitLocked(
                                 mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit());
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
index 71c6d09..7cf459c 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
@@ -38,9 +38,10 @@
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_MIN_START_CTP_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP_CAKES;
-import static android.app.tare.EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES;
+import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_EXEMPTED_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER_CAKES;
 import static android.app.tare.EconomyManager.DEFAULT_JS_MIN_SATIATED_BALANCE_OTHER_APP_CAKES;
@@ -84,9 +85,10 @@
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_MIN_START_CTP;
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_BASE_PRICE;
 import static android.app.tare.EconomyManager.KEY_JS_ACTION_JOB_TIMEOUT_PENALTY_CTP;
-import static android.app.tare.EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT;
+import static android.app.tare.EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_JS_MAX_SATIATED_BALANCE;
+import static android.app.tare.EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT;
 import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED;
 import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_INCREMENT_APP_UPDATER;
 import static android.app.tare.EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP;
@@ -159,7 +161,8 @@
     private long mMinSatiatedBalanceIncrementalAppUpdater;
     private long mMaxSatiatedBalance;
     private long mInitialSatiatedConsumptionLimit;
-    private long mHardSatiatedConsumptionLimit;
+    private long mMinSatiatedConsumptionLimit;
+    private long mMaxSatiatedConsumptionLimit;
 
     private final KeyValueListParser mParser = new KeyValueListParser(',');
     private final Injector mInjector;
@@ -216,8 +219,13 @@
     }
 
     @Override
-    long getHardSatiatedConsumptionLimit() {
-        return mHardSatiatedConsumptionLimit;
+    long getMinSatiatedConsumptionLimit() {
+        return mMinSatiatedConsumptionLimit;
+    }
+
+    @Override
+    long getMaxSatiatedConsumptionLimit() {
+        return mMaxSatiatedConsumptionLimit;
     }
 
     @NonNull
@@ -260,12 +268,15 @@
         mMaxSatiatedBalance = getConstantAsCake(mParser, properties,
             KEY_JS_MAX_SATIATED_BALANCE, DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES,
             Math.max(arcToCake(1), mMinSatiatedBalanceExempted));
+        mMinSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+                KEY_JS_MIN_CONSUMPTION_LIMIT, DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES,
+                arcToCake(1));
         mInitialSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
-            KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES,
-            arcToCake(1));
-        mHardSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
-            KEY_JS_HARD_CONSUMPTION_LIMIT, DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES,
-            mInitialSatiatedConsumptionLimit);
+                KEY_JS_INITIAL_CONSUMPTION_LIMIT, DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES,
+                mMinSatiatedConsumptionLimit);
+        mMaxSatiatedConsumptionLimit = getConstantAsCake(mParser, properties,
+                KEY_JS_MAX_CONSUMPTION_LIMIT, DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES,
+                mInitialSatiatedConsumptionLimit);
 
         mActions.put(ACTION_JOB_MAX_START, new Action(ACTION_JOB_MAX_START,
                 getConstantAsCake(mParser, properties,
@@ -420,9 +431,11 @@
         pw.decreaseIndent();
         pw.print("Max satiated balance", cakeToString(mMaxSatiatedBalance)).println();
         pw.print("Consumption limits: [");
+        pw.print(cakeToString(mMinSatiatedConsumptionLimit));
+        pw.print(", ");
         pw.print(cakeToString(mInitialSatiatedConsumptionLimit));
         pw.print(", ");
-        pw.print(cakeToString(mHardSatiatedConsumptionLimit));
+        pw.print(cakeToString(mMaxSatiatedConsumptionLimit));
         pw.println("]");
 
         pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 27d00b7..ee448b5 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -33,12 +33,12 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/api/Android.bp b/api/Android.bp
index 9306671..37b5d4c 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -98,6 +98,7 @@
         "framework-configinfrastructure",
         "framework-connectivity",
         "framework-connectivity-t",
+        "framework-devicelock",
         "framework-federatedcompute",
         "framework-graphics",
         "framework-healthconnect",
@@ -112,6 +113,7 @@
         "framework-sdksandbox",
         "framework-tethering",
         "framework-uwb",
+        "framework-virtualization",
         "framework-wifi",
         "i18n.module.public.api",
     ],
diff --git a/api/api.go b/api/api.go
index 6a6c493..ba0fdc1 100644
--- a/api/api.go
+++ b/api/api.go
@@ -27,8 +27,16 @@
 const art = "art.module.public.api"
 const conscrypt = "conscrypt.module.public.api"
 const i18n = "i18n.module.public.api"
+const virtualization = "framework-virtualization"
 
 var core_libraries_modules = []string{art, conscrypt, i18n}
+// List of modules that are not yet updatable, and hence they can still compile
+// against hidden APIs. These modules are filtered out when building the
+// updatable-framework-module-impl (because updatable-framework-module-impl is
+// built against module_current SDK). Instead they are directly statically
+// linked into the all-framework-module-lib, which is building against hidden
+// APIs.
+var non_updatable_modules = []string{virtualization}
 
 // The intention behind this soong plugin is to generate a number of "merged"
 // API-related modules that would otherwise require a large amount of very
@@ -249,12 +257,31 @@
 func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) {
 	// This module is for the "framework-all" module, which should not include the core libraries.
 	modules = removeAll(modules, core_libraries_modules)
-	props := libraryProps{}
-	props.Name = proptools.StringPtr("all-framework-module-impl")
-	props.Static_libs = transformArray(modules, "", ".impl")
-	props.Sdk_version = proptools.StringPtr("module_current")
-	props.Visibility = []string{"//frameworks/base"}
-	ctx.CreateModule(java.LibraryFactory, &props)
+	// Remove the modules that belong to non-updatable APEXes since those are allowed to compile
+	// against unstable APIs.
+	modules = removeAll(modules, non_updatable_modules)
+	// First create updatable-framework-module-impl, which contains all updatable modules.
+	// This module compiles against module_lib SDK.
+	{
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("updatable-framework-module-impl")
+		props.Static_libs = transformArray(modules, "", ".impl")
+		props.Sdk_version = proptools.StringPtr("module_current")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
+
+	// Now create all-framework-module-impl, which contains updatable-framework-module-impl
+	// and all non-updatable modules. This module compiles against hidden APIs.
+	{
+		props := libraryProps{}
+		props.Name = proptools.StringPtr("all-framework-module-impl")
+		props.Static_libs = transformArray(non_updatable_modules, "", ".impl")
+		props.Static_libs = append(props.Static_libs, "updatable-framework-module-impl")
+		props.Sdk_version = proptools.StringPtr("core_platform")
+		props.Visibility = []string{"//frameworks/base"}
+		ctx.CreateModule(java.LibraryFactory, &props)
+	}
 }
 
 func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules []string) {
diff --git a/boot/Android.bp b/boot/Android.bp
index 9fdb9bc..6e52914 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -72,6 +72,10 @@
             module: "com.android.conscrypt-bootclasspath-fragment",
         },
         {
+            apex: "com.android.devicelock",
+            module: "com.android.devicelock-bootclasspath-fragment",
+        },
+        {
             apex: "com.android.federatedcompute",
             module: "com.android.federatedcompute-bootclasspath-fragment",
         },
@@ -132,6 +136,10 @@
             apex: "com.android.car.framework",
             module: "com.android.car.framework-bootclasspath-fragment",
         },
+        {
+            apex: "com.android.virt",
+            module: "com.android.virt-bootclasspath-fragment",
+        },
     ],
 
     // Additional information needed by hidden api processing.
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 8be8cda..c4d90c6 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -130,14 +130,14 @@
     uniform sampler2D uTexture;
     uniform float uFade;
     uniform float uColorProgress;
-    uniform vec4 uStartColor0;
-    uniform vec4 uStartColor1;
-    uniform vec4 uStartColor2;
-    uniform vec4 uStartColor3;
-    uniform vec4 uEndColor0;
-    uniform vec4 uEndColor1;
-    uniform vec4 uEndColor2;
-    uniform vec4 uEndColor3;
+    uniform vec3 uStartColor0;
+    uniform vec3 uStartColor1;
+    uniform vec3 uStartColor2;
+    uniform vec3 uStartColor3;
+    uniform vec3 uEndColor0;
+    uniform vec3 uEndColor1;
+    uniform vec3 uEndColor2;
+    uniform vec3 uEndColor3;
     varying highp vec2 vUv;
     void main() {
         vec4 mask = texture2D(uTexture, vUv);
@@ -150,12 +150,12 @@
             * step(cWhiteMaskThreshold, g)
             * step(cWhiteMaskThreshold, b)
             * step(cWhiteMaskThreshold, a);
-        vec4 color = r * mix(uStartColor0, uEndColor0, uColorProgress)
+        vec3 color = r * mix(uStartColor0, uEndColor0, uColorProgress)
                 + g * mix(uStartColor1, uEndColor1, uColorProgress)
                 + b * mix(uStartColor2, uEndColor2, uColorProgress)
                 + a * mix(uStartColor3, uEndColor3, uColorProgress);
-        color = mix(color, vec4(vec3((r + g + b + a) * 0.25), 1.0), useWhiteMask);
-        gl_FragColor = vec4(color.x, color.y, color.z, (1.0 - uFade)) * color.a;
+        color = mix(color, vec3((r + g + b + a) * 0.25), useWhiteMask);
+        gl_FragColor = vec4(color.x, color.y, color.z, (1.0 - uFade));
     })";
 static const char IMAGE_FRAG_SHADER_SOURCE[] = R"(
     precision mediump float;
@@ -1439,12 +1439,12 @@
     for (int i = 0; i < DYNAMIC_COLOR_COUNT; i++) {
         float *startColor = mAnimation->startColors[i];
         float *endColor = mAnimation->endColors[i];
-        glUniform4f(glGetUniformLocation(mImageShader,
+        glUniform3f(glGetUniformLocation(mImageShader,
             (U_START_COLOR_PREFIX + std::to_string(i)).c_str()),
-            startColor[0], startColor[1], startColor[2], 1 /* alpha */);
-        glUniform4f(glGetUniformLocation(mImageShader,
+            startColor[0], startColor[1], startColor[2]);
+        glUniform3f(glGetUniformLocation(mImageShader,
             (U_END_COLOR_PREFIX + std::to_string(i)).c_str()),
-            endColor[0], endColor[1], endColor[2], 1 /* alpha */);
+            endColor[0], endColor[1], endColor[2]);
     }
     mImageColorProgressLocation = glGetUniformLocation(mImageShader, U_COLOR_PROGRESS);
 }
diff --git a/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp
index 4f8faca..7a08cbd 100644
--- a/cmds/idmap2/Android.bp
+++ b/cmds/idmap2/Android.bp
@@ -222,6 +222,7 @@
     },
     data: [
         "tests/data/**/*.apk",
+        "tests/data/**/*.png",
     ],
     compile_multilib: "first",
     test_options: {
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp
index 4431164..10947dc 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.cpp
+++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp
@@ -39,6 +39,7 @@
 #include "idmap2/PrettyPrintVisitor.h"
 #include "idmap2/Result.h"
 #include "idmap2/SysTrace.h"
+#include <fcntl.h>
 
 using android::base::StringPrintf;
 using android::binder::Status;
@@ -238,6 +239,9 @@
     if (res.dataType == Res_value::TYPE_STRING) {
       builder.SetResourceValue(res.resourceName, res.dataType, res.stringData.value(),
             res.configuration.value_or(std::string()));
+    } else if (res.binaryData.has_value()) {
+      builder.SetResourceValue(res.resourceName, res.binaryData->get(),
+            res.configuration.value_or(std::string()));
     } else {
       builder.SetResourceValue(res.resourceName, res.dataType, res.data,
             res.configuration.value_or(std::string()));
@@ -264,6 +268,7 @@
                              file_name.c_str(), kMaxFileNameLength));
     }
   } while (std::filesystem::exists(path));
+  builder.setFrroPath(path);
 
   const uid_t uid = IPCThreadState::self()->getCallingUid();
   if (!UidHasWriteAccessToPath(uid, path)) {
diff --git a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl
index c773e11..3ad6d58 100644
--- a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl
+++ b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl
@@ -24,5 +24,6 @@
     int dataType;
     int data;
     @nullable @utf8InCpp String stringData;
+    @nullable ParcelFileDescriptor binaryData;
     @nullable @utf8InCpp String configuration;
 }
\ No newline at end of file
diff --git a/cmds/idmap2/include/idmap2/FabricatedOverlay.h b/cmds/idmap2/include/idmap2/FabricatedOverlay.h
index 05b0618..9f57710 100644
--- a/cmds/idmap2/include/idmap2/FabricatedOverlay.h
+++ b/cmds/idmap2/include/idmap2/FabricatedOverlay.h
@@ -28,6 +28,7 @@
 
 #include "idmap2/ResourceContainer.h"
 #include "idmap2/Result.h"
+#include <binder/ParcelFileDescriptor.h>
 
 namespace android::idmap2 {
 
@@ -45,6 +46,15 @@
                               const std::string& data_string_value,
                               const std::string& configuration);
 
+    Builder& SetResourceValue(const std::string& resource_name,
+                              std::optional<android::base::borrowed_fd>&& binary_value,
+                              const std::string& configuration);
+
+    inline Builder& setFrroPath(std::string frro_path) {
+      frro_path_ = std::move(frro_path);
+      return *this;
+    }
+
     WARN_UNUSED Result<FabricatedOverlay> Build();
 
    private:
@@ -53,6 +63,7 @@
       DataType data_type;
       DataValue data_value;
       std::string data_string_value;
+      std::optional<android::base::borrowed_fd> data_binary_value;
       std::string configuration;
     };
 
@@ -60,6 +71,7 @@
     std::string name_;
     std::string target_package_name_;
     std::string target_overlayable_;
+    std::string frro_path_;
     std::vector<Entry> entries_;
   };
 
@@ -79,10 +91,14 @@
 
   explicit FabricatedOverlay(pb::FabricatedOverlay&& overlay,
                              std::string&& string_pool_data_,
+                             std::vector<android::base::borrowed_fd> binary_files_,
+                             off_t total_binary_bytes_,
                              std::optional<uint32_t> crc_from_disk = {});
 
   pb::FabricatedOverlay overlay_pb_;
   std::string string_pool_data_;
+  std::vector<android::base::borrowed_fd> binary_files_;
+  uint32_t total_binary_bytes_;
   std::optional<uint32_t> crc_from_disk_;
   mutable std::optional<SerializedData> data_;
 
diff --git a/cmds/idmap2/include/idmap2/ResourceUtils.h b/cmds/idmap2/include/idmap2/ResourceUtils.h
index af4dd89..2214a83 100644
--- a/cmds/idmap2/include/idmap2/ResourceUtils.h
+++ b/cmds/idmap2/include/idmap2/ResourceUtils.h
@@ -19,6 +19,7 @@
 
 #include <optional>
 #include <string>
+#include <android-base/unique_fd.h>
 
 #include "androidfw/AssetManager2.h"
 #include "idmap2/Result.h"
@@ -41,6 +42,7 @@
   DataType data_type;
   DataValue data_value;
   std::string data_string_value;
+  std::optional<android::base::borrowed_fd> data_binary_value;
 };
 
 struct TargetValueWithConfig {
diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp
index bde9b0b..d517e29 100644
--- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp
+++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp
@@ -16,6 +16,10 @@
 
 #include "idmap2/FabricatedOverlay.h"
 
+#include <sys/stat.h>   // umask
+#include <sys/types.h>  // umask
+
+#include <android-base/file.h>
 #include <androidfw/ResourceUtils.h>
 #include <androidfw/StringPool.h>
 #include <google/protobuf/io/coded_stream.h>
@@ -51,9 +55,13 @@
 
 FabricatedOverlay::FabricatedOverlay(pb::FabricatedOverlay&& overlay,
                                      std::string&& string_pool_data,
+                                     std::vector<android::base::borrowed_fd> binary_files,
+                                     off_t total_binary_bytes,
                                      std::optional<uint32_t> crc_from_disk)
     : overlay_pb_(std::forward<pb::FabricatedOverlay>(overlay)),
     string_pool_data_(std::move(string_pool_data)),
+    binary_files_(std::move(binary_files)),
+    total_binary_bytes_(total_binary_bytes),
     crc_from_disk_(crc_from_disk) {
 }
 
@@ -72,14 +80,23 @@
 FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue(
     const std::string& resource_name, uint8_t data_type, uint32_t data_value,
     const std::string& configuration) {
-  entries_.emplace_back(Entry{resource_name, data_type, data_value, "", configuration});
+  entries_.emplace_back(
+      Entry{resource_name, data_type, data_value, "", std::nullopt, configuration});
   return *this;
 }
 
 FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue(
     const std::string& resource_name, uint8_t data_type, const std::string& data_string_value,
     const std::string& configuration) {
-  entries_.emplace_back(Entry{resource_name, data_type, 0, data_string_value, configuration});
+  entries_.emplace_back(
+      Entry{resource_name, data_type, 0, data_string_value, std::nullopt, configuration});
+  return *this;
+}
+
+FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue(
+    const std::string& resource_name, std::optional<android::base::borrowed_fd>&& binary_value,
+    const std::string& configuration) {
+  entries_.emplace_back(Entry{resource_name, 0, 0, "", binary_value, configuration});
   return *this;
 }
 
@@ -135,7 +152,7 @@
     }
 
     value->second = TargetValue{res_entry.data_type, res_entry.data_value,
-        res_entry.data_string_value};
+        res_entry.data_string_value, res_entry.data_binary_value};
   }
 
   pb::FabricatedOverlay overlay_pb;
@@ -144,6 +161,11 @@
   overlay_pb.set_target_package_name(target_package_name_);
   overlay_pb.set_target_overlayable(target_overlayable_);
 
+  std::vector<android::base::borrowed_fd> binary_files;
+  size_t total_binary_bytes = 0;
+  // 16 for the number of bytes in the frro file before the binary data
+  const size_t FRRO_HEADER_SIZE = 16;
+
   for (auto& package : package_map) {
     auto package_pb = overlay_pb.add_packages();
     package_pb->set_name(package.first);
@@ -162,6 +184,20 @@
           if (value.second.data_type == Res_value::TYPE_STRING) {
             auto ref = string_pool.MakeRef(value.second.data_string_value);
             pb_value->set_data_value(ref.index());
+          } else if (value.second.data_binary_value.has_value()) {
+              pb_value->set_data_type(Res_value::TYPE_STRING);
+              struct stat s;
+              if (fstat(value.second.data_binary_value->get(), &s) == -1) {
+                return Error("unable to get size of binary file: %d", errno);
+              }
+              std::string uri
+                  = StringPrintf("frro:/%s?offset=%d&size=%d", frro_path_.c_str(),
+                                 static_cast<int> (FRRO_HEADER_SIZE + total_binary_bytes),
+                                 static_cast<int> (s.st_size));
+              total_binary_bytes += s.st_size;
+              binary_files.emplace_back(value.second.data_binary_value->get());
+              auto ref = string_pool.MakeRef(std::move(uri));
+              pb_value->set_data_value(ref.index());
           } else {
             pb_value->set_data_value(value.second.data_value);
           }
@@ -169,10 +205,10 @@
       }
     }
   }
-
   android::BigBuffer string_buffer(kBufferSize);
   android::StringPool::FlattenUtf8(&string_buffer, string_pool, nullptr);
-  return FabricatedOverlay(std::move(overlay_pb), string_buffer.to_string());
+  return FabricatedOverlay(std::move(overlay_pb), string_buffer.to_string(),
+      std::move(binary_files), total_binary_bytes);
 }
 
 Result<FabricatedOverlay> FabricatedOverlay::FromBinaryStream(std::istream& stream) {
@@ -190,7 +226,7 @@
     return Error("Failed to read fabricated overlay version.");
   }
 
-  if (version != 1 && version != 2) {
+  if (version < 1 || version > 3) {
     return Error("Invalid fabricated overlay version '%u'.", version);
   }
 
@@ -201,7 +237,14 @@
 
   pb::FabricatedOverlay overlay{};
   std::string sp_data;
-  if (version == 2) {
+  uint32_t total_binary_bytes;
+  if (version == 3) {
+    if (!Read32(stream, &total_binary_bytes)) {
+      return Error("Failed read total binary bytes.");
+    }
+    stream.seekg(total_binary_bytes, std::istream::cur);
+  }
+  if (version >= 2) {
     uint32_t sp_size;
     if (!Read32(stream, &sp_size)) {
       return Error("Failed read string pool size.");
@@ -211,20 +254,15 @@
       return Error("Failed to read string pool.");
     }
     sp_data = buf;
-
-    if (!overlay.ParseFromIstream(&stream)) {
-      return Error("Failed read fabricated overlay proto.");
-    }
-  } else {
-    if (!overlay.ParseFromIstream(&stream)) {
-      return Error("Failed read fabricated overlay proto.");
-    }
+  }
+  if (!overlay.ParseFromIstream(&stream)) {
+    return Error("Failed read fabricated overlay proto.");
   }
 
   // If the proto version is the latest version, then the contents of the proto must be the same
   // when the proto is re-serialized; otherwise, the crc must be calculated because migrating the
   // proto to the latest version will likely change the contents of the fabricated overlay.
-  return FabricatedOverlay(std::move(overlay), std::move(sp_data),
+  return FabricatedOverlay(std::move(overlay), std::move(sp_data), {}, total_binary_bytes,
                            version == kFabricatedOverlayCurrentVersion
                                                    ? std::optional<uint32_t>(crc)
                                                    : std::nullopt);
@@ -274,6 +312,14 @@
   Write32(stream, kFabricatedOverlayMagic);
   Write32(stream, kFabricatedOverlayCurrentVersion);
   Write32(stream, (*data)->pb_crc);
+  Write32(stream, total_binary_bytes_);
+  std::string file_contents;
+  for (const android::base::borrowed_fd fd : binary_files_) {
+    if (!ReadFdToString(fd, &file_contents)) {
+      return Error("Failed to read binary file data.");
+    }
+    stream.write(file_contents.data(), file_contents.length());
+  }
   Write32(stream, (*data)->sp_data.length());
   stream.write((*data)->sp_data.data(), (*data)->sp_data.length());
   if (stream.bad()) {
diff --git a/cmds/idmap2/tests/FabricatedOverlayTests.cpp b/cmds/idmap2/tests/FabricatedOverlayTests.cpp
index e804c87..e13a0eb 100644
--- a/cmds/idmap2/tests/FabricatedOverlayTests.cpp
+++ b/cmds/idmap2/tests/FabricatedOverlayTests.cpp
@@ -17,6 +17,7 @@
 #include <android-base/file.h>
 #include <gtest/gtest.h>
 #include <idmap2/FabricatedOverlay.h>
+#include "TestHelpers.h"
 
 #include <fstream>
 #include <utility>
@@ -41,6 +42,10 @@
 }
 
 TEST(FabricatedOverlayTests, SetResourceValue) {
+  auto path = GetTestDataPath() + "/overlay/res/drawable/android.png";
+  auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC));
+  ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path;
+
   auto overlay =
       FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "com.example.target")
           .SetResourceValue(
@@ -54,6 +59,8 @@
               Res_value::TYPE_STRING,
               "foobar",
               "en-rUS-normal-xxhdpi-v21")
+          .SetResourceValue("com.example.target:drawable/dr1", fd, "port-xxhdpi-v7")
+          .setFrroPath("/foo/bar/biz.frro")
           .Build();
   ASSERT_TRUE(overlay);
   auto container = FabricatedOverlayContainer::FromOverlay(std::move(*overlay));
@@ -67,19 +74,28 @@
 
   auto pairs = container->GetOverlayData(*info);
   ASSERT_TRUE(pairs);
-  ASSERT_EQ(4U, pairs->pairs.size());
+  ASSERT_EQ(5U, pairs->pairs.size());
   auto string_pool = ResStringPool(pairs->string_pool_data->data.get(),
                                         pairs->string_pool_data->data_length, false);
 
   auto& it = pairs->pairs[0];
-  ASSERT_EQ("com.example.target:integer/int1", it.resource_name);
+  ASSERT_EQ("com.example.target:drawable/dr1", it.resource_name);
   auto entry = std::get_if<TargetValueWithConfig>(&it.value);
   ASSERT_NE(nullptr, entry);
+  ASSERT_EQ(std::string("frro://foo/bar/biz.frro?offset=16&size=8341"),
+      string_pool.string8At(entry->value.data_value).value_or(""));
+  ASSERT_EQ(Res_value::TYPE_STRING, entry->value.data_type);
+  ASSERT_EQ("port-xxhdpi-v7", entry->config);
+
+  it = pairs->pairs[1];
+  ASSERT_EQ("com.example.target:integer/int1", it.resource_name);
+  entry = std::get_if<TargetValueWithConfig>(&it.value);
+  ASSERT_NE(nullptr, entry);
   ASSERT_EQ(1U, entry->value.data_value);
   ASSERT_EQ(Res_value::TYPE_INT_DEC, entry->value.data_type);
   ASSERT_EQ("port", entry->config);
 
-  it = pairs->pairs[1];
+  it = pairs->pairs[2];
   ASSERT_EQ("com.example.target:string/int3", it.resource_name);
   entry = std::get_if<TargetValueWithConfig>(&it.value);
   ASSERT_NE(nullptr, entry);
@@ -87,7 +103,7 @@
   ASSERT_EQ(Res_value::TYPE_REFERENCE, entry->value.data_type);
   ASSERT_EQ("xxhdpi-v7", entry->config);
 
-  it = pairs->pairs[2];
+  it = pairs->pairs[3];
   ASSERT_EQ("com.example.target:string/string1", it.resource_name);
   entry = std::get_if<TargetValueWithConfig>(&it.value);
   ASSERT_NE(nullptr, entry);
@@ -95,7 +111,7 @@
   ASSERT_EQ(std::string("foobar"), string_pool.string8At(entry->value.data_value).value_or(""));
   ASSERT_EQ("en-rUS-normal-xxhdpi-v21", entry->config);
 
-  it = pairs->pairs[3];
+  it = pairs->pairs[4];
   ASSERT_EQ("com.example.target.split:integer/int2", it.resource_name);
   entry = std::get_if<TargetValueWithConfig>(&it.value);
   ASSERT_NE(nullptr, entry);
diff --git a/cmds/idmap2/tests/IdmapTests.cpp b/cmds/idmap2/tests/IdmapTests.cpp
index 7b7dc17..b473f26 100644
--- a/cmds/idmap2/tests/IdmapTests.cpp
+++ b/cmds/idmap2/tests/IdmapTests.cpp
@@ -260,11 +260,17 @@
   auto target = TargetResourceContainer::FromPath(target_apk_path);
   ASSERT_TRUE(target);
 
+  auto path = GetTestDataPath() + "/overlay/res/drawable/android.png";
+  auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC));
+  ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path;
+
   auto frro = FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "test.target")
                   .SetOverlayable("TestResources")
                   .SetResourceValue("integer/int1", Res_value::TYPE_INT_DEC, 2U, "land-xxhdpi-v7")
                   .SetResourceValue("string/str1", Res_value::TYPE_REFERENCE, 0x7f010000, "land")
                   .SetResourceValue("string/str2", Res_value::TYPE_STRING, "foobar", "xxhdpi-v7")
+                  .SetResourceValue("drawable/dr1", fd, "port-xxhdpi-v7")
+                  .setFrroPath("/foo/bar/biz.frro")
                   .Build();
 
   ASSERT_TRUE(frro);
@@ -293,14 +299,19 @@
   auto string_pool_data = data->GetStringPoolData();
   auto string_pool = ResStringPool(string_pool_data.data(), string_pool_data.size(), false);
 
+  std::u16string expected_uri = u"frro://foo/bar/biz.frro?offset=16&size=8341";
+  uint32_t uri_index
+      = string_pool.indexOfString(expected_uri.data(), expected_uri.length()).value_or(-1);
 
   const auto& target_inline_entries = data->GetTargetInlineEntries();
-  ASSERT_EQ(target_inline_entries.size(), 3U);
-  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[0], R::target::integer::int1, "land-xxhdpi-v7",
+  ASSERT_EQ(target_inline_entries.size(), 4U);
+  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[0], R::target::drawable::dr1, "port-xxhdpi-v7",
+                             Res_value::TYPE_STRING, uri_index);
+  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[1], R::target::integer::int1, "land-xxhdpi-v7",
                              Res_value::TYPE_INT_DEC, 2U);
-  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[1], R::target::string::str1, "land",
+  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[2], R::target::string::str1, "land",
                              Res_value::TYPE_REFERENCE, 0x7f010000);
-  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[2], R::target::string::str2, "xxhdpi-v7",
+  ASSERT_TARGET_INLINE_ENTRY(target_inline_entries[3], R::target::string::str2, "xxhdpi-v7",
                              Res_value::TYPE_STRING,
                              (uint32_t) (string_pool.indexOfString(u"foobar", 6)).value_or(-1));
 }
diff --git a/cmds/idmap2/tests/R.h b/cmds/idmap2/tests/R.h
index ad998b9..80c062d 100644
--- a/cmds/idmap2/tests/R.h
+++ b/cmds/idmap2/tests/R.h
@@ -26,24 +26,27 @@
 // clang-format off
 namespace R::target {
   namespace integer {  // NOLINT(runtime/indentation_namespace)
-    constexpr ResourceId int1 = 0x7f010000;
+    constexpr ResourceId int1 = 0x7f020000;
+  }
+  namespace drawable {
+    constexpr ResourceId dr1 = 0x7f010000;
   }
   namespace string {  // NOLINT(runtime/indentation_namespace)
-    constexpr ResourceId not_overlayable = 0x7f020003;
-    constexpr ResourceId other = 0x7f020004;
-    constexpr ResourceId policy_actor = 0x7f020005;
-    constexpr ResourceId policy_config_signature = 0x7f020006;
-    constexpr ResourceId policy_odm = 0x7f020007;
-    constexpr ResourceId policy_oem = 0x7f020008;
-    constexpr ResourceId policy_product = 0x7f020009;
-    constexpr ResourceId policy_public = 0x7f02000a;
-    constexpr ResourceId policy_signature = 0x7f02000b;
-    constexpr ResourceId policy_system = 0x7f02000c;
-    constexpr ResourceId policy_system_vendor = 0x7f02000d;
-    constexpr ResourceId str1 = 0x7f02000e;
-    constexpr ResourceId str2 = 0x7f02000f;
-    constexpr ResourceId str3 = 0x7f020010;
-    constexpr ResourceId str4 = 0x7f020011;
+    constexpr ResourceId not_overlayable = 0x7f030003;
+    constexpr ResourceId other = 0x7f030004;
+    constexpr ResourceId policy_actor = 0x7f030005;
+    constexpr ResourceId policy_config_signature = 0x7f030006;
+    constexpr ResourceId policy_odm = 0x7f030007;
+    constexpr ResourceId policy_oem = 0x7f030008;
+    constexpr ResourceId policy_product = 0x7f030009;
+    constexpr ResourceId policy_public = 0x7f03000a;
+    constexpr ResourceId policy_signature = 0x7f03000b;
+    constexpr ResourceId policy_system = 0x7f03000c;
+    constexpr ResourceId policy_system_vendor = 0x7f03000d;
+    constexpr ResourceId str1 = 0x7f03000e;
+    constexpr ResourceId str2 = 0x7f03000f;
+    constexpr ResourceId str3 = 0x7f030010;
+    constexpr ResourceId str4 = 0x7f030011;
   }  // namespace string
 }  // namespace R::target
 
diff --git a/cmds/idmap2/tests/RawPrintVisitorTests.cpp b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
index 7112eeb..68164e2 100644
--- a/cmds/idmap2/tests/RawPrintVisitorTests.cpp
+++ b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
@@ -79,22 +79,22 @@
   ASSERT_CONTAINS_REGEX(ADDRESS "00000000  config count", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "00000004  overlay entry count", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "0000000a  string pool index offset", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  target id: integer/int1", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  target id: integer/int1", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  overlay id: integer/int1", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f02000e  target id: string/str1", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f03000e  target id: string/str1", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000b  overlay id: string/str1", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f020010  target id: string/str3", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f030010  target id: string/str3", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000c  overlay id: string/str3", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f020011  target id: string/str4", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f030011  target id: string/str4", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000d  overlay id: string/str4", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  overlay id: integer/int1", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  target id: integer/int1", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  target id: integer/int1", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000b  overlay id: string/str1", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f02000e  target id: string/str1", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f03000e  target id: string/str1", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000c  overlay id: string/str3", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f020010  target id: string/str3", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f030010  target id: string/str3", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f02000d  overlay id: string/str4", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "7f020011  target id: string/str4", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f030011  target id: string/str4", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "000000b4  string pool size", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "........  string pool", stream.str());
 }
diff --git a/cmds/idmap2/tests/ResourceMappingTests.cpp b/cmds/idmap2/tests/ResourceMappingTests.cpp
index 016d427..380e462 100644
--- a/cmds/idmap2/tests/ResourceMappingTests.cpp
+++ b/cmds/idmap2/tests/ResourceMappingTests.cpp
@@ -23,6 +23,7 @@
 #include <memory>
 #include <string>
 
+#include <fcntl.h>
 #include "R.h"
 #include "TestConstants.h"
 #include "TestHelpers.h"
@@ -76,7 +77,12 @@
   auto target_map = mapping.GetTargetToOverlayMap();
   auto entry_map = target_map.find(target_resource);
   if (entry_map == target_map.end()) {
-    return Error("Failed to find mapping for target resource");
+    std::string keys;
+    for (const auto &pair : target_map) {
+      keys.append(fmt::format("0x{:x}", pair.first)).append(" ");
+    }
+    return Error(R"(Failed to find mapping for target resource "0x%02x": "%s")",
+        target_resource, keys.c_str());
   }
 
   auto actual_overlay_resource = std::get_if<ResourceId>(&entry_map->second);
@@ -108,7 +114,12 @@
   auto target_map = mapping.GetTargetToOverlayMap();
   auto entry_map = target_map.find(target_resource);
   if (entry_map == target_map.end()) {
-    return Error("Failed to find mapping for target resource");
+    std::string keys;
+    for (const auto &pair : target_map) {
+      keys.append(fmt::format("{:x}", pair.first)).append(" ");
+    }
+    return Error(R"(Failed to find mapping for target resource "0x%02x": "%s")",
+        target_resource, keys.c_str());
   }
 
   auto config_map = std::get_if<ConfigMap>(&entry_map->second);
@@ -193,11 +204,16 @@
 }
 
 TEST(ResourceMappingTests, FabricatedOverlay) {
+  auto path = GetTestDataPath() + "/overlay/res/drawable/android.png";
+  auto fd = android::base::unique_fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC));
+  ASSERT_TRUE(fd > 0) << "errno " << errno << " for path " << path;
   auto frro = FabricatedOverlay::Builder("com.example.overlay", "SandTheme", "test.target")
                   .SetOverlayable("TestResources")
                   .SetResourceValue("integer/int1", Res_value::TYPE_INT_DEC, 2U, "")
                   .SetResourceValue("string/str1", Res_value::TYPE_REFERENCE, 0x7f010000, "")
                   .SetResourceValue("string/str2", Res_value::TYPE_STRING, "foobar", "")
+                  .SetResourceValue("drawable/dr1", fd, "")
+                  .setFrroPath("/foo/bar/biz.frro")
                   .Build();
 
   ASSERT_TRUE(frro);
@@ -214,11 +230,16 @@
   auto string_pool_data = res.GetStringPoolData();
   auto string_pool = ResStringPool(string_pool_data.data(), string_pool_data.size(), false);
 
-  ASSERT_EQ(res.GetTargetToOverlayMap().size(), 3U);
+  std::u16string expected_uri = u"frro://foo/bar/biz.frro?offset=16&size=8341";
+  uint32_t uri_index
+      = string_pool.indexOfString(expected_uri.data(), expected_uri.length()).value_or(-1);
+
+  ASSERT_EQ(res.GetTargetToOverlayMap().size(), 4U);
   ASSERT_EQ(res.GetOverlayToTargetMap().size(), 0U);
   ASSERT_RESULT(MappingExists(res, R::target::string::str1, Res_value::TYPE_REFERENCE, 0x7f010000));
   ASSERT_RESULT(MappingExists(res, R::target::string::str2, Res_value::TYPE_STRING,
                               (uint32_t) (string_pool.indexOfString(u"foobar", 6)).value_or(-1)));
+  ASSERT_RESULT(MappingExists(res, R::target::drawable::dr1, Res_value::TYPE_STRING, uri_index));
   ASSERT_RESULT(MappingExists(res, R::target::integer::int1, Res_value::TYPE_INT_DEC, 2U));
 }
 
diff --git a/cmds/idmap2/tests/TestConstants.h b/cmds/idmap2/tests/TestConstants.h
index d5799ad..794d622 100644
--- a/cmds/idmap2/tests/TestConstants.h
+++ b/cmds/idmap2/tests/TestConstants.h
@@ -19,8 +19,8 @@
 
 namespace android::idmap2::TestConstants {
 
-constexpr const auto TARGET_CRC = 0x7c2d4719;
-constexpr const auto TARGET_CRC_STRING = "7c2d4719";
+constexpr const auto TARGET_CRC = 0xa960a69;
+constexpr const auto TARGET_CRC_STRING = "0a960a69";
 
 constexpr const auto OVERLAY_CRC = 0xb71095cf;
 constexpr const auto OVERLAY_CRC_STRING = "b71095cf";
diff --git a/cmds/idmap2/tests/data/overlay/res/drawable/android.png b/cmds/idmap2/tests/data/overlay/res/drawable/android.png
new file mode 100644
index 0000000..b7317b0
--- /dev/null
+++ b/cmds/idmap2/tests/data/overlay/res/drawable/android.png
Binary files differ
diff --git a/cmds/idmap2/tests/data/target/build b/cmds/idmap2/tests/data/target/build
index e6df742..cd13a7e 100755
--- a/cmds/idmap2/tests/data/target/build
+++ b/cmds/idmap2/tests/data/target/build
@@ -17,5 +17,7 @@
 rm compiled.flata
 
 aapt2 compile res/values/values.xml -o .
-aapt2 link --manifest AndroidManifest.xml -A assets -o target-no-overlayable.apk values_values.arsc.flat
-rm values_values.arsc.flat
\ No newline at end of file
+aapt2 compile res/drawable/dr1.png -o .
+aapt2 link --manifest AndroidManifest.xml -A assets -o target-no-overlayable.apk values_values.arsc.flat drawable_dr1.png.flat
+rm values_values.arsc.flat
+rm drawable_dr1.png.flat
diff --git a/cmds/idmap2/tests/data/target/res/drawable/dr1.png b/cmds/idmap2/tests/data/target/res/drawable/dr1.png
new file mode 100644
index 0000000..1a56e68
--- /dev/null
+++ b/cmds/idmap2/tests/data/target/res/drawable/dr1.png
Binary files differ
diff --git a/cmds/idmap2/tests/data/target/res/values/overlayable.xml b/cmds/idmap2/tests/data/target/res/values/overlayable.xml
index 57e6c43..aac9081 100644
--- a/cmds/idmap2/tests/data/target/res/values/overlayable.xml
+++ b/cmds/idmap2/tests/data/target/res/values/overlayable.xml
@@ -63,6 +63,7 @@
         <item type="string" name="y" />
         <item type="string" name="z" />
         <item type="integer" name="int1" />
+        <item type="drawable" name="dr1" />
     </policy>
 </overlayable>
 
diff --git a/cmds/idmap2/tests/data/target/target-no-overlayable.apk b/cmds/idmap2/tests/data/target/target-no-overlayable.apk
index cc3491d..680eeb6 100644
--- a/cmds/idmap2/tests/data/target/target-no-overlayable.apk
+++ b/cmds/idmap2/tests/data/target/target-no-overlayable.apk
Binary files differ
diff --git a/cmds/idmap2/tests/data/target/target.apk b/cmds/idmap2/tests/data/target/target.apk
index 4a58c5e..145e737 100644
--- a/cmds/idmap2/tests/data/target/target.apk
+++ b/cmds/idmap2/tests/data/target/target.apk
Binary files differ
diff --git a/cmds/uiautomator/OWNERS b/cmds/uiautomator/OWNERS
new file mode 100644
index 0000000..5c7f452
--- /dev/null
+++ b/cmds/uiautomator/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 833089
+peykov@google.com
+normancheung@google.com
+guran@google.com
diff --git a/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java b/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
index 3b14be7..24727c5 100644
--- a/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
+++ b/cmds/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
@@ -107,7 +107,7 @@
                         DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
                 int rotation = display.getRotation();
                 Point size = new Point();
-                display.getSize(size);
+                display.getRealSize(size);
                 AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x,
                         size.y);
             }
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
index ab198b3..488292d 100644
--- a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
@@ -139,7 +139,7 @@
                 serializer.attribute("", "id", Integer.toString(displayId));
                 int rotation = display.getRotation();
                 Point size = new Point();
-                display.getSize(size);
+                display.getRealSize(size);
                 for (int i = 0, n = windows.size(); i < n; ++i) {
                     dumpWindowRec(windows.get(i), serializer, i, size.x, size.y, rotation);
                 }
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
index 6fd2bf2..1bcd343e 100644
--- a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
@@ -767,7 +767,7 @@
         if(root != null) {
             Display display = getAutomatorBridge().getDefaultDisplay();
             Point size = new Point();
-            display.getSize(size);
+            display.getRealSize(size);
             AccessibilityNodeInfoDumper.dumpWindowToFile(root,
                     new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
                     display.getRotation(), size.x, size.y);
diff --git a/core/api/current.txt b/core/api/current.txt
index 9c6dac2..f80c095 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34,6 +34,7 @@
     field public static final String BIND_COMPANION_DEVICE_SERVICE = "android.permission.BIND_COMPANION_DEVICE_SERVICE";
     field public static final String BIND_CONDITION_PROVIDER_SERVICE = "android.permission.BIND_CONDITION_PROVIDER_SERVICE";
     field public static final String BIND_CONTROLS = "android.permission.BIND_CONTROLS";
+    field public static final String BIND_CREDENTIAL_PROVIDER_SERVICE = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE";
     field public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
     field public static final String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE";
     field public static final String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE";
@@ -83,12 +84,25 @@
     field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES";
     field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES";
     field public static final String DELIVER_COMPANION_MESSAGES = "android.permission.DELIVER_COMPANION_MESSAGES";
+    field public static final String DETECT_SCREEN_CAPTURE = "android.permission.DETECT_SCREEN_CAPTURE";
     field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC";
     field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD";
     field public static final String DUMP = "android.permission.DUMP";
     field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR";
     field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST";
     field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE";
+    field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA";
+    field public static final String FOREGROUND_SERVICE_CONNECTED_DEVICE = "android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE";
+    field public static final String FOREGROUND_SERVICE_DATA_SYNC = "android.permission.FOREGROUND_SERVICE_DATA_SYNC";
+    field public static final String FOREGROUND_SERVICE_HEALTH = "android.permission.FOREGROUND_SERVICE_HEALTH";
+    field public static final String FOREGROUND_SERVICE_LOCATION = "android.permission.FOREGROUND_SERVICE_LOCATION";
+    field public static final String FOREGROUND_SERVICE_MEDIA_PLAYBACK = "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK";
+    field public static final String FOREGROUND_SERVICE_MEDIA_PROJECTION = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION";
+    field public static final String FOREGROUND_SERVICE_MICROPHONE = "android.permission.FOREGROUND_SERVICE_MICROPHONE";
+    field public static final String FOREGROUND_SERVICE_PHONE_CALL = "android.permission.FOREGROUND_SERVICE_PHONE_CALL";
+    field public static final String FOREGROUND_SERVICE_REMOTE_MESSAGING = "android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING";
+    field public static final String FOREGROUND_SERVICE_SPECIAL_USE = "android.permission.FOREGROUND_SERVICE_SPECIAL_USE";
+    field public static final String FOREGROUND_SERVICE_SYSTEM_EXEMPTED = "android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED";
     field public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS";
     field public static final String GET_ACCOUNTS_PRIVILEGED = "android.permission.GET_ACCOUNTS_PRIVILEGED";
     field public static final String GET_PACKAGE_SIZE = "android.permission.GET_PACKAGE_SIZE";
@@ -106,6 +120,7 @@
     field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
+    field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE";
     field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
     field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
     field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA";
@@ -141,6 +156,7 @@
     field public static final String READ_MEDIA_AUDIO = "android.permission.READ_MEDIA_AUDIO";
     field public static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
     field public static final String READ_MEDIA_VIDEO = "android.permission.READ_MEDIA_VIDEO";
+    field public static final String READ_MEDIA_VISUAL_USER_SELECTED = "android.permission.READ_MEDIA_VISUAL_USER_SELECTED";
     field public static final String READ_NEARBY_STREAMING_POLICY = "android.permission.READ_NEARBY_STREAMING_POLICY";
     field public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS";
     field public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
@@ -170,6 +186,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";
@@ -1490,6 +1507,7 @@
     field public static final int targetCellWidth = 16844340; // 0x1010634
     field public static final int targetClass = 16842799; // 0x101002f
     field @Deprecated public static final int targetDescriptions = 16843680; // 0x10103a0
+    field public static final int targetDisplayCategory;
     field public static final int targetId = 16843740; // 0x10103dc
     field public static final int targetName = 16843853; // 0x101044d
     field public static final int targetPackage = 16842785; // 0x1010021
@@ -3106,10 +3124,13 @@
     method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
     method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
     method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
+    method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
     field public static final int ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR = 1; // 0x1
     field public static final int ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT = 3; // 0x3
     field public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4; // 0x4
+    field public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5; // 0x5
     field public static final int ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS = 2; // 0x2
+    field public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6; // 0x6
     field public static final int GESTURE_2_FINGER_DOUBLE_TAP = 20; // 0x14
     field public static final int GESTURE_2_FINGER_DOUBLE_TAP_AND_HOLD = 40; // 0x28
     field public static final int GESTURE_2_FINGER_SINGLE_TAP = 19; // 0x13
@@ -4600,23 +4621,24 @@
 
   public class AlarmManager {
     method public boolean canScheduleExactAlarms();
-    method public void cancel(android.app.PendingIntent);
-    method public void cancel(android.app.AlarmManager.OnAlarmListener);
+    method public void cancel(@NonNull android.app.PendingIntent);
+    method public void cancel(@NonNull android.app.AlarmManager.OnAlarmListener);
     method public void cancelAll();
     method public android.app.AlarmManager.AlarmClockInfo getNextAlarmClock();
-    method public void set(int, long, android.app.PendingIntent);
-    method public void set(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
-    method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(android.app.AlarmManager.AlarmClockInfo, android.app.PendingIntent);
-    method public void setAndAllowWhileIdle(int, long, android.app.PendingIntent);
-    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, android.app.PendingIntent);
-    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
-    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, android.app.PendingIntent);
-    method public void setInexactRepeating(int, long, long, android.app.PendingIntent);
-    method public void setRepeating(int, long, long, android.app.PendingIntent);
+    method public void set(int, long, @NonNull android.app.PendingIntent);
+    method public void set(int, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler);
+    method @RequiresPermission(android.Manifest.permission.SCHEDULE_EXACT_ALARM) public void setAlarmClock(@NonNull android.app.AlarmManager.AlarmClockInfo, @NonNull android.app.PendingIntent);
+    method public void setAndAllowWhileIdle(int, long, @NonNull android.app.PendingIntent);
+    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, @NonNull android.app.PendingIntent);
+    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExact(int, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler);
+    method @RequiresPermission(value=android.Manifest.permission.SCHEDULE_EXACT_ALARM, conditional=true) public void setExactAndAllowWhileIdle(int, long, @NonNull android.app.PendingIntent);
+    method public void setInexactRepeating(int, long, long, @NonNull android.app.PendingIntent);
+    method public void setRepeating(int, long, long, @NonNull android.app.PendingIntent);
     method @RequiresPermission(android.Manifest.permission.SET_TIME) public void setTime(long);
     method @RequiresPermission(android.Manifest.permission.SET_TIME_ZONE) public void setTimeZone(String);
-    method public void setWindow(int, long, long, android.app.PendingIntent);
-    method public void setWindow(int, long, long, String, android.app.AlarmManager.OnAlarmListener, android.os.Handler);
+    method public void setWindow(int, long, long, @NonNull android.app.PendingIntent);
+    method public void setWindow(int, long, long, @Nullable String, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler);
+    method public void setWindow(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.app.AlarmManager.OnAlarmListener);
     field public static final String ACTION_NEXT_ALARM_CLOCK_CHANGED = "android.app.action.NEXT_ALARM_CLOCK_CHANGED";
     field public static final String ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED = "android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED";
     field public static final int ELAPSED_REALTIME = 3; // 0x3
@@ -5268,6 +5290,13 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.ForegroundServiceStartNotAllowedException> CREATOR;
   }
 
+  public final class ForegroundServiceTypeNotAllowedException extends android.app.ServiceStartNotAllowedException implements android.os.Parcelable {
+    ctor public ForegroundServiceTypeNotAllowedException(@NonNull String);
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.ForegroundServiceTypeNotAllowedException> CREATOR;
+  }
+
   @Deprecated public class Fragment implements android.content.ComponentCallbacks2 android.view.View.OnCreateContextMenuListener {
     ctor @Deprecated public Fragment();
     method @Deprecated public void dump(String, java.io.FileDescriptor, java.io.PrintWriter, String[]);
@@ -6929,7 +6958,7 @@
     method public void onTrimMemory(int);
     method public boolean onUnbind(android.content.Intent);
     method public final void startForeground(int, android.app.Notification);
-    method public final void startForeground(int, @NonNull android.app.Notification, int);
+    method public final void startForeground(int, @NonNull android.app.Notification, @RequiresPermission int);
     method @Deprecated public final void stopForeground(boolean);
     method public final void stopForeground(int);
     method public final void stopSelf();
@@ -7453,6 +7482,7 @@
     method public long getMaximumTimeToLock(@Nullable android.content.ComponentName);
     method @NonNull public java.util.List<java.lang.String> getMeteredDataDisabledPackages(@NonNull android.content.ComponentName);
     method public int getMinimumRequiredWifiSecurityLevel();
+    method public int getMtePolicy();
     method @RequiresPermission(value=android.Manifest.permission.READ_NEARBY_STREAMING_POLICY, conditional=true) public int getNearbyAppStreamingPolicy();
     method @RequiresPermission(value=android.Manifest.permission.READ_NEARBY_STREAMING_POLICY, conditional=true) public int getNearbyNotificationStreamingPolicy();
     method @Deprecated @ColorInt public int getOrganizationColor(@NonNull android.content.ComponentName);
@@ -7601,6 +7631,7 @@
     method public void setMaximumTimeToLock(@NonNull android.content.ComponentName, long);
     method @NonNull public java.util.List<java.lang.String> setMeteredDataDisabledPackages(@NonNull android.content.ComponentName, @NonNull java.util.List<java.lang.String>);
     method public void setMinimumRequiredWifiSecurityLevel(int);
+    method public void setMtePolicy(int);
     method public void setNearbyAppStreamingPolicy(int);
     method public void setNearbyNotificationStreamingPolicy(int);
     method public void setNetworkLoggingEnabled(@Nullable android.content.ComponentName, boolean);
@@ -7660,6 +7691,7 @@
     method public boolean updateOverrideApn(@NonNull android.content.ComponentName, int, @NonNull android.telephony.data.ApnSetting);
     method public void wipeData(int);
     method public void wipeData(int, @NonNull CharSequence);
+    method public void wipeDevice(int);
     field public static final String ACTION_ADD_DEVICE_ADMIN = "android.app.action.ADD_DEVICE_ADMIN";
     field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
     field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
@@ -7784,6 +7816,9 @@
     field public static final int LOCK_TASK_FEATURE_SYSTEM_INFO = 1; // 0x1
     field public static final int MAKE_USER_EPHEMERAL = 2; // 0x2
     field public static final String MIME_TYPE_PROVISIONING_NFC = "application/com.android.managedprovisioning";
+    field public static final int MTE_DISABLED = 2; // 0x2
+    field public static final int MTE_ENABLED = 1; // 0x1
+    field public static final int MTE_NOT_CONTROLLED_BY_POLICY = 0; // 0x0
     field public static final int NEARBY_STREAMING_DISABLED = 1; // 0x1
     field public static final int NEARBY_STREAMING_ENABLED = 2; // 0x2
     field public static final int NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY = 0; // 0x0
@@ -8436,19 +8471,31 @@
 
   public abstract class JobService extends android.app.Service {
     ctor public JobService();
+    method public long getTransferredDownloadBytes();
+    method public long getTransferredDownloadBytes(@NonNull android.app.job.JobWorkItem);
+    method public long getTransferredUploadBytes();
+    method public long getTransferredUploadBytes(@NonNull android.app.job.JobWorkItem);
     method public final void jobFinished(android.app.job.JobParameters, boolean);
     method public final android.os.IBinder onBind(android.content.Intent);
     method public abstract boolean onStartJob(android.app.job.JobParameters);
     method public abstract boolean onStopJob(android.app.job.JobParameters);
+    method public final void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, long, long);
+    method public final void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
+    method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, long, long);
+    method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
     field public static final String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
   }
 
   public abstract class JobServiceEngine {
     ctor public JobServiceEngine(android.app.Service);
     method public final android.os.IBinder getBinder();
+    method public long getTransferredDownloadBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem);
+    method public long getTransferredUploadBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem);
     method public void jobFinished(android.app.job.JobParameters, boolean);
     method public abstract boolean onStartJob(android.app.job.JobParameters);
     method public abstract boolean onStopJob(android.app.job.JobParameters);
+    method public void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
+    method public void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, @Nullable android.app.job.JobWorkItem, long, long);
   }
 
   public final class JobWorkItem implements android.os.Parcelable {
@@ -8945,9 +8992,18 @@
 
 package android.companion {
 
+  public final class AssociatedDevice implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.bluetooth.le.ScanResult getBleDevice();
+    method @Nullable public android.bluetooth.BluetoothDevice getBluetoothDevice();
+    method @Nullable public android.net.wifi.ScanResult getWifiDevice();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.AssociatedDevice> CREATOR;
+  }
+
   public final class AssociationInfo implements android.os.Parcelable {
     method public int describeContents();
-    method @Nullable public android.os.Parcelable getAssociatedDevice();
+    method @Nullable public android.companion.AssociatedDevice getAssociatedDevice();
     method @Nullable public android.net.MacAddress getDeviceMacAddress();
     method @Nullable public String getDeviceProfile();
     method @Nullable public CharSequence getDisplayName();
@@ -9265,6 +9321,7 @@
     field public static final int CLASSIFICATION_NOT_COMPLETE = 1; // 0x1
     field public static final int CLASSIFICATION_NOT_PERFORMED = 2; // 0x2
     field @NonNull public static final android.os.Parcelable.Creator<android.content.ClipDescription> CREATOR;
+    field public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE";
     field public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE";
     field public static final String MIMETYPE_TEXT_HTML = "text/html";
     field public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent";
@@ -9844,6 +9901,7 @@
     field public static final int CONTEXT_RESTRICTED = 4; // 0x4
     field public static final String CREDENTIAL_SERVICE = "credential";
     field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
+    field public static final String DEVICE_LOCK_SERVICE = "device_lock";
     field public static final String DEVICE_POLICY_SERVICE = "device_policy";
     field public static final String DISPLAY_HASH_SERVICE = "display_hash";
     field public static final String DISPLAY_SERVICE = "display";
@@ -10370,6 +10428,7 @@
     field public static final String ACTION_SEND_MULTIPLE = "android.intent.action.SEND_MULTIPLE";
     field public static final String ACTION_SET_WALLPAPER = "android.intent.action.SET_WALLPAPER";
     field public static final String ACTION_SHOW_APP_INFO = "android.intent.action.SHOW_APP_INFO";
+    field public static final String ACTION_SHOW_OUTPUT_SWITCHER = "android.intent.action.SHOW_OUTPUT_SWITCHER";
     field public static final String ACTION_SHOW_WORK_APPS = "android.intent.action.SHOW_WORK_APPS";
     field public static final String ACTION_SHUTDOWN = "android.intent.action.ACTION_SHUTDOWN";
     field public static final String ACTION_SYNC = "android.intent.action.SYNC";
@@ -11135,6 +11194,7 @@
     field public int screenOrientation;
     field public int softInputMode;
     field public String targetActivity;
+    field @Nullable public String targetDisplayCategory;
     field public String taskAffinity;
     field public int theme;
     field public int uiOptions;
@@ -11642,7 +11702,7 @@
   public static final class PackageInstaller.PreapprovalDetails implements android.os.Parcelable {
     method public int describeContents();
     method @Nullable public android.graphics.Bitmap getIcon();
-    method @NonNull public String getLabel();
+    method @NonNull public CharSequence getLabel();
     method @NonNull public android.icu.util.ULocale getLocale();
     method @NonNull public String getPackageName();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -11653,7 +11713,7 @@
     ctor public PackageInstaller.PreapprovalDetails.Builder();
     method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails build();
     method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(@NonNull android.graphics.Bitmap);
-    method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(@NonNull String);
+    method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(@NonNull CharSequence);
     method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(@NonNull android.icu.util.ULocale);
     method @NonNull public android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(@NonNull String);
   }
@@ -11990,6 +12050,7 @@
     field public static final String FEATURE_CONTROLS = "android.software.controls";
     field public static final String FEATURE_CREDENTIALS = "android.software.credentials";
     field public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin";
+    field public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
     field public static final String FEATURE_EMBEDDED = "android.hardware.type.embedded";
     field public static final String FEATURE_ETHERNET = "android.hardware.ethernet";
     field public static final String FEATURE_EXPANDED_PICTURE_IN_PICTURE = "android.software.expanded_picture_in_picture";
@@ -12084,6 +12145,7 @@
     field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND = "android.hardware.touchscreen.multitouch.jazzhand";
     field public static final String FEATURE_USB_ACCESSORY = "android.hardware.usb.accessory";
     field public static final String FEATURE_USB_HOST = "android.hardware.usb.host";
+    field public static final String FEATURE_UWB = "android.hardware.uwb";
     field public static final String FEATURE_VERIFIED_BOOT = "android.software.verified_boot";
     field public static final String FEATURE_VR_HEADTRACKING = "android.hardware.vr.headtracking";
     field @Deprecated public static final String FEATURE_VR_MODE = "android.software.vr.mode";
@@ -12145,6 +12207,7 @@
     field public static final int PERMISSION_DENIED = -1; // 0xffffffff
     field public static final int PERMISSION_GRANTED = 0; // 0x0
     field public static final String PROPERTY_MEDIA_CAPABILITIES = "android.media.PROPERTY_MEDIA_CAPABILITIES";
+    field public static final String PROPERTY_SPECIAL_USE_FGS_SUBTYPE = "android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE";
     field public static final int SIGNATURE_FIRST_NOT_SIGNED = -1; // 0xffffffff
     field public static final int SIGNATURE_MATCH = 0; // 0x0
     field public static final int SIGNATURE_NEITHER_SIGNED = 1; // 0x1
@@ -12358,16 +12421,20 @@
     field public static final int FLAG_SINGLE_USER = 1073741824; // 0x40000000
     field public static final int FLAG_STOP_WITH_TASK = 1; // 0x1
     field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8
-    field public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40
-    field public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
-    field public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1
-    field public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10
+    field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8
     field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff
-    field public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2
-    field public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20
-    field public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80
-    field public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0
-    field public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4
+    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2
+    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 32; // 0x20
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE}, anyOf={android.Manifest.permission.CAPTURE_AUDIO_OUTPUT, android.Manifest.permission.RECORD_AUDIO}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 128; // 0x80
+    field @Deprecated public static final int FOREGROUND_SERVICE_TYPE_NONE = 0; // 0x0
+    field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL}, anyOf={android.Manifest.permission.MANAGE_OWN_CALLS}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4; // 0x4
+    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 512; // 0x200
+    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1073741824; // 0x40000000
+    field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024; // 0x400
     field public int flags;
     field public String permission;
   }
@@ -16559,6 +16626,7 @@
     field public static final int FONT_WEIGHT_NORMAL = 400; // 0x190
     field public static final int FONT_WEIGHT_SEMI_BOLD = 600; // 0x258
     field public static final int FONT_WEIGHT_THIN = 100; // 0x64
+    field public static final int FONT_WEIGHT_UNSPECIFIED = -1; // 0xffffffff
   }
 
   public final class FontVariationAxis {
@@ -17583,10 +17651,12 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Rational> CONTROL_AE_COMPENSATION_STEP;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> CONTROL_AE_LOCK_AVAILABLE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AF_AVAILABLE_MODES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> CONTROL_AUTOFRAMING_AVAILABLE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_EFFECTS;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.Capability[]> CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_CAPABILITIES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_SCENE_MODES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_SETTINGS_OVERRIDES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AWB_AVAILABLE_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> CONTROL_AWB_LOCK_AVAILABLE;
@@ -17626,6 +17696,7 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REPROCESS_MAX_CAPTURE_STALL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> REQUEST_AVAILABLE_CAPABILITIES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.ColorSpaceProfiles> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.DynamicRangeProfiles> REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REQUEST_MAX_NUM_INPUT_STREAMS;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REQUEST_MAX_NUM_OUTPUT_PROC;
@@ -17744,6 +17815,7 @@
     method @NonNull public <T> java.util.List<android.util.Size> getExtensionSupportedSizes(int, @NonNull Class<T>);
     method @NonNull public java.util.List<android.util.Size> getExtensionSupportedSizes(int, int);
     method @NonNull public java.util.List<java.lang.Integer> getSupportedExtensions();
+    method public boolean isCaptureProcessProgressAvailable(int);
     field public static final int EXTENSION_AUTOMATIC = 0; // 0x0
     field @Deprecated public static final int EXTENSION_BEAUTY = 1; // 0x1
     field public static final int EXTENSION_BOKEH = 2; // 0x2
@@ -17763,6 +17835,7 @@
   public abstract static class CameraExtensionSession.ExtensionCaptureCallback {
     ctor public CameraExtensionSession.ExtensionCaptureCallback();
     method public void onCaptureFailed(@NonNull android.hardware.camera2.CameraExtensionSession, @NonNull android.hardware.camera2.CaptureRequest);
+    method public void onCaptureProcessProgressed(@NonNull android.hardware.camera2.CameraExtensionSession, @NonNull android.hardware.camera2.CaptureRequest, @IntRange(from=0, to=100) int);
     method public void onCaptureProcessStarted(@NonNull android.hardware.camera2.CameraExtensionSession, @NonNull android.hardware.camera2.CaptureRequest);
     method public void onCaptureResultAvailable(@NonNull android.hardware.camera2.CameraExtensionSession, @NonNull android.hardware.camera2.CaptureRequest, @NonNull android.hardware.camera2.TotalCaptureResult);
     method public void onCaptureSequenceAborted(@NonNull android.hardware.camera2.CameraExtensionSession, int);
@@ -17883,6 +17956,12 @@
     field public static final int CONTROL_AF_TRIGGER_CANCEL = 2; // 0x2
     field public static final int CONTROL_AF_TRIGGER_IDLE = 0; // 0x0
     field public static final int CONTROL_AF_TRIGGER_START = 1; // 0x1
+    field public static final int CONTROL_AUTOFRAMING_AUTO = 2; // 0x2
+    field public static final int CONTROL_AUTOFRAMING_OFF = 0; // 0x0
+    field public static final int CONTROL_AUTOFRAMING_ON = 1; // 0x1
+    field public static final int CONTROL_AUTOFRAMING_STATE_CONVERGED = 2; // 0x2
+    field public static final int CONTROL_AUTOFRAMING_STATE_FRAMING = 1; // 0x1
+    field public static final int CONTROL_AUTOFRAMING_STATE_INACTIVE = 0; // 0x0
     field public static final int CONTROL_AWB_MODE_AUTO = 1; // 0x1
     field public static final int CONTROL_AWB_MODE_CLOUDY_DAYLIGHT = 6; // 0x6
     field public static final int CONTROL_AWB_MODE_DAYLIGHT = 5; // 0x5
@@ -17940,6 +18019,8 @@
     field public static final int CONTROL_SCENE_MODE_STEADYPHOTO = 11; // 0xb
     field public static final int CONTROL_SCENE_MODE_SUNSET = 10; // 0xa
     field public static final int CONTROL_SCENE_MODE_THEATRE = 7; // 0x7
+    field public static final int CONTROL_SETTINGS_OVERRIDE_OFF = 0; // 0x0
+    field public static final int CONTROL_SETTINGS_OVERRIDE_ZOOM = 1; // 0x1
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0; // 0x0
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_ON = 1; // 0x1
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION = 2; // 0x2
@@ -17989,6 +18070,7 @@
     field public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; // 0x4
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE = 0; // 0x0
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE = 6; // 0x6
+    field public static final int REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES = 20; // 0x14
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO = 9; // 0x9
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT = 8; // 0x8
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT = 18; // 0x12
@@ -18127,6 +18209,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AF_REGIONS;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_TRIGGER;
+    field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AUTOFRAMING;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Boolean> CONTROL_AWB_LOCK;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AWB_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS;
@@ -18137,6 +18220,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_POST_RAW_SENSITIVITY_BOOST;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SCENE_MODE;
+    field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR;
@@ -18216,6 +18300,8 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_SCENE_CHANGE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_STATE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AF_TRIGGER;
+    field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AUTOFRAMING;
+    field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AUTOFRAMING_STATE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Boolean> CONTROL_AWB_LOCK;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AWB_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS;
@@ -18227,6 +18313,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_POST_RAW_SENSITIVITY_BOOST;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SCENE_MODE;
+    field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
@@ -18340,6 +18427,15 @@
     method @NonNull public android.util.Range<java.lang.Float> getZoomRatioRange();
   }
 
+  public final class ColorSpaceProfiles {
+    ctor public ColorSpaceProfiles(@NonNull long[]);
+    method @NonNull public java.util.Set<android.graphics.ColorSpace.Named> getSupportedColorSpaces(int);
+    method @NonNull public java.util.Set<android.graphics.ColorSpace.Named> getSupportedColorSpacesForDynamicRange(int, long);
+    method @NonNull public java.util.Set<java.lang.Long> getSupportedDynamicRangeProfiles(@NonNull android.graphics.ColorSpace.Named, int);
+    method @NonNull public java.util.Set<java.lang.Integer> getSupportedImageFormatsForColorSpace(@NonNull android.graphics.ColorSpace.Named);
+    field public static final int UNSPECIFIED = -1; // 0xffffffff
+  }
+
   public final class ColorSpaceTransform {
     ctor public ColorSpaceTransform(android.util.Rational[]);
     ctor public ColorSpaceTransform(int[]);
@@ -18394,7 +18490,7 @@
     method public android.graphics.Point getLeftEyePosition();
     method public android.graphics.Point getMouthPosition();
     method public android.graphics.Point getRightEyePosition();
-    method public int getScore();
+    method @IntRange(from=android.hardware.camera2.params.Face.SCORE_MIN, to=android.hardware.camera2.params.Face.SCORE_MAX) public int getScore();
     field public static final int ID_UNSUPPORTED = -1; // 0xffffffff
     field public static final int SCORE_MAX = 100; // 0x64
     field public static final int SCORE_MIN = 1; // 0x1
@@ -18409,7 +18505,7 @@
     method @NonNull public android.hardware.camera2.params.Face.Builder setLeftEyePosition(@NonNull android.graphics.Point);
     method @NonNull public android.hardware.camera2.params.Face.Builder setMouthPosition(@NonNull android.graphics.Point);
     method @NonNull public android.hardware.camera2.params.Face.Builder setRightEyePosition(@NonNull android.graphics.Point);
-    method @NonNull public android.hardware.camera2.params.Face.Builder setScore(int);
+    method @NonNull public android.hardware.camera2.params.Face.Builder setScore(@IntRange(from=android.hardware.camera2.params.Face.SCORE_MIN, to=android.hardware.camera2.params.Face.SCORE_MAX) int);
   }
 
   public final class InputConfiguration {
@@ -18571,13 +18667,16 @@
 
   public final class SessionConfiguration implements android.os.Parcelable {
     ctor public SessionConfiguration(int, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
+    method public void clearColorSpace();
     method public int describeContents();
+    method @Nullable public android.graphics.ColorSpace getColorSpace();
     method public java.util.concurrent.Executor getExecutor();
     method public android.hardware.camera2.params.InputConfiguration getInputConfiguration();
     method public java.util.List<android.hardware.camera2.params.OutputConfiguration> getOutputConfigurations();
     method public android.hardware.camera2.CaptureRequest getSessionParameters();
     method public int getSessionType();
     method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback();
+    method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
     method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration);
     method public void setSessionParameters(android.hardware.camera2.CaptureRequest);
     method public void writeToParcel(android.os.Parcel, int);
@@ -19447,6 +19546,7 @@
 
   public final class GnssCapabilities implements android.os.Parcelable {
     method public int describeContents();
+    method @NonNull public java.util.List<android.location.GnssSignalType> getGnssSignalTypes();
     method public boolean hasAntennaInfo();
     method public boolean hasGeofencing();
     method @Deprecated public boolean hasGnssAntennaInfo();
@@ -19471,7 +19571,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;
   }
@@ -19480,6 +19580,7 @@
     ctor public GnssCapabilities.Builder();
     ctor public GnssCapabilities.Builder(@NonNull android.location.GnssCapabilities);
     method @NonNull public android.location.GnssCapabilities build();
+    method @NonNull public android.location.GnssCapabilities.Builder setGnssSignalTypes(@NonNull java.util.List<android.location.GnssSignalType>);
     method @NonNull public android.location.GnssCapabilities.Builder setHasAntennaInfo(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasGeofencing(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasLowPowerMode(boolean);
@@ -19503,7 +19604,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 {
@@ -19692,6 +19793,16 @@
     field @Deprecated public static final int STATUS_READY = 1; // 0x1
   }
 
+  public final class GnssSignalType implements android.os.Parcelable {
+    method @NonNull public static android.location.GnssSignalType create(int, @FloatRange(from=0.0f, fromInclusive=false) double, @NonNull String);
+    method public int describeContents();
+    method @FloatRange(from=0.0f, fromInclusive=false) public double getCarrierFrequencyHz();
+    method @NonNull public String getCodeType();
+    method public int getConstellationType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssSignalType> CREATOR;
+  }
+
   public final class GnssStatus implements android.os.Parcelable {
     method public int describeContents();
     method @FloatRange(from=0, to=360) public float getAzimuthDegrees(@IntRange(from=0) int);
@@ -19701,7 +19812,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);
@@ -23192,6 +23303,7 @@
     method public int describeContents();
     method @Nullable public String getClientPackageName();
     method public int getConnectionState();
+    method @NonNull public java.util.Set<java.lang.String> getDeduplicationIds();
     method @Nullable public CharSequence getDescription();
     method @Nullable public android.os.Bundle getExtras();
     method @NonNull public java.util.List<java.lang.String> getFeatures();
@@ -23225,6 +23337,7 @@
     method @NonNull public android.media.MediaRoute2Info.Builder clearFeatures();
     method @NonNull public android.media.MediaRoute2Info.Builder setClientPackageName(@Nullable String);
     method @NonNull public android.media.MediaRoute2Info.Builder setConnectionState(int);
+    method @NonNull public android.media.MediaRoute2Info.Builder setDeduplicationIds(@NonNull java.util.Set<java.lang.String>);
     method @NonNull public android.media.MediaRoute2Info.Builder setDescription(@Nullable CharSequence);
     method @NonNull public android.media.MediaRoute2Info.Builder setExtras(@Nullable android.os.Bundle);
     method @NonNull public android.media.MediaRoute2Info.Builder setIconUri(@Nullable android.net.Uri);
@@ -26484,6 +26597,7 @@
     method public boolean onKeyMultiple(int, int, @NonNull android.view.KeyEvent);
     method public boolean onKeyUp(int, @NonNull android.view.KeyEvent);
     method public void onMediaViewSizeChanged(@Px int, @Px int);
+    method public void onRecordingStarted(@NonNull String);
     method public abstract void onRelease();
     method public void onResetInteractiveApp();
     method public abstract boolean onSetSurface(@Nullable android.view.Surface);
@@ -26509,6 +26623,7 @@
     method @CallSuper public void requestCurrentChannelUri();
     method @CallSuper public void requestCurrentTvInputId();
     method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+    method @CallSuper public void requestStartRecording(@Nullable android.net.Uri);
     method @CallSuper public void requestStreamVolume();
     method @CallSuper public void requestTrackInfoList();
     method @CallSuper public void sendPlaybackCommandRequest(@NonNull String, @Nullable android.os.Bundle);
@@ -26540,6 +26655,7 @@
     method public boolean dispatchUnhandledInputEvent(@NonNull android.view.InputEvent);
     method @Nullable public android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
     method public void notifyError(@NonNull String, @NonNull android.os.Bundle);
+    method public void notifyRecordingStarted(@NonNull String);
     method public void onAttachedToWindow();
     method public void onDetachedFromWindow();
     method public void onLayout(boolean, int, int, int, int);
@@ -26581,6 +26697,7 @@
     method public void onRequestCurrentChannelUri(@NonNull String);
     method public void onRequestCurrentTvInputId(@NonNull String);
     method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+    method public void onRequestStartRecording(@NonNull String, @Nullable android.net.Uri);
     method public void onRequestStreamVolume(@NonNull String);
     method public void onRequestTrackInfoList(@NonNull String);
     method public void onSetVideoBounds(@NonNull String, @NonNull android.graphics.Rect);
@@ -32104,7 +32221,12 @@
   public static class PerformanceHintManager.Session implements java.io.Closeable {
     method public void close();
     method public void reportActualWorkDuration(long);
+    method public void sendHint(int);
     method public void updateTargetWorkDuration(long);
+    field public static final int CPU_LOAD_DOWN = 1; // 0x1
+    field public static final int CPU_LOAD_RESET = 2; // 0x2
+    field public static final int CPU_LOAD_RESUME = 3; // 0x3
+    field public static final int CPU_LOAD_UP = 0; // 0x0
   }
 
   public final class PersistableBundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable {
@@ -32203,6 +32325,7 @@
     method public static final boolean is64Bit();
     method public static boolean isApplicationUid(int);
     method public static final boolean isIsolated();
+    method public static final boolean isIsolatedUid(int);
     method public static final boolean isSdkSandbox();
     method public static final void killProcess(int);
     method public static final int myPid();
@@ -32477,7 +32600,7 @@
     method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle);
     method public android.os.Bundle getUserRestrictions();
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle);
-    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.List<android.os.UserHandle> getVisibleUsers();
+    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers();
     method public boolean hasUserRestriction(String);
     method public boolean isDemoUser();
     method public static boolean isHeadlessSystemUserMode();
@@ -32511,6 +32634,7 @@
     field public static final String DISALLOW_BLUETOOTH = "no_bluetooth";
     field public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing";
     field public static final String DISALLOW_CAMERA_TOGGLE = "disallow_camera_toggle";
+    field public static final String DISALLOW_CELLULAR_2G = "no_cellular_2g";
     field public static final String DISALLOW_CHANGE_WIFI_STATE = "no_change_wifi_state";
     field public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
     field public static final String DISALLOW_CONFIG_BRIGHTNESS = "no_config_brightness";
@@ -32553,6 +32677,7 @@
     field public static final String DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI = "no_sharing_admin_configured_wifi";
     field public static final String DISALLOW_SMS = "no_sms";
     field public static final String DISALLOW_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs";
+    field public static final String DISALLOW_ULTRA_WIDEBAND_RADIO = "no_ultra_wideband_radio";
     field public static final String DISALLOW_UNIFIED_PASSWORD = "no_unified_password";
     field public static final String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps";
     field public static final String DISALLOW_UNMUTE_MICROPHONE = "no_unmute_microphone";
@@ -35735,6 +35860,7 @@
     field public static final String ACTION_MANAGE_UNKNOWN_APP_SOURCES = "android.settings.MANAGE_UNKNOWN_APP_SOURCES";
     field public static final String ACTION_MANAGE_WRITE_SETTINGS = "android.settings.action.MANAGE_WRITE_SETTINGS";
     field public static final String ACTION_MEMORY_CARD_SETTINGS = "android.settings.MEMORY_CARD_SETTINGS";
+    field public static final String ACTION_MEMTAG_SETTINGS = "android.settings.MEMTAG_SETTINGS";
     field public static final String ACTION_NETWORK_OPERATOR_SETTINGS = "android.settings.NETWORK_OPERATOR_SETTINGS";
     field public static final String ACTION_NFCSHARING_SETTINGS = "android.settings.NFCSHARING_SETTINGS";
     field public static final String ACTION_NFC_PAYMENT_SETTINGS = "android.settings.NFC_PAYMENT_SETTINGS";
@@ -39317,6 +39443,7 @@
     method public final void setNotificationsShown(String[]);
     method public final void snoozeNotification(String, long);
     method public final void updateNotificationChannel(@NonNull String, @NonNull android.os.UserHandle, @NonNull android.app.NotificationChannel);
+    field public static final String ACTION_SETTINGS_HOME = "android.service.notification.action.SETTINGS_HOME";
     field public static final int FLAG_FILTER_TYPE_ALERTING = 2; // 0x2
     field public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; // 0x1
     field public static final int FLAG_FILTER_TYPE_ONGOING = 8; // 0x8
@@ -39840,7 +39967,7 @@
   public abstract class WallpaperService extends android.app.Service {
     ctor public WallpaperService();
     method public final android.os.IBinder onBind(android.content.Intent);
-    method public abstract android.service.wallpaper.WallpaperService.Engine onCreateEngine();
+    method @MainThread public abstract android.service.wallpaper.WallpaperService.Engine onCreateEngine();
     field public static final String SERVICE_INTERFACE = "android.service.wallpaper.WallpaperService";
     field public static final String SERVICE_META_DATA = "android.service.wallpaper";
   }
@@ -39855,20 +39982,20 @@
     method public boolean isPreview();
     method public boolean isVisible();
     method public void notifyColorsChanged();
-    method public void onApplyWindowInsets(android.view.WindowInsets);
-    method public android.os.Bundle onCommand(String, int, int, int, android.os.Bundle, boolean);
-    method @Nullable public android.app.WallpaperColors onComputeColors();
-    method public void onCreate(android.view.SurfaceHolder);
-    method public void onDesiredSizeChanged(int, int);
-    method public void onDestroy();
-    method public void onOffsetsChanged(float, float, float, float, int, int);
-    method public void onSurfaceChanged(android.view.SurfaceHolder, int, int, int);
-    method public void onSurfaceCreated(android.view.SurfaceHolder);
-    method public void onSurfaceDestroyed(android.view.SurfaceHolder);
-    method public void onSurfaceRedrawNeeded(android.view.SurfaceHolder);
-    method public void onTouchEvent(android.view.MotionEvent);
-    method public void onVisibilityChanged(boolean);
-    method public void onZoomChanged(@FloatRange(from=0.0f, to=1.0f) float);
+    method @MainThread public void onApplyWindowInsets(android.view.WindowInsets);
+    method @MainThread public android.os.Bundle onCommand(String, int, int, int, android.os.Bundle, boolean);
+    method @MainThread @Nullable public android.app.WallpaperColors onComputeColors();
+    method @MainThread public void onCreate(android.view.SurfaceHolder);
+    method @MainThread public void onDesiredSizeChanged(int, int);
+    method @MainThread public void onDestroy();
+    method @MainThread public void onOffsetsChanged(float, float, float, float, int, int);
+    method @MainThread public void onSurfaceChanged(android.view.SurfaceHolder, int, int, int);
+    method @MainThread public void onSurfaceCreated(android.view.SurfaceHolder);
+    method @MainThread public void onSurfaceDestroyed(android.view.SurfaceHolder);
+    method @MainThread public void onSurfaceRedrawNeeded(android.view.SurfaceHolder);
+    method @MainThread public void onTouchEvent(android.view.MotionEvent);
+    method @MainThread public void onVisibilityChanged(boolean);
+    method @MainThread public void onZoomChanged(@FloatRange(from=0.0f, to=1.0f) float);
     method public void setOffsetNotificationsEnabled(boolean);
     method public void setTouchEventsEnabled(boolean);
   }
@@ -40303,6 +40430,7 @@
     field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE";
     field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE";
     field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID";
+    field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
     field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
     field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED";
     field public static final String EXTRA_SUGGESTED_PHONE_ACCOUNTS = "android.telecom.extra.SUGGESTED_PHONE_ACCOUNTS";
@@ -41556,7 +41684,7 @@
     field public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool";
     field public static final String KEY_CARRIER_CONFIG_VERSION_STRING = "carrier_config_version_string";
     field public static final String KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL = "carrier_cross_sim_ims_available_bool";
-    field public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = "carrier_data_call_permanent_failure_strings";
+    field @Deprecated public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = "carrier_data_call_permanent_failure_strings";
     field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_DCFAILURE_STRING_ARRAY = "carrier_default_actions_on_dcfailure_string_array";
     field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_DEFAULT_NETWORK_AVAILABLE = "carrier_default_actions_on_default_network_available_string_array";
     field public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY = "carrier_default_actions_on_redirection_string_array";
@@ -41655,7 +41783,7 @@
     field public static final String KEY_GSM_ROAMING_NETWORKS_STRING_ARRAY = "gsm_roaming_networks_string_array";
     field public static final String KEY_HAS_IN_CALL_NOISE_SUPPRESSION_BOOL = "has_in_call_noise_suppression_bool";
     field public static final String KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL = "hide_carrier_network_settings_bool";
-    field public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
+    field @Deprecated public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
     field public static final String KEY_HIDE_ENHANCED_4G_LTE_BOOL = "hide_enhanced_4g_lte_bool";
     field public static final String KEY_HIDE_IMS_APN_BOOL = "hide_ims_apn_bool";
     field public static final String KEY_HIDE_LTE_PLUS_DATA_ICON_BOOL = "hide_lte_plus_data_icon_bool";
@@ -41693,6 +41821,7 @@
     field public static final String KEY_MMS_MMS_READ_REPORT_ENABLED_BOOL = "enableMMSReadReports";
     field public static final String KEY_MMS_MULTIPART_SMS_ENABLED_BOOL = "enableMultipartSMS";
     field public static final String KEY_MMS_NAI_SUFFIX_STRING = "naiSuffix";
+    field public static final String KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT = "mms_network_release_timeout_millis_int";
     field public static final String KEY_MMS_NOTIFY_WAP_MMSC_ENABLED_BOOL = "enabledNotifyWapMMSC";
     field public static final String KEY_MMS_RECIPIENT_LIMIT_INT = "recipientLimit";
     field public static final String KEY_MMS_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_BOOL = "sendMultipartSmsAsSeparateMessages";
@@ -41723,10 +41852,14 @@
     field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long";
     field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
     field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
+    field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_daily_notification_count_int";
+    field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT = "premium_capability_maximum_monthly_notification_count_int";
+    field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING = "premium_capability_purchase_url_string";
+    field public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = "premium_capability_supported_on_lte_bool";
     field public static final String KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL = "prevent_clir_activation_and_deactivation_code_bool";
     field public static final String KEY_RADIO_RESTART_FAILURE_CAUSES_INT_ARRAY = "radio_restart_failure_causes_int_array";
     field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string";
@@ -41790,6 +41923,9 @@
     field public static final String KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL = "voicemail_notification_persistent_bool";
     field public static final String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool";
     field public static final String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int";
+    field public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
+    field public static final String KEY_VONR_ON_BY_DEFAULT_BOOL = "vonr_on_by_default_bool";
+    field public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
     field public static final String KEY_VT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_RTT_CALL_BOOL = "vt_upgrade_supported_for_downgraded_rtt_call";
     field public static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool";
     field public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
@@ -43377,14 +43513,18 @@
     field public static final int RESULT_RECEIVE_WHILE_ENCRYPTED = 504; // 0x1f8
     field public static final int RESULT_REMOTE_EXCEPTION = 31; // 0x1f
     field public static final int RESULT_REQUEST_NOT_SUPPORTED = 24; // 0x18
+    field public static final int RESULT_RIL_ABORTED = 137; // 0x89
     field public static final int RESULT_RIL_ACCESS_BARRED = 122; // 0x7a
     field public static final int RESULT_RIL_BLOCKED_DUE_TO_CALL = 123; // 0x7b
     field public static final int RESULT_RIL_CANCELLED = 119; // 0x77
+    field public static final int RESULT_RIL_DEVICE_IN_USE = 136; // 0x88
     field public static final int RESULT_RIL_ENCODING_ERR = 109; // 0x6d
     field public static final int RESULT_RIL_GENERIC_ERROR = 124; // 0x7c
     field public static final int RESULT_RIL_INTERNAL_ERR = 113; // 0x71
     field public static final int RESULT_RIL_INVALID_ARGUMENTS = 104; // 0x68
     field public static final int RESULT_RIL_INVALID_MODEM_STATE = 115; // 0x73
+    field public static final int RESULT_RIL_INVALID_RESPONSE = 125; // 0x7d
+    field public static final int RESULT_RIL_INVALID_SIM_STATE = 130; // 0x82
     field public static final int RESULT_RIL_INVALID_SMSC_ADDRESS = 110; // 0x6e
     field public static final int RESULT_RIL_INVALID_SMS_FORMAT = 107; // 0x6b
     field public static final int RESULT_RIL_INVALID_STATE = 103; // 0x67
@@ -43393,14 +43533,23 @@
     field public static final int RESULT_RIL_NETWORK_NOT_READY = 116; // 0x74
     field public static final int RESULT_RIL_NETWORK_REJECT = 102; // 0x66
     field public static final int RESULT_RIL_NO_MEMORY = 105; // 0x69
+    field public static final int RESULT_RIL_NO_NETWORK_FOUND = 135; // 0x87
     field public static final int RESULT_RIL_NO_RESOURCES = 118; // 0x76
+    field public static final int RESULT_RIL_NO_SMS_TO_ACK = 131; // 0x83
+    field public static final int RESULT_RIL_NO_SUBSCRIPTION = 134; // 0x86
     field public static final int RESULT_RIL_OPERATION_NOT_ALLOWED = 117; // 0x75
     field public static final int RESULT_RIL_RADIO_NOT_AVAILABLE = 100; // 0x64
     field public static final int RESULT_RIL_REQUEST_NOT_SUPPORTED = 114; // 0x72
     field public static final int RESULT_RIL_REQUEST_RATE_LIMITED = 106; // 0x6a
     field public static final int RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED = 121; // 0x79
     field public static final int RESULT_RIL_SIM_ABSENT = 120; // 0x78
+    field public static final int RESULT_RIL_SIM_BUSY = 132; // 0x84
+    field public static final int RESULT_RIL_SIM_ERROR = 129; // 0x81
+    field public static final int RESULT_RIL_SIM_FULL = 133; // 0x85
+    field public static final int RESULT_RIL_SIM_PIN2 = 126; // 0x7e
+    field public static final int RESULT_RIL_SIM_PUK2 = 127; // 0x7f
     field public static final int RESULT_RIL_SMS_SEND_FAIL_RETRY = 101; // 0x65
+    field public static final int RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE = 128; // 0x80
     field public static final int RESULT_RIL_SYSTEM_ERR = 108; // 0x6c
     field public static final int RESULT_SMS_BLOCKED_DURING_EMERGENCY = 29; // 0x1d
     field public static final int RESULT_SMS_SEND_RETRY_FAILED = 30; // 0x1e
@@ -43891,6 +44040,8 @@
     field public static final int APPTYPE_USIM = 2; // 0x2
     field public static final int AUTHTYPE_EAP_AKA = 129; // 0x81
     field public static final int AUTHTYPE_EAP_SIM = 128; // 0x80
+    field public static final int AUTHTYPE_GBA_BOOTSTRAP = 132; // 0x84
+    field public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = 133; // 0x85
     field public static final int CALL_COMPOSER_STATUS_OFF = 0; // 0x0
     field public static final int CALL_COMPOSER_STATUS_ON = 1; // 0x1
     field public static final int CALL_STATE_IDLE = 0; // 0x0
@@ -43975,7 +44126,7 @@
     field public static final long NETWORK_TYPE_BITMASK_HSUPA = 256L; // 0x100L
     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
@@ -43992,7 +44143,7 @@
     field public static final int NETWORK_TYPE_HSPA = 10; // 0xa
     field public static final int NETWORK_TYPE_HSPAP = 15; // 0xf
     field public static final int NETWORK_TYPE_HSUPA = 9; // 0x9
-    field public static final int NETWORK_TYPE_IDEN = 11; // 0xb
+    field @Deprecated public static final int NETWORK_TYPE_IDEN = 11; // 0xb
     field public static final int NETWORK_TYPE_IWLAN = 18; // 0x12
     field public static final int NETWORK_TYPE_LTE = 13; // 0xd
     field public static final int NETWORK_TYPE_NR = 20; // 0x14
@@ -44003,20 +44154,22 @@
     field public static final int PHONE_TYPE_GSM = 1; // 0x1
     field public static final int PHONE_TYPE_NONE = 0; // 0x0
     field public static final int PHONE_TYPE_SIP = 3; // 0x3
-    field public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; // 0x1
+    field public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; // 0x4
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR = 8; // 0x8
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
-    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14; // 0xe
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT = 9; // 0x9
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED = 6; // 0x6
-    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; // 0x5
     field public static final int SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION = 2; // 0x2
     field public static final int SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE = 3; // 0x3
     field public static final int SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION = 4; // 0x4
@@ -45398,7 +45551,7 @@
     method public final int getParagraphLeft(int);
     method public final int getParagraphRight(int);
     method public float getPrimaryHorizontal(int);
-    method @Nullable public android.util.Range<java.lang.Integer> getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
+    method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
     method public float getSecondaryHorizontal(int);
     method public void getSelectionPath(int, int, android.graphics.Path);
     method public final float getSpacingAdd();
@@ -45527,6 +45680,14 @@
     field public static final int DONE = -1; // 0xffffffff
   }
 
+  public static class SegmentFinder.DefaultSegmentFinder extends android.text.SegmentFinder {
+    ctor public SegmentFinder.DefaultSegmentFinder(@NonNull int[]);
+    method public int nextEndBoundary(@IntRange(from=0) int);
+    method public int nextStartBoundary(@IntRange(from=0) int);
+    method public int previousEndBoundary(@IntRange(from=0) int);
+    method public int previousStartBoundary(@IntRange(from=0) int);
+  }
+
   public class Selection {
     method public static boolean extendDown(android.text.Spannable, android.text.Layout);
     method public static boolean extendLeft(android.text.Spannable, android.text.Layout);
@@ -47397,6 +47558,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
@@ -48515,6 +48677,12 @@
     field public static final int VERTICAL_GRAVITY_MASK = 112; // 0x70
   }
 
+  public class HandwritingDelegateConfiguration {
+    ctor public HandwritingDelegateConfiguration(@IdRes int, @NonNull Runnable);
+    method public int getDelegatorViewId();
+    method @NonNull public Runnable getInitiationCallback();
+  }
+
   public class HapticFeedbackConstants {
     field public static final int CLOCK_TICK = 4; // 0x4
     field public static final int CONFIRM = 16; // 0x10
@@ -49532,16 +49700,13 @@
   }
 
   public final class PixelCopy {
-    method @NonNull public static android.view.PixelCopy.Request ofSurface(@NonNull android.view.Surface, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>);
-    method @NonNull public static android.view.PixelCopy.Request ofSurface(@NonNull android.view.SurfaceView, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>);
-    method @NonNull public static android.view.PixelCopy.Request ofWindow(@NonNull android.view.Window, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>);
-    method @NonNull public static android.view.PixelCopy.Request ofWindow(@NonNull android.view.View, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.CopyResult>);
     method public static void request(@NonNull android.view.SurfaceView, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
     method public static void request(@NonNull android.view.SurfaceView, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
     method public static void request(@NonNull android.view.Surface, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
     method public static void request(@NonNull android.view.Surface, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
     method public static void request(@NonNull android.view.Window, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
     method public static void request(@NonNull android.view.Window, @Nullable android.graphics.Rect, @NonNull android.graphics.Bitmap, @NonNull android.view.PixelCopy.OnPixelCopyFinishedListener, @NonNull android.os.Handler);
+    method public static void request(@NonNull android.view.PixelCopy.Request);
     field public static final int ERROR_DESTINATION_INVALID = 5; // 0x5
     field public static final int ERROR_SOURCE_INVALID = 4; // 0x4
     field public static final int ERROR_SOURCE_NO_DATA = 3; // 0x3
@@ -49550,19 +49715,28 @@
     field public static final int SUCCESS = 0; // 0x0
   }
 
-  public static final class PixelCopy.CopyResult {
-    method @NonNull public android.graphics.Bitmap getBitmap();
-    method public int getStatus();
-  }
-
   public static interface PixelCopy.OnPixelCopyFinishedListener {
     method public void onPixelCopyFinished(int);
   }
 
   public static final class PixelCopy.Request {
-    method public void request();
-    method @NonNull public android.view.PixelCopy.Request setDestinationBitmap(@Nullable android.graphics.Bitmap);
-    method @NonNull public android.view.PixelCopy.Request setSourceRect(@Nullable android.graphics.Rect);
+    method @Nullable public android.graphics.Bitmap getDestinationBitmap();
+    method @Nullable public android.graphics.Rect getSourceRect();
+    method @NonNull public static android.view.PixelCopy.Request.Builder ofSurface(@NonNull android.view.Surface, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>);
+    method @NonNull public static android.view.PixelCopy.Request.Builder ofSurface(@NonNull android.view.SurfaceView, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>);
+    method @NonNull public static android.view.PixelCopy.Request.Builder ofWindow(@NonNull android.view.Window, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>);
+    method @NonNull public static android.view.PixelCopy.Request.Builder ofWindow(@NonNull android.view.View, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.PixelCopy.Result>);
+  }
+
+  public static final class PixelCopy.Request.Builder {
+    method @NonNull public android.view.PixelCopy.Request build();
+    method @NonNull public android.view.PixelCopy.Request.Builder setDestinationBitmap(@Nullable android.graphics.Bitmap);
+    method @NonNull public android.view.PixelCopy.Request.Builder setSourceRect(@Nullable android.graphics.Rect);
+  }
+
+  public static final class PixelCopy.Result {
+    method @NonNull public android.graphics.Bitmap getBitmap();
+    method public int getStatus();
   }
 
   public final class PointerIcon implements android.os.Parcelable {
@@ -49911,10 +50085,13 @@
     method public void clear();
     method public void computeCurrentVelocity(int);
     method public void computeCurrentVelocity(int, float);
+    method public float getAxisVelocity(int, int);
+    method public float getAxisVelocity(int);
     method public float getXVelocity();
     method public float getXVelocity(int);
     method public float getYVelocity();
     method public float getYVelocity(int);
+    method public boolean isAxisSupported(int);
     method public static android.view.VelocityTracker obtain();
     method public void recycle();
   }
@@ -50007,7 +50184,7 @@
     method public void dispatchCreateViewTranslationRequest(@NonNull java.util.Map<android.view.autofill.AutofillId,long[]>, @NonNull int[], @NonNull android.view.translation.TranslationCapability, @NonNull java.util.List<android.view.translation.ViewTranslationRequest>);
     method public void dispatchDisplayHint(int);
     method public boolean dispatchDragEvent(android.view.DragEvent);
-    method protected void dispatchDraw(android.graphics.Canvas);
+    method protected void dispatchDraw(@NonNull android.graphics.Canvas);
     method public void dispatchDrawableHotspotChanged(float, float);
     method @CallSuper public void dispatchFinishTemporaryDetach();
     method protected boolean dispatchGenericFocusedEvent(android.view.MotionEvent);
@@ -50045,7 +50222,7 @@
     method @NonNull public android.view.WindowInsetsAnimation.Bounds dispatchWindowInsetsAnimationStart(@NonNull android.view.WindowInsetsAnimation, @NonNull android.view.WindowInsetsAnimation.Bounds);
     method @Deprecated public void dispatchWindowSystemUiVisiblityChanged(int);
     method public void dispatchWindowVisibilityChanged(int);
-    method @CallSuper public void draw(android.graphics.Canvas);
+    method @CallSuper public void draw(@NonNull android.graphics.Canvas);
     method @CallSuper public void drawableHotspotChanged(float, float);
     method @CallSuper protected void drawableStateChanged();
     method public android.view.View findFocus();
@@ -50122,6 +50299,7 @@
     method public float getHandwritingBoundsOffsetLeft();
     method public float getHandwritingBoundsOffsetRight();
     method public float getHandwritingBoundsOffsetTop();
+    method @Nullable public android.view.HandwritingDelegateConfiguration getHandwritingDelegateConfiguration();
     method public final boolean getHasOverlappingRendering();
     method public final int getHeight();
     method public void getHitRect(android.graphics.Rect);
@@ -50337,9 +50515,9 @@
     method @CallSuper protected void onDetachedFromWindow();
     method protected void onDisplayHint(int);
     method public boolean onDragEvent(android.view.DragEvent);
-    method protected void onDraw(android.graphics.Canvas);
-    method public void onDrawForeground(android.graphics.Canvas);
-    method protected final void onDrawScrollBars(android.graphics.Canvas);
+    method protected void onDraw(@NonNull android.graphics.Canvas);
+    method public void onDrawForeground(@NonNull android.graphics.Canvas);
+    method protected final void onDrawScrollBars(@NonNull android.graphics.Canvas);
     method public boolean onFilterTouchEventForSecurity(android.view.MotionEvent);
     method @CallSuper protected void onFinishInflate();
     method public void onFinishTemporaryDetach();
@@ -50488,6 +50666,7 @@
     method public void setForegroundTintList(@Nullable android.content.res.ColorStateList);
     method public void setForegroundTintMode(@Nullable android.graphics.PorterDuff.Mode);
     method public void setHandwritingBoundsOffsets(float, float, float, float);
+    method public void setHandwritingDelegateConfiguration(@Nullable android.view.HandwritingDelegateConfiguration);
     method public void setHapticFeedbackEnabled(boolean);
     method public void setHasTransientState(boolean);
     method public void setHorizontalFadingEdgeEnabled(boolean);
@@ -50824,7 +51003,7 @@
     ctor public View.DragShadowBuilder(android.view.View);
     ctor public View.DragShadowBuilder();
     method public final android.view.View getView();
-    method public void onDrawShadow(android.graphics.Canvas);
+    method public void onDrawShadow(@NonNull android.graphics.Canvas);
     method public void onProvideShadowMetrics(android.graphics.Point, android.graphics.Point);
   }
 
@@ -50934,6 +51113,7 @@
     method public int getScaledDoubleTapSlop();
     method public int getScaledEdgeSlop();
     method public int getScaledFadingEdgeLength();
+    method public int getScaledHandwritingGestureLineMargin();
     method public int getScaledHandwritingSlop();
     method public float getScaledHorizontalScrollFactor();
     method public int getScaledHoverSlop();
@@ -51053,7 +51233,7 @@
     method public void dispatchSetActivated(boolean);
     method public void dispatchSetSelected(boolean);
     method protected void dispatchThawSelfOnly(android.util.SparseArray<android.os.Parcelable>);
-    method protected boolean drawChild(android.graphics.Canvas, android.view.View, long);
+    method protected boolean drawChild(@NonNull android.graphics.Canvas, android.view.View, long);
     method public void endViewTransition(android.view.View);
     method public android.view.View focusSearch(android.view.View, int);
     method public void focusableViewAvailable(android.view.View);
@@ -51790,6 +51970,7 @@
     method public static int statusBars();
     method public static int systemBars();
     method public static int systemGestures();
+    method public static int systemOverlays();
     method public static int tappableElement();
   }
 
@@ -52226,6 +52407,7 @@
     method public CharSequence getClassName();
     method public android.view.accessibility.AccessibilityNodeInfo.CollectionInfo getCollectionInfo();
     method public android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo getCollectionItemInfo();
+    method @Nullable public CharSequence getContainerTitle();
     method public CharSequence getContentDescription();
     method public int getDrawingOrder();
     method public CharSequence getError();
@@ -52237,6 +52419,7 @@
     method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
     method public int getLiveRegion();
     method public int getMaxTextLength();
+    method public int getMinMillisBetweenContentChanges();
     method public int getMovementGranularities();
     method public CharSequence getPackageName();
     method @Nullable public CharSequence getPaneTitle();
@@ -52302,6 +52485,7 @@
     method public void setClickable(boolean);
     method public void setCollectionInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionInfo);
     method public void setCollectionItemInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo);
+    method public void setContainerTitle(@Nullable CharSequence);
     method public void setContentDescription(CharSequence);
     method public void setContentInvalid(boolean);
     method public void setContextClickable(boolean);
@@ -52323,6 +52507,7 @@
     method public void setLiveRegion(int);
     method public void setLongClickable(boolean);
     method public void setMaxTextLength(int);
+    method public void setMinMillisBetweenContentChanges(int);
     method public void setMovementGranularities(int);
     method public void setMultiLine(boolean);
     method public void setPackageName(CharSequence);
@@ -52402,11 +52587,13 @@
     field public static final int FOCUS_ACCESSIBILITY = 2; // 0x2
     field public static final int FOCUS_INPUT = 1; // 0x1
     field public static final int MAX_NUMBER_OF_PREFETCHED_NODES = 50; // 0x32
+    field public static final int MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = 100; // 0x64
     field public static final int MOVEMENT_GRANULARITY_CHARACTER = 1; // 0x1
     field public static final int MOVEMENT_GRANULARITY_LINE = 4; // 0x4
     field public static final int MOVEMENT_GRANULARITY_PAGE = 16; // 0x10
     field public static final int MOVEMENT_GRANULARITY_PARAGRAPH = 8; // 0x8
     field public static final int MOVEMENT_GRANULARITY_WORD = 2; // 0x2
+    field public static final int UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = -1; // 0xffffffff
   }
 
   public static final class AccessibilityNodeInfo.AccessibilityAction implements android.os.Parcelable {
@@ -53266,6 +53453,7 @@
     method public android.graphics.Matrix getMatrix();
     method public int getSelectionEnd();
     method public int getSelectionStart();
+    method @Nullable public android.view.inputmethod.TextAppearanceInfo getTextAppearanceInfo();
     method @NonNull public java.util.List<android.graphics.RectF> getVisibleLineBounds();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.CursorAnchorInfo> CREATOR;
@@ -53286,9 +53474,10 @@
     method public android.view.inputmethod.CursorAnchorInfo.Builder setInsertionMarkerLocation(float, float, float, float, int);
     method public android.view.inputmethod.CursorAnchorInfo.Builder setMatrix(android.graphics.Matrix);
     method public android.view.inputmethod.CursorAnchorInfo.Builder setSelectionRange(int, int);
+    method @NonNull public android.view.inputmethod.CursorAnchorInfo.Builder setTextAppearanceInfo(@Nullable android.view.inputmethod.TextAppearanceInfo);
   }
 
-  public final class DeleteGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+  public final class DeleteGesture extends android.view.inputmethod.PreviewableHandwritingGesture implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.graphics.RectF getDeletionArea();
     method public int getGranularity();
@@ -53304,7 +53493,7 @@
     method @NonNull public android.view.inputmethod.DeleteGesture.Builder setGranularity(int);
   }
 
-  public final class DeleteRangeGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+  public final class DeleteRangeGesture extends android.view.inputmethod.PreviewableHandwritingGesture implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.graphics.RectF getDeletionEndArea();
     method @NonNull public android.graphics.RectF getDeletionStartArea();
@@ -53346,11 +53535,13 @@
     method @Nullable public CharSequence getInitialTextAfterCursor(@IntRange(from=0) int, int);
     method @Nullable public CharSequence getInitialTextBeforeCursor(@IntRange(from=0) int, int);
     method public int getInitialToolType();
+    method @NonNull public java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>> getSupportedHandwritingGesturePreviews();
     method @NonNull public java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>> getSupportedHandwritingGestures();
     method public final void makeCompatible(int);
     method public void setInitialSurroundingSubText(@NonNull CharSequence, int);
     method public void setInitialSurroundingText(@NonNull CharSequence);
     method public void setInitialToolType(int);
+    method public void setSupportedHandwritingGesturePreviews(@NonNull java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>>);
     method public void setSupportedHandwritingGestures(@NonNull java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>>);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.EditorInfo> CREATOR;
@@ -53525,10 +53716,12 @@
     method public default void performHandwritingGesture(@NonNull android.view.inputmethod.HandwritingGesture, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.IntConsumer);
     method public boolean performPrivateCommand(String, android.os.Bundle);
     method public default boolean performSpellCheck();
+    method public default boolean previewHandwritingGesture(@NonNull android.view.inputmethod.PreviewableHandwritingGesture, @Nullable android.os.CancellationSignal);
     method public default boolean replaceText(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull CharSequence, int, @Nullable android.view.inputmethod.TextAttribute);
     method public boolean reportFullscreenMode(boolean);
     method public boolean requestCursorUpdates(int);
     method public default boolean requestCursorUpdates(int, int);
+    method public default void requestTextBoundsInfo(@NonNull android.graphics.RectF, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.inputmethod.TextBoundsInfoResult>);
     method public boolean sendKeyEvent(android.view.KeyEvent);
     method public boolean setComposingRegion(int, int);
     method public default boolean setComposingRegion(int, int, @Nullable android.view.inputmethod.TextAttribute);
@@ -53540,6 +53733,7 @@
     field public static final int CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS = 8; // 0x8
     field public static final int CURSOR_UPDATE_FILTER_EDITOR_BOUNDS = 4; // 0x4
     field public static final int CURSOR_UPDATE_FILTER_INSERTION_MARKER = 16; // 0x10
+    field public static final int CURSOR_UPDATE_FILTER_TEXT_APPEARANCE = 64; // 0x40
     field public static final int CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS = 32; // 0x20
     field public static final int CURSOR_UPDATE_IMMEDIATE = 1; // 0x1
     field public static final int CURSOR_UPDATE_MONITOR = 2; // 0x2
@@ -53730,6 +53924,7 @@
     method @NonNull public String getLanguageTag();
     method @Deprecated @NonNull public String getLocale();
     method public String getMode();
+    method @NonNull public CharSequence getNameOverride();
     method public int getNameResId();
     method public boolean isAsciiCapable();
     method public boolean isAuxiliary();
@@ -53750,6 +53945,7 @@
     method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeId(int);
     method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeLocale(String);
     method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeMode(String);
+    method @NonNull public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameOverride(@NonNull CharSequence);
     method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameResId(int);
   }
 
@@ -53783,6 +53979,9 @@
     method @NonNull public android.view.inputmethod.JoinOrSplitGesture.Builder setJoinOrSplitPoint(@NonNull android.graphics.PointF);
   }
 
+  public abstract class PreviewableHandwritingGesture extends android.view.inputmethod.HandwritingGesture {
+  }
+
   public final class RemoveSpaceGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.graphics.PointF getEndPoint();
@@ -53798,7 +53997,7 @@
     method @NonNull public android.view.inputmethod.RemoveSpaceGesture.Builder setPoints(@NonNull android.graphics.PointF, @NonNull android.graphics.PointF);
   }
 
-  public final class SelectGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+  public final class SelectGesture extends android.view.inputmethod.PreviewableHandwritingGesture implements android.os.Parcelable {
     method public int describeContents();
     method public int getGranularity();
     method @NonNull public android.graphics.RectF getSelectionArea();
@@ -53814,7 +54013,7 @@
     method @NonNull public android.view.inputmethod.SelectGesture.Builder setSelectionArea(@NonNull android.graphics.RectF);
   }
 
-  public final class SelectRangeGesture extends android.view.inputmethod.HandwritingGesture implements android.os.Parcelable {
+  public final class SelectRangeGesture extends android.view.inputmethod.PreviewableHandwritingGesture implements android.os.Parcelable {
     method public int describeContents();
     method public int getGranularity();
     method @NonNull public android.graphics.RectF getSelectionEndArea();
@@ -53843,6 +54042,61 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.SurroundingText> CREATOR;
   }
 
+  public final class TextAppearanceInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public String getFontFeatureSettings();
+    method @Nullable public String getFontVariationSettings();
+    method @ColorInt public int getHighlightTextColor();
+    method @ColorInt public int getHintTextColor();
+    method public float getLetterSpacing();
+    method public int getLineBreakStyle();
+    method public int getLineBreakWordStyle();
+    method @ColorInt public int getLinkTextColor();
+    method @ColorInt public int getShadowColor();
+    method @Px public float getShadowDx();
+    method @Px public float getShadowDy();
+    method @Px public float getShadowRadius();
+    method @Nullable public String getSystemFontFamilyName();
+    method @ColorInt public int getTextColor();
+    method @IntRange(from=android.graphics.fonts.FontStyle.FONT_WEIGHT_UNSPECIFIED, to=android.graphics.fonts.FontStyle.FONT_WEIGHT_MAX) public int getTextFontWeight();
+    method @NonNull public android.os.LocaleList getTextLocales();
+    method public float getTextScaleX();
+    method @Px public float getTextSize();
+    method public int getTextStyle();
+    method public boolean isAllCaps();
+    method public boolean isElegantTextHeight();
+    method public boolean isFallbackLineSpacing();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextAppearanceInfo> CREATOR;
+  }
+
+  public static final class TextAppearanceInfo.Builder {
+    ctor public TextAppearanceInfo.Builder();
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo build();
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setAllCaps(boolean);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setElegantTextHeight(boolean);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFallbackLineSpacing(boolean);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFontFeatureSettings(@Nullable String);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setFontVariationSettings(@Nullable String);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setHighlightTextColor(@ColorInt int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setHintTextColor(@ColorInt int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLetterSpacing(float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLineBreakStyle(int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLineBreakWordStyle(int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setLinkTextColor(@ColorInt int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowColor(@ColorInt int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowDx(@Px float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowDy(@Px float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setShadowRadius(@Px float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setSystemFontFamilyName(@Nullable String);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextColor(@ColorInt int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextFontWeight(@IntRange(from=android.graphics.fonts.FontStyle.FONT_WEIGHT_UNSPECIFIED, to=android.graphics.fonts.FontStyle.FONT_WEIGHT_MAX) int);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextLocales(@NonNull android.os.LocaleList);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextScaleX(float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextSize(@Px float);
+    method @NonNull public android.view.inputmethod.TextAppearanceInfo.Builder setTextStyle(int);
+  }
+
   public final class TextAttribute implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.os.PersistableBundle getExtras();
@@ -53858,6 +54112,50 @@
     method @NonNull public android.view.inputmethod.TextAttribute.Builder setTextConversionSuggestions(@NonNull java.util.List<java.lang.String>);
   }
 
+  public final class TextBoundsInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @IntRange(from=0, to=125) public int getCharacterBidiLevel(int);
+    method @NonNull public android.graphics.RectF getCharacterBounds(int);
+    method public int getCharacterFlags(int);
+    method public int getEnd();
+    method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder();
+    method @NonNull public android.text.SegmentFinder getLineSegmentFinder();
+    method @NonNull public android.graphics.Matrix getMatrix();
+    method public int getStart();
+    method @NonNull public android.text.SegmentFinder getWordSegmentFinder();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextBoundsInfo> CREATOR;
+    field public static final int FLAG_CHARACTER_LINEFEED = 2; // 0x2
+    field public static final int FLAG_CHARACTER_PUNCTUATION = 4; // 0x4
+    field public static final int FLAG_CHARACTER_WHITESPACE = 1; // 0x1
+    field public static final int FLAG_LINE_IS_RTL = 8; // 0x8
+  }
+
+  public static final class TextBoundsInfo.Builder {
+    ctor public TextBoundsInfo.Builder();
+    method @NonNull public android.view.inputmethod.TextBoundsInfo build();
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder clear();
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBidiLevel(@NonNull int[]);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBounds(@NonNull float[]);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterFlags(@NonNull int[]);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setGraphemeSegmentFinder(@NonNull android.text.SegmentFinder);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setLineSegmentFinder(@NonNull android.text.SegmentFinder);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setMatrix(@NonNull android.graphics.Matrix);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setStartAndEnd(@IntRange(from=0) int, @IntRange(from=0) int);
+    method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setWordSegmentFinder(@NonNull android.text.SegmentFinder);
+  }
+
+  public final class TextBoundsInfoResult {
+    ctor public TextBoundsInfoResult(int);
+    ctor public TextBoundsInfoResult(int, @NonNull android.view.inputmethod.TextBoundsInfo);
+    method public int getResultCode();
+    method @Nullable public android.view.inputmethod.TextBoundsInfo getTextBoundsInfo();
+    field public static final int CODE_CANCELLED = 3; // 0x3
+    field public static final int CODE_FAILED = 2; // 0x2
+    field public static final int CODE_SUCCESS = 1; // 0x1
+    field public static final int CODE_UNSUPPORTED = 0; // 0x0
+  }
+
   public final class TextSnapshot {
     ctor public TextSnapshot(@NonNull android.view.inputmethod.SurroundingText, @IntRange(from=0xffffffff) int, @IntRange(from=0xffffffff) int, int);
     method @IntRange(from=0xffffffff) public int getCompositionEnd();
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index e890005..e6ddf9f 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();
   }
@@ -344,10 +340,6 @@
     method public boolean shouldBypassCache(@NonNull Q);
   }
 
-  public interface Parcelable {
-    method public default int getStability();
-  }
-
   public class Process {
     method public static final int getAppUidForSdkSandboxUid(int);
     method public static final boolean isSdkSandboxUid(int);
@@ -357,6 +349,7 @@
   }
 
   public final class ServiceManager {
+    method @NonNull public static String[] getDeclaredInstances(@NonNull String);
     method public static boolean isDeclared(@NonNull String);
     method @Nullable public static android.os.IBinder waitForDeclaredService(@NonNull String);
     method @Nullable public static android.os.IBinder waitForService(@NonNull String);
@@ -392,6 +385,7 @@
     method public static void traceBegin(long, @NonNull String);
     method public static void traceCounter(long, @NonNull String, int);
     method public static void traceEnd(long);
+    field public static final long TRACE_TAG_AIDL = 16777216L; // 0x1000000L
     field public static final long TRACE_TAG_NETWORK = 2097152L; // 0x200000L
   }
 
@@ -419,6 +413,7 @@
 
   public final class DeviceConfig {
     field public static final String NAMESPACE_ALARM_MANAGER = "alarm_manager";
+    field public static final String NAMESPACE_APP_CLONING = "app_cloning";
     field public static final String NAMESPACE_APP_STANDBY = "app_standby";
     field public static final String NAMESPACE_DEVICE_IDLE = "device_idle";
   }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7c1c8ba..cb9b76d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -145,6 +145,7 @@
     field public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
     field public static final String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
     field public static final String INVOKE_CARRIER_SETUP = "android.permission.INVOKE_CARRIER_SETUP";
+    field public static final String KILL_ALL_BACKGROUND_PROCESSES = "android.permission.KILL_ALL_BACKGROUND_PROCESSES";
     field public static final String KILL_UID = "android.permission.KILL_UID";
     field public static final String LAUNCH_DEVICE_MANAGER_SETUP = "android.permission.LAUNCH_DEVICE_MANAGER_SETUP";
     field public static final String LOCAL_MAC_ADDRESS = "android.permission.LOCAL_MAC_ADDRESS";
@@ -189,6 +190,7 @@
     field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER";
     field public static final String MANAGE_SPEECH_RECOGNITION = "android.permission.MANAGE_SPEECH_RECOGNITION";
     field public static final String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS";
+    field public static final String MANAGE_SUBSCRIPTION_USER_ASSOCIATION = "android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION";
     field public static final String MANAGE_TEST_NETWORKS = "android.permission.MANAGE_TEST_NETWORKS";
     field public static final String MANAGE_TIME_AND_ZONE_DETECTION = "android.permission.MANAGE_TIME_AND_ZONE_DETECTION";
     field public static final String MANAGE_UI_TRANSLATION = "android.permission.MANAGE_UI_TRANSLATION";
@@ -401,6 +403,7 @@
 
   public static final class R.dimen {
     field public static final int config_restrictedIconSize = 17104903; // 0x1050007
+    field public static final int config_viewConfigurationHandwritingGestureLineMargin;
   }
 
   public static final class R.drawable {
@@ -521,10 +524,12 @@
   }
 
   public class AlarmManager {
-    method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, android.app.PendingIntent, android.os.WorkSource);
-    method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, android.app.AlarmManager.OnAlarmListener, android.os.Handler, android.os.WorkSource);
+    method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, @NonNull android.app.PendingIntent, @Nullable android.os.WorkSource);
+    method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void set(int, long, long, long, @NonNull android.app.AlarmManager.OnAlarmListener, @Nullable android.os.Handler, @Nullable android.os.WorkSource);
     method @RequiresPermission(allOf={android.Manifest.permission.UPDATE_DEVICE_STATS, android.Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional=true) public void setExact(int, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener);
+    method @RequiresPermission(allOf={android.Manifest.permission.UPDATE_DEVICE_STATS, android.Manifest.permission.SCHEDULE_EXACT_ALARM}, conditional=true) public void setExactAndAllowWhileIdle(int, long, @Nullable String, @NonNull java.util.concurrent.Executor, @Nullable android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener);
     method @RequiresPermission(android.Manifest.permission.SCHEDULE_PRIORITIZED_ALARM) public void setPrioritized(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @NonNull android.app.AlarmManager.OnAlarmListener);
+    method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public void setWindow(int, long, long, @Nullable String, @NonNull java.util.concurrent.Executor, @Nullable android.os.WorkSource, @NonNull android.app.AlarmManager.OnAlarmListener);
   }
 
   public class AppOpsManager {
@@ -582,8 +587,11 @@
     field public static final String OPSTR_READ_MEDIA_AUDIO = "android:read_media_audio";
     field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images";
     field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video";
+    field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected";
+    field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data";
     field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio";
     field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast";
+    field public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio";
     field public static final String OPSTR_REQUEST_DELETE_PACKAGES = "android:request_delete_packages";
     field public static final String OPSTR_REQUEST_INSTALL_PACKAGES = "android:request_install_packages";
     field public static final String OPSTR_RUN_ANY_IN_BACKGROUND = "android:run_any_in_background";
@@ -771,11 +779,14 @@
   }
 
   public class BroadcastOptions {
+    method public void clearDeliveryGroupPolicy();
     method public void clearRequireCompatChange();
+    method public int getDeliveryGroupPolicy();
     method public boolean isPendingIntentBackgroundActivityLaunchAllowed();
     method public static android.app.BroadcastOptions makeBasic();
     method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long);
     method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean);
+    method public void setDeliveryGroupPolicy(int);
     method public void setDontSendToRestrictedApps(boolean);
     method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean);
     method public void setRequireAllOfPermissions(@Nullable String[]);
@@ -784,6 +795,8 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppAllowlist(long, int, int, @Nullable String);
     method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppWhitelistDuration(long);
     method public android.os.Bundle toBundle();
+    field public static final int DELIVERY_GROUP_POLICY_ALL = 0; // 0x0
+    field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1
   }
 
   public class DownloadManager {
@@ -1096,6 +1109,7 @@
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.os.UserHandle createAndProvisionManagedProfile(@NonNull android.app.admin.ManagedProfileProvisioningParams) throws android.app.admin.ProvisioningException;
     method @Nullable public android.content.Intent createProvisioningIntentFromNfcIntent(@NonNull android.content.Intent);
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void finalizeWorkProfileProvisioning(@NonNull android.os.UserHandle, @Nullable android.accounts.Account);
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public java.util.Set<java.lang.Integer> getApplicationExemptions(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public boolean getBluetoothContactSharingDisabled(@NonNull android.os.UserHandle);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getDeviceOwner();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}) public android.content.ComponentName getDeviceOwnerComponentOnAnyUser();
@@ -1121,6 +1135,7 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException;
     method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException;
+    method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException;
     method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setDeviceProvisioningConfigApplied();
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setDpcDownloaded(boolean);
     method @Deprecated @RequiresPermission(value=android.Manifest.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS, conditional=true) public void setProfileOwnerCanAccessDeviceIds(@NonNull android.content.ComponentName);
@@ -1142,6 +1157,7 @@
     field public static final String ACTION_SET_PROFILE_OWNER = "android.app.action.SET_PROFILE_OWNER";
     field @Deprecated public static final String ACTION_STATE_USER_SETUP_COMPLETE = "android.app.action.STATE_USER_SETUP_COMPLETE";
     field @RequiresPermission(android.Manifest.permission.LAUNCH_DEVICE_MANAGER_SETUP) public static final String ACTION_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER = "android.app.action.UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER";
+    field public static final int EXEMPT_FROM_APP_STANDBY = 0; // 0x0
     field public static final String EXTRA_FORCE_UPDATE_ROLE_HOLDER = "android.app.extra.FORCE_UPDATE_ROLE_HOLDER";
     field public static final String EXTRA_LOST_MODE_LOCATION = "android.app.extra.LOST_MODE_LOCATION";
     field public static final String EXTRA_PROFILE_OWNER_NAME = "android.app.extra.PROFILE_OWNER_NAME";
@@ -2501,11 +2517,49 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.ExternalTimeSuggestion> CREATOR;
   }
 
+  public final class TimeCapabilities implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getConfigureAutoDetectionEnabledCapability();
+    method public int getSetManualTimeCapability();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilities> CREATOR;
+  }
+
+  public final class TimeCapabilitiesAndConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.TimeCapabilities getCapabilities();
+    method @NonNull public android.app.time.TimeConfiguration getConfiguration();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilitiesAndConfig> CREATOR;
+  }
+
+  public final class TimeConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isAutoDetectionEnabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeConfiguration> CREATOR;
+  }
+
+  public static final class TimeConfiguration.Builder {
+    ctor public TimeConfiguration.Builder();
+    ctor public TimeConfiguration.Builder(@NonNull android.app.time.TimeConfiguration);
+    method @NonNull public android.app.time.TimeConfiguration build();
+    method @NonNull public android.app.time.TimeConfiguration.Builder setAutoDetectionEnabled(boolean);
+  }
+
   public final class TimeManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void addTimeZoneDetectorListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeState getTimeState();
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneState getTimeZoneState();
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void removeTimeZoneDetectorListener(@NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String);
     method @RequiresPermission(android.Manifest.permission.SUGGEST_EXTERNAL_TIME) public void suggestExternalTime(@NonNull android.app.time.ExternalTimeSuggestion);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull android.app.time.TimeConfiguration);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeZoneConfiguration(@NonNull android.app.time.TimeZoneConfiguration);
   }
 
@@ -2513,10 +2567,19 @@
     method public void onChange();
   }
 
+  public final class TimeState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.UnixEpochTime getUnixEpochTime();
+    method public boolean getUserShouldConfirmTime();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeState> CREATOR;
+  }
+
   public final class TimeZoneCapabilities implements android.os.Parcelable {
     method public int describeContents();
     method public int getConfigureAutoDetectionEnabledCapability();
     method public int getConfigureGeoDetectionEnabledCapability();
+    method public int getSetManualTimeZoneCapability();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneCapabilities> CREATOR;
   }
@@ -2545,6 +2608,24 @@
     method @NonNull public android.app.time.TimeZoneConfiguration.Builder setGeoDetectionEnabled(boolean);
   }
 
+  public final class TimeZoneState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getId();
+    method public boolean getUserShouldConfirmId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneState> CREATOR;
+  }
+
+  public final class UnixEpochTime implements android.os.Parcelable {
+    ctor public UnixEpochTime(long, long);
+    method @NonNull public android.app.time.UnixEpochTime at(long);
+    method public int describeContents();
+    method public long getElapsedRealtimeMillis();
+    method public long getUnixEpochTimeMillis();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.UnixEpochTime> CREATOR;
+  }
+
 }
 
 package android.app.usage {
@@ -2831,6 +2912,7 @@
     method @NonNull public java.util.Set<android.content.ComponentName> getBlockedCrossTaskNavigations();
     method public int getDefaultActivityPolicy();
     method public int getDefaultNavigationPolicy();
+    method public int getDevicePolicy(int);
     method public int getLockState();
     method @Nullable public String getName();
     method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
@@ -2838,14 +2920,18 @@
     field public static final int ACTIVITY_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int ACTIVITY_POLICY_DEFAULT_BLOCKED = 1; // 0x1
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.VirtualDeviceParams> CREATOR;
+    field public static final int DEVICE_POLICY_CUSTOM = 1; // 0x1
+    field public static final int DEVICE_POLICY_DEFAULT = 0; // 0x0
     field public static final int LOCK_STATE_ALWAYS_UNLOCKED = 1; // 0x1
     field public static final int LOCK_STATE_DEFAULT = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
+    field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
   }
 
   public static final class VirtualDeviceParams.Builder {
     ctor public VirtualDeviceParams.Builder();
+    method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder addDevicePolicy(int, int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams build();
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setAllowedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
@@ -3424,6 +3510,7 @@
     field public static final String FEATURE_REBOOT_ESCROW = "android.hardware.reboot_escrow";
     field public static final String FEATURE_TELEPHONY_CARRIERLOCK = "android.hardware.telephony.carrierlock";
     field public static final String FEATURE_TELEPHONY_IMS_SINGLE_REGISTRATION = "android.hardware.telephony.ims.singlereg";
+    field public static final String FEATURE_VIRTUALIZATION_FRAMEWORK = "android.software.virtualization_framework";
     field public static final int FLAGS_PERMISSION_RESERVED_PERMISSION_CONTROLLER = -268435456; // 0xf0000000
     field public static final int FLAG_PERMISSION_APPLY_RESTRICTION = 16384; // 0x4000
     field public static final int FLAG_PERMISSION_AUTO_REVOKED = 131072; // 0x20000
@@ -3540,6 +3627,7 @@
     field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000
     field @Deprecated public static final int PROTECTION_FLAG_WELLBEING = 131072; // 0x20000
     field @Nullable public final String backgroundPermission;
+    field @NonNull public java.util.Set<java.lang.String> knownCerts;
     field @StringRes public int requestRes;
   }
 
@@ -5478,6 +5566,28 @@
     method @NonNull public android.location.CorrelationVector.Builder setSamplingWidthMeters(@FloatRange(from=0.0f, fromInclusive=false) double);
   }
 
+  public final class Country implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getCountryIso();
+    method public int getSource();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int COUNTRY_SOURCE_LOCALE = 3; // 0x3
+    field public static final int COUNTRY_SOURCE_LOCATION = 1; // 0x1
+    field public static final int COUNTRY_SOURCE_NETWORK = 0; // 0x0
+    field public static final int COUNTRY_SOURCE_SIM = 2; // 0x2
+    field @NonNull public static final android.os.Parcelable.Creator<android.location.Country> CREATOR;
+  }
+
+  public class CountryDetector {
+    method public void addCountryListener(@NonNull android.location.CountryListener, @Nullable android.os.Looper);
+    method @Nullable public android.location.Country detectCountry();
+    method public void removeCountryListener(@NonNull android.location.CountryListener);
+  }
+
+  public interface CountryListener {
+    method public void onCountryDetected(@NonNull android.location.Country);
+  }
+
   public final class GnssCapabilities implements android.os.Parcelable {
     method @Deprecated public boolean hasMeasurementCorrectionsReflectingPane();
     method @Deprecated public boolean hasNavMessages();
@@ -6269,12 +6379,21 @@
   public final class AudioPlaybackConfiguration implements android.os.Parcelable {
     method public int getClientPid();
     method public int getClientUid();
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMutedBy();
     method public int getPlayerInterfaceId();
     method public android.media.PlayerProxy getPlayerProxy();
     method public int getPlayerState();
     method public int getPlayerType();
     method @IntRange(from=0) public int getSessionId();
     method public boolean isActive();
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isMuted();
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_APP_OPS = 8; // 0x8
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_CLIENT_VOLUME = 16; // 0x10
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_MASTER = 1; // 0x1
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_STREAM_MUTED = 4; // 0x4
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_STREAM_VOLUME = 2; // 0x2
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_UNKNOWN = -1; // 0xffffffff
+    field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_VOLUME_SHAPER = 32; // 0x20
     field public static final int PLAYER_STATE_IDLE = 1; // 0x1
     field public static final int PLAYER_STATE_PAUSED = 3; // 0x3
     field public static final int PLAYER_STATE_RELEASED = 0; // 0x0
@@ -7046,6 +7165,7 @@
 
   public class Tuner implements java.lang.AutoCloseable {
     ctor @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER) public Tuner(@NonNull android.content.Context, @Nullable String, int);
+    method public int applyFrontend(@NonNull android.media.tv.tuner.frontend.FrontendInfo);
     method public int cancelScanning();
     method public int cancelTuning();
     method public void clearOnTuneEventListener();
@@ -9276,6 +9396,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);
   }
 
@@ -9594,6 +9715,7 @@
   }
 
   public interface Parcelable {
+    method public default int getStability();
     field public static final int PARCELABLE_STABILITY_LOCAL = 0; // 0x0
     field public static final int PARCELABLE_STABILITY_VINTF = 1; // 0x1
   }
@@ -9602,7 +9724,6 @@
     ctor public ParcelableHolder(int);
     method public int describeContents();
     method @Nullable public <T extends android.os.Parcelable> T getParcelable(@NonNull Class<T>);
-    method public int getStability();
     method public void readFromParcel(@NonNull android.os.Parcel);
     method public void setParcelable(@Nullable android.os.Parcelable);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -9857,9 +9978,10 @@
     method public boolean isCloneProfile();
     method public boolean isCredentialSharableWithParent();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isGuestUser();
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isMainUser();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public boolean isManagedProfile(int);
     method public boolean isMediaSharedWithParent();
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isPrimaryUser();
+    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isPrimaryUser();
     method public static boolean isRemoveResultSuccessful(int);
     method public boolean isRestrictedProfile();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean isRestrictedProfile(@NonNull android.os.UserHandle);
@@ -11813,11 +11935,39 @@
     method public abstract void onStopUpdates();
     method public final void reportPermanentFailure(@NonNull Throwable);
     method public final void reportSuggestion(@NonNull android.service.timezone.TimeZoneProviderSuggestion);
+    method public final void reportSuggestion(@NonNull android.service.timezone.TimeZoneProviderSuggestion, @NonNull android.service.timezone.TimeZoneProviderStatus);
     method public final void reportUncertain();
+    method public final void reportUncertain(@NonNull android.service.timezone.TimeZoneProviderStatus);
     field public static final String PRIMARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.PrimaryLocationTimeZoneProviderService";
     field public static final String SECONDARY_LOCATION_TIME_ZONE_PROVIDER_SERVICE_INTERFACE = "android.service.timezone.SecondaryLocationTimeZoneProviderService";
   }
 
+  public final class TimeZoneProviderStatus implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getConnectivityDependencyStatus();
+    method public int getLocationDetectionDependencyStatus();
+    method public int getTimeZoneResolutionOperationStatus();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.timezone.TimeZoneProviderStatus> CREATOR;
+    field public static final int DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = 4; // 0x4
+    field public static final int DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = 6; // 0x6
+    field public static final int DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS = 5; // 0x5
+    field public static final int DEPENDENCY_STATUS_NOT_APPLICABLE = 1; // 0x1
+    field public static final int DEPENDENCY_STATUS_OK = 2; // 0x2
+    field public static final int DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE = 3; // 0x3
+    field public static final int OPERATION_STATUS_FAILED = 3; // 0x3
+    field public static final int OPERATION_STATUS_NOT_APPLICABLE = 1; // 0x1
+    field public static final int OPERATION_STATUS_OK = 2; // 0x2
+  }
+
+  public static final class TimeZoneProviderStatus.Builder {
+    ctor public TimeZoneProviderStatus.Builder();
+    method @NonNull public android.service.timezone.TimeZoneProviderStatus build();
+    method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setConnectivityDependencyStatus(int);
+    method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setLocationDetectionDependencyStatus(int);
+    method @NonNull public android.service.timezone.TimeZoneProviderStatus.Builder setTimeZoneResolutionOperationStatus(int);
+  }
+
   public final class TimeZoneProviderSuggestion implements android.os.Parcelable {
     method public int describeContents();
     method public long getElapsedRealtimeMillis();
@@ -11979,7 +12129,7 @@
   public final class HotwordAudioStream implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.media.AudioFormat getAudioFormat();
-    method @NonNull public android.os.ParcelFileDescriptor getAudioStream();
+    method @NonNull public android.os.ParcelFileDescriptor getAudioStreamParcelFileDescriptor();
     method @NonNull public android.os.PersistableBundle getMetadata();
     method @Nullable public android.media.AudioTimestamp getTimestamp();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -11990,7 +12140,7 @@
     ctor public HotwordAudioStream.Builder(@NonNull android.media.AudioFormat, @NonNull android.os.ParcelFileDescriptor);
     method @NonNull public android.service.voice.HotwordAudioStream build();
     method @NonNull public android.service.voice.HotwordAudioStream.Builder setAudioFormat(@NonNull android.media.AudioFormat);
-    method @NonNull public android.service.voice.HotwordAudioStream.Builder setAudioStream(@NonNull android.os.ParcelFileDescriptor);
+    method @NonNull public android.service.voice.HotwordAudioStream.Builder setAudioStreamParcelFileDescriptor(@NonNull android.os.ParcelFileDescriptor);
     method @NonNull public android.service.voice.HotwordAudioStream.Builder setMetadata(@NonNull android.os.PersistableBundle);
     method @NonNull public android.service.voice.HotwordAudioStream.Builder setTimestamp(@NonNull android.media.AudioTimestamp);
   }
@@ -12009,6 +12159,7 @@
     method public static int getMaxScore();
     method @Nullable public android.media.MediaSyncEvent getMediaSyncEvent();
     method public int getPersonalizedScore();
+    method public int getProximity();
     method public int getScore();
     method public boolean isHotwordDetectionPersonalized();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -12022,6 +12173,9 @@
     field public static final int CONFIDENCE_LEVEL_VERY_HIGH = 6; // 0x6
     field @NonNull public static final android.os.Parcelable.Creator<android.service.voice.HotwordDetectedResult> CREATOR;
     field public static final int HOTWORD_OFFSET_UNSET = -1; // 0xffffffff
+    field public static final int PROXIMITY_FAR = 2; // 0x2
+    field public static final int PROXIMITY_NEAR = 1; // 0x1
+    field public static final int PROXIMITY_UNKNOWN = -1; // 0xffffffff
   }
 
   public static final class HotwordDetectedResult.Builder {
@@ -12110,7 +12264,7 @@
 
   public class WallpaperService.Engine {
     method public boolean isInAmbientMode();
-    method public void onAmbientModeChanged(boolean, long);
+    method @MainThread public void onAmbientModeChanged(boolean, long);
   }
 
 }
@@ -12358,6 +12512,8 @@
     field public static final int CS_BOUND = 6; // 0x6
     field public static final int DIRECT_TO_VM_FINISHED = 103; // 0x67
     field public static final int DIRECT_TO_VM_INITIATED = 102; // 0x66
+    field public static final int DND_CHECK_COMPLETED = 110; // 0x6e
+    field public static final int DND_CHECK_INITIATED = 109; // 0x6d
     field public static final int FILTERING_COMPLETED = 107; // 0x6b
     field public static final int FILTERING_INITIATED = 106; // 0x6a
     field public static final int FILTERING_TIMED_OUT = 108; // 0x6c
@@ -12397,6 +12553,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.telecom.ParcelableCallAnalytics.EventTiming> CREATOR;
     field public static final int DIRECT_TO_VM_FINISHED_TIMING = 8; // 0x8
     field public static final int DISCONNECT_TIMING = 2; // 0x2
+    field public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12; // 0xc
     field public static final int FILTERING_COMPLETED_TIMING = 10; // 0xa
     field public static final int FILTERING_TIMED_OUT_TIMING = 11; // 0xb
     field public static final int HOLD_TIMING = 3; // 0x3
@@ -13037,6 +13194,7 @@
     field public static final int PRECISE_CALL_STATE_HOLDING = 2; // 0x2
     field public static final int PRECISE_CALL_STATE_IDLE = 0; // 0x0
     field public static final int PRECISE_CALL_STATE_INCOMING = 5; // 0x5
+    field public static final int PRECISE_CALL_STATE_INCOMING_SETUP = 9; // 0x9
     field public static final int PRECISE_CALL_STATE_NOT_VALID = -1; // 0xffffffff
     field public static final int PRECISE_CALL_STATE_WAITING = 6; // 0x6
   }
@@ -13322,6 +13480,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 getSubscriptionUserHandle(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int);
     method public void requestEmbeddedSubscriptionInfoListRefresh();
     method public void requestEmbeddedSubscriptionInfoListRefresh(int);
@@ -13331,6 +13490,7 @@
     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);
     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;
@@ -13626,6 +13786,7 @@
     field public static final String ACTION_SIM_SLOT_STATUS_CHANGED = "android.telephony.action.SIM_SLOT_STATUS_CHANGED";
     field public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3; // 0x3
     field public static final int ALLOWED_NETWORK_TYPES_REASON_POWER = 1; // 0x1
+    field public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4; // 0x4
     field public static final int CALL_WAITING_STATUS_DISABLED = 2; // 0x2
     field public static final int CALL_WAITING_STATUS_ENABLED = 1; // 0x1
     field public static final int CALL_WAITING_STATUS_FDN_CHECK_FAILURE = 5; // 0x5
@@ -13663,6 +13824,7 @@
     field public static final int INVALID_EMERGENCY_NUMBER_DB_VERSION = -1; // 0xffffffff
     field public static final int KEY_TYPE_EPDG = 1; // 0x1
     field public static final int KEY_TYPE_WLAN = 2; // 0x2
+    field public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3; // 0x3
     field public static final int MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL = 1; // 0x1
     field public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2; // 0x2
     field public static final int NR_DUAL_CONNECTIVITY_DISABLE = 2; // 0x2
@@ -14039,6 +14201,7 @@
     ctor public QualifiedNetworksService.NetworkAvailabilityProvider(int);
     method public abstract void close();
     method public final int getSlotIndex();
+    method public void reportEmergencyDataNetworkPreferredTransportChanged(int);
     method public void reportThrottleStatusChanged(@NonNull java.util.List<android.telephony.data.ThrottleStatus>);
     method public final void updateQualifiedNetworkTypes(int, @NonNull java.util.List<java.lang.Integer>);
   }
@@ -14121,7 +14284,8 @@
     field public static final int RESULT_CALLER_NOT_ALLOWED = -3; // 0xfffffffd
     field public static final int RESULT_EUICC_NOT_FOUND = -2; // 0xfffffffe
     field public static final int RESULT_OK = 0; // 0x0
-    field public static final int RESULT_PROFILE_NOT_FOUND = 1; // 0x1
+    field public static final int RESULT_PROFILE_DOES_NOT_EXIST = -4; // 0xfffffffc
+    field @Deprecated public static final int RESULT_PROFILE_NOT_FOUND = 1; // 0x1
     field public static final int RESULT_UNKNOWN_ERROR = -1; // 0xffffffff
   }
 
@@ -15288,6 +15452,16 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.SipMessage> CREATOR;
   }
 
+  public final class SrvccCall implements android.os.Parcelable {
+    ctor public SrvccCall(@NonNull String, int, @NonNull android.telephony.ims.ImsCallProfile);
+    method public int describeContents();
+    method @NonNull public String getCallId();
+    method @NonNull public android.telephony.ims.ImsCallProfile getImsCallProfile();
+    method public int getPreciseCallState();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.SrvccCall> CREATOR;
+  }
+
 }
 
 package android.telephony.ims.feature {
@@ -15348,6 +15522,10 @@
     method public final void notifyCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities);
     method public final void notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull android.os.Bundle);
     method public final void notifyRejectedCall(@NonNull android.telephony.ims.ImsCallProfile, @NonNull android.telephony.ims.ImsReasonInfo);
+    method public void notifySrvccCanceled();
+    method public void notifySrvccCompleted();
+    method public void notifySrvccFailed();
+    method public void notifySrvccStarted(@NonNull java.util.function.Consumer<java.util.List<android.telephony.ims.SrvccCall>>);
     method public final void notifyVoiceMessageCountUpdate(int);
     method public void onFeatureReady();
     method public void onFeatureRemoved();
@@ -15842,10 +16020,17 @@
 
 package android.view.accessibility {
 
+  public abstract class AccessibilityDisplayProxy {
+    ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>);
+    method public int getDisplayId();
+  }
+
   public final class AccessibilityManager {
     method public int getAccessibilityWindowId(@Nullable android.os.IBinder);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void performAccessibilityShortcut();
+    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean registerDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void registerSystemAction(@NonNull android.app.RemoteAction, int);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean unregisterDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void unregisterSystemAction(int);
   }
 
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 025e862..47588d9 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -3,6 +3,10 @@
     Method should return Collection<CharSequence> (or subclass) instead of raw array; was `java.lang.CharSequence[]`
 
 
+ExecutorRegistration: android.location.CountryDetector#addCountryListener(android.location.CountryListener, android.os.Looper):
+    Registration methods should have overload that accepts delivery Executor: `addCountryListener`
+
+
 GenericException: android.app.prediction.AppPredictor#finalize():
     Methods must not throw generic exceptions (`java.lang.Throwable`)
 GenericException: android.hardware.location.ContextHubClient#finalize():
@@ -127,6 +131,8 @@
     SAM-compatible parameters (such as parameter 1, "pw", in android.content.pm.PackageItemInfo.dumpFront) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
 SamShouldBeLast: android.content.pm.ResolveInfo#dump(android.util.Printer, String):
     SAM-compatible parameters (such as parameter 1, "pw", in android.content.pm.ResolveInfo.dump) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
+SamShouldBeLast: android.location.CountryDetector#addCountryListener(android.location.CountryListener, android.os.Looper):
+    SAM-compatible parameters (such as parameter 1, "listener", in android.location.CountryDetector.addCountryListener) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
 SamShouldBeLast: android.location.Location#dump(android.util.Printer, String):
     SAM-compatible parameters (such as parameter 1, "pw", in android.location.Location.dump) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
 SamShouldBeLast: android.location.LocationManager#addNmeaListener(android.location.OnNmeaMessageListener, android.os.Handler):
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1e4023e..3fee610 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -47,6 +47,7 @@
     field public static final String SET_KEYBOARD_LAYOUT = "android.permission.SET_KEYBOARD_LAYOUT";
     field public static final String SUSPEND_APPS = "android.permission.SUSPEND_APPS";
     field public static final String TEST_BIOMETRIC = "android.permission.TEST_BIOMETRIC";
+    field public static final String TEST_INPUT_METHOD = "android.permission.TEST_INPUT_METHOD";
     field public static final String TEST_MANAGE_ROLLBACKS = "android.permission.TEST_MANAGE_ROLLBACKS";
     field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS";
     field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG";
@@ -134,7 +135,7 @@
     method public static void resumeAppSwitches() throws android.os.RemoteException;
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean);
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
@@ -419,7 +420,7 @@
   }
 
   public final class SyncNotedAppOp implements android.os.Parcelable {
-    ctor public SyncNotedAppOp(int, @IntRange(from=0L) int, @Nullable String, @NonNull String);
+    ctor public SyncNotedAppOp(int, @IntRange(from=0L) int, @Nullable String, @Nullable String);
   }
 
   public class TaskInfo {
@@ -757,6 +758,7 @@
     method public int getUserId();
     method public void setAutofillOptions(@Nullable android.content.AutofillOptions);
     method public void setContentCaptureOptions(@Nullable android.content.ContentCaptureOptions);
+    field public static final String ATTENTION_SERVICE = "attention";
     field public static final String CONTENT_CAPTURE_MANAGER_SERVICE = "content_capture";
     field public static final String DEVICE_IDLE_CONTROLLER = "deviceidle";
     field public static final String DREAM_SERVICE = "dream";
@@ -903,6 +905,7 @@
     method public boolean isFull();
     method public boolean isGuest();
     method public boolean isInitialized();
+    method public boolean isMain();
     method public boolean isManagedProfile();
     method public boolean isPrimary();
     method public boolean isProfile();
@@ -921,6 +924,7 @@
     field public static final int FLAG_FULL = 1024; // 0x400
     field @Deprecated public static final int FLAG_GUEST = 4; // 0x4
     field public static final int FLAG_INITIALIZED = 16; // 0x10
+    field public static final int FLAG_MAIN = 16384; // 0x4000
     field @Deprecated public static final int FLAG_MANAGED_PROFILE = 32; // 0x20
     field public static final int FLAG_PRIMARY = 1; // 0x1
     field public static final int FLAG_PROFILE = 4096; // 0x1000
@@ -1170,6 +1174,20 @@
 
 }
 
+package android.hardware.camera2.params {
+
+  public final class ColorSpaceProfiles {
+    method @NonNull public java.util.Map<android.graphics.ColorSpace.Named,java.util.Map<java.lang.Integer,java.util.Set<java.lang.Long>>> getProfileMap();
+  }
+
+  public final class OutputConfiguration implements android.os.Parcelable {
+    method public void clearColorSpace();
+    method @Nullable public android.graphics.ColorSpace getColorSpace();
+    method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
+  }
+
+}
+
 package android.hardware.devicestate {
 
   public final class DeviceStateManager {
@@ -1237,6 +1255,7 @@
     method @RequiresPermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS) public boolean shouldAlwaysRespectAppRequestedMode();
     field public static final int SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS = 2; // 0x2
     field public static final int SWITCHING_TYPE_NONE = 0; // 0x0
+    field public static final int SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY = 3; // 0x3
     field public static final int SWITCHING_TYPE_WITHIN_GROUPS = 1; // 0x1
     field public static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 512; // 0x200
   }
@@ -1695,6 +1714,7 @@
   public class Build {
     method public static boolean is64BitAbi(String);
     method public static boolean isDebuggable();
+    method public static boolean isSecure();
     field public static final boolean IS_EMULATOR;
   }
 
@@ -1883,6 +1903,7 @@
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
     method public static boolean isSplitSystemUser();
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean isUserTypeEnabled(@NonNull String);
     method public boolean isUsersOnSecondaryDisplaysSupported();
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
   }
@@ -2482,6 +2503,10 @@
     method @NonNull public android.service.voice.AlwaysOnHotwordDetector.EventPayload.Builder setKeyphraseRecognitionExtras(@NonNull java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
   }
 
+  public abstract class HotwordDetectionService extends android.app.Service {
+    field public static final boolean ENABLE_PROXIMITY_RESULT = true;
+  }
+
   public final class VisibleActivityInfo implements android.os.Parcelable {
     ctor public VisibleActivityInfo(int, @NonNull android.os.IBinder);
   }
@@ -2605,13 +2630,23 @@
     method public int getCarrierIdListVersion();
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<java.lang.String> getCertsFromCarrierPrivilegeAccessRules();
     method @NonNull public java.util.List<android.telephony.data.ApnSetting> getDevicePolicyOverrideApns(@NonNull android.content.Context);
+    method @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getHalVersion(int);
     method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getLine1AlphaTag();
-    method public android.util.Pair<java.lang.Integer,java.lang.Integer> getRadioHalVersion();
+    method @Deprecated public android.util.Pair<java.lang.Integer,java.lang.Integer> getRadioHalVersion();
     method public boolean modifyDevicePolicyOverrideApn(@NonNull android.content.Context, int, @NonNull android.telephony.data.ApnSetting);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void refreshUiccProfile();
     method @Deprecated public void setCarrierTestOverride(String, String, String, String, String, String, String);
     method public void setCarrierTestOverride(String, String, String, String, String, String, String, String, String);
     method @RequiresPermission(android.Manifest.permission.BIND_TELECOM_CONNECTION_SERVICE) public void setVoiceServiceStateOverride(boolean);
+    field public static final int HAL_SERVICE_DATA = 1; // 0x1
+    field public static final int HAL_SERVICE_IMS = 7; // 0x7
+    field public static final int HAL_SERVICE_MESSAGING = 2; // 0x2
+    field public static final int HAL_SERVICE_MODEM = 3; // 0x3
+    field public static final int HAL_SERVICE_NETWORK = 4; // 0x4
+    field public static final int HAL_SERVICE_SIM = 5; // 0x5
+    field public static final int HAL_SERVICE_VOICE = 6; // 0x6
+    field public static final android.util.Pair HAL_VERSION_UNKNOWN;
+    field public static final android.util.Pair HAL_VERSION_UNSUPPORTED;
     field public static final int UNKNOWN_CARRIER_ID_LIST_VERSION = -1; // 0xffffffff
   }
 
@@ -2928,6 +2963,7 @@
     method public static int getHoverTooltipHideTimeout();
     method public static int getHoverTooltipShowTimeout();
     method public static int getLongPressTooltipHideTimeout();
+    method public static long getSendRecurringAccessibilityEventsInterval();
     method public boolean isPreferKeepClearForFocusEnabled();
   }
 
@@ -3142,12 +3178,12 @@
   }
 
   public final class InputMethodManager {
-    method public void addVirtualStylusIdForTestSession();
+    method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession();
     method public int getDisplayId();
-    method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);
+    method @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);
     method public boolean hasActiveInputConnection(@Nullable android.view.View);
-    method public boolean isInputMethodPickerShown();
-    method @RequiresPermission("android.permission.TEST_INPUT_METHOD") public void setStylusWindowIdleTimeoutForTest(long);
+    method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShown();
+    method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void setStylusWindowIdleTimeoutForTest(long);
     field public static final long CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING = 214016041L; // 0xcc1a029L
   }
 
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index f9b8a30..8e21d8c 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -567,6 +567,10 @@
     Missing nullability on parameter `destAddress` in method `checkSmsShortCodeDestination`
 MissingNullability: android.telephony.SmsManager#checkSmsShortCodeDestination(String, String) parameter #1:
     Missing nullability on parameter `countryIso` in method `checkSmsShortCodeDestination`
+MissingNullability: android.telephony.TelephonyManager#HAL_VERSION_UNKNOWN:
+    Missing nullability on field `HAL_VERSION_UNKNOWN` in class `class android.telephony.TelephonyManager`
+MissingNullability: android.telephony.TelephonyManager#HAL_VERSION_UNSUPPORTED:
+    Missing nullability on field `HAL_VERSION_UNSUPPORTED` in class `class android.telephony.TelephonyManager`
 MissingNullability: android.telephony.TelephonyManager#getLine1AlphaTag():
     Missing nullability on method `getLine1AlphaTag` return
 MissingNullability: android.telephony.TelephonyManager#getRadioHalVersion():
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index e86d2f3..2fe5d51 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -696,7 +696,8 @@
             ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
             ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS,
             ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT,
-            ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY
+            ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY,
+            ERROR_TAKE_SCREENSHOT_INVALID_WINDOW
     })
     public @interface ScreenshotErrorCode {}
 
@@ -728,6 +729,18 @@
     public static final int ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY = 4;
 
     /**
+     * The status of taking screenshot is failure and the reason is invalid accessibility window Id.
+     */
+    public static final int ERROR_TAKE_SCREENSHOT_INVALID_WINDOW = 5;
+
+    /**
+     * The status of taking screenshot is failure and the reason is the window contains secure
+     * content.
+     * @see WindowManager.LayoutParams#FLAG_SECURE
+     */
+    public static final int ERROR_TAKE_SCREENSHOT_SECURE_WINDOW = 6;
+
+    /**
      * The interval time of calling
      * {@link AccessibilityService#takeScreenshot(int, Executor, Consumer)} API.
      * @hide
@@ -2568,6 +2581,7 @@
      * @param executor Executor on which to run the callback.
      * @param callback The callback invoked when taking screenshot has succeeded or failed.
      *                 See {@link TakeScreenshotCallback} for details.
+     * @see #takeScreenshotOfWindow
      */
     public void takeScreenshot(int displayId, @NonNull @CallbackExecutor Executor executor,
             @NonNull TakeScreenshotCallback callback) {
@@ -2589,7 +2603,8 @@
                 final HardwareBuffer hardwareBuffer =
                         result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER, android.hardware.HardwareBuffer.class);
                 final ParcelableColorSpace colorSpace =
-                        result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, android.graphics.ParcelableColorSpace.class);
+                        result.getParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE,
+                                android.graphics.ParcelableColorSpace.class);
                 final ScreenshotResult screenshot = new ScreenshotResult(hardwareBuffer,
                         colorSpace.getColorSpace(),
                         result.getLong(KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP));
@@ -2601,6 +2616,37 @@
     }
 
     /**
+     * Takes a screenshot of the specified window and returns it via an
+     * {@link AccessibilityService.ScreenshotResult}. You can use {@link Bitmap#wrapHardwareBuffer}
+     * to construct the bitmap from the ScreenshotResult's payload.
+     * <p>
+     * <strong>Note:</strong> In order to take screenshots your service has
+     * to declare the capability to take screenshot by setting the
+     * {@link android.R.styleable#AccessibilityService_canTakeScreenshot}
+     * property in its meta-data. For details refer to {@link #SERVICE_META_DATA}.
+     * </p>
+     * <p>
+     * Both this method and {@link #takeScreenshot} can be used for machine learning-based visual
+     * screen understanding. Use <code>takeScreenshotOfWindow</code> if your target window might be
+     * visually underneath an accessibility overlay (from your or another accessibility service) in
+     * order to capture the window contents without the screenshot being covered by the overlay
+     * contents drawn on the screen.
+     * </p>
+     *
+     * @param accessibilityWindowId The window id, from {@link AccessibilityWindowInfo#getId()}.
+     * @param executor Executor on which to run the callback.
+     * @param callback The callback invoked when taking screenshot has succeeded or failed.
+     *                 See {@link TakeScreenshotCallback} for details.
+     * @see #takeScreenshot
+     */
+    public void takeScreenshotOfWindow(int accessibilityWindowId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull TakeScreenshotCallback callback) {
+        AccessibilityInteractionClient.getInstance(this).takeScreenshotOfWindow(
+                        mConnectionId, accessibilityWindowId, executor, callback);
+    }
+
+    /**
      * Sets the strokeWidth and color of the accessibility focus rectangle.
      * <p>
      * <strong>Note:</strong> This setting persists until this or another active
@@ -3113,7 +3159,8 @@
         private final @NonNull ColorSpace mColorSpace;
         private final long mTimestamp;
 
-        private ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer,
+        /** @hide */
+        public ScreenshotResult(@NonNull HardwareBuffer hardwareBuffer,
                 @NonNull ColorSpace colorSpace, long timestamp) {
             Preconditions.checkNotNull(hardwareBuffer, "hardwareBuffer cannot be null");
             Preconditions.checkNotNull(colorSpace, "colorSpace cannot be null");
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 9abce3a..da14b50 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -30,6 +30,7 @@
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
 import android.view.accessibility.AccessibilityWindowInfo;
 import java.util.List;
+import android.window.ScreenCapture;
 
 /**
  * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService.
@@ -122,6 +123,10 @@
 
     void takeScreenshot(int displayId, in RemoteCallback callback);
 
+    void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId,
+        in ScreenCapture.ScreenCaptureListener listener,
+        IAccessibilityInteractionConnectionCallback callback);
+
     void setGestureDetectionPassthroughRegion(int displayId, in Region region);
 
     void setTouchExplorationPassthroughRegion(int displayId, in Region region);
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java
index c2c065b..45515dd 100644
--- a/core/java/android/accounts/AbstractAccountAuthenticator.java
+++ b/core/java/android/accounts/AbstractAccountAuthenticator.java
@@ -117,27 +117,27 @@
     /**
      * Bundle key used for the {@link String} account type in session bundle.
      * This is used in the default implementation of
-     * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
+     * {@link #startAddAccountSession} and {@link #startUpdateCredentialsSession}.
      */
     private static final String KEY_AUTH_TOKEN_TYPE =
             "android.accounts.AbstractAccountAuthenticato.KEY_AUTH_TOKEN_TYPE";
     /**
      * Bundle key used for the {@link String} array of required features in
      * session bundle. This is used in the default implementation of
-     * {@link #startAddAccountSession} and {@link startUpdateCredentialsSession}.
+     * {@link #startAddAccountSession} and {@link #startUpdateCredentialsSession}.
      */
     private static final String KEY_REQUIRED_FEATURES =
             "android.accounts.AbstractAccountAuthenticator.KEY_REQUIRED_FEATURES";
     /**
      * Bundle key used for the {@link Bundle} options in session bundle. This is
      * used in default implementation of {@link #startAddAccountSession} and
-     * {@link startUpdateCredentialsSession}.
+     * {@link #startUpdateCredentialsSession}.
      */
     private static final String KEY_OPTIONS =
             "android.accounts.AbstractAccountAuthenticator.KEY_OPTIONS";
     /**
      * Bundle key used for the {@link Account} account in session bundle. This is used
-     * used in default implementation of {@link startUpdateCredentialsSession}.
+     * used in default implementation of {@link #startUpdateCredentialsSession}.
      */
     private static final String KEY_ACCOUNT =
             "android.accounts.AbstractAccountAuthenticator.KEY_ACCOUNT";
@@ -154,6 +154,8 @@
         public void addAccount(IAccountAuthenticatorResponse response, String accountType,
                 String authTokenType, String[] features, Bundle options)
                 throws RemoteException {
+            super.addAccount_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "addAccount: accountType " + accountType
                         + ", authTokenType " + authTokenType
@@ -184,6 +186,8 @@
         @Override
         public void confirmCredentials(IAccountAuthenticatorResponse response,
                 Account account, Bundle options) throws RemoteException {
+            super.confirmCredentials_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "confirmCredentials: " + account);
             }
@@ -210,6 +214,8 @@
         public void getAuthTokenLabel(IAccountAuthenticatorResponse response,
                 String authTokenType)
                 throws RemoteException {
+            super.getAuthTokenLabel_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "getAuthTokenLabel: authTokenType " + authTokenType);
             }
@@ -235,6 +241,8 @@
         public void getAuthToken(IAccountAuthenticatorResponse response,
                 Account account, String authTokenType, Bundle loginOptions)
                 throws RemoteException {
+            super.getAuthToken_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "getAuthToken: " + account
                         + ", authTokenType " + authTokenType);
@@ -262,6 +270,8 @@
         @Override
         public void updateCredentials(IAccountAuthenticatorResponse response, Account account,
                 String authTokenType, Bundle loginOptions) throws RemoteException {
+            super.updateCredentials_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "updateCredentials: " + account
                         + ", authTokenType " + authTokenType);
@@ -291,6 +301,8 @@
         @Override
         public void editProperties(IAccountAuthenticatorResponse response,
                 String accountType) throws RemoteException {
+            super.editProperties_enforcePermission();
+
             try {
                 final Bundle result = AbstractAccountAuthenticator.this.editProperties(
                     new AccountAuthenticatorResponse(response), accountType);
@@ -306,6 +318,8 @@
         @Override
         public void hasFeatures(IAccountAuthenticatorResponse response,
                 Account account, String[] features) throws RemoteException {
+            super.hasFeatures_enforcePermission();
+
             try {
                 final Bundle result = AbstractAccountAuthenticator.this.hasFeatures(
                     new AccountAuthenticatorResponse(response), account, features);
@@ -321,6 +335,8 @@
         @Override
         public void getAccountRemovalAllowed(IAccountAuthenticatorResponse response,
                 Account account) throws RemoteException {
+            super.getAccountRemovalAllowed_enforcePermission();
+
             try {
                 final Bundle result = AbstractAccountAuthenticator.this.getAccountRemovalAllowed(
                     new AccountAuthenticatorResponse(response), account);
@@ -336,6 +352,8 @@
         @Override
         public void getAccountCredentialsForCloning(IAccountAuthenticatorResponse response,
                 Account account) throws RemoteException {
+            super.getAccountCredentialsForCloning_enforcePermission();
+
             try {
                 final Bundle result =
                         AbstractAccountAuthenticator.this.getAccountCredentialsForCloning(
@@ -353,6 +371,8 @@
         public void addAccountFromCredentials(IAccountAuthenticatorResponse response,
                 Account account,
                 Bundle accountCredentials) throws RemoteException {
+            super.addAccountFromCredentials_enforcePermission();
+
             try {
                 final Bundle result =
                         AbstractAccountAuthenticator.this.addAccountFromCredentials(
@@ -371,6 +391,8 @@
         public void startAddAccountSession(IAccountAuthenticatorResponse response,
                 String accountType, String authTokenType, String[] features, Bundle options)
                 throws RemoteException {
+            super.startAddAccountSession_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG,
                         "startAddAccountSession: accountType " + accountType
@@ -403,6 +425,8 @@
                 Account account,
                 String authTokenType,
                 Bundle loginOptions) throws RemoteException {
+            super.startUpdateCredentialsSession_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "startUpdateCredentialsSession: "
                         + account
@@ -441,6 +465,8 @@
                 IAccountAuthenticatorResponse response,
                 String accountType,
                 Bundle sessionBundle) throws RemoteException {
+            super.finishSession_enforcePermission();
+
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, "finishSession: accountType " + accountType);
             }
@@ -468,6 +494,8 @@
                 IAccountAuthenticatorResponse response,
                 Account account,
                 String statusToken) throws RemoteException {
+            super.isCredentialsUpdateSuggested_enforcePermission();
+
             try {
                 final Bundle result = AbstractAccountAuthenticator.this
                         .isCredentialsUpdateSuggested(
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index a573776..b3db38d 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -27,7 +27,6 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.UserHandleAware;
-import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.PropertyInvalidatedCache;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -37,6 +36,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.database.SQLException;
 import android.os.Build;
@@ -348,43 +348,11 @@
     */
     public static final int CACHE_ACCOUNTS_DATA_SIZE = 4;
 
-    private static final class UserIdPackage
-    {
-        @UserIdInt
-        public int userId;
-        public String packageName;
-
-        public UserIdPackage(int UserId, String PackageName) {
-            this.userId = UserId;
-            this.packageName = PackageName;
-        }
-
-        @Override
-        public boolean equals(@Nullable Object o) {
-            if (o == null) {
-                return false;
-            }
-            if (o == this) {
-                return true;
-            }
-            if (o.getClass() != getClass()) {
-                return false;
-            }
-            UserIdPackage e = (UserIdPackage) o;
-            return e.userId == userId && e.packageName.equals(packageName);
-        }
-
-        @Override
-        public int hashCode() {
-            return userId ^ packageName.hashCode();
-        }
-    }
-
-    PropertyInvalidatedCache<UserIdPackage, Account[]> mAccountsForUserCache =
-                new PropertyInvalidatedCache<UserIdPackage, Account[]>(
+    PropertyInvalidatedCache<UserPackage, Account[]> mAccountsForUserCache =
+                new PropertyInvalidatedCache<UserPackage, Account[]>(
                 CACHE_ACCOUNTS_DATA_SIZE, CACHE_KEY_ACCOUNTS_DATA_PROPERTY) {
         @Override
-        public Account[] recompute(UserIdPackage userAndPackage) {
+        public Account[] recompute(UserPackage userAndPackage) {
             try {
                 return mService.getAccountsAsUser(null, userAndPackage.userId, userAndPackage.packageName);
             } catch (RemoteException e) {
@@ -392,7 +360,7 @@
             }
         }
         @Override
-        public boolean bypass(UserIdPackage query) {
+        public boolean bypass(UserPackage query) {
             return query.userId < 0;
         }
         @Override
@@ -731,7 +699,7 @@
      */
     @NonNull
     public Account[] getAccountsAsUser(int userId) {
-        UserIdPackage userAndPackage = new UserIdPackage(userId, mContext.getOpPackageName());
+        UserPackage userAndPackage = UserPackage.of(userId, mContext.getOpPackageName());
         return mAccountsForUserCache.query(userAndPackage);
     }
 
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 1403ba2..dcabf57 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -219,12 +219,14 @@
             return;
         }
         for (int i = 0; i < mAnimationCallbacks.size(); ++i) {
-            Animator animator = ((Animator) mAnimationCallbacks.get(i));
-            if (animator != null
-                    && animator.getTotalDuration() == Animator.DURATION_INFINITE
-                    && !animator.isPaused()) {
-                mPausedAnimators.add(animator);
-                animator.pause();
+            AnimationFrameCallback callback = mAnimationCallbacks.get(i);
+            if (callback instanceof Animator) {
+                Animator animator = ((Animator) callback);
+                if (animator.getTotalDuration() == Animator.DURATION_INFINITE
+                        && !animator.isPaused()) {
+                    mPausedAnimators.add(animator);
+                    animator.pause();
+                }
             }
         }
     };
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 32d0d75..30ff052 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1685,7 +1685,7 @@
                 .isOnBackInvokedCallbackEnabled(this);
         if (aheadOfTimeBack) {
             // Add onBackPressed as default back behavior.
-            mDefaultBackCallback = this::navigateBack;
+            mDefaultBackCallback = this::onBackInvoked;
             getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback);
         }
     }
@@ -4003,22 +4003,19 @@
         if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {
             return;
         }
-        navigateBack();
+        onBackInvoked();
     }
 
-    private void navigateBack() {
-        if (!isTaskRoot()) {
-            // If the activity is not the root of the task, allow finish to proceed normally.
-            finishAfterTransition();
-            return;
-        }
-        // Inform activity task manager that the activity received a back press while at the
-        // root of the task. This call allows ActivityTaskManager to intercept or move the task
-        // to the back.
-        ActivityClient.getInstance().onBackPressedOnTaskRoot(mToken,
+    private void onBackInvoked() {
+        // Inform activity task manager that the activity received a back press.
+        // This call allows ActivityTaskManager to intercept or move the task
+        // to the back when needed.
+        ActivityClient.getInstance().onBackPressed(mToken,
                 new RequestFinishCallback(new WeakReference<>(this)));
 
-        getAutofillClientController().onActivityBackPressed(mIntent);
+        if (isTaskRoot()) {
+            getAutofillClientController().onActivityBackPressed(mIntent);
+        }
     }
 
     /**
@@ -8364,11 +8361,17 @@
     }
 
     final void performNewIntent(@NonNull Intent intent) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performNewIntent");
         mCanEnterPictureInPicture = true;
         onNewIntent(intent);
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
     }
 
     final void performStart(String reason) {
+        if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
+            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performStart:"
+                    + mComponent.getClassName());
+        }
         dispatchActivityPreStarted();
         mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
         mFragments.noteStateNotSaved();
@@ -8415,6 +8418,7 @@
 
         mActivityTransitionState.enterReady(this);
         dispatchActivityPostStarted();
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
     }
 
     /**
@@ -8423,7 +8427,8 @@
      *              The option to not start immediately is needed in case a transaction with
      *              multiple lifecycle transitions is in progress.
      */
-    final void performRestart(boolean start, String reason) {
+    final void performRestart(boolean start) {
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performRestart");
         mCanEnterPictureInPicture = true;
         mFragments.noteStateNotSaved();
 
@@ -8458,17 +8463,18 @@
             final long startTime = SystemClock.uptimeMillis();
             mInstrumentation.callActivityOnRestart(this);
             final long duration = SystemClock.uptimeMillis() - startTime;
-            EventLogTags.writeWmOnRestartCalled(mIdent, getComponentName().getClassName(), reason,
-                    duration);
+            EventLogTags.writeWmOnRestartCalled(mIdent, getComponentName().getClassName(),
+                    "performRestart", duration);
             if (!mCalled) {
                 throw new SuperNotCalledException(
                     "Activity " + mComponent.toShortString() +
                     " did not call through to super.onRestart()");
             }
             if (start) {
-                performStart(reason);
+                performStart("performRestart");
             }
         }
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
     }
 
     final void performResume(boolean followedByPause, String reason) {
@@ -8477,7 +8483,6 @@
                     + mComponent.getClassName());
         }
         dispatchActivityPreResumed();
-        performRestart(true /* start */, reason);
 
         mFragments.execPendingActions();
 
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index d1e6780..4cf48ab 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -525,9 +525,9 @@
         }
     }
 
-    void onBackPressedOnTaskRoot(IBinder token, IRequestFinishCallback callback) {
+    void onBackPressed(IBinder token, IRequestFinishCallback callback) {
         try {
-            getActivityClientController().onBackPressedOnTaskRoot(token, callback);
+            getActivityClientController().onBackPressed(token, callback);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 576b572..d6c10ae 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -81,8 +81,6 @@
 import android.util.DisplayMetrics;
 import android.util.Singleton;
 import android.util.Size;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.window.TaskSnapshot;
 
 import com.android.internal.app.LocalePicker;
@@ -92,6 +90,8 @@
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.MemInfoReader;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import java.io.FileDescriptor;
@@ -3953,6 +3953,10 @@
      * processes to reclaim memory; the system will take care of restarting
      * these processes in the future as needed.
      *
+     * <p class="note">On devices with a {@link Build.VERSION#SECURITY_PATCH} of 2022-12-01 or
+     * greater, third party applications can only use this API to kill their own processes.
+     * </p>
+     *
      * @param packageName The name of the package whose processes are to
      * be killed.
      */
@@ -4401,7 +4405,7 @@
      */
     @TestApi
     @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.CREATE_USERS})
+            android.Manifest.permission.INTERACT_ACROSS_USERS})
     public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId,
             int displayId) {
         if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 81aa6da..dc7331f 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -221,6 +221,18 @@
     public abstract boolean isSystemReady();
 
     /**
+     * @return {@code true} if system is using the "modern" broadcast queue,
+     *         {@code false} otherwise.
+     */
+    public abstract boolean isModernQueueEnabled();
+
+    /**
+     * Enforce capability restrictions on use of the given BroadcastOptions
+     */
+    public abstract void enforceBroadcastOptionsPermissions(@Nullable Bundle options,
+            int callingUid);
+
+    /**
      * Returns package name given pid.
      *
      * @param pid The pid we are searching package name for.
@@ -294,7 +306,7 @@
     public abstract int handleIncomingUser(int callingPid, int callingUid, @UserIdInt int userId,
             boolean allowAll, int allowMode, String name, String callerPackage);
 
-    /** Checks if the calling binder pid as the permission. */
+    /** Checks if the calling binder pid/uid has the given permission. */
     @PermissionMethod
     public abstract void enforceCallingPermission(@PermissionName String permission, String func);
 
@@ -383,6 +395,10 @@
      */
     public abstract boolean isAppStartModeDisabled(int uid, String packageName);
 
+    /**
+     * Returns the ids of the current user and all of its profiles (if any), regardless of the
+     * running state of the profiles.
+     */
     public abstract int[] getCurrentProfileIds();
     public abstract UserInfo getCurrentUser();
     public abstract void ensureNotSpecialUser(@UserIdInt int userId);
@@ -420,10 +436,11 @@
 
     public abstract int broadcastIntentInPackage(String packageName, @Nullable String featureId,
             int uid, int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
-            IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
-            String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-            @UserIdInt int userId, boolean allowBackgroundActivityStarts,
-            @Nullable IBinder backgroundActivityStartsToken, @Nullable int[] broadcastAllowList);
+            IApplicationThread resultToThread, IIntentReceiver resultTo, int resultCode,
+            String resultData, Bundle resultExtras, String requiredPermission, Bundle bOptions,
+            boolean serialized, boolean sticky, @UserIdInt int userId,
+            boolean allowBackgroundActivityStarts, @Nullable IBinder backgroundActivityStartsToken,
+            @Nullable int[] broadcastAllowList);
 
     public abstract ComponentName startServiceInPackage(int uid, Intent service,
             String resolvedType, boolean fgRequired, String callingPackage,
@@ -632,6 +649,8 @@
      * using the rules of package visibility. Returns extras with legitimate package info that the
      * receiver is able to access, or {@code null} if none of the packages is visible to the
      * receiver.
+     * @param serialized Specifies whether or not the broadcast should be delivered to the
+     *                   receivers in a serial order.
      *
      * @see com.android.server.am.ActivityManagerService#broadcastIntentWithFeature(
      *      IApplicationThread, String, Intent, String, IIntentReceiver, int, String, Bundle,
@@ -645,6 +664,19 @@
             @Nullable Bundle bOptions);
 
     /**
+     * Variant of
+     * {@link #broadcastIntent(Intent, IIntentReceiver, String[], boolean, int, int[], BiFunction, Bundle)}
+     * that allows sender to receive a finish callback once the broadcast delivery is completed,
+     * but provides no ordering guarantee for how the broadcast is delivered to receivers.
+     */
+    public abstract int broadcastIntentWithCallback(Intent intent,
+            IIntentReceiver resultTo,
+            String[] requiredPermissions,
+            int userId, int[] appIdAllowList,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
+            @Nullable Bundle bOptions);
+
+    /**
      * Add uid to the ActivityManagerService PendingStartActivityUids list.
      * @param uid uid
      * @param pid pid of the ProcessRecord that is pending top.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 7a9f3c1..1f63343 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -173,7 +173,6 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
 import android.view.Display;
-import android.view.DisplayAdjustments;
 import android.view.SurfaceControl;
 import android.view.ThreadedRenderer;
 import android.view.View;
@@ -244,7 +243,6 @@
 import java.util.TimeZone;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
 
 /**
  * This manages the execution of the main thread in an
@@ -409,7 +407,6 @@
     boolean mInstrumentingWithoutRestart;
     boolean mSystemThread = false;
     boolean mSomeActivitiesChanged = false;
-    /* package */ boolean mHiddenApiWarningShown = false;
 
     // These can be accessed by multiple threads; mResourcesManager is the lock.
     // XXX For now we keep around information about all packages we have
@@ -438,16 +435,10 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private final ResourcesManager mResourcesManager;
 
-    /** The active adjustments that override the {@link DisplayAdjustments} in resources. */
-    private ArrayList<Pair<IBinder, Consumer<DisplayAdjustments>>> mActiveRotationAdjustments;
-
     // Registry of remote cancellation transports pending a reply with reply handles.
     @GuardedBy("this")
     private @Nullable Map<SafeCancellationTransport, CancellationSignal> mRemoteCancellations;
 
-    private final Map<IBinder, Integer> mLastReportedWindowingMode = Collections.synchronizedMap(
-            new ArrayMap<>());
-
     private static final class ProviderKey {
         final String authority;
         final int userId;
@@ -518,8 +509,6 @@
      */
     private final Object mCoreSettingsLock = new Object();
 
-    boolean mHasImeComponent = false;
-
     private IContentCaptureOptionsCallback.Stub mContentCaptureOptionsCallback = null;
 
     /** A client side controller to handle process level configuration changes. */
@@ -599,6 +588,12 @@
         /** Whether this activiy was launched from a bubble. */
         boolean mLaunchedFromBubble;
 
+        /**
+         * This can be different from the current configuration because a new configuration may not
+         * always update to activity, e.g. windowing mode change without size change.
+         */
+        int mLastReportedWindowingMode = WINDOWING_MODE_UNDEFINED;
+
         @LifecycleState
         private int mLifecycleState = PRE_ON_CREATE;
 
@@ -658,7 +653,8 @@
                                 "Received config update for non-existing activity");
                     }
                     activity.mMainThread.handleActivityConfigurationChanged(
-                            ActivityClientRecord.this, overrideConfig, newDisplayId);
+                            ActivityClientRecord.this, overrideConfig, newDisplayId,
+                            false /* alwaysReportChange */);
                 }
 
                 @Override
@@ -3653,8 +3649,7 @@
                         "Activity " + r.intent.getComponent().toShortString() +
                         " did not call through to super.onCreate()");
                 }
-                mLastReportedWindowingMode.put(activity.getActivityToken(),
-                        config.windowConfiguration.getWindowingMode());
+                r.mLastReportedWindowingMode = config.windowConfiguration.getWindowingMode();
             }
             r.setState(ON_CREATE);
 
@@ -3713,12 +3708,14 @@
         // Call postOnCreate()
         if (pendingActions.shouldCallOnPostCreate()) {
             activity.mCalled = false;
+            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "onPostCreate");
             if (r.isPersistable()) {
                 mInstrumentation.callActivityOnPostCreate(activity, r.state,
                         r.persistentState);
             } else {
                 mInstrumentation.callActivityOnPostCreate(activity, r.state);
             }
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
             if (!activity.mCalled) {
                 throw new SuperNotCalledException(
                         "Activity " + r.intent.getComponent().toShortString()
@@ -5267,7 +5264,7 @@
     @Override
     public void performRestartActivity(ActivityClientRecord r, boolean start) {
         if (r.stopped) {
-            r.activity.performRestart(start, "performRestartActivity");
+            r.activity.performRestart(start);
             if (start) {
                 r.setState(ON_START);
             }
@@ -5284,7 +5281,7 @@
     private void onCoreSettingsChange() {
         if (updateDebugViewAttributeState()) {
             // request all activities to relaunch for the changes to take place
-            relaunchAllActivities(false /* preserveWindows */, "onCoreSettingsChange");
+            relaunchAllActivities(true /* preserveWindows */, "onCoreSettingsChange");
         }
     }
 
@@ -5432,7 +5429,6 @@
             }
         }
         r.setState(ON_DESTROY);
-        mLastReportedWindowingMode.remove(r.activity.getActivityToken());
         schedulePurgeIdler();
         synchronized (this) {
             if (mSplashScreenGlobal != null) {
@@ -5859,20 +5855,20 @@
      * @return {@link Configuration} instance sent to client, null if not sent.
      */
     private Configuration performConfigurationChangedForActivity(ActivityClientRecord r,
-            Configuration newBaseConfig, int displayId) {
+            Configuration newBaseConfig, int displayId, boolean alwaysReportChange) {
         r.tmpConfig.setTo(newBaseConfig);
         if (r.overrideConfig != null) {
             r.tmpConfig.updateFrom(r.overrideConfig);
         }
-        final Configuration reportedConfig = performActivityConfigurationChanged(r.activity,
-                r.tmpConfig, r.overrideConfig, displayId);
+        final Configuration reportedConfig = performActivityConfigurationChanged(r,
+                r.tmpConfig, r.overrideConfig, displayId, alwaysReportChange);
         freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
         return reportedConfig;
     }
 
     /**
      * Decides whether to update an Activity's configuration and whether to inform it.
-     * @param activity The activity to notify of configuration change.
+     * @param r The activity client record to notify of configuration change.
      * @param newConfig The new configuration.
      * @param amOverrideConfig The override config that differentiates the Activity's configuration
      *                         from the base global configuration. This is supplied by
@@ -5880,29 +5876,30 @@
      * @param displayId Id of the display where activity currently resides.
      * @return Configuration sent to client, null if no changes and not moved to different display.
      */
-    private Configuration performActivityConfigurationChanged(Activity activity,
-            Configuration newConfig, Configuration amOverrideConfig, int displayId) {
+    private Configuration performActivityConfigurationChanged(ActivityClientRecord r,
+            Configuration newConfig, Configuration amOverrideConfig, int displayId,
+            boolean alwaysReportChange) {
+        final Activity activity = r.activity;
         final IBinder activityToken = activity.getActivityToken();
 
         // WindowConfiguration differences aren't considered as public, check it separately.
         // multi-window / pip mode changes, if any, should be sent before the configuration
         // change callback, see also PinnedStackTests#testConfigurationChangeOrderDuringTransition
-        handleWindowingModeChangeIfNeeded(activity, newConfig);
+        handleWindowingModeChangeIfNeeded(r, newConfig);
 
         final boolean movedToDifferentDisplay = isDifferentDisplay(activity.getDisplayId(),
                 displayId);
         final Configuration currentResConfig = activity.getResources().getConfiguration();
         final int diff = currentResConfig.diffPublicOnly(newConfig);
         final boolean hasPublicResConfigChange = diff != 0;
-        final ActivityClientRecord r = getActivityClient(activityToken);
         // TODO(b/173090263): Use diff instead after the improvement of AssetManager and
         // ResourcesImpl constructions.
         final boolean shouldUpdateResources = hasPublicResConfigChange
                 || shouldUpdateResources(activityToken, currentResConfig, newConfig,
                 amOverrideConfig, movedToDifferentDisplay, hasPublicResConfigChange);
-        final boolean shouldReportChange = shouldReportChange(activity.mCurrentConfig, newConfig,
-                r != null ? r.mSizeConfigurations : null,
-                activity.mActivityInfo.getRealConfigChanged());
+        final boolean shouldReportChange = shouldReportChange(
+                activity.mCurrentConfig, newConfig, r.mSizeConfigurations,
+                activity.mActivityInfo.getRealConfigChanged(), alwaysReportChange);
         // Nothing significant, don't proceed with updating and reporting.
         if (!shouldUpdateResources && !shouldReportChange) {
             return null;
@@ -5962,12 +5959,18 @@
     @VisibleForTesting
     public static boolean shouldReportChange(@Nullable Configuration currentConfig,
             @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets sizeBuckets,
-            int handledConfigChanges) {
+            int handledConfigChanges, boolean alwaysReportChange) {
         final int publicDiff = currentConfig.diffPublicOnly(newConfig);
         // Don't report the change if there's no public diff between current and new config.
         if (publicDiff == 0) {
             return false;
         }
+
+        // Report the change regardless if the changes across size-config-buckets.
+        if (alwaysReportChange) {
+            return true;
+        }
+
         final int diffWithBucket = SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig,
                 newConfig, sizeBuckets);
         // Compare to the diff which filter the change without crossing size buckets with
@@ -6004,12 +6007,11 @@
      * See also {@link Activity#onMultiWindowModeChanged(boolean, Configuration)} and
      * {@link Activity#onPictureInPictureModeChanged(boolean, Configuration)}
      */
-    private void handleWindowingModeChangeIfNeeded(Activity activity,
+    private void handleWindowingModeChangeIfNeeded(ActivityClientRecord r,
             Configuration newConfiguration) {
+        final Activity activity = r.activity;
         final int newWindowingMode = newConfiguration.windowConfiguration.getWindowingMode();
-        final IBinder token = activity.getActivityToken();
-        final int oldWindowingMode = mLastReportedWindowingMode.getOrDefault(token,
-                WINDOWING_MODE_UNDEFINED);
+        final int oldWindowingMode = r.mLastReportedWindowingMode;
         if (oldWindowingMode == newWindowingMode) return;
         // PiP callback is sent before the MW one.
         if (newWindowingMode == WINDOWING_MODE_PINNED) {
@@ -6024,7 +6026,7 @@
         if (wasInMultiWindowMode != nowInMultiWindowMode) {
             activity.dispatchMultiWindowModeChanged(nowInMultiWindowMode, newConfiguration);
         }
-        mLastReportedWindowingMode.put(token, newWindowingMode);
+        r.mLastReportedWindowingMode = newWindowingMode;
     }
 
     /**
@@ -6094,6 +6096,18 @@
         }
     }
 
+    @Override
+    public void handleActivityConfigurationChanged(ActivityClientRecord r,
+            @NonNull Configuration overrideConfig, int displayId) {
+        handleActivityConfigurationChanged(r, overrideConfig, displayId,
+                // This is the only place that uses alwaysReportChange=true. The entry point should
+                // be from ActivityConfigurationChangeItem or MoveToDisplayItem, so the server side
+                // has confirmed the activity should handle the configuration instead of relaunch.
+                // If Activity#onConfigurationChanged is called unexpectedly, then we can know it is
+                // something wrong from server side.
+                true /* alwaysReportChange */);
+    }
+
     /**
      * Handle new activity configuration and/or move to a different display. This method is a noop
      * if {@link #updatePendingActivityConfiguration(IBinder, Configuration)} has been
@@ -6104,9 +6118,8 @@
      * @param displayId Id of the display where activity was moved to, -1 if there was no move and
      *                  value didn't change.
      */
-    @Override
-    public void handleActivityConfigurationChanged(ActivityClientRecord r,
-            @NonNull Configuration overrideConfig, int displayId) {
+    void handleActivityConfigurationChanged(ActivityClientRecord r,
+            @NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) {
         synchronized (mPendingOverrideConfigs) {
             final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(r.token);
             if (overrideConfig.isOtherSeqNewer(pendingOverrideConfig)) {
@@ -6150,7 +6163,8 @@
         }
         final Configuration reportedConfig = performConfigurationChangedForActivity(r,
                 mConfigurationController.getCompatConfiguration(),
-                movedToDifferentDisplay ? displayId : r.activity.getDisplayId());
+                movedToDifferentDisplay ? displayId : r.activity.getDisplayId(),
+                alwaysReportChange);
         // Notify the ViewRootImpl instance about configuration changes. It may have initiated this
         // update to make sure that resources are updated before updating itself.
         if (viewRoot != null) {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 0cb00d9..d5879fb 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -25,6 +25,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
@@ -41,6 +42,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.database.DatabaseUtils;
+import android.healthconnect.HealthConnectManager;
 import android.media.AudioAttributes.AttributeUsage;
 import android.os.Binder;
 import android.os.Build;
@@ -866,7 +868,7 @@
 
     // when adding one of these:
     //  - increment _NUM_OP
-    //  - define an OPSTR_* constant (marked as @SystemApi)
+    //  - define an OPSTR_* constant (and mark as @SystemApi if needed)
     //  - add row to sAppOpInfos
     //  - add descriptive strings to Settings/res/values/arrays.xml
     //  - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
@@ -1342,9 +1344,104 @@
     public static final int OP_RECEIVE_AMBIENT_TRIGGER_AUDIO =
             AppProtoEnums.APP_OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 
+     /**
+      * Receive audio from near-field mic (ie. TV remote)
+      * Allows audio recording regardless of sensor privacy state,
+      *  as it is an intentional user interaction: hold-to-talk
+      *
+      * @hide
+      */
+    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;
+
+    /**
+     * Notify apps that they have been granted URI permission photos
+     *
+     * @hide
+     */
+    public static final int OP_READ_MEDIA_VISUAL_USER_SELECTED =
+            AppProtoEnums.APP_OP_READ_MEDIA_VISUAL_USER_SELECTED;
+
+    /**
+     * Prevent an app from being placed into app standby buckets.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final int OP_SYSTEM_EXEMPT_FROM_APP_STANDBY =
+            AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_APP_STANDBY;
+
+    /**
+     * Prevent an app from being placed into forced app standby.
+     * {@link ActivityManager#isBackgroundRestricted()}
+     * {@link #OP_RUN_ANY_IN_BACKGROUND}
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final int OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY =
+            AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY;
+
+    /**
+     * An app op for reading/writing health connect data.
+     *
+     * @hide
+     */
+    public static final int OP_READ_WRITE_HEALTH_DATA = AppProtoEnums.APP_OP_READ_WRITE_HEALTH_DATA;
+
+    /**
+     * Use foreground service with the type
+     * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}.
+     *
+     * @hide
+     */
+    public static final int OP_FOREGROUND_SERVICE_SPECIAL_USE =
+            AppProtoEnums.APP_OP_FOREGROUND_SERVICE_SPECIAL_USE;
+
+    /**
+     * Exempt from start foreground service from background restriction.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final int OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION =
+            AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION;
+
+    /**
+     * Exempt from start foreground service from background with while in user permission
+     * restriction.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final int OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION =
+            AppProtoEnums
+                    .APP_OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION;
+
+    /**
+     * Hide foreground service stop button in quick settings.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final int OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON =
+            AppProtoEnums.APP_OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 121;
+    public static final int _NUM_OP = 131;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1815,6 +1912,104 @@
     @SystemApi
     public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO =
             "android:receive_ambient_trigger_audio";
+    /**
+     * Notify apps that they have been granted URI permission photos
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED =
+            "android:read_media_visual_user_selected";
+
+    /**
+     * An app op for reading/writing health connect data.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String OPSTR_READ_WRITE_HEALTH_DATA =
+            "android:read_write_health_data";
+
+    /**
+     * Record audio from near-field microphone (ie. TV remote)
+     * Allows audio recording regardless of sensor privacy state,
+     *  as it is an intentional user interaction: hold-to-talk
+     *
+     * @hide
+     */
+    @SystemApi
+    @SuppressLint("IntentName")
+    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";
+
+    /**
+     * Prevent an app from being placed into app standby buckets.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final String OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY =
+            "android:system_exempt_from_app_standby";
+
+    /**
+     * Prevent an app from being placed into forced app standby.
+     * {@link ActivityManager#isBackgroundRestricted()}
+     * {@link #OP_RUN_ANY_IN_BACKGROUND}
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final String OPSTR_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY =
+            "android:system_exempt_from_forced_app_standby";
+
+    /**
+     * Start a foreground service with the type "specialUse".
+     *
+     * @hide
+     */
+    public static final String OPSTR_FOREGROUND_SERVICE_SPECIAL_USE =
+            "android:foreground_service_special_use";
+
+    /**
+     * Exempt from start foreground service from background restriction.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final String OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION =
+            "android:system_exempt_from_fgs_bg_start_restriction";
+
+    /**
+     * Exempt from start foreground service from background with while in user permission
+     * restriction.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final String
+            OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION =
+            "android:system_exempt_from_fgs_bg_start_while_in_use_permission_restriction";
+
+    /**
+     * Hide foreground service stop button in quick settings.
+     *
+     * Only to be used by the system.
+     *
+     * @hide
+     */
+    public static final String OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON =
+            "android:system_exempt_from_fgs_stop_button";
 
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
@@ -1910,6 +2105,9 @@
             OP_SCHEDULE_EXACT_ALARM,
             OP_MANAGE_MEDIA,
             OP_TURN_SCREEN_ON,
+            OP_RUN_LONG_JOBS,
+            OP_READ_MEDIA_VISUAL_USER_SELECTED,
+            OP_FOREGROUND_SERVICE_SPECIAL_USE,
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2285,9 +2483,44 @@
             .setDisableReset(true).setRestrictRead(true).build(),
         new AppOpInfo.Builder(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
                 "RECEIVE_SOUNDTRIGGER_AUDIO").setDefaultMode(AppOpsManager.MODE_ALLOWED)
-                .setForceCollectNotes(true).build()
+                .setForceCollectNotes(true).build(),
+        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(),
+        new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS")
+                .setPermission(Manifest.permission.RUN_LONG_JOBS).build(),
+            new AppOpInfo.Builder(OP_READ_MEDIA_VISUAL_USER_SELECTED,
+                    OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, "READ_MEDIA_VISUAL_USER_SELECTED")
+                    .setPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
+                    .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_APP_STANDBY,
+                OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY,
+                "SYSTEM_EXEMPT_FROM_APP_STANDBY").build(),
+        new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY,
+                OPSTR_SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY,
+                "SYSTEM_EXEMPT_FROM_FORCED_APP_STANDBY").build(),
+        new AppOpInfo.Builder(OP_READ_WRITE_HEALTH_DATA, OPSTR_READ_WRITE_HEALTH_DATA,
+                "READ_WRITE_HEALTH_DATA").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_FOREGROUND_SERVICE_SPECIAL_USE,
+                OPSTR_FOREGROUND_SERVICE_SPECIAL_USE, "FOREGROUND_SERVICE_SPECIAL_USE")
+                .setPermission(Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE).build(),
+        new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION,
+                OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION,
+                "SYSTEM_EXEMPT_FROM_FGS_BG_START_RESTRICTION").build(),
+        new AppOpInfo.Builder(
+                OP_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION,
+                OPSTR_SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION,
+                "SYSTEM_EXEMPT_FROM_FGS_BG_START_WHILE_IN_USE_PERMISSION_RESTRICTION")
+                .build(),
+        new AppOpInfo.Builder(OP_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON,
+                OPSTR_SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON,
+                "SYSTEM_EXEMPT_FROM_FGS_STOP_BUTTON").build()
     };
 
+    // The number of longs needed to form a full bitmask of app ops
+    private static final int BITMASK_LEN = ((_NUM_OP - 1) / Long.SIZE) + 1;
+
     /**
      * @hide
      */
@@ -2322,8 +2555,8 @@
      * @see #getNotedOpCollectionMode
      * @see #collectNotedOpSync
      */
-    private static final ThreadLocal<ArrayMap<String, long[]>> sAppOpsNotedInThisBinderTransaction =
-            new ThreadLocal<>();
+    private static final ThreadLocal<ArrayMap<String, BitSet>>
+            sAppOpsNotedInThisBinderTransaction = new ThreadLocal<>();
 
     static {
         if (sAppOpInfos.length != _NUM_OP) {
@@ -2340,12 +2573,6 @@
                 sPermToOp.put(sAppOpInfos[op].permission, op);
             }
         }
-
-        if ((_NUM_OP + Long.SIZE - 1) / Long.SIZE != 2) {
-            // The code currently assumes that the length of sAppOpsNotedInThisBinderTransaction is
-            // two longs
-            throw new IllegalStateException("notedAppOps collection code assumes < 128 appops");
-        }
     }
 
     /** Config used to control app ops access messages sampling */
@@ -2442,7 +2669,14 @@
     @TestApi
     public static int permissionToOpCode(String permission) {
         Integer boxedOpCode = sPermToOp.get(permission);
-        return boxedOpCode != null ? boxedOpCode : OP_NONE;
+        if (boxedOpCode != null) {
+            return boxedOpCode;
+        }
+        if (permission != null && HealthConnectManager.isHealthPermission(
+                ActivityThread.currentApplication(), permission)) {
+            return OP_READ_WRITE_HEALTH_DATA;
+        }
+        return OP_NONE;
     }
 
     /**
@@ -7106,10 +7340,14 @@
      */
     public static @Nullable String permissionToOp(@NonNull String permission) {
         final Integer opCode = sPermToOp.get(permission);
-        if (opCode == null) {
-            return null;
+        if (opCode != null) {
+            return sAppOpInfos[opCode].name;
         }
-        return sAppOpInfos[opCode].name;
+        if (HealthConnectManager.isHealthPermission(ActivityThread.currentApplication(),
+                permission)) {
+            return sAppOpInfos[OP_READ_WRITE_HEALTH_DATA].name;
+        }
+        return null;
     }
 
     /**
@@ -8338,8 +8576,9 @@
      */
     public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource,
             @Nullable String message, boolean skipProxyOperation) {
-        return startProxyOpNoThrow(op, attributionSource, message, skipProxyOperation,
-                ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_CHAIN_ID_NONE);
+        return startProxyOpNoThrow(attributionSource.getToken(), op, attributionSource, message,
+                skipProxyOperation, ATTRIBUTION_FLAGS_NONE, ATTRIBUTION_FLAGS_NONE,
+                ATTRIBUTION_CHAIN_ID_NONE);
     }
 
     /**
@@ -8351,7 +8590,8 @@
      *
      * @hide
      */
-    public int startProxyOpNoThrow(int op, @NonNull AttributionSource attributionSource,
+    public int startProxyOpNoThrow(@NonNull IBinder clientId, int op,
+            @NonNull AttributionSource attributionSource,
             @Nullable String message, boolean skipProxyOperation, @AttributionFlags
             int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags,
             int attributionChainId) {
@@ -8369,7 +8609,7 @@
                 }
             }
 
-            SyncNotedAppOp syncOp = mService.startProxyOperation(op,
+            SyncNotedAppOp syncOp = mService.startProxyOperation(clientId, op,
                     attributionSource, false, collectionMode == COLLECT_ASYNC, message,
                     shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
                     proxiedAttributionFlags, attributionChainId);
@@ -8467,9 +8707,10 @@
      */
     public void finishProxyOp(@NonNull String op, int proxiedUid,
             @NonNull String proxiedPackageName, @Nullable String proxiedAttributionTag) {
-        finishProxyOp(op, new AttributionSource(mContext.getAttributionSource(),
+        IBinder token = mContext.getAttributionSource().getToken();
+        finishProxyOp(token, op, new AttributionSource(mContext.getAttributionSource(),
                 new AttributionSource(proxiedUid, proxiedPackageName,  proxiedAttributionTag,
-                        mContext.getAttributionSource().getToken())), /*skipProxyOperation*/ false);
+                        token)), /*skipProxyOperation*/ false);
     }
 
     /**
@@ -8484,10 +8725,11 @@
      *
      * @hide
      */
-    public void finishProxyOp(@NonNull String op, @NonNull AttributionSource attributionSource,
-            boolean skipProxyOperation) {
+    public void finishProxyOp(@NonNull IBinder clientId, @NonNull String op,
+            @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
         try {
-            mService.finishProxyOperation(strOpToOp(op), attributionSource, skipProxyOperation);
+            mService.finishProxyOperation(clientId, strOpToOp(op), attributionSource,
+                    skipProxyOperation);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -8569,10 +8811,10 @@
      */
     public static class PausedNotedAppOpsCollection {
         final int mUid;
-        final @Nullable ArrayMap<String, long[]> mCollectedNotedAppOps;
+        final @Nullable ArrayMap<String, BitSet> mCollectedNotedAppOps;
 
         PausedNotedAppOpsCollection(int uid, @Nullable ArrayMap<String,
-                long[]> collectedNotedAppOps) {
+                BitSet> collectedNotedAppOps) {
             mUid = uid;
             mCollectedNotedAppOps = collectedNotedAppOps;
         }
@@ -8590,7 +8832,7 @@
     public static @Nullable PausedNotedAppOpsCollection pauseNotedAppOpsCollection() {
         Integer previousUid = sBinderThreadCallingUid.get();
         if (previousUid != null) {
-            ArrayMap<String, long[]> previousCollectedNotedAppOps =
+            ArrayMap<String, BitSet> previousCollectedNotedAppOps =
                     sAppOpsNotedInThisBinderTransaction.get();
 
             sBinderThreadCallingUid.remove();
@@ -8664,23 +8906,19 @@
         // We are inside of a two-way binder call. Delivered to caller via
         // {@link #prefixParcelWithAppOpsIfNeeded}
         int op = sOpStrToOp.get(syncOp.getOp());
-        ArrayMap<String, long[]> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get();
+        ArrayMap<String, BitSet> appOpsNoted = sAppOpsNotedInThisBinderTransaction.get();
         if (appOpsNoted == null) {
             appOpsNoted = new ArrayMap<>(1);
             sAppOpsNotedInThisBinderTransaction.set(appOpsNoted);
         }
 
-        long[] appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag());
+        BitSet appOpsNotedForAttribution = appOpsNoted.get(syncOp.getAttributionTag());
         if (appOpsNotedForAttribution == null) {
-            appOpsNotedForAttribution = new long[2];
+            appOpsNotedForAttribution = new BitSet(_NUM_OP);
             appOpsNoted.put(syncOp.getAttributionTag(), appOpsNotedForAttribution);
         }
 
-        if (op < 64) {
-            appOpsNotedForAttribution[0] |= 1L << op;
-        } else {
-            appOpsNotedForAttribution[1] |= 1L << (op - 64);
-        }
+        appOpsNotedForAttribution.set(op);
     }
 
     /** @hide */
@@ -8754,7 +8992,7 @@
      */
     // TODO (b/186872903) Refactor how sync noted ops are propagated.
     public static void prefixParcelWithAppOpsIfNeeded(@NonNull Parcel p) {
-        ArrayMap<String, long[]> notedAppOps = sAppOpsNotedInThisBinderTransaction.get();
+        ArrayMap<String, BitSet> notedAppOps = sAppOpsNotedInThisBinderTransaction.get();
         if (notedAppOps == null) {
             return;
         }
@@ -8766,8 +9004,15 @@
 
         for (int i = 0; i < numAttributionWithNotesAppOps; i++) {
             p.writeString(notedAppOps.keyAt(i));
-            p.writeLong(notedAppOps.valueAt(i)[0]);
-            p.writeLong(notedAppOps.valueAt(i)[1]);
+            // Bitmask's toLongArray will truncate the array, if upper bits arent used
+            long[] notedOpsMask = notedAppOps.valueAt(i).toLongArray();
+            for (int j = 0; j < BITMASK_LEN; j++) {
+                if (j < notedOpsMask.length) {
+                    p.writeLong(notedOpsMask[j]);
+                } else {
+                    p.writeLong(0);
+                }
+            }
         }
     }
 
@@ -8786,12 +9031,13 @@
 
         for (int i = 0; i < numAttributionsWithNotedAppOps; i++) {
             String attributionTag = p.readString();
-            long[] rawNotedAppOps = new long[2];
-            rawNotedAppOps[0] = p.readLong();
-            rawNotedAppOps[1] = p.readLong();
+            long[] rawNotedAppOps = new long[BITMASK_LEN];
+            for (int j = 0; j < rawNotedAppOps.length; j++) {
+                rawNotedAppOps[j] = p.readLong();
+            }
+            BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps);
 
-            if (rawNotedAppOps[0] != 0 || rawNotedAppOps[1] != 0) {
-                BitSet notedAppOps = BitSet.valueOf(rawNotedAppOps);
+            if (!notedAppOps.isEmpty()) {
 
                 synchronized (sLock) {
                     for (int code = notedAppOps.nextSetBit(0); code != -1;
diff --git a/core/java/android/app/AppOpsManagerInternal.java b/core/java/android/app/AppOpsManagerInternal.java
index 4d6e4ae..43023fe 100644
--- a/core/java/android/app/AppOpsManagerInternal.java
+++ b/core/java/android/app/AppOpsManagerInternal.java
@@ -26,13 +26,11 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.app.IAppOpsCallback;
-import com.android.internal.util.function.DecFunction;
 import com.android.internal.util.function.HeptFunction;
 import com.android.internal.util.function.HexFunction;
 import com.android.internal.util.function.QuadFunction;
 import com.android.internal.util.function.QuintConsumer;
 import com.android.internal.util.function.QuintFunction;
-import com.android.internal.util.function.TriFunction;
 import com.android.internal.util.function.UndecFunction;
 
 /**
@@ -135,6 +133,7 @@
         /**
          * Allows overriding start proxy operation behavior.
          *
+         * @param clientId The client calling start, represented by an IBinder
          * @param code The op code to start.
          * @param attributionSource The permission identity of the caller.
          * @param startIfModeDefault Whether to start the op of the mode is default.
@@ -148,11 +147,12 @@
          * @param superImpl The super implementation.
          * @return The app op note result.
          */
-        SyncNotedAppOp startProxyOperation(int code, @NonNull AttributionSource attributionSource,
-                boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message,
-                boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags
-                int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags,
-                int attributionChainId, @NonNull DecFunction<Integer, AttributionSource, Boolean,
+        SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
+                @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
+                boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
+                boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
+                @AttributionFlags int proxiedAttributionFlags, int attributionChainId,
+                @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean,
                         Boolean, String, Boolean, Boolean, Integer, Integer, Integer,
                         SyncNotedAppOp> superImpl);
 
@@ -176,10 +176,15 @@
          *
          * @param code The op code to finish.
          * @param attributionSource The permission identity of the caller.
+         * @param skipProxyOperation Whether to skip the proxy in the proxy/proxied operation
+         * @param clientId The client calling finishProxyOperation
+         * @param superImpl The "standard" implementation to potentially call
          */
-        void finishProxyOperation(int code, @NonNull AttributionSource attributionSource,
+        void finishProxyOperation(@NonNull IBinder clientId, int code,
+                @NonNull AttributionSource attributionSource,
                 boolean skipProxyOperation,
-                @NonNull TriFunction<Integer, AttributionSource, Boolean, Void> superImpl);
+                @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean,
+                        Void> superImpl);
     }
 
     /**
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index a8d8c75..5517c57 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -1196,7 +1196,8 @@
         return sb.toString();
     }
 
-    private static String reasonCodeToString(@Reason int reason) {
+    /** @hide */
+    public static String reasonCodeToString(@Reason int reason) {
         switch (reason) {
             case REASON_EXIT_SELF:
                 return "EXIT_SELF";
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index c2df802..1777f37 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -30,10 +31,15 @@
 import android.content.IntentFilter;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.BundleMerger;
 import android.os.PowerExemptionManager;
 import android.os.PowerExemptionManager.ReasonCode;
 import android.os.PowerExemptionManager.TempAllowListType;
 
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -59,6 +65,9 @@
     private boolean mIsAlarmBroadcast = false;
     private long mIdForResponseEvent;
     private @Nullable IntentFilter mRemoveMatchingFilter;
+    private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
+    private @Nullable String mDeliveryGroupKey;
+    private @Nullable BundleMerger mDeliveryGroupExtrasMerger;
 
     /**
      * Change ID which is invalid.
@@ -190,6 +199,63 @@
     private static final String KEY_REMOVE_MATCHING_FILTER =
             "android:broadcast.removeMatchingFilter";
 
+    /**
+     * Corresponds to {@link #setDeliveryGroupPolicy(int)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_POLICY =
+            "android:broadcast.deliveryGroupPolicy";
+
+    /**
+     * Corresponds to {@link #setDeliveryGroupKey(String, String)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_KEY =
+            "android:broadcast.deliveryGroupKey";
+
+    /**
+     * Corresponds to {@link #setDeliveryGroupExtrasMerger(BundleMerger)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_EXTRAS_MERGER =
+            "android:broadcast.deliveryGroupExtrasMerger";
+
+    /**
+     * The list of delivery group policies which specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = {
+            DELIVERY_GROUP_POLICY_ALL,
+            DELIVERY_GROUP_POLICY_MOST_RECENT,
+            DELIVERY_GROUP_POLICY_MERGED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeliveryGroupPolicy {}
+
+    /**
+     * Delivery group policy that indicates that all the broadcasts in the delivery group
+     * need to be delivered as is.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DELIVERY_GROUP_POLICY_ALL = 0;
+
+    /**
+     * Delivery group policy that indicates that only the most recent broadcast in the delivery
+     * group need to be delivered and the rest can be dropped.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1;
+
+    /**
+     * Delivery group policy that indicates that the extras data from the broadcasts in the
+     * delivery group need to be merged into a single broadcast and the rest can be dropped.
+     *
+     * @hide
+     */
+    public static final int DELIVERY_GROUP_POLICY_MERGED = 2;
+
     public static BroadcastOptions makeBasic() {
         BroadcastOptions opts = new BroadcastOptions();
         return opts;
@@ -236,6 +302,11 @@
         mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
         mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
                 IntentFilter.class);
+        mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
+                DELIVERY_GROUP_POLICY_ALL);
+        mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY);
+        mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER,
+                BundleMerger.class);
     }
 
     /**
@@ -639,12 +710,89 @@
     }
 
     /**
+     * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) {
+        mDeliveryGroupPolicy = policy;
+    }
+
+    /**
+     * Get the delivery group policy for this broadcast that specifies how multiple broadcasts
+     * belonging to the same delivery group has to be handled.
+     *
+     * @hide
+     */
+    @SystemApi
+    public @DeliveryGroupPolicy int getDeliveryGroupPolicy() {
+        return mDeliveryGroupPolicy;
+    }
+
+    /**
+     * Clears any previously set delivery group policies using
+     * {@link #setDeliveryGroupKey(String, String)} and resets the delivery group policy to
+     * the default value ({@link #DELIVERY_GROUP_POLICY_ALL}).
+     *
+     * @hide
+     */
+    @SystemApi
+    public void clearDeliveryGroupPolicy() {
+        mDeliveryGroupPolicy = DELIVERY_GROUP_POLICY_ALL;
+    }
+
+    /**
+     * Set namespace and key to identify the delivery group that this broadcast belongs to.
+     * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be
+     * used to identify the delivery group.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) {
+        Preconditions.checkArgument(!namespace.contains("/"),
+                "namespace should not contain '/'");
+        Preconditions.checkArgument(!key.contains("/"),
+                "key should not contain '/'");
+        mDeliveryGroupKey = namespace + "/" + key;
+    }
+
+    /** @hide */
+    public String getDeliveryGroupKey() {
+        return mDeliveryGroupKey;
+    }
+
+    /**
+     * Set the {@link BundleMerger} that specifies how to merge the extras data from
+     * broadcasts in a delivery group.
+     *
+     * <p>Note that this value will be ignored if the delivery group policy is not set as
+     * {@link #DELIVERY_GROUP_POLICY_MERGED}.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupExtrasMerger(@NonNull BundleMerger extrasMerger) {
+        Preconditions.checkNotNull(extrasMerger);
+        mDeliveryGroupExtrasMerger = extrasMerger;
+    }
+
+    /** @hide */
+    public @Nullable BundleMerger getDeliveryGroupExtrasMerger() {
+        return mDeliveryGroupExtrasMerger;
+    }
+
+    /**
      * Returns the created options as a Bundle, which can be passed to
      * {@link android.content.Context#sendBroadcast(android.content.Intent)
      * Context.sendBroadcast(Intent)} and related methods.
      * Note that the returned Bundle is still owned by the BroadcastOptions
      * object; you must not modify it, but can supply it to the sendBroadcast
      * methods that take an options Bundle.
+     *
+     * @throws IllegalStateException if the broadcast option values are inconsistent. For example,
+     *                               if the delivery group policy is specified as "MERGED" but no
+     *                               extras merger is supplied.
      */
     @Override
     public Bundle toBundle() {
@@ -686,6 +834,21 @@
         if (mRemoveMatchingFilter != null) {
             b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter);
         }
+        if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) {
+            b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy);
+        }
+        if (mDeliveryGroupKey != null) {
+            b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey);
+        }
+        if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) {
+            if (mDeliveryGroupExtrasMerger != null) {
+                b.putParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER,
+                        mDeliveryGroupExtrasMerger);
+            } else {
+                throw new IllegalStateException("Extras merger cannot be empty "
+                        + "when delivery group policy is 'MERGED'");
+            }
+        }
         return b.isEmpty() ? null : b;
     }
 }
diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java
index 4e5e384..74db39f 100644
--- a/core/java/android/app/ComponentOptions.java
+++ b/core/java/android/app/ComponentOptions.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.RequiresPermission;
 import android.os.Bundle;
 
 /**
@@ -45,8 +46,15 @@
     public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION =
             "android.pendingIntent.backgroundActivityAllowedByPermission";
 
+    /**
+     * Corresponds to {@link #setInteractive(boolean)}
+     * @hide
+     */
+    public static final String KEY_INTERACTIVE = "android:component.isInteractive";
+
     private boolean mPendingIntentBalAllowed = PENDING_INTENT_BAL_ALLOWED_DEFAULT;
     private boolean mPendingIntentBalAllowedByPermission = false;
+    private boolean mIsInteractive = false;
 
     ComponentOptions() {
     }
@@ -61,6 +69,29 @@
         setPendingIntentBackgroundActivityLaunchAllowedByPermission(
                 opts.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION,
                         false));
+        mIsInteractive = opts.getBoolean(KEY_INTERACTIVE, false);
+    }
+
+    /**
+     * When set, a broadcast will be understood as having originated from
+     * some direct interaction by the user such as a notification tap or button
+     * press.  Only the OS itself may use this option.
+     * @hide
+     * @param interactive
+     * @see #isInteractive()
+     */
+    @RequiresPermission(android.Manifest.permission.COMPONENT_OPTION_INTERACTIVE)
+    public void setInteractive(boolean interactive) {
+        mIsInteractive = interactive;
+    }
+
+    /**
+     * Did this PendingIntent send originate with a direct user interaction?
+     * @return true if this is the result of an interaction, false otherwise
+     * @hide
+     */
+    public boolean isInteractive() {
+        return mIsInteractive;
     }
 
     /**
@@ -103,6 +134,9 @@
             b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION,
                     mPendingIntentBalAllowedByPermission);
         }
+        if (mIsInteractive) {
+            b.putBoolean(KEY_INTERACTIVE, true);
+        }
         return b;
     }
 }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 10cdf53..042bdd7 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1814,12 +1814,6 @@
             }
         }
         try {
-            ActivityThread thread = ActivityThread.currentActivityThread();
-            Instrumentation instrumentation = thread.getInstrumentation();
-            if (instrumentation.isInstrumenting()
-                    && ((flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
-                flags = flags | Context.RECEIVER_EXPORTED;
-            }
             final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
                     mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
                     AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java
index ae3a9e6..0d14c0b 100644
--- a/core/java/android/app/DisabledWallpaperManager.java
+++ b/core/java/android/app/DisabledWallpaperManager.java
@@ -193,6 +193,16 @@
     }
 
     @Override
+    public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which) {
+        return unsupported();
+    }
+
+    @Override
+    public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which, int userId) {
+        return unsupported();
+    }
+
+    @Override
     public int getWallpaperId(int which) {
         return unsupportedInt();
     }
diff --git a/core/java/android/app/ForegroundServiceTypeNotAllowedException.java b/core/java/android/app/ForegroundServiceTypeNotAllowedException.java
new file mode 100644
index 0000000..c258242
--- /dev/null
+++ b/core/java/android/app/ForegroundServiceTypeNotAllowedException.java
@@ -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 android.app;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Exception thrown when an app tries to start a foreground {@link Service} without a valid type.
+ */
+public final class ForegroundServiceTypeNotAllowedException
+        extends ServiceStartNotAllowedException implements Parcelable {
+    /**
+     * Constructor.
+     */
+    public ForegroundServiceTypeNotAllowedException(@NonNull String message) {
+        super(message);
+    }
+
+    ForegroundServiceTypeNotAllowedException(@NonNull Parcel source) {
+        super(source.readString());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(getMessage());
+    }
+
+    public static final @NonNull Creator<android.app.ForegroundServiceTypeNotAllowedException>
+            CREATOR = new Creator<android.app.ForegroundServiceTypeNotAllowedException>() {
+                @NonNull
+                public android.app.ForegroundServiceTypeNotAllowedException createFromParcel(
+                        Parcel source) {
+                    return new android.app.ForegroundServiceTypeNotAllowedException(source);
+                }
+
+                @NonNull
+                public android.app.ForegroundServiceTypeNotAllowedException[] newArray(int size) {
+                    return new android.app.ForegroundServiceTypeNotAllowedException[size];
+                }
+            };
+}
diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java
new file mode 100644
index 0000000..eccc563
--- /dev/null
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -0,0 +1,1033 @@
+/*
+ * 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;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_FOREGROUND;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.compat.CompatChanges;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.Overridable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.content.pm.ServiceInfo.ForegroundServiceType;
+import android.hardware.usb.UsbAccessory;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.compat.CompatibilityChangeConfig;
+import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Optional;
+
+/**
+ * This class enforces the policies around the foreground service types.
+ *
+ * @hide
+ */
+public abstract class ForegroundServiceTypePolicy {
+    static final String TAG = "ForegroundServiceTypePolicy";
+    static final boolean DEBUG_FOREGROUND_SERVICE_TYPE_POLICY = false;
+
+    /**
+     * The FGS type enforcement:
+     * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     *
+     * <p>Starting a FGS with this type (equivalent of no type) from apps with
+     * targetSdkVersion {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will
+     * result in a warning in the log.</p>
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
+    @Overridable
+    public static final long FGS_TYPE_NONE_DEPRECATION_CHANGE_ID = 255042465L;
+
+    /**
+     * The FGS type enforcement:
+     * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     *
+     * <p>Starting a FGS with this type (equivalent of no type) from apps with
+     * targetSdkVersion {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will
+     * result in an exception.</p>
+     *
+     * @hide
+     */
+    // TODO (b/254661666): Change to @EnabledAfter(T)
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long FGS_TYPE_NONE_DISABLED_CHANGE_ID = 255038118L;
+
+    /**
+     * The FGS type enforcement:
+     * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}.
+     *
+     * <p>Starting a FGS with this type from apps with targetSdkVersion
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will
+     * result in a warning in the log.</p>
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
+    @Overridable
+    public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L;
+
+    /**
+     * The FGS type enforcement:
+     * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}.
+     *
+     * <p>Starting a FGS with this type from apps with targetSdkVersion
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will
+     * result in an exception.</p>
+     *
+     * @hide
+     */
+    // TODO (b/254661666): Change to @EnabledSince(U) in next OS release
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long FGS_TYPE_DATA_SYNC_DISABLED_CHANGE_ID = 255659651L;
+
+    /**
+     * The FGS type enforcement: Starting a FGS from apps with targetSdkVersion
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later but without the required
+     * permissions associated with the FGS type will result in a SecurityException.
+     *
+     * @hide
+     */
+    // TODO (b/254661666): Change to @EnabledAfter(T)
+    @ChangeId
+    @Disabled
+    @Overridable
+    public static final long FGS_TYPE_PERMISSION_CHANGE_ID = 254662522L;
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_NONE =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_NONE,
+            FGS_TYPE_NONE_DEPRECATION_CHANGE_ID,
+            FGS_TYPE_NONE_DISABLED_CHANGE_ID,
+            null,
+            null
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_DATA_SYNC =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_DATA_SYNC,
+            FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID,
+            FGS_TYPE_DATA_SYNC_DISABLED_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC)
+            }, true),
+            null
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PLAYBACK =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK)
+            }, true),
+            null
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_PHONE_CALL}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_PHONE_CALL =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_PHONE_CALL,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.MANAGE_OWN_CALLS)
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_LOCATION}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_LOCATION =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_LOCATION,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_LOCATION)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.ACCESS_COARSE_LOCATION),
+                new RegularPermission(Manifest.permission.ACCESS_FINE_LOCATION),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_CONNECTED_DEVICE =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.BLUETOOTH_CONNECT),
+                new RegularPermission(Manifest.permission.CHANGE_NETWORK_STATE),
+                new RegularPermission(Manifest.permission.CHANGE_WIFI_STATE),
+                new RegularPermission(Manifest.permission.CHANGE_WIFI_MULTICAST_STATE),
+                new RegularPermission(Manifest.permission.NFC),
+                new RegularPermission(Manifest.permission.TRANSMIT_IR),
+                new UsbDevicePermission(),
+                new UsbAccessoryPermission(),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MEDIA_PROJECTION =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.CAPTURE_VIDEO_OUTPUT),
+                new AppOpPermission(AppOpsManager.OP_PROJECT_MEDIA)
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_CAMERA}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_CAMERA =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_CAMERA,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.CAMERA),
+                new RegularPermission(Manifest.permission.SYSTEM_CAMERA),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_MICROPHONE}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_MICROPHONE =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_MICROPHONE,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD),
+                new RegularPermission(Manifest.permission.CAPTURE_AUDIO_OUTPUT),
+                new RegularPermission(Manifest.permission.CAPTURE_MEDIA_OUTPUT),
+                new RegularPermission(Manifest.permission.CAPTURE_TUNER_AUDIO_INPUT),
+                new RegularPermission(Manifest.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT),
+                new RegularPermission(Manifest.permission.RECORD_AUDIO),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_HEALTH}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_HEALTH =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_HEALTH,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION),
+                new RegularPermission(Manifest.permission.BODY_SENSORS),
+                new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_REMOTE_MESSAGING =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING)
+            }, true),
+            null
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_SYSTEM_EXEMPTED =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED)
+            }, true),
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.SCHEDULE_EXACT_ALARM),
+                new RegularPermission(Manifest.permission.USE_EXACT_ALARM),
+                new AppOpPermission(AppOpsManager.OP_ACTIVATE_VPN),
+                new AppOpPermission(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
+            }, false)
+    );
+
+    /**
+     * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}.
+     *
+     * @hide
+     */
+    public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_SPECIAL_USE =
+            new ForegroundServiceTypePolicyInfo(
+            FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID,
+            new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] {
+                new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE)
+            }, true),
+            null
+    );
+
+    /**
+     * Foreground service policy check result code: this one is not actually being used.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_UNKNOWN =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_UNKNOWN;
+
+    /**
+     * Foreground service policy check result code: okay to go.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_OK =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_OK;
+
+    /**
+     * Foreground service policy check result code: this foreground service type is deprecated.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_DEPRECATED =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_DEPRECATED;
+
+    /**
+     * Foreground service policy check result code: this foreground service type is disabled.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_DISABLED =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_DISABLED;
+
+    /**
+     * Foreground service policy check result code: the caller doesn't have permission to start
+     * foreground service with this type, but the policy is permissive.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE;
+
+    /**
+     * Foreground service policy check result code: the caller doesn't have permission to start
+     * foreground service with this type, and the policy is enforced.
+     *
+     * @hide
+     */
+    public static final int FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED =
+            AppProtoEnums.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "FGS_TYPE_POLICY_CHECK_" }, value = {
+         FGS_TYPE_POLICY_CHECK_UNKNOWN,
+         FGS_TYPE_POLICY_CHECK_OK,
+         FGS_TYPE_POLICY_CHECK_DEPRECATED,
+         FGS_TYPE_POLICY_CHECK_DISABLED,
+         FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE,
+         FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ForegroundServicePolicyCheckCode{}
+
+    /**
+     * @return The policy info for the given type.
+     */
+    @NonNull
+    public abstract ForegroundServiceTypePolicyInfo getForegroundServiceTypePolicyInfo(
+            @ForegroundServiceType int type, @ForegroundServiceType int defaultToType);
+
+    /**
+     * Run check on the foreground service type policy for the given uid/pid
+     *
+     * @hide
+     */
+    @ForegroundServicePolicyCheckCode
+    public abstract int checkForegroundServiceTypePolicy(@NonNull Context context,
+            @NonNull String packageName, int callerUid, int callerPid, boolean allowWhileInUse,
+            @NonNull ForegroundServiceTypePolicyInfo policy);
+
+    @GuardedBy("sLock")
+    private static ForegroundServiceTypePolicy sDefaultForegroundServiceTypePolicy = null;
+
+    private static final Object sLock = new Object();
+
+    /**
+     * Return the default policy for FGS type.
+     */
+    public static @NonNull ForegroundServiceTypePolicy getDefaultPolicy() {
+        synchronized (sLock) {
+            if (sDefaultForegroundServiceTypePolicy == null) {
+                sDefaultForegroundServiceTypePolicy = new DefaultForegroundServiceTypePolicy();
+            }
+            return sDefaultForegroundServiceTypePolicy;
+        }
+    }
+
+    /**
+     * Constructor.
+     *
+     * @hide
+     */
+    public ForegroundServiceTypePolicy() {
+    }
+
+    /**
+     * This class represents the policy for a specific FGS service type.
+     *
+     * @hide
+     */
+    public static final class ForegroundServiceTypePolicyInfo {
+        /**
+         * The foreground service type.
+         */
+        final @ForegroundServiceType int mType;
+
+        /**
+         * The change id to tell if this FGS type is deprecated.
+         *
+         * <p>A 0 indicates it's not deprecated.</p>
+         */
+        final long mDeprecationChangeId;
+
+        /**
+         * The change id to tell if this FGS type is disabled.
+         *
+         * <p>A 0 indicates it's not disabled.</p>
+         */
+        final long mDisabledChangeId;
+
+        /**
+         * The required permissions to start a foreground with this type, all of them
+         * MUST have been granted.
+         */
+        final @Nullable ForegroundServiceTypePermissions mAllOfPermissions;
+
+        /**
+         * The required permissions to start a foreground with this type, any one of them
+         * being granted is sufficient.
+         */
+        final @Nullable ForegroundServiceTypePermissions mAnyOfPermissions;
+
+        /**
+         * A customized check for the permissions.
+         */
+        @Nullable ForegroundServiceTypePermission mCustomPermission;
+
+        /**
+         * Not a real change id, but a place holder.
+         */
+        private static final long INVALID_CHANGE_ID = 0L;
+
+        /**
+         * @return {@code true} if the given change id is valid.
+         */
+        private static boolean isValidChangeId(long changeId) {
+            return changeId != INVALID_CHANGE_ID;
+        }
+
+        /**
+         * Construct a new instance.
+         *
+         * @hide
+         */
+        public ForegroundServiceTypePolicyInfo(@ForegroundServiceType int type,
+                long deprecationChangeId, long disabledChangeId,
+                @Nullable ForegroundServiceTypePermissions allOfPermissions,
+                @Nullable ForegroundServiceTypePermissions anyOfPermissions) {
+            mType = type;
+            mDeprecationChangeId = deprecationChangeId;
+            mDisabledChangeId = disabledChangeId;
+            mAllOfPermissions = allOfPermissions;
+            mAnyOfPermissions = anyOfPermissions;
+        }
+
+        /**
+         * @return The foreground service type.
+         */
+        @ForegroundServiceType
+        public int getForegroundServiceType() {
+            return mType;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = toPermissionString(new StringBuilder());
+            sb.append("type=0x");
+            sb.append(Integer.toHexString(mType));
+            sb.append(" deprecationChangeId=");
+            sb.append(mDeprecationChangeId);
+            sb.append(" disabledChangeId=");
+            sb.append(mDisabledChangeId);
+            sb.append(" customPermission=");
+            sb.append(mCustomPermission);
+            return sb.toString();
+        }
+
+        /**
+         * @return The required permissions.
+         */
+        public String toPermissionString() {
+            return toPermissionString(new StringBuilder()).toString();
+        }
+
+        private StringBuilder toPermissionString(StringBuilder sb) {
+            if (mAllOfPermissions != null) {
+                sb.append("all of the permissions ");
+                sb.append(mAllOfPermissions.toString());
+                sb.append(' ');
+            }
+            if (mAnyOfPermissions != null) {
+                sb.append("any of the permissions ");
+                sb.append(mAnyOfPermissions.toString());
+                sb.append(' ');
+            }
+            return sb;
+        }
+
+        /**
+         * @hide
+         */
+        public void setCustomPermission(
+                @Nullable ForegroundServiceTypePermission customPermission) {
+            mCustomPermission = customPermission;
+        }
+
+        /**
+         * @return The name of the permissions which are all required.
+         *         It may contain app op names.
+         *
+         * For test only.
+         */
+        public @NonNull Optional<String[]> getRequiredAllOfPermissionsForTest() {
+            if (mAllOfPermissions == null) {
+                return Optional.empty();
+            }
+            return Optional.of(mAllOfPermissions.toStringArray());
+        }
+
+        /**
+         * @return The name of the permissions where any of the is granted is sufficient.
+         *         It may contain app op names.
+         *
+         * For test only.
+         */
+        public @NonNull Optional<String[]> getRequiredAnyOfPermissionsForTest() {
+            if (mAnyOfPermissions == null) {
+                return Optional.empty();
+            }
+            return Optional.of(mAnyOfPermissions.toStringArray());
+        }
+
+        /**
+         * Whether or not this type is disabled.
+         */
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        public boolean isTypeDisabled(int callerUid) {
+            return isValidChangeId(mDisabledChangeId)
+                    && CompatChanges.isChangeEnabled(mDisabledChangeId, callerUid);
+        }
+
+        /**
+         * Override the type disabling change Id.
+         *
+         * For test only.
+         */
+        public void setTypeDisabledForTest(boolean disabled, @NonNull String packageName)
+                throws RemoteException {
+            overrideChangeIdForTest(mDisabledChangeId, disabled, packageName);
+        }
+
+        /**
+         * clear the type disabling change Id.
+         *
+         * For test only.
+         */
+        public void clearTypeDisabledForTest(@NonNull String packageName) throws RemoteException {
+            clearOverrideForTest(mDisabledChangeId, packageName);
+        }
+
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        boolean isTypeDeprecated(int callerUid) {
+            return isValidChangeId(mDeprecationChangeId)
+                    && CompatChanges.isChangeEnabled(mDeprecationChangeId, callerUid);
+        }
+
+        private void overrideChangeIdForTest(long changeId, boolean enable, String packageName)
+                throws RemoteException {
+            if (!isValidChangeId(changeId)) {
+                return;
+            }
+            final ArraySet<Long> enabled = new ArraySet<>();
+            final ArraySet<Long> disabled = new ArraySet<>();
+            if (enable) {
+                enabled.add(changeId);
+            } else {
+                disabled.add(changeId);
+            }
+            final CompatibilityChangeConfig overrides = new CompatibilityChangeConfig(
+                    new Compatibility.ChangeConfig(enabled, disabled));
+            IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
+                        ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+            platformCompat.setOverridesForTest(overrides, packageName);
+        }
+
+        private void clearOverrideForTest(long changeId, @NonNull String packageName)
+                throws RemoteException {
+            IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface(
+                        ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+            platformCompat.clearOverrideForTest(changeId, packageName);
+        }
+    }
+
+    /**
+     * This represents the set of permissions that's going to be required
+     * for a specific service type.
+     *
+     * @hide
+     */
+    public static class ForegroundServiceTypePermissions {
+        /**
+         * The set of the permissions to be required.
+         */
+        final @NonNull ForegroundServiceTypePermission[] mPermissions;
+
+        /**
+         * Are we requiring all of the permissions to be granted or any of them.
+         */
+        final boolean mAllOf;
+
+        /**
+         * Constructor.
+         */
+        public ForegroundServiceTypePermissions(
+                @NonNull ForegroundServiceTypePermission[] permissions, boolean allOf) {
+            mPermissions = permissions;
+            mAllOf = allOf;
+        }
+
+        /**
+         * Check the permissions.
+         */
+        @PackageManager.PermissionResult
+        public int checkPermissions(@NonNull Context context, int callerUid, int callerPid,
+                @NonNull String packageName, boolean allowWhileInUse) {
+            if (mAllOf) {
+                for (ForegroundServiceTypePermission perm : mPermissions) {
+                    final int result = perm.checkPermission(context, callerUid, callerPid,
+                            packageName, allowWhileInUse);
+                    if (result != PERMISSION_GRANTED) {
+                        return PERMISSION_DENIED;
+                    }
+                }
+                return PERMISSION_GRANTED;
+            } else {
+                boolean anyOfGranted = false;
+                for (ForegroundServiceTypePermission perm : mPermissions) {
+                    final int result = perm.checkPermission(context, callerUid, callerPid,
+                            packageName, allowWhileInUse);
+                    if (result == PERMISSION_GRANTED) {
+                        anyOfGranted = true;
+                        break;
+                    }
+                }
+                return anyOfGranted ? PERMISSION_GRANTED : PERMISSION_DENIED;
+            }
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("allOf=");
+            sb.append(mAllOf);
+            sb.append(' ');
+            sb.append('[');
+            for (int i = 0; i < mPermissions.length; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                sb.append(mPermissions[i].toString());
+            }
+            sb.append(']');
+            return sb.toString();
+        }
+
+        @NonNull String[] toStringArray() {
+            final String[] names = new String[mPermissions.length];
+            for (int i = 0; i < mPermissions.length; i++) {
+                names[i] = mPermissions[i].mName;
+            }
+            return names;
+        }
+    }
+
+    /**
+     * This represents a permission that's going to be required for a specific service type.
+     *
+     * @hide
+     */
+    public abstract static class ForegroundServiceTypePermission {
+        /**
+         * The name of this permission.
+         */
+        final @NonNull String mName;
+
+        /**
+         * Constructor.
+         */
+        public ForegroundServiceTypePermission(@NonNull String name) {
+            mName = name;
+        }
+
+        /**
+         * Check if the given uid/pid/package has the access to the permission.
+         */
+        @PackageManager.PermissionResult
+        public abstract int checkPermission(@NonNull Context context, int callerUid, int callerPid,
+                @NonNull String packageName, boolean allowWhileInUse);
+
+        @Override
+        public String toString() {
+            return mName;
+        }
+    }
+
+    /**
+     * This represents a regular Android permission to be required for a specific service type.
+     */
+    static class RegularPermission extends ForegroundServiceTypePermission {
+        RegularPermission(@NonNull String name) {
+            super(name);
+        }
+
+        @Override
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        @PackageManager.PermissionResult
+        public int checkPermission(Context context, int callerUid, int callerPid,
+                String packageName, boolean allowWhileInUse) {
+            // Simple case, check if it's already granted.
+            if (context.checkPermission(mName, callerPid, callerUid) == PERMISSION_GRANTED) {
+                return PERMISSION_GRANTED;
+            }
+            if (allowWhileInUse) {
+                // Check its appops
+                final int opCode = AppOpsManager.permissionToOpCode(mName);
+                final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+                if (opCode != AppOpsManager.OP_NONE) {
+                    final int currentMode = appOpsManager.unsafeCheckOpRawNoThrow(opCode, callerUid,
+                            packageName);
+                    if (currentMode == MODE_FOREGROUND) {
+                        // It's in foreground only mode and we're allowing while-in-use.
+                        return PERMISSION_GRANTED;
+                    }
+                }
+            }
+            return PERMISSION_DENIED;
+        }
+    }
+
+    /**
+     * This represents an app op permission to be required for a specific service type.
+     */
+    static class AppOpPermission extends ForegroundServiceTypePermission {
+        final int mOpCode;
+
+        AppOpPermission(int opCode) {
+            super(AppOpsManager.opToPublicName(opCode));
+            mOpCode = opCode;
+        }
+
+        @Override
+        @PackageManager.PermissionResult
+        public int checkPermission(Context context, int callerUid, int callerPid,
+                String packageName, boolean allowWhileInUse) {
+            final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+            final int mode = appOpsManager.unsafeCheckOpRawNoThrow(mOpCode, callerUid, packageName);
+            return (mode == MODE_ALLOWED || (allowWhileInUse && mode == MODE_FOREGROUND))
+                    ? PERMISSION_GRANTED : PERMISSION_DENIED;
+        }
+    }
+
+    /**
+     * This represents a special Android permission to be required for accessing usb devices.
+     */
+    static class UsbDevicePermission extends ForegroundServiceTypePermission {
+        UsbDevicePermission() {
+            super("USB Device");
+        }
+
+        @Override
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        @PackageManager.PermissionResult
+        public int checkPermission(Context context, int callerUid, int callerPid,
+                String packageName, boolean allowWhileInUse) {
+            final UsbManager usbManager = context.getSystemService(UsbManager.class);
+            final HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
+            if (!ArrayUtils.isEmpty(devices)) {
+                for (UsbDevice device : devices.values()) {
+                    if (usbManager.hasPermission(device, packageName, callerPid, callerUid)) {
+                        return PERMISSION_GRANTED;
+                    }
+                }
+            }
+            return PERMISSION_DENIED;
+        }
+    }
+
+    /**
+     * This represents a special Android permission to be required for accessing usb accessories.
+     */
+    static class UsbAccessoryPermission extends ForegroundServiceTypePermission {
+        UsbAccessoryPermission() {
+            super("USB Accessory");
+        }
+
+        @Override
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        @PackageManager.PermissionResult
+        public int checkPermission(Context context, int callerUid, int callerPid,
+                String packageName, boolean allowWhileInUse) {
+            final UsbManager usbManager = context.getSystemService(UsbManager.class);
+            final UsbAccessory[] accessories = usbManager.getAccessoryList();
+            if (!ArrayUtils.isEmpty(accessories)) {
+                for (UsbAccessory accessory: accessories) {
+                    if (usbManager.hasPermission(accessory, callerPid, callerUid)) {
+                        return PERMISSION_GRANTED;
+                    }
+                }
+            }
+            return PERMISSION_DENIED;
+        }
+    }
+
+    /**
+     * The default policy for the foreground service types.
+     *
+     * @hide
+     */
+    public static class DefaultForegroundServiceTypePolicy extends ForegroundServiceTypePolicy {
+        private final SparseArray<ForegroundServiceTypePolicyInfo> mForegroundServiceTypePolicies =
+                new SparseArray<>();
+
+        /**
+         * Constructor
+         */
+        public DefaultForegroundServiceTypePolicy() {
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_NONE,
+                    FGS_TYPE_POLICY_NONE);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_DATA_SYNC,
+                    FGS_TYPE_POLICY_DATA_SYNC);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
+                    FGS_TYPE_POLICY_MEDIA_PLAYBACK);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_PHONE_CALL,
+                    FGS_TYPE_POLICY_PHONE_CALL);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_LOCATION,
+                    FGS_TYPE_POLICY_LOCATION);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+                    FGS_TYPE_POLICY_CONNECTED_DEVICE);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION,
+                    FGS_TYPE_POLICY_MEDIA_PROJECTION);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_CAMERA,
+                    FGS_TYPE_POLICY_CAMERA);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_MICROPHONE,
+                    FGS_TYPE_POLICY_MICROPHONE);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_HEALTH,
+                    FGS_TYPE_POLICY_HEALTH);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING,
+                    FGS_TYPE_POLICY_REMOTE_MESSAGING);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
+                    FGS_TYPE_POLICY_SYSTEM_EXEMPTED);
+            mForegroundServiceTypePolicies.put(FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+                    FGS_TYPE_POLICY_SPECIAL_USE);
+        }
+
+        @Override
+        public ForegroundServiceTypePolicyInfo getForegroundServiceTypePolicyInfo(
+                @ForegroundServiceType int type, @ForegroundServiceType int defaultToType) {
+            ForegroundServiceTypePolicyInfo info = mForegroundServiceTypePolicies.get(type);
+            if (info == null) {
+                // Unknown type, fallback to the defaultToType
+                info = mForegroundServiceTypePolicies.get(defaultToType);
+                if (info == null) {
+                    // It shouldn't happen.
+                    throw new IllegalArgumentException("Invalid default fgs type " + defaultToType);
+                }
+            }
+            return info;
+        }
+
+        @Override
+        @SuppressLint("AndroidFrameworkRequiresPermission")
+        @ForegroundServicePolicyCheckCode
+        public int checkForegroundServiceTypePolicy(Context context, String packageName,
+                int callerUid, int callerPid, boolean allowWhileInUse,
+                @NonNull ForegroundServiceTypePolicyInfo policy) {
+            // Has this FGS type been disabled and not allowed to use anymore?
+            if (policy.isTypeDisabled(callerUid)) {
+                return FGS_TYPE_POLICY_CHECK_DISABLED;
+            }
+            int permissionResult = PERMISSION_DENIED;
+            // Do we have the permission to start FGS with this type.
+            if (policy.mAllOfPermissions != null) {
+                permissionResult = policy.mAllOfPermissions.checkPermissions(context,
+                        callerUid, callerPid, packageName, allowWhileInUse);
+            }
+            // If it has the "all of" permissions granted, check the "any of" ones.
+            if (permissionResult == PERMISSION_GRANTED) {
+                boolean checkCustomPermission = true;
+                // Check the "any of" permissions.
+                if (policy.mAnyOfPermissions != null) {
+                    permissionResult = policy.mAnyOfPermissions.checkPermissions(context,
+                            callerUid, callerPid, packageName, allowWhileInUse);
+                    if (permissionResult == PERMISSION_GRANTED) {
+                        // We have one of them granted, no need to check custom permissions.
+                        checkCustomPermission = false;
+                    }
+                }
+                // If we have a customized permission checker, also call it now.
+                if (checkCustomPermission && policy.mCustomPermission != null) {
+                    permissionResult = policy.mCustomPermission.checkPermission(context,
+                            callerUid, callerPid, packageName, allowWhileInUse);
+                }
+            }
+            if (permissionResult != PERMISSION_GRANTED) {
+                return (CompatChanges.isChangeEnabled(
+                        FGS_TYPE_PERMISSION_CHANGE_ID, callerUid))
+                        ? FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED
+                        : FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE;
+            }
+            // Has this FGS type been deprecated?
+            if (policy.isTypeDeprecated(callerUid)) {
+                return FGS_TYPE_POLICY_CHECK_DEPRECATED;
+            }
+            return FGS_TYPE_POLICY_CHECK_OK;
+        }
+    }
+}
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 9aa67bc..62481ba 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -145,10 +145,9 @@
     void unregisterRemoteAnimations(in IBinder token);
 
     /**
-     * Reports that an Activity received a back key press when there were no additional activities
-     * on the back stack.
+     * Reports that an Activity received a back key press.
      */
-    oneway void onBackPressedOnTaskRoot(in IBinder activityToken,
+    oneway void onBackPressed(in IBinder activityToken,
             in IRequestFinishCallback callback);
 
     /** Reports that the splash screen view has attached to activity.  */
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 6404a1f..7475ef8 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -556,7 +556,8 @@
     void startConfirmDeviceCredentialIntent(in Intent intent, in Bundle options);
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     void sendIdleJobTrigger();
-    int sendIntentSender(in IIntentSender target, in IBinder whitelistToken, int code,
+    int sendIntentSender(in IApplicationThread caller, in IIntentSender target,
+            in IBinder whitelistToken, int code,
             in Intent intent, in String resolvedType, in IIntentReceiver finishedReceiver,
             in String requiredPermission, in Bundle options);
     boolean isBackgroundRestricted(in String packageName);
diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl
index 37c5cab..8111184 100644
--- a/core/java/android/app/IBackupAgent.aidl
+++ b/core/java/android/app/IBackupAgent.aidl
@@ -16,9 +16,12 @@
 
 package android.app;
 
+import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.IBackupCallback;
 import android.app.backup.IBackupManager;
 import android.os.ParcelFileDescriptor;
+
+import com.android.internal.infra.AndroidFuture;
  
 /**
  * Interface presented by applications being asked to participate in the
@@ -193,4 +196,14 @@
      * @param message The message to be passed to the agent's application in an exception.
      */
     void fail(String message);
+
+    /**
+     * Provides the logging results that were accumulated in the BackupAgent during a backup or
+     * restore operation. This method should be called after the agent completes its backup or
+     * restore.
+     *
+     * @param resultsFuture a future that is completed with the logging results.
+     */
+    void getLoggerResults(
+            in AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> resultsFuture);
 }
diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl
index 3002c8b..c38b64f 100644
--- a/core/java/android/app/ILocaleManager.aidl
+++ b/core/java/android/app/ILocaleManager.aidl
@@ -33,7 +33,7 @@
      /**
       * Sets a specified app’s app-specific UI locales.
       */
-     void setApplicationLocales(String packageName, int userId, in LocaleList locales);
+     void setApplicationLocales(String packageName, int userId, in LocaleList locales, boolean fromDelegate);
 
      /**
       * Returns the specified app's app-specific locales.
@@ -45,4 +45,4 @@
        */
      LocaleList getSystemLocales();
 
- }
\ No newline at end of file
+ }
diff --git a/core/java/android/app/IRequestFinishCallback.aidl b/core/java/android/app/IRequestFinishCallback.aidl
index 22c20c8..72426df 100644
--- a/core/java/android/app/IRequestFinishCallback.aidl
+++ b/core/java/android/app/IRequestFinishCallback.aidl
@@ -18,7 +18,7 @@
 
 /**
  * This callback allows ActivityTaskManager to ask the calling Activity
- * to finish in response to a call to onBackPressedOnTaskRoot.
+ * to finish in response to a call to onBackPressed.
  *
  * {@hide}
  */
diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java
index 2e83308..99f864c 100644
--- a/core/java/android/app/IntentService.java
+++ b/core/java/android/app/IntentService.java
@@ -57,8 +57,7 @@
  * @deprecated IntentService is subject to all the
  *   <a href="{@docRoot}about/versions/oreo/background.html">background execution limits</a>
  *   imposed with Android 8.0 (API level 26). Consider using {@link androidx.work.WorkManager}
- *   or {@link androidx.core.app.JobIntentService}, which uses jobs
- *   instead of services when running on Android 8.0 or higher.
+ *   instead.
  */
 @Deprecated
 public abstract class IntentService extends Service {
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 794c694..70c014f 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -71,7 +71,7 @@
      */
     @UserHandleAware
     public void setApplicationLocales(@NonNull LocaleList locales) {
-        setApplicationLocales(mContext.getPackageName(), locales);
+        setApplicationLocales(mContext.getPackageName(), locales, false);
     }
 
     /**
@@ -100,9 +100,14 @@
     @RequiresPermission(Manifest.permission.CHANGE_CONFIGURATION)
     @UserHandleAware
     public void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales) {
+        setApplicationLocales(appPackageName, locales, true);
+    }
+
+    private void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales,
+            boolean fromDelegate) {
         try {
             mService.setApplicationLocales(appPackageName, mContext.getUser().getIdentifier(),
-                    locales);
+                    locales, fromDelegate);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -127,6 +132,7 @@
      * <p>This API can be used by an app's installer
      * (per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName}) to retrieve
      * the app's locales.
+     * <p>This API can be used by the current input method to retrieve locales of another packages.
      * All other cases require {@code android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}.
      * Apps should generally retrieve their own locales via their in-process LocaleLists,
      * or by calling {@link #getApplicationLocales()}.
@@ -173,7 +179,7 @@
     @TestApi
     public void setSystemLocales(@NonNull LocaleList locales) {
         try {
-            Configuration conf = ActivityManager.getService().getConfiguration();
+            Configuration conf = new Configuration();
             conf.setLocales(locales);
             ActivityManager.getService().updatePersistentConfiguration(conf);
         } catch (RemoteException e) {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 74eb1c5..f9ef3cc 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8467,8 +8467,8 @@
             }
 
             int maxAvatarSize = resources.getDimensionPixelSize(
-                    isLowRam ? R.dimen.notification_person_icon_max_size
-                            : R.dimen.notification_person_icon_max_size_low_ram);
+                    isLowRam ? R.dimen.notification_person_icon_max_size_low_ram
+                            : R.dimen.notification_person_icon_max_size);
             if (mUser != null && mUser.getIcon() != null) {
                 mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize);
             }
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 7215987..9615b68 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -33,12 +33,12 @@
 import android.service.notification.NotificationListenerService;
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 5c29eb3..3bd86c1 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -23,10 +23,11 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index f3fc468..8ec313ec 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -11,6 +11,7 @@
 per-file ApplicationThreadConstants.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file BroadcastOptions.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file ContentProviderHolder* = file:/services/core/java/com/android/server/am/OWNERS
+per-file ForegroundService* = file:/services/core/java/com/android/server/am/OWNERS
 per-file IActivityController.aidl = file:/services/core/java/com/android/server/am/OWNERS
 per-file IActivityManager.aidl = file:/services/core/java/com/android/server/am/OWNERS
 per-file IApplicationThread.aidl = file:/services/core/java/com/android/server/am/OWNERS
@@ -29,10 +30,12 @@
 per-file Service* = file:/services/core/java/com/android/server/am/OWNERS
 per-file SystemServiceRegistry.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file *UserSwitchObserver* = file:/services/core/java/com/android/server/am/OWNERS
-per-file UiAutomation* = file:/services/accessibility/OWNERS
+per-file *UiAutomation* = file:/services/accessibility/OWNERS
 per-file GameManager* = file:/GAME_MANAGER_OWNERS
+per-file GameMode* = file:/GAME_MANAGER_OWNERS
 per-file GameState* = file:/GAME_MANAGER_OWNERS
 per-file IGameManager* = file:/GAME_MANAGER_OWNERS
+per-file IGameMode* = file:/GAME_MANAGER_OWNERS
 
 # ActivityThread
 per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index bc78df5..db47a4c 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -1009,7 +1009,9 @@
                 options = activityOptions.toBundle();
             }
 
-            return ActivityManager.getService().sendIntentSender(
+            final IApplicationThread app = ActivityThread.currentActivityThread()
+                    .getApplicationThread();
+            return ActivityManager.getService().sendIntentSender(app,
                     mTarget, mWhitelistToken, code, intent, resolvedType,
                     onFinished != null
                             ? new FinishedDispatcher(this, onFinished, handler)
diff --git a/core/java/android/app/PictureInPictureParams.java b/core/java/android/app/PictureInPictureParams.java
index 3f1844e..96d874e 100644
--- a/core/java/android/app/PictureInPictureParams.java
+++ b/core/java/android/app/PictureInPictureParams.java
@@ -157,13 +157,18 @@
         }
 
         /**
-         * Sets the source bounds hint. These bounds are only used when an activity first enters
-         * picture-in-picture, and describe the bounds in window coordinates of activity entering
-         * picture-in-picture that will be visible following the transition. For the best effect,
-         * these bounds should also match the aspect ratio in the arguments.
+         * Sets the window-coordinate bounds of an activity transitioning to picture-in-picture.
+         * The bounds is the area of an activity that will be visible in the transition to
+         * picture-in-picture mode. For the best effect, these bounds should also match the
+         * aspect ratio in the arguments.
+         *
+         * In Android 12+ these bounds are also reused to improve the exit transition from 
+         * picture-in-picture mode. See
+         * <a href="{@docRoot}develop/ui/views/picture-in-picture#smoother-exit">Support
+         * smoother animations when exiting out of PiP mode</a> for more details.
          *
          * @param launchBounds window-coordinate bounds indicating the area of the activity that
-         * will still be visible following the transition into picture-in-picture (eg. the video
+         * will still be visible following the transition into picture-in-picture (e.g. the video
          * view bounds in a video player)
          *
          * @return this builder instance.
diff --git a/core/java/android/app/SearchableInfo.java b/core/java/android/app/SearchableInfo.java
index 5388282..bd5d105 100644
--- a/core/java/android/app/SearchableInfo.java
+++ b/core/java/android/app/SearchableInfo.java
@@ -396,6 +396,17 @@
         private final String mSuggestActionMsg;
         private final String mSuggestActionMsgColumn;
 
+        public static final Parcelable.Creator<ActionKeyInfo> CREATOR =
+                new Parcelable.Creator<ActionKeyInfo>() {
+                    public ActionKeyInfo createFromParcel(Parcel in) {
+                        return new ActionKeyInfo(in);
+                    }
+
+                    public ActionKeyInfo[] newArray(int size) {
+                        return new ActionKeyInfo[size];
+                    }
+                };
+
         /**
          * Create one object using attributeset as input data.
          * @param activityContext runtime context of the activity that the action key information
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
index 7635138..754e3b6 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -17,10 +17,13 @@
 package android.app;
 
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
+import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import static android.text.TextUtils.formatSimple;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentCallbacks2;
 import android.content.ComponentName;
@@ -33,6 +36,7 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.Trace;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.contentcapture.ContentCaptureManager;
@@ -726,10 +730,32 @@
      * for more details.
      * </div>
      *
+     * <div class="caution">
+     * <p><strong>Note:</strong>
+     * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+     * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+     * or higher are not allowed to start foreground services without specifying a valid
+     * foreground service type in the manifest attribute
+     * {@link android.R.attr#foregroundServiceType}.
+     * See
+     * <a href="{@docRoot}/about/versions/14/behavior-changes-14">
+     * Behavior changes: Apps targeting Android 14
+     * </a>
+     * for more details.
+     * </div>
+     *
      * @throws ForegroundServiceStartNotAllowedException
      * If the app targeting API is
      * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from
      * becoming foreground service due to background restriction.
+     * @throws ForegroundServiceTypeNotAllowedException
+     * If the app targeting API is
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later, and the manifest attribute
+     * {@link android.R.attr#foregroundServiceType} is not set.
+     * @throws SecurityException If the app targeting API is
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later and doesn't have the
+     * permission to start the foreground service with the specified type in the manifest attribute
+     * {@link android.R.attr#foregroundServiceType}.
      *
      * @param id The identifier for this notification as per
      * {@link NotificationManager#notify(int, Notification)
@@ -740,64 +766,94 @@
      */
     public final void startForeground(int id, Notification notification) {
         try {
+            final ComponentName comp = new ComponentName(this, mClassName);
             mActivityManager.setServiceForeground(
-                    new ComponentName(this, mClassName), mToken, id,
+                    comp, mToken, id,
                     notification, 0, FOREGROUND_SERVICE_TYPE_MANIFEST);
             clearStartForegroundServiceStackTrace();
+            logForegroundServiceStart(comp, FOREGROUND_SERVICE_TYPE_MANIFEST);
         } catch (RemoteException ex) {
         }
     }
 
-  /**
-   * An overloaded version of {@link #startForeground(int, Notification)} with additional
-   * foregroundServiceType parameter.
-   *
-   * <p>Apps built with SDK version {@link android.os.Build.VERSION_CODES#Q} or later can specify
-   * the foreground service types using attribute {@link android.R.attr#foregroundServiceType} in
-   * service element of manifest file. The value of attribute
-   * {@link android.R.attr#foregroundServiceType} can be multiple flags ORed together.</p>
-   *
-   * <p>The foregroundServiceType parameter must be a subset flags of what is specified in manifest
-   * attribute {@link android.R.attr#foregroundServiceType}, if not, an IllegalArgumentException is
-   * thrown. Specify foregroundServiceType parameter as
-   * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST} to use all flags that
-   * is specified in manifest attribute foregroundServiceType.</p>
-   *
-   * <div class="caution">
-   * <p><strong>Note:</strong>
-   * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#S},
-   * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#S}
-   * or higher are not allowed to start foreground services from the background.
-   * See
-   * <a href="{@docRoot}/about/versions/12/behavior-changes-12">
-   * Behavior changes: Apps targeting Android 12
-   * </a>
-   * for more details.
-   * </div>
-   *
-   * @param id The identifier for this notification as per
-   * {@link NotificationManager#notify(int, Notification)
-   * NotificationManager.notify(int, Notification)}; must not be 0.
-   * @param notification The Notification to be displayed.
-   * @param foregroundServiceType must be a subset flags of manifest attribute
-   * {@link android.R.attr#foregroundServiceType} flags.
-   *
-   * @throws IllegalArgumentException if param foregroundServiceType is not subset of manifest
-   *     attribute {@link android.R.attr#foregroundServiceType}.
-   * @throws ForegroundServiceStartNotAllowedException
-   * If the app targeting API is
-   * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from
-   * becoming foreground service due to background restriction.
-   *
-   * @see android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST
-   */
+    /**
+     * An overloaded version of {@link #startForeground(int, Notification)} with additional
+     * foregroundServiceType parameter.
+     *
+     * <p>Apps built with SDK version {@link android.os.Build.VERSION_CODES#Q} or later can specify
+     * the foreground service types using attribute {@link android.R.attr#foregroundServiceType} in
+     * service element of manifest file. The value of attribute
+     * {@link android.R.attr#foregroundServiceType} can be multiple flags ORed together.</p>
+     *
+     * <p>The foregroundServiceType parameter must be a subset flags of what is specified in
+     * manifest attribute {@link android.R.attr#foregroundServiceType}, if not, an
+     * IllegalArgumentException is thrown. Specify foregroundServiceType parameter as
+     * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST} to use all flags that
+     * is specified in manifest attribute foregroundServiceType.</p>
+     *
+     * <div class="caution">
+     * <p><strong>Note:</strong>
+     * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#S},
+     * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#S}
+     * or higher are not allowed to start foreground services from the background.
+     * See
+     * <a href="{@docRoot}/about/versions/12/behavior-changes-12">
+     * Behavior changes: Apps targeting Android 12
+     * </a>
+     * for more details.
+     * </div>
+     *
+     * <div class="caution">
+     * <p><strong>Note:</strong>
+     * Beginning with SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+     * apps targeting SDK Version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+     * or higher are not allowed to start foreground services without specifying a valid
+     * foreground service type in the manifest attribute
+     * {@link android.R.attr#foregroundServiceType}, and the parameter {@code foregroundServiceType}
+     * here must not be the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     * See
+     * <a href="{@docRoot}/about/versions/14/behavior-changes-14">
+     * Behavior changes: Apps targeting Android 14
+     * </a>
+     * for more details.
+     * </div>
+     *
+     * @param id The identifier for this notification as per
+     * {@link NotificationManager#notify(int, Notification)
+     * NotificationManager.notify(int, Notification)}; must not be 0.
+     * @param notification The Notification to be displayed.
+     * @param foregroundServiceType must be a subset flags of manifest attribute
+     * {@link android.R.attr#foregroundServiceType} flags; must not be
+     * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     *
+     * @throws IllegalArgumentException if param foregroundServiceType is not subset of manifest
+     *     attribute {@link android.R.attr#foregroundServiceType}.
+     * @throws ForegroundServiceStartNotAllowedException
+     * If the app targeting API is
+     * {@link android.os.Build.VERSION_CODES#S} or later, and the service is restricted from
+     * becoming foreground service due to background restriction.
+     * @throws ForegroundServiceTypeNotAllowedException
+     * If the app targeting API is
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later, and the manifest attribute
+     * {@link android.R.attr#foregroundServiceType} is not set, or the param
+     * {@code foregroundServiceType} is {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     * @throws SecurityException If the app targeting API is
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later and doesn't have the
+     * permission to start the foreground service with the specified type in
+     * {@code foregroundServiceType}.
+     * {@link android.R.attr#foregroundServiceType}.
+     *
+     * @see android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MANIFEST
+     */
     public final void startForeground(int id, @NonNull Notification notification,
-            @ForegroundServiceType int foregroundServiceType) {
+            @RequiresPermission @ForegroundServiceType int foregroundServiceType) {
         try {
+            final ComponentName comp = new ComponentName(this, mClassName);
             mActivityManager.setServiceForeground(
-                    new ComponentName(this, mClassName), mToken, id,
+                    comp, mToken, id,
                     notification, 0, foregroundServiceType);
             clearStartForegroundServiceStackTrace();
+            logForegroundServiceStart(comp, foregroundServiceType);
         } catch (RemoteException ex) {
         }
     }
@@ -848,6 +904,7 @@
             mActivityManager.setServiceForeground(
                     new ComponentName(this, mClassName), mToken, 0, null,
                     notificationBehavior, 0);
+            logForegroundServiceStopIfNecessary();
         } catch (RemoteException ex) {
         }
     }
@@ -943,6 +1000,7 @@
      */
     public final void detachAndCleanUp() {
         mToken = null;
+        logForegroundServiceStopIfNecessary();
     }
 
     final String getClassName() {
@@ -976,6 +1034,50 @@
     private boolean mStartCompatibility = false;
 
     /**
+     * This will be set to the title of the system trace when this service is started as
+     * a foreground service, and will be set to null when it's no longer in foreground
+     * service state.
+     */
+    @GuardedBy("mForegroundServiceTraceTitleLock")
+    private @Nullable String mForegroundServiceTraceTitle = null;
+
+    private final Object mForegroundServiceTraceTitleLock = new Object();
+
+    private static final String TRACE_TRACK_NAME_FOREGROUND_SERVICE = "FGS";
+
+    private void logForegroundServiceStart(ComponentName comp,
+            @ForegroundServiceType int foregroundServiceType) {
+        synchronized (mForegroundServiceTraceTitleLock) {
+            if (mForegroundServiceTraceTitle == null) {
+                mForegroundServiceTraceTitle = formatSimple("comp=%s type=%s",
+                        comp.toShortString(), Integer.toHexString(foregroundServiceType));
+                // The service is not in foreground state, emit a start event.
+                Trace.asyncTraceForTrackBegin(TRACE_TAG_ACTIVITY_MANAGER,
+                        TRACE_TRACK_NAME_FOREGROUND_SERVICE,
+                        mForegroundServiceTraceTitle,
+                        System.identityHashCode(this));
+            } else {
+                // The service is already in foreground state, emit an one-off event.
+                Trace.instantForTrack(TRACE_TAG_ACTIVITY_MANAGER,
+                        TRACE_TRACK_NAME_FOREGROUND_SERVICE,
+                        mForegroundServiceTraceTitle);
+            }
+        }
+    }
+
+    private void logForegroundServiceStopIfNecessary() {
+        synchronized (mForegroundServiceTraceTitleLock) {
+            if (mForegroundServiceTraceTitle != null) {
+                Trace.asyncTraceForTrackEnd(TRACE_TAG_ACTIVITY_MANAGER,
+                        TRACE_TRACK_NAME_FOREGROUND_SERVICE,
+                        mForegroundServiceTraceTitle,
+                        System.identityHashCode(this));
+                mForegroundServiceTraceTitle = null;
+            }
+        }
+    }
+
+    /**
      * This keeps track of the stacktrace where Context.startForegroundService() was called
      * for each service class. We use that when we crash the app for not calling
      * {@link #startForeground} in time, in {@link ActivityThread#throwRemoteServiceException}.
@@ -1004,4 +1106,16 @@
             return sStartForegroundServiceStackTraces.get(className);
         }
     }
+
+    /**
+     * Callback called on timeout for {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}.
+     *
+     * TODO Implement it
+     * TODO Javadoc
+     *
+     * @param startId
+     * @hide
+     */
+    public void onTimeout(int startId) {
+    }
 }
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 6d28972..a035375 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -229,6 +229,8 @@
     public static final int CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP = 1;
     /** @hide */
     public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2;
+    /** @hide */
+    public static final int CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE = 3;
 
     /**
      * Session flag for {@link #registerSessionListener} indicating the listener
diff --git a/core/java/android/app/SyncNotedAppOp.java b/core/java/android/app/SyncNotedAppOp.java
index f156b30..f674e88 100644
--- a/core/java/android/app/SyncNotedAppOp.java
+++ b/core/java/android/app/SyncNotedAppOp.java
@@ -56,7 +56,7 @@
      * The package this op applies to
      * @hide
      */
-    private final @NonNull String mPackageName;
+    private final @Nullable String mPackageName;
 
     /**
      * Native code relies on parcel ordering, do not change
@@ -64,7 +64,7 @@
      */
     @TestApi
     public SyncNotedAppOp(int opMode, @IntRange(from = 0L) int opCode,
-            @Nullable String attributionTag, @NonNull String packageName) {
+            @Nullable String attributionTag, @Nullable String packageName) {
         this.mOpCode = opCode;
         com.android.internal.util.AnnotationValidations.validate(
                 IntRange.class, null, mOpCode,
@@ -101,7 +101,7 @@
      * @hide
      */
     public SyncNotedAppOp(@IntRange(from = 0L) int opCode, @Nullable String attributionTag,
-            @NonNull String packageName) {
+            @Nullable String packageName) {
         this(AppOpsManager.MODE_IGNORED, opCode, attributionTag, packageName);
     }
 
@@ -152,7 +152,7 @@
      * @hide
      */
     @DataClass.Generated.Member
-    public @NonNull String getPackageName() {
+    public @Nullable String getPackageName() {
         return mPackageName;
     }
 
@@ -211,11 +211,12 @@
 
         byte flg = 0;
         if (mAttributionTag != null) flg |= 0x4;
+        if (mPackageName != null) flg |= 0x8;
         dest.writeByte(flg);
         dest.writeInt(mOpMode);
         dest.writeInt(mOpCode);
         if (mAttributionTag != null) dest.writeString(mAttributionTag);
-        dest.writeString(mPackageName);
+        if (mPackageName != null) dest.writeString(mPackageName);
     }
 
     @Override
@@ -233,7 +234,7 @@
         int opMode = in.readInt();
         int opCode = in.readInt();
         String attributionTag = (flg & 0x4) == 0 ? null : in.readString();
-        String packageName = in.readString();
+        String packageName = (flg & 0x8) == 0 ? null : in.readString();
 
         this.mOpMode = opMode;
         this.mOpCode = opCode;
@@ -243,8 +244,6 @@
                 "to", AppOpsManager._NUM_OP - 1);
         this.mAttributionTag = attributionTag;
         this.mPackageName = packageName;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mPackageName);
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -264,10 +263,10 @@
     };
 
     @DataClass.Generated(
-            time = 1643320427700L,
+            time = 1667247337573L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/app/SyncNotedAppOp.java",
-            inputSignatures = "private final  int mOpMode\nprivate final @android.annotation.IntRange int mOpCode\nprivate final @android.annotation.Nullable java.lang.String mAttributionTag\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic @android.annotation.NonNull java.lang.String getOp()\npublic  int getOpMode()\nprivate  java.lang.String opCodeToString()\nclass SyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genConstructor=false, genToString=true)")
+            inputSignatures = "private final  int mOpMode\nprivate final @android.annotation.IntRange int mOpCode\nprivate final @android.annotation.Nullable java.lang.String mAttributionTag\nprivate final @android.annotation.Nullable java.lang.String mPackageName\npublic @android.annotation.NonNull java.lang.String getOp()\npublic  int getOpMode()\nprivate  java.lang.String opCodeToString()\nclass SyncNotedAppOp extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genConstructor=false, genToString=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 4ddfdb6..aaa3d21 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -85,6 +85,7 @@
 import android.credentials.ICredentialManager;
 import android.debug.AdbManager;
 import android.debug.IAdbManager;
+import android.devicelock.DeviceLockFrameworkInitializer;
 import android.graphics.fonts.FontManager;
 import android.hardware.ConsumerIrManager;
 import android.hardware.ISerialManager;
@@ -172,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;
@@ -1365,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
@@ -1555,6 +1565,7 @@
             ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers();
             NearbyFrameworkInitializer.registerServiceWrappers();
             OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers();
+            DeviceLockFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5b0bd96..ef10c0b 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -115,6 +115,15 @@
             "file_patterns": ["(/|^)VoiceInteract[^/]*"]
         },
         {
+            "name": "CtsLocalVoiceInteraction",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                }
+            ],
+            "file_patterns": ["(/|^)VoiceInteract[^/]*"]
+        },
+        {
             "name": "CtsOsTestCases",
             "options": [
                 {
@@ -175,6 +184,23 @@
             "file_patterns": [
                 "(/|^)KeyguardManager.java"
             ]
+        },
+        {
+            "name": "FrameworksCoreTests",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation": "org.junit.Ignore"
+                },
+                {
+                    "include-filter": "android.app.PropertyInvalidatedCacheTests"
+                }
+            ],
+            "file_patterns": [
+                "(/|^)PropertyInvalidatedCache.java"
+            ]
         }
     ],
     "presubmit-large": [
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index a2dc47d..5a2f261 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -458,7 +458,8 @@
                 && isVisible == that.isVisible
                 && isSleeping == that.isSleeping
                 && Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId)
-                && parentTaskId == that.parentTaskId;
+                && parentTaskId == that.parentTaskId
+                && Objects.equals(topActivity, that.topActivity);
     }
 
     /**
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 95e9c22..01f02fc 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1318,18 +1318,16 @@
     }
 
     /**
-     * Returns the information about the wallpaper if the current wallpaper is
-     * a live wallpaper component. Otherwise, if the wallpaper is a static image,
-     * this returns null.
+     * Returns the information about the home screen wallpaper if its current wallpaper is a live
+     * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
      */
     public WallpaperInfo getWallpaperInfo() {
         return getWallpaperInfo(mContext.getUserId());
     }
 
     /**
-     * Returns the information about the wallpaper if the current wallpaper is
-     * a live wallpaper component. Otherwise, if the wallpaper is a static image,
-     * this returns null.
+     * Returns the information about the home screen wallpaper if its current wallpaper is a live
+     * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
      *
      * @param userId Owner of the wallpaper.
      * @hide
@@ -1348,6 +1346,29 @@
     }
 
     /**
+     * Returns the information about the home screen wallpaper if its current wallpaper is a live
+     * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
+     *
+     * @param which Specifies wallpaper destination (home or lock).
+     * @hide
+     */
+    public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which) {
+        return getWallpaperInfo();
+    }
+
+    /**
+     * Returns the information about the designated wallpaper if its current wallpaper is a live
+     * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
+     *
+     * @param which Specifies wallpaper destination (home or lock).
+     * @param userId Owner of the wallpaper.
+     * @hide
+     */
+    public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which, int userId) {
+        return getWallpaperInfo(userId);
+    }
+
+    /**
      * Get the ID of the current wallpaper of the given kind.  If there is no
      * such wallpaper configured, returns a negative number.
      *
@@ -2456,7 +2477,12 @@
 
         public void waitForCompletion() {
             try {
-                mLatch.await(30, TimeUnit.SECONDS);
+                final boolean completed = mLatch.await(30, TimeUnit.SECONDS);
+                if (completed) {
+                    Log.d(TAG, "Wallpaper set completion.");
+                } else {
+                    Log.d(TAG, "Timeout waiting for wallpaper set completion!");
+                }
             } catch (InterruptedException e) {
                 // This might be legit: the crop may take a very long time. Don't sweat
                 // it in that case; we are okay with display lagging behind in order to
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 41256d0..67408a4 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -37,10 +37,11 @@
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index de19687..be4df9d 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -103,6 +103,7 @@
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.net.NetworkUtilsInternal;
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
 import com.android.org.conscrypt.TrustedCertificateStore;
 
@@ -171,6 +172,7 @@
     private final boolean mParentInstance;
     private final DevicePolicyResourcesManager mResourcesManager;
 
+
     /** @hide */
     public DevicePolicyManager(Context context, IDevicePolicyManager service) {
         this(context, service, false);
@@ -3822,6 +3824,27 @@
     public static final int OPERATION_SAFETY_REASON_DRIVING_DISTRACTION = 1;
 
     /**
+     * Prevent an app from being placed into app standby buckets, such that it will not be subject
+     * to device resources restrictions as a result of app standby buckets.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int EXEMPT_FROM_APP_STANDBY =  0;
+
+    /**
+     * Exemptions to platform restrictions, given to an application through
+     * {@link #setApplicationExemptions(String, Set)}.
+     *
+     * @hide
+     */
+    @IntDef(prefix = { "EXEMPT_FROM_"}, value = {
+            EXEMPT_FROM_APP_STANDBY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ApplicationExemptionConstants {}
+
+    /**
      * Broadcast action: notify system apps (e.g. settings, SysUI, etc) that the device management
      * resources with IDs {@link #EXTRA_RESOURCE_IDS} has been updated, the updated resources can be
      * retrieved using {@link DevicePolicyResourcesManager#getDrawable} and
@@ -3864,6 +3887,56 @@
     public static final String EXTRA_RESOURCE_IDS =
             "android.app.extra.RESOURCE_IDS";
 
+    /** Allow the user to choose whether to enable MTE on the device. */
+    public static final int MTE_NOT_CONTROLLED_BY_POLICY = 0;
+
+    /**
+     * Require that MTE be enabled on the device, if supported. Can be set by a device owner or a
+     * profile owner of an organization-owned managed profile.
+     */
+    public static final int MTE_ENABLED = 1;
+
+    /** Require that MTE be disabled on the device. Can be set by a device owner. */
+    public static final int MTE_DISABLED = 2;
+
+    /** @hide */
+    @IntDef(
+            prefix = {"MTE_"},
+            value = {MTE_ENABLED, MTE_DISABLED, MTE_NOT_CONTROLLED_BY_POLICY})
+    @Retention(RetentionPolicy.SOURCE)
+    public static @interface MtePolicy {}
+
+    /**
+     * Set MTE policy for device. MTE_ENABLED does not necessarily enable MTE if set on a device
+     * that does not support MTE.
+     *
+     * The default policy is MTE_NOT_CONTROLLED_BY_POLICY.
+     *
+     * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain
+     * classes of security problems at a small runtime performance cost overhead.
+     *
+     * @param policy the policy to be set
+     */
+    public void setMtePolicy(@MtePolicy int policy) {
+        // TODO(b/244290023): implement
+        // This is SecurityException to temporarily make ParentProfileTest happy.
+        // This is not used.
+        throw new SecurityException("not implemented");
+    }
+
+    /**
+     * Get currently set MTE policy. This is not necessarily the same as the state of MTE on the
+     * device, as the device might not support MTE.
+     *
+     * @return the currently set policy
+     */
+    public @MtePolicy int getMtePolicy() {
+        // TODO(b/244290023): implement
+        // This is SecurityException to temporarily make ParentProfileTest happy.
+        // This is not used.
+        throw new SecurityException("not implemented");
+    }
+
     /**
      * This object is a single place to tack on invalidation and disable calls.  All
      * binder caches in this class derive from this Config, so all can be invalidated or
@@ -6157,46 +6230,46 @@
     public static final int WIPE_SILENTLY = 0x0008;
 
     /**
-     * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
-     * other users will remain unaffected. Calling from the primary user will cause the device to
-     * reboot, erasing all device data - including all the secondary users and their data - while
-     * booting up.
-     * <p>
-     * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
-     * be able to call this method; if it has not, a security exception will be thrown.
-     *
-     * If the caller is a profile owner of an organization-owned managed profile, it may
-     * additionally call this method on the parent instance.
-     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
-     * entire device, while calling it on the current profile instance would relinquish the device
-     * for personal use, removing the managed profile and all policies set by the profile owner.
+     * See {@link #wipeData(int, CharSequence)}
      *
      * @param flags Bit mask of additional options: currently supported flags are
-     *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
-     *            {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
-     * @throws SecurityException if the calling application does not own an active administrator
-     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
-     *            {@link android.Manifest.permission#MASTER_CLEAR} permission.
+     *              {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+     *              {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+     * @throws SecurityException     if the calling application does not own an active
+     *                               administrator
+     *                               that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is
+     *                               not granted the
+     *                               {@link android.Manifest.permission#MASTER_CLEAR} permission.
+     * @throws IllegalStateException if called on last full-user or system-user
+     * @see #wipeDevice(int)
+     * @see #wipeData(int, CharSequence)
      */
     public void wipeData(int flags) {
-        wipeDataInternal(flags, "");
+        wipeDataInternal(flags,
+                /* wipeReasonForUser= */ "",
+                /* factoryReset= */ false);
     }
 
     /**
-     * Ask that all user data be wiped. If called as a secondary user, the user will be removed and
-     * other users will remain unaffected, the provided reason for wiping data can be shown to
-     * user. Calling from the primary user will cause the device to reboot, erasing all device data
-     * - including all the secondary users and their data - while booting up. In this case, we don't
-     * show the reason to the user since the device would be factory reset.
-     * <p>
-     * The calling device admin must have requested {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to
-     * be able to call this method; if it has not, a security exception will be thrown.
+     * Ask that all user data be wiped.
      *
-     * If the caller is a profile owner of an organization-owned managed profile, it may
-     * additionally call this method on the parent instance.
-     * Calling this method on the parent {@link DevicePolicyManager} instance would wipe the
-     * entire device, while calling it on the current profile instance would relinquish the device
-     * for personal use, removing the managed profile and all policies set by the profile owner.
+     * <p>
+     * If called as a secondary user or managed profile, the user itself and its associated user
+     * data will be wiped. In particular, If the caller is a profile owner of an
+     * organization-owned managed profile, calling this method will relinquish the device for
+     * personal use, removing the managed profile and all policies set by the profile owner.
+     * </p>
+     *
+     * <p>
+     * Calling this method from the primary user will only work if the calling app is targeting
+     * Android 13 or below, in which case it will cause the device to reboot, erasing all device
+     * data - including all the secondary users and their data - while booting up. If an app
+     * targeting Android 13+ is calling this method from the primary user or last full user,
+     * {@link IllegalStateException} will be thrown.
+     * </p>
+     *
+     * If an app wants to wipe the entire device irrespective of which user they are from, they
+     * should use {@link #wipeDevice} instead.
      *
      * @param flags Bit mask of additional options: currently supported flags are
      *            {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA} and
@@ -6204,30 +6277,61 @@
      * @param reason a string that contains the reason for wiping data, which can be
      *            presented to the user.
      * @throws SecurityException if the calling application does not own an active administrator
-     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} or is not granted the
+     *            that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not granted the
      *            {@link android.Manifest.permission#MASTER_CLEAR} permission.
      * @throws IllegalArgumentException if the input reason string is null or empty, or if
      *            {@link #WIPE_SILENTLY} is set.
+     * @throws IllegalStateException if called on last full-user or system-user
+     * @see #wipeDevice(int)
+     * @see #wipeData(int)
      */
     public void wipeData(int flags, @NonNull CharSequence reason) {
         Objects.requireNonNull(reason, "reason string is null");
         Preconditions.checkStringNotEmpty(reason, "reason string is empty");
         Preconditions.checkArgument((flags & WIPE_SILENTLY) == 0, "WIPE_SILENTLY cannot be set");
-        wipeDataInternal(flags, reason.toString());
+        wipeDataInternal(flags, reason.toString(), /* factoryReset= */ false);
     }
 
     /**
-     * Internal function for both {@link #wipeData(int)} and
-     * {@link #wipeData(int, CharSequence)} to call.
+     * Ask that the device be wiped and factory reset.
      *
+     * <p>
+     * The calling Device Owner or Organization Owned Profile Owner must have requested
+     * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} to be able to call this method; if it has
+     * not, a security exception will be thrown.
+     *
+     * @param flags Bit mask of additional options: currently supported flags are
+     *              {@link #WIPE_EXTERNAL_STORAGE}, {@link #WIPE_RESET_PROTECTION_DATA},
+     *              {@link #WIPE_EUICC} and {@link #WIPE_SILENTLY}.
+     * @throws SecurityException if the calling application does not own an active administrator
+     *                           that uses {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA} and is not
+     *                           granted the {@link android.Manifest.permission#MASTER_CLEAR}
+     *                           permission.
      * @see #wipeData(int)
      * @see #wipeData(int, CharSequence)
-     * @hide
      */
-    private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser) {
+    // TODO(b/255323293) Add host-side tests
+    public void wipeDevice(int flags) {
+        wipeDataInternal(flags,
+                /* wipeReasonForUser= */ "",
+                /* factoryReset= */ true);
+    }
+
+    /**
+     * Internal function for {@link #wipeData(int)}, {@link #wipeData(int, CharSequence)}
+     * and {@link #wipeDevice(int)} to call.
+     *
+     * @hide
+     * @see #wipeData(int)
+     * @see #wipeData(int, CharSequence)
+     * @see #wipeDevice(int)
+     */
+    private void wipeDataInternal(int flags, @NonNull String wipeReasonForUser,
+            boolean factoryReset) {
         if (mService != null) {
             try {
-                mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance);
+                mService.wipeDataWithReason(flags, wipeReasonForUser, mParentInstance,
+                        factoryReset);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -8592,7 +8696,7 @@
     public void reportFailedPasswordAttempt(int userHandle) {
         if (mService != null) {
             try {
-                mService.reportFailedPasswordAttempt(userHandle);
+                mService.reportFailedPasswordAttempt(userHandle, mParentInstance);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -14645,6 +14749,95 @@
     }
 
     /**
+     * Service-specific error code used in {@link #setApplicationExemptions(String, Set)} and
+     * {@link #getApplicationExemptions(String)}.
+     * @hide
+     */
+    public static final int ERROR_PACKAGE_NAME_NOT_FOUND = 1;
+
+    /**
+     * Called by an application with the
+     * {@link  android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission, to
+     * grant platform restriction exemptions to a given application.
+     *
+     * @param  packageName The package name of the application to be exempt.
+     * @param  exemptions The set of exemptions to be applied.
+     * @throws SecurityException If the caller does not have
+     *             {@link  android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS}
+     * @throws NameNotFoundException If either the package is not installed or the package is not
+     *              visible to the caller.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)
+    public void setApplicationExemptions(@NonNull String packageName,
+            @NonNull @ApplicationExemptionConstants Set<Integer> exemptions)
+            throws NameNotFoundException {
+        throwIfParentInstance("setApplicationExemptions");
+        if (mService != null) {
+            try {
+                mService.setApplicationExemptions(packageName,
+                        ArrayUtils.convertToIntArray(new ArraySet<>(exemptions)));
+            } catch (ServiceSpecificException e) {
+                switch (e.errorCode) {
+                    case ERROR_PACKAGE_NAME_NOT_FOUND:
+                        throw new NameNotFoundException(e.getMessage());
+                    default:
+                        throw new RuntimeException(
+                                "Unknown error setting application exemptions: " + e.errorCode, e);
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Returns all the platform restriction exemptions currently applied to an application. Called
+     * by an application with the
+     * {@link  android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS} permission.
+     *
+     * @param  packageName The package name to check.
+     * @return A set of platform restrictions an application is exempt from.
+     * @throws SecurityException If the caller does not have
+     *             {@link  android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_EXEMPTIONS}
+     * @throws NameNotFoundException If either the package is not installed or the package is not
+     *              visible to the caller.
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)
+    public Set<Integer> getApplicationExemptions(@NonNull String packageName)
+            throws NameNotFoundException {
+        throwIfParentInstance("getApplicationExemptions");
+        if (mService == null) {
+            return Collections.emptySet();
+        }
+        try {
+            return intArrayToSet(mService.getApplicationExemptions(packageName));
+        } catch (ServiceSpecificException e) {
+            switch (e.errorCode) {
+                case ERROR_PACKAGE_NAME_NOT_FOUND:
+                    throw new NameNotFoundException(e.getMessage());
+                default:
+                    throw new RuntimeException(
+                            "Unknown error getting application exemptions: " + e.errorCode, e);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private Set<Integer> intArrayToSet(int[] array) {
+        Set<Integer> set = new ArraySet<>();
+        for (int item : array) {
+            set.add(item);
+        }
+        return set;
+    }
+
+    /**
      * Called by a device owner or a profile owner to disable user control over apps. User will not
      * be able to clear app data or force-stop packages. When called by a device owner, applies to
      * all users on the device. Starting from Android 13, packages with user control disabled are
diff --git a/core/java/android/app/admin/FactoryResetProtectionPolicy.java b/core/java/android/app/admin/FactoryResetProtectionPolicy.java
index 7e95177..efa23dd 100644
--- a/core/java/android/app/admin/FactoryResetProtectionPolicy.java
+++ b/core/java/android/app/admin/FactoryResetProtectionPolicy.java
@@ -27,8 +27,9 @@
 import android.os.Parcelable;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 75bfc25..8a40265 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -117,7 +117,10 @@
 
     void lockNow(int flags, boolean parent);
 
-    void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent);
+    /**
+    * @param factoryReset only applicable when `targetSdk >= U`, either tries to factoryReset/fail or removeUser/fail otherwise
+    **/
+    void wipeDataWithReason(int flags, String wipeReasonForUser, boolean parent, boolean factoryReset);
 
     void setFactoryResetProtectionPolicy(in ComponentName who, in FactoryResetProtectionPolicy policy);
     FactoryResetProtectionPolicy getFactoryResetProtectionPolicy(in ComponentName who);
@@ -161,7 +164,7 @@
     boolean hasGrantedPolicy(in ComponentName policyReceiver, int usesPolicy, int userHandle);
 
     void reportPasswordChanged(in PasswordMetrics metrics, int userId);
-    void reportFailedPasswordAttempt(int userHandle);
+    void reportFailedPasswordAttempt(int userHandle, boolean parent);
     void reportSuccessfulPasswordAttempt(int userHandle);
     void reportFailedBiometricAttempt(int userHandle);
     void reportSuccessfulBiometricAttempt(int userHandle);
@@ -565,4 +568,7 @@
     boolean shouldAllowBypassingDevicePolicyManagementRoleQualification();
 
     List<UserHandle> getPolicyManagedProfiles(in UserHandle userHandle);
+
+    void setApplicationExemptions(String packageName, in int[]exemptions);
+    int[] getApplicationExemptions(String packageName);
 }
diff --git a/core/java/android/app/admin/ParcelableResource.java b/core/java/android/app/admin/ParcelableResource.java
index a297665..5b438f8 100644
--- a/core/java/android/app/admin/ParcelableResource.java
+++ b/core/java/android/app/admin/ParcelableResource.java
@@ -30,8 +30,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
index 63c9839..b0ea499 100644
--- a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
+++ b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
@@ -27,8 +27,9 @@
 import android.os.Parcelable;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/SystemUpdateInfo.java b/core/java/android/app/admin/SystemUpdateInfo.java
index b88bf76..9e6c91f 100644
--- a/core/java/android/app/admin/SystemUpdateInfo.java
+++ b/core/java/android/app/admin/SystemUpdateInfo.java
@@ -22,8 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/SystemUpdatePolicy.java b/core/java/android/app/admin/SystemUpdatePolicy.java
index 68ac4cc..b100eb2 100644
--- a/core/java/android/app/admin/SystemUpdatePolicy.java
+++ b/core/java/android/app/admin/SystemUpdatePolicy.java
@@ -26,8 +26,9 @@
 import android.os.Parcelable;
 import android.util.Log;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index b1b59b0..a4f612d 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -41,6 +41,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
 
 import libcore.io.IoUtils;
 
@@ -202,6 +203,7 @@
 
     Handler mHandler = null;
 
+    @Nullable private volatile BackupRestoreEventLogger mLogger = null;
     @Nullable private UserHandle mUser;
      // This field is written from the main thread (in onCreate), and read in a Binder thread (in
      // onFullBackup that is called from system_server via Binder).
@@ -234,6 +236,20 @@
         } catch (InterruptedException e) { /* ignored */ }
     }
 
+    /**
+     * Get a logger to record app-specific backup and restore events that are happening during a
+     * backup or restore operation.
+     *
+     * <p>The logger instance had been created by the system with the correct {@link
+     * BackupRestoreEventLogger.OperationType} that corresponds to the operation the {@code
+     * BackupAgent} is currently handling.
+     *
+     * @hide
+     */
+    @Nullable
+    public BackupRestoreEventLogger getBackupRestoreEventLogger() {
+        return mLogger;
+    }
 
     public BackupAgent() {
         super(null);
@@ -264,6 +280,9 @@
      * @hide
      */
     public void onCreate(UserHandle user, @OperationType int operationType) {
+        // TODO: Instantiate with the correct type using a parameter.
+        mLogger = new BackupRestoreEventLogger(BackupRestoreEventLogger.OperationType.BACKUP);
+
         onCreate();
 
         mUser = user;
@@ -1305,6 +1324,16 @@
                 }
             }
         }
+
+        @Override
+        public void getLoggerResults(
+                AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> in) {
+            if (mLogger != null) {
+                in.complete(mLogger.getLoggingResults());
+            } else {
+                in.complete(Collections.emptyList());
+            }
+        }
     }
 
     static class FailRunnable implements Runnable {
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 88a7c0f..d2c7972 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -1123,18 +1122,4 @@
             });
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
-
 }
diff --git a/core/java/android/app/backup/BackupManagerMonitorWrapper.java b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
new file mode 100644
index 0000000..0b18995
--- /dev/null
+++ b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+/**
+ * Wrapper around {@link BackupManagerMonitor} that helps with IPC between the caller of backup
+ * APIs and the backup service.
+ *
+ * The caller implements {@link BackupManagerMonitor} and passes it into framework APIs that run on
+ * the caller's process. Those framework APIs will then wrap it around this class when doing the
+ * actual IPC.
+ */
+class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
+    private final BackupManagerMonitor mMonitor;
+
+    BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
+        mMonitor = monitor;
+    }
+
+    @Override
+    public void onEvent(final Bundle event) throws RemoteException {
+        mMonitor.onEvent(event);
+    }
+}
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.aidl b/core/java/android/app/backup/BackupRestoreEventLogger.aidl
new file mode 100644
index 0000000..d6ef4e6
--- /dev/null
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+parcelable BackupRestoreEventLogger.DataTypeResult;
\ No newline at end of file
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
new file mode 100644
index 0000000..68740cb
--- /dev/null
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.Slog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+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;
+
+// TODO(b/244436184): Make this @SystemApi
+/**
+ * Class to log B&R stats for each data type that is backed up and restored by the calling app.
+ *
+ * The logger instance is designed to accept a limited number of unique
+ * {link @BackupRestoreDataType} values, as determined by the underlying implementation. Apps are
+ * expected to have a small pre-defined set of data type values they use. Attempts to log too many
+ * unique values will be rejected.
+ *
+ * @hide
+ */
+public class BackupRestoreEventLogger {
+    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.
+     */
+    public static final int DATA_TYPES_ALLOWED = 15;
+
+    /**
+     * Operation types for which this logger can be used.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            OperationType.BACKUP,
+            OperationType.RESTORE
+    })
+    @interface OperationType {
+        int BACKUP = 1;
+        int RESTORE = 2;
+    }
+
+    /**
+     * Denotes that the annotated element identifies a data type as required by the logging methods
+     * of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreDataType {}
+
+    /**
+     * Denotes that the annotated element identifies an error type as required by the logging
+     * methods of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreError {}
+
+    private final int mOperationType;
+    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
+     *                      {@link OperationType}. Attempts to use logging methods that don't match
+     *                      the specified operation type will be rejected (e.g. use backup methods
+     *                      for a restore logger and vice versa).
+     *
+     * @hide
+     */
+    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;
+    }
+
+    /**
+     * Report progress during a backup operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles for any items of that type that have been
+     * successfully backed up. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have been successfully backed up.
+     */
+    public void logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) {
+        logSuccess(OperationType.BACKUP, dataType, count);
+    }
+
+    /**
+     * Report errors during a backup operation. Call this method whenever items of a certain data
+     * type failed to back up. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have failed to back up.
+     * @param error optional, the error that has caused the failure.
+     */
+    public void logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        logFailure(OperationType.BACKUP, dataType, count, error);
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being backed up, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same {@code
+     * dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed up.
+     * @param metaData the metadata associated with the data type.
+     */
+    public void logBackupMetaData(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull String metaData) {
+        logMetaData(OperationType.BACKUP, dataType, metaData);
+    }
+
+    /**
+     * Report progress during a restore operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles if any items of that type have been
+     * successfully restored. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have been successfully restored.
+     */
+    public void logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) {
+        logSuccess(OperationType.RESTORE, dataType, count);
+    }
+
+    /**
+     * Report errors during a restore operation. Call this method whenever items of a certain data
+     * type failed to restore. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have failed to restore.
+     * @param error optional, the error that has caused the failure.
+     */
+    public void logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        logFailure(OperationType.RESTORE, dataType, count, error);
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being restored, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same
+     * {@code dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param metadata the metadata associated with the data type.
+     */
+    public void logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull  String metadata) {
+        logMetaData(OperationType.RESTORE, dataType, metadata);
+    }
+
+    /**
+     * Get the contents of this logger. This method should only be used by B&R code in Android
+     * Framework.
+     *
+     * @hide
+     */
+    public List<DataTypeResult> getLoggingResults() {
+        return new ArrayList<>(mResults.values());
+    }
+
+    /**
+     * Get the operation type for which this logger was created. This method should only be used
+     * by B&R code in Android Framework.
+     *
+     * @hide
+     */
+    @OperationType
+    public int getOperationType() {
+        return mOperationType;
+    }
+
+    private void logSuccess(@OperationType int operationType,
+            @BackupRestoreDataType String dataType, int count) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return;
+        }
+
+        dataTypeResult.mSuccessCount += count;
+        mResults.put(dataType, dataTypeResult);
+    }
+
+    private void logFailure(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return;
+        }
+
+        dataTypeResult.mFailCount += count;
+        if (error != null) {
+            dataTypeResult.mErrors.merge(error, count, Integer::sum);
+        }
+    }
+
+    private void logMetaData(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) {
+        if (mHashDigest == null) {
+            return;
+        }
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return;
+        }
+
+        dataTypeResult.mMetadataHash = getMetaDataHash(metaData);
+    }
+
+    /**
+     * 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 implements Parcelable {
+        @BackupRestoreDataType
+        private final String mDataType;
+        private int mSuccessCount;
+        private int mFailCount;
+        private final Map<String, Integer> mErrors = new HashMap<>();
+        private byte[] mMetadataHash;
+
+        public DataTypeResult(String dataType) {
+            mDataType = dataType;
+        }
+
+        @NonNull
+        @BackupRestoreDataType
+        public String getDataType() {
+            return mDataType;
+        }
+
+        /**
+         * @return number of items of the given data type that have been successfully backed up or
+         *         restored.
+         */
+        public int getSuccessCount() {
+            return mSuccessCount;
+        }
+
+        /**
+         * @return 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.
+         */
+        @NonNull
+        public Map<String, Integer> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * @return SHA-256 hash of the metadata or {@code null} of no metadata has been logged for
+         *         this data type.
+         */
+        @Nullable
+        public byte[] getMetadataHash() {
+            return mMetadataHash;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mDataType);
+
+            dest.writeInt(mSuccessCount);
+
+            dest.writeInt(mFailCount);
+
+            Bundle errorsBundle = new Bundle();
+            for (Map.Entry<String, Integer> e : mErrors.entrySet()) {
+                errorsBundle.putInt(e.getKey(), e.getValue());
+            }
+            dest.writeBundle(errorsBundle);
+
+            dest.writeByteArray(mMetadataHash);
+        }
+
+        public static final Parcelable.Creator<DataTypeResult> CREATOR =
+                new Parcelable.Creator<>() {
+                    public DataTypeResult createFromParcel(Parcel in) {
+                        String dataType = in.readString();
+
+                        int successCount = in.readInt();
+
+                        int failCount = in.readInt();
+
+                        Map<String, Integer> errors = new ArrayMap<>();
+                        Bundle errorsBundle = in.readBundle(getClass().getClassLoader());
+                        for (String key : errorsBundle.keySet()) {
+                            errors.put(key, errorsBundle.getInt(key));
+                        }
+
+                        byte[] metadataHash = in.createByteArray();
+
+                        DataTypeResult result = new DataTypeResult(dataType);
+                        result.mSuccessCount = successCount;
+                        result.mFailCount = failCount;
+                        result.mErrors.putAll(errors);
+                        result.mMetadataHash = metadataHash;
+                        return result;
+                    }
+
+                    public DataTypeResult[] newArray(int size) {
+                        return new DataTypeResult[size];
+                    }
+                };
+    }
+}
diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java
index f6de72b..90e9df4 100644
--- a/core/java/android/app/backup/BackupTransport.java
+++ b/core/java/android/app/backup/BackupTransport.java
@@ -656,6 +656,20 @@
     }
 
     /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * <p>Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     *
+     * @hide
+     */
+    @Nullable
+    public BackupManagerMonitor getBackupManagerMonitor() {
+        return null;
+    }
+
+    /**
      * Bridge between the actual IBackupTransport implementation and the stable API.  If the
      * binder interface needs to change, we use this layer to translate so that we can
      * (if appropriate) decouple those framework-side changes from the BackupTransport
@@ -952,5 +966,15 @@
                 callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
             }
         }
+
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture) {
+            try {
+                BackupManagerMonitor result = BackupTransport.this.getBackupManagerMonitor();
+                resultFuture.complete(new BackupManagerMonitorWrapper(result));
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
+        }
     }
 }
diff --git a/core/java/android/app/backup/RestoreSession.java b/core/java/android/app/backup/RestoreSession.java
index 9336704..fe68ec1 100644
--- a/core/java/android/app/backup/RestoreSession.java
+++ b/core/java/android/app/backup/RestoreSession.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -393,17 +392,4 @@
                     mHandler.obtainMessage(MSG_RESTORE_FINISHED, error, 0));
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
 }
diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java
index 2581daa..d628b7f 100644
--- a/core/java/android/app/prediction/AppPredictor.java
+++ b/core/java/android/app/prediction/AppPredictor.java
@@ -30,6 +30,8 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import dalvik.system.CloseGuard;
 
 import java.util.List;
@@ -79,6 +81,7 @@
     private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
 
     private final AppPredictionSessionId mSessionId;
+    @GuardedBy("itself")
     private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();
 
     /**
@@ -94,7 +97,7 @@
         IBinder b = ServiceManager.getService(Context.APP_PREDICTION_SERVICE);
         mPredictionManager = IPredictionManager.Stub.asInterface(b);
         mSessionId = new AppPredictionSessionId(
-                context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId());
+                context.getPackageName() + ":" + UUID.randomUUID(), context.getUserId());
         try {
             mPredictionManager.createPredictionSession(predictionContext, mSessionId, getToken());
         } catch (RemoteException e) {
@@ -155,6 +158,15 @@
      */
     public void registerPredictionUpdates(@NonNull @CallbackExecutor Executor callbackExecutor,
             @NonNull AppPredictor.Callback callback) {
+        synchronized (mRegisteredCallbacks) {
+            registerPredictionUpdatesLocked(callbackExecutor, callback);
+        }
+    }
+
+    @GuardedBy("mRegisteredCallbacks")
+    private void registerPredictionUpdatesLocked(
+            @NonNull @CallbackExecutor Executor callbackExecutor,
+            @NonNull AppPredictor.Callback callback) {
         if (mIsClosed.get()) {
             throw new IllegalStateException("This client has already been destroyed.");
         }
@@ -183,6 +195,13 @@
      * @param callback The callback to be unregistered.
      */
     public void unregisterPredictionUpdates(@NonNull AppPredictor.Callback callback) {
+        synchronized (mRegisteredCallbacks) {
+            unregisterPredictionUpdatesLocked(callback);
+        }
+    }
+
+    @GuardedBy("mRegisteredCallbacks")
+    private void unregisterPredictionUpdatesLocked(@NonNull AppPredictor.Callback callback) {
         if (mIsClosed.get()) {
             throw new IllegalStateException("This client has already been destroyed.");
         }
@@ -235,7 +254,7 @@
         }
 
         try {
-            mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice(targets),
+            mPredictionManager.sortAppTargets(mSessionId, new ParceledListSlice<>(targets),
                     new CallbackWrapper(callbackExecutor, callback));
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to sort targets", e);
@@ -251,19 +270,25 @@
         if (!mIsClosed.getAndSet(true)) {
             mCloseGuard.close();
 
-            // Do destroy;
-            try {
-                mPredictionManager.onDestroyPredictionSession(mSessionId);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed to notify app target event", e);
-                e.rethrowAsRuntimeException();
+            synchronized (mRegisteredCallbacks) {
+                destroySessionLocked();
             }
-            mRegisteredCallbacks.clear();
         } else {
             throw new IllegalStateException("This client has already been destroyed.");
         }
     }
 
+    @GuardedBy("mRegisteredCallbacks")
+    private void destroySessionLocked() {
+        try {
+            mPredictionManager.onDestroyPredictionSession(mSessionId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to notify app target event", e);
+            e.rethrowAsRuntimeException();
+        }
+        mRegisteredCallbacks.clear();
+    }
+
     @Override
     protected void finalize() throws Throwable {
         try {
diff --git a/core/java/android/app/search/SearchSession.java b/core/java/android/app/search/SearchSession.java
index 2cd1d96..10db337 100644
--- a/core/java/android/app/search/SearchSession.java
+++ b/core/java/android/app/search/SearchSession.java
@@ -23,9 +23,11 @@
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.util.Log;
 
 import dalvik.system.CloseGuard;
@@ -70,7 +72,7 @@
  * @hide
  */
 @SystemApi
-public final class SearchSession implements AutoCloseable{
+public final class SearchSession implements AutoCloseable {
 
     private static final String TAG = SearchSession.class.getSimpleName();
     private static final boolean DEBUG = false;
@@ -229,7 +231,14 @@
                 if (DEBUG) {
                     Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList());
                 }
-                mExecutor.execute(() -> mCallback.accept(result.getList()));
+                List<SearchTarget> list = result.getList();
+                if (list.size() > 0) {
+                    Bundle bundle = list.get(0).getExtras();
+                    if (bundle != null) {
+                        bundle.putLong("key_ipc_start", SystemClock.elapsedRealtime());
+                    }
+                }
+                mExecutor.execute(() -> mCallback.accept(list));
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java
index 30a6c31..ee14708 100644
--- a/core/java/android/app/servertransaction/ClientTransaction.java
+++ b/core/java/android/app/servertransaction/ClientTransaction.java
@@ -176,7 +176,6 @@
     /** Write to Parcel. */
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeStrongBinder(mClient.asBinder());
         final boolean writeActivityToken = mActivityToken != null;
         dest.writeBoolean(writeActivityToken);
         if (writeActivityToken) {
@@ -192,7 +191,6 @@
 
     /** Read from Parcel. */
     private ClientTransaction(Parcel in) {
-        mClient = (IApplicationThread) in.readStrongBinder();
         final boolean readActivityToken = in.readBoolean();
         if (readActivityToken) {
             mActivityToken = in.readStrongBinder();
diff --git a/core/java/android/app/time/DetectorStatusTypes.java b/core/java/android/app/time/DetectorStatusTypes.java
new file mode 100644
index 0000000..3643fc9
--- /dev/null
+++ b/core/java/android/app/time/DetectorStatusTypes.java
@@ -0,0 +1,227 @@
+/*
+ * 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.time;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A set of constants that can relate to time or time zone detector status.
+ *
+ * <ul>
+ *     <li>Detector status - the status of the overall detector.</li>
+ *     <li>Detection algorithm status - the status of an algorithm that a detector can use.
+ *     Each detector is expected to have one or more known algorithms to detect its chosen property,
+ *     e.g. for time zone devices can have a "location" detection algorithm, where the device's
+ *     location is used to detect the time zone.</li>
+ * </ul>
+ *
+ * @hide
+ */
+public final class DetectorStatusTypes {
+
+    /** A status code for a detector. */
+    @IntDef(prefix = "DETECTOR_STATUS_", value = {
+            DETECTOR_STATUS_UNKNOWN,
+            DETECTOR_STATUS_NOT_SUPPORTED,
+            DETECTOR_STATUS_NOT_RUNNING,
+            DETECTOR_STATUS_RUNNING,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DetectorStatus {}
+
+    /**
+     * The detector status is unknown. Expected only for use as a placeholder before the actual
+     * status is known.
+     */
+    public static final @DetectorStatus int DETECTOR_STATUS_UNKNOWN = 0;
+
+    /** The detector is not supported on this device. */
+    public static final @DetectorStatus int DETECTOR_STATUS_NOT_SUPPORTED = 1;
+
+    /** The detector is supported but is not running. */
+    public static final @DetectorStatus int DETECTOR_STATUS_NOT_RUNNING = 2;
+
+    /** The detector is supported and is running. */
+    public static final @DetectorStatus int DETECTOR_STATUS_RUNNING = 3;
+
+    private DetectorStatusTypes() {}
+
+    /**
+     * A status code for a detection algorithm.
+     */
+    @IntDef(prefix = "DETECTION_ALGORITHM_STATUS_", value = {
+            DETECTION_ALGORITHM_STATUS_UNKNOWN,
+            DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED,
+            DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+            DETECTION_ALGORITHM_STATUS_RUNNING,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DetectionAlgorithmStatus {}
+
+    /**
+     * The detection algorithm status is unknown. Expected only for use as a placeholder before the
+     * actual status is known.
+     */
+    public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_UNKNOWN = 0;
+
+    /** The detection algorithm is not supported on this device. */
+    public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1;
+
+    /** The detection algorithm supported but is not running. */
+    public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2;
+
+    /** The detection algorithm supported and is running. */
+    public static final @DetectionAlgorithmStatus int DETECTION_ALGORITHM_STATUS_RUNNING = 3;
+
+    /**
+     * Validates the supplied value is one of the known {@code DETECTOR_STATUS_} constants and
+     * returns it if it is valid. {@link #DETECTOR_STATUS_UNKNOWN} is considered valid.
+     *
+     * @throws IllegalArgumentException if the value is not recognized
+     */
+    public static @DetectorStatus int requireValidDetectorStatus(
+            @DetectorStatus int detectorStatus) {
+        if (detectorStatus < DETECTOR_STATUS_UNKNOWN || detectorStatus > DETECTOR_STATUS_RUNNING) {
+            throw new IllegalArgumentException("Invalid detector status: " + detectorStatus);
+        }
+        return detectorStatus;
+    }
+
+    /**
+     * Returns a string for each {@code DETECTOR_STATUS_} constant. See also
+     * {@link #detectorStatusFromString(String)}.
+     *
+     * @throws IllegalArgumentException if the value is not recognized
+     */
+    @NonNull
+    public static String detectorStatusToString(@DetectorStatus int detectorStatus) {
+        switch (detectorStatus) {
+            case DETECTOR_STATUS_UNKNOWN:
+                return "UNKNOWN";
+            case DETECTOR_STATUS_NOT_SUPPORTED:
+                return "NOT_SUPPORTED";
+            case DETECTOR_STATUS_NOT_RUNNING:
+                return "NOT_RUNNING";
+            case DETECTOR_STATUS_RUNNING:
+                return "RUNNING";
+            default:
+                throw new IllegalArgumentException("Unknown status: " + detectorStatus);
+        }
+    }
+
+    /**
+     * Returns {@code DETECTOR_STATUS_} constant value from a string. See also
+     * {@link #detectorStatusToString(int)}.
+     *
+     * @throws IllegalArgumentException if the value is not recognized or is invalid
+     */
+    public static @DetectorStatus int detectorStatusFromString(
+            @Nullable String detectorStatusString) {
+        if (TextUtils.isEmpty(detectorStatusString)) {
+            throw new IllegalArgumentException("Empty status: " + detectorStatusString);
+        }
+
+        switch (detectorStatusString) {
+            case "UNKNOWN":
+                return DETECTOR_STATUS_UNKNOWN;
+            case "NOT_SUPPORTED":
+                return DETECTOR_STATUS_NOT_SUPPORTED;
+            case "NOT_RUNNING":
+                return DETECTOR_STATUS_NOT_RUNNING;
+            case "RUNNING":
+                return DETECTOR_STATUS_RUNNING;
+            default:
+                throw new IllegalArgumentException("Unknown status: " + detectorStatusString);
+        }
+    }
+
+    /**
+     * Validates the supplied value is one of the known {@code DETECTION_ALGORITHM_} constants and
+     * returns it if it is valid. {@link #DETECTION_ALGORITHM_STATUS_UNKNOWN} is considered valid.
+     *
+     * @throws IllegalArgumentException if the value is not recognized
+     */
+    public static @DetectionAlgorithmStatus int requireValidDetectionAlgorithmStatus(
+            @DetectionAlgorithmStatus int detectionAlgorithmStatus) {
+        if (detectionAlgorithmStatus < DETECTION_ALGORITHM_STATUS_UNKNOWN
+                || detectionAlgorithmStatus > DETECTION_ALGORITHM_STATUS_RUNNING) {
+            throw new IllegalArgumentException(
+                    "Invalid detection algorithm: " + detectionAlgorithmStatus);
+        }
+        return detectionAlgorithmStatus;
+    }
+
+    /**
+     * Returns a string for each {@code DETECTION_ALGORITHM_} constant. See also
+     * {@link #detectionAlgorithmStatusFromString(String)}
+     *
+     * @throws IllegalArgumentException if the value is not recognized
+     */
+    @NonNull
+    public static String detectionAlgorithmStatusToString(
+            @DetectionAlgorithmStatus int detectorAlgorithmStatus) {
+        switch (detectorAlgorithmStatus) {
+            case DETECTION_ALGORITHM_STATUS_UNKNOWN:
+                return "UNKNOWN";
+            case DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED:
+                return "NOT_SUPPORTED";
+            case DETECTION_ALGORITHM_STATUS_NOT_RUNNING:
+                return "NOT_RUNNING";
+            case DETECTION_ALGORITHM_STATUS_RUNNING:
+                return "RUNNING";
+            default:
+                throw new IllegalArgumentException("Unknown status: " + detectorAlgorithmStatus);
+        }
+    }
+
+    /**
+     * Returns {@code DETECTION_ALGORITHM_} constant value from a string. See also
+     * {@link #detectionAlgorithmStatusToString(int)} (String)}
+     *
+     * @throws IllegalArgumentException if the value is not recognized or is invalid
+     */
+    public static @DetectionAlgorithmStatus int detectionAlgorithmStatusFromString(
+            @Nullable String detectorAlgorithmStatusString) {
+
+        if (TextUtils.isEmpty(detectorAlgorithmStatusString)) {
+            throw new IllegalArgumentException("Empty status: " + detectorAlgorithmStatusString);
+        }
+
+        switch (detectorAlgorithmStatusString) {
+            case "UNKNOWN":
+                return DETECTION_ALGORITHM_STATUS_UNKNOWN;
+            case "NOT_SUPPORTED":
+                return DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED;
+            case "NOT_RUNNING":
+                return DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+            case "RUNNING":
+                return DETECTION_ALGORITHM_STATUS_RUNNING;
+            default:
+                throw new IllegalArgumentException(
+                        "Unknown status: " + detectorAlgorithmStatusString);
+        }
+    }
+}
diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl
new file mode 100644
index 0000000..7184b12
--- /dev/null
+++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+parcelable LocationTimeZoneAlgorithmStatus;
diff --git a/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java
new file mode 100644
index 0000000..710b8c4
--- /dev/null
+++ b/core/java/android/app/time/LocationTimeZoneAlgorithmStatus.java
@@ -0,0 +1,363 @@
+/*
+ * 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.time;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN;
+import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusFromString;
+import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString;
+import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.timezone.TimeZoneProviderStatus;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Information about the status of the location-based time zone detection algorithm.
+ *
+ * @hide
+ */
+public final class LocationTimeZoneAlgorithmStatus implements Parcelable {
+
+    /**
+     * An enum that describes a location time zone provider's status.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "PROVIDER_STATUS_", value = {
+            PROVIDER_STATUS_NOT_PRESENT,
+            PROVIDER_STATUS_NOT_READY,
+            PROVIDER_STATUS_IS_CERTAIN,
+            PROVIDER_STATUS_IS_UNCERTAIN,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProviderStatus {}
+
+    /**
+     * Indicates a provider is not present because it has not been configured, the configuration
+     * is bad, or the provider has reported a permanent failure.
+     */
+    public static final @ProviderStatus int PROVIDER_STATUS_NOT_PRESENT = 1;
+
+    /**
+     * Indicates a provider has not reported it is certain or uncertain. This may be because it has
+     * just started running, or it has been stopped.
+     */
+    public static final @ProviderStatus int PROVIDER_STATUS_NOT_READY = 2;
+
+    /**
+     * Indicates a provider last reported it is certain.
+     */
+    public static final @ProviderStatus int PROVIDER_STATUS_IS_CERTAIN = 3;
+
+    /**
+     * Indicates a provider last reported it is uncertain.
+     */
+    public static final @ProviderStatus int PROVIDER_STATUS_IS_UNCERTAIN = 4;
+
+    /**
+     * An instance that provides no information about algorithm status because the algorithm has not
+     * yet reported. Effectively a "null" status placeholder.
+     */
+    @NonNull
+    public static final LocationTimeZoneAlgorithmStatus UNKNOWN =
+            new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_UNKNOWN,
+                    PROVIDER_STATUS_NOT_READY, null, PROVIDER_STATUS_NOT_READY, null);
+
+    private final @DetectionAlgorithmStatus int mStatus;
+    private final @ProviderStatus int mPrimaryProviderStatus;
+    // May be populated when mPrimaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN
+    // or PROVIDER_STATUS_IS_UNCERTAIN
+    @Nullable private final TimeZoneProviderStatus mPrimaryProviderReportedStatus;
+
+    private final @ProviderStatus int mSecondaryProviderStatus;
+    // May be populated when mSecondaryProviderReportedStatus == PROVIDER_STATUS_IS_CERTAIN
+    // or PROVIDER_STATUS_IS_UNCERTAIN
+    @Nullable private final TimeZoneProviderStatus mSecondaryProviderReportedStatus;
+
+    public LocationTimeZoneAlgorithmStatus(
+            @DetectionAlgorithmStatus int status,
+            @ProviderStatus int primaryProviderStatus,
+            @Nullable TimeZoneProviderStatus primaryProviderReportedStatus,
+            @ProviderStatus int secondaryProviderStatus,
+            @Nullable TimeZoneProviderStatus secondaryProviderReportedStatus) {
+
+        mStatus = requireValidDetectionAlgorithmStatus(status);
+        mPrimaryProviderStatus = requireValidProviderStatus(primaryProviderStatus);
+        mPrimaryProviderReportedStatus = primaryProviderReportedStatus;
+        mSecondaryProviderStatus = requireValidProviderStatus(secondaryProviderStatus);
+        mSecondaryProviderReportedStatus = secondaryProviderReportedStatus;
+
+        boolean primaryProviderHasReported = hasProviderReported(primaryProviderStatus);
+        boolean primaryProviderReportedStatusPresent = primaryProviderReportedStatus != null;
+        if (!primaryProviderHasReported && primaryProviderReportedStatusPresent) {
+            throw new IllegalArgumentException(
+                    "primaryProviderReportedStatus=" + primaryProviderReportedStatus
+                            + ", primaryProviderStatus="
+                            + providerStatusToString(primaryProviderStatus));
+        }
+
+        boolean secondaryProviderHasReported = hasProviderReported(secondaryProviderStatus);
+        boolean secondaryProviderReportedStatusPresent = secondaryProviderReportedStatus != null;
+        if (!secondaryProviderHasReported && secondaryProviderReportedStatusPresent) {
+            throw new IllegalArgumentException(
+                    "secondaryProviderReportedStatus=" + secondaryProviderReportedStatus
+                            + ", secondaryProviderStatus="
+                            + providerStatusToString(secondaryProviderStatus));
+        }
+
+        // If the algorithm isn't running, providers can't report.
+        if (status != DETECTION_ALGORITHM_STATUS_RUNNING
+                && (primaryProviderHasReported || secondaryProviderHasReported)) {
+            throw new IllegalArgumentException(
+                    "algorithmStatus=" + detectionAlgorithmStatusToString(status)
+                            + ", primaryProviderReportedStatus=" + primaryProviderReportedStatus
+                            + ", secondaryProviderReportedStatus="
+                            + secondaryProviderReportedStatus);
+        }
+    }
+
+    /**
+     * Returns the status value of the detection algorithm.
+     */
+    public @DetectionAlgorithmStatus int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Returns the status of the primary location time zone provider as categorized by the detection
+     * algorithm.
+     */
+    public @ProviderStatus int getPrimaryProviderStatus() {
+        return mPrimaryProviderStatus;
+    }
+
+    /**
+     * Returns the status of the primary location time zone provider as reported by the provider
+     * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has.
+     */
+    @Nullable
+    public TimeZoneProviderStatus getPrimaryProviderReportedStatus() {
+        return mPrimaryProviderReportedStatus;
+    }
+
+    /**
+     * Returns the status of the secondary location time zone provider as categorized by the
+     * detection algorithm.
+     */
+    public @ProviderStatus int getSecondaryProviderStatus() {
+        return mSecondaryProviderStatus;
+    }
+
+    /**
+     * Returns the status of the secondary location time zone provider as reported by the provider
+     * itself. Can be {@code null} when the provider hasn't reported, or omitted when it has.
+     */
+    @Nullable
+    public TimeZoneProviderStatus getSecondaryProviderReportedStatus() {
+        return mSecondaryProviderReportedStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "LocationTimeZoneAlgorithmStatus{"
+                + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mStatus)
+                + ", mPrimaryProviderStatus=" + providerStatusToString(mPrimaryProviderStatus)
+                + ", mPrimaryProviderReportedStatus=" + mPrimaryProviderReportedStatus
+                + ", mSecondaryProviderStatus=" + providerStatusToString(mSecondaryProviderStatus)
+                + ", mSecondaryProviderReportedStatus=" + mSecondaryProviderReportedStatus
+                + '}';
+    }
+
+    /**
+     * Parses a {@link LocationTimeZoneAlgorithmStatus} from a toString() string for manual
+     * command-line testing.
+     */
+    @NonNull
+    public static LocationTimeZoneAlgorithmStatus parseCommandlineArg(@NonNull String arg) {
+        // Note: "}" has to be escaped on Android with "\\}" because the regexp library is not based
+        // on OpenJDK code.
+        Pattern pattern = Pattern.compile("LocationTimeZoneAlgorithmStatus\\{"
+                + "mAlgorithmStatus=(.+)"
+                + ", mPrimaryProviderStatus=([^,]+)"
+                + ", mPrimaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})"
+                + ", mSecondaryProviderStatus=([^,]+)"
+                + ", mSecondaryProviderReportedStatus=(null|TimeZoneProviderStatus\\{[^}]+\\})"
+                + "\\}"
+        );
+        Matcher matcher = pattern.matcher(arg);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Unable to parse algorithm status arg: " + arg);
+        }
+        @DetectionAlgorithmStatus int algorithmStatus =
+                detectionAlgorithmStatusFromString(matcher.group(1));
+        @ProviderStatus int primaryProviderStatus = providerStatusFromString(matcher.group(2));
+        TimeZoneProviderStatus primaryProviderReportedStatus =
+                parseTimeZoneProviderStatusOrNull(matcher.group(3));
+        @ProviderStatus int secondaryProviderStatus = providerStatusFromString(matcher.group(4));
+        TimeZoneProviderStatus secondaryProviderReportedStatus =
+                parseTimeZoneProviderStatusOrNull(matcher.group(5));
+        return new LocationTimeZoneAlgorithmStatus(
+                algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus,
+                secondaryProviderStatus, secondaryProviderReportedStatus);
+    }
+
+    @Nullable
+    private static TimeZoneProviderStatus parseTimeZoneProviderStatusOrNull(
+            String providerReportedStatusString) {
+        TimeZoneProviderStatus providerReportedStatus;
+        if ("null".equals(providerReportedStatusString)) {
+            providerReportedStatus = null;
+        } else {
+            providerReportedStatus =
+                    TimeZoneProviderStatus.parseProviderStatus(providerReportedStatusString);
+        }
+        return providerReportedStatus;
+    }
+
+    @NonNull
+    public static final Creator<LocationTimeZoneAlgorithmStatus> CREATOR = new Creator<>() {
+        @Override
+        public LocationTimeZoneAlgorithmStatus createFromParcel(Parcel in) {
+            @DetectionAlgorithmStatus int algorithmStatus = in.readInt();
+            @ProviderStatus int primaryProviderStatus = in.readInt();
+            TimeZoneProviderStatus primaryProviderReportedStatus =
+                    in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class);
+            @ProviderStatus int secondaryProviderStatus = in.readInt();
+            TimeZoneProviderStatus secondaryProviderReportedStatus =
+                    in.readParcelable(getClass().getClassLoader(), TimeZoneProviderStatus.class);
+            return new LocationTimeZoneAlgorithmStatus(
+                    algorithmStatus, primaryProviderStatus, primaryProviderReportedStatus,
+                    secondaryProviderStatus, secondaryProviderReportedStatus);
+        }
+
+        @Override
+        public LocationTimeZoneAlgorithmStatus[] newArray(int size) {
+            return new LocationTimeZoneAlgorithmStatus[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mStatus);
+        parcel.writeInt(mPrimaryProviderStatus);
+        parcel.writeParcelable(mPrimaryProviderReportedStatus, flags);
+        parcel.writeInt(mSecondaryProviderStatus);
+        parcel.writeParcelable(mSecondaryProviderReportedStatus, flags);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        LocationTimeZoneAlgorithmStatus that = (LocationTimeZoneAlgorithmStatus) o;
+        return mStatus == that.mStatus
+                && mPrimaryProviderStatus == that.mPrimaryProviderStatus
+                && Objects.equals(
+                        mPrimaryProviderReportedStatus, that.mPrimaryProviderReportedStatus)
+                && mSecondaryProviderStatus == that.mSecondaryProviderStatus
+                && Objects.equals(
+                        mSecondaryProviderReportedStatus, that.mSecondaryProviderReportedStatus);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mStatus,
+                mPrimaryProviderStatus, mPrimaryProviderReportedStatus,
+                mSecondaryProviderStatus, mSecondaryProviderReportedStatus);
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    @NonNull
+    public static String providerStatusToString(@ProviderStatus int providerStatus) {
+        switch (providerStatus) {
+            case PROVIDER_STATUS_NOT_PRESENT:
+                return "NOT_PRESENT";
+            case PROVIDER_STATUS_NOT_READY:
+                return "NOT_READY";
+            case PROVIDER_STATUS_IS_CERTAIN:
+                return "IS_CERTAIN";
+            case PROVIDER_STATUS_IS_UNCERTAIN:
+                return "IS_UNCERTAIN";
+            default:
+                throw new IllegalArgumentException("Unknown status: " + providerStatus);
+        }
+    }
+
+    /** @hide */
+    @VisibleForTesting public static @ProviderStatus int providerStatusFromString(
+            @Nullable String providerStatusString) {
+        if (TextUtils.isEmpty(providerStatusString)) {
+            throw new IllegalArgumentException("Empty status: " + providerStatusString);
+        }
+
+        switch (providerStatusString) {
+            case "NOT_PRESENT":
+                return PROVIDER_STATUS_NOT_PRESENT;
+            case "NOT_READY":
+                return PROVIDER_STATUS_NOT_READY;
+            case "IS_CERTAIN":
+                return PROVIDER_STATUS_IS_CERTAIN;
+            case "IS_UNCERTAIN":
+                return PROVIDER_STATUS_IS_UNCERTAIN;
+            default:
+                throw new IllegalArgumentException("Unknown status: " + providerStatusString);
+        }
+    }
+
+    private static boolean hasProviderReported(@ProviderStatus int providerStatus) {
+        return providerStatus == PROVIDER_STATUS_IS_CERTAIN
+                || providerStatus == PROVIDER_STATUS_IS_UNCERTAIN;
+    }
+
+    /** @hide */
+    @VisibleForTesting public static @ProviderStatus int requireValidProviderStatus(
+            @ProviderStatus int providerStatus) {
+        if (providerStatus < PROVIDER_STATUS_NOT_PRESENT
+                || providerStatus > PROVIDER_STATUS_IS_UNCERTAIN) {
+            throw new IllegalArgumentException(
+                    "Invalid provider status: " + providerStatus);
+        }
+        return providerStatus;
+    }
+}
diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl
new file mode 100644
index 0000000..0eb5b63
--- /dev/null
+++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+parcelable TelephonyTimeZoneAlgorithmStatus;
diff --git a/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java
new file mode 100644
index 0000000..95240c0
--- /dev/null
+++ b/core/java/android/app/time/TelephonyTimeZoneAlgorithmStatus.java
@@ -0,0 +1,96 @@
+/*
+ * 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.time;
+
+import static android.app.time.DetectorStatusTypes.detectionAlgorithmStatusToString;
+import static android.app.time.DetectorStatusTypes.requireValidDetectionAlgorithmStatus;
+
+import android.annotation.NonNull;
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Information about the status of the telephony-based time zone detection algorithm.
+ *
+ * @hide
+ */
+public final class TelephonyTimeZoneAlgorithmStatus implements Parcelable {
+
+    private final @DetectionAlgorithmStatus int mAlgorithmStatus;
+
+    public TelephonyTimeZoneAlgorithmStatus(@DetectionAlgorithmStatus int algorithmStatus) {
+        mAlgorithmStatus = requireValidDetectionAlgorithmStatus(algorithmStatus);
+    }
+
+    /**
+     * Returns the status of the detection algorithm.
+     */
+    public @DetectionAlgorithmStatus int getAlgorithmStatus() {
+        return mAlgorithmStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "TelephonyTimeZoneAlgorithmStatus{"
+                + "mAlgorithmStatus=" + detectionAlgorithmStatusToString(mAlgorithmStatus)
+                + '}';
+    }
+
+    @NonNull
+    public static final Creator<TelephonyTimeZoneAlgorithmStatus> CREATOR = new Creator<>() {
+        @Override
+        public TelephonyTimeZoneAlgorithmStatus createFromParcel(Parcel in) {
+            @DetectionAlgorithmStatus int algorithmStatus = in.readInt();
+            return new TelephonyTimeZoneAlgorithmStatus(algorithmStatus);
+        }
+
+        @Override
+        public TelephonyTimeZoneAlgorithmStatus[] newArray(int size) {
+            return new TelephonyTimeZoneAlgorithmStatus[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mAlgorithmStatus);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TelephonyTimeZoneAlgorithmStatus that = (TelephonyTimeZoneAlgorithmStatus) o;
+        return mAlgorithmStatus == that.mAlgorithmStatus;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAlgorithmStatus);
+    }
+}
diff --git a/core/java/android/app/time/TimeCapabilities.java b/core/java/android/app/time/TimeCapabilities.java
index 76bad58..752caac 100644
--- a/core/java/android/app/time/TimeCapabilities.java
+++ b/core/java/android/app/time/TimeCapabilities.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.time.Capabilities.CapabilityState;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -37,6 +38,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilities implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilities> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeCapabilitiesAndConfig.java b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
index b6a0818..c9a45e0 100644
--- a/core/java/android/app/time/TimeCapabilitiesAndConfig.java
+++ b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
@@ -17,6 +17,7 @@
 package android.app.time;
 
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -27,6 +28,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilitiesAndConfig implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilitiesAndConfig> CREATOR =
diff --git a/core/java/android/app/time/TimeConfiguration.java b/core/java/android/app/time/TimeConfiguration.java
index 7d98698..048f85a 100644
--- a/core/java/android/app/time/TimeConfiguration.java
+++ b/core/java/android/app/time/TimeConfiguration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.StringDef;
+import android.annotation.SystemApi;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -40,6 +41,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeConfiguration implements Parcelable {
 
     public static final @NonNull Creator<TimeConfiguration> CREATOR =
@@ -155,6 +157,7 @@
      *
      * @hide
      */
+    @SystemApi
     public static final class Builder {
 
         private final Bundle mBundle = new Bundle();
diff --git a/core/java/android/app/time/TimeManager.java b/core/java/android/app/time/TimeManager.java
index 9f66f09..e35e359 100644
--- a/core/java/android/app/time/TimeManager.java
+++ b/core/java/android/app/time/TimeManager.java
@@ -88,8 +88,6 @@
 
     /**
      * Returns the calling user's time capabilities and configuration.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -107,10 +105,26 @@
     /**
      * Modifies the time detection configuration.
      *
-     * @return {@code true} if all the configuration settings specified have been set to the
-     * new values, {@code false} if none have
+     * <p>The ability to modify configuration settings can be subject to restrictions. For
+     * example, they may be determined by device hardware, general policy (i.e. only the primary
+     * user can set them), or by a managed device policy. Use {@link
+     * #getTimeCapabilitiesAndConfig()} to obtain information at runtime about the user's
+     * capabilities.
      *
-     * @hide
+     * <p>Attempts to modify configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_SUPPORTED} or {@link
+     * Capabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false}
+     * will be returned. Modifying configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_APPLICABLE} or {@link
+     * Capabilities#CAPABILITY_POSSESSED} will succeed. See {@link
+     * TimeZoneCapabilities} for further details.
+     *
+     * <p>If the supplied configuration only has some values set, then only the specified settings
+     * will be updated (where the user's capabilities allow) and other settings will be left
+     * unchanged.
+     *
+     * @return {@code true} if all the configuration settings specified have been set to the
+     *   new values, {@code false} if none have
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean updateTimeConfiguration(@NonNull TimeConfiguration configuration) {
@@ -280,8 +294,6 @@
     /**
      * Returns a snapshot of the device's current system clock time state. See also {@link
      * #confirmTime(UnixEpochTime)} for how this information can be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -306,8 +318,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time being
      * confirmed is no longer the time the device is currently set to. Confirming a time
      * in which the system already has high confidence will return {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -329,8 +339,6 @@
      * capabilities prevents the time being accepted, e.g. if the device is currently set to
      * "automatic time detection". This method returns {@code true} if the time was accepted even
      * if it is the same as the current device time.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -353,8 +361,6 @@
      * Returns a snapshot of the device's current time zone state. See also {@link
      * #confirmTimeZone(String)} and {@link #setManualTimeZone(String)} for how this information may
      * be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -379,8 +385,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time zone ID being
      * confirmed is no longer the time zone ID the device is currently set to. Confirming a time
      * zone ID in which the system already has high confidence returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTimeZone(@NonNull String timeZoneId) {
@@ -402,8 +406,6 @@
      * capabilities prevents the time zone being accepted, e.g. if the device is currently set to
      * "automatic time zone detection". {@code true} is returned if the time zone is accepted. A
      * time zone that is accepted and matches the current device time zone returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTimeZone(@NonNull String timeZoneId) {
diff --git a/core/java/android/app/time/TimeState.java b/core/java/android/app/time/TimeState.java
index 01c869d..c209cde 100644
--- a/core/java/android/app/time/TimeState.java
+++ b/core/java/android/app/time/TimeState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeState implements Parcelable {
 
     public static final @NonNull Creator<TimeState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java
index 2f147ce..b647fc3 100644
--- a/core/java/android/app/time/TimeZoneCapabilities.java
+++ b/core/java/android/app/time/TimeZoneCapabilities.java
@@ -114,8 +114,6 @@
      * <p>The time zone will be ignored in all cases unless the value is {@link
      * Capabilities#CAPABILITY_POSSESSED}. See also
      * {@link TimeZoneConfiguration#isAutoDetectionEnabled()}.
-     *
-     * @hide
      */
     @CapabilityState
     public int getSetManualTimeZoneCapability() {
diff --git a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java
index cd91b04..4684c6a 100644
--- a/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java
+++ b/core/java/android/app/time/TimeZoneCapabilitiesAndConfig.java
@@ -23,27 +23,40 @@
 import android.os.Parcelable;
 
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
- * A pair containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}.
+ * An object containing a user's {@link TimeZoneCapabilities} and {@link TimeZoneConfiguration}.
  *
  * @hide
  */
 @SystemApi
 public final class TimeZoneCapabilitiesAndConfig implements Parcelable {
 
-    public static final @NonNull Creator<TimeZoneCapabilitiesAndConfig> CREATOR =
-            new Creator<TimeZoneCapabilitiesAndConfig>() {
-                public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) {
-                    return TimeZoneCapabilitiesAndConfig.createFromParcel(in);
-                }
+    public static final @NonNull Creator<TimeZoneCapabilitiesAndConfig> CREATOR = new Creator<>() {
+        public TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) {
+            return TimeZoneCapabilitiesAndConfig.createFromParcel(in);
+        }
 
-                public TimeZoneCapabilitiesAndConfig[] newArray(int size) {
-                    return new TimeZoneCapabilitiesAndConfig[size];
-                }
-            };
+        public TimeZoneCapabilitiesAndConfig[] newArray(int size) {
+            return new TimeZoneCapabilitiesAndConfig[size];
+        }
+    };
 
-
+    /**
+     * The time zone detector status.
+     *
+     * Implementation note for future platform engineers: This field is only needed by SettingsUI
+     * initially and so it has not been added to the SDK API. {@link TimeZoneDetectorStatus}
+     * contains details about the internals of the time zone detector so thought should be given to
+     * abstraction / exposing a lightweight version if something unbundled needs access to detector
+     * details. Also, that could be good time to add separate APIs for bundled components, or add
+     * new APIs that return something more extensible and generic like a Bundle or a less
+     * constraining name. See also {@link
+     * TimeManager#addTimeZoneDetectorListener(Executor, TimeManager.TimeZoneDetectorListener)},
+     * which notified of changes to any fields in this class, including the detector status.
+     */
+    @NonNull private final TimeZoneDetectorStatus mDetectorStatus;
     @NonNull private final TimeZoneCapabilities mCapabilities;
     @NonNull private final TimeZoneConfiguration mConfiguration;
 
@@ -53,26 +66,41 @@
      * @hide
      */
     public TimeZoneCapabilitiesAndConfig(
+            @NonNull TimeZoneDetectorStatus detectorStatus,
             @NonNull TimeZoneCapabilities capabilities,
             @NonNull TimeZoneConfiguration configuration) {
-        this.mCapabilities = Objects.requireNonNull(capabilities);
-        this.mConfiguration = Objects.requireNonNull(configuration);
+        mDetectorStatus = Objects.requireNonNull(detectorStatus);
+        mCapabilities = Objects.requireNonNull(capabilities);
+        mConfiguration = Objects.requireNonNull(configuration);
     }
 
     @NonNull
     private static TimeZoneCapabilitiesAndConfig createFromParcel(Parcel in) {
-        TimeZoneCapabilities capabilities = in.readParcelable(null, android.app.time.TimeZoneCapabilities.class);
-        TimeZoneConfiguration configuration = in.readParcelable(null, android.app.time.TimeZoneConfiguration.class);
-        return new TimeZoneCapabilitiesAndConfig(capabilities, configuration);
+        TimeZoneDetectorStatus detectorStatus =
+                in.readParcelable(null, TimeZoneDetectorStatus.class);
+        TimeZoneCapabilities capabilities = in.readParcelable(null, TimeZoneCapabilities.class);
+        TimeZoneConfiguration configuration = in.readParcelable(null, TimeZoneConfiguration.class);
+        return new TimeZoneCapabilitiesAndConfig(detectorStatus, capabilities, configuration);
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mDetectorStatus, flags);
         dest.writeParcelable(mCapabilities, flags);
         dest.writeParcelable(mConfiguration, flags);
     }
 
     /**
+     * Returns the time zone detector's status.
+     *
+     * @hide
+     */
+    @NonNull
+    public TimeZoneDetectorStatus getDetectorStatus() {
+        return mDetectorStatus;
+    }
+
+    /**
      * Returns the user's time zone behavior capabilities.
      */
     @NonNull
@@ -102,7 +130,8 @@
             return false;
         }
         TimeZoneCapabilitiesAndConfig that = (TimeZoneCapabilitiesAndConfig) o;
-        return mCapabilities.equals(that.mCapabilities)
+        return mDetectorStatus.equals(that.mDetectorStatus)
+                && mCapabilities.equals(that.mCapabilities)
                 && mConfiguration.equals(that.mConfiguration);
     }
 
@@ -114,7 +143,8 @@
     @Override
     public String toString() {
         return "TimeZoneCapabilitiesAndConfig{"
-                + "mCapabilities=" + mCapabilities
+                + "mDetectorStatus=" + mDetectorStatus
+                + ", mCapabilities=" + mCapabilities
                 + ", mConfiguration=" + mConfiguration
                 + '}';
     }
diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.aidl b/core/java/android/app/time/TimeZoneDetectorStatus.aidl
new file mode 100644
index 0000000..32204df
--- /dev/null
+++ b/core/java/android/app/time/TimeZoneDetectorStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+parcelable TimeZoneDetectorStatus;
diff --git a/core/java/android/app/time/TimeZoneDetectorStatus.java b/core/java/android/app/time/TimeZoneDetectorStatus.java
new file mode 100644
index 0000000..1637463
--- /dev/null
+++ b/core/java/android/app/time/TimeZoneDetectorStatus.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 android.app.time;
+
+import static android.app.time.DetectorStatusTypes.DetectorStatus;
+import static android.app.time.DetectorStatusTypes.requireValidDetectorStatus;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Information about the status of the automatic time zone detector. Used by SettingsUI to display
+ * status information to the user.
+ *
+ * @hide
+ */
+public final class TimeZoneDetectorStatus implements Parcelable {
+
+    private final @DetectorStatus int mDetectorStatus;
+    @NonNull private final TelephonyTimeZoneAlgorithmStatus mTelephonyTimeZoneAlgorithmStatus;
+    @NonNull private final LocationTimeZoneAlgorithmStatus mLocationTimeZoneAlgorithmStatus;
+
+    public TimeZoneDetectorStatus(
+            @DetectorStatus int detectorStatus,
+            @NonNull TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus,
+            @NonNull LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus) {
+        mDetectorStatus = requireValidDetectorStatus(detectorStatus);
+        mTelephonyTimeZoneAlgorithmStatus =
+                Objects.requireNonNull(telephonyTimeZoneAlgorithmStatus);
+        mLocationTimeZoneAlgorithmStatus = Objects.requireNonNull(locationTimeZoneAlgorithmStatus);
+    }
+
+    public @DetectorStatus int getDetectorStatus() {
+        return mDetectorStatus;
+    }
+
+    @NonNull
+    public TelephonyTimeZoneAlgorithmStatus getTelephonyTimeZoneAlgorithmStatus() {
+        return mTelephonyTimeZoneAlgorithmStatus;
+    }
+
+    @NonNull
+    public LocationTimeZoneAlgorithmStatus getLocationTimeZoneAlgorithmStatus() {
+        return mLocationTimeZoneAlgorithmStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "TimeZoneDetectorStatus{"
+                + "mDetectorStatus=" + DetectorStatusTypes.detectorStatusToString(mDetectorStatus)
+                + ", mTelephonyTimeZoneAlgorithmStatus=" + mTelephonyTimeZoneAlgorithmStatus
+                + ", mLocationTimeZoneAlgorithmStatus=" + mLocationTimeZoneAlgorithmStatus
+                + '}';
+    }
+
+    public static final @NonNull Creator<TimeZoneDetectorStatus> CREATOR = new Creator<>() {
+        @Override
+        public TimeZoneDetectorStatus createFromParcel(Parcel in) {
+            @DetectorStatus int detectorStatus = in.readInt();
+            TelephonyTimeZoneAlgorithmStatus telephonyTimeZoneAlgorithmStatus =
+                    in.readParcelable(getClass().getClassLoader(),
+                            TelephonyTimeZoneAlgorithmStatus.class);
+            LocationTimeZoneAlgorithmStatus locationTimeZoneAlgorithmStatus =
+                    in.readParcelable(getClass().getClassLoader(),
+                            LocationTimeZoneAlgorithmStatus.class);
+            return new TimeZoneDetectorStatus(detectorStatus,
+                    telephonyTimeZoneAlgorithmStatus, locationTimeZoneAlgorithmStatus);
+        }
+
+        @Override
+        public TimeZoneDetectorStatus[] newArray(int size) {
+            return new TimeZoneDetectorStatus[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mDetectorStatus);
+        parcel.writeParcelable(mTelephonyTimeZoneAlgorithmStatus, flags);
+        parcel.writeParcelable(mLocationTimeZoneAlgorithmStatus, flags);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TimeZoneDetectorStatus that = (TimeZoneDetectorStatus) o;
+        return mDetectorStatus == that.mDetectorStatus
+                && mTelephonyTimeZoneAlgorithmStatus.equals(that.mTelephonyTimeZoneAlgorithmStatus)
+                && mLocationTimeZoneAlgorithmStatus.equals(that.mLocationTimeZoneAlgorithmStatus);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDetectorStatus, mTelephonyTimeZoneAlgorithmStatus,
+                mLocationTimeZoneAlgorithmStatus);
+    }
+}
diff --git a/core/java/android/app/time/TimeZoneState.java b/core/java/android/app/time/TimeZoneState.java
index 8e87111..beb6dc6 100644
--- a/core/java/android/app/time/TimeZoneState.java
+++ b/core/java/android/app/time/TimeZoneState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeZoneState implements Parcelable {
 
     public static final @NonNull Creator<TimeZoneState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java
index 576bf64..3a35f3c 100644
--- a/core/java/android/app/time/UnixEpochTime.java
+++ b/core/java/android/app/time/UnixEpochTime.java
@@ -19,6 +19,7 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -38,6 +39,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class UnixEpochTime implements Parcelable {
     @ElapsedRealtimeLong private final long mElapsedRealtimeMillis;
     private final long mUnixEpochTimeMillis;
@@ -153,9 +155,8 @@
      * Creates a new Unix epoch time value at {@code elapsedRealtimeTimeMillis} by adjusting this
      * Unix epoch time by the difference between the elapsed realtime value supplied and the one
      * associated with this instance.
-     *
-     * @hide
      */
+    @NonNull
     public UnixEpochTime at(@ElapsedRealtimeLong long elapsedRealtimeTimeMillis) {
         long adjustedUnixEpochTimeMillis =
                 (elapsedRealtimeTimeMillis - mElapsedRealtimeMillis) + mUnixEpochTimeMillis;
diff --git a/core/java/android/app/timezonedetector/TimeZoneDetector.java b/core/java/android/app/timezonedetector/TimeZoneDetector.java
index 0e9e28b..f357fb2 100644
--- a/core/java/android/app/timezonedetector/TimeZoneDetector.java
+++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java
@@ -81,11 +81,11 @@
     String SHELL_COMMAND_SET_GEO_DETECTION_ENABLED = "set_geo_detection_enabled";
 
     /**
-     * A shell command that injects a geolocation time zone suggestion (as if from the
+     * A shell command that injects a location algorithm event (as if from the
      * location_time_zone_manager).
      * @hide
      */
-    String SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE = "suggest_geo_location_time_zone";
+    String SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT = "handle_location_algorithm_event";
 
     /**
      * A shell command that injects a manual time zone suggestion (as if from the SettingsUI or
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index cc303fb..2dced96 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -329,6 +329,22 @@
     }
 
     /**
+     * Set the visibiity of all widgets associated with this host to hidden
+     *
+     * @hide
+     */
+    public void setAppWidgetHidden() {
+        if (sService == null) {
+            return;
+        }
+        try {
+            sService.setAppWidgetHidden(mContextOpPackageName, mHostId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("System server dead?", e);
+        }
+    }
+
+    /**
      * Set the host's interaction handler.
      *
      * @hide
@@ -418,14 +434,7 @@
         AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
         view.setInteractionHandler(mInteractionHandler);
         view.setAppWidget(appWidgetId, appWidget);
-        addListener(appWidgetId, view);
-        RemoteViews views;
-        try {
-            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
-        } catch (RemoteException e) {
-            throw new RuntimeException("system server dead?", e);
-        }
-        view.updateAppWidget(views);
+        setListener(appWidgetId, view);
 
         return view;
     }
@@ -513,13 +522,19 @@
      * The AppWidgetHost retains a pointer to the newly-created listener.
      * @param appWidgetId The ID of the app widget for which to add the listener
      * @param listener The listener interface that deals with actions towards the widget view
-     *
      * @hide
      */
-    public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
+    public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
         synchronized (mListeners) {
             mListeners.put(appWidgetId, listener);
         }
+        RemoteViews views = null;
+        try {
+            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("system server dead?", e);
+        }
+        listener.updateAppWidget(views);
     }
 
     /**
diff --git a/core/java/android/companion/AssociatedDevice.java b/core/java/android/companion/AssociatedDevice.java
index 3758cdb..a833661 100644
--- a/core/java/android/companion/AssociatedDevice.java
+++ b/core/java/android/companion/AssociatedDevice.java
@@ -16,6 +16,7 @@
 
 package android.companion;
 
+import android.bluetooth.BluetoothDevice;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -23,19 +24,14 @@
 import androidx.annotation.Nullable;
 
 /**
- * Loose wrapper around device parcelable. Device can be one of three types:
+ * Container for device info from an association that is not self-managed.
+ * Device can be one of three types:
  *
  * <ul>
  *     <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li>
  *     <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li>
  *     <li>for WiFi - {@link android.net.wifi.ScanResult}</li>
  * </ul>
- *
- * This class serves as temporary wrapper to deliver a loosely-typed parcelable object from
- * {@link com.android.companiondevicemanager.CompanionDeviceActivity} to the Companion app,
- * and should only be used internally.
- *
- * @hide
  */
 public final class AssociatedDevice implements Parcelable {
     private static final int CLASSIC_BLUETOOTH = 0;
@@ -44,6 +40,7 @@
 
     @NonNull private final Parcelable mDevice;
 
+    /** @hide */
     public AssociatedDevice(@NonNull Parcelable device) {
         mDevice = device;
     }
@@ -54,11 +51,39 @@
     }
 
     /**
-     * Return device info. Cast to expected device type.
+     * Return bluetooth device info. Null if associated device is not a bluetooth device.
+     * @return Remote bluetooth device details containing MAC address.
      */
-    @NonNull
-    public Parcelable getDevice() {
-        return mDevice;
+    @Nullable
+    public BluetoothDevice getBluetoothDevice() {
+        if (mDevice instanceof BluetoothDevice) {
+            return (BluetoothDevice) mDevice;
+        }
+        return null;
+    }
+
+    /**
+     * Return bluetooth LE device info. Null if associated device is not a BLE device.
+     * @return BLE scan result containing details of detected BLE device.
+     */
+    @Nullable
+    public android.bluetooth.le.ScanResult getBleDevice() {
+        if (mDevice instanceof android.bluetooth.le.ScanResult) {
+            return (android.bluetooth.le.ScanResult) mDevice;
+        }
+        return null;
+    }
+
+    /**
+     * Return Wi-Fi device info. Null if associated device is not a Wi-Fi device.
+     * @return Wi-Fi scan result containing details of detected access point.
+     */
+    @Nullable
+    public android.net.wifi.ScanResult getWifiDevice() {
+        if (mDevice instanceof android.net.wifi.ScanResult) {
+            return (android.net.wifi.ScanResult) mDevice;
+        }
+        return null;
     }
 
     @Override
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index 93964b3..5fd39fe 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -164,20 +164,19 @@
 
     /**
      * Companion device that was associated. Note that this field is not persisted across sessions.
-     *
-     * Cast to expected device type before use:
+     * Device can be one of the following types:
      *
      * <ul>
-     *     <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li>
-     *     <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li>
-     *     <li>for WiFi - {@link android.net.wifi.ScanResult}</li>
+     *     <li>for classic Bluetooth - {@link AssociatedDevice#getBluetoothDevice()}</li>
+     *     <li>for Bluetooth LE - {@link AssociatedDevice#getBleDevice()}</li>
+     *     <li>for WiFi - {@link AssociatedDevice#getWifiDevice()}</li>
      * </ul>
      *
      * @return the companion device that was associated, or {@code null} if the device is
-     *         self-managed.
+     *         self-managed or this association info was retrieved from persistent storage.
      */
-    public @Nullable Parcelable getAssociatedDevice() {
-        return mAssociatedDevice == null ? null : mAssociatedDevice.getDevice();
+    public @Nullable AssociatedDevice getAssociatedDevice() {
+        return mAssociatedDevice;
     }
 
     /**
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index e7f19166..295d69d 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -88,6 +88,7 @@
             IBinder token,
             in Point screenSize);
     void unregisterInputDevice(IBinder token);
+    int getInputDeviceId(IBinder token);
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index 82d7534..7d6336a 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -52,6 +52,11 @@
     List<VirtualDevice> getVirtualDevices();
 
     /**
+     * Returns the device policy for the given virtual device and policy type.
+     */
+    int getDevicePolicy(int deviceId, int policyType);
+
+    /**
      * Creates a virtual display owned by a particular virtual device.
      *
      * @param virtualDisplayConfig The configuration used in creating the display
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 0bb86fb..c14bb1b 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -182,6 +182,28 @@
     }
 
     /**
+     * Returns the device policy for the given virtual device and policy type.
+     *
+     * <p>In case the virtual device identifier is not valid, or there's no explicitly specified
+     * policy for that device and policy type, then
+     * {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT} is returned.
+     *
+     * @hide
+     */
+    public @VirtualDeviceParams.DevicePolicy int getDevicePolicy(
+            int deviceId, @VirtualDeviceParams.PolicyType int policyType) {
+        if (mService == null) {
+            Log.w(TAG, "Failed to retrieve device policy; no virtual device manager service.");
+            return VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+        }
+        try {
+            return mService.getDevicePolicy(deviceId, policyType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * A virtual device has its own virtual display, audio output, microphone, and camera etc. The
      * creator of a virtual device can take the output from the virtual display and stream it over
      * to another device, and inject input events that are received from the remote device.
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index d40c9d6..c6e6f83 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -28,6 +28,7 @@
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.util.ArraySet;
+import android.util.SparseIntArray;
 
 import com.android.internal.util.Preconditions;
 
@@ -103,6 +104,47 @@
      */
     public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1;
 
+    /** @hide */
+    @IntDef(prefix = "DEVICE_POLICY_",  value = {DEVICE_POLICY_DEFAULT, DEVICE_POLICY_CUSTOM})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    public @interface DevicePolicy {}
+
+    /**
+     * Indicates that there is no special logic for this virtual device and it should be treated
+     * the same way as the default device, keeping the default behavior unchanged.
+     */
+    public static final int DEVICE_POLICY_DEFAULT = 0;
+
+    /**
+     * Indicates that there is custom logic, specific to this virtual device, which should be
+     * triggered instead of the default behavior.
+     */
+    public static final int DEVICE_POLICY_CUSTOM = 1;
+
+    /**
+     * Any relevant component must be able to interpret the correct meaning of a custom policy for
+     * a given policy type.
+     * @hide
+     */
+    @IntDef(prefix = "POLICY_TYPE_",  value = {POLICY_TYPE_SENSORS})
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+    public @interface PolicyType {}
+
+    /**
+     * Tells the sensor framework how to handle sensor requests from contexts associated with this
+     * virtual device, namely the sensors returned by
+     * {@link android.hardware.SensorManager#getSensorList}:
+     *
+     * <ul>
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Return the sensors of the default device.
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Return the sensors of the virtual device. Note that if
+     *     the virtual device did not create any virtual sensors, then an empty list is returned.
+     * </ul>
+     */
+    public static final int POLICY_TYPE_SENSORS = 0;
+
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
     @NonNull private final ArraySet<ComponentName> mAllowedCrossTaskNavigations;
@@ -114,6 +156,8 @@
     @ActivityPolicy
     private final int mDefaultActivityPolicy;
     @Nullable private final String mName;
+    // Mapping of @PolicyType to @DevicePolicy
+    @NonNull private final SparseIntArray mDevicePolicies;
 
     private VirtualDeviceParams(
             @LockState int lockState,
@@ -124,12 +168,14 @@
             @NonNull Set<ComponentName> allowedActivities,
             @NonNull Set<ComponentName> blockedActivities,
             @ActivityPolicy int defaultActivityPolicy,
-            @Nullable String name) {
+            @Nullable String name,
+            @NonNull SparseIntArray devicePolicies) {
         Preconditions.checkNotNull(usersWithMatchingAccounts);
         Preconditions.checkNotNull(allowedCrossTaskNavigations);
         Preconditions.checkNotNull(blockedCrossTaskNavigations);
         Preconditions.checkNotNull(allowedActivities);
         Preconditions.checkNotNull(blockedActivities);
+        Preconditions.checkNotNull(devicePolicies);
 
         mLockState = lockState;
         mUsersWithMatchingAccounts = new ArraySet<>(usersWithMatchingAccounts);
@@ -140,6 +186,7 @@
         mBlockedActivities = new ArraySet<>(blockedActivities);
         mDefaultActivityPolicy = defaultActivityPolicy;
         mName = name;
+        mDevicePolicies = devicePolicies;
     }
 
     @SuppressWarnings("unchecked")
@@ -153,6 +200,7 @@
         mBlockedActivities = (ArraySet<ComponentName>) parcel.readArraySet(null);
         mDefaultActivityPolicy = parcel.readInt();
         mName = parcel.readString8();
+        mDevicePolicies = parcel.readSparseIntArray();
     }
 
     /**
@@ -258,6 +306,16 @@
         return mName;
     }
 
+    /**
+     * Returns the policy specified for this policy type, or {@link #DEVICE_POLICY_DEFAULT} if no
+     * policy for this type has been explicitly specified.
+     *
+     * @see Builder#addDevicePolicy
+     */
+    public @DevicePolicy int getDevicePolicy(@PolicyType int policyType) {
+        return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT);
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -274,6 +332,7 @@
         dest.writeArraySet(mBlockedActivities);
         dest.writeInt(mDefaultActivityPolicy);
         dest.writeString8(mName);
+        dest.writeSparseIntArray(mDevicePolicies);
     }
 
     @Override
@@ -285,6 +344,18 @@
             return false;
         }
         VirtualDeviceParams that = (VirtualDeviceParams) o;
+        final int devicePoliciesCount = mDevicePolicies.size();
+        if (devicePoliciesCount != that.mDevicePolicies.size()) {
+            return false;
+        }
+        for (int i = 0; i < devicePoliciesCount; i++) {
+            if (mDevicePolicies.keyAt(i) != that.mDevicePolicies.keyAt(i)) {
+                return false;
+            }
+            if (mDevicePolicies.valueAt(i) != that.mDevicePolicies.valueAt(i)) {
+                return false;
+            }
+        }
         return mLockState == that.mLockState
                 && mUsersWithMatchingAccounts.equals(that.mUsersWithMatchingAccounts)
                 && Objects.equals(mAllowedCrossTaskNavigations, that.mAllowedCrossTaskNavigations)
@@ -298,10 +369,15 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(
+        int hashCode = Objects.hash(
                 mLockState, mUsersWithMatchingAccounts, mAllowedCrossTaskNavigations,
                 mBlockedCrossTaskNavigations, mDefaultNavigationPolicy,  mAllowedActivities,
-                mBlockedActivities, mDefaultActivityPolicy, mName);
+                mBlockedActivities, mDefaultActivityPolicy, mName, mDevicePolicies);
+        for (int i = 0; i < mDevicePolicies.size(); i++) {
+            hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
+            hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
+        }
+        return hashCode;
     }
 
     @Override
@@ -317,6 +393,7 @@
                 + " mBlockedActivities=" + mBlockedActivities
                 + " mDefaultActivityPolicy=" + mDefaultActivityPolicy
                 + " mName=" + mName
+                + " mDevicePolicies=" + mDevicePolicies
                 + ")";
     }
 
@@ -350,6 +427,7 @@
         private int mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED;
         private boolean mDefaultActivityPolicyConfigured = false;
         @Nullable private String mName;
+        @NonNull private SparseIntArray mDevicePolicies = new SparseIntArray();
 
         /**
          * Sets the lock state of the device. The permission {@code ADD_ALWAYS_UNLOCKED_DISPLAY}
@@ -528,6 +606,18 @@
         }
 
         /**
+         * Specifies a policy for this virtual device.
+         *
+         * @param policyType the type of policy, i.e. which behavior to specify a policy for.
+         * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior.
+         */
+        @NonNull
+        public Builder addDevicePolicy(@PolicyType int policyType, @DevicePolicy int devicePolicy) {
+            mDevicePolicies.put(policyType, devicePolicy);
+            return this;
+        }
+
+        /**
          * Builds the {@link VirtualDeviceParams} instance.
          */
         @NonNull
@@ -541,7 +631,8 @@
                     mAllowedActivities,
                     mBlockedActivities,
                     mDefaultActivityPolicy,
-                    mName);
+                    mName,
+                    mDevicePolicies);
         }
     }
 }
diff --git a/core/java/android/content/ActivityNotFoundException.java b/core/java/android/content/ActivityNotFoundException.java
index 16149bb..5b50189 100644
--- a/core/java/android/content/ActivityNotFoundException.java
+++ b/core/java/android/content/ActivityNotFoundException.java
@@ -31,5 +31,4 @@
     {
         super(name);
     }
-};
-
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index b0c6cbc..e981581 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -29,6 +29,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.Process;
+import android.os.UserHandle;
 import android.permission.PermissionManager;
 import android.util.ArraySet;
 
@@ -297,7 +298,7 @@
     public boolean checkCallingUid() {
         final int callingUid = Binder.getCallingUid();
         if (callingUid != Process.ROOT_UID
-                && callingUid != Process.SYSTEM_UID
+                && UserHandle.getAppId(callingUid) != Process.SYSTEM_UID
                 && callingUid != mAttributionSourceState.uid) {
             return false;
         }
diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java
index bf46611..de2ba44 100644
--- a/core/java/android/content/ClipDescription.java
+++ b/core/java/android/content/ClipDescription.java
@@ -139,21 +139,28 @@
      * password or credit card number.
      * <p>
      * Type: boolean
-     * </p>
      * <p>
      * This extra can be used to indicate that a ClipData contains sensitive information that
      * should be redacted or hidden from view until a user takes explicit action to reveal it
      * (e.g., by pasting).
-     * </p>
      * <p>
      * Adding this extra does not change clipboard behavior or add additional security to
      * the ClipData. Its purpose is essentially a rendering hint from the source application,
      * asking that the data within be obfuscated or redacted, unless the user has taken action
      * to make it visible.
-     * </p>
      */
     public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE";
 
+    /** Indicates that a ClipData's source is a remote device.
+     * <p>
+     *     Type: boolean
+     * <p>
+     *     This extra can be used to indicate that a ClipData comes from a separate device rather
+     *     than being local. It is a rendering hint that can be used to take different behavior
+     *     based on the source device of copied data.
+     */
+    public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE";
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value =
diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java
index 5f85984..f12e971 100644
--- a/core/java/android/content/ComponentName.java
+++ b/core/java/android/content/ComponentName.java
@@ -314,17 +314,14 @@
      */
     @Override
     public boolean equals(@Nullable Object obj) {
-        try {
-            if (obj != null) {
-                ComponentName other = (ComponentName)obj;
-                // Note: no null checks, because mPackage and mClass can
-                // never be null.
-                return mPackage.equals(other.mPackage)
-                        && mClass.equals(other.mClass);
-            }
-        } catch (ClassCastException e) {
+        if (obj instanceof ComponentName) {
+            ComponentName other = (ComponentName) obj;
+            // mPackage and mClass can never be null.
+            return mPackage.equals(other.mPackage)
+                    && mClass.equals(other.mClass);
+        } else {
+            return false;
         }
-        return false;
     }
 
     @Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 753c936..ae1f689 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3938,6 +3938,7 @@
             //@hide: SAFETY_CENTER_SERVICE,
             DISPLAY_HASH_SERVICE,
             CREDENTIAL_SERVICE,
+            DEVICE_LOCK_SERVICE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ServiceName {}
@@ -4916,6 +4917,7 @@
      * @see android.server.attention.AttentionManagerService
      * @hide
      */
+    @TestApi
     public static final String ATTENTION_SERVICE = "attention";
 
     /**
@@ -5141,6 +5143,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.
@@ -5193,6 +5203,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.
@@ -6073,6 +6092,14 @@
     public static final String CREDENTIAL_SERVICE = "credential";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.devicelock.DeviceLockManager}.
+     *
+     * @see #getSystemService(String)
+     */
+    public static final String DEVICE_LOCK_SERVICE = "device_lock";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 43fa617..9d82274 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -49,6 +49,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.BundleMerger;
 import android.os.IBinder;
 import android.os.IncidentManager;
 import android.os.Parcel;
@@ -3560,6 +3561,17 @@
     public static final String ACTION_MEDIA_BUTTON = "android.intent.action.MEDIA_BUTTON";
 
     /**
+     * Broadcast action: Launch System output switcher. Includes a single extra field,
+     * {@link #EXTRA_PACKAGE_NAME}, which specifies the package name of the calling app
+     * so that the system can get the corresponding MediaSession for the output switcher.
+     *
+     * @see #EXTRA_PACKAGE_NAME
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SHOW_OUTPUT_SWITCHER =
+            "android.intent.action.SHOW_OUTPUT_SWITCHER";
+
+    /**
      * Broadcast Action:  The "Camera Button" was pressed.  Includes a single
      * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that
      * caused the broadcast.
@@ -11072,6 +11084,20 @@
     }
 
     /**
+     * Merge the extras data in this intent with that of other supplied intent using the
+     * strategy specified using {@code extrasMerger}.
+     *
+     * <p> Note the extras data in this intent is treated as the {@code first} param
+     * and the extras data in {@code other} intent is treated as the {@code last} param
+     * when using the passed in {@link BundleMerger} object.
+     *
+     * @hide
+     */
+    public void mergeExtras(@NonNull Intent other, @NonNull BundleMerger extrasMerger) {
+        mExtras = extrasMerger.merge(mExtras, other.mExtras);
+    }
+
+    /**
      * Wrapper class holding an Intent and implementing comparisons on it for
      * the purpose of filtering.  The class implements its
      * {@link #equals equals()} and {@link #hashCode hashCode()} methods as
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index b3435b1..8b6c4dd 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -28,6 +28,7 @@
 import android.os.PatternMatcher;
 import android.text.TextUtils;
 import android.util.AndroidException;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Printer;
 import android.util.proto.ProtoOutputStream;
@@ -302,7 +303,7 @@
     @UnsupportedAppUsage
     private int mOrder;
     @UnsupportedAppUsage
-    private final ArrayList<String> mActions;
+    private final ArraySet<String> mActions;
     private ArrayList<String> mCategories = null;
     private ArrayList<String> mDataSchemes = null;
     private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null;
@@ -433,7 +434,7 @@
      */
     public IntentFilter() {
         mPriority = 0;
-        mActions = new ArrayList<String>();
+        mActions = new ArraySet<>();
     }
 
     /**
@@ -445,7 +446,7 @@
      */
     public IntentFilter(String action) {
         mPriority = 0;
-        mActions = new ArrayList<String>();
+        mActions = new ArraySet<>();
         addAction(action);
     }
 
@@ -468,7 +469,7 @@
     public IntentFilter(String action, String dataType)
         throws MalformedMimeTypeException {
         mPriority = 0;
-        mActions = new ArrayList<String>();
+        mActions = new ArraySet<>();
         addAction(action);
         addDataType(dataType);
     }
@@ -481,7 +482,7 @@
     public IntentFilter(IntentFilter o) {
         mPriority = o.mPriority;
         mOrder = o.mOrder;
-        mActions = new ArrayList<String>(o.mActions);
+        mActions = new ArraySet<>(o.mActions);
         if (o.mCategories != null) {
             mCategories = new ArrayList<String>(o.mCategories);
         }
@@ -742,9 +743,7 @@
      * @param action Name of the action to match, such as Intent.ACTION_VIEW.
      */
     public final void addAction(String action) {
-        if (!mActions.contains(action)) {
-            mActions.add(action.intern());
-        }
+        mActions.add(action.intern());
     }
 
     /**
@@ -758,7 +757,7 @@
      * Return an action in the filter.
      */
     public final String getAction(int index) {
-        return mActions.get(index);
+        return mActions.valueAt(index);
     }
 
     /**
@@ -797,8 +796,11 @@
             if (ignoreActions == null) {
                 return !mActions.isEmpty();
             }
+            if (mActions.size() > ignoreActions.size()) {
+                return true;    // some actions are definitely not ignored
+            }
             for (int i = mActions.size() - 1; i >= 0; i--) {
-                if (!ignoreActions.contains(mActions.get(i))) {
+                if (!ignoreActions.contains(mActions.valueAt(i))) {
                     return true;
                 }
             }
@@ -1918,7 +1920,7 @@
         int N = countActions();
         for (int i=0; i<N; i++) {
             serializer.startTag(null, ACTION_STR);
-            serializer.attribute(null, NAME_STR, mActions.get(i));
+            serializer.attribute(null, NAME_STR, mActions.valueAt(i));
             serializer.endTag(null, ACTION_STR);
         }
         N = countCategories();
@@ -2313,7 +2315,7 @@
     }
 
     public final void writeToParcel(Parcel dest, int flags) {
-        dest.writeStringList(mActions);
+        dest.writeStringArray(mActions.toArray(new String[mActions.size()]));
         if (mCategories != null) {
             dest.writeInt(1);
             dest.writeStringList(mCategories);
@@ -2407,8 +2409,9 @@
 
     /** @hide */
     public IntentFilter(Parcel source) {
-        mActions = new ArrayList<String>();
-        source.readStringList(mActions);
+        List<String> actions = new ArrayList<>();
+        source.readStringList(actions);
+        mActions = new ArraySet<>(actions);
         if (source.readInt() != 0) {
             mCategories = new ArrayList<String>();
             source.readStringList(mCategories);
diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java
index b1252fd..8853b70 100644
--- a/core/java/android/content/IntentSender.java
+++ b/core/java/android/content/IntentSender.java
@@ -18,6 +18,8 @@
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.IApplicationThread;
 import android.app.ActivityManager.PendingIntentInfo;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Bundle;
@@ -194,7 +196,9 @@
             String resolvedType = intent != null ?
                     intent.resolveTypeIfNeeded(context.getContentResolver())
                     : null;
-            int res = ActivityManager.getService().sendIntentSender(mTarget, mWhitelistToken,
+            final IApplicationThread app = ActivityThread.currentActivityThread()
+                    .getApplicationThread();
+            int res = ActivityManager.getService().sendIntentSender(app, mTarget, mWhitelistToken,
                     code, intent, resolvedType,
                     onFinished != null
                             ? new FinishedDispatcher(this, onFinished, handler)
diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java
index 495f94f..bf9dc8e 100644
--- a/core/java/android/content/SyncAdaptersCache.java
+++ b/core/java/android/content/SyncAdaptersCache.java
@@ -25,10 +25,10 @@
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/content/integrity/AtomicFormula.java b/core/java/android/content/integrity/AtomicFormula.java
index f888813..1b5f64c 100644
--- a/core/java/android/content/integrity/AtomicFormula.java
+++ b/core/java/android/content/integrity/AtomicFormula.java
@@ -261,8 +261,8 @@
             }
             LongAtomicFormula that = (LongAtomicFormula) o;
             return getKey() == that.getKey()
-                    && mValue == that.mValue
-                    && mOperator == that.mOperator;
+                    && Objects.equals(mValue, that.mValue)
+                    && Objects.equals(mOperator, that.mOperator);
         }
 
         @Override
@@ -628,7 +628,7 @@
                 return false;
             }
             BooleanAtomicFormula that = (BooleanAtomicFormula) o;
-            return getKey() == that.getKey() && mValue == that.mValue;
+            return getKey() == that.getKey() && Objects.equals(mValue, that.mValue);
         }
 
         @Override
diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java
index 3ca0560..cc7977a 100644
--- a/core/java/android/content/om/FabricatedOverlay.java
+++ b/core/java/android/content/om/FabricatedOverlay.java
@@ -20,11 +20,13 @@
 import android.annotation.Nullable;
 import android.os.FabricatedOverlayInternal;
 import android.os.FabricatedOverlayInternalEntry;
+import android.os.ParcelFileDescriptor;
 import android.text.TextUtils;
 
 import com.android.internal.util.Preconditions;
 
 import java.util.ArrayList;
+import java.util.Objects;
 
 /**
  * Fabricated Runtime Resource Overlays (FRROs) are overlays generated ar runtime.
@@ -88,6 +90,27 @@
         }
 
         /**
+         * Ensure the resource name is in the form [package]:type/entry.
+         *
+         * @param name name of the target resource to overlay (in the form [package]:type/entry)
+         * @return the valid name
+         */
+        private static String ensureValidResourceName(@NonNull String name) {
+            Objects.requireNonNull(name);
+            final int slashIndex = name.indexOf('/'); /* must contain '/' */
+            final int colonIndex = name.indexOf(':'); /* ':' should before '/' if ':' exist */
+
+            // The minimum length of resource type is "id".
+            Preconditions.checkArgument(
+                    slashIndex >= 0 /* It must contain the type name */
+                    && colonIndex != 0 /* 0 means the package name is empty */
+                    && (slashIndex - colonIndex) > 2 /* The shortest length of type is "id" */,
+                    "\"%s\" is invalid resource name",
+                    name);
+            return name;
+        }
+
+        /**
          * Sets the value of the fabricated overlay
          *
          * @param resourceName name of the target resource to overlay (in the form
@@ -98,6 +121,8 @@
          * @see android.util.TypedValue#type
          */
         public Builder setResourceValue(@NonNull String resourceName, int dataType, int value) {
+            ensureValidResourceName(resourceName);
+
             final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
             entry.resourceName = resourceName;
             entry.dataType = dataType;
@@ -119,6 +144,8 @@
          */
         public Builder setResourceValue(@NonNull String resourceName, int dataType, int value,
                 String configuration) {
+            ensureValidResourceName(resourceName);
+
             final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
             entry.resourceName = resourceName;
             entry.dataType = dataType;
@@ -139,6 +166,8 @@
          * @see android.util.TypedValue#type
          */
         public Builder setResourceValue(@NonNull String resourceName, int dataType, String value) {
+            ensureValidResourceName(resourceName);
+
             final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
             entry.resourceName = resourceName;
             entry.dataType = dataType;
@@ -160,6 +189,8 @@
          */
         public Builder setResourceValue(@NonNull String resourceName, int dataType, String value,
                 String configuration) {
+            ensureValidResourceName(resourceName);
+
             final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
             entry.resourceName = resourceName;
             entry.dataType = dataType;
@@ -169,6 +200,26 @@
             return this;
         }
 
+        /**
+         * Sets the value of the fabricated overlay
+         *
+         * @param resourceName name of the target resource to overlay (in the form
+         *                     [package]:type/entry)
+         * @param value the file descriptor whose contents are the value of the frro
+         * @param configuration The string representation of the config this overlay is enabled for
+         */
+        public Builder setResourceValue(@NonNull String resourceName, ParcelFileDescriptor value,
+                String configuration) {
+            ensureValidResourceName(resourceName);
+
+            final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
+            entry.resourceName = resourceName;
+            entry.binaryData = value;
+            entry.configuration = configuration;
+            mEntries.add(entry);
+            return this;
+        }
+
         /** Builds an immutable fabricated overlay. */
         public FabricatedOverlay build() {
             final FabricatedOverlayInternal overlay = new FabricatedOverlayInternal();
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 26c947b..fda4119 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -221,6 +221,23 @@
     public String launchToken;
 
     /**
+     * Specifies the category of the target display the activity is expected to run on. Set from
+     * the {@link android.R.attr#targetDisplayCategory} attribute. Upon creation, a virtual display
+     * can specify which display categories it supports and one of the category must be present in
+     * the activity's manifest to allow this activity to run. The default value is {@code null},
+     * which indicates the activity does not belong to a restricted display category and thus can
+     * only run on a display that didn't specify any display categories. Each activity can only
+     * specify one category it targets to but a virtual display can support multiple restricted
+     * categories.
+     *
+     * This field should be formatted as a Java-language-style free form string(for example,
+     * com.google.automotive_entertainment), which may contain uppercase or lowercase letters ('A'
+     * through 'Z'), numbers, and underscores ('_') but may only start with letters.
+     */
+    @Nullable
+    public String targetDisplayCategory;
+
+    /**
      * Activity can not be resized and always occupies the fullscreen area with all windows fully
      * visible.
      * @hide
@@ -1313,6 +1330,7 @@
         mMaxAspectRatio = orig.mMaxAspectRatio;
         mMinAspectRatio = orig.mMinAspectRatio;
         supportsSizeChanges = orig.supportsSizeChanges;
+        targetDisplayCategory = orig.targetDisplayCategory;
     }
 
     /**
@@ -1651,6 +1669,9 @@
         if (mKnownActivityEmbeddingCerts != null) {
             pw.println(prefix + "knownActivityEmbeddingCerts=" + mKnownActivityEmbeddingCerts);
         }
+        if (targetDisplayCategory != null) {
+            pw.println(prefix + "targetDisplayCategory=" + targetDisplayCategory);
+        }
         super.dumpBack(pw, prefix, dumpFlags);
     }
 
@@ -1697,6 +1718,7 @@
         dest.writeFloat(mMinAspectRatio);
         dest.writeBoolean(supportsSizeChanges);
         sForStringSet.parcel(mKnownActivityEmbeddingCerts, dest, flags);
+        dest.writeString8(targetDisplayCategory);
     }
 
     /**
@@ -1822,6 +1844,7 @@
         if (mKnownActivityEmbeddingCerts.isEmpty()) {
             mKnownActivityEmbeddingCerts = null;
         }
+        targetDisplayCategory = source.readString8();
     }
 
     /**
diff --git a/core/java/android/content/pm/IBackgroundInstallControlService.aidl b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
new file mode 100644
index 0000000..c8e7cae
--- /dev/null
+++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.content.pm;
+
+import android.content.pm.ParceledListSlice;
+
+/**
+ * {@hide}
+ */
+interface IBackgroundInstallControlService {
+    ParceledListSlice getBackgroundInstalledPackages(long flags, int userId);
+}
diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java
index 56b8bd8..2b40fdf 100644
--- a/core/java/android/content/pm/IntentFilterVerificationInfo.java
+++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java
@@ -28,10 +28,10 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index d2fb1fb..d7686e2 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -3346,7 +3346,7 @@
         /**
          * The label representing the app to be installed.
          */
-        private final @NonNull String mLabel;
+        private final @NonNull CharSequence mLabel;
         /**
          * The locale of the app label being used.
          */
@@ -3388,7 +3388,7 @@
         @DataClass.Generated.Member
         public PreapprovalDetails(
                 @Nullable Bitmap icon,
-                @NonNull String label,
+                @NonNull CharSequence label,
                 @NonNull ULocale locale,
                 @NonNull String packageName) {
             this.mIcon = icon;
@@ -3417,7 +3417,7 @@
          * The label representing the app to be installed.
          */
         @DataClass.Generated.Member
-        public @NonNull String getLabel() {
+        public @NonNull CharSequence getLabel() {
             return mLabel;
         }
 
@@ -3461,7 +3461,7 @@
             if (mIcon != null) flg |= 0x1;
             dest.writeByte(flg);
             if (mIcon != null) mIcon.writeToParcel(dest, flags);
-            dest.writeString8(mLabel);
+            dest.writeCharSequence(mLabel);
             dest.writeString8(mLocale.toString());
             dest.writeString8(mPackageName);
         }
@@ -3479,7 +3479,7 @@
 
             byte flg = in.readByte();
             Bitmap icon = (flg & 0x1) == 0 ? null : Bitmap.CREATOR.createFromParcel(in);
-            String label = in.readString8();
+            CharSequence label = (CharSequence) in.readCharSequence();
             ULocale locale = new ULocale(in.readString8());
             String packageName = in.readString8();
 
@@ -3519,7 +3519,7 @@
         public static final class Builder {
 
             private @Nullable Bitmap mIcon;
-            private @NonNull String mLabel;
+            private @NonNull CharSequence mLabel;
             private @NonNull ULocale mLocale;
             private @NonNull String mPackageName;
 
@@ -3545,7 +3545,7 @@
              * The label representing the app to be installed.
              */
             @DataClass.Generated.Member
-            public @NonNull Builder setLabel(@NonNull String value) {
+            public @NonNull Builder setLabel(@NonNull CharSequence value) {
                 checkNotUsed();
                 mBuilderFieldsSet |= 0x2;
                 mLabel = value;
@@ -3596,10 +3596,10 @@
         }
 
         @DataClass.Generated(
-                time = 1664257135109L,
+                time = 1666748098353L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
-                inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.String mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genBuilder=true, genToString=true)")
+                inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genBuilder=true, genToString=true)")
         @Deprecated
         private void __metadata() {}
 
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index db991dc..485d04d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -165,6 +165,21 @@
             "android.internal.PROPERTY_NO_APP_DATA_STORAGE";
 
     /**
+     * &lt;service&gt; level {@link android.content.pm.PackageManager.Property} tag specifying
+     * the actual use case of the service if it's foreground service with the type
+     * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE}.
+     *
+     * <p>
+     * For example:
+     * &lt;service&gt;
+     *   &lt;property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+     *     android:value="foo"/&gt;
+     * &lt;/service&gt;
+     */
+    public static final String PROPERTY_SPECIAL_USE_FGS_SUBTYPE =
+            "android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE";
+
+    /**
      * A property value set within the manifest.
      * <p>
      * The value of a property will only have a single type, as defined by
@@ -189,12 +204,12 @@
         @VisibleForTesting
         public Property(@NonNull String name, int type,
                 @NonNull String packageName, @Nullable String className) {
-            assert name != null;
-            assert type >= TYPE_BOOLEAN && type <= TYPE_STRING;
-            assert packageName != null;
-            this.mName = name;
+            if (type < TYPE_BOOLEAN || type > TYPE_STRING) {
+                throw new IllegalArgumentException("Invalid type");
+            }
+            this.mName = Objects.requireNonNull(name);
             this.mType = type;
-            this.mPackageName = packageName;
+            this.mPackageName = Objects.requireNonNull(packageName);
             this.mClassName = className;
         }
         /** @hide */
@@ -442,9 +457,8 @@
          */
         public ComponentEnabledSetting(@NonNull ComponentName componentName,
                 @EnabledState int newState, @EnabledFlags int flags) {
-            Objects.nonNull(componentName);
             mPackageName = null;
-            mComponentName = componentName;
+            mComponentName = Objects.requireNonNull(componentName);
             mEnabledState = newState;
             mEnabledFlags = flags;
         }
@@ -460,8 +474,7 @@
          */
         public ComponentEnabledSetting(@NonNull String packageName,
                 @EnabledState int newState, @EnabledFlags int flags) {
-            Objects.nonNull(packageName);
-            mPackageName = packageName;
+            mPackageName = Objects.requireNonNull(packageName);
             mComponentName = null;
             mEnabledState = newState;
             mEnabledFlags = flags;
@@ -2205,6 +2218,13 @@
      */
     public static final int INSTALL_ACTIVATION_FAILED = -128;
 
+    /**
+     * Installation failed return code: requesting user pre-approval is currently unavailable.
+     *
+     * @hide
+     */
+    public static final int INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE = -129;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "DELETE_" }, value = {
             DELETE_KEEP_DATA,
@@ -2939,6 +2959,18 @@
     public static final String FEATURE_OPENGLES_EXTENSION_PACK = "android.hardware.opengles.aep";
 
     /**
+     * Feature for {@link #getSystemAvailableFeatures()} and {@link #hasSystemFeature(String)}.
+     * This feature indicates whether device supports
+     * <a href="https://source.android.com/docs/core/virtualization">Android Virtualization Framework</a>.
+     *
+     * @hide
+     */
+    @SystemApi
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_VIRTUALIZATION_FRAMEWORK =
+            "android.software.virtualization_framework";
+
+    /**
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature(String, int)}: If this feature is supported, the Vulkan
      * implementation on this device is hardware accelerated, and the Vulkan native API will
@@ -3400,7 +3432,6 @@
      * Feature for {@link #getSystemAvailableFeatures} and
      * {@link #hasSystemFeature}: The device is capable of communicating with
      * other devices via ultra wideband.
-     * @hide
      */
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_UWB = "android.hardware.uwb";
@@ -4194,6 +4225,14 @@
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_CREDENTIALS = "android.software.credentials";
 
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device supports locking (for example, by a financing provider in case of a missed
+     * payment).
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
+
     /** @hide */
     public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true;
 
@@ -9635,6 +9674,7 @@
             case INSTALL_FAILED_NO_MATCHING_ABIS: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
             case INSTALL_FAILED_ABORTED: return PackageInstaller.STATUS_FAILURE_ABORTED;
             case INSTALL_FAILED_MISSING_SPLIT: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
+            case INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE: return PackageInstaller.STATUS_FAILURE_BLOCKED;
             default: return PackageInstaller.STATUS_FAILURE;
         }
     }
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index bb88486..7c22c088 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -33,6 +34,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
 import java.util.Set;
 
 /**
@@ -486,7 +488,10 @@
      *
      * @hide
      */
-    public @Nullable Set<String> knownCerts;
+    // Already being used as mutable and most other fields in this class are also mutable.
+    @SuppressLint("MutableBareField")
+    @SystemApi
+    public @NonNull Set<String> knownCerts = Collections.emptySet();
 
     /** @hide */
     public static int fixProtectionLevel(int level) {
@@ -620,6 +625,8 @@
         descriptionRes = orig.descriptionRes;
         requestRes = orig.requestRes;
         nonLocalizedDescription = orig.nonLocalizedDescription;
+        // Note that knownCerts wasn't properly copied before Android U.
+        knownCerts = orig.knownCerts;
     }
 
     /**
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 78984bd..104527e 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -36,14 +36,14 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 88d7004..14f03ea 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -16,7 +16,9 @@
 
 package android.content.pm;
 
+import android.Manifest;
 import android.annotation.IntDef;
+import android.annotation.RequiresPermission;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Printer;
@@ -100,7 +102,15 @@
 
     /**
      * The default foreground service type if not been set in manifest file.
+     *
+     * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and
+     * later should NOT use this type,
+     * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with
+     * this type will get a {@link android.app.ForegroundServiceTypeNotAllowedException}.</p>
+     *
+     * @deprecated Do not use.
      */
+    @Deprecated
     public static final int FOREGROUND_SERVICE_TYPE_NONE = 0;
 
     /**
@@ -108,14 +118,36 @@
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Data(photo, file, account) upload/download, backup/restore, import/export, fetch,
      * transfer over network between device and cloud.
+     *
+     * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and
+     * later should NOT use this type:
+     * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with
+     * this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} is still
+     * allowed, but calling it with this type on devices running future platform releases may get a
+     * {@link android.app.ForegroundServiceTypeNotAllowedException}.</p>
+     *
+     * @deprecated Use {@link android.app.job.JobInfo.Builder} data transfer APIs instead.
      */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC,
+            conditional = true
+    )
+    @Deprecated
     public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0;
 
     /**
      * Constant corresponding to <code>mediaPlayback</code> in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Music, video, news or other media playback.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PLAYBACK}.
      */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK,
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 1 << 1;
 
     /**
@@ -123,28 +155,94 @@
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Ongoing operations related to phone calls, video conferencing,
      * or similar interactive communication.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and
+     * {@link android.Manifest.permission#MANAGE_OWN_CALLS}.
      */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL,
+            },
+            anyOf = {
+                Manifest.permission.MANAGE_OWN_CALLS,
+            },
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 1 << 2;
 
     /**
      * Constant corresponding to <code>location</code> in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * GPS, map, navigation location update.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_LOCATION} and one of the
+     * following permissions:
+     * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION},
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
      */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_LOCATION,
+            },
+            anyOf = {
+                Manifest.permission.ACCESS_COARSE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION,
+            },
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 1 << 3;
 
     /**
      * Constant corresponding to <code>connectedDevice</code> in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Auto, bluetooth, TV or other devices connection, monitoring and interaction.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_CONNECTED_DEVICE} and one of the
+     * following permissions:
+     * {@link android.Manifest.permission#BLUETOOTH_CONNECT},
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE},
+     * {@link android.Manifest.permission#CHANGE_WIFI_STATE},
+     * {@link android.Manifest.permission#CHANGE_WIFI_MULTICAST_STATE},
+     * {@link android.Manifest.permission#NFC},
+     * {@link android.Manifest.permission#TRANSMIT_IR},
+     * or has been granted the access to one of the attached USB devices/accessories.
      */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE,
+            },
+            anyOf = {
+                Manifest.permission.BLUETOOTH_CONNECT,
+                Manifest.permission.CHANGE_NETWORK_STATE,
+                Manifest.permission.CHANGE_WIFI_STATE,
+                Manifest.permission.CHANGE_WIFI_MULTICAST_STATE,
+                Manifest.permission.NFC,
+                Manifest.permission.TRANSMIT_IR,
+            },
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 1 << 4;
 
     /**
      * Constant corresponding to {@code mediaProjection} in
      * the {@link android.R.attr#foregroundServiceType} attribute.
      * Managing a media projection session, e.g for screen recording or taking screenshots.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROJECTION}, and the user must
+     * have allowed the screen capture request from this app.
      */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION,
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION = 1 << 5;
 
     /**
@@ -155,7 +253,21 @@
      * above, a foreground service will not be able to access the camera if this type is not
      * specified in the manifest and in
      * {@link android.app.Service#startForeground(int, android.app.Notification, int)}.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_CAMERA} and
+     * {@link android.Manifest.permission#CAMERA}.
      */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_CAMERA,
+            },
+            anyOf = {
+                Manifest.permission.CAMERA,
+            },
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 1 << 6;
 
     /**
@@ -166,17 +278,170 @@
      * above, a foreground service will not be able to access the microphone if this type is not
      * specified in the manifest and in
      * {@link android.app.Service#startForeground(int, android.app.Notification, int)}.
+     *
+     * <p>Starting foreground service with this type from apps targeting API level
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and later, will require permission
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_MICROPHONE} and one of the following
+     * permissions:
+     * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT},
+     * {@link android.Manifest.permission#RECORD_AUDIO}.
      */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_MICROPHONE,
+            },
+            anyOf = {
+                Manifest.permission.CAPTURE_AUDIO_OUTPUT,
+                Manifest.permission.RECORD_AUDIO,
+            },
+            conditional = true
+    )
     public static final int FOREGROUND_SERVICE_TYPE_MICROPHONE = 1 << 7;
 
     /**
-     * The number of foreground service types, this doesn't include
-     * the {@link #FOREGROUND_SERVICE_TYPE_MANIFEST} and {@link #FOREGROUND_SERVICE_TYPE_NONE}
-     * as they're not real service types.
+     * Constant corresponding to {@code health} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * Health, wellness and fitness.
+     *
+     * <p>The caller app is required to have the permissions
+     * {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following
+     * permissions:
+     * {@link android.Manifest.permission#ACTIVITY_RECOGNITION},
+     * {@link android.Manifest.permission#BODY_SENSORS},
+     * {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}.
+     */
+    @RequiresPermission(
+            allOf = {
+                Manifest.permission.FOREGROUND_SERVICE_HEALTH,
+            },
+            anyOf = {
+                Manifest.permission.ACTIVITY_RECOGNITION,
+                Manifest.permission.BODY_SENSORS,
+                Manifest.permission.HIGH_SAMPLING_RATE_SENSORS,
+            },
+            conditional = true
+    )
+    public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 1 << 8;
+
+    /**
+     * Constant corresponding to {@code remoteMessaging} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * Messaging use cases which host local server to relay messages across devices.
+     */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING,
+            conditional = true
+    )
+    public static final int FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING = 1 << 9;
+
+    /**
+     * Constant corresponding to {@code systemExempted} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * The system exmpted foreground service use cases.
+     *
+     * <p class="note">Note, apps are allowed to use this type only in the following cases:
+     * <ul>
+     *   <li>App has a UID &lt; {@link android.os.Process#FIRST_APPLICATION_UID}</li>
+     *   <li>App is on Doze allowlist</li>
+     *   <li>Device is running in <a href="https://android.googlesource.com/platform/frameworks/base/+/master/packages/SystemUI/docs/demo_mode.md">Demo Mode</a></li>
+     *   <li><a href="https://source.android.com/devices/tech/admin/provision">Device owner app</a><li>
+     *   <li><a href="https://source.android.com/devices/tech/admin/managed-profiles">Profile owner apps</a><li>
+     *   <li>Persistent apps</li>
+     *   <li><a href="https://source.android.com/docs/core/connect/carrier">Carrier privileged apps</a></li>
+     *   <li>Apps that have the {@code android.app.role.RoleManager#ROLE_EMERGENCY} role</li>
+     *   <li>Headless system apps</li>
+     *   <li><a href="{@docRoot}guide/topics/admin/device-admin">Device admin apps</a></li>
+     *   <li>Active VPN apps</li>
+     *   <li>Apps holding {@link Manifest.permission#SCHEDULE_EXACT_ALARM} or
+     *       {@link Manifest.permission#USE_EXACT_ALARM} permission.</li>
+     * </ul>
+     * </p>
+     */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED,
+            conditional = true
+    )
+    public static final int FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1 << 10;
+
+    /**
+     * Foreground service type corresponding to {@code shortService} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     *
+     * TODO Implement it
+     *
+     * TODO Expand the javadoc
+     *
+     * This type is not associated with specific use cases unlike other types, but this has
+     * unique restrictions.
+     * <ul>
+     *     <li>Has a timeout
+     *     <li>Cannot start other foreground services from this
+     *     <li>
+     * </ul>
+     *
+     * @see Service#onTimeout
      *
      * @hide
      */
-    public static final int NUM_OF_FOREGROUND_SERVICE_TYPES = 8;
+    public static final int FOREGROUND_SERVICE_TYPE_SHORT_SERVICE = 1 << 11;
+
+    /**
+     * Constant corresponding to {@code specialUse} in
+     * the {@link android.R.attr#foregroundServiceType} attribute.
+     * Use cases that can't be categorized into any other foreground service types, but also
+     * can't use {@link android.app.job.JobInfo.Builder} APIs.
+     *
+     * <p>The use of this foreground service type may be restricted. Additionally, apps must declare
+     * a service-level {@link PackageManager#PROPERTY_SPECIAL_USE_FGS_SUBTYPE &lt;property&gt;} in
+     * {@code AndroidManifest.xml} as a hint of what the exact use case here is.
+     * Here is an example:
+     * <pre>
+     *  &lt;uses-permission
+     *      android:name="android.permissions.FOREGROUND_SERVICE_SPECIAL_USE"
+     *  /&gt;
+     *  &lt;service
+     *      android:name=".MySpecialForegroundService"
+     *      android:foregroundServiceType="specialUse"&gt;
+     *      &lt;property
+     *          android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+     *          android:value="foo"
+     *      /&gt;
+     * &lt;/service&gt;
+     * </pre>
+     *
+     * In a future release of Android, if the above foreground service type {@code foo} is supported
+     * by the platform, to offer the backward compatibility, the app could specify
+     * the {@code android:maxSdkVersion} attribute in the &lt;uses-permission&gt; section,
+     * and also add the foreground service type {@code foo} into
+     * the {@code android:foregroundServiceType}, therefore the same app could be installed
+     * in both platforms.
+     * <pre>
+     *  &lt;uses-permission
+     *      android:name="android.permissions.FOREGROUND_SERVICE_SPECIAL_USE"
+     *      android:maxSdkVersion="last_sdk_version_without_type_foo"
+     *  /&gt;
+     *  &lt;service
+     *      android:name=".MySpecialForegroundService"
+     *      android:foregroundServiceType="specialUse|foo"&gt;
+     *      &lt;property
+     *          android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE""
+     *          android:value="foo"
+     *      /&gt;
+     * &lt;/service&gt;
+     * </pre>
+     */
+    @RequiresPermission(
+            value = Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE,
+            conditional = true
+    )
+    public static final int FOREGROUND_SERVICE_TYPE_SPECIAL_USE = 1 << 30;
+
+    /**
+     * The max index being used in the definition of foreground service types.
+     *
+     * @hide
+     */
+    public static final int FOREGROUND_SERVICE_TYPES_MAX_INDEX = 30;
 
     /**
      * A special value indicates to use all types set in manifest file.
@@ -199,7 +464,12 @@
             FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
             FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION,
             FOREGROUND_SERVICE_TYPE_CAMERA,
-            FOREGROUND_SERVICE_TYPE_MICROPHONE
+            FOREGROUND_SERVICE_TYPE_MICROPHONE,
+            FOREGROUND_SERVICE_TYPE_HEALTH,
+            FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING,
+            FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
+            FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
+            FOREGROUND_SERVICE_TYPE_SPECIAL_USE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ForegroundServiceType {}
@@ -275,6 +545,16 @@
                 return "camera";
             case FOREGROUND_SERVICE_TYPE_MICROPHONE:
                 return "microphone";
+            case FOREGROUND_SERVICE_TYPE_HEALTH:
+                return "health";
+            case FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING:
+                return "remoteMessaging";
+            case FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED:
+                return "systemExempted";
+            case FOREGROUND_SERVICE_TYPE_SHORT_SERVICE:
+                return "shortService";
+            case FOREGROUND_SERVICE_TYPE_SPECIAL_USE:
+                return "specialUse";
             default:
                 return "unknown";
         }
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 1f83d75..295df5c 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -50,6 +50,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
+import java.lang.IllegalArgumentException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -1360,7 +1361,9 @@
         @NonNull
         public Builder setIntents(@NonNull Intent[] intents) {
             Objects.requireNonNull(intents, "intents cannot be null");
-            Objects.requireNonNull(intents.length, "intents cannot be empty");
+            if (intents.length == 0) {
+                throw new IllegalArgumentException("intents cannot be empty");
+            }
             for (Intent intent : intents) {
                 Objects.requireNonNull(intent, "intents cannot contain null");
                 Objects.requireNonNull(intent.getAction(), "intent's action must be set");
@@ -1398,7 +1401,9 @@
         @NonNull
         public Builder setPersons(@NonNull Person[] persons) {
             Objects.requireNonNull(persons, "persons cannot be null");
-            Objects.requireNonNull(persons.length, "persons cannot be empty");
+            if (persons.length == 0) {
+                throw new IllegalArgumentException("persons cannot be empty");
+            }
             for (Person person : persons) {
                 Objects.requireNonNull(person, "persons cannot contain null");
             }
diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java
index d94b0d8..b049880 100644
--- a/core/java/android/content/pm/Signature.java
+++ b/core/java/android/content/pm/Signature.java
@@ -21,9 +21,9 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
diff --git a/core/java/android/content/pm/SuspendDialogInfo.java b/core/java/android/content/pm/SuspendDialogInfo.java
index 23945ee..8786f7c 100644
--- a/core/java/android/content/pm/SuspendDialogInfo.java
+++ b/core/java/android/content/pm/SuspendDialogInfo.java
@@ -29,11 +29,11 @@
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 9baa6ba..2be0323 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -159,6 +159,18 @@
     public static final int FLAG_EPHEMERAL_ON_CREATE = 0x00002000;
 
     /**
+     * Indicates that this user is the designated main user on the device. This user may have access
+     * to certain features which are limited to at most one user.
+     *
+     * <p>Currently, this will be the first user to go through setup on the device, but in future
+     * releases this status may be transferable or may even not be given to any users.
+     *
+     * <p>This is not necessarily the system user. For example, it will not be the system user on
+     * devices for which {@link UserManager#isHeadlessSystemUserMode()} returns true.
+     */
+    public static final int FLAG_MAIN = 0x00004000;
+
+    /**
      * @hide
      */
     @IntDef(flag = true, prefix = "FLAG_", value = {
@@ -175,7 +187,8 @@
             FLAG_FULL,
             FLAG_SYSTEM,
             FLAG_PROFILE,
-            FLAG_EPHEMERAL_ON_CREATE
+            FLAG_EPHEMERAL_ON_CREATE,
+            FLAG_MAIN
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface UserInfoFlag {
@@ -369,6 +382,13 @@
     }
 
     /**
+     * @see #FLAG_MAIN
+     */
+    public boolean isMain() {
+        return (flags & FLAG_MAIN) == FLAG_MAIN;
+    }
+
+    /**
      * Returns true if the user is a split system user.
      * <p>If {@link UserManager#isSplitSystemUser split system user mode} is not enabled,
      * the method always returns false.
diff --git a/core/java/android/content/pm/UserPackage.java b/core/java/android/content/pm/UserPackage.java
new file mode 100644
index 0000000..7ca92c3
--- /dev/null
+++ b/core/java/android/content/pm/UserPackage.java
@@ -0,0 +1,134 @@
+/*
+ * 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.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.SparseArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Objects;
+
+/**
+ * POJO to represent a package for a specific user ID.
+ *
+ * @hide
+ */
+public final class UserPackage {
+    private static final boolean ENABLE_CACHING = true;
+
+    @UserIdInt
+    public final int userId;
+    public final String packageName;
+
+    private static final Object sCacheLock = new Object();
+    @GuardedBy("sCacheLock")
+    private static final SparseArrayMap<String, UserPackage> sCache = new SparseArrayMap<>();
+
+    private static final class NoPreloadHolder {
+        /** Set of userIDs to cache objects for. */
+        @GuardedBy("sCacheLock")
+        private static int[] sUserIds = new int[]{UserHandle.getUserId(Process.myUid())};
+    }
+
+    private UserPackage(int userId, String packageName) {
+        this.userId = userId;
+        this.packageName = packageName;
+    }
+
+    @Override
+    public String toString() {
+        return "<" + userId + ">" + packageName;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof UserPackage) {
+            UserPackage other = (UserPackage) obj;
+            return userId == other.userId && Objects.equals(packageName, other.packageName);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 0;
+        result = 31 * result + userId;
+        result = 31 * result + packageName.hashCode();
+        return result;
+    }
+
+    /** Return an instance of this class representing the given userId + packageName combination. */
+    @NonNull
+    public static UserPackage of(@UserIdInt int userId, @NonNull String packageName) {
+        if (!ENABLE_CACHING) {
+            return new UserPackage(userId, packageName);
+        }
+
+        synchronized (sCacheLock) {
+            if (!ArrayUtils.contains(NoPreloadHolder.sUserIds, userId)) {
+                // Don't cache objects for invalid userIds.
+                return new UserPackage(userId, packageName);
+            }
+
+            UserPackage up = sCache.get(userId, packageName);
+            if (up == null) {
+                packageName = packageName.intern();
+                up = new UserPackage(userId, packageName);
+                sCache.add(userId, packageName, up);
+            }
+            return up;
+        }
+    }
+
+    /** Remove the specified app from the cache. */
+    public static void removeFromCache(@UserIdInt int userId, @NonNull String packageName) {
+        if (!ENABLE_CACHING) {
+            return;
+        }
+
+        synchronized (sCacheLock) {
+            sCache.delete(userId, packageName);
+        }
+    }
+
+    /** Indicate the list of valid user IDs on the device. */
+    public static void setValidUserIds(@NonNull int[] userIds) {
+        if (!ENABLE_CACHING) {
+            return;
+        }
+
+        userIds = userIds.clone();
+        synchronized (sCacheLock) {
+            NoPreloadHolder.sUserIds = userIds;
+
+            for (int u = sCache.numMaps() - 1; u >= 0; --u) {
+                final int userId = sCache.keyAt(u);
+                if (!ArrayUtils.contains(userIds, userId)) {
+                    sCache.deleteAt(u);
+                }
+            }
+        }
+    }
+}
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 1a82e4d..fd35378 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -22,10 +22,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -43,17 +43,26 @@
     // Attribute strings for reading/writing properties to/from XML.
     private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher";
     private static final String ATTR_START_WITH_PARENT = "startWithParent";
+    private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
+    private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
+    private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
 
     /** Index values of each property (to indicate whether they are present in this object). */
     @IntDef(prefix = "INDEX_", value = {
             INDEX_SHOW_IN_LAUNCHER,
             INDEX_START_WITH_PARENT,
+            INDEX_SHOW_IN_SETTINGS,
+            INDEX_INHERIT_DEVICE_POLICY,
+            INDEX_USE_PARENTS_CONTACTS,
     })
     @Retention(RetentionPolicy.SOURCE)
     private @interface PropertyIndex {
     }
     private static final int INDEX_SHOW_IN_LAUNCHER = 0;
     private static final int INDEX_START_WITH_PARENT = 1;
+    private static final int INDEX_SHOW_IN_SETTINGS = 2;
+    private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
+    private static final int INDEX_USE_PARENTS_CONTACTS = 4;
     /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
     private long mPropertiesPresent = 0;
 
@@ -87,6 +96,68 @@
     public static final int SHOW_IN_LAUNCHER_NO = 2;
 
     /**
+     * Possible values for whether or how to show this user in the Settings app.
+     * @hide
+     */
+    @IntDef(prefix = "SHOW_IN_SETTINGS_", value = {
+            SHOW_IN_SETTINGS_WITH_PARENT,
+            SHOW_IN_SETTINGS_SEPARATE,
+            SHOW_IN_SETTINGS_NO,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShowInSettings {
+    }
+    /**
+     * Suggests that the Settings app should show this user's apps in the main tab.
+     * That is, either this user is a full user, so its apps should be presented accordingly, or, if
+     * this user is a profile, then its apps should be shown alongside its parent's apps.
+     * @hide
+     */
+    public static final int SHOW_IN_SETTINGS_WITH_PARENT = 0;
+    /**
+     * Suggests that the Settings app should show this user's apps, but separately from the apps of
+     * this user's parent.
+     * @hide
+     */
+    public static final int SHOW_IN_SETTINGS_SEPARATE = 1;
+    /**
+     * Suggests that the Settings app should not show this user.
+     * @hide
+     */
+    public static final int SHOW_IN_SETTINGS_NO = 2;
+
+    /**
+     * Possible values for whether (and from whom) to inherit select user restrictions
+     * or device policies.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "INHERIT_DEVICE_POLICY", value = {
+            INHERIT_DEVICE_POLICY_NO,
+            INHERIT_DEVICE_POLICY_FROM_PARENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InheritDevicePolicy {
+    }
+    /**
+     * Suggests that the given user profile should not inherit user restriction or device policy
+     * from any other user. This is the default value for any new user type.
+     * @hide
+     */
+    public static final int INHERIT_DEVICE_POLICY_NO = 0;
+    /**
+     * Suggests that the given user profile should inherit select user restrictions or
+     * device policies from its parent profile.
+     *
+     *<p> All the user restrictions and device policies would be not propagated to the profile
+     * with this property value. The {(TODO:b/256978256) @link DevicePolicyEngine}
+     * uses this property to determine and propagate only select ones to the given profile.
+     *
+     * @hide
+     */
+    public static final int INHERIT_DEVICE_POLICY_FROM_PARENT = 1;
+
+    /**
      * Reference to the default user properties for this user's user type.
      * <li>If non-null, then any absent property will use the default property from here instead.
      * <li>If null, then any absent property indicates that the caller lacks permission to see it,
@@ -114,7 +185,7 @@
     public UserProperties(UserProperties orig,
             boolean exposeAllFields,
             boolean hasManagePermission,
-            boolean hasQueryPermission) {
+            boolean hasQueryOrManagePermission) {
 
         if (orig.mDefaultProperties == null) {
             throw new IllegalArgumentException("Attempting to copy a non-original UserProperties.");
@@ -122,17 +193,22 @@
 
         this.mDefaultProperties = null;
 
+        // Insert each setter into the following hierarchy based on its permission requirements.
         // NOTE: Copy each property using getters to ensure default values are copied if needed.
         if (exposeAllFields) {
+            // Add items that require exposeAllFields to be true (strictest permission level).
             setStartWithParent(orig.getStartWithParent());
+            setInheritDevicePolicy(orig.getInheritDevicePolicy());
         }
         if (hasManagePermission) {
-            // Add any items that require this permission.
+            // Add items that require MANAGE_USERS or stronger.
+            setShowInSettings(orig.getShowInSettings());
+            setUseParentsContacts(orig.getUseParentsContacts());
         }
-        if (hasQueryPermission) {
-            // Add any items that require this permission.
+        if (hasQueryOrManagePermission) {
+            // Add items that require QUERY_USERS or stronger.
         }
-        // Add any items that require no permissions at all.
+        // Add items that have no permission requirements at all.
         setShowInLauncher(orig.getShowInLauncher());
     }
 
@@ -181,6 +257,33 @@
     private @ShowInLauncher int mShowInLauncher;
 
     /**
+     * Returns whether, and how, a user should be shown in the Settings app.
+     * This is generally inapplicable for non-profile users.
+     *
+     * Possible return values include
+     *    {@link #SHOW_IN_SETTINGS_WITH_PARENT}},
+     *    {@link #SHOW_IN_SETTINGS_SEPARATE},
+     *    and {@link #SHOW_IN_SETTINGS_NO}.
+     *
+     * <p> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this
+     * property.
+     *
+     * @return whether, and how, a profile should be shown in the Settings.
+     * @hide
+     */
+    public @ShowInSettings int getShowInSettings() {
+        if (isPresent(INDEX_SHOW_IN_SETTINGS)) return mShowInSettings;
+        if (mDefaultProperties != null) return mDefaultProperties.mShowInSettings;
+        throw new SecurityException("You don't have permission to query mShowInSettings");
+    }
+    /** @hide */
+    public void setShowInSettings(@ShowInSettings int val) {
+        this.mShowInSettings = val;
+        setPresent(INDEX_SHOW_IN_SETTINGS);
+    }
+    private @ShowInSettings int mShowInSettings;
+
+    /**
      * Returns whether a profile should be started when its parent starts (unless in quiet mode).
      * This only applies for users that have parents (i.e. for profiles).
      * @hide
@@ -197,6 +300,60 @@
     }
     private boolean mStartWithParent;
 
+    /**
+     * Return whether, and how, select user restrictions or device policies should be inherited
+     * from other user.
+     *
+     * Possible return values include
+     * {@link #INHERIT_DEVICE_POLICY_FROM_PARENT} or {@link #INHERIT_DEVICE_POLICY_NO}
+     *
+     * @hide
+     */
+    public @InheritDevicePolicy int getInheritDevicePolicy() {
+        if (isPresent(INDEX_INHERIT_DEVICE_POLICY)) return mInheritDevicePolicy;
+        if (mDefaultProperties != null) return mDefaultProperties.mInheritDevicePolicy;
+        throw new SecurityException("You don't have permission to query inheritDevicePolicy");
+    }
+    /** @hide */
+    public void setInheritDevicePolicy(@InheritDevicePolicy int val) {
+        this.mInheritDevicePolicy = val;
+        setPresent(INDEX_INHERIT_DEVICE_POLICY);
+    }
+    private @InheritDevicePolicy int mInheritDevicePolicy;
+
+    /**
+     * Returns whether the current user must use parent user's contacts. If true, writes to the
+     * ContactsProvider corresponding to the current user will be disabled and reads will be
+     * redirected to the parent.
+     *
+     * This only applies to users that have parents (i.e. profiles) and is used to ensure
+     * they can access contacts from the parent profile. This will be generally inapplicable for
+     * non-profile users.
+     *
+     * Please note that in case of the clone profiles, only the allow-listed apps would be allowed
+     * to access contacts across profiles and other apps will not see any contacts.
+     * TODO(b/256126819) Add link to the method returning apps allow-listed for app-cloning
+     *
+     * @return whether contacts access from an associated profile is enabled for the user
+     * @hide
+     */
+    public boolean getUseParentsContacts() {
+        if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
+        if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
+        throw new SecurityException("You don't have permission to query useParentsContacts");
+    }
+    /** @hide */
+    public void setUseParentsContacts(boolean val) {
+        this.mUseParentsContacts = val;
+        setPresent(INDEX_USE_PARENTS_CONTACTS);
+    }
+    /**
+     * Indicates whether the current user should use parent user's contacts.
+     * If this property is set true, the user will be blocked from storing any contacts in its
+     * own contacts database and will serve all read contacts calls through the parent's contacts.
+     */
+    private boolean mUseParentsContacts;
+
     @Override
     public String toString() {
         // Please print in increasing order of PropertyIndex.
@@ -204,6 +361,9 @@
                 + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
                 + ", mShowInLauncher=" + getShowInLauncher()
                 + ", mStartWithParent=" + getStartWithParent()
+                + ", mShowInSettings=" + getShowInSettings()
+                + ", mInheritDevicePolicy=" + getInheritDevicePolicy()
+                + ", mUseParentsContacts=" + getUseParentsContacts()
                 + "}";
     }
 
@@ -217,6 +377,9 @@
         pw.println(prefix + "    mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent));
         pw.println(prefix + "    mShowInLauncher=" + getShowInLauncher());
         pw.println(prefix + "    mStartWithParent=" + getStartWithParent());
+        pw.println(prefix + "    mShowInSettings=" + getShowInSettings());
+        pw.println(prefix + "    mInheritDevicePolicy=" + getInheritDevicePolicy());
+        pw.println(prefix + "    mUseParentsContacts=" + getUseParentsContacts());
     }
 
     /**
@@ -256,6 +419,15 @@
                 case ATTR_START_WITH_PARENT:
                     setStartWithParent(parser.getAttributeBoolean(i));
                     break;
+                case ATTR_SHOW_IN_SETTINGS:
+                    setShowInSettings(parser.getAttributeInt(i));
+                    break;
+                case ATTR_INHERIT_DEVICE_POLICY:
+                    setInheritDevicePolicy(parser.getAttributeInt(i));
+                    break;
+                case ATTR_USE_PARENTS_CONTACTS:
+                    setUseParentsContacts(parser.getAttributeBoolean(i));
+                    break;
                 default:
                     Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
             }
@@ -279,6 +451,17 @@
         if (isPresent(INDEX_START_WITH_PARENT)) {
             serializer.attributeBoolean(null, ATTR_START_WITH_PARENT, mStartWithParent);
         }
+        if (isPresent(INDEX_SHOW_IN_SETTINGS)) {
+            serializer.attributeInt(null, ATTR_SHOW_IN_SETTINGS, mShowInSettings);
+        }
+        if (isPresent(INDEX_INHERIT_DEVICE_POLICY)) {
+            serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
+                    mInheritDevicePolicy);
+        }
+        if (isPresent(INDEX_USE_PARENTS_CONTACTS)) {
+            serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
+                    mUseParentsContacts);
+        }
     }
 
     // For use only with an object that has already had any permission-lacking fields stripped out.
@@ -287,6 +470,9 @@
         dest.writeLong(mPropertiesPresent);
         dest.writeInt(mShowInLauncher);
         dest.writeBoolean(mStartWithParent);
+        dest.writeInt(mShowInSettings);
+        dest.writeInt(mInheritDevicePolicy);
+        dest.writeBoolean(mUseParentsContacts);
     }
 
     /**
@@ -299,6 +485,9 @@
         mPropertiesPresent = source.readLong();
         mShowInLauncher = source.readInt();
         mStartWithParent = source.readBoolean();
+        mShowInSettings = source.readInt();
+        mInheritDevicePolicy = source.readInt();
+        mUseParentsContacts = source.readBoolean();
     }
 
     @Override
@@ -325,6 +514,9 @@
         // UserProperties fields and their default values.
         private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT;
         private boolean mStartWithParent = false;
+        private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
+        private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
+        private boolean mUseParentsContacts = false;
 
         public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
             mShowInLauncher = showInLauncher;
@@ -336,21 +528,48 @@
             return this;
         }
 
+        /** Sets the value for {@link #mShowInSettings} */
+        public Builder setShowInSettings(@ShowInSettings int showInSettings) {
+            mShowInSettings = showInSettings;
+            return this;
+        }
+
+        /** Sets the value for {@link #mInheritDevicePolicy}*/
+        public Builder setInheritDevicePolicy(
+                @InheritDevicePolicy int inheritRestrictionsDevicePolicy) {
+            mInheritDevicePolicy = inheritRestrictionsDevicePolicy;
+            return this;
+        }
+
+        public Builder setUseParentsContacts(boolean useParentsContacts) {
+            mUseParentsContacts = useParentsContacts;
+            return this;
+        }
+
         /** Builds a UserProperties object with *all* values populated. */
         public UserProperties build() {
             return new UserProperties(
                     mShowInLauncher,
-                    mStartWithParent);
+                    mStartWithParent,
+                    mShowInSettings,
+                    mInheritDevicePolicy,
+                    mUseParentsContacts);
         }
     } // end Builder
 
     /** Creates a UserProperties with the given properties. Intended for building default values. */
     private UserProperties(
             @ShowInLauncher int showInLauncher,
-            boolean startWithParent) {
+            boolean startWithParent,
+            @ShowInSettings int showInSettings,
+            @InheritDevicePolicy int inheritDevicePolicy,
+            boolean useParentsContacts) {
 
         mDefaultProperties = null;
         setShowInLauncher(showInLauncher);
         setStartWithParent(startWithParent);
+        setShowInSettings(showInSettings);
+        setInheritDevicePolicy(inheritDevicePolicy);
+        setUseParentsContacts(useParentsContacts);
     }
 }
diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java
index 51cd6ca..d748aa1 100644
--- a/core/java/android/content/pm/XmlSerializerAndParser.java
+++ b/core/java/android/content/pm/XmlSerializerAndParser.java
@@ -17,10 +17,10 @@
 package android.content.pm;
 
 import android.compat.annotation.UnsupportedAppUsage;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index ff07291..09d24d4 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -40,8 +40,10 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.DrawableContainer;
 import android.icu.text.PluralRules;
+import android.net.Uri;
 import android.os.Build;
 import android.os.LocaleList;
+import android.os.ParcelFileDescriptor;
 import android.os.Trace;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
@@ -59,6 +61,8 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
@@ -799,7 +803,21 @@
     private Drawable decodeImageDrawable(@NonNull AssetInputStream ais,
             @NonNull Resources wrapper, @NonNull TypedValue value) {
         ImageDecoder.Source src = new ImageDecoder.AssetInputStreamSource(ais,
-                            wrapper, value);
+                wrapper, value);
+        try {
+            return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
+                decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+            });
+        } catch (IOException ioe) {
+            // This is okay. This may be something that ImageDecoder does not
+            // support, like SVG.
+            return null;
+        }
+    }
+
+    @Nullable
+    private Drawable decodeImageDrawable(@NonNull FileInputStream fis, @NonNull Resources wrapper) {
+        ImageDecoder.Source src = ImageDecoder.createSource(wrapper, fis);
         try {
             return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
@@ -860,6 +878,17 @@
                     } else {
                         dr = loadXmlDrawable(wrapper, value, id, density, file);
                     }
+                } else if (file.startsWith("frro://")) {
+                    Uri uri = Uri.parse(file);
+                    File f = new File('/' + uri.getHost() + uri.getPath());
+                    ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f,
+                            ParcelFileDescriptor.MODE_READ_ONLY);
+                    AssetFileDescriptor afd = new AssetFileDescriptor(
+                            pfd,
+                            Long.parseLong(uri.getQueryParameter("offset")),
+                            Long.parseLong(uri.getQueryParameter("size")));
+                    FileInputStream is = afd.createInputStream();
+                    dr = decodeImageDrawable(is, wrapper);
                 } else {
                     final InputStream is = mAssets.openNonAsset(
                             value.assetCookie, file, AssetManager.ACCESS_STREAMING);
diff --git a/core/java/android/credentials/Credential.java b/core/java/android/credentials/Credential.java
index a247d16..db89170 100644
--- a/core/java/android/credentials/Credential.java
+++ b/core/java/android/credentials/Credential.java
@@ -32,6 +32,14 @@
 public final class Credential implements Parcelable {
 
     /**
+     * The type value for password credential related operations.
+     *
+     * @hide
+     */
+    @NonNull public static final String TYPE_PASSWORD_CREDENTIAL =
+            "android.credentials.TYPE_PASSWORD_CREDENTIAL";
+
+    /**
      * The credential type.
      */
     @NonNull
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index b9cef0f..04d57ad 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -22,7 +22,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.IntentSender;
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
@@ -62,10 +64,10 @@
      * <p>The execution can potentially launch UI flows to collect user consent to using a
      * credential, display a picker when multiple credentials exist, etc.
      *
-     * @param request the request specifying type(s) of credentials to get from the user.
-     * @param cancellationSignal an optional signal that allows for cancelling this call.
-     * @param executor the callback will take place on this {@link Executor}.
-     * @param callback the callback invoked when the request succeeds or fails.
+     * @param request the request specifying type(s) of credentials to get from the user
+     * @param cancellationSignal an optional signal that allows for cancelling this call
+     * @param executor the callback will take place on this {@link Executor}
+     * @param callback the callback invoked when the request succeeds or fails
      */
     public void executeGetCredential(
             @NonNull GetCredentialRequest request,
@@ -84,8 +86,11 @@
 
         ICancellationSignal cancelRemote = null;
         try {
-            cancelRemote = mService.executeGetCredential(request,
-                    new GetCredentialTransport(executor, callback));
+            cancelRemote = mService.executeGetCredential(
+                    request,
+                    // TODO: use a real activity instead of context.
+                    new GetCredentialTransport(mContext, executor, callback),
+                    mContext.getOpPackageName());
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
@@ -101,10 +106,10 @@
      * <p>The execution can potentially launch UI flows to collect user consent to creating
      * or storing the new credential, etc.
      *
-     * @param request the request specifying type(s) of credentials to get from the user.
-     * @param cancellationSignal an optional signal that allows for cancelling this call.
-     * @param executor the callback will take place on this {@link Executor}.
-     * @param callback the callback invoked when the request succeeds or fails.
+     * @param request the request specifying type(s) of credentials to get from the user
+     * @param cancellationSignal an optional signal that allows for cancelling this call
+     * @param executor the callback will take place on this {@link Executor}
+     * @param callback the callback invoked when the request succeeds or fails
      */
     public void executeCreateCredential(
             @NonNull CreateCredentialRequest request,
@@ -124,7 +129,47 @@
         ICancellationSignal cancelRemote = null;
         try {
             cancelRemote = mService.executeCreateCredential(request,
-                    new CreateCredentialTransport(executor, callback));
+                    // TODO: use a real activity instead of context.
+                    new CreateCredentialTransport(mContext, executor, callback),
+                    mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        if (cancellationSignal != null && cancelRemote != null) {
+            cancellationSignal.setRemote(cancelRemote);
+        }
+    }
+
+    /**
+     * Clears the current user credential session from all credential providers.
+     *
+     * <p>Usually invoked after your user signs out of your app so that they will not be
+     * automatically signed in the next time.
+     *
+     * @param cancellationSignal an optional signal that allows for cancelling this call
+     * @param executor the callback will take place on this {@link Executor}
+     * @param callback the callback invoked when the request succeeds or fails
+     *
+     * @hide
+     */
+    public void clearCredentialSession(
+            @Nullable CancellationSignal cancellationSignal,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Void, CredentialManagerException> callback) {
+        requireNonNull(executor, "executor must not be null");
+        requireNonNull(callback, "callback must not be null");
+
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            Log.w(TAG, "executeCreateCredential already canceled");
+            return;
+        }
+
+        ICancellationSignal cancelRemote = null;
+        try {
+            cancelRemote = mService.clearCredentialSession(
+                    new ClearCredentialSessionTransport(executor, callback),
+                    mContext.getOpPackageName());
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
@@ -137,17 +182,30 @@
     private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
         // TODO: listen for cancellation to release callback.
 
+        private final Context mActivityContext;
         private final Executor mExecutor;
         private final OutcomeReceiver<
                 GetCredentialResponse, CredentialManagerException> mCallback;
 
-        private GetCredentialTransport(Executor executor,
+        private GetCredentialTransport(Context activityContext, Executor executor,
                 OutcomeReceiver<GetCredentialResponse, CredentialManagerException> callback) {
+            mActivityContext = activityContext;
             mExecutor = executor;
             mCallback = callback;
         }
 
         @Override
+        public void onPendingIntent(PendingIntent pendingIntent) {
+            try {
+                mActivityContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+            } catch (IntentSender.SendIntentException e) {
+                Log.e(TAG, "startIntentSender() failed for intent:"
+                        + pendingIntent.getIntentSender(), e);
+                // TODO: propagate the error.
+            }
+        }
+
+        @Override
         public void onResponse(GetCredentialResponse response) {
             mExecutor.execute(() -> mCallback.onResult(response));
         }
@@ -162,17 +220,30 @@
     private static class CreateCredentialTransport extends ICreateCredentialCallback.Stub {
         // TODO: listen for cancellation to release callback.
 
+        private final Context mActivityContext;
         private final Executor mExecutor;
         private final OutcomeReceiver<
                 CreateCredentialResponse, CredentialManagerException> mCallback;
 
-        private CreateCredentialTransport(Executor executor,
+        private CreateCredentialTransport(Context activityContext, Executor executor,
                 OutcomeReceiver<CreateCredentialResponse, CredentialManagerException> callback) {
+            mActivityContext = activityContext;
             mExecutor = executor;
             mCallback = callback;
         }
 
         @Override
+        public void onPendingIntent(PendingIntent pendingIntent) {
+            try {
+                mActivityContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+            } catch (IntentSender.SendIntentException e) {
+                Log.e(TAG, "startIntentSender() failed for intent:"
+                        + pendingIntent.getIntentSender(), e);
+                // TODO: propagate the error.
+            }
+        }
+
+        @Override
         public void onResponse(CreateCredentialResponse response) {
             mExecutor.execute(() -> mCallback.onResult(response));
         }
@@ -183,4 +254,29 @@
                     () -> mCallback.onError(new CredentialManagerException(errorCode, message)));
         }
     }
+
+    private static class ClearCredentialSessionTransport
+            extends IClearCredentialSessionCallback.Stub {
+        // TODO: listen for cancellation to release callback.
+
+        private final Executor mExecutor;
+        private final OutcomeReceiver<Void, CredentialManagerException> mCallback;
+
+        private ClearCredentialSessionTransport(Executor executor,
+                OutcomeReceiver<Void, CredentialManagerException> callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onSuccess() {
+            mCallback.onResult(null);
+        }
+
+        @Override
+        public void onError(int errorCode, String message) {
+            mExecutor.execute(
+                    () -> mCallback.onError(new CredentialManagerException(errorCode, message)));
+        }
+    }
 }
diff --git a/core/java/android/credentials/IClearCredentialSessionCallback.aidl b/core/java/android/credentials/IClearCredentialSessionCallback.aidl
new file mode 100644
index 0000000..903e7f5
--- /dev/null
+++ b/core/java/android/credentials/IClearCredentialSessionCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials;
+
+/**
+ * Listener for clearCredentialSession request.
+ *
+ * @hide
+ */
+interface IClearCredentialSessionCallback {
+    oneway void onSuccess();
+    oneway void onError(int errorCode, String message);
+}
\ No newline at end of file
diff --git a/core/java/android/credentials/ICreateCredentialCallback.aidl b/core/java/android/credentials/ICreateCredentialCallback.aidl
index 75620fa..87fd36f 100644
--- a/core/java/android/credentials/ICreateCredentialCallback.aidl
+++ b/core/java/android/credentials/ICreateCredentialCallback.aidl
@@ -16,6 +16,7 @@
 
 package android.credentials;
 
+import android.app.PendingIntent;
 import android.credentials.CreateCredentialResponse;
 
 /**
@@ -24,6 +25,7 @@
  * @hide
  */
 interface ICreateCredentialCallback {
+    oneway void onPendingIntent(in PendingIntent pendingIntent);
     oneway void onResponse(in CreateCredentialResponse response);
     oneway void onError(int errorCode, String message);
 }
\ No newline at end of file
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index dcf7106..35688d7 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -18,6 +18,7 @@
 
 import android.credentials.CreateCredentialRequest;
 import android.credentials.GetCredentialRequest;
+import android.credentials.IClearCredentialSessionCallback;
 import android.credentials.ICreateCredentialCallback;
 import android.credentials.IGetCredentialCallback;
 import android.os.ICancellationSignal;
@@ -29,7 +30,9 @@
  */
 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);
+
+    @nullable ICancellationSignal clearCredentialSession(in IClearCredentialSessionCallback callback, String callingPackage);
 }
diff --git a/core/java/android/credentials/IGetCredentialCallback.aidl b/core/java/android/credentials/IGetCredentialCallback.aidl
index 92e5851..da152ba 100644
--- a/core/java/android/credentials/IGetCredentialCallback.aidl
+++ b/core/java/android/credentials/IGetCredentialCallback.aidl
@@ -16,6 +16,7 @@
 
 package android.credentials;
 
+import android.app.PendingIntent;
 import android.credentials.GetCredentialResponse;
 
 /**
@@ -24,6 +25,7 @@
  * @hide
  */
 interface IGetCredentialCallback {
+    oneway void onPendingIntent(in PendingIntent pendingIntent);
     oneway void onResponse(in GetCredentialResponse response);
     oneway void onError(int errorCode, String message);
 }
\ No newline at end of file
diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java
new file mode 100644
index 0000000..cf5f036
--- /dev/null
+++ b/core/java/android/credentials/ui/BaseDialogResult.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Base dialog result data.
+ *
+ * Returned for simple use cases like cancellation. Can also be subclassed when more information
+ * is needed, e.g. {@link UserSelectionDialogResult}.
+ *
+ * @hide
+ */
+public class BaseDialogResult implements Parcelable {
+    /** Parses and returns a BaseDialogResult from the given resultData. */
+    @Nullable
+    public static BaseDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(EXTRA_BASE_RESULT, BaseDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_BASE_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code BaseDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_BASE_RESULT =
+            "android.credentials.ui.extra.BASE_RESULT";
+
+    /** User intentionally canceled the dialog. */
+    public static final int RESULT_CODE_DIALOG_CANCELED = 0;
+    /**
+     * User made a selection and the dialog finished. The user selection result is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION = 1;
+    /**
+     * The user has acknowledged the consent page rendered for when they first used Credential
+     * Manager on this device.
+     */
+    public static final int RESULT_CODE_CREDENTIAL_MANAGER_CONSENT_ACKNOWLEDGED = 2;
+    /**
+     * The user has acknowledged the consent page rendered for enabling a new provider.
+     * This should only happen during the first time use. The provider info is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_PROVIDER_ENABLED = 3;
+    /**
+     * The user has consented to switching to a new default provider. The provider info is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_DEFAULT_PROVIDER_CHANGED = 4;
+
+    @NonNull
+    private final IBinder mRequestToken;
+
+    public BaseDialogResult(@NonNull IBinder requestToken) {
+        mRequestToken = requestToken;
+    }
+
+    /** Returns the unique identifier for the request that launched the operation. */
+    @NonNull
+    public IBinder getRequestToken() {
+        return mRequestToken;
+    }
+
+    protected BaseDialogResult(@NonNull Parcel in) {
+        IBinder requestToken = in.readStrongBinder();
+        mRequestToken = requestToken;
+        AnnotationValidations.validate(NonNull.class, null, mRequestToken);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mRequestToken);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<BaseDialogResult> CREATOR =
+            new Creator<BaseDialogResult>() {
+        @Override
+        public BaseDialogResult createFromParcel(@NonNull Parcel in) {
+            return new BaseDialogResult(in);
+        }
+
+        @Override
+        public BaseDialogResult[] newArray(int size) {
+            return new BaseDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
new file mode 100644
index 0000000..53ad40d
--- /dev/null
+++ b/core/java/android/credentials/ui/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+/**
+ * Constants for the ui protocol that doesn't fit into other individual data structures.
+ *
+ * @hide
+ */
+public class Constants {
+
+    /**
+    * The intent extra key for the {@code ResultReceiver} object when launching the UX
+    * activities.
+    */
+    public static final String EXTRA_RESULT_RECEIVER =
+            "android.credentials.ui.extra.RESULT_RECEIVER";
+}
diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java
new file mode 100644
index 0000000..0444278
--- /dev/null
+++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Per-provider metadata and entries for the create-credential flow.
+ *
+ * @hide
+ */
+public class CreateCredentialProviderData extends ProviderData implements Parcelable {
+    @NonNull
+    private final List<Entry> mSaveEntries;
+    private final boolean mIsDefaultProvider;
+    @Nullable
+    private final Entry mRemoteEntry;
+
+    public CreateCredentialProviderData(
+            @NonNull String providerFlattenedComponentName, @NonNull List<Entry> saveEntries,
+            boolean isDefaultProvider, @Nullable Entry remoteEntry) {
+        super(providerFlattenedComponentName);
+        mSaveEntries = saveEntries;
+        mIsDefaultProvider = isDefaultProvider;
+        mRemoteEntry = remoteEntry;
+    }
+
+    @NonNull
+    public List<Entry> getSaveEntries() {
+        return mSaveEntries;
+    }
+
+    public boolean isDefaultProvider() {
+        return mIsDefaultProvider;
+    }
+
+    @Nullable
+    public Entry getRemoteEntry() {
+        return mRemoteEntry;
+    }
+
+    protected CreateCredentialProviderData(@NonNull Parcel in) {
+        super(in);
+
+        List<Entry> credentialEntries = new ArrayList<>();
+        in.readTypedList(credentialEntries, Entry.CREATOR);
+        mSaveEntries = credentialEntries;
+        AnnotationValidations.validate(NonNull.class, null, mSaveEntries);
+
+        mIsDefaultProvider = in.readBoolean();
+
+        Entry remoteEntry = in.readTypedObject(Entry.CREATOR);
+        mRemoteEntry = remoteEntry;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeTypedList(mSaveEntries);
+        dest.writeBoolean(isDefaultProvider());
+        dest.writeTypedObject(mRemoteEntry, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<CreateCredentialProviderData> CREATOR =
+            new Creator<CreateCredentialProviderData>() {
+        @Override
+        public CreateCredentialProviderData createFromParcel(@NonNull Parcel in) {
+            return new CreateCredentialProviderData(in);
+        }
+
+        @Override
+        public CreateCredentialProviderData[] newArray(int size) {
+            return new CreateCredentialProviderData[size];
+        }
+    };
+
+    /**
+     * Builder for {@link CreateCredentialProviderData}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private @NonNull String mProviderFlattenedComponentName;
+        private @NonNull List<Entry> mSaveEntries = new ArrayList<>();
+        private boolean mIsDefaultProvider = false;
+        private @Nullable Entry mRemoteEntry = null;
+
+        /** Constructor with required properties. */
+        public Builder(@NonNull String providerFlattenedComponentName) {
+            mProviderFlattenedComponentName = providerFlattenedComponentName;
+        }
+
+        /** Sets the list of save credential entries to be displayed to the user. */
+        @NonNull
+        public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) {
+            mSaveEntries = credentialEntries;
+            return this;
+        }
+
+        /** Sets whether this provider is the user's selected default provider. */
+        @NonNull
+        public Builder setIsDefaultProvider(boolean isDefaultProvider) {
+            mIsDefaultProvider = isDefaultProvider;
+            return this;
+        }
+
+        /** Sets the remote entry of the provider. */
+        @NonNull
+        public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+            mRemoteEntry = remoteEntry;
+            return this;
+        }
+
+        /** Builds a {@link CreateCredentialProviderData}. */
+        @NonNull
+        public CreateCredentialProviderData build() {
+            return new CreateCredentialProviderData(mProviderFlattenedComponentName,
+                    mSaveEntries, mIsDefaultProvider, mRemoteEntry);
+        }
+    }
+}
diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java
new file mode 100644
index 0000000..73c8dbe
--- /dev/null
+++ b/core/java/android/credentials/ui/DisabledProviderData.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Metadata of a disabled provider.
+ *
+ * @hide
+ */
+public class DisabledProviderData extends ProviderData implements Parcelable {
+
+    public DisabledProviderData(
+            @NonNull String providerFlattenedComponentName) {
+        super(providerFlattenedComponentName);
+    }
+
+    protected DisabledProviderData(@NonNull Parcel in) {
+        super(in);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<DisabledProviderData> CREATOR = new Creator<>() {
+                @Override
+                public DisabledProviderData createFromParcel(@NonNull Parcel in) {
+                    return new DisabledProviderData(in);
+                }
+
+                @Override
+                public DisabledProviderData[] newArray(int size) {
+                    return new DisabledProviderData[size];
+                }
+    };
+}
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 122c54a..5c07e6e 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -17,7 +17,10 @@
 package android.credentials.ui;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
 import android.app.slice.Slice;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -30,12 +33,40 @@
  * @hide
  */
 public class Entry implements Parcelable {
-    // TODO: move to jetpack.
+    // TODO: these constants should go to jetpack.
     public static final String VERSION = "v1";
     public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice");
-    public static final String HINT_TITLE = "hint_title";
-    public static final String HINT_SUBTITLE = "hint_subtitle";
-    public static final String HINT_ICON = "hint_icon";
+    // TODO: remove these hint constants and use the credential entry & action ones defined below.
+    public static final String HINT_TITLE = "HINT_TITLE";
+    public static final String HINT_SUBTITLE = "HINT_SUBTITLE";
+    public static final String HINT_ICON = "HINT_ICON";
+    /**
+     * 1. CREDENTIAL ENTRY CONSTANTS
+     */
+    // User profile picture associated with this credential entry.
+    public static final String HINT_PROFILE_ICON = "HINT_PROFILE_ICON";
+    public static final String HINT_CREDENTIAL_TYPE_ICON = "HINT_CREDENTIAL_TYPE_ICON";
+     // The user account name of this provider app associated with this entry.
+     // Note: this is independent from the request app.
+    public static final String HINT_USER_PROVIDER_ACCOUNT_NAME = "HINT_USER_PROVIDER_ACCOUNT_NAME";
+    public static final String HINT_PASSWORD_COUNT = "HINT_PASSWORD_COUNT";
+    public static final String HINT_PASSKEY_COUNT = "HINT_PASSKEY_COUNT";
+    public static final String HINT_TOTAL_CREDENTIAL_COUNT = "HINT_TOTAL_CREDENTIAL_COUNT";
+    public static final String HINT_LAST_USED_TIME_MILLIS = "HINT_LAST_USED_TIME_MILLIS";
+    /** Below are only available for get flows. */
+    public static final String HINT_NOTE = "HINT_NOTE";
+    public static final String HINT_USER_NAME = "HINT_USER_NAME";
+    public static final String HINT_CREDENTIAL_TYPE_DISPLAY_NAME =
+            "HINT_CREDENTIAL_TYPE_DISPLAY_NAME";
+    public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME";
+    public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE";
+
+    /**
+     * 2. ACTION CONSTANTS
+     */
+    public static final String HINT_ACTION_TITLE = "HINT_ACTION_TITLE";
+    public static final String HINT_ACTION_SUBTEXT = "HINT_ACTION_SUBTEXT";
+    public static final String HINT_ACTION_ICON = "HINT_ACTION_ICON";
 
     /**
     * The intent extra key for the action chip {@code Entry} list when launching the UX activities.
@@ -55,32 +86,63 @@
     public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION =
             "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
 
-    // TODO: may be changed to other type depending on the service implementation.
-    private final int mId;
+    @NonNull private final String mKey;
+    @NonNull private final String mSubkey;
+    @Nullable private PendingIntent mPendingIntent;
+    @Nullable private Intent mFrameworkExtrasIntent;
 
     @NonNull
     private final Slice mSlice;
 
     protected Entry(@NonNull Parcel in) {
-        int entryId = in.readInt();
+        String key = in.readString8();
+        String subkey = in.readString8();
         Slice slice = Slice.CREATOR.createFromParcel(in);
 
-        mId = entryId;
+        mKey = key;
+        AnnotationValidations.validate(NonNull.class, null, mKey);
+        mSubkey = subkey;
+        AnnotationValidations.validate(NonNull.class, null, mSubkey);
         mSlice = slice;
         AnnotationValidations.validate(NonNull.class, null, mSlice);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR);
     }
 
-    public Entry(int id, @NonNull Slice slice) {
-        mId = id;
+    /** Constructor to be used for an entry that does not require further activities
+     * to be invoked when selected.
+     */
+    public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) {
+        mKey = key;
+        mSubkey = subkey;
         mSlice = slice;
     }
 
+    /** Constructor to be used for an entry that requires a pending intent to be invoked
+     * when clicked.
+     */
+    public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
+            @NonNull PendingIntent pendingIntent, @Nullable Intent intent) {
+        this(key, subkey, slice);
+        mPendingIntent = pendingIntent;
+        mFrameworkExtrasIntent = intent;
+    }
+
     /**
-    * Returns the id of this entry that's unique within the context of the CredentialManager
+    * Returns the identifier of this entry that's unique within the context of the CredentialManager
     * request.
     */
-    public int getEntryId() {
-        return mId;
+    @NonNull
+    public String getKey() {
+        return mKey;
+    }
+
+    /**
+     * Returns the sub-identifier of this entry that's unique within the context of the {@code key}.
+     */
+    @NonNull
+    public String getSubkey() {
+        return mSubkey;
     }
 
     /**
@@ -91,10 +153,23 @@
         return mSlice;
     }
 
+    @Nullable
+    public PendingIntent getPendingIntent() {
+        return mPendingIntent;
+    }
+
+    @Nullable
+    public Intent getFrameworkExtrasIntent() {
+        return mFrameworkExtrasIntent;
+    }
+
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeInt(mId);
+        dest.writeString8(mKey);
+        dest.writeString8(mSubkey);
         mSlice.writeToParcel(dest, flags);
+        mPendingIntent.writeToParcel(dest, flags);
+        mFrameworkExtrasIntent.writeToParcel(dest, flags);
     }
 
     @Override
diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java
new file mode 100644
index 0000000..834f9825
--- /dev/null
+++ b/core/java/android/credentials/ui/GetCredentialProviderData.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Per-provider metadata and entries for the get-credential flow.
+ *
+ * @hide
+ */
+public class GetCredentialProviderData extends ProviderData implements Parcelable {
+    @NonNull
+    private final List<Entry> mCredentialEntries;
+    @NonNull
+    private final List<Entry> mActionChips;
+    @Nullable
+    private final Entry mAuthenticationEntry;
+    @Nullable
+    private final Entry mRemoteEntry;
+
+    public GetCredentialProviderData(
+            @NonNull String providerFlattenedComponentName, @NonNull List<Entry> credentialEntries,
+            @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
+            @Nullable Entry remoteEntry) {
+        super(providerFlattenedComponentName);
+        mCredentialEntries = credentialEntries;
+        mActionChips = actionChips;
+        mAuthenticationEntry = authenticationEntry;
+        mRemoteEntry = remoteEntry;
+    }
+
+    @NonNull
+    public List<Entry> getCredentialEntries() {
+        return mCredentialEntries;
+    }
+
+    @NonNull
+    public List<Entry> getActionChips() {
+        return mActionChips;
+    }
+
+    @Nullable
+    public Entry getAuthenticationEntry() {
+        return mAuthenticationEntry;
+    }
+
+    @Nullable
+    public Entry getRemoteEntry() {
+        return mRemoteEntry;
+    }
+
+    protected GetCredentialProviderData(@NonNull Parcel in) {
+        super(in);
+
+        List<Entry> credentialEntries = new ArrayList<>();
+        in.readTypedList(credentialEntries, Entry.CREATOR);
+        mCredentialEntries = credentialEntries;
+        AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
+
+        List<Entry> actionChips  = new ArrayList<>();
+        in.readTypedList(actionChips, Entry.CREATOR);
+        mActionChips = actionChips;
+        AnnotationValidations.validate(NonNull.class, null, mActionChips);
+
+        Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
+        mAuthenticationEntry = authenticationEntry;
+
+        Entry remoteEntry = in.readTypedObject(Entry.CREATOR);
+        mRemoteEntry = remoteEntry;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeTypedList(mCredentialEntries);
+        dest.writeTypedList(mActionChips);
+        dest.writeTypedObject(mAuthenticationEntry, flags);
+        dest.writeTypedObject(mRemoteEntry, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<GetCredentialProviderData> CREATOR =
+            new Creator<GetCredentialProviderData>() {
+        @Override
+        public GetCredentialProviderData createFromParcel(@NonNull Parcel in) {
+            return new GetCredentialProviderData(in);
+        }
+
+        @Override
+        public GetCredentialProviderData[] newArray(int size) {
+            return new GetCredentialProviderData[size];
+        }
+    };
+
+    /**
+     * Builder for {@link GetCredentialProviderData}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private @NonNull String mProviderFlattenedComponentName;
+        private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
+        private @NonNull List<Entry> mActionChips = new ArrayList<>();
+        private @Nullable Entry mAuthenticationEntry = null;
+        private @Nullable Entry mRemoteEntry = null;
+
+        /** Constructor with required properties. */
+        public Builder(@NonNull String providerFlattenedComponentName) {
+            mProviderFlattenedComponentName = providerFlattenedComponentName;
+        }
+
+        /** Sets the list of save / get credential entries to be displayed to the user. */
+        @NonNull
+        public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+            mCredentialEntries = credentialEntries;
+            return this;
+        }
+
+        /** Sets the list of action chips to be displayed to the user. */
+        @NonNull
+        public Builder setActionChips(@NonNull List<Entry> actionChips) {
+            mActionChips = actionChips;
+            return this;
+        }
+
+        /** Sets the authentication entry to be displayed to the user. */
+        @NonNull
+        public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
+            mAuthenticationEntry = authenticationEntry;
+            return this;
+        }
+
+        /** Sets the remote entry to be displayed to the user. */
+        @NonNull
+        public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+            mRemoteEntry = remoteEntry;
+            return this;
+        }
+
+        /** Builds a {@link GetCredentialProviderData}. */
+        @NonNull
+        public GetCredentialProviderData build() {
+            return new GetCredentialProviderData(mProviderFlattenedComponentName,
+                    mCredentialEntries, mActionChips, mAuthenticationEntry, mRemoteEntry);
+        }
+    }
+}
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
new file mode 100644
index 0000000..b608e65
--- /dev/null
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+
+import java.util.ArrayList;
+
+/**
+ * Helpers for generating the intents and related extras parameters to launch the UI activities.
+ *
+ * @hide
+ */
+public class IntentFactory {
+    /** Generate a new launch intent to the . */
+    public static Intent newIntent(
+            RequestInfo requestInfo,
+            ArrayList<ProviderData> enabledProviderDataList,
+            ArrayList<DisabledProviderData> disabledProviderDataList,
+            ResultReceiver resultReceiver) {
+        Intent intent = new Intent();
+        ComponentName componentName = ComponentName.unflattenFromString(
+                Resources.getSystem().getString(
+                        com.android.internal.R.string.config_credentialManagerDialogComponent));
+        intent.setComponent(componentName);
+
+        intent.putParcelableArrayListExtra(
+                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
+        intent.putParcelableArrayListExtra(
+                ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
+        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
+        intent.putExtra(Constants.EXTRA_RESULT_RECEIVER,
+                toIpcFriendlyResultReceiver(resultReceiver));
+
+        return intent;
+    }
+
+    /**
+    * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+    * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+    * unmarshall.
+    */
+    private static <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
+            T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
+    }
+
+    private IntentFactory() {}
+}
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 18e6ba4..eeaeb46 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -17,111 +17,61 @@
 package android.credentials.ui;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import com.android.internal.util.AnnotationValidations;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /**
- * Holds metadata and credential entries for a single provider.
+ * Super class for data structures that hold metadata and credential entries for a single provider.
  *
  * @hide
  */
-public class ProviderData implements Parcelable {
+public abstract class ProviderData implements Parcelable {
 
     /**
-     * The intent extra key for the list of {@code ProviderData} when launching the UX
-     * activities.
+     * The intent extra key for the list of {@code ProviderData} from active providers when
+     * launching the UX activities.
      */
-    public static final String EXTRA_PROVIDER_DATA_LIST =
-            "android.credentials.ui.extra.PROVIDER_DATA_LIST";
+    public static final String EXTRA_ENABLED_PROVIDER_DATA_LIST =
+            "android.credentials.ui.extra.ENABLED_PROVIDER_DATA_LIST";
+    /**
+     * The intent extra key for the list of {@code ProviderData} from disabled providers when
+     * launching the UX activities.
+     */
+    public static final String EXTRA_DISABLED_PROVIDER_DATA_LIST =
+            "android.credentials.ui.extra.DISABLED_PROVIDER_DATA_LIST";
 
     @NonNull
-    private final String mPackageName;
-    @NonNull
-    private final List<Entry> mCredentialEntries;
-    @NonNull
-    private final List<Entry> mActionChips;
-    @Nullable
-    private final Entry mAuthenticationEntry;
+    private final String mProviderFlattenedComponentName;
 
     public ProviderData(
-            @NonNull String packageName,
-            @NonNull List<Entry> credentialEntries,
-            @NonNull List<Entry> actionChips,
-            @Nullable Entry authenticationEntry) {
-        mPackageName = packageName;
-        mCredentialEntries = credentialEntries;
-        mActionChips = actionChips;
-        mAuthenticationEntry = authenticationEntry;
+            @NonNull String providerFlattenedComponentName) {
+        mProviderFlattenedComponentName = providerFlattenedComponentName;
     }
 
-    /** Returns the provider package name. */
+    /**
+     * Returns provider component name.
+     * It also serves as the unique identifier for this provider.
+     */
     @NonNull
-    public String getPackageName() {
-        return mPackageName;
-    }
-
-    @NonNull
-    public List<Entry> getCredentialEntries() {
-        return mCredentialEntries;
-    }
-
-    @NonNull
-    public List<Entry> getActionChips() {
-        return mActionChips;
-    }
-
-    @Nullable
-    public Entry getAuthenticationEntry() {
-        return mAuthenticationEntry;
+    public String getProviderFlattenedComponentName() {
+        return mProviderFlattenedComponentName;
     }
 
     protected ProviderData(@NonNull Parcel in) {
-        String packageName = in.readString8();
-        mPackageName = packageName;
-        AnnotationValidations.validate(NonNull.class, null, mPackageName);
-
-        List<Entry> credentialEntries = new ArrayList<>();
-        in.readTypedList(credentialEntries, Entry.CREATOR);
-        mCredentialEntries = credentialEntries;
-        AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
-
-        List<Entry> actionChips  = new ArrayList<>();
-        in.readTypedList(actionChips, Entry.CREATOR);
-        mActionChips = actionChips;
-        AnnotationValidations.validate(NonNull.class, null, mActionChips);
-
-        Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
-        mAuthenticationEntry = authenticationEntry;
+        String providerFlattenedComponentName = in.readString8();
+        mProviderFlattenedComponentName = providerFlattenedComponentName;
+        AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName);
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString8(mPackageName);
-        dest.writeTypedList(mCredentialEntries);
-        dest.writeTypedList(mActionChips);
-        dest.writeTypedObject(mAuthenticationEntry, flags);
+        dest.writeString8(mProviderFlattenedComponentName);
     }
 
     @Override
     public int describeContents() {
         return 0;
     }
-
-    public static final @NonNull Creator<ProviderData> CREATOR = new Creator<ProviderData>() {
-        @Override
-        public ProviderData createFromParcel(@NonNull Parcel in) {
-            return new ProviderData(in);
-        }
-
-        @Override
-        public ProviderData[] newArray(int size) {
-            return new ProviderData[size];
-        }
-    };
 }
diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java
new file mode 100644
index 0000000..9d1be20
--- /dev/null
+++ b/core/java/android/credentials/ui/ProviderDialogResult.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link
+ * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}.
+ *
+ * @hide
+ */
+public class ProviderDialogResult extends BaseDialogResult implements Parcelable {
+    /** Parses and returns a ProviderDialogResult from the given resultData. */
+    @Nullable
+    public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(
+            @NonNull ProviderDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_PROVIDER_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code ProviderDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_PROVIDER_RESULT =
+            "android.credentials.ui.extra.PROVIDER_RESULT";
+
+    @NonNull
+    private final String mProviderId;
+
+    public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) {
+        super(requestToken);
+        mProviderId = providerId;
+    }
+
+    @NonNull
+    public String getProviderId() {
+        return mProviderId;
+    }
+
+    protected ProviderDialogResult(@NonNull Parcel in) {
+        super(in);
+        String providerId = in.readString8();
+        mProviderId = providerId;
+        AnnotationValidations.validate(NonNull.class, null, mProviderId);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString8(mProviderId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<ProviderDialogResult> CREATOR =
+            new Creator<ProviderDialogResult>() {
+        @Override
+        public ProviderDialogResult createFromParcel(@NonNull Parcel in) {
+            return new ProviderDialogResult(in);
+        }
+
+        @Override
+        public ProviderDialogResult[] newArray(int size) {
+            return new ProviderDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java
new file mode 100644
index 0000000..420956f
--- /dev/null
+++ b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java
@@ -0,0 +1,79 @@
+/*
+ * 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.credentials.ui;
+
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Response from a provider's pending intent
+ *
+ * @hide
+ */
+public final class ProviderPendingIntentResponse implements Parcelable {
+    private final int mResultCode;
+    @Nullable
+    private final Intent mResultData;
+
+    public ProviderPendingIntentResponse(int resultCode, @Nullable Intent resultData) {
+        mResultCode = resultCode;
+        mResultData = resultData;
+    }
+
+    protected ProviderPendingIntentResponse(Parcel in) {
+        mResultCode = in.readInt();
+        mResultData = in.readTypedObject(Intent.CREATOR);
+    }
+
+    public static final Creator<ProviderPendingIntentResponse> CREATOR =
+            new Creator<ProviderPendingIntentResponse>() {
+                @Override
+                public ProviderPendingIntentResponse createFromParcel(Parcel in) {
+                    return new ProviderPendingIntentResponse(in);
+                }
+
+                @Override
+                public ProviderPendingIntentResponse[] newArray(int size) {
+                    return new ProviderPendingIntentResponse[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeSerializable(mResultCode);
+        dest.writeTypedObject(mResultData, flags);
+    }
+
+    /** Returns the result code associated with this pending intent activity result. */
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /** Returns the result data associated with this pending intent activity result. */
+    @NonNull public Intent getResultData() {
+        return mResultData;
+    }
+}
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index 5de6d73..c3937b6 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -17,12 +17,19 @@
 package android.credentials.ui;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.credentials.CreateCredentialRequest;
+import android.credentials.GetCredentialRequest;
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import com.android.internal.util.AnnotationValidations;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Contains information about the request that initiated this UX flow.
  *
@@ -36,30 +43,51 @@
      */
     public static final @NonNull String EXTRA_REQUEST_INFO =
             "android.credentials.ui.extra.REQUEST_INFO";
-    /**
-     * The intent extra key for the {@code ResultReceiver} object when launching the UX
-     * activities.
-     */
-    public static final @NonNull String EXTRA_RESULT_RECEIVER =
-            "android.credentials.ui.extra.RESULT_RECEIVER";
 
     /** Type value for an executeGetCredential request. */
     public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";
     /** Type value for an executeCreateCredential request. */
     public static final @NonNull String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(value = { TYPE_GET, TYPE_CREATE })
+    public @interface RequestType {}
+
     @NonNull
     private final IBinder mToken;
 
+    @Nullable
+    private final CreateCredentialRequest mCreateCredentialRequest;
+
+    @Nullable
+    private final GetCredentialRequest mGetCredentialRequest;
+
     @NonNull
+    @RequestType
     private final String mType;
 
     private final boolean mIsFirstUsage;
 
-    public RequestInfo(@NonNull IBinder token, @NonNull String type, boolean isFirstUsage) {
-        mToken = token;
-        mType = type;
-        mIsFirstUsage = isFirstUsage;
+    @NonNull
+    private final String mAppPackageName;
+
+    /** Creates new {@code RequestInfo} for a create-credential flow. */
+    public static RequestInfo newCreateRequestInfo(
+            @NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest,
+            boolean isFirstUsage, @NonNull String appPackageName) {
+        return new RequestInfo(
+                token, TYPE_CREATE, isFirstUsage, appPackageName,
+                createCredentialRequest, null);
+    }
+
+    /** Creates new {@code RequestInfo} for a get-credential flow. */
+    public static RequestInfo newGetRequestInfo(
+            @NonNull IBinder token, @NonNull GetCredentialRequest getCredentialRequest,
+            boolean isFirstUsage, @NonNull String appPackageName) {
+        return new RequestInfo(
+                token, TYPE_GET, isFirstUsage, appPackageName,
+                null, getCredentialRequest);
     }
 
     /** Returns the request token matching the user request. */
@@ -70,6 +98,7 @@
 
     /** Returns the request type. */
     @NonNull
+    @RequestType
     public String getType() {
         return mType;
     }
@@ -84,16 +113,61 @@
         return mIsFirstUsage;
     }
 
+    /** Returns the display name of the app that made this request. */
+    @NonNull
+    public String getAppPackageName() {
+        return mAppPackageName;
+    }
+
+    /**
+     * Returns the non-null CreateCredentialRequest when the type of the request is {@link
+     * #TYPE_CREATE}, or null otherwise.
+     */
+    @Nullable
+    public CreateCredentialRequest getCreateCredentialRequest() {
+        return mCreateCredentialRequest;
+    }
+
+    /**
+     * Returns the non-null GetCredentialRequest when the type of the request is {@link
+     * #TYPE_GET}, or null otherwise.
+     */
+    @Nullable
+    public GetCredentialRequest getGetCredentialRequest() {
+        return mGetCredentialRequest;
+    }
+
+    private RequestInfo(@NonNull IBinder token, @NonNull @RequestType String type,
+            boolean isFirstUsage, @NonNull String appPackageName,
+            @Nullable CreateCredentialRequest createCredentialRequest,
+            @Nullable GetCredentialRequest getCredentialRequest) {
+        mToken = token;
+        mType = type;
+        mIsFirstUsage = isFirstUsage;
+        mAppPackageName = appPackageName;
+        mCreateCredentialRequest = createCredentialRequest;
+        mGetCredentialRequest = getCredentialRequest;
+    }
+
     protected RequestInfo(@NonNull Parcel in) {
         IBinder token = in.readStrongBinder();
         String type = in.readString8();
         boolean isFirstUsage = in.readBoolean();
+        String appPackageName = in.readString8();
+        CreateCredentialRequest createCredentialRequest =
+                in.readTypedObject(CreateCredentialRequest.CREATOR);
+        GetCredentialRequest getCredentialRequest =
+                in.readTypedObject(GetCredentialRequest.CREATOR);
 
         mToken = token;
         AnnotationValidations.validate(NonNull.class, null, mToken);
         mType = type;
         AnnotationValidations.validate(NonNull.class, null, mType);
         mIsFirstUsage = isFirstUsage;
+        mAppPackageName = appPackageName;
+        AnnotationValidations.validate(NonNull.class, null, mAppPackageName);
+        mCreateCredentialRequest = createCredentialRequest;
+        mGetCredentialRequest = getCredentialRequest;
     }
 
     @Override
@@ -101,6 +175,9 @@
         dest.writeStrongBinder(mToken);
         dest.writeString8(mType);
         dest.writeBoolean(mIsFirstUsage);
+        dest.writeString8(mAppPackageName);
+        dest.writeTypedObject(mCreateCredentialRequest, flags);
+        dest.writeTypedObject(mGetCredentialRequest, flags);
     }
 
     @Override
diff --git a/core/java/android/credentials/ui/UserSelectionDialogResult.java b/core/java/android/credentials/ui/UserSelectionDialogResult.java
new file mode 100644
index 0000000..0e8e7b6
--- /dev/null
+++ b/core/java/android/credentials/ui/UserSelectionDialogResult.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Result data matching {@link BaseDialogResult#RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION}.
+ *
+ * @hide
+ */
+public class UserSelectionDialogResult extends BaseDialogResult implements Parcelable {
+    /** Parses and returns a UserSelectionDialogResult from the given resultData. */
+    @Nullable
+    public static UserSelectionDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(
+            EXTRA_USER_SELECTION_RESULT, UserSelectionDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(
+            @NonNull UserSelectionDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_USER_SELECTION_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code UserSelectionDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_USER_SELECTION_RESULT =
+            "android.credentials.ui.extra.USER_SELECTION_RESULT";
+
+    @NonNull private final String mProviderId;
+    @NonNull private final String mEntryKey;
+    @NonNull private final String mEntrySubkey;
+    @Nullable private ProviderPendingIntentResponse mProviderPendingIntentResponse;
+
+    public UserSelectionDialogResult(
+            @NonNull IBinder requestToken, @NonNull String providerId,
+            @NonNull String entryKey, @NonNull String entrySubkey) {
+        super(requestToken);
+        mProviderId = providerId;
+        mEntryKey = entryKey;
+        mEntrySubkey = entrySubkey;
+    }
+
+    public UserSelectionDialogResult(
+            @NonNull IBinder requestToken, @NonNull String providerId,
+            @NonNull String entryKey, @NonNull String entrySubkey,
+            @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
+        super(requestToken);
+        mProviderId = providerId;
+        mEntryKey = entryKey;
+        mEntrySubkey = entrySubkey;
+        mProviderPendingIntentResponse = providerPendingIntentResponse;
+    }
+
+    /** Returns provider package name whose entry was selected by the user. */
+    @NonNull
+    public String getProviderId() {
+        return mProviderId;
+    }
+
+    /** Returns the key of the visual entry that the user selected. */
+    @NonNull
+    public String getEntryKey() {
+        return mEntryKey;
+    }
+
+    /** Returns the subkey of the visual entry that the user selected. */
+    @NonNull
+    public String getEntrySubkey() {
+        return mEntrySubkey;
+    }
+
+    /** Returns the pending intent response from the provider. */
+    @Nullable
+    public ProviderPendingIntentResponse getPendingIntentProviderResponse() {
+        return mProviderPendingIntentResponse;
+    }
+
+    protected UserSelectionDialogResult(@NonNull Parcel in) {
+        super(in);
+        String providerId = in.readString8();
+        String entryKey = in.readString8();
+        String entrySubkey = in.readString8();
+
+        mProviderId = providerId;
+        AnnotationValidations.validate(NonNull.class, null, mProviderId);
+        mEntryKey = entryKey;
+        AnnotationValidations.validate(NonNull.class, null, mEntryKey);
+        mEntrySubkey = entrySubkey;
+        AnnotationValidations.validate(NonNull.class, null, mEntrySubkey);
+        mProviderPendingIntentResponse = in.readTypedObject(ProviderPendingIntentResponse.CREATOR);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString8(mProviderId);
+        dest.writeString8(mEntryKey);
+        dest.writeString8(mEntrySubkey);
+        dest.writeTypedObject(mProviderPendingIntentResponse, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<UserSelectionDialogResult> CREATOR =
+            new Creator<UserSelectionDialogResult>() {
+        @Override
+        public UserSelectionDialogResult createFromParcel(@NonNull Parcel in) {
+            return new UserSelectionDialogResult(in);
+        }
+
+        @Override
+        public UserSelectionDialogResult[] newArray(int size) {
+            return new UserSelectionDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionResult.java
deleted file mode 100644
index 0927fb8..0000000
--- a/core/java/android/credentials/ui/UserSelectionResult.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.credentials.ui;
-
-import android.annotation.NonNull;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.AnnotationValidations;
-
-/**
- * User selection result information of a UX flow.
- *
- * Returned as part of the activity result intent data when the user dialog completes
- * successfully.
- *
- * @hide
- */
-public class UserSelectionResult implements Parcelable {
-
-    /**
-    * The intent extra key for the {@code UserSelectionResult} object when the credential selector
-    * activity finishes.
-    */
-    public static final String EXTRA_USER_SELECTION_RESULT =
-            "android.credentials.ui.extra.USER_SELECTION_RESULT";
-
-    @NonNull
-    private final IBinder mRequestToken;
-
-    // TODO: consider switching to string or other types, depending on the service implementation.
-    private final int mEntryId;
-
-    public UserSelectionResult(@NonNull IBinder requestToken, int entryId) {
-        mRequestToken = requestToken;
-        mEntryId = entryId;
-    }
-
-    /** Returns token of the app request that initiated this user dialog. */
-    @NonNull
-    public IBinder getRequestToken() {
-        return mRequestToken;
-    }
-
-    /** Returns the id of the visual entry that the user selected. */
-    public int geEntryId() {
-        return mEntryId;
-    }
-
-    protected UserSelectionResult(@NonNull Parcel in) {
-        IBinder requestToken = in.readStrongBinder();
-        int entryId = in.readInt();
-
-        mRequestToken = requestToken;
-        AnnotationValidations.validate(NonNull.class, null, mRequestToken);
-        mEntryId = entryId;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeStrongBinder(mRequestToken);
-        dest.writeInt(mEntryId);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    public static final @NonNull Creator<UserSelectionResult> CREATOR =
-            new Creator<UserSelectionResult>() {
-        @Override
-        public UserSelectionResult createFromParcel(@NonNull Parcel in) {
-            return new UserSelectionResult(in);
-        }
-
-        @Override
-        public UserSelectionResult[] newArray(int size) {
-            return new UserSelectionResult[size];
-        }
-    };
-}
diff --git a/core/java/android/graphics/fonts/FontManager.java b/core/java/android/graphics/fonts/FontManager.java
index 24480e9..beb7f36 100644
--- a/core/java/android/graphics/fonts/FontManager.java
+++ b/core/java/android/graphics/fonts/FontManager.java
@@ -198,6 +198,15 @@
      */
     public static final int RESULT_ERROR_INVALID_XML = -10007;
 
+    /**
+     * Indicates a failure due to invalid debug certificate file.
+     *
+     * This error code is only used with the shell command interaction.
+     *
+     * @hide
+     */
+    public static final int RESULT_ERROR_INVALID_DEBUG_CERTIFICATE = -10008;
+
     private FontManager(@NonNull IFontManager iFontManager) {
         mIFontManager = iFontManager;
     }
diff --git a/core/java/android/graphics/fonts/FontUpdateRequest.java b/core/java/android/graphics/fonts/FontUpdateRequest.java
index dae09f0..510985c 100644
--- a/core/java/android/graphics/fonts/FontUpdateRequest.java
+++ b/core/java/android/graphics/fonts/FontUpdateRequest.java
@@ -24,7 +24,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 import android.text.FontConfig;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/hardware/CameraInfo.java b/core/java/android/hardware/CameraInfo.java
index 072be50..41ef6aa 100644
--- a/core/java/android/hardware/CameraInfo.java
+++ b/core/java/android/hardware/CameraInfo.java
@@ -60,4 +60,4 @@
             return new CameraInfo[size];
         }
     };
-};
+}
diff --git a/core/java/android/hardware/CameraStatus.java b/core/java/android/hardware/CameraStatus.java
index 874af29..fa35efb 100644
--- a/core/java/android/hardware/CameraStatus.java
+++ b/core/java/android/hardware/CameraStatus.java
@@ -68,4 +68,4 @@
             return new CameraStatus[size];
         }
     };
-};
+}
diff --git a/core/java/android/hardware/CameraStreamStats.java b/core/java/android/hardware/CameraStreamStats.java
index 3952467..aed5a12 100644
--- a/core/java/android/hardware/CameraStreamStats.java
+++ b/core/java/android/hardware/CameraStreamStats.java
@@ -16,6 +16,7 @@
 package android.hardware;
 
 import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -50,6 +51,7 @@
     private long[] mHistogramCounts;
     private long mDynamicRangeProfile;
     private long mStreamUseCase;
+    private int mColorSpace;
 
     private static final String TAG = "CameraStreamStats";
 
@@ -68,12 +70,13 @@
         mHistogramType = HISTOGRAM_TYPE_UNKNOWN;
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
     }
 
     public CameraStreamStats(int width, int height, int format, float maxPreviewFps,
             int dataSpace, long usage, long requestCount, long errorCount,
             int startLatencyMs, int maxHalBuffers, int maxAppBuffers, long dynamicRangeProfile,
-            long streamUseCase) {
+            long streamUseCase, int colorSpace) {
         mWidth = width;
         mHeight = height;
         mFormat = format;
@@ -88,6 +91,7 @@
         mHistogramType = HISTOGRAM_TYPE_UNKNOWN;
         mDynamicRangeProfile = dynamicRangeProfile;
         mStreamUseCase = streamUseCase;
+        mColorSpace = colorSpace;
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<CameraStreamStats> CREATOR =
@@ -136,6 +140,7 @@
         dest.writeLongArray(mHistogramCounts);
         dest.writeLong(mDynamicRangeProfile);
         dest.writeLong(mStreamUseCase);
+        dest.writeInt(mColorSpace);
     }
 
     public void readFromParcel(Parcel in) {
@@ -155,6 +160,7 @@
         mHistogramCounts = in.createLongArray();
         mDynamicRangeProfile = in.readLong();
         mStreamUseCase = in.readLong();
+        mColorSpace = in.readInt();
     }
 
     public int getWidth() {
@@ -217,6 +223,10 @@
         return mDynamicRangeProfile;
     }
 
+    public int getColorSpace() {
+        return mColorSpace;
+    }
+
     public long getStreamUseCase() {
         return mStreamUseCase;
     }
diff --git a/core/java/android/hardware/SensorPrivacyManager.java b/core/java/android/hardware/SensorPrivacyManager.java
index 99b58c9..535b551 100644
--- a/core/java/android/hardware/SensorPrivacyManager.java
+++ b/core/java/android/hardware/SensorPrivacyManager.java
@@ -73,6 +73,13 @@
             + ".extra.all_sensors";
 
     /**
+     * An extra containing the sensor type
+     * @hide
+     */
+    public static final String EXTRA_TOGGLE_TYPE = SensorPrivacyManager.class.getName()
+            + ".extra.toggle_type";
+
+    /**
      * Sensor constants which are used in {@link SensorPrivacyManager}
      */
     public static class Sensors {
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 2eb8cb9..6b044fc 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -639,5 +639,24 @@
             }
         }
     }
+
+    /**
+     * Notifies AuthService that keyguard has been dismissed for the given userId.
+     *
+     * @param userId
+     * @param hardwareAuthToken
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void resetLockout(int userId, byte[] hardwareAuthToken) {
+        if (mService != null) {
+            try {
+                mService.resetLockout(userId, hardwareAuthToken);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+    }
 }
 
diff --git a/core/java/android/hardware/biometrics/BiometricStateListener.java b/core/java/android/hardware/biometrics/BiometricStateListener.java
index b167cc6..71b7850 100644
--- a/core/java/android/hardware/biometrics/BiometricStateListener.java
+++ b/core/java/android/hardware/biometrics/BiometricStateListener.java
@@ -73,4 +73,5 @@
      */
     public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {
     }
+
 }
diff --git a/core/java/android/hardware/biometrics/CryptoObject.java b/core/java/android/hardware/biometrics/CryptoObject.java
index d415706..267ef36 100644
--- a/core/java/android/hardware/biometrics/CryptoObject.java
+++ b/core/java/android/hardware/biometrics/CryptoObject.java
@@ -118,4 +118,4 @@
         }
         return AndroidKeyStoreProvider.getKeyStoreOperationHandle(mCrypto);
     }
-};
+}
diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl
index 7c3cc10b..c2e5c0b 100644
--- a/core/java/android/hardware/biometrics/IAuthService.aidl
+++ b/core/java/android/hardware/biometrics/IAuthService.aidl
@@ -79,6 +79,9 @@
     void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId, int userId,
             in byte[] hardwareAuthToken);
 
+    // See documentation in BiometricManager.
+    void resetLockout(int userId, in byte[] hardwareAuthToken);
+
     // Provides a localized string that may be used as the label for a button that invokes
     // BiometricPrompt.
     CharSequence getButtonLabel(int userId, String opPackageName, int authenticators);
diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl
index 08f9ed6..c88af5a 100644
--- a/core/java/android/hardware/biometrics/IBiometricService.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricService.aidl
@@ -91,6 +91,10 @@
     void resetLockoutTimeBound(IBinder token, String opPackageName, int fromSensorId, int userId,
             in byte[] hardwareAuthToken);
 
+    // See documentation in BiometricManager.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void resetLockout(int userId, in byte[] hardwareAuthToken);
+
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     int getCurrentStrength(int sensorId);
 
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 8873807..f561278 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1286,6 +1286,46 @@
             new Key<android.hardware.camera2.params.HighSpeedVideoConfiguration[]>("android.control.availableHighSpeedVideoConfigurationsMaximumResolution", android.hardware.camera2.params.HighSpeedVideoConfiguration[].class);
 
     /**
+     * <p>List of available settings overrides supported by the camera device that can
+     * be used to speed up certain controls.</p>
+     * <p>When not all controls within a CaptureRequest are required to take effect
+     * at the same time on the outputs, the camera device may apply certain request keys sooner
+     * to improve latency. This list contains such supported settings overrides. Each settings
+     * override corresponds to a set of CaptureRequest keys that can be sped up when applying.</p>
+     * <p>A supported settings override can be passed in via
+     * {@link android.hardware.camera2.CaptureRequest#CONTROL_SETTINGS_OVERRIDE }, and the
+     * CaptureRequest keys corresponding to the override are applied as soon as possible, not
+     * bound by per-frame synchronization. See {@link CaptureRequest#CONTROL_SETTINGS_OVERRIDE android.control.settingsOverride} for the
+     * CaptureRequest keys for each override.</p>
+     * <p>OFF is always included in this list.</p>
+     * <p><b>Range of valid values:</b><br>
+     * Any value listed in {@link CaptureRequest#CONTROL_SETTINGS_OVERRIDE android.control.settingsOverride}</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_SETTINGS_OVERRIDE
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<int[]> CONTROL_AVAILABLE_SETTINGS_OVERRIDES =
+            new Key<int[]>("android.control.availableSettingsOverrides", int[].class);
+
+    /**
+     * <p>Whether the camera device supports {@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}.</p>
+     * <p>Will be <code>false</code> if auto-framing is not available.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_AUTOFRAMING
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Boolean> CONTROL_AUTOFRAMING_AVAILABLE =
+            new Key<Boolean>("android.control.autoframingAvailable", boolean.class);
+
+    /**
      * <p>List of edge enhancement modes for {@link CaptureRequest#EDGE_MODE android.edge.mode} that are supported by this camera
      * device.</p>
      * <p>Full-capability camera devices must always support OFF; camera devices that support
@@ -2224,6 +2264,7 @@
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING REMOSAIC_REPROCESSING}</li>
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT DYNAMIC_RANGE_TEN_BIT}</li>
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE STREAM_USE_CASE}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES COLOR_SPACE_PROFILES}</li>
      * </ul>
      *
      * <p>This key is available on all devices.</p>
@@ -2249,6 +2290,7 @@
      * @see #REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING
      * @see #REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
      * @see #REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE
+     * @see #REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES
      */
     @PublicKey
     @NonNull
@@ -2473,6 +2515,82 @@
             new Key<Long>("android.request.recommendedTenBitDynamicRangeProfile", long.class);
 
     /**
+     * <p>An interface for querying the color space profiles supported by a camera device.</p>
+     * <p>A color space profile is a combination of a color space, an image format, and a dynamic
+     * range profile. Camera clients can retrieve the list of supported color spaces by calling
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpaces } or
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpacesForDynamicRange }.
+     * If a camera does not support the
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT }
+     * capability, the dynamic range profile will always be
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }. Color space
+     * capabilities are queried in combination with an {@link android.graphics.ImageFormat }.
+     * If a camera client wants to know the general color space capabilities of a camera device
+     * regardless of image format, it can specify {@link android.graphics.ImageFormat#UNKNOWN }.
+     * The color space for a session can be configured by setting the SessionConfiguration
+     * color space via {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace }.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     */
+    @PublicKey
+    @NonNull
+    @SyntheticKey
+    public static final Key<android.hardware.camera2.params.ColorSpaceProfiles> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES =
+            new Key<android.hardware.camera2.params.ColorSpaceProfiles>("android.request.availableColorSpaceProfiles", android.hardware.camera2.params.ColorSpaceProfiles.class);
+
+    /**
+     * <p>A list of all possible color space profiles supported by a camera device.</p>
+     * <p>A color space profile is a combination of a color space, an image format, and a dynamic range
+     * profile. If a camera does not support the
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT }
+     * capability, the dynamic range profile will always be
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }. Camera clients can
+     * use {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace } to select
+     * a color space.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED UNSPECIFIED}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SRGB SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_SRGB LINEAR_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_EXTENDED_SRGB EXTENDED_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_EXTENDED_SRGB LINEAR_EXTENDED_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT709 BT709}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT2020 BT2020}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DCI_P3 DCI_P3}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DISPLAY_P3 DISPLAY_P3}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_NTSC_1953 NTSC_1953}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SMPTE_C SMPTE_C}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ADOBE_RGB ADOBE_RGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_PRO_PHOTO_RGB PRO_PHOTO_RGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACES ACES}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACESCG ACESCG}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_XYZ CIE_XYZ}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_LAB CIE_LAB}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_EXTENDED_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_EXTENDED_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT709
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT2020
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DCI_P3
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DISPLAY_P3
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_NTSC_1953
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SMPTE_C
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ADOBE_RGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_PRO_PHOTO_RGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACES
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACESCG
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_XYZ
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_LAB
+     * @hide
+     */
+    public static final Key<long[]> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP =
+            new Key<long[]>("android.request.availableColorSpaceProfilesMap", long[].class);
+
+    /**
      * <p>The list of image formats that are supported by this
      * camera device for output streams.</p>
      * <p>All camera devices will support JPEG and YUV_420_888 formats.</p>
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 2a47851..bbdb626 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -802,6 +802,46 @@
     }
 
     /**
+     * Retrieve support for capture progress callbacks via
+     *  {@link CameraExtensionSession.ExtensionCaptureCallback#onCaptureProcessProgressed}.
+     *
+     * @param extension         the extension type
+     * @return {@code true} in case progress callbacks are supported, {@code false} otherwise
+     *
+     * @throws IllegalArgumentException in case of an unsupported extension.
+     */
+    public boolean isCaptureProcessProgressAvailable(@Extension int extension) {
+        long clientId = registerClient(mContext);
+        if (clientId < 0) {
+            throw new IllegalArgumentException("Unsupported extensions");
+        }
+
+        try {
+            if (!isExtensionSupported(mCameraId, extension, mChars)) {
+                throw new IllegalArgumentException("Unsupported extension");
+            }
+
+            if (areAdvancedExtensionsSupported()) {
+                IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
+                extender.init(mCameraId);
+                return extender.isCaptureProcessProgressAvailable();
+            } else {
+                Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
+                        initializeExtension(extension);
+                extenders.second.init(mCameraId, mChars.getNativeMetadata());
+                return extenders.second.isCaptureProcessProgressAvailable();
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
+                    + " not respond!");
+        } finally {
+            unregisterClient(clientId);
+        }
+
+        return false;
+    }
+
+    /**
      * Returns the set of keys supported by a {@link CaptureRequest} submitted in a
      * {@link CameraExtensionSession} with a given extension type.
      *
diff --git a/core/java/android/hardware/camera2/CameraExtensionSession.java b/core/java/android/hardware/camera2/CameraExtensionSession.java
index 6ddaddf..6f895d5 100644
--- a/core/java/android/hardware/camera2/CameraExtensionSession.java
+++ b/core/java/android/hardware/camera2/CameraExtensionSession.java
@@ -16,6 +16,7 @@
 
 package android.hardware.camera2;
 
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 
 import java.util.concurrent.Executor;
@@ -198,6 +199,41 @@
                 @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
             // default empty implementation
         }
+
+        /**
+         * This method is called when image capture processing is ongoing between
+         * {@link #onCaptureProcessStarted} and the processed still capture frame returning
+         * to the client surface.
+         *
+         * <p>The value included in the arguments provides clients with an estimate
+         * of the post-processing progress which could take significantly more time
+         * relative to the rest of the {@link #capture} sequence.</p>
+         *
+         * <p>The callback will be triggered only by extensions that return {@code true}
+         * from calls
+         * {@link CameraExtensionCharacteristics#isCaptureProcessProgressAvailable}.</p>
+         *
+         * <p>If support for this callback is present, then clients will be notified at least once
+         * with progress value 100.</p>
+         *
+         * <p>The callback will be triggered only for still capture requests {@link #capture} and
+         * is not supported for repeating requests {@link #setRepeatingRequest}.</p>
+         *
+         * <p>The default implementation of this method does nothing.</p>
+         *
+         * @param session The session received during
+         *                {@link StateCallback#onConfigured(CameraExtensionSession)}
+         * @param request The request that was given to the CameraDevice
+         * @param progress Value between 0 and 100 (inclusive) indicating the current
+         *                post-processing progress
+         *
+         * @see CameraExtensionCharacteristics#isCaptureProcessProgressAvailable
+         *
+         */
+        public void onCaptureProcessProgressed(@NonNull CameraExtensionSession session,
+                @NonNull CaptureRequest request, @IntRange(from = 0, to = 100) int progress) {
+            // default empty implementation
+        }
     }
 
     /**
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index dff2f7e..50551fee 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -133,9 +133,6 @@
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private FoldStateListener mFoldStateListener;
-    @GuardedBy("mLock")
-    private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>();
-    private boolean mFoldedDeviceState;
 
     /**
      * @hide
@@ -144,31 +141,39 @@
         void onDeviceStateChanged(boolean folded);
     }
 
-    private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+    private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
         private final int[] mFoldedDeviceStates;
 
+        private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners =
+                new ArrayList<>();
+        private boolean mFoldedDeviceState;
+
         public FoldStateListener(Context context) {
             mFoldedDeviceStates = context.getResources().getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
         }
 
-        private void handleStateChange(int state) {
+        private synchronized void handleStateChange(int state) {
             boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
-            synchronized (mLock) {
-                mFoldedDeviceState = folded;
-                ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
-                for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
-                    DeviceStateListener callback = listener.get();
-                    if (callback != null) {
-                        callback.onDeviceStateChanged(folded);
-                    } else {
-                        invalidListeners.add(listener);
-                    }
-                }
-                if (!invalidListeners.isEmpty()) {
-                    mDeviceStateListeners.removeAll(invalidListeners);
+
+            mFoldedDeviceState = folded;
+            ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
+            for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
+                DeviceStateListener callback = listener.get();
+                if (callback != null) {
+                    callback.onDeviceStateChanged(folded);
+                } else {
+                    invalidListeners.add(listener);
                 }
             }
+            if (!invalidListeners.isEmpty()) {
+                mDeviceStateListeners.removeAll(invalidListeners);
+            }
+        }
+
+        public synchronized void addDeviceStateListener(DeviceStateListener listener) {
+            listener.onDeviceStateChanged(mFoldedDeviceState);
+            mDeviceStateListeners.add(new WeakReference<>(listener));
         }
 
         @Override
@@ -192,9 +197,8 @@
     public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) {
         synchronized (mLock) {
             DeviceStateListener listener = chars.getDeviceStateListener();
-            listener.onDeviceStateChanged(mFoldedDeviceState);
             if (mFoldStateListener != null) {
-                mDeviceStateListeners.add(new WeakReference<>(listener));
+                mFoldStateListener.addDeviceStateListener(listener);
             }
         }
     }
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index c67a560..44f8b1b 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -1257,6 +1257,24 @@
      */
     public static final int REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE = 19;
 
+    /**
+     * <p>The device supports querying the possible combinations of color spaces, image
+     * formats, and dynamic range profiles supported by the camera and requesting a
+     * particular color space for a session via
+     * {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace }.</p>
+     * <p>Cameras that enable this capability may or may not also implement dynamic range
+     * profiles. If they don't,
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedDynamicRangeProfiles }
+     * will return only
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD } and
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpacesForDynamicRange }
+     * will assume support of the
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }
+     * profile in all combinations of color spaces and image formats.</p>
+     * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+     */
+    public static final int REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES = 20;
+
     //
     // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP
     //
@@ -1367,6 +1385,18 @@
     public static final int REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP_MAX = 0x1000;
 
     //
+    // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP
+    //
+
+    /**
+     * <p>Default value, when not explicitly specified. The Camera device will choose the color
+     * space to employ.</p>
+     * @see CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP
+     * @hide
+     */
+    public static final int REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED = -1;
+
+    //
     // Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE
     //
 
@@ -3171,6 +3201,71 @@
     public static final int CONTROL_EXTENDED_SCENE_MODE_VENDOR_START = 0x40;
 
     //
+    // Enumeration values for CaptureRequest#CONTROL_SETTINGS_OVERRIDE
+    //
+
+    /**
+     * <p>No keys are applied sooner than the other keys when applying CaptureRequest
+     * settings to the camera device. This is the default value.</p>
+     * @see CaptureRequest#CONTROL_SETTINGS_OVERRIDE
+     */
+    public static final int CONTROL_SETTINGS_OVERRIDE_OFF = 0;
+
+    /**
+     * <p>Zoom related keys are applied sooner than the other keys in the CaptureRequest. The
+     * zoom related keys are:</p>
+     * <ul>
+     * <li>{@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}</li>
+     * <li>{@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}</li>
+     * <li>{@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}</li>
+     * <li>{@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions}</li>
+     * <li>{@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions}</li>
+     * </ul>
+     * <p>Even though {@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}, {@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions},
+     * and {@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions} are not directly zoom related, applications
+     * typically scale these regions together with {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} to have a
+     * consistent mapping within the current field of view. In this aspect, they are
+     * related to {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} and {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_REGIONS
+     * @see CaptureRequest#CONTROL_AF_REGIONS
+     * @see CaptureRequest#CONTROL_AWB_REGIONS
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see CaptureRequest#CONTROL_SETTINGS_OVERRIDE
+     */
+    public static final int CONTROL_SETTINGS_OVERRIDE_ZOOM = 1;
+
+    /**
+     * <p>Vendor defined settingsOverride. These depend on vendor implementation.</p>
+     * @see CaptureRequest#CONTROL_SETTINGS_OVERRIDE
+     * @hide
+     */
+    public static final int CONTROL_SETTINGS_OVERRIDE_VENDOR_START = 0x4000;
+
+    //
+    // Enumeration values for CaptureRequest#CONTROL_AUTOFRAMING
+    //
+
+    /**
+     * <p>Disable autoframing.</p>
+     * @see CaptureRequest#CONTROL_AUTOFRAMING
+     */
+    public static final int CONTROL_AUTOFRAMING_OFF = 0;
+
+    /**
+     * <p>Enable autoframing to keep people in the frame's field of view.</p>
+     * @see CaptureRequest#CONTROL_AUTOFRAMING
+     */
+    public static final int CONTROL_AUTOFRAMING_ON = 1;
+
+    /**
+     * <p>Automatically select ON or OFF based on the system level preferences.</p>
+     * @see CaptureRequest#CONTROL_AUTOFRAMING
+     */
+    public static final int CONTROL_AUTOFRAMING_AUTO = 2;
+
+    //
     // Enumeration values for CaptureRequest#EDGE_MODE
     //
 
@@ -3924,6 +4019,29 @@
     public static final int CONTROL_AF_SCENE_CHANGE_DETECTED = 1;
 
     //
+    // Enumeration values for CaptureResult#CONTROL_AUTOFRAMING_STATE
+    //
+
+    /**
+     * <p>Auto-framing is inactive.</p>
+     * @see CaptureResult#CONTROL_AUTOFRAMING_STATE
+     */
+    public static final int CONTROL_AUTOFRAMING_STATE_INACTIVE = 0;
+
+    /**
+     * <p>Auto-framing is in process - either zooming in, zooming out or pan is taking place.</p>
+     * @see CaptureResult#CONTROL_AUTOFRAMING_STATE
+     */
+    public static final int CONTROL_AUTOFRAMING_STATE_FRAMING = 1;
+
+    /**
+     * <p>Auto-framing has reached a stable state (frame/fov is not being adjusted). The state
+     * may transition back to FRAMING if the scene changes.</p>
+     * @see CaptureResult#CONTROL_AUTOFRAMING_STATE
+     */
+    public static final int CONTROL_AUTOFRAMING_STATE_CONVERGED = 2;
+
+    //
     // Enumeration values for CaptureResult#FLASH_STATE
     //
 
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index c5cf0f6..8bb6fa5 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2429,6 +2429,126 @@
             new Key<Boolean>("android.control.awbRegionsSet", boolean.class);
 
     /**
+     * <p>The desired CaptureRequest settings override with which certain keys are
+     * applied earlier so that they can take effect sooner.</p>
+     * <p>There are some CaptureRequest keys which can be applied earlier than others
+     * when controls within a CaptureRequest aren't required to take effect at the same time.
+     * One such example is zoom. Zoom can be applied at a later stage of the camera pipeline.
+     * As soon as the camera device receives the CaptureRequest, it can apply the requested
+     * zoom value onto an earlier request that's already in the pipeline, thus improves zoom
+     * latency.</p>
+     * <p>This key's value in the capture result reflects whether the controls for this capture
+     * are overridden "by" a newer request. This means that if a capture request turns on
+     * settings override, the capture result of an earlier request will contain the key value
+     * of ZOOM. On the other hand, if a capture request has settings override turned on,
+     * but all newer requests have it turned off, the key's value in the capture result will
+     * be OFF because this capture isn't overridden by a newer capture. In the two examples
+     * below, the capture results columns illustrate the settingsOverride values in different
+     * scenarios.</p>
+     * <p>Assuming the zoom settings override can speed up by 1 frame, below example illustrates
+     * the speed-up at the start of capture session:</p>
+     * <pre><code>Camera session created
+     * Request 1 (zoom=1.0x, override=ZOOM) -&gt;
+     * Request 2 (zoom=1.2x, override=ZOOM) -&gt;
+     * Request 3 (zoom=1.4x, override=ZOOM) -&gt;  Result 1 (zoom=1.2x, override=ZOOM)
+     * Request 4 (zoom=1.6x, override=ZOOM) -&gt;  Result 2 (zoom=1.4x, override=ZOOM)
+     * Request 5 (zoom=1.8x, override=ZOOM) -&gt;  Result 3 (zoom=1.6x, override=ZOOM)
+     *                                      -&gt;  Result 4 (zoom=1.8x, override=ZOOM)
+     *                                      -&gt;  Result 5 (zoom=1.8x, override=OFF)
+     * </code></pre>
+     * <p>The application can turn on settings override and use zoom as normal. The example
+     * shows that the later zoom values (1.2x, 1.4x, 1.6x, and 1.8x) overwrite the zoom
+     * values (1.0x, 1.2x, 1.4x, and 1.8x) of earlier requests (#1, #2, #3, and #4).</p>
+     * <p>The application must make sure the settings override doesn't interfere with user
+     * journeys requiring simultaneous application of all controls in CaptureRequest on the
+     * requested output targets. For example, if the application takes a still capture using
+     * CameraCaptureSession#capture, and the repeating request immediately sets a different
+     * zoom value using override, the inflight still capture could have its zoom value
+     * overwritten unexpectedly.</p>
+     * <p>So the application is strongly recommended to turn off settingsOverride when taking
+     * still/burst captures, and turn it back on when there is only repeating viewfinder
+     * request and no inflight still/burst captures.</p>
+     * <p>Below is the example demonstrating the transitions in and out of the
+     * settings override:</p>
+     * <pre><code>Request 1 (zoom=1.0x, override=OFF)
+     * Request 2 (zoom=1.2x, override=OFF)
+     * Request 3 (zoom=1.4x, override=ZOOM)  -&gt; Result 1 (zoom=1.0x, override=OFF)
+     * Request 4 (zoom=1.6x, override=ZOOM)  -&gt; Result 2 (zoom=1.4x, override=ZOOM)
+     * Request 5 (zoom=1.8x, override=OFF)   -&gt; Result 3 (zoom=1.6x, override=ZOOM)
+     *                                       -&gt; Result 4 (zoom=1.6x, override=OFF)
+     *                                       -&gt; Result 5 (zoom=1.8x, override=OFF)
+     * </code></pre>
+     * <p>This example shows that:</p>
+     * <ul>
+     * <li>The application "ramps in" settings override by setting the control to ZOOM.
+     * In the example, request #3 enables zoom settings override. Because the camera device
+     * can speed up applying zoom by 1 frame, the outputs of request #2 has 1.4x zoom, the
+     * value specified in request #3.</li>
+     * <li>The application "ramps out" of settings override by setting the control to OFF. In
+     * the example, request #5 changes the override to OFF. Because request #4's zoom
+     * takes effect in result #3, result #4's zoom remains the same until new value takes
+     * effect in result #5.</li>
+     * </ul>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_SETTINGS_OVERRIDE_OFF OFF}</li>
+     *   <li>{@link #CONTROL_SETTINGS_OVERRIDE_ZOOM ZOOM}</li>
+     * </ul>
+     *
+     * <p><b>Available values for this device:</b><br>
+     * {@link CameraCharacteristics#CONTROL_AVAILABLE_SETTINGS_OVERRIDES android.control.availableSettingsOverrides}</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#CONTROL_AVAILABLE_SETTINGS_OVERRIDES
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see #CONTROL_SETTINGS_OVERRIDE_OFF
+     * @see #CONTROL_SETTINGS_OVERRIDE_ZOOM
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Integer> CONTROL_SETTINGS_OVERRIDE =
+            new Key<Integer>("android.control.settingsOverride", int.class);
+
+    /**
+     * <p>Automatic crop, pan and zoom to keep objects in the center of the frame.</p>
+     * <p>Auto-framing is a special mode provided by the camera device to dynamically crop, zoom
+     * or pan the camera feed to try to ensure that the people in a scene occupy a reasonable
+     * portion of the viewport. It is primarily designed to support video calling in
+     * situations where the user isn't directly in front of the device, especially for
+     * wide-angle cameras.
+     * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} and {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in CaptureResult will be used
+     * to denote the coordinates of the auto-framed region.
+     * Zoom and video stabilization controls are disabled when auto-framing is enabled. The 3A
+     * regions must map the screen coordinates into the scaler crop returned from the capture
+     * result instead of using the active array sensor.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_AUTOFRAMING_OFF OFF}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_ON ON}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_AUTO AUTO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_AUTOFRAMING_OFF
+     * @see #CONTROL_AUTOFRAMING_ON
+     * @see #CONTROL_AUTOFRAMING_AUTO
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Integer> CONTROL_AUTOFRAMING =
+            new Key<Integer>("android.control.autoframing", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1a15596..c5246b5 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -2633,6 +2633,163 @@
             new Key<Float>("android.control.zoomRatio", float.class);
 
     /**
+     * <p>The desired CaptureRequest settings override with which certain keys are
+     * applied earlier so that they can take effect sooner.</p>
+     * <p>There are some CaptureRequest keys which can be applied earlier than others
+     * when controls within a CaptureRequest aren't required to take effect at the same time.
+     * One such example is zoom. Zoom can be applied at a later stage of the camera pipeline.
+     * As soon as the camera device receives the CaptureRequest, it can apply the requested
+     * zoom value onto an earlier request that's already in the pipeline, thus improves zoom
+     * latency.</p>
+     * <p>This key's value in the capture result reflects whether the controls for this capture
+     * are overridden "by" a newer request. This means that if a capture request turns on
+     * settings override, the capture result of an earlier request will contain the key value
+     * of ZOOM. On the other hand, if a capture request has settings override turned on,
+     * but all newer requests have it turned off, the key's value in the capture result will
+     * be OFF because this capture isn't overridden by a newer capture. In the two examples
+     * below, the capture results columns illustrate the settingsOverride values in different
+     * scenarios.</p>
+     * <p>Assuming the zoom settings override can speed up by 1 frame, below example illustrates
+     * the speed-up at the start of capture session:</p>
+     * <pre><code>Camera session created
+     * Request 1 (zoom=1.0x, override=ZOOM) -&gt;
+     * Request 2 (zoom=1.2x, override=ZOOM) -&gt;
+     * Request 3 (zoom=1.4x, override=ZOOM) -&gt;  Result 1 (zoom=1.2x, override=ZOOM)
+     * Request 4 (zoom=1.6x, override=ZOOM) -&gt;  Result 2 (zoom=1.4x, override=ZOOM)
+     * Request 5 (zoom=1.8x, override=ZOOM) -&gt;  Result 3 (zoom=1.6x, override=ZOOM)
+     *                                      -&gt;  Result 4 (zoom=1.8x, override=ZOOM)
+     *                                      -&gt;  Result 5 (zoom=1.8x, override=OFF)
+     * </code></pre>
+     * <p>The application can turn on settings override and use zoom as normal. The example
+     * shows that the later zoom values (1.2x, 1.4x, 1.6x, and 1.8x) overwrite the zoom
+     * values (1.0x, 1.2x, 1.4x, and 1.8x) of earlier requests (#1, #2, #3, and #4).</p>
+     * <p>The application must make sure the settings override doesn't interfere with user
+     * journeys requiring simultaneous application of all controls in CaptureRequest on the
+     * requested output targets. For example, if the application takes a still capture using
+     * CameraCaptureSession#capture, and the repeating request immediately sets a different
+     * zoom value using override, the inflight still capture could have its zoom value
+     * overwritten unexpectedly.</p>
+     * <p>So the application is strongly recommended to turn off settingsOverride when taking
+     * still/burst captures, and turn it back on when there is only repeating viewfinder
+     * request and no inflight still/burst captures.</p>
+     * <p>Below is the example demonstrating the transitions in and out of the
+     * settings override:</p>
+     * <pre><code>Request 1 (zoom=1.0x, override=OFF)
+     * Request 2 (zoom=1.2x, override=OFF)
+     * Request 3 (zoom=1.4x, override=ZOOM)  -&gt; Result 1 (zoom=1.0x, override=OFF)
+     * Request 4 (zoom=1.6x, override=ZOOM)  -&gt; Result 2 (zoom=1.4x, override=ZOOM)
+     * Request 5 (zoom=1.8x, override=OFF)   -&gt; Result 3 (zoom=1.6x, override=ZOOM)
+     *                                       -&gt; Result 4 (zoom=1.6x, override=OFF)
+     *                                       -&gt; Result 5 (zoom=1.8x, override=OFF)
+     * </code></pre>
+     * <p>This example shows that:</p>
+     * <ul>
+     * <li>The application "ramps in" settings override by setting the control to ZOOM.
+     * In the example, request #3 enables zoom settings override. Because the camera device
+     * can speed up applying zoom by 1 frame, the outputs of request #2 has 1.4x zoom, the
+     * value specified in request #3.</li>
+     * <li>The application "ramps out" of settings override by setting the control to OFF. In
+     * the example, request #5 changes the override to OFF. Because request #4's zoom
+     * takes effect in result #3, result #4's zoom remains the same until new value takes
+     * effect in result #5.</li>
+     * </ul>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_SETTINGS_OVERRIDE_OFF OFF}</li>
+     *   <li>{@link #CONTROL_SETTINGS_OVERRIDE_ZOOM ZOOM}</li>
+     * </ul>
+     *
+     * <p><b>Available values for this device:</b><br>
+     * {@link CameraCharacteristics#CONTROL_AVAILABLE_SETTINGS_OVERRIDES android.control.availableSettingsOverrides}</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#CONTROL_AVAILABLE_SETTINGS_OVERRIDES
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see #CONTROL_SETTINGS_OVERRIDE_OFF
+     * @see #CONTROL_SETTINGS_OVERRIDE_ZOOM
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Integer> CONTROL_SETTINGS_OVERRIDE =
+            new Key<Integer>("android.control.settingsOverride", int.class);
+
+    /**
+     * <p>Automatic crop, pan and zoom to keep objects in the center of the frame.</p>
+     * <p>Auto-framing is a special mode provided by the camera device to dynamically crop, zoom
+     * or pan the camera feed to try to ensure that the people in a scene occupy a reasonable
+     * portion of the viewport. It is primarily designed to support video calling in
+     * situations where the user isn't directly in front of the device, especially for
+     * wide-angle cameras.
+     * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} and {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in CaptureResult will be used
+     * to denote the coordinates of the auto-framed region.
+     * Zoom and video stabilization controls are disabled when auto-framing is enabled. The 3A
+     * regions must map the screen coordinates into the scaler crop returned from the capture
+     * result instead of using the active array sensor.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_AUTOFRAMING_OFF OFF}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_ON ON}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_AUTO AUTO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_AUTOFRAMING_OFF
+     * @see #CONTROL_AUTOFRAMING_ON
+     * @see #CONTROL_AUTOFRAMING_AUTO
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Integer> CONTROL_AUTOFRAMING =
+            new Key<Integer>("android.control.autoframing", int.class);
+
+    /**
+     * <p>Current state of auto-framing.</p>
+     * <p>When the camera doesn't have auto-framing available (i.e
+     * <code>{@link CameraCharacteristics#CONTROL_AUTOFRAMING_AVAILABLE android.control.autoframingAvailable}</code> == false) or it is not enabled (i.e
+     * <code>{@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}</code> == OFF), the state will always be INACTIVE.
+     * Other states indicate the current auto-framing state:</p>
+     * <ul>
+     * <li>When <code>{@link CaptureRequest#CONTROL_AUTOFRAMING android.control.autoframing}</code> is set to ON, auto-framing will take
+     * place. While the frame is aligning itself to center the object (doing things like
+     * zooming in, zooming out or pan), the state will be FRAMING.</li>
+     * <li>When field of view is not being adjusted anymore and has reached a stable state, the
+     * state will be CONVERGED.</li>
+     * </ul>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_AUTOFRAMING_STATE_INACTIVE INACTIVE}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_STATE_FRAMING FRAMING}</li>
+     *   <li>{@link #CONTROL_AUTOFRAMING_STATE_CONVERGED CONVERGED}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_AUTOFRAMING
+     * @see CameraCharacteristics#CONTROL_AUTOFRAMING_AVAILABLE
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see #CONTROL_AUTOFRAMING_STATE_INACTIVE
+     * @see #CONTROL_AUTOFRAMING_STATE_FRAMING
+     * @see #CONTROL_AUTOFRAMING_STATE_CONVERGED
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<Integer> CONTROL_AUTOFRAMING_STATE =
+            new Key<Integer>("android.control.autoframingState", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
diff --git a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
index 97ce183..84ca2b6 100644
--- a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
+++ b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl
@@ -24,4 +24,5 @@
     List<CameraOutputConfig> outputConfigs;
     CameraMetadataNative sessionParameter;
     int sessionTemplateId;
+    int sessionType;
 }
diff --git a/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
index 935a542..fa2cbe7 100644
--- a/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IAdvancedExtenderImpl.aidl
@@ -33,4 +33,5 @@
     ISessionProcessorImpl getSessionProcessor();
     CameraMetadataNative getAvailableCaptureRequestKeys(in String cameraId);
     CameraMetadataNative getAvailableCaptureResultKeys(in String cameraId);
+    boolean isCaptureProcessProgressAvailable();
 }
diff --git a/core/java/android/hardware/camera2/extension/ICaptureCallback.aidl b/core/java/android/hardware/camera2/extension/ICaptureCallback.aidl
index f3062ad..02a4690 100644
--- a/core/java/android/hardware/camera2/extension/ICaptureCallback.aidl
+++ b/core/java/android/hardware/camera2/extension/ICaptureCallback.aidl
@@ -27,4 +27,5 @@
     void onCaptureSequenceCompleted(int captureSequenceId);
     void onCaptureSequenceAborted(int captureSequenceId);
     void onCaptureCompleted(long shutterTimestamp, int requestId, in CameraMetadataNative results);
+    void onCaptureProcessProgressed(int progress);
 }
diff --git a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
index a8a7866e..615536b 100644
--- a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
@@ -31,6 +31,7 @@
     @nullable CaptureStageImpl onPresetSession();
     @nullable CaptureStageImpl onEnableSession();
     @nullable CaptureStageImpl onDisableSession();
+    int getSessionType();
 
     boolean isExtensionAvailable(in String cameraId, in CameraMetadataNative chars);
     void init(in String cameraId, in CameraMetadataNative chars);
@@ -41,4 +42,5 @@
     LatencyRange getEstimatedCaptureLatencyRange(in Size outputSize);
     CameraMetadataNative getAvailableCaptureRequestKeys();
     CameraMetadataNative getAvailableCaptureResultKeys();
+    boolean isCaptureProcessProgressAvailable();
 }
diff --git a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
index 2d67344..01046d0 100644
--- a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl
@@ -34,6 +34,7 @@
     void init(in String cameraId, in CameraMetadataNative chars);
     boolean isExtensionAvailable(in String cameraId, in CameraMetadataNative chars);
     @nullable CaptureStageImpl getCaptureStage();
+    int getSessionType();
 
     const int PROCESSOR_TYPE_REQUEST_UPDATE_ONLY = 0;
     const int PROCESSOR_TYPE_IMAGE_PROCESSOR = 1;
diff --git a/core/java/android/hardware/camera2/extension/IProcessResultImpl.aidl b/core/java/android/hardware/camera2/extension/IProcessResultImpl.aidl
index 4114edb..57f94c0 100644
--- a/core/java/android/hardware/camera2/extension/IProcessResultImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IProcessResultImpl.aidl
@@ -21,4 +21,5 @@
 interface IProcessResultImpl
 {
     void onCaptureCompleted(long shutterTimestamp, in CameraMetadataNative results);
+    void onCaptureProcessProgressed(int progress);
 }
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index c8dc2d0..3a8dc03 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -243,9 +243,16 @@
             mCameraConfigMap.put(cameraOutput.getSurface(), output);
         }
 
-        SessionConfiguration sessionConfiguration = new SessionConfiguration(
-                SessionConfiguration.SESSION_REGULAR, outputList,
-                new CameraExtensionUtils.HandlerExecutor(mHandler), new SessionStateHandler());
+        int sessionType = SessionConfiguration.SESSION_REGULAR;
+        if (sessionConfig.sessionType != -1 &&
+                (sessionConfig.sessionType != SessionConfiguration.SESSION_HIGH_SPEED)) {
+            sessionType = sessionConfig.sessionType;
+            Log.v(TAG, "Using session type: " + sessionType);
+        }
+
+        SessionConfiguration sessionConfiguration = new SessionConfiguration(sessionType,
+                outputList, new CameraExtensionUtils.HandlerExecutor(mHandler),
+                new SessionStateHandler());
 
         if ((sessionConfig.sessionParameter != null) &&
                 (!sessionConfig.sessionParameter.isEmpty())) {
@@ -656,6 +663,19 @@
                 Binder.restoreCallingIdentity(ident);
             }
         }
+
+        @Override
+        public void onCaptureProcessProgressed(int progress) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(
+                        () -> mClientCallbacks.onCaptureProcessProgressed(
+                                CameraAdvancedExtensionSessionImpl.this, mClientRequest,
+                                progress));
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
     }
 
     private final class CaptureCallbackHandler extends CameraCaptureSession.CaptureCallback {
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 41822e7..389c214 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -415,6 +415,18 @@
                     "Session already initialized");
             return;
         }
+        int previewSessionType = mPreviewExtender.getSessionType();
+        int imageSessionType = mImageExtender.getSessionType();
+        if (previewSessionType != imageSessionType) {
+            throw new IllegalStateException("Preview extender session type: " + previewSessionType +
+                "and image extender session type: " + imageSessionType + " mismatch!");
+        }
+        int sessionType = SessionConfiguration.SESSION_REGULAR;
+        if ((previewSessionType != -1) &&
+                (previewSessionType != SessionConfiguration.SESSION_HIGH_SPEED)) {
+            sessionType = previewSessionType;
+            Log.v(TAG, "Using session type: " + sessionType);
+        }
 
         ArrayList<CaptureStageImpl> sessionParamsList = new ArrayList<>();
         ArrayList<OutputConfiguration> outputList = new ArrayList<>();
@@ -432,7 +444,7 @@
         }
 
         SessionConfiguration sessionConfig = new SessionConfiguration(
-                SessionConfiguration.SESSION_REGULAR,
+                sessionType,
                 outputList,
                 new CameraExtensionUtils.HandlerExecutor(mHandler),
                 new SessionStateHandler());
@@ -1375,6 +1387,18 @@
                 Binder.restoreCallingIdentity(ident);
             }
         }
+
+        @Override
+        public void onCaptureProcessProgressed(int progress) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(
+                        () -> mCallbacks.onCaptureProcessProgressed(CameraExtensionSessionImpl.this,
+                                mClientRequest, progress));
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
     }
 
     // This handler can operate in three modes:
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index ee12df5..012fad5 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -50,6 +50,7 @@
 import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration;
 import android.hardware.camera2.marshal.impl.MarshalQueryableString;
 import android.hardware.camera2.params.Capability;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.params.Face;
@@ -813,6 +814,15 @@
                     }
                 });
         sGetCommandMap.put(
+                CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES.getNativeKey(),
+                        new GetCommand() {
+                    @Override
+                    @SuppressWarnings("unchecked")
+                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
+                        return (T) metadata.getColorSpaceProfiles();
+                    }
+                });
+        sGetCommandMap.put(
                 CaptureResult.STATISTICS_OIS_SAMPLES.getNativeKey(),
                         new GetCommand() {
                     @Override
@@ -1068,6 +1078,17 @@
         return new DynamicRangeProfiles(profileArray);
     }
 
+    private ColorSpaceProfiles getColorSpaceProfiles() {
+        long[] profileArray = getBase(
+                CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP);
+
+        if (profileArray == null) {
+            return null;
+        }
+
+        return new ColorSpaceProfiles(profileArray);
+    }
+
     private Location getGpsLocation() {
         String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD);
         double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES);
diff --git a/core/java/android/hardware/camera2/impl/FrameNumberTracker.java b/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
index 7b6a457..8304796 100644
--- a/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
+++ b/core/java/android/hardware/camera2/impl/FrameNumberTracker.java
@@ -26,6 +26,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.TreeMap;
 
 /**
@@ -63,11 +64,11 @@
     }
 
     private void update() {
-        Iterator iter = mFutureErrorMap.entrySet().iterator();
+        Iterator<Map.Entry<Long, Integer>> iter = mFutureErrorMap.entrySet().iterator();
         while (iter.hasNext()) {
-            TreeMap.Entry pair = (TreeMap.Entry)iter.next();
-            Long errorFrameNumber = (Long)pair.getKey();
-            int requestType = (int) pair.getValue();
+            Map.Entry<Long, Integer> pair = iter.next();
+            long errorFrameNumber = pair.getKey();
+            int requestType = pair.getValue();
             Boolean removeError = false;
             if (errorFrameNumber == mCompletedFrameNumber[requestType] + 1) {
                 removeError = true;
diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java
index 621a418..92a2fb6 100644
--- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java
+++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java
@@ -103,6 +103,7 @@
         return new MarshalerEnum(managedType, nativeType);
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     @Override
     public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) {
         if (nativeType == TYPE_INT32 || nativeType == TYPE_BYTE) {
diff --git a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java
new file mode 100644
index 0000000..2e3af80
--- /dev/null
+++ b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java
@@ -0,0 +1,257 @@
+/*
+ * 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.camera2.params;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.graphics.ColorSpace;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraMetadata;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Immutable class with information about supported color space profiles.
+ *
+ * <p>An instance of this class can be queried by retrieving the value of
+ * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}.
+ * </p>
+ *
+ * <p>All camera devices supporting the
+ * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES}
+ * capability must advertise the supported color space profiles in
+ * {@link #getSupportedColorSpaces}</p>
+ *
+ * @see SessionConfiguration#setColorSpace
+ */
+public final class ColorSpaceProfiles {
+    /*
+     * @hide
+     */
+    public static final int UNSPECIFIED =
+            CameraMetadata.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED;
+
+    private final Map<ColorSpace.Named, Map<Integer, Set<Long>>> mProfileMap = new ArrayMap<>();
+
+    /**
+     * Create a new immutable ColorSpaceProfiles instance.
+     *
+     * <p>This constructor takes over the array; do not write to the array afterwards.</p>
+     *
+     * <p>Do note that the constructor is available for testing purposes only!
+     * Camera clients must always retrieve the value of
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}.
+     * for a given camera id in order to retrieve the device capabilities.</p>
+     *
+     * @param elements
+     *          An array of elements describing the map. It contains three elements per entry which
+     *          describe the supported color space profile value in the first element, a compatible
+     *          image format in the second, and in the third element a bitmap of compatible dynamic
+     *          range profiles (see {@link DynamicRangeProfiles#STANDARD} and others for the
+     *          individual bitmap components).
+     *
+     * @throws IllegalArgumentException
+     *            if the {@code elements} array length is invalid, not divisible by 3 or contains
+     *            invalid element values
+     * @throws NullPointerException
+     *            if {@code elements} is {@code null}
+     *
+     */
+    public ColorSpaceProfiles(@NonNull final long[] elements) {
+        if ((elements.length % 3) != 0) {
+            throw new IllegalArgumentException("Color space profile map length "
+                    + elements.length + " is not divisible by 3!");
+        }
+
+        for (int i = 0; i < elements.length; i += 3) {
+            int colorSpace = (int) elements[i];
+            checkProfileValue(colorSpace);
+            ColorSpace.Named namedColorSpace = ColorSpace.Named.values()[colorSpace];
+            int imageFormat = (int) elements[i + 1];
+            long dynamicRangeProfileBitmap = elements[i + 2];
+
+            if (!mProfileMap.containsKey(namedColorSpace)) {
+                ArrayMap<Integer, Set<Long>> imageFormatMap = new ArrayMap<>();
+                mProfileMap.put(namedColorSpace, imageFormatMap);
+            }
+
+            if (!mProfileMap.get(namedColorSpace).containsKey(imageFormat)) {
+                ArraySet<Long> dynamicRangeProfiles = new ArraySet<>();
+                mProfileMap.get(namedColorSpace).put(imageFormat, dynamicRangeProfiles);
+            }
+
+            if (dynamicRangeProfileBitmap != 0) {
+                for (long dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+                        dynamicRangeProfile < DynamicRangeProfiles.PUBLIC_MAX;
+                        dynamicRangeProfile <<= 1) {
+                    if ((dynamicRangeProfileBitmap & dynamicRangeProfile) != 0) {
+                        mProfileMap.get(namedColorSpace).get(imageFormat).add(dynamicRangeProfile);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static void checkProfileValue(int colorSpace) {
+        boolean found = false;
+        for (ColorSpace.Named value : ColorSpace.Named.values()) {
+            if (colorSpace == value.ordinal()) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            throw new IllegalArgumentException("Unknown ColorSpace " + colorSpace);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public @NonNull Map<ColorSpace.Named, Map<Integer, Set<Long>>> getProfileMap() {
+        return mProfileMap;
+    }
+
+    /**
+     * Return a list of color spaces that are compatible with an ImageFormat. If ImageFormat.UNKNOWN
+     * is provided, this function will return a set of all unique color spaces supported by the
+     * device, regardless of image format.
+     *
+     * Color spaces which are compatible with ImageFormat.PRIVATE are able to be used with
+     * SurfaceView, SurfaceTexture, MediaCodec and MediaRecorder.
+     *
+     * @return set of color spaces
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     */
+    public @NonNull Set<ColorSpace.Named> getSupportedColorSpaces(
+            @ImageFormat.Format int imageFormat) {
+        ArraySet<ColorSpace.Named> supportedColorSpaceProfiles = new ArraySet<>();
+        for (ColorSpace.Named colorSpace : mProfileMap.keySet()) {
+            if (imageFormat == ImageFormat.UNKNOWN) {
+                supportedColorSpaceProfiles.add(colorSpace);
+            } else {
+                Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+                if (imageFormatMap.containsKey(imageFormat)) {
+                    supportedColorSpaceProfiles.add(colorSpace);
+                }
+            }
+        }
+        return supportedColorSpaceProfiles;
+    }
+
+    /**
+     * Return a list of image formats that are compatible with a color space.
+     *
+     * Color spaces which are compatible with ImageFormat.PRIVATE are able to be used with
+     * SurfaceView, SurfaceTexture, MediaCodec and MediaRecorder.
+     *
+     * @return set of image formats
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     */
+    public @NonNull Set<Integer> getSupportedImageFormatsForColorSpace(
+            @NonNull ColorSpace.Named colorSpace) {
+        Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+        if (imageFormatMap == null) {
+            return new ArraySet<Integer>();
+        }
+
+        return imageFormatMap.keySet();
+    }
+
+    /**
+     * Return a list of dynamic range profiles that are compatible with a color space and
+     * ImageFormat. If ImageFormat.UNKNOWN is provided, this function will return a set of
+     * all unique dynamic range profiles supported by the device given a color space,
+     * regardless of image format.
+     *
+     * @return set of dynamic range profiles.
+     * @see OutputConfiguration#setDynamicRangeProfile
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     * @see DynamicRangeProfiles.Profile
+     */
+    public @NonNull Set<Long> getSupportedDynamicRangeProfiles(@NonNull ColorSpace.Named colorSpace,
+            @ImageFormat.Format int imageFormat) {
+        Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+        if (imageFormatMap == null) {
+            return new ArraySet<Long>();
+        }
+
+        Set<Long> dynamicRangeProfiles = null;
+        if (imageFormat == ImageFormat.UNKNOWN) {
+            dynamicRangeProfiles = new ArraySet<>();
+            for (int supportedImageFormat : imageFormatMap.keySet()) {
+                Set<Long> supportedDynamicRangeProfiles = imageFormatMap.get(
+                        supportedImageFormat);
+                for (Long supportedDynamicRangeProfile : supportedDynamicRangeProfiles) {
+                    dynamicRangeProfiles.add(supportedDynamicRangeProfile);
+                }
+            }
+        } else {
+            dynamicRangeProfiles = imageFormatMap.get(imageFormat);
+            if (dynamicRangeProfiles == null) {
+                return new ArraySet<>();
+            }
+        }
+
+        return dynamicRangeProfiles;
+    }
+
+    /**
+     * Return a list of color spaces that are compatible with an ImageFormat and a dynamic range
+     * profile. If ImageFormat.UNKNOWN is provided, this function will return a set of all unique
+     * color spaces compatible with the given dynamic range profile, regardless of image format.
+     *
+     * @return set of color spaces
+     * @see SessionConfiguration#setColorSpace
+     * @see OutputConfiguration#setDynamicRangeProfile
+     * @see ColorSpace.Named
+     * @see DynamicRangeProfiles.Profile
+     */
+    public @NonNull Set<ColorSpace.Named> getSupportedColorSpacesForDynamicRange(
+            @ImageFormat.Format int imageFormat,
+            @DynamicRangeProfiles.Profile long dynamicRangeProfile) {
+        ArraySet<ColorSpace.Named> supportedColorSpaceProfiles = new ArraySet<>();
+        for (ColorSpace.Named colorSpace : mProfileMap.keySet()) {
+            Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+            if (imageFormat == ImageFormat.UNKNOWN) {
+                for (int supportedImageFormat : imageFormatMap.keySet()) {
+                    Set<Long> dynamicRangeProfiles = imageFormatMap.get(supportedImageFormat);
+                    if (dynamicRangeProfiles.contains(dynamicRangeProfile)) {
+                        supportedColorSpaceProfiles.add(colorSpace);
+                    }
+                }
+            } else if (imageFormatMap.containsKey(imageFormat)) {
+                Set<Long> dynamicRangeProfiles = imageFormatMap.get(imageFormat);
+                if (dynamicRangeProfiles.contains(dynamicRangeProfile)) {
+                    supportedColorSpaceProfiles.add(colorSpace);
+                }
+            }
+        }
+        return supportedColorSpaceProfiles;
+    }
+}
diff --git a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
index b9a327b..d9ee561 100644
--- a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
+++ b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
@@ -204,7 +204,7 @@
          *
          */
         @SuppressLint("MissingGetterMatchingBuilder")
-        public @NonNull Builder addOrientationForState(long deviceState, long angle) {
+        public @NonNull Builder addOrientationForState(@DeviceState long deviceState, long angle) {
             if (angle % 90 != 0) {
                 throw new IllegalArgumentException("Sensor orientation not divisible by 90: "
                         + angle);
diff --git a/core/java/android/hardware/camera2/params/Face.java b/core/java/android/hardware/camera2/params/Face.java
index 1d9a5a3a..32688a72 100644
--- a/core/java/android/hardware/camera2/params/Face.java
+++ b/core/java/android/hardware/camera2/params/Face.java
@@ -17,6 +17,7 @@
 
 package android.hardware.camera2.params;
 
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Point;
@@ -173,6 +174,7 @@
      * @see #SCORE_MAX
      * @see #SCORE_MIN
      */
+    @IntRange(from = SCORE_MIN, to = SCORE_MAX)
     public int getScore() {
         return mScore;
     }
@@ -377,7 +379,7 @@
          * @param score Confidence level between {@value #SCORE_MIN}-{@value #SCORE_MAX}.
          * @return This builder.
          */
-        public @NonNull Builder setScore(int score) {
+        public @NonNull Builder setScore(@IntRange(from = SCORE_MIN, to = SCORE_MAX) int score) {
             checkNotUsed();
             checkScore(score);
             mBuilderFieldsSet |= FIELD_SCORE;
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 90e92db..f4b87b9 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -22,7 +22,10 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.ColorSpace;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
@@ -30,7 +33,6 @@
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.MultiResolutionImageReader;
 import android.hardware.camera2.params.DynamicRangeProfiles;
-import android.hardware.camera2.params.DynamicRangeProfiles.Profile;
 import android.hardware.camera2.params.MultiResolutionStreamInfo;
 import android.hardware.camera2.utils.HashCodeHelpers;
 import android.hardware.camera2.utils.SurfaceUtils;
@@ -454,7 +456,7 @@
      * {@link android.media.MediaCodec} etc.)
      * or {@link ImageFormat#YCBCR_P010}.</p>
      */
-    public void setDynamicRangeProfile(@Profile long profile) {
+    public void setDynamicRangeProfile(@DynamicRangeProfiles.Profile long profile) {
         mDynamicRangeProfile = profile;
     }
 
@@ -463,11 +465,54 @@
      *
      * @return the currently set dynamic range profile
      */
-    public @Profile long getDynamicRangeProfile() {
+    public @DynamicRangeProfiles.Profile long getDynamicRangeProfile() {
         return mDynamicRangeProfile;
     }
 
     /**
+     * Set a specific device-supported color space.
+     *
+     * <p>Clients can choose from any profile advertised as supported in
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}
+     * queried using {@link ColorSpaceProfiles#getSupportedColorSpaces}.
+     * When set, the colorSpace will override the default color spaces of the output targets,
+     * or the color space implied by the dataSpace passed into an {@link ImageReader}'s
+     * constructor.</p>
+     *
+     * @hide
+     */
+    @TestApi
+    public void setColorSpace(@NonNull ColorSpace.Named colorSpace) {
+        mColorSpace = colorSpace.ordinal();
+    }
+
+    /**
+     * Clear the color space, such that the default color space will be used.
+     *
+     * @hide
+     */
+    @TestApi
+    public void clearColorSpace() {
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
+    }
+
+    /**
+     * Return the current color space.
+     *
+     * @return the currently set color space
+     * @hide
+     */
+    @TestApi
+    @SuppressLint("MethodNameUnits")
+    public @Nullable ColorSpace getColorSpace() {
+        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
+        } else {
+            return null;
+        }
+    }
+
+    /**
      * Create a new {@link OutputConfiguration} instance.
      *
      * <p>This constructor takes an argument for desired camera rotation</p>
@@ -530,6 +575,7 @@
         mIsMultiResolution = false;
         mSensorPixelModesUsed = new ArrayList<Integer>();
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
         mTimestampBase = TIMESTAMP_BASE_DEFAULT;
         mMirrorMode = MIRROR_MODE_AUTO;
@@ -631,6 +677,7 @@
         mIsMultiResolution = false;
         mSensorPixelModesUsed = new ArrayList<Integer>();
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
     }
 
@@ -1079,6 +1126,7 @@
         this.mIsMultiResolution = other.mIsMultiResolution;
         this.mSensorPixelModesUsed = other.mSensorPixelModesUsed;
         this.mDynamicRangeProfile = other.mDynamicRangeProfile;
+        this.mColorSpace = other.mColorSpace;
         this.mStreamUseCase = other.mStreamUseCase;
         this.mTimestampBase = other.mTimestampBase;
         this.mMirrorMode = other.mMirrorMode;
@@ -1105,6 +1153,7 @@
         checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
         long dynamicRangeProfile = source.readLong();
         DynamicRangeProfiles.checkProfileValue(dynamicRangeProfile);
+        int colorSpace = source.readInt();
 
         int timestampBase = source.readInt();
         int mirrorMode = source.readInt();
@@ -1132,6 +1181,7 @@
         mIsMultiResolution = isMultiResolutionOutput;
         mSensorPixelModesUsed = convertIntArrayToIntegerList(sensorPixelModesUsed);
         mDynamicRangeProfile = dynamicRangeProfile;
+        mColorSpace = colorSpace;
         mStreamUseCase = streamUseCase;
         mTimestampBase = timestampBase;
         mMirrorMode = mirrorMode;
@@ -1251,6 +1301,7 @@
         // writeList doesn't seem to work well with Integer list.
         dest.writeIntArray(convertIntegerToIntList(mSensorPixelModesUsed));
         dest.writeLong(mDynamicRangeProfile);
+        dest.writeInt(mColorSpace);
         dest.writeLong(mStreamUseCase);
         dest.writeInt(mTimestampBase);
         dest.writeInt(mMirrorMode);
@@ -1293,7 +1344,8 @@
                 return false;
             }
             for (int j = 0; j < mSensorPixelModesUsed.size(); j++) {
-                if (mSensorPixelModesUsed.get(j) != other.mSensorPixelModesUsed.get(j)) {
+                if (!Objects.equals(
+                        mSensorPixelModesUsed.get(j), other.mSensorPixelModesUsed.get(j))) {
                     return false;
                 }
             }
@@ -1305,6 +1357,9 @@
             if (mDynamicRangeProfile != other.mDynamicRangeProfile) {
                 return false;
             }
+            if (mColorSpace != other.mColorSpace) {
+                return false;
+            }
 
             return true;
         }
@@ -1325,7 +1380,8 @@
                     mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0,
                     mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
                     mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
-                    mDynamicRangeProfile, mStreamUseCase, mTimestampBase, mMirrorMode);
+                    mDynamicRangeProfile, mColorSpace, mStreamUseCase,
+                    mTimestampBase, mMirrorMode);
         }
 
         return HashCodeHelpers.hashCode(
@@ -1334,7 +1390,7 @@
                 mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0,
                 mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
                 mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
-                mDynamicRangeProfile, mStreamUseCase, mTimestampBase,
+                mDynamicRangeProfile, mColorSpace, mStreamUseCase, mTimestampBase,
                 mMirrorMode);
     }
 
@@ -1369,6 +1425,8 @@
     private ArrayList<Integer> mSensorPixelModesUsed;
     // Dynamic range profile
     private long mDynamicRangeProfile;
+    // Color space
+    private int mColorSpace;
     // Stream use case
     private long mStreamUseCase;
     // Timestamp base
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index cfb6efa..385f107 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -23,6 +23,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.ColorSpace;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
@@ -30,6 +32,7 @@
 import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.utils.HashCodeHelpers;
+import android.media.ImageReader;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
@@ -94,6 +97,7 @@
     private Executor mExecutor = null;
     private InputConfiguration mInputConfig = null;
     private CaptureRequest mSessionParameters = null;
+    private int mColorSpace;
 
     /**
      * Create a new {@link SessionConfiguration}.
@@ -314,4 +318,45 @@
     public CaptureRequest getSessionParameters() {
         return mSessionParameters;
     }
+
+    /**
+     * Set a specific device-supported color space.
+     *
+     * <p>Clients can choose from any profile advertised as supported in
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}
+     * queried using {@link ColorSpaceProfiles#getSupportedColorSpaces}.
+     * When set, the colorSpace will override the default color spaces of the output targets,
+     * or the color space implied by the dataSpace passed into an {@link ImageReader}'s
+     * constructor.</p>
+     */
+    public void setColorSpace(@NonNull ColorSpace.Named colorSpace) {
+        mColorSpace = colorSpace.ordinal();
+        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
+            outputConfiguration.setColorSpace(colorSpace);
+        }
+    }
+
+    /**
+     * Clear the color space, such that the default color space will be used.
+     */
+    public void clearColorSpace() {
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
+        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
+            outputConfiguration.clearColorSpace();
+        }
+    }
+
+    /**
+     * Return the current color space.
+     *
+     * @return the currently set color space
+     */
+    @SuppressLint("MethodNameUnits")
+    public @Nullable ColorSpace getColorSpace() {
+        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/core/java/android/hardware/display/BrightnessConfiguration.java b/core/java/android/hardware/display/BrightnessConfiguration.java
index 366734e..007b37f 100644
--- a/core/java/android/hardware/display/BrightnessConfiguration.java
+++ b/core/java/android/hardware/display/BrightnessConfiguration.java
@@ -24,11 +24,11 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/hardware/display/BrightnessCorrection.java b/core/java/android/hardware/display/BrightnessCorrection.java
index 2919ec3..5ff08ba 100644
--- a/core/java/android/hardware/display/BrightnessCorrection.java
+++ b/core/java/android/hardware/display/BrightnessCorrection.java
@@ -23,10 +23,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.MathUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 8311190..9b07d3a 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -439,12 +439,13 @@
             SWITCHING_TYPE_NONE,
             SWITCHING_TYPE_WITHIN_GROUPS,
             SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS,
+            SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SwitchingType {}
 
     /**
-     * No mode switching will happen.
+     * No display mode switching will happen.
      * @hide
      */
     @TestApi
@@ -467,6 +468,13 @@
     public static final int SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS = 2;
 
     /**
+     * Allow render frame rate switches, but not physical modes.
+     * @hide
+     */
+    @TestApi
+    public static final int SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY = 3;
+
+    /**
      * @hide
      */
     @LongDef(flag = true, prefix = {"EVENT_FLAG_"}, value = {
@@ -1308,6 +1316,7 @@
         switch (switchingType) {
             case SWITCHING_TYPE_NONE:
                 return MATCH_CONTENT_FRAMERATE_NEVER;
+            case SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY:
             case SWITCHING_TYPE_WITHIN_GROUPS:
                 return MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY;
             case SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS:
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 00bccc6..6b3e673 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -24,11 +24,11 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.util.IntArray;
-import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
 import android.view.SurfaceControl.Transaction;
 import android.window.DisplayWindowPolicyController;
 import android.window.ScreenCapture;
@@ -36,7 +36,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -651,72 +650,6 @@
     }
 
     /**
-     * Information about the min and max refresh rate DM would like to set the display to.
-     */
-    public static final class RefreshRateRange {
-        public static final String TAG = "RefreshRateRange";
-
-        // The tolerance within which we consider something approximately equals.
-        public static final float FLOAT_TOLERANCE = 0.01f;
-
-        /**
-         * The lowest desired refresh rate.
-         */
-        public float min;
-
-        /**
-         * The highest desired refresh rate.
-         */
-        public float max;
-
-        public RefreshRateRange() {}
-
-        public RefreshRateRange(float min, float max) {
-            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
-                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
-                        + min + " " + max);
-                this.min = this.max = 0;
-                return;
-            }
-            if (min > max) {
-                // Min and max are within epsilon of each other, but in the wrong order.
-                float t = min;
-                min = max;
-                max = t;
-            }
-            this.min = min;
-            this.max = max;
-        }
-
-        /**
-         * Checks whether the two objects have the same values.
-         */
-        @Override
-        public boolean equals(Object other) {
-            if (other == this) {
-                return true;
-            }
-
-            if (!(other instanceof RefreshRateRange)) {
-                return false;
-            }
-
-            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
-            return (min == refreshRateRange.min && max == refreshRateRange.max);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(min, max);
-        }
-
-        @Override
-        public String toString() {
-            return "(" + min + " " + max + ")";
-        }
-    }
-
-    /**
      * Describes a limitation on a display's refresh rate. Includes the allowed refresh rate
      * range as well as information about when it applies, such as high-brightness-mode.
      */
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 7247ef7..197739b 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -768,6 +768,20 @@
         }
     }
 
+    /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private void cancelEnrollment(long requestId) {
         if (mService != null) {
             try {
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 9b56f43..2bf187a 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -172,4 +172,9 @@
 
     // Registers BiometricStateListener.
     void registerBiometricStateListener(IBiometricStateListener listener);
+
+    // Internal operation used to clear face biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/Fingerprint.java b/core/java/android/hardware/fingerprint/Fingerprint.java
index 9ce834ca..c01c94c 100644
--- a/core/java/android/hardware/fingerprint/Fingerprint.java
+++ b/core/java/android/hardware/fingerprint/Fingerprint.java
@@ -69,4 +69,4 @@
             return new Fingerprint[size];
         }
     };
-};
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 0fd164d..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
@@ -1080,7 +1096,7 @@
      */
     public boolean isPowerbuttonFps() {
         final FingerprintSensorPropertiesInternal sensorProps = getFirstFingerprintSensor();
-        return sensorProps.sensorType == TYPE_POWER_BUTTON;
+        return sensorProps == null ? false : sensorProps.sensorType == TYPE_POWER_BUTTON;
     }
 
     /**
@@ -1125,6 +1141,20 @@
     }
 
     /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide
      */
     public void addLockoutResetCallback(final LockoutResetCallback callback) {
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 1ba9a04..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);
@@ -208,4 +213,9 @@
     // Sends a power button pressed event to all listeners.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     oneway void onPowerPressed();
+
+    // Internal operation used to clear fingerprint biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl b/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
deleted file mode 100644
index 9c2aa66..0000000
--- a/core/java/android/hardware/fingerprint/IUdfpsHbmListener.aidl
+++ /dev/null
@@ -1,43 +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.
- */
-package android.hardware.fingerprint;
-
-/**
- * A listener for the high-brightness mode (HBM) transitions. This allows other components to
- * perform certain actions when the HBM is toggled on or off. For example, a display manager
- * implementation can subscribe to these events from UdfpsController and adjust the display's
- * refresh rate when the HBM is enabled.
- *
- * @hide
- */
-oneway interface IUdfpsHbmListener {
-    /**
-     * UdfpsController will call this method when the HBM is enabled.
-     *
-     * @param displayId The displayId for which the HBM is enabled. See
-     *        {@link android.view.Display#getDisplayId()}.
-     */
-    void onHbmEnabled(int displayId);
-
-    /**
-     * UdfpsController will call this method when the HBM is disabled.
-     *
-     * @param displayId The displayId for which the HBM is disabled. See
-     *        {@link android.view.Display#getDisplayId()}.
-     */
-    void onHbmDisabled(int displayId);
-}
-
diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
new file mode 100644
index 0000000..c99fccc
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.fingerprint;
+
+/**
+ * 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/fingerprint/IUdfpsRefreshRateRequestCallback.aidl b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
new file mode 100644
index 0000000..8587348
--- /dev/null
+++ b/core/java/android/hardware/fingerprint/IUdfpsRefreshRateRequestCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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.fingerprint;
+
+/**
+ * A callback for UDFPS refresh rate. This allows other components to
+ * perform certain actions when the refresh rate is enabled or disabled.
+ * For example, a display manager implementation can subscribe to these
+ * events from UdfpsController when refresh rate is enabled or disabled.
+ *
+ * @hide
+ */
+oneway interface IUdfpsRefreshRateRequestCallback {
+    /**
+     * Sets the appropriate display refresh rate for UDFPS.
+     *
+     * @param displayId The displayId for which the refresh rate should be set. See
+     *        {@link android.view.Display#getDisplayId()}.
+     */
+    void onRequestEnabled(int displayId);
+
+    /**
+     * Unsets the appropriate display refresh rate for UDFPS.
+     *
+     * @param displayId The displayId for which the refresh rate should be unset. See
+     *        {@link android.view.Display#getDisplayId()}.
+     */
+    void onRequestDisabled(int displayId);
+}
+
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index f213224b..fd3d1ac 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -161,4 +161,16 @@
     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);
+
+    @EnforcePermission("MONITOR_INPUT")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.MONITOR_INPUT)")
+    void pilferPointers(IBinder inputChannelToken);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 8d4aac4..702b39b 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
@@ -1702,6 +1720,34 @@
     }
 
     /**
+     * Pilfer pointers from an input channel.
+     *
+     * Takes all the current pointer event streams that are currently being sent to the given
+     * input channel and generates appropriate cancellations for all other windows that are
+     * receiving these pointers.
+     *
+     * This API is intended to be used in conjunction with spy windows. When a spy window pilfers
+     * pointers, the foreground windows and all other spy windows that are receiving any of the
+     * pointers that are currently being dispatched to the pilfering window will have those pointers
+     * canceled. Only the pilfering window will continue to receive events for the affected pointers
+     * until the pointer is lifted.
+     *
+     * This method should be used with caution as unexpected pilfering can break fundamental user
+     * interactions.
+     *
+     * @see android.os.InputConfig#SPY
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MONITOR_INPUT)
+    public void pilferPointers(IBinder inputChannelToken) {
+        try {
+            mIm.pilferPointers(inputChannelToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Adds a battery listener to be notified about {@link BatteryState} changes for an input
      * device. The same listener can be registered for multiple input devices.
      * The listener will be notified of the initial battery state of the device after it is
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index d7cda9e..4d61553 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -39,7 +38,7 @@
  * @hide
  */
 @SystemApi
-public class VirtualDpad implements Closeable {
+public class VirtualDpad extends VirtualInputDevice {
 
     private final Set<Integer> mSupportedKeyCodes =
             Collections.unmodifiableSet(
@@ -50,23 +49,10 @@
                                     KeyEvent.KEYCODE_DPAD_LEFT,
                                     KeyEvent.KEYCODE_DPAD_RIGHT,
                                     KeyEvent.KEYCODE_DPAD_CENTER)));
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
new file mode 100644
index 0000000..772ba8e
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+import android.annotation.RequiresPermission;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.Closeable;
+
+/**
+ * The base class for all virtual input devices such as VirtualKeyboard, VirtualMouse.
+ * This implements the shared functionality such as closing the device and keeping track of
+ * identifiers.
+ *
+ * @hide
+ */
+abstract class VirtualInputDevice implements Closeable {
+
+    /**
+     * The virtual device to which this VirtualInputDevice belongs to.
+     */
+    protected final IVirtualDevice mVirtualDevice;
+
+    /**
+     * The token used to uniquely identify the virtual input device.
+     */
+    protected final IBinder mToken;
+
+    /** @hide */
+    VirtualInputDevice(
+            IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+    /**
+     * @return The device id of this device.
+     * @hide
+     */
+    public int getInputDeviceId() {
+        try {
+            return mVirtualDevice.getInputDeviceId(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index 901401fe..e569dbf 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -24,8 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
  * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
@@ -36,26 +34,13 @@
  * @hide
  */
 @SystemApi
-public class VirtualKeyboard implements Closeable {
+public class VirtualKeyboard extends VirtualInputDevice {
 
     private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER;
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 6e2b56a..7eba2b8 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -25,8 +25,6 @@
 import android.os.RemoteException;
 import android.view.MotionEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
  * trackpad.
@@ -37,25 +35,11 @@
  * @hide
  */
 @SystemApi
-public class VirtualMouse implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
+public class VirtualMouse extends VirtualInputDevice {
 
     /** @hide */
     public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index c8d602a..0d07753 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -23,8 +23,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
-import java.io.Closeable;
-
 /**
  * A virtual touchscreen representing a touch-based display input mechanism on a remote device.
  *
@@ -34,25 +32,10 @@
  * @hide
  */
 @SystemApi
-public class VirtualTouchscreen implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
-
+public class VirtualTouchscreen extends VirtualInputDevice {
     /** @hide */
     public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/location/ActivityRecognitionHardware.java b/core/java/android/hardware/location/ActivityRecognitionHardware.java
index 20d6338..2754096 100644
--- a/core/java/android/hardware/location/ActivityRecognitionHardware.java
+++ b/core/java/android/hardware/location/ActivityRecognitionHardware.java
@@ -91,12 +91,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public String[] getSupportedActivities() {
+        super.getSupportedActivities_enforcePermission();
+
         return mSupportedActivities;
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public boolean isActivitySupported(String activity) {
+        super.isActivitySupported_enforcePermission();
+
         int activityType = getActivityType(activity);
         return activityType != INVALID_ACTIVITY_TYPE;
     }
@@ -104,12 +108,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public boolean registerSink(IActivityRecognitionHardwareSink sink) {
+        super.registerSink_enforcePermission();
+
         return mSinks.register(sink);
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public boolean unregisterSink(IActivityRecognitionHardwareSink sink) {
+        super.unregisterSink_enforcePermission();
+
         return mSinks.unregister(sink);
     }
 
@@ -117,6 +125,8 @@
     @Override
     public boolean enableActivityEvent(String activity, int eventType, long reportLatencyNs) {
 
+        super.enableActivityEvent_enforcePermission();
+
         int activityType = getActivityType(activity);
         if (activityType == INVALID_ACTIVITY_TYPE) {
             return false;
@@ -134,6 +144,8 @@
     @Override
     public boolean disableActivityEvent(String activity, int eventType) {
 
+        super.disableActivityEvent_enforcePermission();
+
         int activityType = getActivityType(activity);
         if (activityType == INVALID_ACTIVITY_TYPE) {
             return false;
@@ -150,6 +162,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public boolean flush() {
+        super.flush_enforcePermission();
+
         int result = nativeFlush();
         return result == NATIVE_SUCCESS_RESULT;
     }
diff --git a/core/java/android/hardware/location/GeofenceHardwareService.java b/core/java/android/hardware/location/GeofenceHardwareService.java
index 106bfd5..99c1e16 100644
--- a/core/java/android/hardware/location/GeofenceHardwareService.java
+++ b/core/java/android/hardware/location/GeofenceHardwareService.java
@@ -79,6 +79,8 @@
         @Override
         public int[] getMonitoringTypes() {
 
+            super.getMonitoringTypes_enforcePermission();
+
             return mGeofenceHardwareImpl.getMonitoringTypes();
         }
 
@@ -86,6 +88,8 @@
         @Override
         public int getStatusOfMonitoringType(int monitoringType) {
 
+            super.getStatusOfMonitoringType_enforcePermission();
+
             return mGeofenceHardwareImpl.getStatusOfMonitoringType(monitoringType);
         }
 
@@ -95,6 +99,8 @@
                 int monitoringType,
                 GeofenceHardwareRequestParcelable request,
                 IGeofenceHardwareCallback callback) {
+            super.addCircularFence_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.addCircularFence(monitoringType, request, callback);
         }
@@ -103,6 +109,8 @@
         @Override
         public boolean removeGeofence(int id, int monitoringType) {
 
+            super.removeGeofence_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.removeGeofence(id, monitoringType);
         }
@@ -111,6 +119,8 @@
         @Override
         public boolean pauseGeofence(int id, int monitoringType) {
 
+            super.pauseGeofence_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.pauseGeofence(id, monitoringType);
         }
@@ -119,6 +129,8 @@
         @Override
         public boolean resumeGeofence(int id, int monitoringType, int monitorTransitions) {
 
+            super.resumeGeofence_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.resumeGeofence(id, monitoringType, monitorTransitions);
         }
@@ -128,6 +140,8 @@
         public boolean registerForMonitorStateChangeCallback(int monitoringType,
                 IGeofenceHardwareMonitorCallback callback) {
 
+            super.registerForMonitorStateChangeCallback_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.registerForMonitorStateChangeCallback(monitoringType,
                     callback);
@@ -138,6 +152,8 @@
         public boolean unregisterForMonitorStateChangeCallback(int monitoringType,
                 IGeofenceHardwareMonitorCallback callback) {
 
+            super.unregisterForMonitorStateChangeCallback_enforcePermission();
+
             checkPermission(Binder.getCallingPid(), Binder.getCallingUid(), monitoringType);
             return mGeofenceHardwareImpl.unregisterForMonitorStateChangeCallback(monitoringType,
                     callback);
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
index 8febed3..3ba3ebc 100644
--- a/core/java/android/hardware/radio/Announcement.java
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -85,9 +85,9 @@
     /** @hide */
     public Announcement(@NonNull ProgramSelector selector, @Type int type,
             @NonNull Map<String, String> vendorInfo) {
-        mSelector = Objects.requireNonNull(selector);
-        mType = Objects.requireNonNull(type);
-        mVendorInfo = Objects.requireNonNull(vendorInfo);
+        mSelector = Objects.requireNonNull(selector, "Program selector cannot be null");
+        mType = type;
+        mVendorInfo = Objects.requireNonNull(vendorInfo, "Vendor info cannot be null");
     }
 
     private Announcement(@NonNull Parcel in) {
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index f2525d1..ade9fd6 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -160,6 +160,7 @@
      * Disables list updates and releases all resources.
      */
     public void close() {
+        OnCloseListener onCompleteListenersCopied = null;
         synchronized (mLock) {
             if (mIsClosed) return;
             mIsClosed = true;
@@ -167,10 +168,14 @@
             mListCallbacks.clear();
             mOnCompleteListeners.clear();
             if (mOnCloseListener != null) {
-                mOnCloseListener.onClose();
+                onCompleteListenersCopied = mOnCloseListener;
                 mOnCloseListener = null;
             }
         }
+
+        if (onCompleteListenersCopied != null) {
+            onCompleteListenersCopied.onClose();
+        }
     }
 
     void apply(Chunk chunk) {
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 36ac1a0..8a92135 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -533,7 +533,6 @@
         mProgramType = in.readInt();
         mPrimaryId = in.readTypedObject(Identifier.CREATOR);
         mSecondaryIds = in.createTypedArray(Identifier.CREATOR);
-        Arrays.sort(mSecondaryIds);
         if (Stream.of(mSecondaryIds).anyMatch(id -> id == null)) {
             throw new IllegalArgumentException("secondaryIds list must not contain nulls");
         }
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 4334116..59465db 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -36,6 +36,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -250,7 +251,8 @@
                     Objects.requireNonNull(entry.getValue());
                 }
             }
-            mDabFrequencyTable = dabFrequencyTable;
+            mDabFrequencyTable = (dabFrequencyTable == null || dabFrequencyTable.isEmpty())
+                    ? null : dabFrequencyTable;
             mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo;
         }
 
@@ -445,7 +447,8 @@
             mIsBgScanSupported = in.readInt() == 1;
             mSupportedProgramTypes = arrayToSet(in.createIntArray());
             mSupportedIdentifierTypes = arrayToSet(in.createIntArray());
-            mDabFrequencyTable = Utils.readStringIntMap(in);
+            Map<String, Integer> dabFrequencyTableIn = Utils.readStringIntMap(in);
+            mDabFrequencyTable = (dabFrequencyTableIn.isEmpty()) ? null : dabFrequencyTableIn;
             mVendorInfo = Utils.readStringMap(in);
         }
 
@@ -1851,9 +1854,17 @@
     /**
      * @hide
      */
-    public RadioManager(@NonNull Context context) throws ServiceNotFoundException {
+    public RadioManager(Context context) throws ServiceNotFoundException {
+        this(context, IRadioService.Stub.asInterface(ServiceManager.getServiceOrThrow(
+                Context.RADIO_SERVICE)));
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public RadioManager(Context context, IRadioService service) {
         mContext = context;
-        mService = IRadioService.Stub.asInterface(
-                ServiceManager.getServiceOrThrow(Context.RADIO_SERVICE));
+        mService = service;
     }
 }
diff --git a/core/java/android/hardware/usb/IUsbManager.aidl b/core/java/android/hardware/usb/IUsbManager.aidl
index 5a44244..51236fe3 100644
--- a/core/java/android/hardware/usb/IUsbManager.aidl
+++ b/core/java/android/hardware/usb/IUsbManager.aidl
@@ -79,9 +79,20 @@
     /* Returns true if the caller has permission to access the device. */
     boolean hasDevicePermission(in UsbDevice device, String packageName);
 
+    /* Returns true if the given package/pid/uid has permission to access the device. */
+    @JavaPassthrough(annotation=
+            "@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USB)")
+    boolean hasDevicePermissionWithIdentity(in UsbDevice device, String packageName,
+            int pid, int uid);
+
     /* Returns true if the caller has permission to access the accessory. */
     boolean hasAccessoryPermission(in UsbAccessory accessory);
 
+    /* Returns true if the given pid/uid has permission to access the accessory. */
+    @JavaPassthrough(annotation=
+            "@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USB)")
+    boolean hasAccessoryPermissionWithIdentity(in UsbAccessory accessory, int pid, int uid);
+
     /* Requests permission for the given package to access the device.
      * Will display a system dialog to query the user if permission
      * had not already been given.
diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java
index 60d8cac..7c2e518 100644
--- a/core/java/android/hardware/usb/UsbDeviceConnection.java
+++ b/core/java/android/hardware/usb/UsbDeviceConnection.java
@@ -108,6 +108,34 @@
     }
 
     /**
+     * This is meant to be called by UsbRequest's queue() in order to synchronize on
+     * UsbDeviceConnection's mLock to prevent the connection being closed while queueing.
+     */
+    /* package */ boolean queueRequest(UsbRequest request, ByteBuffer buffer, int length) {
+        synchronized (mLock) {
+            if (!isOpen()) {
+                return false;
+            }
+
+            return request.queueIfConnectionOpen(buffer, length);
+        }
+    }
+
+    /**
+     * This is meant to be called by UsbRequest's queue() in order to synchronize on
+     * UsbDeviceConnection's mLock to prevent the connection being closed while queueing.
+     */
+    /* package */ boolean queueRequest(UsbRequest request, @Nullable ByteBuffer buffer) {
+        synchronized (mLock) {
+            if (!isOpen()) {
+                return false;
+            }
+
+            return request.queueIfConnectionOpen(buffer);
+        }
+    }
+
+    /**
      * Releases all system resources related to the device.
      * Once the object is closed it cannot be used again.
      * The client must call {@link UsbManager#openDevice} again
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index 2c38f70..50dd0064 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -838,6 +838,28 @@
     }
 
     /**
+     * Returns true if the caller has permission to access the device. It's similar to the
+     * {@link #hasPermission(UsbDevice)} but allows to specify a different package/uid/pid.
+     *
+     * <p>Not for third-party apps.</p>
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_USB)
+    @RequiresFeature(PackageManager.FEATURE_USB_HOST)
+    public boolean hasPermission(@NonNull UsbDevice device, @NonNull String packageName,
+            int pid, int uid) {
+        if (mService == null) {
+            return false;
+        }
+        try {
+            return mService.hasDevicePermissionWithIdentity(device, packageName, pid, uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Returns true if the caller has permission to access the accessory.
      * Permission might have been granted temporarily via
      * {@link #requestPermission(UsbAccessory, PendingIntent)} or
@@ -859,6 +881,27 @@
     }
 
     /**
+     * Returns true if the caller has permission to access the accessory. It's similar to the
+     * {@link #hasPermission(UsbAccessory)} but allows to specify a different uid/pid.
+     *
+     * <p>Not for third-party apps.</p>
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_USB)
+    @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY)
+    public boolean hasPermission(@NonNull UsbAccessory accessory, int pid, int uid) {
+        if (mService == null) {
+            return false;
+        }
+        try {
+            return mService.hasAccessoryPermissionWithIdentity(accessory, pid, uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+   /**
      * Requests temporary permission for the given package to access the device.
      * This may result in a system dialog being displayed to the user
      * if permission had not already been granted.
diff --git a/core/java/android/hardware/usb/UsbRequest.java b/core/java/android/hardware/usb/UsbRequest.java
index 6ac5e8d..beb0f8d 100644
--- a/core/java/android/hardware/usb/UsbRequest.java
+++ b/core/java/android/hardware/usb/UsbRequest.java
@@ -113,11 +113,13 @@
      * Releases all resources related to this request.
      */
     public void close() {
-        if (mNativeContext != 0) {
-            mEndpoint = null;
-            mConnection = null;
-            native_close();
-            mCloseGuard.close();
+        synchronized (mLock) {
+            if (mNativeContext != 0) {
+                mEndpoint = null;
+                mConnection = null;
+                native_close();
+                mCloseGuard.close();
+            }
         }
     }
 
@@ -191,10 +193,32 @@
      */
     @Deprecated
     public boolean queue(ByteBuffer buffer, int length) {
+        UsbDeviceConnection connection = mConnection;
+        if (connection == null) {
+            // The expected exception by CTS Verifier - USB Device test
+            throw new NullPointerException("invalid connection");
+        }
+
+        // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent
+        // the connection being closed while queueing.
+        return connection.queueRequest(this, buffer, length);
+    }
+
+    /**
+     * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over
+     * there, to prevent the connection being closed while queueing.
+     */
+    /* package */ boolean queueIfConnectionOpen(ByteBuffer buffer, int length) {
+        UsbDeviceConnection connection = mConnection;
+        if (connection == null || !connection.isOpen()) {
+            // The expected exception by CTS Verifier - USB Device test
+            throw new NullPointerException("invalid connection");
+        }
+
         boolean out = (mEndpoint.getDirection() == UsbConstants.USB_DIR_OUT);
         boolean result;
 
-        if (mConnection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P
+        if (connection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P
                 && length > MAX_USBFS_BUFFER_SIZE) {
             length = MAX_USBFS_BUFFER_SIZE;
         }
@@ -243,6 +267,28 @@
      * @return true if the queueing operation succeeded
      */
     public boolean queue(@Nullable ByteBuffer buffer) {
+        UsbDeviceConnection connection = mConnection;
+        if (connection == null) {
+            // The expected exception by CTS Verifier - USB Device test
+            throw new IllegalStateException("invalid connection");
+        }
+
+        // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent
+        // the connection being closed while queueing.
+        return connection.queueRequest(this, buffer);
+    }
+
+    /**
+     * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over
+     * there, to prevent the connection being closed while queueing.
+     */
+    /* package */ boolean queueIfConnectionOpen(@Nullable ByteBuffer buffer) {
+        UsbDeviceConnection connection = mConnection;
+        if (connection == null || !connection.isOpen()) {
+            // The expected exception by CTS Verifier - USB Device test
+            throw new IllegalStateException("invalid connection");
+        }
+
         // Request need to be initialized
         Preconditions.checkState(mNativeContext != 0, "request is not initialized");
 
@@ -260,7 +306,7 @@
                 mIsUsingNewQueue = true;
                 wasQueued = native_queue(null, 0, 0);
             } else {
-                if (mConnection.getContext().getApplicationInfo().targetSdkVersion
+                if (connection.getContext().getApplicationInfo().targetSdkVersion
                         < Build.VERSION_CODES.P) {
                     // Can only send/receive MAX_USBFS_BUFFER_SIZE bytes at once
                     Preconditions.checkArgumentInRange(buffer.remaining(), 0, MAX_USBFS_BUFFER_SIZE,
@@ -363,11 +409,12 @@
      * @return true if cancelling succeeded
      */
     public boolean cancel() {
-        if (mConnection == null) {
+        UsbDeviceConnection connection = mConnection;
+        if (connection == null) {
             return false;
         }
 
-        return mConnection.cancelRequest(this);
+        return connection.cancelRequest(this);
     }
 
     /**
@@ -382,7 +429,8 @@
      * @return true if cancelling succeeded.
      */
     /* package */ boolean cancelIfOpen() {
-        if (mNativeContext == 0 || (mConnection != null && !mConnection.isOpen())) {
+        UsbDeviceConnection connection = mConnection;
+        if (mNativeContext == 0 || (connection != null && !connection.isOpen())) {
             Log.w(TAG,
                     "Detected attempt to cancel a request on a connection which isn't open");
             return false;
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index d23fb36..d55367f 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -32,6 +32,7 @@
 import android.util.Log;
 import android.view.InputChannel;
 import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethod;
@@ -93,9 +94,10 @@
     final int mTargetSdkVersion;
 
     /**
-     * This is not {@null} only between {@link #bindInput(InputBinding)} and {@link #unbindInput()}
-     * so that {@link RemoteInputConnection} can query if {@link #unbindInput()} has already been
-     * called or not, mainly to avoid unnecessary blocking operations.
+     * This is not {@code null} only between {@link #bindInput(InputBinding)} and
+     * {@link #unbindInput()} so that {@link RemoteInputConnection} can query if
+     * {@link #unbindInput()} has already been called or not, mainly to avoid unnecessary
+     * blocking operations.
      *
      * <p>This field must be set and cleared only from the binder thread(s), where the system
      * guarantees that {@link #bindInput(InputBinding)},
@@ -219,18 +221,26 @@
                 return;
             case DO_SHOW_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
+                final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
                 if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
+                    ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                     inputMethod.showSoftInputWithToken(
-                            msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+                            msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                 }
                 args.recycle();
                 return;
             }
             case DO_HIDE_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
+                final ImeTracker.Token statsToken = (ImeTracker.Token) args.arg3;
                 if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
+                    ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                     inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
-                            (IBinder) args.arg1);
+                            (IBinder) args.arg1, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_IME_WRAPPER_DISPATCH);
                 }
                 args.recycle();
                 return;
@@ -416,16 +426,20 @@
 
     @BinderThread
     @Override
-    public void showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
-        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_SHOW_SOFT_INPUT,
-                flags, showInputToken, resultReceiver));
+    public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
+                flags, showInputToken, resultReceiver, statsToken));
     }
 
     @BinderThread
     @Override
-    public void hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
-        mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_HIDE_SOFT_INPUT,
-                flags, hideInputToken, resultReceiver));
+    public void hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
+        mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_HIDE_SOFT_INPUT,
+                flags, hideInputToken, resultReceiver, statsToken));
     }
 
     @BinderThread
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index 891da24..8759a6a 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -16,30 +16,30 @@
 
 package android.inputmethodservice;
 
+import static android.view.inputmethod.TextBoundsInfoResult.CODE_CANCELLED;
+
 import android.annotation.AnyThread;
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
-import android.view.inputmethod.DeleteGesture;
-import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.HandwritingGesture;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputContentInfo;
-import android.view.inputmethod.InsertGesture;
-import android.view.inputmethod.JoinOrSplitGesture;
-import android.view.inputmethod.RemoveSpaceGesture;
-import android.view.inputmethod.SelectGesture;
-import android.view.inputmethod.SelectRangeGesture;
+import android.view.inputmethod.ParcelableHandwritingGesture;
 import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.TextAttribute;
+import android.view.inputmethod.TextBoundsInfo;
+import android.view.inputmethod.TextBoundsInfoResult;
 
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.inputmethod.IRemoteInputConnection;
@@ -47,6 +47,7 @@
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -98,6 +99,44 @@
     };
 
     /**
+     * Subclass of {@link ResultReceiver} used by
+     * {@link #requestTextBoundsInfo(RectF, Executor, Consumer)} for providing
+     * callback.
+     */
+    private static final class TextBoundsInfoResultReceiver extends ResultReceiver {
+        @Nullable
+        private Consumer<TextBoundsInfoResult> mConsumer;
+        @Nullable
+        private Executor mExecutor;
+
+        TextBoundsInfoResultReceiver(@NonNull Executor executor,
+                @NonNull Consumer<TextBoundsInfoResult> consumer) {
+            super(null);
+            mExecutor = executor;
+            mConsumer = consumer;
+        }
+
+        @Override
+        protected void onReceiveResult(@TextBoundsInfoResult.ResultCode int resultCode,
+                @Nullable Bundle resultData) {
+            synchronized (this) {
+                if (mExecutor != null && mConsumer != null) {
+                    final TextBoundsInfoResult textBoundsInfoResult = new TextBoundsInfoResult(
+                            resultCode, TextBoundsInfo.createFromBundle(resultData));
+                    mExecutor.execute(() -> mConsumer.accept(textBoundsInfoResult));
+                    // provide callback only once.
+                    clear();
+                }
+            }
+        }
+
+        private void clear() {
+            mExecutor = null;
+            mConsumer = null;
+        }
+    }
+
+    /**
      * Creates a new instance of {@link IRemoteInputConnectionInvoker} for the given
      * {@link IRemoteInputConnection}.
      *
@@ -637,50 +676,19 @@
     }
 
     /**
-     * Invokes one of {@link IRemoteInputConnection#performHandwritingSelectGesture},
-     * {@link IRemoteInputConnection#performHandwritingSelectRangeGesture},
-     * {@link IRemoteInputConnection#performHandwritingDeleteGesture},
-     * {@link IRemoteInputConnection#performHandwritingDeleteRangeGesture},
-     * {@link IRemoteInputConnection#performHandwritingInsertGesture},
-     * {@link IRemoteInputConnection#performHandwritingRemoveSpaceGesture},
-     * {@link IRemoteInputConnection#performHandwritingJoinOrSplitGesture}.
+     * Invokes {@link IRemoteInputConnection#performHandwritingGesture(
+     * InputConnectionCommandHeader, ParcelableHandwritingGesture, ResultReceiver)}.
      */
     @AnyThread
-    public void performHandwritingGesture(
-            @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
-            @Nullable IntConsumer consumer) {
-
+    public void performHandwritingGesture(@NonNull ParcelableHandwritingGesture gesture,
+            @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer) {
         ResultReceiver resultReceiver = null;
         if (consumer != null) {
             Objects.requireNonNull(executor);
             resultReceiver = new IntResultReceiver(executor, consumer);
         }
         try {
-            if (gesture instanceof SelectGesture) {
-                mConnection.performHandwritingSelectGesture(
-                        createHeader(), (SelectGesture) gesture, resultReceiver);
-            } else if (gesture instanceof SelectRangeGesture) {
-                mConnection.performHandwritingSelectRangeGesture(
-                        createHeader(), (SelectRangeGesture) gesture, resultReceiver);
-            } else if (gesture instanceof InsertGesture) {
-                mConnection.performHandwritingInsertGesture(
-                        createHeader(), (InsertGesture) gesture, resultReceiver);
-            } else if (gesture instanceof DeleteGesture) {
-                mConnection.performHandwritingDeleteGesture(
-                        createHeader(), (DeleteGesture) gesture, resultReceiver);
-            } else if (gesture instanceof DeleteRangeGesture) {
-                mConnection.performHandwritingDeleteRangeGesture(
-                        createHeader(), (DeleteRangeGesture) gesture, resultReceiver);
-            } else if (gesture instanceof RemoveSpaceGesture) {
-                mConnection.performHandwritingRemoveSpaceGesture(
-                        createHeader(), (RemoveSpaceGesture) gesture, resultReceiver);
-            } else if (gesture instanceof JoinOrSplitGesture) {
-                mConnection.performHandwritingJoinOrSplitGesture(
-                        createHeader(), (JoinOrSplitGesture) gesture, resultReceiver);
-            } else if (consumer != null && executor != null) {
-                executor.execute(()
-                        -> consumer.accept(InputConnection.HANDWRITING_GESTURE_RESULT_UNSUPPORTED));
-            }
+            mConnection.performHandwritingGesture(createHeader(), gesture, resultReceiver);
         } catch (RemoteException e) {
             if (consumer != null && executor != null) {
                 executor.execute(() -> consumer.accept(
@@ -690,6 +698,27 @@
     }
 
     /**
+     * Invokes one of {@link IRemoteInputConnection#previewHandwritingGesture(
+     * InputConnectionCommandHeader, ParcelableHandwritingGesture, CancellationSignal)}
+     */
+    @AnyThread
+    public boolean previewHandwritingGesture(
+            @NonNull ParcelableHandwritingGesture gesture,
+            @Nullable CancellationSignal cancellationSignal) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return false; // cancelled.
+        }
+
+        // TODO(b/254727073): Implement CancellationSignal
+        try {
+            mConnection.previewHandwritingGesture(createHeader(), gesture, null);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
      * Invokes {@link IRemoteInputConnection#requestCursorUpdates(InputConnectionCommandHeader, int,
      * int, AndroidFuture)}.
      *
@@ -736,6 +765,28 @@
     }
 
     /**
+     * Invokes {@link IRemoteInputConnection#requestTextBoundsInfo(InputConnectionCommandHeader,
+     * RectF, ResultReceiver)}
+     * @param rectF {@code rectF} parameter to be passed.
+     * @param executor {@code Executor} parameter to be passed.
+     * @param consumer {@code Consumer} parameter to be passed.
+     */
+    @AnyThread
+    public void requestTextBoundsInfo(
+            @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<TextBoundsInfoResult> consumer) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(consumer);
+
+        final ResultReceiver resultReceiver = new TextBoundsInfoResultReceiver(executor, consumer);
+        try {
+            mConnection.requestTextBoundsInfo(createHeader(), rectF, resultReceiver);
+        } catch (RemoteException e) {
+            executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_CANCELLED)));
+        }
+    }
+
+    /**
      * Invokes {@link IRemoteInputConnection#commitContent(InputConnectionCommandHeader,
      * InputContentInfo, int, Bundle, AndroidFuture)}.
      *
@@ -777,8 +828,7 @@
     }
 
     /**
-     * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
-     * CharSequence, TextAttribute)}.
+     * Replaces the specific range in the current input field with suggested text.
      *
      * @param start the character index where the replacement should start.
      * @param end the character index where the replacement should end.
@@ -788,6 +838,9 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the specific range is replaced successfully, {@code false} otherwise.
+     * @see android.view.inputmethod.InputConnection#replaceText(int, int, CharSequence, int,
+     *     TextAttribute)
      */
     @AnyThread
     public boolean replaceText(
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 39d362b..bf4fc4a 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -124,6 +124,7 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InlineSuggestionsRequest;
 import android.view.inputmethod.InlineSuggestionsResponse;
 import android.view.inputmethod.InputBinding;
@@ -669,6 +670,10 @@
      */
     private IBinder mCurHideInputToken;
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mCurStatsToken;
+
     final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
         onComputeInsets(mTmpInsets);
         if (!mViewsCreated) {
@@ -870,10 +875,12 @@
         @MainThread
         @Override
         public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-                IBinder hideInputToken) {
+                IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
             mSystemCallingHideSoftInput = true;
             mCurHideInputToken = hideInputToken;
+            mCurStatsToken = statsToken;
             hideSoftInput(flags, resultReceiver);
+            mCurStatsToken = null;
             mCurHideInputToken = null;
             mSystemCallingHideSoftInput = false;
         }
@@ -884,6 +891,7 @@
         @MainThread
         @Override
         public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
+            ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_HIDE_SOFT_INPUT);
             if (DEBUG) Log.v(TAG, "hideSoftInput()");
             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
                     && !mSystemCallingHideSoftInput) {
@@ -918,12 +926,17 @@
         @MainThread
         @Override
         public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-                IBinder showInputToken) {
+                IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
             mSystemCallingShowSoftInput = true;
             mCurShowInputToken = showInputToken;
-            showSoftInput(flags, resultReceiver);
-            mCurShowInputToken = null;
-            mSystemCallingShowSoftInput = false;
+            mCurStatsToken = statsToken;
+            try {
+                showSoftInput(flags, resultReceiver);
+            } finally {
+                mCurStatsToken = null;
+                mCurShowInputToken = null;
+                mSystemCallingShowSoftInput = false;
+            }
         }
 
         /**
@@ -932,6 +945,7 @@
         @MainThread
         @Override
         public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+            ImeTracker.get().onProgress(mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
             if (DEBUG) Log.v(TAG, "showSoftInput()");
             // TODO(b/148086656): Disallow IME developers from calling InputMethodImpl methods.
             if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R
@@ -947,7 +961,12 @@
                     null /* icProto */);
             final boolean wasVisible = isInputViewShown();
             if (dispatchOnShowInputRequested(flags, false)) {
+                ImeTracker.get().onProgress(mCurStatsToken,
+                        ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
                 showWindow(true);
+            } else {
+                ImeTracker.get().onFailed(mCurStatsToken,
+                        ImeTracker.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE);
             }
             setImeWindowStatus(mapToImeWindowStatus(), mBackDisposition);
 
@@ -1774,16 +1793,13 @@
     }
 
     /**
-     * Implement to return our standard {@link InputMethodImpl}.  Subclasses
-     * can override to provide their own customized version.
+     * Implement to return our standard {@link InputMethodImpl}.
      *
-     * @deprecated IME developers don't need to override this method to get callbacks information.
-     * Most methods in {@link InputMethodImpl} have corresponding callbacks.
-     * Use {@link InputMethodService#onBindInput()}, {@link InputMethodService#onUnbindInput()},
-     * {@link InputMethodService#onWindowShown()}, {@link InputMethodService#onWindowHidden()}, etc.
-     *
-     * <p>Starting from Android U and later, override this method won't guarantee that IME works
-     * as previous platform behavior.</p>
+     * @deprecated Overriding or calling this method is strongly discouraged. A future version of
+     * Android will remove the ability to use this method. Use the callbacks on
+     * {@link InputMethodService} as {@link InputMethodService#onBindInput()},
+     * {@link InputMethodService#onUnbindInput()}, {@link InputMethodService#onWindowShown()},
+     * {@link InputMethodService#onWindowHidden()}, etc.
      */
     @Deprecated
     @Override
@@ -1792,18 +1808,17 @@
     }
     
     /**
-     * Implement to return our standard {@link InputMethodSessionImpl}.  Subclasses
-     * can override to provide their own customized version.
+     * Implement to return our standard {@link InputMethodSessionImpl}.
      *
-     * @deprecated IME developers don't need to override this method to get callbacks information.
+     * <p>IMEs targeting on Android U and above cannot override this method, or an
+     * {@link LinkageError} would be thrown.</p>
+     *
+     * @deprecated Overriding or calling this method is strongly discouraged.
      * Most methods in {@link InputMethodSessionImpl} have corresponding callbacks.
      * Use {@link InputMethodService#onFinishInput()},
      * {@link InputMethodService#onDisplayCompletions(CompletionInfo[])},
      * {@link InputMethodService#onUpdateExtractedText(int, ExtractedText)},
      * {@link InputMethodService#onUpdateSelection(int, int, int, int, int, int)} instead.
-     *
-     * <p>IMEs targeting on Android U and above cannot override this method, or an
-     * {@link LinkageError} would be thrown.</p>
      */
     @Deprecated
     @Override
@@ -2272,6 +2287,8 @@
      * current input field.
      * 
      * @param id Unique identifier of the new input method to start.
+     * @throws IllegalArgumentException if the input method is unknown or filtered
+     * by the rules of <a href="/training/basics/intents/package-visibility">package visibility</a>.
      */
     public void switchInputMethod(String id) {
         mPrivOps.setInputMethod(id);
@@ -2284,6 +2301,8 @@
      *
      * @param id Unique identifier of the new input method to start.
      * @param subtype The new subtype of the new input method to be switched to.
+     * @throws IllegalArgumentException if the input method is unknown or filtered
+     * by the rules of <a href="/training/basics/intents/package-visibility">package visibility</a>.
      */
     public final void switchInputMethod(String id, InputMethodSubtype subtype) {
         mPrivOps.setInputMethodAndSubtype(id, subtype);
@@ -2923,8 +2942,10 @@
         ImeTracing.getInstance().triggerServiceDump(
                 "InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
                 null /* icProto */);
+        ImeTracker.get().onProgress(mCurStatsToken,
+                ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
         mPrivOps.applyImeVisibilityAsync(setVisible
-                ? mCurShowInputToken : mCurHideInputToken, setVisible);
+                ? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
     }
 
     private void finishViews(boolean finishingInput) {
diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java
index 09e86c4..f93f9ab 100644
--- a/core/java/android/inputmethodservice/RemoteInputConnection.java
+++ b/core/java/android/inputmethodservice/RemoteInputConnection.java
@@ -21,7 +21,9 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -32,8 +34,11 @@
 import android.view.inputmethod.HandwritingGesture;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.ParcelableHandwritingGesture;
+import android.view.inputmethod.PreviewableHandwritingGesture;
 import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.TextAttribute;
+import android.view.inputmethod.TextBoundsInfoResult;
 
 import com.android.internal.inputmethod.CancellationGroup;
 import com.android.internal.inputmethod.CompletableFutureUtil;
@@ -44,6 +49,7 @@
 import java.lang.ref.WeakReference;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -418,7 +424,16 @@
     public void performHandwritingGesture(
             @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
             @Nullable IntConsumer consumer) {
-        mInvoker.performHandwritingGesture(gesture, executor, consumer);
+        mInvoker.performHandwritingGesture(ParcelableHandwritingGesture.of(gesture), executor,
+                consumer);
+    }
+
+    @AnyThread
+    public boolean previewHandwritingGesture(
+            @NonNull PreviewableHandwritingGesture gesture,
+            @Nullable CancellationSignal cancellationSignal) {
+        return mInvoker.previewHandwritingGesture(ParcelableHandwritingGesture.of(gesture),
+                cancellationSignal);
     }
 
     @AnyThread
@@ -460,6 +475,13 @@
     }
 
     @AnyThread
+    public void requestTextBoundsInfo(
+            @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<TextBoundsInfoResult> consumer) {
+        mInvoker.requestTextBoundsInfo(rectF, executor, consumer);
+    }
+
+    @AnyThread
     public Handler getHandler() {
         // Nothing should happen when called from input method.
         return null;
diff --git a/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java b/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
index 76ee097..d4b76c8 100644
--- a/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
+++ b/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
@@ -37,7 +37,7 @@
 import android.util.AtomicFile;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FastDataInput;
+import com.android.internal.util.ArtFastDataInput;
 
 import libcore.io.IoUtils;
 
@@ -53,8 +53,8 @@
 import java.net.ProtocolException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -89,12 +89,10 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface Prefix {}
 
-    private static final HashMap<String, String> sPrefixLegacyFileNameMap =
-            new HashMap<String, String>() {{
-                put(PREFIX_XT, "netstats_xt.bin");
-                put(PREFIX_UID, "netstats_uid.bin");
-                put(PREFIX_UID_TAG, "netstats_uid.bin");
-            }};
+    private static final Map<String, String> sPrefixLegacyFileNameMap = Map.of(
+            PREFIX_XT, "netstats_xt.bin",
+            PREFIX_UID, "netstats_uid.bin",
+            PREFIX_UID_TAG, "netstats_uid.bin");
 
     // These version constants are copied from NetworkStatsCollection/History, which is okay for
     // OEMs to modify to adapt their own logic.
@@ -254,7 +252,7 @@
     private static void readPlatformCollection(@NonNull NetworkStatsCollection.Builder builder,
             @NonNull File file) throws IOException {
         final FileInputStream is = new FileInputStream(file);
-        final FastDataInput dataIn = new FastDataInput(is, BUFFER_SIZE);
+        final ArtFastDataInput dataIn = new ArtFastDataInput(is, BUFFER_SIZE);
         try {
             readPlatformCollection(builder, dataIn);
         } finally {
diff --git a/core/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java b/core/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java
index 4bc5b49..0427742 100644
--- a/core/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java
+++ b/core/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java
@@ -53,7 +53,7 @@
         if (in.keySet().size() != EXPECTED_BUNDLE_KEY_CNT) {
             throw new IllegalArgumentException(
                     String.format(
-                            "Expect PersistableBundle to have %d element but found: %d",
+                            "Expect PersistableBundle to have %d element but found: %s",
                             EXPECTED_BUNDLE_KEY_CNT, in.keySet()));
         }
 
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 64c1211..3282d56 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -29,7 +29,6 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.IntentFilter;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.nfc.tech.MifareClassic;
@@ -525,66 +524,6 @@
     }
 
     /**
-     * Helper to check if this device has FEATURE_NFC_BEAM, but without using
-     * a context.
-     * Equivalent to
-     * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)
-     */
-    private static boolean hasBeamFeature() {
-        IPackageManager pm = ActivityThread.getPackageManager();
-        if (pm == null) {
-            Log.e(TAG, "Cannot get package manager, assuming no Android Beam feature");
-            return false;
-        }
-        try {
-            return pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM, 0);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Package manager query failed, assuming no Android Beam feature", e);
-            return false;
-        }
-    }
-
-    /**
-     * Helper to check if this device has FEATURE_NFC, but without using
-     * a context.
-     * Equivalent to
-     * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)
-     */
-    private static boolean hasNfcFeature() {
-        IPackageManager pm = ActivityThread.getPackageManager();
-        if (pm == null) {
-            Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
-            return false;
-        }
-        try {
-            return pm.hasSystemFeature(PackageManager.FEATURE_NFC, 0);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
-            return false;
-        }
-    }
-
-    /**
-     * Helper to check if this device is NFC HCE capable, by checking for
-     * FEATURE_NFC_HOST_CARD_EMULATION and/or FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
-     * but without using a context.
-     */
-    private static boolean hasNfcHceFeature() {
-        IPackageManager pm = ActivityThread.getPackageManager();
-        if (pm == null) {
-            Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
-            return false;
-        }
-        try {
-            return pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)
-                || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
-            return false;
-        }
-    }
-
-    /**
      * Return list of Secure Elements which support off host card emulation.
      *
      * @return List<String> containing secure elements on the device which supports
@@ -593,23 +532,21 @@
      * @hide
      */
     public @NonNull List<String> getSupportedOffHostSecureElements() {
+        if (mContext == null) {
+            throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
+                    + " getSupportedOffHostSecureElements APIs");
+        }
         List<String> offHostSE = new ArrayList<String>();
-        IPackageManager pm = ActivityThread.getPackageManager();
+        PackageManager pm = mContext.getPackageManager();
         if (pm == null) {
             Log.e(TAG, "Cannot get package manager, assuming no off-host CE feature");
             return offHostSE;
         }
-        try {
-            if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC, 0)) {
-                offHostSE.add("SIM");
-            }
-            if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE, 0)) {
-                offHostSE.add("eSE");
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "Package manager query failed, assuming no off-host CE feature", e);
-            offHostSE.clear();
-            return offHostSE;
+        if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC)) {
+            offHostSE.add("SIM");
+        }
+        if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
+            offHostSE.add("eSE");
         }
         return offHostSE;
     }
@@ -621,10 +558,19 @@
      */
     @UnsupportedAppUsage
     public static synchronized NfcAdapter getNfcAdapter(Context context) {
+        if (context == null) {
+            if (sNullContextNfcAdapter == null) {
+                sNullContextNfcAdapter = new NfcAdapter(null);
+            }
+            return sNullContextNfcAdapter;
+        }
         if (!sIsInitialized) {
-            sHasNfcFeature = hasNfcFeature();
-            sHasBeamFeature = hasBeamFeature();
-            boolean hasHceFeature = hasNfcHceFeature();
+            PackageManager pm = context.getPackageManager();
+            sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
+            sHasBeamFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM);
+            boolean hasHceFeature =
+                    pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
+                    || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF);
             /* is this device meant to have NFC */
             if (!sHasNfcFeature && !hasHceFeature) {
                 Log.v(TAG, "this device does not have NFC support");
@@ -660,12 +606,6 @@
 
             sIsInitialized = true;
         }
-        if (context == null) {
-            if (sNullContextNfcAdapter == null) {
-                sNullContextNfcAdapter = new NfcAdapter(null);
-            }
-            return sNullContextNfcAdapter;
-        }
         NfcAdapter adapter = sNfcAdapters.get(context);
         if (adapter == null) {
             adapter = new NfcAdapter(context);
@@ -676,8 +616,12 @@
 
     /** get handle to NFC service interface */
     private static INfcAdapter getServiceInterface() {
+        if (!sHasNfcFeature) {
+            /* NFC is not supported */
+            return null;
+        }
         /* get a handle to NFC service */
-        IBinder b = ServiceManager.getService("nfc");
+        IBinder b = ServiceManager.waitForService("nfc");
         if (b == null) {
             return null;
         }
@@ -707,6 +651,15 @@
                     "context not associated with any application (using a mock context?)");
         }
 
+        synchronized (NfcAdapter.class) {
+            if (!sIsInitialized) {
+                PackageManager pm = context.getPackageManager();
+                sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
+            }
+            if (!sHasNfcFeature) {
+                return null;
+            }
+        }
         if (getServiceInterface() == null) {
             // NFC is not available
             return null;
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 0b56d19..6a42091 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -22,11 +22,9 @@
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.Activity;
-import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.nfc.INfcCardEmulation;
 import android.nfc.NfcAdapter;
@@ -158,18 +156,13 @@
             throw new UnsupportedOperationException();
         }
         if (!sIsInitialized) {
-            IPackageManager pm = ActivityThread.getPackageManager();
+            PackageManager pm = context.getPackageManager();
             if (pm == null) {
                 Log.e(TAG, "Cannot get PackageManager");
                 throw new UnsupportedOperationException();
             }
-            try {
-                if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)) {
-                    Log.e(TAG, "This device does not support card emulation");
-                    throw new UnsupportedOperationException();
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "PackageManager query failed.");
+            if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) {
+                Log.e(TAG, "This device does not support card emulation");
                 throw new UnsupportedOperationException();
             }
             sIsInitialized = true;
diff --git a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
index 3c92455..48bbf5b6 100644
--- a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
+++ b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
@@ -17,10 +17,8 @@
 package android.nfc.cardemulation;
 
 import android.app.Activity;
-import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.nfc.INfcFCardEmulation;
 import android.nfc.NfcAdapter;
@@ -70,18 +68,13 @@
             throw new UnsupportedOperationException();
         }
         if (!sIsInitialized) {
-            IPackageManager pm = ActivityThread.getPackageManager();
+            PackageManager pm = context.getPackageManager();
             if (pm == null) {
                 Log.e(TAG, "Cannot get PackageManager");
                 throw new UnsupportedOperationException();
             }
-            try {
-                if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0)) {
-                    Log.e(TAG, "This device does not support NFC-F card emulation");
-                    throw new UnsupportedOperationException();
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "PackageManager query failed.");
+            if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) {
+                Log.e(TAG, "This device does not support NFC-F card emulation");
                 throw new UnsupportedOperationException();
             }
             sIsInitialized = true;
diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java
index 068df22..7a153ef 100644
--- a/core/java/android/os/AggregateBatteryConsumer.java
+++ b/core/java/android/os/AggregateBatteryConsumer.java
@@ -17,10 +17,11 @@
 package android.os;
 
 import android.annotation.NonNull;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 0c5f778..e2c52ce 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -22,12 +22,12 @@
 import android.database.CursorWindow;
 import android.util.Range;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.BatteryStatsHistoryIterator;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 4df0139..8e55692 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();
 
     /**
@@ -911,11 +911,15 @@
             final String transactionName = getTransactionName(transactionCode);
             final StringBuffer buf = new StringBuffer();
 
+            // Keep trace name consistent with cpp trace name in:
+            // system/tools/aidl/generate_cpp.cpp
+            buf.append("AIDL::java::");
             if (transactionName != null) {
-                buf.append(mSimpleDescriptor).append(":").append(transactionName);
+                buf.append(mSimpleDescriptor).append("::").append(transactionName);
             } else {
-                buf.append(mSimpleDescriptor).append("#").append(transactionCode);
+                buf.append(mSimpleDescriptor).append("::#").append(transactionCode);
             }
+            buf.append("::server");
 
             transactionTraceName = buf.toString();
             mTransactionTraceNames.setRelease(index, transactionTraceName);
@@ -1219,25 +1223,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.
@@ -1246,8 +1265,21 @@
         // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions
         // disappear into the ether.
         final boolean tagEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_AIDL);
+        final boolean hasFullyQualifiedName = getMaxTransactionId() > 0;
         final String transactionTraceName;
-        if (tagEnabled) {
+
+        if (tagEnabled && hasFullyQualifiedName) {
+            // If tracing enabled and we have a fully qualified name, fetch the name
+            transactionTraceName = getTransactionTraceName(code);
+        } else if (tagEnabled && isStackTrackingEnabled()) {
+            // If tracing is enabled and we *don't* have a fully qualified name, fetch the
+            // 'best effort' name only for stack tracking. This works around noticeable perf impact
+            // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and
+            // the perf impact can be quite noticeable while benchmarking such binder calls.
+            // The primary culprits are ContentProviders and Cursors which convenienty don't
+            // autogenerate their AIDL and hence will not have a fully qualified name.
+            //
+            // TODO(b/253426478): Relax this constraint after a more robust fix
             transactionTraceName = getTransactionTraceName(code);
         } else {
             transactionTraceName = null;
@@ -1255,8 +1287,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);
             }
@@ -1264,7 +1298,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);
@@ -1307,8 +1344,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/BinderProxy.java b/core/java/android/os/BinderProxy.java
index 6330661..1929a4d 100644
--- a/core/java/android/os/BinderProxy.java
+++ b/core/java/android/os/BinderProxy.java
@@ -536,8 +536,8 @@
             mWarnOnBlocking = false;
             warnOnBlocking = false;
 
-            if (Build.IS_USERDEBUG) {
-                // Log this as a WTF on userdebug builds.
+            if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                // Log this as a WTF on userdebug and eng builds.
                 Log.wtf(Binder.TAG,
                         "Outgoing transactions from this process must be FLAG_ONEWAY",
                         new Throwable());
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 249f486..44a1fa5 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -1446,6 +1446,28 @@
         return IS_DEBUGGABLE;
     }
 
+
+    /**
+     * Returns true if the device is running a secure build, such as "user" or "userdebug".
+     *
+     * Secure builds drop adbd privileges by default, though debuggable builds still allow users
+     * to gain root access via local shell. See should_drop_privileges() in adb for details.
+     * @hide
+     */
+    private static final boolean IS_SECURE =
+            SystemProperties.getBoolean("ro.secure", true);
+    /**
+     * Returns true if the device is running a secure build, such as "user" or "userdebug".
+     *
+     * Secure builds drop adbd privileges by default, though debuggable builds still allow users
+     * to gain root access via local shell. See should_drop_privileges() in adb for details.
+     * @hide
+     */
+    @TestApi
+    public static boolean isSecure() {
+        return IS_SECURE;
+    }
+
     /** {@hide} */
     public static final boolean IS_ENG = "eng".equals(TYPE);
     /** {@hide} */
diff --git a/core/java/android/os/BundleMerger.java b/core/java/android/os/BundleMerger.java
new file mode 100644
index 0000000..51bd4ea
--- /dev/null
+++ b/core/java/android/os/BundleMerger.java
@@ -0,0 +1,379 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+
+/**
+ * Configured rules for merging two {@link Bundle} instances.
+ * <p>
+ * By default, values from both {@link Bundle} instances are blended together on
+ * a key-wise basis, and conflicting value definitions for a key are dropped.
+ * <p>
+ * Nuanced strategies for handling conflicting value definitions can be applied
+ * using {@link #setMergeStrategy(String, int)} and
+ * {@link #setDefaultMergeStrategy(int)}.
+ * <p>
+ * When conflicting values have <em>inconsistent</em> data types (such as trying
+ * to merge a {@link String} and a {@link Integer}), both conflicting values are
+ * rejected and the key becomes undefined, regardless of the requested strategy.
+ *
+ * @hide
+ */
+public class BundleMerger implements Parcelable {
+    private static final String TAG = "BundleMerger";
+
+    private @Strategy int mDefaultStrategy = STRATEGY_REJECT;
+
+    private final ArrayMap<String, Integer> mStrategies = new ArrayMap<>();
+
+    /**
+     * Merge strategy that rejects both conflicting values.
+     */
+    public static final int STRATEGY_REJECT = 0;
+
+    /**
+     * Merge strategy that selects the first of conflicting values.
+     */
+    public static final int STRATEGY_FIRST = 1;
+
+    /**
+     * Merge strategy that selects the last of conflicting values.
+     */
+    public static final int STRATEGY_LAST = 2;
+
+    /**
+     * Merge strategy that selects the "minimum" of conflicting values which are
+     * {@link Comparable} with each other.
+     */
+    public static final int STRATEGY_COMPARABLE_MIN = 3;
+
+    /**
+     * Merge strategy that selects the "maximum" of conflicting values which are
+     * {@link Comparable} with each other.
+     */
+    public static final int STRATEGY_COMPARABLE_MAX = 4;
+
+    /**
+     * Merge strategy that numerically adds both conflicting values.
+     */
+    public static final int STRATEGY_NUMBER_ADD = 5;
+
+    /**
+     * Merge strategy that numerically increments the first conflicting value by
+     * {@code 1} and ignores the last conflicting value.
+     */
+    public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 6;
+
+    /**
+     * Merge strategy that combines conflicting values using a boolean "and"
+     * operation.
+     */
+    public static final int STRATEGY_BOOLEAN_AND = 7;
+
+    /**
+     * Merge strategy that combines conflicting values using a boolean "or"
+     * operation.
+     */
+    public static final int STRATEGY_BOOLEAN_OR = 8;
+
+    /**
+     * Merge strategy that combines two conflicting array values by appending
+     * the last array after the first array.
+     */
+    public static final int STRATEGY_ARRAY_APPEND = 9;
+
+    /**
+     * Merge strategy that combines two conflicting {@link ArrayList} values by
+     * appending the last {@link ArrayList} after the first {@link ArrayList}.
+     */
+    public static final int STRATEGY_ARRAY_LIST_APPEND = 10;
+
+    @IntDef(flag = false, prefix = { "STRATEGY_" }, value = {
+            STRATEGY_REJECT,
+            STRATEGY_FIRST,
+            STRATEGY_LAST,
+            STRATEGY_COMPARABLE_MIN,
+            STRATEGY_COMPARABLE_MAX,
+            STRATEGY_NUMBER_ADD,
+            STRATEGY_NUMBER_INCREMENT_FIRST,
+            STRATEGY_BOOLEAN_AND,
+            STRATEGY_BOOLEAN_OR,
+            STRATEGY_ARRAY_APPEND,
+            STRATEGY_ARRAY_LIST_APPEND,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Strategy {}
+
+    /**
+     * Create a empty set of rules for merging two {@link Bundle} instances.
+     */
+    public BundleMerger() {
+    }
+
+    private BundleMerger(@NonNull Parcel in) {
+        mDefaultStrategy = in.readInt();
+        final int N = in.readInt();
+        for (int i = 0; i < N; i++) {
+            mStrategies.put(in.readString(), in.readInt());
+        }
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(mDefaultStrategy);
+        final int N = mStrategies.size();
+        out.writeInt(N);
+        for (int i = 0; i < N; i++) {
+            out.writeString(mStrategies.keyAt(i));
+            out.writeInt(mStrategies.valueAt(i));
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Configure the default merge strategy to be used when there isn't a
+     * more-specific strategy defined for a particular key via
+     * {@link #setMergeStrategy(String, int)}.
+     */
+    public void setDefaultMergeStrategy(@Strategy int strategy) {
+        mDefaultStrategy = strategy;
+    }
+
+    /**
+     * Configure the merge strategy to be used for the given key.
+     * <p>
+     * Subsequent calls for the same key will overwrite any previously
+     * configured strategy.
+     */
+    public void setMergeStrategy(@NonNull String key, @Strategy int strategy) {
+        mStrategies.put(key, strategy);
+    }
+
+    /**
+     * Return the merge strategy to be used for the given key, as defined by
+     * {@link #setMergeStrategy(String, int)}.
+     * <p>
+     * If no specific strategy has been configured for the given key, this
+     * returns {@link #setDefaultMergeStrategy(int)}.
+     */
+    public @Strategy int getMergeStrategy(@NonNull String key) {
+        return (int) mStrategies.getOrDefault(key, mDefaultStrategy);
+    }
+
+    /**
+     * Return a {@link BinaryOperator} which applies the strategies configured
+     * in this object to merge the two given {@link Bundle} arguments.
+     */
+    public BinaryOperator<Bundle> asBinaryOperator() {
+        return this::merge;
+    }
+
+    /**
+     * Apply the strategies configured in this object to merge the two given
+     * {@link Bundle} arguments.
+     *
+     * @return the merged {@link Bundle} result. If one argument is {@code null}
+     *         it will return the other argument. If both arguments are null it
+     *         will return {@code null}.
+     */
+    @SuppressWarnings("deprecation")
+    public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) {
+        if (first == null && last == null) {
+            return null;
+        }
+        if (first == null) {
+            first = Bundle.EMPTY;
+        }
+        if (last == null) {
+            last = Bundle.EMPTY;
+        }
+
+        // Start by bulk-copying all values without attempting to unpack any
+        // custom parcelables; we'll circle back to handle conflicts below
+        final Bundle res = new Bundle();
+        res.putAll(first);
+        res.putAll(last);
+
+        final ArraySet<String> conflictingKeys = new ArraySet<>();
+        conflictingKeys.addAll(first.keySet());
+        conflictingKeys.retainAll(last.keySet());
+        for (int i = 0; i < conflictingKeys.size(); i++) {
+            final String key = conflictingKeys.valueAt(i);
+            final int strategy = getMergeStrategy(key);
+            final Object firstValue = first.get(key);
+            final Object lastValue = last.get(key);
+            try {
+                res.putObject(key, merge(strategy, firstValue, lastValue));
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and "
+                        + lastValue + " using strategy " + strategy, e);
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Merge the two given values. If only one of the values is defined, it
+     * always wins, otherwise the given strategy is applied.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static @Nullable Object merge(@Strategy int strategy,
+            @Nullable Object first, @Nullable Object last) {
+        if (first == null) return last;
+        if (last == null) return first;
+
+        if (first.getClass() != last.getClass()) {
+            throw new IllegalArgumentException("Merging requires consistent classes; first "
+                    + first.getClass() + " last " + last.getClass());
+        }
+
+        switch (strategy) {
+            case STRATEGY_REJECT:
+                // Only actually reject when the values are different
+                if (Objects.deepEquals(first, last)) {
+                    return first;
+                } else {
+                    return null;
+                }
+            case STRATEGY_FIRST:
+                return first;
+            case STRATEGY_LAST:
+                return last;
+            case STRATEGY_COMPARABLE_MIN:
+                return comparableMin(first, last);
+            case STRATEGY_COMPARABLE_MAX:
+                return comparableMax(first, last);
+            case STRATEGY_NUMBER_ADD:
+                return numberAdd(first, last);
+            case STRATEGY_NUMBER_INCREMENT_FIRST:
+                return numberIncrementFirst(first, last);
+            case STRATEGY_BOOLEAN_AND:
+                return booleanAnd(first, last);
+            case STRATEGY_BOOLEAN_OR:
+                return booleanOr(first, last);
+            case STRATEGY_ARRAY_APPEND:
+                return arrayAppend(first, last);
+            case STRATEGY_ARRAY_LIST_APPEND:
+                return arrayListAppend(first, last);
+            default:
+                throw new UnsupportedOperationException();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) {
+        return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) {
+        return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last;
+    }
+
+    private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) {
+        if (first instanceof Integer) {
+            return ((Integer) first) + ((Integer) last);
+        } else if (first instanceof Long) {
+            return ((Long) first) + ((Long) last);
+        } else if (first instanceof Float) {
+            return ((Float) first) + ((Float) last);
+        } else if (first instanceof Double) {
+            return ((Double) first) + ((Double) last);
+        } else {
+            throw new IllegalArgumentException("Unable to add " + first.getClass());
+        }
+    }
+
+    private static @NonNull Number numberIncrementFirst(@NonNull Object first,
+            @NonNull Object last) {
+        if (first instanceof Integer) {
+            return ((Integer) first) + 1;
+        } else if (first instanceof Long) {
+            return ((Long) first) + 1L;
+        } else {
+            throw new IllegalArgumentException("Unable to add " + first.getClass());
+        }
+    }
+
+    private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) {
+        return ((Boolean) first) && ((Boolean) last);
+    }
+
+    private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) {
+        return ((Boolean) first) || ((Boolean) last);
+    }
+
+    private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) {
+        if (!first.getClass().isArray()) {
+            throw new IllegalArgumentException("Unable to append " + first.getClass());
+        }
+        final Class<?> clazz = first.getClass().getComponentType();
+        final int firstLength = Array.getLength(first);
+        final int lastLength = Array.getLength(last);
+        final Object res = Array.newInstance(clazz, firstLength + lastLength);
+        System.arraycopy(first, 0, res, 0, firstLength);
+        System.arraycopy(last, 0, res, firstLength, lastLength);
+        return res;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) {
+        if (!(first instanceof ArrayList)) {
+            throw new IllegalArgumentException("Unable to append " + first.getClass());
+        }
+        final ArrayList<Object> firstList = (ArrayList<Object>) first;
+        final ArrayList<Object> lastList = (ArrayList<Object>) last;
+        final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size());
+        res.addAll(firstList);
+        res.addAll(lastList);
+        return res;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR =
+            new Parcelable.Creator<BundleMerger>() {
+                @Override
+                public BundleMerger createFromParcel(Parcel in) {
+                    return new BundleMerger(in);
+                }
+
+                @Override
+                public BundleMerger[] newArray(int size) {
+                    return new BundleMerger[size];
+                }
+            };
+}
diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java
index d5c3de1..b478a379 100644
--- a/core/java/android/os/FileUtils.java
+++ b/core/java/android/os/FileUtils.java
@@ -84,7 +84,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -1314,31 +1313,31 @@
     private static long toBytes(long value, String unit) {
         unit = unit.toUpperCase();
 
-        if (List.of("B").contains(unit)) {
+        if ("B".equals(unit)) {
             return value;
         }
 
-        if (List.of("K", "KB").contains(unit)) {
+        if ("K".equals(unit) || "KB".equals(unit)) {
             return DataUnit.KILOBYTES.toBytes(value);
         }
 
-        if (List.of("M", "MB").contains(unit)) {
+        if ("M".equals(unit) || "MB".equals(unit)) {
             return DataUnit.MEGABYTES.toBytes(value);
         }
 
-        if (List.of("G", "GB").contains(unit)) {
+        if ("G".equals(unit) || "GB".equals(unit)) {
             return DataUnit.GIGABYTES.toBytes(value);
         }
 
-        if (List.of("KI", "KIB").contains(unit)) {
+        if ("KI".equals(unit) || "KIB".equals(unit)) {
             return DataUnit.KIBIBYTES.toBytes(value);
         }
 
-        if (List.of("MI", "MIB").contains(unit)) {
+        if ("MI".equals(unit) || "MIB".equals(unit)) {
             return DataUnit.MEBIBYTES.toBytes(value);
         }
 
-        if (List.of("GI", "GIB").contains(unit)) {
+        if ("GI".equals(unit) || "GIB".equals(unit)) {
             return DataUnit.GIBIBYTES.toBytes(value);
         }
 
@@ -1370,7 +1369,7 @@
                 sign = -1;
             }
 
-            fmtSize = fmtSize.replace(first + "", "");
+            fmtSize = fmtSize.substring(1);
         }
 
         int index = 0;
diff --git a/core/java/android/os/IHintSession.aidl b/core/java/android/os/IHintSession.aidl
index 09bc4cc..0d1dde1 100644
--- a/core/java/android/os/IHintSession.aidl
+++ b/core/java/android/os/IHintSession.aidl
@@ -22,4 +22,5 @@
     void updateTargetWorkDuration(long targetDurationNanos);
     void reportActualWorkDuration(in long[] actualDurationNanos, in long[] timeStampNanos);
     void close();
+    void sendHint(int hint);
 }
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 933769a..a887f2a 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -69,6 +69,7 @@
     boolean canAddMoreManagedProfiles(int userId, boolean allowedToRemoveOne);
     UserInfo getProfileParent(int userId);
     boolean isSameProfileGroup(int userId, int otherUserHandle);
+    boolean isHeadlessSystemUserMode();
     boolean isUserOfType(int userId, in String userType);
     @UnsupportedAppUsage
     UserInfo getUserInfo(int userId);
@@ -129,7 +130,7 @@
     boolean isUserRunning(int userId);
     boolean isUserForeground(int userId);
     boolean isUserVisible(int userId);
-    List<UserHandle> getVisibleUsers();
+    int[] getVisibleUsers();
     boolean isUserNameSet(int userId);
     boolean hasRestrictedProfiles(int userId);
     boolean requestQuietModeEnabled(String callingPackage, boolean enableQuietMode, int userId, in IntentSender target, int flags);
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index d84037f..1924dc6 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -70,3 +70,6 @@
 
 # Tracing
 per-file Trace.java = file:/TRACE_OWNERS
+
+# PermissionEnforcer
+per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d451765..1673ade 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);
@@ -559,9 +561,11 @@
      */
     public final void recycle() {
         if (mRecycled) {
-            Log.w(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: "
+            Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: "
                     + Log.getStackTraceString(new Throwable())
                     + " Original recycle call (if DEBUG_RECYCLE): ", mStack);
+
+            return;
         }
         mRecycled = true;
 
@@ -644,6 +648,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/Parcelable.java b/core/java/android/os/Parcelable.java
index 8a80457..a2b0486 100644
--- a/core/java/android/os/Parcelable.java
+++ b/core/java/android/os/Parcelable.java
@@ -188,7 +188,7 @@
      * @return true if this parcelable is stable.
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
     default @Stability int getStability() {
         return PARCELABLE_STABILITY_LOCAL;
     }
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index a75b5ef..86135bc 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
@@ -24,6 +25,10 @@
 import com.android.internal.util.Preconditions;
 
 import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Reference;
+
 
 /** The PerformanceHintManager allows apps to send performance hint to system. */
 @SystemService(Context.PERFORMANCE_HINT_SERVICE)
@@ -104,6 +109,40 @@
             mNativeSessionPtr = nativeSessionPtr;
         }
 
+        /**
+        * This hint indicates a sudden increase in CPU workload intensity. It means
+        * that this hint session needs extra CPU resources immediately to meet the
+        * target duration for the current work cycle.
+        */
+        public static final int CPU_LOAD_UP = 0;
+        /**
+        * This hint indicates a decrease in CPU workload intensity. It means that
+        * this hint session can reduce CPU resources and still meet the target duration.
+        */
+        public static final int CPU_LOAD_DOWN = 1;
+        /*
+        * This hint indicates an upcoming CPU workload that is completely changed and
+        * unknown. It means that the hint session should reset CPU resources to a known
+        * baseline to prepare for an arbitrary load, and must wake up if inactive.
+        */
+        public static final int CPU_LOAD_RESET = 2;
+        /*
+        * This hint indicates that the most recent CPU workload is resuming after a
+        * period of inactivity. It means that the hint session should allocate similar
+        * CPU resources to what was used previously, and must wake up if inactive.
+        */
+        public static final int CPU_LOAD_RESUME = 3;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(prefix = {"CPU_LOAD_"}, value = {
+            CPU_LOAD_UP,
+            CPU_LOAD_DOWN,
+            CPU_LOAD_RESET,
+            CPU_LOAD_RESUME
+        })
+        public @interface Hint {}
+
         /** @hide */
         @Override
         protected void finalize() throws Throwable {
@@ -152,6 +191,21 @@
                 mNativeSessionPtr = 0;
             }
         }
+
+        /**
+         * Sends performance hints to inform the hint session of changes in the workload.
+         *
+         * @param hint The hint to send to the session.
+         */
+        public void sendHint(@Hint int hint) {
+            Preconditions.checkArgumentNonNegative(hint, "the hint ID should be at least"
+                    + " zero.");
+            try {
+                nativeSendHint(mNativeSessionPtr, hint);
+            } finally {
+                Reference.reachabilityFence(this);
+            }
+        }
     }
 
     private static native long nativeAcquireManager();
@@ -163,4 +217,5 @@
     private static native void nativeReportActualWorkDuration(long nativeSessionPtr,
             long actualDurationNanos);
     private static native void nativeCloseSession(long nativeSessionPtr);
+    private static native void nativeSendHint(long nativeSessionPtr, int hint);
 }
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/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index acfd15c..02704f5 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -22,12 +22,12 @@
 import android.annotation.Nullable;
 import android.util.ArrayMap;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index 522807b..5dffa0a 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -22,10 +22,11 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index f62cc87..8afd6de 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -341,4 +341,10 @@
      * device is not awake.
      */
     public abstract void nap(long eventTime, boolean allowWake);
+
+    /**
+     * Returns true if ambient display is suppressed by any app with any token. This method will
+     * return false if ambient display is not available.
+     */
+    public abstract boolean isAmbientDisplaySuppressed();
 }
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index e483328..ac1583a 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -895,9 +895,21 @@
         return isIsolated(myUid());
     }
 
-    /** {@hide} */
-    @UnsupportedAppUsage
+    /**
+     * @deprecated Use {@link #isIsolatedUid(int)} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@link #isIsolatedUid(int)} instead.")
     public static final boolean isIsolated(int uid) {
+        return isIsolatedUid(uid);
+    }
+
+    /**
+     * Returns whether the process with the given {@code uid} is an isolated sandbox.
+     */
+    public static final boolean isIsolatedUid(int uid) {
         uid = UserHandle.getAppId(uid);
         return (uid >= FIRST_ISOLATED_UID && uid <= LAST_ISOLATED_UID)
                 || (uid >= FIRST_APP_ZYGOTE_ISOLATED_UID && uid <= LAST_APP_ZYGOTE_ISOLATED_UID);
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index e321a66..b6ff102 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -258,12 +258,14 @@
      * waitForService should always be able to return the service.
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @NonNull
     public static String[] getDeclaredInstances(@NonNull String iface) {
         try {
             return getIServiceManager().getDeclaredInstances(iface);
         } catch (RemoteException e) {
             Log.e(TAG, "error in getDeclaredInstances", e);
-            return null;
+            throw e.rethrowFromSystemServer();
         }
     }
 
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index 2dcf674..f2143f6 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -98,6 +98,10 @@
         return mServiceManager.updatableViaApex(name);
     }
 
+    public String[] getUpdatableNames(String apexName) throws RemoteException {
+        return mServiceManager.getUpdatableNames(apexName);
+    }
+
     public ConnectionInfo getConnectionInfo(String name) throws RemoteException {
         return mServiceManager.getConnectionInfo(name);
     }
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 6091bf9..bf72b1d 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.hardware.vibrator.IVibrator;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Range;
@@ -313,8 +314,14 @@
         private static final float EPSILON = 1e-5f;
 
         public MultiVibratorInfo(VibratorInfo[] vibrators) {
+            // Need to use an extra constructor to share the computation in super initialization.
+            this(vibrators, frequencyProfileIntersection(vibrators));
+        }
+
+        private MultiVibratorInfo(VibratorInfo[] vibrators,
+                VibratorInfo.FrequencyProfile mergedProfile) {
             super(/* id= */ -1,
-                    capabilitiesIntersection(vibrators),
+                    capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
                     supportedEffectsIntersection(vibrators),
                     supportedBrakingIntersection(vibrators),
                     supportedPrimitivesAndDurationsIntersection(vibrators),
@@ -323,14 +330,19 @@
                     integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
                     integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
                     floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
-                    frequencyProfileIntersection(vibrators));
+                    mergedProfile);
         }
 
-        private static int capabilitiesIntersection(VibratorInfo[] infos) {
+        private static int capabilitiesIntersection(VibratorInfo[] infos,
+                boolean frequencyProfileIsEmpty) {
             int intersection = ~0;
             for (VibratorInfo info : infos) {
                 intersection &= info.getCapabilities();
             }
+            if (frequencyProfileIsEmpty) {
+                // Revoke frequency control if the merged frequency profile ended up empty.
+                intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+            }
             return intersection;
         }
 
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index 8bfa0e9..fb197f5 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -100,6 +100,7 @@
     /** @hide */
     public static final long TRACE_TAG_VIBRATOR = 1L << 23;
     /** @hide */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final long TRACE_TAG_AIDL = 1L << 24;
     /** @hide */
     public static final long TRACE_TAG_NNAPI = 1L << 25;
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index d2d6bec..4a6772d 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -20,8 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java
index e1ec5cd..6b4a5cf 100644
--- a/core/java/android/os/UserBatteryConsumer.java
+++ b/core/java/android/os/UserBatteryConsumer.java
@@ -17,8 +17,9 @@
 package android.os;
 
 import android.annotation.NonNull;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 51dc643..1f21bfe 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -91,7 +91,6 @@
 public class UserManager {
 
     private static final String TAG = "UserManager";
-    private static final boolean VERBOSE = false;
 
     @UnsupportedAppUsage
     private final IUserManager mService;
@@ -104,6 +103,9 @@
     /** The userType of UserHandle.myUserId(); empty string if not a profile; null until cached. */
     private String mProfileTypeOfProcessUser = null;
 
+    /** Whether the device is in headless system user mode; null until cached. */
+    private static Boolean sIsHeadlessSystemUser = null;
+
     /**
      * User type representing a {@link UserHandle#USER_SYSTEM system} user that is a human user.
      * This type of user cannot be created; it can only pre-exist on first boot.
@@ -1488,6 +1490,46 @@
     public static final String KEY_RESTRICTIONS_PENDING = "restrictions_pending";
 
     /**
+     * Specifies if a user is not allowed to use 2g networks.
+     *
+     * <p>This restriction can only be set by a device owner or a profile owner of an
+     * organization-owned managed profile on the parent profile.
+     * In all cases, the setting applies globally on the device and will prevent the device from
+     * scanning for or connecting to 2g networks, except in the case of an emergency.
+     *
+     * <p>The default value is <code>false</code>.
+     *
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_CELLULAR_2G = "no_cellular_2g";
+
+    /**
+     * This user restriction specifies if Ultra-wideband is disallowed on the device. If
+     * Ultra-wideband is disallowed it cannot be turned on via Settings.
+     *
+     * <p>This restriction can only be set by a device owner or a profile owner of an
+     * organization-owned managed profile on the parent profile.
+     * In both cases, the restriction applies globally on the device and will turn off the
+     * ultra-wideband radio if it's currently on and prevent the radio from being turned on in
+     * the future.
+     *
+     * <p>
+     * Ultra-wideband (UWB) is a radio technology that can use a very low energy level
+     * for short-range, high-bandwidth communications over a large portion of the radio spectrum.
+     *
+     * <p>Default is <code>false</code>.
+     *
+     * <p>Key for user restrictions.
+     * <p>Type: Boolean
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_ULTRA_WIDEBAND_RADIO = "no_ultra_wideband_radio";
+
+    /**
      * List of key values that can be passed into the various user restriction related methods
      * in {@link UserManager} & {@link DevicePolicyManager}.
      * Note: This is slightly different from the real set of user restrictions listed in {@link
@@ -1568,6 +1610,8 @@
             DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI,
             DISALLOW_WIFI_DIRECT,
             DISALLOW_ADD_WIFI_CONFIG,
+            DISALLOW_CELLULAR_2G,
+            DISALLOW_ULTRA_WIDEBAND_RADIO,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface UserRestrictionKey {}
@@ -1593,6 +1637,16 @@
     /** @hide */
     public static final String SYSTEM_USER_MODE_EMULATION_HEADLESS = "headless";
 
+    /**
+     * System Property used to override whether users can be created even if their type is disabled
+     * or their limit is reached. Set value to 1 to enable.
+     *
+     * <p>Only used on non-user builds.
+     *
+     * @hide
+     */
+    public static final String DEV_CREATE_OVERRIDE_PROPERTY = "debug.user.creation_override";
+
     private static final String ACTION_CREATE_USER = "android.os.action.CREATE_USER";
 
     /**
@@ -2051,28 +2105,20 @@
      * @return whether the device is running in a headless system user mode.
      */
     public static boolean isHeadlessSystemUserMode() {
-        final boolean realMode = RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER;
-        if (!Build.isDebuggable()) {
-            return realMode;
+        // No need for synchronization.  Once it becomes non-null, it'll be non-null forever.
+        // (Its value is determined when UMS is constructed and cannot change.)
+        // Worst case we might end up calling the AIDL method multiple times but that's fine.
+        if (sIsHeadlessSystemUser == null) {
+            // Unfortunately this API is static, but the property no longer is. So go fetch the UMS.
+            try {
+                final IUserManager service = IUserManager.Stub.asInterface(
+                        ServiceManager.getService(Context.USER_SERVICE));
+                sIsHeadlessSystemUser = service.isHeadlessSystemUserMode();
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
         }
-
-        final String emulatedMode = SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY);
-        switch (emulatedMode) {
-            case SYSTEM_USER_MODE_EMULATION_FULL:
-                if (VERBOSE) Log.v(TAG, "isHeadlessSystemUserMode(): emulating as false");
-                return false;
-            case SYSTEM_USER_MODE_EMULATION_HEADLESS:
-                if (VERBOSE) Log.v(TAG, "isHeadlessSystemUserMode(): emulating as true");
-                return true;
-            case SYSTEM_USER_MODE_EMULATION_DEFAULT:
-            case "": // property not set
-                return realMode;
-            default:
-                Log.wtf(TAG, "isHeadlessSystemUserMode(): invalid value of property "
-                        + SYSTEM_USER_MODE_EMULATION_PROPERTY + " (" + emulatedMode + "); using"
-                                + " default value (headless=" + realMode + ")");
-                return realMode;
-        }
+        return sIsHeadlessSystemUser;
     }
 
     /**
@@ -2295,12 +2341,18 @@
     }
 
     /**
-     * Used to check if the context user is the primary user. The primary user
-     * is the first human user on a device. This is not supported in headless system user mode.
+     * Used to check if the context user is the primary user. The primary user is the first human
+     * user on a device. This is not supported in headless system user mode.
      *
      * @return whether the context user is the primary user.
+     *
+     * @deprecated This method always returns true for the system user, who may not be a full user
+     * if {@link #isHeadlessSystemUserMode} is true. Use {@link #isSystemUser}, {@link #isAdminUser}
+     * or {@link #isMainUser} instead.
+     *
      * @hide
      */
+    @Deprecated
     @SystemApi
     @RequiresPermission(anyOf = {
             Manifest.permission.MANAGE_USERS,
@@ -2325,6 +2377,29 @@
     }
 
     /**
+     * Returns true if the context user is the designated "main user" of the device. This user may
+     * have access to certain features which are limited to at most one user.
+     *
+     * <p>Currently, the first human user on the device will be the main user; in the future, the
+     * concept may be transferable, so a different user (or even no user at all) may be designated
+     * the main user instead.
+     *
+     * <p>Note that this will be the not be the system user on devices for which
+     * {@link #isHeadlessSystemUserMode()} returns true.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            Manifest.permission.MANAGE_USERS,
+            Manifest.permission.CREATE_USERS,
+            Manifest.permission.QUERY_USERS})
+    @UserHandleAware
+    public boolean isMainUser() {
+        final UserInfo user = getUserInfo(mUserId);
+        return user != null && user.isMain();
+    }
+
+    /**
      * Used to check if the context user is an admin user. An admin user is allowed to
      * modify or configure certain settings that aren't available to non-admin users,
      * create and delete additional users, etc. There can be more than one admin users.
@@ -2893,12 +2968,19 @@
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS})
-    public @NonNull List<UserHandle> getVisibleUsers() {
+    public @NonNull Set<UserHandle> getVisibleUsers() {
+        ArraySet<UserHandle> result = new ArraySet<>();
         try {
-            return mService.getVisibleUsers();
+            int[] visibleUserIds = mService.getVisibleUsers();
+            if (visibleUserIds != null) {
+                for (int userId : visibleUserIds) {
+                    result.add(UserHandle.of(userId));
+                }
+            }
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
+        return result;
     }
 
     /**
@@ -4339,6 +4421,7 @@
      * @return true if the creation of users of the given user type is enabled on this device.
      * @hide
      */
+    @TestApi
     @RequiresPermission(anyOf = {
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.CREATE_USERS
diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS
index 1f686e5..c80c57c 100644
--- a/core/java/android/os/storage/OWNERS
+++ b/core/java/android/os/storage/OWNERS
@@ -1,11 +1,15 @@
 # Bug component: 95221
 
-corinac@google.com
-nandana@google.com
-zezeozue@google.com
-maco@google.com
-sahanas@google.com
+# Android Storage Team
 abkaur@google.com
-chiangi@google.com
-narayan@google.com
+corinac@google.com
 dipankarb@google.com
+krishang@google.com
+sahanas@google.com
+sergeynv@google.com
+shubhisaxena@google.com
+tylersaunders@google.com
+
+maco@google.com
+nandana@google.com
+narayan@google.com
diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl
index 8534c66..16ae3bc 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -77,8 +77,7 @@
     List<SplitPermissionInfoParcelable> getSplitPermissions();
 
     void startOneTimePermissionSession(String packageName, int userId, long timeout,
-            long revokeAfterKilledDelay, int importanceToResetTimer,
-            int importanceToKeepSessionAlive);
+            long revokeAfterKilledDelay);
 
     @EnforcePermission("MANAGE_ONE_TIME_PERMISSION_SESSIONS")
     void stopOneTimePermissionSession(String packageName, int userId);
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 6b540d7..6769954 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1371,8 +1371,7 @@
             @ActivityManager.RunningAppProcessInfo.Importance int importanceToKeepSessionAlive) {
         try {
             mPermissionManager.startOneTimePermissionSession(packageName, mContext.getUserId(),
-                    timeoutMillis, revokeAfterKilledDelayMillis, importanceToResetTimer,
-                    importanceToKeepSessionAlive);
+                    timeoutMillis, revokeAfterKilledDelayMillis);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 3bf9ca0..b117a9a 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -16,7 +16,9 @@
 
 package android.preference;
 
+import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.app.NotificationManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
@@ -35,6 +37,7 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.preference.VolumePreference.VolumeStore;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.System;
@@ -44,6 +47,7 @@
 import android.widget.SeekBar.OnSeekBarChangeListener;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.os.SomeArgs;
 
 import java.util.concurrent.TimeUnit;
@@ -115,7 +119,6 @@
     private final int mMaxStreamVolume;
     private boolean mAffectedByRingerMode;
     private boolean mNotificationOrRing;
-    private final boolean mNotifAliasRing;
     private final Receiver mReceiver = new Receiver();
 
     private Handler mHandler;
@@ -158,6 +161,7 @@
         this(context, streamType, defaultUri, callback, true /* playSample */);
     }
 
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public SeekBarVolumizer(
             Context context,
             int streamType,
@@ -180,8 +184,6 @@
         if (mNotificationOrRing) {
             mRingerMode = mAudioManager.getRingerModeInternal();
         }
-        mNotifAliasRing = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_alias_ring_notif_stream_types);
         mZenMode = mNotificationManager.getZenMode();
 
         if (hasAudioProductStrategies()) {
@@ -288,7 +290,9 @@
              * so that when user attempts to slide the notification seekbar out of vibrate the
              * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
              */
-            if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING
+            if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                    || mStreamType == AudioManager.STREAM_RING
                     || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
                 mSeekBar.setProgress(0, true);
             }
@@ -365,7 +369,9 @@
         // set the time of stop volume
         if ((mStreamType == AudioManager.STREAM_VOICE_CALL
                 || mStreamType == AudioManager.STREAM_RING
-                || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION)
+                || (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                && mStreamType == AudioManager.STREAM_NOTIFICATION)
                 || mStreamType == AudioManager.STREAM_ALARM)) {
             sStopVolumeTime = java.lang.System.currentTimeMillis();
         }
@@ -644,8 +650,10 @@
         }
 
         private void updateVolumeSlider(int streamType, int streamValue) {
-            final boolean streamMatch = mNotifAliasRing && mNotificationOrRing
-                    ? isNotificationOrRing(streamType) : streamType == mStreamType;
+            final boolean streamMatch =  !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
+                    && mNotificationOrRing ? isNotificationOrRing(streamType) :
+                    streamType == mStreamType;
             if (mSeekBar != null && streamMatch && streamValue != -1) {
                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
                         || streamValue == 0;
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 7095d1b..8a09cd7 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -25,9 +25,6 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
-import android.app.ActivityThread;
-import android.content.ContentResolver;
-import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.net.Uri;
@@ -131,6 +128,13 @@
     public static final String NAMESPACE_APP_STANDBY = "app_standby";
 
     /**
+     * Namespace for all App Cloning related features.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final String NAMESPACE_APP_CLONING = "app_cloning";
+
+    /**
      * Namespace for AttentionManagerService related features.
      *
      * @hide
@@ -875,9 +879,8 @@
     @NonNull
     @RequiresPermission(READ_DEVICE_CONFIG)
     public static Properties getProperties(@NonNull String namespace, @NonNull String ... names) {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
         return new Properties(namespace,
-                Settings.Config.getStrings(contentResolver, namespace, Arrays.asList(names)));
+                Settings.Config.getStrings(namespace, Arrays.asList(names)));
     }
 
     /**
@@ -1016,8 +1019,7 @@
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static boolean setProperty(@NonNull String namespace, @NonNull String name,
             @Nullable String value, boolean makeDefault) {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        return Settings.Config.putString(contentResolver, namespace, name, value, makeDefault);
+        return Settings.Config.putString(namespace, name, value, makeDefault);
     }
 
     /**
@@ -1038,8 +1040,7 @@
     @SystemApi
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static boolean setProperties(@NonNull Properties properties) throws BadConfigException {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        return Settings.Config.setStrings(contentResolver, properties.getNamespace(),
+        return Settings.Config.setStrings(properties.getNamespace(),
                 properties.mMap);
     }
 
@@ -1055,8 +1056,7 @@
     @SystemApi
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        return Settings.Config.deleteString(contentResolver, namespace, name);
+        return Settings.Config.deleteString(namespace, name);
     }
 
     /**
@@ -1087,8 +1087,7 @@
     @SystemApi
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static void resetToDefaults(@ResetMode int resetMode, @Nullable String namespace) {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        Settings.Config.resetToDefaults(contentResolver, resetMode, namespace);
+        Settings.Config.resetToDefaults(resetMode, namespace);
     }
 
     /**
@@ -1105,8 +1104,7 @@
      */
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static void setSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        Settings.Config.setSyncDisabledMode(contentResolver, syncDisabledMode);
+        Settings.Config.setSyncDisabledMode(syncDisabledMode);
     }
 
     /**
@@ -1117,8 +1115,7 @@
      */
     @RequiresPermission(WRITE_DEVICE_CONFIG)
     public static @SyncDisabledMode int getSyncDisabledMode() {
-        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        return Settings.Config.getSyncDisabledMode(contentResolver);
+        return Settings.Config.getSyncDisabledMode();
     }
 
     /**
@@ -1141,8 +1138,7 @@
             @NonNull String namespace,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull OnPropertiesChangedListener onPropertiesChangedListener) {
-        enforceReadPermission(ActivityThread.currentApplication().getApplicationContext(),
-                namespace);
+        enforceReadPermission(namespace);
         synchronized (sLock) {
             Pair<String, Executor> oldNamespace = sListeners.get(onPropertiesChangedListener);
             if (oldNamespace == null) {
@@ -1209,7 +1205,7 @@
                     }
                 }
             };
-            ActivityThread.currentApplication().getContentResolver()
+            Settings.Config
                     .registerContentObserver(createNamespaceUri(namespace), true, contentObserver);
             sNamespaces.put(namespace, new Pair<>(contentObserver, 1));
         }
@@ -1233,8 +1229,7 @@
             sNamespaces.put(namespace, new Pair<>(namespaceCount.first, namespaceCount.second - 1));
         } else {
             // Decrementing a namespace to zero means we no longer need its ContentObserver.
-            ActivityThread.currentApplication().getContentResolver()
-                    .unregisterContentObserver(namespaceCount.first);
+            Settings.Config.unregisterContentObserver(namespaceCount.first);
             sNamespaces.remove(namespace);
         }
     }
@@ -1274,8 +1269,8 @@
      * Enforces READ_DEVICE_CONFIG permission if namespace is not one of public namespaces.
      * @hide
      */
-    public static void enforceReadPermission(Context context, String namespace) {
-        if (context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
+    public static void enforceReadPermission(String namespace) {
+        if (Settings.Config.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
                 != PackageManager.PERMISSION_GRANTED) {
             if (!PUBLIC_NAMESPACES.contains(namespace)) {
                 throw new SecurityException("Permission denial: reading from settings requires:"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4e15b38..ef448f5 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -47,9 +47,11 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PermissionName;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.SQLException;
 import android.location.ILocationManager;
@@ -192,6 +194,21 @@
             "android.settings.LOCATION_SCANNING_SETTINGS";
 
     /**
+     * Activity Action: Show settings to manage creation/deletion of cloned apps.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_MANAGE_CLONED_APPS_SETTINGS =
+            "android.settings.MANAGE_CLONED_APPS_SETTINGS";
+
+    /**
      * Activity Action: Show settings to allow configuration of users.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you
@@ -675,6 +692,22 @@
             "android.settings.WIFI_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow configuration of MTE.
+     * <p>
+     * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain
+     * classes of security problems at a small runtime performance cost overhead.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_MEMTAG_SETTINGS =
+            "android.settings.MEMTAG_SETTINGS";
+
+    /**
      * Activity Action: Show settings to allow configuration of a static IP
      * address for Wi-Fi.
      * <p>
@@ -3313,7 +3346,7 @@
         public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
                 List<String> names) {
             String namespace = prefix.substring(0, prefix.length() - 1);
-            DeviceConfig.enforceReadPermission(ActivityThread.currentApplication(), namespace);
+            DeviceConfig.enforceReadPermission(namespace);
             ArrayMap<String, String> keyValues = new ArrayMap<>();
             int currentGeneration = -1;
 
@@ -3374,9 +3407,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;
@@ -6875,6 +6925,14 @@
         @Readable
         public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
 
+
+        /**
+         * The currently selected credential service(s) flattened ComponentName.
+         *
+         * @hide
+         */
+        public static final String CREDENTIAL_SERVICE = "credential_service";
+
         /**
          * The currently selected autofill service flattened ComponentName.
          * @hide
@@ -7127,7 +7185,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";
 
         /**
@@ -7136,7 +7194,7 @@
          * by ':'.
          * @hide
          */
-        @Readable
+        @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
         public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods";
 
         /**
@@ -9851,6 +9909,13 @@
                 "fingerprint_side_fps_auth_downtime";
 
         /**
+         * Whether or not a SFPS device is required to be interactive for auth to unlock the device.
+         * @hide
+         */
+        public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED =
+                "sfps_require_screen_on_to_auth_enabled";
+
+        /**
          * Whether or not debugging is enabled.
          * @hide
          */
@@ -10368,11 +10433,11 @@
         public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
 
         /**
-         * The duration of timeout, in milliseconds, to switch from a non-primary user to the
-         * primary user when the device is docked.
+         * The duration of timeout, in milliseconds, to switch from a non-Dock User to the
+         * Dock User when the device is docked.
          * @hide
          */
-        public static final String TIMEOUT_TO_USER_ZERO = "timeout_to_user_zero";
+        public static final String TIMEOUT_TO_DOCK_USER = "timeout_to_dock_user";
 
         /**
          * Backup manager behavioral parameters.
@@ -16036,6 +16101,18 @@
                 "key_chord_power_volume_up";
 
         /**
+         * Record audio from near-field microphone (ie. TV remote)
+         * Allows audio recording regardless of sensor privacy state,
+         * as it is an intentional user interaction: hold-to-talk
+         *
+         * Type: int (0 to disable, 1 to enable)
+         *
+         * @hide
+         */
+        public static final String RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED =
+                "receive_explicit_user_interaction_audio_enabled";
+
+        /**
          * Keyguard should be on the left hand side of the screen, for wide screen layouts.
          *
          * @hide
@@ -17927,20 +18004,36 @@
 
         /**
          * Look up a name in the database.
-         * @param resolver to access the database with
          * @param name to look up in the table
          * @return the corresponding value, or null if not present
          *
          * @hide
          */
         @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-        static String getString(ContentResolver resolver, String name) {
+        static String getString(String name) {
+            ContentResolver resolver = getContentResolver();
             return sNameValueCache.getStringForUser(resolver, name, resolver.getUserId());
         }
 
         /**
          * Look up a list of names in the database, within the specified namespace.
          *
+         * @param namespace to which the names belong
+         * @param names to look up in the table
+         * @return a non null, but possibly empty, map from name to value for any of the names that
+         *         were found during lookup.
+         *
+         * @hide
+         */
+        @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
+        public static Map<String, String> getStrings(@NonNull String namespace,
+                @NonNull List<String> names) {
+            return getStrings(getContentResolver(), namespace, names);
+        }
+
+        /**
+         * Look up a list of names in the database, within the specified namespace.
+         *
          * @param resolver to access the database with
          * @param namespace to which the names belong
          * @param names to look up in the table
@@ -17978,7 +18071,6 @@
          * <strong>not</strong> be set as the default.
          * </p>
          *
-         * @param resolver to access the database with.
          * @param namespace to store the name/value pair in.
          * @param name to store.
          * @param value to associate with the name.
@@ -17990,8 +18082,9 @@
          * @hide
          */
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static boolean putString(@NonNull ContentResolver resolver, @NonNull String namespace,
+        public static boolean putString(@NonNull String namespace,
                 @NonNull String name, @Nullable String value, boolean makeDefault) {
+            ContentResolver resolver = getContentResolver();
             return sNameValueCache.putStringForUser(resolver, createCompositeName(namespace, name),
                     value, null, makeDefault, resolver.getUserId(),
                     DEFAULT_OVERRIDEABLE_BY_RESTORE);
@@ -18001,6 +18094,23 @@
          * Clear all name/value pairs for the provided namespace and save new name/value pairs in
          * their place.
          *
+         * @param namespace to which the names should be set.
+         * @param keyValues map of key names (without the prefix) to values.
+         * @return true if the name/value pairs were set, false if setting was blocked
+         *
+         * @hide
+         */
+        @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
+        public static boolean setStrings(@NonNull String namespace,
+                @NonNull Map<String, String> keyValues)
+                throws DeviceConfig.BadConfigException {
+            return setStrings(getContentResolver(), namespace, keyValues);
+        }
+
+        /**
+         * Clear all name/value pairs for the provided namespace and save new name/value pairs in
+         * their place.
+         *
          * @param resolver to access the database with.
          * @param namespace to which the names should be set.
          * @param keyValues map of key names (without the prefix) to values.
@@ -18031,7 +18141,6 @@
         /**
          * Delete a name/value pair from the database for the specified namespace.
          *
-         * @param resolver to access the database with.
          * @param namespace to delete the name/value pair from.
          * @param name to delete.
          * @return true if the value was deleted, false on database errors. If the name/value pair
@@ -18042,8 +18151,9 @@
          * @hide
          */
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static boolean deleteString(@NonNull ContentResolver resolver, @NonNull String namespace,
+        static boolean deleteString(@NonNull String namespace,
                 @NonNull String name) {
+            ContentResolver resolver = getContentResolver();
             return sNameValueCache.deleteStringForUser(resolver,
                     createCompositeName(namespace, name), resolver.getUserId());
         }
@@ -18054,7 +18164,6 @@
          * The method accepts an optional prefix parameter. If provided, only pairs with a name that
          * starts with the exact prefix will be reset. Otherwise all will be reset.
          *
-         * @param resolver Handle to the content resolver.
          * @param resetMode The reset mode to use.
          * @param namespace Optionally, to limit which which namespace is reset.
          *
@@ -18063,9 +18172,10 @@
          * @hide
          */
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static void resetToDefaults(@NonNull ContentResolver resolver, @ResetMode int resetMode,
+        static void resetToDefaults(@ResetMode int resetMode,
                 @Nullable String namespace) {
             try {
+                ContentResolver resolver = getContentResolver();
                 Bundle arg = new Bundle();
                 arg.putInt(CALL_METHOD_USER_KEY, resolver.getUserId());
                 arg.putInt(CALL_METHOD_RESET_MODE_KEY, resetMode);
@@ -18088,9 +18198,9 @@
          */
         @SuppressLint("AndroidFrameworkRequiresPermission")
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static void setSyncDisabledMode(
-                @NonNull ContentResolver resolver, @SyncDisabledMode int disableSyncMode) {
+        static void setSyncDisabledMode(@SyncDisabledMode int disableSyncMode) {
             try {
+                ContentResolver resolver = getContentResolver();
                 Bundle args = new Bundle();
                 args.putInt(CALL_METHOD_SYNC_DISABLED_MODE_KEY, disableSyncMode);
                 IContentProvider cp = sProviderHolder.getProvider(resolver);
@@ -18109,8 +18219,9 @@
          */
         @SuppressLint("AndroidFrameworkRequiresPermission")
         @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG)
-        static int getSyncDisabledMode(@NonNull ContentResolver resolver) {
+        static int getSyncDisabledMode() {
             try {
+                ContentResolver resolver = getContentResolver();
                 Bundle args = Bundle.EMPTY;
                 IContentProvider cp = sProviderHolder.getProvider(resolver);
                 Bundle bundle = cp.call(resolver.getAttributionSource(),
@@ -18127,7 +18238,6 @@
         /**
          * Register callback for monitoring Config table.
          *
-         * @param resolver Handle to the content resolver.
          * @param callback callback to register
          *
          * @hide
@@ -18138,6 +18248,50 @@
             registerMonitorCallbackAsUser(resolver, resolver.getUserId(), callback);
         }
 
+
+        /**
+         * Register a content observer
+         *
+         * @hide
+         */
+        public static void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,
+                @NonNull ContentObserver observer) {
+            ActivityThread.currentApplication().getContentResolver()
+               .registerContentObserver(uri, notifyForDescendants, observer);
+        }
+
+        /**
+         * Unregister a content observer
+         *
+         * @hide
+         */
+        public static void unregisterContentObserver(@NonNull ContentObserver observer) {
+            ActivityThread.currentApplication().getContentResolver()
+              .unregisterContentObserver(observer);
+        }
+
+        /**
+         * Determine whether the calling process of an IPC <em>or you</em> have been
+         * granted a particular permission.  This is the same as
+         * {@link #checkCallingPermission}, except it grants your own permissions
+         * if you are not currently processing an IPC.  Use with care!
+         *
+         * @param permission The name of the permission being checked.
+         *
+         * @return {@link PackageManager#PERMISSION_GRANTED} if the calling
+         * pid/uid is allowed that permission, or
+         * {@link PackageManager#PERMISSION_DENIED} if it is not.
+         *
+         * @see PackageManager#checkPermission(String, String)
+         * @see #checkPermission
+         * @see #checkCallingPermission
+         * @hide
+         */
+        public static int checkCallingOrSelfPermission(@NonNull @PermissionName String permission) {
+            return ActivityThread.currentApplication()
+               .getApplicationContext().checkCallingOrSelfPermission(permission);
+        }
+
         private static void registerMonitorCallbackAsUser(
                 @NonNull ContentResolver resolver, @UserIdInt int userHandle,
                 @NonNull RemoteCallback callback) {
@@ -18170,6 +18324,10 @@
             Preconditions.checkNotNull(namespace);
             return namespace + "/";
         }
+
+        private static ContentResolver getContentResolver() {
+            return ActivityThread.currentApplication().getContentResolver();
+        }
     }
 
     /**
@@ -18599,6 +18757,9 @@
     /**
      * Activity Action: For system or preinstalled apps to show their {@link Activity} embedded
      * in Settings app on large screen devices.
+     *
+     * Developers should resolve the Intent action before using it.
+     *
      * <p>
      *     Input: {@link #EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI} must be included to
      * specify the intent for the activity which will be embedded in Settings app.
diff --git a/core/java/android/security/keymaster/ExportResult.java b/core/java/android/security/keymaster/ExportResult.java
index 2c382ef..c78fb1a 100644
--- a/core/java/android/security/keymaster/ExportResult.java
+++ b/core/java/android/security/keymaster/ExportResult.java
@@ -61,4 +61,4 @@
         out.writeInt(resultCode);
         out.writeByteArray(exportData);
     }
-};
+}
diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java
index b4010a4..0f7c9b6 100644
--- a/core/java/android/service/autofill/FillRequest.java
+++ b/core/java/android/service/autofill/FillRequest.java
@@ -111,6 +111,12 @@
      */
     public static final @RequestFlags int FLAG_IME_SHOWING = 0x80;
 
+    /**
+     * Indicates whether autofill session should reset the fill dialog state.
+     * @hide
+     */
+    public static final @RequestFlags int FLAG_RESET_FILL_DIALOG_STATE = 0x100;
+
     /** @hide */
     public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
 
@@ -208,7 +214,8 @@
         FLAG_PASSWORD_INPUT_TYPE,
         FLAG_VIEW_NOT_FOCUSED,
         FLAG_SUPPORTS_FILL_DIALOG,
-        FLAG_IME_SHOWING
+        FLAG_IME_SHOWING,
+        FLAG_RESET_FILL_DIALOG_STATE
     })
     @Retention(RetentionPolicy.SOURCE)
     @DataClass.Generated.Member
@@ -236,6 +243,8 @@
                     return "FLAG_SUPPORTS_FILL_DIALOG";
             case FLAG_IME_SHOWING:
                     return "FLAG_IME_SHOWING";
+            case FLAG_RESET_FILL_DIALOG_STATE:
+                    return "FLAG_RESET_FILL_DIALOG_STATE";
             default: return Integer.toHexString(value);
         }
     }
@@ -312,7 +321,8 @@
                         | FLAG_PASSWORD_INPUT_TYPE
                         | FLAG_VIEW_NOT_FOCUSED
                         | FLAG_SUPPORTS_FILL_DIALOG
-                        | FLAG_IME_SHOWING);
+                        | FLAG_IME_SHOWING
+                        | FLAG_RESET_FILL_DIALOG_STATE);
         this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
         this.mDelayedFillIntentSender = delayedFillIntentSender;
 
@@ -473,7 +483,8 @@
                         | FLAG_PASSWORD_INPUT_TYPE
                         | FLAG_VIEW_NOT_FOCUSED
                         | FLAG_SUPPORTS_FILL_DIALOG
-                        | FLAG_IME_SHOWING);
+                        | FLAG_IME_SHOWING
+                        | FLAG_RESET_FILL_DIALOG_STATE);
         this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
         this.mDelayedFillIntentSender = delayedFillIntentSender;
 
@@ -495,10 +506,10 @@
     };
 
     @DataClass.Generated(
-            time = 1647856966565L,
+            time = 1663290803064L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java",
-            inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final  int INVALID_REQUEST_ID\nprivate final  int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate  void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
+            inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_RESET_FILL_DIALOG_STATE\npublic static final  int INVALID_REQUEST_ID\nprivate final  int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate  void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 47b16a3..d2a4ae2 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -55,6 +55,20 @@
             "android.service.controls.ControlsProviderService";
 
     /**
+     * Manifest metadata to show a custom embedded activity as part of device controls.
+     *
+     * The value of this metadata must be the {@link ComponentName} as a string of an activity in
+     * the same package that will be launched as part of a TaskView.
+     *
+     * The activity must be exported, enabled and protected by
+     * {@link Manifest.permission.BIND_CONTROLS}.
+     *
+     * @hide
+     */
+    public static final String META_DATA_PANEL_ACTIVITY =
+            "android.service.controls.META_DATA_PANEL_ACTIVITY";
+
+    /**
      * @hide
      */
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/core/java/android/service/credentials/Action.java b/core/java/android/service/credentials/Action.java
index e2c11fb..553a324 100644
--- a/core/java/android/service/credentials/Action.java
+++ b/core/java/android/service/credentials/Action.java
@@ -50,9 +50,8 @@
     }
 
     private Action(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
     }
 
     public static final @NonNull Creator<Action> CREATOR = new Creator<Action>() {
@@ -74,8 +73,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/CreateCredentialRequest.java b/core/java/android/service/credentials/CreateCredentialRequest.java
index 6a0bbc0..e6da349 100644
--- a/core/java/android/service/credentials/CreateCredentialRequest.java
+++ b/core/java/android/service/credentials/CreateCredentialRequest.java
@@ -54,7 +54,7 @@
     private CreateCredentialRequest(@NonNull Parcel in) {
         mCallingPackage = in.readString8();
         mType = in.readString8();
-        mData = in.readBundle();
+        mData = in.readTypedObject(Bundle.CREATOR);
     }
 
     public static final @NonNull Creator<CreateCredentialRequest> CREATOR =
@@ -79,7 +79,7 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mCallingPackage);
         dest.writeString8(mType);
-        dest.writeBundle(mData);
+        dest.writeTypedObject(mData, flags);
     }
 
     /** Returns the calling package of the calling app. */
diff --git a/core/java/android/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java
index 613eba8..f69dca8 100644
--- a/core/java/android/service/credentials/CreateCredentialResponse.java
+++ b/core/java/android/service/credentials/CreateCredentialResponse.java
@@ -33,18 +33,21 @@
  * @hide
  */
 public final class CreateCredentialResponse implements Parcelable {
-    private final @Nullable CharSequence mHeader;
     private final @NonNull List<SaveEntry> mSaveEntries;
+    private final @Nullable Action mRemoteSaveEntry;
+    //TODO : Add actions if needed
 
     private CreateCredentialResponse(@NonNull Parcel in) {
-        mHeader = in.readCharSequence();
-        mSaveEntries = in.createTypedArrayList(SaveEntry.CREATOR);
+        List<SaveEntry> saveEntries = new ArrayList<>();
+        in.readTypedList(saveEntries, SaveEntry.CREATOR);
+        mSaveEntries = saveEntries;
+        mRemoteSaveEntry = in.readTypedObject(Action.CREATOR);
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeCharSequence(mHeader);
         dest.writeTypedList(mSaveEntries);
+        dest.writeTypedObject(mRemoteSaveEntry, flags);
     }
 
     @Override
@@ -66,17 +69,12 @@
             };
 
     /* package-private */ CreateCredentialResponse(
-            @Nullable CharSequence header,
-            @NonNull List<SaveEntry> saveEntries) {
-        this.mHeader = header;
+            @NonNull List<SaveEntry> saveEntries,
+            @Nullable Action remoteSaveEntry) {
         this.mSaveEntries = saveEntries;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mSaveEntries);
-    }
-
-    /** Returns the header to be displayed on the UI. */
-    public @Nullable CharSequence getHeader() {
-        return mHeader;
+        this.mRemoteSaveEntry = remoteSaveEntry;
     }
 
     /** Returns the list of save entries to be displayed on the UI. */
@@ -84,20 +82,18 @@
         return mSaveEntries;
     }
 
+    /** Returns the remote save entry to be displayed on the UI. */
+    public @NonNull Action getRemoteSaveEntry() {
+        return mRemoteSaveEntry;
+    }
+
     /**
      * A builder for {@link CreateCredentialResponse}
      */
     @SuppressWarnings("WeakerAccess")
     public static final class Builder {
-
-        private @Nullable CharSequence mHeader;
         private @NonNull List<SaveEntry> mSaveEntries = new ArrayList<>();
-
-        /** Sets the header to be displayed on the UI. */
-        public @NonNull Builder setHeader(@Nullable CharSequence header) {
-            mHeader = header;
-            return this;
-        }
+        private @Nullable Action mRemoteSaveEntry;
 
         /**
          * Sets the list of save entries to be shown on the UI.
@@ -124,6 +120,14 @@
         }
 
         /**
+         * Sets a remote save entry to be shown on the UI.
+         */
+        public @NonNull Builder setRemoteSaveEntry(@Nullable Action remoteSaveEntry) {
+            mRemoteSaveEntry = remoteSaveEntry;
+            return this;
+        }
+
+        /**
          * Builds the instance.
          *
          * @throws IllegalArgumentException If {@code saveEntries} is empty.
@@ -132,8 +136,8 @@
             Preconditions.checkCollectionNotEmpty(mSaveEntries, "saveEntries must "
                     + "not be empty");
             return new CreateCredentialResponse(
-                    mHeader,
-                    mSaveEntries);
+                    mSaveEntries,
+                    mRemoteSaveEntry);
         }
     }
 }
diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java
index 49b8435..98c537a 100644
--- a/core/java/android/service/credentials/CredentialEntry.java
+++ b/core/java/android/service/credentials/CredentialEntry.java
@@ -65,12 +65,10 @@
     }
 
     private CredentialEntry(@NonNull Parcel in) {
-        mType = in.readString();
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(),
-                Credential.class);
+        mType = in.readString8();
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mCredential = in.readTypedObject(Credential.CREATOR);
         mAutoSelectAllowed = in.readBoolean();
     }
 
@@ -95,9 +93,9 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mType);
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
+        dest.writeTypedObject(mCredential, flags);
         dest.writeBoolean(mAutoSelectAllowed);
     }
 
@@ -142,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;
 
         /**
@@ -174,9 +172,11 @@
          * {@code credential}, or the {@code pendingIntent}.
          */
         public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
-            Preconditions.checkState(pendingIntent != null && mCredential != null,
-                    "credential is already set. Cannot set both the pendingIntent "
-                            + "and the credential");
+            if (pendingIntent != null) {
+                Preconditions.checkState(mCredential == null,
+                        "credential is already set. Cannot set both the pendingIntent "
+                                + "and the credential");
+            }
             mPendingIntent = pendingIntent;
             return this;
         }
@@ -188,9 +188,11 @@
          * the {@code pendingIntent}, or the {@code credential}.
          */
         public @NonNull Builder setCredential(@Nullable Credential credential) {
-            Preconditions.checkState(credential != null && mPendingIntent != null,
-                    "pendingIntent is already set. Cannot set both the "
-                            + "pendingIntent and the credential");
+            if (credential != null) {
+                Preconditions.checkState(mPendingIntent == null,
+                        "pendingIntent is already set. Cannot set both the "
+                                + "pendingIntent and the credential");
+            }
             mCredential = credential;
             return this;
         }
@@ -213,10 +215,10 @@
          * is set, or if both are set.
          */
         public @NonNull CredentialEntry build() {
-            Preconditions.checkState(mPendingIntent == null && mCredential == null,
-                    "Either pendingIntent or credential must be set");
-            Preconditions.checkState(mPendingIntent != null && mCredential != null,
-                    "Cannot set both the pendingIntent and credential");
+            Preconditions.checkState(((mPendingIntent != null && mCredential == null)
+                            || (mPendingIntent == null && mCredential != null)),
+                    "Either pendingIntent or credential must be set, and both cannot"
+                            + "be set at the same time");
             return new CredentialEntry(mType, mSlice, mPendingIntent,
                     mCredential, mAutoSelectAllowed);
         }
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
new file mode 100644
index 0000000..f89ad8e
--- /dev/null
+++ b/core/java/android/service/credentials/CredentialProviderInfo.java
@@ -0,0 +1,223 @@
+/*
+ * 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.service.credentials;
+
+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.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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link ServiceInfo} and meta-data about a credential provider.
+ *
+ * @hide
+ */
+public final class CredentialProviderInfo {
+    private static final String TAG = "CredentialProviderInfo";
+
+    @NonNull
+    private final ServiceInfo mServiceInfo;
+    @NonNull
+    private final List<String> mCapabilities;
+
+    @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
+     * @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)
+            throws PackageManager.NameNotFoundException {
+        this(context, getServiceInfoOrThrow(serviceComponent, userId));
+    }
+
+    /**
+     * 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"
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+            throw new SecurityException("Service does not require the expected permission : "
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+        }
+        mContext = context;
+        mServiceInfo = serviceInfo;
+        mCapabilities = new ArrayList<>();
+        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);
+        Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName()
+                .flattenToString());
+        populateProviderCapabilities(context, serviceInfo);
+    }
+
+    private void populateProviderCapabilities(@NonNull Context context, ServiceInfo serviceInfo) {
+        final PackageManager pm = context.getPackageManager();
+        try {
+            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()) {
+                    Slog.i(TAG, "Skipping empty capability");
+                    continue;
+                }
+                Slog.i(TAG, "Capabilities found for provider: " + capability);
+                mCapabilities.add(capability);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.i(TAG, e.getMessage());
+        }
+    }
+
+    private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent,
+            int userId) throws PackageManager.NameNotFoundException {
+        try {
+            ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.GET_META_DATA,
+                    userId);
+            if (si != null) {
+                return si;
+            }
+        } catch (RemoteException e) {
+            Slog.v(TAG, e.getMessage());
+        }
+        throw new PackageManager.NameNotFoundException(serviceComponent.toString());
+    }
+
+    /**
+     * Returns true if the service supports the given {@code credentialType}, false otherwise.
+     */
+    @NonNull
+    public boolean hasCapability(@NonNull String credentialType) {
+        return mCapabilities.contains(credentialType);
+    }
+
+    /** Returns the service info. */
+    @NonNull
+    public ServiceInfo getServiceInfo() {
+        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() {
+        return Collections.unmodifiableList(mCapabilities);
+    }
+
+    /**
+     * 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(CredentialProviderService.SERVICE_INTERFACE),
+                        PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA),
+                        userId);
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            try {
+                services.add(new CredentialProviderInfo(context, serviceInfo));
+            } catch (SecurityException e) {
+                Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e);
+            }
+        }
+        return services;
+    }
+
+    /**
+     * Returns the valid credential provider services available for the user, that can
+     * support the given {@code credentialType}.
+     */
+    @NonNull
+    public static List<CredentialProviderInfo> getAvailableServicesForCapability(
+            @NonNull Context context, @UserIdInt int userId, @NonNull String credentialType) {
+        List<CredentialProviderInfo> servicesForCapability = new ArrayList<>();
+        final List<CredentialProviderInfo> services = getAvailableServices(context, userId);
+
+        for (CredentialProviderInfo service : services) {
+            if (service.hasCapability(credentialType)) {
+                servicesForCapability.add(service);
+            }
+        }
+        return servicesForCapability;
+    }
+}
diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java
index 1cdf186..24b7c3c 100644
--- a/core/java/android/service/credentials/CredentialProviderService.java
+++ b/core/java/android/service/credentials/CredentialProviderService.java
@@ -41,7 +41,31 @@
  * @hide
  */
 public abstract class CredentialProviderService extends Service {
+    /** Extra to be used by provider to populate the credential when ending the activity started
+     * through the {@code pendingIntent} on the selected {@link SaveEntry}. **/
+    public static final String EXTRA_CREATE_CREDENTIAL_RESPONSE =
+            "android.service.credentials.extra.CREATE_CREDENTIAL_RESPONSE";
+
+    /** Extra to be used by provider to populate the {@link CredentialsDisplayContent} when
+     * an authentication action entry is selected. **/
+    public static final String EXTRA_GET_CREDENTIALS_DISPLAY_CONTENT =
+            "android.service.credentials.extra.GET_CREDENTIALS_DISPLAY_CONTENT";
+
+    /**
+     * Provider must read the value against this extra to receive the complete create credential
+     * request parameters, when a pending intent is launched.
+     */
+    public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS =
+            "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS";
+
+    /** Extra to be used by the provider when setting the credential result. */
+    public static final String EXTRA_GET_CREDENTIAL =
+            "android.service.credentials.extra.GET_CREDENTIAL";
+
     private static final String TAG = "CredProviderService";
+
+    public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
+
     private Handler mHandler;
 
     /**
@@ -61,7 +85,7 @@
     }
 
     @Override
-    public final @NonNull IBinder onBind(@NonNull Intent intent) {
+    @NonNull public final IBinder onBind(@NonNull Intent intent) {
         if (SERVICE_INTERFACE.equals(intent.getAction())) {
             return mInterface.asBinder();
         }
@@ -71,12 +95,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 +125,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 +159,7 @@
                         }
                     }
             ));
+            return transport;
         }
     };
 
diff --git a/core/java/android/service/credentials/CredentialsDisplayContent.java b/core/java/android/service/credentials/CredentialsDisplayContent.java
index 4133ea5..4b23800 100644
--- a/core/java/android/service/credentials/CredentialsDisplayContent.java
+++ b/core/java/android/service/credentials/CredentialsDisplayContent.java
@@ -34,27 +34,31 @@
  * @hide
  */
 public final class CredentialsDisplayContent implements Parcelable {
-    /** Header to be displayed on the UI. */
-    private final @Nullable CharSequence mHeader;
-
     /** List of credential entries to be displayed on the UI. */
     private final @NonNull List<CredentialEntry> mCredentialEntries;
 
     /** List of provider actions to be displayed on the UI. */
     private final @NonNull List<Action> mActions;
 
-    private CredentialsDisplayContent(@Nullable CharSequence header,
-            @NonNull List<CredentialEntry> credentialEntries,
-            @NonNull List<Action> actions) {
-        mHeader = header;
+    /** Remote credential entry to get the response from a different device. */
+    private final @Nullable Action mRemoteCredentialEntry;
+
+    private CredentialsDisplayContent(@NonNull List<CredentialEntry> credentialEntries,
+            @NonNull List<Action> actions,
+            @Nullable Action remoteCredentialEntry) {
         mCredentialEntries = credentialEntries;
         mActions = actions;
+        mRemoteCredentialEntry = remoteCredentialEntry;
     }
 
     private CredentialsDisplayContent(@NonNull Parcel in) {
-        mHeader = in.readCharSequence();
-        mCredentialEntries = in.createTypedArrayList(CredentialEntry.CREATOR);
-        mActions = in.createTypedArrayList(Action.CREATOR);
+        List<CredentialEntry> credentialEntries = new ArrayList<>();
+        in.readTypedList(credentialEntries, CredentialEntry.CREATOR);
+        mCredentialEntries = credentialEntries;
+        List<Action> actions = new ArrayList<>();
+        in.readTypedList(actions, Action.CREATOR);
+        mActions = actions;
+        mRemoteCredentialEntry = in.readTypedObject(Action.CREATOR);
     }
 
     public static final @NonNull Creator<CredentialsDisplayContent> CREATOR =
@@ -77,16 +81,9 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeCharSequence(mHeader);
-        dest.writeTypedList(mCredentialEntries);
-        dest.writeTypedList(mActions);
-    }
-
-    /**
-     * Returns the header to be displayed on the UI.
-     */
-    public @Nullable CharSequence getHeader() {
-        return mHeader;
+        dest.writeTypedList(mCredentialEntries, flags);
+        dest.writeTypedList(mActions, flags);
+        dest.writeTypedObject(mRemoteCredentialEntry, flags);
     }
 
     /**
@@ -104,18 +101,25 @@
     }
 
     /**
+     * Returns the remote credential entry to be displayed on the UI.
+     */
+    public @Nullable Action getRemoteCredentialEntry() {
+        return mRemoteCredentialEntry;
+    }
+
+    /**
      * Builds an instance of {@link CredentialsDisplayContent}.
      */
     public static final class Builder {
-        private CharSequence mHeader = null;
         private List<CredentialEntry> mCredentialEntries = new ArrayList<>();
         private List<Action> mActions = new ArrayList<>();
+        private Action mRemoteCredentialEntry;
 
         /**
-         * Sets the header to be displayed on the UI.
+         * Sets the remote credential entry to be displayed on the UI.
          */
-        public @NonNull Builder setHeader(@Nullable CharSequence header) {
-            mHeader = header;
+        public @NonNull Builder setRemoteCredentialEntry(@Nullable Action remoteCredentialEntry) {
+            mRemoteCredentialEntry = remoteCredentialEntry;
             return this;
         }
 
@@ -181,7 +185,8 @@
                 throw new IllegalStateException("credentialEntries and actions must not both "
                         + "be empty");
             }
-            return new CredentialsDisplayContent(mHeader, mCredentialEntries, mActions);
+            return new CredentialsDisplayContent(mCredentialEntries, mActions,
+                    mRemoteCredentialEntry);
         }
     }
 }
diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java
index 5b1a171..03ba20e 100644
--- a/core/java/android/service/credentials/GetCredentialsRequest.java
+++ b/core/java/android/service/credentials/GetCredentialsRequest.java
@@ -49,8 +49,10 @@
     }
 
     private GetCredentialsRequest(@NonNull Parcel in) {
-        mCallingPackage = in.readString16NoHelper();
-        mGetCredentialOptions = in.createTypedArrayList(GetCredentialOption.CREATOR);
+        mCallingPackage = in.readString8();
+        List<GetCredentialOption> getCredentialOptions = new ArrayList<>();
+        in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR);
+        mGetCredentialOptions = getCredentialOptions;
     }
 
     public static final @NonNull Creator<GetCredentialsRequest> CREATOR =
@@ -73,7 +75,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString16NoHelper(mCallingPackage);
+        dest.writeString8(mCallingPackage);
         dest.writeTypedList(mGetCredentialOptions);
     }
 
@@ -117,9 +119,9 @@
          */
         public @NonNull Builder setGetCredentialOptions(
                 @NonNull List<GetCredentialOption> getCredentialOptions) {
-            Preconditions.checkCollectionNotEmpty(mGetCredentialOptions,
+            Preconditions.checkCollectionNotEmpty(getCredentialOptions,
                     "getCredentialOptions");
-            Preconditions.checkCollectionElementsNotNull(mGetCredentialOptions,
+            Preconditions.checkCollectionElementsNotNull(getCredentialOptions,
                     "getCredentialOptions");
             mGetCredentialOptions = getCredentialOptions;
             return this;
diff --git a/core/java/android/service/credentials/GetCredentialsResponse.java b/core/java/android/service/credentials/GetCredentialsResponse.java
index 980d9ae..979a699 100644
--- a/core/java/android/service/credentials/GetCredentialsResponse.java
+++ b/core/java/android/service/credentials/GetCredentialsResponse.java
@@ -78,9 +78,8 @@
     }
 
     private GetCredentialsResponse(@NonNull Parcel in) {
-        mCredentialsDisplayContent = in.readParcelable(CredentialsDisplayContent.class
-                .getClassLoader(), CredentialsDisplayContent.class);
-        mAuthenticationAction = in.readParcelable(Action.class.getClassLoader(), Action.class);
+        mCredentialsDisplayContent = in.readTypedObject(CredentialsDisplayContent.CREATOR);
+        mAuthenticationAction = in.readTypedObject(Action.CREATOR);
     }
 
     public static final @NonNull Creator<GetCredentialsResponse> CREATOR =
@@ -103,8 +102,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mCredentialsDisplayContent, flags);
-        dest.writeParcelable(mAuthenticationAction, flags);
+        dest.writeTypedObject(mCredentialsDisplayContent, flags);
+        dest.writeTypedObject(mAuthenticationAction, flags);
     }
 
     /**
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/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java
index 18644f0..55ff6ff 100644
--- a/core/java/android/service/credentials/SaveEntry.java
+++ b/core/java/android/service/credentials/SaveEntry.java
@@ -17,17 +17,11 @@
 package android.service.credentials;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.app.slice.Slice;
-import android.credentials.Credential;
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.internal.util.Preconditions;
-
-import java.util.Objects;
-
 /**
  * An entry to be shown on the UI. This entry represents where the credential to be created will
  * be stored. Examples include user's account, family group etc.
@@ -36,14 +30,11 @@
  */
 public final class SaveEntry implements Parcelable {
     private final @NonNull Slice mSlice;
-    private final @Nullable PendingIntent mPendingIntent;
-    private final @Nullable Credential mCredential;
+    private final @NonNull PendingIntent mPendingIntent;
 
     private SaveEntry(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(), Credential.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
     }
 
     public static final @NonNull Creator<SaveEntry> CREATOR = new Creator<SaveEntry>() {
@@ -65,20 +56,25 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
     }
 
-    /* package-private */ SaveEntry(
+    /**
+     * Constructs a save entry to be displayed on the UI.
+     *
+     * @param slice the display content to be displayed on the UI, along with this entry
+     * @param pendingIntent the intent to be invoked when the user selects this entry
+     */
+    public SaveEntry(
             @NonNull Slice slice,
-            @Nullable PendingIntent pendingIntent,
-            @Nullable Credential credential) {
+            @NonNull PendingIntent pendingIntent) {
         this.mSlice = slice;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mSlice);
         this.mPendingIntent = pendingIntent;
-        this.mCredential = credential;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, mPendingIntent);
     }
 
     /** Returns the content to be displayed with this save entry on the UI. */
@@ -87,76 +83,7 @@
     }
 
     /** Returns the pendingIntent to be invoked when this save entry on the UI is selectcd. */
-    public @Nullable PendingIntent getPendingIntent() {
+    public @NonNull PendingIntent getPendingIntent() {
         return mPendingIntent;
     }
-
-    /** Returns the credential produced by the {@link CreateCredentialRequest}. */
-    public @Nullable Credential getCredential() {
-        return mCredential;
-    }
-
-    /**
-     * A builder for {@link SaveEntry}.
-     */
-    public static final class Builder {
-
-        private @NonNull Slice mSlice;
-        private @Nullable PendingIntent mPendingIntent;
-        private @Nullable Credential mCredential;
-
-        /**
-         * Builds the instance.
-         * @param slice the content to be displayed with this save entry
-         *
-         * @throws NullPointerException If {@code slice} is null.
-         */
-        public Builder(@NonNull Slice slice) {
-            mSlice = Objects.requireNonNull(slice, "slice must not be null");
-        }
-
-        /**
-         * Sets the pendingIntent to be invoked when this entry is selected by the user.
-         *
-         * @throws IllegalStateException If {@code credential} is already set. Must only set either
-         * {@code credential}, or the {@code pendingIntent}.
-         */
-        public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
-            Preconditions.checkState(pendingIntent != null
-                    && mCredential != null, "credential is already set. Must only set "
-                    + "either the pendingIntent or the credential");
-            mPendingIntent = pendingIntent;
-            return this;
-        }
-
-        /**
-         * Sets the credential to be returned when this entry is selected by the user.
-         *
-         * @throws IllegalStateException If {@code pendingIntent} is already set. Must only
-         * set either the {@code pendingIntent}, or {@code credential}.
-         */
-        public @NonNull Builder setCredential(@Nullable Credential credential) {
-            Preconditions.checkState(credential != null && mPendingIntent != null,
-                    "pendingIntent is already set. Must only set either the credential "
-                            + "or the pendingIntent");
-            mCredential = credential;
-            return this;
-        }
-
-        /**
-         * Builds the instance.
-         *
-         * @throws IllegalStateException if both {@code pendingIntent} and {@code credential}
-         * are null.
-         */
-        public @NonNull SaveEntry build() {
-            Preconditions.checkState(mPendingIntent == null && mCredential == null,
-                    "pendingIntent and credential both must not be null. Must set "
-                            + "either the pendingIntnet or the credential");
-            return new SaveEntry(
-                    mSlice,
-                    mPendingIntent,
-                    mCredential);
-        }
-    }
 }
diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java
index f6a7c8e..a2fa139 100644
--- a/core/java/android/service/dreams/DreamActivity.java
+++ b/core/java/android/service/dreams/DreamActivity.java
@@ -44,6 +44,8 @@
 public class DreamActivity extends Activity {
     static final String EXTRA_CALLBACK = "binder";
     static final String EXTRA_DREAM_TITLE = "title";
+    @Nullable
+    private DreamService.DreamActivityCallbacks mCallback;
 
     public DreamActivity() {}
 
@@ -57,11 +59,19 @@
         }
 
         final Bundle extras = getIntent().getExtras();
-        final DreamService.DreamActivityCallback callback =
-                (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK);
+        mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK);
 
-        if (callback != null) {
-            callback.onActivityCreated(this);
+        if (mCallback != null) {
+            mCallback.onActivityCreated(this);
         }
     }
+
+    @Override
+    public void onDestroy() {
+        if (mCallback != null) {
+            mCallback.onActivityDestroyed();
+        }
+
+        super.onDestroy();
+    }
 }
diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java
index 295171c..cd38e8a 100644
--- a/core/java/android/service/dreams/DreamManagerInternal.java
+++ b/core/java/android/service/dreams/DreamManagerInternal.java
@@ -16,7 +16,6 @@
 
 package android.service.dreams;
 
-import android.content.ComponentName;
 
 /**
  * Dream manager local system service interface.
@@ -54,17 +53,9 @@
     public abstract void requestDream();
 
     /**
-     * Called by the ActivityTaskManagerService to verify that the startDreamActivity
-     * request comes from the current active dream component.
+     * Whether dreaming can start given user settings and the current dock/charge state.
      *
-     * This function and its call path should not acquire the DreamManagerService lock
-     * to avoid deadlock with the ActivityTaskManager lock.
-     *
-     * TODO: Make this interaction push-based - the DreamManager should inform the
-     * ActivityTaskManager whenever the active dream component changes.
-     *
-     * @param doze If true returns the current active doze component. Otherwise, returns the
-     *             active dream component.
+     * @param isScreenOn True if the screen is currently on.
      */
-    public abstract ComponentName getActiveDreamComponent(boolean doze);
+    public abstract boolean canStartDreaming(boolean isScreenOn);
 }
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index aa45c20..6e8198b 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -49,6 +49,17 @@
             mShowComplications = shouldShowComplications;
             onStartDream(layoutParams);
         }
+
+        @Override
+        public void wakeUp() {
+            onWakeUp(() -> {
+                try {
+                    mDreamOverlayCallback.onWakeUpComplete();
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Could not notify dream of wakeUp:" + e);
+                }
+            });
+        }
     };
 
     IDreamOverlayCallback mDreamOverlayCallback;
@@ -71,6 +82,17 @@
     public abstract void onStartDream(@NonNull WindowManager.LayoutParams layoutParams);
 
     /**
+     * This method is overridden by implementations to handle when the dream has been requested
+     * to wakeup. This allows any overlay animations to run.
+     *
+     * @param onCompleteCallback The callback to trigger to notify the dream service that the
+     *                           overlay has completed waking up.
+     * @hide
+     */
+    public void onWakeUp(@NonNull Runnable onCompleteCallback) {
+    }
+
+    /**
      * This method is invoked to request the dream exit.
      */
     public final void requestExit() {
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3c1fef0..8b9852a 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -312,7 +312,14 @@
         @Override
         public void onExitRequested() {
             // Simply finish dream when exit is requested.
-            finish();
+            mHandler.post(() -> finish());
+        }
+
+        @Override
+        public void onWakeUpComplete() {
+            // Finish the dream once overlay animations are complete. Execute on handler since
+            // this is coming in on the overlay binder.
+            mHandler.post(() -> finish());
         }
     };
 
@@ -975,7 +982,18 @@
      * </p>
      */
     public void onWakeUp() {
-        finish();
+        if (mOverlayConnection != null) {
+            mOverlayConnection.addConsumer(overlay -> {
+                try {
+                    overlay.wakeUp();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error waking the overlay service", e);
+                    finish();
+                }
+            });
+        } else {
+            finish();
+        }
     }
 
     /** {@inheritDoc} */
@@ -1047,7 +1065,7 @@
         }
 
         if (mDreamToken == null) {
-            Slog.w(mTag, "Finish was called before the dream was attached.");
+            if (mDebug) Slog.v(mTag, "finish() called when not attached.");
             stopSelf();
             return;
         }
@@ -1294,8 +1312,8 @@
         if (!mWindowless) {
             Intent i = new Intent(this, DreamActivity.class);
             i.setPackage(getApplicationContext().getPackageName());
-            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken));
+            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken));
             final ServiceInfo serviceInfo = fetchServiceInfo(this,
                     new ComponentName(this, getClass()));
             i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo));
@@ -1488,10 +1506,10 @@
     }
 
     /** @hide */
-    final class DreamActivityCallback extends Binder {
+    final class DreamActivityCallbacks extends Binder {
         private final IBinder mActivityDreamToken;
 
-        DreamActivityCallback(IBinder token) {
+        DreamActivityCallbacks(IBinder token) {
             mActivityDreamToken = token;
         }
 
@@ -1516,6 +1534,12 @@
             mActivity = activity;
             onWindowCreated(activity.getWindow());
         }
+
+        // If DreamActivity is destroyed, wake up from Dream.
+        void onActivityDestroyed() {
+            mActivity = null;
+            onDestroy();
+        }
     }
 
     /**
diff --git a/core/java/android/service/dreams/IDreamOverlay.aidl b/core/java/android/service/dreams/IDreamOverlay.aidl
index 05ebbfe..7aeceb2c 100644
--- a/core/java/android/service/dreams/IDreamOverlay.aidl
+++ b/core/java/android/service/dreams/IDreamOverlay.aidl
@@ -38,4 +38,7 @@
     */
     void startDream(in LayoutParams params, in IDreamOverlayCallback callback,
         in String dreamComponent, in boolean shouldShowComplications);
+
+    /** Called when the dream is waking, to do any exit animations */
+    void wakeUp();
 }
diff --git a/core/java/android/service/dreams/IDreamOverlayCallback.aidl b/core/java/android/service/dreams/IDreamOverlayCallback.aidl
index ec76a33..4ad63f1 100644
--- a/core/java/android/service/dreams/IDreamOverlayCallback.aidl
+++ b/core/java/android/service/dreams/IDreamOverlayCallback.aidl
@@ -28,4 +28,7 @@
     * Invoked to request the dream exit.
     */
     void onExitRequested();
+
+    /** Invoked when the dream overlay wakeUp animation is complete. */
+    void onWakeUpComplete();
 }
\ No newline at end of file
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index fb52ed2..e821af1 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -391,6 +391,15 @@
      */
     public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3;
 
+    /**
+     * An optional activity intent action that shows additional settings for what notifications
+     * should be processed by this notification listener service. If defined, the OS may link to
+     * this activity from the system notification listener service filter settings page.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SETTINGS_HOME =
+            "android.service.notification.action.SETTINGS_HOME";
+
     private final Object mLock = new Object();
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
@@ -2356,6 +2365,7 @@
                     UserHandle user= (UserHandle) args.arg2;
                     NotificationChannel channel = (NotificationChannel) args.arg3;
                     int modificationType = (int) args.arg4;
+                    args.recycle();
                     onNotificationChannelModified(pkgName, user, channel, modificationType);
                 } break;
 
@@ -2365,6 +2375,7 @@
                     UserHandle user = (UserHandle) args.arg2;
                     NotificationChannelGroup group = (NotificationChannelGroup) args.arg3;
                     int modificationType = (int) args.arg4;
+                    args.recycle();
                     onNotificationChannelGroupModified(pkgName, user, group, modificationType);
                 } break;
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index e285b1c..eb9901a 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -48,12 +48,12 @@
 import android.util.ArraySet;
 import android.util.PluralsMessageFormatter;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/service/timezone/TimeZoneProviderEvent.java b/core/java/android/service/timezone/TimeZoneProviderEvent.java
index f6433b7..e64bdd6 100644
--- a/core/java/android/service/timezone/TimeZoneProviderEvent.java
+++ b/core/java/android/service/timezone/TimeZoneProviderEvent.java
@@ -57,7 +57,7 @@
 
     /**
      * The provider was uncertain about the time zone. See {@link
-     * TimeZoneProviderService#reportUncertain()}
+     * TimeZoneProviderService#reportUncertain(TimeZoneProviderStatus)}
      */
     public static final @EventType int EVENT_TYPE_UNCERTAIN = 3;
 
@@ -66,42 +66,69 @@
     @ElapsedRealtimeLong
     private final long mCreationElapsedMillis;
 
+    // Populated when mType == EVENT_TYPE_SUGGESTION
     @Nullable
     private final TimeZoneProviderSuggestion mSuggestion;
 
+    // Populated when mType == EVENT_TYPE_PERMANENT_FAILURE
     @Nullable
     private final String mFailureCause;
 
+    // May be populated when EVENT_TYPE_SUGGESTION or EVENT_TYPE_UNCERTAIN
+    @Nullable
+    private final TimeZoneProviderStatus mTimeZoneProviderStatus;
+
     private TimeZoneProviderEvent(@EventType int type,
             @ElapsedRealtimeLong long creationElapsedMillis,
             @Nullable TimeZoneProviderSuggestion suggestion,
-            @Nullable String failureCause) {
-        mType = type;
+            @Nullable String failureCause,
+            @Nullable TimeZoneProviderStatus timeZoneProviderStatus) {
+        mType = validateEventType(type);
         mCreationElapsedMillis = creationElapsedMillis;
         mSuggestion = suggestion;
         mFailureCause = failureCause;
+        mTimeZoneProviderStatus = timeZoneProviderStatus;
+
+        // Confirm the type and the provider status agree.
+        if (mType == EVENT_TYPE_PERMANENT_FAILURE && mTimeZoneProviderStatus != null) {
+            throw new IllegalArgumentException(
+                    "Unexpected status: mType=" + mType
+                            + ", mTimeZoneProviderStatus=" + mTimeZoneProviderStatus);
+        }
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */
+    private static @EventType int validateEventType(@EventType int eventType) {
+        if (eventType < EVENT_TYPE_PERMANENT_FAILURE || eventType > EVENT_TYPE_UNCERTAIN) {
+            throw new IllegalArgumentException(Integer.toString(eventType));
+        }
+        return eventType;
+    }
+
+    /** Returns an event of type {@link #EVENT_TYPE_SUGGESTION}. */
     public static TimeZoneProviderEvent createSuggestionEvent(
             @ElapsedRealtimeLong long creationElapsedMillis,
-            @NonNull TimeZoneProviderSuggestion suggestion) {
+            @NonNull TimeZoneProviderSuggestion suggestion,
+            @Nullable TimeZoneProviderStatus providerStatus) {
         return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, creationElapsedMillis,
-                Objects.requireNonNull(suggestion), null);
+                Objects.requireNonNull(suggestion), null, providerStatus);
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */
+    /** Returns an event of type {@link #EVENT_TYPE_UNCERTAIN}. */
     public static TimeZoneProviderEvent createUncertainEvent(
-            @ElapsedRealtimeLong long creationElapsedMillis) {
-        return new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null);
+            @ElapsedRealtimeLong long creationElapsedMillis,
+            @Nullable TimeZoneProviderStatus timeZoneProviderStatus) {
+
+        return new TimeZoneProviderEvent(
+                EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null,
+                timeZoneProviderStatus);
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
+    /** Returns an event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
     public static TimeZoneProviderEvent createPermanentFailureEvent(
             @ElapsedRealtimeLong long creationElapsedMillis,
             @NonNull String cause) {
         return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, creationElapsedMillis, null,
-                Objects.requireNonNull(cause));
+                Objects.requireNonNull(cause), null);
     }
 
     /**
@@ -126,7 +153,7 @@
     }
 
     /**
-     * Returns the failure cauese. Populated when {@link #getType()} is {@link
+     * Returns the failure cause. Populated when {@link #getType()} is {@link
      * #EVENT_TYPE_PERMANENT_FAILURE}.
      */
     @Nullable
@@ -134,24 +161,34 @@
         return mFailureCause;
     }
 
-    public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR =
-            new Creator<TimeZoneProviderEvent>() {
-                @Override
-                public TimeZoneProviderEvent createFromParcel(Parcel in) {
-                    int type = in.readInt();
-                    long creationElapsedMillis = in.readLong();
-                    TimeZoneProviderSuggestion suggestion =
-                            in.readParcelable(getClass().getClassLoader(), android.service.timezone.TimeZoneProviderSuggestion.class);
-                    String failureCause = in.readString8();
-                    return new TimeZoneProviderEvent(
-                            type, creationElapsedMillis, suggestion, failureCause);
-                }
+    /**
+     * Returns the status of the time zone provider.  May be populated when {@link #getType()} is
+     * {@link #EVENT_TYPE_UNCERTAIN} or {@link #EVENT_TYPE_SUGGESTION}, otherwise {@code null}.
+     */
+    @Nullable
+    public TimeZoneProviderStatus getTimeZoneProviderStatus() {
+        return mTimeZoneProviderStatus;
+    }
 
-                @Override
-                public TimeZoneProviderEvent[] newArray(int size) {
-                    return new TimeZoneProviderEvent[size];
-                }
-            };
+    public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR = new Creator<>() {
+        @Override
+        public TimeZoneProviderEvent createFromParcel(Parcel in) {
+            int type = in.readInt();
+            long creationElapsedMillis = in.readLong();
+            TimeZoneProviderSuggestion suggestion = in.readParcelable(
+                    getClass().getClassLoader(), TimeZoneProviderSuggestion.class);
+            String failureCause = in.readString8();
+            TimeZoneProviderStatus status = in.readParcelable(
+                    getClass().getClassLoader(), TimeZoneProviderStatus.class);
+            return new TimeZoneProviderEvent(
+                    type, creationElapsedMillis, suggestion, failureCause, status);
+        }
+
+        @Override
+        public TimeZoneProviderEvent[] newArray(int size) {
+            return new TimeZoneProviderEvent[size];
+        }
+    };
 
     @Override
     public int describeContents() {
@@ -164,6 +201,7 @@
         parcel.writeLong(mCreationElapsedMillis);
         parcel.writeParcelable(mSuggestion, 0);
         parcel.writeString8(mFailureCause);
+        parcel.writeParcelable(mTimeZoneProviderStatus, 0);
     }
 
     @Override
@@ -173,14 +211,17 @@
                 + ", mCreationElapsedMillis=" + Duration.ofMillis(mCreationElapsedMillis).toString()
                 + ", mSuggestion=" + mSuggestion
                 + ", mFailureCause=" + mFailureCause
+                + ", mTimeZoneProviderStatus=" + mTimeZoneProviderStatus
                 + '}';
     }
 
     /**
      * Similar to {@link #equals} except this methods checks for equivalence, not equality.
-     * i.e. two {@link #EVENT_TYPE_UNCERTAIN} and {@link #EVENT_TYPE_PERMANENT_FAILURE} events are
-     * always equivalent, two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest
-     * the same time zones.
+     * i.e. two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest
+     * the same time zones and have the same provider status, two {@link #EVENT_TYPE_UNCERTAIN}
+     * events are equivalent if they have the same provider status, and {@link
+     * #EVENT_TYPE_PERMANENT_FAILURE} events are always equivalent (the nature of the failure is not
+     * considered).
      */
     @SuppressWarnings("ReferenceEquality")
     public boolean isEquivalentTo(@Nullable TimeZoneProviderEvent other) {
@@ -191,9 +232,10 @@
             return false;
         }
         if (mType == EVENT_TYPE_SUGGESTION) {
-            return mSuggestion.isEquivalentTo(other.getSuggestion());
+            return mSuggestion.isEquivalentTo(other.mSuggestion)
+                    && Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus);
         }
-        return true;
+        return Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus);
     }
 
     @Override
@@ -208,11 +250,13 @@
         return mType == that.mType
                 && mCreationElapsedMillis == that.mCreationElapsedMillis
                 && Objects.equals(mSuggestion, that.mSuggestion)
-                && Objects.equals(mFailureCause, that.mFailureCause);
+                && Objects.equals(mFailureCause, that.mFailureCause)
+                && Objects.equals(mTimeZoneProviderStatus, that.mTimeZoneProviderStatus);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause);
+        return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause,
+                mTimeZoneProviderStatus);
     }
 }
diff --git a/core/java/android/service/timezone/TimeZoneProviderService.java b/core/java/android/service/timezone/TimeZoneProviderService.java
index 0d215f6..41ca94b 100644
--- a/core/java/android/service/timezone/TimeZoneProviderService.java
+++ b/core/java/android/service/timezone/TimeZoneProviderService.java
@@ -44,8 +44,8 @@
  *
  * <p>Once started, providers are expected to detect the time zone if possible, and report the
  * result via {@link #reportSuggestion(TimeZoneProviderSuggestion)} or {@link
- * #reportUncertain()}. Providers may also report that they have permanently failed
- * by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each
+ * #reportUncertain(TimeZoneProviderStatus)}. Providers may also report that they have permanently
+ * failed by calling {@link #reportPermanentFailure(Throwable)}. See the javadocs for each
  * method for details.
  *
  * <p>After starting, providers are expected to issue their first callback within the timeout
@@ -203,6 +203,25 @@
      * details.
      */
     public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion) {
+        TimeZoneProviderStatus providerStatus = null;
+        reportSuggestionInternal(suggestion, providerStatus);
+    }
+
+    /**
+     * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for
+     * details.
+     *
+     * @param providerStatus provider status information that can influence detector service
+     *   behavior and/or be reported via the device UI
+     */
+    public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion,
+            @NonNull TimeZoneProviderStatus providerStatus) {
+        Objects.requireNonNull(providerStatus);
+        reportSuggestionInternal(suggestion, providerStatus);
+    }
+
+    private void reportSuggestionInternal(@NonNull TimeZoneProviderSuggestion suggestion,
+            @Nullable TimeZoneProviderStatus providerStatus) {
         Objects.requireNonNull(suggestion);
 
         mHandler.post(() -> {
@@ -212,7 +231,7 @@
                     try {
                         TimeZoneProviderEvent thisEvent =
                                 TimeZoneProviderEvent.createSuggestionEvent(
-                                        SystemClock.elapsedRealtime(), suggestion);
+                                        SystemClock.elapsedRealtime(), suggestion, providerStatus);
                         if (shouldSendEvent(thisEvent)) {
                             manager.onTimeZoneProviderEvent(thisEvent);
                             mLastEventSent = thisEvent;
@@ -227,10 +246,30 @@
 
     /**
      * Indicates the time zone is not known because of an expected runtime state or error, e.g. when
-     * the provider is unable to detect location, or there was a problem when resolving the location
-     * to a time zone.
+     * the provider is unable to detect location, or there was connectivity issue.
+     *
+     * <p>See {@link #reportUncertain(TimeZoneProviderStatus)} for a more expressive version
      */
     public final void reportUncertain() {
+        TimeZoneProviderStatus providerStatus = null;
+        reportUncertainInternal(providerStatus);
+    }
+
+    /**
+     * Indicates the time zone is not known because of an expected runtime state or error.
+     *
+     * <p>When the status changes then a certain or uncertain report must be made to move the
+     * detector service to the new status.
+     *
+     * @param providerStatus provider status information that can influence detector service
+     *   behavior and/or be reported via the device UI
+     */
+    public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) {
+        Objects.requireNonNull(providerStatus);
+        reportUncertainInternal(providerStatus);
+    }
+
+    private void reportUncertainInternal(@Nullable TimeZoneProviderStatus providerStatus) {
         mHandler.post(() -> {
             synchronized (mLock) {
                 ITimeZoneProviderManager manager = mManager;
@@ -238,7 +277,7 @@
                     try {
                         TimeZoneProviderEvent thisEvent =
                                 TimeZoneProviderEvent.createUncertainEvent(
-                                        SystemClock.elapsedRealtime());
+                                        SystemClock.elapsedRealtime(), providerStatus);
                         if (shouldSendEvent(thisEvent)) {
                             manager.onTimeZoneProviderEvent(thisEvent);
                             mLastEventSent = thisEvent;
@@ -320,8 +359,8 @@
      * <p>Between {@link #onStartUpdates(long)} and {@link #onStopUpdates()} calls, the Android
      * system server holds the latest report from the provider in memory. After an initial report,
      * provider implementations are only required to send a report via {@link
-     * #reportSuggestion(TimeZoneProviderSuggestion)} or via {@link #reportUncertain()} when it
-     * differs from the previous report.
+     * #reportSuggestion(TimeZoneProviderSuggestion, TimeZoneProviderStatus)} or via {@link
+     * #reportUncertain(TimeZoneProviderStatus)} when it differs from the previous report.
      *
      * <p>{@link #reportPermanentFailure(Throwable)} can also be called by provider implementations
      * in rare cases, after which the provider should consider itself stopped and not make any
@@ -333,7 +372,8 @@
      * Android system server may move on to use other providers or detection methods. Providers
      * should therefore make best efforts during this time to generate a report, which could involve
      * increased power usage. Providers should preferably report an explicit {@link
-     * #reportUncertain()} if the time zone(s) cannot be detected within the initialization timeout.
+     * #reportUncertain(TimeZoneProviderStatus)} if the time zone(s) cannot be detected within the
+     * initialization timeout.
      *
      * @see #onStopUpdates() for the signal from the system server to stop sending reports
      */
diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.aidl b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl
new file mode 100644
index 0000000..91dc7e9
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.service.timezone;
+
+/**
+ * @hide
+ */
+parcelable TimeZoneProviderStatus;
diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.java b/core/java/android/service/timezone/TimeZoneProviderStatus.java
new file mode 100644
index 0000000..e0b78e9
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderStatus.java
@@ -0,0 +1,481 @@
+/*
+ * 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.service.timezone;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Information about the status of a {@link TimeZoneProviderService}.
+ *
+ * <p>Not all status properties or status values will apply to all provider implementations.
+ * {@code _NOT_APPLICABLE} status can be used to indicate properties that have no meaning for a
+ * given implementation.
+ *
+ * <p>Time zone providers are expected to work in one of two ways:
+ * <ol>
+ *     <li>Location: Providers will determine location and then map that location to one or more
+ *     time zone IDs.</li>
+ *     <li>External signals: Providers could use indirect signals like country code
+ *     and/or local offset / DST information provided to the device to infer a time zone, e.g.
+ *     signals like MCC and NITZ for telephony devices, IP geo location, or DHCP information
+ *     (RFC4833). The time zone ID could also be fed directly to the device by an external service.
+ *     </li>
+ * </ol>
+ *
+ * <p>The status properties are:
+ * <ul>
+ *     <li>location detection - for location-based providers, the status of the location detection
+ *     mechanism</li>
+ *     <li>connectivity - connectivity can influence providers directly, for example if they use
+ *     a networked service to map location to time zone ID, or use geo IP, or indirectly for
+ *     location detection (e.g. for the network location provider.</li>
+ *     <li>time zone resolution - the status related to determining a time zone ID or using a
+ *     detected time zone ID. For example, a networked service may be reachable (i.e. connectivity
+ *     is working) but the service could return errors, a time zone ID detected may not be usable
+ *     for a device because of TZDB version skew, or external indirect signals may available but
+ *     do not match the properties of a known time zone ID.</li>
+ * </ul>
+ *
+ * @hide
+ */
+@SystemApi
+public final class TimeZoneProviderStatus implements Parcelable {
+
+    /**
+     * A status code related to a dependency a provider may have.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "DEPENDENCY_STATUS_", value = {
+            DEPENDENCY_STATUS_UNKNOWN,
+            DEPENDENCY_STATUS_NOT_APPLICABLE,
+            DEPENDENCY_STATUS_OK,
+            DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE,
+            DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT,
+            DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS,
+            DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DependencyStatus {}
+
+    /**
+     * The dependency's status is unknown.
+     *
+     * @hide
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_UNKNOWN = 0;
+
+    /** The dependency is not used by the provider's implementation. */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_NOT_APPLICABLE = 1;
+
+    /** The dependency is applicable and there are no known problems. */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_OK = 2;
+
+    /**
+     * The dependency is used but is temporarily unavailable, e.g. connectivity has been lost for an
+     * unpredictable amount of time.
+     *
+     * <p>This status is considered normal is may be entered many times a day.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE = 3;
+
+    /**
+     * The dependency is used by the provider but is blocked by the environment in a way that the
+     * provider has detected and is considered likely to persist for some time, e.g. connectivity
+     * has been lost due to boarding a plane.
+     *
+     * <p>This status is considered unusual and could be used by the system as a trigger to try
+     * other time zone providers / time zone detection mechanisms. The bar for using this status
+     * should therefore be set fairly high to avoid a device bringing up other providers or
+     * switching to a different detection mechanism that may provide a different suggestion.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = 4;
+
+    /**
+     * The dependency is used by the provider but is running in a degraded mode due to the user's
+     * settings. A user can take action to improve this, e.g. by changing a setting.
+     *
+     * <p>This status could be used by the system as a trigger to try other time zone
+     * providers / time zone detection mechanisms. The user may be informed.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS = 5;
+
+    /**
+     * The dependency is used by the provider but is completely blocked by the user's settings.
+     * A user can take action to correct this, e.g. by changing a setting.
+     *
+     * <p>This status could be used by the system as a trigger to try other time zone providers /
+     * time zone detection mechanisms. The user may be informed.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = 6;
+
+    /**
+     * A status code related to an operation in a provider's detection algorithm.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "OPERATION_STATUS_", value = {
+            OPERATION_STATUS_UNKNOWN,
+            OPERATION_STATUS_NOT_APPLICABLE,
+            OPERATION_STATUS_OK,
+            OPERATION_STATUS_FAILED,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OperationStatus {}
+
+    /**
+     * The operation's status is unknown.
+     *
+     * @hide
+     */
+    public static final @OperationStatus int OPERATION_STATUS_UNKNOWN = 0;
+
+    /** The operation is not used by the provider's implementation. */
+    public static final @OperationStatus int OPERATION_STATUS_NOT_APPLICABLE = 1;
+
+    /** The operation is applicable and there are no known problems. */
+    public static final @OperationStatus int OPERATION_STATUS_OK = 2;
+
+    /** The operation is applicable and it recently failed. */
+    public static final @OperationStatus int OPERATION_STATUS_FAILED = 3;
+
+    private final @DependencyStatus int mLocationDetectionDependencyStatus;
+    private final @DependencyStatus int mConnectivityDependencyStatus;
+    private final @OperationStatus int mTimeZoneResolutionOperationStatus;
+
+    private TimeZoneProviderStatus(
+            @DependencyStatus int locationDetectionStatus,
+            @DependencyStatus int connectivityStatus,
+            @OperationStatus int timeZoneResolutionStatus) {
+        mLocationDetectionDependencyStatus = locationDetectionStatus;
+        mConnectivityDependencyStatus = connectivityStatus;
+        mTimeZoneResolutionOperationStatus = timeZoneResolutionStatus;
+    }
+
+    /**
+     * Returns the status of the location detection dependencies used by the provider (where
+     * applicable).
+     */
+    public @DependencyStatus int getLocationDetectionDependencyStatus() {
+        return mLocationDetectionDependencyStatus;
+    }
+
+    /**
+     * Returns the status of the connectivity dependencies used by the provider (where applicable).
+     */
+    public @DependencyStatus int getConnectivityDependencyStatus() {
+        return mConnectivityDependencyStatus;
+    }
+
+    /**
+     * Returns the status of the time zone resolution operation used by the provider.
+     */
+    public @OperationStatus int getTimeZoneResolutionOperationStatus() {
+        return mTimeZoneResolutionOperationStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "TimeZoneProviderStatus{"
+                + "mLocationDetectionDependencyStatus="
+                + dependencyStatusToString(mLocationDetectionDependencyStatus)
+                + ", mConnectivityDependencyStatus="
+                + dependencyStatusToString(mConnectivityDependencyStatus)
+                + ", mTimeZoneResolutionOperationStatus="
+                + operationStatusToString(mTimeZoneResolutionOperationStatus)
+                + '}';
+    }
+
+    /**
+     * Parses a {@link TimeZoneProviderStatus} from a toString() string for manual command-line
+     * testing.
+     *
+     * @hide
+     */
+    @NonNull
+    public static TimeZoneProviderStatus parseProviderStatus(@NonNull String arg) {
+        // Note: "}" has to be escaped on Android with "\\}" because the regexp library is not based
+        // on OpenJDK code.
+        Pattern pattern = Pattern.compile("TimeZoneProviderStatus\\{"
+                + "mLocationDetectionDependencyStatus=([^,]+)"
+                + ", mConnectivityDependencyStatus=([^,]+)"
+                + ", mTimeZoneResolutionOperationStatus=([^\\}]+)"
+                + "\\}");
+        Matcher matcher = pattern.matcher(arg);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Unable to parse provider status: " + arg);
+        }
+        @DependencyStatus int locationDependencyStatus =
+                dependencyStatusFromString(matcher.group(1));
+        @DependencyStatus int connectivityDependencyStatus =
+                dependencyStatusFromString(matcher.group(2));
+        @OperationStatus int timeZoneResolutionOperationStatus =
+                operationStatusFromString(matcher.group(3));
+        return new TimeZoneProviderStatus(locationDependencyStatus, connectivityDependencyStatus,
+                timeZoneResolutionOperationStatus);
+    }
+
+    public static final @NonNull Creator<TimeZoneProviderStatus> CREATOR = new Creator<>() {
+        @Override
+        public TimeZoneProviderStatus createFromParcel(Parcel in) {
+            @DependencyStatus int locationDetectionStatus = in.readInt();
+            @DependencyStatus int connectivityStatus = in.readInt();
+            @OperationStatus int timeZoneResolutionStatus = in.readInt();
+            return new TimeZoneProviderStatus(
+                    locationDetectionStatus, connectivityStatus, timeZoneResolutionStatus);
+        }
+
+        @Override
+        public TimeZoneProviderStatus[] newArray(int size) {
+            return new TimeZoneProviderStatus[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mLocationDetectionDependencyStatus);
+        parcel.writeInt(mConnectivityDependencyStatus);
+        parcel.writeInt(mTimeZoneResolutionOperationStatus);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TimeZoneProviderStatus that = (TimeZoneProviderStatus) o;
+        return mLocationDetectionDependencyStatus == that.mLocationDetectionDependencyStatus
+                && mConnectivityDependencyStatus == that.mConnectivityDependencyStatus
+                && mTimeZoneResolutionOperationStatus == that.mTimeZoneResolutionOperationStatus;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mLocationDetectionDependencyStatus, mConnectivityDependencyStatus,
+                mTimeZoneResolutionOperationStatus);
+    }
+
+    /** @hide */
+    public boolean couldEnableTelephonyFallback() {
+        return mLocationDetectionDependencyStatus == DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT
+                || mLocationDetectionDependencyStatus == DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS
+                || mConnectivityDependencyStatus == DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT
+                || mConnectivityDependencyStatus == DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS;
+    }
+
+    /** A builder for {@link TimeZoneProviderStatus}. */
+    public static final class Builder {
+
+        private @DependencyStatus int mLocationDetectionDependencyStatus =
+                DEPENDENCY_STATUS_UNKNOWN;
+        private @DependencyStatus int mConnectivityDependencyStatus = DEPENDENCY_STATUS_UNKNOWN;
+        private @OperationStatus int mTimeZoneResolutionOperationStatus = OPERATION_STATUS_UNKNOWN;
+
+        /**
+         * Creates a new builder instance. At creation time all status properties are set to
+         * their "UNKNOWN" value.
+         */
+        public Builder() {
+        }
+
+        /**
+         * @hide
+         */
+        public Builder(TimeZoneProviderStatus toCopy) {
+            mLocationDetectionDependencyStatus = toCopy.mLocationDetectionDependencyStatus;
+            mConnectivityDependencyStatus = toCopy.mConnectivityDependencyStatus;
+            mTimeZoneResolutionOperationStatus = toCopy.mTimeZoneResolutionOperationStatus;
+        }
+
+        /**
+         * Sets the status of the provider's location detection dependency (where applicable).
+         * See the {@code DEPENDENCY_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setLocationDetectionDependencyStatus(
+                @DependencyStatus int locationDetectionStatus) {
+            mLocationDetectionDependencyStatus = locationDetectionStatus;
+            return this;
+        }
+
+        /**
+         * Sets the status of the provider's connectivity dependency (where applicable).
+         * See the {@code DEPENDENCY_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setConnectivityDependencyStatus(@DependencyStatus int connectivityStatus) {
+            mConnectivityDependencyStatus = connectivityStatus;
+            return this;
+        }
+
+        /**
+         * Sets the status of the provider's time zone resolution operation.
+         * See the {@code OPERATION_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setTimeZoneResolutionOperationStatus(
+                @OperationStatus int timeZoneResolutionStatus) {
+            mTimeZoneResolutionOperationStatus = timeZoneResolutionStatus;
+            return this;
+        }
+
+        /**
+         * Builds a {@link TimeZoneProviderStatus} instance.
+         */
+        @NonNull
+        public TimeZoneProviderStatus build() {
+            return new TimeZoneProviderStatus(
+                    requireValidDependencyStatus(mLocationDetectionDependencyStatus),
+                    requireValidDependencyStatus(mConnectivityDependencyStatus),
+                    requireValidOperationStatus(mTimeZoneResolutionOperationStatus));
+        }
+    }
+
+    private static @OperationStatus int requireValidOperationStatus(
+            @OperationStatus int operationStatus) {
+        if (operationStatus < OPERATION_STATUS_UNKNOWN
+                || operationStatus > OPERATION_STATUS_FAILED) {
+            throw new IllegalArgumentException(Integer.toString(operationStatus));
+        }
+        return operationStatus;
+    }
+
+    /** @hide */
+    @NonNull
+    public static String operationStatusToString(@OperationStatus int operationStatus) {
+        switch (operationStatus) {
+            case OPERATION_STATUS_UNKNOWN:
+                return "UNKNOWN";
+            case OPERATION_STATUS_NOT_APPLICABLE:
+                return "NOT_APPLICABLE";
+            case OPERATION_STATUS_OK:
+                return "OK";
+            case OPERATION_STATUS_FAILED:
+                return "FAILED";
+            default:
+                throw new IllegalArgumentException("Unknown status: " + operationStatus);
+        }
+    }
+
+    /** @hide */
+    public static @OperationStatus int operationStatusFromString(
+            @Nullable String operationStatusString) {
+
+        if (TextUtils.isEmpty(operationStatusString)) {
+            throw new IllegalArgumentException("Empty status: " + operationStatusString);
+        }
+
+        switch (operationStatusString) {
+            case "UNKNOWN":
+                return OPERATION_STATUS_UNKNOWN;
+            case "NOT_APPLICABLE":
+                return OPERATION_STATUS_NOT_APPLICABLE;
+            case "OK":
+                return OPERATION_STATUS_OK;
+            case "FAILED":
+                return OPERATION_STATUS_FAILED;
+            default:
+                throw new IllegalArgumentException("Unknown status: " + operationStatusString);
+        }
+    }
+
+    private static @DependencyStatus int requireValidDependencyStatus(
+            @DependencyStatus int dependencyStatus) {
+        if (dependencyStatus < DEPENDENCY_STATUS_UNKNOWN
+                || dependencyStatus > DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS) {
+            throw new IllegalArgumentException(Integer.toString(dependencyStatus));
+        }
+        return dependencyStatus;
+    }
+
+    /** @hide */
+    @NonNull
+    public static String dependencyStatusToString(@DependencyStatus int dependencyStatus) {
+        switch (dependencyStatus) {
+            case DEPENDENCY_STATUS_UNKNOWN:
+                return "UNKNOWN";
+            case DEPENDENCY_STATUS_NOT_APPLICABLE:
+                return "NOT_APPLICABLE";
+            case DEPENDENCY_STATUS_OK:
+                return "OK";
+            case DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE:
+                return "TEMPORARILY_UNAVAILABLE";
+            case DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT:
+                return "BLOCKED_BY_ENVIRONMENT";
+            case DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS:
+                return "DEGRADED_BY_SETTINGS";
+            case DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS:
+                return "BLOCKED_BY_SETTINGS";
+            default:
+                throw new IllegalArgumentException("Unknown status: " + dependencyStatus);
+        }
+    }
+
+    /** @hide */
+    public static @DependencyStatus int dependencyStatusFromString(
+            @Nullable String dependencyStatusString) {
+
+        if (TextUtils.isEmpty(dependencyStatusString)) {
+            throw new IllegalArgumentException("Empty status: " + dependencyStatusString);
+        }
+
+        switch (dependencyStatusString) {
+            case "UNKNOWN":
+                return DEPENDENCY_STATUS_UNKNOWN;
+            case "NOT_APPLICABLE":
+                return DEPENDENCY_STATUS_NOT_APPLICABLE;
+            case "OK":
+                return DEPENDENCY_STATUS_OK;
+            case "TEMPORARILY_UNAVAILABLE":
+                return DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE;
+            case "BLOCKED_BY_ENVIRONMENT":
+                return DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT;
+            case "DEGRADED_BY_SETTINGS":
+                return DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS;
+            case "BLOCKED_BY_SETTINGS":
+                return DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS;
+            default:
+                throw new IllegalArgumentException(
+                        "Unknown status: " + dependencyStatusString);
+        }
+    }
+}
diff --git a/core/java/android/service/voice/HotwordAudioStream.java b/core/java/android/service/voice/HotwordAudioStream.java
index 18375ad..bf8ee47 100644
--- a/core/java/android/service/voice/HotwordAudioStream.java
+++ b/core/java/android/service/voice/HotwordAudioStream.java
@@ -57,10 +57,10 @@
      * the audio until the stream is shutdown by the {@link HotwordDetectionService}.
      */
     @NonNull
-    private final ParcelFileDescriptor mAudioStream;
+    private final ParcelFileDescriptor mAudioStreamParcelFileDescriptor;
 
     /**
-     * The timestamp when the {@link #getAudioStream()} was captured by the Audio platform.
+     * The timestamp when the audio stream was captured by the Audio platform.
      *
      * <p>
      * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying
@@ -74,6 +74,8 @@
      * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to
      * timestamps.
      * </p>
+     *
+     * @see #getAudioStreamParcelFileDescriptor()
      */
     @Nullable
     private final AudioTimestamp mTimestamp;
@@ -125,6 +127,16 @@
         }
     }
 
+    /**
+     * Provides an instance of {@link Builder} with state corresponding to this instance.
+     * @hide
+     */
+    public Builder buildUpon() {
+        return new Builder(mAudioFormat, mAudioStreamParcelFileDescriptor)
+            .setTimestamp(mTimestamp)
+            .setMetadata(mMetadata);
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -143,15 +155,15 @@
     @DataClass.Generated.Member
     /* package-private */ HotwordAudioStream(
             @NonNull AudioFormat audioFormat,
-            @NonNull ParcelFileDescriptor audioStream,
+            @NonNull ParcelFileDescriptor audioStreamParcelFileDescriptor,
             @Nullable AudioTimestamp timestamp,
             @NonNull PersistableBundle metadata) {
         this.mAudioFormat = audioFormat;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mAudioFormat);
-        this.mAudioStream = audioStream;
+        this.mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor;
         com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mAudioStream);
+                NonNull.class, null, mAudioStreamParcelFileDescriptor);
         this.mTimestamp = timestamp;
         this.mMetadata = metadata;
         com.android.internal.util.AnnotationValidations.validate(
@@ -173,12 +185,12 @@
      * the audio until the stream is shutdown by the {@link HotwordDetectionService}.
      */
     @DataClass.Generated.Member
-    public @NonNull ParcelFileDescriptor getAudioStream() {
-        return mAudioStream;
+    public @NonNull ParcelFileDescriptor getAudioStreamParcelFileDescriptor() {
+        return mAudioStreamParcelFileDescriptor;
     }
 
     /**
-     * The timestamp when the {@link #getAudioStream()} was captured by the Audio platform.
+     * The timestamp when the audio stream was captured by the Audio platform.
      *
      * <p>
      * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying
@@ -192,6 +204,8 @@
      * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to
      * timestamps.
      * </p>
+     *
+     * @see #getAudioStreamParcelFileDescriptor()
      */
     @DataClass.Generated.Member
     public @Nullable AudioTimestamp getTimestamp() {
@@ -214,7 +228,7 @@
 
         return "HotwordAudioStream { " +
                 "audioFormat = " + mAudioFormat + ", " +
-                "audioStream = " + mAudioStream + ", " +
+                "audioStreamParcelFileDescriptor = " + mAudioStreamParcelFileDescriptor + ", " +
                 "timestamp = " + timestampToString() + ", " +
                 "metadata = " + mMetadata +
         " }";
@@ -234,7 +248,7 @@
         //noinspection PointlessBooleanExpression
         return true
                 && Objects.equals(mAudioFormat, that.mAudioFormat)
-                && Objects.equals(mAudioStream, that.mAudioStream)
+                && Objects.equals(mAudioStreamParcelFileDescriptor, that.mAudioStreamParcelFileDescriptor)
                 && Objects.equals(mTimestamp, that.mTimestamp)
                 && Objects.equals(mMetadata, that.mMetadata);
     }
@@ -247,7 +261,7 @@
 
         int _hash = 1;
         _hash = 31 * _hash + Objects.hashCode(mAudioFormat);
-        _hash = 31 * _hash + Objects.hashCode(mAudioStream);
+        _hash = 31 * _hash + Objects.hashCode(mAudioStreamParcelFileDescriptor);
         _hash = 31 * _hash + Objects.hashCode(mTimestamp);
         _hash = 31 * _hash + Objects.hashCode(mMetadata);
         return _hash;
@@ -263,7 +277,7 @@
         if (mTimestamp != null) flg |= 0x4;
         dest.writeByte(flg);
         dest.writeTypedObject(mAudioFormat, flags);
-        dest.writeTypedObject(mAudioStream, flags);
+        dest.writeTypedObject(mAudioStreamParcelFileDescriptor, flags);
         parcelTimestamp(dest, flags);
         dest.writeTypedObject(mMetadata, flags);
     }
@@ -281,16 +295,16 @@
 
         byte flg = in.readByte();
         AudioFormat audioFormat = (AudioFormat) in.readTypedObject(AudioFormat.CREATOR);
-        ParcelFileDescriptor audioStream = (ParcelFileDescriptor) in.readTypedObject(ParcelFileDescriptor.CREATOR);
+        ParcelFileDescriptor audioStreamParcelFileDescriptor = (ParcelFileDescriptor) in.readTypedObject(ParcelFileDescriptor.CREATOR);
         AudioTimestamp timestamp = unparcelTimestamp(in);
         PersistableBundle metadata = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR);
 
         this.mAudioFormat = audioFormat;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, mAudioFormat);
-        this.mAudioStream = audioStream;
+        this.mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor;
         com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mAudioStream);
+                NonNull.class, null, mAudioStreamParcelFileDescriptor);
         this.mTimestamp = timestamp;
         this.mMetadata = metadata;
         com.android.internal.util.AnnotationValidations.validate(
@@ -321,7 +335,7 @@
     public static final class Builder {
 
         private @NonNull AudioFormat mAudioFormat;
-        private @NonNull ParcelFileDescriptor mAudioStream;
+        private @NonNull ParcelFileDescriptor mAudioStreamParcelFileDescriptor;
         private @Nullable AudioTimestamp mTimestamp;
         private @NonNull PersistableBundle mMetadata;
 
@@ -332,19 +346,19 @@
          *
          * @param audioFormat
          *   The {@link AudioFormat} of the audio stream.
-         * @param audioStream
+         * @param audioStreamParcelFileDescriptor
          *   This stream starts with the audio bytes used for hotword detection, but continues streaming
          *   the audio until the stream is shutdown by the {@link HotwordDetectionService}.
          */
         public Builder(
                 @NonNull AudioFormat audioFormat,
-                @NonNull ParcelFileDescriptor audioStream) {
+                @NonNull ParcelFileDescriptor audioStreamParcelFileDescriptor) {
             mAudioFormat = audioFormat;
             com.android.internal.util.AnnotationValidations.validate(
                     NonNull.class, null, mAudioFormat);
-            mAudioStream = audioStream;
+            mAudioStreamParcelFileDescriptor = audioStreamParcelFileDescriptor;
             com.android.internal.util.AnnotationValidations.validate(
-                    NonNull.class, null, mAudioStream);
+                    NonNull.class, null, mAudioStreamParcelFileDescriptor);
         }
 
         /**
@@ -363,15 +377,15 @@
          * the audio until the stream is shutdown by the {@link HotwordDetectionService}.
          */
         @DataClass.Generated.Member
-        public @NonNull Builder setAudioStream(@NonNull ParcelFileDescriptor value) {
+        public @NonNull Builder setAudioStreamParcelFileDescriptor(@NonNull ParcelFileDescriptor value) {
             checkNotUsed();
             mBuilderFieldsSet |= 0x2;
-            mAudioStream = value;
+            mAudioStreamParcelFileDescriptor = value;
             return this;
         }
 
         /**
-         * The timestamp when the {@link #getAudioStream()} was captured by the Audio platform.
+         * The timestamp when the audio stream was captured by the Audio platform.
          *
          * <p>
          * The {@link HotwordDetectionService} egressing the audio is the owner of the underlying
@@ -385,6 +399,8 @@
          * {@link HotwordDetectedResult#getHotwordDurationMillis()} to translate these durations to
          * timestamps.
          * </p>
+         *
+         * @see #getAudioStreamParcelFileDescriptor()
          */
         @DataClass.Generated.Member
         public @NonNull Builder setTimestamp(@NonNull AudioTimestamp value) {
@@ -418,7 +434,7 @@
             }
             HotwordAudioStream o = new HotwordAudioStream(
                     mAudioFormat,
-                    mAudioStream,
+                    mAudioStreamParcelFileDescriptor,
                     mTimestamp,
                     mMetadata);
             return o;
@@ -433,10 +449,10 @@
     }
 
     @DataClass.Generated(
-            time = 1665463434564L,
+            time = 1666342101364L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/service/voice/HotwordAudioStream.java",
-            inputSignatures = "private final @android.annotation.NonNull android.media.AudioFormat mAudioFormat\nprivate final @android.annotation.NonNull android.os.ParcelFileDescriptor mAudioStream\nprivate final @android.annotation.Nullable android.media.AudioTimestamp mTimestamp\nprivate final @android.annotation.NonNull android.os.PersistableBundle mMetadata\nprivate static  android.media.AudioTimestamp defaultTimestamp()\nprivate static  android.os.PersistableBundle defaultMetadata()\nprivate  java.lang.String timestampToString()\nprivate  void parcelTimestamp(android.os.Parcel,int)\nprivate static @android.annotation.Nullable android.media.AudioTimestamp unparcelTimestamp(android.os.Parcel)\nclass HotwordAudioStream extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genParcelable=true, genToString=true)")
+            inputSignatures = "private final @android.annotation.NonNull android.media.AudioFormat mAudioFormat\nprivate final @android.annotation.NonNull android.os.ParcelFileDescriptor mAudioStreamParcelFileDescriptor\nprivate final @android.annotation.Nullable android.media.AudioTimestamp mTimestamp\nprivate final @android.annotation.NonNull android.os.PersistableBundle mMetadata\nprivate static  android.media.AudioTimestamp defaultTimestamp()\nprivate static  android.os.PersistableBundle defaultMetadata()\nprivate  java.lang.String timestampToString()\nprivate  void parcelTimestamp(android.os.Parcel,int)\nprivate static @android.annotation.Nullable android.media.AudioTimestamp unparcelTimestamp(android.os.Parcel)\npublic  android.service.voice.HotwordAudioStream.Builder buildUpon()\nclass HotwordAudioStream extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genParcelable=true, genToString=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/service/voice/HotwordDetectedResult.java b/core/java/android/service/voice/HotwordDetectedResult.java
index 6255d00..dee560b 100644
--- a/core/java/android/service/voice/HotwordDetectedResult.java
+++ b/core/java/android/service/voice/HotwordDetectedResult.java
@@ -31,7 +31,10 @@
 import com.android.internal.util.DataClass;
 import com.android.internal.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
@@ -98,14 +101,30 @@
     private static final int LIMIT_AUDIO_CHANNEL_MAX_VALUE = 63;
 
     /**
-     * The bundle key for proximity value
+     * The bundle key for proximity
      *
      * TODO(b/238896013): Move the proximity logic out of bundle to proper API.
-     *
-     * @hide
      */
-    public static final String EXTRA_PROXIMITY_METERS =
-            "android.service.voice.extra.PROXIMITY_METERS";
+    private static final String EXTRA_PROXIMITY =
+            "android.service.voice.extra.PROXIMITY";
+
+    /** Users’ proximity is unknown (proximity sensing was inconclusive and is unsupported). */
+    public static final int PROXIMITY_UNKNOWN = -1;
+
+    /** Proximity value that represents that the object is near. */
+    public static final int PROXIMITY_NEAR = 1;
+
+    /** Proximity value that represents that the object is far. */
+    public static final int PROXIMITY_FAR = 2;
+
+    /** @hide */
+    @IntDef(prefix = {"PROXIMITY"}, value = {
+            PROXIMITY_UNKNOWN,
+            PROXIMITY_NEAR,
+            PROXIMITY_FAR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProximityValue {}
 
     /** Confidence level in the trigger outcome. */
     @HotwordConfidenceLevelValue
@@ -203,7 +222,7 @@
     @NonNull
     private final List<HotwordAudioStream> mAudioStreams;
     private static List<HotwordAudioStream> defaultAudioStreams() {
-        return new ArrayList<>();
+        return Collections.emptyList();
     }
 
     /**
@@ -219,12 +238,14 @@
      * versions of Android.
      *
      * <p>After the trigger happens, a special case of proximity-related extra, with the key of
-     * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
-     * will be stored to enable proximity logic. The proximity meters is provided by the system,
-     * on devices that support detecting proximity of nearby users, to help disambiguate which
-     * nearby device should respond. When the proximity is unknown, the proximity value will not
-     * be stored. This mapping will be excluded from the max bundle size calculation because this
-     * mapping is included after the result is returned from the hotword detector service.
+     * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+     * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+     * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+     * proximity. The proximity value is provided by the system, on devices that support detecting
+     * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+     * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+     * from the max bundle size calculation because this mapping is included after the result is
+     * returned from the hotword detector service.
      *
      * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
      * that can be used to communicate with other processes.
@@ -347,16 +368,16 @@
             // Remove the proximity key from the bundle before checking the bundle size. The
             // proximity value is added after the privileged module and can avoid the
             // maxBundleSize limitation.
-            if (mExtras.containsKey(EXTRA_PROXIMITY_METERS)) {
-                double proximityMeters = mExtras.getDouble(EXTRA_PROXIMITY_METERS);
-                mExtras.remove(EXTRA_PROXIMITY_METERS);
+            if (mExtras.containsKey(EXTRA_PROXIMITY)) {
+                int proximityValue = mExtras.getInt(EXTRA_PROXIMITY);
+                mExtras.remove(EXTRA_PROXIMITY);
                 // Skip checking parcelable size if the new bundle size is 0. Newly empty bundle
                 // has parcelable size of 4, but the default bundle has parcelable size of 0.
                 if (mExtras.size() > 0) {
                     Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
                             getMaxBundleSize(), "extras");
                 }
-                mExtras.putDouble(EXTRA_PROXIMITY_METERS, proximityMeters);
+                mExtras.putInt(EXTRA_PROXIMITY, proximityValue);
             } else {
                 Preconditions.checkArgumentInRange(getParcelableSize(mExtras), 0,
                         getMaxBundleSize(), "extras");
@@ -364,9 +385,92 @@
         }
     }
 
+    /**
+     * The list of the audio streams containing audio bytes that were used for hotword detection.
+     */
+    public @NonNull List<HotwordAudioStream> getAudioStreams() {
+        return List.copyOf(mAudioStreams);
+    }
+
+    /**
+     * Adds proximity level, either near or far, that is mapped for the given distance into
+     * the bundle. The proximity value is provided by the system, on devices that support detecting
+     * proximity of nearby users, to help disambiguate which nearby device should respond.
+     * This mapping will be excluded from the max bundle size calculation because this mapping is
+     * included after the result is returned from the hotword detector service. The value will not
+     * be included if the proximity was unknown.
+     *
+     * @hide
+     */
+    public void setProximity(double distance) {
+        int proximityLevel = convertToProximityLevel(distance);
+        if (proximityLevel != PROXIMITY_UNKNOWN) {
+            mExtras.putInt(EXTRA_PROXIMITY, proximityLevel);
+        }
+    }
+
+    /**
+     * Returns proximity level, which can be either of {@link HotwordDetectedResult#PROXIMITY_NEAR}
+     * or {@link HotwordDetectedResult#PROXIMITY_FAR}. If the proximity is unknown, it will
+     * return {@link HotwordDetectedResult#PROXIMITY_UNKNOWN}.
+     */
+    @ProximityValue
+    public int getProximity() {
+        return mExtras.getInt(EXTRA_PROXIMITY, PROXIMITY_UNKNOWN);
+    }
+
+    /**
+     * Mapping of the proximity distance (meters) to proximity values, unknown, near, and far.
+     * Currently, this mapping is handled by HotwordDetectedResult because it handles just
+     * HotwordDetectionConnection which we know the mapping of. However, the mapping will need to
+     * move to a more centralized place once there are more clients.
+     *
+     * TODO(b/258531144): Move the proximity mapping to a central location
+     */
+    @ProximityValue
+    private int convertToProximityLevel(double distance) {
+        if (distance < 0) {
+            return PROXIMITY_UNKNOWN;
+        } else if (distance <= 3) {
+            return PROXIMITY_NEAR;
+        } else {
+            return PROXIMITY_FAR;
+        }
+    }
+
     @DataClass.Suppress("addAudioStreams")
     abstract static class BaseBuilder {
+        /**
+         * The list of the audio streams containing audio bytes that were used for hotword
+         * detection.
+         */
+        public @NonNull Builder setAudioStreams(@NonNull List<HotwordAudioStream> value) {
+            Objects.requireNonNull(value, "value should not be null");
+            final Builder builder = (Builder) this;
+            // If the code gen flag in build() is changed, we must update the flag e.g. 0x200 here.
+            builder.mBuilderFieldsSet |= 0x200;
+            builder.mAudioStreams = List.copyOf(value);
+            return builder;
+        }
+    }
 
+    /**
+     * Provides an instance of {@link Builder} with state corresponding to this instance.
+     * @hide
+     */
+    public Builder buildUpon() {
+        return new Builder()
+            .setConfidenceLevel(mConfidenceLevel)
+            .setMediaSyncEvent(mMediaSyncEvent)
+            .setHotwordOffsetMillis(mHotwordOffsetMillis)
+            .setHotwordDurationMillis(mHotwordDurationMillis)
+            .setAudioChannel(mAudioChannel)
+            .setHotwordDetectionPersonalized(mHotwordDetectionPersonalized)
+            .setScore(mScore)
+            .setPersonalizedScore(mPersonalizedScore)
+            .setHotwordPhraseId(mHotwordPhraseId)
+            .setAudioStreams(mAudioStreams)
+            .setExtras(mExtras);
     }
 
 
@@ -394,7 +498,7 @@
         CONFIDENCE_LEVEL_HIGH,
         CONFIDENCE_LEVEL_VERY_HIGH
     })
-    @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    @Retention(RetentionPolicy.SOURCE)
     @DataClass.Generated.Member
     public @interface ConfidenceLevel {}
 
@@ -425,7 +529,7 @@
         LIMIT_HOTWORD_OFFSET_MAX_VALUE,
         LIMIT_AUDIO_CHANNEL_MAX_VALUE
     })
-    @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+    @Retention(RetentionPolicy.SOURCE)
     @DataClass.Generated.Member
     /* package-private */ @interface Limit {}
 
@@ -441,6 +545,30 @@
         }
     }
 
+    /** @hide */
+    @IntDef(prefix = "PROXIMITY_", value = {
+        PROXIMITY_UNKNOWN,
+        PROXIMITY_NEAR,
+        PROXIMITY_FAR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @DataClass.Generated.Member
+    public @interface Proximity {}
+
+    /** @hide */
+    @DataClass.Generated.Member
+    public static String proximityToString(@Proximity int value) {
+        switch (value) {
+            case PROXIMITY_UNKNOWN:
+                    return "PROXIMITY_UNKNOWN";
+            case PROXIMITY_NEAR:
+                    return "PROXIMITY_NEAR";
+            case PROXIMITY_FAR:
+                    return "PROXIMITY_FAR";
+            default: return Integer.toHexString(value);
+        }
+    }
+
     @DataClass.Generated.Member
     /* package-private */ HotwordDetectedResult(
             @HotwordConfidenceLevelValue int confidenceLevel,
@@ -555,14 +683,6 @@
     }
 
     /**
-     * The list of the audio streams containing audio bytes that were used for hotword detection.
-     */
-    @DataClass.Generated.Member
-    public @NonNull List<HotwordAudioStream> getAudioStreams() {
-        return mAudioStreams;
-    }
-
-    /**
      * App-specific extras to support trigger.
      *
      * <p>The size of this bundle will be limited to {@link #getMaxBundleSize}. Results will larger
@@ -575,12 +695,14 @@
      * versions of Android.
      *
      * <p>After the trigger happens, a special case of proximity-related extra, with the key of
-     * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
-     * will be stored to enable proximity logic. The proximity meters is provided by the system,
-     * on devices that support detecting proximity of nearby users, to help disambiguate which
-     * nearby device should respond. When the proximity is unknown, the proximity value will not
-     * be stored. This mapping will be excluded from the max bundle size calculation because this
-     * mapping is included after the result is returned from the hotword detector service.
+     * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+     * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+     * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+     * proximity. The proximity value is provided by the system, on devices that support detecting
+     * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+     * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+     * from the max bundle size calculation because this mapping is included after the result is
+     * returned from the hotword detector service.
      *
      * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
      * that can be used to communicate with other processes.
@@ -881,17 +1003,6 @@
         }
 
         /**
-         * The list of the audio streams containing audio bytes that were used for hotword detection.
-         */
-        @DataClass.Generated.Member
-        public @NonNull Builder setAudioStreams(@NonNull List<HotwordAudioStream> value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x200;
-            mAudioStreams = value;
-            return this;
-        }
-
-        /**
          * App-specific extras to support trigger.
          *
          * <p>The size of this bundle will be limited to {@link #getMaxBundleSize}. Results will larger
@@ -904,12 +1015,14 @@
          * versions of Android.
          *
          * <p>After the trigger happens, a special case of proximity-related extra, with the key of
-         * 'android.service.voice.extra.PROXIMITY_METERS' and the value of distance in meters (double),
-         * will be stored to enable proximity logic. The proximity meters is provided by the system,
-         * on devices that support detecting proximity of nearby users, to help disambiguate which
-         * nearby device should respond. When the proximity is unknown, the proximity value will not
-         * be stored. This mapping will be excluded from the max bundle size calculation because this
-         * mapping is included after the result is returned from the hotword detector service.
+         * 'android.service.voice.extra.PROXIMITY_VALUE' and the value of proximity value (integer)
+         * will be stored to enable proximity logic. {@link HotwordDetectedResult#PROXIMITY_NEAR} will
+         * indicate 'NEAR' proximity and {@link HotwordDetectedResult#PROXIMITY_FAR} will indicate 'FAR'
+         * proximity. The proximity value is provided by the system, on devices that support detecting
+         * proximity of nearby users, to help disambiguate which nearby device should respond. When the
+         * proximity is unknown, the proximity value will not be stored. This mapping will be excluded
+         * from the max bundle size calculation because this mapping is included after the result is
+         * returned from the hotword detector service.
          *
          * <p>This is a PersistableBundle so it doesn't allow any remotable objects or other contents
          * that can be used to communicate with other processes.
@@ -984,10 +1097,10 @@
     }
 
     @DataClass.Generated(
-            time = 1664876310951L,
+            time = 1668385264834L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/service/voice/HotwordDetectedResult.java",
-            inputSignatures = "public static final  int CONFIDENCE_LEVEL_NONE\npublic static final  int CONFIDENCE_LEVEL_LOW\npublic static final  int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final  int CONFIDENCE_LEVEL_HIGH\npublic static final  int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final  int HOTWORD_OFFSET_UNSET\npublic static final  int AUDIO_CHANNEL_UNSET\nprivate static final  int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final  int LIMIT_AUDIO_CHANNEL_MAX_VALUE\npublic static final  java.lang.String EXTRA_PROXIMITY_METERS\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate  int mHotwordOffsetMillis\nprivate  int mHotwordDurationMillis\nprivate  int mAudioChannel\nprivate  boolean mHotwordDetectionPersonalized\nprivate final  int mScore\nprivate final  int mPersonalizedScore\nprivate final  int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static  int sMaxBundleSize\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultScore()\nprivate static  int defaultPersonalizedScore()\npublic static  int getMaxScore()\nprivate static  int defaultHotwordPhraseId()\npublic static  int getMaxHotwordPhraseId()\nprivate static  java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static  android.os.PersistableBundle defaultExtras()\npublic static  int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static  int getParcelableSize(android.os.Parcelable)\npublic static  int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static  int bitCount(long)\nprivate  void onConstructed()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\nclass BaseBuilder extends java.lang.Object implements []")
+            inputSignatures = "public static final  int CONFIDENCE_LEVEL_NONE\npublic static final  int CONFIDENCE_LEVEL_LOW\npublic static final  int CONFIDENCE_LEVEL_LOW_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM\npublic static final  int CONFIDENCE_LEVEL_MEDIUM_HIGH\npublic static final  int CONFIDENCE_LEVEL_HIGH\npublic static final  int CONFIDENCE_LEVEL_VERY_HIGH\npublic static final  int HOTWORD_OFFSET_UNSET\npublic static final  int AUDIO_CHANNEL_UNSET\nprivate static final  int LIMIT_HOTWORD_OFFSET_MAX_VALUE\nprivate static final  int LIMIT_AUDIO_CHANNEL_MAX_VALUE\nprivate static final  java.lang.String EXTRA_PROXIMITY\npublic static final  int PROXIMITY_UNKNOWN\npublic static final  int PROXIMITY_NEAR\npublic static final  int PROXIMITY_FAR\nprivate final @android.service.voice.HotwordDetectedResult.HotwordConfidenceLevelValue int mConfidenceLevel\nprivate @android.annotation.Nullable android.media.MediaSyncEvent mMediaSyncEvent\nprivate  int mHotwordOffsetMillis\nprivate  int mHotwordDurationMillis\nprivate  int mAudioChannel\nprivate  boolean mHotwordDetectionPersonalized\nprivate final  int mScore\nprivate final  int mPersonalizedScore\nprivate final  int mHotwordPhraseId\nprivate final @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> mAudioStreams\nprivate final @android.annotation.NonNull android.os.PersistableBundle mExtras\nprivate static  int sMaxBundleSize\nprivate static  int defaultConfidenceLevel()\nprivate static  int defaultScore()\nprivate static  int defaultPersonalizedScore()\npublic static  int getMaxScore()\nprivate static  int defaultHotwordPhraseId()\npublic static  int getMaxHotwordPhraseId()\nprivate static  java.util.List<android.service.voice.HotwordAudioStream> defaultAudioStreams()\nprivate static  android.os.PersistableBundle defaultExtras()\npublic static  int getMaxBundleSize()\npublic @android.annotation.Nullable android.media.MediaSyncEvent getMediaSyncEvent()\npublic static  int getParcelableSize(android.os.Parcelable)\npublic static  int getUsageSize(android.service.voice.HotwordDetectedResult)\nprivate static  int bitCount(long)\nprivate  void onConstructed()\npublic @android.annotation.NonNull java.util.List<android.service.voice.HotwordAudioStream> getAudioStreams()\npublic  void setProximity(double)\npublic @android.service.voice.HotwordDetectedResult.ProximityValue int getProximity()\nprivate @android.service.voice.HotwordDetectedResult.ProximityValue int convertToProximityLevel(double)\npublic  android.service.voice.HotwordDetectedResult.Builder buildUpon()\nclass HotwordDetectedResult extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.NonNull android.service.voice.HotwordDetectedResult.Builder setAudioStreams(java.util.List<android.service.voice.HotwordAudioStream>)\nclass BaseBuilder extends java.lang.Object implements []")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index df69cc0..552a793 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -25,6 +25,7 @@
 import android.annotation.SdkConstant;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.app.Service;
 import android.content.ContentCaptureOptions;
 import android.content.Context;
@@ -90,10 +91,10 @@
     /**
      * Feature flag for Attention Service.
      *
-     * TODO(b/247920386): Add TestApi annotation
      * @hide
      */
-    public static final boolean ENABLE_PROXIMITY_RESULT = false;
+    @TestApi
+    public static final boolean ENABLE_PROXIMITY_RESULT = true;
 
     /**
      * Indicates that the updated status is successful.
diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java
index 1285d1e..7c125c7 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -349,7 +349,7 @@
      * {@link #createAlwaysOnHotwordDetector(String, Locale, PersistableBundle, SharedMemory,
      * AlwaysOnHotwordDetector.Callback)} or {@link #createHotwordDetector(PersistableBundle,
      * SharedMemory, HotwordDetector.Callback)}, call this will throw an
-     * {@link IllegalArgumentException}.
+     * {@link IllegalStateException}.
      *
      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
      * @param locale The locale for which the enrollment needs to be performed.
@@ -385,7 +385,7 @@
      *
      * <p>Note: If there are any active detectors that are created by using
      * {@link #createAlwaysOnHotwordDetector(String, Locale, AlwaysOnHotwordDetector.Callback)},
-     * call this will throw an {@link IllegalArgumentException}.
+     * call this will throw an {@link IllegalStateException}.
      *
      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
      * @param locale The locale for which the enrollment needs to be performed.
@@ -428,13 +428,18 @@
             if (!CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) {
                 // Allow only one concurrent recognition via the APIs.
                 safelyShutdownAllHotwordDetectors();
-            }
-
-            for (HotwordDetector detector : mActiveHotwordDetectors) {
-                if (detector.isUsingHotwordDetectionService() != supportHotwordDetectionService) {
-                    throw new IllegalArgumentException(
-                            "It disallows to create trusted and non-trusted detectors "
-                                    + "at the same time.");
+            } else {
+                for (HotwordDetector detector : mActiveHotwordDetectors) {
+                    if (detector.isUsingHotwordDetectionService()
+                            != supportHotwordDetectionService) {
+                        throw new IllegalStateException(
+                                "It disallows to create trusted and non-trusted detectors "
+                                        + "at the same time.");
+                    } else if (detector instanceof AlwaysOnHotwordDetector) {
+                        throw new IllegalStateException(
+                                "There is already an active AlwaysOnHotwordDetector. "
+                                        + "It must be destroyed to create a new one.");
+                    }
                 }
             }
 
@@ -442,11 +447,7 @@
                     callback, mKeyphraseEnrollmentInfo, mSystemService,
                     getApplicationContext().getApplicationInfo().targetSdkVersion,
                     supportHotwordDetectionService);
-            if (!mActiveHotwordDetectors.add(dspDetector)) {
-                throw new IllegalArgumentException(
-                        "the keyphrase=" + keyphrase + " and locale=" + locale
-                                + " are already used by another always-on detector");
-            }
+            mActiveHotwordDetectors.add(dspDetector);
 
             try {
                 dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed);
@@ -480,7 +481,7 @@
      *
      * <p>Note: If there are any active detectors that are created by using
      * {@link #createAlwaysOnHotwordDetector(String, Locale, AlwaysOnHotwordDetector.Callback)},
-     * call this will throw an {@link IllegalArgumentException}.
+     * call this will throw an {@link IllegalStateException}.
      *
      * @param options Application configuration data to be provided to the
      * {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or
@@ -513,11 +514,11 @@
             } else {
                 for (HotwordDetector detector : mActiveHotwordDetectors) {
                     if (!detector.isUsingHotwordDetectionService()) {
-                        throw new IllegalArgumentException(
+                        throw new IllegalStateException(
                                 "It disallows to create trusted and non-trusted detectors "
                                         + "at the same time.");
                     } else if (detector instanceof SoftwareHotwordDetector) {
-                        throw new IllegalArgumentException(
+                        throw new IllegalStateException(
                                 "There is already an active SoftwareHotwordDetector. "
                                         + "It must be destroyed to create a new one.");
                     }
@@ -527,6 +528,7 @@
             SoftwareHotwordDetector softwareHotwordDetector =
                     new SoftwareHotwordDetector(
                             mSystemService, null, callback);
+            mActiveHotwordDetectors.add(softwareHotwordDetector);
 
             try {
                 softwareHotwordDetector.registerOnDestroyListener(
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index b559161..007478a 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -31,6 +31,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.FloatRange;
+import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
@@ -81,7 +82,6 @@
 import android.view.InputEventReceiver;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.MotionEvent;
 import android.view.PixelCopy;
 import android.view.Surface;
@@ -251,7 +251,6 @@
         final Rect mDispatchedStableInsets = new Rect();
         DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT;
         final InsetsState mInsetsState = new InsetsState();
-        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0];
         final MergedConfiguration mMergedConfiguration = new MergedConfiguration();
         final Bundle mSyncSeqIdBundle = new Bundle();
@@ -284,7 +283,6 @@
         private Display mDisplay;
         private Context mDisplayContext;
         private int mDisplayState;
-        private @Surface.Rotation int mDisplayInstallOrientation;
         private float mWallpaperDimAmount = 0.05f;
         private float mPreviousWallpaperDimAmount = mWallpaperDimAmount;
         private float mDefaultDimAmount = mWallpaperDimAmount;
@@ -578,6 +576,8 @@
          */
         public void reportEngineShown(boolean waitForEngineShown) {
             if (mIWallpaperEngine.mShownReported) return;
+            Trace.beginSection("WPMS.reportEngineShown-" + waitForEngineShown);
+            Log.d(TAG, "reportEngineShown: shouldWait=" + waitForEngineShown);
             if (!waitForEngineShown) {
                 Message message = mCaller.obtainMessage(MSG_REPORT_SHOWN);
                 mCaller.removeMessages(MSG_REPORT_SHOWN);
@@ -589,6 +589,7 @@
                     mCaller.sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(5));
                 }
             }
+            Trace.endSection();
         }
 
         /**
@@ -657,6 +658,7 @@
          * Called once to initialize the engine.  After returning, the
          * engine's surface will be created by the framework.
          */
+        @MainThread
         public void onCreate(SurfaceHolder surfaceHolder) {
         }
 
@@ -665,6 +667,7 @@
          * surface will be destroyed and this Engine object is no longer
          * valid.
          */
+        @MainThread
         public void onDestroy() {
         }
 
@@ -673,6 +676,7 @@
          * hidden.  <em>It is very important that a wallpaper only use
          * CPU while it is visible.</em>.
          */
+        @MainThread
         public void onVisibilityChanged(boolean visible) {
         }
 
@@ -683,6 +687,7 @@
          *
          * @param insets Insets to apply.
          */
+        @MainThread
         public void onApplyWindowInsets(WindowInsets insets) {
         }
 
@@ -693,6 +698,7 @@
          * user is interacting with, so if it is slow you will get fewer
          * move events.
          */
+        @MainThread
         public void onTouchEvent(MotionEvent event) {
         }
 
@@ -702,6 +708,7 @@
          * call to {@link WallpaperManager#setWallpaperOffsets(IBinder, float, float)
          * WallpaperManager.setWallpaperOffsets()}.
          */
+        @MainThread
         public void onOffsetsChanged(float xOffset, float yOffset,
                 float xOffsetStep, float yOffsetStep,
                 int xPixelOffset, int yPixelOffset) {
@@ -724,6 +731,7 @@
          * @return If returning a result, create a Bundle and place the
          * result data in to it.  Otherwise return null.
          */
+        @MainThread
         public Bundle onCommand(String action, int x, int y, int z,
                 Bundle extras, boolean resultRequested) {
             return null;
@@ -742,6 +750,7 @@
          * @hide
          */
         @SystemApi
+        @MainThread
         public void onAmbientModeChanged(boolean inAmbientMode, long animationDuration) {
         }
 
@@ -749,6 +758,7 @@
          * Called when an application has changed the desired virtual size of
          * the wallpaper.
          */
+        @MainThread
         public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) {
         }
 
@@ -756,6 +766,7 @@
          * Convenience for {@link SurfaceHolder.Callback#surfaceChanged
          * SurfaceHolder.Callback.surfaceChanged()}.
          */
+        @MainThread
         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
         }
 
@@ -763,6 +774,7 @@
          * Convenience for {@link SurfaceHolder.Callback2#surfaceRedrawNeeded
          * SurfaceHolder.Callback.surfaceRedrawNeeded()}.
          */
+        @MainThread
         public void onSurfaceRedrawNeeded(SurfaceHolder holder) {
         }
 
@@ -770,6 +782,7 @@
          * Convenience for {@link SurfaceHolder.Callback#surfaceCreated
          * SurfaceHolder.Callback.surfaceCreated()}.
          */
+        @MainThread
         public void onSurfaceCreated(SurfaceHolder holder) {
         }
 
@@ -777,6 +790,7 @@
          * Convenience for {@link SurfaceHolder.Callback#surfaceDestroyed
          * SurfaceHolder.Callback.surfaceDestroyed()}.
          */
+        @MainThread
         public void onSurfaceDestroyed(SurfaceHolder holder) {
         }
 
@@ -787,6 +801,7 @@
          * @param zoom the zoom level, between 0 indicating fully zoomed in and 1 indicating fully
          *             zoomed out.
          */
+        @MainThread
         public void onZoomChanged(@FloatRange(from = 0f, to = 1f) float zoom) {
         }
 
@@ -836,6 +851,7 @@
          *
          * @return Wallpaper colors.
          */
+        @MainThread
         public @Nullable WallpaperColors onComputeColors() {
             return null;
         }
@@ -1134,8 +1150,9 @@
                         InputChannel inputChannel = new InputChannel();
 
                         if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE,
-                                mDisplay.getDisplayId(), mRequestedVisibilities, inputChannel,
-                                mInsetsState, mTempControls, new Rect(), new float[1]) < 0) {
+                                mDisplay.getDisplayId(), WindowInsets.Type.defaultVisible(),
+                                inputChannel, mInsetsState, mTempControls, new Rect(),
+                                new float[1]) < 0) {
                             Log.w(TAG, "Failed to add window while updating wallpaper surface.");
                             return;
                         }
@@ -1159,7 +1176,7 @@
                             mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle);
 
                     final int transformHint = SurfaceControl.rotationToBufferTransform(
-                            (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                            (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
                     mSurfaceControl.setTransformHint(transformHint);
                     WindowLayout.computeSurfaceSize(mLayout, maxBounds, mWidth, mHeight,
                             mWinFrames.frame, false /* dragResizing */, mSurfaceSize);
@@ -1260,7 +1277,9 @@
                             didSurface = true;
                             if (DEBUG) Log.v(TAG, "onSurfaceCreated("
                                     + mSurfaceHolder + "): " + this);
+                            Trace.beginSection("WPMS.Engine.onSurfaceCreated");
                             onSurfaceCreated(mSurfaceHolder);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1286,8 +1305,10 @@
                                     + ", " + mCurWidth + ", " + mCurHeight
                                     + "): " + this);
                             didSurface = true;
+                            Trace.beginSection("WPMS.Engine.onSurfaceChanged");
                             onSurfaceChanged(mSurfaceHolder, mFormat,
                                     mCurWidth, mCurHeight);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1304,11 +1325,15 @@
                             if (DEBUG) {
                                 Log.v(TAG, "dispatching insets=" + windowInsets);
                             }
+                            Trace.beginSection("WPMS.Engine.onApplyWindowInsets");
                             onApplyWindowInsets(windowInsets);
+                            Trace.endSection();
                         }
 
                         if (redrawNeeded) {
+                            Trace.beginSection("WPMS.Engine.onSurfaceRedrawNeeded");
                             onSurfaceRedrawNeeded(mSurfaceHolder);
+                            Trace.endSection();
                             SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                             if (callbacks != null) {
                                 for (SurfaceHolder.Callback c : callbacks) {
@@ -1333,11 +1358,15 @@
                                 // the state to get them to notice.
                                 if (DEBUG) Log.v(TAG, "onVisibilityChanged(true) at surface: "
                                         + this);
+                                Trace.beginSection("WPMS.Engine.onVisibilityChanged-true");
                                 onVisibilityChanged(true);
+                                Trace.endSection();
                             }
                             if (DEBUG) Log.v(TAG, "onVisibilityChanged(false) at surface: "
                                         + this);
+                            Trace.beginSection("WPMS.Engine.onVisibilityChanged-false");
                             onVisibilityChanged(false);
+                            Trace.endSection();
                         }
                     } finally {
                         mIsCreating = false;
@@ -1420,15 +1449,18 @@
             mWallpaperDimAmount = mDefaultDimAmount;
             mPreviousWallpaperDimAmount = mWallpaperDimAmount;
             mDisplayState = mDisplay.getState();
-            mDisplayInstallOrientation = mDisplay.getInstallOrientation();
 
             if (DEBUG) Log.v(TAG, "onCreate(): " + this);
+            Trace.beginSection("WPMS.Engine.onCreate");
             onCreate(mSurfaceHolder);
+            Trace.endSection();
 
             mInitializing = false;
 
             mReportedVisible = false;
+            Trace.beginSection("WPMS.Engine.updateSurface");
             updateSurface(false, false, false);
+            Trace.endSection();
         }
 
         /**
@@ -2238,14 +2270,15 @@
         public void reportShown() {
             if (!mShownReported) {
                 mShownReported = true;
+                Trace.beginSection("WPMS.mConnection.engineShown");
                 try {
                     mConnection.engineShown(this);
                     Log.d(TAG, "Wallpaper has updated the surface:"
                             + mWallpaperManager.getWallpaperInfo());
                 } catch (RemoteException e) {
                     Log.w(TAG, "Wallpaper host disappeared", e);
-                    return;
                 }
+                Trace.endSection();
             }
         }
 
@@ -2287,6 +2320,27 @@
             return mEngine == null ? null : SurfaceControl.mirrorSurface(mEngine.mSurfaceControl);
         }
 
+        private void doAttachEngine() {
+            Trace.beginSection("WPMS.onCreateEngine");
+            Engine engine = onCreateEngine();
+            Trace.endSection();
+            mEngine = engine;
+            Trace.beginSection("WPMS.mConnection.attachEngine-" + mDisplayId);
+            try {
+                mConnection.attachEngine(this, mDisplayId);
+            } catch (RemoteException e) {
+                engine.detach();
+                Log.w(TAG, "Wallpaper host disappeared", e);
+                return;
+            } finally {
+                Trace.endSection();
+            }
+            mActiveEngines.add(engine);
+            Trace.beginSection("WPMS.engine.attach");
+            engine.attach(this);
+            Trace.endSection();
+        }
+
         private void doDetachEngine() {
             mActiveEngines.remove(mEngine);
             mEngine.detach();
@@ -2312,21 +2366,15 @@
             }
             switch (message.what) {
                 case DO_ATTACH: {
-                    Engine engine = onCreateEngine();
-                    mEngine = engine;
-                    try {
-                        mConnection.attachEngine(this, mDisplayId);
-                    } catch (RemoteException e) {
-                        engine.detach();
-                        Log.w(TAG, "Wallpaper host disappeared", e);
-                        return;
-                    }
-                    mActiveEngines.add(engine);
-                    engine.attach(this);
+                    Trace.beginSection("WPMS.DO_ATTACH");
+                    doAttachEngine();
+                    Trace.endSection();
                     return;
                 }
                 case DO_DETACH: {
+                    Trace.beginSection("WPMS.DO_DETACH");
                     doDetachEngine();
+                    Trace.endSection();
                     return;
                 }
                 case DO_SET_DESIRED_SIZE: {
@@ -2407,7 +2455,9 @@
                     }
                 } break;
                 case MSG_REPORT_SHOWN: {
+                    Trace.beginSection("WPMS.MSG_REPORT_SHOWN");
                     reportShown();
+                    Trace.endSection();
                 } break;
                 default :
                     Log.w(TAG, "Unknown message type " + message.what);
@@ -2431,8 +2481,10 @@
         public void attach(IWallpaperConnection conn, IBinder windowToken,
                 int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding,
                 int displayId, @SetWallpaperFlags int which) {
+            Trace.beginSection("WPMS.ServiceWrapper.attach");
             mEngineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken,
                     windowType, isPreview, reqWidth, reqHeight, padding, displayId);
+            Trace.endSection();
         }
 
         @Override
@@ -2443,16 +2495,20 @@
 
     @Override
     public void onCreate() {
+        Trace.beginSection("WPMS.onCreate");
         super.onCreate();
+        Trace.endSection();
     }
 
     @Override
     public void onDestroy() {
+        Trace.beginSection("WPMS.onDestroy");
         super.onDestroy();
         for (int i=0; i<mActiveEngines.size(); i++) {
             mActiveEngines.get(i).detach();
         }
         mActiveEngines.clear();
+        Trace.endSection();
     }
 
     /**
@@ -2470,6 +2526,7 @@
      * when the wallpaper is currently set as the active wallpaper and the user
      * is in the wallpaper picker viewing a preview of it as well.
      */
+    @MainThread
     public abstract Engine onCreateEngine();
 
     @Override
diff --git a/core/java/android/text/GraphemeClusterSegmentFinder.java b/core/java/android/text/GraphemeClusterSegmentFinder.java
index 3335751..656774f 100644
--- a/core/java/android/text/GraphemeClusterSegmentFinder.java
+++ b/core/java/android/text/GraphemeClusterSegmentFinder.java
@@ -49,6 +49,7 @@
 
     @Override
     public int previousStartBoundary(@IntRange(from = 0) int offset) {
+        if (offset == 0) return DONE;
         int boundary = mTextPaint.getTextRunCursor(
                 mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
         return boundary == -1 ? DONE : boundary;
@@ -56,6 +57,7 @@
 
     @Override
     public int previousEndBoundary(@IntRange(from = 0) int offset) {
+        if (offset == 0) return DONE;
         int boundary = mTextPaint.getTextRunCursor(
                 mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
         // Check that there is another cursor position before, otherwise this is not a valid
@@ -69,6 +71,7 @@
 
     @Override
     public int nextStartBoundary(@IntRange(from = 0) int offset) {
+        if (offset == mText.length()) return DONE;
         int boundary = mTextPaint.getTextRunCursor(
                 mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
         // Check that there is another cursor position after, otherwise this is not a valid
@@ -82,6 +85,7 @@
 
     @Override
     public int nextEndBoundary(@IntRange(from = 0) int offset) {
+        if (offset == mText.length()) return DONE;
         int boundary = mTextPaint.getTextRunCursor(
                 mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
         return boundary == -1 ? DONE : boundary;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 519fc55..54ec07e 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -36,7 +36,6 @@
 import android.text.style.ParagraphStyle;
 import android.text.style.ReplacementSpan;
 import android.text.style.TabStopSpan;
-import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -1859,13 +1858,12 @@
      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
      *     text segment
      * @param inclusionStrategy strategy for determining whether a text segment is inside the
-     *          specified area
-     * @return an integer range where the endpoints are the start (inclusive) and end (exclusive)
-     *     character offsets of the text range, or null if there are no text segments inside the
-     *     area
+     *     specified area
+     * @return int array of size 2 containing the start (inclusive) and end (exclusive) character
+     *     offsets of the text range, or null if there are no text segments inside the area
      */
     @Nullable
-    public Range<Integer> getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
+    public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
             @NonNull TextInclusionStrategy inclusionStrategy) {
         // Find the first line whose bottom (without line spacing) is below the top of the area.
         int startLine = getLineForVertical((int) area.top);
@@ -1923,7 +1921,7 @@
         start = segmentFinder.previousStartBoundary(start + 1);
         end = segmentFinder.nextEndBoundary(end - 1);
 
-        return new Range(start, end);
+        return new int[] {start, end};
     }
 
     /**
@@ -3002,6 +3000,18 @@
         }
 
         /**
+         * Returns the BiDi level of this run.
+         *
+         * @param runIndex the index of the BiDi run
+         * @return the BiDi level of this run.
+         * @hide
+         */
+        @IntRange(from = 0)
+        public int getRunLevel(int runIndex) {
+            return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
+        }
+
+        /**
          * Returns true if the BiDi run is RTL.
          *
          * @param runIndex the index of the BiDi run
diff --git a/core/java/android/text/SegmentFinder.java b/core/java/android/text/SegmentFinder.java
index c21c577..be0094b 100644
--- a/core/java/android/text/SegmentFinder.java
+++ b/core/java/android/text/SegmentFinder.java
@@ -19,6 +19,13 @@
 import android.annotation.IntRange;
 import android.graphics.RectF;
 
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * Finds text segment boundaries within text. Subclasses can implement different types of text
  * segments. Grapheme clusters and words are examples of possible text segments. These are
@@ -63,4 +70,144 @@
      * character offset, or {@code DONE} if there are none.
      */
     public abstract int nextEndBoundary(@IntRange(from = 0) int offset);
+
+    /**
+     * The default {@link SegmentFinder} implementation based on given segment ranges.
+     */
+    public static class DefaultSegmentFinder extends SegmentFinder {
+        private final int[] mSegments;
+
+        /**
+         * Create a SegmentFinder with segments stored in an array, where i-th segment's start is
+         * stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively.
+         *
+         * <p> It is required that segments do not overlap, and are already sorted by their start
+         * indices. </p>
+         * @param segments the array that stores the segment ranges.
+         * @throws IllegalArgumentException if the given segments array's length is not even; the
+         * given segments are not sorted or there are segments overlap with others.
+         */
+        public DefaultSegmentFinder(@NonNull int[] segments) {
+            checkSegmentsValid(segments);
+            mSegments = segments;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int previousStartBoundary(@IntRange(from = 0) int offset) {
+            return findPrevious(offset, /* isStart = */ true);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int previousEndBoundary(@IntRange(from = 0) int offset) {
+            return findPrevious(offset, /* isStart = */ false);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int nextStartBoundary(@IntRange(from = 0) int offset) {
+            return findNext(offset, /* isStart = */ true);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int nextEndBoundary(@IntRange(from = 0) int offset) {
+            return findNext(offset, /* isStart = */ false);
+        }
+
+        private int findNext(int offset, boolean isStart) {
+            if (offset < 0) return DONE;
+            if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE;
+
+            if (offset < mSegments[0]) {
+                return isStart ? mSegments[0] : mSegments[1];
+            }
+
+            int index = Arrays.binarySearch(mSegments, offset);
+            if (index >= 0) {
+                // mSegments may have duplicate elements (The previous segments end equals
+                // to the following segments start.) Move the index forwards since we are searching
+                // for the next segment.
+                if (index + 1 < mSegments.length && mSegments[index + 1] == offset) {
+                    index = index + 1;
+                }
+                // Point the index to the first segment boundary larger than the given offset.
+                index += 1;
+            } else {
+                // binarySearch returns the insertion point, it's the first segment boundary larger
+                // than the given offset.
+                index = -(index + 1);
+            }
+            if (index >= mSegments.length) return DONE;
+
+            //  +---------------------------------------+
+            //  |               | isStart   | isEnd     |
+            //  |---------------+-----------+-----------|
+            //  | indexIsStart  | index     | index + 1 |
+            //  |---------------+-----------+-----------|
+            //  | indexIsEnd    | index + 1 | index     |
+            //  +---------------------------------------+
+            boolean indexIsStart = index % 2 == 0;
+            if (isStart != indexIsStart) {
+                return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE;
+            }
+            return mSegments[index];
+        }
+
+        private int findPrevious(int offset, boolean isStart) {
+            if (mSegments.length < 1 || offset < mSegments[0]) return DONE;
+
+            if (offset > mSegments[mSegments.length - 1]) {
+                return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1];
+            }
+
+            int index = Arrays.binarySearch(mSegments, offset);
+            if (index >= 0) {
+                // mSegments may have duplicate elements (when the previous segments end equal
+                // to the following segments start). Move the index backwards since we are searching
+                // for the previous segment.
+                if (index > 0 && mSegments[index - 1] == offset) {
+                    index = index - 1;
+                }
+                // Point the index to the first segment boundary smaller than the given offset.
+                index -= 1;
+            } else {
+                // binarySearch returns the insertion point, insertionPoint - 1 is the first
+                // segment boundary smaller than the given offset.
+                index = -(index + 1) - 1;
+            }
+            if (index < 0) return DONE;
+
+            //  +---------------------------------------+
+            //  |               | isStart   | isEnd     |
+            //  |---------------+-----------+-----------|
+            //  | indexIsStart  | index     | index - 1 |
+            //  |---------------+-----------+-----------|
+            //  | indexIsEnd    | index - 1 | index     |
+            //  +---------------------------------------+
+            boolean indexIsStart = index % 2 == 0;
+            if (isStart != indexIsStart) {
+                return (index > 0) ? mSegments[index - 1] : DONE;
+            }
+            return mSegments[index];
+        }
+
+        private static void checkSegmentsValid(int[] segments) {
+            Objects.requireNonNull(segments);
+            Preconditions.checkArgument(segments.length % 2 == 0,
+                    "the length of segments must be even");
+            if (segments.length == 0) return;
+            int lastSegmentEnd = Integer.MIN_VALUE;
+            for (int index = 0; index < segments.length; index += 2) {
+                if (segments[index] < lastSegmentEnd) {
+                    throw new IllegalArgumentException("segments can't overlap");
+                }
+                if (segments[index] >= segments[index + 1]) {
+                    throw new IllegalArgumentException("the segment range can't be empty");
+                }
+                lastSegmentEnd = segments[index + 1];
+            }
+        }
+    }
 }
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 596e491..ff66e5f 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -2331,7 +2331,8 @@
         return trimmed;
     }
 
-    private static boolean isNewline(int codePoint) {
+    /** @hide */
+    public static boolean isNewline(int codePoint) {
         int type = Character.getType(codePoint);
         return type == Character.PARAGRAPH_SEPARATOR || type == Character.LINE_SEPARATOR
                 || codePoint == LINE_FEED_CODE_POINT;
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/text/style/AccessibilityURLSpan.java b/core/java/android/text/style/AccessibilityURLSpan.java
index bd81623..e280bdf 100644
--- a/core/java/android/text/style/AccessibilityURLSpan.java
+++ b/core/java/android/text/style/AccessibilityURLSpan.java
@@ -26,6 +26,7 @@
  * It is used to replace URLSpans in {@link AccessibilityNodeInfo#setText(CharSequence)}
  * @hide
  */
+@SuppressWarnings("ParcelableCreator")
 public class AccessibilityURLSpan extends URLSpan implements Parcelable {
     final AccessibilityClickableSpan mAccessibilityClickableSpan;
 
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
index 85b7ae9..d61228b 100644
--- a/core/java/android/text/style/TextAppearanceSpan.java
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -149,7 +149,7 @@
         }
 
         mTextFontWeight = a.getInt(com.android.internal.R.styleable
-                .TextAppearance_textFontWeight, -1);
+                .TextAppearance_textFontWeight, /*defValue*/ FontStyle.FONT_WEIGHT_UNSPECIFIED);
 
         final String localeString = a.getString(com.android.internal.R.styleable
                 .TextAppearance_textLocale);
@@ -215,7 +215,7 @@
         mTextColorLink = linkColor;
         mTypeface = null;
 
-        mTextFontWeight = -1;
+        mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
         mTextLocales = null;
 
         mShadowRadius = 0.0f;
@@ -359,8 +359,8 @@
     }
 
     /**
-     * Returns the text font weight specified by this span, or <code>-1</code>
-     * if it does not specify one.
+     * Returns the text font weight specified by this span, or
+     * <code>FontStyle.FONT_WEIGHT_UNSPECIFIED</code> if it does not specify one.
      */
     public int getTextFontWeight() {
         return mTextFontWeight;
diff --git a/core/java/android/transparency/BinaryTransparencyManager.java b/core/java/android/transparency/BinaryTransparencyManager.java
index 18783f5..f6d7c61 100644
--- a/core/java/android/transparency/BinaryTransparencyManager.java
+++ b/core/java/android/transparency/BinaryTransparencyManager.java
@@ -24,7 +24,7 @@
 
 import com.android.internal.os.IBinaryTransparencyService;
 
-import java.util.Map;
+import java.util.List;
 
 /**
  * BinaryTransparencyManager defines a number of system interfaces that other system apps or
@@ -66,12 +66,15 @@
     }
 
     /**
-     * Returns a map of all installed APEXs consisting of package name to SHA256 hash of the
-     * package.
-     * @return A Map with the following entries: {apex package name : sha256 digest of package}
+     * Gets binary measurements of all installed APEXs, each packed in a Bundle.
+     * @return A List of {@link android.os.Bundle}s with the following keys:
+     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_PACKAGE_INFO}
+     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST_ALGORITHM}
+     *         {@link com.android.server.BinaryTransparencyService#BUNDLE_CONTENT_DIGEST}
      */
+    // TODO(b/259422958): Fix static constants referenced here - should be defined here
     @NonNull
-    public Map getApexInfo() {
+    public List getApexInfo() {
         try {
             Slog.d(TAG, "Calling backend's getApexInfo()");
             return mService.getApexInfo();
diff --git a/core/java/android/util/AndroidException.java b/core/java/android/util/AndroidException.java
index 1345ddf..d1b9d9f 100644
--- a/core/java/android/util/AndroidException.java
+++ b/core/java/android/util/AndroidException.java
@@ -40,5 +40,5 @@
             boolean writableStackTrace) {
         super(message, cause, enableSuppression, writableStackTrace);
     }
-};
+}
 
diff --git a/core/java/android/util/AndroidRuntimeException.java b/core/java/android/util/AndroidRuntimeException.java
index 2b824bf..72c34d8b 100644
--- a/core/java/android/util/AndroidRuntimeException.java
+++ b/core/java/android/util/AndroidRuntimeException.java
@@ -34,5 +34,4 @@
     public AndroidRuntimeException(Exception cause) {
         super(cause);
     }
-};
-
+}
diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
index 3b08c3b..7c83087 100644
--- a/core/java/android/util/CharsetUtils.java
+++ b/core/java/android/util/CharsetUtils.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 
+import com.android.modules.utils.ModifiedUtf8;
+
 import dalvik.annotation.optimization.FastNative;
 
 /**
@@ -30,8 +32,7 @@
  * Callers are cautioned that there is a long-standing ART bug that emits
  * non-standard 4-byte sequences, as described by {@code kUtfUse4ByteSequence}
  * in {@code art/runtime/jni/jni_internal.cc}. If precise modified UTF-8
- * encoding is required, use {@link com.android.internal.util.ModifiedUtf8}
- * instead.
+ * encoding is required, use {@link ModifiedUtf8} instead.
  *
  * @hide
  */
@@ -43,8 +44,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src string value to be encoded
      * @param dest destination byte array to encode into
@@ -66,8 +67,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src string value to be encoded
      * @param srcLen exact length of string to be encoded
@@ -88,8 +89,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src source byte array to decode from
      * @param srcOff offset into source where decoding should begin
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/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index d1f05ec..4afd268 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -103,6 +103,25 @@
     public static final String SETTINGS_NEW_KEYBOARD_UI = "settings_new_keyboard_ui";
 
     /**
+     * Enable new shortcut list UI
+     * @hide
+     */
+    public static final String SETTINGS_NEW_KEYBOARD_SHORTCUT = "settings_new_keyboard_shortcut";
+
+    /**
+     * Enable new modifier key settings UI
+     * @hide
+     */
+    public static final String SETTINGS_NEW_KEYBOARD_MODIFIER_KEY =
+            "settings_new_keyboard_modifier_key";
+
+    /**
+     * Enable new trackpad settings UI
+     * @hide
+     */
+    public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD = "settings_new_keyboard_trackpad";
+
+    /**
      * Enable the new pages which is implemented with SPA.
      * @hide
      */
@@ -113,6 +132,12 @@
      */
     public static final String SETTINGS_ADB_METRICS_WRITER = "settings_adb_metrics_writer";
 
+    /**
+     * Flag to enable/disable biometrics enrollment v2
+     * @hide
+     */
+    public static final String SETTINGS_BIOMETRICS2_ENROLLMENT = "settings_biometrics2_enrollment";
+
     private static final Map<String, String> DEFAULT_FLAGS;
 
     static {
@@ -143,8 +168,12 @@
         DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
         DEFAULT_FLAGS.put(SETTINGS_AUTO_TEXT_WRAPPING, "false");
         DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_UI, "false");
+        DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_SHORTCUT, "false");
+        DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "false");
+        DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "false");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "false");
         DEFAULT_FLAGS.put(SETTINGS_ADB_METRICS_WRITER, "false");
+        DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_ENROLLMENT, "false");
     }
 
     private static final Set<String> PERSISTENT_FLAGS;
@@ -158,6 +187,9 @@
         PERSISTENT_FLAGS.add(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE);
         PERSISTENT_FLAGS.add(SETTINGS_AUTO_TEXT_WRAPPING);
         PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_UI);
+        PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_SHORTCUT);
+        PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
+        PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
     }
 
     /**
diff --git a/core/java/android/util/OWNERS b/core/java/android/util/OWNERS
index d4cf6e6..3772006 100644
--- a/core/java/android/util/OWNERS
+++ b/core/java/android/util/OWNERS
@@ -1,6 +1,6 @@
 per-file Dump* = file:/core/java/com/android/internal/util/dump/OWNERS
 per-file FeatureFlagUtils.java = sbasi@google.com
-per-file FeatureFlagUtils.java = tmfang@google.com
+per-file FeatureFlagUtils.java = edgarwang@google.com
 
 per-file AttributeSet.java = file:/core/java/android/content/res/OWNERS
 per-file TypedValue.java = file:/core/java/android/content/res/OWNERS
diff --git a/core/java/android/util/PackageUtils.java b/core/java/android/util/PackageUtils.java
index 1148120..6f8c5db 100644
--- a/core/java/android/util/PackageUtils.java
+++ b/core/java/android/util/PackageUtils.java
@@ -224,7 +224,7 @@
         byte[] resultBytes = messageDigest.digest();
 
         if (separator == null) {
-            return HexEncoding.encodeToString(resultBytes, true);
+            return HexEncoding.encodeToString(resultBytes, false);
         }
 
         int length = resultBytes.length;
diff --git a/core/java/android/util/Range.java b/core/java/android/util/Range.java
index 9fd0ab9..41c171a 100644
--- a/core/java/android/util/Range.java
+++ b/core/java/android/util/Range.java
@@ -356,4 +356,4 @@
 
     private final T mLower;
     private final T mUpper;
-};
+}
diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java
index 19de396..44318bb 100644
--- a/core/java/android/util/TypedValue.java
+++ b/core/java/android/util/TypedValue.java
@@ -696,5 +696,5 @@
         sb.append("}");
         return sb.toString();
     }
-};
+}
 
diff --git a/core/java/android/util/TypedXmlPullParser.java b/core/java/android/util/TypedXmlPullParser.java
deleted file mode 100644
index aa68bf4..0000000
--- a/core/java/android/util/TypedXmlPullParser.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.util;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-/**
- * Specialization of {@link XmlPullParser} which adds explicit methods to
- * support consistent and efficient conversion of primitive data types.
- *
- * @hide
- */
-public interface TypedXmlPullParser extends XmlPullParser {
-    /**
-     * @return index of requested attribute, otherwise {@code -1} if undefined
-     */
-    default int getAttributeIndex(@Nullable String namespace, @NonNull String name) {
-        final boolean namespaceNull = (namespace == null);
-        final int count = getAttributeCount();
-        for (int i = 0; i < count; i++) {
-            if ((namespaceNull || namespace.equals(getAttributeNamespace(i)))
-                    && name.equals(getAttributeName(i))) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    /**
-     * @return index of requested attribute
-     * @throws XmlPullParserException if the value is undefined
-     */
-    default int getAttributeIndexOrThrow(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) {
-            throw new XmlPullParserException("Missing attribute " + name);
-        } else {
-            return index;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    @NonNull byte[] getAttributeBytesHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    @NonNull byte[] getAttributeBytesBase64(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    int getAttributeInt(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    int getAttributeIntHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    long getAttributeLong(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    long getAttributeLongHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    float getAttributeFloat(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    double getAttributeDouble(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    boolean getAttributeBoolean(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default @NonNull byte[] getAttributeBytesHex(@Nullable String namespace,
-            @NonNull String name) throws XmlPullParserException {
-        return getAttributeBytesHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default @NonNull byte[] getAttributeBytesBase64(@Nullable String namespace,
-            @NonNull String name) throws XmlPullParserException {
-        return getAttributeBytesBase64(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default int getAttributeInt(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeInt(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeIntHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default long getAttributeLong(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeLong(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeLongHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default float getAttributeFloat(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeFloat(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default double getAttributeDouble(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeDouble(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeBoolean(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default @Nullable byte[] getAttributeBytesHex(@Nullable String namespace,
-            @NonNull String name, @Nullable byte[] defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBytesHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default @Nullable byte[] getAttributeBytesBase64(@Nullable String namespace,
-            @NonNull String name, @Nullable byte[] defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBytesBase64(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default int getAttributeInt(@Nullable String namespace, @NonNull String name,
-            int defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeInt(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
-            int defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeIntHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default long getAttributeLong(@Nullable String namespace, @NonNull String name,
-            long defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeLong(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
-            long defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeLongHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
-            float defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeFloat(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
-            double defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeDouble(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
-            boolean defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBoolean(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-}
diff --git a/core/java/android/util/TypedXmlSerializer.java b/core/java/android/util/TypedXmlSerializer.java
deleted file mode 100644
index 3f9eaa8..0000000
--- a/core/java/android/util/TypedXmlSerializer.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.util;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-
-/**
- * Specialization of {@link XmlSerializer} which adds explicit methods to
- * support consistent and efficient conversion of primitive data types.
- *
- * @hide
- */
-public interface TypedXmlSerializer extends XmlSerializer {
-    /**
-     * Functionally equivalent to {@link #attribute(String, String, String)} but
-     * with the additional signal that the given value is a candidate for being
-     * canonicalized, similar to {@link String#intern()}.
-     */
-    @NonNull XmlSerializer attributeInterned(@Nullable String namespace, @NonNull String name,
-            @NonNull String value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBytesHex(@Nullable String namespace, @NonNull String name,
-            @NonNull byte[] value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBytesBase64(@Nullable String namespace, @NonNull String name,
-            @NonNull byte[] value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeInt(@Nullable String namespace, @NonNull String name,
-            int value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeIntHex(@Nullable String namespace, @NonNull String name,
-            int value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeLong(@Nullable String namespace, @NonNull String name,
-            long value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeLongHex(@Nullable String namespace, @NonNull String name,
-            long value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeFloat(@Nullable String namespace, @NonNull String name,
-            float value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeDouble(@Nullable String namespace, @NonNull String name,
-            double value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBoolean(@Nullable String namespace, @NonNull String name,
-            boolean value) throws IOException;
-}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index 38decf9..33058d8 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -22,10 +22,13 @@
 import android.system.ErrnoException;
 import android.system.Os;
 
-import com.android.internal.util.BinaryXmlPullParser;
-import com.android.internal.util.BinaryXmlSerializer;
+import com.android.internal.util.ArtBinaryXmlPullParser;
+import com.android.internal.util.ArtBinaryXmlSerializer;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.BinaryXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.util.XmlObjectFactory;
 
@@ -146,7 +149,7 @@
      * @hide
      */
     public static @NonNull TypedXmlPullParser newBinaryPullParser() {
-        return new BinaryXmlPullParser();
+        return new ArtBinaryXmlPullParser();
     }
 
     /**
@@ -225,7 +228,7 @@
      * @hide
      */
     public static @NonNull TypedXmlSerializer newBinarySerializer() {
-        return new BinaryXmlSerializer();
+        return new ArtBinaryXmlSerializer();
     }
 
     /**
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
index c8c1fd4..eb467e0 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -93,8 +93,9 @@
      * associated with each signer.
      *
      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
-     * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.
-     * @throws IOException if an I/O error occurs while reading the APK file.
+     * @throws SecurityException          if an APK Signature Scheme v2 signature of this APK does
+     *                                    not verify.
+     * @throws IOException                if an I/O error occurs while reading the APK file.
      */
     public static X509Certificate[][] verify(String apkFile)
             throws SignatureNotFoundException, SecurityException, IOException {
@@ -386,7 +387,6 @@
                     break;
             }
         }
-        return;
     }
 
     static byte[] getVerityRootHash(String apkPath)
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index 510bde1..44168ca 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_REQUESTED_KEY;
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
 
+import android.accessibilityservice.AccessibilityService;
 import android.annotation.NonNull;
 import android.graphics.Matrix;
 import android.graphics.Rect;
@@ -46,11 +47,13 @@
 import android.view.accessibility.AccessibilityNodeProvider;
 import android.view.accessibility.AccessibilityRequestPreparer;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.window.ScreenCapture;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
+import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -588,6 +591,43 @@
         }
     }
 
+    /**
+     * Take a screenshot using {@link ScreenCapture} of this {@link ViewRootImpl}'s {@link
+     * SurfaceControl}.
+     */
+    public void takeScreenshotOfWindowClientThread(int interactionId,
+            ScreenCapture.ScreenCaptureListener listener,
+            IAccessibilityInteractionConnectionCallback callback) {
+        Message message = PooledLambda.obtainMessage(
+                AccessibilityInteractionController::takeScreenshotOfWindowUiThread,
+                this, interactionId, listener, callback);
+
+        // Screenshot results are returned to the service asynchronously, so the same-thread
+        // message wait logic from #scheduleMessage() is not needed.
+        mHandler.sendMessage(message);
+    }
+
+    private void takeScreenshotOfWindowUiThread(int interactionId,
+            ScreenCapture.ScreenCaptureListener listener,
+            IAccessibilityInteractionConnectionCallback callback) {
+        try {
+            if ((mViewRootImpl.getWindowFlags() & WindowManager.LayoutParams.FLAG_SECURE) != 0) {
+                callback.sendTakeScreenshotOfWindowError(
+                        AccessibilityService.ERROR_TAKE_SCREENSHOT_SECURE_WINDOW, interactionId);
+                return;
+            }
+            final ScreenCapture.LayerCaptureArgs captureArgs =
+                    new ScreenCapture.LayerCaptureArgs.Builder(mViewRootImpl.getSurfaceControl())
+                            .setChildrenOnly(false).setUid(Process.myUid()).build();
+            if (ScreenCapture.captureLayers(captureArgs, listener) != 0) {
+                callback.sendTakeScreenshotOfWindowError(
+                        AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId);
+            }
+        } catch (RemoteException re) {
+            /* ignore - the other side will time out */
+        }
+    }
+
     public void findFocusClientThread(long accessibilityNodeId, int focusType,
             Region interactiveRegion, int interactionId,
             IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid,
diff --git a/core/java/android/view/CutoutSpecification.java b/core/java/android/view/CutoutSpecification.java
index f8aa934..3fc3b6a 100644
--- a/core/java/android/view/CutoutSpecification.java
+++ b/core/java/android/view/CutoutSpecification.java
@@ -394,7 +394,6 @@
                 Log.e(TAG, "According to SVG definition, it shouldn't happen");
                 return;
             }
-            spec.trim();
             translateMatrix();
 
             final Path newPath = PathParser.createPathFromPathData(spec);
diff --git a/core/java/android/view/HandwritingDelegateConfiguration.java b/core/java/android/view/HandwritingDelegateConfiguration.java
new file mode 100644
index 0000000..719c614
--- /dev/null
+++ b/core/java/android/view/HandwritingDelegateConfiguration.java
@@ -0,0 +1,74 @@
+/*
+ * 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.view;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+
+/**
+ * Configuration for a view to act as a handwriting initiation delegate. This allows handwriting
+ * mode for a delegator editor view to be initiated by stylus movement on the delegate view.
+ *
+ * <p>If a stylus {@link MotionEvent} occurs within the delegate view's bounds, the callback
+ * returned by {@link #getInitiationCallback()} will be called. The callback implementation is
+ * expected to show and focus the delegator editor view. If a view with identifier matching {@link
+ * #getDelegatorViewId()} creates an input connection while the same stylus {@link MotionEvent}
+ * sequence is ongoing, handwriting mode will be initiated for that view.
+ *
+ * <p>A common use case is a custom view which looks like a text editor but does not actually
+ * support text editing itself, and clicking on the custom view causes an EditText to be shown. To
+ * support handwriting initiation in this case, {@link View#setHandwritingDelegateConfiguration} can
+ * be called on the custom view to configure it as a delegate, and set the EditText as the delegator
+ * by passing the EditText's identifier as the {@code delegatorViewId}. The {@code
+ * initiationCallback} implementation is typically the same as the click listener implementation
+ * which shows the EditText.
+ */
+public class HandwritingDelegateConfiguration {
+    @IdRes private final int mDelegatorViewId;
+    @NonNull private final Runnable mInitiationCallback;
+
+    /**
+     * Constructs a HandwritingDelegateConfiguration instance.
+     *
+     * @param delegatorViewId identifier of the delegator editor view for which handwriting mode
+     *     should be initiated
+     * @param initiationCallback callback called when a stylus {@link MotionEvent} occurs within
+     *     this view's bounds. This will be called from the UI thread.
+     */
+    public HandwritingDelegateConfiguration(
+            @IdRes int delegatorViewId, @NonNull Runnable initiationCallback) {
+        mDelegatorViewId = delegatorViewId;
+        mInitiationCallback = initiationCallback;
+    }
+
+    /**
+     * Returns the identifier of the delegator editor view for which handwriting mode should be
+     * initiated.
+     */
+    public int getDelegatorViewId() {
+        return mDelegatorViewId;
+    }
+
+    /**
+     * Returns the callback which should be called when a stylus {@link MotionEvent} occurs within
+     * the delegate view's bounds. The callback should only be called from the UI thread.
+     */
+    @NonNull
+    public Runnable getInitiationCallback() {
+        return mInitiationCallback;
+    }
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index a1ece92..2e4073e 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.annotation.IdRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
@@ -161,8 +162,23 @@
                     if (candidateView != null) {
                         if (candidateView == getConnectedView()) {
                             startHandwriting(candidateView);
+                        } else if (candidateView.getHandwritingDelegateConfiguration() != null) {
+                            mState.mDelegatorViewId =
+                                    candidateView
+                                            .getHandwritingDelegateConfiguration()
+                                            .getDelegatorViewId();
+                            candidateView
+                                    .getHandwritingDelegateConfiguration()
+                                    .getInitiationCallback()
+                                    .run();
                         } else {
-                            candidateView.requestFocus();
+                            if (candidateView.getRevealOnFocusHint()) {
+                                candidateView.setRevealOnFocusHint(false);
+                                candidateView.requestFocus();
+                                candidateView.setRevealOnFocusHint(true);
+                            } else {
+                                candidateView.requestFocus();
+                            }
                         }
                     }
                 }
@@ -253,8 +269,10 @@
         }
 
         final Rect handwritingArea = getViewHandwritingArea(connectedView);
-        if (isInHandwritingArea(handwritingArea, mState.mStylusDownX,
-                mState.mStylusDownY, connectedView)) {
+        if ((mState.mDelegatorViewId != View.NO_ID
+                        && mState.mDelegatorViewId == connectedView.getId())
+                || isInHandwritingArea(
+                        handwritingArea, mState.mStylusDownX, mState.mStylusDownY, connectedView)) {
             startHandwriting(connectedView);
         } else {
             mState.mShouldInitHandwriting = false;
@@ -281,6 +299,11 @@
         if (!view.isAutoHandwritingEnabled()) {
             return false;
         }
+        // The view may be a handwriting initiation delegate, in which case it is not the editor
+        // view for which handwriting would be started. However, in almost all cases, the return
+        // values of View#isStylusHandwritingAvailable will be the same for the delegate view and
+        // the delegator editor view. So the delegate view can be used to decide whether handwriting
+        // should be triggered.
         return view.isStylusHandwritingAvailable();
     }
 
@@ -467,6 +490,13 @@
          * built InputConnection.
          */
         private boolean mExceedHandwritingSlop;
+        /**
+         * If the current ongoing stylus MotionEvent sequence started over a handwriting initiation
+         * delegate view, then this is the view identifier of the corresponding delegator view. If
+         * the delegator view creates an input connection while the MotionEvent sequence is still
+         * ongoing, then handwriting mode will be initiated for the delegator view.
+         */
+        @IdRes private int mDelegatorViewId = View.NO_ID;
 
         /** The pointer id of the stylus pointer that is being tracked. */
         private final int mStylusPointerId;
diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl
index 1940042..91270d4 100644
--- a/core/java/android/view/IDisplayWindowInsetsController.aidl
+++ b/core/java/android/view/IDisplayWindowInsetsController.aidl
@@ -19,7 +19,7 @@
 import android.content.ComponentName;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Singular controller of insets to use when there isn't another obvious controller available.
@@ -32,10 +32,9 @@
      * Called when top focused window changes to determine whether or not to take over insets
      * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
      * @param component: Passes the top application component in the focused window.
-     * @param requestedVisibilities The insets visibilities requested by the focussed window.
+     * @param requestedVisibleTypes The insets types requested visible by the focused window.
      */
-    void topFocusedWindowChanged(in ComponentName component,
-            in InsetsVisibilities insetsVisibilities);
+    void topFocusedWindowChanged(in ComponentName component, int requestedVisibleTypes);
 
     /**
      * @see IWindow#insetsChanged
@@ -50,10 +49,10 @@
     /**
      * @see IWindow#showInsets
      */
-    void showInsets(int types, boolean fromIme);
+    void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     /**
      * @see IWindow#hideInsets
      */
-    void hideInsets(int types, boolean fromIme);
+    void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 }
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index a856474..8e16f24 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -29,6 +29,7 @@
 import android.view.IScrollCaptureResponseListener;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -68,16 +69,18 @@
      *
      * @param types internal insets types (WindowInsets.Type.InsetsType) to show
      * @param fromIme true if this request originated from IME (InputMethodService).
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    void showInsets(int types, boolean fromIme);
+    void showInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     /**
      * Called when a set of insets source window should be hidden by policy.
      *
      * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
      * @param fromIme true if this request originated from IME (InputMethodService).
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    void hideInsets(int types, boolean fromIme);
+    void hideInsets(int types, boolean fromIme, in @nullable ImeTracker.Token statsToken);
 
     void moved(int newX, int newY);
     void dispatchAppVisibility(boolean visible);
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index dddbe39..e2bc566 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -721,7 +721,7 @@
      * Called when a remote process updates the requested visibilities of insets on a display window
      * container.
      */
-    void updateDisplayWindowRequestedVisibilities(int displayId, in InsetsVisibilities vis);
+    void updateDisplayWindowRequestedVisibleTypes(int displayId, int requestedVisibleTypes);
 
     /**
      * Called to get the expected window insets.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 0052e82..03ccb47 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -32,7 +32,6 @@
 import android.view.WindowManager;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -48,15 +47,15 @@
  */
 interface IWindowSession {
     int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs,
-            in int viewVisibility, in int layerStackId, in InsetsVisibilities requestedVisibilities,
+            in int viewVisibility, in int layerStackId, int requestedVisibleTypes,
             out InputChannel outInputChannel, out InsetsState insetsState,
             out InsetsSourceControl[] activeControls, out Rect attachedFrame,
             out float[] sizeCompatScale);
     int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs,
-            in int viewVisibility, in int layerStackId, in int userId,
-            in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel,
-            out InsetsState insetsState, out InsetsSourceControl[] activeControls,
-            out Rect attachedFrame, out float[] sizeCompatScale);
+            in int viewVisibility, in int layerStackId, in int userId, int requestedVisibleTypes,
+            out InputChannel outInputChannel, out InsetsState insetsState,
+            out InsetsSourceControl[] activeControls, out Rect attachedFrame,
+            out float[] sizeCompatScale);
     int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, out InsetsState insetsState,
             out Rect attachedFrame, out float[] sizeCompatScale);
@@ -279,9 +278,9 @@
     oneway void updateTapExcludeRegion(IWindow window, in Region region);
 
     /**
-     * Updates the requested visibilities of insets.
+     * Updates the requested visible types of insets.
      */
-    oneway void updateRequestedVisibilities(IWindow window, in InsetsVisibilities visibilities);
+    oneway void updateRequestedVisibleTypes(IWindow window, int requestedVisibleTypes);
 
     /**
      * Called when the system gesture exclusion has changed.
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 4de7c4f..43828d5 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -108,10 +108,11 @@
     }
 
     /**
-     * @see InputMethodManager#checkFocus()
+     * @see ViewRootImpl#dispatchCheckFocus()
      */
-    public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
-        return getImmDelegate().checkFocus(forceNewFocus, startInput, mViewRootImpl);
+    @UiThread
+    void onScheduledCheckFocus() {
+        getImmDelegate().onScheduledCheckFocus(mViewRootImpl);
     }
 
     @UiThread
@@ -163,7 +164,7 @@
         void onPostWindowGainedFocus(View viewForWindowFocus,
                 @NonNull WindowManager.LayoutParams windowAttribute);
         void onViewFocusChanged(@NonNull View view, boolean hasFocus);
-        boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl);
+        void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl);
         void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl);
         void onWindowDismissed(ViewRootImpl viewRootImpl);
     }
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 332e97c..02e0fcc 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -65,7 +65,7 @@
     public void onWindowFocusGained(boolean hasViewFocus) {
         super.onWindowFocusGained(hasViewFocus);
         getImm().registerImeConsumer(this);
-        if (isRequestedVisible() && getControl() == null) {
+        if ((mController.getRequestedVisibleTypes() & getType()) != 0 && getControl() == null) {
             mIsRequestedVisibleAwaitingControl = true;
         }
     }
@@ -125,7 +125,7 @@
         // If we had a request before to show from IME (tracked with mImeRequestedShow), reaching
         // this code here means that we now got control, so we can start the animation immediately.
         // If client window is trying to control IME and IME is already visible, it is immediate.
-        if (fromIme || mState.getSource(getType()).isVisible() && getControl() != null) {
+        if (fromIme || (mState.getSource(getInternalType()).isVisible() && getControl() != null)) {
             return ShowResult.SHOW_IMMEDIATELY;
         }
 
@@ -169,7 +169,7 @@
 
     @Override
     protected boolean isRequestedVisibleAwaitingControl() {
-        return mIsRequestedVisibleAwaitingControl || isRequestedVisible();
+        return super.isRequestedVisibleAwaitingControl() || mIsRequestedVisibleAwaitingControl;
     }
 
     @Override
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index 9b1d867..3d7843c 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;
@@ -218,8 +219,9 @@
 
     /**
      * The input source is a mouse pointing device.
-     * This code is also used for other mouse-like pointing devices such as trackpads
-     * and trackpoints.
+     * This value is also used for other mouse-like pointing devices such as touchpads and pointing
+     * sticks. When used in combination with {@link #SOURCE_STYLUS}, it denotes an external drawing
+     * tablet.
      *
      * @see #SOURCE_CLASS_POINTER
      */
@@ -290,8 +292,8 @@
     public static final int SOURCE_MOUSE_RELATIVE = 0x00020000 | SOURCE_CLASS_TRACKBALL;
 
     /**
-     * The input source is a touch pad or digitizer tablet that is not
-     * associated with a display (unlike {@link #SOURCE_TOUCHSCREEN}).
+     * The input source is a touchpad (also known as a trackpad). Touchpads that are used to move
+     * the mouse cursor will also have {@link #SOURCE_MOUSE}.
      *
      * @see #SOURCE_CLASS_POSITION
      */
@@ -778,7 +780,7 @@
      * same input device descriptor.  This might happen in situations where a single
      * human input device registers multiple {@link InputDevice} instances (HID collections)
      * that describe separate features of the device, such as a keyboard that also
-     * has a trackpad.  Alternately, it may be that the input devices are simply
+     * has a touchpad.  Alternately, it may be that the input devices are simply
      * indistinguishable, such as two keyboards made by the same manufacturer.
      * </p><p>
      * The input device descriptor returned by {@link #getDescriptor} should only be
@@ -1010,6 +1012,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/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 805727c..e775969 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -58,6 +58,7 @@
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.WindowManager.LayoutParams;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -68,7 +69,7 @@
  * Implements {@link WindowInsetsAnimationController}
  * @hide
  */
-@VisibleForTesting
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
 public class InsetsAnimationControlImpl implements InternalInsetsAnimationController,
         InsetsAnimationControlRunner {
 
@@ -96,6 +97,8 @@
     /** @see WindowInsetsAnimationController#hasZeroInsetsIme */
     private final boolean mHasZeroInsetsIme;
     private final CompatibilityInfo.Translator mTranslator;
+    @Nullable
+    private final ImeTracker.Token mStatsToken;
     private Insets mCurrentInsets;
     private Insets mPendingInsets;
     private float mPendingFraction;
@@ -114,7 +117,7 @@
             @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
             Interpolator interpolator, @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            CompatibilityInfo.Translator translator) {
+            CompatibilityInfo.Translator translator, @Nullable ImeTracker.Token statsToken) {
         mControls = controls;
         mListener = listener;
         mTypes = types;
@@ -128,7 +131,7 @@
                     null /* typeSideMap */);
             mShownInsets = calculateInsets(mInitialInsetsState, frame, controls, true /* shown */,
                     typeSideMap);
-            mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsInternalType(ITYPE_IME);
+            mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsType(WindowInsets.Type.ime());
             if (mHasZeroInsetsIme) {
                 // IME has shownInsets of ZERO, and can't map to a side by default.
                 // Map zero insets IME to bottom, making it a special case of bottom insets.
@@ -141,7 +144,7 @@
             mCurrentInsets = calculateInsets(mInitialInsetsState, controls, true /* shown */);
             mHiddenInsets = calculateInsets(null, controls, false /* shown */);
             mShownInsets = calculateInsets(null, controls, true /* shown */);
-            mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsInternalType(ITYPE_IME);
+            mHasZeroInsetsIme = mShownInsets.bottom == 0 && controlsType(WindowInsets.Type.ime());
             buildSideControlsMap(mSideControlsMap, controls);
         }
         mPendingInsets = mCurrentInsets;
@@ -152,6 +155,7 @@
         mAnimationType = animationType;
         mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation;
         mTranslator = translator;
+        mStatsToken = statsToken;
         mController.startAnimation(this, listener, types, mAnimation,
                 new Bounds(mHiddenInsets, mShownInsets));
     }
@@ -228,6 +232,11 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        return mStatsToken;
+    }
+
+    @Override
     public void setInsetsAndAlpha(Insets insets, float alpha, float fraction) {
         setInsetsAndAlpha(insets, alpha, fraction, false /* allowWhenFinished */);
     }
@@ -253,10 +262,10 @@
         }
     }
 
-    @VisibleForTesting
     /**
      * @return Whether the finish callback of this animation should be invoked.
      */
+    @VisibleForTesting
     public boolean applyChangeInsets(@Nullable InsetsState outState) {
         if (mCancelled) {
             if (DEBUG) Log.d(TAG, "applyChangeInsets canceled");
diff --git a/core/java/android/view/InsetsAnimationControlRunner.java b/core/java/android/view/InsetsAnimationControlRunner.java
index 1cb00e3..cf40e7e 100644
--- a/core/java/android/view/InsetsAnimationControlRunner.java
+++ b/core/java/android/view/InsetsAnimationControlRunner.java
@@ -16,11 +16,12 @@
 
 package android.view;
 
+import android.annotation.Nullable;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsController.AnimationType;
-import android.view.InsetsState.InternalInsetsType;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Interface representing a runner for an insets animation.
@@ -63,10 +64,10 @@
     WindowInsetsAnimation getAnimation();
 
     /**
-     * @return Whether {@link #getTypes()} maps to a specific {@link InternalInsetsType}.
+     * @return Whether {@link #getTypes()} contains a specific {@link InsetsType}.
      */
-    default boolean controlsInternalType(@InternalInsetsType int type) {
-        return InsetsState.toInternalType(getTypes()).contains(type);
+    default boolean controlsType(@InsetsType int type) {
+        return (getTypes() & type) != 0;
     }
 
     /**
@@ -75,6 +76,12 @@
     @AnimationType int getAnimationType();
 
     /**
+     * @return The token tracking the current IME request or {@code null} otherwise.
+     */
+    @Nullable
+    ImeTracker.Token getStatsToken();
+
+    /**
      *
      * Export the state of classes that implement this interface into a protocol buffer
      * output stream.
diff --git a/core/java/android/view/InsetsAnimationThreadControlRunner.java b/core/java/android/view/InsetsAnimationThreadControlRunner.java
index fc97541..f7b9aa2 100644
--- a/core/java/android/view/InsetsAnimationThreadControlRunner.java
+++ b/core/java/android/view/InsetsAnimationThreadControlRunner.java
@@ -34,6 +34,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Insets animation runner that uses {@link InsetsAnimationThread} to run the animation off from the
@@ -112,12 +113,13 @@
             @InsetsType int types, InsetsAnimationControlCallbacks controller, long durationMs,
             Interpolator interpolator, @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            CompatibilityInfo.Translator translator, Handler mainThreadHandler) {
+            CompatibilityInfo.Translator translator, Handler mainThreadHandler,
+            @Nullable ImeTracker.Token statsToken) {
         mMainThreadHandler = mainThreadHandler;
         mOuterCallbacks = controller;
         mControl = new InsetsAnimationControlImpl(controls, frame, state, listener, types,
                 mCallbacks, durationMs, interpolator, animationType, layoutInsetsDuringAnimation,
-                translator);
+                translator, statsToken);
         InsetsAnimationThread.getHandler().post(() -> {
             if (mControl.isCancelled()) {
                 return;
@@ -141,6 +143,11 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        return mControl.getStatsToken();
+    }
+
+    @Override
     @UiThread
     public int getTypes() {
         return mControl.getTypes();
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 4a72a62..fbd8226 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -24,6 +24,8 @@
 import static android.view.InsetsState.toInternalType;
 import static android.view.InsetsState.toPublicType;
 import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
+import static android.view.WindowInsets.Type.FIRST;
+import static android.view.WindowInsets.Type.LAST;
 import static android.view.WindowInsets.Type.all;
 import static android.view.WindowInsets.Type.ime;
 
@@ -42,10 +44,12 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSourceConsumer.ShowResult;
 import android.view.InsetsState.InternalInsetsType;
@@ -57,6 +61,7 @@
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -102,18 +107,18 @@
         void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params);
 
         /**
-         * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean)
+         * @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int)
          */
-        void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
-                boolean hasControl);
+        default void updateCompatSysUiVisibility(@InsetsType int visibleTypes,
+                @InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) { }
 
         /**
          * Called when the requested visibilities of insets have been modified by the client.
          * The visibilities should be reported back to WM.
          *
-         * @param visibilities A collection of the requested visibilities.
+         * @param types Bitwise flags of types requested visible.
          */
-        void updateRequestedVisibilities(InsetsVisibilities visibilities);
+        void updateRequestedVisibleTypes(@InsetsType int types);
 
         /**
          * @return Whether the host has any callbacks it wants to synchronize the animations with.
@@ -564,9 +569,6 @@
     /** The state dispatched from server */
     private final InsetsState mLastDispatchedState = new InsetsState();
 
-    /** The requested visibilities sent to server */
-    private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
-
     private final Rect mFrame = new Rect();
     private final BiFunction<InsetsController, Integer, InsetsSourceConsumer> mConsumerCreator;
     private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
@@ -575,7 +577,6 @@
 
     private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
     private final ArrayList<RunningAnimation> mRunningAnimations = new ArrayList<>();
-    private final ArraySet<InsetsSourceConsumer> mRequestedVisibilityChanged = new ArraySet<>();
     private WindowInsets mLastInsets;
 
     private boolean mAnimCallbackScheduled;
@@ -593,6 +594,7 @@
     private boolean mStartingAnimation;
     private int mCaptionInsetsHeight = 0;
     private boolean mAnimationsDisabled;
+    private boolean mCompatSysUiVisibilityStaled;
 
     private final Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
     private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
@@ -604,6 +606,18 @@
     /** Set of inset types which cannot be controlled by the user animation */
     private @InsetsType int mDisabledUserAnimationInsetsTypes;
 
+    /** Set of inset types which are visible */
+    private @InsetsType int mVisibleTypes = WindowInsets.Type.defaultVisible();
+
+    /** Set of inset types which are requested visible */
+    private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+
+    /** Set of inset types which are requested visible which are reported to the host */
+    private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+
+    /** Set of inset types that we have controls of */
+    private @InsetsType int mControllableTypes;
+
     private final Runnable mInvokeControllableInsetsChangedListeners =
             this::invokeControllableInsetsChangedListeners;
 
@@ -687,8 +701,8 @@
     }
 
     @Override
-    public boolean isRequestedVisible(int type) {
-        return getSourceConsumer(type).isRequestedVisible();
+    public @InsetsType int getRequestedVisibleTypes() {
+        return mRequestedVisibleTypes;
     }
 
     public InsetsState getLastDispatchedState() {
@@ -715,6 +729,7 @@
         final InsetsState lastState = new InsetsState(mState, true /* copySources */);
         updateState(state);
         applyLocalVisibilityOverride();
+        updateCompatSysUiVisibility();
 
         if (!mState.equals(lastState, false /* excludingCaptionInsets */,
                 true /* excludeInvisibleIme */)) {
@@ -727,14 +742,15 @@
 
     private void updateState(InsetsState newState) {
         mState.set(newState, 0 /* types */);
+        @InsetsType int visibleTypes = 0;
         @InsetsType int disabledUserAnimationTypes = 0;
         @InsetsType int[] cancelledUserAnimationTypes = {0};
         for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
             InsetsSource source = newState.peekSource(type);
             if (source == null) continue;
-            @AnimationType int animationType = getAnimationType(type);
+            @InsetsType int insetsType = toPublicType(type);
+            @AnimationType int animationType = getAnimationType(insetsType);
             if (!source.isUserControllable()) {
-                @InsetsType int insetsType = toPublicType(type);
                 // The user animation is not allowed when visible frame is empty.
                 disabledUserAnimationTypes |= insetsType;
                 if (animationType == ANIMATION_TYPE_USER) {
@@ -744,6 +760,15 @@
                 }
             }
             getSourceConsumer(type).updateSource(source, animationType);
+            if (source.isVisible()) {
+                visibleTypes |= insetsType;
+            }
+        }
+        if (mVisibleTypes != visibleTypes) {
+            if (WindowInsets.Type.hasCompatSystemBars(mVisibleTypes ^ visibleTypes)) {
+                mCompatSysUiVisibilityStaled = true;
+            }
+            mVisibleTypes = visibleTypes;
         }
         for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
             // Only update the server side insets here.
@@ -767,8 +792,7 @@
         if (diff != 0) {
             for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
                 InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
-                if (consumer.getControl() != null
-                        && (toPublicType(consumer.getType()) & diff) != 0) {
+                if (consumer.getControl() != null && (consumer.getType() & diff) != 0) {
                     mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners);
                     mHandler.post(mInvokeControllableInsetsChangedListeners);
                     break;
@@ -829,7 +853,8 @@
     }
 
     /**
-     * @see InsetsState#calculateInsets
+     * @see InsetsState#calculateInsets(Rect, InsetsState, boolean, boolean, int, int, int, int,
+     *      int, SparseIntArray)
      */
     @VisibleForTesting
     public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars,
@@ -868,14 +893,14 @@
             }
         }
 
-        boolean requestedVisibilityStale = false;
+        @InsetsType int controllableTypes = 0;
         final int[] showTypes = new int[1];
         final int[] hideTypes = new int[1];
 
         // Ensure to update all existing source consumers
         for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
             final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
-            final InsetsSourceControl control = mTmpControlArray.get(consumer.getType());
+            final InsetsSourceControl control = mTmpControlArray.get(consumer.getInternalType());
 
             // control may be null, but we still need to update the control to null if it got
             // revoked.
@@ -888,22 +913,7 @@
             final @InternalInsetsType int type = control.getType();
             final InsetsSourceConsumer consumer = getSourceConsumer(type);
             consumer.setControl(control, showTypes, hideTypes);
-
-            if (!requestedVisibilityStale) {
-                final boolean requestedVisible = consumer.isRequestedVisible();
-
-                // We might have changed our requested visibilities while we don't have the control,
-                // so we need to update our requested state once we have control. Otherwise, our
-                // requested state at the server side might be incorrect.
-                final boolean requestedVisibilityChanged =
-                        requestedVisible != mRequestedVisibilities.getVisibility(type);
-
-                // The IME client visibility will be reset by insets source provider while updating
-                // control, so if IME is requested visible, we need to send the request to server.
-                final boolean imeRequestedVisible = type == ITYPE_IME && requestedVisible;
-
-                requestedVisibilityStale = requestedVisibilityChanged || imeRequestedVisible;
-            }
+            controllableTypes |= InsetsState.toPublicType(type);
         }
 
         if (mTmpControlArray.size() > 0) {
@@ -921,23 +931,33 @@
         hideTypes[0] &= ~animatingTypes;
 
         if (showTypes[0] != 0) {
-            applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
+            applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
+                    null /* statsToken */);
         }
         if (hideTypes[0] != 0) {
-            applyAnimation(hideTypes[0], false /* show */, false /* fromIme */);
+            applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
+                    null /* statsToken */);
+        }
+
+        if (mControllableTypes != controllableTypes) {
+            if (WindowInsets.Type.hasCompatSystemBars(mControllableTypes ^ controllableTypes)) {
+                mCompatSysUiVisibilityStaled = true;
+            }
+            mControllableTypes = controllableTypes;
         }
 
         // InsetsSourceConsumer#setControl might change the requested visibility.
-        updateRequestedVisibilities();
+        reportRequestedVisibleTypes();
     }
 
     @Override
     public void show(@InsetsType int types) {
-        show(types, false /* fromIme */);
+        show(types, false /* fromIme */, null /* statsToken */);
     }
 
-    @VisibleForTesting
-    public void show(@InsetsType int types, boolean fromIme) {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public void show(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         if ((types & ime()) != 0) {
             Log.d(TAG, "show(ime(), fromIme=" + fromIme + ")");
         }
@@ -964,44 +984,57 @@
                     true /* fromIme */, pendingRequest.durationMs, pendingRequest.interpolator,
                     pendingRequest.animationType,
                     pendingRequest.layoutInsetsDuringAnimation,
-                    pendingRequest.useInsetsAnimationThread);
+                    pendingRequest.useInsetsAnimationThread, statsToken);
             return;
         }
 
         // TODO: Support a ResultReceiver for IME.
         // TODO(b/123718661): Make show() work for multi-session IME.
         int typesReady = 0;
-        final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
-        for (int i = internalTypes.size() - 1; i >= 0; i--) {
-            @InternalInsetsType int internalType = internalTypes.valueAt(i);
-            @AnimationType int animationType = getAnimationType(internalType);
-            InsetsSourceConsumer consumer = getSourceConsumer(internalType);
-            if (consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE
+        for (int type = FIRST; type <= LAST; type = type << 1) {
+            if ((types & type) == 0) {
+                continue;
+            }
+            @AnimationType final int animationType = getAnimationType(type);
+            final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+            final boolean isImeAnimation = type == ime();
+            if (requestedVisible && animationType == ANIMATION_TYPE_NONE
                     || animationType == ANIMATION_TYPE_SHOW) {
                 // no-op: already shown or animating in (because window visibility is
                 // applied before starting animation).
                 if (DEBUG) Log.d(TAG, String.format(
                         "show ignored for type: %d animType: %d requestedVisible: %s",
-                        consumer.getType(), animationType, consumer.isRequestedVisible()));
+                        type, animationType, requestedVisible));
+                if (isImeAnimation) {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
                 continue;
             }
             if (fromIme && animationType == ANIMATION_TYPE_USER) {
                 // App is already controlling the IME, don't cancel it.
+                if (isImeAnimation) {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
                 continue;
             }
-            typesReady |= InsetsState.toPublicType(consumer.getType());
+            if (isImeAnimation) {
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+            }
+            typesReady |= type;
         }
         if (DEBUG) Log.d(TAG, "show typesReady: " + typesReady);
-        applyAnimation(typesReady, true /* show */, fromIme);
+        applyAnimation(typesReady, true /* show */, fromIme, statsToken);
     }
 
     @Override
     public void hide(@InsetsType int types) {
-        hide(types, false /* fromIme */);
+        hide(types, false /* fromIme */, null /* statsToken */);
     }
 
     @VisibleForTesting
-    public void hide(@InsetsType int types, boolean fromIme) {
+    public void hide(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         if (fromIme) {
             ImeTracing.getInstance().triggerClientDump("InsetsController#hide",
                     mHost.getInputMethodManager(), null /* icProto */);
@@ -1010,19 +1043,29 @@
             Trace.asyncTraceBegin(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0);
         }
         int typesReady = 0;
-        final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
-        for (int i = internalTypes.size() - 1; i >= 0; i--) {
-            @InternalInsetsType int internalType = internalTypes.valueAt(i);
-            @AnimationType int animationType = getAnimationType(internalType);
-            InsetsSourceConsumer consumer = getSourceConsumer(internalType);
-            if (!consumer.isRequestedVisible() && animationType == ANIMATION_TYPE_NONE
-                    || animationType == ANIMATION_TYPE_HIDE) {
-                // no-op: already hidden or animating out.
+        for (int type = FIRST; type <= LAST; type = type << 1) {
+            if ((types & type) == 0) {
                 continue;
             }
-            typesReady |= InsetsState.toPublicType(consumer.getType());
+            @AnimationType final int animationType = getAnimationType(type);
+            final boolean requestedVisible = (type & mRequestedVisibleTypes) != 0;
+            final boolean isImeAnimation = type == ime();
+            if (!requestedVisible && animationType == ANIMATION_TYPE_NONE
+                    || animationType == ANIMATION_TYPE_HIDE) {
+                // no-op: already hidden or animating out (because window visibility is
+                // applied before starting animation).
+                if (isImeAnimation) {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+                }
+                continue;
+            }
+            if (isImeAnimation) {
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_APPLY_ANIMATION);
+            }
+            typesReady |= type;
         }
-        applyAnimation(typesReady, false /* show */, fromIme /* fromIme */);
+        applyAnimation(typesReady, false /* show */, fromIme, statsToken);
     }
 
     @Override
@@ -1051,7 +1094,7 @@
 
         controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
                 interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
-                false /* useInsetsAnimationThread */);
+                false /* useInsetsAnimationThread */, null /* statsToken */);
     }
 
     private void controlAnimationUnchecked(@InsetsType int types,
@@ -1060,7 +1103,8 @@
             long durationMs, Interpolator interpolator,
             @AnimationType int animationType,
             @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation,
-            boolean useInsetsAnimationThread) {
+            boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) {
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_CONTROL_ANIMATION);
         if ((types & mTypesBeingCancelled) != 0) {
             throw new IllegalStateException("Cannot start a new insets animation of "
                     + Type.toString(types)
@@ -1082,7 +1126,7 @@
         if (types == 0) {
             // nothing to animate.
             listener.onCancelled(null);
-            updateRequestedVisibilities();
+            reportRequestedVisibleTypes();
             if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
             return;
         }
@@ -1118,7 +1162,7 @@
                     }
                 });
             }
-            updateRequestedVisibilities();
+            reportRequestedVisibleTypes();
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
             return;
         }
@@ -1126,7 +1170,7 @@
         if (typesReady == 0) {
             if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
             listener.onCancelled(null);
-            updateRequestedVisibilities();
+            reportRequestedVisibleTypes();
             return;
         }
 
@@ -1135,14 +1179,16 @@
                 ? new InsetsAnimationThreadControlRunner(controls,
                         frame, mState, listener, typesReady, this, durationMs, interpolator,
                         animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
-                        mHost.getHandler())
+                        mHost.getHandler(), statsToken)
                 : new InsetsAnimationControlImpl(controls,
                         frame, mState, listener, typesReady, this, durationMs, interpolator,
-                        animationType, layoutInsetsDuringAnimation, mHost.getTranslator());
+                        animationType, layoutInsetsDuringAnimation, mHost.getTranslator(),
+                        statsToken);
         if ((typesReady & WindowInsets.Type.ime()) != 0) {
             ImeTracing.getInstance().triggerClientDump("InsetsAnimationControlImpl",
                     mHost.getInputMethodManager(), null /* icProto */);
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
         mRunningAnimations.add(new RunningAnimation(runner, animationType));
         if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
                 + useInsetsAnimationThread);
@@ -1158,7 +1204,7 @@
         } else {
             hideDirectly(types, false /* animationFinished */, animationType, fromIme);
         }
-        updateRequestedVisibilities();
+        reportRequestedVisibleTypes();
     }
 
     // TODO(b/242962223): Make this setter restrictive.
@@ -1214,13 +1260,13 @@
             if (!canRun) {
                 if (WARN) Log.w(TAG, String.format(
                         "collectSourceControls can't continue show for type: %s fromIme: %b",
-                        InsetsState.typeToString(consumer.getType()), fromIme));
+                        InsetsState.typeToString(consumer.getInternalType()), fromIme));
                 continue;
             }
             final InsetsSourceControl control = consumer.getControl();
             if (control != null && control.getLeash() != null) {
-                controls.put(consumer.getType(), new InsetsSourceControl(control));
-                typesReady |= toPublicType(consumer.getType());
+                controls.put(control.getType(), new InsetsSourceControl(control));
+                typesReady |= consumer.getType();
             } else if (animationType == ANIMATION_TYPE_SHOW) {
                 if (DEBUG) Log.d(TAG, "collectSourceControls no control for show(). fromIme: "
                         + fromIme);
@@ -1246,25 +1292,15 @@
 
     private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
             @InsetsType int types) {
-
-        final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
-
         // Generally, we want to layout the opposite of the current state. This is to make animation
         // callbacks easy to use: The can capture the layout values and then treat that as end-state
         // during the animation.
         //
         // However, if controlling multiple sources, we want to treat it as shown if any of the
         // types is currently hidden.
-        for (int i = internalTypes.size() - 1; i >= 0; i--) {
-            InsetsSourceConsumer consumer = mSourceConsumers.get(internalTypes.valueAt(i));
-            if (consumer == null) {
-                continue;
-            }
-            if (!consumer.isRequestedVisible()) {
-                return LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
-            }
-        }
-        return LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
+        return (mRequestedVisibleTypes & types) != types
+                ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
+                : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN;
     }
 
     private void cancelExistingControllers(@InsetsType int types) {
@@ -1304,11 +1340,18 @@
             // requested visibility.
             return;
         }
+        final ImeTracker.Token statsToken = runner.getStatsToken();
         if (shown) {
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_SHOW);
             showDirectly(runner.getTypes(), true /* fromIme */);
+            ImeTracker.get().onShown(statsToken);
         } else {
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_CLIENT_ANIMATION_FINISHED_HIDE);
             hideDirectly(runner.getTypes(), true /* animationFinished */,
                     runner.getAnimationType(), true /* fromIme */);
+            ImeTracker.get().onHidden(statsToken);
         }
     }
 
@@ -1318,24 +1361,33 @@
     }
 
     void notifyControlRevoked(InsetsSourceConsumer consumer) {
-        final @InsetsType int types = toPublicType(consumer.getType());
+        final @InsetsType int type = consumer.getType();
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
             InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
-            control.notifyControlRevoked(types);
+            control.notifyControlRevoked(type);
             if (control.getControllingTypes() == 0) {
                 cancelAnimation(control, true /* invokeCallback */);
             }
         }
-        if (consumer.getType() == ITYPE_IME) {
+        if (type == ime()) {
             abortPendingImeControlRequest();
         }
     }
 
     private void cancelAnimation(InsetsAnimationControlRunner control, boolean invokeCallback) {
-        if (DEBUG) Log.d(TAG, String.format("cancelAnimation of types: %d, animType: %d, host: %s",
-                control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
         if (invokeCallback) {
+            ImeTracker.get().onCancelled(control.getStatsToken(),
+                    ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
             control.cancel();
+        } else {
+            // Succeeds if invokeCallback is false (i.e. when called from notifyFinished).
+            ImeTracker.get().onProgress(control.getStatsToken(),
+                    ImeTracker.PHASE_CLIENT_ANIMATION_CANCEL);
+        }
+        if (DEBUG) {
+            Log.d(TAG, TextUtils.formatSimple(
+                    "cancelAnimation of types: %d, animType: %d, host: %s",
+                    control.getTypes(), control.getAnimationType(), mHost.getRootViewTitle()));
         }
         boolean stateChanged = false;
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
@@ -1386,11 +1438,14 @@
     }
 
     /**
-     * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean)
+     * @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int)
      */
-    public void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
-            boolean hasControl) {
-        mHost.updateCompatSysUiVisibility(type, visible, hasControl);
+    public void updateCompatSysUiVisibility() {
+        if (mCompatSysUiVisibilityStaled) {
+            mCompatSysUiVisibilityStaled = false;
+            mHost.updateCompatSysUiVisibility(
+                    mVisibleTypes, mRequestedVisibleTypes, mControllableTypes);
+        }
     }
 
     /**
@@ -1408,51 +1463,42 @@
     }
 
     @VisibleForTesting
-    public @AnimationType int getAnimationType(@InternalInsetsType int type) {
+    public @AnimationType int getAnimationType(@InsetsType int type) {
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
             InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
-            if (control.controlsInternalType(type)) {
+            if (control.controlsType(type)) {
                 return mRunningAnimations.get(i).type;
             }
         }
         return ANIMATION_TYPE_NONE;
     }
 
-    @VisibleForTesting
-    public void onRequestedVisibilityChanged(InsetsSourceConsumer consumer) {
-        mRequestedVisibilityChanged.add(consumer);
+    void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
+        final @InsetsType int requestedVisibleTypes =
+                (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
+        if (mRequestedVisibleTypes != requestedVisibleTypes) {
+            if (WindowInsets.Type.hasCompatSystemBars(
+                    mRequestedVisibleTypes ^ requestedVisibleTypes)) {
+                mCompatSysUiVisibilityStaled = true;
+            }
+            mRequestedVisibleTypes = requestedVisibleTypes;
+        }
     }
 
     /**
-     * Sends the requested visibilities to window manager if any of them is changed.
+     * Sends the requested visible types to window manager if any of them is changed.
      */
-    private void updateRequestedVisibilities() {
-        boolean changed = false;
-        for (int i = mRequestedVisibilityChanged.size() - 1; i >= 0; i--) {
-            final InsetsSourceConsumer consumer = mRequestedVisibilityChanged.valueAt(i);
-            final @InternalInsetsType int type = consumer.getType();
-            if (type == ITYPE_CAPTION_BAR) {
-                continue;
-            }
-            final boolean requestedVisible = consumer.isRequestedVisible();
-            if (mRequestedVisibilities.getVisibility(type) != requestedVisible) {
-                mRequestedVisibilities.setVisibility(type, requestedVisible);
-                changed = true;
-            }
+    private void reportRequestedVisibleTypes() {
+        updateCompatSysUiVisibility();
+        if (mReportedRequestedVisibleTypes != mRequestedVisibleTypes) {
+            mReportedRequestedVisibleTypes = mRequestedVisibleTypes;
+            mHost.updateRequestedVisibleTypes(mReportedRequestedVisibleTypes);
         }
-        mRequestedVisibilityChanged.clear();
-        if (!changed) {
-            return;
-        }
-        mHost.updateRequestedVisibilities(mRequestedVisibilities);
-    }
-
-    InsetsVisibilities getRequestedVisibilities() {
-        return mRequestedVisibilities;
     }
 
     @VisibleForTesting
-    public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme) {
+    public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         // TODO(b/166736352): We should only skip the animation of specific types, not all types.
         boolean skipAnim = false;
         if ((types & ime()) != 0) {
@@ -1465,12 +1511,12 @@
                         && consumer.hasViewFocusWhenWindowFocusGain();
             }
         }
-        applyAnimation(types, show, fromIme, skipAnim);
+        applyAnimation(types, show, fromIme, skipAnim, statsToken);
     }
 
     @VisibleForTesting
     public void applyAnimation(@InsetsType final int types, boolean show, boolean fromIme,
-            boolean skipAnim) {
+            boolean skipAnim, @Nullable ImeTracker.Token statsToken) {
         if (types == 0) {
             // nothing to animate.
             if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
@@ -1490,12 +1536,11 @@
                 listener.getDurationMs(), listener.getInsetsInterpolator(),
                 show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE,
                 show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
-                !hasAnimationCallbacks /* useInsetsAnimationThread */);
+                !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken);
     }
 
-    private void hideDirectly(
-            @InsetsType int types, boolean animationFinished, @AnimationType int animationType,
-            boolean fromIme) {
+    private void hideDirectly(@InsetsType int types, boolean animationFinished,
+            @AnimationType int animationType, boolean fromIme) {
         if ((types & ime()) != 0) {
             ImeTracing.getInstance().triggerClientDump("InsetsController#hideDirectly",
                     mHost.getInputMethodManager(), null /* icProto */);
@@ -1504,7 +1549,7 @@
         for (int i = internalTypes.size() - 1; i >= 0; i--) {
             getSourceConsumer(internalTypes.valueAt(i)).hide(animationFinished, animationType);
         }
-        updateRequestedVisibilities();
+        reportRequestedVisibleTypes();
 
         if (fromIme) {
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0);
@@ -1520,7 +1565,7 @@
         for (int i = internalTypes.size() - 1; i >= 0; i--) {
             getSourceConsumer(internalTypes.valueAt(i)).show(false /* fromIme */);
         }
-        updateRequestedVisibilities();
+        reportRequestedVisibleTypes();
 
         if (fromIme) {
             Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0);
@@ -1655,9 +1700,9 @@
         @InsetsType int result = 0;
         for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
             InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
-            InsetsSource source = mState.peekSource(consumer.mType);
+            InsetsSource source = mState.peekSource(consumer.getInternalType());
             if (consumer.getControl() != null && source != null && source.isUserControllable()) {
-                result |= toPublicType(consumer.mType);
+                result |= consumer.getType();
             }
         }
         return result & ~mState.calculateUncontrollableInsetsFromFrame(mFrame);
@@ -1698,12 +1743,11 @@
     }
 
     @Override
-    public void reportPerceptible(int types, boolean perceptible) {
-        final ArraySet<Integer> internalTypes = toInternalType(types);
+    public void reportPerceptible(@InsetsType int types, boolean perceptible) {
         final int size = mSourceConsumers.size();
         for (int i = 0; i < size; i++) {
             final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
-            if (internalTypes.contains(consumer.getType())) {
+            if ((consumer.getType() & types) != 0) {
                 consumer.onPerceptible(perceptible);
             }
         }
diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java
index 7275780..da54da16 100644
--- a/core/java/android/view/InsetsFrameProvider.java
+++ b/core/java/android/view/InsetsFrameProvider.java
@@ -273,6 +273,9 @@
     /**
      * Class to describe the insets size to be provided to window with specific window type. If not
      * used, same insets size will be sent as instructed in the insetsSize and source.
+     *
+     * If the insetsSize of given type is set to {@code null}, the insets source frame will be used
+     * directly for that window type.
      */
     public static class InsetsSizeOverride implements Parcelable {
         public final int windowType;
@@ -280,7 +283,7 @@
 
         protected InsetsSizeOverride(Parcel in) {
             windowType = in.readInt();
-            insetsSize = in.readParcelable(null, android.graphics.Insets.class);
+            insetsSize = in.readParcelable(null, Insets.class);
         }
 
         public InsetsSizeOverride(int type, Insets size) {
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index edcfc95..778c677 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -37,6 +37,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Runs a fake animation of resizing insets to produce insets animation callbacks.
@@ -92,6 +93,12 @@
     }
 
     @Override
+    public ImeTracker.Token getStatsToken() {
+        // Return null as resizing the IME view is not explicitly tracked.
+        return null;
+    }
+
+    @Override
     public void cancel() {
         if (mCancelled || mFinished) {
             return;
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 5236fe7..21c0395 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -27,14 +27,12 @@
 import static android.view.InsetsSourceConsumerProto.SOURCE_CONTROL;
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.getDefaultVisibility;
-import static android.view.InsetsState.toPublicType;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.graphics.Rect;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsState.InternalInsetsType;
@@ -75,9 +73,9 @@
     }
 
     protected final InsetsController mController;
-    protected boolean mRequestedVisible;
     protected final InsetsState mState;
-    protected final @InternalInsetsType int mType;
+    private final @InternalInsetsType int mInternalType;
+    private final @InsetsType int mType;
 
     private static final String TAG = "InsetsSourceConsumer";
     private final Supplier<Transaction> mTransactionSupplier;
@@ -100,11 +98,11 @@
      */
     public InsetsSourceConsumer(@InternalInsetsType int type, InsetsState state,
             Supplier<Transaction> transactionSupplier, InsetsController controller) {
-        mType = type;
+        mType = InsetsState.toPublicType(type);
+        mInternalType = type;
         mState = state;
         mTransactionSupplier = transactionSupplier;
         mController = controller;
-        mRequestedVisible = getDefaultVisibility(type);
     }
 
     /**
@@ -118,7 +116,7 @@
      */
     public boolean setControl(@Nullable InsetsSourceControl control,
             @InsetsType int[] showTypes, @InsetsType int[] hideTypes) {
-        if (mType == ITYPE_IME) {
+        if (mInternalType == ITYPE_IME) {
             ImeTracing.getInstance().triggerClientDump("InsetsSourceConsumer#setControl",
                     mController.getHost().getInputMethodManager(), null /* icProto */);
         }
@@ -142,16 +140,14 @@
             mController.notifyControlRevoked(this);
 
             // Check if we need to restore server visibility.
-            final InsetsSource source = mState.getSource(mType);
+            final InsetsSource source = mState.getSource(mInternalType);
             final boolean serverVisibility =
-                    mController.getLastDispatchedState().getSourceOrDefaultVisibility(mType);
+                    mController.getLastDispatchedState().getSourceOrDefaultVisibility(
+                            mInternalType);
             if (source.isVisible() != serverVisibility) {
                 source.setVisible(serverVisibility);
                 mController.notifyVisibilityChanged();
             }
-
-            // For updateCompatSysUiVisibility
-            applyLocalVisibilityOverride();
         } else {
             final boolean requestedVisible = isRequestedVisibleAwaitingControl();
             final SurfaceControl oldLeash = lastControl != null ? lastControl.getLeash() : null;
@@ -163,9 +159,9 @@
                 if (DEBUG) Log.d(TAG, String.format("Gaining leash in %s, requestedVisible: %b",
                         mController.getHost().getRootViewTitle(), requestedVisible));
                 if (requestedVisible) {
-                    showTypes[0] |= toPublicType(getType());
+                    showTypes[0] |= mType;
                 } else {
-                    hideTypes[0] |= toPublicType(getType());
+                    hideTypes[0] |= mType;
                 }
             } else {
                 // We are gaining control, but don't need to run an animation.
@@ -176,7 +172,7 @@
 
                 // If we have a new leash, make sure visibility is up-to-date, even though we
                 // didn't want to run an animation above.
-                if (mController.getAnimationType(control.getType()) == ANIMATION_TYPE_NONE) {
+                if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) {
                     applyRequestedVisibilityToControl();
                 }
 
@@ -199,29 +195,32 @@
 
     /**
      * Determines if the consumer will be shown after control is available.
-     * Note: for system bars this method is same as {@link #isRequestedVisible()}.
      *
      * @return {@code true} if consumer has a pending show.
      */
     protected boolean isRequestedVisibleAwaitingControl() {
-        return isRequestedVisible();
+        return (mController.getRequestedVisibleTypes() & mType) != 0;
     }
 
-    int getType() {
+    @InsetsType int getType() {
         return mType;
     }
 
+    @InternalInsetsType int getInternalType() {
+        return mInternalType;
+    }
+
     @VisibleForTesting
     public void show(boolean fromIme) {
         if (DEBUG) Log.d(TAG, String.format("Call show() for type: %s fromIme: %b ",
-                InsetsState.typeToString(mType), fromIme));
+                InsetsState.typeToString(mInternalType), fromIme));
         setRequestedVisible(true);
     }
 
     @VisibleForTesting
     public void hide() {
         if (DEBUG) Log.d(TAG, String.format("Call hide for %s on %s",
-                InsetsState.typeToString(mType), mController.getHost().getRootViewTitle()));
+                InsetsState.typeToString(mInternalType), mController.getHost().getRootViewTitle()));
         setRequestedVisible(false);
     }
 
@@ -249,69 +248,34 @@
     }
 
     boolean applyLocalVisibilityOverride() {
-        final InsetsSource source = mState.peekSource(mType);
-        final boolean isVisible = source != null ? source.isVisible() : getDefaultVisibility(mType);
+        final InsetsSource source = mState.peekSource(mInternalType);
+        final boolean isVisible = source != null ? source.isVisible() : getDefaultVisibility(
+                mInternalType);
         final boolean hasControl = mSourceControl != null;
+        final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
 
-        if (mType == ITYPE_IME) {
+        if (mInternalType == ITYPE_IME) {
             ImeTracing.getInstance().triggerClientDump(
                     "InsetsSourceConsumer#applyLocalVisibilityOverride",
                     mController.getHost().getInputMethodManager(), null /* icProto */);
         }
 
-        updateCompatSysUiVisibility(hasControl, source, isVisible);
-
         // If we don't have control, we are not able to change the visibility.
         if (!hasControl) {
             if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in "
                     + mController.getHost().getRootViewTitle()
-                    + " requestedVisible " + mRequestedVisible);
+                    + " requestedVisible=" + requestedVisible);
             return false;
         }
-        if (isVisible == mRequestedVisible) {
+        if (isVisible == requestedVisible) {
             return false;
         }
         if (DEBUG) Log.d(TAG, String.format("applyLocalVisibilityOverride: %s requestedVisible: %b",
-                mController.getHost().getRootViewTitle(), mRequestedVisible));
-        mState.getSource(mType).setVisible(mRequestedVisible);
+                mController.getHost().getRootViewTitle(), requestedVisible));
+        mState.getSource(mInternalType).setVisible(requestedVisible);
         return true;
     }
 
-    private void updateCompatSysUiVisibility(boolean hasControl, InsetsSource source,
-            boolean visible) {
-        final @InsetsType int publicType = InsetsState.toPublicType(mType);
-        if (publicType != WindowInsets.Type.statusBars()
-                && publicType != WindowInsets.Type.navigationBars()) {
-            // System UI visibility only controls status bars and navigation bars.
-            return;
-        }
-        final boolean compatVisible;
-        if (hasControl) {
-            compatVisible = mRequestedVisible;
-        } else if (source != null && !source.getFrame().isEmpty()) {
-            compatVisible = visible;
-        } else {
-            final ArraySet<Integer> types = InsetsState.toInternalType(publicType);
-            for (int i = types.size() - 1; i >= 0; i--) {
-                final InsetsSource s = mState.peekSource(types.valueAt(i));
-                if (s != null && !s.getFrame().isEmpty()) {
-                    // The compat system UI visibility would be updated by another consumer which
-                    // handles the same public insets type.
-                    return;
-                }
-            }
-            // No one provides the public type. Use the requested visibility for making the callback
-            // behavior compatible.
-            compatVisible = mRequestedVisible;
-        }
-        mController.updateCompatSysUiVisibility(mType, compatVisible, hasControl);
-    }
-
-    @VisibleForTesting
-    public boolean isRequestedVisible() {
-        return mRequestedVisible;
-    }
-
     /**
      * Request to show current window type.
      *
@@ -350,7 +314,7 @@
 
     @VisibleForTesting(visibility = PACKAGE)
     public void updateSource(InsetsSource newSource, @AnimationType int animationType) {
-        InsetsSource source = mState.peekSource(mType);
+        InsetsSource source = mState.peekSource(mInternalType);
         if (source == null || animationType == ANIMATION_TYPE_NONE
                 || source.getFrame().equals(newSource.getFrame())) {
             mPendingFrame = null;
@@ -375,7 +339,7 @@
     @VisibleForTesting(visibility = PACKAGE)
     public boolean notifyAnimationFinished() {
         if (mPendingFrame != null) {
-            InsetsSource source = mState.getSource(mType);
+            InsetsSource source = mState.getSource(mInternalType);
             source.setFrame(mPendingFrame);
             source.setVisibleFrame(mPendingVisibleFrame);
             mPendingFrame = null;
@@ -390,11 +354,8 @@
      * the moment.
      */
     protected void setRequestedVisible(boolean requestedVisible) {
-        if (mRequestedVisible != requestedVisible) {
-            mRequestedVisible = requestedVisible;
-            mController.onRequestedVisibilityChanged(this);
-            if (DEBUG) Log.d(TAG, "setRequestedVisible: " + requestedVisible);
-        }
+        mController.setRequestedVisibleTypes(requestedVisible ? mType : 0, mType);
+        if (DEBUG) Log.d(TAG, "setRequestedVisible: " + requestedVisible);
         if (applyLocalVisibilityOverride()) {
             mController.notifyVisibilityChanged();
         }
@@ -405,25 +366,26 @@
             return;
         }
 
+        final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
         try (Transaction t = mTransactionSupplier.get()) {
-            if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + mRequestedVisible);
-            if (mRequestedVisible) {
+            if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible);
+            if (requestedVisible) {
                 t.show(mSourceControl.getLeash());
             } else {
                 t.hide(mSourceControl.getLeash());
             }
             // Ensure the alpha value is aligned with the actual requested visibility.
-            t.setAlpha(mSourceControl.getLeash(), mRequestedVisible ? 1 : 0);
+            t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0);
             t.apply();
         }
-        onPerceptible(mRequestedVisible);
+        onPerceptible(requestedVisible);
     }
 
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
         final long token = proto.start(fieldId);
-        proto.write(INTERNAL_INSETS_TYPE, InsetsState.typeToString(mType));
+        proto.write(INTERNAL_INSETS_TYPE, InsetsState.typeToString(mInternalType));
         proto.write(HAS_WINDOW_FOCUS, mHasWindowFocus);
-        proto.write(IS_REQUESTED_VISIBLE, mRequestedVisible);
+        proto.write(IS_REQUESTED_VISIBLE, (mController.getRequestedVisibleTypes() & mType) != 0);
         if (mSourceControl != null) {
             mSourceControl.dumpDebug(proto, SOURCE_CONTROL);
         }
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 9f426a1..a8cc9b6 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -352,7 +352,7 @@
     }
 
     public Insets calculateInsets(Rect frame, @InsetsType int types,
-            InsetsVisibilities overrideVisibilities) {
+            @InsetsType int requestedVisibleTypes) {
         Insets insets = Insets.NONE;
         for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
             InsetsSource source = mSources[type];
@@ -360,10 +360,7 @@
                 continue;
             }
             int publicType = InsetsState.toPublicType(type);
-            if ((publicType & types) == 0) {
-                continue;
-            }
-            if (!overrideVisibilities.getVisibility(type)) {
+            if ((publicType & types & requestedVisibleTypes) == 0) {
                 continue;
             }
             insets = Insets.max(source.calculateInsets(frame, true), insets);
@@ -705,7 +702,7 @@
             result.add(ITYPE_NAVIGATION_BAR);
             result.add(ITYPE_EXTRA_NAVIGATION_BAR);
         }
-        if ((types & Type.GENERIC_OVERLAYS) != 0) {
+        if ((types & Type.SYSTEM_OVERLAYS) != 0) {
             result.add(ITYPE_LEFT_GENERIC_OVERLAY);
             result.add(ITYPE_TOP_GENERIC_OVERLAY);
             result.add(ITYPE_RIGHT_GENERIC_OVERLAY);
@@ -755,7 +752,7 @@
             case ITYPE_TOP_GENERIC_OVERLAY:
             case ITYPE_RIGHT_GENERIC_OVERLAY:
             case ITYPE_BOTTOM_GENERIC_OVERLAY:
-                return Type.GENERIC_OVERLAYS;
+                return Type.SYSTEM_OVERLAYS;
             case ITYPE_CAPTION_BAR:
                 return Type.CAPTION_BAR;
             case ITYPE_IME:
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 9789b56..06c1b25 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -2001,7 +2001,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index ceab310..b3e8fb6 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1294,6 +1294,8 @@
     // NOTE: If you add a new axis here you must also add it to:
     //  frameworks/native/include/android/input.h
     //  frameworks/native/libs/input/InputEventLabels.cpp
+    //  platform/cts/tests/tests/view/src/android/view/cts/MotionEventTest.java
+    //    (testAxisFromToString)
 
     // Symbolic names of all axes.
     private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>();
@@ -1755,7 +1757,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param pointerCount The number of pointers that will be in this event.
@@ -1769,7 +1771,7 @@
      * @param buttonState The state of buttons that are pressed.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
@@ -1777,16 +1779,18 @@
      * @param source The source of this event.
      * @param displayId The display ID associated with this event.
      * @param flags The motion event flags.
+     * @param classification The classification to give this event.
      * @hide
      */
-    static public MotionEvent obtain(long downTime, long eventTime,
+    public static MotionEvent obtain(long downTime, long eventTime,
             int action, int pointerCount, PointerProperties[] pointerProperties,
             PointerCoords[] pointerCoords, int metaState, int buttonState,
             float xPrecision, float yPrecision, int deviceId,
-            int edgeFlags, int source, int displayId, int flags) {
+            int edgeFlags, int source, int displayId, int flags,
+            @Classification int classification) {
         MotionEvent ev = obtain();
         final boolean success = ev.initialize(deviceId, source, displayId, action, flags, edgeFlags,
-                metaState, buttonState, CLASSIFICATION_NONE, 0, 0, xPrecision, yPrecision,
+                metaState, buttonState, classification, 0, 0, xPrecision, yPrecision,
                 downTime * NS_PER_MS, eventTime * NS_PER_MS,
                 pointerCount, pointerProperties, pointerCoords);
         if (!success) {
@@ -1803,7 +1807,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param pointerCount The number of pointers that will be in this event.
@@ -1817,7 +1821,47 @@
      * @param buttonState The state of buttons that are pressed.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
+     * zero indicates that the event didn't come from a physical device; other
+     * numbers are arbitrary and you shouldn't depend on the values.
+     * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
+     * MotionEvent.
+     * @param source The source of this event.
+     * @param displayId The display ID associated with this event.
+     * @param flags The motion event flags.
+     * @hide
+     */
+    public static MotionEvent obtain(long downTime, long eventTime,
+            int action, int pointerCount, PointerProperties[] pointerProperties,
+            PointerCoords[] pointerCoords, int metaState, int buttonState,
+            float xPrecision, float yPrecision, int deviceId,
+            int edgeFlags, int source, int displayId, int flags) {
+        return obtain(downTime, eventTime, action, pointerCount, pointerProperties, pointerCoords,
+                metaState, buttonState, xPrecision, yPrecision, deviceId, edgeFlags, source,
+                displayId, flags, CLASSIFICATION_NONE);
+    }
+
+    /**
+     * Create a new MotionEvent, filling in all of the basic values that
+     * define the motion.
+     *
+     * @param downTime The time (in ms) when the user originally pressed down to start
+     * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
+     * @param eventTime The time (in ms) when this specific event was generated.  This
+     * must be obtained from {@link SystemClock#uptimeMillis()}.
+     * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
+     * @param pointerCount The number of pointers that will be in this event.
+     * @param pointerProperties An array of <em>pointerCount</em> values providing
+     * a {@link PointerProperties} property object for each pointer, which must
+     * include the pointer identifier.
+     * @param pointerCoords An array of <em>pointerCount</em> values providing
+     * a {@link PointerCoords} coordinate object for each pointer.
+     * @param metaState The state of any meta / modifier keys that were in effect when
+     * the event was generated.
+     * @param buttonState The state of buttons that are pressed.
+     * @param xPrecision The precision of the X coordinate being reported.
+     * @param yPrecision The precision of the Y coordinate being reported.
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
@@ -1841,7 +1885,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param pointerCount The number of pointers that will be in this event.
@@ -1853,7 +1897,7 @@
      * the event was generated.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
@@ -1888,7 +1932,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime  The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param x The X coordinate of this event.
@@ -1905,7 +1949,7 @@
      * the event was generated.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
@@ -1925,7 +1969,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime  The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param x The X coordinate of this event.
@@ -1942,7 +1986,7 @@
      * the event was generated.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param source The source of this event.
@@ -1984,7 +2028,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime  The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param pointerCount The number of pointers that are active in this event.
@@ -2002,7 +2046,7 @@
      * the event was generated.
      * @param xPrecision The precision of the X coordinate being reported.
      * @param yPrecision The precision of the Y coordinate being reported.
-     * @param deviceId The id for the device that this event came from.  An id of
+     * @param deviceId The ID for the device that this event came from.  An ID of
      * zero indicates that the event didn't come from a physical device; other
      * numbers are arbitrary and you shouldn't depend on the values.
      * @param edgeFlags A bitfield indicating which edges, if any, were touched by this
@@ -2026,7 +2070,7 @@
      *
      * @param downTime The time (in ms) when the user originally pressed down to start
      * a stream of position events.  This must be obtained from {@link SystemClock#uptimeMillis()}.
-     * @param eventTime  The the time (in ms) when this specific event was generated.  This
+     * @param eventTime The time (in ms) when this specific event was generated.  This
      * must be obtained from {@link SystemClock#uptimeMillis()}.
      * @param action The kind of action being performed, such as {@link #ACTION_DOWN}.
      * @param x The X coordinate of this event.
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index 3fe9110..e8f62fc 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -45,6 +45,7 @@
             = new ArrayList<>();
     private int mCaptionInsetsHeight = 0;
     private WindowInsetsAnimationControlListener mLoggingListener;
+    private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
     @Override
     public void show(int types) {
@@ -52,6 +53,7 @@
             mReplayedInsetsController.show(types);
         } else {
             mRequests.add(new ShowRequest(types));
+            mRequestedVisibleTypes |= types;
         }
     }
 
@@ -61,6 +63,7 @@
             mReplayedInsetsController.hide(types);
         } else {
             mRequests.add(new HideRequest(types));
+            mRequestedVisibleTypes &= ~types;
         }
     }
 
@@ -122,11 +125,11 @@
     }
 
     @Override
-    public boolean isRequestedVisible(int type) {
-
-        // Method is only used once real insets controller is attached, so no need to traverse
-        // requests here.
-        return InsetsState.getDefaultVisibility(type);
+    public @InsetsType int getRequestedVisibleTypes() {
+        if (mReplayedInsetsController != null) {
+            return mReplayedInsetsController.getRequestedVisibleTypes();
+        }
+        return mRequestedVisibleTypes;
     }
 
     @Override
@@ -189,6 +192,7 @@
         mAppearanceMask = 0;
         mAnimationsDisabled = false;
         mLoggingListener = null;
+        mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         // After replaying, we forward everything directly to the replayed instance.
         mReplayedInsetsController = controller;
     }
diff --git a/core/java/android/view/RemoteAnimationDefinition.java b/core/java/android/view/RemoteAnimationDefinition.java
index ea97995..ff282ba 100644
--- a/core/java/android/view/RemoteAnimationDefinition.java
+++ b/core/java/android/view/RemoteAnimationDefinition.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 
 import android.annotation.Nullable;
+import android.annotation.NonNull;
 import android.app.WindowConfiguration.ActivityType;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.IBinder;
@@ -157,7 +158,7 @@
         }
     }
 
-    public static final @android.annotation.NonNull Creator<RemoteAnimationDefinition> CREATOR =
+    public static final @NonNull Creator<RemoteAnimationDefinition> CREATOR =
             new Creator<RemoteAnimationDefinition>() {
         public RemoteAnimationDefinition createFromParcel(Parcel in) {
             return new RemoteAnimationDefinition(in);
@@ -199,18 +200,17 @@
             return 0;
         }
 
-        private static final @android.annotation.NonNull Creator<RemoteAnimationAdapterEntry> CREATOR
-                = new Creator<RemoteAnimationAdapterEntry>() {
+        public static final @NonNull Parcelable.Creator<RemoteAnimationAdapterEntry> CREATOR =
+                new Parcelable.Creator<RemoteAnimationAdapterEntry>() {
+                    @Override
+                    public RemoteAnimationAdapterEntry createFromParcel(Parcel in) {
+                        return new RemoteAnimationAdapterEntry(in);
+                    }
 
-            @Override
-            public RemoteAnimationAdapterEntry createFromParcel(Parcel in) {
-                return new RemoteAnimationAdapterEntry(in);
-            }
-
-            @Override
-            public RemoteAnimationAdapterEntry[] newArray(int size) {
-                return new RemoteAnimationAdapterEntry[size];
-            }
-        };
+                    @Override
+                    public RemoteAnimationAdapterEntry[] newArray(int size) {
+                        return new RemoteAnimationAdapterEntry[size];
+                    }
+                };
     }
 }
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index c46f33a..06851de 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -67,6 +67,7 @@
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Slog;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.Surface.OutOfResourcesException;
@@ -1673,6 +1674,146 @@
         return nativeGetDisplayedContentSample(displayToken, maxFrames, timestamp);
     }
 
+    /**
+     * Information about the min and max refresh rate DM would like to set the display to.
+     * @hide
+     */
+    public static final class RefreshRateRange {
+        public static final String TAG = "RefreshRateRange";
+
+        // The tolerance within which we consider something approximately equals.
+        public static final float FLOAT_TOLERANCE = 0.01f;
+
+        /**
+         * The lowest desired refresh rate.
+         */
+        public float min;
+
+        /**
+         * The highest desired refresh rate.
+         */
+        public float max;
+
+        public RefreshRateRange() {}
+
+        public RefreshRateRange(float min, float max) {
+            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
+                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
+                        + min + " " + max);
+                this.min = this.max = 0;
+                return;
+            }
+            if (min > max) {
+                // Min and max are within epsilon of each other, but in the wrong order.
+                float t = min;
+                min = max;
+                max = t;
+            }
+            this.min = min;
+            this.max = max;
+        }
+
+        /**
+         * Checks whether the two objects have the same values.
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+
+            if (!(other instanceof RefreshRateRange)) {
+                return false;
+            }
+
+            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
+            return (min == refreshRateRange.min && max == refreshRateRange.max);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(min, max);
+        }
+
+        @Override
+        public String toString() {
+            return "(" + min + " " + max + ")";
+        }
+
+        /**
+         * Copies the supplied object's values to this object.
+         */
+        public void copyFrom(RefreshRateRange other) {
+            this.min = other.min;
+            this.max = other.max;
+        }
+    }
+
+    /**
+     * Information about the ranges of refresh rates for the display physical refresh rates and the
+     * render frame rate DM would like to set the policy to.
+     * @hide
+     */
+    public static final class RefreshRateRanges {
+        public static final String TAG = "RefreshRateRanges";
+
+        /**
+         *  The range of refresh rates that the display should run at.
+         */
+        public final RefreshRateRange physical;
+
+        /**
+         *  The range of refresh rates that apps should render at.
+         */
+        public final RefreshRateRange render;
+
+        public RefreshRateRanges() {
+            physical = new RefreshRateRange();
+            render = new RefreshRateRange();
+        }
+
+        public RefreshRateRanges(RefreshRateRange physical, RefreshRateRange render) {
+            this.physical = new RefreshRateRange(physical.min, physical.max);
+            this.render = new RefreshRateRange(render.min, render.max);
+        }
+
+        /**
+         * Checks whether the two objects have the same values.
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+
+            if (!(other instanceof RefreshRateRanges)) {
+                return false;
+            }
+
+            RefreshRateRanges rates = (RefreshRateRanges) other;
+            return physical.equals(rates.physical) && render.equals(
+                    rates.render);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(physical, render);
+        }
+
+        @Override
+        public String toString() {
+            return "physical: " + physical + " render:  " + render;
+        }
+
+        /**
+         * Copies the supplied object's values to this object.
+         */
+        public void copyFrom(RefreshRateRanges other) {
+            this.physical.copyFrom(other.physical);
+            this.render.copyFrom(other.render);
+        }
+    }
+
 
     /**
      * Contains information about desired display configuration.
@@ -1682,44 +1823,49 @@
     public static final class DesiredDisplayModeSpecs {
         public int defaultMode;
         /**
-         * The primary refresh rate range represents display manager's general guidance on the
-         * display configs surface flinger will consider when switching refresh rates. Unless
-         * surface flinger has a specific reason to do otherwise, it will stay within this range.
-         */
-        public float primaryRefreshRateMin;
-        public float primaryRefreshRateMax;
-        /**
-         * The app request refresh rate range allows surface flinger to consider more display
-         * configs when switching refresh rates. Although surface flinger will generally stay within
-         * the primary range, specific considerations, such as layer frame rate settings specified
-         * via the setFrameRate() api, may cause surface flinger to go outside the primary
-         * range. Surface flinger never goes outside the app request range. The app request range
-         * will be greater than or equal to the primary refresh rate range, never smaller.
-         */
-        public float appRequestRefreshRateMin;
-        public float appRequestRefreshRateMax;
-
-        /**
          * If true this will allow switching between modes in different display configuration
          * groups. This way the user may see visual interruptions when the display mode changes.
          */
         public boolean allowGroupSwitching;
 
-        public DesiredDisplayModeSpecs() {}
+        /**
+         * The primary physical and render refresh rate ranges represent display manager's general
+         * guidance on the display configs surface flinger will consider when switching refresh
+         * rates and scheduling the frame rate. Unless surface flinger has a specific reason to do
+         * otherwise, it will stay within this range.
+         */
+        public final RefreshRateRanges primaryRanges;
+
+        /**
+         * The app request physical and render refresh rate ranges allow surface flinger to consider
+         * more display configs when switching refresh rates. Although surface flinger will
+         * generally stay within the primary range, specific considerations, such as layer frame
+         * rate settings specified via the setFrameRate() api, may cause surface flinger to go
+         * outside the primary range. Surface flinger never goes outside the app request range.
+         * The app request range will be greater than or equal to the primary refresh rate range,
+         * never smaller.
+         */
+        public final RefreshRateRanges appRequestRanges;
+
+        public DesiredDisplayModeSpecs() {
+            this.primaryRanges = new RefreshRateRanges();
+            this.appRequestRanges = new RefreshRateRanges();
+        }
 
         public DesiredDisplayModeSpecs(DesiredDisplayModeSpecs other) {
+            this.primaryRanges = new RefreshRateRanges();
+            this.appRequestRanges = new RefreshRateRanges();
             copyFrom(other);
         }
 
         public DesiredDisplayModeSpecs(int defaultMode, boolean allowGroupSwitching,
-                float primaryRefreshRateMin, float primaryRefreshRateMax,
-                float appRequestRefreshRateMin, float appRequestRefreshRateMax) {
+                RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges) {
             this.defaultMode = defaultMode;
             this.allowGroupSwitching = allowGroupSwitching;
-            this.primaryRefreshRateMin = primaryRefreshRateMin;
-            this.primaryRefreshRateMax = primaryRefreshRateMax;
-            this.appRequestRefreshRateMin = appRequestRefreshRateMin;
-            this.appRequestRefreshRateMax = appRequestRefreshRateMax;
+            this.primaryRanges =
+                    new RefreshRateRanges(primaryRanges.physical, primaryRanges.render);
+            this.appRequestRanges =
+                    new RefreshRateRanges(appRequestRanges.physical, appRequestRanges.render);
         }
 
         @Override
@@ -1732,10 +1878,9 @@
          */
         public boolean equals(DesiredDisplayModeSpecs other) {
             return other != null && defaultMode == other.defaultMode
-                    && primaryRefreshRateMin == other.primaryRefreshRateMin
-                    && primaryRefreshRateMax == other.primaryRefreshRateMax
-                    && appRequestRefreshRateMin == other.appRequestRefreshRateMin
-                    && appRequestRefreshRateMax == other.appRequestRefreshRateMax;
+                    && allowGroupSwitching == other.allowGroupSwitching
+                    && primaryRanges.equals(other.primaryRanges)
+                    && appRequestRanges.equals(other.appRequestRanges);
         }
 
         @Override
@@ -1748,18 +1893,17 @@
          */
         public void copyFrom(DesiredDisplayModeSpecs other) {
             defaultMode = other.defaultMode;
-            primaryRefreshRateMin = other.primaryRefreshRateMin;
-            primaryRefreshRateMax = other.primaryRefreshRateMax;
-            appRequestRefreshRateMin = other.appRequestRefreshRateMin;
-            appRequestRefreshRateMax = other.appRequestRefreshRateMax;
+            allowGroupSwitching = other.allowGroupSwitching;
+            primaryRanges.copyFrom(other.primaryRanges);
+            appRequestRanges.copyFrom(other.appRequestRanges);
         }
 
         @Override
         public String toString() {
-            return String.format("defaultConfig=%d primaryRefreshRateRange=[%.0f %.0f]"
-                            + " appRequestRefreshRateRange=[%.0f %.0f]",
-                    defaultMode, primaryRefreshRateMin, primaryRefreshRateMax,
-                    appRequestRefreshRateMin, appRequestRefreshRateMax);
+            return "defaultMode=" + defaultMode
+                    + " allowGroupSwitching=" + allowGroupSwitching
+                    + " primaryRanges=" + primaryRanges
+                    + " appRequestRanges=" + appRequestRanges;
         }
     }
 
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 198ac9d..720813a 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -121,16 +121,23 @@
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_POSITION = false;
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Track {@link SurfaceHolder#addCallback} instead")
     final ArrayList<SurfaceHolder.Callback> mCallbacks = new ArrayList<>();
 
     final int[] mLocation = new int[2];
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@link SurfaceHolder#lockCanvas} instead")
     final ReentrantLock mSurfaceLock = new ReentrantLock();
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@link SurfaceHolder#getSurface} instead")
     final Surface mSurface = new Surface();       // Current surface in use
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Use {@link View#getVisibility} instead")
     boolean mDrawingStopped = true;
     // We use this to track if the application has produced a frame
     // in to the Surface. Up until that point, we should be careful not to punch
@@ -156,13 +163,16 @@
     int mSubLayer = APPLICATION_MEDIA_SUBLAYER;
     int mRequestedSubLayer = APPLICATION_MEDIA_SUBLAYER;
 
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Use {@link SurfaceHolder#isCreating} instead")
     boolean mIsCreating = false;
 
     private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener =
             this::updateSurface;
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Rely on {@link ViewTreeObserver#dispatchOnPreDraw} instead")
     private final ViewTreeObserver.OnPreDrawListener mDrawListener = () -> {
         // reposition ourselves where the surface is
         mHaveFrame = getWidth() > 0 && getHeight() > 0;
@@ -176,24 +186,32 @@
     boolean mViewVisibility = false;
     boolean mWindowStopped = false;
 
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Use {@link View#getWidth} instead")
     int mRequestedWidth = -1;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Use {@link View#getHeight} instead")
     int mRequestedHeight = -1;
     /* Set SurfaceView's format to 565 by default to maintain backward
      * compatibility with applications assuming this format.
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@code SurfaceHolder.Callback#surfaceChanged} instead")
     int mRequestedFormat = PixelFormat.RGB_565;
 
     float mAlpha = 1f;
     boolean mClipSurfaceToBounds;
     int mBackgroundColor = Color.BLACK;
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@link View#getWidth} and {@link View#getHeight} to "
+                    + "determine if the SurfaceView is onscreen and has a frame")
     boolean mHaveFrame = false;
     boolean mSurfaceCreated = false;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Time {@link SurfaceHolder#lockCanvas} instead")
     long mLastLockTime = 0;
 
     boolean mVisible = false;
@@ -202,9 +220,13 @@
     int mSurfaceWidth = -1;
     int mSurfaceHeight = -1;
     float mCornerRadius;
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@code SurfaceHolder.Callback#surfaceChanged} "
+            + "instead")
     int mFormat = -1;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023,
+                         publicAlternatives = "Use {@link SurfaceHolder#getSurfaceFrame} instead")
     final Rect mSurfaceFrame = new Rect();
     int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
     @SurfaceControl.BufferTransform int mTransformHint = 0;
@@ -1051,7 +1073,7 @@
     private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks,
             SyncBufferTransactionCallback syncBufferTransactionCallback) {
 
-        getViewRootImpl().addToSync(syncBufferCallback ->
+        getViewRootImpl().addToSync((parentSyncGroup, syncBufferCallback) ->
                 redrawNeededAsync(callbacks, () -> {
                     Transaction t = null;
                     if (mBlastBufferQueue != null) {
@@ -1059,7 +1081,7 @@
                         t = syncBufferTransactionCallback.waitForTransaction();
                     }
 
-                    syncBufferCallback.onBufferReady(t);
+                    syncBufferCallback.onTransactionReady(t);
                     onDrawFinished();
                 }));
     }
@@ -1070,9 +1092,9 @@
             mSyncGroups.add(syncGroup);
         }
 
-        syncGroup.addToSync(syncBufferCallback -> redrawNeededAsync(callbacks,
-                () -> {
-                    syncBufferCallback.onBufferReady(null);
+        syncGroup.addToSync((parentSyncGroup, syncBufferCallback) ->
+                redrawNeededAsync(callbacks, () -> {
+                    syncBufferCallback.onTransactionReady(null);
                     onDrawFinished();
                     synchronized (mSyncGroups) {
                         mSyncGroups.remove(syncGroup);
@@ -1410,7 +1432,9 @@
      * @return true if the surface has dimensions that are fixed in size
      * @hide
      */
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Track {@link SurfaceHolder#setFixedSize} instead")
     public boolean isFixedSize() {
         return (mRequestedWidth != -1 || mRequestedHeight != -1);
     }
@@ -1446,7 +1470,9 @@
         updateBackgroundColor(t);
     }
 
-    @UnsupportedAppUsage
+    @UnsupportedAppUsage(
+            maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
+            publicAlternatives = "Use {@link SurfaceView#getHolder} instead")
     private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
         private static final String LOG_TAG = "SurfaceHolder";
 
diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java
index a52fc75..00170cb 100644
--- a/core/java/android/view/VelocityTracker.java
+++ b/core/java/android/view/VelocityTracker.java
@@ -28,7 +28,7 @@
 import java.util.Map;
 
 /**
- * Helper for tracking the velocity of touch events, for implementing
+ * Helper for tracking the velocity of motion events, for implementing
  * flinging and other such gestures.
  *
  * Use {@link #obtain} to retrieve a new instance of the class when you are going
@@ -43,6 +43,15 @@
 
     private static final int ACTIVE_POINTER_ID = -1;
 
+    /** @hide */
+    @IntDef(value = {
+            MotionEvent.AXIS_X,
+            MotionEvent.AXIS_Y,
+            MotionEvent.AXIS_SCROLL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VelocityTrackableMotionEventAxis {}
+
     /**
      * Velocity Tracker Strategy: Invalid.
      *
@@ -183,6 +192,7 @@
     private static native float nativeGetVelocity(long ptr, int axis, int id);
     private static native boolean nativeGetEstimator(
             long ptr, int axis, int id, Estimator outEstimator);
+    private static native boolean nativeIsAxisSupported(int axis);
 
     static {
         // Strategy string and IDs mapping lookup.
@@ -305,6 +315,24 @@
     }
 
     /**
+     * Checks whether a given velocity-trackable {@link MotionEvent} axis is supported for velocity
+     * tracking by this {@link VelocityTracker} instance (refer to
+     * {@link #getAxisVelocity(int, int)} for a list of potentially velocity-trackable axes).
+     *
+     * <p>Note that the value returned from this method will stay the same for a given instance, so
+     * a single check for axis support is enough per a {@link VelocityTracker} instance.
+     *
+     * @param axis The axis to check for velocity support.
+     * @return {@code true} if {@code axis} is supported for velocity tracking, or {@code false}
+     * otherwise.
+     * @see #getAxisVelocity(int, int)
+     * @see #getAxisVelocity(int)
+     */
+    public boolean isAxisSupported(@VelocityTrackableMotionEventAxis int axis) {
+        return nativeIsAxisSupported(axis);
+    }
+
+    /**
      * Reset the velocity tracker back to its initial state.
      */
     public void clear() {
@@ -345,7 +373,9 @@
      * {@link #getYVelocity()}.
      *
      * @param units The units you would like the velocity in.  A value of 1
-     * provides pixels per millisecond, 1000 provides pixels per second, etc.
+     * provides units per millisecond, 1000 provides units per second, etc.
+     * Note that the units referred to here are the same units with which motion is reported. For
+     * axes X and Y, the units are pixels.
      * @param maxVelocity The maximum velocity that can be computed by this method.
      * This value must be declared in the same unit as the units parameter. This value
      * must be positive.
@@ -397,6 +427,48 @@
     }
 
     /**
+     * Retrieve the last computed velocity for a given motion axis. You must first call
+     * {@link #computeCurrentVelocity(int)} or {@link #computeCurrentVelocity(int, float)} before
+     * calling this function.
+     *
+     * <p>In addition to {@link MotionEvent#AXIS_X} and {@link MotionEvent#AXIS_Y} which have been
+     * supported since the introduction of this class, the following axes can be candidates for this
+     * method:
+     * <ul>
+     *   <li> {@link MotionEvent#AXIS_SCROLL}: supported starting
+     *        {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+     * </ul>
+     *
+     * <p>Before accessing velocities of an axis using this method, check that your
+     * {@link VelocityTracker} instance supports the axis by using {@link #isAxisSupported(int)}.
+     *
+     * @param axis Which axis' velocity to return.
+     * @param id Which pointer's velocity to return.
+     * @return The previously computed velocity for {@code axis} for pointer ID of {@code id} if
+     * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not supported
+     * for the axis.
+     * @see #isAxisSupported(int)
+     */
+    public float getAxisVelocity(@VelocityTrackableMotionEventAxis int axis, int id) {
+        return nativeGetVelocity(mPtr, axis, id);
+    }
+
+    /**
+     * Equivalent to calling {@link #getAxisVelocity(int, int)} for {@code axis} and the active
+     * pointer.
+     *
+     * @param axis Which axis' velocity to return.
+     * @return The previously computed velocity for {@code axis} for the active pointer if
+     * {@code axis} is supported for velocity tracking, or 0 if velocity tracking is not supported
+     * for the axis.
+     * @see #isAxisSupported(int)
+     * @see #getAxisVelocity(int, int)
+     */
+    public float getAxisVelocity(@VelocityTrackableMotionEventAxis int axis) {
+        return nativeGetVelocity(mPtr, axis, ACTIVE_POINTER_ID);
+    }
+
+    /**
      * Get an estimator for the movements of a pointer using past movements of the
      * pointer to predict future movements.
      *
@@ -426,8 +498,8 @@
      * Past estimated positions are at negative times and future estimated positions
      * are at positive times.
      *
-     * First coefficient is position (in pixels), second is velocity (in pixels per second),
-     * third is acceleration (in pixels per second squared).
+     * First coefficient is position (in units), second is velocity (in units per second),
+     * third is acceleration (in units per second squared).
      *
      * @hide For internal use only.  Not a final API.
      */
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 7b6ebf7..d709840 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5063,6 +5063,13 @@
     private boolean mHoveringTouchDelegate = false;
 
     /**
+     * Configuration for this view to act as a handwriting initiation delegate. This allows
+     * handwriting mode for a delegator editor view to be initiated by stylus movement on this
+     * delegate view.
+     */
+    private HandwritingDelegateConfiguration mHandwritingDelegateConfiguration;
+
+    /**
      * Solid color to use as a background when creating the drawing cache. Enables
      * the cache to use 16 bit bitmaps instead of 32 bit.
      */
@@ -8450,6 +8457,18 @@
      * accurately supplying the semantics of their UI.
      * They should not need to specify what exactly is announced to users.
      *
+     * <p>
+     * In general, only announce transitions and don’t generate a confirmation message for simple
+     * actions like a button press. Label your controls concisely and precisely instead, and for
+     * significant UI changes like window changes, use
+     * {@link android.app.Activity#setTitle(CharSequence)} and
+     * {@link View#setAccessibilityPaneTitle(CharSequence)}.
+     *
+     * <p>
+     * Use {@link View#setAccessibilityLiveRegion(int)} to inform the user of changes to critical
+     * views within the user interface. These should still be used sparingly as they may generate
+     * announcements every time a View is updated.
+     *
      * @param text The announcement text.
      */
     public void announceForAccessibility(CharSequence text) {
@@ -12255,6 +12274,30 @@
     }
 
     /**
+     * Configures this view to act as a handwriting initiation delegate. This allows handwriting
+     * mode for a delegator editor view to be initiated by stylus movement on this delegate view.
+     *
+     * <p>If {@code null} is passed, this view will no longer act as a handwriting initiation
+     * delegate.
+     */
+    public void setHandwritingDelegateConfiguration(
+            @Nullable HandwritingDelegateConfiguration configuration) {
+        mHandwritingDelegateConfiguration = configuration;
+        if (configuration != null) {
+            setHandwritingArea(new Rect(0, 0, getWidth(), getHeight()));
+        }
+    }
+
+    /**
+     * If this view has been configured as a handwriting initiation delegate, returns the delegate
+     * configuration.
+     */
+    @Nullable
+    public HandwritingDelegateConfiguration getHandwritingDelegateConfiguration() {
+        return mHandwritingDelegateConfiguration;
+    }
+
+    /**
      * Gets the coordinates of this view in the coordinate space of the
      * {@link Surface} that contains the view.
      *
@@ -17403,7 +17446,7 @@
      * (but after its own view has been drawn).
      * @param canvas the canvas on which to draw the view
      */
-    protected void dispatchDraw(Canvas canvas) {
+    protected void dispatchDraw(@NonNull Canvas canvas) {
 
     }
 
@@ -20688,7 +20731,7 @@
         out.bottom = mScrollY + mBottom - mTop;
     }
 
-    private void onDrawScrollIndicators(Canvas c) {
+    private void onDrawScrollIndicators(@NonNull Canvas c) {
         if ((mPrivateFlags3 & SCROLL_INDICATORS_PFLAG3_MASK) == 0) {
             // No scroll indicators enabled.
             return;
@@ -20872,7 +20915,7 @@
      *
      * @see #awakenScrollBars(int)
      */
-    protected final void onDrawScrollBars(Canvas canvas) {
+    protected final void onDrawScrollBars(@NonNull Canvas canvas) {
         // scrollbars are drawn only when the animation is running
         final ScrollabilityCache cache = mScrollCache;
 
@@ -20984,7 +21027,7 @@
      * @hide
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar,
+    protected void onDrawHorizontalScrollBar(@NonNull Canvas canvas, Drawable scrollBar,
             int l, int t, int r, int b) {
         scrollBar.setBounds(l, t, r, b);
         scrollBar.draw(canvas);
@@ -21004,7 +21047,7 @@
      * @hide
      */
     @UnsupportedAppUsage
-    protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+    protected void onDrawVerticalScrollBar(@NonNull Canvas canvas, Drawable scrollBar,
             int l, int t, int r, int b) {
         scrollBar.setBounds(l, t, r, b);
         scrollBar.draw(canvas);
@@ -21015,7 +21058,7 @@
      *
      * @param canvas the canvas on which the background will be drawn
      */
-    protected void onDraw(Canvas canvas) {
+    protected void onDraw(@NonNull Canvas canvas) {
     }
 
     /*
@@ -23130,7 +23173,7 @@
      *
      * @hide
      */
-    protected final boolean drawsWithRenderNode(Canvas canvas) {
+    protected final boolean drawsWithRenderNode(@NonNull Canvas canvas) {
         return mAttachInfo != null
                 && mAttachInfo.mHardwareAccelerated
                 && canvas.isHardwareAccelerated();
@@ -23142,7 +23185,7 @@
      * This is where the View specializes rendering behavior based on layer type,
      * and hardware acceleration.
      */
-    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
+    boolean draw(@NonNull Canvas canvas, ViewGroup parent, long drawingTime) {
 
         final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
 
@@ -23430,7 +23473,7 @@
         return (int) (dips * scale + 0.5f);
     }
 
-    final private void debugDrawFocus(Canvas canvas) {
+    private void debugDrawFocus(@NonNull Canvas canvas) {
         if (isFocused()) {
             final int cornerSquareSize = dipsToPixels(DEBUG_CORNERS_SIZE_DIP);
             final int l = mScrollX;
@@ -23465,7 +23508,7 @@
      * @param canvas The Canvas to which the View is rendered.
      */
     @CallSuper
-    public void draw(Canvas canvas) {
+    public void draw(@NonNull Canvas canvas) {
         final int privateFlags = mPrivateFlags;
         mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
 
@@ -23700,7 +23743,7 @@
      * @param canvas Canvas on which to draw the background
      */
     @UnsupportedAppUsage
-    private void drawBackground(Canvas canvas) {
+    private void drawBackground(@NonNull Canvas canvas) {
         final Drawable background = mBackground;
         if (background == null) {
             return;
@@ -24205,7 +24248,7 @@
             }
         }
         rebuildOutline();
-        if (onCheckIsTextEditor()) {
+        if (onCheckIsTextEditor() || mHandwritingDelegateConfiguration != null) {
             setHandwritingArea(new Rect(0, 0, newWidth, newHeight));
         }
     }
@@ -24600,7 +24643,7 @@
      * Draw the default focus highlight onto the canvas if there is one and this view is focused.
      * @param canvas the canvas where we're drawing the highlight.
      */
-    private void drawDefaultFocusHighlight(Canvas canvas) {
+    private void drawDefaultFocusHighlight(@NonNull Canvas canvas) {
         if (mDefaultFocusHighlight != null && isFocused()) {
             if (mDefaultFocusHighlightSizeChanged) {
                 mDefaultFocusHighlightSizeChanged = false;
@@ -25398,7 +25441,7 @@
      *
      * @param canvas canvas to draw into
      */
-    public void onDrawForeground(Canvas canvas) {
+    public void onDrawForeground(@NonNull Canvas canvas) {
         onDrawScrollIndicators(canvas);
         onDrawScrollBars(canvas);
 
@@ -27456,7 +27499,7 @@
          *
          * @param canvas A {@link android.graphics.Canvas} object in which to draw the shadow image.
          */
-        public void onDrawShadow(Canvas canvas) {
+        public void onDrawShadow(@NonNull Canvas canvas) {
             final View view = mView.get();
             if (view != null) {
                 view.draw(canvas);
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index e5a535b..58aee61 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -218,6 +218,11 @@
     private static final int WINDOW_TOUCH_SLOP = 16;
 
     /**
+     * Margin in dips around text line bounds where stylus handwriting gestures should be supported.
+     */
+    private static final int HANDWRITING_GESTURE_LINE_MARGIN = 16;
+
+    /**
      * Minimum velocity to initiate a fling, as measured in dips per second
      */
     private static final int MINIMUM_FLING_VELOCITY = 50;
@@ -338,6 +343,7 @@
     private final int mPagingTouchSlop;
     private final int mDoubleTapSlop;
     private final int mWindowTouchSlop;
+    private final int mHandwritingGestureLineMargin;
     private final float mAmbiguousGestureMultiplier;
     private final int mMaximumDrawingCacheSize;
     private final int mOverscrollDistance;
@@ -381,6 +387,7 @@
         mPagingTouchSlop = PAGING_TOUCH_SLOP;
         mDoubleTapSlop = DOUBLE_TAP_SLOP;
         mWindowTouchSlop = WINDOW_TOUCH_SLOP;
+        mHandwritingGestureLineMargin = HANDWRITING_GESTURE_LINE_MARGIN;
         mAmbiguousGestureMultiplier = AMBIGUOUS_GESTURE_MULTIPLIER;
         //noinspection deprecation
         mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
@@ -490,6 +497,9 @@
 
         mDoubleTapTouchSlop = mTouchSlop;
 
+        mHandwritingGestureLineMargin = res.getDimensionPixelSize(
+                com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
+
         mMinimumFlingVelocity = res.getDimensionPixelSize(
                 com.android.internal.R.dimen.config_viewMinFlingVelocity);
         mMaximumFlingVelocity = res.getDimensionPixelSize(
@@ -796,6 +806,14 @@
     }
 
     /**
+     * @return margin in pixels around text line bounds where stylus handwriting gestures should be
+     *     supported.
+     */
+    public int getScaledHandwritingGestureLineMargin() {
+        return mHandwritingGestureLineMargin;
+    }
+
+    /**
      * Interval for dispatching a recurring accessibility event in milliseconds.
      * This interval guarantees that a recurring event will be send at most once
      * during the {@link #getSendRecurringAccessibilityEventsInterval()} time frame.
@@ -804,6 +822,7 @@
      *
      * @hide
      */
+    @TestApi
     public static long getSendRecurringAccessibilityEventsInterval() {
         return SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS;
     }
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index c4f20dc..46b2cfc 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -4166,7 +4166,7 @@
     /**
      * @hide
      */
-    protected void onDebugDrawMargins(Canvas canvas, Paint paint) {
+    protected void onDebugDrawMargins(@NonNull Canvas canvas, Paint paint) {
         for (int i = 0; i < getChildCount(); i++) {
             View c = getChildAt(i);
             c.getLayoutParams().onDebugDraw(c, canvas, paint);
@@ -4176,7 +4176,7 @@
     /**
      * @hide
      */
-    protected void onDebugDraw(Canvas canvas) {
+    protected void onDebugDraw(@NonNull Canvas canvas) {
         Paint paint = getDebugPaint();
 
         // Draw optical bounds
@@ -4224,7 +4224,7 @@
     }
 
     @Override
-    protected void dispatchDraw(Canvas canvas) {
+    protected void dispatchDraw(@NonNull Canvas canvas) {
         final int childrenCount = mChildrenCount;
         final View[] children = mChildren;
         int flags = mGroupFlags;
@@ -4533,7 +4533,7 @@
      * @param drawingTime The time at which draw is occurring
      * @return True if an invalidate() was issued
      */
-    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+    protected boolean drawChild(@NonNull Canvas canvas, View child, long drawingTime) {
         return child.draw(canvas, this, drawingTime);
     }
 
@@ -9208,7 +9208,8 @@
         }
     }
 
-    private static void drawRect(Canvas canvas, Paint paint, int x1, int y1, int x2, int y2) {
+    private static void drawRect(@NonNull Canvas canvas, Paint paint, int x1, int y1,
+                                 int x2, int y2) {
         if (sDebugLines== null) {
             // TODO: This won't work with multiple UI threads in a single process
             sDebugLines = new float[16];
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 58c8126..5e1dc34 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -88,6 +88,7 @@
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
 
 import android.Manifest;
+import android.accessibilityservice.AccessibilityService;
 import android.animation.AnimationHandler;
 import android.animation.LayoutTransition;
 import android.annotation.AnyThread;
@@ -162,7 +163,6 @@
 import android.util.TypedValue;
 import android.util.proto.ProtoOutputStream;
 import android.view.InputDevice.InputSourceClass;
-import android.view.InsetsState.InternalInsetsType;
 import android.view.Surface.OutOfResourcesException;
 import android.view.SurfaceControl.Transaction;
 import android.view.View.AttachInfo;
@@ -193,12 +193,14 @@
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.ContentCaptureSession;
 import android.view.contentcapture.MainContentCaptureSession;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Scroller;
 import android.window.ClientWindowFrames;
 import android.window.CompatOnBackInvokedCallback;
 import android.window.OnBackInvokedCallback;
 import android.window.OnBackInvokedDispatcher;
+import android.window.ScreenCapture;
 import android.window.SurfaceSyncGroup;
 import android.window.WindowOnBackInvokedDispatcher;
 
@@ -229,6 +231,7 @@
 import java.util.Objects;
 import java.util.Queue;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 
 /**
  * The top of a view hierarchy, implementing the needed protocol between View
@@ -428,8 +431,6 @@
     final DisplayManager mDisplayManager;
     final String mBasePackageName;
 
-    private @Surface.Rotation int mDisplayInstallOrientation;
-
     final int[] mTmpLocation = new int[2];
 
     final TypedValue mTmpValue = new TypedValue();
@@ -714,7 +715,7 @@
     private final InsetsState mTempInsets = new InsetsState();
     private final InsetsSourceControl[] mTempControls = new InsetsSourceControl[SIZE];
     private final WindowConfiguration mTempWinConfig = new WindowConfiguration();
-    private float mInvSizeCompatScale = 1f;
+    private float mInvCompatScale = 1f;
     final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets
             = new ViewTreeObserver.InternalInsetsInfo();
 
@@ -851,7 +852,7 @@
     }
 
     private SurfaceSyncGroup mSyncGroup;
-    private SurfaceSyncGroup.SyncBufferCallback mSyncBufferCallback;
+    private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback;
     private int mNumSyncsInProgress = 0;
 
     private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
@@ -890,20 +891,18 @@
     static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback =
         new BLASTBufferQueue.TransactionHangCallback() {
             @Override
-            public void onTransactionHang(boolean isGPUHang) {
-                if (isGPUHang && !sAnrReported) {
-                    sAnrReported = true;
-                    try {
-                        ActivityManager.getService().appNotResponding(
-                            "Buffer processing hung up due to stuck fence. Indicates GPU hang");
-                    } catch (RemoteException e) {
-                        // We asked the system to crash us, but the system
-                        // already crashed. Unfortunately things may be
-                        // out of control.
-                    }
-                } else {
-                    // TODO: Do something with this later. For now we just ANR
-                    // in dequeue buffer later like we always have.
+            public void onTransactionHang(String reason) {
+                if (sAnrReported) {
+                    return;
+                }
+
+                sAnrReported = true;
+                try {
+                    ActivityManager.getService().appNotResponding(reason);
+                } catch (RemoteException e) {
+                    // We asked the system to crash us, but the system
+                    // already crashed. Unfortunately things may be
+                    // out of control.
                 }
             }
         };
@@ -1110,11 +1109,11 @@
 
     private WindowConfiguration getCompatWindowConfiguration() {
         final WindowConfiguration winConfig = getConfiguration().windowConfiguration;
-        if (mInvSizeCompatScale == 1f) {
+        if (mInvCompatScale == 1f) {
             return winConfig;
         }
         mTempWinConfig.setTo(winConfig);
-        mTempWinConfig.scale(mInvSizeCompatScale);
+        mTempWinConfig.scale(mInvCompatScale);
         return mTempWinConfig;
     }
 
@@ -1134,7 +1133,6 @@
             if (mView == null) {
                 mView = view;
 
-                mDisplayInstallOrientation = mDisplay.getInstallOrientation();
                 mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                 mFallbackEventHandler.setView(view);
                 mWindowAttributes.copyFrom(attrs);
@@ -1247,11 +1245,11 @@
                     controlInsetsForCompatibility(mWindowAttributes);
 
                     Rect attachedFrame = new Rect();
-                    final float[] sizeCompatScale = { 1f };
+                    final float[] compatScale = { 1f };
                     res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                             getHostVisibility(), mDisplay.getDisplayId(), userId,
-                            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
-                            mTempControls, attachedFrame, sizeCompatScale);
+                            mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
+                            mTempControls, attachedFrame, compatScale);
                     if (!attachedFrame.isValid()) {
                         attachedFrame = null;
                     }
@@ -1261,9 +1259,9 @@
                         mTranslator.translateRectInScreenToAppWindow(attachedFrame);
                     }
                     mTmpFrames.attachedFrame = attachedFrame;
-                    mTmpFrames.sizeCompatScale = sizeCompatScale[0];
-                    mInvSizeCompatScale = 1f / sizeCompatScale[0];
-                } catch (RemoteException e) {
+                    mTmpFrames.compatScale = compatScale[0];
+                    mInvCompatScale = 1f / compatScale[0];
+                } catch (RemoteException | RuntimeException e) {
                     mAdded = false;
                     mView = null;
                     mAttachInfo.mRootView = null;
@@ -1289,7 +1287,7 @@
                 mWindowLayout.computeFrames(mWindowAttributes, state,
                         displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
                         UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
-                        mInsetsController.getRequestedVisibilities(), 1f /* compactScale */,
+                        mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */,
                         mTmpFrames);
                 setFrame(mTmpFrames.frame);
                 registerBackCallbackOnWindow();
@@ -1793,24 +1791,24 @@
             mTranslator.translateRectInScreenToAppWindow(displayFrame);
             mTranslator.translateRectInScreenToAppWindow(attachedFrame);
         }
-        final float sizeCompatScale = frames.sizeCompatScale;
+        final float compatScale = frames.compatScale;
         final boolean frameChanged = !mWinFrame.equals(frame);
         final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
         final boolean attachedFrameChanged = LOCAL_LAYOUT
                 && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
         final boolean displayChanged = mDisplay.getDisplayId() != displayId;
         final boolean resizeModeChanged = mResizeMode != resizeMode;
-        final boolean sizeCompatScaleChanged = mTmpFrames.sizeCompatScale != sizeCompatScale;
+        final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale;
         if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged
                 && !displayChanged && !resizeModeChanged && !forceNextWindowRelayout
-                && !sizeCompatScaleChanged) {
+                && !compatScaleChanged) {
             return;
         }
 
         mPendingDragResizing = resizeMode != RESIZE_MODE_INVALID;
         mResizeMode = resizeMode;
-        mTmpFrames.sizeCompatScale = sizeCompatScale;
-        mInvSizeCompatScale = 1f / sizeCompatScale;
+        mTmpFrames.compatScale = compatScale;
+        mInvCompatScale = 1f / compatScale;
 
         if (configChanged) {
             // If configuration changed - notify about that and, maybe, about move to display.
@@ -1905,7 +1903,6 @@
         updateInternalDisplay(displayId, mView.getResources());
         mImeFocusController.onMovedToDisplay();
         mAttachInfo.mDisplayState = mDisplay.getState();
-        mDisplayInstallOrientation = mDisplay.getInstallOrientation();
         // Internal state updated, now notify the view hierarchy.
         mView.dispatchMovedToDisplay(mDisplay, config);
     }
@@ -2382,7 +2379,7 @@
             mCompatibleVisibilityInfo.globalVisibility =
                     (mCompatibleVisibilityInfo.globalVisibility & ~View.SYSTEM_UI_FLAG_LOW_PROFILE)
                             | (mAttachInfo.mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE);
-            dispatchDispatchSystemUiVisibilityChanged(mCompatibleVisibilityInfo);
+            dispatchDispatchSystemUiVisibilityChanged();
             if (mAttachInfo.mKeepScreenOn != oldScreenOn
                     || mAttachInfo.mSystemUiVisibility != params.subtreeSystemUiVisibility
                     || mAttachInfo.mHasSystemUiListeners != params.hasSystemUiListeners) {
@@ -2410,24 +2407,29 @@
 
     /**
      * Update the compatible system UI visibility for dispatching it to the legacy app.
-     *
-     * @param type Indicates which type of the insets source we are handling.
-     * @param visible True if the insets source is visible.
-     * @param hasControl True if we can control the insets source.
      */
-    void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible,
-            boolean hasControl) {
-        @InsetsType final int publicType = InsetsState.toPublicType(type);
-        if (publicType != Type.statusBars() && publicType != Type.navigationBars()) {
-            return;
-        }
+    void updateCompatSysUiVisibility(@InsetsType int visibleTypes,
+            @InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) {
+        // If a type is controllable, the visibility is overridden by the requested visibility.
+        visibleTypes =
+                (requestedVisibleTypes & controllableTypes) | (visibleTypes & ~controllableTypes);
+
+        updateCompatSystemUiVisibilityInfo(SYSTEM_UI_FLAG_FULLSCREEN, Type.statusBars(),
+                visibleTypes, controllableTypes);
+        updateCompatSystemUiVisibilityInfo(SYSTEM_UI_FLAG_HIDE_NAVIGATION, Type.navigationBars(),
+                visibleTypes, controllableTypes);
+        dispatchDispatchSystemUiVisibilityChanged();
+    }
+
+    private void updateCompatSystemUiVisibilityInfo(int systemUiFlag, @InsetsType int insetsType,
+            @InsetsType int visibleTypes, @InsetsType int controllableTypes) {
         final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo;
-        final int systemUiFlag = publicType == Type.statusBars()
-                ? View.SYSTEM_UI_FLAG_FULLSCREEN
-                : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
-        if (visible) {
+        final boolean willBeVisible = (visibleTypes & insetsType) != 0;
+        final boolean hasControl = (controllableTypes & insetsType) != 0;
+        final boolean wasInvisible = (mAttachInfo.mSystemUiVisibility & systemUiFlag) != 0;
+        if (willBeVisible) {
             info.globalVisibility &= ~systemUiFlag;
-            if (hasControl && (mAttachInfo.mSystemUiVisibility & systemUiFlag) != 0) {
+            if (hasControl && wasInvisible) {
                 // The local system UI visibility can only be cleared while we have the control.
                 info.localChanges |= systemUiFlag;
             }
@@ -2435,7 +2437,6 @@
             info.globalVisibility |= systemUiFlag;
             info.localChanges &= ~systemUiFlag;
         }
-        dispatchDispatchSystemUiVisibilityChanged(info);
     }
 
     /**
@@ -2451,25 +2452,28 @@
                 && (info.globalVisibility & SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
             info.globalVisibility &= ~SYSTEM_UI_FLAG_LOW_PROFILE;
             info.localChanges |= SYSTEM_UI_FLAG_LOW_PROFILE;
-            dispatchDispatchSystemUiVisibilityChanged(info);
+            dispatchDispatchSystemUiVisibilityChanged();
         }
     }
 
-    private void dispatchDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
-        if (mDispatchedSystemUiVisibility != args.globalVisibility) {
+    private void dispatchDispatchSystemUiVisibilityChanged() {
+        if (mDispatchedSystemUiVisibility != mCompatibleVisibilityInfo.globalVisibility) {
             mHandler.removeMessages(MSG_DISPATCH_SYSTEM_UI_VISIBILITY);
-            mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY, args));
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY));
         }
     }
 
-    private void handleDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
-        if (mView == null) return;
-        if (args.localChanges != 0) {
-            mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges);
-            args.localChanges = 0;
+    private void handleDispatchSystemUiVisibilityChanged() {
+        if (mView == null) {
+            return;
+        }
+        final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo;
+        if (info.localChanges != 0) {
+            mView.updateLocalSystemUiVisibility(info.localValue, info.localChanges);
+            info.localChanges = 0;
         }
 
-        final int visibility = args.globalVisibility & View.SYSTEM_UI_CLEARABLE_FLAGS;
+        final int visibility = info.globalVisibility & View.SYSTEM_UI_CLEARABLE_FLAGS;
         if (mDispatchedSystemUiVisibility != visibility) {
             mDispatchedSystemUiVisibility = visibility;
             mView.dispatchSystemUiVisibilityChanged(visibility);
@@ -3607,8 +3611,8 @@
                 mPendingTransitions.clear();
             }
 
-            if (mSyncBufferCallback != null) {
-                mSyncBufferCallback.onBufferReady(null);
+            if (mTransactionReadyCallback != null) {
+                mTransactionReadyCallback.onTransactionReady(null);
             }
         } else if (cancelAndRedraw) {
             mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener
@@ -3623,8 +3627,8 @@
                 }
                 mPendingTransitions.clear();
             }
-            if (!performDraw() && mSyncBufferCallback != null) {
-                mSyncBufferCallback.onBufferReady(null);
+            if (!performDraw() && mTransactionReadyCallback != null) {
+                mTransactionReadyCallback.onTransactionReady(null);
             }
         }
 
@@ -3638,7 +3642,7 @@
         if (!cancelAndRedraw) {
             mReportNextDraw = false;
             mLastReportNextDrawReason = null;
-            mSyncBufferCallback = null;
+            mTransactionReadyCallback = null;
             mSyncBuffer = false;
             if (isInLocalSync()) {
                 mSyncGroup.markSyncReady();
@@ -4385,7 +4389,7 @@
             return false;
         }
 
-        final boolean fullRedrawNeeded = mFullRedrawNeeded || mSyncBufferCallback != null;
+        final boolean fullRedrawNeeded = mFullRedrawNeeded || mTransactionReadyCallback != null;
         mFullRedrawNeeded = false;
 
         mIsDrawing = true;
@@ -4393,9 +4397,9 @@
 
         addFrameCommitCallbackIfNeeded();
 
-        boolean usingAsyncReport = isHardwareEnabled() && mSyncBufferCallback != null;
+        boolean usingAsyncReport = isHardwareEnabled() && mTransactionReadyCallback != null;
         if (usingAsyncReport) {
-            registerCallbacksForSync(mSyncBuffer, mSyncBufferCallback);
+            registerCallbacksForSync(mSyncBuffer, mTransactionReadyCallback);
         } else if (mHasPendingTransactions) {
             // These callbacks are only needed if there's no sync involved and there were calls to
             // applyTransactionOnDraw. These callbacks check if the draw failed for any reason and
@@ -4446,10 +4450,11 @@
             }
 
             if (mSurfaceHolder != null && mSurface.isValid()) {
-                final SurfaceSyncGroup.SyncBufferCallback syncBufferCallback = mSyncBufferCallback;
+                final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback =
+                        mTransactionReadyCallback;
                 SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() ->
-                        mHandler.post(() -> syncBufferCallback.onBufferReady(null)));
-                mSyncBufferCallback = null;
+                        mHandler.post(() -> transactionReadyCallback.onTransactionReady(null)));
+                mTransactionReadyCallback = null;
 
                 SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
 
@@ -4460,8 +4465,8 @@
                 }
             }
         }
-        if (mSyncBufferCallback != null && !usingAsyncReport) {
-            mSyncBufferCallback.onBufferReady(null);
+        if (mTransactionReadyCallback != null && !usingAsyncReport) {
+            mTransactionReadyCallback.onTransactionReady(null);
         }
         if (mPerformContentCapture) {
             performContentCaptureInitialReport();
@@ -4970,7 +4975,7 @@
     }
 
     void reportKeepClearAreasChanged() {
-        if (!mHasPendingKeepClearAreaChange) {
+        if (!mHasPendingKeepClearAreaChange || mView == null) {
             return;
         }
         mHasPendingKeepClearAreaChange = false;
@@ -5649,17 +5654,23 @@
                     break;
                 }
                 case MSG_SHOW_INSETS: {
+                    final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_CLIENT_HANDLE_SHOW_INSETS);
                     if (mView == null) {
                         Log.e(TAG,
                                 String.format("Calling showInsets(%d,%b) on window that no longer"
                                         + " has views.", msg.arg1, msg.arg2 == 1));
                     }
                     clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
-                    mInsetsController.show(msg.arg1, msg.arg2 == 1);
+                    mInsetsController.show(msg.arg1, msg.arg2 == 1, statsToken);
                     break;
                 }
                 case MSG_HIDE_INSETS: {
-                    mInsetsController.hide(msg.arg1, msg.arg2 == 1);
+                    final ImeTracker.Token statsToken = (ImeTracker.Token) msg.obj;
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_CLIENT_HANDLE_HIDE_INSETS);
+                    mInsetsController.hide(msg.arg1, msg.arg2 == 1, statsToken);
                     break;
                 }
                 case MSG_WINDOW_MOVED:
@@ -5718,7 +5729,7 @@
                     enqueueInputEvent(event, null, 0, true);
                 } break;
                 case MSG_CHECK_FOCUS: {
-                    getImeFocusController().checkFocus(false, true);
+                    getImeFocusController().onScheduledCheckFocus();
                 } break;
                 case MSG_CLOSE_SYSTEM_DIALOGS: {
                     if (mView != null) {
@@ -5734,7 +5745,7 @@
                     handleDragEvent(event);
                 } break;
                 case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: {
-                    handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj);
+                    handleDispatchSystemUiVisibilityChanged();
                 } break;
                 case MSG_UPDATE_CONFIGURATION: {
                     Configuration config = (Configuration) msg.obj;
@@ -8154,7 +8165,7 @@
             state.getDisplayCutoutSafe(displayCutoutSafe);
             mWindowLayout.computeFrames(mWindowAttributes.forRotation(winConfig.getRotation()),
                     state, displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
-                    measuredWidth, measuredHeight, mInsetsController.getRequestedVisibilities(),
+                    measuredWidth, measuredHeight, mInsetsController.getRequestedVisibleTypes(),
                     1f /* compatScale */, mTmpFrames);
             mWinFrameInScreen.set(mTmpFrames.frame);
             if (mTranslator != null) {
@@ -8225,7 +8236,7 @@
                 mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
                 mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
             }
-            mInvSizeCompatScale = 1f / mTmpFrames.sizeCompatScale;
+            mInvCompatScale = 1f / mTmpFrames.compatScale;
             CompatibilityInfo.applyOverrideScaleIfNeeded(mPendingMergedConfiguration);
             mInsetsController.onStateChanged(mTempInsets);
             mInsetsController.onControlsChanged(mTempControls);
@@ -8235,7 +8246,7 @@
         }
 
         final int transformHint = SurfaceControl.rotationToBufferTransform(
-                (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
 
         WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth,
                 requestedHeight, mWinFrameInScreen, mPendingDragResizing, mSurfaceSize);
@@ -8260,7 +8271,7 @@
         }
 
         mLastTransformHint = transformHint;
-      
+
         mSurfaceControl.setTransformHint(transformHint);
 
         if (mAttachInfo.mContentCaptureManager != null) {
@@ -8811,12 +8822,14 @@
         mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
     }
 
-    private void showInsets(@InsetsType int types, boolean fromIme) {
-        mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+    private void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
+        mHandler.obtainMessage(MSG_SHOW_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
     }
 
-    private void hideInsets(@InsetsType int types, boolean fromIme) {
-        mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0).sendToTarget();
+    private void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
+        mHandler.obtainMessage(MSG_HIDE_INSETS, types, fromIme ? 1 : 0, statsToken).sendToTarget();
     }
 
     public void dispatchMoved(int newX, int newY) {
@@ -10180,7 +10193,8 @@
         }
 
         @Override
-        public void showInsets(@InsetsType int types, boolean fromIme) {
+        public void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (fromIme) {
                 ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#showInsets",
@@ -10188,13 +10202,16 @@
                         null /* icProto */);
             }
             if (viewAncestor != null) {
-                viewAncestor.showInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
+                viewAncestor.showInsets(types, fromIme, statsToken);
+            } else {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_SHOW_INSETS);
             }
         }
 
         @Override
-        public void hideInsets(@InsetsType int types, boolean fromIme) {
-
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (fromIme) {
                 ImeTracing.getInstance().triggerClientDump("ViewRootImpl.W#hideInsets",
@@ -10202,7 +10219,10 @@
                         null /* icProto */);
             }
             if (viewAncestor != null) {
-                viewAncestor.hideInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
+                viewAncestor.hideInsets(types, fromIme, statsToken);
+            } else {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_HIDE_INSETS);
             }
         }
 
@@ -10691,6 +10711,25 @@
                         .notifyOutsideTouchClientThread();
             }
         }
+
+        @Override
+        public void takeScreenshotOfWindow(int interactionId,
+                ScreenCapture.ScreenCaptureListener listener,
+                IAccessibilityInteractionConnectionCallback callback) {
+            ViewRootImpl viewRootImpl = mViewRootImpl.get();
+            if (viewRootImpl != null && viewRootImpl.mView != null) {
+                viewRootImpl.getAccessibilityInteractionController()
+                        .takeScreenshotOfWindowClientThread(interactionId, listener, callback);
+            } else {
+                try {
+                    callback.sendTakeScreenshotOfWindowError(
+                            AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
+                            interactionId);
+                } catch (RemoteException re) {
+                    /* best effort - ignore */
+                }
+            }
+        }
     }
 
     /**
@@ -11097,7 +11136,7 @@
     }
 
     private void registerCallbacksForSync(boolean syncBuffer,
-            final SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) {
+            final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
         if (!isHardwareEnabled()) {
             return;
         }
@@ -11124,7 +11163,7 @@
                 // pendingDrawFinished.
                 if ((syncResult
                         & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
-                    syncBufferCallback.onBufferReady(
+                    transactionReadyCallback.onTransactionReady(
                             mBlastBufferQueue.gatherPendingTransactions(frame));
                     return null;
                 }
@@ -11134,7 +11173,8 @@
                 }
 
                 if (syncBuffer) {
-                    mBlastBufferQueue.syncNextTransaction(syncBufferCallback::onBufferReady);
+                    mBlastBufferQueue.syncNextTransaction(
+                            transactionReadyCallback::onTransactionReady);
                 }
 
                 return didProduceBuffer -> {
@@ -11154,7 +11194,7 @@
                         // since the frame didn't draw on this vsync. It's possible the frame will
                         // draw later, but it's better to not be sync than to block on a frame that
                         // may never come.
-                        syncBufferCallback.onBufferReady(
+                        transactionReadyCallback.onTransactionReady(
                                 mBlastBufferQueue.gatherPendingTransactions(frame));
                         return;
                     }
@@ -11163,22 +11203,49 @@
                     // syncNextTransaction callback. Instead, just report back to the Syncer so it
                     // knows that this sync request is complete.
                     if (!syncBuffer) {
-                        syncBufferCallback.onBufferReady(null);
+                        transactionReadyCallback.onTransactionReady(null);
                     }
                 };
             }
         });
     }
 
+    private final Executor mPostAtFrontExecutor = new Executor() {
+        @Override
+        public void execute(Runnable command) {
+            mHandler.postAtFrontOfQueue(command);
+        }
+    };
+
     public final SurfaceSyncGroup.SyncTarget mSyncTarget = new SurfaceSyncGroup.SyncTarget() {
         @Override
-        public void onReadyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) {
-            readyToSync(syncBufferCallback);
+        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
+                SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
+            updateSyncInProgressCount(parentSyncGroup);
+            if (!isInLocalSync()) {
+                // Always sync the buffer if the sync request did not come from VRI.
+                mSyncBuffer = true;
+            }
+            if (mAttachInfo.mThreadedRenderer != null) {
+                HardwareRenderer.setRtAnimationsEnabled(false);
+            }
+
+            if (mTransactionReadyCallback != null) {
+                Log.d(mTag, "Already set sync for the next draw.");
+                mTransactionReadyCallback.onTransactionReady(null);
+            }
+            if (DEBUG_BLAST) {
+                Log.d(mTag, "Setting syncFrameCallback");
+            }
+            mTransactionReadyCallback = transactionReadyCallback;
+            if (!mIsInTraversal && !mTraversalScheduled) {
+                scheduleTraversals();
+            }
         }
 
-        @Override
-        public void onSyncComplete() {
-            mHandler.postAtFrontOfQueue(() -> {
+        private void updateSyncInProgressCount(SurfaceSyncGroup parentSyncGroup) {
+            mNumSyncsInProgress++;
+            parentSyncGroup.addSyncCompleteCallback(mPostAtFrontExecutor, () -> {
                 if (--mNumSyncsInProgress == 0 && mAttachInfo.mThreadedRenderer != null) {
                     HardwareRenderer.setRtAnimationsEnabled(true);
                 }
@@ -11191,29 +11258,6 @@
         return mSyncTarget;
     }
 
-    private void readyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) {
-        mNumSyncsInProgress++;
-        if (!isInLocalSync()) {
-            // Always sync the buffer if the sync request did not come from VRI.
-            mSyncBuffer = true;
-        }
-        if (mAttachInfo.mThreadedRenderer != null) {
-            HardwareRenderer.setRtAnimationsEnabled(false);
-        }
-
-        if (mSyncBufferCallback != null) {
-            Log.d(mTag, "Already set sync for the next draw.");
-            mSyncBufferCallback.onBufferReady(null);
-        }
-        if (DEBUG_BLAST) {
-            Log.d(mTag, "Setting syncFrameCallback");
-        }
-        mSyncBufferCallback = syncBufferCallback;
-        if (!mIsInTraversal && !mTraversalScheduled) {
-            scheduleTraversals();
-        }
-    }
-
     void mergeSync(SurfaceSyncGroup otherSyncGroup) {
         if (!isInLocalSync()) {
             return;
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index d960ba1..c59d83e 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -145,15 +145,17 @@
     }
 
     @Override
-    public void updateCompatSysUiVisibility(int type, boolean visible, boolean hasControl) {
-        mViewRoot.updateCompatSysUiVisibility(type, visible, hasControl);
+    public void updateCompatSysUiVisibility(int visibleTypes, int requestedVisibleTypes,
+            int controllableTypes) {
+        mViewRoot.updateCompatSysUiVisibility(visibleTypes, requestedVisibleTypes,
+                controllableTypes);
     }
 
     @Override
-    public void updateRequestedVisibilities(InsetsVisibilities vis) {
+    public void updateRequestedVisibleTypes(@WindowInsets.Type.InsetsType int types) {
         try {
             if (mViewRoot.mAdded) {
-                mViewRoot.mWindowSession.updateRequestedVisibilities(mViewRoot.mWindow, vis);
+                mViewRoot.mWindowSession.updateRequestedVisibleTypes(mViewRoot.mWindow, types);
             }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to call insetsModified", e);
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index c1dddbe..d77e499 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -1425,10 +1425,12 @@
 
         static final int WINDOW_DECOR = 1 << 8;
 
-        static final int GENERIC_OVERLAYS = 1 << 9;
-        static final int LAST = GENERIC_OVERLAYS;
+        static final int SYSTEM_OVERLAYS = 1 << 9;
+        static final int LAST = SYSTEM_OVERLAYS;
         static final int SIZE = 10;
 
+        static final int DEFAULT_VISIBLE = ~IME;
+
         static int indexOf(@InsetsType int type) {
             switch (type) {
                 case STATUS_BARS:
@@ -1449,7 +1451,7 @@
                     return 7;
                 case WINDOW_DECOR:
                     return 8;
-                case GENERIC_OVERLAYS:
+                case SYSTEM_OVERLAYS:
                     return 9;
                 default:
                     throw new IllegalArgumentException("type needs to be >= FIRST and <= LAST,"
@@ -1457,7 +1459,8 @@
             }
         }
 
-        static String toString(@InsetsType int types) {
+        /** @hide */
+        public static String toString(@InsetsType int types) {
             StringBuilder result = new StringBuilder();
             if ((types & STATUS_BARS) != 0) {
                 result.append("statusBars |");
@@ -1486,8 +1489,8 @@
             if ((types & WINDOW_DECOR) != 0) {
                 result.append("windowDecor |");
             }
-            if ((types & GENERIC_OVERLAYS) != 0) {
-                result.append("genericOverlays |");
+            if ((types & SYSTEM_OVERLAYS) != 0) {
+                result.append("systemOverlays |");
             }
             if (result.length() > 0) {
                 result.delete(result.length() - 2, result.length());
@@ -1502,7 +1505,7 @@
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(flag = true, value = {STATUS_BARS, NAVIGATION_BARS, CAPTION_BAR, IME, WINDOW_DECOR,
                 SYSTEM_GESTURES, MANDATORY_SYSTEM_GESTURES, TAPPABLE_ELEMENT, DISPLAY_CUTOUT,
-                GENERIC_OVERLAYS})
+                SYSTEM_OVERLAYS})
         public @interface InsetsType {
         }
 
@@ -1590,11 +1593,36 @@
         }
 
         /**
+         * System overlays represent the insets caused by the system visible elements. Unlike
+         * {@link #navigationBars()} or {@link #statusBars()}, system overlays might not be
+         * hidden by the client.
+         *
+         * For compatibility reasons, this type is included in {@link #systemBars()}. In this
+         * way, views which fit {@link #systemBars()} fit {@link #systemOverlays()}.
+         *
+         * Examples include climate controls, multi-tasking affordances, etc.
+         *
+         * @return An insets type representing the system overlays.
+         */
+        public static @InsetsType int systemOverlays() {
+            return SYSTEM_OVERLAYS;
+        }
+
+        /**
          * @return All system bars. Includes {@link #statusBars()}, {@link #captionBar()} as well as
-         *         {@link #navigationBars()}, but not {@link #ime()}.
+         *         {@link #navigationBars()}, {@link #systemOverlays()}, but not {@link #ime()}.
          */
         public static @InsetsType int systemBars() {
-            return STATUS_BARS | NAVIGATION_BARS | CAPTION_BAR | GENERIC_OVERLAYS;
+            return STATUS_BARS | NAVIGATION_BARS | CAPTION_BAR | SYSTEM_OVERLAYS;
+        }
+
+        /**
+         * @return Default visible types.
+         *
+         * @hide
+         */
+        public static @InsetsType int defaultVisible() {
+            return DEFAULT_VISIBLE;
         }
 
         /**
@@ -1605,6 +1633,15 @@
         public static @InsetsType int all() {
             return 0xFFFFFFFF;
         }
+
+        /**
+         * @return System bars which can be controlled by {@link View.SystemUiVisibility}.
+         *
+         * @hide
+         */
+        public static boolean hasCompatSystemBars(@InsetsType int types) {
+            return (types & (STATUS_BARS | NAVIGATION_BARS)) != 0;
+        }
     }
 
     /**
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 63f9e13..bc0bab7 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -23,7 +23,6 @@
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.CancellationSignal;
-import android.view.InsetsState.InternalInsetsType;
 import android.view.WindowInsets.Type;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.animation.Interpolator;
@@ -279,11 +278,10 @@
     InsetsState getState();
 
     /**
-     * @return Whether the specified insets source is currently requested to be visible by the
-     *         application.
+     * @return Insets types that have been requested to be visible.
      * @hide
      */
-    boolean isRequestedVisible(@InternalInsetsType int type);
+    @InsetsType int getRequestedVisibleTypes();
 
     /**
      * Adds a {@link OnControllableInsetsChangedListener} to the window insets controller.
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index 5ed9d2f..7077804 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -40,6 +40,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Log;
+import android.view.WindowInsets.Type.InsetsType;
 import android.window.ClientWindowFrames;
 
 /**
@@ -63,7 +64,7 @@
 
     public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
             Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
-            int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities,
+            int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes,
             float compatScale, ClientWindowFrames frames) {
         final int type = attrs.type;
         final int fl = attrs.flags;
@@ -130,7 +131,7 @@
                     && (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
                     || cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)) {
                 final Insets systemBarsInsets = state.calculateInsets(
-                        displayFrame, WindowInsets.Type.systemBars(), requestedVisibilities);
+                        displayFrame, WindowInsets.Type.systemBars(), requestedVisibleTypes);
                 if (systemBarsInsets.left > 0) {
                     displayCutoutSafeExceptMaybeBars.left = MIN_X;
                 }
@@ -288,7 +289,7 @@
                 + " displayCutoutSafe=" + displayCutoutSafe
                 + " attrs=" + attrs
                 + " state=" + state
-                + " requestedVisibilities=" + requestedVisibilities);
+                + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
     }
 
     public static void extendFrameByCutout(Rect displayCutoutSafe,
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index cc85181..16f6cea 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3408,6 +3408,11 @@
          * alt="Screenshot of an activity on a display with a cutout on the long edge in portrait,
          *         letterbox is applied."/>
          *
+         * <p>
+         * Note: Android might not allow the content view to overlap the system bars in view level.
+         * To override this behavior and allow content to be able to extend into the cutout area,
+         * call {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false}.
+         *
          * @see DisplayCutout
          * @see WindowInsets#getDisplayCutout()
          * @see #layoutInDisplayCutoutMode
@@ -3443,6 +3448,11 @@
          * In this mode, the window extends under cutouts on the all edges of the display in both
          * portrait and landscape, regardless of whether the window is hiding the system bars.
          *
+         * <p>
+         * Note: Android might not allow the content view to overlap the system bars in view level.
+         * To override this behavior and allow content to be able to extend into the cutout area,
+         * call {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false}.
+         *
          * @see DisplayCutout
          * @see WindowInsets#getDisplayCutout()
          * @see #layoutInDisplayCutoutMode
diff --git a/core/java/android/view/WindowlessWindowLayout.java b/core/java/android/view/WindowlessWindowLayout.java
index 5bec5b6..8ef4d78 100644
--- a/core/java/android/view/WindowlessWindowLayout.java
+++ b/core/java/android/view/WindowlessWindowLayout.java
@@ -18,6 +18,7 @@
 
 import android.app.WindowConfiguration.WindowingMode;
 import android.graphics.Rect;
+import android.view.WindowInsets.Type.InsetsType;
 import android.window.ClientWindowFrames;
 
 /**
@@ -29,7 +30,7 @@
     @Override
     public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
             Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
-            int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities,
+            int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes,
             float compatScale, ClientWindowFrames frames) {
         frames.frame.set(0, 0, attrs.width, attrs.height);
         frames.displayFrame.set(frames.frame);
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index fbf7456..69340aa 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -30,6 +30,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.MergedConfiguration;
+import android.view.WindowInsets.Type.InsetsType;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
 
@@ -147,7 +148,7 @@
      */
     @Override
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
+            int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
@@ -198,11 +199,11 @@
      */
     @Override
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
+            int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
-        return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities,
+        return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibleTypes,
                 outInputChannel, outInsetsState, outActiveControls, outAttachedFrame,
                 outSizeCompatScale);
     }
@@ -491,7 +492,8 @@
     }
 
     @Override
-    public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities)  {
+    public void updateRequestedVisibleTypes(IWindow window,
+            @InsetsType int requestedVisibleTypes)  {
     }
 
     @Override
diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
new file mode 100644
index 0000000..85f5056
--- /dev/null
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -0,0 +1,181 @@
+/*
+ * 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.view.accessibility;
+
+import android.accessibilityservice.AccessibilityGestureEvent;
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.MagnificationConfig;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.graphics.Region;
+import android.os.IBinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
+import com.android.internal.inputmethod.RemoteAccessibilityInputConnection;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a privileged app - an app with MANAGE_ACCESSIBILITY permission and SystemAPI access - to
+ * interact with the windows in the display that this proxy represents. Proxying the default display
+ * or a display that is not tracked will throw an exception. Only the real user has access to global
+ * clients like SystemUI.
+ *
+ * <p>
+ * To register and unregister a proxy, use
+ * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)}
+ * and {@link AccessibilityManager#unregisterDisplayProxy(AccessibilityDisplayProxy)}. If the app
+ * that has registered the proxy dies, the system will remove the proxy.
+ *
+ * TODO(241429275): Complete proxy impl and add additional support (if necessary) like cache methods
+ * @hide
+ */
+@SystemApi
+public abstract class AccessibilityDisplayProxy {
+    private static final String LOG_TAG = "AccessibilityDisplayProxy";
+    private static final int INVALID_CONNECTION_ID = -1;
+
+    private List<AccessibilityServiceInfo> mInstalledAndEnabledServices;
+    private Executor mExecutor;
+    private int mConnectionId = INVALID_CONNECTION_ID;
+    private int mDisplayId;
+    IAccessibilityServiceClient mServiceClient;
+
+    /**
+     * Constructs an AccessibilityDisplayProxy instance.
+     * @param displayId the id of the display to proxy.
+     * @param executor the executor used to execute proxy callbacks.
+     * @param installedAndEnabledServices the list of infos representing the installed and
+     *                                    enabled a11y services.
+     */
+    public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor,
+            @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) {
+        mDisplayId = displayId;
+        mExecutor = executor;
+        // Typically, the context is the Service context of an accessibility service.
+        // Context is used for ResolveInfo check, which a proxy won't have, IME input
+        // (FLAG_INPUT_METHOD_EDITOR), which the proxy doesn't need, and tracing
+        // A11yInteractionClient methods.
+        // TODO(254097475): Enable tracing, potentially without exposing Context.
+        mServiceClient = new IAccessibilityServiceClientImpl(null, mExecutor);
+        mInstalledAndEnabledServices = installedAndEnabledServices;
+    }
+
+    /**
+     * Returns the id of the display being proxy-ed.
+     */
+    public int getDisplayId() {
+        return mDisplayId;
+    }
+
+    /**
+     * An IAccessibilityServiceClient that handles interrupts and accessibility events.
+     */
+    private class IAccessibilityServiceClientImpl extends
+            AccessibilityService.IAccessibilityServiceClientWrapper {
+
+        IAccessibilityServiceClientImpl(Context context, Executor executor) {
+            super(context, executor, new AccessibilityService.Callbacks() {
+                @Override
+                public void onAccessibilityEvent(AccessibilityEvent event) {
+                    // TODO: call AccessiiblityProxy.onAccessibilityEvent
+                }
+
+                @Override
+                public void onInterrupt() {
+                    // TODO: call AccessiiblityProxy.onInterrupt
+                }
+                @Override
+                public void onServiceConnected() {
+                    // TODO: send service infos and call AccessiiblityProxy.onProxyConnected
+                }
+                @Override
+                public void init(int connectionId, IBinder windowToken) {
+                    mConnectionId = connectionId;
+                }
+
+                @Override
+                public boolean onGesture(AccessibilityGestureEvent gestureInfo) {
+                    return false;
+                }
+
+                @Override
+                public boolean onKeyEvent(KeyEvent event) {
+                    return false;
+                }
+
+                @Override
+                public void onMagnificationChanged(int displayId, @NonNull Region region,
+                        MagnificationConfig config) {
+                }
+
+                @Override
+                public void onMotionEvent(MotionEvent event) {
+                }
+
+                @Override
+                public void onTouchStateChanged(int displayId, int state) {
+                }
+
+                @Override
+                public void onSoftKeyboardShowModeChanged(int showMode) {
+                }
+
+                @Override
+                public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
+                }
+
+                @Override
+                public void onFingerprintCapturingGesturesChanged(boolean active) {
+                }
+
+                @Override
+                public void onFingerprintGesture(int gesture) {
+                }
+
+                @Override
+                public void onAccessibilityButtonClicked(int displayId) {
+                }
+
+                @Override
+                public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+                }
+
+                @Override
+                public void onSystemActionsChanged() {
+                }
+
+                @Override
+                public void createImeSession(IAccessibilityInputMethodSessionCallback callback) {
+                }
+
+                @Override
+                public void startInput(@Nullable RemoteAccessibilityInputConnection inputConnection,
+                        @NonNull EditorInfo editorInfo, boolean restarting) {
+                }
+            });
+        }
+    }
+}
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index b0cf504..a52a99b 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -24,6 +24,7 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.View;
 
 import com.android.internal.util.BitUtils;
 
@@ -519,6 +520,9 @@
 
     /**
      * Represents the event of an application making an announcement.
+     * <p>
+     * In general, follow the practices described in
+     * {@link View#announceForAccessibility(CharSequence)}.
      */
     public static final int TYPE_ANNOUNCEMENT = 0x00004000;
 
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index e3ffc9d..7030ab5 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -22,7 +22,9 @@
 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS_MASK;
 import static android.view.accessibility.AccessibilityNodeInfo.FLAG_PREFETCH_MASK;
 
+import android.accessibilityservice.AccessibilityService;
 import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -31,17 +33,21 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.Pair;
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 import android.view.Display;
 import android.view.ViewConfiguration;
+import android.window.ScreenCapture;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -53,6 +59,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Queue;
+import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -146,6 +153,10 @@
 
     private boolean mPerformAccessibilityActionResult;
 
+    // SparseArray of interaction ID -> screenshot executor+callback.
+    private final SparseArray<Pair<Executor, AccessibilityService.TakeScreenshotCallback>>
+            mTakeScreenshotOfWindowCallbacks = new SparseArray<>();
+
     private Message mSameThreadMessage;
 
     private int mInteractionIdWaitingForPrefetchResult = -1;
@@ -779,6 +790,59 @@
     }
 
     /**
+     * Takes a screenshot of the window with the provided {@code accessibilityWindowId} and
+     * returns the answer asynchronously. This async behavior is similar to {@link
+     * AccessibilityService#takeScreenshot} but unlike other methods in this class which perform
+     * synchronous waiting in the AccessibilityService client.
+     *
+     * @see AccessibilityService#takeScreenshotOfWindow
+     */
+    public void takeScreenshotOfWindow(int connectionId, int accessibilityWindowId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AccessibilityService.TakeScreenshotCallback callback) {
+        synchronized (mInstanceLock) {
+            try {
+                IAccessibilityServiceConnection connection = getConnection(connectionId);
+                if (connection == null) {
+                    executor.execute(() -> callback.onFailure(
+                            AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR));
+                    return;
+                }
+                final long identityToken = Binder.clearCallingIdentity();
+                try {
+                    final int interactionId = mInteractionIdCounter.getAndIncrement();
+                    mTakeScreenshotOfWindowCallbacks.put(interactionId,
+                            Pair.create(executor, callback));
+                    // Create a ScreenCaptureListener to receive the screenshot directly from
+                    // SurfaceFlinger instead of requiring an extra IPC from the app:
+                    //   A11yService -> App -> SurfaceFlinger -> A11yService
+                    ScreenCapture.ScreenCaptureListener listener =
+                            new ScreenCapture.ScreenCaptureListener(
+                                    screenshot -> sendWindowScreenshotSuccess(screenshot,
+                                            interactionId));
+                    connection.takeScreenshotOfWindow(accessibilityWindowId, interactionId,
+                            listener, this);
+                    new Handler(Looper.getMainLooper()).postDelayed(() -> {
+                        synchronized (mInstanceLock) {
+                            // Notify failure if we still haven't sent a response after timeout.
+                            if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) {
+                                sendTakeScreenshotOfWindowError(
+                                        AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR,
+                                        interactionId);
+                            }
+                        }
+                    }, TIMEOUT_INTERACTION_MILLIS);
+                } finally {
+                    Binder.restoreCallingIdentity(identityToken);
+                }
+            } catch (RemoteException re) {
+                executor.execute(() -> callback.onFailure(
+                        AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR));
+            }
+        }
+    }
+
+    /**
      * Finds {@link AccessibilityNodeInfo}s by View text. The match is case
      * insensitive containment. The search is performed in the window whose
      * id is specified and starts from the node whose accessibility id is
@@ -1254,6 +1318,55 @@
     }
 
     /**
+     * Sends the result of a window screenshot request to the requesting client.
+     *
+     * {@link #takeScreenshotOfWindow} does not perform synchronous waiting, so this method
+     * does not notify any wait lock.
+     */
+    private void sendWindowScreenshotSuccess(ScreenCapture.ScreenshotHardwareBuffer screenshot,
+            int interactionId) {
+        if (screenshot == null) {
+            sendTakeScreenshotOfWindowError(
+                    AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId);
+            return;
+        }
+        synchronized (mInstanceLock) {
+            if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) {
+                final AccessibilityService.ScreenshotResult result =
+                        new AccessibilityService.ScreenshotResult(screenshot.getHardwareBuffer(),
+                                screenshot.getColorSpace(), SystemClock.uptimeMillis());
+                final Pair<Executor, AccessibilityService.TakeScreenshotCallback> pair =
+                        mTakeScreenshotOfWindowCallbacks.get(interactionId);
+                final Executor executor = pair.first;
+                final AccessibilityService.TakeScreenshotCallback callback = pair.second;
+                executor.execute(() -> callback.onSuccess(result));
+                mTakeScreenshotOfWindowCallbacks.remove(interactionId);
+            }
+        }
+    }
+
+    /**
+     * Sends an error code for a window screenshot request to the requesting client.
+     *
+     * @param errorCode The error code from {@link AccessibilityService.ScreenshotErrorCode}.
+     * @param interactionId The interaction id of the request.
+     */
+    @Override
+    public void sendTakeScreenshotOfWindowError(
+            @AccessibilityService.ScreenshotErrorCode int errorCode, int interactionId) {
+        synchronized (mInstanceLock) {
+            if (mTakeScreenshotOfWindowCallbacks.contains(interactionId)) {
+                final Pair<Executor, AccessibilityService.TakeScreenshotCallback> pair =
+                        mTakeScreenshotOfWindowCallbacks.get(interactionId);
+                final Executor executor = pair.first;
+                final AccessibilityService.TakeScreenshotCallback callback = pair.second;
+                executor.execute(() -> callback.onFailure(errorCode));
+                mTakeScreenshotOfWindowCallbacks.remove(interactionId);
+            }
+        }
+    }
+
+    /**
      * Clears the result state.
      */
     private void clearResultLocked() {
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 5433fa0..423c560 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -1921,6 +1921,67 @@
         }
     }
 
+    /**
+     * Registers an {@link AccessibilityDisplayProxy}, so this proxy can access UI content specific
+     * to its display.
+     *
+     * @param proxy the {@link AccessibilityDisplayProxy} to register.
+     * @return {@code true} if the proxy is successfully registered.
+     *
+     * @throws IllegalArgumentException if the proxy's display is not currently tracked by a11y, is
+     * {@link android.view.Display#DEFAULT_DISPLAY}, is or lower than
+     * {@link android.view.Display#INVALID_DISPLAY}, or is already being proxy-ed.
+     *
+     * @throws SecurityException if the app does not hold the
+     * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    public boolean registerDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+        }
+
+        try {
+            return service.registerProxyForDisplay(proxy.mServiceClient, proxy.getDisplayId());
+        }  catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters an {@link AccessibilityDisplayProxy}.
+     *
+     * @return {@code true} if the proxy is successfully unregistered.
+     *
+     * @throws SecurityException if the app does not hold the
+     * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    public boolean unregisterDisplayProxy(@NonNull AccessibilityDisplayProxy proxy)  {
+        final IAccessibilityManager service;
+        synchronized (mLock) {
+            service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+        }
+        try {
+            return service.unregisterProxyForDisplay(proxy.getDisplayId());
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     private IAccessibilityManager getServiceLocked() {
         if (mService == null) {
             tryConnectToServiceLocked(null);
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index d07a797..88adb2e 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -127,6 +127,16 @@
     /** @hide */
     public static final long UNDEFINED_NODE_ID = makeNodeId(UNDEFINED_ITEM_ID, UNDEFINED_ITEM_ID);
 
+    /**
+     * The default value for {@link #getMinMillisBetweenContentChanges};
+     */
+    public static final int UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = -1;
+
+    /**
+     * The minimum value for {@link #setMinMillisBetweenContentChanges};
+     */
+    public static final int MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES = 100;
+
     /** @hide */
     public static final long ROOT_NODE_ID = makeNodeId(ROOT_ITEM_ID,
             AccessibilityNodeProvider.HOST_VIEW_ID);
@@ -879,6 +889,9 @@
     private long mTraversalBefore = UNDEFINED_NODE_ID;
     private long mTraversalAfter = UNDEFINED_NODE_ID;
 
+    private int mMinMillisBetweenContentChanges =
+            UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES;
+
     private int mBooleanProperties;
     private final Rect mBoundsInParent = new Rect();
     private final Rect mBoundsInScreen = new Rect();
@@ -897,6 +910,7 @@
     private CharSequence mTooltipText;
     private String mViewIdResourceName;
     private String mUniqueId;
+    private CharSequence mContainerTitle;
     private ArrayList<String> mExtraDataKeys;
 
     @UnsupportedAppUsage
@@ -1781,6 +1795,42 @@
     }
 
     /**
+     * Sets the minimum time duration between two content change events, which is used in throttling
+     * content change events in accessibility services.
+     *
+     * <p>
+     * <strong>Note:</strong>
+     * This value should not be smaller than {@link #MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES},
+     * otherwise it would be ignored by accessibility services.
+     * </p>
+     *
+     * <p>
+     * Example: An app can set MinMillisBetweenContentChanges as 1 min for a view which sends
+     * content change events to accessibility services one event per second.
+     * Accessibility service will throttle those content change events and only handle one event
+     * per minute for that view.
+     * </p>
+     *
+     * @see AccessibilityEvent#getContentChangeTypes for all content change types.
+     * @param minMillisBetweenContentChanges the minimum duration between content change events.
+     */
+    public void setMinMillisBetweenContentChanges(int minMillisBetweenContentChanges) {
+        enforceNotSealed();
+        mMinMillisBetweenContentChanges = minMillisBetweenContentChanges
+                >= MINIMUM_MIN_MILLIS_BETWEEN_CONTENT_CHANGES
+                ? minMillisBetweenContentChanges
+                : UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES;
+    }
+
+    /**
+     * Gets the minimum time duration between two content change events. This method may return
+     * {@link #UNDEFINED_MIN_MILLIS_BETWEEN_CONTENT_CHANGES}
+     */
+    public int getMinMillisBetweenContentChanges() {
+        return mMinMillisBetweenContentChanges;
+    }
+
+    /**
      * Performs an action on the node.
      * <p>
      *   <strong>Note:</strong> An action can be performed only if the request is made
@@ -3631,6 +3681,47 @@
     }
 
     /**
+     * Sets the container title for app-developer-defined container which can be any type of
+     * ViewGroup or layout.
+     * Container title will be used to group together related controls, similar to HTML fieldset.
+     * Or container title may identify a large piece of the UI that is visibly grouped together,
+     * such as a toolbar or a card, etc.
+     * <p>
+     * Container title helps to assist in navigation across containers and other groups.
+     * For example, a screen reader may use this to determine where to put accessibility focus.
+     * </p>
+     * <p>
+     * Container title is different from pane title{@link #setPaneTitle} which indicates that the
+     * node represents a window or activity.
+     * </p>
+     *
+     * <p>
+     *  Example: An app can set container titles on several non-modal menus, containing TextViews
+     *  or ImageButtons that have content descriptions, text, etc. Screen readers can quickly
+     *  switch accessibility focus among menus instead of child views.  Other accessibility-services
+     *  can easily find the menu.
+     * </p>
+     *
+     * @param containerTitle The container title that is associated with a ViewGroup/Layout on the
+     *                       screen.
+     */
+    public void setContainerTitle(@Nullable CharSequence containerTitle) {
+        enforceNotSealed();
+        mContainerTitle = (containerTitle == null) ? null
+                : containerTitle.subSequence(0, containerTitle.length());
+    }
+
+    /**
+     * Returns the container title.
+     *
+     * @see #setContainerTitle for details.
+     */
+    @Nullable
+    public CharSequence getContainerTitle() {
+        return mContainerTitle;
+    }
+
+    /**
      * Sets the token and node id of the leashed parent.
      *
      * @param token The token.
@@ -3909,6 +4000,11 @@
         fieldIndex++;
         if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex);
         fieldIndex++;
+        if (mMinMillisBetweenContentChanges
+                != DEFAULT.mMinMillisBetweenContentChanges) {
+            nonDefaultFields |= bitAt(fieldIndex);
+        }
+        fieldIndex++;
         if (mConnectionId != DEFAULT.mConnectionId) nonDefaultFields |= bitAt(fieldIndex);
         fieldIndex++;
         if (!LongArray.elementsEqual(mChildNodeIds, DEFAULT.mChildNodeIds)) {
@@ -3963,6 +4059,10 @@
             nonDefaultFields |= bitAt(fieldIndex);
         }
         fieldIndex++;
+        if (!Objects.equals(mContainerTitle, DEFAULT.mContainerTitle)) {
+            nonDefaultFields |= bitAt(fieldIndex);
+        }
+        fieldIndex++;
         if (!Objects.equals(mViewIdResourceName, DEFAULT.mViewIdResourceName)) {
             nonDefaultFields |= bitAt(fieldIndex);
         }
@@ -4034,6 +4134,9 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter);
+        if (isBitSet(nonDefaultFields, fieldIndex++)) {
+            parcel.writeInt(mMinMillisBetweenContentChanges);
+        }
 
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mConnectionId);
 
@@ -4108,10 +4211,10 @@
         }
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPaneTitle);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mTooltipText);
+        if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mContainerTitle);
 
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeString(mViewIdResourceName);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeString(mUniqueId);
-
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionStart);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionEnd);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mInputType);
@@ -4189,6 +4292,7 @@
         mLabeledById = other.mLabeledById;
         mTraversalBefore = other.mTraversalBefore;
         mTraversalAfter = other.mTraversalAfter;
+        mMinMillisBetweenContentChanges = other.mMinMillisBetweenContentChanges;
         mWindowId = other.mWindowId;
         mConnectionId = other.mConnectionId;
         mUniqueId = other.mUniqueId;
@@ -4204,6 +4308,7 @@
         mContentDescription = other.mContentDescription;
         mPaneTitle = other.mPaneTitle;
         mTooltipText = other.mTooltipText;
+        mContainerTitle = other.mContainerTitle;
         mViewIdResourceName = other.mViewIdResourceName;
 
         if (mActions != null) mActions.clear();
@@ -4291,6 +4396,9 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong();
+        if (isBitSet(nonDefaultFields, fieldIndex++)) {
+            mMinMillisBetweenContentChanges = parcel.readInt();
+        }
 
         if (isBitSet(nonDefaultFields, fieldIndex++)) mConnectionId = parcel.readInt();
 
@@ -4347,6 +4455,7 @@
         }
         if (isBitSet(nonDefaultFields, fieldIndex++)) mPaneTitle = parcel.readCharSequence();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTooltipText = parcel.readCharSequence();
+        if (isBitSet(nonDefaultFields, fieldIndex++)) mContainerTitle = parcel.readCharSequence();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mViewIdResourceName = parcel.readString();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mUniqueId = parcel.readString();
 
@@ -4638,6 +4747,8 @@
             builder.append("; mParentNodeId: 0x").append(Long.toHexString(mParentNodeId));
             builder.append("; traversalBefore: 0x").append(Long.toHexString(mTraversalBefore));
             builder.append("; traversalAfter: 0x").append(Long.toHexString(mTraversalAfter));
+            builder.append("; minMillisBetweenContentChanges: ")
+                    .append(mMinMillisBetweenContentChanges);
 
             int granularities = mMovementGranularities;
             builder.append("; MovementGranularities: [");
@@ -4675,6 +4786,7 @@
         builder.append("; stateDescription: ").append(mStateDescription);
         builder.append("; contentDescription: ").append(mContentDescription);
         builder.append("; tooltipText: ").append(mTooltipText);
+        builder.append("; containerTitle: ").append(mContainerTitle);
         builder.append("; viewIdResName: ").append(mViewIdResourceName);
         builder.append("; uniqueId: ").append(mUniqueId);
 
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
index 472a363..fb01921 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl
@@ -22,6 +22,7 @@
 import android.view.MagnificationSpec;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.window.ScreenCapture;
 
 /**
  * Interface for interaction between the AccessibilityManagerService
@@ -60,4 +61,8 @@
     void clearAccessibilityFocus();
 
     void notifyOutsideTouch();
+
+    void takeScreenshotOfWindow(int interactionId,
+        in ScreenCapture.ScreenCaptureListener listener,
+        IAccessibilityInteractionConnectionCallback callback);
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
index 231e75a..456bf58 100644
--- a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl
@@ -63,4 +63,9 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     void setPerformAccessibilityActionResult(boolean succeeded, int interactionId);
+
+    /**
+    * Sends an error code for a window screenshot request to the requesting client.
+    */
+    void sendTakeScreenshotOfWindowError(int errorCode, int interactionId);
 }
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 36fdcce4..a251948 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -109,9 +109,9 @@
 
     oneway void setAccessibilityWindowAttributes(int displayId, int windowId, int userId, in AccessibilityWindowAttributes attributes);
 
-    // Requires Manifest.permission.MANAGE_ACCESSIBILITY
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
     boolean registerProxyForDisplay(IAccessibilityServiceClient proxy, int displayId);
 
-    // Requires Manifest.permission.MANAGE_ACCESSIBILITY
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
     boolean unregisterProxyForDisplay(int displayId);
 }
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 70cfc3e..ef683b7 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -19,6 +19,7 @@
 import static android.service.autofill.FillRequest.FLAG_IME_SHOWING;
 import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
 import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE;
+import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE;
 import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG;
 import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED;
 import static android.view.ContentInfo.SOURCE_AUTOFILL;
@@ -734,7 +735,7 @@
      * Autofill will automatically trigger a fill request after activity
      * start if there is any field is autofillable. But if there is a field that
      * triggered autofill, it is unnecessary to trigger again through
-     * AutofillManager#notifyViewEnteredForActivityStarted.
+     * AutofillManager#notifyViewEnteredForFillDialog.
      */
     private AtomicBoolean mIsFillRequested;
 
@@ -747,6 +748,10 @@
 
     private final String[] mFillDialogEnabledHints;
 
+    // Tracked all views that have appeared, including views that there are no
+    // dataset in responses. Used to avoid request pre-fill request again and again.
+    private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>();
+
     /** @hide */
     public interface AutofillClient {
         /**
@@ -1192,6 +1197,16 @@
      * @hide
      */
     public void notifyViewEnteredForFillDialog(View v) {
+        synchronized (mLock) {
+            if (mTrackedViews != null) {
+                // To support the fill dialog can show for the autofillable Views in
+                // different pages but in the same Activity. We need to reset the
+                // mIsFillRequested flag to allow asking for a new FillRequest when
+                // user switches to other page
+                mTrackedViews.checkViewState(v.getAutofillId());
+            }
+        }
+
         // Skip if the fill request has been performed for a view.
         if (mIsFillRequested.get()) {
             return;
@@ -1318,6 +1333,10 @@
                         }
                         mForAugmentedAutofillOnly = false;
                     }
+
+                    if ((flags & FLAG_SUPPORTS_FILL_DIALOG) != 0) {
+                        flags |= FLAG_RESET_FILL_DIALOG_STATE;
+                    }
                     updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags);
                 }
                 addEnteredIdLocked(id);
@@ -2217,6 +2236,7 @@
         mIsFillRequested.set(false);
         mShowAutofillDialogCalled = false;
         mFillDialogTriggerIds = null;
+        mAllTrackedViews.clear();
         if (resetEnteredIds) {
             mEnteredIds = null;
         }
@@ -2776,14 +2796,9 @@
                         + ", mFillableIds=" + mFillableIds
                         + ", mEnabled=" + mEnabled
                         + ", mSessionId=" + mSessionId);
-
             }
+
             if (mEnabled && mSessionId == sessionId) {
-                if (saveOnAllViewsInvisible) {
-                    mTrackedViews = new TrackedViews(trackedIds);
-                } else {
-                    mTrackedViews = null;
-                }
                 mSaveOnFinish = saveOnFinish;
                 if (fillableIds != null) {
                     if (mFillableIds == null) {
@@ -2805,6 +2820,27 @@
                     mSaveTriggerId = saveTriggerId;
                     setNotifyOnClickLocked(mSaveTriggerId, true);
                 }
+
+                if (!saveOnAllViewsInvisible) {
+                    trackedIds = null;
+                }
+
+                final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
+                if (mFillableIds != null) {
+                    allFillableIds.addAll(mFillableIds);
+                }
+                if (trackedIds != null) {
+                    for (AutofillId id : trackedIds) {
+                        id.resetSessionId();
+                        allFillableIds.add(id);
+                    }
+                }
+
+                if (!allFillableIds.isEmpty()) {
+                    mTrackedViews = new TrackedViews(trackedIds, Helper.toArray(allFillableIds));
+                } else {
+                    mTrackedViews = null;
+                }
             }
         }
     }
@@ -3576,10 +3612,19 @@
      */
     private class TrackedViews {
         /** Visible tracked views */
-        @Nullable private ArraySet<AutofillId> mVisibleTrackedIds;
+        @NonNull private final ArraySet<AutofillId> mVisibleTrackedIds;
 
         /** Invisible tracked views */
-        @Nullable private ArraySet<AutofillId> mInvisibleTrackedIds;
+        @NonNull private final ArraySet<AutofillId> mInvisibleTrackedIds;
+
+        /** Visible tracked views for fill dialog */
+        @NonNull private final ArraySet<AutofillId> mVisibleDialogTrackedIds;
+
+        /** Invisible tracked views for fill dialog */
+        @NonNull private final ArraySet<AutofillId> mInvisibleDialogTrackedIds;
+
+        boolean mHasNewTrackedView;
+        boolean mIsTrackedSaveView;
 
         /**
          * Check if set is null or value is in set.
@@ -3645,43 +3690,65 @@
          *
          * @param trackedIds The views to be tracked
          */
-        TrackedViews(@Nullable AutofillId[] trackedIds) {
-            final AutofillClient client = getClient();
-            if (!ArrayUtils.isEmpty(trackedIds) && client != null) {
-                final boolean[] isVisible;
+        TrackedViews(@Nullable AutofillId[] trackedIds, @Nullable AutofillId[] allTrackedIds) {
+            mVisibleTrackedIds = new ArraySet<>();
+            mInvisibleTrackedIds = new ArraySet<>();
+            if (!ArrayUtils.isEmpty(trackedIds)) {
+                mIsTrackedSaveView = true;
+                initialTrackedViews(trackedIds, mVisibleTrackedIds, mInvisibleTrackedIds);
+            }
 
-                if (client.autofillClientIsVisibleForAutofill()) {
-                    if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
-                    isVisible = client.autofillClientGetViewVisibility(trackedIds);
-                } else {
-                    // All false
-                    isVisible = new boolean[trackedIds.length];
-                }
-
-                final int numIds = trackedIds.length;
-                for (int i = 0; i < numIds; i++) {
-                    final AutofillId id = trackedIds[i];
-                    id.resetSessionId();
-
-                    if (isVisible[i]) {
-                        mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
-                    } else {
-                        mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
-                    }
-                }
+            mVisibleDialogTrackedIds = new ArraySet<>();
+            mInvisibleDialogTrackedIds = new ArraySet<>();
+            if (!ArrayUtils.isEmpty(allTrackedIds)) {
+                initialTrackedViews(allTrackedIds, mVisibleDialogTrackedIds,
+                        mInvisibleDialogTrackedIds);
+                mAllTrackedViews.addAll(Arrays.asList(allTrackedIds));
             }
 
             if (sVerbose) {
                 Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): "
                         + " mVisibleTrackedIds=" + mVisibleTrackedIds
-                        + " mInvisibleTrackedIds=" + mInvisibleTrackedIds);
+                        + " mInvisibleTrackedIds=" + mInvisibleTrackedIds
+                        + " allTrackedIds=" + Arrays.toString(allTrackedIds)
+                        + " mVisibleDialogTrackedIds=" + mVisibleDialogTrackedIds
+                        + " mInvisibleDialogTrackedIds=" + mInvisibleDialogTrackedIds);
             }
 
-            if (mVisibleTrackedIds == null) {
+            if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
                 finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
             }
         }
 
+        private void initialTrackedViews(AutofillId[] trackedIds,
+                @NonNull ArraySet<AutofillId> visibleSet,
+                @NonNull ArraySet<AutofillId> invisibleSet) {
+            final boolean[] isVisible;
+            final AutofillClient client = getClient();
+            if (ArrayUtils.isEmpty(trackedIds) || client == null) {
+                return;
+            }
+            if (client.autofillClientIsVisibleForAutofill()) {
+                if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
+                isVisible = client.autofillClientGetViewVisibility(trackedIds);
+            } else {
+                // All false
+                isVisible = new boolean[trackedIds.length];
+            }
+
+            final int numIds = trackedIds.length;
+            for (int i = 0; i < numIds; i++) {
+                final AutofillId id = trackedIds[i];
+                id.resetSessionId();
+
+                if (isVisible[i]) {
+                    addToSet(visibleSet, id);
+                } else {
+                    addToSet(invisibleSet, id);
+                }
+            }
+        }
+
         /**
          * Called when a {@link View view's} visibility changes.
          *
@@ -3698,22 +3765,37 @@
             if (isClientVisibleForAutofillLocked()) {
                 if (isVisible) {
                     if (isInSet(mInvisibleTrackedIds, id)) {
-                        mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
-                        mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
+                        removeFromSet(mInvisibleTrackedIds, id);
+                        addToSet(mVisibleTrackedIds, id);
+                    }
+                    if (isInSet(mInvisibleDialogTrackedIds, id)) {
+                        removeFromSet(mInvisibleDialogTrackedIds, id);
+                        addToSet(mVisibleDialogTrackedIds, id);
                     }
                 } else {
                     if (isInSet(mVisibleTrackedIds, id)) {
-                        mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id);
-                        mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+                        removeFromSet(mVisibleTrackedIds, id);
+                        addToSet(mInvisibleTrackedIds, id);
+                    }
+                    if (isInSet(mVisibleDialogTrackedIds, id)) {
+                        removeFromSet(mVisibleDialogTrackedIds, id);
+                        addToSet(mInvisibleDialogTrackedIds, id);
                     }
                 }
             }
 
-            if (mVisibleTrackedIds == null) {
+            if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
                 if (sVerbose) {
                     Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleTrackedIds);
                 }
                 finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
+
+            }
+            if (mVisibleDialogTrackedIds.isEmpty()) {
+                if (sVerbose) {
+                    Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleDialogTrackedIds);
+                }
+                processNoVisibleTrackedAllViews();
             }
         }
 
@@ -3727,66 +3809,66 @@
             // The visibility of the views might have changed while the client was not be visible,
             // hence update the visibility state for all views.
             AutofillClient client = getClient();
-            ArraySet<AutofillId> updatedVisibleTrackedIds = null;
-            ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
             if (client != null) {
                 if (sVerbose) {
                     Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds
                             + " vis=" + mVisibleTrackedIds);
                 }
-                if (mInvisibleTrackedIds != null) {
-                    final ArrayList<AutofillId> orderedInvisibleIds =
-                            new ArrayList<>(mInvisibleTrackedIds);
-                    final boolean[] isVisible = client.autofillClientGetViewVisibility(
-                            Helper.toArray(orderedInvisibleIds));
 
-                    final int numInvisibleTrackedIds = orderedInvisibleIds.size();
-                    for (int i = 0; i < numInvisibleTrackedIds; i++) {
-                        final AutofillId id = orderedInvisibleIds.get(i);
-                        if (isVisible[i]) {
-                            updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
-
-                            if (sDebug) {
-                                Log.d(TAG, "onVisibleForAutofill() " + id + " became visible");
-                            }
-                        } else {
-                            updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
-                        }
-                    }
-                }
-
-                if (mVisibleTrackedIds != null) {
-                    final ArrayList<AutofillId> orderedVisibleIds =
-                            new ArrayList<>(mVisibleTrackedIds);
-                    final boolean[] isVisible = client.autofillClientGetViewVisibility(
-                            Helper.toArray(orderedVisibleIds));
-
-                    final int numVisibleTrackedIds = orderedVisibleIds.size();
-                    for (int i = 0; i < numVisibleTrackedIds; i++) {
-                        final AutofillId id = orderedVisibleIds.get(i);
-
-                        if (isVisible[i]) {
-                            updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
-                        } else {
-                            updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
-
-                            if (sDebug) {
-                                Log.d(TAG, "onVisibleForAutofill() " + id + " became invisible");
-                            }
-                        }
-                    }
-                }
-
-                mInvisibleTrackedIds = updatedInvisibleTrackedIds;
-                mVisibleTrackedIds = updatedVisibleTrackedIds;
+                onVisibleForAutofillChangedInternalLocked(mVisibleTrackedIds, mInvisibleTrackedIds);
+                onVisibleForAutofillChangedInternalLocked(
+                        mVisibleDialogTrackedIds, mInvisibleDialogTrackedIds);
             }
 
-            if (mVisibleTrackedIds == null) {
+            if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
                 if (sVerbose) {
-                    Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
+                    Log.v(TAG,  "onVisibleForAutofillChangedLocked(): no more visible ids");
                 }
                 finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
             }
+            if (mVisibleDialogTrackedIds.isEmpty()) {
+                if (sVerbose) {
+                    Log.v(TAG,  "onVisibleForAutofillChangedLocked(): no more visible ids");
+                }
+                processNoVisibleTrackedAllViews();
+            }
+        }
+
+        void onVisibleForAutofillChangedInternalLocked(@NonNull ArraySet<AutofillId> visibleSet,
+                @NonNull ArraySet<AutofillId> invisibleSet) {
+            // The visibility of the views might have changed while the client was not be visible,
+            // hence update the visibility state for all views.
+            if (sVerbose) {
+                Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + invisibleSet
+                        + " vis=" + visibleSet);
+            }
+
+            ArraySet<AutofillId> allTrackedIds = new ArraySet<>();
+            allTrackedIds.addAll(visibleSet);
+            allTrackedIds.addAll(invisibleSet);
+            if (!allTrackedIds.isEmpty()) {
+                visibleSet.clear();
+                invisibleSet.clear();
+                initialTrackedViews(Helper.toArray(allTrackedIds), visibleSet, invisibleSet);
+            }
+        }
+
+        private void processNoVisibleTrackedAllViews() {
+            mShowAutofillDialogCalled = false;
+        }
+
+        void checkViewState(AutofillId id) {
+            if (mAllTrackedViews.contains(id)) {
+                return;
+            }
+            // Add the id as tracked to avoid triggering fill request again and again.
+            mAllTrackedViews.add(id);
+            if (mHasNewTrackedView) {
+                return;
+            }
+            // First one new tracks view
+            mIsFillRequested.set(false);
+            mHasNewTrackedView = true;
         }
     }
 
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 1664637..d067d4b 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -378,7 +378,7 @@
     private final Object mLock = new Object();
 
     @NonNull
-    private final Context mContext;
+    private final StrippedContext mContext;
 
     @NonNull
     private final IContentCaptureManager mService;
@@ -414,9 +414,37 @@
     }
 
     /** @hide */
+    static class StrippedContext {
+        final String mPackageName;
+        final String mContext;
+        final @UserIdInt int mUserId;
+
+        private StrippedContext(Context context) {
+            mPackageName = context.getPackageName();
+            mContext = context.toString();
+            mUserId = context.getUserId();
+        }
+
+        @Override
+        public String toString() {
+            return mContext;
+        }
+
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        @UserIdInt
+        public int getUserId() {
+            return mUserId;
+        }
+    }
+
+    /** @hide */
     public ContentCaptureManager(@NonNull Context context,
             @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
-        mContext = Objects.requireNonNull(context, "context cannot be null");
+        Objects.requireNonNull(context, "context cannot be null");
+        mContext = new StrippedContext(context);
         mService = Objects.requireNonNull(service, "service cannot be null");
         mOptions = Objects.requireNonNull(options, "options cannot be null");
 
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index c32ca9e..a989558 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -36,7 +36,6 @@
 import android.annotation.Nullable;
 import android.annotation.UiThread;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Insets;
 import android.graphics.Rect;
@@ -103,7 +102,7 @@
     private final AtomicBoolean mDisabled = new AtomicBoolean(false);
 
     @NonNull
-    private final Context mContext;
+    private final ContentCaptureManager.StrippedContext mContext;
 
     @NonNull
     private final ContentCaptureManager mManager;
@@ -197,7 +196,7 @@
         }
     }
 
-    protected MainContentCaptureSession(@NonNull Context context,
+    protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext context,
             @NonNull ContentCaptureManager manager, @NonNull Handler handler,
             @NonNull IContentCaptureManager systemServerInterface) {
         mContext = context;
diff --git a/core/java/android/view/inputmethod/CursorAnchorInfo.java b/core/java/android/view/inputmethod/CursorAnchorInfo.java
index a8ed96e..2d974db 100644
--- a/core/java/android/view/inputmethod/CursorAnchorInfo.java
+++ b/core/java/android/view/inputmethod/CursorAnchorInfo.java
@@ -20,12 +20,14 @@
 import android.annotation.Nullable;
 import android.graphics.Matrix;
 import android.graphics.RectF;
+import android.inputmethodservice.InputMethodService;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.Layout;
 import android.text.SpannedString;
 import android.text.TextUtils;
 import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder;
+import android.widget.TextView;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -110,7 +112,7 @@
     /**
      * Container of rectangular position of Editor in the local coordinates that will be transformed
      * with the transformation matrix when rendered on the screen.
-     * @see {@link EditorBoundsInfo}.
+     * @see EditorBoundsInfo
      */
     private final EditorBoundsInfo mEditorBoundsInfo;
 
@@ -122,6 +124,12 @@
     private final float[] mMatrixValues;
 
     /**
+     * Information about text appearance in the editor for use by {@link InputMethodService}.
+     */
+    @Nullable
+    private final TextAppearanceInfo mTextAppearanceInfo;
+
+    /**
      * A list of visible line bounds stored in a float array. This array is divided into segment of
      * four where each element in the segment represents left, top, right respectively and bottom
      * of the line bounds.
@@ -157,10 +165,11 @@
         mInsertionMarkerTop = source.readFloat();
         mInsertionMarkerBaseline = source.readFloat();
         mInsertionMarkerBottom = source.readFloat();
-        mCharacterBoundsArray = source.readParcelable(SparseRectFArray.class.getClassLoader(), android.view.inputmethod.SparseRectFArray.class);
+        mCharacterBoundsArray = source.readTypedObject(SparseRectFArray.CREATOR);
         mEditorBoundsInfo = source.readTypedObject(EditorBoundsInfo.CREATOR);
         mMatrixValues = source.createFloatArray();
         mVisibleLineBounds = source.createFloatArray();
+        mTextAppearanceInfo = source.readTypedObject(TextAppearanceInfo.CREATOR);
     }
 
     /**
@@ -181,10 +190,11 @@
         dest.writeFloat(mInsertionMarkerTop);
         dest.writeFloat(mInsertionMarkerBaseline);
         dest.writeFloat(mInsertionMarkerBottom);
-        dest.writeParcelable(mCharacterBoundsArray, flags);
+        dest.writeTypedObject(mCharacterBoundsArray, flags);
         dest.writeTypedObject(mEditorBoundsInfo, flags);
         dest.writeFloatArray(mMatrixValues);
         dest.writeFloatArray(mVisibleLineBounds);
+        dest.writeTypedObject(mTextAppearanceInfo, flags);
     }
 
     @Override
@@ -262,6 +272,11 @@
                 return false;
             }
         }
+
+        if (!Objects.equals(mTextAppearanceInfo, that.mTextAppearanceInfo)) {
+            return false;
+        }
+
         return true;
     }
 
@@ -270,16 +285,17 @@
         return "CursorAnchorInfo{mHashCode=" + mHashCode
                 + " mSelection=" + mSelectionStart + "," + mSelectionEnd
                 + " mComposingTextStart=" + mComposingTextStart
-                + " mComposingText=" + Objects.toString(mComposingText)
+                + " mComposingText=" + mComposingText
                 + " mInsertionMarkerFlags=" + mInsertionMarkerFlags
                 + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal
                 + " mInsertionMarkerTop=" + mInsertionMarkerTop
                 + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
                 + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
-                + " mCharacterBoundsArray=" + Objects.toString(mCharacterBoundsArray)
+                + " mCharacterBoundsArray=" + mCharacterBoundsArray
                 + " mEditorBoundsInfo=" + mEditorBoundsInfo
                 + " mVisibleLineBounds=" + getVisibleLineBounds()
                 + " mMatrix=" + Arrays.toString(mMatrixValues)
+                + " mTextAppearanceInfo=" + mTextAppearanceInfo
                 + "}";
     }
 
@@ -303,6 +319,7 @@
         private boolean mMatrixInitialized = false;
         private float[] mVisibleLineBounds = new float[LINE_BOUNDS_INITIAL_SIZE * 4];
         private int mVisibleLineBoundsCount = 0;
+        private TextAppearanceInfo mTextAppearanceInfo = null;
 
         /**
          * Sets the text range of the selection. Calling this can be skipped if there is no
@@ -416,6 +433,17 @@
         }
 
         /**
+         * Set the information related to text appearance, which is extracted from the original
+         * {@link TextView}.
+         * @param textAppearanceInfo {@link TextAppearanceInfo} of TextView.
+         */
+        @NonNull
+        public Builder setTextAppearanceInfo(@Nullable TextAppearanceInfo textAppearanceInfo) {
+            mTextAppearanceInfo = textAppearanceInfo;
+            return this;
+        }
+
+        /**
          * Add the bounds of a visible text line of the current editor.
          *
          * The line bounds should not include the vertical space between lines or the horizontal
@@ -504,6 +532,7 @@
             }
             mEditorBoundsInfo = null;
             clearVisibleLineBounds();
+            mTextAppearanceInfo = null;
         }
     }
 
@@ -524,7 +553,8 @@
                 builder.mInsertionMarkerHorizontal, builder.mInsertionMarkerTop,
                 builder.mInsertionMarkerBaseline, builder.mInsertionMarkerBottom,
                 characterBoundsArray, builder.mEditorBoundsInfo, matrixValues,
-                Arrays.copyOf(builder.mVisibleLineBounds, builder.mVisibleLineBoundsCount));
+                Arrays.copyOf(builder.mVisibleLineBounds, builder.mVisibleLineBoundsCount),
+                builder.mTextAppearanceInfo);
     }
 
     private CursorAnchorInfo(int selectionStart, int selectionEnd, int composingTextStart,
@@ -533,7 +563,8 @@
             float insertionMarkerBaseline, float insertionMarkerBottom,
             @Nullable SparseRectFArray characterBoundsArray,
             @Nullable EditorBoundsInfo editorBoundsInfo,
-            @NonNull float[] matrixValues, @Nullable float[] visibleLineBounds) {
+            @NonNull float[] matrixValues, @Nullable float[] visibleLineBounds,
+            @Nullable TextAppearanceInfo textAppearanceInfo) {
         mSelectionStart = selectionStart;
         mSelectionEnd = selectionEnd;
         mComposingTextStart = composingTextStart;
@@ -547,6 +578,7 @@
         mEditorBoundsInfo = editorBoundsInfo;
         mMatrixValues = matrixValues;
         mVisibleLineBounds = visibleLineBounds;
+        mTextAppearanceInfo = textAppearanceInfo;
 
         // To keep hash function simple, we only use some complex objects for hash.
         int hashCode = Objects.hashCode(mComposingText);
@@ -573,7 +605,7 @@
                 original.mInsertionMarkerTop, original.mInsertionMarkerBaseline,
                 original.mInsertionMarkerBottom, original.mCharacterBoundsArray,
                 original.mEditorBoundsInfo, computeMatrixValues(parentMatrix, original),
-                original.mVisibleLineBounds);
+                original.mVisibleLineBounds, original.mTextAppearanceInfo);
     }
 
     /**
@@ -741,6 +773,16 @@
     }
 
     /**
+     * Returns {@link TextAppearanceInfo} for the current editor, or {@code null} if IME is not
+     * subscribed with {@link InputConnection#CURSOR_UPDATE_FILTER_TEXT_APPEARANCE}
+     * or {@link InputConnection#CURSOR_UPDATE_MONITOR}.
+     */
+    @Nullable
+    public TextAppearanceInfo getTextAppearanceInfo() {
+        return mTextAppearanceInfo;
+    }
+
+    /**
      * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
      * matrix that is to be applied other positional data in this class.
      * @return a new instance (copy) of the transformation matrix.
diff --git a/core/java/android/view/inputmethod/DeleteGesture.java b/core/java/android/view/inputmethod/DeleteGesture.java
index c88158f..b843594 100644
--- a/core/java/android/view/inputmethod/DeleteGesture.java
+++ b/core/java/android/view/inputmethod/DeleteGesture.java
@@ -33,7 +33,7 @@
  * <p>Note: This deletes all text <em>within</em> the given area. To delete a range <em>between</em>
  * two areas, use {@link DeleteRangeGesture}.</p>
  */
-public final class DeleteGesture extends HandwritingGesture implements Parcelable {
+public final class DeleteGesture extends PreviewableHandwritingGesture implements Parcelable {
 
     private @Granularity int mGranularity;
     private RectF mArea;
diff --git a/core/java/android/view/inputmethod/DeleteRangeGesture.java b/core/java/android/view/inputmethod/DeleteRangeGesture.java
index 53b4209..0bce15e 100644
--- a/core/java/android/view/inputmethod/DeleteRangeGesture.java
+++ b/core/java/android/view/inputmethod/DeleteRangeGesture.java
@@ -34,7 +34,7 @@
  * <p>Note: this deletes text within a range <em>between</em> two given areas. To delete all text
  * <em>within</em> a single area, use {@link DeleteGesture}.</p>
  */
-public final class DeleteRangeGesture extends HandwritingGesture implements Parcelable {
+public final class DeleteRangeGesture extends PreviewableHandwritingGesture implements Parcelable {
 
     private @Granularity int mGranularity;
     private RectF mStartArea;
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 4a79ba6..9ebaa67 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -55,8 +55,10 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * An EditorInfo describes several attributes of a text editing object
@@ -534,6 +536,8 @@
 
     private @HandwritingGesture.GestureTypeFlags int mSupportedHandwritingGestureTypes;
 
+    private @HandwritingGesture.GestureTypeFlags int mSupportedHandwritingGesturePreviewTypes;
+
     /**
      * Set the Handwriting gestures supported by the current {@code Editor}.
      * For an editor that supports Stylus Handwriting
@@ -541,8 +545,11 @@
      * supported gestures.
      * <p> If editor doesn't support one of the declared types, IME will not send those Gestures
      *  to the editor. Instead they will fallback to using normal text input. </p>
+     * <p>Note: A supported gesture may not have preview supported
+     * {@link #getSupportedHandwritingGesturePreviews()}.</p>
      * @param gestures List of supported gesture classes including any of {@link SelectGesture},
      * {@link InsertGesture}, {@link DeleteGesture}.
+     * @see #setSupportedHandwritingGesturePreviews(Set)
      */
     public void setSupportedHandwritingGestures(
             @NonNull List<Class<? extends HandwritingGesture>> gestures) {
@@ -584,6 +591,7 @@
      * {@link InputMethodManager#startStylusHandwriting}, it also declares supported gestures.
      * @return List of supported gesture classes including any of {@link SelectGesture},
      * {@link InsertGesture}, {@link DeleteGesture}.
+     * @see #getSupportedHandwritingGesturePreviews()
      */
     @NonNull
     public List<Class<? extends HandwritingGesture>> getSupportedHandwritingGestures() {
@@ -595,6 +603,10 @@
                 == HandwritingGesture.GESTURE_TYPE_SELECT) {
             list.add(SelectGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_SELECT_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) {
+            list.add(SelectRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_INSERT)
                 == HandwritingGesture.GESTURE_TYPE_INSERT) {
             list.add(InsertGesture.class);
@@ -603,6 +615,10 @@
                 == HandwritingGesture.GESTURE_TYPE_DELETE) {
             list.add(DeleteGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_DELETE_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) {
+            list.add(DeleteRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE)
                 == HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE) {
             list.add(RemoveSpaceGesture.class);
@@ -615,6 +631,86 @@
     }
 
     /**
+     * Set the Handwriting gesture previews supported by the current {@code Editor}.
+     * For an editor that supports Stylus Handwriting
+     * {@link InputMethodManager#startStylusHandwriting}, it is also recommended that it declares
+     * supported gesture previews.
+     * <p>Note: A supported gesture {@link EditorInfo#getSupportedHandwritingGestures()} may not
+     * have preview supported {@link EditorInfo#getSupportedHandwritingGesturePreviews()}.</p>
+     * <p> If editor doesn't support one of the declared types, gesture preview will be ignored.</p>
+     * @param gestures Set of supported gesture classes. One of {@link SelectGesture},
+     * {@link SelectRangeGesture}, {@link DeleteGesture}, {@link DeleteRangeGesture}.
+     * @see #setSupportedHandwritingGestures(List)
+     */
+    public void setSupportedHandwritingGesturePreviews(
+            @NonNull Set<Class<? extends PreviewableHandwritingGesture>> gestures) {
+        Objects.requireNonNull(gestures);
+        if (gestures.isEmpty()) {
+            mSupportedHandwritingGesturePreviewTypes = 0;
+            return;
+        }
+
+        int supportedTypes = 0;
+        for (Class<? extends PreviewableHandwritingGesture> gesture : gestures) {
+            Objects.requireNonNull(gesture);
+            if (gesture.equals(SelectGesture.class)) {
+                supportedTypes |= HandwritingGesture.GESTURE_TYPE_SELECT;
+            } else if (gesture.equals(SelectRangeGesture.class)) {
+                supportedTypes |= HandwritingGesture.GESTURE_TYPE_SELECT_RANGE;
+            } else if (gesture.equals(DeleteGesture.class)) {
+                supportedTypes |= HandwritingGesture.GESTURE_TYPE_DELETE;
+            } else if (gesture.equals(DeleteRangeGesture.class)) {
+                supportedTypes |= HandwritingGesture.GESTURE_TYPE_DELETE_RANGE;
+            } else {
+                throw new IllegalArgumentException(
+                        "Unsupported gesture type for preview: " + gesture);
+            }
+        }
+
+        mSupportedHandwritingGesturePreviewTypes = supportedTypes;
+    }
+
+    /**
+     * Returns the combination of Stylus handwriting gesture preview types
+     * supported by the current {@code Editor}.
+     * For an editor that supports Stylus Handwriting.
+     * {@link InputMethodManager#startStylusHandwriting}, it also declares supported gesture
+     * previews.
+     * <p>Note: A supported gesture {@link EditorInfo#getSupportedHandwritingGestures()} may not
+     * have preview supported {@link EditorInfo#getSupportedHandwritingGesturePreviews()}.</p>
+     * @return Set of supported gesture preview classes. One of {@link SelectGesture},
+     * {@link SelectRangeGesture}, {@link DeleteGesture}, {@link DeleteRangeGesture}.
+     * @see #getSupportedHandwritingGestures()
+     */
+    @NonNull
+    public Set<Class<? extends PreviewableHandwritingGesture>>
+            getSupportedHandwritingGesturePreviews() {
+        Set<Class<? extends PreviewableHandwritingGesture>> set  = new HashSet<>();
+        if (mSupportedHandwritingGesturePreviewTypes == 0) {
+            return set;
+        }
+        if ((mSupportedHandwritingGesturePreviewTypes & HandwritingGesture.GESTURE_TYPE_SELECT)
+                == HandwritingGesture.GESTURE_TYPE_SELECT) {
+            set.add(SelectGesture.class);
+        }
+        if ((mSupportedHandwritingGesturePreviewTypes
+                & HandwritingGesture.GESTURE_TYPE_SELECT_RANGE)
+                        == HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) {
+            set.add(SelectRangeGesture.class);
+        }
+        if ((mSupportedHandwritingGesturePreviewTypes & HandwritingGesture.GESTURE_TYPE_DELETE)
+                == HandwritingGesture.GESTURE_TYPE_DELETE) {
+            set.add(DeleteGesture.class);
+        }
+        if ((mSupportedHandwritingGesturePreviewTypes
+                & HandwritingGesture.GESTURE_TYPE_DELETE_RANGE)
+                        == HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) {
+            set.add(DeleteRangeGesture.class);
+        }
+        return set;
+    }
+
+    /**
      * If not {@code null}, this editor needs to talk to IMEs that run for the specified user, no
      * matter what user ID the calling process has.
      *
@@ -1106,6 +1202,9 @@
         pw.println(prefix + "supportedHandwritingGestureTypes="
                 + InputMethodDebug.handwritingGestureTypeFlagsToString(
                         mSupportedHandwritingGestureTypes));
+        pw.println(prefix + "supportedHandwritingGesturePreviewTypes="
+                + InputMethodDebug.handwritingGestureTypeFlagsToString(
+                        mSupportedHandwritingGesturePreviewTypes));
         pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes));
         if (targetInputMethodUser != null) {
             pw.println(prefix + "targetInputMethodUserId=" + targetInputMethodUser.getIdentifier());
@@ -1141,6 +1240,8 @@
         newEditorInfo.contentMimeTypes = ArrayUtils.cloneOrNull(contentMimeTypes);
         newEditorInfo.targetInputMethodUser = targetInputMethodUser;
         newEditorInfo.mSupportedHandwritingGestureTypes = mSupportedHandwritingGestureTypes;
+        newEditorInfo.mSupportedHandwritingGesturePreviewTypes =
+                mSupportedHandwritingGesturePreviewTypes;
         return newEditorInfo;
     }
 
@@ -1169,6 +1270,7 @@
         dest.writeString(fieldName);
         dest.writeBundle(extras);
         dest.writeInt(mSupportedHandwritingGestureTypes);
+        dest.writeInt(mSupportedHandwritingGesturePreviewTypes);
         dest.writeBoolean(mInitialSurroundingText != null);
         if (mInitialSurroundingText != null) {
             mInitialSurroundingText.writeToParcel(dest, flags);
@@ -1207,6 +1309,7 @@
                     res.fieldName = source.readString();
                     res.extras = source.readBundle();
                     res.mSupportedHandwritingGestureTypes = source.readInt();
+                    res.mSupportedHandwritingGesturePreviewTypes = source.readInt();
                     boolean hasInitialSurroundingText = source.readBoolean();
                     if (hasInitialSurroundingText) {
                         res.mInitialSurroundingText =
@@ -1250,6 +1353,8 @@
                 && initialCapsMode == that.initialCapsMode
                 && fieldId == that.fieldId
                 && mSupportedHandwritingGestureTypes == that.mSupportedHandwritingGestureTypes
+                && mSupportedHandwritingGesturePreviewTypes
+                        == that.mSupportedHandwritingGesturePreviewTypes
                 && Objects.equals(autofillId, that.autofillId)
                 && Objects.equals(privateImeOptions, that.privateImeOptions)
                 && Objects.equals(packageName, that.packageName)
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
new file mode 100644
index 0000000..6eae63a
--- /dev/null
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -0,0 +1,530 @@
+/*
+ * 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.view.inputmethod;
+
+import android.Manifest;
+import android.annotation.AnyThread;
+import android.annotation.DurationMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.view.WindowManager;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.inputmethod.DirectBootAwareness;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+import com.android.internal.view.IInputMethodManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A global wrapper to directly invoke {@link IInputMethodManager} IPCs.
+ *
+ * <p>All public static methods are guaranteed to be thread-safe.</p>
+ *
+ * <p>All public methods are guaranteed to do nothing when {@link IInputMethodManager} is
+ * unavailable.</p>
+ *
+ * <p>If you want to use any of this method outside of {@code android.view.inputmethod}, create
+ * a wrapper method in {@link InputMethodManagerGlobal} instead of making this class public.</p>
+ */
+final class IInputMethodManagerGlobalInvoker {
+    @Nullable
+    private static volatile IInputMethodManager sServiceCache = null;
+
+    /**
+     * @return {@code true} if {@link IInputMethodManager} is available.
+     */
+    @AnyThread
+    static boolean isAvailable() {
+        return getService() != null;
+    }
+
+    @AnyThread
+    @Nullable
+    static IInputMethodManager getService() {
+        IInputMethodManager service = sServiceCache;
+        if (service == null) {
+            if (InputMethodManager.isInEditModeInternal()) {
+                return null;
+            }
+            service = IInputMethodManager.Stub.asInterface(
+                    ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
+            if (service == null) {
+                return null;
+            }
+            sServiceCache = service;
+        }
+        return service;
+    }
+
+    @AnyThread
+    private static void handleRemoteExceptionOrRethrow(@NonNull RemoteException e,
+            @Nullable Consumer<RemoteException> exceptionHandler) {
+        if (exceptionHandler != null) {
+            exceptionHandler.accept(e);
+        } else {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#startProtoDump(byte[], int, String)}.
+     *
+     * @param protoDump client or service side information to be stored by the server
+     * @param source where the information is coming from, refer to
+     *               {@link com.android.internal.inputmethod.ImeTracing#IME_TRACING_FROM_CLIENT} and
+     *               {@link com.android.internal.inputmethod.ImeTracing#IME_TRACING_FROM_IMS}
+     * @param where where the information is coming from.
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresNoPermission
+    static void startProtoDump(byte[] protoDump, int source, String where,
+            @Nullable Consumer<RemoteException> exceptionHandler) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.startProtoDump(protoDump, source, where);
+        } catch (RemoteException e) {
+            handleRemoteExceptionOrRethrow(e, exceptionHandler);
+        }
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#startImeTrace()}.
+     *
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
+    static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.startImeTrace();
+        } catch (RemoteException e) {
+            handleRemoteExceptionOrRethrow(e, exceptionHandler);
+        }
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#stopImeTrace()}.
+     *
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
+    static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.stopImeTrace();
+        } catch (RemoteException e) {
+            handleRemoteExceptionOrRethrow(e, exceptionHandler);
+        }
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#isImeTraceEnabled()}.
+     *
+     * @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}.
+     */
+    @AnyThread
+    @RequiresNoPermission
+    static boolean isImeTraceEnabled() {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.isImeTraceEnabled();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#removeImeSurface()}
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.removeImeSurface();
+        } catch (RemoteException e) {
+            handleRemoteExceptionOrRethrow(e, exceptionHandler);
+        }
+    }
+
+    @AnyThread
+    static void addClient(@NonNull IInputMethodClient client,
+            @NonNull IRemoteInputConnection fallbackInputConnection, int untrustedDisplayId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.addClient(client, fallbackInputConnection, untrustedDisplayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static List<InputMethodInfo> getInputMethodList(@UserIdInt int userId,
+            @DirectBootAwareness int directBootAwareness) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return new ArrayList<>();
+        }
+        try {
+            return service.getInputMethodList(userId, directBootAwareness);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return new ArrayList<>();
+        }
+        try {
+            return service.getEnabledInputMethodList(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable String imiId,
+            boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return new ArrayList<>();
+        }
+        try {
+            return service.getEnabledInputMethodSubtypeList(imiId,
+                    allowsImplicitlyEnabledSubtypes, userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @Nullable
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return null;
+        }
+        try {
+            return service.getLastInputMethodSubtype(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
+            @Nullable ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType,
+                    resultReceiver, reason);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags,
+            @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
+                    reason);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static InputBindResult startInputOrWindowGainedFocus(@StartInputReason int startInputReason,
+            @NonNull IInputMethodClient client, @Nullable IBinder windowToken,
+            @StartInputFlags int startInputFlags,
+            @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode,
+            @WindowManager.LayoutParams.Flags int windowFlags, @Nullable EditorInfo editorInfo,
+            @Nullable IRemoteInputConnection remoteInputConnection,
+            @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
+            int unverifiedTargetSdkVersion, @UserIdInt int userId,
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return InputBindResult.NULL;
+        }
+        try {
+            return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken,
+                    startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
+                    remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
+                    imeDispatcher);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static void showInputMethodPickerFromClient(@NonNull IInputMethodClient client,
+            int auxiliarySubtypeMode) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.showInputMethodPickerFromClient(client, auxiliarySubtypeMode);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    static void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+    static boolean isInputMethodPickerShownForTest() {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.isInputMethodPickerShownForTest();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @Nullable
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return null;
+        }
+        try {
+            return service.getCurrentInputMethodSubtype(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static void setAdditionalInputMethodSubtypes(@NonNull String imeId,
+            @NonNull InputMethodSubtype[] subtypes, @UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.setAdditionalInputMethodSubtypes(imeId, subtypes, userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId,
+            @NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return 0;
+        }
+        try {
+            return service.getInputMethodWindowVisibleHeight(client);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static void reportVirtualDisplayGeometryAsync(@NonNull IInputMethodClient client,
+            int childDisplayId, @Nullable float[] matrixValues) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.reportVirtualDisplayGeometryAsync(client, childDisplayId, matrixValues);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static void reportPerceptibleAsync(@NonNull IBinder windowToken, boolean perceptible) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.reportPerceptibleAsync(windowToken, perceptible);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static void removeImeSurfaceFromWindowAsync(@NonNull IBinder windowToken) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.removeImeSurfaceFromWindowAsync(windowToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    static void startStylusHandwriting(@NonNull IInputMethodClient client) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.startStylusHandwriting(client);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
+    static boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return false;
+        }
+        try {
+            return service.isStylusHandwritingAvailableAsUser(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+    static void addVirtualStylusIdForTestSession(IInputMethodClient client) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.addVirtualStylusIdForTestSession(client);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+    static void setStylusWindowIdleTimeoutForTest(
+            IInputMethodClient client, @DurationMillisLong long timeout) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.setStylusWindowIdleTimeoutForTest(client, timeout);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java
deleted file mode 100644
index 01e8b34..0000000
--- a/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java
+++ /dev/null
@@ -1,288 +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 android.view.inputmethod;
-
-import android.annotation.AnyThread;
-import android.annotation.DurationMillisLong;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.view.WindowManager;
-import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
-import android.window.ImeOnBackInvokedDispatcher;
-
-import com.android.internal.inputmethod.DirectBootAwareness;
-import com.android.internal.inputmethod.IInputMethodClient;
-import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
-import com.android.internal.inputmethod.IRemoteInputConnection;
-import com.android.internal.inputmethod.InputBindResult;
-import com.android.internal.inputmethod.SoftInputShowHideReason;
-import com.android.internal.inputmethod.StartInputFlags;
-import com.android.internal.inputmethod.StartInputReason;
-import com.android.internal.view.IInputMethodManager;
-
-import java.util.List;
-
-/**
- * A wrapper class to invoke IPCs defined in {@link IInputMethodManager}.
- */
-final class IInputMethodManagerInvoker {
-    @NonNull
-    private final IInputMethodManager mTarget;
-
-    private IInputMethodManagerInvoker(@NonNull IInputMethodManager target) {
-        mTarget = target;
-    }
-
-    @AnyThread
-    @NonNull
-    static IInputMethodManagerInvoker create(@NonNull IInputMethodManager imm) {
-        return new IInputMethodManagerInvoker(imm);
-    }
-
-    @AnyThread
-    void addClient(@NonNull IInputMethodClient client,
-            @NonNull IRemoteInputConnection fallbackInputConnection, int untrustedDisplayId) {
-        try {
-            mTarget.addClient(client, fallbackInputConnection, untrustedDisplayId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @NonNull
-    List<InputMethodInfo> getInputMethodList(@UserIdInt int userId,
-            @DirectBootAwareness int directBootAwareness) {
-        try {
-            return mTarget.getInputMethodList(userId, directBootAwareness);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @NonNull
-    List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
-        try {
-            return mTarget.getEnabledInputMethodList(userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @NonNull
-    List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable String imiId,
-            boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
-        try {
-            return mTarget.getEnabledInputMethodSubtypeList(imiId,
-                    allowsImplicitlyEnabledSubtypes, userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @Nullable
-    InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) {
-        try {
-            return mTarget.getLastInputMethodSubtype(userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
-        try {
-            return mTarget.showSoftInput(
-                    client, windowToken, flags, lastClickToolType, resultReceiver, reason);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            int flags, @Nullable ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
-        try {
-            return mTarget.hideSoftInput(client, windowToken, flags, resultReceiver, reason);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @NonNull
-    InputBindResult startInputOrWindowGainedFocus(@StartInputReason int startInputReason,
-            @NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
-            @WindowManager.LayoutParams.Flags int windowFlags, @Nullable EditorInfo editorInfo,
-            @Nullable IRemoteInputConnection remoteInputConnection,
-            @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
-            int unverifiedTargetSdkVersion, @UserIdInt int userId,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
-        try {
-            return mTarget.startInputOrWindowGainedFocus(startInputReason, client, windowToken,
-                    startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection,
-                    remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
-                    imeDispatcher);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void showInputMethodPickerFromClient(@NonNull IInputMethodClient client,
-            int auxiliarySubtypeMode) {
-        try {
-            mTarget.showInputMethodPickerFromClient(client, auxiliarySubtypeMode);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void showInputMethodPickerFromSystem(@NonNull IInputMethodClient client,
-            int auxiliarySubtypeMode, int displayId) {
-        try {
-            mTarget.showInputMethodPickerFromSystem(client, auxiliarySubtypeMode, displayId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    boolean isInputMethodPickerShownForTest() {
-        try {
-            return mTarget.isInputMethodPickerShownForTest();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    @Nullable
-    InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
-        try {
-            return mTarget.getCurrentInputMethodSubtype(userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void setAdditionalInputMethodSubtypes(@NonNull String imeId,
-            @NonNull InputMethodSubtype[] subtypes, @UserIdInt int userId) {
-        try {
-            mTarget.setAdditionalInputMethodSubtypes(imeId, subtypes, userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId,
-            @NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
-        try {
-            mTarget.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) {
-        try {
-            return mTarget.getInputMethodWindowVisibleHeight(client);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void reportVirtualDisplayGeometryAsync(@NonNull IInputMethodClient client, int childDisplayId,
-            @Nullable float[] matrixValues) {
-        try {
-            mTarget.reportVirtualDisplayGeometryAsync(client, childDisplayId, matrixValues);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void reportPerceptibleAsync(@NonNull IBinder windowToken, boolean perceptible) {
-        try {
-            mTarget.reportPerceptibleAsync(windowToken, perceptible);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void removeImeSurfaceFromWindowAsync(@NonNull IBinder windowToken) {
-        try {
-            mTarget.removeImeSurfaceFromWindowAsync(windowToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void startStylusHandwriting(@NonNull IInputMethodClient client) {
-        try {
-            mTarget.startStylusHandwriting(client);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
-        try {
-            return mTarget.isStylusHandwritingAvailableAsUser(userId);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void addVirtualStylusIdForTestSession(IInputMethodClient client) {
-        try {
-            mTarget.addVirtualStylusIdForTestSession(client);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    @AnyThread
-    void setStylusWindowIdleTimeoutForTest(
-            IInputMethodClient client, @DurationMillisLong long timeout) {
-        try {
-            mTarget.setStylusWindowIdleTimeoutForTest(client, timeout);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-}
diff --git a/core/java/android/view/inputmethod/ImeTracker.aidl b/core/java/android/view/inputmethod/ImeTracker.aidl
new file mode 100644
index 0000000..1988f48
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+parcelable ImeTracker.Token;
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
new file mode 100644
index 0000000..f4ecdff
--- /dev/null
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -0,0 +1,488 @@
+/*
+ * 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.view.inputmethod;
+
+import static android.view.inputmethod.ImeTracker.Debug.originToString;
+import static android.view.inputmethod.ImeTracker.Debug.phaseToString;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/** @hide */
+public interface ImeTracker {
+
+    String TAG = "ImeTracker";
+
+    /**
+     * The origin of the IME request
+     *
+     * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+     * where the origin is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+     */
+    @IntDef(prefix = { "ORIGIN_" }, value = {
+            ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+            ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+            ORIGIN_SERVER_START_INPUT,
+            ORIGIN_SERVER_HIDE_INPUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Origin {}
+
+    /**
+     * The IME show request originated in the client.
+     */
+    int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0;
+
+    /**
+     * The IME hide request originated in the client.
+     */
+    int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1;
+
+    /**
+     * The IME show request originated in the server.
+     */
+    int ORIGIN_SERVER_START_INPUT = 2;
+
+    /**
+     * The IME hide request originated in the server.
+     */
+    int ORIGIN_SERVER_HIDE_INPUT = 3;
+
+    /**
+     * The current phase of the IME request.
+     *
+     * The name follows the format {@code PHASE_x_...} where {@code x} denotes
+     * where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server).
+     */
+    @IntDef(prefix = { "PHASE_" }, value = {
+            PHASE_CLIENT_VIEW_SERVED,
+            PHASE_SERVER_CLIENT_KNOWN,
+            PHASE_SERVER_CLIENT_FOCUSED,
+            PHASE_SERVER_ACCESSIBILITY,
+            PHASE_SERVER_SYSTEM_READY,
+            PHASE_SERVER_HIDE_IMPLICIT,
+            PHASE_SERVER_HIDE_NOT_ALWAYS,
+            PHASE_SERVER_WAIT_IME,
+            PHASE_SERVER_HAS_IME,
+            PHASE_SERVER_SHOULD_HIDE,
+            PHASE_IME_WRAPPER,
+            PHASE_IME_WRAPPER_DISPATCH,
+            PHASE_IME_SHOW_SOFT_INPUT,
+            PHASE_IME_HIDE_SOFT_INPUT,
+            PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE,
+            PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER,
+            PHASE_SERVER_APPLY_IME_VISIBILITY,
+            PHASE_WM_SHOW_IME_RUNNER,
+            PHASE_WM_SHOW_IME_READY,
+            PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET,
+            PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS,
+            PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS,
+            PHASE_WM_REMOTE_INSETS_CONTROLLER,
+            PHASE_WM_ANIMATION_CREATE,
+            PHASE_WM_ANIMATION_RUNNING,
+            PHASE_CLIENT_SHOW_INSETS,
+            PHASE_CLIENT_HIDE_INSETS,
+            PHASE_CLIENT_HANDLE_SHOW_INSETS,
+            PHASE_CLIENT_HANDLE_HIDE_INSETS,
+            PHASE_CLIENT_APPLY_ANIMATION,
+            PHASE_CLIENT_CONTROL_ANIMATION,
+            PHASE_CLIENT_ANIMATION_RUNNING,
+            PHASE_CLIENT_ANIMATION_CANCEL,
+            PHASE_CLIENT_ANIMATION_FINISHED_SHOW,
+            PHASE_CLIENT_ANIMATION_FINISHED_HIDE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Phase {}
+
+    /** The view that requested the IME has been served by the IMM. */
+    int PHASE_CLIENT_VIEW_SERVED = 0;
+
+    /** The IME client that requested the IME has window manager focus. */
+    int PHASE_SERVER_CLIENT_KNOWN = 1;
+
+    /** The IME client that requested the IME has IME focus. */
+    int PHASE_SERVER_CLIENT_FOCUSED = 2;
+
+    /** The IME request complies with the current accessibility settings. */
+    int PHASE_SERVER_ACCESSIBILITY = 3;
+
+    /** The server is ready to run third party code. */
+    int PHASE_SERVER_SYSTEM_READY = 4;
+
+    /** Checked the implicit hide request against any explicit show requests. */
+    int PHASE_SERVER_HIDE_IMPLICIT = 5;
+
+    /** Checked the not-always hide request against any forced show requests. */
+    int PHASE_SERVER_HIDE_NOT_ALWAYS = 6;
+
+    /** The server is waiting for a connection to the IME. */
+    int PHASE_SERVER_WAIT_IME = 7;
+
+    /** The server has a connection to the IME. */
+    int PHASE_SERVER_HAS_IME = 8;
+
+    /** The server decided the IME should be hidden. */
+    int PHASE_SERVER_SHOULD_HIDE = 9;
+
+    /** Reached the IME wrapper. */
+    int PHASE_IME_WRAPPER = 10;
+
+    /** Dispatched from the IME wrapper to the IME. */
+    int PHASE_IME_WRAPPER_DISPATCH = 11;
+
+    /** Reached the IME' showSoftInput method. */
+    int PHASE_IME_SHOW_SOFT_INPUT = 12;
+
+    /** Reached the IME' hideSoftInput method. */
+    int PHASE_IME_HIDE_SOFT_INPUT = 13;
+
+    /** The server decided the IME should be shown. */
+    int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14;
+
+    /** Requested applying the IME visibility in the insets source consumer. */
+    int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15;
+
+    /** Applied the IME visibility. */
+    int PHASE_SERVER_APPLY_IME_VISIBILITY = 16;
+
+    /** Created the show IME runner. */
+    int PHASE_WM_SHOW_IME_RUNNER = 17;
+
+    /** Ready to show IME. */
+    int PHASE_WM_SHOW_IME_READY = 18;
+
+    /** The Window Manager has a connection to the IME insets control target. */
+    int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19;
+
+    /** Reached the window insets control target's show insets method. */
+    int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20;
+
+    /** Reached the window insets control target's hide insets method. */
+    int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21;
+
+    /** Reached the remote insets control target's show insets method. */
+    int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22;
+
+    /** Reached the remote insets control target's hide insets method. */
+    int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23;
+
+    /** Reached the remote insets controller. */
+    int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24;
+
+    /** Created the IME window insets show animation. */
+    int PHASE_WM_ANIMATION_CREATE = 25;
+
+    /** Started the IME window insets show animation. */
+    int PHASE_WM_ANIMATION_RUNNING = 26;
+
+    /** Reached the client's show insets method. */
+    int PHASE_CLIENT_SHOW_INSETS = 27;
+
+    /** Reached the client's hide insets method. */
+    int PHASE_CLIENT_HIDE_INSETS = 28;
+
+    /** Handling the IME window insets show request. */
+    int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29;
+
+    /** Handling the IME window insets hide request. */
+    int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30;
+
+    /** Applied the IME window insets show animation. */
+    int PHASE_CLIENT_APPLY_ANIMATION = 31;
+
+    /** Started the IME window insets show animation. */
+    int PHASE_CLIENT_CONTROL_ANIMATION = 32;
+
+    /** Queued the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_RUNNING = 33;
+
+    /** Cancelled the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_CANCEL = 34;
+
+    /** Finished the IME window insets show animation. */
+    int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35;
+
+    /** Finished the IME window insets hide animation. */
+    int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36;
+
+    /**
+     * Called when an IME show request is created.
+     *
+     * @param token the token tracking the current IME show request or {@code null} otherwise.
+     * @param origin the origin of the IME show request.
+     * @param reason the reason why the IME show request was created.
+     */
+    void onRequestShow(@Nullable Token token, @Origin int origin,
+            @SoftInputShowHideReason int reason);
+
+    /**
+     * Called when an IME hide request is created.
+     *
+     * @param token the token tracking the current IME hide request or {@code null} otherwise.
+     * @param origin the origin of the IME hide request.
+     * @param reason the reason why the IME hide request was created.
+     */
+    void onRequestHide(@Nullable Token token, @Origin int origin,
+            @SoftInputShowHideReason int reason);
+
+    /**
+     * Called when an IME request progresses to a further phase.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the new phase the IME request reached.
+     */
+    void onProgress(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request fails.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request failed at.
+     */
+    void onFailed(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request reached a flow that is not yet implemented.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request was currently at.
+     */
+    void onTodo(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when an IME request is cancelled.
+     *
+     * @param token the token tracking the current IME request or {@code null} otherwise.
+     * @param phase the phase the IME request was cancelled at.
+     */
+    void onCancelled(@Nullable Token token, @Phase int phase);
+
+    /**
+     * Called when the IME show request is successful.
+     *
+     * @param token the token tracking the current IME show request or {@code null} otherwise.
+     */
+    void onShown(@Nullable Token token);
+
+    /**
+     * Called when the IME hide request is successful.
+     *
+     * @param token the token tracking the current IME hide request or {@code null} otherwise.
+     */
+    void onHidden(@Nullable Token token);
+
+    /**
+     * Get the singleton instance of this class.
+     *
+     * @return the singleton instance of this class
+     */
+    @NonNull
+    static ImeTracker get() {
+        return SystemProperties.getBoolean("persist.debug.imetracker", false)
+                ? LOGGER
+                : NOOP_LOGGER;
+    }
+
+    /** The singleton IME tracker instance. */
+    ImeTracker LOGGER = new ImeTracker() {
+
+        @Override
+        public void onRequestShow(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin)
+                    + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+        }
+
+        @Override
+        public void onRequestHide(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin)
+                    + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason));
+        }
+
+        @Override
+        public void onProgress(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onFailed(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onTodo(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onCancelled(@Nullable Token token, int phase) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase));
+        }
+
+        @Override
+        public void onShown(@Nullable Token token) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onShown");
+        }
+
+        @Override
+        public void onHidden(@Nullable Token token) {
+            if (token == null) return;
+            Log.i(TAG, token.mTag + ": onHidden");
+        }
+    };
+
+    /** The singleton no-op IME tracker instance. */
+    ImeTracker NOOP_LOGGER = new ImeTracker() {
+
+        @Override
+        public void onRequestShow(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {}
+
+        @Override
+        public void onRequestHide(@Nullable Token token, int origin,
+                @SoftInputShowHideReason int reason) {}
+
+        @Override
+        public void onProgress(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onFailed(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onTodo(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onCancelled(@Nullable Token token, int phase) {}
+
+        @Override
+        public void onShown(@Nullable Token token) {}
+
+        @Override
+        public void onHidden(@Nullable Token token) {}
+    };
+
+    /** A token that tracks the progress of an IME request. */
+    class Token implements Parcelable {
+
+        private final IBinder mBinder;
+        private final String mTag;
+
+        public Token() {
+            this(ActivityThread.currentProcessName());
+        }
+
+        public Token(String component) {
+            this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt())));
+        }
+
+        private Token(IBinder binder, String tag) {
+            mBinder = binder;
+            mTag = tag;
+        }
+
+        /** For Parcelable, no special marshalled objects. */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeStrongBinder(mBinder);
+            dest.writeString8(mTag);
+        }
+
+        @NonNull
+        public static final Creator<Token> CREATOR = new Creator<>() {
+            @Override
+            public Token createFromParcel(Parcel source) {
+                IBinder binder = source.readStrongBinder();
+                String tag = source.readString8();
+                return new Token(binder, tag);
+            }
+
+            @Override
+            public Token[] newArray(int size) {
+                return new Token[size];
+            }
+        };
+    }
+
+    /**
+     * Utilities for mapping phases and origins IntDef values to their names.
+     *
+     * Note: This is held in a separate class so that it only gets initialized when actually needed.
+     */
+    class Debug {
+
+        private static final Map<Integer, String> sOrigins =
+                getFieldMapping(ImeTracker.class, "ORIGIN_");
+        private static final Map<Integer, String> sPhases =
+                getFieldMapping(ImeTracker.class, "PHASE_");
+
+        public static String originToString(int origin) {
+            return sOrigins.getOrDefault(origin, "ORIGIN_" + origin);
+        }
+
+        public static String phaseToString(int phase) {
+            return sPhases.getOrDefault(phase, "PHASE_" + phase);
+        }
+
+        private static Map<Integer, String> getFieldMapping(Class<?> cls, String fieldPrefix) {
+            return Arrays.stream(cls.getDeclaredFields())
+                    .filter(field -> field.getName().startsWith(fieldPrefix))
+                    .collect(Collectors.toMap(Debug::getFieldValue, Field::getName));
+        }
+
+        private static int getFieldValue(Field field) {
+            try {
+                return field.getInt(null);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index 7d268a9..9b519c3 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -16,13 +16,17 @@
 
 package android.view.inputmethod;
 
+import static android.view.inputmethod.TextBoundsInfoResult.CODE_UNSUPPORTED;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.inputmethodservice.InputMethodService;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.text.TextUtils;
 import android.view.KeyCharacterMap;
@@ -32,7 +36,9 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -1026,6 +1032,8 @@
     /**
      * Perform a handwriting gesture on text.
      *
+     * <p>Note: A supported gesture {@link EditorInfo#getSupportedHandwritingGestures()} may not
+     * have preview supported {@link EditorInfo#getSupportedHandwritingGesturePreviews()}.</p>
      * @param gesture the gesture to perform
      * @param executor The executor to run the callback on.
      * @param consumer if the caller passes a non-null consumer, the editor must invoke this
@@ -1036,6 +1044,7 @@
      * completed. Will be invoked on the given {@link Executor}.
      * Default implementation provides a callback to {@link IntConsumer} with
      * {@link #HANDWRITING_GESTURE_RESULT_UNSUPPORTED}.
+     * @see #previewHandwritingGesture(PreviewableHandwritingGesture, CancellationSignal)
      */
     default void performHandwritingGesture(
             @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
@@ -1046,14 +1055,41 @@
     }
 
     /**
+     * Preview a handwriting gesture on text.
+     * Provides a real-time preview for a gesture to user for an ongoing gesture. e.g. as user
+     * begins to draw a circle around text, resulting selection {@link SelectGesture} is previewed
+     * while stylus is moving over applicable text.
+     *
+     * <p>Note: A supported gesture {@link EditorInfo#getSupportedHandwritingGestures()} might not
+     * have preview supported {@link EditorInfo#getSupportedHandwritingGesturePreviews()}.</p>
+     * @param gesture the gesture to preview. Preview support for a gesture (regardless of whether
+     *  implemented by editor) can be determined if gesture subclasses
+     *  {@link PreviewableHandwritingGesture}. Supported previewable gestures include
+     *  {@link SelectGesture}, {@link SelectRangeGesture}, {@link DeleteGesture} and
+     *  {@link DeleteRangeGesture}.
+     * @param cancellationSignal signal to cancel an ongoing preview.
+     * @return true on successfully sending command to Editor, false if not implemented by editor or
+     * the input connection is no longer valid or preview was cancelled with
+     * {@link CancellationSignal}.
+     * @see #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)
+     */
+    default boolean previewHandwritingGesture(
+            @NonNull PreviewableHandwritingGesture gesture,
+            @Nullable CancellationSignal cancellationSignal) {
+        return false;
+    }
+
+    /**
      * The editor is requested to call
      * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)} at
      * once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
      * used together with {@link #CURSOR_UPDATE_MONITOR}.
      * <p>
      * Note by default all of {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS},
-     * {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS} and
-     * {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER} are included but specifying them can
+     * {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
+     * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE}, and
+     * {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER}, are included but specifying them can
      * filter-out others.
      * It can be CPU intensive to include all, filtering specific info is recommended.
      * </p>
@@ -1071,7 +1107,8 @@
      * <p>
      * Note by default all of {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS},
      * {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
-     * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS} and
+     * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE}, and
      * {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER}, are included but specifying them can
      * filter-out others.
      * It can be CPU intensive to include all, filtering specific info is recommended.
@@ -1087,6 +1124,7 @@
      * <p>
      * This flag can be used together with filters: {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
      * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE},
      * {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER} and update flags
      * {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
      * </p>
@@ -1102,8 +1140,8 @@
      * <p>
      * This flag can be combined with other filters: {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS},
      * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
-     * {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER} and update flags
-     * {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE}, {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER}
+     * and update flags {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
      * </p>
      */
     int CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS = 1 << 3;
@@ -1118,8 +1156,8 @@
      * <p>
      * This flag can be combined with other filters: {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
      * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
-     * {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS} and update flags {@link #CURSOR_UPDATE_IMMEDIATE}
-     * and {@link #CURSOR_UPDATE_MONITOR}.
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE}, {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS}
+     * and update flags {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
      * </p>
      */
     int CURSOR_UPDATE_FILTER_INSERTION_MARKER = 1 << 4;
@@ -1133,14 +1171,29 @@
      * {@link InputConnection#requestCursorUpdates(int)} again with this flag off.
      * <p>
      * This flag can be combined with other filters: {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
-     * {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS}, {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER}
-     * and update flags {@link #CURSOR_UPDATE_IMMEDIATE}
-     * and {@link #CURSOR_UPDATE_MONITOR}.
+     * {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS}, {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER},
+     * {@link #CURSOR_UPDATE_FILTER_TEXT_APPEARANCE} and update flags
+     * {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
      * </p>
      */
     int CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS = 1 << 5;
 
     /**
+     * The editor is requested to call
+     * {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)}
+     * with new text appearance info {@link CursorAnchorInfo#getTextAppearanceInfo()}}
+     * whenever cursor/anchor position is changed. To disable monitoring, call
+     * {@link InputConnection#requestCursorUpdates(int)} again with this flag off.
+     * <p>
+     * This flag can be combined with other filters: {@link #CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS},
+     * {@link #CURSOR_UPDATE_FILTER_EDITOR_BOUNDS}, {@link #CURSOR_UPDATE_FILTER_INSERTION_MARKER},
+     * {@link #CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS} and update flags
+     * {@link #CURSOR_UPDATE_IMMEDIATE} and {@link #CURSOR_UPDATE_MONITOR}.
+     * </p>
+     */
+    int CURSOR_UPDATE_FILTER_TEXT_APPEARANCE = 1 << 6;
+
+    /**
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
@@ -1153,7 +1206,8 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {CURSOR_UPDATE_FILTER_EDITOR_BOUNDS, CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS,
-            CURSOR_UPDATE_FILTER_INSERTION_MARKER, CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS},
+            CURSOR_UPDATE_FILTER_INSERTION_MARKER, CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS,
+            CURSOR_UPDATE_FILTER_TEXT_APPEARANCE},
             flag = true, prefix = { "CURSOR_UPDATE_FILTER_" })
     @interface CursorUpdateFilter{}
 
@@ -1205,6 +1259,40 @@
         return false;
     }
 
+
+    /**
+     * Called by input method to request the {@link TextBoundsInfo} for a range of text which is
+     * covered by or in vicinity of the given {@code RectF}. It can be used as a supplementary
+     * method to implement the handwriting gesture API -
+     * {@link #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}.
+     *
+     * <p><strong>Editor authors</strong>: It's preferred that the editor returns a
+     * {@link TextBoundsInfo} of all the text lines whose bounds intersect with the given
+     * {@code rectF}.
+     * </p>
+     *
+     * <p><strong>IME authors</strong>: This method is expensive when the text is long. Please
+     * consider that both the text bounds computation and IPC round-trip to send the data are time
+     * consuming. It's preferable to only request text bounds in smaller areas.
+     * </p>
+     *
+     * @param rectF the interested area where the text bounds are requested, in the screen
+     *              coordinates.
+     * @param executor the executor to run the callback.
+     * @param consumer the callback invoked by editor to return the result. It must return a
+     *                 non-null object.
+     *
+     * @see TextBoundsInfo
+     * @see android.view.inputmethod.TextBoundsInfoResult
+     */
+    default void requestTextBoundsInfo(
+            @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<TextBoundsInfoResult> consumer) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(consumer);
+        executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_UNSUPPORTED)));
+    }
+
     /**
      * Called by the system to enable application developers to specify a dedicated thread on which
      * {@link InputConnection} methods are called back.
@@ -1352,6 +1440,8 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the replace command was sent to the associated editor (regardless of
+     *     whether the replacement is success or not), {@code false} otherwise.
      */
     default boolean replaceText(
             @IntRange(from = 0) int start,
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 56beddf..4befd6f 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -20,13 +20,16 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.view.KeyEvent;
 
 import com.android.internal.util.Preconditions;
 
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -338,6 +341,17 @@
      * @throws NullPointerException if the target is {@code null}.
      */
     @Override
+    public boolean previewHandwritingGesture(
+            @NonNull PreviewableHandwritingGesture gesture,
+            @Nullable CancellationSignal cancellationSignal) {
+        return mTarget.previewHandwritingGesture(gesture, cancellationSignal);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @throws NullPointerException if the target is {@code null}.
+     */
+    @Override
     public boolean requestCursorUpdates(int cursorUpdateMode) {
         return mTarget.requestCursorUpdates(cursorUpdateMode);
     }
@@ -347,6 +361,17 @@
      * @throws NullPointerException if the target is {@code null}.
      */
     @Override
+    public void requestTextBoundsInfo(
+            @NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<TextBoundsInfoResult> consumer) {
+        mTarget.requestTextBoundsInfo(rectF, executor, consumer);
+    }
+
+    /**
+     * {@inheritDoc}
+     * @throws NullPointerException if the target is {@code null}.
+     */
+    @Override
     public Handler getHandler() {
         return mTarget.getHandler();
     }
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 4d5a17d..92380ed 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -300,11 +300,12 @@
      * @param showInputToken an opaque {@link android.os.Binder} token to identify which API call
      *        of {@link InputMethodManager#showSoftInput(View, int)} is associated with
      *        this callback.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      * @hide
      */
     @MainThread
-    default public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-            IBinder showInputToken) {
+    public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+            IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
         showSoftInput(flags, resultReceiver);
     }
 
@@ -338,11 +339,14 @@
      * @param hideInputToken an opaque {@link android.os.Binder} token to identify which API call
      *         of {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}} is associated
      *         with this callback.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      * @hide
      */
     @MainThread
-    public void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-            IBinder hideInputToken);
+    public default void hideSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+            IBinder hideInputToken, @Nullable ImeTracker.Token statsToken) {
+        hideSoftInput(flags, resultReceiver);
+    }
 
     /**
      * Request that any soft input part of the input method be hidden from the user.
@@ -369,7 +373,7 @@
 
     /**
      * Checks if IME is ready to start stylus handwriting session.
-     * If yes, {@link #startStylusHandwriting(InputChannel, List)} is called.
+     * If yes, {@link #startStylusHandwriting(int, InputChannel, List)} is called.
      * @param requestId
      * @hide
      */
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 18b3e21..ee31fd5 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -16,8 +16,6 @@
 
 package android.view.inputmethod;
 
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE;
 import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID;
@@ -30,7 +28,9 @@
 import static android.view.inputmethod.InputMethodManagerProto.ACTIVE;
 import static android.view.inputmethod.InputMethodManagerProto.CUR_ID;
 import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE;
+import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW;
 import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING;
+import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW;
 
 import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS;
 
@@ -68,8 +68,6 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.ResultReceiver;
-import android.os.ServiceManager;
-import android.os.ServiceManager.ServiceNotFoundException;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -128,6 +126,7 @@
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 /**
@@ -293,7 +292,7 @@
     private static final String SUBTYPE_MODE_VOICE = "voice";
 
     /**
-     * Provide this to {@link IInputMethodManagerInvoker#startInputOrWindowGainedFocus(int,
+     * Provide this to {@link IInputMethodManagerGlobalInvoker#startInputOrWindowGainedFocus(int,
      * IInputMethodClient, IBinder, int, int, int, EditorInfo,
      * com.android.internal.inputmethod.IRemoteInputConnection, IRemoteAccessibilityInputConnection,
      * int, int, ImeOnBackInvokedDispatcher)} to receive
@@ -420,16 +419,13 @@
             SystemProperties.getBoolean("debug.imm.optimize_noneditable_views", true);
 
     /**
-     * @deprecated Use {@link #mServiceInvoker} instead.
+     * @deprecated Use {@link IInputMethodManagerGlobalInvoker} instead.
      */
     @Deprecated
     @UnsupportedAppUsage
     final IInputMethodManager mService;
     private final Looper mMainLooper;
 
-    @NonNull
-    private final IInputMethodManagerInvoker mServiceInvoker;
-
     // For scheduling work on the main thread.  This also serves as our
     // global lock.
     // Remark on @UnsupportedAppUsage: there were context leaks on old versions
@@ -636,6 +632,8 @@
 
     private final DelegateImpl mDelegate = new DelegateImpl();
 
+    private static boolean sPreventImeStartupUnlessTextEditor;
+
     // -----------------------------------------------------------
 
     private static final int MSG_DUMP = 1;
@@ -733,7 +731,7 @@
      * @hide
      */
     public void reportPerceptible(@NonNull IBinder windowToken, boolean perceptible) {
-        mServiceInvoker.reportPerceptibleAsync(windowToken, perceptible);
+        IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible);
     }
 
     private final class DelegateImpl implements
@@ -763,39 +761,37 @@
                     forceFocus = true;
                 }
             }
-            startInputOnWindowFocusGain(viewForWindowFocus,
-                    windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
-        }
 
-        private void startInputOnWindowFocusGain(View focusedView,
-                @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
-            int startInputFlags = getStartInputFlags(focusedView, 0);
+            final int softInputMode = windowAttribute.softInputMode;
+            final int windowFlags = windowAttribute.flags;
+
+            int startInputFlags = getStartInputFlags(viewForWindowFocus, 0);
             startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS;
 
             ImeTracing.getInstance().triggerClientDump(
                     "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain",
                     InputMethodManager.this, null /* icProto */);
 
-            final ImeFocusController controller = getFocusController();
-            if (controller == null) {
-                return;
-            }
-
+            boolean checkFocusResult;
             synchronized (mH) {
+                if (mCurRootView == null) {
+                    return;
+                }
                 if (mRestartOnNextWindowFocus) {
                     if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true");
                     mRestartOnNextWindowFocus = false;
-                    forceNewFocus = true;
+                    forceFocus = true;
                 }
+                checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView);
             }
 
-            if (controller.checkFocus(forceNewFocus, false)) {
+            if (checkFocusResult) {
                 // We need to restart input on the current focus view.  This
                 // should be done in conjunction with telling the system service
                 // about the window gaining focus, to help make the transition
                 // smooth.
                 if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN,
-                        focusedView, startInputFlags, softInputMode, windowFlags)) {
+                        viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) {
                     return;
                 }
             }
@@ -808,9 +804,9 @@
                 }
 
                 // ignore the result
-                mServiceInvoker.startInputOrWindowGainedFocus(
+                IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
-                        focusedView.getWindowToken(), startInputFlags, softInputMode,
+                        viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode,
                         windowFlags,
                         null,
                         null, null,
@@ -825,9 +821,15 @@
         }
 
         @Override
-        public boolean checkFocus(boolean forceNewFocus, boolean startInput,
-                ViewRootImpl viewRootImpl) {
-            return checkFocusInternal(forceNewFocus, startInput, viewRootImpl);
+        public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) {
+            synchronized (mH) {
+                if (!checkFocusInternalLocked(false, viewRootImpl)) {
+                    return;
+                }
+            }
+            startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS,
+                    null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */,
+                    0 /* windowFlags */);
         }
 
         @Override
@@ -897,8 +899,6 @@
     /**
      * Checks whether the active input connection (if any) is for the given view.
      *
-     * TODO(b/182259171): Clean-up hasActiveConnection to simplify the logic.
-     *
      * Note that this method is only intended for restarting input after focus gain
      * (e.g. b/160391516), DO NOT leverage this method to do another check.
      */
@@ -909,7 +909,6 @@
             }
 
             return mServedInputConnection != null
-                    && mServedInputConnection.isActive()
                     && mServedInputConnection.isAssociatedWith(view);
         }
     }
@@ -937,15 +936,6 @@
         return mCurRootView != null ? mNextServedView : null;
     }
 
-    private ImeFocusController getFocusController() {
-        synchronized (mH) {
-            if (mCurRootView != null) {
-                return mCurRootView.getImeFocusController();
-            }
-            return null;
-        }
-    }
-
     /**
      * Returns {@code true} when the given view has been served by Input Method.
      */
@@ -1128,14 +1118,16 @@
                         if (mCurRootView == null) {
                             return;
                         }
-                        if (!mCurRootView.getImeFocusController().checkFocus(
-                                mRestartOnNextWindowFocus, false)) {
+                        if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) {
                             return;
                         }
-                        final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
-                                : StartInputReason.DEACTIVATED_BY_IMMS;
-                        startInputOnWindowFocusGainInternal(reason, null, 0, 0, 0);
+                        mCurrentEditorInfo = null;
+                        mCompletions = null;
+                        mServedConnecting = true;
                     }
+                    final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
+                            : StartInputReason.DEACTIVATED_BY_IMMS;
+                    startInputInner(reason, null, 0, 0, 0);
                     return;
                 }
                 case MSG_SET_INTERACTIVE: {
@@ -1357,6 +1349,10 @@
         return false;
     }
 
+    static boolean isInEditModeInternal() {
+        return isInEditMode();
+    }
+
     @NonNull
     private static InputMethodManager createInstance(int displayId, Looper looper) {
         return isInEditMode() ? createStubInstance(displayId, looper)
@@ -1365,12 +1361,9 @@
 
     @NonNull
     private static InputMethodManager createRealInstance(int displayId, Looper looper) {
-        final IInputMethodManager service;
-        try {
-            service = IInputMethodManager.Stub.asInterface(
-                    ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
-        } catch (ServiceNotFoundException e) {
-            throw new IllegalStateException(e);
+        final IInputMethodManager service = IInputMethodManagerGlobalInvoker.getService();
+        if (service == null) {
+            throw new IllegalStateException("IInputMethodManager is not available");
         }
         final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
         // InputMethodManagerService#addClient() relies on Binder.getCalling{Pid, Uid}() to
@@ -1382,7 +1375,8 @@
         // 1) doing so has no effect for A and 2) doing so is sufficient for B.
         final long identity = Binder.clearCallingIdentity();
         try {
-            imm.mServiceInvoker.addClient(imm.mClient, imm.mFallbackInputConnection, displayId);
+            IInputMethodManagerGlobalInvoker.addClient(imm.mClient, imm.mFallbackInputConnection,
+                    displayId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
@@ -1422,7 +1416,6 @@
 
     private InputMethodManager(@NonNull IInputMethodManager service, int displayId, Looper looper) {
         mService = service;  // For @UnsupportedAppUsage
-        mServiceInvoker = IInputMethodManagerInvoker.create(service);
         mMainLooper = looper;
         mH = new H(looper);
         mDisplayId = displayId;
@@ -1444,6 +1437,10 @@
         // display case.
         final Looper looper = displayId == Display.DEFAULT_DISPLAY
                 ? Looper.getMainLooper() : context.getMainLooper();
+        // Keep track of whether to expect the IME to be unavailable so as to avoid log spam in
+        // sendInputEventOnMainLooperLocked() by not logging a verbose message on every DPAD event
+        sPreventImeStartupUnlessTextEditor = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
         return forContextInternal(displayId, looper);
     }
 
@@ -1516,7 +1513,8 @@
         // We intentionally do not use UserHandle.getCallingUserId() here because for system
         // services InputMethodManagerInternal.getInputMethodListAsUser() should be used
         // instead.
-        return mServiceInvoker.getInputMethodList(UserHandle.myUserId(), DirectBootAwareness.AUTO);
+        return IInputMethodManagerGlobalInvoker.getInputMethodList(UserHandle.myUserId(),
+                DirectBootAwareness.AUTO);
     }
 
     /**
@@ -1532,12 +1530,18 @@
     /**
      * Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled for
      * the given userId.
-     * If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be
-     * called and Stylus touch should continue as normal touch input.
+     *
+     * <p>If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be
+     * called and Stylus touch should continue as normal touch input.</p>
+     *
+     * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+     * {@code userId} is different from the user id of the current process.</p>
+     *
      * @see #startStylusHandwriting(View)
      * @param userId user ID to query.
      * @hide
      */
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
         final Context fallbackContext = ActivityThread.currentApplication();
         if (fallbackContext == null) {
@@ -1550,37 +1554,44 @@
             }
             return false;
         }
-        return mServiceInvoker.isStylusHandwritingAvailableAsUser(userId);
+        return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser(userId);
     }
 
     /**
      * Returns the list of installed input methods for the specified user.
      *
+     * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+     * {@code userId} is different from the user id of the current process.</p>
+     *
      * @param userId user ID to query
      * @return {@link List} of {@link InputMethodInfo}.
      * @hide
      */
     @TestApi
-    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
     @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
-        return mServiceInvoker.getInputMethodList(userId, DirectBootAwareness.AUTO);
+        return IInputMethodManagerGlobalInvoker.getInputMethodList(userId,
+                DirectBootAwareness.AUTO);
     }
 
     /**
      * Returns the list of installed input methods for the specified user.
      *
+     * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+     * {@code userId} is different from the user id of the current process.</p>
+     *
      * @param userId user ID to query
      * @param directBootAwareness {@code true} if caller want to query installed input methods list
      * on user locked state.
      * @return {@link List} of {@link InputMethodInfo}.
      * @hide
      */
-    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
     @NonNull
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId,
             @DirectBootAwareness int directBootAwareness) {
-        return mServiceInvoker.getInputMethodList(userId, directBootAwareness);
+        return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, directBootAwareness);
     }
 
     /**
@@ -1595,19 +1606,22 @@
         // We intentionally do not use UserHandle.getCallingUserId() here because for system
         // services InputMethodManagerInternal.getEnabledInputMethodListAsUser() should be used
         // instead.
-        return mServiceInvoker.getEnabledInputMethodList(UserHandle.myUserId());
+        return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(UserHandle.myUserId());
     }
 
     /**
      * Returns the list of enabled input methods for the specified user.
      *
+     * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+     * {@code userId} is different from the user id of the current process.</p>
+     *
      * @param userId user ID to query
      * @return {@link List} of {@link InputMethodInfo}.
      * @hide
      */
-    @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
-        return mServiceInvoker.getEnabledInputMethodList(userId);
+        return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(userId);
     }
 
     /**
@@ -1624,7 +1638,7 @@
     @NonNull
     public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable InputMethodInfo imi,
             boolean allowsImplicitlyEnabledSubtypes) {
-        return mServiceInvoker.getEnabledInputMethodSubtypeList(
+        return IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList(
                 imi == null ? null : imi.getId(),
                 allowsImplicitlyEnabledSubtypes,
                 UserHandle.myUserId());
@@ -1908,8 +1922,11 @@
      * a result receiver: explicitly request that the current input method's
      * soft input area be shown to the user, if needed.
      *
-     * @param view The currently focused view, which would like to receive
-     * soft keyboard input.
+     * @param view The currently focused view, which would like to receive soft keyboard input.
+     *             Note that this view is only considered focused here if both it itself has
+     *             {@link View#isFocused view focus}, and its containing window has
+     *             {@link View#hasWindowFocus window focus}. Otherwise the call fails and
+     *             returns {@code false}.
      * @param flags Provides additional operating flags.  Currently may be
      * 0 or have the {@link #SHOW_IMPLICIT} bit set.
      */
@@ -1971,8 +1988,11 @@
      * can be garbage collected regardless of the lifetime of
      * {@link ResultReceiver}.
      *
-     * @param view The currently focused view, which would like to receive
-     * soft keyboard input.
+     * @param view The currently focused view, which would like to receive soft keyboard input.
+     *             Note that this view is only considered focused here if both it itself has
+     *             {@link View#isFocused view focus}, and its containing window has
+     *             {@link View#hasWindowFocus window focus}. Otherwise the call fails and
+     *             returns {@code false}.
      * @param flags Provides additional operating flags.  Currently may be
      * 0 or have the {@link #SHOW_IMPLICIT} bit set.
      * @param resultReceiver If non-null, this will be called by the IME when
@@ -1987,6 +2007,10 @@
 
     private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+                reason);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this,
                 null /* icProto */);
         // Re-dispatch if there is a context mismatch.
@@ -1998,18 +2022,22 @@
         checkFocus();
         synchronized (mH) {
             if (!hasServedByInputMethodLocked(view)) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "Ignoring showSoftInput() as view=" + view + " is not served.");
                 return false;
             }
 
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
             // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
             // TODO(b/229426865): call WindowInsetsController#show instead.
             mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
             Log.d(TAG, "showSoftInput() view=" + view + " flags=" + flags + " reason="
                     + InputMethodDebug.softInputDisplayReasonToString(reason));
-            return mServiceInvoker.showSoftInput(
+            return IInputMethodManagerGlobalInvoker.showSoftInput(
                     mClient,
                     view.getWindowToken(),
+                    statsToken,
                     flags,
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
@@ -2029,19 +2057,28 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
     public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
         synchronized (mH) {
+            final ImeTracker.Token statsToken = new ImeTracker.Token();
+            ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
+                    SoftInputShowHideReason.SHOW_SOFT_INPUT);
+
             Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be"
                     + " removed soon. If you are using androidx.appcompat.widget.SearchView,"
                     + " please update to version 26.0 or newer version.");
             if (mCurRootView == null || mCurRootView.getView() == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "No current root view, ignoring showSoftInputUnchecked()");
                 return;
             }
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
             // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
             // TODO(b/229426865): call WindowInsetsController#show instead.
             mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED));
-            mServiceInvoker.showSoftInput(
+            IInputMethodManagerGlobalInvoker.showSoftInput(
                     mClient,
                     mCurRootView.getView().getWindowToken(),
+                    statsToken,
                     flags,
                     mCurRootView.getLastClickToolType(),
                     resultReceiver,
@@ -2111,17 +2148,24 @@
 
     private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                reason);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
                 this, null /* icProto */);
         checkFocus();
         synchronized (mH) {
             final View servedView = getServedViewLocked();
             if (servedView == null || servedView.getWindowToken() != windowToken) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 return false;
             }
 
-            return mServiceInvoker.hideSoftInput(mClient, windowToken, flags, resultReceiver,
-                    reason);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+            return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+                    flags, resultReceiver, reason);
         }
     }
 
@@ -2169,7 +2213,7 @@
                 return;
             }
 
-            mServiceInvoker.startStylusHandwriting(mClient);
+            IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient);
             // TODO(b/210039666): do we need any extra work for supporting non-native
             //   UI toolkits?
         }
@@ -2349,11 +2393,14 @@
     }
 
     /**
-     * Called from {@link #checkFocusInternal(boolean, boolean, ViewRootImpl)},
-     * {@link #restartInput(View)}, {@link #MSG_BIND} or {@link #MSG_UNBIND}.
+     * Starts an input connection from the served view that gains the window focus.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
+     *
+     * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+     * {@code userId} is different from the user id of the current process.</p>
      */
+    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
     private boolean startInputInner(@StartInputReason int startInputReason,
             @Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
             @SoftInputModeFlags int softInputMode, int windowFlags) {
@@ -2503,7 +2550,7 @@
             }
             final int targetUserId = editorInfo.targetInputMethodUser != null
                     ? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId();
-            res = mServiceInvoker.startInputOrWindowGainedFocus(
+            res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus(
                     startInputReason, mClient, windowGainingFocus, startInputFlags,
                     softInputMode, windowFlags, editorInfo, servedInputConnection,
                     servedInputConnection == null ? null
@@ -2597,9 +2644,10 @@
      * @hide
      */
     @TestApi
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
     public void addVirtualStylusIdForTestSession() {
         synchronized (mH) {
-            mServiceInvoker.addVirtualStylusIdForTestSession(mClient);
+            IInputMethodManagerGlobalInvoker.addVirtualStylusIdForTestSession(mClient);
         }
     }
 
@@ -2609,11 +2657,11 @@
      * @param timeout to set in milliseconds. To reset to default, use a value <= zero.
      * @hide
      */
-    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
     @TestApi
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
     public void setStylusWindowIdleTimeoutForTest(@DurationMillisLong long timeout) {
         synchronized (mH) {
-            mServiceInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout);
+            IInputMethodManagerGlobalInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout);
         }
     }
 
@@ -2658,52 +2706,53 @@
     }
 
     /**
-     * Check the next served view from {@link ImeFocusController} if needs to start input.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      * @hide
      */
     @UnsupportedAppUsage
     public void checkFocus() {
-        final ImeFocusController controller = getFocusController();
-        if (controller != null) {
-            controller.checkFocus(false /* forceNewFocus */, true /* startInput */);
+        synchronized (mH) {
+            if (mCurRootView == null) {
+                return;
+            }
+            if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) {
+                return;
+            }
         }
+        startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
+                null /* focusedView */,
+                0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
     }
 
-    private boolean checkFocusInternal(boolean forceNewFocus, boolean startInput,
-            ViewRootImpl viewRootImpl) {
-        synchronized (mH) {
-            if (mCurRootView != viewRootImpl) {
-                return false;
-            }
-            if (mServedView == mNextServedView && !forceNewFocus) {
-                return false;
-            }
-            if (DEBUG) {
-                Log.v(TAG, "checkFocus: view=" + mServedView
-                        + " next=" + mNextServedView
-                        + " force=" + forceNewFocus
-                        + " package="
-                        + (mServedView != null ? mServedView.getContext().getPackageName()
-                        : "<none>"));
-            }
-            // Close the connection when no next served view coming.
-            if (mNextServedView == null) {
-                finishInputLocked();
-                closeCurrentInput();
-                return false;
-            }
-            mServedView = mNextServedView;
-            if (mServedInputConnection != null) {
-                mServedInputConnection.finishComposingTextFromImm();
-            }
+    /**
+     * Check the next served view if needs to start input.
+     */
+    @GuardedBy("mH")
+    private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) {
+        if (mCurRootView != viewRootImpl) {
+            return false;
         }
-
-        if (startInput) {
-            startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
-                    null /* focusedView */,
-                    0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+        if (mServedView == mNextServedView && !forceNewFocus) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "checkFocus: view=" + mServedView
+                    + " next=" + mNextServedView
+                    + " force=" + forceNewFocus
+                    + " package="
+                    + (mServedView != null ? mServedView.getContext().getPackageName()
+                    : "<none>"));
+        }
+        // Close the connection when no next served view coming.
+        if (mNextServedView == null) {
+            finishInputLocked();
+            closeCurrentInput();
+            return false;
+        }
+        mServedView = mNextServedView;
+        if (mServedInputConnection != null) {
+            mServedInputConnection.finishComposingTextFromImm();
         }
         return true;
     }
@@ -2744,14 +2793,23 @@
 
     @UnsupportedAppUsage
     void closeCurrentInput() {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                SoftInputShowHideReason.HIDE_SOFT_INPUT);
+
         synchronized (mH) {
             if (mCurRootView == null || mCurRootView.getView() == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
                 Log.w(TAG, "No current root view, ignoring closeCurrentInput()");
                 return;
             }
-            mServiceInvoker.hideSoftInput(
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+            IInputMethodManagerGlobalInvoker.hideSoftInput(
                     mClient,
                     mCurRootView.getView().getWindowToken(),
+                    statsToken,
                     HIDE_NOT_ALWAYS,
                     null,
                     SoftInputShowHideReason.HIDE_SOFT_INPUT);
@@ -2820,15 +2878,24 @@
      * @hide
      */
     public void notifyImeHidden(IBinder windowToken) {
+        final ImeTracker.Token statsToken = new ImeTracker.Token();
+        ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+                SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+
         ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this,
                 null /* icProto */);
         synchronized (mH) {
-            if (isImeSessionAvailableLocked() && mCurRootView != null
-                    && mCurRootView.getWindowToken() == windowToken) {
-                mServiceInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */,
-                        null /* resultReceiver */,
-                        SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+            if (!isImeSessionAvailableLocked() || mCurRootView == null
+                    || mCurRootView.getWindowToken() != windowToken) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+                return;
             }
+
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
+
+            IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, statsToken,
+                    0 /* flags */, null /* resultReceiver */,
+                    SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
         }
     }
 
@@ -2839,7 +2906,7 @@
      */
     public void removeImeSurface(@NonNull IBinder windowToken) {
         synchronized (mH) {
-            mServiceInvoker.removeImeSurfaceFromWindowAsync(windowToken);
+            IInputMethodManagerGlobalInvoker.removeImeSurfaceFromWindowAsync(windowToken);
         }
     }
 
@@ -3088,7 +3155,7 @@
      *
      * <p>On Android {@link Build.VERSION_CODES#Q} and later devices, the undocumented behavior that
      * token can be {@code null} when the caller has
-     * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update
+     * {@link Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update
      * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
      * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
      *
@@ -3096,6 +3163,8 @@
      * when it was started, which allows it to perform this operation on
      * itself.
      * @param id The unique identifier for the new input method to be switched to.
+     * @throws IllegalArgumentException if the input method is unknown or filtered by the rules of
+     * <a href="/training/basics/intents/package-visibility">package visibility</a>.
      * @deprecated Use {@link InputMethodService#switchInputMethod(String)}
      * instead. This method was intended for IME developers who should be accessing APIs through
      * the service. APIs in this class are intended for app developers interacting with the IME.
@@ -3120,7 +3189,7 @@
             if (fallbackContext == null) {
                 return;
             }
-            if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+            if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
                     != PackageManager.PERMISSION_GRANTED) {
                 return;
             }
@@ -3157,7 +3226,7 @@
      * from an application or a service which has a token of the currently active input method.
      *
      * <p>On Android {@link Build.VERSION_CODES#Q} and later devices, {@code token} cannot be
-     * {@code null} even with {@link android.Manifest.permission#WRITE_SECURE_SETTINGS}. Instead,
+     * {@code null} even with {@link Manifest.permission#WRITE_SECURE_SETTINGS}. Instead,
      * update {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
      * {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
      *
@@ -3166,6 +3235,8 @@
      * itself.
      * @param id The unique identifier for the new input method to be switched to.
      * @param subtype The new subtype of the new input method to be switched to.
+     * @throws IllegalArgumentException if the input method is unknown or filtered by the rules of
+     * <a href="/training/basics/intents/package-visibility">package visibility</a>.
      * @deprecated Use
      * {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}
      * instead. This method was intended for IME developers who should be accessing APIs through
@@ -3341,8 +3412,12 @@
                 return DISPATCH_IN_PROGRESS;
             }
 
-            Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
-                    + " dropping: " + event);
+            if (sPreventImeStartupUnlessTextEditor) {
+                Log.d(TAG, "Dropping event because IME is evicted: " + event);
+            } else {
+                Log.w(TAG, "Unable to send input event to IME: " + getImeIdLocked()
+                        + " dropping: " + event);
+            }
         }
         return DISPATCH_NOT_HANDLED;
     }
@@ -3439,17 +3514,18 @@
      * @param displayId The ID of the display where the chooser dialog should be shown.
      * @hide
      */
-    @RequiresPermission(WRITE_SECURE_SETTINGS)
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
     public void showInputMethodPickerFromSystem(boolean showAuxiliarySubtypes, int displayId) {
         final int mode = showAuxiliarySubtypes
                 ? SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES
                 : SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES;
-        mServiceInvoker.showInputMethodPickerFromSystem(mClient, mode, displayId);
+        IInputMethodManagerGlobalInvoker.showInputMethodPickerFromSystem(mode, displayId);
     }
 
     @GuardedBy("mH")
     private void showInputMethodPickerLocked() {
-        mServiceInvoker.showInputMethodPickerFromClient(mClient, SHOW_IM_PICKER_MODE_AUTO);
+        IInputMethodManagerGlobalInvoker.showInputMethodPickerFromClient(mClient,
+                SHOW_IM_PICKER_MODE_AUTO);
     }
 
     /**
@@ -3464,8 +3540,9 @@
      * @hide
      */
     @TestApi
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
     public boolean isInputMethodPickerShown() {
-        return mServiceInvoker.isInputMethodPickerShownForTest();
+        return IInputMethodManagerGlobalInvoker.isInputMethodPickerShownForTest();
     }
 
     /**
@@ -3504,7 +3581,7 @@
      */
     @Nullable
     public InputMethodSubtype getCurrentInputMethodSubtype() {
-        return mServiceInvoker.getCurrentInputMethodSubtype(UserHandle.myUserId());
+        return IInputMethodManagerGlobalInvoker.getCurrentInputMethodSubtype(UserHandle.myUserId());
     }
 
     /**
@@ -3516,11 +3593,11 @@
      *             {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}, which
      *             does not require any permission as long as the caller is the current IME.
      *             If the calling process is some privileged app that already has
-     *             {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission, just
+     *             {@link Manifest.permission#WRITE_SECURE_SETTINGS} permission, just
      *             directly update {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}.
      */
     @Deprecated
-    @RequiresPermission(WRITE_SECURE_SETTINGS)
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) {
         if (Process.myUid() == Process.SYSTEM_UID) {
             Log.w(TAG, "System process should not call setCurrentInputMethodSubtype() because "
@@ -3537,7 +3614,7 @@
         if (fallbackContext == null) {
             return false;
         }
-        if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+        if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
                 != PackageManager.PERMISSION_GRANTED) {
             return false;
         }
@@ -3549,7 +3626,7 @@
             return false;
         }
         final List<InputMethodSubtype> enabledSubtypes =
-                mServiceInvoker.getEnabledInputMethodSubtypeList(imeId, true,
+                IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList(imeId, true,
                         UserHandle.myUserId());
         final int numSubtypes = enabledSubtypes.size();
         for (int i = 0; i < numSubtypes; ++i) {
@@ -3615,7 +3692,33 @@
     @UnsupportedAppUsage(trackingBug = 204906124, maxTargetSdk = Build.VERSION_CODES.TIRAMISU,
             publicAlternatives = "Use {@link android.view.WindowInsets} instead")
     public int getInputMethodWindowVisibleHeight() {
-        return mServiceInvoker.getInputMethodWindowVisibleHeight(mClient);
+        return IInputMethodManagerGlobalInvoker.getInputMethodWindowVisibleHeight(mClient);
+    }
+
+    /**
+     * {@code true} means that
+     * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)} returns
+     * {@code false} when the IME client and the IME run in different displays.
+     */
+    final AtomicBoolean mRequestCursorUpdateDisplayIdCheck = new AtomicBoolean(true);
+
+    /**
+     * Controls the display ID mismatch validation in
+     * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)}.
+     *
+     * <p>{@link #updateCursorAnchorInfo(View, CursorAnchorInfo)} is not guaranteed to work
+     * correctly when the IME client and the IME run in different displays.  This is why
+     * {@link RemoteInputConnectionImpl#requestCursorUpdatesInternal(int, int, int)} returns
+     * {@code false} by default when the display ID does not match. This method allows special apps
+     * to override this behavior when they are sure that it should work.</p>
+     *
+     * <p>By default the validation is enabled.</p>
+     *
+     * @param enabled {@code false} to disable the display ID validation.
+     * @hide
+     */
+    public void setRequestCursorUpdateDisplayIdCheck(boolean enabled) {
+        mRequestCursorUpdateDisplayIdCheck.set(enabled);
     }
 
     /**
@@ -3635,7 +3738,8 @@
             matrixValues = new float[9];
             matrix.getValues(matrixValues);
         }
-        mServiceInvoker.reportVirtualDisplayGeometryAsync(mClient, childDisplayId, matrixValues);
+        IInputMethodManagerGlobalInvoker.reportVirtualDisplayGeometryAsync(mClient, childDisplayId,
+                matrixValues);
     }
 
     /**
@@ -3742,7 +3846,8 @@
     @Deprecated
     public void setAdditionalInputMethodSubtypes(@NonNull String imiId,
             @NonNull InputMethodSubtype[] subtypes) {
-        mServiceInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes, UserHandle.myUserId());
+        IInputMethodManagerGlobalInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes,
+                UserHandle.myUserId());
     }
 
     /**
@@ -3791,8 +3896,8 @@
      */
     public void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imiId,
             @NonNull int[] subtypeHashCodes) {
-        mServiceInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId, subtypeHashCodes,
-                UserHandle.myUserId());
+        IInputMethodManagerGlobalInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId,
+                subtypeHashCodes, UserHandle.myUserId());
     }
 
     /**
@@ -3802,7 +3907,7 @@
      */
     @Nullable
     public InputMethodSubtype getLastInputMethodSubtype() {
-        return mServiceInvoker.getLastInputMethodSubtype(UserHandle.myUserId());
+        return IInputMethodManagerGlobalInvoker.getLastInputMethodSubtype(UserHandle.myUserId());
     }
 
     /**
@@ -3914,7 +4019,7 @@
 
         /**
          * As reported by {@link InputBindResult}. This value is determined by
-         * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
+         * {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecker}.
          */
         final boolean mIsInputMethodSuppressingSpellChecker;
 
@@ -3996,6 +4101,8 @@
             proto.write(FULLSCREEN_MODE, mFullscreenMode);
             proto.write(ACTIVE, mActive);
             proto.write(SERVED_CONNECTING, mServedConnecting);
+            proto.write(SERVED_VIEW, Objects.toString(mServedView));
+            proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView));
             proto.end(token);
             if (mCurRootView != null) {
                 mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL);
diff --git a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
new file mode 100644
index 0000000..5df9fd1
--- /dev/null
+++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
@@ -0,0 +1,105 @@
+/*
+ * 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.view.inputmethod;
+
+import android.Manifest;
+import android.annotation.AnyThread;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.os.RemoteException;
+
+import com.android.internal.inputmethod.ImeTracing;
+import com.android.internal.view.IInputMethodManager;
+
+import java.util.function.Consumer;
+
+/**
+ * Defines a set of static methods that can be used globally by framework classes.
+ *
+ * @hide
+ */
+public class InputMethodManagerGlobal {
+    /**
+     * @return {@code true} if IME tracing is currently is available.
+     */
+    @AnyThread
+    public static boolean isImeTraceAvailable() {
+        return IInputMethodManagerGlobalInvoker.isAvailable();
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#startProtoDump(byte[], int, String)}.
+     *
+     * @param protoDump client or service side information to be stored by the server
+     * @param source where the information is coming from, refer to
+     *               {@link ImeTracing#IME_TRACING_FROM_CLIENT} and
+     *               {@link ImeTracing#IME_TRACING_FROM_IMS}
+     * @param where where the information is coming from.
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresNoPermission
+    public static void startProtoDump(byte[] protoDump, int source, String where,
+            @Nullable Consumer<RemoteException> exceptionHandler) {
+        IInputMethodManagerGlobalInvoker.startProtoDump(protoDump, source, where, exceptionHandler);
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#startImeTrace()}.
+     *
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
+    public static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
+        IInputMethodManagerGlobalInvoker.startImeTrace(exceptionHandler);
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#stopImeTrace()}.
+     *
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
+    public static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
+        IInputMethodManagerGlobalInvoker.stopImeTrace(exceptionHandler);
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#isImeTraceEnabled()}.
+     *
+     * @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}.
+     */
+    @AnyThread
+    @RequiresNoPermission
+    public static boolean isImeTraceEnabled() {
+        return IInputMethodManagerGlobalInvoker.isImeTraceEnabled();
+    }
+
+    /**
+     * Invokes {@link IInputMethodManager#removeImeSurface()}
+     *
+     * @param exceptionHandler an optional {@link RemoteException} handler.
+     */
+    @AnyThread
+    @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
+        IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler);
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java
index 121839b..6a02198 100644
--- a/core/java/android/view/inputmethod/InputMethodSubtype.java
+++ b/core/java/android/view/inputmethod/InputMethodSubtype.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -23,6 +24,7 @@
 import android.content.res.Configuration;
 import android.icu.text.DisplayContext;
 import android.icu.text.LocaleDisplayNames;
+import android.icu.util.ULocale;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -74,12 +76,17 @@
     /** {@hide} */
     public static final int SUBTYPE_ID_NONE = 0;
 
+    private static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
+
+    private static final String UNDEFINED_LANGUAGE_TAG = "und";
+
     private final boolean mIsAuxiliary;
     private final boolean mOverridesImplicitlyEnabledSubtype;
     private final boolean mIsAsciiCapable;
     private final int mSubtypeHashCode;
     private final int mSubtypeIconResId;
     private final int mSubtypeNameResId;
+    private final CharSequence mSubtypeNameOverride;
     private final int mSubtypeId;
     private final String mSubtypeLocale;
     private final String mSubtypeLanguageTag;
@@ -90,6 +97,14 @@
     private volatile HashMap<String, String> mExtraValueHashMapCache;
 
     /**
+     * A volatile cache to optimize {@link #getCanonicalizedLanguageTag()}.
+     *
+     * <p>{@code null} means that the initial evaluation is not yet done.</p>
+     */
+    @Nullable
+    private volatile String mCachedCanonicalizedLanguageTag;
+
+    /**
      * InputMethodSubtypeBuilder is a builder class of InputMethodSubtype.
      * This class is designed to be used with
      * {@link android.view.inputmethod.InputMethodManager#setAdditionalInputMethodSubtypes}.
@@ -160,6 +175,21 @@
         private int mSubtypeNameResId = 0;
 
         /**
+         * Sets the untranslatable name of the subtype.
+         *
+         * This string is used as the subtype's display name if subtype's name res Id is 0.
+         *
+         * @param nameOverride is the name to set.
+         */
+        @NonNull
+        public InputMethodSubtypeBuilder setSubtypeNameOverride(
+                @NonNull CharSequence nameOverride) {
+            mSubtypeNameOverride = nameOverride;
+            return this;
+        }
+        private CharSequence mSubtypeNameOverride = "";
+
+        /**
          * @param subtypeId is the unique ID for this subtype. The input method framework keeps
          * track of enabled subtypes by ID. When the IME package gets upgraded, enabled IDs will
          * stay enabled even if other attributes are different. If the ID is unspecified or 0,
@@ -215,23 +245,23 @@
         public InputMethodSubtype build() {
             return new InputMethodSubtype(this);
         }
-     }
+    }
 
-     private static InputMethodSubtypeBuilder getBuilder(int nameId, int iconId, String locale,
-             String mode, String extraValue, boolean isAuxiliary,
-             boolean overridesImplicitlyEnabledSubtype, int id, boolean isAsciiCapable) {
-         final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
-         builder.mSubtypeNameResId = nameId;
-         builder.mSubtypeIconResId = iconId;
-         builder.mSubtypeLocale = locale;
-         builder.mSubtypeMode = mode;
-         builder.mSubtypeExtraValue = extraValue;
-         builder.mIsAuxiliary = isAuxiliary;
-         builder.mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
-         builder.mSubtypeId = id;
-         builder.mIsAsciiCapable = isAsciiCapable;
-         return builder;
-     }
+    private static InputMethodSubtypeBuilder getBuilder(int nameId, int iconId,
+            String locale, String mode, String extraValue, boolean isAuxiliary,
+            boolean overridesImplicitlyEnabledSubtype, int id, boolean isAsciiCapable) {
+        final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
+        builder.mSubtypeNameResId = nameId;
+        builder.mSubtypeIconResId = iconId;
+        builder.mSubtypeLocale = locale;
+        builder.mSubtypeMode = mode;
+        builder.mSubtypeExtraValue = extraValue;
+        builder.mIsAuxiliary = isAuxiliary;
+        builder.mOverridesImplicitlyEnabledSubtype = overridesImplicitlyEnabledSubtype;
+        builder.mSubtypeId = id;
+        builder.mIsAsciiCapable = isAsciiCapable;
+        return builder;
+    }
 
     /**
      * Constructor with no subtype ID specified.
@@ -291,6 +321,7 @@
      */
     private InputMethodSubtype(InputMethodSubtypeBuilder builder) {
         mSubtypeNameResId = builder.mSubtypeNameResId;
+        mSubtypeNameOverride = builder.mSubtypeNameOverride;
         mSubtypeIconResId = builder.mSubtypeIconResId;
         mSubtypeLocale = builder.mSubtypeLocale;
         mSubtypeLanguageTag = builder.mSubtypeLanguageTag;
@@ -313,6 +344,8 @@
     InputMethodSubtype(Parcel source) {
         String s;
         mSubtypeNameResId = source.readInt();
+        CharSequence cs = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+        mSubtypeNameOverride = cs != null ? cs : "";
         mSubtypeIconResId = source.readInt();
         s = source.readString();
         mSubtypeLocale = s != null ? s : "";
@@ -337,6 +370,14 @@
     }
 
     /**
+     * @return The subtype's untranslatable name string.
+     */
+    @NonNull
+    public CharSequence getNameOverride() {
+        return mSubtypeNameOverride;
+    }
+
+    /**
      * @return Resource ID of the subtype icon drawable.
      */
     public int getIconResId() {
@@ -392,6 +433,65 @@
     }
 
     /**
+     * Returns a canonicalized BCP 47 Language Tag initialized with {@link #getLocaleObject()}.
+     *
+     * <p>This has an internal cache mechanism.  Subsequent calls are in general cheap and fast.</p>
+     *
+     * @return a canonicalized BCP 47 Language Tag initialized with {@link #getLocaleObject()}. An
+     *         empty string if {@link #getLocaleObject()} returns {@code null} or an empty
+     *         {@link Locale} object.
+     * @hide
+     */
+    @AnyThread
+    @NonNull
+    public String getCanonicalizedLanguageTag() {
+        final String cachedValue = mCachedCanonicalizedLanguageTag;
+        if (cachedValue != null) {
+            return cachedValue;
+        }
+
+        String result = null;
+        final Locale locale = getLocaleObject();
+        if (locale != null) {
+            final String langTag = locale.toLanguageTag();
+            if (!TextUtils.isEmpty(langTag)) {
+                result = ULocale.createCanonical(ULocale.forLanguageTag(langTag)).toLanguageTag();
+            }
+        }
+        result = TextUtils.emptyIfNull(result);
+        mCachedCanonicalizedLanguageTag = result;
+        return result;
+    }
+
+    /**
+     * Determines whether this {@link InputMethodSubtype} can be used as the key of mapping rules
+     * between {@link InputMethodSubtype} and hardware keyboard layout.
+     *
+     * <p>Note that in a future build may require different rules.  Design the system so that the
+     * system can automatically take care of any rule changes upon OTAs.</p>
+     *
+     * @return {@code true} if this {@link InputMethodSubtype} can be used as the key of mapping
+     *         rules between {@link InputMethodSubtype} and hardware keyboard layout.
+     * @hide
+     */
+    public boolean isSuitableForPhysicalKeyboardLayoutMapping() {
+        if (hashCode() == SUBTYPE_ID_NONE) {
+            return false;
+        }
+        if (!TextUtils.equals(getMode(), SUBTYPE_MODE_KEYBOARD)) {
+            return false;
+        }
+        if (isAuxiliary()) {
+            return false;
+        }
+        final String langTag = getCanonicalizedLanguageTag();
+        if (langTag.isEmpty() || TextUtils.equals(langTag, UNDEFINED_LANGUAGE_TAG)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * @return The mode of the subtype.
      */
     public String getMode() {
@@ -459,8 +559,11 @@
     public CharSequence getDisplayName(
             Context context, String packageName, ApplicationInfo appInfo) {
         if (mSubtypeNameResId == 0) {
-            return getLocaleDisplayName(getLocaleFromContext(context), getLocaleObject(),
-                    DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU);
+            return TextUtils.isEmpty(mSubtypeNameOverride)
+                    ? getLocaleDisplayName(
+                            getLocaleFromContext(context), getLocaleObject(),
+                            DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU)
+                    : mSubtypeNameOverride;
         }
 
         final CharSequence subtypeName = context.getPackageManager().getText(
@@ -625,6 +728,7 @@
     @Override
     public void writeToParcel(Parcel dest, int parcelableFlags) {
         dest.writeInt(mSubtypeNameResId);
+        TextUtils.writeToParcel(mSubtypeNameOverride, dest, parcelableFlags);
         dest.writeInt(mSubtypeIconResId);
         dest.writeString(mSubtypeLocale);
         dest.writeString(mSubtypeLanguageTag);
@@ -692,4 +796,4 @@
         }
         return sortedList;
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/android/view/inputmethod/InsertGesture.java b/core/java/android/view/inputmethod/InsertGesture.java
index 9f03289..0449a16 100644
--- a/core/java/android/view/inputmethod/InsertGesture.java
+++ b/core/java/android/view/inputmethod/InsertGesture.java
@@ -21,7 +21,6 @@
 import android.graphics.PointF;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.text.TextUtils;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
@@ -52,7 +51,8 @@
         mPoint = source.readTypedObject(PointF.CREATOR);
     }
 
-    /** Returns the text that will be inserted at {@link #getInsertionPoint()} **/
+    /** Returns the text that will be inserted at {@link #getInsertionPoint()}. When text is
+     * empty, cursor should be moved the insertion point. **/
     @NonNull
     public String getTextToInsert() {
         return mTextToInsert;
@@ -75,7 +75,11 @@
         private PointF mPoint;
         private String mFallbackText;
 
-        /** set the text that will be inserted at {@link #setInsertionPoint(PointF)} **/
+        /**
+         * Set the text that will be inserted at {@link #setInsertionPoint(PointF)}. When set with
+         * an empty string, cursor will be moved to {@link #getInsertionPoint()} and no text
+         * would be inserted.
+         */
         @NonNull
         @SuppressLint("MissingGetterMatchingBuilder")
         public Builder setTextToInsert(@NonNull String text) {
@@ -114,8 +118,8 @@
             if (mPoint == null) {
                 throw new IllegalArgumentException("Insertion point must be set.");
             }
-            if (TextUtils.isEmpty(mText)) {
-                throw new IllegalArgumentException("Text to insert must be non-empty.");
+            if (mText == null) {
+                throw new IllegalArgumentException("Text to insert must be set.");
             }
             return new InsertGesture(mText, mPoint, mFallbackText);
         }
diff --git a/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl
new file mode 100644
index 0000000..ffadf82
--- /dev/null
+++ b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+parcelable ParcelableHandwritingGesture;
diff --git a/core/java/android/view/inputmethod/ParcelableHandwritingGesture.java b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.java
new file mode 100644
index 0000000..e4066fc
--- /dev/null
+++ b/core/java/android/view/inputmethod/ParcelableHandwritingGesture.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 android.view.inputmethod;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A generic container of parcelable {@link HandwritingGesture}.
+ *
+ * @hide
+ */
+public final class ParcelableHandwritingGesture implements Parcelable {
+    @NonNull
+    private final HandwritingGesture mGesture;
+    @NonNull
+    private final Parcelable mGestureAsParcelable;
+
+    private ParcelableHandwritingGesture(@NonNull HandwritingGesture gesture) {
+        mGesture = gesture;
+        // For fail-fast.
+        mGestureAsParcelable = (Parcelable) gesture;
+    }
+
+    /**
+     * Creates {@link ParcelableHandwritingGesture} from {@link HandwritingGesture}, which also
+     * implements {@link Parcelable}.
+     *
+     * @param gesture {@link HandwritingGesture} object to be stored.
+     * @return {@link ParcelableHandwritingGesture} to be stored in {@link Parcel}.
+     */
+    @NonNull
+    public static ParcelableHandwritingGesture of(@NonNull HandwritingGesture gesture) {
+        return new ParcelableHandwritingGesture(Objects.requireNonNull(gesture));
+    }
+
+    /**
+     * @return {@link HandwritingGesture} object stored in this container.
+     */
+    @NonNull
+    public HandwritingGesture get() {
+        return mGesture;
+    }
+
+    private static HandwritingGesture createFromParcelInternal(
+            @HandwritingGesture.GestureType int gestureType, @NonNull Parcel parcel) {
+        switch (gestureType) {
+            case HandwritingGesture.GESTURE_TYPE_NONE:
+                throw new UnsupportedOperationException("GESTURE_TYPE_NONE is not supported");
+            case HandwritingGesture.GESTURE_TYPE_SELECT:
+                return SelectGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_SELECT_RANGE:
+                return SelectRangeGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_INSERT:
+                return InsertGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_DELETE:
+                return DeleteGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_DELETE_RANGE:
+                return DeleteRangeGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_JOIN_OR_SPLIT:
+                return JoinOrSplitGesture.CREATOR.createFromParcel(parcel);
+            case HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE:
+                return RemoveSpaceGesture.CREATOR.createFromParcel(parcel);
+            default:
+                throw new UnsupportedOperationException("Unknown type=" + gestureType);
+        }
+    }
+
+    public static final Creator<ParcelableHandwritingGesture> CREATOR = new Parcelable.Creator<>() {
+        @Override
+        public ParcelableHandwritingGesture createFromParcel(Parcel in) {
+            final int gestureType = in.readInt();
+            return new ParcelableHandwritingGesture(createFromParcelInternal(gestureType, in));
+        }
+
+        @Override
+        public ParcelableHandwritingGesture[] newArray(int size) {
+            return new ParcelableHandwritingGesture[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return mGestureAsParcelable.describeContents();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mGesture.getGestureType());
+        mGestureAsParcelable.writeToParcel(dest, flags);
+    }
+}
diff --git a/core/java/android/view/inputmethod/PreviewableHandwritingGesture.java b/core/java/android/view/inputmethod/PreviewableHandwritingGesture.java
new file mode 100644
index 0000000..7683ece
--- /dev/null
+++ b/core/java/android/view/inputmethod/PreviewableHandwritingGesture.java
@@ -0,0 +1,38 @@
+/*
+ * 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.view.inputmethod;
+
+import android.os.CancellationSignal;
+
+import java.util.Set;
+
+/**
+ * A {@link HandwritingGesture} that can be
+ * {@link InputConnection#previewHandwritingGesture(
+ *  PreviewableHandwritingGesture, CancellationSignal) previewed}.
+ *
+ *  Note: An editor might only implement a subset of gesture previews and declares the supported
+ *  ones via {@link EditorInfo#getSupportedHandwritingGesturePreviews}.
+ *
+ * @see EditorInfo#setSupportedHandwritingGesturePreviews(Set)
+ * @see EditorInfo#getSupportedHandwritingGesturePreviews()
+ */
+public abstract class PreviewableHandwritingGesture extends HandwritingGesture {
+    PreviewableHandwritingGesture() {
+        // intentionally empty.
+    }
+}
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index fa18eec..7525d72 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -28,8 +28,11 @@
 import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
+import android.os.ICancellationSignal;
 import android.os.Looper;
 import android.os.ResultReceiver;
 import android.os.Trace;
@@ -214,7 +217,7 @@
         }
     }
 
-    public boolean isActive() {
+    private boolean isActive() {
         return mParentInputMethodManager.isActive() && !isFinished();
     }
 
@@ -982,62 +985,9 @@
 
     @Dispatching(cancellable = true)
     @Override
-    public void performHandwritingSelectGesture(
-            InputConnectionCommandHeader header, SelectGesture gesture,
+    public void performHandwritingGesture(
+            InputConnectionCommandHeader header, ParcelableHandwritingGesture gestureContainer,
             ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingSelectRangeGesture(
-            InputConnectionCommandHeader header, SelectRangeGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingInsertGesture(
-            InputConnectionCommandHeader header, InsertGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingDeleteGesture(
-            InputConnectionCommandHeader header, DeleteGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingDeleteRangeGesture(
-            InputConnectionCommandHeader header, DeleteRangeGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingRemoveSpaceGesture(
-            InputConnectionCommandHeader header, RemoveSpaceGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    @Dispatching(cancellable = true)
-    @Override
-    public void performHandwritingJoinOrSplitGesture(
-            InputConnectionCommandHeader header, JoinOrSplitGesture gesture,
-            ResultReceiver resultReceiver) {
-        performHandwritingGestureInternal(header, gesture, resultReceiver);
-    }
-
-    private <T extends HandwritingGesture> void performHandwritingGestureInternal(
-            InputConnectionCommandHeader header,  T gesture, ResultReceiver resultReceiver) {
         dispatchWithTracing("performHandwritingGesture", () -> {
             if (header.mSessionId != mCurrentSessionId.get()) {
                 if (resultReceiver != null) {
@@ -1059,7 +1009,7 @@
             // TODO(210039666): implement Cleaner to return HANDWRITING_GESTURE_RESULT_UNKNOWN if
             //  editor doesn't return any type.
             ic.performHandwritingGesture(
-                    gesture,
+                    gestureContainer.get(),
                     resultReceiver != null ? Runnable::run : null,
                     resultReceiver != null
                             ? (resultCode) -> resultReceiver.send(resultCode, null /* resultData */)
@@ -1067,24 +1017,31 @@
         });
     }
 
-    /**
-     * Dispatches {@link InputConnection#requestCursorUpdates(int)}.
-     *
-     * <p>This method is intended to be called only from {@link InputMethodManager}.</p>
-     * @param cursorUpdateMode the mode for {@link InputConnection#requestCursorUpdates(int, int)}
-     * @param cursorUpdateFilter the filter for
-     *      {@link InputConnection#requestCursorUpdates(int, int)}
-     * @param imeDisplayId displayId on which IME is displayed.
-     */
     @Dispatching(cancellable = true)
-    public void requestCursorUpdatesFromImm(int cursorUpdateMode, int cursorUpdateFilter,
-            int imeDisplayId) {
-        final int currentSessionId = mCurrentSessionId.get();
-        dispatchWithTracing("requestCursorUpdatesFromImm", () -> {
-            if (currentSessionId != mCurrentSessionId.get()) {
+    @Override
+    public void previewHandwritingGesture(
+            InputConnectionCommandHeader header, ParcelableHandwritingGesture gestureContainer,
+            ICancellationSignal transport) {
+
+        // TODO(b/254727073): Implement CancellationSignal receiver
+        final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
+        // Previews always use PreviewableHandwritingGesture but if incorrectly wrong class is
+        // passed, ClassCastException will be sent back to caller.
+        final PreviewableHandwritingGesture gesture =
+                (PreviewableHandwritingGesture) gestureContainer.get();
+
+        dispatchWithTracing("previewHandwritingGesture", () -> {
+            if (header.mSessionId != mCurrentSessionId.get()
+                    || (cancellationSignal != null && cancellationSignal.isCanceled())) {
                 return;  // cancelled
             }
-            requestCursorUpdatesInternal(cursorUpdateMode, cursorUpdateFilter, imeDisplayId);
+            InputConnection ic = getInputConnection();
+            if (ic == null || !isActive()) {
+                Log.w(TAG, "previewHandwritingGesture on inactive InputConnection");
+                return; // cancelled
+            }
+
+            ic.previewHandwritingGesture(gesture, cancellationSignal);
         });
     }
 
@@ -1123,7 +1080,8 @@
             Log.w(TAG, "requestCursorAnchorInfo on inactive InputConnection");
             return false;
         }
-        if (mParentInputMethodManager.getDisplayId() != imeDisplayId
+        if (mParentInputMethodManager.mRequestCursorUpdateDisplayIdCheck.get()
+                && mParentInputMethodManager.getDisplayId() != imeDisplayId
                 && !mParentInputMethodManager.hasVirtualDisplayToScreenMatrix()) {
             // requestCursorUpdates() is not currently supported across displays.
             return false;
@@ -1147,6 +1105,36 @@
 
     @Dispatching(cancellable = true)
     @Override
+    public void requestTextBoundsInfo(
+            InputConnectionCommandHeader header, RectF rectF,
+            @NonNull ResultReceiver resultReceiver) {
+        dispatchWithTracing("requestTextBoundsInfo", () -> {
+            if (header.mSessionId != mCurrentSessionId.get()) {
+                resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null);
+                return;  // cancelled
+            }
+            InputConnection ic = getInputConnection();
+            if (ic == null || !isActive()) {
+                Log.w(TAG, "requestTextBoundsInfo on inactive InputConnection");
+                resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null);
+                return;
+            }
+
+            ic.requestTextBoundsInfo(
+                    rectF,
+                    Runnable::run,
+                    (textBoundsInfoResult) -> {
+                        final int resultCode = textBoundsInfoResult.getResultCode();
+                        final TextBoundsInfo textBoundsInfo =
+                                textBoundsInfoResult.getTextBoundsInfo();
+                        resultReceiver.send(resultCode,
+                                textBoundsInfo == null ? null : textBoundsInfo.toBundle());
+                    });
+        });
+    }
+
+    @Dispatching(cancellable = true)
+    @Override
     public void commitContent(InputConnectionCommandHeader header,
             InputContentInfo inputContentInfo, int flags, Bundle opts,
             AndroidFuture future /* T=Boolean */) {
diff --git a/core/java/android/view/inputmethod/SelectGesture.java b/core/java/android/view/inputmethod/SelectGesture.java
index 6dc4ed2..ba600df 100644
--- a/core/java/android/view/inputmethod/SelectGesture.java
+++ b/core/java/android/view/inputmethod/SelectGesture.java
@@ -33,7 +33,7 @@
  * <p>Note: This selects all text <em>within</em> the given area. To select a range <em>between</em>
  * two areas, use {@link SelectRangeGesture}.</p>
  */
-public final class SelectGesture extends HandwritingGesture implements Parcelable {
+public final class SelectGesture extends PreviewableHandwritingGesture implements Parcelable {
 
     private @Granularity int mGranularity;
     private RectF mArea;
@@ -72,7 +72,6 @@
         return mArea;
     }
 
-
     /**
      * Builder for {@link SelectGesture}. This class is not designed to be thread-safe.
      */
diff --git a/core/java/android/view/inputmethod/SelectRangeGesture.java b/core/java/android/view/inputmethod/SelectRangeGesture.java
index 7cb6002..c31bc27 100644
--- a/core/java/android/view/inputmethod/SelectRangeGesture.java
+++ b/core/java/android/view/inputmethod/SelectRangeGesture.java
@@ -34,7 +34,7 @@
  * <p>Note: this selects text within a range <em>between</em> two given areas. To select all text
  * <em>within</em> a single area, use {@link SelectGesture}</p>
  */
-public final class SelectRangeGesture extends HandwritingGesture implements Parcelable {
+public final class SelectRangeGesture extends PreviewableHandwritingGesture implements Parcelable {
 
     private @Granularity int mGranularity;
     private RectF mStartArea;
@@ -87,7 +87,6 @@
         return mEndArea;
     }
 
-
     /**
      * Builder for {@link SelectRangeGesture}. This class is not designed to be thread-safe.
      */
diff --git a/core/java/android/view/inputmethod/TextAppearanceInfo.java b/core/java/android/view/inputmethod/TextAppearanceInfo.java
new file mode 100644
index 0000000..500c41c
--- /dev/null
+++ b/core/java/android/view/inputmethod/TextAppearanceInfo.java
@@ -0,0 +1,798 @@
+/*
+ * 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.view.inputmethod;
+
+import static android.graphics.Typeface.NORMAL;
+
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.fonts.FontStyle;
+import android.graphics.text.LineBreakConfig;
+import android.inputmethodservice.InputMethodService;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+/**
+ * Information about text appearance in an editor, passed through
+ * {@link CursorAnchorInfo} for use by {@link InputMethodService}.
+ * @see TextView
+ * @see Paint
+ * @see CursorAnchorInfo.Builder#setTextAppearanceInfo(TextAppearanceInfo)
+ * @see CursorAnchorInfo#getTextAppearanceInfo()
+ */
+public final class TextAppearanceInfo implements Parcelable {
+    /**
+     * The text size (in pixels) for current editor.
+     */
+    private final @Px float mTextSize;
+
+    /**
+     * The {@link LocaleList} of the text.
+     */
+    @NonNull private final LocaleList mTextLocales;
+
+    /**
+     * The font family name if the {@link Typeface} of the text is created from a system font
+     * family, otherwise this value should be null.
+     */
+    @Nullable private final String mSystemFontFamilyName;
+
+    /**
+     * The weight of the text.
+     */
+    @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+    private final int mTextFontWeight;
+
+    /**
+     * The style (normal, bold, italic, bold|italic) of the text, see {@link Typeface}.
+     */
+    private final @Typeface.Style int mTextStyle;
+
+    /**
+     * Whether the transformation method applied to the current editor is set to all caps.
+     */
+    private final boolean mAllCaps;
+
+    /**
+     * The horizontal offset (in pixels) of the text shadow.
+     */
+    private final @Px float mShadowDx;
+
+    /**
+     * The vertical offset (in pixels) of the text shadow.
+     */
+    private final @Px float mShadowDy;
+
+    /**
+     * The blur radius (in pixels) of the text shadow.
+     */
+    private final @Px float mShadowRadius;
+
+    /**
+     * The shadow color of the text shadow.
+     */
+    private final @ColorInt int mShadowColor;
+
+    /**
+     * The elegant text height, especially for less compacted complex script text.
+     */
+    private final boolean mElegantTextHeight;
+
+    /**
+     * Whether to expand linespacing based on fallback fonts.
+     */
+    private final boolean mFallbackLineSpacing;
+
+    /**
+     * The text letter-spacing (in ems), which determines the spacing between characters.
+     */
+    private final float mLetterSpacing;
+
+    /**
+     * The font feature settings.
+     */
+    @Nullable private final String mFontFeatureSettings;
+
+    /**
+     * The font variation settings.
+     */
+    @Nullable private final String mFontVariationSettings;
+
+    /**
+     * The line-break strategies for text wrapping.
+     */
+    private final @LineBreakConfig.LineBreakStyle int mLineBreakStyle;
+
+    /**
+     * The line-break word strategies for text wrapping.
+     */
+    private final @LineBreakConfig.LineBreakWordStyle int mLineBreakWordStyle;
+
+    /**
+     * The extent by which text should be stretched horizontally. Returns 1.0 if not specified.
+     */
+    private final float mTextScaleX;
+
+    /**
+     * The color of the text selection highlight.
+     */
+    private final @ColorInt int mHighlightTextColor;
+
+    /**
+     * The current text color of the editor.
+     */
+    private final @ColorInt int mTextColor;
+
+    /**
+     *  The current color of the hint text.
+     */
+    private final @ColorInt int mHintTextColor;
+
+    /**
+     * The text color used to paint the links in the editor.
+     */
+    private final @ColorInt int mLinkTextColor;
+
+    private TextAppearanceInfo(@NonNull final TextAppearanceInfo.Builder builder) {
+        mTextSize = builder.mTextSize;
+        mTextLocales = builder.mTextLocales;
+        mSystemFontFamilyName = builder.mSystemFontFamilyName;
+        mTextFontWeight = builder.mTextFontWeight;
+        mTextStyle = builder.mTextStyle;
+        mAllCaps = builder.mAllCaps;
+        mShadowDx = builder.mShadowDx;
+        mShadowDy = builder.mShadowDy;
+        mShadowRadius = builder.mShadowRadius;
+        mShadowColor = builder.mShadowColor;
+        mElegantTextHeight = builder.mElegantTextHeight;
+        mFallbackLineSpacing = builder.mFallbackLineSpacing;
+        mLetterSpacing = builder.mLetterSpacing;
+        mFontFeatureSettings = builder.mFontFeatureSettings;
+        mFontVariationSettings = builder.mFontVariationSettings;
+        mLineBreakStyle = builder.mLineBreakStyle;
+        mLineBreakWordStyle = builder.mLineBreakWordStyle;
+        mTextScaleX = builder.mTextScaleX;
+        mHighlightTextColor = builder.mHighlightTextColor;
+        mTextColor = builder.mTextColor;
+        mHintTextColor = builder.mHintTextColor;
+        mLinkTextColor = builder.mLinkTextColor;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeFloat(mTextSize);
+        mTextLocales.writeToParcel(dest, flags); // NonNull
+        dest.writeBoolean(mAllCaps);
+        dest.writeString8(mSystemFontFamilyName);
+        dest.writeInt(mTextFontWeight);
+        dest.writeInt(mTextStyle);
+        dest.writeFloat(mShadowDx);
+        dest.writeFloat(mShadowDy);
+        dest.writeFloat(mShadowRadius);
+        dest.writeInt(mShadowColor);
+        dest.writeBoolean(mElegantTextHeight);
+        dest.writeBoolean(mFallbackLineSpacing);
+        dest.writeFloat(mLetterSpacing);
+        dest.writeString8(mFontFeatureSettings);
+        dest.writeString8(mFontVariationSettings);
+        dest.writeInt(mLineBreakStyle);
+        dest.writeInt(mLineBreakWordStyle);
+        dest.writeFloat(mTextScaleX);
+        dest.writeInt(mHighlightTextColor);
+        dest.writeInt(mTextColor);
+        dest.writeInt(mHintTextColor);
+        dest.writeInt(mLinkTextColor);
+    }
+
+    TextAppearanceInfo(@NonNull Parcel in) {
+        mTextSize = in.readFloat();
+        mTextLocales = LocaleList.CREATOR.createFromParcel(in);
+        mAllCaps = in.readBoolean();
+        mSystemFontFamilyName = in.readString8();
+        mTextFontWeight = in.readInt();
+        mTextStyle = in.readInt();
+        mShadowDx = in.readFloat();
+        mShadowDy = in.readFloat();
+        mShadowRadius = in.readFloat();
+        mShadowColor = in.readInt();
+        mElegantTextHeight = in.readBoolean();
+        mFallbackLineSpacing = in.readBoolean();
+        mLetterSpacing = in.readFloat();
+        mFontFeatureSettings = in.readString8();
+        mFontVariationSettings = in.readString8();
+        mLineBreakStyle = in.readInt();
+        mLineBreakWordStyle = in.readInt();
+        mTextScaleX = in.readFloat();
+        mHighlightTextColor = in.readInt();
+        mTextColor = in.readInt();
+        mHintTextColor = in.readInt();
+        mLinkTextColor = in.readInt();
+    }
+
+    @NonNull
+    public static final Creator<TextAppearanceInfo> CREATOR = new Creator<TextAppearanceInfo>() {
+        @Override
+        public TextAppearanceInfo createFromParcel(@NonNull Parcel in) {
+            return new TextAppearanceInfo(in);
+        }
+
+        @Override
+        public TextAppearanceInfo[] newArray(int size) {
+            return new TextAppearanceInfo[size];
+        }
+    };
+
+    /**
+     * Returns the text size (in pixels) for current editor.
+     */
+    public @Px float getTextSize() {
+        return mTextSize;
+    }
+
+    /**
+     * Returns the {@link LocaleList} of the text.
+     */
+    @NonNull
+    public LocaleList getTextLocales() {
+        return mTextLocales;
+    }
+
+    /**
+     * Returns the font family name if the {@link Typeface} of the text is created from a
+     * system font family. Returns null if no {@link Typeface} is specified, or it is not created
+     * from a system font family.
+     *
+     * @see Typeface#getSystemFontFamilyName()
+     */
+    @Nullable
+    public String getSystemFontFamilyName() {
+        return mSystemFontFamilyName;
+    }
+
+    /**
+     * Returns the weight of the text, or {@code FontStyle#FONT_WEIGHT_UNSPECIFIED}
+     * when no {@link Typeface} is specified.
+     */
+    @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+    public int getTextFontWeight() {
+        return mTextFontWeight;
+    }
+
+    /**
+     * Returns the style (normal, bold, italic, bold|italic) of the text. Returns
+     * {@link Typeface#NORMAL} when no {@link Typeface} is specified.
+     *
+     * @see Typeface
+     */
+    public @Typeface.Style int getTextStyle() {
+        return mTextStyle;
+    }
+
+    /**
+     * Returns whether the transformation method applied to the current editor is set to all caps.
+     *
+     * @see TextView#setAllCaps(boolean)
+     * @see TextView#setTransformationMethod(TransformationMethod)
+     */
+    public boolean isAllCaps() {
+        return mAllCaps;
+    }
+
+    /**
+     * Returns the horizontal offset (in pixels) of the text shadow.
+     *
+     * @see Paint#setShadowLayer(float, float, float, int)
+     */
+    public @Px float getShadowDx() {
+        return mShadowDx;
+    }
+
+    /**
+     * Returns the vertical offset (in pixels) of the text shadow.
+     *
+     * @see Paint#setShadowLayer(float, float, float, int)
+     */
+    public @Px float getShadowDy() {
+        return mShadowDy;
+    }
+
+    /**
+     * Returns the blur radius (in pixels) of the text shadow.
+     *
+     * @see Paint#setShadowLayer(float, float, float, int)
+     */
+    public @Px float getShadowRadius() {
+        return mShadowRadius;
+    }
+
+    /**
+     * Returns the color of the text shadow.
+     *
+     * @see Paint#setShadowLayer(float, float, float, int)
+     */
+    public @ColorInt int getShadowColor() {
+        return mShadowColor;
+    }
+
+    /**
+     * Returns {@code true} if the elegant height metrics flag is set. This setting selects font
+     * variants that have not been compacted to fit Latin-based vertical metrics, and also increases
+     * top and bottom bounds to provide more space.
+     *
+     * @see Paint#isElegantTextHeight()
+     */
+    public boolean isElegantTextHeight() {
+        return mElegantTextHeight;
+    }
+
+    /**
+     * Returns whether to expand linespacing based on fallback fonts.
+     *
+     * @see TextView#setFallbackLineSpacing(boolean)
+     */
+    public boolean isFallbackLineSpacing() {
+        return mFallbackLineSpacing;
+    }
+
+    /**
+     * Returns the text letter-spacing, which determines the spacing between characters.
+     * The value is in 'EM' units. Normally, this value is 0.0.
+     */
+    public float getLetterSpacing() {
+        return mLetterSpacing;
+    }
+
+    /**
+     * Returns the font feature settings. Returns null if not specified.
+     *
+     * @see Paint#getFontFeatureSettings()
+     */
+    @Nullable
+    public String getFontFeatureSettings() {
+        return mFontFeatureSettings;
+    }
+
+    /**
+     * Returns the font variation settings. Returns null if no variation is specified.
+     *
+     * @see Paint#getFontVariationSettings()
+     */
+    @Nullable
+    public String getFontVariationSettings() {
+        return mFontVariationSettings;
+    }
+
+    /**
+     * Returns the line-break strategies for text wrapping.
+     *
+     * @see TextView#setLineBreakStyle(int)
+     */
+    public @LineBreakConfig.LineBreakStyle int getLineBreakStyle() {
+        return mLineBreakStyle;
+    }
+
+    /**
+     * Returns the line-break word strategies for text wrapping.
+     *
+     * @see TextView#setLineBreakWordStyle(int)
+     */
+    public @LineBreakConfig.LineBreakWordStyle int getLineBreakWordStyle() {
+        return mLineBreakWordStyle;
+    }
+
+    /**
+     * Returns the extent by which text should be stretched horizontally. Returns 1.0 if not
+     * specified.
+     */
+    public float getTextScaleX() {
+        return mTextScaleX;
+    }
+
+    /**
+     * Returns the color of the text selection highlight.
+     *
+     * @see TextView#getHighlightColor()
+     */
+    public @ColorInt int getHighlightTextColor() {
+        return mHighlightTextColor;
+    }
+
+    /**
+     * Returns the current text color of the editor.
+     *
+     * @see TextView#getCurrentTextColor()
+     */
+    public @ColorInt int getTextColor() {
+        return mTextColor;
+    }
+
+    /**
+     * Returns the current color of the hint text.
+     *
+     * @see TextView#getCurrentHintTextColor()
+     */
+    public @ColorInt int getHintTextColor() {
+        return mHintTextColor;
+    }
+
+    /**
+     * Returns the text color used to paint the links in the editor.
+     *
+     * @see TextView#getLinkTextColors()
+     */
+    public @ColorInt int getLinkTextColor() {
+        return mLinkTextColor;
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof TextAppearanceInfo)) return false;
+        TextAppearanceInfo that = (TextAppearanceInfo) o;
+        return Float.compare(that.mTextSize, mTextSize) == 0
+                && mTextFontWeight == that.mTextFontWeight && mTextStyle == that.mTextStyle
+                && mAllCaps == that.mAllCaps && Float.compare(that.mShadowDx, mShadowDx) == 0
+                && Float.compare(that.mShadowDy, mShadowDy) == 0 && Float.compare(
+                that.mShadowRadius, mShadowRadius) == 0 && that.mShadowColor == mShadowColor
+                && mElegantTextHeight == that.mElegantTextHeight
+                && mFallbackLineSpacing == that.mFallbackLineSpacing && Float.compare(
+                that.mLetterSpacing, mLetterSpacing) == 0 && mLineBreakStyle == that.mLineBreakStyle
+                && mLineBreakWordStyle == that.mLineBreakWordStyle
+                && mHighlightTextColor == that.mHighlightTextColor
+                && mTextColor == that.mTextColor
+                && mLinkTextColor == that.mLinkTextColor
+                && mHintTextColor == that.mHintTextColor
+                && Objects.equals(mTextLocales, that.mTextLocales)
+                && Objects.equals(mSystemFontFamilyName, that.mSystemFontFamilyName)
+                && Objects.equals(mFontFeatureSettings, that.mFontFeatureSettings)
+                && Objects.equals(mFontVariationSettings, that.mFontVariationSettings)
+                && Float.compare(that.mTextScaleX, mTextScaleX) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTextSize, mTextLocales, mSystemFontFamilyName, mTextFontWeight,
+                mTextStyle, mAllCaps, mShadowDx, mShadowDy, mShadowRadius, mShadowColor,
+                mElegantTextHeight, mFallbackLineSpacing, mLetterSpacing, mFontFeatureSettings,
+                mFontVariationSettings, mLineBreakStyle, mLineBreakWordStyle, mTextScaleX,
+                mHighlightTextColor, mTextColor, mHintTextColor, mLinkTextColor);
+    }
+
+    @Override
+    public String toString() {
+        return "TextAppearanceInfo{"
+                + "mTextSize=" + mTextSize
+                + ", mTextLocales=" + mTextLocales
+                + ", mSystemFontFamilyName='" + mSystemFontFamilyName + '\''
+                + ", mTextFontWeight=" + mTextFontWeight
+                + ", mTextStyle=" + mTextStyle
+                + ", mAllCaps=" + mAllCaps
+                + ", mShadowDx=" + mShadowDx
+                + ", mShadowDy=" + mShadowDy
+                + ", mShadowRadius=" + mShadowRadius
+                + ", mShadowColor=" + mShadowColor
+                + ", mElegantTextHeight=" + mElegantTextHeight
+                + ", mFallbackLineSpacing=" + mFallbackLineSpacing
+                + ", mLetterSpacing=" + mLetterSpacing
+                + ", mFontFeatureSettings='" + mFontFeatureSettings + '\''
+                + ", mFontVariationSettings='" + mFontVariationSettings + '\''
+                + ", mLineBreakStyle=" + mLineBreakStyle
+                + ", mLineBreakWordStyle=" + mLineBreakWordStyle
+                + ", mTextScaleX=" + mTextScaleX
+                + ", mHighlightTextColor=" + mHighlightTextColor
+                + ", mTextColor=" + mTextColor
+                + ", mHintTextColor=" + mHintTextColor
+                + ", mLinkTextColor=" + mLinkTextColor
+                + '}';
+    }
+
+    /**
+     * Builder for {@link TextAppearanceInfo}.
+     */
+    public static final class Builder {
+        private @Px float mTextSize = -1;
+        private @NonNull LocaleList mTextLocales = LocaleList.getAdjustedDefault();
+        @Nullable private String mSystemFontFamilyName = null;
+        @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+        private int mTextFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
+        private @Typeface.Style int mTextStyle = NORMAL;
+        private boolean mAllCaps = false;
+        private @Px float mShadowDx = 0;
+        private @Px float mShadowDy = 0;
+        private @Px float mShadowRadius = 0;
+        private @ColorInt int mShadowColor = 0;
+        private boolean mElegantTextHeight = false;
+        private boolean mFallbackLineSpacing = false;
+        private float mLetterSpacing = 0;
+        @Nullable private String mFontFeatureSettings = null;
+        @Nullable private String mFontVariationSettings = null;
+        @LineBreakConfig.LineBreakStyle
+        private int mLineBreakStyle = LineBreakConfig.LINE_BREAK_STYLE_NONE;
+        @LineBreakConfig.LineBreakWordStyle
+        private int mLineBreakWordStyle = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE;
+        private float mTextScaleX = 1;
+        private @ColorInt int mHighlightTextColor = 0;
+        private @ColorInt int mTextColor = 0;
+        private @ColorInt int mHintTextColor = 0;
+        private @ColorInt int mLinkTextColor = 0;
+
+        /**
+         * Set the text size (in pixels) obtained from the current editor.
+         */
+        @NonNull
+        public Builder setTextSize(@Px float textSize) {
+            mTextSize = textSize;
+            return this;
+        }
+
+        /**
+         * Set the {@link LocaleList} of the text.
+         */
+        @NonNull
+        public Builder setTextLocales(@NonNull LocaleList textLocales) {
+            mTextLocales = textLocales;
+            return this;
+        }
+
+        /**
+         * Set the system font family name if the {@link Typeface} of the text is created from a
+         * system font family.
+         *
+         * @see Typeface#getSystemFontFamilyName()
+         */
+        @NonNull
+        public Builder setSystemFontFamilyName(@Nullable String systemFontFamilyName) {
+            mSystemFontFamilyName = systemFontFamilyName;
+            return this;
+        }
+
+        /**
+         * Set the weight of the text.
+         */
+        @NonNull
+        public Builder setTextFontWeight(
+                @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED,
+                        to = FontStyle.FONT_WEIGHT_MAX) int textFontWeight) {
+            mTextFontWeight = textFontWeight;
+            return this;
+        }
+
+        /**
+         * Set the style (normal, bold, italic, bold|italic) of the text.
+         *
+         * @see Typeface
+         */
+        @NonNull
+        public Builder setTextStyle(@Typeface.Style int textStyle) {
+            mTextStyle = textStyle;
+            return this;
+        }
+
+        /**
+         * Set whether the transformation method applied to the current editor  is set to all caps.
+         *
+         * @see TextView#setAllCaps(boolean)
+         * @see TextView#setTransformationMethod(TransformationMethod)
+         */
+        @NonNull
+        public Builder setAllCaps(boolean allCaps) {
+            mAllCaps = allCaps;
+            return this;
+        }
+
+        /**
+         * Set the horizontal offset (in pixels) of the text shadow.
+         *
+         * @see Paint#setShadowLayer(float, float, float, int)
+         */
+        @NonNull
+        public Builder setShadowDx(@Px float shadowDx) {
+            mShadowDx = shadowDx;
+            return this;
+        }
+
+        /**
+         * Set the vertical offset (in pixels) of the text shadow.
+         *
+         * @see Paint#setShadowLayer(float, float, float, int)
+         */
+        @NonNull
+        public Builder setShadowDy(@Px float shadowDy) {
+            mShadowDy = shadowDy;
+            return this;
+        }
+
+        /**
+         * Set the blur radius (in pixels) of the text shadow.
+         *
+         * @see Paint#setShadowLayer(float, float, float, int)
+         */
+        @NonNull
+        public Builder setShadowRadius(@Px float shadowRadius) {
+            mShadowRadius = shadowRadius;
+            return this;
+        }
+
+        /**
+         * Set the color of the text shadow.
+         *
+         * @see Paint#setShadowLayer(float, float, float, int)
+         */
+        @NonNull
+        public Builder setShadowColor(@ColorInt int shadowColor) {
+            mShadowColor = shadowColor;
+            return this;
+        }
+
+        /**
+         * Set the elegant height metrics flag. This setting selects font variants that
+         * have not been compacted to fit Latin-based vertical metrics, and also increases
+         * top and bottom bounds to provide more space.
+         *
+         * @see Paint#isElegantTextHeight()
+         */
+        @NonNull
+        public Builder setElegantTextHeight(boolean elegantTextHeight) {
+            mElegantTextHeight = elegantTextHeight;
+            return this;
+        }
+
+        /**
+         * Set whether to expand linespacing based on fallback fonts.
+         *
+         * @see TextView#setFallbackLineSpacing(boolean)
+         */
+        @NonNull
+        public Builder setFallbackLineSpacing(boolean fallbackLineSpacing) {
+            mFallbackLineSpacing = fallbackLineSpacing;
+            return this;
+        }
+
+        /**
+         * Set the text letter-spacing, which determines the spacing between characters.
+         * The value is in 'EM' units. Normally, this value is 0.0.
+         */
+        @NonNull
+        public Builder setLetterSpacing(float letterSpacing) {
+            mLetterSpacing = letterSpacing;
+            return this;
+        }
+
+        /**
+         * Set the font feature settings.
+         *
+         * @see Paint#getFontFeatureSettings()
+         */
+        @NonNull
+        public Builder setFontFeatureSettings(@Nullable String fontFeatureSettings) {
+            mFontFeatureSettings = fontFeatureSettings;
+            return this;
+        }
+
+        /**
+         * Set the font variation settings. Returns null if no variation is specified.
+         *
+         * @see Paint#getFontVariationSettings()
+         */
+        @NonNull
+        public Builder setFontVariationSettings(@Nullable String fontVariationSettings) {
+            mFontVariationSettings = fontVariationSettings;
+            return this;
+        }
+
+        /**
+         * Set the line-break strategies for text wrapping.
+         *
+         * @see TextView#setLineBreakStyle(int)
+         */
+        @NonNull
+        public Builder setLineBreakStyle(@LineBreakConfig.LineBreakStyle int lineBreakStyle) {
+            mLineBreakStyle = lineBreakStyle;
+            return this;
+        }
+
+        /**
+         * Set the line-break word strategies for text wrapping.
+         *
+         * @see TextView#setLineBreakWordStyle(int)
+         */
+        @NonNull
+        public Builder setLineBreakWordStyle(
+                @LineBreakConfig.LineBreakWordStyle int lineBreakWordStyle) {
+            mLineBreakWordStyle = lineBreakWordStyle;
+            return this;
+        }
+
+        /**
+         * Set the extent by which text should be stretched horizontally.
+         */
+        @NonNull
+        public Builder setTextScaleX(float textScaleX) {
+            mTextScaleX = textScaleX;
+            return this;
+        }
+
+        /**
+         * Set the color of the text selection highlight.
+         *
+         * @see TextView#getHighlightColor()
+         */
+        @NonNull
+        public Builder setHighlightTextColor(@ColorInt int highlightTextColor) {
+            mHighlightTextColor = highlightTextColor;
+            return this;
+        }
+
+        /**
+         * Set the current text color of the editor.
+         *
+         * @see TextView#getCurrentTextColor()
+         */
+        @NonNull
+        public Builder setTextColor(@ColorInt int textColor) {
+            mTextColor = textColor;
+            return this;
+        }
+
+        /**
+         * Set the current color of the hint text.
+         *
+         * @see TextView#getCurrentHintTextColor()
+         */
+        @NonNull
+        public Builder setHintTextColor(@ColorInt int hintTextColor) {
+            mHintTextColor = hintTextColor;
+            return this;
+        }
+
+        /**
+         * Set the text color used to paint the links in the editor.
+         *
+         * @see TextView#getLinkTextColors()
+         */
+        @NonNull
+        public Builder setLinkTextColor(@ColorInt int linkTextColor) {
+            mLinkTextColor = linkTextColor;
+            return this;
+        }
+
+        /**
+         * Returns {@link TextAppearanceInfo} using parameters in this
+         * {@link TextAppearanceInfo.Builder}.
+         */
+        @NonNull
+        public TextAppearanceInfo build() {
+            return new TextAppearanceInfo(this);
+        }
+    }
+}
diff --git a/core/java/android/view/inputmethod/TextBoundsInfo.java b/core/java/android/view/inputmethod/TextBoundsInfo.java
new file mode 100644
index 0000000..4e87405
--- /dev/null
+++ b/core/java/android/view/inputmethod/TextBoundsInfo.java
@@ -0,0 +1,844 @@
+/*
+ * 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.view.inputmethod;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.SegmentFinder;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * The text bounds information of a slice of text in the editor.
+ *
+ * <p> This class provides IME the layout information of the text within the range from
+ * {@link #getStart()} to {@link #getEnd()}. It's intended to be used by IME as a supplementary API
+ * to support handwriting gestures.
+ * </p>
+ */
+public final class TextBoundsInfo implements Parcelable {
+    /**
+     * The flag indicating that the character is a whitespace.
+     *
+     * @see Builder#setCharacterFlags(int[])
+     * @see #getCharacterFlags(int)
+     */
+    public static final int FLAG_CHARACTER_WHITESPACE = 1;
+
+    /**
+     * The flag indicating that the character is a linefeed character.
+     *
+     * @see Builder#setCharacterFlags(int[])
+     * @see #getCharacterFlags(int)
+     */
+    public static final int FLAG_CHARACTER_LINEFEED = 1 << 1;
+
+    /**
+     * The flag indicating that the character is a punctuation.
+     *
+     * @see Builder#setCharacterFlags(int[])
+     * @see #getCharacterFlags(int)
+     */
+    public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2;
+
+    /**
+     * The flag indicating that the line this character belongs to has RTL line direction. It's
+     * required that all characters in the same line must have the same direction.
+     *
+     * @see Builder#setCharacterFlags(int[])
+     * @see #getCharacterFlags(int)
+     */
+    public static final int FLAG_LINE_IS_RTL = 1 << 3;
+
+
+    /** @hide */
+    @IntDef(prefix = "FLAG_", flag = true, value = {
+            FLAG_CHARACTER_WHITESPACE,
+            FLAG_CHARACTER_LINEFEED,
+            FLAG_CHARACTER_PUNCTUATION,
+            FLAG_LINE_IS_RTL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CharacterFlags {}
+
+    /** All the valid flags. */
+    private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE
+            | FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION  | FLAG_LINE_IS_RTL;
+
+    /**
+     * The amount of shift to get the character's BiDi level from the internal character flags.
+     */
+    private static final int BIDI_LEVEL_SHIFT = 19;
+
+    /**
+     * The mask used to get the character's BiDi level from the internal character flags.
+     */
+    private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT;
+
+    /**
+     * The flag indicating that the character at the index is the start of a line segment.
+     * This flag is only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_LINE_SEGMENT_START = 1 << 31;
+
+    /**
+     * The flag indicating that the character at the index is the end of a line segment.
+     * This flag is only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_LINE_SEGMENT_END = 1 << 30;
+
+    /**
+     * The flag indicating that the character at the index is the start of a word segment.
+     * This flag is only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_WORD_SEGMENT_START = 1 << 29;
+
+    /**
+     * The flag indicating that the character at the index is the end of a word segment.
+     * This flag is only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_WORD_SEGMENT_END = 1 << 28;
+
+    /**
+     * The flag indicating that the character at the index is the start of a grapheme segment.
+     * It's only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27;
+
+    /**
+     * The flag indicating that the character at the index is the end of a grapheme segment.
+     * It's only used internally to serialize the {@link SegmentFinder}.
+     *
+     * @see #writeToParcel(Parcel, int)
+     */
+    private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26;
+
+    private final int mStart;
+    private final int mEnd;
+    private final float[] mMatrixValues;
+    private final float[] mCharacterBounds;
+    /**
+     * The array that encodes character and BiDi levels. They are stored together to save memory
+     * space, and it's easier during serialization.
+     */
+    private final int[] mInternalCharacterFlags;
+    private final SegmentFinder mLineSegmentFinder;
+    private final SegmentFinder mWordSegmentFinder;
+    private final SegmentFinder mGraphemeSegmentFinder;
+
+    /**
+     * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
+     * matrix that is to be applied other positional data in this class.
+     *
+     * @return a new instance (copy) of the transformation matrix.
+     */
+    @NonNull
+    public Matrix getMatrix() {
+        final Matrix matrix = new Matrix();
+        matrix.setValues(mMatrixValues);
+        return matrix;
+    }
+
+    /**
+     * Returns the index of the first character whose bounds information is available in this
+     * {@link TextBoundsInfo}, inclusive.
+     *
+     * @see Builder#setStartAndEnd(int, int)
+     */
+    public int getStart() {
+        return mStart;
+    }
+
+    /**
+     * Returns the index of the last character whose bounds information is available in this
+     * {@link TextBoundsInfo}, exclusive.
+     *
+     * @see Builder#setStartAndEnd(int, int)
+     */
+    public int getEnd() {
+        return mEnd;
+    }
+
+    /**
+     * Return the bounds of the character at the given {@code index}, in the coordinates of the
+     * editor.
+     *
+     * @param index the index of the queried character.
+     * @return the bounding box of the queried character.
+     *
+     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
+     * the {@code start} to the {@code end}.
+     */
+    @NonNull
+    public RectF getCharacterBounds(int index) {
+        if (index < mStart || index >= mEnd) {
+            throw new IndexOutOfBoundsException("Index is out of the bounds of "
+                    + "[" + mStart + ", " + mEnd + ").");
+        }
+        final int offset = 4 * (index - mStart);
+        return new RectF(mCharacterBounds[offset], mCharacterBounds[offset + 1],
+                mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]);
+    }
+
+    /**
+     * Return the flags associated with the character at the given {@code index}.
+     * The flags contain the following information:
+     * <ul>
+     *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
+     *     whitespace. </li>
+     *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
+     *     linefeed. </li>
+     *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
+     *     punctuation. </li>
+     *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
+     *     has RTL line direction. All characters in the same line must have the same line
+     *     direction. Check {@link #getLineSegmentFinder()} for more information of
+     *     line boundaries. </li>
+     * </ul>
+     *
+     * @param index the index of the queried character.
+     * @return the flags associated with the queried character.
+     *
+     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
+     * the {@code start} to the {@code end}.
+     *
+     * @see #FLAG_CHARACTER_WHITESPACE
+     * @see #FLAG_CHARACTER_LINEFEED
+     * @see #FLAG_CHARACTER_PUNCTUATION
+     * @see #FLAG_LINE_IS_RTL
+     */
+    @CharacterFlags
+    public int getCharacterFlags(int index) {
+        if (index < mStart || index >= mEnd) {
+            throw new IndexOutOfBoundsException("Index is out of the bounds of "
+                    + "[" + mStart + ", " + mEnd + ").");
+        }
+        final int offset = index - mStart;
+        return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS;
+    }
+
+    /**
+     * The BiDi level of the character at the given {@code index}. <br/>
+     * BiDi level is defined by
+     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
+     * bidirectional algorithm </a>. One can determine whether a character's direction is
+     * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
+     * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
+     * character must be in the range of [0, 125].
+     *
+     * @param index the index of the queried character.
+     * @return the BiDi level of the character, which is an integer in the range of [0, 125].
+     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
+     * the {@code start} to the {@code end}.
+     *
+     * @see Builder#setCharacterBidiLevel(int[])
+     */
+    @IntRange(from = 0, to = 125)
+    public int getCharacterBidiLevel(int index) {
+        if (index < mStart || index >= mEnd) {
+            throw new IndexOutOfBoundsException("Index is out of the bounds of "
+                    + "[" + mStart + ", " + mEnd + ").");
+        }
+        final int offset = index - mStart;
+        return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT;
+    }
+
+    /**
+     * Returns the {@link SegmentFinder} that locates the word boundaries.
+     *
+     * @see Builder#setWordSegmentFinder(SegmentFinder)
+     */
+    @NonNull
+    public SegmentFinder getWordSegmentFinder() {
+        return mWordSegmentFinder;
+    }
+
+    /**
+     * Returns the {@link SegmentFinder} that locates the grapheme boundaries.
+     *
+     * @see Builder#setGraphemeSegmentFinder(SegmentFinder)
+     */
+    @NonNull
+    public SegmentFinder getGraphemeSegmentFinder() {
+        return mGraphemeSegmentFinder;
+    }
+
+    /**
+     * Returns the {@link SegmentFinder} that locates the line boundaries.
+     *
+     * @see Builder#setLineSegmentFinder(SegmentFinder)
+     */
+    @NonNull
+    public SegmentFinder getLineSegmentFinder() {
+        return mLineSegmentFinder;
+    }
+
+    /**
+     * Describe the kinds of special objects contained in this Parcelable
+     * instance's marshaled representation. For example, if the object will
+     * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
+     * the return value of this method must include the
+     * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
+     *
+     * @return a bitmask indicating the set of special object types marshaled
+     * by this Parcelable object instance.
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Flatten this object in to a Parcel.
+     *
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mStart);
+        dest.writeInt(mEnd);
+        dest.writeFloatArray(mMatrixValues);
+        dest.writeFloatArray(mCharacterBounds);
+
+        // The end can also be a break position. We need an extra space to encode the breaks.
+        final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1);
+        encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END,
+                mStart, mEnd, mGraphemeSegmentFinder);
+        encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart,
+                mEnd, mWordSegmentFinder);
+        encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart,
+                mEnd, mLineSegmentFinder);
+        dest.writeIntArray(encodedFlags);
+    }
+
+    private TextBoundsInfo(Parcel source) {
+        mStart = source.readInt();
+        mEnd  = source.readInt();
+        mMatrixValues = Objects.requireNonNull(source.createFloatArray());
+        mCharacterBounds = Objects.requireNonNull(source.createFloatArray());
+        final int[] encodedFlags = Objects.requireNonNull(source.createIntArray());
+
+        mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START,
+                FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd);
+        mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START,
+                FLAG_WORD_SEGMENT_END, mStart, mEnd);
+        mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START,
+                FLAG_LINE_SEGMENT_END, mStart, mEnd);
+
+        final int length = mEnd - mStart;
+        final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK;
+        mInternalCharacterFlags = new int[length];
+        for (int i = 0; i < length; ++i) {
+            // Remove the flags used to encoded segment boundaries.
+            mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask;
+        }
+    }
+
+    private TextBoundsInfo(Builder builder) {
+        mStart = builder.mStart;
+        mEnd = builder.mEnd;
+        mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9);
+        final int length = mEnd - mStart;
+        mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length);
+        // Store characterFlags and characterBidiLevels to save memory.
+        mInternalCharacterFlags = new int[length];
+        for (int index = 0; index < length; ++index) {
+            mInternalCharacterFlags[index] = builder.mCharacterFlags[index]
+                    | (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT);
+        }
+        mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder;
+        mWordSegmentFinder = builder.mWordSegmentFinder;
+        mLineSegmentFinder = builder.mLineSegmentFinder;
+    }
+
+    /**
+     * The CREATOR to make this class Parcelable.
+     */
+    @NonNull
+    public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() {
+        @Override
+        public TextBoundsInfo createFromParcel(Parcel source) {
+            return new TextBoundsInfo(source);
+        }
+
+        @Override
+        public TextBoundsInfo[] newArray(int size) {
+            return new TextBoundsInfo[size];
+        }
+    };
+
+    private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo";
+
+    /**
+     * Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by
+     * {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor
+     * to IME.
+     *
+     * @see TextBoundsInfoResult
+     * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
+     * @hide
+     */
+    @NonNull
+    public Bundle toBundle() {
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this);
+        return bundle;
+
+    }
+
+    /** @hide */
+    @Nullable
+    public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) {
+        if (bundle == null) return null;
+        return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class);
+    }
+
+    /**
+     * The builder class to create a {@link TextBoundsInfo} object.
+     */
+    public static final class Builder {
+        private final float[] mMatrixValues = new float[9];
+        private boolean mMatrixInitialized;
+        private int mStart;
+        private int mEnd;
+        private float[] mCharacterBounds;
+        private int[] mCharacterFlags;
+        private int[] mCharacterBidiLevels;
+        private SegmentFinder mLineSegmentFinder;
+        private SegmentFinder mWordSegmentFinder;
+        private SegmentFinder mGraphemeSegmentFinder;
+
+        /** Clear all the parameters set on this {@link Builder} to reuse it. */
+        @NonNull
+        public Builder clear() {
+            mMatrixInitialized = false;
+            mStart = -1;
+            mEnd = -1;
+            mCharacterBounds = null;
+            mCharacterFlags = null;
+            mLineSegmentFinder = null;
+            mWordSegmentFinder = null;
+            mGraphemeSegmentFinder = null;
+            return this;
+        }
+
+        /**
+         * Sets the matrix that transforms local coordinates into screen coordinates.
+         *
+         * @param matrix transformation matrix from local coordinates into screen coordinates.
+         * @throws NullPointerException if the given {@code matrix} is {@code null}.
+         */
+        @NonNull
+        public Builder setMatrix(@NonNull Matrix matrix) {
+            Objects.requireNonNull(matrix).getValues(mMatrixValues);
+            mMatrixInitialized = true;
+            return this;
+        }
+
+        /**
+         * Set the start and end index of the {@link TextBoundsInfo}. It's the range of the
+         * characters whose information is available in the {@link TextBoundsInfo}.
+         *
+         * @param start the start index of the {@link TextBoundsInfo}, inclusive.
+         * @param end the end index of the {@link TextBoundsInfo}, exclusive.
+         * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
+         * or {@code end} is smaller than the {@code start}.
+         */
+        @NonNull
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+            Preconditions.checkArgument(start >= 0);
+            Preconditions.checkArgumentInRange(start, 0, end, "start");
+            mStart = start;
+            mEnd = end;
+            return this;
+        }
+
+        /**
+         * Set the characters bounds, in the coordinates of the editor. <br/>
+         *
+         * The given array should be divided into groups of four where each element represents
+         * left, top, right and bottom of the character bounds respectively.
+         * The bounds of the i-th character in the editor should be stored at index
+         * 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/>
+         *
+         * Sometimes multiple characters in a single grapheme are rendered as one symbol on the
+         * screen. So those characters only have one shared bounds. In this case, we recommend the
+         * editor to assign all the width to the bounds of the first character in the grapheme,
+         * and make the rest characters' bounds zero-width. <br/>
+         *
+         * For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face
+         * emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the
+         * string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ].
+         *
+         * @param characterBounds the array of the flattened character bounds.
+         * @throws NullPointerException if the given {@code characterBounds} is {@code null}.
+         */
+        @NonNull
+        public Builder setCharacterBounds(@NonNull float[] characterBounds) {
+            mCharacterBounds = Objects.requireNonNull(characterBounds);
+            return this;
+        }
+
+        /**
+         * Set the flags of the characters. The flags of the i-th character in the editor is stored
+         * at index (i - start). The length of the given array must equal to (end - start).
+         * The flags contain the following information:
+         * <ul>
+         *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
+         *     whitespace. </li>
+         *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
+         *     linefeed. </li>
+         *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
+         *     punctuation. </li>
+         *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
+         *     is RTL. All all character in the same line must have the same line direction. Check
+         *     {@link #getLineSegmentFinder()} for more information of line boundaries. </li>
+         * </ul>
+         *
+         * @param characterFlags the array of the character's flags.
+         * @throws NullPointerException if the given {@code characterFlags} is {@code null}.
+         * @throws IllegalArgumentException if the given {@code characterFlags} contains invalid
+         * flags.
+         *
+         * @see #getCharacterFlags(int)
+         */
+        @NonNull
+        public Builder setCharacterFlags(@NonNull int[] characterFlags) {
+            Objects.requireNonNull(characterFlags);
+            for (int characterFlag : characterFlags) {
+                if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) {
+                    throw new IllegalArgumentException("characterFlags contains invalid flags.");
+                }
+            }
+            mCharacterFlags = characterFlags;
+            return this;
+        }
+
+        /**
+         * Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor
+         * is stored at index (i - start). The length of the given array must equal to
+         * (end - start). <br/>
+         *
+         * BiDi level is defined by
+         * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
+         * bidirectional algorithm </a>. One can determine whether a character's direction is
+         * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
+         * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
+         * character must be in the range of [0, 125].
+         * @param characterBidiLevels the array of the character's BiDi level.
+         *
+         * @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}.
+         * @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an
+         * element that's out of the range [0, 125].
+         *
+         * @see #getCharacterBidiLevel(int)
+         */
+        @NonNull
+        public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) {
+            Objects.requireNonNull(characterBidiLevels);
+            for (int index = 0; index < characterBidiLevels.length; ++index) {
+                Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125,
+                        "bidiLevels[" + index + "]");
+            }
+            mCharacterBidiLevels = characterBidiLevels;
+            return this;
+        }
+
+        /**
+         * Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is
+         * defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">
+         * the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character.
+         * And it's usually the minimal unit for selection, backspace, deletion etc. <br/>
+         *
+         * Please note that only the grapheme segments within the range from start to end will
+         * be available to the IME. The remaining information will be discarded during serialization
+         * for better performance.
+         *
+         * @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster
+         *                              boundaries.
+         * @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}.
+         *
+         * @see #getGraphemeSegmentFinder()
+         * @see SegmentFinder
+         * @see SegmentFinder.DefaultSegmentFinder
+         */
+        @NonNull
+        public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) {
+            mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder);
+            return this;
+        }
+
+        /**
+         * Set the {@link SegmentFinder} that locates the word boundaries. <br/>
+         *
+         * Please note that only the word segments within the range from start to end will
+         * be available to the IME. The remaining information will be discarded during serialization
+         * for better performance.
+         * @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries.
+         * @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}.
+         *
+         * @see #getWordSegmentFinder()
+         * @see SegmentFinder
+         * @see SegmentFinder.DefaultSegmentFinder
+         */
+        @NonNull
+        public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) {
+            mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder);
+            return this;
+        }
+
+        /**
+         * Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard
+         * breaks in the text, it should also locate the soft line breaks added by the editor.
+         * It is expected that the characters within the same line is rendered on the same baseline.
+         * (Except for some text formatted as subscript and superscript.) <br/>
+         *
+         * Please note that only the line segments within the range from start to end will
+         * be available to the IME. The remaining information will be discarded during serialization
+         * for better performance.
+         * @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries.
+         * @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}.
+         *
+         * @see #getLineSegmentFinder()
+         * @see SegmentFinder
+         * @see SegmentFinder.DefaultSegmentFinder
+         */
+        @NonNull
+        public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) {
+            mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder);
+            return this;
+        }
+
+        /**
+         * Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}.
+         *
+         * @throws IllegalStateException in the following conditions:
+         * <ul>
+         *     <li>if the {@code start} or {@code end} is not set.</li>
+         *     <li>if the {@code matrix} is not set.</li>
+         *     <li>if {@code characterBounds} is not set or its length doesn't equal to
+         *     4 * ({@code end} - {@code start}).</li>
+         *     <li>if the {@code characterFlags} is not set or its length doesn't equal to
+         *     ({@code end} - {@code start}).</li>
+         *     <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or
+         *     {@code lineSegmentFinder} is not set.</li>
+         *     <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL}
+         *     flag.</li>
+         * </ul>
+         */
+        @NonNull
+        public TextBoundsInfo build() {
+            if (mStart < 0 || mEnd < 0) {
+                throw new IllegalStateException("Start and end must be set.");
+            }
+
+            if (!mMatrixInitialized) {
+                throw new IllegalStateException("Matrix must be set.");
+            }
+
+            if (mCharacterBounds == null) {
+                throw new IllegalStateException("CharacterBounds must be set.");
+            }
+
+            if (mCharacterFlags == null) {
+                throw new IllegalStateException("CharacterFlags must be set.");
+            }
+
+            if (mCharacterBidiLevels == null) {
+                throw new IllegalStateException("CharacterBidiLevel must be set.");
+            }
+
+            if (mCharacterBounds.length != 4 * (mEnd - mStart)) {
+                throw new IllegalStateException("The length of characterBounds doesn't match the "
+                        + "length of the given start and end."
+                        + " Expected length: " + (4 * (mEnd - mStart))
+                        + " characterBounds length: " + mCharacterBounds.length);
+            }
+            if (mCharacterFlags.length != mEnd - mStart) {
+                throw new IllegalStateException("The length of characterFlags doesn't match the "
+                        + "length of the given start and end."
+                        + " Expected length: " + (mEnd - mStart)
+                        + " characterFlags length: " + mCharacterFlags.length);
+            }
+            if (mCharacterBidiLevels.length != mEnd - mStart) {
+                throw new IllegalStateException("The length of characterBidiLevels doesn't match"
+                        + " the length of the given start and end."
+                        + " Expected length: " + (mEnd - mStart)
+                        + " characterFlags length: " + mCharacterBidiLevels.length);
+            }
+            if (mGraphemeSegmentFinder == null) {
+                throw new IllegalStateException("GraphemeSegmentFinder must be set.");
+            }
+            if (mWordSegmentFinder == null) {
+                throw new IllegalStateException("WordSegmentFinder must be set.");
+            }
+            if (mLineSegmentFinder == null) {
+                throw new IllegalStateException("LineSegmentFinder must be set.");
+            }
+
+            if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) {
+                throw new IllegalStateException("characters in the same line must have the same "
+                        + "FLAG_LINE_IS_RTL flag value.");
+            }
+            return new TextBoundsInfo(this);
+        }
+    }
+
+    /**
+     * Encode the segment start and end positions in {@link SegmentFinder} to a flags array.
+     *
+     * For example:
+     * Text: "A BC DE"
+     * Input:
+     *     start: 2, end: 7                                     // substring "BC DE"
+     *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]     // a word break iterator
+     *     flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace
+     *     segmentStartFlag: 0x0100
+     *     segmentEndFlag: 0x0200
+     * Output:
+     *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
+     *  The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end.
+     *
+     * @param flags the flags array to receive the results.
+     * @param segmentStartFlag the flag used to encode the segment start.
+     * @param segmentEndFlag the flag used to encode the segment end.
+     * @param start the start index of the encoded range, inclusive.
+     * @param end the end index of the encoded range, inclusive.
+     * @param segmentFinder the SegmentFinder to be encoded.
+     *
+     * @see #decodeSegmentFinder(int[], int, int, int, int)
+     */
+    private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag,
+            int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) {
+        if (end - start + 1 != flags.length) {
+            throw new IllegalStateException("The given flags array must have the same length as"
+                    + " the given range. flags length: " + flags.length
+                    + " range: [" + start + ", " + end + "]");
+        }
+
+        int segmentEnd = segmentFinder.nextEndBoundary(start);
+        if (segmentEnd == SegmentFinder.DONE) return;
+        int segmentStart = segmentFinder.previousStartBoundary(segmentEnd);
+
+        while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) {
+            if (segmentStart >= start) {
+                flags[segmentStart - start] |= segmentStartFlag;
+                flags[segmentEnd - start] |= segmentEndFlag;
+            }
+            segmentStart = segmentFinder.nextStartBoundary(segmentStart);
+            segmentEnd = segmentFinder.nextEndBoundary(segmentEnd);
+        }
+    }
+
+    /**
+     * Decode a {@link SegmentFinder} from a flags array.
+     *
+     * For example:
+     * Text: "A BC DE"
+     * Input:
+     *     start: 2, end: 7                                     // substring "BC DE"
+     *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
+     *     segmentStartFlag: 0x0100
+     *     segmentEndFlag: 0x0200
+     * Output:
+     *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]
+     *
+     * @param flags the flags array to decode the SegmentFinder.
+     * @param segmentStartFlag the flag to decode a segment start.
+     * @param segmentEndFlag the flag to decode a segment end.
+     * @param start the start index of the interested range, inclusive.
+     * @param end the end index of the interested range, inclusive.
+     *
+     * @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder)
+     */
+    private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag,
+            int segmentEndFlag, int start, int end) {
+        if (end - start + 1 != flags.length) {
+            throw new IllegalStateException("The given flags array must have the same length as"
+                    + " the given range. flags length: " + flags.length
+                    + " range: [" + start + ", " + end + "]");
+        }
+        int[] breaks = ArrayUtils.newUnpaddedIntArray(10);
+        int count = 0;
+        for (int offset = 0; offset < flags.length; ++offset) {
+            if ((flags[offset] & segmentStartFlag) == segmentStartFlag) {
+                breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
+            }
+            if ((flags[offset] & segmentEndFlag) == segmentEndFlag) {
+                breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
+            }
+        }
+        return new SegmentFinder.DefaultSegmentFinder(Arrays.copyOf(breaks, count));
+    }
+
+    /**
+     * Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line.
+     * @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag.
+     */
+    private static boolean isLineDirectionFlagConsistent(int[] characterFlags,
+            SegmentFinder lineSegmentFinder, int start, int end) {
+        int segmentEnd = lineSegmentFinder.nextEndBoundary(start);
+        if (segmentEnd == SegmentFinder.DONE) return true;
+        int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd);
+
+        while (segmentStart != SegmentFinder.DONE && segmentStart < end) {
+            final int lineStart = Math.max(segmentStart, start);
+            final int lineEnd = Math.min(segmentEnd, end);
+            final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0;
+            for (int index = lineStart + 1; index < lineEnd; ++index) {
+                final int flags = characterFlags[index - start];
+                final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0;
+                if (characterLineIsRtl != lineIsRtl) {
+                    return false;
+                }
+            }
+
+            segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart);
+            segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd);
+        }
+        return true;
+    }
+}
diff --git a/core/java/android/view/inputmethod/TextBoundsInfoResult.java b/core/java/android/view/inputmethod/TextBoundsInfoResult.java
new file mode 100644
index 0000000..62df17a
--- /dev/null
+++ b/core/java/android/view/inputmethod/TextBoundsInfoResult.java
@@ -0,0 +1,137 @@
+/*
+ * 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.view.inputmethod;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.RectF;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * The object that holds the result of the
+ * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
+ *
+ * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
+ */
+public final class TextBoundsInfoResult {
+    private final int mResultCode;
+    private final TextBoundsInfo mTextBoundsInfo;
+
+    /**
+     * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
+     * editor doesn't implement the method.
+     */
+    public static final int CODE_UNSUPPORTED = 0;
+
+    /**
+     * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
+     * editor successfully returns a {@link TextBoundsInfo}.
+     */
+    public static final int CODE_SUCCESS = 1;
+
+    /**
+     * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
+     * request failed. This result code is returned when the editor can't provide a valid
+     * {@link TextBoundsInfo}. (e.g. The editor view is not laid out.)
+     */
+    public static final int CODE_FAILED = 2;
+
+    /**
+     * Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
+     * request is cancelled. This happens when the {@link InputConnection} is or becomes
+     * invalidated while requesting the
+     * {@link TextBoundsInfo}, for example because a new {@code InputConnection} was started, or
+     * due to {@link InputMethodManager#invalidateInput}.
+     */
+    public static final int CODE_CANCELLED = 3;
+
+    /** @hide */
+    @IntDef(prefix = { "CODE_" }, value = {
+            CODE_UNSUPPORTED,
+            CODE_SUCCESS,
+            CODE_FAILED,
+            CODE_CANCELLED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {}
+
+    /**
+     * Create a {@link TextBoundsInfoResult} object with no {@link TextBoundsInfo}.
+     * The given {@code resultCode} can't be {@link #CODE_SUCCESS}.
+     * @param resultCode the result code of the
+     * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
+     */
+    public TextBoundsInfoResult(@ResultCode int resultCode) {
+        this(resultCode, null);
+    }
+
+    /**
+     * Create a {@link TextBoundsInfoResult} object.
+     *
+     * @param resultCode the result code of the
+     * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
+     * @param textBoundsInfo the returned {@link TextBoundsInfo} of the
+     * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. It can't be
+     *                       null if the {@code resultCode} is {@link #CODE_SUCCESS}.
+     *
+     * @throws IllegalStateException if the resultCode is
+     * {@link #CODE_SUCCESS} but the given {@code textBoundsInfo}
+     * is null.
+     */
+    public TextBoundsInfoResult(@ResultCode int resultCode,
+            @NonNull TextBoundsInfo textBoundsInfo) {
+        if (resultCode == CODE_SUCCESS && textBoundsInfo == null) {
+            throw new IllegalStateException("TextBoundsInfo must be provided when the resultCode "
+                    + "is CODE_SUCCESS.");
+        }
+        mResultCode = resultCode;
+        mTextBoundsInfo = textBoundsInfo;
+    }
+
+    /**
+     * Return the result code of the
+     * {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
+     * Its value is one of the {@link #CODE_UNSUPPORTED}, {@link #CODE_SUCCESS},
+     * {@link #CODE_FAILED} and {@link #CODE_CANCELLED}.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /**
+     * Return the {@link TextBoundsInfo} provided by the editor. It is non-null if the
+     * {@code resultCode} is {@link #CODE_SUCCESS}.
+     * Otherwise, it can be null in the following conditions:
+     * <ul>
+     *    <li>the editor doesn't support
+     *      {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)}.</li>
+     *    <li>the editor doesn't have the text bounds information at the moment. (e.g. the editor
+     *    view is not laid out yet.) </li>
+     *    <li> the {@link InputConnection} is or become inactive during the request. </li>
+     * <ul/>
+     */
+    @Nullable
+    public TextBoundsInfo getTextBoundsInfo() {
+        return  mTextBoundsInfo;
+    }
+}
diff --git a/core/java/android/webkit/ConsoleMessage.java b/core/java/android/webkit/ConsoleMessage.java
index 5474557..89cb6b2 100644
--- a/core/java/android/webkit/ConsoleMessage.java
+++ b/core/java/android/webkit/ConsoleMessage.java
@@ -68,4 +68,4 @@
     public int lineNumber() {
         return mLineNumber;
     }
-};
+}
diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java
index 5c7d97f..3d5bb49 100644
--- a/core/java/android/webkit/ValueCallback.java
+++ b/core/java/android/webkit/ValueCallback.java
@@ -25,4 +25,4 @@
      * @param value The value.
      */
     public void onReceiveValue(T value);
-};
+}
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 8f590f8..5740f86 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -21,6 +21,7 @@
 
 import android.R;
 import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -37,6 +38,7 @@
 import android.content.UndoOwner;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -49,8 +51,10 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.RenderNode;
+import android.graphics.Typeface;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.fonts.FontStyle;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.LocaleList;
@@ -125,6 +129,7 @@
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.TextAppearanceInfo;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassificationManager;
 import android.widget.AdapterView.OnItemClickListener;
@@ -4630,14 +4635,17 @@
                     (filter & InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER) != 0;
             boolean includeVisibleLineBounds =
                     (filter & InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS) != 0;
+            boolean includeTextAppearance =
+                    (filter & InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE) != 0;
             boolean includeAll =
                     (!includeEditorBounds && !includeCharacterBounds && !includeInsertionMarker
-                    && !includeVisibleLineBounds);
+                    && !includeVisibleLineBounds && !includeTextAppearance);
 
             includeEditorBounds |= includeAll;
             includeCharacterBounds |= includeAll;
             includeInsertionMarker |= includeAll;
             includeVisibleLineBounds |= includeAll;
+            includeTextAppearance |= includeAll;
 
             final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
             builder.reset();
@@ -4757,6 +4765,43 @@
                 }
             }
 
+            if (includeTextAppearance) {
+                Typeface typeface = mTextView.getPaint().getTypeface();
+                String systemFontFamilyName = null;
+                int textFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
+                if (typeface != null) {
+                    systemFontFamilyName = typeface.getSystemFontFamilyName();
+                    textFontWeight = typeface.getWeight();
+                }
+                ColorStateList linkTextColors = mTextView.getLinkTextColors();
+                @ColorInt int linkTextColor = linkTextColors != null
+                        ? linkTextColors.getDefaultColor() : 0;
+
+                TextAppearanceInfo.Builder appearanceBuilder = new TextAppearanceInfo.Builder();
+                appearanceBuilder.setTextSize(mTextView.getTextSize())
+                        .setTextLocales(mTextView.getTextLocales())
+                        .setSystemFontFamilyName(systemFontFamilyName)
+                        .setTextFontWeight(textFontWeight)
+                        .setTextStyle(mTextView.getTypefaceStyle())
+                        .setAllCaps(mTextView.isAllCaps())
+                        .setShadowDx(mTextView.getShadowDx())
+                        .setShadowDy(mTextView.getShadowDy())
+                        .setShadowRadius(mTextView.getShadowRadius())
+                        .setShadowColor(mTextView.getShadowColor())
+                        .setElegantTextHeight(mTextView.isElegantTextHeight())
+                        .setFallbackLineSpacing(mTextView.isFallbackLineSpacing())
+                        .setLetterSpacing(mTextView.getLetterSpacing())
+                        .setFontFeatureSettings(mTextView.getFontFeatureSettings())
+                        .setFontVariationSettings(mTextView.getFontVariationSettings())
+                        .setLineBreakStyle(mTextView.getLineBreakStyle())
+                        .setLineBreakWordStyle(mTextView.getLineBreakWordStyle())
+                        .setTextScaleX(mTextView.getTextScaleX())
+                        .setHighlightTextColor(mTextView.getHighlightColor())
+                        .setTextColor(mTextView.getCurrentTextColor())
+                        .setHintTextColor(mTextView.getCurrentHintTextColor())
+                        .setLinkTextColor(linkTextColor);
+                builder.setTextAppearanceInfo(appearanceBuilder.build());
+            }
             imm.updateCursorAnchorInfo(mTextView, builder.build());
 
             // Drop the immediate flag if any.
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index 5e74381..510a92d 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -116,9 +116,7 @@
  * <p class="note">ListView attempts to reuse view objects in order to improve performance and
  * avoid a lag in response to user scrolls.  To take advantage of this feature, check if the
  * {@code convertView} provided to {@code getView(...)} is null before creating or inflating a new
- * view object.  See
- * <a href="{@docRoot}training/improving-layouts/smooth-scrolling.html">
- * Making ListView Scrolling Smooth</a> for more ways to ensure a smooth user experience.</p>
+ * view object.</p>
  *
  * <p>To specify an action when a user clicks or taps on a single list item, see
  * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#HandlingUserSelections">
diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java
index 84b6f65..a5e7086 100644
--- a/core/java/android/widget/ScrollView.java
+++ b/core/java/android/widget/ScrollView.java
@@ -547,6 +547,18 @@
                         handled = fullScroll(View.FOCUS_DOWN);
                     }
                     break;
+                case KeyEvent.KEYCODE_MOVE_HOME:
+                    handled = fullScroll(View.FOCUS_UP);
+                    break;
+                case KeyEvent.KEYCODE_MOVE_END:
+                    handled = fullScroll(View.FOCUS_DOWN);
+                    break;
+                case KeyEvent.KEYCODE_PAGE_UP:
+                    handled = pageScroll(View.FOCUS_UP);
+                    break;
+                case KeyEvent.KEYCODE_PAGE_DOWN:
+                    handled = pageScroll(View.FOCUS_DOWN);
+                    break;
                 case KeyEvent.KEYCODE_SPACE:
                     pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
                     break;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index ce5365a..bf1a2bd 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -69,6 +69,7 @@
 import android.graphics.BlendMode;
 import android.graphics.Canvas;
 import android.graphics.Insets;
+import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
 import android.graphics.Path;
@@ -151,7 +152,6 @@
 import android.util.FeatureFlagUtils;
 import android.util.IntArray;
 import android.util.Log;
-import android.util.Range;
 import android.util.SparseIntArray;
 import android.util.TypedValue;
 import android.view.AccessibilityIterators.TextSegmentIterator;
@@ -189,6 +189,7 @@
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -199,6 +200,8 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
+import android.view.inputmethod.TextBoundsInfo;
 import android.view.inspector.InspectableProperty;
 import android.view.inspector.InspectableProperty.EnumEntry;
 import android.view.inspector.InspectableProperty.FlagEntry;
@@ -979,8 +982,11 @@
 
     /**
      * The last input source on this TextView.
+     *
+     * Use the SOURCE_TOUCHSCREEN as the default value for backward compatibility. There could be a
+     * non UI event originated ActionMode initiation, e.g. API call, a11y events, etc.
      */
-    private int mLastInputSource = InputDevice.SOURCE_UNKNOWN;
+    private int mLastInputSource = InputDevice.SOURCE_TOUCHSCREEN;
 
     /**
      * The TextView does not auto-size text (default).
@@ -2283,11 +2289,13 @@
      * @param familyName family name string, e.g. "serif"
      * @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF.
      * @param style a typeface style
-     * @param weight a weight value for the Typeface or -1 if not specified.
+     * @param weight a weight value for the Typeface or {@code FontStyle.FONT_WEIGHT_UNSPECIFIED}
+     *               if not specified.
      */
     private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
             @XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
-            @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+            @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+                    int weight) {
         if (typeface == null && familyName != null) {
             // Lookup normal Typeface from system font map.
             final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
@@ -2314,7 +2322,8 @@
     }
 
     private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style,
-            @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
+            @IntRange(from = FontStyle.FONT_WEIGHT_UNSPECIFIED, to = FontStyle.FONT_WEIGHT_MAX)
+                    int weight) {
         if (weight >= 0) {
             weight = Math.min(FontStyle.FONT_WEIGHT_MAX, weight);
             final boolean italic = (style & Typeface.ITALIC) != 0;
@@ -4015,7 +4024,7 @@
         boolean mFontFamilyExplicit = false;
         int mTypefaceIndex = -1;
         int mTextStyle = 0;
-        int mFontWeight = -1;
+        int mFontWeight = FontStyle.FONT_WEIGHT_UNSPECIFIED;
         boolean mAllCaps = false;
         int mShadowColor = 0;
         float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
@@ -6940,18 +6949,18 @@
         if (isPassword) {
             setTransformationMethod(PasswordTransformationMethod.getInstance());
             setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
-                    Typeface.NORMAL, -1 /* weight, not specifeid */);
+                    Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
         } else if (isVisiblePassword) {
             if (mTransformation == PasswordTransformationMethod.getInstance()) {
                 forceUpdate = true;
             }
             setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE,
-                    Typeface.NORMAL, -1 /* weight, not specified */);
+                    Typeface.NORMAL, FontStyle.FONT_WEIGHT_UNSPECIFIED);
         } else if (wasPassword || wasVisiblePassword) {
             // not in password mode, clean up typeface and transformation
             setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */,
                     DEFAULT_TYPEFACE /* typeface index */, Typeface.NORMAL,
-                    -1 /* weight, not specified */);
+                    FontStyle.FONT_WEIGHT_UNSPECIFIED);
             if (mTransformation == PasswordTransformationMethod.getInstance()) {
                 forceUpdate = true;
             }
@@ -9096,7 +9105,9 @@
 
                 ArrayList<Class<? extends HandwritingGesture>> gestures = new ArrayList<>();
                 gestures.add(SelectGesture.class);
+                gestures.add(SelectRangeGesture.class);
                 gestures.add(DeleteGesture.class);
+                gestures.add(DeleteRangeGesture.class);
                 gestures.add(InsertGesture.class);
                 gestures.add(RemoveSpaceGesture.class);
                 gestures.add(JoinOrSplitGesture.class);
@@ -9313,90 +9324,149 @@
 
     /** @hide */
     public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getSelectionArea()),
                 gesture.getGranularity());
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        Selection.setSelection(getEditableText(), range.getLower(), range.getUpper());
+        Selection.setSelection(getEditableText(), range[0], range[1]);
+        mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    /** @hide */
+    public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) {
+        int[] startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
+        Selection.setSelection(getEditableText(), range[0], range[1]);
         mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
     public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionArea()),
                 gesture.getGranularity());
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        int start = range.getLower();
-        int end = range.getUpper();
 
-        // For word granularity, adjust the start and end offsets to remove extra whitespace around
-        // the deleted text.
         if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
-            // If the deleted text is at the start of the text, the behavior is the same as the case
-            // where the deleted text follows a new line character.
-            int codePointBeforeStart = start > 0
-                    ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
-            // If the deleted text is at the end of the text, the behavior is the same as the case
-            // where the deleted text precedes a new line character.
-            int codePointAtEnd = end < mText.length()
-                    ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
-            if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
-                    && (TextUtils.isWhitespace(codePointAtEnd)
-                            || TextUtils.isPunctuation(codePointAtEnd))) {
-                // Remove whitespace (except new lines) before the deleted text, in these cases:
-                // - There is whitespace following the deleted text
-                //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
-                // - There is punctuation following the deleted text
-                //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
-                // - There is a new line following the deleted text
-                //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
-                // - The deleted text is at the end of the text
-                //     e.g. "one [deleted]" -> "one |" -> "one|"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    start -= Character.charCount(codePointBeforeStart);
-                    if (start == 0) break;
-                    codePointBeforeStart = Character.codePointBefore(mText, start);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
-            } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
-                    && (TextUtils.isWhitespace(codePointBeforeStart)
-                            || TextUtils.isPunctuation(codePointBeforeStart))) {
-                // Remove whitespace (except new lines) after the deleted text, in these cases:
-                // - There is punctuation preceding the deleted text
-                //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
-                // - There is a new line preceding the deleted text
-                //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
-                // - The deleted text is at the start of the text
-                //     e.g. "[deleted] two" -> "| two" -> "|two"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    end += Character.charCount(codePointAtEnd);
-                    if (end == mText.length()) break;
-                    codePointAtEnd = Character.codePointAt(mText, end);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
-            }
+            range = adjustHandwritingDeleteGestureRange(range);
         }
 
-        getEditableText().delete(start, end);
-        Selection.setSelection(getEditableText(), start);
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
+    public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) {
+        int[] startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
+
+        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
+            range = adjustHandwritingDeleteGestureRange(range);
+        }
+
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    private int[] adjustHandwritingDeleteGestureRange(int[] range) {
+        // For handwriting delete gestures with word granularity, adjust the start and end offsets
+        // to remove extra whitespace around the deleted text.
+
+        int start = range[0];
+        int end = range[1];
+
+        // If the deleted text is at the start of the text, the behavior is the same as the case
+        // where the deleted text follows a new line character.
+        int codePointBeforeStart = start > 0
+                ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
+        // If the deleted text is at the end of the text, the behavior is the same as the case where
+        // the deleted text precedes a new line character.
+        int codePointAtEnd = end < mText.length()
+                ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
+
+        if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
+                && (TextUtils.isWhitespace(codePointAtEnd)
+                        || TextUtils.isPunctuation(codePointAtEnd))) {
+            // Remove whitespace (except new lines) before the deleted text, in these cases:
+            // - There is whitespace following the deleted text
+            //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
+            // - There is punctuation following the deleted text
+            //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
+            // - There is a new line following the deleted text
+            //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
+            // - The deleted text is at the end of the text
+            //     e.g. "one [deleted]" -> "one |" -> "one|"
+            // (The pipe | indicates the cursor position.)
+            do {
+                start -= Character.charCount(codePointBeforeStart);
+                if (start == 0) break;
+                codePointBeforeStart = Character.codePointBefore(mText, start);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
+            return new int[] {start, end};
+        }
+
+        if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
+                && (TextUtils.isWhitespace(codePointBeforeStart)
+                        || TextUtils.isPunctuation(codePointBeforeStart))) {
+            // Remove whitespace (except new lines) after the deleted text, in these cases:
+            // - There is punctuation preceding the deleted text
+            //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
+            // - There is a new line preceding the deleted text
+            //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
+            // - The deleted text is at the start of the text
+            //     e.g. "[deleted] two" -> "| two" -> "|two"
+            // (The pipe | indicates the cursor position.)
+            do {
+                end += Character.charCount(codePointAtEnd);
+                if (end == mText.length()) break;
+                codePointAtEnd = Character.codePointAt(mText, end);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
+            return new int[] {start, end};
+        }
+
+        // Return the original range.
+        return range;
+    }
+
+    /** @hide */
     public int performHandwritingInsertGesture(@NonNull InsertGesture gesture) {
         PointF point = convertFromScreenToContentCoordinates(gesture.getInsertionPoint());
-        int line = mLayout.getLineForVertical((int) point.y);
-        if (point.y < mLayout.getLineTop(line)
-                || point.y > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
-            return handleGestureFailure(gesture);
-        }
-        if (point.x < mLayout.getLineLeft(line) || point.x > mLayout.getLineRight(line)) {
+        int line = getLineForHandwritingGesture(point);
+        if (line == -1) {
             return handleGestureFailure(gesture);
         }
         int offset = mLayout.getOffsetForHorizontal(line, point.x);
@@ -9412,27 +9482,17 @@
         PointF startPoint = convertFromScreenToContentCoordinates(gesture.getStartPoint());
         PointF endPoint = convertFromScreenToContentCoordinates(gesture.getEndPoint());
 
-        // The operation should be applied to the first line of text touched by the line joining
-        // the points.
-        int yMin = (int) Math.min(startPoint.y, endPoint.y);
-        int yMax = (int) Math.max(startPoint.y, endPoint.y);
-        int line = mLayout.getLineForVertical(yMin);
-        if (yMax < mLayout.getLineTop(line)) {
-            // Both points are above the top of the first line.
-            return handleGestureFailure(gesture);
-        }
-        if (yMin > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
-            if (line == mLayout.getLineCount() - 1 || yMax < mLayout.getLineTop(line + 1)) {
-                // The points are below the last line, or they are between two lines.
+        // The operation should be applied to the first line of text containing one of the points.
+        int startPointLine = getLineForHandwritingGesture(startPoint);
+        int endPointLine = getLineForHandwritingGesture(endPoint);
+        int line;
+        if (startPointLine == -1) {
+            if (endPointLine == -1) {
                 return handleGestureFailure(gesture);
-            } else {
-                // Apply the operation to the next line.
-                line++;
             }
-        }
-        if (Math.max(startPoint.x, endPoint.x) < mLayout.getLineLeft(line)
-                || Math.min(startPoint.x, endPoint.x) > mLayout.getLineRight(line)) {
-            return handleGestureFailure(gesture);
+            line = endPointLine;
+        } else {
+            line = (endPointLine == -1) ? startPointLine : Math.min(startPointLine, endPointLine);
         }
 
         // The operation should be applied to all characters touched by the line joining the points.
@@ -9445,14 +9505,14 @@
                 lineVerticalCenter + 0.1f,
                 Math.max(startPoint.x, endPoint.x),
                 lineVerticalCenter - 0.1f);
-        Range<Integer> range = mLayout.getRangeForRect(
+        int[] range = mLayout.getRangeForRect(
                 area, new GraphemeClusterSegmentFinder(mText, mTextPaint),
                 Layout.INCLUSION_STRATEGY_ANY_OVERLAP);
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        int startOffset = range.getLower();
-        int endOffset = range.getUpper();
+        int startOffset = range[0];
+        int endOffset = range[1];
         // TODO(b/247557062): This doesn't handle bidirectional text correctly.
 
         Pattern whitespacePattern = getWhitespacePattern();
@@ -9479,12 +9539,8 @@
     public int performHandwritingJoinOrSplitGesture(@NonNull JoinOrSplitGesture gesture) {
         PointF point = convertFromScreenToContentCoordinates(gesture.getJoinOrSplitPoint());
 
-        int line = mLayout.getLineForVertical((int) point.y);
-        if (point.y < mLayout.getLineTop(line)
-                || point.y > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
-            return handleGestureFailure(gesture);
-        }
-        if (point.x < mLayout.getLineLeft(line) || point.x > mLayout.getLineRight(line)) {
+        int line = getLineForHandwritingGesture(point);
+        if (line == -1) {
             return handleGestureFailure(gesture);
         }
 
@@ -9529,8 +9585,39 @@
         return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED;
     }
 
+    /**
+     * Returns the closest line such that the point is either inside the line bounds or within
+     * {@link ViewConfiguration#getScaledHandwritingGestureLineMargin} of the line bounds. Returns
+     * -1 if the point is not within the margin of any line bounds.
+     */
+    private int getLineForHandwritingGesture(PointF point) {
+        int line = mLayout.getLineForVertical((int) point.y);
+        int lineMargin = ViewConfiguration.get(mContext).getScaledHandwritingGestureLineMargin();
+        if (line < mLayout.getLineCount() - 1
+                && point.y > mLayout.getLineBottom(line) - lineMargin
+                && point.y
+                        > (mLayout.getLineBottom(line, false) + mLayout.getLineBottom(line)) / 2f) {
+            // If a point is in the space between line i and line (i + 1), Layout#getLineForVertical
+            // returns i. If the point is within lineMargin of line (i + 1), and closer to line
+            // (i + 1) than line i, then the gesture operation should be applied to line (i + 1).
+            line++;
+        } else if (point.y < mLayout.getLineTop(line) - lineMargin
+                || point.y
+                        > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)
+                                + lineMargin) {
+            // The point is not within lineMargin of a line.
+            return -1;
+        }
+        if (point.x < mLayout.getLineLeft(line) - lineMargin
+                || point.x > mLayout.getLineRight(line) + lineMargin) {
+            // The point is not within lineMargin of a line.
+            return -1;
+        }
+        return line;
+    }
+
     @Nullable
-    private Range<Integer> getRangeForRect(@NonNull RectF area, int granularity) {
+    private int[] getRangeForRect(@NonNull RectF area, int granularity) {
         SegmentFinder segmentFinder;
         if (granularity == HandwritingGesture.GRANULARITY_WORD) {
             WordIterator wordIterator = getWordIterator();
@@ -12874,18 +12961,15 @@
         getLocalVisibleRect(rect);
         final RectF visibleRect = new RectF(rect);
 
-        final float[] characterBounds = new float[4 * (endIndex - startIndex)];
-        mLayout.fillCharacterBounds(startIndex, endIndex, characterBounds, 0);
+
+        final float[] characterBounds = getCharacterBounds(startIndex, endIndex,
+                viewportToContentHorizontalOffset, viewportToContentVerticalOffset);
         final int limit = endIndex - startIndex;
         for (int offset = 0; offset < limit; ++offset) {
-            final float left =
-                    characterBounds[offset * 4] + viewportToContentHorizontalOffset;
-            final float top =
-                    characterBounds[offset * 4 + 1] + viewportToContentVerticalOffset;
-            final float right =
-                    characterBounds[offset * 4 + 2] + viewportToContentHorizontalOffset;
-            final float bottom =
-                    characterBounds[offset * 4 + 3] + viewportToContentVerticalOffset;
+            final float left = characterBounds[offset * 4];
+            final float top = characterBounds[offset * 4 + 1];
+            final float right = characterBounds[offset * 4 + 2];
+            final float bottom = characterBounds[offset * 4 + 3];
 
             final boolean hasVisibleRegion = visibleRect.intersects(left, top, right, bottom);
             final boolean hasInVisibleRegion = !visibleRect.contains(left, top, right, bottom);
@@ -12906,6 +12990,149 @@
     }
 
     /**
+     * Return the bounds of the characters in the given range, in TextView's coordinates.
+     *
+     * @param start the start index of the interested text range, inclusive.
+     * @param end the end index of the interested text range, exclusive.
+     * @param layoutLeft the left of the given {@code layout} in the editor view's coordinates.
+     * @param layoutTop  the top of the given {@code layout} in the editor view's coordinates.
+     * @return the character bounds stored in a flattened array, in the editor view's coordinates.
+     */
+    private float[] getCharacterBounds(int start, int end, float layoutLeft, float layoutTop) {
+        final float[] characterBounds = new float[4 * (end - start)];
+        mLayout.fillCharacterBounds(start, end, characterBounds, 0);
+        for (int offset = 0; offset < end - start; ++offset) {
+            characterBounds[4 * offset] += layoutLeft;
+            characterBounds[4 * offset + 1] += layoutTop;
+            characterBounds[4 * offset + 2] += layoutLeft;
+            characterBounds[4 * offset + 3] += layoutTop;
+        }
+        return characterBounds;
+    }
+
+    /**
+     * Creates the {@link TextBoundsInfo} for the text lines that intersects with the {@code rectF}.
+     * @hide
+     */
+    public TextBoundsInfo getTextBoundsInfo(@NonNull RectF rectF) {
+        final Layout layout = getLayout();
+        if (layout == null) {
+            // No valid text layout, return null.
+            return null;
+        }
+        final CharSequence text = layout.getText();
+        if (text == null) {
+            // It's impossible that a layout has no text. Check here to avoid NPE.
+            return null;
+        }
+
+        final Matrix localToGlobalMatrix = new Matrix();
+        transformMatrixToGlobal(localToGlobalMatrix);
+        final Matrix globalToLocalMatrix = new Matrix();
+        if (!localToGlobalMatrix.invert(globalToLocalMatrix)) {
+            // Can't map global rectF to local coordinates, this is almost impossible in practice.
+            return null;
+        }
+
+        final float layoutLeft = viewportToContentHorizontalOffset();
+        final float layoutTop = viewportToContentVerticalOffset();
+
+        final RectF localRectF = new RectF(rectF);
+        globalToLocalMatrix.mapRect(localRectF);
+        localRectF.offset(-layoutLeft, -layoutTop);
+
+        // Text length is 0. There is no character bounds, return empty TextBoundsInfo.
+        // rectF doesn't intersect with the layout, return empty TextBoundsInfo.
+        if (!localRectF.intersects(0f, 0f, layout.getWidth(), layout.getHeight())
+                || text.length() == 0) {
+            final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder();
+            final SegmentFinder emptySegmentFinder =
+                    new SegmentFinder.DefaultSegmentFinder(new int[0]);
+            builder.setStartAndEnd(0, 0)
+                    .setMatrix(localToGlobalMatrix)
+                    .setCharacterBounds(new float[0])
+                    .setCharacterBidiLevel(new int[0])
+                    .setCharacterFlags(new int[0])
+                    .setGraphemeSegmentFinder(emptySegmentFinder)
+                    .setLineSegmentFinder(emptySegmentFinder)
+                    .setWordSegmentFinder(emptySegmentFinder);
+            return  builder.build();
+        }
+
+        final int startLine = layout.getLineForVertical((int) Math.floor(localRectF.top));
+        final int endLine = layout.getLineForVertical((int) Math.floor(localRectF.bottom));
+        final int start = layout.getLineStart(startLine);
+        final int end = layout.getLineEnd(endLine);
+
+        // Compute character bounds.
+        final float[] characterBounds = getCharacterBounds(start, end, layoutLeft, layoutTop);
+
+        // Compute character flags and BiDi levels.
+        final int[] characterFlags = new int[end - start];
+        final int[] characterBidiLevels = new int[end - start];
+        for (int line = startLine; line <= endLine; ++line) {
+            final int lineStart = layout.getLineStart(line);
+            final int lineEnd = layout.getLineEnd(line);
+            final Layout.Directions directions = layout.getLineDirections(line);
+            for (int i = 0; i < directions.getRunCount(); ++i) {
+                final int runStart = directions.getRunStart(i) + lineStart;
+                final int runEnd = Math.min(runStart + directions.getRunLength(i), lineEnd);
+                final int runLevel = directions.getRunLevel(i);
+                Arrays.fill(characterBidiLevels, runStart - start, runEnd - start, runLevel);
+            }
+
+            final boolean lineIsRtl =
+                    layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
+            for (int index = lineStart; index < lineEnd; ++index) {
+                int flags = 0;
+                if (TextUtils.isWhitespace(text.charAt(index))) {
+                    flags |= TextBoundsInfo.FLAG_CHARACTER_WHITESPACE;
+                }
+                if (TextUtils.isPunctuation(Character.codePointAt(text, index))) {
+                    flags |= TextBoundsInfo.FLAG_CHARACTER_PUNCTUATION;
+                }
+                if (TextUtils.isNewline(Character.codePointAt(text, index))) {
+                    flags |= TextBoundsInfo.FLAG_CHARACTER_LINEFEED;
+                }
+                if (lineIsRtl) {
+                    flags |= TextBoundsInfo.FLAG_LINE_IS_RTL;
+                }
+                characterFlags[index - start] = flags;
+            }
+        }
+
+        // Create grapheme SegmentFinder.
+        final SegmentFinder graphemeSegmentFinder =
+                new GraphemeClusterSegmentFinder(text, layout.getPaint());
+
+        // Create word SegmentFinder.
+        final WordIterator wordIterator = getWordIterator();
+        wordIterator.setCharSequence(text, 0, text.length());
+        final SegmentFinder wordSegmentFinder = new WordSegmentFinder(text, wordIterator);
+
+        // Create line SegmentFinder.
+        final int lineCount = endLine - startLine + 1;
+        final int[] lineRanges = new int[2 * lineCount];
+        for (int line = startLine; line <= endLine; ++line) {
+            final int offset = line - startLine;
+            lineRanges[2 * offset] = layout.getLineStart(line);
+            lineRanges[2 * offset + 1] = layout.getLineEnd(line);
+        }
+        final SegmentFinder lineSegmentFinder = new SegmentFinder.DefaultSegmentFinder(lineRanges);
+
+        final TextBoundsInfo.Builder builder = new TextBoundsInfo.Builder();
+        builder.setStartAndEnd(start, end)
+                .setMatrix(localToGlobalMatrix)
+                .setCharacterBounds(characterBounds)
+                .setCharacterBidiLevel(characterBidiLevels)
+                .setCharacterFlags(characterFlags)
+                .setGraphemeSegmentFinder(graphemeSegmentFinder)
+                .setLineSegmentFinder(lineSegmentFinder)
+                .setWordSegmentFinder(wordSegmentFinder);
+        return  builder.build();
+    }
+
+    /**
      * @hide
      */
     public boolean isPositionVisible(final float positionX, final float positionY) {
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 4a4f561..85b2881 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -18,8 +18,10 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -50,6 +52,8 @@
 
     @SwipeEdge
     private final int mSwipeEdge;
+    @Nullable
+    private final RemoteAnimationTarget mDepartingAnimationTarget;
 
     /**
      * Creates a new {@link BackEvent} instance.
@@ -58,12 +62,16 @@
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
+     * @param departingAnimationTarget The remote animation target of the departing
+     *                                 application window.
      */
-    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
+    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
+            @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
+        mDepartingAnimationTarget = departingAnimationTarget;
     }
 
     private BackEvent(@NonNull Parcel in) {
@@ -71,6 +79,7 @@
         mTouchY = in.readFloat();
         mProgress = in.readFloat();
         mSwipeEdge = in.readInt();
+        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
 
     public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
@@ -96,6 +105,7 @@
         dest.writeFloat(mTouchY);
         dest.writeFloat(mProgress);
         dest.writeInt(mSwipeEdge);
+        dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
 
     /**
@@ -126,6 +136,16 @@
         return mSwipeEdge;
     }
 
+    /**
+     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+     * or {@code null} if the top window should not be moved for the current type of back
+     * destination.
+     */
+    @Nullable
+    public RemoteAnimationTarget getDepartingAnimationTarget() {
+        return mDepartingAnimationTarget;
+    }
+
     @Override
     public String toString() {
         return "BackEvent{"
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index 9b91cf2..a25e035 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -89,8 +89,6 @@
     @Nullable
     private final IOnBackInvokedCallback mOnBackInvokedCallback;
     private final boolean mPrepareRemoteAnimation;
-    @Nullable
-    private WindowContainerToken mDepartingWindowContainerToken;
 
     /**
      * Create a new {@link BackNavigationInfo} instance.
@@ -100,20 +98,15 @@
      *                                back preview.
      * @param onBackInvokedCallback   The back callback registered by the current top level window.
      * @param departingWindowContainerToken The {@link WindowContainerToken} of departing window.
-     * @param isPrepareRemoteAnimation  Return whether the core is preparing a back gesture
-     *                                  animation, if true, the caller of startBackNavigation should
-     *                                  be expected to receive an animation start callback.
      */
     private BackNavigationInfo(@BackTargetType int type,
             @Nullable RemoteCallback onBackNavigationDone,
             @Nullable IOnBackInvokedCallback onBackInvokedCallback,
-            boolean isPrepareRemoteAnimation,
-            @Nullable WindowContainerToken departingWindowContainerToken) {
+            boolean isPrepareRemoteAnimation) {
         mType = type;
         mOnBackNavigationDone = onBackNavigationDone;
         mOnBackInvokedCallback = onBackInvokedCallback;
         mPrepareRemoteAnimation = isPrepareRemoteAnimation;
-        mDepartingWindowContainerToken = departingWindowContainerToken;
     }
 
     private BackNavigationInfo(@NonNull Parcel in) {
@@ -121,7 +114,6 @@
         mOnBackNavigationDone = in.readTypedObject(RemoteCallback.CREATOR);
         mOnBackInvokedCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder());
         mPrepareRemoteAnimation = in.readBoolean();
-        mDepartingWindowContainerToken = in.readTypedObject(WindowContainerToken.CREATOR);
     }
 
     @Override
@@ -130,7 +122,6 @@
         dest.writeTypedObject(mOnBackNavigationDone, flags);
         dest.writeStrongInterface(mOnBackInvokedCallback);
         dest.writeBoolean(mPrepareRemoteAnimation);
-        dest.writeTypedObject(mDepartingWindowContainerToken, flags);
     }
 
     /**
@@ -164,18 +155,6 @@
     }
 
     /**
-     * Returns the {@link WindowContainerToken} of the highest container in the hierarchy being
-     * removed.
-     * <p>
-     * For example, if an Activity is the last one of its Task, the Task's token will be given.
-     * Otherwise, it will be the Activity's token.
-     */
-    @Nullable
-    public WindowContainerToken getDepartingWindowContainerToken() {
-        return mDepartingWindowContainerToken;
-    }
-
-    /**
      * Callback to be called when the back preview is finished in order to notify the server that
      * it can clean up the resources created for the animation.
      *
@@ -212,7 +191,6 @@
                 + "mType=" + typeToString(mType) + " (" + mType + ")"
                 + ", mOnBackNavigationDone=" + mOnBackNavigationDone
                 + ", mOnBackInvokedCallback=" + mOnBackInvokedCallback
-                + ", mWindowContainerToken=" + mDepartingWindowContainerToken
                 + '}';
     }
 
@@ -248,8 +226,6 @@
         @Nullable
         private IOnBackInvokedCallback mOnBackInvokedCallback = null;
         private boolean mPrepareRemoteAnimation;
-        @Nullable
-        private WindowContainerToken mDepartingWindowContainerToken = null;
 
         /**
          * @see BackNavigationInfo#getType()
@@ -285,20 +261,12 @@
         }
 
         /**
-         * @see BackNavigationInfo#getDepartingWindowContainerToken()
-         */
-        public void setDepartingWCT(@NonNull WindowContainerToken windowContainerToken) {
-            mDepartingWindowContainerToken = windowContainerToken;
-        }
-
-        /**
          * Builds and returns an instance of {@link BackNavigationInfo}
          */
         public BackNavigationInfo build() {
             return new BackNavigationInfo(mType, mOnBackNavigationDone,
                     mOnBackInvokedCallback,
-                    mPrepareRemoteAnimation,
-                    mDepartingWindowContainerToken);
+                    mPrepareRemoteAnimation);
         }
     }
 }
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
new file mode 100644
index 0000000..2e3afde
--- /dev/null
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.window;
+
+import android.util.FloatProperty;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+
+/**
+ * An animator that drives the predictive back progress with a spring.
+ *
+ * The back gesture's latest touch point and committal state determines the final position of
+ * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with
+ * smoothly transitioning progress values.
+ *
+ * @hide
+ */
+public class BackProgressAnimator {
+    /**
+     *  A factor to scale the input progress by, so that it works better with the spring.
+     *  We divide the output progress by this value before sending it to apps, so that apps
+     *  always receive progress values in [0, 1].
+     */
+    private static final float SCALE_FACTOR = 100f;
+    private final SpringAnimation mSpring;
+    private ProgressCallback mCallback;
+    private float mProgress = 0;
+    private BackEvent mLastBackEvent;
+    private boolean mStarted = false;
+
+    private void setProgress(float progress) {
+        mProgress = progress;
+    }
+
+    private float getProgress() {
+        return mProgress;
+    }
+
+    private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP =
+            new FloatProperty<BackProgressAnimator>("progress") {
+                @Override
+                public void setValue(BackProgressAnimator animator, float value) {
+                    animator.setProgress(value);
+                    animator.updateProgressValue(value);
+                }
+
+                @Override
+                public Float get(BackProgressAnimator object) {
+                    return object.getProgress();
+                }
+            };
+
+
+    /** A callback to be invoked when there's a progress value update from the animator. */
+    public interface ProgressCallback {
+        /** Called when there's a progress value update. */
+        void onProgressUpdate(BackEvent event);
+    }
+
+    public BackProgressAnimator() {
+        mSpring = new SpringAnimation(this, PROGRESS_PROP);
+        mSpring.setSpring(new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+    }
+
+    /**
+     * Sets a new target position for the back progress.
+     *
+     * @param event the {@link BackEvent} containing the latest target progress.
+     */
+    public void onBackProgressed(BackEvent event) {
+        if (!mStarted) {
+            return;
+        }
+        mLastBackEvent = event;
+        if (mSpring == null) {
+            return;
+        }
+        mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR);
+    }
+
+    /**
+     * Starts the back progress animation.
+     *
+     * @param event the {@link BackEvent} that started the gesture.
+     * @param callback the back callback to invoke for the gesture. It will receive back progress
+     *                 dispatches as the progress animation updates.
+     */
+    public void onBackStarted(BackEvent event, ProgressCallback callback) {
+        reset();
+        mLastBackEvent = event;
+        mCallback = callback;
+        mStarted = true;
+    }
+
+    /**
+     * Resets the back progress animation. This should be called when back is invoked or cancelled.
+     */
+    public void reset() {
+        mSpring.animateToFinalPosition(0);
+        if (mSpring.canSkipToEnd()) {
+            mSpring.skipToEnd();
+        } else {
+            // Should never happen.
+            mSpring.cancel();
+        }
+        mStarted = false;
+        mLastBackEvent = null;
+        mCallback = null;
+        mProgress = 0;
+    }
+
+    private void updateProgressValue(float progress) {
+        if (mLastBackEvent == null || mCallback == null || !mStarted) {
+            return;
+        }
+        mCallback.onProgressUpdate(
+                new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
+                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
+                        mLastBackEvent.getDepartingAnimationTarget()));
+    }
+
+}
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index f274d1a..0ce076b6 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -49,7 +49,7 @@
 
     public boolean isParentFrameClippedByDisplayCutout;
 
-    public float sizeCompatScale = 1f;
+    public float compatScale = 1f;
 
     public ClientWindowFrames() {
     }
@@ -62,7 +62,7 @@
             attachedFrame = new Rect(other.attachedFrame);
         }
         isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout;
-        sizeCompatScale = other.sizeCompatScale;
+        compatScale = other.compatScale;
     }
 
     private ClientWindowFrames(Parcel in) {
@@ -76,7 +76,7 @@
         parentFrame.readFromParcel(in);
         attachedFrame = in.readTypedObject(Rect.CREATOR);
         isParentFrameClippedByDisplayCutout = in.readBoolean();
-        sizeCompatScale = in.readFloat();
+        compatScale = in.readFloat();
     }
 
     @Override
@@ -86,7 +86,7 @@
         parentFrame.writeToParcel(dest, flags);
         dest.writeTypedObject(attachedFrame, flags);
         dest.writeBoolean(isParentFrameClippedByDisplayCutout);
-        dest.writeFloat(sizeCompatScale);
+        dest.writeFloat(compatScale);
     }
 
     @Override
@@ -97,7 +97,7 @@
                 + " parentFrame=" + parentFrame.toShortString(sb)
                 + (attachedFrame != null ? " attachedFrame=" + attachedFrame.toShortString() : "")
                 + (isParentFrameClippedByDisplayCutout ? " parentClippedByDisplayCutout" : "")
-                + (sizeCompatScale != 1f ? " sizeCompatScale=" + sizeCompatScale : "") +  "}";
+                + (compatScale != 1f ? " sizeCompatScale=" + compatScale : "") +  "}";
     }
 
     @Override
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index a5aefd5..f55932e 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -16,6 +16,8 @@
 
 package android.window;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
 import android.annotation.NonNull;
 import android.app.WindowConfiguration;
 import android.content.ComponentName;
@@ -142,6 +144,14 @@
      */
     public void onRunningAppsChanged(ArraySet<Integer> runningUids) {}
 
+    /**
+     * This is called when an Activity is entering PIP.
+     * Returns {@code true} if the Activity is allowed to enter PIP.
+     */
+    public boolean isEnteringPipAllowed(int uid) {
+        return isWindowingModeSupported(WINDOWING_MODE_PINNED);
+    }
+
     /** Dump debug data */
     public void dump(String prefix, final PrintWriter pw) {
         pw.println(prefix + "DisplayWindowPolicyController{" + super.toString() + "}");
diff --git a/core/java/android/window/IBackAnimationFinishedCallback.aidl b/core/java/android/window/IBackAnimationFinishedCallback.aidl
index 8afc003..f034339 100644
--- a/core/java/android/window/IBackAnimationFinishedCallback.aidl
+++ b/core/java/android/window/IBackAnimationFinishedCallback.aidl
@@ -22,6 +22,6 @@
  * @param trigger Whether the back gesture has passed the triggering threshold.
  * {@hide}
  */
-oneway interface IBackAnimationFinishedCallback {
+interface IBackAnimationFinishedCallback {
     void onAnimationFinished(in boolean triggerBack);
 }
\ No newline at end of file
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 47796de..6af8ddd 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -28,17 +28,18 @@
 oneway interface IOnBackInvokedCallback {
    /**
     * Called when a back gesture has been started, or back button has been pressed down.
-    * Wraps {@link OnBackInvokedCallback#onBackStarted()}.
+    * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
+    *
+    * @param backEvent The {@link BackEvent} containing information about the touch or button press.
     */
-    void onBackStarted();
+    void onBackStarted(in BackEvent backEvent);
 
     /**
      * Called on back gesture progress.
-     * Wraps {@link OnBackInvokedCallback#onBackProgressed()}.
+     * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
      *
-     * @param touchX Absolute X location of the touch point.
-     * @param touchY Absolute Y location of the touch point.
-     * @param progress Value between 0 and 1 on how far along the back gesture is.
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
      */
     void onBackProgressed(in BackEvent backEvent);
 
diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl
index 36eaf49..57e0ce8 100644
--- a/core/java/android/window/IWindowOrganizerController.aidl
+++ b/core/java/android/window/IWindowOrganizerController.aidl
@@ -105,4 +105,7 @@
 
     /** @return An interface enabling the transition players to report its metrics. */
     ITransitionMetricsReporter getTransitionMetricsReporter();
+
+    /** @return The transaction queue token used by WM. */
+    IBinder getApplyToken();
 }
diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java
index 1a37e57..c05809b 100644
--- a/core/java/android/window/OnBackAnimationCallback.java
+++ b/core/java/android/window/OnBackAnimationCallback.java
@@ -13,14 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.window;
-
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.View;
-
 /**
  * Interface for applications to register back animation callbacks along their custom back
  * handling.
@@ -40,11 +37,10 @@
  * @hide
  */
 public interface OnBackAnimationCallback extends OnBackInvokedCallback {
-   /**
-    * Called when a back gesture has been started, or back button has been pressed down.
-    */
+    /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     */
     default void onBackStarted() { }
-
     /**
      * Called on back gesture progress.
      *
@@ -53,7 +49,6 @@
      * @see BackEvent
      */
     default void onBackProgressed(@NonNull BackEvent backEvent) { }
-
     /**
      * Called when a back gesture or back button press has been cancelled.
      */
diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java
index 6e2d4f9..62c41bf 100644
--- a/core/java/android/window/OnBackInvokedCallback.java
+++ b/core/java/android/window/OnBackInvokedCallback.java
@@ -16,6 +16,7 @@
 
 package android.window;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.Window;
@@ -41,8 +42,35 @@
 @SuppressWarnings("deprecation")
 public interface OnBackInvokedCallback {
     /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the touch or
+     *                  button press.
+     *
+     * @hide
+     */
+    default void onBackStarted(@NonNull BackEvent backEvent) {}
+
+    /**
+     * Called when a back gesture has been progressed.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
+     *
+     * @hide
+     */
+    default void onBackProgressed(@NonNull BackEvent backEvent) {}
+
+    /**
      * Called when a back gesture has been completed and committed, or back button pressed
      * has been released and committed.
      */
     void onBackInvoked();
+
+    /**
+     * Called when a back gesture or button press has been cancelled.
+     *
+     * @hide
+     */
+    default void onBackCancelled() {}
 }
diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java
index d161037..1b64e61 100644
--- a/core/java/android/window/StartingWindowInfo.java
+++ b/core/java/android/window/StartingWindowInfo.java
@@ -25,7 +25,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
 
 /**
@@ -181,11 +182,7 @@
      */
     public TaskSnapshot taskSnapshot;
 
-    /**
-     * The requested insets visibility of the top main window.
-     * @hide
-     */
-    public final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+    public @InsetsType int requestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
     public StartingWindowInfo() {
 
@@ -218,7 +215,7 @@
         dest.writeInt(splashScreenThemeResId);
         dest.writeBoolean(isKeyguardOccluded);
         dest.writeTypedObject(taskSnapshot, flags);
-        requestedVisibilities.writeToParcel(dest, flags);
+        dest.writeInt(requestedVisibleTypes);
     }
 
     void readFromParcel(@NonNull Parcel source) {
@@ -232,7 +229,7 @@
         splashScreenThemeResId = source.readInt();
         isKeyguardOccluded = source.readBoolean();
         taskSnapshot = source.readTypedObject(TaskSnapshot.CREATOR);
-        requestedVisibilities.readFromParcel(source);
+        requestedVisibleTypes = source.readInt();
     }
 
     @Override
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index 4248096..3950739 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -57,12 +57,13 @@
  * option is provided.
  *
  * The following is what happens within the {@link SurfaceSyncGroup}
- * 1. Each SyncTarget will get a {@link SyncTarget#onReadyToSync} callback that contains a
- * {@link SyncBufferCallback}.
- * 2. Each {@link SyncTarget} needs to invoke {@link SyncBufferCallback#onBufferReady(Transaction)}.
- * This makes sure the SurfaceSyncGroup knows when the SyncTarget is complete, allowing the
- * SurfaceSyncGroup to get the Transaction that contains the buffer.
- * 3. When the final SyncBufferCallback finishes for the SurfaceSyncGroup, in most cases the
+ * 1. Each SyncTarget will get a {@link SyncTarget#onAddedToSyncGroup} callback that contains a
+ * {@link TransactionReadyCallback}.
+ * 2. Each {@link SyncTarget} needs to invoke
+ * {@link TransactionReadyCallback#onTransactionReady(Transaction)}. This makes sure the
+ * SurfaceSyncGroup knows when the SyncTarget is complete, allowing the SurfaceSyncGroup to get the
+ * Transaction that contains the buffer.
+ * 3. When the final TransactionReadyCallback finishes for the SurfaceSyncGroup, in most cases the
  * transaction is applied and then the sync complete callbacks are invoked, letting the callers know
  * the sync is now complete.
  *
@@ -86,8 +87,6 @@
     private final Transaction mTransaction = sTransactionFactory.get();
     @GuardedBy("mLock")
     private boolean mSyncReady;
-    @GuardedBy("mLock")
-    private final Set<SyncTarget> mSyncTargets = new ArraySet<>();
 
     @GuardedBy("mLock")
     private Consumer<Transaction> mSyncRequestCompleteCallback;
@@ -197,14 +196,13 @@
      * Add a {@link SyncTarget} to a sync set. The sync set will wait for all
      * SyncableSurfaces to complete before notifying.
      *
-     * @param syncTarget A SyncableSurface that implements how to handle syncing
-     *                   buffers.
+     * @param syncTarget A SyncTarget that implements how to handle syncing transactions.
      * @return true if the SyncTarget was successfully added to the SyncGroup, false otherwise.
      */
     public boolean addToSync(SyncTarget syncTarget) {
-        SyncBufferCallback syncBufferCallback = new SyncBufferCallback() {
+        TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() {
             @Override
-            public void onBufferReady(Transaction t) {
+            public void onTransactionReady(Transaction t) {
                 synchronized (mLock) {
                     if (t != null) {
                         mTransaction.merge(t);
@@ -221,10 +219,9 @@
                         + "SyncTargets can be added.");
                 return false;
             }
-            mPendingSyncs.add(syncBufferCallback.hashCode());
-            mSyncTargets.add(syncTarget);
+            mPendingSyncs.add(transactionReadyCallback.hashCode());
         }
-        syncTarget.onReadyToSync(syncBufferCallback);
+        syncTarget.onAddedToSyncGroup(this, transactionReadyCallback);
         return true;
     }
 
@@ -256,17 +253,13 @@
             Log.d(TAG, "Successfully finished sync id=" + this);
         }
 
-        for (SyncTarget syncTarget : mSyncTargets) {
-            syncTarget.onSyncComplete();
-        }
-        mSyncTargets.clear();
         mSyncRequestCompleteCallback.accept(mTransaction);
         mFinished = true;
     }
 
     /**
      * Add a Transaction to this sync set. This allows the caller to provide other info that
-     * should be synced with the buffers.
+     * should be synced with the transactions.
      */
     public void addTransactionToSync(Transaction t) {
         synchronized (mLock) {
@@ -334,9 +327,10 @@
         }
 
         @Override
-        public void onReadyToSync(SyncBufferCallback syncBufferCallback) {
+        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
+                TransactionReadyCallback transactionReadyCallback) {
             mFrameCallbackConsumer.accept(
-                    () -> mSurfaceView.syncNextFrame(syncBufferCallback::onBufferReady));
+                    () -> mSurfaceView.syncNextFrame(transactionReadyCallback::onTransactionReady));
         }
     }
 
@@ -345,22 +339,19 @@
      */
     public interface SyncTarget {
         /**
-         * Called when the Syncable is ready to begin handing a sync request. When invoked, the
-         * implementor is required to call {@link SyncBufferCallback#onBufferReady(Transaction)}
-         * and {@link SyncBufferCallback#onBufferReady(Transaction)} in order for this Syncable
-         * to be marked as complete.
+         * Called when the SyncTarget has been added to a SyncGroup as is ready to begin handing a
+         * sync request. When invoked, the implementor is required to call
+         * {@link TransactionReadyCallback#onTransactionReady(Transaction)} in order for this
+         * SurfaceSyncGroup to fully complete.
          *
          * Always invoked on the thread that initiated the call to {@link #addToSync(SyncTarget)}
          *
-         * @param syncBufferCallback A SyncBufferCallback that the caller must invoke onBufferReady
+         * @param parentSyncGroup The sync group this target has been added to.
+         * @param transactionReadyCallback A TransactionReadyCallback that the caller must invoke
+         *                                 onTransactionReady
          */
-        void onReadyToSync(SyncBufferCallback syncBufferCallback);
-
-        /**
-         * There's no guarantee about the thread this callback is invoked on.
-         */
-        default void onSyncComplete() {
-        }
+        void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
+                TransactionReadyCallback transactionReadyCallback);
     }
 
     /**
@@ -368,14 +359,14 @@
      * completed. The caller should invoke the calls when the rendering has started and finished a
      * frame.
      */
-    public interface SyncBufferCallback {
+    public interface TransactionReadyCallback {
         /**
-         * Invoked when the transaction contains the buffer and is ready to sync.
+         * Invoked when the transaction is ready to sync.
          *
-         * @param t The transaction that contains the buffer to be synced. This can be null if
-         *          there's nothing to sync
+         * @param t The transaction that contains the anything to be included in the synced. This
+         *          can be null if there's nothing to sync
          */
-        void onBufferReady(@Nullable Transaction t);
+        void onTransactionReady(@Nullable Transaction t);
     }
 
     /**
diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java
index e2c8a31..dc60edd 100644
--- a/core/java/android/window/TaskFragmentInfo.java
+++ b/core/java/android/window/TaskFragmentInfo.java
@@ -83,6 +83,12 @@
     private final boolean mIsTaskFragmentClearedForPip;
 
     /**
+     * Whether the last running activity of the TaskFragment was removed because it was reordered to
+     * front of the Task.
+     */
+    private final boolean mIsClearedForReorderActivityToFront;
+
+    /**
      * The maximum {@link ActivityInfo.WindowLayout#minWidth} and
      * {@link ActivityInfo.WindowLayout#minHeight} aggregated from the TaskFragment's child
      * activities.
@@ -96,7 +102,7 @@
             @NonNull Configuration configuration, int runningActivityCount,
             boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
             boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip,
-            @NonNull Point minimumDimensions) {
+            boolean isClearedForReorderActivityToFront, @NonNull Point minimumDimensions) {
         mFragmentToken = requireNonNull(fragmentToken);
         mToken = requireNonNull(token);
         mConfiguration.setTo(configuration);
@@ -106,6 +112,7 @@
         mPositionInParent.set(positionInParent);
         mIsTaskClearedForReuse = isTaskClearedForReuse;
         mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
+        mIsClearedForReorderActivityToFront = isClearedForReorderActivityToFront;
         mMinimumDimensions.set(minimumDimensions);
     }
 
@@ -160,6 +167,11 @@
         return mIsTaskFragmentClearedForPip;
     }
 
+    /** @hide */
+    public boolean isClearedForReorderActivityToFront() {
+        return mIsClearedForReorderActivityToFront;
+    }
+
     @WindowingMode
     public int getWindowingMode() {
         return mConfiguration.windowConfiguration.getWindowingMode();
@@ -207,6 +219,7 @@
                 && mPositionInParent.equals(that.mPositionInParent)
                 && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
                 && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip
+                && mIsClearedForReorderActivityToFront == that.mIsClearedForReorderActivityToFront
                 && mMinimumDimensions.equals(that.mMinimumDimensions);
     }
 
@@ -220,6 +233,7 @@
         mPositionInParent.readFromParcel(in);
         mIsTaskClearedForReuse = in.readBoolean();
         mIsTaskFragmentClearedForPip = in.readBoolean();
+        mIsClearedForReorderActivityToFront = in.readBoolean();
         mMinimumDimensions.readFromParcel(in);
     }
 
@@ -235,6 +249,7 @@
         mPositionInParent.writeToParcel(dest, flags);
         dest.writeBoolean(mIsTaskClearedForReuse);
         dest.writeBoolean(mIsTaskFragmentClearedForPip);
+        dest.writeBoolean(mIsClearedForReorderActivityToFront);
         mMinimumDimensions.writeToParcel(dest, flags);
     }
 
@@ -262,8 +277,9 @@
                 + " activities=" + mActivities
                 + " positionInParent=" + mPositionInParent
                 + " isTaskClearedForReuse=" + mIsTaskClearedForReuse
-                + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip
-                + " minimumDimensions" + mMinimumDimensions
+                + " isTaskFragmentClearedForPip=" + mIsTaskFragmentClearedForPip
+                + " mIsClearedForReorderActivityToFront=" + mIsClearedForReorderActivityToFront
+                + " minimumDimensions=" + mMinimumDimensions
                 + "}";
     }
 
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index db15145..e62d5c9 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -296,7 +296,7 @@
                     out.append((i == 0 ? "" : ",") + TransitionInfo.modeToString(mModes[i]));
                 }
             }
-            out.append("]").toString();
+            out.append("]");
             out.append(" flags=" + TransitionInfo.flagsToString(mFlags));
             out.append(" mustBeTask=" + mMustBeTask);
             out.append(" order=" + containerOrderToString(mOrder));
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 8815ab3..c2da638 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -138,8 +138,15 @@
     /** The container is a system window, excluding wallpaper and input-method. */
     public static final int FLAG_IS_SYSTEM_WINDOW = 1 << 16;
 
+    /** The window was animated by back gesture. */
+    public static final int FLAG_BACK_GESTURE_ANIMATED = 1 << 17;
+
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 17;
+    public static final int FLAG_FIRST_CUSTOM = 1 << 18;
+
+    /** The change belongs to a window that won't contain activities. */
+    public static final int FLAGS_IS_NON_APP_WINDOW =
+            FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD | FLAG_IS_SYSTEM_WINDOW;
 
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
@@ -161,6 +168,7 @@
             FLAG_IS_BEHIND_STARTING_WINDOW,
             FLAG_IS_OCCLUDED,
             FLAG_IS_SYSTEM_WINDOW,
+            FLAG_BACK_GESTURE_ANIMATED,
             FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
@@ -376,6 +384,9 @@
         if ((flags & FLAG_IS_SYSTEM_WINDOW) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FLAG_IS_SYSTEM_WINDOW");
         }
+        if ((flags & FLAG_BACK_GESTURE_ANIMATED) != 0) {
+            sb.append(sb.length() == 0 ? "" : "|").append("FLAG_BACK_GESTURE_ANIMATED");
+        }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
@@ -579,11 +590,16 @@
             return mFlags;
         }
 
-        /** Whether the given change flags has included in this change. */
+        /** Whether this change contains any of the given change flags. */
         public boolean hasFlags(@ChangeFlags int flags) {
             return (mFlags & flags) != 0;
         }
 
+        /** Whether this change contains all of the given change flags. */
+        public boolean hasAllFlags(@ChangeFlags int flags) {
+            return (mFlags & flags) == flags;
+        }
+
         /**
          * @return the bounds of the container before the change. It may be empty if the container
          * is coming into existence.
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 2d29c59..fc64eb9 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -454,6 +454,23 @@
     }
 
     /**
+     * Sets whether a container is being drag-resized.
+     * When {@code true}, the client will reuse a single (larger) surface size to avoid
+     * continuous allocations on every size change.
+     *
+     * @param container WindowContainerToken of the task that changed its drag resizing state
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction setDragResizing(@NonNull WindowContainerToken container,
+            boolean dragResizing) {
+        final Change change = getOrCreateChange(container.asBinder());
+        change.mChangeMask |= Change.CHANGE_DRAG_RESIZING;
+        change.mDragResizing = dragResizing;
+        return this;
+    }
+
+    /**
      * Sends a pending intent in sync.
      * @param sender The PendingIntent sender.
      * @param intent The fillIn intent to patch over the sender's base intent.
@@ -711,6 +728,29 @@
     }
 
     /**
+     * Sets the TaskFragment {@code container} to have a companion TaskFragment {@code companion}.
+     * This indicates that the organizer will remove the TaskFragment when the companion
+     * TaskFragment is removed.
+     *
+     * @param container the TaskFragment container
+     * @param companion the companion TaskFragment. If it is {@code null}, the transaction will
+     *                  reset the companion TaskFragment.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder container,
+            @Nullable IBinder companion) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT)
+                        .setContainer(container)
+                        .setReparentContainer(companion)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Sets/removes the always on top flag for this {@code windowContainer}. See
      * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}.
      * Please note that this method is only intended to be used for a
@@ -894,12 +934,14 @@
         public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 5;
         public static final int CHANGE_FORCE_NO_PIP = 1 << 6;
         public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 7;
+        public static final int CHANGE_DRAG_RESIZING = 1 << 8;
 
         private final Configuration mConfiguration = new Configuration();
         private boolean mFocusable = true;
         private boolean mHidden = false;
         private boolean mIgnoreOrientationRequest = false;
         private boolean mForceTranslucent = false;
+        private boolean mDragResizing = false;
 
         private int mChangeMask = 0;
         private @ActivityInfo.Config int mConfigSetMask = 0;
@@ -920,6 +962,7 @@
             mHidden = in.readBoolean();
             mIgnoreOrientationRequest = in.readBoolean();
             mForceTranslucent = in.readBoolean();
+            mDragResizing = in.readBoolean();
             mChangeMask = in.readInt();
             mConfigSetMask = in.readInt();
             mWindowSetMask = in.readInt();
@@ -968,6 +1011,9 @@
             if ((other.mChangeMask & CHANGE_FORCE_TRANSLUCENT) != 0) {
                 mForceTranslucent = other.mForceTranslucent;
             }
+            if ((other.mChangeMask & CHANGE_DRAG_RESIZING) != 0) {
+                mDragResizing = other.mDragResizing;
+            }
             mChangeMask |= other.mChangeMask;
             if (other.mActivityWindowingMode >= 0) {
                 mActivityWindowingMode = other.mActivityWindowingMode;
@@ -1027,6 +1073,15 @@
             return mForceTranslucent;
         }
 
+        /** Gets the requested drag resizing state. */
+        public boolean getDragResizing() {
+            if ((mChangeMask & CHANGE_DRAG_RESIZING) == 0) {
+                throw new RuntimeException("Drag resizing not set. "
+                        + "Check CHANGE_DRAG_RESIZING first");
+            }
+            return mDragResizing;
+        }
+
         public int getChangeMask() {
             return mChangeMask;
         }
@@ -1088,6 +1143,9 @@
             if ((mChangeMask & CHANGE_FOCUSABLE) != 0) {
                 sb.append("focusable:" + mFocusable + ",");
             }
+            if ((mChangeMask & CHANGE_DRAG_RESIZING) != 0) {
+                sb.append("dragResizing:" + mDragResizing + ",");
+            }
             if (mBoundsChangeTransaction != null) {
                 sb.append("hasBoundsTransaction,");
             }
@@ -1105,6 +1163,7 @@
             dest.writeBoolean(mHidden);
             dest.writeBoolean(mIgnoreOrientationRequest);
             dest.writeBoolean(mForceTranslucent);
+            dest.writeBoolean(mDragResizing);
             dest.writeInt(mChangeMask);
             dest.writeInt(mConfigSetMask);
             dest.writeInt(mWindowSetMask);
@@ -1169,6 +1228,7 @@
         public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19;
         public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20;
         public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21;
+        public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1383,6 +1443,11 @@
         }
 
         @NonNull
+        public IBinder getCompanionContainer() {
+            return mReparent;
+        }
+
+        @NonNull
         public IBinder getCallingActivity() {
             return mReparent;
         }
@@ -1492,6 +1557,9 @@
                     return "{RemoveTask: task=" + mContainer + "}";
                 case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
                     return "{finishActivity: activity=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT:
+                    return "{setCompanionTaskFragment: container = " + mContainer + " companion = "
+                            + mReparent + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
                             + " mToTop=" + mToTop
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 0730f3d..fda39c1 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -218,19 +218,24 @@
     public Checker getChecker() {
         return mChecker;
     }
+    @NonNull
+    private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
 
     static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
         private final WeakReference<OnBackInvokedCallback> mCallback;
+
         OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
             mCallback = new WeakReference<>(callback);
         }
 
         @Override
-        public void onBackStarted() {
+        public void onBackStarted(BackEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackStarted();
+                    mProgressAnimator.onBackStarted(backEvent, event ->
+                            callback.onBackProgressed(event));
+                    callback.onBackStarted(backEvent);
                 }
             });
         }
@@ -240,7 +245,7 @@
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackProgressed(backEvent);
+                    mProgressAnimator.onBackProgressed(backEvent);
                 }
             });
         }
@@ -248,6 +253,7 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     callback.onBackCancelled();
@@ -258,6 +264,7 @@
         @Override
         public void onBackInvoked() throws RemoteException {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackInvokedCallback callback = mCallback.get();
                 if (callback == null) {
                     return;
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 2a80d02..930aaa2 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -26,6 +26,7 @@
 import android.os.RemoteException;
 import android.util.Singleton;
 import android.view.RemoteAnimationAdapter;
+import android.view.SurfaceControl;
 
 /**
  * Base class for organizing specific types of windows like Tasks and DisplayAreas
@@ -184,6 +185,26 @@
         }
     }
 
+    /**
+     * Use WM's transaction-queue instead of Shell's independent one. This is necessary
+     * if WM and Shell need to coordinate transactions (eg. for shell transitions).
+     * @return true if successful, false otherwise.
+     * @hide
+     */
+    public boolean shareTransactionQueue() {
+        final IBinder wmApplyToken;
+        try {
+            wmApplyToken = getWindowOrganizerController().getApplyToken();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        if (wmApplyToken == null) {
+            return false;
+        }
+        SurfaceControl.Transaction.setDefaultApplyToken(wmApplyToken);
+        return true;
+    }
+
     static IWindowOrganizerController getWindowOrganizerController() {
         return IWindowOrganizerControllerSingleton.get();
     }
diff --git a/core/java/android/window/WindowProvider.java b/core/java/android/window/WindowProvider.java
index b078b93..dbdc68f 100644
--- a/core/java/android/window/WindowProvider.java
+++ b/core/java/android/window/WindowProvider.java
@@ -15,8 +15,10 @@
  */
 package android.window;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.view.WindowManager.LayoutParams.WindowType;
 
 /**
@@ -36,4 +38,11 @@
     /** Gets the launch options of this provider */
     @Nullable
     Bundle getWindowContextOptions();
+
+    /**
+     * Gets the WindowContextToken of this provider.
+     * @see android.content.Context#getWindowContextToken
+     */
+    @NonNull
+    IBinder getWindowContextToken();
 }
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index 2d2c8de..fdc3e5a 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -27,7 +27,10 @@
 import android.app.ActivityThread;
 import android.app.LoadedApk;
 import android.app.Service;
+import android.content.ComponentCallbacks;
+import android.content.ComponentCallbacksController;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -54,6 +57,8 @@
     private final WindowContextController mController = new WindowContextController(mWindowToken);
     private WindowManager mWindowManager;
     private boolean mInitialized;
+    private final ComponentCallbacksController mCallbacksController =
+            new ComponentCallbacksController();
 
     /**
      * Returns {@code true} if the {@code windowContextOptions} declares that it is a
@@ -118,6 +123,48 @@
         return mOptions;
     }
 
+    @SuppressLint({"OnNameExpected", "ExecutorRegistration"})
+    // Suppress lint because this is a legacy named function and doesn't have an optional param
+    // for executor.
+    // TODO(b/259347943): Update documentation for U.
+    /**
+     * Here we override to prevent WindowProviderService from invoking
+     * {@link Application.registerComponentCallback}, which will result in callback registered
+     * for process-level Configuration change updates.
+     */
+    @Override
+    public void registerComponentCallbacks(@NonNull ComponentCallbacks callback) {
+        // For broadcasting Configuration Changes.
+        mCallbacksController.registerCallbacks(callback);
+    }
+
+    @SuppressLint("OnNameExpected")
+    @Override
+    public void unregisterComponentCallbacks(@NonNull ComponentCallbacks callback) {
+        mCallbacksController.unregisterCallbacks(callback);
+    }
+
+    @SuppressLint("OnNameExpected")
+    @Override
+    public void onConfigurationChanged(@Nullable Configuration configuration) {
+        // This is only called from WindowTokenClient.
+        mCallbacksController.dispatchConfigurationChanged(configuration);
+    }
+
+    /**
+     * Override {@link Service}'s empty implementation and listen to {@link ActivityThread} for
+     * low memory and trim memory events.
+     */
+    @Override
+    public void onLowMemory() {
+        mCallbacksController.dispatchLowMemory();
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        mCallbacksController.dispatchTrimMemory(level);
+    }
+
     /**
      * Returns the display ID to launch this {@link WindowProviderService}.
      *
@@ -181,5 +228,6 @@
     public void onDestroy() {
         super.onDestroy();
         mController.detachIfNeeded();
+        mCallbacksController.clearCallbacks();
     }
 }
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
new file mode 100644
index 0000000..65e8c64
--- /dev/null
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -0,0 +1,139 @@
+/*
+ * 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.app;
+
+import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
+import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.LocaleList;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/** The Locale data collector for per-app language. */
+class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+    private static final String TAG = AppLocaleCollector.class.getSimpleName();
+    private final Context mContext;
+    private final String mAppPackageName;
+    private final LocaleStore.LocaleInfo mAppCurrentLocale;
+
+    AppLocaleCollector(Context context, String appPackageName) {
+        mContext = context;
+        mAppPackageName = appPackageName;
+        mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo(
+                mContext, mAppPackageName);
+    }
+
+    @Override
+    public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
+        HashSet<String> langTagsToIgnore = new HashSet<>();
+
+        LocaleList systemLangList = LocaleList.getDefault();
+        for(int i = 0; i < systemLangList.size(); i++) {
+            langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
+        }
+
+        if (mAppCurrentLocale != null) {
+            langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
+        }
+        return langTagsToIgnore;
+    }
+
+    @Override
+    public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
+            boolean translatedOnly, boolean isForCountryMode) {
+        AppLocaleStore.AppLocaleResult result =
+                AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+        Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
+        Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>();
+        Set<LocaleStore.LocaleInfo> systemLocaleList;
+        boolean shouldShowList =
+                result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG
+                        || result.mLocaleStatus == GET_SUPPORTED_LANGUAGE_FROM_ASSET;
+
+        // Get system supported locale list
+        if (isForCountryMode) {
+            systemLocaleList = LocaleStore.getLevelLocales(mContext,
+                    langTagsToIgnore, parent, translatedOnly);
+        } else {
+            systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
+                    null /* no parent */, translatedOnly);
+        }
+
+        // Add current app locale
+        if (mAppCurrentLocale != null && !isForCountryMode) {
+            appLocaleList.add(mAppCurrentLocale);
+        }
+
+        // Add current system language into suggestion list
+        for(LocaleStore.LocaleInfo localeInfo:
+                LocaleStore.getSystemCurrentLocaleInfo()) {
+            boolean isNotCurrentLocale = mAppCurrentLocale == null
+                    || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
+            if (!isForCountryMode && isNotCurrentLocale) {
+                appLocaleList.add(localeInfo);
+            }
+        }
+
+        // Add the languages that included in system supported locale
+        if (shouldShowList) {
+            appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale(
+                    systemLocaleList, result.mAppSupportedLocales));
+        }
+
+        // Add "system language" option
+        if (!isForCountryMode && shouldShowList) {
+            appLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo(
+                    mAppCurrentLocale == null));
+        }
+
+        if (Build.isDebuggable()) {
+            Log.d(TAG, "App locale list: " + appLocaleList);
+        }
+
+        return appLocaleList;
+    }
+
+    @Override
+    public boolean hasSpecificPackageName() {
+        return true;
+    }
+
+    private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale(
+            Set<LocaleStore.LocaleInfo> systemLocaleList,
+            HashSet<Locale> appSupportedLocales) {
+        Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
+
+        for(LocaleStore.LocaleInfo li: systemLocaleList) {
+            if (appSupportedLocales.contains(li.getLocale())) {
+                filteredList.add(li);
+            } else {
+                for(Locale l: appSupportedLocales) {
+                    if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
+                        filteredList.add(li);
+                        break;
+                    }
+                }
+            }
+        }
+        return filteredList;
+    }
+}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c8eec7d..d37779b 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2096,7 +2096,8 @@
         return matchingShortcuts;
     }
 
-    private void sendShortcutManagerShareTargetResults(
+    @VisibleForTesting
+    protected void sendShortcutManagerShareTargetResults(
             int shortcutType, ServiceResultInfo[] results) {
         final Message msg = Message.obtain();
         msg.what = ChooserHandler.SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS;
@@ -3873,7 +3874,11 @@
         }
     }
 
-    static class ServiceResultInfo {
+    /**
+     * Shortcuts grouped by application.
+     */
+    @VisibleForTesting
+    public static class ServiceResultInfo {
         public final DisplayResolveInfo originalTarget;
         public final List<ChooserTarget> resultTargets;
         public final UserHandle userHandle;
diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl
index 30da4b4..88447da 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -58,11 +58,12 @@
     SyncNotedAppOp noteProxyOperation(int code, in AttributionSource attributionSource,
             boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
             boolean skipProxyOperation);
-    SyncNotedAppOp startProxyOperation(int code, in AttributionSource attributionSource,
-            boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message,
-            boolean shouldCollectMessage, boolean skipProxyOperation, int proxyAttributionFlags,
-            int proxiedAttributionFlags, int attributionChainId);
-    void finishProxyOperation(int code, in AttributionSource attributionSource,
+    SyncNotedAppOp startProxyOperation(IBinder clientId, int code,
+            in AttributionSource attributionSource, boolean startIfModeDefault,
+            boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
+            boolean skipProxyOperation, int proxyAttributionFlags, int proxiedAttributionFlags,
+            int attributionChainId);
+    void finishProxyOperation(IBinder clientId, int code, in AttributionSource attributionSource,
             boolean skipProxyOperation);
 
     // Remaining methods are only used in Java.
diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java
index 999be08..65372be 100644
--- a/core/java/com/android/internal/app/LocalePicker.java
+++ b/core/java/com/android/internal/app/LocalePicker.java
@@ -314,8 +314,7 @@
 
         try {
             final IActivityManager am = ActivityManager.getService();
-            final Configuration config = am.getConfiguration();
-
+            final Configuration config = new Configuration();
             config.setLocales(locales);
             config.userSetLocale = true;
 
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index 965895f..3efd279 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -16,16 +16,12 @@
 
 package com.android.internal.app;
 
-import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus;
-
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.app.ListFragment;
 import android.content.Context;
 import android.os.Bundle;
-import android.os.LocaleList;
 import android.text.TextUtils;
-import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -36,7 +32,6 @@
 
 import com.android.internal.R;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
@@ -54,6 +49,7 @@
 
     private SuggestedLocaleAdapter mAdapter;
     private LocaleSelectedListener mListener;
+    private LocaleCollectorBase mLocalePickerCollector;
     private Set<LocaleStore.LocaleInfo> mLocaleList;
     private LocaleStore.LocaleInfo mParentLocale;
     private boolean mTranslatedOnly = false;
@@ -62,7 +58,6 @@
     private boolean mPreviousSearchHadFocus = false;
     private int mFirstVisiblePosition = 0;
     private int mTopDistance = 0;
-    private String mAppPackageName;
     private CharSequence mTitle = null;
     private OnActionExpandListener mOnActionExpandListener;
 
@@ -79,31 +74,50 @@
         void onLocaleSelected(LocaleStore.LocaleInfo locale);
     }
 
-    private static LocalePickerWithRegion createCountryPicker(Context context,
+    /**
+     * The interface which provides the locale list.
+     */
+    interface LocaleCollectorBase {
+        /** Gets the ignored locale list. */
+        HashSet<String> getIgnoredLocaleList(boolean translatedOnly);
+
+        /** Gets the supported locale list. */
+        Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
+                boolean translatedOnly, boolean isForCountryMode);
+
+        /** Indicates if the class work for specific package. */
+        boolean hasSpecificPackageName();
+    }
+
+    private static LocalePickerWithRegion createCountryPicker(
             LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
-            boolean translatedOnly, String appPackageName,
-            OnActionExpandListener onActionExpandListener) {
+            boolean translatedOnly, OnActionExpandListener onActionExpandListener,
+            LocaleCollectorBase localePickerCollector) {
         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
         localePicker.setOnActionExpandListener(onActionExpandListener);
-        boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
-                translatedOnly, appPackageName);
+        boolean shouldShowTheList = localePicker.setListener(listener, parent,
+                translatedOnly, localePickerCollector);
         return shouldShowTheList ? localePicker : null;
     }
 
     public static LocalePickerWithRegion createLanguagePicker(Context context,
             LocaleSelectedListener listener, boolean translatedOnly) {
-        LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
-        localePicker.setListener(context, listener, /* parent */ null, translatedOnly, null);
-        return localePicker;
+        return createLanguagePicker(context, listener, translatedOnly, null, null);
     }
 
     public static LocalePickerWithRegion createLanguagePicker(Context context,
             LocaleSelectedListener listener, boolean translatedOnly, String appPackageName,
             OnActionExpandListener onActionExpandListener) {
+        LocaleCollectorBase localePickerController;
+        if (TextUtils.isEmpty(appPackageName)) {
+            localePickerController = new SystemLocaleCollector(context);
+        } else {
+            localePickerController = new AppLocaleCollector(context, appPackageName);
+        }
         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
         localePicker.setOnActionExpandListener(onActionExpandListener);
-        localePicker.setListener(
-                context, listener, /* parent */ null, translatedOnly, appPackageName);
+        localePicker.setListener(listener, /* parent */ null, translatedOnly,
+                localePickerController);
         return localePicker;
     }
 
@@ -120,109 +134,23 @@
      * In this case we don't even show the list, we call the listener with that locale,
      * "pretending" it was selected, and return false.</p>
      */
-    private boolean setListener(Context context, LocaleSelectedListener listener,
-            LocaleStore.LocaleInfo parent, boolean translatedOnly, String appPackageName) {
+    private boolean setListener(LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
+            boolean translatedOnly, LocaleCollectorBase localePickerController) {
         this.mParentLocale = parent;
         this.mListener = listener;
         this.mTranslatedOnly = translatedOnly;
-        this.mAppPackageName = appPackageName;
+        this.mLocalePickerCollector = localePickerController;
         setRetainInstance(true);
 
-        final HashSet<String> langTagsToIgnore = new HashSet<>();
-        LocaleStore.LocaleInfo appCurrentLocale =
-                LocaleStore.getAppCurrentLocaleInfo(context, appPackageName);
-        boolean isForCountryMode = parent != null;
+        mLocaleList = localePickerController.getSupportedLocaleList(
+                parent, translatedOnly, parent != null);
 
-        if (!TextUtils.isEmpty(appPackageName) && !isForCountryMode) {
-            // Filter current system locale to add them into suggestion
-            LocaleList systemLangList = LocaleList.getDefault();
-            for(int i = 0; i < systemLangList.size(); i++) {
-                langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
-            }
-
-            if (appCurrentLocale != null) {
-                Log.d(TAG, "appCurrentLocale: " + appCurrentLocale.getLocale().toLanguageTag());
-                langTagsToIgnore.add(appCurrentLocale.getLocale().toLanguageTag());
-            } else {
-                Log.d(TAG, "appCurrentLocale is null");
-            }
-        } else if (!translatedOnly) {
-            final LocaleList userLocales = LocalePicker.getLocales();
-            final String[] langTags = userLocales.toLanguageTags().split(",");
-            Collections.addAll(langTagsToIgnore, langTags);
-        }
-
-        if (isForCountryMode) {
-            mLocaleList = LocaleStore.getLevelLocales(context,
-                    langTagsToIgnore, parent, translatedOnly);
-            if (mLocaleList.size() <= 1) {
-                if (listener != null && (mLocaleList.size() == 1)) {
-                    listener.onLocaleSelected(mLocaleList.iterator().next());
-                }
-                return false;
-            }
+        if (parent != null && listener != null && mLocaleList.size() == 1) {
+            listener.onLocaleSelected(mLocaleList.iterator().next());
+            return false;
         } else {
-            mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
-                    null /* no parent */, translatedOnly);
+            return true;
         }
-        Log.d(TAG, "mLocaleList size:  " + mLocaleList.size());
-
-        // Adding current locale and system default option into suggestion list
-        if(!TextUtils.isEmpty(appPackageName)) {
-            if (appCurrentLocale != null && !isForCountryMode) {
-                mLocaleList.add(appCurrentLocale);
-            }
-
-            AppLocaleStore.AppLocaleResult result =
-                    AppLocaleStore.getAppSupportedLocales(context, appPackageName);
-            boolean shouldShowList =
-                    result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG
-                    || result.mLocaleStatus == LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
-
-            // Add current system language into suggestion list
-            for(LocaleStore.LocaleInfo localeInfo: LocaleStore.getSystemCurrentLocaleInfo()) {
-                boolean isNotCurrentLocale = appCurrentLocale == null
-                        || !localeInfo.getLocale().equals(appCurrentLocale.getLocale());
-                if (!isForCountryMode && isNotCurrentLocale) {
-                    mLocaleList.add(localeInfo);
-                }
-            }
-
-            // Filter the language not support in app
-            mLocaleList = filterTheLanguagesNotSupportedInApp(
-                    shouldShowList, result.mAppSupportedLocales);
-
-            Log.d(TAG, "mLocaleList after app-supported filter:  " + mLocaleList.size());
-
-            // Add "system language"
-            if (!isForCountryMode && shouldShowList) {
-                mLocaleList.add(LocaleStore.getSystemDefaultLocaleInfo(appCurrentLocale == null));
-            }
-        }
-        return true;
-    }
-
-    private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp(
-            boolean shouldShowList, HashSet<Locale> supportedLocales) {
-        Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
-        if (!shouldShowList) {
-            return filteredList;
-        }
-
-        for(LocaleStore.LocaleInfo li: mLocaleList) {
-            if (supportedLocales.contains(li.getLocale())) {
-                filteredList.add(li);
-            } else {
-                for(Locale l: supportedLocales) {
-                    if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
-                        filteredList.add(li);
-                        break;
-                    }
-                }
-            }
-        }
-
-        return filteredList;
     }
 
     private void returnToParentFrame() {
@@ -246,7 +174,9 @@
         mTitle = getActivity().getTitle();
         final boolean countryMode = mParentLocale != null;
         final Locale sortingLocale = countryMode ? mParentLocale.getLocale() : Locale.getDefault();
-        mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, mAppPackageName);
+        final boolean hasSpecificPackageName =
+                mLocalePickerCollector != null && mLocalePickerCollector.hasSpecificPackageName();
+        mAdapter = new SuggestedLocaleAdapter(mLocaleList, countryMode, hasSpecificPackageName);
         final LocaleHelper.LocaleInfoComparator comp =
                 new LocaleHelper.LocaleInfoComparator(sortingLocale, countryMode);
         mAdapter.sort(comp);
@@ -321,8 +251,8 @@
             returnToParentFrame();
         } else {
             LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
-                    getContext(), mListener, locale, mTranslatedOnly /* translate only */,
-                    mAppPackageName, mOnActionExpandListener);
+                    mListener, locale, mTranslatedOnly /* translate only */,
+                    mOnActionExpandListener, this.mLocalePickerCollector);
             if (selector != null) {
                 getFragmentManager().beginTransaction()
                         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
@@ -340,7 +270,8 @@
             inflater.inflate(R.menu.language_selection_list, menu);
 
             final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu);
-            if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) {
+            if (mLocalePickerCollector.hasSpecificPackageName()
+                    && mOnActionExpandListener != null) {
                 searchMenuItem.setOnActionExpandListener(mOnActionExpandListener);
             }
 
diff --git a/core/java/com/android/internal/app/LogAccessDialogActivity.java b/core/java/com/android/internal/app/LogAccessDialogActivity.java
deleted file mode 100644
index 4adb867..0000000
--- a/core/java/com/android/internal/app/LogAccessDialogActivity.java
+++ /dev/null
@@ -1,260 +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.internal.app;
-
-import android.annotation.StyleRes;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Configuration;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.text.Html;
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.text.style.TypefaceSpan;
-import android.text.style.URLSpan;
-import android.util.Slog;
-import android.view.ContextThemeWrapper;
-import android.view.InflateException;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.android.internal.R;
-
-/**
- * Dialog responsible for obtaining user consent per-use log access
- */
-public class LogAccessDialogActivity extends Activity implements
-        View.OnClickListener {
-    private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
-    public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
-
-
-    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
-    private static final int MSG_DISMISS_DIALOG = 0;
-
-    private String mPackageName;
-    private int mUid;
-    private ILogAccessDialogCallback mCallback;
-
-    private String mAlertTitle;
-    private String mAlertBody;
-    private String mAlertLearnMore;
-    private AlertDialog.Builder mAlertDialog;
-    private AlertDialog mAlert;
-    private View mAlertView;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        // retrieve Intent extra information
-        if (!readIntentInfo(getIntent())) {
-            Slog.e(TAG, "Invalid Intent extras, finishing");
-            finish();
-            return;
-        }
-
-        // retrieve the title string from passed intent extra
-        try {
-            mAlertTitle = getTitleString(this, mPackageName, mUid);
-        } catch (NameNotFoundException e) {
-            Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e);
-            declineLogAccess();
-            finish();
-            return;
-        }
-
-        mAlertBody = getResources().getString(R.string.log_access_confirmation_body);
-        mAlertLearnMore = getResources().getString(R.string.log_access_confirmation_learn_more);
-
-        // create View
-        boolean isDarkTheme = (getResources().getConfiguration().uiMode
-                & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
-        int themeId = isDarkTheme ? android.R.style.Theme_DeviceDefault_Dialog_Alert :
-                android.R.style.Theme_DeviceDefault_Light_Dialog_Alert;
-        mAlertView = createView(themeId);
-
-        // create AlertDialog
-        mAlertDialog = new AlertDialog.Builder(this, themeId);
-        mAlertDialog.setView(mAlertView);
-        mAlertDialog.setOnCancelListener(dialog -> declineLogAccess());
-        mAlertDialog.setOnDismissListener(dialog -> finish());
-
-        // show Alert
-        mAlert = mAlertDialog.create();
-        mAlert.getWindow().setHideOverlayWindows(true);
-        mAlert.show();
-
-        // set Alert Timeout
-        mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT);
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        if (!isChangingConfigurations() && mAlert != null && mAlert.isShowing()) {
-            mAlert.dismiss();
-        }
-        mAlert = null;
-    }
-
-    private boolean readIntentInfo(Intent intent) {
-        if (intent == null) {
-            Slog.e(TAG, "Intent is null");
-            return false;
-        }
-
-        mCallback = ILogAccessDialogCallback.Stub.asInterface(
-                intent.getExtras().getBinder(EXTRA_CALLBACK));
-        if (mCallback == null) {
-            Slog.e(TAG, "Missing callback");
-            return false;
-        }
-
-        mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
-        if (mPackageName == null || mPackageName.length() == 0) {
-            Slog.e(TAG, "Missing package name extra");
-            return false;
-        }
-
-        if (!intent.hasExtra(Intent.EXTRA_UID)) {
-            Slog.e(TAG, "Missing EXTRA_UID");
-            return false;
-        }
-
-        mUid = intent.getIntExtra(Intent.EXTRA_UID, 0);
-
-        return true;
-    }
-
-    private Handler mHandler = new Handler() {
-        public void handleMessage(android.os.Message msg) {
-            switch (msg.what) {
-                case MSG_DISMISS_DIALOG:
-                    if (mAlert != null) {
-                        mAlert.dismiss();
-                        mAlert = null;
-                        declineLogAccess();
-                    }
-                    break;
-
-                default:
-                    break;
-            }
-        }
-    };
-
-    private String getTitleString(Context context, String callingPackage, int uid)
-            throws NameNotFoundException {
-        PackageManager pm = context.getPackageManager();
-
-        CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage,
-                PackageManager.MATCH_DIRECT_BOOT_AUTO,
-                UserHandle.getUserId(uid)).loadLabel(pm);
-
-        String titleString = context.getString(
-                com.android.internal.R.string.log_access_confirmation_title, appLabel);
-
-        return titleString;
-    }
-
-    private Spannable styleFont(String text) {
-        Spannable s = (Spannable) Html.fromHtml(text);
-        for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
-            TypefaceSpan typefaceSpan = new TypefaceSpan("google-sans");
-            s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
-        }
-        return s;
-    }
-
-    /**
-     * Returns the dialog view.
-     * If we cannot retrieve the package name, it returns null and we decline the full device log
-     * access
-     */
-    private View createView(@StyleRes int themeId) {
-        Context themedContext = new ContextThemeWrapper(this, themeId);
-        final View view = LayoutInflater.from(themedContext).inflate(
-                R.layout.log_access_user_consent_dialog_permission, null /*root*/);
-
-        if (view == null) {
-            throw new InflateException();
-        }
-
-        ((TextView) view.findViewById(R.id.log_access_dialog_title))
-            .setText(mAlertTitle);
-
-        if (!TextUtils.isEmpty(mAlertLearnMore)) {
-            Spannable mSpannableLearnMore = styleFont(mAlertLearnMore);
-
-            ((TextView) view.findViewById(R.id.log_access_dialog_body))
-                    .setText(TextUtils.concat(mAlertBody, "\n\n", mSpannableLearnMore));
-
-            ((TextView) view.findViewById(R.id.log_access_dialog_body))
-                    .setMovementMethod(LinkMovementMethod.getInstance());
-        } else {
-            ((TextView) view.findViewById(R.id.log_access_dialog_body))
-                    .setText(mAlertBody);
-        }
-
-        Button button_allow = (Button) view.findViewById(R.id.log_access_dialog_allow_button);
-        button_allow.setOnClickListener(this);
-
-        Button button_deny = (Button) view.findViewById(R.id.log_access_dialog_deny_button);
-        button_deny.setOnClickListener(this);
-
-        return view;
-
-    }
-
-    @Override
-    public void onClick(View view) {
-        try {
-            switch (view.getId()) {
-                case R.id.log_access_dialog_allow_button:
-                    mCallback.approveAccessForClient(mUid, mPackageName);
-                    finish();
-                    break;
-                case R.id.log_access_dialog_deny_button:
-                    declineLogAccess();
-                    finish();
-                    break;
-            }
-        } catch (RemoteException e) {
-            finish();
-        }
-    }
-
-    private void declineLogAccess() {
-        try {
-            mCallback.declineAccessForClient(mUid, mPackageName);
-        } catch (RemoteException e) {
-            finish();
-        }
-    }
-}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 822393f..a237e98 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -492,6 +492,12 @@
                 /* workProfileUserHandle= */ null);
     }
 
+    private UserHandle getIntentUser() {
+        return getIntent().hasExtra(EXTRA_CALLING_USER)
+                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER, android.os.UserHandle.class)
+                : getUser();
+    }
+
     private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
             Intent[] initialIntents,
             List<ResolveInfo> rList,
@@ -500,9 +506,7 @@
         // the intent resolver is started in the other profile. Since this is the only case when
         // this happens, we check for it here and set the current profile's tab.
         int selectedProfile = getCurrentProfile();
-        UserHandle intentUser = getIntent().hasExtra(EXTRA_CALLING_USER)
-                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER, android.os.UserHandle.class)
-                : getUser();
+        UserHandle intentUser = getIntentUser();
         if (!getUser().equals(intentUser)) {
             if (getPersonalProfileUserHandle().equals(intentUser)) {
                 selectedProfile = PROFILE_PERSONAL;
@@ -1098,6 +1102,9 @@
     @Override // ResolverListCommunicator
     public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
             boolean rebuildCompleted) {
+        if (isDestroyed()) {
+            return;
+        }
         if (isAutolaunching()) {
             return;
         }
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 4a1f7eb..42b46cd 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -647,15 +647,16 @@
 
         if (info instanceof DisplayResolveInfo) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
-            boolean hasLabel = dri.hasDisplayLabel();
-            holder.bindLabel(
-                    dri.getDisplayLabel(),
-                    dri.getExtendedInfo(),
-                    hasLabel && alwaysShowSubLabel());
-            holder.bindIcon(info);
-            if (!hasLabel) {
+            if (dri.hasDisplayLabel()) {
+                holder.bindLabel(
+                        dri.getDisplayLabel(),
+                        dri.getExtendedInfo(),
+                        alwaysShowSubLabel());
+            } else {
+                holder.bindLabel("", "", false);
                 loadLabel(dri);
             }
+            holder.bindIcon(info);
             if (!dri.hasDisplayIcon()) {
                 loadIcon(dri);
             }
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index 8f6bc43..a61a6d7 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -69,17 +69,17 @@
     protected Locale mDisplayLocale = null;
     // used to potentially cache a modified Context that uses mDisplayLocale
     protected Context mContextOverride = null;
-    private String mAppPackageName;
+    private boolean mHasSpecificAppPackageName;
 
     public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) {
-        this(localeOptions, countryMode, null);
+        this(localeOptions, countryMode, false);
     }
 
     public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode,
-            String appPackageName) {
+            boolean hasSpecificAppPackageName) {
         mCountryMode = countryMode;
         mLocaleOptions = new ArrayList<>(localeOptions.size());
-        mAppPackageName = appPackageName;
+        mHasSpecificAppPackageName = hasSpecificAppPackageName;
 
         for (LocaleStore.LocaleInfo li : localeOptions) {
             if (li.isSuggested()) {
@@ -136,7 +136,7 @@
 
     @Override
     public int getViewTypeCount() {
-        if (!TextUtils.isEmpty(mAppPackageName) && showHeaders()) {
+        if (mHasSpecificAppPackageName && showHeaders()) {
             // Two headers, 1 "System language", 1 current locale
             return APP_LANGUAGE_PICKER_TYPE_COUNT;
         } else if (showHeaders()) {
diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java
new file mode 100644
index 0000000..9a6d4c1
--- /dev/null
+++ b/core/java/com/android/internal/app/SystemLocaleCollector.java
@@ -0,0 +1,66 @@
+/*
+ * 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.app;
+
+import android.content.Context;
+import android.os.LocaleList;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** The Locale data collector for System language. */
+class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+    private final Context mContext;
+
+    SystemLocaleCollector(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
+        HashSet<String> ignoreList = new HashSet<>();
+        if (!translatedOnly) {
+            final LocaleList userLocales = LocalePicker.getLocales();
+            final String[] langTags = userLocales.toLanguageTags().split(",");
+            Collections.addAll(ignoreList, langTags);
+        }
+        return ignoreList;
+    }
+
+    @Override
+    public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
+            boolean translatedOnly, boolean isForCountryMode) {
+        Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
+        Set<LocaleStore.LocaleInfo> localeList;
+
+        if (isForCountryMode) {
+            localeList = LocaleStore.getLevelLocales(mContext,
+                    langTagsToIgnore, parent, translatedOnly);
+        } else {
+            localeList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
+                    null /* no parent */, translatedOnly);
+        }
+        return localeList;
+    }
+
+
+    @Override
+    public boolean hasSpecificPackageName() {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/procstats/AssociationState.java b/core/java/com/android/internal/app/procstats/AssociationState.java
index 97f4b0f..a21a842 100644
--- a/core/java/com/android/internal/app/procstats/AssociationState.java
+++ b/core/java/com/android/internal/app/procstats/AssociationState.java
@@ -59,6 +59,7 @@
     /**
      * The state of the source process of an association.
      */
+    @SuppressWarnings("ParcelableCreator")
     public static final class SourceState implements Parcelable {
         private @NonNull final ProcessStats mProcessStats;
         private @Nullable final AssociationState mAssociationState;
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index e748982..8e7207f 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -45,6 +45,7 @@
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId);
     int[] getAppWidgetIdsForHost(String callingPackage, int hostId);
+    void setAppWidgetHidden(in String callingPackage, int hostId);
     IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId,
             int intentFlags);
 
diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl
index f09e176..21c7baa 100644
--- a/core/java/com/android/internal/backup/IBackupTransport.aidl
+++ b/core/java/com/android/internal/backup/IBackupTransport.aidl
@@ -16,6 +16,7 @@
 
 package com.android.internal.backup;
 
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -400,4 +401,13 @@
      * <p>For supported flags see {@link android.app.backup.BackupAgent}.
      */
     void getTransportFlags(in AndroidFuture<int> resultFuture);
+
+    /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     */
+    void getBackupManagerMonitor(in AndroidFuture<IBackupManagerMonitor> resultFuture);
 }
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 44997b4..0f64f6d 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -556,6 +556,11 @@
             "show_stop_button_for_user_allowlisted_apps";
 
     /**
+     * (boolean) Whether to show notification volume control slider separate from ring.
+     */
+    public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
+
+    /**
      * (boolean) Whether the clipboard overlay is enabled.
      */
     public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled";
diff --git a/core/java/com/android/internal/expresslog/Counter.java b/core/java/com/android/internal/expresslog/Counter.java
new file mode 100644
index 0000000..7571073
--- /dev/null
+++ b/core/java/com/android/internal/expresslog/Counter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.expresslog;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.FrameworkStatsLog;
+
+/** Counter encapsulates StatsD write API calls */
+public final class Counter {
+
+    // Not instantiable.
+    private Counter() {}
+
+    /**
+     * Increments Telemetry Express Counter metric by 1
+     * @hide
+     */
+    public static void logIncrement(@NonNull String metricId) {
+        logIncrement(metricId, 1);
+    }
+
+    /**
+     * Increments Telemetry Express Counter metric by arbitrary value
+     * @hide
+     */
+    public static void logIncrement(@NonNull String metricId, long amount) {
+        final long metricIdHash = hashString(metricId);
+        FrameworkStatsLog.write(FrameworkStatsLog.EXPRESS_EVENT_REPORTED, metricIdHash, amount);
+    }
+
+    private static native long hashString(String stringToHash);
+}
diff --git a/core/java/com/android/internal/expresslog/OWNERS b/core/java/com/android/internal/expresslog/OWNERS
new file mode 100644
index 0000000..ee865b1
--- /dev/null
+++ b/core/java/com/android/internal/expresslog/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/stats/OWNERS
diff --git a/core/java/com/android/internal/infra/AbstractRemoteService.java b/core/java/com/android/internal/infra/AbstractRemoteService.java
index f725b37..d5f7ba5 100644
--- a/core/java/com/android/internal/infra/AbstractRemoteService.java
+++ b/core/java/com/android/internal/infra/AbstractRemoteService.java
@@ -20,10 +20,14 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ApplicationExitInfo;
+import android.app.IActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.ParceledListSlice;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
@@ -39,6 +43,7 @@
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base class representing a remote service.
@@ -66,6 +71,7 @@
 @Deprecated
 public abstract class AbstractRemoteService<S extends AbstractRemoteService<S, I>,
         I extends IInterface> implements DeathRecipient {
+    private static final int SERVICE_NOT_EXIST = -1;
     private static final int MSG_BIND = 1;
     private static final int MSG_UNBIND = 2;
 
@@ -96,6 +102,9 @@
 
     // Used just for debugging purposes (on dump)
     private long mNextUnbind;
+    // Used just for debugging purposes (on dump)
+    private int mServiceExitReason;
+    private int mServiceExitSubReason;
 
     /** Requests that have been scheduled, but that are not finished yet */
     private final ArrayList<BasePendingRequest<S, I>> mUnfinishedRequests = new ArrayList<>();
@@ -126,6 +135,8 @@
         mUserId = userId;
         mHandler = new Handler(handler.getLooper());
         mBindingFlags = bindingFlags;
+        mServiceExitReason = SERVICE_NOT_EXIST;
+        mServiceExitSubReason = SERVICE_NOT_EXIST;
     }
 
     /**
@@ -229,6 +240,7 @@
         if (mService != null) {
             mService.asBinder().unlinkToDeath(this, 0);
         }
+        updateServicelicationExitInfo(mComponentName, mUserId);
         mConnecting = true;
         mService = null;
         mServiceDied = true;
@@ -239,6 +251,33 @@
         handleBindFailure();
     }
 
+    private void updateServicelicationExitInfo(ComponentName componentName, int userId) {
+        IActivityManager am = ActivityManager.getService();
+        String packageName = componentName.getPackageName();
+        ParceledListSlice<ApplicationExitInfo> plistSlice = null;
+        try {
+            plistSlice = am.getHistoricalProcessExitReasons(packageName, 0, 1, userId);
+        } catch (RemoteException e) {
+            // do nothing. The local binder so it can not throw it.
+        }
+        if (plistSlice == null) {
+            return;
+        }
+        List<ApplicationExitInfo> list = plistSlice.getList();
+        if (list.isEmpty()) {
+            return;
+        }
+        ApplicationExitInfo info = list.get(0);
+        mServiceExitReason = info.getReason();
+        mServiceExitSubReason = info.getSubReason();
+        if (mVerbose) {
+            Slog.v(mTag, "updateServicelicationExitInfo: exitReason="
+                    + ApplicationExitInfo.reasonCodeToString(mServiceExitReason)
+                    + " exitSubReason= " + ApplicationExitInfo.subreasonToString(
+                    mServiceExitSubReason));
+        }
+    }
+
     // Note: we are dumping without a lock held so this is a bit racy but
     // adding a lock to a class that offloads to a handler thread would
     // mean adding a lock adding overhead to normal runtime operation.
@@ -272,6 +311,16 @@
             }
         }
         pw.println();
+        if (mServiceExitReason != SERVICE_NOT_EXIST) {
+            pw.append(prefix).append(tab).append("serviceExistReason=")
+                    .append(ApplicationExitInfo.reasonCodeToString(mServiceExitReason));
+            pw.println();
+        }
+        if (mServiceExitSubReason != SERVICE_NOT_EXIST) {
+            pw.append(prefix).append(tab).append("serviceExistSubReason=")
+                    .append(ApplicationExitInfo.subreasonToString(mServiceExitSubReason));
+            pw.println();
+        }
         pw.append(prefix).append("mBindingFlags=").println(mBindingFlags);
         pw.append(prefix).append("idleTimeout=")
             .append(Long.toString(idleTimeout / 1000)).append("s\n");
@@ -498,6 +547,8 @@
                 return;
             }
             mService = getServiceInterface(service);
+            mServiceExitReason = SERVICE_NOT_EXIST;
+            mServiceExitSubReason = SERVICE_NOT_EXIST;
             handleOnConnectedStateChangedInternal(true);
             mServiceDied = false;
         }
diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
index f260d7d..330ff8e 100644
--- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java
+++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
@@ -25,6 +25,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.RectF;
 import android.os.Bundle;
 import android.text.Editable;
 import android.text.Selection;
@@ -35,6 +36,7 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.DumpableInputConnection;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -44,9 +46,13 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
+import android.view.inputmethod.TextBoundsInfo;
+import android.view.inputmethod.TextBoundsInfoResult;
 import android.widget.TextView;
 
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 import java.util.function.IntConsumer;
 
 /**
@@ -231,7 +237,9 @@
                 | InputConnection.CURSOR_UPDATE_MONITOR;
         final int knownFilterFlags = InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS
                 | InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER
-                | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS;
+                | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS
+                | InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS
+                | InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE;
 
         // It is possible that any other bit is used as a valid flag in a future release.
         // We should reject the entire request in such a case.
@@ -260,6 +268,23 @@
     }
 
     @Override
+    public void requestTextBoundsInfo(
+            @NonNull RectF rectF, @Nullable @CallbackExecutor Executor executor,
+            @NonNull Consumer<TextBoundsInfoResult> consumer) {
+        final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(rectF);
+        final int resultCode;
+        if (textBoundsInfo != null) {
+            resultCode = TextBoundsInfoResult.CODE_SUCCESS;
+        } else {
+            resultCode = TextBoundsInfoResult.CODE_FAILED;
+        }
+        final TextBoundsInfoResult textBoundsInfoResult =
+                new TextBoundsInfoResult(resultCode, textBoundsInfo);
+
+        executor.execute(() -> consumer.accept(textBoundsInfoResult));
+    }
+
+    @Override
     public boolean setImeConsumesInput(boolean imeConsumesInput) {
         if (mTextView == null) {
             return super.setImeConsumesInput(imeConsumesInput);
@@ -275,8 +300,12 @@
         int result;
         if (gesture instanceof SelectGesture) {
             result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture);
+        } else if (gesture instanceof SelectRangeGesture) {
+            result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture);
         } else if (gesture instanceof DeleteGesture) {
             result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture);
+        } else if (gesture instanceof DeleteRangeGesture) {
+            result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture);
         } else if (gesture instanceof InsertGesture) {
             result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture);
         } else if (gesture instanceof RemoveSpaceGesture) {
diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
index c62fba9..1e3714e 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethod.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethod.aidl
@@ -21,6 +21,7 @@
 import android.view.InputChannel;
 import android.view.MotionEvent;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputMethodSubtype;
 import android.window.ImeOnBackInvokedDispatcher;
@@ -69,9 +70,11 @@
 
     void setSessionEnabled(IInputMethodSession session, boolean enabled);
 
-    void showSoftInput(in IBinder showInputToken, int flags, in ResultReceiver resultReceiver);
+    void showSoftInput(in IBinder showInputToken, in @nullable ImeTracker.Token statsToken,
+            int flags, in ResultReceiver resultReceiver);
 
-    void hideSoftInput(in IBinder hideInputToken, int flags, in ResultReceiver resultReceiver);
+    void hideSoftInput(in IBinder hideInputToken, in @nullable ImeTracker.Token statsToken,
+            int flags, in ResultReceiver resultReceiver);
 
     void updateEditorToolType(int toolType);
 
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java b/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java
deleted file mode 100644
index f0fe573..0000000
--- a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java
+++ /dev/null
@@ -1,158 +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.internal.inputmethod;
-
-import android.annotation.AnyThread;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.RequiresNoPermission;
-import android.annotation.RequiresPermission;
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-
-import com.android.internal.view.IInputMethodManager;
-
-import java.util.function.Consumer;
-
-/**
- * A global wrapper to directly invoke {@link IInputMethodManager} IPCs.
- *
- * <p>All public static methods are guaranteed to be thread-safe.</p>
- *
- * <p>All public methods are guaranteed to do nothing when {@link IInputMethodManager} is
- * unavailable.</p>
- */
-public final class IInputMethodManagerGlobal {
-    @Nullable
-    private static volatile IInputMethodManager sServiceCache = null;
-
-    /**
-     * @return {@code true} if {@link IInputMethodManager} is available.
-     */
-    @AnyThread
-    public static boolean isAvailable() {
-        return getService() != null;
-    }
-
-    @AnyThread
-    @Nullable
-    private static IInputMethodManager getService() {
-        IInputMethodManager service = sServiceCache;
-        if (service == null) {
-            service = IInputMethodManager.Stub.asInterface(
-                    ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
-            if (service == null) {
-                return null;
-            }
-            sServiceCache = service;
-        }
-        return service;
-    }
-
-    @AnyThread
-    private static void handleRemoteExceptionOrRethrow(@NonNull RemoteException e,
-            @Nullable Consumer<RemoteException> exceptionHandler) {
-        if (exceptionHandler != null) {
-            exceptionHandler.accept(e);
-        } else {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * Invokes {@link IInputMethodManager#startProtoDump(byte[], int, String)}.
-     *
-     * @param protoDump client or service side information to be stored by the server
-     * @param source where the information is coming from, refer to
-     *               {@link ImeTracing#IME_TRACING_FROM_CLIENT} and
-     *               {@link ImeTracing#IME_TRACING_FROM_IMS}
-     * @param where where the information is coming from.
-     * @param exceptionHandler an optional {@link RemoteException} handler.
-     */
-    @RequiresNoPermission
-    @AnyThread
-    public static void startProtoDump(byte[] protoDump, int source, String where,
-            @Nullable Consumer<RemoteException> exceptionHandler) {
-        final IInputMethodManager service = getService();
-        if (service == null) {
-            return;
-        }
-        try {
-            service.startProtoDump(protoDump, source, where);
-        } catch (RemoteException e) {
-            handleRemoteExceptionOrRethrow(e, exceptionHandler);
-        }
-    }
-
-    /**
-     * Invokes {@link IInputMethodManager#startImeTrace()}.
-     *
-     * @param exceptionHandler an optional {@link RemoteException} handler.
-     */
-    @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
-    @AnyThread
-    public static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
-        final IInputMethodManager service = getService();
-        if (service == null) {
-            return;
-        }
-        try {
-            service.startImeTrace();
-        } catch (RemoteException e) {
-            handleRemoteExceptionOrRethrow(e, exceptionHandler);
-        }
-    }
-
-    /**
-     * Invokes {@link IInputMethodManager#stopImeTrace()}.
-     *
-     * @param exceptionHandler an optional {@link RemoteException} handler.
-     */
-    @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
-    @AnyThread
-    public static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
-        final IInputMethodManager service = getService();
-        if (service == null) {
-            return;
-        }
-        try {
-            service.stopImeTrace();
-        } catch (RemoteException e) {
-            handleRemoteExceptionOrRethrow(e, exceptionHandler);
-        }
-    }
-
-    /**
-     * Invokes {@link IInputMethodManager#isImeTraceEnabled()}.
-     *
-     * @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}.
-     */
-    @RequiresNoPermission
-    @AnyThread
-    public static boolean isImeTraceEnabled() {
-        final IInputMethodManager service = getService();
-        if (service == null) {
-            return false;
-        }
-        try {
-            return service.isImeTraceEnabled();
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-}
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 4babb70..f77e962 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.inputmethod;
 
 import android.net.Uri;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.infra.AndroidFuture;
@@ -41,7 +42,8 @@
     void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
     void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
     void notifyUserActionAsync();
-    void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible);
+    void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+            in @nullable ImeTracker.Token statsToken);
     void onStylusHandwritingReady(int requestId, int pid);
     void resetStylusHandwriting(int requestId);
 }
diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
index ea5c9a3..65016c2 100644
--- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
+++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
@@ -16,20 +16,16 @@
 
 package com.android.internal.inputmethod;
 
+import android.graphics.RectF;
 import android.os.Bundle;
+import android.os.ICancellationSignal;
 import android.os.ResultReceiver;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
-import android.view.inputmethod.DeleteGesture;
-import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputContentInfo;
-import android.view.inputmethod.InsertGesture;
-import android.view.inputmethod.JoinOrSplitGesture;
-import android.view.inputmethod.RemoveSpaceGesture;
-import android.view.inputmethod.SelectGesture;
-import android.view.inputmethod.SelectRangeGesture;
+import android.view.inputmethod.ParcelableHandwritingGesture;
 import android.view.inputmethod.TextAttribute;
 
 import com.android.internal.infra.AndroidFuture;
@@ -94,26 +90,11 @@
     void performPrivateCommand(in InputConnectionCommandHeader header, String action,
             in Bundle data);
 
-    void performHandwritingSelectGesture(in InputConnectionCommandHeader header,
-            in SelectGesture gesture, in ResultReceiver resultReceiver);
+    void performHandwritingGesture(in InputConnectionCommandHeader header,
+            in ParcelableHandwritingGesture gesture, in ResultReceiver resultReceiver);
 
-    void performHandwritingSelectRangeGesture(in InputConnectionCommandHeader header,
-            in SelectRangeGesture gesture, in ResultReceiver resultReceiver);
-
-    void performHandwritingInsertGesture(in InputConnectionCommandHeader header,
-            in InsertGesture gesture, in ResultReceiver resultReceiver);
-
-    void performHandwritingDeleteGesture(in InputConnectionCommandHeader header,
-            in DeleteGesture gesture, in ResultReceiver resultReceiver);
-
-    void performHandwritingDeleteRangeGesture(in InputConnectionCommandHeader header,
-                in DeleteRangeGesture gesture, in ResultReceiver resultReceiver);
-
-    void performHandwritingRemoveSpaceGesture(in InputConnectionCommandHeader header,
-            in RemoveSpaceGesture gesture, in ResultReceiver resultReceiver);
-
-    void performHandwritingJoinOrSplitGesture(in InputConnectionCommandHeader header,
-            in JoinOrSplitGesture gesture, in ResultReceiver resultReceiver);
+    void previewHandwritingGesture(in InputConnectionCommandHeader header,
+            in ParcelableHandwritingGesture gesture, in ICancellationSignal transport);
 
     void setComposingRegion(in InputConnectionCommandHeader header, int start, int end);
 
@@ -130,6 +111,9 @@
                 int cursorUpdateMode, int cursorUpdateFilter, int imeDisplayId,
                  in AndroidFuture future /* T=Boolean */);
 
+    void requestTextBoundsInfo(in InputConnectionCommandHeader header, in RectF rect,
+           in ResultReceiver resultReceiver /* T=TextBoundsInfoResult */);
+
     void commitContent(in InputConnectionCommandHeader header, in InputContentInfo inputContentInfo,
             int flags, in Bundle opts, in AndroidFuture future /* T=Boolean */);
 
diff --git a/core/java/com/android/internal/inputmethod/ImeTracing.java b/core/java/com/android/internal/inputmethod/ImeTracing.java
index a4328cc..e6a9b54 100644
--- a/core/java/com/android/internal/inputmethod/ImeTracing.java
+++ b/core/java/com/android/internal/inputmethod/ImeTracing.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodManagerGlobal;
 
 import java.io.PrintWriter;
 
@@ -44,7 +45,7 @@
     private static ImeTracing sInstance;
     static boolean sEnabled = false;
 
-    private final boolean mIsAvailable = IInputMethodManagerGlobal.isAvailable();
+    private final boolean mIsAvailable = InputMethodManagerGlobal.isImeTraceAvailable();
 
     protected boolean mDumpInProgress;
     protected final Object mDumpInProgressLock = new Object();
@@ -81,7 +82,7 @@
      * @param where
      */
     public void sendToService(byte[] protoDump, int source, String where) {
-        IInputMethodManagerGlobal.startProtoDump(protoDump, source, where,
+        InputMethodManagerGlobal.startProtoDump(protoDump, source, where,
                 e -> Log.e(TAG, "Exception while sending ime-related dump to server", e));
     }
 
@@ -90,7 +91,7 @@
      */
     @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
     public final void startImeTrace() {
-        IInputMethodManagerGlobal.startImeTrace(e -> Log.e(TAG, "Could not start ime trace.", e));
+        InputMethodManagerGlobal.startImeTrace(e -> Log.e(TAG, "Could not start ime trace.", e));
     }
 
     /**
@@ -98,7 +99,7 @@
      */
     @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
     public final void stopImeTrace() {
-        IInputMethodManagerGlobal.stopImeTrace(e -> Log.e(TAG, "Could not stop ime trace.", e));
+        InputMethodManagerGlobal.stopImeTrace(e -> Log.e(TAG, "Could not stop ime trace.", e));
     }
 
     /**
diff --git a/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java b/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java
index 4caca84..95ed4ed 100644
--- a/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java
+++ b/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.util.proto.ProtoOutputStream;
 import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodManagerGlobal;
 
 import java.io.PrintWriter;
 
@@ -28,7 +29,7 @@
  */
 class ImeTracingClientImpl extends ImeTracing {
     ImeTracingClientImpl() {
-        sEnabled = IInputMethodManagerGlobal.isImeTraceEnabled();
+        sEnabled = InputMethodManagerGlobal.isImeTraceEnabled();
     }
 
     @Override
diff --git a/core/java/com/android/internal/inputmethod/ImeTracingServerImpl.java b/core/java/com/android/internal/inputmethod/ImeTracingServerImpl.java
index 2a242a5..edd74f6 100644
--- a/core/java/com/android/internal/inputmethod/ImeTracingServerImpl.java
+++ b/core/java/com/android/internal/inputmethod/ImeTracingServerImpl.java
@@ -138,7 +138,7 @@
     private void writeTracesToFilesLocked() {
         try {
             long timeOffsetNs =
-                    TimeUnit.NANOSECONDS.convert(System.currentTimeMillis(), TimeUnit.NANOSECONDS)
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
                     - SystemClock.elapsedRealtimeNanos();
 
             ProtoOutputStream clientsProto = new ProtoOutputStream();
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 09c97b3..1b4afd6 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -49,6 +49,8 @@
                 return "WINDOW_FOCUS_GAIN";
             case StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY:
                 return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case StartInputReason.SCHEDULED_CHECK_FOCUS:
+                return "SCHEDULED_CHECK_FOCUS";
             case StartInputReason.APP_CALLED_RESTART_INPUT_API:
                 return "APP_CALLED_RESTART_INPUT_API";
             case StartInputReason.CHECK_FOCUS:
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 67c2103..66e3333 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -25,6 +25,7 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.View;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.GuardedBy;
@@ -100,7 +101,7 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int}.
+     * Calls {@link IInputMethodPrivilegedOperations#setImeWindowStatusAsync(int, int)}.
      *
      * @param vis visibility flags
      * @param backDisposition disposition flags
@@ -250,7 +251,7 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, IVoidResultCallback)}
+     * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
      *
      * @param flags additional operating flags
      * @param reason the reason to hide soft input
@@ -316,7 +317,7 @@
 
     /**
      * Calls {@link IInputMethodPrivilegedOperations#switchToNextInputMethod(boolean,
-     * IBooleanResultCallback)}
+     * AndroidFuture)}
      *
      * @param onlyCurrentIme {@code true} to switch to a {@link InputMethodSubtype} within the same
      *                       IME
@@ -375,22 +376,25 @@
     }
 
     /**
-     * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean)}.
+     * Calls {@link IInputMethodPrivilegedOperations#applyImeVisibilityAsync(IBinder, boolean,
+     * ImeTracker.Token)}.
      *
      * @param showOrHideInputToken placeholder token that maps to window requesting
      *        {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} or
-     *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow
-     *        (IBinder, int)}
+     *        {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow(IBinder,
+     *        int)}
      * @param setVisible {@code true} to set IME visible, else hidden.
+     * @param statsToken the token tracking the current IME request or {@code null} otherwise.
      */
     @AnyThread
-    public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible) {
+    public void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
+            @Nullable ImeTracker.Token statsToken) {
         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
         if (ops == null) {
             return;
         }
         try {
-            ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible);
+            ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl
new file mode 100644
index 0000000..18bd6e5
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.inputmethod;
+
+parcelable InputMethodSubtypeHandle;
diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
new file mode 100644
index 0000000..780c637
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeHandle.java
@@ -0,0 +1,254 @@
+/*
+ * 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.inputmethod;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils.SimpleStringSplitter;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.security.InvalidParameterException;
+import java.util.Objects;
+
+/**
+ * A stable and serializable identifier for the pair of {@link InputMethodInfo#getId()} and
+ * {@link android.view.inputmethod.InputMethodSubtype}.
+ *
+ * <p>To save {@link InputMethodSubtypeHandle} to storage, call {@link #toStringHandle()} to get a
+ * {@link String} handle and just save it.  Once you load a {@link String} handle, you can obtain a
+ * {@link InputMethodSubtypeHandle} instance from {@link #of(String)}.</p>
+ *
+ * <p>For better readability, consider specifying {@link RawHandle} annotation to {@link String}
+ * object when it is a raw {@link String} handle.</p>
+ */
+public final class InputMethodSubtypeHandle implements Parcelable {
+    private static final String SUBTYPE_TAG = "subtype";
+    private static final char DATA_SEPARATOR = ':';
+
+    /**
+     * Can be used to annotate {@link String} object if it is raw handle format.
+     */
+    @Retention(SOURCE)
+    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.LOCAL_VARIABLE,
+            ElementType.PARAMETER})
+    public @interface RawHandle {
+    }
+
+    /**
+     * The main content of this {@link InputMethodSubtypeHandle}.  Is designed to be safe to be
+     * saved into storage.
+     */
+    @RawHandle
+    private final String mHandle;
+
+    /**
+     * Encode {@link InputMethodInfo} and {@link InputMethodSubtype#hashCode()} into
+     * {@link RawHandle}.
+     *
+     * @param imeId {@link InputMethodInfo#getId()} to be used.
+     * @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be used.
+     * @return The encoded {@link RawHandle} string.
+     */
+    @AnyThread
+    @RawHandle
+    @NonNull
+    private static String encodeHandle(@NonNull String imeId, int subtypeHashCode) {
+        return imeId + DATA_SEPARATOR + SUBTYPE_TAG + DATA_SEPARATOR + subtypeHashCode;
+    }
+
+    private InputMethodSubtypeHandle(@NonNull String handle) {
+        mHandle = handle;
+    }
+
+    /**
+     * Creates {@link InputMethodSubtypeHandle} from {@link InputMethodInfo} and
+     * {@link InputMethodSubtype}.
+     *
+     * @param imi {@link InputMethodInfo} to be used.
+     * @param subtype {@link InputMethodSubtype} to be used.
+     * @return A {@link InputMethodSubtypeHandle} object.
+     */
+    @AnyThread
+    @NonNull
+    public static InputMethodSubtypeHandle of(
+            @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+        final int subtypeHashCode =
+                subtype != null ? subtype.hashCode() : InputMethodSubtype.SUBTYPE_ID_NONE;
+        return new InputMethodSubtypeHandle(encodeHandle(imi.getId(), subtypeHashCode));
+    }
+
+    /**
+     * Creates {@link InputMethodSubtypeHandle} from a {@link RawHandle} {@link String}, which can
+     * be obtained by {@link #toStringHandle()}.
+     *
+     * @param stringHandle {@link RawHandle} {@link String} to be parsed.
+     * @return A {@link InputMethodSubtypeHandle} object.
+     * @throws NullPointerException when {@code stringHandle} is {@code null}
+     * @throws InvalidParameterException when {@code stringHandle} is not a valid {@link RawHandle}.
+     */
+    @AnyThread
+    @NonNull
+    public static InputMethodSubtypeHandle of(@RawHandle @NonNull String stringHandle) {
+        final SimpleStringSplitter splitter = new SimpleStringSplitter(DATA_SEPARATOR);
+        splitter.setString(Objects.requireNonNull(stringHandle));
+        if (!splitter.hasNext()) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        final String imeId = splitter.next();
+        final ComponentName componentName = ComponentName.unflattenFromString(imeId);
+        if (componentName == null) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        // TODO: Consolidate IME ID validation logic into one place.
+        if (!Objects.equals(componentName.flattenToShortString(), imeId)) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        if (!splitter.hasNext()) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        final String source = splitter.next();
+        if (!Objects.equals(source, SUBTYPE_TAG)) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        if (!splitter.hasNext()) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        final String hashCodeStr = splitter.next();
+        if (splitter.hasNext()) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+        final int subtypeHashCode;
+        try {
+            subtypeHashCode = Integer.parseInt(hashCodeStr);
+        } catch (NumberFormatException ignore) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+
+        // Redundant expressions (e.g. "0001" instead of "1") are not allowed.
+        if (!Objects.equals(encodeHandle(imeId, subtypeHashCode), stringHandle)) {
+            throw new InvalidParameterException("Invalid handle=" + stringHandle);
+        }
+
+        return new InputMethodSubtypeHandle(stringHandle);
+    }
+
+    /**
+     * @return {@link ComponentName} of the input method.
+     * @see InputMethodInfo#getComponent()
+     */
+    @AnyThread
+    @NonNull
+    public ComponentName getComponentName() {
+        return ComponentName.unflattenFromString(getImeId());
+    }
+
+    /**
+     * @return IME ID.
+     * @see InputMethodInfo#getId()
+     */
+    @AnyThread
+    @NonNull
+    public String getImeId() {
+        return mHandle.substring(0, mHandle.indexOf(DATA_SEPARATOR));
+    }
+
+    /**
+     * @return {@link RawHandle} {@link String} data that should be stable and persistable.
+     * @see #of(String)
+     */
+    @RawHandle
+    @AnyThread
+    @NonNull
+    public String toStringHandle() {
+        return mHandle;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @AnyThread
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof InputMethodSubtypeHandle)) {
+            return false;
+        }
+        final InputMethodSubtypeHandle that = (InputMethodSubtypeHandle) obj;
+        return Objects.equals(mHandle, that.mHandle);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @AnyThread
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mHandle);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @AnyThread
+    @NonNull
+    @Override
+    public String toString() {
+        return "InputMethodSubtypeHandle{mHandle=" + mHandle + "}";
+    }
+
+    /**
+     * {@link Creator} for parcelable.
+     */
+    public static final Creator<InputMethodSubtypeHandle> CREATOR = new Creator<>() {
+        @Override
+        public InputMethodSubtypeHandle createFromParcel(Parcel in) {
+            return of(in.readString8());
+        }
+
+        @Override
+        public InputMethodSubtypeHandle[] newArray(int size) {
+            return new InputMethodSubtypeHandle[size];
+        }
+    };
+
+    /**
+     * {@inheritDoc}
+     */
+    @AnyThread
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @AnyThread
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(toStringHandle());
+    }
+}
diff --git a/core/java/com/android/internal/inputmethod/StartInputReason.java b/core/java/com/android/internal/inputmethod/StartInputReason.java
index 51ed841..733d975 100644
--- a/core/java/com/android/internal/inputmethod/StartInputReason.java
+++ b/core/java/com/android/internal/inputmethod/StartInputReason.java
@@ -31,6 +31,7 @@
         StartInputReason.UNSPECIFIED,
         StartInputReason.WINDOW_FOCUS_GAIN,
         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY,
+        StartInputReason.SCHEDULED_CHECK_FOCUS,
         StartInputReason.APP_CALLED_RESTART_INPUT_API,
         StartInputReason.CHECK_FOCUS,
         StartInputReason.BOUND_TO_IMMS,
@@ -58,6 +59,11 @@
      */
     int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
     /**
+     * Similar to {@link #CHECK_FOCUS}, but the one scheduled with
+     * {@link android.view.ViewRootImpl#dispatchCheckFocus()}.
+     */
+    int SCHEDULED_CHECK_FOCUS = 3;
+    /**
      * {@link android.view.inputmethod.InputMethodManager#restartInput(android.view.View)} is
      * either explicitly called by the application or indirectly called by some Framework class
      * (e.g. {@link android.widget.EditText}).
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 76f33a6..614f962 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -27,6 +27,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_ICON;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_RECENTS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_WIDGET;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
@@ -45,6 +46,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL;
@@ -89,7 +91,6 @@
 import android.annotation.NonNull;
 import android.annotation.UiThread;
 import android.annotation.WorkerThread;
-import android.app.ActivityThread;
 import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
@@ -224,6 +225,8 @@
     public static final int CUJ_SHADE_CLEAR_ALL = 62;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63;
     public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
+    public static final int CUJ_RECENTS_SCROLLING = 65;
+    public static final int CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS = 66;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -297,6 +300,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS,
     };
 
     private static class InstanceHolder {
@@ -385,7 +390,9 @@
             CUJ_TASKBAR_COLLAPSE,
             CUJ_SHADE_CLEAR_ALL,
             CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
-            CUJ_LOCKSCREEN_OCCLUSION
+            CUJ_LOCKSCREEN_OCCLUSION,
+            CUJ_RECENTS_SCROLLING,
+            CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -409,7 +416,6 @@
     public InteractionJankMonitor(@NonNull HandlerThread worker) {
         // Check permission early.
         DeviceConfig.enforceReadPermission(
-            ActivityThread.currentApplication().getApplicationContext(),
             DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR);
 
         mRunningTrackers = new SparseArray<>();
@@ -900,6 +906,10 @@
                 return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION";
             case CUJ_LOCKSCREEN_OCCLUSION:
                 return "LOCKSCREEN_OCCLUSION";
+            case CUJ_RECENTS_SCROLLING:
+                return "RECENTS_SCROLLING";
+            case CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS:
+                return "LAUNCHER_APP_SWIPE_TO_RECENTS";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 681b46a..0489dc81 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -35,7 +35,10 @@
 
 // Manages the NotificationChannels used by the frameworks itself.
 public class SystemNotificationChannels {
-    public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
+    /**
+     * @deprecated Legacy system channel, which is no longer used,
+     */
+    @Deprecated public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
     public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
     public static String SECURITY = "SECURITY";
     public static String CAR_MODE = "CAR_MODE";
@@ -72,13 +75,6 @@
     public static void createAll(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
         List<NotificationChannel> channelsList = new ArrayList<NotificationChannel>();
-        final NotificationChannel keyboard = new NotificationChannel(
-                VIRTUAL_KEYBOARD,
-                context.getString(R.string.notification_channel_virtual_keyboard),
-                NotificationManager.IMPORTANCE_LOW);
-        keyboard.setBlockable(true);
-        channelsList.add(keyboard);
-
         final NotificationChannel physicalKeyboardChannel = new NotificationChannel(
                 PHYSICAL_KEYBOARD,
                 context.getString(R.string.notification_channel_physical_keyboard),
@@ -237,6 +233,7 @@
     /** Remove notification channels which are no longer used */
     public static void removeDeprecated(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
+        nm.deleteNotificationChannel(VIRTUAL_KEYBOARD);
         nm.deleteNotificationChannel(DEVICE_ADMIN_DEPRECATED);
         nm.deleteNotificationChannel(SYSTEM_CHANGES_DEPRECATED);
     }
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 696f0ff..556e146 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -403,7 +403,7 @@
      * Returns true if this instance only supports reading history.
      */
     public boolean isReadOnly() {
-        return mActiveFile == null;
+        return mActiveFile == null || mHistoryDir == null;
     }
 
     /**
@@ -1292,7 +1292,9 @@
                 && mHistoryLastWritten.batteryHealth == cur.batteryHealth
                 && mHistoryLastWritten.batteryPlugType == cur.batteryPlugType
                 && mHistoryLastWritten.batteryTemperature == cur.batteryTemperature
-                && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage) {
+                && mHistoryLastWritten.batteryVoltage == cur.batteryVoltage
+                && mHistoryLastWritten.measuredEnergyDetails == null
+                && mHistoryLastWritten.cpuUsageDetails == null) {
             // We can merge this new change in with the last one.  Merging is
             // allowed as long as only the states have changed, and within those states
             // as long as no bit has changed both between now and the last entry, as
@@ -1761,8 +1763,8 @@
      * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} .
      */
     public void writeHistory() {
-        if (mActiveFile == null) {
-            Slog.w(TAG, "writeHistory: no history file associated with this instance");
+        if (isReadOnly()) {
+            Slog.w(TAG, "writeHistory: this instance instance is read-only");
             return;
         }
 
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index 0a29fc52..eb62cb0 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -735,7 +735,7 @@
     }
 
     protected boolean shouldRecordDetailedData() {
-        return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
+        return mRandom.nextInt(mPeriodicSamplingInterval) == 0;
     }
 
     /**
diff --git a/core/java/com/android/internal/os/BinderLatencyObserver.java b/core/java/com/android/internal/os/BinderLatencyObserver.java
index e9d55db..1276fb9 100644
--- a/core/java/com/android/internal/os/BinderLatencyObserver.java
+++ b/core/java/com/android/internal/os/BinderLatencyObserver.java
@@ -236,7 +236,7 @@
     }
 
     protected boolean shouldKeepSample() {
-        return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
+        return mRandom.nextInt(mPeriodicSamplingInterval) == 0;
     }
 
     /** Updates the sampling interval. */
diff --git a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
index 9be686a..a1ad5d5 100644
--- a/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
+++ b/core/java/com/android/internal/os/IBinaryTransparencyService.aidl
@@ -26,5 +26,7 @@
 interface IBinaryTransparencyService {
     String getSignedImageInfo();
 
-    Map getApexInfo();
+    List getApexInfo();
+
+    List getMeasurementsForAllPackages();
 }
\ No newline at end of file
diff --git a/core/java/com/android/internal/os/LooperStats.java b/core/java/com/android/internal/os/LooperStats.java
index 2805dcc..0645eb7 100644
--- a/core/java/com/android/internal/os/LooperStats.java
+++ b/core/java/com/android/internal/os/LooperStats.java
@@ -290,7 +290,7 @@
     }
 
     protected boolean shouldCollectDetailedData() {
-        return ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
+        return ThreadLocalRandom.current().nextInt(mSamplingInterval) == 0;
     }
 
     private static class DispatchSession {
diff --git a/core/java/com/android/internal/os/ProcLocksReader.java b/core/java/com/android/internal/os/ProcLocksReader.java
index 2143bc1..9ddb8c7 100644
--- a/core/java/com/android/internal/os/ProcLocksReader.java
+++ b/core/java/com/android/internal/os/ProcLocksReader.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.os;
 
+import android.util.IntArray;
+
 import com.android.internal.util.ProcFileReader;
 
 import java.io.FileInputStream;
@@ -35,6 +37,7 @@
 public class ProcLocksReader {
     private final String mPath;
     private ProcFileReader mReader = null;
+    private IntArray mPids = new IntArray();
 
     public ProcLocksReader() {
         mPath = "/proc/locks";
@@ -51,9 +54,13 @@
     public interface ProcLocksReaderCallback {
         /**
          * Call the callback function of handleBlockingFileLocks().
-         * @param pid Each process that hold file locks blocking other processes.
+         * @param pids Each process that hold file locks blocking other processes.
+         *             pids[0] is the process blocking others
+         *             pids[1..n-1] are the processes being blocked
+         * NOTE: pids are cleared immediately after onBlockingFileLock() returns. If the caller
+         * needs to cache it, please make a copy, e.g. by calling pids.toArray().
          */
-        void onBlockingFileLock(int pid);
+        void onBlockingFileLock(IntArray pids);
     }
 
     /**
@@ -64,8 +71,7 @@
     public void handleBlockingFileLocks(ProcLocksReaderCallback callback) throws IOException {
         long last = -1;
         long id; // ordinal position of the lock in the list
-        int owner = -1; // the PID of the process that owns the lock
-        int pid = -1; // the PID of the process blocking others
+        int pid = -1; // the PID of the process being blocked
 
         if (mReader == null) {
             mReader = new ProcFileReader(new FileInputStream(mPath));
@@ -73,26 +79,49 @@
             mReader.rewind();
         }
 
+        mPids.clear();
         while (mReader.hasMoreData()) {
             id = mReader.nextLong(true); // lock id
             if (id == last) {
-                mReader.finishLine(); // blocked lock
-                if (pid < 0) {
-                    pid = owner; // get pid from the previous line
-                    callback.onBlockingFileLock(pid);
+                // blocked lock found
+                mReader.nextIgnored(); // ->
+                mReader.nextIgnored(); // lock type: POSIX?
+                mReader.nextIgnored(); // lock type: MANDATORY?
+                mReader.nextIgnored(); // lock type: RW?
+
+                pid = mReader.nextInt(); // pid
+                if (pid > 0) {
+                    mPids.add(pid);
                 }
-                continue;
+
+                mReader.finishLine();
             } else {
-                pid = -1; // a new lock
+                // process blocking lock and move on to a new lock
+                if (mPids.size() > 1) {
+                    callback.onBlockingFileLock(mPids);
+                    mPids.clear();
+                }
+
+                // new lock found
+                mReader.nextIgnored(); // lock type: POSIX?
+                mReader.nextIgnored(); // lock type: MANDATORY?
+                mReader.nextIgnored(); // lock type: RW?
+
+                pid = mReader.nextInt(); // pid
+                if (pid > 0) {
+                    if (mPids.size() == 0) {
+                        mPids.add(pid);
+                    } else {
+                        mPids.set(0, pid);
+                    }
+                }
+                mReader.finishLine();
+                last = id;
             }
-
-            mReader.nextIgnored(); // lock type: POSIX?
-            mReader.nextIgnored(); // lock type: MANDATORY?
-            mReader.nextIgnored(); // lock type: RW?
-
-            owner = mReader.nextInt(); // pid
-            mReader.finishLine();
-            last = id;
+        }
+        // The last unprocessed blocking lock immediately before EOF
+        if (mPids.size() > 1) {
+            callback.onBlockingFileLock(mPids);
         }
     }
 }
diff --git a/core/java/com/android/internal/os/RoSystemProperties.java b/core/java/com/android/internal/os/RoSystemProperties.java
index 98d81c9..6870d09 100644
--- a/core/java/com/android/internal/os/RoSystemProperties.java
+++ b/core/java/com/android/internal/os/RoSystemProperties.java
@@ -51,6 +51,15 @@
     // ------ ro.fw.* ------------ //
     public static final boolean FW_SYSTEM_USER_SPLIT =
             SystemProperties.getBoolean("ro.fw.system_user_split", false);
+    /**
+     * Indicates whether the device should run in headless system user mode,
+     *   in which user 0 only runs the system, not a real user.
+     * <p>WARNING about changing this value during an non-wiping update (OTA):
+     *   <li>If this value is modified via an update, the change will have no effect, since an
+     *       already-existing system user cannot change its mode.
+     *   <li>Changing this value during an OTA from a pre-R device is not permitted; attempting to
+     *       do so will corrupt the system user.
+     */
     public static final boolean MULTIUSER_HEADLESS_SYSTEM_USER =
             SystemProperties.getBoolean("ro.fw.mu.headless_system_user", false);
 
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index 28b98d6..8a9445d 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -16,10 +16,14 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;
+
+import android.annotation.TestApi;
 import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.ApplicationErrorReport;
 import android.app.IActivityManager;
+import android.app.compat.CompatChanges;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.type.DefaultMimeMapFactory;
 import android.net.TrafficStats;
@@ -36,6 +40,7 @@
 
 import dalvik.system.RuntimeHooks;
 import dalvik.system.VMRuntime;
+import dalvik.system.ZipPathValidator;
 
 import libcore.content.type.MimeMap;
 
@@ -260,10 +265,31 @@
          */
         TrafficStats.attachSocketTagger();
 
+        /*
+         * Initialize the zip path validator callback depending on the targetSdk.
+         */
+        initZipPathValidatorCallback();
+
         initialized = true;
     }
 
     /**
+     * If targetSDK >= U: set the safe zip path validator callback which disallows dangerous zip
+     * entry names.
+     * Otherwise: clear the callback to the default validation.
+     *
+     * @hide
+     */
+    @TestApi
+    public static void initZipPathValidatorCallback() {
+        if (CompatChanges.isChangeEnabled(VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL)) {
+            ZipPathValidator.setCallback(new SafeZipPathValidatorCallback());
+        } else {
+            ZipPathValidator.clearCallback();
+        }
+    }
+
+    /**
      * Returns an HTTP user agent of the form
      * "Dalvik/1.1.0 (Linux; U; Android Eclair Build/MAIN)".
      */
diff --git a/core/java/com/android/internal/os/SafeZipPathValidatorCallback.java b/core/java/com/android/internal/os/SafeZipPathValidatorCallback.java
new file mode 100644
index 0000000..a6ee108
--- /dev/null
+++ b/core/java/com/android/internal/os/SafeZipPathValidatorCallback.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.internal.os;
+
+import android.annotation.NonNull;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.os.Build;
+
+import dalvik.system.ZipPathValidator;
+
+import java.io.File;
+import java.util.zip.ZipException;
+
+/**
+ * A child implementation of the {@link dalvik.system.ZipPathValidator.Callback} that removes the
+ * risk of zip path traversal vulnerabilities.
+ *
+ * @hide
+ */
+public class SafeZipPathValidatorCallback implements ZipPathValidator.Callback {
+    /**
+     * This change targets zip path traversal vulnerabilities by throwing
+     * {@link java.util.zip.ZipException} if zip path entries contain ".." or start with "/".
+     * <p>
+     * The exception will be thrown in {@link java.util.zip.ZipInputStream#getNextEntry} or
+     * {@link java.util.zip.ZipFile#ZipFile(String)}.
+     * <p>
+     * This validation is enabled for apps with targetSDK >= U.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL = 242716250L;
+
+    @Override
+    public void onZipEntryAccess(@NonNull String path) throws ZipException {
+        if (path.startsWith("/")) {
+            throw new ZipException("Invalid zip entry path: " + path);
+        }
+        if (path.contains("..")) {
+            // If the string does contain "..", break it down into its actual name elements to
+            // ensure it actually contains ".." as a name, not just a name like "foo..bar" or even
+            // "foo..", which should be fine.
+            File file = new File(path);
+            while (file != null) {
+                if (file.getName().equals("..")) {
+                    throw new ZipException("Invalid zip entry path: " + path);
+                }
+                file = file.getParentFile();
+            }
+        }
+    }
+}
diff --git a/core/java/com/android/internal/os/SystemServerClassLoaderFactory.java b/core/java/com/android/internal/os/SystemServerClassLoaderFactory.java
index a03bac4..90ad34d 100644
--- a/core/java/com/android/internal/os/SystemServerClassLoaderFactory.java
+++ b/core/java/com/android/internal/os/SystemServerClassLoaderFactory.java
@@ -87,6 +87,10 @@
         if (isTestOnly) {
             return true;
         }
+        // If system server is being profiled, it's OK to create class loaders anytime.
+        if (ZygoteInit.shouldProfileSystemServer()) {
+            return true;
+        }
         return false;
     }
 
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index be3f172..680f8fe 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.content.Intent;
 import android.os.SystemClock;
 
 import com.android.internal.os.anr.AnrLatencyTracker;
@@ -92,7 +93,17 @@
 
     /** Record for a broadcast receiver timeout. */
     @NonNull
-    public static TimeoutRecord forBroadcastReceiver(@NonNull String reason) {
+    public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent) {
+        String reason = "Broadcast of " + intent.toString();
+        return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason);
+    }
+
+    /** Record for a broadcast receiver timeout. */
+    @NonNull
+    public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent,
+            long timeoutDurationMs) {
+        String reason = "Broadcast of " + intent.toString() + ", waited " + timeoutDurationMs
+                + "ms";
         return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason);
     }
 
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index 73fb7fe..076e4e1 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -238,6 +238,21 @@
         Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
     }
 
+    private static boolean isExperimentEnabled(String experiment) {
+        boolean defaultValue = SystemProperties.getBoolean(
+                "dalvik.vm." + experiment,
+                /*def=*/false);
+        // Can't use device_config since we are the zygote, and it's not initialized at this point.
+        return SystemProperties.getBoolean(
+                "persist.device_config." + DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT
+                        + "." + experiment,
+                defaultValue);
+    }
+
+    /* package-private */ static boolean shouldProfileSystemServer() {
+        return isExperimentEnabled("profilesystemserver");
+    }
+
     /**
      * Performs Zygote process initialization. Loads and initializes commonly used classes.
      *
@@ -341,14 +356,7 @@
             // If we are profiling the boot image, reset the Jit counters after preloading the
             // classes. We want to preload for performance, and we can use method counters to
             // infer what clases are used after calling resetJitCounters, for profile purposes.
-            // Can't use device_config since we are the zygote.
-            String prop = SystemProperties.get(
-                    "persist.device_config.runtime_native_boot.profilebootclasspath", "");
-            // Might be empty if the property is unset since the default is "".
-            if (prop.length() == 0) {
-                prop = SystemProperties.get("dalvik.vm.profilebootclasspath", "");
-            }
-            if ("true".equals(prop)) {
+            if (isExperimentEnabled("profilebootclasspath")) {
                 Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ResetJitCounters");
                 VMRuntime.resetJitCounters();
                 Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
@@ -489,16 +497,6 @@
         ZygoteHooks.gcAndFinalize();
     }
 
-    private static boolean shouldProfileSystemServer() {
-        boolean defaultValue = SystemProperties.getBoolean("dalvik.vm.profilesystemserver",
-                /*default=*/ false);
-        // Can't use DeviceConfig since it's not initialized at this point.
-        return SystemProperties.getBoolean(
-                "persist.device_config." + DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT
-                        + ".profilesystemserver",
-                defaultValue);
-    }
-
     /**
      * Finish remaining work for the newly forked system server process.
      */
@@ -585,6 +583,13 @@
      * in the forked system server process in the zygote SELinux domain.
      */
     private static void prefetchStandaloneSystemServerJars() {
+        if (shouldProfileSystemServer()) {
+            // We don't prefetch AOT artifacts if we are profiling system server, as we are going to
+            // JIT it.
+            // This method only gets called from native and should already be skipped if we profile
+            // system server. Still, be robust and check it again.
+            return;
+        }
         String envStr = Os.getenv("STANDALONE_SYSTEMSERVER_JARS");
         if (TextUtils.isEmpty(envStr)) {
             return;
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index a352063..145aeaf 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -20,8 +20,6 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.os.Build.VERSION_CODES.M;
 import static android.os.Build.VERSION_CODES.N;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.clearsCompatInsets;
 import static android.view.View.MeasureSpec.AT_MOST;
 import static android.view.View.MeasureSpec.EXACTLY;
@@ -79,8 +77,6 @@
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.InputQueue;
-import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
 import android.view.LayoutInflater;
@@ -98,6 +94,7 @@
 import android.view.Window;
 import android.view.WindowCallbacks;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowManager;
@@ -145,13 +142,15 @@
             new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS,
                     Gravity.TOP, Gravity.LEFT, Gravity.RIGHT,
                     Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME,
-                    com.android.internal.R.id.statusBarBackground, ITYPE_STATUS_BAR);
+                    com.android.internal.R.id.statusBarBackground,
+                    WindowInsets.Type.statusBars());
 
     public static final ColorViewAttributes NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES =
             new ColorViewAttributes(FLAG_TRANSLUCENT_NAVIGATION,
                     Gravity.BOTTOM, Gravity.RIGHT, Gravity.LEFT,
                     Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME,
-                    com.android.internal.R.id.navigationBarBackground, ITYPE_NAVIGATION_BAR);
+                    com.android.internal.R.id.navigationBarBackground,
+                    WindowInsets.Type.navigationBars());
 
     // This is used to workaround an issue where the PiP shadow can be transparent if the window
     // background is transparent
@@ -1106,6 +1105,7 @@
         int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
 
         final WindowInsetsController controller = getWindowInsetsController();
+        final @InsetsType int requestedVisibleTypes = controller.getRequestedVisibleTypes();
 
         // IME is an exceptional floating window that requires color view.
         final boolean isImeWindow =
@@ -1164,7 +1164,7 @@
                     mWindow.mNavigationBarDividerColor, navBarSize,
                     navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
                     0 /* sideInset */, animate && !disallowAnimate,
-                    mForceWindowDrawsBarBackgrounds, controller);
+                    mForceWindowDrawsBarBackgrounds, requestedVisibleTypes);
             boolean oldDrawLegacy = mDrawLegacyNavigationBarBackground;
             mDrawLegacyNavigationBarBackground =
                     (mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0;
@@ -1187,7 +1187,7 @@
             updateColorViewInt(mStatusColorViewState, statusBarColor, 0,
                     mLastTopInset, false /* matchVertical */, statusBarNeedsLeftInset,
                     statusBarSideInset, animate && !disallowAnimate,
-                    mForceWindowDrawsBarBackgrounds, controller);
+                    mForceWindowDrawsBarBackgrounds, requestedVisibleTypes);
 
             if (mHasCaption) {
                 mDecorCaptionView.getCaption().setBackgroundColor(statusBarColor);
@@ -1206,7 +1206,7 @@
         // Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer
         // consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.
         boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
-                || !(controller == null || controller.isRequestedVisible(ITYPE_NAVIGATION_BAR));
+                || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0;
         boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
         boolean forceConsumingNavBar =
                 ((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled)
@@ -1226,10 +1226,10 @@
         // If we didn't request fullscreen layout, but we still got it because of the
         // mForceWindowDrawsBarBackgrounds flag, also consume top inset.
         // If we should always consume system bars, only consume that if the app wanted to go to
-        // fullscreen, as othrewise we can expect the app to handle it.
+        // fullscreen, as otherwise we can expect the app to handle it.
         boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
                 || (attrs.flags & FLAG_FULLSCREEN) != 0
-                || !(controller == null || controller.isRequestedVisible(ITYPE_STATUS_BAR));
+                || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0;
         boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
                 && decorFitsSystemWindows
                 && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
@@ -1438,10 +1438,10 @@
      */
     private void updateColorViewInt(final ColorViewState state, int color, int dividerColor,
             int size, boolean verticalBar, boolean seascape, int sideMargin, boolean animate,
-            boolean force, WindowInsetsController controller) {
+            boolean force, @InsetsType int requestedVisibleTypes) {
         state.present = state.attributes.isPresent(
-                (controller.isRequestedVisible(state.attributes.insetsType)
-                        || mLastShouldAlwaysConsumeSystemBars),
+                (requestedVisibleTypes & state.attributes.insetsType) != 0
+                        || mLastShouldAlwaysConsumeSystemBars,
                 mWindow.getAttributes().flags, force);
         boolean show = state.attributes.isVisible(state.present, color,
                 mWindow.getAttributes().flags, force);
@@ -2402,7 +2402,7 @@
             return;
         }
         final ThreadedRenderer renderer = getThreadedRenderer();
-        if (renderer != null) {
+        if (renderer != null && !CAPTION_ON_SHELL) {
             loadBackgroundDrawablesIfNeeded();
             WindowInsets rootInsets = getRootWindowInsets();
             mBackdropFrameRenderer = new BackdropFrameRenderer(this, renderer,
@@ -2686,11 +2686,10 @@
         final int horizontalGravity;
         final int seascapeGravity;
         final String transitionName;
-        final @InternalInsetsType int insetsType;
+        final @InsetsType int insetsType;
 
         private ColorViewAttributes(int translucentFlag, int verticalGravity, int horizontalGravity,
-                int seascapeGravity, String transitionName, int id,
-                @InternalInsetsType int insetsType) {
+                int seascapeGravity, String transitionName, int id, @InsetsType int insetsType) {
             this.id = id;
             this.translucentFlag = translucentFlag;
             this.verticalGravity = verticalGravity;
@@ -2707,13 +2706,14 @@
 
         public boolean isVisible(boolean present, int color, int windowFlags, boolean force) {
             return present
-                    && (color & Color.BLACK) != 0
-                    && ((windowFlags & translucentFlag) == 0  || force);
+                    && Color.alpha(color) != 0
+                    && ((windowFlags & translucentFlag) == 0 || force);
         }
 
-        public boolean isVisible(InsetsState state, int color, int windowFlags, boolean force) {
-            final boolean present = isPresent(state.getSource(insetsType).isVisible(), windowFlags,
-                    force);
+        public boolean isVisible(@InsetsType int requestedVisibleTypes, int color, int windowFlags,
+                boolean force) {
+            final boolean requestedVisible = (requestedVisibleTypes & insetsType) != 0;
+            final boolean present = isPresent(requestedVisible, windowFlags, force);
             return isVisible(present, color, windowFlags, force);
         }
     }
diff --git a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
index a09c823..04dd2d7 100644
--- a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
+++ b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
@@ -97,7 +97,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
@@ -224,7 +223,6 @@
             }
 
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 295dc54..25ac1bd 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -41,12 +41,16 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorSpace;
 import android.graphics.Picture;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.hardware.HardwareBuffer;
+import android.media.Image;
+import android.media.ImageReader;
 import android.os.SystemProperties;
 import android.util.Slog;
+import android.view.SurfaceControl;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
 import android.view.WindowManager.TransitionType;
@@ -59,9 +63,11 @@
 import android.view.animation.PathInterpolator;
 import android.view.animation.ScaleAnimation;
 import android.view.animation.TranslateAnimation;
+import android.window.ScreenCapture;
 
 import com.android.internal.R;
 
+import java.nio.ByteBuffer;
 import java.util.List;
 
 /** @hide */
@@ -1262,4 +1268,90 @@
 
         return set;
     }
+
+    /** Returns whether the hardware buffer passed in is marked as protected. */
+    public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
+        return (hardwareBuffer.getUsage() & HardwareBuffer.USAGE_PROTECTED_CONTENT)
+                == HardwareBuffer.USAGE_PROTECTED_CONTENT;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) {
+        final ScreenCapture.ScreenshotHardwareBuffer buffer =
+                ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1);
+        if (buffer != null) {
+            return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
+        }
+        return 0;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(HardwareBuffer hwBuffer, ColorSpace colorSpace) {
+        if (hwBuffer == null) {
+            return 0;
+        }
+        final int format = hwBuffer.getFormat();
+        // Only support RGB format in 4 bytes. And protected buffer is not readable.
+        if (format != HardwareBuffer.RGBA_8888 || hasProtectedContent(hwBuffer)) {
+            return 0;
+        }
+
+        final ImageReader ir = ImageReader.newInstance(hwBuffer.getWidth(), hwBuffer.getHeight(),
+                format, 1 /* maxImages */);
+        ir.getSurface().attachAndQueueBufferWithColorSpace(hwBuffer, colorSpace);
+        final Image image = ir.acquireLatestImage();
+        if (image == null || image.getPlaneCount() < 1) {
+            return 0;
+        }
+
+        final Image.Plane plane = image.getPlanes()[0];
+        final ByteBuffer buffer = plane.getBuffer();
+        final int width = image.getWidth();
+        final int height = image.getHeight();
+        final int pixelStride = plane.getPixelStride();
+        final int rowStride = plane.getRowStride();
+        final int sampling = 10;
+        final int[] borderLumas = new int[(width + height) * 2 / sampling];
+
+        // Grab the top and bottom borders.
+        int i = 0;
+        for (int x = 0, size = width - sampling; x < size; x += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
+        }
+
+        // Grab the left and right borders.
+        for (int y = 0, size = height - sampling; y < size; y += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
+        }
+
+        ir.close();
+
+        // Get "mode" by histogram.
+        final int[] histogram = new int[256];
+        int maxCount = 0;
+        int mostLuma = 0;
+        for (int luma : borderLumas) {
+            final int count = ++histogram[luma];
+            if (count > maxCount) {
+                maxCount = count;
+                mostLuma = luma;
+            }
+        }
+        return mostLuma / 255f;
+    }
+
+    /** Returns the luminance of the pixel in 0~255. */
+    private static int getPixelLuminance(ByteBuffer buffer, int x, int y, int pixelStride,
+            int rowStride) {
+        final int color = buffer.getInt(y * rowStride + x * pixelStride);
+        // The buffer from ImageReader is always in native order (little-endian), so extract the
+        // color components in reversed order.
+        final int r = color & 0xff;
+        final int g = (color >> 8) & 0xff;
+        final int b = (color >> 16) & 0xff;
+        // Approximation of WCAG 2.0 relative luminance.
+        return ((r * 8) + (g * 22) + (b * 2)) >> 5;
+    }
 }
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..3ab11a8 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -17,16 +17,36 @@
 package com.android.internal.security;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Build;
 import android.os.SystemProperties;
 import android.system.Os;
 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.Path;
 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 {
@@ -59,17 +79,23 @@
         return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
     }
 
-    /** Enables fs-verity for the file with a PKCS#7 detached signature file. */
-    public static void setUpFsverity(@NonNull String filePath, @NonNull String signaturePath)
+    /** Enables fs-verity for the file with an optional PKCS#7 detached signature file. */
+    public static void setUpFsverity(@NonNull String filePath, @Nullable String signaturePath)
             throws IOException {
-        if (Files.size(Paths.get(signaturePath)) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
-            throw new SecurityException("Signature file is unexpectedly large: " + signaturePath);
+        byte[] rawSignature = null;
+        if (signaturePath != null) {
+            Path path = Paths.get(signaturePath);
+            if (Files.size(path) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
+                throw new SecurityException("Signature file is unexpectedly large: "
+                        + signaturePath);
+            }
+            rawSignature = Files.readAllBytes(path);
         }
-        setUpFsverity(filePath, Files.readAllBytes(Paths.get(signaturePath)));
+        setUpFsverity(filePath, rawSignature);
     }
 
-    /** Enables fs-verity for the file with a PKCS#7 detached signature bytes. */
-    public static void setUpFsverity(@NonNull String filePath, @NonNull byte[] pkcs7Signature)
+    /** Enables fs-verity for the file with an optional PKCS#7 detached signature bytes. */
+    public static void setUpFsverity(@NonNull String filePath, @Nullable byte[] pkcs7Signature)
             throws IOException {
         // This will fail if the public key is not already in .fs-verity kernel keyring.
         int errno = enableFsverityNative(filePath, pkcs7Signature);
@@ -91,6 +117,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,8 +221,21 @@
         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);
+            @Nullable byte[] pkcs7Signature);
     private static native int measureFsverityNative(@NonNull String filePath,
             @NonNull byte[] digest);
     private static native int statxForFsverityNative(@NonNull String filePath);
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 44cfe1a..edbdc86 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -23,13 +23,12 @@
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.service.notification.StatusBarNotification;
-import android.view.InsetsVisibilities;
 
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -175,9 +174,9 @@
     void setBiometicContextListener(in IBiometricContextListener listener);
 
     /**
-     * Sets an instance of IUdfpsHbmListener for UdfpsController.
+     * Sets an instance of IUdfpsRefreshRateRequestCallback for UdfpsController.
      */
-    void setUdfpsHbmListener(in IUdfpsHbmListener listener);
+    void setUdfpsRefreshRateCallback(in IUdfpsRefreshRateRequestCallback callback);
 
     /**
      * Notifies System UI that the display is ready to show system decorations.
@@ -201,13 +200,13 @@
      *                         stacks.
      * @param navbarColorManagedByIme {@code true} if navigation bar color is managed by IME.
      * @param behavior the behavior of the focused window.
-     * @param requestedVisibilities the collection of the requested visibilities of system insets.
+     * @param requestedVisibleTypes the collection of insets types requested visible.
      * @param packageName the package name of the focused app.
      * @param letterboxDetails a set of letterbox details of apps visible on the screen.
      */
     void onSystemBarAttributesChanged(int displayId, int appearance,
             in AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            int behavior, in InsetsVisibilities requestedVisibilities, String packageName,
+            int behavior, int requestedVisibleTypes, String packageName,
             in LetterboxDetails[] letterboxDetails);
 
     /**
@@ -322,4 +321,7 @@
 
     /** Unregisters a nearby media devices provider. */
     void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider);
+
+    /** Dump protos from SystemUI. The proto definition is defined there */
+    void dumpProto(in String[] args, in ParcelFileDescriptor pfd);
 }
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index ef8f2db..d190681 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -23,7 +23,7 @@
 import android.hardware.biometrics.IBiometricContextListener;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
 import android.net.Uri;
@@ -136,9 +136,9 @@
     void setBiometicContextListener(in IBiometricContextListener listener);
 
     /**
-     * Sets an instance of IUdfpsHbmListener for UdfpsController.
+     * Sets an instance of IUdfpsRefreshRateRequestCallback for UdfpsController.
      */
-    void setUdfpsHbmListener(in IUdfpsHbmListener listener);
+    void setUdfpsRefreshRateCallback(in IUdfpsRefreshRateRequestCallback callback);
 
     /**
      * Show a warning that the device is about to go to sleep due to user inactivity.
diff --git a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
index 8b898f0..54221ce 100644
--- a/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
+++ b/core/java/com/android/internal/statusbar/RegisterStatusBarResult.java
@@ -21,7 +21,6 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
-import android.view.InsetsVisibilities;
 
 import com.android.internal.view.AppearanceRegion;
 
@@ -40,7 +39,7 @@
     public final IBinder mImeToken;
     public final boolean mNavbarColorManagedByIme;
     public final int mBehavior;
-    public final InsetsVisibilities mRequestedVisibilities;
+    public final int mRequestedVisibleTypes;
     public final String mPackageName;
     public final int[] mTransientBarTypes;
     public final LetterboxDetails[] mLetterboxDetails;
@@ -48,7 +47,7 @@
     public RegisterStatusBarResult(ArrayMap<String, StatusBarIcon> icons, int disabledFlags1,
             int appearance, AppearanceRegion[] appearanceRegions, int imeWindowVis,
             int imeBackDisposition, boolean showImeSwitcher, int disabledFlags2, IBinder imeToken,
-            boolean navbarColorManagedByIme, int behavior, InsetsVisibilities requestedVisibilities,
+            boolean navbarColorManagedByIme, int behavior, int requestedVisibleTypes,
             String packageName, @NonNull int[] transientBarTypes,
             LetterboxDetails[] letterboxDetails) {
         mIcons = new ArrayMap<>(icons);
@@ -62,7 +61,7 @@
         mImeToken = imeToken;
         mNavbarColorManagedByIme = navbarColorManagedByIme;
         mBehavior = behavior;
-        mRequestedVisibilities = requestedVisibilities;
+        mRequestedVisibleTypes = requestedVisibleTypes;
         mPackageName = packageName;
         mTransientBarTypes = transientBarTypes;
         mLetterboxDetails = letterboxDetails;
@@ -86,7 +85,7 @@
         dest.writeStrongBinder(mImeToken);
         dest.writeBoolean(mNavbarColorManagedByIme);
         dest.writeInt(mBehavior);
-        dest.writeTypedObject(mRequestedVisibilities, 0);
+        dest.writeInt(mRequestedVisibleTypes);
         dest.writeString(mPackageName);
         dest.writeIntArray(mTransientBarTypes);
         dest.writeParcelableArray(mLetterboxDetails, flags);
@@ -112,8 +111,7 @@
                     final IBinder imeToken = source.readStrongBinder();
                     final boolean navbarColorManagedByIme = source.readBoolean();
                     final int behavior = source.readInt();
-                    final InsetsVisibilities requestedVisibilities =
-                            source.readTypedObject(InsetsVisibilities.CREATOR);
+                    final int requestedVisibleTypes = source.readInt();
                     final String packageName = source.readString();
                     final int[] transientBarTypes = source.createIntArray();
                     final LetterboxDetails[] letterboxDetails =
@@ -121,7 +119,7 @@
                     return new RegisterStatusBarResult(icons, disabledFlags1, appearance,
                             appearanceRegions, imeWindowVis, imeBackDisposition, showImeSwitcher,
                             disabledFlags2, imeToken, navbarColorManagedByIme, behavior,
-                            requestedVisibilities, packageName, transientBarTypes,
+                            requestedVisibleTypes, packageName, transientBarTypes,
                             letterboxDetails);
                 }
 
diff --git a/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java b/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java
new file mode 100644
index 0000000..c56bc49
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java
@@ -0,0 +1,39 @@
+/*
+ * 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.util;
+
+import android.annotation.NonNull;
+
+import com.android.modules.utils.BinaryXmlPullParser;
+import com.android.modules.utils.FastDataInput;
+
+import java.io.DataInput;
+import java.io.InputStream;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This decodes large code-points using 4-byte sequences, and <em>is not</em> compatible with the
+ * {@link DataInput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtBinaryXmlPullParser extends BinaryXmlPullParser {
+    @NonNull
+    protected FastDataInput obtainFastDataInput(@NonNull InputStream is) {
+        return ArtFastDataInput.obtain(is);
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java b/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java
new file mode 100644
index 0000000..98a2135
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java
@@ -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.internal.util;
+
+import android.annotation.NonNull;
+
+import com.android.modules.utils.BinaryXmlSerializer;
+import com.android.modules.utils.FastDataOutput;
+
+import java.io.DataOutput;
+import java.io.OutputStream;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This encodes large code-points using 4-byte sequences and <em>is not</em> compatible with the
+ * {@link DataOutput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtBinaryXmlSerializer extends BinaryXmlSerializer {
+    @NonNull
+    @Override
+    protected FastDataOutput obtainFastDataOutput(@NonNull OutputStream os) {
+        return ArtFastDataOutput.obtain(os);
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtFastDataInput.java b/core/java/com/android/internal/util/ArtFastDataInput.java
new file mode 100644
index 0000000..3e8916c
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtFastDataInput.java
@@ -0,0 +1,94 @@
+/*
+ * 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.util;
+
+import android.annotation.NonNull;
+import android.util.CharsetUtils;
+
+import com.android.modules.utils.FastDataInput;
+
+import java.io.DataInput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This decodes large code-points using 4-byte sequences, and <em>is not</em> compatible with the
+ * {@link DataInput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtFastDataInput extends FastDataInput {
+    private static AtomicReference<ArtFastDataInput> sInCache = new AtomicReference<>();
+
+    private final long mBufferPtr;
+
+    public ArtFastDataInput(@NonNull InputStream in, int bufferSize) {
+        super(in, bufferSize);
+
+        mBufferPtr = mRuntime.addressOf(mBuffer);
+    }
+
+    /**
+     * Obtain a {@link ArtFastDataInput} configured with the given
+     * {@link InputStream} and which decodes large code-points using 4-byte
+     * sequences.
+     * <p>
+     * This <em>is not</em> compatible with the {@link DataInput} API contract,
+     * which specifies that large code-points must be encoded with 3-byte
+     * sequences.
+     */
+    public static ArtFastDataInput obtain(@NonNull InputStream in) {
+        ArtFastDataInput instance = sInCache.getAndSet(null);
+        if (instance != null) {
+            instance.setInput(in);
+            return instance;
+        }
+        return new ArtFastDataInput(in, DEFAULT_BUFFER_SIZE);
+    }
+
+    /**
+     * Release a {@link ArtFastDataInput} to potentially be recycled. You must not
+     * interact with the object after releasing it.
+     */
+    public void release() {
+        super.release();
+
+        if (mBufferCap == DEFAULT_BUFFER_SIZE) {
+            // Try to return to the cache.
+            sInCache.compareAndSet(null, this);
+        }
+    }
+
+    @Override
+    public String readUTF() throws IOException {
+        // Attempt to read directly from buffer space if there's enough room,
+        // otherwise fall back to chunking into place
+        final int len = readUnsignedShort();
+        if (mBufferCap > len) {
+            if (mBufferLim - mBufferPos < len) fill(len);
+            final String res = CharsetUtils.fromModifiedUtf8Bytes(mBufferPtr, mBufferPos, len);
+            mBufferPos += len;
+            return res;
+        } else {
+            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+            readFully(tmp, 0, len);
+            return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtFastDataOutput.java b/core/java/com/android/internal/util/ArtFastDataOutput.java
new file mode 100644
index 0000000..ac595b6
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtFastDataOutput.java
@@ -0,0 +1,103 @@
+/*
+ * 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.util;
+
+import android.annotation.NonNull;
+import android.util.CharsetUtils;
+
+import com.android.modules.utils.FastDataOutput;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This encodes large code-points using 4-byte sequences and <em>is not</em> compatible with the
+ * {@link DataOutput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtFastDataOutput extends FastDataOutput {
+    private static AtomicReference<ArtFastDataOutput> sOutCache = new AtomicReference<>();
+
+    private final long mBufferPtr;
+
+    public ArtFastDataOutput(@NonNull OutputStream out, int bufferSize) {
+        super(out, bufferSize);
+
+        mBufferPtr = mRuntime.addressOf(mBuffer);
+    }
+
+    /**
+     * Obtain an {@link ArtFastDataOutput} configured with the given
+     * {@link OutputStream} and which encodes large code-points using 4-byte
+     * sequences.
+     * <p>
+     * This <em>is not</em> compatible with the {@link DataOutput} API contract,
+     * which specifies that large code-points must be encoded with 3-byte
+     * sequences.
+     */
+    public static ArtFastDataOutput obtain(@NonNull OutputStream out) {
+        ArtFastDataOutput instance = sOutCache.getAndSet(null);
+        if (instance != null) {
+            instance.setOutput(out);
+            return instance;
+        }
+        return new ArtFastDataOutput(out, DEFAULT_BUFFER_SIZE);
+    }
+
+    @Override
+    public void release() {
+        super.release();
+
+        if (mBufferCap == DEFAULT_BUFFER_SIZE) {
+            // Try to return to the cache.
+            sOutCache.compareAndSet(null, this);
+        }
+    }
+
+    @Override
+    public void writeUTF(String s) throws IOException {
+        // Attempt to write directly to buffer space if there's enough room,
+        // otherwise fall back to chunking into place
+        if (mBufferCap - mBufferPos < 2 + s.length()) drain();
+
+        // Magnitude of this returned value indicates the number of bytes
+        // required to encode the string; sign indicates success/failure
+        int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap);
+        if (Math.abs(len) > MAX_UNSIGNED_SHORT) {
+            throw new IOException("Modified UTF-8 length too large: " + len);
+        }
+
+        if (len >= 0) {
+            // Positive value indicates the string was encoded into the buffer
+            // successfully, so we only need to prefix with length
+            writeShort(len);
+            mBufferPos += len;
+        } else {
+            // Negative value indicates buffer was too small and we need to
+            // allocate a temporary buffer for encoding
+            len = -len;
+            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+            CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
+            writeShort(len);
+            write(tmp, 0, len);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/util/BinaryXmlPullParser.java b/core/java/com/android/internal/util/BinaryXmlPullParser.java
deleted file mode 100644
index d3abac9..0000000
--- a/core/java/com/android/internal/util/BinaryXmlPullParser.java
+++ /dev/null
@@ -1,939 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import static com.android.internal.util.BinaryXmlSerializer.ATTRIBUTE;
-import static com.android.internal.util.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_BASE64;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_DOUBLE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_FLOAT;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_NULL;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING_INTERNED;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * Parser that reads XML documents using a custom binary wire protocol which
- * benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()}
- * for a typical {@code packages.xml}.
- * <p>
- * The high-level design of the wire protocol is to directly serialize the event
- * stream, while efficiently and compactly writing strongly-typed primitives
- * delivered through the {@link TypedXmlSerializer} interface.
- * <p>
- * Each serialized event is a single byte where the lower half is a normal
- * {@link XmlPullParser} token and the upper half is an optional data type
- * signal, such as {@link #TYPE_INT}.
- * <p>
- * This parser has some specific limitations:
- * <ul>
- * <li>Only the UTF-8 encoding is supported.
- * <li>Variable length values, such as {@code byte[]} or {@link String}, are
- * limited to 65,535 bytes in length. Note that {@link String} values are stored
- * as UTF-8 on the wire.
- * <li>Namespaces, prefixes, properties, and options are unsupported.
- * </ul>
- */
-public final class BinaryXmlPullParser implements TypedXmlPullParser {
-    private FastDataInput mIn;
-
-    private int mCurrentToken = START_DOCUMENT;
-    private int mCurrentDepth = 0;
-    private String mCurrentName;
-    private String mCurrentText;
-
-    /**
-     * Pool of attributes parsed for the currently tag. All interactions should
-     * be done via {@link #obtainAttribute()}, {@link #findAttribute(String)},
-     * and {@link #resetAttributes()}.
-     */
-    private int mAttributeCount = 0;
-    private Attribute[] mAttributes;
-
-    @Override
-    public void setInput(InputStream is, String encoding) throws XmlPullParserException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-
-        if (mIn != null) {
-            mIn.release();
-            mIn = null;
-        }
-
-        mIn = FastDataInput.obtainUsing4ByteSequences(is);
-
-        mCurrentToken = START_DOCUMENT;
-        mCurrentDepth = 0;
-        mCurrentName = null;
-        mCurrentText = null;
-
-        mAttributeCount = 0;
-        mAttributes = new Attribute[8];
-        for (int i = 0; i < mAttributes.length; i++) {
-            mAttributes[i] = new Attribute();
-        }
-
-        try {
-            final byte[] magic = new byte[4];
-            mIn.readFully(magic);
-            if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) {
-                throw new IOException("Unexpected magic " + bytesToHexString(magic));
-            }
-
-            // We're willing to immediately consume a START_DOCUMENT if present,
-            // but we're okay if it's missing
-            if (peekNextExternalToken() == START_DOCUMENT) {
-                consumeToken();
-            }
-        } catch (IOException e) {
-            throw new XmlPullParserException(e.toString());
-        }
-    }
-
-    @Override
-    public void setInput(Reader in) throws XmlPullParserException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int next() throws XmlPullParserException, IOException {
-        while (true) {
-            final int token = nextToken();
-            switch (token) {
-                case START_TAG:
-                case END_TAG:
-                case END_DOCUMENT:
-                    return token;
-                case TEXT:
-                    consumeAdditionalText();
-                    // Per interface docs, empty text regions are skipped
-                    if (mCurrentText == null || mCurrentText.length() == 0) {
-                        continue;
-                    } else {
-                        return TEXT;
-                    }
-            }
-        }
-    }
-
-    @Override
-    public int nextToken() throws XmlPullParserException, IOException {
-        if (mCurrentToken == XmlPullParser.END_TAG) {
-            mCurrentDepth--;
-        }
-
-        int token;
-        try {
-            token = peekNextExternalToken();
-            consumeToken();
-        } catch (EOFException e) {
-            token = END_DOCUMENT;
-        }
-        switch (token) {
-            case XmlPullParser.START_TAG:
-                // We need to peek forward to find the next external token so
-                // that we parse all pending INTERNAL_ATTRIBUTE tokens
-                peekNextExternalToken();
-                mCurrentDepth++;
-                break;
-        }
-        mCurrentToken = token;
-        return token;
-    }
-
-    /**
-     * Peek at the next "external" token without consuming it.
-     * <p>
-     * External tokens, such as {@link #START_TAG}, are expected by typical
-     * {@link XmlPullParser} clients. In contrast, internal tokens, such as
-     * {@link #ATTRIBUTE}, are not expected by typical clients.
-     * <p>
-     * This method consumes any internal events until it reaches the next
-     * external event.
-     */
-    private int peekNextExternalToken() throws IOException, XmlPullParserException {
-        while (true) {
-            final int token = peekNextToken();
-            switch (token) {
-                case ATTRIBUTE:
-                    consumeToken();
-                    continue;
-                default:
-                    return token;
-            }
-        }
-    }
-
-    /**
-     * Peek at the next token in the underlying stream without consuming it.
-     */
-    private int peekNextToken() throws IOException {
-        return mIn.peekByte() & 0x0f;
-    }
-
-    /**
-     * Parse and consume the next token in the underlying stream.
-     */
-    private void consumeToken() throws IOException, XmlPullParserException {
-        final int event = mIn.readByte();
-        final int token = event & 0x0f;
-        final int type = event & 0xf0;
-        switch (token) {
-            case ATTRIBUTE: {
-                final Attribute attr = obtainAttribute();
-                attr.name = mIn.readInternedUTF();
-                attr.type = type;
-                switch (type) {
-                    case TYPE_NULL:
-                    case TYPE_BOOLEAN_TRUE:
-                    case TYPE_BOOLEAN_FALSE:
-                        // Nothing extra to fill in
-                        break;
-                    case TYPE_STRING:
-                        attr.valueString = mIn.readUTF();
-                        break;
-                    case TYPE_STRING_INTERNED:
-                        attr.valueString = mIn.readInternedUTF();
-                        break;
-                    case TYPE_BYTES_HEX:
-                    case TYPE_BYTES_BASE64:
-                        final int len = mIn.readUnsignedShort();
-                        final byte[] res = new byte[len];
-                        mIn.readFully(res);
-                        attr.valueBytes = res;
-                        break;
-                    case TYPE_INT:
-                    case TYPE_INT_HEX:
-                        attr.valueInt = mIn.readInt();
-                        break;
-                    case TYPE_LONG:
-                    case TYPE_LONG_HEX:
-                        attr.valueLong = mIn.readLong();
-                        break;
-                    case TYPE_FLOAT:
-                        attr.valueFloat = mIn.readFloat();
-                        break;
-                    case TYPE_DOUBLE:
-                        attr.valueDouble = mIn.readDouble();
-                        break;
-                    default:
-                        throw new IOException("Unexpected data type " + type);
-                }
-                break;
-            }
-            case XmlPullParser.START_DOCUMENT: {
-                mCurrentName = null;
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.END_DOCUMENT: {
-                mCurrentName = null;
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.START_TAG: {
-                mCurrentName = mIn.readInternedUTF();
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.END_TAG: {
-                mCurrentName = mIn.readInternedUTF();
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.TEXT:
-            case XmlPullParser.CDSECT:
-            case XmlPullParser.PROCESSING_INSTRUCTION:
-            case XmlPullParser.COMMENT:
-            case XmlPullParser.DOCDECL:
-            case XmlPullParser.IGNORABLE_WHITESPACE: {
-                mCurrentName = null;
-                mCurrentText = mIn.readUTF();
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.ENTITY_REF: {
-                mCurrentName = mIn.readUTF();
-                mCurrentText = resolveEntity(mCurrentName);
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            default: {
-                throw new IOException("Unknown token " + token + " with type " + type);
-            }
-        }
-    }
-
-    /**
-     * When the current tag is {@link #TEXT}, consume all subsequent "text"
-     * events, as described by {@link #next}. When finished, the current event
-     * will still be {@link #TEXT}.
-     */
-    private void consumeAdditionalText() throws IOException, XmlPullParserException {
-        String combinedText = mCurrentText;
-        while (true) {
-            final int token = peekNextExternalToken();
-            switch (token) {
-                case COMMENT:
-                case PROCESSING_INSTRUCTION:
-                    // Quietly consumed
-                    consumeToken();
-                    break;
-                case TEXT:
-                case CDSECT:
-                case ENTITY_REF:
-                    // Additional text regions collected
-                    consumeToken();
-                    combinedText += mCurrentText;
-                    break;
-                default:
-                    // Next token is something non-text, so wrap things up
-                    mCurrentToken = TEXT;
-                    mCurrentName = null;
-                    mCurrentText = combinedText;
-                    return;
-            }
-        }
-    }
-
-    static @NonNull String resolveEntity(@NonNull String entity)
-            throws XmlPullParserException {
-        switch (entity) {
-            case "lt": return "<";
-            case "gt": return ">";
-            case "amp": return "&";
-            case "apos": return "'";
-            case "quot": return "\"";
-        }
-        if (entity.length() > 1 && entity.charAt(0) == '#') {
-            final char c = (char) Integer.parseInt(entity.substring(1));
-            return new String(new char[] { c });
-        }
-        throw new XmlPullParserException("Unknown entity " + entity);
-    }
-
-    @Override
-    public void require(int type, String namespace, String name)
-            throws XmlPullParserException, IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-    }
-
-    @Override
-    public String nextText() throws XmlPullParserException, IOException {
-        if (getEventType() != START_TAG) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-        int eventType = next();
-        if (eventType == TEXT) {
-            String result = getText();
-            eventType = next();
-            if (eventType != END_TAG) {
-                throw new XmlPullParserException(getPositionDescription());
-            }
-            return result;
-        } else if (eventType == END_TAG) {
-            return "";
-        } else {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-    }
-
-    @Override
-    public int nextTag() throws XmlPullParserException, IOException {
-        int eventType = next();
-        if (eventType == TEXT && isWhitespace()) {
-            eventType = next();
-        }
-        if (eventType != START_TAG && eventType != END_TAG) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-        return eventType;
-    }
-
-    /**
-     * Allocate and return a new {@link Attribute} associated with the tag being
-     * currently processed. This will automatically grow the internal pool as
-     * needed.
-     */
-    private @NonNull Attribute obtainAttribute() {
-        if (mAttributeCount == mAttributes.length) {
-            final int before = mAttributes.length;
-            final int after = before + (before >> 1);
-            mAttributes = Arrays.copyOf(mAttributes, after);
-            for (int i = before; i < after; i++) {
-                mAttributes[i] = new Attribute();
-            }
-        }
-        return mAttributes[mAttributeCount++];
-    }
-
-    /**
-     * Clear any {@link Attribute} instances that have been allocated by
-     * {@link #obtainAttribute()}, returning them into the pool for recycling.
-     */
-    private void resetAttributes() {
-        for (int i = 0; i < mAttributeCount; i++) {
-            mAttributes[i].reset();
-        }
-        mAttributeCount = 0;
-    }
-
-    @Override
-    public int getAttributeIndex(String namespace, String name) {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        for (int i = 0; i < mAttributeCount; i++) {
-            if (Objects.equals(mAttributes[i].name, name)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    @Override
-    public String getAttributeValue(String namespace, String name) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index != -1) {
-            return mAttributes[index].getValueString();
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public String getAttributeValue(int index) {
-        return mAttributes[index].getValueString();
-    }
-
-    @Override
-    public byte[] getAttributeBytesHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBytesHex();
-    }
-
-    @Override
-    public byte[] getAttributeBytesBase64(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBytesBase64();
-    }
-
-    @Override
-    public int getAttributeInt(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueInt();
-    }
-
-    @Override
-    public int getAttributeIntHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueIntHex();
-    }
-
-    @Override
-    public long getAttributeLong(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueLong();
-    }
-
-    @Override
-    public long getAttributeLongHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueLongHex();
-    }
-
-    @Override
-    public float getAttributeFloat(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueFloat();
-    }
-
-    @Override
-    public double getAttributeDouble(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueDouble();
-    }
-
-    @Override
-    public boolean getAttributeBoolean(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBoolean();
-    }
-
-    @Override
-    public String getText() {
-        return mCurrentText;
-    }
-
-    @Override
-    public char[] getTextCharacters(int[] holderForStartAndLength) {
-        final char[] chars = mCurrentText.toCharArray();
-        holderForStartAndLength[0] = 0;
-        holderForStartAndLength[1] = chars.length;
-        return chars;
-    }
-
-    @Override
-    public String getInputEncoding() {
-        return StandardCharsets.UTF_8.name();
-    }
-
-    @Override
-    public int getDepth() {
-        return mCurrentDepth;
-    }
-
-    @Override
-    public String getPositionDescription() {
-        // Not very helpful, but it's the best information we have
-        return "Token " + mCurrentToken + " at depth " + mCurrentDepth;
-    }
-
-    @Override
-    public int getLineNumber() {
-        return -1;
-    }
-
-    @Override
-    public int getColumnNumber() {
-        return -1;
-    }
-
-    @Override
-    public boolean isWhitespace() throws XmlPullParserException {
-        switch (mCurrentToken) {
-            case IGNORABLE_WHITESPACE:
-                return true;
-            case TEXT:
-            case CDSECT:
-                return !TextUtils.isGraphic(mCurrentText);
-            default:
-                throw new XmlPullParserException("Not applicable for token " + mCurrentToken);
-        }
-    }
-
-    @Override
-    public String getNamespace() {
-        switch (mCurrentToken) {
-            case START_TAG:
-            case END_TAG:
-                // Namespaces are unsupported
-                return NO_NAMESPACE;
-            default:
-                return null;
-        }
-    }
-
-    @Override
-    public String getName() {
-        return mCurrentName;
-    }
-
-    @Override
-    public String getPrefix() {
-        // Prefixes are not supported
-        return null;
-    }
-
-    @Override
-    public boolean isEmptyElementTag() throws XmlPullParserException {
-        switch (mCurrentToken) {
-            case START_TAG:
-                try {
-                    return (peekNextExternalToken() == END_TAG);
-                } catch (IOException e) {
-                    throw new XmlPullParserException(e.toString());
-                }
-            default:
-                throw new XmlPullParserException("Not at START_TAG");
-        }
-    }
-
-    @Override
-    public int getAttributeCount() {
-        return mAttributeCount;
-    }
-
-    @Override
-    public String getAttributeNamespace(int index) {
-        // Namespaces are unsupported
-        return NO_NAMESPACE;
-    }
-
-    @Override
-    public String getAttributeName(int index) {
-        return mAttributes[index].name;
-    }
-
-    @Override
-    public String getAttributePrefix(int index) {
-        // Prefixes are not supported
-        return null;
-    }
-
-    @Override
-    public String getAttributeType(int index) {
-        // Validation is not supported
-        return "CDATA";
-    }
-
-    @Override
-    public boolean isAttributeDefault(int index) {
-        // Validation is not supported
-        return false;
-    }
-
-    @Override
-    public int getEventType() throws XmlPullParserException {
-        return mCurrentToken;
-    }
-
-    @Override
-    public int getNamespaceCount(int depth) throws XmlPullParserException {
-        // Namespaces are unsupported
-        return 0;
-    }
-
-    @Override
-    public String getNamespacePrefix(int pos) throws XmlPullParserException {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getNamespaceUri(int pos) throws XmlPullParserException {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getNamespace(String prefix) {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void defineEntityReplacementText(String entityName, String replacementText)
-            throws XmlPullParserException {
-        // Custom entities are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setFeature(String name, boolean state) throws XmlPullParserException {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean getFeature(String name) {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setProperty(String name, Object value) throws XmlPullParserException {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Object getProperty(String name) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    private static IllegalArgumentException illegalNamespace() {
-        throw new IllegalArgumentException("Namespaces are not supported");
-    }
-
-    /**
-     * Holder representing a single attribute. This design enables object
-     * recycling without resorting to autoboxing.
-     * <p>
-     * To support conversion between human-readable XML and binary XML, the
-     * various accessor methods will transparently convert from/to
-     * human-readable values when needed.
-     */
-    private static class Attribute {
-        public String name;
-        public int type;
-
-        public String valueString;
-        public byte[] valueBytes;
-        public int valueInt;
-        public long valueLong;
-        public float valueFloat;
-        public double valueDouble;
-
-        public void reset() {
-            name = null;
-            valueString = null;
-            valueBytes = null;
-        }
-
-        public @Nullable String getValueString() {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    return valueString;
-                case TYPE_BYTES_HEX:
-                    return bytesToHexString(valueBytes);
-                case TYPE_BYTES_BASE64:
-                    return Base64.encodeToString(valueBytes, Base64.NO_WRAP);
-                case TYPE_INT:
-                    return Integer.toString(valueInt);
-                case TYPE_INT_HEX:
-                    return Integer.toString(valueInt, 16);
-                case TYPE_LONG:
-                    return Long.toString(valueLong);
-                case TYPE_LONG_HEX:
-                    return Long.toString(valueLong, 16);
-                case TYPE_FLOAT:
-                    return Float.toString(valueFloat);
-                case TYPE_DOUBLE:
-                    return Double.toString(valueDouble);
-                case TYPE_BOOLEAN_TRUE:
-                    return "true";
-                case TYPE_BOOLEAN_FALSE:
-                    return "false";
-                default:
-                    // Unknown data type; null is the best we can offer
-                    return null;
-            }
-        }
-
-        public @Nullable byte[] getValueBytesHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_BYTES_HEX:
-                case TYPE_BYTES_BASE64:
-                    return valueBytes;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return hexStringToBytes(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public @Nullable byte[] getValueBytesBase64() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_BYTES_HEX:
-                case TYPE_BYTES_BASE64:
-                    return valueBytes;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Base64.decode(valueString, Base64.NO_WRAP);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public int getValueInt() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_INT:
-                case TYPE_INT_HEX:
-                    return valueInt;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Integer.parseInt(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public int getValueIntHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_INT:
-                case TYPE_INT_HEX:
-                    return valueInt;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Integer.parseInt(valueString, 16);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public long getValueLong() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_LONG:
-                case TYPE_LONG_HEX:
-                    return valueLong;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Long.parseLong(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public long getValueLongHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_LONG:
-                case TYPE_LONG_HEX:
-                    return valueLong;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Long.parseLong(valueString, 16);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public float getValueFloat() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_FLOAT:
-                    return valueFloat;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Float.parseFloat(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public double getValueDouble() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_DOUBLE:
-                    return valueDouble;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Double.parseDouble(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public boolean getValueBoolean() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_BOOLEAN_TRUE:
-                    return true;
-                case TYPE_BOOLEAN_FALSE:
-                    return false;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    if ("true".equalsIgnoreCase(valueString)) {
-                        return true;
-                    } else if ("false".equalsIgnoreCase(valueString)) {
-                        return false;
-                    } else {
-                        throw new XmlPullParserException(
-                                "Invalid attribute " + name + ": " + valueString);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-    }
-
-    // NOTE: To support unbundled clients, we include an inlined copy
-    // of hex conversion logic from HexDump below
-    private final static char[] HEX_DIGITS =
-            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-
-    private static int toByte(char c) {
-        if (c >= '0' && c <= '9') return (c - '0');
-        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
-        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
-        throw new IllegalArgumentException("Invalid hex char '" + c + "'");
-    }
-
-    static String bytesToHexString(byte[] value) {
-        final int length = value.length;
-        final char[] buf = new char[length * 2];
-        int bufIndex = 0;
-        for (int i = 0; i < length; i++) {
-            byte b = value[i];
-            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
-            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
-        }
-        return new String(buf);
-    }
-
-    static byte[] hexStringToBytes(String value) {
-        final int length = value.length();
-        if (length % 2 != 0) {
-            throw new IllegalArgumentException("Invalid hex length " + length);
-        }
-        byte[] buffer = new byte[length / 2];
-        for (int i = 0; i < length; i += 2) {
-            buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4)
-                    | toByte(value.charAt(i + 1)));
-        }
-        return buffer;
-    }
-}
diff --git a/core/java/com/android/internal/util/BinaryXmlSerializer.java b/core/java/com/android/internal/util/BinaryXmlSerializer.java
deleted file mode 100644
index 485430a..0000000
--- a/core/java/com/android/internal/util/BinaryXmlSerializer.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import static org.xmlpull.v1.XmlPullParser.CDSECT;
-import static org.xmlpull.v1.XmlPullParser.COMMENT;
-import static org.xmlpull.v1.XmlPullParser.DOCDECL;
-import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.END_TAG;
-import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
-import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
-import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
-import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.START_TAG;
-import static org.xmlpull.v1.XmlPullParser.TEXT;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.util.TypedXmlSerializer;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-
-/**
- * Serializer that writes XML documents using a custom binary wire protocol
- * which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
- * than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
- * <p>
- * The high-level design of the wire protocol is to directly serialize the event
- * stream, while efficiently and compactly writing strongly-typed primitives
- * delivered through the {@link TypedXmlSerializer} interface.
- * <p>
- * Each serialized event is a single byte where the lower half is a normal
- * {@link XmlPullParser} token and the upper half is an optional data type
- * signal, such as {@link #TYPE_INT}.
- * <p>
- * This serializer has some specific limitations:
- * <ul>
- * <li>Only the UTF-8 encoding is supported.
- * <li>Variable length values, such as {@code byte[]} or {@link String}, are
- * limited to 65,535 bytes in length. Note that {@link String} values are stored
- * as UTF-8 on the wire.
- * <li>Namespaces, prefixes, properties, and options are unsupported.
- * </ul>
- */
-public final class BinaryXmlSerializer implements TypedXmlSerializer {
-    /**
-     * The wire protocol always begins with a well-known magic value of
-     * {@code ABX_}, representing "Android Binary XML." The final byte is a
-     * version number which may be incremented as the protocol changes.
-     */
-    public static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
-
-    /**
-     * Internal token which represents an attribute associated with the most
-     * recent {@link #START_TAG} token.
-     */
-    static final int ATTRIBUTE = 15;
-
-    static final int TYPE_NULL = 1 << 4;
-    static final int TYPE_STRING = 2 << 4;
-    static final int TYPE_STRING_INTERNED = 3 << 4;
-    static final int TYPE_BYTES_HEX = 4 << 4;
-    static final int TYPE_BYTES_BASE64 = 5 << 4;
-    static final int TYPE_INT = 6 << 4;
-    static final int TYPE_INT_HEX = 7 << 4;
-    static final int TYPE_LONG = 8 << 4;
-    static final int TYPE_LONG_HEX = 9 << 4;
-    static final int TYPE_FLOAT = 10 << 4;
-    static final int TYPE_DOUBLE = 11 << 4;
-    static final int TYPE_BOOLEAN_TRUE = 12 << 4;
-    static final int TYPE_BOOLEAN_FALSE = 13 << 4;
-
-    private FastDataOutput mOut;
-
-    /**
-     * Stack of tags which are currently active via {@link #startTag} and which
-     * haven't been terminated via {@link #endTag}.
-     */
-    private int mTagCount = 0;
-    private String[] mTagNames;
-
-    /**
-     * Write the given token and optional {@link String} into our buffer.
-     */
-    private void writeToken(int token, @Nullable String text) throws IOException {
-        if (text != null) {
-            mOut.writeByte(token | TYPE_STRING);
-            mOut.writeUTF(text);
-        } else {
-            mOut.writeByte(token | TYPE_NULL);
-        }
-    }
-
-    @Override
-    public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-
-        mOut = FastDataOutput.obtainUsing4ByteSequences(os);
-        mOut.write(PROTOCOL_MAGIC_VERSION_0);
-
-        mTagCount = 0;
-        mTagNames = new String[8];
-    }
-
-    @Override
-    public void setOutput(Writer writer) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void flush() throws IOException {
-        if (mOut != null) {
-            mOut.flush();
-        }
-    }
-
-    @Override
-    public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
-            throws IOException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-        if (standalone != null && !standalone) {
-            throw new UnsupportedOperationException();
-        }
-        mOut.writeByte(START_DOCUMENT | TYPE_NULL);
-    }
-
-    @Override
-    public void endDocument() throws IOException {
-        mOut.writeByte(END_DOCUMENT | TYPE_NULL);
-        flush();
-
-        mOut.release();
-        mOut = null;
-    }
-
-    @Override
-    public int getDepth() {
-        return mTagCount;
-    }
-
-    @Override
-    public String getNamespace() {
-        // Namespaces are unsupported
-        return XmlPullParser.NO_NAMESPACE;
-    }
-
-    @Override
-    public String getName() {
-        return mTagNames[mTagCount - 1];
-    }
-
-    @Override
-    public XmlSerializer startTag(String namespace, String name) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (mTagCount == mTagNames.length) {
-            mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
-        }
-        mTagNames[mTagCount++] = name;
-        mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer endTag(String namespace, String name) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mTagCount--;
-        mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_STRING);
-        mOut.writeInternedUTF(name);
-        mOut.writeUTF(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeInterned(String namespace, String name, String value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        mOut.writeInternedUTF(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeShort(value.length);
-        mOut.write(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
-        mOut.writeInternedUTF(name);
-        mOut.writeShort(value.length);
-        mOut.write(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeInt(String namespace, String name, int value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_INT);
-        mOut.writeInternedUTF(name);
-        mOut.writeInt(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeIntHex(String namespace, String name, int value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeInt(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeLong(String namespace, String name, long value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_LONG);
-        mOut.writeInternedUTF(name);
-        mOut.writeLong(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeLongHex(String namespace, String name, long value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeLong(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeFloat(String namespace, String name, float value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
-        mOut.writeInternedUTF(name);
-        mOut.writeFloat(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeDouble(String namespace, String name, double value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
-        mOut.writeInternedUTF(name);
-        mOut.writeDouble(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (value) {
-            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
-            mOut.writeInternedUTF(name);
-        } else {
-            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
-            mOut.writeInternedUTF(name);
-        }
-        return this;
-    }
-
-    @Override
-    public XmlSerializer text(char[] buf, int start, int len) throws IOException {
-        writeToken(TEXT, new String(buf, start, len));
-        return this;
-    }
-
-    @Override
-    public XmlSerializer text(String text) throws IOException {
-        writeToken(TEXT, text);
-        return this;
-    }
-
-    @Override
-    public void cdsect(String text) throws IOException {
-        writeToken(CDSECT, text);
-    }
-
-    @Override
-    public void entityRef(String text) throws IOException {
-        writeToken(ENTITY_REF, text);
-    }
-
-    @Override
-    public void processingInstruction(String text) throws IOException {
-        writeToken(PROCESSING_INSTRUCTION, text);
-    }
-
-    @Override
-    public void comment(String text) throws IOException {
-        writeToken(COMMENT, text);
-    }
-
-    @Override
-    public void docdecl(String text) throws IOException {
-        writeToken(DOCDECL, text);
-    }
-
-    @Override
-    public void ignorableWhitespace(String text) throws IOException {
-        writeToken(IGNORABLE_WHITESPACE, text);
-    }
-
-    @Override
-    public void setFeature(String name, boolean state) {
-        // Quietly handle no-op features
-        if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
-            return;
-        }
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean getFeature(String name) {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setProperty(String name, Object value) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Object getProperty(String name) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setPrefix(String prefix, String namespace) {
-        // Prefixes are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getPrefix(String namespace, boolean generatePrefix) {
-        // Prefixes are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    private static IllegalArgumentException illegalNamespace() {
-        throw new IllegalArgumentException("Namespaces are not supported");
-    }
-}
diff --git a/core/java/com/android/internal/util/FastDataInput.java b/core/java/com/android/internal/util/FastDataInput.java
deleted file mode 100644
index 5117034..0000000
--- a/core/java/com/android/internal/util/FastDataInput.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import android.annotation.NonNull;
-import android.util.CharsetUtils;
-
-import dalvik.system.VMRuntime;
-
-import java.io.BufferedInputStream;
-import java.io.Closeable;
-import java.io.DataInput;
-import java.io.DataInputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Optimized implementation of {@link DataInput} which buffers data in memory
- * from the underlying {@link InputStream}.
- * <p>
- * Benchmarks have demonstrated this class is 3x more efficient than using a
- * {@link DataInputStream} with a {@link BufferedInputStream}.
- */
-public class FastDataInput implements DataInput, Closeable {
-    private static final int MAX_UNSIGNED_SHORT = 65_535;
-
-    private static final int DEFAULT_BUFFER_SIZE = 32_768;
-
-    private static AtomicReference<FastDataInput> sInCache = new AtomicReference<>();
-
-    private final VMRuntime mRuntime;
-
-    private final byte[] mBuffer;
-    private final long mBufferPtr;
-    private final int mBufferCap;
-    private final boolean mUse4ByteSequence;
-
-    private InputStream mIn;
-    private int mBufferPos;
-    private int mBufferLim;
-
-    /**
-     * Values that have been "interned" by {@link #readInternedUTF()}.
-     */
-    private int mStringRefCount = 0;
-    private String[] mStringRefs = new String[32];
-
-    /**
-     * @deprecated callers must specify {@code use4ByteSequence} so they make a
-     *             clear choice about working around a long-standing ART bug, as
-     *             described by the {@code kUtfUse4ByteSequence} comments in
-     *             {@code art/runtime/jni/jni_internal.cc}.
-     */
-    @Deprecated
-    public FastDataInput(@NonNull InputStream in, int bufferSize) {
-        this(in, bufferSize, true /* use4ByteSequence */);
-    }
-
-    public FastDataInput(@NonNull InputStream in, int bufferSize, boolean use4ByteSequence) {
-        mRuntime = VMRuntime.getRuntime();
-        mIn = Objects.requireNonNull(in);
-        if (bufferSize < 8) {
-            throw new IllegalArgumentException();
-        }
-
-        mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize);
-        mBufferPtr = mRuntime.addressOf(mBuffer);
-        mBufferCap = mBuffer.length;
-        mUse4ByteSequence = use4ByteSequence;
-    }
-
-    /**
-     * Obtain a {@link FastDataInput} configured with the given
-     * {@link InputStream} and which encodes large code-points using 3-byte
-     * sequences.
-     * <p>
-     * This <em>is</em> compatible with the {@link DataInput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataInput obtainUsing3ByteSequences(@NonNull InputStream in) {
-        return new FastDataInput(in, DEFAULT_BUFFER_SIZE, false /* use4ByteSequence */);
-    }
-
-    /**
-     * Obtain a {@link FastDataInput} configured with the given
-     * {@link InputStream} and which decodes large code-points using 4-byte
-     * sequences.
-     * <p>
-     * This <em>is not</em> compatible with the {@link DataInput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataInput obtainUsing4ByteSequences(@NonNull InputStream in) {
-        FastDataInput instance = sInCache.getAndSet(null);
-        if (instance != null) {
-            instance.setInput(in);
-            return instance;
-        }
-        return new FastDataInput(in, DEFAULT_BUFFER_SIZE, true /* use4ByteSequence */);
-    }
-
-    /**
-     * Release a {@link FastDataInput} to potentially be recycled. You must not
-     * interact with the object after releasing it.
-     */
-    public void release() {
-        mIn = null;
-        mBufferPos = 0;
-        mBufferLim = 0;
-        mStringRefCount = 0;
-
-        if (mBufferCap == DEFAULT_BUFFER_SIZE && mUse4ByteSequence) {
-            // Try to return to the cache.
-            sInCache.compareAndSet(null, this);
-        }
-    }
-
-    /**
-     * Re-initializes the object for the new input.
-     */
-    private void setInput(@NonNull InputStream in) {
-        mIn = Objects.requireNonNull(in);
-        mBufferPos = 0;
-        mBufferLim = 0;
-        mStringRefCount = 0;
-    }
-
-    private void fill(int need) throws IOException {
-        final int remain = mBufferLim - mBufferPos;
-        System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain);
-        mBufferPos = 0;
-        mBufferLim = remain;
-        need -= remain;
-
-        while (need > 0) {
-            int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim);
-            if (c == -1) {
-                throw new EOFException();
-            } else {
-                mBufferLim += c;
-                need -= c;
-            }
-        }
-    }
-
-    @Override
-    public void close() throws IOException {
-        mIn.close();
-        release();
-    }
-
-    @Override
-    public void readFully(byte[] b) throws IOException {
-        readFully(b, 0, b.length);
-    }
-
-    @Override
-    public void readFully(byte[] b, int off, int len) throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap >= len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            System.arraycopy(mBuffer, mBufferPos, b, off, len);
-            mBufferPos += len;
-        } else {
-            final int remain = mBufferLim - mBufferPos;
-            System.arraycopy(mBuffer, mBufferPos, b, off, remain);
-            mBufferPos += remain;
-            off += remain;
-            len -= remain;
-
-            while (len > 0) {
-                int c = mIn.read(b, off, len);
-                if (c == -1) {
-                    throw new EOFException();
-                } else {
-                    off += c;
-                    len -= c;
-                }
-            }
-        }
-    }
-
-    @Override
-    public String readUTF() throws IOException {
-        if (mUse4ByteSequence) {
-            return readUTFUsing4ByteSequences();
-        } else {
-            return readUTFUsing3ByteSequences();
-        }
-    }
-
-    private String readUTFUsing4ByteSequences() throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        final int len = readUnsignedShort();
-        if (mBufferCap > len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            final String res = CharsetUtils.fromModifiedUtf8Bytes(mBufferPtr, mBufferPos, len);
-            mBufferPos += len;
-            return res;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            readFully(tmp, 0, len);
-            return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
-        }
-    }
-
-    private String readUTFUsing3ByteSequences() throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        final int len = readUnsignedShort();
-        if (mBufferCap > len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            final String res = ModifiedUtf8.decode(mBuffer, new char[len], mBufferPos, len);
-            mBufferPos += len;
-            return res;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            readFully(tmp, 0, len);
-            return ModifiedUtf8.decode(tmp, new char[len], 0, len);
-        }
-    }
-
-    /**
-     * Read a {@link String} value with the additional signal that the given
-     * value is a candidate for being canonicalized, similar to
-     * {@link String#intern()}.
-     * <p>
-     * Canonicalization is implemented by writing each unique string value once
-     * the first time it appears, and then writing a lightweight {@code short}
-     * reference when that string is written again in the future.
-     *
-     * @see FastDataOutput#writeInternedUTF(String)
-     */
-    public @NonNull String readInternedUTF() throws IOException {
-        final int ref = readUnsignedShort();
-        if (ref == MAX_UNSIGNED_SHORT) {
-            final String s = readUTF();
-
-            // We can only safely intern when we have remaining values; if we're
-            // full we at least sent the string value above
-            if (mStringRefCount < MAX_UNSIGNED_SHORT) {
-                if (mStringRefCount == mStringRefs.length) {
-                    mStringRefs = Arrays.copyOf(mStringRefs,
-                            mStringRefCount + (mStringRefCount >> 1));
-                }
-                mStringRefs[mStringRefCount++] = s;
-            }
-
-            return s;
-        } else {
-            return mStringRefs[ref];
-        }
-    }
-
-    @Override
-    public boolean readBoolean() throws IOException {
-        return readByte() != 0;
-    }
-
-    /**
-     * Returns the same decoded value as {@link #readByte()} but without
-     * actually consuming the underlying data.
-     */
-    public byte peekByte() throws IOException {
-        if (mBufferLim - mBufferPos < 1) fill(1);
-        return mBuffer[mBufferPos];
-    }
-
-    @Override
-    public byte readByte() throws IOException {
-        if (mBufferLim - mBufferPos < 1) fill(1);
-        return mBuffer[mBufferPos++];
-    }
-
-    @Override
-    public int readUnsignedByte() throws IOException {
-        return Byte.toUnsignedInt(readByte());
-    }
-
-    @Override
-    public short readShort() throws IOException {
-        if (mBufferLim - mBufferPos < 2) fill(2);
-        return (short) (((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                        ((mBuffer[mBufferPos++] & 0xff) <<  0));
-    }
-
-    @Override
-    public int readUnsignedShort() throws IOException {
-        return Short.toUnsignedInt((short) readShort());
-    }
-
-    @Override
-    public char readChar() throws IOException {
-        return (char) readShort();
-    }
-
-    @Override
-    public int readInt() throws IOException {
-        if (mBufferLim - mBufferPos < 4) fill(4);
-        return (((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0));
-    }
-
-    @Override
-    public long readLong() throws IOException {
-        if (mBufferLim - mBufferPos < 8) fill(8);
-        int h = ((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0);
-        int l = ((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0);
-        return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
-    }
-
-    @Override
-    public float readFloat() throws IOException {
-        return Float.intBitsToFloat(readInt());
-    }
-
-    @Override
-    public double readDouble() throws IOException {
-        return Double.longBitsToDouble(readLong());
-    }
-
-    @Override
-    public int skipBytes(int n) throws IOException {
-        // Callers should read data piecemeal
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String readLine() throws IOException {
-        // Callers should read data piecemeal
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/core/java/com/android/internal/util/FastDataOutput.java b/core/java/com/android/internal/util/FastDataOutput.java
deleted file mode 100644
index 5b6075e..0000000
--- a/core/java/com/android/internal/util/FastDataOutput.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import android.annotation.NonNull;
-import android.util.CharsetUtils;
-
-import dalvik.system.VMRuntime;
-
-import java.io.BufferedOutputStream;
-import java.io.Closeable;
-import java.io.DataOutput;
-import java.io.DataOutputStream;
-import java.io.Flushable;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Optimized implementation of {@link DataOutput} which buffers data in memory
- * before flushing to the underlying {@link OutputStream}.
- * <p>
- * Benchmarks have demonstrated this class is 2x more efficient than using a
- * {@link DataOutputStream} with a {@link BufferedOutputStream}.
- */
-public class FastDataOutput implements DataOutput, Flushable, Closeable {
-    private static final int MAX_UNSIGNED_SHORT = 65_535;
-
-    private static final int DEFAULT_BUFFER_SIZE = 32_768;
-
-    private static AtomicReference<FastDataOutput> sOutCache = new AtomicReference<>();
-
-    private final VMRuntime mRuntime;
-
-    private final byte[] mBuffer;
-    private final long mBufferPtr;
-    private final int mBufferCap;
-    private final boolean mUse4ByteSequence;
-
-    private OutputStream mOut;
-    private int mBufferPos;
-
-    /**
-     * Values that have been "interned" by {@link #writeInternedUTF(String)}.
-     */
-    private final HashMap<String, Integer> mStringRefs = new HashMap<>();
-
-    /**
-     * @deprecated callers must specify {@code use4ByteSequence} so they make a
-     *             clear choice about working around a long-standing ART bug, as
-     *             described by the {@code kUtfUse4ByteSequence} comments in
-     *             {@code art/runtime/jni/jni_internal.cc}.
-     */
-    @Deprecated
-    public FastDataOutput(@NonNull OutputStream out, int bufferSize) {
-        this(out, bufferSize, true /* use4ByteSequence */);
-    }
-
-    public FastDataOutput(@NonNull OutputStream out, int bufferSize, boolean use4ByteSequence) {
-        mRuntime = VMRuntime.getRuntime();
-        if (bufferSize < 8) {
-            throw new IllegalArgumentException();
-        }
-
-        mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize);
-        mBufferPtr = mRuntime.addressOf(mBuffer);
-        mBufferCap = mBuffer.length;
-        mUse4ByteSequence = use4ByteSequence;
-
-        setOutput(out);
-    }
-
-    /**
-     * Obtain a {@link FastDataOutput} configured with the given
-     * {@link OutputStream} and which encodes large code-points using 3-byte
-     * sequences.
-     * <p>
-     * This <em>is</em> compatible with the {@link DataOutput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataOutput obtainUsing3ByteSequences(@NonNull OutputStream out) {
-        return new FastDataOutput(out, DEFAULT_BUFFER_SIZE, false /* use4ByteSequence */);
-    }
-
-    /**
-     * Obtain a {@link FastDataOutput} configured with the given
-     * {@link OutputStream} and which encodes large code-points using 4-byte
-     * sequences.
-     * <p>
-     * This <em>is not</em> compatible with the {@link DataOutput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataOutput obtainUsing4ByteSequences(@NonNull OutputStream out) {
-        FastDataOutput instance = sOutCache.getAndSet(null);
-        if (instance != null) {
-            instance.setOutput(out);
-            return instance;
-        }
-        return new FastDataOutput(out, DEFAULT_BUFFER_SIZE, true /* use4ByteSequence */);
-    }
-
-    /**
-     * Release a {@link FastDataOutput} to potentially be recycled. You must not
-     * interact with the object after releasing it.
-     */
-    public void release() {
-        if (mBufferPos > 0) {
-            throw new IllegalStateException("Lingering data, call flush() before releasing.");
-        }
-
-        mOut = null;
-        mBufferPos = 0;
-        mStringRefs.clear();
-
-        if (mBufferCap == DEFAULT_BUFFER_SIZE && mUse4ByteSequence) {
-            // Try to return to the cache.
-            sOutCache.compareAndSet(null, this);
-        }
-    }
-
-    /**
-     * Re-initializes the object for the new output.
-     */
-    private void setOutput(@NonNull OutputStream out) {
-        mOut = Objects.requireNonNull(out);
-        mBufferPos = 0;
-        mStringRefs.clear();
-    }
-
-    private void drain() throws IOException {
-        if (mBufferPos > 0) {
-            mOut.write(mBuffer, 0, mBufferPos);
-            mBufferPos = 0;
-        }
-    }
-
-    @Override
-    public void flush() throws IOException {
-        drain();
-        mOut.flush();
-    }
-
-    @Override
-    public void close() throws IOException {
-        mOut.close();
-        release();
-    }
-
-    @Override
-    public void write(int b) throws IOException {
-        writeByte(b);
-    }
-
-    @Override
-    public void write(byte[] b) throws IOException {
-        write(b, 0, b.length);
-    }
-
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException {
-        if (mBufferCap < len) {
-            drain();
-            mOut.write(b, off, len);
-        } else {
-            if (mBufferCap - mBufferPos < len) drain();
-            System.arraycopy(b, off, mBuffer, mBufferPos, len);
-            mBufferPos += len;
-        }
-    }
-
-    @Override
-    public void writeUTF(String s) throws IOException {
-        if (mUse4ByteSequence) {
-            writeUTFUsing4ByteSequences(s);
-        } else {
-            writeUTFUsing3ByteSequences(s);
-        }
-    }
-
-    private void writeUTFUsing4ByteSequences(String s) throws IOException {
-        // Attempt to write directly to buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap - mBufferPos < 2 + s.length()) drain();
-
-        // Magnitude of this returned value indicates the number of bytes
-        // required to encode the string; sign indicates success/failure
-        int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap);
-        if (Math.abs(len) > MAX_UNSIGNED_SHORT) {
-            throw new IOException("Modified UTF-8 length too large: " + len);
-        }
-
-        if (len >= 0) {
-            // Positive value indicates the string was encoded into the buffer
-            // successfully, so we only need to prefix with length
-            writeShort(len);
-            mBufferPos += len;
-        } else {
-            // Negative value indicates buffer was too small and we need to
-            // allocate a temporary buffer for encoding
-            len = -len;
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
-            writeShort(len);
-            write(tmp, 0, len);
-        }
-    }
-
-    private void writeUTFUsing3ByteSequences(String s) throws IOException {
-        final int len = (int) ModifiedUtf8.countBytes(s, false);
-        if (len > MAX_UNSIGNED_SHORT) {
-            throw new IOException("Modified UTF-8 length too large: " + len);
-        }
-
-        // Attempt to write directly to buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap >= 2 + len) {
-            if (mBufferCap - mBufferPos < 2 + len) drain();
-            writeShort(len);
-            ModifiedUtf8.encode(mBuffer, mBufferPos, s);
-            mBufferPos += len;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            ModifiedUtf8.encode(tmp, 0, s);
-            writeShort(len);
-            write(tmp, 0, len);
-        }
-    }
-
-    /**
-     * Write a {@link String} value with the additional signal that the given
-     * value is a candidate for being canonicalized, similar to
-     * {@link String#intern()}.
-     * <p>
-     * Canonicalization is implemented by writing each unique string value once
-     * the first time it appears, and then writing a lightweight {@code short}
-     * reference when that string is written again in the future.
-     *
-     * @see FastDataInput#readInternedUTF()
-     */
-    public void writeInternedUTF(@NonNull String s) throws IOException {
-        Integer ref = mStringRefs.get(s);
-        if (ref != null) {
-            writeShort(ref);
-        } else {
-            writeShort(MAX_UNSIGNED_SHORT);
-            writeUTF(s);
-
-            // We can only safely intern when we have remaining values; if we're
-            // full we at least sent the string value above
-            ref = mStringRefs.size();
-            if (ref < MAX_UNSIGNED_SHORT) {
-                mStringRefs.put(s, ref);
-            }
-        }
-    }
-
-    @Override
-    public void writeBoolean(boolean v) throws IOException {
-        writeByte(v ? 1 : 0);
-    }
-
-    @Override
-    public void writeByte(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 1) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeShort(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 2) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeChar(int v) throws IOException {
-        writeShort((short) v);
-    }
-
-    @Override
-    public void writeInt(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 4) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeLong(long v) throws IOException {
-        if (mBufferCap - mBufferPos < 8) drain();
-        int i = (int) (v >> 32);
-        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
-        i = (int) v;
-        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeFloat(float v) throws IOException {
-        writeInt(Float.floatToIntBits(v));
-    }
-
-    @Override
-    public void writeDouble(double v) throws IOException {
-        writeLong(Double.doubleToLongBits(v));
-    }
-
-    @Override
-    public void writeBytes(String s) throws IOException {
-        // Callers should use writeUTF()
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void writeChars(String s) throws IOException {
-        // Callers should use writeUTF()
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 8fcb6d5..4b7b91c 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -468,7 +468,7 @@
         boolean shouldSample;
         int traceThreshold;
         synchronized (mLock) {
-            shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
+            shouldSample = ThreadLocalRandom.current().nextInt(mSamplingInterval) == 0;
             traceThreshold = mTraceThresholdPerAction[action];
         }
 
diff --git a/core/java/com/android/internal/util/ModifiedUtf8.java b/core/java/com/android/internal/util/ModifiedUtf8.java
deleted file mode 100644
index a144c00..0000000
--- a/core/java/com/android/internal/util/ModifiedUtf8.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You 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.util;
-
-import java.io.UTFDataFormatException;
-
-public class ModifiedUtf8 {
-    /**
-     * Decodes a byte array containing <i>modified UTF-8</i> bytes into a string.
-     *
-     * <p>Note that although this method decodes the (supposedly impossible) zero byte to U+0000,
-     * that's what the RI does too.
-     */
-    public static String decode(byte[] in, char[] out, int offset, int utfSize)
-            throws UTFDataFormatException {
-        int count = 0, s = 0, a;
-        while (count < utfSize) {
-            if ((out[s] = (char) in[offset + count++]) < '\u0080') {
-                s++;
-            } else if (((a = out[s]) & 0xe0) == 0xc0) {
-                if (count >= utfSize) {
-                    throw new UTFDataFormatException("bad second byte at " + count);
-                }
-                int b = in[offset + count++];
-                if ((b & 0xC0) != 0x80) {
-                    throw new UTFDataFormatException("bad second byte at " + (count - 1));
-                }
-                out[s++] = (char) (((a & 0x1F) << 6) | (b & 0x3F));
-            } else if ((a & 0xf0) == 0xe0) {
-                if (count + 1 >= utfSize) {
-                    throw new UTFDataFormatException("bad third byte at " + (count + 1));
-                }
-                int b = in[offset + count++];
-                int c = in[offset + count++];
-                if (((b & 0xC0) != 0x80) || ((c & 0xC0) != 0x80)) {
-                    throw new UTFDataFormatException("bad second or third byte at " + (count - 2));
-                }
-                out[s++] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F));
-            } else {
-                throw new UTFDataFormatException("bad byte at " + (count - 1));
-            }
-        }
-        return new String(out, 0, s);
-    }
-
-    /**
-     * Returns the number of bytes the modified UTF-8 representation of 's' would take. Note
-     * that this is just the space for the bytes representing the characters, not the length
-     * which precedes those bytes, because different callers represent the length differently,
-     * as two, four, or even eight bytes. If {@code shortLength} is true, we'll throw an
-     * exception if the string is too long for its length to be represented by a short.
-     */
-    public static long countBytes(String s, boolean shortLength) throws UTFDataFormatException {
-        long result = 0;
-        final int length = s.length();
-        for (int i = 0; i < length; ++i) {
-            char ch = s.charAt(i);
-            if (ch != 0 && ch <= 127) { // U+0000 uses two bytes.
-                ++result;
-            } else if (ch <= 2047) {
-                result += 2;
-            } else {
-                result += 3;
-            }
-            if (shortLength && result > 65535) {
-                throw new UTFDataFormatException("String more than 65535 UTF bytes long");
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Encodes the <i>modified UTF-8</i> bytes corresponding to string {@code s} into the
-     * byte array {@code dst}, starting at the given {@code offset}.
-     */
-    public static void encode(byte[] dst, int offset, String s) {
-        final int length = s.length();
-        for (int i = 0; i < length; i++) {
-            char ch = s.charAt(i);
-            if (ch != 0 && ch <= 127) { // U+0000 uses two bytes.
-                dst[offset++] = (byte) ch;
-            } else if (ch <= 2047) {
-                dst[offset++] = (byte) (0xc0 | (0x1f & (ch >> 6)));
-                dst[offset++] = (byte) (0x80 | (0x3f & ch));
-            } else {
-                dst[offset++] = (byte) (0xe0 | (0x0f & (ch >> 12)));
-                dst[offset++] = (byte) (0x80 | (0x3f & (ch >> 6)));
-                dst[offset++] = (byte) (0x80 | (0x3f & ch));
-            }
-        }
-    }
-
-    private ModifiedUtf8() {
-    }
-}
diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java
index de6b65f3..af5e3b3 100644
--- a/core/java/com/android/internal/util/XmlUtils.java
+++ b/core/java/com/android/internal/util/XmlUtils.java
@@ -25,10 +25,11 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Base64;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.util.HexEncoding;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index fe77236..2ac4309 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.view;
 
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.input.InputManager;
 import android.os.Bundle;
@@ -31,6 +32,7 @@
 import android.view.PointerIcon;
 import android.view.ScrollCaptureResponse;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -66,11 +68,13 @@
     }
 
     @Override
-    public void showInsets(@InsetsType int types, boolean fromIme) {
+    public void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     @Override
-    public void hideInsets(@InsetsType int types, boolean fromIme) {
+    public void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     @Override
diff --git a/core/java/com/android/internal/view/BaseSurfaceHolder.java b/core/java/com/android/internal/view/BaseSurfaceHolder.java
index 32ce0fe..1ae1307 100644
--- a/core/java/com/android/internal/view/BaseSurfaceHolder.java
+++ b/core/java/com/android/internal/view/BaseSurfaceHolder.java
@@ -241,4 +241,4 @@
         mSurfaceFrame.right = width;
         mSurfaceFrame.bottom = height;
     }
-};
+}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 423642a..00bc3f2 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.view;
 
 import android.os.ResultReceiver;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 import android.view.inputmethod.EditorInfo;
@@ -54,11 +55,12 @@
             + "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
     InputMethodSubtype getLastInputMethodSubtype(int userId);
 
-    boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
-            int lastClickToolType, in @nullable ResultReceiver resultReceiver, int reason);
-    boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken, int flags,
+    boolean showSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+            in @nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
             in @nullable ResultReceiver resultReceiver, int reason);
-
+    boolean hideSoftInput(in IInputMethodClient client, @nullable IBinder windowToken,
+            in @nullable ImeTracker.Token statsToken, int flags,
+            in @nullable ResultReceiver resultReceiver, int reason);
     // If windowToken is null, this just does startInput().  Otherwise this reports that a window
     // has gained focus, and if 'editorInfo' is non-null then also does startInput.
     // @NonNull
@@ -81,8 +83,7 @@
     @EnforcePermission("WRITE_SECURE_SETTINGS")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
             + "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
-    void showInputMethodPickerFromSystem(in IInputMethodClient client,
-            int auxiliarySubtypeMode, int displayId);
+    void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId);
 
     @EnforcePermission("TEST_INPUT_METHOD")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
@@ -147,9 +148,9 @@
     boolean isStylusHandwritingAvailableAsUser(int userId);
 
     /** add virtual stylus id for test Stylus handwriting session **/
-    @EnforcePermission("INJECT_EVENTS")
+    @EnforcePermission("TEST_INPUT_METHOD")
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
-            + "android.Manifest.permission.INJECT_EVENTS)")
+            + "android.Manifest.permission.TEST_INPUT_METHOD)")
     void addVirtualStylusIdForTestSession(in IInputMethodClient client);
 
     /** Set a stylus idle-timeout after which handwriting {@code InkWindow} will be removed. */
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 5b08bb1..6063c90 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -54,6 +54,12 @@
     // TODO(b/183140900) split store escrow key errors into detailed ones.
 
     /**
+     * This is called when Weaver is guaranteed to be available (if the device supports Weaver).
+     * It does any synthetic password related work that was delayed from earlier in the boot.
+     */
+    public abstract void onThirdPartyAppsStarted();
+
+    /**
      * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e.
      * doesn't have an LSKF.
      * <p>
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index d59a51a..cc3d906 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -34,6 +34,8 @@
         "-Wno-error=deprecated-declarations",
         "-Wunused",
         "-Wunreachable-code",
+
+        "-DNAMESPACE_FOR_HASH_FUNCTIONS=farmhash",
     ],
 
     cppflags: ["-Wno-conversion-null"],
@@ -212,6 +214,7 @@
                 "android_content_res_ResourceTimer.cpp",
                 "android_security_Scrypt.cpp",
                 "com_android_internal_content_om_OverlayConfig.cpp",
+                "com_android_internal_expresslog_Counter.cpp",
                 "com_android_internal_net_NetworkUtilsInternal.cpp",
                 "com_android_internal_os_ClassLoaderFactory.cpp",
                 "com_android_internal_os_FuseAppLoop.cpp",
@@ -247,6 +250,7 @@
                 "libscrypt_static",
                 "libstatssocket_lazy",
                 "libskia",
+                "libtextclassifier_hash_static",
             ],
 
             shared_libs: [
@@ -329,7 +333,7 @@
             header_libs: [
                 "bionic_libc_platform_headers",
                 "dnsproxyd_protocol_headers",
-                "libandroid_runtime_vm_headers",
+                "libtextclassifier_hash_headers",
             ],
         },
         host: {
@@ -418,24 +422,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..0798110 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -19,10 +19,10 @@
 #define LOG_NDEBUG 1
 
 #include <android-base/macros.h>
+#include <android-base/parsebool.h>
 #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>
@@ -53,6 +53,8 @@
 using namespace android;
 using android::base::GetBoolProperty;
 using android::base::GetProperty;
+using android::base::ParseBool;
+using android::base::ParseBoolResult;
 
 extern int register_android_os_Binder(JNIEnv* env);
 extern int register_android_os_Process(JNIEnv* env);
@@ -195,6 +197,7 @@
 extern int register_com_android_internal_content_F2fsUtils(JNIEnv* env);
 extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env);
 extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env);
+extern int register_com_android_internal_expresslog_Counter(JNIEnv* env);
 extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
 extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env);
 extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
@@ -703,17 +706,24 @@
 
     // Read if we are using the profile configuration, do this at the start since the last ART args
     // take precedence.
-    property_get("dalvik.vm.profilebootclasspath", propBuf, "");
-    std::string profile_boot_class_path_flag = propBuf;
-    // Empty means the property is unset and we should default to the phenotype property.
-    // The possible values are {"true", "false", ""}
-    if (profile_boot_class_path_flag.empty()) {
-        profile_boot_class_path_flag = server_configurable_flags::GetServerConfigurableFlag(
-                RUNTIME_NATIVE_BOOT_NAMESPACE,
-                PROFILE_BOOT_CLASS_PATH,
-                /*default_value=*/ "");
+    std::string profile_boot_class_path_flag =
+            server_configurable_flags::GetServerConfigurableFlag(RUNTIME_NATIVE_BOOT_NAMESPACE,
+                                                                 PROFILE_BOOT_CLASS_PATH,
+                                                                 /*default_value=*/"");
+    bool profile_boot_class_path;
+    switch (ParseBool(profile_boot_class_path_flag)) {
+        case ParseBoolResult::kError:
+            // Default to the system property.
+            profile_boot_class_path =
+                    GetBoolProperty("dalvik.vm.profilebootclasspath", /*default_value=*/false);
+            break;
+        case ParseBoolResult::kTrue:
+            profile_boot_class_path = true;
+            break;
+        case ParseBoolResult::kFalse:
+            profile_boot_class_path = false;
+            break;
     }
-    const bool profile_boot_class_path = (profile_boot_class_path_flag == "true");
     if (profile_boot_class_path) {
         addOption("-Xcompiler-option");
         addOption("--count-hotness-in-compiled-code");
@@ -1576,6 +1586,7 @@
         REG_JNI(register_android_os_SharedMemory),
         REG_JNI(register_android_os_incremental_IncrementalManager),
         REG_JNI(register_com_android_internal_content_om_OverlayConfig),
+        REG_JNI(register_com_android_internal_expresslog_Counter),
         REG_JNI(register_com_android_internal_net_NetworkUtilsInternal),
         REG_JNI(register_com_android_internal_os_ClassLoaderFactory),
         REG_JNI(register_com_android_internal_os_LongArrayMultiStateCounter),
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index a068008..9f39c32 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -99,3 +99,5 @@
 # PM
 per-file com_android_internal_content_* = file:/PACKAGE_MANAGER_OWNERS
 
+# Stats/expresslog
+per-file *expresslog* = file:/services/core/java/com/android/server/stats/OWNERS
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 1520ea5..0381510 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -71,10 +71,12 @@
         }
     }
 
-    void onTransactionHang(bool isGpuHang) {
+    void onTransactionHang(const std::string& reason) {
         if (mTransactionHangObject) {
+            JNIEnv* env = getenv(mVm);
+            ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str()));
             getenv(mVm)->CallVoidMethod(mTransactionHangObject,
-                                        gTransactionHangCallback.onTransactionHang, isGpuHang);
+                                        gTransactionHangCallback.onTransactionHang, jReason.get());
         }
     }
 
@@ -177,7 +179,7 @@
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
     return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl));
 }
-  
+
 static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr,
                                              jobject transactionHangCallback) {
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
@@ -186,9 +188,8 @@
     } else {
         sp<TransactionHangCallbackWrapper> wrapper =
                 new TransactionHangCallbackWrapper{env, transactionHangCallback};
-        queue->setTransactionHangCallback([wrapper](bool isGpuHang) {
-            wrapper->onTransactionHang(isGpuHang);
-        });
+        queue->setTransactionHangCallback(
+                [wrapper](const std::string& reason) { wrapper->onTransactionHang(reason); });
     }
 }
 
@@ -236,7 +237,8 @@
     jclass transactionHangClass =
             FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback");
     gTransactionHangCallback.onTransactionHang =
-            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V");
+            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang",
+                             "(Ljava/lang/String;)V");
 
     return 0;
 }
diff --git a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
index 09f3a72..2437a51 100644
--- a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
+++ b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
@@ -89,6 +89,24 @@
 
 extern "C" {
 
+static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
+    ALOGV("nativeDetectSurfaceType");
+    sp<ANativeWindow> anw;
+    if ((anw = getNativeWindow(env, surface)) == NULL) {
+        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
+        return BAD_VALUE;
+    }
+    int32_t fmt = 0;
+    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &fmt);
+    if (err != NO_ERROR) {
+        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
+              strerror(-err), err);
+        OVERRIDE_SURFACE_ERROR(err);
+        return err;
+    }
+    return fmt;
+}
+
 static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) {
     ALOGV("nativeDetectSurfaceDataspace");
     sp<ANativeWindow> anw;
@@ -107,27 +125,6 @@
     return fmt;
 }
 
-static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
-    ALOGV("nativeDetectSurfaceType");
-    sp<ANativeWindow> anw;
-    if ((anw = getNativeWindow(env, surface)) == NULL) {
-        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
-        return BAD_VALUE;
-    }
-    int32_t halFmt = 0;
-    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &halFmt);
-    if (err != NO_ERROR) {
-        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
-              strerror(-err), err);
-        OVERRIDE_SURFACE_ERROR(err);
-        return err;
-    }
-    int32_t dataspace = SurfaceUtils_nativeDetectSurfaceDataspace(env, thiz, surface);
-    int32_t fmt = static_cast<int32_t>(
-            mapHalFormatDataspaceToPublicFormat(halFmt, static_cast<android_dataspace>(dataspace)));
-    return fmt;
-}
-
 static jint SurfaceUtils_nativeDetectSurfaceDimens(JNIEnv* env, jobject thiz, jobject surface,
                                                    jintArray dimens) {
     ALOGV("nativeGetSurfaceDimens");
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_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp
index d05a24f..ac1401d 100644
--- a/core/jni/android_os_PerformanceHintManager.cpp
+++ b/core/jni/android_os_PerformanceHintManager.cpp
@@ -40,6 +40,7 @@
 typedef void (*APH_updateTargetWorkDuration)(APerformanceHintSession*, int64_t);
 typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t);
 typedef void (*APH_closeSession)(APerformanceHintSession* session);
+typedef void (*APH_sendHint)(APerformanceHintSession*, int32_t);
 
 bool gAPerformanceHintBindingInitialized = false;
 APH_getManager gAPH_getManagerFn = nullptr;
@@ -48,6 +49,7 @@
 APH_updateTargetWorkDuration gAPH_updateTargetWorkDurationFn = nullptr;
 APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr;
 APH_closeSession gAPH_closeSessionFn = nullptr;
+APH_sendHint gAPH_sendHintFn = nullptr;
 
 void ensureAPerformanceHintBindingInitialized() {
     if (gAPerformanceHintBindingInitialized) return;
@@ -88,6 +90,11 @@
     LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr,
                         "Failed to find required symbol APerformanceHint_closeSession!");
 
+    gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint");
+    LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr,
+                        "Failed to find required symbol "
+                        "APerformanceHint_sendHint!");
+
     gAPerformanceHintBindingInitialized = true;
 }
 
@@ -138,6 +145,11 @@
     gAPH_closeSessionFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr));
 }
 
+static void nativeSendHint(JNIEnv* env, jclass clazz, jlong nativeSessionPtr, jint hint) {
+    ensureAPerformanceHintBindingInitialized();
+    gAPH_sendHintFn(reinterpret_cast<APerformanceHintSession*>(nativeSessionPtr), hint);
+}
+
 static const JNINativeMethod gPerformanceHintMethods[] = {
         {"nativeAcquireManager", "()J", (void*)nativeAcquireManager},
         {"nativeGetPreferredUpdateRateNanos", "(J)J", (void*)nativeGetPreferredUpdateRateNanos},
@@ -145,6 +157,7 @@
         {"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
         {"nativeReportActualWorkDuration", "(JJ)V", (void*)nativeReportActualWorkDuration},
         {"nativeCloseSession", "(J)V", (void*)nativeCloseSession},
+        {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
 };
 
 int register_android_os_PerformanceHintManager(JNIEnv* env) {
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 9f88f33..01837f4 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -25,6 +25,7 @@
 #include <inttypes.h>
 #include <mutex>
 #include <stdio.h>
+#include <string>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
@@ -880,7 +881,7 @@
         case FAILED_TRANSACTION: {
             ALOGE("!!! FAILED BINDER TRANSACTION !!!  (parcel size = %d)", parcelSize);
             const char* exceptionToThrow;
-            char msg[128];
+            std::string msg;
             // TransactionTooLargeException is a checked exception, only throw from certain methods.
             // TODO(b/28321379): Transaction size is the most common cause for FAILED_TRANSACTION
             //        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
@@ -890,7 +891,7 @@
             if (canThrowRemoteException && parcelSize > 200*1024) {
                 // bona fide large payload
                 exceptionToThrow = "android/os/TransactionTooLargeException";
-                snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);
+                msg = base::StringPrintf("data parcel size %d bytes", parcelSize);
             } else {
                 // Heuristic: a payload smaller than this threshold "shouldn't" be too
                 // big, so it's probably some other, more subtle problem.  In practice
@@ -899,11 +900,10 @@
                 exceptionToThrow = (canThrowRemoteException)
                         ? "android/os/DeadObjectException"
                         : "java/lang/RuntimeException";
-                snprintf(msg, sizeof(msg) - 1,
-                         "Transaction failed on small parcel; remote process probably died, but "
-                         "this could also be caused by running out of binder buffer space");
+                msg = "Transaction failed on small parcel; remote process probably died, but "
+                      "this could also be caused by running out of binder buffer space";
             }
-            jniThrowException(env, exceptionToThrow, msg);
+            jniThrowException(env, exceptionToThrow, msg.c_str());
         } break;
         case FDS_NOT_ALLOWED:
             jniThrowException(env, "java/lang/RuntimeException",
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/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp
index 2b932cb..98814bf 100644
--- a/core/jni/android_view_InputEventReceiver.cpp
+++ b/core/jni/android_view_InputEventReceiver.cpp
@@ -380,8 +380,8 @@
                 if (kDebugDispatchCycle) {
                     ALOGD("channel '%s' ~ Received motion event.", getInputChannelName().c_str());
                 }
-                MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
-                if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
+                const MotionEvent& motionEvent = static_cast<const MotionEvent&>(*inputEvent);
+                if ((motionEvent.getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
                     *outConsumedBatch = true;
                 }
                 inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index a30935b..80df0ea 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -82,7 +82,7 @@
             reinterpret_cast<jlong>(event));
 }
 
-jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent* event) {
+jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& event) {
     jobject eventObj = env->CallStaticObjectMethod(gMotionEventClassInfo.clazz,
             gMotionEventClassInfo.obtain);
     if (env->ExceptionCheck() || !eventObj) {
@@ -98,7 +98,7 @@
         android_view_MotionEvent_setNativePtr(env, eventObj, destEvent);
     }
 
-    destEvent->copyFrom(event, true);
+    destEvent->copyFrom(&event, true);
     return eventObj;
 }
 
diff --git a/core/jni/android_view_MotionEvent.h b/core/jni/android_view_MotionEvent.h
index 9ce4bf3..32a280e 100644
--- a/core/jni/android_view_MotionEvent.h
+++ b/core/jni/android_view_MotionEvent.h
@@ -26,7 +26,7 @@
 
 /* Obtains an instance of a DVM MotionEvent object as a copy of a native MotionEvent instance.
  * Returns NULL on error. */
-extern jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent* event);
+extern jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& event);
 
 /* Gets the underlying native MotionEvent instance within a DVM MotionEvent object.
  * Returns NULL if the event is NULL or if it is uninitialized. */
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index e36815c..eaec58b 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -190,12 +190,24 @@
 static struct {
     jclass clazz;
     jmethodID ctor;
+    jfieldID min;
+    jfieldID max;
+} gRefreshRateRangeClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctor;
+    jfieldID physical;
+    jfieldID render;
+} gRefreshRateRangesClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctor;
     jfieldID defaultMode;
     jfieldID allowGroupSwitching;
-    jfieldID primaryRefreshRateMin;
-    jfieldID primaryRefreshRateMax;
-    jfieldID appRequestRefreshRateMin;
-    jfieldID appRequestRefreshRateMax;
+    jfieldID primaryRanges;
+    jfieldID appRequestRanges;
 } gDesiredDisplayModeSpecsClassInfo;
 
 static struct {
@@ -1195,30 +1207,35 @@
     sp<IBinder> token(ibinderForJavaObject(env, tokenObj));
     if (token == nullptr) return JNI_FALSE;
 
-    ui::DisplayModeId defaultMode = env->GetIntField(DesiredDisplayModeSpecs,
-                                                     gDesiredDisplayModeSpecsClassInfo.defaultMode);
-    jboolean allowGroupSwitching =
+    const auto makeRanges = [env](jobject obj) {
+        const auto makeRange = [env](jobject obj) {
+            gui::DisplayModeSpecs::RefreshRateRanges::RefreshRateRange range;
+            range.min = env->GetFloatField(obj, gRefreshRateRangeClassInfo.min);
+            range.max = env->GetFloatField(obj, gRefreshRateRangeClassInfo.max);
+            return range;
+        };
+
+        gui::DisplayModeSpecs::RefreshRateRanges ranges;
+        ranges.physical = makeRange(env->GetObjectField(obj, gRefreshRateRangesClassInfo.physical));
+        ranges.render = makeRange(env->GetObjectField(obj, gRefreshRateRangesClassInfo.render));
+        return ranges;
+    };
+
+    gui::DisplayModeSpecs specs;
+    specs.defaultMode = env->GetIntField(DesiredDisplayModeSpecs,
+                                         gDesiredDisplayModeSpecsClassInfo.defaultMode);
+    specs.allowGroupSwitching =
             env->GetBooleanField(DesiredDisplayModeSpecs,
                                  gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching);
-    jfloat primaryRefreshRateMin =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMin);
-    jfloat primaryRefreshRateMax =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMax);
-    jfloat appRequestRefreshRateMin =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMin);
-    jfloat appRequestRefreshRateMax =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMax);
 
-    size_t result = SurfaceComposerClient::setDesiredDisplayModeSpecs(token, defaultMode,
-                                                                      allowGroupSwitching,
-                                                                      primaryRefreshRateMin,
-                                                                      primaryRefreshRateMax,
-                                                                      appRequestRefreshRateMin,
-                                                                      appRequestRefreshRateMax);
+    specs.primaryRanges =
+            makeRanges(env->GetObjectField(DesiredDisplayModeSpecs,
+                                           gDesiredDisplayModeSpecsClassInfo.primaryRanges));
+    specs.appRequestRanges =
+            makeRanges(env->GetObjectField(DesiredDisplayModeSpecs,
+                                           gDesiredDisplayModeSpecsClassInfo.appRequestRanges));
+
+    size_t result = SurfaceComposerClient::setDesiredDisplayModeSpecs(token, specs);
     return result == NO_ERROR ? JNI_TRUE : JNI_FALSE;
 }
 
@@ -1226,24 +1243,26 @@
     sp<IBinder> token(ibinderForJavaObject(env, tokenObj));
     if (token == nullptr) return nullptr;
 
-    ui::DisplayModeId defaultMode;
-    bool allowGroupSwitching;
-    float primaryRefreshRateMin;
-    float primaryRefreshRateMax;
-    float appRequestRefreshRateMin;
-    float appRequestRefreshRateMax;
-    if (SurfaceComposerClient::getDesiredDisplayModeSpecs(token, &defaultMode, &allowGroupSwitching,
-                                                          &primaryRefreshRateMin,
-                                                          &primaryRefreshRateMax,
-                                                          &appRequestRefreshRateMin,
-                                                          &appRequestRefreshRateMax) != NO_ERROR) {
+    const auto rangesToJava = [env](const gui::DisplayModeSpecs::RefreshRateRanges& ranges) {
+        const auto rangeToJava =
+                [env](const gui::DisplayModeSpecs::RefreshRateRanges::RefreshRateRange& range) {
+                    return env->NewObject(gRefreshRateRangeClassInfo.clazz,
+                                          gRefreshRateRangeClassInfo.ctor, range.min, range.max);
+                };
+
+        return env->NewObject(gRefreshRateRangesClassInfo.clazz, gRefreshRateRangesClassInfo.ctor,
+                              rangeToJava(ranges.physical), rangeToJava(ranges.render));
+    };
+
+    gui::DisplayModeSpecs specs;
+    if (SurfaceComposerClient::getDesiredDisplayModeSpecs(token, &specs) != NO_ERROR) {
         return nullptr;
     }
 
     return env->NewObject(gDesiredDisplayModeSpecsClassInfo.clazz,
-                          gDesiredDisplayModeSpecsClassInfo.ctor, defaultMode, allowGroupSwitching,
-                          primaryRefreshRateMin, primaryRefreshRateMax, appRequestRefreshRateMin,
-                          appRequestRefreshRateMax);
+                          gDesiredDisplayModeSpecsClassInfo.ctor, specs.defaultMode,
+                          specs.allowGroupSwitching, rangesToJava(specs.primaryRanges),
+                          rangesToJava(specs.appRequestRanges));
 }
 
 static jobject nativeGetDisplayNativePrimaries(JNIEnv* env, jclass, jobject tokenObj) {
@@ -2235,23 +2254,45 @@
     gDisplayPrimariesClassInfo.white = GetFieldIDOrDie(env, displayPrimariesClazz, "white",
             "Landroid/view/SurfaceControl$CieXyz;");
 
+    jclass RefreshRateRangeClazz =
+            FindClassOrDie(env, "android/view/SurfaceControl$RefreshRateRange");
+    gRefreshRateRangeClassInfo.clazz = MakeGlobalRefOrDie(env, RefreshRateRangeClazz);
+    gRefreshRateRangeClassInfo.ctor =
+            GetMethodIDOrDie(env, gRefreshRateRangeClassInfo.clazz, "<init>", "(FF)V");
+    gRefreshRateRangeClassInfo.min = GetFieldIDOrDie(env, RefreshRateRangeClazz, "min", "F");
+    gRefreshRateRangeClassInfo.max = GetFieldIDOrDie(env, RefreshRateRangeClazz, "max", "F");
+
+    jclass RefreshRateRangesClazz =
+            FindClassOrDie(env, "android/view/SurfaceControl$RefreshRateRanges");
+    gRefreshRateRangesClassInfo.clazz = MakeGlobalRefOrDie(env, RefreshRateRangesClazz);
+    gRefreshRateRangesClassInfo.ctor =
+            GetMethodIDOrDie(env, gRefreshRateRangesClassInfo.clazz, "<init>",
+                             "(Landroid/view/SurfaceControl$RefreshRateRange;Landroid/view/"
+                             "SurfaceControl$RefreshRateRange;)V");
+    gRefreshRateRangesClassInfo.physical =
+            GetFieldIDOrDie(env, RefreshRateRangesClazz, "physical",
+                            "Landroid/view/SurfaceControl$RefreshRateRange;");
+    gRefreshRateRangesClassInfo.render =
+            GetFieldIDOrDie(env, RefreshRateRangesClazz, "render",
+                            "Landroid/view/SurfaceControl$RefreshRateRange;");
+
     jclass DesiredDisplayModeSpecsClazz =
             FindClassOrDie(env, "android/view/SurfaceControl$DesiredDisplayModeSpecs");
     gDesiredDisplayModeSpecsClassInfo.clazz = MakeGlobalRefOrDie(env, DesiredDisplayModeSpecsClazz);
     gDesiredDisplayModeSpecsClassInfo.ctor =
-            GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>", "(IZFFFF)V");
+            GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>",
+                             "(IZLandroid/view/SurfaceControl$RefreshRateRanges;Landroid/view/"
+                             "SurfaceControl$RefreshRateRanges;)V");
     gDesiredDisplayModeSpecsClassInfo.defaultMode =
             GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "defaultMode", "I");
     gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching =
             GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "allowGroupSwitching", "Z");
-    gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMin =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRefreshRateMin", "F");
-    gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMax =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRefreshRateMax", "F");
-    gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMin =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRefreshRateMin", "F");
-    gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMax =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRefreshRateMax", "F");
+    gDesiredDisplayModeSpecsClassInfo.primaryRanges =
+            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRanges",
+                            "Landroid/view/SurfaceControl$RefreshRateRanges;");
+    gDesiredDisplayModeSpecsClassInfo.appRequestRanges =
+            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRanges",
+                            "Landroid/view/SurfaceControl$RefreshRateRanges;");
 
     jclass jankDataClazz =
                 FindClassOrDie(env, "android/view/SurfaceControl$JankData");
diff --git a/core/jni/android_view_VelocityTracker.cpp b/core/jni/android_view_VelocityTracker.cpp
index 16b9f00..343e9d8 100644
--- a/core/jni/android_view_VelocityTracker.cpp
+++ b/core/jni/android_view_VelocityTracker.cpp
@@ -153,6 +153,11 @@
     return result;
 }
 
+static jboolean android_view_VelocityTracker_nativeIsAxisSupported(JNIEnv* env, jclass clazz,
+                                                                   jint axis) {
+    return VelocityTracker::isAxisSupported(axis);
+}
+
 // --- JNI Registration ---
 
 static const JNINativeMethod gVelocityTrackerMethods[] = {
@@ -167,6 +172,8 @@
         {"nativeGetVelocity", "(JII)F", (void*)android_view_VelocityTracker_nativeGetVelocity},
         {"nativeGetEstimator", "(JIILandroid/view/VelocityTracker$Estimator;)Z",
          (void*)android_view_VelocityTracker_nativeGetEstimator},
+        {"nativeIsAxisSupported", "(I)Z",
+         (void*)android_view_VelocityTracker_nativeIsAxisSupported},
 };
 
 int register_android_view_VelocityTracker(JNIEnv* env) {
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index 2cd9f89..6762e45 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -22,6 +22,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>
+#include <linux/fs.h>
 #include <nativehelper/ScopedUtfChars.h>
 #include <stdlib.h>
 #include <string.h>
@@ -250,6 +251,16 @@
         return INSTALL_FAILED_CONTAINER_ERROR;
     }
 
+    // If a filesystem like f2fs supports per-file compression, set the compression bit before data
+    // writes
+    unsigned int flags;
+    if (ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1) {
+        ALOGE("Failed to call FS_IOC_GETFLAGS on %s: %s\n", localTmpFileName, strerror(errno));
+    } else if ((flags & FS_COMPR_FL) == 0) {
+        flags |= FS_COMPR_FL;
+        ioctl(fd, FS_IOC_SETFLAGS, &flags);
+    }
+
     if (!zipFile->uncompressEntry(zipEntry, fd)) {
         ALOGE("Failed uncompressing %s to %s\n", fileName, localTmpFileName);
         close(fd);
diff --git a/core/jni/com_android_internal_expresslog_Counter.cpp b/core/jni/com_android_internal_expresslog_Counter.cpp
new file mode 100644
index 0000000..d4a8c23
--- /dev/null
+++ b/core/jni/com_android_internal_expresslog_Counter.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include <utils/hash/farmhash.h>
+
+#include "core_jni_helpers.h"
+
+// ----------------------------------------------------------------------------
+// JNI Glue
+// ----------------------------------------------------------------------------
+
+static jclass g_stringClass = nullptr;
+
+/**
+ * Class:     com_android_internal_expresslog_Counter
+ * Method:    hashString
+ * Signature: (Ljava/lang/String;)J
+ */
+static jlong hashString(JNIEnv* env, jclass /*class*/, jstring metricNameObj) {
+    ScopedUtfChars name(env, metricNameObj);
+    if (name.c_str() == nullptr) {
+        return 0;
+    }
+
+    return static_cast<jlong>(farmhash::Fingerprint64(name.c_str(), name.size()));
+}
+
+static const JNINativeMethod g_methods[] = {
+        {"hashString", "(Ljava/lang/String;)J", (void*)hashString},
+};
+
+static const char* const kCounterPathName = "com/android/internal/expresslog/Counter";
+
+namespace android {
+
+int register_com_android_internal_expresslog_Counter(JNIEnv* env) {
+    jclass stringClass = FindClassOrDie(env, "java/lang/String");
+    g_stringClass = MakeGlobalRefOrDie(env, stringClass);
+
+    return RegisterMethodsOrDie(env, kCounterPathName, g_methods, NELEM(g_methods));
+}
+
+} // namespace android
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 550259f..664e964 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -343,6 +343,7 @@
 // Must match values in com.android.internal.os.Zygote.
 enum RuntimeFlags : uint32_t {
     DEBUG_ENABLE_JDWP = 1,
+    PROFILE_SYSTEM_SERVER = 1 << 14,
     PROFILE_FROM_SHELL = 1 << 15,
     MEMORY_TAG_LEVEL_MASK = (1 << 19) | (1 << 20),
     MEMORY_TAG_LEVEL_TBI = 1 << 19,
@@ -1821,9 +1822,11 @@
                                            instruction_set.value().c_str());
     }
 
-    if (is_system_server) {
+    if (is_system_server && !(runtime_flags & RuntimeFlags::PROFILE_SYSTEM_SERVER)) {
         // Prefetch the classloader for the system server. This is done early to
         // allow a tie-down of the proper system server selinux domain.
+        // We don't prefetch when the system server is being profiled to avoid
+        // loading AOT code.
         env->CallStaticObjectMethod(gZygoteInitClass, gGetOrCreateSystemServerClassLoader);
         if (env->ExceptionCheck()) {
             // Be robust here. The Java code will attempt to create the classloader
diff --git a/core/jni/com_android_internal_security_VerityUtils.cpp b/core/jni/com_android_internal_security_VerityUtils.cpp
index 8305bd0..dabee69 100644
--- a/core/jni/com_android_internal_security_VerityUtils.cpp
+++ b/core/jni/com_android_internal_security_VerityUtils.cpp
@@ -48,10 +48,6 @@
     if (rfd.get() < 0) {
         return errno;
     }
-    ScopedByteArrayRO signature_bytes(env, signature);
-    if (signature_bytes.get() == nullptr) {
-        return EINVAL;
-    }
 
     fsverity_enable_arg arg = {};
     arg.version = 1;
@@ -59,8 +55,18 @@
     arg.block_size = 4096;
     arg.salt_size = 0;
     arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
-    arg.sig_size = signature_bytes.size();
-    arg.sig_ptr = reinterpret_cast<uintptr_t>(signature_bytes.get());
+
+    if (signature != nullptr) {
+        ScopedByteArrayRO signature_bytes(env, signature);
+        if (signature_bytes.get() == nullptr) {
+            return EINVAL;
+        }
+        arg.sig_size = signature_bytes.size();
+        arg.sig_ptr = reinterpret_cast<uintptr_t>(signature_bytes.get());
+    } else {
+        arg.sig_size = 0;
+        arg.sig_ptr = reinterpret_cast<uintptr_t>(nullptr);
+    }
 
     if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
         return errno;
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index 40f6e4f..5c71f69 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -580,6 +580,7 @@
       // TODO(narayan): This will be an error in a future android release.
       // error = true;
       // ALOGW("Zygote closed file descriptor %d.", it->first);
+      delete it->second;
       it = open_fd_map_.erase(it);
     } else {
       // The entry from the file descriptor table is still open. Restat
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/proto/android/app/location_time_zone_manager.proto b/core/proto/android/app/location_time_zone_manager.proto
index 5fdcfdf..7037a6c 100644
--- a/core/proto/android/app/location_time_zone_manager.proto
+++ b/core/proto/android/app/location_time_zone_manager.proto
@@ -40,7 +40,7 @@
 message LocationTimeZoneManagerServiceStateProto {
   option (android.msg_privacy).dest = DEST_AUTOMATIC;
 
-  optional GeolocationTimeZoneSuggestionProto last_suggestion = 1;
+  optional LocationTimeZoneProviderEventProto last_event = 1;
   repeated TimeZoneProviderStateProto primary_provider_states = 2;
   repeated TimeZoneProviderStateProto secondary_provider_states = 3;
   repeated ControllerStateEnum controller_states = 4;
diff --git a/core/proto/android/app/time_zone_detector.proto b/core/proto/android/app/time_zone_detector.proto
index b52aa82..cd4a36f 100644
--- a/core/proto/android/app/time_zone_detector.proto
+++ b/core/proto/android/app/time_zone_detector.proto
@@ -22,13 +22,38 @@
 option java_multiple_files = true;
 option java_outer_classname = "TimeZoneDetectorProto";
 
-// Represents a GeolocationTimeZoneSuggestion that can be / has been passed to the time zone
+// Represents a LocationTimeZoneProviderEvent that can be / has been passed to the time zone
 // detector.
+message LocationTimeZoneProviderEventProto {
+  option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+  optional GeolocationTimeZoneSuggestionProto suggestion = 1;
+  repeated string debug_info = 2;
+  optional LocationTimeZoneAlgorithmStatusProto algorithm_status = 3;
+}
+
+// Represents a LocationTimeZoneAlgorithmStatus that can be / has been passed to the time zone
+// detector.
+message LocationTimeZoneAlgorithmStatusProto {
+  option (android.msg_privacy).dest = DEST_AUTOMATIC;
+
+  optional DetectionAlgorithmStatusEnum status = 1;
+}
+
+// The state enum for detection algorithms.
+enum DetectionAlgorithmStatusEnum {
+    DETECTION_ALGORITHM_STATUS_UNKNOWN = 0;
+    DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED = 1;
+    DETECTION_ALGORITHM_STATUS_NOT_RUNNING = 2;
+    DETECTION_ALGORITHM_STATUS_RUNNING = 3;
+}
+
+// Represents a GeolocationTimeZoneSuggestion that can be contained in a
+// LocationTimeZoneProviderEvent.
 message GeolocationTimeZoneSuggestionProto {
   option (android.msg_privacy).dest = DEST_AUTOMATIC;
 
   repeated string zone_ids = 1;
-  repeated string debug_info = 2;
 }
 
 /*
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 5099dd2..4650000 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -977,6 +977,12 @@
         optional int32 profile = 2;
     }
     repeated UserProfile user_profile_group_ids = 4;
+    repeated int32 visible_users_array = 5;
+
+    // current_user contains the id of the current user, while current_profiles contains the ids of
+    // both the current user and its profiles (if any)
+    optional int32 current_user = 6;
+    repeated int32 current_profiles = 7;
 }
 
 // sync with com.android.server.am.AppTimeTracker.java
diff --git a/core/proto/android/view/imefocuscontroller.proto b/core/proto/android/view/imefocuscontroller.proto
index ff9dee6..ccde9b7 100644
--- a/core/proto/android/view/imefocuscontroller.proto
+++ b/core/proto/android/view/imefocuscontroller.proto
@@ -25,6 +25,6 @@
  */
 message ImeFocusControllerProto {
     optional bool has_ime_focus = 1;
-    optional string served_view = 2;
-    optional string next_served_view = 3;
+    optional string served_view = 2 [deprecated = true];
+    optional string next_served_view = 3 [deprecated = true];
 }
\ No newline at end of file
diff --git a/core/proto/android/view/inputmethod/inputmethodmanager.proto b/core/proto/android/view/inputmethod/inputmethodmanager.proto
index 9fed0ef..ea5f1e8 100644
--- a/core/proto/android/view/inputmethod/inputmethodmanager.proto
+++ b/core/proto/android/view/inputmethod/inputmethodmanager.proto
@@ -29,4 +29,6 @@
     optional int32 display_id = 3;
     optional bool active = 4;
     optional bool served_connecting = 5;
+    optional string served_view = 6;
+    optional string next_served_view = 7;
 }
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 1f23eb6..e24d667 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -668,7 +668,6 @@
     <protected-broadcast android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED" />
-    <protected-broadcast android:name="com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER" />
 
     <!-- Time zone rules update intents fired by the system server -->
     <protected-broadcast android:name="com.android.intent.action.timezone.RULES_UPDATE_OPERATION" />
@@ -1151,8 +1150,41 @@
                 android:description="@string/permdesc_readMediaImages"
                 android:protectionLevel="dangerous" />
 
+    <!-- Allows an application to read image or video files from external storage that a user has
+      selected via the permission prompt photo picker. Apps can check this permission to verify that
+      a user has decided to use the photo picker, instead of granting access to
+      {@link #READ_MEDIA_IMAGES or #READ_MEDIA_VIDEO}. It does not prevent apps from accessing the
+      standard photo picker manually.
+   <p>Protection level: dangerous -->
+    <permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:label="@string/permlab_readVisualUserSelect"
+        android:description="@string/permdesc_readVisualUserSelect"
+        android:protectionLevel="dangerous" />
+
     <!-- Allows an application to write to external storage.
-         <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+         <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or
+         higher, this permission has no effect.
+
+         <p>If your app is on a device that runs API level 19 or higher, you don't need to declare
+         this permission to read and write files in your application-specific directories returned
+         by {@link android.content.Context#getExternalFilesDir} and
+         {@link android.content.Context#getExternalCacheDir}.
+
+         <p>Learn more about how to
+         <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media
+         files</a> that your app doesn't own, and how to
+         <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a>
+         that your app doesn't own.
+
+         <p>If your app is a file manager and needs broad access to external storage files, then
+         the system must place your app on an allowlist so that you can successfully request the
+         <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission.
+         Learn more about the appropriate use cases for
+         <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage
+         device</a>.
+
+         <p>If <em>both</em> your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
          minSdkVersion}</a> and <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
@@ -1160,12 +1192,6 @@
          grants your app this permission. If you don't need this permission, be sure your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
          targetSdkVersion}</a> is 4 or higher.
-         <p>Starting in API level 19, this permission is <em>not</em> required to
-         read/write files in your application-specific directories returned by
-         {@link android.content.Context#getExternalFilesDir} and
-         {@link android.content.Context#getExternalCacheDir}.
-         <p>If this permission is not allowlisted for an app that targets an API level before
-         {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p>
          <p>Protection level: dangerous</p>
     -->
     <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -2507,6 +2533,19 @@
         android:description="@string/permdesc_modifyAudioSettings"
         android:protectionLevel="normal" />
 
+    <!-- ==================================================== -->
+    <!-- Permissions related to screen capture   -->
+    <!-- ==================================================== -->
+    <eat-comment />
+
+    <!-- Allows an application to get notified when a screen capture of its windows is attempted.
+         <p>Protection level: normal
+    -->
+    <permission android:name="android.permission.DETECT_SCREEN_CAPTURE"
+                android:label="@string/permlab_detectScreenCapture"
+                android:description="@string/permdesc_detectScreenCapture"
+                android:protectionLevel="normal" />
+
     <!-- ======================================== -->
     <!-- Permissions for factory reset protection -->
     <!-- ======================================== -->
@@ -3043,6 +3082,12 @@
     <permission android:name="android.permission.CREATE_USERS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Allows an application to set user association
+         with a certain subscription. Used by Enterprise to associate a
+         subscription with a work or personal profile. -->
+    <permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the
          device. -->
     <permission android:name="android.permission.QUERY_USERS"
@@ -3132,6 +3177,12 @@
     <permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"
                 android:protectionLevel="signature|privileged|vendorPrivileged|oem|verifier|role"/>
 
+    <!-- Allows an application to hint that a component lifecycle operation such as sending
+         a broadcast is associated with an "interactive" usage scenario.
+         @hide -->
+    <permission android:name="android.permission.COMPONENT_OPTION_INTERACTIVE"
+                android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi Must be required by activities that handle the intent action
          {@link Intent#ACTION_SEND_SHOW_SUSPENDED_APP_DETAILS}. This is for use by apps that
          hold {@link Manifest.permission#SUSPEND_APPS} to interact with the system.
@@ -3156,6 +3207,13 @@
 
     <!-- Allows an application to call
         {@link android.app.ActivityManager#killBackgroundProcesses}.
+        <p>As of Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+        the {@link android.app.ActivityManager#killBackgroundProcesses} is no longer available to
+        third party applications. For backwards compatibility, the background processes of the
+        caller's own package will still be killed when calling this API. If the caller has
+        the system permission {@code KILL_ALL_BACKGROUND_PROCESSES}, other processes will be
+        killed too.
+
          <p>Protection level: normal
     -->
     <permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"
@@ -3163,6 +3221,16 @@
         android:description="@string/permdesc_killBackgroundProcesses"
         android:protectionLevel="normal" />
 
+    <!-- @SystemApi @hide Allows an application to call
+        {@link android.app.ActivityManager#killBackgroundProcesses}
+        to kill background processes of other apps.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"
+        android:label="@string/permlab_killBackgroundProcesses"
+        android:description="@string/permdesc_killBackgroundProcesses"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi @hide Allows an application to query process states and current
          OOM adjustment scores.
          <p>Not for use by third-party applications. -->
@@ -4090,7 +4158,8 @@
         android:protectionLevel="signature" />
 
     <!-- Allows access to Test APIs defined in {@link android.view.inputmethod.InputMethodManager}.
-         @hide -->
+         @hide
+         @TestApi -->
     <permission android:name="android.permission.TEST_INPUT_METHOD"
         android:protectionLevel="signature" />
 
@@ -4261,6 +4330,13 @@
     <permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by a CredentialProviderService to ensure that only the
+         system can bind to it.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
+                android:protectionLevel="signature" />
+
    <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE.
         This permission was renamed during the O previews but it was supported on the final O
         release, so we need to carry it over.
@@ -6108,6 +6184,116 @@
         android:label="@string/permlab_foregroundService"
         android:protectionLevel="normal|instant" />
 
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "camera".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"
+        android:description="@string/permdesc_foregroundServiceCamera"
+        android:label="@string/permlab_foregroundServiceCamera"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "connectedDevice".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"
+        android:description="@string/permdesc_foregroundServiceConnectedDevice"
+        android:label="@string/permlab_foregroundServiceConnectedDevice"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "dataSync".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
+        android:description="@string/permdesc_foregroundServiceDataSync"
+        android:label="@string/permlab_foregroundServiceDataSync"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "location".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"
+        android:description="@string/permdesc_foregroundServiceLocation"
+        android:label="@string/permlab_foregroundServiceLocation"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "mediaPlayback".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
+        android:description="@string/permdesc_foregroundServiceMediaPlayback"
+        android:label="@string/permlab_foregroundServiceMediaPlayback"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "mediaProjection".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"
+        android:description="@string/permdesc_foregroundServiceMediaProjection"
+        android:label="@string/permlab_foregroundServiceMediaProjection"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "microphone".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"
+        android:description="@string/permdesc_foregroundServiceMicrophone"
+        android:label="@string/permlab_foregroundServiceMicrophone"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "phoneCall".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"
+        android:description="@string/permdesc_foregroundServicePhoneCall"
+        android:label="@string/permlab_foregroundServicePhoneCall"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "health".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH"
+        android:description="@string/permdesc_foregroundServiceHealth"
+        android:label="@string/permlab_foregroundServiceHealth"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "remoteMessaging".
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"
+        android:description="@string/permdesc_foregroundServiceRemoteMessaging"
+        android:label="@string/permlab_foregroundServiceRemoteMessaging"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "systemExempted".
+         Apps are allowed to use this type only in the use cases listed in
+         {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}.
+         <p>Protection level: normal|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"
+        android:description="@string/permdesc_foregroundServiceSystemExempted"
+        android:label="@string/permlab_foregroundServiceSystemExempted"
+        android:protectionLevel="normal|instant" />
+
+    <!-- Allows a regular application to use {@link android.app.Service#startForeground
+         Service.startForeground} with the type "specialUse".
+         <p>Protection level: signature|appop|instant
+    -->
+    <permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
+        android:description="@string/permdesc_foregroundServiceSpecialUse"
+        android:label="@string/permlab_foregroundServiceSpecialUse"
+        android:protectionLevel="signature|appop|instant" />
+
     <!-- @SystemApi Allows to access all app shortcuts.
          @hide -->
     <permission android:name="android.permission.ACCESS_SHORTCUTS"
@@ -6571,6 +6757,22 @@
                 android:protectionLevel="signature" />
     <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
 
+    <!-- Allows financed device kiosk apps to perform actions on the Device Lock service
+         <p>Protection level: internal|role
+         <p>Intended for use by the FINANCED_DEVICE_KIOSK role only.
+    -->
+    <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. -->
@@ -6829,13 +7031,6 @@
                   android:exported="false">
         </activity>
 
-        <activity android:name="com.android.internal.app.LogAccessDialogActivity"
-                  android:theme="@style/Theme.Translucent.NoTitleBar"
-                  android:process=":ui"
-                  android:excludeFromRecents="true"
-                  android:exported="false">
-        </activity>
-
         <activity android:name="com.android.server.notification.NASLearnMoreActivity"
                   android:theme="@style/Theme.Dialog.Confirmation"
                   android:excludeFromRecents="true"
@@ -7132,6 +7327,10 @@
             </intent-filter>
         </service>
 
+        <service android:name="com.android.server.art.BackgroundDexOptJobService"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+
         <provider
             android:name="com.android.server.textclassifier.IconsContentProvider"
             android:authorities="com.android.textclassifier.icons"
diff --git a/core/res/res/anim/dock_bottom_enter.xml b/core/res/res/anim/dock_bottom_enter.xml
deleted file mode 100644
index bfb97b6..0000000
--- a/core/res/res/anim/dock_bottom_enter.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the bottom of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/decelerate_cubic">
-    <translate android:fromYDelta="100%" android:toYDelta="0"
-        android:duration="@integer/dock_enter_exit_duration"/>
-</set>
diff --git a/core/res/res/anim/dock_bottom_exit.xml b/core/res/res/anim/dock_bottom_exit.xml
deleted file mode 100644
index 4e15448..0000000
--- a/core/res/res/anim/dock_bottom_exit.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the bottom of the screen is exiting. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/accelerate_cubic">
-    <translate android:fromYDelta="0" android:toYDelta="100%"
-        android:startOffset="100" android:duration="@integer/dock_enter_exit_duration"/>
-</set>
diff --git a/core/res/res/anim/dock_bottom_exit_keyguard.xml b/core/res/res/anim/dock_bottom_exit_keyguard.xml
deleted file mode 100644
index 4de3ce5..0000000
--- a/core/res/res/anim/dock_bottom_exit_keyguard.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-  ~ Copyright (C) 2016 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
-  -->
-
-<!-- Animation for when a dock window at the bottom of the screen is exiting while on Keyguard -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:interpolator="@android:interpolator/fast_out_linear_in">
-    <translate android:fromYDelta="0" android:toYDelta="100%"
-        android:duration="200"/>
-</set>
\ No newline at end of file
diff --git a/core/res/res/anim/dock_left_enter.xml b/core/res/res/anim/dock_left_enter.xml
deleted file mode 100644
index 7f5dfd5..0000000
--- a/core/res/res/anim/dock_left_enter.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the left of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/decelerate_cubic">
-    <translate android:fromXDelta="-100%" android:toXDelta="0"
-        android:duration="250"/>
-</set>
diff --git a/core/res/res/anim/dock_left_exit.xml b/core/res/res/anim/dock_left_exit.xml
deleted file mode 100644
index 11cbc0b3..0000000
--- a/core/res/res/anim/dock_left_exit.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the right of the screen is exiting. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/accelerate_cubic">
-    <translate android:fromXDelta="0" android:toXDelta="-100%"
-        android:startOffset="100" android:duration="250"/>
-</set>
diff --git a/core/res/res/anim/dock_right_enter.xml b/core/res/res/anim/dock_right_enter.xml
deleted file mode 100644
index a92c7d2..0000000
--- a/core/res/res/anim/dock_right_enter.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the right of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/decelerate_cubic">
-    <translate android:fromXDelta="100%" android:toXDelta="0"
-        android:duration="250"/>
-</set>
diff --git a/core/res/res/anim/dock_right_exit.xml b/core/res/res/anim/dock_right_exit.xml
deleted file mode 100644
index 80e4dc3..0000000
--- a/core/res/res/anim/dock_right_exit.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2012, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the right of the screen is exiting. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/accelerate_cubic">
-    <translate android:fromXDelta="0" android:toXDelta="100%"
-        android:startOffset="100" android:duration="250"/>
-</set>
diff --git a/core/res/res/anim/dock_top_enter.xml b/core/res/res/anim/dock_top_enter.xml
deleted file mode 100644
index f763fb5..0000000
--- a/core/res/res/anim/dock_top_enter.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2007, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the top of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/decelerate_cubic">
-    <translate android:fromYDelta="-100%" android:toYDelta="0"
-        android:duration="@integer/dock_enter_exit_duration"/>
-</set>
diff --git a/core/res/res/anim/dock_top_exit.xml b/core/res/res/anim/dock_top_exit.xml
deleted file mode 100644
index 995b7d0..0000000
--- a/core/res/res/anim/dock_top_exit.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 2007, 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.
-*/
--->
-
-<!-- Animation for when a dock window at the top of the screen is exiting. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-        android:interpolator="@android:interpolator/accelerate_cubic">
-    <translate android:fromYDelta="0" android:toYDelta="-100%"
-        android:startOffset="100" android:duration="@integer/dock_enter_exit_duration"/>
-</set>
diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml
index c4599da..8df624f 100644
--- a/core/res/res/anim/dream_activity_close_exit.xml
+++ b/core/res/res/anim/dream_activity_close_exit.xml
@@ -19,5 +19,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="0.0"
-    android:duration="100" />
+    android:duration="@integer/config_dreamCloseAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml
index 9e1c6e2..d6d9c5c 100644
--- a/core/res/res/anim/dream_activity_open_enter.xml
+++ b/core/res/res/anim/dream_activity_open_enter.xml
@@ -22,5 +22,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="0.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml
index 740f528..2c2e501 100644
--- a/core/res/res/anim/dream_activity_open_exit.xml
+++ b/core/res/res/anim/dream_activity_open_exit.xml
@@ -22,4 +22,4 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
diff --git a/core/res/res/color/letterbox_background.xml b/core/res/res/color/letterbox_background.xml
new file mode 100644
index 0000000..955948a
--- /dev/null
+++ b/core/res/res/color/letterbox_background.xml
@@ -0,0 +1,19 @@
+<?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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/system_neutral1_500" android:lStar="5" />
+</selector>
diff --git a/core/res/res/drawable-hdpi/ic_notification_ime_default.png b/core/res/res/drawable-hdpi/ic_notification_ime_default.png
deleted file mode 100644
index 369c88d..0000000
--- a/core/res/res/drawable-hdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png
deleted file mode 100644
index 7d97eb5..0000000
--- a/core/res/res/drawable-mdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 900801a..0000000
--- a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 6c8222e..0000000
--- a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/layout/log_access_user_consent_dialog_permission.xml b/core/res/res/layout/log_access_user_consent_dialog_permission.xml
deleted file mode 100644
index 3da14c8..0000000
--- a/core/res/res/layout/log_access_user_consent_dialog_permission.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2022, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:tools="http://schemas.android.com/tools"
-        android:layout_width="380dp"
-        android:layout_height="match_parent"
-        android:clipToPadding="false">
-    <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="vertical"
-            android:gravity="center"
-            android:paddingLeft="24dp"
-            android:paddingRight="24dp"
-            android:paddingTop="24dp"
-            android:paddingBottom="24dp">
-
-        <ImageView
-                android:id="@+id/log_access_image_view"
-                android:layout_width="32dp"
-                android:layout_height="32dp"
-                android:layout_marginBottom="16dp"
-                android:src="@drawable/ic_doc_document"
-                tools:layout_editor_absoluteX="148dp"
-                tools:layout_editor_absoluteY="35dp"
-                android:gravity="center" />
-
-        <TextView
-                android:id="@+id/log_access_dialog_title"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:layout_marginBottom="32dp"
-                android:text="@string/log_access_confirmation_title"
-                android:textAppearance="@style/AllowLogAccess"
-                android:textColor="?android:attr/textColorPrimary"
-                android:gravity="center" />
-
-        <TextView
-                android:id="@+id/log_access_dialog_body"
-                android:layout_height="wrap_content"
-                android:layout_width="wrap_content"
-                android:layout_marginBottom="40dp"
-                android:text="@string/log_access_confirmation_body"
-                android:textAppearance="@style/PrimaryAllowLogAccess"
-                android:textColor="?android:attr/textColorPrimary"
-                android:gravity="center" />
-
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="0dp"
-                android:layout_weight="1" />
-
-        <LinearLayout
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-            <Button
-                    android:id="@+id/log_access_dialog_allow_button"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/log_access_confirmation_allow"
-                    style="@style/PermissionGrantButtonTop"
-                    android:textAppearance="@style/PermissionGrantButtonTextAppearance"
-                    android:layout_marginBottom="5dp"
-                    android:layout_centerHorizontal="true"
-                    android:layout_alignParentTop="true"
-                    android:layout_alignParentBottom="true"
-                    android:clipToOutline="true"
-                    android:gravity="center" />
-
-            <Button
-                    android:id="@+id/log_access_dialog_deny_button"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/log_access_confirmation_deny"
-                    style="@style/PermissionGrantButtonBottom"
-                    android:textAppearance="@style/PermissionGrantButtonTextAppearance"
-                    android:layout_centerHorizontal="true"
-                    android:layout_alignParentTop="true"
-                    android:layout_alignParentBottom="true"
-                    android:clipToOutline="true"
-                    android:gravity="center" />
-        </LinearLayout>
-    </LinearLayout>
-</ScrollView>
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index a7f2aa7..be1c939 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -24,6 +24,7 @@
     android:gravity="center_vertical"
     android:orientation="horizontal"
     android:theme="@style/Theme.DeviceDefault.Notification"
+    android:importantForAccessibility="no"
     >
 
     <ImageView
diff --git a/core/res/res/values-af/strings.xml b/core/res/res/values-af/strings.xml
index 4b9abad..4a6d4b0 100644
--- a/core/res/res/values-af/strings.xml
+++ b/core/res/res/values-af/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Beller-ID se verstek is nie beperk nie. Volgende oproep: nie beperk nie"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Diens nie verskaf nie."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Jy kan nie die beller-ID-instelling verander nie."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Het data oorgeskakel na <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Jy kan dit enige tyd in instellings verander"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Geen mobiele datadiens nie"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Noodoproepe is onbeskikbaar"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Geen stemdiens nie"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Snelsluit"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nuwe kennisgewing"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuele sleutelbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fisieke sleutelbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sekuriteit"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Motormodus"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Laat die program toe om videolêers in jou gedeelde berging te lees."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lees prentlêers in gedeelde berging"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Laat die program toe om prentlêers in jou gedeelde berging te lees."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"lees prent- en videolêers wat gebruiker in gedeelde berging kies"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Laat die app toe om prent- en videolêers te lees wat jy in jou gedeelde berging kies."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"verander of vee jou gedeelde berging se inhoud uit"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Laat die program toe om jou gedeelde berging se inhoud te skryf."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"maak en/of ontvang SIP-oproepe"</string>
diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml
index b6fdcdf..1850cbe 100644
--- a/core/res/res/values-am/strings.xml
+++ b/core/res/res/values-am/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"የደዋይ ID ነባሪዎች ወደአልተከለከለም። ቀጥሎ ጥሪ፡አልተከለከለም"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"አገልግሎት አልቀረበም።"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"የደዋይ መታወቂያ ቅንብሮች  መለወጥ አትችልም፡፡"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ውሂብ ወደ <xliff:g id="CARRIERDISPLAY">%s</xliff:g> ተቀይሯል"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ይህን በማንኛውም ጊዜ በቅንብሮች ውስጥ መለወጥ ይችላሉ"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ምንም የተንቀሳቃሽ ስልክ ውሂብ አገልግሎት የለም"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"የድንገተኛ አደጋ ጥሪ አይገኝም"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ምንም የድምፅ ጥሪ አገልግሎት የለም"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"መቆለፊያ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"አዲስ ማሳወቂያ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ምናባዊ የቁልፍ ሰሌዳ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"አካላዊ ቁልፍ ሰሌዳ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ደህንነት"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"የመኪና ሁነታ"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"መተግበሪያው ከእርስዎ የተጋራ ማከማቻ የቪዲዮ ፋይሎችን እንዲያነብ ይፈቅድለታል።"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ከጋራ ማከማቻ የምስል ፋይሎችን አንብብ"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"መተግበሪያው ከእርስዎ የተጋራ ማከማቻ የምስል ፋይሎችን እንዲያነብ ይፈቅድለታል።"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"የተጋራ ማከማቻዎን ይዘቶች ይቀይሩ ወይም ይሰርዙ"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"መተግበሪያው የእርስዎን የተጋራ ማከማቻ ይዘቶችን እንዲጽፍ ያስችለዋል።"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"የSIP ጥሪዎችን ያድርጉ/ይቀበሉ"</string>
diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml
index 977cf8e..f113413 100644
--- a/core/res/res/values-ar/strings.xml
+++ b/core/res/res/values-ar/strings.xml
@@ -76,6 +76,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"الإعداد التلقائي لمعرف المتصل هو غير محظور  . الاتصال التالي: غير محظور"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"الخدمة غير متوفرة."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"لا يمكنك تغيير إعداد معرّف المتصل."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"تم تبديل البيانات إلى <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"يمكنك تغيير هذه الميزة في أي وقت في الإعدادات."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"لا تتوفّر خدمة بيانات جوّال."</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"لا تتوفّر مكالمات طوارئ."</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"لا تتوفر خدمة صوتية"</string>
@@ -268,7 +270,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"إلغاء التأمين"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"إشعار جديد"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"لوحة المفاتيح الافتراضية"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"لوحة المفاتيح الخارجية"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"الأمان"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"وضع السيارة"</string>
@@ -701,6 +702,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"يسمح للتطبيق بقراءة ملفات الفيديو من مساحة التخزين المشتركة."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"قراءة ملفات الصور من مساحة التخزين المشتركة"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"يسمح هذا الإذن للتطبيق بقراءة ملفات الصور من مساحة التخزين المشتركة."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"تعديل محتوى مساحة التخزين المشتركة أو حذفه"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"للسماح للتطبيق بالكتابة إلى محتوى مساحة التخزين المشتركة."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"‏إجراء/تلقي مكالمات SIP"</string>
diff --git a/core/res/res/values-as/strings.xml b/core/res/res/values-as/strings.xml
index dbce595..687fc99 100644
--- a/core/res/res/values-as/strings.xml
+++ b/core/res/res/values-as/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"কলাৰ আইডি সীমিত নকৰিবলৈ পূর্বনির্ধাৰণ কৰা হৈছে। পৰৱৰ্তী কল: সীমিত কৰা হোৱা নাই"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"সুবিধা যোগান ধৰা হোৱা নাই।"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"আপুনি কলাৰ আইডি ছেটিং সলনি কৰিব নোৱাৰে।"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ডেটা <xliff:g id="CARRIERDISPLAY">%s</xliff:g>লৈ সলনি কৰা হৈছে"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"আপুনি ছেটিঙত এইটো যিকোনো সময়তে সলনি কৰিব পাৰে"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"কোনো ম’বাইল ডেটা সেৱা নাই"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"জৰুৰীকালীন কল কৰাৰ সুবিধা উপলব্ধ নহয়"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"কোনো ভইচ সেৱা নাই"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"লকডাউন"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"৯৯৯+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"নতুন জাননী"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ভাৰ্শ্বুৱল কীব\'ৰ্ড"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"কায়িক কীব’ৰ্ড"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"সুৰক্ষা"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"গাড়ী ম\'ড"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ পৰা ভিডিঅ’ ফাইল পঢ়িবলৈ এপ্‌টোক অনুমতি দিয়ে।"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ পৰা প্ৰতিচ্ছবিৰ ফাইল পঢ়ক"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ পৰা প্ৰতিচ্ছবিৰ ফাইল পঢ়িবলৈ এপ্‌টোক অনুমতি দিয়ে।"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ সমল সংশোধন কৰিব বা মচিব পাৰে"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"আপোনাৰ শ্বেয়াৰ কৰি ৰখা ষ্ট’ৰেজৰ সমল লিখিবলৈ এপ্‌টোক অনুমতি দিয়ে।"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP কল কৰা/পোৱা"</string>
diff --git a/core/res/res/values-az/strings.xml b/core/res/res/values-az/strings.xml
index 586adef..f82a007 100644
--- a/core/res/res/values-az/strings.xml
+++ b/core/res/res/values-az/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Zəng edənin kimliyi defolt olaraq qadağan deyil. Növbəti zəng: Qadağan deyil"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Xidmət təmin edilməyib."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Çağrı kimliyi ayarını dəyişə bilməzsiniz."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Data <xliff:g id="CARRIERDISPLAY">%s</xliff:g> operatoruna keçirilib"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Bunu istənilən zaman Ayarlarda dəyişə bilərsiniz"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Mobil data xidməti yoxdur"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Təcili zəng əlçatan deyil"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Səsli xidmət yoxdur"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kilidləyin"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yeni bildiriş"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual klaviatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziki klaviatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Güvənlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Avtomobil rejimi"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Tətbiqə paylaşılan yaddaşdakı video fayllarını oxumaq imkanı verir."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"paylaşılan yaddaşdakı şəkil fayllarını oxumaq"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Tətbiqə paylaşılan yaddaşdakı şəkil fayllarını oxumaq imkanı verir."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"paylaşılan yaddaşdakı kontenti dəyişmək və ya silmək"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Tətbiqə paylaşılan yaddaşdakı kontenti yazmaq imkanı verir."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP çağrıları göndərin/qəbul edin"</string>
diff --git a/core/res/res/values-b+sr+Latn/strings.xml b/core/res/res/values-b+sr+Latn/strings.xml
index c19338d..40b126f 100644
--- a/core/res/res/values-b+sr+Latn/strings.xml
+++ b/core/res/res/values-b+sr+Latn/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID pozivaoca podrazumevano nije ograničen. Sledeći poziv: Nije ograničen."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Usluga nije dobavljena."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ne možete da promenite podešavanje ID-a korisnika."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Mobilni podaci su prebačeni na operatera <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ovo možete u svakom trenutku da promenite u Podešavanjima"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nema usluge mobilnih podataka"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hitni pozivi nisu dostupni"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Nema glasovne usluge"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obaveštenje"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelna tastatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tastatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bezbednost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim rada u automobilu"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Omogućava aplikaciji da čita video fajlove iz deljenog memorijskog prostora."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"čitanje fajlova slika iz deljenog memorijskog prostora"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Omogućava aplikaciji da čita fajlove slika iz deljenog memorijskog prostora."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"menjanje ili brisanje sadržaja deljenog memorijskog prostora"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Dozvoljava aplikaciji da upisuje sadržaj deljenog memorijskog prostora."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"upućivanje/prijem SIP poziva"</string>
diff --git a/core/res/res/values-be/strings.xml b/core/res/res/values-be/strings.xml
index ab338c7..bf1302c 100644
--- a/core/res/res/values-be/strings.xml
+++ b/core/res/res/values-be/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Налады ідэнтыфікатару АВН па змаўчанні: не абмяжавана. Наступны выклік: не абмежавана"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Служба не прадастаўляецца."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Вы не можаце змяніць налады ідэнтыфікатара абанента, якi тэлефануе."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Мабільны трафік пераключаны на аператара \"<xliff:g id="CARRIERDISPLAY">%s</xliff:g>\""</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Вы можаце змяніць гэта ў любы час у Наладах"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Мабільная перадача даных недаступная"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Экстранныя выклікі недаступныя"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Няма сэрвісу галасавых выклікаў"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блакіроўка"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Новае апавяшчэнне"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Віртуальная клавіятура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Фізічная клавіятура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Бяспека"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Рэжым \"У машыне\""</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Праграма зможа счытваць відэафайлы з абагуленага сховішча."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"счытваць файлы відарысаў з абагуленага сховішча"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Праграма зможа счытваць файлы відарысаў з абагуленага сховішча."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"змяненне або выдаленне змесціва абагуленага сховішча"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Дазваляе праграме запісваць змесціва абагуленага сховішча."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"ажыццяўленне/прыманне выклікаў SIP"</string>
diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml
index 46ab1ad..b472930 100644
--- a/core/res/res/values-bg/strings.xml
+++ b/core/res/res/values-bg/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Стандартната идентификация на повикванията е „разрешено“. За следващото обаждане тя е разрешена."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Услугата не е обезпечена."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Не можете да променяте настройката за идентификация на обажданията."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Преминахте към мобилни данни от <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Можете да промените това по всяко време в „Настройки“"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Няма достъп до мобилната услуга за данни"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Няма достъп до спешните обаждания"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Няма услуга за гласови обаждания"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Заключване"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново известие"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуална клавиатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физическа клавиатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Сигурност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Моторежим"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Разрешава на приложението да чете видеофайлове от споделеното ви хранилище."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"да чете графични файлове от споделеното хранилище"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Разрешава на приложението да чете графични файлове от споделеното ви хранилище."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"промяна или изтрив. на съдърж. от сподел. ви хранил."</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Разрешава на прил. да записва съдърж. от сподел. ви хранил."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"извършване/получаване на обаждания чрез SIP"</string>
diff --git a/core/res/res/values-bn/strings.xml b/core/res/res/values-bn/strings.xml
index ed77eef..aba5e7d 100644
--- a/core/res/res/values-bn/strings.xml
+++ b/core/res/res/values-bn/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ডিফল্টরূপে কলার আইডি সীমাবদ্ধ করা থাকে না৷ পরবর্তী কল: সীমাবদ্ধ নয়"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"পরিষেবা প্রস্তুত নয়৷"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"আপনি কলার আইডি এর সেটিংস পরিবর্তন করতে পারবেন না৷"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g>-এর ডেটা ব্যবহার করা হয়েছে"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"আপনি যে কোনও সময় সেটিংস থেকে এটি পরিবর্তন করতে পারবেন"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"মোবাইল ডেটা পরিষেবা নেই"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"জরুরি কল করা যাবে না"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ভয়েস পরিষেবা নেই"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"লকডাউন"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"৯৯৯+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"নতুন বিজ্ঞপ্তি"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ভার্চুয়াল কীবোর্ড"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ফিজিক্যাল কীবোর্ড"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"নিরাপত্তা"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"গাড়ি মোড"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"আপনার শেয়ার করা স্টোরেজ থেকে ভিডিও ফাইল রিড করতে অ্যাপকে অনুমতি দিন।"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"শেয়ার করা স্টোরেজ থেকে ছবির ফাইল রিড করা"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"আপনার শেয়ার করা স্টোরেজ থেকে ছবির ফাইল রিড করার জন্য অ্যাপকে অনুমতি দেয়।"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"শেয়ার করা স্টোরেজের কন্টেন্ট মুছে ফেলুন বা পরিবর্তন করুন"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"শেয়ার করা স্টোরেজের কন্টেন্ট লেখার জন্য অ্যাপটিকে অনুমতি দিন।"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP কল করুন/গ্রহণ করুন"</string>
diff --git a/core/res/res/values-bs/strings.xml b/core/res/res/values-bs/strings.xml
index 081d8f2..815bca8 100644
--- a/core/res/res/values-bs/strings.xml
+++ b/core/res/res/values-bs/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Prikaz ID-a pozivaoca u zadanim postavkama nije zabranjen. Sljedeći poziv: nije zabranjen"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Uslugu nije moguće koristiti."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ne možete promijeniti postavke ID-a pozivaoca."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Prijenos podataka usmjeravanjem na <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ove postavke možete uvijek promijeniti u Postavkama"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nema usluge prijenosa podataka na mobilnoj mreži"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hitni pozivi su nedostupni"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Nema usluge govornih poziva"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obavještenje"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelna tastatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tastatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sigurnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način rada u automobilu"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Omogućava aplikaciji da čita fajlove videozapisa iz vaše dijeljene pohrane."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"čitanje fajlova slika iz dijeljene pohrane"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Omogućava aplikaciji da čita fajlove slika iz vaše dijeljene pohrane."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"čitati slikovne i videodatoteke koje je korisnik odabrao iz dijeljene pohrane"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Aplikaciji omogućuje čitanje slikovnih i videodatoteka koje odaberete iz dijeljene pohrane."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"mijenja ili briše sadržaj vaše dijeljene pohrane"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Omogućava aplikaciji da piše sadržaj vaše dijeljene pohrane."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"Uputi/primi SIP pozive"</string>
diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml
index aad5668..b423b65 100644
--- a/core/res/res/values-ca/strings.xml
+++ b/core/res/res/values-ca/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"El valor predeterminat de l\'identificador de trucada és no restringit. Trucada següent: no restringit"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"No s\'ha proveït el servei."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"No pots canviar la configuració de l\'identificador de trucada."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Les dades s\'han canviat a <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Pots canviar aquesta opció en qualsevol moment a Configuració"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No hi ha servei de dades mòbils"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Les trucades d\'emergència no estan disponibles"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sense servei de veu"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueig de seguretat"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"+999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificació nova"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclat virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclat físic"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguretat"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode de cotxe"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permet que l\'aplicació llegeixi fitxers de vídeo de l\'emmagatzematge compartit."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"llegir fitxers d\'imatge de l\'emmagatzematge compartit"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permet que l\'aplicació llegeixi fitxers d\'imatge de l\'emmagatzematge compartit."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"editar o suprimir cont. d\'emmagatzematge compartit"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"L\'app pot editar contingut de l\'emmagatzematge compartit."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"Fer i rebre trucades de SIP"</string>
diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml
index e9d5ab2..c22628c 100644
--- a/core/res/res/values-cs/strings.xml
+++ b/core/res/res/values-cs/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Ve výchozím nastavení není funkce ID volajícího omezena. Příští hovor: Neomezeno"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Služba není zřízena."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nastavení ID volajícího nesmíte měnit."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Datové připojení bylo přepnuto na operátora <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Volbu můžete kdykoli změnit v nastavení"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Není k dispozici žádná mobilní datová služba"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Tísňová volání jsou nedostupná"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Hlasová volání nejsou k dispozici"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zamknuto"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nové oznámení"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuální klávesnice"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyzická klávesnice"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Zabezpečení"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim Auto"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Umožňuje aplikaci čtení videosouborů ze sdíleného úložiště."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"čtení obrázkových souborů ze sdíleného úložiště"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Umožňuje aplikaci číst obrázkové soubory ze sdíleného úložiště."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"úprava nebo mazání obsahu sdíleného úložiště"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Umožňuje aplikaci zápis obsahu do sdíleného úložiště."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"uskutečňování/příjem volání SIP"</string>
diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml
index edb962c5..59c18bd 100644
--- a/core/res/res/values-da/strings.xml
+++ b/core/res/res/values-da/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Standarder for opkalds-id til ikke begrænset. Næste opkald: Ikke begrænset"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Tjenesten provisioneres ikke."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Du kan ikke ændre indstillingen for opkalds-id\'et."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Der blev skiftet til <xliff:g id="CARRIERDISPLAY">%s</xliff:g>-data"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Du kan altid ændre dette under Indstillinger"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ingen mobildatatjeneste"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Det er ikke muligt at foretage nødopkald"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ingen taletjeneste"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lås enhed"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ny notifikation"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelt tastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysisk tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sikkerhed"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Biltilstand"</string>
@@ -333,7 +334,7 @@
     <string name="capability_title_canPerformGestures" msgid="9106545062106728987">"Udføre bevægelser"</string>
     <string name="capability_desc_canPerformGestures" msgid="6619457251067929726">"Kan trykke, stryge, knibe sammen og udføre andre bevægelser."</string>
     <string name="capability_title_canCaptureFingerprintGestures" msgid="1189053104594608091">"Fingeraftryksbevægelser"</string>
-    <string name="capability_desc_canCaptureFingerprintGestures" msgid="6861869337457461274">"Kan registrere bevægelser, der foretages på enhedens fingeraftrykslæser."</string>
+    <string name="capability_desc_canCaptureFingerprintGestures" msgid="6861869337457461274">"Kan registrere bevægelser, der foretages på enhedens fingeraftrykssensor."</string>
     <string name="capability_title_canTakeScreenshot" msgid="3895812893130071930">"Tag screenshot"</string>
     <string name="capability_desc_canTakeScreenshot" msgid="7762297374317934052">"Kan tage et screenshot af skærmen."</string>
     <string name="permlab_statusBar" msgid="8798267849526214017">"deaktivere eller redigere statuslinje"</string>
@@ -582,11 +583,11 @@
     <string name="biometric_error_generic" msgid="6784371929985434439">"Der opstod fejl i forbindelse med godkendelse"</string>
     <string name="screen_lock_app_setting_name" msgid="6054944352976789228">"Brug skærmlås"</string>
     <string name="screen_lock_dialog_default_subtitle" msgid="120359538048533695">"Angiv din skærmlås for at fortsætte"</string>
-    <string name="fingerprint_acquired_partial" msgid="4323789264604479684">"Hold fingeren nede på læseren"</string>
+    <string name="fingerprint_acquired_partial" msgid="4323789264604479684">"Hold fingeren nede på sensoren"</string>
     <string name="fingerprint_acquired_insufficient" msgid="623888149088216458">"Fingeraftrykket kan ikke genkendes. Prøv igen."</string>
-    <string name="fingerprint_acquired_imager_dirty" msgid="1770676120848224250">"Rengør fingeraftrykslæseren, og prøv igen"</string>
-    <string name="fingerprint_acquired_imager_dirty_alt" msgid="9169582140486372897">"Rengør læseren, og prøv igen"</string>
-    <string name="fingerprint_acquired_too_fast" msgid="1628459767349116104">"Hold fingeren nede på læseren"</string>
+    <string name="fingerprint_acquired_imager_dirty" msgid="1770676120848224250">"Rengør fingeraftrykssensoren, og prøv igen"</string>
+    <string name="fingerprint_acquired_imager_dirty_alt" msgid="9169582140486372897">"Rengør sensoren, og prøv igen"</string>
+    <string name="fingerprint_acquired_too_fast" msgid="1628459767349116104">"Hold fingeren nede på sensoren"</string>
     <string name="fingerprint_acquired_too_slow" msgid="6683510291554497580">"Du bevægede fingeren for langsomt. Prøv igen."</string>
     <string name="fingerprint_acquired_already_enrolled" msgid="2285166003936206785">"Prøv med et andet fingeraftryk"</string>
     <string name="fingerprint_acquired_too_bright" msgid="3863560181670915607">"Der er for lyst"</string>
@@ -609,9 +610,9 @@
     <string name="fingerprint_error_lockout_permanent" msgid="9060651300306264843">"Du har brugt for mange forsøg. Brug skærmlåsen i stedet."</string>
     <string name="fingerprint_error_unable_to_process" msgid="2446280592818621224">"Fingeraftrykket kan ikke behandles. Prøv igen."</string>
     <string name="fingerprint_error_no_fingerprints" msgid="8671811719699072411">"Der er ikke registreret nogen fingeraftryk."</string>
-    <string name="fingerprint_error_hw_not_present" msgid="578914350967423382">"Denne enhed har ingen fingeraftrykslæser."</string>
+    <string name="fingerprint_error_hw_not_present" msgid="578914350967423382">"Denne enhed har ingen fingeraftrykssensor."</string>
     <string name="fingerprint_error_security_update_required" msgid="7750187320640856433">"Sensoren er midlertidigt deaktiveret."</string>
-    <string name="fingerprint_error_bad_calibration" msgid="4385512597740168120">"Fingeraftrykslæseren kan ikke bruges. Få den repareret"</string>
+    <string name="fingerprint_error_bad_calibration" msgid="4385512597740168120">"Fingeraftrykssensoren kan ikke bruges. Få den repareret"</string>
     <string name="fingerprint_error_power_pressed" msgid="5479524500542129414">"Der blev trykket på afbryderknappen"</string>
     <string name="fingerprint_name_template" msgid="8941662088160289778">"Fingeraftryk <xliff:g id="FINGERID">%d</xliff:g>"</string>
     <string name="fingerprint_app_setting_name" msgid="4253767877095495844">"Brug fingeraftryk"</string>
@@ -631,7 +632,7 @@
     <string name="fingerprint_setup_notification_title" msgid="2002630611398849495">"Konfigurer flere måder at låse op på"</string>
     <string name="fingerprint_setup_notification_content" msgid="205578121848324852">"Tryk for at tilføje et fingeraftryk"</string>
     <string name="fingerprint_recalibrate_notification_name" msgid="1414578431898579354">"Oplåsning med fingeraftryk"</string>
-    <string name="fingerprint_recalibrate_notification_title" msgid="2406561052064558497">"Fingeraftrykslæseren kan ikke bruges"</string>
+    <string name="fingerprint_recalibrate_notification_title" msgid="2406561052064558497">"Fingeraftrykssensoren kan ikke bruges"</string>
     <string name="fingerprint_recalibrate_notification_content" msgid="8519935717822194943">"Få den repareret."</string>
     <string name="face_acquired_insufficient" msgid="6889245852748492218">"Din ansigtsmodel kan ikke oprettes. Prøv igen."</string>
     <string name="face_acquired_too_bright" msgid="8070756048978079164">"Der er for lyst. Prøv en mere dæmpet belysning."</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Tillader, at appen læser videofiler fra din fælles lagerplads."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"Læse billedfiler fra den delte lagerplads"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Tillader, at appen læser billedfiler fra din delte lagerplads."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ændre eller slette indholdet af din delte lagerplads"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Tillader, at appen kan skrive indholdet af din delte lagerplads."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"foretage/modtage SIP-opkald"</string>
diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml
index b7a0b02..e14d3e9 100644
--- a/core/res/res/values-de/strings.xml
+++ b/core/res/res/values-de/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Anrufer-ID ist standardmäßig nicht beschränkt. Nächster Anruf: Nicht beschränkt"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Dienst nicht eingerichtet."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Du kannst die Einstellung für die Anrufer-ID nicht ändern."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Mobile Daten wurden auf <xliff:g id="CARRIERDISPLAY">%s</xliff:g> umgestellt"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Du kannst dies jederzeit in den Einstellungen ändern"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Kein mobiler Datendienst"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Notrufe nicht möglich"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Keine Anrufe"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Sperren"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Neue Benachrichtigung"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Bildschirmtastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physische Tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sicherheit"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automodus"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Gewährt der App Lesezugriff auf Videodateien in deinem freigegebenen Speicher."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"Lesezugriff auf Bilddateien im freigegebenen Speicher"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Gewährt der App Lesezugriff auf Bilddateien in deinem freigegebenen Speicher."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"Inhalte deines freigegebenen Speichers ändern oder löschen"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"So kann die App Inhalte deines freigegebenen Speichers erstellen."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP-Anrufe tätigen/empfangen"</string>
diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml
index a9dd1cf..47421cd 100644
--- a/core/res/res/values-el/strings.xml
+++ b/core/res/res/values-el/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Η αναγνώριση κλήσης βρίσκεται από προεπιλογή στην \"μη περιορισμένη\". Επόμενη κλήση: Μη περιορισμένη"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Η υπηρεσία δεν προβλέπεται."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Δεν μπορείτε να αλλάξετε τη ρύθμιση του αναγνωριστικού καλούντος."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Έγινε εναλλαγή των δεδομένων σε <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Μπορείτε να αλλάξετε αυτήν την επιλογή ανά πάσα στιγμή στις Ρυθμίσεις"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Δεν υπάρχει υπηρεσία δεδομένων κινητής τηλεφωνίας"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Οι κλήσεις έκτακτης ανάγκης δεν είναι διαθέσιμες"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Δεν υπάρχει φωνητική υπηρεσία"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Κλείδωμα"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Νέα ειδοποίηση"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Εικονικό πληκτρολόγιο"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Κανονικό πληκτρολόγιο"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Ασφάλεια"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Λειτουργία αυτοκινήτου"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Επιτρέπει στην εφαρμογή την ανάγνωση αρχείων βίντεο από τον κοινόχρηστο αποθηκευτικό σας χώρο."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ανάγνωση αρχείων εικόνας από τον κοινόχρηστο αποθηκευτικό χώρο"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Επιτρέπει στην εφαρμογή την ανάγνωση αρχείων εικόνας από τον κοινόχρηστο αποθηκευτικό σας χώρο."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"τροποποιεί ή διαγράφει το περιεχόμενο του κοινόχρηστου αποθηκευτικού χώρου σας"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Επιτρέπει στην εφαρμογή την εγγραφή του περιεχομένου του κοινόχρηστου αποθηκευτικού χώρου σας."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"πραγματοποιεί/λαμβάνει κλήσεις SIP"</string>
diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml
index 1cc8d50..e37dbcd 100644
--- a/core/res/res/values-en-rAU/strings.xml
+++ b/core/res/res/values-en-rAU/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Caller ID defaults to not restricted. Next call: Not restricted"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Service not provisioned."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"You can\'t change the caller ID setting."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Switched data to <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"You can change this at any time in Settings"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No mobile data service"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Emergency calling unavailable"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"No voice service"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Allows the app to read video files from your shared storage."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"read image files from shared storage"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Allows the app to read image files from your shared storage."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"read user selected image and video files from shared storage"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Allows the app to read image and video files that you select from your shared storage."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modify or delete the contents of your shared storage"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Allows the app to write the contents of your shared storage."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"make/receive SIP calls"</string>
diff --git a/core/res/res/values-en-rCA/strings.xml b/core/res/res/values-en-rCA/strings.xml
index 6fa02f3..3b6bf27 100644
--- a/core/res/res/values-en-rCA/strings.xml
+++ b/core/res/res/values-en-rCA/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Caller ID defaults to not restricted. Next call: Not restricted"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Service not provisioned."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"You can\'t change the caller ID setting."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Switched data to <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"You can change this at any time in Settings"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No mobile data service"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Emergency calling unavailable"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"No voice service"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Allows the app to read video files from your shared storage."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"read image files from shared storage"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Allows the app to read image files from your shared storage."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"read user selected image and video files from shared storage"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Allows the app to read image and video files that you select from your shared storage."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modify or delete the contents of your shared storage"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Allows the app to write the contents of your shared storage."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"make/receive SIP calls"</string>
diff --git a/core/res/res/values-en-rGB/strings.xml b/core/res/res/values-en-rGB/strings.xml
index fac706e..6faf78b 100644
--- a/core/res/res/values-en-rGB/strings.xml
+++ b/core/res/res/values-en-rGB/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Caller ID defaults to not restricted. Next call: Not restricted"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Service not provisioned."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"You can\'t change the caller ID setting."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Switched data to <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"You can change this at any time in Settings"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No mobile data service"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Emergency calling unavailable"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"No voice service"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Allows the app to read video files from your shared storage."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"read image files from shared storage"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Allows the app to read image files from your shared storage."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"read user selected image and video files from shared storage"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Allows the app to read image and video files that you select from your shared storage."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modify or delete the contents of your shared storage"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Allows the app to write the contents of your shared storage."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"make/receive SIP calls"</string>
diff --git a/core/res/res/values-en-rIN/strings.xml b/core/res/res/values-en-rIN/strings.xml
index 55c121ac..1f5b6a1 100644
--- a/core/res/res/values-en-rIN/strings.xml
+++ b/core/res/res/values-en-rIN/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Caller ID defaults to not restricted. Next call: Not restricted"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Service not provisioned."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"You can\'t change the caller ID setting."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Switched data to <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"You can change this at any time in Settings"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No mobile data service"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Emergency calling unavailable"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"No voice service"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Allows the app to read video files from your shared storage."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"read image files from shared storage"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Allows the app to read image files from your shared storage."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"read user selected image and video files from shared storage"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Allows the app to read image and video files that you select from your shared storage."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modify or delete the contents of your shared storage"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Allows the app to write the contents of your shared storage."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"make/receive SIP calls"</string>
diff --git a/core/res/res/values-en-rXC/strings.xml b/core/res/res/values-en-rXC/strings.xml
index 1b190e3..5e6afc3 100644
--- a/core/res/res/values-en-rXC/strings.xml
+++ b/core/res/res/values-en-rXC/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‏‎‏‎‏‎‎‏‏‎Caller ID defaults to not restricted. Next call: Not restricted‎‏‎‎‏‎"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‎‎‏‏‏‎‏‎‎‎‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎Service not provisioned.‎‏‎‎‏‎"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‎‎‏‎‏‏‎‎‏‎‏‎‏‎‏‎‏‏‎‎‏‏‎‎‎‏‏‎‏‏‎‏‏‏‏‏‎‎You can\'t change the caller ID setting.‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‏‎‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‏‏‎‎‏‏‎‎‎‎‏‏‎‎‏‎Switched data to ‎‏‎‎‏‏‎<xliff:g id="CARRIERDISPLAY">%s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‎You can change this anytime in Settings‎‏‎‎‏‎"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‎‏‏‎‎‏‏‎‎‎‏‏‏‏‏‏‎‎‎‎‏‎‏‏‏‎‎No mobile data service‎‏‎‎‏‎"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‎‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎Emergency calling unavailable‎‏‎‎‏‎"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‎‎‏‏‎‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‎‏‏‏‎‏‏‏‎‎‏‎No voice service‎‏‎‎‏‎"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‎‎‏‏‎Lockdown‎‏‎‎‏‎"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‏‏‎‏‏‏‎‎‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‏‏‎‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎999+‎‏‎‎‏‎"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‏‏‎‎‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‏‏‏‎New notification‎‏‎‎‏‎"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‎‏‏‏‎Virtual keyboard‎‏‎‎‏‎"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‎‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‏‎‎‏‏‎‏‎‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‎‎‎‎‎Physical keyboard‎‏‎‎‏‎"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‎‎‏‏‎‎‏‎‏‏‏‎‎‏‏‏‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‏‏‎‏‎‎‏‎Security‎‏‎‎‏‎"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‎‏‏‎‏‎‏‎‎‏‏‎‏‎‏‎‏‏‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‎‎‏‎‏‎‎‎Car mode‎‏‎‎‏‎"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‏‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‎‏‏‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‎‎Allows the app to read video files from your shared storage.‎‏‎‎‏‎"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‏‏‏‎‏‏‏‎‏‎‏‎‎‏‎‏‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‎‎‎‏‏‎‏‎‎‏‎‏‎read image files from shared storage‎‏‎‎‏‎"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‏‎Allows the app to read image files from your shared storage.‎‏‎‎‏‎"</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‏‏‎‏‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‎‏‏‎‎‏‎‎‏‎‎‎‎‏‎‎read user selected image and video files from shared storage‎‏‎‎‏‎"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‎‏‏‏‎‏‎‎‏‏‎‎‏‎Allows the app to read image and video files that you select from your shared storage.‎‏‎‎‏‎"</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‎‏‎‎‎‏‏‎‎‎‎‎‎‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‏‎‏‏‏‎‎‎modify or delete the contents of your shared storage‎‏‎‎‏‎"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‎‏‏‎‏‏‎‎‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‎‎‏‎‎‏‎‎‎‎‏‎‎‎‏‏‎‏‏‎‏‏‏‏‎‎Allows the app to write the contents of your shared storage.‎‏‎‎‏‎"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‎‎‏‎‏‎‎‎‏‏‏‏‎‎‎‎‎‎‏‎‏‎‎‏‏‏‏‏‏‏‎‎‎‎‎‏‎‎‎‎‎‏‏‎‎‎‎‏‎‏‎make/receive SIP calls‎‏‎‎‏‎"</string>
diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml
index 106935c2..247aed6 100644
--- a/core/res/res/values-es-rUS/strings.xml
+++ b/core/res/res/values-es-rUS/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"El Identificador de llamadas está predeterminado en no restringido. Llamada siguiente: no restringido"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Servicio no suministrado."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"No puedes cambiar la configuración del identificador de llamadas."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Se cambiaron los datos a <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Puedes cambiar esto cuando quieras en Configuración"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No hay ningún servicio de datos móviles"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Servicio de llamadas de emergencia no disponible"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sin servicio de voz"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueo"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nueva"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo auto"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que la app lea los archivos de video del almacenamiento compartido."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"leer los archivos de imagen del almacenamiento compartido"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que la app lea los archivos de imagen del almacenamiento compartido."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"leer los archivos de imagen y video que el usuario haya seleccionado del almacenamiento compartido"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Permite que la app lea los archivos de imagen y video que selecciones del almacenamiento compartido."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"cambiar o borrar contenido de almacenamiento compartido"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Editar almacen. compartido"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"realizar/recibir llamadas SIP"</string>
@@ -1970,8 +1973,8 @@
     <string name="usb_mtp_launch_notification_description" msgid="6942535713629852684">"Presiona para ver archivos"</string>
     <string name="pin_target" msgid="8036028973110156895">"Fijar"</string>
     <string name="pin_specific_target" msgid="7824671240625957415">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
-    <string name="unpin_target" msgid="3963318576590204447">"No fijar"</string>
-    <string name="unpin_specific_target" msgid="3859828252160908146">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+    <string name="unpin_target" msgid="3963318576590204447">"Dejar de fijar"</string>
+    <string name="unpin_specific_target" msgid="3859828252160908146">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
     <string name="app_info" msgid="6113278084877079851">"Información de apps"</string>
     <string name="negative_duration" msgid="1938335096972945232">"−<xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="demo_starting_message" msgid="6577581216125805905">"Iniciando demostración…"</string>
diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml
index 3ae013b..63383fc 100644
--- a/core/res/res/values-es/strings.xml
+++ b/core/res/res/values-es/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"La identificación del emisor presenta el valor predeterminado de no restringido. Siguiente llamada: No restringido"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"El servicio no se suministra."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"No puedes modificar la identificación de emisor."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Datos cambiados a <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Puedes cambiar esta opción en cualquier momento en Ajustes"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"No hay ningún servicio de datos móviles"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Servicio de llamadas de emergencia no disponible"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sin servicio de voz"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueo de seguridad"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt; 999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nueva"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo de coche"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que la aplicación lea archivos de vídeo desde tu almacenamiento compartido."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"leer archivos de imagen desde el almacenamiento compartido"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que la aplicación lea archivos de imagen desde tu almacenamiento compartido."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"editar/eliminar contenido de almacenamiento compartido"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite que app edite contenido de almacenamiento compartido."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"hacer/recibir llamadas SIP"</string>
diff --git a/core/res/res/values-et/strings.xml b/core/res/res/values-et/strings.xml
index 182aa65..6c0b4ff 100644
--- a/core/res/res/values-et/strings.xml
+++ b/core/res/res/values-et/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Helistaja ID pole vaikimisi piiratud. Järgmine kõne: pole piiratud"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Teenus pole ette valmistatud."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Helistaja ID seadet ei saa muuta."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Mobiilne andmeside lülitati operaatorile <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Saate seda igal ajal seadetes muuta"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Mobiilne andmesideteenus puudub"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hädaabikõned pole saadaval"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Häälkõned pole saadaval"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lukustamine"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Uus märguanne"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuaalne klaviatuur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Füüsiline klaviatuur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Turvalisus"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autorežiim"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Võimaldab rakendusel lugeda teie jagatud salvestusruumis olevaid videofaile."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lugeda teie jagatud salvestusruumis olevaid pildifaile"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Võimaldab rakendusel lugeda teie jagatud salvestusruumis olevaid pildifaile."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"Jagatud salvestusruumi sisu muutmine või kustutamine"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Lubab rakendusel kirjutada jagatud salvestusruumi sisu."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP-kõnede tegemine/vastuvõtmine"</string>
diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml
index 31cc0b6..ff12661 100644
--- a/core/res/res/values-eu/strings.xml
+++ b/core/res/res/values-eu/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Deitzailearen identitatea zerbitzuaren balio lehenetsiak ez du murriztapenik ezartzen. Hurrengo deia: murriztapenik gabe."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Zerbitzua ez da hornitu."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ezin duzu aldatu deitzailearen identitatearen ezarpena."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g> operadorearen datu-konexiora aldatu zara"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ezarpenak atalean alda dezakezu aukera hori"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ez dago mugikorreko datu-zerbitzurik"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Ezin da egin larrialdi-deirik"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ez dago ahots-deien zerbitzurik"</string>
@@ -175,7 +177,7 @@
     <string name="low_memory" product="watch" msgid="3479447988234030194">"Erlojuaren memoria beteta dago. Tokia egiteko, ezabatu fitxategi batzuk."</string>
     <string name="low_memory" product="tv" msgid="6663680413790323318">"Android TV gailuaren memoria beteta dago. Tokia egiteko, ezabatu fitxategi batzuk."</string>
     <string name="low_memory" product="default" msgid="2539532364144025569">"Telefonoaren memoria beteta dago. Tokia egiteko, ezabatu fitxategi batzuk."</string>
-    <string name="ssl_ca_cert_warning" msgid="7233573909730048571">"{count,plural, =1{Ziurtagiri-emaile bat dago instalatuta}other{Ziurtagiri-emaile bat baino gehiago daude instalatuta}}"</string>
+    <string name="ssl_ca_cert_warning" msgid="7233573909730048571">"{count,plural, =1{Autoritate ziurtagiri-emaile bat dago instalatuta}other{Autoritate ziurtagiri-emaile bat baino gehiago daude instalatuta}}"</string>
     <string name="ssl_ca_cert_noti_by_unknown" msgid="4961102218216815242">"Hirugarren alderdi ezezagun baten arabera"</string>
     <string name="ssl_ca_cert_noti_by_administrator" msgid="4564941950768783879">"Laneko profilen administratzaileak"</string>
     <string name="ssl_ca_cert_noti_managed" msgid="217337232273211674">"<xliff:g id="MANAGING_DOMAIN">%s</xliff:g> da arduraduna"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blokeoa"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Jakinarazpen berria"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teklatu birtuala"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teklatu fisikoa"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurtasuna"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Auto modua"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Biltegi partekatuko bideo-fitxategiak irakurtzeko baimena ematen die aplikazioei."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"irakurri biltegi partekatuko irudi-fitxategiak"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Biltegi partekatuko irudi-fitxategiak irakurtzeko baimena ematen die aplikazioei."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"aldatu edo ezabatu biltegi partekatuko edukia"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Biltegi partekatuko edukian idazteko baimena ematen die aplikazioei."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"egin/jaso SIP deiak"</string>
@@ -1455,7 +1460,7 @@
     <string name="permdesc_requestDeletePackages" msgid="6133633516423860381">"Paketeak ezabatzeko eskatzea baimentzen die aplikazioei."</string>
     <string name="permlab_requestIgnoreBatteryOptimizations" msgid="7646611326036631439">"eskatu bateria-optimizazioei ez ikusi egitea"</string>
     <string name="permdesc_requestIgnoreBatteryOptimizations" msgid="634260656917874356">"Bateriaren optimizazioei ez ikusi egiteko baimena eskatzea baimentzen die aplikazioei."</string>
-    <string name="permlab_queryAllPackages" msgid="2928450604653281650">"Kontsultatu pakete guztiak"</string>
+    <string name="permlab_queryAllPackages" msgid="2928450604653281650">"kontsultatu pakete guztiak"</string>
     <string name="permdesc_queryAllPackages" msgid="5339069855520996010">"Instalatutako pakete guztiak ikusteko baimena ematen dio aplikazioari."</string>
     <string name="tutorial_double_tap_to_zoom_message_short" msgid="1842872462124648678">"Sakatu birritan zooma kontrolatzeko"</string>
     <string name="gadget_host_error_inflating" msgid="2449961590495198720">"Ezin izan da widgeta gehitu."</string>
diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml
index 58a7f62..029d9d1 100644
--- a/core/res/res/values-fa/strings.xml
+++ b/core/res/res/values-fa/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"پیش‌فرض شناسه تماس‌گیرنده روی غیرمحدود است. تماس بعدی: بدون محدودیت"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"سرویس دارای مجوز نیست."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"‏شما می‎توانید تنظیم شناسه تماس‌گیرنده را تغییر دهید."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"داده به <xliff:g id="CARRIERDISPLAY">%s</xliff:g> تغییر کرد"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"هرزمان بخواهید می‌توانید این را در «تنظیمات» تغییر دهید"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"بدون سرویس داده تلفن همراه"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"تماس اضطراری دردسترس نیست"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"سرویس صوتی دردسترس نیست"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"قفل همه"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"۹۹۹+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"اعلان جدید"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"صفحه‌‌کلید مجازی"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"صفحه‌کلید فیزیکی"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"امنیت"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"حالت خودرو"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"به برنامه اجازه می‌دهد فایل‌های ویدیویی موجود در فضای ذخیره‌سازی هم‌رسانی‌شده را بخواند."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"خواندن فایل‌های تصویری موجود در فضای ذخیره‌سازی مشترک"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"به برنامه اجازه می‌دهد فایل‌های تصویری موجود در فضای ذخیره‌سازی مشترک را بخواند."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"تغییر یا حذف محتوای فضای ذخیره‌سازی مشترک"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"به برنامه اجازه می‌دهد محتوای فضای ذخیره‌سازی مشترکتان را بنویسد."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"‏تماس گرفتن/دریافت تماس از طریق SIP"</string>
diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml
index 31d2571..487d199 100644
--- a/core/res/res/values-fi/strings.xml
+++ b/core/res/res/values-fi/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Soittajan tunnukseksi muutetaan rajoittamaton. Seuraava puhelu: ei rajoitettu"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Palvelua ei tarjota."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Et voi muuttaa soittajan tunnuksen asetusta."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Data vaihdettu: <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Voit vaihtaa valintasi milloin tahansa asetuksista."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ei mobiilidatapalvelua"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hätäpuhelut eivät ole käytettävissä"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ei äänipuheluja"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lukitse"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Uusi ilmoitus"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuaalinen näppäimistö"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyysinen näppäimistö"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Turvallisuus"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autotila"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Sallii sovelluksen lukea jaetun tallennustilan videotiedostoja."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lue jaetun tallennustilan kuvatiedostoja"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Sallii sovelluksen lukea jaetun tallennustilan kuvatiedostoja."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"muokata tai poistaa jaetun tallennustilan sisältöä"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Antaa sovelluksen kirjoittaa jaetun tallennustilan sisällön."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"soita/vastaanota SIP-puheluja"</string>
diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml
index 5150da9..722a66e 100644
--- a/core/res/res/values-fr-rCA/strings.xml
+++ b/core/res/res/values-fr-rCA/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Par défaut, les numéros des appelants ne sont pas restreints. Appel suivant : non restreint"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Ce service n\'est pas pris en charge."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Impossible de modifier le paramètre relatif au numéro de l\'appelant."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Données changées à <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Vous pouvez modifier cette option en tout temps dans les paramètres"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Aucun service de données cellulaires"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Le service d\'appel d\'urgence n\'est pas accessible"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Aucun service vocal"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Verrouillage"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nouvelle notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Clavier virtuel"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Clavier physique"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sécurité"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode Voiture"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permet à l\'application de lire les fichiers vidéo de votre espace de stockage partagé."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lire des fichiers d\'image à partir de l\'espace de stockage partagé"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permet à l\'application de lire les fichiers d\'image de votre espace de stockage partagé."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modifier ou supprimer le contenu de votre espace de stockage partagé"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Autorise l\'application à écrire le contenu de votre espace de stockage partagé."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"faire et recevoir des appels SIP"</string>
diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml
index 3736890..e20fb5f 100644
--- a/core/res/res/values-fr/strings.xml
+++ b/core/res/res/values-fr/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Par défaut, les numéros des appelants ne sont pas restreints. Appel suivant : non restreint"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Ce service n\'est pas pris en charge."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Impossible de modifier le paramètre relatif au numéro de l\'appelant."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Données transférées vers <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Vous pouvez modifier cette option à tout moment dans les paramètres."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Aucun service de données mobiles"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Appels d\'urgence non disponibles"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Aucun service vocal"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Verrouiller"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nouvelle notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Clavier virtuel"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Clavier physique"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sécurité"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode Voiture"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permettre à l\'application de lire les fichiers vidéo de votre espace de stockage partagé."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lire les fichiers image de l\'espace de stockage partagé"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permettre à l\'application de lire les fichiers image de votre espace de stockage partagé."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modifier/supprimer contenu mémoire stockage partagée"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permet de modifier le contenu mémoire de stockage partagée."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"effectuer/recevoir des appels SIP"</string>
diff --git a/core/res/res/values-gl/strings.xml b/core/res/res/values-gl/strings.xml
index 7575d68..9a3bfd7 100644
--- a/core/res/res/values-gl/strings.xml
+++ b/core/res/res/values-gl/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"O valor predeterminado do identificador de chamada é restrinxido. Próxima chamada: non restrinxido"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Servizo non ofrecido."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Non podes cambiar a configuración do identificador de chamada."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Cambiouse a conexión de datos a <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Podes cambiar esta opción en calquera momento en Configuración"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Non hai servizo de datos para móbiles"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"As chamadas de emerxencia non están dispoñibles"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Non hai servizo de chamadas de voz"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloquear"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nova"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguranza"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo coche"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que a aplicación acceda a ficheiros de vídeo do almacenamento compartido."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"acceder a ficheiros de imaxe do almacenamento compartido"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que a aplicación acceda a ficheiros de imaxe do almacenamento compartido."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modificar ou eliminar o almacenamento compartido"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite á aplicación escribir no almacenamento compartido."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"facer/recibir chamadas SIP"</string>
diff --git a/core/res/res/values-gu/strings.xml b/core/res/res/values-gu/strings.xml
index 42bad0a..f312d2b 100644
--- a/core/res/res/values-gu/strings.xml
+++ b/core/res/res/values-gu/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"કૉલર ID પ્રતિબંધિત નહીં પર ડિફોલ્ટ છે. આગલો કૉલ: પ્રતિબંધિત નહીં"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"સેવાની જોગવાઈ કરી નથી."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"તમે કૉલર ID સેટિંગ બદલી શકતાં નથી."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ડેટા <xliff:g id="CARRIERDISPLAY">%s</xliff:g> પર સ્વિચ કર્યો"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"સેટિંગમાં તમે આને કોઈપણ સમયે બદલી શકો છો"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"કોઈ મોબાઇલ ડેટા સેવા નથી"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"કોટોકટીની કૉલિંગ સેવા અનુપલબ્ધ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"કોઈ વૉઇસ સેવા નથી"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"લૉકડાઉન"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"નવું નોટિફિકેશન"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"વર્ચ્યુઅલ કીબોર્ડ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ભૌતિક કીબોર્ડ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"સુરક્ષા"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"કાર મોડ"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ઍપને તમારા શેર કરાયેલા સ્ટોરેજમાંથી વીડિયો ફાઇલો વાંચવાની મંજૂરી આપે છે."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"શેર કરાયેલા સ્ટોરેજમાંથી છબી ફાઇલો વાંચવા માટે"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ઍપને તમારા શેર કરાયેલા સ્ટોરેજમાંથી છબી ફાઇલો વાંચવાની મંજૂરી આપે છે."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"શેર કરેલા સ્ટોરેજ કન્ટેન્ટમાં ફેરફાર કરો/ડિલીટ કરો"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"શેર કરેલા સ્ટોરેજ કન્ટેન્ટમાં લખવાની મંજૂરી આપે છે."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP કૉલ્સ કરો/પ્રાપ્ત કરો"</string>
diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml
index 83cacea..b103619 100644
--- a/core/res/res/values-hi/strings.xml
+++ b/core/res/res/values-hi/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"कॉलर आईडी डिफ़ॉल्ट रूप से सीमित नहीं है. अगली कॉल: सीमित नहीं"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"सेवा प्रावधान की हुई नहीं है."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"आप कॉलर आईडी सेटिंग नहीं बदल सकते."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"डेटा को <xliff:g id="CARRIERDISPLAY">%s</xliff:g> पर स्विच किया गया"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"सेटिंग में जाकर, इसे कभी भी बंद किया जा सकता है"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"मोबाइल डेटा सेवा पर रोक लगा दी गई है"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"आपातकालीन कॉल पर रोक लगा दी गई है"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"कोई वॉइस सेवा नहीं है"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"फ़ाेन लॉक करें"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नई सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"वर्चुअल कीबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"सामान्य कीबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"अपने डिवाइस के शेयर किए गए स्टोरेज से, ऐप्लिकेशन को वीडियो फ़ाइलें पढ़ने की अनुमति दें."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"डिवाइस के शेयर किए गए स्टोरेज से, इमेज फ़ाइलें देखने की अनुमति"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"अपने डिवाइस के शेयर किए गए स्टोरेज से, ऐप्लिकेशन को इमेज फ़ाइलें देखने की अनुमति दें."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"आपकी शेयर की गई मेमोरी की सामग्री में बदलाव करना या उसे मिटाना"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ऐप्लिकेशन को आपकी शेयर की गई मेमोरी की सामग्री लिखने देती है."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP कॉल करें/पाएं"</string>
@@ -1136,7 +1141,7 @@
     <string name="copy" msgid="5472512047143665218">"कॉपी करें"</string>
     <string name="failed_to_copy_to_clipboard" msgid="725919885138539875">"क्लिपबोर्ड पर कॉपी नहीं हो सका"</string>
     <string name="paste" msgid="461843306215520225">"चिपकाएं"</string>
-    <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे पाठ के रूप में चिपकाएं"</string>
+    <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे टेक्स्ट के रूप में चिपकाएं"</string>
     <string name="replace" msgid="7842675434546657444">"बदलें•"</string>
     <string name="delete" msgid="1514113991712129054">"मिटाएं"</string>
     <string name="copyUrl" msgid="6229645005987260230">"यूआरएल को कॉपी करें"</string>
diff --git a/core/res/res/values-hr/strings.xml b/core/res/res/values-hr/strings.xml
index 6f81009..346ea39 100644
--- a/core/res/res/values-hr/strings.xml
+++ b/core/res/res/values-hr/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Zadana postavka ID-a pozivatelja nema ograničenje. Sljedeći poziv: Nije ograničen"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Usluga nije rezervirana."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ne možete promijeniti postavku ID-a pozivatelja."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Podaci su prebačeni na <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"To uvijek možete promijeniti u postavkama"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nema podatkovne mobilne usluge"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hitni pozivi nisu dostupni"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Nema glasovnih usluga"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova obavijest"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtualna tipkovnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tipkovnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sigurnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način rada u automobilu"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Aplikaciji omogućuje čitanje videodatoteka iz dijeljene pohrane."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"čitati slikovne datoteke iz dijeljene pohrane"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Aplikaciji omogućuje čitanje slikovnih datoteka iz dijeljene pohrane."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"čitati slikovne i videodatoteke koje je korisnik odabrao iz dijeljene pohrane"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Aplikaciji omogućuje čitanje slikovnih i videodatoteka koje odaberete iz dijeljene pohrane."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"izmjena ili brisanje sadržaja dijeljene pohrane"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Aplikaciji omogućuje pisanje sadržaja u dijeljenu pohranu."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"upućivanje/primanje SIP poziva"</string>
diff --git a/core/res/res/values-hu/strings.xml b/core/res/res/values-hu/strings.xml
index 71687d4..291ae7d 100644
--- a/core/res/res/values-hu/strings.xml
+++ b/core/res/res/values-hu/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"A hívóazonosító alapértelmezett értéke nem korlátozott. Következő hívás: nem korlátozott"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"A szolgáltatás nincs biztosítva."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nem tudja módosítani a hívó fél azonosítója beállítást."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Adatforgalom átváltva a következőre: <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"A Beállításokban ezt bármikor módosíthatja"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nincs mobiladat-szolgáltatás"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Segélyhívás nem lehetséges"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Hangszolgáltatás letiltva"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zárolás"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Új értesítés"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuális billentyűzet"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizikai billentyűzet"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Biztonság"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autós üzemmód"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Engedélyezi az alkalmazásnak a megosztott tárhelyen található videófájlok olvasását."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"a megosztott tárhelyen található képfájlok olvasása"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Engedélyezi az alkalmazásnak a megosztott tárhelyen található képfájlok olvasását."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"a közös tárhely tartalmainak törlése és módosítása"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Engedélyezi az alkalmazás számára a közös tárhely tartalmainak felülírását."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP-hívások indítása/fogadása"</string>
diff --git a/core/res/res/values-hy/strings.xml b/core/res/res/values-hy/strings.xml
index b7146f0..ce62e20 100644
--- a/core/res/res/values-hy/strings.xml
+++ b/core/res/res/values-hy/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Զանգողի ID-ն լռելյայն չսահմանափակված է: Հաջորդ զանգը` չսահմանափակված"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Ծառայությունը չի տրամադրվում:"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Դուք չեք կարող փոխել զանգողի ID-ի կարգավորումները:"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Օգտագործվում է <xliff:g id="CARRIERDISPLAY">%s</xliff:g>-ի բջջային ինտերնետը"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Այս գործառույթը կարող եք փոփոխել Կարգավորումներում"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Բջջային ինտերնետի ծառայությունն արգելափակված է"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Շտապ կանչերը հասանելի չեն"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ձայնային ծառայությունն անհասանելի է"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Արգելափակում"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Նոր ծանուցում"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Վիրտուալ ստեղնաշար"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Ֆիզիկական ստեղնաշար"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Անվտանգություն"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Մեքենայի ռեժիմ"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Հավելվածին թույլ է տալիս կարդալ ձեր սարքի ընդհանուր հիշողության մեջ պահված վիդեո ֆայլերը։"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"կարդալ ձեր սարքի ընդհանուր հիշողության մեջ պահված գրաֆիկական ֆայլերը"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Հավելվածին թույլ է տալիս կարդալ ձեր սարքի ընդհանուր հիշողության մեջ պահված գրաֆիկական ֆայլերը։"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"փոփոխել կամ ջնջել ձեր ընդհանուր հիշողության բովանդակությունը"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Հավելվածին թույլ է տալիս փոփոխել ձեր ընդհանուր հիշողության պարունակությունը:"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"կատարել կամ ստանալ SIP զանգեր"</string>
diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml
index 66fc3fb..2be19a6 100644
--- a/core/res/res/values-in/strings.xml
+++ b/core/res/res/values-in/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID penelepon diatur default ke tidak dibatasi. Panggilan selanjutnya: Tidak dibatasi"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Layanan tidak diperlengkapi."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Anda tidak dapat mengubah setelan ID penelepon."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Mengalihkan data seluler ke <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Anda dapat mengubahnya kapan saja di Setelan"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Tidak ada layanan data seluler"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Panggilan darurat tidak tersedia"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Tidak ada layanan panggilan suara"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kunci total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notifikasi baru"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Keyboard virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Keyboard fisik"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Keamanan"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode mobil"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Mengizinkan aplikasi membaca file video dari penyimpanan bersama."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"membaca file gambar dari penyimpanan bersama"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Mengizinkan aplikasi membaca file gambar dari penyimpanan bersama."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"memodifikasi atau menghapus konten penyimpanan bersama Anda"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Mengizinkan aplikasi menulis konten penyimpanan bersama Anda."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"lakukan/terima panggilan SIP"</string>
diff --git a/core/res/res/values-is/strings.xml b/core/res/res/values-is/strings.xml
index 64b2340..f5473a2 100644
--- a/core/res/res/values-is/strings.xml
+++ b/core/res/res/values-is/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Númerabirting er sjálfgefið án takmarkana. Næsta símtal: Án takmarkana"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Þjónustu ekki útdeilt."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Þú getur ekki breytt stillingu númerabirtingar."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Skipt yfir í farsímagögn hjá <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Þú getur breytt þessu hvenær sem er í stillingum"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Engin gagnaþjónusta fyrir farsíma"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Neyðarsímtöl eru ekki í boði"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Símtöl eru ekki í boði"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Læsing"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ný tilkynning"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Sýndarlyklaborð"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Vélbúnaðarlyklaborð"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Öryggi"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Bílastilling"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Leyfir forritinu að lesa myndskeiðaskrár úr samnýtta geymslurýminu þínu."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lesa myndskrár úr samnýttu geymslurými"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Leyfir forritinu að lesa myndskrár úr samnýtta geymslurýminu þínu."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"breyta eða eyða innihaldi samnýtta geymslurýmisins"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Leyfir forriti að skrifa í innihald samnýtta geymslurýmisins."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"hringja/svara SIP-símtölum"</string>
diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml
index d25bb06..f88ad40 100644
--- a/core/res/res/values-it/strings.xml
+++ b/core/res/res/values-it/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID chiamante generalmente non limitato. Prossima chiamata: non limitato"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Servizio non fornito."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Non è possibile modificare l\'impostazione ID chiamante."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"I dati sono stati trasferiti a <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Puoi modificare questa opzione in qualsiasi momento in Impostazioni"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nessun servizio dati mobile"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Chiamate di emergenza non disponibili"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Nessun servizio di telefonia"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blocco"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nuova notifica"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastiera virtuale"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastiera fisica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sicurezza"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modalità automobile"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Consente all\'app di leggere i file video dal tuo spazio di archiviazione condiviso."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"Lettura dei file immagine dallo spazio di archiviazione condiviso"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Consente all\'app di leggere i file immagine dal tuo spazio di archiviazione condiviso."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"Modifica/eliminazione dei contenuti dell\'archivio condiviso"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Consente all\'app di modificare i contenuti del tuo archivio condiviso."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"invio/ricezione di chiamate SIP"</string>
diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml
index a1373c3..29e33dff 100644
--- a/core/res/res/values-iw/strings.xml
+++ b/core/res/res/values-iw/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"זיהוי מתקשר עובר כברירת מחדל למצב לא מוגבל. השיחה הבאה: לא מוגבלת"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"השירות לא הוקצה."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"אינך יכול לשנות את הגדרת זיהוי המתקשר."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"הנתונים עברו אל <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"תמיד אפשר לשנות זאת ב\'הגדרות\'"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"אין שירות של חבילת גלישה"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"שיחות חירום לא זמינות"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"אין אפשרות לבצע שיחות רגילות"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"נעילה"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"התראה חדשה"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"מקלדת וירטואלית"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"מקלדת פיזית"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"אבטחה"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"מצב נהיגה"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"מאפשרת לאפליקציה לקרוא קובצי וידאו מתוך האחסון המשותף."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"קריאה של קובצי תמונה מתוך האחסון המשותף"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"מאפשרת לאפליקציה לקרוא קובצי תמונה מתוך האחסון המשותף."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"שינוי או מחיקה של תוכן האחסון המשותף שלך"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"מאפשרת לאפליקציה לכתוב את התוכן של האחסון המשותף."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"‏ביצוע/קבלה של שיחות SIP"</string>
@@ -1251,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"מתבצעת הפעלה של אפליקציות."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"תהליך האתחול בשלבי סיום."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"לחצת על לחצן ההפעלה – בדרך כלל הפעולה הזו מכבה את המסך.\n\nעליך לנסות להקיש בעדינות במהלך ההגדרה של טביעת האצבע שלך."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"כדי לסיים את ההגדרה צריך לכבות את המסך"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"לסיום ההגדרה, יש לכבות את המסך"</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"השבתה"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"להמשיך לאמת את טביעת האצבע שלך?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"לחצת על לחצן ההפעלה – בדרך כלל הפעולה הזו מכבה את המסך.\n\nעליך לנסות להקיש בעדינות כדי לאמת את טביעת האצבע שלך."</string>
diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml
index ed55a7f..eb5cc2f 100644
--- a/core/res/res/values-ja/strings.xml
+++ b/core/res/res/values-ja/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"既定: 発信者番号通知、次の発信: 通知"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"提供可能なサービスがありません。"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"発信者番号の設定は変更できません。"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"データが <xliff:g id="CARRIERDISPLAY">%s</xliff:g> に切り替わりました"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"これは [設定] でいつでも変更できます"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"モバイルデータ サービスのブロック"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"緊急通報のブロック"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"音声通話サービス停止"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ロックダウン"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新しい通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"仮想キーボード"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"物理キーボード"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"セキュリティ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"運転モード"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"共有ストレージからの動画ファイルの読み取りをアプリに許可します。"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"共有ストレージからの画像ファイルの読み取り"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"共有ストレージからの画像ファイルの読み取りをアプリに許可します。"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"共有ストレージのコンテンツの変更または削除"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"共有ストレージのコンテンツの書き込みをアプリに許可します。"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP通話の発着信"</string>
diff --git a/core/res/res/values-ka/strings.xml b/core/res/res/values-ka/strings.xml
index 0b06d7c..634da8e 100644
--- a/core/res/res/values-ka/strings.xml
+++ b/core/res/res/values-ka/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ნაგულისხმებად დაყენებულია ნომრის დაფარვის გამორთვა. შემდეგი ზარი: არ არის დაფარული."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"სერვისი არ არის მიწოდებული."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"არ შეგიძლიათ აბონენტის ID პარამეტრების შეცვლა."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"მონაცემები გადართულია <xliff:g id="CARRIERDISPLAY">%s</xliff:g>-ზე"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ამის შეცვლა ნებისმიერ დროს შეგიძლიათ პარამეტრებში"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"მობილური ინტერნეტის სერვისი არ არის"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"გადაუდებელი ზარი მიუწვდომელია"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ხმოვანი ზარების სერვისი არ არის"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"დაბლოკვა"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ახალი შეტყობინება"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ვირტუალური კლავიატურა"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ფიზიკური კლავიატურა"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"უსაფრთხოება"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"მანქანის რეჟიმი"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"საშუალებას აძლევს აპს, წაიკითხოს ვიდეო ფაილები თქვენი ზიარი მეხსიერებიდან."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"სურათების ფაილების წაკითხვა ზიარი მეხსიერებიდან"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"საშუალებას აძლევს აპს, წაიკითხოს სურათის ფაილები თქვენი ზიარი მეხსიერებიდან."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"თქვენი ზიარი მეხსიერების შიგთავსის შეცვლა ან წაშლა"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"საშუალებას აძლევს აპს, ჩაწეროს თქვენი ზიარი მეხსიერების შიგთავსი."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP ზარების წამოწყება/მიღება"</string>
diff --git a/core/res/res/values-kk/strings.xml b/core/res/res/values-kk/strings.xml
index a44d09d..20e7d3e 100644
--- a/core/res/res/values-kk/strings.xml
+++ b/core/res/res/values-kk/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Қоңырау шалушының жеке анықтағышы бастапқы бойынша шектелмеген. Келесі қоңырау: Шектелмеген"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Қызмет ұсынылмаған."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Қоңырау шалушы идентификаторы параметрін өзгерту мүмкін емес."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Деректер <xliff:g id="CARRIERDISPLAY">%s</xliff:g> операторына ауыстырылды"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Бұны кез келген уақытта \"Параметрлер\" бөлімінен өзгертуге болады."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Мобильдік интернет қызметі жоқ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Жедел қызметке қоңырау шалу қолжетімді емес"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Дауыстық қоңыраулар қызметі жоқ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Құлыптау"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Жаңа хабарландыру"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуалдық пернетақта"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физикалық пернетақта"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Қауіпсіздік"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Көлік режимі"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Қолданбаға ортақ жадтың бейнефайлдарын оқуға мүмкіндік береді."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ортақ жадтың кескін файлдарын оқу"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Қолданбаға ортақ жадтың кескін файлдарын оқуға мүмкіндік береді."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ортақ жадтың мазмұнын өзгерту немесе жою"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Қолданбаға ортақ жадтың мазмұнын жазуға мүмкіндік береді."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP қоңырауларын шалу/қабылдау"</string>
diff --git a/core/res/res/values-km/strings.xml b/core/res/res/values-km/strings.xml
index 2fada73..22bb80f 100644
--- a/core/res/res/values-km/strings.xml
+++ b/core/res/res/values-km/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"មិន​បាន​ដាក់កម្រិត​លំនាំដើម​លេខ​សម្គាល់​អ្នក​ហៅ។ ការ​ហៅ​បន្ទាប់៖ មិន​បាន​ដាក់​កម្រិត។"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"មិន​បាន​ផ្ដល់​សេវាកម្ម។"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"អ្នក​មិន​អាច​ប្ដូរ​ការ​កំណត់​លេខ​សម្គាល់​អ្នក​ហៅ​បានទេ។"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"បានប្ដូរទិន្នន័យទៅ <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"អ្នក​អាច​ផ្លាស់ប្ដូរ​លក្ខណៈនេះ​នៅ​ពេល​ណា​ក៏​បាន​នៅ​ក្នុង​ការ​កំណត់"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"គ្មាន​​សេវាកម្ម​ទិន្នន័យ​ចល័ត​ទេ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ការ​ហៅ​បន្ទាន់​មិន​អាច​ប្រើ​បាន​ទេ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"គ្មាន​សេវាកម្ម​ជា​សំឡេង​ទេ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ការចាក់​សោ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ការជូនដំណឹងថ្មី"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ក្ដារ​ចុច​និម្មិត"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ក្ដារចុច​រូបវន្ត"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"សុវត្ថិភាព"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"មុខងារ​រថយន្ត"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"អនុញ្ញាតឱ្យ​កម្មវិធី​អានឯកសារវីដេអូពីទំហំផ្ទុករួមរបស់អ្នក។"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"អានឯកសាររូបភាពពីទំហំ​ផ្ទុករួម"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"អនុញ្ញាតឱ្យ​កម្មវិធី​អានឯកសាររូបភាពពីទំហំផ្ទុករួមរបស់អ្នក។"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"កែប្រែ ឬលុប​ខ្លឹមសារនៃ​ទំហំផ្ទុករួម​របស់អ្នក"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"អនុញ្ញាតឱ្យ​កម្មវិធី​សរសេរខ្លឹមសារនៃ​ទំហំផ្ទុករួម​របស់អ្នក។"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"បង្កើត/ទទួល ការ​ហៅ SIP"</string>
diff --git a/core/res/res/values-kn/strings.xml b/core/res/res/values-kn/strings.xml
index c39d9f7..953dd60 100644
--- a/core/res/res/values-kn/strings.xml
+++ b/core/res/res/values-kn/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ಕರೆಮಾಡುವವರ ID ಅನ್ನು ನಿರ್ಬಂಧಿಸದಿರುವಂತೆ ಡಿಫಾಲ್ಟ್ ಮಾಡಲಾಗಿದೆ. ಮುಂದಿನ ಕರೆ: ನಿರ್ಬಂಧಿಸಲಾಗಿಲ್ಲ"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ಸೇವೆಯನ್ನು ಪೂರೈಸಲಾಗಿಲ್ಲ."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"ನೀವು ಕಾಲರ್‌ ID ಸೆಟ್ಟಿಂಗ್‌ ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g> ಗೆ ಡೇಟಾವನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ನೀವು ಇದನ್ನು ಯಾವುದೇ ಸಮಯದಲ್ಲಿಯೂ ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಬದಲಿಸಬಹುದು"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ಮೊಬೈಲ್ ಡೇಟಾ ಸೇವೆಯಿಲ್ಲ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ತುರ್ತು ಕರೆ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ಧ್ವನಿ ಸೇವೆಯಿಲ್ಲ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ಲಾಕ್‌ಡೌನ್‌"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ಹೊಸ ಅಧಿಸೂಚನೆ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ವರ್ಚುಯಲ್ ಕೀಬೋರ್ಡ್"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ಭೌತಿಕ ಕೀಬೋರ್ಡ್‌"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ಭದ್ರತೆ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ಕಾರ್ ಮೋಡ್"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ನಿಮ್ಮ ಹಂಚಿಕೊಂಡ ಸಂಗ್ರಹಣೆಯಿಂದ ವೀಡಿಯೊ ಫೈಲ್‌ಗಳನ್ನು ಓದಲು ಆ್ಯಪ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ಹಂಚಿಕೊಂಡ ಸಂಗ್ರಹಣೆಯಿಂದ ಚಿತ್ರದ ಫೈಲ್‌ಗಳನ್ನು ಓದಿ"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ನಿಮ್ಮ ಹಂಚಿಕೊಂಡ ಸಂಗ್ರಹಣೆಯಿಂದ ಚಿತ್ರದ ಫೈಲ್‌ಗಳನ್ನು ಓದಲು ಆ್ಯಪ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ನಿಮ್ಮ ಹಂಚಿಕೊಂಡ ಸಂಗ್ರಹಣೆಯ ವಿಷಯಗಳನ್ನು ಮಾರ್ಪಡಿಸಿ ಅಥವಾ ಅಳಿಸಿ"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ನಿಮ್ಮ ಹಂಚಿಕೊಂಡ ಸಂಗ್ರಹಣೆಯ ವಿಷಯಗಳನ್ನು ಬರೆಯಲು ಆ್ಯಪ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"ಎಸ್‌ಐಪಿ ಕರೆಗಳನ್ನು ಮಾಡಿ/ಸ್ವೀಕರಿಸಿ"</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index 55bae4d..a591b43 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"발신자 번호가 기본적으로 제한되지 않음으로 설정됩니다. 다음 통화: 제한되지 않음"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"서비스가 준비되지 않았습니다."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"발신자 번호 설정을 변경할 수 없습니다."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g> 이동통신사로 데이터가 변경됨"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"언제든지 설정에서 변경할 수 있습니다."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"모바일 데이터 서비스가 차단됨"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"긴급 전화를 사용할 수 없음"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"음성 서비스를 이용할 수 없음"</string>
@@ -247,7 +249,7 @@
     <string name="bugreport_message" msgid="5212529146119624326">"현재 기기 상태에 대한 정보를 수집하여 이메일 메시지로 전송합니다. 버그 신고를 시작하여 전송할 준비가 되려면 약간 시간이 걸립니다."</string>
     <string name="bugreport_option_interactive_title" msgid="7968287837902871289">"대화형 보고서"</string>
     <string name="bugreport_option_interactive_summary" msgid="8493795476325339542">"대부분의 경우 이 옵션을 사용합니다. 신고 진행 상황을 추적하고 문제에 대한 세부정보를 입력하고 스크린샷을 찍을 수 있습니다. 신고하기에 시간이 너무 오래 걸리고 사용 빈도가 낮은 일부 섹션을 생략할 수 있습니다."</string>
-    <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 보고서"</string>
+    <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 신고"</string>
     <string name="bugreport_option_full_summary" msgid="1975130009258435885">"기기가 응답하지 않거나 너무 느리거나 모든 보고서 섹션이 필요한 경우 이 옵션을 사용하여 시스템 방해를 최소화합니다. 세부정보를 추가하거나 스크린샷을 추가로 찍을 수 없습니다."</string>
     <string name="bugreport_countdown" msgid="6418620521782120755">"{count,plural, =1{버그 신고 스크린샷을 #초 후에 찍습니다.}other{버그 신고 스크린샷을 #초 후에 찍습니다.}}"</string>
     <string name="bugreport_screenshot_success_toast" msgid="7986095104151473745">"버그 신고용 스크린샷 촬영 완료"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"잠금"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"새 알림"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"가상 키보드"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"물리적 키보드"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"보안"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"운전 모드"</string>
@@ -595,7 +596,7 @@
     <string name="fingerprint_acquired_immobile" msgid="1621891895241888048">"지문을 등록할 때마다 손가락을 조금씩 이동하세요"</string>
   <string-array name="fingerprint_acquired_vendor">
   </string-array>
-    <string name="fingerprint_error_not_match" msgid="4599441812893438961">"지문이 인식되지 않습니다."</string>
+    <string name="fingerprint_error_not_match" msgid="4599441812893438961">"지문이 인식되지 않았습니다."</string>
     <string name="fingerprint_udfps_error_not_match" msgid="8236930793223158856">"지문을 인식할 수 없습니다."</string>
     <string name="fingerprint_authenticated" msgid="2024862866860283100">"지문이 인증됨"</string>
     <string name="face_authenticated_no_confirmation_required" msgid="8867889115112348167">"얼굴이 인증되었습니다"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"앱이 공유 저장소에서 동영상 파일을 읽도록 허용합니다."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"공유 저장소에서 이미지 파일 읽기"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"앱이 공유 저장소에서 이미지 파일을 읽도록 허용합니다."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"공유 저장공간의 콘텐츠 수정 또는 삭제"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"앱이 공유 저장공간의 콘텐츠에 쓰도록 허용합니다."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP 통화 발신/수신"</string>
@@ -1250,7 +1255,7 @@
     <string name="android_upgrading_complete" msgid="409800058018374746">"부팅 완료"</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"전원 버튼을 눌렀습니다. 이러면 보통 화면이 꺼집니다.\n\n지문 설정 중에 가볍게 탭하세요."</string>
     <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"설정을 완료하려면 화면을 끄세요"</string>
-    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"사용 중지"</string>
+    <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"화면 끄기"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"지문 인증을 계속할까요?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"전원 버튼을 눌렀습니다. 이러면 보통 화면이 꺼집니다.\n\n지문을 인식하려면 화면을 가볍게 탭하세요."</string>
     <string name="fp_power_button_bp_positive_button" msgid="728945472408552251">"화면 끄기"</string>
diff --git a/core/res/res/values-ky/strings.xml b/core/res/res/values-ky/strings.xml
index ad01dafc..61d64c1 100644
--- a/core/res/res/values-ky/strings.xml
+++ b/core/res/res/values-ky/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Номурду аныктоонун демейки абалы \"чектелбейт\" деп коюлган. Кийинки чалуу: Чектелбейт"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Кызмат камсыздалган эмес."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Чалуучунун далдаштырма дайындары жөндөөлөрүн өзгөртө албайсыз."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Мобилдик Интернет <xliff:g id="CARRIERDISPLAY">%s</xliff:g> которулду"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Бул функциянын параметрлерин \"Тууралоо\" бөлүмүнөн өзгөртө аласыз"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Мобилдик Интернет кызматы жок"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Шашылыш чалуу бөгөттөлгөн"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Аудио чалуу кызматы бөгөттөлгөн"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Бекем кулпулоо"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Жаңы эскертме"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуалдык баскычтоп"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Аппараттык баскычтоп"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Коопсуздук"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Унаа режими"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Колдонмого жалпы сактагычыңыздагы видеолорду окуу мүмкүнчүлүгүн берет."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"жалпы сактагычтагы сүрөт файлдарды окуу"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Колдонмого жалпы сактагычыңыздагы сүрөт файлдарды окуу мүмкүнчүлүгүн берет."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"жалпы сактагычыңыздын мазмунун өзгөртүү же жок кылуу"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Колдонмого жалпы сактагычыңыздын мазмунун жазуу мүмкүнчүлүгүн берет."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP чалуу/чалууну кабыл алуу"</string>
diff --git a/core/res/res/values-lo/strings.xml b/core/res/res/values-lo/strings.xml
index 02df227..1263a83 100644
--- a/core/res/res/values-lo/strings.xml
+++ b/core/res/res/values-lo/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ໝາຍເລກຜູ່ໂທ ໄດ້ຮັບການຕັ້ງຄ່າເລີ່ມຕົ້ນເປັນ ບໍ່ຖືກຈຳກັດ. ການໂທຄັ້ງຕໍ່ໄປ: ບໍ່ຖືກຈຳກັດ."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ບໍ່ໄດ້ເປີດໃຊ້ບໍລິການ."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"ທ່ານບໍ່ສາມາດປ່ຽນແປງການຕັ້ງຄ່າ Caller ID"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ປ່ຽນໄປໃຊ້ອິນເຕີເນັດມືຖືຂອງ <xliff:g id="CARRIERDISPLAY">%s</xliff:g> ແລ້ວ"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ທ່ານສາມາດປ່ຽນສິ່ງນີ້ຕອນໃດກໍໄດ້ໃນການຕັ້ງຄ່າ"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ບໍ່ມີບໍລິການອິນເຕີເນັດມືຖື"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ບໍ່ສາມາດໃຊ້ການໂທສຸກເສີນໄດ້"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ບໍ່ມີບໍລິການໂທສຽງ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ລັອກໄວ້"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ການແຈ້ງເຕືອນໃໝ່"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ແປ້ນພິມສະເໝືອນ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ແປ້ນພິມພາຍນອກ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ຄວາມປອດໄພ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ໂໝດຂັບລົດ"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ອະນຸຍາດໃຫ້ແອັບອ່ານໄຟລ໌ວິດີໂອຈາກບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນຂອງທ່ານ."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ອ່ານໄຟລ໌ຮູບຈາກບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນ"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ອະນຸຍາດໃຫ້ແອັບອ່ານໄຟລ໌ຮູບຈາກບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນຂອງທ່ານ."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"ອ່ານໄຟລ໌ຮູບ ແລະ ວິດີໂອທີ່ຜູ້ໃຊ້ເລືອກຈາກບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນ"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"ອະນຸຍາດໃຫ້ແອັບອ່ານໄຟລ໌ຮູບ ແລະ ວິດີໂອທີ່ທ່ານເລືອກຈາກບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນຂອງທ່ານ."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ແກ້ໄຂ ຫຼືລຶບເນື້ອຫາໃນບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນຂອງທ່ານ"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ອະນຸຍາດໃຫ້ແອັບຂຽນເນື້ອຫາຕ່າງໆຂອງບ່ອນຈັດເກັບຂໍ້ມູນທີ່ແບ່ງປັນຂອງທ່ານ."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"ຮັບສາຍ/ໂທອອກ ຜ່ານ SIP"</string>
diff --git a/core/res/res/values-lt/strings.xml b/core/res/res/values-lt/strings.xml
index 4543ef6..8a07e05 100644
--- a/core/res/res/values-lt/strings.xml
+++ b/core/res/res/values-lt/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Skambintojo ID pagal numatytuosius nustatymus yra neapribotas. Kitas skambutis: neapribotas"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Paslauga neteikiama."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Negalima pakeisti skambinančiojo ID nustatymo."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Duomenys perjungti į „<xliff:g id="CARRIERDISPLAY">%s</xliff:g>“"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Galite tai bet kada pakeisti nustatymuose"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Duomenų paslaugos mobiliesiems nėra"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Skambučių pagalbos numeriu paslaugos nėra"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Balso skambučių paslauga neteikiama"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Užrakinimas"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Naujas pranešimas"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtualioji klaviatūra"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizinė klaviatūra"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sauga"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automobilio režimas"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Leidžiama programai nuskaityti vaizdo įrašo failus iš bendrinamos saugyklos."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"nuskaityti vaizdo failus iš bendrinamos saugyklos"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Leidžiama programai nuskaityti vaizdo failus iš bendrinamos saugyklos."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"keisti / trinti bendr. atm. t."</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Pr. leidž. raš. bendr. atm. t."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"skambinti / priimti SIP skambučius"</string>
diff --git a/core/res/res/values-lv/strings.xml b/core/res/res/values-lv/strings.xml
index 64c855b..2a2f9e7 100644
--- a/core/res/res/values-lv/strings.xml
+++ b/core/res/res/values-lv/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Zvanītāja ID noklusējumi ir iestatīti uz Nav ierobežots. Nākamais zvans: nav ierobežots"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Pakalpojums netiek nodrošināts."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Zvanītāja ID iestatījumu nevar mainīt."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Tiek izmantots operatora <xliff:g id="CARRIERDISPLAY">%s</xliff:g> datu savienojums"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Šo opciju jebkurā brīdī var mainīt iestatījumos"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nav pieejams neviens datu pakalpojums mobilajām ierīcēm"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Nav pieejami ārkārtas izsaukumi"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Balss izsaukumu pakalpojums nedarbojas"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloķēšana"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"Pārsniedz"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Jauns paziņojums"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuālā tastatūra"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziskā tastatūra"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Drošība"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automašīnas režīms"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Ļauj lietotnei lasīt video failus jūsu koplietotajā krātuvē."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lasīt attēlu failus koplietotajā krātuvē"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Ļauj lietotnei lasīt attēlu failus jūsu koplietotajā krātuvē."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"Jūsu kopīgotās krātuves satura pārveidošana vai dzēšana"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Ļauj lietotnei rakstīt jūsu kopīgotās krātuves saturu."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP zvanu veikšana/saņemšana"</string>
diff --git a/core/res/res/values-mk/strings.xml b/core/res/res/values-mk/strings.xml
index 0af4cdd..b9ce23b 100644
--- a/core/res/res/values-mk/strings.xml
+++ b/core/res/res/values-mk/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Стандардно, ID на повикувач не е скриен. Следен повик: не е скриен"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Услугата не е предвидена."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Не може да го промените поставувањето за ID на повикувач."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Мобилниот интернет се префрли на <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ова може да го промените во секое време во „Поставки“"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Нема услуга за мобилен интернет"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Итните повици се недостапни"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Нема услуга за говорни повици"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Заклучување"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново известување"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуелна тастатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физичка тастатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безбедност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим на работа во автомобил"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Дозволува апликацијата да ги чита видеодатотеките од споделениот капацитет."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"да чита датотеки со слики од споделениот капацитет"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Дозволува апликацијата да ги чита датотеките со слики од споделениот капацитет."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"да чита датотеки со слики и видеа избрани од корисникот од споделениот простор."</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Дозволува апликацијата да чита датотеки со слики и видеа што ќе ги изберете од вашиот споделен простор."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ги менува или брише содржините на заедничкото место за складирање"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Дозволува апликацијата да ги пишува содржините на заедничкото место за складирање."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"остварува/прима повици преку SIP"</string>
diff --git a/core/res/res/values-ml/strings.xml b/core/res/res/values-ml/strings.xml
index bc12c07..026d8bd 100644
--- a/core/res/res/values-ml/strings.xml
+++ b/core/res/res/values-ml/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"നിയന്ത്രിക്കേണ്ടതല്ലാത്ത സ്ഥിര കോളർ ഐഡികൾ. അടുത്ത കോൾ: നിയന്ത്രിച്ചിട്ടില്ല"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"സേവനം വ്യവസ്ഥ ചെയ്‌തിട്ടില്ല."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"വിളിച്ച നമ്പർ ക്രമീകരണം നിങ്ങൾക്ക് മാറ്റാനാവില്ല."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g> എന്നതിലേക്ക് ഡാറ്റ മാറ്റി"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"നിങ്ങൾക്ക് ക്രമീകരണത്തിൽ ഏതുസമയത്തും ഇത് മാറ്റാം"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"മൊബൈൽ ഡാറ്റാ സേവനമില്ല"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"എമർജൻസി കോളിംഗ് ലഭ്യമല്ല"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"വോയ്സ് സേവനമില്ല"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ലോക്ക്‌ഡൗൺ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"പുതിയ അറിയിപ്പ്"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"വെർച്വൽ കീബോഡ്"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ഫിസിക്കൽ കീബോഡ്"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"സുരക്ഷ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"കാർ മോഡ്"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"നിങ്ങളുടെ പങ്കിട്ട സ്‌റ്റോറേജിൽ നിന്നുള്ള വീഡിയോ ഫയലുകൾ വായിക്കാൻ ആപ്പിനെ അനുവദിക്കുന്നു."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"പങ്കിട്ട സ്റ്റോറേജിൽ നിന്നുള്ള ചിത്ര ഫയലുകൾ വായിക്കുക"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"നിങ്ങളുടെ പങ്കിട്ട സ്‌റ്റോറേജിൽ നിന്നുള്ള ചിത്ര ഫയലുകൾ വായിക്കാൻ ആപ്പിനെ അനുവദിക്കുന്നു."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"നിങ്ങൾ പങ്കിടുന്ന സ്‌റ്റോറേജിലെ ഉള്ളടക്കങ്ങൾ പരിഷ്‌ക്കരിക്കുക അല്ലെങ്കിൽ ഇല്ലാതാക്കുക"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"നിങ്ങൾ പങ്കിടുന്ന സ്‌റ്റോറേജിലെ ഉള്ളടക്കങ്ങൾ എഴുതാൻ ആപ്പിനെ അനുവദിക്കുന്നു."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP കോളുകൾ വിളിക്കുക/സ്വീകരിക്കുക"</string>
diff --git a/core/res/res/values-mn/strings.xml b/core/res/res/values-mn/strings.xml
index 4e8c314..45d2f31 100644
--- a/core/res/res/values-mn/strings.xml
+++ b/core/res/res/values-mn/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Дуудлага хийгчийн ID хязгаарлагдсан. Дараагийн дуудлага: Хязгаарлагдсан"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Үйлчилгээ провишн хийгдээгүй ."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Та дуудлага хийгчийн ID тохиргоог солиж чадахгүй."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Өгөгдлийг <xliff:g id="CARRIERDISPLAY">%s</xliff:g> руу шилжүүлсэн"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Та үүнийг Тохиргооноос хэдийд ч өөрчлөх боломжтой."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Мобайл дата үйлчилгээ алга"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Яаралтай дуудлага боломжтой"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Дуу хоолойны үйлчилгээ алга"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Түгжих"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Шинэ мэдэгдэл"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуал гар"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Биет гар"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Аюулгүй байдал"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Машины горим"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Аппад таны дундын хадгалах сангаас видео файлыг унших боломжийг олгодог."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"дундын хадгалах сангаас зургийн файл унших"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Аппад таны дундын хадгалах сангаас зургийн файлыг унших зөвшөөрөл олгодог."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"дундын хадгалах сангийнхаа контентыг өөрчлөх эсвэл устгах"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Аппад таны дундын хадгалах сангийн контентыг бичихийг зөвшөөрдөг."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP дуудлага хийх/хүлээн авах"</string>
diff --git a/core/res/res/values-mr/strings.xml b/core/res/res/values-mr/strings.xml
index aa8c1e9..360221c 100644
--- a/core/res/res/values-mr/strings.xml
+++ b/core/res/res/values-mr/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"कॉलर आयडी डीफॉल्‍ट रूपात प्रतिबंधित नाही वर सेट असतो. पुढील कॉल: प्रतिबंधित नाही"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"सेवेची तरतूद केलेली नाही."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"तुम्ही कॉलर आयडी सेटिंग बदलू शकत नाही."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"डेटा <xliff:g id="CARRIERDISPLAY">%s</xliff:g> वर स्विच केला"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"तुम्ही हे सेटिंग्जमध्ये कधीही बदलू शकता"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"मोबाइल डेटा सेवा नाही"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"आणीबाणी कॉलिंग अनुपलब्‍ध आहे"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"व्हॉइस सेवा नाही"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"लॉकडाउन"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नवीन सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"व्हर्च्युअल कीबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"वास्तविक कीबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ॲपला तुमच्या शेअर केलेल्या स्टोरेजमधून व्हिडिओ फाइल वाचण्याची अनुमती देते."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"शेअर केलेल्या स्टोरेजमधून इमेज फाइल वाचा"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ॲपला तुमच्या शेअर केलेल्या स्टोरेजमधून इमेज फाइल वाचण्याची अनुमती देते."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"तुमच्या शेअर केलेल्या स्टोरेजच्या आशयांमध्ये सुधारणा करा किंवा हटवा"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ॲपला तुमच्या शेअर केलेल्या स्टोरेजचे आशय लिहिण्याची अनमती देते."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP कॉल करा/मिळवा"</string>
diff --git a/core/res/res/values-ms/strings.xml b/core/res/res/values-ms/strings.xml
index 9a6ee3b..9162690 100644
--- a/core/res/res/values-ms/strings.xml
+++ b/core/res/res/values-ms/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID pemanggil secara lalainya ditetapkan kepada tidak dihadkan. Panggilan seterusnya: Tidak terhad"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Perkhidmatan yang tidak diuntukkan."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Anda tidak boleh mengubah tetapan ID pemanggil."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Data ditukar kepada <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Anda boleh menukar pilihan ini pada bila-bila masa dalam Tetapan"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Tiada perkhidmatan data mudah alih"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Panggilan kecemasan tidak tersedia"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Tiada perkhidmatan suara"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kunci semua"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Pemberitahuan baharu"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Papan kekunci maya"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Papan kekunci fizikal"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Keselamatan"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mod kereta"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Membenarkan apl membaca fail video daripada storan kongsi anda."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"baca fail imej daripada storan kongsi"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Membenarkan apl membaca fail imej daripada storan kongsi anda."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"baca fail imej dan video yang dipilih oleh pengguna daripada storan kongsi"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Membenarkan apl membaca fail imej dan video yang anda pilih daripada storan kongsi anda."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"mengubah suai atau memadamkan kandungan storan kongsi anda"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Membenarkan apl menulis kandungan storan kongsi anda."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"buat/terima panggilan SIP"</string>
diff --git a/core/res/res/values-my/strings.xml b/core/res/res/values-my/strings.xml
index 716f5d7..e4f937b 100644
--- a/core/res/res/values-my/strings.xml
+++ b/core/res/res/values-my/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ပုံသေအားဖြင့် ခေါ်ဆိုသူအိုင်ဒီ(Caller ID)အား ကန့်သတ်မထားပါ။ နောက်ထပ်အဝင်ခေါ်ဆိုမှု-ကန့်သတ်မထားပါ။"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ဝန်ဆောင်မှုအား ကန့်သတ်မထားပါ"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"သင်သည် ခေါ်ဆိုသူ ID ဆက်တင်ကို မပြောင်းလဲနိုင်ပါ။"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ဒေတာကို <xliff:g id="CARRIERDISPLAY">%s</xliff:g> သို့ ပြောင်းထားသည်"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"၎င်းကို ဆက်တင်များတွင် အချိန်မရွေး ပြောင်းနိုင်သည်"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"မိုဘိုင်း ဒေတာဝန်ဆောင်မှု မရှိပါ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"အရေးပေါ်ခေါ်ဆိုမှု မရနိုင်ပါ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ဖုန်းဝန်ဆောင်မှု မရှိပါ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"လော့ခ်ဒေါင်း"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"၉၉၉+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"အကြောင်းကြားချက်အသစ်"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ပကတိအသွင်ကီးဘုတ်"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"စက်၏ ကီးဘုတ်"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"လုံခြုံရေး"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ကားမုဒ်"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"သင်၏မျှဝေထားသော သိုလှောင်ခန်းမှ ဗီဒီယိုဖိုင်များဖတ်ရန် အက်ပ်ကိုခွင့်ပြုသည်။"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"မျှဝေထားသည့် သိုလှောင်ခန်းမှ ပုံပါဝင်သောဖိုင်များဖတ်ရန်"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"သင်၏မျှဝေထားသည့် သိုလှောင်ခန်းမှ ပုံပါဝင်သောဖိုင်များဖတ်ရန် အက်ပ်ကို ခွင့်ပြုသည်။"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"မျှဝေသိုလှောင်ခန်းမှ အရာများ ပြုပြင်/ဖျက်ခြင်း"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"မျှဝေသိုလှောင်ခန်းမှ အရာများ ရေးခွင့်ပြုသည်။"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP ခေါ်ဆိုမှုများ ခေါ်ရန်/လက်ခံရန်"</string>
diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml
index 3d16ea7..ba55359 100644
--- a/core/res/res/values-nb/strings.xml
+++ b/core/res/res/values-nb/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Nummervisning er ikke begrenset som standard. Neste anrop: Ikke begrenset"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"SIM-kortet er ikke tilrettelagt for tjenesten."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Du kan ikke endre innstillingen for anrops-ID."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Byttet data til <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Du kan endre dette når som helst i innstillingene"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ingen mobildatatjeneste"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Nødanrop er utilgjengelig"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ingen taletjeneste"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Låsing"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nytt varsel"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelt tastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysisk tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sikkerhet"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Bilmodus"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Lar appen lese videofiler fra den delte lagringsplassen din."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"lese bildefiler fra delt lagringsplass"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Lar appen lese bildefiler fra den delte lagringsplassen din."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"endre eller slette innholdet i den delte lagringen din"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Lar appen skrive innholdet i den delte lagringen din."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"foreta/motta SIP-anrop"</string>
diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml
index bdb31bc..ae833bf 100644
--- a/core/res/res/values-ne/strings.xml
+++ b/core/res/res/values-ne/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"कलर ID पूर्वनिर्धारितको लागि रोकावट छैन। अर्को कल: रोकावट छैन"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"सेवाको व्यवस्था छैन।"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"तपाईं कलर ID सेटिङ परिवर्तन गर्न सक्नुहुन्न।"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g> को डेटा प्रयोग गर्न थालिएको छ"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"तपाईं जुनसुकै बेला सेटिङमा गई यो कुरा बदल्न सक्नुहुन्छ"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"कुनै पनि मोबाइल डेटा सेवा उपलब्ध छैन"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"आपत्‌कालीन कल सेवा उपलब्ध छैन"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"कुनै पनि भ्वाइस सेवा उपलब्ध छैन"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"लकडाउन गर्नु…"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"९९९+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नयाँ सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"भर्चुअल किबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"फिजिकल किबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"एपलाई तपाईंको साझा भण्डारणमा भएका भिडियो फाइलहरू पढ्ने अनुमति दिन्छ।"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"साझा भण्डारणमा भएका फोटो फाइलहरू पढ्ने"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"एपलाई तपाईंको साझा भण्डारणमा भएका फोटो फाइलहरू पढ्ने अनुमति दिन्छ।"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"तपाईंको आदान प्रदान गरिएको भण्डारणको विषयवस्तुहरूलाई परिमार्जन गर्नहोस् वा मेटाउनुहोस्"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"एपलाई तपाईंको आदान प्रदान गरिएको भण्डारणको सामग्री लेख्न अनुमति दिन्छ।"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP कलहरू प्राप्त/बनाउन"</string>
diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml
index 162d3e8..37d9798 100644
--- a/core/res/res/values-nl/strings.xml
+++ b/core/res/res/values-nl/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Beller-ID standaard ingesteld op \'onbeperkt\'. Volgend gesprek: onbeperkt."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Service niet voorzien."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"U kunt de instelling voor de beller-ID niet wijzigen."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Mobiele data overgeschakeld naar <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Je kunt dit altijd wijzigen via Instellingen"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Geen service voor mobiele data"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Noodoproepen niet beschikbaar"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Geen belservice"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999 +"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nieuwe melding"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtueel toetsenbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysiek toetsenbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Beveiliging"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automodus"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Hiermee kan de app videobestanden in je gedeelde opslag lezen."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"afbeeldingsbestanden in gedeelde opslag lezen"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Hiermee kan de app afbeeldingsbestanden in je gedeelde opslag lezen."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"de content van je gedeelde opslag aanpassen of verwijderen"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Hiermee kan de app de content van je gedeelde opslag schrijven."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"Bellen of gebeld worden via SIP"</string>
diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml
index 440245e..5ee80ce 100644
--- a/core/res/res/values-or/strings.xml
+++ b/core/res/res/values-or/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"କଲର୍ ଆଇଡି ଡିଫଲ୍ଟ ଭାବରେ ପ୍ରତିବନ୍ଧିତ ନୁହେଁ। ପରବର୍ତ୍ତୀ କଲ୍: ପ୍ରତିବନ୍ଧିତ ନୁହେଁ"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ସେବାର ସୁବିଧା ନାହିଁ।"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"ଆପଣ କଲର୍‍ ID ସେଟିଙ୍ଗ ବଦଳାଇପାରିବେ ନାହିଁ।"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"<xliff:g id="CARRIERDISPLAY">%s</xliff:g>କୁ ଡାଟା ସ୍ୱିଚ କରାଯାଇଛି"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ଆପଣ ଯେ କୌଣସି ସମୟରେ ସେଟିଂସରେ ଏହାକୁ ପରିବର୍ତ୍ତନ କରିପାରିବେ"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"କୌଣସି ମୋବାଇଲ୍ ଡାଟା ସେବା ନାହିଁ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ଜରୁରୀକାଳୀନ କଲ୍ ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"କୌଣସି ଭଏସ୍‍ ସେବା ନାହିଁ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ଲକ୍ କରନ୍ତୁ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ନୂଆ ବିଜ୍ଞପ୍ତି"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ଭର୍ଚୁଆଲ୍‌ କୀ\'ବୋର୍ଡ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ଫିଜିକଲ୍ କୀ’ବୋର୍ଡ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ସୁରକ୍ଷା"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"କାର୍ ମୋଡ୍"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ଆପଣଙ୍କ ସେୟାର କରାଯାଇଥିବା ଷ୍ଟୋରେଜରୁ ଭିଡିଓ ଫାଇଲଗୁଡ଼ିକୁ ପଢ଼ିବା ପାଇଁ ଆପକୁ ଅନୁମତି ଦିଏ।"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ସେୟାର କରାଯାଇଥିବା ଷ୍ଟୋରେଜରୁ ଇମେଜ ଫାଇଲଗୁଡ଼ିକୁ ପଢ଼ନ୍ତୁ"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ଆପଣଙ୍କ ସେୟାର କରାଯାଇଥିବା ଷ୍ଟୋରେଜରୁ ଇମେଜ ଫାଇଲଗୁଡ଼ିକୁ ପଢ଼ିବା ପାଇଁ ଆପକୁ ଅନୁମତି ଦିଏ।"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ଆପଣଙ୍କତ ସେୟାର୍‍ ହୋଇଥିବା ଷ୍ଟୋରେଜ୍‍ର ବିଷୟବସ୍ତୁ ସଂଶୋଧନ କିମ୍ବା ଡିଲିଟ୍‍ କରନ୍ତୁ"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ଆପଣଙ୍କର ସେୟାର୍‍ ହୋଇଥିବା ଷ୍ଟୋରେଜ୍‍ର ବିଷୟବସ୍ତୁ ଲେଖିବାକୁ ଅନୁମତି କରିଥାଏ।"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP କଲ୍‌ କରନ୍ତୁ ଏବଂ ଗ୍ରହଣ କରନ୍ତୁ"</string>
diff --git a/core/res/res/values-pa/strings.xml b/core/res/res/values-pa/strings.xml
index a9e3531..62f2da5 100644
--- a/core/res/res/values-pa/strings.xml
+++ b/core/res/res/values-pa/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ਪ੍ਰਤਿਬੰਧਿਤ ਨਾ ਕਰਨ ਲਈ ਕਾਲਰ ਆਈ.ਡੀ. ਪੂਰਵ-ਨਿਰਧਾਰਤ। ਅਗਲੀ ਕਾਲ: ਪ੍ਰਤਿਬੰਧਿਤ ਨਹੀਂ"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ਸੇਵਾ ਪ੍ਰਬੰਧਿਤ ਨਹੀਂ ਹੈ।"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"ਤੁਸੀਂ ਕਾਲਰ ਆਈ.ਡੀ. ਸੈਟਿੰਗ ਨਹੀਂ ਬਦਲ ਸਕਦੇ।"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ਡਾਟੇ ਨੂੰ <xliff:g id="CARRIERDISPLAY">%s</xliff:g> \'ਤੇ ਸਵਿੱਚ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ਤੁਸੀਂ ਕਿਸੇ ਵੇਲੇ ਵੀ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ ਇਸਨੂੰ ਬਦਲ ਸਕਦੇ ਹੋ"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ਕੋਈ ਮੋਬਾਈਲ ਡਾਟਾ ਸੇਵਾ ਨਹੀਂ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ਸੰਕਟਕਾਲੀਨ ਕਾਲਿੰਗ ਉਪਲਬਧ ਨਹੀਂ"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ਕੋਈ ਆਵਾਜ਼ੀ ਸੇਵਾ ਨਹੀਂ"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ਲਾਕਡਾਊਨ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ਨਵੀਂ ਸੂਚਨਾ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ਆਭਾਸੀ ਕੀ-ਬੋਰਡ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ਭੌਤਿਕ ਕੀ-ਬੋਰਡ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ਸੁਰੱਖਿਆ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ਕਾਰ ਮੋਡ"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ਐਪ ਨੂੰ ਤੁਹਾਡੀ ਸਾਂਝੀ ਕੀਤੀ ਸਟੋਰੇਜ ਤੋਂ ਵੀਡੀਓ ਫ਼ਾਈਲਾਂ ਪੜ੍ਹਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ਸਾਂਝੀ ਕੀਤੀ ਸਟੋਰੇਜ ਤੋਂ ਚਿੱਤਰ ਫ਼ਾਈਲਾਂ ਪੜ੍ਹੋ"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ਐਪ ਨੂੰ ਤੁਹਾਡੀ ਸਾਂਝੀ ਕੀਤੀ ਸਟੋਰੇਜ ਤੋਂ ਚਿੱਤਰ ਫ਼ਾਈਲਾਂ ਪੜ੍ਹਨ ਦੀ ਆਗਿਆ ਦਿੰਦਾ ਹੈ।"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ਸਮੱਗਰੀਆਂ ਦਾ ਸੰਸ਼ੋਧਨ ਕਰੋ ਜਾਂ ਮਿਟਾਓ"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ਐਪ ਨੂੰ ਸਮੱਗਰੀਆਂ ਲਿਖਣ ਦਿੰਦੀ ਹੈ।"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP ਕਾਲਾਂ ਕਰੋ/ਪ੍ਰਾਪਤ ਕਰੋ"</string>
diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml
index 7b47a5c..76bd69b 100644
--- a/core/res/res/values-pl/strings.xml
+++ b/core/res/res/values-pl/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID rozmówcy ustawiony jest domyślnie na „nie zastrzeżony”. Następne połączenie: nie zastrzeżony"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Usługa nie jest świadczona."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nie możesz zmienić ustawienia ID rozmówcy."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Przełączono mobilną transmisję danych na: <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Możesz to zmienić w dowolnym momencie w Ustawieniach"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Brak komórkowej usługi transmisji danych"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Połączenia alarmowe są niedostępne"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Brak usługi połączeń głosowych"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blokada"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nowe powiadomienie"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Klawiatura wirtualna"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Klawiatura fizyczna"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bezpieczeństwo"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Tryb samochodowy"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Zezwala na odczyt przez aplikację plików wideo w pamięci współdzielonej."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"odczyt plików graficznych z pamięci współdzielonej"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Zezwala na odczyt przez aplikację plików graficznych w pamięci współdzielonej."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modyfikowanie i usuwanie zawartości pamięci współdzielonej"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Zezwala aplikacji na zapis zawartości pamięci współdzielonej."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"wykonywanie/odbieranie połączeń SIP"</string>
diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml
index 8635d76..82f23f2 100644
--- a/core/res/res/values-pt-rBR/strings.xml
+++ b/core/res/res/values-pt-rBR/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"O identificador de chamadas assume o padrão de não restrito. Próxima chamada: Não restrita"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"O serviço não foi habilitado."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Não é possível alterar a configuração do identificador de chamadas."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Dados da <xliff:g id="CARRIERDISPLAY">%s</xliff:g> ativados"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"É possível mudar essa opção a qualquer momento nas Configurações"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nenhum serviço móvel de dados"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Chamadas de emergência indisponíveis"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sem serviço de voz"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueio total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo carro"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que o app leia arquivos de vídeo do armazenamento compartilhado."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ler arquivos de imagem do armazenamento compartilhado"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que o app leia arquivos de imagem do armazenamento compartilhado."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"ler arquivos de imagem e vídeo selecionados pelo usuário no armazenamento compartilhado"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Permite que o app leia arquivos de imagem e vídeo que você selecionar no armazenamento compartilhado."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"alterar ou excluir conteúdo do armaz. compartilhado"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite que o app grave o conteúdo do armaz. compartilhado."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"fazer/receber chamadas SIP"</string>
diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml
index 5dc59e1..7efd2a1 100644
--- a/core/res/res/values-pt-rPT/strings.xml
+++ b/core/res/res/values-pt-rPT/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID do autor da chamada é predefinido com não restrito. Chamada seguinte: Não restrita"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Serviço não fornecido."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Não pode alterar a definição da identificação de chamadas."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Os dados móveis foram alterados para o operador <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Pode alterar isto em qualquer altura nas Definições"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Sem serviço de dados móveis"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Chamadas de emergência indisponíveis"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sem serviço de voz"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloquear"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo automóvel"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que a app leia ficheiros de vídeo do armazenamento partilhado."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ler ficheiros de imagem do armazenamento partilhado"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que a app leia ficheiros de imagem do armazenamento partilhado."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"ler ficheiros de imagem e vídeo do armazenamento partilhado selecionados pelo utilizador"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Permite que a app leia ficheiros de imagem e vídeo que selecionar no seu armazenamento partilhado."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modif./elim. os conteúdos do armazenam. partilhado"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite que a apl. escreva conteúd. do armazen. partilhado."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"efetuar/receber chamadas SIP"</string>
diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml
index 8635d76..82f23f2 100644
--- a/core/res/res/values-pt/strings.xml
+++ b/core/res/res/values-pt/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"O identificador de chamadas assume o padrão de não restrito. Próxima chamada: Não restrita"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"O serviço não foi habilitado."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Não é possível alterar a configuração do identificador de chamadas."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Dados da <xliff:g id="CARRIERDISPLAY">%s</xliff:g> ativados"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"É possível mudar essa opção a qualquer momento nas Configurações"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nenhum serviço móvel de dados"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Chamadas de emergência indisponíveis"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sem serviço de voz"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueio total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo carro"</string>
@@ -698,6 +699,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite que o app leia arquivos de vídeo do armazenamento compartilhado."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"ler arquivos de imagem do armazenamento compartilhado"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite que o app leia arquivos de imagem do armazenamento compartilhado."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"ler arquivos de imagem e vídeo selecionados pelo usuário no armazenamento compartilhado"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Permite que o app leia arquivos de imagem e vídeo que você selecionar no armazenamento compartilhado."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"alterar ou excluir conteúdo do armaz. compartilhado"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite que o app grave o conteúdo do armaz. compartilhado."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"fazer/receber chamadas SIP"</string>
diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml
index acd1df6..8d8059c 100644
--- a/core/res/res/values-ro/strings.xml
+++ b/core/res/res/values-ro/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID-ul apelantului este nerestricționat în mod prestabilit. Apelul următor: nerestricționat"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Nu se asigură accesul la acest serviciu."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nu poți modifica setarea pentru ID-ul apelantului."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"S-a trecut la datele mobile <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Poți modifica oricând opțiunea din Setări"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Fără serviciu de date mobile"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Apelurile de urgență nu sunt disponibile"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Fără servicii vocale"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blocare strictă"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"˃999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificare nouă"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastatură virtuală"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastatură fizică"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Securitate"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mod Mașină"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Permite aplicației să citească fișiere video din spațiul de stocare comun."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"să citească fișiere imagine din spațiul de stocare comun"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Permite aplicației să citească fișiere imagine din spațiul de stocare comun."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"să modifice sau să șteargă conținutul spațiului de stocare comun"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Permite aplicației scrierea conținutul spațiului de stocare comun."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"efectuarea/primirea apelurilor SIP"</string>
diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml
index fb4775b..5cf8eff 100644
--- a/core/res/res/values-ru/strings.xml
+++ b/core/res/res/values-ru/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Идентификация абонента по умолчанию не запрещена. След. вызов: разрешена"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Услуга не предоставляется."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Невозможно изменить параметр идентификатора вызывающего абонента."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Используется мобильный интернет <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Этот параметр можно в любой момент изменить в настройках."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Мобильный Интернет недоступен"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Экстренные вызовы недоступны"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Голосовые вызовы недоступны"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блокировка входа"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Новое уведомление"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуальная клавиатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физическая клавиатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безопасность"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим \"В авто\""</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Приложение сможет считывать видеофайлы из общего хранилища."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"считывание изображений из общего хранилища"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Приложение сможет считывать изображения из общего хранилища."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"Изменение или удаление данных на общем накопителе"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Приложение сможет записывать данные на общий накопитель."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"Входящие и исходящие вызовы SIP"</string>
diff --git a/core/res/res/values-si/strings.xml b/core/res/res/values-si/strings.xml
index 098e622..8d329fc 100644
--- a/core/res/res/values-si/strings.xml
+++ b/core/res/res/values-si/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"අමතන්නාගේ ID සුපුරුදු අනුව සීමා වී නැත. මීළඟ ඇමතුම: සීමා කර ඇත"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"සේවාවන් සපයා නැත."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"අමතන්නාගේ ID සැකසීම ඔබට වෙනස්කල නොහැක."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"දත්ත <xliff:g id="CARRIERDISPLAY">%s</xliff:g> වෙත මාරු කරන ලදි"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"ඔබට සැකසීම් තුළ මෙය ඕනෑම වේලාවක වෙනස් කළ හැක"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ජංගම දත්ත සේවාව නැත"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"හදිසි ඇමතුම් ලබා ගත නොහැකිය"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"හඬ සේවාව නැත"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"අගුලු දැමීම"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"නව දැනුම්දීම"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"අතථ්‍ය යතුරු පුවරුව"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"භෞතික යතුරු පුවරුව"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ආරක්ෂාව"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"මෝටර් රථ ආකාරය"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ඔබගේ බෙදා ගත් ගබඩාවෙන් වීඩියෝ ගොනු කියවීමට යෙදුමට ඉඩ දෙයි."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"බෙදා ගත් ගබඩාවෙන් රූප ගොනු කියවන්න"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ඔබගේ බෙදා ගත් ගබඩාවෙන් රූප ගොනු කියවීමට යෙදුමට ඉඩ දෙයි."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ඔබේ බෙදා ගත් ගබඩාවේ අන්තර්ගත වෙනස් කරන්න නැතහොත් මකන්න"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"යෙදුමට ඔබේ බෙදා ගත් ගබඩාවේ අන්තර්ගත කියවීමට ඉඩ දෙයි."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP ඇමතුම් සිදුකිරීමට/ලබාගැනීමට"</string>
diff --git a/core/res/res/values-sk/strings.xml b/core/res/res/values-sk/strings.xml
index 47e3d3b..2d058e8 100644
--- a/core/res/res/values-sk/strings.xml
+++ b/core/res/res/values-sk/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"V predvolenom nastavení nie je identifikácia volajúceho obmedzená. Ďalší hovor: Bez obmedzenia"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Služba nie je poskytovaná."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nemôžete meniť nastavenie identifikácie volajúcich."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Dátové pripojenie bolo prepnuté na operátora <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Toto môžete kedykoľvek zmeniť v Nastaveniach"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Žiadna mobilná dátová služba"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Tiesňové volania nie sú k dispozícii"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Žiadne hlasové hovory"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Uzamknúť"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nové upozornenie"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuálna klávesnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyzická klávesnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Zabezpečenie"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim v aute"</string>
@@ -699,6 +700,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Umožňuje aplikácii čítať videosúbory z vášho zdieľaného priestoru."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"čítať súbory obrázka zo zdieľaného priestoru"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Umožňuje aplikácii čítať súbory obrázka z vášho zdieľaného priestoru."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"čítanie obrázkových súborov a videosúborov vybraných používateľom v zdieľanom ukladacom priestore"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Umožňuje aplikácii čítať obrázkové súbory a videosúbory, ktoré vyberiete vo svojom zdieľanom ukladacom priestore."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"upravovanie alebo odstraňovanie obsahu zdieľaného úložiska"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Umožňuje aplikácii zapisovať obsah zdieľaného úložiska."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"uskutočňovanie/príjem hovorov SIP"</string>
diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml
index 6e722e3..883beb1 100644
--- a/core/res/res/values-sl/strings.xml
+++ b/core/res/res/values-sl/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID klicatelja je ponastavljen na neomejeno. Naslednji klic: ni omejeno"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Storitev ni nastavljena in omogočena."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ne morete spremeniti nastavitve ID-ja klicatelja."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Prenos podatkov je preklopljen na operaterja <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"To lahko kadar koli spremenite v nastavitvah."</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ni mobilne podatkovne storitve"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Klicanje v sili ni na voljo"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ni storitve za glasovne klice"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zakleni"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999 +"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obvestilo"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Navidezna tipkovnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizična tipkovnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Varnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način za avtomobil"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Aplikaciji omogoča branje videodatotek v deljeni shrambi."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"branje slikovnih datotek v deljeni shrambi"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Aplikaciji omogoča branje slikovnih datotek v deljeni shrambi."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"spreminjanje ali brisanje vsebine skupne shrambe"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Aplikaciji omogoča zapisovanje vsebine skupne shrambe."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"opravljanje/sprejemanje klicev SIP"</string>
@@ -1251,7 +1256,7 @@
     <string name="android_upgrading_starting_apps" msgid="6206161195076057075">"Zagon aplikacij."</string>
     <string name="android_upgrading_complete" msgid="409800058018374746">"Dokončevanje zagona."</string>
     <string name="fp_power_button_enrollment_message" msgid="5648173517663246140">"Pritisnili ste gumb za vklop, s čimer običajno izklopite zaslon.\n\nPoskusite se narahlo dotakniti med nastavljanjem prstnega odtisa."</string>
-    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Za končanje nastavitve izklopite zaslon"</string>
+    <string name="fp_power_button_enrollment_title" msgid="6976841690455338563">"Za končanje nastavitve izklopite zaslon."</string>
     <string name="fp_power_button_enrollment_button_text" msgid="3199783266386029200">"Izklopi"</string>
     <string name="fp_power_button_bp_title" msgid="5585506104526820067">"Želite nadaljevati preverjanje prstnega odtisa?"</string>
     <string name="fp_power_button_bp_message" msgid="2983163038168903393">"Pritisnili ste gumb za vklop, s čimer običajno izklopite zaslon.\n\nZa preverjanje prstnega odtisa se poskusite narahlo dotakniti."</string>
diff --git a/core/res/res/values-sq/strings.xml b/core/res/res/values-sq/strings.xml
index 0a70e9a..9380bf4 100644
--- a/core/res/res/values-sq/strings.xml
+++ b/core/res/res/values-sq/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ID-ja e telefonuesit kalon me paracaktim në listën e të telefonuesve të pakufizuar. Telefonata e radhës: e pakufizuar!"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Shërbimi nuk është përgatitur."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Nuk mund ta ndryshosh cilësimin e ID-së së telefonuesit."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Të dhënat u kaluan te <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Mund ta ndryshosh këtë në çdo kohë te \"Cilësimet\""</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Nuk ka shërbim të të dhënave celulare"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Telefonatat e urgjencës nuk ofrohen"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Nuk ka shërbim zanor"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blloko"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Njoftim i ri"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastiera virtuale"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastiera fizike"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Siguria"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modaliteti \"në makinë\""</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Lejon që aplikacioni të lexojë skedarët e videove nga hapësira ruajtëse e ndarë."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"të lexojë skedarët e imazheve nga hapësira ruajtëse e ndarë"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Lejon që aplikacioni të lexojë skedarët e imazheve nga hapësira ruajtëse e ndarë."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"modifiko ose fshi përmbajtjet e hapësirës ruajtëse të ndarë"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Lejon që aplikacioni të shkruajë përmbajtjet e hapësirës ruajtëse të ndarë."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"bëj/merr telefonata SIP"</string>
diff --git a/core/res/res/values-sr/strings.xml b/core/res/res/values-sr/strings.xml
index 0e50810..ff9ab54 100644
--- a/core/res/res/values-sr/strings.xml
+++ b/core/res/res/values-sr/strings.xml
@@ -73,6 +73,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"ИД позиваоца подразумевано није ограничен. Следећи позив: Није ограничен."</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Услуга није добављена."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Не можете да промените подешавање ИД-а корисника."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Мобилни подаци су пребачени на оператера <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ово можете у сваком тренутку да промените у Подешавањима"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Нема услуге мобилних података"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Хитни позиви нису доступни"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Нема гласовне услуге"</string>
@@ -265,7 +267,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Закључавање"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново обавештење"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуелна тастатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физичка тастатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безбедност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим рада у аутомобилу"</string>
@@ -698,6 +699,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Омогућава апликацији да чита видео фајлове из дељеног меморијског простора."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"читање фајлова слика из дељеног меморијског простора"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Омогућава апликацији да чита фајлове слика из дељеног меморијског простора."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"мењање или брисање садржаја дељеног меморијског простора"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Дозвољава апликацији да уписује садржај дељеног меморијског простора."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"упућивање/пријем SIP позива"</string>
diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml
index f8b3fdf..ee528db 100644
--- a/core/res/res/values-sv/strings.xml
+++ b/core/res/res/values-sv/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Nummerpresentatörens standardinställning är inte blockerad. Nästa samtal: Inte blockerad"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Tjänsten är inte etablerad."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Det går inte att ändra inställningen för nummerpresentatör."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Du har bytt mobildata till <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Du kan ändra det här när som helst i inställningarna"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ingen mobildatatjänst"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Det går inte att ringa nödsamtal"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Tjänsten för röstsamtal har blockerats"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Låsning"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ny avisering"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuellt tangentbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysiskt tangentbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Säkerhet"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Billäge"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Tillåter att appen läser videofiler från delad lagring."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"läsa bildfiler från delad lagring"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Tillåter att appen läser bildfiler från delad lagring."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"ändra eller ta bort innehåll på delat lagringsutrymme"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Tillåter att appen skriver innehåll på ditt delade lagringsutrymme."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"gör/ta emot SIP-anrop"</string>
diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml
index 5829fae..1fb9f1e 100644
--- a/core/res/res/values-sw/strings.xml
+++ b/core/res/res/values-sw/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Chaguomsingi za ID ya mpigaji simu za kutozuia. Simu ifuatayo: Haijazuiliwa"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Huduma haitathminiwi."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Hauwezi kubadilisha mpangilio wa kitambulisho cha anayepiga."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Sasa unatumia data ya mtandao wa <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Unaweza kubadilisha hali hii wakati wowote kwenye Mipangilio"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Hakuna huduma ya data kwa vifaa vya mkononi"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Huduma ya kupiga simu za dharura haipatikani"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Hakuna huduma za simu za sauti"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Funga"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Arifa mpya"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Kibodi pepe"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Kibodi halisi"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Usalama"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Hali ya gari"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Huruhusu programu kusoma faili za video kutoka kwenye hifadhi unayoshiriki."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"soma faili za picha kutoka kwenye hifadhi iliyoshirikiwa"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Huruhusu programu kusoma faili za picha kutoka kwenye hifadhi yako iliyoshirikiwa."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"irekebishe au ifute maudhui ya hifadhi unayoshiriki"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Huruhusu programu iandike maudhui ya hifadhi unayoshiriki."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"piga/pokea simu za SIP"</string>
diff --git a/core/res/res/values-ta/strings.xml b/core/res/res/values-ta/strings.xml
index 1533c36..5021c23 100644
--- a/core/res/res/values-ta/strings.xml
+++ b/core/res/res/values-ta/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"அழைப்பாளர் ஐடி ஆனது வரையறுக்கப்படவில்லை என்பதற்கு இயல்பாக அமைக்கப்பட்டது. அடுத்த அழைப்பு: வரையறுக்கப்படவில்லை"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"சேவை ஒதுக்கப்படவில்லை."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"அழைப்பாளர் ஐடி அமைப்பை மாற்ற முடியாது."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"டேட்டா <xliff:g id="CARRIERDISPLAY">%s</xliff:g>க்கு மாற்றப்பட்டது"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"அமைப்புகளில் இதை எப்போது வேண்டுமானாலும் மாற்றிக்கொள்ளலாம்"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"மொபைல் டேட்டா சேவையைப் பயன்படுத்த முடியாது"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"அவசர அழைப்பைச் செய்ய முடியாது"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"குரல் சேவை இல்லை"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"பூட்டு"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"புதிய அறிவிப்பு"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"விர்ச்சுவல் கீபோர்டு"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"கைமுறை கீபோர்டு"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"பாதுகாப்பு"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"கார் பயன்முறை"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"உங்கள் பகிர்ந்த சேமிப்பகத்திலுள்ள வீடியோ ஃபைல்களைப் படிக்க ஆப்ஸை அனுமதிக்கும்."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"பகிர்ந்த சேமிப்பகத்திலுள்ள பட ஃபைல்களைப் படித்தல்"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"உங்கள் பகிர்ந்த சேமிப்பகத்திலுள்ள பட ஃபைல்களைப் படிக்க ஆப்ஸை அனுமதிக்கும்."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"பகிர்ந்த சேமிப்பகத்தின் உள்ளடக்கங்களை மாற்றும் அல்லது நீக்கும்"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"பகிர்ந்த சேமிப்பகத்தின் உள்ளடக்கத்தில் மாற்றங்களைச் செய்ய அனுமதிக்கும்."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP அழைப்புகளைச் செய்தல்/பெறுதல்"</string>
diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml
index 313bc80..aa8e4db 100644
--- a/core/res/res/values-te/strings.xml
+++ b/core/res/res/values-te/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"కాలర్ ID ఆటోమేటిక్‌లపై పరిమితి లేదు. తర్వాత కాల్: పరిమితి లేదు"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"సేవ కేటాయించబడలేదు."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"మీరు కాలర్ ID సెట్టింగ్‌ను మార్చలేరు."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"డేటాను <xliff:g id="CARRIERDISPLAY">%s</xliff:g>కు స్విచ్ చేశారు"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"మీరు ఎప్పుడైనా సెట్టింగ్‌లలో దీనిని మార్చవచ్చు"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"మొబైల్ డేటా సేవ లేదు"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"అత్యవసర కాలింగ్ అందుబాటులో లేదు"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"వాయిస్ సర్వీస్ లేదు"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"లాక్ చేయి"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"కొత్త నోటిఫికేషన్"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"వర్చువల్ కీబోర్డ్"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"భౌతిక కీబోర్డ్"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"సెక్యూరిటీ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"కార్‌ మోడ్"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"మీ షేర్ చేయబడిన స్టోరేజ్ నుండి వీడియో ఫైల్‌లను చదవడానికి యాప్‌ను అనుమతిస్తుంది."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"షేర్ చేయబడిన స్టోరేజ్ నుండి ఇమేజ్ ఫైల్‌లను చదవండి"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"మీ షేర్ చేయబడిన స్టోరేజ్ నుండి ఇమేజ్ ఫైల్‌లను చదవడానికి యాప్‌ను అనుమతిస్తుంది."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"మీ షేర్ చేసిన నిల్వ యొక్క కంటెంట్‌లను ఎడిట్ చేయండి లేదా తొలగించండి"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"మీ షేర్ చేసిన నిల్వ యొక్క కంటెంట్‌లను రాయడానికి యాప్‌ను అనుమతిస్తుంది."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP కాల్స్‌ను చేయడానికి/స్వీకరించడానికి"</string>
@@ -2072,7 +2077,7 @@
     <string name="notification_appops_camera_active" msgid="8177643089272352083">"కెమెరా"</string>
     <string name="notification_appops_microphone_active" msgid="581333393214739332">"మైక్రోఫోన్"</string>
     <string name="notification_appops_overlay_active" msgid="5571732753262836481">"మీ స్క్రీన్‌పై ఇతర యాప్‌ల ద్వారా ప్రదర్శించబడుతోంది"</string>
-    <string name="notification_feedback_indicator" msgid="663476517711323016">"ఫీడ్‌బ్యాక్‌ను అందించండి"</string>
+    <string name="notification_feedback_indicator" msgid="663476517711323016">"ఫీడ్‌బ్యాక్ ఇవ్వండి"</string>
     <string name="notification_feedback_indicator_alerted" msgid="6552871804121942099">"ఈ నోటిఫికేషన్, ఆటోమేటిక్ సెట్టింగ్‌కు ప్రమోట్ చేయబడింది. ఫీడ్‌బ్యాక్‌ను అందించడానికి ట్యాప్ చేయండి."</string>
     <string name="notification_feedback_indicator_silenced" msgid="3799442124723177262">"ఈ నోటిఫికేషన్ స్థాయి నిశ్శబ్దంగా ఉండేలా తగ్గించబడింది. ఫీడ్‌బ్యాక్‌ను అందించడానికి ట్యాప్ చేయండి."</string>
     <string name="notification_feedback_indicator_promoted" msgid="9030204303764698640">"ఈ నోటిఫికేషన్‌కు ఎక్కువ ర్యాంక్ ఇవ్వబడింది. ఫీడ్‌బ్యాక్‌ను అందించడానికి ట్యాప్ చేయండి."</string>
diff --git a/core/res/res/values-television/config.xml b/core/res/res/values-television/config.xml
index 88bf18c..eaadc20 100644
--- a/core/res/res/values-television/config.xml
+++ b/core/res/res/values-television/config.xml
@@ -54,6 +54,12 @@
         com.android.systemui/com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity
     </string>
 
+    <!-- Component name of the activity used to inform a user about a sensor privacy update from
+     SW/HW privacy switches. -->
+    <string name="config_sensorStateChangedActivity" translatable="false">
+        com.android.systemui/com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity
+    </string>
+
     <!-- Component name of the activity that shows the request for access to a usb device. -->
     <string name="config_usbPermissionActivity" translatable="false">
         com.android.systemui/com.android.systemui.usb.tv.TvUsbPermissionActivity
diff --git a/core/res/res/values-television/styles.xml b/core/res/res/values-television/styles.xml
deleted file mode 100644
index ad9140c..0000000
--- a/core/res/res/values-television/styles.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<resources>
-    <style name="PermissionGrantButtonTop"
-        parent="@style/Widget.Leanback.Button.ButtonBarGravityStart" />
-    <style name="PermissionGrantButtonBottom"
-        parent="@style/Widget.Leanback.Button.ButtonBarGravityStart" />
-</resources>
\ No newline at end of file
diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml
index 447b42b..4c96aa1 100644
--- a/core/res/res/values-th/strings.xml
+++ b/core/res/res/values-th/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"หมายเลขผู้โทรได้รับการตั้งค่าเริ่มต้นเป็นไม่จำกัด การโทรครั้งต่อไป: ไม่จำกัด"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"ไม่มีการนำเสนอบริการ"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"คุณไม่สามารถเปลี่ยนการตั้งค่าหมายเลขผู้โทร"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"เปลี่ยนไปใช้อินเทอร์เน็ตมือถือของ <xliff:g id="CARRIERDISPLAY">%s</xliff:g> แล้ว"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"คุณเปลี่ยนตัวเลือกนี้ได้ทุกเมื่อในการตั้งค่า"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"ไม่มีบริการเน็ตมือถือ"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"หมายเลขฉุกเฉินไม่พร้อมใช้งาน"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"ไม่มีบริการเสียง"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ปิดล็อก"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"การแจ้งเตือนใหม่"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"แป้นพิมพ์เสมือน"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"แป้นพิมพ์จริง"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ความปลอดภัย"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"โหมดรถยนต์"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"อนุญาตให้แอปอ่านไฟล์วิดีโอจากพื้นที่เก็บข้อมูลที่แชร์"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"อ่านไฟล์ภาพจากพื้นที่เก็บข้อมูลที่แชร์"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"อนุญาตให้แอปอ่านไฟล์ภาพจากพื้นที่เก็บข้อมูลที่แชร์"</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"อ่านไฟล์รูปภาพและวิดีโอที่ผู้ใช้เลือกจากพื้นที่เก็บข้อมูลที่ใช้ร่วมกัน"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"อนุญาตให้แอปอ่านไฟล์รูปภาพและวิดีโอที่คุณเลือกจากพื้นที่เก็บข้อมูลที่ใช้ร่วมกัน"</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"แก้ไขหรือลบเนื้อหาในพื้นที่จัดเก็บข้อมูลที่ใช้ร่วมกัน"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"อนุญาตให้แอปเขียนเนื้อหาในพื้นที่จัดเก็บข้อมูลที่ใช้ร่วมกัน"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"โทร/รับสาย SIP"</string>
diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml
index 9aaa1b8..93495d2 100644
--- a/core/res/res/values-tl/strings.xml
+++ b/core/res/res/values-tl/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Naka-default na hindi pinaghihigpitan ang Caller ID. Susunod na tawag: Hindi pinaghihigpitan"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Hindi naprobisyon ang serbisyo."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Hindi mo mababago ang setting ng caller ID."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Nailipat sa <xliff:g id="CARRIERDISPLAY">%s</xliff:g> ang data"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Puwede mo itong baguhin anumang oras sa Mga Setting"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Walang serbisyo ng data sa mobile"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Hindi available ang pang-emergency na pagtawag"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Walang serbisyo para sa boses"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"I-lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Bagong notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual na keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Pisikal na keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Nagbibigay-daan sa app na magbasa ng mga video file mula sa iyong nakabahaging storage."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"magbasa ng mga image file mula sa nakabahaging storage"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Nagbibigay-daan sa app na magbasa ng mga image file mula sa iyong nakabahaging storage."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"baguhin o i-delete ang content ng nakabahagi mong storage"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Pinapayagan ang app na mag-write sa content ng nakabahagi mong storage."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"magsagawa/tumanggap ng mga tawag sa SIP"</string>
diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml
index 2f80037..ca6ff7f 100644
--- a/core/res/res/values-tr/strings.xml
+++ b/core/res/res/values-tr/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Arayan kimliği varsayılanları kısıtlanmamıştır. Sonraki çağrı: Kısıtlanmamış"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Hizmet sağlanamadı."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Arayanın kimliği ayarını değiştiremezsiniz."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Veriler şuraya aktarıldı: <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Bunu istediğiniz zaman Ayarlar\'da değiştirebilirsiniz"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Mobil veri hizmeti yok"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Acil durum çağrısı kullanılamaz"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Sesli çağrı hizmeti yok"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Tam gizlilik"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yeni bildirim"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Sanal klavye"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziksel klavye"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Güvenlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Araç modu"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Uygulamaya, paylaşılan depolama alanınızdaki video dosyalarını okuma izni verir."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"paylaşılan depolama alanınızdaki resim dosyalarını okuma"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Uygulamaya, paylaşılan depolama alanınızdaki resim dosyalarını okuma izni verir."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"paylaşılan depolama alanımın içeriğini değiştir veya sil"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Uygulamanın paylaşılan depolama alanınıza içerik yazmasına izin verir."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP aramaları yapma/alma"</string>
diff --git a/core/res/res/values-uk/strings.xml b/core/res/res/values-uk/strings.xml
index b7bb63b..459f9f0 100644
--- a/core/res/res/values-uk/strings.xml
+++ b/core/res/res/values-uk/strings.xml
@@ -74,6 +74,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Ідентиф. абонента за умовч. не обмеж. Наст. дзвінок: не обмежений"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Службу не ініціалізовано."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ви не можете змінювати налаштування ідентифікатора абонента."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Мобільний Інтернет переключено на <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Цей параметр можна будь-коли змінити в налаштуваннях"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Службу передавання мобільних даних заблоковано"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Екстрені виклики недоступні"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Немає голосової служби"</string>
@@ -266,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блокування"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Нове сповіщення"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Віртуальна клавіатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Фізична клавіатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безпека"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим автомобіля"</string>
@@ -699,6 +700,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Дозволяє додатку зчитувати відеофайли з вашого спільного сховища."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"зчитувати файли зображень зі спільного сховища"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Дозволяє додатку зчитувати файли зображень із вашого спільного сховища."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"змінювати чи видаляти вміст у спільній пам’яті"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Додаток може писати вміст у спільній пам’яті."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"здійснювати й отримувати дзвінки через протокол SIP"</string>
diff --git a/core/res/res/values-ur/strings.xml b/core/res/res/values-ur/strings.xml
index 58e342f..91b0efd 100644
--- a/core/res/res/values-ur/strings.xml
+++ b/core/res/res/values-ur/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"‏کالر ID کی ڈیفالٹ ترتیب غیر محدود کردہ ہے۔ اگلی کال: غیر محدود کردہ"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"سروس فراہم نہیں کی گئی۔"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"‏آپ کالر ID کی ترتیبات تبدیل نہیں کر سکتے ہیں۔"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"ڈیٹا <xliff:g id="CARRIERDISPLAY">%s</xliff:g> پر سوئچ کیا گیا"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"آپ اسے ترتیبات میں کسی بھی وقت تبدیل کر سکتے ہیں"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"کوئی موبائل ڈیٹا سروس دستیاب نہیں ہے"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"ہنگامی کالنگ دستیاب نہیں ہے"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"کوئی صوتی سروس نہیں"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"مقفل"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"‎999+‎"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"نئی اطلاع"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ورچوئل کی بورڈ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"فزیکل کی بورڈ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"سیکیورٹی"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"کار وضع"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"ایپ کو آپ کی اشتراک کردہ اسٹوریج سے ویڈیو فائلز کو پڑھنے کی اجازت دیتا ہے۔"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"اشتراک کردہ اسٹوریج سے تصویری فائلز کو پڑھیں"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"ایپ کو آپ کی اشتراک کردہ اسٹوریج سے تصویری فائلز کو پڑھنے کی اجازت دیتا ہے۔"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"اپنے اشتراک کردہ اسٹوریج کے مواد میں ترمیم کریں یا اسے حذف کریں"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"ایپ کو آپ کے اشتراک کردہ اسٹوریج کے مواد کو لکھنے کی اجازت دیتا ہے۔"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"‏SIP کالز کریں/موصول کریں"</string>
diff --git a/core/res/res/values-uz/strings.xml b/core/res/res/values-uz/strings.xml
index cf4478d..0a114fe 100644
--- a/core/res/res/values-uz/strings.xml
+++ b/core/res/res/values-uz/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Qo‘ng‘iroq qiluvchi ma’lumotlari cheklanmagan. Keyingi qo‘ng‘iroq: cheklanmagan"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Xizmat ishalamaydi."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Qo‘ng‘iroq qiluvchining ID raqami sozlamasini o‘zgartirib bo‘lmaydi."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Internet <xliff:g id="CARRIERDISPLAY">%s</xliff:g> operatoriga almashtirildi"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Buni istalgan vaqtda sozlamalardan o‘zgartirish mumkin"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Mobil internet ishlamayapti"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Favqulodda chaqiruv ishlamayapti"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ovozli chaqiruvlar ishlamaydi"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloklash"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yangi bildirishnoma"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual klaviatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tashqi klaviatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Xavfsizlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Avtomobil rejimi"</string>
@@ -697,6 +698,8 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Ilovaga video fayllarni umumiy xotiradan oʻqish imkonini beradi."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"umumiy xotiradan rasmli fayllarni oʻqish"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Ilovaga rasm fayllarini umumiy xotiradan oʻqish imkonini beradi."</string>
+    <string name="permlab_readVisualUserSelect" msgid="5516204215354667586">"umumiy xotiradan siz tanlagan rasm va video fayllarni oʻqish"</string>
+    <string name="permdesc_readVisualUserSelect" msgid="8027174717714968217">"Ilovaga siz tanlagan rasm va video fayllarni umumiy xotiradan oʻqish imkonini beradi."</string>
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"umumiy xotiradagi kontentlarni tahrirlash yoki oʻchirib tashlash"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Ilovaga umumiy xotiradagi kontentlarga yozish imkonini beradi."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"SIP qo‘ng‘iroqlarini amalga oshirish/qabul qilish"</string>
@@ -1593,7 +1596,7 @@
     <string name="issued_by" msgid="7872459822431585684">"Tegishli:"</string>
     <string name="validity_period" msgid="1717724283033175968">"Yaroqliligi:"</string>
     <string name="issued_on" msgid="5855489688152497307">"Chiqarilgan sanasi:"</string>
-    <string name="expires_on" msgid="1623640879705103121">"Amal qilish muddati:"</string>
+    <string name="expires_on" msgid="1623640879705103121">"Muddati:"</string>
     <string name="serial_number" msgid="3479576915806623429">"Serial raqam:"</string>
     <string name="fingerprints" msgid="148690767172613723">"Barmoq izlari:"</string>
     <string name="sha256_fingerprint" msgid="7103976380961964600">"SHA-256 barmoq izi:"</string>
diff --git a/core/res/res/values-vi/strings.xml b/core/res/res/values-vi/strings.xml
index 1a1bb91..3842634 100644
--- a/core/res/res/values-vi/strings.xml
+++ b/core/res/res/values-vi/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"Số gọi đến mặc định thành không bị giới hạn. Cuộc gọi tiếp theo. Không bị giới hạn"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Dịch vụ không được cấp phép."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Bạn không thể thay đổi cài đặt ID người gọi."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Đã chuyển dữ liệu sang <xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Bạn có thể thay đổi chế độ này bất cứ lúc nào trong phần Cài đặt"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Không có dịch vụ dữ liệu di động"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Không có dịch vụ gọi khẩn cấp"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Không có dịch vụ thoại"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Khóa"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Thông báo mới"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Bàn phím ảo"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Bàn phím vật lý"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bảo mật"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Chế độ trên ô tô"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Cho phép ứng dụng đọc tệp video trong bộ nhớ dùng chung."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"đọc tệp hình ảnh trong bộ nhớ dùng chung"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Cho phép ứng dụng đọc tệp hình ảnh trong bộ nhớ dùng chung."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"sửa đổi hoặc xóa nội dung của bộ nhớ dùng chung"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Cho phép ứng dụng ghi nội dung của bộ nhớ dùng chung."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"thực hiện/nhận các cuộc gọi qua SIP"</string>
diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml
index 4cb587b..1a84172 100644
--- a/core/res/res/values-zh-rCN/strings.xml
+++ b/core/res/res/values-zh-rCN/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"默认显示本机号码,在下一次通话中也显示"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"未提供服务。"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"您无法更改来电显示设置。"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"移动数据网络已切换至“<xliff:g id="CARRIERDISPLAY">%s</xliff:g>”"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"您可以随时在“设置”中更改这项设置"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"无法使用移动数据服务"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"无法使用紧急呼救服务"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"无法使用语音通话服务"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"锁定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虚拟键盘"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"实体键盘"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"车载模式"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"允许应用读取您共享存储空间中的视频文件。"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"读取共享存储空间中的图片文件"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"允许应用读取您共享存储空间中的图片文件。"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"修改或删除您共享存储空间中的内容"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"允许该应用写入您共享存储空间中的内容。"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"拨打/接听SIP电话"</string>
diff --git a/core/res/res/values-zh-rHK/strings.xml b/core/res/res/values-zh-rHK/strings.xml
index b9185b7..7af19ce 100644
--- a/core/res/res/values-zh-rHK/strings.xml
+++ b/core/res/res/values-zh-rHK/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"預設顯示來電號碼,下一通電話也繼續顯示。"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"未提供此服務。"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"您無法更改來電顯示設定。"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"流動數據已切換至「<xliff:g id="CARRIERDISPLAY">%s</xliff:g>」"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"您隨時可在「設定」中變更此設定"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"無法使用流動數據服務"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"無法撥打緊急電話"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"沒有語音服務"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"鎖定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虛擬鍵盤"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"實體鍵盤"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"車用模式"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"允許應用程式讀取共用儲存空間中的影片檔案。"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"讀取共用儲存空間中的圖片檔案"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"允許應用程式讀取共用儲存空間中的圖片檔案。"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"修改或刪除您共用儲存空間的內容"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"允許應用程式寫入您共用儲存空間的內容。"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"撥打/接聽 SIP 電話"</string>
diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml
index 4baced8..09b6f90 100644
--- a/core/res/res/values-zh-rTW/strings.xml
+++ b/core/res/res/values-zh-rTW/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"預設顯示本機號碼,下一通電話也繼續顯示。"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"無法提供此服務。"</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"你無法變更來電顯示設定。"</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"行動數據已切換至「<xliff:g id="CARRIERDISPLAY">%s</xliff:g>」"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"你隨時可以前往「設定」變更這項設定"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"沒有行動數據傳輸服務"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"無法撥打緊急電話"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"無法使用語音通話服務"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"鎖定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"超過 999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虛擬鍵盤"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"實體鍵盤"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"車用模式"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"允許應用程式讀取共用儲存空間中的影片檔案。"</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"讀取共用儲存空間中的圖片檔案"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"允許應用程式讀取共用儲存空間中的圖片檔案。"</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"修改或刪除共用儲存空間中的內容"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"允許這個應用程式寫入共用儲存空間中的內容。"</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"撥打/接聽 SIP 通話"</string>
diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml
index 23a557a..1e20552 100644
--- a/core/res/res/values-zu/strings.xml
+++ b/core/res/res/values-zu/strings.xml
@@ -72,6 +72,8 @@
     <string name="CLIRDefaultOffNextCallOff" msgid="2491576172356463443">"I-ID Yomshayeli ishintshela kokungavinjelwe. Ucingo olulandelayo: Aluvinjelwe"</string>
     <string name="serviceNotProvisioned" msgid="8289333510236766193">"Isevisi ayilungiselelwe."</string>
     <string name="CLIRPermanent" msgid="166443681876381118">"Ngeke ukwazi ukuguqul izilungiselelo zemininingwane yoshayayo."</string>
+    <string name="auto_data_switch_title" msgid="3286350716870518297">"Ushintshele idatha ku-<xliff:g id="CARRIERDISPLAY">%s</xliff:g>"</string>
+    <string name="auto_data_switch_content" msgid="803557715007110959">"Ungashintsha lokhu noma nini kumasethingi"</string>
     <string name="RestrictedOnDataTitle" msgid="1500576417268169774">"Ayikho isevisi yedatha yeselula"</string>
     <string name="RestrictedOnEmergencyTitle" msgid="2852916906106191866">"Ukushaya okuphuthumayo akutholakali"</string>
     <string name="RestrictedOnNormalTitle" msgid="7009474589746551737">"Ayikho isevisi yezwi"</string>
@@ -264,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Khiya"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Isaziso esisha"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Ikhibhodi ebonakalayo"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Ikhibhodi ephathekayo"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Ukuphepha"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Imodi yemoto"</string>
@@ -697,6 +698,10 @@
     <string name="permdesc_readMediaVideo" msgid="3846400073770403528">"Ivumela i-app ukuthi ifunde amafayela amavidiyo kwisitoreji sakho owabelane ngaso."</string>
     <string name="permlab_readMediaImages" msgid="4057590631020986789">"funda amafayela womfanekiso wesitoreji okwabelenwe ngaso"</string>
     <string name="permdesc_readMediaImages" msgid="5836219373138469259">"Ivumela i-app ukuthi ifunde amafayela ezithombe kwisitoreji sakho owabelane ngaso."</string>
+    <!-- no translation found for permlab_readVisualUserSelect (5516204215354667586) -->
+    <skip />
+    <!-- no translation found for permdesc_readVisualUserSelect (8027174717714968217) -->
+    <skip />
     <string name="permlab_sdcardWrite" msgid="4863021819671416668">"guqula noma susa okuqukethwe kwesitoreji sakho esabiwe"</string>
     <string name="permdesc_sdcardWrite" msgid="8376047679331387102">"Ivumela uhlelo lokusebenza ukuthi lubhale okuqukethwe kwesitoreji sakho esabiwe."</string>
     <string name="permlab_use_sip" msgid="8250774565189337477">"yenza/thola amakholi we-SIP"</string>
diff --git a/core/res/res/values/arrays.xml b/core/res/res/values/arrays.xml
index dff7751..9e735d0 100644
--- a/core/res/res/values/arrays.xml
+++ b/core/res/res/values/arrays.xml
@@ -166,12 +166,12 @@
     </string-array>
 
     <array name="sim_colors">
-        <item>@color/Teal_700</item>
-        <item>@color/Blue_700</item>
-        <item>@color/Indigo_700</item>
-        <item>@color/Purple_700</item>
-        <item>@color/Pink_700</item>
-        <item>@color/Red_700</item>
+        <item>@color/SIM_color_cyan</item>
+        <item>@color/SIM_color_blue</item>
+        <item>@color/SIM_color_green</item>
+        <item>@color/SIM_color_purple</item>
+        <item>@color/SIM_color_pink</item>
+        <item>@color/SIM_color_orange</item>
     </array>
 
     <!-- Used in ResolverTargetActionsDialogFragment -->
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index b5c7ea6..a5c0827 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1586,19 +1586,71 @@
          together. -->
     <attr name="foregroundServiceType">
         <!-- Data (photo, file, account) upload/download, backup/restore, import/export, fetch,
-        transfer over network between device and cloud.  -->
+            transfer over network between device and cloud.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, this type should NOT
+            be used: calling
+            {@link android.app.Service#startForeground(int, android.app.Notification, int)} with
+            this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}
+            is still allowed, but calling it with this type on devices running future platform
+            releases may get a {@link android.app.ForegroundServiceTypeNotAllowedException}.
+        -->
         <flag name="dataSync" value="0x01" />
-        <!-- Music, video, news or other media play. -->
+        <!-- Music, video, news or other media play.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PLAYBACK}.
+        -->
         <flag name="mediaPlayback" value="0x02" />
         <!-- Ongoing operations related to phone calls, video conferencing,
-             or similar interactive communication. -->
+            or similar interactive communication.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_PHONE_CALL} and
+            {@link android.Manifest.permission#MANAGE_OWN_CALLS}.
+        -->
         <flag name="phoneCall" value="0x04" />
-        <!-- GPS, map, navigation location update. -->
+        <!-- GPS, map, navigation location update.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_LOCATION} and one of the
+            following permissions:
+            {@link android.Manifest.permission#ACCESS_COARSE_LOCATION},
+            {@link android.Manifest.permission#ACCESS_FINE_LOCATION}.
+        -->
         <flag name="location" value="0x08" />
-        <!-- Auto, bluetooth, TV or other devices connection, monitoring and interaction. -->
+        <!-- Auto, bluetooth, TV or other devices connection, monitoring and interaction.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_CONNECTED_DEVICE} and one of the
+            following permissions:
+            {@link android.Manifest.permission#BLUETOOTH_CONNECT},
+            {@link android.Manifest.permission#CHANGE_NETWORK_STATE},
+            {@link android.Manifest.permission#CHANGE_WIFI_STATE},
+            {@link android.Manifest.permission#CHANGE_WIFI_MULTICAST_STATE},
+            {@link android.Manifest.permission#NFC},
+            {@link android.Manifest.permission#TRANSMIT_IR},
+            or has been granted the access to one of the attached USB devices/accessories.
+        -->
         <flag name="connectedDevice" value="0x10" />
         <!-- Managing a media projection session, e.g, for screen recording or taking
-             screenshots.-->
+            screenshots.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_MEDIA_PROJECTION}, and the user
+            must have allowed the screen capture request from this app.
+        -->
         <flag name="mediaProjection" value="0x20" />
         <!-- Use the camera device or record video.
 
@@ -1606,6 +1658,12 @@
             and above, a foreground service will not be able to access the camera if this type is
             not specified in the manifest and in
             {@link android.app.Service#startForeground(int, android.app.Notification, int)}.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_CAMERA} and
+            {@link android.Manifest.permission#CAMERA}.
             -->
         <flag name="camera" value="0x40" />
         <!--Use the microphone device or record audio.
@@ -1614,8 +1672,54 @@
             and above, a foreground service will not be able to access the microphone if this type
             is not specified in the manifest and in
             {@link android.app.Service#startForeground(int, android.app.Notification, int)}.
+
+            <p>For apps with <code>targetSdkVersion</code>
+            {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, starting a foreground
+            service with this type will require permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_MICROPHONE} and one of the
+            following permissions:
+            {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT},
+            {@link android.Manifest.permission#RECORD_AUDIO}.
             -->
         <flag name="microphone" value="0x80" />
+        <!--Health, wellness and fitness.
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following
+            permissions
+            {@link android.Manifest.permission#ACTIVITY_RECOGNITION},
+            {@link android.Manifest.permission#BODY_SENSORS},
+            {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}.
+        -->
+        <flag name="health" value="0x100" />
+        <!-- Messaging use cases which host local server to relay messages across devices.
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_REMOTE_MESSAGING} in order to use
+            this type.
+        -->
+        <flag name="remoteMessaging" value="0x200" />
+        <!-- The system exmpted foreground service use cases.
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_SYSTEM_EXEMPTED} in order to use
+            this type. Apps are allowed to use this type only in the use cases listed in
+            {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED}.
+        -->
+        <flag name="systemExempted" value="0x400" />
+        <!-- "Short service" foreground service type. See
+           TODO: Change it to a real link
+           {@code android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE}.
+           for more details.
+        -->
+        <flag name="shortService" value="0x800" />
+        <!-- Use cases that can't be categorized into any other foreground service types, but also
+            can't use @link android.app.job.JobInfo.Builder} APIs.
+            See {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_SPECIAL_USE} for the
+            best practice of the use of this type.
+
+            <p>Requires the app to hold the permission
+            {@link android.Manifest.permission#FOREGROUND_SERVICE_SPECIAL_USE} in order to use
+            this type.
+        -->
+        <flag name="specialUse" value="0x40000000" />
     </attr>
 
     <!-- Enable sampled memory bug detection in this process.
@@ -3062,6 +3166,20 @@
         <attr name="canDisplayOnRemoteDevices" format="boolean"/>
         <attr name="allowUntrustedActivityEmbedding" />
         <attr name="knownActivityEmbeddingCerts" />
+        <!-- Specifies the category of the target display the activity is expected to run on. Upon
+             creation, a virtual display can specify which display categories it supports and one of
+             the category must be present in the activity's manifest to allow this activity to run.
+             The default value is {@code null}, which indicates the activity does not belong to a
+             restricted display category and thus can only run on a display that didn't specify any
+             display categories. Each activity can only specify one category it targets to but a
+             virtual display can accommodate multiple restricted categories.
+
+             <p> This field should be formatted as a Java-language-style free form string(for
+             example, com.google.automotive_entertainment), which may contain uppercase or lowercase
+             letters ('A' through 'Z'), numbers, and underscores ('_') but may only start with
+             letters.
+         -->
+        <attr name="targetDisplayCategory" format="string"/>
     </declare-styleable>
 
     <!-- The <code>activity-alias</code> tag declares a new
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 77d7c43..8c356b4 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -198,18 +198,18 @@
     <color name="instant_app_badge">#ff757575</color><!-- Grey -->
 
     <!-- Multi-sim sim colors -->
-    <color name="Teal_700">#ff00796b</color>
-    <color name="Teal_800">#ff00695c</color>
-    <color name="Blue_700">#ff3367d6</color>
-    <color name="Blue_800">#ff2a56c6</color>
-    <color name="Indigo_700">#ff303f9f</color>
-    <color name="Indigo_800">#ff283593</color>
-    <color name="Purple_700">#ff7b1fa2</color>
-    <color name="Purple_800">#ff6a1b9a</color>
-    <color name="Pink_700">#ffc2185b</color>
-    <color name="Pink_800">#ffad1457</color>
-    <color name="Red_700">#ffc53929</color>
-    <color name="Red_800">#ffb93221</color>
+    <color name="SIM_color_cyan">#ff006D74</color><!-- Material Custom Cyan -->
+    <color name="SIM_dark_mode_color_cyan">#ff4DD0E1</color><!-- Material Cyan 300 -->
+    <color name="SIM_color_blue">#ff185ABC</color><!-- Material Blue 800 -->
+    <color name="SIM_dark_mode_color_blue">#ff8AB4F8</color><!-- Material Blue 300 -->
+    <color name="SIM_color_green">#ff137333</color><!-- Material Green 800 -->
+    <color name="SIM_dark_mode_color_green">#ff81C995</color><!-- Material Green 300 -->
+    <color name="SIM_color_purple">#ff7627bb</color><!-- Material Purple 800 -->
+    <color name="SIM_dark_mode_color_purple">#ffC58AF9</color><!-- Material Purple 300 -->
+    <color name="SIM_color_pink">#ffb80672</color><!-- Material Pink 800 -->
+    <color name="SIM_dark_mode_color_pink">#ffff8bcb</color><!-- Material Pink 300 -->
+    <color name="SIM_color_orange">#ff995400</color><!-- Material Custom Orange -->
+    <color name="SIM_dark_mode_color_orange">#fffcad70</color><!-- Material Orange 300 -->
 
     <color name="resize_shadow_start_color">#2a000000</color>
     <color name="resize_shadow_end_color">#00000000</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8fde9d5..6c18259 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -342,22 +342,6 @@
     <!-- Mask to use when checking skb mark defined in config_networkWakeupPacketMark above. -->
     <integer name="config_networkWakeupPacketMask">0</integer>
 
-    <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
-         Those frames are identified by the field Eth-type having values
-         less than 0x600 -->
-    <bool translatable="false" name="config_apfDrop802_3Frames">true</bool>
-
-    <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
-         will be dropped
-         TODO: need to put proper values, these are for testing purposes only -->
-    <integer-array translatable="false" name="config_apfEthTypeBlackList">
-        <item>0x88A2</item>
-        <item>0x88A4</item>
-        <item>0x88B8</item>
-        <item>0x88CD</item>
-        <item>0x88E3</item>
-    </integer-array>
-
     <!-- Default value for ConnectivityManager.getMultipathPreference() on metered networks. Actual
          device behaviour is controlled by Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE.
          This is the default value of that setting. -->
@@ -1871,6 +1855,10 @@
     -->
     <string name="config_defaultCaptivePortalLoginPackageName" translatable="false">com.android.captiveportallogin</string>
 
+    <!-- The package name of the dock manager app. Must be granted the
+         POST_NOTIFICATIONS permission. -->
+    <string name="config_defaultDockManagerPackageName" translatable="false"></string>
+
     <!-- Whether to enable geocoder overlay which allows geocoder to be replaced
          by an app at run-time. When disabled, only the
          config_geocoderProviderPackageName package will be searched for
@@ -2027,10 +2015,6 @@
          STREAM_MUSIC as if it's on TV platform. -->
     <bool name="config_single_volume">false</bool>
 
-    <!-- Flag indicating whether notification and ringtone volumes
-         are controlled together (aliasing is true) or not. -->
-    <bool name="config_alias_ring_notif_stream_types">true</bool>
-
     <!-- The number of volume steps for the notification stream -->
     <integer name="config_audio_notif_vol_steps">7</integer>
 
@@ -2442,17 +2426,24 @@
     <integer name="config_dreamsBatteryLevelDrainCutoff">5</integer>
     <!-- Limit of how long the device can remain unlocked due to attention checking.  -->
     <integer name="config_attentionMaximumExtension">900000</integer> <!-- 15 minutes.  -->
-    <!-- Is the system user the only user allowed to dream. -->
-    <bool name="config_dreamsOnlyEnabledForSystemUser">false</bool>
+    <!-- Whether there is to be a chosen Dock User who is the only user allowed to dream. -->
+    <bool name="config_dreamsOnlyEnabledForDockUser">false</bool>
     <!-- Whether dreams are disabled when ambient mode is suppressed. -->
     <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
 
+    <!-- The duration in milliseconds of the dream opening animation.  -->
+    <integer name="config_dreamOpenAnimationDuration">250</integer>
+    <!-- The duration in milliseconds of the dream closing animation.  -->
+    <integer name="config_dreamCloseAnimationDuration">300</integer>
+
     <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to
          assistant activities (ACTIVITY_TYPE_ASSISTANT) -->
     <bool name="config_dismissDreamOnActivityStart">false</bool>
 
-    <!-- The prefix of dream component names that are loggable. If empty, logs "other" for all. -->
-    <string name="config_loggable_dream_prefix" translatable="false"></string>
+    <!-- The prefixes of dream component names that are loggable.
+         Matched against ComponentName#flattenToString() for dream components.
+         If empty, logs "other" for all. -->
+    <string-array name="config_loggable_dream_prefixes"></string-array>
 
     <!-- ComponentName of a dream to show whenever the system would otherwise have
          gone to sleep.  When the PowerManager is asked to go to sleep, it will instead
@@ -2612,6 +2603,9 @@
          movement threshold under which hover is considered "stationary". -->
     <dimen name="config_viewConfigurationHoverSlop">4dp</dimen>
 
+    <!-- Margin around text line bounds where stylus handwriting gestures should be supported. -->
+    <dimen name="config_viewConfigurationHandwritingGestureLineMargin">16dp</dimen>
+
     <!-- Multiplier for gesture thresholds when a MotionEvent classification is ambiguous. -->
     <item name="config_ambiguousGestureMultiplier" format="float" type="dimen">2.0</item>
 
@@ -2682,9 +2676,9 @@
          Should be false for most devices, except automotive vehicle with passenger displays. -->
     <bool name="config_multiuserUsersOnSecondaryDisplays">false</bool>
 
-    <!-- Whether to automatically switch a non-primary user back to the primary user after a
-         timeout when the device is docked.  -->
-    <bool name="config_enableTimeoutToUserZeroWhenDocked">false</bool>
+    <!-- Whether to automatically switch to the designated Dock User (the user chosen for
+         displaying dreams, etc.) after a timeout when the device is docked.  -->
+    <bool name="config_enableTimeoutToDockUserWhenDocked">false</bool>
 
     <!-- Whether to only install system packages on a user if they're allowlisted for that user
          type. These are flags and can be freely combined.
@@ -2895,7 +2889,7 @@
             >com.android.systemui/com.android.systemui.usb.UsbDebuggingActivity</string>
 
     <!-- Name of the activity that prompts the secondary user to acknowledge they need to
-         switch to the primary user to enable USB debugging.
+         switch to an admin user to enable USB debugging.
          Can be customized for other product types -->
     <string name="config_customAdbPublicKeyConfirmationSecondaryUserComponent"
             >com.android.systemui/com.android.systemui.usb.UsbDebuggingSecondaryUserActivity</string>
@@ -2907,7 +2901,7 @@
             >com.android.systemui/com.android.systemui.wifi.WifiDebuggingActivity</string>
 
     <!-- Name of the activity that prompts the secondary user to acknowledge they need to
-         switch to the primary user to enable wireless debugging.
+         switch to an admin user to enable wireless debugging.
          Can be customized for other product types -->
     <string name="config_customAdbWifiNetworkConfirmationSecondaryUserComponent"
             >com.android.systemui/com.android.systemui.wifi.WifiDebuggingSecondaryUserActivity</string>
@@ -2934,6 +2928,9 @@
     <string name="config_usbResolverActivity" translatable="false"
             >com.android.systemui/com.android.systemui.usb.UsbResolverActivity</string>
 
+    <!-- Component name of the activity used to inform a user about a sensor privacy state chage. -->
+    <string name="config_sensorStateChangedActivity" translatable="false"></string>
+
     <!-- Component name of the activity used to inform a user about a sensory being blocked because
      of privacy settings. -->
     <string name="config_sensorUseStartedActivity" translatable="false"
@@ -2965,6 +2962,10 @@
     <string name="config_carrierAppInstallDialogComponent" translatable="false"
             >com.android.simappdialog/com.android.simappdialog.InstallCarrierAppActivity</string>
 
+    <!-- Name of the dialog that is used to get or save an app credential -->
+    <string name="config_credentialManagerDialogComponent" translatable="false"
+            >com.android.credentialmanager/com.android.credentialmanager.CredentialSelectorActivity</string>
+
     <!-- Apps that are authorized to access shared accounts, overridden by product overlays -->
     <string name="config_appsAuthorizedForSharedAccounts" translatable="false">;com.android.settings;</string>
 
@@ -3556,9 +3557,9 @@
          config_sidefpsSkipWaitForPowerVendorAcquireMessage -->
     <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer>
 
-    <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped.
-         config_sidefpsSkipWaitForPowerOnFingerUp must be true and
-         config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. -->
+    <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped
+         when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire
+         message equals this constant -->
     <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer>
 
     <!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
@@ -3687,6 +3688,12 @@
          experience while the device is non-interactive. -->
     <bool name="config_emergencyGestureEnabled">true</bool>
 
+    <!-- Default value for Use Emergency SOS in Settings false = disabled, true = enabled -->
+    <bool name="config_defaultEmergencyGestureEnabled">true</bool>
+
+    <!-- Default value for Use Play countdown alarm in Settings false = disabled, true = enabled -->
+    <bool name="config_defaultEmergencyGestureSoundEnabled">false</bool>
+
     <!-- Allow the gesture power + volume up to change the ringer mode while the device
          is interactive. -->
     <bool name="config_volumeHushGestureEnabled">true</bool>
@@ -3971,7 +3978,7 @@
     <!-- Colon separated list of package names that should be granted DND access -->
     <string name="config_defaultDndAccessPackages" translatable="false">com.android.camera2</string>
 
-    <!-- User restrictions set when the first user is created.
+    <!-- User restrictions set on the SYSTEM user when it is first created.
          Note: Also update appropriate overlay files. -->
     <string-array translatable="false" name="config_defaultFirstUserRestrictions">
     </string-array>
@@ -4413,17 +4420,21 @@
         or empty if the default should be used. -->
     <string translatable="false" name="config_deviceSpecificDeviceStatePolicyProvider"></string>
 
+    <!-- Class name of the device specific implementation of InputMethodManagerService
+        or empty if the default should be used. -->
+    <string translatable="false" name="config_deviceSpecificInputMethodManagerService"></string>
+
     <!-- Component name of media projection permission dialog -->
     <string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity</string>
 
     <!-- Corner radius of system dialogs -->
-    <dimen name="config_dialogCornerRadius">2dp</dimen>
+    <dimen name="config_dialogCornerRadius">28dp</dimen>
     <!-- Corner radius of system buttons -->
-    <dimen name="config_buttonCornerRadius">@dimen/control_corner_material</dimen>
+    <dimen name="config_buttonCornerRadius">4dp</dimen>
     <!-- Corner radius for bottom sheet system dialogs -->
-    <dimen name="config_bottomDialogCornerRadius">@dimen/config_dialogCornerRadius</dimen>
+    <dimen name="config_bottomDialogCornerRadius">16dp</dimen>
     <!-- Corner radius of system progress bars -->
-    <dimen name="config_progressBarCornerRadius">@dimen/progress_bar_corner_material</dimen>
+    <dimen name="config_progressBarCornerRadius">1000dp</dimen>
     <!-- Controls whether system buttons use all caps for text -->
     <bool name="config_buttonTextAllCaps">true</bool>
     <!-- Name of the font family used for system surfaces where the font should use medium weight -->
@@ -4937,6 +4948,11 @@
     <!-- If face auth sends the user directly to home/last open app, or stays on keyguard -->
     <bool name="config_faceAuthDismissesKeyguard">true</bool>
 
+    <!-- Default value for whether a SFPS device is required to be
+        {@link KeyguardUpdateMonitor#isDeviceInteractive()} for fingerprint auth
+        to unlock the device. -->
+    <bool name="config_requireScreenOnToAuthEnabled">false</bool>
+
     <!-- The component name for the default profile supervisor, which can be set as a profile owner
     even after user setup is complete. The defined component should be used for supervision purposes
     only. The component must be part of a system app. -->
@@ -5209,7 +5225,7 @@
             but isn't supported on the device or both dark scrim alpha and blur radius aren't
             provided.
      -->
-    <color name="config_letterboxBackgroundColor">@android:color/system_neutral2_900</color>
+    <color name="config_letterboxBackgroundColor">@color/letterbox_background</color>
 
     <!-- Horizontal position of a center of the letterboxed app window.
         0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
@@ -5937,4 +5953,9 @@
     <!-- Whether the lock screen is allowed to run its own live wallpaper,
          different from the home screen wallpaper. -->
     <bool name="config_independentLockscreenLiveWallpaper">false</bool>
+
+    <!-- List of certificate to be used for font fs-verity integrity verification -->
+    <string-array translatable="false" name="config_fontManagerServiceCerts">
+    </string-array>
+
 </resources>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index a1d73ff..f2a16d3 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -117,4 +117,22 @@
     <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController -->
     <bool name="config_using_subscription_manager_service">false</bool>
     <java-symbol type="bool" name="config_using_subscription_manager_service" />
+
+    <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config,
+         should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in
+         Android Emergency DB. If this value is true, emergency numbers for a country, sourced from
+         modem/config, will be ignored if that country is 'locked' in Android Emergency DB. -->
+    <bool name="ignore_modem_config_emergency_numbers">false</bool>
+    <java-symbol type="bool" name="ignore_modem_config_emergency_numbers" />
+
+    <!-- Boolean indicating whether emergency numbers routing from the android emergency number
+         database should be ignored (i.e. routing will always be set to UNKNOWN). If this value is
+         true, routing from the android emergency number database will be ignored. -->
+    <bool name="ignore_emergency_number_routing_from_db">false</bool>
+    <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" />
+
+    <!-- Whether "Virtual DSDA", i.e. in-call IMS connectivity can be provided on both subs with
+         only single logical modem, by using its data connection in addition to cellular IMS. -->
+    <bool name="config_enable_virtual_dsda">false</bool>
+    <java-symbol type="bool" name="config_enable_virtual_dsda" />
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 1997261..9dbb6a0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -979,9 +979,9 @@
     <dimen name="controls_thumbnail_image_max_width">280dp</dimen>
 
     <!-- System-provided radius for the background view of app widgets. The resolved value of this resource may change at runtime. -->
-    <dimen name="system_app_widget_background_radius">16dp</dimen>
+    <dimen name="system_app_widget_background_radius">28dp</dimen>
     <!-- System-provided radius for inner views on app widgets. The resolved value of this resource may change at runtime. -->
-    <dimen name="system_app_widget_inner_radius">8dp</dimen>
+    <dimen name="system_app_widget_inner_radius">20dp</dimen>
     <!-- System-provided padding for inner views on app widgets. The resolved value of this resource may change at runtime. @removed -->
     <dimen name="__removed_system_app_widget_internal_padding">16dp</dimen>
 
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 89741ef..61229cb 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -116,6 +116,7 @@
     <public name="handwritingBoundsOffsetBottom" />
     <public name="accessibilityDataPrivate" />
     <public name="enableTextStylingShortcuts" />
+    <public name="targetDisplayCategory"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01cd0000">
@@ -131,6 +132,8 @@
   </staging-public-group>
 
   <staging-public-group type="dimen" first-id="0x01ca0000">
+    <!-- @hide @SystemApi -->
+    <public name="config_viewConfigurationHandwritingGestureLineMargin" />
   </staging-public-group>
 
   <staging-public-group type="color" first-id="0x01c90000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 5f99113..7714082 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -202,6 +202,11 @@
     <!-- Displayed to tell the user that they cannot change the caller ID setting. -->
     <string name="CLIRPermanent">You can\'t change the caller ID setting.</string>
 
+    <!-- Notification title to tell the user that auto data switch has occurred. [CHAR LIMIT=NOTIF_TITLE] -->
+    <string name="auto_data_switch_title">Switched data to <xliff:g id="carrierDisplay" example="Verizon">%s</xliff:g></string>
+    <!-- Notification content to tell the user that auto data switch can be disabled at settings. [CHAR LIMIT=NOTIF_BODY] -->
+    <string name="auto_data_switch_content">You can change this anytime in Settings</string>
+
     <!-- Notification title to tell the user that data service is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] -->
     <string name="RestrictedOnDataTitle">No mobile data service</string>
     <!-- Notification title to tell the user that emergency calling is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] -->
@@ -741,9 +746,6 @@
     <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen -->
     <string name="notification_hidden_text">New notification</string>
 
-    <!-- Text shown when viewing channel settings for notifications related to the virtual keyboard -->
-    <string name="notification_channel_virtual_keyboard">Virtual keyboard</string>
-
     <!-- Text shown when viewing channel settings for notifications related to the hardware keyboard -->
     <string name="notification_channel_physical_keyboard">Physical keyboard</string>
 
@@ -1141,6 +1143,66 @@
     <string name="permdesc_foregroundService">Allows the app to make use of foreground services.</string>
 
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceCamera">run foreground service with the type \"camera\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceCamera">Allows the app to make use of foreground services with the type \"camera\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceConnectedDevice">run foreground service with the type \"connectedDevice\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceConnectedDevice">Allows the app to make use of foreground services with the type \"connectedDevice\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceDataSync">run foreground service with the type \"dataSync\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceDataSync">Allows the app to make use of foreground services with the type \"dataSync\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceLocation">run foreground service with the type \"location\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceLocation">Allows the app to make use of foreground services with the type \"location\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceMediaPlayback">run foreground service with the type \"mediaPlayback\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceMediaPlayback">Allows the app to make use of foreground services with the type \"mediaPlayback\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceMediaProjection">run foreground service with the type \"mediaProjection\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceMediaProjection">Allows the app to make use of foreground services with the type \"mediaProjection\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceMicrophone">run foreground service with the type \"microphone\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceMicrophone">Allows the app to make use of foreground services with the type \"microphone\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServicePhoneCall">run foreground service with the type \"phoneCall\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServicePhoneCall">Allows the app to make use of foreground services with the type \"phoneCall\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceHealth">run foreground service with the type \"health\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceHealth">Allows the app to make use of foreground services with the type \"health\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceRemoteMessaging">run foreground service with the type \"remoteMessaging\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceRemoteMessaging">Allows the app to make use of foreground services with the type \"remoteMessaging\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceSystemExempted">run foreground service with the type \"systemExempted\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceSystemExempted">Allows the app to make use of foreground services with the type \"systemExempted\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permlab_foregroundServiceSpecialUse">run foreground service with the type \"specialUse\"</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+    <string name="permdesc_foregroundServiceSpecialUse">Allows the app to make use of foreground services with the type \"specialUse\"</string>
+
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_getPackageSize">measure app storage space</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permdesc_getPackageSize">Allows the app to retrieve its code, data, and cache sizes</string>
@@ -1300,6 +1362,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE] -->
     <string name="permdesc_recordBackgroundAudio">This app can record audio using the microphone at any time.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] -->
+    <string name="permlab_detectScreenCapture">detect screen captures of app windows</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] -->
+    <string name="permdesc_detectScreenCapture">This app will get notified when a screenshot is taken while the app is in use.</string>
+
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_sim_communication">send commands to the SIM</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
@@ -1931,6 +1998,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
     <string name="permdesc_readMediaImages">Allows the app to read image files from your shared storage.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
+    <string name="permlab_readVisualUserSelect">read user selected image and video files from shared storage</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
+    <string name="permdesc_readVisualUserSelect">Allows the app to read image and video files that you select from your shared storage.</string>
+
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] -->
     <string name="permlab_sdcardWrite">modify or delete the contents of your shared storage</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] -->
@@ -5752,32 +5824,6 @@
     <!-- Title for the harmful app warning dialog. [CHAR LIMIT=40] -->
     <string name="harmful_app_warning_title">Harmful app detected</string>
 
-    <!-- Title for the log access confirmation dialog. [CHAR LIMIT=NONE] -->
-    <string name="log_access_confirmation_title">Allow <xliff:g id="log_access_app_name" example="Example App">%s</xliff:g> to access all device logs?</string>
-    <!-- Label for the allow button on the log access confirmation dialog. [CHAR LIMIT=40] -->
-    <string name="log_access_confirmation_allow">Allow one-time access</string>
-    <!-- Label for the deny button on the log access confirmation dialog. [CHAR LIMIT=20] -->
-    <string name="log_access_confirmation_deny">Don\u2019t allow</string>
-
-    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
-    <string name="log_access_confirmation_body" product="default">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
-        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.
-    </string>
-
-    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
-    <string name="log_access_confirmation_body" product="tv">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
-        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.\n\nLearn more at g.co/android/devicelogs.
-    </string>
-
-    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
-    <string name="log_access_confirmation_learn_more" product="default" translatable="false">&lt;a href="https://support.google.com/android?p=system_logs#topic=7313011"&gt;Learn more&lt;/a&gt;</string>
-
-    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
-    <string name="log_access_confirmation_learn_more" product="tv" translatable="false"></string>
-
-    <!-- Privacy notice do not show [CHAR LIMIT=20] -->
-    <string name="log_access_do_not_show_again">Don\u2019t show again</string>
-
     <!-- Text describing a permission request for one app to show another app's
          slices [CHAR LIMIT=NONE] -->
     <string name="slices_permission_request"><xliff:g id="app" example="Example App">%1$s</xliff:g> wants to show <xliff:g id="app_2" example="Other Example App">%2$s</xliff:g> slices</string>
@@ -6345,6 +6391,8 @@
     <string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
     <!-- Error message indicating the user cannot access secure content when running on a virtual device. [CHAR LIMIT=NONE] -->
     <string name="vdm_secure_window">This can’t be accessed while streaming. Try on your phone instead.</string>
+    <!-- Error message indicating the user cannot view picture-in-picture when running on a virtual device. [CHAR LIMIT=NONE] -->
+    <string name="vdm_pip_blocked">Can’t view picture-in-picture while streaming</string>
 
     <!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
     <string name="system_locale_title">System default</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 2dd563d..476c18e 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1541,40 +1541,4 @@
     <style name="NotificationTombstoneAction" parent="NotificationAction">
       <item name="textColor">#555555</item>
     </style>
-
-    <!-- The style for log access consent text -->
-    <style name="AllowLogAccess">
-        <item name="android:textSize">24sp</item>
-        <item name="android:fontFamily">google-sans</item>
-    </style>
-
-    <style name="PrimaryAllowLogAccess">
-        <item name="android:textSize">14sp</item>
-        <item name="android:fontFamily">google-sans-text</item>
-    </style>
-
-    <style name="PermissionGrantButtonTextAppearance">
-        <item name="android:fontFamily">google-sans-medium</item>
-        <item name="android:textSize">14sp</item>
-        <item name="android:textColor">@android:color/system_neutral1_900</item>
-    </style>
-
-    <style name="PermissionGrantButtonTop"
-           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
-        <item name="android:layout_width">332dp</item>
-        <item name="android:layout_height">56dp</item>
-        <item name="android:layout_marginTop">2dp</item>
-        <item name="android:layout_marginBottom">2dp</item>
-        <item name="android:background">@drawable/grant_permissions_buttons_top</item>
-    </style>
-
-    <style name="PermissionGrantButtonBottom"
-           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
-        <item name="android:layout_width">332dp</item>
-        <item name="android:layout_height">56dp</item>
-        <item name="android:layout_marginTop">2dp</item>
-        <item name="android:layout_marginBottom">2dp</item>
-        <item name="android:background">@drawable/grant_permissions_buttons_bottom</item>
-    </style>
-
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b94d799..5811ed9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -282,7 +282,6 @@
   <java-symbol type="attr" name="autofillSaveCustomSubtitleMaxHeight"/>
   <java-symbol type="bool" name="action_bar_embed_tabs" />
   <java-symbol type="bool" name="action_bar_expanded_action_views_exclusive" />
-  <java-symbol type="bool" name="config_alias_ring_notif_stream_types" />
   <java-symbol type="integer" name="config_audio_notif_vol_default" />
   <java-symbol type="integer" name="config_audio_notif_vol_steps" />
   <java-symbol type="integer" name="config_audio_ring_vol_default" />
@@ -374,6 +373,7 @@
   <java-symbol type="string" name="config_usbResolverActivity" />
   <java-symbol type="string" name="config_sensorUseStartedActivity" />
   <java-symbol type="string" name="config_sensorUseStartedActivity_hwToggle" />
+  <java-symbol type="string" name="config_sensorStateChangedActivity" />
   <java-symbol type="string" name="config_hdmiCecSetMenuLanguageActivity" />
   <java-symbol type="integer" name="config_minNumVisibleRecentTasks_lowRam" />
   <java-symbol type="integer" name="config_maxNumVisibleRecentTasks_lowRam" />
@@ -466,7 +466,7 @@
   <java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
   <java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
   <java-symbol type="bool" name="config_multiuserUsersOnSecondaryDisplays" />
-  <java-symbol type="bool" name="config_enableTimeoutToUserZeroWhenDocked" />
+  <java-symbol type="bool" name="config_enableTimeoutToDockUserWhenDocked" />
   <java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
   <java-symbol type="xml" name="config_user_types" />
   <java-symbol type="integer" name="config_safe_media_volume_index" />
@@ -483,6 +483,7 @@
   <java-symbol type="array" name="config_deviceSpecificSystemServices" />
   <java-symbol type="string" name="config_deviceSpecificDevicePolicyManagerService" />
   <java-symbol type="string" name="config_deviceSpecificAudioService" />
+  <java-symbol type="string" name="config_deviceSpecificInputMethodManagerService" />
   <java-symbol type="integer" name="config_num_physical_slots" />
   <java-symbol type="integer" name="config_default_cellular_usage_setting" />
   <java-symbol type="array" name="config_supported_cellular_usage_settings" />
@@ -602,6 +603,8 @@
   <java-symbol type="string" name="RestrictedOnEmergencyTitle" />
   <java-symbol type="string" name="RestrictedOnNormalTitle" />
   <java-symbol type="string" name="RestrictedStateContent" />
+  <java-symbol type="string" name="auto_data_switch_title" />
+  <java-symbol type="string" name="auto_data_switch_content" />
   <java-symbol type="string" name="RestrictedStateContentMsimTemplate" />
   <java-symbol type="string" name="notification_channel_network_alert" />
   <java-symbol type="string" name="notification_channel_call_forward" />
@@ -1688,15 +1691,6 @@
 
   <!-- From android.policy -->
   <java-symbol type="anim" name="app_starting_exit" />
-  <java-symbol type="anim" name="dock_top_enter" />
-  <java-symbol type="anim" name="dock_top_exit" />
-  <java-symbol type="anim" name="dock_bottom_enter" />
-  <java-symbol type="anim" name="dock_bottom_exit" />
-  <java-symbol type="anim" name="dock_bottom_exit_keyguard" />
-  <java-symbol type="anim" name="dock_left_enter" />
-  <java-symbol type="anim" name="dock_left_exit" />
-  <java-symbol type="anim" name="dock_right_enter" />
-  <java-symbol type="anim" name="dock_right_exit" />
   <java-symbol type="anim" name="fade_in" />
   <java-symbol type="anim" name="fade_out" />
   <java-symbol type="anim" name="voice_activity_close_exit" />
@@ -1992,7 +1986,6 @@
   <java-symbol type="color" name="config_defaultNotificationColor" />
   <java-symbol type="color" name="decor_view_status_guard" />
   <java-symbol type="color" name="decor_view_status_guard_light" />
-  <java-symbol type="drawable" name="ic_notification_ime_default" />
   <java-symbol type="drawable" name="ic_menu_refresh" />
   <java-symbol type="drawable" name="ic_settings" />
   <java-symbol type="drawable" name="ic_voice_search" />
@@ -2044,8 +2037,6 @@
   <java-symbol type="integer" name="config_networkAvoidBadWifi" />
   <java-symbol type="integer" name="config_networkWakeupPacketMark" />
   <java-symbol type="integer" name="config_networkWakeupPacketMask" />
-  <java-symbol type="bool" name="config_apfDrop802_3Frames" />
-  <java-symbol type="array" name="config_apfEthTypeBlackList" />
   <java-symbol type="integer" name="config_networkDefaultDailyMultipathQuotaBytes" />
   <java-symbol type="integer" name="config_networkMeteredMultipathPreference" />
   <java-symbol type="array" name="config_networkSupportedKeepaliveCount" />
@@ -2236,14 +2227,16 @@
   <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" />
   <java-symbol type="string" name="config_dreamsDefaultComponent" />
   <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
-  <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+  <java-symbol type="bool" name="config_dreamsOnlyEnabledForDockUser" />
+  <java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
+  <java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
   <java-symbol type="array" name="config_supportedDreamComplications" />
   <java-symbol type="array" name="config_disabledDreamComponents" />
   <java-symbol type="bool" name="config_dismissDreamOnActivityStart" />
   <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" />
   <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" />
   <java-symbol type="integer" name="config_minDreamOverlayDurationMs" />
-  <java-symbol type="string" name="config_loggable_dream_prefix" />
+  <java-symbol type="array" name="config_loggable_dream_prefixes" />
   <java-symbol type="string" name="config_dozeComponent" />
   <java-symbol type="string" name="enable_explore_by_touch_warning_title" />
   <java-symbol type="string" name="enable_explore_by_touch_warning_message" />
@@ -2262,6 +2255,7 @@
   <java-symbol type="string" name="config_customVpnAlwaysOnDisconnectedDialogComponent" />
   <java-symbol type="string" name="config_platformVpnConfirmDialogComponent" />
   <java-symbol type="string" name="config_carrierAppInstallDialogComponent" />
+  <java-symbol type="string" name="config_credentialManagerDialogComponent" />
   <java-symbol type="string" name="config_defaultNetworkScorerPackageName" />
   <java-symbol type="string" name="config_persistentDataPackageName" />
   <java-symbol type="string" name="config_deviceConfiguratorPackageName" />
@@ -2306,6 +2300,7 @@
   <java-symbol type="id" name="media_actions" />
 
   <java-symbol type="dimen" name="config_mediaMetadataBitmapMaxSize" />
+  <java-symbol type="array" name="config_fontManagerServiceCerts" />
 
     <!-- From SystemUI -->
   <java-symbol type="anim" name="push_down_in" />
@@ -2722,6 +2717,7 @@
   <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" />
   <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" />
   <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" />
+  <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" />
 
   <!-- Face config -->
   <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" />
@@ -3019,6 +3015,8 @@
   <java-symbol type="integer" name="config_cameraLiftTriggerSensorType" />
   <java-symbol type="string" name="config_cameraLiftTriggerSensorStringType" />
   <java-symbol type="bool" name="config_emergencyGestureEnabled" />
+  <java-symbol type="bool" name="config_defaultEmergencyGestureEnabled" />
+  <java-symbol type="bool" name="config_defaultEmergencyGestureSoundEnabled" />
   <java-symbol type="bool" name="config_volumeHushGestureEnabled" />
 
   <java-symbol type="drawable" name="platlogo_m" />
@@ -3442,7 +3440,7 @@
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayNominalWhite" />
   <java-symbol type="bool" name="config_displayWhiteBalanceLightModeAllowed" />
 
-  <!-- Default first user restrictions -->
+  <!-- Default user restrictions for the SYSTEM user -->
   <java-symbol type="array" name="config_defaultFirstUserRestrictions" />
 
   <java-symbol type="bool" name="config_permissionsIndividuallyControlled" />
@@ -3475,6 +3473,9 @@
   <!-- Captive Portal Login -->
   <java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" />
 
+  <!-- Dock Manager -->
+  <java-symbol type="string" name="config_defaultDockManagerPackageName" />
+
   <!-- Optional IPsec algorithms -->
   <java-symbol type="array" name="config_optionalIpSecAlgorithms" />
 
@@ -3724,7 +3725,6 @@
   <java-symbol type="integer" name="config_maxUiWidth" />
 
   <!-- system notification channels -->
-  <java-symbol type="string" name="notification_channel_virtual_keyboard" />
   <java-symbol type="string" name="notification_channel_physical_keyboard" />
   <java-symbol type="string" name="notification_channel_security" />
   <java-symbol type="string" name="notification_channel_car_mode" />
@@ -3936,17 +3936,6 @@
   <java-symbol type="string" name="harmful_app_warning_title" />
   <java-symbol type="layout" name="harmful_app_warning_dialog" />
 
-  <java-symbol type="string" name="log_access_confirmation_allow" />
-  <java-symbol type="string" name="log_access_confirmation_deny" />
-  <java-symbol type="string" name="log_access_confirmation_title" />
-  <java-symbol type="string" name="log_access_confirmation_body" />
-  <java-symbol type="string" name="log_access_confirmation_learn_more" />
-  <java-symbol type="layout" name="log_access_user_consent_dialog_permission" />
-  <java-symbol type="id" name="log_access_dialog_title" />
-  <java-symbol type="id" name="log_access_dialog_body" />
-  <java-symbol type="id" name="log_access_dialog_allow_button" />
-  <java-symbol type="id" name="log_access_dialog_deny_button" />
-
   <java-symbol type="string" name="config_defaultAssistantAccessComponent" />
 
   <java-symbol type="string" name="slices_permission_request" />
@@ -4865,6 +4854,7 @@
   <!-- For VirtualDeviceManager -->
   <java-symbol type="string" name="vdm_camera_access_denied" />
   <java-symbol type="string" name="vdm_secure_window" />
+  <java-symbol type="string" name="vdm_pip_blocked" />
 
   <java-symbol type="color" name="camera_privacy_light_day"/>
   <java-symbol type="color" name="camera_privacy_light_night"/>
diff --git a/core/res/res/xml/config_user_types.xml b/core/res/res/xml/config_user_types.xml
index 7663150..df6b7b2 100644
--- a/core/res/res/xml/config_user_types.xml
+++ b/core/res/res/xml/config_user_types.xml
@@ -83,6 +83,8 @@
 For profile and full users:
     default-restrictions (with values defined in UserRestrictionUtils.USER_RESTRICTIONS)
     enabled
+    user-properties
+    max-allowed
 For profile users only:
     max-allowed-per-parent
     icon-badge
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 113f45d..436f058 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -23,23 +23,32 @@
 
 android_test {
     name: "BroadcastRadioTests",
+    srcs: ["src/**/*.java"],
     privileged: true,
     certificate: "platform",
     // TODO(b/13282254): uncomment when b/13282254 is fixed
     // sdk_version: "current"
     platform_apis: true,
-    static_libs: [
-        "compatibility-device-util-axt",
-        "androidx.test.rules",
-        "testng",
-        "services.core",
-    ],
-    libs: ["android.test.base"],
-    srcs: ["src/**/*.java"],
     dex_preopt: {
         enabled: false,
     },
     optimize: {
         enabled: false,
     },
+    static_libs: [
+        "services.core",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "testng",
+        "mockito-target-extended",
+    ],
+    libs: ["android.test.base"],
+    test_suites: [
+        "general-tests",
+    ],
+    // mockito-target-inline dependency
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
 }
diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml
index ce12cc9..869b484 100644
--- a/core/tests/BroadcastRadioTests/AndroidManifest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/core/tests/BroadcastRadioTests/AndroidTest.xml b/core/tests/BroadcastRadioTests/AndroidTest.xml
new file mode 100644
index 0000000..ed88537
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+<configuration description="Runs Broadcast Radio Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="BroadcastRadioTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="BroadcastRadioTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.hardware.radio.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 11eb158..3f35e99 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -33,11 +33,9 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
-import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
@@ -47,6 +45,7 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -56,8 +55,7 @@
 /**
  * A test for broadcast radio API.
  */
-@RunWith(AndroidJUnit4.class)
-@MediumTest
+@RunWith(MockitoJUnitRunner.class)
 public class RadioTunerTest {
     private static final String TAG = "BroadcastRadioTests.RadioTuner";
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
new file mode 100644
index 0000000..2fa3f876
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.junit.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public final class DefaultRadioTunerTest {
+
+    private static final RadioTuner DEFAULT_RADIO_TUNER = new RadioTuner() {
+        @Override
+        public void close() {}
+
+        @Override
+        public int setConfiguration(RadioManager.BandConfig config) {
+            return 0;
+        }
+
+        @Override
+        public int getConfiguration(RadioManager.BandConfig[] config) {
+            return 0;
+        }
+
+        @Override
+        public int setMute(boolean mute) {
+            return 0;
+        }
+
+        @Override
+        public boolean getMute() {
+            return false;
+        }
+
+        @Override
+        public int step(int direction, boolean skipSubChannel) {
+            return 0;
+        }
+
+        @Override
+        public int scan(int direction, boolean skipSubChannel) {
+            return 0;
+        }
+
+        @Override
+        public int tune(int channel, int subChannel) {
+            return 0;
+        }
+
+        @Override
+        public void tune(@NonNull ProgramSelector selector) {}
+
+        @Override
+        public int cancel() {
+            return 0;
+        }
+
+        @Override
+        public void cancelAnnouncement() {}
+
+        @Override
+        public int getProgramInformation(RadioManager.ProgramInfo[] info) {
+            return 0;
+        }
+
+        @Nullable
+        @Override
+        public Bitmap getMetadataImage(int id) {
+            return null;
+        }
+
+        @Override
+        public boolean startBackgroundScan() {
+            return false;
+        }
+
+        @NonNull
+        @Override
+        public List<RadioManager.ProgramInfo> getProgramList(
+                @Nullable Map<String, String> vendorFilter) {
+            return new ArrayList<>();
+        }
+
+        @Override
+        public boolean isAnalogForced() {
+            return false;
+        }
+
+        @Override
+        public void setAnalogForced(boolean isForced) {}
+
+        @Override
+        public boolean isAntennaConnected() {
+            return false;
+        }
+
+        @Override
+        public boolean hasControl() {
+            return false;
+        }
+    };
+
+    @Test
+    public void getDynamicProgramList_forRadioTuner_returnsNull() {
+        assertWithMessage("Dynamic program list obtained from default radio tuner")
+                .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
+    }
+
+    @Test
+    public void isConfigFlagSupported_forRadioTuner_throwsException() {
+        assertWithMessage("Dynamic program list obtained from default radio tuner")
+                .that(DEFAULT_RADIO_TUNER.isConfigFlagSupported(/* flag= */ 1)).isFalse();
+    }
+
+    @Test
+    public void isConfigFlagSet_forRadioTuner_throwsException() {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            DEFAULT_RADIO_TUNER.isConfigFlagSet(/* flag= */ 1);
+        });
+    }
+
+    @Test
+    public void setConfigFlag_forRadioTuner_throwsException() {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            DEFAULT_RADIO_TUNER.setConfigFlag(/* flag= */ 1, /* value= */ false);
+        });
+    }
+
+    @Test
+    public void setParameters_forRadioTuner_throwsException() {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            DEFAULT_RADIO_TUNER.setParameters(Map.of("testKey", "testValue"));
+        });
+    }
+
+    @Test
+    public void getParameters_forRadioTuner_throwsException() {
+        assertThrows(UnsupportedOperationException.class, () -> {
+            DEFAULT_RADIO_TUNER.getParameters(List.of("testKey"));
+        });
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
new file mode 100644
index 0000000..2b9de18
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
@@ -0,0 +1,511 @@
+/*
+ * 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.junit.Assert.assertThrows;
+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.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.util.ArraySet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.verification.VerificationWithTimeout;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class ProgramListTest {
+
+    private static final int CREATOR_ARRAY_SIZE = 3;
+    private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 500);
+
+    private static final boolean IS_PURGE = false;
+    private static final boolean IS_COMPLETE = true;
+
+    private static final boolean INCLUDE_CATEGORIES = true;
+    private static final boolean EXCLUDE_MODIFICATIONS = false;
+
+    private static final ProgramSelector.Identifier FM_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+                    /* value= */ 94300);
+    private static final ProgramSelector.Identifier RDS_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000111);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1013);
+    private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo(
+            createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER));
+    private static final RadioManager.ProgramInfo RDS_PROGRAM_INFO = createFmProgramInfo(
+            createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, RDS_IDENTIFIER));
+
+    private static final Set<Integer> FILTER_IDENTIFIER_TYPES = Set.of(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
+    private static final Set<ProgramSelector.Identifier> FILTER_IDENTIFIERS = Set.of(FM_IDENTIFIER);
+
+    private static final ProgramList.Chunk FM_RDS_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE,
+            IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
+            Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+    private static final ProgramList.Chunk FM_ADD_INCOMPLETE_CHUNK = new ProgramList.Chunk(IS_PURGE,
+            /* complete= */ false, Set.of(FM_PROGRAM_INFO), new ArraySet<>());
+    private static final ProgramList.Filter TEST_FILTER = new ProgramList.Filter(
+            FILTER_IDENTIFIER_TYPES, FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+    private static final Map<String, String> VENDOR_FILTER = Map.of("testVendorKey1",
+            "testVendorValue1", "testVendorKey2", "testVendorValue2");
+
+    private final Executor mExecutor = new Executor() {
+        @Override
+        public void execute(Runnable command) {
+            command.run();
+        }
+    };
+
+    private RadioTuner mRadioTuner;
+    private ITunerCallback mTunerCallback;
+    private ProgramList mProgramList;
+
+    private ProgramList.ListCallback[] mListCallbackMocks;
+    private ProgramList.OnCompleteListener[] mOnCompleteListenerMocks;
+    @Mock
+    private IRadioService mRadioServiceMock;
+    @Mock
+    private Context mContextMock;
+    @Mock
+    private ITuner mTunerMock;
+    @Mock
+    private RadioTuner.Callback mTunerCallbackMock;
+
+    @Test
+    public void getIdentifierTypes_forFilter() {
+        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Filtered identifier types").that(filter.getIdentifierTypes())
+                .containsExactlyElementsIn(FILTER_IDENTIFIER_TYPES);
+    }
+
+    @Test
+    public void getIdentifiers_forFilter() {
+        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Filtered identifiers").that(filter.getIdentifiers())
+                .containsExactlyElementsIn(FILTER_IDENTIFIERS);
+    }
+
+    @Test
+    public void areCategoriesIncluded_forFilter() {
+        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Filter including categories")
+                .that(filter.areCategoriesIncluded()).isEqualTo(INCLUDE_CATEGORIES);
+    }
+
+    @Test
+    public void areModificationsExcluded_forFilter() {
+        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Filter excluding modifications")
+                .that(filter.areModificationsExcluded()).isEqualTo(EXCLUDE_MODIFICATIONS);
+    }
+
+    @Test
+    public void getVendorFilter_forFilterWithoutVendorFilter_returnsNull() {
+        ProgramList.Filter filter = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Filter vendor obtained from filter without vendor filter")
+                .that(filter.getVendorFilter()).isNull();
+    }
+
+    @Test
+    public void getVendorFilter_forFilterWithVendorFilter() {
+        ProgramList.Filter vendorFilter = new ProgramList.Filter(VENDOR_FILTER);
+
+        assertWithMessage("Filter vendor obtained from filter with vendor filter")
+                .that(vendorFilter.getVendorFilter()).isEqualTo(VENDOR_FILTER);
+    }
+
+    @Test
+    public void describeContents_forFilter() {
+        assertWithMessage("Filter contents").that(TEST_FILTER.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void hashCode_withTheSameFilters_equals() {
+        ProgramList.Filter filterCompared = new ProgramList.Filter(FILTER_IDENTIFIER_TYPES,
+                FILTER_IDENTIFIERS, INCLUDE_CATEGORIES, EXCLUDE_MODIFICATIONS);
+
+        assertWithMessage("Hash code of the same filter")
+                .that(filterCompared.hashCode()).isEqualTo(TEST_FILTER.hashCode());
+    }
+
+    @Test
+    public void hashCode_withDifferentFilters_notEquals() {
+        ProgramList.Filter filterCompared = new ProgramList.Filter();
+
+        assertWithMessage("Hash code of the different filter")
+                .that(filterCompared.hashCode()).isNotEqualTo(TEST_FILTER.hashCode());
+    }
+
+    @Test
+    public void writeToParcel_forFilter() {
+        Parcel parcel = Parcel.obtain();
+
+        TEST_FILTER.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        ProgramList.Filter filterFromParcel =
+                ProgramList.Filter.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Filter created from parcel")
+                .that(filterFromParcel).isEqualTo(TEST_FILTER);
+    }
+
+    @Test
+    public void newArray_forFilterCreator() {
+        ProgramList.Filter[] filters = ProgramList.Filter.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Program filters").that(filters).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void isPurge_forChunk() {
+        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
+                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
+                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+
+        assertWithMessage("Puring chunk").that(chunk.isPurge()).isEqualTo(IS_PURGE);
+    }
+
+    @Test
+    public void isComplete_forChunk() {
+        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
+                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
+                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+
+        assertWithMessage("Complete chunk").that(chunk.isComplete()).isEqualTo(IS_COMPLETE);
+    }
+
+    @Test
+    public void getModified_forChunk() {
+        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
+                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
+                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+
+        assertWithMessage("Modified program info in chunk")
+                .that(chunk.getModified()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
+    }
+
+    @Test
+    public void getRemoved_forChunk() {
+        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
+                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
+                Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+
+        assertWithMessage("Removed program identifiers in chunk").that(chunk.getRemoved())
+                .containsExactly(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER);
+    }
+
+    @Test
+    public void describeContents_forChunk() {
+        assertWithMessage("Chunk contents").that(FM_RDS_ADD_CHUNK.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forChunk() {
+        Parcel parcel = Parcel.obtain();
+
+        FM_RDS_ADD_CHUNK.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        ProgramList.Chunk chunkFromParcel =
+                ProgramList.Chunk.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Chunk created from parcel")
+                .that(chunkFromParcel).isEqualTo(FM_RDS_ADD_CHUNK);
+    }
+
+    @Test
+    public void newArray_forChunkCreator() {
+        ProgramList.Chunk[] chunks = ProgramList.Chunk.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Chunks").that(chunks).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void getDynamicProgramList_forTunerAdapter() throws Exception {
+        createRadioTuner();
+
+        mRadioTuner.getDynamicProgramList(TEST_FILTER);
+
+        verify(mTunerMock).startProgramListUpdates(TEST_FILTER);
+    }
+
+    @Test
+    public void getDynamicProgramList_forTunerAdapterWithServiceDied_throwsException()
+            throws Exception {
+        createRadioTuner();
+        doThrow(new RemoteException()).when(mTunerMock).startProgramListUpdates(any());
+
+        RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+            mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        });
+
+        assertWithMessage("Exception for radio HAL client service died")
+                .that(thrown).hasMessageThat().contains("Service died");
+    }
+
+    @Test
+    public void onProgramListUpdated_withNewIdsAdded_invokesMockedCallbacks() throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+        addOnCompleteListeners(/* numListeners= */ 1);
+
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(RDS_IDENTIFIER);
+        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT).onComplete();
+        assertWithMessage("Program info in program list after adding FM and RDS info")
+                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
+    }
+
+    @Test
+    public void onProgramListUpdated_withIdsRemoved_invokesMockedCallbacks() throws Exception {
+        ProgramList.Chunk fmRemovedChunk = new ProgramList.Chunk(/* purge= */ false,
+                /* complete= */ false, new ArraySet<>(), Set.of(FM_IDENTIFIER));
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        mTunerCallback.onProgramListUpdated(fmRemovedChunk);
+
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
+        assertWithMessage("Program info in program list after removing FM id")
+                .that(mProgramList.toList()).containsExactly(RDS_PROGRAM_INFO);
+        assertWithMessage("Program info FM identifier")
+                .that(mProgramList.get(RDS_IDENTIFIER)).isEqualTo(RDS_PROGRAM_INFO);
+    }
+
+    @Test
+    public void onProgramListUpdated_withIncompleteChunk_notInvokesOnCompleteListener()
+            throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        addOnCompleteListeners(/* numListeners= */ 1);
+
+        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);
+
+        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT.times(0)).onComplete();
+    }
+
+    @Test
+    public void onProgramListUpdated_withPurgeChunk() throws Exception {
+        ProgramList.Chunk purgeChunk = new ProgramList.Chunk(/* purge= */ true,
+                /* complete= */ true, new ArraySet<>(), new ArraySet<>());
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        mTunerCallback.onProgramListUpdated(purgeChunk);
+
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(RDS_IDENTIFIER);
+        assertWithMessage("Program list after purge chunk applied")
+                .that(mProgramList.toList()).isEmpty();
+    }
+
+    @Test
+    public void onItemChanged_forListCallbackRegisteredWithExecutor_invokesWhenIdAdded()
+            throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        ProgramList.ListCallback listCallbackMock = mock(ProgramList.ListCallback.class);
+        mProgramList.registerListCallback(mExecutor, listCallbackMock);
+
+        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);
+
+        verify(listCallbackMock, CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
+    }
+
+    @Test
+    public void onItemRemoved_forListCallbackRegisteredWithExecutor_invokesWhenIdRemoved()
+            throws Exception {
+        ProgramList.Chunk purgeChunk = new ProgramList.Chunk(/* purge= */ true,
+                /* complete= */ true, new ArraySet<>(), new ArraySet<>());
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        ProgramList.ListCallback listCallbackMock = mock(ProgramList.ListCallback.class);
+        mProgramList.registerListCallback(mExecutor, listCallbackMock);
+        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);
+
+        mTunerCallback.onProgramListUpdated(purgeChunk);
+
+        verify(listCallbackMock, CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
+    }
+
+    @Test
+    public void onProgramListUpdated_withMultipleListCallBacks() throws Exception {
+        int numCallbacks = 3;
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(numCallbacks);
+
+        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);
+
+        for (int index = 0; index < numCallbacks; index++) {
+            verify(mListCallbackMocks[index], CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
+        }
+    }
+
+    @Test
+    public void unregisterListCallback_withProgramUpdated_notInvokesCallback() throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+
+        mProgramList.unregisterListCallback(mListCallbackMocks[0]);
+        mTunerCallback.onProgramListUpdated(FM_ADD_INCOMPLETE_CHUNK);
+
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onItemChanged(any());
+    }
+
+    @Test
+    public void addOnCompleteListener_withExecutor() throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        ProgramList.OnCompleteListener onCompleteListenerMock =
+                mock(ProgramList.OnCompleteListener.class);
+
+        mProgramList.addOnCompleteListener(mExecutor, onCompleteListenerMock);
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        verify(onCompleteListenerMock, CALLBACK_TIMEOUT).onComplete();
+    }
+
+    @Test
+    public void onProgramListUpdated_withMultipleOnCompleteListeners() throws Exception {
+        int numListeners = 3;
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        addOnCompleteListeners(numListeners);
+
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        for (int index = 0; index < numListeners; index++) {
+            verify(mOnCompleteListenerMocks[index], CALLBACK_TIMEOUT).onComplete();
+        }
+    }
+
+    @Test
+    public void removeOnCompleteListener_withProgramUpdated_notInvokesListener() throws Exception {
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        addOnCompleteListeners(/* numListeners= */ 1);
+
+        mProgramList.removeOnCompleteListener(mOnCompleteListenerMocks[0]);
+        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+
+        verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT.times(0)).onComplete();
+    }
+
+    @Test
+    public void close_forProgramList_invokesStopProgramListUpdates() throws Exception {
+        createRadioTuner();
+        ProgramList programList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+
+        programList.close();
+
+        verify(mTunerMock, CALLBACK_TIMEOUT).stopProgramListUpdates();
+    }
+
+    private static ProgramSelector createProgramSelector(int programType,
+            ProgramSelector.Identifier identifier) {
+        return new ProgramSelector(programType, identifier, /* secondaryIds= */ null,
+                /* vendorIds= */ null);
+    }
+
+    private static RadioManager.ProgramInfo createFmProgramInfo(ProgramSelector selector) {
+        return new RadioManager.ProgramInfo(selector, selector.getPrimaryId(),
+                selector.getPrimaryId(), /* relatedContents= */ null, /* infoFlags= */ 0,
+                /* signalQuality= */ 1, new RadioMetadata.Builder().build(),
+                /* vendorInfo= */ null);
+    }
+
+    private void createRadioTuner() throws Exception {
+        RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
+        RadioManager.BandConfig band = 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));
+
+        doAnswer(invocation -> {
+            mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
+            return mTunerMock;
+        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+
+        mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, band,
+                /* withAudio= */ true, mTunerCallbackMock, /* handler= */ null);
+    }
+
+    private void registerListCallbacks(int numCallbacks) {
+        mListCallbackMocks = new ProgramList.ListCallback[numCallbacks];
+        for (int index = 0; index < numCallbacks; index++) {
+            mListCallbackMocks[index] = mock(ProgramList.ListCallback.class);
+            mProgramList.registerListCallback(mListCallbackMocks[index]);
+        }
+    }
+
+    private void addOnCompleteListeners(int numListeners) {
+        mOnCompleteListenerMocks = new ProgramList.OnCompleteListener[numListeners];
+        for (int index = 0; index < numListeners; index++) {
+            mOnCompleteListenerMocks[index] = mock(ProgramList.OnCompleteListener.class);
+            mProgramList.addOnCompleteListener(mOnCompleteListenerMocks[index]);
+        }
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
index 57b9cb1..5bd018b 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
@@ -23,11 +23,13 @@
 import android.annotation.Nullable;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.os.Parcel;
 
 import org.junit.Test;
 
 public final class ProgramSelectorTest {
 
+    private static final int CREATOR_ARRAY_SIZE = 2;
     private static final int FM_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_FM;
     private static final int DAB_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_DAB;
     private static final long FM_FREQUENCY = 88500;
@@ -97,6 +99,33 @@
     }
 
     @Test
+    public void describeContents_forIdentifier() {
+        assertWithMessage("FM identifier contents")
+                .that(FM_IDENTIFIER.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forIdentifierCreator() {
+        ProgramSelector.Identifier[] identifiers =
+                ProgramSelector.Identifier.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Identifiers").that(identifiers).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void writeToParcel_forIdentifier() {
+        Parcel parcel = Parcel.obtain();
+
+        FM_IDENTIFIER.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        ProgramSelector.Identifier identifierFromParcel =
+                ProgramSelector.Identifier.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Identifier created from parcel")
+                .that(identifierFromParcel).isEqualTo(FM_IDENTIFIER);
+    }
+
+    @Test
     public void getProgramType() {
         ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
 
@@ -394,6 +423,34 @@
                 .that(selector1.strictEquals(selector2)).isTrue();
     }
 
+    @Test
+    public void describeContents_forProgramSelector() {
+        assertWithMessage("FM selector contents")
+                .that(getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null)
+                        .describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forProgramSelectorCreator() {
+        ProgramSelector[] programSelectors = ProgramSelector.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Program selectors").that(programSelectors).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void writeToParcel_forProgramSelector() {
+        ProgramSelector selectorExpected =
+                getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+        Parcel parcel = Parcel.obtain();
+
+        selectorExpected.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        ProgramSelector selectorFromParcel = ProgramSelector.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Program selector created from parcel")
+                .that(selectorFromParcel).isEqualTo(selectorExpected);
+    }
+
     private ProgramSelector getFmSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
             @Nullable long[] vendorIds) {
         return new ProgramSelector(FM_PROGRAM_TYPE, FM_IDENTIFIER, secondaryIds, vendorIds);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
new file mode 100644
index 0000000..6e1bb4b4
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.junit.Assert.assertThrows;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.ProgramSelector;
+import android.os.Parcel;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public final class RadioAnnouncementTest {
+    private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 90500);
+    private static final ProgramSelector FM_PROGRAM_SELECTOR = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, /* secondaryIds= */ null,
+            /* vendorIds= */ null);
+    private static final int TRAFFIC_ANNOUNCEMENT_TYPE = Announcement.TYPE_TRAFFIC;
+    private static final Map<String, String> VENDOR_INFO = createVendorInfo();
+    private static final Announcement TEST_ANNOUNCEMENT =
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+
+    @Test
+    public void constructor_withNullSelector_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(/* selector= */ null, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+        });
+
+        assertWithMessage("Exception for null program selector in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Program selector cannot be null");
+    }
+
+    @Test
+    public void constructor_withNullVendorInfo_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE,
+                    /* vendorInfo= */ null);
+        });
+
+        assertWithMessage("Exception for null vendor info in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Vendor info cannot be null");
+    }
+
+    @Test
+    public void getSelector() {
+        assertWithMessage("Radio announcement selector")
+                .that(TEST_ANNOUNCEMENT.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR);
+    }
+
+    @Test
+    public void getType() {
+        assertWithMessage("Radio announcement type")
+                .that(TEST_ANNOUNCEMENT.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE);
+    }
+
+    @Test
+    public void getVendorInfo() {
+        assertWithMessage("Radio announcement vendor info")
+                .that(TEST_ANNOUNCEMENT.getVendorInfo()).isEqualTo(VENDOR_INFO);
+    }
+
+    private static Map<String, String> createVendorInfo() {
+        Map<String, String> vendorInfo = new ArrayMap<>();
+        vendorInfo.put("vendorKeyMock", "vendorValueMock");
+        return vendorInfo;
+    }
+
+    @Test
+    public void describeContents_forAnnouncement() {
+        assertWithMessage("Radio announcement contents")
+                .that(TEST_ANNOUNCEMENT.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forAnnouncementCreator() {
+        int sizeExpected = 2;
+
+        Announcement[] announcements = Announcement.CREATOR.newArray(sizeExpected);
+
+        assertWithMessage("Announcements").that(announcements).hasLength(sizeExpected);
+    }
+
+    @Test
+    public void writeToParcel_forAnnouncement() {
+        Parcel parcel = Parcel.obtain();
+
+        TEST_ANNOUNCEMENT.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        Announcement announcementFromParcel = Announcement.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Selector of announcement created from parcel")
+                .that(announcementFromParcel.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR);
+        assertWithMessage("Type of announcement created from parcel")
+                .that(announcementFromParcel.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE);
+        assertWithMessage("Vendor info of announcement created from parcel")
+                .that(announcementFromParcel.getVendorInfo()).isEqualTo(VENDOR_INFO);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
new file mode 100644
index 0000000..365b901
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -0,0 +1,1083 @@
+/*
+ * 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.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class RadioManagerTest {
+
+    private static final int REGION = RadioManager.REGION_ITU_2;
+    private static final int FM_LOWER_LIMIT = 87500;
+    private static final int FM_UPPER_LIMIT = 108000;
+    private static final int FM_SPACING = 200;
+    private static final int AM_LOWER_LIMIT = 540;
+    private static final int AM_UPPER_LIMIT = 1700;
+    private static final int AM_SPACING = 10;
+    private static final boolean STEREO_SUPPORTED = true;
+    private static final boolean RDS_SUPPORTED = true;
+    private static final boolean TA_SUPPORTED = false;
+    private static final boolean AF_SUPPORTED = false;
+    private static final boolean EA_SUPPORTED = false;
+
+    private static final int PROPERTIES_ID = 10;
+    private static final String SERVICE_NAME = "ServiceNameMock";
+    private static final int CLASS_ID = RadioManager.CLASS_AM_FM;
+    private static final String IMPLEMENTOR = "ImplementorMock";
+    private static final String PRODUCT = "ProductMock";
+    private static final String VERSION = "VersionMock";
+    private static final String SERIAL = "SerialMock";
+    private static final int NUM_TUNERS = 1;
+    private static final int NUM_AUDIO_SOURCES = 1;
+    private static final boolean IS_INITIALIZATION_REQUIRED = false;
+    private static final boolean IS_CAPTURE_SUPPORTED = false;
+    private static final boolean IS_BG_SCAN_SUPPORTED = true;
+    private static final int[] SUPPORTED_PROGRAM_TYPES = new int[]{
+            ProgramSelector.PROGRAM_TYPE_AM, ProgramSelector.PROGRAM_TYPE_FM};
+    private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI};
+
+    private static final int CREATOR_ARRAY_SIZE = 3;
+
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
+            createFmBandDescriptor();
+    private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR =
+            createAmBandDescriptor();
+    private static final RadioManager.FmBandConfig FM_BAND_CONFIG = createFmBandConfig();
+    private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig();
+    private static final RadioManager.ModuleProperties AMFM_PROPERTIES =
+            createAmFmProperties(/* dabFrequencyTable= */ null);
+
+    /**
+     * Info flags with live, tuned and stereo enabled
+     */
+    private static final int INFO_FLAGS = 0b110001;
+    private static final int SIGNAL_QUALITY = 2;
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000111);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000113);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1013);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 95500);
+    private static final ProgramSelector DAB_SELECTOR =
+            new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                    new ProgramSelector.Identifier[]{
+                            DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER},
+                    /* vendorIds= */ null);
+    private static final RadioMetadata METADATA = createMetadata();
+    private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO =
+            createDabProgramInfo(DAB_SELECTOR);
+
+    private static final int EVENT_ANNOUNCEMENT_TYPE = Announcement.TYPE_EVENT;
+    private static final List<Announcement> TEST_ANNOUNCEMENT_LIST = Arrays.asList(
+            new Announcement(DAB_SELECTOR, EVENT_ANNOUNCEMENT_TYPE,
+                    /* vendorInfo= */ new ArrayMap<>()));
+
+    private RadioManager mRadioManager;
+
+    @Mock
+    private IRadioService mRadioServiceMock;
+    @Mock
+    private Context mContextMock;
+    @Mock
+    private RadioTuner.Callback mCallbackMock;
+    @Mock
+    private Announcement.OnListUpdatedListener mEventListener;
+    @Mock
+    private ICloseHandle mCloseHandleMock;
+
+    @Test
+    public void getType_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor type")
+                .that(bandDescriptor.getType()).isEqualTo(RadioManager.BAND_AM);
+    }
+
+    @Test
+    public void getRegion_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor region")
+                .that(bandDescriptor.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor lower limit")
+                .that(bandDescriptor.getLowerLimit()).isEqualTo(FM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor upper limit")
+                .that(bandDescriptor.getUpperLimit()).isEqualTo(AM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor spacing")
+                .that(bandDescriptor.getSpacing()).isEqualTo(AM_SPACING);
+    }
+
+    @Test
+    public void describeContents_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("Band Descriptor contents")
+                .that(bandDescriptor.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forBandDescriptorCreator() {
+        RadioManager.BandDescriptor[] bandDescriptors =
+                RadioManager.BandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Band Descriptors").that(bandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void isAmBand_forAmBandDescriptor_returnsTrue() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an AM band")
+                .that(bandDescriptor.isAmBand()).isTrue();
+    }
+
+    @Test
+    public void isFmBand_forAmBandDescriptor_returnsFalse() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an FM band")
+                .that(bandDescriptor.isFmBand()).isFalse();
+    }
+
+    @Test
+    public void isStereoSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor stereo")
+                .that(FM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void isRdsSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor RDS or RBDS")
+                .that(FM_BAND_DESCRIPTOR.isRdsSupported()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void isTaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor traffic announcement")
+                .that(FM_BAND_DESCRIPTOR.isTaSupported()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void isAfSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor alternate frequency")
+                .that(FM_BAND_DESCRIPTOR.isAfSupported()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void isEaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor emergency announcement")
+                .that(FM_BAND_DESCRIPTOR.isEaSupported()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void describeContents_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor contents")
+                .that(FM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forFmBandDescriptor() {
+        Parcel parcel = Parcel.obtain();
+
+        FM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.FmBandDescriptor fmBandDescriptorFromParcel =
+                RadioManager.FmBandDescriptor.CREATOR.createFromParcel(parcel);
+        assertWithMessage("FM Band Descriptor created from parcel")
+                .that(fmBandDescriptorFromParcel).isEqualTo(FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void newArray_forFmBandDescriptorCreator() {
+        RadioManager.FmBandDescriptor[] fmBandDescriptors =
+                RadioManager.FmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("FM Band Descriptors")
+                .that(fmBandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void isStereoSupported_forAmBandDescriptor() {
+        assertWithMessage("AM Band Descriptor stereo")
+                .that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void describeContents_forAmBandDescriptor() {
+        assertWithMessage("AM Band Descriptor contents")
+                .that(AM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forAmBandDescriptor() {
+        Parcel parcel = Parcel.obtain();
+
+        AM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.AmBandDescriptor amBandDescriptorFromParcel =
+                RadioManager.AmBandDescriptor.CREATOR.createFromParcel(parcel);
+        assertWithMessage("FM Band Descriptor created from parcel")
+                .that(amBandDescriptorFromParcel).isEqualTo(AM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void newArray_forAmBandDescriptorCreator() {
+        RadioManager.AmBandDescriptor[] amBandDescriptors =
+                RadioManager.AmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("AM Band Descriptors")
+                .that(amBandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void equals_withSameFmBandDescriptors_returnsTrue() {
+        RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor();
+
+        assertWithMessage("The same FM Band Descriptor")
+                .that(FM_BAND_DESCRIPTOR).isEqualTo(fmBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withSameAmBandDescriptors_returnsTrue() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor();
+
+        assertWithMessage("The same AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmBandDescriptorsOfDifferentUpperLimits_returnsFalse() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared =
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT + AM_SPACING, AM_SPACING, STEREO_SUPPORTED);
+
+        assertWithMessage("AM Band Descriptor of different upper limit")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmAndFmBandDescriptors_returnsFalse() {
+        assertWithMessage("AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void hashCode_withSameFmBandDescriptors_equals() {
+        RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor();
+
+        assertWithMessage("Hash code of the same FM Band Descriptor")
+                .that(fmBandDescriptorCompared.hashCode()).isEqualTo(FM_BAND_DESCRIPTOR.hashCode());
+    }
+
+    @Test
+    public void hashCode_withSameAmBandDescriptors_equals() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor();
+
+        assertWithMessage("Hash code of the same AM Band Descriptor")
+                .that(amBandDescriptorCompared.hashCode()).isEqualTo(AM_BAND_DESCRIPTOR.hashCode());
+    }
+
+    @Test
+    public void hashCode_withFmBandDescriptorsOfDifferentAfSupports_notEquals() {
+        RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor(
+                REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING,
+                STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, !AF_SUPPORTED, EA_SUPPORTED);
+
+        assertWithMessage("Hash code of FM Band Descriptor of different spacing")
+                .that(fmBandDescriptorCompared.hashCode())
+                .isNotEqualTo(FM_BAND_DESCRIPTOR.hashCode());
+    }
+
+    @Test
+    public void hashCode_withAmBandDescriptorsOfDifferentSpacings_notEquals() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared =
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT, AM_SPACING * 2, STEREO_SUPPORTED);
+
+        assertWithMessage("Hash code of AM Band Descriptor of different spacing")
+                .that(amBandDescriptorCompared.hashCode())
+                .isNotEqualTo(AM_BAND_DESCRIPTOR.hashCode());
+    }
+
+    @Test
+    public void getType_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config type")
+                .that(fmBandConfig.getType()).isEqualTo(RadioManager.BAND_FM);
+    }
+
+    @Test
+    public void getRegion_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config region")
+                .that(amBandConfig.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config lower limit")
+                .that(amBandConfig.getLowerLimit()).isEqualTo(AM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config upper limit")
+                .that(fmBandConfig.getUpperLimit()).isEqualTo(FM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config spacing")
+                .that(fmBandConfig.getSpacing()).isEqualTo(FM_SPACING);
+    }
+
+    @Test
+    public void describeContents_forBandConfig() {
+        RadioManager.BandConfig bandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config contents")
+                .that(bandConfig.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forBandConfigCreator() {
+        RadioManager.BandConfig[] bandConfigs =
+                RadioManager.BandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Band Configs").that(bandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void getStereo_forFmBandConfig() {
+        assertWithMessage("FM Band Config stereo")
+                .that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void getRds_forFmBandConfig() {
+        assertWithMessage("FM Band Config RDS or RBDS")
+                .that(FM_BAND_CONFIG.getRds()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void getTa_forFmBandConfig() {
+        assertWithMessage("FM Band Config traffic announcement")
+                .that(FM_BAND_CONFIG.getTa()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void getAf_forFmBandConfig() {
+        assertWithMessage("FM Band Config alternate frequency")
+                .that(FM_BAND_CONFIG.getAf()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void getEa_forFmBandConfig() {
+        assertWithMessage("FM Band Config emergency Announcement")
+                .that(FM_BAND_CONFIG.getEa()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void describeContents_forFmBandConfig() {
+        assertWithMessage("FM Band Config contents")
+                .that(FM_BAND_CONFIG.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forFmBandConfig() {
+        Parcel parcel = Parcel.obtain();
+
+        FM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.FmBandConfig fmBandConfigFromParcel =
+                RadioManager.FmBandConfig.CREATOR.createFromParcel(parcel);
+        assertWithMessage("FM Band Config created from parcel")
+                .that(fmBandConfigFromParcel).isEqualTo(FM_BAND_CONFIG);
+    }
+
+    @Test
+    public void newArray_forFmBandConfigCreator() {
+        RadioManager.FmBandConfig[] fmBandConfigs =
+                RadioManager.FmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("FM Band Configs").that(fmBandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void getStereo_forAmBandConfig() {
+        assertWithMessage("AM Band Config stereo")
+                .that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void describeContents_forAmBandConfig() {
+        assertWithMessage("AM Band Config contents")
+                .that(AM_BAND_CONFIG.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forAmBandConfig() {
+        Parcel parcel = Parcel.obtain();
+
+        AM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.AmBandConfig amBandConfigFromParcel =
+                RadioManager.AmBandConfig.CREATOR.createFromParcel(parcel);
+        assertWithMessage("AM Band Config created from parcel")
+                .that(amBandConfigFromParcel).isEqualTo(AM_BAND_CONFIG);
+    }
+
+    @Test
+    public void newArray_forAmBandConfigCreator() {
+        RadioManager.AmBandConfig[] amBandConfigs =
+                RadioManager.AmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("AM Band Configs").that(amBandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void equals_withSameFmBandConfigs_returnsTrue() {
+        RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
+
+        assertWithMessage("The same FM Band Config")
+                .that(FM_BAND_CONFIG).isEqualTo(fmBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withFmBandConfigsOfDifferentAfs_returnsFalse() {
+        RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder(
+                createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED)
+                .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED);
+        RadioManager.FmBandConfig fmBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("FM Band Config of different af value")
+                .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigFromBuilder);
+    }
+
+    @Test
+    public void equals_withFmAndAmBandConfigs_returnsFalse() {
+        assertWithMessage("FM Band Config")
+                .that(FM_BAND_CONFIG).isNotEqualTo(AM_BAND_CONFIG);
+    }
+
+    @Test
+    public void equals_withSameAmBandConfigs_returnsTrue() {
+        RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig();
+
+        assertWithMessage("The same AM Band Config")
+                .that(AM_BAND_CONFIG).isEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentTypes_returnsFalse() {
+        RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig(
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM_HD, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED));
+
+        assertWithMessage("AM Band Config of different type")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentStereoValues_returnsFalse() {
+        RadioManager.AmBandConfig.Builder builder = new RadioManager.AmBandConfig.Builder(
+                createAmBandDescriptor()).setStereo(!STEREO_SUPPORTED);
+        RadioManager.AmBandConfig amBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("AM Band Config of different stereo value")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigFromBuilder);
+    }
+
+    @Test
+    public void hashCode_withSameFmBandConfigs_equals() {
+        RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
+
+        assertWithMessage("Hash code of the same FM Band Config")
+                .that(FM_BAND_CONFIG.hashCode()).isEqualTo(fmBandConfigCompared.hashCode());
+    }
+
+    @Test
+    public void hashCode_withSameAmBandConfigs_equals() {
+        RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig();
+
+        assertWithMessage("Hash code of the same AM Band Config")
+                .that(amBandConfigCompared.hashCode()).isEqualTo(AM_BAND_CONFIG.hashCode());
+    }
+
+    @Test
+    public void hashCode_withFmBandConfigsOfDifferentTypes_notEquals() {
+        RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig(
+                new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM_HD, FM_LOWER_LIMIT,
+                        FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
+                        AF_SUPPORTED, EA_SUPPORTED));
+
+        assertWithMessage("Hash code of FM Band Config with different type")
+                .that(fmBandConfigCompared.hashCode()).isNotEqualTo(FM_BAND_CONFIG.hashCode());
+    }
+
+    @Test
+    public void hashCode_withAmBandConfigsOfDifferentStereoSupports_notEquals() {
+        RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig(
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT, AM_SPACING, !STEREO_SUPPORTED));
+
+        assertWithMessage("Hash code of AM Band Config with different stereo support")
+                .that(amBandConfigCompared.hashCode()).isNotEqualTo(AM_BAND_CONFIG.hashCode());
+    }
+
+    @Test
+    public void getId_forModuleProperties() {
+        assertWithMessage("Properties id")
+                .that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID);
+    }
+
+    @Test
+    public void getServiceName_forModuleProperties() {
+        assertWithMessage("Properties service name")
+                .that(AMFM_PROPERTIES.getServiceName()).isEqualTo(SERVICE_NAME);
+    }
+
+    @Test
+    public void getClassId_forModuleProperties() {
+        assertWithMessage("Properties class ID")
+                .that(AMFM_PROPERTIES.getClassId()).isEqualTo(CLASS_ID);
+    }
+
+    @Test
+    public void getImplementor_forModuleProperties() {
+        assertWithMessage("Properties implementor")
+                .that(AMFM_PROPERTIES.getImplementor()).isEqualTo(IMPLEMENTOR);
+    }
+
+    @Test
+    public void getProduct_forModuleProperties() {
+        assertWithMessage("Properties product")
+                .that(AMFM_PROPERTIES.getProduct()).isEqualTo(PRODUCT);
+    }
+
+    @Test
+    public void getVersion_forModuleProperties() {
+        assertWithMessage("Properties version")
+                .that(AMFM_PROPERTIES.getVersion()).isEqualTo(VERSION);
+    }
+
+    @Test
+    public void getSerial_forModuleProperties() {
+        assertWithMessage("Serial properties")
+                .that(AMFM_PROPERTIES.getSerial()).isEqualTo(SERIAL);
+    }
+
+    @Test
+    public void getNumTuners_forModuleProperties() {
+        assertWithMessage("Number of tuners in properties")
+                .that(AMFM_PROPERTIES.getNumTuners()).isEqualTo(NUM_TUNERS);
+    }
+
+    @Test
+    public void getNumAudioSources_forModuleProperties() {
+        assertWithMessage("Number of audio sources in properties")
+                .that(AMFM_PROPERTIES.getNumAudioSources()).isEqualTo(NUM_AUDIO_SOURCES);
+    }
+
+    @Test
+    public void isInitializationRequired_forModuleProperties() {
+        assertWithMessage("Initialization required in properties")
+                .that(AMFM_PROPERTIES.isInitializationRequired())
+                .isEqualTo(IS_INITIALIZATION_REQUIRED);
+    }
+
+    @Test
+    public void isCaptureSupported_forModuleProperties() {
+        assertWithMessage("Capture support in properties")
+                .that(AMFM_PROPERTIES.isCaptureSupported()).isEqualTo(IS_CAPTURE_SUPPORTED);
+    }
+
+    @Test
+    public void isBackgroundScanningSupported_forModuleProperties() {
+        assertWithMessage("Background scan support in properties")
+                .that(AMFM_PROPERTIES.isBackgroundScanningSupported())
+                .isEqualTo(IS_BG_SCAN_SUPPORTED);
+    }
+
+    @Test
+    public void isProgramTypeSupported_withSupportedType_forModuleProperties() {
+        assertWithMessage("AM/FM frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY))
+                .isTrue();
+    }
+
+    @Test
+    public void isProgramTypeSupported_withNonSupportedType_forModuleProperties() {
+        assertWithMessage("DAB frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("AM/FM frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)).isTrue();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withNonSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("DAB frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void getDabFrequencyTable_forModulePropertiesInitializedWithNullTable() {
+        assertWithMessage("Properties DAB frequency table")
+                .that(AMFM_PROPERTIES.getDabFrequencyTable()).isNull();
+    }
+
+    @Test
+    public void getDabFrequencyTable_forModulePropertiesInitializedWithEmptyTable() {
+        RadioManager.ModuleProperties properties = createAmFmProperties(new ArrayMap<>());
+
+        assertWithMessage("Properties DAB frequency table")
+                .that(properties.getDabFrequencyTable()).isNull();
+    }
+
+    @Test
+    public void getVendorInfo_forModuleProperties() {
+        assertWithMessage("Properties vendor info")
+                .that(AMFM_PROPERTIES.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void getBands_forModuleProperties() {
+        assertWithMessage("Properties bands")
+                .that(AMFM_PROPERTIES.getBands()).asList()
+                .containsExactly(AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void describeContents_forModuleProperties() {
+        assertWithMessage("Module properties contents")
+                .that(AMFM_PROPERTIES.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void writeToParcel_forModulePropertiesWithNullDabFrequencyTable() {
+        Parcel parcel = Parcel.obtain();
+
+        AMFM_PROPERTIES.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.ModuleProperties modulePropertiesFromParcel =
+                RadioManager.ModuleProperties.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Module properties created from parcel")
+                .that(modulePropertiesFromParcel).isEqualTo(AMFM_PROPERTIES);
+    }
+
+    @Test
+    public void writeToParcel_forModulePropertiesWithNonnullDabFrequencyTable() {
+        Parcel parcel = Parcel.obtain();
+        RadioManager.ModuleProperties propertiesToParcel = createAmFmProperties(
+                Map.of("5A", 174928, "12D", 229072));
+
+        propertiesToParcel.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.ModuleProperties modulePropertiesFromParcel =
+                RadioManager.ModuleProperties.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Module properties created from parcel")
+                .that(modulePropertiesFromParcel).isEqualTo(propertiesToParcel);
+    }
+
+    @Test
+    public void equals_withSameProperties_returnsTrue() {
+        RadioManager.ModuleProperties propertiesCompared =
+                createAmFmProperties(/* dabFrequencyTable= */ null);
+
+        assertWithMessage("The same module properties")
+                .that(AMFM_PROPERTIES).isEqualTo(propertiesCompared);
+    }
+
+    @Test
+    public void equals_withModulePropertiesOfDifferentIds_returnsFalse() {
+        RadioManager.ModuleProperties propertiesDab = new RadioManager.ModuleProperties(
+                PROPERTIES_ID + 1, SERVICE_NAME, CLASS_ID, IMPLEMENTOR, PRODUCT, VERSION,
+                SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, IS_INITIALIZATION_REQUIRED,
+                IS_CAPTURE_SUPPORTED, /* bands= */ null, IS_BG_SCAN_SUPPORTED,
+                SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES, Map.of("5A", 174928),
+                /* vendorInfo= */ null);
+
+        assertWithMessage("Module properties of different id")
+                .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab);
+    }
+
+    @Test
+    public void hashCode_withSameModuleProperties_equals() {
+        RadioManager.ModuleProperties propertiesCompared =
+                createAmFmProperties(/* dabFrequencyTable= */ null);
+
+        assertWithMessage("Hash code of the same module properties")
+                .that(propertiesCompared.hashCode()).isEqualTo(AMFM_PROPERTIES.hashCode());
+    }
+
+    @Test
+    public void newArray_forModulePropertiesCreator() {
+        RadioManager.ModuleProperties[] modulePropertiesArray =
+                RadioManager.ModuleProperties.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Module properties array")
+                .that(modulePropertiesArray).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void getSelector_forProgramInfo() {
+        assertWithMessage("Selector of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR);
+    }
+
+    @Test
+    public void getLogicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier logically tuned to in DAB program info")
+                .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER);
+    }
+
+    @Test
+    public void getPhysicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier physically tuned to DAB program info")
+                .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER);
+    }
+
+    @Test
+    public void getRelatedContent_forProgramInfo() {
+        assertWithMessage("DAB program info contents")
+                .that(DAB_PROGRAM_INFO.getRelatedContent())
+                .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED);
+    }
+
+    @Test
+    public void getChannel_forProgramInfo() {
+        assertWithMessage("Main channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void getSubChannel_forProgramInfo() {
+        assertWithMessage("Sub channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSubChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void isTuned_forProgramInfo() {
+        assertWithMessage("Tuned status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isTuned()).isTrue();
+    }
+
+    @Test
+    public void isStereo_forProgramInfo() {
+        assertWithMessage("Stereo support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isStereo()).isTrue();
+    }
+
+    @Test
+    public void isDigital_forProgramInfo() {
+        assertWithMessage("Digital DAB program info")
+                .that(DAB_PROGRAM_INFO.isDigital()).isTrue();
+    }
+
+    @Test
+    public void isLive_forProgramInfo() {
+        assertWithMessage("Live status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isLive()).isTrue();
+    }
+
+    @Test
+    public void isMuted_forProgramInfo() {
+        assertWithMessage("Muted status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isMuted()).isFalse();
+    }
+
+    @Test
+    public void isTrafficProgram_forProgramInfo() {
+        assertWithMessage("Traffic program support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficProgram()).isFalse();
+    }
+
+    @Test
+    public void isTrafficAnnouncementActive_forProgramInfo() {
+        assertWithMessage("Active traffic announcement for DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficAnnouncementActive()).isFalse();
+    }
+
+    @Test
+    public void getSignalStrength_forProgramInfo() {
+        assertWithMessage("Signal strength of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSignalStrength()).isEqualTo(SIGNAL_QUALITY);
+    }
+
+    @Test
+    public void getMetadata_forProgramInfo() {
+        assertWithMessage("Metadata of DAB program info")
+                .that(DAB_PROGRAM_INFO.getMetadata()).isEqualTo(METADATA);
+    }
+
+    @Test
+    public void getVendorInfo_forProgramInfo() {
+        assertWithMessage("Vendor info of DAB program info")
+                .that(DAB_PROGRAM_INFO.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void describeContents_forProgramInfo() {
+        assertWithMessage("Program info contents")
+                .that(DAB_PROGRAM_INFO.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forProgramInfoCreator() {
+        RadioManager.ProgramInfo[] programInfoArray =
+                RadioManager.ProgramInfo.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Program infos").that(programInfoArray).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void writeToParcel_forProgramInfo() {
+        Parcel parcel = Parcel.obtain();
+
+        DAB_PROGRAM_INFO.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioManager.ProgramInfo programInfoFromParcel =
+                RadioManager.ProgramInfo.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Program info created from parcel")
+                .that(programInfoFromParcel).isEqualTo(DAB_PROGRAM_INFO);
+    }
+
+    @Test
+    public void equals_withSameProgramInfo_returnsTrue() {
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR);
+
+        assertWithMessage("The same program info")
+                .that(dabProgramInfoCompared).isEqualTo(DAB_PROGRAM_INFO);
+    }
+
+    @Test
+    public void equals_withSameProgramInfoOfDifferentSecondaryIdSelectors_returnsFalse() {
+        ProgramSelector dabSelectorCompared = new ProgramSelector(
+                ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                new ProgramSelector.Identifier[]{DAB_FREQUENCY_IDENTIFIER},
+                /* vendorIds= */ null);
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(dabSelectorCompared);
+
+        assertWithMessage("Program info with different secondary id selectors")
+                .that(DAB_PROGRAM_INFO).isNotEqualTo(dabProgramInfoCompared);
+    }
+
+    @Test
+    public void listModules_forRadioManager() throws Exception {
+        createRadioManager();
+        List<RadioManager.ModuleProperties> modules = new ArrayList<>();
+
+        mRadioManager.listModules(modules);
+
+        assertWithMessage("Modules in radio manager")
+                .that(modules).containsExactly(AMFM_PROPERTIES);
+    }
+
+    @Test
+    public void openTuner_forRadioModule() throws Exception {
+        createRadioManager();
+        int moduleId = 0;
+        boolean withAudio = true;
+
+        mRadioManager.openTuner(moduleId, FM_BAND_CONFIG, withAudio, mCallbackMock,
+                /* handler= */ null);
+
+        verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any());
+    }
+
+    @Test
+    public void addAnnouncementListener_withListenerNotAddedBefore() throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        int[] enableTypesExpected = new int[]{EVENT_ANNOUNCEMENT_TYPE};
+        ArgumentCaptor<IAnnouncementListener> announcementListener =
+                ArgumentCaptor.forClass(IAnnouncementListener.class);
+
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        verify(mRadioServiceMock).addAnnouncementListener(eq(enableTypesExpected),
+                announcementListener.capture());
+
+        announcementListener.getValue().onListUpdated(TEST_ANNOUNCEMENT_LIST);
+
+        verify(mEventListener).onListUpdated(TEST_ANNOUNCEMENT_LIST);
+    }
+
+    @Test
+    public void addAnnouncementListener_withListenerAddedBefore_closesPreviousOne()
+            throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        verify(mCloseHandleMock).close();
+    }
+
+    @Test
+    public void removeAnnouncementListener_withListenerNotAddedBefore_ignores() throws Exception {
+        createRadioManager();
+
+        mRadioManager.removeAnnouncementListener(mEventListener);
+
+        verify(mCloseHandleMock, never()).close();
+    }
+
+    @Test
+    public void removeAnnouncementListener_withListenerAddedTwice_closesTheFirstOne()
+            throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        mRadioManager.removeAnnouncementListener(mEventListener);
+
+        verify(mCloseHandleMock).close();
+    }
+
+    private static RadioManager.ModuleProperties createAmFmProperties(
+            @Nullable Map<String, Integer>  dabFrequencyTable) {
+        return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID,
+                IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES,
+                IS_INITIALIZATION_REQUIRED, IS_CAPTURE_SUPPORTED,
+                new RadioManager.BandDescriptor[]{AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR},
+                IS_BG_SCAN_SUPPORTED, SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES,
+                dabFrequencyTable, /* vendorInfo= */ null);
+    }
+
+    private static RadioManager.FmBandDescriptor createFmBandDescriptor() {
+        return new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT,
+                FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
+                AF_SUPPORTED, EA_SUPPORTED);
+    }
+
+    private static RadioManager.AmBandDescriptor createAmBandDescriptor() {
+        return new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED);
+    }
+
+    private static RadioManager.FmBandConfig createFmBandConfig() {
+        return new RadioManager.FmBandConfig(createFmBandDescriptor());
+    }
+
+    private static RadioManager.AmBandConfig createAmBandConfig() {
+        return new RadioManager.AmBandConfig(createAmBandDescriptor());
+    }
+
+    private static RadioMetadata createMetadata() {
+        RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder();
+        return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest").build();
+    }
+
+    private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) {
+        return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER,
+                DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS,
+                SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null);
+    }
+
+    private void createRadioManager() throws RemoteException {
+        when(mRadioServiceMock.listModules()).thenReturn(Arrays.asList(AMFM_PROPERTIES));
+        when(mRadioServiceMock.addAnnouncementListener(any(), any())).thenReturn(mCloseHandleMock);
+
+        mRadioManager = new RadioManager(mContextMock, mRadioServiceMock);
+    }
+
+    private Set<Integer> createAnnouncementTypeSet(int enableType) {
+        return Set.of(enableType);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
index fe15597..5771135 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
@@ -20,18 +20,63 @@
 
 import static org.junit.Assert.assertThrows;
 
+import android.graphics.Bitmap;
 import android.hardware.radio.RadioMetadata;
+import android.os.Parcel;
 
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.Set;
 
+@RunWith(MockitoJUnitRunner.class)
 public final class RadioMetadataTest {
 
+    private static final int CREATOR_ARRAY_SIZE = 3;
     private static final int INT_KEY_VALUE = 1;
+    private static final long TEST_UTC_SECOND_SINCE_EPOCH = 200;
+    private static final int TEST_TIME_ZONE_OFFSET_MINUTES = 1;
 
     private final RadioMetadata.Builder mBuilder = new RadioMetadata.Builder();
 
+    @Mock
+    private Bitmap mBitmapValue;
+
+    @Test
+    public void describeContents_forClock() {
+        RadioMetadata.Clock clock = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH,
+                TEST_TIME_ZONE_OFFSET_MINUTES);
+
+        assertWithMessage("Describe contents for metadata clock")
+                .that(clock.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forClockCreator() {
+        RadioMetadata.Clock[] clocks = RadioMetadata.Clock.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Clock array size").that(clocks.length).isEqualTo(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void writeToParcel_forClock() {
+        RadioMetadata.Clock clockExpected = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH,
+                TEST_TIME_ZONE_OFFSET_MINUTES);
+        Parcel parcel = Parcel.obtain();
+
+        clockExpected.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioMetadata.Clock clockFromParcel = RadioMetadata.Clock.CREATOR.createFromParcel(parcel);
+        assertWithMessage("UTC second since epoch of metadata clock created from parcel")
+                .that(clockFromParcel.getUtcEpochSeconds()).isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH);
+        assertWithMessage("Time zone offset minutes of metadata clock created from parcel")
+                .that(clockFromParcel.getTimezoneOffsetMinutes())
+                .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES);
+    }
+
     @Test
     public void putString_withIllegalKey() {
         String invalidStringKey = RadioMetadata.METADATA_KEY_RDS_PI;
@@ -129,22 +174,56 @@
     }
 
     @Test
+    public void getBitmap_withKeyInMetadata() {
+        String key = RadioMetadata.METADATA_KEY_ICON;
+        RadioMetadata metadata = mBuilder.putBitmap(key, mBitmapValue).build();
+
+        assertWithMessage("Bitmap value for key %s in metadata", key)
+                .that(metadata.getBitmap(key)).isEqualTo(mBitmapValue);
+    }
+
+    @Test
+    public void getBitmap_withKeyNotInMetadata() {
+        String key = RadioMetadata.METADATA_KEY_ICON;
+        RadioMetadata metadata = mBuilder.build();
+
+        assertWithMessage("Bitmap value for key %s not in metadata", key)
+                .that(metadata.getBitmap(key)).isNull();
+    }
+
+    @Test
+    public void getBitmapId_withKeyInMetadata() {
+        String key = RadioMetadata.METADATA_KEY_ART;
+        RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build();
+
+        assertWithMessage("Bitmap id value for key %s in metadata", key)
+                .that(metadata.getBitmapId(key)).isEqualTo(INT_KEY_VALUE);
+    }
+
+    @Test
+    public void getBitmapId_withKeyNotInMetadata() {
+        String key = RadioMetadata.METADATA_KEY_ART;
+        RadioMetadata metadata = mBuilder.build();
+
+        assertWithMessage("Bitmap id value for key %s not in metadata", key)
+                .that(metadata.getBitmapId(key)).isEqualTo(0);
+    }
+
+    @Test
     public void getClock_withKeyInMetadata() {
         String key = RadioMetadata.METADATA_KEY_CLOCK;
-        long utcSecondsSinceEpochExpected = 200;
-        int timezoneOffsetMinutesExpected = 1;
         RadioMetadata metadata = mBuilder
-                .putClock(key, utcSecondsSinceEpochExpected, timezoneOffsetMinutesExpected)
+                .putClock(key, TEST_UTC_SECOND_SINCE_EPOCH, TEST_TIME_ZONE_OFFSET_MINUTES)
                 .build();
 
         RadioMetadata.Clock clockExpected = metadata.getClock(key);
 
         assertWithMessage("Number of seconds since epoch of value for key %s in metadata", key)
                 .that(clockExpected.getUtcEpochSeconds())
-                .isEqualTo(utcSecondsSinceEpochExpected);
+                .isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH);
         assertWithMessage("Offset of timezone in minutes of value for key %s in metadata", key)
                 .that(clockExpected.getTimezoneOffsetMinutes())
-                .isEqualTo(timezoneOffsetMinutesExpected);
+                .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES);
     }
 
     @Test
@@ -180,12 +259,13 @@
         RadioMetadata metadata = mBuilder
                 .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
                 .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+                .putBitmap(RadioMetadata.METADATA_KEY_ICON, mBitmapValue)
                 .build();
 
         Set<String> metadataSet = metadata.keySet();
 
         assertWithMessage("Metadata set of non-empty metadata")
-                .that(metadataSet).containsExactly(
+                .that(metadataSet).containsExactly(RadioMetadata.METADATA_KEY_ICON,
                         RadioMetadata.METADATA_KEY_RDS_PI, RadioMetadata.METADATA_KEY_ARTIST);
     }
 
@@ -208,4 +288,46 @@
                 .that(key).isEqualTo(RadioMetadata.METADATA_KEY_RDS_PI);
     }
 
+    @Test
+    public void equals_forMetadataWithSameContents_returnsTrue() {
+        RadioMetadata metadata = mBuilder
+                .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
+                .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+                .build();
+        RadioMetadata.Builder copyBuilder = new RadioMetadata.Builder(metadata);
+        RadioMetadata metadataCopied = copyBuilder.build();
+
+        assertWithMessage("Metadata with the same contents")
+                .that(metadataCopied).isEqualTo(metadata);
+    }
+
+    @Test
+    public void describeContents_forMetadata() {
+        RadioMetadata metadata = mBuilder.build();
+
+        assertWithMessage("Metadata contents").that(metadata.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void newArray_forRadioMetadataCreator() {
+        RadioMetadata[] metadataArray = RadioMetadata.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+        assertWithMessage("Radio metadata array").that(metadataArray).hasLength(CREATOR_ARRAY_SIZE);
+    }
+
+    @Test
+    public void writeToParcel_forRadioMetadata() {
+        RadioMetadata metadataExpected = mBuilder
+                .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
+                .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+                .build();
+        Parcel parcel = Parcel.obtain();
+
+        metadataExpected.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+
+        RadioMetadata metadataFromParcel = RadioMetadata.CREATOR.createFromParcel(parcel);
+        assertWithMessage("Radio metadata created from parcel")
+                .that(metadataFromParcel).isEqualTo(metadataExpected);
+    }
 }
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..bdba6a1
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
@@ -0,0 +1,443 @@
+/*
+ * 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 startBackgroundScan_forTunerAdapter() throws Exception {
+        when(mTunerMock.startBackgroundScan()).thenReturn(false);
+
+        boolean scanStatus = mRadioTuner.startBackgroundScan();
+
+        verify(mTunerMock).startBackgroundScan();
+        assertWithMessage("Status for starting background scan").that(scanStatus).isFalse();
+    }
+
+    @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/ExtendedRadioMockitoTestCase.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/ExtendedRadioMockitoTestCase.java
new file mode 100644
index 0000000..c6021ec
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/ExtendedRadioMockitoTestCase.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.broadcastradio;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import android.util.Log;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/**
+ * Base class to make it easier to write tests that uses {@code ExtendedMockito} for radio.
+ *
+ */
+public abstract class ExtendedRadioMockitoTestCase {
+
+    private static final String TAG = "RadioMockitoTestCase";
+
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private MockitoSession mSession;
+
+    @Before
+    public void startSession() {
+        StaticMockitoSessionBuilder builder = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT);
+        initializeSession(builder);
+        mSession = builder.startMocking();
+    }
+
+    /**
+     * Initializes the mockito session for radio test.
+     *
+     * <p>Typically used to define which classes should have static methods mocked or spied.
+     */
+    protected void initializeSession(StaticMockitoSessionBuilder builder) {
+        if (DEBUG) {
+            Log.d(TAG, "initializeSession()");
+        }
+    }
+
+    @After
+    public final void finishSession() {
+        if (mSession == null) {
+            Log.w(TAG, "finishSession(): no session");
+            return;
+        }
+        try {
+            if (DEBUG) {
+                Log.d(TAG, "finishSession()");
+            }
+        } finally {
+            // mSession.finishMocking() must ALWAYS be called (hence the over-protective try/finally
+            // statements), otherwise it would cause failures on future tests as mockito
+            // cannot start a session when a previous one is not finished
+            mSession.finishMocking();
+        }
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
new file mode 100644
index 0000000..a2df426
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+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.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.os.IBinder;
+import android.os.ServiceManager;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.List;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with AIDL HAL implementation
+ */
+public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCase {
+
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+    private static final String AM_FM_SERVICE_NAME =
+            "android.hardware.broadcastradio.IBroadcastRadio/amfm";
+    private static final String DAB_SERVICE_NAME =
+            "android.hardware.broadcastradio.IBroadcastRadio/dab";
+
+    private IRadioServiceAidlImpl mAidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private IBinder mServiceBinderMock;
+    @Mock
+    private BroadcastRadioServiceImpl mHalMock;
+    @Mock
+    private RadioManager.ModuleProperties mModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mTunerMock;
+
+    @Before
+    public void setUp() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+
+        when(mHalMock.listModules()).thenReturn(List.of(mModuleMock));
+        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mTunerMock);
+        when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mAidlImpl = new IRadioServiceAidlImpl(mServiceMock, mHalMock);
+    }
+
+    @Override
+    protected void initializeSession(StaticMockitoSessionBuilder builder) {
+        builder.spyStatic(ServiceManager.class);
+    }
+
+    @Test
+    public void getServicesNames_forAidlImpl() {
+        doReturn(null).when(() -> ServiceManager.waitForDeclaredService(
+                AM_FM_SERVICE_NAME));
+        doReturn(mServiceBinderMock).when(() -> ServiceManager.waitForDeclaredService(
+                DAB_SERVICE_NAME));
+
+        assertWithMessage("Names of services available")
+                .that(IRadioServiceAidlImpl.getServicesNames()).containsExactly(DAB_SERVICE_NAME);
+    }
+
+    @Test
+    public void loadModules_forAidlImpl() {
+        assertWithMessage("Modules loaded in AIDL HAL")
+                .that(mAidlImpl.listModules()).containsExactly(mModuleMock);
+    }
+
+    @Test
+    public void openTuner_forAidlImpl() throws Exception {
+        ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in AIDL HAL")
+                .that(tuner).isEqualTo(mTunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forAidlImpl() {
+        ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHalMock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
new file mode 100644
index 0000000..5ab9435
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with HIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceHidlImplTest {
+
+    private static final int HAL1_MODULE_ID = 0;
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceHidlImpl mHidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Mock;
+    @Mock
+    private com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Mock;
+    @Mock
+    private RadioManager.ModuleProperties mHal1ModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mHal2ModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mHal1TunerMock;
+    @Mock
+    private ITuner mHal2TunerMock;
+
+    @Before
+    public void setup() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+        when(mHal1Mock.loadModules()).thenReturn(Arrays.asList(mHal1ModuleMock));
+        when(mHal1Mock.openTuner(anyInt(), any(), anyBoolean(), any())).thenReturn(mHal1TunerMock);
+
+        when(mHal2Mock.listModules()).thenReturn(Arrays.asList(mHal2ModuleMock));
+        doAnswer(invocation -> {
+            int moduleId = (int) invocation.getArguments()[0];
+            return moduleId != HAL1_MODULE_ID;
+        }).when(mHal2Mock).hasModule(anyInt());
+        when(mHal2Mock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mHal2TunerMock);
+        when(mHal2Mock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mHidlImpl = new IRadioServiceHidlImpl(mServiceMock, mHal1Mock, mHal2Mock);
+    }
+
+    @Test
+    public void loadModules_forHidlImpl() {
+        assertWithMessage("Modules loaded in HIDL HAL")
+                .that(mHidlImpl.listModules())
+                .containsExactly(mHal1ModuleMock, mHal2ModuleMock);
+    }
+
+    @Test
+    public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 1")
+                .that(tuner).isEqualTo(mHal1TunerMock);
+    }
+
+    @Test
+    public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 2")
+                .that(tuner).isEqualTo(mHal2TunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forHidlImpl() {
+        when(mHal2Mock.hasAnyModules()).thenReturn(true);
+        ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHal2Mock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
index e2556d67..a421218 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
@@ -15,9 +15,11 @@
  */
 package com.android.server.broadcastradio.aidl;
 
+import android.hardware.broadcastradio.IdentifierType;
 import android.hardware.broadcastradio.Metadata;
 import android.hardware.broadcastradio.ProgramIdentifier;
 import android.hardware.broadcastradio.ProgramInfo;
+import android.hardware.broadcastradio.VendorKeyValue;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
@@ -29,6 +31,16 @@
         throw new UnsupportedOperationException("AidlTestUtils class is noninstantiable");
     }
 
+    static RadioManager.ModuleProperties makeDefaultModuleProperties() {
+        return new RadioManager.ModuleProperties(
+                /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+                /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+                /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+                /* isCaptureSupported= */ false, /* bands= */ null,
+                /* isBgScanSupported= */ false, new int[] {}, new int[] {},
+                /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+    }
+
     static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
         return new RadioManager.ProgramInfo(selector,
                 selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
@@ -42,7 +54,7 @@
         return makeProgramInfo(selector, signalQuality);
     }
 
-    static ProgramSelector makeFMSelector(long freq) {
+    static ProgramSelector makeFmSelector(long freq) {
         return makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
                 new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                         freq));
@@ -54,6 +66,18 @@
                 /* vendorIds= */ null);
     }
 
+    static android.hardware.broadcastradio.ProgramSelector makeHalFmSelector(int freq) {
+        ProgramIdentifier halId = new ProgramIdentifier();
+        halId.type = IdentifierType.AMFM_FREQUENCY_KHZ;
+        halId.value = freq;
+
+        android.hardware.broadcastradio.ProgramSelector halSelector =
+                new android.hardware.broadcastradio.ProgramSelector();
+        halSelector.primaryId = halId;
+        halSelector.secondaryIds = new ProgramIdentifier[0];
+        return halSelector;
+    }
+
     static ProgramInfo programInfoToHalProgramInfo(RadioManager.ProgramInfo info) {
         // Note that because ConversionUtils does not by design provide functions for all
         // conversions, this function only copies fields that are set by makeProgramInfo().
@@ -69,7 +93,7 @@
         return hwInfo;
     }
 
-    static ProgramInfo makeHalProgramSelector(
+    static ProgramInfo makeHalProgramInfo(
             android.hardware.broadcastradio.ProgramSelector hwSel, int hwSignalQuality) {
         ProgramInfo hwInfo = new ProgramInfo();
         hwInfo.selector = hwSel;
@@ -80,4 +104,21 @@
         hwInfo.metadata = new Metadata[]{};
         return hwInfo;
     }
+
+    static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) {
+        VendorKeyValue vendorKeyValue = new VendorKeyValue();
+        vendorKeyValue.key = vendorKey;
+        vendorKeyValue.value = vendorValue;
+        return vendorKeyValue;
+    }
+
+    static android.hardware.broadcastradio.Announcement makeAnnouncement(int type,
+            int selectorFreq) {
+        android.hardware.broadcastradio.Announcement halAnnouncement =
+                new android.hardware.broadcastradio.Announcement();
+        halAnnouncement.type = (byte) type;
+        halAnnouncement.selector = makeHalFmSelector(selectorFreq);
+        halAnnouncement.vendorInfo = new VendorKeyValue[]{};
+        return halAnnouncement;
+    }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java
new file mode 100644
index 0000000..9a1af19
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for AIDL HAL AnnouncementAggregator.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class AnnouncementAggregatorTest {
+    private static final int[] TEST_ENABLED_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private final Object mLock = new Object();
+    private AnnouncementAggregator mAnnouncementAggregator;
+    private IBinder.DeathRecipient mDeathRecipient;
+
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private IBinder mBinderMock;
+    private RadioModule[] mRadioModuleMocks;
+    private ICloseHandle[] mCloseHandleMocks;
+    private Announcement[] mAnnouncementMocks;
+
+    @Before
+    public void setUp() throws Exception {
+        ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
+                ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
+        when(mListenerMock.asBinder()).thenReturn(mBinderMock);
+
+        mAnnouncementAggregator = new AnnouncementAggregator(mListenerMock, mLock);
+
+        verify(mBinderMock).linkToDeath(deathRecipientCaptor.capture(), eq(0));
+        mDeathRecipient = deathRecipientCaptor.getValue();
+    }
+
+    @Test
+    public void onListUpdated_withOneModuleWatcher() throws Exception {
+        ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor =
+                ArgumentCaptor.forClass(IAnnouncementListener.class);
+        watchModules(/* moduleNumber= */ 1);
+
+        verify(mRadioModuleMocks[0]).addAnnouncementListener(moduleWatcherCaptor.capture(), any());
+
+        moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[0]));
+
+        verify(mListenerMock).onListUpdated(any());
+    }
+
+    @Test
+    public void onListUpdated_withMultipleModuleWatchers() throws Exception {
+        int moduleNumber = 3;
+        watchModules(moduleNumber);
+
+        for (int index = 0; index < moduleNumber; index++) {
+            ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor =
+                    ArgumentCaptor.forClass(IAnnouncementListener.class);
+            ArgumentCaptor<List<Announcement>> announcementsCaptor =
+                    ArgumentCaptor.forClass(List.class);
+            verify(mRadioModuleMocks[index])
+                    .addAnnouncementListener(moduleWatcherCaptor.capture(), any());
+
+            moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[index]));
+
+            verify(mListenerMock, times(index + 1)).onListUpdated(announcementsCaptor.capture());
+            assertWithMessage("Number of announcements %s after %s announcements were updated",
+                    announcementsCaptor.getValue(), index + 1)
+                    .that(announcementsCaptor.getValue().size()).isEqualTo(index + 1);
+        }
+    }
+
+    @Test
+    public void close_withOneModuleWatcher_invokesCloseHandle() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mAnnouncementAggregator.close();
+
+        verify(mCloseHandleMocks[0]).close();
+        verify(mBinderMock).unlinkToDeath(mDeathRecipient, 0);
+    }
+
+    @Test
+    public void close_withMultipleModuleWatcher_invokesCloseHandles() throws Exception {
+        int moduleNumber = 3;
+        watchModules(moduleNumber);
+
+        mAnnouncementAggregator.close();
+
+        for (int index = 0; index < moduleNumber; index++) {
+            verify(mCloseHandleMocks[index]).close();
+        }
+    }
+
+    @Test
+    public void close_twice_invokesCloseHandleOnce() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mAnnouncementAggregator.close();
+        mAnnouncementAggregator.close();
+
+        verify(mCloseHandleMocks[0]).close();
+        verify(mBinderMock).unlinkToDeath(mDeathRecipient, 0);
+    }
+
+    @Test
+    public void binderDied_forDeathRecipient_invokesCloseHandle() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mDeathRecipient.binderDied();
+
+        verify(mCloseHandleMocks[0]).close();
+
+    }
+
+    private void watchModules(int moduleNumber) throws RemoteException {
+        mRadioModuleMocks = new RadioModule[moduleNumber];
+        mCloseHandleMocks = new ICloseHandle[moduleNumber];
+        mAnnouncementMocks = new Announcement[moduleNumber];
+
+        for (int index = 0; index < moduleNumber; index++) {
+            mRadioModuleMocks[index] = mock(RadioModule.class);
+            mCloseHandleMocks[index] = mock(ICloseHandle.class);
+            mAnnouncementMocks[index] = mock(Announcement.class);
+
+            when(mRadioModuleMocks[index].addAnnouncementListener(any(), any()))
+                    .thenReturn(mCloseHandleMocks[index]);
+            mAnnouncementAggregator.watchModule(mRadioModuleMocks[index], TEST_ENABLED_TYPES);
+        }
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
new file mode 100644
index 0000000..635d1e7
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.os.IBinder;
+import android.os.IServiceCallback;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
+
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTestCase {
+
+    private static final int FM_RADIO_MODULE_ID = 0;
+    private static final int DAB_RADIO_MODULE_ID = 1;
+    private static final ArrayList<String> SERVICE_LIST =
+            new ArrayList<>(Arrays.asList("FmService", "DabService"));
+
+    private BroadcastRadioServiceImpl mBroadcastRadioService;
+    private IBinder.DeathRecipient mFmDeathRecipient;
+
+    @Mock
+    private RadioManager.ModuleProperties mFmModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mDabModuleMock;
+    @Mock
+    private RadioModule mFmRadioModuleMock;
+    @Mock
+    private RadioModule mDabRadioModuleMock;
+    @Mock
+    private IBroadcastRadio mFmHalServiceMock;
+    @Mock
+    private IBroadcastRadio mDabHalServiceMock;
+    @Mock
+    private IBinder mFmBinderMock;
+    @Mock
+    private IBinder mDabBinderMock;
+    @Mock
+    private TunerSession mFmTunerSessionMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+
+    @Override
+    protected void initializeSession(StaticMockitoSessionBuilder builder) {
+        builder.spyStatic(ServiceManager.class)
+                .spyStatic(RadioModule.class);
+    }
+
+    @Test
+    public void listModules_withMultipleServiceNames() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Radio modules in AIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.listModules())
+                .containsExactly(mFmModuleMock, mDabModuleMock);
+    }
+
+    @Test
+    public void hasModules_withIdFoundInModules() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("DAB radio module in AIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasModule(DAB_RADIO_MODULE_ID)).isTrue();
+    }
+
+    @Test
+    public void hasModules_withIdNotFoundInModules() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Radio module of id not found in AIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasModule(DAB_RADIO_MODULE_ID + 1)).isFalse();
+    }
+
+    @Test
+    public void hasAnyModules_withModulesExist() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Any radio module in AIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasAnyModules()).isTrue();
+    }
+
+    @Test
+    public void openSession_withIdFound() throws Exception {
+        createBroadcastRadioService();
+
+        ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
+                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Session opened in FM radio module")
+                .that(session).isEqualTo(mFmTunerSessionMock);
+    }
+
+    @Test
+    public void openSession_withIdNotFound() throws Exception {
+        createBroadcastRadioService();
+
+        ITuner session = mBroadcastRadioService.openSession(DAB_RADIO_MODULE_ID + 1,
+                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Session opened with id not found").that(session).isNull();
+    }
+
+    @Test
+    public void binderDied_forDeathRecipient() throws Exception {
+        createBroadcastRadioService();
+
+        mFmDeathRecipient.binderDied();
+
+        verify(mFmRadioModuleMock).closeSessions(eq(RadioTuner.ERROR_HARDWARE_FAILURE));
+        assertWithMessage("FM radio module after FM broadcast radio HAL service died")
+                .that(mBroadcastRadioService.hasModule(FM_RADIO_MODULE_ID)).isFalse();
+    }
+
+    private void createBroadcastRadioService() throws RemoteException {
+        mockServiceManager();
+        mBroadcastRadioService = new BroadcastRadioServiceImpl(SERVICE_LIST);
+    }
+
+    private void mockServiceManager() throws RemoteException {
+        doAnswer((Answer<Void>) invocation -> {
+            String serviceName = (String) invocation.getArguments()[0];
+            IServiceCallback serviceCallback = (IServiceCallback) invocation.getArguments()[1];
+            IBinder mockBinder = serviceName.equals("FmService") ? mFmBinderMock : mDabBinderMock;
+            serviceCallback.onRegistration(serviceName, mockBinder);
+            return null;
+        }).when(() -> ServiceManager.registerForNotifications(anyString(),
+                any(IServiceCallback.class)));
+
+        doReturn(mFmRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+                eq(FM_RADIO_MODULE_ID), anyString(), any(IBinder.class), any(Object.class)));
+        doReturn(mDabRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+                eq(DAB_RADIO_MODULE_ID), anyString(), any(IBinder.class), any(Object.class)));
+
+        when(mFmRadioModuleMock.getProperties()).thenReturn(mFmModuleMock);
+        when(mDabRadioModuleMock.getProperties()).thenReturn(mDabModuleMock);
+
+        when(mFmRadioModuleMock.getService()).thenReturn(mFmHalServiceMock);
+        when(mDabRadioModuleMock.getService()).thenReturn(mDabHalServiceMock);
+
+        when(mFmHalServiceMock.asBinder()).thenReturn(mFmBinderMock);
+        when(mDabHalServiceMock.asBinder()).thenReturn(mDabBinderMock);
+
+        doAnswer(invocation -> {
+            mFmDeathRecipient = (IBinder.DeathRecipient) invocation.getArguments()[0];
+            return null;
+        }).when(mFmBinderMock).linkToDeath(any(), anyInt());
+
+        when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock)))
+                .thenReturn(mFmTunerSessionMock);
+    }
+}
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/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
index 7f71921..7a8475f 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/RadioModuleTest.java
@@ -19,13 +19,18 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Matchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.graphics.Bitmap;
 import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
 import android.hardware.radio.RadioManager;
 import android.os.RemoteException;
 
@@ -41,27 +46,34 @@
 @RunWith(MockitoJUnitRunner.class)
 public final class RadioModuleTest {
 
+    private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EVENT;
+    private static final RadioManager.ModuleProperties TEST_MODULE_PROPERTIES =
+            AidlTestUtils.makeDefaultModuleProperties();
+
     // Mocks
     @Mock
     private IBroadcastRadio mBroadcastRadioMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private android.hardware.broadcastradio.ICloseHandle mHalCloseHandleMock;
 
     private final Object mLock = new Object();
     // RadioModule under test
     private RadioModule mRadioModule;
+    private android.hardware.broadcastradio.IAnnouncementListener mHalListener;
 
     @Before
     public void setup() throws RemoteException {
-        mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
-                /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
-                /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
-                /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
-                /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
-                /* supportedProgramTypes= */ new int[]{},
-                /* supportedIdentifierTypes */ new int[]{},
-                /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+        mRadioModule = new RadioModule(mBroadcastRadioMock, TEST_MODULE_PROPERTIES, mLock);
 
         // TODO(b/241118988): test non-null image for getImage method
         when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(null);
+        doAnswer(invocation -> {
+            mHalListener = (android.hardware.broadcastradio.IAnnouncementListener) invocation
+                    .getArguments()[0];
+            return null;
+        }).when(mBroadcastRadioMock).registerAnnouncementListener(any(), any());
     }
 
     @Test
@@ -71,7 +83,13 @@
     }
 
     @Test
-    public void setInternalHalCallback_callbackSetInHal() throws RemoteException {
+    public void getProperties() {
+        assertWithMessage("Module properties of radio module")
+                .that(mRadioModule.getProperties()).isEqualTo(TEST_MODULE_PROPERTIES);
+    }
+
+    @Test
+    public void setInternalHalCallback_callbackSetInHal() throws Exception {
         mRadioModule.setInternalHalCallback();
 
         verify(mBroadcastRadioMock).setTunerCallback(any());
@@ -83,7 +101,7 @@
 
         Bitmap imageTest = mRadioModule.getImage(imageId);
 
-        assertWithMessage("Image got from radio module").that(imageTest).isNull();
+        assertWithMessage("Image from radio module").that(imageTest).isNull();
     }
 
     @Test
@@ -97,4 +115,36 @@
         assertWithMessage("Exception for getting image with invalid ID")
                 .that(thrown).hasMessageThat().contains("Image ID is missing");
     }
+
+    @Test
+    public void addAnnouncementListener_listenerRegistered() throws Exception {
+        mRadioModule.addAnnouncementListener(mListenerMock, new int[]{TEST_ENABLED_TYPE});
+
+        verify(mBroadcastRadioMock)
+                .registerAnnouncementListener(any(), eq(new byte[]{TEST_ENABLED_TYPE}));
+    }
+
+    @Test
+    public void onListUpdate_forAnnouncementListener() throws Exception {
+        android.hardware.broadcastradio.Announcement halAnnouncement =
+                AidlTestUtils.makeAnnouncement(TEST_ENABLED_TYPE, /* selectorFreq= */ 96300);
+        mRadioModule.addAnnouncementListener(mListenerMock, new int[]{TEST_ENABLED_TYPE});
+
+        mHalListener.onListUpdated(
+                new android.hardware.broadcastradio.Announcement[]{halAnnouncement});
+
+        verify(mListenerMock).onListUpdated(any());
+    }
+
+    @Test
+    public void close_forCloseHandle() throws Exception {
+        when(mBroadcastRadioMock.registerAnnouncementListener(any(), any()))
+                .thenReturn(mHalCloseHandleMock);
+        ICloseHandle closeHandle =
+                mRadioModule.addAnnouncementListener(mListenerMock, new int[]{TEST_ENABLED_TYPE});
+
+        closeHandle.close();
+
+        verify(mHalCloseHandleMock).close();
+    }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 8354ad1..3bf993c 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -19,9 +19,10 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
@@ -31,17 +32,19 @@
 import android.graphics.Bitmap;
 import android.hardware.broadcastradio.IBroadcastRadio;
 import android.hardware.broadcastradio.ITunerCallback;
+import android.hardware.broadcastradio.IdentifierType;
 import android.hardware.broadcastradio.ProgramInfo;
 import android.hardware.broadcastradio.Result;
+import android.hardware.broadcastradio.VendorKeyValue;
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
-import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -58,19 +61,20 @@
  */
 @RunWith(MockitoJUnitRunner.class)
 public final class TunerSessionTest {
+
     private static final VerificationWithTimeout CALLBACK_TIMEOUT =
             timeout(/* millis= */ 200);
-
-    private final int mSignalQuality = 1;
-    private final long mAmfmFrequencySpacing = 500;
-    private final long[] mAmfmFrequencyList = {97500, 98100, 99100};
-    private final RadioManager.FmBandDescriptor mFmBandDescriptor =
+    private static final int SIGNAL_QUALITY = 1;
+    private static final long AM_FM_FREQUENCY_SPACING = 500;
+    private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100};
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
             new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
                     /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100,
                     /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false,
                     /* ea= */ false);
-    private final RadioManager.BandConfig mFmBandConfig =
-            new RadioManager.FmBandConfig(mFmBandDescriptor);
+    private static final RadioManager.BandConfig FM_BAND_CONFIG =
+            new RadioManager.FmBandConfig(FM_BAND_DESCRIPTOR);
+    private static final int UNSUPPORTED_CONFIG_FLAG = 0;
 
     // Mocks
     @Mock private IBroadcastRadio mBroadcastRadioMock;
@@ -83,20 +87,14 @@
     // Objects created by mRadioModule
     private ITunerCallback mHalTunerCallback;
     private ProgramInfo mHalCurrentInfo;
-    private final int mUnsupportedConfigFlag = 0;
     private final ArrayMap<Integer, Boolean> mHalConfigMap = new ArrayMap<>();
 
     private TunerSession[] mTunerSessions;
 
     @Before
-    public void setup() throws RemoteException {
-        mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(
-                /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
-                /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
-                /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
-                /* isCaptureSupported= */ false, /* bands= */ null, /* isBgScanSupported= */ false,
-                new int[] {}, new int[] {},
-                /* dabFrequencyTable= */ null, /* vendorInfo= */ null), mLock);
+    public void setup() throws Exception {
+        mRadioModule = new RadioModule(mBroadcastRadioMock,
+                AidlTestUtils.makeDefaultModuleProperties(), mLock);
 
         doAnswer(invocation -> {
             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
@@ -105,48 +103,58 @@
         mRadioModule.setInternalHalCallback();
 
         doAnswer(invocation -> {
-            mHalCurrentInfo = AidlTestUtils.makeHalProgramSelector(
-                    (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0],
-                    mSignalQuality);
+            android.hardware.broadcastradio.ProgramSelector halSel =
+                    (android.hardware.broadcastradio.ProgramSelector) invocation.getArguments()[0];
+            mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY);
+            if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ) {
+                throw new ServiceSpecificException(Result.NOT_SUPPORTED);
+            }
             mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return null;
+            return Result.OK;
         }).when(mBroadcastRadioMock).tune(any());
 
         doAnswer(invocation -> {
             if ((boolean) invocation.getArguments()[0]) {
-                mHalCurrentInfo.selector.primaryId.value += mAmfmFrequencySpacing;
+                mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING;
             } else {
-                mHalCurrentInfo.selector.primaryId.value -= mAmfmFrequencySpacing;
+                mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING;
             }
             mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
             mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
             mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return null;
+            return Result.OK;
         }).when(mBroadcastRadioMock).step(anyBoolean());
 
         doAnswer(invocation -> {
+            if (mHalCurrentInfo == null) {
+                android.hardware.broadcastradio.ProgramSelector placeHolderSelector =
+                        AidlTestUtils.makeHalFmSelector(/* freq= */ 97300);
+
+                mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector);
+                return Result.OK;
+            }
             mHalCurrentInfo.selector.primaryId.value = getSeekFrequency(
                     mHalCurrentInfo.selector.primaryId.value,
                     !(boolean) invocation.getArguments()[0]);
             mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
             mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
             mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
-            return null;
+            return Result.OK;
         }).when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean());
 
         when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(null);
 
-        mHalConfigMap.clear();
         doAnswer(invocation -> {
             int configFlag = (int) invocation.getArguments()[0];
-            if (configFlag == mUnsupportedConfigFlag) {
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
                 throw new ServiceSpecificException(Result.NOT_SUPPORTED);
             }
             return mHalConfigMap.getOrDefault(configFlag, false);
         }).when(mBroadcastRadioMock).isConfigFlagSet(anyInt());
+
         doAnswer(invocation -> {
             int configFlag = (int) invocation.getArguments()[0];
-            if (configFlag == mUnsupportedConfigFlag) {
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
                 throw new ServiceSpecificException(Result.NOT_SUPPORTED);
             }
             mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]);
@@ -154,8 +162,13 @@
         }).when(mBroadcastRadioMock).setConfigFlag(anyInt(), anyBoolean());
     }
 
+    @After
+    public void cleanUp() {
+        mHalConfigMap.clear();
+    }
+
     @Test
-    public void openSession_withMultipleSessions() throws RemoteException {
+    public void openSession_withMultipleSessions() throws Exception {
         int numSessions = 3;
 
         openAidlClients(numSessions);
@@ -167,47 +180,47 @@
     }
 
     @Test
-    public void setConfiguration() throws RemoteException {
+    public void setConfiguration() throws Exception {
         openAidlClients(/* numClients= */ 1);
 
-        mTunerSessions[0].setConfiguration(mFmBandConfig);
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
 
-        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onConfigurationChanged(mFmBandConfig);
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onConfigurationChanged(FM_BAND_CONFIG);
     }
 
     @Test
-    public void getConfiguration() throws RemoteException {
+    public void getConfiguration() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        mTunerSessions[0].setConfiguration(mFmBandConfig);
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
 
         RadioManager.BandConfig config = mTunerSessions[0].getConfiguration();
 
         assertWithMessage("Session configuration").that(config)
-                .isEqualTo(mFmBandConfig);
+                .isEqualTo(FM_BAND_CONFIG);
     }
 
     @Test
-    public void setMuted_withUnmuted() throws RemoteException {
+    public void setMuted_withUnmuted() throws Exception {
         openAidlClients(/* numClients= */ 1);
 
         mTunerSessions[0].setMuted(/* mute= */ false);
 
-        assertWithMessage("Session mute state after setting muted %s", false)
+        assertWithMessage("Session mute state after setting unmuted")
                 .that(mTunerSessions[0].isMuted()).isFalse();
     }
 
     @Test
-    public void setMuted_withMuted() throws RemoteException {
+    public void setMuted_withMuted() throws Exception {
         openAidlClients(/* numClients= */ 1);
 
         mTunerSessions[0].setMuted(/* mute= */ true);
 
-        assertWithMessage("Session mute state after setting muted %s", true)
+        assertWithMessage("Session mute state after setting muted")
                 .that(mTunerSessions[0].isMuted()).isTrue();
     }
 
     @Test
-    public void close_withOneSession() throws RemoteException {
+    public void close_withOneSession() throws Exception {
         openAidlClients(/* numClients= */ 1);
 
         mTunerSessions[0].close();
@@ -217,7 +230,7 @@
     }
 
     @Test
-    public void close_withOnlyOneSession_withMultipleSessions() throws RemoteException {
+    public void close_withOnlyOneSession_withMultipleSessions() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
         int closeIdx = 0;
@@ -238,7 +251,7 @@
     }
 
     @Test
-    public void close_withOneSession_withError() throws RemoteException {
+    public void close_withOneSession_withError() throws Exception {
         openAidlClients(/* numClients= */ 1);
         int errorCode = RadioTuner.ERROR_SERVER_DIED;
 
@@ -250,7 +263,7 @@
     }
 
     @Test
-    public void closeSessions_withMultipleSessions_withError() throws RemoteException {
+    public void closeSessions_withMultipleSessions_withError() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
 
@@ -265,11 +278,11 @@
     }
 
     @Test
-    public void tune_withOneSession() throws RemoteException {
+    public void tune_withOneSession() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(mAmfmFrequencyList[1]);
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
         RadioManager.ProgramInfo tuneInfo =
-                AidlTestUtils.makeProgramInfo(initialSel, mSignalQuality);
+                AidlTestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
 
         mTunerSessions[0].tune(initialSel);
 
@@ -277,12 +290,12 @@
     }
 
     @Test
-    public void tune_withMultipleSessions() throws RemoteException {
+    public void tune_withMultipleSessions() throws Exception {
         int numSessions = 3;
         openAidlClients(numSessions);
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(mAmfmFrequencyList[1]);
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
         RadioManager.ProgramInfo tuneInfo =
-                AidlTestUtils.makeProgramInfo(initialSel, mSignalQuality);
+                AidlTestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
 
         mTunerSessions[0].tune(initialSel);
 
@@ -293,15 +306,29 @@
     }
 
     @Test
-    public void step_withDirectionUp() throws RemoteException {
-        long initFreq = mAmfmFrequencyList[1];
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(initFreq);
-        RadioManager.ProgramInfo stepUpInfo = AidlTestUtils.makeProgramInfo(
-                AidlTestUtils.makeFMSelector(initFreq + mAmfmFrequencySpacing),
-                mSignalQuality);
+    public void tune_withUnsupportedSelector_throwsException() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        mHalCurrentInfo = AidlTestUtils.makeHalProgramSelector(
-                ConversionUtils.programSelectorToHalProgramSelector(initialSel), mSignalQuality);
+        ProgramSelector unsupportedSelector = AidlTestUtils.makeProgramSelector(
+                ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300));
+
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> mTunerSessions[0].tune(unsupportedSelector));
+
+        assertWithMessage("Exception for tuning on unsupported program selector")
+                .that(thrown).hasMessageThat().contains("tune: NOT_SUPPORTED");
+    }
+
+    @Test
+    public void step_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo stepUpInfo = AidlTestUtils.makeProgramInfo(
+                AidlTestUtils.makeFmSelector(initFreq + AM_FM_FREQUENCY_SPACING),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+                ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
         mTunerSessions[0].step(/* directionDown= */ false, /* skipSubChannel= */ false);
 
@@ -310,15 +337,15 @@
     }
 
     @Test
-    public void step_withDirectionDown() throws RemoteException {
-        long initFreq = mAmfmFrequencyList[1];
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(initFreq);
+    public void step_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
         RadioManager.ProgramInfo stepDownInfo = AidlTestUtils.makeProgramInfo(
-                AidlTestUtils.makeFMSelector(initFreq - mAmfmFrequencySpacing),
-                mSignalQuality);
+                AidlTestUtils.makeFmSelector(initFreq - AM_FM_FREQUENCY_SPACING),
+                SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
-        mHalCurrentInfo = AidlTestUtils.makeHalProgramSelector(
-                ConversionUtils.programSelectorToHalProgramSelector(initialSel), mSignalQuality);
+        mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+                ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
         mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
 
@@ -327,15 +354,15 @@
     }
 
     @Test
-    public void scan_withDirectionUp() throws RemoteException {
-        long initFreq = mAmfmFrequencyList[2];
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(initFreq);
+    public void scan_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
         RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
-                AidlTestUtils.makeFMSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
-                mSignalQuality);
+                AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
+                SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
-        mHalCurrentInfo = AidlTestUtils.makeHalProgramSelector(
-                ConversionUtils.programSelectorToHalProgramSelector(initialSel), mSignalQuality);
+        mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+                ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
         mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
 
@@ -344,15 +371,28 @@
     }
 
     @Test
-    public void scan_withDirectionDown() throws RemoteException {
-        long initFreq = mAmfmFrequencyList[2];
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(initFreq);
+    public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+        int numSessions = 2;
+        openAidlClients(numSessions);
+
+        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onTuneFailed(eq(Result.TIMEOUT), any());
+        }
+    }
+
+    @Test
+    public void scan_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
         RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
-                AidlTestUtils.makeFMSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
-                mSignalQuality);
+                AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
+                SIGNAL_QUALITY);
         openAidlClients(/* numClients= */ 1);
-        mHalCurrentInfo = AidlTestUtils.makeHalProgramSelector(
-                ConversionUtils.programSelectorToHalProgramSelector(initialSel), mSignalQuality);
+        mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
+                ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
 
         mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
@@ -360,9 +400,9 @@
     }
 
     @Test
-    public void cancel() throws RemoteException {
+    public void cancel() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        ProgramSelector initialSel = AidlTestUtils.makeFMSelector(mAmfmFrequencyList[1]);
+        ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
         mTunerSessions[0].tune(initialSel);
 
         mTunerSessions[0].cancel();
@@ -371,7 +411,7 @@
     }
 
     @Test
-    public void getImage_withInvalidId_throwsIllegalArgumentException() throws RemoteException {
+    public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception {
         openAidlClients(/* numClients= */ 1);
         int imageId = IBroadcastRadio.INVALID_IMAGE;
 
@@ -379,12 +419,12 @@
             mTunerSessions[0].getImage(imageId);
         });
 
-        assertWithMessage("Exception for getting image with invalid ID")
+        assertWithMessage("Get image exception")
                 .that(thrown).hasMessageThat().contains("Image ID is missing");
     }
 
     @Test
-    public void getImage_withValidId() throws RemoteException {
+    public void getImage_withValidId() throws Exception {
         openAidlClients(/* numClients= */ 1);
         int imageId = 1;
 
@@ -394,7 +434,7 @@
     }
 
     @Test
-    public void startBackgroundScan() throws RemoteException {
+    public void startBackgroundScan() throws Exception {
         openAidlClients(/* numClients= */ 1);
 
         mTunerSessions[0].startBackgroundScan();
@@ -403,7 +443,7 @@
     }
 
     @Test
-    public void stopProgramListUpdates() throws RemoteException {
+    public void stopProgramListUpdates() throws Exception {
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ false);
@@ -415,20 +455,20 @@
     }
 
     @Test
-    public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws RemoteException {
+    public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag;
+        int flag = UNSUPPORTED_CONFIG_FLAG;
 
         boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
 
         verify(mBroadcastRadioMock).isConfigFlagSet(flag);
-        assertWithMessage("Config  flag %s is supported", flag).that(isSupported).isFalse();
+        assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
     }
 
     @Test
-    public void isConfigFlagSupported_withSupportedFlag_returnsTrue() throws RemoteException {
+    public void isConfigFlagSupported_withSupportedFlag_returnsTrue() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag + 1;
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
 
         boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
 
@@ -437,9 +477,9 @@
     }
 
     @Test
-    public void setConfigFlag_withUnsupportedFlag_throwsRuntimeException() throws RemoteException {
+    public void setConfigFlag_withUnsupportedFlag_throwsRuntimeException() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag;
+        int flag = UNSUPPORTED_CONFIG_FLAG;
 
         RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
             mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
@@ -450,9 +490,9 @@
     }
 
     @Test
-    public void setConfigFlag_withFlagSetToTrue() throws RemoteException {
+    public void setConfigFlag_withFlagSetToTrue() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag + 1;
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
 
         mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
 
@@ -460,9 +500,9 @@
     }
 
     @Test
-    public void setConfigFlag_withFlagSetToFalse() throws RemoteException {
+    public void setConfigFlag_withFlagSetToFalse() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag + 1;
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
 
         mTunerSessions[0].setConfigFlag(flag, /* value= */ false);
 
@@ -471,9 +511,9 @@
 
     @Test
     public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException()
-            throws RemoteException {
+            throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag;
+        int flag = UNSUPPORTED_CONFIG_FLAG;
 
         RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
             mTunerSessions[0].isConfigFlagSet(flag);
@@ -484,9 +524,9 @@
     }
 
     @Test
-    public void isConfigFlagSet_withSupportedFlag() throws RemoteException {
+    public void isConfigFlagSet_withSupportedFlag() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        int flag = mUnsupportedConfigFlag + 1;
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
         boolean expectedConfigFlagValue = true;
         mTunerSessions[0].setConfigFlag(flag, /* value= */ expectedConfigFlagValue);
 
@@ -497,11 +537,10 @@
     }
 
     @Test
-    public void setParameters_withMockParameters() throws RemoteException {
+    public void setParameters_withMockParameters() throws Exception {
         openAidlClients(/* numClients= */ 1);
-        Map<String, String> parametersSet = new ArrayMap<>();
-        parametersSet.put("mockParam1", "mockValue1");
-        parametersSet.put("mockParam2", "mockValue2");
+        Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
+                "mockParam2", "mockValue2");
 
         mTunerSessions[0].setParameters(parametersSet);
 
@@ -510,7 +549,7 @@
     }
 
     @Test
-    public void getParameters_withMockKeys() throws RemoteException {
+    public void getParameters_withMockKeys() throws Exception {
         openAidlClients(/* numClients= */ 1);
         List<String> parameterKeys = new ArrayList<>(2);
         parameterKeys.add("mockKey1");
@@ -522,7 +561,36 @@
                 parameterKeys.toArray(new String[0]));
     }
 
-    private void openAidlClients(int numClients) throws RemoteException {
+    @Test
+    public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+
+        mHalTunerCallback.onAntennaStateChange(/* connected= */ false);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onAntennaState(/* connected= */ false);
+        }
+    }
+
+    @Test
+    public void onParametersUpdated_forTunerCallback() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        VendorKeyValue[] parametersUpdates = {
+                AidlTestUtils.makeVendorKeyValue("com.vendor.parameter1", "value1")};
+        Map<String, String> parametersExpected = Map.of("com.vendor.parameter1", "value1");
+
+        mHalTunerCallback.onParametersUpdated(parametersUpdates);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onParametersUpdated(parametersExpected);
+        }
+    }
+
+    private void openAidlClients(int numClients) throws Exception {
         mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
         mTunerSessions = new TunerSession[numClients];
         for (int index = 0; index < numClients; index++) {
@@ -534,18 +602,18 @@
     private long getSeekFrequency(long currentFrequency, boolean seekDown) {
         long seekFrequency;
         if (seekDown) {
-            seekFrequency = mAmfmFrequencyList[mAmfmFrequencyList.length - 1];
-            for (int i = mAmfmFrequencyList.length - 1; i >= 0; i--) {
-                if (mAmfmFrequencyList[i] < currentFrequency) {
-                    seekFrequency = mAmfmFrequencyList[i];
+            seekFrequency = AM_FM_FREQUENCY_LIST[AM_FM_FREQUENCY_LIST.length - 1];
+            for (int i = AM_FM_FREQUENCY_LIST.length - 1; i >= 0; i--) {
+                if (AM_FM_FREQUENCY_LIST[i] < currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[i];
                     break;
                 }
             }
         } else {
-            seekFrequency = mAmfmFrequencyList[0];
-            for (int index = 0; index < mAmfmFrequencyList.length; index++) {
-                if (mAmfmFrequencyList[index] > currentFrequency) {
-                    seekFrequency = mAmfmFrequencyList[index];
+            seekFrequency = AM_FM_FREQUENCY_LIST[0];
+            for (int index = 0; index < AM_FM_FREQUENCY_LIST.length; index++) {
+                if (AM_FM_FREQUENCY_LIST[index] > currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[index];
                     break;
                 }
             }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java
new file mode 100644
index 0000000..b68e65f
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.hal2;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for HIDL 2.0 HAL AnnouncementAggregator.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class AnnouncementAggregatorHidlTest {
+
+    private static final int[] TEST_ENABLED_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private final Object mLock = new Object();
+    private AnnouncementAggregator mAnnouncementAggregator;
+    private IBinder.DeathRecipient mDeathRecipient;
+
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private IBinder mBinderMock;
+    private RadioModule[] mRadioModuleMocks;
+    private ICloseHandle[] mCloseHandleMocks;
+    private Announcement[] mAnnouncementMocks;
+
+    @Before
+    public void setUp() throws Exception {
+        ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
+                ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
+        when(mListenerMock.asBinder()).thenReturn(mBinderMock);
+
+        mAnnouncementAggregator = new AnnouncementAggregator(mListenerMock, mLock);
+
+        verify(mBinderMock).linkToDeath(deathRecipientCaptor.capture(), eq(0));
+        mDeathRecipient = deathRecipientCaptor.getValue();
+    }
+
+    @Test
+    public void onListUpdated_withOneModuleWatcher() throws Exception {
+        ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor =
+                ArgumentCaptor.forClass(IAnnouncementListener.class);
+        watchModules(/* moduleNumber= */ 1);
+
+        verify(mRadioModuleMocks[0]).addAnnouncementListener(any(), moduleWatcherCaptor.capture());
+
+        moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[0]));
+
+        verify(mListenerMock).onListUpdated(any());
+    }
+
+    @Test
+    public void onListUpdated_withMultipleModuleWatchers() throws Exception {
+        int moduleNumber = 3;
+        watchModules(moduleNumber);
+
+        for (int index = 0; index < moduleNumber; index++) {
+            ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor =
+                    ArgumentCaptor.forClass(IAnnouncementListener.class);
+            ArgumentCaptor<List<Announcement>> announcementsCaptor =
+                    ArgumentCaptor.forClass(List.class);
+            verify(mRadioModuleMocks[index])
+                    .addAnnouncementListener(any(), moduleWatcherCaptor.capture());
+
+            moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[index]));
+
+            verify(mListenerMock, times(index + 1)).onListUpdated(announcementsCaptor.capture());
+            assertWithMessage("Number of announcements %s after %s announcements were updated",
+                    announcementsCaptor.getValue(), index + 1)
+                    .that(announcementsCaptor.getValue().size()).isEqualTo(index + 1);
+        }
+    }
+
+    @Test
+    public void close_withOneModuleWatcher_invokesCloseHandle() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mAnnouncementAggregator.close();
+
+        verify(mCloseHandleMocks[0]).close();
+        verify(mBinderMock).unlinkToDeath(mDeathRecipient, 0);
+    }
+
+    @Test
+    public void close_withMultipleModuleWatcher_invokesCloseHandles() throws Exception {
+        int moduleNumber = 3;
+        watchModules(moduleNumber);
+
+        mAnnouncementAggregator.close();
+
+        for (int index = 0; index < moduleNumber; index++) {
+            verify(mCloseHandleMocks[index]).close();
+        }
+    }
+
+    @Test
+    public void close_twice_invokesCloseHandleOnce() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mAnnouncementAggregator.close();
+        mAnnouncementAggregator.close();
+
+        verify(mCloseHandleMocks[0]).close();
+        verify(mBinderMock).unlinkToDeath(mDeathRecipient, 0);
+    }
+
+    @Test
+    public void binderDied_forDeathRecipient_invokesCloseHandle() throws Exception {
+        watchModules(/* moduleNumber= */ 1);
+
+        mDeathRecipient.binderDied();
+
+        verify(mCloseHandleMocks[0]).close();
+
+    }
+
+    private void watchModules(int moduleNumber) throws RemoteException {
+        mRadioModuleMocks = new RadioModule[moduleNumber];
+        mCloseHandleMocks = new ICloseHandle[moduleNumber];
+        mAnnouncementMocks = new Announcement[moduleNumber];
+
+        for (int index = 0; index < moduleNumber; index++) {
+            mRadioModuleMocks[index] = mock(RadioModule.class);
+            mCloseHandleMocks[index] = mock(ICloseHandle.class);
+            mAnnouncementMocks[index] = mock(Announcement.class);
+
+            when(mRadioModuleMocks[index].addAnnouncementListener(any(), any()))
+                    .thenReturn(mCloseHandleMocks[index]);
+            mAnnouncementAggregator.watchModule(mRadioModuleMocks[index], TEST_ENABLED_TYPES);
+        }
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java
new file mode 100644
index 0000000..4d0b753
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.hal2;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.hidl.manager.V1_0.IServiceManager;
+import android.hidl.manager.V1_0.IServiceNotification;
+import android.os.IBinder;
+import android.os.IHwBinder.DeathRecipient;
+import android.os.RemoteException;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
+
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public final class BroadcastRadioServiceHidlTest extends ExtendedRadioMockitoTestCase {
+
+    private static final int FM_RADIO_MODULE_ID = 0;
+    private static final int DAB_RADIO_MODULE_ID = 1;
+    private static final ArrayList<String> SERVICE_LIST =
+            new ArrayList<>(Arrays.asList("FmService", "DabService"));
+    private static final int[] TEST_ENABLED_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private final Object mLock = new Object();
+
+    private BroadcastRadioService mBroadcastRadioService;
+    private DeathRecipient mFmDeathRecipient;
+
+    @Mock
+    private IServiceManager mServiceManagerMock;
+    @Mock
+    private RadioManager.ModuleProperties mFmModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mDabModuleMock;
+    @Mock
+    private RadioModule mFmRadioModuleMock;
+    @Mock
+    private RadioModule mDabRadioModuleMock;
+    @Mock
+    private IBroadcastRadio mFmHalServiceMock;
+    @Mock
+    private IBroadcastRadio mDabHalServiceMock;
+    @Mock
+    private TunerSession mFmTunerSessionMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private ICloseHandle mFmCloseHandleMock;
+    @Mock
+    private ICloseHandle mDabCloseHandleMock;
+    @Mock
+    private IAnnouncementListener mAnnouncementListenerMock;
+    @Mock
+    private IBinder mBinderMock;
+
+    @Override
+    protected void initializeSession(StaticMockitoSessionBuilder builder) {
+        builder.spyStatic(RadioModule.class);
+    }
+
+    @Test
+    public void listModules_withMultipleServiceNames() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Radio modules in HIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.listModules())
+                .containsExactly(mFmModuleMock, mDabModuleMock);
+    }
+
+    @Test
+    public void hasModules_withIdFoundInModules() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("DAB radio module in HIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasModule(FM_RADIO_MODULE_ID)).isTrue();
+    }
+
+    @Test
+    public void hasModules_withIdNotFoundInModules() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Radio module of id not found in HIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasModule(DAB_RADIO_MODULE_ID + 1)).isFalse();
+    }
+
+    @Test
+    public void hasAnyModules_withModulesExist() throws Exception {
+        createBroadcastRadioService();
+
+        assertWithMessage("Any radio module in HIDL broadcast radio HAL client")
+                .that(mBroadcastRadioService.hasAnyModules()).isTrue();
+    }
+
+    @Test
+    public void openSession_withIdFound() throws Exception {
+        createBroadcastRadioService();
+
+        ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
+                /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Session opened in FM radio module")
+                .that(session).isEqualTo(mFmTunerSessionMock);
+    }
+
+    @Test
+    public void openSession_withIdNotFound() throws Exception {
+        createBroadcastRadioService();
+        int moduleIdInvalid = DAB_RADIO_MODULE_ID + 1;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            mBroadcastRadioService.openSession(moduleIdInvalid, /* legacyConfig= */ null,
+                    /* withAudio= */ true, mTunerCallbackMock);
+        });
+
+        assertWithMessage("Exception for opening session with module id %s", moduleIdInvalid)
+                .that(thrown).hasMessageThat().contains("Invalid module ID");
+    }
+
+    @Test
+    public void addAnnouncementListener_addsOnAllRadioModules() throws Exception {
+        createBroadcastRadioService();
+        when(mAnnouncementListenerMock.asBinder()).thenReturn(mBinderMock);
+        when(mFmRadioModuleMock.addAnnouncementListener(any(), any()))
+                .thenReturn(mFmCloseHandleMock);
+        when(mDabRadioModuleMock.addAnnouncementListener(any(), any()))
+                .thenReturn(mDabCloseHandleMock);
+
+        mBroadcastRadioService.addAnnouncementListener(TEST_ENABLED_TYPES,
+                mAnnouncementListenerMock);
+
+        verify(mFmRadioModuleMock).addAnnouncementListener(any(), any());
+        verify(mDabRadioModuleMock).addAnnouncementListener(any(), any());
+    }
+
+    @Test
+    public void binderDied_forDeathRecipient() throws Exception {
+        createBroadcastRadioService();
+
+        mFmDeathRecipient.serviceDied(FM_RADIO_MODULE_ID);
+
+        verify(mFmRadioModuleMock).closeSessions(eq(RadioTuner.ERROR_HARDWARE_FAILURE));
+        assertWithMessage("FM radio module after FM broadcast radio HAL service died")
+                .that(mBroadcastRadioService.hasModule(FM_RADIO_MODULE_ID)).isFalse();
+    }
+
+    private void createBroadcastRadioService() throws RemoteException {
+        mockServiceManager();
+        mBroadcastRadioService = new BroadcastRadioService(/* nextModuleId= */ FM_RADIO_MODULE_ID,
+                mLock, mServiceManagerMock);
+    }
+
+    private void mockServiceManager() throws RemoteException {
+        doAnswer(invocation -> {
+            mFmDeathRecipient = (DeathRecipient) invocation.getArguments()[0];
+            return null;
+        }).when(mFmHalServiceMock).linkToDeath(any(), eq((long) FM_RADIO_MODULE_ID));
+
+        when(mServiceManagerMock.registerForNotifications(anyString(), anyString(),
+                any(IServiceNotification.class))).thenAnswer(invocation -> {
+                    IServiceNotification serviceCallback =
+                            (IServiceNotification) invocation.getArguments()[2];
+                    for (int index = 0; index < SERVICE_LIST.size(); index++) {
+                        serviceCallback.onRegistration(IBroadcastRadio.kInterfaceName,
+                                SERVICE_LIST.get(index), /* b= */ false);
+                    }
+                    return true;
+                }).thenReturn(true);
+
+        doReturn(mFmRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+                eq(FM_RADIO_MODULE_ID), anyString(), any(Object.class)));
+        doReturn(mDabRadioModuleMock).when(() -> RadioModule.tryLoadingModule(
+                eq(DAB_RADIO_MODULE_ID), anyString(), any(Object.class)));
+
+        when(mFmRadioModuleMock.getProperties()).thenReturn(mFmModuleMock);
+        when(mDabRadioModuleMock.getProperties()).thenReturn(mDabModuleMock);
+
+        when(mFmRadioModuleMock.getService()).thenReturn(mFmHalServiceMock);
+        when(mDabRadioModuleMock.getService()).thenReturn(mDabHalServiceMock);
+
+        when(mFmRadioModuleMock.openSession(mTunerCallbackMock))
+                .thenReturn(mFmTunerSessionMock);
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertTest.java
new file mode 100644
index 0000000..3de4f5d
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.hal2;
+
+import android.hardware.broadcastradio.V2_0.AmFmBandRange;
+import android.hardware.broadcastradio.V2_0.AmFmRegionConfig;
+import android.hardware.broadcastradio.V2_0.DabTableEntry;
+import android.hardware.broadcastradio.V2_0.IdentifierType;
+import android.hardware.broadcastradio.V2_0.Properties;
+import android.hardware.broadcastradio.V2_0.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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public final class ConvertTest {
+
+    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 =
+            Convert.announcementFromHal(
+                    TestUtils.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();
+        List<DabTableEntry> dabTableEntries = Arrays.asList(
+                createDabTableEntry(DAB_ENTRY_LABEL_1, DAB_ENTRY_FREQUENCY_1),
+                createDabTableEntry(DAB_ENTRY_LABEL_2, DAB_ENTRY_FREQUENCY_2));
+        Properties properties = createHalProperties();
+
+        return Convert.propertiesFromHal(TEST_ID, TEST_SERVICE_NAME, properties,
+                amFmConfig, dabTableEntries);
+    }
+
+    private static AmFmRegionConfig createAmFmRegionConfig() {
+        AmFmRegionConfig amFmRegionConfig = new AmFmRegionConfig();
+        amFmRegionConfig.ranges = new ArrayList<AmFmBandRange>(Arrays.asList(
+                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.scanSpacing = bandRange.spacing;
+        return bandRange;
+    }
+
+    private static DabTableEntry createDabTableEntry(String label, int value) {
+        DabTableEntry dabTableEntry = new DabTableEntry();
+        dabTableEntry.label = label;
+        dabTableEntry.frequency = value;
+        return dabTableEntry;
+    }
+
+    private static Properties createHalProperties() {
+        Properties halProperties = new Properties();
+        halProperties.supportedIdentifierTypes = new ArrayList<Integer>(Arrays.asList(
+                IdentifierType.AMFM_FREQUENCY, 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 ArrayList<VendorKeyValue>(Arrays.asList(
+                TestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_1, VENDOR_INFO_VALUE_1),
+                TestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_2, VENDOR_INFO_VALUE_2)));
+        return halProperties;
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java
new file mode 100644
index 0000000..48f5a46
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/RadioModuleHidlTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.hal2;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.V2_0.Constants;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.RadioManager;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Tests for HIDL HAL RadioModule.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class RadioModuleHidlTest {
+
+    private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EVENT;
+    private static final RadioManager.ModuleProperties TEST_MODULE_PROPERTIES =
+            TestUtils.makeDefaultModuleProperties();
+
+    @Mock
+    private IBroadcastRadio mBroadcastRadioMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private android.hardware.broadcastradio.V2_0.ICloseHandle mHalCloseHandleMock;
+
+    private final Object mLock = new Object();
+    private RadioModule mRadioModule;
+    private android.hardware.broadcastradio.V2_0.IAnnouncementListener mHalListener;
+
+    @Before
+    public void setup() throws RemoteException {
+        mRadioModule = new RadioModule(mBroadcastRadioMock, TEST_MODULE_PROPERTIES, mLock);
+
+        when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(new ArrayList<Byte>(0));
+
+        doAnswer(invocation -> {
+            mHalListener = (android.hardware.broadcastradio.V2_0.IAnnouncementListener) invocation
+                    .getArguments()[1];
+            IBroadcastRadio.registerAnnouncementListenerCallback cb =
+                    (IBroadcastRadio.registerAnnouncementListenerCallback)
+                            invocation.getArguments()[2];
+            cb.onValues(Result.OK, mHalCloseHandleMock);
+            return null;
+        }).when(mBroadcastRadioMock).registerAnnouncementListener(any(), any(), any());
+    }
+
+    @Test
+    public void getService() {
+        assertWithMessage("Service of radio module")
+                .that(mRadioModule.getService()).isEqualTo(mBroadcastRadioMock);
+    }
+
+    @Test
+    public void getProperties() {
+        assertWithMessage("Module properties of radio module")
+                .that(mRadioModule.getProperties()).isEqualTo(TEST_MODULE_PROPERTIES);
+    }
+
+    @Test
+    public void getImage_withValidIdFromRadioModule() {
+        int imageId = 1;
+
+        Bitmap imageTest = mRadioModule.getImage(imageId);
+
+        assertWithMessage("Image from radio module").that(imageTest).isNull();
+    }
+
+    @Test
+    public void getImage_withInvalidIdFromRadioModule_throwsIllegalArgumentException() {
+        int invalidImageId = Constants.INVALID_IMAGE;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            mRadioModule.getImage(invalidImageId);
+        });
+
+        assertWithMessage("Exception for getting image with invalid ID")
+                .that(thrown).hasMessageThat().contains("Image ID is missing");
+    }
+
+    @Test
+    public void addAnnouncementListener_listenerRegistered() throws Exception {
+        ArrayList<Byte> enabledListExpected = new ArrayList<Byte>(Arrays.asList(
+                (byte) TEST_ENABLED_TYPE));
+        mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+        verify(mBroadcastRadioMock)
+                .registerAnnouncementListener(eq(enabledListExpected), any(), any());
+    }
+
+    @Test
+    public void onListUpdate_forAnnouncementListener() throws Exception {
+        android.hardware.broadcastradio.V2_0.Announcement halAnnouncement =
+                TestUtils.makeAnnouncement(TEST_ENABLED_TYPE, /* selectorFreq= */ 96300);
+        mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+        mHalListener.onListUpdated(
+                new ArrayList<android.hardware.broadcastradio.V2_0.Announcement>(
+                        Arrays.asList(halAnnouncement)));
+
+        verify(mListenerMock).onListUpdated(any());
+    }
+
+    @Test
+    public void close_forCloseHandle() throws Exception {
+        ICloseHandle closeHandle =
+                mRadioModule.addAnnouncementListener(new int[]{TEST_ENABLED_TYPE}, mListenerMock);
+
+        closeHandle.close();
+
+        verify(mHalCloseHandleMock).close();
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
index 25bf93f..d104359 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
@@ -95,9 +95,8 @@
     public void setup() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
-        mRadioModule = new RadioModule(mBroadcastRadioMock, new RadioManager.ModuleProperties(0, "",
-                  0, "", "", "", "", 0, 0, false, false, null, false, new int[] {}, new int[] {},
-                  null, null), mLock);
+        mRadioModule = new RadioModule(mBroadcastRadioMock,
+                TestUtils.makeDefaultModuleProperties(), mLock);
 
         doAnswer((Answer) invocation -> {
             mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
index 4944803..4eedd2f 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
@@ -15,22 +15,61 @@
  */
 package com.android.server.broadcastradio.hal2;
 
+import android.hardware.broadcastradio.V2_0.IdentifierType;
+import android.hardware.broadcastradio.V2_0.ProgramIdentifier;
 import android.hardware.broadcastradio.V2_0.ProgramInfo;
+import android.hardware.broadcastradio.V2_0.VendorKeyValue;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
+import android.util.ArrayMap;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 
 final class TestUtils {
+
+    private TestUtils() {
+        throw new UnsupportedOperationException("TestUtils class is noninstantiable");
+    }
+
+    static RadioManager.ModuleProperties makeDefaultModuleProperties() {
+        return new RadioManager.ModuleProperties(
+                /* id= */ 0, /* serviceName= */ "", /* classId= */ 0, /* implementor= */ "",
+                /* product= */ "", /* version= */ "", /* serial= */ "", /* numTuners= */ 0,
+                /* numAudioSources= */ 0, /* isInitializationRequired= */ false,
+                /* isCaptureSupported= */ false, /* bands= */ null,
+                /* isBgScanSupported= */ false, new int[] {}, new int[] {},
+                /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+    }
+
+    static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
+        return new RadioManager.ProgramInfo(selector,
+                selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null,
+                /* infoFlags= */ 0, signalQuality,
+                new RadioMetadata.Builder().build(), new ArrayMap<>());
+    }
+
     static RadioManager.ProgramInfo makeProgramInfo(int programType,
             ProgramSelector.Identifier identifier, int signalQuality) {
         // Note: If you set new fields, check if programInfoToHal() needs to be updated as well.
-        return new RadioManager.ProgramInfo(new ProgramSelector(programType, identifier, null,
-                null), null, null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
+        return new RadioManager.ProgramInfo(makeProgramSelector(programType, identifier), null,
+                null, null, 0, signalQuality, new RadioMetadata.Builder().build(),
                 new HashMap<String, String>());
     }
 
+    static ProgramSelector makeFmSelector(long freq) {
+        return makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
+                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+                        freq));
+    }
+
+    static ProgramSelector makeProgramSelector(int programType,
+            ProgramSelector.Identifier identifier) {
+        return new ProgramSelector(programType, identifier, /* secondaryIds= */ null,
+                /* vendorIds= */ null);
+    }
+
     static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) {
         // Note that because Convert does not by design provide functions for all conversions, this
         // function only copies fields that are set by makeProgramInfo().
@@ -39,4 +78,45 @@
         hwInfo.signalQuality = info.getSignalStrength();
         return hwInfo;
     }
+
+    static android.hardware.broadcastradio.V2_0.ProgramSelector makeHalFmSelector(int freq) {
+        ProgramIdentifier halId = new ProgramIdentifier();
+        halId.type = IdentifierType.AMFM_FREQUENCY;
+        halId.value = freq;
+
+        android.hardware.broadcastradio.V2_0.ProgramSelector halSelector =
+                new android.hardware.broadcastradio.V2_0.ProgramSelector();
+        halSelector.primaryId = halId;
+        halSelector.secondaryIds = new ArrayList<ProgramIdentifier>();
+        return halSelector;
+    }
+
+    static ProgramInfo makeHalProgramInfo(
+            android.hardware.broadcastradio.V2_0.ProgramSelector hwSel, int hwSignalQuality) {
+        ProgramInfo hwInfo = new ProgramInfo();
+        hwInfo.selector = hwSel;
+        hwInfo.logicallyTunedTo = hwSel.primaryId;
+        hwInfo.physicallyTunedTo = hwSel.primaryId;
+        hwInfo.signalQuality = hwSignalQuality;
+        hwInfo.relatedContent = new ArrayList<>();
+        hwInfo.metadata = new ArrayList<>();
+        return hwInfo;
+    }
+
+    static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) {
+        VendorKeyValue vendorKeyValue = new VendorKeyValue();
+        vendorKeyValue.key = vendorKey;
+        vendorKeyValue.value = vendorValue;
+        return vendorKeyValue;
+    }
+
+    static android.hardware.broadcastradio.V2_0.Announcement makeAnnouncement(int type,
+            int selectorFreq) {
+        android.hardware.broadcastradio.V2_0.Announcement halAnnouncement =
+                new android.hardware.broadcastradio.V2_0.Announcement();
+        halAnnouncement.type = (byte) type;
+        halAnnouncement.selector = makeHalFmSelector(selectorFreq);
+        halAnnouncement.vendorInfo = new ArrayList<>();
+        return halAnnouncement;
+    }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
new file mode 100644
index 0000000..936e606
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
@@ -0,0 +1,622 @@
+/*
+ * 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.hal2;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.V2_0.Constants;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.ITunerCallback;
+import android.hardware.broadcastradio.V2_0.ITunerSession;
+import android.hardware.broadcastradio.V2_0.IdentifierType;
+import android.hardware.broadcastradio.V2_0.ProgramInfo;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.broadcastradio.V2_0.VendorKeyValue;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.verification.VerificationWithTimeout;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Tests for HIDL HAL TunerSession.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class TunerSessionHidlTest {
+
+    private static final VerificationWithTimeout CALLBACK_TIMEOUT =
+            timeout(/* millis= */ 200);
+    private static final int SIGNAL_QUALITY = 1;
+    private static final long AM_FM_FREQUENCY_SPACING = 500;
+    private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100};
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
+            new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
+                    /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100,
+                    /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false,
+                    /* ea= */ false);
+    private static final RadioManager.BandConfig FM_BAND_CONFIG =
+            new RadioManager.FmBandConfig(FM_BAND_DESCRIPTOR);
+    private static final int UNSUPPORTED_CONFIG_FLAG = 0;
+
+    private final Object mLock = new Object();
+    private final ArrayMap<Integer, Boolean> mHalConfigMap = new ArrayMap<>();
+    private RadioModule mRadioModule;
+    private ITunerCallback mHalTunerCallback;
+    private ProgramInfo mHalCurrentInfo;
+    private TunerSession[] mTunerSessions;
+
+    @Mock private IBroadcastRadio mBroadcastRadioMock;
+    @Mock ITunerSession mHalTunerSessionMock;
+    private android.hardware.radio.ITunerCallback[] mAidlTunerCallbackMocks;
+
+    @Before
+    public void setup() throws Exception {
+        mRadioModule = new RadioModule(mBroadcastRadioMock,
+                TestUtils.makeDefaultModuleProperties(), mLock);
+
+        doAnswer(invocation -> {
+            mHalTunerCallback = (ITunerCallback) invocation.getArguments()[0];
+            IBroadcastRadio.openSessionCallback cb = (IBroadcastRadio.openSessionCallback)
+                    invocation.getArguments()[1];
+            cb.onValues(Result.OK, mHalTunerSessionMock);
+            return null;
+        }).when(mBroadcastRadioMock).openSession(any(), any());
+
+        doAnswer(invocation -> {
+            android.hardware.broadcastradio.V2_0.ProgramSelector halSel =
+                    (android.hardware.broadcastradio.V2_0.ProgramSelector)
+                            invocation.getArguments()[0];
+            mHalCurrentInfo = TestUtils.makeHalProgramInfo(halSel, SIGNAL_QUALITY);
+            if (halSel.primaryId.type != IdentifierType.AMFM_FREQUENCY) {
+                return Result.NOT_SUPPORTED;
+            }
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).tune(any());
+
+        doAnswer(invocation -> {
+            if ((boolean) invocation.getArguments()[0]) {
+                mHalCurrentInfo.selector.primaryId.value += AM_FM_FREQUENCY_SPACING;
+            } else {
+                mHalCurrentInfo.selector.primaryId.value -= AM_FM_FREQUENCY_SPACING;
+            }
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).step(anyBoolean());
+
+        doAnswer(invocation -> {
+            if (mHalCurrentInfo == null) {
+                android.hardware.broadcastradio.V2_0.ProgramSelector placeHolderSelector =
+                        TestUtils.makeHalFmSelector(/* freq= */ 97300);
+
+                mHalTunerCallback.onTuneFailed(Result.TIMEOUT, placeHolderSelector);
+                return Result.OK;
+            }
+            mHalCurrentInfo.selector.primaryId.value = getSeekFrequency(
+                    mHalCurrentInfo.selector.primaryId.value,
+                    !(boolean) invocation.getArguments()[0]);
+            mHalCurrentInfo.logicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalCurrentInfo.physicallyTunedTo = mHalCurrentInfo.selector.primaryId;
+            mHalTunerCallback.onCurrentProgramInfoChanged(mHalCurrentInfo);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).scan(anyBoolean(), anyBoolean());
+
+        when(mBroadcastRadioMock.getImage(anyInt())).thenReturn(new ArrayList<Byte>(0));
+
+        doAnswer(invocation -> {
+            int configFlag = (int) invocation.getArguments()[0];
+            ITunerSession.isConfigFlagSetCallback cb = (ITunerSession.isConfigFlagSetCallback)
+                    invocation.getArguments()[1];
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+                cb.onValues(Result.NOT_SUPPORTED, false);
+                return null;
+            }
+            cb.onValues(Result.OK, mHalConfigMap.getOrDefault(configFlag, false));
+            return null;
+        }).when(mHalTunerSessionMock).isConfigFlagSet(anyInt(), any());
+
+        doAnswer(invocation -> {
+            int configFlag = (int) invocation.getArguments()[0];
+            if (configFlag == UNSUPPORTED_CONFIG_FLAG) {
+                return Result.NOT_SUPPORTED;
+            }
+            mHalConfigMap.put(configFlag, (boolean) invocation.getArguments()[1]);
+            return Result.OK;
+        }).when(mHalTunerSessionMock).setConfigFlag(anyInt(), anyBoolean());
+    }
+
+    @After
+    public void cleanUp() {
+        mHalConfigMap.clear();
+    }
+
+    @Test
+    public void openSession_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+
+        openAidlClients(numSessions);
+
+        for (int index = 0; index < numSessions; index++) {
+            assertWithMessage("Session of index %s close state", index)
+                    .that(mTunerSessions[index].isClosed()).isFalse();
+        }
+    }
+
+    @Test
+    public void setConfiguration() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onConfigurationChanged(FM_BAND_CONFIG);
+    }
+
+    @Test
+    public void getConfiguration() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        mTunerSessions[0].setConfiguration(FM_BAND_CONFIG);
+
+        RadioManager.BandConfig config = mTunerSessions[0].getConfiguration();
+
+        assertWithMessage("Session configuration").that(config)
+                .isEqualTo(FM_BAND_CONFIG);
+    }
+
+    @Test
+    public void setMuted_withUnmuted() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setMuted(/* mute= */ false);
+
+        assertWithMessage("Session mute state after setting unmuted")
+                .that(mTunerSessions[0].isMuted()).isFalse();
+    }
+
+    @Test
+    public void setMuted_withMuted() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].setMuted(/* mute= */ true);
+
+        assertWithMessage("Session mute state after setting muted")
+                .that(mTunerSessions[0].isMuted()).isTrue();
+    }
+
+    @Test
+    public void close_withOneSession() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].close();
+
+        assertWithMessage("Close state of broadcast radio service session")
+                .that(mTunerSessions[0].isClosed()).isTrue();
+    }
+
+    @Test
+    public void close_withOnlyOneSession_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        int closeIdx = 0;
+
+        mTunerSessions[closeIdx].close();
+
+        for (int index = 0; index < numSessions; index++) {
+            if (index == closeIdx) {
+                assertWithMessage(
+                        "Close state of broadcast radio service session of index %s", index)
+                        .that(mTunerSessions[index].isClosed()).isTrue();
+            } else {
+                assertWithMessage(
+                        "Close state of broadcast radio service session of index %s", index)
+                        .that(mTunerSessions[index].isClosed()).isFalse();
+            }
+        }
+    }
+
+    @Test
+    public void close_withOneSession_withError() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int errorCode = RadioTuner.ERROR_SERVER_DIED;
+
+        mTunerSessions[0].close(errorCode);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onError(errorCode);
+        assertWithMessage("Close state of broadcast radio service session")
+                .that(mTunerSessions[0].isClosed()).isTrue();
+    }
+
+    @Test
+    public void closeSessions_withMultipleSessions_withError() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+
+        int errorCode = RadioTuner.ERROR_SERVER_DIED;
+        mRadioModule.closeSessions(errorCode);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT).onError(errorCode);
+            assertWithMessage("Close state of broadcast radio service session of index %s", index)
+                    .that(mTunerSessions[index].isClosed()).isTrue();
+        }
+    }
+
+    @Test
+    public void tune_withOneSession() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        RadioManager.ProgramInfo tuneInfo =
+                TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+        mTunerSessions[0].tune(initialSel);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onCurrentProgramInfoChanged(tuneInfo);
+    }
+
+    @Test
+    public void tune_withMultipleSessions() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        RadioManager.ProgramInfo tuneInfo =
+                TestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+        mTunerSessions[0].tune(initialSel);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onCurrentProgramInfoChanged(tuneInfo);
+        }
+    }
+
+    @Test
+    public void tune_withUnsupportedSelector_throwsException() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector unsupportedSelector = TestUtils.makeProgramSelector(
+                ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300));
+
+        UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+                () -> mTunerSessions[0].tune(unsupportedSelector));
+
+        assertWithMessage("Exception for tuning on unsupported program selector")
+                .that(thrown).hasMessageThat().contains("tune: NOT_SUPPORTED");
+    }
+
+    @Test
+    public void step_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo stepUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(initFreq + AM_FM_FREQUENCY_SPACING), SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].step(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(stepUpInfo);
+    }
+
+    @Test
+    public void step_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[1];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo stepDownInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(initFreq - AM_FM_FREQUENCY_SPACING),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(stepDownInfo);
+    }
+
+    @Test
+    public void scan_withDirectionUp() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(scanUpInfo);
+    }
+
+    @Test
+    public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+        int numSessions = 2;
+        openAidlClients(numSessions);
+
+        mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onTuneFailed(eq(Result.TIMEOUT), any());
+        }
+    }
+
+    @Test
+    public void scan_withDirectionDown() throws Exception {
+        long initFreq = AM_FM_FREQUENCY_LIST[2];
+        ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
+        RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+                TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
+                SIGNAL_QUALITY);
+        openAidlClients(/* numClients= */ 1);
+        mHalCurrentInfo = TestUtils.makeHalProgramInfo(
+                Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
+
+        mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
+                .onCurrentProgramInfoChanged(scanUpInfo);
+    }
+
+    @Test
+    public void cancel() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+        mTunerSessions[0].tune(initialSel);
+
+        mTunerSessions[0].cancel();
+
+        verify(mHalTunerSessionMock).cancel();
+    }
+
+    @Test
+    public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int imageId = Constants.INVALID_IMAGE;
+
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            mTunerSessions[0].getImage(imageId);
+        });
+
+        assertWithMessage("Get image exception")
+                .that(thrown).hasMessageThat().contains("Image ID is missing");
+    }
+
+    @Test
+    public void getImage_withValidId() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int imageId = 1;
+
+        Bitmap imageTest = mTunerSessions[0].getImage(imageId);
+
+        assertWithMessage("Null image").that(imageTest).isEqualTo(null);
+    }
+
+    @Test
+    public void startBackgroundScan() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+
+        mTunerSessions[0].startBackgroundScan();
+
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onBackgroundScanComplete();
+    }
+
+    @Test
+    public void stopProgramListUpdates() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
+                /* includeCategories= */ true, /* excludeModifications= */ false);
+        mTunerSessions[0].startProgramListUpdates(aidlFilter);
+
+        mTunerSessions[0].stopProgramListUpdates();
+
+        verify(mHalTunerSessionMock).stopProgramListUpdates();
+    }
+
+    @Test
+    public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG;
+
+        boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
+
+        verify(mHalTunerSessionMock).isConfigFlagSet(eq(flag), any());
+        assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
+    }
+
+    @Test
+    public void isConfigFlagSupported_withSupportedFlag_returnsTrue() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+        boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
+
+        verify(mHalTunerSessionMock).isConfigFlagSet(eq(flag), any());
+        assertWithMessage("Config flag %s is supported", flag).that(isSupported).isTrue();
+    }
+
+    @Test
+    public void setConfigFlag_withUnsupportedFlag_throwsRuntimeException() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG;
+
+        RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+            mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
+        });
+
+        assertWithMessage("Exception for setting unsupported flag %s", flag)
+                .that(thrown).hasMessageThat().contains("setConfigFlag: NOT_SUPPORTED");
+    }
+
+    @Test
+    public void setConfigFlag_withFlagSetToTrue() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+        mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
+
+        verify(mHalTunerSessionMock).setConfigFlag(flag, /* value= */ true);
+    }
+
+    @Test
+    public void setConfigFlag_withFlagSetToFalse() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+
+        mTunerSessions[0].setConfigFlag(flag, /* value= */ false);
+
+        verify(mHalTunerSessionMock).setConfigFlag(flag, /* value= */ false);
+    }
+
+    @Test
+    public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException()
+            throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG;
+
+        RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+            mTunerSessions[0].isConfigFlagSet(flag);
+        });
+
+        assertWithMessage("Exception for check if unsupported flag %s is set", flag)
+                .that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED");
+    }
+
+    @Test
+    public void isConfigFlagSet_withSupportedFlag() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+        boolean expectedConfigFlagValue = true;
+        mTunerSessions[0].setConfigFlag(flag, /* value= */ expectedConfigFlagValue);
+
+        boolean isSet = mTunerSessions[0].isConfigFlagSet(flag);
+
+        assertWithMessage("Config flag %s is set", flag)
+                .that(isSet).isEqualTo(expectedConfigFlagValue);
+    }
+
+    @Test
+    public void setParameters_withMockParameters() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1",
+                "mockParam2", "mockValue2");
+
+        mTunerSessions[0].setParameters(parametersSet);
+
+        verify(mHalTunerSessionMock).setParameters(Convert.vendorInfoToHal(parametersSet));
+    }
+
+    @Test
+    public void getParameters_withMockKeys() throws Exception {
+        openAidlClients(/* numClients= */ 1);
+        ArrayList<String> parameterKeys = new ArrayList<>(Arrays.asList("mockKey1", "mockKey2"));
+
+        mTunerSessions[0].getParameters(parameterKeys);
+
+        verify(mHalTunerSessionMock).getParameters(parameterKeys);
+    }
+
+    @Test
+    public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+
+        mHalTunerCallback.onAntennaStateChange(/* connected= */ false);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onAntennaState(/* connected= */ false);
+        }
+    }
+
+    @Test
+    public void onParametersUpdated_forTunerCallback() throws Exception {
+        int numSessions = 3;
+        openAidlClients(numSessions);
+        ArrayList<VendorKeyValue> parametersUpdates = new ArrayList<VendorKeyValue>(Arrays.asList(
+                TestUtils.makeVendorKeyValue("com.vendor.parameter1", "value1")));
+        Map<String, String> parametersExpected = Map.of("com.vendor.parameter1", "value1");
+
+        mHalTunerCallback.onParametersUpdated(parametersUpdates);
+
+        for (int index = 0; index < numSessions; index++) {
+            verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+                    .onParametersUpdated(parametersExpected);
+        }
+    }
+
+    private void openAidlClients(int numClients) throws Exception {
+        mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
+        mTunerSessions = new TunerSession[numClients];
+        for (int index = 0; index < numClients; index++) {
+            mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class);
+            mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]);
+        }
+    }
+
+    private long getSeekFrequency(long currentFrequency, boolean seekDown) {
+        long seekFrequency;
+        if (seekDown) {
+            seekFrequency = AM_FM_FREQUENCY_LIST[AM_FM_FREQUENCY_LIST.length - 1];
+            for (int i = AM_FM_FREQUENCY_LIST.length - 1; i >= 0; i--) {
+                if (AM_FM_FREQUENCY_LIST[i] < currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[i];
+                    break;
+                }
+            }
+        } else {
+            seekFrequency = AM_FM_FREQUENCY_LIST[0];
+            for (int index = 0; index < AM_FM_FREQUENCY_LIST.length; index++) {
+                if (AM_FM_FREQUENCY_LIST[index] > currentFrequency) {
+                    seekFrequency = AM_FM_FREQUENCY_LIST[index];
+                    break;
+                }
+            }
+        }
+        return seekFrequency;
+    }
+}
diff --git a/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerTestBase.java b/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerTestBase.java
index 100eb99..b3af895 100644
--- a/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerTestBase.java
+++ b/core/tests/ConnectivityManagerTest/src/com/android/connectivitymanagertest/ConnectivityManagerTestBase.java
@@ -145,7 +145,8 @@
         mIntentFilter.addAction(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
         mIntentFilter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
         mIntentFilter.addAction(TetheringManager.ACTION_TETHER_STATE_CHANGED);
-        mContext.registerReceiver(mWifiReceiver, mIntentFilter);
+        mContext.registerReceiver(mWifiReceiver, mIntentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
 
         logv("Clear Wifi before we start the test.");
         removeConfiguredNetworksAndDisableWifi();
diff --git a/core/tests/GameManagerTests/OWNERS b/core/tests/GameManagerTests/OWNERS
new file mode 100644
index 0000000..0992440
--- /dev/null
+++ b/core/tests/GameManagerTests/OWNERS
@@ -0,0 +1 @@
+include /GAME_MANAGER_OWNERS
\ No newline at end of file
diff --git a/core/tests/bandwidthtests/src/com/android/bandwidthtest/util/ConnectionUtil.java b/core/tests/bandwidthtests/src/com/android/bandwidthtest/util/ConnectionUtil.java
index 1a63660..191756a 100644
--- a/core/tests/bandwidthtests/src/com/android/bandwidthtest/util/ConnectionUtil.java
+++ b/core/tests/bandwidthtests/src/com/android/bandwidthtest/util/ConnectionUtil.java
@@ -99,7 +99,8 @@
         // Register a download receiver for ACTION_DOWNLOAD_COMPLETE
         mDownloadReceiver = new DownloadReceiver();
         mContext.registerReceiver(mDownloadReceiver,
-                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
+                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+                Context.RECEIVER_EXPORTED_UNAUDITED);
 
         // Register a wifi receiver
         mWifiReceiver = new WifiReceiver();
diff --git a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
index 1cf4302..372bca4 100644
--- a/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
+++ b/core/tests/benchmarks/src/android/os/ParcelableBenchmark.java
@@ -88,6 +88,7 @@
         }
     }
 
+    @SuppressWarnings("ParcelableCreator")
     @SuppressLint("ParcelCreator")
     private static class PointArray implements Parcelable {
         Rect mBounds = new Rect();
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/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index 04952bd..e2cdbf3 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -25,6 +25,11 @@
         <option name="test-file-name" value="BinderDeathRecipientHelperApp2.apk" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- TODO(b/254155965): Design a mechanism to finally remove this command. -->
+        <option name="run-command" value="settings put global device_config_sync_disabled 0" />
+    </target_preparer>
+
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DeviceInteractionHelperInstaller" />
 
     <option name="test-tag" value="FrameworksCoreTests" />
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/DownloadManagerBaseTest.java b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
index f4709ff..cb66fc8 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
@@ -471,7 +471,7 @@
     protected MultipleDownloadsCompletedReceiver registerNewMultipleDownloadsReceiver() {
         MultipleDownloadsCompletedReceiver receiver = new MultipleDownloadsCompletedReceiver();
         mContext.registerReceiver(receiver, new IntentFilter(
-                DownloadManager.ACTION_DOWNLOAD_COMPLETE));
+                DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED_UNAUDITED);
         return receiver;
     }
 
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0b8b29b..bcb13d2 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -48,6 +48,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertEquals;
@@ -56,7 +57,9 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
 import android.app.Notification.CallStyle;
@@ -68,6 +71,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
+import android.graphics.Typeface;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
@@ -79,7 +83,9 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
+import android.util.Pair;
 import android.widget.RemoteViews;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,6 +95,8 @@
 import com.android.internal.R;
 import com.android.internal.util.ContrastColorUtil;
 
+import junit.framework.Assert;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -218,8 +226,10 @@
 
     @Test
     public void allPendingIntents_recollectedAfterReusingBuilder() {
-        PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
-        PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE);
 
         Notification.Builder builder = new Notification.Builder(mContext, "channel");
         builder.setContentIntent(intent1);
@@ -669,30 +679,23 @@
         Notification notification = new Notification.Builder(mContext, "Channel").setStyle(
                 style).build();
 
+        int targetSize = mContext.getResources().getDimensionPixelSize(
+                ActivityManager.isLowRamDeviceStatic()
+                        ? R.dimen.notification_person_icon_max_size_low_ram
+                        : R.dimen.notification_person_icon_max_size);
+
         Bitmap personIcon = style.getUser().getIcon().getBitmap();
-        assertThat(personIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(personIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(personIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(personIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap();
-        assertThat(avatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(avatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(avatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(avatarIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap historicAvatarIcon = style.getHistoricMessages().get(
                 0).getSenderPerson().getIcon().getBitmap();
-        assertThat(historicAvatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(historicAvatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize);
     }
 
     @Test
@@ -780,7 +783,6 @@
         assertFalse(notification.isMediaNotification());
     }
 
-    @Test
     public void validateColorizedPaletteForColor(int rawColor) {
         Notification.Colors cDay = new Notification.Colors();
         Notification.Colors cNight = new Notification.Colors();
@@ -861,19 +863,22 @@
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() {
-        Notification.Style style = new Notification.MessagingStyle();
+        Notification.Style style = new Notification.MessagingStyle("test");
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
@@ -885,22 +890,33 @@
         fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle());
         fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Call_invalidExtra_noCrash() {
-        Notification.Style style = new CallStyle();
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Notification.Style style = Notification.CallStyle.forIncomingCall(
+                new Person.Builder().setName("hi").build(), intent1, intent1);
+
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
-
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
         // no crash, good
     }
 
@@ -962,7 +978,11 @@
         fakeTypes.putParcelable(KEY_ON_READ, new Bundle());
         fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle());
         fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle());
-        Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes);
+
+        Notification n = new Notification.Builder(mContext, "test")
+                .setExtras(fakeTypes)
+                .build();
+        Notification.CarExtender extender = new Notification.CarExtender(n);
 
         // no crash, good
     }
@@ -980,6 +1000,493 @@
         // no crash, good
     }
 
+
+    @Test
+    public void testDoesNotStripsExtenders() {
+        Notification.Builder nb = new Notification.Builder(mContext, "channel");
+        nb.extend(new Notification.CarExtender().setColor(Color.RED));
+        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
+        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
+        Notification before = nb.build();
+        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
+
+        assertTrue(before == after);
+
+        Assert.assertEquals("different channel",
+                new Notification.TvExtender(before).getChannelId());
+        Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
+        Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyles() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_changeStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle());
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testInboxTextChange() {
+        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
+        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
+    }
+
+    @Test
+    public void testBigTextTextChange() {
+        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("something"));
+        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("else"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
+    }
+
+    @Test
+    public void testBigPictureChange() {
+        Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+        Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+
+        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
+        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
+    }
+
+    @Test
+    public void testMessagingChange_text() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build()))
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "b", 100, new Person.Builder().setName("hi").build()))
+                );
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_data() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())
+                                .setData("text", mock(Uri.class))));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_sender() {
+        Person a = new Person.Builder().setName("A").build();
+        Person b = new Person.Builder().setName("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_key() {
+        Person a = new Person.Builder().setName("hi").setKey("A").build();
+        Person b = new Person.Builder().setName("hi").setKey("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_ignoreTimeChange() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 1000, new Person.Builder().setName("hi").build()))
+                );
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testRemoteViews_nullChange() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test");
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(189);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(2);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(1);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferent_null() {
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSame() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentText() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSpannables() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon,
+                        new SpannableStringBuilder().append("test1",
+                                new StyleSpan(Typeface.BOLD),
+                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
+                        intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentNumber() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentIntent() {
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsIgnoresRemoteInputs() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"i", "m"})
+                                .build())
+                        .build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"t", "m"})
+                                .build())
+                        .build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_noRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(false));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
+
+        Notification.Action actionWithRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification.Action actionWithoutRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutRemoteInput)
+                .addAction(actionWithRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(false);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(remoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(
+                                new RemoteInput.Builder("a")
+                                        .setAllowFreeFormInput(false).build())
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(true));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput =
+                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
+        RemoteInput freeformRemoteInput =
+                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
+
+        Notification.Action actionWithFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(freeformRemoteInput)
+                        .build();
+
+        Notification.Action actionWithoutFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutFreeformRemoteInput)
+                .addAction(actionWithFreeformRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(true);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
+    }
+
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/app/activity/BroadcastTest.java b/core/tests/coretests/src/android/app/activity/BroadcastTest.java
index 0f81896..10452fd 100644
--- a/core/tests/coretests/src/android/app/activity/BroadcastTest.java
+++ b/core/tests/coretests/src/android/app/activity/BroadcastTest.java
@@ -18,6 +18,8 @@
 
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.BroadcastOptions;
+import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -536,4 +538,40 @@
             Log.i("foo", "Unregister exception", e);
         }
     }
+
+    public void testBroadcastOption_interactive() throws Exception {
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setInteractive(true);
+        final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED);
+
+        try {
+            getContext().sendBroadcast(intent, null, options.toBundle());
+            fail("No exception thrown with BroadcastOptions.setInteractive(true)");
+        } catch (SecurityException se) {
+            // Expected, correct behavior - this case intentionally empty
+        } catch (Exception e) {
+            fail("Unexpected exception " + e.getMessage()
+                    + " thrown with BroadcastOptions.setInteractive(true)");
+        }
+    }
+
+    public void testBroadcastOption_interactive_PendingIntent() throws Exception {
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setInteractive(true);
+        final Intent intent = makeBroadcastIntent(BROADCAST_REGISTERED);
+        PendingIntent brPending = PendingIntent.getBroadcast(getContext(),
+                1, intent, PendingIntent.FLAG_IMMUTABLE);
+
+        try {
+            brPending.send(getContext(), 1, null, null, null, null, options.toBundle());
+            fail("No exception thrown with BroadcastOptions.setInteractive(true)");
+        } catch (SecurityException se) {
+            // Expected, correct behavior - this case intentionally empty
+        } catch (Exception e) {
+            fail("Unexpected exception " + e.getMessage()
+                    + " thrown with BroadcastOptions.setInteractive(true)");
+        } finally {
+            brPending.cancel();
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/app/activity/LocalReceiver.java b/core/tests/coretests/src/android/app/activity/LocalReceiver.java
index 7f81339..5ac84f8 100644
--- a/core/tests/coretests/src/android/app/activity/LocalReceiver.java
+++ b/core/tests/coretests/src/android/app/activity/LocalReceiver.java
@@ -36,7 +36,8 @@
         if (BroadcastTest.BROADCAST_FAIL_REGISTER.equals(intent.getAction())) {
             resultString = "Successfully registered, but expected it to fail";
             try {
-                context.registerReceiver(this, new IntentFilter("foo.bar"));
+                context.registerReceiver(this, new IntentFilter("foo.bar"),
+                        Context.RECEIVER_EXPORTED_UNAUDITED);
                 context.unregisterReceiver(this);
             } catch (ReceiverCallNotAllowedException e) {
                 //resultString = "This is the correct behavior but not yet implemented";
diff --git a/core/tests/coretests/src/android/app/activity/OWNERS b/core/tests/coretests/src/android/app/activity/OWNERS
index 0862c05..7e24aef 100644
--- a/core/tests/coretests/src/android/app/activity/OWNERS
+++ b/core/tests/coretests/src/android/app/activity/OWNERS
@@ -1 +1,2 @@
 include /services/core/java/com/android/server/wm/OWNERS
+include /services/core/java/com/android/server/am/OWNERS
diff --git a/core/tests/coretests/src/android/app/activity/ServiceTest.java b/core/tests/coretests/src/android/app/activity/ServiceTest.java
index c89f37d..3f3d6a3 100644
--- a/core/tests/coretests/src/android/app/activity/ServiceTest.java
+++ b/core/tests/coretests/src/android/app/activity/ServiceTest.java
@@ -172,7 +172,7 @@
                 pidResult.complete(intent.getIntExtra(EXTRA_PID, NOT_STARTED));
                 mContext.unregisterReceiver(this);
             }
-        }, new IntentFilter(ACTION_SERVICE_STARTED));
+        }, new IntentFilter(ACTION_SERVICE_STARTED), Context.RECEIVER_EXPORTED_UNAUDITED);
 
         serviceTrigger.run();
         try {
diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
index 37cf470..4d5b0d2 100644
--- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
+++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java
@@ -46,6 +46,7 @@
 public class BackupAgentTest {
     // An arbitrary user.
     private static final UserHandle USER_HANDLE = new UserHandle(15);
+    private static final String DATA_TYPE_BACKED_UP = "test data type";
 
     @Mock FullBackup.BackupScheme mBackupScheme;
 
@@ -73,6 +74,42 @@
         assertThat(rules).isEqualTo(expectedRules);
     }
 
+    @Test
+    public void getBackupRestoreEventLogger_beforeOnCreate_isNull() {
+        BackupAgent agent = new TestFullBackupAgent();
+
+        assertThat(agent.getBackupRestoreEventLogger()).isNull();
+    }
+
+    @Test
+    public void getBackupRestoreEventLogger_afterOnCreateForBackup_initializedForBackup() {
+        BackupAgent agent = new TestFullBackupAgent();
+        agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+
+        assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1);
+    }
+
+    @Test
+    public void getBackupRestoreEventLogger_afterOnCreateForRestore_initializedForRestore() {
+        BackupAgent agent = new TestFullBackupAgent();
+        agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+
+        assertThat(agent.getBackupRestoreEventLogger().getOperationType()).isEqualTo(1);
+    }
+
+    @Test
+    public void getBackupRestoreEventLogger_afterBackup_containsLogsLoggedByAgent()
+            throws Exception {
+        BackupAgent agent = new TestFullBackupAgent();
+        agent.onCreate(USER_HANDLE, OperationType.BACKUP); // TODO: pass in new operation type
+
+        // TestFullBackupAgent logs DATA_TYPE_BACKED_UP when onFullBackup is called.
+        agent.onFullBackup(new FullBackupDataOutput(/* quota = */ 0));
+
+        assertThat(agent.getBackupRestoreEventLogger().getLoggingResults().get(0).getDataType())
+                .isEqualTo(DATA_TYPE_BACKED_UP);
+    }
+
     private BackupAgent getAgentForOperationType(@OperationType int operationType) {
         BackupAgent agent = new TestFullBackupAgent();
         agent.onCreate(USER_HANDLE, operationType);
@@ -88,6 +125,11 @@
         }
 
         @Override
+        public void onFullBackup(FullBackupDataOutput data) {
+            getBackupRestoreEventLogger().logItemsBackedUp(DATA_TYPE_BACKED_UP, 1);
+        }
+
+        @Override
         public void onRestore(BackupDataInput data, int appVersionCode,
                 ParcelFileDescriptor newState) throws IOException {
             // Left empty as this is a full backup agent.
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..b9fdc6d
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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.os.Parcel;
+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.ArrayList;
+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);
+
+        mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1);
+        mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata");
+
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_1)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testRestoreLogger_rejectsBackupLogs() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5);
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1);
+        mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata");
+
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_1)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testBackupLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+            String dataType = DATA_TYPE_1 + i;
+            mLogger.logItemsBackedUp(dataType, /* count */ 5);
+            mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null);
+            mLogger.logBackupMetaData(dataType, METADATA_1);
+
+            assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo(
+                    Optional.empty());
+        }
+
+        mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5);
+        mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null);
+        mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1);
+        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;
+            mLogger.logItemsRestored(dataType, /* count */ 5);
+            mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null);
+            mLogger.logRestoreMetadata(dataType, METADATA_1);
+
+            assertThat(getResultForDataTypeIfPresent(mLogger, dataType)).isNotEqualTo(
+                    Optional.empty());
+        }
+
+        mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null);
+        mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1);
+        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);
+    }
+
+    @Test
+    public void testGetLoggingResults_resultsParceledAndUnparceled_recreatedCorrectly() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+        int firstTypeSuccessCount = 1;
+        int firstTypeErrorOneCount = 2;
+        int firstTypeErrorTwoCount = 3;
+        mLogger.logItemsRestored(DATA_TYPE_1, firstTypeSuccessCount);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstTypeErrorOneCount, ERROR_1);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstTypeErrorTwoCount, ERROR_2);
+        mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_1);
+        int secondTypeSuccessCount = 4;
+        int secondTypeErrorOneCount = 5;
+        mLogger.logItemsRestored(DATA_TYPE_2, secondTypeSuccessCount);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_2, secondTypeErrorOneCount, ERROR_1);
+
+        List<DataTypeResult> resultsList = mLogger.getLoggingResults();
+        Parcel parcel = Parcel.obtain();
+
+        parcel.writeParcelableList(resultsList, /* flags= */ 0);
+
+        parcel.setDataPosition(0);
+        List<DataTypeResult> recreatedList = new ArrayList<>();
+        parcel.readParcelableList(
+                recreatedList, DataTypeResult.class.getClassLoader(), DataTypeResult.class);
+
+        assertThat(recreatedList.get(0).getDataType()).isEqualTo(DATA_TYPE_1);
+        assertThat(recreatedList.get(0).getSuccessCount()).isEqualTo(firstTypeSuccessCount);
+        assertThat(recreatedList.get(0).getFailCount())
+                .isEqualTo(firstTypeErrorOneCount + firstTypeErrorTwoCount);
+        assertThat(recreatedList.get(0).getErrors().get(ERROR_1)).isEqualTo(firstTypeErrorOneCount);
+        assertThat(recreatedList.get(0).getErrors().get(ERROR_2)).isEqualTo(firstTypeErrorTwoCount);
+        assertThat(recreatedList.get(1).getDataType()).isEqualTo(DATA_TYPE_2);
+        assertThat(recreatedList.get(1).getSuccessCount()).isEqualTo(secondTypeSuccessCount);
+        assertThat(recreatedList.get(1).getFailCount()).isEqualTo(secondTypeErrorOneCount);
+        assertThat(recreatedList.get(1).getErrors().get(ERROR_1))
+                .isEqualTo(secondTypeErrorOneCount);
+    }
+
+    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/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index b292d7d..a0ed026 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -294,10 +294,9 @@
 
         StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */);
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.addCallback(callback1);
         transaction.addCallback(callback2);
         transaction.setLifecycleStateRequest(lifecycleRequest);
@@ -318,10 +317,9 @@
         ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
                 config());
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.addCallback(callback1);
         transaction.addCallback(callback2);
 
@@ -339,10 +337,9 @@
         // Write to parcel
         StopActivityItem lifecycleRequest = StopActivityItem.obtain(78 /* configChanges */);
 
-        IApplicationThread appThread = new StubAppThread();
         Binder activityToken = new Binder();
 
-        ClientTransaction transaction = ClientTransaction.obtain(appThread, activityToken);
+        ClientTransaction transaction = ClientTransaction.obtain(null, activityToken);
         transaction.setLifecycleStateRequest(lifecycleRequest);
 
         writeAndPrepareForReading(transaction);
@@ -400,286 +397,4 @@
             }
         };
     }
-
-    /** Stub implementation of IApplicationThread that can be presented as {@link Binder}. */
-    class StubAppThread extends android.app.IApplicationThread.Stub  {
-
-        @Override
-        public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleReceiver(Intent intent, ActivityInfo activityInfo,
-                CompatibilityInfo compatibilityInfo, int i, String s, Bundle bundle, boolean b,
-                int i1, int i2) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCreateService(IBinder iBinder, ServiceInfo serviceInfo,
-                CompatibilityInfo compatibilityInfo, int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleStopService(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void bindApplication(String s, ApplicationInfo applicationInfo,
-                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
-                ProviderInfoList list, ComponentName componentName, ProfilerInfo profilerInfo,
-                Bundle bundle, IInstrumentationWatcher iInstrumentationWatcher,
-                IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
-                boolean b2, boolean b3, Configuration configuration,
-                CompatibilityInfo compatibilityInfo, Map map, Bundle bundle1, String s1,
-                AutofillOptions ao, ContentCaptureOptions co, long[] disableCompatChanges,
-                SharedMemory serializedSystemFontMap,
-                long startRequestedElapsedTime, long startRequestedUptime)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleExit() throws RemoteException {
-        }
-
-        @Override
-        public void scheduleServiceArgs(IBinder iBinder, ParceledListSlice parceledListSlice)
-                throws RemoteException {
-        }
-
-        @Override
-        public void updateTimeZone() throws RemoteException {
-        }
-
-        @Override
-        public void processInBackground() throws RemoteException {
-        }
-
-        @Override
-        public void scheduleBindService(IBinder iBinder, Intent intent, boolean b, int i)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleUnbindService(IBinder iBinder, Intent intent) throws RemoteException {
-        }
-
-        @Override
-        public void dumpService(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleRegisteredReceiver(IIntentReceiver iIntentReceiver, Intent intent,
-                int i, String s, Bundle bundle, boolean b, boolean b1, int i1, int i2)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleLowMemory() throws RemoteException {
-        }
-
-        @Override
-        public void profilerControl(boolean b, ProfilerInfo profilerInfo, int i)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setSchedulingGroup(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCreateBackupAgent(ApplicationInfo applicationInfo,
-                int i, int userId, int operatioType)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleDestroyBackupAgent(ApplicationInfo applicationInfo,
-                int userId) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleOnNewActivityOptions(IBinder iBinder, Bundle bundle)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleSuicide() throws RemoteException {
-        }
-
-        @Override
-        public void dispatchPackageBroadcast(int i, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleCrash(String s, int i, Bundle extras) throws RemoteException {
-        }
-
-        @Override
-        public void dumpActivity(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String s, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void clearDnsCache() throws RemoteException {
-        }
-
-        @Override
-        public void updateHttpProxy() throws RemoteException {
-        }
-
-        @Override
-        public void setCoreSettings(Bundle bundle) throws RemoteException {
-        }
-
-        @Override
-        public void updatePackageCompatibilityInfo(String s, CompatibilityInfo compatibilityInfo)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleTrimMemory(int i) throws RemoteException {
-        }
-
-        @Override
-        public void dumpMemInfo(ParcelFileDescriptor parcelFileDescriptor,
-                Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2, boolean b3,
-                boolean b4, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpMemInfoProto(ParcelFileDescriptor parcelFileDescriptor,
-                Debug.MemoryInfo memoryInfo, boolean b, boolean b1, boolean b2,
-                boolean b3, String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpGfxInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void dumpCacheInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void dumpProvider(ParcelFileDescriptor parcelFileDescriptor, IBinder iBinder,
-                String[] strings) throws RemoteException {
-        }
-
-        @Override
-        public void dumpDbInfo(ParcelFileDescriptor parcelFileDescriptor, String[] strings)
-                throws RemoteException {
-        }
-
-        @Override
-        public void unstableProviderDied(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void requestAssistContextExtras(IBinder iBinder, IBinder iBinder1, int i, int i1,
-                int i2) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleTranslucentConversionComplete(IBinder iBinder, boolean b)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setProcessState(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleInstallProvider(ProviderInfo providerInfo) throws RemoteException {
-        }
-
-        @Override
-        public void updateTimePrefs(int i) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleEnterAnimationComplete(IBinder iBinder) throws RemoteException {
-        }
-
-        @Override
-        public void notifyCleartextNetwork(byte[] bytes) throws RemoteException {
-        }
-
-        @Override
-        public void startBinderTracking() throws RemoteException {
-        }
-
-        @Override
-        public void stopBinderTrackingAndDump(ParcelFileDescriptor parcelFileDescriptor)
-                throws RemoteException {
-        }
-
-        @Override
-        public void scheduleLocalVoiceInteractionStarted(IBinder iBinder,
-                IVoiceInteractor iVoiceInteractor) throws RemoteException {
-        }
-
-        @Override
-        public void handleTrustStorageUpdate() throws RemoteException {
-        }
-
-        @Override
-        public void attachAgent(String s) throws RemoteException {
-        }
-
-        @Override
-        public void attachStartupAgents(String s) throws RemoteException {
-        }
-
-        @Override
-        public void scheduleApplicationInfoChanged(ApplicationInfo applicationInfo)
-                throws RemoteException {
-        }
-
-        @Override
-        public void setNetworkBlockSeq(long l) throws RemoteException {
-        }
-
-        @Override
-        public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String path,
-                ParcelFileDescriptor fd, RemoteCallback finishCallback) {
-        }
-
-        @Override
-        public void dumpResources(ParcelFileDescriptor fd, RemoteCallback finishCallback) {
-        }
-
-        @Override
-        public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) {
-        }
-
-        @Override
-        public void requestDirectActions(IBinder activityToken, IVoiceInteractor interactor,
-                RemoteCallback cancellationCallback, RemoteCallback resultCallback) {
-        }
-
-        @Override
-        public void performDirectAction(IBinder activityToken, String actionId, Bundle arguments,
-                RemoteCallback cancellationCallback, RemoteCallback resultCallback) {
-        }
-
-        @Override
-        public void notifyContentProviderPublishStatus(ContentProviderHolder holder, String auth,
-                int userId, boolean published) {
-        }
-
-        @Override
-        public void instrumentWithoutRestart(ComponentName instrumentationName,
-                Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher,
-                IUiAutomationConnection instrumentationUiConnection, ApplicationInfo targetInfo) {
-        }
-
-        @Override
-        public void updateUiTranslationState(IBinder activityToken, int state,
-                TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds,
-                UiTranslationSpec uiTranslationSpec) {
-        }
-    }
 }
diff --git a/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java
new file mode 100644
index 0000000..f57ee43
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/DetectorStatusTypesTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_UNKNOWN;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
+import android.app.time.DetectorStatusTypes.DetectorStatus;
+
+import org.junit.Test;
+
+public class DetectorStatusTypesTest {
+
+    @Test
+    public void testRequireValidDetectionAlgorithmStatus() {
+        for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN;
+                status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) {
+            assertEquals(status, DetectorStatusTypes.requireValidDetectionAlgorithmStatus(status));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus(
+                        DETECTION_ALGORITHM_STATUS_UNKNOWN - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.requireValidDetectionAlgorithmStatus(
+                        DETECTION_ALGORITHM_STATUS_RUNNING + 1));
+    }
+
+    @Test
+    public void testFormatAndParseDetectionAlgorithmStatus() {
+        for (@DetectionAlgorithmStatus int status = DETECTION_ALGORITHM_STATUS_UNKNOWN;
+                status <= DETECTION_ALGORITHM_STATUS_RUNNING; status++) {
+            assertEquals(status, DetectorStatusTypes.detectionAlgorithmStatusFromString(
+                    DetectorStatusTypes.detectionAlgorithmStatusToString(status)));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusToString(
+                        DETECTION_ALGORITHM_STATUS_UNKNOWN - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusToString(
+                        DETECTION_ALGORITHM_STATUS_RUNNING + 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString(null));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString(""));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString("FOO"));
+    }
+
+    @Test
+    public void testRequireValidDetectorStatus() {
+        for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN;
+                status <= DETECTOR_STATUS_RUNNING; status++) {
+            assertEquals(status, DetectorStatusTypes.requireValidDetectorStatus(status));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_UNKNOWN - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.requireValidDetectorStatus(DETECTOR_STATUS_RUNNING + 1));
+    }
+
+    @Test
+    public void testFormatAndParseDetectorStatus() {
+        for (@DetectorStatus int status = DETECTOR_STATUS_UNKNOWN;
+                status <= DETECTOR_STATUS_RUNNING; status++) {
+            assertEquals(status, DetectorStatusTypes.detectorStatusFromString(
+                    DetectorStatusTypes.detectorStatusToString(status)));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_UNKNOWN - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusToString(DETECTOR_STATUS_RUNNING + 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString(null));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString(""));
+        assertThrows(IllegalArgumentException.class,
+                () -> DetectorStatusTypes.detectorStatusFromString("FOO"));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java
new file mode 100644
index 0000000..a648a88
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/LocationTimeZoneAlgorithmStatusTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus;
+import android.service.timezone.TimeZoneProviderStatus;
+
+import org.junit.Test;
+
+public class LocationTimeZoneAlgorithmStatusTest {
+
+    private static final TimeZoneProviderStatus ARBITRARY_PROVIDER_RUNNING_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                    .build();
+
+    @Test
+    public void testConstructorValidation() {
+        // Sample some invalid cases
+
+        // There can't be a reported provider status if the algorithm isn't running.
+        new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS);
+        assertThrows(IllegalArgumentException.class,
+                () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                        PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                        PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS));
+
+        new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        assertThrows(IllegalArgumentException.class,
+                () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                        PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                        PROVIDER_STATUS_NOT_PRESENT, null));
+
+        new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_NOT_PRESENT, null,
+                PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS);
+        assertThrows(IllegalArgumentException.class,
+                () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                        PROVIDER_STATUS_NOT_PRESENT, null,
+                        PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS));
+
+        // No reported provider status expected if the associated provider isn't ready / present.
+        new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_NOT_PRESENT, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        assertThrows(IllegalArgumentException.class,
+                () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                        PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                        PROVIDER_STATUS_NOT_PRESENT, null));
+        new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_NOT_READY, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        assertThrows(IllegalArgumentException.class,
+                () -> new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                        PROVIDER_STATUS_NOT_READY, null,
+                        PROVIDER_STATUS_NOT_PRESENT, ARBITRARY_PROVIDER_RUNNING_STATUS));
+    }
+
+    @Test
+    public void testEquals() {
+        LocationTimeZoneAlgorithmStatus one = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        assertEqualsAndHashCode(one, one);
+
+        {
+            LocationTimeZoneAlgorithmStatus two = new LocationTimeZoneAlgorithmStatus(
+                    DETECTION_ALGORITHM_STATUS_RUNNING,
+                    PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                    PROVIDER_STATUS_NOT_PRESENT, null);
+            assertEqualsAndHashCode(one, two);
+        }
+
+        {
+            LocationTimeZoneAlgorithmStatus three = new LocationTimeZoneAlgorithmStatus(
+                    DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                    PROVIDER_STATUS_NOT_READY, null,
+                    PROVIDER_STATUS_NOT_PRESENT, null);
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
+    }
+
+    @Test
+    public void testParcelable() {
+        // Primary provider only.
+        {
+            LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new LocationTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_RUNNING,
+                            PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                            PROVIDER_STATUS_NOT_PRESENT, null);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+
+        // Secondary provider only
+        {
+            LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new LocationTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_RUNNING,
+                            PROVIDER_STATUS_NOT_PRESENT, null,
+                            PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+
+        // Algorithm not running.
+        {
+            LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new LocationTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                            PROVIDER_STATUS_NOT_PRESENT, null,
+                            PROVIDER_STATUS_NOT_PRESENT, null);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+    }
+
+    @Test
+    public void testRequireValidProviderStatus() {
+        for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT;
+                status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) {
+            assertEquals(status,
+                    LocationTimeZoneAlgorithmStatus.requireValidProviderStatus(status));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus(
+                        PROVIDER_STATUS_NOT_PRESENT - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.requireValidProviderStatus(
+                        PROVIDER_STATUS_IS_UNCERTAIN + 1));
+    }
+
+    @Test
+    public void testFormatAndParseProviderStatus() {
+        for (@ProviderStatus int status = PROVIDER_STATUS_NOT_PRESENT;
+                status <= PROVIDER_STATUS_IS_UNCERTAIN; status++) {
+            assertEquals(status, LocationTimeZoneAlgorithmStatus.providerStatusFromString(
+                    LocationTimeZoneAlgorithmStatus.providerStatusToString(status)));
+        }
+
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.providerStatusToString(
+                        PROVIDER_STATUS_NOT_PRESENT - 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.providerStatusToString(
+                        PROVIDER_STATUS_IS_UNCERTAIN + 1));
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString(null));
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString(""));
+        assertThrows(IllegalArgumentException.class,
+                () -> LocationTimeZoneAlgorithmStatus.providerStatusFromString("FOO"));
+    }
+
+    @Test
+    public void testParseCommandlineArg_noNullReportedStatuses() {
+        LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS,
+                PROVIDER_STATUS_IS_UNCERTAIN, ARBITRARY_PROVIDER_RUNNING_STATUS);
+        assertEquals(status,
+                LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString()));
+    }
+
+    @Test
+    public void testParseCommandlineArg_withNullReportedStatuses() {
+        LocationTimeZoneAlgorithmStatus status = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_IS_CERTAIN, null,
+                PROVIDER_STATUS_IS_UNCERTAIN, null);
+        assertEquals(status,
+                LocationTimeZoneAlgorithmStatus.parseCommandlineArg(status.toString()));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/ParcelableTestSupport.java b/core/tests/coretests/src/android/app/time/ParcelableTestSupport.java
new file mode 100644
index 0000000..13e5e14
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/ParcelableTestSupport.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 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 android.app.time;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.reflect.Field;
+
+/** Utility methods related to {@link Parcelable} objects used in several tests. */
+public final class ParcelableTestSupport {
+
+    private ParcelableTestSupport() {}
+
+    /** Returns the result of parceling and unparceling the argument. */
+    @SuppressWarnings("unchecked")
+    public static <T extends Parcelable> T roundTripParcelable(T parcelable) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeTypedObject(parcelable, 0);
+        parcel.setDataPosition(0);
+
+        Parcelable.Creator<T> creator;
+        try {
+            Field creatorField = parcelable.getClass().getField("CREATOR");
+            creator = (Parcelable.Creator<T>) creatorField.get(null);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new AssertionError(e);
+        }
+        T toReturn = parcel.readTypedObject(creator);
+        parcel.recycle();
+        return toReturn;
+    }
+
+    public static <T extends Parcelable> void assertRoundTripParcelable(T instance) {
+        assertEqualsAndHashCode(instance, roundTripParcelable(instance));
+    }
+
+    /** Asserts that the objects are equal and return identical hash codes. */
+    public static void assertEqualsAndHashCode(Object one, Object two) {
+        assertEquals(one, two);
+        assertEquals(two, one);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java
new file mode 100644
index 0000000..b90c485
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/TelephonyTimeZoneAlgorithmStatusTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+
+import static org.junit.Assert.assertNotEquals;
+
+import org.junit.Test;
+
+public class TelephonyTimeZoneAlgorithmStatusTest {
+
+    @Test
+    public void testEquals() {
+        TelephonyTimeZoneAlgorithmStatus one = new TelephonyTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
+        assertEqualsAndHashCode(one, one);
+
+        {
+            TelephonyTimeZoneAlgorithmStatus two = new TelephonyTimeZoneAlgorithmStatus(
+                    DETECTION_ALGORITHM_STATUS_RUNNING);
+            assertEqualsAndHashCode(one, two);
+        }
+
+        {
+            TelephonyTimeZoneAlgorithmStatus three = new TelephonyTimeZoneAlgorithmStatus(
+                    DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
+    }
+
+    @Test
+    public void testParcelable() {
+        // Algorithm running.
+        {
+            TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new TelephonyTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_RUNNING);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+
+        // Algorithm not running.
+        {
+            TelephonyTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new TelephonyTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java b/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java
index c9b96c6..1a276ad 100644
--- a/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java
+++ b/core/tests/coretests/src/android/app/time/TimeCapabilitiesTest.java
@@ -21,7 +21,8 @@
 import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE;
 import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED;
 import static android.app.time.Capabilities.CAPABILITY_POSSESSED;
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -55,7 +56,7 @@
         {
             TimeCapabilities one = builder1.build();
             TimeCapabilities two = builder2.build();
-            assertEquals(one, two);
+            assertEqualsAndHashCode(one, two);
         }
 
         builder2.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED);
@@ -69,7 +70,7 @@
         {
             TimeCapabilities one = builder1.build();
             TimeCapabilities two = builder2.build();
-            assertEquals(one, two);
+            assertEqualsAndHashCode(one, two);
         }
 
         builder2.setSetManualTimeCapability(CAPABILITY_NOT_ALLOWED);
@@ -83,7 +84,7 @@
         {
             TimeCapabilities one = builder1.build();
             TimeCapabilities two = builder2.build();
-            assertEquals(one, two);
+            assertEqualsAndHashCode(one, two);
         }
     }
 
diff --git a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java b/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
deleted file mode 100644
index 7c7cd12..0000000
--- a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
+++ /dev/null
@@ -1,56 +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.
- */
-
-package android.app.time;
-
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TimeConfigurationTest {
-
-    @Test
-    public void testBuilder() {
-        TimeConfiguration first = new TimeConfiguration.Builder()
-                .setAutoDetectionEnabled(true)
-                .build();
-
-        assertThat(first.isAutoDetectionEnabled()).isTrue();
-
-        TimeConfiguration copyFromBuilderConfiguration = new TimeConfiguration.Builder(first)
-                .build();
-
-        assertThat(first).isEqualTo(copyFromBuilderConfiguration);
-    }
-
-    @Test
-    public void testParcelable() {
-        TimeConfiguration.Builder builder = new TimeConfiguration.Builder();
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(true).build());
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(false).build());
-    }
-
-}
diff --git a/core/tests/coretests/src/android/app/time/TimeStateTest.java b/core/tests/coretests/src/android/app/time/TimeStateTest.java
index bce0909..25e6e2b 100644
--- a/core/tests/coretests/src/android/app/time/TimeStateTest.java
+++ b/core/tests/coretests/src/android/app/time/TimeStateTest.java
@@ -16,7 +16,8 @@
 
 package android.app.time;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
@@ -52,11 +53,6 @@
         assertNotEquals(time1False_1, time2False);
     }
 
-    private static void assertEqualsAndHashCode(Object one, Object two) {
-        assertEquals(one, two);
-        assertEquals(one.hashCode(), two.hashCode());
-    }
-
     @Test
     public void testParceling() {
         UnixEpochTime time = new UnixEpochTime(1, 2);
diff --git a/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java
index 3f7da8a..8bed31f 100644
--- a/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java
+++ b/core/tests/coretests/src/android/app/time/TimeZoneCapabilitiesTest.java
@@ -18,7 +18,7 @@
 
 import static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED;
 import static android.app.time.Capabilities.CAPABILITY_POSSESSED;
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
 
 import static com.google.common.truth.Truth.assertThat;
 
diff --git a/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java
new file mode 100644
index 0000000..dfff7ec
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/TimeZoneDetectorStatusTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.time;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+
+import static org.junit.Assert.assertNotEquals;
+
+import org.junit.Test;
+
+public class TimeZoneDetectorStatusTest {
+
+    private static final TelephonyTimeZoneAlgorithmStatus ARBITRARY_TELEPHONY_ALGORITHM_STATUS =
+            new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING);
+
+    private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_ALGORITHM_STATUS =
+            new LocationTimeZoneAlgorithmStatus(
+                    DETECTION_ALGORITHM_STATUS_RUNNING,
+                    LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null,
+                    LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT, null);
+
+    @Test
+    public void testEquals() {
+        TimeZoneDetectorStatus one = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING,
+                ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS);
+        assertEqualsAndHashCode(one, one);
+
+        {
+            TimeZoneDetectorStatus two = new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING,
+                    ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS);
+            assertEqualsAndHashCode(one, two);
+        }
+
+        {
+            TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING,
+                    ARBITRARY_TELEPHONY_ALGORITHM_STATUS, ARBITRARY_LOCATION_ALGORITHM_STATUS);
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
+
+        {
+            TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus =
+                    new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
+            assertNotEquals(telephonyAlgorithmStatus, ARBITRARY_TELEPHONY_ALGORITHM_STATUS);
+
+            TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING,
+                    telephonyAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS);
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
+
+        {
+            LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                    new LocationTimeZoneAlgorithmStatus(
+                            DETECTION_ALGORITHM_STATUS_NOT_RUNNING,
+                            LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null,
+                            LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY, null);
+            assertNotEquals(locationAlgorithmStatus, ARBITRARY_LOCATION_ALGORITHM_STATUS);
+
+            TimeZoneDetectorStatus three = new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING,
+                    ARBITRARY_TELEPHONY_ALGORITHM_STATUS, locationAlgorithmStatus);
+            assertNotEquals(one, three);
+            assertNotEquals(three, one);
+        }
+    }
+
+    @Test
+    public void testParcelable() {
+        // Detector running.
+        {
+            TimeZoneDetectorStatus locationAlgorithmStatus = new TimeZoneDetectorStatus(
+                    DETECTOR_STATUS_RUNNING, ARBITRARY_TELEPHONY_ALGORITHM_STATUS,
+                    ARBITRARY_LOCATION_ALGORITHM_STATUS);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+
+        // Detector not running.
+        {
+            TimeZoneDetectorStatus locationAlgorithmStatus =
+                    new TimeZoneDetectorStatus(DETECTOR_STATUS_NOT_RUNNING,
+                            ARBITRARY_TELEPHONY_ALGORITHM_STATUS,
+                            ARBITRARY_LOCATION_ALGORITHM_STATUS);
+            assertRoundTripParcelable(locationAlgorithmStatus);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java b/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java
index 35a9dbc..595b700 100644
--- a/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java
+++ b/core/tests/coretests/src/android/app/time/TimeZoneStateTest.java
@@ -16,7 +16,8 @@
 
 package android.app.time;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertEqualsAndHashCode;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
@@ -52,11 +53,6 @@
         assertNotEquals(zone1False_1, zone2False);
     }
 
-    private static void assertEqualsAndHashCode(Object one, Object two) {
-        assertEquals(one, two);
-        assertEquals(one.hashCode(), two.hashCode());
-    }
-
     @Test
     public void testParceling() {
         assertRoundTripParcelable(new TimeZoneState("Europe/London", true));
diff --git a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
index 3ab01f3..e7d352c 100644
--- a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
+++ b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
@@ -16,11 +16,9 @@
 
 package android.app.time;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 
 import android.os.ShellCommand;
 
@@ -31,35 +29,12 @@
 
 /**
  * Tests for non-SDK methods on {@link UnixEpochTime}.
+ *
+ * <p>See also {@link android.app.time.cts.UnixEpochTimeTest} for SDK methods.
  */
 @RunWith(AndroidJUnit4.class)
 public class UnixEpochTimeTest {
 
-    @Test
-    public void testEqualsAndHashcode() {
-        UnixEpochTime one1000one = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000one);
-
-        UnixEpochTime one1000two = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000two);
-
-        UnixEpochTime two1000 = new UnixEpochTime(1000, 2);
-        assertNotEquals(one1000one, two1000);
-
-        UnixEpochTime one2000 = new UnixEpochTime(2000, 1);
-        assertNotEquals(one1000one, one2000);
-    }
-
-    private static void assertEqualsAndHashCode(Object one, Object two) {
-        assertEquals(one, two);
-        assertEquals(one.hashCode(), two.hashCode());
-    }
-
-    @Test
-    public void testParceling() {
-        assertRoundTripParcelable(new UnixEpochTime(1000, 1));
-    }
-
     @Test(expected = IllegalArgumentException.class)
     public void testParseCommandLineArg_noElapsedRealtime() {
         ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
@@ -91,22 +66,6 @@
     }
 
     @Test
-    public void testAt() {
-        long timeMillis = 1000L;
-        int elapsedRealtimeMillis = 100;
-        UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMillis, timeMillis);
-        // Reference time is after the timestamp.
-        UnixEpochTime at125 = unixEpochTime.at(125);
-        assertEquals(timeMillis + (125 - elapsedRealtimeMillis), at125.getUnixEpochTimeMillis());
-        assertEquals(125, at125.getElapsedRealtimeMillis());
-
-        // Reference time is before the timestamp.
-        UnixEpochTime at75 = unixEpochTime.at(75);
-        assertEquals(timeMillis + (75 - elapsedRealtimeMillis), at75.getUnixEpochTimeMillis());
-        assertEquals(75, at75.getElapsedRealtimeMillis());
-    }
-
-    @Test
     public void testElapsedRealtimeDifference() {
         UnixEpochTime value1 = new UnixEpochTime(1000, 123L);
         assertEquals(0, UnixEpochTime.elapsedRealtimeDifference(value1, value1));
diff --git a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
index 0c7c8c1..28da164 100644
--- a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
@@ -16,8 +16,8 @@
 
 package android.app.timedetector;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.roundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
diff --git a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
index 26cb902..e9ca069 100644
--- a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
@@ -16,8 +16,8 @@
 
 package android.app.timedetector;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.roundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
diff --git a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java
index 17838bb1..b5bdea7 100644
--- a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java
@@ -16,8 +16,8 @@
 
 package android.app.timezonedetector;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.roundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
diff --git a/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java b/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java
deleted file mode 100644
index 0073d86..0000000
--- a/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 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 android.app.timezonedetector;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import java.lang.reflect.Field;
-
-/** Utility methods related to {@link Parcelable} objects used in several tests. */
-public final class ParcelableTestSupport {
-
-    private ParcelableTestSupport() {}
-
-    /** Returns the result of parceling and unparceling the argument. */
-    @SuppressWarnings("unchecked")
-    public static <T extends Parcelable> T roundTripParcelable(T parcelable) {
-        Parcel parcel = Parcel.obtain();
-        parcel.writeTypedObject(parcelable, 0);
-        parcel.setDataPosition(0);
-
-        Parcelable.Creator<T> creator;
-        try {
-            Field creatorField = parcelable.getClass().getField("CREATOR");
-            creator = (Parcelable.Creator<T>) creatorField.get(null);
-        } catch (NoSuchFieldException | IllegalAccessException e) {
-            throw new AssertionError(e);
-        }
-        T toReturn = parcel.readTypedObject(creator);
-        parcel.recycle();
-        return toReturn;
-    }
-
-    public static <T extends Parcelable> void assertRoundTripParcelable(T instance) {
-        assertEquals(instance, roundTripParcelable(instance));
-    }
-}
diff --git a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java
index 28009d4..d5dcac2 100644
--- a/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timezonedetector/TelephonyTimeZoneSuggestionTest.java
@@ -16,8 +16,8 @@
 
 package android.app.timezonedetector;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.roundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
diff --git a/core/tests/coretests/src/android/content/BroadcastReceiverTests.java b/core/tests/coretests/src/android/content/BroadcastReceiverTests.java
index 1509ff9..5dbeac2 100644
--- a/core/tests/coretests/src/android/content/BroadcastReceiverTests.java
+++ b/core/tests/coretests/src/android/content/BroadcastReceiverTests.java
@@ -49,7 +49,8 @@
         final IntentFilter mockFilter = new IntentFilter("android.content.tests.TestAction");
         try {
             for (int i = 0; i < RECEIVER_LIMIT_PER_APP + 1; i++) {
-                mContext.registerReceiver(new EmptyReceiver(), mockFilter);
+                mContext.registerReceiver(new EmptyReceiver(), mockFilter,
+                        Context.RECEIVER_EXPORTED_UNAUDITED);
             }
             fail("No exception thrown when registering "
                     + (RECEIVER_LIMIT_PER_APP + 1) + " receivers");
diff --git a/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java b/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
index 98485c0..ee73f00 100644
--- a/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
+++ b/core/tests/coretests/src/android/content/pm/ConstrainDisplayApisConfigTest.java
@@ -29,10 +29,11 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
- * Test class for {@link ConstrainDisplayApisConfig}.
+ * Test for {@link ConstrainDisplayApisConfig}.
  *
  * Build/Install/Run:
  * atest FrameworksCoreTests:ConstrainDisplayApisConfigTest
@@ -72,6 +73,7 @@
         testNeverConstrainDisplayApis("com.android.test", /* version= */ 1, /* expected= */ false);
     }
 
+    @Ignore("b/257375674")
     @Test
     public void neverConstrainDisplayApis_flagsHasSingleEntry_returnsTrueForPackageWithinRange() {
         setNeverConstrainDisplayApisFlag("com.android.test:1:1");
@@ -107,6 +109,7 @@
         testNeverConstrainDisplayApis("com.android.test4", /* version= */ 9, /* expected= */ false);
     }
 
+    @Ignore("b/257375674")
     @Test
     public void neverConstrainDisplayApis_flagHasInvalidEntries_ignoresInvalidEntries() {
         // We add a valid entry before and after the invalid ones to make sure they are applied.
diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerPropertyTests.java b/core/tests/coretests/src/android/content/pm/PackageManagerPropertyTests.java
index d505492..86e95832 100644
--- a/core/tests/coretests/src/android/content/pm/PackageManagerPropertyTests.java
+++ b/core/tests/coretests/src/android/content/pm/PackageManagerPropertyTests.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 import android.content.pm.PackageManager.Property;
@@ -162,40 +163,30 @@
 
     @Test
     public void testProperty_invalidName() throws Exception {
-        try {
+        assertThrows(NullPointerException.class, () -> {
             final Property p = new Property(null, 1, "android", null);
-            fail("expected assertion error");
-        } catch (AssertionError expected) {
-        }
+        });
     }
 
     @Test
     public void testProperty_invalidType() throws Exception {
-        try {
+        assertThrows(IllegalArgumentException.class, () -> {
             final Property p = new Property("invalidTypeProperty", 0, "android", null);
-            fail("expected assertion error");
-        } catch (AssertionError expected) {
-        }
+        });
 
-        try {
+        assertThrows(IllegalArgumentException.class, () -> {
             final Property p = new Property("invalidTypeProperty", 6, "android", null);
-            fail("expected assertion error");
-        } catch (AssertionError expected) {
-        }
+        });
 
-        try {
+        assertThrows(IllegalArgumentException.class, () -> {
             final Property p = new Property("invalidTypeProperty", -1, "android", null);
-            fail("expected assertion error");
-        } catch (AssertionError expected) {
-        }
+        });
     }
 
     @Test
     public void testProperty_noPackageName() throws Exception {
-        try {
+        assertThrows(NullPointerException.class, () -> {
             final Property p = new Property(null, 1, null, null);
-            fail("expected assertion error");
-        } catch (AssertionError expected) {
-        }
+        });
     }
 }
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index fa4952e1..5553902 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -25,11 +25,12 @@
 import android.test.AndroidTestCase;
 import android.util.AttributeSet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import androidx.test.filters.LargeTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
diff --git a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
index e750454..1c7ab74 100644
--- a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
+++ b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
@@ -23,13 +23,14 @@
 
 import android.os.Parcel;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/tests/coretests/src/android/os/BundleMergerTest.java b/core/tests/coretests/src/android/os/BundleMergerTest.java
new file mode 100644
index 0000000..b7012ba
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BundleMergerTest.java
@@ -0,0 +1,408 @@
+/*
+ * 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 static android.os.BundleMerger.STRATEGY_ARRAY_APPEND;
+import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND;
+import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND;
+import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR;
+import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX;
+import static android.os.BundleMerger.STRATEGY_COMPARABLE_MIN;
+import static android.os.BundleMerger.STRATEGY_FIRST;
+import static android.os.BundleMerger.STRATEGY_LAST;
+import static android.os.BundleMerger.STRATEGY_NUMBER_ADD;
+import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST;
+import static android.os.BundleMerger.STRATEGY_REJECT;
+import static android.os.BundleMerger.merge;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class BundleMergerTest {
+    /**
+     * Strategies are only applied when there is an actual conflict; in the
+     * absence of conflict we pick whichever value is defined.
+     */
+    @Test
+    public void testNoConflict() throws Exception {
+        for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) {
+            assertEquals(null, merge(strategy, null, null));
+            assertEquals(10, merge(strategy, 10, null));
+            assertEquals(20, merge(strategy, null, 20));
+        }
+    }
+
+    /**
+     * Strategies are only applied to identical data types; if there are mixed
+     * types we always reject the two conflicting values.
+     */
+    @Test
+    public void testMixedTypes() throws Exception {
+        for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) {
+            final int finalStrategy = strategy;
+            assertThrows(Exception.class, () -> {
+                merge(finalStrategy, 10, "foo");
+            });
+            assertThrows(Exception.class, () -> {
+                merge(finalStrategy, List.of("foo"), "bar");
+            });
+            assertThrows(Exception.class, () -> {
+                merge(finalStrategy, new String[] { "foo" }, "bar");
+            });
+            assertThrows(Exception.class, () -> {
+                merge(finalStrategy, Integer.valueOf(10), Long.valueOf(10));
+            });
+        }
+    }
+
+    @Test
+    public void testStrategyReject() throws Exception {
+        assertEquals(null, merge(STRATEGY_REJECT, 10, 20));
+
+        // Identical values aren't technically a conflict, so they're passed
+        // through without being rejected
+        assertEquals(10, merge(STRATEGY_REJECT, 10, 10));
+        assertArrayEquals(new int[] {10},
+                (int[]) merge(STRATEGY_REJECT, new int[] {10}, new int[] {10}));
+    }
+
+    @Test
+    public void testStrategyFirst() throws Exception {
+        assertEquals(10, merge(STRATEGY_FIRST, 10, 20));
+    }
+
+    @Test
+    public void testStrategyLast() throws Exception {
+        assertEquals(20, merge(STRATEGY_LAST, 10, 20));
+    }
+
+    @Test
+    public void testStrategyComparableMin() throws Exception {
+        assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 10, 20));
+        assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 20, 10));
+        assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "a", "z"));
+        assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "z", "a"));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_COMPARABLE_MIN, new Binder(), new Binder());
+        });
+    }
+
+    @Test
+    public void testStrategyComparableMax() throws Exception {
+        assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 10, 20));
+        assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 20, 10));
+        assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "a", "z"));
+        assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "z", "a"));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_COMPARABLE_MAX, new Binder(), new Binder());
+        });
+    }
+
+    @Test
+    public void testStrategyNumberAdd() throws Exception {
+        assertEquals(30, merge(STRATEGY_NUMBER_ADD, 10, 20));
+        assertEquals(30, merge(STRATEGY_NUMBER_ADD, 20, 10));
+        assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 10L, 20L));
+        assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 20L, 10L));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_NUMBER_ADD, new Binder(), new Binder());
+        });
+    }
+
+    @Test
+    public void testStrategyNumberIncrementFirst() throws Exception {
+        assertEquals(11, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10, 20));
+        assertEquals(21, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20, 10));
+        assertEquals(11L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10L, 20L));
+        assertEquals(21L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20L, 10L));
+    }
+
+    @Test
+    public void testStrategyBooleanAnd() throws Exception {
+        assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, false));
+        assertEquals(false, merge(STRATEGY_BOOLEAN_AND, true, false));
+        assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, true));
+        assertEquals(true, merge(STRATEGY_BOOLEAN_AND, true, true));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_BOOLEAN_AND, "True!", "False?");
+        });
+    }
+
+    @Test
+    public void testStrategyBooleanOr() throws Exception {
+        assertEquals(false, merge(STRATEGY_BOOLEAN_OR, false, false));
+        assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, false));
+        assertEquals(true, merge(STRATEGY_BOOLEAN_OR, false, true));
+        assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, true));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_BOOLEAN_OR, "True!", "False?");
+        });
+    }
+
+    @Test
+    public void testStrategyArrayAppend() throws Exception {
+        assertArrayEquals(new int[] {},
+                (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {}));
+        assertArrayEquals(new int[] {10},
+                (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {}));
+        assertArrayEquals(new int[] {20},
+                (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {20}));
+        assertArrayEquals(new int[] {10, 20},
+                (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {20}));
+        assertArrayEquals(new int[] {10, 30, 20, 40},
+                (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10, 30}, new int[] {20, 40}));
+        assertArrayEquals(new String[] {"a", "b"},
+                (String[]) merge(STRATEGY_ARRAY_APPEND, new String[] {"a"}, new String[] {"b"}));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_ARRAY_APPEND, 10, 20);
+        });
+    }
+
+    @Test
+    public void testStrategyArrayListAppend() throws Exception {
+        assertEquals(arrayListOf(),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf()));
+        assertEquals(arrayListOf(10),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf()));
+        assertEquals(arrayListOf(20),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf(20)));
+        assertEquals(arrayListOf(10, 20),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf(20)));
+        assertEquals(arrayListOf(10, 30, 20, 40),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10, 30), arrayListOf(20, 40)));
+        assertEquals(arrayListOf("a", "b"),
+                merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf("a"), arrayListOf("b")));
+
+        assertThrows(Exception.class, () -> {
+            merge(STRATEGY_ARRAY_LIST_APPEND, 10, 20);
+        });
+    }
+
+    @Test
+    public void testMerge_Simple() throws Exception {
+        final BundleMerger merger = new BundleMerger();
+        final Bundle probe = new Bundle();
+        probe.putInt(Intent.EXTRA_INDEX, 42);
+
+        assertEquals(null, merger.merge(null, null));
+        assertEquals(probe.keySet(), merger.merge(probe, null).keySet());
+        assertEquals(probe.keySet(), merger.merge(null, probe).keySet());
+        assertEquals(probe.keySet(), merger.merge(probe, probe).keySet());
+    }
+
+    /**
+     * Verify that we can merge parcelables present in the base classpath, since
+     * everyone on the device will be able to unpack them.
+     */
+    @Test
+    public void testMerge_Parcelable_BCP() throws Exception {
+        final BundleMerger merger = new BundleMerger();
+        merger.setMergeStrategy(Intent.EXTRA_STREAM, STRATEGY_COMPARABLE_MIN);
+
+        Bundle a = new Bundle();
+        a.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.com"));
+        a = parcelAndUnparcel(a);
+
+        Bundle b = new Bundle();
+        b.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.net"));
+        b = parcelAndUnparcel(b);
+
+        assertEquals(Uri.parse("http://example.com"),
+                merger.merge(a, b).getParcelable(Intent.EXTRA_STREAM, Uri.class));
+        assertEquals(Uri.parse("http://example.com"),
+                merger.merge(b, a).getParcelable(Intent.EXTRA_STREAM, Uri.class));
+    }
+
+    /**
+     * Verify that we tiptoe around custom parcelables while still merging other
+     * known data types. Custom parcelables aren't in the base classpath, so not
+     * everyone on the device will be able to unpack them.
+     */
+    @Test
+    public void testMerge_Parcelable_Custom() throws Exception {
+        final BundleMerger merger = new BundleMerger();
+        merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_NUMBER_ADD);
+
+        Bundle a = new Bundle();
+        a.putInt(Intent.EXTRA_INDEX, 10);
+        a.putString(Intent.EXTRA_CC, "foo@bar.com");
+        a.putParcelable(Intent.EXTRA_SUBJECT, new ExplodingParcelable());
+        a = parcelAndUnparcel(a);
+
+        Bundle b = new Bundle();
+        b.putInt(Intent.EXTRA_INDEX, 20);
+        a.putString(Intent.EXTRA_BCC, "foo@baz.com");
+        b.putParcelable(Intent.EXTRA_STREAM, new ExplodingParcelable());
+        b = parcelAndUnparcel(b);
+
+        Bundle ab = merger.merge(a, b);
+        assertEquals(Set.of(Intent.EXTRA_INDEX, Intent.EXTRA_CC, Intent.EXTRA_BCC,
+                Intent.EXTRA_SUBJECT, Intent.EXTRA_STREAM), ab.keySet());
+        assertEquals(30, ab.getInt(Intent.EXTRA_INDEX));
+        assertEquals("foo@bar.com", ab.getString(Intent.EXTRA_CC));
+        assertEquals("foo@baz.com", ab.getString(Intent.EXTRA_BCC));
+
+        // And finally, make sure that if we try unpacking one of our custom
+        // values that we actually explode
+        assertThrows(BadParcelableException.class, () -> {
+            ab.getParcelable(Intent.EXTRA_SUBJECT, ExplodingParcelable.class);
+        });
+        assertThrows(BadParcelableException.class, () -> {
+            ab.getParcelable(Intent.EXTRA_STREAM, ExplodingParcelable.class);
+        });
+    }
+
+    @Test
+    public void testMerge_PackageChanged() throws Exception {
+        final BundleMerger merger = new BundleMerger();
+        merger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, STRATEGY_ARRAY_APPEND);
+
+        final Bundle first = new Bundle();
+        first.putInt(Intent.EXTRA_UID, 10001);
+        first.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] {
+                "com.example.Foo",
+        });
+
+        final Bundle second = new Bundle();
+        second.putInt(Intent.EXTRA_UID, 10001);
+        second.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] {
+                "com.example.Bar",
+                "com.example.Baz",
+        });
+
+        final Bundle res = merger.merge(first, second);
+        assertEquals(10001, res.getInt(Intent.EXTRA_UID));
+        assertArrayEquals(new String[] {
+                "com.example.Foo", "com.example.Bar", "com.example.Baz",
+        }, res.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST));
+    }
+
+    /**
+     * Each event in isolation reports "zero events dropped", but if we need to
+     * merge them together, then we start incrementing.
+     */
+    @Test
+    public void testMerge_DropBox() throws Exception {
+        final BundleMerger merger = new BundleMerger();
+        merger.setMergeStrategy(DropBoxManager.EXTRA_TIME,
+                STRATEGY_COMPARABLE_MAX);
+        merger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT,
+                STRATEGY_NUMBER_INCREMENT_FIRST);
+
+        final long now = System.currentTimeMillis();
+        final Bundle a = new Bundle();
+        a.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+        a.putLong(DropBoxManager.EXTRA_TIME, now);
+        a.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+        final Bundle b = new Bundle();
+        b.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+        b.putLong(DropBoxManager.EXTRA_TIME, now + 1000);
+        b.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+        final Bundle c = new Bundle();
+        c.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+        c.putLong(DropBoxManager.EXTRA_TIME, now + 2000);
+        c.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+        final Bundle ab = merger.merge(a, b);
+        assertEquals("system_server_strictmode", ab.getString(DropBoxManager.EXTRA_TAG));
+        assertEquals(now + 1000, ab.getLong(DropBoxManager.EXTRA_TIME));
+        assertEquals(1, ab.getInt(DropBoxManager.EXTRA_DROPPED_COUNT));
+
+        final Bundle abc = merger.merge(ab, c);
+        assertEquals("system_server_strictmode", abc.getString(DropBoxManager.EXTRA_TAG));
+        assertEquals(now + 2000, abc.getLong(DropBoxManager.EXTRA_TIME));
+        assertEquals(2, abc.getInt(DropBoxManager.EXTRA_DROPPED_COUNT));
+    }
+
+    private static ArrayList<Object> arrayListOf(Object... values) {
+        final ArrayList<Object> res = new ArrayList<>(values.length);
+        for (Object value : values) {
+            res.add(value);
+        }
+        return res;
+    }
+
+    private static Bundle parcelAndUnparcel(Bundle input) {
+        final Parcel parcel = Parcel.obtain();
+        try {
+            input.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            return Bundle.CREATOR.createFromParcel(parcel);
+        } finally {
+            parcel.recycle();
+        }
+    }
+
+    /**
+     * Object that only offers to parcel itself; if something tries unparceling
+     * it, it will "explode" by throwing an exception.
+     * <p>
+     * Useful for verifying interactions that must leave unknown data in a
+     * parceled state.
+     */
+    public static class ExplodingParcelable implements Parcelable {
+        public ExplodingParcelable() {
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(42);
+        }
+
+        public static final Creator<ExplodingParcelable> CREATOR =
+                new Creator<ExplodingParcelable>() {
+                    @Override
+                    public ExplodingParcelable createFromParcel(Parcel in) {
+                        throw new BadParcelableException("exploding!");
+                    }
+
+                    @Override
+                    public ExplodingParcelable[] newArray(int size) {
+                        throw new BadParcelableException("exploding!");
+                    }
+                };
+    }
+}
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/os/PerformanceHintManagerTest.java b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
index 69eb13f..d1d14f6 100644
--- a/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
+++ b/core/tests/coretests/src/android/os/PerformanceHintManagerTest.java
@@ -114,6 +114,23 @@
     }
 
     @Test
+    public void testSendHint() {
+        Session s = createSession();
+        assumeNotNull(s);
+        s.sendHint(Session.CPU_LOAD_UP);
+        s.sendHint(Session.CPU_LOAD_RESET);
+    }
+
+    @Test
+    public void testSendHintWithNegativeHint() {
+        Session s = createSession();
+        assumeNotNull(s);
+        assertThrows(IllegalArgumentException.class, () -> {
+            s.sendHint(-1);
+        });
+    }
+
+    @Test
     public void testCloseHintSession() {
         Session s = createSession();
         assumeNotNull(s);
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 7ebebc9..c59a3f5 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -246,10 +246,12 @@
     @Test
     public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(1f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(2f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
                 .build();
@@ -258,6 +260,7 @@
 
         assertTrue(Float.isNaN(info.getQFactor()));
         assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
 
         // One vibrator with values undefined.
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
@@ -266,16 +269,19 @@
 
         assertTrue(Float.isNaN(info.getQFactor()));
         assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(10f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
                         /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setQFactor(10f)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
                         /* resonantFrequencyHz= */ 11, 5, 1, null))
@@ -285,113 +291,131 @@
 
         assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
         assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+
+        // No frequency range defined.
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
     }
 
     @Test
     public void getFrequencyProfile_noVibrator_returnsEmpty() {
         VibratorInfo info = new SystemVibrator.NoVibratorInfo();
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, differentResonantFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_missingValues_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
                         new float[] { 0, 1 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingResonantFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingMinFrequency});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
                         new float[] { 0, 1 }))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
 
         VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
                 .build();
         info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 0, 1, 1, 0 }))
                 .build();
         VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
                 new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
 
-        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEmptyFrequencyProfileAndControl(info);
     }
 
     @Test
     public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
         VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
                         new float[] { 0.5f, 1, 1, 0.5f }))
                 .build();
         VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 1, 1, 1 }))
                 .build();
         VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
                         new float[] { 0.8f, 1, 0.8f, 0.5f }))
                 .build();
@@ -401,6 +425,20 @@
         assertEquals(
                 new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
                 info.getFrequencyProfile());
+        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+        // Third vibrator without frequency control capability.
+        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+        info = new SystemVibrator.MultiVibratorInfo(
+                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
     }
 
     @Test
@@ -547,4 +585,12 @@
         VibrationAttributes vibrationAttributes = captor.getValue();
         assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
     }
+
+    /**
+     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+     */
+    void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
 }
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
index c8de190..7f772dd 100644
--- a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
@@ -16,11 +16,19 @@
 
 package android.service.timezone;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.time.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -31,8 +39,114 @@
 
 public class TimeZoneProviderEventTest {
 
+    public static final TimeZoneProviderStatus ARBITRARY_TIME_ZONE_PROVIDER_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                    .build();
+
+    @Test
+    public void createPermanentFailure() {
+        long creationElapsedMillis = 1111L;
+        String cause = "Cause";
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent(
+                creationElapsedMillis, cause);
+
+        assertEquals(EVENT_TYPE_PERMANENT_FAILURE, event.getType());
+        assertEquals(cause, event.getFailureCause());
+        assertEquals(creationElapsedMillis, event.getCreationElapsedMillis());
+        assertNull(event.getSuggestion());
+        assertNull(event.getTimeZoneProviderStatus());
+    }
+
+    @Test
+    public void createSuggestion() {
+        long creationElapsedMillis = 1111L;
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(2222L)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+
+        TimeZoneProviderStatus reportedStatus = new TimeZoneProviderStatus.Builder()
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+
+        assertThrows(NullPointerException.class, () -> TimeZoneProviderEvent.createSuggestionEvent(
+                creationElapsedMillis, /*suggestion=*/null, reportedStatus));
+
+        // Only TimeZoneProvider can report itself certain.
+        {
+            TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                    creationElapsedMillis, suggestion, reportedStatus);
+            assertEquals(EVENT_TYPE_SUGGESTION, event.getType());
+            assertEquals(creationElapsedMillis, event.getCreationElapsedMillis());
+            assertNull(event.getFailureCause());
+            assertEquals(suggestion, event.getSuggestion());
+        }
+
+        // Legacy API events can be created where the TimeZoneProviderStatus is omitted.
+        {
+            TimeZoneProviderStatus legacyStatus = null;
+            TimeZoneProviderEvent legacyEvent = TimeZoneProviderEvent.createSuggestionEvent(
+                    creationElapsedMillis, suggestion, legacyStatus);
+            assertEquals(legacyStatus, legacyEvent.getTimeZoneProviderStatus());
+        }
+    }
+
+    @Test
+    public void createUncertain() {
+        long creationElapsedMillis = 1111L;
+
+        // The TimeZoneProvider can report itself uncertain.
+        {
+            TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(
+                    creationElapsedMillis, ARBITRARY_TIME_ZONE_PROVIDER_STATUS);
+            assertEquals(EVENT_TYPE_UNCERTAIN, event.getType());
+            assertEquals(creationElapsedMillis, event.getCreationElapsedMillis());
+            assertNull(event.getFailureCause());
+            assertNull(event.getSuggestion());
+        }
+
+        // Legacy API events can be created where the TimeZoneProviderStatus is omitted.
+        {
+            TimeZoneProviderStatus legacyStatus = null;
+            TimeZoneProviderEvent legacyEvent = TimeZoneProviderEvent.createUncertainEvent(
+                    creationElapsedMillis, legacyStatus);
+            assertEquals(legacyStatus, legacyEvent.getTimeZoneProviderStatus());
+        }
+    }
+
     @Test
     public void isEquivalentToAndEquals() {
+        long creationElapsedMillis = 1111L;
+        TimeZoneProviderEvent failEvent =
+                TimeZoneProviderEvent.createPermanentFailureEvent(creationElapsedMillis, "one");
+        TimeZoneProviderStatus providerStatus = ARBITRARY_TIME_ZONE_PROVIDER_STATUS;
+
+        TimeZoneProviderEvent uncertainEvent =
+                TimeZoneProviderEvent.createUncertainEvent(creationElapsedMillis, providerStatus);
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(creationElapsedMillis)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+        TimeZoneProviderEvent suggestionEvent = TimeZoneProviderEvent.createSuggestionEvent(
+                creationElapsedMillis, suggestion, providerStatus);
+
+        assertNotEquals(failEvent, uncertainEvent);
+        assertNotEquivalentTo(failEvent, uncertainEvent);
+
+        assertNotEquals(failEvent, suggestionEvent);
+        assertNotEquivalentTo(failEvent, suggestionEvent);
+
+        assertNotEquals(uncertainEvent, suggestionEvent);
+        assertNotEquivalentTo(uncertainEvent, suggestionEvent);
+    }
+
+    @Test
+    public void isEquivalentToAndEquals_permanentFailure() {
         TimeZoneProviderEvent fail1v1 =
                 TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "one");
         assertEquals(fail1v1, fail1v1);
@@ -51,44 +165,79 @@
             assertNotEquals(fail1v1, fail2);
             assertIsEquivalentTo(fail1v1, fail2);
         }
+    }
 
-        TimeZoneProviderEvent uncertain1v1 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+    @Test
+    public void isEquivalentToAndEquals_uncertain() {
+        TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+        TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+                .build();
+
+        TimeZoneProviderEvent uncertain1v1 =
+                TimeZoneProviderEvent.createUncertainEvent(1111L, status1);
         assertEquals(uncertain1v1, uncertain1v1);
         assertIsEquivalentTo(uncertain1v1, uncertain1v1);
         assertNotEquals(uncertain1v1, null);
         assertNotEquivalentTo(uncertain1v1, null);
 
         {
-            TimeZoneProviderEvent uncertain1v2 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+            TimeZoneProviderEvent uncertain1v2 =
+                    TimeZoneProviderEvent.createUncertainEvent(1111L, status1);
             assertEquals(uncertain1v1, uncertain1v2);
             assertIsEquivalentTo(uncertain1v1, uncertain1v2);
 
-            TimeZoneProviderEvent uncertain2 = TimeZoneProviderEvent.createUncertainEvent(2222L);
+            TimeZoneProviderEvent uncertain2 =
+                    TimeZoneProviderEvent.createUncertainEvent(2222L, status1);
             assertNotEquals(uncertain1v1, uncertain2);
             assertIsEquivalentTo(uncertain1v1, uncertain2);
-        }
 
+            TimeZoneProviderEvent uncertain3 =
+                    TimeZoneProviderEvent.createUncertainEvent(1111L, status2);
+            assertNotEquals(uncertain1v1, uncertain3);
+            assertNotEquivalentTo(uncertain1v1, uncertain3);
+        }
+    }
+
+    @Test
+    public void isEquivalentToAndEquals_suggestion() {
+        TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+        TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+                .build();
         TimeZoneProviderSuggestion suggestion1 = new TimeZoneProviderSuggestion.Builder()
                 .setElapsedRealtimeMillis(1111L)
                 .setTimeZoneIds(Collections.singletonList("Europe/London"))
                 .build();
         TimeZoneProviderEvent certain1v1 =
-                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1);
         assertEquals(certain1v1, certain1v1);
         assertIsEquivalentTo(certain1v1, certain1v1);
         assertNotEquals(certain1v1, null);
         assertNotEquivalentTo(certain1v1, null);
 
         {
-            // Same suggestion, same time.
+            // Same time, suggestion, and status.
             TimeZoneProviderEvent certain1v2 =
-                    TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+                    TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1);
             assertEquals(certain1v1, certain1v2);
             assertIsEquivalentTo(certain1v1, certain1v2);
 
-            // Same suggestion, different time.
+            // Different time, same suggestion and status.
             TimeZoneProviderEvent certain1v3 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status1);
             assertNotEquals(certain1v1, certain1v3);
             assertIsEquivalentTo(certain1v1, certain1v3);
 
@@ -100,7 +249,7 @@
             assertNotEquals(suggestion1, suggestion2);
             TimeZoneProviderSuggestionTest.assertIsEquivalentTo(suggestion1, suggestion2);
             TimeZoneProviderEvent certain2 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2, status1);
             assertNotEquals(certain1v1, certain2);
             assertIsEquivalentTo(certain1v1, certain2);
 
@@ -109,16 +258,15 @@
                     .setTimeZoneIds(Collections.singletonList("Europe/Paris"))
                     .build();
             TimeZoneProviderEvent certain3 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3, status1);
             assertNotEquals(certain1v1, certain3);
             assertNotEquivalentTo(certain1v1, certain3);
+
+            TimeZoneProviderEvent certain4 =
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status2);
+            assertNotEquals(certain1v1, certain4);
+            assertNotEquivalentTo(certain1v1, certain4);
         }
-
-        assertNotEquals(fail1v1, uncertain1v1);
-        assertNotEquivalentTo(fail1v1, uncertain1v1);
-
-        assertNotEquals(fail1v1, certain1v1);
-        assertNotEquivalentTo(fail1v1, certain1v1);
     }
 
     @Test
@@ -130,7 +278,14 @@
 
     @Test
     public void testParcelable_uncertain() {
-        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(1111L);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(
+                1111L, ARBITRARY_TIME_ZONE_PROVIDER_STATUS);
+        assertRoundTripParcelable(event);
+    }
+
+    @Test
+    public void testParcelable_uncertain_legacy() {
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(1111L, null);
         assertRoundTripParcelable(event);
     }
 
@@ -139,8 +294,18 @@
         TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
                 .setTimeZoneIds(Arrays.asList("Europe/London", "Europe/Paris"))
                 .build();
-        TimeZoneProviderEvent event =
-                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                1111L, suggestion, ARBITRARY_TIME_ZONE_PROVIDER_STATUS);
+        assertRoundTripParcelable(event);
+    }
+
+    @Test
+    public void testParcelable_suggestion_legacy() {
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(Arrays.asList("Europe/London", "Europe/Paris"))
+                .build();
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                1111L, suggestion, null);
         assertRoundTripParcelable(event);
     }
 
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java
new file mode 100644
index 0000000..9006cd9
--- /dev/null
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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.
+ */
+
+package android.service.timezone;
+
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/** Non-SDK tests. See CTS for SDK API tests. */
+public class TimeZoneProviderStatusTest {
+
+    @Test
+    public void parseProviderStatus() {
+        TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder()
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+
+        assertEquals(status, TimeZoneProviderStatus.parseProviderStatus(status.toString()));
+    }
+}
diff --git a/core/tests/coretests/src/android/text/format/DateFormatTest.java b/core/tests/coretests/src/android/text/format/DateFormatTest.java
index 212cc44..8459330 100644
--- a/core/tests/coretests/src/android/text/format/DateFormatTest.java
+++ b/core/tests/coretests/src/android/text/format/DateFormatTest.java
@@ -156,8 +156,8 @@
     @DisableCompatChanges({DateFormat.DISALLOW_DUPLICATE_FIELD_IN_SKELETON})
     public void testGetBestDateTimePattern_enableDuplicateField() {
         // en-US uses 12-hour format by default.
-        assertEquals("h:mm a", DateFormat.getBestDateTimePattern(Locale.US, "jmma"));
-        assertEquals("h:mm a", DateFormat.getBestDateTimePattern(Locale.US, "ahmma"));
+        assertEquals("h:mm\u202fa", DateFormat.getBestDateTimePattern(Locale.US, "jmma"));
+        assertEquals("h:mm\u202fa", DateFormat.getBestDateTimePattern(Locale.US, "ahmma"));
     }
 
     private static void assertIllegalArgumentException(Locale l, String skeleton) {
diff --git a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
index 9c06395..de7244d 100644
--- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
+++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
@@ -93,7 +93,8 @@
         assertEquals("January 19",
                 formatDateRange(en_US, tz, timeWithCurrentYear, timeWithCurrentYear + HOUR,
                         FORMAT_SHOW_DATE));
-        assertEquals("3:30 AM", formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_SHOW_TIME));
+        assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime,
+                FORMAT_SHOW_TIME));
         assertEquals("January 19, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + HOUR, FORMAT_SHOW_YEAR));
         assertEquals("January 19",
@@ -101,27 +102,27 @@
         assertEquals("January",
                 formatDateRange(en_US, tz, timeWithCurrentYear, timeWithCurrentYear + HOUR,
                         FORMAT_NO_MONTH_DAY));
-        assertEquals("3:30 AM",
+        assertEquals("3:30\u202fAM",
                 formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_12HOUR | FORMAT_SHOW_TIME));
         assertEquals("03:30",
                 formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_24HOUR | FORMAT_SHOW_TIME));
-        assertEquals("3:30 AM", formatDateRange(en_US, tz, fixedTime, fixedTime,
+        assertEquals("3:30\u202fAM", formatDateRange(en_US, tz, fixedTime, fixedTime,
                 FORMAT_12HOUR /*| FORMAT_CAP_AMPM*/ | FORMAT_SHOW_TIME));
-        assertEquals("12:00 PM",
+        assertEquals("12:00\u202fPM",
                 formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration,
                         FORMAT_12HOUR | FORMAT_SHOW_TIME));
-        assertEquals("12:00 PM",
+        assertEquals("12:00\u202fPM",
                 formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration,
                         FORMAT_12HOUR | FORMAT_SHOW_TIME /*| FORMAT_CAP_NOON*/));
-        assertEquals("12:00 PM",
+        assertEquals("12:00\u202fPM",
                 formatDateRange(en_US, tz, fixedTime + noonDuration, fixedTime + noonDuration,
                         FORMAT_12HOUR /*| FORMAT_NO_NOON*/ | FORMAT_SHOW_TIME));
-        assertEquals("12:00 AM", formatDateRange(en_US, tz, fixedTime - midnightDuration,
+        assertEquals("12:00\u202fAM", formatDateRange(en_US, tz, fixedTime - midnightDuration,
                 fixedTime - midnightDuration,
                 FORMAT_12HOUR | FORMAT_SHOW_TIME /*| FORMAT_NO_MIDNIGHT*/));
-        assertEquals("3:30 AM",
+        assertEquals("3:30\u202fAM",
                 formatDateRange(en_US, tz, fixedTime, fixedTime, FORMAT_SHOW_TIME | FORMAT_UTC));
-        assertEquals("3 AM", formatDateRange(en_US, tz, onTheHour, onTheHour,
+        assertEquals("3\u202fAM", formatDateRange(en_US, tz, onTheHour, onTheHour,
                 FORMAT_SHOW_TIME | FORMAT_ABBREV_TIME));
         assertEquals("Mon", formatDateRange(en_US, tz, fixedTime, fixedTime + HOUR,
                 FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_WEEKDAY));
@@ -134,13 +135,13 @@
 
         assertEquals("1/19/2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * HOUR,
                 FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("1/19/2009 – 1/22/2009",
+        assertEquals("1/19/2009\u2009\u2013\u20091/22/2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("1/19/2009 – 4/22/2009",
+        assertEquals("1/19/2009\u2009\u2013\u20094/22/2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("1/19/2009 – 2/9/2012",
+        assertEquals("1/19/2009\u2009\u2013\u20092/9/2012",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
 
@@ -151,7 +152,7 @@
         assertEquals("19.01. – 22.04.2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("19.01.2009 – 09.02.2012",
+        assertEquals("19.01.2009\u2009\u2013\u200909.02.2012",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
 
@@ -169,48 +170,48 @@
 
         assertEquals("19/1/2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + HOUR,
                 FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("19/1/2009 – 22/1/2009",
+        assertEquals("19/1/2009\u2009\u2013\u200922/1/2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("19/1/2009 – 22/4/2009",
+        assertEquals("19/1/2009\u2009\u2013\u200922/4/2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
-        assertEquals("19/1/2009 – 9/2/2012",
+        assertEquals("19/1/2009\u2009\u2013\u20099/2/2012",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
 
         // These are some random other test cases I came up with.
 
-        assertEquals("January 19 – 22, 2009",
+        assertEquals("January 19\u2009\u2013\u200922, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, 0));
-        assertEquals("Jan 19 – 22, 2009", formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY,
-                FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("Mon, Jan 19 – Thu, Jan 22, 2009",
+        assertEquals("Jan 19\u2009\u2013\u200922, 2009", formatDateRange(en_US, tz, fixedTime,
+                fixedTime + 3 * DAY, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
+        assertEquals("Mon, Jan 19\u2009\u2013\u2009Thu, Jan 22, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
-        assertEquals("Monday, January 19 – Thursday, January 22, 2009",
+        assertEquals("Monday, January 19\u2009\u2013\u2009Thursday, January 22, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY));
 
-        assertEquals("January 19 – April 22, 2009",
+        assertEquals("January 19\u2009\u2013\u2009April 22, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, 0));
-        assertEquals("Jan 19 – Apr 22, 2009",
+        assertEquals("Jan 19\u2009\u2013\u2009Apr 22, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("Mon, Jan 19 – Wed, Apr 22, 2009",
+        assertEquals("Mon, Jan 19\u2009\u2013\u2009Wed, Apr 22, 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
-        assertEquals("January – April 2009",
+        assertEquals("January\u2009\u2013\u2009April 2009",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY));
 
-        assertEquals("Jan 19, 2009 – Feb 9, 2012",
+        assertEquals("Jan 19, 2009\u2009\u2013\u2009Feb 9, 2012",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("Jan 2009 – Feb 2012",
+        assertEquals("Jan 2009\u2009\u2013\u2009Feb 2012",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL));
-        assertEquals("January 19, 2009 – February 9, 2012",
+        assertEquals("January 19, 2009\u2009\u2013\u2009February 9, 2012",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, 0));
-        assertEquals("Monday, January 19, 2009 – Thursday, February 9, 2012",
+        assertEquals("Monday, January 19, 2009\u2009\u2013\u2009Thursday, February 9, 2012",
                 formatDateRange(en_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY));
 
         // The same tests but for de_DE.
@@ -225,26 +226,26 @@
         assertEquals("Montag, 19. – Donnerstag, 22. Januar 2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY));
 
-        assertEquals("19. Januar – 22. April 2009",
+        assertEquals("19. Januar\u2009\u2013\u200922. April 2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, 0));
-        assertEquals("19. Jan. – 22. Apr. 2009",
+        assertEquals("19. Jan.\u2009\u2013\u200922. Apr. 2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("Mo., 19. Jan. – Mi., 22. Apr. 2009",
+        assertEquals("Mo., 19. Jan.\u2009\u2013\u2009Mi., 22. Apr. 2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
         assertEquals("Januar–April 2009",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY));
 
-        assertEquals("19. Jan. 2009 – 9. Feb. 2012",
+        assertEquals("19. Jan. 2009\u2009\u2013\u20099. Feb. 2012",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("Jan. 2009 – Feb. 2012",
+        assertEquals("Jan. 2009\u2009\u2013\u2009Feb. 2012",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL));
-        assertEquals("19. Januar 2009 – 9. Februar 2012",
+        assertEquals("19. Januar 2009\u2009\u2013\u20099. Februar 2012",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, 0));
-        assertEquals("Montag, 19. Januar 2009 – Donnerstag, 9. Februar 2012",
+        assertEquals("Montag, 19. Januar 2009\u2009\u2013\u2009Donnerstag, 9. Februar 2012",
                 formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY));
 
         // The same tests but for es_US.
@@ -254,32 +255,32 @@
         assertEquals("19–22 de ene de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("lun, 19 de ene – jue, 22 de ene de 2009",
+        assertEquals("lun, 19 de ene\u2009\u2013\u2009jue, 22 de ene de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
-        assertEquals("lunes, 19 de enero – jueves, 22 de enero de 2009",
+        assertEquals("lunes, 19 de enero\u2009\u2013\u2009jueves, 22 de enero de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY));
 
-        assertEquals("19 de enero – 22 de abril de 2009",
+        assertEquals("19 de enero\u2009\u2013\u200922 de abril de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, 0));
-        assertEquals("19 de ene – 22 de abr 2009",
+        assertEquals("19 de ene\u2009\u2013\u200922 de abr 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("lun, 19 de ene – mié, 22 de abr de 2009",
+        assertEquals("lun, 19 de ene\u2009\u2013\u2009mié, 22 de abr de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
         assertEquals("enero–abril de 2009",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY));
 
-        assertEquals("19 de ene de 2009 – 9 de feb de 2012",
+        assertEquals("19 de ene de 2009\u2009\u2013\u20099 de feb de 2012",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("ene de 2009 – feb de 2012",
+        assertEquals("ene de 2009\u2009\u2013\u2009feb de 2012",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL));
-        assertEquals("19 de enero de 2009 – 9 de febrero de 2012",
+        assertEquals("19 de enero de 2009\u2009\u2013\u20099 de febrero de 2012",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, 0));
-        assertEquals("lunes, 19 de enero de 2009 – jueves, 9 de febrero de 2012",
+        assertEquals("lunes, 19 de enero de 2009\u2009\u2013\u2009jueves, 9 de febrero de 2012",
                 formatDateRange(es_US, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY));
 
         // The same tests but for es_ES.
@@ -288,32 +289,32 @@
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, 0));
         assertEquals("19–22 ene 2009", formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY,
                 FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("lun, 19 ene – jue, 22 ene 2009",
+        assertEquals("lun, 19 ene\u2009\u2013\u2009jue, 22 ene 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
-        assertEquals("lunes, 19 de enero – jueves, 22 de enero de 2009",
+        assertEquals("lunes, 19 de enero\u2009\u2013\u2009jueves, 22 de enero de 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY));
 
-        assertEquals("19 de enero – 22 de abril de 2009",
+        assertEquals("19 de enero\u2009\u2013\u200922 de abril de 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, 0));
-        assertEquals("19 ene – 22 abr 2009",
+        assertEquals("19 ene\u2009\u2013\u200922 abr 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("lun, 19 ene – mié, 22 abr 2009",
+        assertEquals("lun, 19 ene\u2009\u2013\u2009mié, 22 abr 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH,
                         FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
         assertEquals("enero–abril de 2009",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_NO_MONTH_DAY));
 
-        assertEquals("19 ene 2009 – 9 feb 2012",
+        assertEquals("19 ene 2009\u2009\u2013\u20099 feb 2012",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
-        assertEquals("ene 2009 – feb 2012",
+        assertEquals("ene 2009\u2009\u2013\u2009feb 2012",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR,
                         FORMAT_NO_MONTH_DAY | FORMAT_ABBREV_ALL));
-        assertEquals("19 de enero de 2009 – 9 de febrero de 2012",
+        assertEquals("19 de enero de 2009\u2009\u2013\u20099 de febrero de 2012",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, 0));
-        assertEquals("lunes, 19 de enero de 2009 – jueves, 9 de febrero de 2012",
+        assertEquals("lunes, 19 de enero de 2009\u2009\u2013\u2009jueves, 9 de febrero de 2012",
                 formatDateRange(es_ES, tz, fixedTime, fixedTime + 3 * YEAR, FORMAT_SHOW_WEEKDAY));
     }
 
@@ -330,7 +331,7 @@
         c.set(2046, Calendar.OCTOBER, 4, 3, 30);
         long oct_4_2046 = c.getTimeInMillis();
         int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL;
-        assertEquals("Jan 19, 2042 – Oct 4, 2046",
+        assertEquals("Jan 19, 2042\u2009\u2013\u2009Oct 4, 2046",
                 formatDateRange(l, tz, jan_19_2042, oct_4_2046, flags));
     }
 
@@ -343,15 +344,15 @@
         int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL | FORMAT_SHOW_TIME | FORMAT_24HOUR;
 
         // The Unix epoch is UTC, so 0 is 1970-01-01T00:00Z...
-        assertEquals("Jan 1, 1970, 00:00 – Jan 2, 1970, 00:00",
+        assertEquals("Jan 1, 1970, 00:00\u2009\u2013\u2009Jan 2, 1970, 00:00",
                 formatDateRange(l, utc, 0, DAY + 1, flags));
         // But MTV is hours behind, so 0 was still the afternoon of the previous day...
-        assertEquals("Dec 31, 1969, 16:00 – Jan 1, 1970, 16:00",
+        assertEquals("Dec 31, 1969, 16:00\u2009\u2013\u2009Jan 1, 1970, 16:00",
                 formatDateRange(l, pacific, 0, DAY, flags));
     }
 
     // http://b/10318326 - we can drop the minutes in a 12-hour time if they're zero,
-    // but not if we're using the 24-hour clock. That is: "4 PM" is reasonable, "16" is not.
+    // but not if we're using the 24-hour clock. That is: "4\u202fPM" is reasonable, "16" is not.
     @Test
     public void test10318326() throws Exception {
         long midnight = 0;
@@ -367,23 +368,26 @@
 
         // Full length on-the-hour times.
         assertEquals("00:00", formatDateRange(l, utc, midnight, midnight, time24));
-        assertEquals("12:00 AM", formatDateRange(l, utc, midnight, midnight, time12));
+        assertEquals("12:00\u202fAM", formatDateRange(l, utc, midnight, midnight, time12));
         assertEquals("16:00", formatDateRange(l, utc, teaTime, teaTime, time24));
-        assertEquals("4:00 PM", formatDateRange(l, utc, teaTime, teaTime, time12));
+        assertEquals("4:00\u202fPM", formatDateRange(l, utc, teaTime, teaTime, time12));
 
         // Abbreviated on-the-hour times.
         assertEquals("00:00", formatDateRange(l, utc, midnight, midnight, abbr24));
-        assertEquals("12 AM", formatDateRange(l, utc, midnight, midnight, abbr12));
+        assertEquals("12\u202fAM", formatDateRange(l, utc, midnight, midnight, abbr12));
         assertEquals("16:00", formatDateRange(l, utc, teaTime, teaTime, abbr24));
-        assertEquals("4 PM", formatDateRange(l, utc, teaTime, teaTime, abbr12));
+        assertEquals("4\u202fPM", formatDateRange(l, utc, teaTime, teaTime, abbr12));
 
         // Abbreviated on-the-hour ranges.
-        assertEquals("00:00 – 16:00", formatDateRange(l, utc, midnight, teaTime, abbr24));
-        assertEquals("12 AM – 4 PM", formatDateRange(l, utc, midnight, teaTime, abbr12));
+        assertEquals("00:00\u2009\u2013\u200916:00", formatDateRange(l, utc, midnight, teaTime,
+                abbr24));
+        assertEquals("12\u202fAM\u2009\u2013\u20094\u202fPM", formatDateRange(l, utc, midnight,
+                teaTime, abbr12));
 
         // Abbreviated mixed ranges.
-        assertEquals("00:00 – 16:01", formatDateRange(l, utc, midnight, teaTime + MINUTE, abbr24));
-        assertEquals("12:00 AM – 4:01 PM",
+        assertEquals("00:00\u2009\u2013\u200916:01", formatDateRange(l, utc, midnight,
+                teaTime + MINUTE, abbr24));
+        assertEquals("12:00\u202fAM\u2009\u2013\u20094:01\u202fPM",
                 formatDateRange(l, utc, midnight, teaTime + MINUTE, abbr12));
     }
 
@@ -406,12 +410,12 @@
 
         // Run one millisecond over, though, and you're into the next day.
         long nextMorning = 1 * DAY + 1;
-        assertEquals("Thursday, January 1 – Friday, January 2, 1970",
+        assertEquals("Thursday, January 1\u2009\u2013\u2009Friday, January 2, 1970",
                 formatDateRange(l, utc, midnight, nextMorning, flags));
 
         // But the same reasoning applies for that day.
         long nextMidnight = 2 * DAY;
-        assertEquals("Thursday, January 1 – Friday, January 2, 1970",
+        assertEquals("Thursday, January 1\u2009\u2013\u2009Friday, January 2, 1970",
                 formatDateRange(l, utc, midnight, nextMidnight, flags));
     }
 
@@ -424,9 +428,9 @@
 
         int flags = FORMAT_SHOW_TIME | FORMAT_24HOUR | FORMAT_SHOW_DATE;
 
-        assertEquals("January 1, 1970, 22:00 – 00:00",
+        assertEquals("January 1, 1970, 22:00\u2009\u2013\u200900:00",
                 formatDateRange(l, utc, 22 * HOUR, 24 * HOUR, flags));
-        assertEquals("January 1, 1970 at 22:00 – January 2, 1970 at 00:30",
+        assertEquals("January 1, 1970 at 22:00\u2009\u2013\u2009January 2, 1970 at 00:30",
                 formatDateRange(l, utc, 22 * HOUR, 24 * HOUR + 30 * MINUTE, flags));
     }
 
@@ -443,9 +447,9 @@
         c.clear();
         c.set(1980, Calendar.JANUARY, 1, 0, 0);
         long jan_1_1980 = c.getTimeInMillis();
-        assertEquals("January 1, 1980, 22:00 – 00:00",
+        assertEquals("January 1, 1980, 22:00\u2009\u2013\u200900:00",
                 formatDateRange(l, utc, jan_1_1980 + 22 * HOUR, jan_1_1980 + 24 * HOUR, flags));
-        assertEquals("January 1, 1980 at 22:00 – January 2, 1980 at 00:30",
+        assertEquals("January 1, 1980 at 22:00\u2009\u2013\u2009January 2, 1980 at 00:30",
                 formatDateRange(l, utc, jan_1_1980 + 22 * HOUR,
                         jan_1_1980 + 24 * HOUR + 30 * MINUTE, flags));
     }
@@ -463,12 +467,12 @@
         c.clear();
         c.set(1980, Calendar.JANUARY, 1, 0, 0);
         long jan_1_1980 = c.getTimeInMillis();
-        assertEquals("January 1, 1980, 22:00 – 00:00",
+        assertEquals("January 1, 1980, 22:00\u2009\u2013\u200900:00",
                 formatDateRange(l, pacific, jan_1_1980 + 22 * HOUR, jan_1_1980 + 24 * HOUR, flags));
 
         c.set(1980, Calendar.JULY, 1, 0, 0);
         long jul_1_1980 = c.getTimeInMillis();
-        assertEquals("July 1, 1980, 22:00 – 00:00",
+        assertEquals("July 1, 1980, 22:00\u2009\u2013\u200900:00",
                 formatDateRange(l, pacific, jul_1_1980 + 22 * HOUR, jul_1_1980 + 24 * HOUR, flags));
     }
 
@@ -531,11 +535,13 @@
                 formatDateRange(l, utc, oldYear, oldYear, FORMAT_SHOW_DATE | FORMAT_NO_YEAR));
 
         // ...or the start and end years aren't the same...
-        assertEquals(String.format("February 10, 1980 – February 10, %d", c.get(Calendar.YEAR)),
+        assertEquals(String.format("February 10, 1980\u2009\u2013\u2009February 10, %d",
+                        c.get(Calendar.YEAR)),
                 formatDateRange(l, utc, oldYear, thisYear, FORMAT_SHOW_DATE));
 
         // (And you can't avoid that --- icu4c steps in and overrides you.)
-        assertEquals(String.format("February 10, 1980 – February 10, %d", c.get(Calendar.YEAR)),
+        assertEquals(String.format("February 10, 1980\u2009\u2013\u2009February 10, %d",
+                        c.get(Calendar.YEAR)),
                 formatDateRange(l, utc, oldYear, thisYear, FORMAT_SHOW_DATE | FORMAT_NO_YEAR));
     }
 
@@ -595,7 +601,7 @@
                 formatDateRange(new ULocale("fa"), utc, thisYear, thisYear, flags));
         assertEquals("يونۍ د ۱۹۸۰ د فبروري ۱۰",
                 formatDateRange(new ULocale("ps"), utc, thisYear, thisYear, flags));
-        assertEquals("วันอาทิตย์ที่ 10 กุมภาพันธ์ ค.ศ. 1980",
+        assertEquals("วันอาทิตย์ที่ 10 กุมภาพันธ์ 1980",
                 formatDateRange(new ULocale("th"), utc, thisYear, thisYear, flags));
     }
 
@@ -607,9 +613,12 @@
 
         int flags = FORMAT_SHOW_TIME | FORMAT_ABBREV_ALL | FORMAT_12HOUR;
 
-        assertEquals("10 – 11 AM", formatDateRange(l, utc, 10 * HOUR, 11 * HOUR, flags));
-        assertEquals("11 AM – 1 PM", formatDateRange(l, utc, 11 * HOUR, 13 * HOUR, flags));
-        assertEquals("2 – 3 PM", formatDateRange(l, utc, 14 * HOUR, 15 * HOUR, flags));
+        assertEquals("10\u2009\u2013\u200911\u202fAM", formatDateRange(l, utc,
+                10 * HOUR, 11 * HOUR, flags));
+        assertEquals("11\u202fAM\u2009\u2013\u20091\u202fPM", formatDateRange(l, utc,
+                11 * HOUR, 13 * HOUR, flags));
+        assertEquals("2\u2009\u2013\u20093\u202fPM", formatDateRange(l, utc,
+                14 * HOUR, 15 * HOUR, flags));
     }
 
     // http://b/20708022
@@ -618,8 +627,8 @@
         final ULocale locale = new ULocale("en");
         final TimeZone timeZone = TimeZone.getTimeZone("UTC");
 
-        assertEquals("11:00 PM – 12:00 AM", formatDateRange(locale, timeZone,
-                1430434800000L, 1430438400000L, FORMAT_SHOW_TIME));
+        assertEquals("11:00\u202fPM\u2009\u2013\u200912:00\u202fAM", formatDateRange(locale,
+                timeZone, 1430434800000L, 1430438400000L, FORMAT_SHOW_TIME));
     }
 
     // http://b/68847519
@@ -629,23 +638,25 @@
                 ENGLISH, GMT_ZONE, from, to, FORMAT_SHOW_DATE | FORMAT_SHOW_TIME | FORMAT_24HOUR);
         // If we're showing times and the end-point is midnight the following day, we want the
         // behaviour of suppressing the date for the end...
-        assertEquals("February 27, 2007, 04:00 – 00:00", fmt.apply(1172548800000L, 1172620800000L));
+        assertEquals("February 27, 2007, 04:00\u2009\u2013\u200900:00", fmt.apply(1172548800000L,
+                1172620800000L));
         // ...unless the start-point is also midnight, in which case we need dates to disambiguate.
-        assertEquals("February 27, 2007 at 00:00 – February 28, 2007 at 00:00",
+        assertEquals("February 27, 2007 at 00:00\u2009\u2013\u2009February 28, 2007 at 00:00",
                 fmt.apply(1172534400000L, 1172620800000L));
         // We want to show the date if the end-point is a millisecond after midnight the following
         // day, or if it is exactly midnight the day after that.
-        assertEquals("February 27, 2007 at 04:00 – February 28, 2007 at 00:00",
+        assertEquals("February 27, 2007 at 04:00\u2009\u2013\u2009February 28, 2007 at 00:00",
                 fmt.apply(1172548800000L, 1172620800001L));
-        assertEquals("February 27, 2007 at 04:00 – March 1, 2007 at 00:00",
+        assertEquals("February 27, 2007 at 04:00\u2009\u2013\u2009March 1, 2007 at 00:00",
                 fmt.apply(1172548800000L, 1172707200000L));
         // We want to show the date if the start-point is anything less than a minute after
       // midnight,
         // since that gets displayed as midnight...
-        assertEquals("February 27, 2007 at 00:00 – February 28, 2007 at 00:00",
+        assertEquals("February 27, 2007 at 00:00\u2009\u2013\u2009February 28, 2007 at 00:00",
                 fmt.apply(1172534459999L, 1172620800000L));
         // ...but not if it is exactly one minute after midnight.
-        assertEquals("February 27, 2007, 00:01 – 00:00", fmt.apply(1172534460000L, 1172620800000L));
+        assertEquals("February 27, 2007, 00:01\u2009\u2013\u200900:00", fmt.apply(1172534460000L,
+                1172620800000L));
     }
 
     // http://b/68847519
@@ -656,16 +667,20 @@
         // If we're only showing dates and the end-point is midnight of any day, we want the
         // behaviour of showing an end date one earlier. So if the end-point is March 2, 2007 00:00,
         // show March 1, 2007 instead (whether the start-point is midnight or not).
-        assertEquals("February 27 – March 1, 2007", fmt.apply(1172534400000L, 1172793600000L));
-        assertEquals("February 27 – March 1, 2007", fmt.apply(1172548800000L, 1172793600000L));
+        assertEquals("February 27\u2009\u2013\u2009March 1, 2007",
+                fmt.apply(1172534400000L, 1172793600000L));
+        assertEquals("February 27\u2009\u2013\u2009March 1, 2007",
+                fmt.apply(1172548800000L, 1172793600000L));
         // We want to show the true date if the end-point is a millisecond after midnight.
-        assertEquals("February 27 – March 2, 2007", fmt.apply(1172534400000L, 1172793600001L));
+        assertEquals("February 27\u2009\u2013\u2009March 2, 2007",
+                fmt.apply(1172534400000L, 1172793600001L));
 
         // 2006-02-27 00:00:00.000 GMT - 2007-03-02 00:00:00.000 GMT
-        assertEquals("February 27, 2006 – March 1, 2007",
+        assertEquals("February 27, 2006\u2009\u2013\u2009March 1, 2007",
                 fmt.apply(1140998400000L, 1172793600000L));
 
         // Spans a leap year's Feb 29th.
-        assertEquals("February 27 – March 1, 2004", fmt.apply(1077840000000L, 1078185600000L));
+        assertEquals("February 27\u2009\u2013\u2009March 1, 2004",
+                fmt.apply(1077840000000L, 1078185600000L));
     }
 }
diff --git a/core/tests/coretests/src/android/text/format/DateUtilsTest.java b/core/tests/coretests/src/android/text/format/DateUtilsTest.java
index 381c051..39ed82ef 100644
--- a/core/tests/coretests/src/android/text/format/DateUtilsTest.java
+++ b/core/tests/coretests/src/android/text/format/DateUtilsTest.java
@@ -139,16 +139,16 @@
                 fixedTime, java.text.DateFormat.SHORT, java.text.DateFormat.FULL));
 
         final long hourDuration = 2 * 60 * 60 * 1000;
-        assertEquals("5:30:15 AM Greenwich Mean Time", DateUtils.formatSameDayTime(
+        assertEquals("5:30:15\u202fAM Greenwich Mean Time", DateUtils.formatSameDayTime(
                 fixedTime + hourDuration, fixedTime, java.text.DateFormat.FULL,
                 java.text.DateFormat.FULL));
-        assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
+        assertEquals("5:30:15\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
                 fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.DEFAULT));
-        assertEquals("5:30:15 AM GMT", DateUtils.formatSameDayTime(fixedTime + hourDuration,
+        assertEquals("5:30:15\u202fAM GMT", DateUtils.formatSameDayTime(fixedTime + hourDuration,
                 fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.LONG));
-        assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
+        assertEquals("5:30:15\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
                 fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.MEDIUM));
-        assertEquals("5:30 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
+        assertEquals("5:30\u202fAM", DateUtils.formatSameDayTime(fixedTime + hourDuration,
                 fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.SHORT));
     }
 
diff --git a/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java b/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java
index b342516..2337802 100644
--- a/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java
+++ b/core/tests/coretests/src/android/text/format/RelativeDateTimeFormatterTest.java
@@ -468,37 +468,37 @@
         cal.set(2015, Calendar.FEBRUARY, 5, 10, 50, 0);
         final long base = cal.getTimeInMillis();
 
-        assertEquals("5 seconds ago, 10:49 AM",
+        assertEquals("5 seconds ago, 10:49\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * SECOND_IN_MILLIS, base, 0,
                         MINUTE_IN_MILLIS, 0));
-        assertEquals("5 min. ago, 10:45 AM",
+        assertEquals("5 min. ago, 10:45\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * MINUTE_IN_MILLIS, base, 0,
                         HOUR_IN_MILLIS, FORMAT_ABBREV_RELATIVE));
-        assertEquals("0 hr. ago, 10:45 AM",
+        assertEquals("0 hr. ago, 10:45\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * MINUTE_IN_MILLIS, base,
                         HOUR_IN_MILLIS, DAY_IN_MILLIS, FORMAT_ABBREV_RELATIVE));
-        assertEquals("5 hours ago, 5:50 AM",
+        assertEquals("5 hours ago, 5:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base,
                         HOUR_IN_MILLIS, DAY_IN_MILLIS, 0));
-        assertEquals("Yesterday, 7:50 PM",
+        assertEquals("Yesterday, 7:50\u202fPM",
                 getRelativeDateTimeString(en_US, tz, base - 15 * HOUR_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, FORMAT_ABBREV_RELATIVE));
-        assertEquals("5 days ago, 10:50 AM",
+        assertEquals("5 days ago, 10:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * DAY_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
-        assertEquals("Jan 29, 10:50 AM",
+        assertEquals("Jan 29, 10:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 7 * DAY_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
-        assertEquals("11/27/2014, 10:50 AM",
+        assertEquals("11/27/2014, 10:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
-        assertEquals("11/27/2014, 10:50 AM",
+        assertEquals("11/27/2014, 10:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0,
                         YEAR_IN_MILLIS, 0));
 
         // User-supplied flags should be ignored when formatting the date clause.
         final int FORMAT_SHOW_WEEKDAY = 0x00002;
-        assertEquals("11/27/2014, 10:50 AM",
+        assertEquals("11/27/2014, 10:50\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base - 10 * WEEK_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS,
                         FORMAT_ABBREV_ALL | FORMAT_SHOW_WEEKDAY));
@@ -514,14 +514,14 @@
         // So 5 hours before 3:15 AM should be formatted as 'Yesterday, 9:15 PM'.
         cal.set(2014, Calendar.MARCH, 9, 3, 15, 0);
         long base = cal.getTimeInMillis();
-        assertEquals("Yesterday, 9:15 PM",
+        assertEquals("Yesterday, 9:15\u202fPM",
                 getRelativeDateTimeString(en_US, tz, base - 5 * HOUR_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
 
         // 1 hour after 2:00 AM should be formatted as 'In 1 hour, 4:00 AM'.
         cal.set(2014, Calendar.MARCH, 9, 2, 0, 0);
         base = cal.getTimeInMillis();
-        assertEquals("In 1 hour, 4:00 AM",
+        assertEquals("In 1 hour, 4:00\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base + 1 * HOUR_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
 
@@ -529,22 +529,22 @@
         // 1:00 AM. 8 hours before 5:20 AM should be 'Yesterday, 10:20 PM'.
         cal.set(2014, Calendar.NOVEMBER, 2, 5, 20, 0);
         base = cal.getTimeInMillis();
-        assertEquals("Yesterday, 10:20 PM",
+        assertEquals("Yesterday, 10:20\u202fPM",
                 getRelativeDateTimeString(en_US, tz, base - 8 * HOUR_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
 
         cal.set(2014, Calendar.NOVEMBER, 2, 0, 45, 0);
         base = cal.getTimeInMillis();
         // 45 minutes after 0:45 AM should be 'In 45 minutes, 1:30 AM'.
-        assertEquals("In 45 minutes, 1:30 AM",
+        assertEquals("In 45 minutes, 1:30\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base + 45 * MINUTE_IN_MILLIS, base, 0,
                         WEEK_IN_MILLIS, 0));
         // 45 minutes later, it should be 'In 45 minutes, 1:15 AM'.
-        assertEquals("In 45 minutes, 1:15 AM",
+        assertEquals("In 45 minutes, 1:15\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base + 90 * MINUTE_IN_MILLIS,
                         base + 45 * MINUTE_IN_MILLIS, 0, WEEK_IN_MILLIS, 0));
         // Another 45 minutes later, it should be 'In 45 minutes, 2:00 AM'.
-        assertEquals("In 45 minutes, 2:00 AM",
+        assertEquals("In 45 minutes, 2:00\u202fAM",
                 getRelativeDateTimeString(en_US, tz, base + 135 * MINUTE_IN_MILLIS,
                         base + 90 * MINUTE_IN_MILLIS, 0, WEEK_IN_MILLIS, 0));
     }
@@ -593,7 +593,7 @@
         Calendar yesterdayCalendar1 = Calendar.getInstance(tz, en_US);
         yesterdayCalendar1.set(2011, Calendar.SEPTEMBER, 1, 10, 24, 0);
         long yesterday1 = yesterdayCalendar1.getTimeInMillis();
-        assertEquals("Yesterday, 10:24 AM",
+        assertEquals("Yesterday, 10:24\u202fAM",
                 getRelativeDateTimeString(en_US, tz, yesterday1, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -601,7 +601,7 @@
         Calendar yesterdayCalendar2 = Calendar.getInstance(tz, en_US);
         yesterdayCalendar2.set(2011, Calendar.SEPTEMBER, 1, 10, 22, 0);
         long yesterday2 = yesterdayCalendar2.getTimeInMillis();
-        assertEquals("Yesterday, 10:22 AM",
+        assertEquals("Yesterday, 10:22\u202fAM",
                 getRelativeDateTimeString(en_US, tz, yesterday2, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -609,7 +609,7 @@
         Calendar twoDaysAgoCalendar1 = Calendar.getInstance(tz, en_US);
         twoDaysAgoCalendar1.set(2011, Calendar.AUGUST, 31, 10, 24, 0);
         long twoDaysAgo1 = twoDaysAgoCalendar1.getTimeInMillis();
-        assertEquals("2 days ago, 10:24 AM",
+        assertEquals("2 days ago, 10:24\u202fAM",
                 getRelativeDateTimeString(en_US, tz, twoDaysAgo1, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -617,7 +617,7 @@
         Calendar twoDaysAgoCalendar2 = Calendar.getInstance(tz, en_US);
         twoDaysAgoCalendar2.set(2011, Calendar.AUGUST, 31, 10, 22, 0);
         long twoDaysAgo2 = twoDaysAgoCalendar2.getTimeInMillis();
-        assertEquals("2 days ago, 10:22 AM",
+        assertEquals("2 days ago, 10:22\u202fAM",
                 getRelativeDateTimeString(en_US, tz, twoDaysAgo2, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -625,7 +625,7 @@
         Calendar tomorrowCalendar1 = Calendar.getInstance(tz, en_US);
         tomorrowCalendar1.set(2011, Calendar.SEPTEMBER, 3, 10, 22, 0);
         long tomorrow1 = tomorrowCalendar1.getTimeInMillis();
-        assertEquals("Tomorrow, 10:22 AM",
+        assertEquals("Tomorrow, 10:22\u202fAM",
                 getRelativeDateTimeString(en_US, tz, tomorrow1, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -633,7 +633,7 @@
         Calendar tomorrowCalendar2 = Calendar.getInstance(tz, en_US);
         tomorrowCalendar2.set(2011, Calendar.SEPTEMBER, 3, 10, 24, 0);
         long tomorrow2 = tomorrowCalendar2.getTimeInMillis();
-        assertEquals("Tomorrow, 10:24 AM",
+        assertEquals("Tomorrow, 10:24\u202fAM",
                 getRelativeDateTimeString(en_US, tz, tomorrow2, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -641,7 +641,7 @@
         Calendar twoDaysLaterCalendar1 = Calendar.getInstance(tz, en_US);
         twoDaysLaterCalendar1.set(2011, Calendar.SEPTEMBER, 4, 10, 22, 0);
         long twoDaysLater1 = twoDaysLaterCalendar1.getTimeInMillis();
-        assertEquals("In 2 days, 10:22 AM",
+        assertEquals("In 2 days, 10:22\u202fAM",
                 getRelativeDateTimeString(en_US, tz, twoDaysLater1, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
 
@@ -649,7 +649,7 @@
         Calendar twoDaysLaterCalendar2 = Calendar.getInstance(tz, en_US);
         twoDaysLaterCalendar2.set(2011, Calendar.SEPTEMBER, 4, 10, 24, 0);
         long twoDaysLater2 = twoDaysLaterCalendar2.getTimeInMillis();
-        assertEquals("In 2 days, 10:24 AM",
+        assertEquals("In 2 days, 10:24\u202fAM",
                 getRelativeDateTimeString(en_US, tz, twoDaysLater2, now, MINUTE_IN_MILLIS,
                         WEEK_IN_MILLIS, 0));
     }
@@ -664,11 +664,11 @@
         cal.set(2012, Calendar.FEBRUARY, 5, 10, 50, 0);
         long base = cal.getTimeInMillis();
 
-        assertEquals("Feb 5, 5:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("Feb 5, 5:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 5 * HOUR_IN_MILLIS, base, 0, MINUTE_IN_MILLIS, 0));
-        assertEquals("Jan 29, 10:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("Jan 29, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 7 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0));
-        assertEquals("11/27/2011, 10:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("11/27/2011, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0));
 
         assertEquals("January 6", getRelativeTimeSpanString(en_US, tz,
@@ -687,11 +687,11 @@
         // Feb 5, 2018 at 10:50 PST
         cal.set(2018, Calendar.FEBRUARY, 5, 10, 50, 0);
         base = cal.getTimeInMillis();
-        assertEquals("Feb 5, 5:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("Feb 5, 5:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 5 * HOUR_IN_MILLIS, base, 0, MINUTE_IN_MILLIS, 0));
-        assertEquals("Jan 29, 10:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("Jan 29, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 7 * DAY_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0));
-        assertEquals("11/27/2017, 10:50 AM", getRelativeDateTimeString(en_US, tz,
+        assertEquals("11/27/2017, 10:50\u202fAM", getRelativeDateTimeString(en_US, tz,
                 base - 10 * WEEK_IN_MILLIS, base, 0, WEEK_IN_MILLIS, 0));
 
         assertEquals("January 6", getRelativeTimeSpanString(en_US, tz,
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/android/util/BinaryXmlTest.java b/core/tests/coretests/src/android/util/BinaryXmlTest.java
index fd625dce..025e831 100644
--- a/core/tests/coretests/src/android/util/BinaryXmlTest.java
+++ b/core/tests/coretests/src/android/util/BinaryXmlTest.java
@@ -30,6 +30,9 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/core/tests/coretests/src/android/util/XmlTest.java b/core/tests/coretests/src/android/util/XmlTest.java
index 1cd4d13..91ebc2a 100644
--- a/core/tests/coretests/src/android/util/XmlTest.java
+++ b/core/tests/coretests/src/android/util/XmlTest.java
@@ -29,6 +29,8 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 44bb062..0bf133f 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -24,6 +24,7 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -98,14 +99,14 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             // test if setVisibility can show IME
             mImeConsumer.onWindowFocusGained(true);
-            mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertTrue((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
 
             // test if setVisibility can hide IME
-            mController.hide(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.hide(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertFalse((mController.getRequestedVisibleTypes() & WindowInsets.Type.ime()) != 0);
         });
     }
 
@@ -117,7 +118,7 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             // Request IME visible before control is available.
             mImeConsumer.onWindowFocusGained(true);
-            mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
 
             // set control and verify visibility is applied.
             InsetsSourceControl control =
@@ -125,9 +126,11 @@
             mController.onControlsChanged(new InsetsSourceControl[] { control });
             // IME show animation should be triggered when control becomes available.
             verify(mController).applyAnimation(
-                    eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */);
+                    eq(WindowInsets.Type.ime()), eq(true) /* show */, eq(true) /* fromIme */,
+                    any() /* statsToken */);
             verify(mController, never()).applyAnimation(
-                    eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */);
+                    eq(WindowInsets.Type.ime()), eq(false) /* show */, eq(true) /* fromIme */,
+                    any() /* statsToken */);
         });
     }
 
@@ -153,7 +156,8 @@
             mImeConsumer.onWindowFocusGained(hasWindowFocus);
             final boolean imeVisible = hasWindowFocus && hasViewFocus;
             if (imeVisible) {
-                mController.show(WindowInsets.Type.ime(), true /* fromIme */);
+                mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+                        null /* statsToken */);
             }
 
             // set control and verify visibility is applied.
@@ -169,20 +173,21 @@
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(false) /* fromIme */,
-                        eq(expectSkipAnim) /* skipAnim */);
+                        eq(expectSkipAnim) /* skipAnim */, null /* statsToken */);
             }
 
             // If previously hasViewFocus is false, verify when requesting the IME visible next
             // time will not skip animation.
             if (!hasViewFocus) {
-                mController.show(WindowInsets.Type.ime(), true);
+                mController.show(WindowInsets.Type.ime(), true /* fromIme */,
+                        null /* statsToken */);
                 mController.onControlsChanged(new InsetsSourceControl[]{ control });
                 // Verify IME show animation should be triggered when control becomes available and
                 // the animation will be skipped by getAndClearSkipAnimationOnce invoked.
                 verify(control).getAndClearSkipAnimationOnce();
                 verify(mController).applyAnimation(eq(WindowInsets.Type.ime()),
                         eq(true) /* show */, eq(true) /* fromIme */,
-                        eq(false) /* skipAnim */);
+                        eq(false) /* skipAnim */, null /* statsToken */);
             }
         });
     }
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index d0f7fe04..c88255e 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -27,7 +27,6 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -90,7 +89,6 @@
         mInsetsState = new InsetsState();
         mInsetsState.getSource(ITYPE_STATUS_BAR).setFrame(new Rect(0, 0, 500, 100));
         mInsetsState.getSource(ITYPE_NAVIGATION_BAR).setFrame(new Rect(400, 0, 500, 500));
-        doNothing().when(mMockController).onRequestedVisibilityChanged(any());
         InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(ITYPE_STATUS_BAR, mInsetsState,
                 () -> mMockTransaction, mMockController);
         topConsumer.setControl(
@@ -111,7 +109,8 @@
         mController = new InsetsAnimationControlImpl(controls,
                 new Rect(0, 0, 500, 500), mInsetsState, mMockListener, systemBars(),
                 mMockController, 10 /* durationMs */, new LinearInterpolator(),
-                0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */);
+                0 /* animationType */, 0 /* layoutInsetsDuringAnimation */, null /* translator */,
+                null /* statsToken */);
         mController.setReadyDispatched(true);
     }
 
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index cc68fce..c6fa778 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -31,9 +31,12 @@
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.LAST_TYPE;
 import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
+import static android.view.WindowInsets.Type.all;
+import static android.view.WindowInsets.Type.defaultVisible;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
+import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
@@ -63,7 +66,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsState.InternalInsetsType;
 import android.view.SurfaceControl.Transaction;
-import android.view.WindowInsets.Type;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
 import android.view.WindowManager.BadTokenException;
 import android.view.WindowManager.LayoutParams;
@@ -245,35 +248,29 @@
             mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
             // since there is no focused view, forcefully make IME visible.
-            mController.show(Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             verify(loggingListener).onReady(notNull(), anyInt());
         });
     }
 
     @Test
     public void testAnimationEndState() {
-        InsetsSourceControl[] controls = prepareControls();
-        InsetsSourceControl navBar = controls[0];
-        InsetsSourceControl statusBar = controls[1];
-        InsetsSourceControl ime = controls[2];
+        prepareControls();
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
             // since there is no focused view, forcefully make IME visible.
-            mController.show(Type.ime(), true /* fromIme */);
-            mController.show(Type.all());
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
+            mController.show(all());
             // quickly jump to final state by cancelling it.
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            final @InsetsType int types = navigationBars() | statusBars() | ime();
+            assertEquals(types, mController.getRequestedVisibleTypes() & types);
 
-            mController.hide(Type.ime(), true /* fromIme */);
-            mController.hide(Type.all());
+            mController.hide(ime(), true /* fromIme */, null /* statsToken */);
+            mController.hide(all());
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(0, mController.getRequestedVisibleTypes() & types);
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost();
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -285,12 +282,12 @@
         mController.onControlsChanged(new InsetsSourceControl[] { ime });
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
-            mController.show(Type.ime(), true /* fromIme */);
+            mController.show(WindowInsets.Type.ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
-            mController.hide(Type.ime(), true /* fromIme */);
+            assertTrue(isRequestedVisible(mController, ime()));
+            mController.hide(ime(), true /* fromIme */, null /* statsToken */);
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, ime()));
             mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost();
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -304,26 +301,22 @@
         InsetsSourceControl ime = controls[2];
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            int types = Type.navigationBars() | Type.systemBars();
+            int types = navigationBars() | statusBars();
             // test hide select types.
             mController.hide(types);
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR));
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_STATUS_BAR));
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(statusBars()));
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
 
-            // test hide all
+            // test show all
             mController.show(types);
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_STATUS_BAR));
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(types, mController.getRequestedVisibleTypes() & (types | ime()));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -336,33 +329,27 @@
         InsetsSourceControl ime = controls[2];
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            int types = Type.navigationBars() | Type.systemBars();
+            int types = navigationBars() | statusBars();
             // test show select types.
             mController.show(types);
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(types, mController.getRequestedVisibleTypes() & types);
+            assertEquals(0, mController.getRequestedVisibleTypes() & ime());
 
             // test hide all
-            mController.hide(Type.all());
+            mController.hide(all());
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
 
             // test single show
-            mController.show(Type.navigationBars());
+            mController.show(navigationBars());
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(navigationBars(),
+                    mController.getRequestedVisibleTypes() & (types | ime()));
 
             // test single hide
-            mController.hide(Type.navigationBars());
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            mController.hide(navigationBars());
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
 
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -370,49 +357,38 @@
 
     @Test
     public void testShowHideMultiple() {
-        InsetsSourceControl[] controls = prepareControls();
-        InsetsSourceControl navBar = controls[0];
-        InsetsSourceControl statusBar = controls[1];
-        InsetsSourceControl ime = controls[2];
+        prepareControls();
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             // start two animations and see if previous is cancelled and final state is reached.
-            mController.hide(Type.navigationBars());
-            mController.hide(Type.systemBars());
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR));
+            mController.hide(navigationBars());
+            mController.hide(systemBars());
+            int types = navigationBars() | statusBars();
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
 
-            mController.show(Type.navigationBars());
-            mController.show(Type.systemBars());
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_STATUS_BAR));
+            mController.show(navigationBars());
+            mController.show(systemBars());
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(types, mController.getRequestedVisibleTypes() & (types | ime()));
 
-            int types = Type.navigationBars() | Type.systemBars();
             // show two at a time and hide one by one.
             mController.show(types);
-            mController.hide(Type.navigationBars());
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_STATUS_BAR));
+            mController.hide(navigationBars());
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(statusBars(), mController.getRequestedVisibleTypes() & (types | ime()));
 
-            mController.hide(Type.systemBars());
-            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_NAVIGATION_BAR));
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR));
+            mController.hide(systemBars());
+            assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(navigationBars()));
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -425,20 +401,16 @@
         InsetsSourceControl ime = controls[2];
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            int types = Type.navigationBars() | Type.systemBars();
+            int types = navigationBars() | statusBars();
             // show two at a time and hide one by one.
             mController.show(types);
-            mController.hide(Type.navigationBars());
+            mController.hide(navigationBars());
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(statusBars(), mController.getRequestedVisibleTypes() & (types | ime()));
 
-            mController.hide(Type.systemBars());
+            mController.hide(systemBars());
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible());
-            assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible());
+            assertEquals(0, mController.getRequestedVisibleTypes() & (types | ime()));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -448,9 +420,9 @@
         mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            mController.hide(Type.statusBars());
+            mController.hide(statusBars());
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, statusBars()));
             assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
 
             // Loosing control
@@ -458,14 +430,14 @@
             state.setSourceVisible(ITYPE_STATUS_BAR, true);
             mController.onStateChanged(state);
             mController.onControlsChanged(new InsetsSourceControl[0]);
-            assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, statusBars()));
             assertTrue(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
 
             // Gaining control
             mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR));
-            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR));
+            assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(statusBars()));
             mController.cancelExistingAnimations();
-            assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, statusBars()));
             assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible());
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -480,14 +452,14 @@
             assertFalse(mController.getState().getSource(ITYPE_IME).isVisible());
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             // Gaining control shortly after
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
 
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_IME));
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(ITYPE_IME).isRequestedVisible());
+            assertTrue(isRequestedVisible(mController, ime()));
             assertTrue(mController.getState().getSource(ITYPE_IME).isVisible());
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -504,11 +476,11 @@
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
-            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_IME));
+            assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
             mController.cancelExistingAnimations();
-            assertTrue(mController.getSourceConsumer(ITYPE_IME).isRequestedVisible());
+            assertTrue(isRequestedVisible(mController, ime()));
             assertTrue(mController.getState().getSource(ITYPE_IME).isVisible());
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -535,7 +507,7 @@
         });
         waitUntilNextFrame();
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, statusBars()));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -562,7 +534,7 @@
         });
         waitUntilNextFrame();
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, statusBars()));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -582,7 +554,7 @@
             verify(listener, never()).onReady(any(), anyInt());
 
             // Pretend that IME is calling.
-            mController.show(ime(), true);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             // Ready gets deferred until next predraw
             mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
@@ -666,7 +638,7 @@
             mController.onControlsChanged(createSingletonControl(ITYPE_IME));
 
             // Pretend IME is calling
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
 
             InsetsState copy = new InsetsState(mController.getState(), true /* copySources */);
             copy.getSource(ITYPE_IME).setFrame(0, 1, 2, 3);
@@ -689,7 +661,7 @@
     public void testResizeAnimation_insetsTypes() {
         for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
             final @AnimationType int expectedAnimationType =
-                    (InsetsState.toPublicType(type) & Type.systemBars()) != 0
+                    (InsetsState.toPublicType(type) & systemBars()) != 0
                             ? ANIMATION_TYPE_RESIZE
                             : ANIMATION_TYPE_NONE;
             doTestResizeAnimation_insetsTypes(type, expectedAnimationType);
@@ -698,6 +670,7 @@
 
     private void doTestResizeAnimation_insetsTypes(@InternalInsetsType int type,
             @AnimationType int expectedAnimationType) {
+        final @InsetsType int publicType = InsetsState.toPublicType(type);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             final InsetsState state1 = new InsetsState();
             state1.getSource(type).setVisible(true);
@@ -708,15 +681,15 @@
 
             // New insets source won't cause the resize animation.
             mController.onStateChanged(state1);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
 
             // Changing frame might cause the resize animation. This depends on the insets type.
             mController.onStateChanged(state2);
-            assertEquals(message, expectedAnimationType, mController.getAnimationType(type));
+            assertEquals(message, expectedAnimationType, mController.getAnimationType(publicType));
 
             // Cancel the existing animations for the next iteration.
             mController.cancelExistingAnimations();
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -725,6 +698,7 @@
     public void testResizeAnimation_displayFrame() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             final @InternalInsetsType int type = ITYPE_STATUS_BAR;
+            final @InsetsType int publicType = statusBars();
             final InsetsState state1 = new InsetsState();
             state1.setDisplayFrame(new Rect(0, 0, 500, 1000));
             state1.getSource(type).setFrame(0, 0, 500, 50);
@@ -735,11 +709,11 @@
 
             // New insets source won't cause the resize animation.
             mController.onStateChanged(state1);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
 
             // Changing frame won't cause the resize animation if the display frame is also changed.
             mController.onStateChanged(state2);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -748,6 +722,7 @@
     public void testResizeAnimation_visibility() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             final @InternalInsetsType int type = ITYPE_STATUS_BAR;
+            final @InsetsType int publicType = statusBars();
             final InsetsState state1 = new InsetsState();
             state1.getSource(type).setVisible(true);
             state1.getSource(type).setFrame(0, 0, 500, 50);
@@ -761,17 +736,17 @@
 
             // New insets source won't cause the resize animation.
             mController.onStateChanged(state1);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
 
             // Changing source visibility (visible --> invisible) won't cause the resize animation.
             // The previous source and the current one must be both visible.
             mController.onStateChanged(state2);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
 
             // Changing source visibility (invisible --> visible) won't cause the resize animation.
             // The previous source and the current one must be both visible.
             mController.onStateChanged(state3);
-            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type));
+            assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(publicType));
         });
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
@@ -824,15 +799,13 @@
     @Test
     public void testRequestedState() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
-            final InsetsVisibilities request = mTestHost.getRequestedVisibilities();
-
             mController.hide(statusBars() | navigationBars());
-            assertFalse(request.getVisibility(ITYPE_STATUS_BAR));
-            assertFalse(request.getVisibility(ITYPE_NAVIGATION_BAR));
+            assertFalse(mTestHost.isRequestedVisible(statusBars()));
+            assertFalse(mTestHost.isRequestedVisible(navigationBars()));
 
             mController.show(statusBars() | navigationBars());
-            assertTrue(request.getVisibility(ITYPE_STATUS_BAR));
-            assertTrue(request.getVisibility(ITYPE_NAVIGATION_BAR));
+            assertTrue(mTestHost.isRequestedVisible(statusBars()));
+            assertTrue(mTestHost.isRequestedVisible(navigationBars()));
         });
     }
 
@@ -872,7 +845,7 @@
 
             // Showing invisible ime should only causes insets change once.
             clearInvocations(mTestHost);
-            mController.show(ime(), true /* fromIme */);
+            mController.show(ime(), true /* fromIme */, null /* statsToken */);
             verify(mTestHost, times(1)).notifyInsetsChanged();
 
             // Sending the same insets state should not cause insets change.
@@ -939,10 +912,10 @@
 
             // Verify IME requested visibility should be updated to IME consumer from controller.
             mController.show(ime());
-            assertTrue(imeInsetsConsumer.isRequestedVisible());
+            assertTrue(isRequestedVisible(mController, ime()));
 
             mController.hide(ime());
-            assertFalse(imeInsetsConsumer.isRequestedVisible());
+            assertFalse(isRequestedVisible(mController, ime()));
         });
     }
 
@@ -979,22 +952,26 @@
         return controls;
     }
 
+    private static boolean isRequestedVisible(InsetsController controller, @InsetsType int type) {
+        return (controller.getRequestedVisibleTypes() & type) != 0;
+    }
+
     public static class TestHost extends ViewRootInsetsControllerHost {
 
-        private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        private @InsetsType int mRequestedVisibleTypes = defaultVisible();
 
         TestHost(ViewRootImpl viewRoot) {
             super(viewRoot);
         }
 
         @Override
-        public void updateRequestedVisibilities(InsetsVisibilities visibilities) {
-            mRequestedVisibilities.set(visibilities);
-            super.updateRequestedVisibilities(visibilities);
+        public void updateRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
+            mRequestedVisibleTypes = requestedVisibleTypes;
+            super.updateRequestedVisibleTypes(requestedVisibleTypes);
         }
 
-        public InsetsVisibilities getRequestedVisibilities() {
-            return mRequestedVisibilities;
+        public boolean isRequestedVisible(@InsetsType int types) {
+            return (mRequestedVisibleTypes & types) != 0;
         }
     }
 }
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 8cf118c..1253278 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -122,7 +122,6 @@
     public void testHide() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
             mConsumer.hide();
-            assertFalse("Consumer should not be visible", mConsumer.isRequestedVisible());
             verify(mSpyInsetsSource).setVisible(eq(false));
         });
 
@@ -134,7 +133,6 @@
             // Insets source starts out visible
             mConsumer.hide();
             mConsumer.show(false /* fromIme */);
-            assertTrue("Consumer should be visible", mConsumer.isRequestedVisible());
             verify(mSpyInsetsSource).setVisible(eq(false));
             verify(mSpyInsetsSource).setVisible(eq(true));
         });
@@ -240,7 +238,7 @@
             // visibility won't be updated when the consumer received the same leash in setControl.
             insetsController.controlWindowInsetsAnimation(ime(), 0L,
                     null /* interpolator */, null /* cancellationSignal */, null /* listener */);
-            assertTrue(insetsController.getAnimationType(ITYPE_IME) == ANIMATION_TYPE_USER);
+            assertEquals(ANIMATION_TYPE_USER, insetsController.getAnimationType(ime()));
             imeConsumer.setControl(new InsetsSourceControl(ITYPE_IME, mLeash,
                     true /* initialVisible */, new Point(), Insets.NONE), new int[1], new int[1]);
             verify(mMockTransaction, never()).show(mLeash);
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
index bb1a3b18..ee1e10f 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
 import android.app.Instrumentation;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
@@ -34,6 +35,7 @@
 import android.graphics.drawable.Icon;
 import android.os.UserHandle;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -51,6 +53,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executors;
 
 /**
  * Tests for the AccessibilityManager by mocking the backing service.
@@ -70,6 +73,7 @@
             LABEL,
             DESCRIPTION,
             TEST_PENDING_INTENT);
+    private static final int DISPLAY_ID = 22;
 
     @Mock private IAccessibilityManager mMockService;
     private MessageCapturingHandler mHandler;
@@ -224,4 +228,45 @@
         assertEquals(mFocusColorDefaultValue,
                 manager.getAccessibilityFocusColor());
     }
+
+    @Test
+    public void testRegisterAccessibilityProxy() throws Exception {
+        // Accessibility does not need to be enabled for a proxy to be registered.
+        AccessibilityManager manager =
+                new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
+                        UserHandle.USER_CURRENT, true);
+
+
+        ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
+        infos.add(new AccessibilityServiceInfo());
+        AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
+        manager.registerDisplayProxy(proxy);
+        // Cannot access proxy.mServiceClient directly due to visibility.
+        verify(mMockService).registerProxyForDisplay(any(IAccessibilityServiceClient.class),
+                any(Integer.class));
+    }
+
+    @Test
+    public void testUnregisterAccessibilityProxy() throws Exception {
+        // Accessibility does not need to be enabled for a proxy to be registered.
+        final AccessibilityManager manager =
+                new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
+                        UserHandle.USER_CURRENT, true);
+
+        final ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
+        infos.add(new AccessibilityServiceInfo());
+
+        final AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
+        manager.registerDisplayProxy(proxy);
+        manager.unregisterDisplayProxy(proxy);
+        verify(mMockService).unregisterProxyForDisplay(proxy.getDisplayId());
+    }
+
+    private class MyAccessibilityProxy extends AccessibilityDisplayProxy {
+        // TODO(241429275): Will override A11yProxy methods in the future.
+        MyAccessibilityProxy(int displayId,
+                @NonNull List<AccessibilityServiceInfo> serviceInfos) {
+            super(displayId, Executors.newSingleThreadExecutor(), serviceInfos);
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index 99670d9..cc02bbb 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@
     // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
     // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
     // and assertAccessibilityNodeInfoCleared in that class.
-    private static final int NUM_MARSHALLED_PROPERTIES = 40;
+    private static final int NUM_MARSHALLED_PROPERTIES = 42;
 
     /**
      * The number of properties that are purposely not marshalled
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 35d5948..7a5ab045 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -27,6 +27,7 @@
 import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
+import android.window.ScreenCapture;
 
 import java.util.Collections;
 import java.util.List;
@@ -180,6 +181,10 @@
 
     public void takeScreenshot(int displayId, RemoteCallback callback) {}
 
+    public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId,
+            ScreenCapture.ScreenCaptureListener listener,
+            IAccessibilityInteractionConnectionCallback callback) {}
+
     public void setFocusAppearance(int strokeWidth, int color) {}
 
     public void setCacheEnabled(boolean enabled) {}
diff --git a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java
index 599cf97..6e73b9f 100644
--- a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java
@@ -52,6 +52,8 @@
 import org.junit.runner.RunWith;
 
 import java.util.Arrays;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Supplemental tests that cannot be covered by CTS (e.g. due to hidden API dependencies).
@@ -500,6 +502,7 @@
                 + "prefix: extras=null\n"
                 + "prefix: hintLocales=null\n"
                 + "prefix: supportedHandwritingGestureTypes=(none)\n"
+                + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n"
                 + "prefix: contentMimeTypes=null\n");
     }
 
@@ -516,6 +519,8 @@
         info.label = "testLabel";
         info.setInitialToolType(MotionEvent.TOOL_TYPE_STYLUS);
         info.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class));
+        info.setSupportedHandwritingGesturePreviews(
+                Stream.of(SelectGesture.class).collect(Collectors.toSet()));
         info.packageName = "android.view.inputmethod";
         info.autofillId = new AutofillId(123);
         info.fieldId = 456;
@@ -538,6 +543,7 @@
                         + "prefix2: extras=Bundle[{testKey=testValue}]\n"
                         + "prefix2: hintLocales=[en,es,zh]\n"
                         + "prefix2: supportedHandwritingGestureTypes=SELECT\n"
+                        + "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n"
                         + "prefix2: contentMimeTypes=[image/png]\n"
                         + "prefix2: targetInputMethodUserId=10\n");
     }
@@ -558,6 +564,7 @@
                         + "prefix: packageName=null autofillId=null fieldId=0 fieldName=null\n"
                         + "prefix: hintLocales=null\n"
                         + "prefix: supportedHandwritingGestureTypes=(none)\n"
+                        + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n"
                         + "prefix: contentMimeTypes=null\n");
     }
 
diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
index f5fcb03..297b07f 100644
--- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
+++ b/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
@@ -20,8 +20,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
@@ -33,6 +35,7 @@
 
 import java.util.Locale;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -41,28 +44,28 @@
     public void verifyLocale(final String localeString) {
         // InputMethodSubtype#getLocale() returns exactly the same string that is passed to the
         // constructor.
-        assertEquals(localeString, createDummySubtype(localeString).getLocale());
+        assertEquals(localeString, createSubtype(localeString).getLocale());
 
         // InputMethodSubtype#getLocale() should be preserved via marshaling.
-        assertEquals(createDummySubtype(localeString).getLocale(),
-                cloneViaParcel(createDummySubtype(localeString)).getLocale());
+        assertEquals(createSubtype(localeString).getLocale(),
+                cloneViaParcel(createSubtype(localeString)).getLocale());
 
         // InputMethodSubtype#getLocale() should be preserved via marshaling.
-        assertEquals(createDummySubtype(localeString).getLocale(),
-                cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).getLocale());
+        assertEquals(createSubtype(localeString).getLocale(),
+                cloneViaParcel(cloneViaParcel(createSubtype(localeString))).getLocale());
 
         // Make sure InputMethodSubtype#hashCode() returns the same hash code.
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                createDummySubtype(localeString).hashCode());
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                cloneViaParcel(createDummySubtype(localeString)).hashCode());
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                createSubtype(localeString).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                cloneViaParcel(createSubtype(localeString)).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                cloneViaParcel(cloneViaParcel(createSubtype(localeString))).hashCode());
     }
 
     @Test
     public void testLocaleObj_locale() {
-        final InputMethodSubtype usSubtype = createDummySubtype("en_US");
+        final InputMethodSubtype usSubtype = createSubtype("en_US");
         Locale localeObject = usSubtype.getLocaleObject();
         assertEquals("en", localeObject.getLanguage());
         assertEquals("US", localeObject.getCountry());
@@ -73,7 +76,7 @@
 
     @Test
     public void testLocaleObj_languageTag() {
-        final InputMethodSubtype usSubtype = createDummySubtypeUsingLanguageTag("en-US");
+        final InputMethodSubtype usSubtype = createSubtypeUsingLanguageTag("en-US");
         Locale localeObject = usSubtype.getLocaleObject();
         assertNotNull(localeObject);
         assertEquals("en", localeObject.getLanguage());
@@ -85,7 +88,7 @@
 
     @Test
     public void testLocaleObj_emptyLocale() {
-        final InputMethodSubtype emptyLocaleSubtype = createDummySubtype("");
+        final InputMethodSubtype emptyLocaleSubtype = createSubtype("");
         assertNull(emptyLocaleSubtype.getLocaleObject());
         // It should continue returning null when called multiple times.
         assertNull(emptyLocaleSubtype.getLocaleObject());
@@ -110,8 +113,8 @@
     @Test
     public void testDeprecatedLocaleString() throws Exception {
         // Make sure "iw" is not automatically replaced with "he".
-        final InputMethodSubtype subtypeIw = createDummySubtype("iw");
-        final InputMethodSubtype subtypeHe = createDummySubtype("he");
+        final InputMethodSubtype subtypeIw = createSubtype("iw");
+        final InputMethodSubtype subtypeHe = createSubtype("he");
         assertEquals("iw", subtypeIw.getLocale());
         assertEquals("he", subtypeHe.getLocale());
         assertFalse(Objects.equals(subtypeIw, subtypeHe));
@@ -125,6 +128,64 @@
         assertEquals("he", clonedSubtypeHe.getLocale());
     }
 
+    @Test
+    public void testCanonicalizedLanguageTagObjectCache() {
+        final InputMethodSubtype subtype = createSubtypeUsingLanguageTag("en-US");
+        // Verify that the returned object is cached and any subsequent call should return the same
+        // object, which is strictly guaranteed if the method gets called only on a single thread.
+        assertSame(subtype.getCanonicalizedLanguageTag(), subtype.getCanonicalizedLanguageTag());
+    }
+
+    @Test
+    public void testCanonicalizedLanguageTag() {
+        verifyCanonicalizedLanguageTag("en", "en");
+        verifyCanonicalizedLanguageTag("en-US", "en-US");
+        verifyCanonicalizedLanguageTag("en-Latn-US-t-k0-qwerty", "en-Latn-US-t-k0-qwerty");
+
+        verifyCanonicalizedLanguageTag("en-us", "en-US");
+        verifyCanonicalizedLanguageTag("EN-us", "en-US");
+
+        verifyCanonicalizedLanguageTag(null, "");
+        verifyCanonicalizedLanguageTag("", "");
+
+        verifyCanonicalizedLanguageTag("und", "und");
+        verifyCanonicalizedLanguageTag("apparently invalid language tag!!!", "und");
+    }
+
+    private void verifyCanonicalizedLanguageTag(
+            @Nullable String languageTag, @Nullable String expectedLanguageTag) {
+        final InputMethodSubtype subtype = createSubtypeUsingLanguageTag(languageTag);
+        assertEquals(subtype.getCanonicalizedLanguageTag(), expectedLanguageTag);
+    }
+
+    @Test
+    public void testIsSuitableForPhysicalKeyboardLayoutMapping() {
+        final Supplier<InputMethodSubtypeBuilder> getValidBuilder = () ->
+                new InputMethodSubtypeBuilder()
+                        .setLanguageTag("en-US")
+                        .setIsAuxiliary(false)
+                        .setSubtypeMode("keyboard")
+                        .setSubtypeId(1);
+
+        assertTrue(getValidBuilder.get().build().isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // mode == "voice" is not suitable.
+        assertFalse(getValidBuilder.get().setSubtypeMode("voice").build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // Auxiliary subtype not suitable.
+        assertFalse(getValidBuilder.get().setIsAuxiliary(true).build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // languageTag == null is not suitable.
+        assertFalse(getValidBuilder.get().setLanguageTag(null).build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // languageTag == "und" is not suitable.
+        assertFalse(getValidBuilder.get().setLanguageTag("und").build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+    }
+
     private static InputMethodSubtype cloneViaParcel(final InputMethodSubtype original) {
         Parcel parcel = null;
         try {
@@ -139,7 +200,7 @@
         }
     }
 
-    private static InputMethodSubtype createDummySubtype(final String locale) {
+    private static InputMethodSubtype createSubtype(final String locale) {
         final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
         return builder.setSubtypeNameResId(0)
                 .setSubtypeIconResId(0)
@@ -148,7 +209,7 @@
                 .build();
     }
 
-    private static InputMethodSubtype createDummySubtypeUsingLanguageTag(
+    private static InputMethodSubtype createSubtypeUsingLanguageTag(
             final String languageTag) {
         final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
         return builder.setSubtypeNameResId(0)
diff --git a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java b/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java
new file mode 100644
index 0000000..79aeaa3
--- /dev/null
+++ b/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.view.inputmethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ParcelableHandwritingGestureTest {
+
+    @Test
+    public void testCreationFailWithNullPointerException() {
+        assertThrows(NullPointerException.class, () -> ParcelableHandwritingGesture.of(null));
+    }
+
+    @Test
+    public void testInvalidTypeHeader() {
+        Parcel parcel = null;
+        try {
+            parcel = Parcel.obtain();
+            // GESTURE_TYPE_NONE is not a supported header.
+            parcel.writeInt(HandwritingGesture.GESTURE_TYPE_NONE);
+            final Parcel initializedParcel = parcel;
+            assertThrows(UnsupportedOperationException.class,
+                    () -> ParcelableHandwritingGesture.CREATOR.createFromParcel(initializedParcel));
+        } finally {
+            if (parcel != null) {
+                parcel.recycle();
+            }
+        }
+    }
+
+    @Test
+    public void  testSelectGesture() {
+        verifyEqualityAfterUnparcel(new SelectGesture.Builder()
+                .setGranularity(HandwritingGesture.GRANULARITY_WORD)
+                .setSelectionArea(new RectF(1, 2, 3, 4))
+                .setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testSelectRangeGesture() {
+        verifyEqualityAfterUnparcel(new SelectRangeGesture.Builder()
+                .setGranularity(HandwritingGesture.GRANULARITY_WORD)
+                .setSelectionStartArea(new RectF(1, 2, 3, 4))
+                .setSelectionEndArea(new RectF(5, 6, 7, 8))
+                .setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testInsertGestureGesture() {
+        verifyEqualityAfterUnparcel(new InsertGesture.Builder()
+                .setTextToInsert("text")
+                .setInsertionPoint(new PointF(1, 1)).setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testDeleteGestureGesture() {
+        verifyEqualityAfterUnparcel(new DeleteGesture.Builder()
+                .setGranularity(HandwritingGesture.GRANULARITY_WORD)
+                .setDeletionArea(new RectF(1, 2, 3, 4))
+                .setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testDeleteRangeGestureGesture() {
+        verifyEqualityAfterUnparcel(new DeleteRangeGesture.Builder()
+                .setGranularity(HandwritingGesture.GRANULARITY_WORD)
+                .setDeletionStartArea(new RectF(1, 2, 3, 4))
+                .setDeletionEndArea(new RectF(5, 6, 7, 8))
+                .setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testRemoveSpaceGestureGesture() {
+        verifyEqualityAfterUnparcel(new RemoveSpaceGesture.Builder()
+                .setPoints(new PointF(1f, 2f), new PointF(3f, 4f))
+                .setFallbackText("")
+                .build());
+    }
+
+    @Test
+    public void  testJoinOrSplitGestureGesture() {
+        verifyEqualityAfterUnparcel(new JoinOrSplitGesture.Builder()
+                .setJoinOrSplitPoint(new PointF(1f, 2f))
+                .setFallbackText("")
+                .build());
+    }
+
+    static void verifyEqualityAfterUnparcel(@NonNull HandwritingGesture gesture) {
+        assertEquals(gesture, cloneViaParcel(ParcelableHandwritingGesture.of(gesture)).get());
+    }
+
+    private static ParcelableHandwritingGesture cloneViaParcel(
+            @NonNull ParcelableHandwritingGesture original) {
+        Parcel parcel = null;
+        try {
+            parcel = Parcel.obtain();
+            original.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            return ParcelableHandwritingGesture.CREATOR.createFromParcel(parcel);
+        } finally {
+            if (parcel != null) {
+                parcel.recycle();
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index d4a6632..95aa5d0 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.view.HandwritingDelegateConfiguration;
 import android.view.HandwritingInitiator;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -208,6 +209,30 @@
     }
 
     @Test
+    public void onTouchEvent_startHandwriting_delegate() {
+        int delegatorViewId = 234;
+        View delegatorView = new View(mContext);
+        delegatorView.setId(delegatorViewId);
+
+        mTestView.setHandwritingDelegateConfiguration(
+                new HandwritingDelegateConfiguration(
+                        delegatorViewId,
+                        () -> mHandwritingInitiator.onInputConnectionCreated(delegatorView)));
+
+        final int x1 = (sHwArea.left + sHwArea.right) / 2;
+        final int y1 = (sHwArea.top + sHwArea.bottom) / 2;
+        MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
+        mHandwritingInitiator.onTouchEvent(stylusEvent1);
+
+        final int x2 = x1 + mHandwritingSlop * 2;
+        final int y2 = y1;
+        MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
+        mHandwritingInitiator.onTouchEvent(stylusEvent2);
+
+        verify(mHandwritingInitiator, times(1)).startHandwriting(delegatorView);
+    }
+
+    @Test
     public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
         final Rect rect = new Rect(600, 600, 900, 900);
         final View testView = createView(rect, true /* autoHandwritingEnabled */,
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 00b3693..bbf9f3c 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -128,6 +128,7 @@
         RemoteViews clone = child.clone();
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     @Test
     public void clone_repeatedly() {
         RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
@@ -485,6 +486,7 @@
         }
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     @Test
     public void nestedAddViews() {
         RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
@@ -509,6 +511,7 @@
         parcelAndRecreate(views);
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     @Test
     public void nestedLandscapeViews() {
         RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f448cb3..f370ebd 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -60,6 +60,8 @@
     private OnBackAnimationCallback mCallback1;
     @Mock
     private OnBackAnimationCallback mCallback2;
+    @Mock
+    private BackEvent mBackEvent;
 
     @Before
     public void setUp() throws Exception {
@@ -85,14 +87,14 @@
         verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo(
                 Mockito.eq(mWindow),
                 captor.capture());
-        captor.getAllValues().get(0).getCallback().onBackStarted();
+        captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
         verifyZeroInteractions(mCallback2);
 
-        captor.getAllValues().get(1).getCallback().onBackStarted();
+        captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
         verifyNoMoreInteractions(mCallback1);
     }
 
@@ -110,9 +112,9 @@
                 Mockito.eq(mWindow), captor.capture());
         verifyNoMoreInteractions(mWindowSession);
         assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
     }
 
     @Test
@@ -148,8 +150,8 @@
         mDispatcher.registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2);
         verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
index 499f7a5..875cd0b 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityOverrideData.java
@@ -24,11 +24,13 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.os.UserHandle;
+import android.util.Pair;
 
 import com.android.internal.app.chooser.TargetInfo;
 import com.android.internal.logging.MetricsLogger;
 
 import java.util.List;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 /**
@@ -50,6 +52,9 @@
     public Function<PackageManager, PackageManager> createPackageManager;
     public Function<TargetInfo, Boolean> onSafelyStartCallback;
     public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
+    public BiFunction<
+            IChooserWrapper, ChooserListAdapter, Pair<Integer, ChooserActivity.ServiceResultInfo[]>>
+            directShareTargets;
     public ResolverListController resolverListController;
     public ResolverListController workResolverListController;
     public Boolean isVoiceInteraction;
@@ -72,6 +77,7 @@
     public void reset() {
         onSafelyStartCallback = null;
         onQueryDirectShareTargets = null;
+        directShareTargets = null;
         isVoiceInteraction = null;
         createPackageManager = null;
         previewThumbnail = null;
@@ -112,4 +118,3 @@
 
     private ChooserActivityOverrideData() {}
 }
-
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index e8c7ce0..d656678 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -81,6 +81,7 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.service.chooser.ChooserTarget;
+import android.util.Pair;
 import android.view.View;
 
 import androidx.annotation.CallSuper;
@@ -89,6 +90,7 @@
 import androidx.test.rule.ActivityTestRule;
 
 import com.android.internal.R;
+import com.android.internal.app.ChooserActivity.ServiceResultInfo;
 import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
 import com.android.internal.app.chooser.DisplayResolveInfo;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -2166,8 +2168,8 @@
         assertThat(logger.numCalls(), is(6));
     }
 
-    @Test @Ignore
-    public void testDirectTargetLogging() throws InterruptedException {
+    @Test
+    public void testDirectTargetLogging() {
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -2187,30 +2189,35 @@
                 resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
         ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
 
+        ChooserActivityOverrideData
+                .getInstance()
+                .directShareTargets = (activity, adapter) -> {
+                    DisplayResolveInfo displayInfo = activity.createTestDisplayResolveInfo(
+                            sendIntent,
+                            ri,
+                             "testLabel",
+                             "testInfo",
+                            sendIntent,
+                            /* resolveInfoPresentationGetter */ null);
+                    ServiceResultInfo[] results = {
+                            new ServiceResultInfo(
+                                    displayInfo,
+                                    serviceTargets,
+                                    adapter.getUserHandle())};
+                    // TODO: consider covering the other type.
+                    //  Only 2 types are expected out of the shortcut loading logic:
+                    //  - TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, if shortcuts were loaded from
+                    //    the ShortcutManager, and;
+                    //  - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, if shortcuts were loaded
+                    //    from AppPredictor.
+                    //  Ideally, our tests should cover all of them.
+                    return new Pair<>(TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, results);
+                };
+
         // Start activity
         final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
 
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        directShareToShortcutInfos.put(serviceTargets.get(0), null);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> activity.getAdapter().addServiceResults(
-                        activity.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_CHOOSER_TARGET,
-                        directShareToShortcutInfos)
-        );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
-
         assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
                 activity.getAdapter().getCount(), is(3));
         assertThat("Chooser should have exactly one selectable direct target",
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index 7f85982..4c3235c 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -31,6 +31,7 @@
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.UserHandle;
+import android.util.Pair;
 import android.util.Size;
 
 import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
@@ -239,6 +240,12 @@
     @Override
     protected void queryDirectShareTargets(ChooserListAdapter adapter,
             boolean skipAppPredictionService) {
+        if (sOverrides.directShareTargets != null) {
+            Pair<Integer, ServiceResultInfo[]> result =
+                    sOverrides.directShareTargets.apply(this, adapter);
+            sendShortcutManagerShareTargetResults(result.first, result.second);
+            return;
+        }
         if (sOverrides.onQueryDirectShareTargets != null) {
             sOverrides.onQueryDirectShareTargets.apply(adapter);
         }
diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java
new file mode 100644
index 0000000..f111bf6f
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.inputmethod;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.InvalidParameterException;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class InputMethodSubtypeHandleTest {
+
+    @Test
+    public void testCreateFromRawHandle() {
+        {
+            final InputMethodSubtypeHandle handle =
+                    InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1");
+            assertNotNull(handle);
+            assertEquals("com.android.test/.Ime1:subtype:1", handle.toStringHandle());
+            assertEquals("com.android.test/.Ime1", handle.getImeId());
+            assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+                    handle.getComponentName());
+        }
+
+        assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(""));
+
+        // The IME ID must use ComponentName#flattenToShortString(), not #flattenToString().
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/com.android.test.Ime1:subtype:1"));
+
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:0001"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:1!"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:1:"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:1:2"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:a"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:subtype:0x01"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "com.android.test/.Ime1:Subtype:a"));
+        assertThrows(InvalidParameterException.class, () -> InputMethodSubtypeHandle.of(
+                "ime1:subtype:1"));
+    }
+
+    @Test
+    public void testCreateFromInputMethodInfo() {
+        final InputMethodInfo imi = new InputMethodInfo(
+                "com.android.test", "com.android.test.Ime1", "TestIME", null);
+        {
+            final InputMethodSubtypeHandle handle = InputMethodSubtypeHandle.of(imi, null);
+            assertNotNull(handle);
+            assertEquals("com.android.test/.Ime1:subtype:0", handle.toStringHandle());
+            assertEquals("com.android.test/.Ime1", handle.getImeId());
+            assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+                    handle.getComponentName());
+        }
+
+        final InputMethodSubtype subtype =
+                new InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(1).build();
+        {
+            final InputMethodSubtypeHandle handle = InputMethodSubtypeHandle.of(imi, subtype);
+            assertNotNull(handle);
+            assertEquals("com.android.test/.Ime1:subtype:1", handle.toStringHandle());
+            assertEquals("com.android.test/.Ime1", handle.getImeId());
+            assertEquals(ComponentName.unflattenFromString("com.android.test/.Ime1"),
+                    handle.getComponentName());
+        }
+
+        assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null, null));
+        assertThrows(NullPointerException.class, () -> InputMethodSubtypeHandle.of(null, subtype));
+    }
+
+    @Test
+    public void testEquality() {
+        assertEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+                InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"));
+        assertEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1").hashCode(),
+                InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1").hashCode());
+
+        assertNotEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+                InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:2"));
+        assertNotEquals(InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1"),
+                InputMethodSubtypeHandle.of("com.android.test/.Ime2:subtype:1"));
+    }
+
+    @Test
+    public void testParcelablility() {
+        final InputMethodSubtypeHandle original =
+                InputMethodSubtypeHandle.of("com.android.test/.Ime1:subtype:1");
+        final InputMethodSubtypeHandle cloned = cloneHandle(original);
+        assertEquals(original, cloned);
+        assertEquals(original.hashCode(), cloned.hashCode());
+        assertEquals(original.getComponentName(), cloned.getComponentName());
+        assertEquals(original.getImeId(), cloned.getImeId());
+        assertEquals(original.toStringHandle(), cloned.toStringHandle());
+    }
+
+    @Test
+    public void testNoUnnecessaryStringInstantiationInToStringHandle() {
+        final String validHandleStr = "com.android.test/.Ime1:subtype:1";
+        // Verify that toStringHandle() returns the same String object if the input is valid for
+        // an efficient memory usage.
+        assertSame(validHandleStr, InputMethodSubtypeHandle.of(validHandleStr).toStringHandle());
+    }
+
+    @NonNull
+    private static InputMethodSubtypeHandle cloneHandle(
+            @NonNull InputMethodSubtypeHandle original) {
+        Parcel parcel = null;
+        try {
+            parcel = Parcel.obtain();
+            original.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            return InputMethodSubtypeHandle.CREATOR.createFromParcel(parcel);
+        } finally {
+            if (parcel != null) {
+                parcel.recycle();
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
index 82b2bf4..8207c9e 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
@@ -1054,10 +1054,23 @@
             super(new Injector() {
                 public Random getRandomGenerator() {
                     return new Random() {
-                        int mCallCount = 0;
+                        int mCallCount = -1;
 
                         public int nextInt() {
-                            return mCallCount++;
+                            throw new IllegalStateException("Should not use nextInt()");
+                        }
+
+                        public int nextInt(int x) {
+                            if (mCallCount == -1) {
+                                // The tests are written such that they expect
+                                // the first call to nextInt() to be on the first
+                                // callEnded(). However, the BinderCallsStats
+                                // constructor also calls nextInt(). Fake 0 being
+                                // rolled twice.
+                                mCallCount++;
+                                return 0;
+                            }
+                            return (mCallCount++) % x;
                         }
                     };
                 }
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java b/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java
index 5af7376..7bd53b9 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java
@@ -98,7 +98,7 @@
         assertEquals(1, latencyHistograms.size());
         LatencyDims dims = latencyHistograms.keySet().iterator().next();
         assertEquals(binder.getClass(), dims.getBinderClass());
-        assertEquals(1, dims.getTransactionCode());
+        assertEquals(2, dims.getTransactionCode()); // the first nextInt() is in the constructor
         assertThat(latencyHistograms.get(dims)).asList().containsExactly(1, 0, 0, 0, 0).inOrder();
     }
 
@@ -313,11 +313,11 @@
                                 int mCallCount = 0;
 
                                 public int nextInt() {
-                                    return mCallCount++;
+                                    throw new IllegalStateException("Should not use nextInt()");
                                 }
 
                                 public int nextInt(int x) {
-                                    return 1;
+                                    return (mCallCount++) % x;
                                 }
                             };
                         }
diff --git a/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java b/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java
index b34554c..c3d40eb 100644
--- a/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/ProcLocksReaderTest.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.os.FileUtils;
+import android.util.IntArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -33,13 +34,15 @@
 import java.io.File;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ProcLocksReaderTest implements
         ProcLocksReader.ProcLocksReaderCallback {
     private File mProcDirectory;
-    private ArrayList<Integer> mPids = new ArrayList<>();
+
+    private ArrayList<int[]> mPids = new ArrayList<>();
 
     @Before
     public void setUp() {
@@ -54,41 +57,51 @@
 
     @Test
     public void testRunSimpleLocks() throws Exception {
-        String simpleLocks =
-                "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n" +
-                "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n";
+        String simpleLocks = "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n"
+                           + "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n";
         runHandleBlockingFileLocks(simpleLocks);
         assertTrue(mPids.isEmpty());
     }
 
     @Test
     public void testRunBlockingLocks() throws Exception {
-        String blockedLocks =
-                "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n" +
-                "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n" +
-                "2: -> POSIX  ADVISORY  WRITE 18291 fd:09:34062 0 EOF\n" +
-                "2: -> POSIX  ADVISORY  WRITE 18293 fd:09:34062 0 EOF\n" +
-                "3: POSIX  ADVISORY  READ  3888 fd:09:13992 128 128\n" +
-                "4: POSIX  ADVISORY  READ  3888 fd:09:14230 1073741826 1073742335\n";
+        String blockedLocks = "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n"
+                            + "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18291 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18293 fd:09:34062 0 EOF\n"
+                            + "3: POSIX  ADVISORY  READ  3888 fd:09:13992 128 128\n"
+                            + "4: POSIX  ADVISORY  READ  3888 fd:09:14230 1073741826 1073742335\n";
         runHandleBlockingFileLocks(blockedLocks);
-        assertTrue(mPids.remove(0).equals(18292));
+        assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293}));
+        assertTrue(mPids.isEmpty());
+    }
+
+    @Test
+    public void testRunLastBlockingLocks() throws Exception {
+        String blockedLocks = "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n"
+                            + "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18291 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18293 fd:09:34062 0 EOF\n";
+        runHandleBlockingFileLocks(blockedLocks);
+        assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293}));
         assertTrue(mPids.isEmpty());
     }
 
     @Test
     public void testRunMultipleBlockingLocks() throws Exception {
-        String blockedLocks =
-                "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n" +
-                "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n" +
-                "2: -> POSIX  ADVISORY  WRITE 18291 fd:09:34062 0 EOF\n" +
-                "2: -> POSIX  ADVISORY  WRITE 18293 fd:09:34062 0 EOF\n" +
-                "3: POSIX  ADVISORY  READ  3888 fd:09:13992 128 128\n" +
-                "4: FLOCK  ADVISORY  WRITE 3840 fe:01:5111809 0 EOF\n" +
-                "4: -> FLOCK  ADVISORY  WRITE 3841 fe:01:5111809 0 EOF\n" +
-                "5: POSIX  ADVISORY  READ  3888 fd:09:14230 1073741826 1073742335\n";
+        String blockedLocks = "1: POSIX  ADVISORY  READ  18403 fd:09:9070 1073741826 1073742335\n"
+                            + "2: POSIX  ADVISORY  WRITE 18292 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18291 fd:09:34062 0 EOF\n"
+                            + "2: -> POSIX  ADVISORY  WRITE 18293 fd:09:34062 0 EOF\n"
+                            + "3: POSIX  ADVISORY  READ  3888 fd:09:13992 128 128\n"
+                            + "4: FLOCK  ADVISORY  WRITE 3840 fe:01:5111809 0 EOF\n"
+                            + "4: -> FLOCK  ADVISORY  WRITE 3841 fe:01:5111809 0 EOF\n"
+                            + "5: FLOCK  ADVISORY  READ  3888 fd:09:14230 0 EOF\n"
+                            + "5: -> FLOCK  ADVISORY  READ  3887 fd:09:14230 0 EOF\n";
         runHandleBlockingFileLocks(blockedLocks);
-        assertTrue(mPids.remove(0).equals(18292));
-        assertTrue(mPids.remove(0).equals(3840));
+        assertTrue(Arrays.equals(mPids.remove(0), new int[]{18292, 18291, 18293}));
+        assertTrue(Arrays.equals(mPids.remove(0), new int[]{3840, 3841}));
+        assertTrue(Arrays.equals(mPids.remove(0), new int[]{3888, 3887}));
         assertTrue(mPids.isEmpty());
     }
 
@@ -102,11 +115,12 @@
 
     /**
      * Call the callback function of handleBlockingFileLocks().
-     *
-     * @param pid Each process that hold file locks blocking other processes.
+     * @param pids Each process that hold file locks blocking other processes.
+     *             pids[0] is the process blocking others
+     *             pids[1..n-1] are the processes being blocked
      */
     @Override
-    public void onBlockingFileLock(int pid) {
-        mPids.add(pid);
+    public void onBlockingFileLock(IntArray pids) {
+        mPids.add(pids.toArray());
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java b/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java
new file mode 100644
index 0000000..c540a15
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/SafeZipPathValidatorCallbackTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.os;
+
+import static org.junit.Assert.assertThrows;
+
+import android.compat.testing.PlatformCompatChangeRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Test SafeZipPathCallback.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SafeZipPathValidatorCallbackTest {
+    @Rule
+    public TestRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+    @Before
+    public void setUp() {
+        RuntimeInit.initZipPathValidatorCallback();
+    }
+
+    @Test
+    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void testNewZipFile_whenZipFileHasDangerousEntriesAndChangeEnabled_throws()
+            throws Exception {
+        final String[] dangerousEntryNames = {
+            "../foo.bar",
+            "foo/../bar.baz",
+            "foo/../../bar.baz",
+            "foo.bar/..",
+            "foo.bar/../",
+            "..",
+            "../",
+            "/foo",
+        };
+        for (String entryName : dangerousEntryNames) {
+            final File tempFile = File.createTempFile("smdc", "zip");
+            try {
+                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);
+
+                assertThrows(
+                        "ZipException expected for entry: " + entryName,
+                        ZipException.class,
+                        () -> {
+                            new ZipFile(tempFile);
+                        });
+            } finally {
+                tempFile.delete();
+            }
+        }
+    }
+
+    @Test
+    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void
+            testZipInputStreamGetNextEntry_whenZipFileHasDangerousEntriesAndChangeEnabled_throws()
+                    throws Exception {
+        final String[] dangerousEntryNames = {
+            "../foo.bar",
+            "foo/../bar.baz",
+            "foo/../../bar.baz",
+            "foo.bar/..",
+            "foo.bar/../",
+            "..",
+            "../",
+            "/foo",
+        };
+        for (String entryName : dangerousEntryNames) {
+            byte[] badZipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
+            try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(badZipBytes))) {
+                assertThrows(
+                        "ZipException expected for entry: " + entryName,
+                        ZipException.class,
+                        () -> {
+                            zis.getNextEntry();
+                        });
+            }
+        }
+    }
+
+    @Test
+    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void testNewZipFile_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow()
+            throws Exception {
+        final String[] normalEntryNames = {
+            "foo", "foo.bar", "foo..bar",
+        };
+        for (String entryName : normalEntryNames) {
+            final File tempFile = File.createTempFile("smdc", "zip");
+            try {
+                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);
+                try {
+                    new ZipFile((tempFile));
+                } catch (ZipException e) {
+                    throw new AssertionError("ZipException not expected for entry: " + entryName);
+                }
+            } finally {
+                tempFile.delete();
+            }
+        }
+    }
+
+    @Test
+    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void
+            testZipInputStreamGetNextEntry_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow()
+                    throws Exception {
+        final String[] normalEntryNames = {
+            "foo", "foo.bar", "foo..bar",
+        };
+        for (String entryName : normalEntryNames) {
+            byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
+            try {
+                ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes));
+                zis.getNextEntry();
+            } catch (ZipException e) {
+                throw new AssertionError("ZipException not expected for entry: " + entryName);
+            }
+        }
+    }
+
+    @Test
+    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void
+            testNewZipFile_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow()
+                    throws Exception {
+        final String[] entryNames = {
+            "../foo.bar",
+            "foo/../bar.baz",
+            "foo/../../bar.baz",
+            "foo.bar/..",
+            "foo.bar/../",
+            "..",
+            "../",
+            "/foo",
+            "foo",
+            "foo.bar",
+            "foo..bar",
+        };
+        for (String entryName : entryNames) {
+            final File tempFile = File.createTempFile("smdc", "zip");
+            try {
+                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);
+                try {
+                    new ZipFile((tempFile));
+                } catch (ZipException e) {
+                    throw new AssertionError("ZipException not expected for entry: " + entryName);
+                }
+            } finally {
+                tempFile.delete();
+            }
+        }
+    }
+
+    @Test
+    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
+    public void
+            testZipInputStreamGetNextEntry_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow()
+                    throws Exception {
+        final String[] entryNames = {
+            "../foo.bar",
+            "foo/../bar.baz",
+            "foo/../../bar.baz",
+            "foo.bar/..",
+            "foo.bar/../",
+            "..",
+            "../",
+            "/foo",
+            "foo",
+            "foo.bar",
+            "foo..bar",
+        };
+        for (String entryName : entryNames) {
+            byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
+            try {
+                ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes));
+                zis.getNextEntry();
+            } catch (ZipException e) {
+                throw new AssertionError("ZipException not expected for entry: " + entryName);
+            }
+        }
+    }
+
+    private void writeZipFileOutputStreamWithEmptyEntry(File tempFile, String entryName)
+            throws IOException {
+        FileOutputStream tempFileStream = new FileOutputStream(tempFile);
+        writeZipOutputStreamWithEmptyEntry(tempFileStream, entryName);
+        tempFileStream.close();
+    }
+
+    private byte[] getZipBytesFromZipOutputStreamWithEmptyEntry(String entryName)
+            throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        writeZipOutputStreamWithEmptyEntry(bos, entryName);
+        return bos.toByteArray();
+    }
+
+    private void writeZipOutputStreamWithEmptyEntry(OutputStream os, String entryName)
+            throws IOException {
+        ZipOutputStream zos = new ZipOutputStream(os);
+        ZipEntry entry = new ZipEntry(entryName);
+        zos.putNextEntry(entry);
+        zos.write(new byte[2]);
+        zos.closeEntry();
+        zos.close();
+    }
+}
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/OWNERS b/core/tests/coretests/src/com/android/internal/security/OWNERS
new file mode 100644
index 0000000..4f4d8d7
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/security/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 36824
+
+per-file VerityUtilsTest.java = file:platform/system/security:/fsverity/OWNERS
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/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
index c53fb23..048c48b 100644
--- a/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
+++ b/core/tests/coretests/src/com/android/internal/statusbar/RegisterStatusBarResultTest.java
@@ -25,7 +25,7 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 import android.util.ArrayMap;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -65,7 +65,7 @@
                 new Binder() /* imeToken */,
                 true /* navbarColorManagedByIme */,
                 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                new InsetsVisibilities() /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 "test" /* packageName */,
                 new int[0] /* transientBarTypes */,
                 new LetterboxDetails[] {letterboxDetails});
@@ -87,7 +87,7 @@
         assertThat(copy.mImeToken).isSameInstanceAs(original.mImeToken);
         assertThat(copy.mNavbarColorManagedByIme).isEqualTo(original.mNavbarColorManagedByIme);
         assertThat(copy.mBehavior).isEqualTo(original.mBehavior);
-        assertThat(copy.mRequestedVisibilities).isEqualTo(original.mRequestedVisibilities);
+        assertThat(copy.mRequestedVisibleTypes).isEqualTo(original.mRequestedVisibleTypes);
         assertThat(copy.mPackageName).isEqualTo(original.mPackageName);
         assertThat(copy.mTransientBarTypes).isEqualTo(original.mTransientBarTypes);
         assertThat(copy.mLetterboxDetails).isEqualTo(original.mLetterboxDetails);
diff --git a/core/tests/coretests/src/com/android/internal/util/FastDataTest.java b/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
index 04dfd6e..de325ab 100644
--- a/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
+++ b/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
@@ -23,6 +23,9 @@
 import android.annotation.NonNull;
 import android.util.ExceptionUtils;
 
+import com.android.modules.utils.FastDataInput;
+import com.android.modules.utils.FastDataOutput;
+
 import libcore.util.HexEncoding;
 
 import org.junit.Assume;
@@ -39,6 +42,8 @@
 import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
@@ -61,14 +66,32 @@
         this.use4ByteSequence = use4ByteSequence;
     }
 
+    @NonNull
+    private FastDataInput createFastDataInput(@NonNull InputStream in, int bufferSize) {
+        if (use4ByteSequence) {
+            return new ArtFastDataInput(in, bufferSize);
+        } else {
+            return new FastDataInput(in, bufferSize);
+        }
+    }
+
+    @NonNull
+    private FastDataOutput createFastDataOutput(@NonNull OutputStream out, int bufferSize) {
+        if (use4ByteSequence) {
+            return new ArtFastDataOutput(out, bufferSize);
+        } else {
+            return new FastDataOutput(out, bufferSize);
+        }
+    }
+
     @Test
     public void testEndOfFile_Int() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readInt());
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             assertEquals(1, in.readByte());
             assertThrows(EOFException.class, () -> in.readInt());
         }
@@ -76,25 +99,25 @@
 
     @Test
     public void testEndOfFile_String() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readUTF());
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readUTF());
         }
     }
 
     @Test
     public void testEndOfFile_Bytes_Small() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             final byte[] tmp = new byte[10];
             assertThrows(EOFException.class, () -> in.readFully(tmp));
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             final byte[] tmp = new byte[10_000];
             assertThrows(EOFException.class, () -> in.readFully(tmp));
         }
@@ -103,8 +126,7 @@
     @Test
     public void testUTF_Bounds() throws Exception {
         final char[] buf = new char[65_534];
-        try (FastDataOutput out = new FastDataOutput(new ByteArrayOutputStream(),
-                BOUNCE_SIZE, use4ByteSequence)) {
+        try (FastDataOutput out = createFastDataOutput(new ByteArrayOutputStream(), BOUNCE_SIZE)) {
             // Writing simple string will fit fine
             Arrays.fill(buf, '!');
             final String simple = new String(buf);
@@ -132,17 +154,15 @@
             doTranscodeWrite(out);
             out.flush();
 
-            final FastDataInput in = new FastDataInput(
-                    new ByteArrayInputStream(outStream.toByteArray()),
-                    BOUNCE_SIZE, use4ByteSequence);
+            final FastDataInput in = createFastDataInput(
+                    new ByteArrayInputStream(outStream.toByteArray()), BOUNCE_SIZE);
             doTranscodeRead(in);
         }
 
         // Verify that fast data can be read by upstream
         {
             final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-            final FastDataOutput out = new FastDataOutput(outStream,
-                    BOUNCE_SIZE, use4ByteSequence);
+            final FastDataOutput out = createFastDataOutput(outStream, BOUNCE_SIZE);
             doTranscodeWrite(out);
             out.flush();
 
@@ -299,7 +319,7 @@
         final DataOutput slowData = new DataOutputStream(slowStream);
 
         final ByteArrayOutputStream fastStream = new ByteArrayOutputStream();
-        final FastDataOutput fastData = FastDataOutput.obtainUsing3ByteSequences(fastStream);
+        final FastDataOutput fastData = FastDataOutput.obtain(fastStream);
 
         for (int cp = Character.MIN_CODE_POINT; cp < Character.MAX_CODE_POINT; cp++) {
             if (Character.isValidCodePoint(cp)) {
@@ -416,16 +436,14 @@
     private void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
             @NonNull ThrowingConsumer<FastDataInput> in, int count) throws Exception {
         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-        final FastDataOutput outData = new FastDataOutput(outStream,
-                BOUNCE_SIZE, use4ByteSequence);
+        final FastDataOutput outData = createFastDataOutput(outStream, BOUNCE_SIZE);
         for (int i = 0; i < count; i++) {
             out.accept(outData);
         }
         outData.flush();
 
         final ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
-        final FastDataInput inData = new FastDataInput(inStream,
-                BOUNCE_SIZE, use4ByteSequence);
+        final FastDataInput inData = createFastDataInput(inStream, BOUNCE_SIZE);
         for (int i = 0; i < count; i++) {
             in.accept(inData);
         }
diff --git a/core/tests/coretests/src/com/android/internal/util/TokenBucketTest.java b/core/tests/coretests/src/com/android/internal/util/TokenBucketTest.java
index 6c50bce..8b30828 100644
--- a/core/tests/coretests/src/com/android/internal/util/TokenBucketTest.java
+++ b/core/tests/coretests/src/com/android/internal/util/TokenBucketTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.util;
 
+import static org.junit.Assert.assertThrows;
+
 import android.os.SystemClock;
 import android.text.format.DateUtils;
 
@@ -170,10 +172,9 @@
     }
 
     void assertThrow(Fn fn) {
-        try {
+        assertThrows(Throwable.class, () -> {
             fn.call();
-            fail("expected n exception to be thrown.");
-        } catch (Throwable t) { }
+        });
     }
 
     interface Fn { void call(); }
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/src/com/android/multidexlegacytestapp/Test.java b/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/src/com/android/multidexlegacytestapp/Test.java
index 41b8956f..a226325 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/src/com/android/multidexlegacytestapp/Test.java
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/src/com/android/multidexlegacytestapp/Test.java
@@ -31,6 +31,7 @@
         assertEquals(3366, getActivity().getValue());
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     public void testAnnotation() throws Exception {
         assertEquals(ReferencedByAnnotation.B,
                 ((AnnotationWithEnum) TestApplication.annotation).value());
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index 613eddd..88b2de7 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -207,7 +207,7 @@
 
         assertFalse("Must not report change if no public diff",
                 shouldReportChange(currentConfig, newConfig, null /* sizeBuckets */,
-                        0 /* handledConfigChanges */));
+                        0 /* handledConfigChanges */, false /* alwaysReportChange */));
 
         final int[] verticalThresholds = {100, 400};
         final SizeConfigurationBuckets buckets = new SizeConfigurationBuckets(
@@ -221,24 +221,33 @@
 
         assertFalse("Must not report changes if the diff is small and not handled",
                 shouldReportChange(currentConfig, newConfig, buckets,
-                        CONFIG_FONT_SCALE /* handledConfigChanges */));
+                        CONFIG_FONT_SCALE /* handledConfigChanges */,
+                        false /* alwaysReportChange */));
 
         assertTrue("Must report changes if the small diff is handled",
                 shouldReportChange(currentConfig, newConfig, buckets,
-                        CONFIG_SCREEN_SIZE /* handledConfigChanges */));
+                        CONFIG_SCREEN_SIZE /* handledConfigChanges */,
+                        false /* alwaysReportChange */));
+
+        assertTrue("Must report changes if it should, even it is small and not handled",
+                shouldReportChange(currentConfig, newConfig, buckets,
+                        CONFIG_FONT_SCALE /* handledConfigChanges */,
+                        true /* alwaysReportChange */));
 
         currentConfig.fontScale = 0.8f;
         newConfig.fontScale = 1.2f;
 
         assertTrue("Must report handled changes regardless of small unhandled change",
                 shouldReportChange(currentConfig, newConfig, buckets,
-                        CONFIG_FONT_SCALE /* handledConfigChanges */));
+                        CONFIG_FONT_SCALE /* handledConfigChanges */,
+                        false /* alwaysReportChange */));
 
         newConfig.screenHeightDp = 500;
 
         assertFalse("Must not report changes if there's unhandled big changes",
                 shouldReportChange(currentConfig, newConfig, buckets,
-                        CONFIG_FONT_SCALE /* handledConfigChanges */));
+                        CONFIG_FONT_SCALE /* handledConfigChanges */,
+                        false /* alwaysReportChange */));
     }
 
     private void recreateAndVerifyNoRelaunch(ActivityThread activityThread, TestActivity activity) {
diff --git a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
index 3465989..2da9a2e 100644
--- a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
+++ b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertThrows;
@@ -45,7 +44,6 @@
 import org.junit.runners.JUnit4;
 
 import java.util.Collections;
-import java.util.List;
 import java.util.concurrent.TimeoutException;
 
 @RunWith(JUnit4.class)
@@ -221,11 +219,56 @@
     }
 
     @Test
+    public void setResourceValue_withNullResourceName() throws Exception {
+        final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder(
+                "android", TEST_OVERLAY_NAME, mContext.getPackageName());
+
+        assertThrows(NullPointerException.class,
+                () -> builder.setResourceValue(null, TypedValue.TYPE_INT_DEC, 1));
+    }
+
+    @Test
+    public void setResourceValue_withEmptyResourceName() throws Exception {
+        final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder(
+                "android", TEST_OVERLAY_NAME, mContext.getPackageName());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setResourceValue("", TypedValue.TYPE_INT_DEC, 1));
+    }
+
+    @Test
+    public void setResourceValue_withEmptyPackageName() throws Exception {
+        final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder(
+                "android", TEST_OVERLAY_NAME, mContext.getPackageName());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setResourceValue(":color/mycolor", TypedValue.TYPE_INT_DEC, 1));
+    }
+
+    @Test
+    public void setResourceValue_withInvalidTypeName() throws Exception {
+        final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder(
+                "android", TEST_OVERLAY_NAME, mContext.getPackageName());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setResourceValue("c/mycolor", TypedValue.TYPE_INT_DEC, 1));
+    }
+
+    @Test
+    public void setResourceValue_withEmptyTypeName() throws Exception {
+        final FabricatedOverlay.Builder builder = new FabricatedOverlay.Builder(
+                "android", TEST_OVERLAY_NAME, mContext.getPackageName());
+
+        assertThrows(IllegalArgumentException.class,
+                () -> builder.setResourceValue("/mycolor", TypedValue.TYPE_INT_DEC, 1));
+    }
+
+    @Test
     public void testInvalidResourceValues() throws Exception {
         final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
                 "android", TEST_OVERLAY_NAME, mContext.getPackageName())
                 .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
-                .setResourceValue("something", TypedValue.TYPE_INT_DEC, 1)
+                .setResourceValue("color/something", TypedValue.TYPE_INT_DEC, 1)
                 .build();
 
         waitForResourceValue(0);
diff --git a/core/tests/utiltests/src/com/android/internal/util/CallbackRegistryTest.java b/core/tests/utiltests/src/com/android/internal/util/CallbackRegistryTest.java
index c53f4cc..1581abb 100644
--- a/core/tests/utiltests/src/com/android/internal/util/CallbackRegistryTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/CallbackRegistryTest.java
@@ -20,6 +20,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Objects;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -39,11 +40,11 @@
     Integer argValue;
 
     private void addNotifyCount(Integer callback) {
-        if (callback == callback1) {
+        if (Objects.equals(callback, callback1)) {
             notify1++;
-        } else if (callback == callback2) {
+        } else if (Objects.equals(callback, callback2)) {
             notify2++;
-        } else if (callback == callback3) {
+        } else if (Objects.equals(callback, callback3)) {
             notify3++;
         }
         deepNotifyCount[callback]++;
@@ -114,7 +115,7 @@
                     public void onNotifyCallback(Integer callback, CallbackRegistryTest sender,
                             int arg1, Integer arg) {
                         addNotifyCount(callback);
-                        if (callback == callback1) {
+                        if (Objects.equals(callback, callback1)) {
                             registry.remove(callback1);
                             registry.remove(callback2);
                         }
@@ -166,9 +167,9 @@
                     public void onNotifyCallback(Integer callback, CallbackRegistryTest sender,
                             int arg1, Integer arg) {
                         addNotifyCount(callback);
-                        if (callback == callback1) {
+                        if (Objects.equals(callback, callback1)) {
                             registry.remove(callback2);
-                        } else if (callback == callback3) {
+                        } else if (Objects.equals(callback, callback3)) {
                             registry.add(callback2);
                         }
                     }
diff --git a/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
index 06e9759..0484068 100644
--- a/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
@@ -18,10 +18,11 @@
 
 import static org.junit.Assert.assertArrayEquals;
 
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import junit.framework.TestCase;
 
 import java.io.ByteArrayInputStream;
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 699e794..3e2b71f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -265,6 +265,7 @@
         <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/>
         <permission name="android.permission.INSTALL_PACKAGES"/>
         <permission name="android.permission.INSTALL_PACKAGE_UPDATES"/>
+        <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/>
         <!-- Needed for test only -->
         <permission name="android.permission.ACCESS_MTP"/>
         <!-- Needed for test only -->
@@ -494,6 +495,10 @@
         <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
         <!-- Permission required for CTS test - CtsTelephonyTestCases -->
         <permission name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
+        <!-- Permission required for CTS test - CtsAppTestCases -->
+        <permission name="android.permission.CAPTURE_MEDIA_OUTPUT" />
+        <permission name="android.permission.CAPTURE_TUNER_AUDIO_INPUT" />
+        <permission name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index ca543f4..4cc06e3 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1357,6 +1357,12 @@
       "group": "WM_DEBUG_ANIM",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "-787664727": {
+      "message": "Cannot launch dream activity due to invalid state. dream component: %s packageName: %s",
+      "level": "ERROR",
+      "group": "WM_DEBUG_DREAM",
+      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
+    },
     "-784959154": {
       "message": "Attempted to add private presentation window to a non-private display.  Aborting.",
       "level": "WARN",
@@ -1951,6 +1957,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "-240296576": {
+      "message": "handleAppTransitionReady: displayId=%d appTransition={%s} openingApps=[%s] closingApps=[%s] transit=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "-237664290": {
       "message": "Pause the recording session on display %s",
       "level": "VERBOSE",
@@ -2053,12 +2065,6 @@
       "group": "WM_DEBUG_CONTENT_RECORDING",
       "at": "com\/android\/server\/wm\/ContentRecorder.java"
     },
-    "-134793542": {
-      "message": "handleAppTransitionReady: displayId=%d appTransition={%s} excludeLauncherFromAnimation=%b openingApps=[%s] closingApps=[%s] transit=%s",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_APP_TRANSITIONS",
-      "at": "com\/android\/server\/wm\/AppTransitionController.java"
-    },
     "-134091882": {
       "message": "Screenshotting Activity %s",
       "level": "VERBOSE",
@@ -2173,12 +2179,6 @@
       "group": "WM_DEBUG_ANIM",
       "at": "com\/android\/server\/wm\/WindowContainer.java"
     },
-    "-23020844": {
-      "message": "Back: Reset surfaces",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_BACK_PREVIEW",
-      "at": "com\/android\/server\/wm\/BackNavigationController.java"
-    },
     "-21399771": {
       "message": "activity %s already destroying, skipping request with reason:%s",
       "level": "VERBOSE",
@@ -2599,6 +2599,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "323235828": {
+      "message": "Delaying app transition for recents animation to finish",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_APP_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/AppTransitionController.java"
+    },
     "327461496": {
       "message": "Complete pause: %s",
       "level": "VERBOSE",
@@ -2887,6 +2893,12 @@
       "group": "WM_DEBUG_BOOT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "601283564": {
+      "message": "Dream packageName does not match active dream. Package %s does not match %s",
+      "level": "ERROR",
+      "group": "WM_DEBUG_DREAM",
+      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
+    },
     "608694300": {
       "message": "  NEW SURFACE SESSION %s",
       "level": "INFO",
@@ -3127,12 +3139,6 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/WindowStateAnimator.java"
     },
-    "829869827": {
-      "message": "Cannot launch dream activity due to invalid state. dreaming: %b packageName: %s",
-      "level": "ERROR",
-      "group": "WM_DEBUG_DREAM",
-      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
-    },
     "835814848": {
       "message": "%s",
       "level": "INFO",
@@ -3811,12 +3817,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1544805551": {
-      "message": "Skipping app transition animation. task=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_BACK_PREVIEW",
-      "at": "com\/android\/server\/wm\/Task.java"
-    },
     "1557732761": {
       "message": "For Intent %s bringing to top: %s",
       "level": "DEBUG",
@@ -4153,12 +4153,6 @@
       "group": "WM_DEBUG_WINDOW_ORGANIZER",
       "at": "com\/android\/server\/wm\/TaskOrganizerController.java"
     },
-    "1918771553": {
-      "message": "Dream packageName does not match active dream. Package %s does not match %s or %s",
-      "level": "ERROR",
-      "group": "WM_DEBUG_DREAM",
-      "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java"
-    },
     "1921821199": {
       "message": "Preserving %s until the new one is added",
       "level": "VERBOSE",
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientXmlChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientXmlChecker.java
index 8706a68..42e3046 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientXmlChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientXmlChecker.java
@@ -168,6 +168,7 @@
      */
     private static final Matcher<ExpressionTree> CONVERT_PRIMITIVE_TO_STRING =
             new Matcher<ExpressionTree>() {
+        @SuppressWarnings("TreeToString") //TODO: Fix me
         @Override
         public boolean matches(ExpressionTree tree, VisitorState state) {
             if (PRIMITIVE_TO_STRING.matches(tree, state)) {
@@ -205,6 +206,7 @@
      */
     private static final Matcher<ExpressionTree> CONVERT_STRING_TO_PRIMITIVE =
             new Matcher<ExpressionTree>() {
+        @SuppressWarnings("TreeToString") //TODO: Fix me
         @Override
         public boolean matches(ExpressionTree tree, VisitorState state) {
             if (PRIMITIVE_PARSE.matches(tree, state)) {
diff --git a/errorprone/refaster/EfficientXml.java b/errorprone/refaster/EfficientXml.java
index ae797c4..87a902a 100644
--- a/errorprone/refaster/EfficientXml.java
+++ b/errorprone/refaster/EfficientXml.java
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
 
diff --git a/errorprone/refaster/EfficientXml.java.refaster b/errorprone/refaster/EfficientXml.java.refaster
index 750c2db..c285e9b 100644
--- a/errorprone/refaster/EfficientXml.java.refaster
+++ b/errorprone/refaster/EfficientXml.java.refaster
Binary files differ
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index 1c41d06..9940ca3 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -47,7 +47,7 @@
             TransactionHangCallback callback);
 
     public interface TransactionHangCallback {
-        void onTransactionHang(boolean isGpuHang);
+        void onTransactionHang(String reason);
     }
 
     /** Create a new connection with the surface flinger. */
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index abf7e99..42c892a 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -1667,6 +1667,9 @@
      * effectively treating them as zeros. In API level {@value Build.VERSION_CODES#P} and above
      * these parameters will be respected.
      *
+     * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is
+     * ignored.</p>
+     *
      * @param bitmap The bitmap to draw using the mesh
      * @param meshWidth The number of columns in the mesh. Nothing is drawn if this is 0
      * @param meshHeight The number of rows in the mesh. Nothing is drawn if this is 0
@@ -1678,7 +1681,7 @@
      *            null, there must be at least (meshWidth+1) * (meshHeight+1) + colorOffset values
      *            in the array.
      * @param colorOffset Number of color elements to skip before drawing
-     * @param paint May be null. The paint used to draw the bitmap
+     * @param paint May be null. The paint used to draw the bitmap. Antialiasing is not supported.
      */
     public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
             @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
@@ -1832,9 +1835,12 @@
     /**
      * Draws the specified bitmap as an N-patch (most often, a 9-patch.)
      *
+     * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is
+     * ignored.</p>
+     *
      * @param patch The ninepatch object to render
      * @param dst The destination rectangle.
-     * @param paint The paint to draw the bitmap with. may be null
+     * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported.
      */
     public void drawPatch(@NonNull NinePatch patch, @NonNull Rect dst, @Nullable Paint paint) {
         super.drawPatch(patch, dst, paint);
@@ -1843,9 +1849,12 @@
     /**
      * Draws the specified bitmap as an N-patch (most often, a 9-patch.)
      *
+     * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is
+     * ignored.</p>
+     *
      * @param patch The ninepatch object to render
      * @param dst The destination rectangle.
-     * @param paint The paint to draw the bitmap with. may be null
+     * @param paint The paint to draw the bitmap with. May be null. Antialiasing is not supported.
      */
     public void drawPatch(@NonNull NinePatch patch, @NonNull RectF dst, @Nullable Paint paint) {
         super.drawPatch(patch, dst, paint);
@@ -2278,6 +2287,9 @@
      * array is optional, but if it is present, then it is used to specify the index of each
      * triangle, rather than just walking through the arrays in order.
      *
+     * <p>Note: antialiasing is not supported, therefore {@link Paint#ANTI_ALIAS_FLAG} is
+     * ignored.</p>
+     *
      * @param mode How to interpret the array of vertices
      * @param vertexCount The number of values in the vertices array (and corresponding texs and
      *            colors arrays if non-null). Each logical vertex is two values (x, y), vertexCount
@@ -2292,8 +2304,9 @@
      * @param colorOffset Number of values in colors to skip before drawing.
      * @param indices If not null, array of indices to reference into the vertex (texs, colors)
      *            array.
-     * @param indexCount number of entries in the indices array (if not null).
-     * @param paint Specifies the shader to use if the texs array is non-null.
+     * @param indexCount Number of entries in the indices array (if not null).
+     * @param paint Specifies the shader to use if the texs array is non-null. Antialiasing is not
+     *            supported.
      */
     public void drawVertices(@NonNull VertexMode mode, int vertexCount, @NonNull float[] verts,
             int vertOffset, @Nullable float[] texs, int texOffset, @Nullable int[] colors,
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 1a80ab3..f0e496f 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -137,6 +137,13 @@
      * <p>Enabling this flag will cause all draw operations that support
      * antialiasing to use it.</p>
      *
+     * <p>Notable draw operations that do <b>not</b> support antialiasing include:</p>
+     * <ul>
+     *      <li>{@link android.graphics.Canvas#drawBitmapMesh}</li>
+     *      <li>{@link android.graphics.Canvas#drawPatch}</li>
+     *      <li>{@link android.graphics.Canvas#drawVertices}</li>
+     * </ul>
+     *
      * @see #Paint(int)
      * @see #setFlags(int)
      */
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index 54c9f62..1a878df 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -766,7 +766,7 @@
         if (mBackground != null) {
             mBackground.onHotspotBoundsChanged();
         }
-        float newRadius = Math.round(getComputedRadius());
+        float newRadius = getComputedRadius();
         for (int i = 0; i < mRunningAnimations.size(); i++) {
             RippleAnimationSession s = mRunningAnimations.get(i);
             s.setRadius(newRadius);
diff --git a/graphics/java/android/graphics/drawable/VectorDrawable.java b/graphics/java/android/graphics/drawable/VectorDrawable.java
index 4065bd1..e25ee90 100644
--- a/graphics/java/android/graphics/drawable/VectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/VectorDrawable.java
@@ -60,7 +60,7 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Map;
 import java.util.Stack;
 
 /**
@@ -1171,18 +1171,14 @@
 
         private static final int NATIVE_ALLOCATION_SIZE = 100;
 
-        private static final HashMap<String, Integer> sPropertyIndexMap =
-                new HashMap<String, Integer>() {
-                    {
-                        put("translateX", TRANSLATE_X_INDEX);
-                        put("translateY", TRANSLATE_Y_INDEX);
-                        put("scaleX", SCALE_X_INDEX);
-                        put("scaleY", SCALE_Y_INDEX);
-                        put("pivotX", PIVOT_X_INDEX);
-                        put("pivotY", PIVOT_Y_INDEX);
-                        put("rotation", ROTATION_INDEX);
-                    }
-                };
+        private static final Map<String, Integer> sPropertyIndexMap = Map.of(
+                "translateX", TRANSLATE_X_INDEX,
+                "translateY", TRANSLATE_Y_INDEX,
+                "scaleX", SCALE_X_INDEX,
+                "scaleY", SCALE_Y_INDEX,
+                "pivotX", PIVOT_X_INDEX,
+                "pivotY", PIVOT_Y_INDEX,
+                "rotation", ROTATION_INDEX);
 
         static int getPropertyIndex(String propertyName) {
             if (sPropertyIndexMap.containsKey(propertyName)) {
@@ -1285,18 +1281,15 @@
                     }
                 };
 
-        private static final HashMap<String, Property> sPropertyMap =
-                new HashMap<String, Property>() {
-                    {
-                        put("translateX", TRANSLATE_X);
-                        put("translateY", TRANSLATE_Y);
-                        put("scaleX", SCALE_X);
-                        put("scaleY", SCALE_Y);
-                        put("pivotX", PIVOT_X);
-                        put("pivotY", PIVOT_Y);
-                        put("rotation", ROTATION);
-                    }
-                };
+        private static final Map<String, Property> sPropertyMap = Map.of(
+                "translateX", TRANSLATE_X,
+                "translateY", TRANSLATE_Y,
+                "scaleX", SCALE_X,
+                "scaleY", SCALE_Y,
+                "pivotX", PIVOT_X,
+                "pivotY", PIVOT_Y,
+                "rotation", ROTATION);
+
         // Temp array to store transform values obtained from native.
         private float[] mTransform;
         /////////////////////////////////////////////////////
@@ -1762,19 +1755,15 @@
 
         private static final int NATIVE_ALLOCATION_SIZE = 264;
         // Property map for animatable attributes.
-        private final static HashMap<String, Integer> sPropertyIndexMap
-                = new HashMap<String, Integer> () {
-            {
-                put("strokeWidth", STROKE_WIDTH_INDEX);
-                put("strokeColor", STROKE_COLOR_INDEX);
-                put("strokeAlpha", STROKE_ALPHA_INDEX);
-                put("fillColor", FILL_COLOR_INDEX);
-                put("fillAlpha", FILL_ALPHA_INDEX);
-                put("trimPathStart", TRIM_PATH_START_INDEX);
-                put("trimPathEnd", TRIM_PATH_END_INDEX);
-                put("trimPathOffset", TRIM_PATH_OFFSET_INDEX);
-            }
-        };
+        private static final Map<String, Integer> sPropertyIndexMap = Map.of(
+                "strokeWidth", STROKE_WIDTH_INDEX,
+                "strokeColor", STROKE_COLOR_INDEX,
+                "strokeAlpha", STROKE_ALPHA_INDEX,
+                "fillColor", FILL_COLOR_INDEX,
+                "fillAlpha", FILL_ALPHA_INDEX,
+                "trimPathStart", TRIM_PATH_START_INDEX,
+                "trimPathEnd", TRIM_PATH_END_INDEX,
+                "trimPathOffset", TRIM_PATH_OFFSET_INDEX);
 
         // Below are the Properties that wrap the setters to avoid reflection overhead in animations
         private static final Property<VFullPath, Float> STROKE_WIDTH =
@@ -1881,19 +1870,15 @@
                     }
                 };
 
-        private final static HashMap<String, Property> sPropertyMap
-                = new HashMap<String, Property> () {
-            {
-                put("strokeWidth", STROKE_WIDTH);
-                put("strokeColor", STROKE_COLOR);
-                put("strokeAlpha", STROKE_ALPHA);
-                put("fillColor", FILL_COLOR);
-                put("fillAlpha", FILL_ALPHA);
-                put("trimPathStart", TRIM_PATH_START);
-                put("trimPathEnd", TRIM_PATH_END);
-                put("trimPathOffset", TRIM_PATH_OFFSET);
-            }
-        };
+        private static final Map<String, Property> sPropertyMap = Map.of(
+                "strokeWidth", STROKE_WIDTH,
+                "strokeColor", STROKE_COLOR,
+                "strokeAlpha", STROKE_ALPHA,
+                "fillColor", FILL_COLOR,
+                "fillAlpha", FILL_ALPHA,
+                "trimPathStart", TRIM_PATH_START,
+                "trimPathEnd", TRIM_PATH_END,
+                "trimPathOffset", TRIM_PATH_OFFSET);
 
         // Temp array to store property data obtained from native getter.
         private byte[] mPropertyData;
diff --git a/graphics/java/android/graphics/fonts/FontStyle.java b/graphics/java/android/graphics/fonts/FontStyle.java
index 09799fd..48969aa 100644
--- a/graphics/java/android/graphics/fonts/FontStyle.java
+++ b/graphics/java/android/graphics/fonts/FontStyle.java
@@ -48,6 +48,10 @@
     private static final String TAG = "FontStyle";
 
     /**
+     * A default value when font weight is unspecified
+     */
+    public static final int FONT_WEIGHT_UNSPECIFIED = -1;
+    /**
      * A minimum weight value for the font
      */
     public static final int FONT_WEIGHT_MIN = 1;
diff --git a/graphics/java/android/view/PixelCopy.java b/graphics/java/android/view/PixelCopy.java
index 0f82c8f..889edb3 100644
--- a/graphics/java/android/view/PixelCopy.java
+++ b/graphics/java/android/view/PixelCopy.java
@@ -311,11 +311,11 @@
     /**
      * Contains the result of a PixelCopy request
      */
-    public static final class CopyResult {
+    public static final class Result {
         private int mStatus;
         private Bitmap mBitmap;
 
-        private CopyResult(@CopyResultStatus int status, Bitmap bitmap) {
+        private Result(@CopyResultStatus int status, Bitmap bitmap) {
             mStatus = status;
             mBitmap = bitmap;
         }
@@ -335,8 +335,8 @@
 
         /**
          * If the PixelCopy {@link Request} was given a destination bitmap with
-         * {@link Request#setDestinationBitmap(Bitmap)} then the returned bitmap will be the same
-         * as the one given. If no destination bitmap was provided, then this
+         * {@link Request.Builder#setDestinationBitmap(Bitmap)} then the returned bitmap will be
+         * the same as the one given. If no destination bitmap was provided, then this
          * will contain the automatically allocated Bitmap to hold the result.
          *
          * @return the Bitmap the copy request was stored in.
@@ -349,66 +349,199 @@
     }
 
     /**
-     * A builder to create the complete PixelCopy request, which is then executed by calling
-     * {@link #request()}
+     * Represents a PixelCopy request.
+     *
+     * To create a copy request, use either of the PixelCopy.Request.ofWindow or
+     * PixelCopy.Request.ofSurface factories to create a {@link Request.Builder} for the
+     * given source content. After setting any optional parameters, such as
+     * {@link Builder#setSourceRect(Rect)}, build the request with {@link Builder#build()} and
+     * then execute it with {@link PixelCopy#request(Request)}
      */
     public static final class Request {
+        private final Surface mSource;
+        private final Consumer<Result> mListener;
+        private final Executor mListenerThread;
+        private final Rect mSourceInsets;
+        private Rect mSrcRect;
+        private Bitmap mDest;
+
         private Request(Surface source, Rect sourceInsets, Executor listenerThread,
-                        Consumer<CopyResult> listener) {
+                        Consumer<Result> listener) {
             this.mSource = source;
             this.mSourceInsets = sourceInsets;
             this.mListenerThread = listenerThread;
             this.mListener = listener;
         }
 
-        private final Surface mSource;
-        private final Consumer<CopyResult> mListener;
-        private final Executor mListenerThread;
-        private final Rect mSourceInsets;
-        private Rect mSrcRect;
-        private Bitmap mDest;
-
         /**
-         * Sets the region of the source to copy from. By default, the entire source is copied to
-         * the output. If only a subset of the source is necessary to be copied, specifying a
-         * srcRect will improve performance by reducing
-         * the amount of data being copied.
-         *
-         * @param srcRect The area of the source to read from. Null or empty will be treated to
-         *                mean the entire source
-         * @return this
+         * A builder to create the complete PixelCopy request, which is then executed by calling
+         * {@link #request(Request)} with the built request returned from {@link #build()}
          */
-        public @NonNull Request setSourceRect(@Nullable Rect srcRect) {
-            this.mSrcRect = srcRect;
-            return this;
-        }
+        public static final class Builder {
+            private Request mRequest;
 
-        /**
-         * Specifies the output bitmap in which to store the result. By default, a Bitmap of format
-         * {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height matching that
-         * of the {@link #setSourceRect(Rect) source area} will be created to place the result.
-         *
-         * @param destination The bitmap to store the result, or null to have a bitmap
-         *                    automatically created of the appropriate size. If not null, must not
-         *                    be {@link Bitmap#isRecycled() recycled} and must be
-         *                    {@link Bitmap#isMutable() mutable}.
-         * @return this
-         */
-        public @NonNull Request setDestinationBitmap(@Nullable Bitmap destination) {
-            if (destination != null) {
-                validateBitmapDest(destination);
+            private Builder(Request request) {
+                mRequest = request;
             }
-            this.mDest = destination;
-            return this;
+
+            private void requireNotBuilt() {
+                if (mRequest == null) {
+                    throw new IllegalStateException("build() already called on this builder");
+                }
+            }
+
+            /**
+             * Sets the region of the source to copy from. By default, the entire source is copied
+             * to the output. If only a subset of the source is necessary to be copied, specifying
+             * a srcRect will improve performance by reducing
+             * the amount of data being copied.
+             *
+             * @param srcRect The area of the source to read from. Null or empty will be treated to
+             *                mean the entire source
+             * @return this
+             */
+            public @NonNull Builder setSourceRect(@Nullable Rect srcRect) {
+                requireNotBuilt();
+                mRequest.mSrcRect = srcRect;
+                return this;
+            }
+
+            /**
+             * Specifies the output bitmap in which to store the result. By default, a Bitmap of
+             * format {@link android.graphics.Bitmap.Config#ARGB_8888} with a width & height
+             * matching that of the {@link #setSourceRect(Rect) source area} will be created to
+             * place the result.
+             *
+             * @param destination The bitmap to store the result, or null to have a bitmap
+             *                    automatically created of the appropriate size. If not null, must
+             *                    not be {@link Bitmap#isRecycled() recycled} and must be
+             *                    {@link Bitmap#isMutable() mutable}.
+             * @return this
+             */
+            public @NonNull Builder setDestinationBitmap(@Nullable Bitmap destination) {
+                requireNotBuilt();
+                if (destination != null) {
+                    validateBitmapDest(destination);
+                }
+                mRequest.mDest = destination;
+                return this;
+            }
+
+            /**
+             * @return The built {@link PixelCopy.Request}
+             */
+            public @NonNull Request build() {
+                requireNotBuilt();
+                Request ret = mRequest;
+                mRequest = null;
+                return ret;
+            }
         }
 
         /**
-         * Executes the request.
+         * Creates a PixelCopy request for the given {@link Window}
+         * @param source The Window to copy from
+         * @param callbackExecutor The executor to run the callback on
+         * @param listener The callback for when the copy request is completed
+         * @return A {@link Builder} builder to set the optional params & execute the request
+         */
+        public static @NonNull Builder ofWindow(@NonNull Window source,
+                                                @NonNull Executor callbackExecutor,
+                                                @NonNull Consumer<Result> listener) {
+            final Rect insets = new Rect();
+            final Surface surface = sourceForWindow(source, insets);
+            return new Builder(new Request(surface, insets, callbackExecutor, listener));
+        }
+
+        /**
+         * Creates a PixelCopy request for the {@link Window} that the given {@link View} is
+         * attached to.
+         *
+         * Note that this copy request is not cropped to the area the View occupies by default. If
+         * that behavior is desired, use {@link View#getLocationInWindow(int[])} combined with
+         * {@link Builder#setSourceRect(Rect)} to set a crop area to restrict the copy operation.
+         *
+         * @param source A View that {@link View#isAttachedToWindow() is attached} to a window that
+         *               will be used to retrieve the window to copy from.
+         * @param callbackExecutor The executor to run the callback on
+         * @param listener The callback for when the copy request is completed
+         * @return A {@link Builder} builder to set the optional params & execute the request
+         */
+        public static @NonNull Builder ofWindow(@NonNull View source,
+                                                @NonNull Executor callbackExecutor,
+                                                @NonNull Consumer<Result> listener) {
+            if (source == null || !source.isAttachedToWindow()) {
+                throw new IllegalArgumentException(
+                        "View must not be null & must be attached to window");
+            }
+            final Rect insets = new Rect();
+            Surface surface = null;
+            final ViewRootImpl root = source.getViewRootImpl();
+            if (root != null) {
+                surface = root.mSurface;
+                insets.set(root.mWindowAttributes.surfaceInsets);
+            }
+            if (surface == null || !surface.isValid()) {
+                throw new IllegalArgumentException(
+                        "Window doesn't have a backing surface!");
+            }
+            return new Builder(new Request(surface, insets, callbackExecutor, listener));
+        }
+
+        /**
+         * Creates a PixelCopy request for the given {@link Surface}
+         *
+         * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}.
+         * @param callbackExecutor The executor to run the callback on
+         * @param listener The callback for when the copy request is completed
+         * @return A {@link Builder} builder to set the optional params & execute the request
+         */
+        public static @NonNull Builder ofSurface(@NonNull Surface source,
+                                                 @NonNull Executor callbackExecutor,
+                                                 @NonNull Consumer<Result> listener) {
+            if (source == null || !source.isValid()) {
+                throw new IllegalArgumentException("Source must not be null & must be valid");
+            }
+            return new Builder(new Request(source, null, callbackExecutor, listener));
+        }
+
+        /**
+         * Creates a PixelCopy request for the {@link Surface} belonging to the
+         * given {@link SurfaceView}
+         *
+         * @param source The SurfaceView to copy from. The backing surface must be
+         *               {@link Surface#isValid() valid}
+         * @param callbackExecutor The executor to run the callback on
+         * @param listener The callback for when the copy request is completed
+         * @return A {@link Builder} builder to set the optional params & execute the request
+         */
+        public static @NonNull Builder ofSurface(@NonNull SurfaceView source,
+                                                 @NonNull Executor callbackExecutor,
+                                                 @NonNull Consumer<Result> listener) {
+            return ofSurface(source.getHolder().getSurface(), callbackExecutor, listener);
+        }
+
+        /**
+         * @return The destination bitmap as set by {@link Builder#setDestinationBitmap(Bitmap)}
+         */
+        public @Nullable Bitmap getDestinationBitmap() {
+            return mDest;
+        }
+
+        /**
+         * @return The source rect to copy from as set by {@link Builder#setSourceRect(Rect)}
+         */
+        public @Nullable Rect getSourceRect() {
+            return mSrcRect;
+        }
+
+        /**
+         * @hide
          */
         public void request() {
             if (!mSource.isValid()) {
                 mListenerThread.execute(() -> mListener.accept(
-                        new CopyResult(ERROR_SOURCE_INVALID, null)));
+                        new Result(ERROR_SOURCE_INVALID, null)));
                 return;
             }
             HardwareRenderer.copySurfaceInto(mSource, new HardwareRenderer.CopyRequest(
@@ -416,93 +549,18 @@
                 @Override
                 public void onCopyFinished(int result) {
                     mListenerThread.execute(() -> mListener.accept(
-                            new CopyResult(result, mDestinationBitmap)));
+                            new Result(result, mDestinationBitmap)));
                 }
             });
         }
     }
 
     /**
-     * Creates a PixelCopy request for the given {@link Window}
-     * @param source The Window to copy from
-     * @param callbackExecutor The executor to run the callback on
-     * @param listener The callback for when the copy request is completed
-     * @return A {@link Request} builder to set the optional params & execute the request
+     * Executes the pixel copy request
+     * @param request The request to execute
      */
-    public static @NonNull Request ofWindow(@NonNull Window source,
-                                            @NonNull Executor callbackExecutor,
-                                            @NonNull Consumer<CopyResult> listener) {
-        final Rect insets = new Rect();
-        final Surface surface = sourceForWindow(source, insets);
-        return new Request(surface, insets, callbackExecutor, listener);
-    }
-
-    /**
-     * Creates a PixelCopy request for the {@link Window} that the given {@link View} is
-     * attached to.
-     *
-     * Note that this copy request is not cropped to the area the View occupies by default. If that
-     * behavior is desired, use {@link View#getLocationInWindow(int[])} combined with
-     * {@link Request#setSourceRect(Rect)} to set a crop area to restrict the copy operation.
-     *
-     * @param source A View that {@link View#isAttachedToWindow() is attached} to a window that
-     *               will be used to retrieve the window to copy from.
-     * @param callbackExecutor The executor to run the callback on
-     * @param listener The callback for when the copy request is completed
-     * @return A {@link Request} builder to set the optional params & execute the request
-     */
-    public static @NonNull Request ofWindow(@NonNull View source,
-                                            @NonNull Executor callbackExecutor,
-                                            @NonNull Consumer<CopyResult> listener) {
-        if (source == null || !source.isAttachedToWindow()) {
-            throw new IllegalArgumentException(
-                    "View must not be null & must be attached to window");
-        }
-        final Rect insets = new Rect();
-        Surface surface = null;
-        final ViewRootImpl root = source.getViewRootImpl();
-        if (root != null) {
-            surface = root.mSurface;
-            insets.set(root.mWindowAttributes.surfaceInsets);
-        }
-        if (surface == null || !surface.isValid()) {
-            throw new IllegalArgumentException(
-                    "Window doesn't have a backing surface!");
-        }
-        return new Request(surface, insets, callbackExecutor, listener);
-    }
-
-    /**
-     * Creates a PixelCopy request for the given {@link Surface}
-     *
-     * @param source The Surface to copy from. Must be {@link Surface#isValid() valid}.
-     * @param callbackExecutor The executor to run the callback on
-     * @param listener The callback for when the copy request is completed
-     * @return A {@link Request} builder to set the optional params & execute the request
-     */
-    public static @NonNull Request ofSurface(@NonNull Surface source,
-                                             @NonNull Executor callbackExecutor,
-                                             @NonNull Consumer<CopyResult> listener) {
-        if (source == null || !source.isValid()) {
-            throw new IllegalArgumentException("Source must not be null & must be valid");
-        }
-        return new Request(source, null, callbackExecutor, listener);
-    }
-
-    /**
-     * Creates a PixelCopy request for the {@link Surface} belonging to the
-     * given {@link SurfaceView}
-     *
-     * @param source The SurfaceView to copy from. The backing surface must be
-     *               {@link Surface#isValid() valid}
-     * @param callbackExecutor The executor to run the callback on
-     * @param listener The callback for when the copy request is completed
-     * @return A {@link Request} builder to set the optional params & execute the request
-     */
-    public static @NonNull Request ofSurface(@NonNull SurfaceView source,
-                                             @NonNull Executor callbackExecutor,
-                                             @NonNull Consumer<CopyResult> listener) {
-        return ofSurface(source.getHolder().getSurface(), callbackExecutor, listener);
+    public static void request(@NonNull Request request) {
+        request.request();
     }
 
     private PixelCopy() {}
diff --git a/keystore/java/android/security/keystore/KeyProperties.java b/keystore/java/android/security/keystore/KeyProperties.java
index dbd918e..6245598 100644
--- a/keystore/java/android/security/keystore/KeyProperties.java
+++ b/keystore/java/android/security/keystore/KeyProperties.java
@@ -30,6 +30,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.ECParameterSpec;
 import java.security.spec.MGF1ParameterSpec;
 import java.util.Collection;
 import java.util.Locale;
@@ -914,6 +915,51 @@
     }
 
     /**
+     * @hide
+     */
+    public abstract static class EcCurve {
+        private EcCurve() {}
+
+        /**
+         * @hide
+         */
+        public static int toKeymasterCurve(ECParameterSpec spec) {
+            int keySize = spec.getCurve().getField().getFieldSize();
+            switch (keySize) {
+                case 224:
+                    return android.hardware.security.keymint.EcCurve.P_224;
+                case 256:
+                    return android.hardware.security.keymint.EcCurve.P_256;
+                case 384:
+                    return android.hardware.security.keymint.EcCurve.P_384;
+                case 521:
+                    return android.hardware.security.keymint.EcCurve.P_521;
+                default:
+                    return -1;
+            }
+        }
+
+        /**
+         * @hide
+         */
+        public static int fromKeymasterCurve(int ecCurve) {
+            switch (ecCurve) {
+                case android.hardware.security.keymint.EcCurve.P_224:
+                    return 224;
+                case android.hardware.security.keymint.EcCurve.P_256:
+                case android.hardware.security.keymint.EcCurve.CURVE_25519:
+                    return 256;
+                case android.hardware.security.keymint.EcCurve.P_384:
+                    return 384;
+                case android.hardware.security.keymint.EcCurve.P_521:
+                    return 521;
+                default:
+                    return -1;
+            }
+        }
+    }
+
+    /**
      * Namespaces provide system developers and vendors with a way to use keystore without
      * requiring an applications uid. Namespaces can be configured using SEPolicy.
      * See <a href="https://source.android.com/security/keystore#access-control">
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
index 5216a90..ace2053 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
@@ -203,6 +203,11 @@
         for (Authorization a : key.getAuthorizations()) {
             if (a.keyParameter.tag == KeymasterDefs.KM_TAG_KEY_SIZE) {
                 keySizeBits = KeyStore2ParameterUtils.getUnsignedInt(a);
+                break;
+            } else if (a.keyParameter.tag == KeymasterDefs.KM_TAG_EC_CURVE) {
+                keySizeBits = KeyProperties.EcCurve.fromKeymasterCurve(
+                        a.keyParameter.value.getEcCurve());
+                break;
             }
         }
 
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
index 94bf122..7a320ba 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -66,6 +66,7 @@
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.security.interfaces.ECKey;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -566,6 +567,22 @@
                         spec.getMaxUsageCount()
                 ));
             }
+            if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(key.getAlgorithm())) {
+                if (key instanceof ECKey) {
+                    ECKey ecKey = (ECKey) key;
+                    importArgs.add(KeyStore2ParameterUtils.makeEnum(
+                            KeymasterDefs.KM_TAG_EC_CURVE,
+                            KeyProperties.EcCurve.toKeymasterCurve(ecKey.getParams())
+                    ));
+                }
+            }
+            /* TODO: check for Ed25519(EdDSA) or X25519(XDH) key algorithm and
+             *  add import args for KM_TAG_EC_CURVE as EcCurve.CURVE_25519.
+             *  Currently conscrypt does not support EdDSA key import and XDH keys are not an
+             *  instance of XECKey, hence these conditions are not added, once it is fully
+             *  implemented by conscrypt, we can add CURVE_25519 argument for EdDSA and XDH
+             *  algorithms.
+             */
         } catch (IllegalArgumentException | IllegalStateException e) {
             throw new KeyStoreException(e);
         }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 74303e2..9d841ea 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -18,6 +18,12 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
+import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
+import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
+import static androidx.window.extensions.embedding.SplitContainer.shouldFinishPrimaryWithSecondary;
+import static androidx.window.extensions.embedding.SplitContainer.shouldFinishSecondaryWithPrimary;
+
 import android.app.Activity;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.Intent;
@@ -140,6 +146,8 @@
 
         // Set adjacent to each other so that the containers below will be invisible.
         setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+        setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule,
+                false /* isStacked */);
     }
 
     /**
@@ -215,6 +223,28 @@
         wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
     }
 
+    void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule,
+            boolean isStacked) {
+        final boolean finishPrimaryWithSecondary;
+        if (isStacked) {
+            finishPrimaryWithSecondary = shouldFinishAssociatedContainerWhenStacked(
+                    getFinishPrimaryWithSecondaryBehavior(splitRule));
+        } else {
+            finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule);
+        }
+        wct.setCompanionTaskFragment(primary, finishPrimaryWithSecondary ? secondary : null);
+
+        final boolean finishSecondaryWithPrimary;
+        if (isStacked) {
+            finishSecondaryWithPrimary = shouldFinishAssociatedContainerWhenStacked(
+                    getFinishSecondaryWithPrimaryBehavior(splitRule));
+        } else {
+            finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule);
+        }
+        wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null);
+    }
+
     TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken,
             @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
         if (mFragmentInfos.containsKey(fragmentToken)) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 00be5a6e..77284c41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -109,6 +109,12 @@
         return (mSplitRule instanceof SplitPlaceholderRule);
     }
 
+    @NonNull
+    SplitInfo toSplitInfo() {
+        return new SplitInfo(mPrimaryContainer.toActivityStack(),
+                mSecondaryContainer.toActivityStack(), mSplitAttributes);
+    }
+
     static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
         final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
         final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index bf7326a..d52caaf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -389,6 +389,10 @@
                 // launching activity in the Task.
                 mTransactionManager.getCurrentTransactionRecord().setOriginType(TRANSIT_CLOSE);
                 mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+            } else if (taskFragmentInfo.isClearedForReorderActivityToFront()) {
+                // Do not finish the dependents if this TaskFragment was cleared to reorder
+                // the launching Activity to front of the Task.
+                mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
             } else if (!container.isWaitingActivityAppear()) {
                 // Do not finish the container before the expected activity appear until
                 // timeout.
@@ -1422,6 +1426,11 @@
     @GuardedBy("mLock")
     void updateContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
+        if (!container.getTaskContainer().isVisible()) {
+            // Wait until the Task is visible to avoid unnecessary update when the Task is still in
+            // background.
+            return;
+        }
         if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
@@ -1643,16 +1652,14 @@
     /**
      * Notifies listeners about changes to split states if necessary.
      */
+    @VisibleForTesting
     @GuardedBy("mLock")
-    private void updateCallbackIfNecessary() {
-        if (mEmbeddingCallback == null) {
+    void updateCallbackIfNecessary() {
+        if (mEmbeddingCallback == null || !readyToReportToClient()) {
             return;
         }
-        if (!allActivitiesCreated()) {
-            return;
-        }
-        List<SplitInfo> currentSplitStates = getActiveSplitStates();
-        if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
+        final List<SplitInfo> currentSplitStates = getActiveSplitStates();
+        if (mLastReportedSplitStates.equals(currentSplitStates)) {
             return;
         }
         mLastReportedSplitStates.clear();
@@ -1661,48 +1668,27 @@
     }
 
     /**
-     * @return a list of descriptors for currently active split states. If the value returned is
-     * null, that indicates that the active split states are in an intermediate state and should
-     * not be reported.
+     * Returns a list of descriptors for currently active split states.
      */
     @GuardedBy("mLock")
-    @Nullable
+    @NonNull
     private List<SplitInfo> getActiveSplitStates() {
-        List<SplitInfo> splitStates = new ArrayList<>();
+        final List<SplitInfo> splitStates = new ArrayList<>();
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i)
-                    .mSplitContainers;
-            for (SplitContainer container : splitContainers) {
-                if (container.getPrimaryContainer().isEmpty()
-                        || container.getSecondaryContainer().isEmpty()) {
-                    // We are in an intermediate state because either the split container is about
-                    // to be removed or the primary or secondary container are about to receive an
-                    // activity.
-                    return null;
-                }
-                final ActivityStack primaryContainer = container.getPrimaryContainer()
-                        .toActivityStack();
-                final ActivityStack secondaryContainer = container.getSecondaryContainer()
-                        .toActivityStack();
-                final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer,
-                        container.getSplitAttributes());
-                splitStates.add(splitState);
-            }
+            mTaskContainers.valueAt(i).getSplitStates(splitStates);
         }
         return splitStates;
     }
 
     /**
-     * Checks if all activities that are registered with the containers have already appeared in
-     * the client.
+     * Whether we can now report the split states to the client.
      */
-    private boolean allActivitiesCreated() {
+    @GuardedBy("mLock")
+    private boolean readyToReportToClient() {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
-            for (TaskFragmentContainer container : containers) {
-                if (!container.taskInfoActivityCountMatchesCreated()) {
-                    return false;
-                }
+            if (mTaskContainers.valueAt(i).isInIntermediateState()) {
+                // If any Task is in an intermediate state, wait for the server update.
+                return false;
             }
         }
         return true;
@@ -1891,6 +1877,11 @@
         @Override
         public void onActivityPreCreated(@NonNull Activity activity,
                 @Nullable Bundle savedInstanceState) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 final IBinder activityToken = activity.getActivityToken();
                 final IBinder initialTaskFragmentToken =
@@ -1922,6 +1913,11 @@
         @Override
         public void onActivityPostCreated(@NonNull Activity activity,
                 @Nullable Bundle savedInstanceState) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             // Calling after Activity#onCreate is complete to allow the app launch something
             // first. In case of a configured placeholder activity we want to make sure
             // that we don't launch it if an activity itself already requested something to be
@@ -1939,6 +1935,11 @@
 
         @Override
         public void onActivityConfigurationChanged(@NonNull Activity activity) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 final TransactionRecord transactionRecord = mTransactionManager
                         .startNewTransaction();
@@ -1952,6 +1953,11 @@
 
         @Override
         public void onActivityPostDestroyed(@NonNull Activity activity) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 SplitController.this.onActivityDestroyed(activity);
             }
@@ -1987,7 +1993,11 @@
             if (who instanceof Activity) {
                 // We will check if the new activity should be split with the activity that launched
                 // it.
-                launchingActivity = (Activity) who;
+                final Activity activity = (Activity) who;
+                // For Activity that is child of another Activity (ActivityGroup), treat the parent
+                // Activity as the launching one because it's window will just be a child of the
+                // parent Activity window.
+                launchingActivity = activity.isChild() ? activity.getParent() : activity;
                 if (isInPictureInPicture(launchingActivity)) {
                     // We don't embed activity when it is in PIP.
                     return super.onStartActivity(who, intent, options);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 7960323..cb470ba 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -227,7 +227,7 @@
         final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
                 secondaryActivity);
         TaskFragmentContainer containerToAvoid = primaryContainer;
-        if (curSecondaryContainer != null
+        if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer
                 && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) {
             // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below
             // the primary TaskFragment.
@@ -371,13 +371,16 @@
             @NonNull SplitAttributes splitAttributes) {
         // Clear adjacent TaskFragments if the container is shown in fullscreen, or the
         // secondaryContainer could not be finished.
-        if (!shouldShowSplit(splitAttributes)) {
+        boolean isStacked = !shouldShowSplit(splitAttributes);
+        if (isStacked) {
             setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
                     null /* secondary */, null /* splitRule */);
         } else {
             setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
                     secondaryContainer.getTaskFragmentToken(), splitRule);
         }
+        setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
+                secondaryContainer.getTaskFragmentToken(), splitRule, isStacked);
     }
 
     /**
@@ -489,8 +492,15 @@
                     || splitContainer.getSecondaryContainer().getInfo() == null) {
                 return RESULT_EXPAND_FAILED_NO_TF_INFO;
             }
-            expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken());
-            expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken());
+            final IBinder primaryToken =
+                    splitContainer.getPrimaryContainer().getTaskFragmentToken();
+            final IBinder secondaryToken =
+                    splitContainer.getSecondaryContainer().getTaskFragmentToken();
+            expandTaskFragment(wct, primaryToken);
+            expandTaskFragment(wct, secondaryToken);
+            // Set the companion TaskFragment when the two containers stacked.
+            setCompanionTaskFragment(wct, primaryToken, secondaryToken,
+                    splitContainer.getSplitRule(), true /* isStacked */);
             return RESULT_EXPANDED;
         }
         return RESULT_NOT_EXPANDED;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 00943f2d..231da05 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -221,6 +221,24 @@
         return mContainers.indexOf(child);
     }
 
+    /** Whether the Task is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.isInIntermediateState()) {
+                // We are in an intermediate state to wait for server update on this TaskFragment.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
+    void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
+        for (SplitContainer container : mSplitContainers) {
+            outSplitStates.add(container.toSplitInfo());
+        }
+    }
+
     /**
      * A wrapper class which contains the display ID and {@link Configuration} of a
      * {@link TaskContainer}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
index ef5ea56..a7d47ef 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -161,7 +161,7 @@
         // The position should be 0-based as we will post translate in
         // TaskFragmentAnimationAdapter#onAnimationUpdate
         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
-                0, 0);
+                startBounds.top - endBounds.top, 0);
         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
         endSet.addAnimation(endTranslate);
         // The end leash is resizing, we should update the window crop based on the clip rect.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 18712ae..71b8840 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -166,16 +166,34 @@
         return allActivities;
     }
 
-    /**
-     * Checks if the count of activities from the same process in task fragment info corresponds to
-     * the ones created and available on the client side.
-     */
-    boolean taskInfoActivityCountMatchesCreated() {
+    /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
         if (mInfo == null) {
-            return false;
+            // Haven't received onTaskFragmentAppeared event.
+            return true;
         }
-        return mPendingAppearedActivities.isEmpty()
-                && mInfo.getActivities().size() == collectNonFinishingActivities().size();
+        if (mInfo.isEmpty()) {
+            // Empty TaskFragment will be removed or will have activity launched into it soon.
+            return true;
+        }
+        if (!mPendingAppearedActivities.isEmpty()) {
+            // Reparented activity hasn't appeared.
+            return true;
+        }
+        // Check if there is any reported activity that is no longer alive.
+        for (IBinder token : mInfo.getActivities()) {
+            final Activity activity = mController.getActivity(token);
+            if (activity == null && !mTaskContainer.isVisible()) {
+                // Activity can be null if the activity is not attached to process yet. That can
+                // happen when the activity is started in background.
+                continue;
+            }
+            if (activity == null || activity.isFinishing()) {
+                // One of the reported activity is no longer alive, wait for the server update.
+                return true;
+            }
+        }
+        return false;
     }
 
     @NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index b516e140..2192b5c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -36,6 +36,7 @@
 import android.os.IBinder;
 import android.util.ArrayMap;
 import android.window.WindowContext;
+import android.window.WindowProvider;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -71,7 +72,7 @@
 
     private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>();
 
-    private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners =
+    private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners =
             new ArrayMap<>();
 
     public WindowLayoutComponentImpl(@NonNull Context context) {
@@ -121,21 +122,21 @@
         }
         if (!context.isUiContext()) {
             throw new IllegalArgumentException("Context must be a UI Context, which should be"
-                    + " an Activity or a WindowContext");
+                    + " an Activity, WindowContext or InputMethodService");
         }
         mFoldingFeatureProducer.getData((features) -> {
-            // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
             WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
             consumer.accept(newWindowLayout);
         });
         mWindowLayoutChangeListeners.put(context, consumer);
 
-        if (context instanceof WindowContext) {
+        // TODO(b/258065175) Further extend this to ContextWrappers.
+        if (context instanceof WindowProvider) {
             final IBinder windowContextToken = context.getWindowContextToken();
-            final WindowContextConfigListener listener =
-                    new WindowContextConfigListener(windowContextToken);
+            final ConfigurationChangeListener listener =
+                    new ConfigurationChangeListener(windowContextToken);
             context.registerComponentCallbacks(listener);
-            mWindowContextConfigListeners.put(windowContextToken, listener);
+            mConfigurationChangeListeners.put(windowContextToken, listener);
         }
     }
 
@@ -150,10 +151,10 @@
             if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) {
                 continue;
             }
-            if (context instanceof WindowContext) {
+            if (context instanceof WindowProvider) {
                 final IBinder token = context.getWindowContextToken();
-                context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token));
-                mWindowContextConfigListeners.remove(token);
+                context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token));
+                mConfigurationChangeListeners.remove(token);
             }
             break;
         }
@@ -349,10 +350,10 @@
         }
     }
 
-    private final class WindowContextConfigListener implements ComponentCallbacks {
+    private final class ConfigurationChangeListener implements ComponentCallbacks {
         final IBinder mToken;
 
-        WindowContextConfigListener(IBinder token) {
+        ConfigurationChangeListener(IBinder token) {
             mToken = token;
         }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 40f7a27..92011af 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -169,6 +169,7 @@
                 new Point(),
                 false /* isTaskClearedForReuse */,
                 false /* isTaskFragmentClearedForPip */,
+                false /* isClearedForReorderActivityToFront */,
                 new Point());
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 957a248..79813c7 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -144,6 +144,6 @@
                 mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,
                 false /* isVisible */, new ArrayList<>(), new Point(),
                 false /* isTaskClearedForReuse */, false /* isTaskFragmentClearedForPip */,
-                new Point());
+                false /* isClearedForReorderActivityToFront */, new Point());
     }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index a403031..87d0278 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -102,6 +102,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Test class for {@link SplitController}.
@@ -132,6 +133,8 @@
 
     private SplitController mSplitController;
     private SplitPresenter mSplitPresenter;
+    private Consumer<List<SplitInfo>> mEmbeddingCallback;
+    private List<SplitInfo> mSplitInfos;
     private TransactionManager mTransactionManager;
 
     @Before
@@ -141,9 +144,16 @@
                 .getCurrentWindowLayoutInfo(anyInt(), any());
         mSplitController = new SplitController(mWindowLayoutComponent);
         mSplitPresenter = mSplitController.mPresenter;
+        mSplitInfos = new ArrayList<>();
+        mEmbeddingCallback = splitInfos -> {
+            mSplitInfos.clear();
+            mSplitInfos.addAll(splitInfos);
+        };
+        mSplitController.setSplitInfoCallback(mEmbeddingCallback);
         mTransactionManager = mSplitController.mTransactionManager;
         spyOn(mSplitController);
         spyOn(mSplitPresenter);
+        spyOn(mEmbeddingCallback);
         spyOn(mTransactionManager);
         doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
         final Configuration activityConfig = new Configuration();
@@ -329,6 +339,30 @@
     }
 
     @Test
+    public void testUpdateContainer_skipIfTaskIsInvisible() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+        final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+        spyOn(taskContainer);
+
+        // No update when the Task is invisible.
+        clearInvocations(mSplitPresenter);
+        doReturn(false).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+
+        // Update the split when the Task is visible.
+        doReturn(true).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+                taskFragmentContainer, mTransaction);
+    }
+
+    @Test
     public void testOnStartActivityResultError() {
         final Intent intent = new Intent();
         final TaskContainer taskContainer = createTestTaskContainer();
@@ -1162,14 +1196,69 @@
                         new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
     }
 
+    @Test
+    public void testSplitInfoCallback_reportSplit() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(1, mSplitInfos.size());
+        final SplitInfo splitInfo = mSplitInfos.get(0);
+        assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size());
+        assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size());
+        assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0));
+        assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0));
+    }
+
+    @Test
+    public void testSplitInfoCallback_reportSplitInMultipleTasks() {
+        final int taskId0 = 1;
+        final int taskId1 = 2;
+        final Activity r0 = createMockActivity(taskId0);
+        final Activity r1 = createMockActivity(taskId0);
+        final Activity r2 = createMockActivity(taskId1);
+        final Activity r3 = createMockActivity(taskId1);
+        addSplitTaskFragments(r0, r1);
+        addSplitTaskFragments(r2, r3);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(2, mSplitInfos.size());
+    }
+
+    @Test
+    public void testSplitInfoCallback_doNotReportIfInIntermediateState() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0);
+        final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1);
+        spyOn(tf0);
+        spyOn(tf1);
+
+        // Do not report if activity has not appeared in the TaskFragmentContainer in split.
+        doReturn(true).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback, never()).accept(any());
+
+        doReturn(false).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback).accept(any());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
+        return createMockActivity(TASK_ID);
+    }
+
+    /** Creates a mock activity in the organizer process. */
+    private Activity createMockActivity(int taskId) {
         final Activity activity = mock(Activity.class);
         doReturn(mActivityResources).when(activity).getResources();
         final IBinder activityToken = new Binder();
         doReturn(activityToken).when(activity).getActivityToken();
         doReturn(activity).when(mSplitController).getActivity(activityToken);
-        doReturn(TASK_ID).when(activity).getTaskId();
+        doReturn(taskId).when(activity).getTaskId();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
         doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
         return activity;
@@ -1177,7 +1266,8 @@
 
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
     private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
-        final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
+        final TaskFragmentContainer container = mSplitController.newContainer(activity,
+                activity.getTaskId());
         setupTaskFragmentInfo(container, activity);
         return container;
     }
@@ -1268,7 +1358,7 @@
 
         // We need to set those in case we are not respecting clear top.
         // TODO(b/231845476) we should always respect clearTop.
-        final int windowingMode = mSplitController.getTaskContainer(TASK_ID)
+        final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId())
                 .getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
         primaryContainer.setLastRequestedWindowingMode(windowingMode);
         secondaryContainer.setLastRequestedWindowingMode(windowingMode);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 35415d8..d43c471 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -334,6 +334,70 @@
         assertFalse(container.hasActivity(mActivity.getActivityToken()));
     }
 
+    @Test
+    public void testIsInIntermediateState() {
+        // True if no info set.
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                mIntent, taskContainer, mController);
+        spyOn(taskContainer);
+        doReturn(true).when(taskContainer).isVisible();
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if empty info set.
+        final List<IBinder> activities = new ArrayList<>();
+        doReturn(activities).when(mInfo).getActivities();
+        doReturn(true).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if info is not empty.
+        doReturn(false).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is pending appeared activity.
+        container.addPendingAppearedActivity(mActivity);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if the activity is finishing.
+        activities.add(mActivity.getActivityToken());
+        doReturn(true).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if the activity is not finishing.
+        doReturn(false).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is a token that can't find associated activity.
+        activities.clear();
+        activities.add(new Binder());
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if there is a token that can't find associated activity when the Task is invisible.
+        doReturn(false).when(taskContainer).isVisible();
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
new file mode 100644
index 0000000..8779cc0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateX="6.0"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
new file mode 100644
index 0000000..ea0fbb0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateX="6.0"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M18.167,21.875H29.833V10.208H18.167ZM7.875,35.833Q6.375,35.833 5.271,34.729Q4.167,33.625 4.167,32.125V7.875Q4.167,6.375 5.271,5.271Q6.375,4.167 7.875,4.167H32.125Q33.625,4.167 34.729,5.271Q35.833,6.375 35.833,7.875V32.125Q35.833,33.625 34.729,34.729Q33.625,35.833 32.125,35.833ZM7.875,32.125H32.125Q32.125,32.125 32.125,32.125Q32.125,32.125 32.125,32.125V7.875Q32.125,7.875 32.125,7.875Q32.125,7.875 32.125,7.875H7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125ZM7.875,7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125V7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
new file mode 100644
index 0000000..c55cbe2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateX="6.0"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M34.042,14.625V9.333Q34.042,9.333 34.042,9.333Q34.042,9.333 34.042,9.333H28.708V5.708H33.917Q35.458,5.708 36.562,6.833Q37.667,7.958 37.667,9.458V14.625ZM2.375,14.625V9.458Q2.375,7.958 3.479,6.833Q4.583,5.708 6.125,5.708H11.292V9.333H6Q6,9.333 6,9.333Q6,9.333 6,9.333V14.625ZM28.708,34.25V30.667H34.042Q34.042,30.667 34.042,30.667Q34.042,30.667 34.042,30.667V25.333H37.667V30.542Q37.667,32 36.562,33.125Q35.458,34.25 33.917,34.25ZM6.125,34.25Q4.583,34.25 3.479,33.125Q2.375,32 2.375,30.542V25.333H6V30.667Q6,30.667 6,30.667Q6,30.667 6,30.667H11.292V34.25ZM9.333,27.292V12.667H30.708V27.292ZM12.917,23.708H27.125V16.25H12.917ZM12.917,23.708V16.25V23.708Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
new file mode 100644
index 0000000..447df43
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateX="6.0"
+           android:translateY="6.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M8.083,22.833Q6.917,22.833 6.104,22Q5.292,21.167 5.292,20Q5.292,18.833 6.125,18Q6.958,17.167 8.125,17.167Q9.292,17.167 10.125,18Q10.958,18.833 10.958,20Q10.958,21.167 10.125,22Q9.292,22.833 8.083,22.833ZM20,22.833Q18.833,22.833 18,22Q17.167,21.167 17.167,20Q17.167,18.833 18,18Q18.833,17.167 20,17.167Q21.167,17.167 22,18Q22.833,18.833 22.833,20Q22.833,21.167 22,22Q21.167,22.833 20,22.833ZM31.875,22.833Q30.708,22.833 29.875,22Q29.042,21.167 29.042,20Q29.042,18.833 29.875,18Q30.708,17.167 31.917,17.167Q33.083,17.167 33.896,18Q34.708,18.833 34.708,20Q34.708,21.167 33.875,22Q33.042,22.833 31.875,22.833Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
new file mode 100644
index 0000000..c334a54
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="32.0dp"
+        android:height="32.0dp"
+        android:viewportWidth="32.0"
+        android:viewportHeight="32.0"
+>
+    <group android:translateX="6.0"
+           android:translateY="8.0">
+        <path
+            android:fillColor="@android:color/black"
+            android:pathData="M18 14L13 14L13 2L18 2L18 14ZM20 14L20 2C20 0.9 19.1 -3.93402e-08 18 -8.74228e-08L13 -3.0598e-07C11.9 -3.54062e-07 11 0.9 11 2L11 14C11 15.1 11.9 16 13 16L18 16C19.1 16 20 15.1 20 14ZM7 14L2 14L2 2L7 2L7 14ZM9 14L9 2C9 0.9 8.1 -5.20166e-07 7 -5.68248e-07L2 -7.86805e-07C0.9 -8.34888e-07 -3.93403e-08 0.9 -8.74228e-08 2L-6.11959e-07 14C-6.60042e-07 15.1 0.9 16 2 16L7 16C8.1 16 9 15.1 9 14Z"/>    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml
new file mode 100644
index 0000000..e307f00
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="210.0dp"
+        android:height="64.0dp"
+        android:tint="@color/decor_button_light_color"
+>
+    <group android:scaleX="0.5"
+           android:scaleY="0.5"
+           android:translateX="8.0"
+           android:translateY="8.0" >
+        <path
+            android:fillColor="@android:color/white"
+            android:pathData="M18.3334 14L13.3334 14L13.3334 2L18.3334 2L18.3334 14ZM20.3334 14L20.3334 2C20.3334 0.9 19.4334 -3.93402e-08 18.3334 -8.74228e-08L13.3334 -3.0598e-07C12.2334 -3.54062e-07 11.3334 0.9 11.3334 2L11.3334 14C11.3334 15.1 12.2334 16 13.3334 16L18.3334 16C19.4334 16 20.3334 15.1 20.3334 14ZM7.33337 14L2.33337 14L2.33337 2L7.33337 2L7.33337 14ZM9.33337 14L9.33337 2C9.33337 0.899999 8.43337 -5.20166e-07 7.33337 -5.68248e-07L2.33337 -7.86805e-07C1.23337 -8.34888e-07 0.333374 0.899999 0.333374 2L0.333373 14C0.333373 15.1 1.23337 16 2.33337 16L7.33337 16C8.43337 16 9.33337 15.1 9.33337 14Z"/>
+    </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
new file mode 100644
index 0000000..d9a140b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
@@ -0,0 +1,49 @@
+<?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.
+      -->
+<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
+xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/handle_menu"
+android:layout_width="wrap_content"
+android:layout_height="wrap_content"
+android:gravity="center_horizontal"
+android:background="@drawable/decor_caption_title">
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/fullscreen_button"
+        android:contentDescription="@string/fullscreen_text"
+        android:background="@drawable/caption_fullscreen_button"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/split_screen_button"
+        android:contentDescription="@string/split_screen_text"
+        android:background="@drawable/caption_split_screen_button"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/floating_button"
+        android:contentDescription="@string/float_button_text"
+        android:background="@drawable/caption_floating_button"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/desktop_button"
+        android:contentDescription="@string/desktop_text"
+        android:background="@drawable/caption_desktop_button"/>
+    <Button
+        style="@style/CaptionButtonStyle"
+        android:id="@+id/more_button"
+        android:contentDescription="@string/more_button_text"
+        android:background="@drawable/caption_more_button"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
index 38cd570..51e634c 100644
--- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
+++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
@@ -19,14 +19,10 @@
     android:id="@+id/caption"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:gravity="center_horizontal"
     android:background="@drawable/decor_caption_title">
     <Button
+        style="@style/CaptionButtonStyle"
         android:id="@+id/back_button"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:layout_margin="5dp"
-        android:padding="4dp"
         android:contentDescription="@string/back_button_text"
         android:background="@drawable/decor_back_button_dark"
     />
@@ -39,11 +35,8 @@
         android:contentDescription="@string/handle_text"
         android:background="@drawable/decor_handle_dark"/>
     <Button
+        style="@style/CaptionButtonStyle"
         android:id="@+id/close_window"
-        android:layout_width="32dp"
-        android:layout_height="32dp"
-        android:layout_margin="5dp"
-        android:padding="4dp"
         android:contentDescription="@string/close_button_text"
         android:background="@drawable/decor_close_button_dark"/>
 </com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index fc0c20e..42da075 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Maak toe"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Terug"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handvatsel"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Volskerm"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Rekenaarmodus"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Verdeelde skerm"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Meer"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Sweef"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index 57a7ad0..97ae8b3 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml
index 23f1c6f..83710f5 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string>
     <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string>
+    <string name="handle_text" msgid="1766582106752184456">"مقبض"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml
index 57a763e..349f3502 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"সৰ্বাধিক মাত্ৰালৈ বঢ়াওক"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string>
     <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string>
+    <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml
index 610ee10..ad7ad9a 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Böyüdün"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
index 1e78b3c..d3e010b 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Zatvorite"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string>
     <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index 1a24478..e57d2c6 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml
index 1269c37..14195ef 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml
index 31a11cd..52cde90 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"বন্ধ করুন"</string>
     <string name="back_button_text" msgid="1469718707134137085">"ফিরে যান"</string>
     <string name="handle_text" msgid="1766582106752184456">"হাতল"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index 71c805f..4342700 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -86,6 +86,11 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziranje"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string>
-    <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string>
-    <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string>
+    <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Puni zaslon"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Stolni način rada"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Razdvojeni zaslon"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Više"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Plutajući"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml
index 564d448..cd6b00d 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximitza"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Tanca"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ansa"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml
index 555c252..99ccf07f 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Zavřít"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Zpět"</string>
     <string name="handle_text" msgid="1766582106752184456">"Úchyt"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index 4729c23..ccd32e2 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimér"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Luk"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml
index 969eef8..f261b02 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximieren"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Schließen"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml
index 79e2dab..f35a2a0 100644
--- a/libs/WindowManager/Shell/res/values-el/strings.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Κλείσιμο"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Πίσω"</string>
     <string name="handle_text" msgid="1766582106752184456">"Λαβή"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
index 6db010a..231c2649 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Close"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"More"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Float"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
index 6db010a..231c2649 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Close"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"More"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Float"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 6db010a..231c2649 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Close"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"More"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Float"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
index 6db010a..231c2649 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Close"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Back"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Full screen"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Desktop mode"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Split screen"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"More"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Float"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
index 37b4fc7..f3e60d2 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‎‏‏‏‎‎‎‎‎‏‏‏‎‏‎‎‎‏‎‏‎‎‏‎‎‎‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‎‎‏‏‎Close‎‏‎‎‏‎"</string>
     <string name="back_button_text" msgid="1469718707134137085">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‎‏‏‎‎‏‎‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‎‎‏‎‎‏‎‎‏‎‎‏‏‏‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‏‏‏‎‏‎Back‎‏‎‎‏‎"</string>
     <string name="handle_text" msgid="1766582106752184456">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‎‎‏‎‎‎‎Handle‎‏‎‎‏‎"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎‏‎‏‏‎‎‎‎‎‏‏‎‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‏‎Fullscreen‎‏‎‎‏‎"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‏‎‏‎‎‏‎‎‎‎‏‏‎‎‎‎‎‎‏‎‏‎‎‎‎‏‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‎‏‏‏‎‏‏‎‎Desktop Mode‎‏‎‎‏‎"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‎‎‏‎‎‎‎‎‏‏‏‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎‏‎‎‎‏‏‎‎Split Screen‎‏‎‎‏‎"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎‏‏‏‎‏‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‏‎‏‏‎‏‏‏‎‎‎‎‎‏‏‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‎‏‎‎More‎‏‎‎‏‎"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‎‎‎‎‎‏‏‎‎‎‎‏‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎Float‎‏‎‎‏‎"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index 7965358..fe29baa 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -86,8 +86,11 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Pantalla completa"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Modo de escritorio"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Pantalla dividida"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Más"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Flotante"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index d39fd41..7788f47 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml
index cb26c0a..d10fd23 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeeri"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sule"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Käepide"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index 6bc1d91..fbd0fc0 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Itxi"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Atzera"</string>
     <string name="handle_text" msgid="1766582106752184456">"Kontu-izena"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 1dd88d9..45a56f6 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"بزرگ کردن"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string>
     <string name="close_button_text" msgid="2913281996024033299">"بستن"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string>
+    <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml
index b6224ef..15b8bf0 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Suurenna"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sulje"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Kahva"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
index ff8417b..a56232e 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Retour"</string>
     <string name="handle_text" msgid="1766582106752184456">"Identifiant"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 4f992f5..4256d27 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Retour"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Poignée"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml
index b349302..5276979 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Pechar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml
index 5207e19..c663489 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"બંધ કરો"</string>
     <string name="back_button_text" msgid="1469718707134137085">"પાછળ"</string>
     <string name="handle_text" msgid="1766582106752184456">"હૅન્ડલ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index c2732ec..0896907 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string>
+    <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml
index 08aa262..8d20c9d1 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Zatvori"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string>
     <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Puni zaslon"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Stolni način rada"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Razdvojeni zaslon"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Više"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Plutajući"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml
index 8ad0a01..27483e5 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Bezárás"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Vissza"</string>
     <string name="handle_text" msgid="1766582106752184456">"Fogópont"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index ca98d6b..fd799da 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml
index b3bbba1..fdc30ae 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimalkan"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Tuas"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml
index 456f152..d38c90b 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Stækka"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Loka"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Handfang"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index 9a023f5..455050c 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Chiudi"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Indietro"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index 2f8b774..396474c 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string>
     <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string>
+    <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml
index d0b5462..8bc3cd2 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"閉じる"</string>
     <string name="back_button_text" msgid="1469718707134137085">"戻る"</string>
     <string name="handle_text" msgid="1766582106752184456">"ハンドル"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml
index e15b376..05161b1 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"მაქსიმალურად გაშლა"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string>
     <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"უკან"</string>
+    <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml
index a8fd31d..7ad3aa8 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml
index bdfd775..f85fd83 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ពង្រីក"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string>
     <string name="close_button_text" msgid="2913281996024033299">"បិទ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml
index acad7c1..6475cac 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"ಮುಚ್ಚಿರಿ"</string>
     <string name="back_button_text" msgid="1469718707134137085">"ಹಿಂದಕ್ಕೆ"</string>
     <string name="handle_text" msgid="1766582106752184456">"ಹ್ಯಾಂಡಲ್"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml
index bb52084..6837ed3 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string>
     <string name="close_button_text" msgid="2913281996024033299">"닫기"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string>
+    <string name="handle_text" msgid="1766582106752184456">"핸들"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index 9ad82de..2ac4a67 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Жабуу"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Артка"</string>
     <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml
index d5e3d84..e0a92b8 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -86,8 +86,11 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"ເຕັມຈໍ"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"ໂໝດເດັສທັອບ"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"ແບ່ງໜ້າຈໍ"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"ເພີ່ມເຕີມ"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"ລອຍ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml
index db2c717..3d9f626 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Padidinti"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml
index 6b1f76c..3661562 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizēt"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Turis"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml
index 00f2900..41f549e 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Затвори"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
     <string name="handle_text" msgid="1766582106752184456">"Прекар"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Цел екран"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Режим за компјутер"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Поделен екран"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Повеќе"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Лебдечко"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index ab3286d..9ff9d78 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"വലുതാക്കുക"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string>
     <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml
index 3d598e4..32149e7 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Хаах"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Буцах"</string>
     <string name="handle_text" msgid="1766582106752184456">"Бариул"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml
index 678a2c5..8c0f69d 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"मोठे करा"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string>
+    <string name="handle_text" msgid="1766582106752184456">"हँडल"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml
index 4dc8dca..e648a7a 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string>
     <string name="handle_text" msgid="1766582106752184456">"Pemegang"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Skrin penuh"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Mod Desktop"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Skrin Pisah"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Lagi"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Terapung"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml
index 0bb6acf..9ee25ab 100644
--- a/libs/WindowManager/Shell/res/values-my/strings.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"ပိတ်ရန်"</string>
     <string name="back_button_text" msgid="1469718707134137085">"နောက်သို့"</string>
     <string name="handle_text" msgid="1766582106752184456">"သုံးသူအမည်"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index 4620012..d8b1f28 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimer"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml
index cdddcdc..6a946db 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml
index d31d7e4..98e6d62 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximaliseren"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Terug"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml
index 9e5a96d..a7b5c30 100644
--- a/libs/WindowManager/Shell/res/values-or/strings.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="back_button_text" msgid="1469718707134137085">"ପଛକୁ ଫେରନ୍ତୁ"</string>
     <string name="handle_text" msgid="1766582106752184456">"ହେଣ୍ଡେଲ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index 48c9a9f..7f84480 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml
index 347b01d..fb807e5 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksymalizuj"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index 353c02d..cad59e0 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string>
     <string name="handle_text" msgid="1766582106752184456">"Alça"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Mais"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
index 97d40b5..26772dc 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Anterior"</string>
     <string name="handle_text" msgid="1766582106752184456">"Indicador"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Ecrã inteiro"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Modo de ambiente de trabalho"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Ecrã dividido"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Mais"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Flutuar"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml
index 353c02d..cad59e0 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Fechar"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Voltar"</string>
     <string name="handle_text" msgid="1766582106752184456">"Alça"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Tela cheia"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Modo área de trabalho"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Tela dividida"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Mais"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Ponto flutuante"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index a085f02..c187fe3 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Închide"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Înapoi"</string>
     <string name="handle_text" msgid="1766582106752184456">"Ghidaj"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index 3b6efc1..dabeacb 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Развернуть"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml
index 4be32cf..b0e2c80 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string>
     <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string>
+    <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml
index 4007498..f20e940 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"Zavrieť"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Späť"</string>
     <string name="handle_text" msgid="1766582106752184456">"Rukoväť"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Celá obrazovka"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Režim počítača"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Rozdelená obrazovka"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Viac"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Plávajúce"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index e4fa7e9..83156e7 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiraj"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zapri"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ročica"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml
index bbd312b..6f7704c 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizo"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Pas"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml
index 5beb31c..8b6794f 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Затворите"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
     <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index c4bcef4..3163fa1 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Utöka"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Stäng"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Handtag"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml
index 5ad1985..a7ad6f8 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Panua"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Funga"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ncha"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml
index 1cb9cd76..e55e231 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string>
     <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml
index 18c3719..bec1a07 100644
--- a/libs/WindowManager/Shell/res/values-te/strings.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"మూసివేయండి"</string>
     <string name="back_button_text" msgid="1469718707134137085">"వెనుకకు"</string>
     <string name="handle_text" msgid="1766582106752184456">"హ్యాండిల్"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml
index 9e11d66..76cbb1a 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings.xml
@@ -88,4 +88,9 @@
     <string name="close_button_text" msgid="2913281996024033299">"ปิด"</string>
     <string name="back_button_text" msgid="1469718707134137085">"กลับ"</string>
     <string name="handle_text" msgid="1766582106752184456">"แฮนเดิล"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"เต็มหน้าจอ"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"โหมดเดสก์ท็อป"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"แยกหน้าจอ"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"เพิ่มเติม"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"ล่องลอย"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml
index fbe0347..a990b52 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Isara"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Bumalik"</string>
     <string name="handle_text" msgid="1766582106752184456">"Handle"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index 7c557cb..44791e0 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Ekranı Kapla"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Kapat"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Geri"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml
index 73cb754..dbfb389 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml
index 0ff1b6c..e9ac973 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"بند کریں"</string>
     <string name="back_button_text" msgid="1469718707134137085">"پیچھے"</string>
     <string name="handle_text" msgid="1766582106752184456">"ہینڈل"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml
index 1cf6228..3b9324f 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -86,8 +86,11 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Yoyish"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Yopish"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string>
+    <string name="fullscreen_text" msgid="1162316685217676079">"Butun ekran"</string>
+    <string name="desktop_text" msgid="1077633567027630454">"Desktop rejimi"</string>
+    <string name="split_screen_text" msgid="1396336058129570886">"Ekranni ikkiga ajratish"</string>
+    <string name="more_button_text" msgid="3655388105592893530">"Yana"</string>
+    <string name="float_button_text" msgid="9221657008391364581">"Pufakli"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml
index ce10e46..ccea4e0 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Phóng to"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Đóng"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
index 824f46e..9497ae8 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -86,8 +86,16 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string>
     <string name="close_button_text" msgid="2913281996024033299">"关闭"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
+    <string name="back_button_text" msgid="1469718707134137085">"返回"</string>
+    <string name="handle_text" msgid="1766582106752184456">"处理"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
     <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
     <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
index 5dce250..c6fdd14 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"關閉"</string>
     <string name="back_button_text" msgid="1469718707134137085">"返去"</string>
     <string name="handle_text" msgid="1766582106752184456">"控點"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
index c449c2e..84afaa8 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"關閉"</string>
     <string name="back_button_text" msgid="1469718707134137085">"返回"</string>
     <string name="handle_text" msgid="1766582106752184456">"控點"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml
index d452d25..c5e8e38 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings.xml
@@ -19,7 +19,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string>
     <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string>
-    <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string>
+    <string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</string>
     <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string>
     <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string>
     <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Imenyu Yesithombe-Esithombeni"</string>
@@ -88,4 +88,14 @@
     <string name="close_button_text" msgid="2913281996024033299">"Vala"</string>
     <string name="back_button_text" msgid="1469718707134137085">"Emuva"</string>
     <string name="handle_text" msgid="1766582106752184456">"Isibambo"</string>
+    <!-- no translation found for fullscreen_text (1162316685217676079) -->
+    <skip />
+    <!-- no translation found for desktop_text (1077633567027630454) -->
+    <skip />
+    <!-- no translation found for split_screen_text (1396336058129570886) -->
+    <skip />
+    <!-- no translation found for more_button_text (3655388105592893530) -->
+    <skip />
+    <!-- no translation found for float_button_text (9221657008391364581) -->
+    <skip />
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 30c3d50..c6197c8 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>
 
@@ -107,4 +111,8 @@
 
     <!-- Whether to dim a split-screen task when the other is the IME target -->
     <bool name="config_dimNonImeAttachedSide">true</bool>
+
+    <!-- Components support to launch multiple instances into split-screen -->
+    <string-array name="config_componentsSupportMultiInstancesSplit">
+    </string-array>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0bc7085..3ee20ea 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -321,4 +321,21 @@
 
     <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
     <dimen name="floating_dismiss_circle_small">120dp</dimen>
+
+    <!-- The thickness of shadows of a window that has focus in DIP. -->
+    <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen>
+
+    <!-- The thickness of shadows of a window that doesn't have focus in DIP. -->
+    <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen>
+
+    <!-- Height of button (32dp)  + 2 * margin (5dp each). -->
+    <dimen name="freeform_decor_caption_height">42dp</dimen>
+
+    <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
+    <dimen name="freeform_decor_caption_width">216dp</dimen>
+
+    <dimen name="freeform_resize_handle">30dp</dimen>
+
+    <dimen name="freeform_resize_corner">44dp</dimen>
+
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 4807f08..097a567 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -202,4 +202,14 @@
     <string name="back_button_text">Back</string>
     <!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] -->
     <string name="handle_text">Handle</string>
+    <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
+    <string name="fullscreen_text">Fullscreen</string>
+    <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
+    <string name="desktop_text">Desktop Mode</string>
+    <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] -->
+    <string name="split_screen_text">Split Screen</string>
+    <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] -->
+    <string name="more_button_text">More</string>
+    <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] -->
+    <string name="float_button_text">Float</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 19f7c3e..a859721 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -30,6 +30,13 @@
         <item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
     </style>
 
+    <style name="CaptionButtonStyle">
+        <item name="android:layout_width">32dp</item>
+        <item name="android:layout_height">32dp</item>
+        <item name="android:layout_margin">5dp</item>
+        <item name="android:padding">4dp</item>
+    </style>
+
     <style name="DockedDividerBackground">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">@dimen/split_divider_bar_width</item>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index dec1e38..065fd95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -16,14 +16,12 @@
 
 package com.android.wm.shell;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -49,7 +47,6 @@
 import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
-import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -568,6 +565,22 @@
         }
     }
 
+    /**
+     * Return list of {@link RunningTaskInfo}s for the given display.
+     *
+     * @return filtered list of tasks or empty list
+     */
+    public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) {
+        ArrayList<RunningTaskInfo> result = new ArrayList<>();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId) {
+                result.add(taskInfo);
+            }
+        }
+        return result;
+    }
+
     /** Gets running task by taskId. Returns {@code null} if no such task observed. */
     @Nullable
     public RunningTaskInfo getRunningTaskInfo(int taskId) {
@@ -694,57 +707,6 @@
         taskListener.reparentChildSurfaceToTask(taskId, sc, t);
     }
 
-    /**
-     * Create a {@link WindowContainerTransaction} to clear task bounds.
-     *
-     * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
-     * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
-     *
-     * @param displayId display id for tasks that will have bounds cleared
-     * @return {@link WindowContainerTransaction} with pending operations to clear bounds
-     */
-    public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) {
-        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        for (int i = 0; i < mTasks.size(); i++) {
-            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
-            if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType()
-                    == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) {
-                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
-                        taskInfo.token, taskInfo);
-                wct.setBounds(taskInfo.token, null);
-            }
-        }
-        return wct;
-    }
-
-    /**
-     * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
-     *
-     * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to
-     * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}.
-     *
-     * @param displayId display id for tasks that will have windowing mode reset to {@link
-     *                  WindowConfiguration#WINDOWING_MODE_UNDEFINED}
-     * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
-     */
-    public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) {
-        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        for (int i = 0; i < mTasks.size(); i++) {
-            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
-            if (taskInfo.displayId == displayId
-                    && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
-                    && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
-                ProtoLog.d(WM_SHELL_DESKTOP_MODE,
-                        "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
-                        taskInfo);
-                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
-            }
-        }
-        return wct;
-    }
-
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
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/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 490975c..921861a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -303,6 +303,7 @@
         // 3. Animate the TaskFragment using Activity Change info (start/end bounds).
         // This is because the TaskFragment surface/change won't contain the Activity's before its
         // reparent.
+        Animation changeAnimation = null;
         for (TransitionInfo.Change change : info.getChanges()) {
             if (change.getMode() != TRANSIT_CHANGE
                     || change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
@@ -325,8 +326,14 @@
                 }
             }
 
+            // There are two animations in the array. The first one is for the start leash
+            // (snapshot), and the second one is for the end leash (TaskFragment).
             final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change,
                     boundsAnimationChange.getEndAbsBounds());
+            // Keep track as we might need to add background color for the animation.
+            // Although there may be multiple change animation, record one of them is sufficient
+            // because the background color will be added to the root leash for the whole animation.
+            changeAnimation = animations[1];
 
             // Create a screenshot based on change, but attach it to the top of the
             // boundsAnimationChange.
@@ -345,6 +352,9 @@
                     animations[1], boundsAnimationChange));
         }
 
+        // If there is no corresponding open/close window with the change, we should show background
+        // color to cover the empty part of the screen.
+        boolean shouldShouldBackgroundColor = true;
         // Handle the other windows that don't have bounds change in the same transition.
         for (TransitionInfo.Change change : info.getChanges()) {
             if (handledChanges.contains(change)) {
@@ -359,11 +369,20 @@
                 animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
             } else if (Transitions.isClosingType(change.getMode())) {
                 animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+                shouldShouldBackgroundColor = false;
             } else {
                 animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+                shouldShouldBackgroundColor = false;
             }
             adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
         }
+
+        if (shouldShouldBackgroundColor && changeAnimation != null) {
+            // Change animation may leave part of the screen empty. Show background color to cover
+            // that.
+            changeAnimation.setShowBackdrop(true);
+        }
+
         return adapters;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 58b2366..2bb7369 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -158,7 +158,7 @@
         // The position should be 0-based as we will post translate in
         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
-                0, 0);
+                startBounds.top - endBounds.top, 0);
         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
         endSet.addAnimation(endTranslate);
         // The end leash is resizing, we should update the window crop based on the clip rect.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 43f39b7..f811940 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -61,7 +61,6 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -69,32 +68,28 @@
  * Controls the window animation run when a user initiates a back gesture.
  */
 public class BackAnimationController implements RemoteCallable<BackAnimationController> {
-    private static final String TAG = "BackAnimationController";
+    private static final String TAG = "ShellBackPreview";
     private static final int SETTING_VALUE_OFF = 0;
     private static final int SETTING_VALUE_ON = 1;
-    private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
-            "persist.wm.debug.predictive_back_progress_threshold";
     public static final boolean IS_ENABLED =
             SystemProperties.getInt("persist.wm.debug.predictive_back",
-                    SETTING_VALUE_ON) != SETTING_VALUE_OFF;
-    private static final int PROGRESS_THRESHOLD = SystemProperties
-            .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
-
-    // TODO (b/241808055) Find a appropriate time to remove during refactor
-    private static final boolean ENABLE_SHELL_TRANSITIONS = Transitions.ENABLE_SHELL_TRANSITIONS;
-    /**
-     * Max duration to wait for a transition to finish before accepting another gesture start
-     * request.
-     */
-    private static final long MAX_TRANSITION_DURATION = 2000;
-
+                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
+     /** Flag for U animation features */
+    public static boolean IS_U_ANIMATION_ENABLED =
+            SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
+                    SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+    /** Predictive back animation developer option */
     private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
+    /**
+     * Max duration to wait for an animation to finish before triggering the real back.
+     */
+    private static final long MAX_ANIMATION_DURATION = 2000;
 
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
-    /** Tracks if an uninterruptible transition is in progress */
-    private boolean mTransitionInProgress = false;
+    /** Tracks if an uninterruptible animation is in progress */
+    private boolean mPostCommitAnimationInProgress = false;
     /** Tracks if we should start the back gesture on the next motion move event */
     private boolean mShouldStartOnNextMoveEvent = false;
     /** @see #setTriggerBack(boolean) */
@@ -108,9 +103,9 @@
     private final ShellController mShellController;
     private final ShellExecutor mShellExecutor;
     private final Handler mBgHandler;
-    private final Runnable mResetTransitionRunnable = () -> {
-        ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Transition didn't finish in %d ms. Resetting...",
-                MAX_TRANSITION_DURATION);
+    private final Runnable mAnimationTimeoutRunnable = () -> {
+        ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
+                MAX_ANIMATION_DURATION);
         onBackAnimationFinished();
     };
 
@@ -121,8 +116,8 @@
     private final TouchTracker mTouchTracker = new TouchTracker();
 
     private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
-    private final Transitions mTransitions;
-    private BackTransitionHandler mBackTransitionHandler;
+    @Nullable
+    private IOnBackInvokedCallback mActiveCallback;
 
     @VisibleForTesting
     final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
@@ -131,9 +126,9 @@
         @Override
         public void focusLost(IBinder inputToken) {
             mShellExecutor.execute(() -> {
-                if (!mBackGestureStarted || mTransitionInProgress) {
-                    // If an uninterruptible transition is already in progress, we should ignore
-                    // this due to the transition may cause focus lost. (alpha = 0)
+                if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
+                    // If an uninterruptible animation is already in progress, we should ignore
+                    // this due to it may cause focus lost. (alpha = 0)
                     return;
                 }
                 ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Target window lost focus.");
@@ -143,63 +138,14 @@
         }
     };
 
-    /**
-     * Helper class to record the touch location for gesture start and latest.
-     */
-    private static class TouchTracker {
-        /**
-         * Location of the latest touch event
-         */
-        private float mLatestTouchX;
-        private float mLatestTouchY;
-        private int mSwipeEdge;
-        private float mProgressThreshold;
-
-        /**
-         * Location of the initial touch event of the back gesture.
-         */
-        private float mInitTouchX;
-        private float mInitTouchY;
-
-        void update(float touchX, float touchY, int swipeEdge) {
-            mLatestTouchX = touchX;
-            mLatestTouchY = touchY;
-            mSwipeEdge = swipeEdge;
-        }
-
-        void setGestureStartLocation(float touchX, float touchY) {
-            mInitTouchX = touchX;
-            mInitTouchY = touchY;
-        }
-
-        void setProgressThreshold(float progressThreshold) {
-            mProgressThreshold = progressThreshold;
-        }
-
-        float getProgress(float touchX) {
-            int deltaX = Math.round(touchX - mInitTouchX);
-            float progressThreshold = PROGRESS_THRESHOLD >= 0
-                    ? PROGRESS_THRESHOLD : mProgressThreshold;
-            return Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1);
-        }
-
-        void reset() {
-            mInitTouchX = 0;
-            mInitTouchY = 0;
-            mSwipeEdge = -1;
-        }
-    }
-
     public BackAnimationController(
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
             @NonNull @ShellBackgroundThread Handler backgroundHandler,
-            Context context,
-            Transitions transitions) {
+            Context context) {
         this(shellInit, shellController, shellExecutor, backgroundHandler,
-                ActivityTaskManager.getService(), context, context.getContentResolver(),
-                transitions);
+                ActivityTaskManager.getService(), context, context.getContentResolver());
     }
 
     @VisibleForTesting
@@ -209,8 +155,7 @@
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
             @NonNull @ShellBackgroundThread Handler bgHandler,
             @NonNull IActivityTaskManager activityTaskManager,
-            Context context, ContentResolver contentResolver,
-            Transitions transitions) {
+            Context context, ContentResolver contentResolver) {
         mShellController = shellController;
         mShellExecutor = shellExecutor;
         mActivityTaskManager = activityTaskManager;
@@ -218,18 +163,32 @@
         mContentResolver = contentResolver;
         mBgHandler = bgHandler;
         shellInit.addInitCallback(this::onInit, this);
-        mTransitions = transitions;
+    }
+
+    @VisibleForTesting
+    void setEnableUAnimation(boolean enable) {
+        IS_U_ANIMATION_ENABLED = enable;
     }
 
     private void onInit() {
         setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
         createAdapter();
-        if (ENABLE_SHELL_TRANSITIONS) {
-            mBackTransitionHandler = new BackTransitionHandler(this);
-            mTransitions.addHandler(mBackTransitionHandler);
-        }
         mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                 this::createExternalInterface, this);
+
+        initBackAnimationRunners();
+    }
+
+    private void initBackAnimationRunners() {
+        if (!IS_U_ANIMATION_ENABLED) {
+            return;
+        }
+
+        final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
+        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
+                new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
+        // TODO (238474994): register cross activity animation when it's completed.
+        // TODO (236760237): register dialog close animation when it's completed.
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -254,8 +213,7 @@
                 Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
         boolean isEnabled = settingValue == SETTING_VALUE_ON;
         mEnableAnimations.set(isEnabled);
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
-                isEnabled);
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled);
     }
 
     public BackAnimation getBackAnimationImpl() {
@@ -309,7 +267,9 @@
         public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
                 IRemoteAnimationRunner runner) {
             executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
-                    (controller) -> controller.setBackToLauncherCallback(callback, runner));
+                    (controller) -> controller.registerAnimation(
+                            BackNavigationInfo.TYPE_RETURN_TO_HOME,
+                            new BackAnimationRunner(callback, runner)));
         }
 
         @Override
@@ -324,57 +284,26 @@
         }
     }
 
-    @VisibleForTesting
-    void setBackToLauncherCallback(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
-        mAnimationDefinition.set(BackNavigationInfo.TYPE_RETURN_TO_HOME,
-                new BackAnimationRunner(callback, runner));
+    void registerAnimation(@BackNavigationInfo.BackTargetType int type,
+            @NonNull BackAnimationRunner runner) {
+        mAnimationDefinition.set(type, runner);
     }
 
     private void clearBackToLauncherCallback() {
         mAnimationDefinition.remove(BackNavigationInfo.TYPE_RETURN_TO_HOME);
     }
 
-    @VisibleForTesting
-    void onBackAnimationFinished() {
-        if (!mTransitionInProgress) {
-            return;
-        }
-
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
-
-        // Trigger real back.
-        if (mBackNavigationInfo != null) {
-            IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
-            if (mTriggerBack) {
-                dispatchOnBackInvoked(callback);
-            } else {
-                dispatchOnBackCancelled(callback);
-            }
-        }
-
-        // In legacy transition, it would use `Task.mBackGestureStarted` in core to handle the
-        // following transition when back callback is invoked.
-        // If the back callback is not invoked, we should reset the token and finish the whole back
-        // navigation without waiting the transition.
-        if (!ENABLE_SHELL_TRANSITIONS) {
-            finishBackNavigation();
-        } else if (!mTriggerBack) {
-            // reset the token to prevent it consume next transition.
-            mBackTransitionHandler.setDepartingWindowContainerToken(null);
-            finishBackNavigation();
-        }
-    }
-
     /**
      * Called when a new motion event needs to be transferred to this
      * {@link BackAnimationController}
      */
     public void onMotionEvent(float touchX, float touchY, int keyAction,
             @BackEvent.SwipeEdge int swipeEdge) {
-        if (mTransitionInProgress) {
+        if (mPostCommitAnimationInProgress) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
+
+        mTouchTracker.update(touchX, touchY);
         if (keyAction == MotionEvent.ACTION_DOWN) {
             if (!mBackGestureStarted) {
                 mShouldStartOnNextMoveEvent = true;
@@ -384,10 +313,10 @@
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                 // could be happened when ACTION_DOWN, it may change the current focus that we
                 // would access it when startBackNavigation.
-                onGestureStarted(touchX, touchY);
+                onGestureStarted(touchX, touchY, swipeEdge);
                 mShouldStartOnNextMoveEvent = false;
             }
-            onMove(touchX, touchY, swipeEdge);
+            onMove();
         } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
             ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                     "Finishing gesture with event action: %d", keyAction);
@@ -398,14 +327,14 @@
         }
     }
 
-    private void onGestureStarted(float touchX, float touchY) {
+    private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
         if (mBackGestureStarted || mBackNavigationInfo != null) {
             Log.e(TAG, "Animation is being initialized but is already started.");
             finishBackNavigation();
         }
 
-        mTouchTracker.setGestureStartLocation(touchX, touchY);
+        mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
         mBackGestureStarted = true;
 
         try {
@@ -425,34 +354,28 @@
             return;
         }
         final int backType = backNavigationInfo.getType();
-        final IOnBackInvokedCallback targetCallback;
-        final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
+        final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
         if (shouldDispatchToAnimator) {
-            targetCallback = mAnimationDefinition.get(backType).getGestureStartedCallback();
+            if (mAnimationDefinition.contains(backType)) {
+                mActiveCallback = mAnimationDefinition.get(backType).getCallback();
+                mAnimationDefinition.get(backType).startGesture();
+            } else {
+                mActiveCallback = null;
+            }
         } else {
-            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-        }
-        if (shouldDispatchToAnimator) {
-            dispatchOnBackStarted(targetCallback);
+            mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
+            dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null));
         }
     }
 
-    private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
-        if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
+    private void onMove() {
+        if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()
+                || mActiveCallback == null) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
-        float progress = mTouchTracker.getProgress(touchX);
-        int backType = mBackNavigationInfo.getType();
 
-        BackEvent backEvent = new BackEvent(touchX, touchY, progress, swipeEdge);
-        IOnBackInvokedCallback targetCallback = null;
-        if (shouldDispatchToAnimator(backType)) {
-            targetCallback = mAnimationDefinition.get(backType).getCallback();
-        } else {
-            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-        }
-        dispatchOnBackProgressed(targetCallback, backEvent);
+        final BackEvent backEvent = mTouchTracker.createProgressEvent();
+        dispatchOnBackProgressed(mActiveCallback, backEvent);
     }
 
     private void injectBackKey() {
@@ -474,6 +397,106 @@
         }
     }
 
+    private boolean shouldDispatchToAnimator() {
+        return mEnableAnimations.get()
+                && mBackNavigationInfo != null
+                && mBackNavigationInfo.isPrepareRemoteAnimation();
+    }
+
+    private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
+            BackEvent backEvent) {
+        if (callback == null) {
+            return;
+        }
+        try {
+            if (mEnableAnimations.get()) {
+                callback.onBackStarted(backEvent);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "dispatchOnBackStarted error: ", e);
+        }
+    }
+
+    private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
+        if (callback == null) {
+            return;
+        }
+        try {
+            callback.onBackInvoked();
+        } catch (RemoteException e) {
+            Log.e(TAG, "dispatchOnBackInvoked error: ", e);
+        }
+    }
+
+    private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
+        if (callback == null) {
+            return;
+        }
+        try {
+            if (mEnableAnimations.get()) {
+                callback.onBackCancelled();
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "dispatchOnBackCancelled error: ", e);
+        }
+    }
+
+    private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
+            BackEvent backEvent) {
+        if (callback == null) {
+            return;
+        }
+        try {
+            if (mEnableAnimations.get()) {
+                callback.onBackProgressed(backEvent);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "dispatchOnBackProgressed error: ", e);
+        }
+    }
+
+    /**
+     * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
+     */
+    public void setTriggerBack(boolean triggerBack) {
+        if (mPostCommitAnimationInProgress) {
+            return;
+        }
+        mTriggerBack = triggerBack;
+        mTouchTracker.setTriggerBack(triggerBack);
+    }
+
+    private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
+        mTouchTracker.setProgressThreshold(progressThreshold);
+    }
+
+    private void invokeOrCancelBack() {
+        // Make a synchronized call to core before dispatch back event to client side.
+        // If the close transition happens before the core receives onAnimationFinished, there will
+        // play a second close animation for that transition.
+        if (mBackAnimationFinishedCallback != null) {
+            try {
+                mBackAnimationFinishedCallback.onAnimationFinished(mTriggerBack);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e);
+            }
+            mBackAnimationFinishedCallback = null;
+        }
+
+        if (mBackNavigationInfo != null) {
+            final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
+            if (mTriggerBack) {
+                dispatchOnBackInvoked(callback);
+            } else {
+                dispatchOnBackCancelled(callback);
+            }
+        }
+        finishBackNavigation();
+    }
+
+    /**
+     * Called when the gesture is released, then it could start the post commit animation.
+     */
     private void onGestureFinished(boolean fromTouch) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
         if (!mBackGestureStarted) {
@@ -487,7 +510,8 @@
             mBackGestureStarted = false;
         }
 
-        if (mTransitionInProgress) {
+        if (mPostCommitAnimationInProgress) {
+            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
             return;
         }
 
@@ -501,148 +525,74 @@
             return;
         }
 
-        int backType = mBackNavigationInfo.getType();
-        boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
+        final int backType = mBackNavigationInfo.getType();
+        // Simply trigger and finish back navigation when no animator defined.
+        if (!shouldDispatchToAnimator() || mActiveCallback == null) {
+            invokeOrCancelBack();
+            return;
+        }
+
         final BackAnimationRunner runner = mAnimationDefinition.get(backType);
-        IOnBackInvokedCallback targetCallback = shouldDispatchToAnimator
-                ? runner.getCallback() : mBackNavigationInfo.getOnBackInvokedCallback();
-
-        if (shouldDispatchToAnimator) {
-            if (runner.onGestureFinished(mTriggerBack)) {
-                Log.w(TAG, "Gesture released, but animation didn't ready.");
-                return;
-            }
-            startTransition();
+        if (runner.isWaitingAnimation()) {
+            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
+            return;
         }
+        startPostCommitAnimation();
+    }
+
+    /**
+     * Start the phase 2 animation when gesture is released.
+     * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
+     */
+    private void startPostCommitAnimation() {
+        if (mPostCommitAnimationInProgress) {
+            return;
+        }
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
+        mPostCommitAnimationInProgress = true;
+        mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION);
+
+        // The next callback should be {@link #onBackAnimationFinished}.
         if (mTriggerBack) {
-            dispatchOnBackInvoked(targetCallback);
+            dispatchOnBackInvoked(mActiveCallback);
         } else {
-            dispatchOnBackCancelled(targetCallback);
-        }
-        if (!shouldDispatchToAnimator) {
-            // Animation callback missing. Simply finish animation.
-            finishBackNavigation();
-        }
-    }
-
-    private boolean shouldDispatchToAnimator(int backType) {
-        return mEnableAnimations.get()
-                && mBackNavigationInfo != null
-                && mBackNavigationInfo.isPrepareRemoteAnimation()
-                && mAnimationDefinition.contains(backType);
-    }
-
-    private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
-        if (callback == null) {
-            return;
-        }
-        try {
-            callback.onBackStarted();
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatchOnBackStarted error: ", e);
-        }
-    }
-
-    private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
-        if (callback == null) {
-            return;
-        }
-        try {
-            callback.onBackInvoked();
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatchOnBackInvoked error: ", e);
-        }
-    }
-
-    private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
-        if (callback == null) {
-            return;
-        }
-        try {
-            callback.onBackCancelled();
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatchOnBackCancelled error: ", e);
-        }
-    }
-
-    private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
-            BackEvent backEvent) {
-        if (callback == null) {
-            return;
-        }
-        try {
-            callback.onBackProgressed(backEvent);
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatchOnBackProgressed error: ", e);
+            dispatchOnBackCancelled(mActiveCallback);
         }
     }
 
     /**
-     * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
+     * Called when the post commit animation is completed or timeout.
+     * This will trigger the real {@link IOnBackInvokedCallback} behavior.
      */
-    public void setTriggerBack(boolean triggerBack) {
-        if (mTransitionInProgress) {
+    @VisibleForTesting
+    void onBackAnimationFinished() {
+        if (!mPostCommitAnimationInProgress) {
             return;
         }
-        mTriggerBack = triggerBack;
+        // Stop timeout runner.
+        mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+        mPostCommitAnimationInProgress = false;
+
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");
+
+        // Trigger the real back.
+        invokeOrCancelBack();
     }
 
-    private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
-        mTouchTracker.setProgressThreshold(progressThreshold);
-    }
-
+    /**
+     * This should be called after the whole back navigation is completed.
+     */
     @VisibleForTesting
     void finishBackNavigation() {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
-        BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
-        boolean triggerBack = mTriggerBack;
-        mBackNavigationInfo = null;
-        mTriggerBack = false;
         mShouldStartOnNextMoveEvent = false;
         mTouchTracker.reset();
-        if (backNavigationInfo == null) {
-            return;
+        mActiveCallback = null;
+        if (mBackNavigationInfo != null) {
+            mBackNavigationInfo.onBackNavigationFinished(mTriggerBack);
+            mBackNavigationInfo = null;
         }
-        stopTransition();
-        if (mBackAnimationFinishedCallback != null) {
-            try {
-                mBackAnimationFinishedCallback.onAnimationFinished(triggerBack);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e);
-            }
-            mBackAnimationFinishedCallback = null;
-        }
-        backNavigationInfo.onBackNavigationFinished(triggerBack);
-    }
-
-    private void startTransition() {
-        if (mTransitionInProgress) {
-            return;
-        }
-        mTransitionInProgress = true;
-        if (ENABLE_SHELL_TRANSITIONS) {
-            mBackTransitionHandler.setDepartingWindowContainerToken(
-                    mBackNavigationInfo.getDepartingWindowContainerToken());
-        }
-        mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
-    }
-
-    void stopTransition() {
-        mShellExecutor.removeCallbacks(mResetTransitionRunnable);
-        mTransitionInProgress = false;
-    }
-
-    /**
-     * This should be called from {@link BackTransitionHandler#startAnimation} when the following
-     * transition is triggered by the real back callback in {@link #onBackAnimationFinished}.
-     * Will consume the default transition and finish current back navigation.
-     */
-    void finishTransition(Transitions.TransitionFinishCallback finishCallback) {
-        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishTransition()");
-        mShellExecutor.execute(() -> {
-            finishBackNavigation();
-            finishCallback.onTransitionFinished(null, null);
-        });
+        mTriggerBack = false;
     }
 
     private void createAdapter() {
@@ -668,17 +618,20 @@
                     mBackAnimationFinishedCallback = finishedCallback;
 
                     ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
-                    runner.startAnimation(apps, wallpapers, nonApps,
-                            BackAnimationController.this::onBackAnimationFinished);
+                    runner.startAnimation(apps, wallpapers, nonApps, () -> mShellExecutor.execute(
+                            BackAnimationController.this::onBackAnimationFinished));
+
+                    if (apps.length >= 1) {
+                        dispatchOnBackStarted(
+                                mActiveCallback, mTouchTracker.createStartEvent(apps[0]));
+                    }
 
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackEvent backFinish = new BackEvent(
-                                mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1,
-                                mTouchTracker.mSwipeEdge);
-                        startTransition();
-                        runner.consumeIfGestureFinished(backFinish);
+                        final BackEvent backFinish = mTouchTracker.createProgressEvent();
+                        dispatchOnBackProgressed(mActiveCallback, backFinish);
+                        startPostCommitAnimation();
                     }
                 });
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 12bbf73..d70b8f5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -18,12 +18,12 @@
 
 import static android.view.WindowManager.TRANSIT_OLD_UNSET;
 
+import android.annotation.NonNull;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.RemoteAnimationTarget;
-import android.window.BackEvent;
 import android.window.IBackAnimationRunner;
 import android.window.IOnBackInvokedCallback;
 
@@ -38,11 +38,11 @@
     private final IOnBackInvokedCallback mCallback;
     private final IRemoteAnimationRunner mRunner;
 
-    private boolean mTriggerBack;
     // Whether we are waiting to receive onAnimationStart
     private boolean mWaitingAnimation;
 
-    BackAnimationRunner(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) {
+    BackAnimationRunner(@NonNull IOnBackInvokedCallback callback,
+            @NonNull IRemoteAnimationRunner runner) {
         mCallback = callback;
         mRunner = runner;
     }
@@ -79,30 +79,11 @@
         }
     }
 
-    IOnBackInvokedCallback getGestureStartedCallback() {
+    void startGesture() {
         mWaitingAnimation = true;
-        return mCallback;
     }
 
-    boolean onGestureFinished(boolean triggerBack) {
-        if (mWaitingAnimation) {
-            mTriggerBack = triggerBack;
-            return true;
-        }
-        return false;
-    }
-
-    void consumeIfGestureFinished(final BackEvent backFinish) {
-        Log.d(TAG, "Start transition due to gesture is finished");
-        try {
-            mCallback.onBackProgressed(backFinish);
-            if (mTriggerBack) {
-                mCallback.onBackInvoked();
-            } else {
-                mCallback.onBackCancelled();
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "dispatch error: ", e);
-        }
+    boolean isWaitingAnimation() {
+        return mWaitingAnimation;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackTransitionHandler.java
deleted file mode 100644
index 6d72d9c..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackTransitionHandler.java
+++ /dev/null
@@ -1,78 +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.wm.shell.back;
-
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.window.TransitionInfo;
-import android.window.TransitionRequestInfo;
-import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.wm.shell.transition.Transitions;
-
-class BackTransitionHandler implements Transitions.TransitionHandler {
-    private BackAnimationController mBackAnimationController;
-    private WindowContainerToken mDepartingWindowContainerToken;
-
-    BackTransitionHandler(@NonNull BackAnimationController backAnimationController) {
-        mBackAnimationController = backAnimationController;
-    }
-
-    @Override
-    public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        if (mDepartingWindowContainerToken != null) {
-            final TransitionInfo.Change change = info.getChange(mDepartingWindowContainerToken);
-            if (change == null) {
-                return false;
-            }
-
-            startTransaction.hide(change.getLeash());
-            startTransaction.apply();
-            mDepartingWindowContainerToken = null;
-            mBackAnimationController.finishTransition(finishCallback);
-            return true;
-        }
-
-        return false;
-    }
-
-    @Nullable
-    @Override
-    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
-            @NonNull TransitionRequestInfo request) {
-        return null;
-    }
-
-    @Override
-    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
-            @NonNull Transitions.TransitionFinishCallback finishCallback) {
-    }
-
-    void setDepartingWindowContainerToken(
-            @Nullable WindowContainerToken departingWindowContainerToken) {
-        mDepartingWindowContainerToken = departingWindowContainerToken;
-    }
-}
-
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
new file mode 100644
index 0000000..2074b6a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -0,0 +1,365 @@
+/*
+ * 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.wm.shell.back;
+
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.window.BackEvent.EDGE_RIGHT;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.RemoteException;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.window.BackEvent;
+import android.window.BackProgressAnimator;
+import android.window.IOnBackInvokedCallback;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+
+/**
+ * Controls the animation of swiping back and returning to another task.
+ *
+ * This is a two part animation. The first part is an animation that tracks gesture location to
+ * scale and move the closing and entering app windows.
+ * Once the gesture is committed, the second part remains the closing window in place.
+ * The entering window plays the rest of app opening transition to enter full screen.
+ *
+ * This animation is used only for apps that enable back dispatching via
+ * {@link android.window.OnBackInvokedDispatcher}. The controller registers
+ * an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
+ * navigation to launcher starts.
+ */
+@ShellMainThread
+class CrossTaskBackAnimation {
+    private static final float[] BACKGROUNDCOLOR = {0.263f, 0.263f, 0.227f};
+
+    /**
+     * Minimum scale of the entering window.
+     */
+    private static final float ENTERING_MIN_WINDOW_SCALE = 0.85f;
+
+    /**
+     * Minimum scale of the closing window.
+     */
+    private static final float CLOSING_MIN_WINDOW_SCALE = 0.75f;
+
+    /**
+     * Minimum color scale of the closing window.
+     */
+    private static final float CLOSING_MIN_WINDOW_COLOR_SCALE = 0.1f;
+
+    /**
+     * The margin between the entering window and the closing window
+     */
+    private static final int WINDOW_MARGIN = 35;
+
+    /** Max window translation in the Y axis. */
+    private static final int WINDOW_MAX_DELTA_Y = 160;
+
+    private final Rect mStartTaskRect = new Rect();
+    private final float mCornerRadius;
+
+    // The closing window properties.
+    private final RectF mClosingCurrentRect = new RectF();
+
+    // The entering window properties.
+    private final Rect mEnteringStartRect = new Rect();
+    private final RectF mEnteringCurrentRect = new RectF();
+
+    private final PointF mInitialTouchPos = new PointF();
+    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+
+    private final Matrix mTransformMatrix = new Matrix();
+
+    private final float[] mTmpFloat9 = new float[9];
+    private final float[] mTmpTranslate = {0, 0, 0};
+
+    private RemoteAnimationTarget mEnteringTarget;
+    private RemoteAnimationTarget mClosingTarget;
+    private SurfaceControl mBackgroundSurface;
+    private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+
+    private boolean mBackInProgress = false;
+
+    private boolean mIsRightEdge;
+    private float mProgress = 0;
+    private PointF mTouchPos = new PointF();
+    private IRemoteAnimationFinishedCallback mFinishCallback;
+
+    private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+
+    final IOnBackInvokedCallback mCallback = new IOnBackInvokedCallback.Default() {
+        @Override
+        public void onBackStarted(BackEvent backEvent) {
+            mProgressAnimator.onBackStarted(backEvent,
+                    CrossTaskBackAnimation.this::onGestureProgress);
+        }
+
+        @Override
+        public void onBackProgressed(@NonNull BackEvent backEvent) {
+            mProgressAnimator.onBackProgressed(backEvent);
+        }
+
+        @Override
+        public void onBackCancelled() {
+            mProgressAnimator.reset();
+            finishAnimation();
+        }
+
+        @Override
+        public void onBackInvoked() {
+            mProgressAnimator.reset();
+            onGestureCommitted();
+        }
+    };
+
+    final IRemoteAnimationRunner mRunner = new IRemoteAnimationRunner.Default() {
+        @Override
+        public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                IRemoteAnimationFinishedCallback finishedCallback) {
+            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to task animation.");
+            for (RemoteAnimationTarget a : apps) {
+                if (a.mode == MODE_CLOSING) {
+                    mClosingTarget = a;
+                }
+                if (a.mode == MODE_OPENING) {
+                    mEnteringTarget = a;
+                }
+            }
+
+            startBackAnimation();
+            mFinishCallback = finishedCallback;
+        }
+    };
+
+    CrossTaskBackAnimation(Context context) {
+        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+    }
+
+    private float getInterpolatedProgress(float backProgress) {
+        return 1 - (1 - backProgress) * (1 - backProgress) * (1 - backProgress);
+    }
+
+    private void startBackAnimation() {
+        if (mEnteringTarget == null || mClosingTarget == null) {
+            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
+            return;
+        }
+
+        // Offset start rectangle to align task bounds.
+        mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
+        mStartTaskRect.offsetTo(0, 0);
+
+        // Draw background.
+        mBackgroundSurface = new SurfaceControl.Builder()
+                .setName("Background of Back Navigation")
+                .setColorLayer()
+                .setHidden(false)
+                .build();
+        mTransaction.setColor(mBackgroundSurface, BACKGROUNDCOLOR)
+                .setLayer(mBackgroundSurface, -1);
+        mTransaction.apply();
+    }
+
+    private void updateGestureBackProgress(float progress, BackEvent event) {
+        if (mEnteringTarget == null || mClosingTarget == null) {
+            return;
+        }
+
+        float touchX = event.getTouchX();
+        float touchY = event.getTouchY();
+        float dX = Math.abs(touchX - mInitialTouchPos.x);
+
+        // The 'follow width' is the width of the window if it completely matches
+        // the gesture displacement.
+        final int width = mStartTaskRect.width();
+        final int height = mStartTaskRect.height();
+
+        // The 'progress width' is the width of the window if it strictly linearly interpolates
+        // to minimum scale base on progress.
+        float enteringScale = mapRange(progress, 1, ENTERING_MIN_WINDOW_SCALE);
+        float closingScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_SCALE);
+        float closingColorScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_COLOR_SCALE);
+
+        // The final width is derived from interpolating between the follow with and progress width
+        // using gesture progress.
+        float enteringWidth = enteringScale * width;
+        float closingWidth = closingScale * width;
+        float enteringHeight = (float) height / width * enteringWidth;
+        float closingHeight = (float) height / width * closingWidth;
+
+        float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
+        // Base the window movement in the Y axis on the touch movement in the Y axis.
+        float deltaY = (float) Math.sin(deltaYRatio * Math.PI * 0.5f) * WINDOW_MAX_DELTA_Y;
+        // Move the window along the Y axis.
+        float closingTop = (height - closingHeight) * 0.5f + deltaY;
+        float enteringTop = (height - enteringHeight) * 0.5f + deltaY;
+        // Move the window along the X axis.
+        float right = width - (progress * WINDOW_MARGIN);
+        float left = right - closingWidth;
+
+        mClosingCurrentRect.set(left, closingTop, right, closingTop + closingHeight);
+        mEnteringCurrentRect.set(left - enteringWidth - WINDOW_MARGIN, enteringTop,
+                left - WINDOW_MARGIN, enteringTop + enteringHeight);
+
+        applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
+        applyColorTransform(mClosingTarget.leash, closingColorScale);
+        applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
+        mTransaction.apply();
+    }
+
+    private void updatePostCommitClosingAnimation(float progress) {
+        mTransaction.setLayer(mClosingTarget.leash, 0);
+        float alpha = mapRange(progress, 1, 0);
+        mTransaction.setAlpha(mClosingTarget.leash, alpha);
+    }
+
+    private void updatePostCommitEnteringAnimation(float progress) {
+        float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
+        float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
+        float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
+        float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
+
+        mEnteringCurrentRect.set(left, top, left + width, top + height);
+        applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
+    }
+
+    /** Transform the target window to match the target rect. */
+    private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
+        if (leash == null) {
+            return;
+        }
+
+        final float scale = targetRect.width() / mStartTaskRect.width();
+        mTransformMatrix.reset();
+        mTransformMatrix.setScale(scale, scale);
+        mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
+        mTransaction.setMatrix(leash, mTransformMatrix, mTmpFloat9)
+                .setWindowCrop(leash, mStartTaskRect)
+                .setCornerRadius(leash, cornerRadius);
+    }
+
+    private void applyColorTransform(SurfaceControl leash, float colorScale) {
+        if (leash == null) {
+            return;
+        }
+        computeScaleTransformMatrix(colorScale, mTmpFloat9);
+        mTransaction.setColorTransform(leash, mTmpFloat9, mTmpTranslate);
+    }
+
+    static void computeScaleTransformMatrix(float scale, float[] matrix) {
+        matrix[0] = scale;
+        matrix[1] = 0;
+        matrix[2] = 0;
+        matrix[3] = 0;
+        matrix[4] = scale;
+        matrix[5] = 0;
+        matrix[6] = 0;
+        matrix[7] = 0;
+        matrix[8] = scale;
+    }
+
+    private void finishAnimation() {
+        if (mEnteringTarget != null) {
+            mEnteringTarget.leash.release();
+            mEnteringTarget = null;
+        }
+        if (mClosingTarget != null) {
+            mClosingTarget.leash.release();
+            mClosingTarget = null;
+        }
+
+        if (mBackgroundSurface != null) {
+            mBackgroundSurface.release();
+            mBackgroundSurface = null;
+        }
+
+        mBackInProgress = false;
+        mTransformMatrix.reset();
+        mClosingCurrentRect.setEmpty();
+        mInitialTouchPos.set(0, 0);
+
+        if (mFinishCallback != null) {
+            try {
+                mFinishCallback.onAnimationFinished();
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+            mFinishCallback = null;
+        }
+    }
+
+    private void onGestureProgress(@NonNull BackEvent backEvent) {
+        if (!mBackInProgress) {
+            mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+            mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
+            mBackInProgress = true;
+        }
+        mProgress = backEvent.getProgress();
+        mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+        updateGestureBackProgress(getInterpolatedProgress(mProgress), backEvent);
+    }
+
+    private void onGestureCommitted() {
+        if (mEnteringTarget == null || mClosingTarget == null) {
+            finishAnimation();
+            return;
+        }
+
+        // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+        // coordinate of the gesture driven phase.
+        mEnteringCurrentRect.round(mEnteringStartRect);
+
+        ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(300);
+        valueAnimator.setInterpolator(mInterpolator);
+        valueAnimator.addUpdateListener(animation -> {
+            float progress = animation.getAnimatedFraction();
+            updatePostCommitEnteringAnimation(progress);
+            updatePostCommitClosingAnimation(progress);
+            mTransaction.apply();
+        });
+
+        valueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finishAnimation();
+            }
+        });
+        valueAnimator.start();
+    }
+
+    private static float mapRange(float value, float min, float max) {
+        return min + (value * (max - min));
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS
new file mode 100644
index 0000000..1e0f9bc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS
@@ -0,0 +1,5 @@
+# WM shell sub-module back navigation owners
+# Bug component: 1152663
+shanh@google.com
+arthurhung@google.com
+wilsonshih@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
new file mode 100644
index 0000000..ccfac65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -0,0 +1,119 @@
+/*
+ * 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.wm.shell.back;
+
+import android.os.SystemProperties;
+import android.view.RemoteAnimationTarget;
+import android.window.BackEvent;
+
+/**
+ * Helper class to record the touch location for gesture and generate back events.
+ */
+class TouchTracker {
+    private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
+            "persist.wm.debug.predictive_back_progress_threshold";
+    private static final int PROGRESS_THRESHOLD = SystemProperties
+            .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
+    private float mProgressThreshold;
+    /**
+     * Location of the latest touch event
+     */
+    private float mLatestTouchX;
+    private float mLatestTouchY;
+    private boolean mTriggerBack;
+
+    /**
+     * Location of the initial touch event of the back gesture.
+     */
+    private float mInitTouchX;
+    private float mInitTouchY;
+    private float mStartThresholdX;
+    private int mSwipeEdge;
+    private boolean mCancelled;
+
+    void update(float touchX, float touchY) {
+        /**
+         * If back was previously cancelled but the user has started swiping in the forward
+         * direction again, restart back.
+         */
+        if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT)
+                || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
+            mCancelled = false;
+            mStartThresholdX = touchX;
+        }
+        mLatestTouchX = touchX;
+        mLatestTouchY = touchY;
+    }
+
+    void setTriggerBack(boolean triggerBack) {
+        if (mTriggerBack != triggerBack && !triggerBack) {
+            mCancelled = true;
+        }
+        mTriggerBack = triggerBack;
+    }
+
+    void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
+        mInitTouchX = touchX;
+        mInitTouchY = touchY;
+        mSwipeEdge = swipeEdge;
+        mStartThresholdX = mInitTouchX;
+    }
+
+    void reset() {
+        mInitTouchX = 0;
+        mInitTouchY = 0;
+        mStartThresholdX = 0;
+        mCancelled = false;
+        mTriggerBack = false;
+        mSwipeEdge = BackEvent.EDGE_LEFT;
+    }
+
+    BackEvent createStartEvent(RemoteAnimationTarget target) {
+        return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+    }
+
+    BackEvent createProgressEvent() {
+        float progressThreshold = PROGRESS_THRESHOLD >= 0
+                ? PROGRESS_THRESHOLD : mProgressThreshold;
+        progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
+        float progress = 0;
+        // Progress is always 0 when back is cancelled and not restarted.
+        if (!mCancelled) {
+            // If back is committed, progress is the distance between the last and first touch
+            // point, divided by the max drag distance. Otherwise, it's the distance between
+            // the last touch point and the starting threshold, divided by max drag distance.
+            // The starting threshold is initially the first touch location, and updated to
+            // the location everytime back is restarted after being cancelled.
+            float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
+            float deltaX = Math.max(
+                    mSwipeEdge == BackEvent.EDGE_LEFT
+                            ? mLatestTouchX - startX
+                            : startX - mLatestTouchX,
+                    0);
+            progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1);
+        }
+        return createProgressEvent(progress);
+    }
+
+    BackEvent createProgressEvent(float progress) {
+        return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+    }
+
+    public void setProgressThreshold(float progressThreshold) {
+        mProgressThreshold = progressThreshold;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
index d6803e8..d3a9a67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
@@ -52,7 +52,7 @@
             userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
         }
         Bitmap userBadgedBitmap = createIconBitmap(
-                userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW);
+                userBadgedAppIcon, 1, MODE_WITH_SHADOW);
         return createIconBitmap(userBadgedBitmap);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 725b205..1fd91de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -24,6 +24,7 @@
 import static android.view.View.VISIBLE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 
+import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
@@ -37,7 +38,6 @@
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
-import static com.android.wm.shell.floating.FloatingTasksController.SHOW_FLOATING_TASKS_AS_BUBBLES;
 
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
@@ -150,6 +150,9 @@
 
     private final ShellExecutor mBackgroundExecutor;
 
+    // Whether or not we should show bubbles pinned at the bottom of the screen.
+    private boolean mIsBubbleBarEnabled;
+
     private BubbleLogger mLogger;
     private BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
@@ -210,7 +213,6 @@
     /** Drag and drop controller to register listener for onDragStarted. */
     private DragAndDropController mDragAndDropController;
 
-  
     public BubbleController(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
@@ -526,6 +528,12 @@
         mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
     }
 
+    // TODO(b/256873975): Should pass this into the constructor once flags are available to shell.
+    /** Sets whether the bubble bar is enabled (i.e. bubbles pinned to bottom on large screens). */
+    public void setBubbleBarEnabled(boolean enabled) {
+        mIsBubbleBarEnabled = enabled;
+    }
+
     /** Whether this userId belongs to the current user. */
     private boolean isCurrentProfile(int userId) {
         return userId == UserHandle.USER_ALL
@@ -591,7 +599,8 @@
             }
             mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
         }
-        if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubblePositioner.isLargeScreen()) {
+
+        if (mIsBubbleBarEnabled && mBubblePositioner.isLargeScreen()) {
             mBubblePositioner.setUsePinnedLocation(true);
         } else {
             mBubblePositioner.setUsePinnedLocation(false);
@@ -671,10 +680,18 @@
             return;
         }
 
+        mAddedToWindowManager = false;
+        // Put on background for this binder call, was causing jank
+        mBackgroundExecutor.execute(() -> {
+            try {
+                mContext.unregisterReceiver(mBroadcastReceiver);
+            } catch (IllegalArgumentException e) {
+                // Not sure if this happens in production, but was happening in tests
+                // (b/253647225)
+                e.printStackTrace();
+            }
+        });
         try {
-            mAddedToWindowManager = false;
-            // Put on background for this binder call, was causing jank
-            mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
             if (mStackView != null) {
                 mWindowManager.removeView(mStackView);
                 mBubbleData.getOverflow().cleanUpExpandedState();
@@ -692,7 +709,7 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         filter.addAction(Intent.ACTION_SCREEN_OFF);
-        mContext.registerReceiver(mBroadcastReceiver, filter);
+        mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
     }
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -959,14 +976,18 @@
     }
 
     /**
-     * Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification
-     * and remain until the user dismisses the bubble or bubble stack. Only one intent bubble
-     * is supported at a time.
+     * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n
+     * otification and remain until the user dismisses the bubble or bubble stack. Only one intent
+     * bubble is supported at a time.
      *
      * @param intent the intent to display in the bubble expanded view.
      */
-    public void addAppBubble(Intent intent) {
+    public void showAppBubble(Intent intent) {
         if (intent == null || intent.getPackage() == null) return;
+
+        PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
+        if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
+
         Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
         b.setShouldAutoExpand(true);
         inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
@@ -1489,18 +1510,23 @@
         }
         PackageManager packageManager = getPackageManagerForUser(
                 context, entry.getStatusBarNotification().getUser().getIdentifier());
-        ActivityInfo info =
-                intent.getIntent().resolveActivityInfo(packageManager, 0);
+        return isResizableActivity(intent.getIntent(), packageManager, entry.getKey());
+    }
+
+    static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) {
+        if (intent == null) {
+            Log.w(TAG, "Unable to send as bubble: " + key + " null intent");
+            return false;
+        }
+        ActivityInfo info = intent.resolveActivityInfo(packageManager, 0);
         if (info == null) {
-            Log.w(TAG, "Unable to send as bubble, "
-                    + entry.getKey() + " couldn't find activity info for intent: "
-                    + intent);
+            Log.w(TAG, "Unable to send as bubble: " + key
+                    + " couldn't find activity info for intent: " + intent);
             return false;
         }
         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
-            Log.w(TAG, "Unable to send as bubble, "
-                    + entry.getKey() + " activity is not resizable for intent: "
-                    + intent);
+            Log.w(TAG, "Unable to send as bubble: " + key
+                    + " activity is not resizable for intent: " + intent);
             return false;
         }
         return true;
@@ -1674,6 +1700,13 @@
         }
 
         @Override
+        public void showAppBubble(Intent intent) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.showAppBubble(intent);
+            });
+        }
+
+        @Override
         public boolean handleDismissalInterception(BubbleEntry entry,
                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
                 Executor callbackExecutor) {
@@ -1784,6 +1817,13 @@
         }
 
         @Override
+        public void setBubbleBarEnabled(boolean enabled) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.setBubbleBarEnabled(enabled);
+            });
+        }
+
+        @Override
         public void onNotificationPanelExpandedChanged(boolean expanded) {
             mMainExecutor.execute(
                     () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
index 5dab8a0..4ded3ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
@@ -79,6 +79,6 @@
                 true /* shrinkNonAdaptiveIcons */,
                 null /* outscale */,
                 outScale);
-        return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW);
+        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index f31a27d..f621351 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -83,7 +83,6 @@
 import com.android.wm.shell.bubbles.animation.ExpandedAnimationController;
 import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController;
 import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl;
-import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerStub;
 import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout;
 import com.android.wm.shell.bubbles.animation.StackAnimationController;
 import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -105,11 +104,6 @@
  */
 public class BubbleStackView extends FrameLayout
         implements ViewTreeObserver.OnComputeInternalInsetsListener {
-    /**
-     * Set to {@code true} to enable home gesture handling in bubbles
-     */
-    public static final boolean HOME_GESTURE_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true);
 
     public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
             SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
@@ -898,12 +892,8 @@
         mExpandedAnimationController = new ExpandedAnimationController(mPositioner,
                 onBubbleAnimatedOut, this);
 
-        if (HOME_GESTURE_ENABLED) {
-            mExpandedViewAnimationController =
-                    new ExpandedViewAnimationControllerImpl(context, mPositioner);
-        } else {
-            mExpandedViewAnimationController = new ExpandedViewAnimationControllerStub();
-        }
+        mExpandedViewAnimationController =
+                new ExpandedViewAnimationControllerImpl(context, mPositioner);
 
         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
 
@@ -1964,11 +1954,7 @@
 
         if (wasExpanded) {
             stopMonitoringSwipeUpGesture();
-            if (HOME_GESTURE_ENABLED) {
-                animateCollapse();
-            } else {
-                animateCollapseWithScale();
-            }
+            animateCollapse();
             logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
         } else {
             animateExpansion();
@@ -1976,13 +1962,11 @@
             logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
             logBubbleEvent(mExpandedBubble,
                     FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
-            if (HOME_GESTURE_ENABLED) {
-                mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
-                    if (!notifPanelExpanded && mIsExpanded) {
-                        startMonitoringSwipeUpGesture();
-                    }
-                });
-            }
+            mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> {
+                if (!notifPanelExpanded && mIsExpanded) {
+                    startMonitoringSwipeUpGesture();
+                }
+            });
         }
         notifyExpansionChanged(mExpandedBubble, mIsExpanded);
     }
@@ -2298,106 +2282,6 @@
         mMainExecutor.executeDelayed(mDelayedAnimation, startDelay);
     }
 
-    private void animateCollapseWithScale() {
-        cancelDelayedExpandCollapseSwitchAnimations();
-
-        if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) {
-            mManageEduView.hide();
-        }
-        // Hide the menu if it's visible.
-        showManageMenu(false);
-
-        mIsExpanded = false;
-        mIsExpansionAnimating = true;
-
-        showScrim(false);
-
-        mBubbleContainer.cancelAllAnimations();
-
-        // If we were in the middle of swapping, the animating-out surface would have been scaling
-        // to zero - finish it off.
-        PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
-        mAnimatingOutSurfaceContainer.setScaleX(0f);
-        mAnimatingOutSurfaceContainer.setScaleY(0f);
-
-        // Let the expanded animation controller know that it shouldn't animate child adds/reorders
-        // since we're about to animate collapsed.
-        mExpandedAnimationController.notifyPreparingToCollapse();
-
-        mExpandedAnimationController.collapseBackToStack(
-                mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
-                /* collapseTo */,
-                () -> mBubbleContainer.setActiveController(mStackAnimationController));
-
-        int index;
-        if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) {
-            index = mBubbleData.getBubbles().size();
-        } else {
-            index = mBubbleData.getBubbles().indexOf(mExpandedBubble);
-        }
-        // Value the bubble is animating from (back into the stack).
-        final PointF p = mPositioner.getExpandedBubbleXY(index, getState());
-        if (mPositioner.showBubblesVertically()) {
-            float pivotX;
-            float pivotY = p.y + mBubbleSize / 2f;
-            if (mStackOnLeftOrWillBe) {
-                pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding;
-            } else {
-                pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding;
-            }
-            mExpandedViewContainerMatrix.setScale(
-                    1f, 1f,
-                    pivotX, pivotY);
-        } else {
-            mExpandedViewContainerMatrix.setScale(
-                    1f, 1f,
-                    p.x + mBubbleSize / 2f,
-                    p.y + mBubbleSize + mExpandedViewPadding);
-        }
-
-        mExpandedViewAlphaAnimator.reverse();
-
-        // When the animation completes, we should no longer be showing the content.
-        if (mExpandedBubble.getExpandedView() != null) {
-            mExpandedBubble.getExpandedView().setContentVisibility(false);
-        }
-
-        PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
-        PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
-                .spring(AnimatableScaleMatrix.SCALE_X,
-                        AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
-                                1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
-                        mScaleOutSpringConfig)
-                .spring(AnimatableScaleMatrix.SCALE_Y,
-                        AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
-                                1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT),
-                        mScaleOutSpringConfig)
-                .addUpdateListener((target, values) -> {
-                    mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
-                })
-                .withEndActions(() -> {
-                    final BubbleViewProvider previouslySelected = mExpandedBubble;
-                    beforeExpandedViewAnimation();
-                    if (mManageEduView != null) {
-                        mManageEduView.hide();
-                    }
-
-                    if (DEBUG_BUBBLE_STACK_VIEW) {
-                        Log.d(TAG, "animateCollapse");
-                        Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
-                                mExpandedBubble));
-                    }
-                    updateOverflowVisibility();
-                    updateZOrder();
-                    updateBadges(true /* setBadgeForCollapsedStack */);
-                    afterExpandedViewAnimation();
-                    if (previouslySelected != null) {
-                        previouslySelected.setTaskViewVisibility(false);
-                    }
-                })
-                .start();
-    }
-
     private void animateCollapse() {
         cancelDelayedExpandCollapseSwitchAnimations();
 
@@ -2579,65 +2463,6 @@
      * and clip the expanded view.
      */
     public void setImeVisible(boolean visible) {
-        if (HOME_GESTURE_ENABLED) {
-            setImeVisibleInternal(visible);
-        } else {
-            setImeVisibleWithoutClipping(visible);
-        }
-    }
-
-    private void setImeVisibleWithoutClipping(boolean visible) {
-        if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
-            // This will update the animation so the bubbles move to position for the IME
-            mExpandedAnimationController.expandFromStack(() -> {
-                updatePointerPosition(false /* forIme */);
-                afterExpandedViewAnimation();
-            } /* after */);
-            return;
-        }
-
-        if (!mIsExpanded && getBubbleCount() > 0) {
-            final float stackDestinationY =
-                    mStackAnimationController.animateForImeVisibility(visible);
-
-            // How far the stack is animating due to IME, we'll just animate the flyout by that
-            // much too.
-            final float stackDy =
-                    stackDestinationY - mStackAnimationController.getStackPosition().y;
-
-            // If the flyout is visible, translate it along with the bubble stack.
-            if (mFlyout.getVisibility() == VISIBLE) {
-                PhysicsAnimator.getInstance(mFlyout)
-                        .spring(DynamicAnimation.TRANSLATION_Y,
-                                mFlyout.getTranslationY() + stackDy,
-                                FLYOUT_IME_ANIMATION_SPRING_CONFIG)
-                        .start();
-            }
-        } else if (mPositioner.showBubblesVertically() && mIsExpanded
-                && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
-            float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
-                    getState()).y;
-            float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
-            mExpandedBubble.getExpandedView().setImeVisible(visible);
-            if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
-                mExpandedViewContainer.animate().translationY(newExpandedViewTop);
-            }
-
-            List<Animator> animList = new ArrayList();
-            for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
-                View child = mBubbleContainer.getChildAt(i);
-                float transY = mPositioner.getExpandedBubbleXY(i, getState()).y;
-                ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY);
-                animList.add(anim);
-            }
-            updatePointerPosition(true /* forIme */);
-            AnimatorSet set = new AnimatorSet();
-            set.playTogether(animList);
-            set.start();
-        }
-    }
-
-    private void setImeVisibleInternal(boolean visible) {
         if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) {
             // This will update the animation so the bubbles move to position for the IME
             mExpandedAnimationController.expandFromStack(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 7f891ec..465d1ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -22,6 +22,7 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.app.NotificationChannel;
+import android.content.Intent;
 import android.content.pm.UserInfo;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
@@ -108,6 +109,15 @@
     void expandStackAndSelectBubble(Bubble bubble);
 
     /**
+     * Adds and expands bubble that is not notification based, but instead based on an intent from
+     * the app. The intent must be explicit (i.e. include a package name or fully qualified
+     * component class name) and the activity for it should be resizable.
+     *
+     * @param intent the intent to populate the bubble.
+     */
+    void showAppBubble(Intent intent);
+
+    /**
      * @return a bubble that matches the provided shortcutId, if one exists.
      */
     @Nullable
@@ -232,6 +242,11 @@
      */
     void onUserRemoved(int removedUserId);
 
+    /**
+     * Sets whether bubble bar should be enabled or not.
+     */
+    void setBubbleBarEnabled(boolean enabled);
+
     /** Listener to find out about stack expansion / collapse events. */
     interface BubbleExpandListener {
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index b91062f..33629f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -20,7 +20,6 @@
 
 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
 import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
-import static com.android.wm.shell.bubbles.BubbleStackView.HOME_GESTURE_ENABLED;
 
 import android.content.res.Resources;
 import android.graphics.Path;
@@ -81,11 +80,6 @@
             new PhysicsAnimator.SpringConfig(
                     EXPAND_COLLAPSE_ANIM_STIFFNESS, SpringForce.DAMPING_RATIO_NO_BOUNCY);
 
-    private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfigWithoutHomeGesture =
-            new PhysicsAnimator.SpringConfig(
-                    EXPAND_COLLAPSE_ANIM_STIFFNESS_WITHOUT_HOME_GESTURE,
-                    SpringForce.DAMPING_RATIO_NO_BOUNCY);
-
     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
     private float mStackOffsetPx;
     /** Size of each bubble. */
@@ -307,14 +301,8 @@
                     (firstBubbleLeads && index == 0)
                             || (!firstBubbleLeads && index == mLayout.getChildCount() - 1);
 
-            Interpolator interpolator;
-            if (HOME_GESTURE_ENABLED) {
-                // When home gesture is enabled, we use a different animation timing for collapse
-                interpolator = expanding
-                        ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE;
-            } else {
-                interpolator = Interpolators.LINEAR;
-            }
+            Interpolator interpolator = expanding
+                    ? Interpolators.EMPHASIZED_ACCELERATE : Interpolators.EMPHASIZED_DECELERATE;
 
             animation
                     .followAnimatedTargetAlongPath(
@@ -564,16 +552,10 @@
             finishRemoval.run();
             mOnBubbleAnimatedOutAction.run();
         } else {
-            PhysicsAnimator.SpringConfig springConfig;
-            if (HOME_GESTURE_ENABLED) {
-                springConfig = mAnimateOutSpringConfig;
-            } else {
-                springConfig = mAnimateOutSpringConfigWithoutHomeGesture;
-            }
             PhysicsAnimator.getInstance(child)
                     .spring(DynamicAnimation.ALPHA, 0f)
-                    .spring(DynamicAnimation.SCALE_X, 0f, springConfig)
-                    .spring(DynamicAnimation.SCALE_Y, 0f, springConfig)
+                    .spring(DynamicAnimation.SCALE_X, 0f, mAnimateOutSpringConfig)
+                    .spring(DynamicAnimation.SCALE_Y, 0f, mAnimateOutSpringConfig)
                     .withEndActions(finishRemoval, mOnBubbleAnimatedOutAction)
                     .start();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java
deleted file mode 100644
index bb8a3aa..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerStub.java
+++ /dev/null
@@ -1,57 +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.wm.shell.bubbles.animation;
-
-import com.android.wm.shell.bubbles.BubbleExpandedView;
-
-/**
- * Stub implementation {@link ExpandedViewAnimationController} that does not animate the
- * {@link BubbleExpandedView}
- */
-public class ExpandedViewAnimationControllerStub implements ExpandedViewAnimationController {
-    @Override
-    public void setExpandedView(BubbleExpandedView expandedView) {
-    }
-
-    @Override
-    public void updateDrag(float distance) {
-    }
-
-    @Override
-    public void setSwipeVelocity(float velocity) {
-    }
-
-    @Override
-    public boolean shouldCollapse() {
-        return false;
-    }
-
-    @Override
-    public void animateCollapse(Runnable startStackCollapse, Runnable after) {
-    }
-
-    @Override
-    public void animateBackToExpanded() {
-    }
-
-    @Override
-    public void animateForImeVisibilityChange(boolean visible) {
-    }
-
-    @Override
-    public void reset() {
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 266cf29..d9b4f47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,13 +20,12 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.IDisplayWindowInsetsController;
@@ -34,16 +33,17 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManagerGlobal;
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.internal.view.IInputMethodManager;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
@@ -114,7 +114,7 @@
         }
         if (mDisplayController.getDisplayLayout(displayId).rotation()
                 != pd.mRotation && isImeShowing(displayId)) {
-            pd.startAnimation(true, false /* forceRestart */);
+            pd.startAnimation(true, false /* forceRestart */, null /* statsToken */);
         }
     }
 
@@ -209,7 +209,7 @@
     public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener {
         final int mDisplayId;
         final InsetsState mInsetsState = new InsetsState();
-        final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         InsetsSourceControl mImeSourceControl = null;
         int mAnimationDirection = DIRECTION_NONE;
         ValueAnimator mAnimation = null;
@@ -246,7 +246,7 @@
             mInsetsState.set(insetsState, true /* copySources */);
             if (mImeShowing && !newFrame.equals(oldFrame) && newSource.isVisible()) {
                 if (DEBUG) Slog.d(TAG, "insetsChanged when IME showing, restart animation");
-                startAnimation(mImeShowing, true /* forceRestart */);
+                startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
             }
         }
 
@@ -282,7 +282,7 @@
                         !haveSameLeash(mImeSourceControl, imeSourceControl);
                 if (mAnimation != null) {
                     if (positionChanged) {
-                        startAnimation(mImeShowing, true /* forceRestart */);
+                        startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */);
                     }
                 } else {
                     if (leashChanged) {
@@ -314,26 +314,27 @@
         }
 
         @Override
-        public void showInsets(int types, boolean fromIme) {
+        public void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
             if (DEBUG) Slog.d(TAG, "Got showInsets for ime");
-            startAnimation(true /* show */, false /* forceRestart */);
+            startAnimation(true /* show */, false /* forceRestart */, statsToken);
         }
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             if ((types & WindowInsets.Type.ime()) == 0) {
                 return;
             }
             if (DEBUG) Slog.d(TAG, "Got hideInsets for ime");
-            startAnimation(false /* show */, false /* forceRestart */);
+            startAnimation(false /* show */, false /* forceRestart */, statsToken);
         }
 
         @Override
-        public void topFocusedWindowChanged(ComponentName component,
-                InsetsVisibilities requestedVisibilities) {
+        public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) {
             // Do nothing
         }
 
@@ -342,10 +343,12 @@
          */
         private void setVisibleDirectly(boolean visible) {
             mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible);
-            mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible);
+            mRequestedVisibleTypes = visible
+                    ? mRequestedVisibleTypes | WindowInsets.Type.ime()
+                    : mRequestedVisibleTypes & ~WindowInsets.Type.ime();
             try {
-                mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId,
-                        mRequestedVisibilities);
+                mWmService.updateDisplayWindowRequestedVisibleTypes(mDisplayId,
+                        mRequestedVisibleTypes);
             } catch (RemoteException e) {
             }
         }
@@ -368,9 +371,11 @@
                     .navBarFrameHeight();
         }
 
-        private void startAnimation(final boolean show, final boolean forceRestart) {
+        private void startAnimation(final boolean show, final boolean forceRestart,
+                @Nullable ImeTracker.Token statsToken) {
             final InsetsSource imeSource = mInsetsState.getSource(InsetsState.ITYPE_IME);
             if (imeSource == null || mImeSourceControl == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
                 return;
             }
             final Rect newFrame = imeSource.getFrame();
@@ -391,8 +396,9 @@
                         + (mAnimationDirection == DIRECTION_SHOW ? "SHOW"
                         : (mAnimationDirection == DIRECTION_HIDE ? "HIDE" : "NONE")));
             }
-            if (!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show)
+            if ((!forceRestart && (mAnimationDirection == DIRECTION_SHOW && show))
                     || (mAnimationDirection == DIRECTION_HIDE && !show)) {
+                ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
                 return;
             }
             boolean seek = false;
@@ -436,8 +442,11 @@
                 mTransactionPool.release(t);
             });
             mAnimation.setInterpolator(INTERPOLATOR);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
             mAnimation.addListener(new AnimatorListenerAdapter() {
                 private boolean mCancelled = false;
+                @Nullable
+                private final ImeTracker.Token mStatsToken = statsToken;
 
                 @Override
                 public void onAnimationStart(Animator animation) {
@@ -456,6 +465,8 @@
                             : 1.f;
                     t.setAlpha(mImeSourceControl.getLeash(), alpha);
                     if (mAnimationDirection == DIRECTION_SHOW) {
+                        ImeTracker.get().onProgress(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                         t.show(mImeSourceControl.getLeash());
                     }
                     t.apply();
@@ -477,8 +488,16 @@
                     }
                     dispatchEndPositioning(mDisplayId, mCancelled, t);
                     if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
+                        ImeTracker.get().onProgress(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                         t.hide(mImeSourceControl.getLeash());
                         removeImeSurface();
+                        ImeTracker.get().onHidden(mStatsToken);
+                    } else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
+                        ImeTracker.get().onShown(mStatsToken);
+                    } else if (mCancelled) {
+                        ImeTracker.get().onCancelled(mStatsToken,
+                                ImeTracker.PHASE_WM_ANIMATION_RUNNING);
                     }
                     t.apply();
                     mTransactionPool.release(t);
@@ -514,16 +533,10 @@
     }
 
     void removeImeSurface() {
-        final IInputMethodManager imms = getImms();
-        if (imms != null) {
-            try {
-                // Remove the IME surface to make the insets invisible for
-                // non-client controlled insets.
-                imms.removeImeSurface();
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to remove IME surface.", e);
-            }
-        }
+        // Remove the IME surface to make the insets invisible for
+        // non-client controlled insets.
+        InputMethodManagerGlobal.removeImeSurface(
+                e -> Slog.e(TAG, "Failed to remove IME surface.", e));
     }
 
     /**
@@ -597,11 +610,6 @@
         }
     }
 
-    public IInputMethodManager getImms() {
-        return IInputMethodManager.Stub.asInterface(
-                ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
-    }
-
     private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) {
         if (a == b) {
             return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 90a01f8..8759301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.common;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -24,7 +25,8 @@
 import android.view.IWindowManager;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 import androidx.annotation.BinderThread;
 
@@ -156,34 +158,40 @@
             }
         }
 
-        private void showInsets(int types, boolean fromIme) {
+        private void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
             if (listeners == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
                 return;
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
             for (OnInsetsChangedListener listener : listeners) {
-                listener.showInsets(types, fromIme);
+                listener.showInsets(types, fromIme, statsToken);
             }
         }
 
-        private void hideInsets(int types, boolean fromIme) {
+        private void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
             if (listeners == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
                 return;
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROLLER);
             for (OnInsetsChangedListener listener : listeners) {
-                listener.hideInsets(types, fromIme);
+                listener.hideInsets(types, fromIme, statsToken);
             }
         }
 
         private void topFocusedWindowChanged(ComponentName component,
-                InsetsVisibilities requestedVisibilities) {
+                @InsetsType int requestedVisibleTypes) {
             CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
             if (listeners == null) {
                 return;
             }
             for (OnInsetsChangedListener listener : listeners) {
-                listener.topFocusedWindowChanged(component, requestedVisibilities);
+                listener.topFocusedWindowChanged(component, requestedVisibleTypes);
             }
         }
 
@@ -192,9 +200,9 @@
                 extends IDisplayWindowInsetsController.Stub {
             @Override
             public void topFocusedWindowChanged(ComponentName component,
-                    InsetsVisibilities requestedVisibilities) throws RemoteException {
+                    @InsetsType int requestedVisibleTypes) throws RemoteException {
                 mMainExecutor.execute(() -> {
-                    PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities);
+                    PerDisplay.this.topFocusedWindowChanged(component, requestedVisibleTypes);
                 });
             }
 
@@ -214,16 +222,18 @@
             }
 
             @Override
-            public void showInsets(int types, boolean fromIme) throws RemoteException {
+            public void showInsets(@InsetsType int types, boolean fromIme,
+                    @Nullable ImeTracker.Token statsToken) throws RemoteException {
                 mMainExecutor.execute(() -> {
-                    PerDisplay.this.showInsets(types, fromIme);
+                    PerDisplay.this.showInsets(types, fromIme, statsToken);
                 });
             }
 
             @Override
-            public void hideInsets(int types, boolean fromIme) throws RemoteException {
+            public void hideInsets(@InsetsType int types, boolean fromIme,
+                    @Nullable ImeTracker.Token statsToken) throws RemoteException {
                 mMainExecutor.execute(() -> {
-                    PerDisplay.this.hideInsets(types, fromIme);
+                    PerDisplay.this.hideInsets(types, fromIme, statsToken);
                 });
             }
         }
@@ -239,11 +249,13 @@
         /**
          * Called when top focused window changes to determine whether or not to take over insets
          * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
+         *
          * @param component The application component that is open in the top focussed window.
-         * @param requestedVisibilities The insets visibilities requested by the focussed window.
+         * @param requestedVisibleTypes The {@link InsetsType} requested visible by the focused
+         *                              window.
          */
         default void topFocusedWindowChanged(ComponentName component,
-                InsetsVisibilities requestedVisibilities) {}
+                @InsetsType int requestedVisibleTypes) {}
 
         /**
          * Called when the window insets configuration has changed.
@@ -259,17 +271,23 @@
         /**
          * Called when a set of insets source window should be shown by policy.
          *
-         * @param types internal insets types (WindowInsets.Type.InsetsType) to show
+         * @param types {@link InsetsType} to show
          * @param fromIme true if this request originated from IME (InputMethodService).
+         * @param statsToken the token tracking the current IME show request
+         *                   or {@code null} otherwise.
          */
-        default void showInsets(int types, boolean fromIme) {}
+        default void showInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {}
 
         /**
          * Called when a set of insets source window should be hidden by policy.
          *
-         * @param types internal insets types (WindowInsets.Type.InsetsType) to hide
+         * @param types {@link InsetsType} to hide
          * @param fromIme true if this request originated from IME (InputMethodService).
+         * @param statsToken the token tracking the current IME hide request
+         *                   or {@code null} otherwise.
          */
-        default void hideInsets(int types, boolean fromIme) {}
+        default void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {}
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e270edb..af13bf5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -19,6 +19,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Region;
@@ -46,6 +47,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -351,10 +353,10 @@
                 InsetsSourceControl[] activeControls) {}
 
         @Override
-        public void showInsets(int types, boolean fromIme) {}
+        public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {}
+        public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
 
         @Override
         public void moved(int newX, int newY) {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 8bc16bc..1474754 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -46,8 +46,10 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 /**
  * Divider for multi window splits.
@@ -364,8 +366,11 @@
         mViewHost.relayout(lp);
     }
 
-    void setInteractive(boolean interactive) {
+    void setInteractive(boolean interactive, String from) {
         if (interactive == mInteractive) return;
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive",
+                from);
         mInteractive = interactive;
         releaseTouching();
         mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
new file mode 100644
index 0000000..7237d2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-modules splitscreen owner
+chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 74f8bf9..6e116b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -48,6 +48,7 @@
 
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
+import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.SurfaceUtils;
 
 import java.util.function.Consumer;
@@ -74,10 +75,14 @@
 
     private boolean mShown;
     private boolean mIsResizing;
-    private Rect mBounds = new Rect();
+    private final Rect mBounds = new Rect();
+    private final Rect mResizingBounds = new Rect();
+    private final Rect mTempRect = new Rect();
     private ValueAnimator mFadeAnimator;
 
     private int mIconSize;
+    private int mOffsetX;
+    private int mOffsetY;
 
     public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
             SurfaceSession surfaceSession) {
@@ -158,7 +163,8 @@
 
     /** Showing resizing hint. */
     public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
-            Rect sideBounds, SurfaceControl.Transaction t) {
+            Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
+            boolean immediately) {
         if (mResizingIconView == null) {
             return;
         }
@@ -167,11 +173,14 @@
             mIsResizing = true;
             mBounds.set(newBounds);
         }
+        mResizingBounds.set(newBounds);
+        mOffsetX = offsetX;
+        mOffsetY = offsetY;
 
         final boolean show =
                 newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height();
-        final boolean animate = show != mShown;
-        if (animate && mFadeAnimator != null && mFadeAnimator.isRunning()) {
+        final boolean update = show != mShown;
+        if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) {
             // If we need to animate and animator still running, cancel it before we ensure both
             // background and icon surfaces are non null for next animation.
             mFadeAnimator.cancel();
@@ -184,7 +193,7 @@
                     .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
         }
 
-        if (mGapBackgroundLeash == null) {
+        if (mGapBackgroundLeash == null && !immediately) {
             final boolean isLandscape = newBounds.height() == sideBounds.height();
             final int left = isLandscape ? mBounds.width() : 0;
             final int top = isLandscape ? 0 : mBounds.height();
@@ -213,19 +222,50 @@
                 newBounds.width() / 2 - mIconSize / 2,
                 newBounds.height() / 2 - mIconSize / 2);
 
-        if (animate) {
-            startFadeAnimation(show, null /* finishedConsumer */);
+        if (update) {
+            if (immediately) {
+                t.setVisibility(mBackgroundLeash, show);
+                t.setVisibility(mIconLeash, show);
+            } else {
+                startFadeAnimation(show, null /* finishedConsumer */);
+            }
             mShown = show;
         }
     }
 
     /** Stops showing resizing hint. */
     public void onResized(SurfaceControl.Transaction t) {
+        if (!mShown && mIsResizing) {
+            mTempRect.set(mResizingBounds);
+            mTempRect.offsetTo(-mOffsetX, -mOffsetY);
+            final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t,
+                    mHostLeash, mTempRect, Integer.MAX_VALUE - 1);
+
+            final SurfaceControl.Transaction animT = new SurfaceControl.Transaction();
+            final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
+            va.addUpdateListener(valueAnimator -> {
+                final float progress = (float) valueAnimator.getAnimatedValue();
+                animT.setAlpha(screenshot, progress);
+                animT.apply();
+            });
+            va.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) {
+                    animT.remove(screenshot);
+                    animT.apply();
+                    animT.close();
+                }
+            });
+            va.start();
+        }
+
         if (mResizingIconView == null) {
             return;
         }
 
         mIsResizing = false;
+        mOffsetX = 0;
+        mOffsetY = 0;
         if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
             if (!mShown) {
                 // If fade-out animation is running, just add release callback to it.
@@ -285,10 +325,12 @@
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
                 if (show) {
-                    animT.show(mBackgroundLeash).show(mIconLeash).show(mGapBackgroundLeash).apply();
-                } else {
-                    animT.hide(mGapBackgroundLeash).apply();
+                    animT.show(mBackgroundLeash).show(mIconLeash);
                 }
+                if (mGapBackgroundLeash != null) {
+                    animT.setVisibility(mGapBackgroundLeash, show);
+                }
+                animT.apply();
             }
 
             @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index c2ad1a9..ec9e6f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -69,6 +69,7 @@
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
 
 import java.io.PrintWriter;
+import java.util.function.Consumer;
 
 /**
  * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
@@ -82,12 +83,12 @@
 
     private static final int FLING_RESIZE_DURATION = 250;
     private static final int FLING_SWITCH_DURATION = 350;
-    private static final int FLING_ENTER_DURATION = 350;
-    private static final int FLING_EXIT_DURATION = 350;
+    private static final int FLING_ENTER_DURATION = 450;
+    private static final int FLING_EXIT_DURATION = 450;
 
-    private final int mDividerWindowWidth;
-    private final int mDividerInsets;
-    private final int mDividerSize;
+    private int mDividerWindowWidth;
+    private int mDividerInsets;
+    private int mDividerSize;
 
     private final Rect mTempRect = new Rect();
     private final Rect mRootBounds = new Rect();
@@ -130,6 +131,7 @@
         mContext = context.createConfigurationContext(configuration);
         mOrientation = configuration.orientation;
         mRotation = configuration.windowConfiguration.getRotation();
+        mDensity = configuration.densityDpi;
         mSplitLayoutHandler = splitLayoutHandler;
         mDisplayImeController = displayImeController;
         mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration,
@@ -138,24 +140,22 @@
         mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId());
         mSurfaceEffectPolicy = new ResizingEffectPolicy(parallaxType);
 
-        final Resources resources = context.getResources();
-        mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width);
-        mDividerInsets = getDividerInsets(resources, context.getDisplay());
-        mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
+        updateDividerConfig(mContext);
 
         mRootBounds.set(configuration.windowConfiguration.getBounds());
         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
         resetDividerPosition();
 
-        mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
+        mDimNonImeSide = mContext.getResources().getBoolean(R.bool.config_dimNonImeAttachedSide);
 
         updateInvisibleRect();
     }
 
-    private int getDividerInsets(Resources resources, Display display) {
+    private void updateDividerConfig(Context context) {
+        final Resources resources = context.getResources();
+        final Display display = context.getDisplay();
         final int dividerInset = resources.getDimensionPixelSize(
                 com.android.internal.R.dimen.docked_stack_divider_insets);
-
         int radius = 0;
         RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
@@ -166,7 +166,9 @@
         corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
         radius = corner != null ? Math.max(radius, corner.getRadius()) : radius;
 
-        return Math.max(dividerInset, radius);
+        mDividerInsets = Math.max(dividerInset, radius);
+        mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width);
+        mDividerWindowWidth = mDividerSize + 2 * mDividerInsets;
     }
 
     /** Gets bounds of the primary split with screen based coordinate. */
@@ -308,6 +310,7 @@
         mRotation = rotation;
         mDensity = density;
         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
+        updateDividerConfig(mContext);
         initDividerPosition(mTempRect);
         updateInvisibleRect();
 
@@ -445,7 +448,8 @@
      */
     void updateDivideBounds(int position) {
         updateBounds(position);
-        mSplitLayoutHandler.onLayoutSizeChanging(this);
+        mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x,
+                mSurfaceEffectPolicy.mParallaxOffset.y);
     }
 
     void setDividePosition(int position, boolean applyLayoutChange) {
@@ -597,9 +601,9 @@
         animator.start();
     }
 
-    /** Swich both surface position with animation. */
+    /** Switch both surface position with animation. */
     public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1,
-            SurfaceControl leash2, Runnable finishCallback) {
+            SurfaceControl leash2, Consumer<Rect> finishCallback) {
         final boolean isLandscape = isLandscape();
         final Rect insets = getDisplayInsets(mContext);
         insets.set(isLandscape ? insets.left : 0, isLandscape ? 0 : insets.top,
@@ -617,18 +621,13 @@
         distBounds1.offset(-mRootBounds.left, -mRootBounds.top);
         distBounds2.offset(-mRootBounds.left, -mRootBounds.top);
         distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top);
-        // DO NOT move to insets area for smooth animation.
-        distBounds1.set(distBounds1.left, distBounds1.top,
-                distBounds1.right - insets.right, distBounds1.bottom - insets.bottom);
-        distBounds2.set(distBounds2.left + insets.left, distBounds2.top + insets.top,
-                distBounds2.right, distBounds2.bottom);
 
         ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1,
-                false /* alignStart */);
+                -insets.left, -insets.top);
         ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2,
-                true /* alignStart */);
+                insets.left, insets.top);
         ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(),
-                distDividerBounds, true /* alignStart */);
+                distDividerBounds, 0 /* offsetX */, 0 /* offsetY */);
 
         AnimatorSet set = new AnimatorSet();
         set.playTogether(animator1, animator2, animator3);
@@ -638,14 +637,14 @@
             public void onAnimationEnd(Animator animation) {
                 mDividePosition = dividerPos;
                 updateBounds(mDividePosition);
-                finishCallback.run();
+                finishCallback.accept(insets);
             }
         });
         set.start();
     }
 
     private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash,
-            Rect start, Rect end, boolean alignStart) {
+            Rect start, Rect end, float offsetX, float offsetY) {
         Rect tempStart = new Rect(start);
         Rect tempEnd = new Rect(end);
         final float diffX = tempEnd.left - tempStart.left;
@@ -661,15 +660,15 @@
             final float distY = tempStart.top + scale * diffY;
             final int width = (int) (tempStart.width() + scale * diffWidth);
             final int height = (int) (tempStart.height() + scale * diffHeight);
-            if (alignStart) {
+            if (offsetX == 0 && offsetY == 0) {
                 t.setPosition(leash, distX, distY);
                 t.setWindowCrop(leash, width, height);
             } else {
-                final int offsetX = width - tempStart.width();
-                final int offsetY = height - tempStart.height();
-                t.setPosition(leash, distX + offsetX, distY + offsetY);
+                final int diffOffsetX = (int) (scale * offsetX);
+                final int diffOffsetY = (int) (scale * offsetY);
+                t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY);
                 mTempRect.set(0, 0, width, height);
-                mTempRect.offsetTo(-offsetX, -offsetY);
+                mTempRect.offsetTo(-diffOffsetX, -diffOffsetY);
                 t.setCrop(leash, mTempRect);
             }
             t.apply();
@@ -813,7 +812,7 @@
          * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
          * SurfaceControl, SurfaceControl, boolean)
          */
-        void onLayoutSizeChanging(SplitLayout layout);
+        void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY);
 
         /**
          * Calls when finish resizing the split bounds.
@@ -1094,7 +1093,8 @@
             // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough
             // because DividerView won't receive onImeVisibilityChanged callback after it being
             // re-inflated.
-            mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus);
+            mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus,
+                    "onImeStartPositioning");
 
             return needOffset ? IME_ANIMATION_NO_ALPHA : 0;
         }
@@ -1120,7 +1120,7 @@
             // Restore the split layout when wm-shell is not controlling IME insets anymore.
             if (!controlling && mImeShown) {
                 reset();
-                mSplitWindowManager.setInteractive(true);
+                mSplitWindowManager.setInteractive(true, "onImeControlTargetChanged");
                 mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this);
                 mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 7fea237..5397552 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -167,9 +167,9 @@
         }
     }
 
-    void setInteractive(boolean interactive) {
+    void setInteractive(boolean interactive, String from) {
         if (mDividerView == null) return;
-        mDividerView.setInteractive(interactive);
+        mDividerView.setInteractive(interactive, from);
     }
 
     View getDividerView() {
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..962be9d 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
@@ -24,7 +24,6 @@
 import android.os.Handler;
 import android.os.SystemProperties;
 import android.view.IWindowManager;
-import android.view.WindowManager;
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.launcher3.icons.IconProvider;
@@ -65,8 +64,6 @@
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
-import com.android.wm.shell.floating.FloatingTasks;
-import com.android.wm.shell.floating.FloatingTasksController;
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.fullscreen.FullscreenTaskListener;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
@@ -263,13 +260,12 @@
             ShellInit shellInit,
             ShellController shellController,
             @ShellMainThread ShellExecutor shellExecutor,
-            @ShellBackgroundThread Handler backgroundHandler,
-            Transitions transitions
+            @ShellBackgroundThread Handler backgroundHandler
     ) {
         if (BackAnimationController.IS_ENABLED) {
             return Optional.of(
                     new BackAnimationController(shellInit, shellController, shellExecutor,
-                            backgroundHandler, context, transitions));
+                            backgroundHandler, context));
         }
         return Optional.empty();
     }
@@ -507,6 +503,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);
     }
@@ -572,47 +572,6 @@
     }
 
     //
-    // Floating tasks
-    //
-
-    @WMSingleton
-    @Provides
-    static Optional<FloatingTasks> provideFloatingTasks(
-            Optional<FloatingTasksController> floatingTaskController) {
-        return floatingTaskController.map((controller) -> controller.asFloatingTasks());
-    }
-
-    @WMSingleton
-    @Provides
-    static Optional<FloatingTasksController> provideFloatingTasksController(Context context,
-            ShellInit shellInit,
-            ShellController shellController,
-            ShellCommandHandler shellCommandHandler,
-            Optional<BubbleController> bubbleController,
-            WindowManager windowManager,
-            ShellTaskOrganizer organizer,
-            TaskViewTransitions taskViewTransitions,
-            @ShellMainThread ShellExecutor mainExecutor,
-            @ShellBackgroundThread ShellExecutor bgExecutor,
-            SyncTransactionQueue syncQueue) {
-        if (FloatingTasksController.FLOATING_TASKS_ENABLED) {
-            return Optional.of(new FloatingTasksController(context,
-                    shellInit,
-                    shellController,
-                    shellCommandHandler,
-                    bubbleController,
-                    windowManager,
-                    organizer,
-                    taskViewTransitions,
-                    mainExecutor,
-                    bgExecutor,
-                    syncQueue));
-        } else {
-            return Optional.empty();
-        }
-    }
-
-    //
     // Starting window
     //
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 44a467f..cbd544c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,9 +18,21 @@
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to interact with desktop mode feature in shell.
  */
 @ExternalThread
 public interface DesktopMode {
+
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor);
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index b96facf..abc4024 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -16,10 +16,14 @@
 
 package com.android.wm.shell.desktopmode;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
@@ -60,6 +64,7 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.concurrent.Executor;
 
 /**
  * Handles windowing changes when desktop mode system setting changes
@@ -132,27 +137,35 @@
         return new IDesktopModeImpl(this);
     }
 
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor) {
+        mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
 
         int displayId = mContext.getDisplayId();
 
+        ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId);
+
         WindowContainerTransaction wct = new WindowContainerTransaction();
-        // Reset freeform windowing mode that is set per task level (tasks should inherit
-        // container value)
-        wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId),
-                true /* transfer */);
-        int targetWindowingMode;
+        // Reset freeform windowing mode that is set per task level so tasks inherit it
+        clearFreeformForStandardTasks(runningTasks, wct);
         if (active) {
-            targetWindowingMode = WINDOWING_MODE_FREEFORM;
+            moveHomeBehindVisibleTasks(runningTasks, wct);
+            setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct);
         } else {
-            targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
-            // Clear any resized bounds
-            wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId),
-                    true /* transfer */);
+            clearBoundsForStandardTasks(runningTasks, wct);
+            setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct);
         }
-        prepareWindowingModeChange(wct, displayId, targetWindowingMode);
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
         } else {
@@ -160,17 +173,69 @@
         }
     }
 
-    private void prepareWindowingModeChange(WindowContainerTransaction wct,
-            int displayId, @WindowConfiguration.WindowingMode int windowingMode) {
-        DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer
-                .getDisplayAreaInfo(displayId);
+    private WindowContainerTransaction clearBoundsForStandardTasks(
+            ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks");
+        for (RunningTaskInfo taskInfo : runningTasks) {
+            if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
+                ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+                        taskInfo.token, taskInfo);
+                wct.setBounds(taskInfo.token, null);
+            }
+        }
+        return wct;
+    }
+
+    private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks,
+            WindowContainerTransaction wct) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks");
+        for (RunningTaskInfo taskInfo : runningTasks) {
+            if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
+                    && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
+                ProtoLog.v(WM_SHELL_DESKTOP_MODE,
+                        "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+                        taskInfo);
+                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+            }
+        }
+    }
+
+    private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks,
+            WindowContainerTransaction wct) {
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks");
+        RunningTaskInfo homeTask = null;
+        ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>();
+        for (RunningTaskInfo taskInfo : runningTasks) {
+            if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+                homeTask = taskInfo;
+            } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
+                    && taskInfo.isVisible()) {
+                visibleTasks.add(taskInfo);
+            }
+        }
+        if (homeTask == null) {
+            ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found");
+        } else {
+            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d",
+                    visibleTasks.size());
+            wct.reorder(homeTask.getToken(), true /* onTop */);
+            for (RunningTaskInfo task : visibleTasks) {
+                wct.reorder(task.getToken(), true /* onTop */);
+            }
+        }
+    }
+
+    private void setDisplayAreaWindowingMode(int displayId,
+            @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) {
+        DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+                displayId);
         if (displayAreaInfo == null) {
             ProtoLog.e(WM_SHELL_DESKTOP_MODE,
                     "unable to update windowing mode for display %d display not found", displayId);
             return;
         }
 
-        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE,
                 "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
                 displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
                 windowingMode);
@@ -181,7 +246,18 @@
     /**
      * Show apps on desktop
      */
-    WindowContainerTransaction showDesktopApps() {
+    void showDesktopApps() {
+        WindowContainerTransaction wct = bringDesktopAppsToFront();
+
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
+        } else {
+            mShellTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    @NonNull
+    private WindowContainerTransaction bringDesktopAppsToFront() {
         ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
         ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -197,11 +273,6 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-
-        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mShellTaskOrganizer.applyTransaction(wct);
-        }
-
         return wct;
     }
 
@@ -237,17 +308,29 @@
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
-
-        // Only do anything if we are in desktop mode and opening a task/app
-        if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+        // Only do anything if we are in desktop mode and opening a task/app in freeform
+        if (!DesktopModeStatus.isActive(mContext)) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: desktop mode not active");
             return null;
         }
+        if (request.getType() != TRANSIT_OPEN) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: only supports TRANSIT_OPEN");
+            return null;
+        }
+        if (request.getTriggerTask() == null
+                || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
+            return null;
+        }
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
 
         WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
         if (wct == null) {
             wct = new WindowContainerTransaction();
         }
-        wct.merge(showDesktopApps(), true /* transfer */);
+        wct.merge(bringDesktopAppsToFront(), true /* transfer */);
         wct.reorder(request.getTriggerTask().token, true /* onTop */);
 
         return wct;
@@ -293,7 +376,14 @@
      */
     @ExternalThread
     private final class DesktopModeImpl implements DesktopMode {
-        // Do nothing
+
+        @Override
+        public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+                Executor callbackExecutor) {
+            mMainExecutor.execute(() -> {
+                DesktopModeController.this.addListener(listener, callbackExecutor);
+            });
+        }
     }
 
     /**
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 988601c..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
@@ -16,7 +16,9 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.util.ArrayMap
 import android.util.ArraySet
+import java.util.concurrent.Executor
 
 /**
  * Keeps track of task data related to desktop mode.
@@ -30,40 +32,65 @@
      * Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
      */
     private val activeTasks = ArraySet<Int>()
-    private val listeners = ArraySet<Listener>()
+    private val visibleTasks = ArraySet<Int>()
+    private val activeTasksListeners = ArraySet<ActiveTasksListener>()
+    // Track visible tasks separately because a task may be part of the desktop but not visible.
+    private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
 
     /**
-     * Add a [Listener] to be notified of updates to the repository.
+     * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
      */
-    fun addListener(listener: Listener) {
-        listeners.add(listener)
+    fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.add(activeTasksListener)
     }
 
     /**
-     * Remove a previously registered [Listener]
+     * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
      */
-    fun removeListener(listener: Listener) {
-        listeners.remove(listener)
+    fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
+        visibleTasksListeners.put(visibleTasksListener, executor)
+        executor.execute(
+                Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) })
+    }
+
+    /**
+     * Remove a previously registered [ActiveTasksListener]
+     */
+    fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.remove(activeTasksListener)
+    }
+
+    /**
+     * Remove a previously registered [VisibleTasksListener]
+     */
+    fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
+        visibleTasksListeners.remove(visibleTasksListener)
     }
 
     /**
      * 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) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            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) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
+        return removed
     }
 
     /**
@@ -81,9 +108,43 @@
     }
 
     /**
-     * Defines interface for classes that can listen to changes in repository state.
+     * Updates whether a freeform task with this id is visible or not and notifies listeners.
      */
-    interface Listener {
-        fun onActiveTasksChanged()
+    fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
+        val prevCount: Int = visibleTasks.size
+        if (visible) {
+            visibleTasks.add(taskId)
+        } else {
+            visibleTasks.remove(taskId)
+        }
+        if (prevCount == 0 && visibleTasks.size == 1 ||
+                prevCount > 0 && visibleTasks.size == 0) {
+            for ((listener, executor) in visibleTasksListeners) {
+                executor.execute(
+                        Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) })
+            }
+        }
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for active tasks in desktop mode.
+     */
+    interface ActiveTasksListener {
+        /**
+         * Called when the active tasks change in desktop mode.
+         */
+        @JvmDefault
+        fun onActiveTasksChanged() {}
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
+     */
+    interface VisibleTasksListener {
+        /**
+         * Called when the desktop starts or stops showing freeform tasks.
+         */
+        @JvmDefault
+        fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 497a6f6..55378a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -311,7 +311,7 @@
                     animateSplitContainers(true, null /* animCompleteCallback */);
                     animateHighlight(target);
                 }
-            } else {
+            } else if (mCurrentTarget.type != target.type) {
                 // Switching between targets
                 mDropZoneView1.animateSwitch();
                 mDropZoneView2.animateSwitch();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
deleted file mode 100644
index 83a1734..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingDismissController.java
+++ /dev/null
@@ -1,259 +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.wm.shell.floating;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.view.MotionEvent;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.dynamicanimation.animation.DynamicAnimation;
-
-import com.android.wm.shell.R;
-import com.android.wm.shell.bubbles.DismissView;
-import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
-import com.android.wm.shell.floating.views.FloatingTaskLayer;
-import com.android.wm.shell.floating.views.FloatingTaskView;
-
-import java.util.Objects;
-
-/**
- * Controls a floating dismiss circle that has a 'magnetic' field around it, causing views moved
- * close to the target to be stuck to it unless moved out again.
- */
-public class FloatingDismissController {
-
-    /** Velocity required to dismiss the view without dragging it into the dismiss target. */
-    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
-    /**
-     * Max velocity that the view can be moving through the target with to stick (i.e. if it's
-     * more than this velocity, it will pass through the target.
-     */
-    private static final float STICK_TO_TARGET_MAX_X_VELOCITY = 2000f;
-    /**
-     * Percentage of the target width to use to determine if an object flung towards the target
-     * should dismiss (e.g. if target is 100px and this is set ot 2f, anything flung within a
-     * 200px-wide area around the target will be considered 'near' enough get dismissed).
-     */
-    private static final float FLING_TO_TARGET_WIDTH_PERCENT = 2f;
-    /** Minimum alpha to apply to the view being dismissed when it is in the target. */
-    private static final float DISMISS_VIEW_MIN_ALPHA = 0.6f;
-    /** Amount to scale down the view being dismissed when it is in the target. */
-    private static final float DISMISS_VIEW_SCALE_DOWN_PERCENT = 0.15f;
-
-    private Context mContext;
-    private FloatingTasksController mController;
-    private FloatingTaskLayer mParent;
-
-    private DismissView mDismissView;
-    private ValueAnimator mDismissAnimator;
-    private View mViewBeingDismissed;
-    private float mDismissSizePercent;
-    private float mDismissSize;
-
-    /**
-     * The currently magnetized object, which is being dragged and will be attracted to the magnetic
-     * dismiss target.
-     */
-    private MagnetizedObject<View> mMagnetizedObject;
-    /**
-     * The MagneticTarget instance for our circular dismiss view. This is added to the
-     * MagnetizedObject instances for the view being dragged.
-     */
-    private MagnetizedObject.MagneticTarget mMagneticTarget;
-    /** Magnet listener that handles animating and dismissing the view. */
-    private MagnetizedObject.MagnetListener mFloatingViewMagnetListener;
-
-    public FloatingDismissController(Context context, FloatingTasksController controller,
-            FloatingTaskLayer parent) {
-        mContext = context;
-        mController = controller;
-        mParent = parent;
-        updateSizes();
-        createAndAddDismissView();
-
-        mDismissAnimator = ValueAnimator.ofFloat(1f, 0f);
-        mDismissAnimator.addUpdateListener(animation -> {
-            final float value = (float) animation.getAnimatedValue();
-            if (mDismissView != null) {
-                mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f);
-                mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f);
-                final float scaleValue = Math.max(value, mDismissSizePercent);
-                mDismissView.getCircle().setScaleX(scaleValue);
-                mDismissView.getCircle().setScaleY(scaleValue);
-            }
-            if (mViewBeingDismissed != null) {
-                // TODO: alpha doesn't actually apply to taskView currently.
-                mViewBeingDismissed.setAlpha(Math.max(value, DISMISS_VIEW_MIN_ALPHA));
-                mViewBeingDismissed.setScaleX(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
-                mViewBeingDismissed.setScaleY(Math.max(value, DISMISS_VIEW_SCALE_DOWN_PERCENT));
-            }
-        });
-
-        mFloatingViewMagnetListener = new MagnetizedObject.MagnetListener() {
-            @Override
-            public void onStuckToTarget(
-                    @NonNull MagnetizedObject.MagneticTarget target) {
-                animateDismissing(/* dismissing= */ true);
-            }
-
-            @Override
-            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
-                    float velX, float velY, boolean wasFlungOut) {
-                animateDismissing(/* dismissing= */ false);
-                mParent.onUnstuckFromTarget((FloatingTaskView) mViewBeingDismissed, velX, velY,
-                        wasFlungOut);
-            }
-
-            @Override
-            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
-                doDismiss();
-            }
-        };
-    }
-
-    /** Updates all the sizes used and applies them to the {@link DismissView}. */
-    public void updateSizes() {
-        Resources res = mContext.getResources();
-        mDismissSize = res.getDimensionPixelSize(
-                R.dimen.floating_task_dismiss_circle_size);
-        final float minDismissSize = res.getDimensionPixelSize(
-                R.dimen.floating_dismiss_circle_small);
-        mDismissSizePercent = minDismissSize / mDismissSize;
-
-        if (mDismissView != null) {
-            mDismissView.updateResources();
-        }
-    }
-
-    /** Prepares the view being dragged to be magnetic. */
-    public void setUpMagneticObject(View viewBeingDragged) {
-        mViewBeingDismissed = viewBeingDragged;
-        mMagnetizedObject = getMagnetizedView(viewBeingDragged);
-        mMagnetizedObject.clearAllTargets();
-        mMagnetizedObject.addTarget(mMagneticTarget);
-        mMagnetizedObject.setMagnetListener(mFloatingViewMagnetListener);
-    }
-
-    /** Shows or hides the dismiss target. */
-    public void showDismiss(boolean show) {
-        if (show) {
-            mDismissView.show();
-        } else {
-            mDismissView.hide();
-        }
-    }
-
-    /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
-    public boolean passEventToMagnetizedObject(MotionEvent event) {
-        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
-    }
-
-    private void createAndAddDismissView() {
-        if (mDismissView != null) {
-            mParent.removeView(mDismissView);
-        }
-        mDismissView = new DismissView(mContext);
-        mDismissView.setTargetSizeResId(R.dimen.floating_task_dismiss_circle_size);
-        mDismissView.updateResources();
-        mParent.addView(mDismissView);
-
-        final float dismissRadius = mDismissSize;
-        // Save the MagneticTarget instance for the newly set up view - we'll add this to the
-        // MagnetizedObjects when the dismiss view gets shown.
-        mMagneticTarget = new MagnetizedObject.MagneticTarget(
-                mDismissView.getCircle(), (int) dismissRadius);
-    }
-
-    private MagnetizedObject<View> getMagnetizedView(View v) {
-        if (mMagnetizedObject != null
-                && Objects.equals(mMagnetizedObject.getUnderlyingObject(), v)) {
-            // Same view being dragged, we can reuse the magnetic object.
-            return mMagnetizedObject;
-        }
-        MagnetizedObject<View> magnetizedView = new MagnetizedObject<View>(
-                mContext,
-                v,
-                DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y
-        ) {
-            @Override
-            public float getWidth(@NonNull View underlyingObject) {
-                return underlyingObject.getWidth();
-            }
-
-            @Override
-            public float getHeight(@NonNull View underlyingObject) {
-                return underlyingObject.getHeight();
-            }
-
-            @Override
-            public void getLocationOnScreen(@NonNull View underlyingObject,
-                    @NonNull int[] loc) {
-                loc[0] = (int) underlyingObject.getTranslationX();
-                loc[1] = (int) underlyingObject.getTranslationY();
-            }
-        };
-        magnetizedView.setHapticsEnabled(true);
-        magnetizedView.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
-        magnetizedView.setStickToTargetMaxXVelocity(STICK_TO_TARGET_MAX_X_VELOCITY);
-        magnetizedView.setFlingToTargetWidthPercent(FLING_TO_TARGET_WIDTH_PERCENT);
-        return magnetizedView;
-    }
-
-    /** Animates the dismiss treatment on the view being dismissed. */
-    private void animateDismissing(boolean shouldDismiss) {
-        if (mViewBeingDismissed == null) {
-            return;
-        }
-        if (shouldDismiss) {
-            mDismissAnimator.removeAllListeners();
-            mDismissAnimator.start();
-        } else {
-            mDismissAnimator.removeAllListeners();
-            mDismissAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    resetDismissAnimator();
-                }
-            });
-            mDismissAnimator.reverse();
-        }
-    }
-
-    /** Actually dismisses the view. */
-    private void doDismiss() {
-        mDismissView.hide();
-        mController.removeTask();
-        resetDismissAnimator();
-        mViewBeingDismissed = null;
-    }
-
-    private void resetDismissAnimator() {
-        mDismissAnimator.removeAllListeners();
-        mDismissAnimator.cancel();
-        if (mDismissView != null) {
-            mDismissView.cancelAnimators();
-            mDismissView.getCircle().setScaleX(1f);
-            mDismissView.getCircle().setScaleY(1f);
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
deleted file mode 100644
index f86d467..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasks.java
+++ /dev/null
@@ -1,36 +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.wm.shell.floating;
-
-import android.content.Intent;
-
-import com.android.wm.shell.common.annotations.ExternalThread;
-
-/**
- * Interface to interact with floating tasks.
- */
-@ExternalThread
-public interface FloatingTasks {
-
-    /**
-     * Shows, stashes, or un-stashes the floating task depending on state:
-     * - If there is no floating task for this intent, it shows the task for the provided intent.
-     * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
-     * - If there is a floating task for this intent, and it's not stashed, this stashes it.
-     */
-    void showOrSetStashed(Intent intent);
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
deleted file mode 100644
index b3c09d3..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
+++ /dev/null
@@ -1,454 +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.wm.shell.floating;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.content.res.Configuration;
-import android.graphics.PixelFormat;
-import android.graphics.Point;
-import android.os.SystemProperties;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-import androidx.annotation.BinderThread;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.TaskViewTransitions;
-import com.android.wm.shell.bubbles.BubbleController;
-import com.android.wm.shell.common.ExternalInterfaceBinder;
-import com.android.wm.shell.common.RemoteCallable;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.floating.views.FloatingTaskLayer;
-import com.android.wm.shell.floating.views.FloatingTaskView;
-import com.android.wm.shell.sysui.ConfigurationChangeListener;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellController;
-import com.android.wm.shell.sysui.ShellInit;
-
-import java.io.PrintWriter;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Entry point for creating and managing floating tasks.
- *
- * A single window layer is added and the task(s) are displayed using a {@link FloatingTaskView}
- * within that window.
- *
- * Currently optimized for a single task. Multiple tasks are not supported.
- */
-public class FloatingTasksController implements RemoteCallable<FloatingTasksController>,
-        ConfigurationChangeListener {
-
-    private static final String TAG = FloatingTasksController.class.getSimpleName();
-
-    public static final boolean FLOATING_TASKS_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
-    public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES =
-            SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", false);
-
-    @VisibleForTesting
-    static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600;
-
-    // Only used for testing
-    private Configuration mConfig;
-    private boolean mFloatingTasksEnabledForTests;
-
-    private FloatingTaskImpl mImpl = new FloatingTaskImpl();
-    private Context mContext;
-    private ShellController mShellController;
-    private ShellCommandHandler mShellCommandHandler;
-    private @Nullable BubbleController mBubbleController;
-    private WindowManager mWindowManager;
-    private ShellTaskOrganizer mTaskOrganizer;
-    private TaskViewTransitions mTaskViewTransitions;
-    private @ShellMainThread ShellExecutor mMainExecutor;
-    // TODO: mBackgroundThread is not used but we'll probs need it eventually?
-    private @ShellBackgroundThread ShellExecutor mBackgroundThread;
-    private SyncTransactionQueue mSyncQueue;
-
-    private boolean mIsFloatingLayerAdded;
-    private FloatingTaskLayer mFloatingTaskLayer;
-    private final Point mLastPosition = new Point(-1, -1);
-
-    private Task mTask;
-
-    // Simple class to hold onto info for intent or shortcut based tasks.
-    public static class Task {
-        public int taskId = INVALID_TASK_ID;
-        @Nullable
-        public Intent intent;
-        @Nullable
-        public ShortcutInfo info;
-        @Nullable
-        public FloatingTaskView floatingView;
-    }
-
-    public FloatingTasksController(Context context,
-            ShellInit shellInit,
-            ShellController shellController,
-            ShellCommandHandler shellCommandHandler,
-            Optional<BubbleController> bubbleController,
-            WindowManager windowManager,
-            ShellTaskOrganizer organizer,
-            TaskViewTransitions transitions,
-            @ShellMainThread ShellExecutor mainExecutor,
-            @ShellBackgroundThread ShellExecutor bgExceutor,
-            SyncTransactionQueue syncTransactionQueue) {
-        mContext = context;
-        mShellController = shellController;
-        mShellCommandHandler = shellCommandHandler;
-        mBubbleController = bubbleController.get();
-        mWindowManager = windowManager;
-        mTaskOrganizer = organizer;
-        mTaskViewTransitions = transitions;
-        mMainExecutor = mainExecutor;
-        mBackgroundThread = bgExceutor;
-        mSyncQueue = syncTransactionQueue;
-        if (isFloatingTasksEnabled()) {
-            shellInit.addInitCallback(this::onInit, this);
-        }
-    }
-
-    protected void onInit() {
-        mShellController.addConfigurationChangeListener(this);
-        mShellController.addExternalInterface(KEY_EXTRA_SHELL_FLOATING_TASKS,
-                this::createExternalInterface, this);
-        mShellCommandHandler.addDumpCallback(this::dump, this);
-    }
-
-    /** Only used for testing. */
-    @VisibleForTesting
-    void setConfig(Configuration config) {
-        mConfig = config;
-    }
-
-    /** Only used for testing. */
-    @VisibleForTesting
-    void setFloatingTasksEnabled(boolean enabled) {
-        mFloatingTasksEnabledForTests = enabled;
-    }
-
-    /** Whether the floating layer is available. */
-    boolean isFloatingLayerAvailable() {
-        Configuration config = mConfig == null
-                ? mContext.getResources().getConfiguration()
-                : mConfig;
-        return config.smallestScreenWidthDp >= SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
-    }
-
-    /** Whether floating tasks are enabled.  */
-    boolean isFloatingTasksEnabled() {
-        return FLOATING_TASKS_ENABLED || mFloatingTasksEnabledForTests;
-    }
-
-    private ExternalInterfaceBinder createExternalInterface() {
-        return new IFloatingTasksImpl(this);
-    }
-
-    @Override
-    public void onThemeChanged() {
-        if (mIsFloatingLayerAdded) {
-            mFloatingTaskLayer.updateSizes();
-        }
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        // TODO: probably other stuff here to do (e.g. handle rotation)
-        if (mIsFloatingLayerAdded) {
-            mFloatingTaskLayer.updateSizes();
-        }
-    }
-
-    /** Returns false if the task shouldn't be shown. */
-    private boolean canShowTask(Intent intent) {
-        ProtoLog.d(WM_SHELL_FLOATING_APPS, "canShowTask --  %s", intent);
-        if (!isFloatingTasksEnabled() || !isFloatingLayerAvailable()) return false;
-        if (intent == null) {
-            ProtoLog.e(WM_SHELL_FLOATING_APPS, "canShowTask given null intent, doing nothing");
-            return false;
-        }
-        return true;
-    }
-
-    /** Returns true if the task was or should be shown as a bubble. */
-    private boolean maybeShowTaskAsBubble(Intent intent) {
-        if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) {
-            removeFloatingLayer();
-            if (intent.getPackage() != null) {
-                mBubbleController.addAppBubble(intent);
-                ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent);
-            } else {
-                ProtoLog.d(WM_SHELL_FLOATING_APPS,
-                        "failed to show floating task as bubble: %s; unknown package", intent);
-            }
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Shows, stashes, or un-stashes the floating task depending on state:
-     * - If there is no floating task for this intent, it shows this the provided task.
-     * - If there is a floating task for this intent, but it's stashed, this un-stashes it.
-     * - If there is a floating task for this intent, and it's not stashed, this stashes it.
-     */
-    public void showOrSetStashed(Intent intent) {
-        if (!canShowTask(intent)) return;
-        if (maybeShowTaskAsBubble(intent)) return;
-
-        addFloatingLayer();
-
-        if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
-            // The task is already added, toggle the stash state.
-            mFloatingTaskLayer.setStashed(mTask, !mTask.floatingView.isStashed());
-            return;
-        }
-
-        // If we're here it's either a new or different task
-        showNewTask(intent);
-    }
-
-    /**
-     * Shows a floating task with the provided intent.
-     * If the same task is present it will un-stash it or do nothing if it is already un-stashed.
-     * Removes any other floating tasks that might exist.
-     */
-    public void showTask(Intent intent) {
-        if (!canShowTask(intent)) return;
-        if (maybeShowTaskAsBubble(intent)) return;
-
-        addFloatingLayer();
-
-        if (isTaskAttached(mTask) && intent.filterEquals(mTask.intent)) {
-            // The task is already added, show it if it's stashed.
-            if (mTask.floatingView.isStashed()) {
-                mFloatingTaskLayer.setStashed(mTask, false);
-            }
-            return;
-        }
-        showNewTask(intent);
-    }
-
-    private void showNewTask(Intent intent) {
-        if (mTask != null && !intent.filterEquals(mTask.intent)) {
-            mFloatingTaskLayer.removeAllTaskViews();
-            mTask.floatingView.cleanUpTaskView();
-            mTask = null;
-        }
-
-        FloatingTaskView ftv = new FloatingTaskView(mContext, this);
-        ftv.createTaskView(mContext, mTaskOrganizer, mTaskViewTransitions, mSyncQueue);
-
-        mTask = new Task();
-        mTask.floatingView = ftv;
-        mTask.intent = intent;
-
-        // Add & start the task.
-        mFloatingTaskLayer.addTask(mTask);
-        ProtoLog.d(WM_SHELL_FLOATING_APPS, "showNewTask, startingIntent: %s", intent);
-        mTask.floatingView.startTask(mMainExecutor, mTask);
-    }
-
-    /**
-     * Removes the task and cleans up the view.
-     */
-    public void removeTask() {
-        if (mTask != null) {
-            ProtoLog.d(WM_SHELL_FLOATING_APPS, "Removing task with id=%d", mTask.taskId);
-
-            if (mTask.floatingView != null) {
-                // TODO: animate it
-                mFloatingTaskLayer.removeView(mTask.floatingView);
-                mTask.floatingView.cleanUpTaskView();
-            }
-            removeFloatingLayer();
-        }
-    }
-
-    /**
-     * Whether there is a floating task and if it is stashed.
-     */
-    public boolean isStashed() {
-        return isTaskAttached(mTask) && mTask.floatingView.isStashed();
-    }
-
-    /**
-     * If a floating task exists, this sets whether it is stashed and animates if needed.
-     */
-    public void setStashed(boolean shouldStash) {
-        if (mTask != null && mTask.floatingView != null && mIsFloatingLayerAdded) {
-            mFloatingTaskLayer.setStashed(mTask, shouldStash);
-        }
-    }
-
-    /**
-     * Saves the last position the floating task was in so that it can be put there again.
-     */
-    public void setLastPosition(int x, int y) {
-        mLastPosition.set(x, y);
-    }
-
-    /**
-     * Returns the last position the floating task was in.
-     */
-    public Point getLastPosition() {
-        return mLastPosition;
-    }
-
-    /**
-     * Whether the provided task has a view that's attached to the floating layer.
-     */
-    private boolean isTaskAttached(Task t) {
-        return t != null && t.floatingView != null
-                && mIsFloatingLayerAdded
-                && mFloatingTaskLayer.getTaskViewCount() > 0
-                && Objects.equals(mFloatingTaskLayer.getFirstTaskView(), t.floatingView);
-    }
-
-    // TODO: when this is added, if there are bubbles, they get hidden? Is only one layer of this
-    //  type allowed? Bubbles & floating tasks should probably be in the same layer to reduce
-    //  # of windows.
-    private void addFloatingLayer() {
-        if (mIsFloatingLayerAdded) {
-            return;
-        }
-
-        mFloatingTaskLayer = new FloatingTaskLayer(mContext, this, mWindowManager);
-
-        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT,
-                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
-                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-                        | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
-                PixelFormat.TRANSLUCENT
-        );
-        params.setTrustedOverlay();
-        params.setFitInsetsTypes(0);
-        params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
-        params.setTitle("FloatingTaskLayer");
-        params.packageName = mContext.getPackageName();
-        params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-        params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
-
-        try {
-            mIsFloatingLayerAdded = true;
-            mWindowManager.addView(mFloatingTaskLayer, params);
-        } catch (IllegalStateException e) {
-            // This means the floating layer has already been added which shouldn't happen.
-            e.printStackTrace();
-        }
-    }
-
-    private void removeFloatingLayer() {
-        if (!mIsFloatingLayerAdded) {
-            return;
-        }
-        try {
-            mIsFloatingLayerAdded = false;
-            if (mFloatingTaskLayer != null) {
-                mWindowManager.removeView(mFloatingTaskLayer);
-            }
-        } catch (IllegalArgumentException e) {
-            // This means the floating layer has already been removed which shouldn't happen.
-            e.printStackTrace();
-        }
-    }
-
-    /**
-     * Description of current floating task state.
-     */
-    private void dump(PrintWriter pw, String prefix) {
-        pw.println("FloatingTaskController state:");
-        pw.print("   isFloatingLayerAvailable= "); pw.println(isFloatingLayerAvailable());
-        pw.print("   isFloatingTasksEnabled= "); pw.println(isFloatingTasksEnabled());
-        pw.print("   mIsFloatingLayerAdded= "); pw.println(mIsFloatingLayerAdded);
-        pw.print("   mLastPosition= "); pw.println(mLastPosition);
-        pw.println();
-    }
-
-    /** Returns the {@link FloatingTasks} implementation. */
-    public FloatingTasks asFloatingTasks() {
-        return mImpl;
-    }
-
-    @Override
-    public Context getContext() {
-        return mContext;
-    }
-
-    @Override
-    public ShellExecutor getRemoteCallExecutor() {
-        return mMainExecutor;
-    }
-
-    /**
-     * The interface for calls from outside the shell, within the host process.
-     */
-    @ExternalThread
-    private class FloatingTaskImpl implements FloatingTasks {
-        @Override
-        public void showOrSetStashed(Intent intent) {
-            mMainExecutor.execute(() -> FloatingTasksController.this.showOrSetStashed(intent));
-        }
-    }
-
-    /**
-     * The interface for calls from outside the host process.
-     */
-    @BinderThread
-    private static class IFloatingTasksImpl extends IFloatingTasks.Stub
-            implements ExternalInterfaceBinder {
-        private FloatingTasksController mController;
-
-        IFloatingTasksImpl(FloatingTasksController controller) {
-            mController = controller;
-        }
-
-        /**
-         * Invalidates this instance, preventing future calls from updating the controller.
-         */
-        @Override
-        public void invalidate() {
-            mController = null;
-        }
-
-        public void showTask(Intent intent) {
-            executeRemoteCallWithTaskPermission(mController, "showTask",
-                    (controller) ->  controller.showTask(intent));
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
deleted file mode 100644
index f79ca10..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/IFloatingTasks.aidl
+++ /dev/null
@@ -1,28 +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.wm.shell.floating;
-
-import android.content.Intent;
-
-/**
- * Interface that is exposed to remote callers to manipulate floating task features.
- */
-interface IFloatingTasks {
-
-    void showTask(in Intent intent) = 1;
-
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
deleted file mode 100644
index c922109..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingMenuView.java
+++ /dev/null
@@ -1,73 +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.wm.shell.floating.views;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.wm.shell.R;
-
-/**
- * Displays the menu items for a floating task view (e.g. close).
- */
-public class FloatingMenuView extends LinearLayout {
-
-    private int mItemSize;
-    private int mItemMargin;
-
-    public FloatingMenuView(Context context) {
-        super(context);
-        setOrientation(LinearLayout.HORIZONTAL);
-        setGravity(Gravity.CENTER);
-
-        mItemSize = context.getResources().getDimensionPixelSize(
-                R.dimen.floating_task_menu_item_size);
-        mItemMargin = context.getResources().getDimensionPixelSize(
-                R.dimen.floating_task_menu_item_padding);
-    }
-
-    /** Adds a clickable item to the menu bar. Items are ordered as added. */
-    public void addMenuItem(@Nullable Drawable drawable, View.OnClickListener listener) {
-        ImageView itemView = new ImageView(getContext());
-        itemView.setScaleType(ImageView.ScaleType.CENTER);
-        if (drawable != null) {
-            itemView.setImageDrawable(drawable);
-        }
-        LinearLayout.LayoutParams lp = new LayoutParams(mItemSize,
-                ViewGroup.LayoutParams.MATCH_PARENT);
-        lp.setMarginStart(mItemMargin);
-        lp.setMarginEnd(mItemMargin);
-        addView(itemView, lp);
-
-        itemView.setOnClickListener(listener);
-    }
-
-    /**
-     * The menu extends past the top of the TaskView because of the rounded corners. This means
-     * to center content in the menu we must subtract the radius (i.e. the amount of space covered
-     * by TaskView).
-     */
-    public void setCornerRadius(float radius) {
-        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
deleted file mode 100644
index 16dab24..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskLayer.java
+++ /dev/null
@@ -1,687 +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.wm.shell.floating.views;
-
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.dynamicanimation.animation.DynamicAnimation;
-import androidx.dynamicanimation.animation.FlingAnimation;
-
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.R;
-import com.android.wm.shell.floating.FloatingDismissController;
-import com.android.wm.shell.floating.FloatingTasksController;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * This is the layout that {@link FloatingTaskView}s are contained in. It handles input and
- * movement of the task views.
- */
-public class FloatingTaskLayer extends FrameLayout
-        implements ViewTreeObserver.OnComputeInternalInsetsListener {
-
-    private static final String TAG = FloatingTaskLayer.class.getSimpleName();
-
-    /** How big to make the task view based on screen width of the largest size. */
-    private static final float START_SIZE_WIDTH_PERCENT = 0.33f;
-    /** Min fling velocity required to move the view from one side of the screen to the other. */
-    private static final float ESCAPE_VELOCITY = 750f;
-    /** Amount of friction to apply to fling animations. */
-    private static final float FLING_FRICTION = 1.9f;
-
-    private final FloatingTasksController mController;
-    private final FloatingDismissController mDismissController;
-    private final WindowManager mWindowManager;
-    private final TouchHandlerImpl mTouchHandler;
-
-    private final Region mTouchableRegion = new Region();
-    private final Rect mPositionRect = new Rect();
-    private final Point mDefaultStartPosition = new Point();
-    private final Point mTaskViewSize = new Point();
-    private WindowInsets mWindowInsets;
-    private int mVerticalPadding;
-    private int mOverhangWhenStashed;
-
-    private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
-    private ViewTreeObserver.OnDrawListener mSystemGestureExclusionListener =
-            this::updateSystemGestureExclusion;
-
-    /** Interface allowing something to handle the touch events going to a task. */
-    interface FloatingTaskTouchHandler {
-        void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
-                float viewInitialX, float viewInitialY);
-
-        void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
-                 float dx, float dy);
-
-        void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
-                float dx, float dy, float velX, float velY);
-
-        void onClick(@NonNull FloatingTaskView v);
-    }
-
-    public FloatingTaskLayer(Context context,
-            FloatingTasksController controller,
-            WindowManager windowManager) {
-        super(context);
-        // TODO: Why is this necessary? Without it FloatingTaskView does not render correctly.
-        setBackgroundColor(Color.argb(0, 0, 0, 0));
-
-        mController = controller;
-        mWindowManager = windowManager;
-        updateSizes();
-
-        // TODO: Might make sense to put dismiss controller in the touch handler since that's the
-        //  main user of dismiss controller.
-        mDismissController = new FloatingDismissController(context, mController, this);
-        mTouchHandler = new TouchHandlerImpl();
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
-        getViewTreeObserver().addOnDrawListener(mSystemGestureExclusionListener);
-        setOnApplyWindowInsetsListener((view, windowInsets) -> {
-            if (!windowInsets.equals(mWindowInsets)) {
-                mWindowInsets = windowInsets;
-                updateSizes();
-            }
-            return windowInsets;
-        });
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
-        getViewTreeObserver().removeOnDrawListener(mSystemGestureExclusionListener);
-    }
-
-    @Override
-    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
-        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-        mTouchableRegion.setEmpty();
-        getTouchableRegion(mTouchableRegion);
-        inoutInfo.touchableRegion.set(mTouchableRegion);
-    }
-
-    /** Adds a floating task to the layout. */
-    public void addTask(FloatingTasksController.Task task) {
-        if (task.floatingView == null) return;
-
-        task.floatingView.setTouchHandler(mTouchHandler);
-        addView(task.floatingView, new LayoutParams(mTaskViewSize.x, mTaskViewSize.y));
-        updateTaskViewPosition(task.floatingView);
-    }
-
-    /** Animates the stashed state of the provided task, if it's part of the floating layer. */
-    public void setStashed(FloatingTasksController.Task task, boolean shouldStash) {
-        if (task.floatingView != null && task.floatingView.getParent() == this) {
-            mTouchHandler.stashTaskView(task.floatingView, shouldStash);
-        }
-    }
-
-    /** Removes all {@link FloatingTaskView} from the layout. */
-    public void removeAllTaskViews() {
-        int childCount = getChildCount();
-        ArrayList<View> viewsToRemove = new ArrayList<>();
-        for (int i = 0; i < childCount; i++) {
-            if (getChildAt(i) instanceof FloatingTaskView) {
-                viewsToRemove.add(getChildAt(i));
-            }
-        }
-        for (View v : viewsToRemove) {
-            removeView(v);
-        }
-    }
-
-    /** Returns the number of task views in the layout. */
-    public int getTaskViewCount() {
-        int taskViewCount = 0;
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            if (getChildAt(i) instanceof FloatingTaskView) {
-                taskViewCount++;
-            }
-        }
-        return taskViewCount;
-    }
-
-    /**
-     * Called when the task view is un-stuck from the dismiss target.
-     * @param v the task view being moved.
-     * @param velX the x velocity of the motion event.
-     * @param velY the y velocity of the motion event.
-     * @param wasFlungOut true if the user flung the task view out of the dismiss target (i.e. there
-     *                    was an 'up' event), otherwise the user is still dragging.
-     */
-    public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
-            boolean wasFlungOut) {
-        mTouchHandler.onUnstuckFromTarget(v, velX, velY, wasFlungOut);
-    }
-
-    /**
-     * Updates dimensions and applies them to any task views.
-     */
-    public void updateSizes() {
-        if (mDismissController != null) {
-            mDismissController.updateSizes();
-        }
-
-        mOverhangWhenStashed = getResources().getDimensionPixelSize(
-                R.dimen.floating_task_stash_offset);
-        mVerticalPadding = getResources().getDimensionPixelSize(
-                R.dimen.floating_task_vertical_padding);
-
-        WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
-        WindowInsets windowInsets = windowMetrics.getWindowInsets();
-        Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
-                | WindowInsets.Type.statusBars()
-                | WindowInsets.Type.displayCutout());
-        Rect bounds = windowMetrics.getBounds();
-        mPositionRect.set(bounds.left + insets.left,
-                bounds.top + insets.top + mVerticalPadding,
-                bounds.right - insets.right,
-                bounds.bottom - insets.bottom - mVerticalPadding);
-
-        int taskViewWidth = Math.max(bounds.height(), bounds.width());
-        int taskViewHeight = Math.min(bounds.height(), bounds.width());
-        taskViewHeight = taskViewHeight - (insets.top + insets.bottom + (mVerticalPadding * 2));
-        mTaskViewSize.set((int) (taskViewWidth * START_SIZE_WIDTH_PERCENT), taskViewHeight);
-        mDefaultStartPosition.set(mPositionRect.left, mPositionRect.top);
-
-        // Update existing views
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            if (getChildAt(i) instanceof FloatingTaskView) {
-                FloatingTaskView child = (FloatingTaskView) getChildAt(i);
-                LayoutParams lp = (LayoutParams) child.getLayoutParams();
-                lp.width = mTaskViewSize.x;
-                lp.height = mTaskViewSize.y;
-                child.setLayoutParams(lp);
-                updateTaskViewPosition(child);
-            }
-        }
-    }
-
-    /** Returns the first floating task view in the layout. (Currently only ever 1 view). */
-    @Nullable
-    public FloatingTaskView getFirstTaskView() {
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child instanceof FloatingTaskView) {
-                return (FloatingTaskView) child;
-            }
-        }
-        return null;
-    }
-
-    private void updateTaskViewPosition(FloatingTaskView floatingView) {
-        Point lastPosition = mController.getLastPosition();
-        if (lastPosition.x == -1 && lastPosition.y == -1) {
-            floatingView.setX(mDefaultStartPosition.x);
-            floatingView.setY(mDefaultStartPosition.y);
-        } else {
-            floatingView.setX(lastPosition.x);
-            floatingView.setY(lastPosition.y);
-        }
-        if (mTouchHandler.isStashedPosition(floatingView)) {
-            floatingView.setStashed(true);
-        }
-        floatingView.updateLocation();
-    }
-
-    /**
-     * Updates the area of the screen that shouldn't allow the back gesture due to the placement
-     * of task view (i.e. when task view is stashed on an edge, tapping or swiping that edge would
-     * un-stash the task view instead of performing the back gesture).
-     */
-    private void updateSystemGestureExclusion() {
-        Rect excludeZone = mSystemGestureExclusionRects.get(0);
-        FloatingTaskView floatingTaskView = getFirstTaskView();
-        if (floatingTaskView != null && floatingTaskView.isStashed()) {
-            excludeZone.set(floatingTaskView.getLeft(),
-                    floatingTaskView.getTop(),
-                    floatingTaskView.getRight(),
-                    floatingTaskView.getBottom());
-            excludeZone.offset((int) (floatingTaskView.getTranslationX()),
-                    (int) (floatingTaskView.getTranslationY()));
-            setSystemGestureExclusionRects(mSystemGestureExclusionRects);
-        } else {
-            excludeZone.setEmpty();
-            setSystemGestureExclusionRects(Collections.emptyList());
-        }
-    }
-
-    /**
-     * Fills in the touchable region for floating windows. This is used by WindowManager to
-     * decide which touch events go to the floating windows.
-     */
-    private void getTouchableRegion(Region outRegion) {
-        int childCount = getChildCount();
-        Rect temp = new Rect();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            if (child instanceof FloatingTaskView) {
-                child.getBoundsOnScreen(temp);
-                outRegion.op(temp, Region.Op.UNION);
-            }
-        }
-    }
-
-    /**
-     * Implementation of the touch handler. Animates the task view based on touch events.
-     */
-    private class TouchHandlerImpl implements FloatingTaskTouchHandler {
-        /**
-         * The view can be stashed by swiping it towards the current edge or moving it there. If
-         * the view gets moved in a way that is not one of these gestures, this is flipped to false.
-         */
-        private boolean mCanStash = true;
-        /**
-         * This is used to indicate that the view has been un-stuck from the dismiss target and
-         * needs to spring to the current touch location.
-         */
-        // TODO: implement this behavior
-        private boolean mSpringToTouchOnNextMotionEvent = false;
-
-        private ArrayList<FlingAnimation> mFlingAnimations;
-        private ViewPropertyAnimator mViewPropertyAnimation;
-
-        private float mViewInitialX;
-        private float mViewInitialY;
-
-        private float[] mMinMax = new float[2];
-
-        @Override
-        public void onDown(@NonNull FloatingTaskView v, @NonNull MotionEvent ev, float viewInitialX,
-                float viewInitialY) {
-            mCanStash = true;
-            mViewInitialX = viewInitialX;
-            mViewInitialY = viewInitialY;
-            mDismissController.setUpMagneticObject(v);
-            mDismissController.passEventToMagnetizedObject(ev);
-        }
-
-        @Override
-        public void onMove(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
-                float dx, float dy) {
-            // Shows the magnetic dismiss target if needed.
-            mDismissController.showDismiss(/* show= */ true);
-
-            // Send it to magnetic target first.
-            if (mDismissController.passEventToMagnetizedObject(ev)) {
-                v.setStashed(false);
-                mCanStash = true;
-
-                return;
-            }
-
-            // If we're here magnetic target didn't want it so move as per normal.
-
-            v.setTranslationX(capX(v, mViewInitialX + dx, /* isMoving= */ true));
-            v.setTranslationY(capY(v, mViewInitialY + dy));
-            if (v.isStashed()) {
-                // Check if we've moved far enough to be not stashed.
-                final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
-                final boolean viewInitiallyOnLeftSide = mViewInitialX < centerX;
-                if (viewInitiallyOnLeftSide) {
-                    if (v.getTranslationX() > mPositionRect.left) {
-                        v.setStashed(false);
-                        mCanStash = true;
-                    }
-                } else if (v.getTranslationX() + v.getWidth() < mPositionRect.right) {
-                    v.setStashed(false);
-                    mCanStash = true;
-                }
-            }
-        }
-
-        // Reference for math / values: StackAnimationController#flingStackThenSpringToEdge.
-        // TODO clean up the code here, pretty hard to comprehend
-        // TODO code here doesn't work the best when in portrait (e.g. can't fling up/down on edges)
-        @Override
-        public void onUp(@NonNull FloatingTaskView v, @NonNull MotionEvent ev,
-                float dx, float dy, float velX, float velY) {
-
-            // Send it to magnetic target first.
-            if (mDismissController.passEventToMagnetizedObject(ev)) {
-                v.setStashed(false);
-                return;
-            }
-            mDismissController.showDismiss(/* show= */ false);
-
-            // If we're here magnetic target didn't want it so handle up as per normal.
-
-            final float x = capX(v, mViewInitialX + dx, /* isMoving= */ false);
-            final float centerX = mPositionRect.centerX();
-            final boolean viewInitiallyOnLeftSide = mViewInitialX + v.getWidth() < centerX;
-            final boolean viewOnLeftSide = x + v.getWidth() < centerX;
-            final boolean isFling = Math.abs(velX) > ESCAPE_VELOCITY;
-            final boolean isFlingLeft = isFling && velX < ESCAPE_VELOCITY;
-            // TODO: check velX here sometimes it doesn't stash on move when I think it should
-            final boolean shouldStashFromMove =
-                    (velX < 0 && v.getTranslationX() < mPositionRect.left)
-                            || (velX > 0
-                            && v.getTranslationX() + v.getWidth() > mPositionRect.right);
-            final boolean shouldStashFromFling = viewInitiallyOnLeftSide == viewOnLeftSide
-                    && isFling
-                    && ((viewOnLeftSide && velX < ESCAPE_VELOCITY)
-                    || (!viewOnLeftSide && velX > ESCAPE_VELOCITY));
-            final boolean shouldStash = mCanStash && (shouldStashFromFling || shouldStashFromMove);
-
-            ProtoLog.d(WM_SHELL_FLOATING_APPS,
-                    "shouldStash=%s shouldStashFromFling=%s shouldStashFromMove=%s"
-                    + " viewInitiallyOnLeftSide=%s viewOnLeftSide=%s isFling=%s velX=%f"
-                    + " isStashed=%s", shouldStash, shouldStashFromFling, shouldStashFromMove,
-                    viewInitiallyOnLeftSide, viewOnLeftSide, isFling, velX, v.isStashed());
-
-            if (v.isStashed()) {
-                mMinMax[0] = viewOnLeftSide
-                        ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
-                        : mPositionRect.right - v.getWidth();
-                mMinMax[1] = viewOnLeftSide
-                        ? mPositionRect.left
-                        : mPositionRect.right - mOverhangWhenStashed;
-            } else {
-                populateMinMax(v, viewOnLeftSide, shouldStash, mMinMax);
-            }
-
-            boolean movingLeft = isFling ? isFlingLeft : viewOnLeftSide;
-            float destinationRelativeX = movingLeft
-                    ? mMinMax[0]
-                    : mMinMax[1];
-
-            // TODO: why is this necessary / when does this happen?
-            if (mMinMax[1] < v.getTranslationX()) {
-                mMinMax[1] = v.getTranslationX();
-            }
-            if (v.getTranslationX() < mMinMax[0]) {
-                mMinMax[0] = v.getTranslationX();
-            }
-
-            // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
-            // so that it'll make it all the way to the side of the screen.
-            final float minimumVelocityToReachEdge =
-                    getMinimumVelocityToReachEdge(v, destinationRelativeX);
-            final float startXVelocity = movingLeft
-                    ? Math.min(minimumVelocityToReachEdge, velX)
-                    : Math.max(minimumVelocityToReachEdge, velX);
-
-            cancelAnyAnimations(v);
-
-            mFlingAnimations = getAnimationForUpEvent(v, shouldStash,
-                    startXVelocity, mMinMax[0], mMinMax[1], destinationRelativeX);
-            for (int i = 0; i < mFlingAnimations.size(); i++) {
-                mFlingAnimations.get(i).start();
-            }
-        }
-
-        @Override
-        public void onClick(@NonNull FloatingTaskView v) {
-            if (v.isStashed()) {
-                final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
-                final boolean viewOnLeftSide = v.getTranslationX() < centerX;
-                final float destinationRelativeX = viewOnLeftSide
-                        ? mPositionRect.left
-                        : mPositionRect.right - v.getWidth();
-                final float minimumVelocityToReachEdge =
-                        getMinimumVelocityToReachEdge(v, destinationRelativeX);
-                populateMinMax(v, viewOnLeftSide, /* stashed= */ true, mMinMax);
-
-                cancelAnyAnimations(v);
-
-                FlingAnimation flingAnimation = new FlingAnimation(v,
-                        DynamicAnimation.TRANSLATION_X);
-                flingAnimation.setFriction(FLING_FRICTION)
-                        .setStartVelocity(minimumVelocityToReachEdge)
-                        .setMinValue(mMinMax[0])
-                        .setMaxValue(mMinMax[1])
-                        .addEndListener((animation, canceled, value, velocity) -> {
-                            if (canceled) return;
-                            mController.setLastPosition((int) v.getTranslationX(),
-                                    (int) v.getTranslationY());
-                            v.setStashed(false);
-                            v.updateLocation();
-                        });
-                mFlingAnimations = new ArrayList<>();
-                mFlingAnimations.add(flingAnimation);
-                flingAnimation.start();
-            }
-        }
-
-        public void onUnstuckFromTarget(FloatingTaskView v, float velX, float velY,
-                boolean wasFlungOut) {
-            if (wasFlungOut) {
-                snapTaskViewToEdge(v, velX, /* shouldStash= */ false);
-            } else {
-                // TODO: use this for something / to spring the view to the touch location
-                mSpringToTouchOnNextMotionEvent = true;
-            }
-        }
-
-        public void stashTaskView(FloatingTaskView v, boolean shouldStash) {
-            if (v.isStashed() == shouldStash) {
-                return;
-            }
-            final float centerX = mPositionRect.centerX() - (v.getWidth() / 2f);
-            final boolean viewOnLeftSide = v.getTranslationX() < centerX;
-            snapTaskViewToEdge(v, viewOnLeftSide ? -ESCAPE_VELOCITY : ESCAPE_VELOCITY, shouldStash);
-        }
-
-        public boolean isStashedPosition(View v) {
-            return v.getTranslationX() < mPositionRect.left
-                    || v.getTranslationX() + v.getWidth() > mPositionRect.right;
-        }
-
-        // TODO: a lot of this is duplicated in onUp -- can it be unified?
-        private void snapTaskViewToEdge(FloatingTaskView v, float velX, boolean shouldStash) {
-            final boolean movingLeft = velX < ESCAPE_VELOCITY;
-            populateMinMax(v, movingLeft, shouldStash, mMinMax);
-            float destinationRelativeX = movingLeft
-                    ? mMinMax[0]
-                    : mMinMax[1];
-
-            // TODO: why is this necessary / when does this happen?
-            if (mMinMax[1] < v.getTranslationX()) {
-                mMinMax[1] = v.getTranslationX();
-            }
-            if (v.getTranslationX() < mMinMax[0]) {
-                mMinMax[0] = v.getTranslationX();
-            }
-
-            // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity
-            // so that it'll make it all the way to the side of the screen.
-            final float minimumVelocityToReachEdge =
-                    getMinimumVelocityToReachEdge(v, destinationRelativeX);
-            final float startXVelocity = movingLeft
-                    ? Math.min(minimumVelocityToReachEdge, velX)
-                    : Math.max(minimumVelocityToReachEdge, velX);
-
-            cancelAnyAnimations(v);
-
-            mFlingAnimations = getAnimationForUpEvent(v,
-                    shouldStash, startXVelocity,  mMinMax[0], mMinMax[1],
-                    destinationRelativeX);
-            for (int i = 0; i < mFlingAnimations.size(); i++) {
-                mFlingAnimations.get(i).start();
-            }
-        }
-
-        private void cancelAnyAnimations(FloatingTaskView v) {
-            if (mFlingAnimations != null) {
-                for (int i = 0; i < mFlingAnimations.size(); i++) {
-                    if (mFlingAnimations.get(i).isRunning()) {
-                        mFlingAnimations.get(i).cancel();
-                    }
-                }
-            }
-            if (mViewPropertyAnimation != null) {
-                mViewPropertyAnimation.cancel();
-                mViewPropertyAnimation = null;
-            }
-        }
-
-        private ArrayList<FlingAnimation> getAnimationForUpEvent(FloatingTaskView v,
-                boolean shouldStash, float startVelX, float minValue, float maxValue,
-                float destinationRelativeX) {
-            final float ty = v.getTranslationY();
-            final ArrayList<FlingAnimation> animations = new ArrayList<>();
-            if (ty != capY(v, ty)) {
-                // The view was being dismissed so the Y is out of bounds, need to animate that.
-                FlingAnimation yFlingAnimation = new FlingAnimation(v,
-                        DynamicAnimation.TRANSLATION_Y);
-                yFlingAnimation.setFriction(FLING_FRICTION)
-                        .setStartVelocity(startVelX)
-                        .setMinValue(mPositionRect.top)
-                        .setMaxValue(mPositionRect.bottom - mTaskViewSize.y);
-                animations.add(yFlingAnimation);
-            }
-            FlingAnimation flingAnimation = new FlingAnimation(v, DynamicAnimation.TRANSLATION_X);
-            flingAnimation.setFriction(FLING_FRICTION)
-                    .setStartVelocity(startVelX)
-                    .setMinValue(minValue)
-                    .setMaxValue(maxValue)
-                    .addEndListener((animation, canceled, value, velocity) -> {
-                        if (canceled) return;
-                        Runnable endAction = () -> {
-                            v.setStashed(shouldStash);
-                            v.updateLocation();
-                            if (!v.isStashed()) {
-                                mController.setLastPosition((int) v.getTranslationX(),
-                                        (int) v.getTranslationY());
-                            }
-                        };
-                        if (!shouldStash) {
-                            final int xTranslation = (int) v.getTranslationX();
-                            if (xTranslation != destinationRelativeX) {
-                                // TODO: this animation doesn't feel great, should figure out
-                                //  a better way to do this or remove the need for it all together.
-                                mViewPropertyAnimation = v.animate()
-                                        .translationX(destinationRelativeX)
-                                        .setListener(getAnimationListener(endAction));
-                                mViewPropertyAnimation.start();
-                            } else {
-                                endAction.run();
-                            }
-                        } else {
-                            endAction.run();
-                        }
-                    });
-            animations.add(flingAnimation);
-            return animations;
-        }
-
-        private AnimatorListenerAdapter getAnimationListener(Runnable endAction) {
-            return new AnimatorListenerAdapter() {
-                boolean translationCanceled = false;
-                @Override
-                public void onAnimationCancel(Animator animation) {
-                    super.onAnimationCancel(animation);
-                    translationCanceled = true;
-                }
-
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    if (!translationCanceled) {
-                        endAction.run();
-                    }
-                }
-            };
-        }
-
-        private void populateMinMax(FloatingTaskView v, boolean onLeft, boolean shouldStash,
-                float[] out) {
-            if (shouldStash) {
-                out[0] = onLeft
-                        ? mPositionRect.left - v.getWidth() + mOverhangWhenStashed
-                        : mPositionRect.right - v.getWidth();
-                out[1] = onLeft
-                        ? mPositionRect.left
-                        : mPositionRect.right - mOverhangWhenStashed;
-            } else {
-                out[0] = mPositionRect.left;
-                out[1] = mPositionRect.right - mTaskViewSize.x;
-            }
-        }
-
-        private float getMinimumVelocityToReachEdge(FloatingTaskView v,
-                float destinationRelativeX) {
-            // Minimum velocity required for the view to make it to the targeted side of the screen,
-            // taking friction into account (4.2f is the number that friction scalars are multiplied
-            // by in DynamicAnimation.DragForce). This is an estimate and could be slightly off, the
-            // animation at the end will ensure that it reaches the destination X regardless.
-            return (destinationRelativeX - v.getTranslationX()) * (FLING_FRICTION * 4.2f);
-        }
-
-        private float capX(FloatingTaskView v, float x, boolean isMoving) {
-            final int width = v.getWidth();
-            if (v.isStashed() || isMoving) {
-                if (x < mPositionRect.left - v.getWidth() + mOverhangWhenStashed) {
-                    return mPositionRect.left - v.getWidth() + mOverhangWhenStashed;
-                }
-                if (x > mPositionRect.right - mOverhangWhenStashed) {
-                    return mPositionRect.right - mOverhangWhenStashed;
-                }
-            } else {
-                if (x < mPositionRect.left) {
-                    return mPositionRect.left;
-                }
-                if (x > mPositionRect.right - width) {
-                    return mPositionRect.right - width;
-                }
-            }
-            return x;
-        }
-
-        private float capY(FloatingTaskView v, float y) {
-            final int height = v.getHeight();
-            if (y < mPositionRect.top) {
-                return mPositionRect.top;
-            }
-            if (y > mPositionRect.bottom - height) {
-                return mPositionRect.bottom - height;
-            }
-            return y;
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
deleted file mode 100644
index 581204a..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/views/FloatingTaskView.java
+++ /dev/null
@@ -1,385 +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.wm.shell.floating.views;
-
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
-
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FLOATING_APPS;
-
-import android.app.ActivityManager;
-import android.app.ActivityOptions;
-import android.app.ActivityTaskManager;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.R;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.TaskView;
-import com.android.wm.shell.TaskViewTransitions;
-import com.android.wm.shell.bubbles.RelativeTouchListener;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.floating.FloatingTasksController;
-
-/**
- * A view that holds a floating task using {@link TaskView} along with additional UI to manage
- * the task.
- */
-public class FloatingTaskView extends FrameLayout {
-
-    private static final String TAG = FloatingTaskView.class.getSimpleName();
-
-    private FloatingTasksController mController;
-
-    private FloatingMenuView mMenuView;
-    private int mMenuHeight;
-    private TaskView mTaskView;
-
-    private float mCornerRadius = 0f;
-    private int mBackgroundColor;
-
-    private FloatingTasksController.Task mTask;
-
-    private boolean mIsStashed;
-
-    /**
-     * Creates a floating task view.
-     *
-     * @param context the context to use.
-     * @param controller the controller to notify about changes in the floating task (e.g. removal).
-     */
-    public FloatingTaskView(Context context, FloatingTasksController controller) {
-        super(context);
-        mController = controller;
-        setElevation(getResources().getDimensionPixelSize(R.dimen.floating_task_elevation));
-        mMenuHeight = context.getResources().getDimensionPixelSize(R.dimen.floating_task_menu_size);
-        mMenuView = new FloatingMenuView(context);
-        addView(mMenuView);
-
-        applyThemeAttrs();
-
-        setClipToOutline(true);
-        setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
-            }
-        });
-    }
-
-    // TODO: call this when theme/config changes
-    void applyThemeAttrs() {
-        boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
-                mContext.getResources());
-        final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
-                android.R.attr.dialogCornerRadius,
-                android.R.attr.colorBackgroundFloating});
-        mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
-        mCornerRadius = mCornerRadius / 2f;
-        mBackgroundColor = ta.getColor(1, Color.WHITE);
-
-        ta.recycle();
-
-        mMenuView.setCornerRadius(mCornerRadius);
-        mMenuHeight = getResources().getDimensionPixelSize(
-                R.dimen.floating_task_menu_size);
-
-        if (mTaskView != null) {
-            mTaskView.setCornerRadius(mCornerRadius);
-        }
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        int height = MeasureSpec.getSize(heightMeasureSpec);
-
-        // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
-        int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
-        measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
-                MeasureSpec.getMode(heightMeasureSpec)));
-
-        if (mTaskView != null) {
-            int taskViewHeight = height - menuViewHeight;
-            measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
-                    MeasureSpec.getMode(heightMeasureSpec)));
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        // Drag handle above
-        final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
-        mMenuView.layout(l, t, r, dragHandleBottom);
-        if (mTaskView != null) {
-            // Subtract radius so that the menu extends behind the rounded corners of TaskView.
-            mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
-                    dragHandleBottom + mTaskView.getMeasuredHeight());
-        }
-    }
-
-    /**
-     * Constructs the TaskView to display the task. Must be called for {@link #startTask} to work.
-     */
-    public void createTaskView(Context context, ShellTaskOrganizer organizer,
-            TaskViewTransitions transitions, SyncTransactionQueue syncQueue) {
-        mTaskView = new TaskView(context, organizer, transitions, syncQueue);
-        addView(mTaskView);
-        mTaskView.setEnableSurfaceClipping(true);
-        mTaskView.setCornerRadius(mCornerRadius);
-    }
-
-    /**
-     * Starts the provided task in the TaskView, if the TaskView exists. This should be called after
-     * {@link #createTaskView}.
-     */
-    public void startTask(@ShellMainThread ShellExecutor executor,
-            FloatingTasksController.Task task) {
-        if (mTaskView == null) {
-            Log.e(TAG, "starting task before creating the view!");
-            return;
-        }
-        mTask = task;
-        mTaskView.setListener(executor, mTaskViewListener);
-    }
-
-    /**
-     * Sets the touch handler for the view.
-     *
-     * @param handler the touch handler for the view.
-     */
-    public void setTouchHandler(FloatingTaskLayer.FloatingTaskTouchHandler handler) {
-        setOnTouchListener(new RelativeTouchListener() {
-            @Override
-            public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
-                handler.onDown(FloatingTaskView.this, ev, v.getTranslationX(), v.getTranslationY());
-                return true;
-            }
-
-            @Override
-            public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
-                    float viewInitialY, float dx, float dy) {
-                handler.onMove(FloatingTaskView.this, ev, dx, dy);
-            }
-
-            @Override
-            public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
-                    float viewInitialY, float dx, float dy, float velX, float velY) {
-                handler.onUp(FloatingTaskView.this, ev, dx, dy, velX, velY);
-            }
-        });
-        setOnClickListener(view -> {
-            handler.onClick(FloatingTaskView.this);
-        });
-
-        mMenuView.addMenuItem(null, view -> {
-            if (mIsStashed) {
-                // If we're stashed all clicks un-stash.
-                handler.onClick(FloatingTaskView.this);
-            }
-        });
-    }
-
-    private void setContentVisibility(boolean visible) {
-        if (mTaskView == null) return;
-        mTaskView.setAlpha(visible ? 1f : 0f);
-    }
-
-    /**
-     * Sets the alpha of both this view and the TaskView.
-     */
-    public void setTaskViewAlpha(float alpha) {
-        if (mTaskView != null) {
-            mTaskView.setAlpha(alpha);
-        }
-        setAlpha(alpha);
-    }
-
-    /**
-     * Call when the location or size of the view has changed to update TaskView.
-     */
-    public void updateLocation() {
-        if (mTaskView == null) return;
-        mTaskView.onLocationChanged();
-    }
-
-    private void updateMenuColor() {
-        ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
-        int color = info != null ? info.taskDescription.getBackgroundColor() : -1;
-        if (color != -1) {
-            mMenuView.setBackgroundColor(color);
-        } else {
-            mMenuView.setBackgroundColor(mBackgroundColor);
-        }
-    }
-
-    /**
-     * Sets whether the view is stashed or not.
-     *
-     * Also updates the touchable area based on this. If the view is stashed we don't direct taps
-     * on the activity to the activity, instead a tap will un-stash the view.
-     */
-    public void setStashed(boolean isStashed) {
-        if (mIsStashed != isStashed) {
-            mIsStashed = isStashed;
-            if (mTaskView == null) {
-                return;
-            }
-            updateObscuredTouchRect();
-        }
-    }
-
-    /** Whether the view is stashed at the edge of the screen or not. **/
-    public boolean isStashed() {
-        return mIsStashed;
-    }
-
-    private void updateObscuredTouchRect() {
-        if (mIsStashed) {
-            Rect tmpRect = new Rect();
-            getBoundsOnScreen(tmpRect);
-            mTaskView.setObscuredTouchRect(tmpRect);
-        } else {
-            mTaskView.setObscuredTouchRect(null);
-        }
-    }
-
-    /**
-     * Whether the task needs to be restarted, this can happen when {@link #cleanUpTaskView()} has
-     * been called on this view or if
-     * {@link #startTask(ShellExecutor, FloatingTasksController.Task)} was never called.
-     */
-    public boolean needsTaskStarted() {
-        // If the task needs to be restarted then TaskView would have been cleaned up.
-        return mTaskView == null;
-    }
-
-    /** Call this when the floating task activity is no longer in use. */
-    public void cleanUpTaskView() {
-        if (mTask != null && mTask.taskId != INVALID_TASK_ID) {
-            try {
-                ActivityTaskManager.getService().removeTask(mTask.taskId);
-            } catch (RemoteException e) {
-                Log.e(TAG, e.getMessage());
-            }
-        }
-        if (mTaskView != null) {
-            mTaskView.release();
-            removeView(mTaskView);
-            mTaskView = null;
-        }
-    }
-
-    // TODO: use task background colour / how to get the taskInfo ?
-    private static int getDragBarColor(ActivityManager.RunningTaskInfo taskInfo) {
-        final int taskBgColor = taskInfo.taskDescription.getStatusBarColor();
-        return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
-    }
-
-    private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
-        private boolean mInitialized = false;
-        private boolean mDestroyed = false;
-
-        @Override
-        public void onInitialized() {
-            if (mDestroyed || mInitialized) {
-                return;
-            }
-            // Custom options so there is no activity transition animation
-            ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
-                    /* enterResId= */ 0, /* exitResId= */ 0);
-
-            Rect launchBounds = new Rect();
-            mTaskView.getBoundsOnScreen(launchBounds);
-
-            try {
-                options.setTaskAlwaysOnTop(true);
-                if (mTask.intent != null) {
-                    Intent fillInIntent = new Intent();
-                    // Apply flags to make behaviour match documentLaunchMode=always.
-                    fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
-                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-
-                    PendingIntent pi = PendingIntent.getActivity(mContext, 0, mTask.intent,
-                            PendingIntent.FLAG_MUTABLE,
-                            null);
-                    mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
-                } else {
-                    ProtoLog.e(WM_SHELL_FLOATING_APPS, "Tried to start a task with null intent");
-                }
-            } catch (RuntimeException e) {
-                ProtoLog.e(WM_SHELL_FLOATING_APPS, "Exception while starting task: %s",
-                        e.getMessage());
-                mController.removeTask();
-            }
-            mInitialized = true;
-        }
-
-        @Override
-        public void onReleased() {
-            mDestroyed = true;
-        }
-
-        @Override
-        public void onTaskCreated(int taskId, ComponentName name) {
-            mTask.taskId = taskId;
-            updateMenuColor();
-            setContentVisibility(true);
-        }
-
-        @Override
-        public void onTaskVisibilityChanged(int taskId, boolean visible) {
-            setContentVisibility(visible);
-        }
-
-        @Override
-        public void onTaskRemovalStarted(int taskId) {
-            // Must post because this is called from a binder thread.
-            post(() -> {
-                mController.removeTask();
-                cleanUpTaskView();
-            });
-        }
-
-        @Override
-        public void onBackPressedOnTaskRoot(int taskId) {
-            if (mTask.taskId == taskId && !mIsStashed) {
-                // TODO: is removing the window the desired behavior?
-                post(() -> mController.removeTask());
-            }
-        }
-    };
-}
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 f82a346..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,9 +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(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);
+            });
         }
     }
 
@@ -100,9 +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(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) {
@@ -119,11 +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(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/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index f4888fb..168f6d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -98,9 +98,11 @@
 
             switch (change.getMode()) {
                 case WindowManager.TRANSIT_OPEN:
-                case WindowManager.TRANSIT_TO_FRONT:
                     onOpenTransitionReady(change, startT, finishT);
                     break;
+                case WindowManager.TRANSIT_TO_FRONT:
+                    onToFrontTransitionReady(change, startT, finishT);
+                    break;
                 case WindowManager.TRANSIT_CLOSE: {
                     taskInfoList.add(change.getTaskInfo());
                     onCloseTransitionReady(change, startT, finishT);
@@ -138,6 +140,21 @@
                 change.getTaskInfo(), startT, finishT);
     }
 
+    private void onToFrontTransitionReady(
+            TransitionInfo.Change change,
+            SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT) {
+        boolean exists = mWindowDecorViewModel.setupWindowDecorationForTransition(
+                change.getTaskInfo(),
+                startT,
+                finishT);
+        if (!exists) {
+            // Window caption does not exist, create it
+            mWindowDecorViewModel.createWindowDecoration(
+                    change.getTaskInfo(), change.getLeash(), startT, finishT);
+        }
+    }
+
     @Override
     public void onTransitionStarting(@NonNull IBinder transition) {}
 
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/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 2b36b4c..85bad17 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -335,6 +335,7 @@
         final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo();
         if (taskInfo != null) {
             startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(),
+                    mPipBoundsState.getBounds(), mPipBoundsState.getBounds(),
                     new Rect(mExitDestinationBounds), Surface.ROTATION_0);
         }
         mExitDestinationBounds.setEmpty();
@@ -475,6 +476,20 @@
                     taskInfo);
             return;
         }
+
+        // When exiting PiP, the PiP leash may be an Activity of a multi-windowing Task, for which
+        // case it may not be in the screen coordinate.
+        // Reparent the pip leash to the root with max layer so that we can animate it outside of
+        // parent crop, and make sure it is not covered by other windows.
+        final SurfaceControl pipLeash = pipChange.getLeash();
+        startTransaction.reparent(pipLeash, info.getRootLeash());
+        startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
+        // Note: because of this, the bounds to animate should be translated to the root coordinate.
+        final Point offset = info.getRootOffset();
+        final Rect currentBounds = mPipBoundsState.getBounds();
+        currentBounds.offset(-offset.x, -offset.y);
+        startTransaction.setPosition(pipLeash, currentBounds.left, currentBounds.top);
+
         mFinishCallback = (wct, wctCB) -> {
             mPipOrganizer.onExitPipFinished(taskInfo);
             finishCallback.onTransitionFinished(wct, wctCB);
@@ -496,18 +511,17 @@
             if (displayRotationChange != null) {
                 // Exiting PIP to fullscreen with orientation change.
                 startExpandAndRotationAnimation(info, startTransaction, finishTransaction,
-                        displayRotationChange, taskInfo, pipChange);
+                        displayRotationChange, taskInfo, pipChange, offset);
                 return;
             }
         }
 
         // Set the initial frame as scaling the end to the start.
         final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
-        final Point offset = pipChange.getEndRelOffset();
         destinationBounds.offset(-offset.x, -offset.y);
-        startTransaction.setWindowCrop(pipChange.getLeash(), destinationBounds);
-        mSurfaceTransactionHelper.scale(startTransaction, pipChange.getLeash(),
-                destinationBounds, mPipBoundsState.getBounds());
+        startTransaction.setWindowCrop(pipLeash, destinationBounds);
+        mSurfaceTransactionHelper.scale(startTransaction, pipLeash, destinationBounds,
+                currentBounds);
         startTransaction.apply();
 
         // Check if it is fixed rotation.
@@ -532,19 +546,21 @@
                 y = destinationBounds.bottom;
             }
             mSurfaceTransactionHelper.rotateAndScaleWithCrop(finishTransaction,
-                    pipChange.getLeash(), endBounds, endBounds, new Rect(), degree, x, y,
+                    pipLeash, endBounds, endBounds, new Rect(), degree, x, y,
                     true /* isExpanding */, rotationDelta == ROTATION_270 /* clockwise */);
         } else {
             rotationDelta = Surface.ROTATION_0;
         }
-        startExpandAnimation(taskInfo, pipChange.getLeash(), destinationBounds, rotationDelta);
+        startExpandAnimation(taskInfo, pipLeash, currentBounds, currentBounds, destinationBounds,
+                rotationDelta);
     }
 
     private void startExpandAndRotationAnimation(@NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull TransitionInfo.Change displayRotationChange,
-            @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange) {
+            @NonNull TaskInfo taskInfo, @NonNull TransitionInfo.Change pipChange,
+            @NonNull Point offset) {
         final int rotateDelta = deltaRotation(displayRotationChange.getStartRotation(),
                 displayRotationChange.getEndRotation());
 
@@ -556,7 +572,6 @@
         final Rect startBounds = new Rect(pipChange.getStartAbsBounds());
         rotateBounds(startBounds, displayRotationChange.getStartAbsBounds(), rotateDelta);
         final Rect endBounds = new Rect(pipChange.getEndAbsBounds());
-        final Point offset = pipChange.getEndRelOffset();
         startBounds.offset(-offset.x, -offset.y);
         endBounds.offset(-offset.x, -offset.y);
 
@@ -592,11 +607,12 @@
     }
 
     private void startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash,
-            final Rect destinationBounds, final int rotationDelta) {
+            final Rect baseBounds, final Rect startBounds, final Rect endBounds,
+            final int rotationDelta) {
         final PipAnimationController.PipTransitionAnimator animator =
-                mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(),
-                        mPipBoundsState.getBounds(), destinationBounds, null,
-                        TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, rotationDelta);
+                mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds,
+                        endBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_LEAVE_PIP,
+                        0 /* startingAngle */, rotationDelta);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
index 7365b95..1f7a7fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java
@@ -29,6 +29,7 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.accessibility.IAccessibilityInteractionConnection;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.window.ScreenCapture;
 
 import androidx.annotation.BinderThread;
 
@@ -362,6 +363,15 @@
         }
 
         @Override
+        public void takeScreenshotOfWindow(int interactionId,
+                ScreenCapture.ScreenCaptureListener listener,
+                IAccessibilityInteractionConnectionCallback callback) throws RemoteException {
+            // AbstractAccessibilityServiceConnection uses the standard
+            // IAccessibilityInteractionConnection for takeScreenshotOfWindow for Pip windows,
+            // so do nothing here.
+        }
+
+        @Override
         public void clearAccessibilityFocus() throws RemoteException {
             // Do nothing
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 616d447..d28a9f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -612,9 +612,24 @@
                 new DisplayInsetsController.OnInsetsChangedListener() {
                     @Override
                     public void insetsChanged(InsetsState insetsState) {
+                        int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
                         onDisplayChanged(
                                 mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
                                 false /* saveRestoreSnapFraction */);
+                        int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
+                        if (!mEnablePipKeepClearAlgorithm) {
+                            int pipTop = mPipBoundsState.getBounds().top;
+                            int diff = newMaxMovementBound - oldMaxMovementBound;
+                            if (diff < 0 && pipTop > newMaxMovementBound) {
+                                // bottom inset has increased, move PiP up if it is too low
+                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(),
+                                        newMaxMovementBound - pipTop);
+                            }
+                            if (diff > 0 && oldMaxMovementBound == pipTop) {
+                                // bottom inset has decreased, move PiP down if it was by the edge
+                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff);
+                            }
+                        }
                     }
                 });
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index afb64c9..43d3f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -60,7 +60,7 @@
         FloatingContentCoordinator.FloatingContent {
 
     public static final boolean ENABLE_FLING_TO_DISMISS_PIP =
-            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true);
+            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false);
     private static final String TAG = "PipMotionHelper";
     private static final boolean DEBUG = false;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 975d4bb..a9a97be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -427,7 +427,7 @@
         // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not
         // occluded by the IME or shelf.
         if (fromImeAdjustment || fromShelfAdjustment) {
-            if (mTouchState.isUserInteracting()) {
+            if (mTouchState.isUserInteracting() && mTouchState.isDragging()) {
                 // Defer the update of the current movement bounds until after the user finishes
                 // touching the screen
             } else if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index c52ed24..75f9a4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -42,8 +42,8 @@
             Consts.TAG_WM_SHELL),
     WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
-    WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
-            Consts.TAG_WM_SHELL),
+    WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+            Consts.TAG_WM_SPLIT_SCREEN),
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
     WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
@@ -110,6 +110,7 @@
     private static class Consts {
         private static final String TAG_WM_SHELL = "WindowManagerShell";
         private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow";
+        private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
 
         private static final boolean ENABLE_DEBUG = true;
         private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
index b71cc32..1a6c1d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.recents;
 
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
 
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
@@ -44,5 +44,5 @@
     /**
      * Gets the set of running tasks.
      */
-    ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
+    RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 59f7233..e8f58fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.recents;
 
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
 
 /**
  * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
@@ -31,10 +31,10 @@
     /**
      * Called when a running task appears.
      */
-    void onRunningTaskAppeared(in ActivityManager.RunningTaskInfo taskInfo);
+    void onRunningTaskAppeared(in RunningTaskInfo taskInfo);
 
     /**
      * Called when a running task vanishes.
      */
-    void onRunningTaskVanished(in ActivityManager.RunningTaskInfo taskInfo);
-}
\ No newline at end of file
+    void onRunningTaskVanished(in RunningTaskInfo taskInfo);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 08f3db6..f9172ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -68,7 +68,7 @@
  * Manages the recent task list from the system, caching it as necessary.
  */
 public class RecentTasksController implements TaskStackListenerCallback,
-        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener {
+        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
@@ -147,7 +147,7 @@
                 this::createExternalInterface, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
-        mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
+        mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index eb08d0e..56aa742 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -86,8 +86,8 @@
     /**
      * Starts a pair of intent and task in one transition.
      */
-    oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent,
-            in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio,
+    oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId,
+            in Bundle options2, int sidePosition, float splitRatio,
             in RemoteTransition remoteTransition, in InstanceId instanceId) = 16;
 
     /**
@@ -95,7 +95,7 @@
      */
     oneway void startShortcutAndTask(in ShortcutInfo shortcutInfo, in Bundle options1, int taskId,
             in Bundle options2, int splitPosition, float splitRatio,
-             in RemoteTransition remoteTransition, in InstanceId instanceId) = 17;
+            in RemoteTransition remoteTransition, in InstanceId instanceId) = 17;
 
     /**
      * Version of startTasks using legacy transition system.
@@ -108,9 +108,8 @@
      * Starts a pair of intent and task using legacy transition system.
      */
     oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent,
-            in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2,
-            int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
-            in InstanceId instanceId) = 12;
+            in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio,
+            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12;
 
     /**
      * Starts a pair of shortcut and task using legacy transition system.
@@ -120,6 +119,21 @@
             in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 15;
 
     /**
+     * Start a pair of intents using legacy transition system.
+     */
+    oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1,
+            in Bundle options1, in PendingIntent pendingIntent2, in Bundle options2,
+            int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
+            in InstanceId instanceId) = 18;
+
+    /**
+     * Start a pair of intents in one transition.
+     */
+    oneway void startIntents(in PendingIntent pendingIntent1, in Bundle options1,
+            in PendingIntent pendingIntent2, in Bundle options2, int splitPosition,
+            float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 19;
+
+    /**
      * Blocking call that notifies and gets additional split-screen targets when entering
      * recents (for example: the dividerBar).
      * @param appTargets apps that will be re-parented to display area
@@ -133,4 +147,4 @@
      */
     RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14;
 }
-// Last id = 17
+// Last id = 19
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index c6a2b83..1774dd5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.START_SUCCESS;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -32,6 +33,8 @@
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
@@ -60,13 +63,12 @@
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
@@ -166,8 +168,11 @@
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
     private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
+    private final String[] mMultiInstancesComponents;
 
-    private StageCoordinator mStageCoordinator;
+    @VisibleForTesting
+    StageCoordinator mStageCoordinator;
+
     // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
     // outside the bounds of the roots by being reparented into a higher level fullscreen container
     private SurfaceControl mGoingToRecentsTasksLayer;
@@ -210,6 +215,51 @@
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             shellInit.addInitCallback(this::onInit, this);
         }
+
+        // TODO(255224696): Remove the config once having a way for client apps to opt-in
+        //                  multi-instances split.
+        mMultiInstancesComponents = mContext.getResources()
+                .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
+    }
+
+    @VisibleForTesting
+    SplitScreenController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
+            ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            DisplayController displayController,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            DragAndDropController dragAndDropController,
+            Transitions transitions,
+            TransactionPool transactionPool,
+            IconProvider iconProvider,
+            RecentTasksController recentTasks,
+            ShellExecutor mainExecutor,
+            StageCoordinator stageCoordinator) {
+        mShellCommandHandler = shellCommandHandler;
+        mShellController = shellController;
+        mTaskOrganizer = shellTaskOrganizer;
+        mSyncQueue = syncQueue;
+        mContext = context;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mMainExecutor = mainExecutor;
+        mDisplayController = displayController;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mDragAndDropController = dragAndDropController;
+        mTransitions = transitions;
+        mTransactionPool = transactionPool;
+        mIconProvider = iconProvider;
+        mRecentTasksOptional = Optional.of(recentTasks);
+        mStageCoordinator = stageCoordinator;
+        mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
+        shellInit.addInitCallback(this::onInit, this);
+        mMultiInstancesComponents = mContext.getResources()
+                .getStringArray(R.array.config_componentsSupportMultiInstancesSplit);
     }
 
     public SplitScreen asSplitScreen() {
@@ -471,72 +521,140 @@
         startIntent(intent, fillInIntent, position, options);
     }
 
+    private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
+            @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
+        Intent fillInIntent = null;
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+            fillInIntent = new Intent();
+            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent,
+                options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId);
+    }
+
+    private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+            int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+            float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        Intent fillInIntent = null;
+        if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId)
+                && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) {
+            fillInIntent = new Intent();
+            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId,
+                options2, splitPosition, splitRatio, remoteTransition, instanceId);
+    }
+
+    private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
+            @Nullable Bundle options1, PendingIntent pendingIntent2,
+            @Nullable Bundle options2, @SplitPosition int splitPosition,
+            float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
+        Intent fillInIntent1 = null;
+        Intent fillInIntent2 = null;
+        if (launchSameComponentAdjacently(pendingIntent1, pendingIntent2)
+                && supportMultiInstancesSplit(pendingIntent1.getIntent().getComponent())) {
+            fillInIntent1 = new Intent();
+            fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+            fillInIntent2 = new Intent();
+            fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+        }
+        mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1,
+                pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter,
+                instanceId);
+    }
+
     @Override
     public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
             @SplitPosition int position, @Nullable Bundle options) {
-        if (fillInIntent == null) {
-            fillInIntent = new Intent();
-        }
-        // Flag this as a no-user-action launch to prevent sending user leaving event to the
-        // current top activity since it's going to be put into another side of the split. This
-        // prevents the current top activity from going into pip mode due to user leaving event.
+        // Flag this as a no-user-action launch to prevent sending user leaving event to the current
+        // top activity since it's going to be put into another side of the split. This prevents the
+        // current top activity from going into pip mode due to user leaving event.
+        if (fillInIntent == null) fillInIntent = new Intent();
         fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
 
-        // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
-        // split and there is no reusable background task.
-        if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
-            final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
-                    ? mRecentTasksOptional.get().findTaskInBackground(
-                            intent.getIntent().getComponent())
-                    : null;
-            if (taskInfo != null) {
-                startTask(taskInfo.taskId, position, options);
+        if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) {
+            final ComponentName launching = intent.getIntent().getComponent();
+            if (supportMultiInstancesSplit(launching)) {
+                // To prevent accumulating large number of instances in the background, reuse task
+                // in the background with priority.
+                final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
+                        .map(recentTasks -> recentTasks.findTaskInBackground(launching))
+                        .orElse(null);
+                if (taskInfo != null) {
+                    startTask(taskInfo.taskId, position, options);
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                            "Start task in background");
+                    return;
+                }
+
+                // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
+                // the split and there is no reusable background task.
+                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
+            } else if (isSplitScreenVisible()) {
+                mStageCoordinator.switchSplitPosition("startIntent");
                 return;
             }
-            fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
         }
 
-        if (!ENABLE_SHELL_TRANSITIONS) {
-            mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options);
-            return;
-        }
         mStageCoordinator.startIntent(intent, fillInIntent, position, options);
     }
 
     /** Returns {@code true} if it's launching the same component on both sides of the split. */
-    @VisibleForTesting
-    boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) {
-        if (startIntent == null) {
-            return false;
-        }
+    private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent,
+            @SplitPosition int position, int taskId) {
+        if (pendingIntent == null || pendingIntent.getIntent() == null) return false;
 
-        final ComponentName launchingActivity = startIntent.getComponent();
-        if (launchingActivity == null) {
-            return false;
-        }
+        final ComponentName launchingActivity = pendingIntent.getIntent().getComponent();
+        if (launchingActivity == null) return false;
 
-        if (isSplitScreenVisible()) {
-            // To prevent users from constantly dropping the same app to the same side resulting in
-            // a large number of instances in the background.
-            final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position);
-            final ComponentName targetActivity = targetTaskInfo != null
-                    ? targetTaskInfo.baseIntent.getComponent() : null;
-            if (Objects.equals(launchingActivity, targetActivity)) {
-                return false;
+        if (taskId != INVALID_TASK_ID) {
+            final ActivityManager.RunningTaskInfo taskInfo =
+                    mTaskOrganizer.getRunningTaskInfo(taskId);
+            if (taskInfo != null) {
+                return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
             }
-
-            // Allow users to start a new instance the same to adjacent side.
-            final ActivityManager.RunningTaskInfo pairedTaskInfo =
-                    getTaskInfo(SplitLayout.reversePosition(position));
-            final ComponentName pairedActivity = pairedTaskInfo != null
-                    ? pairedTaskInfo.baseIntent.getComponent() : null;
-            return Objects.equals(launchingActivity, pairedActivity);
+            return false;
         }
 
-        final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
-        if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
-            return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+        if (!isSplitScreenVisible()) {
+            // Split screen is not yet activated, check if the current top running task is valid to
+            // split together.
+            final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo();
+            if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) {
+                return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity);
+            }
+            return false;
+        }
+
+        // Compare to the adjacent side of the split to determine if this is launching the same
+        // component adjacently.
+        final ActivityManager.RunningTaskInfo pairedTaskInfo =
+                getTaskInfo(SplitLayout.reversePosition(position));
+        final ComponentName pairedActivity = pairedTaskInfo != null
+                ? pairedTaskInfo.baseIntent.getComponent() : null;
+        return Objects.equals(launchingActivity, pairedActivity);
+    }
+
+    private boolean launchSameComponentAdjacently(PendingIntent pendingIntent1,
+            PendingIntent pendingIntent2) {
+        return Objects.equals(pendingIntent1.getIntent().getComponent(),
+                pendingIntent2.getIntent().getComponent());
+    }
+
+    @VisibleForTesting
+    /** Returns {@code true} if the component supports multi-instances split. */
+    boolean supportMultiInstancesSplit(@Nullable ComponentName launching) {
+        if (launching == null) return false;
+
+        final String componentName = launching.flattenToString();
+        for (int i = 0; i < mMultiInstancesComponents.length; i++) {
+            if (mMultiInstancesComponents[i].equals(componentName)) {
+                return true;
+            }
         }
 
         return false;
@@ -839,14 +957,13 @@
 
         @Override
         public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent,
-                Intent fillInIntent, Bundle options1, int taskId, Bundle options2,
-                int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
-                InstanceId instanceId) {
+                Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio,
+                RemoteAnimationAdapter adapter, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController,
                     "startIntentAndTaskWithLegacyTransition", (controller) ->
-                            controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition(
-                                    pendingIntent, fillInIntent, options1, taskId, options2,
-                                    splitPosition, splitRatio, adapter, instanceId));
+                            controller.startIntentAndTaskWithLegacyTransition(pendingIntent,
+                                    options1, taskId, options2, splitPosition, splitRatio, adapter,
+                                    instanceId));
         }
 
         @Override
@@ -872,14 +989,13 @@
         }
 
         @Override
-        public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
-                @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
-                @SplitPosition int splitPosition, float splitRatio,
-                @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+        public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1,
+                int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
+                float splitRatio, @Nullable RemoteTransition remoteTransition,
+                InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startIntentAndTask",
-                    (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent,
-                            fillInIntent, options1, taskId, options2, splitPosition, splitRatio,
-                            remoteTransition, instanceId));
+                    (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId,
+                            options2, splitPosition, splitRatio, remoteTransition, instanceId));
         }
 
         @Override
@@ -894,6 +1010,27 @@
         }
 
         @Override
+        public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
+                @Nullable Bundle options1, PendingIntent pendingIntent2, @Nullable Bundle options2,
+                @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+                InstanceId instanceId) {
+            executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition",
+                    (controller) ->
+                        controller.startIntentsWithLegacyTransition(
+                                pendingIntent1, options1, pendingIntent2, options2, splitPosition,
+                                splitRatio, adapter, instanceId)
+                    );
+        }
+
+        @Override
+        public void startIntents(PendingIntent pendingIntent1, @Nullable Bundle options1,
+                PendingIntent pendingIntent2, @Nullable Bundle options2,
+                @SplitPosition int splitPosition, float splitRatio,
+                @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+            // TODO(b/259368992): To be implemented.
+        }
+
+        @Override
         public void startShortcut(String packageName, String shortcutId, int position,
                 @Nullable Bundle options, UserHandle user, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startShortcut",
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..acb71a8 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
@@ -24,7 +24,6 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -115,6 +114,7 @@
 import com.android.wm.shell.common.DisplayImeController;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
@@ -169,6 +169,7 @@
     private ValueAnimator mDividerFadeInAnimator;
     private boolean mDividerVisible;
     private boolean mKeyguardShowing;
+    private boolean mShowDecorImmediately;
     private final SyncTransactionQueue mSyncQueue;
     private final ShellTaskOrganizer mTaskOrganizer;
     private final Context mContext;
@@ -226,33 +227,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 +393,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 */);
@@ -428,39 +428,35 @@
     /** Launches an activity into split. */
     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
             @Nullable Bundle options) {
+        if (!ENABLE_SHELL_TRANSITIONS) {
+            startIntentLegacy(intent, fillInIntent, position, options);
+            return;
+        }
+
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         prepareEvictChildTasks(position, evictWct);
 
         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,
+                null /* consumedCallback */,
+                (finishWct, finishT) -> {
+                    if (!evictWct.isEmpty()) {
+                        finishWct.merge(evictWct, true);
                     }
-
-                    @Override
-                    public void onTransitionFinished(WindowContainerTransaction finishWct,
-                            SurfaceControl.Transaction finishT) {
-                        if (!evictWct.isEmpty()) {
-                            finishWct.merge(evictWct, true);
-                        }
-                    }
-                });
+                } /* finishedCallback */);
     }
 
     /** Launches an activity into split by legacy transition. */
     void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
-            @SplitPosition int position, @androidx.annotation.Nullable Bundle options) {
+            @SplitPosition int position, @Nullable Bundle options) {
         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
         prepareEvictChildTasks(position, evictWct);
 
@@ -476,12 +472,6 @@
                                 exitSplitScreen(mMainStage.getChildCount() == 0
                                         ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
                         mSplitUnsupportedToast.show();
-                    } else {
-                        // Switch the split position if launching as MULTIPLE_TASK failed.
-                        if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
-                            setSideStagePosition(SplitLayout.reversePosition(
-                                    getSideStagePosition()), null);
-                        }
                     }
 
                     // Do nothing when the animation was cancelled.
@@ -564,9 +554,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,15 +582,14 @@
         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);
     }
 
     /** Starts a pair of tasks using legacy transition. */
     void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1,
             int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition,
-            float splitRatio, RemoteAnimationAdapter adapter,
-            InstanceId instanceId) {
+            float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (options1 == null) options1 = new Bundle();
         addActivityOptions(options1, mSideStage);
@@ -610,7 +599,20 @@
                 instanceId);
     }
 
-    /** Starts a pair of intent and task using legacy transition. */
+    /** Starts a pair of intents using legacy transition. */
+    void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1,
+            @Nullable Bundle options1, PendingIntent pendingIntent2, Intent fillInIntent2,
+            @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
+            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (options1 == null) options1 = new Bundle();
+        addActivityOptions(options1, mSideStage);
+        wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+
+        startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, options2, splitPosition,
+                splitRatio, adapter, instanceId);
+    }
+
     void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
             @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
@@ -638,12 +640,29 @@
                 instanceId);
     }
 
+    private void startWithLegacyTransition(WindowContainerTransaction wct,
+            @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
+            @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
+            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+        startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent,
+                mainOptions, sidePosition, splitRatio, adapter, instanceId);
+    }
+
+    private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
+            @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
+            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+        startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */,
+                null /* mainFillInIntent */, mainOptions, sidePosition, splitRatio, adapter,
+                instanceId);
+    }
+
     /**
-     * @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)}
      */
     private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
+            @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
             @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
             RemoteAnimationAdapter adapter, InstanceId instanceId) {
         // Init divider first to make divider leash for remote animation target.
@@ -712,7 +731,11 @@
         if (mainOptions == null) mainOptions = new Bundle();
         addActivityOptions(mainOptions, mMainStage);
         updateWindowBounds(mSplitLayout, wct);
-        wct.startTask(mainTaskId, mainOptions);
+        if (mainTaskId == INVALID_TASK_ID) {
+            wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions);
+        } else {
+            wct.startTask(mainTaskId, mainOptions);
+        }
         wct.reorder(mRootTaskInfo.token, true);
         wct.setForceTranslucent(mRootTaskInfo.token, false);
 
@@ -774,9 +797,8 @@
         mSideStage.evictInvisibleChildren(wct);
     }
 
-    Bundle resolveStartStage(@StageType int stage,
-            @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
-            @androidx.annotation.Nullable WindowContainerTransaction wct) {
+    Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
         switch (stage) {
             case STAGE_TYPE_UNDEFINED: {
                 if (position != SPLIT_POSITION_UNDEFINED) {
@@ -847,19 +869,52 @@
                 : mMainStage.getTopVisibleChildTaskId();
     }
 
-    void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) {
-        if (mSideStagePosition == sideStagePosition) return;
-        SurfaceControl.Transaction t = mTransactionPool.acquire();
+    void switchSplitPosition(String reason) {
+        final SurfaceControl.Transaction t = mTransactionPool.acquire();
+        mTempRect1.setEmpty();
         final StageTaskListener topLeftStage =
                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t,
+                topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
         final StageTaskListener bottomRightStage =
                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t,
+                bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
         mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
-                () -> {
-                    setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition),
-                            null /* wct */);
-                    mTransactionPool.release(t);
+                insets -> {
+                    WindowContainerTransaction wct = new WindowContainerTransaction();
+                    setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), wct);
+                    mSyncQueue.queue(wct);
+                    mSyncQueue.runInSync(st -> {
+                        updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */);
+                        st.setPosition(topLeftScreenshot, -insets.left, -insets.top);
+                        st.setPosition(bottomRightScreenshot, insets.left, insets.top);
+
+                        final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
+                        va.addUpdateListener(valueAnimator-> {
+                            final float progress = (float) valueAnimator.getAnimatedValue();
+                            t.setAlpha(topLeftScreenshot, progress);
+                            t.setAlpha(bottomRightScreenshot, progress);
+                            t.apply();
+                        });
+                        va.addListener(new AnimatorListenerAdapter() {
+                            @Override
+                            public void onAnimationEnd(
+                                    @androidx.annotation.NonNull Animator animation) {
+                                t.remove(topLeftScreenshot);
+                                t.remove(bottomRightScreenshot);
+                                t.apply();
+                                mTransactionPool.release(t);
+                            }
+                        });
+                        va.start();
+                    });
                 });
+
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
+        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                mSplitLayout.isLandscape());
     }
 
     void setSideStagePosition(@SplitPosition int sideStagePosition,
@@ -1071,7 +1126,7 @@
             activityTaskManagerService.setFocusedTask(getTaskId(stageToFocus));
         } catch (RemoteException | NullPointerException e) {
             ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                    "%s: Unable to update focus on the chosen stage, %s", TAG, e);
+                    "Unable to update focus on the chosen stage: %s", e.getMessage());
         }
     }
 
@@ -1082,15 +1137,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:
@@ -1408,14 +1463,14 @@
         }
 
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                "%s: Request to %s divider bar from %s.", TAG,
+                "Request to %s divider bar from %s.",
                 (visible ? "show" : "hide"), Debug.getCaller());
 
         // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard
         // dismissing animation.
         if (visible && mKeyguardShowing) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                    "%s:   Defer showing divider bar due to keyguard showing.", TAG);
+                    "   Defer showing divider bar due to keyguard showing.");
             return;
         }
 
@@ -1424,7 +1479,7 @@
 
         if (mIsDividerRemoteAnimating) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                    "%s:   Skip animating divider bar due to it's remote animating.", TAG);
+                    "   Skip animating divider bar due to it's remote animating.");
             return;
         }
 
@@ -1439,12 +1494,12 @@
         final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
         if (dividerLeash == null) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                    "%s:   Skip animating divider bar due to divider leash not ready.", TAG);
+                    "   Skip animating divider bar due to divider leash not ready.");
             return;
         }
         if (mIsDividerRemoteAnimating) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                    "%s:   Skip animating divider bar due to it's remote animating.", TAG);
+                    "   Skip animating divider bar due to it's remote animating.");
             return;
         }
 
@@ -1535,6 +1590,7 @@
                 if (mLogger.isEnterRequestedByDrag()) {
                     updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
                 } else {
+                    mShowDecorImmediately = true;
                     mSplitLayout.flingDividerToCenter();
                 }
             });
@@ -1591,10 +1647,7 @@
 
     @Override
     public void onDoubleTappedDivider() {
-        setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition));
-        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                mSplitLayout.isLandscape());
+        switchSplitPosition("double tap");
     }
 
     @Override
@@ -1607,20 +1660,22 @@
     }
 
     @Override
-    public void onLayoutSizeChanging(SplitLayout layout) {
+    public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) {
         final SurfaceControl.Transaction t = mTransactionPool.acquire();
         t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
         updateSurfaceBounds(layout, t, true /* applyResizingOffset */);
         getMainStageBounds(mTempRect1);
         getSideStageBounds(mTempRect2);
-        mMainStage.onResizing(mTempRect1, mTempRect2, t);
-        mSideStage.onResizing(mTempRect2, mTempRect1, t);
+        mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately);
+        mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately);
         t.apply();
         mTransactionPool.release(t);
     }
 
     @Override
     public void onLayoutSizeChanged(SplitLayout layout) {
+        // Reset this flag every time onLayoutSizeChanged.
+        mShowDecorImmediately = false;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         updateWindowBounds(layout, wct);
         sendOnBoundsChanged();
@@ -1839,7 +1894,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 +1908,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 +1928,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 +2086,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 +2364,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/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 6b90eab..bcf900b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -288,9 +288,11 @@
         }
     }
 
-    void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t) {
+    void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX,
+            int offsetY, boolean immediately) {
         if (mSplitDecorManager != null && mRootTaskInfo != null) {
-            mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t);
+            mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX,
+                    offsetY, immediately);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 8cee4f1..6ce981e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -432,7 +432,8 @@
                     final ShapeIconFactory factory = new ShapeIconFactory(
                             SplashscreenContentDrawer.this.mContext,
                             scaledIconDpi, mFinalIconSize);
-                    final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable);
+                    final Bitmap bitmap = factory.createScaledBitmap(iconDrawable,
+                            BaseIconFactory.MODE_DEFAULT);
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                     createIconDrawable(new BitmapDrawable(bitmap), true,
                             mHighResIconProvider.mLoadInDetail);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index a0e176c..ff6f2b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -430,7 +430,8 @@
         }
 
         @Override
-        public @Nullable SplashScreenView get() {
+        @Nullable
+        public SplashScreenView get() {
             synchronized (this) {
                 while (!mIsViewSet) {
                     try {
@@ -691,7 +692,7 @@
         private final TaskSnapshotWindow mTaskSnapshotWindow;
         private SplashScreenView mContentView;
         private boolean mSetSplashScreen;
-        private @StartingWindowType int mSuggestType;
+        @StartingWindowType private int mSuggestType;
         private int mBGColor;
         private final long mCreateTime;
         private int mSystemBarAppearance;
@@ -732,7 +733,7 @@
 
         // Reset the system bar color which set by splash screen, make it align to the app.
         private void clearSystemBarColor() {
-            if (mDecorView == null) {
+            if (mDecorView == null || !mDecorView.isAttachedToWindow()) {
                 return;
             }
             if (mDecorView.getLayoutParams() instanceof WindowManager.LayoutParams) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 7b498e4..3929e83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -77,6 +77,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.window.ClientWindowFrames;
@@ -223,7 +224,7 @@
         final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow(
                 surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance,
                 windowFlags, windowPrivateFlags, taskBounds, orientation, activityType,
-                topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
+                info.requestedVisibleTypes, clearWindowHandler, splashScreenExecutor);
         final Window window = snapshotSurface.mWindow;
 
         final InsetsState tmpInsetsState = new InsetsState();
@@ -233,7 +234,7 @@
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
             final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
-                    info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls,
+                    info.requestedVisibleTypes, tmpInputChannel, tmpInsetsState, tmpControls,
                     new Rect(), sizeCompatScale);
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             if (res < 0) {
@@ -263,7 +264,7 @@
     public TaskSnapshotWindow(SurfaceControl surfaceControl,
             TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription,
             int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds,
-            int currentOrientation, int activityType, InsetsState topWindowInsetsState,
+            int currentOrientation, int activityType, @InsetsType int requestedVisibleTypes,
             Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) {
         mSplashScreenExecutor = splashScreenExecutor;
         mSession = WindowManagerGlobal.getWindowSession();
@@ -276,7 +277,7 @@
         mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE);
         mTaskBounds = taskBounds;
         mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags,
-                windowPrivateFlags, appearance, taskDescription, 1f, topWindowInsetsState);
+                windowPrivateFlags, appearance, taskDescription, 1f, requestedVisibleTypes);
         mStatusBarColor = taskDescription.getStatusBarColor();
         mOrientationOnCreation = currentOrientation;
         mActivityType = activityType;
@@ -569,11 +570,12 @@
         private final int mWindowFlags;
         private final int mWindowPrivateFlags;
         private final float mScale;
-        private final InsetsState mInsetsState;
+        private final @InsetsType int mRequestedVisibleTypes;
         private final Rect mSystemBarInsets = new Rect();
 
         SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance,
-                TaskDescription taskDescription, float scale, InsetsState insetsState) {
+                TaskDescription taskDescription, float scale,
+                @InsetsType int requestedVisibleTypes) {
             mWindowFlags = windowFlags;
             mWindowPrivateFlags = windowPrivateFlags;
             mScale = scale;
@@ -592,7 +594,7 @@
                             && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim));
             mStatusBarPaint.setColor(mStatusBarColor);
             mNavigationBarPaint.setColor(mNavigationBarColor);
-            mInsetsState = insetsState;
+            mRequestedVisibleTypes = requestedVisibleTypes;
         }
 
         void setInsets(Rect systemBarInsets) {
@@ -603,7 +605,7 @@
             final boolean forceBarBackground =
                     (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
             if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
-                    mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) {
+                    mRequestedVisibleTypes, mStatusBarColor, mWindowFlags, forceBarBackground)) {
                 return (int) (mSystemBarInsets.top * mScale);
             } else {
                 return 0;
@@ -614,7 +616,7 @@
             final boolean forceBarBackground =
                     (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
             return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
-                    mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground);
+                    mRequestedVisibleTypes, mNavigationBarColor, mWindowFlags, forceBarBackground);
         }
 
         void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9c2c2fa..63d4a6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -41,11 +41,13 @@
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
 import static android.window.TransitionInfo.FLAG_FILLS_TASK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
+import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
@@ -319,6 +321,17 @@
         final int wallpaperTransit = getWallpaperTransitType(info);
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.hasAllFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
+                    | FLAG_IS_BEHIND_STARTING_WINDOW)) {
+                // Don't animate embedded activity if it is covered by the starting window.
+                // Non-embedded case still needs animation because the container can still animate
+                // the starting window together, e.g. CLOSE or CHANGE type.
+                continue;
+            }
+            if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) {
+                // Wallpaper, IME, and system windows don't need any default animations.
+                continue;
+            }
             final boolean isTask = change.getTaskInfo() != null;
             boolean isSeamlessDisplayChange = false;
 
@@ -383,6 +396,11 @@
                 }
             }
 
+            // The back gesture has animated this change before transition happen, so here we don't
+            // play the animation again.
+            if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) {
+                continue;
+            }
             // Don't animate anything that isn't independent.
             if (!TransitionInfo.isIndependent(change, info)) continue;
 
@@ -619,12 +637,13 @@
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
-        va.addUpdateListener(animation -> {
+        final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
             final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
 
             applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
                     position, cornerRadius, clipRect);
-        });
+        };
+        va.addUpdateListener(updateListener);
 
         final Runnable finisher = () -> {
             applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
@@ -637,20 +656,30 @@
             });
         };
         va.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
             private boolean mFinished = false;
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (mFinished) return;
-                mFinished = true;
-                finisher.run();
+                onFinish();
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
                 if (mFinished) return;
                 mFinished = true;
                 finisher.run();
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                va.removeUpdateListener(updateListener);
             }
         });
         animations.add(va);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 2b27bae..66d0a2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.transition;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
-import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 import static android.util.RotationUtils.deltaRotation;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
@@ -37,8 +35,6 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
 import android.util.Slog;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -50,12 +46,11 @@
 import android.window.TransitionInfo;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
-import java.util.Arrays;
 
 /**
  * This class handles the rotation animation when the device is rotated.
@@ -173,7 +168,7 @@
                 t.setBuffer(mScreenshotLayer, hardwareBuffer);
                 t.show(mScreenshotLayer);
                 if (!isCustomRotate()) {
-                    mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
+                    mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace);
                 }
             }
 
@@ -404,93 +399,6 @@
         mTransactionPool.release(t);
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    /**
-     * @return whether the hardwareBuffer passed in is marked as protected.
-     */
-    private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
-        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
-    }
-
     private static void applyColor(int startColor, int endColor, float[] rgbFloat,
             float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
         final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
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..56d51bd 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
@@ -24,7 +24,6 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.fixScale;
-import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
 import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -122,6 +121,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 +164,21 @@
                 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() {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mOrganizer.shareTransactionQueue();
+        }
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
+                this::createExternalInterface, this);
 
         ContentResolver resolver = mContext.getContentResolver();
         mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
@@ -186,13 +189,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(
@@ -322,14 +335,26 @@
         boolean isOpening = isOpeningType(info.getType());
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
-            if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) {
+            if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) {
                 // Currently system windows are controlled by WindowState, so don't change their
-                // surfaces. Otherwise their window tokens could be hidden unexpectedly.
+                // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly.
+                // This includes Wallpaper (always z-ordered at bottom) and IME (associated with
+                // app), because there may not be a transition associated with their visibility
+                // changes, and currently they don't need transition animation.
                 continue;
             }
             final SurfaceControl leash = change.getLeash();
             final int mode = info.getChanges().get(i).getMode();
 
+            if (mode == TRANSIT_TO_FRONT
+                    && ((change.getStartAbsBounds().height() != change.getEndAbsBounds().height()
+                    || change.getStartAbsBounds().width() != change.getEndAbsBounds().width()))) {
+                // When the window is moved to front with a different size, make sure the crop is
+                // updated to prevent it from using the old crop.
+                t.setWindowCrop(leash, change.getEndAbsBounds().width(),
+                        change.getEndAbsBounds().height());
+            }
+
             // Don't move anything that isn't independent within its parents
             if (!TransitionInfo.isIndependent(change, info)) {
                 if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) {
@@ -352,16 +377,7 @@
                     finishT.setAlpha(leash, 1.f);
                 }
             } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
-                // Wallpaper/IME are anomalies: their visibility is tied to other WindowStates.
-                // As a result, we actually can't hide their WindowTokens because there may not be a
-                // transition associated with them becoming visible again. Fortunately, since
-                // wallpapers are always z-ordered to the back, we don't have to worry about it
-                // flickering to the front during reparenting. Similarly, the IME is reparented to
-                // the associated app, so its visibility is coupled. So, an explicit hide is not
-                // needed visually anyways.
-                if ((change.getFlags() & (FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD)) == 0) {
-                    finishT.hide(leash);
-                }
+                finishT.hide(leash);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
index e903897..f209521 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
@@ -33,6 +33,8 @@
     // This class is orientation-agnostic, so we compute both for later use
     public final float topTaskPercent;
     public final float leftTaskPercent;
+    public final float dividerWidthPercent;
+    public final float dividerHeightPercent;
     /**
      * If {@code true}, that means at the time of creation of this object, the
      * split-screened apps were vertically stacked. This is useful in scenarios like
@@ -62,8 +64,12 @@
             appsStackedVertically = false;
         }
 
-        leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right;
-        topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom;
+        float totalWidth = rightBottomBounds.right - leftTopBounds.left;
+        float totalHeight = rightBottomBounds.bottom - leftTopBounds.top;
+        leftTaskPercent = leftTopBounds.width() / totalWidth;
+        topTaskPercent = leftTopBounds.height() / totalHeight;
+        dividerWidthPercent = visualDividerBounds.width() / totalWidth;
+        dividerHeightPercent = visualDividerBounds.height() / totalHeight;
     }
 
     public SplitBounds(Parcel parcel) {
@@ -75,6 +81,8 @@
         appsStackedVertically = parcel.readBoolean();
         leftTopTaskId = parcel.readInt();
         rightBottomTaskId = parcel.readInt();
+        dividerWidthPercent = parcel.readInt();
+        dividerHeightPercent = parcel.readInt();
     }
 
     @Override
@@ -87,6 +95,8 @@
         parcel.writeBoolean(appsStackedVertically);
         parcel.writeInt(leftTopTaskId);
         parcel.writeInt(rightBottomTaskId);
+        parcel.writeFloat(dividerWidthPercent);
+        parcel.writeFloat(dividerHeightPercent);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index dca516a..ebe5c5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,11 +26,16 @@
 import android.content.Context;
 import android.hardware.input.InputManager;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Choreographer;
+import android.view.InputChannel;
 import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -64,8 +69,11 @@
     private final SyncTransactionQueue mSyncQueue;
     private FreeformTaskTransitionStarter mTransitionStarter;
     private DesktopModeController mDesktopModeController;
+    private EventReceiver mEventReceiver;
+    private InputMonitor mInputMonitor;
 
     private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+    private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
 
     public CaptionWindowDecorViewModel(
             Context context,
@@ -97,6 +105,11 @@
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
         if (!shouldShowWindowDecor(taskInfo)) return false;
+        CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId);
+        if (oldDecoration != null) {
+            // close the old decoration if it exists to avoid two window decorations being added
+            oldDecoration.close();
+        }
         final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration(
                 mContext,
                 mDisplayController,
@@ -108,12 +121,20 @@
                 mSyncQueue);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
 
-        TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
+        TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration,
+                mDragStartListener);
         CaptionTouchEventListener touchEventListener =
-                new CaptionTouchEventListener(taskInfo, taskPositioner);
+                new CaptionTouchEventListener(taskInfo, taskPositioner,
+                        windowDecoration.getDragDetector());
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
         windowDecoration.setDragResizeCallback(taskPositioner);
         setupWindowDecorationForTransition(taskInfo, startT, finishT);
+        if (mInputMonitor == null) {
+            mInputMonitor = InputManager.getInstance().monitorGestureInput(
+                    "caption-touch", mContext.getDisplayId());
+            mEventReceiver = new EventReceiver(
+                    mInputMonitor.getInputChannel(), Looper.myLooper());
+        }
         return true;
     }
 
@@ -126,23 +147,25 @@
     }
 
     @Override
-    public void setupWindowDecorationForTransition(
+    public boolean setupWindowDecorationForTransition(
             RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT) {
         final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
-        if (decoration == null) return;
+        if (decoration == null) return false;
 
         decoration.relayout(taskInfo, startT, finishT);
+        return true;
     }
 
     @Override
-    public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
+    public boolean destroyWindowDecoration(RunningTaskInfo taskInfo) {
         final CaptionWindowDecoration decoration =
                 mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
-        if (decoration == null) return;
+        if (decoration == null) return false;
 
         decoration.close();
+        return true;
     }
 
     private class CaptionTouchEventListener implements
@@ -151,20 +174,23 @@
         private final int mTaskId;
         private final WindowContainerToken mTaskToken;
         private final DragResizeCallback mDragResizeCallback;
+        private final DragDetector mDragDetector;
 
         private int mDragPointerId = -1;
-        private boolean mDragActive = false;
 
         private CaptionTouchEventListener(
                 RunningTaskInfo taskInfo,
-                DragResizeCallback dragResizeCallback) {
+                DragResizeCallback dragResizeCallback,
+                DragDetector dragDetector) {
             mTaskId = taskInfo.taskId;
             mTaskToken = taskInfo.token;
             mDragResizeCallback = dragResizeCallback;
+            mDragDetector = dragDetector;
         }
 
         @Override
         public void onClick(View v) {
+            CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final int id = v.getId();
             if (id == R.id.close_window) {
                 WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -176,6 +202,15 @@
                 }
             } else if (id == R.id.back_button) {
                 injectBackKey();
+            } else if (id == R.id.caption_handle) {
+                decoration.createHandleMenu();
+            } else if (id == R.id.desktop_button) {
+                mDesktopModeController.setDesktopModeActive(true);
+                decoration.closeHandleMenu();
+            } else if (id == R.id.fullscreen_button) {
+                mDesktopModeController.setDesktopModeActive(false);
+                decoration.closeHandleMenu();
+                decoration.setButtonVisibility();
             }
         }
         private void injectBackKey() {
@@ -199,19 +234,21 @@
 
         @Override
         public boolean onTouch(View v, MotionEvent e) {
+            boolean isDrag = false;
             int id = v.getId();
             if (id != R.id.caption_handle && id != R.id.caption) {
                 return false;
             }
-            if (id == R.id.caption_handle || mDragActive) {
+            if (id == R.id.caption_handle) {
+                isDrag = mDragDetector.detectDragEvent(e);
                 handleEventForMove(e);
             }
             if (e.getAction() != MotionEvent.ACTION_DOWN) {
-                return false;
+                return isDrag;
             }
             RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
             if (taskInfo.isFocused) {
-                return false;
+                return isDrag;
             }
             WindowContainerTransaction wct = new WindowContainerTransaction();
             wct.reorder(mTaskToken, true /* onTop */);
@@ -219,6 +256,10 @@
             return true;
         }
 
+        /**
+         * @param e {@link MotionEvent} to process
+         * @return {@code true} if a drag is happening; or {@code false} if it is not
+         */
         private void handleEventForMove(MotionEvent e) {
             RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
             int windowingMode =  mDesktopModeController
@@ -227,12 +268,12 @@
                 return;
             }
             switch (e.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    mDragActive = true;
-                    mDragPointerId  = e.getPointerId(0);
+                case MotionEvent.ACTION_DOWN: {
+                    mDragPointerId = e.getPointerId(0);
                     mDragResizeCallback.onDragResizeStart(
                             0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
                     break;
+                }
                 case MotionEvent.ACTION_MOVE: {
                     int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     mDragResizeCallback.onDragResizeMove(
@@ -241,7 +282,6 @@
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
-                    mDragActive = false;
                     int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                     int statusBarHeight = mDisplayController.getDisplayLayout(taskInfo.displayId)
                             .stableInsets().top;
@@ -257,6 +297,36 @@
         }
     }
 
+    // InputEventReceiver to listen for touch input outside of caption bounds
+    private class EventReceiver extends InputEventReceiver {
+        EventReceiver(InputChannel channel, Looper looper) {
+            super(channel, looper);
+        }
+
+        @Override
+        public void onInputEvent(InputEvent event) {
+            boolean handled = false;
+            if (event instanceof MotionEvent
+                    && ((MotionEvent) event).getActionMasked() == MotionEvent.ACTION_UP) {
+                handled = true;
+                CaptionWindowDecorViewModel.this.handleMotionEvent((MotionEvent) event);
+            }
+            finishInputEvent(event, handled);
+        }
+    }
+
+    // If any input received is outside of caption bounds, turn off handle menu
+    private void handleMotionEvent(MotionEvent ev) {
+        int size = mWindowDecorByTaskId.size();
+        for (int i = 0; i < size; i++) {
+            CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+            if (decoration != null) {
+                decoration.closeHandleMenuIfNeeded(ev);
+            }
+        }
+    }
+
+
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
         return DesktopModeStatus.IS_SUPPORTED
@@ -264,4 +334,11 @@
                 && mDisplayController.getDisplayContext(taskInfo.displayId)
                 .getResources().getConfiguration().smallestScreenWidthDp >= 600;
     }
+
+    private class DragStartListenerImpl implements TaskPositioner.DragStartListener{
+        @Override
+        public void onDragStart(int taskId) {
+            mWindowDecorByTaskId.get(taskId).closeHandleMenu();
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 87700ee..affde30 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -20,11 +20,14 @@
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Resources;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
 import android.view.Choreographer;
+import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -43,22 +46,6 @@
  * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
  */
 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
-    // The thickness of shadows of a window that has focus in DIP.
-    private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
-    // The thickness of shadows of a window that doesn't have focus in DIP.
-    private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
-
-    // Height of button (32dp)  + 2 * margin (5dp each)
-    private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
-    // Width of buttons (64dp) + handle (128dp) + padding (24dp total)
-    private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216;
-    private static final int RESIZE_HANDLE_IN_DIP = 30;
-    private static final int RESIZE_CORNER_IN_DIP = 44;
-
-    private static final Rect EMPTY_OUTSET = new Rect();
-    private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
-            RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);
-
     private final Handler mHandler;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
@@ -69,11 +56,16 @@
 
     private DragResizeInputListener mDragResizeListener;
 
+    private RelayoutParams mRelayoutParams = new RelayoutParams();
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
 
     private boolean mDesktopActive;
 
+    private DragDetector mDragDetector;
+
+    private AdditionalWindow mHandleMenu;
+
     CaptionWindowDecoration(
             Context context,
             DisplayController displayController,
@@ -89,6 +81,7 @@
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
         mDesktopActive = DesktopModeStatus.isActive(mContext);
+        mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
     }
 
     void setCaptionListeners(
@@ -102,6 +95,10 @@
         mDragResizeCallback = dragResizeCallback;
     }
 
+    DragDetector getDragDetector() {
+        return mDragDetector;
+    }
+
     @Override
     void relayout(ActivityManager.RunningTaskInfo taskInfo) {
         final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
@@ -114,19 +111,45 @@
 
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
-        final int shadowRadiusDp = taskInfo.isFocused
-                ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
-        final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
-                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
-        final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
-        final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;
+        final int shadowRadiusID = taskInfo.isFocused
+                ? R.dimen.freeform_decor_shadow_focused_thickness
+                : R.dimen.freeform_decor_shadow_unfocused_thickness;
+        final boolean isFreeform =
+                taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+        final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
 
         WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
-                DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp,
-                startT, finishT, wct, mResult);
+
+        int outsetLeftId = R.dimen.freeform_resize_handle;
+        int outsetTopId = R.dimen.freeform_resize_handle;
+        int outsetRightId = R.dimen.freeform_resize_handle;
+        int outsetBottomId = R.dimen.freeform_resize_handle;
+
+        mRelayoutParams.reset();
+        mRelayoutParams.mRunningTaskInfo = taskInfo;
+        mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
+        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
+        mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+        if (isDragResizeable) {
+            mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+        }
+        final Resources resources = mDecorWindowContext.getResources();
+        final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
+        final int captionHeight = loadDimensionPixelSize(resources,
+                mRelayoutParams.mCaptionHeightId);
+        final int captionWidth = loadDimensionPixelSize(resources,
+                mRelayoutParams.mCaptionWidthId);
+        final int captionLeft = taskBounds.width() / 2
+                - captionWidth / 2;
+        final int captionTop = taskBounds.top
+                <= captionHeight / 2 ? 0 : -captionHeight / 2;
+        mRelayoutParams.setCaptionPosition(captionLeft, captionTop);
+
+        relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+        taskInfo = null; // Clear it just in case we use it accidentally
 
         mTaskOrganizer.applyTransaction(wct);
 
@@ -140,15 +163,14 @@
         }
 
         // If this task is not focused, do not show caption.
-        setCaptionVisibility(taskInfo.isFocused);
+        setCaptionVisibility(mTaskInfo.isFocused);
 
         // Only handle should show if Desktop Mode is inactive.
         boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext);
-        if (mDesktopActive != desktopCurrentStatus && taskInfo.isFocused) {
+        if (mDesktopActive != desktopCurrentStatus && mTaskInfo.isFocused) {
             mDesktopActive = desktopCurrentStatus;
             setButtonVisibility();
         }
-        taskInfo = null; // Clear it just in case we use it accidentally
 
         if (!isDragResizeable) {
             closeDragResizeListener();
@@ -167,10 +189,14 @@
         }
 
         int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
+        mDragDetector.setTouchSlop(touchSlop);
 
+        int resize_handle = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+        int resize_corner = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_corner);
         mDragResizeListener.setGeometry(
-                mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP),
-                (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop);
+                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
     }
 
     /**
@@ -185,9 +211,22 @@
         back.setOnClickListener(mOnCaptionButtonClickListener);
         View handle = caption.findViewById(R.id.caption_handle);
         handle.setOnTouchListener(mOnCaptionTouchListener);
+        handle.setOnClickListener(mOnCaptionButtonClickListener);
         setButtonVisibility();
     }
 
+    private void setupHandleMenu() {
+        View menu = mHandleMenu.mWindowViewHost.getView();
+        View fullscreen = menu.findViewById(R.id.fullscreen_button);
+        fullscreen.setOnClickListener(mOnCaptionButtonClickListener);
+        View desktop = menu.findViewById(R.id.desktop_button);
+        desktop.setOnClickListener(mOnCaptionButtonClickListener);
+        View split = menu.findViewById(R.id.split_screen_button);
+        split.setOnClickListener(mOnCaptionButtonClickListener);
+        View more = menu.findViewById(R.id.more_button);
+        more.setOnClickListener(mOnCaptionButtonClickListener);
+    }
+
     /**
      * Sets caption visibility based on task focus.
      *
@@ -195,8 +234,9 @@
      */
     private void setCaptionVisibility(boolean visible) {
         int v = visible ? View.VISIBLE : View.GONE;
-        View caption = mResult.mRootView.findViewById(R.id.caption);
-        caption.setVisibility(v);
+        View captionView = mResult.mRootView.findViewById(R.id.caption);
+        captionView.setVisibility(v);
+        if (!visible) closeHandleMenu();
     }
 
     /**
@@ -204,6 +244,7 @@
      *
      */
     public void setButtonVisibility() {
+        mDesktopActive = DesktopModeStatus.isActive(mContext);
         int v = mDesktopActive ? View.VISIBLE : View.GONE;
         View caption = mResult.mRootView.findViewById(R.id.caption);
         View back = caption.findViewById(R.id.back_button);
@@ -221,6 +262,10 @@
         caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT);
     }
 
+    public boolean isHandleMenuActive() {
+        return mHandleMenu != null;
+    }
+
     private void closeDragResizeListener() {
         if (mDragResizeListener == null) {
             return;
@@ -229,9 +274,67 @@
         mDragResizeListener = null;
     }
 
+    /**
+     * Create and display handle menu window
+     */
+    public void createHandleMenu() {
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        final Resources resources = mDecorWindowContext.getResources();
+        int x = mRelayoutParams.mCaptionX;
+        int y = mRelayoutParams.mCaptionY;
+        int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+        int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+        String namePrefix = "Caption Menu";
+        mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t,
+                x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY,
+                width, height);
+        mSyncQueue.runInSync(transaction -> {
+            transaction.merge(t);
+            t.close();
+        });
+        setupHandleMenu();
+    }
+
+    /**
+     * Close the handle menu window
+     */
+    public void closeHandleMenu() {
+        if (!isHandleMenuActive()) return;
+        mHandleMenu.releaseView();
+        mHandleMenu = null;
+    }
+
+    @Override
+    void releaseViews() {
+        closeHandleMenu();
+        super.releaseViews();
+    }
+
+    /**
+     * Close an open handle menu if input is outside of menu coordinates
+     * @param ev the tapped point to compare against
+     * @return
+     */
+    public void closeHandleMenuIfNeeded(MotionEvent ev) {
+        if (mHandleMenu != null) {
+            Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
+                    .positionInParent;
+            final Resources resources = mDecorWindowContext.getResources();
+            ev.offsetLocation(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
+            ev.offsetLocation(-positionInParent.x, -positionInParent.y);
+            int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+            int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+            if (!(ev.getX() >= 0 && ev.getY()  >= 0
+                    && ev.getX()  <= width && ev.getY()  <= height)) {
+                closeHandleMenu();
+            }
+        }
+    }
+
     @Override
     public void close() {
         closeDragResizeListener();
+        closeHandleMenu();
         super.close();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
new file mode 100644
index 0000000..0abe8ab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -0,0 +1,87 @@
+/*
+ * 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.wm.shell.windowdecor;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+
+/**
+ * A detector for touch inputs that differentiates between drag and click inputs.
+ * All touch events must be passed through this class to track a drag event.
+ */
+public class DragDetector {
+    private int mTouchSlop;
+    private PointF mInputDownPoint;
+    private boolean mIsDragEvent;
+    private int mDragPointerId;
+    public DragDetector(int touchSlop) {
+        mTouchSlop = touchSlop;
+        mInputDownPoint = new PointF();
+        mIsDragEvent = false;
+        mDragPointerId = -1;
+    }
+
+    /**
+     * Determine if {@link MotionEvent} is part of a drag event.
+     * @return {@code true} if this is a drag event, {@code false} if not
+     */
+    public boolean detectDragEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case ACTION_DOWN: {
+                mDragPointerId = ev.getPointerId(0);
+                float rawX = ev.getRawX(0);
+                float rawY = ev.getRawY(0);
+                mInputDownPoint.set(rawX, rawY);
+                return false;
+            }
+            case ACTION_MOVE: {
+                if (!mIsDragEvent) {
+                    int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
+                    float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
+                    float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
+                    if (Math.hypot(dx, dy) > mTouchSlop) {
+                        mIsDragEvent = true;
+                    }
+                }
+                return mIsDragEvent;
+            }
+            case ACTION_UP: {
+                boolean result = mIsDragEvent;
+                mIsDragEvent = false;
+                mInputDownPoint.set(0, 0);
+                mDragPointerId = -1;
+                return result;
+            }
+            case ACTION_CANCEL: {
+                mIsDragEvent = false;
+                mInputDownPoint.set(0, 0);
+                mDragPointerId = -1;
+                return false;
+            }
+        }
+        return mIsDragEvent;
+    }
+
+    public void setTouchSlop(int touchSlop) {
+        mTouchSlop = touchSlop;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index b9f16b6..d3f1332 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -22,7 +22,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import android.content.Context;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
@@ -38,6 +37,7 @@
 import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.view.SurfaceControl;
+import android.view.ViewConfiguration;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.view.BaseIWindow;
@@ -76,7 +76,7 @@
     private Rect mRightBottomCornerBounds;
 
     private int mDragPointerId = -1;
-    private int mTouchSlop;
+    private DragDetector mDragDetector;
 
     DragResizeInputListener(
             Context context,
@@ -115,6 +115,7 @@
         mInputEventReceiver = new TaskResizeInputEventReceiver(
                 mInputChannel, mHandler, mChoreographer);
         mCallback = callback;
+        mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
     }
 
     /**
@@ -146,7 +147,7 @@
         mHeight = height;
         mResizeHandleThickness = resizeHandleThickness;
         mCornerSize = cornerSize;
-        mTouchSlop = touchSlop;
+        mDragDetector.setTouchSlop(touchSlop);
 
         Region touchRegion = new Region();
         final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness);
@@ -228,7 +229,6 @@
         private boolean mConsumeBatchEventScheduled;
         private boolean mShouldHandleEvents;
         private boolean mDragging;
-        private final PointF mActionDownPoint = new PointF();
 
         private TaskResizeInputEventReceiver(
                 InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -276,7 +276,9 @@
             // Check if this is a touch event vs mouse event.
             // Touch events are tracked in four corners. Other events are tracked in resize edges.
             boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
-
+            if (isTouch) {
+                mDragging = mDragDetector.detectDragEvent(e);
+            }
             switch (e.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN: {
                     float x = e.getX(0);
@@ -290,7 +292,6 @@
                         mDragPointerId = e.getPointerId(0);
                         float rawX = e.getRawX(0);
                         float rawY = e.getRawY(0);
-                        mActionDownPoint.set(rawX, rawY);
                         int ctrlType = calculateCtrlType(isTouch, x, y);
                         mCallback.onDragResizeStart(ctrlType, rawX, rawY);
                         result = true;
@@ -304,14 +305,7 @@
                     int dragPointerIndex = e.findPointerIndex(mDragPointerId);
                     float rawX = e.getRawX(dragPointerIndex);
                     float rawY = e.getRawY(dragPointerIndex);
-                    if (isTouch) {
-                        // Check for touch slop for touch events
-                        float dx = rawX - mActionDownPoint.x;
-                        float dy = rawY - mActionDownPoint.y;
-                        if (!mDragging && Math.hypot(dx, dy) > mTouchSlop) {
-                            mDragging = true;
-                        }
-                    } else {
+                    if (!isTouch) {
                         // For all other types allow immediate dragging.
                         mDragging = true;
                     }
@@ -323,14 +317,13 @@
                 }
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL: {
-                    if (mDragging) {
+                    if (mShouldHandleEvents && mDragging) {
                         int dragPointerIndex = e.findPointerIndex(mDragPointerId);
                         mCallback.onDragResizeEnd(
                                 e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
                     }
                     mDragging = false;
                     mShouldHandleEvents = false;
-                    mActionDownPoint.set(0, 0);
                     mDragPointerId = -1;
                     result = true;
                     break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
index 27c1011..a49a300 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
@@ -40,16 +40,29 @@
     private final Rect mTaskBoundsAtDragStart = new Rect();
     private final PointF mResizeStartPoint = new PointF();
     private final Rect mResizeTaskBounds = new Rect();
+    // Whether the |dragResizing| hint should be sent with the next bounds change WCT.
+    // Used to optimized fluid resizing of freeform tasks.
+    private boolean mPendingDragResizeHint = false;
 
     private int mCtrlType;
+    private DragStartListener mDragStartListener;
 
-    TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) {
+    TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+            DragStartListener dragStartListener) {
         mTaskOrganizer = taskOrganizer;
         mWindowDecoration = windowDecoration;
+        mDragStartListener = dragStartListener;
     }
 
     @Override
     public void onDragResizeStart(int ctrlType, float x, float y) {
+        if (ctrlType != CTRL_TYPE_UNDEFINED) {
+            // The task is being resized, send the |dragResizing| hint to core with the first
+            // bounds-change wct.
+            mPendingDragResizeHint = true;
+        }
+
+        mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
         mCtrlType = ctrlType;
 
         mTaskBoundsAtDragStart.set(
@@ -59,19 +72,31 @@
 
     @Override
     public void onDragResizeMove(float x, float y) {
-        changeBounds(x, y);
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (changeBounds(wct, x, y)) {
+            if (mPendingDragResizeHint) {
+                // This is the first bounds change since drag resize operation started.
+                wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
+                mPendingDragResizeHint = false;
+            }
+            mTaskOrganizer.applyTransaction(wct);
+        }
     }
 
     @Override
     public void onDragResizeEnd(float x, float y) {
-        changeBounds(x, y);
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
+        changeBounds(wct, x, y);
+        mTaskOrganizer.applyTransaction(wct);
 
         mCtrlType = 0;
         mTaskBoundsAtDragStart.setEmpty();
         mResizeStartPoint.set(0, 0);
+        mPendingDragResizeHint = false;
     }
 
-    private void changeBounds(float x, float y) {
+    private boolean changeBounds(WindowContainerTransaction wct, float x, float y) {
         float deltaX = x - mResizeStartPoint.x;
         mResizeTaskBounds.set(mTaskBoundsAtDragStart);
         if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
@@ -92,9 +117,17 @@
         }
 
         if (!mResizeTaskBounds.isEmpty()) {
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
-            mTaskOrganizer.applyTransaction(wct);
+            return true;
         }
+        return false;
+    }
+
+    interface DragStartListener {
+        /**
+         * Inform the implementing class that a drag resize has started
+         * @param taskId id of this positioner's {@link WindowDecoration}
+         */
+        void onDragStart(int taskId);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index d7f71c8..2ce4d04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -44,7 +44,7 @@
      * @param taskSurface the surface of the task
      * @param startT the start transaction to be applied before the transition
      * @param finishT the finish transaction to restore states after the transition
-     * @return the window decoration object
+     * @return {@code true} if window decoration was created, {@code false} otherwise
      */
     boolean createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo,
@@ -66,8 +66,9 @@
      *
      * @param startT the start transaction to be applied before the transition
      * @param finishT the finish transaction to restore states after the transition
+     * @return {@code true} if window decoration exists, {@code false} otherwise
      */
-    void setupWindowDecorationForTransition(
+    boolean setupWindowDecorationForTransition(
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT,
             SurfaceControl.Transaction finishT);
@@ -76,6 +77,7 @@
      * Destroys the window decoration of the give task.
      *
      * @param taskInfo the info of the task
+     * @return {@code true} if window decoration was destroyed, {@code false} otherwise
      */
-    void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
+    boolean destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index bf863ea..7ecb3f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -19,11 +19,11 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.InsetsState;
 import android.view.LayoutInflater;
@@ -91,7 +91,7 @@
     SurfaceControl mTaskBackgroundSurface;
 
     SurfaceControl mCaptionContainerSurface;
-    private CaptionWindowManager mCaptionWindowManager;
+    private WindowlessWindowManager mCaptionWindowManager;
     private SurfaceControlViewHost mViewHost;
 
     private final Rect mCaptionInsetsRect = new Rect();
@@ -142,15 +142,14 @@
      */
     abstract void relayout(RunningTaskInfo taskInfo);
 
-    void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
-            float captionWidthDp, Rect outsetsDp, float shadowRadiusDp,
-            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
-            WindowContainerTransaction wct, RelayoutResult<T> outResult) {
+    void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
+            RelayoutResult<T> outResult) {
         outResult.reset();
 
         final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
-        if (taskInfo != null) {
-            mTaskInfo = taskInfo;
+        if (params.mRunningTaskInfo != null) {
+            mTaskInfo = params.mRunningTaskInfo;
         }
 
         if (!mTaskInfo.isVisible) {
@@ -159,7 +158,7 @@
             return;
         }
 
-        if (rootView == null && layoutResId == 0) {
+        if (rootView == null && params.mLayoutResId == 0) {
             throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
         }
 
@@ -176,15 +175,15 @@
                 return;
             }
             mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
-            if (layoutResId != 0) {
-                outResult.mRootView =
-                        (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            if (params.mLayoutResId != 0) {
+                outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                                .inflate(params.mLayoutResId, null);
             }
         }
 
         if (outResult.mRootView == null) {
-            outResult.mRootView =
-                    (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                            .inflate(params.mLayoutResId , null);
         }
 
         // DecorationContainerSurface
@@ -200,18 +199,20 @@
         }
 
         final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
-        outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
-        final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
-        final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
+        final Resources resources = mDecorWindowContext.getResources();
+        outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+        outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
         outResult.mWidth = taskBounds.width()
-                + (int) (outsetsDp.right * outResult.mDensity)
-                - decorContainerOffsetX;
+                + loadDimensionPixelSize(resources, params.mOutsetRightId)
+                - outResult.mDecorContainerOffsetX;
         outResult.mHeight = taskBounds.height()
-                + (int) (outsetsDp.bottom * outResult.mDensity)
-                - decorContainerOffsetY;
+                + loadDimensionPixelSize(resources, params.mOutsetBottomId)
+                - outResult.mDecorContainerOffsetY;
         startT.setPosition(
-                        mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
-                .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
+                        mDecorationContainerSurface,
+                        outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY)
+                .setWindowCrop(mDecorationContainerSurface,
+                        outResult.mWidth, outResult.mHeight)
                 // TODO(b/244455401): Change the z-order when it's better organized
                 .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
                 .show(mDecorationContainerSurface);
@@ -226,12 +227,13 @@
                     .build();
         }
 
-        float shadowRadius = outResult.mDensity * shadowRadiusDp;
+        float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
         int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
         mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
         mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
         mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
-        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
+        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(),
+                        taskBounds.height())
                 .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
                 .setColor(mTaskBackgroundSurface, mTmpColor)
                 // TODO(b/244455401): Change the z-order when it's better organized
@@ -248,24 +250,22 @@
                     .build();
         }
 
-        final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
-        final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity);
-
-        //Prevent caption from going offscreen if task is too high up
-        final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
+        final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+        final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId);
 
         startT.setPosition(
-                        mCaptionContainerSurface, -decorContainerOffsetX
-                                + taskBounds.width() / 2 - captionWidth / 2,
-                        -decorContainerOffsetY - captionYPos)
-                .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight)
+                        mCaptionContainerSurface,
+                        -outResult.mDecorContainerOffsetX + params.mCaptionX,
+                        -outResult.mDecorContainerOffsetY + params.mCaptionY)
+                .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
                 .show(mCaptionContainerSurface);
 
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
             // of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
-            mCaptionWindowManager = new CaptionWindowManager(
-                    mTaskInfo.getConfiguration(), mCaptionContainerSurface);
+            mCaptionWindowManager = new WindowlessWindowManager(
+                    mTaskInfo.getConfiguration(), mCaptionContainerSurface,
+                    null /* hostInputToken */);
         }
 
         // Caption view
@@ -289,8 +289,10 @@
 
             // Caption insets
             mCaptionInsetsRect.set(taskBounds);
-            mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos;
-            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
+            mCaptionInsetsRect.bottom =
+                    mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
+            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
+                    CAPTION_INSETS_TYPES);
         } else {
             startT.hide(mCaptionContainerSurface);
         }
@@ -298,10 +300,10 @@
         // Task surface itself
         Point taskPosition = mTaskInfo.positionInParent;
         mTaskSurfaceCrop.set(
-                decorContainerOffsetX,
-                decorContainerOffsetY,
-                outResult.mWidth + decorContainerOffsetX,
-                outResult.mHeight + decorContainerOffsetY);
+                outResult.mDecorContainerOffsetX,
+                outResult.mDecorContainerOffsetY,
+                outResult.mWidth + outResult.mDecorContainerOffsetX,
+                outResult.mHeight + outResult.mDecorContainerOffsetY);
         startT.show(mTaskSurface);
         finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
                 .setCrop(mTaskSurface, mTaskSurfaceCrop);
@@ -322,7 +324,7 @@
         return true;
     }
 
-    private void releaseViews() {
+    void releaseViews() {
         if (mViewHost != null) {
             mViewHost.release();
             mViewHost = null;
@@ -365,34 +367,159 @@
         releaseViews();
     }
 
+    static int loadDimensionPixelSize(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
+        }
+        return resources.getDimensionPixelSize(resourceId);
+    }
+
+    static float loadDimension(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
+        }
+        return resources.getDimension(resourceId);
+    }
+
+    /**
+     * Create a window associated with this WindowDecoration.
+     * Note that subclass must dispose of this when the task is hidden/closed.
+     * @param layoutId layout to make the window from
+     * @param t the transaction to apply
+     * @param xPos x position of new window
+     * @param yPos y position of new window
+     * @param width width of new window
+     * @param height height of new window
+     * @return
+     */
+    AdditionalWindow addWindow(int layoutId, String namePrefix,
+            SurfaceControl.Transaction t, int xPos, int yPos, int width, int height) {
+        final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+        SurfaceControl windowSurfaceControl = builder
+                .setName(namePrefix + " of Task=" + mTaskInfo.taskId)
+                .setContainerLayer()
+                .setParent(mDecorationContainerSurface)
+                .build();
+        View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null);
+
+        t.setPosition(
+                windowSurfaceControl, xPos, yPos)
+                .setWindowCrop(windowSurfaceControl, width, height)
+                .show(windowSurfaceControl);
+        final WindowManager.LayoutParams lp =
+                new WindowManager.LayoutParams(width, height,
+                        WindowManager.LayoutParams.TYPE_APPLICATION,
+                        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+        lp.setTitle("Additional window of Task=" + mTaskInfo.taskId);
+        lp.setTrustedOverlay();
+        WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration,
+                windowSurfaceControl, null /* hostInputToken */);
+        SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory
+                .create(mDecorWindowContext, mDisplay, windowManager);
+        viewHost.setView(v, lp);
+        return new AdditionalWindow(windowSurfaceControl, viewHost,
+                mSurfaceControlTransactionSupplier);
+    }
+
+    static class RelayoutParams{
+        RunningTaskInfo mRunningTaskInfo;
+        int mLayoutResId;
+        int mCaptionHeightId;
+        int mCaptionWidthId;
+        int mShadowRadiusId;
+
+        int mOutsetTopId;
+        int mOutsetBottomId;
+        int mOutsetLeftId;
+        int mOutsetRightId;
+
+        int mCaptionX;
+        int mCaptionY;
+
+        void setOutsets(int leftId, int topId, int rightId, int bottomId) {
+            mOutsetLeftId = leftId;
+            mOutsetTopId = topId;
+            mOutsetRightId = rightId;
+            mOutsetBottomId = bottomId;
+        }
+
+        void setCaptionPosition(int left, int top) {
+            mCaptionX = left;
+            mCaptionY = top;
+        }
+
+        void reset() {
+            mLayoutResId = Resources.ID_NULL;
+            mCaptionHeightId = Resources.ID_NULL;
+            mCaptionWidthId = Resources.ID_NULL;
+            mShadowRadiusId = Resources.ID_NULL;
+
+            mOutsetTopId = Resources.ID_NULL;
+            mOutsetBottomId = Resources.ID_NULL;
+            mOutsetLeftId = Resources.ID_NULL;
+            mOutsetRightId = Resources.ID_NULL;
+
+            mCaptionX = 0;
+            mCaptionY = 0;
+        }
+    }
+
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
         int mWidth;
         int mHeight;
-        float mDensity;
         T mRootView;
+        int mDecorContainerOffsetX;
+        int mDecorContainerOffsetY;
 
         void reset() {
             mWidth = 0;
             mHeight = 0;
-            mDensity = 0;
+            mDecorContainerOffsetX = 0;
+            mDecorContainerOffsetY = 0;
             mRootView = null;
         }
     }
 
-    private static class CaptionWindowManager extends WindowlessWindowManager {
-        CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
-            super(config, rootSurface, null /* hostInputToken */);
-        }
-
-        @Override
-        public void setConfiguration(Configuration configuration) {
-            super.setConfiguration(configuration);
-        }
-    }
-
     interface SurfaceControlViewHostFactory {
         default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
             return new SurfaceControlViewHost(c, d, wmm);
         }
     }
-}
\ No newline at end of file
+
+    /**
+     * Subclass for additional windows associated with this WindowDecoration
+     */
+    static class AdditionalWindow {
+        SurfaceControl mWindowSurface;
+        SurfaceControlViewHost mWindowViewHost;
+        Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+
+        private AdditionalWindow(SurfaceControl surfaceControl,
+                SurfaceControlViewHost surfaceControlViewHost,
+                Supplier<SurfaceControl.Transaction> transactionSupplier) {
+            mWindowSurface = surfaceControl;
+            mWindowViewHost = surfaceControlViewHost;
+            mTransactionSupplier = transactionSupplier;
+        }
+
+        void releaseView() {
+            WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM();
+
+            if (mWindowViewHost != null) {
+                mWindowViewHost.release();
+                mWindowViewHost = null;
+            }
+            windowManager = null;
+            final SurfaceControl.Transaction t = mTransactionSupplier.get();
+            boolean released = false;
+            if (mWindowSurface != null) {
+                t.remove(mWindowSurface);
+                mWindowSurface = null;
+                released = true;
+            }
+            if (released) {
+                t.apply();
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index f4efc37..1c28c3d 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -6,3 +6,4 @@
 lbill@google.com
 madym@google.com
 hwwang@google.com
+chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
index 2d6e8f5..08913c6 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
@@ -13,6 +13,8 @@
         <option name="run-command" value="cmd window tracing level all" />
         <!-- set WM tracing to frame (avoid incomplete states) -->
         <option name="run-command" value="cmd window tracing frame" />
+        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+        <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
         <!-- ensure lock screen mode is swipe -->
         <option name="run-command" value="locksettings set-disabled false" />
         <!-- restart launcher to activate TAPL -->
@@ -34,4 +36,4 @@
         <option name="collect-on-run-ended-only" value="true" />
         <option name="clean-up" value="true" />
     </metrics_collector>
-</configuration>
\ No newline at end of file
+</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index 6f1ff99..8765ad1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -45,14 +45,23 @@
 fun FlickerTestParameter.splitScreenEntered(
     component1: IComponentMatcher,
     component2: IComponentMatcher,
-    fromOtherApp: Boolean
+    fromOtherApp: Boolean,
+    appExistAtStart: Boolean = true
 ) {
     if (fromOtherApp) {
-        appWindowIsInvisibleAtStart(component1)
+        if (appExistAtStart) {
+            appWindowIsInvisibleAtStart(component1)
+        } else {
+            appWindowIsNotContainAtStart(component1)
+        }
     } else {
         appWindowIsVisibleAtStart(component1)
     }
-    appWindowIsInvisibleAtStart(component2)
+    if (appExistAtStart) {
+        appWindowIsInvisibleAtStart(component2)
+    } else {
+        appWindowIsNotContainAtStart(component2)
+    }
     splitScreenDividerIsInvisibleAtStart()
 
     appWindowIsVisibleAtEnd(component1)
@@ -315,6 +324,10 @@
     assertWmEnd { this.isAppWindowInvisible(component) }
 }
 
+fun FlickerTestParameter.appWindowIsNotContainAtStart(component: IComponentMatcher) {
+    assertWmStart { this.notContains(component) }
+}
+
 fun FlickerTestParameter.appWindowKeepVisible(component: IComponentMatcher) {
     assertWm { this.isAppWindowVisible(component) }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
index 7997892..651d935 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
@@ -21,6 +21,7 @@
 import com.android.server.wm.traces.common.ComponentNameMatcher
 
 const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
+const val LAUNCHER_UI_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
 val APP_PAIR_SPLIT_DIVIDER_COMPONENT = ComponentNameMatcher("", "AppPairSplitDivider#")
 val DOCKED_STACK_DIVIDER_COMPONENT = ComponentNameMatcher("", "DockedStackDivider#")
 val SPLIT_SCREEN_DIVIDER_COMPONENT = ComponentNameMatcher("", "StageCoordinatorSplitDivider#")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index f802539..7546a55 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -80,7 +80,7 @@
             transitions { tapl.goHome() }
         }
 
-    @FlakyTest
+    @FlakyTest(bugId = 256863309)
     @Test
     override fun pipLayerReduces() {
         testSpec.assertLayers {
@@ -108,14 +108,6 @@
         }
     }
 
-    @FlakyTest(bugId = 239807171)
-    @Test
-    override fun pipAppLayerAlwaysVisible() = super.pipAppLayerAlwaysVisible()
-
-    @FlakyTest(bugId = 239807171)
-    @Test
-    override fun pipLayerRemainInsideVisibleBounds() = super.pipLayerRemainInsideVisibleBounds()
-
     @Presubmit
     @Test
     override fun focusChanges() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 9e76575..9533b91 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -24,6 +24,8 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.EdgeExtensionComponentMatcher
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
 import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
@@ -49,11 +51,13 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
     private val textEditApp = SplitScreenUtils.getIme(instrumentation)
+    private val MagnifierLayer = ComponentNameMatcher("", "magnifier surface bbq wrapper#")
+    private val PopupWindowLayer = ComponentNameMatcher("", "PopupWindow:")
 
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
-            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, textEditApp) }
+            setup { SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, textEditApp) }
             transitions {
                 SplitScreenUtils.copyContentInSplit(
                     instrumentation,
@@ -159,8 +163,18 @@
     /** {@inheritDoc} */
     @Presubmit
     @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+        testSpec.assertLayers {
+            this.visibleLayersShownMoreThanOneConsecutiveEntry(
+                ignoreLayers = listOf(
+                    ComponentNameMatcher.SPLASH_SCREEN,
+                    ComponentNameMatcher.SNAPSHOT,
+                    ComponentNameMatcher.IME_SNAPSHOT,
+                    EdgeExtensionComponentMatcher(),
+                    MagnifierLayer,
+                    PopupWindowLayer))
+        }
+    }
 
     /** {@inheritDoc} */
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index fa783f2..4757498 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -26,7 +26,6 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -35,7 +34,6 @@
 import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
 import com.android.wm.shell.flicker.splitScreenDismissed
 import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
-import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -57,7 +55,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
             }
             transitions {
                 if (tapl.isTablet) {
@@ -95,7 +93,9 @@
     fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
         primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
 
-    private fun secondaryAppBoundsIsFullscreenAtEnd_internal() {
+    @Presubmit
+    @Test
+    fun secondaryAppBoundsIsFullscreenAtEnd() {
         testSpec.assertLayers {
             this.isVisible(secondaryApp)
                 .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -117,20 +117,6 @@
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @FlakyTest(bugId = 250528485)
-    @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @Presubmit
-    @Test
     fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp)
 
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 6cfbb47..1d61955 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -52,7 +52,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
             }
             transitions {
                 tapl.goHome()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index a80c88a..8d771fe 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -56,7 +56,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
             }
             transitions {
                 SplitScreenUtils.dragDividerToResizeAndWait(device, wmHelper)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
new file mode 100644
index 0000000..e9c765a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -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.wm.shell.flicker.splitscreen
+
+import android.view.WindowManagerPolicyConstants
+import android.platform.test.annotations.Postsubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import com.android.wm.shell.flicker.splitScreenEntered
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging a shortcut.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromShortcut`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class EnterSplitScreenByDragFromShortcut(
+    testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+    @Before
+    fun before() {
+        Assume.assumeTrue(testSpec.isTablet)
+    }
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                tapl.goHome()
+                SplitScreenUtils.createShortcutOnHotseatIfNotExist(tapl, secondaryApp.appName)
+                primaryApp.launchViaIntent(wmHelper)
+            }
+            transitions {
+                tapl.launchedAppState.taskbar
+                    .getAppIcon(secondaryApp.appName)
+                    .openDeepShortcutMenu()
+                    .getMenuItem("Split Screen Secondary Activity")
+                    .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`)
+                SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+            }
+        }
+
+    @Postsubmit
+    @Test
+    fun cujCompleted() = testSpec.splitScreenEntered(primaryApp, secondaryApp,
+        fromOtherApp = false, appExistAtStart = false)
+
+    @Postsubmit
+    @Test
+    fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+    @Postsubmit
+    @Test
+    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+
+    @Postsubmit
+    @Test
+    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+
+    @Postsubmit
+    @Test
+    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        primaryApp, landscapePosLeft = false, portraitPosTop = false)
+
+    @Postsubmit
+    @Test
+    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisibleByDrag(
+        secondaryApp)
+
+    @Postsubmit
+    @Test
+    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
+
+    @Postsubmit
+    @Test
+    fun secondaryAppWindowBecomesVisible() {
+        testSpec.assertWm {
+            this.notContains(secondaryApp)
+                .then()
+                .isAppWindowInvisible(secondaryApp, isOptional = true)
+                .then()
+                .isAppWindowVisible(secondaryApp)
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun entireScreenCovered() =
+        super.entireScreenCovered()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun navBarLayerIsVisibleAtStartAndEnd() =
+        super.navBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() =
+        super.navBarLayerPositionAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun navBarWindowIsAlwaysVisible() =
+        super.navBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun statusBarLayerPositionAtStartAndEnd() =
+        super.statusBarLayerPositionAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() =
+        super.statusBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun taskBarLayerIsVisibleAtStartAndEnd() =
+        super.taskBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun taskBarWindowIsAlwaysVisible() =
+        super.taskBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    /** {@inheritDoc} */
+    @Postsubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes =
+                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY))
+        }
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index 936afa9..fb7b8b7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -34,7 +34,6 @@
 import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
 import com.android.wm.shell.flicker.splitScreenEntered
 import org.junit.Assume
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,7 +54,6 @@
         get() = {
             super.transition(this)
             setup {
-                tapl.workspace.switchToOverview().dismissAllTasks()
                 primaryApp.launchViaIntent(wmHelper)
                 secondaryApp.launchViaIntent(wmHelper)
                 tapl.goHome()
@@ -65,7 +63,7 @@
                     .waitForAndVerify()
             }
             transitions {
-                SplitScreenUtils.splitFromOverview(tapl)
+                SplitScreenUtils.splitFromOverview(tapl, device)
                 SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
             }
         }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
index e6d6379..c841333 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt
@@ -34,6 +34,7 @@
                 tapl.setEnableRotation(true)
                 setRotation(testSpec.startRotation)
                 tapl.setExpectedRotation(testSpec.startRotation)
+                tapl.workspace.switchToOverview().dismissAllTasks()
             }
             teardown {
                 primaryApp.exit(wmHelper)
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
index 6453ed8..4a3284e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenUtils.kt
@@ -25,6 +25,7 @@
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.BySelector
 import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
 import com.android.launcher3.tapl.LauncherInstrumentation
 import com.android.server.wm.flicker.helpers.ImeAppHelper
@@ -38,15 +39,19 @@
 import com.android.server.wm.traces.common.IComponentNameMatcher
 import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import org.junit.Assert.assertNotNull
+import java.util.Collections
 
 internal object SplitScreenUtils {
     private const val TIMEOUT_MS = 3_000L
     private const val DRAG_DURATION_MS = 1_000L
     private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
     private const val DIVIDER_BAR = "docked_divider_handle"
+    private const val OVERVIEW_SNAPSHOT = "snapshot"
     private const val GESTURE_STEP_MS = 16L
-    private const val LONG_PRESS_TIME_MS = 100L
+    private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
     private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
 
     private val notificationScrollerSelector: BySelector
@@ -55,6 +60,8 @@
         get() = By.text("Flicker Test Notification")
     private val dividerBarSelector: BySelector
         get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
+    private val overviewSnapshotSelector: BySelector
+        get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
 
     fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
         SimpleAppHelper(
@@ -94,24 +101,39 @@
     fun enterSplit(
         wmHelper: WindowManagerStateHelper,
         tapl: LauncherInstrumentation,
+        device: UiDevice,
         primaryApp: StandardAppHelper,
         secondaryApp: StandardAppHelper
     ) {
-        tapl.workspace.switchToOverview().dismissAllTasks()
         primaryApp.launchViaIntent(wmHelper)
         secondaryApp.launchViaIntent(wmHelper)
         tapl.goHome()
         wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
-        splitFromOverview(tapl)
+        splitFromOverview(tapl, device)
         waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
     }
 
-    fun splitFromOverview(tapl: LauncherInstrumentation) {
+    fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
         // Note: The initial split position in landscape is different between tablet and phone.
         // In landscape, tablet will let the first app split to right side, and phone will
         // split to left side.
         if (tapl.isTablet) {
-            tapl.workspace.switchToOverview().overviewActions.clickSplit().currentTask.open()
+            // TAPL's currentTask on tablet is sometimes not what we expected if the overview
+            // contains more than 3 task views. We need to use uiautomator directly to find the
+            // second task to split.
+            tapl.workspace.switchToOverview().overviewActions.clickSplit()
+            val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
+            if (snapshots == null || snapshots.size < 1) {
+                error("Fail to find a overview snapshot to split.")
+            }
+
+            // Find the second task in the upper right corner in split select mode by sorting
+            // 'left' in descending order and 'top' in ascending order.
+            Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 ->
+                t2.getVisibleBounds().left - t1.getVisibleBounds().left})
+            Collections.sort(snapshots, { t1: UiObject2, t2: UiObject2 ->
+                t1.getVisibleBounds().top - t2.getVisibleBounds().top})
+            snapshots[0].click()
         } else {
             tapl.workspace
                 .switchToOverview()
@@ -254,13 +276,6 @@
         }
     }
 
-    fun longPress(instrumentation: Instrumentation, point: Point) {
-        val downTime = SystemClock.uptimeMillis()
-        touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, point)
-        SystemClock.sleep(LONG_PRESS_TIME_MS)
-        touch(instrumentation, MotionEvent.ACTION_UP, downTime, downTime, TIMEOUT_MS, point)
-    }
-
     fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
         tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
         val allApps = tapl.workspace.switchToAllApps()
@@ -332,9 +347,11 @@
                 Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
                 TIMEOUT_MS
             )
-        longPress(instrumentation, textView.visibleCenter)
+        assertNotNull("Unable to find the TextView", textView)
+        textView.click(LONG_PRESS_TIME_MS)
 
         val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
+        assertNotNull("Unable to find the copy button", copyBtn)
         copyBtn.click()
 
         // Paste text to destinationApp
@@ -343,9 +360,11 @@
                 Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
                 TIMEOUT_MS
             )
-        longPress(instrumentation, editText.visibleCenter)
+        assertNotNull("Unable to find the EditText", editText)
+        editText.click(LONG_PRESS_TIME_MS)
 
         val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
+        assertNotNull("Unable to find the paste button", pasteBtn)
         pasteBtn.click()
 
         // Verify text
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 84a8c0a..f7610c4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
@@ -57,7 +56,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
             }
             transitions {
                 SplitScreenUtils.doubleTapDividerToSwitch(device)
@@ -146,19 +145,19 @@
         // robust enough to get the correct end state.
     }
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp)
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         primaryApp,
@@ -166,9 +165,7 @@
         portraitPosTop = true
     )
 
-    // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to
-    // get the correct end state.
-    @FlakyTest(bugId = 246490534)
+    @Presubmit
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         secondaryApp,
@@ -176,11 +173,11 @@
         portraitPosTop = false
     )
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
+    @Presubmit
     @Test
     fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp)
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index 553840c..993dba2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -52,7 +52,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
 
                 thirdApp.launchViaIntent(wmHelper)
                 wmHelper.StateSyncBuilder().withWindowSurfaceAppeared(thirdApp).waitForAndVerify()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 1f117d0..2a552cd 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -51,7 +51,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
 
                 tapl.goHome()
                 wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index d7b3ec2..7f81bae 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -51,7 +51,7 @@
         get() = {
             super.transition(this)
             setup {
-                SplitScreenUtils.enterSplit(wmHelper, tapl, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
 
                 tapl.goHome()
                 wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
new file mode 100644
index 0000000..d84954d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.FlakyTest
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.appWindowIsInvisibleAtEnd
+import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.layerBecomesInvisible
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtStart
+import com.android.wm.shell.flicker.splitScreenDividerIsVisibleAtEnd
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch between two split pairs.
+ *
+ * To run this test: `atest WMShellFlickerTests:SwitchBetweenSplitPairs`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SwitchBetweenSplitPairs(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+    private val thirdApp = SplitScreenUtils.getIme(instrumentation)
+    private val fourthApp = SplitScreenUtils.getSendNotification(instrumentation)
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp)
+                SplitScreenUtils.enterSplit(wmHelper, tapl, device, thirdApp, fourthApp)
+                SplitScreenUtils.waitForSplitComplete(wmHelper, thirdApp, fourthApp)
+            }
+            transitions {
+                tapl.launchedAppState.quickSwitchToPreviousApp()
+                SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+            }
+            teardown {
+                thirdApp.exit(wmHelper)
+                fourthApp.exit(wmHelper)
+            }
+        }
+
+    @Postsubmit
+    @Test
+    fun cujCompleted() {
+        testSpec.appWindowIsVisibleAtStart(thirdApp)
+        testSpec.appWindowIsVisibleAtStart(fourthApp)
+        testSpec.splitScreenDividerIsVisibleAtStart()
+
+        testSpec.appWindowIsVisibleAtEnd(primaryApp)
+        testSpec.appWindowIsVisibleAtEnd(secondaryApp)
+        testSpec.appWindowIsInvisibleAtEnd(thirdApp)
+        testSpec.appWindowIsInvisibleAtEnd(fourthApp)
+        testSpec.splitScreenDividerIsVisibleAtEnd()
+    }
+
+    @Postsubmit
+    @Test
+    fun splitScreenDividerInvisibleAtMiddle() =
+        testSpec.assertLayers {
+            this.isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+                .then()
+                .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+                .then()
+                .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
+        }
+
+    @FlakyTest(bugId = 247095572)
+    @Test
+    fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp)
+
+    @FlakyTest(bugId = 247095572)
+    @Test
+    fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+
+    @FlakyTest(bugId = 247095572)
+    @Test
+    fun thirdAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(thirdApp)
+
+    @FlakyTest(bugId = 247095572)
+    @Test
+    fun fourthAppLayerBecomesInvisible() = testSpec.layerBecomesInvisible(fourthApp)
+
+    @Postsubmit
+    @Test
+    fun primaryAppBoundsIsVisibleAtEnd() =
+        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+            primaryApp,
+            landscapePosLeft = tapl.isTablet,
+            portraitPosTop = false
+        )
+
+    @Postsubmit
+    @Test
+    fun secondaryAppBoundsIsVisibleAtEnd() =
+        testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+            secondaryApp,
+            landscapePosLeft = !tapl.isTablet,
+            portraitPosTop = true
+        )
+
+    @Postsubmit
+    @Test
+    fun thirdAppBoundsIsVisibleAtBegin() =
+        testSpec.assertLayersStart {
+            this.splitAppLayerBoundsSnapToDivider(
+                thirdApp,
+                landscapePosLeft = tapl.isTablet,
+                portraitPosTop = false,
+                testSpec.startRotation
+            )
+        }
+
+    @Postsubmit
+    @Test
+    fun fourthAppBoundsIsVisibleAtBegin() =
+        testSpec.assertLayersStart {
+            this.splitAppLayerBoundsSnapToDivider(
+                fourthApp,
+                landscapePosLeft = !tapl.isTablet,
+                portraitPosTop = true,
+                testSpec.startRotation
+            )
+        }
+
+    @Postsubmit
+    @Test
+    fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+
+    @Postsubmit
+    @Test
+    fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+
+    @Postsubmit
+    @Test
+    fun thirdAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(thirdApp)
+
+    @Postsubmit
+    @Test
+    fun fourthAppWindowBecomesVisible() = testSpec.appWindowBecomesInvisible(fourthApp)
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 251268711)
+    @Test
+    override fun entireScreenCovered() =
+        super.entireScreenCovered()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarLayerIsVisibleAtStartAndEnd() =
+        super.navBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 206753786)
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() =
+        super.navBarLayerPositionAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun navBarWindowIsAlwaysVisible() =
+        super.navBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun statusBarLayerIsVisibleAtStartAndEnd() =
+        super.statusBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun statusBarLayerPositionAtStartAndEnd() =
+        super.statusBarLayerPositionAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() =
+        super.statusBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun taskBarLayerIsVisibleAtStartAndEnd() =
+        super.taskBarLayerIsVisibleAtStartAndEnd()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun taskBarWindowIsAlwaysVisible() =
+        super.taskBarWindowIsAlwaysVisible()
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+    /** {@inheritDoc} */
+    @Presubmit
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
new file mode 100644
index 0000000..8949a75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
@@ -0,0 +1,27 @@
+<?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>
+    <!-- Resources used in WindowDecorationTests -->
+    <dimen name="test_freeform_decor_caption_height">32dp</dimen>
+    <dimen name="test_freeform_decor_caption_width">216dp</dimen>
+    <dimen name="test_window_decor_left_outset">10dp</dimen>
+    <dimen name="test_window_decor_top_outset">20dp</dimen>
+    <dimen name="test_window_decor_right_outset">30dp</dimen>
+    <dimen name="test_window_decor_bottom_outset">40dp</dimen>
+    <dimen name="test_window_decor_shadow_radius">5dp</dimen>
+    <dimen name="test_window_decor_resize_handle">10dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 7cbace5..081c8ae 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,13 +16,9 @@
 
 package com.android.wm.shell;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -34,8 +30,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -44,11 +38,9 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
-import android.app.WindowConfiguration;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -61,8 +53,6 @@
 import android.window.ITaskOrganizerController;
 import android.window.TaskAppearedInfo;
 import android.window.WindowContainerToken;
-import android.window.WindowContainerTransaction;
-import android.window.WindowContainerTransaction.Change;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -638,130 +628,10 @@
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
 
-    @Test
-    public void testPrepareClearBoundsForStandardTasks() {
-        MockToken token1 = new MockToken();
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
-        mOrganizer.onTaskAppeared(task1, null);
-
-        MockToken token2 = new MockToken();
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
-        mOrganizer.onTaskAppeared(task2, null);
-
-        MockToken otherDisplayToken = new MockToken();
-        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED,
-                otherDisplayToken);
-        otherDisplayTask.displayId = 2;
-        mOrganizer.onTaskAppeared(otherDisplayTask, null);
-
-        WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
-
-        assertEquals(wct.getChanges().size(), 2);
-        Change boundsChange1 = wct.getChanges().get(token1.binder());
-        assertNotNull(boundsChange1);
-        assertNotEquals(
-                (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
-        assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
-
-        Change boundsChange2 = wct.getChanges().get(token2.binder());
-        assertNotNull(boundsChange2);
-        assertNotEquals(
-                (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
-        assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
-    }
-
-    @Test
-    public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() {
-        MockToken token1 = new MockToken();
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1);
-        mOrganizer.onTaskAppeared(task1, null);
-
-        MockToken token2 = new MockToken();
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2);
-        task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
-        mOrganizer.onTaskAppeared(task2, null);
-
-        WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1);
-
-        // Only clear bounds for task1
-        assertEquals(1, wct.getChanges().size());
-        assertNotNull(wct.getChanges().get(token1.binder()));
-    }
-
-    @Test
-    public void testPrepareClearFreeformForStandardTasks() {
-        MockToken token1 = new MockToken();
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
-        mOrganizer.onTaskAppeared(task1, null);
-
-        MockToken token2 = new MockToken();
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2);
-        mOrganizer.onTaskAppeared(task2, null);
-
-        MockToken otherDisplayToken = new MockToken();
-        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM,
-                otherDisplayToken);
-        otherDisplayTask.displayId = 2;
-        mOrganizer.onTaskAppeared(otherDisplayTask, null);
-
-        WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
-
-        // Only task with freeform windowing mode and the right display should be updated
-        assertEquals(wct.getChanges().size(), 1);
-        Change wmModeChange1 = wct.getChanges().get(token1.binder());
-        assertNotNull(wmModeChange1);
-        assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
-    }
-
-    @Test
-    public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() {
-        MockToken token1 = new MockToken();
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1);
-        mOrganizer.onTaskAppeared(task1, null);
-
-        MockToken token2 = new MockToken();
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2);
-        task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
-        mOrganizer.onTaskAppeared(task2, null);
-
-        WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1);
-
-        // Only clear freeform for task1
-        assertEquals(1, wct.getChanges().size());
-        assertNotNull(wct.getChanges().get(token1.binder()));
-    }
-
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
         return taskInfo;
     }
-
-    private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) {
-        RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode);
-        taskInfo.displayId = 1;
-        taskInfo.token = token.token();
-        taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
-        return taskInfo;
-    }
-
-    private static class MockToken {
-        private final WindowContainerToken mToken;
-        private final IBinder mBinder;
-
-        MockToken() {
-            mToken = mock(WindowContainerToken.class);
-            mBinder = mock(IBinder.class);
-            when(mToken.asBinder()).thenReturn(mBinder);
-        }
-
-        WindowContainerToken token() {
-            return mToken;
-        }
-
-        IBinder binder() {
-            return mBinder;
-        }
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 6484b07..d75c36c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -37,6 +38,7 @@
 import android.content.pm.ApplicationInfo;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -55,6 +57,7 @@
 import android.window.IBackAnimationFinishedCallback;
 import android.window.IOnBackInvokedCallback;
 
+import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -64,7 +67,6 @@
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.sysui.ShellSharedConstants;
-import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -94,7 +96,10 @@
     private IActivityTaskManager mActivityTaskManager;
 
     @Mock
-    private IOnBackInvokedCallback mIOnBackInvokedCallback;
+    private IOnBackInvokedCallback mAppCallback;
+
+    @Mock
+    private IOnBackInvokedCallback mAnimatorCallback;
 
     @Mock
     private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@@ -103,14 +108,9 @@
     private IRemoteAnimationRunner mBackAnimationRunner;
 
     @Mock
-    private Transitions mTransitions;
-
-    @Mock
     private ShellController mShellController;
 
     private BackAnimationController mController;
-
-    private int mEventTime = 0;
     private TestableContentResolver mContentResolver;
     private TestableLooper mTestableLooper;
 
@@ -127,18 +127,18 @@
         mController = new BackAnimationController(mShellInit, mShellController,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()),
                 mActivityTaskManager, mContext,
-                mContentResolver, mTransitions);
+                mContentResolver);
+        mController.setEnableUAnimation(true);
         mShellInit.init();
-        mEventTime = 0;
         mShellExecutor.flushAll();
     }
 
-    private void createNavigationInfo(int backType, IOnBackInvokedCallback onBackInvokedCallback) {
+    private void createNavigationInfo(int backType, boolean enableAnimation) {
         BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
                 .setType(backType)
                 .setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
-                .setOnBackInvokedCallback(onBackInvokedCallback)
-                .setPrepareRemoteAnimation(true);
+                .setOnBackInvokedCallback(mAppCallback)
+                .setPrepareRemoteAnimation(enableAnimation);
 
         createNavigationInfo(builder);
     }
@@ -179,26 +179,39 @@
     }
 
     @Test
-    public void verifyAnimationFinishes() {
-        RemoteAnimationTarget animationTarget = createAnimationTarget();
-        boolean[] backNavigationDone = new boolean[]{false};
-        boolean[] triggerBack = new boolean[]{false};
-        createNavigationInfo(new BackNavigationInfo.Builder()
-                .setType(BackNavigationInfo.TYPE_CROSS_ACTIVITY)
-                .setOnBackNavigationDone(
-                        new RemoteCallback(result -> {
-                            backNavigationDone[0] = true;
-                            triggerBack[0] = result.getBoolean(KEY_TRIGGER_BACK);
-                        })));
-        triggerBackGesture();
-        assertTrue("Navigation Done callback not called", backNavigationDone[0]);
-        assertTrue("TriggerBack should have been true", triggerBack[0]);
+    public void verifyNavigationFinishes() throws RemoteException {
+        final int[] testTypes = new int[] {BackNavigationInfo.TYPE_RETURN_TO_HOME,
+                BackNavigationInfo.TYPE_CROSS_TASK,
+                BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+                BackNavigationInfo.TYPE_DIALOG_CLOSE,
+                BackNavigationInfo.TYPE_CALLBACK };
+
+        for (int type: testTypes) {
+            registerAnimation(type);
+        }
+
+        for (int type: testTypes) {
+            final ResultListener result  = new ResultListener();
+            createNavigationInfo(new BackNavigationInfo.Builder()
+                    .setType(type)
+                    .setOnBackInvokedCallback(mAppCallback)
+                    .setPrepareRemoteAnimation(true)
+                    .setOnBackNavigationDone(new RemoteCallback(result)));
+            triggerBackGesture();
+            simulateRemoteAnimationStart(type);
+            simulateRemoteAnimationFinished();
+            mShellExecutor.flushAll();
+
+            assertTrue("Navigation Done callback not called for "
+                    + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+            assertTrue("TriggerBack should have been true", result.mTriggerBack);
+        }
     }
 
     @Test
     public void backToHome_dispatchesEvents() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, mIOnBackInvokedCallback);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
 
@@ -206,15 +219,16 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+
+        verify(mAnimatorCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
         ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
-        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
+        verify(mAnimatorCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verify(mIOnBackInvokedCallback).onBackInvoked();
+        verify(mAnimatorCallback).onBackInvoked();
     }
 
     @Test
@@ -225,100 +239,96 @@
         mController = new BackAnimationController(shellInit, mShellController,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()),
                 mActivityTaskManager, mContext,
-                mContentResolver, mTransitions);
+                mContentResolver);
         shellInit.init();
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
         ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
 
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, false);
 
         triggerBackGesture();
 
-        verify(appCallback, never()).onBackStarted();
-        verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
-        verify(appCallback, times(1)).onBackInvoked();
+        verify(mAppCallback, never()).onBackStarted(any());
+        verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(mAppCallback, times(1)).onBackInvoked();
 
-        verify(mIOnBackInvokedCallback, never()).onBackStarted();
-        verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
-        verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+        verify(mAnimatorCallback, never()).onBackStarted(any());
+        verify(mAnimatorCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(mAnimatorCallback, never()).onBackInvoked();
         verify(mBackAnimationRunner, never()).onAnimationStart(
                 anyInt(), any(), any(), any(), any());
     }
 
     @Test
     public void ignoresGesture_transitionInProgress() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         triggerBackGesture();
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
         // Check that back invocation is dispatched.
-        verify(mIOnBackInvokedCallback).onBackInvoked();
+        verify(mAnimatorCallback).onBackInvoked();
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
-        reset(mIOnBackInvokedCallback);
+        reset(mAnimatorCallback);
         reset(mBackAnimationRunner);
 
         // Verify that we prevent animation from restarting if another gestures happens before
         // the previous transition is finished.
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
-        verifyNoMoreInteractions(mIOnBackInvokedCallback);
-        mController.onBackAnimationFinished();
-        // Pretend the transition handler called finishAnimation.
-        mController.finishBackNavigation();
+        verifyNoMoreInteractions(mAnimatorCallback);
+
+        // Finish back navigation.
+        simulateRemoteAnimationFinished();
 
         // Verify that more events from a rejected swipe cannot start animation.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verifyNoMoreInteractions(mIOnBackInvokedCallback);
+        verifyNoMoreInteractions(mAnimatorCallback);
 
         // Verify that we start accepting gestures again once transition finishes.
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mAnimatorCallback).onBackStarted(any());
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
     }
 
     @Test
     public void acceptsGesture_transitionTimeout() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
+
+        // In case it is still running in animation.
+        doNothing().when(mAnimatorCallback).onBackInvoked();
 
         triggerBackGesture();
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        reset(mIOnBackInvokedCallback);
-
         // Simulate transition timeout.
         mShellExecutor.flushAll();
-        mController.onBackAnimationFinished();
-        // Pretend the transition handler called finishAnimation.
-        mController.finishBackNavigation();
+        reset(mAnimatorCallback);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
-
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mAnimatorCallback).onBackStarted(any());
     }
 
-
     @Test
     public void cancelBackInvokeWhenLostFocus() throws RemoteException {
-        mController.setBackToLauncherCallback(mIOnBackInvokedCallback, mBackAnimationRunner);
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
 
-        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+        createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, true);
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         // Check that back start and progress is dispatched when first move.
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mAnimatorCallback).onBackStarted(any());
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
         // Check that back invocation is dispatched.
@@ -328,11 +338,70 @@
         IBinder token = mock(IBinder.class);
         mController.mFocusObserver.focusLost(token);
         mShellExecutor.flushAll();
-        verify(mIOnBackInvokedCallback).onBackCancelled();
+        verify(mAnimatorCallback).onBackCancelled();
 
         // No more back invoke.
         doMotionEvent(MotionEvent.ACTION_UP, 0);
-        verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+        verify(mAnimatorCallback, never()).onBackInvoked();
+    }
+
+    @Test
+    public void animationNotDefined() throws RemoteException {
+        final int[] testTypes = new int[] {
+                BackNavigationInfo.TYPE_RETURN_TO_HOME,
+                BackNavigationInfo.TYPE_CROSS_TASK,
+                BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+                BackNavigationInfo.TYPE_DIALOG_CLOSE};
+
+        for (int type: testTypes) {
+            final ResultListener result = new ResultListener();
+            createNavigationInfo(new BackNavigationInfo.Builder()
+                    .setType(type)
+                    .setOnBackInvokedCallback(mAppCallback)
+                    .setPrepareRemoteAnimation(true)
+                    .setOnBackNavigationDone(new RemoteCallback(result)));
+            triggerBackGesture();
+            simulateRemoteAnimationStart(type);
+            mShellExecutor.flushAll();
+
+            assertTrue("Navigation Done callback not called for "
+                    + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+            assertTrue("TriggerBack should have been true", result.mTriggerBack);
+        }
+
+        verify(mAppCallback, never()).onBackStarted(any());
+        verify(mAppCallback, never()).onBackProgressed(any());
+        verify(mAppCallback, times(testTypes.length)).onBackInvoked();
+
+        verify(mAnimatorCallback, never()).onBackStarted(any());
+        verify(mAnimatorCallback, never()).onBackProgressed(any());
+        verify(mAnimatorCallback, never()).onBackInvoked();
+    }
+
+    @Test
+    public void callbackShouldDeliverProgress() throws RemoteException {
+        registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+
+        final int type = BackNavigationInfo.TYPE_CALLBACK;
+        final ResultListener result = new ResultListener();
+        createNavigationInfo(new BackNavigationInfo.Builder()
+                .setType(type)
+                .setOnBackInvokedCallback(mAppCallback)
+                .setOnBackNavigationDone(new RemoteCallback(result)));
+        triggerBackGesture();
+        mShellExecutor.flushAll();
+
+        assertTrue("Navigation Done callback not called for "
+                + BackNavigationInfo.typeToString(type), result.mBackNavigationDone);
+        assertTrue("TriggerBack should have been true", result.mTriggerBack);
+
+        verify(mAppCallback, times(1)).onBackStarted(any());
+        verify(mAppCallback, times(1)).onBackProgressed(any());
+        verify(mAppCallback, times(1)).onBackInvoked();
+
+        verify(mAnimatorCallback, never()).onBackStarted(any());
+        verify(mAnimatorCallback, never()).onBackProgressed(any());
+        verify(mAnimatorCallback, never()).onBackInvoked();
     }
 
     private void doMotionEvent(int actionDown, int coordinate) {
@@ -340,7 +409,6 @@
                 coordinate, coordinate,
                 actionDown,
                 BackEvent.EDGE_LEFT);
-        mEventTime += 10;
     }
 
     private void simulateRemoteAnimationStart(int type) throws RemoteException {
@@ -352,4 +420,24 @@
             mShellExecutor.flushAll();
         }
     }
+
+    private void simulateRemoteAnimationFinished() {
+        mController.onBackAnimationFinished();
+        mController.finishBackNavigation();
+    }
+
+    private void registerAnimation(int type) {
+        mController.registerAnimation(type,
+                new BackAnimationRunner(mAnimatorCallback, mBackAnimationRunner));
+    }
+
+    private static class ResultListener implements RemoteCallback.OnResultListener {
+        boolean mBackNavigationDone = false;
+        boolean mTriggerBack = false;
+        @Override
+        public void onResult(@Nullable Bundle result) {
+            mBackNavigationDone = true;
+            mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK);
+        }
+    };
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS
new file mode 100644
index 0000000..1e0f9bc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS
@@ -0,0 +1,5 @@
+# WM shell sub-module back navigation owners
+# Bug component: 1152663
+shanh@google.com
+arthurhung@google.com
+wilsonshih@google.com
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
new file mode 100644
index 0000000..3aefc3f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.wm.shell.back;
+
+import static org.junit.Assert.assertEquals;
+
+import android.window.BackEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TouchTrackerTest {
+    private static final float FAKE_THRESHOLD = 400;
+    private static final float INITIAL_X_LEFT_EDGE = 5;
+    private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE;
+    private TouchTracker mTouchTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        mTouchTracker = new TouchTracker();
+        mTouchTracker.setProgressThreshold(FAKE_THRESHOLD);
+    }
+
+    @Test
+    public void generatesProgress_onStart() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        BackEvent event = mTouchTracker.createStartEvent(null);
+        assertEquals(event.getProgress(), 0f, 0f);
+    }
+
+    @Test
+    public void generatesProgress_leftEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        float touchX = 10;
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX += 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX += 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+    }
+
+    @Test
+    public void generatesProgress_rightEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT);
+        float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX -= 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX += 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+    }
+
+    private float getProgress() {
+        return mTouchTracker.createProgressEvent().getProgress();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 9967e5f..40f2e88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -39,7 +39,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.view.IInputMethodManager;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -56,8 +55,6 @@
     @Mock
     private SurfaceControl.Transaction mT;
     @Mock
-    private IInputMethodManager mMock;
-    @Mock
     private ShellInit mShellInit;
     private DisplayImeController.PerDisplay mPerDisplay;
     private Executor mExecutor;
@@ -77,10 +74,6 @@
             }
         }, mExecutor) {
             @Override
-            public IInputMethodManager getImms() {
-                return mMock;
-            }
-            @Override
             void removeImeSurface() { }
         }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
     }
@@ -104,13 +97,13 @@
 
     @Test
     public void showInsets_schedulesNoWorkOnExecutor() {
-        mPerDisplay.showInsets(ime(), true);
+        mPerDisplay.showInsets(ime(), true /* fromIme */, null /* statsToken */);
         verifyZeroInteractions(mExecutor);
     }
 
     @Test
     public void hideInsets_schedulesNoWorkOnExecutor() {
-        mPerDisplay.hideInsets(ime(), true);
+        mPerDisplay.hideInsets(ime(), true /* fromIme */, null /* statsToken */);
         verifyZeroInteractions(mExecutor);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 5f5a3c5..956f1cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.os.RemoteException;
 import android.util.SparseArray;
@@ -32,7 +33,8 @@
 import android.view.IWindowManager;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
 
 import androidx.test.filters.SmallTest;
 
@@ -108,11 +110,13 @@
         mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener);
 
         mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null,
-                new InsetsVisibilities());
+                WindowInsets.Type.defaultVisible());
         mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null);
         mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null);
-        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false);
-        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false,
+                null /* statsToken */);
+        mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false,
+                null /* statsToken */);
         mExecutor.flushAll();
 
         assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -128,11 +132,13 @@
         assertTrue(secondListener.hideInsetsCount == 0);
 
         mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null,
-                new InsetsVisibilities());
+                WindowInsets.Type.defaultVisible());
         mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null);
         mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null);
-        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false);
-        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false,
+                null /* statsToken */);
+        mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false,
+                null /* statsToken */);
         mExecutor.flushAll();
 
         assertTrue(defaultListener.topFocusedWindowChangedCount == 1);
@@ -175,8 +181,7 @@
         int hideInsetsCount = 0;
 
         @Override
-        public void topFocusedWindowChanged(ComponentName component,
-                InsetsVisibilities requestedVisibilities) {
+        public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) {
             topFocusedWindowChangedCount++;
         }
 
@@ -192,12 +197,12 @@
         }
 
         @Override
-        public void showInsets(int types, boolean fromIme) {
+        public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
             showInsetsCount++;
         }
 
         @Override
-        public void hideInsets(int types, boolean fromIme) {
+        public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {
             hideInsetsCount++;
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index f6d6c03..3d77948 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -105,7 +105,8 @@
     @Test
     public void testUpdateDivideBounds() {
         mSplitLayout.updateDivideBounds(anyInt());
-        verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class));
+        verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(),
+                anyInt());
     }
 
     @Test
@@ -140,7 +141,7 @@
         DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
                 DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START);
 
-        mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+        mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
         waitDividerFlingFinished();
         verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt());
     }
@@ -152,7 +153,7 @@
         DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
                 DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END);
 
-        mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget);
+        mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
         waitDividerFlingFinished();
         verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt());
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index c850a3b..89bafcb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -16,10 +16,15 @@
 
 package com.android.wm.shell.desktopmode;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -28,17 +33,20 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.testing.AndroidTestingRunner;
 import android.window.DisplayAreaInfo;
+import android.window.TransitionRequestInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransaction.Change;
@@ -64,6 +72,9 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class DesktopModeControllerTest extends ShellTestCase {
@@ -79,9 +90,7 @@
     @Mock
     private Handler mMockHandler;
     @Mock
-    private Transitions mMockTransitions;
-    private TestShellExecutor mExecutor;
-
+    private Transitions mTransitions;
     private DesktopModeController mController;
     private DesktopModeTaskRepository mDesktopModeTaskRepository;
     private ShellInit mShellInit;
@@ -93,20 +102,19 @@
         when(DesktopModeStatus.isActive(any())).thenReturn(true);
 
         mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
-        mExecutor = new TestShellExecutor();
 
         mDesktopModeTaskRepository = new DesktopModeTaskRepository();
 
         mController = new DesktopModeController(mContext, mShellInit, mShellController,
-                mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions,
-                mDesktopModeTaskRepository, mMockHandler, mExecutor);
+                mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions,
+                mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor());
 
-        when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn(
-                new WindowContainerTransaction());
+        when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>());
 
         mShellInit.init();
         clearInvocations(mShellTaskOrganizer);
         clearInvocations(mRootTaskDisplayAreaOrganizer);
+        clearInvocations(mTransitions);
     }
 
     @After
@@ -120,113 +128,133 @@
     }
 
     @Test
-    public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
-        // Create a fake WCT to simulate setting task windowing mode to undefined
-        WindowContainerTransaction taskWct = new WindowContainerTransaction();
-        MockToken taskMockToken = new MockToken();
-        taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
-        when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
-                mContext.getDisplayId())).thenReturn(taskWct);
+    public void testDesktopModeEnabled_rootTdaSetToFreeform() {
+        DisplayAreaInfo displayAreaInfo = createMockDisplayArea();
 
-        // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
-        MockToken displayMockToken = new MockToken();
-        DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
-                mContext.getDisplayId(), 0);
-        when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
-                .thenReturn(displayAreaInfo);
-
-        // The test
         mController.updateDesktopModeActive(true);
+        WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
 
-        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
-                WindowContainerTransaction.class);
-        verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
-
-        // WCT should have 2 changes - clear task wm mode and set display wm mode
-        WindowContainerTransaction wct = arg.getValue();
-        assertThat(wct.getChanges()).hasSize(2);
-
-        // Verify executed WCT has a change for setting task windowing mode to undefined
-        Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
-        assertThat(taskWmModeChange).isNotNull();
-        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
-
-        // Verify executed WCT has a change for setting display windowing mode to freeform
-        Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
-        assertThat(displayWmModeChange).isNotNull();
-        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+        // 1 change: Root TDA windowing mode
+        assertThat(wct.getChanges().size()).isEqualTo(1);
+        // Verify WCT has a change for setting windowing mode to freeform
+        Change change = wct.getChanges().get(displayAreaInfo.token.asBinder());
+        assertThat(change).isNotNull();
+        assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
     }
 
     @Test
-    public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
-        // Create a fake WCT to simulate setting task windowing mode to undefined
-        WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
-        MockToken taskWmMockToken = new MockToken();
-        taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
-        when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(
-                mContext.getDisplayId())).thenReturn(taskWmWct);
+    public void testDesktopModeDisabled_rootTdaSetToFullscreen() {
+        DisplayAreaInfo displayAreaInfo = createMockDisplayArea();
 
-        // Create a fake WCT to simulate clearing task bounds
-        WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
-        MockToken taskBoundsMockToken = new MockToken();
-        taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
-        when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(
-                mContext.getDisplayId())).thenReturn(taskBoundsWct);
-
-        // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly
-        MockToken displayMockToken = new MockToken();
-        DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken,
-                mContext.getDisplayId(), 0);
-        when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
-                .thenReturn(displayAreaInfo);
-
-        // The test
         mController.updateDesktopModeActive(false);
+        WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
 
-        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
-                WindowContainerTransaction.class);
-        verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+        // 1 change: Root TDA windowing mode
+        assertThat(wct.getChanges().size()).isEqualTo(1);
+        // Verify WCT has a change for setting windowing mode to fullscreen
+        Change change = wct.getChanges().get(displayAreaInfo.token.asBinder());
+        assertThat(change).isNotNull();
+        assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+    }
 
-        // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
-        WindowContainerTransaction wct = arg.getValue();
-        assertThat(wct.getChanges()).hasSize(3);
+    @Test
+    public void testDesktopModeEnabled_windowingModeCleared() {
+        createMockDisplayArea();
+        RunningTaskInfo freeformTask = createFreeformTask();
+        RunningTaskInfo fullscreenTask = createFullscreenTask();
+        RunningTaskInfo homeTask = createHomeTask();
+        when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+                Arrays.asList(freeformTask, fullscreenTask, homeTask)));
 
-        // Verify executed WCT has a change for setting task windowing mode to undefined
-        Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder());
-        assertThat(taskWmMode).isNotNull();
-        assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+        mController.updateDesktopModeActive(true);
+        WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
 
-        // Verify executed WCT has a change for clearing task bounds
-        Change bounds = wct.getChanges().get(taskBoundsMockToken.binder());
-        assertThat(bounds).isNotNull();
-        assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
-        assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
+        // 2 changes: Root TDA windowing mode and 1 task
+        assertThat(wct.getChanges().size()).isEqualTo(2);
+        // No changes for tasks that are not standard or freeform
+        assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull();
+        assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull();
+        // Standard freeform task has windowing mode cleared
+        Change change = wct.getChanges().get(freeformTask.token.asBinder());
+        assertThat(change).isNotNull();
+        assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+    }
 
-        // Verify executed WCT has a change for setting display windowing mode to fullscreen
-        Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
-        assertThat(displayWmModeChange).isNotNull();
-        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+    @Test
+    public void testDesktopModeDisabled_windowingModeAndBoundsCleared() {
+        createMockDisplayArea();
+        RunningTaskInfo freeformTask = createFreeformTask();
+        RunningTaskInfo fullscreenTask = createFullscreenTask();
+        RunningTaskInfo homeTask = createHomeTask();
+        when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+                Arrays.asList(freeformTask, fullscreenTask, homeTask)));
+
+        mController.updateDesktopModeActive(false);
+        WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
+
+        // 3 changes: Root TDA windowing mode and 2 tasks
+        assertThat(wct.getChanges().size()).isEqualTo(3);
+        // No changes to home task
+        assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull();
+        // Standard tasks have bounds cleared
+        assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder()));
+        assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder()));
+        // Freeform standard tasks have windowing mode cleared
+        assertThat(wct.getChanges().get(
+                freeformTask.token.asBinder()).getWindowingMode()).isEqualTo(
+                WINDOWING_MODE_UNDEFINED);
+    }
+
+    @Test
+    public void testDesktopModeEnabled_homeTaskBehindVisibleTask() {
+        createMockDisplayArea();
+        RunningTaskInfo fullscreenTask1 = createFullscreenTask();
+        fullscreenTask1.isVisible = true;
+        RunningTaskInfo fullscreenTask2 = createFullscreenTask();
+        fullscreenTask2.isVisible = false;
+        RunningTaskInfo homeTask = createHomeTask();
+        when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>(
+                Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask)));
+
+        mController.updateDesktopModeActive(true);
+        WindowContainerTransaction wct = getDesktopModeSwitchTransaction();
+
+        // Check that there are hierarchy changes for home task and visible task
+        assertThat(wct.getHierarchyOps()).hasSize(2);
+        // First show home task
+        WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder());
+
+        // Then visible task on top of it
+        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
+        assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder());
     }
 
     @Test
     public void testShowDesktopApps() {
         // Set up two active tasks on desktop
-        mDesktopModeTaskRepository.addActiveTask(1);
-        mDesktopModeTaskRepository.addActiveTask(2);
-        MockToken token1 = new MockToken();
-        MockToken token2 = new MockToken();
-        ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken(
-                token1.token()).setLastActiveTime(100).build();
-        ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken(
-                token2.token()).setLastActiveTime(200).build();
-        when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1);
-        when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2);
+        RunningTaskInfo freeformTask1 = createFreeformTask();
+        freeformTask1.lastActiveTime = 100;
+        RunningTaskInfo freeformTask2 = createFreeformTask();
+        freeformTask2.lastActiveTime = 200;
+        mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId);
+        mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId);
+        when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn(
+                freeformTask1);
+        when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn(
+                freeformTask2);
 
         // Run show desktop apps logic
         mController.showDesktopApps();
         ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
                 WindowContainerTransaction.class);
-        verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any());
+        } else {
+            verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+        }
         WindowContainerTransaction wct = wctCaptor.getValue();
 
         // Check wct has reorder calls
@@ -235,12 +263,101 @@
         // Task 2 has activity later, must be first
         WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
         assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
-        assertThat(op1.getContainer()).isEqualTo(token2.binder());
+        assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder());
 
         // Task 1 should be second
-        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0);
+        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1);
         assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
-        assertThat(op2.getContainer()).isEqualTo(token2.binder());
+        assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder());
+    }
+
+    @Test
+    public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
+        when(DesktopModeStatus.isActive(any())).thenReturn(false);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notTransitOpen_returnsNull() {
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notFreeform_returnsNull() {
+        RunningTaskInfo trigger = new RunningTaskInfo();
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_returnsWct() {
+        RunningTaskInfo trigger = new RunningTaskInfo();
+        trigger.token = new MockToken().mToken;
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        WindowContainerTransaction wct = mController.handleRequest(
+                mock(IBinder.class),
+                new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */));
+        assertThat(wct).isNotNull();
+    }
+
+    private DisplayAreaInfo createMockDisplayArea() {
+        DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken,
+                mContext.getDisplayId(), 0);
+        when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId()))
+                .thenReturn(displayAreaInfo);
+        return displayAreaInfo;
+    }
+
+    private RunningTaskInfo createFreeformTask() {
+        return new TestRunningTaskInfoBuilder()
+                .setToken(new MockToken().token())
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM)
+                .setLastActiveTime(100)
+                .build();
+    }
+
+    private RunningTaskInfo createFullscreenTask() {
+        return new TestRunningTaskInfoBuilder()
+                .setToken(new MockToken().token())
+                .setActivityType(ACTIVITY_TYPE_STANDARD)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .setLastActiveTime(100)
+                .build();
+    }
+
+    private RunningTaskInfo createHomeTask() {
+        return new TestRunningTaskInfoBuilder()
+                .setToken(new MockToken().token())
+                .setActivityType(ACTIVITY_TYPE_HOME)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+                .setLastActiveTime(100)
+                .build();
+    }
+
+    private WindowContainerTransaction getDesktopModeSwitchTransaction() {
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any());
+        } else {
+            verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture());
+        }
+        return arg.getValue();
+    }
+
+    private void assertThatBoundsCleared(Change change) {
+        assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue();
+        assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
     }
 
     private static class MockToken {
@@ -256,9 +373,5 @@
         WindowContainerToken token() {
             return mToken;
         }
-
-        IBinder binder() {
-            return mBinder;
-        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 9b28d11..aaa5c8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -38,7 +39,7 @@
     @Test
     fun addActiveTask_listenerNotifiedAndTaskIsActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
@@ -48,7 +49,7 @@
     @Test
     fun addActiveTask_sameTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(1)
@@ -58,7 +59,7 @@
     @Test
     fun addActiveTask_multipleTasksAddedNotifiesForEach() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(2)
@@ -68,7 +69,7 @@
     @Test
     fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.removeActiveTask(1)
@@ -80,7 +81,7 @@
     @Test
     fun removeActiveTask_removeNotExistingTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
         repo.removeActiveTask(99)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
     }
@@ -90,10 +91,69 @@
         assertThat(repo.isActiveTask(99)).isFalse()
     }
 
-    class TestListener : DesktopModeTaskRepository.Listener {
+    @Test
+    fun addListener_notifiesVisibleFreeformTask() {
+        repo.updateVisibleFreeformTasks(1, true)
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        repo.updateVisibleFreeformTasks(1, false)
+        executor.flushAll()
+
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+
+        repo.updateVisibleFreeformTasks(2, false)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isFalse()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
+    }
+
+    class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
         var activeTaskChangedCalls = 0
         override fun onActiveTasksChanged() {
             activeTaskChangedCalls++
         }
     }
+
+    class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
+        var hasVisibleFreeformTasks = false
+        var visibleFreeformTaskChangedCalls = 0
+
+        override fun onVisibilityChanged(hasVisibleTasks: Boolean) {
+            hasVisibleFreeformTasks = hasVisibleTasks
+            visibleFreeformTaskChangedCalls++
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
deleted file mode 100644
index d378a17..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
+++ /dev/null
@@ -1,261 +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.wm.shell.floating;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.wm.shell.floating.FloatingTasksController.SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.TaskViewTransitions;
-import com.android.wm.shell.TestShellExecutor;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.floating.views.FloatingTaskLayer;
-import com.android.wm.shell.sysui.ShellCommandHandler;
-import com.android.wm.shell.sysui.ShellController;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.sysui.ShellSharedConstants;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-/**
- * Tests for the floating tasks controller.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FloatingTasksControllerTest extends ShellTestCase {
-    // Some behavior in the controller constructor is dependent on this so we can only
-    // validate if it's working for the real value for those things.
-    private static final boolean FLOATING_TASKS_ACTUALLY_ENABLED =
-            SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
-
-    @Mock private ShellInit mShellInit;
-    @Mock private ShellController mShellController;
-    @Mock private WindowManager mWindowManager;
-    @Mock private ShellTaskOrganizer mTaskOrganizer;
-    @Captor private ArgumentCaptor<FloatingTaskLayer> mFloatingTaskLayerCaptor;
-
-    private FloatingTasksController mController;
-
-    @Before
-    public void setUp() throws RemoteException {
-        MockitoAnnotations.initMocks(this);
-
-        WindowMetrics windowMetrics = mock(WindowMetrics.class);
-        WindowInsets windowInsets = mock(WindowInsets.class);
-        Insets insets = Insets.of(0, 0, 0, 0);
-        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics);
-        when(windowMetrics.getWindowInsets()).thenReturn(windowInsets);
-        when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1000, 1000));
-        when(windowInsets.getInsetsIgnoringVisibility(anyInt())).thenReturn(insets);
-
-        // For the purposes of this test, just run everything synchronously
-        ShellExecutor shellExecutor = new TestShellExecutor();
-        when(mTaskOrganizer.getExecutor()).thenReturn(shellExecutor);
-    }
-
-    @After
-    public void tearDown() {
-        if (mController != null) {
-            mController.removeTask();
-            mController = null;
-        }
-    }
-
-    private void setUpTabletConfig() {
-        Configuration config = mock(Configuration.class);
-        config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET;
-        mController.setConfig(config);
-    }
-
-    private void setUpPhoneConfig() {
-        Configuration config = mock(Configuration.class);
-        config.smallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET - 1;
-        mController.setConfig(config);
-    }
-
-    private void createController() {
-        mController = new FloatingTasksController(mContext,
-                mShellInit,
-                mShellController,
-                mock(ShellCommandHandler.class),
-                Optional.empty(),
-                mWindowManager,
-                mTaskOrganizer,
-                mock(TaskViewTransitions.class),
-                mock(ShellExecutor.class),
-                mock(ShellExecutor.class),
-                mock(SyncTransactionQueue.class));
-        spyOn(mController);
-    }
-
-    //
-    // Shell specific
-    //
-    @Test
-    public void instantiateController_addInitCallback() {
-        if (FLOATING_TASKS_ACTUALLY_ENABLED) {
-            createController();
-            setUpTabletConfig();
-
-            verify(mShellInit, times(1)).addInitCallback(any(), any());
-        }
-    }
-
-    @Test
-    public void instantiateController_doesntAddInitCallback() {
-        if (!FLOATING_TASKS_ACTUALLY_ENABLED) {
-            createController();
-
-            verify(mShellInit, never()).addInitCallback(any(), any());
-        }
-    }
-
-    @Test
-    public void onInit_registerConfigChangeListener() {
-        if (FLOATING_TASKS_ACTUALLY_ENABLED) {
-            createController();
-            setUpTabletConfig();
-            mController.onInit();
-
-            verify(mShellController, times(1)).addConfigurationChangeListener(any());
-        }
-    }
-
-    @Test
-    public void onInit_addExternalInterface() {
-        if (FLOATING_TASKS_ACTUALLY_ENABLED) {
-            createController();
-            setUpTabletConfig();
-            mController.onInit();
-
-            verify(mShellController, times(1)).addExternalInterface(
-                    ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS, any(), any());
-        }
-    }
-
-    //
-    // Tests for floating layer, which is only available for tablets.
-    //
-
-    @Test
-    public void testIsFloatingLayerAvailable_true() {
-        createController();
-        setUpTabletConfig();
-        assertThat(mController.isFloatingLayerAvailable()).isTrue();
-    }
-
-    @Test
-    public void testIsFloatingLayerAvailable_false() {
-        createController();
-        setUpPhoneConfig();
-        assertThat(mController.isFloatingLayerAvailable()).isFalse();
-    }
-
-    //
-    // Tests for floating tasks being enabled, guarded by sysprop flag.
-    //
-
-    @Test
-    public void testIsFloatingTasksEnabled_true() {
-        createController();
-        mController.setFloatingTasksEnabled(true);
-        setUpTabletConfig();
-        assertThat(mController.isFloatingTasksEnabled()).isTrue();
-    }
-
-    @Test
-    public void testIsFloatingTasksEnabled_false() {
-        createController();
-        mController.setFloatingTasksEnabled(false);
-        setUpTabletConfig();
-        assertThat(mController.isFloatingTasksEnabled()).isFalse();
-    }
-
-    //
-    // Tests for behavior depending on flags
-    //
-
-    @Test
-    public void testShowTaskIntent_enabled() {
-        createController();
-        mController.setFloatingTasksEnabled(true);
-        setUpTabletConfig();
-
-        mController.showTask(mock(Intent.class));
-        verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
-        assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
-    }
-
-    @Test
-    public void testShowTaskIntent_notEnabled() {
-        createController();
-        mController.setFloatingTasksEnabled(false);
-        setUpTabletConfig();
-
-        mController.showTask(mock(Intent.class));
-        verify(mWindowManager, never()).addView(any(), any());
-    }
-
-    @Test
-    public void testRemoveTask() {
-        createController();
-        mController.setFloatingTasksEnabled(true);
-        setUpTabletConfig();
-
-        mController.showTask(mock(Intent.class));
-        verify(mWindowManager).addView(mFloatingTaskLayerCaptor.capture(), any());
-        assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(1);
-
-        mController.removeTask();
-        verify(mWindowManager).removeView(mFloatingTaskLayerCaptor.capture());
-        assertThat(mFloatingTaskLayerCaptor.getValue().getTaskViewCount()).isEqualTo(0);
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 55883ab..38b75f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -16,18 +16,24 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -35,10 +41,13 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -64,11 +73,11 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 /**
  * Tests for {@link SplitScreenController}
  */
@@ -90,18 +99,21 @@
     @Mock Transitions mTransitions;
     @Mock TransactionPool mTransactionPool;
     @Mock IconProvider mIconProvider;
-    @Mock Optional<RecentTasksController> mRecentTasks;
+    @Mock StageCoordinator mStageCoordinator;
+    @Mock RecentTasksController mRecentTasks;
+    @Captor ArgumentCaptor<Intent> mIntentCaptor;
 
     private SplitScreenController mSplitScreenController;
 
     @Before
     public void setup() {
+        assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
         MockitoAnnotations.initMocks(this);
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
                 mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
                 mRootTDAOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
-                mIconProvider, mRecentTasks, mMainExecutor));
+                mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator));
     }
 
     @Test
@@ -110,6 +122,7 @@
     }
 
     @Test
+    @UiThreadTest
     public void instantiateController_registerDumpCallback() {
         doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
@@ -118,6 +131,7 @@
     }
 
     @Test
+    @UiThreadTest
     public void instantiateController_registerCommandCallback() {
         doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
@@ -126,6 +140,7 @@
     }
 
     @Test
+    @UiThreadTest
     public void testControllerRegistersKeyguardChangeListener() {
         doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
@@ -134,6 +149,7 @@
     }
 
     @Test
+    @UiThreadTest
     public void instantiateController_addExternalInterface() {
         doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
@@ -143,58 +159,100 @@
     }
 
     @Test
-    public void testShouldAddMultipleTaskFlag_notInSplitScreen() {
-        doReturn(false).when(mSplitScreenController).isSplitScreenVisible();
-        doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any());
-
-        // Verify launching the same activity returns true.
+    public void testStartIntent_appendsNoUserActionFlag() {
         Intent startIntent = createStartIntent("startActivity");
-        ActivityManager.RunningTaskInfo focusTaskInfo =
-                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
-        doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
-        assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
 
-        // Verify launching different activity returns false.
-        Intent diffIntent = createStartIntent("diffActivity");
-        focusTaskInfo =
-                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent);
-        doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo();
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+        assertEquals(FLAG_ACTIVITY_NO_USER_ACTION,
+                mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION);
     }
 
     @Test
-    public void testShouldAddMultipleTaskFlag_inSplitScreen() {
-        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+    public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
         Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into focus task
+        ActivityManager.RunningTaskInfo focusTaskInfo =
+                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(),
+                eq(SPLIT_POSITION_TOP_OR_LEFT), isNull());
+        assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK,
+                mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK);
+    }
+
+    @Test
+    public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into focus task
+        ActivityManager.RunningTaskInfo focusTaskInfo =
+                createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo();
+        doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any());
+        // Put the same component into a task in the background
+        ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
+        doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+                isNull());
+    }
+
+    @Test
+    public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() {
+        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into another side of the split
+        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
         ActivityManager.RunningTaskInfo sameTaskInfo =
                 createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
-        Intent diffIntent = createStartIntent("diffActivity");
-        ActivityManager.RunningTaskInfo differentTaskInfo =
-                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent);
+        doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+                SPLIT_POSITION_BOTTOM_OR_RIGHT);
+        // Put the same component into a task in the background
+        doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
+                .findTaskInBackground(any());
 
-        // Verify launching the same activity return false.
-        doReturn(sameTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
 
-        // Verify launching the same activity as adjacent returns true.
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        doReturn(sameTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
-        assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+        verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
+                isNull());
+    }
 
-        // Verify launching different activity from adjacent returns false.
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
-        doReturn(differentTaskInfo).when(mSplitScreenController)
-                .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
-        assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag(
-                startIntent, SPLIT_POSITION_TOP_OR_LEFT));
+    @Test
+    public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() {
+        doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any());
+        Intent startIntent = createStartIntent("startActivity");
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE);
+        // Put the same component into another side of the split
+        doReturn(true).when(mSplitScreenController).isSplitScreenVisible();
+        ActivityManager.RunningTaskInfo sameTaskInfo =
+                createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent);
+        doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo(
+                SPLIT_POSITION_BOTTOM_OR_RIGHT);
+
+        mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null);
+
+        verify(mStageCoordinator).switchSplitPosition(anyString());
     }
 
     private Intent createStartIntent(String activityName) {
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/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 8350870..3569860 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -50,6 +50,7 @@
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
@@ -113,6 +114,7 @@
     private StageCoordinator mStageCoordinator;
 
     @Before
+    @UiThreadTest
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index e5ae296..11fda8b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -249,7 +249,7 @@
         doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay(
                 any() /* window */, any() /* attrs */,
                 anyInt() /* viewVisibility */, anyInt() /* displayId */,
-                any() /* requestedVisibility */, any() /* outInputChannel */,
+                anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
                 any() /* outInsetsState */, any() /* outActiveControls */,
                 any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
         TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
index 3de50bb..004df2a2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
@@ -39,9 +39,9 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.view.InsetsState;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.WindowInsets;
 import android.window.TaskSnapshot;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -84,7 +84,8 @@
                 createTaskDescription(Color.WHITE, Color.RED, Color.BLUE),
                 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */,
                 taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD,
-                new InsetsState(), null /* clearWindow */, new TestShellExecutor());
+                WindowInsets.Type.defaultVisible(), null /* clearWindow */,
+                new TestShellExecutor());
     }
 
     private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
new file mode 100644
index 0000000..ac10ddb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt
@@ -0,0 +1,130 @@
+package com.android.wm.shell.windowdecor
+
+import android.app.ActivityManager
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [TaskPositioner].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:TaskPositionerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TaskPositionerTest : ShellTestCase() {
+
+    @Mock
+    private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
+    @Mock
+    private lateinit var mockWindowDecoration: WindowDecoration<*>
+    @Mock
+    private lateinit var mockDragStartListener: TaskPositioner.DragStartListener
+
+    @Mock
+    private lateinit var taskToken: WindowContainerToken
+    @Mock
+    private lateinit var taskBinder: IBinder
+
+    private lateinit var taskPositioner: TaskPositioner
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        taskPositioner = TaskPositioner(
+                mockShellTaskOrganizer,
+                mockWindowDecoration,
+                mockDragStartListener
+        )
+        `when`(taskToken.asBinder()).thenReturn(taskBinder)
+        mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+            taskId = TASK_ID
+            token = taskToken
+            configuration.windowConfiguration.bounds = STARTING_BOUNDS
+        }
+    }
+
+    @Test
+    fun testDragResize_move_skipsDragResizingFlag() {
+        taskPositioner.onDragResizeStart(
+                CTRL_TYPE_UNDEFINED, // Move
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Move the task 10px to the right.
+        val newX = STARTING_BOUNDS.left.toFloat() + 10
+        val newY = STARTING_BOUNDS.top.toFloat()
+        taskPositioner.onDragResizeMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragResizeEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) &&
+                        change.dragResizing
+            }
+        })
+    }
+
+    @Test
+    fun testDragResize_resize_setsDragResizingFlag() {
+        taskPositioner.onDragResizeStart(
+                CTRL_TYPE_RIGHT, // Resize right
+                STARTING_BOUNDS.left.toFloat(),
+                STARTING_BOUNDS.top.toFloat()
+        )
+
+        // Resize the task by 10px to the right.
+        val newX = STARTING_BOUNDS.right.toFloat() + 10
+        val newY = STARTING_BOUNDS.top.toFloat()
+        taskPositioner.onDragResizeMove(
+                newX,
+                newY
+        )
+
+        taskPositioner.onDragResizeEnd(newX, newY)
+
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) &&
+                        change.dragResizing
+            }
+        })
+        verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
+            return@argThat wct.changes.any { (token, change) ->
+                token == taskBinder &&
+                        ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) &&
+                        !change.dragResizing
+            }
+        })
+    }
+
+    companion object {
+        private const val TASK_ID = 5
+        private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index fa62b9c..15181b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -35,6 +35,7 @@
 
 import android.app.ActivityManager;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -50,11 +51,13 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.tests.R;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -62,6 +65,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -76,13 +80,9 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class WindowDecorationTests extends ShellTestCase {
-    private static final int CAPTION_HEIGHT_DP = 32;
-    private static final int CAPTION_WIDTH_DP = 216;
-    private static final int SHADOW_RADIUS_DP = 5;
     private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
     private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
 
-    private final Rect mOutsetsDp = new Rect();
     private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
             new WindowDecoration.RelayoutResult<>();
 
@@ -104,11 +104,14 @@
     private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
     private SurfaceControl.Transaction mMockSurfaceControlStartT;
     private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+    private SurfaceControl.Transaction mMockSurfaceControlAddWindowT;
+    private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
 
     @Before
     public void setUp() {
         mMockSurfaceControlStartT = createMockSurfaceControlTransaction();
         mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
+        mMockSurfaceControlAddWindowT = createMockSurfaceControlTransaction();
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
                 .create(any(), any(), any());
@@ -147,7 +150,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -197,8 +204,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
-
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
 
@@ -221,15 +231,16 @@
 
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
         verify(captionContainerSurfaceBuilder).setContainerLayer();
-        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8);
-        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
+        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
+        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64);
         verify(mMockSurfaceControlStartT).show(captionContainerSurface);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
+
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
-                                && lp.width == 300
+                                && lp.width == 432
                                 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
         if (ViewRootImpl.CAPTION_ON_SHELL) {
             verify(mMockView).setTaskFocusState(true);
@@ -248,7 +259,6 @@
 
         assertEquals(380, mRelayoutResult.mWidth);
         assertEquals(220, mRelayoutResult.mHeight);
-        assertEquals(2, mRelayoutResult.mDensity, 0.f);
     }
 
     @Test
@@ -287,7 +297,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -356,9 +370,75 @@
         verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
     }
 
+    @Test
+    public void testAddWindow() {
+        final Display defaultDisplay = mock(Display.class);
+        doReturn(defaultDisplay).when(mMockDisplayController)
+                .getDisplay(Display.DEFAULT_DISPLAY);
+
+        final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder decorContainerSurfaceBuilder =
+                createMockSurfaceControlBuilder(decorContainerSurface);
+        mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+        final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+                createMockSurfaceControlBuilder(taskBackgroundSurface);
+        mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+        final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder captionContainerSurfaceBuilder =
+                createMockSurfaceControlBuilder(captionContainerSurface);
+        mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        mMockSurfaceControlTransactions.add(t);
+        final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+                new ActivityManager.TaskDescription.Builder()
+                        .setBackgroundColor(Color.YELLOW);
+        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setDisplayId(Display.DEFAULT_DISPLAY)
+                .setTaskDescriptionBuilder(taskDescriptionBuilder)
+                .setBounds(TASK_BOUNDS)
+                .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+                .setVisible(true)
+                .build();
+        taskInfo.isFocused = true;
+        taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
+        final SurfaceControl taskSurface = mock(SurfaceControl.class);
+        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+        windowDecor.relayout(taskInfo);
+
+        final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class);
+        final SurfaceControl.Builder additionalWindowSurfaceBuilder =
+                createMockSurfaceControlBuilder(additionalWindowSurface);
+        mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder);
+
+        WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow();
+
+        verify(additionalWindowSurfaceBuilder).setContainerLayer();
+        verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface);
+        verify(additionalWindowSurfaceBuilder).build();
+        verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40);
+        verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 432, 64);
+        verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface);
+        verify(mMockSurfaceControlViewHostFactory, Mockito.times(2))
+                .create(any(), eq(defaultDisplay), any());
+        assertThat(additionalWindow.mWindowViewHost).isNotNull();
+
+        additionalWindow.releaseView();
+
+        assertThat(additionalWindow.mWindowViewHost).isNull();
+        assertThat(additionalWindow.mWindowSurface).isNull();
+    }
+
     private TestWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
-        return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
+        return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
+                mMockDisplayController, mMockShellTaskOrganizer,
                 taskInfo, testSurface,
                 new MockObjectSupplier<>(mMockSurfaceControlBuilders,
                         () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
@@ -410,9 +490,28 @@
 
         @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-            relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP,
-                    CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT,
-                    mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult);
+            mRelayoutParams.mLayoutResId = 0;
+            mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
+            mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+            mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
+
+            relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
+                    mMockWindowContainerTransaction, mMockView, mRelayoutResult);
+        }
+
+        private WindowDecoration.AdditionalWindow addTestWindow() {
+            final Resources resources = mDecorWindowContext.getResources();
+            int x = mRelayoutParams.mCaptionX;
+            int y = mRelayoutParams.mCaptionY;
+            int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+            int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+            String name = "Test Window";
+            WindowDecoration.AdditionalWindow additionalWindow =
+                    addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT,
+                            x - mRelayoutResult.mDecorContainerOffsetX,
+                            y - mRelayoutResult.mDecorContainerOffsetY,
+                            width, height);
+            return additionalWindow;
         }
     }
 }
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index eb8d26a..b1f327c 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -211,6 +211,8 @@
         "tests/data/**/*.apk",
         "tests/data/**/*.arsc",
         "tests/data/**/*.idmap",
+        ":FrameworkResourcesSparseTestApp",
+        ":FrameworkResourcesNotSparseTestApp",
     ],
     test_suites: ["device-tests"],
 }
diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp
index b3fb145..b68143d 100644
--- a/libs/androidfw/LocaleDataTables.cpp
+++ b/libs/androidfw/LocaleDataTables.cpp
@@ -143,6 +143,7 @@
     {0xACE00000u, 46u}, // ahl -> Latn
     {0xB8E00000u,  1u}, // aho -> Ahom
     {0x99200000u, 46u}, // ajg -> Latn
+    {0xCD200000u,  2u}, // ajt -> Arab
     {0x616B0000u, 46u}, // ak -> Latn
     {0xA9400000u, 101u}, // akk -> Xsux
     {0x81600000u, 46u}, // ala -> Latn
@@ -1053,6 +1054,7 @@
     {0xB70D0000u, 46u}, // nyn -> Latn
     {0xA32D0000u, 46u}, // nzi -> Latn
     {0x6F630000u, 46u}, // oc -> Latn
+    {0x6F634553u, 46u}, // oc-ES -> Latn
     {0x88CE0000u, 46u}, // ogc -> Latn
     {0x6F6A0000u, 11u}, // oj -> Cans
     {0xC92E0000u, 11u}, // ojs -> Cans
@@ -1093,6 +1095,7 @@
     {0xB4EF0000u, 71u}, // phn -> Phnx
     {0xAD0F0000u, 46u}, // pil -> Latn
     {0xBD0F0000u, 46u}, // pip -> Latn
+    {0xC90F0000u, 46u}, // pis -> Latn
     {0x814F0000u,  9u}, // pka -> Brah
     {0xB94F0000u, 46u}, // pko -> Latn
     {0x706C0000u, 46u}, // pl -> Latn
@@ -1204,12 +1207,14 @@
     {0xE1720000u, 46u}, // sly -> Latn
     {0x736D0000u, 46u}, // sm -> Latn
     {0x81920000u, 46u}, // sma -> Latn
+    {0x8D920000u, 46u}, // smd -> Latn
     {0xA5920000u, 46u}, // smj -> Latn
     {0xB5920000u, 46u}, // smn -> Latn
     {0xBD920000u, 76u}, // smp -> Samr
     {0xC1920000u, 46u}, // smq -> Latn
     {0xC9920000u, 46u}, // sms -> Latn
     {0x736E0000u, 46u}, // sn -> Latn
+    {0x85B20000u, 46u}, // snb -> Latn
     {0x89B20000u, 46u}, // snc -> Latn
     {0xA9B20000u, 46u}, // snk -> Latn
     {0xBDB20000u, 46u}, // snp -> Latn
@@ -1314,6 +1319,7 @@
     {0x746F0000u, 46u}, // to -> Latn
     {0x95D30000u, 46u}, // tof -> Latn
     {0x99D30000u, 46u}, // tog -> Latn
+    {0xA9D30000u, 46u}, // tok -> Latn
     {0xC1D30000u, 46u}, // toq -> Latn
     {0xA1F30000u, 46u}, // tpi -> Latn
     {0xB1F30000u, 46u}, // tpm -> Latn
@@ -1527,6 +1533,7 @@
     0x61665A414C61746ELLU, // af_Latn_ZA
     0xC0C0434D4C61746ELLU, // agq_Latn_CM
     0xB8E0494E41686F6DLLU, // aho_Ahom_IN
+    0xCD20544E41726162LLU, // ajt_Arab_TN
     0x616B47484C61746ELLU, // ak_Latn_GH
     0xA940495158737578LLU, // akk_Xsux_IQ
     0xB560584B4C61746ELLU, // aln_Latn_XK
@@ -1534,6 +1541,7 @@
     0x616D455445746869LLU, // am_Ethi_ET
     0xB9804E474C61746ELLU, // amo_Latn_NG
     0x616E45534C61746ELLU, // an_Latn_ES
+    0xB5A04E474C61746ELLU, // ann_Latn_NG
     0xE5C049444C61746ELLU, // aoz_Latn_ID
     0x8DE0544741726162LLU, // apd_Arab_TG
     0x6172454741726162LLU, // ar_Arab_EG
@@ -2039,6 +2047,7 @@
     0xB88F49525870656FLLU, // peo_Xpeo_IR
     0xACAF44454C61746ELLU, // pfl_Latn_DE
     0xB4EF4C4250686E78LLU, // phn_Phnx_LB
+    0xC90F53424C61746ELLU, // pis_Latn_SB
     0x814F494E42726168LLU, // pka_Brah_IN
     0xB94F4B454C61746ELLU, // pko_Latn_KE
     0x706C504C4C61746ELLU, // pl_Latn_PL
@@ -2119,11 +2128,13 @@
     0xE17249444C61746ELLU, // sly_Latn_ID
     0x736D57534C61746ELLU, // sm_Latn_WS
     0x819253454C61746ELLU, // sma_Latn_SE
+    0x8D92414F4C61746ELLU, // smd_Latn_AO
     0xA59253454C61746ELLU, // smj_Latn_SE
     0xB59246494C61746ELLU, // smn_Latn_FI
     0xBD92494C53616D72LLU, // smp_Samr_IL
     0xC99246494C61746ELLU, // sms_Latn_FI
     0x736E5A574C61746ELLU, // sn_Latn_ZW
+    0x85B24D594C61746ELLU, // snb_Latn_MY
     0xA9B24D4C4C61746ELLU, // snk_Latn_ML
     0x736F534F4C61746ELLU, // so_Latn_SO
     0x99D2555A536F6764LLU, // sog_Sogd_UZ
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index ba93546..267190a 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -1020,40 +1020,6 @@
     return base::unexpected(std::nullopt);
 }
 
-template <typename TChar, typename SP>
-base::expected<size_t, NullOrIOError> ResStringPool::stringIndex(
-        SP sp, std::unordered_map<SP, size_t>& map) const
-{
-    AutoMutex lock(mStringIndexLock);
-
-    if (map.empty()) {
-        // build string index on the first call
-        for (size_t i = 0; i < mHeader->stringCount; i++) {
-            base::expected<SP, NullOrIOError> s;
-            if constexpr(std::is_same_v<TChar, char16_t>) {
-                s = stringAt(i);
-            } else {
-                s = string8At(i);
-            }
-            if (s.has_value()) {
-                const auto r = map.insert({*s, i});
-                if (!r.second) {
-                    ALOGE("failed to build string index, string id=%zu\n", i);
-                }
-            } else {
-                return base::unexpected(s.error());
-            }
-        }
-    }
-
-    if (!map.empty()) {
-        const auto result = map.find(sp);
-        if (result != map.end())
-            return result->second;
-    }
-    return base::unexpected(std::nullopt);
-}
-
 base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_t* str,
                                                                    size_t strLen) const
 {
@@ -1061,28 +1027,134 @@
         return base::unexpected(std::nullopt);
     }
 
-    if (kDebugStringPoolNoisy) {
-        ALOGI("indexOfString (%s): %s", isUTF8() ? "UTF-8" : "UTF-16",
-                String8(str, strLen).string());
-    }
-
-    base::expected<size_t, NullOrIOError> idx;
-    if (isUTF8()) {
-        auto str8 = String8(str, strLen);
-        idx = stringIndex<char>(StringPiece(str8.c_str(), str8.size()), mStringIndex8);
-    } else {
-        idx = stringIndex<char16_t>(StringPiece16(str, strLen), mStringIndex16);
-    }
-
-    if (UNLIKELY(!idx.has_value())) {
-        return base::unexpected(idx.error());
-    }
-
-    if (*idx < mHeader->stringCount) {
+    if ((mHeader->flags&ResStringPool_header::UTF8_FLAG) != 0) {
         if (kDebugStringPoolNoisy) {
-            ALOGI("MATCH! (idx=%zu)", *idx);
+            ALOGI("indexOfString UTF-8: %s", String8(str, strLen).string());
         }
-        return *idx;
+
+        // The string pool contains UTF 8 strings; we don't want to cause
+        // temporary UTF-16 strings to be created as we search.
+        if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
+            // Do a binary search for the string...  this is a little tricky,
+            // because the strings are sorted with strzcmp16().  So to match
+            // the ordering, we need to convert strings in the pool to UTF-16.
+            // But we don't want to hit the cache, so instead we will have a
+            // local temporary allocation for the conversions.
+            size_t convBufferLen = strLen + 4;
+            std::vector<char16_t> convBuffer(convBufferLen);
+            ssize_t l = 0;
+            ssize_t h = mHeader->stringCount-1;
+
+            ssize_t mid;
+            while (l <= h) {
+                mid = l + (h - l)/2;
+                int c = -1;
+                const base::expected<StringPiece, NullOrIOError> s = string8At(mid);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (s.has_value()) {
+                    char16_t* end = utf8_to_utf16(reinterpret_cast<const uint8_t*>(s->data()),
+                                                  s->size(), convBuffer.data(), convBufferLen);
+                    c = strzcmp16(convBuffer.data(), end-convBuffer.data(), str, strLen);
+                }
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
+                          s->data(), c, (int)l, (int)mid, (int)h);
+                }
+                if (c == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return mid;
+                } else if (c < 0) {
+                    l = mid + 1;
+                } else {
+                    h = mid - 1;
+                }
+            }
+        } else {
+            // It is unusual to get the ID from an unsorted string block...
+            // most often this happens because we want to get IDs for style
+            // span tags; since those always appear at the end of the string
+            // block, start searching at the back.
+            String8 str8(str, strLen);
+            const size_t str8Len = str8.size();
+            for (int i=mHeader->stringCount-1; i>=0; i--) {
+                const base::expected<StringPiece, NullOrIOError> s = string8At(i);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (s.has_value()) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("Looking at %s, i=%d\n", s->data(), i);
+                    }
+                    if (str8Len == s->size()
+                            && memcmp(s->data(), str8.string(), str8Len) == 0) {
+                        if (kDebugStringPoolNoisy) {
+                            ALOGI("MATCH!");
+                        }
+                        return i;
+                    }
+                }
+            }
+        }
+
+    } else {
+        if (kDebugStringPoolNoisy) {
+            ALOGI("indexOfString UTF-16: %s", String8(str, strLen).string());
+        }
+
+        if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
+            // Do a binary search for the string...
+            ssize_t l = 0;
+            ssize_t h = mHeader->stringCount-1;
+
+            ssize_t mid;
+            while (l <= h) {
+                mid = l + (h - l)/2;
+                const base::expected<StringPiece16, NullOrIOError> s = stringAt(mid);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                int c = s.has_value() ? strzcmp16(s->data(), s->size(), str, strLen) : -1;
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
+                          String8(s->data(), s->size()).string(), c, (int)l, (int)mid, (int)h);
+                }
+                if (c == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return mid;
+                } else if (c < 0) {
+                    l = mid + 1;
+                } else {
+                    h = mid - 1;
+                }
+            }
+        } else {
+            // It is unusual to get the ID from an unsorted string block...
+            // most often this happens because we want to get IDs for style
+            // span tags; since those always appear at the end of the string
+            // block, start searching at the back.
+            for (int i=mHeader->stringCount-1; i>=0; i--) {
+                const base::expected<StringPiece16, NullOrIOError> s = stringAt(i);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, i=%d\n", String8(s->data(), s->size()).string(), i);
+                }
+                if (s.has_value() && strLen == s->size() &&
+                        strzcmp16(s->data(), s->size(), str, strLen) == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return i;
+                }
+            }
+        }
     }
     return base::unexpected(std::nullopt);
 }
diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp
index 58fc5bb..a1385f2 100644
--- a/libs/androidfw/ZipUtils.cpp
+++ b/libs/androidfw/ZipUtils.cpp
@@ -35,7 +35,7 @@
 using namespace android;
 
 // TODO: This can go away once the only remaining usage in aapt goes away.
-class FileReader : public zip_archive::Reader {
+class FileReader final : public zip_archive::Reader {
   public:
     explicit FileReader(FILE* fp) : Reader(), mFp(fp), mCurrentOffset(0) {
     }
@@ -66,7 +66,7 @@
     mutable off64_t mCurrentOffset;
 };
 
-class FdReader : public zip_archive::Reader {
+class FdReader final : public zip_archive::Reader {
   public:
     explicit FdReader(int fd) : mFd(fd) {
     }
@@ -79,7 +79,7 @@
     const int mFd;
 };
 
-class BufferReader : public zip_archive::Reader {
+class BufferReader final : public zip_archive::Reader {
   public:
     BufferReader(incfs::map_ptr<void> input, size_t inputSize) : Reader(),
         mInput(input.convert<uint8_t>()),
@@ -105,7 +105,7 @@
     const size_t mInputSize;
 };
 
-class BufferWriter : public zip_archive::Writer {
+class BufferWriter final : public zip_archive::Writer {
   public:
     BufferWriter(void* output, size_t outputSize) : Writer(),
         mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index d588b235..c740832 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -42,7 +42,6 @@
 #include <array>
 #include <map>
 #include <memory>
-#include <unordered_map>
 
 namespace android {
 
@@ -55,7 +54,7 @@
 // The version should only be changed when a backwards-incompatible change must be made to the
 // fabricated overlay file format. Old fabricated overlays must be migrated to the new file format
 // to prevent losing fabricated overlay data.
-constexpr const uint32_t kFabricatedOverlayCurrentVersion = 2;
+constexpr const uint32_t kFabricatedOverlayCurrentVersion = 3;
 
 // Returns whether or not the path represents a fabricated overlay.
 bool IsFabricatedOverlay(const std::string& path);
@@ -564,17 +563,8 @@
     incfs::map_ptr<uint32_t>                      mStyles;
     uint32_t                                      mStylePoolSize;    // number of uint32_t
 
-    // mStringIndex is used to quickly map a string to its ID
-    mutable Mutex                                       mStringIndexLock;
-    mutable std::unordered_map<StringPiece, size_t>     mStringIndex8;
-    mutable std::unordered_map<StringPiece16, size_t>   mStringIndex16;
-
     base::expected<StringPiece, NullOrIOError> stringDecodeAt(
         size_t idx, incfs::map_ptr<uint8_t> str, size_t encLen) const;
-
-    template <typename TChar, typename SP=BasicStringPiece<TChar>>
-    base::expected<size_t, NullOrIOError> stringIndex(
-        SP str, std::unordered_map<SP, size_t>& map) const;
 };
 
 /**
diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp
index 15be80c..d1cfd03 100644
--- a/libs/androidfw/tests/CursorWindow_test.cpp
+++ b/libs/androidfw/tests/CursorWindow_test.cpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include <memory>
 #include <utility>
 
 #include "androidfw/CursorWindow.h"
@@ -184,7 +185,7 @@
     ASSERT_EQ(w->allocRow(), OK);
 
     // Scratch buffer that will fit before inflation
-    void* buf = malloc(kHalfInlineSize);
+    char buf[kHalfInlineSize];
 
     // Store simple value
     ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
@@ -262,7 +263,7 @@
     ASSERT_EQ(w->allocRow(), OK);
 
     // Scratch buffer that will fit before inflation
-    void* buf = malloc(kHalfInlineSize);
+    char buf[kHalfInlineSize];
 
     // Store simple value
     ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
@@ -322,7 +323,8 @@
     ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK);
 
     // Store object that forces inflation
-    void* buf = malloc(kGiantSize);
+    std::unique_ptr<char> bufPtr(new char[kGiantSize]);
+    void* buf = bufPtr.get();
     memset(buf, 42, kGiantSize);
     ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK);
 
diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp
index d214e2d..c90ec19 100644
--- a/libs/androidfw/tests/LoadedArsc_test.cpp
+++ b/libs/androidfw/tests/LoadedArsc_test.cpp
@@ -71,62 +71,6 @@
   ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
 }
 
-TEST(LoadedArscTest, LoadSparseEntryApp) {
-  std::string contents;
-  ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
-                                      &contents));
-
-  std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
-                                                                   contents.length());
-  ASSERT_THAT(loaded_arsc, NotNull());
-
-  const LoadedPackage* package =
-      loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
-  ASSERT_THAT(package, NotNull());
-
-  const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
-  const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
-
-  const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
-  ASSERT_THAT(type_spec, NotNull());
-  ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
-  auto type = type_spec->type_entries[0];
-  ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
-}
-
-TEST(LoadedArscTest, FindSparseEntryApp) {
-  std::string contents;
-  ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
-                                      &contents));
-
-  std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
-                                                                   contents.length());
-  ASSERT_THAT(loaded_arsc, NotNull());
-
-  const LoadedPackage* package =
-      loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_v26));
-  ASSERT_THAT(package, NotNull());
-
-  const uint8_t type_index = get_type_id(sparse::R::string::only_v26) - 1;
-  const uint16_t entry_index = get_entry_id(sparse::R::string::only_v26);
-
-  const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
-  ASSERT_THAT(type_spec, NotNull());
-  ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
-
-  // Ensure that AAPT2 sparsely encoded the v26 config as expected.
-  auto type_entry = std::find_if(
-    type_spec->type_entries.begin(), type_spec->type_entries.end(),
-    [](const TypeSpec::TypeEntry& x) { return x.config.sdkVersion == 26; });
-  ASSERT_NE(type_entry, type_spec->type_entries.end());
-  ASSERT_NE(type_entry->type->flags & ResTable_type::FLAG_SPARSE, 0);
-
-  // Test fetching a resource with only sparsely encoded configs by name.
-  auto id = package->FindEntryByName(u"string", u"only_v26");
-  ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_v26, 0));
-}
-
 TEST(LoadedArscTest, LoadSharedLibrary) {
   std::string contents;
   ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/lib_one/lib_one.apk", "resources.arsc",
@@ -404,4 +348,84 @@
 // sizeof(Res_value) might not be backwards compatible.
 // TEST(LoadedArscTest, LoadingShouldBeForwardsAndBackwardsCompatible) { ASSERT_TRUE(false); }
 
+class LoadedArscParameterizedTest :
+    public testing::TestWithParam<std::string> {
+};
+
+TEST_P(LoadedArscParameterizedTest, LoadSparseEntryApp) {
+  std::string contents;
+  ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+  std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+                                                                   contents.length());
+  ASSERT_THAT(loaded_arsc, NotNull());
+
+  const LoadedPackage* package =
+      loaded_arsc->GetPackageById(get_package_id(sparse::R::integer::foo_9));
+  ASSERT_THAT(package, NotNull());
+
+  const uint8_t type_index = get_type_id(sparse::R::integer::foo_9) - 1;
+  const uint16_t entry_index = get_entry_id(sparse::R::integer::foo_9);
+
+  const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+  ASSERT_THAT(type_spec, NotNull());
+  ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+  auto type = type_spec->type_entries[0];
+  ASSERT_TRUE(LoadedPackage::GetEntry(type.type, entry_index).has_value());
+}
+
+TEST_P(LoadedArscParameterizedTest, FindSparseEntryApp) {
+  std::string contents;
+  ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+  std::unique_ptr<const LoadedArsc> loaded_arsc = LoadedArsc::Load(contents.data(),
+                                                                   contents.length());
+  ASSERT_THAT(loaded_arsc, NotNull());
+
+  const LoadedPackage* package =
+      loaded_arsc->GetPackageById(get_package_id(sparse::R::string::only_land));
+  ASSERT_THAT(package, NotNull());
+
+  const uint8_t type_index = get_type_id(sparse::R::string::only_land) - 1;
+
+  const TypeSpec* type_spec = package->GetTypeSpecByTypeIndex(type_index);
+  ASSERT_THAT(type_spec, NotNull());
+  ASSERT_THAT(type_spec->type_entries.size(), Ge(1u));
+
+  // Type Entry with default orientation is not sparse encoded because the ratio of
+  // populated entries to total entries is above threshold.
+  // Only find out default locale because Soong build system will introduce pseudo
+  // locales for the apk generated at runtime.
+  auto type_entry_default = std::find_if(
+    type_spec->type_entries.begin(), type_spec->type_entries.end(),
+    [] (const TypeSpec::TypeEntry& x) { return x.config.orientation == 0 &&
+                                               x.config.locale == 0; });
+  ASSERT_NE(type_entry_default, type_spec->type_entries.end());
+  ASSERT_EQ(type_entry_default->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+  // Type Entry with land orientation is sparse encoded as expected.
+  // Only find out default locale because Soong build system will introduce pseudo
+  // locales for the apk generated at runtime.
+  auto type_entry_land = std::find_if(
+    type_spec->type_entries.begin(), type_spec->type_entries.end(),
+    [](const TypeSpec::TypeEntry& x) { return x.config.orientation ==
+                                              ResTable_config::ORIENTATION_LAND &&
+                                              x.config.locale == 0; });
+  ASSERT_NE(type_entry_land, type_spec->type_entries.end());
+  ASSERT_NE(type_entry_land->type->flags & ResTable_type::FLAG_SPARSE, 0);
+
+  // Test fetching a resource with only sparsely encoded configs by name.
+  auto id = package->FindEntryByName(u"string", u"only_land");
+  ASSERT_EQ(id.value(), fix_package_id(sparse::R::string::only_land, 0));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+        FrameWorkResourcesLoadedArscTests,
+        LoadedArscParameterizedTest,
+        ::testing::Values(
+          base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+          base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+        ));
+
 }  // namespace android
diff --git a/libs/androidfw/tests/ResTable_test.cpp b/libs/androidfw/tests/ResTable_test.cpp
index 9aeb00c..fbf7098 100644
--- a/libs/androidfw/tests/ResTable_test.cpp
+++ b/libs/androidfw/tests/ResTable_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
 
 #include <codecvt>
 #include <locale>
@@ -41,34 +42,6 @@
   ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
 }
 
-TEST(ResTableTest, ShouldLoadSparseEntriesSuccessfully) {
-  std::string contents;
-  ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/sparse/sparse.apk", "resources.arsc",
-                                      &contents));
-
-  ResTable table;
-  ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
-
-  ResTable_config config;
-  memset(&config, 0, sizeof(config));
-  config.sdkVersion = 26;
-  table.setParameters(&config);
-
-  String16 name(u"com.android.sparse:integer/foo_9");
-  uint32_t flags;
-  uint32_t resid =
-      table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags);
-  ASSERT_NE(0u, resid);
-
-  Res_value val;
-  ResTable_config selected_config;
-  ASSERT_GE(
-      table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
-      0);
-  EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
-  EXPECT_EQ(900u, val.data);
-}
-
 TEST(ResTableTest, SimpleTypeIsRetrievedCorrectly) {
   std::string contents;
   ASSERT_TRUE(ReadFileFromZipToString(GetTestDataPath() + "/basic/basic.apk",
@@ -476,4 +449,43 @@
   ASSERT_FALSE(invalid_pool->stringAt(invalid_val.data).has_value());
 }
 
+class ResTableParameterizedTest :
+    public testing::TestWithParam<std::string> {
+};
+
+TEST_P(ResTableParameterizedTest, ShouldLoadSparseEntriesSuccessfully) {
+  std::string contents;
+  ASSERT_TRUE(ReadFileFromZipToString(GetParam(), "resources.arsc", &contents));
+
+  ResTable table;
+  ASSERT_EQ(NO_ERROR, table.add(contents.data(), contents.size()));
+
+  ResTable_config config;
+  memset(&config, 0, sizeof(config));
+  config.orientation = ResTable_config::ORIENTATION_LAND;
+  table.setParameters(&config);
+
+  String16 name(u"com.android.sparse:integer/foo_9");
+  uint32_t flags;
+  uint32_t resid =
+      table.identifierForName(name.string(), name.size(), nullptr, 0, nullptr, 0, &flags);
+  ASSERT_NE(0u, resid);
+
+  Res_value val;
+  ResTable_config selected_config;
+  ASSERT_GE(
+      table.getResource(resid, &val, false /*mayBeBag*/, 0u /*density*/, &flags, &selected_config),
+      0);
+  EXPECT_EQ(Res_value::TYPE_INT_DEC, val.dataType);
+  EXPECT_EQ(900u, val.data);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+        FrameWorkResourcesResTableTests,
+        ResTableParameterizedTest,
+        ::testing::Values(
+           base::GetExecutableDirectory() + "/tests/data/sparse/sparse.apk",
+           base::GetExecutableDirectory() + "/FrameworkResourcesSparseTestApp.apk"
+        ));
+
 }  // namespace android
diff --git a/libs/androidfw/tests/SparseEntry_bench.cpp b/libs/androidfw/tests/SparseEntry_bench.cpp
index c9b4ad8..fffeeb8 100644
--- a/libs/androidfw/tests/SparseEntry_bench.cpp
+++ b/libs/androidfw/tests/SparseEntry_bench.cpp
@@ -16,6 +16,7 @@
 
 #include "androidfw/AssetManager.h"
 #include "androidfw/ResourceTypes.h"
+#include "android-base/file.h"
 
 #include "BenchmarkHelpers.h"
 #include "data/sparse/R.h"
@@ -24,40 +25,74 @@
 
 namespace android {
 
+static void BM_SparseEntryGetResourceHelper(const std::vector<std::string>& paths,
+                    uint32_t resid, benchmark::State& state, void (*GetResourceBenchmarkFunc)(
+                    const std::vector<std::string>&, const ResTable_config*,
+                    uint32_t, benchmark::State&)){
+    ResTable_config config;
+    memset(&config, 0, sizeof(config));
+    config.orientation = ResTable_config::ORIENTATION_LAND;
+    GetResourceBenchmarkFunc(paths, &config, resid, state);
+}
+
 static void BM_SparseEntryGetResourceOldSparse(benchmark::State& state, uint32_t resid) {
-  ResTable_config config;
-  memset(&config, 0, sizeof(config));
-  config.sdkVersion = 26;
-  GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+  BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+                                    state, &GetResourceBenchmarkOld);
 }
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Small, sparse::R::integer::foo_9);
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparse, Large, sparse::R::string::foo_999);
 
 static void BM_SparseEntryGetResourceOldNotSparse(benchmark::State& state, uint32_t resid) {
-  ResTable_config config;
-  memset(&config, 0, sizeof(config));
-  config.sdkVersion = 26;
-  GetResourceBenchmarkOld({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+   BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+                                   state, &GetResourceBenchmarkOld);
 }
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Small, sparse::R::integer::foo_9);
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparse, Large, sparse::R::string::foo_999);
 
 static void BM_SparseEntryGetResourceSparse(benchmark::State& state, uint32_t resid) {
-  ResTable_config config;
-  memset(&config, 0, sizeof(config));
-  config.sdkVersion = 26;
-  GetResourceBenchmark({GetTestDataPath() + "/sparse/sparse.apk"}, &config, resid, state);
+  BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/sparse.apk"}, resid,
+                                  state, &GetResourceBenchmark);
 }
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Small, sparse::R::integer::foo_9);
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparse, Large, sparse::R::string::foo_999);
 
 static void BM_SparseEntryGetResourceNotSparse(benchmark::State& state, uint32_t resid) {
-  ResTable_config config;
-  memset(&config, 0, sizeof(config));
-  config.sdkVersion = 26;
-  GetResourceBenchmark({GetTestDataPath() + "/sparse/not_sparse.apk"}, &config, resid, state);
+  BM_SparseEntryGetResourceHelper({GetTestDataPath() + "/sparse/not_sparse.apk"}, resid,
+                                  state, &GetResourceBenchmark);
 }
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Small, sparse::R::integer::foo_9);
 BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparse, Large, sparse::R::string::foo_999);
 
+static void BM_SparseEntryGetResourceOldSparseRuntime(benchmark::State& state, uint32_t resid) {
+  BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+                                  "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+                                  &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceOldNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+  BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+                                  "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+                                  &GetResourceBenchmarkOld);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceOldNotSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceSparseRuntime(benchmark::State& state, uint32_t resid) {
+  BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+                                  "/FrameworkResourcesSparseTestApp.apk"}, resid, state,
+                                  &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceSparseRuntime, Large, sparse::R::string::foo_999);
+
+static void BM_SparseEntryGetResourceNotSparseRuntime(benchmark::State& state, uint32_t resid) {
+  BM_SparseEntryGetResourceHelper({base::GetExecutableDirectory() +
+                                  "/FrameworkResourcesNotSparseTestApp.apk"}, resid, state,
+                                  &GetResourceBenchmark);
+}
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Small, sparse::R::integer::foo_9);
+BENCHMARK_CAPTURE(BM_SparseEntryGetResourceNotSparseRuntime, Large, sparse::R::string::foo_999);
+
 }  // namespace android
diff --git a/libs/androidfw/tests/data/sparse/Android.bp b/libs/androidfw/tests/data/sparse/Android.bp
new file mode 100644
index 0000000..0fed79e
--- /dev/null
+++ b/libs/androidfw/tests/data/sparse/Android.bp
@@ -0,0 +1,14 @@
+android_test_helper_app {
+    name: "FrameworkResourcesSparseTestApp",
+    sdk_version: "current",
+    min_sdk_version: "32",
+    aaptflags: [
+        "--enable-sparse-encoding",
+    ],
+}
+
+android_test_helper_app {
+    name: "FrameworkResourcesNotSparseTestApp",
+    sdk_version: "current",
+    min_sdk_version: "32",
+}
diff --git a/libs/androidfw/tests/data/sparse/AndroidManifest.xml b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
index 27911b6..9c23a72 100644
--- a/libs/androidfw/tests/data/sparse/AndroidManifest.xml
+++ b/libs/androidfw/tests/data/sparse/AndroidManifest.xml
@@ -17,4 +17,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.sparse">
     <application />
+    <uses-sdk android:minSdkVersion="32" />
 </manifest>
diff --git a/libs/androidfw/tests/data/sparse/R.h b/libs/androidfw/tests/data/sparse/R.h
index 2492dbf..a66e1af 100644
--- a/libs/androidfw/tests/data/sparse/R.h
+++ b/libs/androidfw/tests/data/sparse/R.h
@@ -42,7 +42,7 @@
   struct string {
     enum : uint32_t {
       foo_999 = 0x7f0203e7,
-      only_v26 = 0x7f0203e8
+      only_land = 0x7f0203e8
     };
   };
 };
diff --git a/libs/androidfw/tests/data/sparse/gen_strings.sh b/libs/androidfw/tests/data/sparse/gen_strings.sh
index 4ea5468..114ecbb 100755
--- a/libs/androidfw/tests/data/sparse/gen_strings.sh
+++ b/libs/androidfw/tests/data/sparse/gen_strings.sh
@@ -1,20 +1,20 @@
 #!/bin/bash
 
 OUTPUT_default=res/values/strings.xml
-OUTPUT_v26=res/values-v26/strings.xml
+OUTPUT_land=res/values-land/strings.xml
 
 echo "<resources>" > $OUTPUT_default
-echo "<resources>" > $OUTPUT_v26
+echo "<resources>" > $OUTPUT_land
 for i in {0..999}
 do
     echo "  <string name=\"foo_$i\">$i</string>" >> $OUTPUT_default
     if [ "$(($i % 3))" -eq "0" ]
     then
-        echo "  <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_v26
+        echo "  <string name=\"foo_$i\">$(($i * 10))</string>" >> $OUTPUT_land
     fi
 done
 echo "</resources>" >> $OUTPUT_default
 
-echo "  <string name=\"only_v26\">only v26</string>" >> $OUTPUT_v26
-echo "</resources>" >> $OUTPUT_v26
+echo "  <string name=\"only_land\">only land</string>" >> $OUTPUT_land
+echo "</resources>" >> $OUTPUT_land
 
diff --git a/libs/androidfw/tests/data/sparse/not_sparse.apk b/libs/androidfw/tests/data/sparse/not_sparse.apk
index b08a621..4d4d4a8 100644
--- a/libs/androidfw/tests/data/sparse/not_sparse.apk
+++ b/libs/androidfw/tests/data/sparse/not_sparse.apk
Binary files differ
diff --git a/libs/androidfw/tests/data/sparse/res/values-land/strings.xml b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
new file mode 100644
index 0000000..66222c3
--- /dev/null
+++ b/libs/androidfw/tests/data/sparse/res/values-land/strings.xml
@@ -0,0 +1,337 @@
+<resources>
+  <string name="foo_0">0</string>
+  <string name="foo_3">30</string>
+  <string name="foo_6">60</string>
+  <string name="foo_9">90</string>
+  <string name="foo_12">120</string>
+  <string name="foo_15">150</string>
+  <string name="foo_18">180</string>
+  <string name="foo_21">210</string>
+  <string name="foo_24">240</string>
+  <string name="foo_27">270</string>
+  <string name="foo_30">300</string>
+  <string name="foo_33">330</string>
+  <string name="foo_36">360</string>
+  <string name="foo_39">390</string>
+  <string name="foo_42">420</string>
+  <string name="foo_45">450</string>
+  <string name="foo_48">480</string>
+  <string name="foo_51">510</string>
+  <string name="foo_54">540</string>
+  <string name="foo_57">570</string>
+  <string name="foo_60">600</string>
+  <string name="foo_63">630</string>
+  <string name="foo_66">660</string>
+  <string name="foo_69">690</string>
+  <string name="foo_72">720</string>
+  <string name="foo_75">750</string>
+  <string name="foo_78">780</string>
+  <string name="foo_81">810</string>
+  <string name="foo_84">840</string>
+  <string name="foo_87">870</string>
+  <string name="foo_90">900</string>
+  <string name="foo_93">930</string>
+  <string name="foo_96">960</string>
+  <string name="foo_99">990</string>
+  <string name="foo_102">1020</string>
+  <string name="foo_105">1050</string>
+  <string name="foo_108">1080</string>
+  <string name="foo_111">1110</string>
+  <string name="foo_114">1140</string>
+  <string name="foo_117">1170</string>
+  <string name="foo_120">1200</string>
+  <string name="foo_123">1230</string>
+  <string name="foo_126">1260</string>
+  <string name="foo_129">1290</string>
+  <string name="foo_132">1320</string>
+  <string name="foo_135">1350</string>
+  <string name="foo_138">1380</string>
+  <string name="foo_141">1410</string>
+  <string name="foo_144">1440</string>
+  <string name="foo_147">1470</string>
+  <string name="foo_150">1500</string>
+  <string name="foo_153">1530</string>
+  <string name="foo_156">1560</string>
+  <string name="foo_159">1590</string>
+  <string name="foo_162">1620</string>
+  <string name="foo_165">1650</string>
+  <string name="foo_168">1680</string>
+  <string name="foo_171">1710</string>
+  <string name="foo_174">1740</string>
+  <string name="foo_177">1770</string>
+  <string name="foo_180">1800</string>
+  <string name="foo_183">1830</string>
+  <string name="foo_186">1860</string>
+  <string name="foo_189">1890</string>
+  <string name="foo_192">1920</string>
+  <string name="foo_195">1950</string>
+  <string name="foo_198">1980</string>
+  <string name="foo_201">2010</string>
+  <string name="foo_204">2040</string>
+  <string name="foo_207">2070</string>
+  <string name="foo_210">2100</string>
+  <string name="foo_213">2130</string>
+  <string name="foo_216">2160</string>
+  <string name="foo_219">2190</string>
+  <string name="foo_222">2220</string>
+  <string name="foo_225">2250</string>
+  <string name="foo_228">2280</string>
+  <string name="foo_231">2310</string>
+  <string name="foo_234">2340</string>
+  <string name="foo_237">2370</string>
+  <string name="foo_240">2400</string>
+  <string name="foo_243">2430</string>
+  <string name="foo_246">2460</string>
+  <string name="foo_249">2490</string>
+  <string name="foo_252">2520</string>
+  <string name="foo_255">2550</string>
+  <string name="foo_258">2580</string>
+  <string name="foo_261">2610</string>
+  <string name="foo_264">2640</string>
+  <string name="foo_267">2670</string>
+  <string name="foo_270">2700</string>
+  <string name="foo_273">2730</string>
+  <string name="foo_276">2760</string>
+  <string name="foo_279">2790</string>
+  <string name="foo_282">2820</string>
+  <string name="foo_285">2850</string>
+  <string name="foo_288">2880</string>
+  <string name="foo_291">2910</string>
+  <string name="foo_294">2940</string>
+  <string name="foo_297">2970</string>
+  <string name="foo_300">3000</string>
+  <string name="foo_303">3030</string>
+  <string name="foo_306">3060</string>
+  <string name="foo_309">3090</string>
+  <string name="foo_312">3120</string>
+  <string name="foo_315">3150</string>
+  <string name="foo_318">3180</string>
+  <string name="foo_321">3210</string>
+  <string name="foo_324">3240</string>
+  <string name="foo_327">3270</string>
+  <string name="foo_330">3300</string>
+  <string name="foo_333">3330</string>
+  <string name="foo_336">3360</string>
+  <string name="foo_339">3390</string>
+  <string name="foo_342">3420</string>
+  <string name="foo_345">3450</string>
+  <string name="foo_348">3480</string>
+  <string name="foo_351">3510</string>
+  <string name="foo_354">3540</string>
+  <string name="foo_357">3570</string>
+  <string name="foo_360">3600</string>
+  <string name="foo_363">3630</string>
+  <string name="foo_366">3660</string>
+  <string name="foo_369">3690</string>
+  <string name="foo_372">3720</string>
+  <string name="foo_375">3750</string>
+  <string name="foo_378">3780</string>
+  <string name="foo_381">3810</string>
+  <string name="foo_384">3840</string>
+  <string name="foo_387">3870</string>
+  <string name="foo_390">3900</string>
+  <string name="foo_393">3930</string>
+  <string name="foo_396">3960</string>
+  <string name="foo_399">3990</string>
+  <string name="foo_402">4020</string>
+  <string name="foo_405">4050</string>
+  <string name="foo_408">4080</string>
+  <string name="foo_411">4110</string>
+  <string name="foo_414">4140</string>
+  <string name="foo_417">4170</string>
+  <string name="foo_420">4200</string>
+  <string name="foo_423">4230</string>
+  <string name="foo_426">4260</string>
+  <string name="foo_429">4290</string>
+  <string name="foo_432">4320</string>
+  <string name="foo_435">4350</string>
+  <string name="foo_438">4380</string>
+  <string name="foo_441">4410</string>
+  <string name="foo_444">4440</string>
+  <string name="foo_447">4470</string>
+  <string name="foo_450">4500</string>
+  <string name="foo_453">4530</string>
+  <string name="foo_456">4560</string>
+  <string name="foo_459">4590</string>
+  <string name="foo_462">4620</string>
+  <string name="foo_465">4650</string>
+  <string name="foo_468">4680</string>
+  <string name="foo_471">4710</string>
+  <string name="foo_474">4740</string>
+  <string name="foo_477">4770</string>
+  <string name="foo_480">4800</string>
+  <string name="foo_483">4830</string>
+  <string name="foo_486">4860</string>
+  <string name="foo_489">4890</string>
+  <string name="foo_492">4920</string>
+  <string name="foo_495">4950</string>
+  <string name="foo_498">4980</string>
+  <string name="foo_501">5010</string>
+  <string name="foo_504">5040</string>
+  <string name="foo_507">5070</string>
+  <string name="foo_510">5100</string>
+  <string name="foo_513">5130</string>
+  <string name="foo_516">5160</string>
+  <string name="foo_519">5190</string>
+  <string name="foo_522">5220</string>
+  <string name="foo_525">5250</string>
+  <string name="foo_528">5280</string>
+  <string name="foo_531">5310</string>
+  <string name="foo_534">5340</string>
+  <string name="foo_537">5370</string>
+  <string name="foo_540">5400</string>
+  <string name="foo_543">5430</string>
+  <string name="foo_546">5460</string>
+  <string name="foo_549">5490</string>
+  <string name="foo_552">5520</string>
+  <string name="foo_555">5550</string>
+  <string name="foo_558">5580</string>
+  <string name="foo_561">5610</string>
+  <string name="foo_564">5640</string>
+  <string name="foo_567">5670</string>
+  <string name="foo_570">5700</string>
+  <string name="foo_573">5730</string>
+  <string name="foo_576">5760</string>
+  <string name="foo_579">5790</string>
+  <string name="foo_582">5820</string>
+  <string name="foo_585">5850</string>
+  <string name="foo_588">5880</string>
+  <string name="foo_591">5910</string>
+  <string name="foo_594">5940</string>
+  <string name="foo_597">5970</string>
+  <string name="foo_600">6000</string>
+  <string name="foo_603">6030</string>
+  <string name="foo_606">6060</string>
+  <string name="foo_609">6090</string>
+  <string name="foo_612">6120</string>
+  <string name="foo_615">6150</string>
+  <string name="foo_618">6180</string>
+  <string name="foo_621">6210</string>
+  <string name="foo_624">6240</string>
+  <string name="foo_627">6270</string>
+  <string name="foo_630">6300</string>
+  <string name="foo_633">6330</string>
+  <string name="foo_636">6360</string>
+  <string name="foo_639">6390</string>
+  <string name="foo_642">6420</string>
+  <string name="foo_645">6450</string>
+  <string name="foo_648">6480</string>
+  <string name="foo_651">6510</string>
+  <string name="foo_654">6540</string>
+  <string name="foo_657">6570</string>
+  <string name="foo_660">6600</string>
+  <string name="foo_663">6630</string>
+  <string name="foo_666">6660</string>
+  <string name="foo_669">6690</string>
+  <string name="foo_672">6720</string>
+  <string name="foo_675">6750</string>
+  <string name="foo_678">6780</string>
+  <string name="foo_681">6810</string>
+  <string name="foo_684">6840</string>
+  <string name="foo_687">6870</string>
+  <string name="foo_690">6900</string>
+  <string name="foo_693">6930</string>
+  <string name="foo_696">6960</string>
+  <string name="foo_699">6990</string>
+  <string name="foo_702">7020</string>
+  <string name="foo_705">7050</string>
+  <string name="foo_708">7080</string>
+  <string name="foo_711">7110</string>
+  <string name="foo_714">7140</string>
+  <string name="foo_717">7170</string>
+  <string name="foo_720">7200</string>
+  <string name="foo_723">7230</string>
+  <string name="foo_726">7260</string>
+  <string name="foo_729">7290</string>
+  <string name="foo_732">7320</string>
+  <string name="foo_735">7350</string>
+  <string name="foo_738">7380</string>
+  <string name="foo_741">7410</string>
+  <string name="foo_744">7440</string>
+  <string name="foo_747">7470</string>
+  <string name="foo_750">7500</string>
+  <string name="foo_753">7530</string>
+  <string name="foo_756">7560</string>
+  <string name="foo_759">7590</string>
+  <string name="foo_762">7620</string>
+  <string name="foo_765">7650</string>
+  <string name="foo_768">7680</string>
+  <string name="foo_771">7710</string>
+  <string name="foo_774">7740</string>
+  <string name="foo_777">7770</string>
+  <string name="foo_780">7800</string>
+  <string name="foo_783">7830</string>
+  <string name="foo_786">7860</string>
+  <string name="foo_789">7890</string>
+  <string name="foo_792">7920</string>
+  <string name="foo_795">7950</string>
+  <string name="foo_798">7980</string>
+  <string name="foo_801">8010</string>
+  <string name="foo_804">8040</string>
+  <string name="foo_807">8070</string>
+  <string name="foo_810">8100</string>
+  <string name="foo_813">8130</string>
+  <string name="foo_816">8160</string>
+  <string name="foo_819">8190</string>
+  <string name="foo_822">8220</string>
+  <string name="foo_825">8250</string>
+  <string name="foo_828">8280</string>
+  <string name="foo_831">8310</string>
+  <string name="foo_834">8340</string>
+  <string name="foo_837">8370</string>
+  <string name="foo_840">8400</string>
+  <string name="foo_843">8430</string>
+  <string name="foo_846">8460</string>
+  <string name="foo_849">8490</string>
+  <string name="foo_852">8520</string>
+  <string name="foo_855">8550</string>
+  <string name="foo_858">8580</string>
+  <string name="foo_861">8610</string>
+  <string name="foo_864">8640</string>
+  <string name="foo_867">8670</string>
+  <string name="foo_870">8700</string>
+  <string name="foo_873">8730</string>
+  <string name="foo_876">8760</string>
+  <string name="foo_879">8790</string>
+  <string name="foo_882">8820</string>
+  <string name="foo_885">8850</string>
+  <string name="foo_888">8880</string>
+  <string name="foo_891">8910</string>
+  <string name="foo_894">8940</string>
+  <string name="foo_897">8970</string>
+  <string name="foo_900">9000</string>
+  <string name="foo_903">9030</string>
+  <string name="foo_906">9060</string>
+  <string name="foo_909">9090</string>
+  <string name="foo_912">9120</string>
+  <string name="foo_915">9150</string>
+  <string name="foo_918">9180</string>
+  <string name="foo_921">9210</string>
+  <string name="foo_924">9240</string>
+  <string name="foo_927">9270</string>
+  <string name="foo_930">9300</string>
+  <string name="foo_933">9330</string>
+  <string name="foo_936">9360</string>
+  <string name="foo_939">9390</string>
+  <string name="foo_942">9420</string>
+  <string name="foo_945">9450</string>
+  <string name="foo_948">9480</string>
+  <string name="foo_951">9510</string>
+  <string name="foo_954">9540</string>
+  <string name="foo_957">9570</string>
+  <string name="foo_960">9600</string>
+  <string name="foo_963">9630</string>
+  <string name="foo_966">9660</string>
+  <string name="foo_969">9690</string>
+  <string name="foo_972">9720</string>
+  <string name="foo_975">9750</string>
+  <string name="foo_978">9780</string>
+  <string name="foo_981">9810</string>
+  <string name="foo_984">9840</string>
+  <string name="foo_987">9870</string>
+  <string name="foo_990">9900</string>
+  <string name="foo_993">9930</string>
+  <string name="foo_996">9960</string>
+  <string name="foo_999">9990</string>
+  <string name="only_land">only land</string>
+</resources>
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/values.xml b/libs/androidfw/tests/data/sparse/res/values-land/values.xml
similarity index 100%
rename from libs/androidfw/tests/data/sparse/res/values-v26/values.xml
rename to libs/androidfw/tests/data/sparse/res/values-land/values.xml
diff --git a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml b/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml
deleted file mode 100644
index d116087e..0000000
--- a/libs/androidfw/tests/data/sparse/res/values-v26/strings.xml
+++ /dev/null
@@ -1,337 +0,0 @@
-<resources>
-  <string name="foo_0">0</string>
-  <string name="foo_3">30</string>
-  <string name="foo_6">60</string>
-  <string name="foo_9">90</string>
-  <string name="foo_12">120</string>
-  <string name="foo_15">150</string>
-  <string name="foo_18">180</string>
-  <string name="foo_21">210</string>
-  <string name="foo_24">240</string>
-  <string name="foo_27">270</string>
-  <string name="foo_30">300</string>
-  <string name="foo_33">330</string>
-  <string name="foo_36">360</string>
-  <string name="foo_39">390</string>
-  <string name="foo_42">420</string>
-  <string name="foo_45">450</string>
-  <string name="foo_48">480</string>
-  <string name="foo_51">510</string>
-  <string name="foo_54">540</string>
-  <string name="foo_57">570</string>
-  <string name="foo_60">600</string>
-  <string name="foo_63">630</string>
-  <string name="foo_66">660</string>
-  <string name="foo_69">690</string>
-  <string name="foo_72">720</string>
-  <string name="foo_75">750</string>
-  <string name="foo_78">780</string>
-  <string name="foo_81">810</string>
-  <string name="foo_84">840</string>
-  <string name="foo_87">870</string>
-  <string name="foo_90">900</string>
-  <string name="foo_93">930</string>
-  <string name="foo_96">960</string>
-  <string name="foo_99">990</string>
-  <string name="foo_102">1020</string>
-  <string name="foo_105">1050</string>
-  <string name="foo_108">1080</string>
-  <string name="foo_111">1110</string>
-  <string name="foo_114">1140</string>
-  <string name="foo_117">1170</string>
-  <string name="foo_120">1200</string>
-  <string name="foo_123">1230</string>
-  <string name="foo_126">1260</string>
-  <string name="foo_129">1290</string>
-  <string name="foo_132">1320</string>
-  <string name="foo_135">1350</string>
-  <string name="foo_138">1380</string>
-  <string name="foo_141">1410</string>
-  <string name="foo_144">1440</string>
-  <string name="foo_147">1470</string>
-  <string name="foo_150">1500</string>
-  <string name="foo_153">1530</string>
-  <string name="foo_156">1560</string>
-  <string name="foo_159">1590</string>
-  <string name="foo_162">1620</string>
-  <string name="foo_165">1650</string>
-  <string name="foo_168">1680</string>
-  <string name="foo_171">1710</string>
-  <string name="foo_174">1740</string>
-  <string name="foo_177">1770</string>
-  <string name="foo_180">1800</string>
-  <string name="foo_183">1830</string>
-  <string name="foo_186">1860</string>
-  <string name="foo_189">1890</string>
-  <string name="foo_192">1920</string>
-  <string name="foo_195">1950</string>
-  <string name="foo_198">1980</string>
-  <string name="foo_201">2010</string>
-  <string name="foo_204">2040</string>
-  <string name="foo_207">2070</string>
-  <string name="foo_210">2100</string>
-  <string name="foo_213">2130</string>
-  <string name="foo_216">2160</string>
-  <string name="foo_219">2190</string>
-  <string name="foo_222">2220</string>
-  <string name="foo_225">2250</string>
-  <string name="foo_228">2280</string>
-  <string name="foo_231">2310</string>
-  <string name="foo_234">2340</string>
-  <string name="foo_237">2370</string>
-  <string name="foo_240">2400</string>
-  <string name="foo_243">2430</string>
-  <string name="foo_246">2460</string>
-  <string name="foo_249">2490</string>
-  <string name="foo_252">2520</string>
-  <string name="foo_255">2550</string>
-  <string name="foo_258">2580</string>
-  <string name="foo_261">2610</string>
-  <string name="foo_264">2640</string>
-  <string name="foo_267">2670</string>
-  <string name="foo_270">2700</string>
-  <string name="foo_273">2730</string>
-  <string name="foo_276">2760</string>
-  <string name="foo_279">2790</string>
-  <string name="foo_282">2820</string>
-  <string name="foo_285">2850</string>
-  <string name="foo_288">2880</string>
-  <string name="foo_291">2910</string>
-  <string name="foo_294">2940</string>
-  <string name="foo_297">2970</string>
-  <string name="foo_300">3000</string>
-  <string name="foo_303">3030</string>
-  <string name="foo_306">3060</string>
-  <string name="foo_309">3090</string>
-  <string name="foo_312">3120</string>
-  <string name="foo_315">3150</string>
-  <string name="foo_318">3180</string>
-  <string name="foo_321">3210</string>
-  <string name="foo_324">3240</string>
-  <string name="foo_327">3270</string>
-  <string name="foo_330">3300</string>
-  <string name="foo_333">3330</string>
-  <string name="foo_336">3360</string>
-  <string name="foo_339">3390</string>
-  <string name="foo_342">3420</string>
-  <string name="foo_345">3450</string>
-  <string name="foo_348">3480</string>
-  <string name="foo_351">3510</string>
-  <string name="foo_354">3540</string>
-  <string name="foo_357">3570</string>
-  <string name="foo_360">3600</string>
-  <string name="foo_363">3630</string>
-  <string name="foo_366">3660</string>
-  <string name="foo_369">3690</string>
-  <string name="foo_372">3720</string>
-  <string name="foo_375">3750</string>
-  <string name="foo_378">3780</string>
-  <string name="foo_381">3810</string>
-  <string name="foo_384">3840</string>
-  <string name="foo_387">3870</string>
-  <string name="foo_390">3900</string>
-  <string name="foo_393">3930</string>
-  <string name="foo_396">3960</string>
-  <string name="foo_399">3990</string>
-  <string name="foo_402">4020</string>
-  <string name="foo_405">4050</string>
-  <string name="foo_408">4080</string>
-  <string name="foo_411">4110</string>
-  <string name="foo_414">4140</string>
-  <string name="foo_417">4170</string>
-  <string name="foo_420">4200</string>
-  <string name="foo_423">4230</string>
-  <string name="foo_426">4260</string>
-  <string name="foo_429">4290</string>
-  <string name="foo_432">4320</string>
-  <string name="foo_435">4350</string>
-  <string name="foo_438">4380</string>
-  <string name="foo_441">4410</string>
-  <string name="foo_444">4440</string>
-  <string name="foo_447">4470</string>
-  <string name="foo_450">4500</string>
-  <string name="foo_453">4530</string>
-  <string name="foo_456">4560</string>
-  <string name="foo_459">4590</string>
-  <string name="foo_462">4620</string>
-  <string name="foo_465">4650</string>
-  <string name="foo_468">4680</string>
-  <string name="foo_471">4710</string>
-  <string name="foo_474">4740</string>
-  <string name="foo_477">4770</string>
-  <string name="foo_480">4800</string>
-  <string name="foo_483">4830</string>
-  <string name="foo_486">4860</string>
-  <string name="foo_489">4890</string>
-  <string name="foo_492">4920</string>
-  <string name="foo_495">4950</string>
-  <string name="foo_498">4980</string>
-  <string name="foo_501">5010</string>
-  <string name="foo_504">5040</string>
-  <string name="foo_507">5070</string>
-  <string name="foo_510">5100</string>
-  <string name="foo_513">5130</string>
-  <string name="foo_516">5160</string>
-  <string name="foo_519">5190</string>
-  <string name="foo_522">5220</string>
-  <string name="foo_525">5250</string>
-  <string name="foo_528">5280</string>
-  <string name="foo_531">5310</string>
-  <string name="foo_534">5340</string>
-  <string name="foo_537">5370</string>
-  <string name="foo_540">5400</string>
-  <string name="foo_543">5430</string>
-  <string name="foo_546">5460</string>
-  <string name="foo_549">5490</string>
-  <string name="foo_552">5520</string>
-  <string name="foo_555">5550</string>
-  <string name="foo_558">5580</string>
-  <string name="foo_561">5610</string>
-  <string name="foo_564">5640</string>
-  <string name="foo_567">5670</string>
-  <string name="foo_570">5700</string>
-  <string name="foo_573">5730</string>
-  <string name="foo_576">5760</string>
-  <string name="foo_579">5790</string>
-  <string name="foo_582">5820</string>
-  <string name="foo_585">5850</string>
-  <string name="foo_588">5880</string>
-  <string name="foo_591">5910</string>
-  <string name="foo_594">5940</string>
-  <string name="foo_597">5970</string>
-  <string name="foo_600">6000</string>
-  <string name="foo_603">6030</string>
-  <string name="foo_606">6060</string>
-  <string name="foo_609">6090</string>
-  <string name="foo_612">6120</string>
-  <string name="foo_615">6150</string>
-  <string name="foo_618">6180</string>
-  <string name="foo_621">6210</string>
-  <string name="foo_624">6240</string>
-  <string name="foo_627">6270</string>
-  <string name="foo_630">6300</string>
-  <string name="foo_633">6330</string>
-  <string name="foo_636">6360</string>
-  <string name="foo_639">6390</string>
-  <string name="foo_642">6420</string>
-  <string name="foo_645">6450</string>
-  <string name="foo_648">6480</string>
-  <string name="foo_651">6510</string>
-  <string name="foo_654">6540</string>
-  <string name="foo_657">6570</string>
-  <string name="foo_660">6600</string>
-  <string name="foo_663">6630</string>
-  <string name="foo_666">6660</string>
-  <string name="foo_669">6690</string>
-  <string name="foo_672">6720</string>
-  <string name="foo_675">6750</string>
-  <string name="foo_678">6780</string>
-  <string name="foo_681">6810</string>
-  <string name="foo_684">6840</string>
-  <string name="foo_687">6870</string>
-  <string name="foo_690">6900</string>
-  <string name="foo_693">6930</string>
-  <string name="foo_696">6960</string>
-  <string name="foo_699">6990</string>
-  <string name="foo_702">7020</string>
-  <string name="foo_705">7050</string>
-  <string name="foo_708">7080</string>
-  <string name="foo_711">7110</string>
-  <string name="foo_714">7140</string>
-  <string name="foo_717">7170</string>
-  <string name="foo_720">7200</string>
-  <string name="foo_723">7230</string>
-  <string name="foo_726">7260</string>
-  <string name="foo_729">7290</string>
-  <string name="foo_732">7320</string>
-  <string name="foo_735">7350</string>
-  <string name="foo_738">7380</string>
-  <string name="foo_741">7410</string>
-  <string name="foo_744">7440</string>
-  <string name="foo_747">7470</string>
-  <string name="foo_750">7500</string>
-  <string name="foo_753">7530</string>
-  <string name="foo_756">7560</string>
-  <string name="foo_759">7590</string>
-  <string name="foo_762">7620</string>
-  <string name="foo_765">7650</string>
-  <string name="foo_768">7680</string>
-  <string name="foo_771">7710</string>
-  <string name="foo_774">7740</string>
-  <string name="foo_777">7770</string>
-  <string name="foo_780">7800</string>
-  <string name="foo_783">7830</string>
-  <string name="foo_786">7860</string>
-  <string name="foo_789">7890</string>
-  <string name="foo_792">7920</string>
-  <string name="foo_795">7950</string>
-  <string name="foo_798">7980</string>
-  <string name="foo_801">8010</string>
-  <string name="foo_804">8040</string>
-  <string name="foo_807">8070</string>
-  <string name="foo_810">8100</string>
-  <string name="foo_813">8130</string>
-  <string name="foo_816">8160</string>
-  <string name="foo_819">8190</string>
-  <string name="foo_822">8220</string>
-  <string name="foo_825">8250</string>
-  <string name="foo_828">8280</string>
-  <string name="foo_831">8310</string>
-  <string name="foo_834">8340</string>
-  <string name="foo_837">8370</string>
-  <string name="foo_840">8400</string>
-  <string name="foo_843">8430</string>
-  <string name="foo_846">8460</string>
-  <string name="foo_849">8490</string>
-  <string name="foo_852">8520</string>
-  <string name="foo_855">8550</string>
-  <string name="foo_858">8580</string>
-  <string name="foo_861">8610</string>
-  <string name="foo_864">8640</string>
-  <string name="foo_867">8670</string>
-  <string name="foo_870">8700</string>
-  <string name="foo_873">8730</string>
-  <string name="foo_876">8760</string>
-  <string name="foo_879">8790</string>
-  <string name="foo_882">8820</string>
-  <string name="foo_885">8850</string>
-  <string name="foo_888">8880</string>
-  <string name="foo_891">8910</string>
-  <string name="foo_894">8940</string>
-  <string name="foo_897">8970</string>
-  <string name="foo_900">9000</string>
-  <string name="foo_903">9030</string>
-  <string name="foo_906">9060</string>
-  <string name="foo_909">9090</string>
-  <string name="foo_912">9120</string>
-  <string name="foo_915">9150</string>
-  <string name="foo_918">9180</string>
-  <string name="foo_921">9210</string>
-  <string name="foo_924">9240</string>
-  <string name="foo_927">9270</string>
-  <string name="foo_930">9300</string>
-  <string name="foo_933">9330</string>
-  <string name="foo_936">9360</string>
-  <string name="foo_939">9390</string>
-  <string name="foo_942">9420</string>
-  <string name="foo_945">9450</string>
-  <string name="foo_948">9480</string>
-  <string name="foo_951">9510</string>
-  <string name="foo_954">9540</string>
-  <string name="foo_957">9570</string>
-  <string name="foo_960">9600</string>
-  <string name="foo_963">9630</string>
-  <string name="foo_966">9660</string>
-  <string name="foo_969">9690</string>
-  <string name="foo_972">9720</string>
-  <string name="foo_975">9750</string>
-  <string name="foo_978">9780</string>
-  <string name="foo_981">9810</string>
-  <string name="foo_984">9840</string>
-  <string name="foo_987">9870</string>
-  <string name="foo_990">9900</string>
-  <string name="foo_993">9930</string>
-  <string name="foo_996">9960</string>
-  <string name="foo_999">9990</string>
-  <string name="only_v26">only v26</string>
-</resources>
diff --git a/libs/androidfw/tests/data/sparse/sparse.apk b/libs/androidfw/tests/data/sparse/sparse.apk
index 9fd01fb..0f2d75a 100644
--- a/libs/androidfw/tests/data/sparse/sparse.apk
+++ b/libs/androidfw/tests/data/sparse/sparse.apk
Binary files differ
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 29f3773..c0a4fdf 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -636,7 +636,7 @@
 cc_defaults {
     name: "hwui_test_defaults",
     defaults: ["hwui_defaults"],
-    test_suites: ["device-tests"],
+    test_suites: ["general-tests"],
     header_libs: ["libandroid_headers_private"],
     target: {
         android: {
@@ -678,6 +678,7 @@
     srcs: [
         "tests/unit/main.cpp",
         "tests/unit/ABitmapTests.cpp",
+        "tests/unit/AutoBackendTextureReleaseTests.cpp",
         "tests/unit/CacheManagerTests.cpp",
         "tests/unit/CanvasContextTests.cpp",
         "tests/unit/CanvasOpTests.cpp",
diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/AndroidTest.xml
index 381fb9f..911315f 100644
--- a/libs/hwui/AndroidTest.xml
+++ b/libs/hwui/AndroidTest.xml
@@ -16,22 +16,22 @@
 <configuration description="Config for hwuimicro">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="hwui_unit_tests->/data/nativetest/hwui_unit_tests" />
-        <option name="push" value="hwuimicro->/data/benchmarktest/hwuimicro" />
-        <option name="push" value="hwuimacro->/data/benchmarktest/hwuimacro" />
+        <option name="push" value="hwui_unit_tests->/data/local/tmp/nativetest/hwui_unit_tests" />
+        <option name="push" value="hwuimicro->/data/local/tmp/benchmarktest/hwuimicro" />
+        <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/nativetest" />
+        <option name="native-test-device-path" value="/data/local/tmp/nativetest" />
         <option name="module-name" value="hwui_unit_tests" />
     </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/benchmarktest" />
+        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimicro" />
         <option name="file-exclusion-filter-regex" value=".*\.config$" />
     </test>
     <test class="com.android.tradefed.testtype.GoogleBenchmarkTest" >
-        <option name="native-benchmark-device-path" value="/data/benchmarktest" />
+        <option name="native-benchmark-device-path" value="/data/local/tmp/benchmarktest" />
         <option name="benchmark-module-name" value="hwuimacro" />
         <option name="file-exclusion-filter-regex" value=".*\.config$" />
     </test>
diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp
index ef5eacb..b656b6a 100644
--- a/libs/hwui/AutoBackendTextureRelease.cpp
+++ b/libs/hwui/AutoBackendTextureRelease.cpp
@@ -32,9 +32,17 @@
     bool createProtectedImage = 0 != (desc.usage & AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT);
     GrBackendFormat backendFormat =
             GrAHardwareBufferUtils::GetBackendFormat(context, buffer, desc.format, false);
+    LOG_ALWAYS_FATAL_IF(!backendFormat.isValid(),
+                        __FILE__ " Invalid GrBackendFormat. GrBackendApi==%" PRIu32
+                                 ", AHardwareBuffer_Format==%" PRIu32 ".",
+                        static_cast<int>(context->backend()), desc.format);
     mBackendTexture = GrAHardwareBufferUtils::MakeBackendTexture(
             context, buffer, desc.width, desc.height, &mDeleteProc, &mUpdateProc, &mImageCtx,
             createProtectedImage, backendFormat, false);
+    LOG_ALWAYS_FATAL_IF(!mBackendTexture.isValid(),
+                        __FILE__ " Invalid GrBackendTexture. Width==%" PRIu32 ", height==%" PRIu32
+                                 ", protected==%d",
+                        desc.width, desc.height, createProtectedImage);
 }
 
 void AutoBackendTextureRelease::unref(bool releaseImage) {
@@ -74,13 +82,13 @@
     AHardwareBuffer_Desc desc;
     AHardwareBuffer_describe(buffer, &desc);
     SkColorType colorType = GrAHardwareBufferUtils::GetSkColorTypeFromBufferFormat(desc.format);
+    // The following ref will be counteracted by Skia calling releaseProc, either during
+    // MakeFromTexture if there is a failure, or later when SkImage is discarded. It must
+    // be called before MakeFromTexture, otherwise Skia may remove HWUI's ref on failure.
+    ref();
     mImage = SkImage::MakeFromTexture(
             context, mBackendTexture, kTopLeft_GrSurfaceOrigin, colorType, kPremul_SkAlphaType,
             uirenderer::DataSpaceToColorSpace(dataspace), releaseProc, this);
-    if (mImage.get()) {
-        // The following ref will be counteracted by releaseProc, when SkImage is discarded.
-        ref();
-    }
 }
 
 void AutoBackendTextureRelease::newBufferContent(GrDirectContext* context) {
diff --git a/libs/hwui/AutoBackendTextureRelease.h b/libs/hwui/AutoBackendTextureRelease.h
index c9bb767..f0eb2a8 100644
--- a/libs/hwui/AutoBackendTextureRelease.h
+++ b/libs/hwui/AutoBackendTextureRelease.h
@@ -25,6 +25,9 @@
 namespace android {
 namespace uirenderer {
 
+// Friend TestUtils serves as a proxy for any test cases that require access to private members.
+class TestUtils;
+
 /**
  * AutoBackendTextureRelease manages EglImage/VkImage lifetime. It is a ref-counted object
  * that keeps GPU resources alive until the last SkImage object using them is destroyed.
@@ -66,6 +69,9 @@
 
     // mImage is the SkImage created from mBackendTexture.
     sk_sp<SkImage> mImage;
+
+    // Friend TestUtils serves as a proxy for any test cases that require access to private members.
+    friend class TestUtils;
 };
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index d0d24a8..673041a 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -15,19 +15,20 @@
  */
 
 #include "CanvasTransform.h"
-#include "Properties.h"
-#include "utils/Color.h"
 
+#include <SkAndroidFrameworkUtils.h>
 #include <SkColorFilter.h>
 #include <SkGradientShader.h>
+#include <SkHighContrastFilter.h>
 #include <SkPaint.h>
 #include <SkShader.h>
+#include <log/log.h>
 
 #include <algorithm>
 #include <cmath>
 
-#include <log/log.h>
-#include <SkHighContrastFilter.h>
+#include "Properties.h"
+#include "utils/Color.h"
 
 namespace android::uirenderer {
 
@@ -82,27 +83,21 @@
     paint.setColor(newColor);
 
     if (paint.getShader()) {
-        SkShader::GradientInfo info;
+        SkAndroidFrameworkUtils::LinearGradientInfo info;
         std::array<SkColor, 10> _colorStorage;
         std::array<SkScalar, _colorStorage.size()> _offsetStorage;
         info.fColorCount = _colorStorage.size();
         info.fColors = _colorStorage.data();
         info.fColorOffsets = _offsetStorage.data();
-        SkShader::GradientType type = paint.getShader()->asAGradient(&info);
 
-        if (info.fColorCount <= 10) {
-            switch (type) {
-                case SkShader::kLinear_GradientType:
-                    for (int i = 0; i < info.fColorCount; i++) {
-                        info.fColors[i] = transformColor(transform, info.fColors[i]);
-                    }
-                    paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors,
-                                                                 info.fColorOffsets, info.fColorCount,
-                                                                 info.fTileMode, info.fGradientFlags, nullptr));
-                    break;
-                default:break;
+        if (SkAndroidFrameworkUtils::ShaderAsALinearGradient(paint.getShader(), &info) &&
+            info.fColorCount <= _colorStorage.size()) {
+            for (int i = 0; i < info.fColorCount; i++) {
+                info.fColors[i] = transformColor(transform, info.fColors[i]);
             }
-
+            paint.setShader(SkGradientShader::MakeLinear(
+                    info.fPoints, info.fColors, info.fColorOffsets, info.fColorCount,
+                    info.fTileMode, info.fGradientFlags, nullptr));
         }
     }
 
diff --git a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
index f2282e66..1a47db5 100644
--- a/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
+++ b/libs/hwui/pipeline/skia/RenderNodeDrawable.cpp
@@ -229,10 +229,10 @@
     // TODO should we let the bound of the drawable do this for us?
     const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
     bool quickRejected = properties.getClipToBounds() && canvas->quickReject(bounds);
-    auto clipBounds = canvas->getLocalClipBounds();
-    SkIRect srcBounds = SkIRect::MakeWH(bounds.width(), bounds.height());
-    SkIPoint offset = SkIPoint::Make(0.0f, 0.0f);
     if (!quickRejected) {
+        auto clipBounds = canvas->getLocalClipBounds();
+        SkIRect srcBounds = SkIRect::MakeWH(bounds.width(), bounds.height());
+        SkIPoint offset = SkIPoint::Make(0.0f, 0.0f);
         SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl();
         const LayerProperties& layerProperties = properties.layerProperties();
         // composing a hardware layer
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/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 75865c7..9d5c13e 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <AutoBackendTextureRelease.h>
 #include <DisplayList.h>
 #include <Matrix.h>
 #include <Properties.h>
@@ -293,6 +294,11 @@
     static SkRect getClipBounds(const SkCanvas* canvas);
     static SkRect getLocalClipBounds(const SkCanvas* canvas);
 
+    static int getUsageCount(const AutoBackendTextureRelease* textureRelease) {
+        EXPECT_NE(nullptr, textureRelease);
+        return textureRelease->mUsageCount;
+    }
+
     struct CallCounts {
         int sync = 0;
         int contextDestroyed = 0;
diff --git a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp
new file mode 100644
index 0000000..2ec78a4
--- /dev/null
+++ b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include "AutoBackendTextureRelease.h"
+#include "tests/common/TestUtils.h"
+
+using namespace android;
+using namespace android::uirenderer;
+
+AHardwareBuffer* allocHardwareBuffer() {
+    AHardwareBuffer* buffer;
+    AHardwareBuffer_Desc desc = {
+            .width = 16,
+            .height = 16,
+            .layers = 1,
+            .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
+            .usage = AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY,
+    };
+    constexpr int kSucceeded = 0;
+    int status = AHardwareBuffer_allocate(&desc, &buffer);
+    EXPECT_EQ(kSucceeded, status);
+    return buffer;
+}
+
+// Expands to AutoBackendTextureRelease_makeImage_invalid_RenderThreadTest,
+// set as friend in AutoBackendTextureRelease.h
+RENDERTHREAD_TEST(AutoBackendTextureRelease, makeImage_invalid) {
+    AHardwareBuffer* buffer = allocHardwareBuffer();
+    AutoBackendTextureRelease* textureRelease =
+            new AutoBackendTextureRelease(renderThread.getGrContext(), buffer);
+
+    EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease));
+
+    // SkImage::MakeFromTexture should fail if given null GrDirectContext.
+    textureRelease->makeImage(buffer, HAL_DATASPACE_UNKNOWN, /*context = */ nullptr);
+
+    EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease));
+
+    textureRelease->unref(true);
+    AHardwareBuffer_release(buffer);
+}
+
+// Expands to AutoBackendTextureRelease_makeImage_valid_RenderThreadTest,
+// set as friend in AutoBackendTextureRelease.h
+RENDERTHREAD_TEST(AutoBackendTextureRelease, makeImage_valid) {
+    AHardwareBuffer* buffer = allocHardwareBuffer();
+    AutoBackendTextureRelease* textureRelease =
+            new AutoBackendTextureRelease(renderThread.getGrContext(), buffer);
+
+    EXPECT_EQ(1, TestUtils::getUsageCount(textureRelease));
+
+    textureRelease->makeImage(buffer, HAL_DATASPACE_UNKNOWN, renderThread.getGrContext());
+
+    EXPECT_EQ(2, TestUtils::getUsageCount(textureRelease));
+
+    textureRelease->unref(true);
+    AHardwareBuffer_release(buffer);
+}
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 54f893e..099efd3 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -22,10 +22,18 @@
 #include <SkBlendMode.h>
 #include <SkCanvas.h>
 #include <SkColor.h>
+#include <android-base/stringprintf.h>
 #include <android-base/thread_annotations.h>
+#include <ftl/enum.h>
+
+#include <mutex>
 
 #include "PointerControllerContext.h"
 
+#define INDENT "  "
+#define INDENT2 "    "
+#define INDENT3 "      "
+
 namespace android {
 
 namespace {
@@ -223,7 +231,7 @@
 }
 
 void PointerController::clearSpotsLocked() {
-    for (auto& [displayID, spotController] : mLocked.spotControllers) {
+    for (auto& [displayId, spotController] : mLocked.spotControllers) {
         spotController.clearSpots();
     }
 }
@@ -235,7 +243,7 @@
 void PointerController::reloadPointerResources() {
     std::scoped_lock lock(getLock());
 
-    for (auto& [displayID, spotController] : mLocked.spotControllers) {
+    for (auto& [displayId, spotController] : mLocked.spotControllers) {
         spotController.reloadSpotResources();
     }
 
@@ -286,13 +294,13 @@
 
     std::scoped_lock lock(getLock());
     for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) {
-        int32_t displayID = it->first;
-        if (!displayIdSet.count(displayID)) {
+        int32_t displayId = it->first;
+        if (!displayIdSet.count(displayId)) {
             /*
              * Ensures that an in-progress animation won't dereference
              * a null pointer to TouchSpotController.
              */
-            mContext.removeAnimationCallback(displayID);
+            mContext.removeAnimationCallback(displayId);
             it = mLocked.spotControllers.erase(it);
         } else {
             ++it;
@@ -313,4 +321,20 @@
     return it != di.end() ? it->transform : kIdentityTransform;
 }
 
+void PointerController::dump(std::string& dump) {
+    dump += INDENT "PointerController:\n";
+    std::scoped_lock lock(getLock());
+    dump += StringPrintf(INDENT2 "Presentation: %s\n",
+                         ftl::enum_string(mLocked.presentation).c_str());
+    dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId);
+    dump += StringPrintf(INDENT2 "Viewports:\n");
+    for (const auto& info : mLocked.mDisplayInfos) {
+        info.dump(dump, INDENT3);
+    }
+    dump += INDENT2 "Spot Controllers:\n";
+    for (const auto& [_, spotController] : mLocked.spotControllers) {
+        spotController.dump(dump, INDENT3);
+    }
+}
+
 } // namespace android
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 33480e8..48d5a57 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -27,6 +27,7 @@
 
 #include <map>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "MouseCursorController.h"
@@ -75,6 +76,8 @@
     void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos)
             REQUIRES(getLock());
 
+    void dump(std::string& dump);
+
 protected:
     using WindowListenerConsumer =
             std::function<void(const sp<android::gui::WindowInfosListener>&)>;
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index 4ac66c4..d9fe599 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -21,8 +21,15 @@
 
 #include "TouchSpotController.h"
 
+#include <android-base/stringprintf.h>
+#include <input/PrintTools.h>
 #include <log/log.h>
 
+#include <mutex>
+
+#define INDENT "  "
+#define INDENT2 "    "
+
 namespace {
 // Time to spend fading out the spot completely.
 const nsecs_t SPOT_FADE_DURATION = 200 * 1000000LL; // 200 ms
@@ -53,6 +60,12 @@
     }
 }
 
+void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const {
+    out += prefix;
+    base::StringAppendF(&out, "Spot{id=%" PRIx32 ", alpha=%f, scale=%f, pos=[%f, %f]}\n", id, alpha,
+                        scale, x, y);
+}
+
 // --- TouchSpotController ---
 
 TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context)
@@ -255,4 +268,22 @@
     mContext.addAnimationCallback(mDisplayId, func);
 }
 
+void TouchSpotController::dump(std::string& out, const char* prefix) const {
+    using base::StringAppendF;
+    out += prefix;
+    out += "SpotController:\n";
+    out += prefix;
+    StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId);
+    std::scoped_lock lock(mLock);
+    out += prefix;
+    StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating));
+    out += prefix;
+    out += INDENT "Spots:\n";
+    std::string spotPrefix = prefix;
+    spotPrefix += INDENT2;
+    for (const auto& spot : mLocked.displaySpots) {
+        spot->dump(out, spotPrefix.c_str());
+    }
+}
+
 } // namespace android
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 703de36..5bbc75d 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -38,6 +38,8 @@
     void reloadSpotResources();
     bool doAnimations(nsecs_t timestamp);
 
+    void dump(std::string& out, const char* prefix = "") const;
+
 private:
     struct Spot {
         static const uint32_t INVALID_ID = 0xffffffff;
@@ -58,6 +60,7 @@
                 mLastIcon(nullptr) {}
 
         void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId);
+        void dump(std::string& out, const char* prefix = "") const;
 
     private:
         const SpriteIcon* mLastIcon;
diff --git a/location/java/android/location/Country.java b/location/java/android/location/Country.java
index 8c40338..8e1bb1f0 100644
--- a/location/java/android/location/Country.java
+++ b/location/java/android/location/Country.java
@@ -16,6 +16,9 @@
 
 package android.location;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -28,7 +31,8 @@
  *
  * @hide
  */
-public class Country implements Parcelable {
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+public final class Country implements Parcelable {
     /**
      * The country code came from the mobile network
      */
@@ -78,6 +82,8 @@
      *        <li>{@link #COUNTRY_SOURCE_SIM}</li>
      *        <li>{@link #COUNTRY_SOURCE_LOCALE}</li>
      *        </ul>
+     *
+     * @hide
      */
     @UnsupportedAppUsage
     public Country(final String countryIso, final int source) {
@@ -100,6 +106,7 @@
         mTimestamp = timestamp;
     }
 
+    /** @hide */
     public Country(Country country) {
         mCountryIso = country.mCountryIso;
         mSource = country.mSource;
@@ -109,8 +116,8 @@
     /**
      * @return the ISO 3166-1 two letters country code
      */
-    @UnsupportedAppUsage
-    public final String getCountryIso() {
+    @NonNull
+    public String getCountryIso() {
         return mCountryIso;
     }
 
@@ -124,20 +131,22 @@
      *         <li>{@link #COUNTRY_SOURCE_LOCALE}</li>
      *         </ul>
      */
-    @UnsupportedAppUsage
-    public final int getSource() {
+    public int getSource() {
         return mSource;
     }
 
     /**
      * Returns the time that this object was created (which we assume to be the time that the source
      * was consulted).
+     *
+     * @hide
      */
-    public final long getTimestamp() {
+    public long getTimestamp() {
         return mTimestamp;
     }
 
-    public static final @android.annotation.NonNull Parcelable.Creator<Country> CREATOR = new Parcelable.Creator<Country>() {
+    @android.annotation.NonNull
+    public static final Parcelable.Creator<Country> CREATOR = new Parcelable.Creator<Country>() {
         public Country createFromParcel(Parcel in) {
             return new Country(in.readString(), in.readInt(), in.readLong());
         }
@@ -147,11 +156,13 @@
         }
     };
 
+    @Override
     public int describeContents() {
         return 0;
     }
 
-    public void writeToParcel(Parcel parcel, int flags) {
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
         parcel.writeString(mCountryIso);
         parcel.writeInt(mSource);
         parcel.writeLong(mTimestamp);
@@ -161,9 +172,10 @@
      * Returns true if this {@link Country} is equivalent to the given object. This ignores
      * the timestamp value and just checks for equivalence of countryIso and source values.
      * Returns false otherwise.
+     *
      */
     @Override
-    public boolean equals(Object object) {
+    public boolean equals(@Nullable Object object) {
         if (object == this) {
             return true;
         }
@@ -194,12 +206,15 @@
      * @param country the country to compare
      * @return true if the specified country's countryIso field is equal to this
      *         country's, false otherwise.
+     *
+     * @hide
      */
     public boolean equalsIgnoreSource(Country country) {
         return country != null && mCountryIso.equals(country.getCountryIso());
     }
 
     @Override
+    @NonNull
     public String toString() {
         return "Country {ISO=" + mCountryIso + ", source=" + mSource + ", time=" + mTimestamp + "}";
     }
diff --git a/location/java/android/location/CountryDetector.java b/location/java/android/location/CountryDetector.java
index e344b82..3a0edfc 100644
--- a/location/java/android/location/CountryDetector.java
+++ b/location/java/android/location/CountryDetector.java
@@ -16,6 +16,9 @@
 
 package android.location;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
@@ -47,6 +50,7 @@
  *
  * @hide
  */
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
 @SystemService(Context.COUNTRY_DETECTOR)
 public class CountryDetector {
 
@@ -101,7 +105,7 @@
      * @return the country if it is available immediately, otherwise null will
      *         be returned.
      */
-    @UnsupportedAppUsage
+    @Nullable
     public Country detectCountry() {
         try {
             return mService.detectCountry();
@@ -120,8 +124,7 @@
      *        implement the callback mechanism. If looper is null then the
      *        callbacks will be called on the main thread.
      */
-    @UnsupportedAppUsage
-    public void addCountryListener(CountryListener listener, Looper looper) {
+    public void addCountryListener(@NonNull CountryListener listener, @Nullable Looper looper) {
         synchronized (mListeners) {
             if (!mListeners.containsKey(listener)) {
                 ListenerTransport transport = new ListenerTransport(listener, looper);
@@ -138,8 +141,7 @@
     /**
      * Remove the listener
      */
-    @UnsupportedAppUsage
-    public void removeCountryListener(CountryListener listener) {
+    public void removeCountryListener(@NonNull CountryListener listener) {
         synchronized (mListeners) {
             ListenerTransport transport = mListeners.get(listener);
             if (transport != null) {
diff --git a/location/java/android/location/CountryListener.java b/location/java/android/location/CountryListener.java
index eb67205..5c06d82 100644
--- a/location/java/android/location/CountryListener.java
+++ b/location/java/android/location/CountryListener.java
@@ -16,7 +16,8 @@
 
 package android.location;
 
-import android.compat.annotation.UnsupportedAppUsage;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
 
 /**
  * The listener for receiving the notification when the country is detected or
@@ -24,10 +25,11 @@
  *
  * @hide
  */
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
 public interface CountryListener {
     /**
      * @param country the changed or detected country.
+     *
      */
-    @UnsupportedAppUsage
-    void onCountryDetected(Country country);
+    void onCountryDetected(@NonNull Country country);
 }
diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java
index 7a412a0..a6da0a3 100644
--- a/location/java/android/location/GnssCapabilities.java
+++ b/location/java/android/location/GnssCapabilities.java
@@ -24,6 +24,9 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -123,20 +126,24 @@
      * @hide
      */
     public static GnssCapabilities empty() {
-        return new GnssCapabilities(0, 0, 0);
+        return new GnssCapabilities(0, 0, 0, Collections.emptyList());
     }
 
     private final @TopHalCapabilityFlags int mTopFlags;
     private final @SubHalMeasurementCorrectionsCapabilityFlags int mMeasurementCorrectionsFlags;
     private final @SubHalPowerCapabilityFlags int mPowerFlags;
+    private final @NonNull List<GnssSignalType> mGnssSignalTypes;
 
     private GnssCapabilities(
             @TopHalCapabilityFlags int topFlags,
             @SubHalMeasurementCorrectionsCapabilityFlags int measurementCorrectionsFlags,
-            @SubHalPowerCapabilityFlags int powerFlags) {
+            @SubHalPowerCapabilityFlags int powerFlags,
+            @NonNull List<GnssSignalType> gnssSignalTypes) {
+        Objects.requireNonNull(gnssSignalTypes);
         mTopFlags = topFlags;
         mMeasurementCorrectionsFlags = measurementCorrectionsFlags;
         mPowerFlags = powerFlags;
+        mGnssSignalTypes = Collections.unmodifiableList(gnssSignalTypes);
     }
 
     /**
@@ -148,7 +155,8 @@
         if (mTopFlags == flags) {
             return this;
         } else {
-            return new GnssCapabilities(flags, mMeasurementCorrectionsFlags, mPowerFlags);
+            return new GnssCapabilities(flags, mMeasurementCorrectionsFlags, mPowerFlags,
+                    mGnssSignalTypes);
         }
     }
 
@@ -163,7 +171,8 @@
         if (mMeasurementCorrectionsFlags == flags) {
             return this;
         } else {
-            return new GnssCapabilities(mTopFlags, flags, mPowerFlags);
+            return new GnssCapabilities(mTopFlags, flags, mPowerFlags,
+                    mGnssSignalTypes);
         }
     }
 
@@ -177,7 +186,23 @@
         if (mPowerFlags == flags) {
             return this;
         } else {
-            return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, flags);
+            return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, flags,
+                    mGnssSignalTypes);
+        }
+    }
+
+    /**
+     * Returns a new GnssCapabilities object with a list of GnssSignalType.
+     *
+     * @hide
+     */
+    public GnssCapabilities withSignalTypes(@NonNull List<GnssSignalType> gnssSignalTypes) {
+        Objects.requireNonNull(gnssSignalTypes);
+        if (mGnssSignalTypes.equals(gnssSignalTypes)) {
+            return this;
+        } else {
+            return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, mPowerFlags,
+                    new ArrayList<>(gnssSignalTypes));
         }
     }
 
@@ -207,7 +232,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;
     }
 
@@ -424,6 +449,14 @@
         return (mPowerFlags & SUB_HAL_POWER_CAPABILITY_OTHER_MODES) != 0;
     }
 
+    /**
+     * Returns the list of {@link GnssSignalType}s that the GNSS chipset supports.
+     */
+    @NonNull
+    public List<GnssSignalType> getGnssSignalTypes() {
+        return mGnssSignalTypes;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -436,19 +469,21 @@
         GnssCapabilities that = (GnssCapabilities) o;
         return mTopFlags == that.mTopFlags
                 && mMeasurementCorrectionsFlags == that.mMeasurementCorrectionsFlags
-                && mPowerFlags == that.mPowerFlags;
+                && mPowerFlags == that.mPowerFlags
+                && mGnssSignalTypes.equals(that.mGnssSignalTypes);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mTopFlags, mMeasurementCorrectionsFlags, mPowerFlags);
+        return Objects.hash(mTopFlags, mMeasurementCorrectionsFlags, mPowerFlags, mGnssSignalTypes);
     }
 
     public static final @NonNull Creator<GnssCapabilities> CREATOR =
             new Creator<GnssCapabilities>() {
                 @Override
                 public GnssCapabilities createFromParcel(Parcel in) {
-                    return new GnssCapabilities(in.readInt(), in.readInt(), in.readInt());
+                    return new GnssCapabilities(in.readInt(), in.readInt(), in.readInt(),
+                            in.createTypedArrayList(GnssSignalType.CREATOR));
                 }
 
                 @Override
@@ -467,6 +502,7 @@
         parcel.writeInt(mTopFlags);
         parcel.writeInt(mMeasurementCorrectionsFlags);
         parcel.writeInt(mPowerFlags);
+        parcel.writeTypedList(mGnssSignalTypes);
     }
 
     @Override
@@ -482,7 +518,7 @@
         if (hasMsa()) {
             builder.append("MSA ");
         }
-        if (hasSingleShot()) {
+        if (hasSingleShotFix()) {
             builder.append("SINGLE_SHOT ");
         }
         if (hasOnDemandTime()) {
@@ -545,6 +581,9 @@
         if (hasPowerOtherModes()) {
             builder.append("OTHER_MODES_POWER ");
         }
+        if (!mGnssSignalTypes.isEmpty()) {
+            builder.append("signalTypes=").append(mGnssSignalTypes).append(" ");
+        }
         if (builder.length() > 1) {
             builder.setLength(builder.length() - 1);
         } else {
@@ -562,17 +601,20 @@
         private @TopHalCapabilityFlags int mTopFlags;
         private @SubHalMeasurementCorrectionsCapabilityFlags int mMeasurementCorrectionsFlags;
         private @SubHalPowerCapabilityFlags int mPowerFlags;
+        private @NonNull List<GnssSignalType> mGnssSignalTypes;
 
         public Builder() {
             mTopFlags = 0;
             mMeasurementCorrectionsFlags = 0;
             mPowerFlags = 0;
+            mGnssSignalTypes = Collections.emptyList();
         }
 
         public Builder(@NonNull GnssCapabilities capabilities) {
             mTopFlags = capabilities.mTopFlags;
             mMeasurementCorrectionsFlags = capabilities.mMeasurementCorrectionsFlags;
             mPowerFlags = capabilities.mPowerFlags;
+            mGnssSignalTypes = capabilities.mGnssSignalTypes;
         }
 
         /**
@@ -602,7 +644,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;
         }
@@ -779,10 +821,19 @@
         }
 
         /**
+         * Sets a list of {@link GnssSignalType}.
+         */
+        public @NonNull Builder setGnssSignalTypes(@NonNull List<GnssSignalType> gnssSignalTypes) {
+            mGnssSignalTypes = gnssSignalTypes;
+            return this;
+        }
+
+        /**
          * Builds a new GnssCapabilities.
          */
         public @NonNull GnssCapabilities build() {
-            return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, mPowerFlags);
+            return new GnssCapabilities(mTopFlags, mMeasurementCorrectionsFlags, mPowerFlags,
+                    new ArrayList<>(mGnssSignalTypes));
         }
 
         private static int setFlag(int value, int flag, boolean set) {
diff --git a/location/java/android/location/GnssSignalType.aidl b/location/java/android/location/GnssSignalType.aidl
new file mode 100644
index 0000000..1c43fe5
--- /dev/null
+++ b/location/java/android/location/GnssSignalType.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.location;
+
+parcelable GnssSignalType;
diff --git a/location/java/android/location/GnssSignalType.java b/location/java/android/location/GnssSignalType.java
new file mode 100644
index 0000000..16c3f2e
--- /dev/null
+++ b/location/java/android/location/GnssSignalType.java
@@ -0,0 +1,152 @@
+/*
+ * 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.location;
+
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * This class represents a GNSS signal type.
+ */
+public final class GnssSignalType implements Parcelable {
+
+    /**
+     * Creates a {@link GnssSignalType} with a full list of parameters.
+     *
+     * @param constellationType the constellation type as defined in
+     * {@link GnssStatus.ConstellationType}
+     * @param carrierFrequencyHz the carrier frequency in Hz
+     * @param codeType the code type as defined in {@link GnssMeasurement#getCodeType()}
+     */
+    @NonNull
+    public static GnssSignalType create(@GnssStatus.ConstellationType int constellationType,
+            @FloatRange(from = 0.0f, fromInclusive = false) double carrierFrequencyHz,
+            @NonNull String codeType) {
+        Preconditions.checkArgument(carrierFrequencyHz > 0,
+                "carrierFrequencyHz must be greater than 0.");
+        Objects.requireNonNull(codeType);
+        return new GnssSignalType(constellationType, carrierFrequencyHz, codeType);
+    }
+
+    @GnssStatus.ConstellationType
+    private final int mConstellationType;
+    @FloatRange(from = 0.0f, fromInclusive = false)
+    private final double mCarrierFrequencyHz;
+    @NonNull
+    private final String mCodeType;
+
+    /**
+     * Creates a {@link GnssSignalType} with a full list of parameters.
+     */
+    private GnssSignalType(@GnssStatus.ConstellationType int constellationType,
+            double carrierFrequencyHz, @NonNull String codeType) {
+        this.mConstellationType = constellationType;
+        this.mCarrierFrequencyHz = carrierFrequencyHz;
+        this.mCodeType = codeType;
+    }
+
+    /** Returns the {@link GnssStatus.ConstellationType}. */
+    @GnssStatus.ConstellationType
+    public int getConstellationType() {
+        return mConstellationType;
+    }
+
+    /** Returns the carrier frequency in Hz. */
+    @FloatRange(from = 0.0f, fromInclusive = false)
+    public double getCarrierFrequencyHz() {
+        return mCarrierFrequencyHz;
+    }
+
+    /**
+     * Return the code type.
+     *
+     * @see GnssMeasurement#getCodeType()
+     */
+    @NonNull
+    public String getCodeType() {
+        return mCodeType;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<GnssSignalType> CREATOR =
+            new Parcelable.Creator<GnssSignalType>() {
+                @Override
+                @NonNull
+                public GnssSignalType createFromParcel(@NonNull Parcel parcel) {
+                    return new GnssSignalType(parcel.readInt(), parcel.readDouble(),
+                            parcel.readString());
+                }
+
+                @Override
+                public GnssSignalType[] newArray(int i) {
+                    return new GnssSignalType[i];
+                }
+            };
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mConstellationType);
+        parcel.writeDouble(mCarrierFrequencyHz);
+        parcel.writeString(mCodeType);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuilder s = new StringBuilder();
+        s.append("GnssSignalType[");
+        s.append("Constellation=").append(mConstellationType);
+        s.append(", CarrierFrequencyHz=").append(mCarrierFrequencyHz);
+        s.append(", CodeType=").append(mCodeType);
+        s.append(']');
+        return s.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof GnssSignalType) {
+            GnssSignalType other = (GnssSignalType) obj;
+            return mConstellationType == other.mConstellationType
+                    && Double.compare(mCarrierFrequencyHz, other.mCarrierFrequencyHz) == 0
+                    && mCodeType.equals(other.mCodeType);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mConstellationType, mCarrierFrequencyHz, mCodeType);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
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/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2547a963..17d7045 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2328,10 +2328,9 @@
         return AudioSystem.SUCCESS;
     }
 
-    private final Map<Integer, Object> mDevRoleForCapturePresetListeners = new HashMap<>(){{
-            put(AudioSystem.DEVICE_ROLE_PREFERRED,
-                    new DevRoleListeners<OnPreferredDevicesForCapturePresetChangedListener>());
-        }};
+    private final Map<Integer, Object> mDevRoleForCapturePresetListeners = Map.of(
+            AudioSystem.DEVICE_ROLE_PREFERRED,
+            new DevRoleListeners<OnPreferredDevicesForCapturePresetChangedListener>());
 
     private class DevRoleListenerInfo<T> {
         final @NonNull Executor mExecutor;
@@ -6515,15 +6514,17 @@
     // AudioPort implementation
     //
 
-    static final int AUDIOPORT_GENERATION_INIT = 0;
-    static Integer sAudioPortGeneration = new Integer(AUDIOPORT_GENERATION_INIT);
-    static ArrayList<AudioPort> sAudioPortsCached = new ArrayList<AudioPort>();
-    static ArrayList<AudioPort> sPreviousAudioPortsCached = new ArrayList<AudioPort>();
-    static ArrayList<AudioPatch> sAudioPatchesCached = new ArrayList<AudioPatch>();
+    private static final int AUDIOPORT_GENERATION_INIT = 0;
+    private static Object sAudioPortGenerationLock = new Object();
+    @GuardedBy("sAudioPortGenerationLock")
+    private static int sAudioPortGeneration = AUDIOPORT_GENERATION_INIT;
+    private static ArrayList<AudioPort> sAudioPortsCached = new ArrayList<AudioPort>();
+    private static ArrayList<AudioPort> sPreviousAudioPortsCached = new ArrayList<AudioPort>();
+    private static ArrayList<AudioPatch> sAudioPatchesCached = new ArrayList<AudioPatch>();
 
     static int resetAudioPortGeneration() {
         int generation;
-        synchronized (sAudioPortGeneration) {
+        synchronized (sAudioPortGenerationLock) {
             generation = sAudioPortGeneration;
             sAudioPortGeneration = AUDIOPORT_GENERATION_INIT;
         }
@@ -6533,7 +6534,7 @@
     static int updateAudioPortCache(ArrayList<AudioPort> ports, ArrayList<AudioPatch> patches,
                                     ArrayList<AudioPort> previousPorts) {
         sAudioPortEventHandler.init();
-        synchronized (sAudioPortGeneration) {
+        synchronized (sAudioPortGenerationLock) {
 
             if (sAudioPortGeneration == AUDIOPORT_GENERATION_INIT) {
                 int[] patchGeneration = new int[1];
@@ -6632,8 +6633,8 @@
             }
         }
         if (k == ports.size()) {
-            // this hould never happen
-            Log.e(TAG, "updatePortConfig port not found for handle: "+port.handle().id());
+            // This can happen in case of stale audio patch referring to a removed device and is
+            // handled by the caller.
             return null;
         }
         AudioGainConfig gainCfg = portCfg.gain();
diff --git a/media/java/android/media/AudioMetadata.java b/media/java/android/media/AudioMetadata.java
index ca175b4..0f962f9 100644
--- a/media/java/android/media/AudioMetadata.java
+++ b/media/java/android/media/AudioMetadata.java
@@ -30,6 +30,7 @@
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
@@ -446,14 +447,13 @@
     // BaseMap is corresponding to audio_utils::metadata::Data
     private static final int AUDIO_METADATA_OBJ_TYPE_BASEMAP = 6;
 
-    private static final HashMap<Class, Integer> AUDIO_METADATA_OBJ_TYPES = new HashMap<>() {{
-            put(Integer.class, AUDIO_METADATA_OBJ_TYPE_INT);
-            put(Long.class, AUDIO_METADATA_OBJ_TYPE_LONG);
-            put(Float.class, AUDIO_METADATA_OBJ_TYPE_FLOAT);
-            put(Double.class, AUDIO_METADATA_OBJ_TYPE_DOUBLE);
-            put(String.class, AUDIO_METADATA_OBJ_TYPE_STRING);
-            put(BaseMap.class, AUDIO_METADATA_OBJ_TYPE_BASEMAP);
-        }};
+    private static final Map<Class, Integer> AUDIO_METADATA_OBJ_TYPES = Map.of(
+            Integer.class, AUDIO_METADATA_OBJ_TYPE_INT,
+            Long.class, AUDIO_METADATA_OBJ_TYPE_LONG,
+            Float.class, AUDIO_METADATA_OBJ_TYPE_FLOAT,
+            Double.class, AUDIO_METADATA_OBJ_TYPE_DOUBLE,
+            String.class, AUDIO_METADATA_OBJ_TYPE_STRING,
+            BaseMap.class, AUDIO_METADATA_OBJ_TYPE_BASEMAP);
 
     private static final Charset AUDIO_METADATA_CHARSET = StandardCharsets.UTF_8;
 
@@ -634,8 +634,8 @@
      *     Datum corresponds to Object
      ****************************************************************************************/
 
-    private static final HashMap<Integer, DataPackage<?>> DATA_PACKAGES = new HashMap<>() {{
-            put(AUDIO_METADATA_OBJ_TYPE_INT, new DataPackage<Integer>() {
+    private static final Map<Integer, DataPackage<?>> DATA_PACKAGES = Map.of(
+            AUDIO_METADATA_OBJ_TYPE_INT, new DataPackage<Integer>() {
                 @Override
                 @Nullable
                 public Integer unpack(ByteBuffer buffer) {
@@ -647,8 +647,8 @@
                     output.putInt(obj);
                     return true;
                 }
-            });
-            put(AUDIO_METADATA_OBJ_TYPE_LONG, new DataPackage<Long>() {
+            },
+            AUDIO_METADATA_OBJ_TYPE_LONG, new DataPackage<Long>() {
                 @Override
                 @Nullable
                 public Long unpack(ByteBuffer buffer) {
@@ -660,8 +660,8 @@
                     output.putLong(obj);
                     return true;
                 }
-            });
-            put(AUDIO_METADATA_OBJ_TYPE_FLOAT, new DataPackage<Float>() {
+            },
+            AUDIO_METADATA_OBJ_TYPE_FLOAT, new DataPackage<Float>() {
                 @Override
                 @Nullable
                 public Float unpack(ByteBuffer buffer) {
@@ -673,8 +673,8 @@
                     output.putFloat(obj);
                     return true;
                 }
-            });
-            put(AUDIO_METADATA_OBJ_TYPE_DOUBLE, new DataPackage<Double>() {
+            },
+            AUDIO_METADATA_OBJ_TYPE_DOUBLE, new DataPackage<Double>() {
                 @Override
                 @Nullable
                 public Double unpack(ByteBuffer buffer) {
@@ -686,8 +686,8 @@
                     output.putDouble(obj);
                     return true;
                 }
-            });
-            put(AUDIO_METADATA_OBJ_TYPE_STRING, new DataPackage<String>() {
+            },
+            AUDIO_METADATA_OBJ_TYPE_STRING, new DataPackage<String>() {
                 @Override
                 @Nullable
                 public String unpack(ByteBuffer buffer) {
@@ -713,9 +713,9 @@
                     output.put(valueArr);
                     return true;
                 }
-            });
-            put(AUDIO_METADATA_OBJ_TYPE_BASEMAP, new BaseMapPackage());
-        }};
+            },
+            AUDIO_METADATA_OBJ_TYPE_BASEMAP, new BaseMapPackage());
+
     // ObjectPackage is a special case that it is expected to unpack audio_utils::metadata::Datum,
     // which contains data type and data size besides the payload for the data.
     private static final ObjectPackage OBJECT_PACKAGE = new ObjectPackage();
diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java
index 819358b..980f63b 100644
--- a/media/java/android/media/AudioPlaybackConfiguration.java
+++ b/media/java/android/media/AudioPlaybackConfiguration.java
@@ -23,6 +23,7 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.os.Binder;
 import android.os.IBinder;
@@ -220,46 +221,59 @@
 
     /**
      * @hide
-     * Mute state used for anonymization.
+     * Mute state used for the initial state and when API is accessed without permission.
      */
-    public static final int PLAYER_MUTE_INVALID = -1;
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_UNKNOWN = -1;
     /**
      * @hide
      * Flag used when muted by master volume.
      */
-    public static final int PLAYER_MUTE_MASTER = (1 << 0);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_MASTER = (1 << 0);
     /**
      * @hide
      * Flag used when muted by stream volume.
      */
-    public static final int PLAYER_MUTE_STREAM_VOLUME = (1 << 1);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_STREAM_VOLUME = (1 << 1);
     /**
      * @hide
      * Flag used when muted by stream mute.
      */
-    public static final int PLAYER_MUTE_STREAM_MUTED = (1 << 2);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_STREAM_MUTED = (1 << 2);
     /**
      * @hide
-     * Flag used when playback is restricted by AppOps manager with OP_PLAY_AUDIO.
+     * Flag used when playback is muted by AppOpsManager#OP_PLAY_AUDIO.
      */
-    public static final int PLAYER_MUTE_PLAYBACK_RESTRICTED = (1 << 3);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_APP_OPS = (1 << 3);
     /**
      * @hide
      * Flag used when muted by client volume.
      */
-    public static final int PLAYER_MUTE_CLIENT_VOLUME = (1 << 4);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_CLIENT_VOLUME = (1 << 4);
     /**
      * @hide
      * Flag used when muted by volume shaper.
      */
-    public static final int PLAYER_MUTE_VOLUME_SHAPER = (1 << 5);
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public static final int MUTED_BY_VOLUME_SHAPER = (1 << 5);
 
     /** @hide */
     @IntDef(
             flag = true,
-            value = {PLAYER_MUTE_MASTER, PLAYER_MUTE_STREAM_VOLUME, PLAYER_MUTE_STREAM_MUTED,
-                    PLAYER_MUTE_PLAYBACK_RESTRICTED, PLAYER_MUTE_CLIENT_VOLUME,
-                    PLAYER_MUTE_VOLUME_SHAPER})
+            value = {MUTED_BY_MASTER, MUTED_BY_STREAM_VOLUME, MUTED_BY_STREAM_MUTED,
+                    MUTED_BY_APP_OPS, MUTED_BY_CLIENT_VOLUME, MUTED_BY_VOLUME_SHAPER})
     @Retention(RetentionPolicy.SOURCE)
     public @interface PlayerMuteEvent {
     }
@@ -303,7 +317,7 @@
         mPlayerType = pic.mPlayerType;
         mClientUid = uid;
         mClientPid = pid;
-        mMutedState = PLAYER_MUTE_INVALID;
+        mMutedState = MUTED_BY_UNKNOWN;
         mDeviceId = PLAYER_DEVICEID_INVALID;
         mPlayerState = PLAYER_STATE_IDLE;
         mPlayerAttr = pic.mAttributes;
@@ -352,7 +366,7 @@
         anonymCopy.mPlayerAttr = builder.build();
         anonymCopy.mDeviceId = in.mDeviceId;
         // anonymized data
-        anonymCopy.mMutedState = PLAYER_MUTE_INVALID;
+        anonymCopy.mMutedState = MUTED_BY_UNKNOWN;
         anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN;
         anonymCopy.mClientUid = PLAYER_UPID_INVALID;
         anonymCopy.mClientPid = PLAYER_UPID_INVALID;
@@ -413,9 +427,27 @@
 
     /**
      * @hide
-     * @return the mute state as a combination of {@link PlayerMuteEvent} flags
+     * Used for determining if the current player is muted.
+     * <br>Note that if this result is true then {@link #getMutedBy} will be > 0.
+     * @return {@code true} if the player associated with this configuration has been muted (by any
+     * given MUTED_BY_* source event) or {@code false} otherwise.
      */
-    @PlayerMuteEvent public int getMutedState() {
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public boolean isMuted() {
+        return mMutedState != 0 && mMutedState != MUTED_BY_UNKNOWN;
+    }
+
+    /**
+     * @hide
+     * Returns a bitmask expressing the mute state as a combination of MUTED_BY_* flags.
+     * <br>Note that if the mute state is not set the result will be {@link #MUTED_BY_UNKNOWN}. A
+     * value of 0 represents a player which is not muted.
+     * @return the mute state.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    @PlayerMuteEvent public int getMutedBy() {
         return mMutedState;
     }
 
@@ -569,6 +601,17 @@
         }
     }
 
+    private boolean isMuteAffectingActiveState() {
+        if (mMutedState == MUTED_BY_UNKNOWN) {
+            // mute state is not set, therefore it will not affect the active state
+            return false;
+        }
+
+        return (mMutedState & MUTED_BY_CLIENT_VOLUME) != 0
+                || (mMutedState & MUTED_BY_VOLUME_SHAPER) != 0
+                || (mMutedState & MUTED_BY_APP_OPS) != 0;
+    }
+
     /**
      * @hide
      * Returns true if the player is considered "active", i.e. actively playing with unmuted
@@ -580,8 +623,7 @@
     public boolean isActive() {
         switch (mPlayerState) {
             case PLAYER_STATE_STARTED:
-                return mMutedState == 0
-                        || mMutedState == PLAYER_MUTE_INVALID;  // only send true if not muted
+                return !isMuteAffectingActiveState();
             case PLAYER_STATE_UNKNOWN:
             case PLAYER_STATE_RELEASED:
             case PLAYER_STATE_IDLE:
@@ -684,27 +726,27 @@
                 "/").append(mClientPid).append(" state:").append(
                 toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(mPlayerAttr).append(
                 " sessionId:").append(mSessionId).append(" mutedState:");
-        if (mMutedState == PLAYER_MUTE_INVALID) {
-            apcToString.append("invalid ");
+        if (mMutedState == MUTED_BY_UNKNOWN) {
+            apcToString.append("unknown ");
         } else if (mMutedState == 0) {
             apcToString.append("none ");
         } else {
-            if ((mMutedState & PLAYER_MUTE_MASTER) != 0) {
+            if ((mMutedState & MUTED_BY_MASTER) != 0) {
                 apcToString.append("master ");
             }
-            if ((mMutedState & PLAYER_MUTE_STREAM_VOLUME) != 0) {
+            if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
                 apcToString.append("streamVolume ");
             }
-            if ((mMutedState & PLAYER_MUTE_STREAM_MUTED) != 0) {
+            if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
                 apcToString.append("streamMute ");
             }
-            if ((mMutedState & PLAYER_MUTE_PLAYBACK_RESTRICTED) != 0) {
-                apcToString.append("playbackRestricted ");
+            if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
+                apcToString.append("appOps ");
             }
-            if ((mMutedState & PLAYER_MUTE_CLIENT_VOLUME) != 0) {
+            if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
                 apcToString.append("clientVolume ");
             }
-            if ((mMutedState & PLAYER_MUTE_VOLUME_SHAPER) != 0) {
+            if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
                 apcToString.append("volumeShaper ");
             }
         }
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 6764890..ad6acea 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -438,6 +438,18 @@
                 return "AUDIO_FORMAT_APTX_TWSP";
             case /* AUDIO_FORMAT_LC3             */ 0x2B000000:
                 return "AUDIO_FORMAT_LC3";
+            case /* AUDIO_FORMAT_MPEGH           */ 0x2C000000:
+                return "AUDIO_FORMAT_MPEGH";
+            case /* AUDIO_FORMAT_IEC60958        */ 0x2D000000:
+                return "AUDIO_FORMAT_IEC60958";
+            case /* AUDIO_FORMAT_DTS_UHD         */ 0x2E000000:
+                return "AUDIO_FORMAT_DTS_UHD";
+            case /* AUDIO_FORMAT_DRA             */ 0x2F000000:
+                return "AUDIO_FORMAT_DRA";
+            case /* AUDIO_FORMAT_APTX_ADAPTIVE_QLEA */ 0x30000000:
+                return "AUDIO_FORMAT_APTX_ADAPTIVE_QLEA";
+            case /* AUDIO_FORMAT_APTX_ADAPTIVE_R4   */ 0x31000000:
+                return "AUDIO_FORMAT_APTX_ADAPTIVE_R4";
 
             /* Aliases */
             case /* AUDIO_FORMAT_PCM_16_BIT        */ 0x1:
@@ -510,10 +522,14 @@
                 return "AUDIO_FORMAT_MAT_2_0"; // (MAT | MAT_SUB_2_0)
             case /* AUDIO_FORMAT_MAT_2_1           */ 0x24000003:
                 return "AUDIO_FORMAT_MAT_2_1"; // (MAT | MAT_SUB_2_1)
-            case /* AUDIO_FORMAT_DTS_UHD */           0x2E000000:
-                return "AUDIO_FORMAT_DTS_UHD";
-            case /* AUDIO_FORMAT_DRA */           0x2F000000:
-                return "AUDIO_FORMAT_DRA";
+            case /* AUDIO_FORMAT_MPEGH_SUB_BL_L3   */ 0x2C000013:
+                return "AUDIO_FORMAT_MPEGH_SUB_BL_L3";
+            case /* AUDIO_FORMAT_MPEGH_SUB_BL_L4   */ 0x2C000014:
+                return "AUDIO_FORMAT_MPEGH_SUB_BL_L4";
+            case /* AUDIO_FORMAT_MPEGH_SUB_LC_L3   */ 0x2C000023:
+                return "AUDIO_FORMAT_MPEGH_SUB_LC_L3";
+            case /* AUDIO_FORMAT_MPEGH_SUB_LC_L4   */ 0x2C000024:
+                return "AUDIO_FORMAT_MPEGH_SUB_LC_L4";
             default:
                 return "AUDIO_FORMAT_(" + audioFormat + ")";
         }
@@ -2407,4 +2423,3 @@
      */
     final static int NATIVE_EVENT_ROUTING_CHANGE = 1000;
 }
-
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 85cd342..d51f1e1 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -48,8 +48,8 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.NioUtils;
-import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -1867,26 +1867,24 @@
     }
 
     // General pair map
-    private static final HashMap<String, Integer> CHANNEL_PAIR_MAP = new HashMap<>() {{
-        put("front", AudioFormat.CHANNEL_OUT_FRONT_LEFT
-                | AudioFormat.CHANNEL_OUT_FRONT_RIGHT);
-        put("back", AudioFormat.CHANNEL_OUT_BACK_LEFT
-                | AudioFormat.CHANNEL_OUT_BACK_RIGHT);
-        put("front of center", AudioFormat.CHANNEL_OUT_FRONT_LEFT_OF_CENTER
-                | AudioFormat.CHANNEL_OUT_FRONT_RIGHT_OF_CENTER);
-        put("side", AudioFormat.CHANNEL_OUT_SIDE_LEFT
-                | AudioFormat.CHANNEL_OUT_SIDE_RIGHT);
-        put("top front", AudioFormat.CHANNEL_OUT_TOP_FRONT_LEFT
-                | AudioFormat.CHANNEL_OUT_TOP_FRONT_RIGHT);
-        put("top back", AudioFormat.CHANNEL_OUT_TOP_BACK_LEFT
-                | AudioFormat.CHANNEL_OUT_TOP_BACK_RIGHT);
-        put("top side", AudioFormat.CHANNEL_OUT_TOP_SIDE_LEFT
-                | AudioFormat.CHANNEL_OUT_TOP_SIDE_RIGHT);
-        put("bottom front", AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT
-                | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT);
-        put("front wide", AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT
-                | AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT);
-    }};
+    private static final Map<String, Integer> CHANNEL_PAIR_MAP = Map.of(
+            "front", AudioFormat.CHANNEL_OUT_FRONT_LEFT
+                    | AudioFormat.CHANNEL_OUT_FRONT_RIGHT,
+            "back", AudioFormat.CHANNEL_OUT_BACK_LEFT
+                    | AudioFormat.CHANNEL_OUT_BACK_RIGHT,
+            "front of center", AudioFormat.CHANNEL_OUT_FRONT_LEFT_OF_CENTER
+                    | AudioFormat.CHANNEL_OUT_FRONT_RIGHT_OF_CENTER,
+            "side", AudioFormat.CHANNEL_OUT_SIDE_LEFT | AudioFormat.CHANNEL_OUT_SIDE_RIGHT,
+            "top front", AudioFormat.CHANNEL_OUT_TOP_FRONT_LEFT
+                    | AudioFormat.CHANNEL_OUT_TOP_FRONT_RIGHT,
+            "top back", AudioFormat.CHANNEL_OUT_TOP_BACK_LEFT
+                    | AudioFormat.CHANNEL_OUT_TOP_BACK_RIGHT,
+            "top side", AudioFormat.CHANNEL_OUT_TOP_SIDE_LEFT
+                    | AudioFormat.CHANNEL_OUT_TOP_SIDE_RIGHT,
+            "bottom front", AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT
+                    | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT,
+            "front wide", AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT
+                    | AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT);
 
     /**
      * Convenience method to check that the channel configuration (a.k.a channel mask) is supported
@@ -1924,7 +1922,7 @@
                 return false;
         }
         // Check all pairs to see that they are matched (front duplicated here).
-        for (HashMap.Entry<String, Integer> e : CHANNEL_PAIR_MAP.entrySet()) {
+        for (Map.Entry<String, Integer> e : CHANNEL_PAIR_MAP.entrySet()) {
             final int positionPair = e.getValue();
             if ((channelConfig & positionPair) != 0
                     && (channelConfig & positionPair) != positionPair) {
diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 0c8cacd..524bde4 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -70,6 +70,7 @@
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.regex.Matcher;
@@ -4836,12 +4837,13 @@
             for (int i = 1; i < entryValues.length; ++i) {
                 final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
                 int first = -1, second = -1;
-                if (guessDataFormat.first == dataFormat.first
-                        || guessDataFormat.second == dataFormat.first) {
+                if (Objects.equals(guessDataFormat.first, dataFormat.first)
+                        || Objects.equals(guessDataFormat.second, dataFormat.first)) {
                     first = dataFormat.first;
                 }
-                if (dataFormat.second != -1 && (guessDataFormat.first == dataFormat.second
-                        || guessDataFormat.second == dataFormat.second)) {
+                if (dataFormat.second != -1
+                        && (Objects.equals(guessDataFormat.first, dataFormat.second)
+                        || Objects.equals(guessDataFormat.second, dataFormat.second))) {
                     second = dataFormat.second;
                 }
                 if (first == -1 && second == -1) {
diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java
index 39b3d0b..0291f64 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -264,10 +264,9 @@
         if (useSurfaceImageFormatInfo) {
             // nativeInit internally overrides UNKNOWN format. So does surface format query after
             // nativeInit and before getEstimatedNativeAllocBytes().
-            imageFormat = SurfaceUtils.getSurfaceFormat(surface);
-            mDataSpace = dataSpace = PublicFormatUtils.getHalDataspace(imageFormat);
-            mHardwareBufferFormat =
-                hardwareBufferFormat = PublicFormatUtils.getHalFormat(imageFormat);
+            mHardwareBufferFormat = hardwareBufferFormat = SurfaceUtils.getSurfaceFormat(surface);
+            mDataSpace = dataSpace = SurfaceUtils.getSurfaceDataspace(surface);
+            imageFormat = PublicFormatUtils.getPublicFormat(hardwareBufferFormat, dataSpace);
         }
 
         // Estimate the native buffer allocation size and register it so it gets accounted for
diff --git a/media/java/android/media/MediaCrypto.java b/media/java/android/media/MediaCrypto.java
index 889a5f7..1930262 100644
--- a/media/java/android/media/MediaCrypto.java
+++ b/media/java/android/media/MediaCrypto.java
@@ -75,14 +75,17 @@
     public final native boolean requiresSecureDecoderComponent(@NonNull String mime);
 
     /**
-     * Associate a MediaDrm session with this MediaCrypto instance.  The
-     * MediaDrm session is used to securely load decryption keys for a
-     * crypto scheme.  The crypto keys loaded through the MediaDrm session
+     * Associate a new MediaDrm session with this MediaCrypto instance.
+     *
+     * <p>The MediaDrm session is used to securely load decryption keys for a
+     * crypto scheme. The crypto keys loaded through the MediaDrm session
      * may be selected for use during the decryption operation performed
      * by {@link android.media.MediaCodec#queueSecureInputBuffer} by specifying
-     * their key ids in the {@link android.media.MediaCodec.CryptoInfo#key} field.
-     * @param sessionId the MediaDrm sessionId to associate with this
-     * MediaCrypto instance
+     * their key IDs in the {@link android.media.MediaCodec.CryptoInfo#key} field.
+     *
+     * @param sessionId The MediaDrm sessionId to associate with this MediaCrypto
+     *         instance. The session's scheme must match the scheme UUID used when
+     *         constructing this MediaCrypto instance.
      * @throws MediaCryptoException on failure to set the sessionId
      */
     public final native void setMediaDrmSession(@NonNull byte[] sessionId)
diff --git a/media/java/android/media/MediaHTTPService.java b/media/java/android/media/MediaHTTPService.java
index 3008067..2342a42 100644
--- a/media/java/android/media/MediaHTTPService.java
+++ b/media/java/android/media/MediaHTTPService.java
@@ -21,6 +21,8 @@
 import android.os.IBinder;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.net.CookieHandler;
 import java.net.CookieManager;
 import java.net.CookieStore;
@@ -31,7 +33,9 @@
 public class MediaHTTPService extends IMediaHTTPService.Stub {
     private static final String TAG = "MediaHTTPService";
     @Nullable private List<HttpCookie> mCookies;
-    private Boolean mCookieStoreInitialized = new Boolean(false);
+    private final Object mCookieStoreInitializedLock = new Object();
+    @GuardedBy("mCookieStoreInitializedLock")
+    private boolean mCookieStoreInitialized = false;
 
     public MediaHTTPService(@Nullable List<HttpCookie> cookies) {
         mCookies = cookies;
@@ -40,7 +44,7 @@
 
     public IMediaHTTPConnection makeHTTPConnection() {
 
-        synchronized (mCookieStoreInitialized) {
+        synchronized (mCookieStoreInitializedLock) {
             // Only need to do it once for all connections
             if ( !mCookieStoreInitialized )  {
                 CookieHandler cookieHandler = CookieHandler.getDefault();
@@ -78,8 +82,8 @@
 
                 Log.v(TAG, "makeHTTPConnection(" + this + "): cookieHandler: " + cookieHandler +
                         " Cookies: " + mCookies);
-            }   // mCookieStoreInitialized
-        }   // synchronized
+            }
+        }
 
         return new MediaHTTPConnection();
     }
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 77b5746..79a5902 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -2507,6 +2507,8 @@
      *
      * @see android.media.MediaPlayer#getTrackInfo
      */
+    // The creator needs to be pulic, which requires removing the @UnsupportedAppUsage
+    @SuppressWarnings("ParcelableCreator")
     static public class TrackInfo implements Parcelable {
         /**
          * Gets the track type.
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 5781537..681e112 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -539,9 +539,9 @@
     }
 
     /**
-     * Gets the Deduplication ID of the route if available.
-     * @see RouteDiscoveryPreference#shouldRemoveDuplicates()
-     * @hide
+     * Gets the deduplication IDs associated to the route.
+     *
+     * <p>Two routes with a matching deduplication ID originate from the same receiver device.
      */
     @NonNull
     public Set<String> getDeduplicationIds() {
@@ -1017,13 +1017,7 @@
         }
 
         /**
-         * Sets the deduplication ID of the route.
-         * Routes have the same ID could be removed even when
-         * they are from different providers.
-         * <p>
-         * If it's {@code null}, the route will not be removed.
-         * @see RouteDiscoveryPreference#shouldRemoveDuplicates()
-         * @hide
+         * Sets the {@link MediaRoute2Info#getDeduplicationIds() deduplication IDs} of the route.
          */
         @NonNull
         public Builder setDeduplicationIds(@NonNull Set<String> id) {
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 26cb9f8..161ea25 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -104,8 +104,8 @@
     private final String mPackageName;
 
     /**
-     * Stores the latest copy of all routes received from {@link MediaRouter2ServiceImpl}, without
-     * any filtering, sorting, or deduplication.
+     * Stores the latest copy of all routes received from the system server, without any filtering,
+     * sorting, or deduplication.
      *
      * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key.
      */
@@ -603,7 +603,7 @@
      */
     public void transferTo(@NonNull MediaRoute2Info route) {
         if (isSystemRouter()) {
-            sManager.selectRoute(mClientPackageName, route);
+            sManager.transfer(mClientPackageName, route);
             return;
         }
 
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index b6f07f4..e403e24 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -448,14 +448,16 @@
     }
 
     /**
-     * Selects media route for the specified package name.
+     * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name
+     * to a {@link MediaRoute2Info media route}.
+     *
+     * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing
+     * session based on the provided package name.
      */
-    public void selectRoute(@NonNull String packageName, @NonNull MediaRoute2Info route) {
+    public void transfer(@NonNull String packageName, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(packageName, "packageName must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
-        Log.v(TAG, "Selecting route. packageName= " + packageName + ", route=" + route);
-
         List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName);
         RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
         transfer(targetSession, route);
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index 82c3139..538e64c 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -158,13 +158,15 @@
 
     /**
      * Creates a local media player for the ringtone using currently set attributes.
+     * @return true if media player creation succeeded or is deferred,
+     * false if it did not succeed and can't be tried remotely.
      * @hide
      */
-    public void createLocalMediaPlayer() {
+    public boolean createLocalMediaPlayer() {
         Trace.beginSection("createLocalMediaPlayer");
         if (mUri == null) {
             Log.e(TAG, "Could not create media player as no URI was provided.");
-            return;
+            return mAllowRemote && mRemotePlayer != null;
         }
         destroyLocalPlayer();
         // try opening uri locally before delegating to remote player
@@ -195,6 +197,30 @@
             }
         }
         Trace.endSection();
+        return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
+    }
+
+    /**
+     * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
+     * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
+     * and if not URI has been set, it will assume no haptic channels are present.
+     * @hide
+     */
+    public boolean hasHapticChannels() {
+        // FIXME: support remote player, or internalize haptic channels support and remove entirely.
+        try {
+            android.os.Trace.beginSection("Ringtone.hasHapticChannels");
+            if (mLocalPlayer != null) {
+                for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
+                    if (trackInfo.hasHapticChannels()) {
+                        return true;
+                    }
+                }
+            }
+        } finally {
+            android.os.Trace.endSection();
+        }
+        return false;
     }
 
     /**
@@ -423,7 +449,6 @@
      */
     public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
         mVolumeShaperConfig = volumeShaperConfig;
-
         mUri = uri;
         if (mUri == null) {
             destroyLocalPlayer();
@@ -443,10 +468,11 @@
         if (mLocalPlayer != null) {
             // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
             // (typically because ringer mode is vibrate).
-            boolean isHapticOnly = AudioManager.hasHapticChannels(mContext, mUri)
-                    && !mAudioAttributes.areHapticChannelsMuted() && mVolume == 0;
-            if (isHapticOnly || mAudioManager.getStreamVolume(
-                    AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) {
+            if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
+                    != 0) {
+                startLocalPlayer();
+            } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
+                // is haptic only ringtone
                 startLocalPlayer();
             }
         } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 27db41c..f15f443 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -720,11 +720,14 @@
             @Nullable VolumeShaper.Configuration volumeShaperConfig,
             AudioAttributes audioAttributes) {
         // Don't set the stream type
-        Ringtone ringtone =
-                getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, false);
+        Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
+                volumeShaperConfig, false);
         if (ringtone != null) {
             ringtone.setAudioAttributesField(audioAttributes);
-            ringtone.createLocalMediaPlayer();
+            if (!ringtone.createLocalMediaPlayer()) {
+                Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
+                return null;
+            }
         }
         return ringtone;
     }
@@ -750,19 +753,6 @@
                 createLocalMediaPlayer);
     }
 
-    //FIXME bypass the notion of stream types within the class
-    /**
-     * Returns a {@link Ringtone} with {@link VolumeShaper} if required for a given sound URI on
-     * the given stream type. Normally, if you change the stream type on the returned
-     * {@link Ringtone}, it will re-create the {@link MediaPlayer}. This is just
-     * an optimized route to avoid that.
-     *
-     * @param streamType The stream type for the ringtone, or -1 if it should
-     *            not be set (and the default used instead).
-     * @param volumeShaperConfig config for volume shaper of the ringtone if applied.
-     * @see #getRingtone(Context, Uri)
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType,
             @Nullable VolumeShaper.Configuration volumeShaperConfig,
             boolean createLocalMediaPlayer) {
@@ -776,7 +766,10 @@
             r.setVolumeShaperConfig(volumeShaperConfig);
             r.setUri(ringtoneUri, volumeShaperConfig);
             if (createLocalMediaPlayer) {
-                r.createLocalMediaPlayer();
+                if (!r.createLocalMediaPlayer()) {
+                    Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
+                    return null;
+                }
             }
             return r;
         } catch (Exception ex) {
@@ -1158,24 +1151,38 @@
             }
 
             // Try finding the scanned ringtone
-            final String filename = getDefaultRingtoneFilename(type);
-            final String whichAudio = getQueryStringForType(type);
-            final String where = MediaColumns.DISPLAY_NAME + "=? AND " + whichAudio + "=?";
-            final Uri baseUri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
-            try (Cursor cursor = context.getContentResolver().query(baseUri,
-                    new String[] { MediaColumns._ID },
-                    where,
-                    new String[] { filename, "1" }, null)) {
-                if (cursor.moveToFirst()) {
-                    final Uri ringtoneUri = context.getContentResolver().canonicalizeOrElse(
-                            ContentUris.withAppendedId(baseUri, cursor.getLong(0)));
-                    RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
-                    Settings.System.putInt(context.getContentResolver(), setting, 1);
-                }
+            Uri ringtoneUri = computeDefaultRingtoneUri(context, type);
+            if (ringtoneUri != null) {
+                RingtoneManager.setActualDefaultRingtoneUri(context, type, ringtoneUri);
+                Settings.System.putInt(context.getContentResolver(), setting, 1);
             }
         }
     }
 
+    /**
+     * @param type the type of ringtone (e.g {@link #TYPE_RINGTONE})
+     * @return the system default URI if found, null otherwise.
+     */
+    private static Uri computeDefaultRingtoneUri(@NonNull Context context, int type) {
+        // Try finding the scanned ringtone
+        final String filename = getDefaultRingtoneFilename(type);
+        final String whichAudio = getQueryStringForType(type);
+        final String where = MediaColumns.DISPLAY_NAME + "=? AND " + whichAudio + "=?";
+        final Uri baseUri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI;
+        try (Cursor cursor = context.getContentResolver().query(baseUri,
+                new String[] { MediaColumns._ID },
+                where,
+                new String[] { filename, "1" }, null)) {
+            if (cursor.moveToFirst()) {
+                final Uri ringtoneUri = context.getContentResolver().canonicalizeOrElse(
+                        ContentUris.withAppendedId(baseUri, cursor.getLong(0)));
+                return ringtoneUri;
+            }
+        }
+
+        return null;
+    }
+
     private static String getDefaultRingtoneSetting(int type) {
         switch (type) {
             case TYPE_RINGTONE: return "ringtone_set";
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index 08655ca..9c0b825f 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -704,8 +704,12 @@
          * Combines all of the matching and exclusion rules that have been set and return a new
          * {@link AudioMixingRule} object.
          * @return a new {@link AudioMixingRule} object
+         * @throws IllegalArgumentException if the rule is empty.
          */
         public AudioMixingRule build() {
+            if (mCriteria.isEmpty()) {
+                throw new IllegalArgumentException("Cannot build AudioMixingRule with no rules.");
+            }
             return new AudioMixingRule(
                     mTargetMixType == AudioMix.MIX_TYPE_INVALID
                             ? AudioMix.MIX_TYPE_PLAYERS : mTargetMixType,
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index 74c5499..ee82588 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -240,8 +240,7 @@
      * @param handler The {@link android.os.Handler Handler} that will be used for delivering the
      *                device notifications. If handler is null, then the thread used for the
      *                callback is unspecified.
-     * @deprecated Use the {@link #registerDeviceCallback}
-     *             method with Executor and transport instead.
+     * @deprecated Use {@link #registerDeviceCallback(int, Executor, DeviceCallback)} instead.
      */
     @Deprecated
     public void registerDeviceCallback(DeviceCallback callback, Handler handler) {
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 1bd12af..7e1bbe3 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -244,12 +244,9 @@
                 mCallback = null;
                 return;
             }
-            if (handler == null) {
-                handler = new Handler();
-            }
+            Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
             callback.mSession = this;
-            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
-                    callback);
+            CallbackMessageHandler msgHandler = new CallbackMessageHandler(looper, callback);
             mCallback = msgHandler;
         }
     }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 6ae7dfb..9b8ec5e 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -45,6 +45,7 @@
     void onRequestTrackInfoList(int seq);
     void onRequestCurrentTvInputId(int seq);
     void onRequestStartRecording(in Uri programUri, int seq);
+    void onRequestStopRecording(in String recordingId, int seq);
     void onRequestSigning(
             in String id, in String algorithm, in String alias, in byte[] data, int seq);
     void onAdRequest(in AdRequest request, int Seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 84b9c9e..38fc717 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -64,6 +64,7 @@
     void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId);
     void notifySignalStrength(in IBinder sessionToken, int stength, int userId);
     void notifyRecordingStarted(in IBinder sessionToken, in String recordingId, int userId);
+    void notifyRecordingStopped(in IBinder sessionToken, in String recordingId, int userId);
     void setSurface(in IBinder sessionToken, in Surface surface, int userId);
     void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
             int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 95b4ffa..9e33536 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -54,6 +54,7 @@
     void notifyContentBlocked(in String rating);
     void notifySignalStrength(int strength);
     void notifyRecordingStarted(in String recordingId);
+    void notifyRecordingStopped(in String recordingId);
     void setSurface(in Surface surface);
     void dispatchSurfaceChanged(int format, int width, int height);
     void notifyBroadcastInfoResponse(in BroadcastInfoResponse response);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 6478057..4ce5871 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -44,6 +44,7 @@
     void onRequestTrackInfoList();
     void onRequestCurrentTvInputId();
     void onRequestStartRecording(in Uri programUri);
+    void onRequestStopRecording(in String recordingId);
     void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
     void onAdRequest(in AdRequest request);
 }
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 042cb15..a2fdfe0 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -82,6 +82,7 @@
     private static final int DO_RELAYOUT_MEDIA_VIEW = 28;
     private static final int DO_REMOVE_MEDIA_VIEW = 29;
     private static final int DO_NOTIFY_RECORDING_STARTED = 30;
+    private static final int DO_NOTIFY_RECORDING_STOPPED = 31;
 
     private final HandlerCaller mCaller;
     private Session mSessionImpl;
@@ -169,6 +170,10 @@
                 mSessionImpl.notifyRecordingStarted((String) msg.obj);
                 break;
             }
+            case DO_NOTIFY_RECORDING_STOPPED: {
+                mSessionImpl.notifyRecordingStopped((String) msg.obj);
+                break;
+            }
             case DO_SEND_SIGNING_RESULT: {
                 SomeArgs args = (SomeArgs) msg.obj;
                 mSessionImpl.sendSigningResult((String) args.arg1, (byte[]) args.arg2);
@@ -392,6 +397,12 @@
     }
 
     @Override
+    public void notifyRecordingStopped(String recordingId) {
+        mCaller.executeOrSendMessage(mCaller.obtainMessageO(
+                DO_NOTIFY_RECORDING_STOPPED, recordingId));
+    }
+
+    @Override
     public void setSurface(Surface surface) {
         mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface));
     }
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index a27fd10..287df40 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -499,6 +499,18 @@
             }
 
             @Override
+            public void onRequestStopRecording(String recordingId, int seq) {
+                synchronized (mSessionCallbackRecordMap) {
+                    SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+                    if (record == null) {
+                        Log.e(TAG, "Callback not found for seq " + seq);
+                        return;
+                    }
+                    record.postRequestStopRecording(recordingId);
+                }
+            }
+
+            @Override
             public void onRequestSigning(
                     String id, String algorithm, String alias, byte[] data, int seq) {
                 synchronized (mSessionCallbackRecordMap) {
@@ -1047,7 +1059,7 @@
             }
         }
 
-        void notifyRecordingStarted(@Nullable String recordingId) {
+        void notifyRecordingStarted(String recordingId) {
             if (mToken == null) {
                 Log.w(TAG, "The session has been already released");
                 return;
@@ -1059,6 +1071,18 @@
             }
         }
 
+        void notifyRecordingStopped(String recordingId) {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.notifyRecordingStopped(mToken, recordingId, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
         void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
             if (mToken == null) {
                 Log.w(TAG, "The session has been already released");
@@ -1729,6 +1753,15 @@
             });
         }
 
+        void postRequestStopRecording(String recordingId) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mSessionCallback.onRequestStopRecording(mSession, recordingId);
+                }
+            });
+        }
+
         void postRequestSigning(String id, String algorithm, String alias, byte[] data) {
             mHandler.post(new Runnable() {
                 @Override
@@ -1884,11 +1917,22 @@
          * called.
          *
          * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param programUri The Uri of the program to be recorded.
          */
         public void onRequestStartRecording(Session session, Uri programUri) {
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#RequestStopRecording} is
+         * called.
+         *
+         * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+         * @param recordingId The recordingId of the recording to be stopped.
+         */
+        public void onRequestStopRecording(Session session, String recordingId) {
+        }
+
+        /**
          * This is called when
          * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
          * called.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 3d65effa..90eed9e 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -456,11 +456,22 @@
 
         /**
          * Receives started recording's ID.
+         *
+         * @param recordingId The ID of the recording started
+         */
+        public void onRecordingStarted(@NonNull String recordingId) {
+        }
+
+        /**
+         * Receives stopped recording's ID.
+         *
+         * @param recordingId The ID of the recording stopped
          * @hide
          */
-        public void onRecordingStarted(@Nullable String recordingId) {
+        public void onRecordingStopped(@NonNull String recordingId) {
         }
 
+
         /**
          * Receives signing result.
          * @param signingId the ID to identify the request. It's the same as the corresponding ID in
@@ -914,7 +925,15 @@
         /**
          * Requests starting of recording
          *
-         * @hide
+         * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
+         * call {@link android.media.tv.TvRecordingClient#startRecording(Uri)} with the provided
+         * {@code programUri}.
+         * A non-null {@code programUri} implies the started recording should be of that specific
+         * program, whereas null {@code programUri} does not impose such a requirement and the
+         * recording can span across multiple TV programs.
+         *
+         * @param programUri The URI for the TV program to record.
+         * @see android.media.tv.TvRecordingClient#startRecording(Uri)
          */
         @CallSuper
         public void requestStartRecording(@Nullable Uri programUri) {
@@ -933,6 +952,33 @@
         }
 
         /**
+         * Requests starting of recording
+         *
+         * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
+         * call {@link android.media.tv.TvRecordingClient#stopRecording()}.
+         * @see android.media.tv.TvRecordingClient#stopRecording()
+         *
+         * @hide
+         */
+        @CallSuper
+        public void requestStopRecording(@NonNull String recordingId) {
+            executeOrPostRunnableOnMainThread(() -> {
+                try {
+                    if (DEBUG) {
+                        Log.d(TAG, "requestStopRecording");
+                    }
+                    if (mSessionCallback != null) {
+                        mSessionCallback.onRequestStopRecording(recordingId);
+                    }
+                } catch (RemoteException e) {
+                    Log.w(TAG, "error in requestStopRecording", e);
+                }
+            });
+        }
+
+
+
+        /**
          * Requests signing of the given data.
          *
          * <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -1142,11 +1188,21 @@
             onAdResponse(response);
         }
 
+        /**
+         * Calls {@link #onRecordingStarted(String)}.
+         */
         void notifyRecordingStarted(String recordingId) {
             onRecordingStarted(recordingId);
         }
 
         /**
+         * Calls {@link #onRecordingStopped(String)}.
+         */
+        void notifyRecordingStopped(String recordingId) {
+            onRecordingStopped(recordingId);
+        }
+
+        /**
          * Notifies when the session state is changed.
          *
          * @param state the current session state.
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 76ba69c..fcd781b 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -581,13 +581,12 @@
     }
 
     /**
-     * Alerts the TV interactive app that a recording has been started with recordingId
+     * Alerts the TV interactive app that a recording has been started.
      *
-     * @param recordingId The Id of the recording started
-     *
-     * @hide
+     * @param recordingId The ID of the recording started. This ID is created and maintained by the
+     *                    TV app and is used to identify the recording in the future.
      */
-    public void notifyRecordingStarted(@Nullable String recordingId) {
+    public void notifyRecordingStarted(@NonNull String recordingId) {
         if (DEBUG) {
             Log.d(TAG, "notifyRecordingStarted");
         }
@@ -597,6 +596,23 @@
     }
 
     /**
+     * Alerts the TV interactive app that a recording has been stopped.
+     *
+     * @param recordingId The ID of the recording stopped. This ID is created and maintained
+     *                    by the TV app when a recording is started.
+     * @see TvInteractiveAppView#notifyRecordingStarted(String)
+     * @hide
+     */
+    public void notifyRecordingStopped(@NonNull String recordingId) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyRecordingStopped");
+        }
+        if (mSession != null) {
+            mSession.notifyRecordingStopped(recordingId);
+        }
+    }
+
+    /**
      * Sends signing result to related TV interactive app.
      *
      * <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -859,10 +875,9 @@
         /**
          * This is called when {@link TvInteractiveAppService.Session#requestStartRecording(Uri)}
          * is called.
+         *
          * @param iAppServiceId The ID of the TV interactive app service bound to this view.
          * @param programUri The program URI to record
-         *
-         * @hide
          */
         public void onRequestStartRecording(
                 @NonNull String iAppServiceId,
@@ -870,6 +885,19 @@
         }
 
         /**
+         * This is called when {@link TvInteractiveAppService.Session#requestStopRecording()}
+         * is called.
+         *
+         * @param iAppServiceId The ID of the TV interactive app service bound to this view.
+         * @param recordingId The ID of the recording to stop.
+         * @hide
+         */
+        public void onRequestStopRecording(
+                @NonNull String iAppServiceId,
+                @NonNull String recordingId) {
+        }
+
+        /**
          * This is called when
          * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is
          * called.
@@ -1207,6 +1235,20 @@
         }
 
         @Override
+        public void onRequestStopRecording(Session session, String recordingId) {
+            if (DEBUG) {
+                Log.d(TAG, "onRequestStopRecording");
+            }
+            if (this != mSessionCallback) {
+                Log.w(TAG, "onRequestStopRecording - session not created");
+                return;
+            }
+            if (mCallback != null) {
+                mCallback.onRequestStopRecording(mIAppServiceId, recordingId);
+            }
+        }
+
+        @Override
         public void onRequestSigning(
                 Session session, String id, String algorithm, String alias, byte[] data) {
             if (DEBUG) {
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index b1c9730..fab63aa 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -289,6 +289,7 @@
     private Integer mFrontendHandle;
     private Tuner mFeOwnerTuner = null;
     private int mFrontendType = FrontendSettings.TYPE_UNDEFINED;
+    private Integer mDesiredFrontendId = null;
     private int mUserId;
     private Lnb mLnb;
     private Integer mLnbHandle;
@@ -1326,10 +1327,18 @@
 
     private boolean requestFrontend() {
         int[] feHandle = new int[1];
-        TunerFrontendRequest request = new TunerFrontendRequest();
-        request.clientId = mClientId;
-        request.frontendType = mFrontendType;
-        boolean granted = mTunerResourceManager.requestFrontend(request, feHandle);
+        boolean granted = false;
+        try {
+            TunerFrontendRequest request = new TunerFrontendRequest();
+            request.clientId = mClientId;
+            request.frontendType = mFrontendType;
+            request.desiredId = mDesiredFrontendId == null
+                    ? TunerFrontendRequest.DEFAULT_DESIRED_ID
+                    : mDesiredFrontendId;
+            granted = mTunerResourceManager.requestFrontend(request, feHandle);
+        } finally {
+            mDesiredFrontendId = null;
+        }
         if (granted) {
             mFrontendHandle = feHandle[0];
             mFrontend = nativeOpenFrontendByHandle(mFrontendHandle);
@@ -2367,6 +2376,46 @@
     }
 
     /**
+     * Request a frontend by frontend info.
+     *
+     * <p> This API is used if the applications want to select a desired frontend before
+     * {@link tune} to use a specific satellite or sending SatCR DiSEqC command for {@link tune}.
+     *
+     * @param desiredFrontendInfo the FrontendInfo of the desired fronted. It can be retrieved by
+     * {@link getAvailableFrontendInfos}
+     *
+     * @return result status of open operation.
+     * @throws SecurityException if the caller does not have appropriate permissions.
+     */
+    @Result
+    public int applyFrontend(@NonNull FrontendInfo desiredFrontendInfo) {
+        Objects.requireNonNull(desiredFrontendInfo, "desiredFrontendInfo must not be null");
+        mFrontendLock.lock();
+        try {
+            if (mFeOwnerTuner != null) {
+                Log.e(TAG, "Operation connot be done by sharee of tuner");
+                return RESULT_INVALID_STATE;
+            }
+            if (mFrontendHandle != null) {
+                Log.e(TAG, "A frontend has been opened before");
+                return RESULT_INVALID_STATE;
+            }
+            mFrontendType = desiredFrontendInfo.getType();
+            mDesiredFrontendId = desiredFrontendInfo.getId();
+            if (DEBUG) {
+                Log.d(TAG, "Applying frontend with type " + mFrontendType + ", id "
+                        + mDesiredFrontendId);
+            }
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
+                return RESULT_UNAVAILABLE;
+            }
+            return RESULT_SUCCESS;
+        } finally {
+            mFrontendLock.unlock();
+        }
+    }
+
+    /**
      * Open a shared filter instance.
      *
      * @param context the context of the caller.
@@ -2445,7 +2494,7 @@
         return granted;
     }
 
-    private boolean checkResource(int resourceType, ReentrantLock localLock)  {
+    private boolean checkResource(int resourceType, ReentrantLock localLock) {
         switch (resourceType) {
             case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
                 if (mFrontendHandle == null && !requestResource(resourceType, localLock)) {
@@ -2483,7 +2532,7 @@
     // 3) if no, then first release the held lock and grab the TRMS lock to avoid deadlock
     // 4) grab the local lock again and release the TRMS lock
     // If localLock is null, we'll assume the caller does not want the lock related operations
-    private boolean requestResource(int resourceType, ReentrantLock localLock)  {
+    private boolean requestResource(int resourceType, ReentrantLock localLock) {
         boolean enableLockOperations = localLock != null;
 
         // release the local lock first to avoid deadlock
diff --git a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
index 1a65832..4bcc3c6 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
@@ -28,6 +28,7 @@
 import android.os.Process;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
 
 import java.util.concurrent.Executor;
@@ -48,7 +49,9 @@
     private static int sInstantId = 0;
     private int mSegmentId = 0;
     private int mOverflow;
-    private Boolean mIsStopped = true;
+    private final Object mIsStoppedLock = new Object();
+    @GuardedBy("mIsStoppedLock")
+    private boolean mIsStopped = true;
     private final Object mListenerLock = new Object();
 
     private native int nativeAttachFilter(Filter filter);
@@ -178,7 +181,7 @@
                 .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
                     FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD,
                     FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STARTED, mSegmentId, 0);
-        synchronized (mIsStopped) {
+        synchronized (mIsStoppedLock) {
             int result = nativeStartDvr();
             if (result == Tuner.RESULT_SUCCESS) {
                 mIsStopped = false;
@@ -201,7 +204,7 @@
                 .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
                     FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD,
                     FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STOPPED, mSegmentId, mOverflow);
-        synchronized (mIsStopped) {
+        synchronized (mIsStoppedLock) {
             int result = nativeStopDvr();
             if (result == Tuner.RESULT_SUCCESS) {
                 mIsStopped = true;
@@ -219,7 +222,7 @@
      */
     @Result
     public int flush() {
-        synchronized (mIsStopped) {
+        synchronized (mIsStoppedLock) {
             if (mIsStopped) {
                 return nativeFlushDvr();
             }
diff --git a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
index 144b98c..e0af76d 100644
--- a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
+++ b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
@@ -140,19 +140,29 @@
     void setLnbInfoList(in int[] lnbIds);
 
     /*
-     * This API is used by the Tuner framework to request an available frontend from the TunerHAL.
+     * This API is used by the Tuner framework to request a frontend from the TunerHAL.
      *
-     * <p>There are three possible scenarios:
+     * <p>There are two cases:
      * <ul>
-     * <li>If there is frontend available, the API would send the id back.
-     *
-     * <li>If no Frontend is available but the current request info can show higher priority than
-     * other uses of Frontend, the API will send
+     * <li>If the desiredId is not {@link TunerFrontendRequest#DEFAULT_DESIRED_ID}
+     * <li><li>If the desired frontend with the given frontendType is available, the API would send
+     * the id back.
+     * <li><li>If the desired frontend with the given frontendType is in use but the current request
+     * info can show higher priority than other uses of Frontend, the API will send
      * {@link IResourcesReclaimListener#onReclaimResources()} to the {@link Tuner}. Tuner would
      * handle the resource reclaim on the holder of lower priority and notify the holder of its
      * resource loss.
+     * <li><li>If no frontend can be granted, the API would return false.
+     * <ul>
      *
-     * <li>If no frontend can be granted, the API would return false.
+     * <li>If the desiredId is {@link TunerFrontendRequest#DEFAULT_DESIRED_ID}
+     * <li><li>If there is frontend available, the API would send the id back.
+     * <li><li>If no Frontend is available but the current request info can show higher priority
+     * than other uses of Frontend, the API will send
+     * {@link IResourcesReclaimListener#onReclaimResources()} to the {@link Tuner}. Tuner would
+     * handle the resource reclaim on the holder of lower priority and notify the holder of its
+     * resource loss.
+     * <li><li>If no frontend can be granted, the API would return false.
      * <ul>
      *
      * <p><strong>Note:</strong> {@link #setFrontendInfoList(TunerFrontendInfo[])} must be called
diff --git a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/TunerFrontendRequest.aidl b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/TunerFrontendRequest.aidl
index 4d98222..c4598a4 100644
--- a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/TunerFrontendRequest.aidl
+++ b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/TunerFrontendRequest.aidl
@@ -22,7 +22,11 @@
  * @hide
  */
 parcelable TunerFrontendRequest {
+    const int DEFAULT_DESIRED_ID = 0xFFFFFFFF;
+
     int clientId;
 
     int frontendType;
-}
\ No newline at end of file
+
+    int desiredId = DEFAULT_DESIRED_ID;
+}
diff --git a/media/java/android/mtp/MtpPropertyGroup.java b/media/java/android/mtp/MtpPropertyGroup.java
index aff2e1b4..89e5e0d 100644
--- a/media/java/android/mtp/MtpPropertyGroup.java
+++ b/media/java/android/mtp/MtpPropertyGroup.java
@@ -230,7 +230,7 @@
                 case MtpConstants.PROPERTY_PERSISTENT_UID:
                     // The persistent uid must be unique and never reused among all objects,
                     // and remain the same between sessions.
-                    long puid = (object.getPath().toString().hashCode() << 32)
+                    long puid = (((long) object.getPath().toString().hashCode()) << 32)
                             + object.getModifiedTime();
                     list.append(id, property.code, property.type, puid);
                     break;
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 95599bd..1183ca3 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -174,6 +174,12 @@
     jfieldID typeId;
 } gDescriptorInfo;
 
+static struct {
+    jclass clazz;
+    jmethodID ctorId;
+    jmethodID setId;
+} gBufferInfo;
+
 struct fields_t {
     jmethodID postEventFromNativeID;
     jmethodID lockAndGetContextID;
@@ -460,11 +466,7 @@
         return err;
     }
 
-    ScopedLocalRef<jclass> clazz(
-            env, env->FindClass("android/media/MediaCodec$BufferInfo"));
-
-    jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
-    env->CallVoidMethod(bufferInfo, method, (jint)offset, (jint)size, timeUs, flags);
+    env->CallVoidMethod(bufferInfo, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags);
 
     return OK;
 }
@@ -1091,13 +1093,7 @@
             CHECK(msg->findInt64("timeUs", &timeUs));
             CHECK(msg->findInt32("flags", (int32_t *)&flags));
 
-            ScopedLocalRef<jclass> clazz(
-                    env, env->FindClass("android/media/MediaCodec$BufferInfo"));
-            jmethodID ctor = env->GetMethodID(clazz.get(), "<init>", "()V");
-            jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
-
-            obj = env->NewObject(clazz.get(), ctor);
-
+            obj = env->NewObject(gBufferInfo.clazz, gBufferInfo.ctorId);
             if (obj == NULL) {
                 if (env->ExceptionCheck()) {
                     ALOGE("Could not create MediaCodec.BufferInfo.");
@@ -1107,7 +1103,7 @@
                 return;
             }
 
-            env->CallVoidMethod(obj, method, (jint)offset, (jint)size, timeUs, flags);
+            env->CallVoidMethod(obj, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags);
             break;
         }
 
@@ -3235,6 +3231,16 @@
 
     gDescriptorInfo.typeId = env->GetFieldID(clazz.get(), "mType", "I");
     CHECK(gDescriptorInfo.typeId != NULL);
+
+    clazz.reset(env->FindClass("android/media/MediaCodec$BufferInfo"));
+    CHECK(clazz.get() != NULL);
+    gBufferInfo.clazz = (jclass)env->NewGlobalRef(clazz.get());
+
+    gBufferInfo.ctorId = env->GetMethodID(clazz.get(), "<init>", "()V");
+    CHECK(gBufferInfo.ctorId != NULL);
+
+    gBufferInfo.setId = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
+    CHECK(gBufferInfo.setId != NULL);
 }
 
 static void android_media_MediaCodec_native_setup(
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index c18edcd..1504930 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -360,6 +360,7 @@
                 lnb,
                 gFields.onLnbEventID,
                 (jint)lnbEventType);
+        env->DeleteLocalRef(lnb);
     } else {
         ALOGE("LnbClientCallbackImpl::onEvent:"
                 "Lnb object has been freed. Ignoring callback.");
@@ -378,6 +379,7 @@
                 lnb,
                 gFields.onLnbDiseqcMessageID,
                 array);
+        env->DeleteLocalRef(lnb);
     } else {
         ALOGE("LnbClientCallbackImpl::onDiseqcMessage:"
                 "Lnb object has been freed. Ignoring callback.");
@@ -404,6 +406,7 @@
     jobject dvr(env->NewLocalRef(mDvrObj));
     if (!env->IsSameObject(dvr, nullptr)) {
         env->CallVoidMethod(dvr, gFields.onDvrRecordStatusID, (jint)status);
+        env->DeleteLocalRef(dvr);
     } else {
         ALOGE("DvrClientCallbackImpl::onRecordStatus:"
                 "Dvr object has been freed. Ignoring callback.");
@@ -416,6 +419,7 @@
     jobject dvr(env->NewLocalRef(mDvrObj));
     if (!env->IsSameObject(dvr, nullptr)) {
         env->CallVoidMethod(dvr, gFields.onDvrPlaybackStatusID, (jint)status);
+        env->DeleteLocalRef(dvr);
     } else {
         ALOGE("DvrClientCallbackImpl::onPlaybackStatus:"
                 "Dvr object has been freed. Ignoring callback.");
@@ -603,6 +607,7 @@
 
     jobject obj = env->NewObject(eventClazz, eventInit, tableId, version, sectionNum, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getMediaEvent(jobjectArray &arr, const int size,
@@ -673,6 +678,10 @@
     }
 
     env->SetObjectArrayElement(arr, size, obj);
+    if(audioDescriptor != nullptr) {
+        env->DeleteLocalRef(audioDescriptor);
+    }
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getPesEvent(jobjectArray &arr, const int size,
@@ -688,6 +697,7 @@
 
     jobject obj = env->NewObject(eventClazz, eventInit, streamId, dataLength, mpuSequenceNumber);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getTsRecordEvent(jobjectArray &arr, const int size,
@@ -725,6 +735,7 @@
     jobject obj =
             env->NewObject(eventClazz, eventInit, jpid, ts, sc, byteNumber, pts, firstMbInSlice);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getMmtpRecordEvent(jobjectArray &arr, const int size,
@@ -745,6 +756,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, scHevcIndexMask, byteNumber,
                                  mpuSequenceNumber, pts, firstMbInSlice, tsIndexMask);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getDownloadEvent(jobjectArray &arr, const int size,
@@ -764,6 +776,7 @@
     jobject obj = env->NewObject(eventClazz, eventInit, itemId, downloadId, mpuSequenceNumber,
                                  itemFragmentIndex, lastItemFragmentIndex, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getIpPayloadEvent(jobjectArray &arr, const int size,
@@ -776,6 +789,7 @@
     jint dataLength = ipPayloadEvent.dataLength;
     jobject obj = env->NewObject(eventClazz, eventInit, dataLength);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getTemiEvent(jobjectArray &arr, const int size,
@@ -794,6 +808,8 @@
 
     jobject obj = env->NewObject(eventClazz, eventInit, pts, descrTag, array);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(array);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getScramblingStatusEvent(jobjectArray &arr, const int size,
@@ -807,6 +823,7 @@
                     .get<DemuxFilterMonitorEvent::Tag::scramblingStatus>();
     jobject obj = env->NewObject(eventClazz, eventInit, scramblingStatus);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getIpCidChangeEvent(jobjectArray &arr, const int size,
@@ -819,6 +836,7 @@
                                                  .get<DemuxFilterMonitorEvent::Tag::cid>();
     jobject obj = env->NewObject(eventClazz, eventInit, cid);
     env->SetObjectArrayElement(arr, size, obj);
+    env->DeleteLocalRef(obj);
 }
 
 void FilterClientCallbackImpl::getRestartEvent(jobjectArray &arr, const int size,
@@ -922,10 +940,12 @@
             methodID = gFields.onSharedFilterEventID;
         }
         env->CallVoidMethod(filter, methodID, array);
+        env->DeleteLocalRef(filter);
     } else {
         ALOGE("FilterClientCallbackImpl::onFilterEvent:"
               "Filter object has been freed. Ignoring callback.");
     }
+    env->DeleteLocalRef(array);
 }
 
 void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) {
@@ -938,6 +958,7 @@
             methodID = gFields.onSharedFilterStatusID;
         }
         env->CallVoidMethod(filter, methodID, (jint)static_cast<uint8_t>(status));
+        env->DeleteLocalRef(filter);
     } else {
         ALOGE("FilterClientCallbackImpl::onFilterStatus:"
               "Filter object has been freed. Ignoring callback.");
@@ -1006,6 +1027,7 @@
                     frontend,
                     gFields.onFrontendEventID,
                     (jint)frontendEventType);
+            env->DeleteLocalRef(frontend);
         } else {
             ALOGW("FrontendClientCallbackImpl::onEvent:"
                     "Frontend object has been freed. Ignoring callback.");
@@ -1028,6 +1050,7 @@
             continue;
         }
         executeOnScanMessage(env, clazz, frontend, type, message);
+        env->DeleteLocalRef(frontend);
     }
 }
 
@@ -1069,6 +1092,7 @@
             env->SetLongArrayRegion(freqs, 0, v.size(), reinterpret_cast<jlong *>(&v[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onFrequenciesReport", "([J)V"),
                                 freqs);
+            env->DeleteLocalRef(freqs);
             break;
         }
         case FrontendScanMessageType::SYMBOL_RATE: {
@@ -1077,6 +1101,7 @@
             env->SetIntArrayRegion(symbolRates, 0, v.size(), reinterpret_cast<jint *>(&v[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onSymbolRates", "([I)V"),
                                 symbolRates);
+            env->DeleteLocalRef(symbolRates);
             break;
         }
         case FrontendScanMessageType::HIERARCHY: {
@@ -1094,6 +1119,7 @@
             jintArray plpIds = env->NewIntArray(jintV.size());
             env->SetIntArrayRegion(plpIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onPlpIds", "([I)V"), plpIds);
+            env->DeleteLocalRef(plpIds);
             break;
         }
         case FrontendScanMessageType::GROUP_IDS: {
@@ -1101,6 +1127,7 @@
             jintArray groupIds = env->NewIntArray(jintV.size());
             env->SetIntArrayRegion(groupIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onGroupIds", "([I)V"), groupIds);
+            env->DeleteLocalRef(groupIds);
             break;
         }
         case FrontendScanMessageType::INPUT_STREAM_IDS: {
@@ -1109,6 +1136,7 @@
             env->SetIntArrayRegion(streamIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onInputStreamIds", "([I)V"),
                                 streamIds);
+            env->DeleteLocalRef(streamIds);
             break;
         }
         case FrontendScanMessageType::STANDARD: {
@@ -1142,12 +1170,14 @@
                 jboolean lls = info.bLlsFlag;
                 jobject obj = env->NewObject(plpClazz, init, plpId, lls);
                 env->SetObjectArrayElement(array, i, obj);
+                env->DeleteLocalRef(obj);
             }
             env->CallVoidMethod(frontend,
                                 env->GetMethodID(clazz, "onAtsc3PlpInfos",
                                                  "([Landroid/media/tv/tuner/frontend/"
                                                  "Atsc3PlpInfo;)V"),
                                 array);
+            env->DeleteLocalRef(array);
             break;
         }
         case FrontendScanMessageType::MODULATION: {
@@ -1219,6 +1249,7 @@
             env->SetIntArrayRegion(cellIds, 0, jintV.size(), reinterpret_cast<jint *>(&jintV[0]));
             env->CallVoidMethod(frontend, env->GetMethodID(clazz, "onDvbtCellIdsReported", "([I)V"),
                                 cellIds);
+            env->DeleteLocalRef(cellIds);
             break;
         }
         default:
@@ -1673,6 +1704,7 @@
     for (int i = 0; i < size; i++) {
         jobject readinessObj = env->NewObject(clazz, init, intTypes[i], readiness[i]);
         env->SetObjectArrayElement(valObj, i, readinessObj);
+        env->DeleteLocalRef(readinessObj);
     }
     return valObj;
 }
@@ -2081,6 +2113,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isDemodLocked>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::snr: {
@@ -2088,6 +2121,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::snr>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::ber: {
@@ -2095,6 +2129,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::ber>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::per: {
@@ -2102,6 +2137,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::per>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::preBer: {
@@ -2109,6 +2145,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::preBer>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::signalQuality: {
@@ -2116,6 +2153,7 @@
                 jobject newIntegerObj = env->NewObject(intClazz, initInt,
                                                        s.get<FrontendStatus::Tag::signalQuality>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::signalStrength: {
@@ -2124,6 +2162,7 @@
                         env->NewObject(intClazz, initInt,
                                        s.get<FrontendStatus::Tag::signalStrength>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::symbolRate: {
@@ -2131,6 +2170,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::symbolRate>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::innerFec: {
@@ -2141,6 +2181,8 @@
                         env->NewObject(longClazz, initLong,
                                        static_cast<long>(s.get<FrontendStatus::Tag::innerFec>()));
                 env->SetObjectField(statusObj, field, newLongObj);
+                env->DeleteLocalRef(longClazz);
+                env->DeleteLocalRef(newLongObj);
                 break;
             }
             case FrontendStatus::Tag::modulationStatus: {
@@ -2183,6 +2225,7 @@
                 if (valid) {
                     jobject newIntegerObj = env->NewObject(intClazz, initInt, intModulation);
                     env->SetObjectField(statusObj, field, newIntegerObj);
+                    env->DeleteLocalRef(newIntegerObj);
                 }
                 break;
             }
@@ -2192,6 +2235,7 @@
                         env->NewObject(intClazz, initInt,
                                        static_cast<jint>(s.get<FrontendStatus::Tag::inversion>()));
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::lnbVoltage: {
@@ -2200,6 +2244,7 @@
                         env->NewObject(intClazz, initInt,
                                        static_cast<jint>(s.get<FrontendStatus::Tag::lnbVoltage>()));
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::plpId: {
@@ -2207,6 +2252,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::plpId>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::isEWBS: {
@@ -2214,6 +2260,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isEWBS>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::agc: {
@@ -2221,6 +2268,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::agc>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::isLnaOn: {
@@ -2228,6 +2276,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isLnaOn>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::isLayerError: {
@@ -2241,6 +2290,7 @@
                     env->SetBooleanArrayRegion(valObj, i, 1, &x);
                 }
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::mer: {
@@ -2248,6 +2298,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::mer>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::freqOffset: {
@@ -2255,6 +2306,7 @@
                 jobject newLongObj = env->NewObject(longClazz, initLong,
                                                     s.get<FrontendStatus::Tag::freqOffset>());
                 env->SetObjectField(statusObj, field, newLongObj);
+                env->DeleteLocalRef(newLongObj);
                 break;
             }
             case FrontendStatus::Tag::hierarchy: {
@@ -2263,6 +2315,7 @@
                         env->NewObject(intClazz, initInt,
                                        static_cast<jint>(s.get<FrontendStatus::Tag::hierarchy>()));
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::isRfLocked: {
@@ -2270,6 +2323,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isRfLocked>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::plpInfo: {
@@ -2289,9 +2343,12 @@
 
                     jobject plpObj = env->NewObject(plpClazz, initPlp, plpId, isLocked, uec);
                     env->SetObjectArrayElement(valObj, i, plpObj);
+                    env->DeleteLocalRef(plpObj);
                 }
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(plpClazz);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::modulations: {
@@ -2374,6 +2431,7 @@
                 if (valid) {
                     env->SetObjectField(statusObj, field, valObj);
                 }
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::bers: {
@@ -2384,6 +2442,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::codeRates: {
@@ -2394,6 +2453,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::bandwidth: {
@@ -2434,6 +2494,7 @@
                 if (valid) {
                     jobject newIntegerObj = env->NewObject(intClazz, initInt, intBandwidth);
                     env->SetObjectField(statusObj, field, newIntegerObj);
+                    env->DeleteLocalRef(newIntegerObj);
                 }
                 break;
             }
@@ -2465,6 +2526,7 @@
                 if (valid) {
                     jobject newIntegerObj = env->NewObject(intClazz, initInt, intInterval);
                     env->SetObjectField(statusObj, field, newIntegerObj);
+                    env->DeleteLocalRef(newIntegerObj);
                 }
                 break;
             }
@@ -2497,6 +2559,7 @@
                 if (valid) {
                     jobject newIntegerObj = env->NewObject(intClazz, initInt, intTransmissionMode);
                     env->SetObjectField(statusObj, field, newIntegerObj);
+                    env->DeleteLocalRef(newIntegerObj);
                 }
                 break;
             }
@@ -2505,6 +2568,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::uec>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::systemId: {
@@ -2512,6 +2576,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::systemId>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::interleaving: {
@@ -2558,6 +2623,7 @@
                 if (valid) {
                     env->SetObjectField(statusObj, field, valObj);
                 }
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::isdbtSegment: {
@@ -2568,6 +2634,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint*>(&v[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::tsDataRate: {
@@ -2578,6 +2645,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&v[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::rollOff: {
@@ -2605,6 +2673,7 @@
                 if (valid) {
                     jobject newIntegerObj = env->NewObject(intClazz, initInt, intRollOff);
                     env->SetObjectField(statusObj, field, newIntegerObj);
+                    env->DeleteLocalRef(newIntegerObj);
                 }
                 break;
             }
@@ -2613,6 +2682,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isMiso>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::isLinear: {
@@ -2620,6 +2690,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isLinear>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::isShortFrames: {
@@ -2627,6 +2698,7 @@
                 jobject newBooleanObj = env->NewObject(booleanClazz, initBoolean,
                                                        s.get<FrontendStatus::Tag::isShortFrames>());
                 env->SetObjectField(statusObj, field, newBooleanObj);
+                env->DeleteLocalRef(newBooleanObj);
                 break;
             }
             case FrontendStatus::Tag::isdbtMode: {
@@ -2634,6 +2706,7 @@
                 jobject newIntegerObj =
                         env->NewObject(intClazz, initInt, s.get<FrontendStatus::Tag::isdbtMode>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::partialReceptionFlag: {
@@ -2643,6 +2716,7 @@
                         env->NewObject(intClazz, initInt,
                                        s.get<FrontendStatus::Tag::partialReceptionFlag>());
                 env->SetObjectField(statusObj, field, newIntegerObj);
+                env->DeleteLocalRef(newIntegerObj);
                 break;
             }
             case FrontendStatus::Tag::streamIdList: {
@@ -2653,6 +2727,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::dvbtCellIds: {
@@ -2663,6 +2738,7 @@
                 env->SetIntArrayRegion(valObj, 0, v.size(), reinterpret_cast<jint *>(&ids[0]));
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(valObj);
                 break;
             }
             case FrontendStatus::Tag::allPlpInfo: {
@@ -2678,9 +2754,12 @@
                     jobject plpObj = env->NewObject(plpClazz, initPlp, plpInfos[i].plpId,
                                                     plpInfos[i].bLlsFlag);
                     env->SetObjectArrayElement(valObj, i, plpObj);
+                    env->DeleteLocalRef(plpObj);
                 }
 
                 env->SetObjectField(statusObj, field, valObj);
+                env->DeleteLocalRef(plpClazz);
+                env->DeleteLocalRef(valObj);
                 break;
             }
         }
@@ -2837,6 +2916,7 @@
                 .fec = fec,
         };
         plps[i] = frontendAtsc3PlpSettings;
+        env->DeleteLocalRef(plp);
     }
     return plps;
 }
@@ -3192,6 +3272,7 @@
                 env->GetIntField(layer, env->GetFieldID(layerClazz, "mCodeRate", "I")));
         frontendIsdbtSettings.layerSettings[i].numOfSegment =
                 env->GetIntField(layer, env->GetFieldID(layerClazz, "mNumOfSegments", "I"));
+        env->DeleteLocalRef(layer);
     }
 
     frontendSettings.set<FrontendSettings::Tag::isdbt>(frontendIsdbtSettings);
diff --git a/media/mca/effect/java/android/media/effect/EffectFactory.java b/media/mca/effect/java/android/media/effect/EffectFactory.java
index f6fcba7..cbb2736 100644
--- a/media/mca/effect/java/android/media/effect/EffectFactory.java
+++ b/media/mca/effect/java/android/media/effect/EffectFactory.java
@@ -486,11 +486,9 @@
 
     private Effect instantiateEffect(Class effectClass, String name) {
         // Make sure this is an Effect subclass
-        try {
-            effectClass.asSubclass(Effect.class);
-        } catch (ClassCastException e) {
+        if (!Effect.class.isAssignableFrom(effectClass)) {
             throw new IllegalArgumentException("Attempting to allocate effect '" + effectClass
-                + "' which is not a subclass of Effect!", e);
+                + "' which is not a subclass of Effect!");
         }
 
         // Look for the correct constructor
diff --git a/media/mca/filterfw/java/android/filterfw/core/Filter.java b/media/mca/filterfw/java/android/filterfw/core/Filter.java
index a608ef5..e82c046 100644
--- a/media/mca/filterfw/java/android/filterfw/core/Filter.java
+++ b/media/mca/filterfw/java/android/filterfw/core/Filter.java
@@ -90,9 +90,7 @@
             return false;
         }
         // Then make sure it's a subclass of Filter.
-        try {
-            filterClass.asSubclass(Filter.class);
-        } catch (ClassCastException e) {
+        if (!Filter.class.isAssignableFrom(filterClass)) {
             return false;
         }
         return true;
diff --git a/media/mca/filterfw/java/android/filterfw/core/FilterFactory.java b/media/mca/filterfw/java/android/filterfw/core/FilterFactory.java
index 779df99..736e511 100644
--- a/media/mca/filterfw/java/android/filterfw/core/FilterFactory.java
+++ b/media/mca/filterfw/java/android/filterfw/core/FilterFactory.java
@@ -112,9 +112,7 @@
 
     public Filter createFilterByClass(Class filterClass, String filterName) {
         // Make sure this is a Filter subclass
-        try {
-            filterClass.asSubclass(Filter.class);
-        } catch (ClassCastException e) {
+        if (!Filter.class.isAssignableFrom(filterClass)) {
             throw new IllegalArgumentException("Attempting to allocate class '" + filterClass
                 + "' which is not a subclass of Filter!");
         }
diff --git a/media/mca/filterfw/java/android/filterfw/core/KeyValueMap.java b/media/mca/filterfw/java/android/filterfw/core/KeyValueMap.java
index 8cf9a13..6ff1885 100644
--- a/media/mca/filterfw/java/android/filterfw/core/KeyValueMap.java
+++ b/media/mca/filterfw/java/android/filterfw/core/KeyValueMap.java
@@ -55,12 +55,12 @@
 
     public int getInt(String key) {
         Object result = get(key);
-        return result != null ? (Integer)result : null;
+        return result != null ? (Integer) result : 0;
     }
 
     public float getFloat(String key) {
         Object result = get(key);
-        return result != null ? (Float)result : null;
+        return result != null ? (Float) result : 0;
     }
 
     @Override
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
index a83e7d3..3cbfd50 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java
@@ -212,6 +212,12 @@
                 containsInAnyOrder(isAudioMixSessionCriterion(TEST_SESSION_ID)));
     }
 
+    @Test
+    public void audioMixingRuleWithNoRulesFails() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new AudioMixingRule.Builder().build());
+    }
+
 
     private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) {
         return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") {
diff --git a/media/tests/CameraBrowser/src/com/android/camerabrowser/MtpClient.java b/media/tests/CameraBrowser/src/com/android/camerabrowser/MtpClient.java
index edb5e37..158e698 100644
--- a/media/tests/CameraBrowser/src/com/android/camerabrowser/MtpClient.java
+++ b/media/tests/CameraBrowser/src/com/android/camerabrowser/MtpClient.java
@@ -158,7 +158,7 @@
         filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
         filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
         filter.addAction(ACTION_USB_PERMISSION);
-        context.registerReceiver(mUsbReceiver, filter);
+        context.registerReceiver(mUsbReceiver, filter, Context.RECEIVER_EXPORTED/*UNAUDITED*/);
     }
 
     /**
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/performance/MediaPlayerPerformance.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/performance/MediaPlayerPerformance.java
index c5281657..8c05725 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/performance/MediaPlayerPerformance.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/performance/MediaPlayerPerformance.java
@@ -296,7 +296,7 @@
                 mMemWriter.write("End Memory :" + mEndMemory + "\n");
             }
         } catch (Exception e) {
-            e.toString();
+            // TODO
         }
     }
 
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraMetadataTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraMetadataTest.java
index 39add7e..c814eba 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraMetadataTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/CameraMetadataTest.java
@@ -264,8 +264,14 @@
                 builder.append("**");
             }
 
-            if (elem instanceof Number) {
-                builder.append(String.format("%x", elem));
+            if (elem instanceof Byte) {
+                builder.append(String.format("%x", (Byte) elem));
+            } else if (elem instanceof Short) {
+                builder.append(String.format("%x", (Short) elem));
+            } else if (elem instanceof Integer) {
+                builder.append(String.format("%x", (Integer) elem));
+            } else if (elem instanceof Long) {
+                builder.append(String.format("%x", (Long) elem));
             } else {
                 builder.append(elem);
             }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerGetCurrentPositionStateUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerGetCurrentPositionStateUnitTest.java
index fd1c2d3..37dd4b5 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerGetCurrentPositionStateUnitTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaPlayerGetCurrentPositionStateUnitTest.java
@@ -18,7 +18,7 @@
 
 import android.media.MediaPlayer;
 import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;;
+import android.test.suitebuilder.annotation.LargeTest;
 
 /**
  * Unit test class to test the set of valid and invalid states that
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index 810b408..4193ffa 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -384,7 +384,7 @@
         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
         assertThat(routeToSelect).isNotNull();
 
-        mManager.selectRoute(mPackageName, routeToSelect);
+        mManager.transfer(mPackageName, routeToSelect);
         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
         assertThat(mManager.getRemoteSessions()).hasSize(1);
     }
@@ -410,7 +410,7 @@
 
         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
 
-        mManager.selectRoute(mPackageName, routeToSelect);
+        mManager.transfer(mPackageName, routeToSelect);
         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
@@ -514,7 +514,7 @@
             }
         });
         awaitOnRouteChangedManager(
-                () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1)),
+                () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID1)),
                 ROUTE_ID1,
                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
@@ -525,7 +525,7 @@
         RoutingSessionInfo sessionInfo = sessions.get(1);
 
         awaitOnRouteChangedManager(
-                () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)),
+                () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)),
                 ROUTE_ID5_TO_TRANSFER_TO,
                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
 
@@ -583,9 +583,9 @@
         assertThat(route1).isNotNull();
         assertThat(route2).isNotNull();
 
-        mManager.selectRoute(mPackageName, route1);
+        mManager.transfer(mPackageName, route1);
         assertThat(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
-        mManager.selectRoute(mPackageName, route2);
+        mManager.transfer(mPackageName, route2);
         assertThat(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         // onTransferFailed/onSessionReleased should not be called.
@@ -703,7 +703,7 @@
             }
         });
 
-        mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1));
+        mManager.transfer(mPackageName, routes.get(ROUTE_ID1));
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
@@ -858,7 +858,7 @@
         });
 
         mRouter2.setOnGetControllerHintsListener(listener);
-        mManager.selectRoute(mPackageName, route);
+        mManager.transfer(mPackageName, route);
         assertThat(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
         assertThat(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
 
@@ -903,7 +903,7 @@
             }
         });
 
-        mManager.selectRoute(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
+        mManager.transfer(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
     }
 
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index cb0f22f..9b0f020 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -238,7 +238,7 @@
     ASurfaceControl_createFromWindow; # introduced=29
     ASurfaceControl_acquire; # introduced=31
     ASurfaceControl_release; # introduced=29
-    ASurfaceControl_fromSurfaceControl; # introduced=34
+    ASurfaceControl_fromJava; # introduced=34
     ASurfaceTexture_acquireANativeWindow; # introduced=28
     ASurfaceTexture_attachToGLContext; # introduced=28
     ASurfaceTexture_detachFromGLContext; # introduced=28
@@ -256,7 +256,7 @@
     ASurfaceTransaction_apply; # introduced=29
     ASurfaceTransaction_create; # introduced=29
     ASurfaceTransaction_delete; # introduced=29
-    ASurfaceTransaction_fromTransaction; # introduced=34
+    ASurfaceTransaction_fromJava; # introduced=34
     ASurfaceTransaction_reparent; # introduced=29
     ASurfaceTransaction_setBuffer; # introduced=29
     ASurfaceTransaction_setBufferAlpha; # introduced=29
@@ -330,6 +330,7 @@
     APerformanceHint_updateTargetWorkDuration; # introduced=Tiramisu
     APerformanceHint_reportActualWorkDuration; # introduced=Tiramisu
     APerformanceHint_closeSession; # introduced=Tiramisu
+    APerformanceHint_sendHint; # introduced=UpsideDownCake
   local:
     *;
 };
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index d627984..7863a7d 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -61,6 +61,7 @@
 
     int updateTargetWorkDuration(int64_t targetDurationNanos);
     int reportActualWorkDuration(int64_t actualDurationNanos);
+    int sendHint(int32_t hint);
 
 private:
     friend struct APerformanceHintManager;
@@ -159,7 +160,7 @@
     }
     binder::Status ret = mHintSession->updateTargetWorkDuration(targetDurationNanos);
     if (!ret.isOk()) {
-        ALOGE("%s: HintSessionn updateTargetWorkDuration failed: %s", __FUNCTION__,
+        ALOGE("%s: HintSession updateTargetWorkDuration failed: %s", __FUNCTION__,
               ret.exceptionMessage().c_str());
         return EPIPE;
     }
@@ -205,6 +206,21 @@
     return 0;
 }
 
+int APerformanceHintSession::sendHint(int32_t hint) {
+    if (hint < 0) {
+        ALOGE("%s: session hint value must be greater than zero", __FUNCTION__);
+        return EINVAL;
+    }
+
+    binder::Status ret = mHintSession->sendHint(hint);
+
+    if (!ret.isOk()) {
+        ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.exceptionMessage().c_str());
+        return EPIPE;
+    }
+    return 0;
+}
+
 // ===================================== C API
 APerformanceHintManager* APerformanceHint_getManager() {
     return APerformanceHintManager::getInstance();
@@ -230,6 +246,10 @@
     return session->reportActualWorkDuration(actualDurationNanos);
 }
 
+int APerformanceHint_sendHint(APerformanceHintSession* session, int32_t hint) {
+    return session->sendHint(hint);
+}
+
 void APerformanceHint_closeSession(APerformanceHintSession* session) {
     delete session;
 }
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 8913799..ea20c6c 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -138,17 +138,14 @@
     SurfaceControl_release(surfaceControl);
 }
 
-ASurfaceControl* ASurfaceControl_fromSurfaceControl(JNIEnv* env, jobject surfaceControlObj) {
-    LOG_ALWAYS_FATAL_IF(!env,
-                        "nullptr passed to ASurfaceControl_fromSurfaceControl as env argument");
+ASurfaceControl* ASurfaceControl_fromJava(JNIEnv* env, jobject surfaceControlObj) {
+    LOG_ALWAYS_FATAL_IF(!env, "nullptr passed to ASurfaceControl_fromJava as env argument");
     LOG_ALWAYS_FATAL_IF(!surfaceControlObj,
-                        "nullptr passed to ASurfaceControl_fromSurfaceControl as surfaceControlObj "
-                        "argument");
+                        "nullptr passed to ASurfaceControl_fromJava as surfaceControlObj argument");
     SurfaceControl* surfaceControl =
             android_view_SurfaceControl_getNativeSurfaceControl(env, surfaceControlObj);
     LOG_ALWAYS_FATAL_IF(!surfaceControl,
-                        "surfaceControlObj passed to ASurfaceControl_fromSurfaceControl is not "
-                        "valid");
+                        "surfaceControlObj passed to ASurfaceControl_fromJava is not valid");
     SurfaceControl_acquire(surfaceControl);
     return reinterpret_cast<ASurfaceControl*>(surfaceControl);
 }
@@ -209,17 +206,15 @@
     delete transaction;
 }
 
-ASurfaceTransaction* ASurfaceTransaction_fromTransaction(JNIEnv* env, jobject transactionObj) {
-    LOG_ALWAYS_FATAL_IF(!env,
-                        "nullptr passed to ASurfaceTransaction_fromTransaction as env argument");
+ASurfaceTransaction* ASurfaceTransaction_fromJava(JNIEnv* env, jobject transactionObj) {
+    LOG_ALWAYS_FATAL_IF(!env, "nullptr passed to ASurfaceTransaction_fromJava as env argument");
     LOG_ALWAYS_FATAL_IF(!transactionObj,
-                        "nullptr passed to ASurfaceTransaction_fromTransaction as transactionObj "
+                        "nullptr passed to ASurfaceTransaction_fromJava as transactionObj "
                         "argument");
     Transaction* transaction =
             android_view_SurfaceTransaction_getNativeSurfaceTransaction(env, transactionObj);
     LOG_ALWAYS_FATAL_IF(!transaction,
-                        "surfaceControlObj passed to ASurfaceTransaction_fromTransaction is not "
-                        "valid");
+                        "surfaceControlObj passed to ASurfaceTransaction_fromJava is not valid");
     return reinterpret_cast<ASurfaceTransaction*>(transaction);
 }
 
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index b17850e..1881e60 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -51,6 +51,7 @@
                 (const ::std::vector<int64_t>& actualDurationNanos,
                  const ::std::vector<int64_t>& timeStampNanos),
                 (override));
+    MOCK_METHOD(Status, sendHint, (int32_t hints), (override));
     MOCK_METHOD(Status, close, (), (override));
     MOCK_METHOD(IBinder*, onAsBinder, (), (override));
 };
@@ -121,6 +122,15 @@
     result = APerformanceHint_reportActualWorkDuration(session, -1L);
     EXPECT_EQ(EINVAL, result);
 
+    // Send both valid and invalid session hints
+    int hintId = 2;
+    EXPECT_CALL(*iSession, sendHint(Eq(2))).Times(Exactly(1));
+    result = APerformanceHint_sendHint(session, hintId);
+    EXPECT_EQ(0, result);
+
+    result = APerformanceHint_sendHint(session, -1);
+    EXPECT_EQ(EINVAL, result);
+
     EXPECT_CALL(*iSession, close()).Times(Exactly(1));
     APerformanceHint_closeSession(session);
 }
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 1709dfd..10c570b 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -93,7 +93,7 @@
     ],
     static_libs: ["libarect"],
     fuzz_config: {
-        cc: ["scroggo@google.com"],
+        cc: ["dichenzhang@google.com","scroggo@google.com"],
         asan_options: [
             "detect_odr_violation=1",
         ],
diff --git a/opengl/java/android/opengl/GLSurfaceView.java b/opengl/java/android/opengl/GLSurfaceView.java
index 75131b0..4738318 100644
--- a/opengl/java/android/opengl/GLSurfaceView.java
+++ b/opengl/java/android/opengl/GLSurfaceView.java
@@ -1667,7 +1667,15 @@
                 mWantRenderNotification = true;
                 mRequestRender = true;
                 mRenderComplete = false;
-                mFinishDrawingRunnable = finishDrawing;
+                final Runnable oldCallback = mFinishDrawingRunnable;
+                mFinishDrawingRunnable = () -> {
+                    if (oldCallback != null) {
+                        oldCallback.run();
+                    }
+                    if (finishDrawing != null) {
+                        finishDrawing.run();
+                    }
+                };
 
                 sGLThreadManager.notifyAll();
             }
diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp
index fc753da..a216381 100644
--- a/packages/CarrierDefaultApp/Android.bp
+++ b/packages/CarrierDefaultApp/Android.bp
@@ -10,6 +10,12 @@
 android_app {
     name: "CarrierDefaultApp",
     srcs: ["src/**/*.java"],
+    libs: ["SlicePurchaseController"],
     platform_apis: true,
     certificate: "platform",
+    optimize: {
+        proguard_flags_files: [
+            "proguard.flags",
+        ],
+    },
 }
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index 632dfb3..c4bb17c 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -28,12 +28,14 @@
     <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" />
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application
         android:label="@string/app_name"
         android:directBootAware="true"
         android:usesCleartextTraffic="true"
-        android:icon="@mipmap/ic_launcher_android">
+        android:icon="@mipmap/ic_launcher_android"
+        android:debuggable="true">
         <receiver android:name="com.android.carrierdefaultapp.CarrierDefaultBroadcastReceiver"
             android:exported="true">
             <intent-filter>
@@ -71,5 +73,22 @@
                 <data android:host="*" />
             </intent-filter>
         </activity-alias>
+
+        <receiver android:name="com.android.carrierdefaultapp.SlicePurchaseBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.phone.slice.action.START_SLICE_PURCHASE_APP" />
+                <action android:name="com.android.phone.slice.action.SLICE_PURCHASE_APP_RESPONSE_TIMEOUT" />
+                <action android:name="com.android.phone.slice.action.NOTIFICATION_CANCELED" />
+            </intent-filter>
+        </receiver>
+        <activity android:name="com.android.carrierdefaultapp.SlicePurchaseActivity"
+                  android:label="@string/slice_purchase_app_label"
+                  android:exported="true"
+                  android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.html b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
new file mode 100644
index 0000000..67d2184
--- /dev/null
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
@@ -0,0 +1,79 @@
+<!--
+  ~ 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.
+  -->
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="description" content="
+    This is a HTML page that calls and verifies responses from the @JavascriptInterface functions of
+    SlicePurchaseWebInterface. Test slice purchase application behavior using ADB shell commands and
+    the APIs below:
+
+    FROM TERMINAL:
+    Allow device to override carrier configs:
+    $ adb root
+    Set PREMIUM_CAPABILITY_PRIORITIZE_LATENCY enabled:
+    $ adb shell cmd phone cc set-value -p supported_premium_capabilities_int_array 34
+    Set the carrier purchase URL to this test HTML file:
+    $ adb shell cmd phone cc set-value -p premium_capability_purchase_url_string \
+      file:///android_asset/slice_purchase_test.html
+    OPTIONAL: Allow premium capability purchase on LTE:
+    $ adb shell cmd phone cc set-value -p premium_capability_supported_on_lte_bool true
+    OPTIONAL: Override ServiceState to fake a NR SA connection:
+    $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --ei data_rat 20
+
+    FROM TEST ACTIVITY:
+    TelephonyManager tm = getApplicationContext().getSystemService(TelephonyManager.class)
+    tm.isPremiumCapabilityAvailable(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+    LinkedBlockingQueue<Integer> purchaseRequests = new LinkedBlockingQueue<>();
+    tm.purchasePremiumCapability(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY,
+            this.getMainExecutor(), request::offer);
+
+    When the test application starts, this HTML will be loaded into the WebView along with the
+    associated JavaScript functions in file:///android_asset/slice_purchase_test.js.
+    Click on the buttons in the HTML to call the corresponding @JavascriptInterface APIs.
+
+    RESET DEVICE STATE:
+    Clear carrier configurations that were set:
+    $ adb shell cmd phone cc clear-values
+    Clear ServiceState override that was set:
+    $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --es action reset
+    ">
+    <title>Test SlicePurchaseActivity</title>
+    <script type="text/javascript" src="slice_purchase_test.js"></script>
+</head>
+<body>
+    <h1>Test SlicePurchaseActivity</h1>
+    <h2>Get requested premium capability</h2>
+    <button type="button" onclick="testGetRequestedCapability()">
+        Get requested premium capability
+    </button>
+    <p id="requested_capability"></p>
+
+    <h2>Notify purchase successful</h2>
+    <button type="button" onclick="testNotifyPurchaseSuccessful(60000)">
+        Notify purchase successful for 1 minute
+    </button>
+    <p id="purchase_successful"></p>
+
+    <h2>Notify purchase failed</h2>
+    <button type="button" onclick="testNotifyPurchaseFailed(2, 'FAILURE_CODE_SERVER_UNREACHABLE')">
+        Notify purchase failed
+    </button>
+    <p id="purchase_failed"></p>
+</body>
+</html>
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.js b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
new file mode 100644
index 0000000..02c4fea
--- /dev/null
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+function testGetRequestedCapability() {
+    let capability = SlicePurchaseWebInterface.getRequestedCapability();
+    document.getElementById("requested_capability").innerHTML =
+            "Premium capability requested: " + capability;
+}
+
+function testNotifyPurchaseSuccessful(duration_ms_long = 0) {
+    SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+    document.getElementById("purchase_successful").innerHTML =
+            "Notified purchase success for duration: " + duration_ms_long;
+}
+
+function testNotifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
+    SlicePurchaseWebInterface.notifyPurchaseFailed(failure_code, failure_reason);
+    document.getElementById("purchase_failed").innerHTML =
+            "Notified purchase failed.";
+}
diff --git a/packages/CarrierDefaultApp/proguard.flags b/packages/CarrierDefaultApp/proguard.flags
new file mode 100644
index 0000000..64fec2c
--- /dev/null
+++ b/packages/CarrierDefaultApp/proguard.flags
@@ -0,0 +1,4 @@
+# Keep classes and methods that have the @JavascriptInterface annotation
+-keepclassmembers class * {
+    @android.webkit.JavascriptInterface <methods>;
+}
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
new file mode 100644
index 0000000..ad8a21c
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path android:fillColor="@android:color/white"
+          android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index 65a7cec..3dcdf00 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -13,4 +13,18 @@
     <string name="ssl_error_warning">The network you&#8217;re trying to join has security issues.</string>
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
+
+    <!-- Telephony notification channel name for network boost notifications. -->
+    <string name="network_boost_notification_channel">Network boost</string>
+    <!-- Notification title text for the network boost notification. -->
+    <string name="network_boost_notification_title">%s recommends a data boost</string>
+    <!-- Notification detail text for the network boost notification. -->
+    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
+    <!-- Notification button text to cancel the network boost notification. -->
+    <string name="network_boost_notification_button_not_now">Not now</string>
+    <!-- Notification button text to manage the network boost notification. -->
+    <string name="network_boost_notification_button_manage">Manage</string>
+
+    <!-- Label to display when the slice purchase application opens. -->
+    <string name="slice_purchase_app_label">Purchase a network boost.</string>
 </resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
new file mode 100644
index 0000000..c524037
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.webkit.URLUtil;
+import android.webkit.WebView;
+
+import com.android.phone.slice.SlicePurchaseController;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity that launches when the user clicks on the network boost notification.
+ * This will open a {@link WebView} for the carrier website to allow the user to complete the
+ * premium capability purchase.
+ * The carrier website can get the requested premium capability using the JavaScript interface
+ * method {@code SlicePurchaseWebInterface.getRequestedCapability()}.
+ * If the purchase is successful, the carrier website shall notify the slice purchase application
+ * using the JavaScript interface method
+ * {@code SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration)}, where {@code duration} is
+ * the optional duration of the network boost.
+ * If the purchase was not successful, the carrier website shall notify the slice purchase
+ * application using the JavaScript interface method
+ * {@code SlicePurchaseWebInterface.notifyPurchaseFailed(code, reason)}, where {@code code} is the
+ * {@link SlicePurchaseController.FailureCode} indicating the reason for failure and {@code reason}
+ * is the human-readable reason for failure if the failure code is
+ * {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN}.
+ * If either of these notification methods are not called, the purchase cannot be completed
+ * successfully and the purchase request will eventually time out.
+ */
+public class SlicePurchaseActivity extends Activity {
+    private static final String TAG = "SlicePurchaseActivity";
+
+    @NonNull private WebView mWebView;
+    @NonNull private Context mApplicationContext;
+    @NonNull private Intent mIntent;
+    @Nullable private URL mUrl;
+    private int mSubId;
+    @TelephonyManager.PremiumCapability protected int mCapability;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIntent = getIntent();
+        mSubId = mIntent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        mCapability = mIntent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        mApplicationContext = getApplicationContext();
+        mUrl = getUrl();
+        logd("onCreate: subId=" + mSubId + ", capability="
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                + ", url=" + mUrl);
+
+        // Cancel network boost notification
+        mApplicationContext.getSystemService(NotificationManager.class)
+                .cancel(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+
+        // Verify intent and values are valid
+        if (!SlicePurchaseBroadcastReceiver.isIntentValid(mIntent)) {
+            loge("Not starting SlicePurchaseActivity with an invalid Intent: " + mIntent);
+            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                    mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mUrl == null) {
+            String error = "Unable to create a URL from carrier configs.";
+            loge(error);
+            Intent data = new Intent();
+            data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE,
+                    SlicePurchaseController.FAILURE_CODE_CARRIER_URL_UNAVAILABLE);
+            data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, error);
+            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
+                    mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) {
+            loge("Unable to start the slice purchase application on the non-default data "
+                    + "subscription: " + mSubId);
+            SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                    mIntent, SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION);
+            finishAndRemoveTask();
+            return;
+        }
+
+        // Create a reference to this activity in SlicePurchaseBroadcastReceiver
+        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(mCapability, this);
+
+        // Create and configure WebView
+        setupWebView();
+    }
+
+    protected void onPurchaseSuccessful(long duration) {
+        logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium "
+                + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                + (duration > 0 ? " for " + TimeUnit.MILLISECONDS.toMinutes(duration) + " minutes."
+                : "."));
+        Intent intent = new Intent();
+        intent.putExtra(SlicePurchaseController.EXTRA_PURCHASE_DURATION, duration);
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
+                mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS, intent);
+        finishAndRemoveTask();
+    }
+
+    protected void onPurchaseFailed(@SlicePurchaseController.FailureCode int failureCode,
+            @Nullable String failureReason) {
+        logd("onPurchaseFailed: Carrier website indicated purchase failed for premium capability "
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability) + " with code: "
+                + failureCode + " and reason: " + failureReason);
+        Intent data = new Intent();
+        data.putExtra(SlicePurchaseController.EXTRA_FAILURE_CODE, failureCode);
+        data.putExtra(SlicePurchaseController.EXTRA_FAILURE_REASON, failureReason);
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
+                mIntent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR, data);
+        finishAndRemoveTask();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+        // Pressing back in the WebView will go to the previous page instead of closing
+        //  the slice purchase application.
+        if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
+            mWebView.goBack();
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    protected void onDestroy() {
+        logd("onDestroy: User canceled the purchase by closing the application.");
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+                mIntent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
+        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(mCapability);
+        super.onDestroy();
+    }
+
+    @Nullable private URL getUrl() {
+        String url = mApplicationContext.getSystemService(CarrierConfigManager.class)
+                .getConfigForSubId(mSubId).getString(
+                        CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+        boolean isUrlValid = URLUtil.isValidUrl(url);
+        if (URLUtil.isAssetUrl(url)) {
+            isUrlValid = url.equals(SlicePurchaseController.SLICE_PURCHASE_TEST_FILE);
+        }
+        if (isUrlValid) {
+            try {
+                return new URL(url);
+            } catch (MalformedURLException ignored) {
+            }
+        }
+        loge("Invalid URL: " + url);
+        return null;
+    }
+
+    private void setupWebView() {
+        // Create WebView
+        mWebView = new WebView(this);
+
+        // Enable JavaScript for the carrier purchase website to send results back to
+        //  the slice purchase application.
+        mWebView.getSettings().setJavaScriptEnabled(true);
+        mWebView.addJavascriptInterface(
+                new SlicePurchaseWebInterface(this), "SlicePurchaseWebInterface");
+
+        // Display WebView
+        setContentView(mWebView);
+
+        // Load the URL
+        mWebView.loadUrl(mUrl.toString());
+    }
+
+    private static void logd(@NonNull String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(@NonNull String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
new file mode 100644
index 0000000..b322b8b
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+import android.telephony.AnomalyReporter;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.phone.slice.SlicePurchaseController;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * The SlicePurchaseBroadcastReceiver listens for
+ * {@link SlicePurchaseController#ACTION_START_SLICE_PURCHASE_APP} from the SlicePurchaseController
+ * in the phone process to start the slice purchase application. It displays the network boost
+ * notification to the user and will start the {@link SlicePurchaseActivity} to display the
+ * {@link WebView} to purchase network boosts from the user's carrier.
+ */
+public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
+    private static final String TAG = "SlicePurchaseBroadcastReceiver";
+
+    /**
+     * UUID to report an anomaly when receiving a PendingIntent from an application or process
+     * other than the Phone process.
+     */
+    private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53";
+
+    /** Weak references to {@link SlicePurchaseActivity} for each capability, if it exists. */
+    private static final Map<Integer, WeakReference<SlicePurchaseActivity>>
+            sSlicePurchaseActivities = new HashMap<>();
+
+    /** Channel ID for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
+    /** Tag for the network boost notification. */
+    public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SlicePurchaseApp.Notification";
+    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_CANCELED =
+            "com.android.phone.slice.action.NOTIFICATION_CANCELED";
+
+    /**
+     * Create a weak reference to {@link SlicePurchaseActivity}. The reference will be removed when
+     * {@link SlicePurchaseActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     * @param slicePurchaseActivity The instance of SlicePurchaseActivity.
+     */
+    public static void updateSlicePurchaseActivity(
+            @TelephonyManager.PremiumCapability int capability,
+            @NonNull SlicePurchaseActivity slicePurchaseActivity) {
+        sSlicePurchaseActivities.put(capability, new WeakReference<>(slicePurchaseActivity));
+    }
+
+    /**
+     * Remove the weak reference to {@link SlicePurchaseActivity} when
+     * {@link SlicePurchaseActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     */
+    public static void removeSlicePurchaseActivity(
+            @TelephonyManager.PremiumCapability int capability) {
+        sSlicePurchaseActivities.remove(capability);
+    }
+
+    /**
+     * Send the PendingIntent containing the corresponding slice purchase application response.
+     *
+     * @param intent The Intent containing the PendingIntent extra.
+     * @param extra The extra to get the PendingIntent to send.
+     */
+    public static void sendSlicePurchaseAppResponse(@NonNull Intent intent, @NonNull String extra) {
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("PendingIntent does not exist for extra: " + extra);
+            return;
+        }
+        try {
+            pendingIntent.send();
+        } catch (PendingIntent.CanceledException e) {
+            loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+        }
+    }
+
+    /**
+     * Send the PendingIntent containing the corresponding slice purchase application response
+     * with additional data.
+     *
+     * @param context The Context to use to send the PendingIntent.
+     * @param intent The Intent containing the PendingIntent extra.
+     * @param extra The extra to get the PendingIntent to send.
+     * @param data The Intent containing additional data to send with the PendingIntent.
+     */
+    public static void sendSlicePurchaseAppResponseWithData(@NonNull Context context,
+            @NonNull Intent intent, @NonNull String extra, @NonNull Intent data) {
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("PendingIntent does not exist for extra: " + extra);
+            return;
+        }
+        try {
+            pendingIntent.send(context, 0 /* unused */, data);
+        } catch (PendingIntent.CanceledException e) {
+            loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+        }
+    }
+
+    /**
+     * Check whether the Intent is valid and can be used to complete purchases in the slice purchase
+     * application. This checks that all necessary extras exist and that the values are valid.
+     *
+     * @param intent The intent to check
+     * @return {@code true} if the intent is valid and {@code false} otherwise.
+     */
+    public static boolean isIntentValid(@NonNull Intent intent) {
+        int phoneId = intent.getIntExtra(SlicePurchaseController.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
+            loge("isIntentValid: invalid phone index: " + phoneId);
+            return false;
+        }
+
+        int subId = intent.getIntExtra(SlicePurchaseController.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            loge("isIntentValid: invalid subscription ID: " + subId);
+            return false;
+        }
+
+        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        if (capability == SlicePurchaseController.PREMIUM_CAPABILITY_INVALID) {
+            loge("isIntentValid: invalid premium capability: " + capability);
+            return false;
+        }
+
+        String appName = intent.getStringExtra(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME);
+        if (TextUtils.isEmpty(appName)) {
+            loge("isIntentValid: empty requesting application name: " + appName);
+            return false;
+        }
+
+        return isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED)
+                && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR)
+                && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED)
+                && isPendingIntentValid(intent,
+                        SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION)
+                && isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_SUCCESS);
+    }
+
+    private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
+        String intentType = getPendingIntentType(extra);
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("isPendingIntentValid: " + intentType + " intent not found.");
+            return false;
+        }
+        String creatorPackage = pendingIntent.getCreatorPackage();
+        if (!creatorPackage.equals(TelephonyManager.PHONE_PROCESS_NAME)) {
+            String logStr = "isPendingIntentValid: " + intentType + " intent was created by "
+                    + creatorPackage + " instead of the phone process.";
+            loge(logStr);
+            AnomalyReporter.reportAnomaly(UUID.fromString(UUID_BAD_PENDING_INTENT), logStr);
+            return false;
+        }
+        if (!pendingIntent.isBroadcast()) {
+            loge("isPendingIntentValid: " + intentType + " intent is not a broadcast.");
+            return false;
+        }
+        return true;
+    }
+
+    @NonNull private static String getPendingIntentType(@NonNull String extra) {
+        switch (extra) {
+            case SlicePurchaseController.EXTRA_INTENT_CANCELED: return "canceled";
+            case SlicePurchaseController.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
+            case SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
+            case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+                return "not default data subscription";
+            case SlicePurchaseController.EXTRA_INTENT_SUCCESS: return "success";
+            default: {
+                loge("Unknown pending intent extra: " + extra);
+                return "unknown(" + extra + ")";
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        logd("onReceive intent: " + intent.getAction());
+        switch (intent.getAction()) {
+            case SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP:
+                onDisplayBoosterNotification(context, intent);
+                break;
+            case SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT:
+                onTimeout(context, intent);
+                break;
+            case ACTION_NOTIFICATION_CANCELED:
+                onUserCanceled(context, intent);
+                break;
+            default:
+                loge("Received unknown action: " + intent.getAction());
+        }
+    }
+
+    private void onDisplayBoosterNotification(@NonNull Context context, @NonNull Intent intent) {
+        if (!isIntentValid(intent)) {
+            sendSlicePurchaseAppResponse(intent,
+                    SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
+            return;
+        }
+
+        NotificationChannel channel = new NotificationChannel(
+                NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
+                context.getResources().getString(R.string.network_boost_notification_channel),
+                NotificationManager.IMPORTANCE_DEFAULT);
+        // CarrierDefaultApp notifications are unblockable by default. Make this channel blockable
+        //  to allow users to disable notifications posted to this channel without affecting other
+        //  notifications in this application.
+        channel.setBlockable(true);
+        context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
+
+        Notification notification =
+                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(String.format(context.getResources().getString(
+                                R.string.network_boost_notification_title),
+                                intent.getStringExtra(
+                                        SlicePurchaseController.EXTRA_REQUESTING_APP_NAME)))
+                        .setContentText(context.getResources().getString(
+                                R.string.network_boost_notification_detail))
+                        .setSmallIcon(R.drawable.ic_network_boost)
+                        .setContentIntent(createContentIntent(context, intent, 1))
+                        .setDeleteIntent(intent.getParcelableExtra(
+                                SlicePurchaseController.EXTRA_INTENT_CANCELED, PendingIntent.class))
+                        // Add an action for the "Not now" button, which has the same behavior as
+                        // the user canceling or closing the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_not_now),
+                                createCanceledIntent(context, intent)).build())
+                        // Add an action for the "Manage" button, which has the same behavior as
+                        // the user clicking on the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_manage),
+                                createContentIntent(context, intent, 2)).build())
+                        .build();
+
+        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        logd("Display the booster notification for capability "
+                + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class).notifyAsUser(
+                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
+    }
+
+    /**
+     * Create the intent for when the user clicks on the "Manage" button on the network boost
+     * notification or the notification itself. This will open {@link SlicePurchaseActivity}.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the slice purchase application.
+     * @param requestCode The request code for the PendingIntent.
+     *
+     * @return The intent to start {@link SlicePurchaseActivity}.
+     */
+    @VisibleForTesting
+    @NonNull public PendingIntent createContentIntent(@NonNull Context context,
+            @NonNull Intent intent, int requestCode) {
+        Intent i = new Intent(context, SlicePurchaseActivity.class);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SlicePurchaseActivity"));
+        i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
+                | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        i.putExtras(intent);
+        return PendingIntent.getActivityAsUser(context, requestCode, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */,
+                UserHandle.CURRENT);
+    }
+
+    /**
+     * Create the canceled intent for when the user clicks the "Not now" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function
+     * as if the user had canceled or removed the notification.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the slice purchase application.
+     *
+     * @return The canceled intent.
+     */
+    @VisibleForTesting
+    @NonNull public PendingIntent createCanceledIntent(@NonNull Context context,
+            @NonNull Intent intent) {
+        Intent i = new Intent(ACTION_NOTIFICATION_CANCELED);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SlicePurchaseBroadcastReceiver"));
+        i.putExtras(intent);
+        return PendingIntent.getBroadcast(context, 0, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void onTimeout(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
+                + " timed out.");
+        if (sSlicePurchaseActivities.get(capability) == null) {
+            // Notification is still active
+            logd("Closing booster notification since the user did not respond in time.");
+            context.getSystemService(NotificationManager.class).cancelAsUser(
+                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        } else {
+            // Notification was dismissed but SlicePurchaseActivity is still active
+            logd("Closing slice purchase application WebView since the user did not complete the "
+                    + "purchase in time.");
+            sSlicePurchaseActivities.get(capability).get().finishAndRemoveTask();
+        }
+    }
+
+    private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
+        logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class)
+                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        sendSlicePurchaseAppResponse(intent, SlicePurchaseController.EXTRA_INTENT_CANCELED);
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
new file mode 100644
index 0000000..8547898
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.webkit.JavascriptInterface;
+
+import com.android.phone.slice.SlicePurchaseController;
+
+/**
+ * Slice purchase web interface class allowing carrier websites to send responses back to the
+ * slice purchase application using JavaScript.
+ */
+public class SlicePurchaseWebInterface {
+    @NonNull SlicePurchaseActivity mActivity;
+
+    public SlicePurchaseWebInterface(@NonNull SlicePurchaseActivity activity) {
+        mActivity = activity;
+    }
+
+    /**
+     * Interface method allowing the carrier website to get the premium capability
+     * that was requested to purchase.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function getRequestedCapability(duration) {
+     *         SlicePurchaseWebInterface.getRequestedCapability();
+     *     }
+     * </script>
+     */
+    @JavascriptInterface
+    @TelephonyManager.PremiumCapability public int getRequestedCapability() {
+        return mActivity.mCapability;
+    }
+
+    /**
+     * Interface method allowing the carrier website to notify the slice purchase application of
+     * a successful premium capability purchase and the duration for which the premium capability is
+     * purchased.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function notifyPurchaseSuccessful(duration_ms_long = 0) {
+     *         SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+     *     }
+     * </script>
+     *
+     * @param duration The duration for which the premium capability is purchased in milliseconds.
+     */
+    @JavascriptInterface
+    public void notifyPurchaseSuccessful(long duration) {
+        mActivity.onPurchaseSuccessful(duration);
+    }
+
+    /**
+     * Interface method allowing the carrier website to notify the slice purchase application of
+     * a failed premium capability purchase.
+     *
+     * This can be called using the JavaScript below:
+     * <script type="text/javascript">
+     *     function notifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
+     *         SlicePurchaseWebInterface.notifyPurchaseFailed();
+     *     }
+     * </script>
+     *
+     * @param failureCode The failure code.
+     * @param failureReason If the failure code is
+     *                      {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN},
+     *                      the human-readable reason for failure.
+     */
+    @JavascriptInterface
+    public void notifyPurchaseFailed(@SlicePurchaseController.FailureCode int failureCode,
+            @Nullable String failureReason) {
+        mActivity.onPurchaseFailed(failureCode, failureReason);
+    }
+}
diff --git a/packages/CarrierDefaultApp/tests/unit/Android.bp b/packages/CarrierDefaultApp/tests/unit/Android.bp
index 54c9016..cdf7957 100644
--- a/packages/CarrierDefaultApp/tests/unit/Android.bp
+++ b/packages/CarrierDefaultApp/tests/unit/Android.bp
@@ -27,11 +27,13 @@
     libs: [
         "android.test.runner",
         "android.test.base",
+        "SlicePurchaseController",
     ],
     static_libs: [
         "androidx.test.rules",
-        "mockito-target-minus-junit4",
+        "mockito-target-inline-minus-junit4",
     ],
+    jni_libs: ["libdexmakerjvmtiagent"],
     // Include all test java files.
     srcs: ["src/**/*.java"],
     platform_apis: true,
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
new file mode 100644
index 0000000..cecc86d
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.test.ActivityUnitTestCase;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.phone.slice.SlicePurchaseController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class SlicePurchaseActivityTest extends ActivityUnitTestCase<SlicePurchaseActivity> {
+    private static final String TAG = "SlicePurchaseActivityTest";
+    private static final String URL = "file:///android_asset/slice_purchase_test.html";
+    private static final int PHONE_ID = 0;
+
+    @Mock PendingIntent mPendingIntent;
+    @Mock PendingIntent mCanceledIntent;
+    @Mock CarrierConfigManager mCarrierConfigManager;
+    @Mock NotificationManager mNotificationManager;
+    @Mock PersistableBundle mPersistableBundle;
+
+    private SlicePurchaseActivity mSlicePurchaseActivity;
+    private Context mContext;
+
+    public SlicePurchaseActivityTest() {
+        super(SlicePurchaseActivity.class);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+
+        // setup context
+        mContext = spy(getInstrumentation().getTargetContext());
+        doReturn(mCarrierConfigManager).when(mContext)
+                .getSystemService(eq(CarrierConfigManager.class));
+        doReturn(URL).when(mPersistableBundle).getString(
+                CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+        doReturn(mPersistableBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
+        doReturn(mNotificationManager).when(mContext)
+                .getSystemService(eq(NotificationManager.class));
+        doReturn(mContext).when(mContext).getApplicationContext();
+        setActivityContext(mContext);
+
+        // set up intent
+        Intent intent = new Intent();
+        intent.putExtra(SlicePurchaseController.EXTRA_PHONE_ID, PHONE_ID);
+        intent.putExtra(SlicePurchaseController.EXTRA_SUB_ID,
+                SubscriptionManager.getDefaultDataSubscriptionId());
+        intent.putExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+        intent.putExtra(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME, TAG);
+        Intent spiedIntent = spy(intent);
+
+        // set up pending intents
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage();
+        doReturn(true).when(mPendingIntent).isBroadcast();
+        doReturn(mPendingIntent).when(spiedIntent).getParcelableExtra(
+                anyString(), eq(PendingIntent.class));
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mCanceledIntent).getCreatorPackage();
+        doReturn(true).when(mCanceledIntent).isBroadcast();
+        doReturn(mCanceledIntent).when(spiedIntent).getParcelableExtra(
+                eq(SlicePurchaseController.EXTRA_INTENT_CANCELED), eq(PendingIntent.class));
+
+        mSlicePurchaseActivity = startActivity(spiedIntent, null, null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSlicePurchaseActivity.onDestroy();
+        super.tearDown();
+    }
+
+    @Test
+    public void testOnPurchaseSuccessful() throws Exception {
+        int duration = 5 * 60 * 1000; // 5 minutes
+        int invalidDuration = -1;
+        mSlicePurchaseActivity.onPurchaseSuccessful(duration);
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture());
+        Intent intent = intentCaptor.getValue();
+        assertEquals(duration, intent.getLongExtra(
+                SlicePurchaseController.EXTRA_PURCHASE_DURATION, invalidDuration));
+    }
+
+    @Test
+    public void testOnPurchaseFailed() throws Exception {
+        int failureCode = SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE;
+        String failureReason = "Server unreachable";
+        mSlicePurchaseActivity.onPurchaseFailed(failureCode, failureReason);
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture());
+        Intent intent = intentCaptor.getValue();
+        assertEquals(failureCode, intent.getIntExtra(
+                SlicePurchaseController.EXTRA_FAILURE_CODE, failureCode));
+        assertEquals(failureReason, intent.getStringExtra(
+                SlicePurchaseController.EXTRA_FAILURE_REASON));
+    }
+
+    @Test
+    public void testOnUserCanceled() throws Exception {
+        mSlicePurchaseActivity.onDestroy();
+        verify(mCanceledIntent).send();
+    }
+}
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
new file mode 100644
index 0000000..5765e5b
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.DisplayMetrics;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.phone.slice.SlicePurchaseController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class SlicePurchaseBroadcastReceiverTest {
+    private static final int PHONE_ID = 0;
+    private static final String TAG = "SlicePurchaseBroadcastReceiverTest";
+    private static final String EXTRA = "EXTRA";
+
+    @Mock Intent mIntent;
+    @Mock Intent mDataIntent;
+    @Mock PendingIntent mPendingIntent;
+    @Mock PendingIntent mCanceledIntent;
+    @Mock PendingIntent mContentIntent1;
+    @Mock PendingIntent mContentIntent2;
+    @Mock Context mContext;
+    @Mock Resources mResources;
+    @Mock NotificationManager mNotificationManager;
+    @Mock ApplicationInfo mApplicationInfo;
+    @Mock PackageManager mPackageManager;
+    @Mock DisplayMetrics mDisplayMetrics;
+    @Mock SlicePurchaseActivity mSlicePurchaseActivity;
+
+    private SlicePurchaseBroadcastReceiver mSlicePurchaseBroadcastReceiver;
+    private ArgumentCaptor<Intent> mIntentCaptor;
+    private ArgumentCaptor<Notification> mNotificationCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mNotificationManager).when(mContext)
+                .getSystemService(eq(NotificationManager.class));
+
+        mIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        mNotificationCaptor = ArgumentCaptor.forClass(Notification.class);
+        mSlicePurchaseBroadcastReceiver = spy(new SlicePurchaseBroadcastReceiver());
+    }
+
+    @Test
+    public void testSendSlicePurchaseAppResponse() throws Exception {
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(mIntent, EXTRA);
+        verify(mPendingIntent, never()).send();
+
+        doReturn(mPendingIntent).when(mIntent).getParcelableExtra(
+                eq(EXTRA), eq(PendingIntent.class));
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(mIntent, EXTRA);
+        verify(mPendingIntent).send();
+    }
+
+    @Test
+    public void testSendSlicePurchaseAppResponseWithData() throws Exception {
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(
+                mContext, mIntent, EXTRA, mDataIntent);
+        verify(mPendingIntent, never()).send(eq(mContext), eq(0), any(Intent.class));
+
+        doReturn(mPendingIntent).when(mIntent).getParcelableExtra(
+                eq(EXTRA), eq(PendingIntent.class));
+        SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(
+                mContext, mIntent, EXTRA, mDataIntent);
+        verify(mPendingIntent).send(eq(mContext), eq(0), mIntentCaptor.capture());
+        assertEquals(mDataIntent, mIntentCaptor.getValue());
+    }
+
+    @Test
+    public void testIsIntentValid() {
+        assertFalse(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent));
+
+        // set up intent
+        doReturn(PHONE_ID).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
+        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        doReturn(TAG).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
+        assertFalse(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent));
+
+        // set up pending intent
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage();
+        doReturn(true).when(mPendingIntent).isBroadcast();
+        doReturn(mPendingIntent).when(mIntent).getParcelableExtra(
+                anyString(), eq(PendingIntent.class));
+        assertTrue(SlicePurchaseBroadcastReceiver.isIntentValid(mIntent));
+    }
+
+    @Test
+    public void testDisplayBoosterNotification() {
+        // set up intent
+        doReturn(SlicePurchaseController.ACTION_START_SLICE_PURCHASE_APP).when(mIntent).getAction();
+        doReturn(PHONE_ID).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PHONE_ID), anyInt());
+        doReturn(SubscriptionManager.getDefaultDataSubscriptionId()).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_SUB_ID), anyInt());
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+        doReturn(TAG).when(mIntent).getStringExtra(
+                eq(SlicePurchaseController.EXTRA_REQUESTING_APP_NAME));
+
+        // set up pending intent
+        doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mPendingIntent).getCreatorPackage();
+        doReturn(true).when(mPendingIntent).isBroadcast();
+        doReturn(mPendingIntent).when(mIntent).getParcelableExtra(
+                anyString(), eq(PendingIntent.class));
+
+        // set up notification
+        doReturn(mResources).when(mContext).getResources();
+        doReturn(mDisplayMetrics).when(mResources).getDisplayMetrics();
+        doReturn("").when(mResources).getString(anyInt());
+        doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+
+        // set up intents created by broadcast receiver
+        doReturn(mContentIntent1).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
+                eq(mContext), eq(mIntent), eq(1));
+        doReturn(mContentIntent2).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
+                eq(mContext), eq(mIntent), eq(2));
+        doReturn(mCanceledIntent).when(mSlicePurchaseBroadcastReceiver).createCanceledIntent(
+                eq(mContext), eq(mIntent));
+
+        // send ACTION_START_SLICE_PURCHASE_APP
+        mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
+
+        // verify network boost notification was shown
+        verify(mNotificationManager).notifyAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                mNotificationCaptor.capture(),
+                eq(UserHandle.ALL));
+
+        Notification notification = mNotificationCaptor.getValue();
+        assertEquals(mContentIntent1, notification.contentIntent);
+        assertEquals(mPendingIntent, notification.deleteIntent);
+        assertEquals(2, notification.actions.length);
+        assertEquals(mCanceledIntent, notification.actions[0].actionIntent);
+        assertEquals(mContentIntent2, notification.actions[1].actionIntent);
+    }
+
+
+    @Test
+    public void testNotificationCanceled() {
+        // set up intent
+        doReturn("com.android.phone.slice.action.NOTIFICATION_CANCELED").when(mIntent).getAction();
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+
+        // send ACTION_NOTIFICATION_CANCELED
+        mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
+
+        // verify notification was canceled
+        verify(mNotificationManager).cancelAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                eq(UserHandle.ALL));
+    }
+
+    @Test
+    public void testNotificationTimeout() {
+        // set up intent
+        doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
+                .getAction();
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+
+        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
+        mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
+
+        // verify notification was canceled
+        verify(mNotificationManager).cancelAsUser(
+                eq(SlicePurchaseBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG),
+                eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+                eq(UserHandle.ALL));
+    }
+
+    @Test
+    // TODO: WebView/Activity should not close on timeout.
+    //  This test should be removed once implementation is fixed.
+    public void testActivityTimeout() {
+        // create and track activity
+        SlicePurchaseBroadcastReceiver.updateSlicePurchaseActivity(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY, mSlicePurchaseActivity);
+
+        // set up intent
+        doReturn(SlicePurchaseController.ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT).when(mIntent)
+                .getAction();
+        doReturn(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY).when(mIntent).getIntExtra(
+                eq(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY), anyInt());
+
+        // send ACTION_SLICE_PURCHASE_APP_RESPONSE_TIMEOUT
+        mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
+
+        // verify activity was canceled
+        verify(mSlicePurchaseActivity).finishAndRemoveTask();
+
+        // untrack activity
+        SlicePurchaseBroadcastReceiver.removeSlicePurchaseActivity(
+                TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+    }
+}
diff --git a/packages/CompanionDeviceManager/TEST_MAPPING b/packages/CompanionDeviceManager/TEST_MAPPING
deleted file mode 100644
index 63f54fa..0000000
--- a/packages/CompanionDeviceManager/TEST_MAPPING
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
-    }
-  ]
-}
diff --git a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
index eef3009..675072b 100644
--- a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_label" msgid="4470785958457506021">"隨附裝置管理員"</string>
+    <string name="app_label" msgid="4470785958457506021">"隨附裝置管理工具"</string>
     <string name="confirmation_title" msgid="3785000297483688997">"允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;存取「<xliff:g id="DEVICE_NAME">%2$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;"</string>
     <string name="profile_name_watch" msgid="576290739483672360">"手錶"</string>
     <string name="chooser_title" msgid="2262294130493605839">"選擇要讓「<xliff:g id="APP_NAME">%2$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;管理的<xliff:g id="PROFILE_NAME">%1$s</xliff:g>"</string>
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/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index a7e1a59..ae40460 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -203,6 +203,7 @@
         initUI();
     }
 
+    @SuppressWarnings("MissingSuperCall") // TODO: Fix me
     @Override
     protected void onNewIntent(Intent intent) {
         // Force cancels the CDM dialog if this activity receives another intent with
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 25529bb..d8577c3 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -17,7 +17,11 @@
     static_libs: [
         "androidx.activity_activity-compose",
         "androidx.appcompat_appcompat",
-        "androidx.compose.material_material",
+        "androidx.compose.animation_animation-core",
+        "androidx.compose.foundation_foundation",
+        "androidx.compose.material3_material3",
+        "androidx.compose.material_material-icons-core",
+        "androidx.compose.material_material-icons-extended",
         "androidx.compose.runtime_runtime",
         "androidx.compose.ui_ui",
         "androidx.compose.ui_ui-tooling",
@@ -27,6 +31,7 @@
         "androidx.lifecycle_lifecycle-runtime-ktx",
         "androidx.lifecycle_lifecycle-viewmodel-compose",
         "androidx.recyclerview_recyclerview",
+        "kotlinx-coroutines-core",
     ],
 
     platform_apis: true,
diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml
index 586ef86..bd27dab 100644
--- a/packages/CredentialManager/AndroidManifest.xml
+++ b/packages/CredentialManager/AndroidManifest.xml
@@ -36,8 +36,6 @@
         android:name=".CredentialSelectorActivity"
         android:exported="true"
         android:label="@string/app_name"
-        android:launchMode="singleInstance"
-        android:noHistory="true"
         android:excludeFromRecents="true"
         android:theme="@style/Theme.CredentialSelector">
     </activity>
diff --git a/packages/CredentialManager/res/drawable/ic_face.xml b/packages/CredentialManager/res/drawable/ic_face.xml
new file mode 100644
index 0000000..16fe144
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_face.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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.
+  -->
+
+<!--TODO: Testing only icon. Remove later. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        tools:ignore="VectorPath"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="#808080"
+        android:pathData="M9.025,14.275Q8.5,14.275 8.125,13.9Q7.75,13.525 7.75,13Q7.75,12.475 8.125,12.1Q8.5,11.725 9.025,11.725Q9.575,11.725 9.938,12.1Q10.3,12.475 10.3,13Q10.3,13.525 9.938,13.9Q9.575,14.275 9.025,14.275ZM14.975,14.275Q14.425,14.275 14.062,13.9Q13.7,13.525 13.7,13Q13.7,12.475 14.062,12.1Q14.425,11.725 14.975,11.725Q15.5,11.725 15.875,12.1Q16.25,12.475 16.25,13Q16.25,13.525 15.875,13.9Q15.5,14.275 14.975,14.275ZM12,19.925Q15.325,19.925 17.625,17.625Q19.925,15.325 19.925,12Q19.925,11.4 19.85,10.85Q19.775,10.3 19.575,9.775Q19.05,9.9 18.538,9.962Q18.025,10.025 17.45,10.025Q15.2,10.025 13.188,9.062Q11.175,8.1 9.775,6.375Q8.975,8.3 7.5,9.712Q6.025,11.125 4.075,11.85Q4.075,11.9 4.075,11.925Q4.075,11.95 4.075,12Q4.075,15.325 6.375,17.625Q8.675,19.925 12,19.925ZM12,22.2Q9.9,22.2 8.038,21.4Q6.175,20.6 4.788,19.225Q3.4,17.85 2.6,15.988Q1.8,14.125 1.8,12Q1.8,9.875 2.6,8.012Q3.4,6.15 4.788,4.775Q6.175,3.4 8.038,2.6Q9.9,1.8 12,1.8Q14.125,1.8 15.988,2.6Q17.85,3.4 19.225,4.775Q20.6,6.15 21.4,8.012Q22.2,9.875 22.2,12Q22.2,14.125 21.4,15.988Q20.6,17.85 19.225,19.225Q17.85,20.6 15.988,21.4Q14.125,22.2 12,22.2Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_manage_accounts.xml b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
new file mode 100644
index 0000000..adad2f1
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_manage_accounts.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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.
+  -->
+
+<!--TODO: Testing only icon. Remove later. -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        tools:ignore="VectorPath"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="#808080"
+        android:pathData="M16.1,21.2 L15.775,19.675Q15.5,19.55 15.25,19.425Q15,19.3 14.75,19.1L13.275,19.575L12.2,17.75L13.375,16.725Q13.325,16.4 13.325,16.112Q13.325,15.825 13.375,15.5L12.2,14.475L13.275,12.65L14.75,13.1Q15,12.925 15.25,12.787Q15.5,12.65 15.775,12.55L16.1,11.025H18.25L18.55,12.55Q18.825,12.65 19.075,12.8Q19.325,12.95 19.575,13.15L21.05,12.65L22.125,14.525L20.95,15.55Q21.025,15.825 21.013,16.137Q21,16.45 20.95,16.725L22.125,17.75L21.05,19.575L19.575,19.1Q19.325,19.3 19.075,19.425Q18.825,19.55 18.55,19.675L18.25,21.2ZM1.8,20.3V17.3Q1.8,16.375 2.275,15.613Q2.75,14.85 3.5,14.475Q4.775,13.825 6.425,13.362Q8.075,12.9 10,12.9Q10.2,12.9 10.4,12.9Q10.6,12.9 10.775,12.95Q9.925,14.85 10.062,16.738Q10.2,18.625 11.4,20.3ZM17.175,18.075Q17.975,18.075 18.55,17.487Q19.125,16.9 19.125,16.1Q19.125,15.3 18.55,14.725Q17.975,14.15 17.175,14.15Q16.375,14.15 15.788,14.725Q15.2,15.3 15.2,16.1Q15.2,16.9 15.788,17.487Q16.375,18.075 17.175,18.075ZM10,11.9Q8.25,11.9 7.025,10.662Q5.8,9.425 5.8,7.7Q5.8,5.95 7.025,4.725Q8.25,3.5 10,3.5Q11.75,3.5 12.975,4.725Q14.2,5.95 14.2,7.7Q14.2,9.425 12.975,10.662Q11.75,11.9 10,11.9Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_other_devices.xml b/packages/CredentialManager/res/drawable/ic_other_devices.xml
new file mode 100644
index 0000000..754648c
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_other_devices.xml
@@ -0,0 +1,15 @@
+<vector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="VectorPath"
+    android:name="vector"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+    <path
+        android:name="path"
+        android:pathData="M 7.6 4.72 L 7.6 7.6 L 4.72 7.6 L 4.72 4.72 L 7.6 4.72 Z M 9.04 3.28 L 3.28 3.28 L 3.28 9.04 L 9.04 9.04 L 9.04 3.28 Z M 7.6 12.4 L 7.6 15.28 L 4.72 15.28 L 4.72 12.4 L 7.6 12.4 Z M 9.04 10.96 L 3.28 10.96 L 3.28 16.72 L 9.04 16.72 L 9.04 10.96 Z M 15.28 4.72 L 15.28 7.6 L 12.4 7.6 L 12.4 4.72 L 15.28 4.72 Z M 16.72 3.28 L 10.96 3.28 L 10.96 9.04 L 16.72 9.04 L 16.72 3.28 Z M 10.96 10.96 L 12.4 10.96 L 12.4 12.4 L 10.96 12.4 L 10.96 10.96 Z M 12.4 12.4 L 13.84 12.4 L 13.84 13.84 L 12.4 13.84 L 12.4 12.4 Z M 13.84 10.96 L 15.28 10.96 L 15.28 12.4 L 13.84 12.4 L 13.84 10.96 Z M 10.96 13.84 L 12.4 13.84 L 12.4 15.28 L 10.96 15.28 L 10.96 13.84 Z M 12.4 15.28 L 13.84 15.28 L 13.84 16.72 L 12.4 16.72 L 12.4 15.28 Z M 13.84 13.84 L 15.28 13.84 L 15.28 15.28 L 13.84 15.28 L 13.84 13.84 Z M 15.28 12.4 L 16.72 12.4 L 16.72 13.84 L 15.28 13.84 L 15.28 12.4 Z M 15.28 15.28 L 16.72 15.28 L 16.72 16.72 L 15.28 16.72 L 15.28 15.28 Z M 19.6 5.2 L 17.68 5.2 L 17.68 2.32 L 14.8 2.32 L 14.8 0.4 L 19.6 0.4 L 19.6 5.2 Z M 19.6 19.6 L 19.6 14.8 L 17.68 14.8 L 17.68 17.68 L 14.8 17.68 L 14.8 19.6 L 19.6 19.6 Z M 0.4 19.6 L 5.2 19.6 L 5.2 17.68 L 2.32 17.68 L 2.32 14.8 L 0.4 14.8 L 0.4 19.6 Z M 0.4 0.4 L 0.4 5.2 L 2.32 5.2 L 2.32 2.32 L 5.2 2.32 L 5.2 0.4 L 0.4 0.4 Z"
+        android:fillColor="#000000"
+        android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/drawable/ic_profile.xml b/packages/CredentialManager/res/drawable/ic_profile.xml
new file mode 100644
index 0000000..ae65940
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/ic_profile.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
+        android:viewportWidth="46"
+        android:viewportHeight="46"
+        android:width="46dp"
+        android:height="46dp">
+    <path
+        android:pathData="M45.4247 22.9953C45.4247 35.0229 35.4133 44.7953 23.0359 44.7953C10.6585 44.7953 0.646973 35.0229 0.646973 22.9953C0.646973 10.9677 10.6585 1.19531 23.0359 1.19531C35.4133 1.19531 45.4247 10.9677 45.4247 22.9953Z"
+        android:strokeColor="#202124"
+        android:strokeAlpha="0.13"
+        android:strokeWidth="1" />
+</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 2c24bf1..114de89 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -1,15 +1,69 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
   <string name="app_name">CredentialManager</string>
   <string name="string_cancel">Cancel</string>
   <string name="string_continue">Continue</string>
   <string name="string_more_options">More options</string>
-  <string name="string_create_at_another_place">Create at another place</string>
+  <string name="string_create_in_another_place">Create in another place</string>
+  <string name="string_save_to_another_place">Save to another place</string>
   <string name="string_no_thanks">No thanks</string>
   <string name="passkey_creation_intro_title">A simple way to sign in safely</string>
   <string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
   <string name="choose_provider_title">Choose your default provider</string>
   <string name="choose_provider_body">This provider will store passkeys and passwords for you and help you easily autofill and sign in. Learn more</string>
-  <string name="choose_create_option_title">Create a passkey at</string>
+  <string name="choose_create_option_passkey_title">Create a passkey in <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
+  <string name="choose_create_option_password_title">Save your password to <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
+  <string name="choose_create_option_sign_in_title">Save your sign-in info to <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
   <string name="choose_sign_in_title">Use saved sign in</string>
-  <string name="create_passkey_at">Create passkey at</string>
+  <string name="create_passkey_in">Create passkey in</string>
+  <string name="save_password_to">Save password to</string>
+  <string name="save_sign_in_to">Save sign-in to</string>
+  <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoDisplayName">%1$s</xliff:g> for all your sign-ins?</string>
+  <string name="set_as_default">Set as default</string>
+  <string name="use_once">Use once</string>
+  <string name="choose_create_option_description">You can use your <xliff:g id="appDomainName">%1$s</xliff:g> <xliff:g id="type">%2$s</xliff:g> on any device. It is saved to <xliff:g id="providerInfoDisplayName">%3$s</xliff:g> for <xliff:g id="createInfoDisplayName">%4$s</xliff:g></string>
+  <string name="more_options_usage_passwords_passkeys"><xliff:g id="passwordsNumber">%1$s</xliff:g> passwords, <xliff:g id="passkeysNumber">%2$s</xliff:g> passkeys</string>
+  <string name="more_options_usage_passwords"><xliff:g id="passwordsNumber">%1$s</xliff:g> passwords</string>
+  <string name="more_options_usage_passkeys"><xliff:g id="passkeysNumber">%1$s</xliff:g> passkeys</string>
+  <string name="passkey">passkey</string>
+  <string name="password">password</string>
+  <string name="sign_ins">sign-ins</string>
+  <string name="another_device">Another device</string>
+  <string name="other_password_manager">Other password manager</string>
+  <!-- TODO: Check the wording here. -->
+  <string name="confirm_default_or_use_once_description">This password manager will store your passwords and passkeys to help you easily sign in.</string>
+  <!-- Spoken content description of an element which will close the sheet when clicked. -->
+  <string name="close_sheet">"Close sheet"</string>
+  <!-- Spoken content description of the back arrow button. -->
+  <string name="accessibility_back_arrow_button">"Go back to the previous page"</string>
+
+  <!-- Strings for the get flow. -->
+  <!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved passkey to sign in to the app. [CHAR LIMIT=200] -->
+  <string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
+  <!-- This appears as the title of the dialog asking for user confirmation to use the single previously saved credential to sign in to the app. [CHAR LIMIT=200] -->
+  <string name="get_dialog_title_use_sign_in_for">Use your saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
+  <!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] -->
+  <string name="get_dialog_title_choose_sign_in_for">Choose a saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string>
+  <!-- Appears as an option row for viewing all the available sign-in options. [CHAR LIMIT=80] -->
+  <string name="get_dialog_use_saved_passkey_for">Sign in another way</string>
+  <!-- Button label to close the dialog when the user does not want to use any sign-in. [CHAR LIMIT=40] -->
+  <string name="get_dialog_button_label_no_thanks">No thanks</string>
+  <!-- Button label to continue with the selected sign-in. [CHAR LIMIT=40] -->
+  <string name="get_dialog_button_label_continue">Continue</string>
+  <!-- Separator for sign-in type and username in a sign-in entry. -->
+  <string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string>
+  <!-- Modal bottom sheet title for displaying all the available sign-in options. [CHAR LIMIT=80] -->
+  <string name="get_dialog_title_sign_in_options">Sign-in options</string>
+  <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=80] -->
+  <string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
+  <!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] -->
+  <string name="get_dialog_heading_locked_password_managers">Locked password managers</string>
+  <!-- Explanatory sub/body text for an option entry to use a locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-in. [CHAR LIMIT=120] -->
+  <string name="locked_credential_entry_label_subtext">Tap to unlock</string>
+  <!-- Column heading for displaying action chips for managing sign-ins from each credential provider. [CHAR LIMIT=80] -->
+  <string name="get_dialog_heading_manage_sign_ins">Manage sign-ins</string>
+  <!-- Column heading for displaying option to use sign-ins saved on a different device. [CHAR LIMIT=80] -->
+  <string name="get_dialog_heading_from_another_device">From another device</string>
+  <!-- Headline text for an option to use sign-ins saved on a different device. [CHAR LIMIT=120] -->
+  <string name="get_dialog_option_headline_use_a_different_device">Use a different device</string>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/themes.xml b/packages/CredentialManager/res/values/themes.xml
index feec746..a58a038 100644
--- a/packages/CredentialManager/res/values/themes.xml
+++ b/packages/CredentialManager/res/values/themes.xml
@@ -2,11 +2,12 @@
 <resources>
 
   <style name="Theme.CredentialSelector" parent="@android:style/ThemeOverlay.Material">
-    <item name="android:statusBarColor">@color/purple_700</item>
+    <item name="android:statusBarColor">@android:color/transparent</item>
     <item name="android:windowContentOverlay">@null</item>
     <item name="android:windowNoTitle">true</item>
     <item name="android:windowBackground">@android:color/transparent</item>
     <item name="android:windowIsTranslucent">true</item>
     <item name="android:colorBackgroundCacheHint">@null</item>
+    <item name="fontFamily">google-sans</item>
   </style>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt
deleted file mode 100644
index ee4f4ca..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialEntryUi.kt
+++ /dev/null
@@ -1,59 +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.credentialmanager
-
-import android.app.slice.Slice
-import android.credentials.ui.Entry
-import android.graphics.drawable.Icon
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class CredentialEntryUi(
-  val userName: CharSequence,
-  val displayName: CharSequence?,
-  val icon: Icon?,
-  val usageData: CharSequence?,
-  // TODO: add last used.
-) {
-  companion object {
-    fun fromSlice(slice: Slice): CredentialEntryUi {
-      val items = slice.items
-
-      var title: String? = null
-      var subTitle: String? = null
-      var icon: Icon? = null
-      var usageData: String? = null
-
-      items.forEach {
-        if (it.hasHint(Entry.HINT_ICON)) {
-          icon = it.icon
-        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == null) {
-          subTitle = it.text.toString()
-        } else if (it.hasHint(Entry.HINT_TITLE)) {
-          title = it.text.toString()
-        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == Slice.SUBTYPE_MESSAGE) {
-          usageData = it.text.toString()
-        }
-      }
-      // TODO: fail NPE more elegantly.
-      return CredentialEntryUi(title!!, subTitle, icon, usageData)
-    }
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index c575fbc..7e69987 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -16,19 +16,38 @@
 
 package com.android.credentialmanager
 
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
 import android.app.slice.Slice
 import android.app.slice.SliceSpec
 import android.content.Context
 import android.content.Intent
+import android.credentials.CreateCredentialRequest
+import android.credentials.GetCredentialOption
+import android.credentials.GetCredentialRequest
+import android.credentials.ui.Constants
 import android.credentials.ui.Entry
+import android.credentials.ui.CreateCredentialProviderData
+import android.credentials.ui.GetCredentialProviderData
+import android.credentials.ui.DisabledProviderData
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
+import android.credentials.ui.BaseDialogResult
+import android.credentials.ui.UserSelectionDialogResult
 import android.graphics.drawable.Icon
 import android.os.Binder
-import com.android.credentialmanager.createflow.CreatePasskeyUiState
+import android.os.Bundle
+import android.os.ResultReceiver
+import com.android.credentialmanager.createflow.ActiveEntry
+import com.android.credentialmanager.createflow.CreateCredentialUiState
 import com.android.credentialmanager.createflow.CreateScreenState
+import com.android.credentialmanager.createflow.EnabledProviderInfo
+import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.getflow.GetScreenState
+import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest.Companion.createFrom
+import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
+import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
 
 // Consider repo per screen, similar to view model?
 class CredentialManagerRepo(
@@ -36,38 +55,113 @@
   intent: Intent,
 ) {
   private val requestInfo: RequestInfo
-  private val providerList: List<ProviderData>
+  private val providerEnabledList: List<ProviderData>
+  private val providerDisabledList: List<DisabledProviderData>
+  // TODO: require non-null.
+  val resultReceiver: ResultReceiver?
 
   init {
     requestInfo = intent.extras?.getParcelable(
       RequestInfo.EXTRA_REQUEST_INFO,
       RequestInfo::class.java
-    ) ?: RequestInfo(
-      Binder(),
-      RequestInfo.TYPE_CREATE,
-      /*isFirstUsage=*/false
-    )
+    ) ?: testCreateRequestInfo()
 
-    providerList = intent.extras?.getParcelableArrayList(
-      ProviderData.EXTRA_PROVIDER_DATA_LIST,
-      ProviderData::class.java
-    ) ?: testProviderList()
+    providerEnabledList = when (requestInfo.type) {
+      RequestInfo.TYPE_CREATE ->
+        intent.extras?.getParcelableArrayList(
+                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+                CreateCredentialProviderData::class.java
+        ) ?: testCreateCredentialEnabledProviderList()
+      RequestInfo.TYPE_GET ->
+        intent.extras?.getParcelableArrayList(
+          ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+          DisabledProviderData::class.java
+        ) ?: testGetCredentialProviderList()
+      else -> {
+        // TODO: fail gracefully
+        throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
+      }
+    }
+
+    providerDisabledList =
+      intent.extras?.getParcelableArrayList(
+        ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST,
+        DisabledProviderData::class.java
+      ) ?: testDisabledProviderList()
+
+    resultReceiver = intent.getParcelableExtra(
+      Constants.EXTRA_RESULT_RECEIVER,
+      ResultReceiver::class.java
+    )
+  }
+
+  fun onCancel() {
+    val resultData = Bundle()
+    BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData)
+    resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_CANCELED, resultData)
+  }
+
+  fun onOptionSelected(providerPackageName: String, entryKey: String, entrySubkey: String) {
+    val userSelectionDialogResult = UserSelectionDialogResult(
+      requestInfo.token,
+      providerPackageName,
+      entryKey,
+      entrySubkey
+    )
+    val resultData = Bundle()
+    UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData)
+    resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, resultData)
   }
 
   fun getCredentialInitialUiState(): GetCredentialUiState {
-    val providerList = GetFlowUtils.toProviderList(providerList, context)
+    val providerEnabledList = GetFlowUtils.toProviderList(
+    // TODO: handle runtime cast error
+      providerEnabledList as List<GetCredentialProviderData>, context)
+    // TODO: covert from real requestInfo
+    val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("tribank")
     return GetCredentialUiState(
-      providerList,
-      GetScreenState.CREDENTIAL_SELECTION,
-      providerList.first()
+      providerEnabledList,
+      GetScreenState.PRIMARY_SELECTION,
+      requestDisplayInfo,
     )
   }
 
-  fun createPasskeyInitialUiState(): CreatePasskeyUiState {
-    val providerList = CreateFlowUtils.toProviderList(providerList, context)
-    return CreatePasskeyUiState(
-      providers = providerList,
-      currentScreenState = CreateScreenState.PASSKEY_INTRO,
+  fun createCredentialInitialUiState(): CreateCredentialUiState {
+    val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
+      // Handle runtime cast error
+      providerEnabledList as List<CreateCredentialProviderData>, context)
+    val providerDisabledList = CreateFlowUtils.toDisabledProviderList(
+      // Handle runtime cast error
+      providerDisabledList as List<DisabledProviderData>, context)
+    var hasDefault = false
+    var defaultProvider: EnabledProviderInfo = providerEnabledList.first()
+    providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
+      providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
+      if (providerInfo.isDefault) {hasDefault = true; defaultProvider = providerInfo} }
+    // TODO: covert from real requestInfo for create passkey
+    var requestDisplayInfo = RequestDisplayInfo(
+      "Elisa Beckett",
+      "beckett-bakert@gmail.com",
+      TYPE_PUBLIC_KEY_CREDENTIAL,
+      "tribank")
+    val createCredentialRequest = requestInfo.createCredentialRequest
+    val createCredentialRequestJetpack = createCredentialRequest?.let { createFrom(it) }
+    if (createCredentialRequestJetpack is CreatePasswordRequest) {
+      requestDisplayInfo = RequestDisplayInfo(
+        createCredentialRequestJetpack.id,
+        createCredentialRequestJetpack.password,
+        TYPE_PASSWORD_CREDENTIAL,
+        "tribank")
+    }
+    return CreateCredentialUiState(
+      enabledProviders = providerEnabledList,
+      disabledProviders = providerDisabledList,
+      if (hasDefault)
+      {CreateScreenState.CREATION_OPTION_SELECTION} else {CreateScreenState.PASSKEY_INTRO},
+      requestDisplayInfo,
+      if (hasDefault) {
+        ActiveEntry(defaultProvider, defaultProvider.createOptions.first())
+      } else null
     )
   }
 
@@ -87,66 +181,250 @@
   }
 
   // TODO: below are prototype functionalities. To be removed for productionization.
-  private fun testProviderList(): List<ProviderData> {
+  private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> {
     return listOf(
-      ProviderData(
-        "com.google",
-        listOf<Entry>(
-          newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(3, "Go to Settings", "",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(4, "Switch Account", "",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
-        "com.dashlane",
-        listOf<Entry>(
-          newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
-        "com.lastpass",
-        listOf<Entry>(
-          newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(),
-        null
-      )
-
+      CreateCredentialProviderData
+        .Builder("io.enpass.app")
+        .setSaveEntries(
+          listOf<Entry>(
+            newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
+              20, 7, 27, 10000),
+            newCreateEntry("key1", "subkey-2", "elisa.work@google.com",
+              20, 7, 27, 11000),
+          )
+        )
+        .setRemoteEntry(
+          newRemoteEntry("key2", "subkey-1")
+        )
+        .setIsDefaultProvider(true)
+        .build(),
+      CreateCredentialProviderData
+        .Builder("com.dashlane")
+        .setSaveEntries(
+          listOf<Entry>(
+            newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
+              20, 7, 27, 30000),
+            newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com",
+              20, 7, 27, 31000),
+          )
+        )
+        .build(),
     )
   }
 
-  private fun newEntry(id: Int, title: String, subtitle: String, usageData: String): Entry {
+  private fun testDisabledProviderList(): List<DisabledProviderData> {
+    return listOf(
+      DisabledProviderData("com.lastpass.lpandroid"),
+      DisabledProviderData("com.google.android.youtube")
+    )
+  }
+
+  private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
+    return listOf(
+      GetCredentialProviderData.Builder("io.enpass.app")
+        .setCredentialEntries(
+          listOf<Entry>(
+            newGetEntry(
+              "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
+              "elisa.bakery@gmail.com", "Elisa Beckett", 300L
+            ),
+            newGetEntry(
+              "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
+              "elisa.bakery@gmail.com", null, 300L
+            ),
+            newGetEntry(
+              "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
+              "elisa.family@outlook.com", null, 100L
+            ),
+          )
+        ).setAuthenticationEntry(
+          newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+        ).setActionChips(
+          listOf(
+            newActionEntry(
+              "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+              Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+              "Open Google Password Manager", "elisa.beckett@gmail.com"
+            ),
+            newActionEntry(
+              "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
+              Icon.createWithResource(context, R.drawable.ic_manage_accounts),
+              "Open Google Password Manager", "beckett-family@gmail.com"
+            ),
+          )
+        ).setRemoteEntry(
+          newRemoteEntry("key4", "subkey-1")
+        ).build(),
+      GetCredentialProviderData.Builder("com.dashlane")
+        .setCredentialEntries(
+          listOf<Entry>(
+            newGetEntry(
+              "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
+              "elisa.family@outlook.com", null, 600L
+            ),
+            newGetEntry(
+              "key1", "subkey-2", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
+              "elisa.family@outlook.com", null, 100L
+            ),
+          )
+        ).setAuthenticationEntry(
+          newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+        ).setActionChips(
+          listOf(
+            newActionEntry(
+              "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+              Icon.createWithResource(context, R.drawable.ic_face),
+              "Open Enpass"
+            ),
+          )
+        ).build(),
+    )
+  }
+
+  private fun newActionEntry(
+    key: String,
+    subkey: String,
+    credentialType: String,
+    icon: Icon,
+    text: String,
+    subtext: String? = null,
+  ): Entry {
+    val slice = Slice.Builder(
+      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+    ).addText(
+      text, null, listOf(Entry.HINT_ACTION_TITLE)
+    ).addIcon(icon, null, listOf(Entry.HINT_ACTION_ICON))
+    if (subtext != null) {
+      slice.addText(subtext, null, listOf(Entry.HINT_ACTION_SUBTEXT))
+    }
+    return Entry(
+      key,
+      subkey,
+      slice.build()
+    )
+  }
+
+  private fun newAuthenticationEntry(
+    key: String,
+    subkey: String,
+    credentialType: String,
+  ): Entry {
+    val slice = Slice.Builder(
+      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+    )
+    return Entry(
+      key,
+      subkey,
+      slice.build()
+    )
+  }
+
+  private fun newGetEntry(
+    key: String,
+    subkey: String,
+    credentialType: String,
+    credentialTypeDisplayName: String,
+    userName: String,
+    userDisplayName: String?,
+    lastUsedTimeMillis: Long?,
+  ): Entry {
+    val slice = Slice.Builder(
+      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
+    ).addText(
+      credentialTypeDisplayName, null, listOf(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME)
+    ).addText(
+      userName, null, listOf(Entry.HINT_USER_NAME)
+    ).addIcon(
+      Icon.createWithResource(context, R.drawable.ic_passkey),
+      null,
+      listOf(Entry.HINT_PROFILE_ICON))
+    if (userDisplayName != null) {
+      slice.addText(userDisplayName, null, listOf(Entry.HINT_PASSKEY_USER_DISPLAY_NAME))
+    }
+    if (lastUsedTimeMillis != null) {
+      slice.addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS))
+    }
+    return Entry(
+      key,
+      subkey,
+      slice.build()
+    )
+  }
+
+  private fun newCreateEntry(
+    key: String,
+    subkey: String,
+    providerDisplayName: String,
+    passwordCount: Int,
+    passkeyCount: Int,
+    totalCredentialCount: Int,
+    lastUsedTimeMillis: Long,
+  ): Entry {
     val slice = Slice.Builder(
       Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
     )
-      .addText(title, null, listOf(Entry.HINT_TITLE))
-      .addText(subtitle, null, listOf(Entry.HINT_SUBTITLE))
+      .addText(
+        providerDisplayName, null, listOf(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME))
       .addIcon(
         Icon.createWithResource(context, R.drawable.ic_passkey),
         null,
-        listOf(Entry.HINT_ICON))
-      .addText(usageData, Slice.SUBTYPE_MESSAGE, listOf(Entry.HINT_SUBTITLE))
+        listOf(Entry.HINT_CREDENTIAL_TYPE_ICON))
+      .addIcon(
+        Icon.createWithResource(context, R.drawable.ic_profile),
+        null,
+        listOf(Entry.HINT_PROFILE_ICON))
+      .addInt(
+        passwordCount, null, listOf(Entry.HINT_PASSWORD_COUNT))
+      .addInt(
+        passkeyCount, null, listOf(Entry.HINT_PASSKEY_COUNT))
+      .addInt(
+        totalCredentialCount, null, listOf(Entry.HINT_TOTAL_CREDENTIAL_COUNT))
+      .addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS))
       .build()
     return Entry(
-      id,
+      key,
+      subkey,
       slice
     )
   }
+
+  private fun newRemoteEntry(
+    key: String,
+    subkey: String,
+  ): Entry {
+    return Entry(
+      key,
+      subkey,
+      Slice.Builder(
+        Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
+      ).build()
+    )
+  }
+
+  private fun testCreateRequestInfo(): RequestInfo {
+    val data = toBundle("beckett-bakert@gmail.com", "password123")
+    return RequestInfo.newCreateRequestInfo(
+      Binder(),
+      CreateCredentialRequest(
+        TYPE_PASSWORD_CREDENTIAL,
+        data
+      ),
+      /*isFirstUsage=*/false,
+      "tribank"
+    )
+  }
+
+  private fun testGetRequestInfo(): RequestInfo {
+    val data = Bundle()
+    return RequestInfo.newGetRequestInfo(
+      Binder(),
+      GetCredentialRequest.Builder()
+        .addGetCredentialOption(
+          GetCredentialOption(TYPE_PUBLIC_KEY_CREDENTIAL, Bundle())
+        )
+        .build(),
+      /*isFirstUsage=*/false,
+      "tribank.us"
+    )
+  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index b538ae7..1041a33 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -23,9 +23,15 @@
 import androidx.activity.compose.setContent
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.runtime.Composable
+import androidx.lifecycle.Observer
+import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.credentialmanager.common.DialogType
-import com.android.credentialmanager.createflow.CreatePasskeyScreen
+import com.android.credentialmanager.common.DialogResult
+import com.android.credentialmanager.common.ResultState
+import com.android.credentialmanager.createflow.CreateCredentialScreen
+import com.android.credentialmanager.createflow.CreateCredentialViewModel
 import com.android.credentialmanager.getflow.GetCredentialScreen
+import com.android.credentialmanager.getflow.GetCredentialViewModel
 import com.android.credentialmanager.ui.theme.CredentialSelectorTheme
 
 @ExperimentalMaterialApi
@@ -57,10 +63,20 @@
     val dialogType = DialogType.toDialogType(operationType)
     when (dialogType) {
       DialogType.CREATE_PASSKEY -> {
-        CreatePasskeyScreen(cancelActivity = onCancel)
+        val viewModel: CreateCredentialViewModel = viewModel()
+        viewModel.observeDialogResult().observe(
+          this@CredentialSelectorActivity,
+          onCancel
+        )
+        CreateCredentialScreen(viewModel = viewModel)
       }
       DialogType.GET_CREDENTIALS -> {
-        GetCredentialScreen(cancelActivity = onCancel)
+        val viewModel: GetCredentialViewModel = viewModel()
+        viewModel.observeDialogResult().observe(
+          this@CredentialSelectorActivity,
+          onCancel
+        )
+        GetCredentialScreen(viewModel = viewModel)
       }
       else -> {
         Log.w("AccountSelector", "Unknown type, not rendering any UI")
@@ -69,7 +85,9 @@
     }
   }
 
-  private val onCancel = {
-    this@CredentialSelectorActivity.finish()
+  private val onCancel = Observer<DialogResult> {
+    if (it.resultState == ResultState.COMPLETE || it.resultState == ResultState.CANCELED) {
+      this@CredentialSelectorActivity.finish()
+    }
   }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index af27ce5..fad9364 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -16,51 +16,136 @@
 
 package com.android.credentialmanager
 
+import android.content.ComponentName
 import android.content.Context
+import android.content.pm.PackageManager
 import android.credentials.ui.Entry
-import android.credentials.ui.ProviderData
+import android.credentials.ui.GetCredentialProviderData
+import android.credentials.ui.CreateCredentialProviderData
+import android.credentials.ui.DisabledProviderData
+import android.graphics.drawable.Drawable
 import com.android.credentialmanager.createflow.CreateOptionInfo
-import com.android.credentialmanager.getflow.CredentialOptionInfo
+import com.android.credentialmanager.createflow.RemoteInfo
+import com.android.credentialmanager.getflow.ActionEntryInfo
+import com.android.credentialmanager.getflow.AuthenticationEntryInfo
+import com.android.credentialmanager.getflow.CredentialEntryInfo
 import com.android.credentialmanager.getflow.ProviderInfo
+import com.android.credentialmanager.getflow.RemoteEntryInfo
+import com.android.credentialmanager.jetpack.provider.ActionUi
+import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
+import com.android.credentialmanager.jetpack.provider.SaveEntryUi
 
 /** Utility functions for converting CredentialManager data structures to or from UI formats. */
 class GetFlowUtils {
   companion object {
 
     fun toProviderList(
-      providerDataList: List<ProviderData>,
+      providerDataList: List<GetCredentialProviderData>,
       context: Context,
     ): List<ProviderInfo> {
+      val packageManager = context.packageManager
       return providerDataList.map {
+        // TODO: get from the actual service info
+        val pkgInfo = packageManager
+          .getPackageInfo(it.providerFlattenedComponentName,
+            PackageManager.PackageInfoFlags.of(0))
+        val providerDisplayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString()
+        // TODO: decide what to do when failed to load a provider icon
+        val providerIcon = pkgInfo.applicationInfo.loadIcon(packageManager)!!
         ProviderInfo(
-          // TODO: replace to extract from the service data structure when available
-          icon = context.getDrawable(R.drawable.ic_passkey)!!,
-          name = it.packageName,
-          appDomainName = "tribank.us",
-          credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
-          credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
+          id = it.providerFlattenedComponentName,
+          // TODO: decide what to do when failed to load a provider icon
+          icon = providerIcon,
+          displayName = providerDisplayName,
+          credentialEntryList = getCredentialOptionInfoList(
+            it.providerFlattenedComponentName, it.credentialEntries, context),
+          authenticationEntry = getAuthenticationEntry(
+            it.providerFlattenedComponentName,
+            providerDisplayName,
+            providerIcon,
+            it.authenticationEntry),
+          remoteEntry = getRemoteEntry(it.providerFlattenedComponentName, it.remoteEntry),
+          actionEntryList = getActionEntryList(
+            it.providerFlattenedComponentName, it.actionChips, context),
         )
       }
     }
 
 
     /* From service data structure to UI credential entry list representation. */
-    private fun toCredentialOptionInfoList(
+    private fun getCredentialOptionInfoList(
+      providerId: String,
       credentialEntries: List<Entry>,
       context: Context,
-    ): List<CredentialOptionInfo> {
+    ): List<CredentialEntryInfo> {
       return credentialEntries.map {
         val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice)
 
         // Consider directly move the UI object into the class.
-        return@map CredentialOptionInfo(
-          // TODO: remove fallbacks
-          icon = credentialEntryUi.icon?.loadDrawable(context)
+        return@map CredentialEntryInfo(
+          providerId = providerId,
+          entryKey = it.key,
+          entrySubkey = it.subkey,
+          credentialType = credentialEntryUi.credentialType.toString(),
+          credentialTypeDisplayName = credentialEntryUi.credentialTypeDisplayName.toString(),
+          userName = credentialEntryUi.userName.toString(),
+          displayName = credentialEntryUi.userDisplayName?.toString(),
+          // TODO: proper fallback
+          icon = credentialEntryUi.entryIcon.loadDrawable(context)
             ?: context.getDrawable(R.drawable.ic_passkey)!!,
-          title = credentialEntryUi.userName.toString(),
-          subtitle = credentialEntryUi.displayName?.toString() ?: "Unknown display name",
-          id = it.entryId,
-          usageData = credentialEntryUi.usageData?.toString() ?: "Unknown usageData",
+          lastUsedTimeMillis = credentialEntryUi.lastUsedTimeMillis,
+        )
+      }
+    }
+
+    private fun getAuthenticationEntry(
+      providerId: String,
+      providerDisplayName: String,
+      providerIcon: Drawable,
+      authEntry: Entry?,
+    ): AuthenticationEntryInfo? {
+      // TODO: should also call fromSlice after getting the official jetpack code.
+
+      if (authEntry == null) {
+        return null
+      }
+      return AuthenticationEntryInfo(
+        providerId = providerId,
+        entryKey = authEntry.key,
+        entrySubkey = authEntry.subkey,
+        title = providerDisplayName,
+        icon = providerIcon,
+      )
+    }
+
+    private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
+      // TODO: should also call fromSlice after getting the official jetpack code.
+      if (remoteEntry == null) {
+        return null
+      }
+      return RemoteEntryInfo(
+        providerId = providerId,
+        entryKey = remoteEntry.key,
+        entrySubkey = remoteEntry.subkey,
+      )
+    }
+
+    private fun getActionEntryList(
+      providerId: String,
+      actionEntries: List<Entry>,
+      context: Context,
+    ): List<ActionEntryInfo> {
+      return actionEntries.map {
+        val actionEntryUi = ActionUi.fromSlice(it.slice)
+
+        return@map ActionEntryInfo(
+          providerId = providerId,
+          entryKey = it.key,
+          entrySubkey = it.subkey,
+          title = actionEntryUi.text.toString(),
+          // TODO: gracefully fail
+          icon = actionEntryUi.icon.loadDrawable(context)!!,
+          subTitle = actionEntryUi.subtext?.toString(),
         )
       }
     }
@@ -70,18 +155,50 @@
 class CreateFlowUtils {
   companion object {
 
-    fun toProviderList(
-      providerDataList: List<ProviderData>,
+    fun toEnabledProviderList(
+      providerDataList: List<CreateCredentialProviderData>,
       context: Context,
-    ): List<com.android.credentialmanager.createflow.ProviderInfo> {
+    ): List<com.android.credentialmanager.createflow.EnabledProviderInfo> {
+      // TODO: get from the actual service info
+      val packageManager = context.packageManager
+
       return providerDataList.map {
-        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.packageName,
-          appDomainName = "tribank.us",
-          credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
-          createOptions = toCreationOptionInfoList(it.credentialEntries, context),
+        val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName)
+        var packageName = componentName?.packageName
+        if (componentName == null) {
+          // TODO: Remove once test data is fixed
+          packageName = it.providerFlattenedComponentName
+        }
+
+        val pkgInfo = packageManager
+          .getPackageInfo(packageName!!,
+            PackageManager.PackageInfoFlags.of(0))
+        com.android.credentialmanager.createflow.EnabledProviderInfo(
+          // TODO: decide what to do when failed to load a provider icon
+          icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
+          name = it.providerFlattenedComponentName,
+          displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
+          createOptions = toCreationOptionInfoList(it.saveEntries, context),
+          isDefault = it.isDefaultProvider,
+          remoteEntry = toRemoteInfo(it.remoteEntry),
+        )
+      }
+    }
+
+    fun toDisabledProviderList(
+      providerDataList: List<DisabledProviderData>,
+      context: Context,
+    ): List<com.android.credentialmanager.createflow.DisabledProviderInfo> {
+      // TODO: get from the actual service info
+      val packageManager = context.packageManager
+      return providerDataList.map {
+        val pkgInfo = packageManager
+          .getPackageInfo(it.providerFlattenedComponentName,
+            PackageManager.PackageInfoFlags.of(0))
+        com.android.credentialmanager.createflow.DisabledProviderInfo(
+          icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
+          name = it.providerFlattenedComponentName,
+          displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
         )
       }
     }
@@ -95,14 +212,31 @@
 
         return@map CreateOptionInfo(
           // TODO: remove fallbacks
-          icon = saveEntryUi.icon?.loadDrawable(context)
+          entryKey = it.key,
+          entrySubkey = it.subkey,
+          userProviderDisplayName = saveEntryUi.userProviderAccountName as String,
+          credentialTypeIcon = saveEntryUi.credentialTypeIcon?.loadDrawable(context)
             ?: context.getDrawable(R.drawable.ic_passkey)!!,
-          title = saveEntryUi.title.toString(),
-          subtitle = saveEntryUi.subTitle?.toString() ?: "Unknown subtitle",
-          id = it.entryId,
-          usageData = saveEntryUi.usageData?.toString() ?: "Unknown usageData",
+          profileIcon = saveEntryUi.profileIcon?.loadDrawable(context)
+            ?: context.getDrawable(R.drawable.ic_profile)!!,
+          passwordCount = saveEntryUi.passwordCount ?: 0,
+          passkeyCount = saveEntryUi.passkeyCount ?: 0,
+          totalCredentialCount = saveEntryUi.totalCredentialCount ?: 0,
+          lastUsedTimeMillis = saveEntryUi.lastUsedTimeMillis ?: 0,
         )
       }
     }
+
+    private fun toRemoteInfo(
+      remoteEntry: Entry?,
+    ): RemoteInfo? {
+      // TODO: should also call fromSlice after getting the official jetpack code.
+      return if (remoteEntry != null) {
+        RemoteInfo(
+          entryKey = remoteEntry.key,
+          entrySubkey = remoteEntry.subkey,
+        )
+      } else null
+    }
   }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt
deleted file mode 100644
index cd52197..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/SaveEntryUi.kt
+++ /dev/null
@@ -1,59 +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.credentialmanager
-
-import android.app.slice.Slice
-import android.credentials.ui.Entry
-import android.graphics.drawable.Icon
-
-/**
- * UI representation for a save entry used during the create credential flow.
- *
- * TODO: move to jetpack.
- */
-class SaveEntryUi(
-  val title: CharSequence,
-  val subTitle: CharSequence?,
-  val icon: Icon?,
-  val usageData: CharSequence?,
-  // TODO: add
-) {
-  companion object {
-    fun fromSlice(slice: Slice): SaveEntryUi {
-      val items = slice.items
-
-      var title: String? = null
-      var subTitle: String? = null
-      var icon: Icon? = null
-      var usageData: String? = null
-
-      items.forEach {
-        if (it.hasHint(Entry.HINT_ICON)) {
-          icon = it.icon
-        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == null) {
-          subTitle = it.text.toString()
-        } else if (it.hasHint(Entry.HINT_TITLE)) {
-          title = it.text.toString()
-        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == Slice.SUBTYPE_MESSAGE) {
-          usageData = it.text.toString()
-        }
-      }
-      // TODO: fail NPE more elegantly.
-      return SaveEntryUi(title!!, subTitle, icon, usageData)
-    }
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt
new file mode 100644
index 0000000..b751663
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/DialogResult.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.credentialmanager.common
+
+enum class ResultState {
+  COMPLETE,
+  CANCELED,
+}
+
+data class DialogResult(
+  val resultState: ResultState,
+)
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt
new file mode 100644
index 0000000..61e11fe
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/ModalBottomSheet.kt
@@ -0,0 +1,481 @@
+/*
+ * 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.credentialmanager.common.material
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.android.credentialmanager.R
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.Expanded
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.HalfExpanded
+import com.android.credentialmanager.common.material.ModalBottomSheetValue.Hidden
+import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.launch
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ * Possible values of [ModalBottomSheetState].
+ */
+enum class ModalBottomSheetValue {
+    /**
+     * The bottom sheet is not visible.
+     */
+    Hidden,
+
+    /**
+     * The bottom sheet is visible at full height.
+     */
+    Expanded,
+
+    /**
+     * The bottom sheet is partially visible at 50% of the screen height. This state is only
+     * enabled if the height of the bottom sheet is more than 50% of the screen height.
+     */
+    HalfExpanded
+}
+
+/**
+ * State of the [ModalBottomSheetLayout] composable.
+ *
+ * @param initialValue The initial value of the state. <b>Must not be set to
+ * [ModalBottomSheetValue.HalfExpanded] if [isSkipHalfExpanded] is set to true.</b>
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param isSkipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+class ModalBottomSheetState(
+    initialValue: ModalBottomSheetValue,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+    internal val isSkipHalfExpanded: Boolean,
+    confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+) : SwipeableState<ModalBottomSheetValue>(
+    initialValue = initialValue,
+    animationSpec = animationSpec,
+    confirmStateChange = confirmStateChange
+) {
+    /**
+     * Whether the bottom sheet is visible.
+     */
+    val isVisible: Boolean
+        get() = currentValue != Hidden
+
+    internal val hasHalfExpandedState: Boolean
+        get() = anchors.values.contains(HalfExpanded)
+
+    constructor(
+        initialValue: ModalBottomSheetValue,
+        animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+        confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+    ) : this(initialValue, animationSpec, isSkipHalfExpanded = false, confirmStateChange)
+
+    init {
+        if (isSkipHalfExpanded) {
+            require(initialValue != HalfExpanded) {
+                "The initial value must not be set to HalfExpanded if skipHalfExpanded is set to" +
+                        " true."
+            }
+        }
+    }
+
+    /**
+     * Show the bottom sheet with animation and suspend until it's shown. If the sheet is taller
+     * than 50% of the parent's height, the bottom sheet will be half expanded. Otherwise it will be
+     * fully expanded.
+     *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    suspend fun show() {
+        val targetValue = when {
+            hasHalfExpandedState -> HalfExpanded
+            else -> Expanded
+        }
+        animateTo(targetValue = targetValue)
+    }
+
+    /**
+     * Half expand the bottom sheet if half expand is enabled with animation and suspend until it
+     * animation is complete or cancelled
+     *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    internal suspend fun halfExpand() {
+        if (!hasHalfExpandedState) {
+            return
+        }
+        animateTo(HalfExpanded)
+    }
+
+    /**
+     * Fully expand the bottom sheet with animation and suspend until it if fully expanded or
+     * animation has been cancelled.
+     * *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    internal suspend fun expand() = animateTo(Expanded)
+
+    /**
+     * Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
+     * been cancelled.
+     *
+     * @throws [CancellationException] if the animation is interrupted
+     */
+    suspend fun hide() = animateTo(Hidden)
+
+    internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [ModalBottomSheetState].
+         */
+        fun Saver(
+            animationSpec: AnimationSpec<Float>,
+            skipHalfExpanded: Boolean,
+            confirmStateChange: (ModalBottomSheetValue) -> Boolean
+        ): Saver<ModalBottomSheetState, *> = Saver(
+            save = { it.currentValue },
+            restore = {
+                ModalBottomSheetState(
+                    initialValue = it,
+                    animationSpec = animationSpec,
+                    isSkipHalfExpanded = skipHalfExpanded,
+                    confirmStateChange = confirmStateChange
+                )
+            }
+        )
+
+        /**
+         * The default [Saver] implementation for [ModalBottomSheetState].
+         */
+        @Deprecated(
+            message = "Please specify the skipHalfExpanded parameter",
+            replaceWith = ReplaceWith(
+                "ModalBottomSheetState.Saver(" +
+                        "animationSpec = animationSpec," +
+                        "skipHalfExpanded = ," +
+                        "confirmStateChange = confirmStateChange" +
+                        ")"
+            )
+        )
+        fun Saver(
+            animationSpec: AnimationSpec<Float>,
+            confirmStateChange: (ModalBottomSheetValue) -> Boolean
+        ): Saver<ModalBottomSheetState, *> = Saver(
+            animationSpec = animationSpec,
+            skipHalfExpanded = false,
+            confirmStateChange = confirmStateChange
+        )
+    }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param skipHalfExpanded Whether the half expanded state, if the sheet is tall enough, should
+ * be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
+ * [Hidden] state when hiding the sheet, either programmatically or by user interaction.
+ * <b>Must not be set to true if the [initialValue] is [ModalBottomSheetValue.HalfExpanded].</b>
+ * If supplied with [ModalBottomSheetValue.HalfExpanded] for the [initialValue], an
+ * [IllegalArgumentException] will be thrown.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun rememberModalBottomSheetState(
+    initialValue: ModalBottomSheetValue,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+    skipHalfExpanded: Boolean,
+    confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState {
+    return rememberSaveable(
+        initialValue, animationSpec, skipHalfExpanded, confirmStateChange,
+        saver = ModalBottomSheetState.Saver(
+            animationSpec = animationSpec,
+            skipHalfExpanded = skipHalfExpanded,
+            confirmStateChange = confirmStateChange
+        )
+    ) {
+        ModalBottomSheetState(
+            initialValue = initialValue,
+            animationSpec = animationSpec,
+            isSkipHalfExpanded = skipHalfExpanded,
+            confirmStateChange = confirmStateChange
+        )
+    }
+}
+
+/**
+ * Create a [ModalBottomSheetState] and [remember] it.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun rememberModalBottomSheetState(
+    initialValue: ModalBottomSheetValue,
+    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+    confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
+): ModalBottomSheetState = rememberModalBottomSheetState(
+    initialValue = initialValue,
+    animationSpec = animationSpec,
+    skipHalfExpanded = false,
+    confirmStateChange = confirmStateChange
+)
+
+/**
+ * <a href="https://material.io/components/sheets-bottom#modal-bottom-sheet" class="external" target="_blank">Material Design modal bottom sheet</a>.
+ *
+ * Modal bottom sheets present a set of choices while blocking interaction with the rest of the
+ * screen. They are an alternative to inline menus and simple dialogs, providing
+ * additional room for content, iconography, and actions.
+ *
+ * ![Modal bottom sheet image](https://developer.android.com/images/reference/androidx/compose/material/modal-bottom-sheet.png)
+ *
+ * A simple example of a modal bottom sheet looks like this:
+ *
+ * @sample androidx.compose.material.samples.ModalBottomSheetSample
+ *
+ * @param sheetContent The content of the bottom sheet.
+ * @param modifier Optional [Modifier] for the entire component.
+ * @param sheetState The state of the bottom sheet.
+ * @param sheetShape The shape of the bottom sheet.
+ * @param sheetElevation The elevation of the bottom sheet.
+ * @param sheetBackgroundColor The background color of the bottom sheet.
+ * @param sheetContentColor The preferred content color provided by the bottom sheet to its
+ * children. Defaults to the matching content color for [sheetBackgroundColor], or if that is not
+ * a color from the theme, this will keep the same content color set above the bottom sheet.
+ * @param scrimColor The color of the scrim that is applied to the rest of the screen when the
+ * bottom sheet is visible. If the color passed is [Color.Unspecified], then a scrim will no
+ * longer be applied and the bottom sheet will not block interaction with the rest of the screen
+ * when visible.
+ * @param content The content of rest of the screen.
+ */
+@Composable
+fun ModalBottomSheetLayout(
+    sheetContent: @Composable ColumnScope.() -> Unit,
+    modifier: Modifier = Modifier,
+    sheetState: ModalBottomSheetState =
+        rememberModalBottomSheetState(Hidden),
+    sheetShape: Shape = MaterialTheme.shapes.large,
+    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+    sheetBackgroundColor: Color = ModalBottomSheetDefaults.scrimColor,
+    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+    content: @Composable () -> Unit
+) {
+    val scope = rememberCoroutineScope()
+    BoxWithConstraints(modifier) {
+        val fullHeight = constraints.maxHeight.toFloat()
+        val sheetHeightState = remember { mutableStateOf<Float?>(null) }
+        Box(Modifier.fillMaxSize()) {
+            content()
+            Scrim(
+                color = scrimColor,
+                onDismiss = {
+                    if (sheetState.confirmStateChange(Hidden)) {
+                        scope.launch { sheetState.hide() }
+                    }
+                },
+                visible = sheetState.targetValue != Hidden
+            )
+        }
+        Surface(
+            Modifier
+                .fillMaxWidth()
+                .nestedScroll(sheetState.nestedScrollConnection)
+                .offset {
+                    val y = if (sheetState.anchors.isEmpty()) {
+                        // if we don't know our anchors yet, render the sheet as hidden
+                        fullHeight.roundToInt()
+                    } else {
+                        // if we do know our anchors, respect them
+                        sheetState.offset.value.roundToInt()
+                    }
+                    IntOffset(0, y)
+                }
+                .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
+                .onGloballyPositioned {
+                    sheetHeightState.value = it.size.height.toFloat()
+                }
+                .semantics {
+                    if (sheetState.isVisible) {
+                        dismiss {
+                            if (sheetState.confirmStateChange(Hidden)) {
+                                scope.launch { sheetState.hide() }
+                            }
+                            true
+                        }
+                        if (sheetState.currentValue == HalfExpanded) {
+                            expand {
+                                if (sheetState.confirmStateChange(Expanded)) {
+                                    scope.launch { sheetState.expand() }
+                                }
+                                true
+                            }
+                        } else if (sheetState.hasHalfExpandedState) {
+                            collapse {
+                                if (sheetState.confirmStateChange(HalfExpanded)) {
+                                    scope.launch { sheetState.halfExpand() }
+                                }
+                                true
+                            }
+                        }
+                    }
+                },
+            shape = sheetShape,
+            shadowElevation = sheetElevation,
+            color = sheetBackgroundColor,
+            contentColor = sheetContentColor
+        ) {
+            Column(content = sheetContent)
+        }
+    }
+}
+
+@Suppress("ModifierInspectorInfo")
+private fun Modifier.bottomSheetSwipeable(
+    sheetState: ModalBottomSheetState,
+    fullHeight: Float,
+    sheetHeightState: State<Float?>
+): Modifier {
+    val sheetHeight = sheetHeightState.value
+    val modifier = if (sheetHeight != null) {
+        val anchors = if (sheetHeight < fullHeight / 2 || sheetState.isSkipHalfExpanded) {
+            mapOf(
+                fullHeight to Hidden,
+                fullHeight - sheetHeight to Expanded
+            )
+        } else {
+            mapOf(
+                fullHeight to Hidden,
+                fullHeight / 2 to HalfExpanded,
+                max(0f, fullHeight - sheetHeight) to Expanded
+            )
+        }
+        Modifier.swipeable(
+            state = sheetState,
+            anchors = anchors,
+            orientation = Orientation.Vertical,
+            enabled = sheetState.currentValue != Hidden,
+            resistance = null
+        )
+    } else {
+        Modifier
+    }
+
+    return this.then(modifier)
+}
+
+@Composable
+private fun Scrim(
+    color: Color,
+    onDismiss: () -> Unit,
+    visible: Boolean
+) {
+    if (color.isSpecified) {
+        val alpha by animateFloatAsState(
+            targetValue = if (visible) 1f else 0f,
+            animationSpec = TweenSpec()
+        )
+        LocalConfiguration.current
+        val resources = LocalContext.current.resources
+        val closeSheet = resources.getString(R.string.close_sheet)
+        val dismissModifier = if (visible) {
+            Modifier
+                .pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
+                .semantics(mergeDescendants = true) {
+                    contentDescription = closeSheet
+                    onClick { onDismiss(); true }
+                }
+        } else {
+            Modifier
+        }
+
+        Canvas(
+            Modifier
+                .fillMaxSize()
+                .then(dismissModifier)
+        ) {
+            drawRect(color = color, alpha = alpha)
+        }
+    }
+}
+
+/**
+ * Contains useful Defaults for [ModalBottomSheetLayout].
+ */
+object ModalBottomSheetDefaults {
+
+    /**
+     * The default elevation used by [ModalBottomSheetLayout].
+     */
+    val Elevation = 16.dp
+
+    /**
+     * The default scrim color used by [ModalBottomSheetLayout].
+     */
+    val scrimColor: Color
+        @Composable
+        get() = LocalAndroidColorScheme.current.colorSurfaceHighlight
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt
new file mode 100644
index 0000000..3e2de83
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/material/Swipeable.kt
@@ -0,0 +1,875 @@
+/*
+ * 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.credentialmanager.common.material
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import com.android.credentialmanager.common.material.SwipeableDefaults.AnimationSpec
+import com.android.credentialmanager.common.material.SwipeableDefaults.StandardResistanceFactor
+import com.android.credentialmanager.common.material.SwipeableDefaults.VelocityThreshold
+import com.android.credentialmanager.common.material.SwipeableDefaults.resistanceConfig
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+import kotlin.math.PI
+import kotlin.math.abs
+import kotlin.math.sign
+import kotlin.math.sin
+
+/**
+ * State of the [swipeable] modifier.
+ *
+ * This contains necessary information about any ongoing swipe or animation and provides methods
+ * to change the state either immediately or by starting an animation. To create and remember a
+ * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Stable
+open class SwipeableState<T>(
+    initialValue: T,
+    internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
+    internal val confirmStateChange: (newValue: T) -> Boolean = { true }
+) {
+    /**
+     * The current value of the state.
+     *
+     * If no swipe or animation is in progress, this corresponds to the anchor at which the
+     * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
+     * the last anchor at which the [swipeable] was settled before the swipe or animation started.
+     */
+    var currentValue: T by mutableStateOf(initialValue)
+        private set
+
+    /**
+     * Whether the state is currently animating.
+     */
+    var isAnimationRunning: Boolean by mutableStateOf(false)
+        private set
+
+    /**
+     * The current position (in pixels) of the [swipeable].
+     *
+     * You should use this state to offset your content accordingly. The recommended way is to
+     * use `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
+     */
+    val offset: State<Float> get() = offsetState
+
+    /**
+     * The amount by which the [swipeable] has been swiped past its bounds.
+     */
+    val overflow: State<Float> get() = overflowState
+
+    // Use `Float.NaN` as a placeholder while the state is uninitialised.
+    private val offsetState = mutableStateOf(0f)
+    private val overflowState = mutableStateOf(0f)
+
+    // the source of truth for the "real"(non ui) position
+    // basically position in bounds + overflow
+    private val absoluteOffset = mutableStateOf(0f)
+
+    // current animation target, if animating, otherwise null
+    private val animationTarget = mutableStateOf<Float?>(null)
+
+    internal var anchors by mutableStateOf(emptyMap<Float, T>())
+
+    private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
+        snapshotFlow { anchors }
+            .filter { it.isNotEmpty() }
+            .take(1)
+
+    internal var minBound = Float.NEGATIVE_INFINITY
+    internal var maxBound = Float.POSITIVE_INFINITY
+
+    internal fun ensureInit(newAnchors: Map<Float, T>) {
+        if (anchors.isEmpty()) {
+            // need to do initial synchronization synchronously :(
+            val initialOffset = newAnchors.getOffset(currentValue)
+            requireNotNull(initialOffset) {
+                "The initial value must have an associated anchor."
+            }
+            offsetState.value = initialOffset
+            absoluteOffset.value = initialOffset
+        }
+    }
+
+    internal suspend fun processNewAnchors(
+        oldAnchors: Map<Float, T>,
+        newAnchors: Map<Float, T>
+    ) {
+        if (oldAnchors.isEmpty()) {
+            // If this is the first time that we receive anchors, then we need to initialise
+            // the state so we snap to the offset associated to the initial value.
+            minBound = newAnchors.keys.minOrNull()!!
+            maxBound = newAnchors.keys.maxOrNull()!!
+            val initialOffset = newAnchors.getOffset(currentValue)
+            requireNotNull(initialOffset) {
+                "The initial value must have an associated anchor."
+            }
+            snapInternalToOffset(initialOffset)
+        } else if (newAnchors != oldAnchors) {
+            // If we have received new anchors, then the offset of the current value might
+            // have changed, so we need to animate to the new offset. If the current value
+            // has been removed from the anchors then we animate to the closest anchor
+            // instead. Note that this stops any ongoing animation.
+            minBound = Float.NEGATIVE_INFINITY
+            maxBound = Float.POSITIVE_INFINITY
+            val animationTargetValue = animationTarget.value
+            // if we're in the animation already, let's find it a new home
+            val targetOffset = if (animationTargetValue != null) {
+                // first, try to map old state to the new state
+                val oldState = oldAnchors[animationTargetValue]
+                val newState = newAnchors.getOffset(oldState)
+                // return new state if exists, or find the closes one among new anchors
+                newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
+            } else {
+                // we're not animating, proceed by finding the new anchors for an old value
+                val actualOldValue = oldAnchors[offset.value]
+                val value = if (actualOldValue == currentValue) currentValue else actualOldValue
+                newAnchors.getOffset(value) ?: newAnchors
+                    .keys.minByOrNull { abs(it - offset.value) }!!
+            }
+            try {
+                animateInternalToOffset(targetOffset, animationSpec)
+            } catch (c: CancellationException) {
+                // If the animation was interrupted for any reason, snap as a last resort.
+                snapInternalToOffset(targetOffset)
+            } finally {
+                currentValue = newAnchors.getValue(targetOffset)
+                minBound = newAnchors.keys.minOrNull()!!
+                maxBound = newAnchors.keys.maxOrNull()!!
+            }
+        }
+    }
+
+    internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
+
+    internal var velocityThreshold by mutableStateOf(0f)
+
+    internal var resistance: ResistanceConfig? by mutableStateOf(null)
+
+    internal val draggableState = DraggableState {
+        val newAbsolute = absoluteOffset.value + it
+        val clamped = newAbsolute.coerceIn(minBound, maxBound)
+        val overflow = newAbsolute - clamped
+        val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
+        offsetState.value = clamped + resistanceDelta
+        overflowState.value = overflow
+        absoluteOffset.value = newAbsolute
+    }
+
+    private suspend fun snapInternalToOffset(target: Float) {
+        draggableState.drag {
+            dragBy(target - absoluteOffset.value)
+        }
+    }
+
+    private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
+        draggableState.drag {
+            var prevValue = absoluteOffset.value
+            animationTarget.value = target
+            isAnimationRunning = true
+            try {
+                Animatable(prevValue).animateTo(target, spec) {
+                    dragBy(this.value - prevValue)
+                    prevValue = this.value
+                }
+            } finally {
+                animationTarget.value = null
+                isAnimationRunning = false
+            }
+        }
+    }
+
+    /**
+     * The target value of the state.
+     *
+     * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
+     * swipe finished. If an animation is running, this is the target value of that animation.
+     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+     */
+    val targetValue: T
+        get() {
+            // TODO(calintat): Track current velocity (b/149549482) and use that here.
+            val target = animationTarget.value ?: computeTarget(
+                offset = offset.value,
+                lastValue = anchors.getOffset(currentValue) ?: offset.value,
+                anchors = anchors.keys,
+                thresholds = thresholds,
+                velocity = 0f,
+                velocityThreshold = Float.POSITIVE_INFINITY
+            )
+            return anchors[target] ?: currentValue
+        }
+
+    /**
+     * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
+     *
+     * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
+     */
+    val progress: SwipeProgress<T>
+        get() {
+            val bounds = findBounds(offset.value, anchors.keys)
+            val from: T
+            val to: T
+            val fraction: Float
+            when (bounds.size) {
+                0 -> {
+                    from = currentValue
+                    to = currentValue
+                    fraction = 1f
+                }
+                1 -> {
+                    from = anchors.getValue(bounds[0])
+                    to = anchors.getValue(bounds[0])
+                    fraction = 1f
+                }
+                else -> {
+                    val (a, b) =
+                        if (direction > 0f) {
+                            bounds[0] to bounds[1]
+                        } else {
+                            bounds[1] to bounds[0]
+                        }
+                    from = anchors.getValue(a)
+                    to = anchors.getValue(b)
+                    fraction = (offset.value - a) / (b - a)
+                }
+            }
+            return SwipeProgress(from, to, fraction)
+        }
+
+    /**
+     * The direction in which the [swipeable] is moving, relative to the current [currentValue].
+     *
+     * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
+     * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
+     */
+    val direction: Float
+        get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
+
+    /**
+     * Set the state without any animation and suspend until it's set
+     *
+     * @param targetValue The new target value to set [currentValue] to.
+     */
+    suspend fun snapTo(targetValue: T) {
+        latestNonEmptyAnchorsFlow.collect { anchors ->
+            val targetOffset = anchors.getOffset(targetValue)
+            requireNotNull(targetOffset) {
+                "The target value must have an associated anchor."
+            }
+            snapInternalToOffset(targetOffset)
+            currentValue = targetValue
+        }
+    }
+
+    /**
+     * Set the state to the target value by starting an animation.
+     *
+     * @param targetValue The new value to animate to.
+     * @param anim The animation that will be used to animate to the new value.
+     */
+    suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
+        latestNonEmptyAnchorsFlow.collect { anchors ->
+            try {
+                val targetOffset = anchors.getOffset(targetValue)
+                requireNotNull(targetOffset) {
+                    "The target value must have an associated anchor."
+                }
+                animateInternalToOffset(targetOffset, anim)
+            } finally {
+                val endOffset = absoluteOffset.value
+                val endValue = anchors
+                    // fighting rounding error once again, anchor should be as close as 0.5 pixels
+                    .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
+                    .values.firstOrNull() ?: currentValue
+                currentValue = endValue
+            }
+        }
+    }
+
+    /**
+     * Perform fling with settling to one of the anchors which is determined by the given
+     * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
+     * since it will settle at the anchor.
+     *
+     * In general cases, [swipeable] flings by itself when being swiped. This method is to be
+     * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+     * want to trigger settling fling when the child scroll container reaches the bound.
+     *
+     * @param velocity velocity to fling and settle with
+     *
+     * @return the reason fling ended
+     */
+    suspend fun performFling(velocity: Float) {
+        latestNonEmptyAnchorsFlow.collect { anchors ->
+            val lastAnchor = anchors.getOffset(currentValue)!!
+            val targetValue = computeTarget(
+                offset = offset.value,
+                lastValue = lastAnchor,
+                anchors = anchors.keys,
+                thresholds = thresholds,
+                velocity = velocity,
+                velocityThreshold = velocityThreshold
+            )
+            val targetState = anchors[targetValue]
+            if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
+            // If the user vetoed the state change, rollback to the previous state.
+            else animateInternalToOffset(lastAnchor, animationSpec)
+        }
+    }
+
+    /**
+     * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
+     * gesture flow.
+     *
+     * Note: This method performs generic drag and it won't settle to any particular anchor, *
+     * leaving swipeable in between anchors. When done dragging, [performFling] must be
+     * called as well to ensure swipeable will settle at the anchor.
+     *
+     * In general cases, [swipeable] drags by itself when being swiped. This method is to be
+     * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+     * want to force drag when the child scroll container reaches the bound.
+     *
+     * @param delta delta in pixels to drag by
+     *
+     * @return the amount of [delta] consumed
+     */
+    fun performDrag(delta: Float): Float {
+        val potentiallyConsumed = absoluteOffset.value + delta
+        val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+        val deltaToConsume = clamped - absoluteOffset.value
+        if (abs(deltaToConsume) > 0) {
+            draggableState.dispatchRawDelta(deltaToConsume)
+        }
+        return deltaToConsume
+    }
+
+    companion object {
+        /**
+         * The default [Saver] implementation for [SwipeableState].
+         */
+        fun <T : Any> Saver(
+            animationSpec: AnimationSpec<Float>,
+            confirmStateChange: (T) -> Boolean
+        ) = Saver<SwipeableState<T>, T>(
+            save = { it.currentValue },
+            restore = { SwipeableState(it, animationSpec, confirmStateChange) }
+        )
+    }
+}
+
+/**
+ * Collects information about the ongoing swipe or animation in [swipeable].
+ *
+ * To access this information, use [SwipeableState.progress].
+ *
+ * @param from The state corresponding to the anchor we are moving away from.
+ * @param to The state corresponding to the anchor we are moving towards.
+ * @param fraction The fraction that the current position represents between [from] and [to].
+ * Must be between `0` and `1`.
+ */
+@Immutable
+class SwipeProgress<T>(
+    val from: T,
+    val to: T,
+    /*@FloatRange(from = 0.0, to = 1.0)*/
+    val fraction: Float
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SwipeProgress<*>) return false
+
+        if (from != other.from) return false
+        if (to != other.to) return false
+        if (fraction != other.fraction) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = from?.hashCode() ?: 0
+        result = 31 * result + (to?.hashCode() ?: 0)
+        result = 31 * result + fraction.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
+    }
+}
+
+/**
+ * Create and [remember] a [SwipeableState] with the default animation clock.
+ *
+ * @param initialValue The initial value of the state.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Composable
+fun <T : Any> rememberSwipeableState(
+    initialValue: T,
+    animationSpec: AnimationSpec<Float> = AnimationSpec,
+    confirmStateChange: (newValue: T) -> Boolean = { true }
+): SwipeableState<T> {
+    return rememberSaveable(
+        saver = SwipeableState.Saver(
+            animationSpec = animationSpec,
+            confirmStateChange = confirmStateChange
+        )
+    ) {
+        SwipeableState(
+            initialValue = initialValue,
+            animationSpec = animationSpec,
+            confirmStateChange = confirmStateChange
+        )
+    }
+}
+
+/**
+ * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
+ *  1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
+ *  2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
+ *  [value] will be notified to update their state to the new value of the [SwipeableState] by
+ *  invoking [onValueChange]. If the owner does not update their state to the provided value for
+ *  some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
+ */
+@Composable
+internal fun <T : Any> rememberSwipeableStateFor(
+    value: T,
+    onValueChange: (T) -> Unit,
+    animationSpec: AnimationSpec<Float> = AnimationSpec
+): SwipeableState<T> {
+    val swipeableState = remember {
+        SwipeableState(
+            initialValue = value,
+            animationSpec = animationSpec,
+            confirmStateChange = { true }
+        )
+    }
+    val forceAnimationCheck = remember { mutableStateOf(false) }
+    LaunchedEffect(value, forceAnimationCheck.value) {
+        if (value != swipeableState.currentValue) {
+            swipeableState.animateTo(value)
+        }
+    }
+    DisposableEffect(swipeableState.currentValue) {
+        if (value != swipeableState.currentValue) {
+            onValueChange(swipeableState.currentValue)
+            forceAnimationCheck.value = !forceAnimationCheck.value
+        }
+        onDispose { }
+    }
+    return swipeableState
+}
+
+/**
+ * Enable swipe gestures between a set of predefined states.
+ *
+ * To use this, you must provide a map of anchors (in pixels) to states (of type [T]).
+ * Note that this map cannot be empty and cannot have two anchors mapped to the same state.
+ *
+ * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
+ * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
+ * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
+ * reached, the value of the [SwipeableState] will also be updated to the state corresponding to
+ * the new anchor. The target anchor is calculated based on the provided positional [thresholds].
+ *
+ * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
+ * past these bounds, a resistance effect will be applied by default. The amount of resistance at
+ * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
+ *
+ * For an example of a [swipeable] with three states, see:
+ *
+ * @sample androidx.compose.material.samples.SwipeableSample
+ *
+ * @param T The type of the state.
+ * @param state The state of the [swipeable].
+ * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
+ * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
+ * used to determine which state to animate to when swiping stops. This is represented as a lambda
+ * that takes two states and returns the threshold between them in the form of a [ThresholdConfig].
+ * Note that the order of the states corresponds to the swipe direction.
+ * @param orientation The orientation in which the [swipeable] can be swiped.
+ * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
+ * swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to
+ * the internal [Modifier.draggable].
+ * @param resistance Controls how much resistance will be applied when swiping past the bounds.
+ * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed
+ * in order to animate to the next state, even if the positional [thresholds] have not been reached.
+ */
+fun <T> Modifier.swipeable(
+    state: SwipeableState<T>,
+    anchors: Map<Float, T>,
+    orientation: Orientation,
+    enabled: Boolean = true,
+    reverseDirection: Boolean = false,
+    interactionSource: MutableInteractionSource? = null,
+    thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
+    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
+    velocityThreshold: Dp = VelocityThreshold
+) = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "swipeable"
+        properties["state"] = state
+        properties["anchors"] = anchors
+        properties["orientation"] = orientation
+        properties["enabled"] = enabled
+        properties["reverseDirection"] = reverseDirection
+        properties["interactionSource"] = interactionSource
+        properties["thresholds"] = thresholds
+        properties["resistance"] = resistance
+        properties["velocityThreshold"] = velocityThreshold
+    }
+) {
+    require(anchors.isNotEmpty()) {
+        "You must have at least one anchor."
+    }
+    require(anchors.values.distinct().count() == anchors.size) {
+        "You cannot have two anchors mapped to the same state."
+    }
+    val density = LocalDensity.current
+    state.ensureInit(anchors)
+    LaunchedEffect(anchors, state) {
+        val oldAnchors = state.anchors
+        state.anchors = anchors
+        state.resistance = resistance
+        state.thresholds = { a, b ->
+            val from = anchors.getValue(a)
+            val to = anchors.getValue(b)
+            with(thresholds(from, to)) { density.computeThreshold(a, b) }
+        }
+        with(density) {
+            state.velocityThreshold = velocityThreshold.toPx()
+        }
+        state.processNewAnchors(oldAnchors, anchors)
+    }
+
+    Modifier.draggable(
+        orientation = orientation,
+        enabled = enabled,
+        reverseDirection = reverseDirection,
+        interactionSource = interactionSource,
+        startDragImmediately = state.isAnimationRunning,
+        onDragStopped = { velocity -> launch { state.performFling(velocity) } },
+        state = state.draggableState
+    )
+}
+
+/**
+ * Interface to compute a threshold between two anchors/states in a [swipeable].
+ *
+ * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
+ */
+@Stable
+interface ThresholdConfig {
+    /**
+     * Compute the value of the threshold (in pixels), once the values of the anchors are known.
+     */
+    fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
+}
+
+/**
+ * A fixed threshold will be at an [offset] away from the first anchor.
+ *
+ * @param offset The offset (in dp) that the threshold will be at.
+ */
+@Immutable
+data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
+    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+        return fromValue + offset.toPx() * sign(toValue - fromValue)
+    }
+}
+
+/**
+ * A fractional threshold will be at a [fraction] of the way between the two anchors.
+ *
+ * @param fraction The fraction (between 0 and 1) that the threshold will be at.
+ */
+@Immutable
+data class FractionalThreshold(
+    /*@FloatRange(from = 0.0, to = 1.0)*/
+    private val fraction: Float
+) : ThresholdConfig {
+    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+        return lerp(fromValue, toValue, fraction)
+    }
+}
+
+/**
+ * Specifies how resistance is calculated in [swipeable].
+ *
+ * There are two things needed to calculate resistance: the resistance basis determines how much
+ * overflow will be consumed to achieve maximum resistance, and the resistance factor determines
+ * the amount of resistance (the larger the resistance factor, the stronger the resistance).
+ *
+ * The resistance basis is usually either the size of the component which [swipeable] is applied
+ * to, or the distance between the minimum and maximum anchors. For a constructor in which the
+ * resistance basis defaults to the latter, consider using [resistanceConfig].
+ *
+ * You may specify different resistance factors for each bound. Consider using one of the default
+ * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user
+ * has run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe
+ * this right now. Also, you can set either factor to 0 to disable resistance at that bound.
+ *
+ * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
+ * @param factorAtMin The factor by which to scale the resistance at the minimum bound.
+ * Must not be negative.
+ * @param factorAtMax The factor by which to scale the resistance at the maximum bound.
+ * Must not be negative.
+ */
+@Immutable
+class ResistanceConfig(
+    /*@FloatRange(from = 0.0, fromInclusive = false)*/
+    val basis: Float,
+    /*@FloatRange(from = 0.0)*/
+    val factorAtMin: Float = StandardResistanceFactor,
+    /*@FloatRange(from = 0.0)*/
+    val factorAtMax: Float = StandardResistanceFactor
+) {
+    fun computeResistance(overflow: Float): Float {
+        val factor = if (overflow < 0) factorAtMin else factorAtMax
+        if (factor == 0f) return 0f
+        val progress = (overflow / basis).coerceIn(-1f, 1f)
+        return basis / factor * sin(progress * PI.toFloat() / 2)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ResistanceConfig) return false
+
+        if (basis != other.basis) return false
+        if (factorAtMin != other.factorAtMin) return false
+        if (factorAtMax != other.factorAtMax) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = basis.hashCode()
+        result = 31 * result + factorAtMin.hashCode()
+        result = 31 * result + factorAtMax.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
+    }
+}
+
+/**
+ *  Given an offset x and a set of anchors, return a list of anchors:
+ *   1. [ ] if the set of anchors is empty,
+ *   2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x'
+ *      is x rounded to the exact value of the matching anchor,
+ *   3. [ min ] if min is the minimum anchor and x < min,
+ *   4. [ max ] if max is the maximum anchor and x > max, or
+ *   5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
+ */
+private fun findBounds(
+    offset: Float,
+    anchors: Set<Float>
+): List<Float> {
+    // Find the anchors the target lies between with a little bit of rounding error.
+    val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
+    val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
+
+    return when {
+        a == null ->
+            // case 1 or 3
+            listOfNotNull(b)
+        b == null ->
+            // case 4
+            listOf(a)
+        a == b ->
+            // case 2
+            // Can't return offset itself here since it might not be exactly equal
+            // to the anchor, despite being considered an exact match.
+            listOf(a)
+        else ->
+            // case 5
+            listOf(a, b)
+    }
+}
+
+private fun computeTarget(
+    offset: Float,
+    lastValue: Float,
+    anchors: Set<Float>,
+    thresholds: (Float, Float) -> Float,
+    velocity: Float,
+    velocityThreshold: Float
+): Float {
+    val bounds = findBounds(offset, anchors)
+    return when (bounds.size) {
+        0 -> lastValue
+        1 -> bounds[0]
+        else -> {
+            val lower = bounds[0]
+            val upper = bounds[1]
+            if (lastValue <= offset) {
+                // Swiping from lower to upper (positive).
+                if (velocity >= velocityThreshold) {
+                    return upper
+                } else {
+                    val threshold = thresholds(lower, upper)
+                    if (offset < threshold) lower else upper
+                }
+            } else {
+                // Swiping from upper to lower (negative).
+                if (velocity <= -velocityThreshold) {
+                    return lower
+                } else {
+                    val threshold = thresholds(upper, lower)
+                    if (offset > threshold) upper else lower
+                }
+            }
+        }
+    }
+}
+
+private fun <T> Map<Float, T>.getOffset(state: T): Float? {
+    return entries.firstOrNull { it.value == state }?.key
+}
+
+/**
+ * Contains useful defaults for [swipeable] and [SwipeableState].
+ */
+object SwipeableDefaults {
+    /**
+     * The default animation used by [SwipeableState].
+     */
+    val AnimationSpec = SpringSpec<Float>()
+
+    /**
+     * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
+     */
+    val VelocityThreshold = 125.dp
+
+    /**
+     * A stiff resistance factor which indicates that swiping isn't available right now.
+     */
+    const val StiffResistanceFactor = 20f
+
+    /**
+     * A standard resistance factor which indicates that the user has run out of things to see.
+     */
+    const val StandardResistanceFactor = 10f
+
+    /**
+     * The default resistance config used by [swipeable].
+     *
+     * This returns `null` if there is one anchor. If there are at least two anchors, it returns
+     * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
+     */
+    fun resistanceConfig(
+        anchors: Set<Float>,
+        factorAtMin: Float = StandardResistanceFactor,
+        factorAtMax: Float = StandardResistanceFactor
+    ): ResistanceConfig? {
+        return if (anchors.size <= 1) {
+            null
+        } else {
+            val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
+            ResistanceConfig(basis, factorAtMin, factorAtMax)
+        }
+    }
+}
+
+// temp default nested scroll connection for swipeables which desire as an opt in
+// revisit in b/174756744 as all types will have their own specific connection probably
+internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
+    get() = object : NestedScrollConnection {
+        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+            val delta = available.toFloat()
+            return if (delta < 0 && source == NestedScrollSource.Drag) {
+                performDrag(delta).toOffset()
+            } else {
+                Offset.Zero
+            }
+        }
+
+        override fun onPostScroll(
+            consumed: Offset,
+            available: Offset,
+            source: NestedScrollSource
+        ): Offset {
+            return if (source == NestedScrollSource.Drag) {
+                performDrag(available.toFloat()).toOffset()
+            } else {
+                Offset.Zero
+            }
+        }
+
+        override suspend fun onPreFling(available: Velocity): Velocity {
+            val toFling = Offset(available.x, available.y).toFloat()
+            return if (toFling < 0 && offset.value > minBound) {
+                performFling(velocity = toFling)
+                // since we go to the anchor with tween settling, consume all for the best UX
+                available
+            } else {
+                Velocity.Zero
+            }
+        }
+
+        override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+            performFling(velocity = Offset(available.x, available.y).toFloat())
+            return available
+        }
+
+        private fun Float.toOffset(): Offset = Offset(0f, this)
+
+        private fun Offset.toFloat(): Float = this.y
+    }
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
new file mode 100644
index 0000000..177d0e0
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.credentialmanager.common.ui
+
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+
+@Composable
+fun CancelButton(text: String, onClick: () -> Unit) {
+    TextButton(onClick = onClick) {
+        Text(text = text)
+    }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt
new file mode 100644
index 0000000..b2b0bdc
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ConfirmButton.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.credentialmanager.common.ui
+
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+
+@Composable
+fun ConfirmButton(text: String, onClick: () -> Unit) {
+    FilledTonalButton(onClick = onClick) {
+        Text(text = text)
+    }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
new file mode 100644
index 0000000..51a1cbb
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.credentialmanager.common.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SuggestionChip
+import androidx.compose.material3.SuggestionChipDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.credentialmanager.ui.theme.EntryShape
+import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun Entry(
+    onClick: () -> Unit,
+    label: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (() -> Unit)? = null,
+) {
+    SuggestionChip(
+        modifier = modifier.fillMaxWidth(),
+        onClick = onClick,
+        shape = EntryShape.FullSmallRoundedCorner,
+        label = label,
+        icon = icon,
+        border = null,
+        colors = SuggestionChipDefaults.suggestionChipColors(
+            containerColor = LocalAndroidColorScheme.current.colorSurface,
+        ),
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TransparentBackgroundEntry(
+    onClick: () -> Unit,
+    label: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    icon: @Composable (() -> Unit)? = null,
+) {
+    SuggestionChip(
+        modifier = modifier.fillMaxWidth(),
+        onClick = onClick,
+        label = label,
+        icon = icon,
+        border = null,
+        colors = SuggestionChipDefaults.suggestionChipColors(
+            containerColor = Color.Transparent,
+        ),
+    )
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
new file mode 100644
index 0000000..27d366d
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -0,0 +1,659 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.android.credentialmanager.createflow
+
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Card
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.outlined.NewReleases
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
+import com.android.credentialmanager.R
+import com.android.credentialmanager.common.material.ModalBottomSheetLayout
+import com.android.credentialmanager.common.material.ModalBottomSheetValue
+import com.android.credentialmanager.common.material.rememberModalBottomSheetState
+import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.common.ui.ConfirmButton
+import com.android.credentialmanager.common.ui.Entry
+import com.android.credentialmanager.ui.theme.EntryShape
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CreateCredentialScreen(
+  viewModel: CreateCredentialViewModel,
+) {
+  val state = rememberModalBottomSheetState(
+    initialValue = ModalBottomSheetValue.Expanded,
+    skipHalfExpanded = true
+  )
+  ModalBottomSheetLayout(
+    sheetState = state,
+    sheetContent = {
+      val uiState = viewModel.uiState
+      when (uiState.currentScreenState) {
+        CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
+          onConfirm = viewModel::onConfirmIntro,
+          onCancel = viewModel::onCancel,
+        )
+        CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
+          enabledProviderList = uiState.enabledProviders,
+          onCancel = viewModel::onCancel,
+          onProviderSelected = viewModel::onProviderSelected
+        )
+        CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
+          requestDisplayInfo = uiState.requestDisplayInfo,
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
+          onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected,
+          onConfirm = viewModel::onPrimaryCreateOptionInfoSelected,
+          onCancel = viewModel::onCancel,
+          multiProvider = uiState.enabledProviders.size > 1,
+          onMoreOptionsSelected = viewModel::onMoreOptionsSelected
+        )
+        CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
+            requestDisplayInfo = uiState.requestDisplayInfo,
+            enabledProviderList = uiState.enabledProviders,
+            disabledProviderList = uiState.disabledProviders,
+            onBackButtonSelected = viewModel::onBackButtonSelected,
+            onOptionSelected = viewModel::onMoreOptionsRowSelected,
+            onDisabledPasswordManagerSelected = viewModel::onDisabledPasswordManagerSelected,
+            onRemoteEntrySelected = viewModel::onRemoteEntrySelected
+          )
+        CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
+        )
+      }
+    },
+    scrimColor = MaterialTheme.colorScheme.scrim,
+    sheetShape = EntryShape.TopRoundedCorner,
+  ) {}
+  LaunchedEffect(state.currentValue) {
+    if (state.currentValue == ModalBottomSheetValue.Hidden) {
+      viewModel.onCancel()
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ConfirmationCard(
+  onConfirm: () -> Unit,
+  onCancel: () -> Unit,
+) {
+  Card() {
+    Column() {
+      Icon(
+        painter = painterResource(R.drawable.ic_passkey),
+        contentDescription = null,
+        tint = Color.Unspecified,
+        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
+      )
+      Text(
+        text = stringResource(R.string.passkey_creation_intro_title),
+        style = MaterialTheme.typography.titleMedium,
+        modifier = Modifier
+          .padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally)
+      )
+      Divider(
+        thickness = 24.dp,
+        color = Color.Transparent
+      )
+      Text(
+        text = stringResource(R.string.passkey_creation_intro_body),
+        style = MaterialTheme.typography.bodyLarge,
+        modifier = Modifier.padding(horizontal = 28.dp)
+      )
+      Divider(
+        thickness = 48.dp,
+        color = Color.Transparent
+      )
+      Row(
+        horizontalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(
+          stringResource(R.string.string_cancel),
+          onClick = onCancel
+        )
+        ConfirmButton(
+          stringResource(R.string.string_continue),
+          onClick = onConfirm
+        )
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 16.dp)
+      )
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ProviderSelectionCard(
+  enabledProviderList: List<EnabledProviderInfo>,
+  onProviderSelected: (String) -> Unit,
+  onCancel: () -> Unit
+) {
+  Card() {
+    Column() {
+      Text(
+        text = stringResource(R.string.choose_provider_title),
+        style = MaterialTheme.typography.titleMedium,
+        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+      )
+      Text(
+        text = stringResource(R.string.choose_provider_body),
+        style = MaterialTheme.typography.bodyLarge,
+        modifier = Modifier.padding(horizontal = 28.dp)
+      )
+      Divider(
+        thickness = 24.dp,
+        color = Color.Transparent
+      )
+      Card(
+        shape = MaterialTheme.shapes.medium,
+        modifier = Modifier
+          .padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally),
+      ) {
+        LazyColumn(
+          verticalArrangement = Arrangement.spacedBy(2.dp)
+        ) {
+          enabledProviderList.forEach {
+            item {
+              ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected)
+            }
+          }
+        }
+      }
+      Divider(
+        thickness = 24.dp,
+        color = Color.Transparent
+      )
+      Row(
+        horizontalArrangement = Arrangement.Start,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(stringResource(R.string.string_cancel), onCancel)
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 16.dp)
+      )
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsSelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
+  enabledProviderList: List<EnabledProviderInfo>,
+  disabledProviderList: List<DisabledProviderInfo>?,
+  onBackButtonSelected: () -> Unit,
+  onOptionSelected: (ActiveEntry) -> Unit,
+  onDisabledPasswordManagerSelected: () -> Unit,
+  onRemoteEntrySelected: () -> Unit,
+) {
+  Card() {
+    Column() {
+      TopAppBar(
+        title = {
+          Text(
+            text = when (requestDisplayInfo.type) {
+              TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.create_passkey_in)
+              TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.save_password_to)
+              else -> stringResource(R.string.save_sign_in_to)
+            },
+            style = MaterialTheme.typography.titleMedium
+          )
+        },
+        navigationIcon = {
+          IconButton(onClick = onBackButtonSelected) {
+            Icon(
+              Icons.Filled.ArrowBack,
+              stringResource(R.string.accessibility_back_arrow_button))
+          }
+        },
+        colors = TopAppBarDefaults.smallTopAppBarColors
+          (containerColor = Color.Transparent),
+      )
+      Divider(
+         thickness = 8.dp,
+         color = Color.Transparent
+      )
+      Card(
+        shape = MaterialTheme.shapes.medium,
+        modifier = Modifier
+          .padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally)
+      ) {
+        LazyColumn(
+          verticalArrangement = Arrangement.spacedBy(2.dp)
+        ) {
+          enabledProviderList.forEach { enabledProviderInfo ->
+            enabledProviderInfo.createOptions.forEach { createOptionInfo ->
+              item {
+                MoreOptionsInfoRow(
+                  providerInfo = enabledProviderInfo,
+                  createOptionInfo = createOptionInfo,
+                  onOptionSelected = {
+                    onOptionSelected(ActiveEntry(enabledProviderInfo, createOptionInfo))
+                  })
+              }
+            }
+          }
+          if (disabledProviderList != null) {
+            item {
+              MoreOptionsDisabledProvidersRow(
+                disabledProviders = disabledProviderList,
+                onDisabledPasswordManagerSelected = onDisabledPasswordManagerSelected,
+              )
+            }
+          }
+          var hasRemoteInfo = false
+          enabledProviderList.forEach {
+            if (it.remoteEntry != null) {
+              hasRemoteInfo = true
+            }
+          }
+          if (hasRemoteInfo) {
+            item {
+              RemoteEntryRow(
+                onRemoteEntrySelected = onRemoteEntrySelected,
+              )
+            }
+          }
+        }
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 40.dp)
+      )
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsRowIntroCard(
+  providerInfo: EnabledProviderInfo,
+  onDefaultOrNotSelected: () -> Unit,
+) {
+  Card() {
+    Column() {
+      Icon(
+        Icons.Outlined.NewReleases,
+        contentDescription = null,
+        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp)
+      )
+      Text(
+        text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName),
+        style = MaterialTheme.typography.titleMedium,
+        modifier = Modifier.padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally),
+        textAlign = TextAlign.Center,
+      )
+      Text(
+        text = stringResource(R.string.confirm_default_or_use_once_description),
+        style = MaterialTheme.typography.bodyLarge,
+        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+      )
+      Row(
+        horizontalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(
+          stringResource(R.string.use_once),
+          onClick = onDefaultOrNotSelected
+        )
+        ConfirmButton(
+          stringResource(R.string.set_as_default),
+          onClick = onDefaultOrNotSelected
+        )
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 40.dp)
+      )
+    }
+  }
+}
+
+@Composable
+fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) {
+  Entry(
+    onClick = {onProviderSelected(providerInfo.name)},
+    icon = {
+      Image(modifier = Modifier.size(32.dp).padding(start = 10.dp),
+            bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
+            // painter = painterResource(R.drawable.ic_passkey),
+            // TODO: add description.
+            contentDescription = "")
+    },
+    label = {
+      Text(
+        text = providerInfo.displayName,
+        style = MaterialTheme.typography.labelLarge,
+        modifier = Modifier.padding(vertical = 18.dp)
+      )
+    }
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CreationSelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
+  providerInfo: ProviderInfo,
+  createOptionInfo: CreateOptionInfo,
+  onOptionSelected: () -> Unit,
+  onConfirm: () -> Unit,
+  onCancel: () -> Unit,
+  multiProvider: Boolean,
+  onMoreOptionsSelected: () -> Unit,
+) {
+  Card() {
+    Column() {
+      Icon(
+        bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
+        contentDescription = null,
+        tint = Color.Unspecified,
+        modifier = Modifier.align(alignment = Alignment.CenterHorizontally)
+          .padding(all = 24.dp).size(32.dp)
+      )
+      Text(
+        text = when (requestDisplayInfo.type) {
+          TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.choose_create_option_passkey_title,
+            providerInfo.displayName)
+          TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.choose_create_option_password_title,
+            providerInfo.displayName)
+          else -> stringResource(R.string.choose_create_option_sign_in_title,
+            providerInfo.displayName)
+        },
+        style = MaterialTheme.typography.titleMedium,
+        modifier = Modifier.padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally),
+        textAlign = TextAlign.Center,
+      )
+      if (createOptionInfo.userProviderDisplayName != null) {
+        Text(
+          text = stringResource(
+            R.string.choose_create_option_description,
+            requestDisplayInfo.appDomainName,
+            when (requestDisplayInfo.type) {
+              TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.passkey)
+              TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.password)
+              else -> stringResource(R.string.sign_ins)
+            },
+            providerInfo.displayName,
+            createOptionInfo.userProviderDisplayName
+          ),
+          style = MaterialTheme.typography.bodyLarge,
+          modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+        )
+      }
+      Card(
+        shape = MaterialTheme.shapes.medium,
+        modifier = Modifier
+          .padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally),
+      ) {
+        LazyColumn(
+          verticalArrangement = Arrangement.spacedBy(2.dp)
+        ) {
+            item {
+              PrimaryCreateOptionRow(
+                requestDisplayInfo = requestDisplayInfo,
+                createOptionInfo = createOptionInfo,
+                onOptionSelected = onOptionSelected
+              )
+            }
+        }
+      }
+      if (multiProvider) {
+        TextButton(
+          onClick = onMoreOptionsSelected,
+          modifier = Modifier
+          .padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally)){
+          Text(
+              text =
+                when (requestDisplayInfo.type) {
+                  TYPE_PUBLIC_KEY_CREDENTIAL ->
+                    stringResource(R.string.string_create_in_another_place)
+                  else -> stringResource(R.string.string_save_to_another_place)},
+            textAlign = TextAlign.Center,
+          )
+        }
+      }
+      Divider(
+        thickness = 24.dp,
+        color = Color.Transparent
+      )
+      Row(
+        horizontalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(
+          stringResource(R.string.string_cancel),
+          onClick = onCancel
+        )
+        ConfirmButton(
+          stringResource(R.string.string_continue),
+          onClick = onConfirm
+        )
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 16.dp)
+      )
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PrimaryCreateOptionRow(
+  requestDisplayInfo: RequestDisplayInfo,
+  createOptionInfo: CreateOptionInfo,
+  onOptionSelected: () -> Unit
+) {
+  Entry(
+    onClick = onOptionSelected,
+    icon = {
+      Image(modifier = Modifier.size(32.dp).padding(start = 10.dp),
+        bitmap = createOptionInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
+        contentDescription = null)
+    },
+    label = {
+      Column() {
+        // TODO: Add the function to hide/view password when the type is create password
+        if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL ||
+          requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) {
+          Text(
+            text = requestDisplayInfo.title,
+            style = MaterialTheme.typography.titleLarge,
+            modifier = Modifier.padding(top = 16.dp)
+          )
+          Text(
+            text = requestDisplayInfo.subtitle,
+            style = MaterialTheme.typography.bodyMedium,
+            modifier = Modifier.padding(bottom = 16.dp)
+          )
+        } else {
+          Text(
+            text = requestDisplayInfo.subtitle,
+            style = MaterialTheme.typography.titleLarge,
+            modifier = Modifier.padding(top = 16.dp, bottom = 16.dp)
+          )
+        }
+      }
+    }
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsInfoRow(
+  providerInfo: EnabledProviderInfo,
+  createOptionInfo: CreateOptionInfo,
+  onOptionSelected: () -> Unit
+) {
+  Entry(
+        onClick = onOptionSelected,
+        icon = {
+            Image(modifier = Modifier.size(32.dp).padding(start = 16.dp),
+                bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
+                contentDescription = null)
+        },
+        label = {
+          Column() {
+              Text(
+                  text = providerInfo.displayName,
+                  style = MaterialTheme.typography.titleLarge,
+                  modifier = Modifier.padding(top = 16.dp, start = 16.dp)
+              )
+            if (createOptionInfo.userProviderDisplayName != null) {
+              Text(
+                text = createOptionInfo.userProviderDisplayName,
+                style = MaterialTheme.typography.bodyMedium,
+                modifier = Modifier.padding(start = 16.dp)
+              )
+            }
+            if (createOptionInfo.passwordCount != null && createOptionInfo.passkeyCount != null) {
+              Text(
+                text =
+                  stringResource(
+                    R.string.more_options_usage_passwords_passkeys,
+                    createOptionInfo.passwordCount,
+                    createOptionInfo.passkeyCount
+                  ),
+                style = MaterialTheme.typography.bodyMedium,
+                modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
+              )
+            } else if (createOptionInfo.passwordCount != null) {
+              Text(
+                text =
+                stringResource(
+                  R.string.more_options_usage_passwords,
+                  createOptionInfo.passwordCount
+                ),
+                style = MaterialTheme.typography.bodyMedium,
+                modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
+              )
+            } else if (createOptionInfo.passkeyCount != null) {
+              Text(
+                text =
+                stringResource(
+                  R.string.more_options_usage_passkeys,
+                  createOptionInfo.passkeyCount
+                ),
+                style = MaterialTheme.typography.bodyMedium,
+                modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
+              )
+            } else if (createOptionInfo.totalCredentialCount != null) {
+              // TODO: Handle the case when there is total count
+              // but no passwords and passkeys after design is set
+            }
+          }
+        }
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsDisabledProvidersRow(
+  disabledProviders: List<ProviderInfo>,
+  onDisabledPasswordManagerSelected: () -> Unit,
+) {
+  Entry(
+    onClick = onDisabledPasswordManagerSelected,
+    icon = {
+      Icon(
+        Icons.Filled.Add,
+        contentDescription = null,
+        modifier = Modifier.padding(start = 16.dp)
+      )
+    },
+    label = {
+      Column() {
+        Text(
+          text = stringResource(R.string.other_password_manager),
+          style = MaterialTheme.typography.titleLarge,
+          modifier = Modifier.padding(top = 16.dp, start = 16.dp)
+        )
+        Text(
+          text = disabledProviders.joinToString(separator = ", "){ it.displayName },
+          style = MaterialTheme.typography.bodyMedium,
+          modifier = Modifier.padding(bottom = 16.dp, start = 16.dp)
+        )
+      }
+    }
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RemoteEntryRow(
+  onRemoteEntrySelected: () -> Unit,
+) {
+  Entry(
+    onClick = onRemoteEntrySelected,
+    icon = {
+      Icon(
+        painter = painterResource(R.drawable.ic_other_devices),
+        contentDescription = null,
+        tint = Color.Unspecified,
+        modifier = Modifier.padding(start = 18.dp)
+      )
+    },
+    label = {
+      Column() {
+        Text(
+          text = stringResource(R.string.another_device),
+          style = MaterialTheme.typography.titleLarge,
+          modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp)
+            .align(alignment = Alignment.CenterHorizontally)
+        )
+      }
+    }
+  )
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
new file mode 100644
index 0000000..6be019f
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.credentialmanager.createflow
+
+import android.util.Log
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.common.DialogResult
+import com.android.credentialmanager.common.ResultState
+
+data class CreateCredentialUiState(
+  val enabledProviders: List<EnabledProviderInfo>,
+  val disabledProviders: List<DisabledProviderInfo>? = null,
+  val currentScreenState: CreateScreenState,
+  val requestDisplayInfo: RequestDisplayInfo,
+  val activeEntry: ActiveEntry? = null,
+)
+
+class CreateCredentialViewModel(
+  credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance()
+) : ViewModel() {
+
+  var uiState by mutableStateOf(credManRepo.createCredentialInitialUiState())
+    private set
+
+  val dialogResult: MutableLiveData<DialogResult> by lazy {
+    MutableLiveData<DialogResult>()
+  }
+
+  fun observeDialogResult(): LiveData<DialogResult> {
+    return dialogResult
+  }
+
+  fun onConfirmIntro() {
+    if (uiState.enabledProviders.size > 1) {
+      uiState = uiState.copy(
+        currentScreenState = CreateScreenState.PROVIDER_SELECTION
+      )
+    } else if (uiState.enabledProviders.size == 1){
+      uiState = uiState.copy(
+        currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+        activeEntry = ActiveEntry(uiState.enabledProviders.first(),
+          uiState.enabledProviders.first().createOptions.first())
+      )
+    } else {
+      throw java.lang.IllegalStateException("Empty provider list.")
+    }
+  }
+
+  fun onProviderSelected(providerName: String) {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+      activeEntry = ActiveEntry(getProviderInfoByName(providerName),
+        getProviderInfoByName(providerName).createOptions.first())
+    )
+  }
+
+  fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
+    return uiState.enabledProviders.single {
+      it.name.equals(providerName)
+    }
+  }
+
+  fun onMoreOptionsSelected() {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
+    )
+  }
+
+  fun onBackButtonSelected() {
+    uiState = uiState.copy(
+        currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+    )
+  }
+
+  fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO,
+      activeEntry = activeEntry
+    )
+  }
+
+  fun onDisabledPasswordManagerSelected() {
+    // TODO: Complete this function
+  }
+
+  fun onRemoteEntrySelected() {
+    // TODO: Complete this function
+  }
+
+  fun onCancel() {
+    CredentialManagerRepo.getInstance().onCancel()
+    dialogResult.value = DialogResult(ResultState.CANCELED)
+  }
+
+  fun onDefaultOrNotSelected() {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+    )
+    // TODO: implement the if choose as default or not logic later
+  }
+
+  fun onPrimaryCreateOptionInfoSelected() {
+    val entryKey = uiState.activeEntry?.activeEntryInfo?.entryKey
+    val entrySubkey = uiState.activeEntry?.activeEntryInfo?.entrySubkey
+    Log.d(
+      "Account Selector",
+      "Option selected for creation: " +
+              "{key = $entryKey, subkey = $entrySubkey}"
+    )
+    if (entryKey != null && entrySubkey != null) {
+      CredentialManagerRepo.getInstance().onOptionSelected(
+        uiState.activeEntry?.activeProvider!!.name,
+        entryKey,
+        entrySubkey
+      )
+    } else {
+      TODO("Gracefully handle illegal state.")
+    }
+    dialogResult.value = DialogResult(
+      ResultState.COMPLETE,
+    )
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 19820d6..1ab234a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -18,20 +18,63 @@
 
 import android.graphics.drawable.Drawable
 
-data class ProviderInfo(
+open class ProviderInfo(
   val icon: Drawable,
   val name: String,
-  val appDomainName: String,
-  val credentialTypeIcon: Drawable,
-  val createOptions: List<CreateOptionInfo>,
+  val displayName: String,
 )
 
-data class CreateOptionInfo(
-  val icon: Drawable,
+class EnabledProviderInfo(
+  icon: Drawable,
+  name: String,
+  displayName: String,
+  var createOptions: List<CreateOptionInfo>,
+  val isDefault: Boolean,
+  var remoteEntry: RemoteInfo?,
+) : ProviderInfo(icon, name, displayName)
+
+class DisabledProviderInfo(
+  icon: Drawable,
+  name: String,
+  displayName: String,
+) : ProviderInfo(icon, name, displayName)
+
+open class EntryInfo (
+  val entryKey: String,
+  val entrySubkey: String,
+)
+
+class CreateOptionInfo(
+  entryKey: String,
+  entrySubkey: String,
+  val userProviderDisplayName: String?,
+  val credentialTypeIcon: Drawable,
+  val profileIcon: Drawable,
+  val passwordCount: Int?,
+  val passkeyCount: Int?,
+  val totalCredentialCount: Int?,
+  val lastUsedTimeMillis: Long?,
+) : EntryInfo(entryKey, entrySubkey)
+
+class RemoteInfo(
+  entryKey: String,
+  entrySubkey: String,
+) : EntryInfo(entryKey, entrySubkey)
+
+data class RequestDisplayInfo(
   val title: String,
   val subtitle: String,
-  val id: Int,
-  val usageData: String
+  val type: String,
+  val appDomainName: String,
+)
+
+/**
+ * This is initialized to be the most recent used. Can then be changed if
+ * user selects a different entry on the more option page.
+ */
+data class ActiveEntry (
+  val activeProvider: EnabledProviderInfo,
+  val activeEntryInfo: EntryInfo,
 )
 
 /** The name of the current screen. */
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
deleted file mode 100644
index 6489d73..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ /dev/null
@@ -1,524 +0,0 @@
-package com.android.credentialmanager.createflow
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonColors
-import androidx.compose.material.ButtonDefaults
-import androidx.compose.material.Card
-import androidx.compose.material.Chip
-import androidx.compose.material.ChipDefaults
-import androidx.compose.material.Divider
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.ModalBottomSheetLayout
-import androidx.compose.material.ModalBottomSheetValue
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.core.graphics.drawable.toBitmap
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.android.credentialmanager.R
-import com.android.credentialmanager.ui.theme.Grey100
-import com.android.credentialmanager.ui.theme.Shapes
-import com.android.credentialmanager.ui.theme.Typography
-import com.android.credentialmanager.ui.theme.lightBackgroundColor
-import com.android.credentialmanager.ui.theme.lightColorAccentSecondary
-import com.android.credentialmanager.ui.theme.lightSurface1
-
-@ExperimentalMaterialApi
-@Composable
-fun CreatePasskeyScreen(
-  viewModel: CreatePasskeyViewModel = viewModel(),
-  cancelActivity: () -> Unit,
-) {
-  val state = rememberModalBottomSheetState(
-    initialValue = ModalBottomSheetValue.Expanded,
-    skipHalfExpanded = true
-  )
-  ModalBottomSheetLayout(
-    sheetState = state,
-    sheetContent = {
-      val uiState = viewModel.uiState
-      when (uiState.currentScreenState) {
-        CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
-          onConfirm = {viewModel.onConfirmIntro()},
-          onCancel = cancelActivity,
-        )
-        CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
-          providerList = uiState.providers,
-          onCancel = cancelActivity,
-          onProviderSelected = {viewModel.onProviderSelected(it)}
-        )
-        CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
-          providerInfo = uiState.selectedProvider!!,
-          onOptionSelected = {viewModel.onCreateOptionSelected(it)},
-          onCancel = cancelActivity,
-          multiProvider = uiState.providers.size > 1,
-          onMoreOptionsSelected = {viewModel.onMoreOptionsSelected(it)}
-        )
-        CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
-            providerInfo = uiState.selectedProvider!!,
-            providerList = uiState.providers,
-            onBackButtonSelected = {viewModel.onBackButtonSelected(it)},
-            onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)}
-          )
-        CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
-        )
-      }
-    },
-    scrimColor = Color.Transparent,
-    sheetShape = Shapes.medium,
-  ) {}
-  LaunchedEffect(state.currentValue) {
-    if (state.currentValue == ModalBottomSheetValue.Hidden) {
-      cancelActivity()
-    }
-  }
-}
-
-@Composable
-fun ConfirmationCard(
-  onConfirm: () -> Unit,
-  onCancel: () -> Unit,
-) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
-    Column() {
-      Icon(
-        painter = painterResource(R.drawable.ic_passkey),
-        contentDescription = null,
-        tint = Color.Unspecified,
-        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
-      )
-      Text(
-        text = stringResource(R.string.passkey_creation_intro_title),
-        style = Typography.subtitle1,
-        modifier = Modifier
-          .padding(horizontal = 24.dp)
-          .align(alignment = Alignment.CenterHorizontally)
-      )
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
-      Text(
-        text = stringResource(R.string.passkey_creation_intro_body),
-        style = Typography.body1,
-        modifier = Modifier.padding(horizontal = 28.dp)
-      )
-      Divider(
-        thickness = 48.dp,
-        color = Color.Transparent
-      )
-      Row(
-        horizontalArrangement = Arrangement.SpaceBetween,
-        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
-      ) {
-        CancelButton(
-          stringResource(R.string.string_cancel),
-          onclick = onCancel
-        )
-        ConfirmButton(
-          stringResource(R.string.string_continue),
-          onclick = onConfirm
-        )
-      }
-      Divider(
-        thickness = 18.dp,
-        color = Color.Transparent,
-        modifier = Modifier.padding(bottom = 16.dp)
-      )
-    }
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun ProviderSelectionCard(
-  providerList: List<ProviderInfo>,
-  onProviderSelected: (String) -> Unit,
-  onCancel: () -> Unit
-) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
-    Column() {
-      Text(
-        text = stringResource(R.string.choose_provider_title),
-        style = Typography.subtitle1,
-        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
-      )
-      Text(
-        text = stringResource(R.string.choose_provider_body),
-        style = Typography.body1,
-        modifier = Modifier.padding(horizontal = 28.dp)
-      )
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
-      Card(
-        shape = Shapes.medium,
-        modifier = Modifier
-          .padding(horizontal = 24.dp)
-          .align(alignment = Alignment.CenterHorizontally)
-      ) {
-        LazyColumn(
-          verticalArrangement = Arrangement.spacedBy(2.dp)
-        ) {
-          providerList.forEach {
-            item {
-              ProviderRow(providerInfo = it, onProviderSelected = onProviderSelected)
-            }
-          }
-        }
-      }
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
-      Row(
-        horizontalArrangement = Arrangement.Start,
-        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
-      ) {
-        CancelButton(stringResource(R.string.string_cancel), onCancel)
-      }
-      Divider(
-        thickness = 18.dp,
-        color = Color.Transparent,
-        modifier = Modifier.padding(bottom = 16.dp)
-      )
-    }
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun MoreOptionsSelectionCard(
-  providerInfo: ProviderInfo,
-  providerList: List<ProviderInfo>,
-  onBackButtonSelected: (String) -> Unit,
-  onOptionSelected: (String) -> Unit
-) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
-    Column() {
-      TopAppBar(
-        title = {
-          Text(text = stringResource(R.string.string_more_options), style = Typography.subtitle1)
-        },
-        backgroundColor = lightBackgroundColor,
-        elevation = 0.dp,
-        navigationIcon =
-        {
-          IconButton(onClick = { onBackButtonSelected(providerInfo.name) }) {
-            Icon(Icons.Filled.ArrowBack, "backIcon"
-            )
-          }
-        }
-      )
-      Divider(
-         thickness = 24.dp,
-         color = Color.Transparent
-      )
-      Text(
-        text = stringResource(R.string.create_passkey_at),
-        style = Typography.body1,
-        modifier = Modifier.padding(horizontal = 28.dp),
-        textAlign = TextAlign.Center
-      )
-      Card(
-        shape = Shapes.medium,
-        modifier = Modifier
-          .padding(horizontal = 24.dp)
-          .align(alignment = Alignment.CenterHorizontally)
-      ) {
-        LazyColumn(
-          verticalArrangement = Arrangement.spacedBy(2.dp)
-        ) {
-          // TODO: change the order according to usage frequency
-          providerList.forEach { providerInfo ->
-            providerInfo.createOptions.forEach { createOptionInfo ->
-              item {
-                MoreOptionsInfoRow(providerInfo = providerInfo,
-                  createOptionInfo = createOptionInfo,
-                  onOptionSelected = onOptionSelected)
-              }
-            }
-          }
-        }
-      }
-      Divider(
-        thickness = 18.dp,
-        color = Color.Transparent,
-        modifier = Modifier.padding(bottom = 40.dp)
-      )
-    }
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun MoreOptionsRowIntroCard(
-) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun ProviderRow(providerInfo: ProviderInfo, onProviderSelected: (String) -> Unit) {
-  Chip(
-    modifier = Modifier.fillMaxWidth(),
-    onClick = {onProviderSelected(providerInfo.name)},
-    leadingIcon = {
-      Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
-            bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
-            // painter = painterResource(R.drawable.ic_passkey),
-            // TODO: add description.
-            contentDescription = "")
-    },
-    colors = ChipDefaults.chipColors(
-      backgroundColor = Grey100,
-      leadingIconContentColor = Grey100
-    ),
-    shape = Shapes.large
-  ) {
-    Text(
-      text = providerInfo.name,
-      style = Typography.button,
-      modifier = Modifier.padding(vertical = 18.dp)
-    )
-  }
-}
-
-@Composable
-fun CancelButton(text: String, onclick: () -> Unit) {
-  val colors = ButtonDefaults.buttonColors(
-    backgroundColor = lightBackgroundColor
-  )
-  NavigationButton(
-    border = BorderStroke(1.dp, lightSurface1),
-    colors = colors,
-    text = text,
-    onclick = onclick)
-}
-
-@Composable
-fun ConfirmButton(text: String, onclick: () -> Unit) {
-  val colors = ButtonDefaults.buttonColors(
-    backgroundColor = lightColorAccentSecondary
-  )
-  NavigationButton(
-    colors = colors,
-    text = text,
-    onclick = onclick)
-}
-
-@Composable
-fun NavigationButton(
-    border: BorderStroke? = null,
-    colors: ButtonColors,
-    text: String,
-    onclick: () -> Unit
-) {
-  Button(
-    onClick = onclick,
-    shape = Shapes.small,
-    colors = colors,
-    border = border
-  ) {
-    Text(text = text, style = Typography.button)
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun CreationSelectionCard(
-  providerInfo: ProviderInfo,
-  onOptionSelected: (Int) -> Unit,
-  onCancel: () -> Unit,
-  multiProvider: Boolean,
-  onMoreOptionsSelected: (String) -> Unit,
-) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
-    Column() {
-      Icon(
-        bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
-        contentDescription = null,
-        tint = Color.Unspecified,
-        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
-      )
-      Text(
-        text = "${stringResource(R.string.choose_create_option_title)} ${providerInfo.name}",
-        style = Typography.subtitle1,
-        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
-      )
-      Text(
-        text = providerInfo.appDomainName,
-        style = Typography.body2,
-        modifier = Modifier.padding(horizontal = 28.dp)
-      )
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
-      Card(
-        shape = Shapes.medium,
-        modifier = Modifier
-          .padding(horizontal = 24.dp)
-          .align(alignment = Alignment.CenterHorizontally)
-      ) {
-        LazyColumn(
-          verticalArrangement = Arrangement.spacedBy(2.dp)
-        ) {
-          providerInfo.createOptions.forEach {
-            item {
-              CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected)
-            }
-          }
-          if (multiProvider) {
-            item {
-              MoreOptionsRow(onSelect = { onMoreOptionsSelected(providerInfo.name) })
-            }
-          }
-        }
-      }
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
-      Row(
-        horizontalArrangement = Arrangement.Start,
-        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
-      ) {
-        CancelButton(stringResource(R.string.string_cancel), onCancel)
-      }
-      Divider(
-        thickness = 18.dp,
-        color = Color.Transparent,
-        modifier = Modifier.padding(bottom = 16.dp)
-      )
-    }
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (Int) -> Unit) {
-  Chip(
-    modifier = Modifier.fillMaxWidth(),
-    onClick = {onOptionSelected(createOptionInfo.id)},
-    leadingIcon = {
-      Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
-        bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
-            // painter = painterResource(R.drawable.ic_passkey),
-        // TODO: add description.
-            contentDescription = "")
-    },
-    colors = ChipDefaults.chipColors(
-      backgroundColor = Grey100,
-      leadingIconContentColor = Grey100
-    ),
-    shape = Shapes.large
-  ) {
-    Column() {
-      Text(
-        text = createOptionInfo.title,
-        style = Typography.h6,
-        modifier = Modifier.padding(top = 16.dp)
-      )
-      Text(
-        text = createOptionInfo.subtitle,
-        style = Typography.body2,
-        modifier = Modifier.padding(bottom = 16.dp)
-      )
-    }
-  }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun MoreOptionsInfoRow(
-  providerInfo: ProviderInfo,
-  createOptionInfo: CreateOptionInfo,
-  onOptionSelected: (String) -> Unit
-) {
-    Chip(
-        modifier = Modifier.fillMaxWidth(),
-        onClick = { onOptionSelected(providerInfo.name) },
-        leadingIcon = {
-            Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
-                bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
-                // painter = painterResource(R.drawable.ic_passkey),
-                // TODO: add description.
-                contentDescription = "")
-        },
-        colors = ChipDefaults.chipColors(
-            backgroundColor = Grey100,
-            leadingIconContentColor = Grey100
-        ),
-        shape = Shapes.large
-    ) {
-        Column() {
-            Text(
-                text = if (providerInfo.createOptions.size > 1)
-                {providerInfo.name + " for " + createOptionInfo.title} else { providerInfo.name},
-                style = Typography.h6,
-                modifier = Modifier.padding(top = 16.dp)
-            )
-            Text(
-                text = createOptionInfo.usageData,
-                style = Typography.body2,
-                modifier = Modifier.padding(bottom = 16.dp)
-            )
-        }
-    }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun MoreOptionsRow(onSelect: () -> Unit) {
-  Chip(
-    modifier = Modifier.fillMaxWidth().height(52.dp),
-    onClick = onSelect,
-    colors = ChipDefaults.chipColors(
-      backgroundColor = Grey100,
-      leadingIconContentColor = Grey100
-    ),
-    shape = Shapes.large
-  ) {
-      Text(
-        text = stringResource(R.string.string_create_at_another_place),
-        style = Typography.h6,
-      )
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
deleted file mode 100644
index 85fe31d..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ /dev/null
@@ -1,91 +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.credentialmanager.createflow
-
-import android.util.Log
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.lifecycle.ViewModel
-import com.android.credentialmanager.CredentialManagerRepo
-
-data class CreatePasskeyUiState(
-  val providers: List<ProviderInfo>,
-  val currentScreenState: CreateScreenState,
-  val selectedProvider: ProviderInfo? = null,
-)
-
-class CreatePasskeyViewModel(
-  credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance()
-) : ViewModel() {
-
-  var uiState by mutableStateOf(credManRepo.createPasskeyInitialUiState())
-    private set
-
-  fun onConfirmIntro() {
-    if (uiState.providers.size > 1) {
-      uiState = uiState.copy(
-        currentScreenState = CreateScreenState.PROVIDER_SELECTION
-      )
-    } else if (uiState.providers.size == 1){
-      uiState = uiState.copy(
-        currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = uiState.providers.first()
-      )
-    } else {
-      throw java.lang.IllegalStateException("Empty provider list.")
-    }
-  }
-
-  fun onProviderSelected(providerName: String) {
-    uiState = uiState.copy(
-      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-      selectedProvider = getProviderInfoByName(providerName)
-    )
-  }
-
-  fun onCreateOptionSelected(createOptionId: Int) {
-    Log.d("Account Selector", "Option selected for creation: $createOptionId")
-  }
-
-  fun getProviderInfoByName(providerName: String): ProviderInfo {
-    return uiState.providers.single {
-      it.name.equals(providerName)
-    }
-  }
-
-  fun onMoreOptionsSelected(providerName: String) {
-    uiState = uiState.copy(
-        currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
-    )
-  }
-
-  fun onBackButtonSelected(providerName: String) {
-    uiState = uiState.copy(
-        currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
-    )
-  }
-
-  fun onMoreOptionsRowSelected(providerName: String) {
-    uiState = uiState.copy(
-      currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO,
-      selectedProvider = getProviderInfoByName(providerName)
-    )
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 0b18822..19a032f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,47 +16,52 @@
 
 package com.android.credentialmanager.getflow
 
+import android.text.TextUtils
+
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material.Card
-import androidx.compose.material.Chip
-import androidx.compose.material.ChipDefaults
-import androidx.compose.material.Divider
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.ModalBottomSheetLayout
-import androidx.compose.material.ModalBottomSheetValue
-import androidx.compose.material.Text
-import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
-import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.credentialmanager.R
-import com.android.credentialmanager.createflow.CancelButton
-import com.android.credentialmanager.ui.theme.Grey100
-import com.android.credentialmanager.ui.theme.Shapes
-import com.android.credentialmanager.ui.theme.Typography
-import com.android.credentialmanager.ui.theme.lightBackgroundColor
+import com.android.credentialmanager.common.material.ModalBottomSheetLayout
+import com.android.credentialmanager.common.material.ModalBottomSheetValue
+import com.android.credentialmanager.common.material.rememberModalBottomSheetState
+import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.common.ui.Entry
+import com.android.credentialmanager.common.ui.TransparentBackgroundEntry
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
 
-@ExperimentalMaterialApi
 @Composable
 fun GetCredentialScreen(
-  viewModel: GetCredentialViewModel = viewModel(),
-  cancelActivity: () -> Unit,
+  viewModel: GetCredentialViewModel,
 ) {
   val state = rememberModalBottomSheetState(
     initialValue = ModalBottomSheetValue.Expanded,
@@ -67,62 +72,62 @@
     sheetContent = {
       val uiState = viewModel.uiState
       when (uiState.currentScreenState) {
-        GetScreenState.CREDENTIAL_SELECTION -> CredentialSelectionCard(
-          providerInfo = uiState.selectedProvider!!,
-          onCancel = cancelActivity,
-          onOptionSelected = {viewModel.onCredentailSelected(it)},
-          multiProvider = uiState.providers.size > 1,
-          onMoreOptionSelected = {viewModel.onMoreOptionSelected()},
+        GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard(
+          requestDisplayInfo = uiState.requestDisplayInfo,
+          providerDisplayInfo = uiState.providerDisplayInfo,
+          onEntrySelected = viewModel::onEntrySelected,
+          onCancel = viewModel::onCancel,
+          onMoreOptionSelected = viewModel::onMoreOptionSelected,
+        )
+        GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard(
+          providerInfoList = uiState.providerInfoList,
+          providerDisplayInfo = uiState.providerDisplayInfo,
+          onEntrySelected = viewModel::onEntrySelected,
+          onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen,
         )
       }
     },
     scrimColor = Color.Transparent,
-    sheetShape = Shapes.medium,
+    sheetShape = MaterialTheme.shapes.medium,
   ) {}
   LaunchedEffect(state.currentValue) {
     if (state.currentValue == ModalBottomSheetValue.Hidden) {
-      cancelActivity()
+      viewModel.onCancel()
     }
   }
 }
 
-@ExperimentalMaterialApi
+/** Draws the primary credential selection page. */
 @Composable
-fun CredentialSelectionCard(
-  providerInfo: ProviderInfo,
-  onOptionSelected: (Int) -> Unit,
+fun PrimarySelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
+  providerDisplayInfo: ProviderDisplayInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
   onCancel: () -> Unit,
-  multiProvider: Boolean,
   onMoreOptionSelected: () -> Unit,
 ) {
-  Card(
-    backgroundColor = lightBackgroundColor,
-  ) {
+  val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+  val authenticationEntryList = providerDisplayInfo.authenticationEntryList
+  Card() {
     Column() {
-      Icon(
-        bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
-        contentDescription = null,
-        tint = Color.Unspecified,
-        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
-      )
       Text(
-        text = stringResource(R.string.choose_sign_in_title),
-        style = Typography.subtitle1,
-        modifier = Modifier
-          .padding(all = 24.dp)
-          .align(alignment = Alignment.CenterHorizontally)
+        modifier = Modifier.padding(all = 24.dp),
+        textAlign = TextAlign.Center,
+        style = MaterialTheme.typography.headlineSmall,
+        text = stringResource(
+          if (sortedUserNameToCredentialEntryList.size == 1) {
+            if (sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList
+                .first().credentialType == PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+            )
+              R.string.get_dialog_title_use_passkey_for
+            else R.string.get_dialog_title_use_sign_in_for
+          } else R.string.get_dialog_title_choose_sign_in_for,
+          requestDisplayInfo.appDomainName
+        ),
       )
-      Text(
-        text = providerInfo.appDomainName,
-        style = Typography.body2,
-        modifier = Modifier.padding(horizontal = 28.dp)
-      )
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
-      )
+
       Card(
-        shape = Shapes.medium,
+        shape = MaterialTheme.shapes.medium,
         modifier = Modifier
           .padding(horizontal = 24.dp)
           .align(alignment = Alignment.CenterHorizontally)
@@ -130,15 +135,20 @@
         LazyColumn(
           verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-          providerInfo.credentialOptions.forEach {
-            item {
-              CredentialOptionRow(credentialOptionInfo = it, onOptionSelected = onOptionSelected)
-            }
+          items(sortedUserNameToCredentialEntryList) {
+            CredentialEntryRow(
+              credentialEntryInfo = it.sortedCredentialEntryList.first(),
+              onEntrySelected = onEntrySelected,
+            )
           }
-          if (multiProvider) {
-            item {
-              MoreOptionRow(onSelect = onMoreOptionSelected)
-            }
+          items(authenticationEntryList) {
+            AuthenticationEntryRow(
+              authenticationEntryInfo = it,
+              onEntrySelected = onEntrySelected,
+            )
+          }
+          item {
+            SignInAnotherWayRow(onSelect = onMoreOptionSelected)
           }
         }
       }
@@ -161,57 +171,319 @@
   }
 }
 
-@ExperimentalMaterialApi
+/** Draws the secondary credential selection page, where all sign-in options are listed. */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
-fun CredentialOptionRow(
-    credentialOptionInfo: CredentialOptionInfo,
-    onOptionSelected: (Int) -> Unit
+fun AllSignInOptionCard(
+  providerInfoList: List<ProviderInfo>,
+  providerDisplayInfo: ProviderDisplayInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
+  onBackButtonClicked: () -> Unit,
 ) {
-  Chip(
-    modifier = Modifier.fillMaxWidth(),
-    onClick = {onOptionSelected(credentialOptionInfo.id)},
-    leadingIcon = {
-      Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
-            bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(),
-        // TODO: add description.
-            contentDescription = "")
-    },
-    colors = ChipDefaults.chipColors(
-      backgroundColor = Grey100,
-      leadingIconContentColor = Grey100
-    ),
-    shape = Shapes.large
-  ) {
+  val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList
+  val authenticationEntryList = providerDisplayInfo.authenticationEntryList
+  Card() {
     Column() {
-      Text(
-        text = credentialOptionInfo.title,
-        style = Typography.h6,
-        modifier = Modifier.padding(top = 16.dp)
+      TopAppBar(
+        colors = TopAppBarDefaults.smallTopAppBarColors(
+          containerColor = Color.Transparent,
+        ),
+        title = {
+          Text(
+            text = stringResource(R.string.get_dialog_title_sign_in_options),
+            style = MaterialTheme.typography.titleMedium
+          )
+        },
+        navigationIcon = {
+          IconButton(onClick = onBackButtonClicked) {
+            Icon(
+              Icons.Filled.ArrowBack,
+              contentDescription = stringResource(R.string.accessibility_back_arrow_button))
+          }
+        },
+        modifier = Modifier.padding(top = 12.dp)
       )
-      Text(
-        text = credentialOptionInfo.subtitle,
-        style = Typography.body2,
-        modifier = Modifier.padding(bottom = 16.dp)
+
+      Card(
+        shape = MaterialTheme.shapes.large,
+        modifier = Modifier
+          .padding(start = 24.dp, end = 24.dp, bottom = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally)
+      ) {
+        LazyColumn(
+          verticalArrangement = Arrangement.spacedBy(8.dp)
+        ) {
+          // For username
+          items(sortedUserNameToCredentialEntryList) { item ->
+            PerUserNameCredentials(
+              perUserNameCredentialEntryList = item,
+              onEntrySelected = onEntrySelected,
+            )
+          }
+          // Locked password manager
+          if (!authenticationEntryList.isEmpty()) {
+            item {
+              LockedCredentials(
+                authenticationEntryList = authenticationEntryList,
+                onEntrySelected = onEntrySelected,
+              )
+            }
+          }
+          // From another device
+          val remoteEntry = providerDisplayInfo.remoteEntry
+          if (remoteEntry != null) {
+            item {
+              RemoteEntryCard(
+                remoteEntry = remoteEntry,
+                onEntrySelected = onEntrySelected,
+              )
+            }
+          }
+          // Manage sign-ins (action chips)
+          item {
+            ActionChips(providerInfoList = providerInfoList, onEntrySelected = onEntrySelected)
+          }
+        }
+      }
+    }
+  }
+}
+
+// TODO: create separate rows for primary and secondary pages.
+// TODO: reuse rows and columns across types.
+
+@Composable
+fun ActionChips(
+  providerInfoList: List<ProviderInfo>,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  val actionChips = providerInfoList.flatMap { it.actionEntryList }
+  if (actionChips.isEmpty()) {
+    return
+  }
+
+  Text(
+    text = stringResource(R.string.get_dialog_heading_manage_sign_ins),
+    style = MaterialTheme.typography.labelLarge,
+    modifier = Modifier.padding(vertical = 8.dp)
+  )
+  // TODO: tweak padding.
+  Card(
+    modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+      actionChips.forEach {
+        ActionEntryRow(it, onEntrySelected)
+      }
+    }
+  }
+}
+
+@Composable
+fun RemoteEntryCard(
+  remoteEntry: RemoteEntryInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  Text(
+    text = stringResource(R.string.get_dialog_heading_from_another_device),
+    style = MaterialTheme.typography.labelLarge,
+    modifier = Modifier.padding(vertical = 8.dp)
+  )
+  Card(
+    modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(
+      modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+      verticalArrangement = Arrangement.spacedBy(2.dp),
+    ) {
+      Entry(
+        onClick = {onEntrySelected(remoteEntry)},
+        icon = {
+          Icon(
+            painter = painterResource(R.drawable.ic_other_devices),
+            contentDescription = null,
+            tint = Color.Unspecified,
+            modifier = Modifier.padding(start = 18.dp)
+          )
+        },
+        label = {
+          Text(
+            text = stringResource(R.string.get_dialog_option_headline_use_a_different_device),
+            style = MaterialTheme.typography.titleLarge,
+            modifier = Modifier.padding(start = 16.dp, top = 18.dp, bottom = 18.dp)
+              .align(alignment = Alignment.CenterHorizontally)
+          )
+        }
       )
     }
   }
 }
 
-@ExperimentalMaterialApi
 @Composable
-fun MoreOptionRow(onSelect: () -> Unit) {
-  Chip(
-    modifier = Modifier.fillMaxWidth().height(52.dp),
-    onClick = onSelect,
-    colors = ChipDefaults.chipColors(
-      backgroundColor = Grey100,
-      leadingIconContentColor = Grey100
-    ),
-    shape = Shapes.large
+fun LockedCredentials(
+  authenticationEntryList: List<AuthenticationEntryInfo>,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  Text(
+    text = stringResource(R.string.get_dialog_heading_locked_password_managers),
+    style = MaterialTheme.typography.labelLarge,
+    modifier = Modifier.padding(vertical = 8.dp)
+  )
+  Card(
+    modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+    shape = MaterialTheme.shapes.medium,
   ) {
-    Text(
-      text = stringResource(R.string.string_more_options),
-      style = Typography.h6,
-    )
+    Column(
+      modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+      verticalArrangement = Arrangement.spacedBy(2.dp),
+    ) {
+      authenticationEntryList.forEach {
+        AuthenticationEntryRow(it, onEntrySelected)
+      }
+    }
   }
 }
+
+@Composable
+fun PerUserNameCredentials(
+  perUserNameCredentialEntryList: PerUserNameCredentialEntryList,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  Text(
+    text = stringResource(
+      R.string.get_dialog_heading_for_username, perUserNameCredentialEntryList.userName),
+    style = MaterialTheme.typography.labelLarge,
+    modifier = Modifier.padding(vertical = 8.dp)
+  )
+  Card(
+    modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+    shape = MaterialTheme.shapes.medium,
+  ) {
+    Column(
+      modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+      verticalArrangement = Arrangement.spacedBy(2.dp),
+    ) {
+      perUserNameCredentialEntryList.sortedCredentialEntryList.forEach {
+        CredentialEntryRow(it, onEntrySelected)
+      }
+    }
+  }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CredentialEntryRow(
+  credentialEntryInfo: CredentialEntryInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  Entry(
+    onClick = {onEntrySelected(credentialEntryInfo)},
+    icon = {
+      Image(modifier = Modifier.padding(start = 10.dp).size(32.dp),
+        bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
+        // TODO: add description.
+        contentDescription = "")
+    },
+    label = {
+      Column() {
+        // TODO: fix the text values.
+        Text(
+          text = credentialEntryInfo.userName,
+          style = MaterialTheme.typography.titleLarge,
+          modifier = Modifier.padding(top = 16.dp)
+        )
+        Text(
+          text =
+          if (TextUtils.isEmpty(credentialEntryInfo.displayName))
+            credentialEntryInfo.credentialTypeDisplayName
+          else
+            credentialEntryInfo.credentialTypeDisplayName +
+                    stringResource(R.string.get_dialog_sign_in_type_username_separator) +
+                    credentialEntryInfo.displayName,
+          style = MaterialTheme.typography.bodyMedium,
+          modifier = Modifier.padding(bottom = 16.dp)
+        )
+      }
+    }
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AuthenticationEntryRow(
+  authenticationEntryInfo: AuthenticationEntryInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  Entry(
+    onClick = {onEntrySelected(authenticationEntryInfo)},
+    icon = {
+      Image(modifier = Modifier.padding(start = 10.dp).size(32.dp),
+        bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
+        // TODO: add description.
+        contentDescription = "")
+    },
+    label = {
+      Column() {
+        // TODO: fix the text values.
+        Text(
+          text = authenticationEntryInfo.title,
+          style = MaterialTheme.typography.titleLarge,
+          modifier = Modifier.padding(top = 16.dp)
+        )
+        Text(
+          text = stringResource(R.string.locked_credential_entry_label_subtext),
+          style = MaterialTheme.typography.bodyMedium,
+          modifier = Modifier.padding(bottom = 16.dp)
+        )
+      }
+    }
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ActionEntryRow(
+  actionEntryInfo: ActionEntryInfo,
+  onEntrySelected: (EntryInfo) -> Unit,
+) {
+  TransparentBackgroundEntry(
+    icon = {
+      Image(modifier = Modifier.padding(start = 10.dp).size(32.dp),
+        bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
+        // TODO: add description.
+        contentDescription = "")
+    },
+    label = {
+      Column() {
+        Text(
+          text = actionEntryInfo.title,
+          style = MaterialTheme.typography.titleLarge,
+        )
+        if (actionEntryInfo.subTitle != null) {
+          Text(
+            text = actionEntryInfo.subTitle,
+            style = MaterialTheme.typography.bodyMedium,
+          )
+        }
+      }
+    },
+    onClick = { onEntrySelected(actionEntryInfo) },
+  )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SignInAnotherWayRow(onSelect: () -> Unit) {
+  Entry(
+    onClick = onSelect,
+    label = {
+      Text(
+        text = stringResource(R.string.get_dialog_use_saved_passkey_for),
+        style = MaterialTheme.typography.titleLarge,
+        modifier = Modifier.padding(vertical = 16.dp)
+      )
+    }
+  )
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 0fdd8ec..22370a9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -20,13 +20,20 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.common.DialogResult
+import com.android.credentialmanager.common.ResultState
+import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
+import com.android.internal.util.Preconditions
 
 data class GetCredentialUiState(
-  val providers: List<ProviderInfo>,
+  val providerInfoList: List<ProviderInfo>,
   val currentScreenState: GetScreenState,
-  val selectedProvider: ProviderInfo? = null,
+  val requestDisplayInfo: RequestDisplayInfo,
+  val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
 )
 
 class GetCredentialViewModel(
@@ -36,11 +43,120 @@
   var uiState by mutableStateOf(credManRepo.getCredentialInitialUiState())
       private set
 
-  fun onCredentailSelected(credentialId: Int) {
-    Log.d("Account Selector", "credential selected: $credentialId")
+  val dialogResult: MutableLiveData<DialogResult> by lazy {
+    MutableLiveData<DialogResult>()
+  }
+
+  fun observeDialogResult(): LiveData<DialogResult> {
+    return dialogResult
+  }
+
+  fun onEntrySelected(entry: EntryInfo) {
+    Log.d("Account Selector", "credential selected:" +
+            " {provider=${entry.providerId}, key=${entry.entryKey}, subkey=${entry.entrySubkey}}")
+    CredentialManagerRepo.getInstance().onOptionSelected(
+      entry.providerId,
+      entry.entryKey,
+      entry.entrySubkey
+    )
+    dialogResult.value = DialogResult(ResultState.COMPLETE)
   }
 
   fun onMoreOptionSelected() {
     Log.d("Account Selector", "More Option selected")
+    uiState = uiState.copy(
+      currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS
+    )
+  }
+
+  fun onBackToPrimarySelectionScreen() {
+    uiState = uiState.copy(
+      currentScreenState = GetScreenState.PRIMARY_SELECTION
+    )
+  }
+
+  fun onCancel() {
+    CredentialManagerRepo.getInstance().onCancel()
+    dialogResult.value = DialogResult(ResultState.CANCELED)
   }
 }
+
+private fun toProviderDisplayInfo(
+  providerInfoList: List<ProviderInfo>
+): ProviderDisplayInfo {
+
+  val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
+  val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
+  val remoteEntryList = mutableListOf<RemoteEntryInfo>()
+  providerInfoList.forEach { providerInfo ->
+    if (providerInfo.authenticationEntry != null) {
+      authenticationEntryList.add(providerInfo.authenticationEntry)
+    }
+    if (providerInfo.remoteEntry != null) {
+      remoteEntryList.add(providerInfo.remoteEntry)
+    }
+
+    providerInfo.credentialEntryList.forEach {
+      userNameToCredentialEntryMap.compute(
+        it.userName
+      ) {
+          _, v ->
+        if (v == null) {
+          mutableListOf(it)
+        } else {
+          v.add(it)
+          v
+        }
+      }
+    }
+  }
+  // There can only be at most one remote entry
+  // TODO: fail elegantly
+  Preconditions.checkState(remoteEntryList.size <= 1)
+
+  // Compose sortedUserNameToCredentialEntryList
+  val comparator = CredentialEntryInfoComparator()
+  // Sort per username
+  userNameToCredentialEntryMap.values.forEach {
+    it.sortWith(comparator)
+  }
+  // Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
+  val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
+    PerUserNameCredentialEntryList(it.key, it.value)
+  }.sortedWith(
+    compareBy(comparator) { it.sortedCredentialEntryList.first() }
+  )
+
+  return ProviderDisplayInfo(
+    sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
+    authenticationEntryList = authenticationEntryList,
+    remoteEntry = remoteEntryList.getOrNull(0),
+  )
+}
+
+internal class CredentialEntryInfoComparator : Comparator<CredentialEntryInfo> {
+  override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
+    // First order by last used timestamp
+    if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
+      if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
+        return 1
+      } else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
+        return -1
+      }
+    } else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
+      return -1
+    } else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
+      return 1
+    }
+
+    // Then prefer passkey type for its security benefits
+    if (p0.credentialType != p1.credentialType) {
+      if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
+        return -1
+      } else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
+        return 1
+      }
+    }
+    return 0
+  }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index a39b211..76d9847 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -19,22 +19,93 @@
 import android.graphics.drawable.Drawable
 
 data class ProviderInfo(
+  /**
+   * Unique id (component name) of this provider.
+   * Not for display purpose - [displayName] should be used for ui rendering.
+   */
+  val id: String,
   val icon: Drawable,
-  val name: String,
-  val appDomainName: String,
-  val credentialTypeIcon: Drawable,
-  val credentialOptions: List<CredentialOptionInfo>,
+  val displayName: String,
+  val credentialEntryList: List<CredentialEntryInfo>,
+  val authenticationEntry: AuthenticationEntryInfo?,
+  val remoteEntry: RemoteEntryInfo?,
+  val actionEntryList: List<ActionEntryInfo>,
 )
 
-data class CredentialOptionInfo(
+/** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
+ *  by the provider id but instead focuses on structures convenient for display purposes. */
+data class ProviderDisplayInfo(
+  /**
+   * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
+   * Note that the list order matters to the display order.
+   */
+  val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
+  val authenticationEntryList: List<AuthenticationEntryInfo>,
+  val remoteEntry: RemoteEntryInfo?
+)
+
+abstract class EntryInfo (
+  /** Unique id combination of this entry. Not for display purpose. */
+  val providerId: String,
+  val entryKey: String,
+  val entrySubkey: String,
+)
+
+class CredentialEntryInfo(
+  providerId: String,
+  entryKey: String,
+  entrySubkey: String,
+  /** Type of this credential used for sorting. Not localized so must not be directly displayed. */
+  val credentialType: String,
+  /** Localized type value of this credential used for display purpose. */
+  val credentialTypeDisplayName: String,
+  val userName: String,
+  val displayName: String?,
   val icon: Drawable,
+  val lastUsedTimeMillis: Long?,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+class AuthenticationEntryInfo(
+  providerId: String,
+  entryKey: String,
+  entrySubkey: String,
   val title: String,
-  val subtitle: String,
-  val id: Int,
-  val usageData: String
+  val icon: Drawable,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+class RemoteEntryInfo(
+  providerId: String,
+  entryKey: String,
+  entrySubkey: String,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+class ActionEntryInfo(
+  providerId: String,
+  entryKey: String,
+  entrySubkey: String,
+  val title: String,
+  val icon: Drawable,
+  val subTitle: String?,
+) : EntryInfo(providerId, entryKey, entrySubkey)
+
+data class RequestDisplayInfo(
+  val appDomainName: String,
+)
+
+/**
+ * @property userName the userName that groups all the entries in this list
+ * @property sortedCredentialEntryList the credential entries associated with the [userName] sorted
+ *                                     by last used timestamps and then by credential types
+ */
+data class PerUserNameCredentialEntryList(
+  val userName: String,
+  val sortedCredentialEntryList: List<CredentialEntryInfo>,
 )
 
 /** The name of the current screen. */
 enum class GetScreenState {
-  CREDENTIAL_SELECTION,
+  /** The primary credential selection page. */
+  PRIMARY_SELECTION,
+  /** The secondary credential selection page, where all sign-in options are listed. */
+  ALL_SIGN_IN_OPTIONS,
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
new file mode 100644
index 0000000..7e7dbde
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.credentials.Credential
+import android.os.Bundle
+
+/**
+ * Base request class for registering a credential.
+ *
+ * @property type the credential type
+ * @property data the request data in the [Bundle] format
+ * @property requireSystemProvider true if must only be fulfilled by a system provider and false
+ *                              otherwise
+ */
+open class CreateCredentialRequest(
+        val type: String,
+        val data: Bundle,
+        val requireSystemProvider: Boolean,
+) {
+    companion object {
+        @JvmStatic
+        fun createFrom(from: android.credentials.CreateCredentialRequest): CreateCredentialRequest {
+            return try {
+                when (from.type) {
+                    Credential.TYPE_PASSWORD_CREDENTIAL ->
+                        CreatePasswordRequest.createFrom(from.data)
+                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        CreatePublicKeyCredentialBaseRequest.createFrom(from.data)
+                    else ->
+                        CreateCredentialRequest(from.type, from.data, from.requireSystemProvider())
+                }
+            } catch (e: FrameworkClassParsingException) {
+                CreateCredentialRequest(from.type, from.data, from.requireSystemProvider())
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
new file mode 100644
index 0000000..f0da9f9
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack.developer
+
+import android.credentials.Credential
+import android.os.Bundle
+
+/**
+ * A request to save the user password credential with their password provider.
+ *
+ * @property id the user id associated with the password
+ * @property password the password
+ * @throws NullPointerException If [id] is null
+ * @throws NullPointerException If [password] is null
+ * @throws IllegalArgumentException If [password] is empty
+ */
+class CreatePasswordRequest constructor(
+        val id: String,
+        val password: String,
+) : CreateCredentialRequest(
+        Credential.TYPE_PASSWORD_CREDENTIAL,
+        toBundle(id, password),
+        false,
+) {
+
+    init {
+        require(password.isNotEmpty()) { "password should not be empty" }
+    }
+
+    companion object {
+        const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
+        const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
+
+        @JvmStatic
+        internal fun toBundle(id: String, password: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_ID, id)
+            bundle.putString(BUNDLE_KEY_PASSWORD, password)
+            return bundle
+        }
+
+        @JvmStatic
+        fun createFrom(data: Bundle): CreatePasswordRequest {
+            try {
+                val id = data.getString(BUNDLE_KEY_ID)
+                val password = data.getString(BUNDLE_KEY_PASSWORD)
+                return CreatePasswordRequest(id!!, password!!)
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt
new file mode 100644
index 0000000..26d61f9
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialBaseRequest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * Base request class for registering a public key credential.
+ *
+ * @property requestJson The request in JSON format
+ * @throws NullPointerException If [requestJson] is null. This is handled by the Kotlin runtime
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * @hide
+ */
+abstract class CreatePublicKeyCredentialBaseRequest constructor(
+        val requestJson: String,
+        type: String,
+        data: Bundle,
+        requireSystemProvider: Boolean,
+) : CreateCredentialRequest(type, data, requireSystemProvider) {
+
+    init {
+        require(requestJson.isNotEmpty()) { "request json must not be empty" }
+    }
+
+    companion object {
+        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
+
+        @JvmStatic
+        fun createFrom(data: Bundle): CreatePublicKeyCredentialBaseRequest {
+            return when (data.getString(BUNDLE_KEY_SUBTYPE)) {
+                CreatePublicKeyCredentialRequest
+                        .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
+                    CreatePublicKeyCredentialRequestPrivileged.createFrom(data)
+                CreatePublicKeyCredentialRequestPrivileged
+                        .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED ->
+                    CreatePublicKeyCredentialRequestPrivileged.createFrom(data)
+                else -> throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
new file mode 100644
index 0000000..2eda90b
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * A request to register a passkey from the user's public key credential provider.
+ *
+ * @property requestJson the request in JSON format
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default
+ * @throws NullPointerException If [requestJson] or [allowHybrid] is null. This is handled by the
+ * Kotlin runtime
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
+        requestJson: String,
+        @get:JvmName("allowHybrid")
+        val allowHybrid: Boolean = true
+) : CreatePublicKeyCredentialBaseRequest(
+        requestJson,
+        PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+        toBundle(requestJson, allowHybrid),
+        false,
+) {
+    companion object {
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
+                "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
+
+        @JvmStatic
+        internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_SUBTYPE,
+                    BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+
+        @JvmStatic
+        fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
+            try {
+                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                return CreatePublicKeyCredentialRequest(requestJson!!, (allowHybrid!!) as Boolean)
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
new file mode 100644
index 0000000..36324f8
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * A privileged request to register a passkey from the user’s public key credential provider, where
+ * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
+ * brower, caBLE, can use this.
+ *
+ * @property requestJson the privileged request in JSON format
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default
+ * @property rp the expected true RP ID which will override the one in the [requestJson]
+ * @property clientDataHash a hash that is used to verify the [rp] Identity
+ * @throws NullPointerException If any of [allowHybrid], [requestJson], [rp], or [clientDataHash] is
+ * null. This is handled by the Kotlin runtime
+ * @throws IllegalArgumentException If any of [requestJson], [rp], or [clientDataHash] is empty
+ *
+ * @hide
+ */
+class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
+        requestJson: String,
+        val rp: String,
+        val clientDataHash: String,
+        @get:JvmName("allowHybrid")
+        val allowHybrid: Boolean = true
+) : CreatePublicKeyCredentialBaseRequest(
+        requestJson,
+        PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+        toBundle(requestJson, rp, clientDataHash, allowHybrid),
+        false,
+) {
+
+    init {
+        require(rp.isNotEmpty()) { "rp must not be empty" }
+        require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
+    }
+
+    /** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */
+    class Builder(var requestJson: String, var rp: String, var clientDataHash: String) {
+
+        private var allowHybrid: Boolean = true
+
+        /**
+         * Sets the privileged request in JSON format.
+         */
+        fun setRequestJson(requestJson: String): Builder {
+            this.requestJson = requestJson
+            return this
+        }
+
+        /**
+         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         */
+        fun setAllowHybrid(allowHybrid: Boolean): Builder {
+            this.allowHybrid = allowHybrid
+            return this
+        }
+
+        /**
+         * Sets the expected true RP ID which will override the one in the [requestJson].
+         */
+        fun setRp(rp: String): Builder {
+            this.rp = rp
+            return this
+        }
+
+        /**
+         * Sets a hash that is used to verify the [rp] Identity.
+         */
+        fun setClientDataHash(clientDataHash: String): Builder {
+            this.clientDataHash = clientDataHash
+            return this
+        }
+
+        /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
+        fun build(): CreatePublicKeyCredentialRequestPrivileged {
+            return CreatePublicKeyCredentialRequestPrivileged(this.requestJson,
+                    this.rp, this.clientDataHash, this.allowHybrid)
+        }
+    }
+
+    companion object {
+        const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP"
+        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+                "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED =
+                "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
+                        "PRIVILEGED"
+
+        @JvmStatic
+        internal fun toBundle(
+                requestJson: String,
+                rp: String,
+                clientDataHash: String,
+                allowHybrid: Boolean
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_SUBTYPE,
+                    BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIVILEGED)
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putString(BUNDLE_KEY_RP, rp)
+            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+
+        @JvmStatic
+        fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
+            try {
+                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+                val rp = data.getString(BUNDLE_KEY_RP)
+                val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
+                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                return CreatePublicKeyCredentialRequestPrivileged(
+                        requestJson!!,
+                        rp!!,
+                        clientDataHash!!,
+                        (allowHybrid!!) as Boolean,
+                )
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
new file mode 100644
index 0000000..ee08e9e
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * Base class for a credential with which the user consented to authenticate to the app.
+ *
+ * @property type the credential type
+ * @property data the credential data in the [Bundle] format.
+ */
+open class Credential(val type: String, val data: Bundle)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
new file mode 100644
index 0000000..497c272
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+/**
+ * Internal exception used to indicate a parsing error while converting from a framework type to
+ * a jetpack type.
+ *
+ * @hide
+ */
+internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
new file mode 100644
index 0000000..eb65241
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.credentials.Credential
+import android.os.Bundle
+
+/**
+ * Base request class for getting a registered credential.
+ *
+ * @property type the credential type
+ * @property data the request data in the [Bundle] format
+ * @property requireSystemProvider true if must only be fulfilled by a system provider and false
+ *                              otherwise
+ */
+open class GetCredentialOption(
+        val type: String,
+        val data: Bundle,
+        val requireSystemProvider: Boolean,
+) {
+    companion object {
+        @JvmStatic
+        fun createFrom(from: android.credentials.GetCredentialOption): GetCredentialOption {
+            return try {
+                when (from.type) {
+                    Credential.TYPE_PASSWORD_CREDENTIAL ->
+                        GetPasswordOption.createFrom(from.data)
+                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        GetPublicKeyCredentialBaseOption.createFrom(from.data)
+                    else ->
+                        GetCredentialOption(from.type, from.data, from.requireSystemProvider())
+                }
+            } catch (e: FrameworkClassParsingException) {
+                GetCredentialOption(from.type, from.data, from.requireSystemProvider())
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
new file mode 100644
index 0000000..7f9256e
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+/**
+ * Encapsulates a request to get a user credential.
+ *
+ * @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose
+ * one to authenticate to the app
+ * @throws IllegalArgumentException If [getCredentialOptions] is empty
+ */
+class GetCredentialRequest constructor(
+        val getCredentialOptions: List<GetCredentialOption>,
+) {
+
+    init {
+        require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" }
+    }
+
+    /** A builder for [GetCredentialRequest]. */
+    class Builder {
+        private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf()
+
+        /** Adds a specific type of [GetCredentialOption]. */
+        fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder {
+            getCredentialOptions.add(getCredentialOption)
+            return this
+        }
+
+        /** Sets the list of [GetCredentialOption]. */
+        fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder {
+            this.getCredentialOptions = getCredentialOptions.toMutableList()
+            return this
+        }
+
+        /**
+         * Builds a [GetCredentialRequest].
+         *
+         * @throws IllegalArgumentException If [getCredentialOptions] is empty
+         */
+        fun build(): GetCredentialRequest {
+            return GetCredentialRequest(getCredentialOptions.toList())
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        fun createFrom(from: android.credentials.GetCredentialRequest): GetCredentialRequest {
+            return GetCredentialRequest(
+                    from.getCredentialOptions.map {GetCredentialOption.createFrom(it)}
+            )
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
new file mode 100644
index 0000000..2facad1
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.credentials.Credential
+import android.os.Bundle
+
+/** A request to retrieve the user's saved application password from their password provider. */
+class GetPasswordOption : GetCredentialOption(
+        Credential.TYPE_PASSWORD_CREDENTIAL,
+        Bundle(),
+        false,
+) {
+    companion object {
+        @JvmStatic
+        fun createFrom(data: Bundle): GetPasswordOption {
+            return GetPasswordOption()
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt
new file mode 100644
index 0000000..9b51b30
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialBaseOption.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * Base request class for getting a registered public key credential.
+ *
+ * @property requestJson the request in JSON format
+ * @throws NullPointerException If [requestJson] is null - auto handled by the
+ * Kotlin runtime
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * @hide
+ */
+abstract class GetPublicKeyCredentialBaseOption constructor(
+        val requestJson: String,
+        type: String,
+        data: Bundle,
+        requireSystemProvider: Boolean,
+) : GetCredentialOption(type, data, requireSystemProvider) {
+
+    init {
+        require(requestJson.isNotEmpty()) { "request json must not be empty" }
+    }
+
+    companion object {
+        const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
+        const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
+
+        @JvmStatic
+        fun createFrom(data: Bundle): GetPublicKeyCredentialBaseOption {
+            return when (data.getString(BUNDLE_KEY_SUBTYPE)) {
+                GetPublicKeyCredentialOption
+                        .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
+                    GetPublicKeyCredentialOption.createFrom(data)
+                GetPublicKeyCredentialOptionPrivileged
+                        .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
+                    GetPublicKeyCredentialOptionPrivileged.createFrom(data)
+                else -> throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
new file mode 100644
index 0000000..6f13c17
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * A request to get passkeys from the user's public key credential provider.
+ *
+ * @property requestJson the request in JSON format
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default
+ * @throws NullPointerException If [requestJson] or [allowHybrid] is null. It is handled by the
+ * Kotlin runtime
+ * @throws IllegalArgumentException If [requestJson] is empty
+ *
+ * @hide
+ */
+class GetPublicKeyCredentialOption @JvmOverloads constructor(
+        requestJson: String,
+        @get:JvmName("allowHybrid")
+        val allowHybrid: Boolean = true,
+) : GetPublicKeyCredentialBaseOption(
+        requestJson,
+        PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+        toBundle(requestJson, allowHybrid),
+        false
+) {
+    companion object {
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
+                "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
+
+        @JvmStatic
+        internal fun toBundle(requestJson: String, allowHybrid: Boolean): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+
+        @JvmStatic
+        fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
+            try {
+                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                return GetPublicKeyCredentialOption(requestJson!!, (allowHybrid!!) as Boolean)
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
new file mode 100644
index 0000000..79c62a1
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * A privileged request to get passkeys from the user's public key credential provider. The caller
+ * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
+ * can use this.
+ *
+ * @property requestJson the privileged request in JSON format
+ * @property allowHybrid defines whether hybrid credentials are allowed to fulfill this request,
+ * true by default
+ * @property rp the expected true RP ID which will override the one in the [requestJson]
+ * @property clientDataHash a hash that is used to verify the [rp] Identity
+ * @throws NullPointerException If any of [allowHybrid], [requestJson], [rp], or [clientDataHash]
+ * is null. This is handled by the Kotlin runtime
+ * @throws IllegalArgumentException If any of [requestJson], [rp], or [clientDataHash] is empty
+ *
+ * @hide
+ */
+class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
+        requestJson: String,
+        val rp: String,
+        val clientDataHash: String,
+        @get:JvmName("allowHybrid")
+        val allowHybrid: Boolean = true
+) : GetPublicKeyCredentialBaseOption(
+        requestJson,
+        PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
+        toBundle(requestJson, rp, clientDataHash, allowHybrid),
+        false,
+) {
+
+    init {
+        require(rp.isNotEmpty()) { "rp must not be empty" }
+        require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
+    }
+
+    /** A builder for [GetPublicKeyCredentialOptionPrivileged]. */
+    class Builder(var requestJson: String, var rp: String, var clientDataHash: String) {
+
+        private var allowHybrid: Boolean = true
+
+        /**
+         * Sets the privileged request in JSON format.
+         */
+        fun setRequestJson(requestJson: String): Builder {
+            this.requestJson = requestJson
+            return this
+        }
+
+        /**
+         * Sets whether hybrid credentials are allowed to fulfill this request, true by default.
+         */
+        fun setAllowHybrid(allowHybrid: Boolean): Builder {
+            this.allowHybrid = allowHybrid
+            return this
+        }
+
+        /**
+         * Sets the expected true RP ID which will override the one in the [requestJson].
+         */
+        fun setRp(rp: String): Builder {
+            this.rp = rp
+            return this
+        }
+
+        /**
+         * Sets a hash that is used to verify the [rp] Identity.
+         */
+        fun setClientDataHash(clientDataHash: String): Builder {
+            this.clientDataHash = clientDataHash
+            return this
+        }
+
+        /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
+        fun build(): GetPublicKeyCredentialOptionPrivileged {
+            return GetPublicKeyCredentialOptionPrivileged(this.requestJson,
+                    this.rp, this.clientDataHash, this.allowHybrid)
+        }
+    }
+
+    companion object {
+        const val BUNDLE_KEY_RP = "androidx.credentials.BUNDLE_KEY_RP"
+        const val BUNDLE_KEY_CLIENT_DATA_HASH =
+                "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
+        const val BUNDLE_KEY_ALLOW_HYBRID = "androidx.credentials.BUNDLE_KEY_ALLOW_HYBRID"
+        const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
+                "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
+                        "_PRIVILEGED"
+
+        @JvmStatic
+        internal fun toBundle(
+                requestJson: String,
+                rp: String,
+                clientDataHash: String,
+                allowHybrid: Boolean
+        ): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
+            bundle.putString(BUNDLE_KEY_RP, rp)
+            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
+            bundle.putBoolean(BUNDLE_KEY_ALLOW_HYBRID, allowHybrid)
+            return bundle
+        }
+
+        @JvmStatic
+        fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
+            try {
+                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
+                val rp = data.getString(BUNDLE_KEY_RP)
+                val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
+                val allowHybrid = data.get(BUNDLE_KEY_ALLOW_HYBRID)
+                return GetPublicKeyCredentialOptionPrivileged(
+                        requestJson!!,
+                        rp!!,
+                        clientDataHash!!,
+                        (allowHybrid!!) as Boolean,
+                )
+            } catch (e: Exception) {
+                throw FrameworkClassParsingException()
+            }
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
new file mode 100644
index 0000000..b45a63b
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.credentialmanager.jetpack.developer
+
+import android.os.Bundle
+
+/**
+ * Represents the user's passkey credential granted by the user for app sign-in.
+ *
+ * @property authenticationResponseJson the public key credential authentication response in
+ * JSON format that follows the standard webauthn json format shown at
+ * [this w3c link](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson)
+ * @throws NullPointerException If [authenticationResponseJson] is null. This is handled by the
+ * kotlin runtime
+ * @throws IllegalArgumentException If [authenticationResponseJson] is empty
+ *
+ * @hide
+ */
+class PublicKeyCredential constructor(
+        val authenticationResponseJson: String
+) : Credential(
+        TYPE_PUBLIC_KEY_CREDENTIAL,
+        toBundle(authenticationResponseJson)
+) {
+
+    init {
+        require(authenticationResponseJson.isNotEmpty()) {
+            "authentication response JSON must not be empty" }
+    }
+    companion object {
+        /** The type value for public key credential related operations. */
+        const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
+                "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+        const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
+                "androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
+
+        @JvmStatic
+        internal fun toBundle(authenticationResponseJson: String): Bundle {
+            val bundle = Bundle()
+            bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson)
+            return bundle
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt
new file mode 100644
index 0000000..1e639fe
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/ActionUi.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack.provider
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class ActionUi(
+  val icon: Icon,
+  val text: CharSequence,
+  val subtext: CharSequence?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): ActionUi {
+      var icon: Icon? = null
+      var text: CharSequence? = null
+      var subtext: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_ACTION_ICON)) {
+          icon = it.icon
+        } else if (it.hasHint(Entry.HINT_ACTION_TITLE)) {
+          text = it.text
+        } else if (it.hasHint(Entry.HINT_ACTION_SUBTEXT)) {
+          subtext = it.text
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return ActionUi(icon!!, text!!, subtext)
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt
new file mode 100644
index 0000000..dfbcae1
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntryUi.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.credentialmanager.jetpack.provider
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class CredentialEntryUi(
+  val credentialType: CharSequence,
+  val credentialTypeDisplayName: CharSequence,
+  val userName: CharSequence,
+  val userDisplayName: CharSequence?,
+  val entryIcon: Icon,
+  val lastUsedTimeMillis: Long?,
+  val note: CharSequence?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      var credentialType = slice.spec!!.type
+      var credentialTypeDisplayName: CharSequence? = null
+      var userName: CharSequence? = null
+      var userDisplayName: CharSequence? = null
+      var entryIcon: Icon? = null
+      var lastUsedTimeMillis: Long? = null
+      var note: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME)) {
+          credentialTypeDisplayName = it.text
+        } else if (it.hasHint(Entry.HINT_USER_NAME)) {
+          userName = it.text
+        } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) {
+          userDisplayName = it.text
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          entryIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        } else if (it.hasHint(Entry.HINT_NOTE)) {
+          note = it.text
+        }
+      }
+
+      return CredentialEntryUi(
+        credentialType, credentialTypeDisplayName!!, userName!!, userDisplayName, entryIcon!!,
+        lastUsedTimeMillis, note,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt
new file mode 100644
index 0000000..bcc0531
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/SaveEntryUi.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.credentialmanager.jetpack.provider
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a save entry used during the create credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class SaveEntryUi(
+  val userProviderAccountName: CharSequence?,
+  val credentialTypeIcon: Icon?,
+  val profileIcon: Icon?,
+  val passwordCount: Int?,
+  val passkeyCount: Int?,
+  val totalCredentialCount: Int?,
+  val lastUsedTimeMillis: Long?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): SaveEntryUi {
+      var userProviderAccountName: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var passwordCount: Int? = null
+      var passkeyCount: Int? = null
+      var totalCredentialCount: Int? = null
+      var lastUsedTimeMillis: Long? = null
+
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) {
+          userProviderAccountName = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PASSWORD_COUNT)) {
+          passwordCount = it.int
+        } else if (it.hasHint(Entry.HINT_PASSKEY_COUNT)) {
+          passkeyCount = it.int
+        } else if (it.hasHint(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) {
+          totalCredentialCount = it.int
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return SaveEntryUi(
+        userProviderAccountName!!, credentialTypeIcon, profileIcon,
+        passwordCount, passkeyCount, totalCredentialCount, lastUsedTimeMillis,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
new file mode 100644
index 0000000..15ae329
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt
@@ -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.credentialmanager.ui.theme
+
+import android.annotation.ColorInt
+import android.content.Context
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+import com.android.internal.R
+
+/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */
+val LocalAndroidColorScheme =
+    staticCompositionLocalOf<AndroidColorScheme> {
+        throw IllegalStateException(
+            "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " +
+                    "Composable surrounded by a CredentialSelectorTheme {}."
+        )
+    }
+
+/**
+ * The Android color scheme.
+ *
+ * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
+ * most of the colors in this class will be removed in favor of their M3 counterpart.
+ */
+class AndroidColorScheme internal constructor(context: Context) {
+
+    val colorPrimary = getColor(context, R.attr.colorPrimary)
+    val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark)
+    val colorAccent = getColor(context, R.attr.colorAccent)
+    val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary)
+    val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary)
+    val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary)
+    val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant)
+    val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant)
+    val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant)
+    val colorSurface = getColor(context, R.attr.colorSurface)
+    val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight)
+    val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant)
+    val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader)
+    val colorError = getColor(context, R.attr.colorError)
+    val colorBackground = getColor(context, R.attr.colorBackground)
+    val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating)
+    val panelColorBackground = getColor(context, R.attr.panelColorBackground)
+    val textColorPrimary = getColor(context, R.attr.textColorPrimary)
+    val textColorSecondary = getColor(context, R.attr.textColorSecondary)
+    val textColorTertiary = getColor(context, R.attr.textColorTertiary)
+    val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse)
+    val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse)
+    val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse)
+    val textColorOnAccent = getColor(context, R.attr.textColorOnAccent)
+    val colorForeground = getColor(context, R.attr.colorForeground)
+    val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
+
+    private fun getColor(context: Context, attr: Int): Color {
+        val ta = context.obtainStyledAttributes(intArrayOf(attr))
+        @ColorInt val color = ta.getColor(0, 0)
+        ta.recycle()
+        return Color(color)
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt
index cba8658..d8a8f16 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Shape.kt
@@ -1,11 +1,20 @@
 package com.android.credentialmanager.ui.theme
 
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Shapes
+import androidx.compose.material3.Shapes
 import androidx.compose.ui.unit.dp
 
 val Shapes = Shapes(
   small = RoundedCornerShape(100.dp),
-  medium = RoundedCornerShape(20.dp),
+  medium = RoundedCornerShape(28.dp),
   large = RoundedCornerShape(0.dp)
 )
+
+object EntryShape {
+  val TopRoundedCorner = RoundedCornerShape(28.dp, 28.dp, 0.dp, 0.dp)
+  val BottomRoundedCorner = RoundedCornerShape(0.dp, 0.dp, 28.dp, 28.dp)
+  // Used for middle entries.
+  val FullSmallRoundedCorner = RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp)
+  // Used for when there's a single entry.
+  val FullMediumRoundedCorner = RoundedCornerShape(28.dp, 28.dp, 28.dp, 28.dp)
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt
index a9d20ae..3ca0e44 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Theme.kt
@@ -1,47 +1,38 @@
 package com.android.credentialmanager.ui.theme
 
 import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
 import androidx.compose.runtime.Composable
-
-private val DarkColorPalette = darkColors(
-  primary = Purple200,
-  primaryVariant = Purple700,
-  secondary = Teal200
-)
-
-private val LightColorPalette = lightColors(
-  primary = Purple500,
-  primaryVariant = Purple700,
-  secondary = Teal200
-
-  /* Other default colors to override
-    background = Color.White,
-    surface = Color.White,
-    onPrimary = Color.White,
-    onSecondary = Color.Black,
-    onBackground = Color.Black,
-    onSurface = Color.Black,
-    */
-)
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
 
 @Composable
 fun CredentialSelectorTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable () -> Unit
 ) {
-  val colors = if (darkTheme) {
-    DarkColorPalette
-  } else {
-    LightColorPalette
-  }
+  val context = LocalContext.current
+
+  val colorScheme =
+    if (darkTheme) {
+      dynamicDarkColorScheme(context)
+    } else {
+      dynamicLightColorScheme(context)
+    }
+  val androidColorScheme = AndroidColorScheme(context)
+  val typography = Typography
 
   MaterialTheme(
-    colors = colors,
-    typography = Typography,
-    shapes = Shapes,
-    content = content
-  )
+    colorScheme,
+    typography = typography,
+    shapes = Shapes
+  ) {
+    CompositionLocalProvider(
+      LocalAndroidColorScheme provides androidColorScheme,
+    ) {
+      content()
+    }
+  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt
index d8fb01c..e09abbb 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Type.kt
@@ -1,6 +1,6 @@
 package com.android.credentialmanager.ui.theme
 
-import androidx.compose.material.Typography
+import androidx.compose.material3.Typography
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
@@ -8,32 +8,32 @@
 
 // Set of Material typography styles to start with
 val Typography = Typography(
-  subtitle1 = TextStyle(
+  titleMedium = TextStyle(
     fontFamily = FontFamily.Default,
     fontWeight = FontWeight.Normal,
     fontSize = 24.sp,
     lineHeight = 32.sp,
   ),
-  body1 = TextStyle(
+  bodyLarge = TextStyle(
     fontFamily = FontFamily.Default,
     fontWeight = FontWeight.Normal,
     fontSize = 14.sp,
     lineHeight = 20.sp,
   ),
-  body2 = TextStyle(
+  bodyMedium = TextStyle(
     fontFamily = FontFamily.Default,
     fontWeight = FontWeight.Normal,
     fontSize = 14.sp,
     lineHeight = 20.sp,
     color = textColorSecondary
   ),
-  button = TextStyle(
+  labelLarge = TextStyle(
     fontFamily = FontFamily.Default,
     fontWeight = FontWeight.Medium,
     fontSize = 14.sp,
     lineHeight = 20.sp,
   ),
-  h6 = TextStyle(
+  titleLarge = TextStyle(
     fontFamily = FontFamily.Default,
     fontWeight = FontWeight.Medium,
     fontSize = 16.sp,
diff --git a/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml b/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml
index 62dba98..0867293 100644
--- a/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml
+++ b/packages/DynamicSystemInstallationService/res/values-en-rCA/strings.xml
@@ -3,8 +3,8 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="keyguard_description" msgid="8582605799129954556">"Please enter your password and continue to Dynamic System Updates"</string>
     <string name="notification_install_completed" msgid="6252047868415172643">"Dynamic system is ready. To start using it, restart your device."</string>
-    <string name="notification_install_inprogress" msgid="7383334330065065017">"Installation in progress"</string>
-    <string name="notification_install_failed" msgid="4066039210317521404">"Installation failed"</string>
+    <string name="notification_install_inprogress" msgid="7383334330065065017">"Install in progress"</string>
+    <string name="notification_install_failed" msgid="4066039210317521404">"Install failed"</string>
     <string name="notification_image_validation_failed" msgid="2720357826403917016">"Image validation failed. Abort installation."</string>
     <string name="notification_dynsystem_in_use" msgid="1053194595682188396">"Currently running a dynamic system. Restart to use the original Android version."</string>
     <string name="notification_action_cancel" msgid="5929299408545961077">"Cancel"</string>
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index f8785f2..e88410c 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -36,7 +36,7 @@
     certificate: "platform",
 
     optimize: {
-        enabled: false,
+        proguard_flags_files: ["proguard.flags"],
     },
 
 	static_libs: [
diff --git a/packages/EasterEgg/proguard.flags b/packages/EasterEgg/proguard.flags
new file mode 100644
index 0000000..b333ab0
--- /dev/null
+++ b/packages/EasterEgg/proguard.flags
@@ -0,0 +1,4 @@
+# Note: This is a very conservative keep rule, but as the amount of app
+# code is small, this minimizes any maintenance risks while providing
+# most of the shrinking benefits for referenced libraries.
+-keep class com.android.egg.** { *; }
diff --git a/packages/InputDevices/res/values-en-rCA/strings.xml b/packages/InputDevices/res/values-en-rCA/strings.xml
index ab48729..1161783 100644
--- a/packages/InputDevices/res/values-en-rCA/strings.xml
+++ b/packages/InputDevices/res/values-en-rCA/strings.xml
@@ -19,7 +19,7 @@
     <string name="keyboard_layout_swiss_german_label" msgid="2305520941993314258">"Swiss German"</string>
     <string name="keyboard_layout_belgian" msgid="2011984572838651558">"Belgian"</string>
     <string name="keyboard_layout_bulgarian" msgid="8951224309972028398">"Bulgarian"</string>
-    <string name="keyboard_layout_bulgarian_phonetic" msgid="7568914730360106653">"Bulgarian, phonetic"</string>
+    <string name="keyboard_layout_bulgarian_phonetic" msgid="7568914730360106653">"Bulgarian, Phonetic"</string>
     <string name="keyboard_layout_italian" msgid="6497079660449781213">"Italian"</string>
     <string name="keyboard_layout_danish" msgid="8036432066627127851">"Danish"</string>
     <string name="keyboard_layout_norwegian" msgid="9090097917011040937">"Norwegian"</string>
diff --git a/packages/PackageInstaller/res/values-te/strings.xml b/packages/PackageInstaller/res/values-te/strings.xml
index c016bfc..3344d4d 100644
--- a/packages/PackageInstaller/res/values-te/strings.xml
+++ b/packages/PackageInstaller/res/values-te/strings.xml
@@ -73,8 +73,8 @@
     <string name="uninstall_all_blocked_profile_owner" msgid="2009393666026751501">"ఈ యాప్ కొందరు వినియోగదారులకు లేదా కొన్ని ప్రొఫైళ్లకు అవసరం, ఇతరులకు అన్‌ఇన్‌స్టాల్ చేయబడింది"</string>
     <string name="uninstall_blocked_profile_owner" msgid="6373897407002404848">"మీ ప్రొఫైల్ కోసం ఈ యాప్ అవసరం, అందువల్ల దీన్ని అన్ఇన్‌స్టాల్ చేయడం కుదరదు."</string>
     <string name="uninstall_blocked_device_owner" msgid="6724602931761073901">"మీ పరికర నిర్వాహకులకు ఈ యాప్ అవసరం, అందువల్ల దీన్ని అన్‌ఇన్‌స్టాల్ చేయడం కుదరదు."</string>
-    <string name="manage_device_administrators" msgid="3092696419363842816">"పరికర నిర్వాహక యాప్‌లను నిర్వహించు"</string>
-    <string name="manage_users" msgid="1243995386982560813">"వినియోగదారులను నిర్వహించు"</string>
+    <string name="manage_device_administrators" msgid="3092696419363842816">"పరికర నిర్వాహక యాప్‌లను మేనేజ్ చేయండి"</string>
+    <string name="manage_users" msgid="1243995386982560813">"వినియోగదారులను మేనేజ్ చేయండి"</string>
     <string name="uninstall_failed_msg" msgid="2176744834786696012">"<xliff:g id="APP_NAME">%1$s</xliff:g>ని అన్‌ఇన్‌స్టాల్ చేయడం సాధ్యపడలేదు."</string>
     <string name="Parse_error_dlg_text" msgid="1661404001063076789">"ప్యాకేజీని అన్వయించడంలో సమస్య ఏర్పడింది."</string>
     <string name="wear_not_allowed_dlg_title" msgid="8664785993465117517">"Android Wear"</string>
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index 688d116..6a3b239 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -189,6 +189,9 @@
     <!-- Text to show in warning dialog on the tv when the app source is not trusted [CHAR LIMIT=NONE] -->
     <string name="untrusted_external_source_warning" product="tv">For your security, your TV currently isn’t allowed to install unknown apps from this source. You can change this in Settings.</string>
 
+    <!-- Text to show in warning dialog on the wear when the app source is not trusted [CHAR LIMIT=NONE] -->
+    <string name="untrusted_external_source_warning" product="watch">For your security, your watch currently isn’t allowed to install unknown apps from this source. You can change this in Settings.</string>
+
     <!-- Text to show in warning dialog on the phone when the app source is not trusted [CHAR LIMIT=NONE] -->
     <string name="untrusted_external_source_warning" product="default">For your security, your phone currently isn’t allowed to install unknown apps from this source. You can change this in Settings.</string>
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index de76632..fa93670 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -124,9 +124,8 @@
     private static final int DLG_INSTALL_ERROR = DLG_BASE + 4;
     private static final int DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER = DLG_BASE + 5;
     private static final int DLG_ANONYMOUS_SOURCE = DLG_BASE + 6;
-    private static final int DLG_NOT_SUPPORTED_ON_WEAR = DLG_BASE + 7;
-    private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 8;
-    private static final int DLG_INSTALL_APPS_RESTRICTED_FOR_USER = DLG_BASE + 9;
+    private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 7;
+    private static final int DLG_INSTALL_APPS_RESTRICTED_FOR_USER = DLG_BASE + 8;
 
     // If unknown sources are temporary allowed
     private boolean mAllowUnknownSources;
@@ -189,8 +188,6 @@
             case DLG_INSTALL_ERROR:
                 return InstallErrorDialog.newInstance(
                         mPm.getApplicationLabel(mPkgInfo.applicationInfo));
-            case DLG_NOT_SUPPORTED_ON_WEAR:
-                return NotSupportedOnWearDialog.newInstance();
             case DLG_INSTALL_APPS_RESTRICTED_FOR_USER:
                 return SimpleErrorDialog.newInstance(
                         R.string.install_apps_user_restriction_dlg_text);
@@ -379,12 +376,8 @@
             return;
         }
 
-        if (DeviceUtils.isWear(this)) {
-            showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
-            return;
-        }
-
         final boolean wasSetUp = processAppSnippet(packageSource);
+
         if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp);
 
         if (!wasSetUp) {
@@ -779,21 +772,6 @@
     }
 
     /**
-     * An error dialog shown when the app is not supported on wear
-     */
-    public static class NotSupportedOnWearDialog extends SimpleErrorDialog {
-        static SimpleErrorDialog newInstance() {
-            return SimpleErrorDialog.newInstance(R.string.wear_not_allowed_dlg_text);
-        }
-
-        @Override
-        public void onCancel(DialogInterface dialog) {
-            getActivity().setResult(RESULT_OK);
-            getActivity().finish();
-        }
-    }
-
-    /**
      * An error dialog shown when the device is out of space
      */
     public static class OutOfSpaceDialog extends AppErrorDialog {
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
index 93e6271..3029d10 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
@@ -55,18 +55,15 @@
     private static final String PRIVET_SERVICE = "_privet._tcp";
 
     /** The required mDNS service types */
-    private static final Set<String> PRINTER_SERVICE_TYPE = new HashSet<String>() {{
-        // Not checking _printer_._sub
-        add(PRIVET_SERVICE);
-    }};
+    private static final Set<String> PRINTER_SERVICE_TYPE = Set.of(
+            PRIVET_SERVICE); // Not checking _printer_._sub
 
     /** All possible connection states */
-    private static final Set<String> POSSIBLE_CONNECTION_STATES = new HashSet<String>() {{
-        add("online");
-        add("offline");
-        add("connecting");
-        add("not-configured");
-    }};
+    private static final Set<String> POSSIBLE_CONNECTION_STATES = Set.of(
+            "online",
+            "offline",
+            "connecting",
+            "not-configured");
 
     private static final byte SUPPORTED_TXTVERS = '1';
 
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
index 34e7e3d..0c5de27 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
@@ -37,9 +37,7 @@
 public class MDNSFilterPlugin implements PrintServicePlugin {
 
     /** The mDNS service types supported */
-    private static final Set<String> PRINTER_SERVICE_TYPES = new HashSet<String>() {{
-        add("_ipp._tcp");
-    }};
+    private static final Set<String> PRINTER_SERVICE_TYPES = Set.of("_ipp._tcp");
 
     /**
      * The printer filter for {@link MDNSFilteredDiscovery} passing only mDNS results
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java
index d03bb1d..b9983c3 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterMopria.java
@@ -23,7 +23,6 @@
 import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
 import com.android.printservice.recommendation.util.MDNSUtils;
 
-import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -32,10 +31,7 @@
 class PrinterFilterMopria implements MDNSFilteredDiscovery.PrinterFilter {
     private static final String TAG = "PrinterFilterMopria";
 
-    static final Set<String> MOPRIA_MDNS_SERVICES = new HashSet<String>() {{
-        add("_ipp._tcp");
-        add("_ipps._tcp");
-    }};
+    static final Set<String> MOPRIA_MDNS_SERVICES = Set.of("_ipp._tcp", "_ipps._tcp");
 
     private static final String PDL__PDF = "application/pdf";
     private static final String PDL__PCLM = "application/PCLm";
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java
index b9b9098..680dd84 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/PrinterFilterSamsung.java
@@ -25,7 +25,6 @@
 import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
 import com.android.printservice.recommendation.util.MDNSUtils;
 
-import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -36,9 +35,7 @@
 class PrinterFilterSamsung implements MDNSFilteredDiscovery.PrinterFilter {
     private static final String TAG = "PrinterFilterSamsung";
 
-    static final Set<String> SAMSUNG_MDNS_SERVICES = new HashSet<String>() {{
-        add("_pdl-datastream._tcp");
-    }};
+    static final Set<String> SAMSUNG_MDNS_SERVICES = Set.of("_pdl-datastream._tcp");
 
     private static final String[] NOT_SUPPORTED_MODELS = new String[]{
             "SCX-5x15",
@@ -57,9 +54,7 @@
     private static final String ATTR_PRODUCT = "product";
     private static final String ATTR_TY = "ty";
 
-    private static Set<String> SAMUNG_VENDOR_SET = new HashSet<String>() {{
-        add("samsung");
-    }};
+    private static final Set<String> SAMUNG_VENDOR_SET = Set.of("samsung");
 
     @Override
     public boolean matchesCriteria(NsdServiceInfo nsdServiceInfo) {
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
index ae1bdce..cbd5833 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/samsung/SamsungRecommendationPlugin.java
@@ -29,10 +29,11 @@
 import java.util.Set;
 
 public class SamsungRecommendationPlugin implements PrintServicePlugin {
-    private static final Set<String> ALL_MDNS_SERVICES = new HashSet<String>() {{
-        addAll(PrinterFilterMopria.MOPRIA_MDNS_SERVICES);
-        addAll(PrinterFilterSamsung.SAMSUNG_MDNS_SERVICES);
-    }};
+    private static final Set<String> ALL_MDNS_SERVICES = new HashSet<String>();
+    static {
+        ALL_MDNS_SERVICES.addAll(PrinterFilterMopria.MOPRIA_MDNS_SERVICES);
+        ALL_MDNS_SERVICES.addAll(PrinterFilterSamsung.SAMSUNG_MDNS_SERVICES);
+    }
 
     private final @NonNull Context mContext;
     private final @NonNull MDNSFilteredDiscovery mMDNSFilteredDiscovery;
diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
index 00b3736..b0aa8f1 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java
@@ -402,7 +402,7 @@
 
         @Override
         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
-            if ((isOptionsClosed() || isOptionsClosed()) && dy <= 0) {
+            if (isOptionsClosed() && dy <= 0) {
                 return;
             }
 
diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp
index 332bebf..c35fb3b 100644
--- a/packages/SettingsLib/ActivityEmbedding/Android.bp
+++ b/packages/SettingsLib/ActivityEmbedding/Android.bp
@@ -26,4 +26,9 @@
         "androidx.window.extensions",
         "androidx.window.sidecar",
     ],
+
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
index 2742558..0949e1d 100644
--- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
+++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
@@ -21,6 +21,7 @@
     <uses-sdk android:minSdkVersion="21" />
 
     <application>
+        <uses-library android:name="org.apache.http.legacy" android:required="false" />
         <uses-library android:name="androidx.window.extensions" android:required="false" />
         <uses-library android:name="androidx.window.sidecar" android:required="false" />
     </application>
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index c659525..f170ead 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -54,6 +54,7 @@
         "SettingsLibSettingsTransition",
         "SettingsLibButtonPreference",
         "SettingsLibDeviceStateRotationLock",
+        "SettingsLibProfileSelector",
         "setupdesign",
         "zxing-core-1.7",
         "androidx.room_room-runtime",
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 8eafbdf..a53782a 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -3,10 +3,11 @@
 edgarwang@google.com
 emilychuang@google.com
 evanlaird@google.com
+hanxu@google.com
 juliacr@google.com
 leifhendrik@google.com
-tmfang@google.com
 virgild@google.com
+ykhung@google.com
 
 # Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS)
 per-file *.xml=*
diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp
new file mode 100644
index 0000000..250cd75
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/Android.bp
@@ -0,0 +1,27 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SettingsLibProfileSelector",
+
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "com.google.android.material_material",
+        "SettingsLibSettingsTheme",
+    ],
+
+    sdk_version: "system_current",
+    min_sdk_version: "23",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.mediaprovider",
+    ],
+}
diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
new file mode 100644
index 0000000..a57469e
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.settingslib.widget">
+
+    <uses-sdk android:minSdkVersion="23" />
+</manifest>
diff --git a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_indicator_color.xml b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_indicator_color.xml
similarity index 100%
rename from packages/SettingsLib/res/color-night-v31/settingslib_tabs_indicator_color.xml
rename to packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_indicator_color.xml
diff --git a/packages/SettingsLib/res/color-night-v31/settingslib_tabs_text_color.xml b/packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_text_color.xml
similarity index 100%
rename from packages/SettingsLib/res/color-night-v31/settingslib_tabs_text_color.xml
rename to packages/SettingsLib/ProfileSelector/res/color-night/settingslib_tabs_text_color.xml
diff --git a/packages/SettingsLib/res/color-v31/settingslib_tabs_indicator_color.xml b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_indicator_color.xml
similarity index 100%
rename from packages/SettingsLib/res/color-v31/settingslib_tabs_indicator_color.xml
rename to packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_indicator_color.xml
diff --git a/packages/SettingsLib/res/color-v31/settingslib_tabs_text_color.xml b/packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_text_color.xml
similarity index 100%
rename from packages/SettingsLib/res/color-v31/settingslib_tabs_text_color.xml
rename to packages/SettingsLib/ProfileSelector/res/color/settingslib_tabs_text_color.xml
diff --git a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_background.xml b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_background.xml
similarity index 100%
rename from packages/SettingsLib/res/drawable-v31/settingslib_tabs_background.xml
rename to packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_background.xml
diff --git a/packages/SettingsLib/res/drawable-v31/settingslib_tabs_indicator_background.xml b/packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_indicator_background.xml
similarity index 100%
rename from packages/SettingsLib/res/drawable-v31/settingslib_tabs_indicator_background.xml
rename to packages/SettingsLib/ProfileSelector/res/drawable/settingslib_tabs_indicator_background.xml
diff --git a/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml b/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml
new file mode 100644
index 0000000..0448c6c
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/res/layout/tab_fragment.xml
@@ -0,0 +1,38 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:theme="@style/Theme.MaterialComponents.DayNight"
+    android:id="@+id/tab_container"
+    android:clipToPadding="true"
+    android:clipChildren="true"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.google.android.material.tabs.TabLayout
+        android:id="@+id/tabs"
+        style="@style/SettingsLibTabsStyle"/>
+
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/view_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    </androidx.viewpager2.widget.ViewPager2>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
new file mode 100644
index 0000000..68d4047
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Header for items under the personal user [CHAR LIMIT=30] -->
+    <string name="settingslib_category_personal">Personal</string>
+    <!-- Header for items under the work user [CHAR LIMIT=30] -->
+    <string name="settingslib_category_work">Work</string>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/values-v31/styles.xml b/packages/SettingsLib/ProfileSelector/res/values/styles.xml
similarity index 100%
rename from packages/SettingsLib/res/values-v31/styles.xml
rename to packages/SettingsLib/ProfileSelector/res/values/styles.xml
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
new file mode 100644
index 0000000..ac426ed
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
@@ -0,0 +1,118 @@
+/*
+ * 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.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+
+/**
+ * Base fragment class for profile settings.
+ */
+public abstract class ProfileSelectFragment extends Fragment {
+
+    /**
+     * Personal or Work profile tab of {@link ProfileSelectFragment}
+     * <p>0: Personal tab.
+     * <p>1: Work profile tab.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT_TAB =
+            ":settings:show_fragment_tab";
+
+    /**
+     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+     */
+    public static final int PERSONAL_TAB = 0;
+
+    /**
+     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+     */
+    public static final int WORK_TAB = 1;
+
+    private ViewGroup mContentView;
+
+    private ViewPager2 mViewPager;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        // Defines the xml file for the fragment
+        mContentView = (ViewGroup) inflater.inflate(R.layout.tab_fragment, container, false);
+
+        final Activity activity = getActivity();
+        final int titleResId = getTitleResId();
+        if (titleResId > 0) {
+            activity.setTitle(titleResId);
+        }
+        final int selectedTab = getTabId(activity, getArguments());
+
+        final View tabContainer = mContentView.findViewById(R.id.tab_container);
+        mViewPager = tabContainer.findViewById(R.id.view_pager);
+        mViewPager.setAdapter(new ProfileViewPagerAdapter(this));
+        final TabLayout tabs = tabContainer.findViewById(R.id.tabs);
+        new TabLayoutMediator(tabs, mViewPager,
+                (tab, position) -> tab.setText(getPageTitle(position))
+        ).attach();
+
+        tabContainer.setVisibility(View.VISIBLE);
+        final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
+        tab.select();
+
+        return mContentView;
+    }
+
+    /**
+     * create Personal or Work profile fragment
+     * <p>0: Personal profile.
+     * <p>1: Work profile.
+     */
+    public abstract Fragment createFragment(int position);
+
+    /**
+     * Returns a resource ID of the title
+     * Override this if the title needs to be updated dynamically.
+     */
+    public int getTitleResId() {
+        return 0;
+    }
+
+    int getTabId(Activity activity, Bundle bundle) {
+        if (bundle != null) {
+            final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1);
+            if (extraTab != -1) {
+                return extraTab;
+            }
+        }
+        return PERSONAL_TAB;
+    }
+
+    private CharSequence getPageTitle(int position) {
+        if (position == WORK_TAB) {
+            return getContext().getString(R.string.settingslib_category_work);
+        }
+
+        return getString(R.string.settingslib_category_personal);
+    }
+}
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
new file mode 100644
index 0000000..daf2564
--- /dev/null
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
@@ -0,0 +1,43 @@
+/*
+ * 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.widget;
+
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+/**
+ * ViewPager Adapter to handle between TabLayout and ViewPager2
+ */
+public class ProfileViewPagerAdapter extends FragmentStateAdapter {
+
+    private final ProfileSelectFragment mParentFragments;
+
+    ProfileViewPagerAdapter(ProfileSelectFragment fragment) {
+        super(fragment);
+        mParentFragments = fragment;
+    }
+
+    @Override
+    public Fragment createFragment(int position) {
+        return mParentFragments.createFragment(position);
+    }
+
+    @Override
+    public int getItemCount() {
+        return 2;
+    }
+}
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
index bcc64d3..b5a21bd 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
@@ -23,5 +23,6 @@
     apex_available: [
         "//apex_available:platform",
         "com.android.permission",
+        "com.android.mediaprovider",
     ],
 }
diff --git a/packages/SettingsLib/SettingsTheme/Android.bp b/packages/SettingsLib/SettingsTheme/Android.bp
index 82e0220..939977f 100644
--- a/packages/SettingsLib/SettingsTheme/Android.bp
+++ b/packages/SettingsLib/SettingsTheme/Android.bp
@@ -24,5 +24,6 @@
         "com.android.permission",
         "com.android.adservices",
         "com.android.healthconnect",
+        "com.android.mediaprovider",
     ],
 }
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 811cdd8..b8fd579 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -16,10 +16,11 @@
 
 buildscript {
     ext {
-        spa_min_sdk = 21
-        jetpack_compose_version = '1.2.0-alpha04'
+        BUILD_TOOLS_VERSION = "30.0.3"
+        MIN_SDK = 21
+        TARGET_SDK = 33
+        jetpack_compose_version = '1.4.0-alpha01'
         jetpack_compose_compiler_version = '1.3.2'
-        jetpack_compose_material3_version = '1.0.0-alpha06'
     }
 }
 plugins {
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 0a4972f..1e52aaf 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.gallery">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application
         android:name=".GalleryApplication"
         android:icon="@mipmap/ic_launcher"
@@ -32,14 +34,38 @@
             </intent-filter>
         </activity>
 
+        <provider
+            android:name="com.android.settingslib.spa.framework.SpaSearchProvider"
+            android:authorities="com.android.spa.gallery.search.provider"
+            android:enabled="true"
+            android:exported="false">
+        </provider>
+
+        <provider android:name="com.android.settingslib.spa.framework.SpaSliceProvider"
+            android:authorities="com.android.spa.gallery.slice.provider"
+            android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.app.slice.category.SLICE" />
+            </intent-filter>
+        </provider>
+
+        <receiver
+            android:name="com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver"
+            android:exported="false">
+        </receiver>
+
         <activity
-            android:name=".GalleryDebugActivity"
+            android:name="com.android.settingslib.spa.framework.debug.BlankActivity"
             android:exported="true">
         </activity>
-
+        <activity
+            android:name="com.android.settingslib.spa.framework.debug.DebugActivity"
+            android:exported="true">
+        </activity>
         <provider
-            android:name=".GalleryEntryProvider"
-            android:authorities="com.android.spa.gallery.provider"
+            android:name="com.android.settingslib.spa.framework.debug.DebugProvider"
+            android:authorities="com.android.spa.gallery.debug.provider"
             android:enabled="true"
             android:exported="false">
         </provider>
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index 551a0b1..7868aff 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,13 @@
 
 android {
     namespace 'com.android.settingslib.spa.gallery'
-    compileSdk 33
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
         applicationId "com.android.settingslib.spa.gallery"
-        minSdk spa_min_sdk
-        targetSdk 33
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
         versionCode 1
         versionName "1.0"
     }
@@ -54,13 +55,8 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
-    implementation(project(":spa"))
+    implementation project(":spa")
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
index 36b58ad..dfbf244 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt
@@ -22,6 +22,6 @@
 class GalleryApplication : Application() {
     override fun onCreate() {
         super.onCreate()
-        SpaEnvironmentFactory.reset(GallerySpaEnvironment)
+        SpaEnvironmentFactory.reset(GallerySpaEnvironment(this))
     }
 }
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
deleted file mode 100644
index 23072a2..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.DebugActivity
-
-class GalleryDebugActivity : DebugActivity()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
deleted file mode 100644
index 817c209f..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryEntryProvider.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.EntryProvider
-
-class GalleryEntryProvider : EntryProvider()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index acb22da..941e770 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.spa.gallery
 
+import android.content.Context
+import com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver
 import com.android.settingslib.spa.framework.common.LocalLogger
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironment
@@ -23,8 +25,10 @@
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
+import com.android.settingslib.spa.gallery.page.ChartPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider
@@ -48,7 +52,7 @@
     // Add your SPPs
 }
 
-object GallerySpaEnvironment : SpaEnvironment() {
+class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
     override val pageProviderRepository = lazy {
         SettingsPageProviderRepository(
             allPageProviders = listOf(
@@ -66,6 +70,8 @@
                 IllustrationPageProvider,
                 CategoryPageProvider,
                 ActionButtonPageProvider,
+                ProgressBarPageProvider,
+                ChartPageProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
@@ -74,8 +80,8 @@
     }
 
     override val browseActivityClass = GalleryMainActivity::class.java
-
-    override val entryProviderAuthorities = "com.android.spa.gallery.provider"
-
+    override val sliceBroadcastReceiverClass = SpaSliceBroadcastReceiver::class.java
+    override val searchProviderAuthorities = "com.android.spa.gallery.search.provider"
+    override val sliceProviderAuthorities = "com.android.spa.gallery.slice.provider"
     override val logger = LocalLogger()
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index e40775a..83c72c7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -18,10 +18,10 @@
 
 import android.os.Bundle
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
@@ -29,8 +29,10 @@
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
 import com.android.settingslib.spa.gallery.page.ArgumentPageModel
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
+import com.android.settingslib.spa.gallery.page.ChartPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
@@ -54,12 +56,18 @@
             IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return SpaEnvironmentFactory.instance.appContext.getString(R.string.app_name)
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        HomeScaffold(title = stringResource(R.string.app_name)) {
+        HomeScaffold(title = getTitle(arguments)) {
             for (entry in buildEntry(arguments)) {
                 if (entry.owner.isCreateBy(SettingsPageProviderEnum.ARGUMENT.name)) {
                     entry.UiLayout(ArgumentPageModel.buildArgument(intParam = 0))
@@ -74,6 +82,7 @@
 @Preview(showBackground = true)
 @Composable
 private fun HomeScreenPreview() {
+    SpaEnvironmentFactory.resetForPreview()
     SettingsTheme {
         HomePageProvider.Page(null)
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
index 8207310..7958d11 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
@@ -23,6 +23,7 @@
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
@@ -98,9 +99,13 @@
             }
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return ArgumentPageModel.genPageTitle()
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = ArgumentPageModel.create(arguments).genPageTitle()) {
+        RegularScaffold(title = getTitle(arguments)) {
             for (entry in buildEntry(arguments)) {
                 if (entry.toPage != null) {
                     entry.UiLayout(ArgumentPageModel.buildNextArgument(arguments))
@@ -115,6 +120,7 @@
 @Preview(showBackground = true)
 @Composable
 private fun ArgumentPagePreview() {
+    SpaEnvironmentFactory.resetForPreview()
     SettingsTheme {
         ArgumentPageProvider.Page(
             ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0)
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
index e5e3c67..5f15865 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt
@@ -81,6 +81,10 @@
             return EntrySearchData(title = PAGE_TITLE, keyword = ARGUMENT_PAGE_KEYWORDS)
         }
 
+        fun genPageTitle(): String {
+            return PAGE_TITLE
+        }
+
         @Composable
         fun create(arguments: Bundle?): ArgumentPageModel {
             val pageModel: ArgumentPageModel = viewModel(key = arguments.toString())
@@ -89,7 +93,6 @@
         }
     }
 
-    private val title = PAGE_TITLE
     private var arguments: Bundle? = null
     private var stringParam: String? = null
     private var intParam: Int? = null
@@ -104,11 +107,6 @@
     }
 
     @Composable
-    fun genPageTitle(): String {
-        return title
-    }
-
-    @Composable
     fun genStringParamPreferenceModel(): PreferenceModel {
         return object : PreferenceModel {
             override val title = STRING_PARAM_TITLE
@@ -131,7 +129,7 @@
             "$INT_PARAM_NAME=" + intParam!!
         )
         return object : PreferenceModel {
-            override val title = genPageTitle()
+            override val title = PAGE_TITLE
             override val summary = stateOf(summaryArray.joinToString(", "))
             override val onClick = navigator(
                 SettingsPageProviderEnum.ARGUMENT.displayName + parameter.navLink(arguments)
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
new file mode 100644
index 0000000..160e77b
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.chart.BarChart
+import com.android.settingslib.spa.widget.chart.BarChartData
+import com.android.settingslib.spa.widget.chart.BarChartModel
+import com.android.settingslib.spa.widget.chart.LineChart
+import com.android.settingslib.spa.widget.chart.LineChartData
+import com.android.settingslib.spa.widget.chart.LineChartModel
+import com.android.settingslib.spa.widget.chart.PieChart
+import com.android.settingslib.spa.widget.chart.PieChartData
+import com.android.settingslib.spa.widget.chart.PieChartModel
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.github.mikephil.charting.formatter.IAxisValueFormatter
+import java.text.NumberFormat
+
+private enum class WeekDay(val num: Int) {
+    Sun(0), Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6),
+}
+private const val TITLE = "Sample Chart"
+
+object ChartPageProvider : SettingsPageProvider {
+    override val name = "Chart"
+
+    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+        val owner = SettingsPage.create(name)
+        val entryList = mutableListOf<SettingsEntry>()
+        entryList.add(
+            SettingsEntryBuilder.create("Line Chart", owner)
+                .setUiLayoutFn {
+                    Preference(object : PreferenceModel {
+                        override val title = "Line Chart"
+                    })
+                    LineChart(
+                        lineChartModel = object : LineChartModel {
+                            override val chartDataList = listOf(
+                                LineChartData(x = 0f, y = 0f),
+                                LineChartData(x = 1f, y = 0.1f),
+                                LineChartData(x = 2f, y = 0.2f),
+                                LineChartData(x = 3f, y = 0.6f),
+                                LineChartData(x = 4f, y = 0.9f),
+                                LineChartData(x = 5f, y = 1.0f),
+                                LineChartData(x = 6f, y = 0.8f),
+                            )
+                            override val xValueFormatter =
+                                IAxisValueFormatter { value, _ ->
+                                    "${WeekDay.values()[value.toInt()]}"
+                                }
+                            override val yValueFormatter =
+                                IAxisValueFormatter { value, _ ->
+                                    NumberFormat.getPercentInstance().format(value)
+                                }
+                        }
+                    )
+                }.build()
+        )
+        entryList.add(
+            SettingsEntryBuilder.create("Bar Chart", owner)
+                .setUiLayoutFn {
+                    Preference(object : PreferenceModel {
+                        override val title = "Bar Chart"
+                    })
+                    BarChart(
+                        barChartModel = object : BarChartModel {
+                            override val chartDataList = listOf(
+                                BarChartData(x = 0f, y = 12f),
+                                BarChartData(x = 1f, y = 5f),
+                                BarChartData(x = 2f, y = 21f),
+                                BarChartData(x = 3f, y = 5f),
+                                BarChartData(x = 4f, y = 10f),
+                                BarChartData(x = 5f, y = 9f),
+                                BarChartData(x = 6f, y = 1f),
+                            )
+                            override val xValueFormatter =
+                                IAxisValueFormatter { value, _ ->
+                                    "${WeekDay.values()[value.toInt()]}"
+                                }
+                            override val yValueFormatter =
+                                IAxisValueFormatter { value, _ ->
+                                    "${value.toInt()}m"
+                                }
+                            override val yAxisMaxValue = 30f
+                        }
+                    )
+                }.build()
+        )
+        entryList.add(
+            SettingsEntryBuilder.create("Pie Chart", owner)
+                .setUiLayoutFn {
+                    Preference(object : PreferenceModel {
+                        override val title = "Pie Chart"
+                    })
+                    PieChart(
+                        pieChartModel = object : PieChartModel {
+                            override val chartDataList = listOf(
+                                PieChartData(label = "Settings", value = 20f),
+                                PieChartData(label = "Chrome", value = 5f),
+                                PieChartData(label = "Gmail", value = 3f),
+                            )
+                            override val centerText = "Today"
+                        }
+                    )
+                }.build()
+        )
+
+        return entryList
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+            .setIsAllowSearch(true)
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(title = TITLE) {
+            for (entry in buildEntry(arguments)) {
+                entry.UiLayout(arguments)
+            }
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ChartPagePreview() {
+    SettingsTheme {
+        ChartPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
index 0fc2a5f..c903cfd 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
@@ -67,9 +67,13 @@
             }
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
+        RegularScaffold(title = getTitle(arguments)) {
             for (entry in buildEntry(arguments)) {
                 entry.UiLayout()
             }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
index a64d4a5..e10cf3a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
@@ -31,7 +31,6 @@
 import com.android.settingslib.spa.widget.ResourceType
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 private const val TITLE = "Sample Illustration"
 
@@ -82,13 +81,8 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
new file mode 100644
index 0000000..9136b04
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.SystemUpdate
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarPreference
+import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CircularLoadingBar
+import com.android.settingslib.spa.widget.ui.CircularProgressBar
+import com.android.settingslib.spa.widget.ui.LinearLoadingBar
+import kotlinx.coroutines.delay
+
+private const val TITLE = "Sample ProgressBar"
+
+object ProgressBarPageProvider : SettingsPageProvider {
+    override val name = "ProgressBar"
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+            .setIsAllowSearch(true)
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        // Mocks a loading time of 2 seconds.
+        var loading by remember { mutableStateOf(true) }
+        LaunchedEffect(Unit) {
+            delay(2000)
+            loading = false
+        }
+
+        RegularScaffold(title = getTitle(arguments)) {
+            // Auto update the progress and finally jump tp 0.4f.
+            var progress by remember { mutableStateOf(0f) }
+            LaunchedEffect(Unit) {
+                delay(2000)
+                while (progress < 1f) {
+                    delay(100)
+                    progress += 0.01f
+                }
+                delay(500)
+                progress = 0.4f
+            }
+
+            // Show as a placeholder for progress bar
+            LargeProgressBar(progress)
+            // The remaining information only shows after loading complete.
+            if (!loading) {
+                SimpleProgressBar()
+                ProgressBarWithData()
+                CircularProgressBar(progress = progress, radius = 160f)
+            }
+        }
+
+        // Add loading bar examples, running for 2 seconds.
+        LinearLoadingBar(isLoading = loading, yOffset = 64.dp)
+        CircularLoadingBar(isLoading = loading)
+    }
+}
+
+@Composable
+private fun LargeProgressBar(progress: Float) {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Large Progress Bar"
+        override val progress = progress
+        override val height = 20f
+    })
+}
+
+@Composable
+private fun SimpleProgressBar() {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Simple Progress Bar"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.SystemUpdate
+    })
+}
+
+@Composable
+private fun ProgressBarWithData() {
+    ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+        override val title = "Progress Bar with Data"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.Delete
+    }, data = "25G")
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ProgressBarPagePreview() {
+    SettingsTheme {
+        ProgressBarPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
index e09ebda..cb58a95 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -17,7 +17,10 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPage
@@ -46,11 +49,17 @@
             }
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        SettingsScaffold(title = TITLE) {
-            SettingsPager(listOf("Personal", "Work")) {
-                PlaceholderTitle("Page $it")
+        SettingsScaffold(title = getTitle(arguments)) { paddingValues ->
+            Box(Modifier.padding(paddingValues)) {
+                SettingsPager(listOf("Personal", "Work")) {
+                    PlaceholderTitle("Page $it")
+                }
             }
         }
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
index 0f95bf6..73b34a5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
@@ -33,11 +33,10 @@
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.SettingsSlider
-import com.android.settingslib.spa.widget.SettingsSliderModel
+import com.android.settingslib.spa.widget.preference.SliderPreference
+import com.android.settingslib.spa.widget.preference.SliderPreferenceModel
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 private const val TITLE = "Sample Slider"
 
@@ -51,7 +50,7 @@
             SettingsEntryBuilder.create("Simple Slider", owner)
                 .setIsAllowSearch(true)
                 .setUiLayoutFn {
-                    SettingsSlider(object : SettingsSliderModel {
+                    SliderPreference(object : SliderPreferenceModel {
                         override val title = "Simple Slider"
                         override val initValue = 40
                     })
@@ -61,7 +60,7 @@
             SettingsEntryBuilder.create("Slider with icon", owner)
                 .setIsAllowSearch(true)
                 .setUiLayoutFn {
-                    SettingsSlider(object : SettingsSliderModel {
+                    SliderPreference(object : SliderPreferenceModel {
                         override val title = "Slider with icon"
                         override val initValue = 30
                         override val onValueChangeFinished = {
@@ -78,7 +77,7 @@
                     val initValue = 0
                     var icon by remember { mutableStateOf(Icons.Outlined.MusicOff) }
                     var sliderPosition by remember { mutableStateOf(initValue) }
-                    SettingsSlider(object : SettingsSliderModel {
+                    SliderPreference(object : SliderPreferenceModel {
                         override val title = "Slider with changeable icon"
                         override val initValue = initValue
                         override val onValueChange = { it: Int ->
@@ -96,7 +95,7 @@
             SettingsEntryBuilder.create("Slider with steps", owner)
                 .setIsAllowSearch(true)
                 .setUiLayoutFn {
-                    SettingsSlider(object : SettingsSliderModel {
+                    SliderPreference(object : SliderPreferenceModel {
                         override val title = "Slider with steps"
                         override val initValue = 2
                         override val valueRange = 1..5
@@ -119,13 +118,8 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
index a8e4938..f38a8d4 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
@@ -33,7 +33,6 @@
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 private const val TITLE = "Sample MainSwitchPreference"
 
@@ -72,13 +71,8 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
index 165eaa0..61925a7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt
@@ -17,7 +17,6 @@
 package com.android.settingslib.spa.gallery.preference
 
 import android.os.Bundle
-import androidx.compose.runtime.Composable
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -25,7 +24,6 @@
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 private const val TITLE = "Category: Preference"
 
@@ -54,12 +52,7 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
index a2a913f..ff89f2b 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
@@ -27,6 +27,8 @@
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -36,6 +38,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
+import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_SUMMARY
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.ASYNC_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.AUTO_UPDATE_PREFERENCE_TITLE
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.DISABLE_PREFERENCE_SUMMARY
@@ -45,11 +48,15 @@
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_KEYWORDS
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_SUMMARY
 import com.android.settingslib.spa.gallery.preference.PreferencePageModel.Companion.SIMPLE_PREFERENCE_TITLE
+import com.android.settingslib.spa.slice.createBrowsePendingIntent
+import com.android.settingslib.spa.slice.provider.createDemoActionSlice
+import com.android.settingslib.spa.slice.provider.createDemoBrowseSlice
+import com.android.settingslib.spa.slice.provider.createDemoSlice
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import com.android.settingslib.spa.widget.ui.SettingsIcon
+import kotlinx.coroutines.delay
 
 private const val TAG = "PreferencePage"
 
@@ -104,6 +111,7 @@
         entryList.add(
             createEntry(EntryEnum.DISABLED_PREFERENCE)
                 .setIsAllowSearch(true)
+                .setHasMutableStatus(true)
                 .setMacro {
                     spaLogger.message(TAG, "create macro for ${EntryEnum.DISABLED_PREFERENCE}")
                     SimplePreferenceMacro(
@@ -113,23 +121,46 @@
                         icon = Icons.Outlined.DisabledByDefault,
                     )
                 }
+                .setStatusDataFn { EntryStatusData(isDisabled = true) }
                 .build()
         )
         entryList.add(
             createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE)
                 .setIsAllowSearch(true)
+                .setHasMutableStatus(true)
                 .setSearchDataFn {
                     EntrySearchData(title = ASYNC_PREFERENCE_TITLE)
                 }
+                .setStatusDataFn { EntryStatusData(isDisabled = false) }
                 .setUiLayoutFn {
                     val model = PreferencePageModel.create()
-                    val asyncSummary = remember { model.getAsyncSummary() }
                     Preference(
                         object : PreferenceModel {
                             override val title = ASYNC_PREFERENCE_TITLE
-                            override val summary = asyncSummary
+                            override val summary = model.asyncSummary
+                            override val enabled = model.asyncEnable
                         }
                     )
+                }
+                .setSliceDataFn { sliceUri, _ ->
+                    val createSliceImpl = { s: String ->
+                        createDemoBrowseSlice(
+                            sliceUri = sliceUri,
+                            title = ASYNC_PREFERENCE_TITLE,
+                            summary = s,
+                        )
+                    }
+                    return@setSliceDataFn object : EntrySliceData() {
+                        init {
+                            postValue(createSliceImpl("(loading)"))
+                        }
+
+                        override suspend fun asyncRunner() {
+                            spaLogger.message(TAG, "Async entry loading")
+                            delay(2000L)
+                            postValue(createSliceImpl(ASYNC_PREFERENCE_SUMMARY))
+                        }
+                    }
                 }.build()
         )
         entryList.add(
@@ -148,6 +179,27 @@
                             }
                         }
                     )
+                }.setSliceDataFn { sliceUri, args ->
+                    val createSliceImpl = { v: Int ->
+                        createDemoActionSlice(
+                            sliceUri = sliceUri,
+                            title = MANUAL_UPDATE_PREFERENCE_TITLE,
+                            summary = "manual update value $v",
+                        )
+                    }
+
+                    return@setSliceDataFn object : EntrySliceData() {
+                        private var tick = args?.getString("init")?.toInt() ?: 0
+
+                        init {
+                            postValue(createSliceImpl(tick))
+                        }
+
+                        override suspend fun asyncAction() {
+                            tick++
+                            postValue(createSliceImpl(tick))
+                        }
+                    }
                 }.build()
         )
         entryList.add(
@@ -166,7 +218,33 @@
                         }
                     )
                 }
-                .build()
+                .setSliceDataFn { sliceUri, args ->
+                    val createSliceImpl = { v: Int ->
+                        createDemoBrowseSlice(
+                            sliceUri = sliceUri,
+                            title = AUTO_UPDATE_PREFERENCE_TITLE,
+                            summary = "auto update value $v",
+                        )
+                    }
+
+                    return@setSliceDataFn object : EntrySliceData() {
+                        private var tick = args?.getString("init")?.toInt() ?: 0
+
+                        init {
+                            postValue(createSliceImpl(tick))
+                        }
+
+                        override suspend fun asyncRunner() {
+                            spaLogger.message(TAG, "autoUpdater.active")
+                            while (true) {
+                                delay(1000L)
+                                tick++
+                                spaLogger.message(TAG, "autoUpdater.value $tick")
+                                postValue(createSliceImpl(tick))
+                            }
+                        }
+                    }
+                }.build()
         )
 
         return entryList
@@ -197,21 +275,33 @@
                     clickRoute = SettingsPageProviderEnum.PREFERENCE.name
                 )
             }
+            .setSliceDataFn { sliceUri, _ ->
+                val intent = owner.createBrowseIntent()?.createBrowsePendingIntent()
+                    ?: return@setSliceDataFn null
+                return@setSliceDataFn object : EntrySliceData() {
+                    init {
+                        postValue(
+                            createDemoSlice(
+                                sliceUri = sliceUri,
+                                title = PAGE_TITLE,
+                                summary = "Injected Entry",
+                                intent = intent,
+                            )
+                        )
+                    }
+                }
+            }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = PAGE_TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return PAGE_TITLE
     }
 }
 
 @Preview(showBackground = true)
 @Composable
 private fun PreferencePagePreview() {
+    SpaEnvironmentFactory.resetForPreview()
     SettingsTheme {
         PreferencePageProvider.Page(null)
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt
index 1e64b2e..fc6f10f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageModel.kt
@@ -44,7 +44,7 @@
         const val DISABLE_PREFERENCE_TITLE = "Disabled"
         const val DISABLE_PREFERENCE_SUMMARY = "Disabled summary"
         const val ASYNC_PREFERENCE_TITLE = "Async Preference"
-        private const val ASYNC_PREFERENCE_SUMMARY = "Async summary"
+        const val ASYNC_PREFERENCE_SUMMARY = "Async summary"
         const val MANUAL_UPDATE_PREFERENCE_TITLE = "Manual Updater"
         const val AUTO_UPDATE_PREFERENCE_TITLE = "Auto Updater"
         val SIMPLE_PREFERENCE_KEYWORDS = listOf("simple keyword1", "simple keyword2")
@@ -59,7 +59,8 @@
 
     private val spaLogger = SpaEnvironmentFactory.instance.logger
 
-    private val asyncSummary = mutableStateOf(" ")
+    val asyncSummary = mutableStateOf("(loading)")
+    val asyncEnable = mutableStateOf(false)
 
     private val manualUpdater = mutableStateOf(0)
 
@@ -87,16 +88,13 @@
     override fun initialize(arguments: Bundle?) {
         spaLogger.message(TAG, "initialize with args " + arguments.toString())
         viewModelScope.launch(Dispatchers.IO) {
+            // Loading your data here.
             delay(2000L)
             asyncSummary.value = ASYNC_PREFERENCE_SUMMARY
+            asyncEnable.value = true
         }
     }
 
-    fun getAsyncSummary(): State<String> {
-        spaLogger.message(TAG, "getAsyncSummary")
-        return asyncSummary
-    }
-
     fun getManualUpdaterSummary(): State<String> {
         spaLogger.message(TAG, "getManualUpdaterSummary")
         return derivedStateOf { manualUpdater.value.toString() }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
index 46b44ca..367766a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
@@ -34,7 +34,6 @@
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import kotlinx.coroutines.delay
 
 private const val TITLE = "Sample SwitchPreference"
@@ -88,13 +87,8 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
index b991f59..22da99c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
@@ -34,7 +34,6 @@
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
 import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import kotlinx.coroutines.delay
 
 private const val TITLE = "Sample TwoTargetSwitchPreference"
@@ -88,13 +87,8 @@
             }
     }
 
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            for (entry in buildEntry(arguments)) {
-                entry.UiLayout()
-            }
-        }
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
     }
 }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
index a4713b9..d87cbe8 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CategoryPage.kt
@@ -48,41 +48,40 @@
             }
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        CategoryPage()
-    }
-}
-
-@Composable
-private fun CategoryPage() {
-    RegularScaffold(title = TITLE) {
-        CategoryTitle("Category A")
-        Preference(remember {
-            object : PreferenceModel {
-                override val title = "Preference 1"
-                override val summary = stateOf("Summary 1")
-            }
-        })
-        Preference(remember {
-            object : PreferenceModel {
-                override val title = "Preference 2"
-                override val summary = stateOf("Summary 2")
-            }
-        })
-        Category("Category B") {
+        RegularScaffold(title = getTitle(arguments)) {
+            CategoryTitle("Category A")
             Preference(remember {
                 object : PreferenceModel {
-                    override val title = "Preference 3"
-                    override val summary = stateOf("Summary 3")
+                    override val title = "Preference 1"
+                    override val summary = stateOf("Summary 1")
                 }
             })
             Preference(remember {
                 object : PreferenceModel {
-                    override val title = "Preference 4"
-                    override val summary = stateOf("Summary 4")
+                    override val title = "Preference 2"
+                    override val summary = stateOf("Summary 2")
                 }
             })
+            Category("Category B") {
+                Preference(remember {
+                    object : PreferenceModel {
+                        override val title = "Preference 3"
+                        override val summary = stateOf("Summary 3")
+                    }
+                })
+                Preference(remember {
+                    object : PreferenceModel {
+                        override val title = "Preference 4"
+                        override val summary = stateOf("Summary 4")
+                    }
+                })
+            }
         }
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
index 03b72d3..ec2f436 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt
@@ -49,9 +49,13 @@
             }
     }
 
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
     @Composable
     override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
+        RegularScaffold(title = getTitle(arguments)) {
             val selectedIndex = rememberSaveable { mutableStateOf(0) }
             Spinner(
                 options = (1..3).map { "Option $it" },
diff --git a/packages/SettingsLib/Spa/settings.gradle b/packages/SettingsLib/Spa/settings.gradle
index 897fa67..b627a70 100644
--- a/packages/SettingsLib/Spa/settings.gradle
+++ b/packages/SettingsLib/Spa/settings.gradle
@@ -26,9 +26,11 @@
     repositories {
         google()
         mavenCentral()
+        maven { url "https://jitpack.io"}
     }
 }
 rootProject.name = "SpaLib"
 include ':spa'
 include ':gallery'
+include ':testutils'
 include ':tests'
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 8b29366..eb7aaa7 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -24,6 +24,9 @@
     srcs: ["src/**/*.kt"],
 
     static_libs: [
+        "androidx.slice_slice-builders",
+        "androidx.slice_slice-core",
+        "androidx.slice_slice-view",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
         "androidx.compose.runtime_runtime",
@@ -33,10 +36,10 @@
         "androidx.navigation_navigation-compose",
         "com.google.android.material_material",
         "lottie_compose",
+        "MPAndroidChart",
     ],
     kotlincflags: [
         "-Xjvm-default=all",
-        "-opt-in=kotlin.RequiresOptIn",
     ],
     min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/spa/AndroidManifest.xml b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
index 410bcdb..62800bd 100644
--- a/packages/SettingsLib/Spa/spa/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
@@ -14,4 +14,7 @@
   limitations under the License.
   -->
 
-<manifest package="com.android.settingslib.spa" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa">
+    <uses-sdk android:minSdkVersion="21"/>
+</manifest>
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 7e05e75..8543596 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa'
-    compileSdk 33
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdk spa_min_sdk
-        targetSdk 33
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
     }
 
     sourceSets {
@@ -43,7 +44,7 @@
     }
     kotlinOptions {
         jvmTarget = '1.8'
-        freeCompilerArgs = ["-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn"]
+        freeCompilerArgs = ["-Xjvm-default=all"]
     }
     buildFeatures {
         compose true
@@ -51,22 +52,21 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
     api "androidx.appcompat:appcompat:1.7.0-alpha01"
-    api "androidx.compose.material3:material3:$jetpack_compose_material3_version"
+    api "androidx.slice:slice-builders:1.1.0-alpha02"
+    api "androidx.slice:slice-core:1.1.0-alpha02"
+    api "androidx.slice:slice-view:1.1.0-alpha02"
+    api "androidx.compose.material3:material3:1.1.0-alpha01"
     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.navigation:navigation-compose:2.5.0"
-    api "com.google.android.material:material:1.6.1"
+    api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha03"
+    api "androidx.navigation:navigation-compose:2.6.0-alpha03"
+    api "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
+    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 89daeb1..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
@@ -35,6 +36,7 @@
 import androidx.navigation.compose.rememberNavController
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.LocalNavController
@@ -44,7 +46,6 @@
 import com.android.settingslib.spa.framework.util.navRoute
 
 private const val TAG = "BrowseActivity"
-private const val NULL_PAGE_NAME = "NULL"
 
 /**
  * The Activity to render ALL SPA pages, and handles jumps between SPA pages.
@@ -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 {
@@ -81,36 +83,21 @@
     private fun MainContent() {
         val sppRepository by spaEnvironment.pageProviderRepository
         val navController = rememberNavController()
+        val nullPage = SettingsPage.createNull()
         CompositionLocalProvider(navController.localNavController()) {
-            NavHost(navController, NULL_PAGE_NAME) {
-                composable(NULL_PAGE_NAME) {}
+            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)
                     }
@@ -121,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/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
deleted file mode 100644
index 6f96818..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import androidx.navigation.NavType
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
-import com.android.settingslib.spa.R
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
-import com.android.settingslib.spa.framework.common.LogCategory
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-import com.android.settingslib.spa.framework.compose.localNavController
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.toState
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.HomeScaffold
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
-
-private const val TAG = "DebugActivity"
-private const val ROUTE_ROOT = "root"
-private const val ROUTE_All_PAGES = "pages"
-private const val ROUTE_All_ENTRIES = "entries"
-private const val ROUTE_PAGE = "page"
-private const val ROUTE_ENTRY = "entry"
-private const val PARAM_NAME_PAGE_ID = "pid"
-private const val PARAM_NAME_ENTRY_ID = "eid"
-
-/**
- * The Debug Activity to display all Spa Pages & Entries.
- * One can open the debug activity by:
- *   $ adb shell am start -n <Activity>
- * For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity
- * For SettingsGoogle, Activity = com.android.settings/.spa.SpaDebugActivity
- */
-open class DebugActivity : ComponentActivity() {
-    private val spaEnvironment get() = SpaEnvironmentFactory.instance
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        setTheme(R.style.Theme_SpaLib_DayNight)
-        super.onCreate(savedInstanceState)
-        spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
-
-        setContent {
-            SettingsTheme {
-                MainContent()
-            }
-        }
-    }
-
-    private fun displayDebugMessage() {
-        val entryProviderAuthorities = spaEnvironment.entryProviderAuthorities ?: return
-
-        try {
-            val query = EntryProvider.QueryEnum.PAGE_INFO_QUERY
-            contentResolver.query(
-                Uri.parse("content://$entryProviderAuthorities/${query.queryPath}"),
-                null, null, null
-            ).use { cursor ->
-                while (cursor != null && cursor.moveToNext()) {
-                    val route = cursor.getString(query, EntryProvider.ColumnEnum.PAGE_ROUTE)
-                    val entryCount = cursor.getInt(query, EntryProvider.ColumnEnum.PAGE_ENTRY_COUNT)
-                    val hasRuntimeParam =
-                        cursor.getBoolean(query, EntryProvider.ColumnEnum.HAS_RUNTIME_PARAM)
-                    val message = "Page Info: $route ($entryCount) " +
-                        (if (hasRuntimeParam) "with" else "no") + "-runtime-params"
-                    spaEnvironment.logger.message(TAG, message, category = LogCategory.FRAMEWORK)
-                }
-            }
-        } catch (e: Exception) {
-            Log.e(TAG, "Provider querying exception:", e)
-        }
-    }
-
-    @Composable
-    private fun MainContent() {
-        val navController = rememberNavController()
-        CompositionLocalProvider(navController.localNavController()) {
-            NavHost(navController, ROUTE_ROOT) {
-                composable(route = ROUTE_ROOT) { RootPage() }
-                composable(route = ROUTE_All_PAGES) { AllPages() }
-                composable(route = ROUTE_All_ENTRIES) { AllEntries() }
-                composable(
-                    route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
-                    arguments = listOf(
-                        navArgument(PARAM_NAME_PAGE_ID) { type = NavType.StringType },
-                    )
-                ) { navBackStackEntry -> OnePage(navBackStackEntry.arguments) }
-                composable(
-                    route = "$ROUTE_ENTRY/{$PARAM_NAME_ENTRY_ID}",
-                    arguments = listOf(
-                        navArgument(PARAM_NAME_ENTRY_ID) { type = NavType.StringType },
-                    )
-                ) { navBackStackEntry -> OneEntry(navBackStackEntry.arguments) }
-            }
-        }
-    }
-
-    @Composable
-    fun RootPage() {
-        val entryRepository by spaEnvironment.entryRepository
-        val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
-        val allEntry = remember { entryRepository.getAllEntries() }
-        HomeScaffold(title = "Settings Debug") {
-            Preference(object : PreferenceModel {
-                override val title = "List All Pages (${allPageWithEntry.size})"
-                override val onClick = navigator(route = ROUTE_All_PAGES)
-            })
-            Preference(object : PreferenceModel {
-                override val title = "List All Entries (${allEntry.size})"
-                override val onClick = navigator(route = ROUTE_All_ENTRIES)
-            })
-            Preference(object : PreferenceModel {
-                override val title = "Query EntryProvider"
-                override val enabled = isEntryProviderAvailable().toState()
-                override val onClick = { displayDebugMessage() }
-            })
-        }
-    }
-
-    @Composable
-    fun AllPages() {
-        val entryRepository by spaEnvironment.entryRepository
-        val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
-        RegularScaffold(title = "All Pages (${allPageWithEntry.size})") {
-            for (pageWithEntry in allPageWithEntry) {
-                Preference(object : PreferenceModel {
-                    override val title =
-                        "${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})"
-                    override val summary = pageWithEntry.page.formatArguments().toState()
-                    override val onClick =
-                        navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}")
-                })
-            }
-        }
-    }
-
-    @Composable
-    fun AllEntries() {
-        val entryRepository by spaEnvironment.entryRepository
-        val allEntry = remember { entryRepository.getAllEntries() }
-        RegularScaffold(title = "All Entries (${allEntry.size})") {
-            EntryList(allEntry)
-        }
-    }
-
-    @Composable
-    fun OnePage(arguments: Bundle?) {
-        val entryRepository by spaEnvironment.entryRepository
-        val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
-        val pageWithEntry = entryRepository.getPageWithEntry(id)!!
-        RegularScaffold(title = "Page - ${pageWithEntry.page.displayName}") {
-            Text(text = "id = ${pageWithEntry.page.id}")
-            Text(text = pageWithEntry.page.formatArguments())
-            Text(text = "Entry size: ${pageWithEntry.entries.size}")
-            Preference(model = object : PreferenceModel {
-                override val title = "open page"
-                override val enabled = isPageClickable(pageWithEntry.page).toState()
-                override val onClick = openPage(pageWithEntry.page)
-            })
-            EntryList(pageWithEntry.entries)
-        }
-    }
-
-    @Composable
-    fun OneEntry(arguments: Bundle?) {
-        val entryRepository by spaEnvironment.entryRepository
-        val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
-        val entry = entryRepository.getEntry(id)!!
-        val entryContent = remember { entry.formatContent() }
-        RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
-            Preference(model = object : PreferenceModel {
-                override val title = "open entry"
-                override val enabled = isEntryClickable(entry).toState()
-                override val onClick = openEntry(entry)
-            })
-            Text(text = entryContent)
-        }
-    }
-
-    @Composable
-    private fun EntryList(entries: Collection<SettingsEntry>) {
-        for (entry in entries) {
-            Preference(object : PreferenceModel {
-                override val title = entry.displayTitle()
-                override val summary =
-                    "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
-                override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}")
-            })
-        }
-    }
-
-    @Composable
-    private fun openPage(page: SettingsPage): (() -> Unit)? {
-        if (!isPageClickable(page)) return null
-        val context = LocalContext.current
-        val route = page.buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-        }
-        return {
-            spaEnvironment.logger.message(
-                TAG, "OpenPage: $route", category = LogCategory.FRAMEWORK
-            )
-            context.startActivity(intent)
-        }
-    }
-
-    @Composable
-    private fun openEntry(entry: SettingsEntry): (() -> Unit)? {
-        if (!isEntryClickable(entry)) return null
-        val context = LocalContext.current
-        val route = entry.containerPage().buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-            putExtra(KEY_HIGHLIGHT_ENTRY, entry.id)
-        }
-        return {
-            spaEnvironment.logger.message(
-                TAG, "OpenEntry: $route", category = LogCategory.FRAMEWORK
-            )
-            context.startActivity(intent)
-        }
-    }
-
-    private fun isEntryProviderAvailable(): Boolean {
-        return spaEnvironment.entryProviderAuthorities != null
-    }
-
-    private fun isPageClickable(page: SettingsPage): Boolean {
-        return spaEnvironment.browseActivityClass != null && !page.hasRuntimeParam()
-    }
-
-    private fun isEntryClickable(entry: SettingsEntry): Boolean {
-        return spaEnvironment.browseActivityClass != null &&
-            !entry.containerPage().hasRuntimeParam()
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
deleted file mode 100644
index 532f63b..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework
-
-import android.content.ComponentName
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
-import android.content.Intent.URI_INTENT_SCHEME
-import android.content.UriMatcher
-import android.content.pm.ProviderInfo
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.net.Uri
-import android.util.Log
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
-
-private const val TAG = "EntryProvider"
-
-/**
- * The content provider to return entry related data, which can be used for search and hierarchy.
- * One can query the provider result by:
- *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
- * For gallery, AuthorityPath = com.android.spa.gallery.provider
- * For SettingsGoogle, AuthorityPath = com.android.settings.spa.provider
- * Some examples:
- *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/page_info
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
- *   $ adb shell content query --uri content://<AuthorityPath>/search_sitemap
- *   $ adb shell content query --uri content://<AuthorityPath>/search_static
- *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
- */
-open class EntryProvider : ContentProvider() {
-    private val spaEnvironment get() = SpaEnvironmentFactory.instance
-
-    /**
-     * Enum to define all column names in provider.
-     */
-    enum class ColumnEnum(val id: String) {
-        // Columns related to page
-        PAGE_ID("pageId"),
-        PAGE_NAME("pageName"),
-        PAGE_ROUTE("pageRoute"),
-        PAGE_INTENT_URI("pageIntent"),
-        PAGE_ENTRY_COUNT("entryCount"),
-        HAS_RUNTIME_PARAM("hasRuntimeParam"),
-        PAGE_START_ADB("pageStartAdb"),
-
-        // Columns related to entry
-        ENTRY_ID("entryId"),
-        ENTRY_NAME("entryName"),
-        ENTRY_ROUTE("entryRoute"),
-        ENTRY_INTENT_URI("entryIntent"),
-        ENTRY_HIERARCHY_PATH("entryPath"),
-        ENTRY_START_ADB("entryStartAdb"),
-
-        // Columns related to search
-        ENTRY_TITLE("entryTitle"),
-        ENTRY_SEARCH_KEYWORD("entrySearchKw"),
-    }
-
-    /**
-     * Enum to define all queries supported in the provider.
-     */
-    enum class QueryEnum(
-        val queryPath: String,
-        val queryMatchCode: Int,
-        val columnNames: List<ColumnEnum>
-    ) {
-        // For debug
-        PAGE_DEBUG_QUERY(
-            "page_debug", 1,
-            listOf(ColumnEnum.PAGE_START_ADB)
-        ),
-        ENTRY_DEBUG_QUERY(
-            "entry_debug", 2,
-            listOf(ColumnEnum.ENTRY_START_ADB)
-        ),
-
-        // page related queries.
-        PAGE_INFO_QUERY(
-            "page_info", 100,
-            listOf(
-                ColumnEnum.PAGE_ID,
-                ColumnEnum.PAGE_NAME,
-                ColumnEnum.PAGE_ROUTE,
-                ColumnEnum.PAGE_INTENT_URI,
-                ColumnEnum.PAGE_ENTRY_COUNT,
-                ColumnEnum.HAS_RUNTIME_PARAM,
-            )
-        ),
-
-        // entry related queries
-        ENTRY_INFO_QUERY(
-            "entry_info", 200,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_NAME,
-                ColumnEnum.ENTRY_ROUTE,
-                ColumnEnum.ENTRY_INTENT_URI,
-            )
-        ),
-
-        // Search related queries
-        SEARCH_SITEMAP_QUERY(
-            "search_sitemap", 300,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_HIERARCHY_PATH,
-            )
-        ),
-        SEARCH_STATIC_DATA_QUERY(
-            "search_static", 301,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-        SEARCH_DYNAMIC_DATA_QUERY(
-            "search_dynamic", 302,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-    }
-
-    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
-    private fun addUri(authority: String, query: QueryEnum) {
-        uriMatcher.addURI(authority, query.queryPath, query.queryMatchCode)
-    }
-
-    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
-        TODO("Implement this to handle requests to delete one or more rows")
-    }
-
-    override fun getType(uri: Uri): String? {
-        TODO(
-            "Implement this to handle requests for the MIME type of the data" +
-                "at the given URI"
-        )
-    }
-
-    override fun insert(uri: Uri, values: ContentValues?): Uri? {
-        TODO("Implement this to handle requests to insert a new row.")
-    }
-
-    override fun update(
-        uri: Uri,
-        values: ContentValues?,
-        selection: String?,
-        selectionArgs: Array<String>?
-    ): Int {
-        TODO("Implement this to handle requests to update one or more rows.")
-    }
-
-    override fun onCreate(): Boolean {
-        Log.d(TAG, "onCreate")
-        return true
-    }
-
-    override fun attachInfo(context: Context?, info: ProviderInfo?) {
-        if (info != null) {
-            addUri(info.authority, QueryEnum.PAGE_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.PAGE_INFO_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_INFO_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_SITEMAP_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_STATIC_DATA_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_DYNAMIC_DATA_QUERY)
-        }
-        super.attachInfo(context, info)
-    }
-
-    override fun query(
-        uri: Uri,
-        projection: Array<String>?,
-        selection: String?,
-        selectionArgs: Array<String>?,
-        sortOrder: String?
-    ): Cursor? {
-        return try {
-            when (uriMatcher.match(uri)) {
-                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
-                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
-                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
-                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
-                QueryEnum.SEARCH_SITEMAP_QUERY.queryMatchCode -> querySearchSitemap()
-                QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
-                QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
-                else -> throw UnsupportedOperationException("Unknown Uri $uri")
-            }
-        } catch (e: UnsupportedOperationException) {
-            throw e
-        } catch (e: Exception) {
-            Log.e(TAG, "Provider querying exception:", e)
-            null
-        }
-    }
-
-    private fun queryPageDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val command = createBrowsePageAdbCommand(pageWithEntry.page)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryEntryDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            val command = createBrowsePageAdbCommand(entry.containerPage(), entry.id)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryPageInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val page = pageWithEntry.page
-            cursor.newRow()
-                .add(ColumnEnum.PAGE_ID.id, page.id)
-                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
-                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
-                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
-                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
-                .add(
-                    ColumnEnum.PAGE_INTENT_URI.id,
-                    createBrowsePageIntent(page).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
-    private fun queryEntryInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            cursor.newRow()
-                .add(ColumnEnum.ENTRY_ID.id, entry.id)
-                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
-                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
-                .add(
-                    ColumnEnum.ENTRY_INTENT_URI.id,
-                    createBrowsePageIntent(entry.containerPage(), entry.id).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
-    private fun querySearchSitemap(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch) continue
-            cursor.newRow()
-                .add(ColumnEnum.ENTRY_ID.id, entry.id)
-                .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id, entryRepository.getEntryPath(entry.id))
-        }
-        return cursor
-    }
-
-    private fun querySearchStaticData(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_DATA_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch || entry.isSearchDataDynamic) continue
-            fetchSearchData(entry, cursor)
-        }
-        return cursor
-    }
-
-    private fun querySearchDynamicData(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch || !entry.isSearchDataDynamic) continue
-            fetchSearchData(entry, cursor)
-        }
-        return cursor
-    }
-
-    private fun fetchSearchData(entry: SettingsEntry, cursor: MatrixCursor) {
-        // Fetch search data. We can add runtime arguments later if necessary
-        val searchData = entry.getSearchData()
-        cursor.newRow()
-            .add(ColumnEnum.ENTRY_ID.id, entry.id)
-            .add(ColumnEnum.ENTRY_TITLE.id, searchData?.title ?: "")
-            .add(
-                ColumnEnum.ENTRY_SEARCH_KEYWORD.id,
-                searchData?.keyword ?: emptyList<String>()
-            )
-    }
-
-    private fun createBrowsePageIntent(page: SettingsPage, entryId: String? = null): Intent {
-        if (!isPageBrowsable(page)) return Intent()
-        return Intent().setComponent(ComponentName(context!!, spaEnvironment.browseActivityClass!!))
-            .apply {
-                putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute())
-                if (entryId != null) {
-                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
-                }
-            }
-    }
-
-    private fun createBrowsePageAdbCommand(page: SettingsPage, entryId: String? = null): String? {
-        if (!isPageBrowsable(page)) return null
-        val packageName = context!!.packageName
-        val activityName = spaEnvironment.browseActivityClass!!.name.replace(packageName, "")
-        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
-        val highlightParam =
-            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
-        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
-    }
-
-    private fun isPageBrowsable(page: SettingsPage): Boolean {
-        return context != null &&
-            spaEnvironment.browseActivityClass != null &&
-            !page.hasRuntimeParam()
-    }
-}
-
-fun EntryProvider.QueryEnum.getColumns(): Array<String> {
-    return columnNames.map { it.id }.toTypedArray()
-}
-
-fun EntryProvider.QueryEnum.getIndex(name: EntryProvider.ColumnEnum): Int {
-    return columnNames.indexOf(name)
-}
-
-fun Cursor.getString(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): String {
-    return this.getString(query.getIndex(columnName))
-}
-
-fun Cursor.getInt(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): Int {
-    return this.getInt(query.getIndex(columnName))
-}
-
-fun Cursor.getBoolean(
-    query: EntryProvider.QueryEnum,
-    columnName: EntryProvider.ColumnEnum
-): Boolean {
-    return this.getInt(query.getIndex(columnName)) == 1
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
new file mode 100644
index 0000000..3689e4e
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSearchProvider.kt
@@ -0,0 +1,183 @@
+/*
+ * 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
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
+
+private const val TAG = "SpaSearchProvider"
+
+/**
+ * The content provider to return entry related data, which can be used for search and hierarchy.
+ * One can query the provider result by:
+ *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
+ * For gallery, AuthorityPath = com.android.spa.gallery.provider
+ * For Settings, AuthorityPath = com.android.settings.spa.provider
+ * Some examples:
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_static
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
+ */
+class SpaSearchProvider : ContentProvider() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+        TODO("Implement this to handle requests to delete one or more rows")
+    }
+
+    override fun getType(uri: Uri): String? {
+        TODO(
+            "Implement this to handle requests for the MIME type of the data" +
+                "at the given URI"
+        )
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        TODO("Implement this to handle requests to insert a new row.")
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int {
+        TODO("Implement this to handle requests to update one or more rows.")
+    }
+
+    override fun onCreate(): Boolean {
+        Log.d(TAG, "onCreate")
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        if (info != null) {
+            QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
+        }
+        super.attachInfo(context, info)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? {
+        return try {
+            when (uriMatcher.match(uri)) {
+                QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
+                QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
+                QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                    querySearchMutableStatusData()
+                QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                    querySearchImmutableStatusData()
+                else -> throw UnsupportedOperationException("Unknown Uri $uri")
+            }
+        } catch (e: UnsupportedOperationException) {
+            throw e
+        } catch (e: Exception) {
+            Log.e(TAG, "Provider querying exception:", e)
+            null
+        }
+    }
+
+    private fun querySearchImmutableStatusData(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            if (!entry.isAllowSearch || entry.hasMutableStatus) continue
+            fetchStatusData(entry, cursor)
+        }
+        return cursor
+    }
+
+    private fun querySearchMutableStatusData(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            if (!entry.isAllowSearch || !entry.hasMutableStatus) continue
+            fetchStatusData(entry, cursor)
+        }
+        return cursor
+    }
+
+    private fun querySearchStaticData(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.SEARCH_STATIC_DATA_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            if (!entry.isAllowSearch || entry.isSearchDataDynamic) continue
+            fetchSearchData(entry, cursor)
+        }
+        return cursor
+    }
+
+    private fun querySearchDynamicData(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            if (!entry.isAllowSearch || !entry.isSearchDataDynamic) continue
+            fetchSearchData(entry, cursor)
+        }
+        return cursor
+    }
+
+    private fun fetchSearchData(entry: SettingsEntry, cursor: MatrixCursor) {
+        val entryRepository by spaEnvironment.entryRepository
+        val browseActivityClass = spaEnvironment.browseActivityClass
+
+        // Fetch search data. We can add runtime arguments later if necessary
+        val searchData = entry.getSearchData() ?: return
+        val intent = entry.containerPage()
+            .createBrowseIntent(context, browseActivityClass, entry.id)
+            ?: Intent()
+        cursor.newRow()
+            .add(ColumnEnum.ENTRY_ID.id, entry.id)
+            .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
+            .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
+            .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
+            .add(ColumnEnum.SEARCH_PATH.id,
+                entryRepository.getEntryPathWithTitle(entry.id, searchData.title))
+    }
+
+    private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
+        // Fetch status data. We can add runtime arguments later if necessary
+        val statusData = entry.getStatusData() ?: return
+        cursor.newRow()
+            .add(ColumnEnum.ENTRY_ID.id, entry.id)
+            .add(ColumnEnum.SEARCH_STATUS_DISABLED.id, statusData.isDisabled)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
new file mode 100644
index 0000000..58131e6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.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.settingslib.spa.framework
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+class SpaSliceBroadcastReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context?, intent: Intent?) {
+        val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository
+        val sliceUri = intent?.data ?: return
+        val sliceData = sliceRepository.getActiveSliceData(sliceUri) ?: return
+        sliceData.doAction()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
new file mode 100644
index 0000000..faa04fd
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
@@ -0,0 +1,77 @@
+/*
+ * 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
+
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.Observer
+import androidx.slice.Slice
+import androidx.slice.SliceProvider
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+
+private const val TAG = "SpaSliceProvider"
+
+class SpaSliceProvider : SliceProvider(), Observer<Slice?> {
+    private fun getOrPutSliceData(sliceUri: Uri): EntrySliceData? {
+        if (!SpaEnvironmentFactory.isReady()) return null
+        val sliceRepository by SpaEnvironmentFactory.instance.sliceDataRepository
+        return sliceRepository.getOrBuildSliceData(sliceUri)
+    }
+
+    override fun onBindSlice(sliceUri: Uri): Slice? {
+        if (context == null) return null
+        Log.d(TAG, "onBindSlice: $sliceUri")
+        return getOrPutSliceData(sliceUri)?.value
+    }
+
+    override fun onSlicePinned(sliceUri: Uri) {
+        Log.d(TAG, "onSlicePinned: $sliceUri")
+        super.onSlicePinned(sliceUri)
+        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+        runBlocking {
+            withContext(Dispatchers.Main) {
+                sliceLiveData.observeForever(this@SpaSliceProvider)
+            }
+        }
+    }
+
+    override fun onSliceUnpinned(sliceUri: Uri) {
+        Log.d(TAG, "onSliceUnpinned: $sliceUri")
+        super.onSliceUnpinned(sliceUri)
+        val sliceLiveData = getOrPutSliceData(sliceUri) ?: return
+        runBlocking {
+            withContext(Dispatchers.Main) {
+                sliceLiveData.removeObserver(this@SpaSliceProvider)
+            }
+        }
+    }
+
+    override fun onChanged(slice: Slice?) {
+        val uri = slice?.uri ?: return
+        Log.d(TAG, "onChanged: $uri")
+        context?.contentResolver?.notifyChange(uri, null)
+    }
+
+    override fun onCreateSliceProvider(): Boolean {
+        Log.d(TAG, "onCreateSliceProvider")
+        return true
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
index 9ec0c01..b3571a1 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
@@ -26,4 +26,5 @@
     @Composable
     fun UiLayout() {}
     fun getSearchData(): EntrySearchData? = null
+    fun getStatusData(): EntryStatusData? = null
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
index 9b262af..9bc620f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
@@ -22,12 +22,4 @@
 data class EntrySearchData(
     val title: String = "",
     val keyword: List<String> = emptyList(),
-) {
-    fun format(): String {
-        val content = listOf(
-            "search_title = $title",
-            "search_keyword = $keyword",
-        )
-        return content.joinToString("\n")
-    }
-}
+)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
new file mode 100644
index 0000000..fc551a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import androidx.lifecycle.LiveData
+import androidx.slice.Slice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+open class EntrySliceData : LiveData<Slice?>() {
+    private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
+    private var asyncRunnerJob: Job? = null
+    private var asyncActionJob: Job? = null
+    private var isActive = false
+
+    open suspend fun asyncRunner() {}
+
+    open suspend fun asyncAction() {}
+
+    override fun onActive() {
+        asyncRunnerJob?.cancel()
+        asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
+        isActive = true
+    }
+
+    override fun onInactive() {
+        asyncRunnerJob?.cancel()
+        asyncRunnerJob = null
+        asyncActionJob?.cancel()
+        asyncActionJob = null
+        isActive = false
+    }
+
+    fun isActive(): Boolean {
+        return isActive
+    }
+
+    fun doAction() {
+        asyncActionJob?.cancel()
+        asyncActionJob = asyncRunnerScope.launch { asyncAction() }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt
new file mode 100644
index 0000000..3e9dd3b
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+/**
+ * Defines the status data of one Settings entry, which could be changed frequently.
+ */
+data class EntryStatusData(
+    val isDisabled: Boolean = false,
+    val isSwitchOff: Boolean = false,
+)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
new file mode 100644
index 0000000..121c07f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import android.content.UriMatcher
+
+/**
+ * Enum to define all column names in provider.
+ */
+enum class ColumnEnum(val id: String) {
+    // Columns related to page
+    PAGE_ID("pageId"),
+    PAGE_NAME("pageName"),
+    PAGE_ROUTE("pageRoute"),
+    PAGE_INTENT_URI("pageIntent"),
+    PAGE_ENTRY_COUNT("entryCount"),
+    HAS_RUNTIME_PARAM("hasRuntimeParam"),
+    PAGE_START_ADB("pageStartAdb"),
+
+    // Columns related to entry
+    ENTRY_ID("entryId"),
+    ENTRY_NAME("entryName"),
+    ENTRY_ROUTE("entryRoute"),
+    ENTRY_INTENT_URI("entryIntent"),
+    ENTRY_HIERARCHY_PATH("entryPath"),
+    ENTRY_START_ADB("entryStartAdb"),
+
+    // Columns related to search
+    SEARCH_TITLE("searchTitle"),
+    SEARCH_KEYWORD("searchKw"),
+    SEARCH_PATH("searchPath"),
+    SEARCH_STATUS_DISABLED("searchDisabled"),
+}
+
+/**
+ * Enum to define all queries supported in the provider.
+ */
+enum class QueryEnum(
+    val queryPath: String,
+    val queryMatchCode: Int,
+    val columnNames: List<ColumnEnum>
+) {
+    // For debug
+    PAGE_DEBUG_QUERY(
+        "page_debug", 1,
+        listOf(ColumnEnum.PAGE_START_ADB)
+    ),
+    ENTRY_DEBUG_QUERY(
+        "entry_debug", 2,
+        listOf(ColumnEnum.ENTRY_START_ADB)
+    ),
+
+    // page related queries.
+    PAGE_INFO_QUERY(
+        "page_info", 100,
+        listOf(
+            ColumnEnum.PAGE_ID,
+            ColumnEnum.PAGE_NAME,
+            ColumnEnum.PAGE_ROUTE,
+            ColumnEnum.PAGE_INTENT_URI,
+            ColumnEnum.PAGE_ENTRY_COUNT,
+            ColumnEnum.HAS_RUNTIME_PARAM,
+        )
+    ),
+
+    // entry related queries
+    ENTRY_INFO_QUERY(
+        "entry_info", 200,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_NAME,
+            ColumnEnum.ENTRY_ROUTE,
+            ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.ENTRY_HIERARCHY_PATH,
+        )
+    ),
+
+    SEARCH_STATIC_DATA_QUERY(
+        "search_static", 301,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
+        )
+    ),
+    SEARCH_DYNAMIC_DATA_QUERY(
+        "search_dynamic", 302,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
+        )
+    ),
+    SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
+        "search_immutable_status", 303,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_STATUS_DISABLED,
+        )
+    ),
+    SEARCH_MUTABLE_STATUS_DATA_QUERY(
+        "search_mutable_status", 304,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_STATUS_DISABLED,
+        )
+    ),
+}
+
+internal fun QueryEnum.getColumns(): Array<String> {
+    return columnNames.map { it.id }.toTypedArray()
+}
+
+internal fun QueryEnum.getIndex(name: ColumnEnum): Int {
+    return columnNames.indexOf(name)
+}
+
+internal fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) {
+    uriMatcher.addURI(authority, queryPath, queryMatchCode)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index a3aeda6..9ee7f9e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -16,16 +16,13 @@
 
 package com.android.settingslib.spa.framework.common
 
+import android.net.Uri
 import android.os.Bundle
-import android.widget.Toast
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ProvidedValue
 import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.platform.LocalContext
 import com.android.settingslib.spa.framework.compose.LocalNavController
 
 const val INJECT_ENTRY_NAME = "INJECT"
@@ -41,7 +38,12 @@
 }
 
 val LocalEntryDataProvider =
-    compositionLocalOf<EntryData> { object : EntryData{} }
+    compositionLocalOf<EntryData> { object : EntryData {} }
+
+typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
+typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
+typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
+typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
 
 /**
  * Defines data of a Settings entry.
@@ -69,8 +71,17 @@
      * ========================================
      */
     val isAllowSearch: Boolean = false,
+
+    // Indicate whether the search indexing data of entry is dynamic.
     val isSearchDataDynamic: Boolean = false,
 
+    // Indicate whether the status of entry is mutable.
+    // If so, for instance, we'll reindex its status for search.
+    val hasMutableStatus: Boolean = false,
+
+    // Indicate whether the entry has SliceProvider support.
+    val hasSliceSupport: Boolean = false,
+
     /**
      * ========================================
      * Defines entry APIs to get data here.
@@ -78,10 +89,22 @@
      */
 
     /**
-     * API to get Search related data for this entry.
-     * Returns null if this entry is not available for the search at the moment.
+     * API to get the status data of the entry, such as isDisabled / isSwitchOff.
+     * Returns null if this entry do NOT have any status.
      */
-    private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
+    private val statusDataImpl: StatusDataGetter = { null },
+
+    /**
+     * API to get Search indexing data for this entry, such as title / keyword.
+     * Returns null if this entry do NOT support search.
+     */
+    private val searchDataImpl: SearchDataGetter = { null },
+
+    /**
+     * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
+     * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
+     */
+    private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
 
     /**
      * API to Render UI of this entry directly. For now, we use it in the internal injection, to
@@ -89,23 +112,8 @@
      * injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
      * use each entries' UI rendering function in the page instead.
      */
-    private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
+    private val uiLayoutImpl: UiLayerRenderer = {},
 ) {
-    fun formatContent(): String {
-        val content = listOf(
-            "id = $id",
-            "owner = ${owner.formatDisplayTitle()}",
-            "linkFrom = ${fromPage?.formatDisplayTitle()}",
-            "linkTo = ${toPage?.formatDisplayTitle()}",
-            "${getSearchData()?.format()}",
-        )
-        return content.joinToString("\n")
-    }
-
-    fun displayTitle(): String {
-        return "${owner.displayName}:$displayName"
-    }
-
     fun containerPage(): SettingsPage {
         // The Container page of the entry, which is the from-page or
         // the owner-page if from-page is unset.
@@ -120,23 +128,20 @@
         return arguments
     }
 
+    fun getStatusData(runtimeArguments: Bundle? = null): EntryStatusData? {
+        return statusDataImpl(fullArgument(runtimeArguments))
+    }
+
     fun getSearchData(runtimeArguments: Bundle? = null): EntrySearchData? {
         return searchDataImpl(fullArgument(runtimeArguments))
     }
 
+    fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
+        return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
+    }
+
     @Composable
     fun UiLayout(runtimeArguments: Bundle? = null) {
-        val context = LocalContext.current
-        val controller = LocalNavController.current
-        val highlight = rememberSaveable {
-            mutableStateOf(controller.highlightEntryId == id)
-        }
-        if (highlight.value) {
-            highlight.value = false
-            // TODO: Add highlight entry logic
-            Toast.makeText(context, "entry $id highlighted", Toast.LENGTH_SHORT).show()
-        }
-
         CompositionLocalProvider(provideLocalEntryData()) {
             uiLayoutImpl(fullArgument(runtimeArguments))
         }
@@ -166,10 +171,14 @@
     // Attributes
     private var isAllowSearch: Boolean = false
     private var isSearchDataDynamic: Boolean = false
+    private var hasMutableStatus: Boolean = false
+    private var hasSliceSupport: Boolean = false
 
     // Functions
-    private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
-    private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
+    private var uiLayoutFn: UiLayerRenderer = { }
+    private var statusDataFn: StatusDataGetter = { null }
+    private var searchDataFn: SearchDataGetter = { null }
+    private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
 
     fun build(): SettingsEntry {
         return SettingsEntry(
@@ -185,9 +194,13 @@
             // attributes
             isAllowSearch = isAllowSearch,
             isSearchDataDynamic = isSearchDataDynamic,
+            hasMutableStatus = hasMutableStatus,
+            hasSliceSupport = hasSliceSupport,
 
             // functions
+            statusDataImpl = statusDataFn,
             searchDataImpl = searchDataFn,
+            sliceDataImpl = sliceDataFn,
             uiLayoutImpl = uiLayoutFn,
         )
     }
@@ -216,7 +229,13 @@
         return this
     }
 
+    fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
+        this.hasMutableStatus = hasMutableStatus
+        return this
+    }
+
     fun setMacro(fn: (arguments: Bundle?) -> EntryMacro): SettingsEntryBuilder {
+        setStatusDataFn { fn(it).getStatusData() }
         setSearchDataFn { fn(it).getSearchData() }
         setUiLayoutFn {
             val macro = remember { fn(it) }
@@ -225,12 +244,23 @@
         return this
     }
 
-    fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
+    fun setStatusDataFn(fn: StatusDataGetter): SettingsEntryBuilder {
+        this.statusDataFn = fn
+        return this
+    }
+
+    fun setSearchDataFn(fn: SearchDataGetter): SettingsEntryBuilder {
         this.searchDataFn = fn
         return this
     }
 
-    fun setUiLayoutFn(fn: @Composable (arguments: Bundle?) -> Unit): SettingsEntryBuilder {
+    fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
+        this.sliceDataFn = fn
+        this.hasSliceSupport = true
+        return this
+    }
+
+    fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
         this.uiLayoutFn = fn
         return this
     }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
index ea20233..14b1629 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
@@ -21,16 +21,23 @@
 
 private const val TAG = "EntryRepository"
 private const val MAX_ENTRY_SIZE = 5000
+private const val MAX_ENTRY_DEPTH = 10
 
 data class SettingsPageWithEntry(
     val page: SettingsPage,
     val entries: List<SettingsEntry>,
+    // The inject entry, which to-page is current page.
+    val injectEntry: SettingsEntry,
 )
 
+private fun SettingsPage.getTitle(sppRepository: SettingsPageProviderRepository): String {
+    return sppRepository.getProviderOrNull(sppName)!!.getTitle(arguments)
+}
+
 /**
  * The repository to maintain all Settings entries
  */
-class SettingsEntryRepository(sppRepository: SettingsPageProviderRepository) {
+class SettingsEntryRepository(private val sppRepository: SettingsPageProviderRepository) {
     // Map of entry unique Id to entry
     private val entryMap: Map<String, SettingsEntry>
 
@@ -42,9 +49,11 @@
         entryMap = mutableMapOf()
         pageWithEntryMap = mutableMapOf()
 
+        val nullPage = SettingsPage.createNull()
         val entryQueue = LinkedList<SettingsEntry>()
         for (page in sppRepository.getAllRootPages()) {
-            val rootEntry = SettingsEntryBuilder.createRoot(owner = page).build()
+            val rootEntry =
+                SettingsEntryBuilder.createRoot(owner = page).setLink(fromPage = nullPage).build()
             if (!entryMap.containsKey(rootEntry.id)) {
                 entryQueue.push(rootEntry)
                 entryMap.put(rootEntry.id, rootEntry)
@@ -57,7 +66,11 @@
             if (page == null || pageWithEntryMap.containsKey(page.id)) continue
             val spp = sppRepository.getProviderOrNull(page.sppName) ?: continue
             val newEntries = spp.buildEntry(page.arguments)
-            pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries)
+            pageWithEntryMap[page.id] = SettingsPageWithEntry(
+                page = page,
+                entries = newEntries,
+                injectEntry = entry
+            )
             for (newEntry in newEntries) {
                 if (!entryMap.containsKey(newEntry.id)) {
                     entryQueue.push(newEntry)
@@ -88,7 +101,30 @@
         return entryMap[entryId]
     }
 
-    fun getEntryPath(entryId: String): String {
-        return "TODO(path_of_$entryId)"
+    private fun getEntryPath(entryId: String): List<SettingsEntry> {
+        val entryPath = ArrayList<SettingsEntry>()
+        var currentEntry = entryMap[entryId]
+        while (currentEntry != null && entryPath.size < MAX_ENTRY_DEPTH) {
+            entryPath.add(currentEntry)
+            val currentPage = currentEntry.containerPage()
+            currentEntry = pageWithEntryMap[currentPage.id]?.injectEntry
+        }
+        return entryPath
+    }
+
+    fun getEntryPathWithDisplayName(entryId: String): List<String> {
+        val entryPath = getEntryPath(entryId)
+        return entryPath.map { it.displayName }
+    }
+
+    fun getEntryPathWithTitle(entryId: String, defaultTitle: String): List<String> {
+        val entryPath = getEntryPath(entryId)
+        return entryPath.map {
+            if (it.toPage == null)
+                defaultTitle
+            else {
+                it.toPage.getTitle(sppRepository)
+            }
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 8f63c47..a372bbd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -16,12 +16,19 @@
 
 package com.android.settingslib.spa.framework.common
 
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
 import android.os.Bundle
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.BrowseActivity
 import com.android.settingslib.spa.framework.util.isRuntimeParam
 import com.android.settingslib.spa.framework.util.navLink
 import com.android.settingslib.spa.framework.util.normalize
 
+private const val NULL_PAGE_NAME = "NULL"
+
 /**
  * Defines data to identify a Settings page.
  */
@@ -42,6 +49,10 @@
     val arguments: Bundle? = null,
 ) {
     companion object {
+        fun createNull(): SettingsPage {
+            return create(NULL_PAGE_NAME)
+        }
+
         fun create(
             name: String,
             displayName: String? = null,
@@ -73,16 +84,6 @@
         return sppName == SppName
     }
 
-    fun formatArguments(): String {
-        val normArguments = parameter.normalize(arguments)
-        if (normArguments == null || normArguments.isEmpty) return "[No arguments]"
-        return normArguments.toString().removeRange(0, 6)
-    }
-
-    fun formatDisplayTitle(): String {
-        return "$displayName ${formatArguments()}"
-    }
-
     fun buildRoute(): String {
         return sppName + parameter.navLink(arguments)
     }
@@ -99,7 +100,7 @@
             id,
             LogEvent.PAGE_ENTER,
             category = LogCategory.FRAMEWORK,
-            details = formatDisplayTitle()
+            details = displayName,
         )
     }
 
@@ -108,9 +109,51 @@
             id,
             LogEvent.PAGE_LEAVE,
             category = LogCategory.FRAMEWORK,
-            details = formatDisplayTitle()
+            details = displayName,
         )
     }
+
+    fun createBrowseIntent(entryId: String? = null): Intent? {
+        val context = SpaEnvironmentFactory.instance.appContext
+        val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass
+        return createBrowseIntent(context, browseActivityClass, entryId)
+    }
+
+    fun createBrowseIntent(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): Intent? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        return Intent().setComponent(ComponentName(context!!, browseActivityClass!!))
+            .apply {
+                putExtra(BrowseActivity.KEY_DESTINATION, buildRoute())
+                if (entryId != null) {
+                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
+                }
+            }
+    }
+
+    fun createBrowseAdbCommand(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): String? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        val packageName = context!!.packageName
+        val activityName = browseActivityClass!!.name.replace(packageName, "")
+        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${buildRoute()}"
+        val highlightParam =
+            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
+        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
+    }
+
+    fun isBrowsable(context: Context?, browseActivityClass: Class<out Activity>?): Boolean {
+        return context != null &&
+            browseActivityClass != null &&
+            !isCreateBy(NULL_PAGE_NAME) &&
+            !hasRuntimeParam()
+    }
 }
 
 fun String.toHashId(): String {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index e8a4411..60599d4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 /**
  * An SettingsPageProvider which is used to create Settings page instances.
@@ -29,18 +30,26 @@
     val name: String
 
     /** The display name of this page provider, for better readability. */
-    val displayName: String?
-        get() = null
+    val displayName: String
+        get() = name
 
     /** The page parameters, default is no parameters. */
     val parameter: List<NamedNavArgument>
         get() = emptyList()
 
-    /** The [Composable] used to render this page. */
-    @Composable
-    fun Page(arguments: Bundle?)
+    fun getTitle(arguments: Bundle?): String = displayName
 
     fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
+
+    /** The [Composable] used to render this page. */
+    @Composable
+    fun Page(arguments: Bundle?) {
+        RegularScaffold(title = getTitle(arguments)) {
+            for (entry in buildEntry(arguments)) {
+                entry.UiLayout()
+            }
+        }
+    }
 }
 
 fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 5baee4f..945add4 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -17,7 +17,12 @@
 package com.android.settingslib.spa.framework.common
 
 import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
 import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.slice.SettingsSliceDataRepository
 
 private const val TAG = "SpaEnvironment"
 
@@ -29,6 +34,24 @@
         Log.d(TAG, "reset")
     }
 
+    @Composable
+    fun resetForPreview() {
+        val context = LocalContext.current
+        spaEnvironment = object : SpaEnvironment(context) {
+            override val pageProviderRepository = lazy {
+                SettingsPageProviderRepository(
+                    allPageProviders = emptyList(),
+                    rootPages = emptyList()
+                )
+            }
+        }
+        Log.d(TAG, "resetForPreview")
+    }
+
+    fun isReady(): Boolean {
+        return spaEnvironment != null
+    }
+
     val instance: SpaEnvironment
         get() {
             if (spaEnvironment == null)
@@ -37,15 +60,20 @@
         }
 }
 
-abstract class SpaEnvironment {
+abstract class SpaEnvironment(context: Context) {
     abstract val pageProviderRepository: Lazy<SettingsPageProviderRepository>
 
     val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) }
 
+    val sliceDataRepository = lazy { SettingsSliceDataRepository(entryRepository.value) }
+
+    // In Robolectric test, applicationContext is not available. Use context as fallback.
+    val appContext: Context = context.applicationContext ?: context
+
     open val browseActivityClass: Class<out Activity>? = null
-
-    open val entryProviderAuthorities: String? = null
-
+    open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
+    open val searchProviderAuthorities: String? = null
+    open val sliceProviderAuthorities: String? = null
     open val logger: SpaLogger = object : SpaLogger {}
 
     // TODO: add other environment setup here.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt
new file mode 100644
index 0000000..dbf8836
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import android.annotation.SuppressLint
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * *************************************************************************************************
+ * This file was forked from AndroidX:
+ * lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt
+ * TODO: Replace with AndroidX when it's usable.
+ */
+
+/**
+ * Collects values from this [StateFlow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * The [StateFlow.value] is used as an initial value. Every time there would be new value posted
+ * into the [StateFlow] the returned [State] will be updated causing recomposition of every
+ * [State.value] usage whenever the [lifecycleOwner]'s lifecycle is at least [minActiveState].
+ *
+ * This [StateFlow] is collected every time the [lifecycleOwner]'s lifecycle reaches the
+ * [minActiveState] Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle
+ * falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this`
+ * flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@SuppressLint("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateWithLifecycle(
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = this.value,
+    lifecycle = lifecycleOwner.lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [StateFlow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * The [StateFlow.value] is used as an initial value. Every time there would be new value posted
+ * into the [StateFlow] the returned [State] will be updated causing recomposition of every
+ * [State.value] usage whenever the [lifecycle] is at least [minActiveState].
+ *
+ * This [StateFlow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle
+ * state. The collection stops when [lifecycle] falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param lifecycle [Lifecycle] used to restart collecting `this` flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@SuppressLint("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateWithLifecycle(
+    lifecycle: Lifecycle,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = this.value,
+    lifecycle = lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [Flow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * Every time there would be new value posted into the [Flow] the returned [State] will be updated
+ * causing recomposition of every [State.value] usage whenever the [lifecycleOwner]'s lifecycle is
+ * at least [minActiveState].
+ *
+ * This [Flow] is collected every time the [lifecycleOwner]'s lifecycle reaches the [minActiveState]
+ * Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle falls below
+ * [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param initialValue The initial value given to the returned [State.value].
+ * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this`
+ * flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@Composable
+fun <T> Flow<T>.collectAsStateWithLifecycle(
+    initialValue: T,
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = initialValue,
+    lifecycle = lifecycleOwner.lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [Flow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * Every time there would be new value posted into the [Flow] the returned [State] will be updated
+ * causing recomposition of every [State.value] usage whenever the [lifecycle] is at
+ * least [minActiveState].
+ *
+ * This [Flow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle
+ * state. The collection stops when [lifecycle] falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param initialValue The initial value given to the returned [State.value].
+ * @param lifecycle [Lifecycle] used to restart collecting `this` flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@Composable
+fun <T> Flow<T>.collectAsStateWithLifecycle(
+    initialValue: T,
+    lifecycle: Lifecycle,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> {
+    return produceState(initialValue, this, lifecycle, minActiveState, context) {
+        lifecycle.repeatOnLifecycle(minActiveState) {
+            if (context == EmptyCoroutineContext) {
+                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
+            } else withContext(context) {
+                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt
new file mode 100644
index 0000000..8d0313f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.KeyboardActionScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+
+/**
+ * An action when run, hides the keyboard if it's open.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun hideKeyboardAction(): KeyboardActionScope.() -> Unit {
+    val keyboardController = LocalSoftwareKeyboardController.current
+    return { keyboardController?.hide() }
+}
+
+/**
+ * Creates a [LazyListState] that is remembered across compositions.
+ *
+ * And when user scrolling the lazy list, hides the keyboard if it's open.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun rememberLazyListStateAndHideKeyboardWhenStartScroll(): LazyListState {
+    val listState = rememberLazyListState()
+    val keyboardController = LocalSoftwareKeyboardController.current
+    LaunchedEffect(listState) {
+        snapshotFlow { listState.isScrollInProgress }
+            .distinctUntilChanged()
+            .filter { it }
+            .collect { keyboardController?.hide() }
+    }
+    return listState
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt
new file mode 100644
index 0000000..1b33dd6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.receiveAsFlow
+
+/**
+ * A flow which result is overridable.
+ */
+class OverridableFlow<T>(flow: Flow<T>) {
+    private val overrideChannel = Channel<T>()
+
+    val flow = merge(overrideChannel.receiveAsFlow(), flow)
+
+    fun override(value: T) {
+        overrideChannel.trySend(value)
+    }
+}
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/compose/Pager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
index bf33857..4df7794 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
@@ -214,6 +213,7 @@
             horizontalAlignment = horizontalAlignment,
             reverseLayout = reverseLayout,
             contentPadding = contentPadding,
+            userScrollEnabled = false,
             modifier = modifier,
         ) {
             items(
@@ -241,6 +241,7 @@
             horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
             reverseLayout = reverseLayout,
             contentPadding = contentPadding,
+            userScrollEnabled = false,
             modifier = modifier,
         ) {
             items(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/TimeMeasurer.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/TimeMeasurer.kt
new file mode 100644
index 0000000..b23f4e0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/TimeMeasurer.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTime::class)
+
+package com.android.settingslib.spa.framework.compose
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import kotlin.time.ExperimentalTime
+import kotlin.time.TimeSource
+
+const val ENABLE_MEASURE_TIME = false
+
+interface TimeMeasurer {
+    fun log(msg: String) {}
+    fun logFirst(msg: String) {}
+
+    companion object {
+        private object EmptyTimeMeasurer : TimeMeasurer
+
+        @Composable
+        fun rememberTimeMeasurer(tag: String): TimeMeasurer = remember {
+            if (ENABLE_MEASURE_TIME) TimeMeasurerImpl(tag) else EmptyTimeMeasurer
+        }
+    }
+}
+
+private class TimeMeasurerImpl(private val tag: String) : TimeMeasurer {
+    private val mark = TimeSource.Monotonic.markNow()
+    private val msgLogged = mutableSetOf<String>()
+
+    override fun log(msg: String) {
+        Log.d(tag, "Timer $msg: ${mark.elapsedNow()}")
+    }
+
+    override fun logFirst(msg: String) {
+        if (msgLogged.add(msg)) {
+            Log.d(tag, "Timer $msg: ${mark.elapsedNow()}")
+        }
+    }
+}
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
new file mode 100644
index 0000000..760064a
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.debug
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import com.android.settingslib.spa.R
+import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.compose.localNavController
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.slice.appendSliceParams
+import com.android.settingslib.spa.slice.presenter.SliceDemo
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.HomeScaffold
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val TAG = "DebugActivity"
+private const val ROUTE_ROOT = "root"
+private const val ROUTE_All_PAGES = "pages"
+private const val ROUTE_All_ENTRIES = "entries"
+private const val ROUTE_All_SLICES = "slices"
+private const val ROUTE_PAGE = "page"
+private const val ROUTE_ENTRY = "entry"
+private const val PARAM_NAME_PAGE_ID = "pid"
+private const val PARAM_NAME_ENTRY_ID = "eid"
+
+/**
+ * The Debug Activity to display all Spa Pages & Entries.
+ * One can open the debug activity by:
+ *   $ adb shell am start -n <Package>/com.android.settingslib.spa.framework.debug.DebugActivity
+ * For gallery, Package = com.android.settingslib.spa.gallery
+ */
+class DebugActivity : ComponentActivity() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        setTheme(R.style.Theme_SpaLib)
+        super.onCreate(savedInstanceState)
+        spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
+
+        setContent {
+            SettingsTheme {
+                MainContent()
+            }
+        }
+    }
+
+    @Composable
+    private fun MainContent() {
+        val navController = rememberNavController()
+        CompositionLocalProvider(navController.localNavController()) {
+            NavHost(navController, ROUTE_ROOT) {
+                composable(route = ROUTE_ROOT) { RootPage() }
+                composable(route = ROUTE_All_PAGES) { AllPages() }
+                composable(route = ROUTE_All_ENTRIES) { AllEntries() }
+                composable(route = ROUTE_All_SLICES) { AllSlices() }
+                composable(
+                    route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
+                    arguments = listOf(
+                        navArgument(PARAM_NAME_PAGE_ID) { type = NavType.StringType },
+                    )
+                ) { navBackStackEntry -> OnePage(navBackStackEntry.arguments) }
+                composable(
+                    route = "$ROUTE_ENTRY/{$PARAM_NAME_ENTRY_ID}",
+                    arguments = listOf(
+                        navArgument(PARAM_NAME_ENTRY_ID) { type = NavType.StringType },
+                    )
+                ) { navBackStackEntry -> OneEntry(navBackStackEntry.arguments) }
+            }
+        }
+    }
+
+    @Composable
+    fun RootPage() {
+        val entryRepository by spaEnvironment.entryRepository
+        val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
+        val allEntry = remember { entryRepository.getAllEntries() }
+        val allSliceEntry =
+            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
+        HomeScaffold(title = "Settings Debug") {
+            Preference(object : PreferenceModel {
+                override val title = "List All Pages (${allPageWithEntry.size})"
+                override val onClick = navigator(route = ROUTE_All_PAGES)
+            })
+            Preference(object : PreferenceModel {
+                override val title = "List All Entries (${allEntry.size})"
+                override val onClick = navigator(route = ROUTE_All_ENTRIES)
+            })
+            Preference(object : PreferenceModel {
+                override val title = "List All Slices (${allSliceEntry.size})"
+                override val onClick = navigator(route = ROUTE_All_SLICES)
+            })
+        }
+    }
+
+    @Composable
+    fun AllPages() {
+        val entryRepository by spaEnvironment.entryRepository
+        val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
+        RegularScaffold(title = "All Pages (${allPageWithEntry.size})") {
+            for (pageWithEntry in allPageWithEntry) {
+                val page = pageWithEntry.page
+                Preference(object : PreferenceModel {
+                    override val title = "${page.debugBrief()} (${pageWithEntry.entries.size})"
+                    override val summary = page.debugArguments().toState()
+                    override val onClick = navigator(route = ROUTE_PAGE + "/${page.id}")
+                })
+            }
+        }
+    }
+
+    @Composable
+    fun AllEntries() {
+        val entryRepository by spaEnvironment.entryRepository
+        val allEntry = remember { entryRepository.getAllEntries() }
+        RegularScaffold(title = "All Entries (${allEntry.size})") {
+            EntryList(allEntry)
+        }
+    }
+
+    @Composable
+    fun AllSlices() {
+        val entryRepository by spaEnvironment.entryRepository
+        val authority = spaEnvironment.sliceProviderAuthorities
+        val allSliceEntry =
+            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
+        RegularScaffold(title = "All Slices (${allSliceEntry.size})") {
+            for (entry in allSliceEntry) {
+                SliceDemo(sliceUri = entry.createSliceUri(authority))
+            }
+        }
+    }
+
+    @Composable
+    fun OnePage(arguments: Bundle?) {
+        val context = LocalContext.current
+        val entryRepository by spaEnvironment.entryRepository
+        val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
+        val pageWithEntry = entryRepository.getPageWithEntry(id)!!
+        val page = pageWithEntry.page
+        RegularScaffold(title = "Page - ${page.debugBrief()}") {
+            Text(text = "id = ${page.id}")
+            Text(text = page.debugArguments())
+            Text(text = "Entry size: ${pageWithEntry.entries.size}")
+            Preference(model = object : PreferenceModel {
+                override val title = "open page"
+                override val enabled =
+                    page.isBrowsable(context, spaEnvironment.browseActivityClass).toState()
+                override val onClick = openPage(page)
+            })
+            EntryList(pageWithEntry.entries)
+        }
+    }
+
+    @Composable
+    fun OneEntry(arguments: Bundle?) {
+        val context = LocalContext.current
+        val entryRepository by spaEnvironment.entryRepository
+        val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
+        val entry = entryRepository.getEntry(id)!!
+        val entryContent = remember { entry.debugContent(entryRepository) }
+        RegularScaffold(title = "Entry - ${entry.debugBrief()}") {
+            Preference(model = object : PreferenceModel {
+                override val title = "open entry"
+                override val enabled =
+                    entry.containerPage().isBrowsable(context, spaEnvironment.browseActivityClass)
+                        .toState()
+                override val onClick = openEntry(entry)
+            })
+            Text(text = entryContent)
+        }
+    }
+
+    @Composable
+    private fun EntryList(entries: Collection<SettingsEntry>) {
+        for (entry in entries) {
+            Preference(object : PreferenceModel {
+                override val title = entry.debugBrief()
+                override val summary =
+                    "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
+                override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}")
+            })
+        }
+    }
+
+    @Composable
+    private fun openPage(page: SettingsPage): (() -> Unit)? {
+        val context = LocalContext.current
+        val intent =
+            page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: return null
+        val route = page.buildRoute()
+        return {
+            spaEnvironment.logger.message(
+                TAG, "OpenPage: $route", category = LogCategory.FRAMEWORK
+            )
+            context.startActivity(intent)
+        }
+    }
+
+    @Composable
+    private fun openEntry(entry: SettingsEntry): (() -> Unit)? {
+        val context = LocalContext.current
+        val intent = entry.containerPage()
+            .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+            ?: return null
+        val route = entry.containerPage().buildRoute()
+        return {
+            spaEnvironment.logger.message(
+                TAG, "OpenEntry: $route", category = LogCategory.FRAMEWORK
+            )
+            context.startActivity(intent)
+        }
+    }
+}
+
+private fun SettingsEntry.createSliceUri(
+    authority: String?,
+    runtimeArguments: Bundle? = null
+): Uri {
+    if (authority == null) return Uri.EMPTY
+    return Uri.Builder().scheme("content").authority(authority).appendSliceParams(
+        route = this.containerPage().buildRoute(),
+        entryId = this.id,
+        runtimeArguments = runtimeArguments,
+    ).build()
+}
+
+/**
+ * A blank activity without any page.
+ */
+class BlankActivity : ComponentActivity()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt
new file mode 100644
index 0000000..538d2b5
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt
@@ -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.settingslib.spa.framework.debug
+
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntryStatusData
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryRepository
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.util.normalize
+
+private fun EntrySearchData.debugContent(): String {
+    val content = listOf(
+        "search_title = $title",
+        "search_keyword = $keyword",
+    )
+    return content.joinToString("\n")
+}
+
+private fun EntryStatusData.debugContent(): String {
+    val content = listOf(
+        "is_disabled = $isDisabled",
+        "is_switch_off = $isSwitchOff",
+    )
+    return content.joinToString("\n")
+}
+
+fun SettingsPage.debugArguments(): String {
+    val normArguments = parameter.normalize(arguments)
+    if (normArguments == null || normArguments.isEmpty) return "[No arguments]"
+    return normArguments.toString().removeRange(0, 6)
+}
+
+fun SettingsPage.debugBrief(): String {
+    return displayName
+}
+
+fun SettingsEntry.debugBrief(): String {
+    return "${owner.displayName}:$displayName"
+}
+
+fun SettingsEntry.debugContent(entryRepository: SettingsEntryRepository): String {
+    val searchData = getSearchData()
+    val statusData = getStatusData()
+    val entryPathWithName = entryRepository.getEntryPathWithDisplayName(id)
+    val entryPathWithTitle = entryRepository.getEntryPathWithTitle(id,
+        searchData?.title ?: displayName)
+    val content = listOf(
+        "------ STATIC ------",
+        "id = $id",
+        "owner = ${owner.debugBrief()} ${owner.debugArguments()}",
+        "linkFrom = ${fromPage?.debugBrief()} ${fromPage?.debugArguments()}",
+        "linkTo = ${toPage?.debugBrief()} ${toPage?.debugArguments()}",
+        "hierarchy_path = $entryPathWithName",
+        "------ SEARCH ------",
+        "search_path = $entryPathWithTitle",
+        searchData?.debugContent() ?: "no search data",
+        statusData?.debugContent() ?: "no status data",
+    )
+    return content.joinToString("\n")
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
new file mode 100644
index 0000000..399278d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.debug
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.URI_INTENT_SCHEME
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
+
+private const val TAG = "DebugProvider"
+
+/**
+ * The content provider to return debug data.
+ * One can query the provider result by:
+ *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
+ * For gallery, AuthorityPath = com.android.spa.gallery.debug
+ * Some examples:
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_info
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
+ */
+class DebugProvider : ContentProvider() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+        TODO("Implement this to handle requests to delete one or more rows")
+    }
+
+    override fun getType(uri: Uri): String? {
+        TODO(
+            "Implement this to handle requests for the MIME type of the data" +
+                "at the given URI"
+        )
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        TODO("Implement this to handle requests to insert a new row.")
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int {
+        TODO("Implement this to handle requests to update one or more rows.")
+    }
+
+    override fun onCreate(): Boolean {
+        Log.d(TAG, "onCreate")
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        if (info != null) {
+            QueryEnum.PAGE_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.PAGE_INFO_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_INFO_QUERY.addUri(uriMatcher, info.authority)
+        }
+        super.attachInfo(context, info)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? {
+        return try {
+            when (uriMatcher.match(uri)) {
+                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
+                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
+                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
+                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
+                else -> throw UnsupportedOperationException("Unknown Uri $uri")
+            }
+        } catch (e: UnsupportedOperationException) {
+            throw e
+        } catch (e: Exception) {
+            Log.e(TAG, "Provider querying exception:", e)
+            null
+        }
+    }
+
+    private fun queryPageDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val command = pageWithEntry.page.createBrowseAdbCommand(
+                context,
+                spaEnvironment.browseActivityClass
+            )
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryEntryDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val command = entry.containerPage()
+                .createBrowseAdbCommand(context, spaEnvironment.browseActivityClass, entry.id)
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryPageInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val page = pageWithEntry.page
+            val intent =
+                page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.PAGE_ID.id, page.id)
+                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
+                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
+                .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
+                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+        }
+        return cursor
+    }
+
+    private fun queryEntryInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val intent = entry.containerPage()
+                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+                ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.ENTRY_ID.id, entry.id)
+                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
+                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
+                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+                .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id,
+                    entryRepository.getEntryPathWithDisplayName(entry.id))
+        }
+        return cursor
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 3fa8c65..52c4893 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -44,3 +44,6 @@
 
 val ColorScheme.divider: Color
     get() = onSurface.copy(SettingsOpacity.Divider)
+
+val ColorScheme.surfaceTone: Color
+    get() = primary.copy(SettingsOpacity.SurfaceTone)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
new file mode 100644
index 0000000..9479228
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalTextApi::class)
+
+package com.android.settingslib.spa.framework.theme
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.DeviceFontFamilyName
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import com.android.settingslib.spa.framework.compose.rememberContext
+
+internal data class SettingsFontFamily(
+    val brand: FontFamily,
+    val plain: FontFamily,
+)
+
+private fun Context.getSettingsFontFamily(): SettingsFontFamily {
+    return SettingsFontFamily(
+        brand = getFontFamily(
+            configFontFamilyNormal = "config_headlineFontFamily",
+            configFontFamilyMedium = "config_headlineFontFamilyMedium",
+        ),
+        plain = getFontFamily(
+            configFontFamilyNormal = "config_bodyFontFamily",
+            configFontFamilyMedium = "config_bodyFontFamilyMedium",
+        ),
+    )
+}
+
+private fun Context.getFontFamily(
+    configFontFamilyNormal: String,
+    configFontFamilyMedium: String,
+): FontFamily {
+    val fontFamilyNormal = getAndroidConfig(configFontFamilyNormal)
+    val fontFamilyMedium = getAndroidConfig(configFontFamilyMedium)
+    if (fontFamilyNormal.isEmpty() || fontFamilyMedium.isEmpty()) return FontFamily.Default
+    return FontFamily(
+        Font(DeviceFontFamilyName(fontFamilyNormal), FontWeight.Normal),
+        Font(DeviceFontFamilyName(fontFamilyMedium), FontWeight.Medium),
+    )
+}
+
+private fun Context.getAndroidConfig(configName: String): String {
+    @SuppressLint("DiscouragedApi")
+    val configId = resources.getIdentifier(configName, "string", "android")
+    return resources.getString(configId)
+}
+
+@Composable
+internal fun rememberSettingsFontFamily(): SettingsFontFamily {
+    return rememberContext(Context::getSettingsFontFamily)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
index 11af6ce..c8faef6 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
@@ -20,4 +20,6 @@
     const val Full = 1f
     const val Disabled = 0.38f
     const val Divider = 0.2f
+    const val SurfaceTone = 0.14f
+    const val Hint = 0.9f
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 07f09ba..03699bf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -20,14 +20,13 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.em
 import androidx.compose.ui.unit.sp
 
-private class SettingsTypography {
-    private val brand = FontFamily.Default
-    private val plain = FontFamily.Default
+private class SettingsTypography(settingsFontFamily: SettingsFontFamily) {
+    private val brand = settingsFontFamily.brand
+    private val plain = settingsFontFamily.plain
 
     val typography = Typography(
         displayLarge = TextStyle(
@@ -140,5 +139,6 @@
 
 @Composable
 internal fun rememberSettingsTypography(): Typography {
-    return remember { SettingsTypography().typography }
+    val settingsFontFamily = rememberSettingsFontFamily()
+    return remember { SettingsTypography(settingsFontFamily).typography }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
index 6c7432e..8d0a35c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
@@ -23,7 +23,7 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 
 @Composable
-fun LogEntryEvent(): (event: LogEvent) -> Unit {
+fun logEntryEvent(): (event: LogEvent) -> Unit {
     val entryId = LocalEntryDataProvider.current.entryId ?: return {}
     return {
         SpaEnvironmentFactory.instance.logger.event(entryId, it, category = LogCategory.VIEW)
@@ -31,9 +31,9 @@
 }
 
 @Composable
-fun WrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
+fun wrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
     if (onClick == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         logEvent(LogEvent.ENTRY_CLICK)
         onClick()
@@ -41,9 +41,9 @@
 }
 
 @Composable
-fun WrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
+fun wrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
     if (onSwitch == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         val event = if (it) LogEvent.ENTRY_SWITCH_ON else LogEvent.ENTRY_SWITCH_OFF
         logEvent(event)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
new file mode 100644
index 0000000..14855a8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SettingsSliceDataRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.slice
+
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.EntrySliceData
+import com.android.settingslib.spa.framework.common.SettingsEntryRepository
+
+private const val TAG = "SliceDataRepository"
+
+class SettingsSliceDataRepository(private val entryRepository: SettingsEntryRepository) {
+    // The map of slice uri to its EntrySliceData, a.k.a. LiveData<Slice?>
+    private val sliceDataMap: MutableMap<String, EntrySliceData> = mutableMapOf()
+
+    // Note: mark this function synchronized, so that we can get the same livedata during the
+    // whole lifecycle of a Slice.
+    @Synchronized
+    fun getOrBuildSliceData(sliceUri: Uri): EntrySliceData? {
+        val sliceString = sliceUri.getSliceId() ?: return null
+        return sliceDataMap[sliceString] ?: buildLiveDataImpl(sliceUri)?.let {
+            sliceDataMap[sliceString] = it
+            it
+        }
+    }
+
+    fun getActiveSliceData(sliceUri: Uri): EntrySliceData? {
+        val sliceString = sliceUri.getSliceId() ?: return null
+        val sliceData = sliceDataMap[sliceString] ?: return null
+        return if (sliceData.isActive()) sliceData else null
+    }
+
+    private fun buildLiveDataImpl(sliceUri: Uri): EntrySliceData? {
+        Log.d(TAG, "buildLiveData: $sliceUri")
+
+        val entryId = sliceUri.getEntryId() ?: return null
+        val entry = entryRepository.getEntry(entryId) ?: return null
+        if (!entry.hasSliceSupport) return null
+        val arguments = sliceUri.getRuntimeArguments()
+        return entry.getSliceData(runtimeArguments = arguments, sliceUri = sliceUri)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
new file mode 100644
index 0000000..ff143ed
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.slice
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
+import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+
+// Defines SliceUri, which contains special query parameters:
+//  -- KEY_DESTINATION: The route that this slice is navigated to.
+//  -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
+//  Other parameters can considered as runtime parameters.
+// Use {entryId, runtimeParams} as the unique Id of this Slice.
+typealias SliceUri = Uri
+
+val RESERVED_KEYS = listOf(
+    KEY_DESTINATION,
+    KEY_HIGHLIGHT_ENTRY
+)
+
+fun SliceUri.getEntryId(): String? {
+    return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
+}
+
+fun SliceUri.getDestination(): String? {
+    return getQueryParameter(KEY_DESTINATION)
+}
+
+fun SliceUri.getRuntimeArguments(): Bundle {
+    val params = Bundle()
+    for (queryName in queryParameterNames) {
+        if (RESERVED_KEYS.contains(queryName)) continue
+        params.putString(queryName, getQueryParameter(queryName))
+    }
+    return params
+}
+
+fun SliceUri.getSliceId(): String? {
+    val entryId = getEntryId() ?: return null
+    val params = getRuntimeArguments()
+    return "${entryId}_$params"
+}
+
+fun Uri.Builder.appendSliceParams(
+    route: String? = null,
+    entryId: String? = null,
+    runtimeArguments: Bundle? = null
+): Uri.Builder {
+    if (route != null) appendQueryParameter(KEY_DESTINATION, route)
+    if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
+    if (runtimeArguments != null) {
+        for (key in runtimeArguments.keySet()) {
+            appendQueryParameter(key, runtimeArguments.getString(key, ""))
+        }
+    }
+    return this
+}
+
+fun SliceUri.createBroadcastPendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val sliceBroadcastClass =
+        SpaEnvironmentFactory.instance.sliceBroadcastReceiverClass ?: return null
+    val entryId = getEntryId() ?: return null
+    return createBroadcastPendingIntent(context, sliceBroadcastClass, entryId)
+}
+
+fun SliceUri.createBrowsePendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+    val destination = getDestination() ?: return null
+    val entryId = getEntryId()
+    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+fun Intent.createBrowsePendingIntent(): PendingIntent? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    val browseActivityClass = SpaEnvironmentFactory.instance.browseActivityClass ?: return null
+    val destination = getStringExtra(KEY_DESTINATION) ?: return null
+    val entryId = getStringExtra(KEY_HIGHLIGHT_ENTRY)
+    return createBrowsePendingIntent(context, browseActivityClass, destination, entryId)
+}
+
+private fun createBrowsePendingIntent(
+    context: Context,
+    browseActivityClass: Class<out Activity>,
+    destination: String,
+    entryId: String?
+): PendingIntent {
+    val intent = Intent().setComponent(ComponentName(context, browseActivityClass))
+        .apply {
+            // Set both extra and data (which is a Uri) in Slice Intent:
+            // 1) extra is used in SPA navigation framework
+            // 2) data is used in Slice framework
+            putExtra(KEY_DESTINATION, destination)
+            if (entryId != null) {
+                putExtra(KEY_HIGHLIGHT_ENTRY, entryId)
+            }
+            data = Uri.Builder().appendSliceParams(destination, entryId).build()
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        }
+
+    return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+}
+
+private fun createBroadcastPendingIntent(
+    context: Context,
+    sliceBroadcastClass: Class<out BroadcastReceiver>,
+    entryId: String
+): PendingIntent {
+    val intent = Intent().setComponent(ComponentName(context, sliceBroadcastClass))
+        .apply { data = Uri.Builder().appendSliceParams(entryId = entryId).build() }
+    return PendingIntent.getBroadcast(
+        context, 0 /* requestCode */, intent,
+        PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
new file mode 100644
index 0000000..cff1c0c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/presenter/Demo.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.slice.presenter
+
+import android.net.Uri
+import androidx.compose.material3.Divider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.slice.widget.SliceLiveData
+import androidx.slice.widget.SliceView
+
+@Composable
+fun SliceDemo(sliceUri: Uri) {
+    val context = LocalContext.current
+    val lifecycleOwner = LocalLifecycleOwner.current
+    val sliceData = remember {
+        SliceLiveData.fromUri(context, sliceUri)
+    }
+
+    Divider()
+    AndroidView(
+        factory = { localContext ->
+            val view = SliceView(localContext)
+            view.setShowTitleItems(true)
+            view.isScrollable = false
+            view
+        },
+        update = { view -> sliceData.observe(lifecycleOwner, view) }
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
new file mode 100644
index 0000000..b65b91f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/provider/Demo.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.slice.provider
+
+import android.app.PendingIntent
+import android.content.Context
+import android.net.Uri
+import androidx.core.graphics.drawable.IconCompat
+import androidx.slice.Slice
+import androidx.slice.SliceManager
+import androidx.slice.builders.ListBuilder
+import androidx.slice.builders.SliceAction
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.slice.createBroadcastPendingIntent
+import com.android.settingslib.spa.slice.createBrowsePendingIntent
+
+fun createDemoBrowseSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+    val intent = sliceUri.createBrowsePendingIntent() ?: return null
+    return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoActionSlice(sliceUri: Uri, title: String, summary: String): Slice? {
+    val intent = sliceUri.createBroadcastPendingIntent() ?: return null
+    return createDemoSlice(sliceUri, title, summary, intent)
+}
+
+fun createDemoSlice(sliceUri: Uri, title: String, summary: String, intent: PendingIntent): Slice? {
+    val context = SpaEnvironmentFactory.instance.appContext
+    if (!SliceManager.getInstance(context).pinnedSlices.contains(sliceUri)) return null
+    return ListBuilder(context, sliceUri, ListBuilder.INFINITY)
+        .addRow(ListBuilder.RowBuilder().apply {
+            setPrimaryAction(createSliceAction(context, intent))
+            setTitle(title)
+            setSubtitle(summary)
+        }).build()
+}
+
+private fun createSliceAction(context: Context, intent: PendingIntent): SliceAction {
+    return SliceAction.create(
+        intent,
+        IconCompat.createWithResource(
+            context,
+            com.google.android.material.R.drawable.navigation_empty_icon
+        ),
+        ListBuilder.ICON_IMAGE,
+        "Enter app"
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/SettingsSlider.kt
deleted file mode 100644
index 4f77a89..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/SettingsSlider.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.widget
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.AccessAlarm
-import androidx.compose.material.icons.outlined.MusicNote
-import androidx.compose.material.icons.outlined.MusicOff
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Slider
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.preference.BaseLayout
-import kotlin.math.roundToInt
-
-/**
- * The widget model for [SettingsSlider] widget.
- */
-interface SettingsSliderModel {
-    /**
-     * The title of this [SettingsSlider].
-     */
-    val title: String
-
-    /**
-     * The initial position of the [SettingsSlider].
-     */
-    val initValue: Int
-
-    /**
-     * The value range for this [SettingsSlider].
-     */
-    val valueRange: IntRange
-        get() = 0..100
-
-    /**
-     * The lambda to be invoked during the value change by dragging or a click. This callback is
-     * used to get the real time value of the [SettingsSlider].
-     */
-    val onValueChange: ((value: Int) -> Unit)?
-        get() = null
-
-    /**
-     * The lambda to be invoked when value change has ended. This callback is used to get when the
-     * user has completed selecting a new value by ending a drag or a click.
-     */
-    val onValueChangeFinished: (() -> Unit)?
-        get() = null
-
-    /**
-     * The icon image for [SettingsSlider]. If not specified, the slider hides the icon by default.
-     */
-    val icon: ImageVector?
-        get() = null
-
-    /**
-     * Indicates whether to show step marks. If show step marks, when user finish sliding,
-     * the slider will automatically jump to the nearest step mark. Otherwise, the slider hides
-     * the step marks by default.
-     *
-     * The step is fixed to 1.
-     */
-    val showSteps: Boolean
-        get() = false
-}
-
-/**
- * Settings slider widget.
- *
- * Data is provided through [SettingsSliderModel].
- */
-@Composable
-fun SettingsSlider(model: SettingsSliderModel) {
-    SettingsSlider(
-        title = model.title,
-        initValue = model.initValue,
-        valueRange = model.valueRange,
-        onValueChange = model.onValueChange,
-        onValueChangeFinished = model.onValueChangeFinished,
-        icon = model.icon,
-        showSteps = model.showSteps,
-    )
-}
-
-@Composable
-internal fun SettingsSlider(
-    title: String,
-    initValue: Int,
-    valueRange: IntRange = 0..100,
-    onValueChange: ((value: Int) -> Unit)? = null,
-    onValueChangeFinished: (() -> Unit)? = null,
-    icon: ImageVector? = null,
-    showSteps: Boolean = false,
-    modifier: Modifier = Modifier,
-) {
-    var sliderPosition by rememberSaveable { mutableStateOf(initValue.toFloat()) }
-    BaseLayout(
-        title = title,
-        subTitle = {
-            Slider(
-                value = sliderPosition,
-                onValueChange = {
-                    sliderPosition = it
-                    onValueChange?.invoke(sliderPosition.roundToInt())
-                },
-                modifier = modifier,
-                valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
-                steps = if (showSteps) (valueRange.count() - 2) else 0,
-                onValueChangeFinished = onValueChangeFinished,
-            )
-        },
-        icon = if (icon != null) ({
-            Icon(imageVector = icon, contentDescription = null)
-        }) else null,
-    )
-}
-
-@Preview
-@Composable
-private fun SettingsSliderPreview() {
-    SettingsTheme {
-        val initValue = 30
-        var sliderPosition by rememberSaveable { mutableStateOf(initValue) }
-        SettingsSlider(
-            title = "Alarm Volume",
-            initValue = 30,
-            onValueChange = { sliderPosition = it },
-            onValueChangeFinished = {
-                println("onValueChangeFinished: the value is $sliderPosition")
-            },
-            icon = Icons.Outlined.AccessAlarm,
-        )
-    }
-}
-
-@Preview
-@Composable
-private fun SettingsSliderIconChangePreview() {
-    SettingsTheme {
-        var icon by remember { mutableStateOf(Icons.Outlined.MusicNote) }
-        SettingsSlider(
-            title = "Media Volume",
-            initValue = 40,
-            onValueChange = { it: Int ->
-                icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff
-            },
-            icon = icon,
-        )
-    }
-}
-
-@Preview
-@Composable
-private fun SettingsSliderStepsPreview() {
-    SettingsTheme {
-        SettingsSlider(
-            title = "Display Text",
-            initValue = 2,
-            valueRange = 1..5,
-            showSteps = true,
-        )
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/BarChart.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/BarChart.kt
new file mode 100644
index 0000000..0b0f07e
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/BarChart.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.chart
+
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.settingslib.spa.framework.theme.divider
+import com.github.mikephil.charting.charts.BarChart
+import com.github.mikephil.charting.components.XAxis
+import com.github.mikephil.charting.data.BarData
+import com.github.mikephil.charting.data.BarDataSet
+import com.github.mikephil.charting.data.BarEntry
+import com.github.mikephil.charting.formatter.IAxisValueFormatter
+
+/**
+ * The chart settings model for [BarChart].
+ */
+interface BarChartModel {
+    /**
+     * The chart data list for [BarChart].
+     */
+    val chartDataList: List<BarChartData>
+
+    /**
+     * The label text formatter for x value.
+     */
+    val xValueFormatter: IAxisValueFormatter?
+        get() = null
+
+    /**
+     * The label text formatter for y value.
+     */
+    val yValueFormatter: IAxisValueFormatter?
+        get() = null
+
+    /**
+     * The minimum value for y-axis.
+     */
+    val yAxisMinValue: Float
+        get() = 0f
+
+    /**
+     * The maximum value for y-axis.
+     */
+    val yAxisMaxValue: Float
+        get() = 1f
+
+    /**
+     * The label count for y-axis.
+     */
+    val yAxisLabelCount: Int
+        get() = 3
+}
+
+data class BarChartData(
+    var x: Float?,
+    var y: Float?,
+)
+
+@Composable
+fun BarChart(barChartModel: BarChartModel) {
+    BarChart(
+        chartDataList = barChartModel.chartDataList,
+        xValueFormatter = barChartModel.xValueFormatter,
+        yValueFormatter = barChartModel.yValueFormatter,
+        yAxisMinValue = barChartModel.yAxisMinValue,
+        yAxisMaxValue = barChartModel.yAxisMaxValue,
+        yAxisLabelCount = barChartModel.yAxisLabelCount,
+    )
+}
+
+@Composable
+fun BarChart(
+    chartDataList: List<BarChartData>,
+    modifier: Modifier = Modifier,
+    xValueFormatter: IAxisValueFormatter? = null,
+    yValueFormatter: IAxisValueFormatter? = null,
+    yAxisMinValue: Float = 0f,
+    yAxisMaxValue: Float = 30f,
+    yAxisLabelCount: Int = 4,
+) {
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .wrapContentHeight(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center
+    ) {
+        Column(
+            modifier = Modifier
+                .padding(16.dp)
+                .height(170.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center
+        ) {
+            val colorScheme = MaterialTheme.colorScheme
+            val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
+            val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
+            Crossfade(targetState = chartDataList) { barChartData ->
+                AndroidView(factory = { context ->
+                    BarChart(context).apply {
+                        // Fixed Settings.
+                        layoutParams = LinearLayout.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                        )
+                        this.description.isEnabled = false
+                        this.legend.isEnabled = false
+                        this.extraBottomOffset = 4f
+                        this.setScaleEnabled(false)
+
+                        this.xAxis.position = XAxis.XAxisPosition.BOTTOM
+                        this.xAxis.setDrawGridLines(false)
+                        this.xAxis.setDrawAxisLine(false)
+                        this.xAxis.textColor = labelTextColor
+                        this.xAxis.textSize = labelTextSize
+                        this.xAxis.yOffset = 10f
+
+                        this.axisLeft.isEnabled = false
+                        this.axisRight.setDrawAxisLine(false)
+                        this.axisRight.textSize = labelTextSize
+                        this.axisRight.textColor = labelTextColor
+                        this.axisRight.gridColor = colorScheme.divider.toArgb()
+                        this.axisRight.xOffset = 10f
+
+                        // Customizable Settings.
+                        this.xAxis.valueFormatter = xValueFormatter
+                        this.axisRight.valueFormatter = yValueFormatter
+
+                        this.axisLeft.axisMinimum = yAxisMinValue
+                        this.axisLeft.axisMaximum = yAxisMaxValue
+                        this.axisRight.axisMinimum = yAxisMinValue
+                        this.axisRight.axisMaximum = yAxisMaxValue
+
+                        this.axisRight.setLabelCount(yAxisLabelCount, true)
+                    }
+                },
+                    modifier = Modifier
+                        .wrapContentSize()
+                        .padding(4.dp),
+                    update = {
+                        updateBarChartWithData(it, barChartData, colorScheme)
+                    }
+                )
+            }
+        }
+    }
+}
+
+fun updateBarChartWithData(
+    chart: BarChart,
+    data: List<BarChartData>,
+    colorScheme: ColorScheme
+) {
+    val entries = ArrayList<BarEntry>()
+    for (i in data.indices) {
+        val item = data[i]
+        entries.add(BarEntry(item.x ?: 0.toFloat(), item.y ?: 0.toFloat()))
+    }
+
+    val ds = BarDataSet(entries, "")
+    ds.colors = arrayListOf(colorScheme.surfaceVariant.toArgb())
+    ds.setDrawValues(false)
+    ds.isHighlightEnabled = true
+    ds.highLightColor = colorScheme.primary.toArgb()
+    ds.highLightAlpha = 255
+    // TODO: Sets round corners for bars.
+
+    val d = BarData(ds)
+    chart.data = d
+    chart.invalidate()
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/ColorPalette.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/ColorPalette.kt
new file mode 100644
index 0000000..70bc017
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/ColorPalette.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.chart
+
+import androidx.compose.ui.graphics.Color
+
+object ColorPalette {
+    // Alpha = 1
+    val red: Color = Color(0xffd93025)
+    val orange: Color = Color(0xffe8710a)
+    val yellow: Color = Color(0xfff9ab00)
+    val green: Color = Color(0xff1e8e3e)
+    val cyan: Color = Color(0xff12b5cb)
+    val blue: Color = Color(0xff1a73e8)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/LineChart.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/LineChart.kt
new file mode 100644
index 0000000..7d48265
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/LineChart.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.chart
+
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.settingslib.spa.framework.theme.divider
+import com.github.mikephil.charting.charts.LineChart
+import com.github.mikephil.charting.components.XAxis
+import com.github.mikephil.charting.data.Entry
+import com.github.mikephil.charting.data.LineData
+import com.github.mikephil.charting.data.LineDataSet
+import com.github.mikephil.charting.formatter.IAxisValueFormatter
+
+/**
+ * The chart settings model for [LineChart].
+ */
+interface LineChartModel {
+    /**
+     * The chart data list for [LineChart].
+     */
+    val chartDataList: List<LineChartData>
+
+    /**
+     * The label text formatter for x value.
+     */
+    val xValueFormatter: IAxisValueFormatter?
+        get() = null
+
+    /**
+     * The label text formatter for y value.
+     */
+    val yValueFormatter: IAxisValueFormatter?
+        get() = null
+
+    /**
+     * The minimum value for y-axis.
+     */
+    val yAxisMinValue: Float
+        get() = 0f
+
+    /**
+     * The maximum value for y-axis.
+     */
+    val yAxisMaxValue: Float
+        get() = 1f
+
+    /**
+     * The label count for y-axis.
+     */
+    val yAxisLabelCount: Int
+        get() = 3
+
+    /**
+     * Indicates whether to smooth the line.
+     */
+    val showSmoothLine: Boolean
+        get() = true
+}
+
+data class LineChartData(
+    var x: Float?,
+    var y: Float?,
+)
+
+@Composable
+fun LineChart(lineChartModel: LineChartModel) {
+    LineChart(
+        chartDataList = lineChartModel.chartDataList,
+        xValueFormatter = lineChartModel.xValueFormatter,
+        yValueFormatter = lineChartModel.yValueFormatter,
+        yAxisMinValue = lineChartModel.yAxisMinValue,
+        yAxisMaxValue = lineChartModel.yAxisMaxValue,
+        yAxisLabelCount = lineChartModel.yAxisLabelCount,
+        showSmoothLine = lineChartModel.showSmoothLine,
+    )
+}
+
+@Composable
+fun LineChart(
+    chartDataList: List<LineChartData>,
+    modifier: Modifier = Modifier,
+    xValueFormatter: IAxisValueFormatter? = null,
+    yValueFormatter: IAxisValueFormatter? = null,
+    yAxisMinValue: Float = 0f,
+    yAxisMaxValue: Float = 1f,
+    yAxisLabelCount: Int = 3,
+    showSmoothLine: Boolean = true,
+) {
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .wrapContentHeight(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center
+    ) {
+        Column(
+            modifier = Modifier
+                .padding(16.dp)
+                .height(170.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center
+        ) {
+            val colorScheme = MaterialTheme.colorScheme
+            val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
+            val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
+            Crossfade(targetState = chartDataList) { lineChartData ->
+                AndroidView(factory = { context ->
+                    LineChart(context).apply {
+                        // Fixed Settings.
+                        layoutParams = LinearLayout.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                        )
+                        this.description.isEnabled = false
+                        this.legend.isEnabled = false
+                        this.extraBottomOffset = 4f
+                        this.setTouchEnabled(false)
+
+                        this.xAxis.position = XAxis.XAxisPosition.BOTTOM
+                        this.xAxis.setDrawGridLines(false)
+                        this.xAxis.setDrawAxisLine(false)
+                        this.xAxis.textColor = labelTextColor
+                        this.xAxis.textSize = labelTextSize
+                        this.xAxis.yOffset = 10f
+
+                        this.axisLeft.isEnabled = false
+
+                        this.axisRight.setDrawAxisLine(false)
+                        this.axisRight.textSize = labelTextSize
+                        this.axisRight.textColor = labelTextColor
+                        this.axisRight.gridColor = colorScheme.divider.toArgb()
+                        this.axisRight.xOffset = 10f
+                        this.axisRight.isGranularityEnabled = true
+
+                        // Customizable Settings.
+                        this.xAxis.valueFormatter = xValueFormatter
+                        this.axisRight.valueFormatter = yValueFormatter
+
+                        this.axisLeft.axisMinimum =
+                            yAxisMinValue - 0.01f * (yAxisMaxValue - yAxisMinValue)
+                        this.axisRight.axisMinimum =
+                            yAxisMinValue - 0.01f * (yAxisMaxValue - yAxisMinValue)
+                        this.axisRight.granularity =
+                            (yAxisMaxValue - yAxisMinValue) / (yAxisLabelCount - 1)
+                    }
+                },
+                    modifier = Modifier
+                        .wrapContentSize()
+                        .padding(4.dp),
+                    update = {
+                        updateLineChartWithData(it, lineChartData, colorScheme, showSmoothLine)
+                    })
+            }
+        }
+    }
+}
+
+fun updateLineChartWithData(
+    chart: LineChart,
+    data: List<LineChartData>,
+    colorScheme: ColorScheme,
+    showSmoothLine: Boolean
+) {
+    val entries = ArrayList<Entry>()
+    for (i in data.indices) {
+        val item = data[i]
+        entries.add(Entry(item.x ?: 0.toFloat(), item.y ?: 0.toFloat()))
+    }
+
+    val ds = LineDataSet(entries, "")
+    ds.colors = arrayListOf(colorScheme.primary.toArgb())
+    ds.lineWidth = 2f
+    if (showSmoothLine) {
+        ds.mode = LineDataSet.Mode.CUBIC_BEZIER
+    }
+    ds.setDrawValues(false)
+    ds.setDrawCircles(false)
+    ds.setDrawFilled(true)
+    ds.fillColor = colorScheme.primary.toArgb()
+    ds.fillAlpha = 38
+    // TODO: enable gradient fill color for line chart.
+
+    val d = LineData(ds)
+    chart.data = d
+    chart.invalidate()
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/PieChart.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/PieChart.kt
new file mode 100644
index 0000000..51a8d0d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/chart/PieChart.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.chart
+
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.github.mikephil.charting.charts.PieChart
+import com.github.mikephil.charting.data.PieData
+import com.github.mikephil.charting.data.PieDataSet
+import com.github.mikephil.charting.data.PieEntry
+
+/**
+ * The chart settings model for [PieChart].
+ */
+interface PieChartModel {
+    /**
+     * The chart data list for [PieChart].
+     */
+    val chartDataList: List<PieChartData>
+
+    /**
+     * The center text in the hole of [PieChart].
+     */
+    val centerText: String?
+        get() = null
+}
+
+val colorPalette = arrayListOf(
+    ColorPalette.blue.toArgb(),
+    ColorPalette.red.toArgb(),
+    ColorPalette.yellow.toArgb(),
+    ColorPalette.green.toArgb(),
+    ColorPalette.orange.toArgb(),
+    ColorPalette.cyan.toArgb(),
+    Color.Blue.toArgb()
+)
+
+data class PieChartData(
+    var value: Float?,
+    var label: String?,
+)
+
+@Composable
+fun PieChart(pieChartModel: PieChartModel) {
+    PieChart(
+        chartDataList = pieChartModel.chartDataList,
+        centerText = pieChartModel.centerText,
+    )
+}
+
+@Composable
+fun PieChart(
+    chartDataList: List<PieChartData>,
+    modifier: Modifier = Modifier,
+    centerText: String? = null,
+) {
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .wrapContentHeight(),
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center
+    ) {
+        Column(
+            modifier = Modifier
+                .padding(16.dp)
+                .height(280.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center
+        ) {
+            val colorScheme = MaterialTheme.colorScheme
+            val labelTextColor = colorScheme.onSurfaceVariant.toArgb()
+            val labelTextSize = MaterialTheme.typography.bodyMedium.fontSize.value
+            val centerTextSize = MaterialTheme.typography.titleLarge.fontSize.value
+            Crossfade(targetState = chartDataList) { pieChartData ->
+                AndroidView(factory = { context ->
+                    PieChart(context).apply {
+                        // Fixed settings.`
+                        layoutParams = LinearLayout.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                        )
+                        this.isRotationEnabled = false
+                        this.description.isEnabled = false
+                        this.legend.isEnabled = false
+                        this.setTouchEnabled(false)
+
+                        this.isDrawHoleEnabled = true
+                        this.holeRadius = 90.0f
+                        this.setHoleColor(Color.Transparent.toArgb())
+                        this.setEntryLabelColor(labelTextColor)
+                        this.setEntryLabelTextSize(labelTextSize)
+                        this.setCenterTextSize(centerTextSize)
+                        this.setCenterTextColor(colorScheme.onSurface.toArgb())
+
+                        // Customizable settings.
+                        this.centerText = centerText
+                    }
+                },
+                    modifier = Modifier
+                        .wrapContentSize()
+                        .padding(4.dp), update = {
+                        updatePieChartWithData(it, pieChartData)
+                    })
+            }
+        }
+    }
+}
+
+fun updatePieChartWithData(
+    chart: PieChart,
+    data: List<PieChartData>,
+) {
+    val entries = ArrayList<PieEntry>()
+    for (i in data.indices) {
+        val item = data[i]
+        entries.add(PieEntry(item.value ?: 0.toFloat(), item.label ?: ""))
+    }
+
+    val ds = PieDataSet(entries, "")
+    ds.setDrawValues(false)
+    ds.colors = colorPalette
+    ds.sliceSpace = 2f
+    ds.yValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE
+    ds.xValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE
+    ds.valueLineColor = Color.Transparent.toArgb()
+    ds.valueLinePart1Length = 0.1f
+    ds.valueLinePart2Length = 0f
+
+    val d = PieData(ds)
+    chart.data = d
+    chart.invalidate()
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index 9a34dbf..6135203 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -72,7 +72,7 @@
 }
 
 @Composable
-private fun BaseIcon(
+internal fun BaseIcon(
     icon: @Composable (() -> Unit)?,
     modifier: Modifier,
     paddingStart: Dp,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index f2fe7ad7..db95e23 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -28,26 +28,29 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 @Composable
 fun MainSwitchPreference(model: SwitchPreferenceModel) {
-    Surface(
-        modifier = Modifier.padding(SettingsDimension.itemPaddingEnd),
-        color = when (model.checked.value) {
-            true -> MaterialTheme.colorScheme.primaryContainer
-            else -> MaterialTheme.colorScheme.secondaryContainer
-        },
-        shape = SettingsShape.CornerLarge,
-    ) {
-        InternalSwitchPreference(
-            title = model.title,
-            checked = model.checked,
-            changeable = model.changeable,
-            onCheckedChange = model.onCheckedChange,
-            paddingStart = 20.dp,
-            paddingEnd = 20.dp,
-            paddingVertical = 18.dp,
-        )
+    EntryHighlight {
+        Surface(
+            modifier = Modifier.padding(SettingsDimension.itemPaddingEnd),
+            color = when (model.checked.value) {
+                true -> MaterialTheme.colorScheme.primaryContainer
+                else -> MaterialTheme.colorScheme.secondaryContainer
+            },
+            shape = SettingsShape.CornerLarge,
+        ) {
+            InternalSwitchPreference(
+                title = model.title,
+                checked = model.checked,
+                changeable = model.changeable,
+                onCheckedChange = model.onCheckedChange,
+                paddingStart = 20.dp,
+                paddingEnd = 20.dp,
+                paddingVertical = 18.dp,
+            )
+        }
     }
 }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
index d1021e2..895edf7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -24,10 +24,12 @@
 import androidx.compose.ui.graphics.vector.ImageVector
 import com.android.settingslib.spa.framework.common.EntryMacro
 import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.compose.stateOf
-import com.android.settingslib.spa.framework.util.WrapOnClickWithLog
+import com.android.settingslib.spa.framework.util.wrapOnClickWithLog
 import com.android.settingslib.spa.widget.ui.createSettingsIcon
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 data class SimplePreferenceMacro(
     val title: String,
@@ -54,6 +56,10 @@
             keyword = searchKeywords
         )
     }
+
+    override fun getStatusData(): EntryStatusData {
+        return EntryStatusData(isDisabled = false)
+    }
 }
 
 /**
@@ -106,7 +112,7 @@
     model: PreferenceModel,
     singleLineSummary: Boolean = false,
 ) {
-    val onClickWithLog = WrapOnClickWithLog(model.onClick)
+    val onClickWithLog = wrapOnClickWithLog(model.onClick)
     val modifier = remember(model.enabled.value) {
         if (onClickWithLog != null) {
             Modifier.clickable(
@@ -115,12 +121,14 @@
             )
         } else Modifier
     }
-    BasePreference(
-        title = model.title,
-        summary = model.summary,
-        singleLineSummary = singleLineSummary,
-        modifier = modifier,
-        icon = model.icon,
-        enabled = model.enabled,
-    )
+    EntryHighlight {
+        BasePreference(
+            title = model.title,
+            summary = model.summary,
+            singleLineSummary = singleLineSummary,
+            modifier = modifier,
+            icon = model.icon,
+            enabled = model.enabled,
+        )
+    }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
new file mode 100644
index 0000000..b8c59ad
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
@@ -0,0 +1,171 @@
+/*
+ * 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.preference
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.LinearProgressBar
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+/**
+ * The widget model for [ProgressBarPreference] widget.
+ */
+interface ProgressBarPreferenceModel {
+    /**
+     * The title of this [ProgressBarPreference].
+     */
+    val title: String
+
+    /**
+     * The progress fraction of the ProgressBar. Should be float in range [0f, 1f]
+     */
+    val progress: Float
+
+    /**
+     * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default.
+     */
+    val icon: ImageVector?
+        get() = null
+
+    /**
+     * The height of the ProgressBar.
+     */
+    val height: Float
+        get() = 4f
+
+    /**
+     * Indicates whether to use rounded corner for the progress bars.
+     */
+    val roundedCorner: Boolean
+        get() = true
+}
+
+/**
+ * Progress bar preference widget.
+ *
+ * Data is provided through [ProgressBarPreferenceModel].
+ */
+@Composable
+fun ProgressBarPreference(model: ProgressBarPreferenceModel) {
+    ProgressBarPreference(
+        title = model.title,
+        progress = model.progress,
+        icon = model.icon,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+/**
+ * Progress bar with data preference widget.
+ */
+@Composable
+fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) {
+    val icon = model.icon
+    ProgressBarWithDataPreference(
+        title = model.title,
+        data = data,
+        progress = model.progress,
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+@Composable
+internal fun ProgressBarPreference(
+    title: String,
+    progress: Float,
+    icon: ImageVector? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    BaseLayout(
+        title = title,
+        subTitle = {
+            LinearProgressBar(progress, height, roundedCorner)
+        },
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+    )
+}
+
+
+@Composable
+internal fun ProgressBarWithDataPreference(
+    title: String,
+    data: String,
+    progress: Float,
+    icon: (@Composable () -> Unit)? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(end = SettingsDimension.itemPaddingEnd),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart)
+        TitleWithData(
+            title = title,
+            data = data,
+            subTitle = {
+                LinearProgressBar(progress, height, roundedCorner)
+            },
+            modifier = Modifier
+                .weight(1f)
+                .padding(vertical = SettingsDimension.itemPaddingVertical),
+        )
+    }
+}
+
+@Composable
+private fun TitleWithData(
+    title: String,
+    data: String,
+    subTitle: @Composable () -> Unit,
+    modifier: Modifier
+) {
+    Column(modifier) {
+        Row {
+            Box(modifier = Modifier.weight(1f)) {
+                SettingsTitle(title)
+            }
+            Text(
+                text = data,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.titleMedium,
+            )
+        }
+        subTitle()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
new file mode 100644
index 0000000..4ee2af0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.preference
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccessAlarm
+import androidx.compose.material.icons.outlined.MusicNote
+import androidx.compose.material.icons.outlined.MusicOff
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.ui.SettingsSlider
+import com.android.settingslib.spa.widget.util.EntryHighlight
+
+/**
+ * The widget model for [SliderPreference] widget.
+ */
+interface SliderPreferenceModel {
+    /**
+     * The title of this [SliderPreference].
+     */
+    val title: String
+
+    /**
+     * The initial position of the [SliderPreference].
+     */
+    val initValue: Int
+
+    /**
+     * The value range for this [SliderPreference].
+     */
+    val valueRange: IntRange
+        get() = 0..100
+
+    /**
+     * The lambda to be invoked during the value change by dragging or a click. This callback is
+     * used to get the real time value of the [SliderPreference].
+     */
+    val onValueChange: ((value: Int) -> Unit)?
+        get() = null
+
+    /**
+     * The lambda to be invoked when value change has ended. This callback is used to get when the
+     * user has completed selecting a new value by ending a drag or a click.
+     */
+    val onValueChangeFinished: (() -> Unit)?
+        get() = null
+
+    /**
+     * The icon image for [SliderPreference]. If not specified, the slider hides the icon by default.
+     */
+    val icon: ImageVector?
+        get() = null
+
+    /**
+     * Indicates whether to show step marks. If show step marks, when user finish sliding,
+     * the slider will automatically jump to the nearest step mark. Otherwise, the slider hides
+     * the step marks by default.
+     *
+     * The step is fixed to 1.
+     */
+    val showSteps: Boolean
+        get() = false
+}
+
+/**
+ * Settings slider widget.
+ *
+ * Data is provided through [SliderPreferenceModel].
+ */
+@Composable
+fun SliderPreference(model: SliderPreferenceModel) {
+    EntryHighlight {
+        SliderPreference(
+            title = model.title,
+            initValue = model.initValue,
+            valueRange = model.valueRange,
+            onValueChange = model.onValueChange,
+            onValueChangeFinished = model.onValueChangeFinished,
+            icon = model.icon,
+            showSteps = model.showSteps,
+        )
+    }
+}
+
+@Composable
+internal fun SliderPreference(
+    title: String,
+    initValue: Int,
+    modifier: Modifier = Modifier,
+    valueRange: IntRange = 0..100,
+    onValueChange: ((value: Int) -> Unit)? = null,
+    onValueChangeFinished: (() -> Unit)? = null,
+    icon: ImageVector? = null,
+    showSteps: Boolean = false,
+) {
+    BaseLayout(
+        title = title,
+        subTitle = {
+            SettingsSlider(
+                initValue,
+                modifier,
+                valueRange,
+                onValueChange,
+                onValueChangeFinished,
+                showSteps
+            )
+        },
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+    )
+}
+
+@Preview
+@Composable
+private fun SliderPreferencePreview() {
+    SettingsTheme {
+        val initValue = 30
+        var sliderPosition by rememberSaveable { mutableStateOf(initValue) }
+        SliderPreference(
+            title = "Alarm Volume",
+            initValue = 30,
+            onValueChange = { sliderPosition = it },
+            onValueChangeFinished = {
+                println("onValueChangeFinished: the value is $sliderPosition")
+            },
+            icon = Icons.Outlined.AccessAlarm,
+        )
+    }
+}
+
+@Preview
+@Composable
+private fun SliderPreferenceIconChangePreview() {
+    SettingsTheme {
+        var icon by remember { mutableStateOf(Icons.Outlined.MusicNote) }
+        SliderPreference(
+            title = "Media Volume",
+            initValue = 40,
+            onValueChange = { it: Int ->
+                icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff
+            },
+            icon = icon,
+        )
+    }
+}
+
+@Preview
+@Composable
+private fun SliderPreferenceStepsPreview() {
+    SettingsTheme {
+        SliderPreference(
+            title = "Display Text",
+            initValue = 2,
+            valueRange = 1..5,
+            showSteps = true,
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
index 992ce9e..2d60619 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
@@ -31,8 +31,9 @@
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SwitchPreference] widget.
@@ -79,13 +80,15 @@
  */
 @Composable
 fun SwitchPreference(model: SwitchPreferenceModel) {
-    InternalSwitchPreference(
-        title = model.title,
-        summary = model.summary,
-        checked = model.checked,
-        changeable = model.changeable,
-        onCheckedChange = model.onCheckedChange,
-    )
+    EntryHighlight {
+        InternalSwitchPreference(
+            title = model.title,
+            summary = model.summary,
+            checked = model.checked,
+            changeable = model.changeable,
+            onCheckedChange = model.onCheckedChange,
+        )
+    }
 }
 
 @Composable
@@ -101,7 +104,7 @@
 ) {
     val checkedValue = checked.value
     val indication = LocalIndication.current
-    val onChangeWithLog = WrapOnSwitchWithLog(onCheckedChange)
+    val onChangeWithLog = wrapOnSwitchWithLog(onCheckedChange)
     val modifier = remember(checkedValue, changeable.value) {
         if (checkedValue != null && onChangeWithLog != null) {
             Modifier.toggleable(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
index f1541b7..fbfcaaa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.spa.widget.preference
 
 import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
 
 @Composable
@@ -25,16 +26,18 @@
     icon: @Composable (() -> Unit)? = null,
     onClick: () -> Unit,
 ) {
-    TwoTargetPreference(
-        title = model.title,
-        summary = model.summary,
-        onClick = onClick,
-        icon = icon,
-    ) {
-        SettingsSwitch(
-            checked = model.checked,
-            changeable = model.changeable,
-            onCheckedChange = model.onCheckedChange,
-        )
+    EntryHighlight {
+        TwoTargetPreference(
+            title = model.title,
+            summary = model.summary,
+            onClick = onClick,
+            icon = icon,
+        ) {
+            SettingsSwitch(
+                checked = model.checked,
+                changeable = model.changeable,
+                onCheckedChange = model.onCheckedChange,
+            )
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 6a88f2d..764973f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -16,9 +16,12 @@
 
 package com.android.settingslib.spa.widget.scaffold
 
+import androidx.appcompat.R
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material.icons.outlined.FindInPage
 import androidx.compose.material.icons.outlined.MoreVert
 import androidx.compose.material3.DropdownMenu
 import androidx.compose.material3.Icon
@@ -31,17 +34,23 @@
 import androidx.compose.ui.res.stringResource
 import com.android.settingslib.spa.framework.compose.LocalNavController
 
+/** Action that navigates back to last page. */
 @Composable
 internal fun NavigateBack() {
     val navController = LocalNavController.current
-    val contentDescription = stringResource(
-        id = androidx.appcompat.R.string.abc_action_bar_up_description,
-    )
+    val contentDescription = stringResource(R.string.abc_action_bar_up_description)
     BackAction(contentDescription) {
         navController.navigateBack()
     }
 }
 
+/** Action that collapses the search bar. */
+@Composable
+internal fun CollapseAction(onClick: () -> Unit) {
+    val contentDescription = stringResource(R.string.abc_toolbar_collapse_description)
+    BackAction(contentDescription, onClick)
+}
+
 @Composable
 private fun BackAction(contentDescription: String, onClick: () -> Unit) {
     IconButton(onClick) {
@@ -52,6 +61,28 @@
     }
 }
 
+/** Action that expends the search bar. */
+@Composable
+internal fun SearchAction(onClick: () -> Unit) {
+    IconButton(onClick) {
+        Icon(
+            imageVector = Icons.Outlined.FindInPage,
+            contentDescription = stringResource(R.string.search_menu_title),
+        )
+    }
+}
+
+/** Action that clear the search query. */
+@Composable
+internal fun ClearAction(onClick: () -> Unit) {
+    IconButton(onClick) {
+        Icon(
+            imageVector = Icons.Outlined.Clear,
+            contentDescription = stringResource(R.string.abc_searchview_description_clear),
+        )
+    }
+}
+
 @Composable
 fun MoreOptionsAction(
     content: @Composable ColumnScope.(onDismissRequest: () -> Unit) -> Unit,
@@ -71,9 +102,7 @@
     IconButton(onClick) {
         Icon(
             imageVector = Icons.Outlined.MoreVert,
-            contentDescription = stringResource(
-                id = androidx.appcompat.R.string.abc_action_menu_overflow_description,
-            )
+            contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
         )
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
new file mode 100644
index 0000000..7db1ca1
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ProvideTextStyle
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.TopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LastBaseline
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+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 kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun CustomizedTopAppBar(
+    title: @Composable () -> Unit,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+) {
+    SingleRowTopAppBar(
+        title = title,
+        titleTextStyle = MaterialTheme.typography.titleMedium,
+        navigationIcon = navigationIcon,
+        actions = actions,
+        windowInsets = TopAppBarDefaults.windowInsets,
+        colors = topAppBarColors(),
+    )
+}
+
+/**
+ * The customized LargeTopAppBar for Settings.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun CustomizedLargeTopAppBar(
+    title: String,
+    modifier: Modifier = Modifier,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+    scrollBehavior: TopAppBarScrollBehavior? = null,
+) {
+    TwoRowsTopAppBar(
+        title = { Title(title = title, maxLines = 2) },
+        titleTextStyle = MaterialTheme.typography.displaySmall,
+        smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+        titleBottomPadding = LargeTitleBottomPadding,
+        smallTitle = { Title(title = title, maxLines = 1) },
+        modifier = modifier,
+        navigationIcon = navigationIcon,
+        actions = actions,
+        colors = topAppBarColors(),
+        windowInsets = TopAppBarDefaults.windowInsets,
+        maxHeight = 176.dp,
+        pinnedHeight = ContainerHeight,
+        scrollBehavior = scrollBehavior,
+    )
+}
+
+@Composable
+private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
+    Text(
+        text = title,
+        modifier = Modifier
+            .padding(
+                WindowInsets.navigationBars
+                    .asPaddingValues()
+                    .horizontalValues()
+            )
+            .padding(horizontal = SettingsDimension.itemPaddingAround),
+        overflow = TextOverflow.Ellipsis,
+        maxLines = maxLines,
+    )
+}
+
+@Composable
+private fun topAppBarColors() = TopAppBarColors(
+    containerColor = MaterialTheme.colorScheme.background,
+    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+    navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
+    titleContentColor = MaterialTheme.colorScheme.onSurface,
+    actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+)
+
+/**
+ * Represents the colors used by a top app bar in different states.
+ * This implementation animates the container color according to the top app bar scroll state. It
+ * does not animate the leading, headline, or trailing colors.
+ */
+@Stable
+private class TopAppBarColors(
+    val containerColor: Color,
+    val scrolledContainerColor: Color,
+    val navigationIconContentColor: Color,
+    val titleContentColor: Color,
+    val actionIconContentColor: Color,
+) {
+
+    /**
+     * Represents the container color used for the top app bar.
+     *
+     * A [colorTransitionFraction] provides a percentage value that can be used to generate a color.
+     * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from
+     * the [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
+     *
+     * @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition
+     * percentage
+     */
+    @Composable
+    fun containerColor(colorTransitionFraction: Float): Color {
+        return lerp(
+            containerColor,
+            scrolledContainerColor,
+            FastOutLinearInEasing.transform(colorTransitionFraction)
+        )
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || other !is TopAppBarColors) return false
+
+        if (containerColor != other.containerColor) return false
+        if (scrolledContainerColor != other.scrolledContainerColor) return false
+        if (navigationIconContentColor != other.navigationIconContentColor) return false
+        if (titleContentColor != other.titleContentColor) return false
+        if (actionIconContentColor != other.actionIconContentColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = containerColor.hashCode()
+        result = 31 * result + scrolledContainerColor.hashCode()
+        result = 31 * result + navigationIconContentColor.hashCode()
+        result = 31 * result + titleContentColor.hashCode()
+        result = 31 * result + actionIconContentColor.hashCode()
+
+        return result
+    }
+}
+
+/**
+ * A single-row top app bar that is designed to be called by the small and center aligned top app
+ * bar composables.
+ */
+@Composable
+private fun SingleRowTopAppBar(
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable (RowScope.() -> Unit),
+    windowInsets: WindowInsets,
+    colors: TopAppBarColors,
+) {
+    // Wrap the given actions in a Row.
+    val actionsRow = @Composable {
+        Row(
+            horizontalArrangement = Arrangement.End,
+            verticalAlignment = Alignment.CenterVertically,
+            content = actions
+        )
+    }
+
+    // Compose a Surface with a TopAppBarLayout content.
+    Surface(color = colors.scrolledContainerColor) {
+        val height = LocalDensity.current.run { ContainerHeight.toPx() }
+        TopAppBarLayout(
+            modifier = Modifier
+                .windowInsetsPadding(windowInsets)
+                // clip after padding so we don't show the title over the inset area
+                .clipToBounds(),
+            heightPx = height,
+            navigationIconContentColor = colors.navigationIconContentColor,
+            titleContentColor = colors.titleContentColor,
+            actionIconContentColor = colors.actionIconContentColor,
+            title = title,
+            titleTextStyle = titleTextStyle,
+            titleAlpha = 1f,
+            titleVerticalArrangement = Arrangement.Center,
+            titleHorizontalArrangement = Arrangement.Start,
+            titleBottomPadding = 0,
+            hideTitleSemantics = false,
+            navigationIcon = navigationIcon,
+            actions = actionsRow,
+        )
+    }
+}
+
+/**
+ * A two-rows top app bar that is designed to be called by the Large and Medium top app bar
+ * composables.
+ *
+ * @throws [IllegalArgumentException] if the given [maxHeight] is equal or smaller than the
+ * [pinnedHeight]
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TwoRowsTopAppBar(
+    modifier: Modifier = Modifier,
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    titleBottomPadding: Dp,
+    smallTitle: @Composable () -> Unit,
+    smallTitleTextStyle: TextStyle,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable RowScope.() -> Unit,
+    windowInsets: WindowInsets,
+    colors: TopAppBarColors,
+    maxHeight: Dp,
+    pinnedHeight: Dp,
+    scrollBehavior: TopAppBarScrollBehavior?
+) {
+    if (maxHeight <= pinnedHeight) {
+        throw IllegalArgumentException(
+            "A TwoRowsTopAppBar max height should be greater than its pinned height"
+        )
+    }
+    val pinnedHeightPx: Float
+    val maxHeightPx: Float
+    val titleBottomPaddingPx: Int
+    LocalDensity.current.run {
+        pinnedHeightPx = pinnedHeight.toPx()
+        maxHeightPx = maxHeight.toPx()
+        titleBottomPaddingPx = titleBottomPadding.roundToPx()
+    }
+
+    // Sets the app bar's height offset limit to hide just the bottom title area and keep top title
+    // visible when collapsed.
+    SideEffect {
+        if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) {
+            scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx
+        }
+    }
+
+    // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the
+    // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or
+    // collapse.
+    // This will potentially animate or interpolate a transition between the container color and the
+    // container's scrolled color according to the app bar's scroll state.
+    val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f
+    val appBarContainerColor by rememberUpdatedState(colors.containerColor(colorTransitionFraction))
+
+    // Wrap the given actions in a Row.
+    val actionsRow = @Composable {
+        Row(
+            horizontalArrangement = Arrangement.End,
+            verticalAlignment = Alignment.CenterVertically,
+            content = actions
+        )
+    }
+    val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction)
+    val bottomTitleAlpha = 1f - colorTransitionFraction
+    // Hide the top row title semantics when its alpha value goes below 0.5 threshold.
+    // Hide the bottom row title semantics when the top title semantics are active.
+    val hideTopRowSemantics = colorTransitionFraction < 0.5f
+    val hideBottomRowSemantics = !hideTopRowSemantics
+
+    // Set up support for resizing the top app bar when vertically dragging the bar itself.
+    val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+        Modifier.draggable(
+            orientation = Orientation.Vertical,
+            state = rememberDraggableState { delta ->
+                scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffset + delta
+            },
+            onDragStopped = { velocity ->
+                settleAppBar(
+                    scrollBehavior.state,
+                    velocity,
+                    scrollBehavior.flingAnimationSpec,
+                    scrollBehavior.snapAnimationSpec
+                )
+            }
+        )
+    } else {
+        Modifier
+    }
+
+    Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
+        Column {
+            TopAppBarLayout(
+                modifier = Modifier
+                    .windowInsetsPadding(windowInsets)
+                    // clip after padding so we don't show the title over the inset area
+                    .clipToBounds(),
+                heightPx = pinnedHeightPx,
+                navigationIconContentColor = colors.navigationIconContentColor,
+                titleContentColor = colors.titleContentColor,
+                actionIconContentColor = colors.actionIconContentColor,
+                title = smallTitle,
+                titleTextStyle = smallTitleTextStyle,
+                titleAlpha = topTitleAlpha,
+                titleVerticalArrangement = Arrangement.Center,
+                titleHorizontalArrangement = Arrangement.Start,
+                titleBottomPadding = 0,
+                hideTitleSemantics = hideTopRowSemantics,
+                navigationIcon = navigationIcon,
+                actions = actionsRow,
+            )
+            TopAppBarLayout(
+                modifier = Modifier.clipToBounds(),
+                heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset
+                    ?: 0f),
+                navigationIconContentColor = colors.navigationIconContentColor,
+                titleContentColor = colors.titleContentColor,
+                actionIconContentColor = colors.actionIconContentColor,
+                title = title,
+                titleTextStyle = titleTextStyle,
+                titleAlpha = bottomTitleAlpha,
+                titleVerticalArrangement = Arrangement.Bottom,
+                titleHorizontalArrangement = Arrangement.Start,
+                titleBottomPadding = titleBottomPaddingPx,
+                hideTitleSemantics = hideBottomRowSemantics,
+                navigationIcon = {},
+                actions = {}
+            )
+        }
+    }
+}
+
+/**
+ * The base [Layout] for all top app bars. This function lays out a top app bar navigation icon
+ * (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and
+ * the actions are optional.
+ *
+ * @param heightPx the total height this layout is capped to
+ * @param navigationIconContentColor the content color that will be applied via a
+ * [LocalContentColor] when composing the navigation icon
+ * @param titleContentColor the color that will be applied via a [LocalContentColor] when composing
+ * the title
+ * @param actionIconContentColor the content color that will be applied via a [LocalContentColor]
+ * when composing the action icons
+ * @param title the top app bar title (header)
+ * @param titleTextStyle the title's text style
+ * @param modifier a [Modifier]
+ * @param titleAlpha the title's alpha
+ * @param titleVerticalArrangement the title's vertical arrangement
+ * @param titleHorizontalArrangement the title's horizontal arrangement
+ * @param titleBottomPadding the title's bottom padding
+ * @param hideTitleSemantics hides the title node from the semantic tree. Apply this
+ * boolean when this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics
+ * from accessibility services. This is needed to avoid having multiple titles visible to
+ * accessibility services at the same time, when animating between collapsed / expanded states.
+ * @param navigationIcon a navigation icon [Composable]
+ * @param actions actions [Composable]
+ */
+@Composable
+private fun TopAppBarLayout(
+    modifier: Modifier,
+    heightPx: Float,
+    navigationIconContentColor: Color,
+    titleContentColor: Color,
+    actionIconContentColor: Color,
+    title: @Composable () -> Unit,
+    titleTextStyle: TextStyle,
+    titleAlpha: Float,
+    titleVerticalArrangement: Arrangement.Vertical,
+    titleHorizontalArrangement: Arrangement.Horizontal,
+    titleBottomPadding: Int,
+    hideTitleSemantics: Boolean,
+    navigationIcon: @Composable () -> Unit,
+    actions: @Composable () -> Unit,
+) {
+    Layout(
+        {
+            Box(
+                Modifier
+                    .layoutId("navigationIcon")
+                    .padding(start = TopAppBarHorizontalPadding)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides navigationIconContentColor,
+                    content = navigationIcon
+                )
+            }
+            Box(
+                Modifier
+                    .layoutId("title")
+                    .padding(horizontal = TopAppBarHorizontalPadding)
+                    .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
+                    .graphicsLayer(alpha = titleAlpha)
+            ) {
+                ProvideTextStyle(value = titleTextStyle) {
+                    CompositionLocalProvider(
+                        LocalContentColor provides titleContentColor,
+                        content = title
+                    )
+                }
+            }
+            Box(
+                Modifier
+                    .layoutId("actionIcons")
+                    .padding(end = TopAppBarHorizontalPadding)
+            ) {
+                CompositionLocalProvider(
+                    LocalContentColor provides actionIconContentColor,
+                    content = actions
+                )
+            }
+        },
+        modifier = modifier
+    ) { measurables, constraints ->
+        val navigationIconPlaceable =
+            measurables.first { it.layoutId == "navigationIcon" }
+                .measure(constraints.copy(minWidth = 0))
+        val actionIconsPlaceable =
+            measurables.first { it.layoutId == "actionIcons" }
+                .measure(constraints.copy(minWidth = 0))
+
+        val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
+            constraints.maxWidth
+        } else {
+            (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
+                .coerceAtLeast(0)
+        }
+        val titlePlaceable =
+            measurables.first { it.layoutId == "title" }
+                .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
+
+        // Locate the title's baseline.
+        val titleBaseline =
+            if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
+                titlePlaceable[LastBaseline]
+            } else {
+                0
+            }
+
+        val layoutHeight = heightPx.roundToInt()
+
+        layout(constraints.maxWidth, layoutHeight) {
+            // Navigation icon
+            navigationIconPlaceable.placeRelative(
+                x = 0,
+                y = (layoutHeight - navigationIconPlaceable.height) / 2
+            )
+
+            // Title
+            titlePlaceable.placeRelative(
+                x = when (titleHorizontalArrangement) {
+                    Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
+                    Arrangement.End ->
+                        constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
+                    // Arrangement.Start.
+                    // A TopAppBarTitleInset will make sure the title is offset in case the
+                    // navigation icon is missing.
+                    else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
+                },
+                y = when (titleVerticalArrangement) {
+                    Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
+                    // Apply bottom padding from the title's baseline only when the Arrangement is
+                    // "Bottom".
+                    Arrangement.Bottom ->
+                        if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
+                        else layoutHeight - titlePlaceable.height - max(
+                            0,
+                            titleBottomPadding - titlePlaceable.height + titleBaseline
+                        )
+                    // Arrangement.Top
+                    else -> 0
+                }
+            )
+
+            // Action icons
+            actionIconsPlaceable.placeRelative(
+                x = constraints.maxWidth - actionIconsPlaceable.width,
+                y = (layoutHeight - actionIconsPlaceable.height) / 2
+            )
+        }
+    }
+}
+
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+private suspend fun settleAppBar(
+    state: TopAppBarState,
+    velocity: Float,
+    flingAnimationSpec: DecayAnimationSpec<Float>?,
+    snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+    // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+    // and just return Zero Velocity.
+    // Note that we don't check for 0f due to float precision with the collapsedFraction
+    // calculation.
+    if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+        return Velocity.Zero
+    }
+    var remainingVelocity = velocity
+    // In case there is an initial velocity that was left after a previous user fling, animate to
+    // continue the motion to expand or collapse the app bar.
+    if (flingAnimationSpec != null && abs(velocity) > 1f) {
+        var lastValue = 0f
+        AnimationState(
+            initialValue = 0f,
+            initialVelocity = velocity,
+        )
+            .animateDecay(flingAnimationSpec) {
+                val delta = value - lastValue
+                val initialHeightOffset = state.heightOffset
+                state.heightOffset = initialHeightOffset + delta
+                val consumed = abs(initialHeightOffset - state.heightOffset)
+                lastValue = value
+                remainingVelocity = this.velocity
+                // avoid rounding errors and stop if anything is unconsumed
+                if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+            }
+    }
+    // Snap if animation specs were provided.
+    if (snapAnimationSpec != null) {
+        if (state.heightOffset < 0 &&
+            state.heightOffset > state.heightOffsetLimit
+        ) {
+            AnimationState(initialValue = state.heightOffset).animateTo(
+                if (state.collapsedFraction < 0.5f) {
+                    0f
+                } else {
+                    state.heightOffsetLimit
+                },
+                animationSpec = snapAnimationSpec
+            ) { state.heightOffset = value }
+        }
+    }
+
+    return Velocity(0f, remainingVelocity)
+}
+
+// An easing function used to compute the alpha value that is applied to the top title part of a
+// Medium or Large app bar.
+private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
+
+private val ContainerHeight = 56.dp
+private val LargeTitleBottomPadding = 28.dp
+private val TopAppBarHorizontalPadding = 4.dp
+
+// A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the
+// navigation icon is missing.
+private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding
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
new file mode 100644
index 0000000..b4852e4
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.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
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+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
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+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 (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.horizontalValues())
+                .padding(top = paddingValues.calculateTopPadding())
+                .fillMaxSize(),
+        ) {
+            content(
+                bottomPadding = paddingValues.calculateBottomPadding(),
+                searchQuery = remember {
+                    derivedStateOf { viewModel.searchQuery?.text ?: "" }
+                },
+            )
+        }
+    }
+}
+
+internal class SearchScaffoldViewModel : ViewModel() {
+    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,
+) {
+    if (searchQuery != null) {
+        SearchTopAppBar(
+            query = searchQuery,
+            onQueryChange = onSearchQueryChange,
+            onClose = { onSearchQueryChange(null) },
+            actions = actions,
+        )
+    } else {
+        SettingsTopAppBar(title, scrollBehavior) {
+            SearchAction {
+                scrollBehavior.collapse()
+                onSearchQueryChange(TextFieldValue())
+            }
+            actions()
+        }
+    }
+}
+
+@Composable
+private fun SearchTopAppBar(
+    query: TextFieldValue,
+    onQueryChange: (TextFieldValue) -> Unit,
+    onClose: () -> Unit,
+    actions: @Composable RowScope.() -> Unit = {},
+) {
+    CustomizedTopAppBar(
+        title = { SearchBox(query, onQueryChange) },
+        navigationIcon = { CollapseAction(onClose) },
+        actions = {
+            if (query.text.isNotEmpty()) {
+                ClearAction { onQueryChange(TextFieldValue()) }
+            }
+            actions()
+        },
+    )
+    BackHandler { onClose() }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) {
+    val focusRequester = remember { FocusRequester() }
+    val textStyle = MaterialTheme.typography.bodyLarge
+    TextField(
+        value = query,
+        onValueChange = onQueryChange,
+        modifier = Modifier
+            .fillMaxWidth()
+            .focusRequester(focusRequester),
+        textStyle = textStyle,
+        placeholder = {
+            Text(
+                text = stringResource(R.string.abc_search_hint),
+                modifier = Modifier.alpha(SettingsOpacity.Hint),
+                style = textStyle,
+            )
+        },
+        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+        keyboardActions = KeyboardActions(onSearch = hideKeyboardAction()),
+        singleLine = true,
+        colors = TextFieldDefaults.textFieldColors(
+            containerColor = Color.Transparent,
+            focusedIndicatorColor = Color.Transparent,
+            unfocusedIndicatorColor = Color.Transparent,
+        ),
+    )
+
+    LaunchedEffect(focusRequester) {
+        focusRequester.requestFocus()
+    }
+}
+
+@Preview
+@Composable
+private fun SearchTopAppBarPreview() {
+    SettingsTheme {
+        SearchTopAppBar(query = TextFieldValue(), onQueryChange = {}, onClose = {}) {}
+    }
+}
+
+@Preview
+@Composable
+private fun SearchScaffoldPreview() {
+    SettingsTheme {
+        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 d17e464..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,20 +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.SmallTopAppBar
-import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.theme.SettingsDimension
+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.
@@ -41,37 +44,30 @@
     actions: @Composable RowScope.() -> Unit = {},
     content: @Composable (PaddingValues) -> Unit,
 ) {
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     Scaffold(
-        topBar = {
-            SmallTopAppBar(
-                title = {
-                    Text(
-                        text = title,
-                        modifier = Modifier.padding(SettingsDimension.itemPaddingAround),
-                        overflow = TextOverflow.Ellipsis,
-                        maxLines = 1,
-                    )
-                },
-                navigationIcon = { NavigateBack() },
-                actions = actions,
-                colors = settingsTopAppBarColors(),
-            )
-        },
-        content = content,
-    )
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        topBar = { SettingsTopAppBar(title, scrollBehavior, actions) },
+    ) { paddingValues ->
+        Box(Modifier.padding(paddingValues.horizontalValues())) {
+            content(paddingValues.verticalValues())
+        }
+    }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
-    containerColor = SettingsTheme.colorScheme.surfaceHeader,
-    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
-)
-
 @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
new file mode 100644
index 0000000..3311792
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.RowScope
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.runtime.Composable
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun SettingsTopAppBar(
+    title: String,
+    scrollBehavior: TopAppBarScrollBehavior,
+    actions: @Composable RowScope.() -> Unit,
+) {
+    CustomizedLargeTopAppBar(
+        title = title,
+        navigationIcon = { NavigateBack() },
+        actions = actions,
+        scrollBehavior = scrollBehavior,
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun TopAppBarScrollBehavior.collapse() {
+    with(state) {
+        heightOffset = heightOffsetLimit
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
new file mode 100644
index 0000000..1741f13
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Indeterminate linear progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun LinearLoadingBar(
+    isLoading: Boolean,
+    xOffset: Dp = 0.dp,
+    yOffset: Dp = 0.dp
+) {
+    if (isLoading) {
+        LinearProgressIndicator(
+            modifier = Modifier
+                .fillMaxWidth()
+                .absoluteOffset(xOffset, yOffset)
+        )
+    }
+}
+
+/**
+ * Indeterminate circular progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun CircularLoadingBar(isLoading: Boolean) {
+    if (isLoading) {
+        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            CircularProgressIndicator()
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
new file mode 100644
index 0000000..5d8502d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.ui
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.dp
+
+/**
+ * Determinate linear progress bar. Displays the current progress of the whole process.
+ *
+ * Rounded corner is supported and enabled by default.
+ */
+@Composable
+fun LinearProgressBar(
+    progress: Float,
+    height: Float = 4f,
+    roundedCorner: Boolean = true
+) {
+    Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) {
+        val color = MaterialTheme.colorScheme.onSurface
+        val trackColor = MaterialTheme.colorScheme.surfaceVariant
+        Canvas(
+            Modifier
+                .progressSemantics(progress)
+                .fillMaxWidth()
+                .height(height.dp)
+        ) {
+            drawLinearBarTrack(trackColor, roundedCorner)
+            drawLinearBar(progress, color, roundedCorner)
+        }
+    }
+}
+
+private fun DrawScope.drawLinearBar(
+    endFraction: Float,
+    color: Color,
+    roundedCorner: Boolean
+) {
+    val width = endFraction * size.width
+    drawRoundRect(
+        color = color,
+        size = Size(width, size.height),
+        cornerRadius = if (roundedCorner) CornerRadius(
+            size.height / 2,
+            size.height / 2
+        ) else CornerRadius.Zero,
+    )
+}
+
+private fun DrawScope.drawLinearBarTrack(
+    color: Color,
+    roundedCorner: Boolean
+) = drawLinearBar(1f, color, roundedCorner)
+
+/**
+ * Determinate circular progress bar. Displays the current progress of the whole process.
+ *
+ * Displayed in default material3 style, and rounded corner is not supported.
+ */
+@Composable
+fun CircularProgressBar(progress: Float, radius: Float = 40f) {
+    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        CircularProgressIndicator(
+            progress = progress,
+            modifier = Modifier.size(radius.dp, radius.dp)
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
new file mode 100644
index 0000000..48fec3b
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.ui
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.theme.surfaceTone
+import kotlin.math.roundToInt
+
+@Composable
+fun SettingsSlider(
+    initValue: Int,
+    modifier: Modifier = Modifier,
+    valueRange: IntRange = 0..100,
+    onValueChange: ((value: Int) -> Unit)? = null,
+    onValueChangeFinished: (() -> Unit)? = null,
+    showSteps: Boolean = false,
+) {
+    var sliderPosition by rememberSaveable { mutableStateOf(initValue.toFloat()) }
+    Slider(
+        value = sliderPosition,
+        onValueChange = {
+            sliderPosition = it
+            onValueChange?.invoke(sliderPosition.roundToInt())
+        },
+        modifier = modifier,
+        valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
+        steps = if (showSteps) (valueRange.count() - 2) else 0,
+        onValueChangeFinished = onValueChangeFinished,
+        colors = SliderDefaults.colors(
+            inactiveTrackColor = MaterialTheme.colorScheme.surfaceTone
+        )
+    )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
index 82ab0be..9831b91 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
@@ -16,30 +16,26 @@
 
 package com.android.settingslib.spa.widget.ui
 
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Switch
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SettingsSwitch(
     checked: State<Boolean?>,
     changeable: State<Boolean>,
     onCheckedChange: ((newChecked: Boolean) -> Unit)? = null,
 ) {
-    // TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is
-    //       updated to date.
     val checkedValue = checked.value
     if (checkedValue != null) {
-        Checkbox(
+        Switch(
             checked = checkedValue,
-            onCheckedChange = WrapOnSwitchWithLog(onCheckedChange),
+            onCheckedChange = wrapOnSwitchWithLog(onCheckedChange),
             enabled = changeable.value,
         )
     } else {
-        Checkbox(
+        Switch(
             checked = false,
             onCheckedChange = null,
             enabled = false,
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
new file mode 100644
index 0000000..e26bdf7
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.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 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 entryIsHighlighted = rememberSaveable { entryData.isHighlighted }
+    var localHighlighted by rememberSaveable { mutableStateOf(false) }
+    SideEffect {
+        localHighlighted = entryIsHighlighted
+    }
+
+    val backgroundColor by animateColorAsState(
+        targetValue = when {
+            localHighlighted -> 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/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
index 1ce49fa..dcfc171 100644
--- a/packages/SettingsLib/Spa/tests/Android.bp
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -26,12 +26,15 @@
 
     static_libs: [
         "SpaLib",
+        "SpaLibTestUtils",
         "androidx.test.runner",
         "androidx.test.ext.junit",
         "androidx.compose.runtime_runtime",
         "androidx.compose.ui_ui-test-junit4",
         "androidx.compose.ui_ui-test-manifest",
+        "mockito-target-minus-junit4",
         "truth-prebuilt",
     ],
     kotlincflags: ["-Xjvm-default=all"],
+    min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
index c224caf..e2db594 100644
--- a/packages/SettingsLib/Spa/tests/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.tests">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index f950e01..2d501fc 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa.tests'
-    compileSdk 33
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdk spa_min_sdk
-        targetSdk 33
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
@@ -55,17 +56,13 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
-    androidTestImplementation(project(":spa"))
-    androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
-    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$jetpack_compose_version")
-    androidTestImplementation 'com.google.truth:truth:1.1.3'
+    androidTestImplementation project(":spa")
+    androidTestImplementation project(":testutils")
+    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
+    androidTestImplementation "com.google.truth:truth:1.1.3"
+    androidTestImplementation "org.mockito:mockito-android:3.4.6"
     androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version"
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
new file mode 100644
index 0000000..9419161
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsEntryRepositoryTest {
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val spaEnvironment = SpaEnvironmentForTest(context)
+    private val entryRepository by spaEnvironment.entryRepository
+
+    @Test
+    fun testGetPageWithEntry() {
+        val pageWithEntry = entryRepository.getAllPageWithEntry()
+        assertThat(pageWithEntry.size).isEqualTo(3)
+        assertThat(
+            entryRepository.getPageWithEntry(getUniquePageId("SppHome"))
+                ?.entries?.size
+        ).isEqualTo(1)
+        assertThat(
+            entryRepository.getPageWithEntry(getUniquePageId("SppLayer1"))
+                ?.entries?.size
+        ).isEqualTo(3)
+        assertThat(
+            entryRepository.getPageWithEntry(getUniquePageId("SppLayer2"))
+                ?.entries?.size
+        ).isEqualTo(2)
+        assertThat(entryRepository.getPageWithEntry(getUniquePageId("SppWithParam"))).isNull()
+    }
+
+    @Test
+    fun testGetEntry() {
+        val entry = entryRepository.getAllEntries()
+        assertThat(entry.size).isEqualTo(7)
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId(
+                    "ROOT",
+                    SppHome.createSettingsPage(),
+                    SettingsPage.createNull(),
+                    SppHome.createSettingsPage(),
+                )
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId(
+                    "INJECT",
+                    SppLayer1.createSettingsPage(),
+                    SppHome.createSettingsPage(),
+                    SppLayer1.createSettingsPage(),
+                )
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId(
+                    "INJECT",
+                    SppLayer2.createSettingsPage(),
+                    SppLayer1.createSettingsPage(),
+                    SppLayer2.createSettingsPage(),
+                )
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+            )
+        ).isNotNull()
+        assertThat(
+            entryRepository.getEntry(
+                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
+            )
+        ).isNotNull()
+    }
+
+    @Test
+    fun testGetEntryPath() {
+        assertThat(
+            entryRepository.getEntryPathWithDisplayName(
+                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+            )
+        ).containsExactly("Layer2Entry1", "INJECT_SppLayer2", "INJECT_SppLayer1", "ROOT_SppHome")
+            .inOrder()
+
+        assertThat(
+            entryRepository.getEntryPathWithTitle(
+                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
+                "entryTitle"
+            )
+        ).containsExactly("entryTitle", "SppLayer2", "TitleLayer1", "TitleHome").inOrder()
+
+        assertThat(
+            entryRepository.getEntryPathWithDisplayName(
+                getUniqueEntryId(
+                    "INJECT",
+                    SppLayer1.createSettingsPage(),
+                    SppHome.createSettingsPage(),
+                    SppLayer1.createSettingsPage(),
+                )
+            )
+        ).containsExactly("INJECT_SppLayer1", "ROOT_SppHome").inOrder()
+
+        assertThat(
+            entryRepository.getEntryPathWithTitle(
+                getUniqueEntryId(
+                    "INJECT",
+                    SppLayer2.createSettingsPage(),
+                    SppLayer1.createSettingsPage(),
+                    SppLayer2.createSettingsPage(),
+                ),
+                "defaultTitle"
+            )
+        ).containsExactly("SppLayer2", "TitleLayer1", "TitleHome").inOrder()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
new file mode 100644
index 0000000..2017d53
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.core.os.bundleOf
+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
+
+const val INJECT_ENTRY_NAME = "INJECT"
+const val ROOT_ENTRY_NAME = "ROOT"
+
+class MacroForTest(private val pageId: String, private val entryId: String) : EntryMacro {
+    @Composable
+    override fun UiLayout() {
+        val entryData = LocalEntryDataProvider.current
+        assertThat(entryData.isHighlighted).isFalse()
+        assertThat(entryData.pageId).isEqualTo(pageId)
+        assertThat(entryData.entryId).isEqualTo(entryId)
+    }
+
+    override fun getSearchData(): EntrySearchData {
+        return EntrySearchData("myTitle")
+    }
+
+    override fun getStatusData(): EntryStatusData {
+        return EntryStatusData(isDisabled = true, isSwitchOff = true)
+    }
+}
+
+@RunWith(AndroidJUnit4::class)
+class SettingsEntryTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun testBuildBasic() {
+        val owner = SettingsPage.create("mySpp")
+        val entry = SettingsEntryBuilder.create(owner, "myEntry").build()
+        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.displayName).isEqualTo("myEntry")
+        assertThat(entry.owner.sppName).isEqualTo("mySpp")
+        assertThat(entry.owner.displayName).isEqualTo("mySpp")
+        assertThat(entry.fromPage).isNull()
+        assertThat(entry.toPage).isNull()
+        assertThat(entry.isAllowSearch).isFalse()
+        assertThat(entry.isSearchDataDynamic).isFalse()
+        assertThat(entry.hasMutableStatus).isFalse()
+    }
+
+    @Test
+    fun testBuildWithLink() {
+        val owner = SettingsPage.create("mySpp")
+        val fromPage = SettingsPage.create("fromSpp")
+        val toPage = SettingsPage.create("toSpp")
+        val entryFrom = SettingsEntryBuilder.createLinkFrom("myEntry", owner)
+            .setLink(toPage = toPage).build()
+        assertThat(entryFrom.id).isEqualTo(getUniqueEntryId("myEntry", owner, owner, toPage))
+        assertThat(entryFrom.displayName).isEqualTo("myEntry")
+        assertThat(entryFrom.fromPage!!.sppName).isEqualTo("mySpp")
+        assertThat(entryFrom.toPage!!.sppName).isEqualTo("toSpp")
+
+        val entryTo = SettingsEntryBuilder.createLinkTo("myEntry", owner)
+            .setLink(fromPage = fromPage).build()
+        assertThat(entryTo.id).isEqualTo(getUniqueEntryId("myEntry", owner, fromPage, owner))
+        assertThat(entryTo.displayName).isEqualTo("myEntry")
+        assertThat(entryTo.fromPage!!.sppName).isEqualTo("fromSpp")
+        assertThat(entryTo.toPage!!.sppName).isEqualTo("mySpp")
+    }
+
+    @Test
+    fun testBuildInject() {
+        val owner = SettingsPage.create("mySpp")
+        val entryInject = SettingsEntryBuilder.createInject(owner).build()
+        assertThat(entryInject.id).isEqualTo(
+            getUniqueEntryId(
+                INJECT_ENTRY_NAME,
+                owner,
+                toPage = owner
+            )
+        )
+        assertThat(entryInject.displayName).isEqualTo("${INJECT_ENTRY_NAME}_mySpp")
+        assertThat(entryInject.fromPage).isNull()
+        assertThat(entryInject.toPage).isNotNull()
+    }
+
+    @Test
+    fun testBuildRoot() {
+        val owner = SettingsPage.create("mySpp")
+        val entryInject = SettingsEntryBuilder.createRoot(owner, "myRootEntry").build()
+        assertThat(entryInject.id).isEqualTo(
+            getUniqueEntryId(
+                ROOT_ENTRY_NAME,
+                owner,
+                toPage = owner
+            )
+        )
+        assertThat(entryInject.displayName).isEqualTo("myRootEntry")
+        assertThat(entryInject.fromPage).isNull()
+        assertThat(entryInject.toPage).isNotNull()
+    }
+
+    @Test
+    fun testSetAttributes() {
+        val owner = SettingsPage.create("mySpp")
+        val entry = SettingsEntryBuilder.create(owner, "myEntry")
+            .setDisplayName("myEntryDisplay")
+            .setIsAllowSearch(true)
+            .setIsSearchDataDynamic(false)
+            .setHasMutableStatus(true)
+            .build()
+        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.displayName).isEqualTo("myEntryDisplay")
+        assertThat(entry.fromPage).isNull()
+        assertThat(entry.toPage).isNull()
+        assertThat(entry.isAllowSearch).isTrue()
+        assertThat(entry.isSearchDataDynamic).isFalse()
+        assertThat(entry.hasMutableStatus).isTrue()
+    }
+
+    @Test
+    fun testSetMarco() {
+        val owner = SettingsPage.create("mySpp", arguments = bundleOf("param" to "v1"))
+        val entry = SettingsEntryBuilder.create(owner, "myEntry")
+            .setMacro {
+                assertThat(it?.getString("param")).isEqualTo("v1")
+                assertThat(it?.getString("rtParam")).isEqualTo("v2")
+                assertThat(it?.getString("unknown")).isNull()
+                MacroForTest(getUniquePageId("mySpp"), getUniqueEntryId("myEntry", owner))
+            }
+            .build()
+
+        val rtArguments = bundleOf("rtParam" to "v2")
+        composeTestRule.setContent { entry.UiLayout(rtArguments) }
+        val searchData = entry.getSearchData(rtArguments)
+        val statusData = entry.getStatusData(rtArguments)
+        assertThat(searchData?.title).isEqualTo("myTitle")
+        assertThat(searchData?.keyword).isEmpty()
+        assertThat(statusData?.isDisabled).isTrue()
+        assertThat(statusData?.isSwitchOff).isTrue()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
new file mode 100644
index 0000000..6c0c652
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsPageProviderRepositoryTest {
+    @Test
+    fun getStartPageTest() {
+        val sppRepoEmpty = SettingsPageProviderRepository(emptyList(), emptyList())
+        assertThat(sppRepoEmpty.getDefaultStartPage()).isEqualTo("")
+        assertThat(sppRepoEmpty.getAllRootPages()).isEmpty()
+
+        val sppRepoNull =
+            SettingsPageProviderRepository(emptyList(), listOf(SettingsPage.createNull()))
+        assertThat(sppRepoNull.getDefaultStartPage()).isEqualTo("NULL")
+        assertThat(sppRepoNull.getAllRootPages()).contains(SettingsPage.createNull())
+
+        val rootPage1 = SettingsPage.create(name = "Spp1", displayName = "Spp1")
+        val rootPage2 = SettingsPage.create(name = "Spp2", displayName = "Spp2")
+        val sppRepo = SettingsPageProviderRepository(emptyList(), listOf(rootPage1, rootPage2))
+        val allRoots = sppRepo.getAllRootPages()
+        assertThat(sppRepo.getDefaultStartPage()).isEqualTo("Spp1")
+        assertThat(allRoots.size).isEqualTo(2)
+        assertThat(allRoots).contains(rootPage1)
+        assertThat(allRoots).contains(rootPage2)
+    }
+
+    @Test
+    fun getProviderTest() {
+        val sppRepoEmpty = SettingsPageProviderRepository(emptyList(), emptyList())
+        assertThat(sppRepoEmpty.getAllProviders()).isEmpty()
+        assertThat(sppRepoEmpty.getProviderOrNull("Spp")).isNull()
+
+        val sppRepo = SettingsPageProviderRepository(listOf(
+            object : SettingsPageProvider {
+                override val name = "Spp"
+            }
+        ), emptyList())
+        assertThat(sppRepo.getAllProviders().size).isEqualTo(1)
+        assertThat(sppRepo.getProviderOrNull("Spp")).isNotNull()
+        assertThat(sppRepo.getProviderOrNull("SppUnknown")).isNull()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
new file mode 100644
index 0000000..7097a5d
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import android.content.Context
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsPageTest {
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val spaLogger = SpaLoggerForTest()
+    private val spaEnvironment = SpaEnvironmentForTest(context, logger = spaLogger)
+
+    @Test
+    fun testNullPage() {
+        val page = SettingsPage.createNull()
+        assertThat(page.id).isEqualTo(getUniquePageId("NULL"))
+        assertThat(page.sppName).isEqualTo("NULL")
+        assertThat(page.displayName).isEqualTo("NULL")
+        assertThat(page.buildRoute()).isEqualTo("NULL")
+        assertThat(page.isCreateBy("NULL")).isTrue()
+        assertThat(page.isCreateBy("Spp")).isFalse()
+        assertThat(page.hasRuntimeParam()).isFalse()
+        assertThat(page.isBrowsable(context, MockActivity::class.java)).isFalse()
+        assertThat(page.createBrowseIntent(context, MockActivity::class.java)).isNull()
+        assertThat(page.createBrowseAdbCommand(context, MockActivity::class.java)).isNull()
+    }
+
+    @Test
+    fun testRegularPage() {
+        val page = SettingsPage.create("mySpp", "SppDisplayName")
+        assertThat(page.id).isEqualTo(getUniquePageId("mySpp"))
+        assertThat(page.sppName).isEqualTo("mySpp")
+        assertThat(page.displayName).isEqualTo("SppDisplayName")
+        assertThat(page.buildRoute()).isEqualTo("mySpp")
+        assertThat(page.isCreateBy("NULL")).isFalse()
+        assertThat(page.isCreateBy("mySpp")).isTrue()
+        assertThat(page.hasRuntimeParam()).isFalse()
+        assertThat(page.isBrowsable(context, MockActivity::class.java)).isTrue()
+        assertThat(page.createBrowseIntent(context, MockActivity::class.java)).isNotNull()
+        assertThat(page.createBrowseAdbCommand(context, MockActivity::class.java)).contains(
+            "-e spaActivityDestination mySpp"
+        )
+    }
+
+    @Test
+    fun testParamPage() {
+        val arguments = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+        )
+        val page = spaEnvironment.createPage("SppWithParam", arguments)
+        assertThat(page.id).isEqualTo(getUniquePageId("SppWithParam", listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        ), arguments))
+        assertThat(page.sppName).isEqualTo("SppWithParam")
+        assertThat(page.displayName).isEqualTo("SppWithParam")
+        assertThat(page.buildRoute()).isEqualTo("SppWithParam/myStr/10")
+        assertThat(page.isCreateBy("SppWithParam")).isTrue()
+        assertThat(page.hasRuntimeParam()).isFalse()
+        assertThat(page.isBrowsable(context, MockActivity::class.java)).isTrue()
+        assertThat(page.createBrowseIntent(context, MockActivity::class.java)).isNotNull()
+        assertThat(page.createBrowseAdbCommand(context, MockActivity::class.java)).contains(
+            "-e spaActivityDestination SppWithParam/myStr/10"
+        )
+    }
+
+    @Test
+    fun testRtParamPage() {
+        val arguments = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+            "rt_param" to "rtStr",
+        )
+        val page = spaEnvironment.createPage("SppWithRtParam", arguments)
+        assertThat(page.id).isEqualTo(getUniquePageId("SppWithRtParam", listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+            navArgument("rt_param") { type = NavType.StringType },
+        ), arguments))
+        assertThat(page.sppName).isEqualTo("SppWithRtParam")
+        assertThat(page.displayName).isEqualTo("SppWithRtParam")
+        assertThat(page.buildRoute()).isEqualTo("SppWithRtParam/myStr/10/rtStr")
+        assertThat(page.isCreateBy("SppWithRtParam")).isTrue()
+        assertThat(page.hasRuntimeParam()).isTrue()
+        assertThat(page.isBrowsable(context, MockActivity::class.java)).isFalse()
+        assertThat(page.createBrowseIntent(context, MockActivity::class.java)).isNull()
+        assertThat(page.createBrowseAdbCommand(context, MockActivity::class.java)).isNull()
+    }
+
+    @Test
+    fun testPageEvent() {
+        spaLogger.reset()
+        SpaEnvironmentFactory.reset(spaEnvironment)
+        val page = spaEnvironment.createPage("SppHome")
+        page.enterPage()
+        page.leavePage()
+        page.enterPage()
+        assertThat(page.createBrowseIntent()).isNotNull()
+        assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK))
+            .isEqualTo(2)
+        assertThat(spaLogger.getEventCount(page.id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK))
+            .isEqualTo(1)
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt
new file mode 100644
index 0000000..b8731a3
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import com.android.settingslib.spa.framework.BrowseActivity
+
+class SpaLoggerForTest : SpaLogger {
+    data class MsgCountKey(val msg: String, val category: LogCategory)
+    data class EventCountKey(val id: String, val event: LogEvent, val category: LogCategory)
+
+    private val messageCount: MutableMap<MsgCountKey, Int> = mutableMapOf()
+    private val eventCount: MutableMap<EventCountKey, Int> = mutableMapOf()
+
+    override fun message(tag: String, msg: String, category: LogCategory) {
+        val key = MsgCountKey("[$tag]$msg", category)
+        messageCount[key] = messageCount.getOrDefault(key, 0) + 1
+    }
+
+    override fun event(id: String, event: LogEvent, category: LogCategory, details: String?) {
+        val key = EventCountKey(id, event, category)
+        eventCount[key] = eventCount.getOrDefault(key, 0) + 1
+    }
+
+    fun getMessageCount(tag: String, msg: String, category: LogCategory): Int {
+        val key = MsgCountKey("[$tag]$msg", category)
+        return messageCount.getOrDefault(key, 0)
+    }
+
+    fun getEventCount(id: String, event: LogEvent, category: LogCategory): Int {
+        val key = EventCountKey(id, event, category)
+        return eventCount.getOrDefault(key, 0)
+    }
+
+    fun reset() {
+        messageCount.clear()
+        eventCount.clear()
+    }
+}
+
+class MockActivity : BrowseActivity()
+
+object SppHome : SettingsPageProvider {
+    override val name = "SppHome"
+
+    override fun getTitle(arguments: Bundle?): String {
+        return "TitleHome"
+    }
+
+    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+        val owner = this.createSettingsPage()
+        return listOf(
+            SppLayer1.buildInject().setLink(fromPage = owner).build(),
+        )
+    }
+}
+
+object SppLayer1 : SettingsPageProvider {
+    override val name = "SppLayer1"
+
+    override fun getTitle(arguments: Bundle?): String {
+        return "TitleLayer1"
+    }
+
+    fun buildInject(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(this.createSettingsPage())
+    }
+
+    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+        val owner = this.createSettingsPage()
+        return listOf(
+            SettingsEntryBuilder.create(owner, "Layer1Entry1").build(),
+            SppLayer2.buildInject().setLink(fromPage = owner).build(),
+            SettingsEntryBuilder.create(owner, "Layer1Entry2").build(),
+        )
+    }
+}
+
+object SppLayer2 : SettingsPageProvider {
+    override val name = "SppLayer2"
+
+    fun buildInject(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(this.createSettingsPage())
+    }
+
+    override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
+        val owner = this.createSettingsPage()
+        return listOf(
+            SettingsEntryBuilder.create(owner, "Layer2Entry1").build(),
+            SettingsEntryBuilder.create(owner, "Layer2Entry2").build(),
+        )
+    }
+}
+
+class SpaEnvironmentForTest(
+    context: Context,
+    override val browseActivityClass: Class<out Activity>? = MockActivity::class.java,
+    override val logger: SpaLogger = SpaLoggerForTest()
+) : SpaEnvironment(context) {
+
+    override val pageProviderRepository = lazy {
+        SettingsPageProviderRepository(
+            listOf(
+                SppHome, SppLayer1, SppLayer2,
+                object : SettingsPageProvider {
+                    override val name = "SppWithParam"
+                    override val parameter = listOf(
+                        navArgument("string_param") { type = NavType.StringType },
+                        navArgument("int_param") { type = NavType.IntType },
+                    )
+                },
+                object : SettingsPageProvider {
+                    override val name = "SppWithRtParam"
+                    override val parameter = listOf(
+                        navArgument("string_param") { type = NavType.StringType },
+                        navArgument("int_param") { type = NavType.IntType },
+                        navArgument("rt_param") { type = NavType.StringType },
+                    )
+                },
+            ),
+            listOf(SettingsPage.create("SppHome"))
+        )
+    }
+
+    fun createPage(sppName: String, arguments: Bundle? = null): SettingsPage {
+        return pageProviderRepository.value
+            .getProviderOrNull(sppName)!!.createSettingsPage(arguments)
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/UniqueIdHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/UniqueIdHelper.kt
new file mode 100644
index 0000000..93f9afe
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/UniqueIdHelper.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.settingslib.spa.framework.common
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.normalize
+
+fun getUniquePageId(
+    name: String,
+    parameter: List<NamedNavArgument> = emptyList(),
+    arguments: Bundle? = null
+): String {
+    val normArguments = parameter.normalize(arguments)
+    return "$name:${normArguments?.toString()}".toHashId()
+}
+
+fun getUniquePageId(page: SettingsPage): String {
+    return getUniquePageId(page.sppName, page.parameter, page.arguments)
+}
+
+fun getUniqueEntryId(
+    name: String,
+    owner: SettingsPage,
+    fromPage: SettingsPage? = null,
+    toPage: SettingsPage? = null
+): String {
+    val ownerId = getUniquePageId(owner)
+    val fromId = if (fromPage == null) "null" else getUniquePageId(fromPage)
+    val toId = if (toPage == null) "null" else getUniquePageId(toPage)
+    return "$name:$ownerId($fromId-$toId)".toHashId()
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt
new file mode 100644
index 0000000..c94572b
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class OverridableFlowTest {
+
+    @Test
+    fun noOverride() = runTest {
+        val overridableFlow = OverridableFlow(flowOf(true))
+
+        launch {
+            val values = collectValues(overridableFlow.flow)
+            assertThat(values).containsExactly(true)
+        }
+    }
+
+    @Test
+    fun whenOverride() = runTest {
+        val overridableFlow = OverridableFlow(flowOf(true))
+
+        overridableFlow.override(false)
+
+        launch {
+            val values = collectValues(overridableFlow.flow)
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+    }
+
+    private suspend fun <T> collectValues(flow: Flow<T>): List<T> = withTimeout(500) {
+        val flowValues = mutableListOf<T>()
+        flow.toList(flowValues)
+        flowValues
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
new file mode 100644
index 0000000..2ff3039
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.theme
+
+import android.content.Context
+import android.content.res.Resources
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.font.FontFamily
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class SettingsThemeTest {
+    @get:Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Mock
+    private lateinit var context: Context
+
+    @Mock
+    private lateinit var resources: Resources
+
+    private var nextMockResId = 1
+
+    @Before
+    fun setUp() {
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getString(anyInt())).thenReturn("")
+    }
+
+    private fun mockAndroidConfig(configName: String, configValue: String) {
+        whenever(resources.getIdentifier(configName, "string", "android"))
+            .thenReturn(nextMockResId)
+        whenever(resources.getString(nextMockResId)).thenReturn(configValue)
+        nextMockResId++
+    }
+
+    @Test
+    fun noFontFamilyConfig_useDefaultFontFamily() {
+        val fontFamily = getFontFamily()
+
+        assertThat(fontFamily.headlineLarge.fontFamily).isSameInstanceAs(FontFamily.Default)
+        assertThat(fontFamily.bodyLarge.fontFamily).isSameInstanceAs(FontFamily.Default)
+    }
+
+    @Test
+    fun hasFontFamilyConfig_useConfiguredFontFamily() {
+        mockAndroidConfig("config_headlineFontFamily", "HeadlineNormal")
+        mockAndroidConfig("config_headlineFontFamilyMedium", "HeadlineMedium")
+        mockAndroidConfig("config_bodyFontFamily", "BodyNormal")
+        mockAndroidConfig("config_bodyFontFamilyMedium", "BodyMedium")
+
+        val fontFamily = getFontFamily()
+
+        val headlineFontFamily = fontFamily.headlineLarge.fontFamily.toString()
+        assertThat(headlineFontFamily).contains("HeadlineNormal")
+        assertThat(headlineFontFamily).contains("HeadlineMedium")
+        val bodyFontFamily = fontFamily.bodyLarge.fontFamily.toString()
+        assertThat(bodyFontFamily).contains("BodyNormal")
+        assertThat(bodyFontFamily).contains("BodyMedium")
+    }
+
+    private fun getFontFamily(): Typography {
+        lateinit var typography: Typography
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                SettingsTheme {
+                    typography = MaterialTheme.typography
+                }
+            }
+        }
+        return typography
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt
new file mode 100644
index 0000000..48ebd8d
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/ParameterTest.kt
@@ -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.settingslib.spa.framework.util
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ParameterTest {
+    @Test
+    fun navRouteTest() {
+        val navArguments = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        )
+
+        val route = navArguments.navRoute()
+        assertThat(route).isEqualTo("/{string_param}/{int_param}")
+    }
+
+    @Test
+    fun navLinkTest() {
+        val navArguments = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        )
+
+        val unsetAllLink = navArguments.navLink()
+        assertThat(unsetAllLink).isEqualTo("/[unset]/[unset]")
+
+        val setAllLink = navArguments.navLink(
+            bundleOf(
+                "string_param" to "myStr",
+                "int_param" to 10,
+            )
+        )
+        assertThat(setAllLink).isEqualTo("/myStr/10")
+
+        val setUnknownLink = navArguments.navLink(
+            bundleOf(
+                "string_param" to "myStr",
+                "int_param" to 10,
+                "unknown_param" to "unknown",
+            )
+        )
+        assertThat(setUnknownLink).isEqualTo("/myStr/10")
+
+        val setWrongTypeLink = navArguments.navLink(
+            bundleOf(
+                "string_param" to "myStr",
+                "int_param" to "wrongStr",
+            )
+        )
+        assertThat(setWrongTypeLink).isEqualTo("/myStr/0")
+    }
+
+    @Test
+    fun normalizeTest() {
+        val emptyArguments = emptyList<NamedNavArgument>()
+        assertThat(emptyArguments.normalize()).isNull()
+
+        val navArguments = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+            navArgument("rt_param") { type = NavType.StringType },
+        )
+
+        val emptyParam = navArguments.normalize()
+        assertThat(emptyParam).isNotNull()
+        assertThat(emptyParam.toString()).isEqualTo(
+            "Bundle[{rt_param=null, unset_string_param=null, unset_int_param=null}]"
+        )
+
+        val setPartialParam = navArguments.normalize(
+            bundleOf(
+                "string_param" to "myStr",
+                "rt_param" to "rtStr",
+            )
+        )
+        assertThat(setPartialParam).isNotNull()
+        assertThat(setPartialParam.toString()).isEqualTo(
+            "Bundle[{rt_param=null, string_param=myStr, unset_int_param=null}]"
+        )
+
+        val setAllParam = navArguments.normalize(
+            bundleOf(
+                "string_param" to "myStr",
+                "int_param" to 10,
+                "rt_param" to "rtStr",
+            )
+        )
+        assertThat(setAllParam).isNotNull()
+        assertThat(setAllParam.toString()).isEqualTo(
+            "Bundle[{rt_param=null, int_param=10, string_param=myStr}]"
+        )
+    }
+
+    @Test
+    fun getArgTest() {
+        val navArguments = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        )
+
+        assertThat(
+            navArguments.getStringArg(
+                "string_param", bundleOf(
+                    "string_param" to "myStr",
+                )
+            )
+        ).isEqualTo("myStr")
+
+        assertThat(
+            navArguments.getStringArg(
+                "string_param", bundleOf(
+                    "string_param" to 10,
+                )
+            )
+        ).isNull()
+
+        assertThat(
+            navArguments.getStringArg(
+                "unknown_param", bundleOf(
+                    "string_param" to "myStr",
+                )
+            )
+        ).isNull()
+
+        assertThat(navArguments.getStringArg("string_param")).isNull()
+
+        assertThat(
+            navArguments.getIntArg(
+                "int_param", bundleOf(
+                    "int_param" to 10,
+                )
+            )
+        ).isEqualTo(10)
+
+        assertThat(
+            navArguments.getIntArg(
+                "int_param", bundleOf(
+                    "int_param" to "10",
+                )
+            )
+        ).isEqualTo(0)
+
+        assertThat(
+            navArguments.getIntArg(
+                "unknown_param", bundleOf(
+                    "int_param" to 10,
+                )
+            )
+        ).isNull()
+
+        assertThat(navArguments.getIntArg("int_param")).isNull()
+    }
+
+    @Test
+    fun isRuntimeParamTest() {
+        val regularParam = navArgument("regular_param") { type = NavType.StringType }
+        val rtParam = navArgument("rt_param") { type = NavType.StringType }
+        assertThat(regularParam.isRuntimeParam()).isFalse()
+        assertThat(rtParam.isRuntimeParam()).isTrue()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ChartTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ChartTest.kt
new file mode 100644
index 0000000..fa7a98a
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ChartTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.chart.BarChart
+import com.android.settingslib.spa.widget.chart.BarChartData
+import com.android.settingslib.spa.widget.chart.LineChart
+import com.android.settingslib.spa.widget.chart.LineChartData
+import com.android.settingslib.spa.widget.chart.PieChart
+import com.android.settingslib.spa.widget.chart.PieChartData
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ChartTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val Chart = SemanticsPropertyKey<String>("Chart")
+    private var SemanticsPropertyReceiver.chart by Chart
+    private fun hasChart(chart: String): SemanticsMatcher =
+        SemanticsMatcher.expectValue(Chart, chart)
+
+    @Test
+    fun line_chart_displayed() {
+        composeTestRule.setContent {
+            LineChart(
+                chartDataList = listOf(
+                    LineChartData(x = 0f, y = 0f),
+                    LineChartData(x = 1f, y = 0.1f),
+                    LineChartData(x = 2f, y = 0.2f),
+                    LineChartData(x = 3f, y = 0.7f),
+                    LineChartData(x = 4f, y = 0.9f),
+                    LineChartData(x = 5f, y = 1.0f),
+                    LineChartData(x = 6f, y = 0.8f),
+                ),
+                modifier = Modifier.semantics { chart = "line" }
+            )
+        }
+
+        composeTestRule.onNode(hasChart("line")).assertIsDisplayed()
+    }
+
+    @Test
+    fun bar_chart_displayed() {
+        composeTestRule.setContent {
+            BarChart(
+                chartDataList = listOf(
+                    BarChartData(x = 0f, y = 12f),
+                    BarChartData(x = 1f, y = 5f),
+                    BarChartData(x = 2f, y = 21f),
+                    BarChartData(x = 3f, y = 5f),
+                    BarChartData(x = 4f, y = 10f),
+                    BarChartData(x = 5f, y = 9f),
+                    BarChartData(x = 6f, y = 1f),
+                ),
+                yAxisMaxValue = 30f,
+                modifier = Modifier.semantics { chart = "bar" }
+            )
+        }
+
+        composeTestRule.onNode(hasChart("bar")).assertIsDisplayed()
+    }
+
+    @Test
+    fun pie_chart_displayed() {
+        composeTestRule.setContent {
+            PieChart(
+                chartDataList = listOf(
+                    PieChartData(label = "Settings", value = 20f),
+                    PieChartData(label = "Chrome", value = 5f),
+                    PieChartData(label = "Gmail", value = 3f),
+                ),
+                centerText = "Today",
+                modifier = Modifier.semantics { chart = "pie" }
+            )
+        }
+
+        composeTestRule.onNode(hasChart("pie")).assertIsDisplayed()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/SettingsSliderTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/SettingsSliderTest.kt
deleted file mode 100644
index 1d95e33..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/SettingsSliderTest.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.widget
-
-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 SettingsSliderTest {
-    @get:Rule
-    val composeTestRule = createComposeRule()
-
-    @Test
-    fun title_displayed() {
-        composeTestRule.setContent {
-            SettingsSlider(object : SettingsSliderModel {
-                override val title = "Slider"
-                override val initValue = 40
-            })
-        }
-
-        composeTestRule.onNodeWithText("Slider").assertIsDisplayed()
-    }
-
-    // TODO: Add more unit tests for SettingsSlider widget.
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
new file mode 100644
index 0000000..5611f8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.preference
+
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
+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 ProgressBarPreferenceTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+    }
+
+    @Test
+    fun data_displayed() {
+        composeTestRule.setContent {
+            ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            }, data = "Data")
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Data").assertIsDisplayed()
+    }
+
+    @Test
+    fun progressBar_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..1f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
new file mode 100644
index 0000000..3e5dd52
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.preference
+
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
+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 SliderPreferenceTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            SliderPreference(object : SliderPreferenceModel {
+                override val title = "Slider"
+                override val initValue = 40
+            })
+        }
+
+        composeTestRule.onNodeWithText("Slider").assertIsDisplayed()
+    }
+
+    @Test
+    fun slider_displayed() {
+        composeTestRule.setContent {
+            SliderPreference(object : SliderPreferenceModel {
+                override val title = "Slider"
+                override val initValue = 40
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..100f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(40f)).assertIsDisplayed()
+    }
+}
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
new file mode 100644
index 0000000..c3e1d54
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
@@ -0,0 +1,146 @@
+/*
+ * 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 android.content.Context
+import androidx.appcompat.R
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.State
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.core.app.ApplicationProvider
+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 SearchScaffoldTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun initialState_titleIsDisplayed() {
+        composeTestRule.setContent {
+            SearchScaffold(title = TITLE) { _, _ -> }
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun initialState_clearButtonNotExist() {
+        setContent()
+
+        onClearButton().assertDoesNotExist()
+    }
+
+    @Test
+    fun initialState_searchQueryIsEmpty() {
+        val searchQuery = setContent()
+
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canEnterSearchMode() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+
+        composeTestRule.onNodeWithText(TITLE).assertDoesNotExist()
+        onSearchHint().assertIsDisplayed()
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canExitSearchMode() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(R.string.abc_toolbar_collapse_description)
+        ).performClick()
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+        onSearchHint().assertDoesNotExist()
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canEnterSearchQuery() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        onSearchHint().performTextInput(QUERY)
+
+        onClearButton().assertIsDisplayed()
+        assertThat(searchQuery.value).isEqualTo(QUERY)
+    }
+
+    @Test
+    fun canClearSearchQuery() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        onSearchHint().performTextInput(QUERY)
+        onClearButton().performClick()
+
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    private fun setContent(): State<String> {
+        lateinit var actualSearchQuery: State<String>
+        composeTestRule.setContent {
+            SearchScaffold(title = TITLE) { _, searchQuery ->
+                SideEffect {
+                    actualSearchQuery = searchQuery
+                }
+            }
+        }
+        return actualSearchQuery
+    }
+
+    private fun clickSearchButton() {
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(R.string.search_menu_title)
+        ).performClick()
+    }
+
+    private fun onSearchHint() = composeTestRule.onNodeWithText(
+        context.getString(R.string.abc_search_hint)
+    )
+
+    private fun onClearButton() = composeTestRule.onNodeWithContentDescription(
+        context.getString(R.string.abc_searchview_description_clear)
+    )
+
+    private companion object {
+        const val TITLE = "title"
+        const val QUERY = "query"
+    }
+}
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/SettingsPagerKtTest.kt
deleted file mode 100644
index 0c84eac..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.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
-import androidx.compose.ui.test.assertIsSelected
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.widget.ui.SettingsTitle
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SettingsPagerKtTest {
-    @get:Rule
-    val composeTestRule = createComposeRule()
-
-    @Test
-    fun twoPage_initialState() {
-        composeTestRule.setContent {
-            TestTwoPage()
-        }
-
-        composeTestRule.onNodeWithText("Personal").assertIsSelected()
-        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
-        composeTestRule.onNodeWithText("Work").assertIsNotSelected()
-        composeTestRule.onNodeWithText("Page 1").assertIsNotDisplayed()
-    }
-
-    @Test
-    fun twoPage_afterSwitch() {
-        composeTestRule.setContent {
-            TestTwoPage()
-        }
-
-        composeTestRule.onNodeWithText("Work").performClick()
-
-        composeTestRule.onNodeWithText("Personal").assertIsNotSelected()
-        composeTestRule.onNodeWithText("Page 0").assertIsNotDisplayed()
-        composeTestRule.onNodeWithText("Work").assertIsSelected()
-        composeTestRule.onNodeWithText("Page 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun onePage_initialState() {
-        composeTestRule.setContent {
-            SettingsPager(listOf("Personal")) {
-                SettingsTitle(title = "Page $it")
-            }
-        }
-
-        composeTestRule.onNodeWithText("Personal").assertDoesNotExist()
-        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
-        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
-    }
-}
-
-@Composable
-private fun TestTwoPage() {
-    SettingsPager(listOf("Personal", "Work")) {
-        SettingsTitle(title = "Page $it")
-    }
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt
new file mode 100644
index 0000000..0c745d5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsPagerTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun twoPage_initialState() {
+        setTwoPagesContent()
+
+        composeTestRule.onNodeWithText("Personal").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Work").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 1").assertIsNotDisplayed()
+    }
+
+    @Test
+    fun twoPage_afterSwitch() {
+        setTwoPagesContent()
+
+        composeTestRule.onNodeWithText("Work").performClick()
+
+        composeTestRule.onNodeWithText("Personal").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 0").assertIsNotDisplayed()
+        composeTestRule.onNodeWithText("Work").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun onePage_initialState() {
+        composeTestRule.setContent {
+            SettingsPager(listOf("Personal")) {
+                SettingsTitle(title = "Page $it")
+            }
+        }
+
+        composeTestRule.onNodeWithText("Personal").assertDoesNotExist()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
+    }
+
+    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/Spa/testutils/Android.bp b/packages/SettingsLib/Spa/testutils/Android.bp
new file mode 100644
index 0000000..68ad414
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+    name: "SpaLibTestUtils",
+
+    srcs: ["src/**/*.kt"],
+
+    static_libs: [
+        "mockito-target-minus-junit4",
+    ],
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
+    min_sdk_version: "31",
+}
diff --git a/packages/SettingsLib/Spa/testutils/AndroidManifest.xml b/packages/SettingsLib/Spa/testutils/AndroidManifest.xml
new file mode 100644
index 0000000..1aa7782
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa.testutils">
+    <uses-sdk android:minSdkVersion="21"/>
+</manifest>
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
new file mode 100644
index 0000000..3e50b29
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id 'com.android.library'
+    id 'kotlin-android'
+}
+
+android {
+    compileSdk TARGET_SDK
+    buildToolsVersion = BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        minSdk MIN_SDK
+        targetSdk TARGET_SDK
+    }
+
+    sourceSets {
+        main {
+            kotlin {
+                srcDir "src"
+            }
+            manifest.srcFile "AndroidManifest.xml"
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+        freeCompilerArgs = ["-Xjvm-default=all"]
+    }
+}
+
+dependencies {
+    api "org.mockito:mockito-android:3.4.6"
+}
diff --git a/packages/SettingsLib/Spa/testutils/src/MockitoHelper.kt b/packages/SettingsLib/Spa/testutils/src/MockitoHelper.kt
new file mode 100644
index 0000000..5ba54c1
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/MockitoHelper.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.testutils
+
+import org.mockito.Mockito
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is
+ * returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any(type)
+
+inline fun <reified T> any(): T = any(T::class.java)
diff --git a/packages/SettingsLib/Spa/testutils/src/SpaTest.kt b/packages/SettingsLib/Spa/testutils/src/SpaTest.kt
new file mode 100644
index 0000000..a397bb4
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/SpaTest.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.testutils
+
+import java.util.concurrent.TimeoutException
+
+/**
+ * Blocks until the given condition is satisfied.
+ */
+fun waitUntil(timeoutMillis: Long = 1000, condition: () -> Boolean) {
+    val startTime = System.currentTimeMillis()
+    while (!condition()) {
+        // Let Android run measure, draw and in general any other async operations.
+        Thread.sleep(10)
+        if (System.currentTimeMillis() - startTime > timeoutMillis) {
+            throw TimeoutException("Condition still not satisfied after $timeoutMillis ms")
+        }
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 18ae09ea..4a7418f 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -30,7 +30,6 @@
     ],
     kotlincflags: [
         "-Xjvm-default=all",
-        "-opt-in=kotlin.RequiresOptIn",
     ],
 }
 
diff --git a/packages/SettingsLib/SpaPrivileged/res/values-en-rCA/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..bc88528
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/res/values-en-rCA/strings.xml
@@ -0,0 +1,26 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="no_applications" msgid="5800789569715871963">"No apps."</string>
+    <string name="menu_show_system" msgid="906304605807554788">"Show system"</string>
+    <string name="menu_hide_system" msgid="374571689914923020">"Hide system"</string>
+    <string name="app_permission_summary_allowed" msgid="6115213465364138103">"Allowed"</string>
+    <string name="app_permission_summary_not_allowed" msgid="58396132188553920">"Not allowed"</string>
+    <string name="version_text" msgid="4001669804596458577">"version <xliff:g id="VERSION_NUM">%1$s</xliff:g>"</string>
+</resources>
diff --git a/packages/SettingsLib/SpaPrivileged/res/values-en-rXC/strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..c395286
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/res/values-en-rXC/strings.xml
@@ -0,0 +1,26 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="no_applications" msgid="5800789569715871963">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‎‎‎‎‎‏‎‎‎‏‎‏‏‎‏‏‎‎‎‎‏‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‎‎‎‏‏‎‏‏‎‏‏‎No apps.‎‏‎‎‏‎"</string>
+    <string name="menu_show_system" msgid="906304605807554788">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‏‏‏‎‏‏‏‎‏‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‎‎Show system‎‏‎‎‏‎"</string>
+    <string name="menu_hide_system" msgid="374571689914923020">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‎‏‎‎‏‏‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‎‏‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‎‎‎Hide system‎‏‎‎‏‎"</string>
+    <string name="app_permission_summary_allowed" msgid="6115213465364138103">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‏‎‏‏‎‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‏‎‎‎‎‏‎‎‎‎‏‏‏‎‏‏‏‎Allowed‎‏‎‎‏‎"</string>
+    <string name="app_permission_summary_not_allowed" msgid="58396132188553920">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‎‎‏‏‎‎‏‏‏‏‎‏‏‏‎‏‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‏‏‎‏‎‏‏‎‏‏‎‎‎‎‎‎‏‎‏‏‎‎‎‎‎‎‎Not allowed‎‏‎‎‏‎"</string>
+    <string name="version_text" msgid="4001669804596458577">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‎‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‏‏‏‏‎‏‎‎‎‏‎‏‎‎‎‏‎version ‎‏‎‎‏‏‎<xliff:g id="VERSION_NUM">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+</resources>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
index 1dc52cb..bb1cd6e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
@@ -1,15 +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.spaprivileged.framework.common
 
+import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.app.admin.DevicePolicyManager
 import android.app.usage.StorageStatsManager
+import android.apphibernation.AppHibernationManager
 import android.content.Context
+import android.content.pm.CrossProfileApps
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.os.UserHandle
 import android.os.UserManager
+import android.permission.PermissionControllerManager
 
-/** The [UserManager] instance. */
-val Context.userManager get() = getSystemService(UserManager::class.java)!!
+/** The [AlarmManager] instance. */
+val Context.alarmManager get() = getSystemService(AlarmManager::class.java)!!
+
+/** The [AppHibernationManager] instance. */
+val Context.appHibernationManager get() = getSystemService(AppHibernationManager::class.java)!!
+
+/** The [AppOpsManager] instance. */
+val Context.appOpsManager get() = getSystemService(AppOpsManager::class.java)!!
+
+/** The [CrossProfileApps] instance. */
+val Context.crossProfileApps get() = getSystemService(CrossProfileApps::class.java)!!
 
 /** The [DevicePolicyManager] instance. */
 val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::class.java)!!
 
+/** The [DomainVerificationManager] instance. */
+val Context.domainVerificationManager
+    get() = getSystemService(DomainVerificationManager::class.java)!!
+
+/** The [PermissionControllerManager] instance. */
+val Context.permissionControllerManager
+    get() = getSystemService(PermissionControllerManager::class.java)!!
+
 /** The [StorageStatsManager] instance. */
 val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!!
+
+/** The [UserManager] instance. */
+val Context.userManager get() = getSystemService(UserManager::class.java)!!
+
+/** Gets a new [Context] for the given [UserHandle]. */
+fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index a7de4ce..b2ea4a0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -23,7 +23,6 @@
 import android.os.UserHandle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
@@ -34,24 +33,25 @@
  */
 @Composable
 fun DisposableBroadcastReceiverAsUser(
-    userId: Int,
     intentFilter: IntentFilter,
+    userHandle: UserHandle,
+    onStart: () -> Unit = {},
     onReceive: (Intent) -> Unit,
 ) {
-    val broadcastReceiver = remember {
-        object : BroadcastReceiver() {
+    val context = LocalContext.current
+    val lifecycleOwner = LocalLifecycleOwner.current
+    DisposableEffect(lifecycleOwner) {
+        val broadcastReceiver = object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 onReceive(intent)
             }
         }
-    }
-    val context = LocalContext.current
-    val lifecycleOwner = LocalLifecycleOwner.current
-    DisposableEffect(lifecycleOwner) {
         val observer = LifecycleEventObserver { _, event ->
             if (event == Lifecycle.Event.ON_START) {
                 context.registerReceiverAsUser(
-                    broadcastReceiver, UserHandle.of(userId), intentFilter, null, null)
+                    broadcastReceiver, userHandle, intentFilter, null, null
+                )
+                onStart()
             } else if (event == Lifecycle.Event.ON_STOP) {
                 context.unregisterReceiver(broadcastReceiver)
             }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt
index 373b57f..a7122d0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt
@@ -54,6 +54,14 @@
     )
 
     /**
+     * Gets the group title of this item.
+     *
+     * Note: Items should be sorted by group in [getComparator] first, this [getGroupTitle] will not
+     * change the list order.
+     */
+    fun getGroupTitle(option: Int, record: T): String? = null
+
+    /**
      * Gets the summary for the given app record.
      *
      * @return null if no summary should be displayed.
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index ee89003..487dbcb 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -21,13 +21,10 @@
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
 
 /**
  * The config used to load the App List.
@@ -40,36 +37,42 @@
 /**
  * The repository to load the App List data.
  */
-internal class AppListRepository(context: Context) {
+internal interface AppListRepository {
+    /** Loads the list of [ApplicationInfo]. */
+    suspend fun loadApps(config: AppListConfig): List<ApplicationInfo>
+
+    /** Gets the flow of predicate that could used to filter system app. */
+    fun showSystemPredicate(
+        userIdFlow: Flow<Int>,
+        showSystemFlow: Flow<Boolean>,
+    ): Flow<(app: ApplicationInfo) -> Boolean>
+}
+
+
+internal class AppListRepositoryImpl(context: Context) : AppListRepository {
     private val packageManager = context.packageManager
 
-    fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow
-        .map { loadApps(it) }
-        .flowOn(Dispatchers.Default)
+    override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope {
+        val hiddenSystemModulesDeferred = async {
+            packageManager.getInstalledModules(0)
+                .filter { it.isHidden }
+                .map { it.packageName }
+                .toSet()
+        }
+        val flags = PackageManager.ApplicationInfoFlags.of(
+            (PackageManager.MATCH_DISABLED_COMPONENTS or
+                PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
+        )
+        val installedApplicationsAsUser =
+            packageManager.getInstalledApplicationsAsUser(flags, config.userId)
 
-    private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> {
-        return coroutineScope {
-            val hiddenSystemModulesDeferred = async {
-                packageManager.getInstalledModules(0)
-                    .filter { it.isHidden }
-                    .map { it.packageName }
-                    .toSet()
-            }
-            val flags = PackageManager.ApplicationInfoFlags.of(
-                (PackageManager.MATCH_DISABLED_COMPONENTS or
-                    PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
-            )
-            val installedApplicationsAsUser =
-                packageManager.getInstalledApplicationsAsUser(flags, config.userId)
-
-            val hiddenSystemModules = hiddenSystemModulesDeferred.await()
-            installedApplicationsAsUser.filter { app ->
-                app.isInAppList(config.showInstantApps, hiddenSystemModules)
-            }
+        val hiddenSystemModules = hiddenSystemModulesDeferred.await()
+        installedApplicationsAsUser.filter { app ->
+            app.isInAppList(config.showInstantApps, hiddenSystemModules)
         }
     }
 
-    fun showSystemPredicate(
+    override fun showSystemPredicate(
         userIdFlow: Flow<Int>,
         showSystemFlow: Flow<Boolean>,
     ): Flow<(app: ApplicationInfo) -> Boolean> =
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
index 1e487da..650b278 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.spaprivileged.model.app
 
 import android.app.Application
+import android.content.Context
 import android.content.pm.ApplicationInfo
 import android.icu.text.Collator
 import androidx.lifecycle.AndroidViewModel
@@ -27,12 +28,16 @@
 import java.util.concurrent.ConcurrentHashMap
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
 
 internal data class AppListData<T : AppRecord>(
@@ -43,9 +48,15 @@
         AppListData(appEntries.filter(predicate), option)
 }
 
-@OptIn(ExperimentalCoroutinesApi::class)
 internal class AppListViewModel<T : AppRecord>(
     application: Application,
+) : AppListViewModelImpl<T>(application)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal open class AppListViewModelImpl<T : AppRecord>(
+    application: Application,
+    appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl,
+    appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl,
 ) : AndroidViewModel(application) {
     val appListConfig = StateFlowBridge<AppListConfig>()
     val listModel = StateFlowBridge<AppListModel<T>>()
@@ -53,16 +64,18 @@
     val option = StateFlowBridge<Int>()
     val searchQuery = StateFlowBridge<String>()
 
-    private val appListRepository = AppListRepository(application)
-    private val appRepository = AppRepositoryImpl(application)
+    private val appListRepository = appListRepositoryFactory(application)
+    private val appRepository = appRepositoryFactory(application)
     private val collator = Collator.getInstance().freeze()
     private val labelMap = ConcurrentHashMap<String, String>()
-    private val scope = viewModelScope + Dispatchers.Default
+    private val scope = viewModelScope + Dispatchers.IO
 
     private val userIdFlow = appListConfig.flow.map { it.userId }
 
+    private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)
+
     private val recordListFlow = listModel.flow
-        .flatMapLatest { it.transform(userIdFlow, appListRepository.loadApps(appListConfig.flow)) }
+        .flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
         .shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
 
     private val systemFilteredFlow =
@@ -83,6 +96,12 @@
         scheduleOnFirstLoaded()
     }
 
+    fun reloadApps() {
+        viewModelScope.launch {
+            appsStateFlow.value = appListRepository.loadApps(appListConfig.flow.first())
+        }
+    }
+
     private fun filterAndSort(option: Int) = listModel.flow.flatMapLatest { listModel ->
         listModel.filter(userIdFlow, option, systemFilteredFlow)
             .asyncMapItem { record ->
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index 34f12af..90710db 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -22,8 +22,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.produceState
+import androidx.compose.ui.res.stringResource
 import com.android.settingslib.Utils
 import com.android.settingslib.spa.framework.compose.rememberContext
+import com.android.settingslib.spaprivileged.R
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 
@@ -34,7 +36,12 @@
     fun loadLabel(app: ApplicationInfo): String
 
     @Composable
-    fun produceLabel(app: ApplicationInfo): State<String>
+    fun produceLabel(app: ApplicationInfo) =
+        produceState(initialValue = stringResource(R.string.summary_placeholder), app) {
+            withContext(Dispatchers.IO) {
+                value = loadLabel(app)
+            }
+        }
 
     @Composable
     fun produceIcon(app: ApplicationInfo): State<Drawable?>
@@ -46,13 +53,6 @@
     override fun loadLabel(app: ApplicationInfo): String = app.loadLabel(packageManager).toString()
 
     @Composable
-    override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) {
-        withContext(Dispatchers.Default) {
-            value = app.loadLabel(packageManager).toString()
-        }
-    }
-
-    @Composable
     override fun produceIcon(app: ApplicationInfo) =
         produceState<Drawable?>(initialValue = null, app) {
             withContext(Dispatchers.Default) {
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/model/app/PackageManagerExt.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt
new file mode 100644
index 0000000..2b2f11c
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.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.settingslib.spaprivileged.model.app
+
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+
+/**
+ * Checks if a package is system module.
+ */
+fun PackageManager.isSystemModule(packageName: String): Boolean = try {
+    getModuleInfo(packageName, 0)
+    true
+} catch (_: PackageManager.NameNotFoundException) {
+    // Expected, not system module
+    false
+}
+
+/**
+ * Resolves the activity to start for a given application and action.
+ */
+fun PackageManager.resolveActionForApp(
+    app: ApplicationInfo,
+    action: String,
+    flags: Int = 0,
+): ActivityInfo? {
+    val intent = Intent(action).apply {
+        `package` = app.packageName
+    }
+    return resolveActivityAsUser(intent, ResolveInfoFlags.of(flags.toLong()), app.userId)
+        ?.activityInfo
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 215d22c..cb0bfc6 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -26,43 +26,76 @@
 
 private const val TAG = "PackageManagers"
 
-object PackageManagers {
-    private val iPackageManager by lazy { AppGlobals.getPackageManager() }
-
-    fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? =
-        getPackageInfoAsUser(packageName, 0, userId)
-
-    fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? =
-        PackageManager.getApplicationInfoAsUserCached(packageName, 0, userId)
+interface IPackageManagers {
+    fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo?
+    fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo?
 
     /** Checks whether a package is installed for a given user. */
-    fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean =
+    fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean
+    fun ApplicationInfo.hasRequestPermission(permission: String): Boolean
+
+    /** Checks whether a permission is currently granted to the application. */
+    fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
+
+    suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
+    fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo?
+}
+
+object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
+
+internal interface PackageManagerWrapper {
+    fun getPackageInfoAsUserCached(
+        packageName: String,
+        flags: Long,
+        userId: Int,
+    ): PackageInfo?
+}
+
+internal object PackageManagerWrapperImpl : PackageManagerWrapper {
+    override fun getPackageInfoAsUserCached(
+        packageName: String,
+        flags: Long,
+        userId: Int,
+    ): PackageInfo? = PackageManager.getPackageInfoAsUserCached(packageName, flags, userId)
+}
+
+internal class PackageManagersImpl(
+    private val packageManagerWrapper: PackageManagerWrapper,
+) : IPackageManagers {
+    private val iPackageManager by lazy { AppGlobals.getPackageManager() }
+
+    override fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo? =
+        getPackageInfoAsUser(packageName, 0, userId)
+
+    override fun getApplicationInfoAsUser(packageName: String, userId: Int): ApplicationInfo? =
+        PackageManager.getApplicationInfoAsUserCached(packageName, 0, userId)
+
+    override fun isPackageInstalledAsUser(packageName: String, userId: Int): Boolean =
         getApplicationInfoAsUser(packageName, userId)?.hasFlag(ApplicationInfo.FLAG_INSTALLED)
             ?: false
 
-    fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
+    override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
         val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
         return packageInfo?.requestedPermissions?.let {
             permission in it
         } ?: false
     }
 
-    fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
+    override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
         val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
-            ?: return false
-        val index = packageInfo.requestedPermissions.indexOf(permission)
+        val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
         return index >= 0 &&
             packageInfo.requestedPermissionsFlags[index].hasFlag(REQUESTED_PERMISSION_GRANTED)
     }
 
-    suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> =
+    override suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String> =
         iPackageManager.getAppOpPermissionPackages(permission, userId).asIterable().asyncFilter {
             iPackageManager.isPackageAvailable(it, userId)
         }.toSet()
 
-    fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
+    override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
         try {
-            PackageManager.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+            packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
         } catch (e: PackageManager.NameNotFoundException) {
             Log.w(TAG, "getPackageInfoAsUserCached() failed", e)
             null
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 6318b4e..681eb1c 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
@@ -16,22 +16,29 @@
 
 package com.android.settingslib.spaprivileged.template.app
 
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
 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.CategoryTitle
 import com.android.settingslib.spa.widget.ui.PlaceholderTitle
 import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
+import com.android.settingslib.spaprivileged.model.app.AppEntry
 import com.android.settingslib.spaprivileged.model.app.AppListConfig
 import com.android.settingslib.spaprivileged.model.app.AppListData
 import com.android.settingslib.spaprivileged.model.app.AppListModel
@@ -40,6 +47,13 @@
 import kotlinx.coroutines.Dispatchers
 
 private const val TAG = "AppList"
+private const val CONTENT_TYPE_HEADER = "header"
+
+internal data class AppListState(
+    val showSystem: State<Boolean>,
+    val option: State<Int>,
+    val searchQuery: State<String>,
+)
 
 /**
  * The template to render an App List.
@@ -48,60 +62,90 @@
  */
 @Composable
 internal fun <T : AppRecord> AppList(
-    appListConfig: AppListConfig,
+    config: AppListConfig,
     listModel: AppListModel<T>,
-    showSystem: State<Boolean>,
-    option: State<Int>,
-    searchQuery: State<String>,
+    state: AppListState,
+    header: @Composable () -> Unit,
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+    bottomPadding: Dp,
+    appListDataSupplier: @Composable () -> State<AppListData<T>?> = {
+        loadAppListData(config, listModel, state)
+    },
 ) {
-    LogCompositions(TAG, appListConfig.userId.toString())
-    val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery)
-    AppListWidget(appListData, listModel, appItem)
+    LogCompositions(TAG, config.userId.toString())
+    val appListData = appListDataSupplier()
+    AppListWidget(appListData, listModel, header, appItem, bottomPadding)
 }
 
 @Composable
 private fun <T : AppRecord> AppListWidget(
     appListData: State<AppListData<T>?>,
     listModel: AppListModel<T>,
+    header: @Composable () -> Unit,
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+    bottomPadding: Dp,
 ) {
+    val timeMeasurer = rememberTimeMeasurer(TAG)
     appListData.value?.let { (list, option) ->
+        timeMeasurer.logFirst("app list first loaded")
         if (list.isEmpty()) {
             PlaceholderTitle(stringResource(R.string.no_applications))
             return
         }
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
-            state = rememberLazyListState(),
-            contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical),
+            state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
+            contentPadding = PaddingValues(bottom = bottomPadding),
         ) {
+            item(contentType = CONTENT_TYPE_HEADER) {
+                header()
+            }
+
             items(count = list.size, key = { option to list[it].record.app.packageName }) {
+                remember(list) { listModel.getGroupTitleIfFirst(option, list, it) }
+                    ?.let { group -> CategoryTitle(title = group) }
+
                 val appEntry = list[it]
                 val summary = listModel.getSummary(option, appEntry.record) ?: "".toState()
-                val itemModel = remember(appEntry) {
+                appItem(remember(appEntry) {
                     AppListItemModel(appEntry.record, appEntry.label, summary)
-                }
-                appItem(itemModel)
+                })
             }
         }
     }
 }
 
-@Composable
-private fun <T : AppRecord> loadAppEntries(
-    appListConfig: AppListConfig,
-    listModel: AppListModel<T>,
-    showSystem: State<Boolean>,
-    option: State<Int>,
-    searchQuery: State<String>,
-): State<AppListData<T>?> {
-    val viewModel: AppListViewModel<T> = viewModel(key = appListConfig.userId.toString())
-    viewModel.appListConfig.setIfAbsent(appListConfig)
-    viewModel.listModel.setIfAbsent(listModel)
-    viewModel.showSystem.Sync(showSystem)
-    viewModel.option.Sync(option)
-    viewModel.searchQuery.Sync(searchQuery)
+/** Returns group title if this is the first item of the group. */
+private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst(
+    option: Int,
+    list: List<AppEntry<T>>,
+    index: Int,
+): String? = getGroupTitle(option, list[index].record)?.takeIf {
+    index == 0 || it != getGroupTitle(option, list[index - 1].record)
+}
 
-    return viewModel.appListDataFlow.collectAsState(null, Dispatchers.Default)
+@Composable
+private fun <T : AppRecord> loadAppListData(
+    config: AppListConfig,
+    listModel: AppListModel<T>,
+    state: AppListState,
+): State<AppListData<T>?> {
+    val viewModel: AppListViewModel<T> = viewModel(key = config.userId.toString())
+    viewModel.appListConfig.setIfAbsent(config)
+    viewModel.listModel.setIfAbsent(listModel)
+    viewModel.showSystem.Sync(state.showSystem)
+    viewModel.option.Sync(state.option)
+    viewModel.searchQuery.Sync(state.searchQuery)
+
+    DisposableBroadcastReceiverAsUser(
+        intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
+            addAction(Intent.ACTION_PACKAGE_REMOVED)
+            addAction(Intent.ACTION_PACKAGE_CHANGED)
+            addDataScheme("package")
+        },
+        userHandle = UserHandle.of(config.userId),
+        onStart = { viewModel.reloadApps() },
+    ) { viewModel.reloadApps() }
+
+    return viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
 }
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 2be1d1c..388a7d8 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
@@ -17,9 +17,7 @@
 package com.android.settingslib.spaprivileged.template.app
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -28,9 +26,8 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
-import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
-import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
+import com.android.settingslib.spa.widget.scaffold.SearchScaffold
 import com.android.settingslib.spa.widget.ui.Spinner
 import com.android.settingslib.spaprivileged.R
 import com.android.settingslib.spaprivileged.model.app.AppListConfig
@@ -40,6 +37,8 @@
 
 /**
  * The full screen template for an App List page.
+ *
+ * @param header the description header appears before all the applications.
  */
 @Composable
 fun <T : AppRecord> AppListPage(
@@ -47,32 +46,35 @@
     listModel: AppListModel<T>,
     showInstantApps: Boolean = false,
     primaryUserOnly: Boolean = false,
+    header: @Composable () -> Unit = {},
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
 ) {
     val showSystem = rememberSaveable { mutableStateOf(false) }
-    // TODO: Use SearchScaffold here.
-    SettingsScaffold(
+    SearchScaffold(
         title = title,
         actions = {
             ShowSystemAction(showSystem.value) { showSystem.value = it }
         },
-    ) { paddingValues ->
-        Spacer(Modifier.padding(paddingValues))
+    ) { bottomPadding, searchQuery ->
         WorkProfilePager(primaryUserOnly) { userInfo ->
             Column(Modifier.fillMaxSize()) {
                 val options = remember { listModel.getSpinnerOptions() }
                 val selectedOption = rememberSaveable { mutableStateOf(0) }
                 Spinner(options, selectedOption.value) { selectedOption.value = it }
                 AppList(
-                    appListConfig = AppListConfig(
+                    config = AppListConfig(
                         userId = userInfo.id,
                         showInstantApps = showInstantApps,
                     ),
                     listModel = listModel,
-                    showSystem = showSystem,
-                    option = selectedOption,
-                    searchQuery = stateOf(""),
+                    state = AppListState(
+                        showSystem = showSystem,
+                        option = selectedOption,
+                        searchQuery = searchQuery,
+                    ),
+                    header = header,
                     appItem = appItem,
+                    bottomPadding = bottomPadding,
                 )
             }
         }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
index 5afe21e..5cd74e3 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/tests/Android.bp
@@ -31,6 +31,7 @@
     ],
 
     static_libs: [
+        "SpaLibTestUtils",
         "androidx.compose.ui_ui-test-junit4",
         "androidx.compose.ui_ui-test-manifest",
         "androidx.test.ext.junit",
@@ -38,7 +39,4 @@
         "mockito-target-minus-junit4",
         "truth-prebuilt",
     ],
-    kotlincflags: [
-        "-opt-in=kotlin.RequiresOptIn",
-    ],
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 5d5a24e..bc6925b 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -24,9 +24,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 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.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -36,11 +33,9 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.eq
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
-
-private const val USER_ID = 0
+import org.mockito.Mockito.`when` as whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
@@ -80,36 +75,28 @@
             packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
         ).thenReturn(emptyList())
 
-        repository = AppListRepository(context)
+        repository = AppListRepositoryImpl(context)
     }
 
     @Test
     fun notShowInstantApps() = runTest {
         val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
 
-        val appListFlow = repository.loadApps(flowOf(appListConfig))
+        val appListFlow = repository.loadApps(appListConfig)
 
-        launch {
-            val flowValues = mutableListOf<List<ApplicationInfo>>()
-            appListFlow.toList(flowValues)
-            assertThat(flowValues).hasSize(1)
-
-            assertThat(flowValues[0]).containsExactly(normalApp)
-        }
+        assertThat(appListFlow).containsExactly(normalApp)
     }
 
     @Test
     fun showInstantApps() = runTest {
         val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = true)
 
-        val appListFlow = repository.loadApps(flowOf(appListConfig))
+        val appListFlow = repository.loadApps(appListConfig)
 
-        launch {
-            val flowValues = mutableListOf<List<ApplicationInfo>>()
-            appListFlow.toList(flowValues)
-            assertThat(flowValues).hasSize(1)
+        assertThat(appListFlow).containsExactly(normalApp, instantApp)
+    }
 
-            assertThat(flowValues[0]).containsExactly(normalApp, instantApp)
-        }
+    private companion object {
+        const val USER_ID = 0
     }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
new file mode 100644
index 0000000..b570815
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.spaprivileged.model.app
+
+import android.app.Application
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.Composable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.framework.util.asyncMapItem
+import com.android.settingslib.spa.testutils.waitUntil
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class AppListViewModelTest {
+    @JvmField
+    @Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var application: Application
+
+    private val listModel = TestAppListModel()
+
+    private fun createViewModel(): AppListViewModelImpl<TestAppRecord> {
+        val viewModel = AppListViewModelImpl<TestAppRecord>(
+            application = application,
+            appListRepositoryFactory = { FakeAppListRepository },
+            appRepositoryFactory = { FakeAppRepository },
+        )
+        viewModel.appListConfig.setIfAbsent(CONFIG)
+        viewModel.listModel.setIfAbsent(listModel)
+        viewModel.showSystem.setIfAbsent(false)
+        viewModel.option.setIfAbsent(0)
+        viewModel.searchQuery.setIfAbsent("")
+        viewModel.reloadApps()
+        return viewModel
+    }
+
+    @Test
+    fun appListDataFlow() = runTest {
+        val viewModel = createViewModel()
+
+        val (appEntries, option) = viewModel.appListDataFlow.first()
+
+        assertThat(appEntries).hasSize(1)
+        assertThat(appEntries[0].record.app).isSameInstanceAs(APP)
+        assertThat(appEntries[0].label).isEqualTo(LABEL)
+        assertThat(option).isEqualTo(0)
+    }
+
+    @Test
+    fun onFirstLoaded_calledWhenLoaded() = runTest {
+        val viewModel = createViewModel()
+
+        viewModel.appListDataFlow.first()
+
+        waitUntil { listModel.onFirstLoadedCalled }
+    }
+
+    private object FakeAppListRepository : AppListRepository {
+        override suspend fun loadApps(config: AppListConfig) = listOf(APP)
+
+        override fun showSystemPredicate(
+            userIdFlow: Flow<Int>,
+            showSystemFlow: Flow<Boolean>,
+        ): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { true }
+    }
+
+    private object FakeAppRepository : AppRepository {
+        override fun loadLabel(app: ApplicationInfo) = LABEL
+
+        @Composable
+        override fun produceIcon(app: ApplicationInfo) = stateOf(null)
+    }
+
+    private companion object {
+        const val USER_ID = 0
+        const val PACKAGE_NAME = "package.name"
+        const val LABEL = "Label"
+        val CONFIG = AppListConfig(userId = USER_ID, showInstantApps = false)
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+        }
+    }
+}
+
+private data class TestAppRecord(override val app: ApplicationInfo) : AppRecord
+
+private class TestAppListModel : AppListModel<TestAppRecord> {
+    var onFirstLoadedCalled = false
+
+    override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+        appListFlow.asyncMapItem { TestAppRecord(it) }
+
+    @Composable
+    override fun getSummary(option: Int, record: TestAppRecord) = null
+
+    override fun filter(
+        userIdFlow: Flow<Int>,
+        option: Int,
+        recordListFlow: Flow<List<TestAppRecord>>,
+    ) = recordListFlow
+
+    override suspend fun onFirstLoaded(recordList: List<TestAppRecord>) {
+        onFirstLoadedCalled = true
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt
new file mode 100644
index 0000000..4207490
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.spaprivileged.model.app
+
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.ModuleInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+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
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class PackageManagerExtTest {
+    @JvmField
+    @Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+
+    private fun mockResolveActivityAsUser(resolveInfo: ResolveInfo?) {
+        whenever(
+            packageManager.resolveActivityAsUser(any(), any<ResolveInfoFlags>(), eq(APP.userId))
+        ).thenReturn(resolveInfo)
+    }
+
+    @Test
+    fun isSystemModule_whenSystemModule_returnTrue() {
+        whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenReturn(ModuleInfo())
+
+        val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME)
+
+        assertThat(isSystemModule).isTrue()
+    }
+
+    @Test
+    fun isSystemModule_whenNotSystemModule_returnFalse() {
+        whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenThrow(NameNotFoundException())
+
+        val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME)
+
+        assertThat(isSystemModule).isFalse()
+    }
+
+    @Test
+    fun resolveActionForApp_noResolveInfo() {
+        mockResolveActivityAsUser(null)
+
+        val activityInfo = packageManager.resolveActionForApp(APP, ACTION)
+
+        assertThat(activityInfo).isNull()
+    }
+
+    @Test
+    fun resolveActionForApp_noActivityInfo() {
+        mockResolveActivityAsUser(ResolveInfo())
+
+        val activityInfo = packageManager.resolveActionForApp(APP, ACTION)
+
+        assertThat(activityInfo).isNull()
+    }
+
+    @Test
+    fun resolveActionForApp_hasActivityInfo() {
+        mockResolveActivityAsUser(ResolveInfo().apply {
+            activityInfo = ActivityInfo().apply {
+                packageName = PACKAGE_NAME
+                name = ACTIVITY_NAME
+            }
+        })
+
+        val activityInfo = packageManager.resolveActionForApp(APP, ACTION)!!
+
+        assertThat(activityInfo.componentName).isEqualTo(ComponentName(PACKAGE_NAME, ACTIVITY_NAME))
+    }
+
+    @Test
+    fun resolveActionForApp_withFlags() {
+        packageManager.resolveActionForApp(
+            app = APP,
+            action = ACTION,
+            flags = PackageManager.GET_META_DATA,
+        )
+
+        val flagsCaptor = ArgumentCaptor.forClass(ResolveInfoFlags::class.java)
+        verify(packageManager).resolveActivityAsUser(any(), flagsCaptor.capture(), eq(APP.userId))
+        assertThat(flagsCaptor.value.value).isEqualTo(PackageManager.GET_META_DATA.toLong())
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+        const val ACTIVITY_NAME = "ActivityName"
+        const val ACTION = "action"
+        const val UID = 123
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            uid = UID
+        }
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt
new file mode 100644
index 0000000..6c31f4b
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.spaprivileged.model.app
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PackageManagersTest {
+
+    private val fakePackageManagerWrapper = FakePackageManagerWrapper()
+
+    private val packageManagersImpl = PackageManagersImpl(fakePackageManagerWrapper)
+
+    @Test
+    fun hasGrantPermission_packageInfoIsNull_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = null
+
+        val hasGrantPermission = with(packageManagersImpl) {
+            APP.hasGrantPermission(PERMISSION_A)
+        }
+
+        assertThat(hasGrantPermission).isFalse()
+    }
+
+    @Test
+    fun hasGrantPermission_requestedPermissionsIsNull_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo()
+
+        val hasGrantPermission = with(packageManagersImpl) {
+            APP.hasGrantPermission(PERMISSION_A)
+        }
+
+        assertThat(hasGrantPermission).isFalse()
+    }
+
+    @Test
+    fun hasGrantPermission_notRequested_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = arrayOf(PERMISSION_B)
+            requestedPermissionsFlags = intArrayOf(PackageInfo.REQUESTED_PERMISSION_GRANTED)
+        }
+
+        val hasGrantPermission = with(packageManagersImpl) {
+            APP.hasGrantPermission(PERMISSION_A)
+        }
+
+        assertThat(hasGrantPermission).isFalse()
+    }
+
+    @Test
+    fun hasGrantPermission_notGranted_returnFalse() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = arrayOf(PERMISSION_A, PERMISSION_B)
+            requestedPermissionsFlags = intArrayOf(0, PackageInfo.REQUESTED_PERMISSION_GRANTED)
+        }
+
+        val hasGrantPermission = with(packageManagersImpl) {
+            APP.hasGrantPermission(PERMISSION_A)
+        }
+
+        assertThat(hasGrantPermission).isFalse()
+    }
+
+    @Test
+    fun hasGrantPermission_granted_returnTrue() {
+        fakePackageManagerWrapper.fakePackageInfo = PackageInfo().apply {
+            requestedPermissions = arrayOf(PERMISSION_A, PERMISSION_B)
+            requestedPermissionsFlags = intArrayOf(PackageInfo.REQUESTED_PERMISSION_GRANTED, 0)
+        }
+
+        val hasGrantPermission = with(packageManagersImpl) {
+            APP.hasGrantPermission(PERMISSION_A)
+        }
+
+        assertThat(hasGrantPermission).isTrue()
+    }
+
+    private inner class FakePackageManagerWrapper : PackageManagerWrapper {
+        var fakePackageInfo: PackageInfo? = null
+
+        override fun getPackageInfoAsUserCached(
+            packageName: String,
+            flags: Long,
+            userId: Int,
+        ): PackageInfo? = fakePackageInfo
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "packageName"
+        const val PERMISSION_A = "permission.A"
+        const val PERMISSION_B = "permission.B"
+        const val UID = 123
+        val APP = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            uid = UID
+        }
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
new file mode 100644
index 0000000..9f20c78
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.spaprivileged.template.app
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.icu.text.CollationKey
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.dp
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.framework.compose.toState
+import com.android.settingslib.spa.framework.util.asyncMapItem
+import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.model.app.AppEntry
+import com.android.settingslib.spaprivileged.model.app.AppListConfig
+import com.android.settingslib.spaprivileged.model.app.AppListData
+import com.android.settingslib.spaprivileged.model.app.AppListModel
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import kotlinx.coroutines.flow.Flow
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AppListTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private var context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun whenNoApps() {
+        setContent(appEntries = emptyList())
+
+        composeTestRule.onNodeWithText(context.getString(R.string.no_applications))
+            .assertIsDisplayed()
+    }
+
+    @Test
+    fun couldShowAppItem() {
+        setContent(appEntries = listOf(APP_ENTRY_A))
+
+        composeTestRule.onNodeWithText(APP_ENTRY_A.label).assertIsDisplayed()
+    }
+
+    @Test
+    fun couldShowHeader() {
+        setContent(appEntries = listOf(APP_ENTRY_A), header = { Text(HEADER) })
+
+        composeTestRule.onNodeWithText(HEADER).assertIsDisplayed()
+    }
+
+    @Test
+    fun whenNotGrouped_groupTitleDoesNotExist() {
+        setContent(appEntries = listOf(APP_ENTRY_A, APP_ENTRY_B), enableGrouping = false)
+
+        composeTestRule.onNodeWithText(GROUP_A).assertDoesNotExist()
+        composeTestRule.onNodeWithText(GROUP_B).assertDoesNotExist()
+    }
+
+    @Test
+    fun whenGrouped_groupTitleDisplayed() {
+        setContent(appEntries = listOf(APP_ENTRY_A, APP_ENTRY_B), enableGrouping = true)
+
+        composeTestRule.onNodeWithText(GROUP_A).assertIsDisplayed()
+        composeTestRule.onNodeWithText(GROUP_B).assertIsDisplayed()
+    }
+
+    private fun setContent(
+        appEntries: List<AppEntry<TestAppRecord>>,
+        header: @Composable () -> Unit = {},
+        enableGrouping: Boolean = false,
+    ) {
+        composeTestRule.setContent {
+            AppList(
+                config = AppListConfig(userId = USER_ID, showInstantApps = false),
+                listModel = TestAppListModel(enableGrouping),
+                state = AppListState(
+                    showSystem = false.toState(),
+                    option = 0.toState(),
+                    searchQuery = "".toState(),
+                ),
+                header = header,
+                appItem = { AppListItem(it) {} },
+                bottomPadding = 0.dp,
+                appListDataSupplier = {
+                    stateOf(AppListData(appEntries, option = 0))
+                }
+            )
+        }
+    }
+
+    private companion object {
+        const val USER_ID = 0
+        const val HEADER = "Header"
+        const val GROUP_A = "Group A"
+        const val GROUP_B = "Group B"
+        val APP_ENTRY_A = AppEntry(
+            record = TestAppRecord(
+                app = ApplicationInfo().apply {
+                    packageName = "package.name.a"
+                },
+                group = GROUP_A,
+            ),
+            label = "Label A",
+            labelCollationKey = CollationKey("", byteArrayOf()),
+        )
+        val APP_ENTRY_B = AppEntry(
+            record = TestAppRecord(
+                app = ApplicationInfo().apply {
+                    packageName = "package.name.b"
+                },
+                group = GROUP_B,
+            ),
+            label = "Label B",
+            labelCollationKey = CollationKey("", byteArrayOf()),
+        )
+    }
+}
+
+private data class TestAppRecord(
+    override val app: ApplicationInfo,
+    val group: String? = null,
+) : AppRecord
+
+private class TestAppListModel(val enableGrouping: Boolean) : AppListModel<TestAppRecord> {
+    override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+        appListFlow.asyncMapItem { TestAppRecord(it) }
+
+    @Composable
+    override fun getSummary(option: Int, record: TestAppRecord) = null
+
+    override fun filter(
+        userIdFlow: Flow<Int>,
+        option: Int,
+        recordListFlow: Flow<List<TestAppRecord>>,
+    ) = recordListFlow
+
+    override fun getGroupTitle(option: Int, record: TestAppRecord) =
+        if (enableGrouping) record.group else null
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
index cec6d7d..b3638c2 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt
@@ -28,7 +28,6 @@
 import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
 import com.android.settingslib.spaprivileged.model.app.userHandle
-import com.google.common.truth.Truth.assertThat
 import java.util.UUID
 import org.junit.Before
 import org.junit.Rule
@@ -77,7 +76,7 @@
             }
         }
 
-        assertThat(storageSize.value).isEqualTo("123 B")
+        composeTestRule.waitUntil { storageSize.value == "123 B" }
     }
 
     companion object {
diff --git a/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml
new file mode 100644
index 0000000..46abff8
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_5g_plus_mobiledata_default.xml
@@ -0,0 +1,33 @@
+<!--
+     Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:viewportWidth="22"
+    android:viewportHeight="17"
+    android:width="22dp"
+    android:height="17dp">
+  <group>
+    <group>
+      <path android:fillColor="#FF000000"
+          android:pathData="M1.03 8.47l0.43-4.96h4.33v1.17H2.48L2.25 7.39C2.66 7.1 3.1 6.96 3.57 6.96c0.77 0 1.38 0.3 1.83 0.9 s0.66 1.41 0.66 2.43c0 1.03-0.24 1.84-0.72 2.43S4.2 13.6 3.36 13.6c-0.75 0-1.36-0.24-1.83-0.73s-0.74-1.16-0.81-2.02h1.13 c0.07 0.57 0.23 1 0.49 1.29s0.59 0.43 1.01 0.43c0.47 0 0.84-0.2 1.1-0.61c0.26-0.41 0.4-0.96 0.4-1.65 c0-0.65-0.14-1.18-0.43-1.59S3.76 8.09 3.28 8.09c-0.4 0-0.72 0.1-0.96 0.31L1.99 8.73L1.03 8.47z"/>
+    </group>
+    <group>
+      <path android:fillColor="#FF000000"
+          android:pathData="M 18.93,5.74 L 18.93,3.39 L 17.63,3.39 L 17.63,5.74 L 15.28,5.74 L 15.28,7.04 L 17.63,7.04 L 17.63,9.39 L 18.93,9.39 L 18.93,7.04 L 21.28,7.04 L 21.28,5.74 z"/>
+    </group>
+    <path android:fillColor="#FF000000"
+        android:pathData="M13.78 12.24l-0.22 0.27c-0.63 0.73-1.55 1.1-2.76 1.1c-1.08 0-1.92-0.36-2.53-1.07s-0.93-1.72-0.94-3.02V7.56 c0-1.39 0.28-2.44 0.84-3.13s1.39-1.04 2.51-1.04c0.95 0 1.69 0.26 2.23 0.79s0.83 1.28 0.89 2.26h-1.25 c-0.05-0.62-0.22-1.1-0.52-1.45s-0.74-0.52-1.34-0.52c-0.72 0-1.24 0.23-1.57 0.7S8.6 6.37 8.59 7.4v2.03c0 1 0.19 1.77 0.57 2.31 c0.38 0.54 0.93 0.8 1.65 0.8c0.67 0 1.19-0.16 1.54-0.49l0.18-0.17V9.59h-1.82V8.52h3.07V12.24z"/>
+  </group>
+</vector>
diff --git a/packages/SettingsLib/res/values-en-rCA/arrays.xml b/packages/SettingsLib/res/values-en-rCA/arrays.xml
index 327e4e9..8a57232 100644
--- a/packages/SettingsLib/res/values-en-rCA/arrays.xml
+++ b/packages/SettingsLib/res/values-en-rCA/arrays.xml
@@ -86,7 +86,7 @@
     <item msgid="8147982633566548515">"map14"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_titles">
-    <item msgid="2494959071796102843">"Use system selection (default)"</item>
+    <item msgid="2494959071796102843">"Use System Selection (Default)"</item>
     <item msgid="4055460186095649420">"SBC"</item>
     <item msgid="720249083677397051">"AAC"</item>
     <item msgid="1049450003868150455">"<xliff:g id="QUALCOMM">Qualcomm®</xliff:g> <xliff:g id="APTX">aptX™</xliff:g> audio"</item>
@@ -96,7 +96,7 @@
     <item msgid="506175145534048710">"Opus"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_summaries">
-    <item msgid="8868109554557331312">"Use system selection (default)"</item>
+    <item msgid="8868109554557331312">"Use System Selection (Default)"</item>
     <item msgid="9024885861221697796">"SBC"</item>
     <item msgid="4688890470703790013">"AAC"</item>
     <item msgid="8627333814413492563">"<xliff:g id="QUALCOMM">Qualcomm®</xliff:g> <xliff:g id="APTX">aptX™</xliff:g> audio"</item>
@@ -106,52 +106,52 @@
     <item msgid="7940970833006181407">"Opus"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_sample_rate_titles">
-    <item msgid="926809261293414607">"Use system selection (default)"</item>
+    <item msgid="926809261293414607">"Use System Selection (Default)"</item>
     <item msgid="8003118270854840095">"44.1 kHz"</item>
     <item msgid="3208896645474529394">"48.0 kHz"</item>
     <item msgid="8420261949134022577">"88.2 kHz"</item>
     <item msgid="8887519571067543785">"96.0 kHz"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_sample_rate_summaries">
-    <item msgid="2284090879080331090">"Use system selection (default)"</item>
+    <item msgid="2284090879080331090">"Use System Selection (Default)"</item>
     <item msgid="1872276250541651186">"44.1 kHz"</item>
     <item msgid="8736780630001704004">"48.0 kHz"</item>
     <item msgid="7698585706868856888">"88.2 kHz"</item>
     <item msgid="8946330945963372966">"96.0 kHz"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_bits_per_sample_titles">
-    <item msgid="2574107108483219051">"Use system selection (default)"</item>
+    <item msgid="2574107108483219051">"Use System Selection (Default)"</item>
     <item msgid="4671992321419011165">"16 bits/sample"</item>
     <item msgid="1933898806184763940">"24 bits/sample"</item>
     <item msgid="1212577207279552119">"32 bits/sample"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_bits_per_sample_summaries">
-    <item msgid="9196208128729063711">"Use system selection (default)"</item>
+    <item msgid="9196208128729063711">"Use System Selection (Default)"</item>
     <item msgid="1084497364516370912">"16 bits/sample"</item>
     <item msgid="2077889391457961734">"24 bits/sample"</item>
     <item msgid="3836844909491316925">"32 bits/sample"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_channel_mode_titles">
-    <item msgid="3014194562841654656">"Use system selection (default)"</item>
+    <item msgid="3014194562841654656">"Use System Selection (Default)"</item>
     <item msgid="5982952342181788248">"Mono"</item>
     <item msgid="927546067692441494">"Stereo"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_channel_mode_summaries">
-    <item msgid="1997302811102880485">"Use system selection (default)"</item>
+    <item msgid="1997302811102880485">"Use System Selection (Default)"</item>
     <item msgid="8005696114958453588">"Mono"</item>
     <item msgid="1333279807604675720">"Stereo"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_ldac_playback_quality_titles">
-    <item msgid="1241278021345116816">"Optimised for Audio Quality (990kbps/909kbps)"</item>
-    <item msgid="3523665555859696539">"Balanced Audio And Connection Quality (660 kbps/606 kbps)"</item>
-    <item msgid="886408010459747589">"Optimised for Connection Quality (330kbps/303kbps)"</item>
+    <item msgid="1241278021345116816">"Optimized for Audio Quality (990kbps/909kbps)"</item>
+    <item msgid="3523665555859696539">"Balanced Audio And Connection Quality (660kbps/606kbps)"</item>
+    <item msgid="886408010459747589">"Optimized for Connection Quality (330kbps/303kbps)"</item>
     <item msgid="3808414041654351577">"Best Effort (Adaptive Bit Rate)"</item>
   </string-array>
   <string-array name="bluetooth_a2dp_codec_ldac_playback_quality_summaries">
-    <item msgid="804499336721569838">"Optimised for Audio Quality"</item>
-    <item msgid="7451422070435297462">"Balanced Audio and Connection Quality"</item>
-    <item msgid="6173114545795428901">"Optimised for Connection Quality"</item>
-    <item msgid="4349908264188040530">"Best effort (adaptive bit rate)"</item>
+    <item msgid="804499336721569838">"Optimized for Audio Quality"</item>
+    <item msgid="7451422070435297462">"Balanced Audio And Connection Quality"</item>
+    <item msgid="6173114545795428901">"Optimized for Connection Quality"</item>
+    <item msgid="4349908264188040530">"Best Effort (Adaptive Bit Rate)"</item>
   </string-array>
   <string-array name="bluetooth_audio_active_device_summaries">
     <item msgid="8019740759207729126"></item>
@@ -161,25 +161,25 @@
   </string-array>
   <string-array name="select_logd_size_titles">
     <item msgid="1191094707770726722">"Off"</item>
-    <item msgid="7839165897132179888">"64 K"</item>
-    <item msgid="2715700596495505626">"256 K"</item>
-    <item msgid="7099386891713159947">"1 M"</item>
-    <item msgid="6069075827077845520">"4 M"</item>
-    <item msgid="6078203297886482480">"8 M"</item>
+    <item msgid="7839165897132179888">"64K"</item>
+    <item msgid="2715700596495505626">"256K"</item>
+    <item msgid="7099386891713159947">"1M"</item>
+    <item msgid="6069075827077845520">"4M"</item>
+    <item msgid="6078203297886482480">"8M"</item>
   </string-array>
   <string-array name="select_logd_size_lowram_titles">
     <item msgid="1145807928339101085">"Off"</item>
-    <item msgid="4064786181089783077">"64 K"</item>
-    <item msgid="3052710745383602630">"256 K"</item>
-    <item msgid="3691785423374588514">"1 M"</item>
+    <item msgid="4064786181089783077">"64K"</item>
+    <item msgid="3052710745383602630">"256K"</item>
+    <item msgid="3691785423374588514">"1M"</item>
   </string-array>
   <string-array name="select_logd_size_summaries">
     <item msgid="409235464399258501">"Off"</item>
-    <item msgid="4195153527464162486">"64 K per log buffer"</item>
-    <item msgid="7464037639415220106">"256 K per log buffer"</item>
-    <item msgid="8539423820514360724">"1 M per log buffer"</item>
-    <item msgid="1984761927103140651">"4 M per log buffer"</item>
-    <item msgid="2983219471251787208">"8 M per log buffer"</item>
+    <item msgid="4195153527464162486">"64K per log buffer"</item>
+    <item msgid="7464037639415220106">"256K per log buffer"</item>
+    <item msgid="8539423820514360724">"1M per log buffer"</item>
+    <item msgid="1984761927103140651">"4M per log buffer"</item>
+    <item msgid="2983219471251787208">"8M per log buffer"</item>
   </string-array>
   <string-array name="select_logpersist_titles">
     <item msgid="704720725704372366">"Off"</item>
@@ -222,7 +222,7 @@
   </string-array>
   <string-array name="overlay_display_devices_entries">
     <item msgid="4497393944195787240">"None"</item>
-    <item msgid="8461943978957133391">"480 p"</item>
+    <item msgid="8461943978957133391">"480p"</item>
     <item msgid="6923083594932909205">"480p (secure)"</item>
     <item msgid="1226941831391497335">"720p"</item>
     <item msgid="7051983425968643928">"720p (secure)"</item>
@@ -258,10 +258,10 @@
   <string-array name="app_process_limit_entries">
     <item msgid="794656271086646068">"Standard limit"</item>
     <item msgid="8628438298170567201">"No background processes"</item>
-    <item msgid="915752993383950932">"At most, 1 process"</item>
-    <item msgid="8554877790859095133">"At most, 2 processes"</item>
-    <item msgid="9060830517215174315">"At most, 3 processes"</item>
-    <item msgid="6506681373060736204">"At most, 4 processes"</item>
+    <item msgid="915752993383950932">"At most 1 process"</item>
+    <item msgid="8554877790859095133">"At most 2 processes"</item>
+    <item msgid="9060830517215174315">"At most 3 processes"</item>
+    <item msgid="6506681373060736204">"At most 4 processes"</item>
   </string-array>
   <string-array name="usb_configuration_titles">
     <item msgid="3358668781763928157">"Charging"</item>
diff --git a/packages/SettingsLib/res/values-en-rCA/strings.xml b/packages/SettingsLib/res/values-en-rCA/strings.xml
index 77b20a6..4714a0b 100644
--- a/packages/SettingsLib/res/values-en-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-en-rCA/strings.xml
@@ -46,8 +46,8 @@
     <string name="wifi_security_passpoint" msgid="2209078477216565387">"Passpoint"</string>
     <string name="wifi_security_sae" msgid="3644520541721422843">"WPA3-Personal"</string>
     <string name="wifi_security_psk_sae" msgid="8135104122179904684">"WPA2/WPA3-Personal"</string>
-    <string name="wifi_security_none_owe" msgid="5241745828327404101">"None/Enhanced open"</string>
-    <string name="wifi_security_owe" msgid="3343421403561657809">"Enhanced open"</string>
+    <string name="wifi_security_none_owe" msgid="5241745828327404101">"None/Enhanced Open"</string>
+    <string name="wifi_security_owe" msgid="3343421403561657809">"Enhanced Open"</string>
     <string name="wifi_security_eap_suiteb" msgid="415842785991698142">"WPA3-Enterprise 192-bit"</string>
     <string name="wifi_remembered" msgid="3266709779723179188">"Saved"</string>
     <string name="wifi_disconnected" msgid="7054450256284661757">"Disconnected"</string>
@@ -59,29 +59,29 @@
     <string name="wifi_check_password_try_again" msgid="8817789642851605628">"Check password and try again"</string>
     <string name="wifi_not_in_range" msgid="1541760821805777772">"Not in range"</string>
     <string name="wifi_no_internet_no_reconnect" msgid="821591791066497347">"Won\'t automatically connect"</string>
-    <string name="wifi_no_internet" msgid="1774198889176926299">"No Internet access"</string>
+    <string name="wifi_no_internet" msgid="1774198889176926299">"No internet access"</string>
     <string name="saved_network" msgid="7143698034077223645">"Saved by <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="connected_via_network_scorer" msgid="7665725527352893558">"Automatically connected via %1$s"</string>
     <string name="connected_via_network_scorer_default" msgid="7973529709744526285">"Automatically connected via network rating provider"</string>
     <string name="connected_via_app" msgid="3532267661404276584">"Connected via <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="tap_to_sign_up" msgid="5356397741063740395">"Tap to sign up"</string>
-    <string name="wifi_connected_no_internet" msgid="5087420713443350646">"No Internet"</string>
+    <string name="wifi_connected_no_internet" msgid="5087420713443350646">"No internet"</string>
     <string name="private_dns_broken" msgid="1984159464346556931">"Private DNS server cannot be accessed"</string>
     <string name="wifi_limited_connection" msgid="1184778285475204682">"Limited connection"</string>
-    <string name="wifi_status_no_internet" msgid="3799933875988829048">"No Internet"</string>
-    <string name="wifi_status_sign_in_required" msgid="2236267500459526855">"Sign-in required"</string>
+    <string name="wifi_status_no_internet" msgid="3799933875988829048">"No internet"</string>
+    <string name="wifi_status_sign_in_required" msgid="2236267500459526855">"Sign in required"</string>
     <string name="wifi_ap_unable_to_handle_new_sta" msgid="5885145407184194503">"Access point temporarily full"</string>
     <string name="osu_opening_provider" msgid="4318105381295178285">"Opening <xliff:g id="PASSPOINTPROVIDER">%1$s</xliff:g>"</string>
     <string name="osu_connect_failed" msgid="9107873364807159193">"Couldn’t connect"</string>
     <string name="osu_completing_sign_up" msgid="8412636665040390901">"Completing sign-up…"</string>
-    <string name="osu_sign_up_failed" msgid="5605453599586001793">"Couldn’t complete sign-up. Tap to try again"</string>
+    <string name="osu_sign_up_failed" msgid="5605453599586001793">"Couldn’t complete sign-up. Tap to try again."</string>
     <string name="osu_sign_up_complete" msgid="7640183358878916847">"Sign-up complete. Connecting…"</string>
     <string name="speed_label_slow" msgid="6069917670665664161">"Slow"</string>
     <string name="speed_label_okay" msgid="1253594383880810424">"OK"</string>
     <string name="speed_label_fast" msgid="2677719134596044051">"Fast"</string>
-    <string name="speed_label_very_fast" msgid="8215718029533182439">"Very fast"</string>
+    <string name="speed_label_very_fast" msgid="8215718029533182439">"Very Fast"</string>
     <string name="wifi_passpoint_expired" msgid="6540867261754427561">"Expired"</string>
-    <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string>
+    <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string>
     <string name="bluetooth_disconnected" msgid="7739366554710388701">"Disconnected"</string>
     <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Disconnecting…"</string>
     <string name="bluetooth_connecting" msgid="5871702668260192755">"Connecting…"</string>
@@ -110,8 +110,8 @@
     <string name="bluetooth_profile_pbap" msgid="4262303387989406171">"Contacts and call history sharing"</string>
     <string name="bluetooth_profile_pbap_summary" msgid="6466456791354759132">"Use for contacts and call history sharing"</string>
     <string name="bluetooth_profile_pan_nap" msgid="7871974753822470050">"Internet connection sharing"</string>
-    <string name="bluetooth_profile_map" msgid="8907204701162107271">"Text messages"</string>
-    <string name="bluetooth_profile_sap" msgid="8304170950447934386">"SIM access"</string>
+    <string name="bluetooth_profile_map" msgid="8907204701162107271">"Text Messages"</string>
+    <string name="bluetooth_profile_sap" msgid="8304170950447934386">"SIM Access"</string>
     <string name="bluetooth_profile_a2dp_high_quality" msgid="4739440941324792775">"HD audio: <xliff:g id="CODEC_NAME">%1$s</xliff:g>"</string>
     <string name="bluetooth_profile_a2dp_high_quality_unknown_codec" msgid="2477639096903834374">"HD audio"</string>
     <string name="bluetooth_profile_hearing_aid" msgid="58154575573984914">"Hearing Aids"</string>
@@ -120,14 +120,14 @@
     <string name="bluetooth_le_audio_profile_summary_connected" msgid="6916226974453480650">"Connected to LE audio"</string>
     <string name="bluetooth_a2dp_profile_summary_connected" msgid="7422607970115444153">"Connected to media audio"</string>
     <string name="bluetooth_headset_profile_summary_connected" msgid="2420981566026949688">"Connected to phone audio"</string>
-    <string name="bluetooth_opp_profile_summary_connected" msgid="2393521801478157362">"Connected to file-transfer server"</string>
+    <string name="bluetooth_opp_profile_summary_connected" msgid="2393521801478157362">"Connected to file transfer server"</string>
     <string name="bluetooth_map_profile_summary_connected" msgid="4141725591784669181">"Connected to map"</string>
     <string name="bluetooth_sap_profile_summary_connected" msgid="1280297388033001037">"Connected to SAP"</string>
-    <string name="bluetooth_opp_profile_summary_not_connected" msgid="3959741824627764954">"Not connected to file-transfer server"</string>
+    <string name="bluetooth_opp_profile_summary_not_connected" msgid="3959741824627764954">"Not connected to file transfer server"</string>
     <string name="bluetooth_hid_profile_summary_connected" msgid="3923653977051684833">"Connected to input device"</string>
-    <string name="bluetooth_pan_user_profile_summary_connected" msgid="380469653827505727">"Connected to device for Internet access"</string>
-    <string name="bluetooth_pan_nap_profile_summary_connected" msgid="3744773111299503493">"Sharing local Internet connection with device"</string>
-    <string name="bluetooth_pan_profile_summary_use_for" msgid="7422039765025340313">"Use for Internet access"</string>
+    <string name="bluetooth_pan_user_profile_summary_connected" msgid="380469653827505727">"Connected to device for internet access"</string>
+    <string name="bluetooth_pan_nap_profile_summary_connected" msgid="3744773111299503493">"Sharing local internet connection with device"</string>
+    <string name="bluetooth_pan_profile_summary_use_for" msgid="7422039765025340313">"Use for internet access"</string>
     <string name="bluetooth_map_profile_summary_use_for" msgid="4453622103977592583">"Use for map"</string>
     <string name="bluetooth_sap_profile_summary_use_for" msgid="6204902866176714046">"Use for SIM access"</string>
     <string name="bluetooth_a2dp_profile_summary_use_for" msgid="7324694226276491807">"Use for media audio"</string>
@@ -146,17 +146,17 @@
     <string name="bluetooth_pairing_rejected_error_message" msgid="5943444352777314442">"Pairing rejected by <xliff:g id="DEVICE_NAME">%1$s</xliff:g>."</string>
     <string name="bluetooth_talkback_computer" msgid="3736623135703893773">"Computer"</string>
     <string name="bluetooth_talkback_headset" msgid="3406852564400882682">"Headset"</string>
-    <string name="bluetooth_talkback_phone" msgid="868393783858123880">"Telephone"</string>
+    <string name="bluetooth_talkback_phone" msgid="868393783858123880">"Phone"</string>
     <string name="bluetooth_talkback_imaging" msgid="8781682986822514331">"Imaging"</string>
     <string name="bluetooth_talkback_headphone" msgid="8613073829180337091">"Headphone"</string>
     <string name="bluetooth_talkback_input_peripheral" msgid="5133944817800149942">"Input Peripheral"</string>
     <string name="bluetooth_talkback_bluetooth" msgid="1143241359781999989">"Bluetooth"</string>
-    <string name="accessibility_wifi_off" msgid="1195445715254137155">"Wi-Fi off."</string>
-    <string name="accessibility_no_wifi" msgid="5297119459491085771">"Wi-Fi disconnected."</string>
-    <string name="accessibility_wifi_one_bar" msgid="6025652717281815212">"Wi-Fi one bar."</string>
-    <string name="accessibility_wifi_two_bars" msgid="687800024970972270">"Wi-Fi two bars."</string>
-    <string name="accessibility_wifi_three_bars" msgid="779895671061950234">"Wi-Fi three bars."</string>
-    <string name="accessibility_wifi_signal_full" msgid="7165262794551355617">"Wi-Fi signal full."</string>
+    <string name="accessibility_wifi_off" msgid="1195445715254137155">"Wifi off."</string>
+    <string name="accessibility_no_wifi" msgid="5297119459491085771">"Wifi disconnected."</string>
+    <string name="accessibility_wifi_one_bar" msgid="6025652717281815212">"Wifi one bar."</string>
+    <string name="accessibility_wifi_two_bars" msgid="687800024970972270">"Wifi two bars."</string>
+    <string name="accessibility_wifi_three_bars" msgid="779895671061950234">"Wifi three bars."</string>
+    <string name="accessibility_wifi_signal_full" msgid="7165262794551355617">"Wifi signal full."</string>
     <string name="accessibility_wifi_security_type_none" msgid="162352241518066966">"Open network"</string>
     <string name="accessibility_wifi_security_type_secured" msgid="2399774097343238942">"Secure network"</string>
     <string name="process_kernel_label" msgid="950292573930336765">"Android OS"</string>
@@ -178,7 +178,7 @@
     <string name="tts_default_rate_title" msgid="3964187817364304022">"Speech rate"</string>
     <string name="tts_default_rate_summary" msgid="3781937042151716987">"Speed at which the text is spoken"</string>
     <string name="tts_default_pitch_title" msgid="6988592215554485479">"Pitch"</string>
-    <string name="tts_default_pitch_summary" msgid="9132719475281551884">"Affects the tone of the synthesised speech"</string>
+    <string name="tts_default_pitch_summary" msgid="9132719475281551884">"Affects the tone of the synthesized speech"</string>
     <string name="tts_default_lang_title" msgid="4698933575028098940">"Language"</string>
     <string name="tts_lang_use_system" msgid="6312945299804012406">"Use system language"</string>
     <string name="tts_lang_not_selected" msgid="7927823081096056147">"Language not selected"</string>
@@ -188,7 +188,7 @@
     <string name="tts_install_data_title" msgid="1829942496472751703">"Install voice data"</string>
     <string name="tts_install_data_summary" msgid="3608874324992243851">"Install the voice data required for speech synthesis"</string>
     <string name="tts_engine_security_warning" msgid="3372432853837988146">"This speech synthesis engine may be able to collect all the text that will be spoken, including personal data like passwords and credit card numbers. It comes from the <xliff:g id="TTS_PLUGIN_ENGINE_NAME">%s</xliff:g> engine. Enable the use of this speech synthesis engine?"</string>
-    <string name="tts_engine_network_required" msgid="8722087649733906851">"This language requires a working network connection for Text-to-Speech output."</string>
+    <string name="tts_engine_network_required" msgid="8722087649733906851">"This language requires a working network connection for text-to-speech output."</string>
     <string name="tts_default_sample_string" msgid="6388016028292967973">"This is an example of speech synthesis"</string>
     <string name="tts_status_title" msgid="8190784181389278640">"Default language status"</string>
     <string name="tts_status_ok" msgid="8583076006537547379">"<xliff:g id="LOCALE">%1$s</xliff:g> is fully supported"</string>
@@ -224,7 +224,7 @@
     <string name="apn_settings_not_available" msgid="1147111671403342300">"Access Point Name settings are not available for this user"</string>
     <string name="enable_adb" msgid="8072776357237289039">"USB debugging"</string>
     <string name="enable_adb_summary" msgid="3711526030096574316">"Debug mode when USB is connected"</string>
-    <string name="clear_adb_keys" msgid="3010148733140369917">"Revoke USB debugging authorisations"</string>
+    <string name="clear_adb_keys" msgid="3010148733140369917">"Revoke USB debugging authorizations"</string>
     <string name="enable_adb_wireless" msgid="6973226350963971018">"Wireless debugging"</string>
     <string name="enable_adb_wireless_summary" msgid="7344391423657093011">"Debug mode when Wi‑Fi is connected"</string>
     <string name="adb_wireless_error" msgid="721958772149779856">"Error"</string>
@@ -233,22 +233,22 @@
     <string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"Pair device with QR code"</string>
     <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"Pair new devices using QR code scanner"</string>
     <string name="adb_pair_method_code_title" msgid="1122590300445142904">"Pair device with pairing code"</string>
-    <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"Pair new devices using six-digit code"</string>
+    <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"Pair new devices using six digit code"</string>
     <string name="adb_paired_devices_title" msgid="5268997341526217362">"Paired devices"</string>
     <string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"Currently connected"</string>
     <string name="adb_wireless_device_details_title" msgid="7129369670526565786">"Device details"</string>
     <string name="adb_device_forget" msgid="193072400783068417">"Forget"</string>
     <string name="adb_device_fingerprint_title_format" msgid="291504822917843701">"Device fingerprint: <xliff:g id="FINGERPRINT_PARAM">%1$s</xliff:g>"</string>
     <string name="adb_wireless_connection_failed_title" msgid="664211177427438438">"Connection unsuccessful"</string>
-    <string name="adb_wireless_connection_failed_message" msgid="9213896700171602073">"Make sure that <xliff:g id="DEVICE_NAME">%1$s</xliff:g> is connected to the correct network"</string>
+    <string name="adb_wireless_connection_failed_message" msgid="9213896700171602073">"Make sure <xliff:g id="DEVICE_NAME">%1$s</xliff:g> is connected to the correct network"</string>
     <string name="adb_pairing_device_dialog_title" msgid="7141739231018530210">"Pair with device"</string>
     <string name="adb_pairing_device_dialog_pairing_code_label" msgid="3639239786669722731">"Wi‑Fi pairing code"</string>
     <string name="adb_pairing_device_dialog_failed_title" msgid="3426758947882091735">"Pairing unsuccessful"</string>
-    <string name="adb_pairing_device_dialog_failed_msg" msgid="6611097519661997148">"Make sure that the device is connected to the same network."</string>
+    <string name="adb_pairing_device_dialog_failed_msg" msgid="6611097519661997148">"Make sure the device is connected to the same network."</string>
     <string name="adb_wireless_qrcode_summary" msgid="8051414549011801917">"Pair device over Wi‑Fi by scanning a QR code"</string>
     <string name="adb_wireless_verifying_qrcode_text" msgid="6123192424916029207">"Pairing device…"</string>
     <string name="adb_qrcode_pairing_device_failed_msg" msgid="6936292092592914132">"Failed to pair the device. Either the QR code was incorrect, or the device is not connected to the same network."</string>
-    <string name="adb_wireless_ip_addr_preference_title" msgid="8335132107715311730">"IP address and port"</string>
+    <string name="adb_wireless_ip_addr_preference_title" msgid="8335132107715311730">"IP address &amp; Port"</string>
     <string name="adb_wireless_qrcode_pairing_title" msgid="1906409667944674707">"Scan QR code"</string>
     <string name="adb_wireless_qrcode_pairing_description" msgid="6014121407143607851">"Pair device over Wi‑Fi by scanning a QR code"</string>
     <string name="adb_wireless_no_network_msg" msgid="2365795244718494658">"Please connect to a Wi‑Fi network"</string>
@@ -268,28 +268,28 @@
     <string name="mock_location_app_set" msgid="4706722469342913843">"Mock location app: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="debug_networking_category" msgid="6829757985772659599">"Networking"</string>
     <string name="wifi_display_certification" msgid="1805579519992520381">"Wireless display certification"</string>
-    <string name="wifi_verbose_logging" msgid="1785910450009679371">"Enable Wi‑Fi verbose logging"</string>
+    <string name="wifi_verbose_logging" msgid="1785910450009679371">"Enable Wi‑Fi Verbose Logging"</string>
     <string name="wifi_scan_throttling" msgid="2985624788509913617">"Wi‑Fi scan throttling"</string>
-    <string name="wifi_non_persistent_mac_randomization" msgid="7482769677894247316">"Wi‑Fi non‑persistent MAC randomisation"</string>
+    <string name="wifi_non_persistent_mac_randomization" msgid="7482769677894247316">"Wi‑Fi non‑persistent MAC randomization"</string>
     <string name="mobile_data_always_on" msgid="8275958101875563572">"Mobile data always active"</string>
     <string name="tethering_hardware_offload" msgid="4116053719006939161">"Tethering hardware acceleration"</string>
     <string name="bluetooth_show_devices_without_names" msgid="923584526471885819">"Show Bluetooth devices without names"</string>
     <string name="bluetooth_disable_absolute_volume" msgid="1452342324349203434">"Disable absolute volume"</string>
     <string name="bluetooth_enable_gabeldorsche" msgid="9131730396242883416">"Enable Gabeldorsche"</string>
-    <string name="bluetooth_select_avrcp_version_string" msgid="1710571610177659127">"Bluetooth AVRCP version"</string>
+    <string name="bluetooth_select_avrcp_version_string" msgid="1710571610177659127">"Bluetooth AVRCP Version"</string>
     <string name="bluetooth_select_avrcp_version_dialog_title" msgid="7846922290083709633">"Select Bluetooth AVRCP Version"</string>
-    <string name="bluetooth_select_map_version_string" msgid="526308145174175327">"Bluetooth MAP version"</string>
-    <string name="bluetooth_select_map_version_dialog_title" msgid="7085934373987428460">"Select Bluetooth MAP version"</string>
-    <string name="bluetooth_select_a2dp_codec_type" msgid="952001408455456494">"Bluetooth audio codec"</string>
+    <string name="bluetooth_select_map_version_string" msgid="526308145174175327">"Bluetooth MAP Version"</string>
+    <string name="bluetooth_select_map_version_dialog_title" msgid="7085934373987428460">"Select Bluetooth MAP Version"</string>
+    <string name="bluetooth_select_a2dp_codec_type" msgid="952001408455456494">"Bluetooth Audio Codec"</string>
     <string name="bluetooth_select_a2dp_codec_type_dialog_title" msgid="7510542404227225545">"Trigger Bluetooth Audio Codec\nSelection"</string>
-    <string name="bluetooth_select_a2dp_codec_sample_rate" msgid="1638623076480928191">"Bluetooth audio sample rate"</string>
+    <string name="bluetooth_select_a2dp_codec_sample_rate" msgid="1638623076480928191">"Bluetooth Audio Sample Rate"</string>
     <string name="bluetooth_select_a2dp_codec_sample_rate_dialog_title" msgid="5876305103137067798">"Trigger Bluetooth Audio Codec\nSelection: Sample Rate"</string>
-    <string name="bluetooth_select_a2dp_codec_type_help_info" msgid="8647200416514412338">"Grey-out means not supported by phone or headset"</string>
-    <string name="bluetooth_select_a2dp_codec_bits_per_sample" msgid="6253965294594390806">"Bluetooth audio bits per sample"</string>
+    <string name="bluetooth_select_a2dp_codec_type_help_info" msgid="8647200416514412338">"Gray-out means not supported by phone or headset"</string>
+    <string name="bluetooth_select_a2dp_codec_bits_per_sample" msgid="6253965294594390806">"Bluetooth Audio Bits Per Sample"</string>
     <string name="bluetooth_select_a2dp_codec_bits_per_sample_dialog_title" msgid="4898693684282596143">"Trigger Bluetooth Audio Codec\nSelection: Bits Per Sample"</string>
-    <string name="bluetooth_select_a2dp_codec_channel_mode" msgid="364277285688014427">"Bluetooth audio channel mode"</string>
+    <string name="bluetooth_select_a2dp_codec_channel_mode" msgid="364277285688014427">"Bluetooth Audio Channel Mode"</string>
     <string name="bluetooth_select_a2dp_codec_channel_mode_dialog_title" msgid="2076949781460359589">"Trigger Bluetooth Audio Codec\nSelection: Channel Mode"</string>
-    <string name="bluetooth_select_a2dp_codec_ldac_playback_quality" msgid="3233402355917446304">"Bluetooth audio LDAC codec: Playback quality"</string>
+    <string name="bluetooth_select_a2dp_codec_ldac_playback_quality" msgid="3233402355917446304">"Bluetooth Audio LDAC Codec: Playback Quality"</string>
     <string name="bluetooth_select_a2dp_codec_ldac_playback_quality_dialog_title" msgid="7274396574659784285">"Trigger Bluetooth Audio LDAC\nCodec Selection: Playback Quality"</string>
     <string name="bluetooth_select_a2dp_codec_streaming_label" msgid="2040810756832027227">"Streaming: <xliff:g id="STREAMING_PARAMETER">%1$s</xliff:g>"</string>
     <string name="select_private_dns_configuration_title" msgid="7887550926056143018">"Private DNS"</string>
@@ -301,14 +301,14 @@
     <string name="private_dns_mode_provider_failure" msgid="8356259467861515108">"Couldn\'t connect"</string>
     <string name="wifi_display_certification_summary" msgid="8111151348106907513">"Show options for wireless display certification"</string>
     <string name="wifi_verbose_logging_summary" msgid="4993823188807767892">"Increase Wi‑Fi logging level, show per SSID RSSI in Wi‑Fi Picker"</string>
-    <string name="wifi_scan_throttling_summary" msgid="2577105472017362814">"Reduces battery drain and improves network performance"</string>
-    <string name="wifi_non_persistent_mac_randomization_summary" msgid="2159794543105053930">"When this mode is enabled, this device’s MAC address may change each time that it connects to a network that has MAC randomisation enabled."</string>
+    <string name="wifi_scan_throttling_summary" msgid="2577105472017362814">"Reduces battery drain &amp; improves network performance"</string>
+    <string name="wifi_non_persistent_mac_randomization_summary" msgid="2159794543105053930">"When this mode is enabled, this device’s MAC address may change each time it connects to a network that has MAC randomization enabled."</string>
     <string name="wifi_metered_label" msgid="8737187690304098638">"Metered"</string>
     <string name="wifi_unmetered_label" msgid="6174142840934095093">"Unmetered"</string>
     <string name="select_logd_size_title" msgid="1604578195914595173">"Logger buffer sizes"</string>
     <string name="select_logd_size_dialog_title" msgid="2105401994681013578">"Select Logger sizes per log buffer"</string>
     <string name="dev_logpersist_clear_warning_title" msgid="8631859265777337991">"Clear logger persistent storage?"</string>
-    <string name="dev_logpersist_clear_warning_message" msgid="6447590867594287413">"When we are no longer monitoring with the persistent logger, we are required to erase the logger data resident on your device."</string>
+    <string name="dev_logpersist_clear_warning_message" msgid="6447590867594287413">"When we no longer are monitoring with the persistent logger, we are required to erase the logger data resident on your device."</string>
     <string name="select_logpersist_title" msgid="447071974007104196">"Store logger data persistently on device"</string>
     <string name="select_logpersist_dialog_title" msgid="7745193591195485594">"Select log buffers to store persistently on device"</string>
     <string name="select_usb_configuration_title" msgid="6339801314922294586">"Select USB Configuration"</string>
@@ -319,22 +319,22 @@
     <string name="mobile_data_always_on_summary" msgid="1112156365594371019">"Always keep mobile data active, even when Wi‑Fi is active (for fast network switching)."</string>
     <string name="tethering_hardware_offload_summary" msgid="7801345335142803029">"Use tethering hardware acceleration if available"</string>
     <string name="adb_warning_title" msgid="7708653449506485728">"Allow USB debugging?"</string>
-    <string name="adb_warning_message" msgid="8145270656419669221">"USB debugging is intended for development purposes only. Use it to copy data between your computer and your device, install apps on your device without notification and read log data."</string>
+    <string name="adb_warning_message" msgid="8145270656419669221">"USB debugging is intended for development purposes only. Use it to copy data between your computer and your device, install apps on your device without notification, and read log data."</string>
     <string name="adbwifi_warning_title" msgid="727104571653031865">"Allow wireless debugging?"</string>
     <string name="adbwifi_warning_message" msgid="8005936574322702388">"Wireless debugging is intended for development purposes only. Use it to copy data between your computer and your device, install apps on your device without notification, and read log data."</string>
-    <string name="adb_keys_warning_message" msgid="2968555274488101220">"Revoke access to USB debugging from all computers you\'ve previously authorised?"</string>
+    <string name="adb_keys_warning_message" msgid="2968555274488101220">"Revoke access to USB debugging from all computers you’ve previously authorized?"</string>
     <string name="dev_settings_warning_title" msgid="8251234890169074553">"Allow development settings?"</string>
     <string name="dev_settings_warning_message" msgid="37741686486073668">"These settings are intended for development use only. They can cause your device and the applications on it to break or misbehave."</string>
     <string name="verify_apps_over_usb_title" msgid="6031809675604442636">"Verify apps over USB"</string>
-    <string name="verify_apps_over_usb_summary" msgid="1317933737581167839">"Check apps installed via ADB/ADT for harmful behaviour."</string>
+    <string name="verify_apps_over_usb_summary" msgid="1317933737581167839">"Check apps installed via ADB/ADT for harmful behavior."</string>
     <string name="bluetooth_show_devices_without_names_summary" msgid="780964354377854507">"Bluetooth devices without names (MAC addresses only) will be displayed"</string>
     <string name="bluetooth_disable_absolute_volume_summary" msgid="2006309932135547681">"Disables the Bluetooth absolute volume feature in case of volume issues with remote devices such as unacceptably loud volume or lack of control."</string>
     <string name="bluetooth_enable_gabeldorsche_summary" msgid="2054730331770712629">"Enables the Bluetooth Gabeldorsche feature stack."</string>
-    <string name="enhanced_connectivity_summary" msgid="1576414159820676330">"Enables the enhanced connectivity feature."</string>
+    <string name="enhanced_connectivity_summary" msgid="1576414159820676330">"Enables the Enhanced Connectivity feature."</string>
     <string name="enable_terminal_title" msgid="3834790541986303654">"Local terminal"</string>
     <string name="enable_terminal_summary" msgid="2481074834856064500">"Enable terminal app that offers local shell access"</string>
     <string name="hdcp_checking_title" msgid="3155692785074095986">"HDCP checking"</string>
-    <string name="hdcp_checking_dialog_title" msgid="7691060297616217781">"Set HDCP checking behaviour"</string>
+    <string name="hdcp_checking_dialog_title" msgid="7691060297616217781">"Set HDCP checking behavior"</string>
     <string name="debug_debugging_category" msgid="535341063709248842">"Debugging"</string>
     <string name="debug_app" msgid="8903350241392391766">"Select debug app"</string>
     <string name="debug_app_not_set" msgid="1934083001283807188">"No debug application set"</string>
@@ -363,7 +363,7 @@
     <string name="debug_hw_overdraw" msgid="8944851091008756796">"Debug GPU overdraw"</string>
     <string name="disable_overlays" msgid="4206590799671557143">"Disable HW overlays"</string>
     <string name="disable_overlays_summary" msgid="1954852414363338166">"Always use GPU for screen compositing"</string>
-    <string name="simulate_color_space" msgid="1206503300335835151">"Simulate colour space"</string>
+    <string name="simulate_color_space" msgid="1206503300335835151">"Simulate color space"</string>
     <string name="enable_opengl_traces_title" msgid="4638773318659125196">"Enable OpenGL traces"</string>
     <string name="usb_audio_disable_routing" msgid="3367656923544254975">"Disable USB audio routing"</string>
     <string name="usb_audio_disable_routing_summary" msgid="8768242894849534699">"Disable automatic routing to USB audio peripherals"</string>
@@ -379,31 +379,31 @@
     <string name="enable_gpu_debug_layers" msgid="4986675516188740397">"Enable GPU debug layers"</string>
     <string name="enable_gpu_debug_layers_summary" msgid="4921521407377170481">"Allow loading GPU debug layers for debug apps"</string>
     <string name="enable_verbose_vendor_logging" msgid="1196698788267682072">"Enable verbose vendor logging"</string>
-    <string name="enable_verbose_vendor_logging_summary" msgid="5426292185780393708">"Include additional device-specific vendor logs in bug reports, which may contain private information, use more battery and/or use more storage."</string>
+    <string name="enable_verbose_vendor_logging_summary" msgid="5426292185780393708">"Include additional device-specific vendor logs in bug reports, which may contain private information, use more battery, and/or use more storage."</string>
     <string name="window_animation_scale_title" msgid="5236381298376812508">"Window animation scale"</string>
     <string name="transition_animation_scale_title" msgid="1278477690695439337">"Transition animation scale"</string>
     <string name="animator_duration_scale_title" msgid="7082913931326085176">"Animator duration scale"</string>
     <string name="overlay_display_devices_title" msgid="5411894622334469607">"Simulate secondary displays"</string>
     <string name="debug_applications_category" msgid="5394089406638954196">"Apps"</string>
-    <string name="immediately_destroy_activities" msgid="1826287490705167403">"Don\'t keep activities"</string>
+    <string name="immediately_destroy_activities" msgid="1826287490705167403">"Don’t keep activities"</string>
     <string name="immediately_destroy_activities_summary" msgid="6289590341144557614">"Destroy every activity as soon as the user leaves it"</string>
     <string name="app_process_limit_title" msgid="8361367869453043007">"Background process limit"</string>
     <string name="show_all_anrs" msgid="9160563836616468726">"Show background ANRs"</string>
-    <string name="show_all_anrs_summary" msgid="8562788834431971392">"Display App Not Responding dialogue for background apps"</string>
+    <string name="show_all_anrs_summary" msgid="8562788834431971392">"Display App Not Responding dialog for background apps"</string>
     <string name="show_notification_channel_warnings" msgid="3448282400127597331">"Show notification channel warnings"</string>
     <string name="show_notification_channel_warnings_summary" msgid="68031143745094339">"Displays on-screen warning when an app posts a notification without a valid channel"</string>
     <string name="force_allow_on_external" msgid="9187902444231637880">"Force allow apps on external"</string>
     <string name="force_allow_on_external_summary" msgid="8525425782530728238">"Makes any app eligible to be written to external storage, regardless of manifest values"</string>
-    <string name="force_resizable_activities" msgid="7143612144399959606">"Force activities to be resizeable"</string>
-    <string name="force_resizable_activities_summary" msgid="2490382056981583062">"Make all activities resizeable for multi-window, regardless of manifest values."</string>
+    <string name="force_resizable_activities" msgid="7143612144399959606">"Force activities to be resizable"</string>
+    <string name="force_resizable_activities_summary" msgid="2490382056981583062">"Make all activities resizable for multi-window, regardless of manifest values."</string>
     <string name="enable_freeform_support" msgid="7599125687603914253">"Enable freeform windows"</string>
     <string name="enable_freeform_support_summary" msgid="1822862728719276331">"Enable support for experimental freeform windows."</string>
     <string name="desktop_mode" msgid="2389067840550544462">"Desktop mode"</string>
     <string name="local_backup_password_title" msgid="4631017948933578709">"Desktop backup password"</string>
-    <string name="local_backup_password_summary_none" msgid="7646898032616361714">"Desktop full backups aren\'t currently protected"</string>
+    <string name="local_backup_password_summary_none" msgid="7646898032616361714">"Desktop full backups aren’t currently protected"</string>
     <string name="local_backup_password_summary_change" msgid="1707357670383995567">"Tap to change or remove the password for desktop full backups"</string>
     <string name="local_backup_password_toast_success" msgid="4891666204428091604">"New backup password set"</string>
-    <string name="local_backup_password_toast_confirmation_mismatch" msgid="2994718182129097733">"New password and confirmation don\'t match"</string>
+    <string name="local_backup_password_toast_confirmation_mismatch" msgid="2994718182129097733">"New password and confirmation don’t match"</string>
     <string name="local_backup_password_toast_validation_failure" msgid="714669442363647122">"Failure setting backup password"</string>
     <string name="loading_injected_setting_summary" msgid="8394446285689070348">"Loading…"</string>
   <string-array name="color_mode_names">
@@ -412,9 +412,9 @@
     <item msgid="6564241960833766170">"Standard"</item>
   </string-array>
   <string-array name="color_mode_descriptions">
-    <item msgid="6828141153199944847">"Enhanced colours"</item>
-    <item msgid="4548987861791236754">"Natural colours as seen by the eye"</item>
-    <item msgid="1282170165150762976">"Colours optimised for digital content"</item>
+    <item msgid="6828141153199944847">"Enhanced colors"</item>
+    <item msgid="4548987861791236754">"Natural colors as seen by the eye"</item>
+    <item msgid="1282170165150762976">"Colors optimized for digital content"</item>
   </string-array>
     <string name="inactive_apps_title" msgid="5372523625297212320">"Standby apps"</string>
     <string name="inactive_app_inactive_summary" msgid="3161222402614236260">"Inactive. Tap to toggle."</string>
@@ -431,15 +431,15 @@
     <string name="select_webview_provider_title" msgid="3917815648099445503">"WebView implementation"</string>
     <string name="select_webview_provider_dialog_title" msgid="2444261109877277714">"Set WebView implementation"</string>
     <string name="select_webview_provider_toast_text" msgid="8512254949169359848">"This choice is no longer valid. Try again."</string>
-    <string name="picture_color_mode" msgid="1013807330552931903">"Picture colour mode"</string>
+    <string name="picture_color_mode" msgid="1013807330552931903">"Picture color mode"</string>
     <string name="picture_color_mode_desc" msgid="151780973768136200">"Use sRGB"</string>
     <string name="daltonizer_mode_disabled" msgid="403424372812399228">"Disabled"</string>
     <string name="daltonizer_mode_monochromacy" msgid="362060873835885014">"Monochromacy"</string>
     <string name="daltonizer_mode_deuteranomaly" msgid="3507284319584683963">"Deuteranomaly (red-green)"</string>
     <string name="daltonizer_mode_protanomaly" msgid="7805583306666608440">"Protanomaly (red-green)"</string>
     <string name="daltonizer_mode_tritanomaly" msgid="7135266249220732267">"Tritanomaly (blue-yellow)"</string>
-    <string name="accessibility_display_daltonizer_preference_title" msgid="1810693571332381974">"Colour correction"</string>
-    <string name="accessibility_display_daltonizer_preference_subtitle" msgid="1522101114585266455">"Colour correction can be helpful when you want to:&lt;br/&gt; &lt;ol&gt; &lt;li&gt;&amp;nbsp;See colours more accurately&lt;/li&gt; &lt;li&gt;&amp;nbsp;Remove colours to help you focus&lt;/li&gt; &lt;/ol&gt;"</string>
+    <string name="accessibility_display_daltonizer_preference_title" msgid="1810693571332381974">"Color correction"</string>
+    <string name="accessibility_display_daltonizer_preference_subtitle" msgid="1522101114585266455">"Color correction can be helpful when you want to:&lt;br/&gt; &lt;ol&gt; &lt;li&gt;&amp;nbsp;See colors more accurately&lt;/li&gt; &lt;li&gt;&amp;nbsp;Remove colors to help you focus&lt;/li&gt; &lt;/ol&gt;"</string>
     <string name="daltonizer_type_overridden" msgid="4509604753672535721">"Overridden by <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="power_remaining_settings_home_page" msgid="4885165789445462557">"<xliff:g id="PERCENTAGE">%1$s</xliff:g> - <xliff:g id="TIME_STRING">%2$s</xliff:g>"</string>
     <string name="power_remaining_duration_only" msgid="8264199158671531431">"About <xliff:g id="TIME_REMAINING">%1$s</xliff:g> left"</string>
@@ -466,7 +466,7 @@
     <string name="power_remaining_duration_shutdown_imminent" product="device" msgid="4374784375644214578">"Device may shut down soon (<xliff:g id="LEVEL">%1$s</xliff:g>)"</string>
     <string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
     <string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
-    <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
+    <string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
     <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
     <string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
     <string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
@@ -477,9 +477,9 @@
     <string name="battery_info_status_discharging" msgid="6962689305413556485">"Not charging"</string>
     <string name="battery_info_status_not_charging" msgid="3371084153747234837">"Connected, not charging"</string>
     <string name="battery_info_status_full" msgid="1339002294876531312">"Charged"</string>
-    <string name="battery_info_status_full_charged" msgid="3536054261505567948">"Fully charged"</string>
+    <string name="battery_info_status_full_charged" msgid="3536054261505567948">"Fully Charged"</string>
     <string name="disabled_by_admin_summary_text" msgid="5343911767402923057">"Controlled by admin"</string>
-    <string name="disabled_by_app_ops_text" msgid="8373595926549098012">"Controlled by restricted setting"</string>
+    <string name="disabled_by_app_ops_text" msgid="8373595926549098012">"Controlled by Restricted Setting"</string>
     <string name="disabled" msgid="8017887509554714950">"Disabled"</string>
     <string name="external_source_trusted" msgid="1146522036773132905">"Allowed"</string>
     <string name="external_source_untrusted" msgid="5037891688911672227">"Not allowed"</string>
@@ -505,13 +505,13 @@
     <string name="active_input_method_subtypes" msgid="4232680535471633046">"Active input methods"</string>
     <string name="use_system_language_to_select_input_method_subtypes" msgid="4865195835541387040">"Use system languages"</string>
     <string name="failed_to_open_app_settings_toast" msgid="764897252657692092">"Failed to open settings for <xliff:g id="SPELL_APPLICATION_NAME">%1$s</xliff:g>"</string>
-    <string name="ime_security_warning" msgid="6547562217880551450">"This input method may be able to collect all the text that you type, including personal data like passwords and credit card numbers. It comes from the app <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g>. Use this input method?"</string>
+    <string name="ime_security_warning" msgid="6547562217880551450">"This input method may be able to collect all the text you type, including personal data like passwords and credit card numbers. It comes from the app <xliff:g id="IME_APPLICATION_NAME">%1$s</xliff:g>. Use this input method?"</string>
     <string name="direct_boot_unaware_dialog_message" msgid="7845398276735021548">"Note: After a reboot, this app can\'t start until you unlock your phone"</string>
     <string name="ims_reg_title" msgid="8197592958123671062">"IMS registration state"</string>
     <string name="ims_reg_status_registered" msgid="884916398194885457">"Registered"</string>
     <string name="ims_reg_status_not_registered" msgid="2989287366045704694">"Not registered"</string>
     <string name="status_unavailable" msgid="5279036186589861608">"Unavailable"</string>
-    <string name="wifi_status_mac_randomized" msgid="466382542497832189">"MAC is randomised"</string>
+    <string name="wifi_status_mac_randomized" msgid="466382542497832189">"MAC is randomized"</string>
     <string name="wifi_tether_connected_summary" msgid="5282919920463340158">"{count,plural, =0{0 device connected}=1{1 device connected}other{# devices connected}}"</string>
     <string name="accessibility_manual_zen_more_time" msgid="5141801092071134235">"More time."</string>
     <string name="accessibility_manual_zen_less_time" msgid="6828877595848229965">"Less time."</string>
@@ -520,7 +520,7 @@
     <string name="done" msgid="381184316122520313">"Done"</string>
     <string name="alarms_and_reminders_label" msgid="6918395649731424294">"Alarms and reminders"</string>
     <string name="alarms_and_reminders_switch_title" msgid="4939393911531826222">"Allow setting alarms and reminders"</string>
-    <string name="alarms_and_reminders_title" msgid="8819933264635406032">"Alarms and reminders"</string>
+    <string name="alarms_and_reminders_title" msgid="8819933264635406032">"Alarms &amp; reminders"</string>
     <string name="alarms_and_reminders_footer_title" msgid="6302587438389079695">"Allow this app to set alarms and schedule time-sensitive actions. This lets the app run in the background, which may use more battery.\n\nIf this permission is off, existing alarms and time-based events scheduled by this app won’t work."</string>
     <string name="keywords_alarms_and_reminders" msgid="6633360095891110611">"schedule, alarm, reminder, clock"</string>
     <string name="zen_mode_enable_dialog_turn_on" msgid="6418297231575050426">"Turn on"</string>
@@ -539,7 +539,7 @@
     <string name="media_transfer_this_device_name" product="default" msgid="2357329267148436433">"This phone"</string>
     <string name="media_transfer_this_device_name" product="tablet" msgid="3714653244000242800">"This tablet"</string>
     <string name="media_transfer_this_phone" msgid="7194341457812151531">"This phone"</string>
-    <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem connecting. Turn device off and back on"</string>
+    <string name="profile_connect_timeout_subtext" msgid="4043408193005851761">"Problem connecting. Turn device off &amp; back on"</string>
     <string name="media_transfer_wired_device_name" msgid="4447880899964056007">"Wired audio device"</string>
     <string name="help_label" msgid="3528360748637781274">"Help and feedback"</string>
     <string name="storage_category" msgid="2287342585424631813">"Storage"</string>
@@ -548,23 +548,23 @@
     <string name="shared_data_no_blobs_text" msgid="3108114670341737434">"There is no shared data for this user."</string>
     <string name="shared_data_query_failure_text" msgid="3489828881998773687">"There was an error fetching shared data. Try again."</string>
     <string name="blob_id_text" msgid="8680078988996308061">"Shared data ID: <xliff:g id="BLOB_ID">%d</xliff:g>"</string>
-    <string name="blob_expires_text" msgid="7882727111491739331">"Expires on <xliff:g id="DATE">%s</xliff:g>"</string>
+    <string name="blob_expires_text" msgid="7882727111491739331">"Expires at <xliff:g id="DATE">%s</xliff:g>"</string>
     <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"There was an error deleting the shared data."</string>
     <string name="shared_data_no_accessors_dialog_text" msgid="8903738462570715315">"There are no leases acquired for this shared data. Would you like to delete it?"</string>
     <string name="accessor_info_title" msgid="8289823651512477787">"Apps sharing data"</string>
     <string name="accessor_no_description_text" msgid="7510967452505591456">"No description provided by the app."</string>
-    <string name="accessor_expires_text" msgid="4625619273236786252">"Lease expires on <xliff:g id="DATE">%s</xliff:g>"</string>
+    <string name="accessor_expires_text" msgid="4625619273236786252">"Lease expires at <xliff:g id="DATE">%s</xliff:g>"</string>
     <string name="delete_blob_text" msgid="2819192607255625697">"Delete shared data"</string>
-    <string name="delete_blob_confirmation_text" msgid="7807446938920827280">"Are you sure that you want to delete this shared data?"</string>
+    <string name="delete_blob_confirmation_text" msgid="7807446938920827280">"Are you sure you want to delete this shared data?"</string>
     <string name="user_add_user_item_summary" msgid="5748424612724703400">"Users have their own apps and content"</string>
     <string name="user_add_profile_item_summary" msgid="5418602404308968028">"You can restrict access to apps and content from your account"</string>
     <string name="user_add_user_item_title" msgid="2394272381086965029">"User"</string>
     <string name="user_add_profile_item_title" msgid="3111051717414643029">"Restricted profile"</string>
     <string name="user_add_user_title" msgid="5457079143694924885">"Add new user?"</string>
-    <string name="user_add_user_message_long" msgid="1527434966294733380">"You can share this device with other people by creating additional users. Each user has their own space, which they can customise with apps, wallpaper and so on. Users can also adjust device settings such as Wi‑Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. Accessibility settings and services may not transfer to the new user."</string>
+    <string name="user_add_user_message_long" msgid="1527434966294733380">"You can share this device with other people by creating additional users. Each user has their own space, which they can customize with apps, wallpaper, and so on. Users can also adjust device settings like Wi‑Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. Accessibility settings and services may not transfer to the new user."</string>
     <string name="user_add_user_message_short" msgid="3295959985795716166">"When you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users."</string>
     <string name="user_setup_dialog_title" msgid="8037342066381939995">"Set up user now?"</string>
-    <string name="user_setup_dialog_message" msgid="269931619868102841">"Make sure that the person is available to take the device and set up their space."</string>
+    <string name="user_setup_dialog_message" msgid="269931619868102841">"Make sure the person is available to take the device and set up their space"</string>
     <string name="user_setup_profile_dialog_message" msgid="4788197052296962620">"Set up profile now?"</string>
     <string name="user_setup_button_setup_now" msgid="1708269547187760639">"Set up now"</string>
     <string name="user_setup_button_setup_later" msgid="8712980133555493516">"Not now"</string>
@@ -573,7 +573,7 @@
     <string name="user_new_profile_name" msgid="2405500423304678841">"New profile"</string>
     <string name="user_info_settings_title" msgid="6351390762733279907">"User info"</string>
     <string name="profile_info_settings_title" msgid="105699672534365099">"Profile info"</string>
-    <string name="user_need_lock_message" msgid="4311424336209509301">"Before you can create a restricted profile, you\'ll need to set up a screen lock to protect your apps and personal data."</string>
+    <string name="user_need_lock_message" msgid="4311424336209509301">"Before you can create a restricted profile, you’ll need to set up a screen lock to protect your apps and personal data."</string>
     <string name="user_set_lock_button" msgid="1427128184982594856">"Set lock"</string>
     <string name="user_switch_to_user" msgid="6975428297154968543">"Switch to <xliff:g id="USER_NAME">%s</xliff:g>"</string>
     <string name="creating_new_user_dialog_message" msgid="7232880257538970375">"Creating new user…"</string>
@@ -616,7 +616,7 @@
     <string name="cached_apps_freezer_disabled" msgid="4816382260660472042">"Disabled"</string>
     <string name="cached_apps_freezer_enabled" msgid="8866703500183051546">"Enabled"</string>
     <string name="cached_apps_freezer_reboot_dialog_text" msgid="695330563489230096">"Your device must be rebooted for this change to apply. Reboot now or cancel."</string>
-    <string name="media_transfer_wired_usb_device_name" msgid="7699141088423210903">"Wired headphones"</string>
+    <string name="media_transfer_wired_usb_device_name" msgid="7699141088423210903">"Wired headphone"</string>
     <string name="wifi_hotspot_switch_on_text" msgid="9212273118217786155">"On"</string>
     <string name="wifi_hotspot_switch_off_text" msgid="7245567251496959764">"Off"</string>
     <string name="carrier_network_change_mode" msgid="4257621815706644026">"Carrier network changing"</string>
diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml
index 29d7d95..cd1e7d1 100644
--- a/packages/SettingsLib/res/values-eu/strings.xml
+++ b/packages/SettingsLib/res/values-eu/strings.xml
@@ -610,7 +610,7 @@
     <string name="user_image_photo_selector" msgid="433658323306627093">"Hautatu argazki bat"</string>
     <string name="failed_attempts_now_wiping_device" msgid="4016329172216428897">"Saiakera oker gehiegi egin dituzu. Gailu honetako datuak ezabatu egingo dira."</string>
     <string name="failed_attempts_now_wiping_user" msgid="469060411789668050">"Saiakera oker gehiegi egin dituzu. Erabiltzailea ezabatu egingo da."</string>
-    <string name="failed_attempts_now_wiping_profile" msgid="7626589520888963129">"Saiakera oker gehiegi egin dituzu. Laneko profila eta bertako datuak ezabatu egingo dira."</string>
+    <string name="failed_attempts_now_wiping_profile" msgid="7626589520888963129">"Saiakera oker gehiegi egin dituzu. Laneko profila eta bertako datuak ezabatuko dira."</string>
     <string name="failed_attempts_now_wiping_dialog_dismiss" msgid="2749889771223578925">"Baztertu"</string>
     <string name="cached_apps_freezer_device_default" msgid="2616594131750144342">"Gailuaren balio lehenetsia"</string>
     <string name="cached_apps_freezer_disabled" msgid="4816382260660472042">"Desgaituta"</string>
diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml
index 1c4e4a0..b1ef50a 100644
--- a/packages/SettingsLib/res/values-hi/strings.xml
+++ b/packages/SettingsLib/res/values-hi/strings.xml
@@ -174,7 +174,7 @@
     <string name="launch_defaults_some" msgid="3631650616557252926">"कुछ डिफ़ॉल्‍ट सेट हैं"</string>
     <string name="launch_defaults_none" msgid="8049374306261262709">"कोई डिफ़ॉल्‍ट सेट नहीं है"</string>
     <string name="tts_settings" msgid="8130616705989351312">"लेख से बोली सेटिंग"</string>
-    <string name="tts_settings_title" msgid="7602210956640483039">"लिखाई को बोली में बदलना"</string>
+    <string name="tts_settings_title" msgid="7602210956640483039">"लिखाई को बोली में बदलने की सुविधा"</string>
     <string name="tts_default_rate_title" msgid="3964187817364304022">"बोली दर"</string>
     <string name="tts_default_rate_summary" msgid="3781937042151716987">"बोलने की गति तय करें"</string>
     <string name="tts_default_pitch_title" msgid="6988592215554485479">"पिच"</string>
diff --git a/packages/SettingsLib/res/values-nb/arrays.xml b/packages/SettingsLib/res/values-nb/arrays.xml
index 7e65fa0..928ebc3 100644
--- a/packages/SettingsLib/res/values-nb/arrays.xml
+++ b/packages/SettingsLib/res/values-nb/arrays.xml
@@ -49,9 +49,9 @@
     <item msgid="1999413958589971747">"Unngår dårlig tilkobling midlertidig"</item>
   </string-array>
   <string-array name="hdcp_checking_titles">
-    <item msgid="2377230797542526134">"Kontrollér aldri"</item>
-    <item msgid="3919638466823112484">"Kontrollér kun DRM-innhold"</item>
-    <item msgid="9048424957228926377">"Kontrollér alltid"</item>
+    <item msgid="2377230797542526134">"Kontroller aldri"</item>
+    <item msgid="3919638466823112484">"Kontroller kun DRM-innhold"</item>
+    <item msgid="9048424957228926377">"Kontroller alltid"</item>
   </string-array>
   <string-array name="hdcp_checking_summaries">
     <item msgid="4045840870658484038">"Bruk aldri HDCP-kontroll"</item>
diff --git a/packages/SettingsLib/res/values-nb/strings.xml b/packages/SettingsLib/res/values-nb/strings.xml
index c292e88..59a2517 100644
--- a/packages/SettingsLib/res/values-nb/strings.xml
+++ b/packages/SettingsLib/res/values-nb/strings.xml
@@ -427,7 +427,7 @@
     <string name="transcode_notification" msgid="5560515979793436168">"Vis omkodingsvarsler"</string>
     <string name="transcode_disable_cache" msgid="3160069309377467045">"Slå av omkodingsbuffer"</string>
     <string name="runningservices_settings_title" msgid="6460099290493086515">"Aktive tjenester"</string>
-    <string name="runningservices_settings_summary" msgid="1046080643262665743">"Se og kontrollér tjenester som kjører"</string>
+    <string name="runningservices_settings_summary" msgid="1046080643262665743">"Se og kontroller tjenester som kjører"</string>
     <string name="select_webview_provider_title" msgid="3917815648099445503">"WebView-implementering"</string>
     <string name="select_webview_provider_dialog_title" msgid="2444261109877277714">"Angi WebView-implementering"</string>
     <string name="select_webview_provider_toast_text" msgid="8512254949169359848">"Dette valget er ikke gyldig lenger. Prøv på nytt."</string>
diff --git a/packages/SettingsLib/res/values-uz/strings.xml b/packages/SettingsLib/res/values-uz/strings.xml
index 1738e01..efb5eb9 100644
--- a/packages/SettingsLib/res/values-uz/strings.xml
+++ b/packages/SettingsLib/res/values-uz/strings.xml
@@ -548,7 +548,7 @@
     <string name="shared_data_no_blobs_text" msgid="3108114670341737434">"Bu foydalanuvchining umumiy maʼlumotlari topilmadi."</string>
     <string name="shared_data_query_failure_text" msgid="3489828881998773687">"Umumiy maʼlumotlarni yuklashda xatolik yuz berdi. Qayta urining."</string>
     <string name="blob_id_text" msgid="8680078988996308061">"Umumiy maʼlumotlar identifikatori: <xliff:g id="BLOB_ID">%d</xliff:g>"</string>
-    <string name="blob_expires_text" msgid="7882727111491739331">"Amal qilish muddati: <xliff:g id="DATE">%s</xliff:g>"</string>
+    <string name="blob_expires_text" msgid="7882727111491739331">"Muddati: <xliff:g id="DATE">%s</xliff:g>"</string>
     <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"Umumiy maʼlumotlarni oʻchirishda xatolik yuz berdi."</string>
     <string name="shared_data_no_accessors_dialog_text" msgid="8903738462570715315">"Bu umumiy maʼlumotlar yuzasidan kelgan soʻrov topilmadi. Oʻchirib tashlansinmi?"</string>
     <string name="accessor_info_title" msgid="8289823651512477787">"Umumiy maʼlumotlar bor ilovalar"</string>
diff --git a/packages/SettingsLib/res/values/carrierid_icon_overrides.xml b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml
new file mode 100644
index 0000000..d2ae52d
--- /dev/null
+++ b/packages/SettingsLib/res/values/carrierid_icon_overrides.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<!--
+  ~ This resource file exists to enumerate all network type icon overrides on a
+  ~ per-carrierId basis
+-->
+<resources>
+    <!--
+    Network type (RAT) icon overrides can be configured here on a per-carrierId basis.
+        1. Add a new TypedArray here, using the naming scheme below
+        2. The entries are (NetworkType, drawable ID) pairs
+        3. Add this array's ID to the MAPPING field of MobileIconCarrierIdOverrides.kt
+    -->
+    <array name="carrierId_2032_iconOverrides">
+        <item>5G_PLUS</item>
+        <item>@drawable/ic_5g_plus_mobiledata_default</item>
+    </array>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/search/stub-src/com/android/settingslib/search/SearchIndexableResourcesBase.java b/packages/SettingsLib/search/stub-src/com/android/settingslib/search/SearchIndexableResourcesBase.java
index 4870d45..4063b93 100644
--- a/packages/SettingsLib/search/stub-src/com/android/settingslib/search/SearchIndexableResourcesBase.java
+++ b/packages/SettingsLib/search/stub-src/com/android/settingslib/search/SearchIndexableResourcesBase.java
@@ -24,11 +24,11 @@
 public class SearchIndexableResourcesBase implements SearchIndexableResources {
 
     @Override
-    public Collection<Class> getProviderValues() {
+    public Collection<SearchIndexableData> getProviderValues() {
         throw new RuntimeException("STUB!");
     }
 
-    public void addIndex(Class indexClass) {
+    public void addIndex(SearchIndexableData indexClass) {
         throw new RuntimeException("STUB!");
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index a822e18..edaa0fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -10,8 +10,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.Signature;
 import android.content.pm.UserInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
@@ -62,7 +60,6 @@
     static final String STORAGE_MANAGER_ENABLED_PROPERTY =
             "ro.storage_manager.enabled";
 
-    private static Signature[] sSystemSignature;
     private static String sPermissionControllerPackageName;
     private static String sServicesSystemSharedLibPackageName;
     private static String sSharedSystemSharedLibPackageName;
@@ -374,13 +371,21 @@
     }
 
     /**
+     * @deprecated Use {@link #isSystemPackage(Resources, PackageManager, ApplicationInfo)} instead.
+     */
+    @Deprecated
+    public static boolean isSystemPackage(Resources resources, PackageManager pm, PackageInfo pkg) {
+        return pkg.applicationInfo != null && isSystemPackage(resources, pm, pkg.applicationInfo);
+    }
+
+    /**
      * Determine whether a package is a "system package", in which case certain things (like
      * disabling notifications or disabling the package altogether) should be disallowed.
+     *
+     * Note: This function is just for UI treatment, and should not be used for security purposes.
      */
-    public static boolean isSystemPackage(Resources resources, PackageManager pm, PackageInfo pkg) {
-        if (sSystemSignature == null) {
-            sSystemSignature = new Signature[]{getSystemSignature(pm)};
-        }
+    public static boolean isSystemPackage(
+            Resources resources, PackageManager pm, @NonNull ApplicationInfo app) {
         if (sPermissionControllerPackageName == null) {
             sPermissionControllerPackageName = pm.getPermissionControllerPackageName();
         }
@@ -390,29 +395,12 @@
         if (sSharedSystemSharedLibPackageName == null) {
             sSharedSystemSharedLibPackageName = pm.getSharedSystemSharedLibraryPackageName();
         }
-        return (sSystemSignature[0] != null
-                && sSystemSignature[0].equals(getFirstSignature(pkg)))
-                || pkg.packageName.equals(sPermissionControllerPackageName)
-                || pkg.packageName.equals(sServicesSystemSharedLibPackageName)
-                || pkg.packageName.equals(sSharedSystemSharedLibPackageName)
-                || pkg.packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
-                || isDeviceProvisioningPackage(resources, pkg.packageName);
-    }
-
-    private static Signature getFirstSignature(PackageInfo pkg) {
-        if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) {
-            return pkg.signatures[0];
-        }
-        return null;
-    }
-
-    private static Signature getSystemSignature(PackageManager pm) {
-        try {
-            final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES);
-            return getFirstSignature(sys);
-        } catch (NameNotFoundException e) {
-        }
-        return null;
+        return app.isSignedWithPlatformKey()
+                || app.packageName.equals(sPermissionControllerPackageName)
+                || app.packageName.equals(sServicesSystemSharedLibPackageName)
+                || app.packageName.equals(sSharedSystemSharedLibPackageName)
+                || app.packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
+                || isDeviceProvisioningPackage(resources, app.packageName);
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 7913c16..65c94ce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -36,8 +36,10 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -1577,8 +1579,8 @@
         public long internalSize;
         public long externalSize;
         public String labelDescription;
-
         public boolean mounted;
+        public boolean showInPersonalTab;
 
         /**
          * Setting this to {@code true} prevents the entry to be filtered by
@@ -1635,6 +1637,33 @@
                 ThreadUtils.postOnBackgroundThread(
                         () -> this.ensureLabelDescriptionLocked(context));
             }
+            this.showInPersonalTab = shouldShowInPersonalTab(context, info.uid);
+        }
+
+        /**
+         * Checks if the user that the app belongs to have the property
+         * {@link UserProperties#SHOW_IN_SETTINGS_WITH_PARENT} set.
+         */
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        boolean shouldShowInPersonalTab(Context context, int uid) {
+            UserManager userManager = UserManager.get(context);
+            int userId = UserHandle.getUserId(uid);
+
+            // Regardless of apk version, if the app belongs to the current user then return true.
+            if (userId == ActivityManager.getCurrentUser()) {
+                return true;
+            }
+
+            // For sdk version < 34, if the app doesn't belong to the current user,
+            // then as per earlier behaviour the app shouldn't be displayed in personal tab.
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                return false;
+            }
+
+            UserProperties userProperties = userManager.getUserProperties(
+                        UserHandle.of(userId));
+            return userProperties.getShowInSettings()
+                        == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT;
         }
 
         public void ensureLabel(Context context) {
@@ -1784,7 +1813,7 @@
 
         @Override
         public boolean filterApp(AppEntry entry) {
-            return UserHandle.getUserId(entry.info.uid) == mCurrentUser;
+            return entry.showInPersonalTab;
         }
     };
 
@@ -1811,7 +1840,7 @@
 
         @Override
         public boolean filterApp(AppEntry entry) {
-            return UserHandle.getUserId(entry.info.uid) != mCurrentUser;
+            return !entry.showInPersonalTab;
         }
     };
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 91b852a..6641db1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -235,7 +235,7 @@
     /**
      * @return whether high quality audio is enabled or not
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         if (bluetoothDevice == null) {
@@ -287,7 +287,7 @@
      * @param device to get codec label from
      * @return the label associated with the device codec
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothDiscoverableTimeoutReceiver.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothDiscoverableTimeoutReceiver.java
index 6ce72bb..3af64e2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothDiscoverableTimeoutReceiver.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothDiscoverableTimeoutReceiver.java
@@ -82,4 +82,4 @@
             Log.e(TAG, "localBluetoothAdapter is NULL!!");
         }
     }
-};
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index eb53ea1..9583a59 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -668,6 +668,12 @@
      * @param bluetoothProfile the Bluetooth profile
      */
     public void onActiveDeviceChanged(boolean isActive, int bluetoothProfile) {
+        if (BluetoothUtils.D) {
+            Log.d(TAG, "onActiveDeviceChanged: "
+                    + "profile " + BluetoothProfile.getProfileName(bluetoothProfile)
+                    + ", device " + mDevice.getAnonymizedAddress()
+                    + ", isActive " + isActive);
+        }
         boolean changed = false;
         switch (bluetoothProfile) {
         case BluetoothProfile.A2DP:
@@ -758,23 +764,16 @@
     }
 
     public boolean isBusy() {
-        for (CachedBluetoothDevice memberDevice : getMemberDevice()) {
-            if (isBusyState(memberDevice)) {
-                return true;
+        synchronized (mProfileLock) {
+            for (LocalBluetoothProfile profile : mProfiles) {
+                int status = getProfileConnectionState(profile);
+                if (status == BluetoothProfile.STATE_CONNECTING
+                        || status == BluetoothProfile.STATE_DISCONNECTING) {
+                    return true;
+                }
             }
+            return getBondState() == BluetoothDevice.BOND_BONDING;
         }
-        return isBusyState(this);
-    }
-
-    private boolean isBusyState(CachedBluetoothDevice device){
-        for (LocalBluetoothProfile profile : device.getProfiles()) {
-            int status = device.getProfileConnectionState(profile);
-            if (status == BluetoothProfile.STATE_CONNECTING
-                    || status == BluetoothProfile.STATE_DISCONNECTING) {
-                return true;
-            }
-        }
-        return device.getBondState() == BluetoothDevice.BOND_BONDING;
     }
 
     private boolean updateProfiles() {
@@ -920,7 +919,14 @@
 
     @Override
     public String toString() {
-        return mDevice.toString();
+        return "CachedBluetoothDevice ("
+                + "anonymizedAddress="
+                + mDevice.getAnonymizedAddress()
+                + ", name="
+                + getName()
+                + ", groupId="
+                + mGroupId
+                + ")";
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 5662ce6..dd56bde 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -188,7 +188,6 @@
     /**
      * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
      * Hearing Aid Service is connected and the HiSyncId's are now available.
-     * @param LocalBluetoothProfileManager profileManager
      */
     public synchronized void updateHearingAidsDevices() {
         mHearingAidDeviceManager.updateHearingAidsDevices();
@@ -356,7 +355,7 @@
      * @return {@code true}, if the device should pair automatically; Otherwise, return
      * {@code false}.
      */
-    public synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
+    private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
         boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null;
         int bondState = device.getBondState();
         if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE
@@ -365,13 +364,47 @@
                     + " , device.getBondState: " + bondState);
             return false;
         }
-
-        Log.d(TAG, "Bond " + device.getName() + " by CSIP");
-        mOngoingSetMemberPair = device;
         return true;
     }
 
     /**
+     * Called when we found a set member of a group. The function will check the {@code groupId} if
+     * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair
+     * , and then pair the device automatically.
+     *
+     * @param device The found device
+     * @param groupId The group id of the found device
+     */
+    public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) {
+        if (!shouldPairByCsip(device, groupId)) {
+            return;
+        }
+        Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP");
+        mOngoingSetMemberPair = device;
+        syncConfigFromMainDevice(device, groupId);
+        device.createBond(BluetoothDevice.TRANSPORT_LE);
+    }
+
+    private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
+        if (!isOngoingPairByCsip(device)) {
+            return;
+        }
+        CachedBluetoothDevice memberDevice = findDevice(device);
+        CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice);
+        if (mainDevice == null) {
+            mainDevice = mCsipDeviceManager.getCachedDevice(groupId);
+        }
+
+        if (mainDevice == null || mainDevice.equals(memberDevice)) {
+            Log.d(TAG, "no mainDevice");
+            return;
+        }
+
+        // The memberDevice set PhonebookAccessPermission
+        device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission());
+    }
+
+    /**
      * Called when the bond state change. If the bond state change is related with the
      * ongoing set member pair, the cachedBluetoothDevice will be created but the UI
      * would not be updated. For the other case, return {@code false} to go through the normal
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index d5de3f0..20a6cd8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -101,7 +101,14 @@
         return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
     }
 
-    private CachedBluetoothDevice getCachedDevice(int groupId) {
+    /**
+     * To find the device with {@code groupId}.
+     *
+     * @param groupId The group id
+     * @return if we could find a device with this {@code groupId} return this device. Otherwise,
+     * return null.
+     */
+    public CachedBluetoothDevice getCachedDevice(int groupId) {
         log("getCachedDevice: groupId: " + groupId);
         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
new file mode 100644
index 0000000..f06aab3
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -0,0 +1,336 @@
+/*
+ * 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.bluetooth;
+
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * HapClientProfile handles the Bluetooth HAP service client role.
+ */
+public class HapClientProfile implements LocalBluetoothProfile {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            HearingAidType.TYPE_INVALID,
+            HearingAidType.TYPE_BINAURAL,
+            HearingAidType.TYPE_MONAURAL,
+            HearingAidType.TYPE_BANDED,
+            HearingAidType.TYPE_RFU
+    })
+
+    /** Hearing aid type definition for HAP Client. */
+    public @interface HearingAidType {
+        int TYPE_INVALID = -1;
+        int TYPE_BINAURAL = BluetoothHapClient.TYPE_BINAURAL;
+        int TYPE_MONAURAL = BluetoothHapClient.TYPE_MONAURAL;
+        int TYPE_BANDED = BluetoothHapClient.TYPE_BANDED;
+        int TYPE_RFU = BluetoothHapClient.TYPE_RFU;
+    }
+
+    static final String NAME = "HapClient";
+    private static final String TAG = "HapClientProfile";
+
+    // Order of this profile in device profiles list
+    private static final int ORDINAL = 1;
+
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final CachedBluetoothDeviceManager mDeviceManager;
+    private final LocalBluetoothProfileManager mProfileManager;
+    private BluetoothHapClient mService;
+    private boolean mIsProfileReady;
+
+    // These callbacks run on the main thread.
+    private final class HapClientServiceListener implements BluetoothProfile.ServiceListener {
+
+        @Override
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            mService = (BluetoothHapClient) proxy;
+            // We just bound to the service, so refresh the UI for any connected HapClient devices.
+            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+            while (!deviceList.isEmpty()) {
+                BluetoothDevice nextDevice = deviceList.remove(0);
+                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+                // Adds a new device into mDeviceManager if it does not exist
+                if (device == null) {
+                    Log.w(TAG, "HapClient profile found new device: " + nextDevice);
+                    device = mDeviceManager.addDevice(nextDevice);
+                }
+                device.onProfileStateChanged(
+                        HapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
+                device.refresh();
+            }
+
+            mIsProfileReady = true;
+            mProfileManager.callServiceConnectedListeners();
+        }
+
+        @Override
+        public void onServiceDisconnected(int profile) {
+            mIsProfileReady = false;
+            mProfileManager.callServiceDisconnectedListeners();
+        }
+    }
+
+    HapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+            LocalBluetoothProfileManager profileManager) {
+        mDeviceManager = deviceManager;
+        mProfileManager = profileManager;
+        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+        if (bluetoothManager != null) {
+            mBluetoothAdapter = bluetoothManager.getAdapter();
+            mBluetoothAdapter.getProfileProxy(context, new HapClientServiceListener(),
+                    BluetoothProfile.HAP_CLIENT);
+        } else {
+            mBluetoothAdapter = null;
+        }
+    }
+
+    /**
+     * Get hearing aid devices matching connection states{
+     * {@code BluetoothProfile.STATE_CONNECTED},
+     * {@code BluetoothProfile.STATE_CONNECTING},
+     * {@code BluetoothProfile.STATE_DISCONNECTING}}
+     *
+     * @return Matching device list
+     */
+    public List<BluetoothDevice> getConnectedDevices() {
+        return getDevicesByStates(new int[] {
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTING});
+    }
+
+    /**
+     * Get hearing aid devices matching connection states{
+     * {@code BluetoothProfile.STATE_DISCONNECTED},
+     * {@code BluetoothProfile.STATE_CONNECTED},
+     * {@code BluetoothProfile.STATE_CONNECTING},
+     * {@code BluetoothProfile.STATE_DISCONNECTING}}
+     *
+     * @return Matching device list
+     */
+    public List<BluetoothDevice> getConnectableDevices() {
+        return getDevicesByStates(new int[] {
+                BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTING});
+    }
+
+    private List<BluetoothDevice> getDevicesByStates(int[] states) {
+        if (mService == null) {
+            return new ArrayList<>(0);
+        }
+        return mService.getDevicesMatchingConnectionStates(states);
+    }
+
+    /**
+     * Gets the hearing aid type of the device.
+     *
+     * @param device is the device for which we want to get the hearing aid type
+     * @return hearing aid type
+     */
+    @HearingAidType
+    public int getHearingAidType(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return HearingAidType.TYPE_INVALID;
+        }
+        return mService.getHearingAidType(device);
+    }
+
+    /**
+     * Gets if this device supports synchronized presets or not
+     *
+     * @param device is the device for which we want to know if supports synchronized presets
+     * @return {@code true} if the device supports synchronized presets
+     */
+    public boolean supportSynchronizedPresets(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return false;
+        }
+        return mService.supportSynchronizedPresets(device);
+    }
+
+    /**
+     * Gets if this device supports independent presets or not
+     *
+     * @param device is the device for which we want to know if supports independent presets
+     * @return {@code true} if the device supports independent presets
+     */
+    public boolean supportIndependentPresets(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return false;
+        }
+        return mService.supportIndependentPresets(device);
+    }
+
+    /**
+     * Gets if this device supports dynamic presets or not
+     *
+     * @param device is the device for which we want to know if supports dynamic presets
+     * @return {@code true} if the device supports dynamic presets
+     */
+    public boolean supportDynamicPresets(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return false;
+        }
+        return mService.supportDynamicPresets(device);
+    }
+
+    /**
+     * Gets if this device supports writable presets or not
+     *
+     * @param device is the device for which we want to know if supports writable presets
+     * @return {@code true} if the device supports writable presets
+     */
+    public boolean supportWritablePresets(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            return false;
+        }
+        return mService.supportWritablePresets(device);
+    }
+
+    @Override
+    public boolean accessProfileEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isAutoConnectable() {
+        return true;
+    }
+
+    @Override
+    public int getConnectionStatus(BluetoothDevice device) {
+        if (mService == null) {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+        return mService.getConnectionState(device);
+    }
+
+    @Override
+    public boolean isEnabled(BluetoothDevice device) {
+        if (mService == null || device == null) {
+            return false;
+        }
+        return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
+    }
+
+    @Override
+    public int getConnectionPolicy(BluetoothDevice device) {
+        if (mService == null || device == null) {
+            return CONNECTION_POLICY_FORBIDDEN;
+        }
+        return mService.getConnectionPolicy(device);
+    }
+
+    @Override
+    public boolean setEnabled(BluetoothDevice device, boolean enabled) {
+        boolean isEnabled = false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        if (enabled) {
+            if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
+                isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
+            }
+        } else {
+            isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
+        }
+
+        return isEnabled;
+    }
+
+    @Override
+    public boolean isProfileReady() {
+        return mIsProfileReady;
+    }
+
+    @Override
+    public int getProfileId() {
+        return BluetoothProfile.HAP_CLIENT;
+    }
+
+    @Override
+    public int getOrdinal() {
+        return ORDINAL;
+    }
+
+    @Override
+    public int getNameResource(BluetoothDevice device) {
+        return R.string.bluetooth_profile_hearing_aid;
+    }
+
+    @Override
+    public int getSummaryResourceForDevice(BluetoothDevice device) {
+        int state = getConnectionStatus(device);
+        switch (state) {
+            case BluetoothProfile.STATE_DISCONNECTED:
+                return R.string.bluetooth_hearing_aid_profile_summary_use_for;
+
+            case BluetoothProfile.STATE_CONNECTED:
+                return R.string.bluetooth_hearing_aid_profile_summary_connected;
+
+            default:
+                return BluetoothUtils.getConnectionStateSummary(state);
+        }
+    }
+
+    @Override
+    public int getDrawableResource(BluetoothClass btClass) {
+        return com.android.internal.R.drawable.ic_bt_hearing_aid;
+    }
+
+    /**
+     * Gets the name of this class
+     *
+     * @return the name of this class
+     */
+    public String toString() {
+        return NAME;
+    }
+
+    protected void finalize() {
+        Log.d(TAG, "finalize()");
+        if (mService != null) {
+            try {
+                mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, mService);
+                mService = null;
+            } catch (Throwable t) {
+                Log.w(TAG, "Error cleaning up HAP Client proxy", t);
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 8a9f9dd..fb861da 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -231,7 +231,7 @@
             if (DEBUG) {
                 Log.d(TAG, "Adding local Volume Control profile");
             }
-            mVolumeControlProfile = new VolumeControlProfile();
+            mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
             // Note: no event handler for VCP, only for being connectable.
             mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile);
         }
@@ -553,6 +553,10 @@
         return mCsipSetCoordinatorProfile;
     }
 
+    public VolumeControlProfile getVolumeControlProfile() {
+        return mVolumeControlProfile;
+    }
+
     /**
      * Fill in a list of LocalBluetoothProfile objects that are supported by
      * the local device and the remote device.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
index 511df28..57867be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
@@ -16,18 +16,91 @@
 
 package com.android.settingslib.bluetooth;
 
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.RequiresApi;
 
 /**
  * VolumeControlProfile handles Bluetooth Volume Control Controller role
  */
 public class VolumeControlProfile implements LocalBluetoothProfile {
     private static final String TAG = "VolumeControlProfile";
+    private static boolean DEBUG = true;
     static final String NAME = "VCP";
     // Order of this profile in device profiles list
-    private static final int ORDINAL = 23;
+    private static final int ORDINAL = 1;
+
+    private Context mContext;
+    private final CachedBluetoothDeviceManager mDeviceManager;
+    private final LocalBluetoothProfileManager mProfileManager;
+
+    private BluetoothVolumeControl mService;
+    private boolean mIsProfileReady;
+
+    // These callbacks run on the main thread.
+    private final class VolumeControlProfileServiceListener
+            implements BluetoothProfile.ServiceListener {
+
+        @RequiresApi(Build.VERSION_CODES.S)
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (DEBUG) {
+                Log.d(TAG, "Bluetooth service connected");
+            }
+            mService = (BluetoothVolumeControl) proxy;
+            // We just bound to the service, so refresh the UI for any connected
+            // VolumeControlProfile devices.
+            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+            while (!deviceList.isEmpty()) {
+                BluetoothDevice nextDevice = deviceList.remove(0);
+                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+                // we may add a new device here, but generally this should not happen
+                if (device == null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
+                    }
+                    device = mDeviceManager.addDevice(nextDevice);
+                }
+                device.onProfileStateChanged(VolumeControlProfile.this,
+                        BluetoothProfile.STATE_CONNECTED);
+                device.refresh();
+            }
+
+            mProfileManager.callServiceConnectedListeners();
+            mIsProfileReady = true;
+        }
+
+        public void onServiceDisconnected(int profile) {
+            if (DEBUG) {
+                Log.d(TAG, "Bluetooth service disconnected");
+            }
+            mProfileManager.callServiceDisconnectedListeners();
+            mIsProfileReady = false;
+        }
+    }
+
+    VolumeControlProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+            LocalBluetoothProfileManager profileManager) {
+        mContext = context;
+        mDeviceManager = deviceManager;
+        mProfileManager = profileManager;
+
+        BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
+                new VolumeControlProfile.VolumeControlProfileServiceListener(),
+                BluetoothProfile.VOLUME_CONTROL);
+    }
 
     @Override
     public boolean accessProfileEnabled() {
@@ -39,29 +112,70 @@
         return true;
     }
 
+    /**
+     * Get VolumeControlProfile devices matching connection states{
+     *
+     * @return Matching device list
+     * @code BluetoothProfile.STATE_CONNECTED,
+     * @code BluetoothProfile.STATE_CONNECTING,
+     * @code BluetoothProfile.STATE_DISCONNECTING}
+     */
+    public List<BluetoothDevice> getConnectedDevices() {
+        if (mService == null) {
+            return new ArrayList<BluetoothDevice>(0);
+        }
+        return mService.getDevicesMatchingConnectionStates(
+                new int[]{BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING,
+                        BluetoothProfile.STATE_DISCONNECTING});
+    }
+
     @Override
     public int getConnectionStatus(BluetoothDevice device) {
-        return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle VCP
+        if (mService == null) {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+        return mService.getConnectionState(device);
     }
 
     @Override
     public boolean isEnabled(BluetoothDevice device) {
-        return false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
     }
 
     @Override
     public int getConnectionPolicy(BluetoothDevice device) {
-        return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle VCP
+        if (mService == null || device == null) {
+            return CONNECTION_POLICY_FORBIDDEN;
+        }
+        return mService.getConnectionPolicy(device);
     }
 
     @Override
     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
-        return false;
+        boolean isSuccessful = false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
+        }
+        if (enabled) {
+            if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
+                isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
+            }
+        } else {
+            isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
+        }
+
+        return isSuccessful;
     }
 
     @Override
     public boolean isProfileReady() {
-        return true;
+        return mIsProfileReady;
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java
index 3e33da5..ece8986 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java
@@ -50,6 +50,34 @@
 
     @Override
     public void clicked(int sourceCategory, String key) {
+        final LogMaker logMaker = new LogMaker(MetricsProto.MetricsEvent.ACTION_SETTINGS_TILE_CLICK)
+                .setType(MetricsProto.MetricsEvent.TYPE_ACTION);
+        if (sourceCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
+            logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, sourceCategory);
+        }
+        if (!TextUtils.isEmpty(key)) {
+            logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME,
+                    key);
+        }
+        MetricsLogger.action(logMaker);
+    }
+
+    @Override
+    public void changed(int category, String key, int value) {
+        final LogMaker logMaker = new LogMaker(
+                MetricsProto.MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE)
+                .setType(MetricsProto.MetricsEvent.TYPE_ACTION);
+        if (category != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
+            logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, category);
+        }
+        if (!TextUtils.isEmpty(key)) {
+            logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME,
+                    key);
+            logMaker.addTaggedData(
+                    MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+                    value);
+        }
+        MetricsLogger.action(logMaker);
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java
index cceca13..dcd6cce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java
@@ -39,6 +39,11 @@
     void clicked(int category, String key);
 
     /**
+     * Logs a value changed event when user changed item value.
+     */
+    void changed(int category, String key, int value);
+
+    /**
      * Logs an user action.
      */
     void action(Context context, int category, Pair<Integer, Object>... taggedData);
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
index 915421a..09abc39 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
@@ -108,6 +108,19 @@
     }
 
     /**
+     * Logs a value changed event when user changed item value.
+     *
+     * @param category the target page id
+     * @param key the key id that user clicked
+     * @param value the value that user changed which converted to integer
+     */
+    public void changed(int category, String key, int value) {
+        for (LogWriter writer : mLoggerWriters) {
+            writer.changed(category, key, value);
+        }
+    }
+
+    /**
      * Logs a simple action without page id or attribution
      *
      * @param category the target page
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
index 869de0de..067afa4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
@@ -35,15 +35,22 @@
     private static final String LOG_TAG = "SharedPreferencesLogger";
 
     private final String mTag;
+    private final int mMetricCategory;
     private final Context mContext;
     private final MetricsFeatureProvider mMetricsFeature;
     private final Set<String> mPreferenceKeySet;
 
     public SharedPreferencesLogger(Context context, String tag,
             MetricsFeatureProvider metricsFeature) {
+        this(context, tag, metricsFeature, SettingsEnums.PAGE_UNKNOWN);
+    }
+
+    public SharedPreferencesLogger(Context context, String tag,
+            MetricsFeatureProvider metricsFeature, int metricCategory) {
         mContext = context;
         mTag = tag;
         mMetricsFeature = metricsFeature;
+        mMetricCategory = metricCategory;
         mPreferenceKeySet = new ConcurrentSkipListSet<>();
     }
 
@@ -151,20 +158,15 @@
             return;
         }
         // Pref key exists in set, log its change in metrics.
-        mMetricsFeature.action(SettingsEnums.PAGE_UNKNOWN,
-                SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
-                SettingsEnums.PAGE_UNKNOWN,
-                prefKey,
-                intVal);
+        mMetricsFeature.changed(mMetricCategory, key, intVal);
     }
 
     @VisibleForTesting
     void logPackageName(String key, String value) {
-        final String prefKey = mTag + "/" + key;
-        mMetricsFeature.action(SettingsEnums.PAGE_UNKNOWN,
+        mMetricsFeature.action(mMetricCategory,
                 SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
                 SettingsEnums.PAGE_UNKNOWN,
-                prefKey + ":" + value,
+                key + ":" + value,
                 0);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 1606540..2614644 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -92,7 +92,8 @@
             COMPLICATION_TYPE_AIR_QUALITY,
             COMPLICATION_TYPE_CAST_INFO,
             COMPLICATION_TYPE_HOME_CONTROLS,
-            COMPLICATION_TYPE_SMARTSPACE
+            COMPLICATION_TYPE_SMARTSPACE,
+            COMPLICATION_TYPE_MEDIA_ENTRY
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ComplicationType {
@@ -105,6 +106,7 @@
     public static final int COMPLICATION_TYPE_CAST_INFO = 5;
     public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6;
     public static final int COMPLICATION_TYPE_SMARTSPACE = 7;
+    public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8;
 
     private final Context mContext;
     private final IDreamManager mDreamManager;
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index 7275d6b..1745379 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -22,6 +22,7 @@
 import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER;
 
 import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.ParentalControlsUtilsInternal;
@@ -45,6 +46,8 @@
             return new BiometricActionDisabledByAdminController(stringProvider);
         } else if (isFinancedDevice(context)) {
             return new FinancedDeviceActionDisabledByAdminController(stringProvider);
+        } else if (isSupervisedDevice(context)) {
+            return new SupervisedDeviceActionDisabledByAdminController(stringProvider, restriction);
         } else {
             return new ManagedDeviceActionDisabledByAdminController(
                     stringProvider,
@@ -54,6 +57,15 @@
         }
     }
 
+    private static boolean isSupervisedDevice(Context context) {
+        DevicePolicyManager devicePolicyManager =
+                context.getSystemService(DevicePolicyManager.class);
+        ComponentName supervisionComponent =
+                devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
+                        new UserHandle(UserHandle.myUserId()));
+        return supervisionComponent != null;
+    }
+
     /**
      * @return true if the restriction == UserManager.DISALLOW_BIOMETRIC and parental consent
      * is required.
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
index 6e93494..714accc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.net.Uri;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -60,6 +61,10 @@
             final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
                     .putExtra(Settings.EXTRA_SUPERVISOR_RESTRICTED_SETTING_KEY,
                             Settings.SUPERVISOR_VERIFICATION_SETTING_BIOMETRICS)
+                    .setData(new Uri.Builder()
+                            .scheme("policy")
+                            .appendPath("biometric")
+                            .build())
                     .setPackage(enforcedAdmin.component.getPackageName());
             context.startActivity(intent);
         };
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
index b83837e..7ff91f85 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
@@ -79,6 +79,11 @@
     String getDisabledBiometricsParentConsentTitle();
 
     /**
+     * Returns the dialog title when the setting is blocked by supervision app.
+     */
+    String getDisabledByParentContent();
+
+    /**
      * Returns the dialog contents for when biometrics require parental consent.
      */
     String getDisabledBiometricsParentConsentContent();
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java
new file mode 100644
index 0000000..815293e9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java
@@ -0,0 +1,79 @@
+/*
+ * 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.enterprise;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settingslib.RestrictedLockUtils;
+
+import org.jetbrains.annotations.Nullable;
+
+final class SupervisedDeviceActionDisabledByAdminController
+        extends BaseActionDisabledByAdminController {
+    private static final String TAG = "SupervisedDeviceActionDisabledByAdminController";
+    private final String mRestriction;
+
+    SupervisedDeviceActionDisabledByAdminController(
+            DeviceAdminStringProvider stringProvider, String restriction) {
+        super(stringProvider);
+        mRestriction = restriction;
+    }
+
+    @Override
+    public void setupLearnMoreButton(Context context) {
+
+    }
+
+    @Override
+    public String getAdminSupportTitle(@Nullable String restriction) {
+        return mStringProvider.getDisabledBiometricsParentConsentTitle();
+    }
+
+    @Override
+    public CharSequence getAdminSupportContentString(Context context,
+            @Nullable CharSequence supportMessage) {
+        return mStringProvider.getDisabledByParentContent();
+    }
+
+    @Nullable
+    @Override
+    public DialogInterface.OnClickListener getPositiveButtonListener(Context context,
+            RestrictedLockUtils.EnforcedAdmin enforcedAdmin) {
+        final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
+                .setData(new Uri.Builder()
+                        .scheme("policy")
+                        .appendPath("user_restrictions")
+                        .appendPath(mRestriction)
+                        .build())
+                .setPackage(enforcedAdmin.component.getPackageName());
+        ComponentName resolvedSupervisionActivity =
+                intent.resolveActivity(context.getPackageManager());
+        if (resolvedSupervisionActivity == null) {
+            return null;
+        }
+        return (dialog, which) -> {
+            Log.d(TAG, "Positive button clicked, component: " + enforcedAdmin.component);
+            context.startActivity(intent);
+        };
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 34da305..3e710e4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -24,7 +24,6 @@
 import android.os.UserHandle;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
-import android.text.TextUtils;
 import android.util.KeyValueListParser;
 import android.util.Log;
 import android.util.Slog;
@@ -221,17 +220,14 @@
     }
 
     /**
-     * Reverts battery saver schedule mode to none if we are in a bad state where routine mode
-     * is selected but no app is configured to actually provide the signal.
+     * Reverts battery saver schedule mode to none if routine mode is selected.
      * @param context a valid context
      */
     public static void revertScheduleToNoneIfNeeded(Context context) {
         ContentResolver resolver = context.getContentResolver();
         final int currentMode = Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                 PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
-        boolean providerConfigured = !TextUtils.isEmpty(context.getString(
-                com.android.internal.R.string.config_batterySaverScheduleProvider));
-        if (currentMode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC && !providerConfigured) {
+        if (currentMode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC) {
             Global.putInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
             Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                     PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
index 5fa04f9..faea5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -412,14 +412,13 @@
     }
 
     companion object {
-        private const val TAG = "ThemedBatteryDrawable"
-        private const val WIDTH = 12f
-        private const val HEIGHT = 20f
+        const val WIDTH = 12f
+        const val HEIGHT = 20f
         private const val CRITICAL_LEVEL = 15
         // On a 12x20 grid, how wide to make the fill protection stroke.
         // Scales when our size changes
         private const val PROTECTION_STROKE_WIDTH = 3f
         // Arbitrarily chosen for visibility at small sizes
-        private const val PROTECTION_MIN_STROKE_WIDTH = 6f
+        const val PROTECTION_MIN_STROKE_WIDTH = 6f
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index c829bc3..3ba51d2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -293,7 +293,7 @@
             return false;
         }
         setConnectedRecord();
-        mRouterManager.selectRoute(mPackageName, mRouteInfo);
+        mRouterManager.transfer(mPackageName, mRouteInfo);
         return true;
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt
new file mode 100644
index 0000000..a0395b5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/MobileIconCarrierIdOverrides.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.mobile
+
+import android.annotation.DrawableRes
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.R
+import com.android.settingslib.SignalIcon.MobileIconGroup
+
+/**
+ * This class defines a network type (3G, 4G, etc.) override mechanism on a per-carrierId basis.
+ *
+ * Traditionally, carrier-customized network type iconography was achieved using the `MCC/MNC`
+ * resource qualifiers, and swapping out the drawable resource by name. It would look like this:
+ *
+ *     res/
+ *       drawable/
+ *         3g_mobiledata_icon.xml
+ *       drawable-MCC-MNC/
+ *         3g_mobiledata_icon.xml
+ *
+ * This would mean that, provided a context created with this MCC/MNC configuration set, loading
+ * the network type icon through [MobileIconGroup] would provide a carrier-defined network type
+ * icon rather than the AOSP-defined default.
+ *
+ * The MCC/MNC mechanism no longer can fully define carrier-specific network type icons, because
+ * there is no longer a 1:1 mapping between MCC/MNC and carrier. With the advent of MVNOs, multiple
+ * carriers can have the same MCC/MNC value, but wish to differentiate based on their carrier ID.
+ * CarrierId is a newer concept than MCC/MNC, and provides more granularity when it comes to
+ * determining the carrier (e.g. MVNOs can share MCC/MNC values with the network owner), therefore
+ * it can fit all of the same use cases currently handled by `MCC/MNC`, without the need to apply a
+ * configuration context in order to get the proper UI for a given SIM icon.
+ *
+ * NOTE: CarrierId icon overrides will always take precedence over those defined using `MCC/MNC`
+ * resource qualifiers.
+ *
+ * [MAPPING] encodes the relationship between CarrierId and the corresponding override array
+ * that exists in the config.xml. An alternative approach could be to generate the resource name
+ * by string concatenation at run-time:
+ *
+ *    val resName = "carrierId_$carrierId_iconOverrides"
+ *    val override = resources.getResourceIdentifier(resName)
+ *
+ * However, that's going to be far less efficient until MAPPING grows to a sufficient size. For now,
+ * given a relatively small number of entries, we should just maintain the mapping here.
+ */
+interface MobileIconCarrierIdOverrides {
+    @DrawableRes
+    fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int
+    fun carrierIdEntryExists(carrierId: Int): Boolean
+}
+
+class MobileIconCarrierIdOverridesImpl : MobileIconCarrierIdOverrides {
+    @DrawableRes
+    override fun getOverrideFor(carrierId: Int, networkType: String, resources: Resources): Int {
+        val resId = MAPPING[carrierId] ?: return 0
+        val ta = resources.obtainTypedArray(resId)
+        val map = parseNetworkIconOverrideTypedArray(ta)
+        ta.recycle()
+        return map[networkType] ?: 0
+    }
+
+    override fun carrierIdEntryExists(carrierId: Int) =
+        overrideExists(carrierId, MAPPING)
+
+    companion object {
+        private const val TAG = "MobileIconOverrides"
+        /**
+         * This map maintains the lookup from the canonical carrier ID (see below link) to the
+         * corresponding overlay resource. New overrides should add an entry below in order to
+         * change the network type icon resources based on carrier ID
+         *
+         * Refer to the link below for the canonical mapping maintained in AOSP:
+         * https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/latest_carrier_id/carrier_list.textpb
+         */
+        private val MAPPING = mapOf(
+            // 2032 == Xfinity Mobile
+            2032 to R.array.carrierId_2032_iconOverrides,
+        )
+
+        /**
+         * Parse `carrierId_XXXX_iconOverrides` for a particular network type. The resource file
+         * "carrierid_icon_overrides.xml" defines a TypedArray format for overriding specific
+         * network type icons (a.k.a. RAT icons) for a particular carrier ID. The format is defined
+         * as an array of (network type name, drawable) pairs:
+         *    <array name="carrierId_XXXX_iconOverrides>
+         *        <item>NET_TYPE_1</item>
+         *        <item>@drawable/net_type_1_override</item>
+         *        <item>NET_TYPE_2</item>
+         *        <item>@drawable/net_type_2_override</item>
+         *    </array>
+         *
+         * @param ta the [TypedArray] defined in carrierid_icon_overrides.xml
+         * @return the overridden drawable resource ID if it exists, or 0 if it does not
+         */
+        @VisibleForTesting
+        @JvmStatic
+        fun parseNetworkIconOverrideTypedArray(ta: TypedArray): Map<String, Int> {
+            if (ta.length() % 2 != 0) {
+                Log.w(TAG,
+                    "override must contain an even number of (key, value) entries. skipping")
+
+                return mapOf()
+            }
+
+            val result = mutableMapOf<String, Int>()
+            // The array is defined as Pair(String, resourceId), so walk by 2
+            for (i in 0 until ta.length() step 2) {
+                val key = ta.getString(i)
+                val override = ta.getResourceId(i + 1, 0)
+                if (key == null || override == 0) {
+                    Log.w(TAG, "Invalid override found. Skipping")
+                    continue
+                }
+                result[key] = override
+            }
+
+            return result
+        }
+
+        @JvmStatic
+        private fun overrideExists(carrierId: Int, mapping: Map<Int, Int>): Boolean =
+            mapping.containsKey(carrierId)
+    }
+}
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/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
index c1ee7ad..ca457b0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
@@ -20,6 +20,7 @@
 import android.util.Log;
 
 import java.util.List;
+import java.util.Objects;
 
 import androidx.lifecycle.LiveData;
 import androidx.room.Database;
@@ -39,17 +40,27 @@
 
     public abstract MobileNetworkInfoDao mMobileNetworkInfoDao();
 
+    private static MobileNetworkDatabase sInstance;
+    private static final Object sLOCK = new Object();
+
+
     /**
      * Create the MobileNetworkDatabase.
      *
      * @param context The context.
      * @return The MobileNetworkDatabase.
      */
-    public static MobileNetworkDatabase createDatabase(Context context) {
-        return Room.inMemoryDatabaseBuilder(context, MobileNetworkDatabase.class)
-                .fallbackToDestructiveMigration()
-                .enableMultiInstanceInvalidation()
-                .build();
+    public static MobileNetworkDatabase getInstance(Context context) {
+        synchronized (sLOCK) {
+            if (Objects.isNull(sInstance)) {
+                Log.d(TAG, "createDatabase.");
+                sInstance = Room.inMemoryDatabaseBuilder(context, MobileNetworkDatabase.class)
+                        .fallbackToDestructiveMigration()
+                        .enableMultiInstanceInvalidation()
+                        .build();
+            }
+        }
+        return sInstance;
     }
 
     /**
@@ -93,7 +104,7 @@
      * Query the subscription info by the subscription ID from the SubscriptionInfoEntity
      * table.
      */
-    public LiveData<SubscriptionInfoEntity> querySubInfoById(String id) {
+    public SubscriptionInfoEntity querySubInfoById(String id) {
         return mSubscriptionInfoDao().querySubInfoById(id);
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
index 4596637..e835125 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
@@ -37,7 +37,7 @@
 
     @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE "
             + DataServiceUtils.SubscriptionInfoData.COLUMN_ID + " = :subId")
-    LiveData<SubscriptionInfoEntity> querySubInfoById(String subId);
+    SubscriptionInfoEntity querySubInfoById(String subId);
 
     @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE "
             + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID
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/SettingsLib/src/com/android/settingslib/qrcode/OWNERS b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS
new file mode 100644
index 0000000..61c73fb
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS
@@ -0,0 +1,8 @@
+# Default reviewers for this and subdirectories.
+bonianchen@google.com
+changbetty@google.com
+goldmanj@google.com
+wengsu@google.com
+zoeychen@google.com
+
+# Emergency approvers in case the above are not available
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java
index 51cf59c..ac9cdac 100644
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrDecorateView.java
@@ -34,16 +34,16 @@
     private static final float CORNER_LINE_LENGTH = 264f;   // 264dp
     private static final float CORNER_RADIUS = 16f;         // 16dp
 
-    final private int mCornerColor;
-    final private int mFocusedCornerColor;
-    final private int mBackgroundColor;
+    private final int mCornerColor;
+    private final int mFocusedCornerColor;
+    private final int mBackgroundColor;
 
-    final private Paint mStrokePaint;
-    final private Paint mTransparentPaint;
-    final private Paint mBackgroundPaint;
+    private final Paint mStrokePaint;
+    private final Paint mTransparentPaint;
+    private final Paint mBackgroundPaint;
 
-    final private float mRadius;
-    final private float mInnerRidus;
+    private final float mRadius;
+    private final float mInnerRadius;
 
     private Bitmap mMaskBitmap;
     private Canvas mMaskCanvas;
@@ -72,7 +72,7 @@
         mRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS,
                 getResources().getDisplayMetrics());
         // Inner radius needs to minus stroke width for keeping the width of border consistent.
-        mInnerRidus = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+        mInnerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 CORNER_RADIUS - CORNER_STROKE_WIDTH, getResources().getDisplayMetrics());
 
         mCornerColor = context.getResources().getColor(R.color.qr_corner_line_color);
@@ -95,7 +95,10 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
 
-        if(mMaskBitmap == null) {
+        if (!isLaidOut()) {
+            return;
+        }
+        if (mMaskBitmap == null) {
             mMaskBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
             mMaskCanvas = new Canvas(mMaskBitmap);
         }
@@ -105,16 +108,18 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        // Set frame line color.
-        mStrokePaint.setColor(mFocused ? mFocusedCornerColor : mCornerColor);
-        // Draw background color.
-        mMaskCanvas.drawColor(mBackgroundColor);
-        // Draw outer corner.
-        mMaskCanvas.drawRoundRect(mOuterFrame, mRadius, mRadius, mStrokePaint);
-        // Draw inner transparent corner.
-        mMaskCanvas.drawRoundRect(mInnerFrame, mInnerRidus, mInnerRidus, mTransparentPaint);
+        if (mMaskCanvas != null && mMaskBitmap != null) {
+            // Set frame line color.
+            mStrokePaint.setColor(mFocused ? mFocusedCornerColor : mCornerColor);
+            // Draw background color.
+            mMaskCanvas.drawColor(mBackgroundColor);
+            // Draw outer corner.
+            mMaskCanvas.drawRoundRect(mOuterFrame, mRadius, mRadius, mStrokePaint);
+            // Draw inner transparent corner.
+            mMaskCanvas.drawRoundRect(mInnerFrame, mInnerRadius, mInnerRadius, mTransparentPaint);
 
-        canvas.drawBitmap(mMaskBitmap, 0, 0, mBackgroundPaint);
+            canvas.drawBitmap(mMaskBitmap, 0, 0, mBackgroundPaint);
+        }
         super.onDraw(canvas);
     }
 
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index f1e1e7d..c5598bf 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -293,4 +293,15 @@
 
         assertThat(ApplicationsState.FILTER_MOVIES.filterApp(mEntry)).isFalse();
     }
+
+    @Test
+    public void testPersonalAndWorkFiltersDisplaysCorrectApps() {
+        mEntry.showInPersonalTab = true;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
+        assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse();
+
+        mEntry.showInPersonalTab = false;
+        assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
+        assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue();
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
index 39977df..f969a63 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
@@ -41,19 +41,19 @@
         MobileNetworkTypeIcon icon =
                 MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G);
 
-        assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name);
+        assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name);
         assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G);
     }
 
     @Test
     public void getNetworkTypeIcon_unknown_returnsUnknown() {
-        SignalIcon.MobileIconGroup unknownGroup =
-                new SignalIcon.MobileIconGroup("testUnknownNameHere", 45, 6);
+        SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup(
+                "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6);
 
         MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup);
 
         assertThat(icon.getName()).isEqualTo("testUnknownNameHere");
-        assertThat(icon.getIconResId()).isEqualTo(45);
-        assertThat(icon.getContentDescriptionResId()).isEqualTo(6);
+        assertThat(icon.getIconResId()).isEqualTo(6);
+        assertThat(icon.getContentDescriptionResId()).isEqualTo(45);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
index fc2bf0a..39875f7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java
@@ -17,6 +17,7 @@
 package com.android.settingslib.applications;
 
 import static android.os.UserHandle.MU_ENABLED;
+import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -48,9 +49,11 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -58,6 +61,8 @@
 import android.text.TextUtils;
 import android.util.IconDrawableFactory;
 
+import androidx.test.core.app.ApplicationProvider;
+
 import com.android.settingslib.applications.ApplicationsState.AppEntry;
 import com.android.settingslib.applications.ApplicationsState.Callbacks;
 import com.android.settingslib.applications.ApplicationsState.Session;
@@ -71,6 +76,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
@@ -79,6 +85,7 @@
 import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowContextImpl;
 import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.util.ReflectionHelpers;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -95,6 +102,7 @@
     private final static String LAUNCHABLE_PACKAGE_NAME = "com.android.launchable";
 
     private static final int PROFILE_USERID = 10;
+    private static final int PROFILE_USERID2 = 11;
 
     private static final String PKG_1 = "PKG1";
     private static final int OWNER_UID_1 = 1001;
@@ -106,6 +114,10 @@
 
     private static final String PKG_3 = "PKG3";
     private static final int OWNER_UID_3 = 1003;
+    private static final int PROFILE_UID_3 = UserHandle.getUid(PROFILE_USERID2, OWNER_UID_3);
+
+    private static final String CLONE_USER = "clone_user";
+    private static final String RANDOM_USER = "random_user";
 
     /** Class under test */
     private ApplicationsState mApplicationsState;
@@ -113,6 +125,8 @@
 
     private Application mApplication;
 
+    @Spy
+    Context mContext = ApplicationProvider.getApplicationContext();
     @Mock
     private Callbacks mCallbacks;
     @Captor
@@ -738,4 +752,51 @@
         when(configChanges.applyNewConfig(any(Resources.class))).thenReturn(false);
         mApplicationsState.setInterestingConfigChanges(configChanges);
     }
+
+    @Test
+    public void shouldShowInPersonalTab_forCurrentUser_returnsTrue() {
+        ApplicationInfo appInfo = createApplicationInfo(PKG_1);
+        AppEntry primaryUserApp = createAppEntry(appInfo, 1);
+
+        assertThat(primaryUserApp.shouldShowInPersonalTab(mContext, appInfo.uid)).isTrue();
+    }
+
+    @Test
+    public void shouldShowInPersonalTab_userProfilePreU_returnsFalse() {
+        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT",
+                Build.VERSION_CODES.TIRAMISU);
+        // Create an app (and subsequent AppEntry) in a non-primary user profile.
+        ApplicationInfo appInfo1 = createApplicationInfo(PKG_1, PROFILE_UID_1);
+        AppEntry nonPrimaryUserApp = createAppEntry(appInfo1, 1);
+
+        assertThat(nonPrimaryUserApp.shouldShowInPersonalTab(mContext, appInfo1.uid)).isFalse();
+    }
+
+    @Test
+    public void shouldShowInPersonalTab_currentUserIsParent_returnsAsPerUserPropertyOfProfile1() {
+        // Mark system user as parent for both profile users.
+        ShadowUserManager shadowUserManager = Shadow
+                .extract(RuntimeEnvironment.application.getSystemService(UserManager.class));
+        shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID,
+                CLONE_USER, 0);
+        shadowUserManager.addProfile(USER_SYSTEM, PROFILE_USERID2,
+                RANDOM_USER, 0);
+        shadowUserManager.setupUserProperty(PROFILE_USERID,
+                /*showInSettings*/ UserProperties.SHOW_IN_SETTINGS_WITH_PARENT);
+        shadowUserManager.setupUserProperty(PROFILE_USERID2,
+                /*showInSettings*/ UserProperties.SHOW_IN_SETTINGS_NO);
+
+        ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT",
+                Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
+
+        // Treat PROFILE_USERID as a clone user profile and create an app PKG_1 in it.
+        ApplicationInfo appInfo1 = createApplicationInfo(PKG_1, PROFILE_UID_1);
+        // Treat PROFILE_USERID2 as a random non-primary profile and create an app PKG_3 in it.
+        ApplicationInfo appInfo2 = createApplicationInfo(PKG_3, PROFILE_UID_3);
+        AppEntry nonPrimaryUserApp1 = createAppEntry(appInfo1, 1);
+        AppEntry nonPrimaryUserApp2 = createAppEntry(appInfo2, 2);
+
+        assertThat(nonPrimaryUserApp1.shouldShowInPersonalTab(mContext, appInfo1.uid)).isTrue();
+        assertThat(nonPrimaryUserApp2.shouldShowInPersonalTab(mContext, appInfo2.uid)).isFalse();
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
index 62552f91..61802a8 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManagerTest.java
@@ -582,4 +582,24 @@
         assertThat(mCachedDeviceManager.isSubDevice(mDevice2)).isTrue();
         assertThat(mCachedDeviceManager.isSubDevice(mDevice3)).isFalse();
     }
+
+    @Test
+    public void pairDeviceByCsip_device2AndCapGroup1_device2StartsPairing() {
+        doReturn(CAP_GROUP1).when(mCsipSetCoordinatorProfile).getGroupUuidMapByDevice(mDevice1);
+        when(mDevice1.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+        when(mDevice1.getPhonebookAccessPermission()).thenReturn(BluetoothDevice.ACCESS_ALLOWED);
+        CachedBluetoothDevice cachedDevice1 = mCachedDeviceManager.addDevice(mDevice1);
+        assertThat(cachedDevice1).isNotNull();
+        when(mDevice2.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+        CachedBluetoothDevice cachedDevice2 = mCachedDeviceManager.addDevice(mDevice2);
+        assertThat(cachedDevice2).isNotNull();
+
+        int groupId = CAP_GROUP1.keySet().stream().findFirst().orElse(
+                BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+        assertThat(groupId).isNotEqualTo(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+        mCachedDeviceManager.pairDeviceByCsip(mDevice2, groupId);
+
+        verify(mDevice2).setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
+        verify(mDevice2).createBond(BluetoothDevice.TRANSPORT_LE);
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 315ab0a..79e9938 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -1069,80 +1069,4 @@
         assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
         assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
     }
-
-    @Test
-    public void isBusy_mainDeviceIsConnecting_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_mainDeviceIsBonding_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_memberDeviceIsConnecting_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_memberDeviceIsBonding_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_allDevicesAreNotBusy_returnsNotBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isFalse();
-    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java
new file mode 100644
index 0000000..03a792a
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.bluetooth;
+
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.Before;
+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;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class HapClientProfileTest {
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private CachedBluetoothDeviceManager mDeviceManager;
+    @Mock
+    private LocalBluetoothProfileManager mProfileManager;
+    @Mock
+    private BluetoothDevice mBluetoothDevice;
+    @Mock
+    private BluetoothHapClient mService;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private BluetoothProfile.ServiceListener mServiceListener;
+    private HapClientProfile mProfile;
+
+    @Before
+    public void setUp() {
+        mProfile = new HapClientProfile(mContext, mDeviceManager, mProfileManager);
+        final BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+        final ShadowBluetoothAdapter shadowBluetoothAdapter =
+                Shadow.extract(bluetoothManager.getAdapter());
+        mServiceListener = shadowBluetoothAdapter.getServiceListener();
+    }
+
+    @Test
+    public void onServiceConnected_isProfileReady() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        assertThat(mProfile.isProfileReady()).isTrue();
+        verify(mProfileManager).callServiceConnectedListeners();
+    }
+
+    @Test
+    public void onServiceDisconnected_isProfileNotReady() {
+        mServiceListener.onServiceDisconnected(BluetoothProfile.HAP_CLIENT);
+
+        assertThat(mProfile.isProfileReady()).isFalse();
+        verify(mProfileManager).callServiceDisconnectedListeners();
+    }
+
+    @Test
+    public void getConnectionStatus_returnCorrectConnectionState() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionState(mBluetoothDevice))
+                .thenReturn(BluetoothProfile.STATE_CONNECTED);
+
+        assertThat(mProfile.getConnectionStatus(mBluetoothDevice))
+                .isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void isEnabled_connectionPolicyAllowed_returnTrue() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
+
+        assertThat(mProfile.isEnabled(mBluetoothDevice)).isTrue();
+    }
+
+    @Test
+    public void isEnabled_connectionPolicyForbidden_returnFalse() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice))
+                .thenReturn(CONNECTION_POLICY_FORBIDDEN);
+
+        assertThat(mProfile.isEnabled(mBluetoothDevice)).isFalse();
+    }
+
+    @Test
+    public void getConnectionPolicy_returnCorrectConnectionPolicy() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
+
+        assertThat(mProfile.getConnectionPolicy(mBluetoothDevice))
+                .isEqualTo(CONNECTION_POLICY_ALLOWED);
+    }
+
+    @Test
+    public void setEnabled_connectionPolicyAllowed_setConnectionPolicyAllowed_returnFalse() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
+        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_ALLOWED))
+                .thenReturn(true);
+
+        assertThat(mProfile.setEnabled(mBluetoothDevice, true)).isFalse();
+    }
+
+    @Test
+    public void setEnabled_connectionPolicyForbidden_setConnectionPolicyAllowed_returnTrue() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice))
+                .thenReturn(CONNECTION_POLICY_FORBIDDEN);
+        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_ALLOWED))
+                .thenReturn(true);
+
+        assertThat(mProfile.setEnabled(mBluetoothDevice, true)).isTrue();
+    }
+
+    @Test
+    public void setEnabled_connectionPolicyAllowed_setConnectionPolicyForbidden_returnTrue() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice)).thenReturn(CONNECTION_POLICY_ALLOWED);
+        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_FORBIDDEN))
+                .thenReturn(true);
+
+        assertThat(mProfile.setEnabled(mBluetoothDevice, false)).isTrue();
+    }
+
+    @Test
+    public void setEnabled_connectionPolicyForbidden_setConnectionPolicyForbidden_returnTrue() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        when(mService.getConnectionPolicy(mBluetoothDevice))
+                .thenReturn(CONNECTION_POLICY_FORBIDDEN);
+        when(mService.setConnectionPolicy(mBluetoothDevice, CONNECTION_POLICY_FORBIDDEN))
+                .thenReturn(true);
+
+        assertThat(mProfile.setEnabled(mBluetoothDevice, false)).isTrue();
+    }
+
+    @Test
+    public void getConnectedDevices_returnCorrectList() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        int[] connectedStates = new int[] {
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTING};
+        List<BluetoothDevice> connectedList = Arrays.asList(
+                mBluetoothDevice,
+                mBluetoothDevice,
+                mBluetoothDevice);
+        when(mService.getDevicesMatchingConnectionStates(connectedStates))
+                .thenReturn(connectedList);
+
+        assertThat(mProfile.getConnectedDevices().size()).isEqualTo(connectedList.size());
+    }
+
+    @Test
+    public void getConnectableDevices_returnCorrectList() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+        int[] connectableStates = new int[] {
+                BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.STATE_CONNECTING,
+                BluetoothProfile.STATE_DISCONNECTING};
+        List<BluetoothDevice> connectableList = Arrays.asList(
+                mBluetoothDevice,
+                mBluetoothDevice,
+                mBluetoothDevice,
+                mBluetoothDevice);
+        when(mService.getDevicesMatchingConnectionStates(connectableStates))
+                .thenReturn(connectableList);
+
+        assertThat(mProfile.getConnectableDevices().size()).isEqualTo(connectableList.size());
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java
index 89de81f..a2b208a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SharedPreferenceLoggerTest.java
@@ -39,7 +39,6 @@
 
     private static final String TEST_TAG = "tag";
     private static final String TEST_KEY = "key";
-    private static final String TEST_TAGGED_KEY = TEST_TAG + "/" + TEST_KEY;
 
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private Context mContext;
@@ -65,10 +64,8 @@
         editor.putInt(TEST_KEY, 2);
         editor.putInt(TEST_KEY, 2);
 
-        verify(mMetricsFeature, times(6)).action(eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE),
-                eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(TEST_TAGGED_KEY),
+        verify(mMetricsFeature, times(6)).changed(eq(SettingsEnums.PAGE_UNKNOWN),
+                eq(TEST_KEY),
                 anyInt());
     }
 
@@ -82,15 +79,11 @@
         editor.putBoolean(TEST_KEY, false);
 
 
-        verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN,
-                SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
-                SettingsEnums.PAGE_UNKNOWN,
-                TEST_TAGGED_KEY,
+        verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN,
+                TEST_KEY,
                 1);
-        verify(mMetricsFeature, times(3)).action(SettingsEnums.PAGE_UNKNOWN,
-                SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
-                SettingsEnums.PAGE_UNKNOWN,
-                TEST_TAGGED_KEY,
+        verify(mMetricsFeature, times(3)).changed(SettingsEnums.PAGE_UNKNOWN,
+                TEST_KEY,
                 0);
     }
 
@@ -103,10 +96,8 @@
         editor.putLong(TEST_KEY, 1);
         editor.putLong(TEST_KEY, 2);
 
-        verify(mMetricsFeature, times(4)).action(eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE),
-                eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(TEST_TAGGED_KEY),
+        verify(mMetricsFeature, times(4)).changed(eq(SettingsEnums.PAGE_UNKNOWN),
+                eq(TEST_KEY),
                 anyInt());
     }
 
@@ -117,10 +108,8 @@
         editor.putLong(TEST_KEY, 1);
         editor.putLong(TEST_KEY, veryBigNumber);
 
-        verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN,
-                SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
-                SettingsEnums.PAGE_UNKNOWN,
-                TEST_TAGGED_KEY,
+        verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN,
+                TEST_KEY,
                 Integer.MAX_VALUE);
     }
 
@@ -131,10 +120,8 @@
         editor.putLong(TEST_KEY, 1);
         editor.putLong(TEST_KEY, veryNegativeNumber);
 
-        verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN,
-                SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
-                SettingsEnums.PAGE_UNKNOWN,
-                TEST_TAGGED_KEY, Integer.MIN_VALUE);
+        verify(mMetricsFeature).changed(SettingsEnums.PAGE_UNKNOWN,
+                TEST_KEY, Integer.MIN_VALUE);
     }
 
     @Test
@@ -146,10 +133,8 @@
         editor.putFloat(TEST_KEY, 1);
         editor.putFloat(TEST_KEY, 2);
 
-        verify(mMetricsFeature, times(4)).action(eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE),
-                eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(TEST_TAGGED_KEY),
+        verify(mMetricsFeature, times(4)).changed(eq(SettingsEnums.PAGE_UNKNOWN),
+                eq(TEST_KEY),
                 anyInt());
     }
 
@@ -159,7 +144,7 @@
         verify(mMetricsFeature).action(SettingsEnums.PAGE_UNKNOWN,
                 ACTION_SETTINGS_PREFERENCE_CHANGE,
                 SettingsEnums.PAGE_UNKNOWN,
-                "tag/key:com.android.settings",
+                "key:com.android.settings",
                 0);
     }
 
@@ -170,10 +155,8 @@
         mSharedPrefLogger.logValue(TEST_KEY, "62");
         mSharedPrefLogger.logValue(TEST_KEY, "0");
 
-        verify(mMetricsFeature, times(3)).action(eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE),
-                eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(TEST_TAGGED_KEY),
+        verify(mMetricsFeature, times(3)).changed(eq(SettingsEnums.PAGE_UNKNOWN),
+                eq(TEST_KEY),
                 anyInt());
     }
 
@@ -185,10 +168,8 @@
         mSharedPrefLogger.logValue(TEST_KEY, "4.2");
         mSharedPrefLogger.logValue(TEST_KEY, "3.0");
 
-        verify(mMetricsFeature, times(0)).action(eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE),
-                eq(SettingsEnums.PAGE_UNKNOWN),
-                eq(TEST_TAGGED_KEY),
+        verify(mMetricsFeature, times(0)).changed(eq(SettingsEnums.PAGE_UNKNOWN),
+                eq(TEST_KEY),
                 anyInt());
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
index 99e13c3..1d5f1b2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
@@ -32,6 +32,7 @@
             "default_disabled_by_policy_title_financed_device";
     static final String DEFAULT_BIOMETRIC_TITLE = "biometric_title";
     static final String DEFAULT_BIOMETRIC_CONTENTS = "biometric_contents";
+    static final String DISABLED_BY_PARENT_CONTENT = "disabled_by_parent_constent";
     static final DeviceAdminStringProvider DEFAULT_DEVICE_ADMIN_STRING_PROVIDER =
             new FakeDeviceAdminStringProvider(/* url = */ null);
 
@@ -97,6 +98,11 @@
     }
 
     @Override
+    public String getDisabledByParentContent() {
+        return DISABLED_BY_PARENT_CONTENT;
+    }
+
+    @Override
     public String getDisabledBiometricsParentConsentContent() {
         return DEFAULT_BIOMETRIC_CONTENTS;
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java
new file mode 100644
index 0000000..5d249c7
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+package com.android.settingslib.enterprise;
+
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ADMIN_COMPONENT;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID;
+import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DEVICE_ADMIN_STRING_PROVIDER;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.TestCase.assertEquals;
+
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowResolveInfo;
+
+@RunWith(RobolectricTestRunner.class)
+public class SupervisedDeviceActionDisabledByAdminControllerTest {
+
+    private Context mContext;
+
+    private ActionDisabledByAdminControllerTestUtils mTestUtils;
+    private SupervisedDeviceActionDisabledByAdminController mController;
+
+    @Before
+    public void setUp() {
+        mContext = Robolectric.buildActivity(Activity.class).setup().get();
+
+        mTestUtils = new ActionDisabledByAdminControllerTestUtils();
+
+        mController = new SupervisedDeviceActionDisabledByAdminController(
+                DEFAULT_DEVICE_ADMIN_STRING_PROVIDER, UserManager.DISALLOW_ADD_USER);
+        mController.initialize(mTestUtils.createLearnMoreButtonLauncher());
+        mController.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID);
+    }
+
+    @Test
+    public void buttonClicked() {
+        Uri restrictionUri = Uri.parse("policy:/user_restrictions/no_add_user");
+        Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
+                .setData(restrictionUri)
+                .setPackage(ADMIN_COMPONENT.getPackageName());
+        ResolveInfo resolveInfo = ShadowResolveInfo.newResolveInfo("Admin Activity",
+                ADMIN_COMPONENT.getPackageName(), "InfoActivity");
+        shadowOf(mContext.getPackageManager()).addResolveInfoForIntent(intent, resolveInfo);
+
+        DialogInterface.OnClickListener listener =
+                mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN);
+        assertNotNull("Supervision controller must supply a non-null listener", listener);
+        listener.onClick(mock(DialogInterface.class), 0 /* which */);
+
+        Intent nextIntent = shadowOf(RuntimeEnvironment.application).getNextStartedActivity();
+        assertEquals(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING,
+                nextIntent.getAction());
+        assertEquals(restrictionUri, nextIntent.getData());
+        assertEquals(ADMIN_COMPONENT.getPackageName(), nextIntent.getPackage());
+    }
+
+    @Test
+    public void noButton() {
+        // No supervisor restricted setting Activity
+        DialogInterface.OnClickListener listener =
+                mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN);
+        assertNull("Supervision controller generates null listener", listener);
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 5399f8a..c058a61 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -461,7 +461,7 @@
     public void connect_shouldSelectRoute() {
         mInfoMediaDevice1.connect();
 
-        verify(mMediaRouter2Manager).selectRoute(TEST_PACKAGE_NAME, mRouteInfo1);
+        verify(mMediaRouter2Manager).transfer(TEST_PACKAGE_NAME, mRouteInfo1);
     }
 
     @Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java
new file mode 100644
index 0000000..740261d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/mobile/MobileIconCarrierIdOverridesTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.mobile;
+
+import static com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl.parseNetworkIconOverrideTypedArray;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.res.TypedArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class MobileIconCarrierIdOverridesTest {
+    private static final String OVERRIDE_ICON_1_NAME = "name_1";
+    private static final int OVERRIDE_ICON_1_RES = 1;
+
+    private static final String OVERRIDE_ICON_2_NAME = "name_2";
+    private static final int OVERRIDE_ICON_2_RES = 2;
+
+    NetworkOverrideTypedArrayMock mResourceMock;
+
+    @Before
+    public void setUp() {
+        mResourceMock = new NetworkOverrideTypedArrayMock(
+                new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME },
+                new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES }
+        );
+    }
+
+    @Test
+    public void testParse_singleOverride() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME },
+                new int[] { OVERRIDE_ICON_1_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES);
+    }
+
+    @Test
+    public void testParse_multipleOverrides() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME, OVERRIDE_ICON_2_NAME },
+                new int[] { OVERRIDE_ICON_1_RES, OVERRIDE_ICON_2_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isEqualTo(OVERRIDE_ICON_2_RES);
+        assertThat(parsed.get(OVERRIDE_ICON_1_NAME)).isEqualTo(OVERRIDE_ICON_1_RES);
+    }
+
+    @Test
+    public void testParse_nonexistentKey_isNull() {
+        mResourceMock.setOverrides(
+                new String[] { OVERRIDE_ICON_1_NAME },
+                new int[] { OVERRIDE_ICON_1_RES }
+        );
+
+        Map<String, Integer> parsed = parseNetworkIconOverrideTypedArray(mResourceMock.getMock());
+
+        assertThat(parsed.get(OVERRIDE_ICON_2_NAME)).isNull();
+    }
+
+    static class NetworkOverrideTypedArrayMock {
+        private Object[] mInterleaved;
+
+        private final TypedArray mMockTypedArray = mock(TypedArray.class);
+
+        NetworkOverrideTypedArrayMock(
+                String[] networkTypes,
+                int[] iconOverrides) {
+
+            mInterleaved = interleaveTypes(networkTypes, iconOverrides);
+
+            doAnswer(invocation -> {
+                return mInterleaved[(int) invocation.getArgument(0)];
+            }).when(mMockTypedArray).getString(/* index */ anyInt());
+
+            doAnswer(invocation -> {
+                return mInterleaved[(int) invocation.getArgument(0)];
+            }).when(mMockTypedArray).getResourceId(/* index */ anyInt(), /* default */ anyInt());
+
+            when(mMockTypedArray.length()).thenAnswer(invocation -> {
+                return mInterleaved.length;
+            });
+        }
+
+        TypedArray getMock() {
+            return mMockTypedArray;
+        }
+
+        void setOverrides(String[] types, int[] resIds) {
+            mInterleaved = interleaveTypes(types, resIds);
+        }
+
+        private Object[] interleaveTypes(String[] strs, int[] ints) {
+            assertThat(strs.length).isEqualTo(ints.length);
+
+            Object[] ret = new Object[strs.length * 2];
+
+            // Keep track of where we are in the interleaved array, but iterate the overrides
+            int interleavedIndex = 0;
+            for (int i = 0; i < strs.length; i++) {
+                ret[interleavedIndex] = strs[i];
+                interleavedIndex += 1;
+                ret[interleavedIndex] = ints[i];
+                interleavedIndex += 1;
+            }
+            return ret;
+        }
+    }
+}
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 3623c78..edea3ab 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -311,4 +311,7 @@
 
     <!-- Whether tilt to bright is enabled by default. -->
     <bool name="def_wearable_tiltToBrightEnabled">false</bool>
+
+    <!-- Whether vibrate icon is shown in the status bar by default. -->
+    <integer name="def_statusBarVibrateIconEnabled">0</integer>
 </resources>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 298bbbd..2828681 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -84,6 +84,7 @@
         Settings.Global.USER_PREFERRED_RESOLUTION_HEIGHT,
         Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH,
         Settings.Global.POWER_BUTTON_LONG_PRESS,
+        Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED,
         Settings.Global.Wearable.SMART_REPLIES_ENABLED,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index def7ddc..98af15a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -122,6 +122,7 @@
         Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME,
+        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
         Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
         Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
         Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 9ef6d8f..e30dd2f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -329,6 +329,8 @@
         VALIDATORS.put(Global.USER_PREFERRED_REFRESH_RATE, NON_NEGATIVE_FLOAT_VALIDATOR);
         VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_HEIGHT, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_WIDTH, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED,
+                       new DiscreteValueValidator(new String[]{"0", "1"}));
         VALIDATORS.put(Global.Wearable.WET_MODE_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.COOLDOWN_MODE_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index cde4bc4..80af69c 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -177,6 +177,7 @@
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
                 NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 808ea9e..6d375ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -549,7 +549,7 @@
 
         try {
             IActivityManager am = ActivityManager.getService();
-            Configuration config = am.getConfiguration();
+            final Configuration config = new Configuration();
             config.setLocales(merged);
             // indicate this isn't some passing default - the user wants this remembered
             config.userSetLocale = true;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 3a25d85..0b7b2f9 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -112,10 +112,10 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.providers.settings.SettingsState.Setting;
 
-import libcore.util.HexEncoding;
-
 import com.google.android.collect.Sets;
 
+import libcore.util.HexEncoding;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
@@ -1144,7 +1144,7 @@
             Slog.v(LOG_TAG, "getConfigSetting(" + name + ")");
         }
 
-        DeviceConfig.enforceReadPermission(getContext(), /*namespace=*/name.split("/")[0]);
+        DeviceConfig.enforceReadPermission(/*namespace=*/name.split("/")[0]);
 
         // Get the value.
         synchronized (mLock) {
@@ -1317,7 +1317,7 @@
             Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
         }
 
-        DeviceConfig.enforceReadPermission(getContext(),
+        DeviceConfig.enforceReadPermission(
                 prefix != null ? prefix.split("/")[0] : null);
 
         synchronized (mLock) {
@@ -3659,7 +3659,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 210;
+            private static final int SETTINGS_VERSION = 211;
 
             private final int mUserId;
 
@@ -5531,7 +5531,21 @@
                     // removed now that feature is enabled for everyone
                     currentVersion = 210;
                 }
-
+                if (currentVersion == 210) {
+                    final SettingsState secureSettings = getSecureSettingsLocked(userId);
+                    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.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 765ee89..c388826 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -42,8 +42,6 @@
 import android.util.Base64;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -51,6 +49,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
@@ -1087,6 +1087,7 @@
             parseStateLocked(parser);
             return true;
         } catch (XmlPullParserException | IOException e) {
+            Slog.e(LOG_TAG, "parse settings xml failed", e);
             return false;
         } finally {
             IoUtils.closeQuietly(in);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 9747a6c..f5c9bcd 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -817,7 +817,8 @@
                  Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                  Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT,
                  Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
-                 Settings.Secure.UI_TRANSLATION_ENABLED);
+                 Settings.Secure.UI_TRANSLATION_ENABLED,
+                 Settings.Secure.CREDENTIAL_SERVICE);
 
     @Test
     public void systemSettingsBackedUpOrDenied() {
@@ -883,7 +884,7 @@
                         Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING,
                         Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
                         Settings.Secure.SPATIAL_AUDIO_ENABLED,
-                        Settings.Secure.TIMEOUT_TO_USER_ZERO,
+                        Settings.Secure.TIMEOUT_TO_DOCK_USER,
                         Settings.Secure.UI_NIGHT_MODE_LAST_COMPUTED,
                         Settings.Secure.UI_NIGHT_MODE_OVERRIDE_OFF,
                         Settings.Secure.UI_NIGHT_MODE_OVERRIDE_ON);
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 4ed28d5..55160fb 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -17,9 +17,10 @@
 
 import android.os.Looper;
 import android.test.AndroidTestCase;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
+
 import com.google.common.base.Strings;
 
 import java.io.ByteArrayOutputStream;
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index fecf124..01c0809 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -154,6 +154,7 @@
     <uses-permission android:name="android.permission.CONTROL_UI_TRACING" />
     <uses-permission android:name="android.permission.SIGNAL_PERSISTENT_PROCESSES" />
     <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+    <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
     <!-- Internal permissions granted to the shell. -->
     <uses-permission android:name="android.permission.FORCE_BACK" />
     <uses-permission android:name="android.permission.BATTERY_STATS" />
@@ -210,6 +211,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.CREATE_USERS" />
+    <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" />
     <uses-permission android:name="android.permission.QUERY_USERS" />
     <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
@@ -715,6 +717,63 @@
     <!-- Permission required for CTS test - ActivityPermissionRationaleTest -->
     <uses-permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY" />
 
+    <!-- Permission required for CTS test - CtsDeviceLockTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
+
+    <!-- Permission required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+
+    <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.CAPTURE_MEDIA_OUTPUT" />
+
+    <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.CAPTURE_TUNER_AUDIO_INPUT" />
+
+    <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT" />
+
+    <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+
+    <!-- Permissions required for CTS test - CtsAppFgsTestCases -->
+    <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
+
+    <!-- Permission required for CTS test - ApplicationExemptionsTests -->
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/Shell/res/values-en-rCA/strings.xml b/packages/Shell/res/values-en-rCA/strings.xml
index 5462813..65ab725 100644
--- a/packages/Shell/res/values-en-rCA/strings.xml
+++ b/packages/Shell/res/values-en-rCA/strings.xml
@@ -28,7 +28,7 @@
     <string name="bugreport_finished_pending_screenshot_text" product="tv" msgid="2343263822812016950">"Select to share your bug report without a screenshot or wait for the screenshot to finish"</string>
     <string name="bugreport_finished_pending_screenshot_text" product="watch" msgid="1474435374470177193">"Tap to share your bug report without a screenshot or wait for the screenshot to finish"</string>
     <string name="bugreport_finished_pending_screenshot_text" product="default" msgid="1474435374470177193">"Tap to share your bug report without a screenshot or wait for the screenshot to finish"</string>
-    <string name="bugreport_confirm" msgid="5917407234515812495">"Bug reports contain data from the system\'s various log files, which may include data that you consider sensitive (such as app-usage and location data). Only share bug reports with people and apps that you trust."</string>
+    <string name="bugreport_confirm" msgid="5917407234515812495">"Bug reports contain data from the system\'s various log files, which may include data you consider sensitive (such as app-usage and location data). Only share bug reports with people and apps you trust."</string>
     <string name="bugreport_confirm_dont_repeat" msgid="6179945398364357318">"Don\'t show again"</string>
     <string name="bugreport_storage_title" msgid="5332488144740527109">"Bug reports"</string>
     <string name="bugreport_unreadable_text" msgid="586517851044535486">"Bug report file could not be read"</string>
diff --git a/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java
index e34f5c8..d866653 100644
--- a/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java
+++ b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java
@@ -104,7 +104,7 @@
 
             final IntentFilter filter = new IntentFilter();
             filter.addAction(CUSTOM_ACTION_SEND_MULTIPLE_INTENT);
-            context.registerReceiver(receiver, filter);
+            context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED_UNAUDITED);
         }
 
         /**
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 753e110..71ad886 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -114,6 +114,7 @@
         "androidx.dynamicanimation_dynamicanimation",
         "androidx-constraintlayout_constraintlayout",
         "androidx.exifinterface_exifinterface",
+        "androidx.test.ext.junit",
         "com.google.android.material_material",
         "kotlinx_coroutines_android",
         "kotlinx_coroutines",
@@ -125,6 +126,7 @@
         "jsr330",
         "lottie",
         "LowLightDreamLib",
+        "motion_tool_lib",
     ],
     manifest: "AndroidManifest.xml",
 
@@ -223,6 +225,7 @@
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "mockito-target-extended-minus-junit4",
+        "androidx.test.ext.junit",
         "testables",
         "truth-prebuilt",
         "monet",
@@ -230,6 +233,7 @@
         "jsr330",
         "WindowManager-Shell",
         "LowLightDreamLib",
+        "motion_tool_lib",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b5145f9..11237dc 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -195,6 +195,9 @@
     <permission android:name="com.android.systemui.permission.FLAGS"
                 android:protectionLevel="signature" />
 
+    <permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+        android:protectionLevel="signature|privileged" />
+
     <!-- Adding Quick Settings tiles -->
     <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
 
@@ -289,6 +292,12 @@
     <!-- Query all packages on device on R+ -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.NOTES" />
+        </intent>
+    </queries>
+
     <!-- Permission to register process observer -->
     <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>
 
@@ -551,6 +560,16 @@
                   android:showForAllUsers="true">
         </activity>
 
+        <!-- started from SensoryPrivacyService -->
+        <activity android:name=".sensorprivacy.television.TvSensorPrivacyChangedActivity"
+            android:exported="true"
+            android:launchMode="singleTop"
+            android:permission="android.permission.MANAGE_SENSOR_PRIVACY"
+            android:theme="@style/BottomSheet"
+            android:finishOnCloseSystemDialogs="true"
+            android:showForAllUsers="true">
+        </activity>
+
 
         <!-- started from UsbDeviceSettingsManager -->
         <activity android:name=".usb.UsbAccessoryUriActivity"
@@ -955,10 +974,11 @@
 
         <receiver android:name=".media.dialog.MediaOutputDialogReceiver"
                   android:exported="true">
-            <intent-filter>
+            <intent-filter android:priority="1">
                 <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
                 <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG" />
                 <action android:name="com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG" />
+                <action android:name="android.intent.action.SHOW_OUTPUT_SWITCHER" />
             </intent-filter>
         </receiver>
 
@@ -970,5 +990,18 @@
                 <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
             </intent-filter>
         </receiver>
+
+        <activity android:name=".logcat.LogAccessDialogActivity"
+                  android:theme="@android:style/Theme.Translucent.NoTitleBar"
+                  android:excludeFromRecents="true"
+                  android:exported="false">
+        </activity>
+
+        <provider
+            android:authorities="com.android.systemui.keyguard.quickaffordance"
+            android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+            android:exported="true"
+            android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+            />
     </application>
 </manifest>
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 6d61fd8..77ddc6e 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -83,7 +83,6 @@
 stwu@google.com
 syeonlee@google.com
 sunnygoyal@google.com
-susikp@google.com
 thiruram@google.com
 tracyzhou@google.com
 tsuji@google.com
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 23cee4d..fdfad2b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -25,7 +25,6 @@
 import android.os.Looper
 import android.util.Log
 import android.util.MathUtils
-import android.view.GhostView
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -86,6 +85,9 @@
          */
         val sourceIdentity: Any
 
+        /** The CUJ associated to this controller. */
+        val cuj: DialogCuj?
+
         /**
          * Move the drawing of the source in the overlay of [viewGroup].
          *
@@ -142,7 +144,31 @@
          * controlled by this controller.
          */
         // TODO(b/252723237): Make this non-nullable
-        fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder?
+        fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder?
+
+        companion object {
+            /**
+             * Create a [Controller] that can animate [source] to and from a dialog.
+             *
+             * Important: The view must be attached to a [ViewGroup] when calling this function and
+             * during the animation. For safety, this method will return null when it is not.
+             *
+             * Note: The background of [view] should be a (rounded) rectangle so that it can be
+             * properly animated.
+             */
+            fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
+                if (source.parent !is ViewGroup) {
+                    Log.e(
+                        TAG,
+                        "Skipping animation as view $source is not attached to a ViewGroup",
+                        Exception(),
+                    )
+                    return null
+                }
+
+                return ViewDialogLaunchAnimatorController(source, cuj)
+            }
+        }
     }
 
     /**
@@ -172,7 +198,12 @@
         cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
-        show(dialog, createController(view), cuj, animateBackgroundBoundsChange)
+        val controller = Controller.fromView(view, cuj)
+        if (controller == null) {
+            dialog.show()
+        } else {
+            show(dialog, controller, animateBackgroundBoundsChange)
+        }
     }
 
     /**
@@ -187,10 +218,10 @@
      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
      */
+    @JvmOverloads
     fun show(
         dialog: Dialog,
         controller: Controller,
-        cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
@@ -207,7 +238,10 @@
                 it.dialog.window.decorView.viewRootImpl == controller.viewRoot
             }
         val animateFrom =
-            animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller
+            animatedParent?.dialogContentWithBackground?.let {
+                Controller.fromView(it, controller.cuj)
+            }
+                ?: controller
 
         if (animatedParent == null && animateFrom !is LaunchableView) {
             // Make sure the View we launch from implements LaunchableView to avoid visibility
@@ -244,96 +278,12 @@
                 animateBackgroundBoundsChange,
                 animatedParent,
                 isForTesting,
-                cuj,
             )
 
         openedDialogs.add(animatedDialog)
         animatedDialog.start()
     }
 
-    /** Create a [Controller] that can animate [source] to & from a dialog. */
-    private fun createController(source: View): Controller {
-        return object : Controller {
-            override val viewRoot: ViewRootImpl
-                get() = source.viewRootImpl
-
-            override val sourceIdentity: Any = source
-
-            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
-                // Create a temporary ghost of the source (which will make it invisible) and add it
-                // to the host dialog.
-                GhostView.addGhost(source, viewGroup)
-
-                // The ghost of the source was just created, so the source is currently invisible.
-                // We need to make sure that it stays invisible as long as the dialog is shown or
-                // animating.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
-            }
-
-            override fun stopDrawingInOverlay() {
-                // Note: here we should remove the ghost from the overlay, but in practice this is
-                // already done by the launch controllers created below.
-
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-                source.visibility = View.VISIBLE
-            }
-
-            override fun createLaunchController(): LaunchAnimator.Controller {
-                val delegate = GhostedViewLaunchAnimatorController(source)
-                return object : LaunchAnimator.Controller by delegate {
-                    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
-                        // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
-                        // ghost (that ghosts only the source content, and not its background) will
-                        // be added right after this by the delegate and will be animated.
-                        GhostView.removeGhost(source)
-                        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
-                    }
-
-                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
-                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
-
-                        // We hide the source when the dialog is showing. We will make this view
-                        // visible again when dismissing the dialog. This does nothing if the source
-                        // implements [LaunchableView], as it's already INVISIBLE in that case.
-                        source.visibility = View.INVISIBLE
-                    }
-                }
-            }
-
-            override fun createExitController(): LaunchAnimator.Controller {
-                return GhostedViewLaunchAnimatorController(source)
-            }
-
-            override fun shouldAnimateExit(): Boolean {
-                // The source should be invisible by now, if it's not then something else changed
-                // its visibility and we probably don't want to run the animation.
-                if (source.visibility != View.INVISIBLE) {
-                    return false
-                }
-
-                return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
-            }
-
-            override fun onExitAnimationCancelled() {
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
-                // If the view is invisible it's probably because of us, so we make it visible
-                // again.
-                if (source.visibility == View.INVISIBLE) {
-                    source.visibility = View.VISIBLE
-                }
-            }
-
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
-                return InteractionJankMonitor.Configuration.Builder.withView(cuj, source)
-            }
-        }
-    }
-
     /**
      * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
      * allow for dismissing the whole stack.
@@ -563,9 +513,6 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
-
-    /** Interaction to which the dialog animation is associated. */
-    private val cuj: DialogCuj? = null
 ) {
     /**
      * The DecorView of this dialog window.
@@ -618,8 +565,9 @@
     private var hasInstrumentedJank = false
 
     fun start() {
+        val cuj = controller.cuj
         if (cuj != null) {
-            val config = controller.jankConfigurationBuilder(cuj.cujType)
+            val config = controller.jankConfigurationBuilder()
             if (config != null) {
                 if (cuj.tag != null) {
                     config.setTag(cuj.tag)
@@ -865,7 +813,7 @@
             return
         }
 
-        ViewRootSync.synchronizeNextDraw(decorView, controller.viewRoot.view, then)
+        ViewRootSync.synchronizeNextDraw(controller.viewRoot.view, decorView, then)
         decorView.invalidate()
         controller.viewRoot.view.invalidate()
     }
@@ -917,7 +865,7 @@
                 }
 
                 if (hasInstrumentedJank) {
-                    interactionJankMonitor.end(cuj!!.cujType)
+                    interactionJankMonitor.end(controller.cuj!!.cujType)
                 }
             }
         )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
index 8ce372d..40a5e97 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
@@ -30,7 +30,12 @@
      */
     fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
 
-    // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
+    /**
+     * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into
+     * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is
+     * currently not attached or visible).
+     */
+    fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller?
 
     companion object {
         /**
@@ -39,6 +44,7 @@
          * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
          * animated.
          */
+        @JvmStatic
         fun fromView(view: View): Expandable {
             return object : Expandable {
                 override fun activityLaunchController(
@@ -46,6 +52,12 @@
                 ): ActivityLaunchAnimator.Controller? {
                     return ActivityLaunchAnimator.Controller.fromView(view, cujType)
                 }
+
+                override fun dialogLaunchController(
+                    cuj: DialogCuj?
+                ): DialogLaunchAnimator.Controller? {
+                    return DialogLaunchAnimator.Controller.fromView(view, cuj)
+                }
             }
         }
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 6780fb7..65d6c83 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -82,6 +82,14 @@
         /** Mutable Y coordinate of the glyph position relative from the baseline. */
         var y: Float = 0f
 
+        /**
+         * The current line of text being drawn, in a multi-line TextView.
+         */
+        var lineNo: Int = 0
+
+        /**
+         * Mutable text size of the glyph in pixels.
+         */
         /** Mutable text size of the glyph in pixels. */
         var textSize: Float = 0f
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index db14fdf..f9fb42c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -231,7 +231,9 @@
                     val origin = layout.getDrawOrigin(lineNo)
                     canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat())
 
-                    run.fontRuns.forEach { fontRun -> drawFontRun(canvas, run, fontRun, tmpPaint) }
+                    run.fontRuns.forEach { fontRun ->
+                        drawFontRun(canvas, run, fontRun, lineNo, tmpPaint)
+                    }
                 } finally {
                     canvas.restore()
                 }
@@ -341,7 +343,7 @@
     var glyphFilter: GlyphCallback? = null
 
     // Draws single font run.
-    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) {
+    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) {
         var arrayIndex = 0
         val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress)
 
@@ -360,11 +362,13 @@
         tmpGlyph.font = font
         tmpGlyph.runStart = run.start
         tmpGlyph.runLength = run.end - run.start
+        tmpGlyph.lineNo = lineNo
 
         tmpPaintForGlyph.set(paint)
         var prevStart = run.start
 
         for (i in run.start until run.end) {
+            tmpGlyph.glyphIndex = i
             tmpGlyph.glyphId = line.glyphIds[i]
             tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
             tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
new file mode 100644
index 0000000..ecee598
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.animation
+
+import android.view.GhostView
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewRootImpl
+import com.android.internal.jank.InteractionJankMonitor
+
+/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
+class ViewDialogLaunchAnimatorController
+internal constructor(
+    private val source: View,
+    override val cuj: DialogCuj?,
+) : DialogLaunchAnimator.Controller {
+    override val viewRoot: ViewRootImpl
+        get() = source.viewRootImpl
+
+    override val sourceIdentity: Any = source
+
+    override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+        // Create a temporary ghost of the source (which will make it invisible) and add it
+        // to the host dialog.
+        GhostView.addGhost(source, viewGroup)
+
+        // The ghost of the source was just created, so the source is currently invisible.
+        // We need to make sure that it stays invisible as long as the dialog is shown or
+        // animating.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+    }
+
+    override fun stopDrawingInOverlay() {
+        // Note: here we should remove the ghost from the overlay, but in practice this is
+        // already done by the launch controllers created below.
+
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+        source.visibility = View.VISIBLE
+    }
+
+    override fun createLaunchController(): LaunchAnimator.Controller {
+        val delegate = GhostedViewLaunchAnimatorController(source)
+        return object : LaunchAnimator.Controller by delegate {
+            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
+                // ghost (that ghosts only the source content, and not its background) will
+                // be added right after this by the delegate and will be animated.
+                GhostView.removeGhost(source)
+                delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+            }
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                // We hide the source when the dialog is showing. We will make this view
+                // visible again when dismissing the dialog. This does nothing if the source
+                // implements [LaunchableView], as it's already INVISIBLE in that case.
+                source.visibility = View.INVISIBLE
+            }
+        }
+    }
+
+    override fun createExitController(): LaunchAnimator.Controller {
+        return GhostedViewLaunchAnimatorController(source)
+    }
+
+    override fun shouldAnimateExit(): Boolean {
+        // The source should be invisible by now, if it's not then something else changed
+        // its visibility and we probably don't want to run the animation.
+        if (source.visibility != View.INVISIBLE) {
+            return false
+        }
+
+        return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
+    }
+
+    override fun onExitAnimationCancelled() {
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+
+        // If the view is invisible it's probably because of us, so we make it visible
+        // again.
+        if (source.visibility == View.INVISIBLE) {
+            source.visibility = View.VISIBLE
+        }
+    }
+
+    override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
+        val type = cuj?.cujType ?: return null
+        return InteractionJankMonitor.Configuration.Builder.withView(type, source)
+    }
+}
diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp
index 9671add..40580d2 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -47,6 +47,10 @@
         "tests/**/*.kt",
         "tests/**/*.java",
     ],
+    data: [
+        ":framework",
+        ":androidx.annotation_annotation",
+    ],
     static_libs: [
         "SystemUILintChecker",
         "junit",
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
index 1d808ba..74e6d85 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BindServiceOnMainThreadDetector.kt
@@ -59,11 +59,11 @@
                     !hasWorkerThreadAnnotation(context, node.getParentOfType(UClass::class.java))
             ) {
                 context.report(
-                    ISSUE,
-                    method,
-                    context.getLocation(node),
-                    "This method should be annotated with `@WorkerThread` because " +
-                        "it calls ${method.name}",
+                    issue = ISSUE,
+                    location = context.getLocation(node),
+                    message =
+                        "This method should be annotated with `@WorkerThread` because " +
+                            "it calls ${method.name}",
                 )
             }
         }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
index 1129929..344d0a3 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
@@ -52,10 +52,9 @@
         val evaluator = context.evaluator
         if (evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
             context.report(
-                    ISSUE,
-                    method,
-                    context.getNameLocation(node),
-                    "`Context.${method.name}()` should be replaced with " +
+                    issue = ISSUE,
+                    location = context.getNameLocation(node),
+                    message = "`Context.${method.name}()` should be replaced with " +
                     "`BroadcastSender.${method.name}()`"
             )
         }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
index bab76ab..14099eb 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedMainThreadDetector.kt
@@ -38,10 +38,9 @@
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
         if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
             context.report(
-                ISSUE,
-                method,
-                context.getNameLocation(node),
-                "Replace with injected `@Main Executor`."
+                issue = ISSUE,
+                location = context.getNameLocation(node),
+                message = "Replace with injected `@Main Executor`."
             )
         }
     }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
index b622900..aa4b2f7 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/NonInjectedServiceDetector.kt
@@ -44,11 +44,11 @@
                 method.containingClass?.qualifiedName == CLASS_CONTEXT
         ) {
             context.report(
-                ISSUE,
-                method,
-                context.getNameLocation(node),
-                "Use `@Inject` to get system-level service handles instead of " +
-                    "`Context.getSystemService()`"
+                issue = ISSUE,
+                location = context.getNameLocation(node),
+                message =
+                    "Use `@Inject` to get system-level service handles instead of " +
+                        "`Context.getSystemService()`"
             )
         } else if (
             evaluator.isStatic(method) &&
@@ -56,10 +56,10 @@
                 method.containingClass?.qualifiedName == "android.accounts.AccountManager"
         ) {
             context.report(
-                ISSUE,
-                method,
-                context.getNameLocation(node),
-                "Replace `AccountManager.get()` with an injected instance of `AccountManager`"
+                issue = ISSUE,
+                location = context.getNameLocation(node),
+                message =
+                    "Replace `AccountManager.get()` with an injected instance of `AccountManager`"
             )
         }
     }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
index 4ba3afc..5840e8f 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
@@ -38,10 +38,10 @@
     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
         if (context.evaluator.isMemberInSubClassOf(method, CLASS_CONTEXT)) {
             context.report(
-                    ISSUE,
-                    method,
-                    context.getNameLocation(node),
-                    "Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
+                    issue = ISSUE,
+                    location = context.getNameLocation(node),
+                    message = "Register `BroadcastReceiver` using `BroadcastDispatcher` instead " +
+                    "of `Context`"
             )
         }
     }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
index 7be21a5..b15a41b 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SlowUserQueryDetector.kt
@@ -46,10 +46,10 @@
                 method.containingClass?.qualifiedName == "android.app.ActivityManager"
         ) {
             context.report(
-                ISSUE_SLOW_USER_ID_QUERY,
-                method,
-                context.getNameLocation(node),
-                "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
+                issue = ISSUE_SLOW_USER_ID_QUERY,
+                location = context.getNameLocation(node),
+                message =
+                    "Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
             )
         }
         if (
@@ -58,10 +58,9 @@
                 method.containingClass?.qualifiedName == "android.os.UserManager"
         ) {
             context.report(
-                ISSUE_SLOW_USER_INFO_QUERY,
-                method,
-                context.getNameLocation(node),
-                "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
+                issue = ISSUE_SLOW_USER_INFO_QUERY,
+                location = context.getNameLocation(node),
+                message = "Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
             )
         }
     }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
index 4eeeb85..bf02589 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
@@ -32,7 +32,8 @@
 class SoftwareBitmapDetector : Detector(), SourceCodeScanner {
 
     override fun getApplicableReferenceNames(): List<String> {
-        return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
+        return mutableListOf(
+            "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
     }
 
     override fun visitReference(
@@ -40,14 +41,12 @@
             reference: UReferenceExpression,
             referenced: PsiElement
     ) {
-
         val evaluator = context.evaluator
         if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) {
             context.report(
-                    ISSUE,
-                    referenced,
-                    context.getNameLocation(referenced),
-                    "Replace software bitmap with `Config.HARDWARE`"
+                    issue = ISSUE,
+                    location = context.getNameLocation(reference),
+                    message = "Replace software bitmap with `Config.HARDWARE`"
             )
         }
     }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
new file mode 100644
index 0000000..22f15bd
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+private const val CLASS_SETTINGS = "android.provider.Settings"
+
+/**
+ * Detects usage of static methods in android.provider.Settings and suggests to use an injected
+ * settings provider instance instead.
+ */
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf(
+            "getFloat",
+            "getInt",
+            "getLong",
+            "getString",
+            "getUriFor",
+            "putFloat",
+            "putInt",
+            "putLong",
+            "putString"
+        )
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        val className = method.containingClass?.qualifiedName
+        if (
+            className != "$CLASS_SETTINGS.Global" &&
+                className != "$CLASS_SETTINGS.Secure" &&
+                className != "$CLASS_SETTINGS.System"
+        ) {
+            return
+        }
+        if (!evaluator.isStatic(method)) {
+            return
+        }
+
+        val subclassName = className.substring(CLASS_SETTINGS.length + 1)
+
+        context.report(
+            issue = ISSUE,
+            location = context.getNameLocation(node),
+            message = "`@Inject` a ${subclassName}Settings instead"
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "StaticSettingsProvider",
+                briefDescription = "Static settings provider usage",
+                explanation =
+                    """
+                    Static settings provider methods, such as `Settings.Global.putInt()`, should \
+                    not be used because they make testing difficult. Instead, use an injected \
+                    settings provider. For example, instead of calling `Settings.Secure.getInt()`, \
+                    annotate the class constructor with `@Inject` and add `SecureSettings` to the \
+                    parameters.
+                    """,
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        StaticSettingsProviderDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                    )
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index cf7c1b5..3f334c1c 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -36,6 +36,7 @@
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
+                StaticSettingsProviderDetector.ISSUE
         )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 486af9d..141dd05 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -18,6 +18,8 @@
 
 import com.android.annotations.NonNull
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile
+import java.io.File
 import org.intellij.lang.annotations.Language
 
 @Suppress("UnstableApiUsage")
@@ -30,132 +32,8 @@
  */
 internal val androidStubs =
     arrayOf(
-        indentedJava(
-            """
-package android.app;
-
-public class ActivityManager {
-    public static int getCurrentUser() {}
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.accounts;
-
-public class AccountManager {
-    public static AccountManager get(Context context) { return null; }
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.os;
-import android.content.pm.UserInfo;
-import android.annotation.UserIdInt;
-
-public class UserManager {
-    public UserInfo getUserInfo(@UserIdInt int userId) {}
-}
-"""
-        ),
-        indentedJava("""
-package android.annotation;
-
-public @interface UserIdInt {}
-"""),
-        indentedJava("""
-package android.content.pm;
-
-public class UserInfo {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Looper {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Handler {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class ServiceConnection {}
-"""),
-        indentedJava("""
-package android.os;
-
-public enum UserHandle {
-    ALL
-}
-"""),
-        indentedJava(
-            """
-package android.content;
-import android.os.UserHandle;
-import android.os.Handler;
-import android.os.Looper;
-import java.util.concurrent.Executor;
-
-public class Context {
-    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {}
-    public void registerReceiverAsUser(
-            BroadcastReceiver receiver, UserHandle user, IntentFilter filter,
-            String broadcastPermission, Handler scheduler) {}
-    public void registerReceiverForAllUsers(
-            BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission,
-            Handler scheduler) {}
-    public void sendBroadcast(Intent intent) {}
-    public void sendBroadcast(Intent intent, String receiverPermission) {}
-    public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {}
-    public void bindService(Intent intent) {}
-    public void bindServiceAsUser(
-            Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {}
-    public void unbindService(ServiceConnection connection) {}
-    public Looper getMainLooper() { return null; }
-    public Executor getMainExecutor() { return null; }
-    public Handler getMainThreadHandler() { return null; }
-    public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; }
-    public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.app;
-import android.content.Context;
-
-public class Activity extends Context {}
-"""
-        ),
-        indentedJava(
-            """
-package android.graphics;
-
-public class Bitmap {
-    public enum Config {
-        ARGB_8888,
-        RGB_565,
-        HARDWARE
-    }
-    public static Bitmap createBitmap(int width, int height, Config config) {
-        return null;
-    }
-}
-"""
-        ),
-        indentedJava("""
-package android.content;
-
-public class BroadcastReceiver {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class IntentFilter {}
-"""),
+        LibraryReferenceTestFile(File("framework.jar").canonicalFile),
+        LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile),
         indentedJava(
             """
 package com.android.systemui.settings;
@@ -167,23 +45,4 @@
 }
 """
         ),
-        indentedJava(
-            """
-package androidx.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-@Retention(SOURCE)
-@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
-public @interface WorkerThread {
-}
-"""
-        ),
     )
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
index 6ae8fd3..426211e 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
 
@@ -129,6 +126,32 @@
     }
 
     @Test
+    fun testSuppressUnbindService() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.content.ServiceConnection;
+
+                    @SuppressLint("BindServiceOnMainThread")
+                    public class TestClass {
+                        public void unbind(Context context, ServiceConnection connection) {
+                          context.unbindService(connection);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BindServiceOnMainThreadDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testWorkerMethod() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 7d42280..30b68f7 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
+class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BroadcastSentViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
 
@@ -132,6 +129,34 @@
     }
 
     @Test
+    fun testSuppressSendBroadcastInActivity() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.app.Activity;
+                    import android.os.UserHandle;
+
+                    public class TestClass {
+                        @SuppressWarnings("BroadcastSentViaContext")
+                        public void send(Activity activity) {
+                          Intent intent = new Intent(Intent.ACTION_VIEW);
+                          activity.sendBroadcastAsUser(intent, UserHandle.ALL, "permission");
+                        }
+
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(BroadcastSentViaContextDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testSendBroadcastInBroadcastSender() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c468af8..ed3d14a 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
 
@@ -64,6 +61,32 @@
     }
 
     @Test
+    fun testSuppressGetMainThreadHandler() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.Context;
+                    import android.os.Handler;
+
+                    @SuppressWarnings("NonInjectedMainThread")
+                    public class TestClass {
+                        public void test(Context context) {
+                          Handler mainThreadHandler = context.getMainThreadHandler();
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(NonInjectedMainThreadDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testGetMainLooper() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index c83a35b..846129a 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedServiceDetectorTest : LintDetectorTest() {
+class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedServiceDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
     override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
 
     @Test
@@ -94,6 +91,32 @@
     }
 
     @Test
+    fun testSuppressGetServiceWithClass() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+                        import android.content.Context;
+                        import android.os.UserManager;
+
+                        public class TestClass {
+                            @SuppressLint("NonInjectedService")
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                context.getSystemService(UserManager.class);
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(NonInjectedServiceDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testGetAccountManager() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index ebcddeb..0ac8f8e 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
+class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
@@ -66,6 +63,34 @@
     }
 
     @Test
+    fun testSuppressRegisterReceiver() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    package test.pkg;
+                    import android.content.BroadcastReceiver;
+                    import android.content.Context;
+                    import android.content.IntentFilter;
+
+                    @SuppressWarnings("RegisterReceiverViaContext")
+                    public class TestClass {
+                        public void bind(Context context, BroadcastReceiver receiver,
+                            IntentFilter filter) {
+                          context.registerReceiver(receiver, filter, 0);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(RegisterReceiverViaContextDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testRegisterReceiverAsUser() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index b03a11c..34a4249 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SlowUserQueryDetectorTest : LintDetectorTest() {
+class SlowUserQueryDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SlowUserQueryDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> =
         listOf(
@@ -79,7 +76,7 @@
                         import android.os.UserManager;
 
                         public class TestClass {
-                            public void slewlyGetUserInfo(UserManager userManager) {
+                            public void slowlyGetUserInfo(UserManager userManager) {
                                 userManager.getUserInfo();
                             }
                         }
@@ -104,6 +101,34 @@
     }
 
     @Test
+    fun testSuppressGetUserInfo() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+                        import android.os.UserManager;
+
+                        public class TestClass {
+                            @SuppressWarnings("SlowUserInfoQuery")
+                            public void slowlyGetUserInfo(UserManager userManager) {
+                                userManager.getUserInfo();
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(
+                SlowUserQueryDetector.ISSUE_SLOW_USER_ID_QUERY,
+                SlowUserQueryDetector.ISSUE_SLOW_USER_INFO_QUERY
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testUserTrackerGetUserId() {
         lint()
             .files(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fb6537e..34becc6 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SoftwareBitmapDetectorTest : LintDetectorTest() {
+class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SoftwareBitmapDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
 
@@ -54,23 +51,48 @@
             .run()
             .expect(
                 """
-                src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        ARGB_8888,
-                        ~~~~~~~~~
-                src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        RGB_565,
-                        ~~~~~~~
+                src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
+                                                                  ~~~~~~~
+                src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+                                                                  ~~~~~~~~~
                 0 errors, 2 warnings
                 """
             )
     }
 
     @Test
+    fun testSuppressSoftwareBitmap() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                    import android.graphics.Bitmap;
+
+                    @SuppressWarnings("SoftwareBitmap")
+                    public class TestClass {
+                        public void test() {
+                          Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
+                          Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+                        }
+                    }
+                """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(SoftwareBitmapDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun testHardwareBitmap() {
         lint()
             .files(
                 TestFiles.java(
-                        """
+                    """
                     import android.graphics.Bitmap;
 
                     public class TestClass {
@@ -79,8 +101,7 @@
                         }
                     }
                 """
-                    )
-                    .indented(),
+                ),
                 *stubs
             )
             .issues(SoftwareBitmapDetector.ISSUE)
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
new file mode 100644
index 0000000..efe4c90
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() {
+
+    override fun getDetector(): Detector = StaticSettingsProviderDetector()
+    override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE)
+
+    @Test
+    fun testSuppressGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+
+                        import android.provider.Settings;
+                        import android.provider.Settings.Global;
+                        import android.provider.Settings.Secure;
+
+                        public class TestClass {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                final ContentResolver cr = mContext.getContentResolver();
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                                Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(StaticSettingsProviderDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                        ~~~~~~~~~
+                0 errors, 36 warnings
+                """
+            )
+    }
+
+    @Test
+    fun testGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+
+                        import android.provider.Settings;
+                        import android.provider.Settings.Global;
+                        import android.provider.Settings.Secure;
+
+                        public class TestClass {
+                            @SuppressWarnings("StaticSettingsProvider")
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                final ContentResolver cr = mContext.getContentResolver();
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(StaticSettingsProviderDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
new file mode 100644
index 0000000..3f93f07
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -0,0 +1,48 @@
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.ClassRule
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+abstract class SystemUILintDetectorTest : LintDetectorTest() {
+
+    companion object {
+        @ClassRule
+        @JvmField
+        val libraryChecker: LibraryExists =
+            LibraryExists("framework.jar", "androidx.annotation_annotation.jar")
+    }
+
+    class LibraryExists(vararg val libraryNames: String) : TestRule {
+        override fun apply(base: Statement, description: Description): Statement {
+            return object : Statement() {
+                override fun evaluate() {
+                    for (libName in libraryNames) {
+                        val libFile = File(libName)
+                        if (!libFile.canonicalFile.exists()) {
+                            throw Exception(
+                                "Could not find $libName in the test's working directory. " +
+                                    "File ${libFile.absolutePath} does not exist."
+                            )
+                        }
+                    }
+                    base.evaluate()
+                }
+            }
+        }
+    }
+    /**
+     * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+     * in Android Studio has the same result as running the tests in atest
+     */
+    override fun lint(): TestLintTask =
+        super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
index 065c314..50c3d7e 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
@@ -40,17 +40,16 @@
 import androidx.compose.ui.unit.LayoutDirection
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.roundToInt
 
-/** A controller that can control animated launches. */
+/** A controller that can control animated launches from an [Expandable]. */
 interface ExpandableController {
-    /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */
-    fun forActivity(): ActivityLaunchAnimator.Controller
-
-    /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */
-    fun forDialog(): DialogLaunchAnimator.Controller
+    /** The [Expandable] controlled by this controller. */
+    val expandable: Expandable
 }
 
 /**
@@ -120,13 +119,26 @@
     private val layoutDirection: LayoutDirection,
     private val isComposed: State<Boolean>,
 ) : ExpandableController {
-    override fun forActivity(): ActivityLaunchAnimator.Controller {
-        return activityController()
-    }
+    override val expandable: Expandable =
+        object : Expandable {
+            override fun activityLaunchController(
+                cujType: Int?,
+            ): ActivityLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
 
-    override fun forDialog(): DialogLaunchAnimator.Controller {
-        return dialogController()
-    }
+                return activityController(cujType)
+            }
+
+            override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
+
+                return dialogController(cuj)
+            }
+        }
 
     /**
      * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
@@ -233,7 +245,7 @@
     }
 
     /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
-    private fun activityController(): ActivityLaunchAnimator.Controller {
+    private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
         val delegate = launchController()
         return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
             override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -248,10 +260,11 @@
         }
     }
 
-    private fun dialogController(): DialogLaunchAnimator.Controller {
+    private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller {
         return object : DialogLaunchAnimator.Controller {
             override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
             override val sourceIdentity: Any = this@ExpandableControllerImpl
+            override val cuj: DialogCuj? = cuj
 
             override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
                 val newOverlay = viewGroup.overlay as ViewGroupOverlay
@@ -294,9 +307,7 @@
                 isDialogShowing.value = false
             }
 
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
+            override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
                 // TODO(b/252723237): Add support for jank monitoring when animating from a
                 // Composable.
                 return null
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index eada40e..278a89f 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -34,6 +34,11 @@
             android:enabled="false"
             tools:replace="android:authorities"
             tools:node="remove" />
+        <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+            android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
         <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
             android:authorities="com.android.systemui.test.keyguard.clock.disabled"
             android:enabled="false"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
deleted file mode 100644
index 4d94bab..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
+++ /dev/null
@@ -1,392 +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.user.ui.compose
-
-import android.graphics.drawable.Drawable
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.painter.ColorPainter
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.core.graphics.drawable.toBitmap
-import com.android.systemui.common.ui.compose.load
-import com.android.systemui.compose.SysUiOutlinedButton
-import com.android.systemui.compose.SysUiTextButton
-import com.android.systemui.compose.features.R
-import com.android.systemui.compose.theme.LocalAndroidColorScheme
-import com.android.systemui.user.ui.viewmodel.UserActionViewModel
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
-import java.lang.Integer.min
-import kotlin.math.ceil
-
-@Composable
-fun UserSwitcherScreen(
-    viewModel: UserSwitcherViewModel,
-    onFinished: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false)
-    val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList())
-    val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1)
-    val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList())
-    val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false)
-    val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false)
-
-    UserSwitcherScreenStateless(
-        isFinishRequested = isFinishRequested,
-        users = users,
-        maxUserColumns = maxUserColumns,
-        menuActions = menuActions,
-        isOpenMenuButtonVisible = isOpenMenuButtonVisible,
-        isMenuVisible = isMenuVisible,
-        onMenuClosed = viewModel::onMenuClosed,
-        onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked,
-        onCancelButtonClicked = viewModel::onCancelButtonClicked,
-        onFinished = {
-            onFinished()
-            viewModel.onFinished()
-        },
-        modifier = modifier,
-    )
-}
-
-@Composable
-private fun UserSwitcherScreenStateless(
-    isFinishRequested: Boolean,
-    users: List<UserViewModel>,
-    maxUserColumns: Int,
-    menuActions: List<UserActionViewModel>,
-    isOpenMenuButtonVisible: Boolean,
-    isMenuVisible: Boolean,
-    onMenuClosed: () -> Unit,
-    onOpenMenuButtonClicked: () -> Unit,
-    onCancelButtonClicked: () -> Unit,
-    onFinished: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    LaunchedEffect(isFinishRequested) {
-        if (isFinishRequested) {
-            onFinished()
-        }
-    }
-
-    Box(
-        modifier =
-            modifier
-                .fillMaxSize()
-                .padding(
-                    horizontal = 60.dp,
-                    vertical = 40.dp,
-                ),
-    ) {
-        UserGrid(
-            users = users,
-            maxUserColumns = maxUserColumns,
-            modifier = Modifier.align(Alignment.Center),
-        )
-
-        Buttons(
-            menuActions = menuActions,
-            isOpenMenuButtonVisible = isOpenMenuButtonVisible,
-            isMenuVisible = isMenuVisible,
-            onMenuClosed = onMenuClosed,
-            onOpenMenuButtonClicked = onOpenMenuButtonClicked,
-            onCancelButtonClicked = onCancelButtonClicked,
-            modifier = Modifier.align(Alignment.BottomEnd),
-        )
-    }
-}
-
-@Composable
-private fun UserGrid(
-    users: List<UserViewModel>,
-    maxUserColumns: Int,
-    modifier: Modifier = Modifier,
-) {
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        verticalArrangement = Arrangement.spacedBy(44.dp),
-        modifier = modifier,
-    ) {
-        val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt()
-        (0 until rowCount).forEach { rowIndex ->
-            Row(
-                horizontalArrangement = Arrangement.spacedBy(64.dp),
-                modifier = modifier,
-            ) {
-                val fromIndex = rowIndex * maxUserColumns
-                val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns)
-                users.subList(fromIndex, toIndex).forEach { user ->
-                    UserItem(
-                        viewModel = user,
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-private fun UserItem(
-    viewModel: UserViewModel,
-) {
-    val onClicked = viewModel.onClicked
-    Column(
-        horizontalAlignment = Alignment.CenterHorizontally,
-        modifier =
-            if (onClicked != null) {
-                    Modifier.clickable { onClicked() }
-                } else {
-                    Modifier
-                }
-                .alpha(viewModel.alpha),
-    ) {
-        Box {
-            UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp))
-
-            UserItemIcon(
-                image = viewModel.image,
-                isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible,
-                modifier = Modifier.align(Alignment.Center).size(222.dp)
-            )
-        }
-
-        // User name
-        val text = viewModel.name.load()
-        if (text != null) {
-            // We use the box to center-align the text vertically as that is not possible with Text
-            // alone.
-            Box(
-                modifier = Modifier.size(width = 222.dp, height = 48.dp),
-            ) {
-                Text(
-                    text = text,
-                    style = MaterialTheme.typography.titleLarge,
-                    color = colorResource(com.android.internal.R.color.system_neutral1_50),
-                    maxLines = 1,
-                    overflow = TextOverflow.Ellipsis,
-                    modifier = Modifier.align(Alignment.Center),
-                )
-            }
-        }
-    }
-}
-
-@Composable
-private fun UserItemBackground(
-    modifier: Modifier = Modifier,
-) {
-    Image(
-        painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground),
-        contentDescription = null,
-        modifier = modifier.clip(CircleShape),
-    )
-}
-
-@Composable
-private fun UserItemIcon(
-    image: Drawable,
-    isSelectionMarkerVisible: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    Image(
-        bitmap = image.toBitmap().asImageBitmap(),
-        contentDescription = null,
-        modifier =
-            if (isSelectionMarkerVisible) {
-                    // Draws a ring
-                    modifier.border(
-                        width = 8.dp,
-                        color = LocalAndroidColorScheme.current.colorAccentPrimary,
-                        shape = CircleShape,
-                    )
-                } else {
-                    modifier
-                }
-                .padding(16.dp)
-                .clip(CircleShape)
-    )
-}
-
-@Composable
-private fun Buttons(
-    menuActions: List<UserActionViewModel>,
-    isOpenMenuButtonVisible: Boolean,
-    isMenuVisible: Boolean,
-    onMenuClosed: () -> Unit,
-    onOpenMenuButtonClicked: () -> Unit,
-    onCancelButtonClicked: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    Row(
-        modifier = modifier,
-    ) {
-        // Cancel button.
-        SysUiTextButton(
-            onClick = onCancelButtonClicked,
-        ) {
-            Text(stringResource(R.string.cancel))
-        }
-
-        // "Open menu" button.
-        if (isOpenMenuButtonVisible) {
-            Spacer(modifier = Modifier.width(8.dp))
-            // To properly use a DropdownMenu in Compose, we need to wrap the button that opens it
-            // and the menu itself in a Box.
-            Box {
-                SysUiOutlinedButton(
-                    onClick = onOpenMenuButtonClicked,
-                ) {
-                    Text(stringResource(R.string.add))
-                }
-                Menu(
-                    viewModel = menuActions,
-                    isMenuVisible = isMenuVisible,
-                    onMenuClosed = onMenuClosed,
-                )
-            }
-        }
-    }
-}
-
-@Composable
-private fun Menu(
-    viewModel: List<UserActionViewModel>,
-    isMenuVisible: Boolean,
-    onMenuClosed: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4
-    DropdownMenu(
-        expanded = isMenuVisible,
-        onDismissRequest = onMenuClosed,
-        modifier =
-            modifier.background(
-                color = MaterialTheme.colorScheme.inverseOnSurface,
-            ),
-    ) {
-        viewModel.forEachIndexed { index, action ->
-            MenuItem(
-                viewModel = action,
-                onClicked = { action.onClicked() },
-                topPadding =
-                    if (index == 0) {
-                        16.dp
-                    } else {
-                        0.dp
-                    },
-                bottomPadding =
-                    if (index == viewModel.size - 1) {
-                        16.dp
-                    } else {
-                        0.dp
-                    },
-                modifier = Modifier.sizeIn(maxWidth = maxItemWidth),
-            )
-        }
-    }
-}
-
-@Composable
-private fun MenuItem(
-    viewModel: UserActionViewModel,
-    onClicked: () -> Unit,
-    topPadding: Dp,
-    bottomPadding: Dp,
-    modifier: Modifier = Modifier,
-) {
-    val context = LocalContext.current
-    val density = LocalDensity.current
-
-    val icon =
-        remember(viewModel.iconResourceId) {
-            val drawable =
-                checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
-            val size = with(density) { 20.dp.toPx() }.toInt()
-            drawable
-                .toBitmap(
-                    width = size,
-                    height = size,
-                )
-                .asImageBitmap()
-        }
-
-    DropdownMenuItem(
-        text = {
-            Text(
-                text = stringResource(viewModel.textResourceId),
-                style = MaterialTheme.typography.bodyMedium,
-            )
-        },
-        onClick = onClicked,
-        leadingIcon = {
-            Spacer(modifier = Modifier.width(10.dp))
-            Image(
-                bitmap = icon,
-                contentDescription = null,
-            )
-        },
-        modifier =
-            modifier
-                .heightIn(
-                    min = 56.dp,
-                )
-                .padding(
-                    start = 18.dp,
-                    end = 65.dp,
-                    top = topPadding,
-                    bottom = bottomPadding,
-                ),
-    )
-}
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/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index d0d3052..7fc9a83 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -16,7 +16,6 @@
 -packages/SystemUI/checks/tests/com/android/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
 -packages/SystemUI/checks/tests/com/android/systemui/lint/SoftwareBitmapDetectorTest.kt
 -packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
--packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
 -packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt
 -packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
 -packages/SystemUI/shared/src/com/android/systemui/flags/FlagListenable.kt
@@ -27,8 +26,6 @@
 -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
 -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
 -packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
--packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt
--packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
 -packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonPositionCalculator.kt
 -packages/SystemUI/shared/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerManager.kt
 -packages/SystemUI/shared/src/com/android/systemui/shared/system/smartspace/SmartspaceState.kt
@@ -189,45 +186,8 @@
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
--packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
--packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
--packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
--packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
--packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
--packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
--packages/SystemUI/src/com/android/systemui/media/MediaData.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
--packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
 -packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
 -packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt
--packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
--packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
--packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
--packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
--packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
--packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
--packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
--packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
--packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
--packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
--packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
 -packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
 -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
 -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -653,26 +613,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
 -packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
@@ -740,8 +680,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
--packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt
--packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/shared/rotation/RotationButtonControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -832,7 +770,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index b3dd955..dee0f5c 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -205,6 +205,13 @@
             n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)),
             n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666))
     )),
+    MONOCHROMATIC(CoreSpec(
+            a1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a2 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a3 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n2 = TonalSpec(HueSource(), ChromaConstant(.0))
+    )),
 }
 
 class ColorScheme(
@@ -219,7 +226,7 @@
     val neutral1: List<Int>
     val neutral2: List<Int>
 
-    constructor(@ColorInt seed: Int, darkTheme: Boolean):
+    constructor(@ColorInt seed: Int, darkTheme: Boolean) :
             this(seed, darkTheme, Style.TONAL_SPOT)
 
     @JvmOverloads
@@ -227,7 +234,7 @@
         wallpaperColors: WallpaperColors,
         darkTheme: Boolean,
         style: Style = Style.TONAL_SPOT
-    ):
+    ) :
             this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
 
     val allAccentColors: List<Int>
@@ -472,4 +479,4 @@
             return huePopulation
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index cafaaf8..7709f21 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -33,6 +33,7 @@
 
     static_libs: [
         "androidx.annotation_annotation",
+        "error_prone_annotations",
         "PluginCoreLib",
         "SystemUIAnimationLib",
     ],
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 1e74c3d..66e44b9 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -14,9 +14,11 @@
 package com.android.systemui.plugins
 
 import android.content.res.Resources
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.view.View
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -68,7 +70,10 @@
     }
 
     /** Optional method for dumping debug information */
-    fun dump(pw: PrintWriter) { }
+    fun dump(pw: PrintWriter) {}
+
+    /** Optional method for debug logging */
+    fun setLogBuffer(logBuffer: LogBuffer) {}
 }
 
 /** Interface for a specific clock face version rendered by the clock */
@@ -83,47 +88,70 @@
 /** Events that should call when various rendering parameters change */
 interface ClockEvents {
     /** Call every time tick */
-    fun onTimeTick() { }
+    fun onTimeTick() {}
 
     /** Call whenever timezone changes */
-    fun onTimeZoneChanged(timeZone: TimeZone) { }
+    fun onTimeZoneChanged(timeZone: TimeZone) {}
 
     /** Call whenever the text time format changes (12hr vs 24hr) */
-    fun onTimeFormatChanged(is24Hr: Boolean) { }
+    fun onTimeFormatChanged(is24Hr: Boolean) {}
 
     /** Call whenever the locale changes */
-    fun onLocaleChanged(locale: Locale) { }
-
-    /** Call whenever font settings change */
-    fun onFontSettingChanged() { }
+    fun onLocaleChanged(locale: Locale) {}
 
     /** Call whenever the color palette should update */
-    fun onColorPaletteChanged(resources: Resources) { }
+    fun onColorPaletteChanged(resources: Resources) {}
 }
 
 /** Methods which trigger various clock animations */
 interface ClockAnimations {
     /** Runs an enter animation (if any) */
-    fun enter() { }
+    fun enter() {}
 
     /** Sets how far into AOD the device currently is. */
-    fun doze(fraction: Float) { }
+    fun doze(fraction: Float) {}
 
     /** Sets how far into the folding animation the device is. */
-    fun fold(fraction: Float) { }
+    fun fold(fraction: Float) {}
 
     /** Runs the battery animation (if any). */
-    fun charge() { }
+    fun charge() {}
+
+    /** Move the clock, for example, if the notification tray appears in split-shade mode. */
+    fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {}
+
+    /**
+     * Whether this clock has a custom position update animation. If true, the keyguard will call
+     * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
+     * animation will be used (e.g. a simple translation).
+     */
+    val hasCustomPositionUpdatedAnimation
+        get() = false
 }
 
 /** Events that have specific data about the related face */
 interface ClockFaceEvents {
     /** Region Darkness specific to the clock face */
-    fun onRegionDarknessChanged(isDark: Boolean) { }
+    fun onRegionDarknessChanged(isDark: Boolean) {}
+
+    /**
+     * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
+     * design is allowed to ignore this target size on a case-by-case basis.
+     */
+    fun onFontSettingChanged(fontSizePx: Float) {}
+
+    /**
+     * Target region information for the clock face. For small clock, this will match the bounds of
+     * the parent view mostly, but have a target height based on the height of the default clock.
+     * For large clocks, the parent view is the entire device size, but most clocks will want to
+     * render within the centered targetRect to avoid obstructing other elements. The specified
+     * targetRegion is relative to the parent view.
+     */
+    fun onTargetRegionChanged(targetRegion: Rect?) {}
 }
 
 /** Some data about a clock design */
 data class ClockMetadata(
     val clockId: ClockId,
-    val name: String
+    val name: String,
 )
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
index c50340c..e52a57f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
@@ -82,6 +82,18 @@
     boolean isFalseTap(@Penalty int penalty);
 
     /**
+     * Returns true if the FalsingManager thinks the last gesture was not a valid long tap.
+     *
+     * Use this method to validate a long tap for launching an action, like long press on a UMO
+     *
+     * The only parameter, penalty, indicates how much this should affect future gesture
+     * classifications if this long tap looks like a false.
+     * As long taps are hard to confirm as false or otherwise,
+     * a low penalty value is encouraged unless context indicates otherwise.
+     */
+    boolean isFalseLongTap(@Penalty int penalty);
+
+    /**
      * Returns true if the last two gestures do not look like a double tap.
      *
      * Only works on data that has already been reported to the FalsingManager. Be sure that
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
new file mode 100644
index 0000000..6436dcb
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.plugins.util.RingBuffer
+import com.google.errorprone.annotations.CompileTimeConstant
+import java.io.PrintWriter
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.BlockingQueue
+import kotlin.concurrent.thread
+import kotlin.math.max
+
+/**
+ * A simple ring buffer of recyclable log messages
+ *
+ * The goal of this class is to enable logging that is both extremely chatty and extremely
+ * lightweight. If done properly, logging a message will not result in any heap allocations or
+ * string generation. Messages are only converted to strings if the log is actually dumped (usually
+ * as the result of taking a bug report).
+ *
+ * You can dump the entire buffer at any time by running:
+ *
+ * ```
+ * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService <bufferName>
+ * ```
+ *
+ * ...where `bufferName` is the (case-sensitive) [name] passed to the constructor.
+ *
+ * By default, only messages of WARN level or higher are echoed to logcat, but this can be adjusted
+ * locally (usually for debugging purposes).
+ *
+ * To enable logcat echoing for an entire buffer:
+ *
+ * ```
+ * $ adb shell settings put global systemui/buffer/<bufferName> <level>
+ * ```
+ *
+ * To enable logcat echoing for a specific tag:
+ *
+ * ```
+ * $ adb shell settings put global systemui/tag/<tag> <level>
+ * ```
+ *
+ * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
+ * the first letter of any of the previous.
+ *
+ * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI
+ * LogBufferFactory.
+ *
+ * @param name The name of this buffer, printed when the buffer is dumped and in some other
+ * situations.
+ * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the
+ * maximum, it behaves like a ring buffer.
+ */
+class LogBuffer
+@JvmOverloads
+constructor(
+    private val name: String,
+    private val maxSize: Int,
+    private val logcatEchoTracker: LogcatEchoTracker,
+    private val systrace: Boolean = true,
+) {
+    private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
+
+    private val echoMessageQueue: BlockingQueue<LogMessage>? =
+        if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
+
+    init {
+        if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
+            thread(start = true, name = "LogBuffer-$name", priority = Thread.NORM_PRIORITY) {
+                try {
+                    while (true) {
+                        echoToDesiredEndpoints(echoMessageQueue.take())
+                    }
+                } catch (e: InterruptedException) {
+                    Thread.currentThread().interrupt()
+                }
+            }
+        }
+    }
+
+    var frozen = false
+        private set
+
+    private val mutable
+        get() = !frozen && maxSize > 0
+
+    /**
+     * Logs a message to the log buffer
+     *
+     * May also log the message to logcat if echoing is enabled for this buffer or tag.
+     *
+     * The actual string of the log message is not constructed until it is needed. To accomplish
+     * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is
+     * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data
+     * on the message's fields. The message is then inserted into the buffer where it waits until it
+     * is either pushed out by newer messages or it needs to printed. If and when this latter moment
+     * occurs, the [messagePrinter] function is called on the message. It reads whatever data the
+     * initializer stored and converts it to a human-readable log message.
+     *
+     * @param tag A string of at most 23 characters, used for grouping logs into categories or
+     * subjects. If this message is echoed to logcat, this will be the tag that is used.
+     * @param level Which level to log the message at, both to the buffer and to logcat if it's
+     * echoed. In general, a module should split most of its logs into either INFO or DEBUG level.
+     * INFO level should be reserved for information that other parts of the system might care
+     * about, leaving the specifics of code's day-to-day operations to DEBUG.
+     * @param messageInitializer A function that will be called immediately to store relevant data
+     * on the log message. The value of `this` will be the LogMessage to be initialized.
+     * @param messagePrinter A function that will be called if and when the message needs to be
+     * dumped to logcat or a bug report. It should read the data stored by the initializer and
+     * convert it to a human-readable string. The value of `this` will be the LogMessage to be
+     * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and
+     * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a
+     * new instance of the printer for each call, thwarting our attempts at avoiding any sort of
+     * allocation.
+     * @param exception Provide any exception that need to be logged. This is saved as
+     * [LogMessage.exception]
+     */
+    @JvmOverloads
+    inline fun log(
+        tag: String,
+        level: LogLevel,
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ) {
+        val message = obtain(tag, level, messagePrinter, exception)
+        messageInitializer(message)
+        commit(message)
+    }
+
+    /**
+     * Logs a compile-time string constant [message] to the log buffer. Use sparingly.
+     *
+     * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for
+     * simpler use-cases where [message] is a compile time string constant. For use-cases where the
+     * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
+     * an initializer and a message printer.
+     *
+     * Log buffers are limited by the number of entries, so logging more frequently will limit the
+     * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a
+     * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
+     * every log may do more to improve overall logging than adding more logs with this method.
+     */
+    fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
+        log(tag, level, { str1 = message }, { str1!! })
+
+    /**
+     * You should call [log] instead of this method.
+     *
+     * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
+     * grows the buffer by one.
+     *
+     * After calling [obtain], the message will now be at the end of the buffer. The caller must
+     * store any relevant data on the message and then call [commit].
+     */
+    @Synchronized
+    fun obtain(
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
+    ): LogMessage {
+        if (!mutable) {
+            return FROZEN_MESSAGE
+        }
+        val message = buffer.advance()
+        message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception)
+        return message
+    }
+
+    /**
+     * You should call [log] instead of this method.
+     *
+     * After acquiring a message via [obtain], call this method to signal to the buffer that you
+     * have finished filling in its data fields. The message will be echoed to logcat if necessary.
+     */
+    @Synchronized
+    fun commit(message: LogMessage) {
+        if (!mutable) {
+            return
+        }
+        // Log in the background thread only if echoMessageQueue exists and has capacity (checking
+        // capacity avoids the possibility of blocking this thread)
+        if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
+            try {
+                echoMessageQueue.put(message)
+            } catch (e: InterruptedException) {
+                // the background thread has been shut down, so just log on this one
+                echoToDesiredEndpoints(message)
+            }
+        } else {
+            echoToDesiredEndpoints(message)
+        }
+    }
+
+    /** Sends message to echo after determining whether to use Logcat and/or systrace. */
+    private fun echoToDesiredEndpoints(message: LogMessage) {
+        val includeInLogcat =
+            logcatEchoTracker.isBufferLoggable(name, message.level) ||
+                logcatEchoTracker.isTagLoggable(message.tag, message.level)
+        echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
+    }
+
+    /** Converts the entire buffer to a newline-delimited string */
+    @Synchronized
+    fun dump(pw: PrintWriter, tailLength: Int) {
+        val iterationStart =
+            if (tailLength <= 0) {
+                0
+            } else {
+                max(0, buffer.size - tailLength)
+            }
+
+        for (i in iterationStart until buffer.size) {
+            buffer[i].dump(pw)
+        }
+    }
+
+    /**
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls
+     * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if
+     * necessary.
+     */
+    @Synchronized
+    fun freeze() {
+        if (!frozen) {
+            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 frozen" })
+            frozen = true
+        }
+    }
+
+    /** Undoes the effects of calling [freeze]. */
+    @Synchronized
+    fun unfreeze() {
+        if (frozen) {
+            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 unfrozen" })
+            frozen = false
+        }
+    }
+
+    private fun echo(message: LogMessage, toLogcat: Boolean, toSystrace: Boolean) {
+        if (toLogcat || toSystrace) {
+            val strMessage = message.messagePrinter(message)
+            if (toSystrace) {
+                echoToSystrace(message, strMessage)
+            }
+            if (toLogcat) {
+                echoToLogcat(message, strMessage)
+            }
+        }
+    }
+
+    private fun echoToSystrace(message: LogMessage, strMessage: String) {
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            "UI Events",
+            "$name - ${message.level.shortString} ${message.tag}: $strMessage"
+        )
+    }
+
+    private fun echoToLogcat(message: LogMessage, strMessage: String) {
+        when (message.level) {
+            LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception)
+            LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception)
+            LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception)
+            LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception)
+            LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception)
+            LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception)
+        }
+    }
+}
+
+/**
+ * A function that will be called immediately to store relevant data on the log message. The value
+ * of `this` will be the LogMessage to be initialized.
+ */
+typealias MessageInitializer = LogMessage.() -> Unit
+
+private const val TAG = "LogBuffer"
+private val FROZEN_MESSAGE = LogMessageImpl.create()
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
new file mode 100644
index 0000000..b036cf0
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+import android.util.Log
+
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
+    VERBOSE(Log.VERBOSE, "V"),
+    DEBUG(Log.DEBUG, "D"),
+    INFO(Log.INFO, "I"),
+    WARNING(Log.WARN, "W"),
+    ERROR(Log.ERROR, "E"),
+    WTF(Log.ASSERT, "WTF")
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
new file mode 100644
index 0000000..9468681
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+import java.io.PrintWriter
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+/**
+ * Generic data class for storing messages logged to a [LogBuffer]
+ *
+ * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic
+ * data slots that may or may not be used, depending on the nature of the specific message being
+ * logged.
+ *
+ * When a message is logged, the code doing the logging stores data in one or more of the generic
+ * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
+ *
+ * See [LogBuffer.log] for more information.
+ */
+interface LogMessage {
+    val level: LogLevel
+    val tag: String
+    val timestamp: Long
+    val messagePrinter: MessagePrinter
+    val exception: Throwable?
+
+    var str1: String?
+    var str2: String?
+    var str3: String?
+    var int1: Int
+    var int2: Int
+    var long1: Long
+    var long2: Long
+    var double1: Double
+    var bool1: Boolean
+    var bool2: Boolean
+    var bool3: Boolean
+    var bool4: Boolean
+
+    /** Function that dumps the [LogMessage] to the provided [writer]. */
+    fun dump(writer: PrintWriter) {
+        val formattedTimestamp = DATE_FORMAT.format(timestamp)
+        val shortLevel = level.shortString
+        val messageToPrint = messagePrinter(this)
+        printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint)
+        exception?.printStackTrace(writer)
+    }
+}
+
+/**
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
+ */
+typealias MessagePrinter = LogMessage.() -> String
+
+private fun printLikeLogcat(
+    pw: PrintWriter,
+    formattedTimestamp: String,
+    shortLogLevel: String,
+    tag: String,
+    message: String
+) {
+    pw.print(formattedTimestamp)
+    pw.print(" ")
+    pw.print(shortLogLevel)
+    pw.print(" ")
+    pw.print(tag)
+    pw.print(": ")
+    pw.println(message)
+}
+
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
new file mode 100644
index 0000000..f2a6a91
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+/** Recyclable implementation of [LogMessage]. */
+data class LogMessageImpl(
+    override var level: LogLevel,
+    override var tag: String,
+    override var timestamp: Long,
+    override var messagePrinter: MessagePrinter,
+    override var exception: Throwable?,
+    override var str1: String?,
+    override var str2: String?,
+    override var str3: String?,
+    override var int1: Int,
+    override var int2: Int,
+    override var long1: Long,
+    override var long2: Long,
+    override var double1: Double,
+    override var bool1: Boolean,
+    override var bool2: Boolean,
+    override var bool3: Boolean,
+    override var bool4: Boolean,
+) : LogMessage {
+
+    fun reset(
+        tag: String,
+        level: LogLevel,
+        timestamp: Long,
+        renderer: MessagePrinter,
+        exception: Throwable? = null,
+    ) {
+        this.level = level
+        this.tag = tag
+        this.timestamp = timestamp
+        this.messagePrinter = renderer
+        this.exception = exception
+        str1 = null
+        str2 = null
+        str3 = null
+        int1 = 0
+        int2 = 0
+        long1 = 0
+        long2 = 0
+        double1 = 0.0
+        bool1 = false
+        bool2 = false
+        bool3 = false
+        bool4 = false
+    }
+
+    companion object Factory {
+        fun create(): LogMessageImpl {
+            return LogMessageImpl(
+                LogLevel.DEBUG,
+                DEFAULT_TAG,
+                0,
+                DEFAULT_PRINTER,
+                null,
+                null,
+                null,
+                null,
+                0,
+                0,
+                0,
+                0,
+                0.0,
+                false,
+                false,
+                false,
+                false
+            )
+        }
+    }
+}
+
+private const val DEFAULT_TAG = "UnknownTag"
+private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
new file mode 100644
index 0000000..cfe894f
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+/** Keeps track of which [LogBuffer] messages should also appear in logcat. */
+interface LogcatEchoTracker {
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
+    fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
+
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
+    fun isTagLoggable(tagName: String, level: LogLevel): Boolean
+
+    /** Whether to log messages in a background thread. */
+    val logInBackgroundThread: Boolean
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
new file mode 100644
index 0000000..d3fabac
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+
+/**
+ * Version of [LogcatEchoTracker] for debuggable builds
+ *
+ * The log level of individual buffers or tags can be controlled via global settings:
+ *
+ * ```
+ * # Echo any message to <bufferName> of <level> or higher
+ * $ adb shell settings put global systemui/buffer/<bufferName> <level>
+ *
+ * # Echo any message of <tag> and of <level> or higher
+ * $ adb shell settings put global systemui/tag/<tag> <level>
+ * ```
+ */
+class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) :
+    LogcatEchoTracker {
+    private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
+    private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
+    override val logInBackgroundThread = true
+
+    companion object Factory {
+        @JvmStatic
+        fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug {
+            val tracker = LogcatEchoTrackerDebug(contentResolver)
+            tracker.attach(mainLooper)
+            return tracker
+        }
+    }
+
+    private fun attach(mainLooper: Looper) {
+        contentResolver.registerContentObserver(
+            Settings.Global.getUriFor(BUFFER_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedBufferLevels.clear()
+                }
+            }
+        )
+
+        contentResolver.registerContentObserver(
+            Settings.Global.getUriFor(TAG_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedTagLevels.clear()
+                }
+            }
+        )
+    }
+
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
+    @Synchronized
+    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
+        return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
+    }
+
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
+    @Synchronized
+    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
+        return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
+    }
+
+    private fun getLogLevel(
+        name: String,
+        path: String,
+        cache: MutableMap<String, LogLevel>
+    ): LogLevel {
+        return cache[name] ?: readSetting("$path/$name").also { cache[name] = it }
+    }
+
+    private fun readSetting(path: String): LogLevel {
+        return try {
+            parseProp(Settings.Global.getString(contentResolver, path))
+        } catch (_: Settings.SettingNotFoundException) {
+            DEFAULT_LEVEL
+        }
+    }
+
+    private fun parseProp(propValue: String?): LogLevel {
+        return when (propValue?.lowercase()) {
+            "verbose" -> LogLevel.VERBOSE
+            "v" -> LogLevel.VERBOSE
+            "debug" -> LogLevel.DEBUG
+            "d" -> LogLevel.DEBUG
+            "info" -> LogLevel.INFO
+            "i" -> LogLevel.INFO
+            "warning" -> LogLevel.WARNING
+            "warn" -> LogLevel.WARNING
+            "w" -> LogLevel.WARNING
+            "error" -> LogLevel.ERROR
+            "e" -> LogLevel.ERROR
+            "assert" -> LogLevel.WTF
+            "wtf" -> LogLevel.WTF
+            else -> DEFAULT_LEVEL
+        }
+    }
+}
+
+private val DEFAULT_LEVEL = LogLevel.WARNING
+private const val BUFFER_PATH = "systemui/buffer"
+private const val TAG_PATH = "systemui/tag"
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
new file mode 100644
index 0000000..3c8bda4
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins.log
+
+/** Production version of [LogcatEchoTracker] that isn't configurable. */
+class LogcatEchoTrackerProd : LogcatEchoTracker {
+    override val logInBackgroundThread = false
+
+    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
+        return level >= LogLevel.WARNING
+    }
+
+    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
+        return level >= LogLevel.WARNING
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
new file mode 100644
index 0000000..68d7890
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.plugins.util
+
+import kotlin.math.max
+
+/**
+ * A simple ring buffer implementation
+ *
+ * Use [advance] to get the least recent item in the buffer (and then presumably fill it with
+ * appropriate data). This will cause it to become the most recent item.
+ *
+ * As the buffer is used, it will grow, allocating new instances of T using [factory] until it
+ * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
+ * instances will be recycled from the back of the buffer and placed at the front.
+ *
+ * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
+ * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
+ * growing to [maxSize].
+ */
+class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> {
+
+    private val buffer = MutableList<T?>(maxSize) { null }
+
+    /**
+     * An abstract representation that points to the "end" of the buffer. Increments every time
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the
+     * backing array. Always points to the "next" available slot in the buffer. Before the buffer
+     * has completely filled, the value pointed to will be null. Afterward, it will be the value at
+     * the "beginning" of the buffer.
+     *
+     * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
+     * omega will overflow after a little under three million years of continuous operation.
+     */
+    private var omega: Long = 0
+
+    /**
+     * The number of items currently stored in the buffer. Calls to [advance] will cause this value
+     * to increase by one until it reaches [maxSize].
+     */
+    val size: Int
+        get() = if (omega < maxSize) omega.toInt() else maxSize
+
+    /**
+     * Advances the buffer's position by one and returns the value that is now present at the "end"
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise,
+     * reuses the value that was previously at the "beginning" of the buffer.
+     *
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was
+     * previously stored on it.
+     */
+    fun advance(): T {
+        val index = indexOf(omega)
+        omega += 1
+        val entry = buffer[index] ?: factory().also { buffer[index] = it }
+        return entry
+    }
+
+    /**
+     * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
+     * of the buffer) to [size]
+     * - 1 (the "end", or newest element of the buffer).
+     */
+    operator fun get(index: Int): T {
+        if (index < 0 || index >= size) {
+            throw IndexOutOfBoundsException("Index $index is out of bounds")
+        }
+
+        // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
+        // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
+        // not yet full and our start should be 0. However, in modspace, maxSize and 0 are
+        // equivalent, so we can get away with using it as the start value instead.
+        val start = max(omega, maxSize.toLong())
+
+        return buffer[indexOf(start + index)]!!
+    }
+
+    inline fun forEach(action: (T) -> Unit) {
+        for (i in 0 until size) {
+            action(get(i))
+        }
+    }
+
+    override fun iterator(): Iterator<T> {
+        return object : Iterator<T> {
+            private var position: Int = 0
+
+            override fun next(): T {
+                if (position >= size) {
+                    throw NoSuchElementException()
+                }
+                return get(position).also { position += 1 }
+            }
+
+            override fun hasNext(): Boolean {
+                return position < size
+            }
+        }
+    }
+
+    private fun indexOf(position: Long): Int {
+        return (position % maxSize).toInt()
+    }
+}
diff --git a/packages/SystemUI/plugin/tests/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
new file mode 100644
index 0000000..a39b856
--- /dev/null
+++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
@@ -0,0 +1,138 @@
+package com.android.systemui.log
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.log.LogBuffer
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+@SmallTest
+@RunWith(MockitoJUnitRunner::class)
+class LogBufferTest : SysuiTestCase() {
+    private lateinit var buffer: LogBuffer
+
+    private lateinit var outputWriter: StringWriter
+
+    @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker
+
+    @Before
+    fun setup() {
+        outputWriter = StringWriter()
+        buffer = createBuffer()
+    }
+
+    private fun createBuffer(): LogBuffer {
+        return LogBuffer("TestBuffer", 1, logcatEchoTracker, false)
+    }
+
+    @Test
+    fun log_shouldSaveLogToBuffer() {
+        buffer.log("Test", LogLevel.INFO, "Some test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Some test message")
+    }
+
+    @Test
+    fun log_shouldRotateIfLogBufferIsFull() {
+        buffer.log("Test", LogLevel.INFO, "This should be rotated")
+        buffer.log("Test", LogLevel.INFO, "New test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("New test message")
+    }
+
+    @Test
+    fun dump_writesExceptionAndStacktrace() {
+        buffer = createBuffer()
+        val exception = createTestException("Exception message", "TestClass")
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Extra message")
+        assertThat(dumpedString).contains("java.lang.RuntimeException: Exception message")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
+    }
+
+    @Test
+    fun dump_writesCauseAndStacktrace() {
+        buffer = createBuffer()
+        val exception =
+            createTestException(
+                "Exception message",
+                "TestClass",
+                cause = createTestException("The real cause!", "TestClass")
+            )
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
+    }
+
+    @Test
+    fun dump_writesSuppressedExceptionAndStacktrace() {
+        buffer = createBuffer()
+        val exception = RuntimeException("Root exception message")
+        exception.addSuppressed(
+            createTestException(
+                "First suppressed exception",
+                "FirstClass",
+                createTestException("Cause of suppressed exp", "ThirdClass")
+            )
+        )
+        exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedStr = dumpBuffer()
+
+        // first suppressed exception
+        assertThat(dumpedStr)
+            .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception")
+        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
+        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
+
+        assertThat(dumpedStr)
+            .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
+        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
+
+        // second suppressed exception
+        assertThat(dumpedStr)
+            .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception")
+        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
+        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
+    }
+
+    private fun createTestException(
+        message: String,
+        errorClass: String,
+        cause: Throwable? = null,
+    ): Exception {
+        val exception = RuntimeException(message, cause)
+        exception.stackTrace =
+            (1..5)
+                .map { lineNumber ->
+                    StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber)
+                }
+                .toTypedArray()
+        return exception
+    }
+
+    private fun dumpBuffer(): String {
+        buffer.dump(PrintWriter(outputWriter), tailLength = 100)
+        return outputWriter.toString()
+    }
+}
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index a3b4b38..6976786 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -25,9 +25,15 @@
 
 -keep class ** extends androidx.preference.PreferenceFragment
 -keep class com.android.systemui.tuner.*
+
+# The plugins and animation subpackages both act as shared libraries that might be referenced in
+# dynamically-loaded plugin APKs.
 -keep class com.android.systemui.plugins.** {
     *;
 }
+-keep class !com.android.systemui.animation.R$**,com.android.systemui.animation.** {
+    *;
+}
 -keep class com.android.systemui.fragments.FragmentService$FragmentCreator {
     *;
 }
diff --git a/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
new file mode 100644
index 0000000..de0e526
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
@@ -0,0 +1,20 @@
+<?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
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+    <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/>
+    <solid android:color="@color/user_switcher_fullscreen_bg"/>
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
deleted file mode 100644
index 9e61236..0000000
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ /dev/null
@@ -1,17 +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.
-  -->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 3ad7c8c..b49afee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -31,12 +31,13 @@
         android:layout_height="wrap_content"
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
-        android:paddingStart="@dimen/clock_padding_start" />
+        android:paddingStart="@dimen/clock_padding_start"
+        android:visibility="invisible" />
     <FrameLayout
         android:id="@+id/lockscreen_clock_view_large"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/keyguard_large_clock_top_margin"
+        android:clipChildren="false"
         android:visibility="gone" />
 
     <!-- Not quite optimal but needed to translate these items as a group. The
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
index 16a1d94..647abee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
@@ -27,6 +27,7 @@
     systemui:layout_constraintEnd_toEndOf="parent"
     systemui:layout_constraintTop_toTopOf="parent"
     android:layout_marginHorizontal="@dimen/status_view_margin_horizontal"
+    android:clipChildren="false"
     android:layout_width="0dp"
     android:layout_height="wrap_content">
     <LinearLayout
diff --git a/packages/SystemUI/res-keyguard/values-af/strings.xml b/packages/SystemUI/res-keyguard/values-af/strings.xml
index d5e84f9..1ff549e 100644
--- a/packages/SystemUI/res-keyguard/values-af/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-af/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wagwoord word vereis nadat toestel herbegin het"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon word vir bykomende sekuriteit vereis"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN word vir bykomende sekuriteit vereis"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wagwoord word vir bykomende sekuriteit vereis"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Gebruik eerder ’n patroon vir bykomende sekuriteit"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Gebruik eerder ’n PIN vir bykomende sekuriteit"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Gebruik eerder ’n wagwoord vir bykomende sekuriteit"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Toestel is deur administrateur gesluit"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Toestel is handmatig gesluit"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie herken nie"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Verstek"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Borrel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ontsluit jou toestel om voort te gaan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-am/strings.xml b/packages/SystemUI/res-keyguard/values-am/strings.xml
index be52c44..f61c8cf 100644
--- a/packages/SystemUI/res-keyguard/values-am/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-am/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"መሣሪያ ዳግም ከጀመረ በኋላ ሥርዓተ ጥለት ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"መሣሪያ ዳግም ከተነሳ በኋላ ፒን ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"መሣሪያ ዳግም ከጀመረ በኋላ የይለፍ ቃል ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ሥርዓተ ጥለት ለተጨማሪ ደህንነት ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ፒን ለተጨማሪ ደህንነት ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"የይለፍ ቃል ለተጨማሪ ደህንነት ያስፈልጋል"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ለተጨማሪ ደህንነት በምትኩ ስርዓተ ጥለት ይጠቀሙ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ለተጨማሪ ደህንነት በምትኩ ፒን ይጠቀሙ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ለተጨማሪ ደህንነት በምትኩ የይለፍ ቃል ይጠቀሙ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"መሣሪያ በአስተዳዳሪ ተቆልፏል"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"መሣሪያ በተጠቃሚው ራሱ ተቆልፏል"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"አልታወቀም"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ነባሪ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"አረፋ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"አናሎግ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ለመቀጠል መሣሪያዎን ይክፈቱ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ar/strings.xml b/packages/SystemUI/res-keyguard/values-ar/strings.xml
index adb57b6..f3256ba 100644
--- a/packages/SystemUI/res-keyguard/values-ar/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ar/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"يجب رسم النقش بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"يجب إدخال رقم التعريف الشخصي بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"يجب إدخال كلمة المرور بعد إعادة تشغيل الجهاز"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"يجب رسم النقش لمزيد من الأمان"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"يجب إدخال رقم التعريف الشخصي لمزيد من الأمان"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"يجب إدخال كلمة المرور لمزيد من الأمان"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"لمزيد من الأمان، استخدِم النقش بدلاً من ذلك."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"لمزيد من الأمان، أدخِل رقم التعريف الشخصي بدلاً من ذلك."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"لمزيد من الأمان، أدخِل كلمة المرور بدلاً من ذلك."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"اختار المشرف قفل الجهاز"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"تم حظر الجهاز يدويًا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"لم يتم التعرّف عليه."</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"تلقائي"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"فقاعة"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ساعة تقليدية"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"يجب فتح قفل الجهاز للمتابعة"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-as/strings.xml b/packages/SystemUI/res-keyguard/values-as/strings.xml
index cbfb325..f9dc46f 100644
--- a/packages/SystemUI/res-keyguard/values-as/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-as/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত আৰ্হি দিয়াটো বাধ্যতামূলক"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পিন দিয়াটো বাধ্যতামূলক"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পাছৱৰ্ড দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিৰিক্ত সুৰক্ষাৰ বাবে আর্হি দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিৰিক্ত সুৰক্ষাৰ বাবে পিন দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিৰিক্ত সুৰক্ষাৰ বাবে পাছৱর্ড দিয়াটো বাধ্যতামূলক"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে আৰ্হি ব্যৱহাৰ কৰক"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে পিন ব্যৱহাৰ কৰক"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"অতিৰিক্ত সুৰক্ষাৰ বাবে, ইয়াৰ পৰিৱৰ্তে পাছৱৰ্ড ব্যৱহাৰ কৰক"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্ৰশাসকে ডিভাইচ লক কৰি ৰাখিছে"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইচটো মেনুৱেলভাৱে লক কৰা হৈছিল"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"চিনাক্ত কৰিব পৰা নাই"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফ’ল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"এনাল’গ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"অব্যাহত ৰাখিবলৈ আপোনাৰ ডিভাইচটো আনলক কৰক"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-az/strings.xml b/packages/SystemUI/res-keyguard/values-az/strings.xml
index 6ec1061..65c1c93 100644
--- a/packages/SystemUI/res-keyguard/values-az/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-az/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yenidən başladıqdan sonra model tələb olunur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıqdan sonra PIN tələb olunur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıqdan sonra parol tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Əlavə təhlükəsizlik üçün model tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Əlavə təhlükəsizlik üçün PIN tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Əlavə təhlükəsizlik üçün parol tələb olunur"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Əlavə təhlükəsizlik üçün modeldən istifadə edin"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Əlavə təhlükəsizlik üçün PIN istifadə edin"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Əlavə təhlükəsizlik üçün paroldan istifadə edin"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz admin tərəfindən kilidlənib"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihaz əl ilə kilidləndi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmır"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Defolt"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Qabarcıq"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoq"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davam etmək üçün cihazınızın kilidini açın"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
index 13d6613..cf363df 100644
--- a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Treba da unesete šablon kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Treba da unesete PIN kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Treba da unesete lozinku kada se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Treba da unesete šablon radi dodatne bezbednosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Treba da unesete PIN radi dodatne bezbednosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Treba da unesete lozinku radi dodatne bezbednosti"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatnu bezbednost koristite šablon"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatnu bezbednost koristite PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatnu bezbednost koristite lozinku"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznat"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Podrazumevani"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da biste nastavili"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-be/strings.xml b/packages/SystemUI/res-keyguard/values-be/strings.xml
index 616d31a..c2dedf30 100644
--- a/packages/SystemUI/res-keyguard/values-be/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-be/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Пасля перазапуску прылады патрабуецца ўзор"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Пасля перазапуску прылады патрабуецца PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Пасля перазапуску прылады патрабуецца пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для забеспячэння дадатковай бяспекі патрабуецца ўзор"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для забеспячэння дадатковай бяспекі патрабуецца PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для забеспячэння дадатковай бяспекі патрабуецца пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"У мэтах дадатковай бяспекі скарыстайце ўзор разблакіроўкі"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"У мэтах дадатковай бяспекі скарыстайце PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"У мэтах дадатковай бяспекі скарыстайце пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Прылада заблакіравана адміністратарам"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Прылада была заблакіравана ўручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распазнана"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартны"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бурбалкі"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Са стрэлкамі"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Каб працягнуць, разблакіруйце прыладу"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bg/strings.xml b/packages/SystemUI/res-keyguard/values-bg/strings.xml
index 366a7f4..546a645 100644
--- a/packages/SystemUI/res-keyguard/values-bg/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bg/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"След рестартиране на устройството се изисква фигура"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"След рестартиране на устройството се изисква ПИН код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"След рестартиране на устройството се изисква парола"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"За допълнителна сигурност се изисква фигура"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"За допълнителна сигурност се изисква ПИН код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"За допълнителна сигурност се изисква парола"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За допълнителна сигурност използвайте фигура вместо това"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За допълнителна сигурност използвайте ПИН код вместо това"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За допълнителна сигурност използвайте парола вместо това"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройството е заключено от администратора"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройството бе заключено ръчно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не е разпознато"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартен"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонен"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогов"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отключете устройството си, за да продължите"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bn/strings.xml b/packages/SystemUI/res-keyguard/values-bn/strings.xml
index c20be5d..7b3df35 100644
--- a/packages/SystemUI/res-keyguard/values-bn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bn/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইসটি পুনরায় চালু হওয়ার পর প্যাটার্নের প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইসটি পুনরায় চালু হওয়ার পর পিন প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইসটি পুনরায় চালু হওয়ার পর পাসওয়ার্ডের প্রয়োজন হবে"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিরিক্ত সুরক্ষার জন্য প্যাটার্ন দেওয়া প্রয়োজন"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিরিক্ত সুরক্ষার জন্য পিন দেওয়া প্রয়োজন"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিরিক্ত সুরক্ষার জন্য পাসওয়ার্ড দেওয়া প্রয়োজন"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে প্যাটার্ন ব্যবহার করুন"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে পিন ব্যবহার করুন"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"অতিরিক্ত সুরক্ষার জন্য, এর বদলে পাসওয়ার্ড ব্যবহার করুন"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্রশাসক ডিভাইসটি লক করেছেন"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইসটিকে ম্যানুয়ালি লক করা হয়েছে"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"শনাক্ত করা যায়নি"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"অ্যানালগ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"চালিয়ে যেতে আপনার ডিভাইস আনলক করুন"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bs/strings.xml b/packages/SystemUI/res-keyguard/values-bs/strings.xml
index f1c00a9..bb9e690 100644
--- a/packages/SystemUI/res-keyguard/values-bs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bs/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Potreban je uzorak nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Potreban je PIN nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Potrebna je lozinka nakon što se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Uzorak je potreban radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN je potreban radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lozinka je potrebna radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Radi dodatne zaštite, umjesto toga koristite uzorak"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Radi dodatne zaštite, umjesto toga koristite PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Radi dodatne zašitite, umjesto toga koristite lozinku"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Uređaj je zaključao administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da nastavite"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ca/strings.xml b/packages/SystemUI/res-keyguard/values-ca/strings.xml
index 709407c..1c81c60 100644
--- a/packages/SystemUI/res-keyguard/values-ca/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ca/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cal introduir el patró quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cal introduir el PIN quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cal introduir la contrasenya quan es reinicia el dispositiu"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Cal introduir el patró per disposar de més seguretat"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Cal introduir el PIN per disposar de més seguretat"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Cal introduir la contrasenya per disposar de més seguretat"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Per a més seguretat, utilitza el patró"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Per a més seguretat, utilitza el PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Per a més seguretat, utilitza la contrasenya"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'administrador ha bloquejat el dispositiu"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositiu s\'ha bloquejat manualment"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No s\'ha reconegut"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminada"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bombolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analògica"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueja el dispositiu per continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-cs/strings.xml b/packages/SystemUI/res-keyguard/values-cs/strings.xml
index a44658c..9a6178c 100644
--- a/packages/SystemUI/res-keyguard/values-cs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-cs/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po restartování zařízení je vyžadováno gesto"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po restartování zařízení je vyžadován kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po restartování zařízení je vyžadováno heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pro ještě lepší zabezpečení je vyžadováno gesto"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pro ještě lepší zabezpečení je vyžadován kód PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pro ještě lepší zabezpečení je vyžadováno heslo"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Z bezpečnostních důvodů raději použijte gesto"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Z bezpečnostních důvodů raději použijte PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Z bezpečnostních důvodů raději použijte heslo"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zařízení je uzamknuto administrátorem"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zařízení bylo ručně uzamčeno"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznáno"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Výchozí"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogové"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Pokud chcete pokračovat, odemkněte zařízení"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-da/strings.xml b/packages/SystemUI/res-keyguard/values-da/strings.xml
index 331c355..aac1b83 100644
--- a/packages/SystemUI/res-keyguard/values-da/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-da/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du skal angive et mønster, når du har genstartet enheden"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Der skal angives en pinkode efter genstart af enheden"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Der skal angives en adgangskode efter genstart af enheden"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Der kræves et mønster som ekstra beskyttelse"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Der kræves en pinkode som ekstra beskyttelse"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Der kræves en adgangskode som ekstra beskyttelse"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Øg sikkerheden ved at bruge dit oplåsningsmønter i stedet"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Øg sikkerheden ved at bruge din pinkode i stedet"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Øg sikkerheden ved at bruge din adgangskode i stedet"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheden er blevet låst af administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheden blev låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke genkendt"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås din enhed op for at fortsætte"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-de/strings.xml b/packages/SystemUI/res-keyguard/values-de/strings.xml
index c19b357..5a340ff 100644
--- a/packages/SystemUI/res-keyguard/values-de/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-de/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nach dem Neustart des Geräts ist die Eingabe des Musters erforderlich"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nach dem Neustart des Geräts ist die Eingabe der PIN erforderlich"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nach dem Neustart des Geräts ist die Eingabe des Passworts erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zur Verbesserung der Sicherheit ist ein Muster erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zur Verbesserung der Sicherheit ist eine PIN erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zur Verbesserung der Sicherheit ist ein Passwort erforderlich"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Verwende für mehr Sicherheit stattdessen dein Muster"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Verwende für mehr Sicherheit stattdessen deine PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Verwende für mehr Sicherheit stattdessen dein Passwort"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Gerät vom Administrator gesperrt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Gerät manuell gesperrt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nicht erkannt"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Gerät entsperren, um fortzufahren"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-el/strings.xml b/packages/SystemUI/res-keyguard/values-el/strings.xml
index 1d6ec82..973139f 100644
--- a/packages/SystemUI/res-keyguard/values-el/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-el/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Απαιτείται μοτίβο μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Απαιτείται PIN μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Απαιτείται κωδικός πρόσβασης μετά από την επανεκκίνηση της συσκευής"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Απαιτείται μοτίβο για πρόσθετη ασφάλεια"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Απαιτείται PIN για πρόσθετη ασφάλεια"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Απαιτείται κωδικός πρόσβασης για πρόσθετη ασφάλεια"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά μοτίβο"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Για πρόσθετη ασφάλεια, χρησιμοποιήστε εναλλακτικά κωδικό πρόσβασης"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Η συσκευή κλειδώθηκε από τον διαχειριστή"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Η συσκευή κλειδώθηκε με μη αυτόματο τρόπο"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Δεν αναγνωρίστηκε"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Προεπιλογή"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Συννεφάκι"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Αναλογικό"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ξεκλειδώστε τη συσκευή σας για να συνεχίσετε"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
index 2b78f96..41eaa389 100644
--- a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
index e1c2532..a948c04 100644
--- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
@@ -23,7 +23,7 @@
     <string name="keyguard_enter_your_pin" msgid="5429932527814874032">"Enter your PIN"</string>
     <string name="keyguard_enter_your_pattern" msgid="351503370332324745">"Enter your pattern"</string>
     <string name="keyguard_enter_your_password" msgid="7225626204122735501">"Enter your password"</string>
-    <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid card."</string>
+    <string name="keyguard_sim_error_message_short" msgid="633630844240494070">"Invalid Card."</string>
     <string name="keyguard_charged" msgid="5478247181205188995">"Charged"</string>
     <string name="keyguard_plugged_in_wireless" msgid="2537874724955057383">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging wirelessly"</string>
     <string name="keyguard_plugged_in_dock" msgid="2122073051904360987">"<xliff:g id="PERCENTAGE">%s</xliff:g> • Charging"</string>
@@ -70,7 +70,7 @@
     <string name="kg_password_wrong_pin_code_pukked" msgid="8047350661459040581">"Incorrect SIM PIN code you must now contact your carrier to unlock your device."</string>
     <string name="kg_password_wrong_pin_code" msgid="5629415765976820357">"{count,plural, =1{Incorrect SIM PIN code, you have # remaining attempt before you must contact your carrier to unlock your device.}other{Incorrect SIM PIN code, you have # remaining attempts. }}"</string>
     <string name="kg_password_wrong_puk_code_dead" msgid="3698285357028468617">"SIM is unusable. Contact your carrier."</string>
-    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code; you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
+    <string name="kg_password_wrong_puk_code" msgid="6820515467645087827">"{count,plural, =1{Incorrect SIM PUK code, you have # remaining attempt before SIM becomes permanently unusable.}other{Incorrect SIM PUK code, you have # remaining attempts before SIM becomes permanently unusable.}}"</string>
     <string name="kg_password_pin_failed" msgid="5136259126330604009">"SIM PIN operation failed!"</string>
     <string name="kg_password_puk_failed" msgid="6778867411556937118">"SIM PUK operation failed!"</string>
     <string name="accessibility_ime_switch_button" msgid="9082358310194861329">"Switch input method"</string>
@@ -78,16 +78,17 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
-    <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
+    <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognized"</string>
     <string name="kg_face_sensor_privacy_enabled" msgid="939511161763558512">"To use Face Unlock, turn on camera access in Settings"</string>
     <string name="kg_password_default_pin_message" msgid="1434544655827987873">"{count,plural, =1{Enter SIM PIN. You have # remaining attempt before you must contact your carrier to unlock your device.}other{Enter SIM PIN. You have # remaining attempts.}}"</string>
     <string name="kg_password_default_puk_message" msgid="1025139786449741950">"{count,plural, =1{SIM is now disabled. Enter PUK code to continue. You have # remaining attempt before SIM becomes permanently unusable. Contact carrier for details.}other{SIM is now disabled. Enter PUK code to continue. You have # remaining attempts before SIM becomes permanently unusable. Contact carrier for details.}}"</string>
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
-    <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
index 2b78f96..41eaa389 100644
--- a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
index 2b78f96..41eaa389 100644
--- a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"For additional security, use pattern instead"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"For additional security, use PIN instead"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"For additional security, use password instead"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Unlock your device to continue"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
index 9052e4f..a23aeb0 100644
--- a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎Pattern required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‎‎‎‎‎‏‎‎‎‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‎PIN required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎Password required after device restarts‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‏‎Pattern required for additional security‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‎‎‎‏‎‏‏‎‏‎‎‎‎‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎PIN required for additional security‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‎‎Password required for additional security‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‎For additional security, use pattern instead‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‏‎‎‎‎‎For additional security, use PIN instead‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎For additional security, use password instead‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‏‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‏‎‏‎Device locked by admin‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎Device was locked manually‎‏‎‎‏‎"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎Not recognized‎‏‎‎‏‎"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎Default‎‏‎‎‏‎"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎‎‏‎‏‏‏‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‏‏‎‏‎Bubble‎‏‎‎‏‎"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎Analog‎‏‎‎‏‎"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‏‏‎‏‏‏‏‎Unlock your device to continue‎‏‎‎‏‎"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
index 9dc054a..6314d90 100644
--- a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Se requiere el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Se requiere el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Se requiere la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Se requiere el patrón por razones de seguridad"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Se requiere el PIN por razones de seguridad"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Se requiere la contraseña por razones de seguridad"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para seguridad adicional, usa un patrón"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para seguridad adicional, usa un PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para seguridad adicional, usa una contraseña"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se bloqueó de forma manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoció"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea tu dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es/strings.xml b/packages/SystemUI/res-keyguard/values-es/strings.xml
index f9f0452..5aecf84 100644
--- a/packages/SystemUI/res-keyguard/values-es/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Debes introducir el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Debes introducir el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Debes introducir la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Debes introducir el patrón como medida de seguridad adicional"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Debes introducir el PIN como medida de seguridad adicional"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Debes introducir la contraseña como medida de seguridad adicional"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para mayor seguridad, usa el patrón"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para mayor seguridad, usa el PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para mayor seguridad, usa la contraseña"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se ha bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoce"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea tu dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-et/strings.xml b/packages/SystemUI/res-keyguard/values-et/strings.xml
index dceb78e..9306ff6 100644
--- a/packages/SystemUI/res-keyguard/values-et/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-et/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pärast seadme taaskäivitamist tuleb sisestada muster"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pärast seadme taaskäivitamist tuleb sisestada PIN-kood"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pärast seadme taaskäivitamist tuleb sisestada parool"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Lisaturvalisuse huvides tuleb sisestada muster"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Lisaturvalisuse huvides tuleb sisestada PIN-kood"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lisaturvalisuse huvides tuleb sisestada parool"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Kasutage tugevama turvalisuse huvides hoopis mustrit"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Kasutage tugevama turvalisuse huvides hoopis PIN-koodi"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Kasutage tugevama turvalisuse huvides hoopis parooli"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administraator lukustas seadme"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Seade lukustati käsitsi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tuvastatud"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Vaikenumbrilaud"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mull"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Jätkamiseks avage oma seade"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-eu/strings.xml b/packages/SystemUI/res-keyguard/values-eu/strings.xml
index 8431268..4ebe0f0 100644
--- a/packages/SystemUI/res-keyguard/values-eu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-eu/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Eredua marraztu beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PINa idatzi beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pasahitza idatzi beharko duzu gailua berrabiarazten denean"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Eredua behar da gailua babestuago izateko"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PINa behar da gailua babestuago izateko"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pasahitza behar da gailua babestuago izateko"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Babestuago egoteko, erabili eredua"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Babestuago egoteko, erabili PINa"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Babestuago egoteko, erabili pasahitza"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratzaileak blokeatu egin du gailua"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Eskuz blokeatu da gailua"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ez da ezagutu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Lehenetsia"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Puxikak"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogikoa"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Aurrera egiteko, desblokeatu gailua"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fa/strings.xml b/packages/SystemUI/res-keyguard/values-fa/strings.xml
index 37bb260..e9a2e87 100644
--- a/packages/SystemUI/res-keyguard/values-fa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fa/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"بعد از بازنشانی دستگاه باید الگو وارد شود"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"بعد از بازنشانی دستگاه باید پین وارد شود"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"بعد از بازنشانی دستگاه باید گذرواژه وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"برای ایمنی بیشتر باید الگو وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"برای ایمنی بیشتر باید پین وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"برای ایمنی بیشتر باید گذرواژه وارد شود"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"برای امنیت بیشتر، به‌جای آن از الگو استفاده کنید"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"برای امنیت بیشتر، به‌جای آن از پین استفاده کنید"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"برای امنیت بیشتر، به‌جای آن از گذرواژه استفاده کنید"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"دستگاه توسط سرپرست سیستم قفل شده است"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"دستگاه به‌صورت دستی قفل شده است"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"شناسایی نشد"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"پیش‌فرض"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"حباب"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"آنالوگ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"برای ادامه، قفل دستگاهتان را باز کنید"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fi/strings.xml b/packages/SystemUI/res-keyguard/values-fi/strings.xml
index f8cec42..e80869a 100644
--- a/packages/SystemUI/res-keyguard/values-fi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fi/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kuvio vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-koodi vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Salasana vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kuvio vaaditaan suojauksen parantamiseksi."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-koodi vaaditaan suojauksen parantamiseksi."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Salasana vaaditaan suojauksen parantamiseksi."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Lisäsuojaa saat, kun käytät sen sijaan kuviota"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Lisäsuojaa saat, kun käytät sen sijaan PIN-koodia"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Lisäsuojaa saat, kun käytät sen sijaan salasanaa"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Järjestelmänvalvoja lukitsi laitteen."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Laite lukittiin manuaalisesti"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tunnistettu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Oletus"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kupla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginen"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Jatka avaamalla laitteen lukitus"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
index 077fe11..66fd7c0 100644
--- a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Le schéma est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Le NIP est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Le mot de passe est exigé après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Le schéma est exigé pour plus de sécurité"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Le NIP est exigé pour plus de sécurité"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Le mot de passe est exigé pour plus de sécurité"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un NIP"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'appareil a été verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"L\'appareil a été verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Doigt non reconnu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouiller votre appareil pour continuer"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr/strings.xml b/packages/SystemUI/res-keyguard/values-fr/strings.xml
index 45dadc1..92d0617 100644
--- a/packages/SystemUI/res-keyguard/values-fr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Veuillez dessiner le schéma après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Veuillez saisir le code après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Veuillez saisir le mot de passe après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Veuillez dessiner le schéma pour renforcer la sécurité"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Veuillez saisir le code pour renforcer la sécurité"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Veuillez saisir le mot de passe pour renforcer la sécurité"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un code"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Appareil verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Appareil verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non reconnu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouillez votre appareil pour continuer"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gl/strings.xml b/packages/SystemUI/res-keyguard/values-gl/strings.xml
index 4fbdd67..776e90a 100644
--- a/packages/SystemUI/res-keyguard/values-gl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necesario o padrón despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necesario o PIN despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necesario o contrasinal despois do reinicio do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"É necesario o padrón para obter seguranza adicional"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"É necesario o PIN para obter seguranza adicional"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"É necesario o contrasinal para obter seguranza adicional"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Utiliza un padrón para obter maior seguranza"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Utiliza un PIN para obter maior seguranza"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Utiliza un contrasinal para obter maior seguranza"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"O administrador bloqueou o dispositivo"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo bloqueouse manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non se recoñeceu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbulla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóxico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloquea o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gu/strings.xml b/packages/SystemUI/res-keyguard/values-gu/strings.xml
index 6caac8a..a8b9a3a 100644
--- a/packages/SystemUI/res-keyguard/values-gu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gu/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પૅટર્ન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પિન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પાસવર્ડ જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"વધારાની સુરક્ષા માટે પૅટર્ન જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"વધારાની સુરક્ષા માટે પિન જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"વધારાની સુરક્ષા માટે પાસવર્ડ જરૂરી છે"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"વધારાની સુરક્ષા માટે, તેના બદલે પૅટર્નનો ઉપયોગ કરો"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"વધારાની સુરક્ષા માટે, તેના બદલે પિનનો ઉપયોગ કરો"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"વધારાની સુરક્ષા માટે, તેના બદલે પાસવર્ડનો ઉપયોગ કરો"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"વ્યવસ્થાપકે ઉપકરણ લૉક કરેલું છે"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ઉપકરણ મેન્યુઅલી લૉક કર્યું હતું"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ઓળખાયેલ નથી"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ડિફૉલ્ટ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"બબલ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"એનાલોગ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ચાલુ રાખવા માટે તમારા ડિવાઇસને અનલૉક કરો"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hi/strings.xml b/packages/SystemUI/res-keyguard/values-hi/strings.xml
index 627576e..47560dd 100644
--- a/packages/SystemUI/res-keyguard/values-hi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hi/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिवाइस फिर से चालू होने के बाद पैटर्न ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिवाइस फिर से चालू होने के बाद पिन ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिवाइस फिर से चालू होने के बाद पासवर्ड ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षा के लिए पैटर्न ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षा के लिए पिन ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षा के लिए पासवर्ड ज़रूरी है"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ज़्यादा सुरक्षा के लिए, इसके बजाय पैटर्न का इस्तेमाल करें"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ज़्यादा सुरक्षा के लिए, इसके बजाय पिन का इस्तेमाल करें"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ज़्यादा सुरक्षा के लिए, इसके बजाय पासवर्ड का इस्तेमाल करें"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"व्यवस्थापक ने डिवाइस को लॉक किया है"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिवाइस को मैन्युअल रूप से लॉक किया गया था"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहचान नहीं हो पाई"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डिफ़ॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालॉग"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"जारी रखने के लिए डिवाइस अनलॉक करें"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hr/strings.xml b/packages/SystemUI/res-keyguard/values-hr/strings.xml
index 8b1b504..efd1cbb 100644
--- a/packages/SystemUI/res-keyguard/values-hr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hr/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nakon ponovnog pokretanja uređaja morate unijeti uzorak"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nakon ponovnog pokretanja uređaja morate unijeti PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nakon ponovnog pokretanja uređaja morate unijeti zaporku"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Unesite uzorak radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Unesite PIN radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Unesite zaporku radi dodatne sigurnosti"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatnu sigurnost upotrijebite uzorak"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatnu sigurnost upotrijebite PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatnu sigurnost upotrijebite zaporku"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurić"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Otključajte uređaj da biste nastavili"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hu/strings.xml b/packages/SystemUI/res-keyguard/values-hu/strings.xml
index 6b75e72..0421ff8 100644
--- a/packages/SystemUI/res-keyguard/values-hu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hu/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Az eszköz újraindítását követően meg kell adni a mintát"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Az eszköz újraindítását követően meg kell adni a PIN-kódot"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Az eszköz újraindítását követően meg kell adni a jelszót"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"A nagyobb biztonság érdekében minta szükséges"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"A nagyobb biztonság érdekében PIN-kód szükséges"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A nagyobb biztonság érdekében jelszó szükséges"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"A nagyobb biztonság érdekében használjon inkább mintát"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"A nagyobb biztonság érdekében használjon inkább PIN-kódot"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"A nagyobb biztonság érdekében használjon inkább jelszót"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"A rendszergazda zárolta az eszközt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Az eszközt manuálisan lezárták"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nem ismerhető fel"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Alapértelmezett"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Buborék"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóg"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"A folytatáshoz oldja fel az eszközét"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hy/strings.xml b/packages/SystemUI/res-keyguard/values-hy/strings.xml
index 3412026..d421c29 100644
--- a/packages/SystemUI/res-keyguard/values-hy/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hy/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել նախշը"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել PIN կոդը"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել նախշը"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել PIN կոդը"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Լրացուցիչ անվտանգության համար օգտագործեք նախշ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Լրացուցիչ անվտանգության համար օգտագործեք PIN կոդ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Լրացուցիչ անվտանգության համար օգտագործեք գաղտնաբառ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Սարքը կողպված է ադմինիստրատորի կողմից"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Սարքը կողպվել է ձեռքով"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Չհաջողվեց ճանաչել"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Կանխադրված"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Պղպջակ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Անալոգային"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Շարունակելու համար ապակողպեք ձեր սարքը"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-in/strings.xml b/packages/SystemUI/res-keyguard/values-in/strings.xml
index 1afb791..2061e85 100644
--- a/packages/SystemUI/res-keyguard/values-in/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-in/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pola diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Sandi diperlukan setelah perangkat dimulai ulang"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pola diperlukan untuk keamanan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keamanan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Sandi diperlukan untuk keamanan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Untuk keamanan tambahan, gunakan pola"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Untuk keamanan tambahan, gunakan PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Untuk keamanan tambahan, gunakan sandi"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Perangkat dikunci oleh admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Perangkat dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Buka kunci perangkat untuk melanjutkan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-is/strings.xml b/packages/SystemUI/res-keyguard/values-is/strings.xml
index 6abdc82..ae3da57 100644
--- a/packages/SystemUI/res-keyguard/values-is/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-is/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Mynsturs er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-númers er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Aðgangsorðs er krafist þegar tækið er endurræst"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mynsturs er krafist af öryggisástæðum"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-númers er krafist af öryggisástæðum"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Aðgangsorðs er krafist af öryggisástæðum"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Fyrir aukið öryggi skaltu nota mynstur í staðinn"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Fyrir aukið öryggi skaltu nota PIN-númer í staðinn"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Fyrir aukið öryggi skaltu nota aðgangsorð í staðinn"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Kerfisstjóri læsti tæki"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Tækinu var læst handvirkt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Þekktist ekki"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Sjálfgefið"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Blaðra"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Með vísum"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Taktu tækið úr lás til að halda áfram"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-it/strings.xml b/packages/SystemUI/res-keyguard/values-it/strings.xml
index 9fed5f7..d1feea6 100644
--- a/packages/SystemUI/res-keyguard/values-it/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-it/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Sequenza obbligatoria dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN obbligatorio dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password obbligatoria dopo il riavvio del dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Sequenza obbligatoria per maggiore sicurezza"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN obbligatorio per maggiore sicurezza"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password obbligatoria per maggiore sicurezza"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Per maggior sicurezza, usa invece la sequenza"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Per maggior sicurezza, usa invece il PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Per maggior sicurezza, usa invece la password"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloccato dall\'amministratore"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Il dispositivo è stato bloccato manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non riconosciuto"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinito"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Sblocca il dispositivo per continuare"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-iw/strings.xml b/packages/SystemUI/res-keyguard/values-iw/strings.xml
index b5b1c53..aab4206 100644
--- a/packages/SystemUI/res-keyguard/values-iw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-iw/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"יש להזין את קו ביטול הנעילה לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"צריך להזין קוד אימות לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"יש להזין סיסמה לאחר הפעלה מחדש של המכשיר"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"יש להזין את קו ביטול הנעילה כדי להגביר את רמת האבטחה"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"יש להזין קוד אימות כדי להגביר את רמת האבטחה"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"יש להזין סיסמה כדי להגביר את רמת האבטחה"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקו ביטול נעילה במקום זאת"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקוד אימות במקום זאת"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"כדי להגביר את רמת האבטחה, כדאי להשתמש בסיסמה במקום זאת"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"המנהל של המכשיר נהל אותו"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"המכשיר ננעל באופן ידני"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"לא זוהתה"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ברירת מחדל"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"בועה"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"אנלוגי"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"צריך לבטל את הנעילה של המכשיר כדי להמשיך"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ja/strings.xml b/packages/SystemUI/res-keyguard/values-ja/strings.xml
index afe0159..1a4fb0b 100644
--- a/packages/SystemUI/res-keyguard/values-ja/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ja/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"デバイスの再起動後はパターンの入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"デバイスの再起動後は PIN の入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"デバイスの再起動後はパスワードの入力が必要となります"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"追加の確認のためパターンが必要です"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"追加の確認のため PIN が必要です"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"追加の確認のためパスワードが必要です"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"セキュリティを強化するには代わりにパターンを使用してください"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"セキュリティを強化するには代わりに PIN を使用してください"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"セキュリティを強化するには代わりにパスワードを使用してください"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"デバイスは管理者によりロックされています"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"デバイスは手動でロックされました"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"認識されませんでした"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"デフォルト"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"バブル"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"アナログ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"続行するにはデバイスのロックを解除してください"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ka/strings.xml b/packages/SystemUI/res-keyguard/values-ka/strings.xml
index b32caa7..b56042a0 100644
--- a/packages/SystemUI/res-keyguard/values-ka/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ka/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა ნიმუშის დახატვა"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა PIN-კოდის შეყვანა"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა პაროლის შეყვანა"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"დამატებითი უსაფრთხოებისთვის საჭიროა ნიმუშის დახატვა"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"დამატებითი უსაფრთხოებისთვის საჭიროა PIN-კოდის შეყვანა"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"დამატებითი უსაფრთხოებისთვის საჭიროა პაროლის შეყვანა"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ ნიმუში"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"დამატებითი უსაფრთხოებისთვის გამოიყენეთ პაროლი"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"მოწყობილობა ჩაკეტილია ადმინისტრატორის მიერ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"მოწყობილობა ხელით ჩაიკეტა"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"არ არის ამოცნობილი"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ნაგულისხმევი"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ბუშტი"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ანალოგური"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"გასაგრძელებლად განბლოკეთ თქვენი მოწყობილობა"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kk/strings.xml b/packages/SystemUI/res-keyguard/values-kk/strings.xml
index d6d5bcd..a4024de 100644
--- a/packages/SystemUI/res-keyguard/values-kk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kk/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Құрылғы қайта іске қосылғаннан кейін, өрнекті енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Құрылғы қайта іске қосылғаннан кейін, PIN кодын енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Құрылғы қайта іске қосылғаннан кейін, құпия сөзді енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Қауіпсіздікті күшейту үшін өрнекті енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Қауіпсіздікті күшейту үшін PIN кодын енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Қауіпсіздікті күшейту үшін құпия сөзді енгізу қажет"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Қосымша қауіпсіздік үшін өрнекті пайдаланыңыз."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Қосымша қауіпсіздік үшін PIN кодын пайдаланыңыз."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Қосымша қауіпсіздік үшін құпия сөзді пайдаланыңыз."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Құрылғыны әкімші құлыптаған"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Құрылғы қолмен құлыпталды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Танылмады"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Әдепкі"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көпіршік"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогтық"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Жалғастыру үшін құрылғының құлпын ашыңыз"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-km/strings.xml b/packages/SystemUI/res-keyguard/values-km/strings.xml
index 00bfe05..329912ab 100644
--- a/packages/SystemUI/res-keyguard/values-km/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-km/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"តម្រូវឲ្យប្រើលំនាំ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"តម្រូវឲ្យបញ្ចូលកូដ PIN បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"តម្រូវឲ្យប្រើលំនាំ ដើម្បីទទួលបានសវុត្ថិភាពបន្ថែម"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"តម្រូវឲ្យបញ្ចូលកូដ PIN ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើលំនាំជំនួសវិញ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើកូដ PIN ជំនួសវិញ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ដើម្បីសុវត្ថិភាពបន្ថែម សូមប្រើពាក្យសម្ងាត់ជំនួសវិញ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ឧបករណ៍​ត្រូវបាន​ចាក់សោ​ដោយអ្នក​គ្រប់គ្រង"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ឧបករណ៍ត្រូវបានចាក់សោដោយអ្នកប្រើផ្ទាល់"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"មិនអាចសម្គាល់បានទេ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"លំនាំដើម"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ពពុះ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"អាណាឡូក"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ដោះសោឧបករណ៍របស់អ្នកដើម្បីបន្ត"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kn/strings.xml b/packages/SystemUI/res-keyguard/values-kn/strings.xml
index 80a98e6..d42d08d 100644
--- a/packages/SystemUI/res-keyguard/values-kn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kn/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಿನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿದೆ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪಿನ್ ಅಗತ್ಯವಿದೆ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿದೆ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪ್ಯಾಟರ್ನ್ ಅನ್ನು ಬಳಸಿ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪಿನ್ ಬಳಸಿ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ, ಬದಲಿಗೆ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬಳಸಿ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ನಿರ್ವಾಹಕರು ಸಾಧನವನ್ನು ಲಾಕ್ ಮಾಡಿದ್ದಾರೆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ಸಾಧನವನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಲಾಕ್‌ ಮಾಡಲಾಗಿದೆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ಡೀಫಾಲ್ಟ್"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ಬಬಲ್"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ಅನಲಾಗ್"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ಮುಂದುವರಿಸಲು, ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅನ್‌ಲಾಕ್ ಮಾಡಿ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ko/strings.xml b/packages/SystemUI/res-keyguard/values-ko/strings.xml
index b952f1b..e916fee 100644
--- a/packages/SystemUI/res-keyguard/values-ko/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ko/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"기기가 다시 시작되면 패턴이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"기기가 다시 시작되면 PIN이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"기기가 다시 시작되면 비밀번호가 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"보안 강화를 위해 패턴이 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"보안 강화를 위해 PIN이 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"보안 강화를 위해 비밀번호가 필요합니다."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"보안 강화를 위해 대신 패턴 사용"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"보안 강화를 위해 대신 PIN 사용"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"보안 강화를 위해 대신 비밀번호 사용"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"관리자가 기기를 잠갔습니다."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"기기가 수동으로 잠금 설정되었습니다."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"인식할 수 없음"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"기본"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"버블"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"아날로그"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"기기를 잠금 해제하여 계속"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ky/strings.xml b/packages/SystemUI/res-keyguard/values-ky/strings.xml
index 485337d..88abd1e 100644
--- a/packages/SystemUI/res-keyguard/values-ky/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ky/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Түзмөк кайра күйгүзүлгөндөн кийин графикалык ачкычты тартуу талап кылынат"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Түзмөк кайра күйгүзүлгөндөн кийин PIN-кодду киргизүү талап кылынат"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Түзмөк кайра күйгүзүлгөндөн кийин сырсөздү киргизүү талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Коопсуздукту бекемдөө үчүн графикалык ачкыч талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Коопсуздукту бекемдөө үчүн PIN-код талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Коопсуздукту бекемдөө үчүн сырсөз талап кылынат"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Кошумча коопсуздук үчүн анын ордуна графикалык ачкычты колдонуңуз"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Кошумча коопсуздук үчүн анын ордуна PIN кодду колдонуңуз"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Кошумча коопсуздук үчүн анын ордуна сырсөздү колдонуңуз"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Түзмөктү администратор кулпулап койгон"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Түзмөк кол менен кулпуланды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таанылган жок"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Демейки"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көбүк"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналог"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Улантуу үчүн түзмөгүңүздүн кулпусун ачыңыз"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml
index a4e7a5f..f1aa544 100644
--- a/packages/SystemUI/res-keyguard/values-land/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml
@@ -27,4 +27,6 @@
     <integer name="scaled_password_text_size">26</integer>
 
     <dimen name="bouncer_user_switcher_y_trans">@dimen/status_bar_height</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lo/strings.xml b/packages/SystemUI/res-keyguard/values-lo/strings.xml
index 17584b5..5001c30 100644
--- a/packages/SystemUI/res-keyguard/values-lo/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lo/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ຈຳເປັນຕ້ອງມີແບບຮູບປົດລັອກຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ຈຳເປັນຕ້ອງມີ PIN ຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ຈຳເປັນຕ້ອງມີແບບຮູບເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ຈຳເປັນຕ້ອງມີ PIN ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ຮູບແບບແທນ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ PIN ແທນ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ, ໃຫ້ໃຊ້ລະຫັດຜ່ານແທນ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ອຸປະກອນຖືກລັອກໂດຍຜູ້ເບິ່ງແຍງລະບົບ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ອຸປະກອນຖືກສັ່ງໃຫ້ລັອກ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ບໍ່ຮູ້ຈັກ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ຄ່າເລີ່ມຕົ້ນ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ຟອງ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ໂມງເຂັມ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ປົດລັອກອຸປະກອນຂອງທ່ານເພື່ອສືບຕໍ່"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lt/strings.xml b/packages/SystemUI/res-keyguard/values-lt/strings.xml
index a066a66..20f6ad2 100644
--- a/packages/SystemUI/res-keyguard/values-lt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lt/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iš naujo paleidus įrenginį būtinas atrakinimo piešinys"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iš naujo paleidus įrenginį būtinas PIN kodas"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iš naujo paleidus įrenginį būtinas slaptažodis"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Norint užtikrinti papildomą saugą būtinas atrakinimo piešinys"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Norint užtikrinti papildomą saugą būtinas PIN kodas"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Norint užtikrinti papildomą saugą būtinas slaptažodis"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Papildomai saugai užtikrinti geriau naudokite atrakinimo piešinį"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Papildomai saugai užtikrinti geriau naudokite PIN kodą"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Papildomai saugai užtikrinti geriau naudokite slaptažodį"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Įrenginį užrakino administratorius"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Įrenginys užrakintas neautomatiškai"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Neatpažinta"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Numatytasis"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Debesėlis"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginis"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Įrenginio atrakinimas norint tęsti"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lv/strings.xml b/packages/SystemUI/res-keyguard/values-lv/strings.xml
index d371a4b..7012c16 100644
--- a/packages/SystemUI/res-keyguard/values-lv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lv/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pēc ierīces restartēšanas ir jāievada atbloķēšanas kombinācija."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pēc ierīces restartēšanas ir jāievada PIN kods."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pēc ierīces restartēšanas ir jāievada parole."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Papildu drošībai ir jāievada atbloķēšanas kombinācija."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Papildu drošībai ir jāievada PIN kods."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Papildu drošībai ir jāievada parole."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Papildu drošībai izmantojiet kombināciju"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Papildu drošībai izmantojiet PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Papildu drošībai izmantojiet paroli"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrators bloķēja ierīci."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Ierīce tika bloķēta manuāli."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nav atpazīts"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Noklusējums"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuļi"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogais"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lai turpinātu, atbloķējiet ierīci"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mk/strings.xml b/packages/SystemUI/res-keyguard/values-mk/strings.xml
index ef22564..77e1b50 100644
--- a/packages/SystemUI/res-keyguard/values-mk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mk/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Потребна е шема по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Потребен е PIN-код по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Потребна е лозинка по рестартирање на уредот"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Потребна е шема за дополнителна безбедност"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Потребен е PIN-код за дополнителна безбедност"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Потребна е лозинка за дополнителна безбедност"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За дополнителна безбедност, користете шема"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За дополнителна безбедност, користете PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За дополнителна безбедност, користете лозинка"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Уредот е заклучен од администраторот"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уредот е заклучен рачно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Непознат"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандарден"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонче"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоген"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отклучете го уредот за да продолжите"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ml/strings.xml b/packages/SystemUI/res-keyguard/values-ml/strings.xml
index 63a542a..7919773 100644
--- a/packages/SystemUI/res-keyguard/values-ml/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ml/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാറ്റേൺ വരയ്‌ക്കേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പിൻ നൽകേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാസ്‌വേഡ് നൽകേണ്ടതുണ്ട്"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"അധിക സുരക്ഷയ്ക്ക് പാറ്റേൺ ആവശ്യമാണ്"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"അധിക സുരക്ഷയ്ക്ക് പിൻ ആവശ്യമാണ്"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"അധിക സുരക്ഷയ്ക്ക് പാസ്‌വേഡ് ആവശ്യമാണ്"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"അധിക സുരക്ഷയ്ക്കായി, പകരം പാറ്റേൺ ഉപയോഗിക്കുക"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"അധിക സുരക്ഷയ്ക്കായി, പകരം പിൻ ഉപയോഗിക്കുക"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"അധിക സുരക്ഷയ്ക്കായി, പകരം പാസ്‍വേഡ് ഉപയോഗിക്കുക"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ഉപകരണം അഡ്‌മിൻ ലോക്കുചെയ്തു"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ഉപകരണം നേരിട്ട് ലോക്കുചെയ്തു"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"തിരിച്ചറിയുന്നില്ല"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ഡിഫോൾട്ട്"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ബബിൾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"അനലോഗ്"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"തുടരാൻ നിങ്ങളുടെ ഉപകരണം അൺലോക്ക് ചെയ്യുക"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mn/strings.xml b/packages/SystemUI/res-keyguard/values-mn/strings.xml
index 71c913f..f2cc5ab 100644
--- a/packages/SystemUI/res-keyguard/values-mn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mn/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Төхөөрөмжийг дахин эхлүүлсний дараа загвар оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Төхөөрөмжийг дахин эхлүүлсний дараа ПИН оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Төхөөрөмжийг дахин эхлүүлсний дараа нууц үг оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Аюулгүй байдлын үүднээс загвар оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Аюулгүй байдлын үүднээс ПИН оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Аюулгүй байдлын үүднээс нууц үг оруулах шаардлагатай"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Нэмэлт аюулгүй байдлын үүднээс оронд нь хээ ашиглана уу"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Нэмэлт аюулгүй байдлын үүднээс оронд нь ПИН ашиглана уу"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Нэмэлт аюулгүй байдлын үүднээс оронд нь нууц үг ашиглана уу"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Админ төхөөрөмжийг түгжсэн"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Төхөөрөмжийг гараар түгжсэн"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таньж чадсангүй"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Өгөгдмөл"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бөмбөлөг"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Aналог"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Үргэлжлүүлэхийн тулд төхөөрөмжийнхөө түгжээг тайлна уу"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mr/strings.xml b/packages/SystemUI/res-keyguard/values-mr/strings.xml
index 6ac13bd..580b547a 100644
--- a/packages/SystemUI/res-keyguard/values-mr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mr/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिव्हाइस रीस्टार्ट झाल्यावर पॅटर्न आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिव्हाइस रीस्टार्ट झाल्यावर पिन आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिव्हाइस रीस्टार्ट झाल्यावर पासवर्ड आवश्यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षिततेसाठी पॅटर्न आवश्‍यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षिततेसाठी पिन आवश्‍यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षिततेसाठी पासवर्ड आवश्‍यक आहे"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पॅटर्न वापरा"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पिन वापरा"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"अतिरिक्त सुरक्षेसाठी, त्याऐवजी पासवर्ड वापरा"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकाद्वारे लॉक केलेले डिव्हाइस"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिव्हाइस मॅन्युअली लॉक केले होते"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ओळखले नाही"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डीफॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"अ‍ॅनालॉग"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"पुढे सुरू ठेवण्यासाठी तुमचे डिव्हाइस अनलॉक करा"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ms/strings.xml b/packages/SystemUI/res-keyguard/values-ms/strings.xml
index 453afc3..c179dcb 100644
--- a/packages/SystemUI/res-keyguard/values-ms/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ms/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Corak diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kata laluan diperlukan setelah peranti dimulakan semula"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Corak diperlukan untuk keselamatan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keselamatan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kata laluan diperlukan untuk keselamatan tambahan"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Untuk keselamatan tambahan, gunakan corak"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Untuk keselamatan tambahan, gunakan PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Untuk keselamatan tambahan, gunakan kata laluan"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Peranti dikunci oleh pentadbir"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Peranti telah dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Lalai"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Gelembung"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Buka kunci peranti anda untuk meneruskan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-my/strings.xml b/packages/SystemUI/res-keyguard/values-my/strings.xml
index 1cc46b1..7c69bdd 100644
--- a/packages/SystemUI/res-keyguard/values-my/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-my/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပုံစံ လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် စကားဝှက် လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပုံစံ လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် စကားဝှက် လိုအပ်ပါသည်"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား ပုံစံသုံးပါ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား ပင်နံပါတ်သုံးပါ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ထပ်ဆောင်းလုံခြုံရေးအတွက် ၎င်းအစား စကားဝှက်သုံးပါ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"စက်ပစ္စည်းကို စီမံခန့်ခွဲသူက လော့ခ်ချထားပါသည်"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"စက်ပစ္စည်းကို ကိုယ်တိုင်ကိုယ်ကျ လော့ခ်ချထားခဲ့သည်"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"မသိ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"မူလ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ပူဖောင်းကွက်"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ရိုးရိုး"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ရှေ့ဆက်ရန် သင့်စက်ကိုဖွင့်ပါ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nb/strings.xml b/packages/SystemUI/res-keyguard/values-nb/strings.xml
index 5310a730..e394d1f 100644
--- a/packages/SystemUI/res-keyguard/values-nb/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nb/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du må tegne mønsteret etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du må skrive inn PIN-koden etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du må skrive inn passordet etter at enheten har startet på nytt"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du må tegne mønsteret for ekstra sikkerhet"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du må skrive inn PIN-koden for ekstra sikkerhet"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du må skrive inn passordet for ekstra sikkerhet"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Bruk mønster i stedet, for å øke sikkerheten"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Bruk PIN-kode i stedet, for å øke sikkerheten"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Bruk passord i stedet, for å øke sikkerheten"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheten er låst av administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten ble låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke gjenkjent"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås opp enheten for å fortsette"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ne/strings.xml b/packages/SystemUI/res-keyguard/values-ne/strings.xml
index 534164b..9f329e9 100644
--- a/packages/SystemUI/res-keyguard/values-ne/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ne/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"यन्त्र पुनः सुरु भएपछि ढाँचा आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"यन्त्र पुनः सुरु भएपछि PIN आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"यन्त्र पुनः सुरु भएपछि पासवर्ड आवश्यक पर्दछ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षाको लागि ढाँचा आवश्यक छ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षाको लागि PIN आवश्यक छ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षाको लागि पासवर्ड आवश्यक छ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो प्याटर्न प्रयोग गर्नुहोस्"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो पिन प्रयोग गर्नुहोस्"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"अतिरिक्त सुरक्षाका लागि यो प्रमाणीकरण विधिको साटो पासवर्ड प्रयोग गर्नुहोस्"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकले यन्त्रलाई लक गर्नुभएको छ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"यन्त्रलाई म्यानुअल तरिकाले लक गरिएको थियो"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहिचान भएन"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डिफल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालग"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"आफ्नो डिभाइस अनलक गरी जारी राख्नुहोस्"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nl/strings.xml b/packages/SystemUI/res-keyguard/values-nl/strings.xml
index 08e226d4..579824a 100644
--- a/packages/SystemUI/res-keyguard/values-nl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pincode vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wachtwoord vereist nadat het apparaat opnieuw is opgestart"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon vereist voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pincode vereist voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wachtwoord vereist voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Gebruik in plaats daarvan het patroon voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Gebruik in plaats daarvan de pincode voor extra beveiliging"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Gebruik in plaats daarvan het wachtwoord voor extra beveiliging"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Apparaat vergrendeld door beheerder"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Apparaat is handmatig vergrendeld"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Niet herkend"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standaard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ontgrendel je apparaat om door te gaan"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-or/strings.xml b/packages/SystemUI/res-keyguard/values-or/strings.xml
index 3cdd264..75f7a89 100644
--- a/packages/SystemUI/res-keyguard/values-or/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-or/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାଟର୍ନ ଆବଶ୍ୟକ ଅଟେ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବାପରେ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାସୱର୍ଡ ଆବଶ୍ୟକ ଅଟେ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାଟର୍ନ ଆବଶ୍ୟକ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ PIN ଆବଶ୍ୟକ ଅଟେ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାଟର୍ନ ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ PIN ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାସୱାର୍ଡ ବ୍ୟବହାର କରନ୍ତୁ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ଡିଭାଇସ୍‍ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଲକ୍‍ କରାଯାଇଛି"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ଡିଭାଇସ୍‍ ମାନୁଆଲ ଭାବେ ଲକ୍‍ କରାଗଲା"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ଚିହ୍ନଟ ହେଲାନାହିଁ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ଡିଫଲ୍ଟ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ବବଲ୍"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ଆନାଲଗ୍"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ଜାରି ରଖିବା ପାଇଁ ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରନ୍ତୁ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pa/strings.xml b/packages/SystemUI/res-keyguard/values-pa/strings.xml
index 409f727..5c3fff7 100644
--- a/packages/SystemUI/res-keyguard/values-pa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pa/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪੈਟਰਨ ਵਰਤੋ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪਿੰਨ ਵਰਤੋ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ, ਇਸਦੀ ਬਜਾਏ ਪਾਸਵਰਡ ਵਰਤੋ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਡੀਵਾਈਸ ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ਡੀਵਾਈਸ ਨੂੰ ਹੱਥੀਂ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ਬੁਲਬੁਲਾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ਐਨਾਲੌਗ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ਜਾਰੀ ਰੱਖਣ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅਣਲਾਕ ਕਰੋ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pl/strings.xml b/packages/SystemUI/res-keyguard/values-pl/strings.xml
index 52bc982..3736386 100644
--- a/packages/SystemUI/res-keyguard/values-pl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po ponownym uruchomieniu urządzenia wymagany jest wzór"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po ponownym uruchomieniu urządzenia wymagany jest kod PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po ponownym uruchomieniu urządzenia wymagane jest hasło"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Dla większego bezpieczeństwa musisz narysować wzór"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Dla większego bezpieczeństwa musisz podać kod PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Dla większego bezpieczeństwa musisz podać hasło"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ze względów bezpieczeństwa użyj wzoru"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ze względów bezpieczeństwa użyj kodu PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ze względów bezpieczeństwa użyj hasła"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Urządzenie zablokowane przez administratora"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Urządzenie zostało zablokowane ręcznie"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie rozpoznano"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Domyślna"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bąbelkowy"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogowy"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Odblokuj urządzenie, aby kontynuować"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
index b934826..3d60e8c 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
index a67bfb0..0a94349 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necessário um padrão após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necessário um PIN após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necessária uma palavra-passe após reiniciar o dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Para segurança adicional, é necessário um padrão"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Para segurança adicional, é necessário um PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Para segurança adicional, é necessária uma palavra-passe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para uma segurança adicional, use antes o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para uma segurança adicional, use antes o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para uma segurança adicional, use antes a palavra-passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo gestor"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido."</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinido"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balão"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt/strings.xml b/packages/SystemUI/res-keyguard/values-pt/strings.xml
index b934826..3d60e8c 100644
--- a/packages/SystemUI/res-keyguard/values-pt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ro/strings.xml b/packages/SystemUI/res-keyguard/values-ro/strings.xml
index 5ee67d91..67ae0fc 100644
--- a/packages/SystemUI/res-keyguard/values-ro/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ro/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Modelul este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Codul PIN este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Parola este necesară după repornirea dispozitivului"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Modelul este necesar pentru securitate suplimentară"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Codul PIN este necesar pentru securitate suplimentară"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Parola este necesară pentru securitate suplimentară"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pentru mai multă securitate, folosește modelul"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pentru mai multă securitate, folosește codul PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pentru mai multă securitate, folosește parola"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispozitiv blocat de administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Dispozitivul a fost blocat manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nu este recunoscut"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Prestabilit"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogic"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Deblochează dispozitivul pentru a continua"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ru/strings.xml b/packages/SystemUI/res-keyguard/values-ru/strings.xml
index 2b8f8d6..f1945ad 100644
--- a/packages/SystemUI/res-keyguard/values-ru/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ru/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"После перезагрузки устройства необходимо ввести графический ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"После перезагрузки устройства необходимо ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"После перезагрузки устройства необходимо ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"В качестве дополнительной меры безопасности введите графический ключ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"В качестве дополнительной меры безопасности введите PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"В качестве дополнительной меры безопасности введите пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"В целях дополнительной безопасности используйте графический ключ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"В целях дополнительной безопасности используйте PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"В целях дополнительной безопасности используйте пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройство заблокировано администратором"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройство было заблокировано вручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распознано"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"По умолчанию"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Пузырь"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Стрелки"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Чтобы продолжить, разблокируйте устройство"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-si/strings.xml b/packages/SystemUI/res-keyguard/values-si/strings.xml
index 4e911de..82df4cb 100644
--- a/packages/SystemUI/res-keyguard/values-si/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-si/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"උපාංගය නැවත ආරම්භ වූ පසු රටාව අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"උපාංගය නැවත ආරම්භ වූ පසු PIN අංකය අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"උපාංගය නැවත ආරම්භ වූ පසු මුරපදය අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"අමතර ආරක්ෂාව සඳහා රටාව අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"අමතර ආරක්ෂාව සඳහා PIN අංකය අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"අමතර ආරක්ෂාව සඳහා මුරපදය අවශ්‍යයි"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට රටාව භාවිතා කරන්න"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට PIN භාවිතා කරන්න"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"අතිරේක ආරක්ෂාව සඳහා, ඒ වෙනුවට මුරපදය භාවිතා කරන්න"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ඔබගේ පරිපාලක විසින් උපාංගය අගුළු දමා ඇත"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"උපාංගය හස්තීයව අගුලු දමන ලදී"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"හඳුනා නොගන්නා ලදී"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"පෙරනිමි"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"බුබුළ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ප්‍රතිසමය"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ඉදිරියට යාමට ඔබේ උපාංගය අගුළු හරින්න"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sk/strings.xml b/packages/SystemUI/res-keyguard/values-sk/strings.xml
index f2d68e3..2d8b3b1 100644
--- a/packages/SystemUI/res-keyguard/values-sk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sk/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po reštartovaní zariadenia musíte zadať bezpečnostný vzor"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po reštartovaní zariadenia musíte zadať kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po reštartovaní zariadenia musíte zadať heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Na ďalšie zabezpečenie musíte zadať bezpečnostný vzor"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Na ďalšie zabezpečenie musíte zadať kód PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Na ďalšie zabezpečenie musíte zadať heslo"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"V rámci zvýšenia zabezpečenia použite radšej vzor"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"V rámci zvýšenia zabezpečenia použite radšej PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"V rámci zvýšenia zabezpečenia použite radšej heslo"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zariadenie zamkol správca"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zariadenie bolo uzamknuté ručne"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznané"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predvolený"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógový"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Ak chcete pokračovať, odomknite zariadenie"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sl/strings.xml b/packages/SystemUI/res-keyguard/values-sl/strings.xml
index 772308f..4c4ea06 100644
--- a/packages/SystemUI/res-keyguard/values-sl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po vnovičnem zagonu naprave je treba vnesti vzorec"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po vnovičnem zagonu naprave je treba vnesti kodo PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po vnovičnem zagonu naprave je treba vnesti geslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zaradi dodatne varnosti morate vnesti vzorec"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zaradi dodatne varnosti morate vnesti kodo PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zaradi dodatne varnosti morate vnesti geslo"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Za dodatno varnost raje uporabite vzorec."</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Za dodatno varnost raje uporabite kodo PIN."</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Za dodatno varnost raje uporabite geslo."</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Napravo je zaklenil skrbnik"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Naprava je bila ročno zaklenjena"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ni prepoznano"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Privzeto"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurček"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogno"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Za nadaljevanje odklenite napravo"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sq/strings.xml b/packages/SystemUI/res-keyguard/values-sq/strings.xml
index c758462..78e217d 100644
--- a/packages/SystemUI/res-keyguard/values-sq/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sq/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kërkohet motivi pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kërkohet kodi PIN pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kërkohet fjalëkalimi pas rinisjes së pajisjes"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kërkohet motivi për më shumë siguri"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kërkohet kodi PIN për më shumë siguri"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kërkohet fjalëkalimi për më shumë siguri"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Për më shumë siguri, përdor motivin më mirë"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Për më shumë siguri, përdor kodin PIN më mirë"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Për më shumë siguri, përdor fjalëkalimin më mirë"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Pajisja është e kyçur nga administratori"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Pajisja është kyçur manualisht"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nuk njihet"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"E parazgjedhur"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Flluskë"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoge"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Shkyç pajisjen tënde për të vazhduar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sr/strings.xml b/packages/SystemUI/res-keyguard/values-sr/strings.xml
index e6fe853..80d8755 100644
--- a/packages/SystemUI/res-keyguard/values-sr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sr/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Треба да унесете шаблон када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Треба да унесете PIN када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Треба да унесете лозинку када се уређај поново покрене"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Треба да унесете шаблон ради додатне безбедности"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Треба да унесете PIN ради додатне безбедности"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Треба да унесете лозинку ради додатне безбедности"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За додатну безбедност користите шаблон"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За додатну безбедност користите PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За додатну безбедност користите лозинку"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Администратор је закључао уређај"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уређај је ручно закључан"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Није препознат"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Подразумевани"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Мехурићи"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогни"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Откључајте уређај да бисте наставили"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sv/strings.xml b/packages/SystemUI/res-keyguard/values-sv/strings.xml
index fa241d9..b5548b9 100644
--- a/packages/SystemUI/res-keyguard/values-sv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sv/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du måste rita mönster när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du måste ange pinkod när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du måste ange lösenord när du har startat om enheten"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du måste rita mönster för ytterligare säkerhet"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du måste ange pinkod för ytterligare säkerhet"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du måste ange lösenord för ytterligare säkerhet"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"För ytterligare säkerhet använder du mönstret i stället"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"För ytterligare säkerhet använder du pinkoden i stället"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"För ytterligare säkerhet använder du lösenordet i stället"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratören har låst enheten"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten har låsts manuellt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Identifierades inte"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubbla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Lås upp enheten för att fortsätta"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw/strings.xml b/packages/SystemUI/res-keyguard/values-sw/strings.xml
index 791bceb..02af18e 100644
--- a/packages/SystemUI/res-keyguard/values-sw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sw/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Unafaa kuchora mchoro baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Unafaa kuweka PIN baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Unafaa kuweka nenosiri baada ya kuwasha kifaa upya"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mchoro unahitajika ili kuongeza usalama"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN inahitajika ili kuongeza usalama"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Nenosiri linahitajika ili kuongeza usalama."</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Kwa usalama wa ziada, tumia mchoro badala yake"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Kwa usalama wa ziada, tumia PIN badala yake"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Kwa usalama wa ziada, tumia nenosiri badala yake"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Msimamizi amefunga kifaa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Umefunga kifaa mwenyewe"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Haitambuliwi"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Chaguomsingi"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kiputo"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogi"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Fungua kifaa chako ili uendelee"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ta/strings.xml b/packages/SystemUI/res-keyguard/values-ta/strings.xml
index 271657d..0d32d46 100644
--- a/packages/SystemUI/res-keyguard/values-ta/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ta/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"சாதனத்தை மீண்டும் தொடங்கியதும், பேட்டர்னை வரைய வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"சாதனத்தை மீண்டும் தொடங்கியதும், பின்னை உள்ளிட வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"சாதனத்தை மீண்டும் தொடங்கியதும், கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"கூடுதல் பாதுகாப்பிற்கு, பேட்டர்னை வரைய வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"கூடுதல் பாதுகாப்பிற்கு, பின்னை உள்ளிட வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"கூடுதல் பாதுகாப்பிற்கு, கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"கூடுதல் பாதுகாப்பிற்குப் பேட்டர்னைப் பயன்படுத்தவும்"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"கூடுதல் பாதுகாப்பிற்குப் பின்னை (PIN) பயன்படுத்தவும்"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"கூடுதல் பாதுகாப்பிற்குக் கடவுச்சொல்லைப் பயன்படுத்தவும்"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"நிர்வாகி சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"பயனர் சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"அடையாளங்காணபடவில்லை"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"இயல்பு"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"பபிள்"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"அனலாக்"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"தொடர, சாதனத்தை அன்லாக் செய்யுங்கள்"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-te/strings.xml b/packages/SystemUI/res-keyguard/values-te/strings.xml
index f62e667..f519daf 100644
--- a/packages/SystemUI/res-keyguard/values-te/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-te/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత నమూనాను గీయాలి"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"డివైజ్‌ను పునఃప్రారంభించిన తర్వాత పిన్ నమోదు చేయాలి"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత పాస్‌వర్డ్‌ను నమోదు చేయాలి"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"అదనపు సెక్యూరిటీ కోసం ఆకృతి అవసరం"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"అదనపు సెక్యూరిటీ కోసం పిన్ ఎంటర్ చేయాలి"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"అదనపు సెక్యూరిటీ కోసం పాస్‌వర్డ్‌ను ఎంటర్ చేయాలి"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"అదనపు సెక్యూరిటీ కోసం, బదులుగా ఆకృతిని ఉపయోగించండి"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"అదనపు సెక్యూరిటీ కోసం, బదులుగా PINను ఉపయోగించండి"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"అదనపు సెక్యూరిటీ కోసం, బదులుగా పాస్‌వర్డ్‌ను ఉపయోగించండి"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"పరికరం నిర్వాహకుల ద్వారా లాక్ చేయబడింది"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"పరికరం మాన్యువల్‌గా లాక్ చేయబడింది"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"గుర్తించలేదు"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ఆటోమేటిక్"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"బబుల్"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ఎనలాగ్"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"కొనసాగించడానికి మీ పరికరాన్ని అన్‌లాక్ చేయండి"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-th/strings.xml b/packages/SystemUI/res-keyguard/values-th/strings.xml
index 62a83bc..14a65a07 100644
--- a/packages/SystemUI/res-keyguard/values-th/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-th/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ต้องวาดรูปแบบหลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ต้องระบุ PIN หลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ต้องป้อนรหัสผ่านหลังจากอุปกรณ์รีสตาร์ท"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ต้องวาดรูปแบบเพื่อความปลอดภัยเพิ่มเติม"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ต้องระบุ PIN เพื่อความปลอดภัยเพิ่มเติม"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ต้องป้อนรหัสผ่านเพื่อความปลอดภัยเพิ่มเติม"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ใช้รูปแบบแทนเพื่อเพิ่มความปลอดภัย"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ใช้ PIN แทนเพื่อเพิ่มความปลอดภัย"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ใช้รหัสผ่านแทนเพื่อเพิ่มความปลอดภัย"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ผู้ดูแลระบบล็อกอุปกรณ์"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"มีการล็อกอุปกรณ์ด้วยตัวเอง"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ไม่รู้จัก"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ค่าเริ่มต้น"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"บับเบิล"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"แอนะล็อก"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ปลดล็อกอุปกรณ์ของคุณเพื่อดำเนินการต่อ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tl/strings.xml b/packages/SystemUI/res-keyguard/values-tl/strings.xml
index 524ea47..7936058 100644
--- a/packages/SystemUI/res-keyguard/values-tl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kailangan ng pattern pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kailangan ng PIN pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kailangan ng password pagkatapos mag-restart ng device"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kinakailangan ang pattern para sa karagdagang seguridad"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kinakailangan ang PIN para sa karagdagang seguridad"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kinakailangan ang password para sa karagdagang seguridad"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para sa karagdagang seguridad, gumamit na lang ng pattern"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para sa karagdagang seguridad, gumamit na lang ng PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para sa karagdagang seguridad, gumamit na lang ng password"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Na-lock ng admin ang device"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Manual na na-lock ang device"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Hindi nakilala"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"I-unlock ang iyong device para magpatuloy"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tr/strings.xml b/packages/SystemUI/res-keyguard/values-tr/strings.xml
index 54aaae3..80dae8c 100644
--- a/packages/SystemUI/res-keyguard/values-tr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tr/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yeniden başladıktan sonra desen gerekir"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıktan sonra PIN gerekir"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıktan sonra şifre gerekir"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Ek güvenlik için desen gerekir"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Ek güvenlik için PIN gerekir"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Ek güvenlik için şifre gerekir"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ek güvenlik için bunun yerine desen kullanın"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ek güvenlik için bunun yerine PIN kullanın"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ek güvenlik için bunun yerine şifre kullanın"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz, yönetici tarafından kilitlendi"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihazın manuel olarak kilitlendi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmadı"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Varsayılan"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Baloncuk"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Devam etmek için cihazınızın kilidini açın"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uk/strings.xml b/packages/SystemUI/res-keyguard/values-uk/strings.xml
index 6144c1c..ff594ae 100644
--- a/packages/SystemUI/res-keyguard/values-uk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uk/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Після перезавантаження пристрою потрібно ввести ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Після перезавантаження пристрою потрібно ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Після перезавантаження пристрою потрібно ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для додаткового захисту потрібно ввести ключ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для додаткового захисту потрібно ввести PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для додаткового захисту потрібно ввести пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"З міркувань додаткової безпеки скористайтеся ключем"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"З міркувань додаткової безпеки скористайтеся PIN-кодом"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"З міркувань додаткової безпеки скористайтеся паролем"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Адміністратор заблокував пристрій"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Пристрій заблоковано вручну"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не розпізнано"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"За умовчанням"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бульбашковий"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоговий"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Розблокуйте пристрій, щоб продовжити"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ur/strings.xml b/packages/SystemUI/res-keyguard/values-ur/strings.xml
index 4e77841..9308260 100644
--- a/packages/SystemUI/res-keyguard/values-ur/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ur/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"آلہ دوبارہ چالو ہونے کے بعد پیٹرن درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‏آلہ دوبارہ چالو ہونے کے بعد PIN درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"آلہ دوبارہ چالو ہونے کے بعد پاس ورڈ درکار ہوتا ہے"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"اضافی سیکیورٹی کیلئے پیٹرن درکار ہے"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‏اضافی سیکیورٹی کیلئے PIN درکار ہے"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"اضافی سیکیورٹی کیلئے پاس ورڈ درکار ہے"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"اضافی سیکیورٹی کے لئے، اس کے بجائے پیٹرن استعمال کریں"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"‏اضافی سیکیورٹی کے لئے، اس کے بجائے PIN استعمال کریں"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"اضافی سیکیورٹی کے لئے، اس کے بجائے پاس ورڈ استعمال کریں"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"آلہ منتظم کی جانب سے مقفل ہے"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"آلہ کو دستی طور پر مقفل کیا گیا تھا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"تسلیم شدہ نہیں ہے"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ڈیفالٹ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"بلبلہ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"اینالاگ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"جاری رکھنے کے لئے اپنا آلہ غیر مقفل کریں"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uz/strings.xml b/packages/SystemUI/res-keyguard/values-uz/strings.xml
index afaf746..2cc9724 100644
--- a/packages/SystemUI/res-keyguard/values-uz/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uz/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Qurilma qayta ishga tushganidan keyin grafik kalitni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Qurilma qayta ishga tushganidan keyin PIN kodni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Qurilma qayta ishga tushganidan keyin parolni kiritish zarur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Qo‘shimcha xavfsizlik chorasi sifatida grafik kalit talab qilinadi"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Qo‘shimcha xavfsizlik chorasi sifatida PIN kod talab qilinadi"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Qo‘shimcha xavfsizlik chorasi sifatida parol talab qilinadi"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Qoʻshimcha xavfsizlik maqsadida oʻrniga grafik kalitdan foydalaning"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Qoʻshimcha xavfsizlik maqsadida oʻrniga PIN koddan foydalaning"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Qoʻshimcha xavfsizlik maqsadida oʻrniga paroldan foydalaning"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Qurilma administrator tomonidan bloklangan"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Qurilma qo‘lda qulflangan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Aniqlanmadi"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Odatiy"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Pufaklar"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davom etish uchun qurilmangizni qulfdan chiqaring"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-vi/strings.xml b/packages/SystemUI/res-keyguard/values-vi/strings.xml
index 1d6cfa8..2771ada 100644
--- a/packages/SystemUI/res-keyguard/values-vi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-vi/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Yêu cầu hình mở khóa sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Yêu cầu mã PIN sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Yêu cầu mật khẩu sau khi thiết bị khởi động lại"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Yêu cầu hình mở khóa để bảo mật thêm"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Yêu cầu mã PIN để bảo mật thêm"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Yêu cầu mật khẩu để bảo mật thêm"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Để tăng cường bảo mật, hãy sử dụng hình mở khoá"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Để tăng cường bảo mật, hãy sử dụng mã PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Để tăng cường bảo mật, hãy sử dụng mật khẩu"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Thiết bị đã bị quản trị viên khóa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Thiết bị đã bị khóa theo cách thủ công"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Không nhận dạng được"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Mặc định"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bong bóng"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Đồng hồ kim"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Mở khoá thiết bị của bạn để tiếp tục"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
index 8c8507e..fb92838 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"重启设备后需要绘制解锁图案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"重启设备后需要输入 PIN 码"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"重启设备后需要输入密码"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"需要绘制解锁图案以进一步确保安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"需要输入 PIN 码以进一步确保安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"需要输入密码以进一步确保安全"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"为增强安全性,请改用图案"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"为增强安全性,请改用 PIN 码"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"为增强安全性,请改用密码"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理员已锁定设备"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"此设备已手动锁定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"无法识别"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"默认"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指针"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解锁设备才能继续操作"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
index c331a92..49050e5 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後,必須畫出上鎖圖案才能使用"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後,必須輸入 PIN 碼才能使用"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後,必須輸入密碼才能使用"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請務必畫出上鎖圖案,以進一步確保安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請務必輸入 PIN 碼,以進一步確保安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請務必輸入密碼,以進一步確保安全"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"為提升安全性,請改用圖案"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"為提升安全性,請改用 PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"為提升安全性,請改用密碼"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"裝置已由管理員鎖定"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"使用者已手動將裝置上鎖"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"未能識別"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指針"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解鎖裝置以繼續"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
index 1e1bec3..e5a363c 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後需要畫出解鎖圖案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後需要輸入 PIN 碼"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後需要輸入密碼"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請畫出解鎖圖案,以進一步確保資訊安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請輸入 PIN 碼,以進一步確保資訊安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請輸入密碼,以進一步確保資訊安全"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"為強化安全性,請改用解鎖圖案"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"為強化安全性,請改用 PIN 碼"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"為強化安全性,請改用密碼"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理員已鎖定裝置"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"裝置已手動鎖定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"無法識別"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"類比"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"解鎖裝置才能繼續操作"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zu/strings.xml b/packages/SystemUI/res-keyguard/values-zu/strings.xml
index c8f78ea..72ca6c0 100644
--- a/packages/SystemUI/res-keyguard/values-zu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zu/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iphethini iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iphinikhodi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iphasiwedi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kudingeka iphethini  ngokuvikeleka okungeziwe"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kudingeka iphinikhodi ngokuvikeleka okungeziwe"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Iphasiwedi idingelwa ukuvikela okungeziwe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa iphetheni esikhundleni salokho"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa i-PIN esikhundleni salokho"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Ukuze uthole ukuvikeleka okwengeziwe, sebenzisa iphasiwedi esikhundleni salokho"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Idivayisi ikhiywe ngumlawuli"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Idivayisi ikhiywe ngokwenza"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Akwaziwa"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Okuzenzekelayo"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Ibhamuza"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"I-Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Vula idivayisi yakho ukuze uqhubeke"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml
index b1d3375..a25ab51 100644
--- a/packages/SystemUI/res-keyguard/values/config.xml
+++ b/packages/SystemUI/res-keyguard/values/config.xml
@@ -28,11 +28,6 @@
     <!-- Will display the bouncer on one side of the display, and the current user icon and
          user switcher on the other side -->
     <bool name="config_enableBouncerUserSwitcher">false</bool>
-    <!-- Whether to show the face scanning animation on devices with face auth supported.
-         The face scanning animation renders in a SW layer in ScreenDecorations.
-         Enabling this will also render the camera protection in the SW layer
-         (instead of HW, if relevant)."=-->
-    <bool name="config_enableFaceScanningAnimation">true</bool>
     <!-- Time to be considered a consecutive fingerprint failure in ms -->
     <integer name="fp_consecutive_failure_time_ms">3500</integer>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 46f6ab2..3861d98 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -119,10 +119,13 @@
     <dimen name="bouncer_user_switcher_width">248dp</dimen>
     <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen>
     <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen>
+    <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen>
     <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen>
     <dimen name="bouncer_user_switcher_y_trans">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
 
     <!-- 2 * the margin + size should equal the plus_margin -->
     <dimen name="user_switcher_icon_large_margin">16dp</dimen>
diff --git a/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml
new file mode 100644
index 0000000..de83df4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml
@@ -0,0 +1,22 @@
+<!--
+  ~ 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/accessibility_floating_menu_message_background"/>
+    <corners android:radius="28dp"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml
new file mode 100644
index 0000000..eefe364
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_button_bg.xml
@@ -0,0 +1,26 @@
+<!--

+    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.

+-->

+<shape xmlns:android="http://schemas.android.com/apk/res/android"

+    android:shape="oval">

+    <solid android:color="@color/accessibility_window_magnifier_button_bg" />

+    <size

+        android:width="40dp"

+        android:height="40dp"/>

+    <corners android:radius="2dp"/>

+    <stroke

+        android:color="@color/accessibility_window_magnifier_button_bg_stroke"

+        android:width="1dp" />

+ </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
index 88d8f78f..569ee76 100644
--- a/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
+++ b/packages/SystemUI/res/drawable/brightness_progress_drawable.xml
@@ -30,7 +30,7 @@
     </item>
     <item android:id="@android:id/progress"
           android:gravity="center_vertical|fill_horizontal">
-            <com.android.systemui.util.RoundedCornerProgressDrawable
+            <com.android.systemui.util.BrightnessProgressDrawable
                 android:drawable="@drawable/brightness_progress_full_drawable"
             />
     </item>
diff --git a/core/res/res/drawable/grant_permissions_buttons_bottom.xml b/packages/SystemUI/res/drawable/grant_permissions_buttons_bottom.xml
similarity index 100%
rename from core/res/res/drawable/grant_permissions_buttons_bottom.xml
rename to packages/SystemUI/res/drawable/grant_permissions_buttons_bottom.xml
diff --git a/core/res/res/drawable/grant_permissions_buttons_top.xml b/packages/SystemUI/res/drawable/grant_permissions_buttons_top.xml
similarity index 100%
rename from core/res/res/drawable/grant_permissions_buttons_top.xml
rename to packages/SystemUI/res/drawable/grant_permissions_buttons_top.xml
diff --git a/packages/SystemUI/res/drawable/ic_doc_document.xml b/packages/SystemUI/res/drawable/ic_doc_document.xml
new file mode 100644
index 0000000..df9ddab
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_doc_document.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2022 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+            android:fillColor="#FF4883F3"
+            android:pathData="M19 3H5c-1.1 0,-2 .9,-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2,-.9 2,-2V5c0,-1.1,-.9,-2,-2,-2zm-1.99 6H7V7h10.01v2zm0 4H7v-2h10.01v2zm-3 4H7v-2h7.01v2z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml
new file mode 100644
index 0000000..d25cd65
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_left.xml
@@ -0,0 +1,33 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M0,24l0,-4l24,-0l0,4z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M0,24l0,-24l4,-0l0,24z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml
new file mode 100644
index 0000000..bb6df3a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_magnification_corner_bottom_right.xml
@@ -0,0 +1,33 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M24,24l-4,-0l-0,-24l4,-0z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M24,24l-24,-0l-0,-4l24,-0z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml
new file mode 100644
index 0000000..8f25930
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_magnification_corner_top_left.xml
@@ -0,0 +1,33 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M0,0h4v24h-4z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M0,0h24v4h-24z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml b/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml
new file mode 100644
index 0000000..291db44
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_magnification_corner_top_right.xml
@@ -0,0 +1,33 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M24,0l-0,4l-24,0l-0,-4z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+  <path
+      android:pathData="M24,0l-0,24l-4,0l-0,-24z"
+      android:strokeWidth="1"
+      android:fillColor="@color/accessibility_window_magnifier_corner_view_color"
+      android:fillType="evenOdd"
+      android:strokeColor="#00000000"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_watch.xml b/packages/SystemUI/res/drawable/ic_watch.xml
new file mode 100644
index 0000000..8ff880c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_watch.xml
@@ -0,0 +1,29 @@
+<?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
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M16,0L8,0l-0.95,5.73C5.19,7.19 4,9.45 4,12s1.19,4.81 3.05,6.27L8,24
+        h8l0.96,-5.73C18.81,16.81 20,14.54 20,12s-1.19,-4.81 -3.04,-6.27L16,0z
+        M12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml
new file mode 100644
index 0000000..8f6b4c2
--- /dev/null
+++ b/packages/SystemUI/res/drawable/internet_dialog_selected_effect.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="?android:attr/colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="@android:color/white"/>
+            <corners android:radius="?android:attr/buttonCornerRadius"/>
+        </shape>
+    </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/media_squiggly_progress.xml
new file mode 100644
index 0000000..9cd3f62
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_squiggly_progress.xml
@@ -0,0 +1,17 @@
+<?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.
+  -->
+<com.android.systemui.media.controls.ui.SquigglyProgress />
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
new file mode 100644
index 0000000..857632e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        android:shape="oval">
+    <solid android:color="?androidprv:attr/colorSurface"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml
index 6ed3a0ae..217656da 100644
--- a/packages/SystemUI/res/drawable/qs_media_background.xml
+++ b/packages/SystemUI/res/drawable/qs_media_background.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<com.android.systemui.media.IlluminationDrawable
+<com.android.systemui.media.controls.ui.IlluminationDrawable
     xmlns:systemui="http://schemas.android.com/apk/res-auto"
     systemui:highlight="15"
     systemui:cornerRadius="@dimen/notification_corner_radius" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_media_light_source.xml b/packages/SystemUI/res/drawable/qs_media_light_source.xml
index b2647c1..849349a 100644
--- a/packages/SystemUI/res/drawable/qs_media_light_source.xml
+++ b/packages/SystemUI/res/drawable/qs_media_light_source.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.media.LightSourceDrawable
+<com.android.systemui.media.controls.ui.LightSourceDrawable
     xmlns:systemui="http://schemas.android.com/apk/res-auto"
     systemui:rippleMinSize="25dp"
     systemui:rippleMaxSize="135dp" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/user_switcher_fullscreen_button_bg.xml b/packages/SystemUI/res/drawable/user_switcher_fullscreen_button_bg.xml
new file mode 100644
index 0000000..ae0f4b2
--- /dev/null
+++ b/packages/SystemUI/res/drawable/user_switcher_fullscreen_button_bg.xml
@@ -0,0 +1,42 @@
+<?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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:insetTop="@dimen/dialog_button_vertical_inset"
+    android:insetBottom="@dimen/dialog_button_vertical_inset">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="20dp"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="20dp"/>
+                <solid android:color="@android:color/transparent"/>
+                <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant"
+                    android:width="1dp"
+                    />
+                <padding android:left="@dimen/dialog_button_horizontal_padding"
+                    android:top="@dimen/dialog_button_vertical_padding"
+                    android:right="@dimen/dialog_button_horizontal_padding"
+                    android:bottom="@dimen/dialog_button_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
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 bc8e540..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,47 +14,49 @@
   ~ 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"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="wrap_content"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </RelativeLayout>
@@ -67,7 +69,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -77,11 +79,11 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content" />
 
     </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 19a85fe..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,93 +14,73 @@
   ~ 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"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
-    <LinearLayout
+    <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="0dp"
         android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:gravity="center"
-        android:orientation="vertical">
-
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+        android:layout_weight="1">
 
         <ImageView
             android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
+
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
         <TextView
-            android:id="@+id/title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Title"/>
-
-        <TextView
             android:id="@+id/subtitle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Subtitle"/>
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Description"/>
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+    </RelativeLayout>
+
+    <FrameLayout
+        android:layout_weight="1"
+        style="?containerStyle"
+        android:layout_width="0dp"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
 
         <TextView
             android:id="@+id/error"
+            style="?errorTextAppearance"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Error"/>
+            android:layout_gravity="center_horizontal|bottom"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+    </FrameLayout>
 
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:gravity="center"
-        android:paddingLeft="0dp"
-        android:paddingRight="0dp"
-        android:paddingTop="0dp"
-        android:paddingBottom="16dp"
-        android:clipToPadding="false">
-
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            style="@style/LockPatternContainerStyle">
-
-            <com.android.internal.widget.LockPatternView
-                android:id="@+id/lockPattern"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_gravity="center"
-                style="@style/LockPatternStyleBiometricPrompt"/>
-
-        </FrameLayout>
-
-    </LinearLayout>
-
-</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 75a80bc..021ebe6 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -14,45 +14,47 @@
   ~ 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"
     android:layout_height="match_parent"
     android:elevation="@dimen/biometric_dialog_elevation"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
     </RelativeLayout>
 
@@ -64,7 +66,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_horizontal"
@@ -74,11 +76,11 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center_horizontal"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </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 dada981..891c6af 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -14,89 +14,68 @@
   ~ 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"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:gravity="center_horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
     <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
+        android:layout_height="wrap_content">
 
-        <LinearLayout
-            android:id="@+id/auth_credential_header"
-            style="@style/AuthCredentialHeaderStyle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+        <ImageView
+            android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
 
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="48dp"
-                android:layout_height="48dp"
-                android:contentDescription="@null" />
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/title"
-                style="@style/TextAppearance.AuthNonBioCredential.Title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
+        <TextView
+            android:id="@+id/subtitle"
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/subtitle"
-                style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-
-            <TextView
-                android:id="@+id/description"
-                style="@style/TextAppearance.AuthNonBioCredential.Description"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_below="@id/auth_credential_header"
-            android:gravity="center"
-            android:orientation="vertical"
-            android:paddingBottom="16dp"
-            android:paddingTop="60dp">
-
-            <FrameLayout
-                style="@style/LockPatternContainerStyle"
-                android:layout_width="wrap_content"
-                android:layout_height="0dp"
-                android:layout_weight="1">
-
-                <com.android.internal.widget.LockPatternView
-                    android:id="@+id/lockPattern"
-                    style="@style/LockPatternStyle"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:layout_gravity="center" />
-
-            </FrameLayout>
-
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true">
-
-            <TextView
-                android:id="@+id/error"
-                style="@style/TextAppearance.AuthNonBioCredential.Error"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-        </LinearLayout>
-
+        <TextView
+            android:id="@+id/description"
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
     </RelativeLayout>
 
-</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
+    <FrameLayout
+        android:id="@+id/auth_credential_container"
+        style="?containerStyle"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <TextView
+            android:id="@+id/error"
+            style="?errorTextAppearance"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|bottom"/>
+    </FrameLayout>
+
+</com.android.systemui.biometrics.ui.CredentialPatternView>
diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml
index 4da7711..bc97e51 100644
--- a/packages/SystemUI/res/layout/chipbar.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -19,12 +19,12 @@
 <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/media_ttt_sender_chip"
+    android:id="@+id/chipbar_root_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
     <LinearLayout
-        android:id="@+id/media_ttt_sender_chip_inner"
+        android:id="@+id/chipbar_inner"
         android:orientation="horizontal"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -39,7 +39,7 @@
         >
 
         <com.android.internal.widget.CachingIconView
-            android:id="@+id/app_icon"
+            android:id="@+id/start_icon"
             android:layout_width="@dimen/media_ttt_app_icon_size"
             android:layout_height="@dimen/media_ttt_app_icon_size"
             android:layout_marginEnd="12dp"
@@ -69,7 +69,7 @@
             />
 
         <ImageView
-            android:id="@+id/failure_icon"
+            android:id="@+id/error"
             android:layout_width="@dimen/media_ttt_status_icon_size"
             android:layout_height="@dimen/media_ttt_status_icon_size"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
@@ -78,11 +78,11 @@
             android:alpha="0.0"
             />
 
+        <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. -->
         <TextView
-            android:id="@+id/undo"
+            android:id="@+id/end_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/media_transfer_undo"
             android:textColor="?androidprv:attr/textColorOnAccent"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
             android:textSize="@dimen/media_ttt_text_size"
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 5dc34b9..a565988 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -73,8 +73,8 @@
         android:singleLine="true"
         android:textDirection="locale"
         android:textAppearance="@style/TextAppearance.QS.Status"
-        android:transformPivotX="0sp"
-        android:transformPivotY="20sp"
+        android:transformPivotX="0dp"
+        android:transformPivotY="24dp"
         android:scaleX="1"
         android:scaleY="1"
     />
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 006b260..9add32c 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -18,6 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/dream_overlay_status_bar"
+    android:visibility="invisible"
     android:layout_width="match_parent"
     android:layout_height="@dimen/dream_overlay_status_bar_height"
     android:paddingEnd="@dimen/dream_overlay_status_bar_margin"
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index 5b96159..f14be41 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -190,6 +190,11 @@
 
                 </LinearLayout>
 
+                <ViewStub android:id="@+id/secondary_mobile_network_stub"
+                  android:inflatedId="@+id/secondary_mobile_network_layout"
+                  android:layout="@layout/qs_dialog_secondary_mobile_network"
+                  style="@style/InternetDialog.Network"/>
+
                 <LinearLayout
                     android:id="@+id/turn_on_wifi_layout"
                     style="@style/InternetDialog.Network"
@@ -307,22 +312,15 @@
 
             <LinearLayout
                 android:id="@+id/see_all_layout"
-                android:layout_width="match_parent"
+                style="@style/InternetDialog.Network"
                 android:layout_height="64dp"
-                android:clickable="true"
-                android:focusable="true"
-                android:background="?android:attr/selectableItemBackground"
-                android:gravity="center_vertical|center_horizontal"
-                android:orientation="horizontal"
-                android:paddingStart="22dp"
-                android:paddingEnd="22dp">
+                android:paddingStart="20dp">
 
                 <FrameLayout
                     android:layout_width="24dp"
                     android:layout_height="24dp"
                     android:clickable="false"
-                    android:layout_gravity="center_vertical|start"
-                    android:layout_marginStart="@dimen/internet_dialog_network_layout_margin">
+                    android:layout_gravity="center_vertical|start">
                     <ImageView
                         android:id="@+id/arrow_forward"
                         android:src="@drawable/ic_arrow_forward"
diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml
index d27fa19..8b85940 100644
--- a/packages/SystemUI/res/layout/keyguard_status_bar.xml
+++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml
@@ -34,30 +34,13 @@
         android:paddingTop="@dimen/status_bar_padding_top"
         android:layout_alignParentEnd="true"
         android:gravity="center_vertical|end" >
-        <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
+
+        <include
             android:id="@+id/user_switcher_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:gravity="center"
-            android:orientation="horizontal"
-            android:paddingTop="4dp"
-            android:paddingBottom="4dp"
-            android:paddingStart="8dp"
-            android:paddingEnd="8dp"
-            android:background="@drawable/status_bar_user_chip_bg"
-            android:visibility="visible" >
-            <ImageView android:id="@+id/current_user_avatar"
-                android:layout_width="@dimen/multi_user_avatar_keyguard_size"
-                android:layout_height="@dimen/multi_user_avatar_keyguard_size"
-                android:scaleType="centerInside"
-                android:paddingEnd="4dp" />
-
-            <TextView android:id="@+id/current_user_name"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:textAppearance="@style/TextAppearance.StatusBar.Clock"
-                />
-        </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
+            android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin"
+            layout="@layout/status_bar_user_chip_container" />
 
         <FrameLayout android:id="@+id/system_icons_container"
             android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/log_access_user_consent_dialog_permission.xml b/packages/SystemUI/res/layout/log_access_user_consent_dialog_permission.xml
new file mode 100644
index 0000000..89e36ac
--- /dev/null
+++ b/packages/SystemUI/res/layout/log_access_user_consent_dialog_permission.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:layout_width="380dp"
+        android:layout_height="match_parent"
+        android:clipToPadding="false">
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:gravity="center"
+            android:paddingLeft="24dp"
+            android:paddingRight="24dp"
+            android:paddingTop="24dp"
+            android:paddingBottom="24dp">
+
+        <ImageView
+                android:id="@+id/log_access_image_view"
+                android:layout_width="32dp"
+                android:layout_height="32dp"
+                android:layout_marginBottom="16dp"
+                android:src="@drawable/ic_doc_document"
+                tools:layout_editor_absoluteX="148dp"
+                tools:layout_editor_absoluteY="35dp"
+                android:gravity="center" />
+
+        <TextView
+                android:id="@+id/log_access_dialog_title"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginBottom="32dp"
+                android:text="@string/log_access_confirmation_title"
+                android:textAppearance="@style/AllowLogAccess"
+                android:textColor="?android:attr/textColorPrimary"
+                android:gravity="center" />
+
+        <TextView
+                android:id="@+id/log_access_dialog_body"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginBottom="40dp"
+                android:text="@string/log_access_confirmation_body"
+                android:textAppearance="@style/PrimaryAllowLogAccess"
+                android:textColor="?android:attr/textColorPrimary"
+                android:gravity="center" />
+
+        <Space
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1" />
+
+        <LinearLayout
+                android:orientation="vertical"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+            <Button
+                    android:id="@+id/log_access_dialog_allow_button"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/log_access_confirmation_allow"
+                    style="?permissionGrantButtonTopStyle"
+                    android:textAppearance="@style/PermissionGrantButtonTextAppearance"
+                    android:layout_marginBottom="5dp"
+                    android:layout_centerHorizontal="true"
+                    android:layout_alignParentTop="true"
+                    android:layout_alignParentBottom="true"
+                    android:clipToOutline="true"
+                    android:gravity="center" />
+
+            <Button
+                    android:id="@+id/log_access_dialog_deny_button"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/log_access_confirmation_deny"
+                    style="?permissionGrantButtonBottomStyle"
+                    android:textAppearance="@style/PermissionGrantButtonTextAppearance"
+                    android:layout_centerHorizontal="true"
+                    android:layout_alignParentTop="true"
+                    android:layout_alignParentBottom="true"
+                    android:clipToOutline="true"
+                    android:gravity="center" />
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 50d3cc4..715c869 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -24,7 +24,7 @@
     android:clipToPadding="false"
     android:forceHasOverlappingRendering="false"
     android:theme="@style/MediaPlayer">
-    <com.android.systemui.media.MediaScrollView
+    <com.android.systemui.media.controls.ui.MediaScrollView
         android:id="@+id/media_carousel_scroller"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -42,7 +42,7 @@
             >
             <!-- QSMediaPlayers will be added here dynamically -->
         </LinearLayout>
-    </com.android.systemui.media.MediaScrollView>
+    </com.android.systemui.media.controls.ui.MediaScrollView>
     <com.android.systemui.qs.PageIndicator
         android:id="@+id/media_page_indicator"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index c526d9c..530db0d 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -44,6 +44,24 @@
         android:background="@drawable/qs_media_outline_album_bg"
         />
 
+    <com.android.systemui.surfaceeffects.ripple.MultiRippleView
+        android:id="@+id/touch_ripple_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded"
+        app:layout_constraintStart_toStartOf="@id/album_art"
+        app:layout_constraintEnd_toEndOf="@id/album_art"
+        app:layout_constraintTop_toTopOf="@id/album_art"
+        app:layout_constraintBottom_toBottomOf="@id/album_art" />
+
+    <com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
+        android:id="@+id/turbulence_noise_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded"
+        app:layout_constraintStart_toStartOf="@id/album_art"
+        app:layout_constraintEnd_toEndOf="@id/album_art"
+        app:layout_constraintTop_toTopOf="@id/album_art"
+        app:layout_constraintBottom_toBottomOf="@id/album_art" />
+
     <!-- Guideline for output switcher -->
     <androidx.constraintlayout.widget.Guideline
         android:id="@+id/center_vertical_guideline"
diff --git a/packages/SystemUI/res/layout/qs_dialog_secondary_mobile_network.xml b/packages/SystemUI/res/layout/qs_dialog_secondary_mobile_network.xml
new file mode 100644
index 0000000..4592c5e
--- /dev/null
+++ b/packages/SystemUI/res/layout/qs_dialog_secondary_mobile_network.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/InternetDialog.Network">
+
+    <FrameLayout
+	android:layout_width="24dp"
+	android:layout_height="24dp"
+	android:clickable="false"
+	android:layout_gravity="center_vertical|start">
+	<ImageView
+	    android:id="@+id/secondary_signal_icon"
+	    android:autoMirrored="true"
+	    android:layout_width="wrap_content"
+	    android:layout_height="wrap_content"
+	    android:layout_gravity="center"/>
+    </FrameLayout>
+
+    <LinearLayout
+	android:layout_weight="1"
+	android:orientation="vertical"
+	android:clickable="false"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:gravity="start|center_vertical">
+	<TextView
+	    android:id="@+id/secondary_mobile_title"
+	    android:maxLines="1"
+	    style="@style/InternetDialog.NetworkTitle"/>
+	<TextView
+	    android:id="@+id/secondary_mobile_summary"
+	    style="@style/InternetDialog.NetworkSummary"/>
+    </LinearLayout>
+
+    <FrameLayout
+	android:layout_width="24dp"
+	android:layout_height="match_parent"
+	android:clickable="false"
+	android:layout_gravity="end|center_vertical"
+	android:gravity="center">
+	<ImageView
+	    android:id="@+id/secondary_settings_icon"
+	    android:src="@drawable/ic_settings_24dp"
+	    android:layout_width="24dp"
+	    android:layout_gravity="end|center_vertical"
+	    android:layout_height="wrap_content"/>
+    </FrameLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 2fb6d6c..9fc3f40 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -23,6 +23,7 @@
     android:layout_height="wrap_content"
     android:layout_gravity="@integer/notification_panel_layout_gravity"
     android:background="@android:color/transparent"
+    android:importantForAccessibility="no"
     android:baselineAligned="false"
     android:clickable="false"
     android:clipChildren="false"
@@ -56,7 +57,7 @@
             android:clipToPadding="false"
             android:focusable="true"
             android:paddingBottom="@dimen/qqs_layout_padding_bottom"
-            android:importantForAccessibility="yes">
+            android:importantForAccessibility="no">
         </com.android.systemui.qs.QuickQSPanel>
     </RelativeLayout>
 
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
index 60bc373..8b5d953 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
@@ -25,6 +25,7 @@
     android:gravity="center"
     android:layout_gravity="top"
     android:orientation="horizontal"
+    android:importantForAccessibility="no"
     android:clickable="true"
     android:minHeight="48dp">
 
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
new file mode 100644
index 0000000..d6c9e98
--- /dev/null
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -0,0 +1,87 @@
+<!--
+  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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <ImageView
+            android:layout_width="@dimen/screenrecord_option_icon_size"
+            android:layout_height="@dimen/screenrecord_option_icon_size"
+            android:src="@drawable/ic_mic_26dp"
+            android:tint="?android:attr/textColorSecondary"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="0"
+            android:layout_marginRight="@dimen/screenrecord_option_padding"
+            android:importantForAccessibility="no"/>
+        <Spinner
+            android:id="@+id/screen_recording_options"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:minHeight="48dp"
+            android:layout_weight="1"
+            android:popupBackground="@drawable/screenrecord_spinner_background"
+            android:dropDownWidth="274dp"
+            android:importantForAccessibility="yes"/>
+        <Switch
+            android:layout_width="wrap_content"
+            android:minWidth="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="0"
+            android:layout_gravity="end"
+            android:id="@+id/screenrecord_audio_switch"
+            style="@style/ScreenRecord.Switch"
+            android:importantForAccessibility="yes"/>
+    </LinearLayout>
+    <LinearLayout
+        android:id="@+id/show_taps"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_marginTop="@dimen/screenrecord_option_padding">
+        <ImageView
+            android:layout_width="@dimen/screenrecord_option_icon_size"
+            android:layout_height="@dimen/screenrecord_option_icon_size"
+            android:layout_weight="0"
+            android:src="@drawable/ic_touch"
+            android:tint="?android:attr/textColorSecondary"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="@dimen/screenrecord_option_padding"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:minHeight="48dp"
+            android:layout_weight="1"
+            android:gravity="center_vertical"
+            android:text="@string/screenrecord_taps_label"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:fontFamily="@*android:string/config_headlineFontFamily"
+            android:textColor="?android:attr/textColorPrimary"
+            android:contentDescription="@string/screenrecord_taps_label"/>
+        <Switch
+            android:layout_width="wrap_content"
+            android:minWidth="48dp"
+            android:layout_height="48dp"
+            android:layout_weight="0"
+            android:id="@+id/screenrecord_taps_switch"
+            style="@style/ScreenRecord.Switch"
+            android:importantForAccessibility="yes"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml
new file mode 100644
index 0000000..ac46cdb
--- /dev/null
+++ b/packages/SystemUI/res/layout/screen_share_dialog.xml
@@ -0,0 +1,94 @@
+<!--
+  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.
+  -->
+
+<!-- Scrollview is necessary to fit everything in landscape layout -->
+<ScrollView  xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/screen_share_permission_dialog"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="@dimen/dialog_side_padding"
+        android:paddingEnd="@dimen/dialog_side_padding"
+        android:paddingTop="@dimen/dialog_top_padding"
+        android:paddingBottom="@dimen/dialog_bottom_padding"
+        android:orientation="vertical"
+        android:gravity="center_horizontal">
+
+        <ImageView
+            android:layout_width="@dimen/screenrecord_logo_size"
+            android:layout_height="@dimen/screenrecord_logo_size"
+            android:src="@drawable/ic_screenrecord"
+            android:tint="@color/screenrecord_icon_color"
+            android:importantForAccessibility="no"/>
+        <TextView
+            android:id="@+id/screen_share_dialog_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:fontFamily="@*android:string/config_headlineFontFamily"
+            android:layout_marginTop="22dp"
+            android:layout_marginBottom="15dp"/>
+        <Spinner
+            android:id="@+id/screen_share_mode_spinner"
+            android:layout_width="320dp"
+            android:layout_height="72dp"
+            android:layout_marginTop="24dp"
+            android:layout_marginBottom="24dp" />
+        <ViewStub
+            android:id="@+id/options_stub"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+        <TextView
+            android:id="@+id/text_warning"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/screenrecord_description"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:textColorSecondary"
+            android:gravity="start"
+            android:layout_marginBottom="20dp"/>
+
+        <!-- Buttons -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginTop="36dp">
+            <TextView
+                android:id="@+id/button_cancel"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:text="@string/cancel"
+                style="@style/Widget.Dialog.Button.BorderButton" />
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"/>
+            <TextView
+                android:id="@+id/button_start"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="0"
+                android:text="@string/screenrecord_start"
+                style="@style/Widget.Dialog.Button" />
+        </LinearLayout>
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 9c02749..8842992 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -44,7 +44,7 @@
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintWidth_percent="1.0"
         app:layout_constraintWidth_max="wrap"
-        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
         app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="parent">
         <LinearLayout
@@ -70,7 +70,7 @@
         android:alpha="0"
         android:background="@drawable/overlay_border"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end"
         app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/>
     <androidx.constraintlayout.widget.Barrier
@@ -103,8 +103,18 @@
         app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
-        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border">
-    </ImageView>
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+    <ImageView
+        android:id="@+id/screenshot_badge"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:padding="4dp"
+        android:visibility="gone"
+        android:background="@drawable/overlay_badge_background"
+        android:elevation="8dp"
+        android:src="@drawable/overlay_cancel"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
     <FrameLayout
         android:id="@+id/screenshot_dismiss_button"
         android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
@@ -132,4 +142,41 @@
         app:layout_constraintStart_toStartOf="@id/screenshot_preview"
         app:layout_constraintTop_toTopOf="@id/screenshot_preview"
         android:elevation="7dp"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/screenshot_message_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginVertical="4dp"
+        android:paddingHorizontal="@dimen/overlay_action_container_padding_right"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:background="@drawable/action_chip_container_background"
+        android:visibility="gone"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <ImageView
+            android:id="@+id/screenshot_message_icon"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:paddingEnd="4dp"
+            android:src="@drawable/ic_work_app_badge"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+        <TextView
+            android:id="@+id/screenshot_message_content"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@id/screenshot_message_icon"
+            app:layout_constraintEnd_toEndOf="parent"/>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
 </com.android.systemui.screenshot.DraggableConstraintLayout>
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index 80e65a3..f7600e6 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -136,31 +136,12 @@
                 android:gravity="center_vertical|end"
                 android:clipChildren="false">
 
-                <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
+                <include
                     android:id="@+id/user_switcher_container"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:gravity="center"
-                    android:orientation="horizontal"
-                    android:paddingTop="4dp"
-                    android:paddingBottom="4dp"
-                    android:paddingStart="8dp"
-                    android:paddingEnd="8dp"
-                    android:layout_marginEnd="16dp"
-                    android:background="@drawable/status_bar_user_chip_bg"
-                    android:visibility="visible" >
-                    <ImageView android:id="@+id/current_user_avatar"
-                        android:layout_width="@dimen/multi_user_avatar_keyguard_size"
-                        android:layout_height="@dimen/multi_user_avatar_keyguard_size"
-                        android:scaleType="centerInside"
-                        android:paddingEnd="4dp" />
-
-                    <TextView android:id="@+id/current_user_name"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:textAppearance="@style/TextAppearance.StatusBar.Clock"
-                        />
-                </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
+                    android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin"
+                    layout="@layout/status_bar_user_chip_container" />
 
                 <include layout="@layout/system_icons" />
             </com.android.keyguard.AlphaOptimizedLinearLayout>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f0e49d5..159323a 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -32,41 +32,8 @@
         android:layout_height="match_parent"
         android:layout_width="match_parent" />
 
-    <include
-        layout="@layout/keyguard_bottom_area"
-        android:visibility="gone" />
-
-    <ViewStub
-        android:id="@+id/keyguard_user_switcher_stub"
-        android:layout="@layout/keyguard_user_switcher"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent" />
-
     <include layout="@layout/status_bar_expanded_plugin_frame"/>
 
-    <include layout="@layout/dock_info_bottom_area_overlay" />
-
-    <com.android.keyguard.LockIconView
-        android:id="@+id/lock_icon_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content">
-        <!-- Background protection -->
-        <ImageView
-            android:id="@+id/lock_icon_bg"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@drawable/fingerprint_bg"
-            android:visibility="invisible"/>
-
-        <ImageView
-            android:id="@+id/lock_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"/>
-
-    </com.android.keyguard.LockIconView>
-
     <com.android.systemui.shade.NotificationsQuickSettingsContainer
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -75,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"/>
@@ -102,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"
@@ -145,6 +115,39 @@
         />
     </com.android.systemui.shade.NotificationsQuickSettingsContainer>
 
+    <include
+        layout="@layout/keyguard_bottom_area"
+        android:visibility="gone" />
+
+    <ViewStub
+        android:id="@+id/keyguard_user_switcher_stub"
+        android:layout="@layout/keyguard_user_switcher"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+    <include layout="@layout/dock_info_bottom_area_overlay" />
+
+    <com.android.keyguard.LockIconView
+        android:id="@+id/lock_icon_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <!-- Background protection -->
+        <ImageView
+            android:id="@+id/lock_icon_bg"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/fingerprint_bg"
+            android:visibility="invisible"/>
+
+        <ImageView
+            android:id="@+id/lock_icon"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:scaleType="centerCrop"/>
+
+    </com.android.keyguard.LockIconView>
+
     <FrameLayout
         android:id="@+id/preview_container"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/status_bar_user_chip_container.xml b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml
new file mode 100644
index 0000000..b374074
--- /dev/null
+++ b/packages/SystemUI/res/layout/status_bar_user_chip_container.xml
@@ -0,0 +1,40 @@
+<?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.
+  -->
+
+<com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_switcher_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="horizontal"
+    android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin"
+    android:background="@drawable/status_bar_user_chip_bg"
+    android:visibility="visible" >
+    <ImageView android:id="@+id/current_user_avatar"
+        android:layout_width="@dimen/status_bar_user_chip_avatar_size"
+        android:layout_height="@dimen/status_bar_user_chip_avatar_size"
+        android:layout_margin="4dp"
+        android:scaleType="centerInside" />
+
+    <TextView android:id="@+id/current_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingEnd="8dp"
+        android:textAppearance="@style/TextAppearance.StatusBar.UserChip"
+        />
+</com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 8388b67..bafdb11 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -26,12 +26,12 @@
     android:fitsSystemWindows="true">
 
     <com.android.systemui.statusbar.BackDropView
-            android:id="@+id/backdrop"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:visibility="gone"
-            sysui:ignoreRightInset="true"
-            >
+        android:id="@+id/backdrop"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        sysui:ignoreRightInset="true"
+    >
         <ImageView android:id="@+id/backdrop_back"
                    android:layout_width="match_parent"
                    android:scaleType="centerCrop"
@@ -49,7 +49,7 @@
         android:layout_height="match_parent"
         android:importantForAccessibility="no"
         sysui:ignoreRightInset="true"
-        />
+    />
 
     <com.android.systemui.scrim.ScrimView
         android:id="@+id/scrim_notifications"
@@ -57,17 +57,17 @@
         android:layout_height="match_parent"
         android:importantForAccessibility="no"
         sysui:ignoreRightInset="true"
-        />
+    />
 
     <com.android.systemui.statusbar.LightRevealScrim
-            android:id="@+id/light_reveal_scrim"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
+        android:id="@+id/light_reveal_scrim"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
 
     <include layout="@layout/status_bar_expanded"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="invisible" />
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:visibility="invisible" />
 
     <include layout="@layout/brightness_mirror_container" />
 
diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
index c2c79cb..fa9d739 100644
--- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
+++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml
@@ -14,58 +14,84 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.user.UserSwitcherRootView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/user_switcher_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_marginVertical="40dp"
-    android:layout_marginHorizontal="60dp">
+    android:orientation="vertical">
 
-  <androidx.constraintlayout.helper.widget.Flow
-      android:id="@+id/flow"
-      android:layout_width="0dp"
-      android:layout_height="wrap_content"
-      app:layout_constraintBottom_toBottomOf="parent"
-      app:layout_constraintTop_toTopOf="parent"
-      app:layout_constraintStart_toStartOf="parent"
-      app:layout_constraintEnd_toEndOf="parent"
-      app:flow_horizontalBias="0.5"
-      app:flow_verticalAlign="center"
-      app:flow_wrapMode="chain"
-      app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap"
-      app:flow_verticalGap="44dp"
-      app:flow_horizontalStyle="packed"/>
+  <ScrollView
+      android:layout_width="match_parent"
+      android:layout_height="0dp"
+      android:layout_weight="1"
+      android:fillViewport="true">
 
-  <TextView
-      android:id="@+id/cancel"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_gravity="center"
-      android:gravity="center"
-      app:layout_constraintHeight_min="48dp"
-      app:layout_constraintEnd_toStartOf="@+id/add"
-      app:layout_constraintBottom_toBottomOf="parent"
-      android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding"
-      android:textSize="@dimen/user_switcher_fullscreen_button_text_size"
-      android:textColor="?androidprv:attr/colorAccentPrimary"
-      android:text="@string/cancel" />
+    <com.android.systemui.user.UserSwitcherRootView
+        android:id="@+id/user_switcher_grid_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="40dp"
+        android:paddingHorizontal="60dp">
 
-  <TextView
-      android:id="@+id/add"
-      style="@style/Widget.Dialog.Button.BorderButton"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_gravity="center"
-      android:gravity="center"
-      android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding"
-      android:text="@string/add"
-      android:textColor="?androidprv:attr/colorAccentPrimary"
-      android:textSize="@dimen/user_switcher_fullscreen_button_text_size"
-      android:visibility="gone"
-      app:layout_constraintBottom_toBottomOf="parent"
-      app:layout_constraintEnd_toEndOf="parent"
-      app:layout_constraintHeight_min="48dp" />
-</com.android.systemui.user.UserSwitcherRootView>
+      <androidx.constraintlayout.helper.widget.Flow
+          android:id="@+id/flow"
+          android:layout_width="0dp"
+          android:layout_height="wrap_content"
+          app:layout_constraintBottom_toBottomOf="parent"
+          app:layout_constraintTop_toTopOf="parent"
+          app:layout_constraintStart_toStartOf="parent"
+          app:layout_constraintEnd_toEndOf="parent"
+          app:flow_horizontalBias="0.5"
+          app:flow_verticalAlign="center"
+          app:flow_wrapMode="chain"
+          app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap"
+          app:flow_verticalGap="44dp"
+          app:flow_horizontalStyle="packed"/>
+    </com.android.systemui.user.UserSwitcherRootView>
+
+  </ScrollView>
+
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="96dp"
+    android:orientation="horizontal"
+    android:gravity="center_vertical|end"
+    android:paddingEnd="48dp">
+
+    <TextView
+        android:id="@+id/cancel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:gravity="center"
+        android:minHeight="48dp"
+        android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding"
+        android:textSize="@dimen/user_switcher_fullscreen_button_text_size"
+        android:textColor="?androidprv:attr/colorAccentPrimary"
+        android:text="@string/cancel" />
+
+    <Space
+        android:layout_width="24dp"
+        android:layout_height="0dp"
+        />
+
+    <TextView
+        android:id="@+id/add"
+        android:background="@drawable/user_switcher_fullscreen_button_bg"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:gravity="center"
+        android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding"
+        android:text="@string/add"
+        android:textColor="?androidprv:attr/colorAccentPrimary"
+        android:textSize="@dimen/user_switcher_fullscreen_button_text_size"
+        android:visibility="gone"
+        android:minHeight="48dp" />
+
+  </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml
index 0bff47c..0be7328 100644
--- a/packages/SystemUI/res/layout/window_magnifier_view.xml
+++ b/packages/SystemUI/res/layout/window_magnifier_view.xml
@@ -40,19 +40,22 @@
             android:id="@+id/top_handle"
             android:layout_width="match_parent"
             android:layout_height="@dimen/magnification_border_drag_size"
-            android:layout_alignParentTop="true"/>
+            android:layout_alignParentTop="true"
+            android:accessibilityTraversalAfter="@id/left_handle"/>
 
         <View
             android:id="@+id/right_handle"
             android:layout_width="@dimen/magnification_border_drag_size"
             android:layout_height="match_parent"
-            android:layout_alignParentEnd="true"/>
+            android:layout_alignParentEnd="true"
+            android:accessibilityTraversalAfter="@id/top_handle"/>
 
         <View
             android:id="@+id/bottom_handle"
             android:layout_width="match_parent"
             android:layout_height="@dimen/magnification_border_drag_size"
-            android:layout_alignParentBottom="true"/>
+            android:layout_alignParentBottom="true"
+            android:accessibilityTraversalAfter="@id/right_handle"/>
 
         <SurfaceView
             android:id="@+id/surface_view"
@@ -62,6 +65,58 @@
     </RelativeLayout>
 
     <ImageView
+        android:id="@+id/top_right_corner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/magnification_outer_border_margin"
+        android:layout_gravity="right|top"
+        android:paddingTop="@dimen/magnifier_drag_handle_padding"
+        android:paddingEnd="@dimen/magnifier_drag_handle_padding"
+        android:scaleType="center"
+        android:contentDescription="@string/magnification_drag_corner_to_resize"
+        android:src="@drawable/ic_magnification_corner_top_right"
+        android:accessibilityTraversalAfter="@id/top_left_corner"/>
+
+    <ImageView
+        android:id="@+id/top_left_corner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/magnification_outer_border_margin"
+        android:layout_gravity="left|top"
+        android:paddingTop="@dimen/magnifier_drag_handle_padding"
+        android:paddingStart="@dimen/magnifier_drag_handle_padding"
+        android:scaleType="center"
+        android:contentDescription="@string/magnification_drag_corner_to_resize"
+        android:src="@drawable/ic_magnification_corner_top_left"
+        android:accessibilityTraversalAfter="@id/bottom_handle"/>
+
+    <ImageView
+        android:id="@+id/bottom_right_corner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/magnification_outer_border_margin"
+        android:layout_gravity="right|bottom"
+        android:paddingEnd="@dimen/magnifier_drag_handle_padding"
+        android:paddingBottom="@dimen/magnifier_drag_handle_padding"
+        android:scaleType="center"
+        android:contentDescription="@string/magnification_drag_corner_to_resize"
+        android:src="@drawable/ic_magnification_corner_bottom_right"
+        android:accessibilityTraversalAfter="@id/top_right_corner"/>
+
+    <ImageView
+        android:id="@+id/bottom_left_corner"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/magnification_outer_border_margin"
+        android:layout_gravity="left|bottom"
+        android:paddingStart="@dimen/magnifier_drag_handle_padding"
+        android:paddingBottom="@dimen/magnifier_drag_handle_padding"
+        android:scaleType="center"
+        android:contentDescription="@string/magnification_drag_corner_to_resize"
+        android:src="@drawable/ic_magnification_corner_bottom_left"
+        android:accessibilityTraversalAfter="@id/bottom_right_corner"/>
+
+    <ImageView
         android:id="@+id/drag_handle"
         android:layout_width="@dimen/magnification_drag_view_size"
         android:layout_height="@dimen/magnification_drag_view_size"
diff --git a/packages/SystemUI/res/layout/wireless_charging_layout.xml b/packages/SystemUI/res/layout/wireless_charging_layout.xml
index 887e3e7..f1bc883 100644
--- a/packages/SystemUI/res/layout/wireless_charging_layout.xml
+++ b/packages/SystemUI/res/layout/wireless_charging_layout.xml
@@ -22,7 +22,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <com.android.systemui.ripple.RippleView
+    <com.android.systemui.surfaceeffects.ripple.RippleView
         android:id="@+id/wireless_charging_ripple"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
diff --git a/packages/SystemUI/res/raw/biometricprompt_folded_base_bottomright.json b/packages/SystemUI/res/raw/biometricprompt_folded_base_bottomright.json
new file mode 100644
index 0000000..2797996
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_folded_base_bottomright.json
@@ -0,0 +1 @@
+{"v":"5.9.0","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"biometricprompt_portrait_base_bottomright","ddd":0,"assets":[{"id":"comp_0","nm":"biometricprompt_landscape_base","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 16","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Null_Circle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[70.333,-88.75,0],"to":[-11.722,17.639,0],"ti":[11.722,-17.639,0]},{"t":-48,"s":[0,17.083,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".grey600","cl":"grey600","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle mask 3","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Finger","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-60,"s":[55]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":110,"s":[0]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":140,"s":[10]},{"t":170,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-60,"s":[92.146,-65.896,0],"to":[1.361,6.667,0],"ti":[-1.361,-6.667,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":0,"s":[100.313,-25.896,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.7,"y":0.7},"t":110,"s":[100.313,-25.896,0],"to":[0,0,0],"ti":[0,0,0]},{"t":170,"s":[100.313,-25.896,0]}],"ix":2,"l":2},"a":{"a":0,"k":[160.315,58.684,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.013,2.518],[5.251,5.023],[8.982,-2.829],[-0.264,-5.587]],"o":[[12.768,-2.854],[-14.961,2.071],[-6.004,1.89],[8.052,1.403]],"v":[[5.115,7.499],[19.814,-10.087],[-16.489,-3.588],[-24.801,8.684]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.760784373564,0.478431402468,0.400000029919,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[34.67,28.053],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.231,-7],[-27.395,-1.197],[-26.792,4.092],[14.179,15.736]],"o":[[-17.931,5.646],[56.062,2.45],[-1.765,-22.396],[-51.819,17.744]],"v":[[-62.102,-8.314],[-39.958,30.079],[80.033,25.905],[54.879,-32.529]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431372549,0.403921598547,0.305882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[80.283,32.779],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"circle mask 7","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".grey600","cl":"grey600","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.25,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":36.9,"ix":2},"o":{"a":0,"k":114.2,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"circle mask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".grey800","cl":"grey800","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.5,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"circle mask 6","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".grey900","cl":"grey900","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":377,"s":[-180]},{"t":417,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":377,"s":[-1.137,1.771,0],"to":[0.375,0,0],"ti":[-0.375,0,0]},{"t":417,"s":[1.113,1.771,0]}],"ix":2,"l":2},"a":{"a":0,"k":[6.238,5.063,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":77,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":107,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":137,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":167,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":197,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":232,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":562,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"t":602,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-4.546,-0.421],[-5.988,1.021],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6.238,5.063],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"circle mask 2","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,8.308,0],"ix":2,"l":2},"a":{"a":0,"k":[41.706,20.979,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[18.645,0],[0,18.645]],"o":[[0,18.645],[-18.644,0],[0,0]],"v":[[33.76,-16.88],[-0.001,16.88],[-33.76,-16.88]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,17.13],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[22.896,0],[0,22.896]],"o":[[0,22.896],[-22.896,0],[0,0]],"v":[[41.457,-20.729],[-0.001,20.729],[-41.457,-20.729]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,20.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle mask 4","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":15,"ty":1,"nm":".grey900","cl":"grey900","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,66,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[52,52,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#202124","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"circle mask 5","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":17,"ty":1,"nm":".black","cl":"black","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-17.333,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[72,72,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#000000","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":".grey800","cl":"grey800","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[-192.25,99.933,0],"to":[5,3.333,0],"ti":[-5,-3.333,0]},{"t":-48,"s":[-162.25,119.933,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-163,100.85,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-108,"s":[100,100,100]},{"t":-48,"s":[59,59,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":".grey900","cl":"grey900","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[0,18.167,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[0,10.667,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-171,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-141,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.512],[0,0.512],[3,3.512]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-111,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"t":-81,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.967],[0,0.967],[3,3.967]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-199,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 4","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-116.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":-199,"s":[71,-101.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":365,"s":[71,-101.083,0],"to":[0,0,0],"ti":[16.833,-14.361,0]},{"t":405,"s":[-30,-14.917,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-239,"s":[29,29]},{"i":{"x":[0.833,0.833],"y":[1,0.833]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-199,"s":[29,38]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":365,"s":[29,36]},{"t":405,"s":[83,83]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":365,"s":[50]},{"t":405,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":".grey900","cl":"grey900","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-82.917,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[71,-90.417,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":-199,"st":-255,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"device frame mask","parent":24,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,1.167,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":".blue400","cl":"blue400","parent":18,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[100.25,-115.167,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-199,"s":[100.25,-100.167,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":-159,"s":[100.25,-105.667,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":365,"s":[100.25,-100.167,0],"to":[0,0,0],"ti":[16.833,-14.361,0]},{"t":405,"s":[-0.75,-14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-239,"s":[29,29]},{"i":{"x":[0.833,0.833],"y":[1,0.833]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-199,"s":[29,38]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":365,"s":[29,36]},{"t":405,"s":[83,83]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":365,"s":[50]},{"t":405,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":24,"ty":3,"nm":"device frame mask 5","parent":18,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":-165,"op":6.00000000000001,"st":-271,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"device frame mask 9","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-181,"op":-62,"st":-181,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-145,"s":[50]},{"t":-75,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-165,"s":[0,0]},{"t":-75,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[50]},{"t":113,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-181,"op":-62,"st":-181,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"device frame mask 8","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-211,"op":-92,"st":-211,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-165,"s":[50]},{"t":-95,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-195,"s":[0,0]},{"t":-105,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[50]},{"t":83,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-211,"op":-92,"st":-211,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"device frame mask 7","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-241,"op":-122,"st":-241,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-195,"s":[50]},{"t":-125,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-225,"s":[0,0]},{"t":-135,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[50]},{"t":53,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-241,"op":-122,"st":-241,"bm":0},{"ddd":0,"ind":34,"ty":4,"nm":"device frame mask 6","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-271,"op":-152,"st":-271,"bm":0},{"ddd":0,"ind":35,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-225,"s":[50]},{"t":-155,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-255,"s":[0,0]},{"t":-165,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-17,"s":[50]},{"t":23,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-271,"op":-152,"st":-271,"bm":0}]}],"layers":[{"ddd":0,"ind":6,"ty":0,"nm":"biometricprompt_landscape_base","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2,"l":2},"a":{"a":0,"k":[170,170,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":340,"h":340,"ip":0,"op":900,"st":0,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
diff --git a/packages/SystemUI/res/raw/biometricprompt_folded_base_default.json b/packages/SystemUI/res/raw/biometricprompt_folded_base_default.json
new file mode 100644
index 0000000..bf65b34
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_folded_base_default.json
@@ -0,0 +1 @@
+{"v":"5.9.0","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"biometricprompt_landscape_base","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 16","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Null_Circle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[70.333,-88.75,0],"to":[-11.722,17.639,0],"ti":[11.722,-17.639,0]},{"t":-48,"s":[0,17.083,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".grey600","cl":"grey600","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"circle mask 3","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Finger","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-60,"s":[55]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":110,"s":[0]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":140,"s":[10]},{"t":170,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-60,"s":[92.146,-65.896,0],"to":[1.361,6.667,0],"ti":[-1.361,-6.667,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":0,"s":[100.313,-25.896,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.7,"y":0.7},"t":110,"s":[100.313,-25.896,0],"to":[0,0,0],"ti":[0,0,0]},{"t":170,"s":[100.313,-25.896,0]}],"ix":2,"l":2},"a":{"a":0,"k":[160.315,58.684,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.013,2.518],[5.251,5.023],[8.982,-2.829],[-0.264,-5.587]],"o":[[12.768,-2.854],[-14.961,2.071],[-6.004,1.89],[8.052,1.403]],"v":[[5.115,7.499],[19.814,-10.087],[-16.489,-3.588],[-24.801,8.684]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.760784373564,0.478431402468,0.400000029919,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[34.67,28.053],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.231,-7],[-27.395,-1.197],[-26.792,4.092],[14.179,15.736]],"o":[[-17.931,5.646],[56.062,2.45],[-1.765,-22.396],[-51.819,17.744]],"v":[[-62.102,-8.314],[-39.958,30.079],[80.033,25.905],[54.879,-32.529]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431372549,0.403921598547,0.305882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[80.283,32.779],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"circle mask 7","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":".grey600","cl":"grey600","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.25,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":36.9,"ix":2},"o":{"a":0,"k":114.2,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"circle mask","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".grey800","cl":"grey800","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.5,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"circle mask 6","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":".grey900","cl":"grey900","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":377,"s":[-180]},{"t":417,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":377,"s":[-1.137,1.771,0],"to":[0.375,0,0],"ti":[-0.375,0,0]},{"t":417,"s":[1.113,1.771,0]}],"ix":2,"l":2},"a":{"a":0,"k":[6.238,5.063,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":77,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":107,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":137,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":167,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":197,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":232,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":562,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"t":602,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-4.546,-0.421],[-5.988,1.021],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6.238,5.063],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"circle mask 2","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":".blue400","cl":"blue400","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,8.308,0],"ix":2,"l":2},"a":{"a":0,"k":[41.706,20.979,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[18.645,0],[0,18.645]],"o":[[0,18.645],[-18.644,0],[0,0]],"v":[[33.76,-16.88],[-0.001,16.88],[-33.76,-16.88]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,17.13],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[22.896,0],[0,22.896]],"o":[[0,22.896],[-22.896,0],[0,0]],"v":[[41.457,-20.729],[-0.001,20.729],[-41.457,-20.729]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,20.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle mask 4","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":15,"ty":1,"nm":".grey900","cl":"grey900","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,66,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[52,52,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#202124","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"circle mask 5","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":17,"ty":1,"nm":".black","cl":"black","parent":2,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-17.333,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[72,72,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#000000","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":".grey800","cl":"grey800","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[-192.25,99.933,0],"to":[5,3.333,0],"ti":[-5,-3.333,0]},{"t":-48,"s":[-162.25,119.933,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-163,100.85,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-108,"s":[100,100,100]},{"t":-48,"s":[59,59,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":".grey900","cl":"grey900","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[0,18.167,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[0,10.667,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-171,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-141,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.512],[0,0.512],[3,3.512]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-111,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"t":-81,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.967],[0,0.967],[3,3.967]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-199,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 4","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-116.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":-199,"s":[71,-101.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":365,"s":[71,-101.083,0],"to":[0,0,0],"ti":[16.833,-14.361,0]},{"t":405,"s":[-30,-14.917,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-239,"s":[29,29]},{"i":{"x":[0.833,0.833],"y":[1,0.833]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-199,"s":[29,38]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":365,"s":[29,36]},{"t":405,"s":[83,83]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":365,"s":[50]},{"t":405,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":".grey900","cl":"grey900","parent":1,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-82.917,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[71,-90.417,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":-199,"st":-255,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"device frame mask","parent":24,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,1.167,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":".blue400","cl":"blue400","parent":18,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[100.25,-115.167,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-199,"s":[100.25,-100.167,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":-159,"s":[100.25,-105.667,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":365,"s":[100.25,-100.167,0],"to":[0,0,0],"ti":[16.833,-14.361,0]},{"t":405,"s":[-0.75,-14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-239,"s":[29,29]},{"i":{"x":[0.833,0.833],"y":[1,0.833]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-199,"s":[29,38]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":365,"s":[29,36]},{"t":405,"s":[83,83]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":365,"s":[50]},{"t":405,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":24,"ty":3,"nm":"device frame mask 5","parent":18,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":-165,"op":6.00000000000001,"st":-271,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"device frame mask 9","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-181,"op":-62,"st":-181,"bm":0},{"ddd":0,"ind":29,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-145,"s":[50]},{"t":-75,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-165,"s":[0,0]},{"t":-75,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[50]},{"t":113,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-181,"op":-62,"st":-181,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"device frame mask 8","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-211,"op":-92,"st":-211,"bm":0},{"ddd":0,"ind":31,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-165,"s":[50]},{"t":-95,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-195,"s":[0,0]},{"t":-105,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":43,"s":[50]},{"t":83,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-211,"op":-92,"st":-211,"bm":0},{"ddd":0,"ind":32,"ty":4,"nm":"device frame mask 7","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-241,"op":-122,"st":-241,"bm":0},{"ddd":0,"ind":33,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-195,"s":[50]},{"t":-125,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-225,"s":[0,0]},{"t":-135,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":13,"s":[50]},{"t":53,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-241,"op":-122,"st":-241,"bm":0},{"ddd":0,"ind":34,"ty":4,"nm":"device frame mask 6","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-29.25,-0.917,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-271,"op":-152,"st":-271,"bm":0},{"ddd":0,"ind":35,"ty":4,"nm":".blue400","cl":"blue400","parent":23,"tt":1,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":-225,"s":[50]},{"t":-155,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-255,"s":[0,0]},{"t":-165,"s":[94,94]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-17,"s":[50]},{"t":23,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4,0.61568627451,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-271,"op":-152,"st":-271,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
diff --git a/packages/SystemUI/res/raw/biometricprompt_folded_base_topleft.json b/packages/SystemUI/res/raw/biometricprompt_folded_base_topleft.json
new file mode 100644
index 0000000..7351d7c
--- /dev/null
+++ b/packages/SystemUI/res/raw/biometricprompt_folded_base_topleft.json
@@ -0,0 +1 @@
+{"v":"5.9.0","fr":60,"ip":0,"op":21,"w":340,"h":340,"nm":"BiometricPrompt_Portrait_Base_TopLeft","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":6,"ty":3,"nm":"Null 16","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[170,170,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":3,"nm":"Null_Circle","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[70.333,-88.75,0],"to":[-11.722,17.639,0],"ti":[11.722,-17.639,0]},{"t":-48,"s":[0,17.083,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".grey600","cl":"grey600","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"circle mask 3","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Finger_Flipped","parent":6,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[-24.98,-35.709,0],"ix":2,"l":2},"a":{"a":0,"k":[31.791,75.23,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[5.03,5.25],[-2.83,8.98],[-5.59,-0.26],[2.52,-11.02]],"o":[[-2.85,12.77],[2.07,-14.96],[1.9,-6],[1.4,8.05],[0,0]],"v":[[7.5,4.99],[-10.09,19.69],[-3.59,-16.61],[8.69,-24.92],[7.5,5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.760784373564,0.478431402468,0.400000029919,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.8,24.94],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-7.01,22.23],[-1.2,-27.39],[4.09,-26.79],[15.73,14.18]],"o":[[5.64,-17.93],[2.45,56.06],[-22.4,-1.77],[17.73,-51.82]],"v":[[-7.57,-66.9],[30.82,-44.76],[26.65,75.23],[-31.78,50.08]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431372549,0.403921598547,0.305882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[31.79,75.23],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":900,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"circle mask 7","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":".grey600","cl":"grey600","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.25,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.501960784314,0.525490196078,0.545098039216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":36.9,"ix":2},"o":{"a":0,"k":114.2,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"circle mask","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":".grey900","cl":"grey900","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.5,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[114.218,-17.096],[-112.938,-17.096]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"circle mask 6","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":".grey900","cl":"grey900","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":377,"s":[-180]},{"t":417,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":377,"s":[-1.137,1.771,0],"to":[0.375,0,0],"ti":[-0.375,0,0]},{"t":417,"s":[1.113,1.771,0]}],"ix":2,"l":2},"a":{"a":0,"k":[6.238,5.063,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":77,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":107,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":137,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":167,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":197,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,-4.637],[-10.23,-3.195],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.7,"y":0},"t":232,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":562,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-8.788,0.393],[-10.23,1.835],[-2.196,9.843],[5.988,1.659],[4.545,0.217],[-2.196,6.948]],"c":false}]},{"t":602,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-4.546,-0.421],[-5.988,1.021],[-2.196,4.813],[5.988,-3.371],[4.545,-4.813],[-2.196,1.918]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6.238,5.063],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"circle mask 2","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":".blue400","cl":"blue400","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,8.308,0],"ix":2,"l":2},"a":{"a":0,"k":[41.706,20.979,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[18.645,0],[0,18.645]],"o":[[0,18.645],[-18.644,0],[0,0]],"v":[[33.76,-16.88],[-0.001,16.88],[-33.76,-16.88]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,17.13],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[22.896,0],[0,22.896]],"o":[[0,22.896],[-22.896,0],[0,0]],"v":[[41.457,-20.729],[-0.001,20.729],[-41.457,-20.729]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.706,20.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"circle mask 4","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":20,"ty":1,"nm":".grey900","cl":"grey900","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,66,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[52,52,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#202124","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"circle mask 5","parent":7,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.333,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-108,"s":[0,0]},{"t":-48,"s":[202,202]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.250980392157,0.282352941176,0.294117647059,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.078246000701,0.610494037703,0.787910970052,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-17.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":22,"ty":1,"nm":".black","cl":"black","parent":7,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-17.333,0],"ix":2,"l":2},"a":{"a":0,"k":[206,150,0],"ix":1,"l":2},"s":{"a":0,"k":[72,72,100],"ix":6,"l":2}},"ao":0,"sw":412,"sh":300,"sc":"#000000","ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":".grey800","cl":"grey800","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-108,"s":[-192.25,99.933,0],"to":[5,3.333,0],"ti":[-5,-3.333,0]},{"t":-48,"s":[-162.25,119.933,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-163,100.85,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.2,0.2,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.167],"y":[0,0,0]},"t":-108,"s":[100,100,100]},{"t":-48,"s":[59,59,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[326,201.699],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":8,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.235294117647,0.250980392157,0.262745098039,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":".grey900","cl":"grey900","parent":23,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[100.25,-87.156,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[100.25,-94.656,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-171,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-141,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.512],[0,0.512],[3,3.512]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-111,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]},{"t":-81,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,3.967],[0,0.967],[3,3.967]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-199,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Shape Layer 4","parent":6,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-116.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":-199,"s":[71,-101.083,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":365,"s":[71,-101.083,0],"to":[0,0,0],"ti":[16.833,-14.361,0]},{"t":405,"s":[-30,-14.917,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-239,"s":[29,29]},{"i":{"x":[0.833,0.833],"y":[1,0.833]},"o":{"x":[0.7,0.7],"y":[0,0]},"t":-199,"s":[29,38]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":365,"s":[29,36]},{"t":405,"s":[83,83]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":365,"s":[50]},{"t":405,"s":[50]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.400000029919,0.61568627451,0.964705942191,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":645,"st":-255,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":".grey900","cl":"grey900","parent":6,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[71,-82.917,0],"to":[0,-1.25,0],"ti":[0,1.25,0]},{"t":-199,"s":[71,-90.417,0]}],"ix":2,"l":2},"a":{"a":0,"k":[5.5,4,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":-239,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.07,1.5],[0,-1.5],[-0.047,1.5]],"c":false}]},{"t":-199,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-3,1.5],[0,-1.5],[3,1.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.129411764706,0.141176470588,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[5.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-255,"op":-199,"st":-255,"bm":0}],"markers":[{"tm":255,"cm":"","dr":0},{"tm":364,"cm":"","dr":0},{"tm":482,"cm":"","dr":0},{"tm":600,"cm":"","dr":0}]}
diff --git a/packages/SystemUI/res/values-af/strings.xml b/packages/SystemUI/res/values-af/strings.xml
index 3dc85d70..afa9818 100644
--- a/packages/SystemUI/res/values-af/strings.xml
+++ b/packages/SystemUI/res/values-af/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Kan nie gesig herken nie"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Gebruik eerder vingerafdruk"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth gekoppel."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batterypersentasie is onbekend."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Gekoppel aan <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Helderheid"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Kleuromkering"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Kleurregstelling"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Gebruikerinstellings"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Bestuur gebruikers"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Klaar"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Maak toe"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Gekoppel"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofoon beskikbaar"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera beskikbaar"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofoon en kamera beskikbaar"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Ander toestel"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Wissel oorsig"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Jy sal nie deur geluide en vibrasies gepla word nie, behalwe deur wekkers, herinneringe, geleenthede en bellers wat jy spesifiseer. Jy sal steeds enigiets hoor wat jy kies om te speel, insluitend musiek, video\'s en speletjies."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Wanneer jy ’n program deel, opneem of uitsaai, het <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> toegang tot enigiets wat in daardie program sigbaar is of daarin gespeel word. Wees dus versigtig met wagwoorde, betalingbesonderhede, boodskappe of ander sensitiewe inligting."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Gaan voort"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Deel of neem ’n program op"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Vee alles uit"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Bestuur"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Geskiedenis"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Skakel mobiele data af?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Jy sal nie deur <xliff:g id="CARRIER">%s</xliff:g> toegang tot data of die internet hê nie. Internet sal net deur Wi-Fi beskikbaar wees."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"jou diensverskaffer"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Skakel weer oor na <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobiele data sal nie outomaties op grond van beskikbaarheid oorskakel nie"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nee, dankie"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, skakel oor"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Instellings kan nie jou antwoord verifieer nie omdat \'n program \'n toestemmingversoek verberg."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Laat <xliff:g id="APP_0">%1$s</xliff:g> toe om <xliff:g id="APP_2">%2$s</xliff:g>-skyfies te wys?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Dit kan inligting van <xliff:g id="APP">%1$s</xliff:g> af lees"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Vergrootglasvensterinstellings"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tik om toeganklikheidkenmerke oop te maak Pasmaak of vervang knoppie in Instellings.\n\n"<annotation id="link">"Bekyk instellings"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Skuif knoppie na kant om dit tydelik te versteek"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Ontdoen"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} kortpad is verwyder}other{# kortpaaie is verwyder}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Beweeg na links bo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Beweeg na regs bo"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Beweeg na links onder"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Beweeg na regs onder"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Beweeg na rand en versteek"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Beweeg weg van rand en wys"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Verwyder"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"wissel"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Toestelkontroles"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Kies program om kontroles by te voeg"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiele data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Gekoppel"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tydelik gekoppel"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Swak verbinding"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobiele data sal nie outomaties koppel nie"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Geen verbinding nie"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Geen ander netwerke beskikbaar nie"</string>
diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml
index fb60f9b..5c4e766 100644
--- a/packages/SystemUI/res/values-am/strings.xml
+++ b/packages/SystemUI/res/values-am/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"መልክን መለየት አልተቻለም"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"በምትኩ የጣት አሻራን ይጠቀሙ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ብሉቱዝ ተያይዟል።"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"የባትሪ መቶኛ አይታወቅም።"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"ከ<xliff:g id="BLUETOOTH">%s</xliff:g> ጋር ተገናኝቷል።"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ብሩህነት"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ተቃራኒ ቀለም"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"የቀለም ማስተካከያ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"የተጠቃሚ ቅንብሮች"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ተጠቃሚዎችን ያስተዳድሩ"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ተከናውኗል"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ዝጋ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ተገናኝቷል"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ማይክሮፎን አለ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ካሜራ አለ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ማይክሮፎን እና ካሜራ አለ"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"ማይክሮፎን በርቷል"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"ማይክሮፎን ጠፍቷል"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"ማይክሮፎን ለሁሉም መተግበሪያዎች እና አገልግሎቶች ነቅቷል።"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"የማይክሮፎን መዳረሻ ለሁሉም መተግበሪያዎች እና አገልግሎቶች ተሰናክሏል። የማይክሮፎን መዳረሻን በቅንብሮች &gt; ግላዊነት &gt; ማይክሮፎን ውስጥ ማንቃት ይችላሉ።"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"የማይክሮፎን መዳረሻ ለሁሉም መተግበሪያዎች እና አገልግሎቶች ተሰናክሏል። ይህን በቅንብሮች &gt; ግላዊነት &gt; ማይክሮፎን ውስጥ መቀየር ይችላሉ።"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ካሜራ በርቷል"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ካሜራ ጠፍቷል"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"ካሜራ ለሁሉም መተግበሪያዎች እና አገልግሎቶች ነቅቷል።"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"የካሜራ መዳረሻ ለሁሉም መተግበሪያዎች እና አገልግሎቶች ተሰናክሏል።"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"የማይክሮፎን አዝራርን ለመጠቀም በቅንብሮች ውስጥ የማይክሮፎን መዳረሻን ያንቁ።"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ቅንብሮችን ክፈት።"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ሌላ መሣሪያ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"አጠቃላይ እይታን ቀያይር"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"እርስዎ ከወሰንዋቸው ማንቂያዎች፣ አስታዋሾች፣ ክስተቶች እና ደዋዮች በስተቀር፣ በድምጾች እና ንዝረቶች አይረበሹም። ሙዚቃ፣ ቪዲዮዎች እና ጨዋታዎች ጨምሮ ለመጫወት የሚመርጡትን ማንኛውም ነገር አሁንም ይሰማሉ።"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"አንድን መተግበሪያ ሲያጋሩ፣ ሲቀርጹ ወይም cast ሲያደርጉ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> በዚያ መተግበሪያ ላይ ለሚታይ ወይም ለሚጫወት ማንኛውም ነገር መዳረሻ አለው። ስለዚህ በይለፍ ቃላት፣ በክፍያ ዝርዝሮች፣ በመልዕክቶች ወይም በሌሎች ልዩ ጥንቃቄ የሚያስፈልጋቸው መረጃዎች ላይ ጥንቃቄ ያድርጉ።"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ቀጥል"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"መተግበሪያ ያጋሩ ወይም ይቅረጹ"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ሁሉንም አጽዳ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ያቀናብሩ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ታሪክ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"የተንቀሳቃሽ ስልክ ውሂብ ይጥፋ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"በ<xliff:g id="CARRIER">%s</xliff:g> በኩል የውሂብ ወይም የበይነመረቡ መዳረሻ አይኖረዎትም። በይነመረብ በWi-Fi በኩል ብቻ ነው የሚገኝ የሚሆነው።"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"የእርስዎ አገልግሎት አቅራቢ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"ወደ <xliff:g id="CARRIER">%s</xliff:g> ተመልሶ ይቀየር?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"የተንቀሳቃሽ ስልክ ውሂብ በተገኝነት መሰረት በራስ ሰር አይቀይርም"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"አይ አመሰግናለሁ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"አዎ፣ ቀይር"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"አንድ መተግበሪያ የፍቃድ ጥያቄ እያገደ ስለሆነ ቅንብሮች ጥያቄዎን ማረጋገጥ አይችሉም።"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> የ<xliff:g id="APP_2">%2$s</xliff:g> ቁራጮችን እንዲያሳይ ይፈቀድለት?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ከ<xliff:g id="APP">%1$s</xliff:g> የመጣ መረጃን ማንበብ ይችላል"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"የማጉያ መስኮት ቅንብሮች"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"የተደራሽነት ባህሪያትን ለመክፈት መታ ያድርጉ። ይህንን አዝራር በቅንብሮች ውስጥ ያብጁ ወይም ይተኩ።\n\n"<annotation id="link">"ቅንብሮችን አሳይ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ለጊዜው ለመደበቅ አዝራሩን ወደ ጠርዝ ያንቀሳቅሱ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ቀልብስ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} አቋራጭ ተወግዷል}one{# አቋራጭ ተወግዷል}other{# አቋራጮች ተወግደዋል}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ወደ ላይኛው ግራ አንቀሳቅስ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ወደ ላይኛው ቀኝ አንቀሳቅስ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"የግርጌውን ግራ አንቀሳቅስ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ታችኛውን ቀኝ አንቀሳቅስ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ወደ ጠርዝ አንቀሳቅስ እና ደደብቅ"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ጠርዙን ወደ ውጭ አንቀሳቅስ እና አሳይ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"አስወግድ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ቀያይር"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"የመሣሪያ መቆጣጠሪያዎች"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"መቆጣጠሪያዎችን ለማከል መተግበሪያ ይምረጡ"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"የተንቀሳቃሽ ስልክ ውሂብ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ተገናኝቷል"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"በጊዜያዊነት ተገናኝቷል"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ደካማ ግንኙነት"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"የተንቀሳቃሽ ስልክ ውሂብ በራስ-ሰር አይገናኝም"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ግንኙነት የለም"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ሌላ አውታረ መረብ የሉም"</string>
diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml
index f8567c7..f9b866d 100644
--- a/packages/SystemUI/res/values-ar/strings.xml
+++ b/packages/SystemUI/res/values-ar/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"يتعذّر التعرّف على الوجه."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"يمكنك استخدام بصمة إصبعك."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"تم توصيل البلوتوث."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"نسبة شحن البطارية غير معروفة."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"متصل بـ <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"السطوع"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"قلب الألوان"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"تصحيح الألوان"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"إعدادات المستخدم"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"إدارة المستخدمين"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"تم"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"إغلاق"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"متصل"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"يمكنك الوصول إلى الميكروفون الآن."</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"يمكنك الوصول إلى الكاميرا الآن."</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"يمكنك الوصول إلى الميكروفون والكاميرا الآن."</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"جهاز آخر"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"تبديل \"النظرة العامة\""</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"لن يتم إزعاجك بالأصوات والاهتزاز، باستثناء المُنبِّهات والتذكيرات والأحداث والمتصلين الذين تحددهم. وسيظل بإمكانك سماع أي عناصر أخرى تختار تشغيلها، بما في ذلك الموسيقى والفيديوهات والألعاب."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"أثناء مشاركة محتوى تطبيق أو تسجيله أو بثه، يمكن لتطبيق <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> الوصول إلى كل العناصر المعروضة أو التي يتم تشغيلها في ذلك التطبيق، لذا يُرجى توخي الحذر بشأن كلمات المرور أو تفاصيل الدفع أو الرسائل أو المعلومات الحساسة الأخرى."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"متابعة"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"مشاركة محتوى تطبيق أو تسجيله"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"محو الكل"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"إدارة"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"السجلّ"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"هل تريد إيقاف بيانات الجوّال؟"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"‏لن تتمكّن من استخدام البيانات أو الإنترنت من خلال <xliff:g id="CARRIER">%s</xliff:g>. ولن يتوفر اتصال الإنترنت إلا عبر Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"مشغّل شبكة الجوّال"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"هل تريد التبديل مرة أخرى إلى \"<xliff:g id="CARRIER">%s</xliff:g>\"؟"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"لن يتم تلقائيًا تبديل بيانات الجوّال بناءً على التوفّر."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"لا، شكرًا"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"نعم، أريد التبديل"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"لا يمكن للإعدادات التحقق من ردك لأن هناك تطبيقًا يحجب طلب الإذن."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"هل تريد السماح لتطبيق <xliff:g id="APP_0">%1$s</xliff:g> بعرض شرائح <xliff:g id="APP_2">%2$s</xliff:g>؟"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- يستطيع قراءة المعلومات من <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"إعدادات نافذة مكبّر الشاشة"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"انقر لفتح ميزات تسهيل الاستخدام. يمكنك تخصيص هذا الزر أو استبداله من الإعدادات.\n\n"<annotation id="link">"عرض الإعدادات"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"يمكنك نقل الزر إلى الحافة لإخفائه مؤقتًا."</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"تراجع"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{تمت إزالة اختصار واحد ({label}).}zero{تمت إزالة # اختصار.}two{تمت إزالة اختصارَين.}few{تمت إزالة # اختصارات.}many{تمت إزالة # اختصارًا.}other{تمت إزالة # اختصار.}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"نقل إلى أعلى يمين الشاشة"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"نقل إلى أعلى يسار الشاشة"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"نقل إلى أسفل يمين الشاشة"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"نقل إلى أسفل يسار الشاشة"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"نقله إلى الحافة وإخفاؤه"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"نقله إلى خارج الحافة وإظهاره"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"إزالة"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"إيقاف/تفعيل"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"التحكم بالجهاز"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"اختيار تطبيق لإضافة عناصر التحكّم"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"بيانات الجوّال"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"متصلة بالإنترنت"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"متصلة مؤقتًا"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"الاتصال ضعيف"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"لن يتم تلقائيًا الاتصال ببيانات الجوّال."</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"لا يتوفّر اتصال بالإنترنت"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"لا تتوفّر شبكات أخرى."</string>
diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml
index 4fca5f2e..a341820 100644
--- a/packages/SystemUI/res/values-as/strings.xml
+++ b/packages/SystemUI/res/values-as/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"মুখাৱয়ব চিনিব নোৱাৰি"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ইয়াৰ সলনি ফিংগাৰপ্ৰিণ্ট ব্যৱহাৰ কৰক"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ব্লুটুথ সংযোগ হ’ল।"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"বেটাৰীৰ চাৰ্জৰ শতাংশ অজ্ঞাত।"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>ৰ লগত সংযোগ কৰা হ’ল।"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"উজ্জ্বলতা"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ৰং বিপৰীতকৰণ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ৰং শুধৰণী"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ব্যৱহাৰকাৰীৰ ছেটিং"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ব্যৱহাৰকাৰী পৰিচালনা কৰক"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"সম্পন্ন কৰা হ’ল"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"বন্ধ কৰক"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"সংযোগ কৰা হ’ল"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"মাইক্ৰ’ফ’ন উপলব্ধ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"কেমেৰা উপলব্ধ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"মাইক্ৰ’ফ’ন আৰু কেমেৰা উপলব্ধ"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"মাইক্ৰ’ফ’ন অন কৰা হ’ল"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"মাইক্ৰ’ফ’ন অফ কৰা হ’ল"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"আটাইবোৰ এপ্ আৰু সেৱাৰ বাবে মাইক্ৰ’ফ’ন সক্ষম কৰা হৈছে।"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"আটাইবোৰ এপ্ আৰু সেৱাৰ বাবে মাইক্ৰ’ফ’নৰ এক্সেছ অক্ষম কৰা হৈছে। আপুনি ছেটিং &gt; গোপনীয়তা &gt; মাইক্ৰ’ফ’নত মাইক্ৰ’ফ’নৰ এক্সেছ সক্ষম কৰিব পাৰে।"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"আটাইবোৰ এপ্ আৰু সেৱাৰ বাবে মাইক্ৰ’ফ’নৰ এক্সেছ অক্ষম কৰা হৈছে। আপুনি এইটো ছেটিং &gt; গোপনীয়তা &gt; মাইক্ৰ’ফ’নত সলনি কৰিব পাৰে।"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"কেমেৰা অন কৰা হ’ল"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"কেমেৰা অফ কৰা হ’ল"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"আটাইবোৰ এপ্ আৰু সেৱাৰ বাবে কেমেৰা সক্ষম কৰা হৈছে।"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"আটাইবোৰ এপ্ আৰু সেৱাৰ বাবে কেমেৰাৰ এক্সেছ অক্ষম কৰা হৈছে।"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"মাইক্ৰ’ফ’নৰ বুটামটো ব্যৱহাৰ কৰিবলৈ, ছেটিঙত মাইক্ৰ’ফ’নৰ এক্সেছ সক্ষম কৰক।"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ছেটিং খোলক।"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"অন্য ডিভাইচ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"অৱলোকন ট’গল কৰক"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"আপুনি নিৰ্দিষ্ট কৰা এলাৰ্ম, ৰিমাইণ্ডাৰ, ইভেন্ট আৰু কল কৰোঁতাৰ বাহিৰে আন কোনো শব্দৰ পৰা আপুনি অসুবিধা নাপাব। কিন্তু, সংগীত, ভিডিঅ\' আৰু খেলসমূহকে ধৰি আপুনি প্লে কৰিব খোজা যিকোনো বস্তু তথাপি শুনিব পাৰিব।"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"আপুনি শ্বেয়াৰ কৰা, ৰেকৰ্ড কৰা অথবা কাষ্ট কৰাৰ সময়ত, সেইটো এপত দৃশ্যমান যিকোনো বস্তু অথবা আপোনাৰ ডিভাইচত প্লে’ কৰা যিকোনো সমললৈ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>ৰ এক্সেছ থাকে। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা অথবা অন্য সংবেদনশীল তথ্যৰ ক্ষেত্ৰত সাৱধান হওক।"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"অব্যাহত ৰাখক"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"এটা এপ্ শ্বেয়াৰ অথবা ৰেকৰ্ড কৰক"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"আটাইবোৰ মচক"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"পৰিচালনা"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ম’বাইল ডেটা অফ কৰিবনে?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"আপুনি <xliff:g id="CARRIER">%s</xliff:g>ৰ জৰিয়তে ডেটা সংযোগ বা ইণ্টাৰনেট সংযোগ নাপাব। কেৱল ৱাই-ফাইৰ যোগেৰে ইণ্টাৰনেট উপলব্ধ হ\'ব।"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"আপোনাৰ বাহক"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"আকৌ <xliff:g id="CARRIER">%s</xliff:g>লৈ সলনি কৰিবনে?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ম’বাইলৰ ডেটা উপলব্ধতাৰ ওপৰত ভিত্তি কৰি স্বয়ংক্ৰিয়ভাৱে সলনি কৰা নহ’ব"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"নালাগে, ধন্যবাদ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"হয়, সলনি কৰক"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"এটা এপে অনুমতি বিচাৰি কৰা অনুৰোধ এটা ঢাকি ধৰা বাবে ছেটিঙৰ পৰা আপোনাৰ উত্তৰ সত্যাপন কৰিব পৰা নাই।"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g>ক <xliff:g id="APP_2">%2$s</xliff:g>ৰ অংশ দেখুওৱাবলৈ অনুমতি দিবনে?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ই <xliff:g id="APP">%1$s</xliff:g>ৰ তথ্য পঢ়িব পাৰে"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"বিবৰ্ধকৰ ৱিণ্ড’ৰ ছেটিং"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"সাধ্য সুবিধাসমূহ খুলিবলৈ টিপক। ছেটিঙত এই বুটামটো কাষ্টমাইজ অথবা সলনি কৰক।\n\n"<annotation id="link">"ছেটিং চাওক"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"বুটামটোক সাময়িকভাৱে লুকুৱাবলৈ ইয়াক একেবাৰে কাষলৈ লৈ যাওক"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"আনডু কৰক"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} টা শ্বৰ্টকাট আঁতৰোৱা হ’ল}one{# টা শ্বৰ্টকাট আঁতৰোৱা হ’ল}other{# টা শ্বৰ্টকাট আঁতৰোৱা হ’ল}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"শীৰ্ষৰ বাওঁফালে নিয়ক"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"শীৰ্ষৰ সোঁফালে নিয়ক"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"তলৰ বাওঁফালে নিয়ক"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"তলৰ সোঁফালে নিয়ক"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"কাষলৈ নিয়ক আৰু লুকুৱাওক"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"কাষৰ বাহিৰলৈ নিয়ক আৰু দেখুৱাওক"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"আঁতৰাওক"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ট’গল কৰক"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ডিভাইচৰ নিয়ন্ত্ৰণসমূহ"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"নিয়ন্ত্ৰণসমূহ যোগ কৰিবলৈ এপ্‌ বাছনি কৰক"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ম’বাইল ডেটা"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"সংযোজিত হৈ আছে"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"অস্থায়ীভাৱে সংযোগ কৰা হৈছে"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"বেয়া সংযোগ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ম’বাইল ডেটা স্বয়ংক্ৰিয়ভাৱে সংযুক্ত নহ’ব"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"সংযোগ নাই"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"অন্য কোনো নেটৱৰ্ক উপলব্ধ নহয়"</string>
diff --git a/packages/SystemUI/res/values-az/strings.xml b/packages/SystemUI/res/values-az/strings.xml
index 7fa603f..ed95d80 100644
--- a/packages/SystemUI/res/values-az/strings.xml
+++ b/packages/SystemUI/res/values-az/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Üzü tanımaq olmur"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Barmaq izi istifadə edin"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth qoşulub."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batareyanın faizi naməlumdur."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> üzərindən qoşuldu."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Parlaqlıq"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Rəng inversiyası"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Rəng korreksiyası"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"İstifadəçi ayarları"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"İstifadəçiləri idarə edin"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Hazır"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Bağlayın"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Qoşulu"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon əlçatandır"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera əlçatandır"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon və kamera əlçatandır"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon aktiv edilib"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon deaktiv edilib"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon bütün tətbiqlər və xidmətlər üçün aktiv edilib."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofon girişi bütün tətbiqlər və xidmətlər üçün deaktiv edilib. Mikrofon girişini Ayarlar &gt; Məxfilik &gt; Mikrofon bölməsində aktiv edə bilərsiniz."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofon girişi bütün tətbiqlər və xidmətlər üçün deaktiv edilib. Bunu Ayarlar &gt; Məxfilik &gt; Mikrofon bölməsində dəyişə bilərsiniz."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera aktivdir"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera deaktivdir"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera bütün tətbiqlər və xidmətlər üçün aktiv edilib."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kamera girişi bütün tətbiqlər və xidmətlər üçün deaktiv edilib."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Mikrofon düyməsini istifadə etmək üçün Ayarlarda mikrofona girişi aktiv edin."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Ayarları açın."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Digər cihaz"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"İcmala Keçin"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Seçdiyiniz siqnal, xatırladıcı, tədbir və zənglər istisna olmaqla səslər və vibrasiyalar Sizi narahat etməyəcək. Musiqi, video və oyunlar da daxil olmaqla oxutmaq istədiyiniz hər şeyi eşidəcəksiniz."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Paylaşdığınız, qeydə aldığınız və ya yayımladığınız zaman <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tətbiqi həmin tətbiqdə göstərilən və ya oxudulan hər şeyə giriş edə bilir. Odur ki, parollar, ödəniş detalları, mesajlar və ya digər həssas məlumatlarla bağlı diqqətli olun."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Davam edin"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Tətbiqi paylaşın və ya qeydə alın"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Hamısını silin"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"İdarə edin"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Tarixçə"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Mobil data söndürülsün?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> ilə data və ya internetə daxil ola bilməyəcəksiniz. İnternet yalnız Wi-Fi ilə əlçatan olacaq."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operatorunuz"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> operatoruna keçirilsin?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobil data əlçatımlıq əsasında avtomatik olaraq keçirilməyəcək"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Xeyr, təşəkkürlər"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Bəli, keçirin"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Tətbiq icazə sorğusunu gizlətdiyi üçün Ayarlar cavabınızı doğrulaya bilməz."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> tətbiqinə <xliff:g id="APP_2">%2$s</xliff:g> hissələrini göstərmək üçün icazə verilsin?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- <xliff:g id="APP">%1$s</xliff:g> tətbiqindən məlumat oxuya bilər"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Böyüdücü pəncərə ayarları"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Əlçatımlılıq funksiyalarını açmaq üçün toxunun. Ayarlarda bu düyməni fərdiləşdirin və ya dəyişdirin.\n\n"<annotation id="link">"Ayarlara baxın"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Düyməni müvəqqəti gizlətmək üçün kənara çəkin"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Geri qaytarın"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} qısayol silindi}other{# qısayol silindi}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Yuxarıya sola köçürün"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Yuxarıya sağa köçürün"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Aşağıya sola köçürün"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Aşağıya sağa köçürün"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"İçəri keçirib gizlədin"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Kənara daşıyıb göstərin"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Silin"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"keçirin"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Cihaz kontrolları"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Kontrol əlavə etmək üçün tətbiq seçin"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobil data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Qoşulub"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Müvəqqəti qoşulub"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Zəif bağlantı"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobil data avtomatik qoşulmayacaq"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Bağlantı yoxdur"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Heç bir başqa şəbəkə əlçatan deyil"</string>
diff --git a/packages/SystemUI/res/values-b+sr+Latn/strings.xml b/packages/SystemUI/res/values-b+sr+Latn/strings.xml
index ffe49b2..a2cbce79 100644
--- a/packages/SystemUI/res/values-b+sr+Latn/strings.xml
+++ b/packages/SystemUI/res/values-b+sr+Latn/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Lice nije prepoznato"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Koristite otisak prsta"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth je priključen."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Procenat napunjenosti baterije nije poznat."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Povezani ste sa <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Osvetljenost"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inverzija boja"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Korekcija boja"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Korisnička podešavanja"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Upravljajte korisnicima"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Gotovo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zatvori"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Povezan"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon je dostupan"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera je dostupna"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon i kamera su dostupni"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Drugi uređaj"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Uključi/isključi pregled"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Neće vas uznemiravati zvukovi i vibracije osim za alarme, podsetnike, događaje i pozivaoce koje navedete. I dalje ćete čuti sve što odaberete da pustite, uključujući muziku, video snimke i igre."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kada delite, snimate ili prebacujete aplikaciju, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ima pristup kompletnom sadržaju koji je vidljiv ili se pušta u toj aplikaciji. Budite pažljivi sa lozinkama, informacijama o plaćanju, porukama ili drugim osetljivim informacijama."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Nastavi"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Delite ili snimite aplikaciju"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Obriši sve"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljajte"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Istorija"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Želite da isključite mobilne podatke?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nećete imati pristup podacima ili internetu preko mobilnog operatera <xliff:g id="CARRIER">%s</xliff:g>. Internet će biti dostupan samo preko WiFi veze."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"mobilni operater"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Želite da se vratite na mobilnog operatera <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilni podaci se neće automatski promeniti na osnovu dostupnosti"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, hvala"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Da, pređi"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Podešavanja ne mogu da verifikuju vaš odgovor jer aplikacija skriva zahtev za dozvolu."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Želite li da dozvolite aplikaciji <xliff:g id="APP_0">%1$s</xliff:g> da prikazuje isečke iz aplikacije <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Može da čita podatke iz aplikacije <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Podešavanja prozora za uvećanje"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Dodirnite za funkcije pristupačnosti. Prilagodite ili zamenite ovo dugme u Podešavanjima.\n\n"<annotation id="link">"Podešavanja"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Pomerite dugme do ivice da biste ga privremeno sakrili"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Opozovite"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} prečica je uklonjena}one{# prečica je uklonjena}few{# prečice su uklonjene}other{# prečica je uklonjeno}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Premesti gore levo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Premesti gore desno"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Premesti dole levo"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Premesti dole desno"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Premesti do ivice i sakrij"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Premesti izvan ivice i prikaži"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Uklonite"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"uključite/isključite"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrole uređaja"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Odaberite aplikaciju za dodavanje kontrola"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilni podaci"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Povezano"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Privremeno povezano"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Veza je loša"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Nije uspelo autom. povezivanje preko mob. podataka"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Veza nije uspostavljena"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nije dostupna nijedna druga mreža"</string>
diff --git a/packages/SystemUI/res/values-be/strings.xml b/packages/SystemUI/res/values-be/strings.xml
index 3e76985..3d05c86 100644
--- a/packages/SystemUI/res/values-be/strings.xml
+++ b/packages/SystemUI/res/values-be/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Твар не распазнаны"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Скарыстайце адбітак пальца"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth-сувязь."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Працэнт зараду акумулятара невядомы."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Падлучаны да <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Яркасць"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Інверсія колераў"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Карэкцыя колераў"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Налады карыстальніка"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Кіраваць карыстальнікамі"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Гатова"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Закрыць"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Падлучана"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Мікрафон можна выкарыстоўваць"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камеру можна выкарыстоўваць"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Мікрафон і камеру можна выкарыстоўваць"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Іншая прылада"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Уключыць/выключыць агляд"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Вас не будуць турбаваць гукі і вібрацыя, за выключэннем будзільнікаў, напамінаў, падзей і выбраных вамі абанентаў. Вы будзеце чуць усё, што ўключыце, у тым ліку музыку, відэа і гульні."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Калі пачынаецца абагульванне, запіс ці трансляцыя змесціва праграмы, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> атрымлівае доступ да ўсяго змесціва, якое паказваецца ці прайграецца ў праграме. Таму прадухіліце паказ пароляў, плацежных рэквізітаў, паведамленняў і іншай канфідэнцыяльнай інфармацыі."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Далей"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Абагульванне або запіс праграмы"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Ачысціць усё"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Кіраваць"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Гісторыя"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Выключыць мабільную перадачу даных?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"У вас не будзе доступу да даных ці інтэрнэту праз аператара <xliff:g id="CARRIER">%s</xliff:g>. Інтэрнэт будзе даступны толькі праз Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ваш аператар"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Зноў пераключыцца на аператара \"<xliff:g id="CARRIER">%s</xliff:g>\"?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мабільны інтэрнэт не будзе аўтаматычна пераключацца ў залежнасці ад даступнасці"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Не, дзякуй"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Так, пераключыцца"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Праграма хавае запыт на дазвол, таму ваш адказ немагчыма спраўдзіць у Наладах."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Дазволіць праграме <xliff:g id="APP_0">%1$s</xliff:g> паказваць зрэзы праграмы <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Можа счытваць інфармацыю з праграмы <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Налады акна лупы"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Націсніце, каб адкрыць спецыяльныя магчымасці. Рэгулюйце ці замяняйце кнопку ў Наладах.\n\n"<annotation id="link">"Прагляд налад"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Каб часова схаваць кнопку, перамясціце яе на край"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Адрабіць"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Выдалены {label} ярлык}one{Выдалены # ярлык}few{Выдалена # ярлыкі}many{Выдалена # ярлыкоў}other{Выдалена # ярлыка}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Перамясціць лявей і вышэй"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Перамясціць правей і вышэй"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Перамясціць лявей і ніжэй"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Перамясціць правей і ніжэй"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Перамясціць на край і схаваць"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Перамясціць за край і паказаць"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Выдаліць"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"уключыць/выключыць"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Элементы кіравання прыладай"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Выберыце праграму для дадавання элементаў кіравання"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мабільная перадача даных"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Падключана"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Падключана часова"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Нестабільнае падключэнне"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мабільная перадача даных не ўключаецца аўтаматычна"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Няма падключэння"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Больш няма даступных сетак"</string>
diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml
index 572e13f..7253b498 100644
--- a/packages/SystemUI/res/values-bg/strings.xml
+++ b/packages/SystemUI/res/values-bg/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Лицето не е разпознато"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Използвайте отпечатък"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth е включен."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Процентът на батерията е неизвестен."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Има връзка с <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Яркост"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Цветове: инверт."</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Корекция на цветове"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Потребителски настройки"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Управление на потребителите"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Готово"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Затваряне"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Установена е връзка"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофонът е налице"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камерата е налице"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофонът и камерата са налице"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофонът е включен"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофонът е изключен"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Достъпът до микрофона е активиран за всички приложения и услуги."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Достъпът до микрофона е деактивиран за всички приложения и услуги. Можете да го активирате от „Настройки &gt; Поверителност &gt; Микрофон“."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Достъпът до микрофона е деактивиран за всички приложения и услуги. Можете да промените това от „Настройки &gt; Поверителност &gt; Микрофон“."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камерата е включена"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камерата е изключена"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Достъпът до камерата е активиран за всички приложения и услуги."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Достъпът до камерата е деактивиран за всички приложения и услуги."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Активирайте достъпа до микрофона, за да използвате съответния бутон."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Отваряне на настройките."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Друго устройство"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Превключване на общия преглед"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Няма да бъдете обезпокоявани от звуци и вибрирания освен от будилници, напомняния, събития и обаждания от посочени от вас контакти. Пак ще чувате всичко, което изберете да се пусне, включително музика, видеоклипове и игри."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Когато споделяте, записвате или предавате, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> има достъп до всичко, което се показва или възпроизвежда в това приложение, затова бъдете внимателни с пароли, подробности за начини на плащане, съобщения или друга поверителна информация."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Напред"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Споделяне или записване на приложение"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Изчистване на всички"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Управление"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"История"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Да се изключат ли мобилните данни?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Няма да можете да използвате данни или интернет чрез <xliff:g id="CARRIER">%s</xliff:g>. Ще имате достъп до интернет само през Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"оператора си"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Искате ли да се върнете към <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мрежата за мобилни данни няма да се превключва автоматично въз основа на наличността"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Не, благодаря"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Да, превключване"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"От Настройки не може да се получи потвърждение за отговора ви, защото заявката за разрешение се прикрива от приложение."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Искате ли да разрешите на <xliff:g id="APP_0">%1$s</xliff:g> да показва части от <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Може да чете информация от <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Настройки за инструмента за увеличаване на прозорци"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Докоснете, за да отворите функциите за достъпност. Персон./заменете бутона от настройките.\n\n"<annotation id="link">"Преглед на настройките"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Преместете бутона до края, за да го скриете временно"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Отмяна"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} пряк път бе премахнат}other{# преки пътища бяха премахнати}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Преместване горе вляво"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Преместване горе вдясно"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Преместване долу вляво"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Преместване долу вдясно"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Преместване в края и скриване"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Преместване в края и показване"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Премахване"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"превключване"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Контроли за устройството"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Изберете приложение, за да добавите контроли"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобилни данни"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Свързано"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Установена е временна връзка"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Слаба връзка"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Връзката за мобилни данни няма да е автоматична"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Няма връзка"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Няма други налични мрежи"</string>
diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml
index 645f6ab..915d0eb 100644
--- a/packages/SystemUI/res/values-bn/strings.xml
+++ b/packages/SystemUI/res/values-bn/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ফেস শনাক্ত করা যায়নি"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"পরিবর্তে ফিঙ্গারপ্রিন্ট ব্যবহার করুন"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ব্লুটুথ সংযুক্ত হয়েছে৷"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ব্যাটারি কত শতাংশ আছে তা জানা যায়নি।"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>এ সংযুক্ত হয়ে আছে।"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"উজ্জ্বলতা"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"কালার ইনভার্সন"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"রঙ সংশোধন"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ব্যবহারকারী সেটিংস"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ব্যবহারকারীদের ম্যানেজ করুন"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"সম্পন্ন হয়েছে"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"বন্ধ করুন"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"সংযুক্ত হয়েছে"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"মাইক্রোফোন উপলভ্য"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ক্যামেরা উপলভ্য"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"মাইক্রোফোন ও ক্যামেরা উপলভ্য"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"মাইক্রোফোন চালু করা আছে"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"মাইক্রোফোন বন্ধ করা আছে"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"সব অ্যাপ ও পরিষেবার জন্য মাইক্রোফোনের অ্যাক্সেস চালু করা হয়েছে।"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"সব অ্যাপ ও পরিষেবার জন্য মাইক্রোফোনের অ্যাক্সেস বন্ধ করা হয়েছে। \'সেটিংস &gt; গোপনীয়তা &gt; মাইক্রোফোন\' বিকল্প থেকে আপনি মাইক্রোফোনের অ্যাক্সেস চালু করতে পারবেন।"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"সব অ্যাপ ও পরিষেবার জন্য মাইক্রোফোনের অ্যাক্সেস বন্ধ করা হয়েছে। \'সেটিংস &gt; গোপনীয়তা &gt; মাইক্রোফোন\' বিকল্প থেকে আপনি এটি পরিবর্তন করতে পারবেন।"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ক্যামেরা চালু করা হয়েছে"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ক্যামেরা বন্ধ করা হয়েছে"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"সব অ্যাপ ও পরিষেবার জন্য ক্যামেরার অ্যাক্সেস চালু করা হয়েছে।"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"সব অ্যাপ ও পরিষেবার জন্য ক্যামেরার অ্যাক্সেস বন্ধ করা হয়েছে।"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"মাইক্রোফোনের বোতাম ব্যবহার করতে, সেটিংস থেকে মাইক্রোফোনের অ্যাক্সেস চালু করুন।"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"সেটিংস খুলুন।"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"অন্য ডিভাইস"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"\'এক নজরে\' বৈশিষ্ট্যটি চালু বা বন্ধ করুন"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"অ্যালার্ম, রিমাইন্ডার, ইভেন্ট, এবং আপনার নির্দিষ্ট করে দেওয়া ব্যক্তিদের কল ছাড়া অন্য কোনও আওয়াজ বা ভাইব্রেশন হবে না। তবে সঙ্গীত, ভিডিও, এবং গেম সহ আপনি যা কিছু চালাবেন তার আওয়াজ শুনতে পাবেন।"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"কোনও অ্যাপ আপনার শেয়ার করা, রেকর্ড করা বা কাস্ট করার সময়, সেই অ্যাপে দেখা যায় বা খেলা হয় এমন সব কিছু অ্যাক্সেস করার অনুমতি <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>-এর আছে। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ বা অন্য সংবেদনশীল তথ্য সম্পর্কে সতর্ক থাকুন।"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"চালিয়ে যান"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"অ্যাপ শেয়ার বা রেকর্ড করা"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"সবকিছু সাফ করুন"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"পরিচালনা করুন"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"মোবাইল ডেটা বন্ধ করবেন?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"আপনি \'<xliff:g id="CARRIER">%s</xliff:g>\'-এর মাধ্যমে ডেটা অথবা ইন্টারনেট অ্যাক্সেস করতে পারবেন না। শুধুমাত্র ওয়াই-ফাইয়ের মাধ্যমেই ইন্টারনেট অ্যাক্সেস করা যাবে।"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"আপনার পরিষেবা প্রদানকারী"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"আবার <xliff:g id="CARRIER">%s</xliff:g>-এর ডেটায় পরিবর্তন করবেন?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"উপলভ্যতার উপরে ভিত্তি করে অটোমেটিক মোবাইল ডেটায় পরিবর্তন করা হবে না"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"না থাক"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"হ্যাঁ, পাল্টান"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"একটি অ্যাপ কোনও অনুমোদনের অনুরোধকে ঢেকে দিচ্ছে, তাই সেটিংস থেকে আপনার প্রতিক্রিয়া যাচাই করা যাচ্ছে না।"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> অ্যাপটিকে <xliff:g id="APP_2">%2$s</xliff:g> এর অংশ দেখানোর অনুমতি দেবেন?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- এটি <xliff:g id="APP">%1$s</xliff:g> এর তথ্য অ্যাক্সেস করতে পারবে"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"\'ম্যাগনিফায়ার উইন্ডো\' সেটিংস"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"অ্যাক্সেসিবিলিটি ফিচার খুলতে ট্যাপ করুন। কাস্টমাইজ করুন বা সেটিংসে এই বোতামটি সরিয়ে দিন।\n\n"<annotation id="link">"সেটিংস দেখুন"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"এটি অস্থায়ীভাবে লুকাতে বোতামটি কোণে সরান"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"আগের অবস্থায় ফিরুন"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label}টি শর্টকাট সরানো হয়েছে}one{#টি শর্টকাট সরানো হয়েছে}other{#টি শর্টকাট সরানো হয়েছে}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"উপরে বাঁদিকে সরান"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"উপরে ডানদিকে সরান"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"নিচে বাঁদিকে সরান"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"নিচে ডান দিকে সরান"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"প্রান্তে যান ও আড়াল করুন"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"প্রান্ত থেকে সরান এবং দেখুন"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"সরিয়ে দিন"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"টগল করুন"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ডিভাইস কন্ট্রোল"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"কন্ট্রোল যোগ করতে অ্যাপ বেছে নিন"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"মোবাইল ডেটা"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"কানেক্ট করা আছে"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"সাময়িকভাবে কানেক্ট করা হয়েছে"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"খারাপ কানেকশন"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"মোবাইল ডেটা নিজে থেকে কানেক্ট হবে না"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"কানেকশন নেই"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"অন্য কোনও নেটওয়ার্ক উপলভ্য নেই"</string>
diff --git a/packages/SystemUI/res/values-bs/strings.xml b/packages/SystemUI/res/values-bs/strings.xml
index 183dfe0..bdb8638 100644
--- a/packages/SystemUI/res/values-bs/strings.xml
+++ b/packages/SystemUI/res/values-bs/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Nije moguće prepoznati lice"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Koristite otisak prsta"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth je povezan."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Postotak napunjenosti baterije nije poznat"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Povezan na <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Osvjetljenje"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inverzija boja"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Ispravka boja"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Korisničke postavke"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Upravljajte korisnicima"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Gotovo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zatvori"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Povezano"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon je dostupan"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera je dostupna"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon i kamera su dostuni"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon je uključen"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon je isključen"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon je omogućen za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Pristup mikrofonu je onemogućen za sve aplikacije i usluge. Možete omogućiti pristup mikrofonu u Postavkama &gt; Privatnost &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Pristup mikrofonu je onemogućen za sve aplikacije i usluge. To možete promijeniti u Postavkama &gt; Privatnost &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera je uključena"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera je isključena"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera je omogućena za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Pristup kamere je onemogućen za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Da koristite dugme za mikrofon, omogućite pristup mikrofonu u Postavkama."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Otvori postavke."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Drugi uređaj"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Pregled uključivanja/isključivanja"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Neće vas ometati zvukovi i vibracije, osim alarma, podsjetnika, događaja i pozivalaca koje odredite. I dalje ćete čuti sve što ste odabrali za reprodukciju, uključujući muziku, videozapise i igre."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kada aplikaciju dijelite, snimate ili emitirate, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ima pristup svemu što se prikazuje ili reproducira u toj aplikaciji. Zato budite oprezni s lozinkama, detaljima o plaćanju, porukama i drugim osjetljivim informacijama."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Nastavi"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Dijelite ili snimite aplikaciju"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Očisti sve"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljajte"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historija"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Isključiti prijenos podataka na mobilnoj mreži?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nećete imati pristup podacima ni internetu putem mobilnog operatera <xliff:g id="CARRIER">%s</xliff:g>. Internet će biti dostupan samo putem WiFi-ja."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"vaš operater"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vratiti na operatera <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Prijenos podataka na mobilnoj mreži se neće automatski promijeniti na osnovu dostupnosti"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, hvala"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Da, promijeni"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Postavke ne mogu potvrditi vaš odgovor jer aplikacija zaklanja zahtjev za odobrenje."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Dozvoliti aplikaciji <xliff:g id="APP_0">%1$s</xliff:g> da prikazuje isječke aplikacije <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Može čitati informacije iz aplikacije <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Postavke prozora povećala"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Dodirnite da otvorite funkcije pristupačnosti. Prilagodite ili zamijenite dugme u Postavkama.\n\n"<annotation id="link">"Postavke"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Premjestite dugme do ivice da ga privremeno sakrijete"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Poništavanje"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} prečica je uklonjena}one{# prečica je uklonjena}few{# prečice su uklonjene}other{# prečica je uklonjenao}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Pomjeranje gore lijevo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Pomjeranje gore desno"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Pomjeranje dolje lijevo"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Pomjeranje dolje desno"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Pomjeranje do ivice i sakrivanje"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Pomjeranje izvan ivice i prikaz"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Uklanjanje"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"aktiviranje/deaktiviranje"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrole uređaja"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Odaberite aplikaciju da dodate kontrole"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Prijenos podataka na mobilnoj mreži"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Povezano"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Privremeno povezano"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Slaba veza"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Prijenos podataka se neće automatski povezati"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Niste povezani s mrežom"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Druge mreže nisu dostupne"</string>
diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml
index daa63e6..484de00 100644
--- a/packages/SystemUI/res/values-ca/strings.xml
+++ b/packages/SystemUI/res/values-ca/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"No es reconeix la cara"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Utilitza l\'empremta digital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connectat."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Es desconeix el percentatge de bateria."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"S\'ha connectat a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brillantor"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversió de colors"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correcció de color"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Configuració d\'usuari"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gestiona els usuaris"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Fet"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Tanca"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connectat"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Micròfon disponible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Càmera disponible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Càmera i micròfon disponibles"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Un altre dispositiu"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Activa o desactiva Aplicacions recents"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"No t\'interromprà cap so ni cap vibració, tret dels de les alarmes, recordatoris, esdeveniments i trucades de les persones que especifiquis. Continuaràs sentint tot allò que decideixis reproduir, com ara música, vídeos i jocs."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quan estàs compartint, gravant o emetent, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> té accés a qualsevol cosa que es vegi a la pantalla o que es reprodueixi a l\'aplicació. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges o altra informació sensible."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continua"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Comparteix o grava una aplicació"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Esborra-ho tot"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gestiona"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vols desactivar les dades mòbils?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"No tindràs accés a dades ni a Internet mitjançant <xliff:g id="CARRIER">%s</xliff:g>. Internet només estarà disponible per Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"el teu operador de telefonia mòbil"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vols tornar a <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Les dades mòbils no canviaran automàticament en funció de la disponibilitat"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No, gràcies"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sí, fes el canvi"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Com que hi ha una aplicació que oculta una sol·licitud de permís, no es pot verificar la teva resposta des de la configuració."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vols permetre que <xliff:g id="APP_0">%1$s</xliff:g> mostri porcions de l\'aplicació <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Pot llegir informació de l\'aplicació <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configuració de la finestra de la lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toca per obrir funcions d\'accessibilitat. Personalitza o substitueix el botó a Configuració.\n\n"<annotation id="link">"Mostra"</annotation>"."</string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mou el botó a l\'extrem per amagar-lo temporalment"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Desfés"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{S\'ha suprimit la drecera {label}}other{S\'han suprimit # dreceres}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mou a dalt a l\'esquerra"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mou a dalt a la dreta"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mou a baix a l\'esquerra"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mou a baix a la dreta"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mou dins de les vores i amaga"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mou fora de les vores i mostra"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Suprimeix"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"commuta"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controls de dispositius"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Selecciona l\'aplicació per afegir controls"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dades mòbils"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connectat"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Connexió temporal"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Connexió feble"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Les dades mòbils no es connectaran automàticament"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sense connexió"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No hi ha cap altra xarxa disponible"</string>
diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml
index cba9448..71bea80 100644
--- a/packages/SystemUI/res/values-cs/strings.xml
+++ b/packages/SystemUI/res/values-cs/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Obličej nelze rozpoznat"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Použijte otisk prstu"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Rozhraní Bluetooth je připojeno."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Procento baterie není známé."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Připojeno k zařízení <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Jas"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Převrácení barev"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Korekce barev"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Uživatelské nastavení"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Správa uživatelů"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Hotovo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zavřít"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Připojeno"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon je k dispozici"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Fotoaparát je k dispozici"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon a fotoaparát jsou k dispozici"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon je zapnutý"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon je vypnutý"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Přístup k mikrofonu je aktivován pro všechny aplikace a služby."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Přístup k mikrofonu je deaktivován pro všechny aplikace a služby. Přístup k mikrofonu můžete udělit v Nastavení &gt; Ochrana soukromí &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Přístup k mikrofonu je deaktivován pro všechny aplikace a služby. Můžete to změnit v Nastavení &gt; Ochrana soukromí &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Fotoaparát je zapnutý"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Fotoaparát je vypnutý"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Přístup k fotoaparátu je aktivován pro všechny aplikace a služby."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Přístup k fotoaparátu je deaktivován pro všechny aplikace a služby."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Pokud chcete použít tlačítko mikrofonu, v nastavení udělte přístup k mikrofonu."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Otevřít nastavení."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Další zařízení"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Přepnout přehled"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Nebudou vás rušit zvuky ani vibrace s výjimkou budíků, upozornění, událostí a volajících, které zadáte. Nadále uslyšíte veškerý obsah, který si sami pustíte (např. hudba, videa nebo hry)."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Když sdílíte, nahráváte nebo odesíláte aplikaci, aplikace <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> má přístup k veškerému obsahu, který je v této aplikaci zobrazen nebo přehráván. Dejte proto pozor na hesla, platební údaje, zprávy nebo jiné citlivé informace."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Pokračovat"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Sdílení nebo nahrání aplikace"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Smazat vše"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Spravovat"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historie"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vypnout mobilní data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Prostřednictvím <xliff:g id="CARRIER">%s</xliff:g> nebudete moci používat data ani internet. Internet bude dostupný pouze přes Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"vašeho operátora"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Přepnout zpět na operátora <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilní data se nebudou automaticky přepínat podle dostupnosti"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, díky"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ano, přepnout"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Žádost o oprávnění je blokována jinou aplikací. Nastavení proto vaši odpověď nemůže ověřit."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Povolit aplikaci <xliff:g id="APP_0">%1$s</xliff:g> zobrazovat ukázky z aplikace <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Může číst informace z aplikace <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Nastavení okna zvětšení"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Klepnutím otevřete funkce přístupnosti. Tlačítko lze upravit nebo nahradit v Nastavení.\n\n"<annotation id="link">"Nastavení"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Přesunutím tlačítka k okraji ho dočasně skryjete"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Vrátit zpět"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Zkratka {label} byla odstraněna}few{Byly odstraněny # zkratky}many{Bylo odstraněno # zkratky}other{Bylo odstraněno # zkratek}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Přesunout vlevo nahoru"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Přesunout vpravo nahoru"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Přesunout vlevo dolů"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Přesunout vpravo dolů"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Přesunout k okraji a skrýt"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Přesunout okraj ven a zobrazit"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Odstranit"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"přepnout"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Ovládání zařízení"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Vyberte aplikaci, pro kterou chcete přidat ovládací prvky"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilní data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Připojeno"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Dočasně připojeno"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Nekvalitní připojení"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobilní data se nebudou připojovat automaticky"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Žádné připojení"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Žádné další sítě nejsou k dispozici"</string>
diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml
index a879cbb..0eb3d95 100644
--- a/packages/SystemUI/res/values-da/strings.xml
+++ b/packages/SystemUI/res/values-da/strings.xml
@@ -161,13 +161,15 @@
     <string name="biometric_dialog_last_pattern_attempt_before_wipe_profile" msgid="6045224069529284686">"Hvis du angiver et forkert mønster i næste forsøg, slettes din arbejdsprofil og de tilhørende data."</string>
     <string name="biometric_dialog_last_pin_attempt_before_wipe_profile" msgid="545567685899091757">"Hvis du angiver en forkert pinkode i næste forsøg, slettes din arbejdsprofil og de tilhørende data."</string>
     <string name="biometric_dialog_last_password_attempt_before_wipe_profile" msgid="8538032972389729253">"Hvis du angiver en forkert adgangskode i næste forsøg, slettes din arbejdsprofil og de tilhørende data."</string>
-    <string name="fingerprint_dialog_touch_sensor" msgid="2817887108047658975">"Sæt fingeren på fingeraftrykslæseren"</string>
+    <string name="fingerprint_dialog_touch_sensor" msgid="2817887108047658975">"Sæt fingeren på fingeraftrykssensoren"</string>
     <string name="accessibility_fingerprint_dialog_fingerprint_icon" msgid="4465698996175640549">"Ikon for fingeraftryk"</string>
     <string name="fingerprint_dialog_use_fingerprint_instead" msgid="6178228876763024452">"Ansigtet kan ikke genkendes. Brug fingeraftryk i stedet."</string>
     <!-- no translation found for keyguard_face_failed_use_fp (7140293906176164263) -->
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Ansigt kan ikke genkendes"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Brug fingeraftryk i stedet"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth tilsluttet."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batteriniveauet er ukendt."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Tilsluttet <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Lysstyrke"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Ombytning af farver"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Farvekorrigering"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Brugerindstillinger"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Administrer brugere"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Udfør"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Luk"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Tilsluttet"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon kan benyttes"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera kan benyttes"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon og kamera kan benyttes"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofonen er aktiveret"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofonen er deaktiveret"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofonen er aktiveret for alle apps og tjenester."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofonadgang er deaktiveret for alle apps og tjenester. Du kan aktivere mikrofonadgang under Indstillinger &gt; Privatliv &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofonadgang er deaktiveret for alle apps og tjenester. Du kan ændre dette under Indstillinger &gt; Privatliv &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kameraet er aktiveret"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kameraet er deaktiveret"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kameraet er aktiveret for alle apps og tjenester."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kameraadgang er deaktiveret for alle apps og tjenester."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Hvis du vil bruge mikrofonknappen, skal du aktivere mikrofonadgang under Indstillinger."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Åbn Indstillinger."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Anden enhed"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Slå Oversigt til/fra"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Du bliver ikke forstyrret af lyde eller vibrationer, undtagen fra alarmer, påmindelser, begivenheder og opkald fra udvalgte personer, du selv angiver. Du kan stadig høre alt, du vælger at afspille, f.eks. musik, videoer og spil."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Når du deler, optager eller caster en app, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> adgang til alt, der vises eller afspilles i den pågældende app. Vær derfor forsigtig med adgangskoder, betalingsoplysninger, beskeder og andre følsomme oplysninger."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsæt"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Del eller optag en app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Ryd alle"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Administrer"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historik"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vil du deaktivere mobildata?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Du vil ikke have data- eller internetadgang via <xliff:g id="CARRIER">%s</xliff:g>. Der vil kun være adgang til internettet via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"dit mobilselskab"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vil du skifte tilbage til <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobildata skifter ikke automatisk på baggrund af tilgængelighed"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nej tak"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, skift"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Indstillinger kan ikke bekræfte dit svar, da en app dækker for en anmodning om tilladelse."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vil du give <xliff:g id="APP_0">%1$s</xliff:g> tilladelse til at vise eksempler fra <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Den kan læse oplysninger fra <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Indstillinger for lupvindue"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tryk for at åbne hjælpefunktioner. Tilpas eller erstat denne knap i Indstillinger.\n\n"<annotation id="link">"Se indstillingerne"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Flyt knappen til kanten for at skjule den midlertidigt"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Fortryd"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} genvej blev fjernet}one{# genvej blev fjernet}other{# genveje blev fjernet}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Flyt op til venstre"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Flyt op til højre"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Flyt ned til venstre"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Flyt ned til højre"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Flyt ud til kanten, og skjul"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Flyt ud til kanten, og vis"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Fjern"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"slå til/fra"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Enhedsstyring"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Vælg en app for at tilføje styring"</string>
@@ -924,15 +954,17 @@
     <string name="battery_state_unknown_notification_title" msgid="8464703640483773454">"Der er problemer med at aflæse dit batteriniveau"</string>
     <string name="battery_state_unknown_notification_text" msgid="13720937839460899">"Tryk for at få flere oplysninger"</string>
     <string name="qs_alarm_tile_no_alarm" msgid="4826472008616807923">"Ingen alarm er indstillet"</string>
-    <string name="accessibility_fingerprint_label" msgid="5255731221854153660">"Fingeraftrykslæser"</string>
+    <string name="accessibility_fingerprint_label" msgid="5255731221854153660">"Fingeraftrykssensor"</string>
     <string name="accessibility_authenticate_hint" msgid="798914151813205721">"godkende"</string>
     <string name="accessibility_enter_hint" msgid="2617864063504824834">"få adgang til enheden"</string>
     <string name="keyguard_try_fingerprint" msgid="2825130772993061165">"Brug fingeraftryk for at åbne"</string>
-    <string name="accessibility_fingerprint_bouncer" msgid="7189102492498735519">"Godkendelse er påkrævet. Sæt fingeren på fingeraftrykslæseren for at godkende."</string>
+    <string name="accessibility_fingerprint_bouncer" msgid="7189102492498735519">"Godkendelse er påkrævet. Sæt fingeren på fingeraftrykssensoren for at godkende."</string>
     <string name="ongoing_phone_call_content_description" msgid="5332334388483099947">"Igangværende telefonopkald"</string>
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobildata"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Forbundet"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Midlertidigt forbundet"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Dårlig forbindelse"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Ingen automatisk mobildataforbindelse"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Der er ingen forbindelse"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Der er ingen andre tilgængelige netværk"</string>
diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml
index fb135ff..2829950 100644
--- a/packages/SystemUI/res/values-de/strings.xml
+++ b/packages/SystemUI/res/values-de/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Gesicht nicht erkannt"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Fingerabdruck verwenden"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Mit Bluetooth verbunden"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Akkustand unbekannt."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Mit <xliff:g id="BLUETOOTH">%s</xliff:g> verbunden"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Helligkeit"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Farbumkehr"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Farbkorrektur"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Nutzereinstellungen"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Nutzer verwalten"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Fertig"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Schließen"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Verbunden"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon verfügbar"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera verfügbar"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon und Kamera verfügbar"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon aktiviert"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon deaktiviert"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon ist für alle Apps und Dienste aktiviert."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofonzugriff ist für alle Apps und Dienste deaktiviert. Du kannst ihn in den Einstellungen unter „Datenschutz“ &gt; „Mikrofon“ aktivieren."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofonzugriff ist für alle Apps und Dienste deaktiviert. Du kannst dies in den Einstellungen unter „Datenschutz“ &gt; „Mikrofon“ ändern."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera eingeschaltet"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera ausgeschaltet"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera ist für alle Apps und Dienste aktiviert."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kamerazugriff ist für alle Apps und Dienste deaktiviert."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Wenn du die Mikrofontaste verwenden möchtest, musst du den Mikrofonzugriff in den Einstellungen aktivieren."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Einstellungen öffnen"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Sonstiges Gerät"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Übersicht ein-/ausblenden"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Klingeltöne und die Vibration werden deaktiviert, außer für Weckrufe, Erinnerungen, Termine sowie Anrufe von zuvor von dir festgelegten Personen. Du hörst jedoch weiterhin Sound, wenn du dir Musik anhörst, Videos ansiehst oder Spiele spielst."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Beim Teilen, Aufnehmen oder Übertragen einer App hat <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> Zugriff auf alle Inhalte, die in dieser App sichtbar sind oder wiedergegeben werden. Sei daher mit Passwörtern, Zahlungsdetails, Nachrichten oder anderen vertraulichen Informationen vorsichtig."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Weiter"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"App teilen oder aufnehmen"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Alle löschen"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Verwalten"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Verlauf"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Mobile Daten deaktivieren?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Du kannst dann nicht mehr über <xliff:g id="CARRIER">%s</xliff:g> auf Daten und das Internet zugreifen. Das Internet ist nur noch über WLAN verfügbar."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"deinen Mobilfunkanbieter"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Zurück zu <xliff:g id="CARRIER">%s</xliff:g> wechseln?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobile Daten werden nicht je nach Verfügbarkeit automatisch gewechselt"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nein danke"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, wechseln"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Deine Eingabe wird von \"Einstellungen\" nicht erkannt, weil die Berechtigungsanfrage von einer App verdeckt wird."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> erlauben, Teile von <xliff:g id="APP_2">%2$s</xliff:g> anzuzeigen?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Darf Informationen aus <xliff:g id="APP">%1$s</xliff:g> lesen"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Einstellungen für das Vergrößerungsfenster"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tippe, um die Bedienungshilfen aufzurufen. Du kannst diese Schaltfläche in den Einstellungen anpassen oder ersetzen.\n\n"<annotation id="link">"Zu den Einstellungen"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Durch Ziehen an den Rand wird die Schaltfläche zeitweise ausgeblendet"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Rückgängig machen"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} Verknüpfung entfernt}other{# Verknüpfungen entfernt}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Nach oben links verschieben"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Nach rechts oben verschieben"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Nach unten links verschieben"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Nach unten rechts verschieben"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"An den Rand verschieben und verbergen"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Vom Rand verschieben und anzeigen"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Entfernen"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"Wechseln"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Gerätesteuerung"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"App zum Hinzufügen von Steuerelementen auswählen"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile Daten"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Verbunden"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Vorübergehend verbunden"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Schwache Verbindung"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Keine automatische Verbindung über mobile Daten"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Keine Verbindung"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Keine anderen Netzwerke verfügbar"</string>
diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml
index 3d15953..51af1f3 100644
--- a/packages/SystemUI/res/values-el/strings.xml
+++ b/packages/SystemUI/res/values-el/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Αδύνατη η αναγν. προσώπου"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Χρησιμ. δακτυλ. αποτύπ."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Το Bluetooth είναι συνδεδεμένο."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Άγνωστο ποσοστό μπαταρίας."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Συνδέθηκε στο <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Φωτεινότητα"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Αντιστροφή χρωμάτων"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Διόρθωση χρωμάτων"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Ρυθμίσεις χρήστη"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Διαχείριση χρηστών"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Τέλος"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Κλείσιμο"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Συνδέθηκε"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Διαθέσιμο μικρόφωνο"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Διαθέσιμη κάμερα"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Διαθέσιμη κάμερα και μικρόφωνο"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Το μικρόφωνο ενεργοποιήθηκε"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Το μικρόφωνο απενεργοποιήθηκε"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Το μικρόφωνο ενεργοποιήθηκε για όλες τις εφαρμογές και τις υπηρεσίες."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Η πρόσβαση στο μικρόφωνο είναι απενεργοποιημένη για όλες τις εφαρμογές και τις υπηρεσίες. Μπορείτε να ενεργοποιήσετε την πρόσβαση στο μικρόφωνο από τις Ρυθμίσεις &gt; Απόρρητο &gt; Μικρόφωνο."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Η πρόσβαση στο μικρόφωνο είναι απενεργοποιημένη για όλες τις εφαρμογές και τις υπηρεσίες. Μπορείτε να αλλάξετε την επιλογή από τις Ρυθμίσεις &gt; Απόρρητο &gt; Μικρόφωνο."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Η κάμερα ενεργοποιήθηκε"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Η κάμερα απενεργοποιήθηκε"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Η κάμερα ενεργοποιήθηκε για όλες τις εφαρμογές και τις υπηρεσίες."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Η πρόσβαση στην κάμερα είναι απενεργοποιημένη για όλες τις εφαρμογές και τις υπηρεσίες."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Για να χρησιμοποιήσετε το κουμπί μικροφώνου, ενεργοποιήστε την πρόσβαση στο μικρόφωνο από τις Ρυθμίσεις."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Άνοιγμα ρυθμίσεων."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Άλλη συσκευή"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Εναλλαγή επισκόπησης"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Δεν θα ενοχλείστε από ήχους και δονήσεις, παρά μόνο από ξυπνητήρια, υπενθυμίσεις, συμβάντα και καλούντες που έχετε καθορίσει. Θα εξακολουθείτε να ακούτε όλο το περιεχόμενο που επιλέγετε να αναπαραγάγετε, συμπεριλαμβανομένης της μουσικής, των βίντεο και των παιχνιδιών."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Όταν κάνετε κοινοποίηση, εγγραφή ή μετάδοση μιας εφαρμογής, η εφαρμογή <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> έχει πρόσβαση σε οτιδήποτε είναι ορατό ή αναπαράγεται στη συγκεκριμένη εφαρμογή. Επομένως, να είστε προσεκτικοί με τους κωδικούς πρόσβασης, τα στοιχεία πληρωμής, τα μηνύματα ή άλλες ευαίσθητες πληροφορίες."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Συνέχεια"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Κοινοποίηση ή εγγραφή εφαρμογής"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Διαγραφή όλων"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Διαχείριση"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Ιστορικό"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Απενεργοποίηση δεδομένων κινητής τηλεφωνίας;"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Δεν θα έχετε πρόσβαση σε δεδομένα ή στο διαδίκτυο μέσω της εταιρείας κινητής τηλεφωνίας <xliff:g id="CARRIER">%s</xliff:g>. Θα έχετε πρόσβαση στο διαδίκτυο μόνο μέσω Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"η εταιρεία κινητής τηλεφωνίας"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Επιστροφή σε <xliff:g id="CARRIER">%s</xliff:g>;"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Δεν θα γίνεται αυτόματα εναλλαγή των δεδομένων κινητής τηλεφωνίας βάσει της διαθεσιμότητας"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Όχι, ευχαριστώ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ναι, εναλλαγή"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Επειδή μια εφαρμογή αποκρύπτει ένα αίτημα άδειας, δεν είναι δυνατή η επαλήθευση της απάντησής σας από τις Ρυθμίσεις."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Να επιτρέπεται στο <xliff:g id="APP_0">%1$s</xliff:g> να εμφανίζει τμήματα της εφαρμογής <xliff:g id="APP_2">%2$s</xliff:g>;"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Μπορεί να διαβάζει πληροφορίες από την εφαρμογή <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Ρυθμίσεις παραθύρου μεγεθυντικού φακού"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Πατήστε για άνοιγμα των λειτουργιών προσβασιμότητας. Προσαρμόστε ή αντικαταστήστε το κουμπί στις Ρυθμίσεις.\n\n"<annotation id="link">"Προβολή ρυθμίσεων"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Μετακινήστε το κουμπί στο άκρο για προσωρινή απόκρυψη"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Αναίρεση"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Η συντόμευση {label} καταργήθηκε}other{Καταργήθηκαν # συντομεύσεις}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Μετακίνηση επάνω αριστερά"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Μετακίνηση επάνω δεξιά"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Μετακίνηση κάτω αριστερά"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Μετακίνηση κάτω δεξιά"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Μετακίν. στο άκρο και απόκρυψη"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Μετακ. εκτός άκρου και εμφάν."</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Κατάργηση"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"εναλλαγή"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Στοιχεία ελέγχου συσκευής"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Επιλογή εφαρμογής για προσθήκη στοιχείων ελέγχου"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Δεδομένα κινητής τηλεφωνίας"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Συνδέθηκε"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Προσωρινή σύνδεση"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Ασθενής σύνδεση"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Χωρίς αυτόματη σύνδεση δεδομένων κινητ. τηλεφωνίας"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Χωρίς σύνδεση"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Δεν υπάρχουν άλλα διαθέσιμα δίκτυα"</string>
diff --git a/packages/SystemUI/res/values-en-rAU/strings.xml b/packages/SystemUI/res/values-en-rAU/strings.xml
index e7166ac..c23bfe6 100644
--- a/packages/SystemUI/res/values-en-rAU/strings.xml
+++ b/packages/SystemUI/res/values-en-rAU/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Can’t recognise face"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use fingerprint instead"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connected."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Battery percentage unknown."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connected to <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brightness"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Colour inversion"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Colour correction"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"User settings"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Manage users"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Done"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Close"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connected"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microphone available"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Camera available"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microphone and camera available"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microphone turned on"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microphone turned off"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microphone is enabled for all apps and services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Microphone access is disabled for all apps and services. You can enable microphone access in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Microphone access is disabled for all apps and services. You can change this in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera turned on"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera turned off"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera is enabled for all apps and services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Camera access is disabled for all apps and services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"To use the microphone button, enable microphone access in Settings."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Open settings."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Other device"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Toggle Overview"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events and callers you specify. You\'ll still hear anything you choose to play including music, videos and games."</string>
@@ -373,6 +386,11 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"When you\'re sharing, recording or casting an app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> has access to anything shown or played on that app. So, be careful with passwords, payment details, messages or other sensitive information."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continue"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Share or record an app"</string>
+    <string name="media_projection_permission_dialog_system_service_title" msgid="6827129613741303726">"Allow this app to share or record?"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen" msgid="8801616203805837575">"When you\'re sharing, recording or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="media_projection_permission_dialog_system_service_warning_single_app" msgid="543310680568419338">"When you\'re sharing, recording or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_title" msgid="2113331792064527203">"Blocked by your IT admin"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_description" msgid="6015975736747696431">"Screen capturing is disabled by device policy"</string>
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Clear all"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Manage"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"History"</string>
@@ -727,6 +745,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Turn off mobile data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"You won\'t have access to data or the Internet through <xliff:g id="CARRIER">%s</xliff:g>. Internet will only be available via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"your operator"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Switch back to <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobile data won\'t automatically switch based on availability"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No thanks"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Yes, switch"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Because an app is obscuring a permission request, Settings can’t verify your response."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Allow <xliff:g id="APP_0">%1$s</xliff:g> to show <xliff:g id="APP_2">%2$s</xliff:g> slices?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– It can read information from <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +807,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Magnifier window settings"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tap to open accessibility features. Customise or replace this button in Settings.\n\n"<annotation id="link">"View settings"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Move button to the edge to hide it temporarily"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Undo"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} shortcut removed}other{# shortcuts removed}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Move top left"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Move top right"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Move bottom left"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Move bottom right"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Move to edge and hide"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Move out edge and show"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remove"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"toggle"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Device controls"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Choose app to add controls"</string>
@@ -933,6 +958,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connected"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporarily connected"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Poor connection"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobile data won\'t auto‑connect"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"No connection"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No other networks available"</string>
diff --git a/packages/SystemUI/res/values-en-rCA/strings.xml b/packages/SystemUI/res/values-en-rCA/strings.xml
index fe746d9..7c1f8fd 100644
--- a/packages/SystemUI/res/values-en-rCA/strings.xml
+++ b/packages/SystemUI/res/values-en-rCA/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Can’t recognise face"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use fingerprint instead"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connected."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Battery percentage unknown."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connected to <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brightness"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Colour inversion"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Colour correction"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"User settings"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Manage users"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Done"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Close"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connected"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microphone available"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Camera available"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microphone and camera available"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microphone turned on"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microphone turned off"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microphone is enabled for all apps and services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Microphone access is disabled for all apps and services. You can enable microphone access in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Microphone access is disabled for all apps and services. You can change this in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera turned on"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera turned off"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera is enabled for all apps and services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Camera access is disabled for all apps and services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"To use the microphone button, enable microphone access in Settings."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Open settings."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Other device"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Toggle Overview"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events and callers you specify. You\'ll still hear anything you choose to play including music, videos and games."</string>
@@ -373,6 +386,11 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"When you\'re sharing, recording or casting an app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> has access to anything shown or played on that app. So, be careful with passwords, payment details, messages or other sensitive information."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continue"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Share or record an app"</string>
+    <string name="media_projection_permission_dialog_system_service_title" msgid="6827129613741303726">"Allow this app to share or record?"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen" msgid="8801616203805837575">"When you\'re sharing, recording or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="media_projection_permission_dialog_system_service_warning_single_app" msgid="543310680568419338">"When you\'re sharing, recording or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_title" msgid="2113331792064527203">"Blocked by your IT admin"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_description" msgid="6015975736747696431">"Screen capturing is disabled by device policy"</string>
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Clear all"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Manage"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"History"</string>
@@ -727,6 +745,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Turn off mobile data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"You won\'t have access to data or the Internet through <xliff:g id="CARRIER">%s</xliff:g>. Internet will only be available via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"your carrier"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Switch back to <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobile data won\'t automatically switch based on availability"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No thanks"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Yes, switch"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Because an app is obscuring a permission request, Settings can’t verify your response."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Allow <xliff:g id="APP_0">%1$s</xliff:g> to show <xliff:g id="APP_2">%2$s</xliff:g> slices?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– It can read information from <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +807,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Magnifier window settings"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tap to open accessibility features. Customise or replace this button in Settings.\n\n"<annotation id="link">"View settings"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Move button to the edge to hide it temporarily"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Undo"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} shortcut removed}other{# shortcuts removed}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Move top left"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Move top right"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Move bottom left"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Move bottom right"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Move to edge and hide"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Move out edge and show"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remove"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"toggle"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Device controls"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Choose app to add controls"</string>
@@ -933,6 +958,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connected"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporarily connected"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Poor connection"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobile data won\'t auto‑connect"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"No connection"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No other networks available"</string>
diff --git a/packages/SystemUI/res/values-en-rCA/strings_tv.xml b/packages/SystemUI/res/values-en-rCA/strings_tv.xml
index e97dbe4..a628846 100644
--- a/packages/SystemUI/res/values-en-rCA/strings_tv.xml
+++ b/packages/SystemUI/res/values-en-rCA/strings_tv.xml
@@ -23,13 +23,13 @@
     <string name="notification_vpn_disconnected" msgid="7150747626448044843">"VPN is disconnected"</string>
     <string name="notification_disclosure_vpn_text" msgid="3873532735584866236">"Via <xliff:g id="VPN_APP">%1$s</xliff:g>"</string>
     <string name="tv_notification_panel_title" msgid="5311050946506276154">"Notifications"</string>
-    <string name="tv_notification_panel_no_notifications" msgid="9115191912267270678">"No notifications"</string>
+    <string name="tv_notification_panel_no_notifications" msgid="9115191912267270678">"No Notifications"</string>
     <string name="mic_recording_announcement" msgid="7587123608060316575">"Microphone is recording"</string>
     <string name="camera_recording_announcement" msgid="7240177719403759112">"Camera is recording"</string>
-    <string name="mic_and_camera_recording_announcement" msgid="8599231390508812667">"Camera and microphone are recording"</string>
+    <string name="mic_and_camera_recording_announcement" msgid="8599231390508812667">"Camera and Microphone are recording"</string>
     <string name="mic_stopped_recording_announcement" msgid="7301537004900721242">"Microphone stopped recording"</string>
     <string name="camera_stopped_recording_announcement" msgid="8540496432367032801">"Camera stopped recording"</string>
-    <string name="mic_camera_stopped_recording_announcement" msgid="8708524579599977412">"Camera and microphone stopped recording"</string>
+    <string name="mic_camera_stopped_recording_announcement" msgid="8708524579599977412">"Camera and Microphone stopped recording"</string>
     <string name="screen_recording_announcement" msgid="2996750593472241520">"Screen recording started"</string>
     <string name="screen_stopped_recording_announcement" msgid="979749439036681416">"Screen recording stopped"</string>
 </resources>
diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml
index e7166ac..c23bfe6 100644
--- a/packages/SystemUI/res/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res/values-en-rGB/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Can’t recognise face"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use fingerprint instead"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connected."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Battery percentage unknown."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connected to <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brightness"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Colour inversion"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Colour correction"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"User settings"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Manage users"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Done"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Close"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connected"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microphone available"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Camera available"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microphone and camera available"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microphone turned on"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microphone turned off"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microphone is enabled for all apps and services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Microphone access is disabled for all apps and services. You can enable microphone access in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Microphone access is disabled for all apps and services. You can change this in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera turned on"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera turned off"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera is enabled for all apps and services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Camera access is disabled for all apps and services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"To use the microphone button, enable microphone access in Settings."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Open settings."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Other device"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Toggle Overview"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events and callers you specify. You\'ll still hear anything you choose to play including music, videos and games."</string>
@@ -373,6 +386,11 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"When you\'re sharing, recording or casting an app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> has access to anything shown or played on that app. So, be careful with passwords, payment details, messages or other sensitive information."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continue"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Share or record an app"</string>
+    <string name="media_projection_permission_dialog_system_service_title" msgid="6827129613741303726">"Allow this app to share or record?"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen" msgid="8801616203805837575">"When you\'re sharing, recording or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="media_projection_permission_dialog_system_service_warning_single_app" msgid="543310680568419338">"When you\'re sharing, recording or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_title" msgid="2113331792064527203">"Blocked by your IT admin"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_description" msgid="6015975736747696431">"Screen capturing is disabled by device policy"</string>
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Clear all"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Manage"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"History"</string>
@@ -727,6 +745,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Turn off mobile data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"You won\'t have access to data or the Internet through <xliff:g id="CARRIER">%s</xliff:g>. Internet will only be available via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"your operator"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Switch back to <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobile data won\'t automatically switch based on availability"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No thanks"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Yes, switch"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Because an app is obscuring a permission request, Settings can’t verify your response."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Allow <xliff:g id="APP_0">%1$s</xliff:g> to show <xliff:g id="APP_2">%2$s</xliff:g> slices?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– It can read information from <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +807,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Magnifier window settings"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tap to open accessibility features. Customise or replace this button in Settings.\n\n"<annotation id="link">"View settings"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Move button to the edge to hide it temporarily"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Undo"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} shortcut removed}other{# shortcuts removed}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Move top left"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Move top right"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Move bottom left"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Move bottom right"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Move to edge and hide"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Move out edge and show"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remove"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"toggle"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Device controls"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Choose app to add controls"</string>
@@ -933,6 +958,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connected"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporarily connected"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Poor connection"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobile data won\'t auto‑connect"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"No connection"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No other networks available"</string>
diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml
index e7166ac..c23bfe6 100644
--- a/packages/SystemUI/res/values-en-rIN/strings.xml
+++ b/packages/SystemUI/res/values-en-rIN/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Can’t recognise face"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use fingerprint instead"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connected."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Battery percentage unknown."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connected to <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brightness"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Colour inversion"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Colour correction"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"User settings"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Manage users"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Done"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Close"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connected"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microphone available"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Camera available"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microphone and camera available"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microphone turned on"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microphone turned off"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microphone is enabled for all apps and services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Microphone access is disabled for all apps and services. You can enable microphone access in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Microphone access is disabled for all apps and services. You can change this in Settings &gt; Privacy &gt; Microphone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera turned on"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera turned off"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera is enabled for all apps and services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Camera access is disabled for all apps and services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"To use the microphone button, enable microphone access in Settings."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Open settings."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Other device"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Toggle Overview"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events and callers you specify. You\'ll still hear anything you choose to play including music, videos and games."</string>
@@ -373,6 +386,11 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"When you\'re sharing, recording or casting an app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> has access to anything shown or played on that app. So, be careful with passwords, payment details, messages or other sensitive information."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continue"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Share or record an app"</string>
+    <string name="media_projection_permission_dialog_system_service_title" msgid="6827129613741303726">"Allow this app to share or record?"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen" msgid="8801616203805837575">"When you\'re sharing, recording or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="media_projection_permission_dialog_system_service_warning_single_app" msgid="543310680568419338">"When you\'re sharing, recording or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages or other sensitive information."</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_title" msgid="2113331792064527203">"Blocked by your IT admin"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_description" msgid="6015975736747696431">"Screen capturing is disabled by device policy"</string>
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Clear all"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Manage"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"History"</string>
@@ -727,6 +745,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Turn off mobile data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"You won\'t have access to data or the Internet through <xliff:g id="CARRIER">%s</xliff:g>. Internet will only be available via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"your operator"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Switch back to <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobile data won\'t automatically switch based on availability"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No thanks"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Yes, switch"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Because an app is obscuring a permission request, Settings can’t verify your response."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Allow <xliff:g id="APP_0">%1$s</xliff:g> to show <xliff:g id="APP_2">%2$s</xliff:g> slices?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– It can read information from <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +807,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Magnifier window settings"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tap to open accessibility features. Customise or replace this button in Settings.\n\n"<annotation id="link">"View settings"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Move button to the edge to hide it temporarily"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Undo"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} shortcut removed}other{# shortcuts removed}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Move top left"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Move top right"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Move bottom left"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Move bottom right"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Move to edge and hide"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Move out edge and show"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remove"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"toggle"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Device controls"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Choose app to add controls"</string>
@@ -933,6 +958,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connected"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporarily connected"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Poor connection"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobile data won\'t auto‑connect"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"No connection"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No other networks available"</string>
diff --git a/packages/SystemUI/res/values-en-rXC/strings.xml b/packages/SystemUI/res/values-en-rXC/strings.xml
index b55b744..06b9f45 100644
--- a/packages/SystemUI/res/values-en-rXC/strings.xml
+++ b/packages/SystemUI/res/values-en-rXC/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‏‏‏‏‎‎‎‏‎‎‏‎‎‏‏‏‎‎‏‏‎‎‎‎‏‎‎‎‎‏‏‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‏‏‎Can’t recognize face‎‏‎‎‏‎"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‎‏‏‎‎‏‎‏‎‎‏‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‎Use fingerprint instead‎‏‎‎‏‎"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‏‎‏‏‎‏‎‏‎‏‎‎‏‎‎‎‎‏‏‏‎‏‎‎‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‎‏‎Bluetooth connected.‎‏‎‎‏‎"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‏‎‎‎‏‎‏‏‎‏‎‎‎‏‏‏‏‎‎‎‎‎‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‏‎‎‎‎‏‏‎‎‏‏‏‏‎‎‎‎Battery percentage unknown.‎‏‎‎‏‎"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‏‏‏‎‏‏‎‏‎‏‏‏‎‏‏‏‏‎‎Connected to ‎‏‎‎‏‏‎<xliff:g id="BLUETOOTH">%s</xliff:g>‎‏‎‎‏‏‏‎.‎‏‎‎‏‎"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‏‏‏‎‏‏‏‎‎‏‎‎‏‏‏‏‎‎‏‎‎‎‏‎‏‏‎‏‏‎‎‏‏‎‏‎‏‏‎‎‏‏‎‏‎‎‎‏‎‏‎‎‎Brightness‎‏‎‎‏‎"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‎‏‎‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‎‎Color inversion‎‏‎‎‏‎"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‏‏‎‏‏‎‎‎‎‏‏‎‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‎‏‎‎‎‎Color correction‎‏‎‎‏‎"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‎‎‎‏‎‎‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‏‎‏‎‏‎‏‎‎‏‎‏‏‏‎‎‏‎‏‏‎‎‎‎‎‎‎‏‎‎User settings‎‏‎‎‏‎"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‎‎‎‏‏‏‎‎‎‎‏‎‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‎Manage users‎‏‎‎‏‎"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‎‎‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‏‏‎‎‏‏‏‎‎‏‏‎‎‎‏‎‎‏‎‎‏‏‎‏‎‎‏‎‏‏‏‏‎‎‎‏‎Done‎‏‎‎‏‎"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‏‏‎‏‏‎‎‎‏‏‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‏‎‏‏‎‎‏‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‎‎‏‎‎‎‏‎Close‎‏‎‎‏‎"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‏‏‎‎‎‎‎‏‏‏‎‎‏‏‏‏‏‎‏‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‏‎‏‏‎Connected‎‏‎‎‏‎"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‎‏‎‎‎‎‎‏‎‎‎‏‏‎‏‎‎‏‏‏‎‎‎‎‏‎‎‏‏‎‎‏‏‏‎‎‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‎‏‏‎‎‎Microphone available‎‏‎‎‏‎"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‎‎‎‎‏‎‎‏‏‏‏‎‎‎‏‏‎‎‎‎‎‎‏‎‏‏‏‏‏‏‎‎‎‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎Camera available‎‏‎‎‏‎"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‎‎‏‎‎‏‎‎‏‎‎‏‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‎Microphone and camera available‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‏‎‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‏‎‏‎‏‎‏‏‎‏‎‎‏‎‎‎‎‏‏‎‎‎‏‎‎‏‎‎‎‎‎‏‎Microphone turned on‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‎‏‎Microphone turned off‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‎‏‎‎‎‏‎‎‏‏‎‏‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎‏‏‎‏‎‎‏‎‏‏‎‏‎‏‎‎‎‎‎Microphone is enabled for all apps and services.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‏‎‎‎‎‏‎‏‎‏‏‏‏‎‏‎‏‎‎‎‏‎‎‏‏‏‏‎‎‎‎‎‎‎‎‎‎‏‎‎‏‎‎Microphone access is disabled for all apps and services. You can enable microphone access in Settings &gt; Privacy &gt; Microphone.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‎‏‏‏‏‏‎‏‎‏‏‏‎‏‎‎‎‏‏‎‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‏‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎Microphone access is disabled for all apps and services. You can change this in Settings &gt; Privacy &gt; Microphone.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‎‏‏‎‏‎‏‎‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎Camera turned on‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎‏‎‎‎‎‏‎‎‏‎‏‏‎‎‏‏‎‎‏‎‎‏‎‏‎‎‎‎Camera turned off‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‏‎‎‏‏‎‏‏‎‏‎‎‎‏‏‏‎‏‎‎‏‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‎‏‎‎‎‎‏‏‎‎Camera is enabled for all apps and services.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‏‎‏‏‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‏‏‏‏‏‎‎‎‎Camera access is disabled for all apps and services.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‏‎‎‏‎‏‎‏‎‎‎‎‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‎‎‎‏‎‎‏‎To use the microphone button, enable microphone access in Settings.‎‏‎‎‏‎"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‏‏‎‎‎‎‎‎‏‎‏‎‏‏‎‏‏‎‎‏‏‎‎‏‎‏‏‎‎‎‏‏‎‏‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‎‎‎‎Open settings.‎‏‎‎‏‎"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‏‎‎‏‏‏‎‏‏‎‎‏‏‏‎‏‎‏‏‎‏‎‏‏‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎Other device‎‏‎‎‏‎"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‎‎‎‏‎‎‎‏‏‏‏‎‎‎‎‎‏‏‎‎‏‏‎‏‏‎‏‎‏‎‎‏‎‏‏‏‎‎‏‏‎‎‏‏‏‎‎‏‎‎‎‏‏‎Toggle Overview‎‏‎‎‏‎"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‏‎‏‎‎‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‏‏‎‏‏‏‏‎‏‎‎‏‏‎‎You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.‎‏‎‎‏‎"</string>
@@ -373,6 +386,11 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‏‎‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‏‏‎‏‎‏‏‎‏‎‎‎‏‏‎When you\'re sharing, recording, or casting an app, ‎‏‎‎‏‏‎<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>‎‏‎‎‏‏‏‎ has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.‎‏‎‎‏‎"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‏‏‏‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‏‏‎‎Continue‎‏‎‎‏‎"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‏‎‎‎‎‎‏‎‏‎‎‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‎Share or record an app‎‏‎‎‏‎"</string>
+    <string name="media_projection_permission_dialog_system_service_title" msgid="6827129613741303726">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‏‏‎‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‏‎‎‏‏‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‎Allow this app to share or record?‎‏‎‎‏‎"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen" msgid="8801616203805837575">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‎‎‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‏‏‎‏‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎When you\'re sharing, recording, or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.‎‏‎‎‏‎"</string>
+    <string name="media_projection_permission_dialog_system_service_warning_single_app" msgid="543310680568419338">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‎‏‏‏‏‏‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‏‎‏‎‎When you\'re sharing, recording, or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.‎‏‎‎‏‎"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_title" msgid="2113331792064527203">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‏‎‏‎‎‎‎‎‏‎‎‎‎‎‎‎‏‎‎‎‏‎‏‏‎‎‏‏‎‏‎‎‎‏‎‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‏‏‎Blocked by your IT admin‎‏‎‎‏‎"</string>
+    <string name="screen_capturing_disabled_by_policy_dialog_description" msgid="6015975736747696431">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‎‎‎‏‎‏‎‎‎‎‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‏‏‏‎Screen capturing is disabled by device policy‎‏‎‎‏‎"</string>
     <string name="clear_all_notifications_text" msgid="348312370303046130">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‎‏‏‏‎‏‎‎‎‏‎‎‎‎‎‏‎‏‎‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‎Clear all‎‏‎‎‏‎"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‏‏‎‎‏‏‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‎‎‎‎‏‏‎‎‏‏‎‏‏‏‏‏‎‏‏‏‏‏‎‎‎Manage‎‏‎‎‏‎"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‎‏‎‎‎‎‎‏‏‏‎‏‏‏‎‎‏‏‎‏‏‏‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‏‏‎‎‏‏‎‎History‎‏‎‎‏‎"</string>
@@ -727,6 +745,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‎‎‎‏‏‏‏‏‏‏‎‎Turn off mobile data?‎‏‎‎‏‎"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‏‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‏‏‏‎‎‏‎‏‎‏‎‎‎‏‎‏‎‎‎‏‎‏‏‎‎‎‏‎‏‏‏‏‎You wont have access to data or the internet through ‎‏‎‎‏‏‎<xliff:g id="CARRIER">%s</xliff:g>‎‏‎‎‏‏‏‎. Internet will only be available via Wi-Fi.‎‏‎‎‏‎"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‏‏‏‏‎‎‎‎‏‎‏‎‏‎‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‎‏‎‎‏‏‏‏‏‏‏‎‎your carrier‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‏‏‏‎‎‎‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‎‎Switch back to ‎‏‎‎‏‏‎<xliff:g id="CARRIER">%s</xliff:g>‎‏‎‎‏‏‏‎?‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‏‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‏‎‏‎‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‎‏‏‏‎‏‏‎‎‎Mobile data wont automatically switch based on availability‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎‏‎‎‎‎‎‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎No thanks‎‏‎‎‏‎"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‎‏‎‎‎‎‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‏‏‏‏‏‎‎‏‏‎‎‎Yes, switch‎‏‎‎‏‎"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‏‏‏‎‎‏‎‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎‏‏‏‎‏‎‏‎‏‏‎‏‎‏‎‎‎Because an app is obscuring a permission request, Settings can’t verify your response.‎‏‎‎‏‎"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‏‏‏‎‎‎‏‎‎‎‎‎‎‎‏‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‎‏‎‎‏‎Allow ‎‏‎‎‏‏‎<xliff:g id="APP_0">%1$s</xliff:g>‎‏‎‎‏‏‏‎ to show ‎‏‎‎‏‏‎<xliff:g id="APP_2">%2$s</xliff:g>‎‏‎‎‏‏‏‎ slices?‎‏‎‎‏‎"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‎‏‎‏‏‏‎‎‏‎‏‎‏‏‏‏‎‏‎‏‎‎‏‏‎‎‎‎‎‏‏‏‎‏‎‏‏‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎‎- It can read information from ‎‏‎‎‏‏‎<xliff:g id="APP">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
@@ -785,12 +807,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‎‏‎‎‏‏‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‏‎‎‏‎‎Magnifier window settings‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‎‏‏‎‎‎‏‏‎‏‎‎‏‏‎‎‎‎‏‎‎‏‏‏‏‎‎‏‏‏‏‏‎‏‎‎‏‏‏‎‎Tap to open accessibility features. Customize or replace this button in Settings.‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎"<annotation id="link">"‎‏‎‎‏‏‏‎View settings‎‏‎‎‏‏‎"</annotation>"‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‎‏‎‎‏‏‎‏‏‎‎‎‎‏‎‎‏‎‏‎‏‎‏‎‎‏‏‎‏‏‏‎‎‎‎‏‎‏‏‏‏‏‏‎‎‎‏‎‎‎‏‏‎‏‎Move button to the edge to hide it temporarily‎‏‎‎‏‎"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‎‎‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‎‎‏‎Undo‎‏‎‎‏‎"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‎‎‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‏‏‏‎‏‎‏‎‎‏‎‎‏‏‎‏‎‏‏‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎{label}‎‏‎‎‏‏‏‎ shortcut removed‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‎‎‎‏‏‏‏‏‎‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‏‏‏‎‏‎‏‎‎‏‎‎‏‏‎‏‎‏‏‏‏‏‎‎‏‎‏‏‎‎‏‎‎# shortcuts removed‎‏‎‎‏‎}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‏‏‎‏‏‏‏‏‏‏‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‎‎‏‎‏‎‎‏‎Move top left‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‏‏‎‏‎‏‎‏‏‏‏‎‏‎‎‎‎‏‎‎‎‏‏‎‏‎‎‎‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‎Move top right‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‏‏‎‎‎‏‏‏‎‎‎‏‏‏‎‏‏‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‎‎‎‏‏‎‏‏‏‎‏‎Move bottom left‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‏‎‎‎‏‏‏‏‎‎‎‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‎‏‎‏‏‏‎‏‎‎‎Move bottom right‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‎‏‎‏‏‎‏‎‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎‎‏‎‎Move to edge and hide‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‏‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‏‎‏‏‏‎‏‏‏‏‎‎Move out edge and show‎‏‎‎‏‎"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‎‏‏‏‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‎‎‏‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‎‏‎‏‎‎‎‏‏‏‎Remove‎‏‎‎‏‎"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎‎‏‎‏‏‎‏‏‎‎‏‎‏‎‎‎‏‎‏‏‎‏‏‏‎‏‎‎‎‏‏‎‎‎‏‎‏‎toggle‎‏‎‎‏‎"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‏‎‎‏‎‏‏‎‎‏‎‎‎‏‏‏‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‎‏‏‎‎‎‎‎‎‏‎Device controls‎‏‎‎‏‎"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‎‎Choose app to add controls‎‏‎‎‏‎"</string>
@@ -933,6 +958,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‎‎‏‏‏‏‎‏‏‎‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‏‎‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‏‎Mobile data‎‏‎‎‏‎"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‏‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‏‎‏‎‎‎‏‎‎‏‏‏‏‏‎‎‏‎‎‏‎‏‎‏‎‎‎‏‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="STATE">%1$s</xliff:g>‎‏‎‎‏‏‏‎ / ‎‏‎‎‏‏‎<xliff:g id="NETWORKMODE">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‎‎‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‎‎‏‏‎‏‏‏‎‎‎‏‎‎‏‎‏‏‏‏‎‎‎‏‎‎‏‏‏‏‏‎‎‏‏‎Connected‎‏‎‎‏‎"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‎‎‏‎‏‎‏‎‎‎‎Temporarily connected‎‏‎‎‏‎"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‏‎‏‏‏‏‏‏‏‎‏‏‏‏‎‎‎‎‎‎‏‎‏‎‏‏‏‎‏‏‏‏‎‏‎‎‏‏‏‎‎‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎Poor connection‎‏‎‎‏‎"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‎‎‏‎‎‏‎‎‎‎‎‎‏‏‎‏‏‎‎‎‏‏‏‎‎‏‏‎‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‎‏‎‎‏‏‏‎Mobile data won\'t auto‑connect‎‏‎‎‏‎"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‏‎‎‏‎‏‏‏‏‏‏‎‎‎‏‎No connection‎‏‎‎‏‎"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‎‏‎‏‎‏‏‏‏‏‏‎‎‎‎‏‏‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎No other networks available‎‏‎‎‏‎"</string>
diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml
index e7a6cf1..d6f9a5e 100644
--- a/packages/SystemUI/res/values-es-rUS/strings.xml
+++ b/packages/SystemUI/res/values-es-rUS/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"No se reconoce el rostro"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Usa la huella dactilar"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth conectado"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Se desconoce el porcentaje de la batería."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectado a <xliff:g id="BLUETOOTH">%s</xliff:g>"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brillo"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Invertir colores"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Corregir colores"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Configuración del usuario"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Administrar usuarios"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Listo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Cerrar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Micrófono disponible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Cámara disponible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Micrófono y cámara disponibles"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Se activó el micrófono"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Se desactivó el micrófono"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Se habilitó el micrófono para todos los servicios y las apps."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"El acceso al micrófono está inhabilitado para todos los servicios y las apps. Puedes habilitar su acceso en Configuración &gt; Privacidad &gt; Micrófono."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"El acceso al micrófono está inhabilitado para todos los servicios y las apps. Puedes habilitarlo en Configuración &gt; Privacidad &gt; Micrófono."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Se activó la cámara"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Se desactivó la cámara"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Se habilitó la cámara para todos los servicios y las apps."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"El acceso a la cámara está inhabilitado para todos los servicios y las apps."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Para habilitar el botón de micrófono, habilita su acceso en Configuración."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir Configuración"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Otro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Ocultar o mostrar Recientes"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"No te molestarán los sonidos ni las vibraciones, excepto las alarmas, los recordatorios, los eventos y las llamadas de los emisores que especifiques. Podrás escuchar el contenido que reproduzcas, como música, videos y juegos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Cuando compartas, grabes o transmitas una app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> podrá acceder a todo el contenido que se muestre o reproduzca en ella. Por lo tanto, debes tener cuidado con contraseñas, detalles de pagos, mensajes y otra información sensible."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartir o grabar una app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Borrar todo"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Administrar"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"¿Deseas desactivar los datos móviles?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"No tendrás acceso a datos móviles ni a Internet a través de <xliff:g id="CARRIER">%s</xliff:g>. Solo podrás conectarte a Internet mediante Wi‑Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"tu proveedor"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"¿Volver a cambiar a <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Los datos móviles no cambiarán automáticamente en función de la disponibilidad"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No, gracias"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sí, cambiar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Como una app está bloqueando una solicitud de permiso, Configuración no puede verificar tu respuesta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"¿Permitir que <xliff:g id="APP_0">%1$s</xliff:g> muestre fragmentos de <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Puede leer información sobre <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configuración de la ventana de ampliación"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Presiona para abrir las funciones de accesibilidad. Personaliza o cambia botón en Config.\n\n"<annotation id="link">"Ver config"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mueve el botón hacia el borde para ocultarlo temporalmente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Deshacer"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Se quitó el acceso directo {label}}many{Se quitaron # de accesos directos}other{Se quitaron # accesos directos}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover arriba a la izquierda"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover arriba a la derecha"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover abajo a la izquierda"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover abajo a la derecha"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover fuera de borde y ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mover fuera de borde y mostrar"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Quitar"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"activar o desactivar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controles de dispositivos"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Elige la app para agregar los controles"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Datos móviles"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conexión establecida"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Conectado temporalmente"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Conexión deficiente"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"No se conectarán automáticamente los datos móviles"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sin conexión"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No hay otras redes disponibles"</string>
diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml
index e8218b0..9480e4c 100644
--- a/packages/SystemUI/res/values-es/strings.xml
+++ b/packages/SystemUI/res/values-es/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"No se reconoce la cara"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Usa la huella digital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth conectado"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Porcentaje de batería desconocido."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectado a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brillo"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Invertir colores"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Corrección de color"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Ajustes de usuario"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gestionar usuarios"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Hecho"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Cerrar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Micrófono disponible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Cámara disponible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Micrófono y cámara disponible"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Micrófono activado"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Micrófono desactivado"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"El micrófono está habilitado para todas las aplicaciones y servicios."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"El acceso al micrófono está inhabilitado para todas las aplicaciones y servicios. Puedes habilitar el acceso al micrófono en Ajustes &gt; Privacidad &gt; Micrófono."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"El acceso al micrófono está inhabilitado para todas las aplicaciones y servicios. Puedes cambiarlo en Ajustes &gt; Privacidad &gt; Micrófono."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Cámara activada"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Cámara desactivada"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"La cámara está habilitada para todas las aplicaciones y servicios."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"El acceso a la cámara está inhabilitado para todas las aplicaciones y servicios."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Para usar el botón del micrófono, habilita el acceso al micrófono en Ajustes."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir ajustes"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Otro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Mostrar u ocultar aplicaciones recientes"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"No te molestarán los sonidos ni las vibraciones, excepto las alarmas, los recordatorios, los eventos y las llamadas que especifiques. Seguirás escuchando el contenido que quieras reproducir, como música, vídeos y juegos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Cuando compartas, grabes o envíes una aplicación, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> podrá acceder a todo lo que muestre o reproduzca la aplicación. Debes tener cuidado con contraseñas, detalles de pagos, mensajes o cualquier otra información sensible."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartir o grabar una aplicación"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Borrar todo"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gestionar"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"¿Desactivar datos móviles?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"No tendrás acceso a datos móviles ni a Internet a través de <xliff:g id="CARRIER">%s</xliff:g>. Solo podrás conectarte a Internet mediante Wi‑Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"tu operador"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"¿Cambiar de nuevo a <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Los datos móviles no cambiarán automáticamente en función de la disponibilidad"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No, gracias"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sí, cambiar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Una aplicación impide ver una solicitud de permiso, por lo que Ajustes no puede verificar tu respuesta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"¿Permitir que <xliff:g id="APP_0">%1$s</xliff:g> muestre fragmentos de <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Puede leer información de <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configuración de la ventana de la lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toca para abrir funciones de accesibilidad. Personaliza o sustituye este botón en Ajustes.\n\n"<annotation id="link">"Ver ajustes"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mueve el botón hacia el borde para ocultarlo temporalmente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Deshacer"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} acceso directo eliminado}many{# accesos directos eliminados}other{# accesos directos eliminados}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover arriba a la izquierda"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover arriba a la derecha"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover abajo a la izquierda"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover abajo a la derecha"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover al borde y ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mover al borde y mostrar"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Quitar"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"activar/desactivar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Control de dispositivos"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Elige una aplicación para añadir controles"</string>
@@ -839,7 +869,7 @@
     <string name="controls_media_seekbar_description" msgid="4389621713616214611">"<xliff:g id="ELAPSED_TIME">%1$s</xliff:g> de <xliff:g id="TOTAL_TIME">%2$s</xliff:g>"</string>
     <string name="controls_media_button_play" msgid="2705068099607410633">"Reproducir"</string>
     <string name="controls_media_button_pause" msgid="8614887780950376258">"Pausar"</string>
-    <string name="controls_media_button_prev" msgid="8126822360056482970">"Pista anterior"</string>
+    <string name="controls_media_button_prev" msgid="8126822360056482970">"Canción anterior"</string>
     <string name="controls_media_button_next" msgid="6662636627525947610">"Siguiente pista"</string>
     <string name="controls_media_button_connecting" msgid="3138354625847598095">"Conectando"</string>
     <string name="controls_media_smartspace_rec_title" msgid="1699818353932537407">"Reproducir"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Datos móviles"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conectado"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Conectada temporalmente"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Conexión inestable"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Los datos móviles no se conectarán automáticamente"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sin conexión"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"No hay otras redes disponibles"</string>
diff --git a/packages/SystemUI/res/values-es/tiles_states_strings.xml b/packages/SystemUI/res/values-es/tiles_states_strings.xml
index d7a8133..fe4cbed 100644
--- a/packages/SystemUI/res/values-es/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values-es/tiles_states_strings.xml
@@ -78,8 +78,8 @@
   </string-array>
   <string-array name="tile_states_location">
     <item msgid="3316542218706374405">"No disponible"</item>
-    <item msgid="4813655083852587017">"Desactivado"</item>
-    <item msgid="6744077414775180687">"Activado"</item>
+    <item msgid="4813655083852587017">"Desactivada"</item>
+    <item msgid="6744077414775180687">"Activada"</item>
   </string-array>
   <string-array name="tile_states_hotspot">
     <item msgid="3145597331197351214">"No disponible"</item>
diff --git a/packages/SystemUI/res/values-et/strings.xml b/packages/SystemUI/res/values-et/strings.xml
index 11a9dc9..c5ac0c7 100644
--- a/packages/SystemUI/res/values-et/strings.xml
+++ b/packages/SystemUI/res/values-et/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Nägu ei õnnestu tuvastada"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Kasutage sõrmejälge"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth on ühendatud."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Aku laetuse protsent on teadmata."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Ühendatud: <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Heledus"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Värvide ümberpööramine"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Värviparandus"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Kasutaja seaded"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Kasutajate haldamine"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Valmis"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Sule"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Ühendatud"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon on saadaval"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kaamera on saadaval"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon ja kaamera on saadaval"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Muu seade"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Lehe Ülevaade sisse- ja väljalülitamine"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Helid ja värinad ei sega teid. Kuulete siiski enda määratud äratusi, meeldetuletusi, sündmusi ja helistajaid. Samuti kuulete kõike, mille esitamise ise valite, sh muusika, videod ja mängud."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kui jagate, salvestate või kannate rakendust üle, on rakendusel <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> juurdepääs kõigele, mida selles rakenduses kuvatakse või esitatakse. Seega olge paroolide, makseteabe, sõnumite ja muu tundliku teabega ettevaatlik."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Jätka"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Rakenduse jagamine või salvestamine"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Tühjenda kõik"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Haldamine"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Ajalugu"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Kas lülitada mobiilne andmeside välja?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Pärast seda pole teil operaatori <xliff:g id="CARRIER">%s</xliff:g> kaudu juurdepääsu andmesidele ega internetile. Internet on saadaval ainult WiFi kaudu."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"teie operaator"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Kas vahetada tagasi operaatorile <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobiilandmeside operaatorit ei vahetata saadavuse alusel automaatselt"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Tänan, ei"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Jah, vaheta"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Seaded ei saa teie vastust kinnitada, sest rakendus varjab loataotlust."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Kas lubada rakendusel <xliff:g id="APP_0">%1$s</xliff:g> näidata rakenduse <xliff:g id="APP_2">%2$s</xliff:g> lõike?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- See saab lugeda teavet rakendusest <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Luubi akna seaded"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Puudutage juurdepääsufunktsioonide avamiseks. Kohandage nuppu või asendage see seadetes.\n\n"<annotation id="link">"Kuva seaded"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Teisaldage nupp serva, et see ajutiselt peita"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Võta tagasi"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} otsetee on eemaldatud}other{# otseteed on eemaldatud}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Teisalda üles vasakule"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Teisalda üles paremale"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Teisalda alla vasakule"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Teisalda alla paremale"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Teisalda serva ja kuva"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Teisalda servast eemale ja kuva"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Eemalda"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"lülita"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Seadmete juhikud"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Valige juhtelementide lisamiseks rakendus"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiilne andmeside"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ühendatud"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Ajutiselt ühendatud"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Kehv ühendus"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobiilset andmesideühendust ei looda automaatselt"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Ühendus puudub"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Ühtegi muud võrku pole saadaval"</string>
diff --git a/packages/SystemUI/res/values-eu/strings.xml b/packages/SystemUI/res/values-eu/strings.xml
index 3714781..2461886 100644
--- a/packages/SystemUI/res/values-eu/strings.xml
+++ b/packages/SystemUI/res/values-eu/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Ezin da ezagutu aurpegia"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Erabili hatz-marka"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetootha konektatuta."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Bateriaren ehunekoa ezezaguna da."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> gailura konektatuta."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Distira"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Kolore-alderantzikatzea"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Koloreen zuzenketa"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Erabiltzaile-ezarpenak"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Kudeatu erabiltzaileak"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Eginda"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Itxi"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Konektatuta"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofonoa erabilgarri dago"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera erabilgarri dago"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofonoa eta kamera erabilgarri daude"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Aktibatu da mikrofonoa"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Desaktibatu da mikrofonoa"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Aplikazio eta zerbitzu guztiek mikrofonoa erabil dezakete."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofonoa erabiltzeko baimena desgaituta dago aplikazio eta zerbitzu guztietarako. Mikrofonoa erabiltzeko baimena gaitzeko, joan Ezarpenak &gt; Pribatutasuna &gt; Mikrofonoa atalera."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofonoa erabiltzeko baimena desgaituta dago aplikazio eta zerbitzu guztietarako. Baimen hori aldatzeko, joan Ezarpenak &gt; Pribatutasuna &gt; Mikrofonoa atalera."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Piztu da kamera"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Itzali da kamera"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Aplikazio eta zerbitzu guztiek kamera erabil dezakete."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kamera erabiltzeko baimena desgaituta dago aplikazio eta zerbitzu guztietarako."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Mikrofonoaren botoia erabiltzeko, gaitu mikrofonoa erabiltzeko baimena ezarpenetan."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Ireki ezarpenak."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Beste gailu bat"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Aldatu ikuspegi orokorra"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Gailuak ez du egingo ez soinurik ez dardararik, baina alarmak, gertaera eta abisuen tonuak, eta aukeratzen dituzun deitzaileen dei-tonuak joko ditu. Bestalde, zuk erreproduzitutako guztia entzungo duzu, besteak beste, musika, bideoak eta jokoak."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Aplikazio bat partekatzen, grabatzen edo igortzen ari zarenean, aplikazio horretan ikusgai dagoen edo bertan erreproduzitzen ari den guztirako sarbidea du <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aplikazioak. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin edo bestelako kontuzko informazioarekin."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Egin aurrera"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partekatu edo grabatu aplikazioak"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Garbitu guztiak"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Kudeatu"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Datu-konexioa desaktibatu nahi duzu?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> erabilita ezingo dituzu erabili datuak edo Internet. Wifi-sare baten bidez soilik konektatu ahal izango zara Internetera."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"Zure operadorea"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> operadorera aldatu nahi duzu berriro?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Datu-konexioa ez da automatikoki aldatuko erabilgarritasunaren arabera"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ez, eskerrik asko"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Bai, aldatu nahi dut"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Aplikazio bat baimen-eskaera oztopatzen ari denez, ezarpenek ezin dute egiaztatu erantzuna."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_2">%2$s</xliff:g> aplikazioaren zatiak erakusteko baimena eman nahi diozu <xliff:g id="APP_0">%1$s</xliff:g> aplikazioari?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- <xliff:g id="APP">%1$s</xliff:g> aplikazioaren informazioa irakur dezake."</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Luparen leihoaren ezarpenak"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Erabilerraztasun-eginbideak irekitzeko, sakatu hau. Ezarpenetan pertsonalizatu edo ordez dezakezu botoia.\n\n"<annotation id="link">"Ikusi ezarpenak"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Eraman botoia ertzera aldi baterako ezkutatzeko"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Desegin"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} lasterbide kendu da}other{# lasterbide kendu dira}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Eraman goialdera, ezkerretara"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Eraman goialdera, eskuinetara"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Eraman behealdera, ezkerretara"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Eraman behealdera, eskuinetara"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Eraman ertzera eta ezkutatu"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Atera ertzetik eta erakutsi"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Kendu"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"aldatu"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Gailuak kontrolatzeko widgetak"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Aukeratu aplikazio bat kontrolatzeko aukerak gehitzeko"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Datu-konexioa"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> (<xliff:g id="NETWORKMODE">%2$s</xliff:g>)"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Konektatuta"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Aldi baterako konektatuta"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Konexio ahula"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Ez da automatikoki aktibatuko datu-konexioa"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Konexiorik gabe"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Ez dago beste sare erabilgarririk"</string>
diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml
index 183a9ed..458b480 100644
--- a/packages/SystemUI/res/values-fa/strings.xml
+++ b/packages/SystemUI/res/values-fa/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"چهره شناسایی نشد"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"از اثر انگشت استفاده کنید"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"بلوتوث متصل است."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"درصد شارژ باتری مشخص نیست."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"به <xliff:g id="BLUETOOTH">%s</xliff:g> متصل شد."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"روشنایی"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"وارونگی رنگ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"تصحیح رنگ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"تنظیمات کاربر"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"مدیریت کاربران"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"تمام"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"بستن"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"متصل"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"میکروفون دردسترس است"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"دوربین دردسترس است"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"میکروفون و دوربین دردسترس‌اند"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"میکروفون روشن شد"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"میکروفون خاموش شد"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"میکروفون برای همه برنامه‌ها و سرویس‌ها فعال است."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"‏دسترسی به میکروفون برای همه برنامه‌ها و سرویس‌ها غیرفعال است. می‌توانید دسترسی به میکروفون را در «تنظیمات &gt; حریم خصوصی &gt; میکروفون» فعال کنید."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"‏دسترسی به میکروفون برای همه برنامه‌ها و سرویس‌ها غیرفعال است. می‌توانید آن را در «تنظیمات &gt; حریم خصوصی &gt; میکروفون» تغییر دهید."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"دوربین روشن شد"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"دوربین خاموش شد"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"دوربین برای همه برنامه‌ها و سرویس‌ها فعال است."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"دسترسی به دوربین برای همه برنامه‌ها و سرویس‌ها غیرفعال است."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"برای استفاده از دکمه میکروفون، دسترسی به میکروفون را در «تنظیمات» فعال کنید."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"باز کردن تنظیمات."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"دستگاه دیگر"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"تغییر وضعیت نمای کلی"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"به‌جز هشدارها، یادآوری‌ها، رویدادها و تماس‌گیرندگانی که خودتان مشخص می‌کنید، هیچ صدا و لرزشی نخواهید داشت. همچنان صدای مواردی را که پخش می‌کنید می‌شنوید (ازجمله صدای موسیقی، ویدیو و بازی)."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"وقتی درحال هم‌رسانی، ضبط، یا پخش محتوای برنامه‌ای هستید، <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> به همه محتوایی که در آن برنامه نمایان است یا پخش می‌شود دسترسی دارد. بنابراین مراقب گذرواژه‌ها، جزئیات پرداخت، پیام‌ها، یا دیگر اطلاعات حساس باشید."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ادامه"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"هم‌رسانی یا ضبط برنامه"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"پاک کردن همه موارد"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"مدیریت"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"سابقه"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"داده تلفن همراه خاموش شود؟"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"‏نمی‌توانید ازطریق <xliff:g id="CARRIER">%s</xliff:g> به داده یا اینترنت دسترسی داشته باشید. اینترنت فقط ازطریق Wi-Fi در دسترس خواهد بود."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"شرکت مخابراتی شما"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"می‌خواهید به <xliff:g id="CARRIER">%s</xliff:g> برگردید؟"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"وضعیت داده تلفن همراه به‌طور خودکار براساس دردسترس بودن تغییر نخواهد کرد"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"نه متشکرم"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"بله، عوض شود"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"چون برنامه‌ای درحال ایجاد تداخل در درخواست مجوز است، «تنظیمات» نمی‌تواند پاسخ شما را تأیید کند."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"به <xliff:g id="APP_0">%1$s</xliff:g> اجازه داده شود تکه‌های <xliff:g id="APP_2">%2$s</xliff:g> را نشان دهد؟"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- می‌تواند اطلاعات <xliff:g id="APP">%1$s</xliff:g> را بخواند"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"تنظیمات پنجره ذره‌بین"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"برای باز کردن ویژگی‌های دسترس‌پذیری ضربه بزنید. در تنظیمات این دکمه را سفارشی یا جایگزین کنید\n\n"<annotation id="link">"تنظیمات"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"برای پنهان کردن موقتی دکمه، آن را به لبه ببرید"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"واگرد"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} میان‌بر برداشته شد}one{# میان‌بر برداشته شد}other{# میان‌بر برداشته شد}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"انتقال به بالا سمت راست"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"انتقال به بالا سمت چپ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"انتقال به پایین سمت راست"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"انتقال به پایین سمت چپ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"انتقال به لبه و پنهان کردن"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"انتقال به خارج از لبه و نمایش"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"برداشتن"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"روشن/ خاموش کردن"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"کنترل‌های دستگاه"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"انتخاب برنامه برای افزودن کنترل‌ها"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"داده تلفن همراه"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"متصل است"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"موقتاً متصل است"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"اتصال ضعیف"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"داده تلفن همراه به‌طور خودکار متصل نخواهد شد"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"اتصال برقرار نیست"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"شبکه دیگری وجود ندارد"</string>
diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml
index df05015..1da012a 100644
--- a/packages/SystemUI/res/values-fi/strings.xml
+++ b/packages/SystemUI/res/values-fi/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Kasvoja ei voi tunnistaa"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Käytä sormenjälkeä"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth yhdistetty."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Akun varaustaso ei tiedossa."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Yhteys: <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Kirkkaus"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Käänteiset värit"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Värinkorjaus"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Käyttäjäasetukset"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Ylläpidä käyttäjiä"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Valmis"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Sulje"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Yhdistetty"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofoni käytettävissä"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera käytettävissä"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofoni ja kamera käytettävissä"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofoni laitettu päälle"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofoni laitettu pois päältä"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofonin käyttö on sallittu kaikille sovelluksille ja palveluille."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Pääsy mikrofoniin on estetty kaikilta sovelluksilta ja palveluilta. Jos haluat sallia pääsyn mikrofoniin, valitse Asetukset &gt; Yksityisyys &gt; Mikrofoni."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Pääsy mikrofoniin on estetty kaikilta sovelluksilta ja palveluilta. Jos haluat muuttaa tätä, valitse Asetukset &gt; Yksityisyys &gt; Mikrofoni."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera laitettu päälle"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera laitettu pois päältä"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kameran käyttö on sallittu kaikille sovelluksille ja palveluille."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Pääsy kameraan on estetty kaikilta sovelluksilta ja palveluilta."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Jos haluat käyttää mikrofonipainiketta, salli pääsy mikrofoniin asetuksista."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Avaa asetukset."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Muu laite"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Näytä/piilota viimeisimmät"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Äänet ja värinät eivät häiritse sinua, paitsi jos ne ovat hälytyksiä, muistutuksia, tapahtumia tai määrittämiäsi soittajia. Kuulet edelleen kaiken valitsemasi sisällön, kuten musiikin, videot ja pelit."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kun jaat, tallennat tai striimaat sovellusta, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> saa pääsyn kaikkeen sovelluksessa näkyvään tai toistettuun sisältöön. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä tai muita arkaluontoisia tietoja."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Jatka"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Jaa sovellus tai tallenna sen sisältöä"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Tyhjennä kaikki"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Muuta asetuksia"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Laitetaanko mobiilidata pois päältä?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> ei enää tarjoa pääsyä dataan eikä internetyhteyttä, joka on saatavilla vain Wi-Fin kautta."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operaattorisi"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Palauta käyttöön <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobiilidata ei vaihdu automaattisesti saatavuuden perusteella"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ei kiitos"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Kyllä, vaihda"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Sovellus peittää käyttöoikeuspyynnön, joten Asetukset ei voi vahvistaa valintaasi."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Saako <xliff:g id="APP_0">%1$s</xliff:g> näyttää osia sovelluksesta <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Se voi lukea tietoja sovelluksesta <xliff:g id="APP">%1$s</xliff:g>."</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Ikkunan suurennuksen asetukset"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Avaa esteettömyysominaisuudet napauttamalla. Yksilöi tai vaihda painike asetuksista.\n\n"<annotation id="link">"Avaa asetukset"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Piilota painike tilapäisesti siirtämällä se reunaan"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Kumoa"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} pikanäppäin poistettu}other{# pikanäppäintä poistettu}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Siirrä vasempaan yläreunaan"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Siirrä oikeaan yläreunaan"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Siirrä vasempaan alareunaan"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Siirrä oikeaan alareunaan"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Siirrä reunaan ja piilota"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Siirrä pois reunasta ja näytä"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Poista"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"vaihda"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Laitehallinta"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Valitse sovellus lisätäksesi säätimiä"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiilidata"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Yhdistetty"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Väliaikaisesti yhdistetty"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Heikko yhteys"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobiilidata ei yhdisty automaattisesti"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Ei yhteyttä"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Ei muita verkkoja käytettävissä"</string>
diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml
index c5c941b..cc8bdb8 100644
--- a/packages/SystemUI/res/values-fr-rCA/strings.xml
+++ b/packages/SystemUI/res/values-fr-rCA/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Visage non reconnu"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Utiliser l\'empreinte digitale"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connecté"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Pourcentage de la pile inconnu."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connecté à : <xliff:g id="BLUETOOTH">%s</xliff:g>"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Luminosité"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversion des couleurs"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correction des couleurs"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Paramètres utilisateur"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gérer les utilisateurs"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Terminé"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Fermer"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connecté"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microphone disponible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Appareil photo disponible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microphone et appareil photo disponibles"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microphone activé"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microphone désactivé"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Le microphone est activé pour toutes les applications et tous les services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"L\'accès au microphone est désactivé pour toutes les applications et tous les services. Vous pouvez l\'activer dans Paramètres &gt; Confidentialité &gt; Microphone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"L\'accès au microphone est désactivé pour toutes les applications et tous les services. Vous pouvez modifier cette option dans Paramètres &gt; Confidentialité &gt; Microphone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Appareil photo activé"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Appareil photo désactivé"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"L\'appareil photo est activé pour toutes les applications et tous les services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"L\'accès à l\'appareil photo est désactivé pour toutes les applications et tous les services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Pour utiliser le bouton du microphone, activez l\'accès au microphone dans les paramètres."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Ouvrir les paramètres."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Autre appareil"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Basculer l\'aperçu"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Vous ne serez pas dérangé par les sons et les vibrations, sauf pour les alarmes, les rappels, les événements et les appelants que vous sélectionnez. Vous entendrez tout ce que vous choisissez d\'écouter, y compris la musique, les vidéos et les jeux."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Lorsque vous partagez, enregistrez ou diffusez une application, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> a accès à tout ce qui est affiché ou lu sur cette application. Par conséquent, soyez prudent avec les mots de passe, les détails du paiement, les messages ou toute autre information confidentielle."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuer"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partager ou enregistrer une application"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Tout effacer"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gérer"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historique"</string>
@@ -607,8 +630,8 @@
     <string name="accessibility_status_bar_headset" msgid="2699275863720926104">"Écouteurs connectés"</string>
     <string name="data_saver" msgid="3484013368530820763">"Économiseur de données"</string>
     <string name="accessibility_data_saver_on" msgid="5394743820189757731">"La fonction Économiseur de données est activée"</string>
-    <string name="switch_bar_on" msgid="1770868129120096114">"Activé"</string>
-    <string name="switch_bar_off" msgid="5669805115416379556">"Désactivé"</string>
+    <string name="switch_bar_on" msgid="1770868129120096114">"Activée"</string>
+    <string name="switch_bar_off" msgid="5669805115416379556">"Désactivée"</string>
     <string name="tile_unavailable" msgid="3095879009136616920">"Non disponible"</string>
     <string name="accessibility_tile_disabled_by_policy_action_description" msgid="6958422730461646926">"En savoir plus"</string>
     <string name="nav_bar" msgid="4642708685386136807">"Barre de navigation"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Désactiver les données cellulaires?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Vous n\'aurez pas accès aux données ni à Internet avec <xliff:g id="CARRIER">%s</xliff:g>. Vous ne pourrez accéder à Internet que par Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"votre fournisseur de services"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Rebasculer vers <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Il n\'y aura pas de basculement automatique entre les données mobiles selon la disponibilité"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Non merci"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Oui, basculer"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Une application obscurcit une demande d\'autorisation, alors Paramètres ne peut pas vérifier votre réponse."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Autoriser <xliff:g id="APP_0">%1$s</xliff:g> à afficher <xliff:g id="APP_2">%2$s</xliff:g> tranches?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Il peut lire de l\'information de <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Paramètres de la fenêtre de loupe"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Touchez pour ouvrir fonction. d\'access. Personnalisez ou remplacez bouton dans Param.\n\n"<annotation id="link">"Afficher param."</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Déplacez le bouton vers le bord pour le masquer temporairement"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Annuler"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} raccourci retiré}one{# raccourci retiré}many{# de raccourcis retirés}other{# raccourcis retirés}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Déplacer dans coin sup. gauche"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Déplacer dans coin sup. droit"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Déplacer dans coin inf. gauche"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Déplacer dans coin inf. droit"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Éloigner du bord et masquer"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Éloigner du bord et afficher"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Retirer"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"basculer"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Commandes des appareils"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Sélectionnez l\'application pour laquelle ajouter des commandes"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Données cellulaires"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connexion active"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Connectée temporairement"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Connexion faible"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Aucune connexion auto. des données cellulaires"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Aucune connexion"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Aucun autre réseau n\'est accessible"</string>
diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml
index 1a2b877..1508b9c 100644
--- a/packages/SystemUI/res/values-fr/strings.xml
+++ b/packages/SystemUI/res/values-fr/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Visage non reconnu"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Utilisez empreinte digit."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth connecté"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Pourcentage de la batterie inconnu."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connecté à : <xliff:g id="BLUETOOTH">%s</xliff:g>"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Luminosité"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversion des couleurs"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correction des couleurs"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Paramètres utilisateur"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gérer les utilisateurs"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"OK"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Fermer"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connecté"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Micro accessible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Caméra accessible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Micro et caméra accessibles"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Micro activé"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Micro désactivé"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Le micro est activé pour tous les services et applis."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"L\'accès au micro est désactivé pour tous les services et applis. Vous pouvez activer l\'accès au micro dans Paramètres &gt; Confidentialité &gt; Micro."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"L\'accès au micro est désactivé pour tous les services et applis. Vous pouvez modifier cette option dans Paramètres &gt; Confidentialité &gt; Micro."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Appareil photo activé"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Appareil photo désactivé"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"L\'appareil photo est activé pour tous les services et applis."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"L\'accès à l\'appareil photo est désactivé pour tous les services et applis."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Pour utiliser le bouton du micro, activez l\'accès au micro dans les paramètres."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Ouvrir les paramètres."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Autre appareil"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Activer/Désactiver l\'aperçu"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Vous ne serez dérangé par aucun son ni aucune vibration, hormis ceux des alarmes, des rappels, des événements et des appels des contacts de votre choix. Le son continuera de fonctionner notamment pour la musique, les vidéos et les jeux."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Lorsque vous partagez, enregistrez ou castez une appli, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> a accès à tout ce qui est visible sur votre écran ou lu sur votre appareil. Faites donc attention à vos mots de passe, détails de mode de paiement, messages ou autres informations sensibles."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuer"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partager ou enregistrer une appli"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Tout effacer"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gérer"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historique"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Désactiver les données mobiles ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Vous n\'aurez pas accès aux données mobiles ni à Internet via <xliff:g id="CARRIER">%s</xliff:g>. Vous ne pourrez accéder à Internet que par Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"votre opérateur"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Rebasculer sur <xliff:g id="CARRIER">%s</xliff:g> ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Il n\'y aura pas de basculement automatique entre les données mobile selon la disponibilité"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Non, merci"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Oui, revenir"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"L\'application Paramètres ne peut pas valider votre réponse, car une application masque la demande d\'autorisation."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Autoriser <xliff:g id="APP_0">%1$s</xliff:g> à afficher des éléments de <xliff:g id="APP_2">%2$s</xliff:g> ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Accès aux informations de <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Paramètres de la fenêtre d\'agrandissement"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Appuyez pour ouvrir fonctionnalités d\'accessibilité. Personnalisez ou remplacez bouton dans paramètres.\n\n"<annotation id="link">"Voir paramètres"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Déplacer le bouton vers le bord pour le masquer temporairement"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Annuler"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} raccourci supprimé}one{# raccourci supprimé}many{# raccourcis supprimés}other{# raccourcis supprimés}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Déplacer en haut à gauche"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Déplacer en haut à droite"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Déplacer en bas à gauche"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Déplacer en bas à droite"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Rapprocher du bord et masquer"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Éloigner du bord et afficher"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Supprimer"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"activer/désactiver"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Commandes des appareils"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Sélectionnez l\'appli pour laquelle ajouter des commandes"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Données mobiles"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connecté"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Connectée temporairement"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Connexion médiocre"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Pas de connexion automatique des données mobiles"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Aucune connexion"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Aucun autre réseau disponible"</string>
diff --git a/packages/SystemUI/res/values-gl/strings.xml b/packages/SystemUI/res/values-gl/strings.xml
index e5def1e..3807a87 100644
--- a/packages/SystemUI/res/values-gl/strings.xml
+++ b/packages/SystemUI/res/values-gl/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Non se recoñeceu a cara"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Usa a impresión dixital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth conectado"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Descoñécese a porcentaxe da batería."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectado a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brillo"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversión da cor"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Corrección da cor"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Configuración de usuario"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Administrar usuarios"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Feito"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Pechar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"O micrófono está dispoñible"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"A cámara está dispoñible"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"O micrófono e a cámara están dispoñibles"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Activouse o micrófono"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Desactivouse o micrófono"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"O micrófono está activado para todas as aplicacións e servizos."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"O acceso ao micrófono está desactivado para todas as aplicacións e servizos. Podes activar o acceso ao micrófono en Configuración &gt; Privacidade &gt; Micrófono."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"O acceso ao micrófono está desactivado para todas as aplicacións e servizos. Podes cambialo en Configuración &gt; Privacidade &gt; Micrófono."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Activouse a cámara"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Desactivouse a cámara"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"A cámara está activada para todas as aplicacións e servizos."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"O acceso á cámara está desactivado para todas as aplicacións e servizos."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Para usar o botón do micrófono, activa o acceso ao micrófono en Configuración."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir configuración."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Outro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Activar/desactivar Visión xeral"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Non te molestará ningún son nin vibración, agás os procedentes de alarmas, recordatorios, eventos e os emisores de chamada especificados. Seguirás escoitando todo aquilo que decidas reproducir, mesmo a música, os vídeos e os xogos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Cando compartes, gravas ou emites unha aplicación, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ten acceso a todo o que se vexa ou se reproduza nela. Polo tanto, debes ter coidado cos contrasinais, os detalles de pago, as mensaxes ou outra información confidencial."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartir ou gravar unha aplicación"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Eliminar todas"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Xestionar"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string>
@@ -727,6 +750,14 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Queres desactivar os datos móbiles?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Non terás acceso aos datos nin a Internet a través de <xliff:g id="CARRIER">%s</xliff:g>. Internet só estará dispoñible mediante a wifi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"o teu operador"</string>
+    <!-- no translation found for auto_data_switch_disable_title (5146527155665190652) -->
+    <skip />
+    <!-- no translation found for auto_data_switch_disable_message (5885533647399535852) -->
+    <skip />
+    <!-- no translation found for auto_data_switch_dialog_negative_button (2370876875999891444) -->
+    <skip />
+    <!-- no translation found for auto_data_switch_dialog_positive_button (8531782041263087564) -->
+    <skip />
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Dado que unha aplicación se superpón sobre unha solicitude de permiso, a configuración non pode verificar a túa resposta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Queres permitir que <xliff:g id="APP_0">%1$s</xliff:g> mostre fragmentos de aplicación de <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Pode ler información da aplicación <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +816,18 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configuración da ventá da lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toca para abrir as funcións de accesibilidade. Cambia este botón en Configuración.\n\n"<annotation id="link">"Ver configuración"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Para ocultar temporalmente o botón, móveo ata o bordo"</string>
+    <!-- no translation found for accessibility_floating_button_undo (511112888715708241) -->
+    <skip />
+    <!-- no translation found for accessibility_floating_button_undo_message_text (3044079592757099698) -->
+    <skip />
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover á parte super. esquerda"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover á parte superior dereita"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover á parte infer. esquerda"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover á parte inferior dereita"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover ao bordo e ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mover fóra do bordo e mostrar"</string>
+    <!-- no translation found for accessibility_floating_button_action_remove_menu (6730432848162552135) -->
+    <skip />
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"activar/desactivar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Control de dispositivos"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Escolle unha aplicación para engadir controis"</string>
@@ -933,6 +970,10 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Datos móbiles"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conectada"</string>
+    <!-- no translation found for mobile_data_temp_connection_active (4590222725908806824) -->
+    <skip />
+    <!-- no translation found for mobile_data_poor_connection (819617772268371434) -->
+    <skip />
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Os datos móbiles non se conectarán automaticamente"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sen conexión"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Non hai outras redes dispoñibles"</string>
diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml
index 552b049..d992d6a 100644
--- a/packages/SystemUI/res/values-gu/strings.xml
+++ b/packages/SystemUI/res/values-gu/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ચહેરો ઓળખાતો નથી"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"તો ફિંગરપ્રિન્ટ વાપરો"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"બ્લૂટૂથ કનેક્ટ થયું."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"બૅટરીની ટકાવારી અજાણ છે."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> થી કનેક્ટ થયાં."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"તેજ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"વિપરીત રંગમાં બદલવું"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"રંગ સુધારણા"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"વપરાશકર્તા સેટિંગ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"વપરાશકર્તાઓને મેનેજ કરો"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"થઈ ગયું"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"બંધ કરો"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"કનેક્ટ થયેલું"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"માઇક્રોફોનનો ઍક્સેસ ઉપલબ્ધ છે"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"કૅમેરાનો ઍક્સેસ ઉપલબ્ધ છે"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"માઇક્રોફોન અને કૅમેરાનો ઍક્સેસ ઉપલબ્ધ છે"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"માઇક્રોફોન ચાલુ કર્યું"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"માઇક્રોફોન બંધ કર્યું"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"બધી ઍપ અને સેવાઓ માટે માઇક્રોફોન ચાલુ કર્યું."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"બધી ઍપ અને સેવાઓ માટે માઇક્રોફોનનો ઍક્સેસ બંધ કર્યો છે. તમે સેટિંગ &gt; પ્રાઇવસી &gt; માઇક્રોફોનમાં જઈને માઇક્રોફોનનો ઍક્સેસ ચાલુ કરી શકો છો."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"બધી ઍપ અને સેવાઓ માટે માઇક્રોફોનનો ઍક્સેસ બંધ કર્યો છે. તમે સેટિંગ &gt; પ્રાઇવસી &gt; માઇક્રોફોનમાં જઈને આને બદલી શકો છો."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"કૅમેરા ચાલુ કર્યો"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"કૅમેરા બંધ કર્યો"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"બધી ઍપ અને સેવાઓ માટે કૅમેરા ચાલુ કર્યો."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"બધી ઍપ અને સેવાઓ માટે કૅમેરાનો ઍક્સેસ બંધ કર્યો છે."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"માઇક્રોફોન બટનનો ઉપયોગ કરવા માટે, સેટિંગમાં માઇક્રોફોનનો ઍક્સેસ ચાલુ કરો."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"સેટિંગ ખોલો."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"અન્ય ડિવાઇસ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ઝલકને ટૉગલ કરો"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"અલાર્મ, રિમાઇન્ડર, ઇવેન્ટ અને તમે ઉલ્લેખ કરો તે કૉલર સિવાય તમને ધ્વનિ કે વાઇબ્રેશન દ્વારા ખલેલ પહોંચાડવામાં આવશે નહીં. સંગીત, વીડિઓ અને રમતો સહિત તમે જે કંઈપણ ચલાવવાનું પસંદ કરશો તે સંભળાતું રહેશે."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"જ્યારે તમે કોઈ ઍપ શેર, રેકોર્ડ અથવા કાસ્ટ કરી રહ્યાં હો, ત્યારે તે ઍપ પર બતાવવામાં કે ચલાવવામાં આવતી હોય તેવી કોઈપણ વસ્તુનો ઍક્સેસ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ધરાવે છે. તેથી પાસવર્ડ, ચુકવણીની વિગતો, મેસેજ અથવા અન્ય સંવેદનશીલ માહિતીની બાબતે સાવચેત રહેશો."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ચાલુ રાખો"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"કોઈ ઍપ શેર કરો અથવા રેકોર્ડ કરો"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"બધુ સાફ કરો"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"મેનેજ કરો"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ઇતિહાસ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"મોબાઇલ ડેટા બંધ કરીએ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"તમને <xliff:g id="CARRIER">%s</xliff:g> મારફતે ડેટા અથવા ઇન્ટરનેટનો ઍક્સેસ મળશે નહીં. ઇન્ટરનેટ માત્ર વાઇ-ફાઇ દ્વારા ઉપલબ્ધ થશે."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"તમારા કૅરિઅર"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> પર પાછા સ્વિચ કરીએ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"મોબાઇલ ડેટાને ઉપલબ્ધતાના આધારે ઑટોમૅટિક રીતે સ્વિચ કરવામાં આવશે નહીં"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ના, આભાર"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"હા, સ્વિચ કરો"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"કોઈ ઍપ પરવાનગી વિનંતીને અસ્પષ્ટ કરતી હોવાને કારણે, સેટિંગ તમારા પ્રતિસાદને ચકાસી શકતું નથી."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g>ને <xliff:g id="APP_2">%2$s</xliff:g> સ્લાઇસ બતાવવાની મંજૂરી આપીએ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- મારાથી <xliff:g id="APP">%1$s</xliff:g>ની માહિતી વાંચી શકાતી નથી"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"મેગ્નિફાયર વિન્ડોના સેટિંગ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ઍક્સેસિબિલિટી સુવિધાઓ ખોલવા માટે ટૅપ કરો. સેટિંગમાં આ બટનને કસ્ટમાઇઝ કરો અથવા બદલો.\n\n"<annotation id="link">"સેટિંગ જુઓ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"તેને હંગામી રૂપે ખસેડવા માટે બટનને કિનારી પર ખસેડો"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"છેલ્લો ફેરફાર રદ કરો"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} શૉર્ટકટ કાઢી નાખ્યો}one{# શૉર્ટકટ કાઢી નાખ્યો}other{# શૉર્ટકટ કાઢી નાખ્યો}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ઉપર ડાબે ખસેડો"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ઉપર જમણે ખસેડો"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"નીચે ડાબે ખસેડો"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"નીચે જમણે ખસેડો"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"કિનારી પર ખસેડો અને છુપાવો"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"કિનારીથી ખસેડો અને બતાવો"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"કાઢી નાખો"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ટૉગલ કરો"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ડિવાઇસનાં નિયંત્રણો"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"નિયંત્રણો ઉમેરવા માટે ઍપ પસંદ કરો"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"મોબાઇલ ડેટા"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"કનેક્ટ કરેલું"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"હંગામી રીતે કનેક્ટ કર્યું"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"નબળું કનેક્શન"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"મોબાઇલ ડેટા ઑટોમૅટિક રીતે કનેક્ટ થશે નહીં"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"કોઈ કનેક્શન નથી"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"બીજાં કોઈ નેટવર્ક ઉપલબ્ધ નથી"</string>
diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml
index c0052d6..e8458dd 100644
--- a/packages/SystemUI/res/values-hi/strings.xml
+++ b/packages/SystemUI/res/values-hi/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"चेहरे की पहचान नहीं हुई"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"फ़िंगरप्रिंट इस्तेमाल करें"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ब्लूटूथ कनेक्ट किया गया."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"इस बारे में जानकारी नहीं है कि अभी बैटरी कितने प्रतिशत चार्ज है."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> से कनेक्ट किया गया."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"स्क्रीन की रोशनी"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"रंग बदलने की सुविधा"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"रंग में सुधार करने की सुविधा"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"उपयोगकर्ता सेटिंग"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"उपयोगकर्ताओं को मैनेज करें"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"हो गया"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"रद्द करें"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"कनेक्ट है"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"माइक्रोफ़ोन का ऐक्सेस उपलब्ध है"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"कैमरे का ऐक्सेस उपलब्ध है"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"माइक्रोफ़ोन और कैमरे का ऐक्सेस उपलब्ध है"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"माइक्रोफ़ोन का ऐक्सेस चालू किया गया"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"माइक्रोफ़ोन का ऐक्सेस बंद किया गया"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"माइक्रोफ़ोन का ऐक्सेस, सभी ऐप्लिकेशन और सेवाओं के लिए चालू किया गया."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"माइक्रोफ़ोन का ऐक्सेस, सभी ऐप्लिकेशन और सेवाओं के लिए बंद किया गया. इसे चालू करने के लिए, सेटिंग &gt; निजता &gt; माइक्रोफ़ोन पर जाएं."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"माइक्रोफ़ोन का ऐक्सेस, सभी ऐप्लिकेशन और सेवाओं के लिए बंद किया गया. इसे चालू करने के लिए, सेटिंग &gt; निजता &gt; माइक्रोफ़ोन पर जाएं."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"कैमरे का ऐक्सेस चालू किया गया"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"कैमरे का ऐक्सेस बंद किया गया"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"कैमरे का ऐक्सेस, सभी ऐप्लिकेशन और सेवाओं के लिए चालू किया गया."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"सभी ऐप्लिकेशन और सेवाओं के लिए कैमरे का ऐक्सेस बंद किया गया."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"माइक्रोफ़ोन बटन का इस्तेमाल करने के लिए, सेटिंग में जाकर माइक्रोफ़ोन का ऐक्सेस चालू करें."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"सेटिंग खोलें."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"अन्य डिवाइस"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"खास जानकारी टॉगल करें"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"आपको अलार्म, रिमाइंडर, इवेंट और चुनिंदा कॉल करने वालों के अलावा किसी और तरह से (आवाज़ करके और थरथरा कर ) परेशान नहीं किया जाएगा. आप फिर भी संगीत, वीडियो और गेम सहित अपना चुना हुआ सब कुछ सुन सकते हैं."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"शेयर, रिकॉर्ड या कास्ट करते समय, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> के पास उस ऐप्लिकेशन पर दिख रही हर चीज़ या उस पर चल रहे हर मीडिया का ऐक्सेस होता है. इसलिए, शेयर, रिकॉर्ड या कास्ट करते समय, पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज या किसी और संवेदनशील जानकारी को लेकर खास सावधानी बरतें."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"जारी रखें"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ऐप्लिकेशन शेयर करें या उसकी रिकॉर्डिंग करें"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"सभी को हटाएं"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"मैनेज करें"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"मोबाइल डेटा बंद करना चाहते हैं?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"आप <xliff:g id="CARRIER">%s</xliff:g> से डेटा या इंटरनेट का इस्तेमाल नहीं कर पाएंगे. इंटरनेट सिर्फ़ वाई-फ़ाई से चलेगा."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"आपको मोबाइल और इंटरनेट सेवा देने वाली कंपनी"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"क्या आपको मोबाइल डेटा, <xliff:g id="CARRIER">%s</xliff:g> पर वापस से स्विच करना है?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"उपलब्ध होने पर, मोबाइल डेटा अपने-आप स्विच नहीं होगा"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"स्विच न करें"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"स्विच करें"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ऐप की वजह से मंज़ूरी के अनुरोध को समझने में दिक्कत हो रही है, इसलिए सेटिंग से आपके जवाब की पुष्टि नहीं हो पा रही है."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> को <xliff:g id="APP_2">%2$s</xliff:g> के हिस्से (स्लाइस) दिखाने की मंज़ूरी दें?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- यह <xliff:g id="APP">%1$s</xliff:g> से सूचना पढ़ सकता है"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ज़ूम करने की सुविधा वाली विंडो से जुड़ी सेटिंग"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"सुलभता सुविधाएं खोलने के लिए टैप करें. सेटिंग में, इस बटन को बदलें या अपने हिसाब से सेट करें.\n\n"<annotation id="link">"सेटिंग देखें"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"बटन को कुछ समय छिपाने के लिए, उसे किनारे पर ले जाएं"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"पहले जैसा करें"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} शॉर्टकट हटाया गया}one{# शॉर्टकट हटाया गया}other{# शॉर्टकट हटाए गए}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"सबसे ऊपर बाईं ओर ले जाएं"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"सबसे ऊपर दाईं ओर ले जाएं"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"सबसे नीचे बाईं ओर ले जाएं"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"सबसे नीचे दाईं ओर ले जाएं"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"एज पर ले जाएं और छिपाएं"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"एज से निकालें और दिखाएं"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"हटाएं"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"टॉगल करें"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"डिवाइस कंट्रोल"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"कंट्रोल जोड़ने के लिए ऐप्लिकेशन चुनें"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"मोबाइल डेटा"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"कनेक्ट हो गया"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"इंटरनेट कनेक्शन कुछ समय के लिए है"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"इंटरनेट कनेक्शन खराब है"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"मोबाइल डेटा अपने-आप कनेक्ट नहीं होगा"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"इंटरनेट कनेक्शन नहीं है"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"कोई दूसरा नेटवर्क उपलब्ध नहीं है"</string>
diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml
index 60f0c8a..d8fd225 100644
--- a/packages/SystemUI/res/values-hr/strings.xml
+++ b/packages/SystemUI/res/values-hr/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Lice nije prepoznato"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Upotrijebite otisak prsta"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth povezan."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Postotak baterije nije poznat."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Spojen na <xliff:g id="BLUETOOTH">%s</xliff:g> ."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Svjetlina"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inverzija boja"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Korekcija boja"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Korisničke postavke"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Upravljajte korisnicima"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Gotovo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zatvori"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Povezano"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon je dostupan"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera je dostupna"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon i kamera su dostupni"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon je uključen"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon je isključen"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon je omogućen za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Pristup mikrofonu onemogućen je za sve aplikacije i usluge. Pristup mikrofonu možete omogućiti u odjeljku Postavke &gt; Privatnost &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Pristup mikrofonu onemogućen je za sve aplikacije i usluge. Tu postavku možete promijeniti u odjeljku Postavke &gt; Privatnost &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera je uključena"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera je isključena"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera je omogućena za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Pristup kameri onemogućen je za sve aplikacije i usluge."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Da biste koristili gumb mikrofona, omogućite pristup mikrofonu u postavkama."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Otvori postavke"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Ostali uređaji"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Uključivanje/isključivanje pregleda"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Neće vas ometati zvukovi i vibracije, osim alarma, podsjetnika, događaja i pozivatelja koje navedete. I dalje ćete čuti sve što želite reproducirati, uključujući glazbu, videozapise i igre."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kad dijelite, snimate ili emitirate aplikaciju, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ima pristup svemu što se prikazuje ili reproducira u toj aplikaciji. Stoga pazite na zaporke, podatke o plaćanju, poruke i druge osjetljive podatke."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Nastavi"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Dijeljenje ili snimanje pomoću aplikacije"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Izbriši sve"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljajte"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Povijest"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Isključiti mobilne podatke?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nećete imati pristup mobilnim podacima ili internetu putem operatera <xliff:g id="CARRIER">%s</xliff:g>. Internet će biti dostupan samo putem Wi-Fija."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"vaš mobilni operater"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vratiti se na mobilnog operatera <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilni podaci neće se automatski prebaciti na temelju dostupnosti"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, hvala"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Da, prebaci"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Budući da aplikacija prekriva zahtjev za dopuštenje, Postavke ne mogu potvrditi vaš odgovor."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Želite li dopustiti aplikaciji <xliff:g id="APP_0">%1$s</xliff:g> da prikazuje isječke aplikacije <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– može čitati informacije aplikacije <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Postavke prozora povećala"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Dodirnite za otvaranje značajki pristupačnosti. Prilagodite ili zamijenite taj gumb u postavkama.\n\n"<annotation id="link">"Pregledajte postavke"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Pomaknite gumb do ruba da biste ga privremeno sakrili"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Poništi"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Uklonjen je prečac {label}}one{# prečac je uklonjen}few{# prečaca su uklonjena}other{# prečaca je uklonjeno}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Premjesti u gornji lijevi kut"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Premjesti u gornji desni kut"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Premjesti u donji lijevi kut"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Premjesti u donji desni kut"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Premjesti na rub i sakrij"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Ukloni s ruba i prikaži"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Ukloni"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"promijeni"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrole uređaja"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Odabir aplikacije za dodavanje kontrola"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilni podaci"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Povezano"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Privremeno povezano"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Slaba veza"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobilna veza neće se automatski uspostaviti"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Niste povezani"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nije dostupna nijedna druga mreža"</string>
diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml
index 0f6daa2..c1a8dd9 100644
--- a/packages/SystemUI/res/values-hu/strings.xml
+++ b/packages/SystemUI/res/values-hu/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Az arc nem ismerhető fel"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Használjon ujjlenyomatot"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth csatlakoztatva."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Az akkumulátor töltöttségi szintje ismeretlen."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Csatlakoztatva a következőhöz: <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Fényerő"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Színek invertálása"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Színjavítás"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Felhasználói beállítások"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Felhasználók kezelése"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Kész"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Bezárás"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Csatlakoztatva"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"A mikrofon használható"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"A kamera használható"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"A mikrofon és a kamera használható"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon bekapcsolva"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon kikapcsolva"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"A mikrofon az összes alkalmazás és szolgáltatás számára engedélyezve van."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"A mikrofonhoz való hozzáférés az összes alkalmazás és szolgáltatás számára le van tiltva. A mikrofonhoz való hozzáférést a következő menüpontban engedélyezheti: Beállítások &gt; Adatvédelem &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"A mikrofonhoz való hozzáférés az összes alkalmazás és szolgáltatás számára le van tiltva. Ezt a következő menüpontban módosíthatja: Beállítások &gt; Adatvédelem &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera bekapcsolva"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera kikapcsolva"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"A kamera az összes alkalmazás és szolgáltatás számára engedélyezve van."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"A kamerához való hozzáférés az összes alkalmazás és szolgáltatás számára le van tiltva."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Ha használni szeretné a Mikrofon gombot, engedélyezze a mikrofonhoz való hozzáférést a Beállításokban."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Beállítások megnyitása."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Más eszköz"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Áttekintés be- és kikapcsolása"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Az Ön által meghatározott ébresztéseken, emlékeztetőkön, eseményeken és hívókon kívül nem fogja Önt más hang vagy rezgés megzavarni. Továbbra is lesz hangjuk azoknak a tartalmaknak, amelyeket Ön elindít, például zenék, videók és játékok."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Amikor Ön megoszt, rögzít vagy átküld egy alkalmazást, a(z) <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> az adott appban látható vagy lejátszott minden tartalomhoz hozzáfér. Ezért legyen elővigyázatos a jelszavakkal, a fizetési adatokkal, az üzenetekkel és más bizalmas információkkal."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Folytatás"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Alkalmazás megosztása és rögzítése"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Az összes törlése"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Kezelés"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Előzmények"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Kikapcsolja a mobiladatokat?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nem lesz adat-, illetve internet-hozzáférése a <xliff:g id="CARRIER">%s</xliff:g> szolgáltatón keresztül. Az internethez csak Wi-Fi-n keresztül csatlakozhat."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"saját mobilszolgáltató"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Átvált a következőre: <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Nem kerül sor a mobiladat-forgalom automatikus átváltására a rendelkezésre állás alapján"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Most nem"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Igen, átváltok"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Mivel az egyik alkalmazás eltakarja az engedélykérést, a Beállítások alkalmazás nem tudja ellenőrizni az Ön válaszát."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Engedélyezi a(z) <xliff:g id="APP_0">%1$s</xliff:g> alkalmazásnak, hogy részleteket mutasson a(z) <xliff:g id="APP_2">%2$s</xliff:g> alkalmazásból?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Információkat olvashat a(z) <xliff:g id="APP">%1$s</xliff:g> alkalmazásból"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Nagyítóablak beállításai"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Koppintson a kisegítő lehetőségek megnyitásához. A gombot a Beállításokban módosíthatja.\n\n"<annotation id="link">"Beállítások"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"A gombot a szélre áthelyezve ideiglenesen elrejtheti"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Visszavonás"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} gyorsparancs eltávolítva}other{# gyorsparancs eltávolítva}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Áthelyezés fel és balra"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Áthelyezés fel és jobbra"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Áthelyezés le és balra"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Áthelyezés le és jobbra"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Áthelyezés a szélen kívül és elrejtés"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Áthelyezés a szélen kívül és mutatás"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Eltávolítás"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"váltás"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Eszközvezérlők"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Válasszon alkalmazást a vezérlők hozzáadásához"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiladat"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="NETWORKMODE">%2$s</xliff:g>/<xliff:g id="STATE">%1$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Csatlakozva"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Ideiglenesen csatlakoztatva"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Gyenge kapcsolat"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Nincs automatikus mobiladat-kapcsolat"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nincs kapcsolat"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nincs több rendelkezésre álló hálózat"</string>
diff --git a/packages/SystemUI/res/values-hy/strings.xml b/packages/SystemUI/res/values-hy/strings.xml
index 4724d5c..691fe64 100644
--- a/packages/SystemUI/res/values-hy/strings.xml
+++ b/packages/SystemUI/res/values-hy/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Դեմքը չի ճանաչվել"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Օգտագործեք մատնահետք"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth-ը միացված է:"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Մարտկոցի լիցքի մակարդակն անհայտ է։"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Միացված է <xliff:g id="BLUETOOTH">%s</xliff:g>-ին:"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Պայծառություն"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Գունաշրջում"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Գունաշտկում"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Օգտատիրոջ կարգավորումներ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Կառավարել օգտատերերին"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Պատրաստ է"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Փակել"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Միացված է"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Խոսափողը հասանելի է"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Տեսախցիկը հասանելի է"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Խոսափողն ու տեսացիկը հասանելի են"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Խոսափողը միացավ"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Խոսափողն անջատվեց"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Խոսափողը բոլոր հավելվածների և ծառայությունների համար միացված է։"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Խոսափողն օգտագործելու թույլտվությունը բոլոր հավելվածների և ծառայությունների համար անջատված է։ Խոսափողի օգտագործումը թույլատրելու համար անցեք Կարգավորումներ &gt; Գաղտնիություն &gt; Խոսափող։"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Խոսափողն օգտագործելու թույլտվությունը բոլոր հավելվածների և ծառայությունների համար անջատված է։ Սա փոխելու համար անցեք Կարգավորումներ &gt; Գաղտնիություն &gt; Խոսափող։"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Տեսախցիկը միացված է"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Տեսախցիկն անջատված է"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Տեսախցիկը բոլոր հավելվածների և ծառայությունների համար միացված է։"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Տեսախցիկն օգտագործելու թույլտվությունը բոլոր հավելվածների և ծառայությունների համար անջատված է։"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Խոսափողի կոճակն օգտագործելու համար կարգավորումներում թույլատրեք խոսափողի օգտագործումը։"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Բացել կարգավորումները։"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Այլ սարք"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Միացնել/անջատել համատեսքը"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Ձայները և թրթռոցները չեն անհանգստացնի ձեզ, բացի ձեր կողմից նշված զարթուցիչները, հիշեցումները, միջոցառումների ծանուցումները և զանգերը։ Դուք կլսեք ձեր ընտրածի նվագարկումը, այդ թվում՝ երաժշտություն, տեսանյութեր և խաղեր:"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Երբ դուք ցուցադրում, տեսագրում կամ հեռարձակում եք որևէ հավելվածի էկրանը, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> հավելվածին հասանելի է դառնում այն ամենը, ինչ ցուցադրվում է կամ նվագարկվում այդ հավելվածում։ Հիշեք այդ մասին, երբ պատրաստվում եք դիտել կամ մուտքագրել գաղտնաբառեր, վճարային տվյալներ, հաղորդագրություններ և այլ կոնֆիդենցիալ տեղեկություններ։"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Շարունակել"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Հավելվածի էկրանի ցուցադրում կամ տեսագրում"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Մաքրել բոլորը"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Կառավարել"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Պատմություն"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Անջատե՞լ բջջային ինտերնետը"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> օպերատորի բջջային ինտերնետը հասանելի չի լինի: Համացանցից կկարողանաք  օգտվել միայն Wi-Fi-ի միջոցով:"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"Ձեր"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Անցնե՞լ <xliff:g id="CARRIER">%s</xliff:g> ցանցին"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Սարքն ավտոմատ չի անցնի բջջային ինտերնետին"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ոչ, շնորհակալություն"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Այո"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Քանի որ ներածումն արգելափակված է ինչ-որ հավելվածի կողմից, Կարգավորումները չեն կարող հաստատել ձեր պատասխանը:"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Թույլատրե՞լ <xliff:g id="APP_0">%1$s</xliff:g> հավելվածին ցուցադրել հատվածներ <xliff:g id="APP_2">%2$s</xliff:g> հավելվածից"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Կարող է կարդալ տեղեկություններ <xliff:g id="APP">%1$s</xliff:g> հավելվածից"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Խոշորացույցի պատուհանի կարգավորումներ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Հատուկ գործառույթները բացելու համար հպեք։ Անհատականացրեք այս կոճակը կարգավորումներում։\n\n"<annotation id="link">"Կարգավորումներ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Կոճակը ժամանակավորապես թաքցնելու համար այն տեղափոխեք էկրանի եզր"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Հետարկել"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} դյուրանցում հեռացվեց}one{# դյուրանցում հեռացվեց}other{# դյուրանցում հեռացվեց}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Տեղափոխել վերև՝ ձախ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Տեղափոխել վերև՝ աջ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Տեղափոխել ներքև՝ ձախ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Տեղափոխել ներքև՝ աջ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Տեղափոխել եզրից դուրս և թաքցնել"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Տեղափոխել եզրից դուրս և ցուցադրել"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Հեռացնել"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"միացնել/անջատել"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Սարքերի կառավարման տարրեր"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Ընտրեք հավելված` կառավարման տարրեր ավելացնելու համար"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Բջջային ինտերնետ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Միացած է"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Ժամանակավոր կապ"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Թույլ կապ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Բջջային ինտերնետն ավտոմատ չի միանա"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Կապ չկա"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Այլ հասանելի ցանցեր չկան"</string>
diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml
index 8fefe40..236ad22 100644
--- a/packages/SystemUI/res/values-in/strings.xml
+++ b/packages/SystemUI/res/values-in/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Tidak mengenali wajah"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Gunakan sidik jari"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth terhubung."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Persentase baterai tidak diketahui."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Terhubung ke <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Kecerahan"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversi warna"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Koreksi warna"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Setelan pengguna"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Kelola pengguna"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Selesai"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Tutup"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Terhubung"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon tersedia"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera tersedia"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon dan kamera tersedia"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon diaktifkan"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon dinonaktifkan"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon diaktifkan untuk semua aplikasi dan layanan."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Akses mikrofon dinonaktifkan untuk semua aplikasi dan layanan. Anda dapat mengaktifkan akses mikrofon di Setelan &gt; Privasi &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Akses mikrofon dinonaktifkan untuk semua aplikasi dan layanan. Anda dapat mengubahnya di Setelan &gt; Privasi &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera diaktifkan"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera dinonaktifkan"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera diaktifkan untuk semua aplikasi dan layanan."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Akses kamera dinonaktifkan untuk semua aplikasi dan layanan."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Untuk menggunakan tombol mikrofon, aktifkan akses mikrofon di Setelan."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Buka setelan."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Perangkat lainnya"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Aktifkan Ringkasan"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Anda tidak akan terganggu oleh suara dan getaran, kecuali dari alarm, pengingat, acara, dan penelepon yang Anda tentukan. Anda akan tetap mendengar apa pun yang telah dipilih untuk diputar, termasuk musik, video, dan game."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Jika Anda membagikan, merekam, atau mentransmisikan suatu aplikasi, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> akan memiliki akses ke semua hal yang ditampilkan atau yang diputar di aplikasi tersebut. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, atau informasi sensitif lainnya."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Lanjutkan"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Bagikan atau rekam aplikasi"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Hapus semua"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Kelola"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Histori"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Nonaktifkan data seluler?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Anda tidak akan dapat mengakses data atau internet melalui <xliff:g id="CARRIER">%s</xliff:g>. Internet hanya akan tersedia melalui Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"Operator Seluler Anda"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Beralih kembali ke <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Data seluler tidak akan dialihkan secara otomatis berdasarkan ketersediaan"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Lain kali"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ya, alihkan"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Karena sebuah aplikasi menghalangi permintaan izin, Setelan tidak dapat memverifikasi respons Anda."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Izinkan <xliff:g id="APP_0">%1$s</xliff:g> menampilkan potongan <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Dapat membaca informasi dari <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Setelan jendela kaca pembesar"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Ketuk untuk membuka fitur aksesibilitas. Sesuaikan atau ganti tombol ini di Setelan.\n\n"<annotation id="link">"Lihat setelan"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Pindahkan tombol ke tepi agar tersembunyi untuk sementara"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Urungkan"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Pintasan {label} dihapus}other{# pintasan dihapus}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Pindahkan ke kiri atas"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Pindahkan ke kanan atas"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Pindahkan ke kiri bawah"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Pindahkan ke kanan bawah"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Pindahkan ke tepi dan sembunyikan"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Pindahkan dari tepi dan tampilkan"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Hapus"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"alihkan"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrol perangkat"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Pilih aplikasi untuk menambahkan kontrol"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Data seluler"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Terhubung"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Terhubung sementara"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Koneksi buruk"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Data seluler tidak akan terhubung otomatis"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Tidak ada koneksi"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Jaringan lain tidak tersedia"</string>
diff --git a/packages/SystemUI/res/values-is/strings.xml b/packages/SystemUI/res/values-is/strings.xml
index d22680e..537af36 100644
--- a/packages/SystemUI/res/values-is/strings.xml
+++ b/packages/SystemUI/res/values-is/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Andlit þekkist ekki"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Nota fingrafar í staðinn"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth tengt."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Staða rafhlöðu óþekkt."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Tengt við <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Birtustig"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Umsnúningur lita"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Litaleiðrétting"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Notandastillingar"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Stjórna notendum"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Lokið"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Loka"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Tengt"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Hljóðnemi tiltækur"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Myndavél tiltæk"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Hljóðnemi og myndavél tiltæk"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Kveikt á hljóðnemanum"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Slökkt á hljóðnemanum"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Kveikt er á hljóðnema fyrir öll forrit og þjónustur."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Slökkt er á aðgangi að hljóðnema fyrir öll forrit og þjónustur. Þú getur veitt aðgang að hljóðnema í „Stillingar &gt; Persónuvernd &gt; Hljóðnemi“."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Slökkt er á aðgangi að hljóðnema fyrir öll forrit og þjónustur. Þú getur breytt þessu í „Stillingar &gt; Persónuvernd &gt; Hljóðnemi“."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kveikt á myndavél"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Slökkt á myndavél"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kveikt er á myndavél fyrir öll forrit og þjónustur."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Slökkt er á aðgangi að myndavél fyrir öll forrit og þjónustur."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Veittu aðgang að hljóðnema í stillingunum til að nota hljóðnemahnappinn."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Opna stillingar."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Annað tæki"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Kveikja/slökkva á yfirliti"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Þú verður ekki fyrir truflunum frá hljóðmerkjum og titringi, fyrir utan vekjara, áminningar, viðburði og símtöl frá þeim sem þú leyfir fyrirfram. Þú heyrir áfram í öllu sem þú velur að spila, svo sem tónlist, myndskeiðum og leikjum."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Þegar þú deilir, tekur upp eða sendir út forrit hefur <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> aðgang að öllu sem sést eða spilast í viðkomandi forriti. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð eða aðrar viðkvæmar upplýsingar."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Áfram"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Deila eða taka upp forrit"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Hreinsa allt"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Stjórna"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Ferill"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Viltu slökkva á farsímagögnum?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Þú munt ekki hafa aðgang að gögnum eða internetinu í gegnum <xliff:g id="CARRIER">%s</xliff:g>. Aðeins verður hægt að tengjast internetinu með Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"símafyrirtækið þitt"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Skipta aftur yfir í <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Ekki verður skipt sjálfkrafa á milli farsímagagna byggt á tiltækileika"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nei takk"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Já, skipta"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Stillingar geta ekki staðfest svarið þitt vegna þess að forrit er að fela heimildarbeiðni."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Viltu leyfa <xliff:g id="APP_0">%1$s</xliff:g> að sýna sneiðar úr <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Það getur lesið upplýsingar úr <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Stillingar stækkunarglugga"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Ýttu til að opna aðgengiseiginleika. Sérsníddu eða skiptu hnappinum út í stillingum.\n\n"<annotation id="link">"Skoða stillingar"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Færðu hnappinn að brúninni til að fela hann tímabundið"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Afturkalla"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} flýtileið fjarlægð}one{# flýtileið fjarlægð}other{# flýtileiðir fjarlægðar}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Færa efst til vinstri"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Færa efst til hægri"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Færa neðst til vinstri"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Færa neðst til hægri"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Færa að jaðri og fela"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Færa að jaðri og birta"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Fjarlægja"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"kveikja/slökkva"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Tækjastjórnun"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Veldu forrit til að bæta við stýringum"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Farsímagögn"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Tengt"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tímabundin tenging"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Léleg tenging"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Farsímagögn tengjast ekki sjálfkrafa"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Engin tenging"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Engin önnur net í boði"</string>
diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml
index df6c14f..255885e 100644
--- a/packages/SystemUI/res/values-it/strings.xml
+++ b/packages/SystemUI/res/values-it/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Volto non riconosciuto"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Usa l\'impronta"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth collegato."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Percentuale della batteria sconosciuta."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Connesso a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Luminosità"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversione dei colori"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correzione del colore"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Impostazioni utente"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gestisci utenti"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Fine"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Chiudi"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Connesso"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfono disponibile"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Fotocamera disponibile"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Fotocamera e microfono disponibili"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfono attivo"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfono non attivo"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Il microfono è attivo per tutti i servizi e le app."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"L\'accesso al microfono è disattivato per tutti i servizi e le app. Puoi attivarlo in Impostazioni &gt; Privacy &gt; Microfono."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"L\'accesso al microfono è disattivato per tutti i servizi e le app. Puoi modificare questa preferenza in Impostazioni &gt; Privacy &gt; Microfono."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Fotocamera attiva"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Fotocamera non attiva"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"La fotocamera è attiva per tutti i servizi e le app."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"L\'accesso alla fotocamera è disattivato per tutti i servizi e le app."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Per usare il pulsante del microfono devi attivare l\'accesso al microfono nelle Impostazioni."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Apri le impostazioni"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Altro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Attiva/disattiva la panoramica"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Non verrai disturbato da suoni e vibrazioni, ad eccezione di sveglie, promemoria, eventi, chiamate da contatti da te specificati ed elementi che hai scelto di continuare a riprodurre, inclusi video, musica e giochi."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quando condividi, registri o trasmetti un\'app, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ha accesso a qualsiasi elemento visualizzato o riprodotto sull\'app. Presta quindi attenzione a password, dati di pagamento, messaggi o altre informazioni sensibili."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continua"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Condividi o registra un\'app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Cancella tutto"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gestisci"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Cronologia"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Disattivare i dati mobili?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Non avrai accesso ai dati o a Internet tramite <xliff:g id="CARRIER">%s</xliff:g>. Internet sarà disponibile soltanto tramite Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"il tuo operatore"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vuoi passare nuovamente all\'operatore <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"I dati mobili non passeranno automaticamente all\'operatore in base alla disponibilità"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"No, grazie"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sì, confermo"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Un\'app sta oscurando una richiesta di autorizzazione, pertanto Impostazioni non può verificare la tua risposta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vuoi consentire all\'app <xliff:g id="APP_0">%1$s</xliff:g> di mostrare porzioni dell\'app <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Può leggere informazioni dell\'app <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Impostazioni della finestra di ingrandimento"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tocca per aprire funzioni di accessibilità. Personalizza o sostituisci il pulsante in Impostazioni.\n\n"<annotation id="link">"Vedi impostazioni"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Sposta il pulsante fino al bordo per nasconderlo temporaneamente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Elimina"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} scorciatoia rimossa}many{# scorciatoie rimosse}other{# scorciatoie rimosse}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Sposta in alto a sinistra"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Sposta in alto a destra"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Sposta in basso a sinistra"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Sposta in basso a destra"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Sposta fino a bordo e nascondi"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Sposta fuori da bordo e mostra"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Rimuovi"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"attiva/disattiva"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controllo dispositivi"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Scegli un\'app per aggiungere controlli"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dati mobili"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Connessione attiva"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Connessa temporaneamente"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Connessione debole"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Nessuna connessione dati mobili automatica"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nessuna connessione"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nessun\'altra rete disponibile"</string>
diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml
index f24964a..8611c272 100644
--- a/packages/SystemUI/res/values-iw/strings.xml
+++ b/packages/SystemUI/res/values-iw/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"לא ניתן לזהות את הפנים"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"שימוש בטביעת אצבע במקום זאת"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"‏Bluetooth מחובר."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"אחוז טעינת הסוללה לא ידוע."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"התבצע חיבור אל <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"בהירות"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"היפוך צבעים"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"תיקון צבע"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"הגדרות המשתמש"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ניהול משתמשים"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"סיום"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"סגירה"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"מחובר"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"המיקרופון זמין"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"המצלמה זמינה"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"המיקרופון והמצלמה זמינים"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"המיקרופון פועל"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"המיקרופון כבוי"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"הגישה למיקרופון הופעלה לכל האפליקציות והשירותים."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"הגישה למיקרופון הושבתה לכל האפליקציות והשירותים. אפשר להפעיל את הגישה למיקרופון ב\'הגדרות\' &gt; \'פרטיות\' &gt; \'מיקרופון\'."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"הגישה למיקרופון הושבתה לכל האפליקציות והשירותים. אפשר לשנות את הגישה ב\'הגדרות\' &gt; \'פרטיות\' &gt; \'מיקרופון\'."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"המצלמה פועלת"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"המצלמה כבויה"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"הגישה למצלמה הופעלה לכל האפליקציות והשירותים."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"הגישה למצלמה הושבתה לכל האפליקציות והשירותים."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"כדי להשתמש בלחצן המיקרופון יש להפעיל את הגישה למיקרופון ב\'הגדרות\'."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"פתיחת ההגדרות."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"מכשיר אחר"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"החלפת מצב של מסכים אחרונים"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"כדי לא להפריע לך, המכשיר לא ירטוט ולא ישמיע שום צליל, חוץ מהתראות, תזכורות, אירועים ושיחות ממתקשרים מסוימים לבחירתך. המצב הזה לא ישפיע על צלילים שהם חלק מתוכן שבחרת להפעיל, כמו מוזיקה, סרטונים ומשחקים."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"‏בזמן שיתוף, הקלטה או העברה (cast) של אפליקציה, תהיה ל-<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> גישה לכל מה שגלוי באפליקציה או מופעל מהאפליקציה. כדאי להיזהר עם סיסמאות, פרטי תשלום, הודעות או מידע רגיש אחר."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"המשך"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"שיתוף או הקלטה של אפליקציה"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ניקוי הכול"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ניהול"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"היסטוריה"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"לכבות את חבילת הגלישה?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"‏לא תהיה לך גישה לנתונים או לאינטרנט באמצעות <xliff:g id="CARRIER">%s</xliff:g>. אינטרנט יהיה זמין רק באמצעות Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"הספק שלך"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"לחזור אל <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"לא תתבצע החלפה אוטומטית של חבילת הגלישה על סמך זמינות"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"לא, תודה"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"כן, אני רוצה להחליף"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"יש אפליקציה שמסתירה את בקשת ההרשאה, ולכן אין אפשרות לאמת את התשובה בהגדרות."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"האם לאפשר ל-<xliff:g id="APP_0">%1$s</xliff:g> להציג חלקים מ-<xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- תהיה לה אפשרות לקרוא מידע מאפליקציית <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ההגדרות של חלון ההגדלה"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"מקישים כדי לפתוח את תכונות הנגישות. אפשר להחליף את הלחצן או להתאים אותו אישית בהגדרות.\n\n"<annotation id="link">"הצגת ההגדרות"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"כדי להסתיר זמנית את הלחצן, יש להזיז אותו לקצה"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ביטול"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{קיצור הדרך אל {label} הוסר}two{# קיצורי דרך הוסרו}many{# קיצורי דרך הוסרו}other{# קיצורי דרך הוסרו}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"העברה לפינה השמאלית העליונה"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"העברה לפינה הימנית העליונה"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"העברה לפינה השמאלית התחתונה"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"העברה לפינה הימנית התחתונה"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"העברה לשוליים והסתרה"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"העברה מהשוליים והצגה"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"הסרה"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"החלפת מצב"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"פקדי מכשירים"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"יש לבחור אפליקציה כדי להוסיף פקדים"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"חבילת גלישה"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"מחובר"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"מחובר באופן זמני"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"חיבור באיכות ירודה"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"החיבור לנתונים סלולריים לא מתבצע באופן אוטומטי"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"אין חיבור"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"אין רשתות זמינות אחרות"</string>
diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml
index 7ea7223..b0d5437 100644
--- a/packages/SystemUI/res/values-ja/strings.xml
+++ b/packages/SystemUI/res/values-ja/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"顔を認識できません"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"指紋認証をお使いください"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetoothに接続済み。"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"バッテリー残量は不明です。"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>に接続しました。"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"画面の明るさ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"色反転"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"色補正"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ユーザー設定"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ユーザーを管理"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"完了"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"閉じる"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"接続済み"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"マイクを利用できます"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"カメラを利用できます"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"マイクとカメラを利用できます"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"マイクを ON にしました"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"マイクを OFF にしました"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"マイクはすべてのアプリとサービスで有効になっています。"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"マイクへのアクセスは、すべてのアプリとサービスで無効になっています。[設定] &gt; [プライバシー] &gt; [マイク] で有効にできます。"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"マイクへのアクセスは、すべてのアプリとサービスで無効になっています。この設定は、[設定] &gt; [プライバシー] &gt; [マイク] で変更できます。"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"カメラを ON にしました"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"カメラを OFF にしました"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"カメラはすべてのアプリとサービスで有効になっています。"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"カメラへのアクセスは、すべてのアプリとサービスで無効になっています。"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"マイクボタンを使用するには、[設定] でマイクへのアクセスを有効にしてください。"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"設定を開く"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"その他のデバイス"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"概要を切り替え"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"アラーム、リマインダー、予定、指定した人からの着信以外の音やバイブレーションに煩わされることはありません。音楽、動画、ゲームなど再生対象として選択したコンテンツは引き続き再生されます。"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"アプリの共有、録画、キャスト中は、そのアプリで表示されている内容や再生している内容に <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> がアクセスできるため、パスワード、お支払いの詳細、メッセージなどの機密情報にご注意ください。"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"続行"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"アプリの共有、録画"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"すべて消去"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"管理"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"履歴"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"モバイルデータを OFF にしますか?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g>でデータやインターネットにアクセスできなくなります。インターネットには Wi-Fi からのみ接続できます。"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"携帯通信会社"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> に戻しますか?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"利用可能な場合でも、モバイルデータを利用するよう自動的に切り替わることはありません"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"キャンセル"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"切り替える"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"アプリが許可リクエストを隠しているため、設定側でユーザーの応答を確認できません。"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"「<xliff:g id="APP_2">%2$s</xliff:g>」のスライスの表示を「<xliff:g id="APP_0">%1$s</xliff:g>」に許可しますか?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- 「<xliff:g id="APP">%1$s</xliff:g>」からの情報の読み取り"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"拡大鏡ウィンドウの設定"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"タップしてユーザー補助機能を開きます。ボタンのカスタマイズや入れ替えを [設定] で行えます。\n\n"<annotation id="link">"設定を表示"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ボタンを一時的に非表示にするには、端に移動させてください"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"元に戻す"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} 個のショートカットを削除}other{# 個のショートカットを削除}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"左上に移動"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"右上に移動"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"左下に移動"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"右下に移動"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"端に移動して非表示"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"端から移動して表示"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"削除"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"切り替え"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"デバイス コントロール"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"コントロールを追加するアプリの選択"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"モバイルデータ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"接続済み"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"一時的に接続されています"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"接続が不安定です"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"モバイルデータには自動接続しません"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"接続なし"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"利用できるネットワークはありません"</string>
diff --git a/packages/SystemUI/res/values-ka/strings.xml b/packages/SystemUI/res/values-ka/strings.xml
index 93c9d94..f7725ae 100644
--- a/packages/SystemUI/res/values-ka/strings.xml
+++ b/packages/SystemUI/res/values-ka/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"სახის ამოცნობა შეუძლებ."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"გამოიყენეთ თითის ანაბეჭდი"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth დაკავშირებულია."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ბატარეის პროცენტული მაჩვენებელი უცნობია."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"დაკავშირებულია <xliff:g id="BLUETOOTH">%s</xliff:g>-თან."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"განათება"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ფერთა ინვერსია"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ფერთა კორექცია"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"მომხმარებლის პარამეტრები"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"მომხმარებლების მართვა"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"დასრულდა"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"დახურვა"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"დაკავშირებულია"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"მიკროფონი ხელმისაწვდომია"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"კამერა ხელმისაწვდომია"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"მიკროფონი და კამერა ხელმისაწვდომია"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"მიკროფონი ჩართულია"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"მიკროფონი გამორთულია"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"მიკროფონი ჩართულია ყველა აპისა და სერვისისთვის."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"მიკროფონზე წვდომა გათიშულია ყველა აპისა და სერვისისთვის. მიკროფონზე წვდომის ჩართვა შეგიძლიათ აქედან: პარამეტრები &gt; კონფიდენციალურობა &gt; მიკროფონი."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"მიკროფონზე წვდომა გათიშულია ყველა აპისა და სერვისისთვის. ამის შეცვლა შეგიძლიათ აქედან: პარამეტრები &gt; კონფიდენციალურობა &gt; მიკროფონი."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"კამერა ჩაირთო"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"კამერა გამოირთო"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"კამერა ჩართულია ყველა აპისა და სერვისისთვის."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"კამერაზე წვდომა გათიშულია ყველა აპისა და სერვისისთვის."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"მიკროფონის ღილაკის გამოსაყენებლად, ჩართეთ მიკროფონზე წვდომა პარამეტრებიდან."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"პარამეტრების გახსნა."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"სხვა მოწყობილობა"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"მიმოხილვის გადართვა"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"თქვენ მიერ მითითებული მაღვიძარების, შეხსენებების, მოვლენებისა და ზარების გარდა, არავითარი ხმა და ვიბრაცია არ შეგაწუხებთ. თქვენ მაინც შეძლებთ სასურველი კონტენტის, მაგალითად, მუსიკის, ვიდეოებისა და თამაშების აუდიოს მოსმენა."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"აპის გაზიარებისას, ჩაწერისას ან ტრანსლირებისას <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> აქვს წვდომა აქვს ყველაფერზე, რაც ჩანს აპში ან ითამაშეთ. ამიტომ იყავით ფრთხილად პაროლებთან, გადახდის დეტალებთან, შეტყობინებებთან ან სხვა მგრძნობიარე ინფორმაციასთან."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"გაგრძელება"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"გააზიარეთ ან ჩაწერეთ აპი"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ყველას გასუფთავება"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"მართვა"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ისტორია"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"გსურთ მობილური ინტერნეტის გამორთვა?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"თქვენ არ გექნებათ მობილურ ინტერნეტზე ან ზოგადად ინტერნეტზე წვდომა <xliff:g id="CARRIER">%s</xliff:g>-ის მეშვეობით. ინტერნეტი მხოლოდ Wi-Fi-კავშირის მეშვეობით იქნება ხელმისაწვდომი."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"თქვენი ოპერატორი"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"გსურთ ისევ <xliff:g id="CARRIER">%s</xliff:g>-ზე გადართვა?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"მობილური მონაცემების ხელმისაწვდომობის მიხედვით ავტომატური გადართვა არ მოხდება"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"არა, გმადლობთ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"დიახ, გადაირთოს"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ვინაიდან აპი ფარავს ნებართვის მოთხოვნას, პარამეტრების მიერ თქვენი პასუხი ვერ დასტურდება."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"ანიჭებთ ნებართვას <xliff:g id="APP_0">%1$s</xliff:g>-ს, აჩვენოს <xliff:g id="APP_2">%2$s</xliff:g>-ის ფრაგმენტები?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- მას შეუძლია ინფორმაციის <xliff:g id="APP">%1$s</xliff:g>-დან წაკითხვა"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"გადიდების ფანჯრის პარამეტრები"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"შეეხეთ მარტივი წვდომის ფუნქციების გასახსნელად. მოარგეთ ან შეცვალეთ ეს ღილაკი პარამეტრებში.\n\n"<annotation id="link">"პარამეტრების ნახვა"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"გადაიტანეთ ღილაკი კიდეში, რათა დროებით დამალოთ ის"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"მოქმედების გაუქმება"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} მალსახმობი ამოშლილია}other{# მალსახმობი ამოშლილია}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ზევით და მარცხნივ გადატანა"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ზევით და მარჯვნივ გადატანა"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ქვევით და მარცხნივ გადატანა"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ქვემოთ და მარჯვნივ გადატანა"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"კიდეში გადატანა და დამალვა"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"კიდეში გადატანა და გამოჩენა"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"წაშლა"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"გადართვა"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"მოწყობილ. მართვის საშუალებები"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"აირჩიეთ აპი მართვის საშუალებების დასამატებლად"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"მობილური ინტერნეტი"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"დაკავშირებული"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"დროებით დაკავშირებული"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"სუსტი კავშირი"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"მობილურ ინტერნეტს ავტომატურად არ დაუკავშირდება"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"კავშირი არ არის"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"სხვა ქსელები მიუწვდომელია"</string>
diff --git a/packages/SystemUI/res/values-kk/strings.xml b/packages/SystemUI/res/values-kk/strings.xml
index e096b7e..ac77719 100644
--- a/packages/SystemUI/res/values-kk/strings.xml
+++ b/packages/SystemUI/res/values-kk/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Бет танылмады."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Орнына саусақ ізін пайдаланыңыз."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth қосылған."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Батарея зарядының мөлшері белгісіз."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> қосылған."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Жарықтығы"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Түс инверсиясы"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Түсті түзету"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Пайдаланушы параметрлері"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Пайдаланушыларды басқару"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Дайын"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Жабу"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Қосылды"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофон қолжетімді"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камера қолжетімді"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофон мен камера қолжетімді"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофон қосулы"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофон өшірулі"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Микрофон барлық қолданба мен қызмет үшін қосулы."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Микрофон пайдалану рұқсаты барлық қолданба мен қызмет үшін өшірулі. Микрофон пайдалану рұқсатын \"Параметрлер&gt; Құпиялылық &gt; Микрофон\" тармағынан қоса аласыз."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Микрофон пайдалану рұқсаты барлық қолданба мен қызмет үшін өшірулі. Мұны \"Параметрлер &gt; Құпиялылық &gt; Микрофон\" тармағынан өзгерте аласыз."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камера қосулы"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камера өшірулі"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камера барлық қолданба мен қызмет үшін қосулы."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Камера пайдалану рұқсаты барлық қолданба мен қызмет үшін өшірулі."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Микрофон түймесін пайдалану үшін параметрлерден микрофон пайдалану рұқсатын қосыңыз."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Параметрлерді ашу."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Басқа құрылғы"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Шолуды қосу/өшіру"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Оятқыш, еске салғыш, іс-шара мен өзіңіз көрсеткен контактілердің қоңырауларынан басқа дыбыстар мен дірілдер мазаламайтын болады. Музыка, бейне және ойын сияқты медиафайлдарды қоссаңыз, оларды естисіз."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Қолданба экранын бөлісу, жазу не трансляциялау кезінде <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> қолданбасы онда көрінетін не ойнатылатын барлық нәрсені пайдалана алады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізу кезінде сақ болыңыз."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Жалғастыру"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Қолданба экранын бөлісу не жазу"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Барлығын тазалау"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Басқару"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Тарих"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Мобильдік интернет өшірілсін бе?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> операторы арқылы деректерге немесе интернетке кіре алмайсыз. Интернетке тек Wi-Fi арқылы кіресіз."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"операторыңыз"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> операторына қайта ауысу керек пе?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мобильдік интернет операторды қолдану мүмкіндігіне қарай автоматты түрде ауыспайды."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Жоқ, рақмет"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Иә, ауыстырылсын"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Басқа қолданба рұқсат сұрауын жасырып тұрғандықтан, параметрлер жауабыңызды растай алмайды."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> қолданбасына <xliff:g id="APP_2">%2$s</xliff:g> қолданбасының үзінділерін көрсетуге рұқсат берілсін бе?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Бұл <xliff:g id="APP">%1$s</xliff:g> қолданбасындағы ақпаратты оқи алады"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Ұлғайтқыш терезесінің параметрлері"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Арнайы мүмкіндікті ашу үшін түртіңіз. Түймені параметрден реттеңіз не ауыстырыңыз.\n\n"<annotation id="link">"Параметрді көру"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Түймені уақытша жасыру үшін оны шетке қарай жылжытыңыз."</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Қайтару"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} таңбаша өшірілді.}other{# таңбаша өшірілді.}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Жоғарғы сол жаққа жылжыту"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Жоғарғы оң жаққа жылжыту"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Төменгі сол жаққа жылжыту"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Төменгі оң жаққа жылжыту"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Шетке жылжыту және жасыру"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Шетке жылжыту және көрсету"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Өшіру"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ауыстыру"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Құрылғыны басқару элементтері"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Басқару элементтері қосылатын қолданбаны таңдаңыз"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобильдік интернет"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Жалғанды"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Уақытша байланыс орнатылды."</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Байланыс нашар."</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мобильдік интернет автоматты түрде қосылмайды."</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Байланыс жоқ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Басқа қолжетімді желі жоқ"</string>
diff --git a/packages/SystemUI/res/values-km/strings.xml b/packages/SystemUI/res/values-km/strings.xml
index aa641e4..6504990 100644
--- a/packages/SystemUI/res/values-km/strings.xml
+++ b/packages/SystemUI/res/values-km/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"មិនអាចសម្គាល់មុខបានទេ"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ប្រើស្នាមម្រាមដៃជំនួសវិញ​"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"បាន​តភ្ជាប់​ប៊្លូធូស។"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"មិនដឹងអំពី​ភាគរយថ្មទេ។"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"បាន​ភ្ជាប់​ទៅ <xliff:g id="BLUETOOTH">%s</xliff:g> ។"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ពន្លឺ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ការបញ្ច្រាស​ពណ៌"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ការ​កែតម្រូវ​ពណ៌"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ការកំណត់អ្នកប្រើប្រាស់"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"គ្រប់គ្រង​អ្នក​ប្រើប្រាស់"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"រួចរាល់"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"បិទ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"បាន​ភ្ជាប់"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"អាចប្រើមីក្រូហ្វូនបាន"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"អាចប្រើកាមេរ៉ាបាន"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"អាចប្រើកាមេរ៉ា និងមីក្រូហ្វូនបាន"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"បានបើក​មីក្រូហ្វូន"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"បានបិទ​មីក្រូហ្វូន"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"មីក្រូហ្វូនត្រូវបានបើកសម្រាប់កម្មវិធី និងសេវាកម្មទាំងអស់។"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"សិទ្ធិចូលប្រើប្រាស់មីក្រូហ្វូនត្រូវបានបិទសម្រាប់កម្មវិធី និងសេវាកម្មទាំងអស់។ អ្នកអាចបើកសិទ្ធិចូលប្រើប្រាស់មីក្រូហ្វូននៅក្នុងការកំណត់ &gt; ឯកជនភាព &gt; មីក្រូហ្វូន។"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"សិទ្ធិចូលប្រើប្រាស់មីក្រូហ្វូនត្រូវបានបិទសម្រាប់កម្មវិធី និងសេវាកម្មទាំងអស់។ អ្នកអាចផ្លាស់ប្ដូរលក្ខណៈនេះនៅក្នុងការកំណត់ &gt; ឯកជនភាព &gt; មីក្រូហ្វូន។"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"បាន​បើក​កាមេរ៉ា"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"បានបិទ​កាមេរ៉ា"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"កាមេរ៉ាត្រូវបានបើកសម្រាប់កម្មវិធី និងសេវាកម្មទាំងអស់។"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"សិទ្ធិចូលប្រើប្រាស់កាមេរ៉ាត្រូវបានបិទសម្រាប់កម្មវិធី និងសេវាកម្មទាំងអស់។"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"ដើម្បីប្រើប្រាស់ប៊ូតុងមីក្រូហ្វូន សូមបើកសិទ្ធិចូលប្រើប្រាស់មីក្រូហ្វូននៅក្នុងការកំណត់។"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"បើកការកំណត់។"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ឧបករណ៍ផ្សេងទៀត"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"បិទ/បើក​ទិដ្ឋភាពរួម"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"សំឡេង និងរំញ័រនឹងមិន​រំខានដល់អ្នកឡើយ លើកលែងតែម៉ោងរោទ៍ ការរំលឹក ព្រឹត្តិការណ៍ និងអ្នកហៅទូរសព្ទដែលអ្នកបញ្ជាក់ប៉ុណ្ណោះ។ អ្នកនឹងនៅតែឮសំឡេងសកម្មភាពគ្រប់យ៉ាងដែលអ្នកលេងដដែល រួមទាំងតន្រ្តី វីដេអូ និងហ្គេម។"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"នៅពេលអ្នកកំពុងចែករំលែក ថត ឬបញ្ជូនកម្មវិធី <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> មានសិទ្ធិចូលប្រើប្រាស់អ្វីៗដែលបង្ហាញ ឬលេងនៅលើកម្មវិធីនោះ។ ដូច្នេះ សូមប្រុងប្រយ័ត្នចំពោះពាក្យសម្ងាត់ ព័ត៌មាន​លម្អិតអំពី​ការ​ទូទាត់ប្រាក់ សារ ឬព័ត៌មានរសើបផ្សេងទៀត។"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"បន្ត"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ចែករំលែក ឬថតកម្មវិធី"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"សម្អាត​ទាំងអស់"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"គ្រប់គ្រង"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ប្រវត្តិ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"បិទទិន្នន័យទូរសព្ទចល័ត?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"អ្នកនឹង​មិន​មាន​សិទ្ធិ​ចូល​ប្រើទិន្នន័យ​ ឬអ៊ីនធឺណិត​តាមរយៈ <xliff:g id="CARRIER">%s</xliff:g> បានឡើយ។ អ៊ីនធឺណិត​នឹងអាច​ប្រើបាន​តាមរយៈ Wi-Fi តែប៉ុណ្ណោះ។"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ក្រុមហ៊ុន​​សេវាទូរសព្ទរបស់អ្នក"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"ប្ដូរទៅ <xliff:g id="CARRIER">%s</xliff:g> វិញឬ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ទិន្នន័យទូរសព្ទចល័តនឹងមិនប្ដូរដោយស្វ័យប្រវត្តិដោយផ្អែកតាមភាពអាចប្រើបាននោះទេ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ទេ អរគុណ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"បាទ/ចាស ប្ដូរ"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ការកំណត់​មិនអាច​ផ្ទៀងផ្ទាត់​ការឆ្លើយតប​របស់អ្នក​បាន​ទេ ដោយសារ​កម្មវិធី​កំពុង​បាំងសំណើ​សុំការ​អនុញ្ញាត។"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"អនុញ្ញាតឱ្យ <xliff:g id="APP_0">%1$s</xliff:g> បង្ហាញ​ស្ថិតិប្រើប្រាស់​របស់ <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- វា​អាច​អាន​ព័ត៌មាន​ពី <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ការកំណត់វិនដូ​កម្មវិធីពង្រីក"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ចុចដើម្បីបើក​មុខងារ​ភាពងាយស្រួល។ ប្ដូរ ឬប្ដូរ​ប៊ូតុងនេះ​តាមបំណង​នៅក្នុង​ការកំណត់។\n\n"<annotation id="link">"មើល​ការកំណត់"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ផ្លាស់ទី​ប៊ូតុង​ទៅគែម ដើម្បីលាក់វា​ជាបណ្ដោះអាសន្ន"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ត្រឡប់វិញ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{បានដក​ផ្លូវកាត់ {label} ចេញ}other{បាន​ដក​ផ្លូវ​កាត់ # ចេញ}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ផ្លាស់ទីទៅខាងលើផ្នែកខាងឆ្វេង"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ផ្លាស់ទីទៅខាងលើផ្នែកខាងស្ដាំ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ផ្លាស់ទីទៅខាងក្រោមផ្នែកខាងឆ្វេង​"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ផ្លាស់ទីទៅខាងក្រោមផ្នែកខាងស្ដាំ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ផ្លាស់ទីទៅផ្នែកខាងចុង រួចលាក់"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ផ្លាស់ទីចេញពីផ្នែកខាងចុង រួចបង្ហាញ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ដកចេញ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"បិទ/បើក"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ផ្ទាំងគ្រប់គ្រងឧបករណ៍"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ជ្រើសរើស​កម្មវិធីដែលត្រូវបញ្ចូល​ផ្ទាំងគ្រប់គ្រង"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ទិន្នន័យ​ទូរសព្ទចល័ត"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"បានភ្ជាប់"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"បានភ្ជាប់ជាបណ្ដោះអាសន្ន"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ការតភ្ជាប់​ខ្សោយ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ទិន្នន័យទូរសព្ទចល័ត​នឹងមិនភ្ជាប់ដោយស្វ័យប្រវត្តិទេ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"មិនមាន​ការតភ្ជាប់ទេ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"មិន​មាន​បណ្ដាញផ្សេងទៀតដែល​អាច​ប្រើ​បានទេ"</string>
diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml
index ab3f379..e1124bb 100644
--- a/packages/SystemUI/res/values-kn/strings.xml
+++ b/packages/SystemUI/res/values-kn/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ಮುಖ ಗುರುತಿಸಲಾಗುತ್ತಿಲ್ಲ"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ಬದಲಿಗೆ ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಬಳಸಿ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ಬ್ಲೂಟೂತ್‌‌ ಸಂಪರ್ಕಗೊಂಡಿದೆ."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ಬ್ಯಾಟರಿ ಶೇಕಡಾವಾರು ತಿಳಿದಿಲ್ಲ."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> ಗೆ ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆ."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ಪ್ರಕಾಶಮಾನ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ಕಲರ್ ಇನ್‍ವರ್ಶನ್"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ಬಣ್ಣದ ತಿದ್ದುಪಡಿ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ಬಳಕೆದಾರರ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ಬಳಕೆದಾರರನ್ನು ನಿರ್ವಹಿಸಿ"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ಮುಗಿದಿದೆ"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ಮುಚ್ಚಿರಿ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ಸಂಪರ್ಕಗೊಂಡಿದೆ"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ಮೈಕ್ರೊಫೋನ್ ಲಭ್ಯವಿದೆ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ಕ್ಯಾಮರಾ ಲಭ್ಯವಿದೆ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ಮೈಕ್ರೊಫೋನ್ ಮತ್ತು ಕ್ಯಾಮರಾ ಲಭ್ಯವಿದೆ"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"ಮೈಕ್ರೊಫೋನ್ ಅನ್ನು ಆನ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"ಮೈಕ್ರೊಫೋನ್ ಅನ್ನು ಆಫ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"ಎಲ್ಲಾ ಆ್ಯಪ್‌ಗಳು ಹಾಗೂ ಸೇವೆಗಳಿಗಾಗಿ ಮೈಕ್ರೊಫೋನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"ಎಲ್ಲಾ ಆ್ಯಪ್‌ಗಳು ಹಾಗೂ ಸೇವೆಗಳಿಗಾಗಿ ಮೈಕ್ರೊಫೋನ್ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಸೆಟ್ಟಿಂಗ್‌ಗಳು &gt; ಗೌಪ್ಯತೆ &gt; ಮೈಕ್ರೊಫೋನ್ ಎಂಬಲ್ಲಿಗೆ ಹೋಗುವ ಮೂಲಕ ನೀವು ಮೈಕ್ರೊಫೋನ್ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"ಎಲ್ಲಾ ಆ್ಯಪ್‌ಗಳು ಹಾಗೂ ಸೇವೆಗಳಿಗಾಗಿ ಮೈಕ್ರೊಫೋನ್ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಸೆಟ್ಟಿಂಗ್‌ಗಳು &gt; ಗೌಪ್ಯತೆ &gt; ಮೈಕ್ರೊಫೋನ್ ಎಂಬಲ್ಲಿಗೆ ಹೋಗುವ ಮೂಲಕ ನೀವು ಇದನ್ನು ಬದಲಾಯಿಸಬಹುದು."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ಕ್ಯಾಮರಾವನ್ನು ಆನ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ಕ್ಯಾಮರಾವನ್ನು ಆಫ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"ಎಲ್ಲಾ ಆ್ಯಪ್‌ಗಳು ಹಾಗೂ ಸೇವೆಗಳಿಗಾಗಿ ಕ್ಯಾಮರಾವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"ಎಲ್ಲಾ ಆ್ಯಪ್‌ಗಳು ಹಾಗೂ ಸೇವೆಗಳಿಗಾಗಿ ಕ್ಯಾಮರಾ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"ಮೈಕ್ರೊಫೋನ್ ಬಟನ್ ಅನ್ನು ಬಳಸಲು, ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಮೈಕ್ರೊಫೋನ್ ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ತೆರೆಯಿರಿ."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ಅನ್ಯ ಸಾಧನ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ಟಾಗಲ್ ನ ಅವಲೋಕನ"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"ಅಲಾರಾಂಗಳು, ಜ್ಞಾಪನೆಗಳು, ಈವೆಂಟ್‌ಗಳು ಹಾಗೂ ನೀವು ಸೂಚಿಸಿರುವ ಕರೆದಾರರನ್ನು ಹೊರತುಪಡಿಸಿ ಬೇರಾವುದೇ ಸದ್ದುಗಳು ಅಥವಾ ವೈಬ್ರೇಶನ್‌ಗಳು ನಿಮಗೆ ತೊಂದರೆ ನೀಡುವುದಿಲ್ಲ. ಹಾಗಿದ್ದರೂ, ನೀವು ಪ್ಲೇ ಮಾಡುವ ಸಂಗೀತ, ವೀಡಿಯೊಗಳು ಮತ್ತು ಆಟಗಳ ಆಡಿಯೊವನ್ನು ನೀವು ಕೇಳಿಸಿಕೊಳ್ಳುತ್ತೀರಿ."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳುತ್ತಿರುವಾಗ, ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ ಅಥವಾ ಬಿತ್ತರಿಸುತ್ತಿರುವಾಗ, ಆ ಆ್ಯಪ್‌ನಲ್ಲಿ ತೋರಿಸಲಾಗುವ ಅಥವಾ ಪ್ಲೇ ಆಗುವ ಯಾವುದೇ ವಿಷಯಕ್ಕೆ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ. ಹಾಗಾಗಿ, ಪಾಸ್‌ವರ್ಡ್‌ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು ಅಥವಾ ಇತರ ಸೂಕ್ಷ್ಮ ಮಾಹಿತಿಯ ಕುರಿತು ಜಾಗರೂಕರಾಗಿರಿ."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ಮುಂದುವರಿಸಿ"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ಆ್ಯಪ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಿ ಅಥವಾ ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ನಿರ್ವಹಿಸಿ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ಇತಿಹಾಸ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ಮೊಬೈಲ್ ಡೇಟಾ ಆಫ್ ಮಾಡಬೇಕೆ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"ನೀವು <xliff:g id="CARRIER">%s</xliff:g> ಮೂಲಕ ಡೇಟಾ ಅಥವಾ ಇಂಟರ್ನೆಟ್‌ಗೆ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿರುವುದಿಲ್ಲ. ಇಂಟರ್ನೆಟ್, ವೈ-ಫೈ ಮೂಲಕ ಮಾತ್ರ ಲಭ್ಯವಿರುತ್ತದೆ."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ನಿಮ್ಮ ವಾಹಕ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> ಗೆ ಬದಲಿಸುವುದೇ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ಲಭ್ಯತೆಯ ಆಧಾರದ ಮೇಲೆ ಮೊಬೈಲ್ ಡೇಟಾ ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಬದಲಾಗುವುದಿಲ್ಲ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ಬೇಡ, ಧನ್ಯವಾದಗಳು"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ಹೌದು, ಬದಲಿಸಿ"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ಅನುಮತಿ ವಿನಂತಿಯನ್ನು ಅಪ್ಲಿಕೇಶನ್ ಮರೆಮಾಚುತ್ತಿರುವ ಕಾರಣ, ಸೆಟ್ಟಿಂಗ್‌ಗಳಿಗೆ ನಿಮ್ಮ ಪ್ರತಿಕ್ರಿಯೆಯನ್ನು ಪರಿಶೀಲಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_2">%2$s</xliff:g> ಸ್ಲೈಸ್‌ಗಳನ್ನು ತೋರಿಸಲು <xliff:g id="APP_0">%1$s</xliff:g> ಅನ್ನು ಅನುಮತಿಸುವುದೇ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ಇದು <xliff:g id="APP">%1$s</xliff:g> ನಿಂದ ಮಾಹಿತಿಯನ್ನು ಓದಬಹುದು"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ಮ್ಯಾಗ್ನಿಫೈರ್ ವಿಂಡೋ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ಪ್ರವೇಶಿಸುವಿಕೆ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ತೆರೆಯಲು ಟ್ಯಾಪ್ ಮಾಡಿ. ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಈ ಬಟನ್ ಅನ್ನು ಕಸ್ಟಮೈಸ್ ಮಾಡಿ ಅಥವಾ ಬದಲಾಯಿಸಿ.\n\n"<annotation id="link">"ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ವೀಕ್ಷಿಸಿ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ಅದನ್ನು ತಾತ್ಕಾಲಿಕವಾಗಿ ಮರೆಮಾಡಲು ಅಂಚಿಗೆ ಬಟನ್ ಸರಿಸಿ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ರದ್ದುಗೊಳಿಸಿ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} ಶಾರ್ಟ್‌ಕಟ್ ಅನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ}one{# ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ}other{# ಶಾರ್ಟ್‌ಕಟ್‌ಗಳನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ಎಡ ಮೇಲ್ಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ಬಲ ಮೇಲ್ಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ಸ್ಕ್ರೀನ್‌ನ ಎಡ ಕೆಳಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ಕೆಳಗಿನ ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ಅಂಚಿಗೆ ಸರಿಸಿ ಮತ್ತು ಮರೆಮಾಡಿ"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ಅಂಚನ್ನು ಸರಿಸಿ ಮತ್ತು ತೋರಿಸಿ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ತೆಗೆದುಹಾಕಿ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ಟಾಗಲ್ ಮಾಡಿ"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ಸಾಧನ ನಿಯಂತ್ರಣಗಳು"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ನಿಯಂತ್ರಣಗಳನ್ನು ಸೇರಿಸಲು ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ಮೊಬೈಲ್ ಡೇಟಾ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ಕನೆಕ್ಟ್ ಆಗಿದೆ"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"ತಾತ್ಕಾಲಿಕವಾಗಿ ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ಕಳಪೆ ಸಂಪರ್ಕ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ಮೊಬೈಲ್ ಡೇಟಾ ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಕನೆಕ್ಟ್ ಆಗುವುದಿಲ್ಲ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ಯಾವುದೇ ಕನೆಕ್ಷನ್ ಇಲ್ಲ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ಇತರ ಯಾವುದೇ ನೆಟ್‌ವರ್ಕ್‌ಗಳು ಲಭ್ಯವಿಲ್ಲ"</string>
diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml
index 11da089..e5e4657 100644
--- a/packages/SystemUI/res/values-ko/strings.xml
+++ b/packages/SystemUI/res/values-ko/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"얼굴을 인식할 수 없습니다."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"대신 지문을 사용하세요."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"블루투스가 연결되었습니다."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"배터리 잔량을 알 수 없습니다."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>에 연결되었습니다."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"밝기"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"색상 반전"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"색상 보정"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"사용자 설정"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"사용자 관리"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"완료"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"닫기"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"연결됨"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"마이크 사용 가능"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"카메라 사용 가능"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"마이크 및 카메라 사용 가능"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"마이크 사용 설정됨"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"마이크 사용 중지됨"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"모든 앱 및 서비스의 마이크가 사용 설정됩니다."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"모든 앱 및 서비스의 마이크 액세스가 사용 중지됩니다. 설정 &gt; 개인 정보 보호 &gt; 마이크에서 마이크 액세스를 사용 설정할 수 있습니다."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"모든 앱 및 서비스의 마이크 액세스가 사용 중지됩니다. 설정 &gt; 개인 정보 보호 &gt; 마이크에서 변경할 수 있습니다."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"카메라 사용 설정됨"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"카메라 사용 중지됨"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"모든 앱 및 서비스의 카메라가 사용 설정됩니다."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"모든 앱 및 서비스의 카메라 액세스가 사용 중지됩니다."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"마이크 버튼을 사용하려면 설정에서 마이크 액세스를 사용 설정하세요."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"설정 열기"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"기타 기기"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"최근 사용 버튼 전환"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"알람, 알림, 일정 및 지정한 발신자로부터 받은 전화를 제외한 소리와 진동을 끕니다. 음악, 동영상, 게임 등 재생하도록 선택한 소리는 정상적으로 들립니다."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"앱을 공유하거나 녹화하거나 전송할 때는 <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>에서 해당 앱에 표시되거나 재생되는 모든 항목에 액세스할 수 있으므로 비밀번호, 결제 세부정보, 메시지 등 민감한 정보가 노출되지 않도록 주의하세요."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"계속"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"앱 공유 또는 녹화"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"모두 지우기"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"관리"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"기록"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"모바일 데이터를 사용 중지하시겠습니까?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g>을(를) 통해 데이터 또는 인터넷에 액세스할 수 없게 됩니다. 인터넷은 Wi-Fi를 통해서만 사용할 수 있습니다."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"이동통신사"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"다시 <xliff:g id="CARRIER">%s</xliff:g>(으)로 전환할까요?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"모바일 데이터가 가용성에 따라 자동으로 전환하지 않습니다."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"나중에"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"예, 전환합니다"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"앱이 권한 요청을 가리고 있기 때문에 설정에서 내 응답을 확인할 수 없습니다."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g>에서 <xliff:g id="APP_2">%2$s</xliff:g>의 슬라이스를 표시하도록 허용하시겠습니까?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- <xliff:g id="APP">%1$s</xliff:g>의 정보를 읽을 수 있음"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"돋보기 창 설정"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"접근성 기능을 열려면 탭하세요. 설정에서 이 버튼을 맞춤설정하거나 교체할 수 있습니다.\n\n"<annotation id="link">"설정 보기"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"버튼을 가장자리로 옮겨서 일시적으로 숨기세요."</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"실행취소"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{바로가기 {label}개 삭제됨}other{바로가기 #개 삭제됨}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"왼쪽 상단으로 이동"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"오른쪽 상단으로 이동"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"왼쪽 하단으로 이동"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"오른쪽 하단으로 이동"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"가장자리로 옮겨서 숨기기"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"가장자리 바깥으로 옮겨서 표시"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"삭제"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"전환"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"기기 컨트롤"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"컨트롤을 추가할 앱을 선택하세요"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"모바일 데이터"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"연결됨"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"일시적으로 연결됨"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"연결 상태 나쁨"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"모바일 데이터가 자동으로 연결되지 않음"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"연결되지 않음"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"사용 가능한 다른 네트워크가 없음"</string>
diff --git a/packages/SystemUI/res/values-ky/strings.xml b/packages/SystemUI/res/values-ky/strings.xml
index 676076c..bef48dc 100644
--- a/packages/SystemUI/res/values-ky/strings.xml
+++ b/packages/SystemUI/res/values-ky/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Жүз таанылбай жатат"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Манжа изин колдонуңуз"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth байланышта"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Батарея кубатынын деңгээли белгисиз."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> менен туташкан."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Жарыктыгы"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Түстөрдү инверсиялоо"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Түстөрдү тууралоо"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Колдонуучунун параметрлери"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Колдонуучуларды тескөө"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Бүттү"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Жабуу"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Туташкан"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофон жеткиликтүү"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камера жеткиликтүү"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофон жана камера жеткиликтүү"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофон күйгүзүлдү"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофон өчүрүлдү"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Микрофон бардык колдонмолор жана кызматтар үчүн иштетилди."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Микрофон бардык колдонмолор жана кызматтар үчүн өчүрүлдү. Микрофонду колдонууну иштетүү үчүн &gt; Купуялык &gt; Микрофон бөлүмүнө өтүңүз."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Микрофон бардык колдонмолор жана кызматтар үчүн өчүрүлдү. Муну Параметрлер &gt; Купуялык &gt; Микрофон бөлүмүнөн өзгөртө аласыз."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камера күйгүзүлдү"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камера өчүрүлдү"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камера бардык колдонмолор жана кызматтар үчүн иштетилди."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Камера бардык колдонмолор жана кызматтар үчүн өчүрүлдү."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Микрофон баскычын колдонуу үчүн Параметрлерден микрофонду колдонууну иштетиңиз."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Параметрлерди ачуу."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Башка түзмөк"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Назар режимин өчүрүү/күйгүзүү"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Ойготкучтардан, эскертүүлөрдөн, жылнаамадагы иш-чараларды эстеткичтерден жана белгиленген байланыштардын чалууларынан тышкары башка үндөр жана дирилдөөлөр тынчыңызды албайт. Бирок ойнотулуп жаткан музыканы, видеолорду жана оюндарды мурдагыдай эле уга бересиз."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Бөлүшүп, жаздырып же тышкы экранда бөлүшкөндө <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ал колдонмодо көрүнүп жана ойнотулуп жаткан нерселерге мүмкүнчүлүк алат. Андыктан сырсөздөрдү, төлөм маалыматын, билдирүүлөрдү жана башка купуя маалыматты көрсөтүп албаңыз."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Улантуу"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Колдонмону бөлүшүү же жаздыруу"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Баарын тазалап салуу"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Башкаруу"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Таржымал"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Мобилдик Интернетти өчүрөсүзбү?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> байланыш оператору аркылуу Интернетке кире албай каласыз. Интернетке Wi-Fi аркылуу гана кирүүгө болот."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"байланыш операторуңуз"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Кайра <xliff:g id="CARRIER">%s</xliff:g> байланыш операторуна которуласызбы?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Жеткиликтүү болгондо мобилдик Интернет автоматтык түрдө которулбайт"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Жок, рахмат"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ооба, которулуу"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Уруксат берүү сурамыңыз көрүнбөй калгандыктан, Жөндөөлөр жообуңузду ырастай албай жатат."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> колдонмосуна <xliff:g id="APP_2">%2$s</xliff:g> үлгүлөрүн көрсөтүүгө уруксат берилсинби?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- <xliff:g id="APP">%1$s</xliff:g> колдонмосунун маалыматын окуйт"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Чоңойткуч терезесинин параметрлери"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Атайын мүмкүнчүлүктөрдү ачуу үчүн басыңыз. Бул баскычты Жөндөөлөрдөн өзгөртүңүз.\n\n"<annotation id="link">"Жөндөөлөрдү көрүү"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Баскычты убактылуу жашыра туруу үчүн экрандын четине жылдырыңыз"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Кайтаруу"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} ыкчам баскыч өчүрүлдү}other{# ыкчам баскыч өчүрүлдү}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Жогорку сол жакка жылдыруу"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Жогорку оң жакка жылдырыңыз"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Төмөнкү сол жакка жылдыруу"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Төмөнкү оң жакка жылдырыңыз"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Ичине жылдырып, көрсөтүңүз"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Сыртка жылдырып, көрсөтүңүз"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Өчүрүү"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"өчүрүү/күйгүзүү"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Түзмөктү башкаруу элементтери"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Башкаруу элементтери кошула турган колдонмону тандоо"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобилдик трафик"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Туташты"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Убактылуу туташып турат"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Байланыш начар"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мобилдик трафик автоматтык түрдө туташтырылбайт"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Байланыш жок"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Башка тармактар жеткиликсиз"</string>
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index ac9a947..aefd998 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -24,7 +24,36 @@
         <item name="android:paddingEnd">24dp</item>
         <item name="android:paddingTop">48dp</item>
         <item name="android:paddingBottom">10dp</item>
-        <item name="android:gravity">top|center_horizontal</item>
+        <item name="android:gravity">top|left</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">320dp</item>
+        <item name="android:maxWidth">320dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">60dp</item>
+        <item name="android:paddingVertical">20dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
 </resources>
diff --git a/packages/SystemUI/res/values-lo/strings.xml b/packages/SystemUI/res/values-lo/strings.xml
index 4fc25e2..0d0d9de 100644
--- a/packages/SystemUI/res/values-lo/strings.xml
+++ b/packages/SystemUI/res/values-lo/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ບໍ່ສາມາດຈຳແນກໃບໜ້າໄດ້"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ກະລຸນາໃຊ້ລາຍນິ້ວມືແທນ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ເຊື່ອມຕໍ່ Bluetooth ແລ້ວ."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ບໍ່ຮູ້ເປີເຊັນແບັດເຕີຣີ."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"ເຊື່ອມ​ຕໍ່​ຫາ <xliff:g id="BLUETOOTH">%s</xliff:g> ແລ້ວ."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ຄວາມແຈ້ງ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ການປີ້ນສີ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ການແກ້ໄຂສີ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ຕັ້ງຄ່າຜູ້ໃຊ້"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ຈັດການຜູ້ໃຊ້"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ແລ້ວໆ"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ປິດ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ເຊື່ອມ​ຕໍ່ແລ້ວ"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ສາມາດໃຊ້ໄມໂຄຣໂຟນໄດ້"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ສາມາດໃຊ້ກ້ອງຖ່າຍຮູບໄດ້"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ສາມາດໃຊ້ໄມໂຄຣໂຟນ ແລະ ກ້ອງຖ່າຍຮູບໄດ້"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"ເປີດໄມໂຄຣໂຟນແລ້ວ"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"ປິດໄມໂຄຣໂຟນແລ້ວ"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"ເປີດການນຳໃຊ້ໄມໂຄຣໂຟນສຳລັບແອັບ ແລະ ບໍລິການທັງໝົດແລ້ວ."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"ສິດເຂົ້າເຖິງໄມໂຄຣໂຟນຖືກປິດການນຳໃຊ້ສຳລັບແອັບ ແລະ ບໍລິການທັງໝົດແລ້ວ. ທ່ານສາມາດເປີດການນຳໃຊ້ສິດເຂົ້າເຖິງໄມໂຄຣໂຟນໄດ້ໃນການຕັ້ງຄ່າ &gt; ຄວາມເປັນສ່ວນຕົວ &gt; ໄມໂຄຣໂຟນ."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"ສິດເຂົ້າເຖິງໄມໂຄຣໂຟນຖືກປິດການນຳໃຊ້ສຳລັບແອັບ ແລະ ບໍລິການທັງໝົດແລ້ວ. ທ່ານສາມາດປ່ຽນສິ່ງນີ້ໄດ້ໃນການຕັ້ງຄ່າ &gt; ຄວາມເປັນສ່ວນຕົວ &gt; ໄມໂຄຣໂຟນ."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ເປີດກ້ອງຖ່າຍຮູບແລ້ວ"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ປິດກ້ອງຖ່າຍຮູບແລ້ວ"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"ເປີດການນຳໃຊ້ກ້ອງຖ່າຍຮູບສຳລັບແອັບ ແລະ ບໍລິການທັງໝົດແລ້ວ."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"ສິດເຂົ້າເຖິງກ້ອງຖ່າຍຮູບຖືກປິດການນຳໃຊ້ສຳລັບແອັບ ແລະ ບໍລິການທັງໝົດແລ້ວ."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"ເພື່ອໃຊ້ປຸ່ມໄມໂຄຣໂຟນ, ໃຫ້ເປີດການນຳໃຊ້ສິດເຂົ້າເຖິງໄມໂຄຣໂຟນໃນການຕັ້ງຄ່າກ່ອນ."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ເປີດການຕັ້ງຄ່າ."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ອຸປະກອນອື່ນໆ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ສະຫຼັບພາບຮວມ"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"ທ່ານຈະບໍ່ໄດ້ຮັບການລົບກວນຈາກສຽງ ແລະ ການສັ່ນເຕືອນ, ຍົກເວັ້ນໃນເວລາໂມງປຸກດັງ, ມີການແຈ້ງເຕືອນ ຫຼື ມີສາຍໂທເຂົ້າຈາກຜູ້ໂທທີ່ທ່ານລະບຸໄວ້. ທ່ານອາດຍັງຄົງໄດ້ຍິນຫາກທ່ານເລືອກຫຼິ້ນເພງ, ວິດີໂອ ແລະ ເກມ."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ໃນຕອນທີ່ທ່ານກຳລັງແບ່ງປັນ, ບັນທຶກ ຫຼື ສົ່ງສັນຍານແອັບ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ມີສິດເຂົ້າເຖິງສິ່ງທີ່ສະແດງ ຫຼື ຫຼິ້ນຢູ່ໃນແອັບນັ້ນ. ດັ່ງນັ້ນໃຫ້ລະມັດລະວັງກ່ຽວກັບລະຫັດຜ່ານ, ລາຍລະອຽດການຈ່າຍເງິນ, ຂໍ້ຄວາມ ຫຼື ຂໍ້ມູນທີ່ລະອຽດອ່ອນອື່ນໆ."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ສືບຕໍ່"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ແບ່ງປັນ ຫຼື ບັນທຶກແອັບ"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ລຶບລ້າງທັງໝົດ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ຈັດການ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ປະຫວັດ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ປິດອິນເຕີເນັດມືຖືໄວ້ບໍ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"ທ່ານຈະບໍ່ມີສິດເຂົ້າເຖິງຂໍ້ມູນ ຫຼື ອິນເຕີເນັດຜ່ານ <xliff:g id="CARRIER">%s</xliff:g>. ອິນເຕີເນັດຈະສາມາດໃຊ້ໄດ້ຜ່ານ Wi-Fi ເທົ່ານັ້ນ."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ຜູ້​ໃຫ້​ບໍ​ລິ​ການ​ຂອງ​ທ່ານ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"ສະຫຼັບກັບໄປໃຊ້ <xliff:g id="CARRIER">%s</xliff:g> ບໍ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ອິນເຕີເນັດມືຖືຈະບໍ່ປ່ຽນຕາມຄວາມພ້ອມໃຫ້ບໍລິການໂດຍອັດຕະໂນມັດ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ບໍ່, ຂອບໃຈ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ແມ່ນແລ້ວ, ສະຫຼັບ"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ເນື່ອງຈາກມີແອັບໃດໜຶ່ງກຳລັງຂັດຂວາງການຂໍອະນຸຍາດ, ການຕັ້ງຄ່າຈຶ່ງບໍ່ສາມາດຢັ້ງຢືນການຕອບຮັບຂອງທ່ານໄດ້."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"ອະນຸຍາດ <xliff:g id="APP_0">%1$s</xliff:g> ໃຫ້ສະແດງ <xliff:g id="APP_2">%2$s</xliff:g> ສະໄລ້ບໍ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ມັນສາມາດອ່ານຂໍ້ມູນຈາກ <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ການຕັ້ງຄ່າໜ້າຈໍຂະຫຍາຍ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ແຕະເພື່ອເປີດຄຸນສົມບັດການຊ່ວຍເຂົ້າເຖິງ. ປັບແຕ່ງ ຫຼື ປ່ຽນປຸ່ມນີ້ໃນການຕັ້ງຄ່າ.\n\n"<annotation id="link">"ເບິ່ງການຕັ້ງຄ່າ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ຍ້າຍປຸ່ມໄປໃສ່ຂອບເພື່ອເຊື່ອງມັນຊົ່ວຄາວ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ຍົກເລີກ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{ລຶບທາງລັດ {label} ອອກແລ້ວ}other{ລຶບທາງລັດ # ອອກແລ້ວ}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ຍ້າຍຊ້າຍເທິງ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ຍ້າຍຂວາເທິງ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ຍ້າຍຊ້າຍລຸ່ມ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ຍ້າຍຂວາລຸ່ມ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ຍ້າຍອອກຂອບ ແລະ ເຊື່ອງ"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ຍ້າຍອອກຂອບ ແລະ ສະແດງ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ລຶບອອກ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ສະຫຼັບ"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ການຄວບຄຸມອຸປະກອນ"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ເລືອກແອັບເພື່ອເພີ່ມການຄວບຄຸມ"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ອິນເຕີເນັດມືຖື"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ເຊື່ອມຕໍ່ແລ້ວ"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"ເຊື່ອມຕໍ່ແລ້ວຊົ່ວຄາວ"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ສັນຍານເຊື່ອມຕໍ່ຊ້າ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ຈະບໍ່ເຊື່ອມຕໍ່ອິນເຕີເນັດມືຖືອັດຕະໂນມັດ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ບໍ່ມີການເຊື່ອມຕໍ່"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ບໍ່ມີເຄືອຂ່າຍອື່ນທີ່ສາມາດໃຊ້ໄດ້"</string>
diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml
index b9d6f33..5c9969f 100644
--- a/packages/SystemUI/res/values-lt/strings.xml
+++ b/packages/SystemUI/res/values-lt/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Veidas neatpažintas"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Naudoti piršto antspaudą"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"„Bluetooth“ prijungtas."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Akumuliatoriaus energija procentais nežinoma."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Prisijungta prie „<xliff:g id="BLUETOOTH">%s</xliff:g>“."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Šviesumas"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Spalvų inversija"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Spalvų taisymas"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Naudotojo nustatymai"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Tvarkyti naudotojus"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Atlikta"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Uždaryti"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Prijungtas"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofonas pasiekiamas"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Vaizdo kamera pasiekiama"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofonas ir vaizdo kamera pasiekiami"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofonas įjungtas"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofonas išjungtas"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofonas įgalintas veikiant visoms programoms ir paslaugoms."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Visų programų ir paslaugų prieiga prie mikrofono išjungta. Prieigą prie mikrofono galite įjungti nuėję į „Nustatymai“ &gt; „Privatumas“ &gt; „Mikrofonas“."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Visų programų ir paslaugų prieiga prie mikrofono išjungta. Tai galite pakeisti nuėję į „Nustatymai“ &gt; „Privatumas“ &gt; „Mikrofonas“."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Fotoaparatas įjungtas"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Fotoaparatas išjungtas"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Fotoaparatas įgalintas veikiant visoms programoms ir paslaugoms."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Visų programų ir paslaugų prieiga prie fotoaparato išjungta."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Norėdami naudotis mikrofono mygtuku, nustatymuose įjunkite prieigą prie mikrofono."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Atidaryti nustatymus."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Kitas įrenginys"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Perjungti apžvalgą"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Jūsų netrikdys garsai ir vibravimas, išskyrus nurodytų signalų, priminimų, įvykių ir skambintojų garsus. Vis tiek girdėsite viską, ką pasirinksite leisti, įskaitant muziką, vaizdo įrašus ir žaidimus."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kai bendrinate, įrašote ar perduodate turinį, „<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>“ gali pasiekti viską, kas rodoma ar leidžiama programoje. Todėl būkite atsargūs su slaptažodžiais, išsamia mokėjimo metodo informacija, pranešimais ar kita neskelbtina informacija."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Tęsti"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Programos bendrinimas ar įrašymas"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Viską išvalyti"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Tvarkyti"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Istorija"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Išjungti mobiliojo ryšio duomenis?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Naudodamiesi „<xliff:g id="CARRIER">%s</xliff:g>“ paslaugomis neturėsite galimybės pasiekti duomenų arba interneto. Internetą galėsite naudoti tik prisijungę prie „Wi-Fi“."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"savo operatoriaus"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Perjungti atgal į „<xliff:g id="CARRIER">%s</xliff:g>“?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobiliojo ryšio duomenys nebus automatiškai perjungti atsižvelgiant į pasiekiamumą"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, ačiū"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Taip, perjungti"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Kadangi programa užstoja leidimo užklausą, nustatymuose negalima patvirtinti jūsų atsakymo."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Leisti „<xliff:g id="APP_0">%1$s</xliff:g>“ rodyti „<xliff:g id="APP_2">%2$s</xliff:g>“ fragmentus?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Gali nuskaityti informaciją iš „<xliff:g id="APP">%1$s</xliff:g>“"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Didinimo lango nustatymai"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Palietę atidarykite pritaikymo neįgaliesiems funkcijas. Tinkinkite arba pakeiskite šį mygtuką nustatymuose.\n\n"<annotation id="link">"Žr. nustatymus"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Perkelkite mygtuką prie krašto, kad laikinai jį paslėptumėte"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Anuliuoti"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Pašalintas spartusis klavišas „{label}“}one{Pašalintas # spartusis klavišas}few{Pašalinti # spartieji klavišai}many{Pašalinta # sparčiojo klavišo}other{Pašalinta # sparčiųjų klavišų}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Perkelti į viršų kairėje"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Perkelti į viršų dešinėje"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Perkelti į apačią kairėje"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Perkelti į apačią dešinėje"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Perkelti į kraštą ir slėpti"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Perkelti iš krašto ir rodyti"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Pašalinti"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"perjungti"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Įrenginio valdikliai"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Pasirinkite programą, kad pridėtumėte valdiklių"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiliojo ryšio duomenys"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Prisijungta"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Laikinai prijungta"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Prastas ryšys"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Naud. mob. r. duomenis nebus autom. prisijungiama"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nėra ryšio"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nėra kitų pasiekiamų tinklų"</string>
diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml
index 882ff7c..415ba8f9 100644
--- a/packages/SystemUI/res/values-lv/strings.xml
+++ b/packages/SystemUI/res/values-lv/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Nevar atpazīt seju"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Lietot pirksta nospiedumu"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth savienojums ir izveidots."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Akumulatora uzlādes līmenis procentos nav zināms."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Ir izveidots savienojum ar <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Spilgtums"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Krāsu inversija"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Krāsu korekcija"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Lietotāja iestatījumi"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Pārvaldīt lietotājus"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Gatavs"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Aizvērt"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Pievienota"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofons ir pieejams."</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera ir pieejama."</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofons un kamera ir pieejami."</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofons tika ieslēgts"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofons tika izslēgts"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Piekļuve mikrofonam ir iespējota visām lietotnēm un pakalpojumiem."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Piekļuve mikrofonam ir atspējota visām lietotnēm un pakalpojumiem. Varat iespējot piekļuvi mikrofonam sadaļā Iestatījumi &gt; Konfidencialitāte &gt; Mikrofons."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Piekļuve mikrofonam ir atspējota visām lietotnēm un pakalpojumiem. Varat to mainīt sadaļā Iestatījumi &gt; Konfidencialitāte &gt; Mikrofons."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera tika ieslēgta"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera tika izslēgta"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Piekļuve kamerai ir iespējota visām lietotnēm un pakalpojumiem."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Piekļuve kamerai ir atspējota visām lietotnēm un pakalpojumiem."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Lai varētu izmantot mikrofona pogu, iespējojiet piekļuvi mikrofonam sadaļā Iestatījumi."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Atvērt iestatījumus"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Cita ierīce"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Pārskata pārslēgšana"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Jūs netraucēs skaņas un vibrācija, izņemot signālus, atgādinājumus, pasākumus un zvanītājus, ko būsiet norādījis. Jūs joprojām dzirdēsiet atskaņošanai izvēlētos vienumus, tostarp mūziku, videoklipus un spēles."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Lietotnes kopīgošanas, ierakstīšanas vai apraides laikā <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> var piekļūt visam, kas tiek rādīts vai atskaņots attiecīgajā lietotnē. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem un citu sensitīvu informāciju."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Turpināt"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Lietotnes kopīgošana vai ierakstīšana"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Dzēst visu"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Pārvaldīt"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Vēsture"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vai izslēgt mobilos datus?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Izmantojot mobilo sakaru operatora <xliff:g id="CARRIER">%s</xliff:g> pakalpojumus, nevarēsiet piekļūt datiem vai internetam. Internetam varēsiet piekļūt, tikai izmantojot Wi-Fi savienojumu."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"jūsu mobilo sakaru operators"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vai pārslēgties atpakaļ uz operatoru <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilie dati netiks automātiski pārslēgti, pamatojoties uz pieejamību."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nē, paldies"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Jā, pārslēgties"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Lietotne Iestatījumi nevar verificēt jūsu atbildi, jo cita lietotne aizsedz atļaujas pieprasījumu."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vai atļaut lietotnei <xliff:g id="APP_0">%1$s</xliff:g> rādīt lietotnes <xliff:g id="APP_2">%2$s</xliff:g> sadaļas?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Var lasīt informāciju no lietotnes <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Lupas loga iestatījumi"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Atveriet pieejamības funkcijas. Pielāgojiet vai aizstājiet šo pogu iestatījumos.\n\n"<annotation id="link">"Skatīt iestatījumus"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Lai īslaicīgi paslēptu pogu, pārvietojiet to uz malu"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Atsaukt"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Noņemta saīsne “{label}”}zero{Noņemtas # saīsnes}one{Noņemta # saīsne}other{Noņemtas # saīsnes}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Pārvietot augšpusē pa kreisi"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Pārvietot augšpusē pa labi"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Pārvietot apakšpusē pa kreisi"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Pārvietot apakšpusē pa labi"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Pārvietot uz malu un paslēpt"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Pārvietot no malas un parādīt"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Noņemt"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"pārslēgt"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Ierīču vadīklas"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Izvēlieties lietotni, lai pievienotu vadīklas"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilie dati"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ir izveidots savienojums"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Īslaicīgi izveidots savienojums"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Vājš savienojums"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobilo datu savienojums netiks veidots automātiski"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nav savienojuma"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nav pieejams neviens cits tīkls"</string>
diff --git a/packages/SystemUI/res/values-mk/strings.xml b/packages/SystemUI/res/values-mk/strings.xml
index 0d98dc6..0ff5545 100644
--- a/packages/SystemUI/res/values-mk/strings.xml
+++ b/packages/SystemUI/res/values-mk/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Не се препознава ликот"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Користи отпечаток"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth е поврзан."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Процентот на батеријата е непознат."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Поврзано со <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Осветленост"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Инверзија на боите"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Корекција на боите"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Кориснички поставки"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Управувајте со корисниците"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Готово"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Затвори"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Поврзано"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофонот е достапен"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камерата е достапна"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофонот и камерата се достапни"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофонот е вклучен"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофонот е исклучен"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Микрофонот е овозможен за сите апликации и услуги."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Пристапот до микрофонот е оневозможен за сите апликации и услуги. Може да овозможите пристап до микрофонот во „Поставки &gt; Приватност &gt; Микрофон“."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Пристапот до микрофонот е оневозможен за сите апликации и услуги. Ова може да го промените во „Поставки &gt; Приватност &gt; Микрофон“."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камерата е вклучена"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камерата е исклучена"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камерата е овозможена за сите апликации и услуги."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Пристапот до камерата е оневозможен за сите апликации и услуги."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"За да го користите копчето за микрофон, овозможете пристап до микрофонот во „Поставки“."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Отворете ги поставките."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Друг уред"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Вклучи/исклучи преглед"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Нема да ве вознемируваат звуци и вибрации, освен од аларми, потсетници, настани и повикувачи што ќе ги наведете. Сѐ уште ќе слушате сѐ што ќе изберете да пуштите, како музика, видеа и игри."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Кога споделувате, снимате или емитувате апликација, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> има пристап до сѐ што се прикажува или пушта на таа апликација. Затоа, бидете внимателни со лозинки, детали за плаќање, пораки или други чувствителни податоци."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Продолжи"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Споделете или снимете апликација"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Избриши сѐ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Управувајте"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Историја"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Да се исклучи мобилниот интернет?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Нема да имате пристап до податоците или интернетот преку <xliff:g id="CARRIER">%s</xliff:g>. Интернетот ќе биде достапен само преку Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"вашиот оператор"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Да се префрли на <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мобилниот интернет нема автоматски да се префрли според достапноста"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Не, фала"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Да, префрли се"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Бидејќи апликацијата го прикрива барањето за дозвола, „Поставките“ не може да го потврдат вашиот одговор."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Да се дозволи <xliff:g id="APP_0">%1$s</xliff:g> да прикажува делови од <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Може да чита информации од <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Поставки за прозорец за лупа"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Допрете за функциите за пристапност. Приспособете или заменете го копчево во „Поставки“.\n\n"<annotation id="link">"Прикажи поставки"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Преместете го копчето до работ за да го сокриете привремено"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Врати"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Отстранета е {label} кратенка}one{Отстранети се # кратенка}other{Отстранети се # кратенки}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Премести горе лево"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Премести горе десно"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Премести долу лево"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Премести долу десно"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Премести до работ и сокриј"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Премести над работ и прикажи"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Отстрани"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"вклучување/исклучување"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Контроли за уредите"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Изберете апликација за да додадете контроли"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобилен интернет"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Поврзано"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Привремено поврзано"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Слаба интернет-врска"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мобилниот интернет не може да се поврзе автоматски"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Нема интернет-врска"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Нема други достапни мрежи"</string>
diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml
index 868d759..e8fdef35 100644
--- a/packages/SystemUI/res/values-ml/strings.xml
+++ b/packages/SystemUI/res/values-ml/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"മുഖം തിരിച്ചറിയാനാകുന്നില്ല"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"പകരം ഫിംഗർപ്രിന്റ് ഉപയോഗിക്കൂ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ബ്ലൂടൂത്ത് കണക്‌റ്റുചെയ്തു."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ബാറ്ററി ശതമാനം അജ്ഞാതമാണ്."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> എന്നതിലേക്ക് കണക്‌റ്റുചെയ്‌തു."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"തെളിച്ചം"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"നിറം വിപരീതമാക്കൽ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"നിറം ശരിയാക്കൽ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ഉപയോക്തൃ ക്രമീകരണം"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ഉപയോക്താക്കളെ മാനേജ് ചെയ്യുക"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"പൂർത്തിയാക്കി"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"അടയ്ക്കുക"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"കണക്‌റ്റുചെയ്‌തു"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"മൈക്രോഫോൺ ലഭ്യമാണ്"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ക്യാമറ ലഭ്യമാണ്"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"മൈക്രോഫോണും ക്യാമറയും ലഭ്യമാണ്"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"മൈക്രോഫോൺ ഓണാക്കി"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"മൈക്രോഫോൺ ഓഫാക്കി"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"എല്ലാ ആപ്പുകൾക്കും സേവനങ്ങൾക്കും മൈക്രോഫോൺ പ്രവർത്തനക്ഷമമാക്കിയിരിക്കുന്നു."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"എല്ലാ ആപ്പുകൾക്കും സേവനങ്ങൾക്കും മൈക്രോഫോൺ ആക്സസ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു. ക്രമീകരണം &gt; സ്വകാര്യത &gt; മൈക്രോഫോൺ എന്നതിൽ നിങ്ങൾക്ക് മൈക്രോഫോൺ ആക്സസ് പ്രവർത്തനക്ഷമമാക്കാം."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"എല്ലാ ആപ്പുകൾക്കും സേവനങ്ങൾക്കും മൈക്രോഫോൺ ആക്സസ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു. ക്രമീകരണം &gt; സ്വകാര്യത &gt; മൈക്രോഫോൺ എന്നതിൽ ഇത് നിങ്ങൾക്ക് മാറ്റാൻ കഴിയും."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ക്യാമറ ഓണാക്കി"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ക്യാമറ ഓഫാക്കി"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"എല്ലാ ആപ്പുകൾക്കും സേവനങ്ങൾക്കും ക്യാമറ പ്രവർത്തനക്ഷമമാക്കിയിരിക്കുന്നു."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"എല്ലാ ആപ്പുകൾക്കും സേവനങ്ങൾക്കും ക്യാമറാ ആക്സസ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"മൈക്രോഫോൺ ബട്ടൺ ഉപയോഗിക്കുന്നതിന്, ക്രമീകരണത്തിൽ മൈക്രോഫോൺ ആക്സസ് പ്രവർത്തനക്ഷമമാക്കുക."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ക്രമീകരണം തുറക്കുക."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"മറ്റ് ഉപകരണം"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"അവലോകനം മാറ്റുക"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"നിങ്ങൾ സജ്ജീകരിച്ച അലാറങ്ങൾ, റിമൈൻഡറുകൾ, ഇവന്റുകൾ, കോളർമാർ എന്നിവയിൽ നിന്നുള്ള ശബ്‌ദങ്ങളും വൈബ്രേഷനുകളുമൊഴികെ മറ്റൊന്നും നിങ്ങളെ ശല്യപ്പെടുത്തുകയില്ല. സംഗീതം, വീഡിയോകൾ, ഗെയിമുകൾ എന്നിവയുൾപ്പെടെ പ്ലേ ചെയ്യുന്നതെന്തും നിങ്ങൾക്ക് ‌തുടർന്നും കേൾക്കാൻ കഴിയും."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ഒരു ആപ്പ് പങ്കിടുമ്പോൾ, റെക്കോർഡ് ചെയ്യുമ്പോൾ അല്ലെങ്കിൽ കാസ്റ്റ് ചെയ്യുമ്പോൾ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> എന്നതിന് ആപ്പിൽ കാണിക്കുന്ന അല്ലെങ്കിൽ പ്ലേ ചെയ്യുന്ന എല്ലാത്തിലേക്കും ആക്സസ് ഉണ്ട്. അതിനാൽ, പാസ്‍വേഡുകൾ, പേയ്‌മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ അല്ലെങ്കിൽ സൂക്ഷ്‌മമായി കൈകാര്യം ചെയ്യേണ്ട മറ്റു വിവരങ്ങൾ എന്നിവ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"തുടരുക"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ഒരു ആപ്പ് പങ്കിടുക അല്ലെങ്കിൽ റെക്കോർഡ് ചെയ്യുക"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"എല്ലാം മായ്‌ക്കുക"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"മാനേജ് ചെയ്യുക"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ചരിത്രം"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"മൊബൈൽ ഡാറ്റ ഓഫാക്കണോ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"നിങ്ങൾക്ക് ഡാറ്റയിലേക്ക് ആക്‌സസോ അല്ലെങ്കിൽ <xliff:g id="CARRIER">%s</xliff:g> മുഖേനയുള്ള ഇന്റർനെറ്റോ ഉണ്ടാകില്ല. വൈഫൈ മുഖേന മാത്രമായിരിക്കും ഇന്റർനെറ്റ് ലഭ്യത."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"നിങ്ങളുടെ കാരിയർ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> എന്നതിലേക്ക് വീണ്ടും മാറണോ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ലഭ്യതയുടെ അടിസ്ഥാനത്തിൽ, മൊബൈൽ ഡാറ്റ സ്വയമേവ മാറില്ല"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"വേണ്ട"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ഉവ്വ്, മാറുക"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"അനുമതി അഭ്യർത്ഥനയെ ഒരു ആപ്പ് മറയ്‌ക്കുന്നതിനാൽ, ക്രമീകരണത്തിന് നിങ്ങളുടെ പ്രതികരണം പരിശോധിച്ചുറപ്പിക്കാനാകില്ല."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_2">%2$s</xliff:g> സ്ലൈസുകൾ കാണിക്കാൻ <xliff:g id="APP_0">%1$s</xliff:g>-നെ അനുവദിക്കണോ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ഇതിന് <xliff:g id="APP">%1$s</xliff:g>-ൽ നിന്ന് വിവരങ്ങൾ വായിക്കാനാകും"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"മാഗ്നിഫയർ വിൻഡോ ക്രമീകരണം"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ഉപയോഗസഹായി ഫീച്ചർ തുറക്കാൻ ടാപ്പ് ചെയ്യൂ. ക്രമീകരണത്തിൽ ഈ ബട്ടൺ ഇഷ്ടാനുസൃതമാക്കാം, മാറ്റാം.\n\n"<annotation id="link">"ക്രമീകരണം കാണൂ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"തൽക്കാലം മറയ്‌ക്കുന്നതിന് ബട്ടൺ അരുകിലേക്ക് നീക്കുക"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"പഴയപടിയാക്കുക"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} കുറുക്കുവഴി നീക്കം ചെയ്‌തു}other{# കുറുക്കുവഴികൾ നീക്കം ചെയ്‌തു}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"മുകളിൽ ഇടതുഭാഗത്തേക്ക് നീക്കുക"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"മുകളിൽ വലതുഭാഗത്തേക്ക് നീക്കുക"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ചുവടെ ഇടതുഭാഗത്തേക്ക് നീക്കുക"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ചുവടെ വലതുഭാഗത്തേക്ക് നീക്കുക"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"എഡ്‌ജിലേക്ക് നീക്കി മറയ്‌ക്കുക"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"എഡ്‌ജിൽ നിന്ന് നീക്കി കാണിക്കൂ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"നീക്കം ചെയ്യുക"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"മാറ്റുക"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ഉപകരണ നിയന്ത്രണങ്ങൾ"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"നിയന്ത്രണങ്ങൾ ചേർക്കാൻ ആപ്പ് തിരഞ്ഞെടുക്കുക"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"മൊബൈൽ ഡാറ്റ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"കണക്റ്റ് ചെയ്തു"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"താൽക്കാലികമായി കണക്റ്റ് ചെയ്തിരിക്കുന്നു"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ദുർബലമായ കണക്ഷൻ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"മൊബൈൽ ഡാറ്റ സ്വയം കണക്റ്റ് ചെയ്യില്ല"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"കണക്ഷനില്ല"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"മറ്റ് നെറ്റ്‌വർക്കുകളൊന്നും ലഭ്യമല്ല"</string>
diff --git a/packages/SystemUI/res/values-mn/strings.xml b/packages/SystemUI/res/values-mn/strings.xml
index 25929e6..6be28dd 100644
--- a/packages/SystemUI/res/values-mn/strings.xml
+++ b/packages/SystemUI/res/values-mn/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Царайг танихгүй байна"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Оронд нь хурууны хээ ашиглах"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth холбогдсон."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Батарейн хувь тодорхойгүй байна."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>-тай холбогдсон."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Тодрол"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Өнгө хувиргалт"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Өнгө тохируулга"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Хэрэглэгчийн тохиргоо"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Хэрэглэгчдийг удирдах"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Дууссан"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Хаах"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Холбогдсон"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофон боломжтой байна"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камер боломжтой байна"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофон болон камер боломжтой байна"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофоныг асаасан"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофоныг унтраасан"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Микрофоныг бүх програм, үйлчилгээнд идэвхжүүлсэн."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Бүх програм, үйлчилгээнд микрофоны хандалтыг идэвхгүй болгосон. Та микрофоны хандалтыг тохиргоо идэвхжүүлж байна &gt; Нууцлал &gt; Микрофон идэвхжүүлж болно."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Бүх програм, үйлчилгээнд микрофоны хандалтыг идэвхгүй болгосон. Та үүнийг Тохиргоо &gt; Нууцлал &gt; Микрофон идэвхжүүлж болно."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камерыг асаасан"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камерыг унтраасан"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камерыг бүх програм, үйлчилгээнд идэвхжүүлсэн."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Бүх апп болон үйлчилгээнд камерын хандалтыг идэвхгүй болгосон."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Микрофоны товчлуурыг ашиглахын тулд \"Тохиргоо\" хэсэгт микрофоны хандалтыг идэвхжүүлнэ үү."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Тохиргоог нээнэ үү."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Бусад төхөөрөмж"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Тоймыг асаах/унтраах"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Танд сэрүүлэг, сануулга, арга хэмжээ, таны сонгосон дуудлага илгээгчээс бусад дуу, чичиргээ саад болохгүй. Та хөгжим, видео, тоглоом зэрэг тоглуулахыг хүссэн бүх зүйлээ сонсох боломжтой хэвээр байна."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Таныг хуваалцаж, бичиж эсвэл дамжуулж байх үед <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> нь тухайн апп дээр харуулсан эсвэл тоглуулсан аливаа зүйлд хандах эрхтэй. Тиймээс нууц үг, төлбөрийн дэлгэрэнгүй, мессеж эсвэл бусад эмзэг мэдээлэлд болгоомжтой хандаарай."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Үргэлжлүүлэх"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Хуваалцах эсвэл бичих апп"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Бүгдийг арилгах"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Удирдах"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Түүх"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Мобайл датаг унтраах уу?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Та <xliff:g id="CARRIER">%s</xliff:g>-р дата эсвэл интернэтэд хандах боломжгүй болно. Интернэтэд зөвхөн Wi-Fi-р холбогдох боломжтой болно."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"таны оператор компани"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> руу буцаан сэлгэх үү?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мобайл дата нь боломжтой эсэхэд тулгуурлан автоматаар сэлгэхгүй"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Үгүй, баярлалаа"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Тийм, сэлгэе"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Апп нь зөвшөөрлийн хүсэлтийг танихгүй байгаа тул Тохиргооноос таны хариултыг баталгаажуулах боломжгүй байна."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g>-д <xliff:g id="APP_2">%2$s</xliff:g>-н хэсгүүдийг (slices) харуулахыг зөвшөөрөх үү?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Энэ нь <xliff:g id="APP">%1$s</xliff:g>-с мэдээлэл унших боломжтой"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Томруулагчийн цонхны тохиргоо"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Хандалтын онцлогуудыг нээхийн тулд товшино уу. Энэ товчлуурыг Тохиргоо хэсэгт өөрчилж эсвэл солиорой.\n\n"<annotation id="link">"Тохиргоог харах"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Үүнийг түр нуухын тулд товчлуурыг зах руу зөөнө үү"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Болих"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} товчлолыг хассан}other{# товчлолыг хассан}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Зүүн дээш зөөх"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Баруун дээш зөөх"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Зүүн доош зөөх"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Баруун доош зөөх"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Ирмэг рүү зөөж, нуух"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Ирмэгээс гаргаж, харуулах"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Хасах"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"асаах/унтраах"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Төхөөрөмжийн хяналт"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Хяналтууд нэмэхийн тулд аппыг сонгоно уу"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобайл дата"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Холбогдсон"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Түр зуур холбогдсон"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Холболт сул байна"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мобайл дата автоматаар холбогдохгүй"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Холболт алга"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Өөр боломжтой сүлжээ байхгүй байна"</string>
diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml
index c6e99a7..7c1dcc3 100644
--- a/packages/SystemUI/res/values-mr/strings.xml
+++ b/packages/SystemUI/res/values-mr/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"चेहरा ओळखू शकत नाही"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"त्याऐवजी फिंगरप्रिंट वापरा"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ब्लूटूथ कनेक्‍ट केले."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"बॅटरीच्या चार्जिंगची टक्केवारी माहित नाही."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> शी कनेक्‍ट केले."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"चमक"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"कलर इन्व्हर्जन"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"रंग सुधारणा"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"वापरकर्ता सेटिंग्ज"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"वापरकर्ते व्यवस्‍थापित करा"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"पूर्ण झाले"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"बंद करा"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"कनेक्ट केलेले"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"मायक्रोफोन उपलब्ध आहे"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"कॅमेरा उपलब्ध आहे"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"मायक्रोफोन आणि कॅमेरा या गोष्टी उपलब्ध आहेत"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"मायक्रोफोन सुरू केला"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"मायक्रोफोन बंद केला"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"सर्व ॲप्स आणि सेवांसाठी मायक्रोफोन सुरू केला आहे."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"सर्व ॲप्स आणि सेवांसाठी मायक्रोफोन अ‍ॅक्सेस बंद केला आहे. तुम्ही मायक्रोफोन अ‍ॅक्सेस सेटिंग्ज &gt; गोपनीयता &gt; मायक्रोफोन मधून सुरू करू शकता."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"सर्व ॲप्स आणि सेवांसाठी मायक्रोफोन अ‍ॅक्सेस बंद केला आहे. तुम्ही हे सेटिंग्ज &gt; गोपनीयता &gt; मायक्रोफोन मधून बदलू शकता ."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"कॅमेरा सुरू केला आहे"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"कॅमेरा बंद केला आहे"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"सर्व ॲप्स आणि सेवांसाठी कॅमेरा सुरू केला आहे."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"सर्व ॲप्स आणि सेवांसाठी कॅमेरा अ‍ॅक्सेस बंद केला आहे."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"मायक्रोफोन बटण वापरण्यासाठी, सेटिंग्जमधून मायक्रोफोन अ‍ॅक्सेस सुरू करा."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"सेटिंग्ज उघडा."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"इतर डिव्हाइस"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"अवलोकन टॉगल करा."</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"अलार्म, रिमाइंडर, इव्‍हेंट आणि तुम्ही निश्चित केलेल्या कॉलर व्यतिरिक्त तुम्हाला कोणत्याही आवाज आणि कंपनांचा व्यत्त्यय आणला जाणार नाही. तरीही तुम्ही प्ले करायचे ठरवलेले कोणतेही संगीत, व्हिडिओ आणि गेमचे आवाज ऐकू शकतात."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"तुम्ही अ‍ॅप शेअर, रेकॉर्ड किंवा कास्ट करत असताना, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ला त्या अ‍ॅपवर दाखवलेल्या किंवा प्ले केलेल्या कोणत्याही गोष्टीचा अ‍ॅक्सेस असतो. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज किंवा इतर संवेदनशील माहिती काळजीपूर्वक वापरा."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"पुढे सुरू ठेवा"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"अ‍ॅप शेअर किंवा रेकॉर्ड करा"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"सर्व साफ करा"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"व्यवस्थापित करा"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"मोबाइल डेटा बंद करायचा?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"तुम्हाला <xliff:g id="CARRIER">%s</xliff:g> मधून डेटा किंवा इंटरनेटचा अ‍ॅक्सेस नसेल. इंटरनेट फक्त वाय-फाय मार्फत उपलब्ध असेल."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"तुमचा वाहक"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> वर परत स्विच करायचे आहे का?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"उपलब्धतेच्या आधारावर मोबाइल डेटा आपोआप स्विच होणार नाही"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"नाही, नको"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"होय, स्विच करा"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"अ‍ॅप परवानगी विनंती अस्पष्‍ट करत असल्‍याने, सेटिंग्ज तुमचा प्रतिसाद पडताळू शकत नाहीत."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> ला <xliff:g id="APP_2">%2$s</xliff:g> चे तुकडे दाखवण्याची अनुमती द्यायची का?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ते <xliff:g id="APP">%1$s</xliff:g> ची माहिती वाचू शकते"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"मॅग्निफायर विंडो सेटिंग्ज"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"अ‍ॅक्सेसिबिलिटी वैशिष्ट्ये उघडण्यासाठी, टॅप करा. सेटिंग्जमध्ये हे बटण कस्टमाइझ करा किंवा बदला.\n\n"<annotation id="link">"सेटिंग्ज पहा"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"बटण तात्पुरते लपवण्यासाठी ते कोपर्‍यामध्ये हलवा"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"पहिल्यासारखे करा"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} शॉर्टकट काढून टाकला आहे}other{# शॉर्टकट काढून टाकले आहेत}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"वर डावीकडे हलवा"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"वर उजवीकडे हलवा"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"तळाशी डावीकडे हलवा"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"तळाशी उजवीकडे हलवा"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"एजवर हलवा आणि लपवा"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"एजवर हलवा आणि दाखवा"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"काढून टाका"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"टॉगल करा"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"डिव्हाइस नियंत्रणे"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"नियंत्रणे जोडण्यासाठी ॲप निवडा"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"मोबाइल डेटा"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"कनेक्ट केले आहे"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"तात्पुरते कनेक्ट केलेले"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"खराब कनेक्शन"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"मोबाइल डेटा ऑटो-कनेक्ट होणार नाही"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"कोणतेही कनेक्शन नाही"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"इतर कोणतेही नेटवर्क उपलब्ध नाहीत"</string>
diff --git a/packages/SystemUI/res/values-ms/strings.xml b/packages/SystemUI/res/values-ms/strings.xml
index 99349b1..08132f4 100644
--- a/packages/SystemUI/res/values-ms/strings.xml
+++ b/packages/SystemUI/res/values-ms/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Tak dapat mengecam wajah"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Gunakan cap jari"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth disambungkan."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Peratusan kuasa bateri tidak diketahui."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Disambungkan kepada <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Kecerahan"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Penyongsangan warna"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Pembetulan warna"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Tetapan pengguna"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Urus pengguna"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Selesai"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Tutup"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Disambungkan"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon tersedia"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera tersedia"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon dan kamera tersedia"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Peranti lain"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Togol Ikhtisar"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Anda tidak akan diganggu oleh bunyi dan getaran, kecuali daripada penggera, peringatan, acara dan pemanggil yang anda tetapkan. Anda masih mendengar item lain yang anda pilih untuk dimainkan termasuk muzik, video dan permainan."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Apabila anda berkongsi, merakam atau menghantar apl, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> mempunyai akses kepada apa-apa yang dipaparkan atau dimainkan pada apl tersebut. Jadi berhati-hati dengan kata laluan, butiran pembayaran, mesej atau maklumat sensitif lain."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Teruskan"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Kongsi atau rakam apl"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Kosongkan semua"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Urus"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Sejarah"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Matikan data mudah alih?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Anda tidak akan mempunyai akses kepada data atau Internet melalui <xliff:g id="CARRIER">%s</xliff:g>. Internet hanya tersedia melaui Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"pembawa anda"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Tukar kembali kepada <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Data mudah alih tidak akan ditukar secara automatik berdasarkan ketersediaan"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Tidak perlu"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ya, tukar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Oleh sebab apl melindungi permintaan kebenaran, Tetapan tidak dapat mengesahkan jawapan anda."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Benarkan <xliff:g id="APP_0">%1$s</xliff:g> menunjukkan <xliff:g id="APP_2">%2$s</xliff:g> hirisan?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Hos hirisan boleh membaca maklumat daripada <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Tetapan tetingkap penggadang"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Ketik untuk membuka ciri kebolehaksesan. Sesuaikan/gantikan butang ini dalam Tetapan.\n\n"<annotation id="link">"Lihat tetapan"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Gerakkan butang ke tepi untuk disembunyikan buat sementara waktu"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Buat asal"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} pintasan dialih keluar}other{# pintasan dialih keluar}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Alihkan ke atas sebelah kiri"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Alihkan ke atas sebelah kanan"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Alihkan ke bawah sebelah kiri"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Alihkan ke bawah sebelah kanan"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Alihkan ke tepi dan sorokkan"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Alihkan ke tepi dan tunjukkan"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Alih keluar"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"togol"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kawalan peranti"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Pilih apl untuk menambahkan kawalan"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Data mudah alih"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Disambungkan"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Disambungkan buat sementara waktu"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Sambungan lemah"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Data mudah alih tidak akan autosambung"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Tiada sambungan"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Tiada rangkaian lain yang tersedia"</string>
diff --git a/packages/SystemUI/res/values-my/strings.xml b/packages/SystemUI/res/values-my/strings.xml
index f30846a..51dbb5d 100644
--- a/packages/SystemUI/res/values-my/strings.xml
+++ b/packages/SystemUI/res/values-my/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"မျက်နှာကို မမှတ်မိပါ"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"လက်ဗွေကို အစားထိုးသုံးပါ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ဘလူးတုသ်ချိတ်ဆက်ထားမှု"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ဘက်ထရီရာခိုင်နှုန်းကို မသိပါ။"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>သို့ ချိတ်ဆက်ထား"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"အလင်းတောက်ပမှု"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"အရောင်ပြောင်းပြန်ပြုလုပ်ရန်"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"အရောင် အမှန်ပြင်ခြင်း"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"အသုံးပြုသူ ဆက်တင်များ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"အသုံးပြုသူများ စီမံရန်"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ပြီးပါပြီ"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ပိတ်ရန်"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ချိတ်ဆက်ထား"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"မိုက်ခရိုဖုန်း သုံးနိုင်သည်"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ကင်မရာ သုံးနိုင်သည်"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"မိုက်ခရိုဖုန်းနှင့် ကင်မရာ သုံးနိုင်သည်"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"မိုက်ခရိုဖုန်းကို ဖွင့်ထားသည်"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"မိုက်ခရိုဖုန်း ပိတ်ထားသည်"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"အက်ပ်နှင့် ဝန်ဆောင်မှုအားလုံးအတွက် မိုက်ခရိုဖုန်းကို ဖွင့်ထားသည်။"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"အက်ပ်နှင့် ဝန်ဆောင်မှုအားလုံးအတွက် မိုက်ခရိုဖုန်းသုံးခွင့်ကို ပိတ်ထားသည်။ မိုက်ခရိုဖုန်းသုံးခွင့်ကို ဆက်တင်များ &gt; ကိုယ်ရေးအချက်အလက်လုံခြုံမှု &gt; မိုက်ခရိုဖုန်း တွင်ဖွင့်နိုင်သည်။"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"အက်ပ်နှင့် ဝန်ဆောင်မှုအားလုံးအတွက် မိုက်ခရိုဖုန်းသုံးခွင့်ကို ပိတ်ထားသည်။ ၎င်းကို ဆက်တင်များ &gt; ကိုယ်ရေးအချက်အလက်လုံခြုံမှု &gt; မိုက်ခရိုဖုန်း တွင်ပြောင်းနိုင်သည်။"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ကင်မရာ ဖွင့်ထားသည်"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ကင်မရာ ပိတ်ထားသည်"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"အက်ပ်နှင့် ဝန်ဆောင်မှုအားလုံးအတွက် ကင်မရာကို ဖွင့်ထားသည်။"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"အက်ပ်နှင့် ဝန်ဆောင်မှုအားလုံးအတွက် ကင်မရာသုံးခွင့်ကို ပိတ်ထားသည်။"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"မိုက်ခရိုဖုန်းခလုတ် အသုံးပြုရန် ဆက်တင်များတွင် မိုက်ခရိုဖုန်းသုံးခွင့်ကို ဖွင့်ပါ။"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ဆက်တင်များဖွင့်ရန်။"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"အခြားစက်ပစ္စည်း"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ဖွင့်၊ ပိတ် အနှစ်ချုပ်"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"နှိုးစက်သံ၊ သတိပေးချက်အသံများ၊ ပွဲစဉ်သတိပေးသံများနှင့် သင်ခွင့်ပြုထားသူများထံမှ ဖုန်းခေါ်မှုများမှလွဲ၍ အခြားအသံများနှင့် တုန်ခါမှုများက သင့်ကို အနှောင့်အယှက်ပြုမည် မဟုတ်ပါ။ သို့သော်လည်း သီချင်း၊ ဗီဒီယိုနှင့် ဂိမ်းများအပါအဝင် သင်ကရွေးချယ်ဖွင့်ထားသည့် အရာတိုင်း၏ အသံကိုမူ ကြားနေရဆဲဖြစ်ပါလိမ့်မည်။"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"အက်ပ်ဖြင့် မျှဝေ၊ ရိုက်ကူး (သို့) ကာစ်လုပ်သည့်အခါ <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> သည် ၎င်းအက်ပ်တွင် ပြထားသည့် (သို့) ဖွင့်ထားသည့် အရာအားလုံးကို တွေ့နိုင်သည်။ ထို့ကြောင့် စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ် (သို့) အခြားအရေးကြီးအချက်အလက်များနှင့်ပတ်သက်၍ ဂရုစိုက်ပါ။"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ရှေ့ဆက်ရန်"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"အက်ပ် မျှဝေခြင်း (သို့) ရိုက်ကူးခြင်း"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"အားလုံးရှင်းရန်"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"စီမံရန်"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"မှတ်တမ်း"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"မိုဘိုင်းဒေတာ ပိတ်မလား။"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> မှတစ်ဆင့် ဒေတာ သို့မဟုတ် အင်တာနက်ကို မသုံးနိုင်ပါ။ Wi-Fi ဖြင့်သာ အင်တာနက် သုံးနိုင်သည်။"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"သင်၏ ဝန်ဆောင်မှုပေးသူ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> သို့ ပြန်ပြောင်းမလား။"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ရနိုင်မှုပေါ် အခြေခံပြီး မိုဘိုင်းဒေတာကို အလိုအလျောက် မပြောင်းပါ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"မလိုပါ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ပြောင်းရန်"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"အပလီကေးရှင်းတစ်ခုက ခွင့်ပြုချက်တောင်းခံမှုကို ပိတ်ထားသောကြောင့် ဆက်တင်များသည် သင်၏ လုပ်ဆောင်ကို တုံ့ပြန်နိုင်ခြင်းမရှိပါ။"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> အား <xliff:g id="APP_2">%2$s</xliff:g> ၏အချပ်များ ပြသခွင့်ပြုပါသလား။"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ၎င်းသည် <xliff:g id="APP">%1$s</xliff:g> မှ အချက်အလက်ကို ဖတ်နိုင်သည်"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"မှန်ဘီလူးဝင်းဒိုး ဆက်တင်များ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"အများသုံးစွဲနိုင်မှုဆိုင်ရာ ဝန်ဆောင်မှုများ ဖွင့်ရန် တို့ပါ။ ဆက်တင်များတွင် ဤခလုတ်ကို စိတ်ကြိုက်ပြင်ပါ (သို့) လဲပါ။\n\n"<annotation id="link">"ဆက်တင်များ ကြည့်ရန်"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ခလုတ်ကို ယာယီဝှက်ရန် အစွန်းသို့ရွှေ့ပါ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"နောက်ပြန်ရန်"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{ဖြတ်လမ်းလင့်ခ် {label} ခု ဖယ်ရှားပြီးပြီ}other{ဖြတ်လမ်းလင့်ခ် # ခု ဖယ်ရှားပြီးပြီ}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ဘယ်ဘက်ထိပ်သို့ ရွှေ့ရန်"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ညာဘက်ထိပ်သို့ ရွှေ့ရန်"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ဘယ်ဘက်အောက်ခြေသို့ ရွှေ့ရန်"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ညာဘက်အောက်ခြေသို့ ရွှေ့ရန်"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"အစွန်းသို့ရွှေ့ပြီး ဝှက်ရန်"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"အစွန်းမှရွှေ့ပြီး ပြရန်"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ဖယ်ရှားရန်"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ပြောင်းရန်"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"စက်ထိန်းစနစ်"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ထိန်းချုပ်မှုများထည့်ရန် အက်ပ်ရွေးခြင်း"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"မိုဘိုင်းဒေတာ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ချိတ်ဆက်ထားသည်"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"ယာယီချိတ်ဆက်ထားသည်"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ချိတ်ဆက်မှုအားနည်းသည်"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"မိုဘိုင်းဒေတာ အော်တိုမချိတ်ပါ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ချိတ်ဆက်မှုမရှိပါ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"အခြားကွန်ရက်များ မရှိပါ"</string>
diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml
index 117c864..eba9731 100644
--- a/packages/SystemUI/res/values-nb/strings.xml
+++ b/packages/SystemUI/res/values-nb/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Ansiktet gjenkjennes ikke"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Bruk fingeravtrykk"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth er tilkoblet."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batteriprosenten er ukjent."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Koblet til <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Lysstyrke"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Fargeinvertering"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Fargekorrigering"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Brukerinnstillinger"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Administrer brukere"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Ferdig"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Lukk"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Tilkoblet"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon er tilgjengelig"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera er tilgjengelig"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon og kamera er tilgjengelig"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofonen er slått på"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofonen er slått av"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofonen er slått på for alle apper og tjenester."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofontilgang er slått av for alle apper og tjenester. Du kan gi mikrofontilgang i Innstillinger &gt; Personvern &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofontilgang er slått av for alle apper og tjenester. Du kan endre dette i Innstillinger &gt; Personvern &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kameraet er slått på"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kameraet er slått av"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kameraet er slått på for alle apper og tjenester."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kameratilgang er slått av for alle apper og tjenester."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"For å bruke mikrofonknappen, gi mikrofontilgang i innstillingene."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Åpne innstillingene."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Annen enhet"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Slå oversikten av eller på"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Du blir ikke forstyrret av lyder og vibrasjoner, med unntak av alarmer, påminnelser, aktiviteter og oppringere du angir. Du kan fremdeles høre alt du velger å spille av, for eksempel musikk, videoer og spill."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Når du deler, tar opp eller caster en app, har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tilgang til alt som vises eller spilles av i den aktuelle appen. Derfor bør du være forsiktig med passord, betalingsopplysninger, meldinger og annen sensitiv informasjon."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsett"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Del eller ta opp en app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Fjern alt"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Administrer"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Logg"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vil du slå av mobildata?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Du får ikke tilgang til data eller internett via <xliff:g id="CARRIER">%s</xliff:g>. Internett er bare tilgjengelig via Wifi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operatøren din"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vil du bytte tilbake til <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Det byttes ikke mobildataoperatør automatisk basert på tilgjengelighet"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nei takk"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, bytt"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Fordi en app skjuler tillatelsesforespørselen, kan ikke Innstillinger bekrefte svaret ditt."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vil du tillate at <xliff:g id="APP_0">%1$s</xliff:g> viser <xliff:g id="APP_2">%2$s</xliff:g>-utsnitt?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Den kan lese informasjon fra <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Innstillinger for forstørringsvindu"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Trykk for å åpne tilgj.funksjoner. Tilpass eller bytt knappen i Innstillinger.\n\n"<annotation id="link">"Se innstillingene"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Flytt knappen til kanten for å skjule den midlertidig"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Angre"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} snarvei er fjernet}other{# snarveier er fjernet}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Flytt til øverst til venstre"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Flytt til øverst til høyre"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Flytt til nederst til venstre"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Flytt til nederst til høyre"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Flytt til kanten og skjul"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Flytt ut kanten og vis"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Fjern"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"slå av/på"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Enhetsstyring"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Velg en app for å legge til kontroller"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobildata"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Tilkoblet"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Koblet til midlertidig"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Dårlig forbindelse"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobildata kobler ikke til automatisk"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Ingen tilkobling"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Ingen andre nettverk er tilgjengelige"</string>
diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml
index 3519715..23ff09b 100644
--- a/packages/SystemUI/res/values-ne/strings.xml
+++ b/packages/SystemUI/res/values-ne/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"अनुहार पहिचान गर्न सकिएन"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"बरु फिंगरप्रिन्ट प्रयोग गर्नुहोस्"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ब्लुटुथ जडान भयो।"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ब्याट्रीमा कति प्रतिशत चार्ज छ भन्ने कुराको जानाकरी छैन।"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> मा जडित।"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"उज्यालपन"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"कलर इन्भर्सन"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"कलर करेक्सन"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"प्रयोगकर्तासम्बन्धी सेटिङ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"प्रयोगकर्ताहरू व्यवस्थित गर्नुहोस्"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"भयो"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"बन्द गर्नुहोस्"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"जोडिएको"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"माइक्रोफोन उपलब्ध छ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"क्यामेरा उपलब्ध छ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"माइक्रोफोन र क्यामेरा उपलब्ध छन्"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"माइक्रोफोन अन गरियो"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"माइक्रोफोन अफ गरियो"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"सबै एप तथा सेवाहरूलाई माइक्रोफोन प्रयोग गर्ने अनुमति दिइएको छ।"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"कुनै पनि एप तथा सेवालाई माइक्रोफोन प्रयोग गर्ने अनुमति दिइएको छैन। तपाईं \"सेटिङ &gt; गोपनीयता &gt; माइक्रोफोन\" मा गई माइक्रोफोन प्रयोग गर्ने अनुमति दिन सक्नुहुन्छ।"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"कुनै पनि एप तथा सेवालाई माइक्रोफोन प्रयोग गर्ने अनुमति दिइएको छैन। तपाईं \"सेटिङ &gt; गोपनीयता &gt; माइक्रोफोन\" मा गई यो कुरा परिवर्तन गर्न सक्नुहुन्छ।"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"क्यामेरा अन गरियो"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"क्यामेरा अफ गरियो"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"सबै एप तथा सेवाहरूलाई क्यामेरा प्रयोग गर्ने अनुमति दिइएको छ।"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"कुनै पनि एप तथा सेवालाई क्यामेरा प्रयोग गर्ने अनुमति दिइएको छैन।"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"तपाईं माइक्रोफोन बटन प्रयोग गर्न चाहनुहुन्छ भने सेटिङमा गई माइक्रोफोन प्रयोग गर्ने अनुमति दिनुहोस्।"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"सेटिङ खोल्नुहोस्।"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"अर्को डिभाइड"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"परिदृश्य टगल गर्नुहोस्"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"तपाईंलाई अलार्म, रिमाइन्डर, कार्यक्रम र तपाईंले निर्दिष्ट गर्नुभएका कलरहरू बाहेकका ध्वनि र कम्पनहरूले बाधा पुऱ्याउने छैनन्। तपाईंले अझै सङ्गीत, भिडियो र खेलहरू लगायत आफूले प्ले गर्न छनौट गरेका जुनसुकै कुरा सुन्न सक्नुहुनेछ।"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"तपाईंले सेयर गर्दा, रेकर्ड गर्दा वा कास्ट गर्दा<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ले तपाईंको स्क्रिनमा देखिने वा डिभाइसमा प्ले गरिएका सबै कुरा खिच्न सक्छ। त्यसैले पासवर्ड, भुक्तानीको विवरण, म्यासेज वा अन्य संवेदनशील जानकारी सुरक्षित राख्नुहोला।"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"जारी राख्नुहोस्"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"सेयर वा रेकर्ड गर्नका लागि एप चयन गर्नुहोस्"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"सबै हटाउनुहोस्"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"व्यवस्थित गर्नुहोस्"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"मोबाइल डेटा निष्क्रिय पार्ने हो?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"तपाईं <xliff:g id="CARRIER">%s</xliff:g> मार्फत डेटा वा इन्टरनेट प्रयोग गर्न सक्नुहुने छैन। Wi-Fi मार्फत मात्र इन्टरनेट उपलब्ध हुने छ।"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"तपाईंको सेवा प्रदायक"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"फेरि <xliff:g id="CARRIER">%s</xliff:g> को मोबाइल डेटा अन गर्ने हो?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"मोबाइल डेटा उपलब्धताका आधारमा स्वतः बदलिँदैन"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"पर्दैन, धन्यवाद"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"हुन्छ, बदल्नुहोस्"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"कुनै एपको कारणले अनुमतिसम्बन्धी अनुरोध बुझ्न गाह्रो भइरहेकोले सेटिङहरूले तपाईंको प्रतिक्रिया प्रमाणित गर्न सक्दैनन्।"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> लाई <xliff:g id="APP_2">%2$s</xliff:g> का स्लाइसहरू देखाउन अनुमति दिने हो?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- यसले <xliff:g id="APP">%1$s</xliff:g> बाट जानकारी पढ्न सक्छ"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"म्याग्निफायर विन्डोसम्बन्धी सेटिङ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"सर्वसुलभता कायम गर्ने सुविधा खोल्न ट्याप गर्नुहोस्। सेटिङमा गई यो बटन कस्टमाइज गर्नुहोस् वा बदल्नुहोस्।\n\n"<annotation id="link">"सेटिङ हेर्नुहोस्"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"यो बटन केही बेर नदेखिने पार्न किनारातिर सार्नुहोस्"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"अन्डू गर्नुहोस्"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} वटा सर्टकट हटाइयो}other{# वटा सर्टकट हटाइयो}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"सिरानको बायाँतिर सार्नुहोस्"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"सिरानको दायाँतिर सार्नुहोस्"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"पुछारको बायाँतिर सार्नुहोस्"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"पुछारको दायाँतिर सार्नुहोस्"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"किनारामा सार्नुहोस् र नदेखिने पार्नु…"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"किनाराबाट सार्नुहोस् र देखिने पार्नु…"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"हटाउनुहोस्"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"टगल गर्नुहोस्"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"डिभाइस नियन्त्रण गर्ने विजेटहरू"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"कन्ट्रोल थप्नु पर्ने एप छान्नुहोस्"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"मोबाइल डेटा"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"इन्टरनेटमा कनेक्ट गरिएको छ"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"केही समयका लागि मोबाइल डेटामा कनेक्ट गरिएको छ"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"इन्टरनेट राम्री चलेको छैन"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"मोबाइल डेटा स्वतः कनेक्ट हुँदैन"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"इन्टरनेट छैन"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"अन्य नेटवर्क उपलब्ध छैनन्"</string>
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index dc2bee5..16152f8 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -99,6 +99,8 @@
 
     <!-- Accessibility floating menu -->
     <color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
+    <color name="accessibility_floating_menu_message_background">@*android:color/background_material_dark</color>
+    <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_dark</color>
 
     <color name="people_tile_background">@color/material_dynamic_secondary20</color>
 </resources>
diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml
index 3c85148..a36f811 100644
--- a/packages/SystemUI/res/values-nl/strings.xml
+++ b/packages/SystemUI/res/values-nl/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Gezicht niet herkend"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Vingerafdruk gebruiken"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth-verbinding ingesteld."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batterijpercentage onbekend."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Verbonden met <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Helderheid"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Kleurinversie"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Kleurcorrectie"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Gebruikersinstellingen"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gebruikers beheren"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Klaar"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Sluiten"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Verbonden"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfoon beschikbaar"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Camera beschikbaar"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microfoon en camera beschikbaar"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfoon staat aan"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfoon staat uit"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microfoon staat aan voor alle apps en services."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Microfoontoegang staat uit voor alle apps en services. Je kunt microfoontoegang aanzetten via Instellingen &gt; Privacy &gt; Microfoon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Microfoontoegang staat uit voor alle apps en services. Je kunt dit wijzigen via Instellingen &gt; Privacy &gt; Microfoon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera staat aan"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera staat uit"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera staat aan voor alle apps en services."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Cameratoegang staat uit voor alle apps en services."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Als je de microfoonknop wilt gebruiken, zet je microfoontoegang aan via Instellingen."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Instellingen openen."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Ander apparaat"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Overzicht aan- of uitzetten"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Je wordt niet gestoord door geluiden en trillingen, behalve bij wekkers, herinneringen, afspraken en specifieke bellers die je selecteert. Je kunt nog steeds alles horen wat je wilt afspelen, waaronder muziek, video\'s en games."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Als je deelt, opneemt of cast, heeft <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> toegang tot alles dat wordt getoond of afgespeeld in die app. Wees daarom voorzichtig met wachtwoorden, betalingsgegevens, berichten en andere gevoelige informatie."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Doorgaan"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"App delen of opnemen"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Alles wissen"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Beheren"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Geschiedenis"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Mobiele data uitzetten?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Je hebt dan geen toegang meer tot data of internet via <xliff:g id="CARRIER">%s</xliff:g>. Internet is alleen nog beschikbaar via wifi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"je provider"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Terugschakelen naar <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobiele data worden niet automatisch overgezet op basis van beschikbaarheid"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nee, bedankt"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, overschakelen"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Aangezien een app een rechtenverzoek afdekt, kan Instellingen je reactie niet verifiëren."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> toestaan om segmenten van <xliff:g id="APP_2">%2$s</xliff:g> te tonen?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Deze kan informatie lezen van <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Instellingen voor vergrotingsvenster"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tik voor toegankelijkheidsfuncties. Wijzig of vervang deze knop via Instellingen.\n\n"<annotation id="link">"Naar Instellingen"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Knop naar de rand verplaatsen om deze tijdelijk te verbergen"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Ongedaan maken"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Snelkoppeling voor {label} verwijderd}other{# snelkoppelingen verwijderd}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Naar linksboven verplaatsen"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Naar rechtsboven verplaatsen"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Naar linksonder verplaatsen"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Naar rechtsonder verplaatsen"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Naar rand verplaatsen en verbergen"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Over rand verplaatsen en tonen"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Verwijderen"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"schakelen"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Apparaatbediening"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Kies de app waaraan je bedieningselementen wilt toevoegen"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobiele data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Verbonden"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tijdelijk verbonden"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Matige verbinding"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobiele data maakt niet automatisch verbinding"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Geen verbinding"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Geen andere netwerken beschikbaar"</string>
diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml
index 1cb3436..fe57b75 100644
--- a/packages/SystemUI/res/values-or/strings.xml
+++ b/packages/SystemUI/res/values-or/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ଫେସ ଚିହ୍ନଟ ହୋଇପାରିବ ନାହିଁ"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ଟିପଚିହ୍ନ ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"ବ୍ଲୁଟୂଥ୍‍‌ ସଂଯୋଗ କରାଯାଇଛି।"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ବ୍ୟାଟେରୀ ଶତକଡ଼ା ଅଜଣା ଅଟେ।"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> ସହ ସଂଯୁକ୍ତ"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ଉଜ୍ଜ୍ୱଳତା"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ରଙ୍ଗ ଇନଭାର୍ସନ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ରଙ୍ଗ ସଂଶୋଧନ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ଉପଯୋଗକର୍ତ୍ତା ସେଟିଂସ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ୟୁଜରମାନଙ୍କୁ ପରିଚାଳନା କରନ୍ତୁ"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ହୋଇଗଲା"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ସଂଯୁକ୍ତ"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ମାଇକ୍ରୋଫୋନ ଉପଲବ୍ଧ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"କ୍ୟାମେରା ଉପଲବ୍ଧ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ମାଇକ୍ରୋଫୋନ ଏବଂ କ୍ୟାମେରା ଉପଲବ୍ଧ"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ଅନ୍ୟ ଡିଭାଇସ୍"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ସଂକ୍ଷିପ୍ତ ବିବରଣୀକୁ ଟୋଗଲ୍ କରନ୍ତୁ"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"ଆଲାର୍ମ, ରିମାଇଣ୍ଡର୍‌, ଇଭେଣ୍ଟ ଏବଂ ଆପଣ ନିର୍ଦ୍ଦିଷ୍ଟ କରିଥିବା କଲର୍‌ଙ୍କ ବ୍ୟତୀତ ଆପଣଙ୍କ ଧ୍ୟାନ ଅନ୍ୟ କୌଣସି ଧ୍ୱନୀ ଏବଂ ଭାଇବ୍ରେଶନ୍‌ରେ ଆକର୍ଷଣ କରାଯିବନାହିଁ। ମ୍ୟୁଜିକ୍‍, ଭିଡିଓ ଏବଂ ଗେମ୍‌ ସମେତ ନିଜେ ଚଲାଇବାକୁ ବାଛିଥିବା ଅନ୍ୟ ସବୁକିଛି ଆପଣ ଶୁଣିପାରିବେ।"</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ଆପଣ ସେୟାର, ରେକର୍ଡ କିମ୍ବା କାଷ୍ଟ କରିବା ସମୟରେ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ଆପରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ପ୍ଲେ ହେଉଥିବା ସବୁକିଛିକୁ ସେହି ଆପର ଆକ୍ସେସ ଅଛି। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ କିମ୍ବା ଅନ୍ୟ ସମ୍ବେଦନଶୀଳ ସୂଚନା ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ଜାରି ରଖନ୍ତୁ"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ଏକ ଆପକୁ ସେୟାର କିମ୍ବା ରେକର୍ଡ କରନ୍ତୁ"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ସମସ୍ତ ଖାଲି କରନ୍ତୁ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ପରିଚାଳନା କରନ୍ତୁ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ଇତିହାସ"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ମୋବାଇଲ୍‌ ଡାଟା ବନ୍ଦ କରିବେ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"ଡାଟା କିମ୍ବା ଇଣ୍ଟରନେଟ୍‌କୁ <xliff:g id="CARRIER">%s</xliff:g> ଦ୍ଵାରା ଆପଣଙ୍କର  ଆକ୍ସେସ୍ ରହିବ ନାହିଁ। ଇଣ୍ଟରନେଟ୍‌ କେବଳ ୱାଇ-ଫାଇ ମାଧ୍ୟମରେ ଉପଲବ୍ଧ ହେବ।"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ଆପଣଙ୍କ କେରିଅର୍"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g>କୁ ପୁଣି ସ୍ୱିଚ କରିବେ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ଉପଲବ୍ଧତା ଆଧାରରେ ମୋବାଇଲ ଡାଟା ସ୍ୱଚାଳିତ ଭାବେ ସ୍ୱିଚ ହେବ ନାହିଁ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ନା, ଧନ୍ୟବାଦ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ହଁ, ସ୍ୱିଚ କରନ୍ତୁ"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ଗୋଟିଏ ଆପ୍‍ ଏକ ଅନୁମତି ଅନୁରୋଧକୁ ଦେଖିବାରେ ବାଧା ଦେଉଥିବାରୁ, ସେଟିଙ୍ଗ ଆପଣଙ୍କ ଉତ୍ତରକୁ ଯାଞ୍ଚ କରିପାରିବ ନାହିଁ।"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_2">%2$s</xliff:g> ସ୍ଲାଇସ୍‌କୁ ଦେଖାଇବା ପାଇଁ <xliff:g id="APP_0">%1$s</xliff:g>କୁ ଅନୁମତି ଦେବେ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ଏହା <xliff:g id="APP">%1$s</xliff:g>ରୁ ସୂଚନାକୁ ପଢ଼ିପାରିବ"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ମ୍ୟାଗ୍ନିଫାୟର ୱିଣ୍ଡୋର ସେଟିଂସ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ଆକ୍ସେସିବିଲିଟୀ ଫିଚର ଖୋଲିବାକୁ ଟାପ କରନ୍ତୁ। ସେଟିଂସରେ ଏହି ବଟନକୁ କଷ୍ଟମାଇଜ କର କିମ୍ବା ବଦଳାଅ।\n\n"<annotation id="link">"ସେଟିଂସ ଦେଖନ୍ତୁ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ବଟନକୁ ଅସ୍ଥାୟୀ ଭାବେ ଲୁଚାଇବା ପାଇଁ ଏହାକୁ ଗୋଟିଏ ଧାରକୁ ମୁଭ୍ କରନ୍ତୁ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ପୂର୍ବବତ୍ କରନ୍ତୁ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label}ଟି ସର୍ଟକଟକୁ କାଢ଼ି ଦିଆଯାଇଛି}other{#ଟି ସର୍ଟକଟକୁ କାଢ଼ି ଦିଆଯାଇଛି}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ଶୀର୍ଷ ବାମକୁ ମୁଭ୍ କରନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ଶୀର୍ଷ ଡାହାଣକୁ ମୁଭ୍ କରନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ନିମ୍ନ ବାମକୁ ମୁଭ୍ କରନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ନିମ୍ନ ଡାହାଣକୁ ମୁଭ୍ କରନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ଧାରକୁ ମୁଭ୍ କରି ଲୁଚାନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ଧାର ବାହାରକୁ ମୁଭ୍ କରି ଦେଖାନ୍ତୁ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"କାଢ଼ି ଦିଅନ୍ତୁ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ଟୋଗଲ୍ କରନ୍ତୁ"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ଡିଭାଇସ୍ ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକ"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ନିୟନ୍ତ୍ରଣଗୁଡ଼ିକୁ ଯୋଗ କରିବାକୁ ଆପ୍ ବାଛନ୍ତୁ"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ମୋବାଇଲ ଡାଟା"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ସଂଯୋଗ କରାଯାଇଛି"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"ଅସ୍ଥାୟୀ ରୂପେ କନେକ୍ଟ କରାଯାଇଛି"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ଦୁର୍ବଳ କନେକ୍ସନ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ମୋବାଇଲ ଡାଟା ସ୍ୱତଃ-ସଂଯୋଗ ହେବ ନାହିଁ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ସଂଯୋଗ ନାହିଁ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ଅନ୍ୟ କୌଣସି ନେଟୱାର୍କ ଉପଲବ୍ଧ ନାହିଁ"</string>
diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml
index 992ffd6..c119e8c 100644
--- a/packages/SystemUI/res/values-pa/strings.xml
+++ b/packages/SystemUI/res/values-pa/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ਚਿਹਰੇ ਦੀ ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ਇਸਦੀ ਬਜਾਏ ਫਿੰਗਰਪ੍ਰਿੰਟ ਵਰਤੋ"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth ਕਨੈਕਟ ਕੀਤੀ।"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ਬੈਟਰੀ ਪ੍ਰਤੀਸ਼ਤ ਅਗਿਆਤ ਹੈ।"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> ਨਾਲ ਕਨੈਕਟ ਕੀਤਾ।"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ਚਮਕ"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"ਰੰਗ ਪਲਟਨਾ"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"ਰੰਗ ਸੁਧਾਈ"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"ਵਰਤੋਂਕਾਰ ਸੈਟਿੰਗਾਂ"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"ਵਰਤੋਂਕਾਰਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ਹੋ ਗਿਆ"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ਬੰਦ ਕਰੋ"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"ਕਨੈਕਟ ਕੀਤਾ"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਉਪਲਬਧ ਹੈ"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ਕੈਮਰਾ ਉਪਲਬਧ ਹੈ"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਅਤੇ ਕੈਮਰਾ ਉਪਲਬਧ ਹੈ"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਚਾਲੂ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਬੰਦ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"ਸਾਰੀਆਂ ਐਪਾਂ ਅਤੇ ਸੇਵਾਵਾਂ ਲਈ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਚਾਲੂ ਹੈ।"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"ਸਾਰੀਆਂ ਐਪਾਂ ਅਤੇ ਸੇਵਾਵਾਂ ਲਈ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਪਹੁੰਚ ਬੰਦ ਹੈ। ਤੁਸੀਂ ਸੈਟਿੰਗਾਂ &gt; ਪਰਦੇਦਾਰੀ &gt; ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਵਿੱਚ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਪਹੁੰਚ ਨੂੰ ਚਾਲੂ ਕਰ ਸਕਦੇ ਹੋ।"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"ਸਾਰੀਆਂ ਐਪਾਂ ਅਤੇ ਸੇਵਾਵਾਂ ਲਈ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਪਹੁੰਚ ਬੰਦ ਹੈ। ਤੁਸੀਂ ਸੈਟਿੰਗਾਂ &gt; ਪਰਦੇਦਾਰੀ &gt; ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਵਿੱਚ ਇਸਨੂੰ ਬਦਲ ਸਕਦੇ ਹੋ।"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"ਕੈਮਰਾ ਚਾਲੂ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ਕੈਮਰਾ ਬੰਦ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"ਸਾਰੀਆਂ ਐਪਾਂ ਅਤੇ ਸੇਵਾਵਾਂ ਲਈ ਕੈਮਰਾ ਚਾਲੂ ਹੈ।"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"ਸਾਰੀਆਂ ਐਪਾਂ ਅਤੇ ਸੇਵਾਵਾਂ ਲਈ ਕੈਮਰਾ ਪਹੁੰਚ ਬੰਦ ਹੈ।"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਬਟਨ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਪਹੁੰਚ ਚਾਲੂ ਕਰੋ।"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"ਸੈਟਿੰਗਾਂ ਖੋਲ੍ਹੋ।"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ਹੋਰ ਡੀਵਾਈਸ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"ਰੂਪ-ਰੇਖਾ ਨੂੰ ਟੌਗਲ ਕਰੋ"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"ਧੁਨੀਆਂ ਅਤੇ ਥਰਥਰਾਹਟਾਂ ਤੁਹਾਨੂੰ ਪਰੇਸ਼ਾਨ ਨਹੀਂ ਕਰਨਗੀਆਂ, ਸਿਵਾਏ ਅਲਾਰਮਾਂ, ਯਾਦ-ਦਹਾਨੀਆਂ, ਵਰਤਾਰਿਆਂ, ਅਤੇ ਤੁਹਾਡੇ ਵੱਲੋਂ ਨਿਰਧਾਰਤ ਕੀਤੇ ਕਾਲਰਾਂ ਦੀ ਸੂਰਤ ਵਿੱਚ। ਤੁਸੀਂ ਅਜੇ ਵੀ ਸੰਗੀਤ, ਵੀਡੀਓ ਅਤੇ ਗੇਮਾਂ ਸਮੇਤ ਆਪਣੀ ਚੋਣ ਅਨੁਸਾਰ ਕੁਝ ਵੀ ਸੁਣ ਸਕਦੇ ਹੋ।"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ਤੁਹਾਡੇ ਵੱਲੋਂ ਸਾਂਝਾ ਕਰਨ, ਰਿਕਾਰਡ ਕਰਨ, ਜਾਂ ਕਾਸਟ ਕਰਨ \'ਤੇ, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ਕੋਲ ਉਸ ਐਪ \'ਤੇ ਦਿਖਾਈ ਗਈ ਜਾਂ ਚਲਾਈ ਗਈ ਹਰੇਕ ਚੀਜ਼ ਤੱਕ ਪਹੁੰਚ ਹੁੰਦੀ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ ਜਾਂ ਹੋਰ ਸੰਵੇਦਨਸ਼ੀਲ ਜਾਣਕਾਰੀ ਸੰਬੰਧੀ ਸਾਵਧਾਨ ਰਹੋ।"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ਜਾਰੀ ਰੱਖੋ"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ਐਪ ਨੂੰ ਸਾਂਝਾ ਕਰੋ ਜਾਂ ਰਿਕਾਰਡ ਕਰੋ"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ਸਭ ਕਲੀਅਰ ਕਰੋ"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ਇਤਿਹਾਸ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ਕੀ ਮੋਬਾਈਲ ਡਾਟਾ ਬੰਦ ਕਰਨਾ ਹੈ?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"ਤੁਸੀਂ <xliff:g id="CARRIER">%s</xliff:g> ਰਾਹੀਂ ਡਾਟੇ ਜਾਂ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਕਰ ਸਕੋਗੇ। ਇੰਟਰਨੈੱਟ ਸਿਰਫ਼ ਵਾਈ-ਫਾਈ ਰਾਹੀਂ ਉਪਲਬਧ ਹੋਵੇਗਾ।"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ਤੁਹਾਡਾ ਕੈਰੀਅਰ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"ਕੀ ਵਾਪਸ <xliff:g id="CARRIER">%s</xliff:g> \'ਤੇ ਸਵਿੱਚ ਕਰਨਾ ਹੈ?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ਮੋਬਾਈਲ ਡਾਟਾ ਉਪਲਬਧਤਾ ਦੇ ਆਧਾਰ \'ਤੇ ਸਵੈਚਲਿਤ ਤੌਰ \'ਤੇ ਸਵਿੱਚ ਨਹੀਂ ਹੋਵੇਗਾ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ਨਹੀਂ ਧੰਨਵਾਦ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ਹਾਂ, ਸਵਿੱਚ ਕਰੋ"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"ਕਿਸੇ ਐਪ ਵੱਲੋਂ ਇਜਾਜ਼ਤ ਬੇਨਤੀ ਨੂੰ ਢਕੇ ਜਾਣ ਕਾਰਨ ਸੈਟਿੰਗਾਂ ਤੁਹਾਡੇ ਜਵਾਬ ਦੀ ਪੁਸ਼ਟੀ ਨਹੀਂ ਕਰ ਸਕਦੀਆਂ।"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"ਕੀ <xliff:g id="APP_0">%1$s</xliff:g> ਨੂੰ <xliff:g id="APP_2">%2$s</xliff:g> ਦੇ ਹਿੱਸੇ ਦਿਖਾਉਣ ਦੇਣੇ ਹਨ?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ਇਹ <xliff:g id="APP">%1$s</xliff:g> ਵਿੱਚੋਂ ਜਾਣਕਾਰੀ ਪੜ੍ਹ ਸਕਦਾ ਹੈ"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"ਵੱਡਦਰਸ਼ੀ ਵਿੰਡੋ ਸੈਟਿੰਗਾਂ"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ਪਹੁੰਚਯੋਗਤਾ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ। ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਇਹ ਬਟਨ ਵਿਉਂਤਬੱਧ ਕਰੋ ਜਾਂ ਬਦਲੋ।\n\n"<annotation id="link">"ਸੈਟਿੰਗਾਂ ਦੇਖੋ"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ਬਟਨ ਨੂੰ ਅਸਥਾਈ ਤੌਰ \'ਤੇ ਲੁਕਾਉਣ ਲਈ ਕਿਨਾਰੇ \'ਤੇ ਲਿਜਾਓ"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"ਅਣਕੀਤਾ ਕਰੋ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} ਸ਼ਾਰਟਕੱਟ ਨੂੰ ਹਟਾਇਆ ਗਿਆ}one{# ਸ਼ਾਰਟਕੱਟ ਨੂੰ ਹਟਾਇਆ ਗਿਆ}other{# ਸ਼ਾਰਟਕੱਟਾਂ ਨੂੰ ਹਟਾਇਆ ਗਿਆ}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ਉੱਪਰ ਵੱਲ ਖੱਬੇ ਲਿਜਾਓ"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ਉੱਪਰ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ਹੇਠਾਂ ਵੱਲ ਖੱਬੇ ਲਿਜਾਓ"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ਹੇਠਾਂ ਵੱਲ ਸੱਜੇ ਲਿਜਾਓ"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ਕਿਨਾਰੇ ਵਿੱਚ ਲਿਜਾ ਕੇ ਲੁਕਾਓ"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ਕਿਨਾਰੇ ਤੋਂ ਬਾਹਰ ਕੱਢ ਕੇ ਦਿਖਾਓ"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ਹਟਾਓ"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ਟੌਗਲ ਕਰੋ"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ਡੀਵਾਈਸ ਕੰਟਰੋਲ"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"ਕੰਟਰੋਲ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਐਪ ਚੁਣੋ"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ਮੋਬਾਈਲ ਡਾਟਾ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"ਕਨੈਕਟ ਹੈ"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"ਕੁਝ ਸਮੇਂ ਲਈ ਕਨੈਕਟ ਹੈ"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"ਖਰਾਬ ਕਨੈਕਸ਼ਨ"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ਮੋਬਾਈਲ ਡਾਟਾ ਸਵੈ-ਕਨੈਕਟ ਨਹੀਂ ਹੋਵੇਗਾ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ਕੋਈ ਕਨੈਕਸ਼ਨ ਨਹੀਂ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ਕੋਈ ਹੋਰ ਨੈੱਟਵਰਕ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml
index b706359..cb496fe 100644
--- a/packages/SystemUI/res/values-pl/strings.xml
+++ b/packages/SystemUI/res/values-pl/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Nie można rozpoznać twarzy"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Użyj odcisku palca"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth połączony."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Poziom naładowania baterii jest nieznany."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Połączono z <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Jasność"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Odwrócenie kolorów"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Korekcja kolorów"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Ustawienia użytkownika"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Zarządzaj użytkownikami"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Gotowe"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zamknij"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Połączono"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon jest dostępny"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Aparat jest dostępny"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon i aparat są dostępne"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Inne urządzenie"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Przełącz Przegląd"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Nie będą Cię niepokoić żadne dźwięki ani wibracje z wyjątkiem alarmów, przypomnień, wydarzeń i połączeń od wybranych osób. Będziesz słyszeć wszystkie odtwarzane treści, takie jak muzyka, filmy czy gry."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Podczas udostępniania, nagrywania lub przesyłania treści aplikacja <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ma dostęp do wszystkiego, co jest w niej wyświetlane lub odtwarzane. Zachowaj ostrożność w przypadku haseł, danych do płatności, wiadomości i innych informacji poufnych."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Dalej"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Udostępnianie i nagrywanie za pomocą aplikacji"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Usuń wszystkie"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Zarządzaj"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Wyłączyć mobilną transmisję danych?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nie będziesz mieć dostępu do transmisji danych ani internetu w <xliff:g id="CARRIER">%s</xliff:g>. Internet będzie dostępny tylko przez Wi‑Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"Twój operator"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Wrócić do operatora <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilna transmisja danych nie będzie automatycznie przełączana na podstawie dostępności"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nie, dziękuję"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Tak, wróć"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Aplikacja Ustawienia nie może zweryfikować Twojej odpowiedzi, ponieważ inna aplikacja zasłania prośbę o udzielenie uprawnień."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Zezwolić aplikacji <xliff:g id="APP_0">%1$s</xliff:g> na pokazywanie wycinków z aplikacji <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Może odczytywać informacje z aplikacji <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Ustawienia okna powiększania"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Kliknij, aby otworzyć ułatwienia dostępu. Dostosuj lub zmień ten przycisk w Ustawieniach.\n\n"<annotation id="link">"Wyświetl ustawienia"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Przesuń przycisk do krawędzi, aby ukryć go tymczasowo"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Cofnij"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} skrót został usunięty}few{# skróty zostały usunięte}many{# skrótów zostało usuniętych}other{# skrótu został usunięte}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Przenieś w lewy górny róg"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Przenieś w prawy górny róg"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Przenieś w lewy dolny róg"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Przenieś w prawy dolny róg"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Przenieś do krawędzi i ukryj"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Przenieś poza krawędź i pokaż"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Usuń"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"przełącz"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Sterowanie urządzeniami"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Wybierz aplikację, do której chcesz dodać elementy sterujące"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilna transmisja danych"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Połączono"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tymczasowe połączenie"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Słabe połączenie"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobilna transmisja danych nie połączy się automatycznie"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Brak połączenia"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Brak innych dostępnych sieci"</string>
diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml
index 78a6409..cbc8fc7a 100644
--- a/packages/SystemUI/res/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res/values-pt-rBR/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Rosto não reconhecido"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use a impressão digital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth conectado."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Porcentagem da bateria desconhecida."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectado a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brilho"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversão de cores"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correção de cor"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Config. do usuário"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gerenciar usuários"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Concluído"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Fechar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfone disponível"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Câmera disponível"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microfone e câmera disponíveis"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfone ativado"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfone desativado"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"O microfone está ativado para todos os apps e serviços."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"O acesso ao microfone está desativado para todos os apps e serviços. Ative em \"Configurações &gt; Privacidade &gt; Microfone\"."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"O acesso ao microfone está desativado para todos os apps e serviços. Você pode mudar isso em \"Configurações &gt; Privacidade &gt; Microfone\"."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Câmera ativada"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Câmera desativada"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"A câmera está ativada para todos os apps e serviços."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"O acesso à câmera está desativado para todos os apps e serviços."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Ative o acesso ao microfone nas Configurações para usar o botão dele."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir configurações."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Outro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Alternar Visão geral"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Você não será perturbado por sons e vibrações, exceto alarmes, lembretes, eventos e chamadas de pessoas especificadas. No entanto, você ouvirá tudo o que decidir reproduzir, como músicas, vídeos e jogos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quando você compartilha, grava ou transmite um app, o <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tem acesso a todas as informações visíveis na tela ou reproduzidas no dispositivo. Tenha cuidado com senhas, detalhes de pagamento, mensagens ou outras informações sensíveis."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartilhar ou gravar um app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Limpar tudo"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gerenciar"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Histórico"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Desativar os dados móveis?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Você não terá acesso a dados ou à Internet pela operadora <xliff:g id="CARRIER">%s</xliff:g>. A Internet só estará disponível via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"sua operadora"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Voltar para a operadora <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"A conexão de dados móveis não vai ser alternada automaticamente de acordo com a disponibilidade"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Agora não"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sim, voltar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Como um app está ocultando uma solicitação de permissão, as configurações não podem verificar sua resposta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Permitir que <xliff:g id="APP_0">%1$s</xliff:g> mostre partes do app <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Pode ler informações do app <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configurações da janela de lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toque para abrir os recursos de acessibilidade. Personalize ou substitua o botão nas Configurações.\n\n"<annotation id="link">"Ver configurações"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mova o botão para a borda para ocultá-lo temporariamente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Desfazer"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} atalho removido}one{# atalho removido}many{# de atalhos removidos}other{# atalhos removidos}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover para o canto superior esquerdo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover para o canto superior direito"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover para o canto inferior esquerdo"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover para o canto inferior direito"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover para a borda e ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mover para fora da borda e exibir"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remover"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"alternar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controles do dispositivo"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Escolha um app para adicionar controles"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dados móveis"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conectado"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporariamente conectado"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Conexão fraca"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Sem conexão automática com dados móveis"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sem conexão"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nenhuma outra rede disponível"</string>
diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml
index 30233b3..d37ac01 100644
--- a/packages/SystemUI/res/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res/values-pt-rPT/strings.xml
@@ -143,7 +143,7 @@
     <string name="biometric_dialog_tap_confirm_with_face_alt_2" msgid="8586608186457385108">"Rosto reconhecido. Prima para continuar."</string>
     <string name="biometric_dialog_tap_confirm_with_face_alt_3" msgid="2192670471930606539">"Rosto reconhecido. Prima ícone de desbloqueio para continuar"</string>
     <string name="biometric_dialog_authenticated" msgid="7337147327545272484">"Autenticado"</string>
-    <string name="biometric_dialog_use_pin" msgid="8385294115283000709">"Utilizar PIN"</string>
+    <string name="biometric_dialog_use_pin" msgid="8385294115283000709">"Usar PIN"</string>
     <string name="biometric_dialog_use_pattern" msgid="2315593393167211194">"Utilizar padrão"</string>
     <string name="biometric_dialog_use_password" msgid="3445033859393474779">"Utilizar palavra-passe"</string>
     <string name="biometric_dialog_wrong_pin" msgid="1878539073972762803">"PIN incorreto."</string>
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Imposs. reconhecer rosto"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Usar impressão digital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth ligado."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Percentagem da bateria desconhecida."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Ligado a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brilho"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversão de cores"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correção da cor"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Definições do utilizador"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gerir utilizadores"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Concluído"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Fechar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Ligado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfone disponível"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Câmara disponível"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microfone e câmara disponíveis"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfone ativado"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfone desativado"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"O microfone está ativado para todas as apps e serviços."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"O acesso ao microfone está desativado para todas as apps e serviços. Pode ativar o acesso ao microfone em Definições &gt; Privacidade &gt; Microfone."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"O acesso ao microfone está desativado para todas as apps e serviços. Pode alterar esta opção em Definições &gt; Privacidade &gt; Microfone."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Câmara ativada"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Câmara desativada"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"A câmara está ativada para todas as apps e serviços."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"O acesso à câmara está desativado para todas as apps e serviços."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Para usar o botão do microfone, ative o acesso do microfone nas Definições."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir definições."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Outro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Ativar/desativar Vista geral"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Não é incomodado por sons e vibrações, exceto de alarmes, lembretes, eventos e autores de chamadas que especificar. Continua a ouvir tudo o que optar por reproduzir, incluindo música, vídeos e jogos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quando está a partilhar, gravar ou transmitir uma app, a app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tem acesso a tudo o que é apresentado ou reproduzido nessa app. Por isso, tenha cuidado com palavras-passe, detalhes de pagamento, mensagens ou outras informações confidenciais."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Partilhe ou grave uma app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Limpar tudo"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gerir"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Histórico"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Desativar os dados móveis?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Não terá acesso a dados ou à Internet através do operador <xliff:g id="CARRIER">%s</xliff:g>. A Internet estará disponível apenas por Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"o seu operador"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Mudar de novo para <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Os dados móveis não vão mudar automaticamente com base na disponibilidade"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Não"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sim, mudar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Uma vez que uma app está a ocultar um pedido de autorização, as Definições não conseguem validar a sua resposta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Permitir que a app <xliff:g id="APP_0">%1$s</xliff:g> mostre partes da app <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Pode ler informações da app <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Definições da janela da lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toque para abrir funcionalidades de acessibilidade. Personal. ou substitua botão em Defin.\n\n"<annotation id="link">"Ver defin."</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mova o botão para a extremidade para o ocultar temporariamente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Anular"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} atalho removido}many{# atalhos removidos}other{# atalhos removidos}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover p/ parte sup. esquerda"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover parte superior direita"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover p/ parte infer. esquerda"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover parte inferior direita"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover p/ extremidade e ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Retirar extremidade e mostrar"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remover"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ativar/desativar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controlos de dispositivos"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Escolha uma app para adicionar controlos"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dados móveis"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ligado"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Ligado temporariamente"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Ligação fraca"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Sem ligação automática com dados móveis"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sem ligação"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nenhuma outra rede disponível"</string>
diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml
index 78a6409..cbc8fc7a 100644
--- a/packages/SystemUI/res/values-pt/strings.xml
+++ b/packages/SystemUI/res/values-pt/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Rosto não reconhecido"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Use a impressão digital"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth conectado."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Porcentagem da bateria desconhecida."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectado a <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brilho"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversão de cores"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Correção de cor"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Config. do usuário"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gerenciar usuários"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Concluído"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Fechar"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectado"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfone disponível"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Câmera disponível"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microfone e câmera disponíveis"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfone ativado"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfone desativado"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"O microfone está ativado para todos os apps e serviços."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"O acesso ao microfone está desativado para todos os apps e serviços. Ative em \"Configurações &gt; Privacidade &gt; Microfone\"."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"O acesso ao microfone está desativado para todos os apps e serviços. Você pode mudar isso em \"Configurações &gt; Privacidade &gt; Microfone\"."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Câmera ativada"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Câmera desativada"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"A câmera está ativada para todos os apps e serviços."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"O acesso à câmera está desativado para todos os apps e serviços."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Ative o acesso ao microfone nas Configurações para usar o botão dele."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Abrir configurações."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Outro dispositivo"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Alternar Visão geral"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Você não será perturbado por sons e vibrações, exceto alarmes, lembretes, eventos e chamadas de pessoas especificadas. No entanto, você ouvirá tudo o que decidir reproduzir, como músicas, vídeos e jogos."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Quando você compartilha, grava ou transmite um app, o <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> tem acesso a todas as informações visíveis na tela ou reproduzidas no dispositivo. Tenha cuidado com senhas, detalhes de pagamento, mensagens ou outras informações sensíveis."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuar"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Compartilhar ou gravar um app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Limpar tudo"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gerenciar"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Histórico"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Desativar os dados móveis?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Você não terá acesso a dados ou à Internet pela operadora <xliff:g id="CARRIER">%s</xliff:g>. A Internet só estará disponível via Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"sua operadora"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Voltar para a operadora <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"A conexão de dados móveis não vai ser alternada automaticamente de acordo com a disponibilidade"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Agora não"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Sim, voltar"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Como um app está ocultando uma solicitação de permissão, as configurações não podem verificar sua resposta."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Permitir que <xliff:g id="APP_0">%1$s</xliff:g> mostre partes do app <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Pode ler informações do app <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Configurações da janela de lupa"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Toque para abrir os recursos de acessibilidade. Personalize ou substitua o botão nas Configurações.\n\n"<annotation id="link">"Ver configurações"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mova o botão para a borda para ocultá-lo temporariamente"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Desfazer"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} atalho removido}one{# atalho removido}many{# de atalhos removidos}other{# atalhos removidos}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mover para o canto superior esquerdo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mover para o canto superior direito"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mover para o canto inferior esquerdo"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mover para o canto inferior direito"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mover para a borda e ocultar"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mover para fora da borda e exibir"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Remover"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"alternar"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Controles do dispositivo"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Escolha um app para adicionar controles"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dados móveis"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conectado"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Temporariamente conectado"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Conexão fraca"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Sem conexão automática com dados móveis"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Sem conexão"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nenhuma outra rede disponível"</string>
diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml
index 0d96062..5667884 100644
--- a/packages/SystemUI/res/values-ro/strings.xml
+++ b/packages/SystemUI/res/values-ro/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Chip nerecunoscut"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Folosește amprenta"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Conectat prin Bluetooth."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Procentajul bateriei este necunoscut."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Conectat la <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Luminozitate"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inversarea culorilor"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Corecția culorii"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Setări de utilizator"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Gestionează utilizatorii"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Terminat"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Închide"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Conectat"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Microfon disponibil"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Cameră foto disponibilă"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Microfon și cameră disponibile"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Microfonul a fost activat"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Microfonul a fost dezactivat"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Microfonul este activat pentru toate aplicațiile și serviciile."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Accesul la microfon este dezactivat pentru toate aplicațiile și serviciile. Activează accesul la microfon în Setări &gt; Confidențialitate &gt; Microfon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Accesul la microfon este dezactivat pentru toate aplicațiile și serviciile. Modifică opțiunea în Setări &gt; Confidențialitate &gt; Microfon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Camera este activată"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Camera este dezactivată"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Camera este activată pentru toate aplicațiile și serviciile."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Accesul la cameră este dezactivat pentru toate aplicațiile și serviciile."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Pentru a folosi butonul microfonului, activează accesul la microfon în Setări."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Deschide setările."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Alt dispozitiv"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Comută secțiunea Recente"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Se vor anunța prin sunete și vibrații numai alarmele, mementourile, evenimentele și apelanții specificați de tine. Totuși, vei auzi tot ce alegi să redai, inclusiv muzică, videoclipuri și jocuri."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Când permiți accesul, înregistrezi sau proiectezi o aplicație, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> are acces la orice se afișează pe ecran sau se redă în aplicație. Ai grijă cu parolele, detaliile de plată, mesajele sau alte informații sensibile."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Continuă"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Permite accesul la o aplicație sau înregistreaz-o"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Șterge toate notificările"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Gestionează"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Istoric"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Dezactivezi datele mobile?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nu vei avea acces la date sau la internet prin intermediul <xliff:g id="CARRIER">%s</xliff:g>. Internetul va fi disponibil numai prin Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operatorul tău"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Revii la <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Comutarea la datele mobile nu se va face automat în funcție de disponibilitate"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nu, mulțumesc"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Da, fac trecerea"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Deoarece o aplicație acoperă o solicitare de permisiune, Setările nu îți pot verifica răspunsul."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Permiți ca <xliff:g id="APP_0">%1$s</xliff:g> să afișeze porțiuni din <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Poate citi informații din <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Setările ferestrei de mărire"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Atinge ca să deschizi funcțiile de accesibilitate. Personalizează sau înlocuiește butonul în setări.\n\n"<annotation id="link">"Vezi setările"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Mută butonul spre margine pentru a-l ascunde temporar"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Anulează"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} comandă rapidă eliminată}few{# comenzi rapide eliminate}other{# de comenzi rapide eliminate}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Mută în stânga sus"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Mută în dreapta sus"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Mută în stânga jos"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Mută în dreapta jos"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Mută la margine și ascunde"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Mută de la margine și afișează"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Elimină"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"Activează / dezactivează"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Comenzile dispozitivelor"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Alege aplicația pentru a adăuga comenzi"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Date mobile"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Conectat"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Conectat temporar"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Conexiune slabă"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Nu se conectează automat la date mobile"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nicio conexiune"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nu sunt disponibile alte rețele"</string>
diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml
index 3d13b2c..3b820ec 100644
--- a/packages/SystemUI/res/values-ru/strings.xml
+++ b/packages/SystemUI/res/values-ru/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Лицо не распознано."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Используйте отпечаток."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth-соединение установлено."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Уровень заряда батареи в процентах неизвестен."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>: подключено."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Яркость"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Инверсия цветов"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Коррекция цвета"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Пользовательские настройки"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Управление пользователями"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Готово"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Закрыть"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Подключено"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофон готов."</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камера готова."</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофон и камера готовы."</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Микрофон включен"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Микрофон отключен"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Микрофон включен для всех приложений и сервисов."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Доступ к микрофону отключен для всех приложений и сервисов. Вы можете разрешить доступ к микрофону, перейдя в Настройки &gt; Конфиденциальность &gt; Микрофон."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Доступ к микрофону отключен для всех приложений и сервисов. Вы можете изменить этот параметр, перейдя в Настройки &gt; Конфиденциальность &gt; Микрофон."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камера включена"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камера отключена"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камера включена для всех приложений и сервисов."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Доступ к камере отключен для всех приложений и сервисов."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Чтобы использовать кнопку микрофона, разрешите доступ к микрофону в настройках."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Открыть настройки"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Другое устройство"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Переключить режим обзора"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Вас не будут отвлекать звуки и вибрация, за исключением сигналов будильника, напоминаний, уведомлений о мероприятиях и звонков от помеченных контактов. Вы по-прежнему будете слышать включенную вами музыку, видео, игры и т. д."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Когда вы демонстрируете, транслируете экран или записываете видео с него, приложение \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" получает доступ ко всему, что видно и воспроизводится на экране устройства. Помните об этом, если соберетесь вводить или просматривать пароли, платежные данные, сообщения и другую конфиденциальную информацию."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Далее"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Демонстрация экрана или запись видео с него"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Очистить все"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Настроить"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"История"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Отключить мобильный Интернет?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Вы не сможете передавать данные или выходить в Интернет через оператора \"<xliff:g id="CARRIER">%s</xliff:g>\". Интернет будет доступен только по сети Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ваш оператор"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Переключиться на сеть \"<xliff:g id="CARRIER">%s</xliff:g>\"?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мобильный интернет не будет переключаться автоматически."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Нет"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Да"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Невозможно принять ваше согласие, поскольку запрос скрыт другим приложением."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Разрешить приложению \"<xliff:g id="APP_0">%1$s</xliff:g>\" показывать фрагменты приложения \"<xliff:g id="APP_2">%2$s</xliff:g>\"?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Ему станут доступны данные из приложения \"<xliff:g id="APP">%1$s</xliff:g>\"."</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Настройка окна лупы"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Нажмите, чтобы открыть спец. возможности. Настройте или замените эту кнопку в настройках.\n\n"<annotation id="link">"Настройки"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Чтобы временно скрыть кнопку, переместите ее к краю экрана"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Отменить"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} сочетание клавиш удалено}one{# сочетание клавиш удалено}few{# сочетания клавиш удалено}many{# сочетаний клавиш удалено}other{# сочетания клавиш удалено}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Перенести в левый верхний угол"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Перенести в правый верхний угол"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Перенести в левый нижний угол"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Перенести в правый нижний угол"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Перенести к краю и скрыть"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Вернуть из-за края и показать"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Убрать"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"включить или отключить"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Управление устройствами"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Чтобы добавить виджеты управления, выберите приложение"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобильный интернет"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Подключено"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Временное подключение"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Слабый сигнал"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Без автоподключения к мобильному интернету"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Нет подключения к интернету"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Нет других доступных сетей"</string>
diff --git a/packages/SystemUI/res/values-si/strings.xml b/packages/SystemUI/res/values-si/strings.xml
index 01be742..5696980 100644
--- a/packages/SystemUI/res/values-si/strings.xml
+++ b/packages/SystemUI/res/values-si/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"මුහුණ හඳුනා ගත නොහැක"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ඒ වෙනුවට ඇඟිලි සලකුණ භාවිත කරන්න"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"බ්ලූටූත් සම්බන්ධිතයි."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"බැටරි ප්‍රතිශතය නොදනී."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> වෙත සම්බන්ධ කරන ලදි."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"දීප්තිමත් බව"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"වර්ණ අපවර්තනය"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"වර්ණ නිවැරදි කිරීම"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"පරිශීලක සැකසීම්"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"පරිශීලකයන් කළමනාකරණය කරන්න"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"නිමයි"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"වසන්න"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"සම්බන්ධිත"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"මයික්‍රෆෝනය ලබා ගත හැකිය"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"කැමරාව ලබා ගත හැකිය"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"මයික්‍රෆෝනය සහ කැමරාව ලබා ගත හැකිය"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"වෙනත් උපාංගය"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"දළ විශ්ලේෂණය ටොගල කරන්න"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"එලාම සිහිකැඳවීම්, සිදුවීම්, සහ ඔබ සඳහන් කළ අමතන්නන් හැර, ශබ්ද සහ කම්පනවලින් ඔබට බාධා නොවනු ඇත. සංගීතය, වීඩියෝ, සහ ක්‍රීඩා ඇතුළු ඔබ වාදනය කිරීමට තෝරන ලද සියලු දේ ඔබට තවම ඇසෙනු ඇත."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ඔබ යෙදුමක් බෙදා ගන්නා විට, පටිගත කරන විට හෝ විකාශය කරන විට, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> හට එම යෙදුමේ පෙන්වන හෝ වාදනය කරන ඕනෑම දෙයකට ප්‍රවේශය ඇත. එබැවින් මුරපද, ගෙවීම් විස්තර, පණිවිඩ හෝ වෙනත් සංවේදී තොරතුරු සමග ප්‍රවේශම් වන්න."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ඉදිරියට යන්න"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"යෙදුමක් බෙදා ගන්න හෝ පටිගත කරන්න"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"සියල්ල හිස් කරන්න"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"කළමනාකරණය කරන්න"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ඉතිහාසය"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ජංගම දත්ත ක්‍රියාවිරහිත කරන්නද?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"ඔබට <xliff:g id="CARRIER">%s</xliff:g> හරහා දත්ත හෝ අන්තර්ජාලයට පිවිසීමේ හැකියාවක් නැත. අන්තර්ජාලය Wi-Fi හරහා පමණක් ලබා ගත හැකිය."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ඔබගේ වාහකය"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> වෙත ආපසු මාරු කරන්නද?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"ජංගම දත්ත ලබා ගත හැකි වීමට අනුව ස්වයංක්‍රීයව මාරු නොවෙයි"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"එපා ස්තුතියි"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ඔව්, මාරු කරන්න"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"යෙදුමක් අවසර ඉල්ලීමක් කරන නිසා, සැකසීම්වලට ඔබගේ ප්‍රතිචාරය සත්‍යාපනය කළ නොහැකිය."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> හට කොටස් <xliff:g id="APP_2">%2$s</xliff:g>ක් පෙන්වීමට ඉඩ දෙන්නද?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- එයට <xliff:g id="APP">%1$s</xliff:g> වෙතින් තොරතුරු කියවිය හැකිය"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"විශාලන කවුළු සැකසීම්"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ප්‍රවේශ්‍යතා විශේෂාංග විවෘත කිරීමට තට්ටු කරන්න. සැකසීම් තුළ මෙම බොත්තම අභිරුචිකරණය හෝ ප්‍රතිස්ථාපනය කරන්න.\n\n"<annotation id="link">"සැකසීම් බලන්න"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"එය තාවකාලිකව සැඟවීමට බොත්තම දාරයට ගෙන යන්න"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"අස් කරන්න"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} කෙටිමඟ ඉවත් කළා}one{කෙටිමං # ක් ඉවත් කළා}other{කෙටිමං # ක් ඉවත් කළා}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ඉහළ වමට ගෙන යන්න"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ඉහළ දකුණට ගෙන යන්න"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"පහළ වමට ගෙන යන්න"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"පහළ දකුණට ගෙන යන්න"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"මායිමට ගෙන යන්න සහ සඟවන්න"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"මායිමෙන් පිටට ගන්න සහ පෙන්වන්න"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ඉවත් කරන්න"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ටොගල් කරන්න"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"උපාංග පාලන"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"පාලන එක් කිරීමට යෙදුම තෝරා ගන්න"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"ජංගම දත්ත"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"සම්බන්ධයි"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"තාවකාලිකව සම්බන්ධ කළා"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"දුර්වල සම්බන්ධතාව"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"ජංගම දත්ත ස්වංක්‍රියව සම්බන්ධ නොවනු ඇත"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"සම්බන්ධතාවයක් නැත"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ලබා ගත හැකි වෙනත් ජාල නැත"</string>
diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml
index 76dce74..6aaafc1 100644
--- a/packages/SystemUI/res/values-sk/strings.xml
+++ b/packages/SystemUI/res/values-sk/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Tvár sa nedá rozpoznať"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Používať radšej odtlačok"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth pripojené."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Percento batérie nie je známe."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Pripojené k zariadeniu <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Jas"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inverzia farieb"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Úprava farieb"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Používateľské nastavenia"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Spravovať používateľov"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Hotovo"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zavrieť"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Pripojené"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofón je k dispozícii"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera je k dispozícii"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofón a kamera sú k dispozícii"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofón je zapnutý"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofón je vypnutý"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofón je povolený pre všetky aplikácie a služby."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Prístup k mikrofónu bol zakázaný pre všetky aplikácie a služby. Môžete ho povoliť v sekcii Nastavenia &gt; Ochrana súkromia &gt; Mikrofón."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Prístup k mikrofónu bol zakázaný pre všetky aplikácie a služby. Môžete to zmeniť v sekcii Nastavenia &gt; Ochrana súkromia &gt; Mikrofón."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera je zapnutá"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera je vypnutá"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera je povolená pre všetky aplikácie a služby."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Prístup ku kamere je zakázaný pre všetky aplikácie a služby."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Ak chcete použiť tlačidlo mikrofónu, povoľte prístup k mikrofónu v Nastaveniach."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Otvoriť nastavenia"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Iné zariadenie"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Prepnúť prehľad"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Nebudú vás vyrušovať zvuky ani vibrácie, iba budíky, pripomenutia, udalosti a volajúci, ktorých určíte. Budete naďalej počuť všetko, čo sa rozhodnete prehrať, ako napríklad hudbu, videá a hry."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Počas zdieľania, nahrávania alebo prenosu bude mať aplikácia <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> prístup k všetkému obsahu, ktorý sa v nej bude zobrazovať alebo prehrávať. Preto venujte zvýšenú pozornosť heslám, platobným údajom, správam a ďalším citlivým údajom."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Pokračovať"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Aplikácia na zdieľanie alebo nahrávanie"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Vymazať všetko"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Spravovať"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"História"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Chcete vypnúť mobilné dáta?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nebudete mať prístup k dátam ani internetu prostredníctvom operátora <xliff:g id="CARRIER">%s</xliff:g>. Internet bude k dispozícii iba cez Wi‑Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"váš operátor"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Chcete prepnúť späť na operátora <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobilné dáta sa nebudú automaticky prepínať na základe dostupnosti"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nie, vďaka"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Áno, prepnúť"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Nastavenia nemôžu overiť vašu odpoveď, pretože určitá aplikácia blokuje žiadosť o povolenie."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Povoliť aplikácii <xliff:g id="APP_0">%1$s</xliff:g> zobrazovať rezy z aplikácie <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Môže čítať informácie z aplikácie <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Nastavenia okna lupy"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Funkcie dostupnosti otvoríte klepnutím. Tlačidlo prispôsobte alebo nahraďte v Nastav.\n\n"<annotation id="link">"Zobraz. nast."</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Ak chcete tlačidlo dočasne skryť, presuňte ho k okraju"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Späť"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Bola odstránená skratka {label}}few{Boli odstránené # skratky}many{# shortcuts removed}other{Bolo odstránených # skratiek}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Presunúť doľava nahor"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Presunúť doprava nahor"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Presunúť doľava nadol"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Presunúť doprava nadol"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Presunúť k okraju a skryť"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Presunúť z okraja a zobraziť"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Odstrániť"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"prepínač"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Ovládanie zariadení"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Vyberte aplikáciu, ktorej ovládače si chcete pridať"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobilné dáta"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Pripojené"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Dočasne pripojené"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Slabé pripojenie"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Automatické pripojenie cez mobilné dáta nefunguje"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Bez pripojenia"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nie sú k dispozícii žiadne ďalšie siete"</string>
diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml
index cb306c2..36eead3 100644
--- a/packages/SystemUI/res/values-sl/strings.xml
+++ b/packages/SystemUI/res/values-sl/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Obraz ni bil prepoznan."</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Uporabite prstni odtis."</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Povezava Bluetooth vzpostavljena."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Neznan odstotek napolnjenosti baterije."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Povezava vzpostavljena z: <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Svetlost"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Inverzija barv"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Popravljanje barv"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Uporabniške nastavitve"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Upravljanje uporabnikov"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Končano"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Zapri"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Povezava je vzpostavljena"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon je na voljo"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera je na voljo"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon in kamera sta na voljo"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Druga naprava"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Vklop/izklop pregleda"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Ne bodo vas motili zvoki ali vibriranje, razen v primeru alarmov, opomnikov, dogodkov in klicateljev, ki jih določite. Še vedno pa boste slišali vse, kar se boste odločili predvajati, vključno z glasbo, videoposnetki in igrami."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Pri deljenju, snemanju ali predvajanju aplikacije ima aplikacija <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> dostop do vsega, kar je prikazano ali predvajano v tej aplikaciji, zato bodite previdni z gesli, podatki za plačilo, sporočili ali drugimi občutljivimi podatki."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Naprej"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Deljenje ali snemanje aplikacije"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Izbriši vse"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljaj"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Zgodovina"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Želite izklopiti prenos podatkov v mobilnih omrežjih?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Prek operaterja »<xliff:g id="CARRIER">%s</xliff:g>« ne boste imeli dostopa do podatkovne povezave ali interneta. Internet bo na voljo samo prek povezave Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"svojega operaterja"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Želite preklopiti nazaj na ponudnika <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Prenos podatkov v mobilnem omrežju ne preklopi samodejno glede na razpoložljivost."</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ne, hvala"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Da, preklopi"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Ker aplikacija zakriva zahtevo za dovoljenje, z nastavitvami ni mogoče preveriti vašega odziva."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Želite dovoliti, da aplikacija <xliff:g id="APP_0">%1$s</xliff:g> prikaže izreze aplikacije <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– lahko bere podatke v aplikaciji <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Nastavitve okna povečevalnika"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Dotaknite se za funkcije za ljudi s posebnimi potrebami. Ta gumb lahko prilagodite ali zamenjate v nastavitvah.\n\n"<annotation id="link">"Ogled nastavitev"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Če želite gumb začasno skriti, ga premaknite ob rob."</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Razveljavi"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Odstranjena bližnjica za fun. {label}}one{Odstranjena # bližnjica}two{Odstranjeni # bližnjici}few{Odstranjene # bližnjice}other{Odstranjenih # bližnjic}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Premakni zgoraj levo"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Premakni zgoraj desno"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Premakni spodaj levo"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Premakni spodaj desno"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Premakni na rob in skrij"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Premakni z roba in pokaži"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Odstrani"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"preklop"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrolniki naprave"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Izberite aplikacijo za dodajanje kontrolnikov"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Prenos podatkov v mobilnem omrežju"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Povezano"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Začasno vzpostavljena povezava"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Slaba povezava"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobilna podatkovna povezava ne bo samodejna."</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Ni povezave"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nobeno drugo omrežje ni na voljo"</string>
diff --git a/packages/SystemUI/res/values-sq/strings.xml b/packages/SystemUI/res/values-sq/strings.xml
index 9a6d73c..e73a322 100644
--- a/packages/SystemUI/res/values-sq/strings.xml
+++ b/packages/SystemUI/res/values-sq/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Fytyra nuk mund të njihet"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Përdor më mirë gjurmën e gishtit"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Pajisja është lidhur me \"bluetooth\"."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Përqindja e baterisë e panjohur."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Lidhur me <xliff:g id="BLUETOOTH">%s</xliff:g>"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Ndriçimi"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Anasjellja e ngjyrës"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Korrigjimi i ngjyrës"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Cilësimet e përdoruesit"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Menaxho përdoruesit"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"U krye"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Mbyll"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"I lidhur"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofoni ofrohet"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera ofrohet"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofoni dhe kamera ofrohen"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofoni u aktivizua"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofoni u çaktivizua"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofoni është aktivizuar për të gjitha aplikacionet dhe shërbimet."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Qasja te mikrofoni është çaktivizuar për të gjitha aplikacionet dhe shërbimet. Mund ta aktivizosh qasjen te mikrofoni te \"Cilësimet &gt; Privatësia &gt; Mikrofoni\"."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Qasja te mikrofoni është çaktivizuar për të gjitha aplikacionet dhe shërbimet. Këtë mund ta ndryshosh te \"Cilësimet &gt; Privatësia &gt; Mikrofoni\"."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera u aktivizua"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera u çaktivizua"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera është aktivizuar për të gjitha aplikacionet dhe shërbimet."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Qasja te kamera është çaktivizuar për të gjitha aplikacionet dhe shërbimet."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Për të përdorur butonin e mikrofonit, aktivizo qasjen te mikrofoni te \"Cilësimet\"."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Hap cilësimet."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Pajisje tjetër"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Kalo te përmbledhja"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Nuk do të shqetësohesh nga tingujt dhe dridhjet, përveç alarmeve, alarmeve rikujtuese, ngjarjeve dhe telefonuesve që specifikon. Do të vazhdosh të dëgjosh çdo gjë që zgjedh të luash duke përfshirë muzikën, videot dhe lojërat."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Gjatë shpërndarjes, regjistrimit ose transmetimit të një aplikacioni, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ka qasje te çdo gjë e dukshme në ekranin tënd ose që po luhet në atë aplikacion. Prandaj, ki kujdes me fjalëkalimet, detajet e pagesës, mesazhet ose informacione të tjera të ndjeshme."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Vazhdo"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Shpërndaj ose regjistro një aplikacion"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Pastroji të gjitha"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Menaxho"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historiku"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Të çaktivizohen të dhënat celulare?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Nuk do të kesh qasje te të dhënat ose interneti nëpërmjet <xliff:g id="CARRIER">%s</xliff:g>. Interneti do të ofrohet vetëm nëpërmjet Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operatori yt celular"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Të kalohet përsëri te <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Të dhënat celulare nuk do të ndërrohen automatikisht në bazë të disponueshmërisë"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Jo, faleminderit"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Po, ndërro"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Duke qenë se një aplikacion po bllokon një kërkesë për leje, \"Cilësimet\" nuk mund të verifikojnë përgjigjen tënde."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Të lejohet <xliff:g id="APP_0">%1$s</xliff:g> që të shfaqë pjesë të <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Mund të lexojë informacion nga <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Cilësimet e dritares së zmadhimit"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Trokit dhe hap veçoritë e qasshmërisë. Modifiko ose ndërro butonin te \"Cilësimet\".\n\n"<annotation id="link">"Shih cilësimet"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Zhvendose butonin në skaj për ta fshehur përkohësisht"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Zhbëj"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Shkurtorja {label} u hoq}other{# shkurtore u hoqën}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Zhvendos lart majtas"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Zhvendos lart djathtas"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Zhvendos poshtë majtas"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Zhvendos poshtë djathtas"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Zhvendose te skaji dhe fshihe"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Zhvendose jashtë skajit dhe shfaqe"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Hiq"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"aktivizo/çaktivizo"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Kontrollet e pajisjes"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Zgjidh aplikacionin për të shtuar kontrollet"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Të dhënat celulare"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Lidhur"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Lidhur përkohësisht"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Lidhje e dobët"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Të dhënat celulare nuk do të lidhen automatikisht"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Nuk ka lidhje"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Nuk ofrohet asnjë rrjet tjetër"</string>
diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml
index 0dff11f..6d0c201 100644
--- a/packages/SystemUI/res/values-sr/strings.xml
+++ b/packages/SystemUI/res/values-sr/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Лице није препознато"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Користите отисак прста"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth је прикључен."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Проценат напуњености батерије није познат."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Повезани сте са <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Осветљеност"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Инверзија боја"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Корекција боја"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Корисничка подешавања"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Управљаjте корисницима"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Готово"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Затвори"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Повезан"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Микрофон је доступан"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камера је доступна"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Микрофон и камера су доступни"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Други уређај"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Укључи/искључи преглед"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Неће вас узнемиравати звукови и вибрације осим за аларме, подсетнике, догађаје и позиваоце које наведете. И даље ћете чути све што одаберете да пустите, укључујући музику, видео снимке и игре."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Када делите, снимате или пребацујете апликацију, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> има приступ комплетном садржају који је видљив или се пушта у тој апликацији. Будите пажљиви са лозинкама, информацијама о плаћању, порукама или другим осетљивим информацијама."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Настави"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Делите или снимите апликацију"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Обриши све"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Управљајте"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Историја"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Желите да искључите мобилне податке?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Нећете имати приступ подацима или интернету преко мобилног оператера <xliff:g id="CARRIER">%s</xliff:g>. Интернет ће бити доступан само преко WiFi везе."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"мобилни оператер"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Желите да се вратите на мобилног оператера <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Мобилни подаци се неће аутоматски променити на основу доступности"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Не, хвала"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Да, пређи"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Подешавања не могу да верификују ваш одговор јер апликација скрива захтев за дозволу."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Желите ли да дозволите апликацији <xliff:g id="APP_0">%1$s</xliff:g> да приказује исечке из апликације <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Може да чита податке из апликације <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Подешавања прозора за увећање"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Додирните за функције приступачности. Прилагодите или замените ово дугме у Подешавањима.\n\n"<annotation id="link">"Подешавања"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Померите дугме до ивице да бисте га привремено сакрили"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Опозовите"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} пречица је уклоњена}one{# пречица је уклоњена}few{# пречице су уклоњене}other{# пречица је уклоњено}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Премести горе лево"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Премести горе десно"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Премести доле лево"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Премести доле десно"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Премести до ивице и сакриј"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Премести изван ивице и прикажи"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Уклоните"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"укључите/искључите"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Контроле уређаја"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Одаберите апликацију за додавање контрола"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобилни подаци"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Повезано"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Привремено повезано"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Веза је лоша"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Није успело аутом. повезивање преко моб. података"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Веза није успостављена"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Није доступна ниједна друга мрежа"</string>
diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml
index c1cdc5b..2eac10f 100644
--- a/packages/SystemUI/res/values-sv/strings.xml
+++ b/packages/SystemUI/res/values-sv/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Ansiktet kändes inte igen"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Använd fingeravtryck"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth ansluten."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Okänd batterinivå."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Ansluten till <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Ljusstyrka"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Färginvertering"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Färgkorrigering"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Användarinställningar"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Hantera användare"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Klart"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Stäng"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Ansluten"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofonen kan användas"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kameran kan användas"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofonen och kameran kan användas"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofonen sattes på"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofonen stängdes av"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofonen är aktiverad för alla appar och tjänster."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofonåtkomst är inaktiverad för alla appar och tjänster. Du kan aktivera mikrofonåtkomst i Inställningar &gt; Integritet &gt; Mikrofon."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofonåtkomst är inaktiverad för alla appar och tjänster. Du kan ändra detta i Inställningar &gt; Integritet &gt; Mikrofon."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kameran sattes på"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kameran stängdes av"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kameran är aktiverad för alla appar och tjänster."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kameraåtkomst är inaktiverad för alla appar och tjänster."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Aktivera mikrofonåtkomst i inställningarna om du vill använda mikrofonknappen."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Öppna inställningarna."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Annan enhet"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Aktivera och inaktivera översikten"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Du blir inte störd av ljud och vibrationer, förutom från alarm, påminnelser, händelser och specifika samtal. Ljudet är fortfarande på för sådant du väljer att spela upp, till exempel musik, videor och spel."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"När du delar, spelar in eller castar en app har <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> åtkomst till allt som visas eller spelas upp i appen. Så var försiktig med lösenord, betalningsuppgifter, meddelanden och andra känsliga uppgifter."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Fortsätt"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Dela eller spela in en app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Rensa alla"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Hantera"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historik"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vill du inaktivera mobildata?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Du kan inte skicka data eller använda internet via <xliff:g id="CARRIER">%s</xliff:g>. Internetanslutning blir bara möjlig via wifi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"din operatör"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Vill du byta tillbaka till <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobildatakällan byts inte automatiskt efter tillgänglighet"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Nej tack"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ja, byt"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Svaret kan inte verifieras av Inställningar eftersom en app skymmer en begäran om behörighet."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Tillåter du att bitar av <xliff:g id="APP_2">%2$s</xliff:g> visas i <xliff:g id="APP_0">%1$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– Kan läsa information från <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Inställningar för förstoringsfönster"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Tryck för att öppna tillgänglighetsfunktioner. Anpassa/ersätt knappen i Inställningar.\n\n"<annotation id="link">"Inställningar"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Flytta knappen till kanten för att dölja den tillfälligt"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Ångra"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} genväg har tagits bort}other{# genvägar har tagits bort}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Flytta högst upp till vänster"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Flytta högst upp till höger"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Flytta längst ned till vänster"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Flytta längst ned till höger"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Flytta till kanten och dölj"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Flytta från kanten och visa"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Ta bort"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"aktivera och inaktivera"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Enhetsstyrning"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Välj en app om du vill lägga till snabbkontroller"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobildata"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ansluten"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tillfälligt ansluten"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Dålig anslutning"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Du ansluts inte till mobildata automatiskt"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Ingen anslutning"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Inga andra nätverk är tillgängliga"</string>
diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml
index 8ed824e..47fbdc43 100644
--- a/packages/SystemUI/res/values-sw/strings.xml
+++ b/packages/SystemUI/res/values-sw/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Imeshindwa kutambua uso"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Badala yake, tumia alama ya kidole"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth imeunganishwa."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Asilimia ya betri haijulikani."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Imeunganishwa kwenye <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Ung\'avu"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Ugeuzaji rangi"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Usahihishaji wa rangirangi"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Mipangilio ya mtumiaji"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Dhibiti watumiaji"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Nimemaliza"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Funga"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Imeunganishwa"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Maikrofoni inapatikana"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera inapatikana"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Maikrofoni na kamera zinapatikana"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Umewasha maikrofoni"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Umezima maikrofoni"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Umewasha maikrofoni kwenye programu na huduma zote."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Umezima ufikiaji wa maikrofoni kwenye programu na huduma zote. Unaweza kuruhusu ufikiaji wa maikrofoni kwenye Mipangilio &gt; Faragha &gt; Maikrofoni."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Umezima ufikiaji wa maikrofoni kwenye programu na huduma zote. Unaweza kubadilisha hali hii kwenye Mipangilio &gt; Faragha &gt; Maikrofoni."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Umewasha kamera"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Umezima kamera"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Umewasha kamera kwenye programu na huduma zote."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Umezima ufikiaji wa kamera kwenye programu na huduma zote."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Ili utumie kitufe cha maikrofoni, ruhusu ufikiaji wa maikrofoni kwenye Mipangilio."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Fungua mipangilio."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Kifaa kingine"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Washa Muhtasari"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Hutasumbuliwa na sauti na mitetemo, isipokuwa kengele, vikumbusho, matukio na simu zinazopigwa na watu uliobainisha. Bado utasikia chochote utakachochagua kucheza, ikiwa ni pamoja na muziki, video na michezo."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Unapotuma, kurekodi au kushiriki programu, programu ya <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> inaweza kufikia kitu chochote kitakachoonekana au kuchezwa kwenye programu hiyo. Hivyo kuwa mwangalifu na manenosiri, maelezo ya malipo, ujumbe au maelezo mengine nyeti."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Endelea"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Shiriki au rekodi programu"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Futa zote"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Dhibiti"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Historia"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Ungependa kuzima data ya mtandao wa simu?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Hutaweza kufikia data au intaneti kupitia <xliff:g id="CARRIER">%s</xliff:g>. Intaneti itapatikana kupitia Wi-Fi pekee."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"mtoa huduma wako"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Ungependa kubadilisha ili utumie data ya mtandao wa <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Data ya mtandao wa simu haitabadilika kiotomatiki kulingana na upatikanaji"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Hapana"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ndiyo, badili"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Kwa sababu programu nyingine inazuia ombi la ruhusa, hatuwezi kuthibitisha jibu lako katika Mipangilio."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Ungependa kuruhusu <xliff:g id="APP_0">%1$s</xliff:g> ionyeshe vipengee <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Inaweza kusoma maelezo kutoka <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Mipangilio ya dirisha la kikuzaji"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Gusa ili ufungue vipengele vya ufikivu. Weka mapendeleo au ubadilishe kitufe katika Mipangilio.\n\n"<annotation id="link">"Angalia mipangilio"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Sogeza kitufe kwenye ukingo ili ukifiche kwa muda"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Tendua"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Njia {label} ya mkato imeondolewa}other{Njia # za mkato zimeondolewa}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Sogeza juu kushoto"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Sogeza juu kulia"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Sogeza chini kushoto"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Sogeza chini kulia"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Sogeza kwenye ukingo kisha ufiche"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Sogeza nje ya ukingo kisha uonyeshe"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Ondoa"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"geuza"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Vidhibiti vya vifaa"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Chagua programu ili uweke vidhibiti"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Data ya mtandao wa simu"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Imeunganishwa"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Imeunganishwa kwa muda"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Muunganisho duni"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Data ya mtandao wa simu haitaunganishwa kiotomatiki"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Hakuna muunganisho"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Hakuna mitandao mingine inayopatikana"</string>
diff --git a/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml
new file mode 100644
index 0000000..aab914f
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+
+<!-- Intended for wide devices that are currently oriented with a lot of available height,
+     such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied
+     to wide devices that are shorter in height, like foldables. -->
+<resources>
+    <!-- Space between status view and notification shelf -->
+    <dimen name="keyguard_status_view_bottom_margin">35dp</dimen>
+    <dimen name="keyguard_clock_top_margin">40dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
new file mode 100644
index 0000000..8148d3d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -0,0 +1,47 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
index 347cf29..707bc9e 100644
--- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
@@ -17,9 +17,6 @@
 <resources>
     <dimen name="notification_panel_margin_horizontal">48dp</dimen>
     <dimen name="status_view_margin_horizontal">62dp</dimen>
-    <dimen name="keyguard_clock_top_margin">40dp</dimen>
-    <dimen name="keyguard_status_view_bottom_margin">40dp</dimen>
-    <dimen name="bouncer_user_switcher_y_trans">20dp</dimen>
 
     <!-- qs_tiles_page_horizontal_margin should be margin / 2, otherwise full space between two
          pages is margin * 2, and that makes tiles page not appear immediately after user swipes to
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
new file mode 100644
index 0000000..771de08
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">180dp</item>
+        <item name="android:paddingVertical">80dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
new file mode 100644
index 0000000..b98165f
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+<!-- Intended for wide devices that are currently oriented with a lot of available height,
+     such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied
+     to wide devices that are shorter in height, like foldables. -->
+<resources>
+    <!-- Space between status view and notification shelf -->
+    <dimen name="keyguard_status_view_bottom_margin">70dp</dimen>
+    <dimen name="keyguard_clock_top_margin">80dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">186dp</dimen>
+    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">110dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
new file mode 100644
index 0000000..f9ed67d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -0,0 +1,48 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index 3d8da8a..8b41a44 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -21,9 +21,6 @@
      for different hardware and product builds. -->
 <resources>
     <dimen name="status_view_margin_horizontal">124dp</dimen>
-    <dimen name="keyguard_clock_top_margin">80dp</dimen>
-    <dimen name="keyguard_status_view_bottom_margin">80dp</dimen>
-    <dimen name="bouncer_user_switcher_y_trans">200dp</dimen>
 
     <dimen name="large_screen_shade_header_left_padding">24dp</dimen>
     <dimen name="qqs_layout_padding_bottom">40dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
new file mode 100644
index 0000000..78d299c
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">240dp</item>
+        <item name="android:paddingVertical">120dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-ta/strings.xml b/packages/SystemUI/res/values-ta/strings.xml
index 9397b6c..f836e95 100644
--- a/packages/SystemUI/res/values-ta/strings.xml
+++ b/packages/SystemUI/res/values-ta/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"முகத்தை கண்டறிய இயலவில்லை"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"கைரேகையை உபயோகிக்கவும்"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"புளூடூத் இணைக்கப்பட்டது."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"பேட்டரி சதவீதம் தெரியவில்லை."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>க்கு இணைக்கப்பட்டது."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ஒளிர்வு"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"கலர் இன்வெர்ஷன்"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"கலர் கரெக்‌ஷன்"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"பயனர் அமைப்புகள்"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"பயனர்களை நிர்வகியுங்கள்"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"முடிந்தது"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"மூடுக"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"இணைக்கப்பட்டது"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"மைக்ரோஃபோன் அணுகல் உள்ளது"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"கேமரா அணுகல் உள்ளது"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"மைக்ரோஃபோன் &amp; கேமரா அணுகல் உள்ளது"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"மைக்ரோஃபோன் அணுகல் இயக்கப்பட்டது"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"மைக்ரோஃபோன் அணுகல் முடக்கப்பட்டது"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"அனைத்து ஆப்ஸுக்கும் சேவைகளுக்கும் மைக்ரோஃபோன் அணுகல் இயக்கப்பட்டது."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"அனைத்து ஆப்ஸுக்கும் சேவைகளுக்கும் மைக்ரோஃபோன் அணுகல் முடக்கப்பட்டது. அமைப்புகள் &gt; தனியுரிமை &gt; மைக்ரோஃபோன் என்பதற்குச் சென்று மைக்ரோஃபோன் அணுகலை இயக்கிக்கொள்ளலாம்."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"அனைத்து ஆப்ஸுக்கும் சேவைகளுக்கும் மைக்ரோஃபோன் அணுகல் முடக்கப்பட்டது. அமைப்புகள் &gt; தனியுரிமை &gt; மைக்ரோஃபோன் என்பதற்குச் சென்று இதை மாற்றிக்கொள்ளலாம்."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"கேமரா அணுகல் இயக்கப்பட்டது"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"கேமரா அணுகல் முடக்கப்பட்டது"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"அனைத்து ஆப்ஸுக்கும் சேவைகளுக்கும் கேமரா அணுகல் இயக்கப்பட்டது."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"அனைத்து ஆப்ஸுக்கும் சேவைகளுக்கும் கேமரா அணுகல் முடக்கப்பட்டது."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"மைக்ரோஃபோன் பட்டனைப் பயன்படுத்த, மைக்ரோஃபோன் அணுகலை அமைப்புகளில் இயக்கவும்."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"அமைப்புகளைத் திற."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"பிற சாதனம்"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"மேலோட்டப் பார்வையை நிலைமாற்று"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"அலாரங்கள், நினைவூட்டல்கள், நிகழ்வுகள் மற்றும் குறிப்பிட்ட அழைப்பாளர்களைத் தவிர்த்து, பிற ஒலிகள் மற்றும் அதிர்வுகளின் தொந்தரவு இருக்காது. எனினும், நீங்கள் எதையேனும் (இசை, வீடியோக்கள், கேம்ஸ் போன்றவை) ஒலிக்கும்படி தேர்ந்தெடுத்திருந்தால், அவை வழக்கம் போல் ஒலிக்கும்."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"ஓர் ஆப்ஸை நீங்கள் பகிரும்போதோ ரெக்கார்டு செய்யும்போதோ அலைபரப்பும்போதோ அந்த ஆப்ஸில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தையும் <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ஆப்ஸால் அணுக முடியும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், பிற பாதுகாக்கப்பட வேண்டிய தகவல்கள் ஆகியவை குறித்து கவனத்துடன் இருங்கள்."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"தொடர்க"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ஆப்ஸைப் பகிர்தல் அல்லது ரெக்கார்டு செய்தல்"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"எல்லாவற்றையும் அழி"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"நிர்வகி"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"இதுவரை வந்த அறிவிப்புகள்"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"மொபைல் டேட்டாவை ஆஃப் செய்யவா?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> மூலம் டேட்டா அல்லது இணையத்தை உங்களால் பயன்படுத்த முடியாது. வைஃபை வழியாக மட்டுமே இணையத்தைப் பயன்படுத்த முடியும்."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"உங்கள் மொபைல் நிறுவனம்"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g>க்கு மறுபடியும் மாற்றவா?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"கிடைக்கும் நிலையின் அடிப்படையில் மொபைல் டேட்டா தானாகவே மாறாது"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"வேண்டாம்"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"மாற்று"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"அனுமதிக் கோரிக்கையை ஆப்ஸ் மறைப்பதால், அமைப்புகளால் உங்கள் பதிலைச் சரிபார்க்க முடியாது."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> ஆப்ஸை, <xliff:g id="APP_2">%2$s</xliff:g> ஆப்ஸின் விழிப்பூட்டல்களைக் காண்பிக்க அனுமதிக்கவா?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- இது, <xliff:g id="APP">%1$s</xliff:g> பயன்பாட்டிலிருந்து தகவலைப் படிக்கும்"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"சாளரத்தைப் பெரிதாக்கும் கருவிக்கான அமைப்புகள்"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"அணுகல்தன்மை அம்சத்தை திறக்க தட்டவும். அமைப்பில் பட்டனை பிரத்தியேகமாக்கலாம்/மாற்றலாம்.\n\n"<annotation id="link">"அமைப்பில் காண்க"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"பட்டனைத் தற்காலிகமாக மறைக்க ஓரத்திற்கு நகர்த்தும்"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"செயல்தவிர்"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} ஷார்ட்கட் அகற்றப்பட்டது}other{# ஷார்ட்கட்கள் அகற்றப்பட்டன}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"மேலே இடதுபுறத்திற்கு நகர்த்து"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"மேலே வலதுபுறத்திற்கு நகர்த்து"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"கீழே இடதுபுறத்திற்கு நகர்த்து"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"கீழே வலதுபுறத்திற்கு நகர்த்து"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ஓரத்திற்கு நகர்த்தி மறை"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ஓரத்திற்கு நகர்த்தி, காட்டு"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"அகற்று"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"நிலைமாற்று"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"சாதனக் கட்டுப்பாடுகள்"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"கட்டுப்பாடுகளைச் சேர்க்க வேண்டிய ஆப்ஸைத் தேர்ந்தெடுங்கள்"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"மொபைல் டேட்டா"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"இணைக்கப்பட்டது"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"தற்காலிகமாக இணைக்கப்பட்டுள்ளது"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"இணைப்பு மோசமாக உள்ளது"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"மொபைல் டேட்டாவுடன் தானாக இணைக்காது"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"இணைப்பு இல்லை"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"வேறு நெட்வொர்க்குகள் எதுவும் கிடைக்கவில்லை"</string>
diff --git a/packages/SystemUI/res/values-te/strings.xml b/packages/SystemUI/res/values-te/strings.xml
index f202fae..002a4b2 100644
--- a/packages/SystemUI/res/values-te/strings.xml
+++ b/packages/SystemUI/res/values-te/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ముఖం గుర్తించడం కుదరలేదు"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"బదులుగా వేలిముద్రను ఉపయోగించండి"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"బ్లూటూత్ కనెక్ట్ చేయబడింది."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"బ్యాటరీ శాతం తెలియదు."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g>కి కనెక్ట్ చేయబడింది."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ప్రకాశం"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"కలర్ మార్పిడి"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"కలర్ కరెక్షన్"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"యూజర్ సెట్టింగ్‌లు"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"యూజర్‌లను మేనేజ్ చేయండి"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"పూర్తయింది"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"మూసివేయి"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"కనెక్ట్ చేయబడినది"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"మైక్రోఫోన్ అందుబాటులో ఉంది"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"కెమెరా అందుబాటులో ఉంది"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"మైక్రోఫోన్, అలాగే కెమెరా అందుబాటులో ఉంది"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"మైక్రోఫోన్ ఆన్ చేయబడింది"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"మైక్రోఫోన్ ఆఫ్ చేయబడింది"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"యాప్‌లు, అలాగే సర్వీస్‌లన్నింటికీ మైక్రోఫోన్ ఎనేబుల్ చేయబడింది."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"యాప్‌లు, అలాగే సర్వీస్‌లన్నింటికీ మైక్రోఫోన్ యాక్సెస్ డిజేబుల్ చేయబడింది. సెట్టింగ్‌లు &gt; గోప్యత &gt; మైక్రోఫోన్‌లో మీరు మైక్రోఫోన్ యాక్సెస్‌ను ఎనేబుల్ చేయవచ్చు."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"యాప్‌లు, అలాగే సర్వీస్‌లన్నింటికీ మైక్రోఫోన్ యాక్సెస్ డిజేబుల్ చేయబడింది. సెట్టింగ్‌లు &gt; గోప్యత &gt; మైక్రోఫోన్‌లో మీరు దీన్ని మార్చవచ్చు."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"కెమెరా ఆన్ చేయబడింది"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"కెమెరా ఆఫ్ చేయబడింది"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"యాప్‌లు, అలాగే సర్వీస్‌లన్నింటికీ కెమెరా ఎనేబుల్ చేయబడింది."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"యాప్‌లు, అలాగే సర్వీస్‌లన్నింటికీ కెమెరా యాక్సెస్ డిజేబుల్ చేయబడింది."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"మైక్రోఫోన్ బటన్‌ను ఉపయోగించడానికి, సెట్టింగ్‌లలో మైక్రోఫోన్ యాక్సెస్‌ను ఎనేబుల్ చేయండి."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"సెట్టింగ్‌లను తెరవండి."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"ఇతర పరికరం"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"స్థూలదృష్టిని టోగుల్ చేయి"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"మీరు పేర్కొనే అలారాలు, రిమైండర్‌లు, ఈవెంట్‌లు మరియు కాలర్‌ల నుండి మినహా మరే ఇతర ధ్వనులు మరియు వైబ్రేషన్‌లతో మీకు అంతరాయం కలగదు. మీరు ఇప్పటికీ సంగీతం, వీడియోలు మరియు గేమ్‌లతో సహా మీరు ప్లే చేయడానికి ఎంచుకున్నవి ఏవైనా వింటారు."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"మీరు ఏదైనా యాప్‌ను షేర్ చేస్తున్నప్పుడు, రికార్డ్ చేస్తున్నప్పుడు, లేదా ప్రసారం చేస్తున్నప్పుడు, ఆ యాప్‌లో చూపబడిన దేనికైనా లేదా ప్లే అయిన దేనికైనా <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>‌కు యాక్సెస్ ఉంటుంది. కాబట్టి, పాస్‌వర్డ్‌లు, పేమెంట్ వివరాలు, మెసేజ్‌లు, లేదా ఏదైనా ఇతర సున్నితమైన సమాచారం పట్ల జాగ్రత్త వహించండి."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"కొనసాగించండి"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"యాప్‌ను షేర్ చేయండి లేదా రికార్డ్ చేయండి"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"అన్నీ క్లియర్ చేయండి"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"మేనేజ్ చేయండి"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"హిస్టరీ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"మొబైల్ డేటాను ఆఫ్ చేయాలా?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"\"<xliff:g id="CARRIER">%s</xliff:g>\" ద్వారా మీకు డేటా లేదా ఇంటర్నెట్‌కు యాక్సెస్ ఉండదు. Wi-Fi ద్వారా మాత్రమే ఇంటర్నెట్ అందుబాటులో ఉంటుంది."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"మీ క్యారియర్"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g>కి తిరిగి మారాలా?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"మొబైల్ డేటా లభ్యత ఆధారంగా ఆటోమేటిక్‌గా స్విచ్ అవ్వదు"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"వద్దు, థ్యాంక్స్"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"అవును, మార్చండి"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"అనుమతి రిక్వెస్ట్‌కు ఒక యాప్ అడ్డు తగులుతున్నందున సెట్టింగ్‌లు మీ ప్రతిస్పందనను ధృవీకరించలేకపోయాయి."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_2">%2$s</xliff:g> స్లైస్‌లను చూపించడానికి <xliff:g id="APP_0">%1$s</xliff:g>ని అనుమతించండి?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- ఇది <xliff:g id="APP">%1$s</xliff:g> నుండి సమాచారాన్ని చదువుతుంది"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"మాగ్నిఫయర్ విండో సెట్టింగ్‌లు"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"యాక్సెసిబిలిటీ ఫీచర్‌లను తెరవడానికి ట్యాప్ చేయండి. సెట్టింగ్‌లలో ఈ బటన్‌ను అనుకూలంగా మార్చండి లేదా రీప్లేస్ చేయండి.\n\n"<annotation id="link">"వీక్షణ సెట్టింగ్‌లు"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"తాత్కాలికంగా దానిని దాచడానికి బటన్‌ను చివరకు తరలించండి"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"చర్య రద్దు చేయండి"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} షార్ట్‌కట్ తీసివేయబడింది}other{# షార్ట్‌కట్‌లు తీసివేయబడ్డాయి}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ఎగువ ఎడమ వైపునకు తరలించు"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ఎగువ కుడి వైపునకు తరలించు"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"దిగువ ఎడమ వైపునకు తరలించు"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"దిగువ కుడి వైపునకు తరలించు"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"అంచుకు తరలించి దాచండి"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"అంచుని తరలించి చూపించు"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"తీసివేయండి"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"టోగుల్ చేయి"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"డివైజ్ కంట్రోల్స్"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"కంట్రోల్స్‌ను యాడ్ చేయడానికి యాప్‌ను ఎంచుకోండి"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"మొబైల్ డేటా"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"కనెక్ట్ చేయబడింది"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"తాత్కాలికంగా కనెక్ట్ చేయబడింది"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"కనెక్షన్ బాగాలేదు"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"మొబైల్ డేటా ఆటోమెటిక్‌గా కనెక్ట్ అవ్వదు"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"కనెక్షన్ లేదు"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ఇతర నెట్‌వర్క్‌లేవీ అందుబాటులో లేవు"</string>
diff --git a/packages/SystemUI/res/values-television/strings.xml b/packages/SystemUI/res/values-television/strings.xml
new file mode 100644
index 0000000..f30b73e
--- /dev/null
+++ b/packages/SystemUI/res/values-television/strings.xml
@@ -0,0 +1,27 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
+    <string name="log_access_confirmation_body">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.\n\nLearn more at g.co/android/devicelogs.
+    </string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" translatable="false"></string>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-television/styles.xml b/packages/SystemUI/res/values-television/styles.xml
index 12020f9..c517845 100644
--- a/packages/SystemUI/res/values-television/styles.xml
+++ b/packages/SystemUI/res/values-television/styles.xml
@@ -63,4 +63,10 @@
         <item name="android:paddingVertical">@dimen/bottom_sheet_button_padding_vertical</item>
         <item name="android:stateListAnimator">@anim/tv_bottom_sheet_button_state_list_animator</item>
     </style>
+
+    <!-- The style for log access consent button -->
+    <style name="LogAccessDialogTheme" parent="@android:style/Theme.DeviceDefault.Dialog.Alert">
+        <item name="permissionGrantButtonTopStyle">?android:buttonBarButtonStyle</item>
+        <item name="permissionGrantButtonBottomStyle">?android:buttonBarButtonStyle</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml
index f216437..fa8118e 100644
--- a/packages/SystemUI/res/values-th/strings.xml
+++ b/packages/SystemUI/res/values-th/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"ไม่รู้จักใบหน้า"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"ใช้ลายนิ้วมือแทน"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"เชื่อมต่อบลูทูธแล้ว"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"ไม่ทราบเปอร์เซ็นต์แบตเตอรี่"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"เชื่อมต่อกับ <xliff:g id="BLUETOOTH">%s</xliff:g> แล้ว"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"ความสว่าง"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"การกลับสี"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"การแก้สี"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"การตั้งค่าผู้ใช้"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"จัดการผู้ใช้"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"เสร็จสิ้น"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"ปิด"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"เชื่อมต่อ"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"ใช้งานไมโครโฟนได้"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"ใช้งานกล้องได้"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"ใช้งานไมโครโฟนและกล้องได้"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"เปิดไมโครโฟนแล้ว"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"ปิดไมโครโฟนแล้ว"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"เปิดไมโครโฟนสำหรับแอปและบริการทั้งหมดแล้ว"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"ปิดการเข้าถึงไมโครโฟนสำหรับแอปและบริการทั้งหมดแล้ว คุณสามารถเปิดการเข้าถึงไมโครโฟนได้จากการตั้งค่า &gt; ความเป็นส่วนตัว &gt; ไมโครโฟน"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"ปิดการเข้าถึงไมโครโฟนสำหรับแอปและบริการทั้งหมดแล้ว คุณเปลี่ยนการตั้งค่านี้ได้ในการตั้งค่า &gt; ความเป็นส่วนตัว &gt; ไมโครโฟน"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"เปิดกล้องแล้ว"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"ปิดกล้องแล้ว"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"เปิดการเข้าถึงกล้องสำหรับแอปและบริการทั้งหมดแล้ว"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"ปิดการเข้าถึงกล้องสำหรับแอปและบริการทั้งหมดแล้ว"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"หากต้องการใช้ปุ่มไมโครโฟน ให้เปิดการเข้าถึงไมโครโฟนในการตั้งค่า"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"เปิดการตั้งค่า"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"อุปกรณ์อื่น"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"สลับภาพรวม"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"คุณจะไม่ถูกรบกวนจากเสียงและการสั่น ยกเว้นเสียงนาฬิกาปลุก การช่วยเตือน กิจกรรม และผู้โทรที่ระบุไว้ คุณจะยังคงได้ยินสิ่งที่คุณเลือกเล่น เช่น เพลง วิดีโอ และเกม"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"เมื่อกำลังแชร์ บันทึก หรือแคสต์แอป \"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>\" จะมีสิทธิ์เข้าถึงทุกสิ่งที่แสดงหรือเล่นอยู่ในแอปดังกล่าว ดังนั้นโปรดระวังเกี่ยวกับรหัสผ่าน รายละเอียดการชำระเงิน ข้อความ หรือข้อมูลที่ละเอียดอ่อนอื่นๆ"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"ต่อไป"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"แชร์หรือบันทึกแอป"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"ล้างทั้งหมด"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"จัดการ"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"ประวัติ"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"ปิดอินเทอร์เน็ตมือถือไหม"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"คุณจะใช้เน็ตมือถือหรืออินเทอร์เน็ตผ่าน \"<xliff:g id="CARRIER">%s</xliff:g>\" ไม่ได้ แต่จะใช้ผ่าน Wi-Fi ได้เท่านั้น"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ผู้ให้บริการของคุณ"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"เปลี่ยนกลับเป็น <xliff:g id="CARRIER">%s</xliff:g> หรือไม่"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"อินเทอร์เน็ตมือถือไม่ได้เปลี่ยนตามความพร้อมบริการโดยอัตโนมัติ"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"ไม่เป็นไร"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ใช่ เปลี่ยนเลย"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"เนื่องจากแอปหนึ่งได้บดบังคำขอสิทธิ์ ระบบจึงไม่สามารถยืนยันคำตอบของคุณสำหรับการตั้งค่าได้"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"อนุญาตให้ <xliff:g id="APP_0">%1$s</xliff:g> แสดงส่วนต่างๆ ของ <xliff:g id="APP_2">%2$s</xliff:g>"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- อ่านข้อมูลจาก <xliff:g id="APP">%1$s</xliff:g> ได้"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"การตั้งค่าหน้าต่างแว่นขยาย"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"แตะเพื่อเปิดฟีเจอร์การช่วยเหลือพิเศษ ปรับแต่งหรือแทนที่ปุ่มนี้ในการตั้งค่า\n\n"<annotation id="link">"ดูการตั้งค่า"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"ย้ายปุ่มไปที่ขอบเพื่อซ่อนชั่วคราว"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"เลิกทำ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{นำทางลัด {label} รายการออกแล้ว}other{นำทางลัด # รายการออกแล้ว}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"ย้ายไปด้านซ้ายบน"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"ย้ายไปด้านขวาบน"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"ย้ายไปด้านซ้ายล่าง"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"ย้ายไปด้านขาวล่าง"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"ย้ายไปที่ขอบและซ่อน"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"ย้ายออกจากขอบและแสดง"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"นำออก"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"สลับ"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"ระบบควบคุมอุปกรณ์"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"เลือกแอปเพื่อเพิ่มตัวควบคุม"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"อินเทอร์เน็ตมือถือ"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"เชื่อมต่อแล้ว"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"เชื่อมต่อแล้วชั่วคราว"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"การเชื่อมต่อไม่ดี"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"อินเทอร์เน็ตมือถือจะไม่เชื่อมต่ออัตโนมัติ"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"ไม่มีการเชื่อมต่อ"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"ไม่มีเครือข่ายอื่นๆ ที่พร้อมใช้งาน"</string>
diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml
index f1acf43..1451ab9 100644
--- a/packages/SystemUI/res/values-tl/strings.xml
+++ b/packages/SystemUI/res/values-tl/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Hindi makilala ang mukha"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Gumamit ng fingerprint"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Nakakonekta ang Bluetooth."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Hindi alam ang porsyento ng baterya."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Nakakonekta sa <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Brightness"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Pag-invert ng kulay"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Pagtatama ng kulay"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Mga setting ng user"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Pamahalaan ang mga user"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Tapos na"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Isara"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Nakakonekta"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Available ang mikropono"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Available ang camera"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Available ang mikropono at camera"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Naka-on ang mikropono"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Naka-off ang mikropono"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Naka-enable para sa lahat ng app at serbisyo ang mikropono."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Naka-disable para sa lahat ng app at serbisyo ang access sa mikropono. Puwede mong i-enable ang access sa mikropono sa Mga Setting &gt; Privacy &gt; Mikropono."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Naka-disable para sa lahat ng app at serbisyo ang access sa mikropono. Puwede mo itong baguhin sa Mga Setting &gt; Privacy &gt; Mikropono."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Naka-on ang camera"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Naka-off ang camera"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Naka-enable para sa lahat ng app at serbisyo ang camera."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Naka-disable para sa lahat ng app at serbisyo ang access sa camera."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Para gamitin ang button ng mikropono, i-enable ang access sa mikropono sa Mga Setting."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Buksan ang mga setting."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Iba pang device"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"I-toggle ang Overview"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Hindi ka maiistorbo ng mga tunog at pag-vibrate, maliban mula sa mga alarm, paalala, event, at tumatawag na tutukuyin mo. Maririnig mo pa rin ang kahit na anong piliin mong i-play kabilang ang mga musika, video, at laro."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Kapag nagbabahagi, nagre-record, o nagka-cast ka ng app, may access ang <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> sa kahit anong ipinapakita o pine-play sa app na iyon. Kaya mag-ingat sa mga password, detalye ng pagbabayad, mensahe, o iba pang impormasyon."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Magpatuloy"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Ibahagi o i-record ang isang app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"I-clear lahat"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Pamahalaan"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"History"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"I-off ang mobile data?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Hindi ka magkaka-access sa data o internet sa pamamagitan ng <xliff:g id="CARRIER">%s</xliff:g>. Available lang ang internet sa pamamagitan ng Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ang iyong carrier"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Bumalik sa <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Hindi awtomatikong magbabago ang mobile data base sa availability"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Hindi, salamat na lang"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Oo, lumipat"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Hindi ma-verify ng Mga Setting ang iyong tugon dahil may app na tumatakip sa isang kahilingan sa pagpapahintulot."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Payagan ang <xliff:g id="APP_0">%1$s</xliff:g> na ipakita ang mga slice ng <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Nakakabasa ito ng impormasyon mula sa <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Mga setting ng window ng magnifier"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"I-tap, buksan mga feature ng accessibility. I-customize o palitan button sa Mga Setting.\n\n"<annotation id="link">"Tingnan ang mga setting"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Ilipat ang button sa gilid para pansamantala itong itago"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"I-undo"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} shortcut ang naalis}one{# shortcut ang naalis}other{# na shortcut ang naalis}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Ilipat sa kaliwa sa itaas"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Ilipat sa kanan sa itaas"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Ilipat sa kaliwa sa ibaba"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Ilipat sa kanan sa ibaba"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Ilipat sa sulok at itago"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Alisin sa sulok at ipakita"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Alisin"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"i-toggle"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Mga kontrol ng device"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Pumili ng app para magdagdag ng mga kontrol"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobile data"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Nakakonekta"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Pansamantalang nakakonekta"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Mahina ang koneksyon"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Hindi awtomatikong kokonekta ang mobile data"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Walang koneksyon"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Walang available na iba pang network"</string>
diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml
index 7b52a41..098091a 100644
--- a/packages/SystemUI/res/values-tr/strings.xml
+++ b/packages/SystemUI/res/values-tr/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Yüz tanınamadı"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Bunun yerine parmak izi kullanın"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth bağlandı."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Pil yüzdesi bilinmiyor."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> ile bağlı."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Parlaklık"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Rengi ters çevirme"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Renk düzeltme"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Kullanıcı ayarları"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Kullanıcıları yönet"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Bitti"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Kapat"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Bağlı"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon kullanılabilir"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera kullanılabilir"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon ve kamera kullanılabilir"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon açık"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon kapalı"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon tüm uygulama ve hizmetler için etkinleştirildi."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofon erişimi tüm uygulama ve hizmetler için devre dışı bırakıldı. Mikrofon erişimini Ayarlar &gt; Gizlilik &gt; Mikrofon\'da etkinleştirebilirsiniz."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofon erişimi tüm uygulama ve hizmetler için devre dışı bırakıldı. Bunu Ayarlar &gt; Gizlilik &gt; Mikrofon\'da değiştirebilirsiniz."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera açıldı"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera devre dışı bırakıldı"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera tüm uygulama ve hizmetler için etkinleştirildi."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kamera erişimi tüm uygulama ve hizmetler için devre dışı bırakıldı."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Mikrofon düğmesini kullanmak için Ayarlar\'da mikrofon erişimini etkinleştirin."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Ayarları aç."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Diğer cihaz"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Genel bakışı aç/kapat"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Alarmlar, hatırlatıcılar, etkinlikler ve sizin seçtiğiniz kişilerden gelen çağrılar dışında hiçbir ses ve titreşimle rahatsız edilmeyeceksiniz. O sırada çaldığınız müzik, seyrettiğiniz video ya da oynadığınız oyunların sesini duymaya devam edeceksiniz."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Bir uygulamayı paylaşma, kaydetme ve yayınlama özelliklerini kullandığınızda <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>, söz konusu uygulamada gösterilen veya oynatılan her şeye erişebilir. Dolayısıyla şifreler, ödeme ayrıntıları, mesajlar veya diğer hassas bilgiler konusunda dikkatli olun."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Devam"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Uygulamayı paylaşın veya kaydedin"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Tümünü temizle"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Yönet"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Geçmiş"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Mobil veri kapatılsın mı?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> üzerinden veri veya internet erişiminiz olmayacak. İnternet yalnızca kablosuz bağlantı üzerinden kullanılabilecek."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"operatörünüz"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> operatörüne geri dönülsün mü?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Uygunluk durumuna göre otomatik olarak mobil veriye geçilmez"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Hayır, teşekkürler"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Evet, geçilsin"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Bir uygulama bir izin isteğinin anlaşılmasını engellediğinden, Ayarlar, yanıtınızı doğrulayamıyor."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> uygulamasının, <xliff:g id="APP_2">%2$s</xliff:g> dilimlerini göstermesine izin verilsin mi?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- <xliff:g id="APP">%1$s</xliff:g> uygulamasından bilgileri okuyabilir"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Büyüteç penceresi ayarları"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Erişilebilirlik özelliklerini açmak için dokunun. Bu düğmeyi Ayarlar\'dan özelleştirin veya değiştirin.\n\n"<annotation id="link">"Ayarları göster"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Düğmeyi geçici olarak gizlemek için kenara taşıyın"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Geri al"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} kısayol kaldırıldı}other{# kısayol kaldırıldı}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Sol üste taşı"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Sağ üste taşı"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Sol alta taşı"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Sağ alta taşı"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Kenara taşıyıp gizle"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Kenarın dışına taşıyıp göster"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Kaldır"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"değiştir"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Cihaz denetimleri"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Denetim eklemek için uygulama seçin"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobil veri"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Bağlı"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Geçici olarak bağlandı"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Bağlantı zayıf"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobil veri otomatik olarak bağlanmıyor"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Bağlantı yok"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Kullanılabilir başka ağ yok"</string>
diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml
index 6bd9e30..ef05be6 100644
--- a/packages/SystemUI/res/values-uk/strings.xml
+++ b/packages/SystemUI/res/values-uk/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Обличчя не розпізнано"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Скористайтеся відбитком"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth під’єднано."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Відсоток заряду акумулятора невідомий."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Підключено до <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Яскравість"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Інверсія кольорів"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Корекція кольору"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Налаштування користувача"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Керувати користувачами"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Готово"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Закрити"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Під’єднано"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Мікрофон доступний"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Камера доступна"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Мікрофон і камера доступні"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Мікрофон увімкнено"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Мікрофон вимкнено"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Мікрофон увімкнено для всіх додатків і сервісів."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Доступ до мікрофона вимкнено для всіх додатків і сервісів. Щоб надати доступ до мікрофона, виберіть \"Налаштування\" &gt; \"Конфіденційність\" &gt; \"Мікрофон\"."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Доступ до мікрофона вимкнено для всіх додатків і сервісів. Щоб змінити це, виберіть \"Налаштування\" &gt; \"Конфіденційність\" &gt; \"Мікрофон\"."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Камеру ввімкнено"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Камеру вимкнено"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Камеру ввімкнено для всіх додатків і сервісів."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Доступ до камери вимкнено для всіх додатків і сервісів."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Щоб використовувати кнопку мікрофона, надайте доступ до мікрофона в налаштуваннях."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Відкрити налаштування"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Інший пристрій"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Увімкнути або вимкнути огляд"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Ви отримуватиме звукові та вібросигнали лише для вибраних будильників, нагадувань, подій і абонентів. Однак ви чутимете все, що захочете відтворити, зокрема музику, відео й ігри."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Коли ви показуєте, записуєте або транслюєте додаток, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> отримує доступ до всього, що відображається або відтворюється в цьому додатку. Тому будьте уважні з паролями, повідомленнями, платіжною й іншою конфіденційною інформацією."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Продовжити"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Показувати або записувати додаток"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Очистити все"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Керувати"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Історія"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Вимкнути мобільний Інтернет?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Ви не матимете доступу до даних чи Інтернету через оператора <xliff:g id="CARRIER">%s</xliff:g>. Інтернет буде доступний лише через Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"ваш оператор"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Перейти на <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Пристрій не перемикатиметься на мобільний Інтернет автоматично"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Ні, дякую"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Так, перемикатися"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Не вдається підтвердити вашу відповідь у налаштуваннях, оскільки інший додаток заступає запит на дозвіл."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Дозволити додатку <xliff:g id="APP_0">%1$s</xliff:g> показувати фрагменти додатка <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Має доступ до інформації з додатка <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Налаштування розміру лупи"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Кнопка спеціальних можливостей. Змініть або замініть її в Налаштуваннях.\n\n"<annotation id="link">"Переглянути налаштування"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Щоб тимчасово сховати кнопку, перемістіть її на край екрана"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Відмінити"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Функцію спеціальних можливостей \"{label}\" вилучено}one{# функцію спеціальних можливостей вилучено}few{# функції спеціальних можливостей вилучено}many{# функцій спеціальних можливостей вилучено}other{# функції спеціальних можливостей вилучено}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Перемістити ліворуч угору"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Перемістити праворуч угору"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Перемістити ліворуч униз"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Перемістити праворуч униз"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Перемістити до краю, приховати"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Перемістити від краю, показати"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Вилучити"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"перемкнути"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Керування пристроями"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Виберіть, для якого додатка налаштувати елементи керування"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Мобільний трафік"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Підключено"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Тимчасово з’єднано"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Погане з’єднання"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Мобільний Інтернет не підключатиметься автоматично"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Немає з\'єднання"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Інші мережі недоступні"</string>
diff --git a/packages/SystemUI/res/values-ur/strings.xml b/packages/SystemUI/res/values-ur/strings.xml
index ec382b2..f1992a7 100644
--- a/packages/SystemUI/res/values-ur/strings.xml
+++ b/packages/SystemUI/res/values-ur/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"چہرے کی پہچان نہیں ہو سکی"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"اس کے بجائے فنگر پرنٹ استعمال کریں"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"بلوٹوتھ مربوط ہے۔"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"بیٹری کی فیصد نامعلوم ہے۔"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"<xliff:g id="BLUETOOTH">%s</xliff:g> سے منسلک ہیں۔"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"چمکیلا پن"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"رنگوں کی تقلیب"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"رنگ کی اصلاح"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"صارف کی ترتیبات"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"صارفین کا نظم کریں"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"ہو گیا"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"بند کریں"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"مربوط"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"مائیکروفون دستیاب ہے"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"کیمرا دستیاب ہے"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"مائیکروفون اور کیمرا دستیاب ہیں"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"دوسرا آلہ"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"مجموعی جائزہ ٹوگل کریں"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"الارمز، یاددہانیوں، ایونٹس اور آپ کے متعین کردہ کالرز کے علاوہ، آپ آوازوں اور وائبریشنز سے ڈسٹرب نہیں ہوں گے۔ موسیقی، ویڈیوز اور گیمز سمیت آپ ابھی بھی ہر وہ چیز سنیں گے جسے چلانے کا آپ انتخاب کرتے ہیں۔"</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"جب آپ اشتراک، ریکارڈنگ یا کاسٹ کر رہے ہوتے ہیں تو <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> کو آپ کی اسکرین پر دکھائی گئی یا آپ کے آلے پر چلائی گئی ہر چیز تک رسائی حاصل ہوتی ہے۔ اس لیے پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، یا دیگر حساس معلومات کے سلسلے میں محتاط رہیں۔"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"جاری رکھیں"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"ایپ کا اشتراک یا ریکارڈ کریں"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"سبھی کو صاف کریں"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"نظم کریں"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"سرگزشت"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"موبائل ڈیٹا آف کریں؟"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"‏آپ کو <xliff:g id="CARRIER">%s</xliff:g> کے ذریعے ڈیٹا یا انٹرنیٹ تک رسائی حاصل نہیں ہوگی۔ انٹرنیٹ صرف Wi-Fi کے ذریعے دستیاب ہوگا۔"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"آپ کا کریئر"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> پر واپس سوئچ کریں؟"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"دستیابی کی بنیاد پر موبائل ڈیٹا خودکار طور پر تبدیل نہیں ہوگا"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"نہیں شکریہ"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"ہاں، سوئچ کریں"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"چونکہ ایک ایپ اجازت کی درخواست کو مبہم کر رہی ہے، لہذا ترتیبات آپ کے جواب کی توثیق نہیں کر سکتی ہیں۔"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> کو <xliff:g id="APP_2">%2$s</xliff:g> کے سلائسز دکھانے کی اجازت دیں؟"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- یہ <xliff:g id="APP">%1$s</xliff:g> کی معلومات پڑھ سکتا ہے"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"میگنیفائر ونڈو کی ترتیبات"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"ایکسیسبیلٹی خصوصیات کھولنے کے لیے تھپتھپائیں۔ ترتیبات میں اس بٹن کو حسب ضرورت بنائیں یا تبدیل کریں۔\n\n"<annotation id="link">"ترتیبات ملاحظہ کریں"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"عارضی طور پر بٹن کو چھپانے کے لئے اسے کنارے پر لے جائیں"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"کالعدم کریں"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} شارٹ کٹ ہٹا دیا گیا}other{# شارٹ کٹس ہٹا دیے گئے}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"اوپر بائیں جانب لے جائیں"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"اوپر دائیں جانب لے جائيں"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"نیچے بائیں جانب لے جائیں"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"نیچے دائیں جانب لے جائیں"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"‏EDGE پر لے جائیں اور چھپائیں"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"‏EDGE اور شو سے باہر منتقل کریں"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"ہٹائیں"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"ٹوگل کریں"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"آلہ کے کنٹرولز"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"کنٹرولز شامل کرنے کے لیے ایپ منتخب کریں"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"موبائل ڈیٹا"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="NETWORKMODE">%2$s</xliff:g> / <xliff:g id="STATE">%1$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"منسلک ہے"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"عارضی طور پر منسلک ہے"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"کمزور کنکشن"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"موبائل ڈیٹا خودکار طور پر منسلک نہیں ہوگا"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"کوئی کنکشن نہیں"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"کوئی دوسرا نیٹ ورک دستیاب نہیں ہے"</string>
diff --git a/packages/SystemUI/res/values-uz/strings.xml b/packages/SystemUI/res/values-uz/strings.xml
index a666432..96e031a 100644
--- a/packages/SystemUI/res/values-uz/strings.xml
+++ b/packages/SystemUI/res/values-uz/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Yuz aniqlanmadi"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Barmoq izi orqali urining"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth ulandi."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Batareya quvvati foizi nomaʼlum."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Ulangan: <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Yorqinlik"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Ranglarni akslantirish"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Ranglarni tuzatish"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Foydalanuvchi sozlamalari"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Foydalanuvchilarni boshqarish"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Tayyor"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Yopish"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Ulangan"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Mikrofon mavjud"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Kamera mavjud"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Mikrofon va kamera mavjud"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Mikrofon yoqildi"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Mikrofon oʻchirildi"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mikrofon barcha ilovalar va xizmatlar uchun yoqilgan."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mikrofon ruxsati barcha ilovalar va xizmatlar uchun oʻchirilgan. Sozlamalar &gt; Maxfiylik &gt; Mikrofon orqali mikrofon ruxsatini yoqishingiz mumkin."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mikrofon ruxsati barcha ilovalar va xizmatlar uchun oʻchirilgan. Buni Sozlamalar &gt; Maxfiylik &gt; Mikrofon menyusida oʻzgartirishingiz mumkin."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Kamera yoqildi"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Kamera oʻchirildi"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Kamera barcha ilovalar va xizmatlar uchun yoqilgan."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Kamera ruxsati barcha ilovalar va xizmatlar uchun oʻchirilgan."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Mikrofon tugmasidan foydalanish uchun Sozlamalar orqali mikrofon ruxsatini yoqing."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Sozlamalarni ochish."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Boshqa qurilma"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Umumiy nazar rejimini almashtirish"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Turli ovoz va tebranishlar endi sizni bezovta qilmaydi. Biroq, signallar, eslatmalar, tadbirlar haqidagi bildirishnomalar va siz tanlagan abonentlardan kelgan chaqiruvlar bundan mustasno. Lekin, ijro etiladigan barcha narsalar, jumladan, musiqa, video va o‘yinlar ovozi eshitiladi."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Ulashish, yozib olish va translatsiya qilish vaqtida <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> ilovasi ekranda chiqadigan yoki qurilmada ijro qilinadigan kontentni koʻra oladi. Shu sababli parollar, toʻlov tafsilotlari, xabarlar yoki boshqa maxfiy axborot chiqmasligi uchun ehtiyot boʻling."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Davom etish"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Ilovada ulashish yoki yozib olish"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Hammasini tozalash"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Boshqarish"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Tarix"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Mobil internet uzilsinmi?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"<xliff:g id="CARRIER">%s</xliff:g> orqali internetdan foydalana olmaysiz. Internet faqat Wi-Fi orqali ishlaydi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"aloqa operatoringiz"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"<xliff:g id="CARRIER">%s</xliff:g> xizmati qaytarilsinmi?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Mobil internet mavjudligi asosida avtomatik almashtirilmaydi"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Kerak emas"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Ha, almashtirilsin"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Ilova ruxsatnoma so‘roviga xalaqit qilayotgani tufayli, “Sozlamalar” ilovasi javobingizni tekshira olmaydi."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"<xliff:g id="APP_0">%1$s</xliff:g> ilovasiga <xliff:g id="APP_2">%2$s</xliff:g> ilovasidan fragmentlar ko‘rsatishga ruxsat berilsinmi?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"– <xliff:g id="APP">%1$s</xliff:g> ma’lumotlarini o‘qiy oladi"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Lupa oynasi sozlamalari"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Maxsus imkoniyatlarni ochish uchun bosing Sozlamalardan moslay yoki almashtira olasiz.\n\n"<annotation id="link">"Sozlamalar"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Vaqtinchalik berkitish uchun tugmani qirra tomon suring"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Bekor qilish"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{{label} ta yorliq olindi}other{# ta yorliq olindi}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Yuqori chapga surish"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Yuqori oʻngga surish"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Quyi chapga surish"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Quyi oʻngga surish"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Chetiga olib borish va yashirish"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Chetidan qaytarish va koʻrsatish"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Olib tashlash"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"oʻzgartirish"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Qurilmalarni boshqarish"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Boshqaruv elementlarini kiritish uchun ilovani tanlang"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Mobil internet"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ulandi"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Vaqtincha ulangan"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Aloqa beqaror"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Mobil internetga avtomatik ulanmaydi"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Internetga ulanmagansiz"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Boshqa tarmoqlar mavjud emas"</string>
diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml
index 21200ca..b253a91 100644
--- a/packages/SystemUI/res/values-vi/strings.xml
+++ b/packages/SystemUI/res/values-vi/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Không nhận ra khuôn mặt"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Hãy dùng vân tay"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Đã kết nối bluetooth."</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Tỷ lệ phần trăm pin không xác định."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Đã kết nối với <xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Độ sáng"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Đảo màu"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Chỉnh màu"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Cài đặt người dùng"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Quản lý người dùng"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Xong"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Đóng"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Đã kết nối"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Micrô đang bật"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Máy ảnh đang bật"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Micrô và máy ảnh đang bật"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"Đã bật micrô"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"Đã tắt micrô"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"Mọi ứng dụng và dịch vụ được phép sử dụng micrô."</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"Mọi ứng dụng và dịch vụ không có quyền truy cập vào micrô. Bạn có thể bật quyền truy cập vào micrô trong phần Cài đặt &gt; Quyền riêng tư &gt; Micrô."</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"Mọi ứng dụng và dịch vụ không có quyền truy cập vào micrô. Bạn có thể thay đổi chế độ này trong phần Cài đặt &gt; Quyền riêng tư &gt; Micrô."</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"Đã bật máy ảnh"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"Đã tắt máy ảnh"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"Mọi ứng dụng và dịch vụ được phép sử dụng máy ảnh."</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"Mọi ứng dụng và dịch vụ không có quyền truy cập vào máy ảnh."</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"Để dùng nút micrô, hãy bật quyền truy cập vào micrô trong phần Cài đặt."</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"Mở phần cài đặt."</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Thiết bị khác"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Bật/tắt chế độ xem Tổng quan"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Bạn sẽ không bị làm phiền bởi âm thanh và tiếng rung, ngoại trừ báo thức, lời nhắc, sự kiện và người gọi mà bạn chỉ định. Bạn sẽ vẫn nghe thấy mọi thứ bạn chọn phát, bao gồm nhạc, video và trò chơi."</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Khi bạn chia sẻ, ghi hoặc truyền ứng dụng, <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> sẽ có quyền truy cập vào mọi nội dung xuất hiện hoặc phát trên ứng dụng đó. Vì vậy, hãy thận trọng để không làm lộ mật khẩu, thông tin thanh toán, tin nhắn hoặc thông tin nhạy cảm khác."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Tiếp tục"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Chia sẻ hoặc ghi ứng dụng"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Xóa tất cả"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Quản lý"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Lịch sử"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Tắt dữ liệu di động?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Bạn sẽ không có quyền sử dụng dữ liệu hoặc truy cập Internet thông qua chế độ <xliff:g id="CARRIER">%s</xliff:g>. Bạn chỉ có thể truy cập Internet thông qua Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"nhà mạng của bạn"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Chuyển về <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Dữ liệu di động sẽ không tự động chuyển dựa trên tình trạng phủ sóng"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Không, cảm ơn"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Có, hãy chuyển"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Vì ứng dụng đang che khuất yêu cầu cấp quyền nên Cài đặt không thể xác minh câu trả lời của bạn."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Cho phép <xliff:g id="APP_0">%1$s</xliff:g> hiển thị các lát của <xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Có thể đọc thông tin từ <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Chế độ cài đặt cửa sổ phóng to"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Nhấn để mở bộ tính năng hỗ trợ tiếp cận. Tuỳ chỉnh/thay thế nút này trong phần Cài đặt.\n\n"<annotation id="link">"Xem chế độ cài đặt"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Di chuyển nút sang cạnh để ẩn nút tạm thời"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Huỷ"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Đã xoá {label} lối tắt}other{Đã xoá # lối tắt}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Chuyển lên trên cùng bên trái"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Chuyển lên trên cùng bên phải"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Chuyển tới dưới cùng bên trái"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Chuyển tới dưới cùng bên phải"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Chuyển đến cạnh và ẩn"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Chuyển ra xa cạnh và hiển thị"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Xoá"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"bật/tắt"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Điều khiển thiết bị"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Chọn ứng dụng để thêm các tùy chọn điều khiển"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Dữ liệu di động"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Đã kết nối"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Tạm thời có kết nối"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Kết nối kém"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Dữ liệu di động sẽ không tự động kết nối"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Không có kết nối mạng"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Không có mạng nào khác"</string>
diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml
index 83508a1..64357a0 100644
--- a/packages/SystemUI/res/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res/values-zh-rCN/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"人脸识别失败"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"改用指纹"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"蓝牙已连接。"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"电池电量百分比未知。"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"已连接到<xliff:g id="BLUETOOTH">%s</xliff:g>。"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"亮度"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"颜色反转"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"色彩校正"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"用户设置"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"管理用户"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"完成"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"关闭"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"已连接"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"麦克风可用"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"摄像头可用"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"麦克风和摄像头可用"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"麦克风已开启"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"麦克风已关闭"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"已允许所有应用和服务访问麦克风。"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"已阻止所有应用和服务访问麦克风。您可依次前往“设置”&gt;“隐私设置”&gt;“麦克风”来启用麦克风访问权限。"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"已阻止所有应用和服务访问麦克风。您可依次前往“设置”&gt;“隐私设置”&gt;“麦克风”来更改此权限设置。"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"摄像头已开启"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"摄像头已关闭"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"已允许所有应用和服务访问摄像头。"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"已阻止所有应用和服务访问摄像头。"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"若要使用麦克风按钮,请在“设置”中启用麦克风访问权限。"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"打开设置。"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"其他设备"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"切换概览"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"您将不会受到声音和振动的打扰(闹钟、提醒、活动和所指定来电者的相关提示音除外)。您依然可以听到您选择播放的任何内容(包括音乐、视频和游戏)的相关音效。"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"在您进行分享、录制或投射时,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以访问通过此应用显示或播放的所有内容。因此,请注意保护密码、付款信息、消息或其他敏感信息。"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"继续"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"分享或录制应用"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"全部清除"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"管理"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"历史记录"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"要关闭移动数据网络吗?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"您将无法通过<xliff:g id="CARRIER">%s</xliff:g>使用移动数据或互联网,只能通过 WLAN 连接到互联网。"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"您的运营商"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"切换回 <xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"移动流量不会根据可用性自动切换"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"不用了"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"是,切换"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"由于某个应用遮挡了权限请求界面,因此“设置”应用无法验证您的回应。"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"要允许“<xliff:g id="APP_0">%1$s</xliff:g>”显示“<xliff:g id="APP_2">%2$s</xliff:g>”图块吗?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- 可以读取“<xliff:g id="APP">%1$s</xliff:g>”中的信息"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"放大镜窗口设置"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"点按即可打开无障碍功能。您可在“设置”中自定义或更换此按钮。\n\n"<annotation id="link">"查看设置"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"将按钮移到边缘,即可暂时将其隐藏"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"撤消"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{已移除快捷方式 {label}}other{已移除 # 个快捷方式}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"移至左上角"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"移至右上角"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"移至左下角"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"移至右下角"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"移至边缘并隐藏"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"移至边缘以外并显示"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"移除"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"开启/关闭"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"设备控制器"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"选择要添加控制器的应用"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"移动数据网络"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"已连接"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"暂时已连接"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"连接状况不佳"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"系统将不会自动连接到移动数据网络"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"无网络连接"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"没有其他可用网络"</string>
diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml
index 28420b3..ec9112c 100644
--- a/packages/SystemUI/res/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res/values-zh-rHK/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"無法辨識面孔"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"請改用指紋"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"藍牙連線已建立。"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"電量百分比不明。"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"已連線至<xliff:g id="BLUETOOTH">%s</xliff:g>。"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"亮度"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"色彩反轉"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"色彩校正"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"使用者設定"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"管理使用者"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"完成"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"關閉"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"已連線"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"可使用麥克風"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"可使用相機"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"可使用麥克風和相機"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"麥克風已開啟"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"麥克風已關閉"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"已為所有應用程式和服務啟用麥克風。"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"已停用所有應用程式和服務的麥克風存取權。您可以在 [設定] &gt; [私隱] &gt; [麥克風] 啟用麥克風存取權。"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"已停用所有應用程式和服務的麥克風存取權。您可以在 [設定] &gt; [私隱] &gt; [麥克風] 更改設定。"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"相機已開啟"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"相機已關閉"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"已為所有應用程式和服務啟用相機。"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"已停用所有應用程式和服務的相機存取權。"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"如要使用麥克風按鈕,請在「設定」中啟用麥克風存取權。"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"開啟設定。"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"其他裝置"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"切換概覽"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"您不會受到聲音和震動騷擾 (鬧鐘、提醒、活動和您指定的來電者鈴聲除外)。當您選擇播放音樂、影片和遊戲等,仍可以聽到該內容的聲音。"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"進行分享、錄製或投放時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可存取顯示在螢幕畫面上或在裝置上播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他敏感資料。"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"繼續"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"分享或錄製應用程式"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"全部清除"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"管理"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"記錄"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"要關閉流動數據嗎?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"您無法透過「<xliff:g id="CARRIER">%s</xliff:g>」使用流動數據或互聯網。如要使用互聯網,您必須連接 Wi-Fi。"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"您的流動網絡供應商"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"要切換回「<xliff:g id="CARRIER">%s</xliff:g>」嗎?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"流動數據不會根據可用性自動切換"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"不用了,謝謝"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"是,切換回 DDS 對話框"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"由於某個應用程式已阻擋權限要求畫面,因此「設定」應用程式無法驗證您的回應。"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"要允許「<xliff:g id="APP_0">%1$s</xliff:g>」顯示「<xliff:g id="APP_2">%2$s</xliff:g>」的快訊嗎?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- 可以讀取「<xliff:g id="APP">%1$s</xliff:g>」中的資料"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"放大鏡視窗設定"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"㩒一下就可以開無障礙功能。喺「設定」度自訂或者取代呢個按鈕。\n\n"<annotation id="link">"查看設定"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"將按鈕移到邊緣即可暫時隱藏"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"復原"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{已移除 {label} 個捷徑}other{已移除 # 個捷徑}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"移去左上方"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"移去右上方"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"移到左下方"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"移去右下方"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"移到邊緣並隱藏"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"從邊緣移出並顯示"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"移除"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"切換"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"裝置控制"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"選擇要新增控制項的應用程式"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"流動數據"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"已連線"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"已暫時連線"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"連線速度欠佳"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"不會自動連線至流動數據"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"沒有連線"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"沒有可用的其他網絡"</string>
diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml
index 5f1863a..0f82675 100644
--- a/packages/SystemUI/res/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res/values-zh-rTW/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"無法辨識臉孔"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"請改用指紋"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"藍牙連線已建立。"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"電池電量不明。"</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"已連線至<xliff:g id="BLUETOOTH">%s</xliff:g>。"</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"亮度"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"色彩反轉"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"色彩校正"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"使用者設定"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"管理使用者"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"完成"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"關閉"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"已連線"</string>
@@ -303,6 +305,17 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"可使用麥克風"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"可使用相機"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"可使用麥克風和相機"</string>
+    <string name="sensor_privacy_mic_turned_on_dialog_title" msgid="6348853159838376513">"麥克風已開啟"</string>
+    <string name="sensor_privacy_mic_turned_off_dialog_title" msgid="5760464281790732849">"麥克風已關閉"</string>
+    <string name="sensor_privacy_mic_unblocked_dialog_content" msgid="4889961886199270224">"所有應用程式和服務的麥克風存取權皆已啟用。"</string>
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content" msgid="5864898470772965394">"所有應用程式和服務的麥克風存取權皆已停用。如要啟用麥克風存取權,請依序前往「設定」&gt;「隱私權」&gt;「麥克風」。"</string>
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content" msgid="810289713700437896">"所有應用程式和服務的麥克風存取權皆已停用。如要變更這項設定,請依序前往「設定」&gt;「隱私權」&gt;「麥克風」。"</string>
+    <string name="sensor_privacy_camera_turned_on_dialog_title" msgid="8039095295100075952">"相機已開啟"</string>
+    <string name="sensor_privacy_camera_turned_off_dialog_title" msgid="1936603903120742696">"相機已關閉"</string>
+    <string name="sensor_privacy_camera_unblocked_dialog_content" msgid="7847190103011782278">"所有應用程式和服務的相機存取權皆已啟用。"</string>
+    <string name="sensor_privacy_camera_blocked_dialog_content" msgid="3182428709314874616">"所有應用程式和服務的相機存取權皆已停用。"</string>
+    <string name="sensor_privacy_htt_blocked_dialog_content" msgid="3333321592997666441">"如要使用麥克風按鈕,請前往「設定」啟用麥克風存取權。"</string>
+    <string name="sensor_privacy_dialog_open_settings" msgid="1503088305279285048">"開啟設定。"</string>
     <string name="media_seamless_other_device" msgid="4654849800789196737">"其他裝置"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"切換總覽"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"裝置不會發出音效或震動造成干擾,但是會保留與鬧鐘、提醒、活動和指定來電者有關的設定。如果你選擇播放音樂、影片和遊戲等內容,還是可以聽見相關音訊。"</string>
@@ -373,6 +386,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"進行分享、錄製或投放應用程式時,<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> 可以存取在該應用程式中顯示或播放的所有內容。因此請謹慎處理密碼、付款資料、訊息或其他機密資訊。"</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"繼續"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"分享或錄製應用程式"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"全部清除"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"管理"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"記錄"</string>
@@ -672,7 +695,7 @@
     <string name="data_connection_no_internet" msgid="691058178914184544">"沒有網際網路連線"</string>
     <string name="accessibility_quick_settings_open_settings" msgid="536838345505030893">"開啟「<xliff:g id="ID_1">%s</xliff:g>」設定。"</string>
     <string name="accessibility_quick_settings_edit" msgid="1523745183383815910">"編輯設定順序。"</string>
-    <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源按鈕選單"</string>
+    <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源鍵選單"</string>
     <string name="accessibility_quick_settings_page" msgid="7506322631645550961">"第 <xliff:g id="ID_1">%1$d</xliff:g> 頁,共 <xliff:g id="ID_2">%2$d</xliff:g> 頁"</string>
     <string name="tuner_lock_screen" msgid="2267383813241144544">"鎖定畫面"</string>
     <string name="thermal_shutdown_title" msgid="2702966892682930264">"手機先前過熱,因此關閉電源"</string>
@@ -727,6 +750,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"要關閉行動數據嗎?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"你將無法透過「<xliff:g id="CARRIER">%s</xliff:g>」使用行動數據或網際網路。你只能透過 Wi-Fi 使用網際網路。"</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"你的電信業者"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"要切換回「<xliff:g id="CARRIER">%s</xliff:g>」嗎?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"行動數據不會依據可用性自動切換"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"不用了,謝謝"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"是,切換回 DDS 對話方塊"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"由於某個應用程式覆蓋了權限要求畫面,因此「設定」應用程式無法驗證你的回應。"</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"要允許「<xliff:g id="APP_0">%1$s</xliff:g>」顯示「<xliff:g id="APP_2">%2$s</xliff:g>」的區塊嗎?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- 它可以讀取「<xliff:g id="APP">%1$s</xliff:g>」的資訊"</string>
@@ -785,12 +812,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"放大鏡視窗設定"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"輕觸即可開啟無障礙功能。你可以前往「設定」自訂或更換這個按鈕。\n\n"<annotation id="link">"查看設定"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"將按鈕移到邊緣處即可暫時隱藏"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"復原"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{已移除 {label} 個捷徑}other{已移除 # 個捷徑}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"移到左上方"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"移到右上方"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"移到左下方"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"移到右下方"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"移到邊緣並隱藏"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"從邊緣移出並顯示"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"移除"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"切換"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"裝置控制"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"選擇應用程式以新增控制項"</string>
@@ -933,6 +963,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"行動數據"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"已連線"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"已暫時建立連線"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"連線品質不佳"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"系統將不會自動使用行動數據連線"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"沒有網路連線"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"沒有可用的其他網路"</string>
diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml
index 9b376f0..061592a 100644
--- a/packages/SystemUI/res/values-zu/strings.xml
+++ b/packages/SystemUI/res/values-zu/strings.xml
@@ -168,6 +168,8 @@
     <skip />
     <string name="keyguard_face_failed" msgid="9044619102286917151">"Ayikwazi ukubona ubuso"</string>
     <string name="keyguard_suggest_fingerprint" msgid="8742015961962702960">"Kunalokho sebenzisa isigxivizo somunwe"</string>
+    <!-- no translation found for keyguard_face_unlock_unavailable (8145547300240405980) -->
+    <skip />
     <string name="accessibility_bluetooth_connected" msgid="4745196874551115205">"Bluetooth ixhunyiwe"</string>
     <string name="accessibility_battery_unknown" msgid="1807789554617976440">"Iphesenti lebhethri alaziwa."</string>
     <string name="accessibility_bluetooth_name" msgid="7300973230214067678">"Xhuma ku-<xliff:g id="BLUETOOTH">%s</xliff:g>."</string>
@@ -248,7 +250,7 @@
     <string name="quick_settings_brightness_dialog_title" msgid="4980669966716685588">"Ukugqama"</string>
     <string name="quick_settings_inversion_label" msgid="3501527749494755688">"Ukuguqulwa kombala"</string>
     <string name="quick_settings_color_correction_label" msgid="5636617913560474664">"Ukulungiswa kombala"</string>
-    <string name="quick_settings_more_user_settings" msgid="1064187451100861954">"Amasethingi womsebenzisi"</string>
+    <string name="quick_settings_more_user_settings" msgid="7634653308485206306">"Phatha abasebenzisi"</string>
     <string name="quick_settings_done" msgid="2163641301648855793">"Kwenziwe"</string>
     <string name="quick_settings_close_user_panel" msgid="5599724542275896849">"Vala"</string>
     <string name="quick_settings_connected" msgid="3873605509184830379">"Ixhunyiwe"</string>
@@ -303,6 +305,28 @@
     <string name="sensor_privacy_mic_unblocked_toast_content" msgid="306555320557065068">"Imakrofoni iyatholakala"</string>
     <string name="sensor_privacy_camera_unblocked_toast_content" msgid="7843105715964332311">"Ikhamera iyatholakala"</string>
     <string name="sensor_privacy_mic_camera_unblocked_toast_content" msgid="7339355093282661115">"Imakrofoni nekhamera kuyatholakala"</string>
+    <!-- no translation found for sensor_privacy_mic_turned_on_dialog_title (6348853159838376513) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_turned_off_dialog_title (5760464281790732849) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_unblocked_dialog_content (4889961886199270224) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_no_exception_dialog_content (5864898470772965394) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_mic_blocked_with_exception_dialog_content (810289713700437896) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_on_dialog_title (8039095295100075952) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_turned_off_dialog_title (1936603903120742696) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_unblocked_dialog_content (7847190103011782278) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_camera_blocked_dialog_content (3182428709314874616) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_htt_blocked_dialog_content (3333321592997666441) -->
+    <skip />
+    <!-- no translation found for sensor_privacy_dialog_open_settings (1503088305279285048) -->
+    <skip />
     <string name="media_seamless_other_device" msgid="4654849800789196737">"Enye idivayisi"</string>
     <string name="quick_step_accessibility_toggle_overview" msgid="7908949976727578403">"Guqula ukubuka konke"</string>
     <string name="zen_priority_introduction" msgid="3159291973383796646">"Ngeke uphazanyiswe imisindo nokudlidliza, ngaphandle kusukela kuma-alamu, izikhumbuzi, imicimbi, nabafonayo obacacisayo. Usazozwa noma yini okhetha ukuyidlala okufaka umculo, amavidiyo, namageyimu."</string>
@@ -373,6 +397,16 @@
     <string name="media_projection_permission_dialog_warning_single_app" msgid="1659532781536753059">"Uma wabelana, urekhoda, noma usakaza i-app, i-<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> inokufinyelela kunoma yini eboniswayo noma edlalwayo kuleyo app. Ngakho-ke qaphela amagama ayimfihlo, imininingwane yokukhokha, imiyalezo, noma olunye ulwazi olubucayi."</string>
     <string name="media_projection_permission_dialog_continue" msgid="1827799658916736006">"Qhubeka"</string>
     <string name="media_projection_permission_app_selector_title" msgid="894251621057480704">"Yabelana noma rekhoda i-app"</string>
+    <!-- no translation found for media_projection_permission_dialog_system_service_title (6827129613741303726) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_entire_screen (8801616203805837575) -->
+    <skip />
+    <!-- no translation found for media_projection_permission_dialog_system_service_warning_single_app (543310680568419338) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_title (2113331792064527203) -->
+    <skip />
+    <!-- no translation found for screen_capturing_disabled_by_policy_dialog_description (6015975736747696431) -->
+    <skip />
     <string name="clear_all_notifications_text" msgid="348312370303046130">"Sula konke"</string>
     <string name="manage_notifications_text" msgid="6885645344647733116">"Phatha"</string>
     <string name="manage_notifications_history_text" msgid="57055985396576230">"Umlando"</string>
@@ -727,6 +761,10 @@
     <string name="mobile_data_disable_title" msgid="5366476131671617790">"Vala idatha yeselula?"</string>
     <string name="mobile_data_disable_message" msgid="8604966027899770415">"Ngeke ube nokufinyelela kudatha noma ku-inthanethi nge-<xliff:g id="CARRIER">%s</xliff:g>. I-inthanethi izotholakala kuphela nge-Wi-Fi."</string>
     <string name="mobile_data_disable_message_default_carrier" msgid="6496033312431658238">"inkampani yakho yenethiwekhi"</string>
+    <string name="auto_data_switch_disable_title" msgid="5146527155665190652">"Shintshela emuva ku-<xliff:g id="CARRIER">%s</xliff:g>?"</string>
+    <string name="auto_data_switch_disable_message" msgid="5885533647399535852">"Idatha yeselula ngeke ishintshe ngokuzenzakalelayo ngokusekelwe ekutholakaleni"</string>
+    <string name="auto_data_switch_dialog_negative_button" msgid="2370876875999891444">"Cha ngiyabonga"</string>
+    <string name="auto_data_switch_dialog_positive_button" msgid="8531782041263087564">"Yebo, shintsha"</string>
     <string name="touch_filtered_warning" msgid="8119511393338714836">"Ngoba uhlelo lokusebenza lusitha isicelo semvume, Izilungiselelo azikwazi ukuqinisekisa impendulo yakho."</string>
     <string name="slice_permission_title" msgid="3262615140094151017">"Vumela i-<xliff:g id="APP_0">%1$s</xliff:g> ukuthi ibonise izingcezu ze-<xliff:g id="APP_2">%2$s</xliff:g>?"</string>
     <string name="slice_permission_text_1" msgid="6675965177075443714">"- Ingafunda ulwazi kusukela ku-<xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -785,12 +823,15 @@
     <string name="accessibility_magnification_magnifier_window_settings" msgid="2834685072221468434">"Amasethingi ewindi lesikhulisi"</string>
     <string name="accessibility_floating_button_migration_tooltip" msgid="5217151214439341902">"Thepha ukuze uvule izakhi zokufinyelela. Enza ngendlela oyifisayo noma shintsha le nkinobho Kumasethingi.\n\n"<annotation id="link">"Buka amasethingi"</annotation></string>
     <string name="accessibility_floating_button_docking_tooltip" msgid="6814897496767461517">"Hambisa inkinobho onqenqemeni ukuze uyifihle okwesikhashana"</string>
+    <string name="accessibility_floating_button_undo" msgid="511112888715708241">"Hlehlisa"</string>
+    <string name="accessibility_floating_button_undo_message_text" msgid="3044079592757099698">"{count,plural, =1{Isinqamuleli se-{label} sisusiwe}one{Izinqamuleli ezingu-# zikhishiwe}other{Izinqamuleli ezingu-# zikhishiwe}}"</string>
     <string name="accessibility_floating_button_action_move_top_left" msgid="6253520703618545705">"Hamba phezulu kwesokunxele"</string>
     <string name="accessibility_floating_button_action_move_top_right" msgid="6106225581993479711">"Hamba phezulu ngakwesokudla"</string>
     <string name="accessibility_floating_button_action_move_bottom_left" msgid="8063394111137429725">"Hamba phansi ngakwesokunxele"</string>
     <string name="accessibility_floating_button_action_move_bottom_right" msgid="6196904373227440500">"Hamba phansi ngakwesokudla"</string>
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half" msgid="662401168245782658">"Hamba onqenqemeni ufihle"</string>
     <string name="accessibility_floating_button_action_move_out_edge_and_show" msgid="8354760891651663326">"Phuma onqenqemeni ubonise"</string>
+    <string name="accessibility_floating_button_action_remove_menu" msgid="6730432848162552135">"Susa"</string>
     <string name="accessibility_floating_button_action_double_tap_to_toggle" msgid="7976492639670692037">"guqula"</string>
     <string name="quick_controls_title" msgid="6839108006171302273">"Izilawuli zezinsiza"</string>
     <string name="controls_providers_title" msgid="6879775889857085056">"Khetha uhlelo lokusebenza ukwengeza izilawuli"</string>
@@ -933,6 +974,8 @@
     <string name="mobile_data_settings_title" msgid="3955246641380064901">"Idatha yeselula"</string>
     <string name="preference_summary_default_combination" msgid="8453246369903749670">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="NETWORKMODE">%2$s</xliff:g>"</string>
     <string name="mobile_data_connection_active" msgid="944490013299018227">"Ixhunyiwe"</string>
+    <string name="mobile_data_temp_connection_active" msgid="4590222725908806824">"Ixhume okwesikhashana"</string>
+    <string name="mobile_data_poor_connection" msgid="819617772268371434">"Uxhumo olungeluhle"</string>
     <string name="mobile_data_off_summary" msgid="3663995422004150567">"Idatha yeselula ngeke ikwazi ukuxhuma ngokuzenzekelayo"</string>
     <string name="mobile_data_no_connection" msgid="1713872434869947377">"Alukho uxhumano"</string>
     <string name="non_carrier_network_unavailable" msgid="770049357024492372">"Awekho amanye amanethiwekhi atholakalayo"</string>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 9a71995..44ba3f6 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -191,5 +191,23 @@
     <declare-styleable name="DelayableMarqueeTextView">
         <attr name="marqueeDelay" format="integer" />
     </declare-styleable>
+
+    <declare-styleable name="AuthCredentialView">
+        <attr name="lockPatternStyle" format="reference" />
+        <attr name="lockPinPasswordStyle" format="reference" />
+        <attr name="containerStyle" format="reference" />
+        <attr name="headerStyle" format="reference" />
+        <attr name="headerIconStyle" format="reference" />
+        <attr name="titleTextAppearance" format="reference" />
+        <attr name="subTitleTextAppearance" format="reference" />
+        <attr name="descriptionTextAppearance" format="reference" />
+        <attr name="passwordTextAppearance" format="reference" />
+        <attr name="errorTextAppearance" format="reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="LogAccessPermissionGrantDialog">
+        <attr name="permissionGrantButtonTopStyle" format="reference"/>
+        <attr name="permissionGrantButtonBottomStyle" format="reference"/>
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/bools.xml b/packages/SystemUI/res/values/bools.xml
index c67ac8d..04fc4b8 100644
--- a/packages/SystemUI/res/values/bools.xml
+++ b/packages/SystemUI/res/values/bools.xml
@@ -18,6 +18,16 @@
 <resources>
     <!-- Whether to show the user switcher in quick settings when only a single user is present. -->
     <bool name="qs_show_user_switcher_for_single_user">false</bool>
+
     <!-- Whether to show a custom biometric prompt size-->
     <bool name="use_custom_bp_size">false</bool>
+
+    <!-- Whether to enable clipping on Quick Settings -->
+    <bool name="qs_enable_clipping">true</bool>
+
+    <!-- Whether to enable clipping on Notification Views -->
+    <bool name="notification_enable_clipping">true</bool>
+
+    <!-- Whether to enable transparent background for notification scrims -->
+    <bool name="notification_scrim_transparent">false</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 9e8bef0..6b4bea1 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -172,6 +172,10 @@
     <color name="accessibility_magnifier_bg">#FCFCFC</color>
     <color name="accessibility_magnifier_bg_stroke">#E0E0E0</color>
     <color name="accessibility_magnifier_icon_color">#252525</color>
+    <color name="accessibility_window_magnifier_button_bg">#0680FD</color>
+    <color name="accessibility_window_magnifier_icon_color">#FAFAFA</color>
+    <color name="accessibility_window_magnifier_button_bg_stroke">#252525</color>
+    <color name="accessibility_window_magnifier_corner_view_color">#0680FD</color>
 
     <!-- Volume dialog colors -->
     <color name="volume_dialog_background_color">@android:color/transparent</color>
@@ -219,6 +223,8 @@
     <!-- Accessibility floating menu -->
     <color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
     <color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% -->
+    <color name="accessibility_floating_menu_message_background">@*android:color/background_material_light</color>
+    <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_light</color>
 
     <!-- Wallet screen -->
     <color name="wallet_card_border">#33FFFFFF</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9188ce0..7a36204 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -485,6 +485,12 @@
     <!-- Whether to show a severe low battery dialog. -->
     <bool name="config_severe_battery_dialog">false</bool>
 
+    <!-- A path representing a shield. Will sometimes be displayed with the battery icon when
+         needed. This path is a 10px wide and 13px tall. -->
+    <string name="config_batterymeterShieldPath" translatable="false">
+        M5 0L0 1.88V6.19C0 9.35 2.13 12.29 5 13.01C7.87 12.29 10 9.35 10 6.19V1.88L5 0Z
+    </string>
+
     <!-- A path similar to frameworks/base/core/res/res/values/config.xml
       config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display
       cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then
@@ -643,6 +649,18 @@
         <item>26</item> <!-- MOUTH_COVERING_DETECTED -->
     </integer-array>
 
+    <!-- Which device wake-ups will trigger face auth. These values correspond with
+         PowerManager#WakeReason. -->
+    <integer-array name="config_face_auth_wake_up_triggers">
+        <item>1</item> <!-- WAKE_REASON_POWER_BUTTON -->
+        <item>4</item> <!-- WAKE_REASON_GESTURE -->
+        <item>6</item> <!-- WAKE_REASON_WAKE_KEY -->
+        <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
+        <item>9</item> <!-- WAKE_REASON_LID -->
+        <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
+        <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
+    </integer-array>
+
     <!-- Whether the communal service should be enabled -->
     <bool name="config_communalServiceEnabled">false</bool>
 
@@ -725,12 +743,46 @@
     <!-- How long in milliseconds before full burn-in protection is achieved. -->
     <integer name="config_dreamOverlayMillisUntilFullJitter">240000</integer>
 
+    <!-- The duration in milliseconds of the y-translation animation when waking up from
+         the dream -->
+    <integer name="config_dreamOverlayOutTranslationYDurationMs">333</integer>
+    <!-- The delay in milliseconds of the y-translation animation when waking up from
+         the dream for the complications at the bottom of the screen -->
+    <integer name="config_dreamOverlayOutTranslationYDelayBottomMs">33</integer>
+    <!-- The delay in milliseconds of the y-translation animation when waking up from
+         the dream for the complications at the top of the screen -->
+    <integer name="config_dreamOverlayOutTranslationYDelayTopMs">117</integer>
+    <!-- The duration in milliseconds of the alpha animation when waking up from the dream -->
+    <integer name="config_dreamOverlayOutAlphaDurationMs">200</integer>
+    <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
+         complications at the top of the screen -->
+    <integer name="config_dreamOverlayOutAlphaDelayTopMs">217</integer>
+    <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
+         complications at the bottom of the screen -->
+    <integer name="config_dreamOverlayOutAlphaDelayBottomMs">133</integer>
+    <!-- The duration in milliseconds of the blur animation when waking up from
+         the dream -->
+    <integer name="config_dreamOverlayOutBlurDurationMs">250</integer>
+
     <integer name="complicationFadeOutMs">500</integer>
 
     <integer name="complicationFadeInMs">500</integer>
 
     <integer name="complicationRestoreMs">1000</integer>
 
+    <integer name="complicationFadeOutDelayMs">200</integer>
+
+    <!-- Duration in milliseconds of the dream in un-blur animation. -->
+    <integer name="config_dreamOverlayInBlurDurationMs">249</integer>
+    <!-- Delay in milliseconds of the dream in un-blur animation. -->
+    <integer name="config_dreamOverlayInBlurDelayMs">133</integer>
+    <!-- Duration in milliseconds of the dream in complications fade-in animation. -->
+    <integer name="config_dreamOverlayInComplicationsDurationMs">282</integer>
+    <!-- Delay in milliseconds of the dream in top complications fade-in animation. -->
+    <integer name="config_dreamOverlayInTopComplicationsDelayMs">216</integer>
+    <!-- Delay in milliseconds of the dream in bottom complications fade-in animation. -->
+    <integer name="config_dreamOverlayInBottomComplicationsDelayMs">299</integer>
+
     <!-- Icons that don't show in a collapsed non-keyguard statusbar -->
     <string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false">
         <item>@*android:string/status_bar_volume</item>
@@ -751,24 +803,27 @@
         <item>com.android.systemui</item>
     </string-array>
 
-    <!-- The thresholds which determine the color used by the AQI dream overlay.
-         NOTE: This must always be kept sorted from low to high -->
-    <integer-array name="config_dreamAqiThresholds">
-        <item>-1</item>
-        <item>50</item>
-        <item>100</item>
-        <item>150</item>
-        <item>200</item>
-        <item>300</item>
-    </integer-array>
+    <!-- Whether the device should display hotspot UI. If true, UI will display only when tethering
+         is available. If false, UI will never show regardless of tethering availability" -->
+    <bool name="config_show_wifi_tethering">true</bool>
 
-    <!-- The color values which correspond to the thresholds above -->
-    <integer-array name="config_dreamAqiColorValues">
-        <item>@color/dream_overlay_aqi_good</item>
-        <item>@color/dream_overlay_aqi_moderate</item>
-        <item>@color/dream_overlay_aqi_unhealthy_sensitive</item>
-        <item>@color/dream_overlay_aqi_unhealthy</item>
-        <item>@color/dream_overlay_aqi_very_unhealthy</item>
-        <item>@color/dream_overlay_aqi_hazardous</item>
-    </integer-array>
+    <!-- A collection of "slots" for placing quick affordance actions on the lock screen when the
+    device is locked. Each item is a string consisting of two parts, separated by the ':' character.
+    The first part is the unique ID for the slot, it is not a human-visible name, but should still
+    be unique across all slots specified. The second part is the capacity and must be a positive
+    integer; this is how many quick affordance actions that user is allowed to add to the slot. -->
+    <string-array name="config_keyguardQuickAffordanceSlots" translatable="false">
+        <item>bottom_start:1</item>
+        <item>bottom_end:1</item>
+    </string-array>
+
+    <!-- A collection of defaults for the quick affordances on the lock screen. Each item must be a
+    string with two parts: the ID of the slot and the comma-delimited list of affordance IDs,
+    separated by a colon ':' character. For example: <item>bottom_end:home,wallet</item>. The
+    default is displayed by System UI as long as the user hasn't made a different choice for that
+    slot. If the user did make a choice, even if the choice is the "None" option, the default is
+    ignored. -->
+    <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
+    </string-array>
+
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 01c9ac1..437d89b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -105,6 +105,12 @@
     so the width of the icon should be 13.0dp * (12.0 / 20.0) -->
     <dimen name="status_bar_battery_icon_width">7.8dp</dimen>
 
+    <!-- The battery icon is 13dp tall, but the other system icons are 15dp tall (see
+         @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in
+         the drawables themselves. So, the battery icon may need an extra 1dp of spacing so that its
+         bottom still aligns with the bottom of all the other system icons. See b/258672854. -->
+    <dimen name="status_bar_battery_extra_vertical_spacing">1dp</dimen>
+
     <!-- The font size for the clock in the status bar. -->
     <dimen name="status_bar_clock_size">14sp</dimen>
 
@@ -407,7 +413,7 @@
     <dimen name="match_parent">-1px</dimen>
 
     <!-- Height of status bar in split shade mode - visible only on large screens -->
-    <dimen name="large_screen_shade_header_height">@*android:dimen/quick_qs_offset_height</dimen>
+    <dimen name="large_screen_shade_header_height">48dp</dimen>
     <dimen name="large_screen_shade_header_min_height">@dimen/qs_header_row_min_height</dimen>
     <dimen name="large_screen_shade_header_left_padding">@dimen/qs_horizontal_margin</dimen>
 
@@ -519,7 +525,7 @@
     <dimen name="qs_tile_margin_horizontal">8dp</dimen>
     <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen>
     <dimen name="qs_tile_margin_top_bottom">4dp</dimen>
-    <dimen name="qs_brightness_margin_top">8dp</dimen>
+    <dimen name="qs_brightness_margin_top">12dp</dimen>
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
     <dimen name="qqs_layout_margin_top">16dp</dimen>
     <dimen name="qqs_layout_padding_bottom">24dp</dimen>
@@ -572,6 +578,7 @@
     <dimen name="qs_header_row_min_height">48dp</dimen>
 
     <dimen name="qs_header_non_clickable_element_height">24dp</dimen>
+    <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
     <dimen name="qs_security_footer_height">88dp</dimen>
@@ -761,7 +768,7 @@
     <dimen name="keyguard_lock_padding">20dp</dimen>
 
     <dimen name="keyguard_indication_margin_bottom">32dp</dimen>
-    <dimen name="lock_icon_margin_bottom">110dp</dimen>
+    <dimen name="lock_icon_margin_bottom">74dp</dimen>
     <dimen name="ambient_indication_margin_bottom">71dp</dimen>
 
 
@@ -1335,6 +1342,14 @@
     <dimen name="accessibility_floating_menu_large_single_radius">35dp</dimen>
     <dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen>
 
+    <dimen name="accessibility_floating_menu_message_container_horizontal_padding">15dp</dimen>
+    <dimen name="accessibility_floating_menu_message_text_vertical_padding">8dp</dimen>
+    <dimen name="accessibility_floating_menu_message_margin">8dp</dimen>
+    <dimen name="accessibility_floating_menu_message_elevation">5dp</dimen>
+    <dimen name="accessibility_floating_menu_message_text_size">14sp</dimen>
+    <dimen name="accessibility_floating_menu_message_min_width">312dp</dimen>
+    <dimen name="accessibility_floating_menu_message_min_height">48dp</dimen>
+
     <dimen name="accessibility_floating_tooltip_arrow_width">8dp</dimen>
     <dimen name="accessibility_floating_tooltip_arrow_height">16dp</dimen>
     <dimen name="accessibility_floating_tooltip_arrow_margin">-2dp</dimen>
@@ -1407,6 +1422,11 @@
     <dimen name="ongoing_call_chip_icon_text_padding">4dp</dimen>
     <dimen name="ongoing_call_chip_corner_radius">28dp</dimen>
 
+    <!-- Status bar user chip -->
+    <dimen name="status_bar_user_chip_avatar_size">16dp</dimen>
+    <dimen name="status_bar_user_chip_end_margin">12dp</dimen>
+    <dimen name="status_bar_user_chip_text_size">12sp</dimen>
+
     <!-- Internet panel related dimensions -->
     <dimen name="internet_dialog_list_max_height">662dp</dimen>
     <!-- The height of the WiFi network in Internet panel. -->
@@ -1487,10 +1507,12 @@
 
     <!-- Dream overlay complications related dimensions -->
     <dimen name="dream_overlay_complication_clock_time_text_size">86sp</dimen>
+    <dimen name="dream_overlay_complication_clock_time_padding">20dp</dimen>
     <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen>
     <dimen name="dream_overlay_complication_preview_text_size">36sp</dimen>
     <dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen>
     <dimen name="dream_overlay_complication_shadow_padding">2dp</dimen>
+    <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen>
 
     <!-- The position of the end guide, which dream overlay complications can align their start with
          if their end is aligned with the parent end. Represented as the percentage over from the
@@ -1535,6 +1557,7 @@
     <dimen name="dream_overlay_complication_margin">0dp</dimen>
 
     <dimen name="dream_overlay_y_offset">80dp</dimen>
+    <dimen name="dream_overlay_exit_y_offset">40dp</dimen>
 
     <dimen name="dream_aqi_badge_corner_radius">28dp</dimen>
     <dimen name="dream_aqi_badge_padding_vertical">6dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 7ca42f7..2b6ab30 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"/>
@@ -194,5 +195,7 @@
     <item type="id" name="pm_lite"/>
     <item type="id" name="settings_button_container"/>
 
+    <item type="id" name="log_access_dialog_allow_button" />
+    <item type="id" name="log_access_dialog_deny_button" />
 </resources>
 
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index 3164ed1..8d44315 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -28,4 +28,13 @@
 
     <!-- The time it takes for the over scroll release animation to complete, in milli seconds.  -->
     <integer name="lockscreen_shade_over_scroll_release_duration">0</integer>
+
+    <!-- Values for transition of QS Headers -->
+    <integer name="fade_out_complete_frame">14</integer>
+    <integer name="fade_in_start_frame">58</integer>
+    <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at
+         fade_out_complete_frame -->
+    <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen>
+
+    <integer name="qs_carrier_max_em">7</integer>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 637ac19..9eafdb9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -142,7 +142,7 @@
     <string name="usb_debugging_secondary_user_title">USB debugging not allowed</string>
 
     <!-- Message of notification shown when trying to enable USB debugging but a secondary user is the current foreground user. -->
-    <string name="usb_debugging_secondary_user_message">The user currently signed in to this device can\'t turn on USB debugging. To use this feature, switch to the primary user.</string>
+    <string name="usb_debugging_secondary_user_message">The user currently signed in to this device can\'t turn on USB debugging. To use this feature, switch to an admin user.</string>
 
     <!-- Title of confirmation dialog for wireless debugging [CHAR LIMIT=80] -->
     <string name="hdmi_cec_set_menu_language_title">Do you want to change the system language to <xliff:g id="language" example="German">%1$s</xliff:g>?</string>
@@ -172,7 +172,7 @@
     <string name="wifi_debugging_secondary_user_title">Wireless debugging not allowed</string>
 
     <!-- Message of notification shown when trying to enable wireless debugging but a secondary user is the current foreground user. [CHAR LIMIT=NONE] -->
-    <string name="wifi_debugging_secondary_user_message">The user currently signed in to this device can\u2019t turn on wireless debugging. To use this feature, switch to the primary user.</string>
+    <string name="wifi_debugging_secondary_user_message">The user currently signed in to this device can\u2019t turn on wireless debugging. To use this feature, switch to an admin user.</string>
 
     <!-- Title of USB contaminant presence dialog [CHAR LIMIT=NONE] -->
     <string name="usb_contaminant_title">USB port disabled</string>
@@ -235,6 +235,8 @@
     <string name="screenshot_left_boundary_pct">Left boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
     <!-- Content description for the right boundary of the screenshot being cropped, with the current position as a percentage. [CHAR LIMIT=NONE] -->
     <string name="screenshot_right_boundary_pct">Right boundary <xliff:g id="percent" example="50">%1$d</xliff:g> percent</string>
+    <!-- Notification displayed when a screenshot is saved in a work profile. [CHAR LIMIT=NONE] -->
+    <string name="screenshot_work_profile_notification" translatable="false">Work screenshots are saved in the work <xliff:g id="app" example="Files">%1$s</xliff:g> app</string>
 
     <!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
     <string name="screenrecord_name">Screen Recorder</string>
@@ -314,7 +316,7 @@
     <!-- Content description of the QR Code scanner for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_qr_code_scanner_button">QR Code Scanner</string>
     <!-- Content description of the unlock button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_unlock_button">Unlock</string>
+    <string name="accessibility_unlock_button">Unlocked</string>
     <!-- Content description of the lock icon for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_lock_icon">Device locked</string>
     <!-- Content description hint of the unlock button when fingerprint is on (not shown on the screen). [CHAR LIMIT=NONE] -->
@@ -403,6 +405,8 @@
     <string name="keyguard_face_failed">Can\u2019t recognize face</string>
     <!-- Message shown to suggest using fingerprint sensor to authenticate after another biometric failed. [CHAR LIMIT=25] -->
     <string name="keyguard_suggest_fingerprint">Use fingerprint instead</string>
+    <!-- Message shown to inform the user that face unlock is not available. [CHAR LIMIT=59] -->
+    <string name="keyguard_face_unlock_unavailable">Face Unlock unavailable</string>
 
     <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_bluetooth_connected">Bluetooth connected.</string>
@@ -435,11 +439,17 @@
     <string name="accessibility_battery_level">Battery <xliff:g id="number">%d</xliff:g> percent.</string>
 
     <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$s</xliff:g> percent, about <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g> left based on your usage</string>
+    <string name="accessibility_battery_level_with_estimate">Battery <xliff:g id="percentage" example="95%">%1$d</xliff:g> percent, <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g></string>
 
     <!-- Content description of the battery level icon for accessibility while the device is charging (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_battery_level_charging">Battery charging, <xliff:g id="battery_percentage">%d</xliff:g> percent.</string>
 
+    <!-- Content description of the battery level icon for accessibility, with information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_battery_level_charging_paused">Battery <xliff:g id="percentage" example="90%">%d</xliff:g> percent, charging paused for battery protection.</string>
+
+    <!-- Content description of the battery level icon for accessibility, including the estimated time remaining before the phone runs out of battery *and* information that the device charging is paused in order to protect the lifetime of the battery (not shown on screen). [CHAR LIMIT=NONE] -->
+    <string name="accessibility_battery_level_charging_paused_with_estimate">Battery <xliff:g id="percentage" example="90%">%1$d</xliff:g> percent, <xliff:g id="time" example="Until 3:15pm">%2$s</xliff:g>, charging paused for battery protection.</string>
+
     <!-- Content description of overflow icon container of the notifications for accessibility (not shown on the screen)[CHAR LIMIT=NONE] -->
     <string name="accessibility_overflow_action">See all notifications</string>
 
@@ -641,7 +651,7 @@
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_color_correction_label">Color correction</string>
     <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] -->
-    <string name="quick_settings_more_user_settings">User settings</string>
+    <string name="quick_settings_more_user_settings">Manage users</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_done">Done</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] -->
@@ -783,6 +793,62 @@
         Microphone and camera available
     </string>
 
+    <!--- Title of dialog triggered if the microphone sensor privacy changed its state to unblocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_mic_turned_on_dialog_title">
+        Microphone turned on
+    </string>
+
+    <!--- Title of dialog triggered if the microphone sensor privacy changed its state to blocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_mic_turned_off_dialog_title">
+        Microphone turned off
+    </string>
+
+    <!--- Content of dialog triggered if the microphone sensor privacy changed its state to unblocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_mic_unblocked_dialog_content">
+        Microphone is enabled for all apps and services.
+    </string>
+
+    <!--- Content of dialog triggered if the microphone sensor privacy changed its state to blocked.
+        and there are no active exceptions for explicit user interaction [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_mic_blocked_no_exception_dialog_content">
+        Microphone access is disabled for all apps and services.
+        You can enable microphone access in Settings &gt; Privacy &gt; Microphone.
+    </string>
+
+    <!--- Content of dialog triggered if the microphone sensor privacy changed its state to blocked.
+        and there are active exceptions for explicit user interaction [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_mic_blocked_with_exception_dialog_content">
+        Microphone access is disabled for all apps and services.
+        You can change this in Settings &gt; Privacy &gt; Microphone.
+    </string>
+
+    <!--- Title of dialog triggered if the camera sensor privacy changed its state to unblocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_camera_turned_on_dialog_title">
+        Camera turned on
+    </string>
+
+    <!--- Title of dialog triggered if the camera sensor privacy changed its state to blocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_camera_turned_off_dialog_title">
+        Camera turned off
+    </string>
+
+    <!--- Content of dialog triggered if the camera sensor privacy changed its state to unblocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_camera_unblocked_dialog_content">
+        Camera is enabled for all apps and services.
+    </string>
+
+    <!--- Content of dialog triggered if the camera sensor privacy changed its state to blocked. [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_camera_blocked_dialog_content">
+        Camera access is disabled for all apps and services.
+    </string>
+
+    <!--- Content of dialog triggered if the microphone sensor privacy changed its state to blocked.
+        and there are active exceptions for explicit user interaction [CHAR LIMIT=NONE] -->
+    <string name="sensor_privacy_htt_blocked_dialog_content">To use the microphone button, enable microphone access in Settings.</string>
+
+    <!-- Sensor privacy dialog: Button to open system settings [CHAR LIMIT=50] -->
+    <string name="sensor_privacy_dialog_open_settings">Open settings.</string>
+
     <!-- Default name for the media device shown in the output switcher when the name is not available [CHAR LIMIT=30] -->
     <string name="media_seamless_other_device">Other device</string>
 
@@ -989,6 +1055,21 @@
     <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] -->
     <string name="media_projection_permission_app_selector_title">Share or record an app</string>
 
+    <!-- Media projection permission dialog title when there is no app name (e.g. it could be a system service when casting). [CHAR LIMIT=100] -->
+    <string name="media_projection_permission_dialog_system_service_title">Allow this app to share or record?</string>
+
+    <!-- Media projection permission warning for capturing the whole screen when a system service requests it (e.g. when casting). [CHAR LIMIT=350] -->
+    <string name="media_projection_permission_dialog_system_service_warning_entire_screen">When you\'re sharing, recording, or casting, this app has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+    <!-- Media projection permission warning for capturing a single app when a system service requests it (e.g. when casting). [CHAR LIMIT=350] -->
+    <string name="media_projection_permission_dialog_system_service_warning_single_app">When you\'re sharing, recording, or casting an app, this app has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+    <!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
+    <string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>
+
+    <!-- Description for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=350] -->
+    <string name="screen_capturing_disabled_by_policy_dialog_description">Screen capturing is disabled by device policy</string>
+
     <!-- The text to clear all notifications. [CHAR LIMIT=60] -->
     <string name="clear_all_notifications_text">Clear all</string>
 
@@ -1294,7 +1375,7 @@
     <string name="wallet_lockscreen_settings_label">Lock screen settings</string>
 
     <!-- QR Code Scanner label, title [CHAR LIMIT=32] -->
-    <string name="qr_code_scanner_title">Scan QR code</string>
+    <string name="qr_code_scanner_title">QR code scanner</string>
 
     <!-- Name of the work status bar icon. -->
     <string name="status_bar_work">Work profile</string>
@@ -1915,6 +1996,9 @@
     <!-- SysUI Tuner: Summary of no shortcut being selected [CHAR LIMIT=60] -->
     <string name="lockscreen_none">None</string>
 
+    <!-- ClockId to use when none is set by user -->
+    <string name="lockscreen_clock_id_fallback" translatable="false">DEFAULT</string>
+
     <!-- SysUI Tuner: Format string for describing launching an app [CHAR LIMIT=60] -->
     <string name="tuner_launch_app">Launch <xliff:g id="app" example="Settings">%1$s</xliff:g></string>
 
@@ -2022,6 +2106,15 @@
     <!-- Text used to refer to the user's current carrier in mobile_data_disable_message if the users's mobile network carrier name is not available [CHAR LIMIT=NONE] -->
     <string name="mobile_data_disable_message_default_carrier">your carrier</string>
 
+    <!-- Title of the dialog to turn off data usage [CHAR LIMIT=NONE] -->
+    <string name="auto_data_switch_disable_title">Switch back to <xliff:g id="carrier" example="T-Mobile">%s</xliff:g>?</string>
+    <!-- Message body of the dialog to turn off data usage [CHAR LIMIT=NONE] -->
+    <string name="auto_data_switch_disable_message">Mobile data won\’t automatically switch based on availability</string>
+    <!-- Negative button title of the quick settings switch back to DDS dialog [CHAR LIMIT=NONE] -->
+    <string name="auto_data_switch_dialog_negative_button">No thanks</string>
+    <!-- Positive button title of the quick settings switch back to DDS dialog [CHAR LIMIT=NONE] -->
+    <string name="auto_data_switch_dialog_positive_button">Yes, switch</string>
+
     <!-- Warning shown when user input has been blocked due to another app overlaying screen
      content. Since we don't know what the app is showing on top of the input target, we
      can't verify user consent. [CHAR LIMIT=NONE] -->
@@ -2147,6 +2240,8 @@
     <string name="magnification_mode_switch_state_window">Magnify part of screen</string>
     <!-- Click action label for magnification switch. [CHAR LIMIT=NONE] -->
     <string name="magnification_mode_switch_click_label">Switch</string>
+    <!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] -->
+    <string name="magnification_drag_corner_to_resize">Drag corner to resize</string>
 
     <!-- Title of the magnification option button allow diagonal scrolling [CHAR LIMIT=NONE]-->
     <string name="accessibility_allow_diagonal_scrolling">Allow diagonal scrolling</string>
@@ -2188,6 +2283,15 @@
     <string name="accessibility_floating_button_migration_tooltip">Tap to open accessibility features. Customize or replace this button in Settings.\n\n<annotation id="link">View settings</annotation></string>
     <!-- Message for the accessibility floating button docking tooltip. It shows when the user first time drag the button. It will tell the user about docking behavior. [CHAR LIMIT=70] -->
     <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
+    <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
+    <string name="accessibility_floating_button_undo">Undo</string>
+
+    <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
+    <string name="accessibility_floating_button_undo_message_text">{count, plural,
+        =1 {{label} shortcut removed}
+        other {# shortcuts removed}
+    }</string>
+
     <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] -->
     <string name="accessibility_floating_button_action_move_top_left">Move top left</string>
     <!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] -->
@@ -2200,6 +2304,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>
 
@@ -2527,6 +2633,12 @@
          Summary indicating that a SIM has an active mobile data connection [CHAR LIMIT=50] -->
     <string name="mobile_data_connection_active">Connected</string>
     <!-- Provider Model:
+         Summary indicating that a SIM is temporarily connected to mobile data [CHAR LIMIT=50] -->
+    <string name="mobile_data_temp_connection_active">Temporarily connected</string>
+    <!-- Provider Model:
+     Summary indicating that a SIM is temporarily connected to mobile data [CHAR LIMIT=50] -->
+    <string name="mobile_data_poor_connection">Poor connection</string>
+    <!-- Provider Model:
      Summary indicating that a SIM has no mobile data connection [CHAR LIMIT=50] -->
     <string name="mobile_data_off_summary">Mobile data won\u0027t auto\u2011connect</string>
     <!-- Provider Model:
@@ -2671,4 +2783,19 @@
 
     <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] -->
     <string name="dream_time_complication_24_hr_time_format">kk:mm</string>
+
+    <!-- Title for the log access confirmation dialog. [CHAR LIMIT=NONE] -->
+    <string name="log_access_confirmation_title">Allow <xliff:g id="log_access_app_name" example="Example App">%s</xliff:g> to access all device logs?</string>
+    <!-- Label for the allow button on the log access confirmation dialog. [CHAR LIMIT=40] -->
+    <string name="log_access_confirmation_allow">Allow one-time access</string>
+    <!-- Label for the deny button on the log access confirmation dialog. [CHAR LIMIT=20] -->
+    <string name="log_access_confirmation_deny">Don\u2019t allow</string>
+
+    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
+    <string name="log_access_confirmation_body">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.
+    </string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" translatable="false">&lt;a href="https://support.google.com/android?p=system_logs#topic=7313011"&gt;Learn more&lt;/a&gt;</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a734fa7..fe4f639 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -23,6 +23,12 @@
         <item name="android:textColor">@color/status_bar_clock_color</item>
     </style>
 
+    <style name="TextAppearance.StatusBar.UserChip" parent="@*android:style/TextAppearance.StatusBar.Icon">
+        <item name="android:textSize">@dimen/status_bar_user_chip_text_size</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textColor">@color/status_bar_clock_color</item>
+    </style>
+
     <style name="TextAppearance.StatusBar.Expanded" parent="@*android:style/TextAppearance.StatusBar">
         <item name="android:textColor">?android:attr/textColorTertiary</item>
     </style>
@@ -128,11 +134,10 @@
     <!-- This is hard coded to be sans-serif-condensed to match the icons -->
 
     <style name="TextAppearance.QS.Status">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">14sp</item>
         <item name="android:letterSpacing">0.01</item>
-        <item name="android:lineHeight">20sp</item>
     </style>
 
     <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
@@ -143,12 +148,10 @@
     <style name="TextAppearance.QS.Status.Carriers" />
 
     <style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="TextAppearance.QS.Status.Build">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
@@ -198,15 +201,11 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
-    <style name="TextAppearance.AuthNonBioCredential.Icon">
-        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
-        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
-    </style>
-
     <style name="TextAppearance.AuthNonBioCredential.Title">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">36sp</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Subtitle">
@@ -218,12 +217,10 @@
     <style name="TextAppearance.AuthNonBioCredential.Description">
         <item name="android:fontFamily">google-sans</item>
         <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">16sp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Error">
-        <item name="android:paddingTop">6dp</item>
-        <item name="android:paddingBottom">18dp</item>
         <item name="android:paddingHorizontal">24dp</item>
         <item name="android:textSize">14sp</item>
         <item name="android:textColor">?android:attr/colorError</item>
@@ -242,12 +239,33 @@
     <style name="AuthCredentialHeaderStyle">
         <item name="android:paddingStart">48dp</item>
         <item name="android:paddingEnd">48dp</item>
-        <item name="android:paddingTop">28dp</item>
-        <item name="android:paddingBottom">20dp</item>
-        <item name="android:orientation">vertical</item>
+        <item name="android:paddingTop">48dp</item>
+        <item name="android:paddingBottom">10dp</item>
         <item name="android:layout_gravity">top</item>
     </style>
 
+    <style name="AuthCredentialIconStyle">
+        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
+        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:padding">20dp</item>
+    </style>
+
+    <style name="AuthCredentialPinPasswordContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">48dp</item>
+        <item name="android:maxWidth">600dp</item>
+        <item name="android:minHeight">48dp</item>
+        <item name="android:minWidth">200dp</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
@@ -285,7 +303,9 @@
         <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item>
         <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item>
         <item name="android:colorError">@*android:color/error_color_material_dark</item>
-        <item name="*android:lockPatternStyle">@style/LockPatternStyle</item>
+        <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item>
+        <item name="lockPatternStyle">@style/LockPatternContainerStyle</item>
+        <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item>
         <item name="passwordStyle">@style/PasswordTheme</item>
         <item name="numPadKeyStyle">@style/NumPadKey</item>
         <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item>
@@ -311,27 +331,33 @@
         <item name="android:textColor">?attr/wallpaperTextColor</item>
     </style>
 
-    <style name="LockPatternContainerStyle">
-        <item name="android:maxHeight">400dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">0dp</item>
-        <item name="android:minWidth">0dp</item>
-        <item name="android:paddingHorizontal">60dp</item>
-        <item name="android:paddingBottom">40dp</item>
+    <style name="AuthCredentialStyle">
+        <item name="*android:regularColor">?android:attr/colorForeground</item>
+        <item name="*android:successColor">?android:attr/colorForeground</item>
+        <item name="*android:errorColor">?android:attr/colorError</item>
+        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+        <item name="headerStyle">@style/AuthCredentialHeaderStyle</item>
+        <item name="headerIconStyle">@style/AuthCredentialIconStyle</item>
+        <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item>
+        <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item>
+        <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item>
+        <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item>
+        <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item>
     </style>
 
-    <style name="LockPatternStyle">
+    <style name="LockPatternViewStyle" >
         <item name="*android:regularColor">?android:attr/colorAccent</item>
         <item name="*android:successColor">?android:attr/textColorPrimary</item>
         <item name="*android:errorColor">?android:attr/colorError</item>
         <item name="*android:dotColor">?android:attr/textColorSecondary</item>
     </style>
 
-    <style name="LockPatternStyleBiometricPrompt">
-        <item name="*android:regularColor">?android:attr/colorForeground</item>
-        <item name="*android:successColor">?android:attr/colorForeground</item>
-        <item name="*android:errorColor">?android:attr/colorError</item>
-        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+    <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item>
+    </style>
+
+    <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item>
     </style>
 
     <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault">
@@ -1071,7 +1097,7 @@
         <item name="android:orientation">horizontal</item>
         <item name="android:focusable">true</item>
         <item name="android:clickable">true</item>
-        <item name="android:background">?android:attr/selectableItemBackground</item>
+        <item name="android:background">@drawable/internet_dialog_selected_effect</item>
     </style>
 
     <style name="InternetDialog.NetworkTitle">
@@ -1254,4 +1280,45 @@
         <item name="android:textColor">?androidprv:attr/textColorOnAccent</item>
         <item name="android:textSize">@dimen/broadcast_dialog_btn_text_size</item>
     </style>
+
+
+    <!-- The style for log access consent dialog -->
+    <style name="LogAccessDialogTheme" parent="@style/Theme.SystemUI.Dialog.Alert">
+        <item name="permissionGrantButtonTopStyle">@style/PermissionGrantButtonTop</item>
+        <item name="permissionGrantButtonBottomStyle">@style/PermissionGrantButtonBottom</item>
+    </style>
+
+    <style name="AllowLogAccess">
+        <item name="android:textSize">24sp</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+    </style>
+
+    <style name="PrimaryAllowLogAccess">
+        <item name="android:textSize">14sp</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+    </style>
+
+    <style name="PermissionGrantButtonTextAppearance">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@android:color/system_neutral1_900</item>
+    </style>
+
+    <style name="PermissionGrantButtonTop"
+           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+        <item name="android:layout_width">332dp</item>
+        <item name="android:layout_height">56dp</item>
+        <item name="android:layout_marginTop">2dp</item>
+        <item name="android:layout_marginBottom">2dp</item>
+        <item name="android:background">@drawable/grant_permissions_buttons_top</item>
+    </style>
+
+    <style name="PermissionGrantButtonBottom"
+           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+        <item name="android:layout_width">332dp</item>
+        <item name="android:layout_height">56dp</item>
+        <item name="android:layout_marginTop">2dp</item>
+        <item name="android:layout_marginBottom">2dp</item>
+        <item name="android:background">@drawable/grant_permissions_buttons_bottom</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index f3866c0..de855e2 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -27,67 +27,60 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="49"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
+                app:percentY="0.5"
                 app:sizePercent="1"
-                app:framePosition="51"
+                app:framePosition="50"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="30"
+                app:framePosition="14"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
-                app:keyPositionType="pathRelative"
+                app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="0"
-                app:curveFit="linear"
-                app:motionTarget="@id/statusIcons" />
-            <KeyPosition
-                app:keyPositionType="pathRelative"
-                app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:sizePercent="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -95,27 +88,27 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/batteryRemainingIcon" />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:motionTarget="@id/carrier_group"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -126,7 +119,7 @@
                 android:alpha="0" />
             <KeyAttribute
                 app:motionTarget="@id/carrier_group"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0" />
         </KeyFrameSet>
     </Transition>
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index cdbf8ab..06d425c 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -107,7 +107,7 @@
         android:id="@+id/privacy_container">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="0dp"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/date"
             app:layout_constraintBottom_toBottomOf="@id/date"
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 9115d42..1eb621e 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -34,6 +34,26 @@
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent" />
 
+    <!-- Touch ripple must have the same constraint as the album art. -->
+    <Constraint
+        android:id="@+id/touch_ripple_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_collapsed"
+        app:layout_constraintStart_toStartOf="@+id/album_art"
+        app:layout_constraintEnd_toEndOf="@+id/album_art"
+        app:layout_constraintTop_toTopOf="@+id/album_art"
+        app:layout_constraintBottom_toBottomOf="@+id/album_art" />
+
+    <!-- Turbulence noise must have the same constraint as the album art. -->
+    <Constraint
+        android:id="@+id/turbulence_noise_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_collapsed"
+        app:layout_constraintStart_toStartOf="@+id/album_art"
+        app:layout_constraintEnd_toEndOf="@+id/album_art"
+        app:layout_constraintTop_toTopOf="@+id/album_art"
+        app:layout_constraintBottom_toBottomOf="@+id/album_art" />
+
     <Constraint
         android:id="@+id/header_title"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml
index 522dc68..64c2ef1 100644
--- a/packages/SystemUI/res/xml/media_session_expanded.xml
+++ b/packages/SystemUI/res/xml/media_session_expanded.xml
@@ -27,6 +27,26 @@
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent" />
 
+    <!-- Touch ripple must have the same constraint as the album art. -->
+    <Constraint
+        android:id="@+id/touch_ripple_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded"
+        app:layout_constraintStart_toStartOf="@+id/album_art"
+        app:layout_constraintEnd_toEndOf="@+id/album_art"
+        app:layout_constraintTop_toTopOf="@+id/album_art"
+        app:layout_constraintBottom_toBottomOf="@+id/album_art" />
+
+    <!-- Turbulence noise must have the same constraint as the album art. -->
+    <Constraint
+        android:id="@+id/turbulence_noise_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/qs_media_session_height_expanded"
+        app:layout_constraintStart_toStartOf="@+id/album_art"
+        app:layout_constraintEnd_toEndOf="@+id/album_art"
+        app:layout_constraintTop_toTopOf="@+id/album_art"
+        app:layout_constraintBottom_toBottomOf="@+id/album_art" />
+
     <Constraint
         android:id="@+id/header_title"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index a82684d03..5d3650c 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -25,7 +25,7 @@
         android:id="@+id/clock">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="0dp"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toStartOf="@id/begin_guide"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
@@ -42,8 +42,9 @@
     <Constraint
         android:id="@+id/date">
         <Layout
-            android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            android:layout_marginStart="8dp"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
@@ -56,14 +57,16 @@
     <Constraint
         android:id="@+id/statusIcons">
         <Layout
-            android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constrainedWidth="true"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="packed"
             />
     </Constraint>
 
@@ -71,20 +74,24 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="packed"
             />
     </Constraint>
 
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
+            app:layout_constraintWidth_min="48dp"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             />
@@ -97,7 +104,7 @@
         android:id="@+id/privacy_container">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="0dp"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
             app:layout_constraintTop_toTopOf="parent"
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index f39e6bd..982c422 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -40,25 +40,26 @@
             android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
-            app:layout_constraintBottom_toTopOf="@id/date"
+            app:layout_constraintBottom_toBottomOf="@id/carrier_group"
             app:layout_constraintEnd_toStartOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
         <Transform
-            android:scaleX="2.4"
-            android:scaleY="2.4"
+            android:scaleX="2.57"
+            android:scaleY="2.57"
             />
     </Constraint>
 
     <Constraint
         android:id="@+id/date">
         <Layout
-            android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/space"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/clock"
+            app:layout_constraintTop_toBottomOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
             app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
@@ -67,16 +68,15 @@
     <Constraint
         android:id="@+id/carrier_group">
         <Layout
-            app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
-            android:minHeight="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintWidth_min="48dp"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintHorizontal_bias="1"
             app:layout_constraintBottom_toTopOf="@id/batteryRemainingIcon"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
             />
         <PropertySet
             android:alpha="1"
@@ -86,8 +86,8 @@
     <Constraint
         android:id="@+id/statusIcons">
         <Layout
-            android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/space"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
@@ -101,13 +101,14 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/date"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
+            app:layout_constraintHorizontal_chainStyle="spread_inside"
             />
     </Constraint>
 
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
index 2e391c7..49cc483 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.graphics.Color
 import android.view.View
+import android.view.Window
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
@@ -51,13 +52,14 @@
 
     /**
      * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in
-     * the context of [emulationSpec].
+     * the context of [emulationSpec]. Window must be specified to capture views that render
+     * hardware buffers.
      */
-    fun screenshotTest(goldenIdentifier: String, view: View) {
+    fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) {
         view.removeElevationRecursively()
 
         ScreenshotRuleAsserter.Builder(screenshotRule)
-            .setScreenshotProvider { view.toBitmap() }
+            .setScreenshotProvider { view.toBitmap(window) }
             .withMatcher(matcher)
             .build()
             .assertGoldenImage(goldenIdentifier)
@@ -94,6 +96,6 @@
             activity.currentFocus?.clearFocus()
         }
 
-        screenshotTest(goldenIdentifier, rootView)
+        screenshotTest(goldenIdentifier, rootView, activity.window)
     }
 }
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 0b0595f..36ac1ff 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -34,6 +34,7 @@
 import platform.test.screenshot.DeviceEmulationRule
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.MaterialYouColorsRule
+import platform.test.screenshot.PathConfig
 import platform.test.screenshot.ScreenshotTestRule
 import platform.test.screenshot.getEmulatedDevicePathConfig
 import platform.test.screenshot.matchers.BitmapMatcher
@@ -41,13 +42,19 @@
 /** A rule for View screenshot diff unit tests. */
 class ViewScreenshotTestRule(
     emulationSpec: DeviceEmulationSpec,
-    private val matcher: BitmapMatcher = UnitTestBitmapMatcher
+    private val matcher: BitmapMatcher = UnitTestBitmapMatcher,
+    pathConfig: PathConfig = getEmulatedDevicePathConfig(emulationSpec),
+    assetsPathRelativeToRepo: String = ""
 ) : TestRule {
     private val colorsRule = MaterialYouColorsRule()
     private val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
     private val screenshotRule =
         ScreenshotTestRule(
-            SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec))
+            if (assetsPathRelativeToRepo.isBlank()) {
+                SystemUIGoldenImagePathManager(pathConfig)
+            } else {
+                SystemUIGoldenImagePathManager(pathConfig, assetsPathRelativeToRepo)
+            }
         )
     private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
     private val delegateRule =
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 9040ea1..8a0fca0 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -52,19 +52,20 @@
         "SystemUIUnfoldLib",
         "androidx.dynamicanimation_dynamicanimation",
         "androidx.concurrent_concurrent-futures",
-        "gson",
+        "androidx.lifecycle_lifecycle-runtime-ktx",
+        "androidx.lifecycle_lifecycle-viewmodel-ktx",
+        "androidx.recyclerview_recyclerview",
+        "kotlinx_coroutines_android",
+        "kotlinx_coroutines",
         "dagger2",
         "jsr330",
     ],
     resource_dirs: [
         "res",
     ],
-    optimize: {
-        proguard_flags_files: ["proguard.flags"],
-    },
-    java_version: "1.8",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
+    kotlincflags: ["-Xjvm-default=enable"],
 }
 
 java_library {
diff --git a/packages/SystemUI/shared/proguard.flags b/packages/SystemUI/shared/proguard.flags
deleted file mode 100644
index 5eda045..0000000
--- a/packages/SystemUI/shared/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Retain signatures of TypeToken and its subclasses for gson usage in ClockRegistry
--keepattributes Signature
--keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
--keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
index f7049cf..196f7f0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
@@ -22,9 +22,19 @@
 import android.os.Parcel
 import android.os.Parcelable
 
+/**
+ * Base interface for flags that can change value on a running device.
+ * @property id unique id to help identify this flag. Must be unique. This will be removed soon.
+ * @property teamfood Set to true to include this flag as part of the teamfood flag. This will
+ *                    be removed soon.
+ * @property name Used for server-side flagging where appropriate. Also used for display. No spaces.
+ * @property namespace The server-side namespace that this flag lives under.
+ */
 interface Flag<T> {
     val id: Int
     val teamfood: Boolean
+    val name: String
+    val namespace: String
 }
 
 interface ParcelableFlag<T> : Flag<T>, Parcelable {
@@ -38,13 +48,10 @@
 }
 
 interface DeviceConfigFlag<T> : Flag<T> {
-    val name: String
-    val namespace: String
     val default: T
 }
 
 interface SysPropFlag<T> : Flag<T> {
-    val name: String
     val default: T
 }
 
@@ -56,6 +63,8 @@
 // Consider using the "parcelize" kotlin library.
 abstract class BooleanFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Boolean = false,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -71,6 +80,8 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readBoolean(),
         teamfood = parcel.readBoolean(),
         overridden = parcel.readBoolean()
@@ -78,6 +89,8 @@
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeBoolean(default)
         parcel.writeBoolean(teamfood)
         parcel.writeBoolean(overridden)
@@ -89,30 +102,36 @@
  *
  * It can be changed or overridden in debug builds but not in release builds.
  */
-data class UnreleasedFlag @JvmOverloads constructor(
+data class UnreleasedFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
-) : BooleanFlag(id, false, teamfood, overridden)
+) : BooleanFlag(id, name, namespace, false, teamfood, overridden)
 
 /**
- * A Flag that is is true by default.
+ * A Flag that is true by default.
  *
  * It can be changed or overridden in any build, meaning it can be turned off if needed.
  */
-data class ReleasedFlag @JvmOverloads constructor(
+data class ReleasedFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
-) : BooleanFlag(id, true, teamfood, overridden)
+) : BooleanFlag(id, name, namespace, true, teamfood, overridden)
 
 /**
  * A Flag that reads its default values from a resource overlay instead of code.
  *
  * Prefer [UnreleasedFlag] and [ReleasedFlag].
  */
-data class ResourceBooleanFlag @JvmOverloads constructor(
+data class ResourceBooleanFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @BoolRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<Boolean>
@@ -124,7 +143,7 @@
  *
  * Prefer [UnreleasedFlag] and [ReleasedFlag].
  */
-data class DeviceConfigBooleanFlag @JvmOverloads constructor(
+data class DeviceConfigBooleanFlag constructor(
     override val id: Int,
     override val name: String,
     override val namespace: String,
@@ -139,17 +158,20 @@
  *
  * Prefer [UnreleasedFlag] and [ReleasedFlag].
  */
-data class SysPropBooleanFlag @JvmOverloads constructor(
+data class SysPropBooleanFlag constructor(
     override val id: Int,
     override val name: String,
-    override val default: Boolean = false
+    override val namespace: String,
+    override val default: Boolean = false,
 ) : SysPropFlag<Boolean> {
     // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
     override val teamfood: Boolean = false
 }
 
-data class StringFlag @JvmOverloads constructor(
+data class StringFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: String = "",
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -164,23 +186,31 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readString() ?: ""
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeString(default)
     }
 }
 
-data class ResourceStringFlag @JvmOverloads constructor(
+data class ResourceStringFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @StringRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<String>
 
-data class IntFlag @JvmOverloads constructor(
+data class IntFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Int = 0,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -196,25 +226,33 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readInt()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeInt(default)
     }
 }
 
-data class ResourceIntFlag @JvmOverloads constructor(
+data class ResourceIntFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     @IntegerRes override val resourceId: Int,
     override val teamfood: Boolean = false
 ) : ResourceFlag<Int>
 
-data class LongFlag @JvmOverloads constructor(
+data class LongFlag constructor(
     override val id: Int,
     override val default: Long = 0,
     override val teamfood: Boolean = false,
+    override val name: String,
+    override val namespace: String,
     override val overridden: Boolean = false
 ) : ParcelableFlag<Long> {
 
@@ -228,17 +266,23 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readLong()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeLong(default)
     }
 }
 
-data class FloatFlag @JvmOverloads constructor(
+data class FloatFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Float = 0f,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -254,23 +298,31 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readFloat()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeFloat(default)
     }
 }
 
-data class ResourceFloatFlag @JvmOverloads constructor(
+data class ResourceFloatFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val resourceId: Int,
-    override val teamfood: Boolean = false
+    override val teamfood: Boolean = false,
 ) : ResourceFlag<Int>
 
-data class DoubleFlag @JvmOverloads constructor(
+data class DoubleFlag constructor(
     override val id: Int,
+    override val name: String,
+    override val namespace: String,
     override val default: Double = 0.0,
     override val teamfood: Boolean = false,
     override val overridden: Boolean = false
@@ -286,11 +338,15 @@
 
     private constructor(parcel: Parcel) : this(
         id = parcel.readInt(),
+        name = parcel.readString(),
+        namespace = parcel.readString(),
         default = parcel.readDouble()
     )
 
     override fun writeToParcel(parcel: Parcel, flags: Int) {
         parcel.writeInt(id)
+        parcel.writeString(name)
+        parcel.writeString(namespace)
         parcel.writeDouble(default)
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt
index e9ea19d..eeb6031 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt
@@ -24,6 +24,7 @@
 private const val FIELD_TYPE = "type"
 private const val TYPE_BOOLEAN = "boolean"
 private const val TYPE_STRING = "string"
+private const val TYPE_INT = "int"
 
 private const val TAG = "FlagSerializer"
 
@@ -77,4 +78,10 @@
     JSONObject::getString
 )
 
+object IntFlagSerializer : FlagSerializer<Int>(
+    TYPE_INT,
+    JSONObject::put,
+    JSONObject::getInt
+)
+
 class InvalidFlagStorageException : Exception("Data found but is invalid")
diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
index 8aa3aba..a14f971 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java
@@ -291,8 +291,10 @@
     }
 
     private void endAnimations(String reason, boolean cancel) {
-        Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
-        Trace.endSection();
+        if (Trace.isEnabled()) {
+            Trace.instant(Trace.TRACE_TAG_APP,
+                    "KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
+        }
         mVisible = false;
         mTmpArray.addAll(mRunningAnimations);
         int size = mTmpArray.size();
@@ -502,20 +504,23 @@
 
         @Override
         public void onAnimationStart(Animator animation) {
-            Trace.beginSection("KeyButtonRipple.start." + mName);
-            Trace.endSection();
+            if (Trace.isEnabled()) {
+                Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.start." + mName);
+            }
         }
 
         @Override
         public void onAnimationCancel(Animator animation) {
-            Trace.beginSection("KeyButtonRipple.cancel." + mName);
-            Trace.endSection();
+            if (Trace.isEnabled()) {
+                Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.cancel." + mName);
+            }
         }
 
         @Override
         public void onAnimationEnd(Animator animation) {
-            Trace.beginSection("KeyButtonRipple.end." + mName);
-            Trace.endSection();
+            if (Trace.isEnabled()) {
+                Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.end." + mName);
+            }
         }
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 860a5da..236aa66 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -20,25 +20,28 @@
 import android.annotation.FloatRange
 import android.annotation.IntRange
 import android.annotation.SuppressLint
-import android.app.compat.ChangeIdStateCache.invalidate
 import android.content.Context
 import android.graphics.Canvas
+import android.graphics.Rect
 import android.text.Layout
 import android.text.TextUtils
 import android.text.format.DateFormat
 import android.util.AttributeSet
+import android.util.MathUtils
 import android.widget.TextView
-import com.android.internal.R.attr.contentDescription
-import com.android.internal.R.attr.format
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Calendar
 import java.util.Locale
 import java.util.TimeZone
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
@@ -51,14 +54,8 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-
-    private var lastMeasureCall: CharSequence? = null
-    private var lastDraw: CharSequence? = null
-    private var lastTextUpdate: CharSequence? = null
-    private var lastOnTextChanged: CharSequence? = null
-    private var lastInvalidate: CharSequence? = null
-    private var lastTimeZoneChange: CharSequence? = null
-    private var lastAnimationCall: CharSequence? = null
+    var tag: String = "UnnamedClockView"
+    var logBuffer: LogBuffer? = null
 
     private val time = Calendar.getInstance()
 
@@ -135,6 +132,7 @@
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
+        logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
         refreshFormat()
     }
 
@@ -150,27 +148,39 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
+        logBuffer?.log(tag, DEBUG,
+                { str1 = formattedText?.toString() },
+                { "refreshTime: new formattedText=$str1" }
+        )
         // Setting text actually triggers a layout pass (because the text view is set to
         // wrap_content width and TextView always relayouts for this). Avoid needless
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
+            logBuffer?.log(tag, DEBUG,
+                    { str1 = formattedText?.toString() },
+                    { "refreshTime: done setting new time text to: $str1" }
+            )
             // Because the TextLayout may mutate under the hood as a result of the new text, we
             // notify the TextAnimator that it may have changed and request a measure/layout. A
             // crash will occur on the next invocation of setTextStyle if the layout is mutated
             // without being notified TextInterpolator being notified.
             if (layout != null) {
                 textAnimator?.updateLayout(layout)
+                logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
             }
             requestLayout()
-            lastTextUpdate = getTimestamp()
+            logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
         time.timeZone = timeZone
         refreshFormat()
-        lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = timeZone?.toString() },
+                { "onTimeZoneChanged newTimeZone=$str1" }
+        )
     }
 
     @SuppressLint("DrawAllocation")
@@ -184,22 +194,24 @@
         } else {
             animator.updateLayout(layout)
         }
-        lastMeasureCall = getTimestamp()
+        logBuffer?.log(tag, DEBUG, "onMeasure")
     }
 
     override fun onDraw(canvas: Canvas) {
-        lastDraw = getTimestamp()
-        // intentionally doesn't call super.onDraw here or else the text will be rendered twice
-        textAnimator?.draw(canvas)
+        // Use textAnimator to render text if animation is enabled.
+        // Otherwise default to using standard draw functions.
+        if (isAnimationEnabled) {
+            // intentionally doesn't call super.onDraw here or else the text will be rendered twice
+            textAnimator?.draw(canvas)
+        } else {
+            super.onDraw(canvas)
+        }
+        logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
     }
 
     override fun invalidate() {
         super.invalidate()
-        lastInvalidate = getTimestamp()
-    }
-
-    private fun getTimestamp(): CharSequence {
-        return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text"
+        logBuffer?.log(tag, DEBUG, "invalidate")
     }
 
     override fun onTextChanged(
@@ -209,7 +221,10 @@
             lengthAfter: Int
     ) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
-        lastOnTextChanged = "${getTimestamp()}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = text.toString() },
+                { "onTextChanged text=$str1" }
+        )
     }
 
     fun setLineSpacingScale(scale: Float) {
@@ -223,7 +238,7 @@
     }
 
     fun animateAppearOnLockscreen() {
-        lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen"
+        logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
             textSize = -1f,
@@ -248,7 +263,7 @@
         if (isAnimationEnabled && textAnimator == null) {
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateFoldAppear"
+        logBuffer?.log(tag, DEBUG, "animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
             textSize = -1f,
@@ -275,7 +290,7 @@
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateCharge"
+        logBuffer?.log(tag, DEBUG, "animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -299,7 +314,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        lastAnimationCall = "${getTimestamp()} call=animateDoze"
+        logBuffer?.log(tag, DEBUG, "animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             textSize = -1f,
@@ -311,7 +326,24 @@
         )
     }
 
-    private val glyphFilter: GlyphCallback? = null // Add text animation tweak here.
+    // The offset of each glyph from where it should be.
+    private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+
+    private var lastSeenAnimationProgress = 1.0f
+
+    // If the animation is being reversed, the target offset for each glyph for the "stop".
+    private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+    private var animationCancelStopPosition = 0.0f
+
+    // Whether the currently playing animation needed a stop (and thus, is shortened).
+    private var currentAnimationNeededStop = false
+
+    private val glyphFilter: GlyphCallback = { positionedGlyph, _ ->
+        val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex
+        if (offset < glyphOffsets.size) {
+            positionedGlyph.x += glyphOffsets[offset]
+        }
+    }
 
     /**
      * Set text style with an optional animation.
@@ -345,6 +377,9 @@
                 onAnimationEnd = onAnimationEnd
             )
             textAnimator?.glyphFilter = glyphFilter
+            if (color != null && !isAnimationEnabled) {
+                setTextColor(color)
+            }
         } else {
             // when the text animator is set, update its start values
             onTextAnimatorInitialized = Runnable {
@@ -359,6 +394,9 @@
                     onAnimationEnd = onAnimationEnd
                 )
                 textAnimator?.glyphFilter = glyphFilter
+                if (color != null && !isAnimationEnabled) {
+                    setTextColor(color)
+                }
             }
         }
     }
@@ -394,9 +432,12 @@
             isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
             else -> DOUBLE_LINE_FORMAT_12_HOUR
         }
+        logBuffer?.log(tag, DEBUG,
+                { str1 = format?.toString() },
+                { "refreshFormat format=$str1" }
+        )
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
-
         refreshTime()
     }
 
@@ -405,15 +446,8 @@
         pw.println("    measuredWidth=$measuredWidth")
         pw.println("    measuredHeight=$measuredHeight")
         pw.println("    singleLineInternal=$isSingleLineInternal")
-        pw.println("    lastTextUpdate=$lastTextUpdate")
-        pw.println("    lastOnTextChanged=$lastOnTextChanged")
-        pw.println("    lastInvalidate=$lastInvalidate")
-        pw.println("    lastMeasureCall=$lastMeasureCall")
-        pw.println("    lastDraw=$lastDraw")
-        pw.println("    lastTimeZoneChange=$lastTimeZoneChange")
         pw.println("    currText=$text")
         pw.println("    currTimeContextDesc=$contentDescription")
-        pw.println("    lastAnimationCall=$lastAnimationCall")
         pw.println("    dozingWeightInternal=$dozingWeightInternal")
         pw.println("    lockScreenWeightInternal=$lockScreenWeightInternal")
         pw.println("    dozingColor=$dozingColor")
@@ -421,6 +455,124 @@
         pw.println("    time=$time")
     }
 
+    fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+        // Do we need to cancel an in-flight animation?
+        // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0,
+        // which trips up the check otherwise.
+        if (lastSeenAnimationProgress != 1.0f &&
+                lastSeenAnimationProgress != 0.0f &&
+                fraction == 0.0f) {
+            // New animation, but need to stop the old one. Figure out where each glyph currently
+            // is in relation to the box position. After that, use the leading digit's current
+            // position as the stop target.
+            currentAnimationNeededStop = true
+
+            // We assume that the current glyph offsets would be relative to the "from" position.
+            val moveAmount = toRect.left - fromRect.left
+
+            // Remap the current glyph offsets to be relative to the new "end" position, and figure
+            // out the start/end positions for the stop animation.
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + glyphOffsets[i]
+                animationCancelStartPosition[i] = glyphOffsets[i]
+            }
+
+            // Use the leading digit's offset as the stop position.
+            if (toRect.left > fromRect.left) {
+                // It _was_ moving left
+                animationCancelStopPosition = glyphOffsets[0]
+            } else {
+                // It was moving right
+                animationCancelStopPosition = glyphOffsets[1]
+            }
+        }
+
+        // Is there a cancellation in progress?
+        if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) {
+            val animationStopProgress = MathUtils.constrainedMap(
+                    0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction
+            )
+
+            // One of the digits has already stopped.
+            val animationStopStep = 1.0f / (NUM_DIGITS - 1)
+
+            for (i in 0 until NUM_DIGITS) {
+                val stopAmount = if (toRect.left > fromRect.left) {
+                    // It was moving left (before flipping)
+                    MOVE_LEFT_DELAYS[i] * animationStopStep
+                } else {
+                    // It was moving right (before flipping)
+                    MOVE_RIGHT_DELAYS[i] * animationStopStep
+                }
+
+                // Leading digit stops immediately.
+                if (stopAmount == 0.0f) {
+                    glyphOffsets[i] = animationCancelStopPosition
+                } else {
+                    val actualStopAmount = MathUtils.constrainedMap(
+                            0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress
+                    )
+                    val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount)
+                    val glyphMoveAmount =
+                            animationCancelStopPosition - animationCancelStartPosition[i]
+                    glyphOffsets[i] =
+                            animationCancelStartPosition[i] + glyphMoveAmount * easedProgress
+                }
+            }
+        } else {
+            // Normal part of the animation.
+            // Do we need to remap the animation progress to take account of the cancellation?
+            val actualFraction = if (currentAnimationNeededStop) {
+                MathUtils.constrainedMap(
+                        0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction
+                )
+            } else {
+                fraction
+            }
+
+            val digitFractions = (0 until NUM_DIGITS).map {
+                // The delay for each digit, in terms of fraction (i.e. the digit should not move
+                // during 0.0 - 0.1).
+                val initialDelay = if (toRect.left > fromRect.left) {
+                    MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP
+                } else {
+                    MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP
+                }
+
+                val f = MathUtils.constrainedMap(
+                        0.0f, 1.0f,
+                        initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME,
+                        actualFraction
+                )
+                MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f))
+            }
+
+            // Was there an animation halt?
+            val moveAmount = if (currentAnimationNeededStop) {
+                // Only need to animate over the remaining space if the animation was aborted.
+                -animationCancelStopPosition
+            } else {
+                toRect.left.toFloat() - fromRect.left.toFloat()
+            }
+
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i])
+            }
+        }
+
+        invalidate()
+
+        if (fraction == 1.0f) {
+            // Reset
+            currentAnimationNeededStop = false
+        }
+
+        lastSeenAnimationProgress = fraction
+
+        // Ensure that the actual clock container is always in the "end" position.
+        this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom)
+    }
+
     // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
     // This is an optimization to ensure we only recompute the patterns when the inputs change.
     private object Patterns {
@@ -444,6 +596,7 @@
             if (!clockView12Skel.contains("a")) {
                 sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
             }
+
             sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
             sCacheKey = key
         }
@@ -458,5 +611,36 @@
         private const val APPEAR_ANIM_DURATION: Long = 350
         private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
         private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
+
+        // Constants for the animation
+        private val MOVE_INTERPOLATOR = Interpolators.STANDARD
+
+        // Calculate the positions of all of the digits...
+        // Offset each digit by, say, 0.1
+        // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
+        // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
+        // from 0.3 - 1.0.
+        private const val NUM_DIGITS = 4
+        private const val DIGITS_PER_LINE = 2
+
+        // How much of "fraction" to spend on canceling the animation, if needed
+        private const val ANIMATION_CANCELLATION_TIME = 0.4f
+
+        // Delays. Each digit's animation should have a slight delay, so we get a nice
+        // "stepping" effect. When moving right, the second digit of the hour should move first.
+        // When moving left, the first digit of the hour should move first. The lists encode
+        // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
+        // by delayMultiplier.
+        private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
+        private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
+
+        // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
+        // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
+        // before moving).
+        private const val MOVE_DIGIT_STEP = 0.1f
+
+        // Total available transition time for each digit, taking into account the step. If step is
+        // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
+        private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index f03fee4..5c2c27a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -18,10 +18,9 @@
 import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.os.Handler
-import android.os.UserHandle
 import android.provider.Settings
 import android.util.Log
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -29,8 +28,7 @@
 import com.android.systemui.plugins.ClockProviderPlugin
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.shared.plugins.PluginManager
-import com.google.gson.Gson
-import javax.inject.Inject
+import org.json.JSONObject
 
 private val TAG = ClockRegistry::class.simpleName
 private const val DEBUG = true
@@ -40,23 +38,16 @@
     val context: Context,
     val pluginManager: PluginManager,
     val handler: Handler,
-    defaultClockProvider: ClockProvider
+    val isEnabled: Boolean,
+    userHandle: Int,
+    defaultClockProvider: ClockProvider,
+    val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
 ) {
-    @Inject constructor(
-        context: Context,
-        pluginManager: PluginManager,
-        @Main handler: Handler,
-        defaultClockProvider: DefaultClockProvider
-    ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
-
     // Usually this would be a typealias, but a SAM provides better java interop
     fun interface ClockChangeListener {
         fun onClockChanged()
     }
 
-    var isEnabled: Boolean = false
-
-    private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
     private val settingObserver = object : ContentObserver(handler) {
@@ -79,15 +70,18 @@
                     context.contentResolver,
                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
                 )
-                gson.fromJson(json, ClockSetting::class.java)?.clockId ?: DEFAULT_CLOCK_ID
+                if (json == null || json.isEmpty()) {
+                    return fallbackClockId
+                }
+                ClockSetting.deserialize(json).clockId
             } catch (ex: Exception) {
                 Log.e(TAG, "Failed to parse clock setting", ex)
-                DEFAULT_CLOCK_ID
+                fallbackClockId
             }
         }
         set(value) {
             try {
-                val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
+                val json = ClockSetting.serialize(ClockSetting(value, System.currentTimeMillis()))
                 Settings.Secure.putString(
                     context.contentResolver,
                     Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
@@ -105,14 +99,19 @@
             )
         }
 
-        pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java,
-            true /* allowMultiple */)
-        context.contentResolver.registerContentObserver(
-            Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
-            false,
-            settingObserver,
-            UserHandle.USER_ALL
-        )
+        if (isEnabled) {
+            pluginManager.addPluginListener(
+                pluginListener,
+                ClockProviderPlugin::class.java,
+                /*allowMultiple=*/ true
+            )
+            context.contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                /*notifyForDescendants=*/ false,
+                settingObserver,
+                userHandle
+            )
+        }
     }
 
     private fun connectClocks(provider: ClockProvider) {
@@ -201,8 +200,28 @@
         val provider: ClockProvider
     )
 
-    private data class ClockSetting(
+    @Keep
+    data class ClockSetting(
         val clockId: ClockId,
         val _applied_timestamp: Long?
-    )
+    ) {
+        companion object {
+            private val KEY_CLOCK_ID = "clockId"
+            private val KEY_TIMESTAMP = "_applied_timestamp"
+
+            fun serialize(setting: ClockSetting): String {
+                return JSONObject()
+                    .put(KEY_CLOCK_ID, setting.clockId)
+                    .put(KEY_TIMESTAMP, setting._applied_timestamp)
+                    .toString()
+            }
+
+            fun deserialize(jsonStr: String): ClockSetting {
+                val json = JSONObject(jsonStr)
+                return ClockSetting(
+                    json.getString(KEY_CLOCK_ID),
+                    if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index b887951..23a7271 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -16,9 +16,11 @@
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Color
+import android.graphics.Rect
 import android.icu.text.NumberFormat
 import android.util.TypedValue
 import android.view.LayoutInflater
+import android.view.View
 import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.plugins.ClockAnimations
@@ -26,6 +28,7 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Locale
@@ -78,19 +81,28 @@
     }
 
     override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
-        largeClock.recomputePadding()
+        largeClock.recomputePadding(null)
         animations = DefaultClockAnimations(dozeFraction, foldFraction)
         events.onColorPaletteChanged(resources)
         events.onTimeZoneChanged(TimeZone.getDefault())
         events.onTimeTick()
     }
 
+    override fun setLogBuffer(logBuffer: LogBuffer) {
+        smallClock.view.tag = "smallClockView"
+        largeClock.view.tag = "largeClockView"
+        smallClock.view.logBuffer = logBuffer
+        largeClock.view.logBuffer = logBuffer
+    }
+
     open inner class DefaultClockFaceController(
         override val view: AnimatableClockView,
     ) : ClockFaceController {
+
         // MAGENTA is a placeholder, and will be assigned correctly in initialize
         private var currentColor = Color.MAGENTA
         private var isRegionDark = false
+        protected var targetRegion: Rect? = null
 
         init {
             view.setColors(currentColor, currentColor)
@@ -102,8 +114,20 @@
                     this@DefaultClockFaceController.isRegionDark = isRegionDark
                     updateColor()
                 }
+
+                override fun onTargetRegionChanged(targetRegion: Rect?) {
+                    this@DefaultClockFaceController.targetRegion = targetRegion
+                    recomputePadding(targetRegion)
+                }
+
+                override fun onFontSettingChanged(fontSizePx: Float) {
+                    view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
+                    recomputePadding(targetRegion)
+                }
             }
 
+        open fun recomputePadding(targetRegion: Rect?) {}
+
         fun updateColor() {
             val color =
                 if (isRegionDark) {
@@ -118,18 +142,31 @@
 
             currentColor = color
             view.setColors(DOZE_COLOR, color)
-            view.animateAppearOnLockscreen()
+            if (!animations.dozeState.isActive) {
+                view.animateAppearOnLockscreen()
+            }
         }
     }
 
     inner class LargeClockFaceController(
         view: AnimatableClockView,
     ) : DefaultClockFaceController(view) {
-        fun recomputePadding() {
+        override fun recomputePadding(targetRegion: Rect?) {
+            // We center the view within the targetRegion instead of within the parent
+            // view by computing the difference and adding that to the padding.
+            val parent = view.parent
+            val yDiff =
+                if (targetRegion != null && parent is View && parent.isLaidOut())
+                    targetRegion.centerY() - parent.height / 2f
+                else 0f
             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
-            lp.topMargin = (-0.5f * view.bottom).toInt()
+            lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
             view.setLayoutParams(lp)
         }
+
+        fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+            view.moveForSplitShade(fromRect, toRect, fraction)
+        }
     }
 
     inner class DefaultClockEvents : ClockEvents {
@@ -141,18 +178,6 @@
         override fun onTimeZoneChanged(timeZone: TimeZone) =
             clocks.forEach { it.onTimeZoneChanged(timeZone) }
 
-        override fun onFontSettingChanged() {
-            smallClock.view.setTextSize(
-                TypedValue.COMPLEX_UNIT_PX,
-                resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
-            )
-            largeClock.view.setTextSize(
-                TypedValue.COMPLEX_UNIT_PX,
-                resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
-            )
-            largeClock.recomputePadding()
-        }
-
         override fun onColorPaletteChanged(resources: Resources) {
             largeClock.updateColor()
             smallClock.updateColor()
@@ -174,13 +199,10 @@
         dozeFraction: Float,
         foldFraction: Float,
     ) : ClockAnimations {
-        private var foldState = AnimationState(0f)
-        private var dozeState = AnimationState(0f)
+        internal val dozeState = AnimationState(dozeFraction)
+        private val foldState = AnimationState(foldFraction)
 
         init {
-            dozeState = AnimationState(dozeFraction)
-            foldState = AnimationState(foldFraction)
-
             if (foldState.isActive) {
                 clocks.forEach { it.animateFoldAppear(false) }
             } else {
@@ -209,13 +231,23 @@
                 clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
             }
         }
+
+        override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {
+            largeClock.moveForSplitShade(fromRect, toRect, fraction)
+        }
+
+        override val hasCustomPositionUpdatedAnimation: Boolean
+            get() = true
     }
 
-    private class AnimationState(
+    class AnimationState(
         var fraction: Float,
     ) {
-        var isActive: Boolean = fraction < 0.5f
+        var isActive: Boolean = fraction > 0.5f
         fun update(newFraction: Float): Pair<Boolean, Boolean> {
+            if (newFraction == fraction) {
+                return Pair(isActive, false)
+            }
             val wasActive = isActive
             val hasJumped =
                 (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
new file mode 100644
index 0000000..f60db2a
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.shared.keyguard.data.content
+
+import android.content.ContentResolver
+import android.net.Uri
+
+/** Contract definitions for querying content about keyguard quick affordances. */
+object KeyguardQuickAffordanceProviderContract {
+
+    const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance"
+    const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
+
+    private val BASE_URI: Uri =
+        Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
+
+    /**
+     * Table for slots.
+     *
+     * Slots are positions where affordances can be placed on the lock screen. Affordances that are
+     * placed on slots are said to be "selected". The system supports the idea of multiple
+     * affordances per slot, though the implementation may limit the number of affordances on each
+     * slot.
+     *
+     * Supported operations:
+     * - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set
+     * will contain rows with the [SlotTable.Columns] columns.
+     */
+    object SlotTable {
+        const val TABLE_NAME = "slots"
+        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+        object Columns {
+            /** String. Unique ID for this slot. */
+            const val ID = "id"
+            /** Integer. The maximum number of affordances that can be placed in the slot. */
+            const val CAPACITY = "capacity"
+        }
+    }
+
+    /**
+     * Table for affordances.
+     *
+     * Affordances are actions/buttons that the user can execute. They are placed on slots on the
+     * lock screen.
+     *
+     * Supported operations:
+     * - Query - to know about all the affordances that are available on the device, regardless of
+     * which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will
+     * contain rows, each with the columns specified in [AffordanceTable.Columns].
+     */
+    object AffordanceTable {
+        const val TABLE_NAME = "affordances"
+        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+        object Columns {
+            /** String. Unique ID for this affordance. */
+            const val ID = "id"
+            /** String. User-visible name for this affordance. */
+            const val NAME = "name"
+            /**
+             * Integer. Resource ID for the drawable to load for this affordance. This is a resource
+             * ID from the system UI package.
+             */
+            const val ICON = "icon"
+        }
+    }
+
+    /**
+     * Table for selections.
+     *
+     * Selections are pairs of slot and affordance IDs.
+     *
+     * Supported operations:
+     * - Insert - to insert an affordance and place it in a slot, insert values for the columns into
+     * the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system.
+     * Selecting a new affordance for a slot that is already full will automatically remove the
+     * oldest affordance from the slot.
+     * - Query - to know which affordances are set on which slots, query the [SelectionTable.URI]
+     * [Uri]. The result set will contain rows, each of which with the columns from
+     * [SelectionTable.Columns].
+     * - Delete - to unselect an affordance, removing it from a slot, delete from the
+     * [SelectionTable.URI] [Uri], passing in values for each column.
+     */
+    object SelectionTable {
+        const val TABLE_NAME = "selections"
+        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+        object Columns {
+            /** String. Unique ID for the slot. */
+            const val SLOT_ID = "slot_id"
+            /** String. Unique ID for the selected affordance. */
+            const val AFFORDANCE_ID = "affordance_id"
+        }
+    }
+
+    /**
+     * Table for flags.
+     *
+     * Flags are key-value pairs.
+     *
+     * Supported operations:
+     * - Query - to know the values of flags, query the [FlagsTable.URI] [Uri]. The result set will
+     * contain rows, each of which with the columns from [FlagsTable.Columns].
+     */
+    object FlagsTable {
+        const val TABLE_NAME = "flags"
+        val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
+
+        /**
+         * Flag denoting whether the customizable lock screen quick affordances feature is enabled.
+         */
+        const val FLAG_NAME_FEATURE_ENABLED = "is_feature_enabled"
+
+        object Columns {
+            /** String. Unique ID for the flag. */
+            const val NAME = "name"
+            /** Int. Value of the flag. `1` means `true` and `0` means `false`. */
+            const val VALUE = "value"
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt
new file mode 100644
index 0000000..2dc7a28
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.shared.keyguard.shared.model
+
+/**
+ * Collection of all supported "slots", placements where keyguard quick affordances can appear on
+ * the lock screen.
+ */
+object KeyguardQuickAffordanceSlots {
+    const val SLOT_ID_BOTTOM_START = "bottom_start"
+    const val SLOT_ID_BOTTOM_END = "bottom_end"
+}
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/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 4613e8b..abefeba 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -20,6 +20,7 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.view.MotionEvent;
+import android.view.SurfaceControl;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
 oneway interface IOverviewProxy {
@@ -44,12 +45,6 @@
     void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) = 8;
 
     /**
-     * Sent when there was an action on one of the onboarding tips view.
-     * TODO: Move this implementation to SystemUI completely
-     */
-    void onTip(int actionType, int viewType) = 10;
-
-    /**
      * Sent when device assistant changes its default assistant whether it is available or not.
      */
     void onAssistantAvailable(boolean available) = 13;
@@ -60,23 +55,11 @@
     void onAssistantVisibilityChanged(float visibility) = 14;
 
     /**
-     * Sent when back is triggered.
-     * TODO: Move this implementation to SystemUI completely
-     */
-    void onBackAction(boolean completed, int downX, int downY, boolean isButton,
-            boolean gestureSwipeLeft) = 15;
-
-    /**
      * Sent when some system ui state changes.
      */
     void onSystemUiStateChanged(int stateFlags) = 16;
 
     /**
-     * Sent when the split screen is resized
-     */
-    void onSplitScreenSecondaryBoundsChanged(in Rect bounds, in Rect insets) = 17;
-
-    /**
      * Sent when suggested rotation button could be shown
      */
     void onRotationProposal(int rotation, boolean isValid) = 18;
@@ -110,4 +93,14 @@
       * Sent when screen started turning off.
       */
      void onScreenTurningOff() = 24;
+
+     /**
+      * Sent when split keyboard shortcut is triggered to enter stage split.
+      */
+     void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
+
+     /**
+      * Sent when the surface for navigation bar is created or changed
+      */
+     void onNavigationBarSurface(in SurfaceControl surface) = 26;
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 2b2b05ce..1c532fe 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -24,7 +24,6 @@
 import android.view.MotionEvent;
 
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
 
 /**
  * Temporary callbacks into SystemUI.
@@ -106,9 +105,6 @@
     /** Sets home rotation enabled. */
     void setHomeRotationEnabled(boolean enabled) = 45;
 
-    /** Notifies that a swipe-up gesture has started */
-    oneway void notifySwipeUpGestureStarted() = 46;
-
     /** Notifies when taskbar status updated */
     oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47;
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 647dd47..0890465 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -20,7 +20,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
 
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskDescription;
@@ -255,7 +255,8 @@
         // Also consider undefined activity type to include tasks in overview right after rebooting
         // the device.
         final boolean isDockable = taskInfo.supportsMultiWindow
-                && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode())
+                && ArrayUtils.contains(
+                        CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, taskInfo.getWindowingMode())
                 && (taskInfo.getActivityType() == ACTIVITY_TYPE_UNDEFINED
                 || ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, taskInfo.getActivityType()));
         return new Task(taskKey,
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index 72f8b7b..f45887c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -1,13 +1,16 @@
 package com.android.systemui.shared.recents.utilities;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.Surface;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.util.SplitBounds;
 
 /**
  * Utility class to position the thumbnail in the TaskView
@@ -16,10 +19,26 @@
 
     public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
 
+    /**
+     * Specifies that a stage is positioned at the top half of the screen if
+     * in portrait mode or at the left half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
+
+    /**
+     * Specifies that a stage is positioned at the bottom half of the screen if
+     * in portrait mode or at the right half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
+
     // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
     private final RectF mClippedInsets = new RectF();
     private final Matrix mMatrix = new Matrix();
     private boolean mIsOrientationChanged;
+    private SplitBounds mSplitBounds;
+    private int mDesiredStagePosition;
 
     public Matrix getMatrix() {
         return mMatrix;
@@ -33,11 +52,17 @@
         return mIsOrientationChanged;
     }
 
+    public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) {
+        mSplitBounds = splitBounds;
+        mDesiredStagePosition = desiredStagePosition;
+    }
+
     /**
      * Updates the matrix based on the provided parameters
      */
     public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
-            int canvasWidth, int canvasHeight, int screenWidthPx, int taskbarSize, boolean isTablet,
+            int canvasWidth, int canvasHeight, int screenWidthPx, int screenHeightPx,
+            int taskbarSize, boolean isTablet,
             int currentRotation, boolean isRtl) {
         boolean isRotated = false;
         boolean isOrientationDifferent;
@@ -45,8 +70,33 @@
         int thumbnailRotation = thumbnailData.rotation;
         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
         RectF thumbnailClipHint = new RectF();
-        float canvasScreenRatio = canvasWidth / (float) screenWidthPx;
-        float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
+
+        float scaledTaskbarSize;
+        float canvasScreenRatio;
+        if (mSplitBounds != null) {
+            float fullscreenTaskWidth;
+            float fullscreenTaskHeight;
+
+            float taskPercent;
+            if (mSplitBounds.appsStackedVertically) {
+                taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT
+                        ? mSplitBounds.topTaskPercent
+                        : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent));
+                fullscreenTaskHeight = screenHeightPx * taskPercent;
+                canvasScreenRatio = canvasHeight / fullscreenTaskHeight;
+            } else {
+                // For landscape, scale the width
+                taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+                        ? mSplitBounds.leftTaskPercent
+                        : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
+                // Scale landscape width to that of actual screen
+                fullscreenTaskWidth = screenWidthPx * taskPercent;
+                canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
+            }
+        } else {
+            canvasScreenRatio = (float) canvasWidth / screenWidthPx;
+        }
+        scaledTaskbarSize = taskbarSize * canvasScreenRatio;
         thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
 
         float scale = thumbnailData.scale;
@@ -180,7 +230,7 @@
      * portrait or vice versa, {@code false} otherwise
      */
     private boolean isOrientationChange(int deltaRotation) {
-        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
     }
 
     private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
@@ -189,13 +239,13 @@
 
         mMatrix.setRotate(90 * deltaRotate);
         switch (deltaRotate) { /* Counter-clockwise */
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 translateX = thumbnailPosition.height();
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 translateY = thumbnailPosition.width();
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 translateX = thumbnailPosition.width();
                 translateY = thumbnailPosition.height();
                 break;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
new file mode 100644
index 0000000..0ee813b
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.shared.regionsampling
+
+import android.graphics.Color
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+
+/** Class for instance of RegionSamplingHelper */
+open class RegionSampler(
+    sampledView: View?,
+    mainExecutor: Executor?,
+    bgExecutor: Executor?,
+    regionSamplingEnabled: Boolean,
+    updateFun: UpdateColorCallback
+) {
+    private var regionDarkness = RegionDarkness.DEFAULT
+    private var samplingBounds = Rect()
+    private val tmpScreenLocation = IntArray(2)
+    @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
+    private var lightForegroundColor = Color.WHITE
+    private var darkForegroundColor = Color.BLACK
+
+    @VisibleForTesting
+    open fun createRegionSamplingHelper(
+        sampledView: View,
+        callback: SamplingCallback,
+        mainExecutor: Executor?,
+        bgExecutor: Executor?
+    ): RegionSamplingHelper {
+        return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
+    }
+
+    /**
+     * Sets the colors to be used for Dark and Light Foreground.
+     *
+     * @param lightColor The color used for Light Foreground.
+     * @param darkColor The color used for Dark Foreground.
+     */
+    fun setForegroundColors(lightColor: Int, darkColor: Int) {
+        lightForegroundColor = lightColor
+        darkForegroundColor = darkColor
+    }
+
+    /**
+     * Determines which foreground color to use based on region darkness.
+     *
+     * @return the determined foreground color
+     */
+    fun currentForegroundColor(): Int {
+        return if (regionDarkness.isDark) {
+            lightForegroundColor
+        } else {
+            darkForegroundColor
+        }
+    }
+
+    private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
+        return if (isRegionDark) {
+            RegionDarkness.DARK
+        } else {
+            RegionDarkness.LIGHT
+        }
+    }
+
+    fun currentRegionDarkness(): RegionDarkness {
+        return regionDarkness
+    }
+
+    /** Start region sampler */
+    fun startRegionSampler() {
+        regionSampler?.start(samplingBounds)
+    }
+
+    /** Stop region sampler */
+    fun stopRegionSampler() {
+        regionSampler?.stop()
+    }
+
+    /** Dump region sampler */
+    fun dump(pw: PrintWriter) {
+        regionSampler?.dump(pw)
+    }
+
+    init {
+        if (regionSamplingEnabled && sampledView != null) {
+            regionSampler =
+                createRegionSamplingHelper(
+                    sampledView,
+                    object : SamplingCallback {
+                        override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+                            regionDarkness = convertToClockDarkness(isRegionDark)
+                            updateFun()
+                        }
+                        /**
+                         * The method getLocationOnScreen is used to obtain the view coordinates
+                         * relative to its left and top edges on the device screen. Directly
+                         * accessing the X and Y coordinates of the view returns the location
+                         * relative to its parent view instead.
+                         */
+                        override fun getSampledRegion(sampledView: View): Rect {
+                            val screenLocation = tmpScreenLocation
+                            sampledView.getLocationOnScreen(screenLocation)
+                            val left = screenLocation[0]
+                            val top = screenLocation[1]
+                            samplingBounds.left = left
+                            samplingBounds.top = top
+                            samplingBounds.right = left + sampledView.width
+                            samplingBounds.bottom = top + sampledView.height
+                            return samplingBounds
+                        }
+
+                        override fun isSamplingEnabled(): Boolean {
+                            return regionSamplingEnabled
+                        }
+                    },
+                    mainExecutor,
+                    bgExecutor
+                )
+        }
+        regionSampler?.setWindowVisible(true)
+    }
+}
+
+typealias UpdateColorCallback = () -> Unit
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
deleted file mode 100644
index dd2e55d..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ /dev/null
@@ -1,128 +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.shared.regionsampling
-
-import android.graphics.Rect
-import android.view.View
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-
-/**
- * Class for instance of RegionSamplingHelper
- */
-open class RegionSamplingInstance(
-        sampledView: View?,
-        mainExecutor: Executor?,
-        bgExecutor: Executor?,
-        regionSamplingEnabled: Boolean,
-        updateFun: UpdateColorCallback
-) {
-    private var isDark = RegionDarkness.DEFAULT
-    private var samplingBounds = Rect()
-    private val tmpScreenLocation = IntArray(2)
-    @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
-
-    /**
-     * Interface for method to be passed into RegionSamplingHelper
-     */
-    @FunctionalInterface
-    interface UpdateColorCallback {
-        /**
-         * Method to update the text colors after clock darkness changed.
-         */
-        fun updateColors()
-    }
-
-    @VisibleForTesting
-    open fun createRegionSamplingHelper(
-            sampledView: View,
-            callback: SamplingCallback,
-            mainExecutor: Executor?,
-            bgExecutor: Executor?
-    ): RegionSamplingHelper {
-        return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
-    }
-
-    private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
-        return if (isRegionDark) {
-            RegionDarkness.DARK
-        } else {
-            RegionDarkness.LIGHT
-        }
-    }
-
-    fun currentRegionDarkness(): RegionDarkness {
-        return isDark
-    }
-
-    /**
-     * Start region sampler
-     */
-    fun startRegionSampler() {
-        regionSampler?.start(samplingBounds)
-    }
-
-    /**
-     * Stop region sampler
-     */
-    fun stopRegionSampler() {
-        regionSampler?.stop()
-    }
-
-    /**
-     * Dump region sampler
-     */
-    fun dump(pw: PrintWriter) {
-        regionSampler?.dump(pw)
-    }
-
-    init {
-        if (regionSamplingEnabled && sampledView != null) {
-            regionSampler = createRegionSamplingHelper(sampledView,
-                    object : SamplingCallback {
-                        override fun onRegionDarknessChanged(isRegionDark: Boolean) {
-                            isDark = convertToClockDarkness(isRegionDark)
-                            updateFun.updateColors()
-                        }
-                        /**
-                        * The method getLocationOnScreen is used to obtain the view coordinates
-                        * relative to its left and top edges on the device screen.
-                        * Directly accessing the X and Y coordinates of the view returns the
-                        * location relative to its parent view instead.
-                        */
-                        override fun getSampledRegion(sampledView: View): Rect {
-                            val screenLocation = tmpScreenLocation
-                            sampledView.getLocationOnScreen(screenLocation)
-                            val left = screenLocation[0]
-                            val top = screenLocation[1]
-                            samplingBounds.left = left
-                            samplingBounds.top = top
-                            samplingBounds.right = left + sampledView.width
-                            samplingBounds.bottom = top + sampledView.height
-                            return samplingBounds
-                        }
-
-                        override fun isSamplingEnabled(): Boolean {
-                            return regionSamplingEnabled
-                        }
-                    }, mainExecutor, bgExecutor)
-        }
-        regionSampler?.setWindowVisible(true)
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 8086172..42422d5 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -194,12 +194,8 @@
                             Rect homeContentInsets, Rect minimizedHomeBounds) {
                         final RecentsAnimationControllerCompat controllerCompat =
                                 new RecentsAnimationControllerCompat(controller);
-                        final RemoteAnimationTargetCompat[] appsCompat =
-                                RemoteAnimationTargetCompat.wrap(apps);
-                        final RemoteAnimationTargetCompat[] wallpapersCompat =
-                                RemoteAnimationTargetCompat.wrap(wallpapers);
-                        animationHandler.onAnimationStart(controllerCompat, appsCompat,
-                                wallpapersCompat, homeContentInsets, minimizedHomeBounds);
+                        animationHandler.onAnimationStart(controllerCompat, apps,
+                                wallpapers, homeContentInsets, minimizedHomeBounds);
                     }
 
                     @Override
@@ -210,12 +206,7 @@
 
                     @Override
                     public void onTasksAppeared(RemoteAnimationTarget[] apps) {
-                        final RemoteAnimationTargetCompat[] compats =
-                                new RemoteAnimationTargetCompat[apps.length];
-                        for (int i = 0; i < apps.length; ++i) {
-                            compats[i] = new RemoteAnimationTargetCompat(apps[i]);
-                        }
-                        animationHandler.onTasksAppeared(compats);
+                        animationHandler.onTasksAppeared(apps);
                     }
                 };
             }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 5d6598d..82d70116 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -51,6 +51,10 @@
             InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION =
             InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
+    public static final int CUJ_RECENTS_SCROLLING =
+            InteractionJankMonitor.CUJ_RECENTS_SCROLLING;
+    public static final int CUJ_APP_SWIPE_TO_RECENTS =
+            InteractionJankMonitor.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -59,7 +63,9 @@
             CUJ_APP_CLOSE_TO_PIP,
             CUJ_QUICK_SWITCH,
             CUJ_APP_LAUNCH_FROM_WIDGET,
-            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            CUJ_RECENTS_SCROLLING,
+            CUJ_APP_SWIPE_TO_RECENTS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index f2742b7..766266d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -110,6 +110,9 @@
     public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24;
     // The voice interaction session window is showing
     public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
+    // Freeform windows are showing in desktop mode
+    public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
+
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -137,7 +140,8 @@
             SYSUI_STATE_BACK_DISABLED,
             SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
             SYSUI_STATE_IMMERSIVE_MODE,
-            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING
+            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
+            SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
     })
     public @interface SystemUiStateFlags {}
 
@@ -173,6 +177,8 @@
                 ? "bubbles_mange_menu_expanded" : "");
         str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : "");
         str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
+        str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
+                ? "freeform_active_in_desktop_mode" : "");
         return str.toString();
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 5cca4a6..8bddf21 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -17,6 +17,7 @@
 package com.android.systemui.shared.system;
 
 import android.graphics.Rect;
+import android.view.RemoteAnimationTarget;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -27,7 +28,7 @@
      * Called when the animation into Recents can start. This call is made on the binder thread.
      */
     void onAnimationStart(RecentsAnimationControllerCompat controller,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
             Rect homeContentInsets, Rect minimizedHomeBounds);
 
     /**
@@ -39,7 +40,7 @@
      * Called when the task of an activity that has been started while the recents animation
      * was running becomes ready for control.
      */
-    void onTasksAppeared(RemoteAnimationTargetCompat[] app);
+    void onTasksAppeared(RemoteAnimationTarget[] app);
 
     /**
      * Called to request that the current task tile be switched out for a screenshot (if not
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
deleted file mode 100644
index 09cf7c5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_NONE;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-import static android.view.WindowManager.TransitionOldType;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
-
-import android.annotation.SuppressLint;
-import android.app.IApplicationThread;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.window.IRemoteTransition;
-import android.window.IRemoteTransitionFinishedCallback;
-import android.window.RemoteTransition;
-import android.window.TransitionInfo;
-
-import com.android.wm.shell.util.CounterRotator;
-
-/**
- * @see RemoteAnimationAdapter
- */
-public class RemoteAnimationAdapterCompat {
-
-    private final RemoteAnimationAdapter mWrapped;
-    private final RemoteTransitionCompat mRemoteTransition;
-
-    public RemoteAnimationAdapterCompat(RemoteAnimationRunnerCompat runner, long duration,
-            long statusBarTransitionDelay, IApplicationThread appThread) {
-        mWrapped = new RemoteAnimationAdapter(wrapRemoteAnimationRunner(runner), duration,
-                statusBarTransitionDelay);
-        mRemoteTransition = buildRemoteTransition(runner, appThread);
-    }
-
-    public RemoteAnimationAdapter getWrapped() {
-        return mWrapped;
-    }
-
-    /** Helper to just build a remote transition. Use this if the legacy adapter isn't needed. */
-    public static RemoteTransitionCompat buildRemoteTransition(RemoteAnimationRunnerCompat runner,
-            IApplicationThread appThread) {
-        return new RemoteTransitionCompat(
-                new RemoteTransition(wrapRemoteTransition(runner), appThread));
-    }
-
-    public RemoteTransitionCompat getRemoteTransition() {
-        return mRemoteTransition;
-    }
-
-    /** Wraps a RemoteAnimationRunnerCompat in an IRemoteAnimationRunner. */
-    public static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
-            final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
-        return new IRemoteAnimationRunner.Stub() {
-            @Override
-            public void onAnimationStart(@TransitionOldType int transit,
-                    RemoteAnimationTarget[] apps,
-                    RemoteAnimationTarget[] wallpapers,
-                    RemoteAnimationTarget[] nonApps,
-                    final IRemoteAnimationFinishedCallback finishedCallback) {
-                final RemoteAnimationTargetCompat[] appsCompat =
-                        RemoteAnimationTargetCompat.wrap(apps);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
-                        RemoteAnimationTargetCompat.wrap(wallpapers);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
-                        RemoteAnimationTargetCompat.wrap(nonApps);
-                final Runnable animationFinishedCallback = new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            finishedCallback.onAnimationFinished();
-                        } catch (RemoteException e) {
-                            Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
-                                    + " finished callback", e);
-                        }
-                    }
-                };
-                remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
-                        nonAppsCompat, animationFinishedCallback);
-            }
-
-            @Override
-            public void onAnimationCancelled(boolean isKeyguardOccluded) {
-                remoteAnimationAdapter.onAnimationCancelled();
-            }
-        };
-    }
-
-    private static IRemoteTransition.Stub wrapRemoteTransition(
-            final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
-        return new IRemoteTransition.Stub() {
-            final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>();
-
-            @Override
-            public void startAnimation(IBinder token, TransitionInfo info,
-                    SurfaceControl.Transaction t,
-                    IRemoteTransitionFinishedCallback finishCallback) {
-                final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] appsCompat =
-                        RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
-                        RemoteAnimationTargetCompat.wrapNonApps(
-                                info, true /* wallpapers */, t, leashMap);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
-                        RemoteAnimationTargetCompat.wrapNonApps(
-                                info, false /* wallpapers */, t, leashMap);
-
-                // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
-                boolean isReturnToHome = false;
-                TransitionInfo.Change launcherTask = null;
-                TransitionInfo.Change wallpaper = null;
-                int launcherLayer = 0;
-                int rotateDelta = 0;
-                float displayW = 0;
-                float displayH = 0;
-                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                    final TransitionInfo.Change change = info.getChanges().get(i);
-                    // skip changes that we didn't wrap
-                    if (!leashMap.containsKey(change.getLeash())) continue;
-                    if (change.getTaskInfo() != null
-                            && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME) {
-                        isReturnToHome = change.getMode() == TRANSIT_OPEN
-                                || change.getMode() == TRANSIT_TO_FRONT;
-                        launcherTask = change;
-                        launcherLayer = info.getChanges().size() - i;
-                    } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
-                        wallpaper = change;
-                    }
-                    if (change.getParent() == null && change.getEndRotation() >= 0
-                            && change.getEndRotation() != change.getStartRotation()) {
-                        rotateDelta = change.getEndRotation() - change.getStartRotation();
-                        displayW = change.getEndAbsBounds().width();
-                        displayH = change.getEndAbsBounds().height();
-                    }
-                }
-
-                // Prepare for rotation if there is one
-                final CounterRotator counterLauncher = new CounterRotator();
-                final CounterRotator counterWallpaper = new CounterRotator();
-                if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) {
-                    counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(),
-                            rotateDelta, displayW, displayH);
-                    if (counterLauncher.getSurface() != null) {
-                        t.setLayer(counterLauncher.getSurface(), launcherLayer);
-                    }
-                }
-
-                if (isReturnToHome) {
-                    if (counterLauncher.getSurface() != null) {
-                        t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3);
-                    }
-                    // Need to "boost" the closing things since that's what launcher expects.
-                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                        final TransitionInfo.Change change = info.getChanges().get(i);
-                        final SurfaceControl leash = leashMap.get(change.getLeash());
-                        // skip changes that we didn't wrap
-                        if (leash == null) continue;
-                        final int mode = info.getChanges().get(i).getMode();
-                        // Only deal with independent layers
-                        if (!TransitionInfo.isIndependent(change, info)) continue;
-                        if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
-                            t.setLayer(leash, info.getChanges().size() * 3 - i);
-                            counterLauncher.addChild(t, leash);
-                        }
-                    }
-                    // Make wallpaper visible immediately since launcher apparently won't do this.
-                    for (int i = wallpapersCompat.length - 1; i >= 0; --i) {
-                        t.show(wallpapersCompat[i].leash);
-                        t.setAlpha(wallpapersCompat[i].leash, 1.f);
-                    }
-                } else {
-                    if (launcherTask != null) {
-                        counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash()));
-                    }
-                    if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) {
-                        counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(),
-                                rotateDelta, displayW, displayH);
-                        if (counterWallpaper.getSurface() != null) {
-                            t.setLayer(counterWallpaper.getSurface(), -1);
-                            counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash()));
-                        }
-                    }
-                }
-                t.apply();
-
-                final Runnable animationFinishedCallback = new Runnable() {
-                    @Override
-                    @SuppressLint("NewApi")
-                    public void run() {
-                        final SurfaceControl.Transaction finishTransaction =
-                                new SurfaceControl.Transaction();
-                        counterLauncher.cleanUp(finishTransaction);
-                        counterWallpaper.cleanUp(finishTransaction);
-                        // Release surface references now. This is apparently to free GPU memory
-                        // while doing quick operations (eg. during CTS).
-                        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                            info.getChanges().get(i).getLeash().release();
-                        }
-                        // Don't release here since launcher might still be using them. Instead
-                        // let launcher release them (eg. via RemoteAnimationTargets)
-                        leashMap.clear();
-                        try {
-                            finishCallback.onTransitionFinished(null /* wct */, finishTransaction);
-                        } catch (RemoteException e) {
-                            Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
-                                    + " finished callback", e);
-                        }
-                    }
-                };
-                synchronized (mFinishRunnables) {
-                    mFinishRunnables.put(token, animationFinishedCallback);
-                }
-                // TODO(bc-unlcok): Pass correct transit type.
-                remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE,
-                        appsCompat, wallpapersCompat, nonAppsCompat, () -> {
-                            synchronized (mFinishRunnables) {
-                                if (mFinishRunnables.remove(token) == null) return;
-                            }
-                            animationFinishedCallback.run();
-                        });
-            }
-
-            @Override
-            public void mergeAnimation(IBinder token, TransitionInfo info,
-                    SurfaceControl.Transaction t, IBinder mergeTarget,
-                    IRemoteTransitionFinishedCallback finishCallback) {
-                // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt
-                //       to legacy cancel.
-                final Runnable finishRunnable;
-                synchronized (mFinishRunnables) {
-                    finishRunnable = mFinishRunnables.remove(mergeTarget);
-                }
-                if (finishRunnable == null) return;
-                remoteAnimationAdapter.onAnimationCancelled();
-                finishRunnable.run();
-            }
-        };
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java
deleted file mode 100644
index ab55037..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.view.RemoteAnimationDefinition;
-
-/**
- * @see RemoteAnimationDefinition
- */
-public class RemoteAnimationDefinitionCompat {
-
-    private final RemoteAnimationDefinition mWrapped = new RemoteAnimationDefinition();
-
-    public void addRemoteAnimation(int transition, RemoteAnimationAdapterCompat adapter) {
-        mWrapped.addRemoteAnimation(transition, adapter.getWrapped());
-    }
-
-    public void addRemoteAnimation(int transition, int activityTypeFilter,
-            RemoteAnimationAdapterCompat adapter) {
-        mWrapped.addRemoteAnimation(transition, activityTypeFilter, adapter.getWrapped());
-    }
-
-    public RemoteAnimationDefinition getWrapped() {
-        return mWrapped;
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 0076292..93c8073 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -16,11 +16,197 @@
 
 package com.android.systemui.shared.system;
 
-import android.view.WindowManager;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_NONE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
-public interface RemoteAnimationRunnerCompat {
-    void onAnimationStart(@WindowManager.TransitionOldType int transit,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
-            RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback);
-    void onAnimationCancelled();
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.view.WindowManager.TransitionOldType;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.util.CounterRotator;
+
+public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub {
+
+    public abstract void onAnimationStart(@WindowManager.TransitionOldType int transit,
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonApps, Runnable finishedCallback);
+
+    @Override
+    public final void onAnimationStart(@TransitionOldType int transit,
+            RemoteAnimationTarget[] apps,
+            RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonApps,
+            final IRemoteAnimationFinishedCallback finishedCallback) {
+
+        onAnimationStart(transit, apps, wallpapers,
+                nonApps, () -> {
+                    try {
+                        finishedCallback.onAnimationFinished();
+                    } catch (RemoteException e) {
+                        Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
+                                + " finished callback", e);
+                    }
+                });
+    }
+
+    public IRemoteTransition toRemoteTransition() {
+        return new IRemoteTransition.Stub() {
+            final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>();
+
+            @Override
+            public void startAnimation(IBinder token, TransitionInfo info,
+                    SurfaceControl.Transaction t,
+                    IRemoteTransitionFinishedCallback finishCallback) {
+                final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
+                final RemoteAnimationTarget[] apps =
+                        RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
+                final RemoteAnimationTarget[] wallpapers =
+                        RemoteAnimationTargetCompat.wrapNonApps(
+                                info, true /* wallpapers */, t, leashMap);
+                final RemoteAnimationTarget[] nonApps =
+                        RemoteAnimationTargetCompat.wrapNonApps(
+                                info, false /* wallpapers */, t, leashMap);
+
+                // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
+                boolean isReturnToHome = false;
+                TransitionInfo.Change launcherTask = null;
+                TransitionInfo.Change wallpaper = null;
+                int launcherLayer = 0;
+                int rotateDelta = 0;
+                float displayW = 0;
+                float displayH = 0;
+                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                    final TransitionInfo.Change change = info.getChanges().get(i);
+                    // skip changes that we didn't wrap
+                    if (!leashMap.containsKey(change.getLeash())) continue;
+                    if (change.getTaskInfo() != null
+                            && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME) {
+                        isReturnToHome = change.getMode() == TRANSIT_OPEN
+                                || change.getMode() == TRANSIT_TO_FRONT;
+                        launcherTask = change;
+                        launcherLayer = info.getChanges().size() - i;
+                    } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
+                        wallpaper = change;
+                    }
+                    if (change.getParent() == null && change.getEndRotation() >= 0
+                            && change.getEndRotation() != change.getStartRotation()) {
+                        rotateDelta = change.getEndRotation() - change.getStartRotation();
+                        displayW = change.getEndAbsBounds().width();
+                        displayH = change.getEndAbsBounds().height();
+                    }
+                }
+
+                // Prepare for rotation if there is one
+                final CounterRotator counterLauncher = new CounterRotator();
+                final CounterRotator counterWallpaper = new CounterRotator();
+                if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) {
+                    counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(),
+                            rotateDelta, displayW, displayH);
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), launcherLayer);
+                    }
+                }
+
+                if (isReturnToHome) {
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3);
+                    }
+                    // Need to "boost" the closing things since that's what launcher expects.
+                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                        final TransitionInfo.Change change = info.getChanges().get(i);
+                        final SurfaceControl leash = leashMap.get(change.getLeash());
+                        // skip changes that we didn't wrap
+                        if (leash == null) continue;
+                        final int mode = info.getChanges().get(i).getMode();
+                        // Only deal with independent layers
+                        if (!TransitionInfo.isIndependent(change, info)) continue;
+                        if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
+                            t.setLayer(leash, info.getChanges().size() * 3 - i);
+                            counterLauncher.addChild(t, leash);
+                        }
+                    }
+                    // Make wallpaper visible immediately since launcher apparently won't do this.
+                    for (int i = wallpapers.length - 1; i >= 0; --i) {
+                        t.show(wallpapers[i].leash);
+                        t.setAlpha(wallpapers[i].leash, 1.f);
+                    }
+                } else {
+                    if (launcherTask != null) {
+                        counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash()));
+                    }
+                    if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) {
+                        counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(),
+                                rotateDelta, displayW, displayH);
+                        if (counterWallpaper.getSurface() != null) {
+                            t.setLayer(counterWallpaper.getSurface(), -1);
+                            counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash()));
+                        }
+                    }
+                }
+                t.apply();
+
+                final Runnable animationFinishedCallback = () -> {
+                    final SurfaceControl.Transaction finishTransaction =
+                            new SurfaceControl.Transaction();
+                    counterLauncher.cleanUp(finishTransaction);
+                    counterWallpaper.cleanUp(finishTransaction);
+                    // Release surface references now. This is apparently to free GPU memory
+                    // while doing quick operations (eg. during CTS).
+                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                        info.getChanges().get(i).getLeash().release();
+                    }
+                    // Don't release here since launcher might still be using them. Instead
+                    // let launcher release them (eg. via RemoteAnimationTargets)
+                    leashMap.clear();
+                    try {
+                        finishCallback.onTransitionFinished(null /* wct */, finishTransaction);
+                    } catch (RemoteException e) {
+                        Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
+                                + " finished callback", e);
+                    }
+                };
+                synchronized (mFinishRunnables) {
+                    mFinishRunnables.put(token, animationFinishedCallback);
+                }
+                // TODO(bc-unlcok): Pass correct transit type.
+                onAnimationStart(TRANSIT_OLD_NONE,
+                        apps, wallpapers, nonApps, () -> {
+                            synchronized (mFinishRunnables) {
+                                if (mFinishRunnables.remove(token) == null) return;
+                            }
+                            animationFinishedCallback.run();
+                        });
+            }
+
+            @Override
+            public void mergeAnimation(IBinder token, TransitionInfo info,
+                    SurfaceControl.Transaction t, IBinder mergeTarget,
+                    IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
+                // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt
+                //       to legacy cancel.
+                final Runnable finishRunnable;
+                synchronized (mFinishRunnables) {
+                    finishRunnable = mFinishRunnables.remove(mergeTarget);
+                }
+                if (finishRunnable == null) return;
+                onAnimationCancelled(false /* isKeyguardOccluded */);
+                finishRunnable.run();
+            }
+        };
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 2d6bef5..e1e8063 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -11,106 +11,50 @@
  * 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
+ * limitations under the License.
  */
 
 package com.android.systemui.shared.system;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.ArrayMap;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
 
 import java.util.ArrayList;
+import java.util.function.BiPredicate;
 
 /**
- * @see RemoteAnimationTarget
+ * Some utility methods for creating {@link RemoteAnimationTarget} instances.
  */
 public class RemoteAnimationTargetCompat {
 
-    public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING;
-    public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING;
-    public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING;
-    public final int mode;
-
-    public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-    public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-    public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME;
-    public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-    public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-    public final int activityType;
-
-    public final int taskId;
-    public final SurfaceControl leash;
-    public final boolean isTranslucent;
-    public final Rect clipRect;
-    public final int prefixOrderIndex;
-    public final Point position;
-    public final Rect localBounds;
-    public final Rect sourceContainerBounds;
-    public final Rect screenSpaceBounds;
-    public final Rect startScreenSpaceBounds;
-    public final boolean isNotInRecents;
-    public final Rect contentInsets;
-    public final ActivityManager.RunningTaskInfo taskInfo;
-    public final boolean allowEnterPip;
-    public final int rotationChange;
-    public final int windowType;
-    public final WindowConfiguration windowConfiguration;
-
-    private final SurfaceControl mStartLeash;
-
-    // Fields used only to unwrap into RemoteAnimationTarget
-    private final Rect startBounds;
-
-    public final boolean willShowImeOnTarget;
-
-    public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
-        taskId = app.taskId;
-        mode = app.mode;
-        leash = app.leash;
-        isTranslucent = app.isTranslucent;
-        clipRect = app.clipRect;
-        position = app.position;
-        localBounds = app.localBounds;
-        sourceContainerBounds = app.sourceContainerBounds;
-        screenSpaceBounds = app.screenSpaceBounds;
-        startScreenSpaceBounds = screenSpaceBounds;
-        prefixOrderIndex = app.prefixOrderIndex;
-        isNotInRecents = app.isNotInRecents;
-        contentInsets = app.contentInsets;
-        activityType = app.windowConfiguration.getActivityType();
-        taskInfo = app.taskInfo;
-        allowEnterPip = app.allowEnterPip;
-        rotationChange = app.rotationChange;
-
-        mStartLeash = app.startLeash;
-        windowType = app.windowType;
-        windowConfiguration = app.windowConfiguration;
-        startBounds = app.startBounds;
-        willShowImeOnTarget = app.willShowImeOnTarget;
-    }
-
     private static int newModeToLegacyMode(int newMode) {
         switch (newMode) {
             case WindowManager.TRANSIT_OPEN:
@@ -120,21 +64,10 @@
             case WindowManager.TRANSIT_TO_BACK:
                 return MODE_CLOSING;
             default:
-                return 2; // MODE_CHANGING
+                return MODE_CHANGING;
         }
     }
 
-    public RemoteAnimationTarget unwrap() {
-        final RemoteAnimationTarget target = new RemoteAnimationTarget(
-                taskId, mode, leash, isTranslucent, clipRect, contentInsets,
-                prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
-                isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
-        );
-        target.setWillShowImeOnTarget(willShowImeOnTarget);
-        target.setRotationChange(rotationChange);
-        return target;
-    }
-
     /**
      * Almost a copy of Transitions#setupStartState.
      * TODO: remove when there is proper cross-process transaction sync.
@@ -206,54 +139,61 @@
         return leashSurface;
     }
 
-    public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
-            TransitionInfo info, SurfaceControl.Transaction t) {
-        mode = newModeToLegacyMode(change.getMode());
+    /**
+     * Creates a new RemoteAnimationTarget from the provided change info
+     */
+    public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+            TransitionInfo info, SurfaceControl.Transaction t,
+            @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+        int taskId;
+        boolean isNotInRecents;
+        ActivityManager.RunningTaskInfo taskInfo;
+        WindowConfiguration windowConfiguration;
+
         taskInfo = change.getTaskInfo();
         if (taskInfo != null) {
             taskId = taskInfo.taskId;
             isNotInRecents = !taskInfo.isRunning;
-            activityType = taskInfo.getActivityType();
             windowConfiguration = taskInfo.configuration.windowConfiguration;
         } else {
             taskId = INVALID_TASK_ID;
             isNotInRecents = true;
-            activityType = ACTIVITY_TYPE_UNDEFINED;
             windowConfiguration = new WindowConfiguration();
         }
 
-        // TODO: once we can properly sync transactions across process, then get rid of this leash.
-        leash = createLeash(info, change, order, t);
-
-        isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0;
-        clipRect = null;
-        position = null;
-        localBounds = new Rect(change.getEndAbsBounds());
+        Rect localBounds = new Rect(change.getEndAbsBounds());
         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
-        sourceContainerBounds = null;
-        screenSpaceBounds = new Rect(change.getEndAbsBounds());
-        startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
 
-        prefixOrderIndex = order;
-        // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
-        contentInsets = new Rect(0, 0, 0, 0);
-        allowEnterPip = change.getAllowEnterPip();
-        mStartLeash = null;
-        rotationChange = change.getEndRotation() - change.getStartRotation();
-        windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
-                ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
-
-        startBounds = change.getStartAbsBounds();
-        willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
-    }
-
-    public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
-        final int length = apps != null ? apps.length : 0;
-        final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length];
-        for (int i = 0; i < length; i++) {
-            appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+        RemoteAnimationTarget target = new RemoteAnimationTarget(
+                taskId,
+                newModeToLegacyMode(change.getMode()),
+                // TODO: once we can properly sync transactions across process,
+                // then get rid of this leash.
+                createLeash(info, change, order, t),
+                (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+                null,
+                // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+                new Rect(0, 0, 0, 0),
+                order,
+                null,
+                localBounds,
+                new Rect(change.getEndAbsBounds()),
+                windowConfiguration,
+                isNotInRecents,
+                null,
+                new Rect(change.getStartAbsBounds()),
+                taskInfo,
+                change.getAllowEnterPip(),
+                (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+                        ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE
+        );
+        target.setWillShowImeOnTarget(
+                (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+        target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+        if (leashMap != null) {
+            leashMap.put(change.getLeash(), target.leash);
         }
-        return appsCompat;
+        return target;
     }
 
     /**
@@ -262,35 +202,20 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+    public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-        final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
-
-            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+        SparseBooleanArray childTaskTargets = new SparseBooleanArray();
+        return wrap(info, t, leashMap, (change, taskInfo) -> {
             // Children always come before parent since changes are in top-to-bottom z-order.
-            if (taskInfo != null) {
-                if (childTaskTargets.contains(taskInfo.taskId)) {
-                    // has children, so not a leaf. Skip.
-                    continue;
-                }
-                if (taskInfo.hasParentTask()) {
-                    childTaskTargets.put(taskInfo.parentTaskId, change);
-                }
+            if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) {
+                // has children, so not a leaf. Skip.
+                return false;
             }
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
+            if (taskInfo.hasParentTask()) {
+                childTaskTargets.put(taskInfo.parentTaskId, true);
             }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+            return true;
+        });
     }
 
     /**
@@ -301,38 +226,23 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers,
+    public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() != null) continue;
-
-            final boolean changeIsWallpaper =
-                    (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
-            if (wallpapers != changeIsWallpaper) continue;
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
-            }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+        return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
+                && wallpapers == change.hasFlags(FLAG_IS_WALLPAPER)
+                && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY));
     }
 
-    /**
-     * @see SurfaceControl#release()
-     */
-    public void release() {
-        if (leash != null) {
-            leash.release();
+    private static RemoteAnimationTarget[] wrap(TransitionInfo info,
+            SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+            BiPredicate<Change, TaskInfo> filter) {
+        final ArrayList<RemoteAnimationTarget> out = new ArrayList<>();
+        for (int i = 0; i < info.getChanges().size(); i++) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (filter.test(change, change.getTaskInfo())) {
+                out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap));
+            }
         }
-        if (mStartLeash != null) {
-            mStartLeash.release();
-        }
+        return out.toArray(new RemoteAnimationTarget[out.size()]);
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.aidl
deleted file mode 100644
index 1550ab3..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.aidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.system;
-
-parcelable RemoteTransitionCompat;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index f679225..d4d3d25 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -17,106 +17,52 @@
 package com.android.systemui.shared.system;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
-import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.IApplicationThread;
-import android.content.ComponentName;
 import android.graphics.Rect;
 import android.os.IBinder;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.IRecentsAnimationController;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
 import android.window.PictureInPictureSurfaceTransaction;
 import android.window.RemoteTransition;
 import android.window.TaskSnapshot;
-import android.window.TransitionFilter;
 import android.window.TransitionInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.DataClass;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 import java.util.ArrayList;
-import java.util.concurrent.Executor;
 
 /**
- * Wrapper to expose RemoteTransition (shell transitions) to Launcher.
- *
- * @see IRemoteTransition
- * @see TransitionFilter
+ * Helper class to build {@link RemoteTransition} objects
  */
-@DataClass
-public class RemoteTransitionCompat implements Parcelable {
+public class RemoteTransitionCompat {
     private static final String TAG = "RemoteTransitionCompat";
 
-    @NonNull final RemoteTransition mTransition;
-    @Nullable TransitionFilter mFilter = null;
-
-    RemoteTransitionCompat(RemoteTransition transition) {
-        mTransition = transition;
-    }
-
-    public RemoteTransitionCompat(@NonNull RemoteTransitionRunner runner,
-            @NonNull Executor executor, @Nullable IApplicationThread appThread) {
-        IRemoteTransition remote = new IRemoteTransition.Stub() {
-            @Override
-            public void startAnimation(IBinder transition, TransitionInfo info,
-                    SurfaceControl.Transaction t,
-                    IRemoteTransitionFinishedCallback finishedCallback) {
-                final Runnable finishAdapter = () ->  {
-                    try {
-                        finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to call transition finished callback", e);
-                    }
-                };
-                executor.execute(() -> runner.startAnimation(transition, info, t, finishAdapter));
-            }
-
-            @Override
-            public void mergeAnimation(IBinder transition, TransitionInfo info,
-                    SurfaceControl.Transaction t, IBinder mergeTarget,
-                    IRemoteTransitionFinishedCallback finishedCallback) {
-                final Runnable finishAdapter = () ->  {
-                    try {
-                        finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Failed to call transition finished callback", e);
-                    }
-                };
-                executor.execute(() -> runner.mergeAnimation(transition, info, t, mergeTarget,
-                        finishAdapter));
-            }
-        };
-        mTransition = new RemoteTransition(remote, appThread);
-    }
-
     /** Constructor specifically for recents animation */
-    public RemoteTransitionCompat(RecentsAnimationListener recents,
+    public static RemoteTransition newRemoteTransition(RecentsAnimationListener recents,
             RecentsAnimationControllerCompat controller, IApplicationThread appThread) {
         IRemoteTransition remote = new IRemoteTransition.Stub() {
             final RecentsControllerWrap mRecentsSession = new RecentsControllerWrap();
@@ -127,9 +73,9 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] apps =
+                final RemoteAnimationTarget[] apps =
                         RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapers =
+                final RemoteAnimationTarget[] wallpapers =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, true /* wallpapers */, t, leashMap);
                 // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
@@ -191,25 +137,7 @@
                 mRecentsSession.commitTasksAppearedIfNeeded(recents);
             }
         };
-        mTransition = new RemoteTransition(remote, appThread);
-    }
-
-    /** Adds a filter check that restricts this remote transition to home open transitions. */
-    public void addHomeOpenCheck(ComponentName homeActivity) {
-        if (mFilter == null) {
-            mFilter = new TransitionFilter();
-        }
-        // No need to handle the transition that also dismisses keyguard.
-        mFilter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
-        mFilter.mRequirements =
-                new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
-                        new TransitionFilter.Requirement()};
-        mFilter.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
-        mFilter.mRequirements[0].mTopActivity = homeActivity;
-        mFilter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
-        mFilter.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
-        mFilter.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
-        mFilter.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+        return new RemoteTransition(remote, appThread);
     }
 
     /**
@@ -230,7 +158,7 @@
         private PictureInPictureSurfaceTransaction mPipTransaction = null;
         private IBinder mTransition = null;
         private boolean mKeyguardLocked = false;
-        private RemoteAnimationTargetCompat[] mAppearedTargets;
+        private RemoteAnimationTarget[] mAppearedTargets;
         private boolean mWillFinishToHome = false;
 
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
@@ -325,18 +253,15 @@
             final int layer = mInfo.getChanges().size() * 3;
             mOpeningLeashes = new ArrayList<>();
             mOpeningHome = cancelRecents;
-            final RemoteAnimationTargetCompat[] targets =
-                    new RemoteAnimationTargetCompat[openingTasks.size()];
+            final RemoteAnimationTarget[] targets =
+                    new RemoteAnimationTarget[openingTasks.size()];
             for (int i = 0; i < openingTasks.size(); ++i) {
                 final TransitionInfo.Change change = openingTasks.valueAt(i);
                 mOpeningLeashes.add(change.getLeash());
                 // We are receiving new opening tasks, so convert to onTasksAppeared.
-                final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat(
-                        change, layer, info, t);
-                mLeashMap.put(mOpeningLeashes.get(i), target.leash);
-                t.reparent(target.leash, mInfo.getRootLeash());
-                t.setLayer(target.leash, layer);
-                targets[i] = target;
+                targets[i] = newTarget(change, layer, info, t, mLeashMap);
+                t.reparent(targets[i].leash, mInfo.getRootLeash());
+                t.setLayer(targets[i].leash, layer);
             }
             t.apply();
             mAppearedTargets = targets;
@@ -506,161 +431,4 @@
         @Override public void animateNavigationBarToApp(long duration) {
         }
     }
-
-
-
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
-    @DataClass.Generated.Member
-    /* package-private */ RemoteTransitionCompat(
-            @NonNull RemoteTransition transition,
-            @Nullable TransitionFilter filter) {
-        this.mTransition = transition;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mTransition);
-        this.mFilter = filter;
-
-        // onConstructed(); // You can define this method to get a callback
-    }
-
-    @DataClass.Generated.Member
-    public @NonNull RemoteTransition getTransition() {
-        return mTransition;
-    }
-
-    @DataClass.Generated.Member
-    public @Nullable TransitionFilter getFilter() {
-        return mFilter;
-    }
-
-    @Override
-    @DataClass.Generated.Member
-    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
-        // You can override field parcelling by defining methods like:
-        // void parcelFieldName(Parcel dest, int flags) { ... }
-
-        byte flg = 0;
-        if (mFilter != null) flg |= 0x2;
-        dest.writeByte(flg);
-        dest.writeTypedObject(mTransition, flags);
-        if (mFilter != null) dest.writeTypedObject(mFilter, flags);
-    }
-
-    @Override
-    @DataClass.Generated.Member
-    public int describeContents() { return 0; }
-
-    /** @hide */
-    @SuppressWarnings({"unchecked", "RedundantCast"})
-    @DataClass.Generated.Member
-    protected RemoteTransitionCompat(@NonNull android.os.Parcel in) {
-        // You can override field unparcelling by defining methods like:
-        // static FieldType unparcelFieldName(Parcel in) { ... }
-
-        byte flg = in.readByte();
-        RemoteTransition transition = (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR);
-        TransitionFilter filter = (flg & 0x2) == 0 ? null : (TransitionFilter) in.readTypedObject(TransitionFilter.CREATOR);
-
-        this.mTransition = transition;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mTransition);
-        this.mFilter = filter;
-
-        // onConstructed(); // You can define this method to get a callback
-    }
-
-    @DataClass.Generated.Member
-    public static final @NonNull Parcelable.Creator<RemoteTransitionCompat> CREATOR
-            = new Parcelable.Creator<RemoteTransitionCompat>() {
-        @Override
-        public RemoteTransitionCompat[] newArray(int size) {
-            return new RemoteTransitionCompat[size];
-        }
-
-        @Override
-        public RemoteTransitionCompat createFromParcel(@NonNull android.os.Parcel in) {
-            return new RemoteTransitionCompat(in);
-        }
-    };
-
-    /**
-     * A builder for {@link RemoteTransitionCompat}
-     */
-    @SuppressWarnings("WeakerAccess")
-    @DataClass.Generated.Member
-    public static class Builder {
-
-        private @NonNull RemoteTransition mTransition;
-        private @Nullable TransitionFilter mFilter;
-
-        private long mBuilderFieldsSet = 0L;
-
-        public Builder(
-                @NonNull RemoteTransition transition) {
-            mTransition = transition;
-            com.android.internal.util.AnnotationValidations.validate(
-                    NonNull.class, null, mTransition);
-        }
-
-        @DataClass.Generated.Member
-        public @NonNull Builder setTransition(@NonNull RemoteTransition value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
-            mTransition = value;
-            return this;
-        }
-
-        @DataClass.Generated.Member
-        public @NonNull Builder setFilter(@NonNull TransitionFilter value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x2;
-            mFilter = value;
-            return this;
-        }
-
-        /** Builds the instance. This builder should not be touched after calling this! */
-        public @NonNull RemoteTransitionCompat build() {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x4; // Mark builder used
-
-            if ((mBuilderFieldsSet & 0x2) == 0) {
-                mFilter = null;
-            }
-            RemoteTransitionCompat o = new RemoteTransitionCompat(
-                    mTransition,
-                    mFilter);
-            return o;
-        }
-
-        private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x4) != 0) {
-                throw new IllegalStateException(
-                        "This Builder should not be reused. Use a new Builder instance instead");
-            }
-        }
-    }
-
-    @DataClass.Generated(
-            time = 1629321609807L,
-            codegenVersion = "1.0.23",
-            sourceFile = "frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java",
-            inputSignatures = "private static final  java.lang.String TAG\nfinal @android.annotation.NonNull android.window.RemoteTransition mTransition\n @android.annotation.Nullable android.window.TransitionFilter mFilter\npublic  void addHomeOpenCheck(android.content.ComponentName)\nclass RemoteTransitionCompat extends java.lang.Object implements [android.os.Parcelable]\nprivate  com.android.systemui.shared.system.RecentsAnimationControllerCompat mWrapped\nprivate  android.window.IRemoteTransitionFinishedCallback mFinishCB\nprivate  android.window.WindowContainerToken mPausingTask\nprivate  android.window.WindowContainerToken mPipTask\nprivate  android.window.TransitionInfo mInfo\nprivate  android.view.SurfaceControl mOpeningLeash\nprivate  android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl> mLeashMap\nprivate  android.window.PictureInPictureSurfaceTransaction mPipTransaction\nprivate  android.os.IBinder mTransition\n  void setup(com.android.systemui.shared.system.RecentsAnimationControllerCompat,android.window.TransitionInfo,android.window.IRemoteTransitionFinishedCallback,android.window.WindowContainerToken,android.window.WindowContainerToken,android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl>,android.os.IBinder)\n @android.annotation.SuppressLint boolean merge(android.window.TransitionInfo,android.view.SurfaceControl.Transaction,com.android.systemui.shared.system.RecentsAnimationListener)\npublic @java.lang.Override com.android.systemui.shared.recents.model.ThumbnailData screenshotTask(int)\npublic @java.lang.Override void setInputConsumerEnabled(boolean)\npublic @java.lang.Override void setAnimationTargetsBehindSystemBars(boolean)\npublic @java.lang.Override void hideCurrentInputMethod()\npublic @java.lang.Override void setFinishTaskTransaction(int,android.window.PictureInPictureSurfaceTransaction,android.view.SurfaceControl)\npublic @java.lang.Override @android.annotation.SuppressLint void finish(boolean,boolean)\npublic @java.lang.Override void setDeferCancelUntilNextTransition(boolean,boolean)\npublic @java.lang.Override void cleanupScreenshot()\npublic @java.lang.Override void setWillFinishToHome(boolean)\npublic @java.lang.Override boolean removeTask(int)\npublic @java.lang.Override void detachNavigationBarFromApp(boolean)\npublic @java.lang.Override void animateNavigationBarToApp(long)\nclass RecentsControllerWrap extends com.android.systemui.shared.system.RecentsAnimationControllerCompat implements []\n@com.android.internal.util.DataClass")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java
deleted file mode 100644
index accc456..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java
+++ /dev/null
@@ -1,42 +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.
- */
-
-package com.android.systemui.shared.system;
-
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.window.TransitionInfo;
-
-/** Interface for something that runs a remote transition animation. */
-public interface RemoteTransitionRunner {
-    /**
-     * Starts a transition animation. Once complete, the implementation should call
-     * `finishCallback`.
-     */
-    void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
-            Runnable finishCallback);
-
-    /**
-     * Attempts to merge a transition into the currently-running animation. If merge is not
-     * possible/supported, this should do nothing. Otherwise, the implementation should call
-     * `finishCallback` immediately to indicate that it merged the transition.
-     *
-     * @param transition The transition that wants to be merged into the running animation.
-     * @param mergeTarget The transition to merge into (that this runner is currently animating).
-     */
-    default void mergeAnimation(IBinder transition, TransitionInfo info,
-            SurfaceControl.Transaction t, IBinder mergeTarget, Runnable finishCallback) { }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
deleted file mode 100644
index 30c062b..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.HardwareRenderer;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Message;
-import android.os.Trace;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-import android.view.ViewRootImpl;
-
-import java.util.function.Consumer;
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread.
- *
- * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't 
- *       currently reference that class from the shared lib as it is hidden.
- */
-public class SyncRtSurfaceTransactionApplierCompat {
-
-    public static final int FLAG_ALL = 0xffffffff;
-    public static final int FLAG_ALPHA = 1;
-    public static final int FLAG_MATRIX = 1 << 1;
-    public static final int FLAG_WINDOW_CROP = 1 << 2;
-    public static final int FLAG_LAYER = 1 << 3;
-    public static final int FLAG_CORNER_RADIUS = 1 << 4;
-    public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
-    public static final int FLAG_VISIBILITY = 1 << 6;
-    public static final int FLAG_RELATIVE_LAYER = 1 << 7;
-    public static final int FLAG_SHADOW_RADIUS = 1 << 8;
-
-    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
-    private final SurfaceControl mBarrierSurfaceControl;
-    private final ViewRootImpl mTargetViewRootImpl;
-    private final Handler mApplyHandler;
-
-    private int mSequenceNumber = 0;
-    private int mPendingSequenceNumber = 0;
-    private Runnable mAfterApplyCallback;
-
-    /**
-     * @param targetView The view in the surface that acts as synchronization anchor.
-     */
-    public SyncRtSurfaceTransactionApplierCompat(View targetView) {
-        mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
-        mBarrierSurfaceControl = mTargetViewRootImpl != null
-            ? mTargetViewRootImpl.getSurfaceControl() : null;
-
-        mApplyHandler = new Handler(new Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
-                    onApplyMessage(msg.arg1);
-                    return true;
-                }
-                return false;
-            }
-        });
-    }
-
-    private void onApplyMessage(int seqNo) {
-        mSequenceNumber = seqNo;
-        if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
-            Runnable r = mAfterApplyCallback;
-            mAfterApplyCallback = null;
-            r.run();
-        }
-    }
-
-    /**
-     * Schedules applying surface parameters on the next frame.
-     *
-     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
-     *               this method to avoid synchronization issues.
-     */
-    public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
-        if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
-            return;
-        }
-
-        mPendingSequenceNumber++;
-        final int toApplySeqNo = mPendingSequenceNumber;
-        mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
-            @Override
-            public void onFrameDraw(long frame) {
-                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
-                    Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                            .sendToTarget();
-                    return;
-                }
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
-                Transaction t = new Transaction();
-                for (int i = params.length - 1; i >= 0; i--) {
-                    SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
-                            params[i];
-                    surfaceParams.applyTo(t);
-                }
-                if (mTargetViewRootImpl != null) {
-                    mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
-                } else {
-                    t.apply();
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                        .sendToTarget();
-            }
-        });
-
-        // Make sure a frame gets scheduled.
-        mTargetViewRootImpl.getView().invalidate();
-    }
-
-    /**
-     * Calls the runnable when any pending apply calls have completed
-     */
-    public void addAfterApplyCallback(final Runnable afterApplyCallback) {
-        if (mSequenceNumber == mPendingSequenceNumber) {
-            afterApplyCallback.run();
-        } else {
-            if (mAfterApplyCallback == null) {
-                mAfterApplyCallback = afterApplyCallback;
-            } else {
-                final Runnable oldCallback = mAfterApplyCallback;
-                mAfterApplyCallback = new Runnable() {
-                    @Override
-                    public void run() {
-                        afterApplyCallback.run();
-                        oldCallback.run();
-                    }
-                };
-            }
-        }
-    }
-
-    public static void applyParams(TransactionCompat t,
-            SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
-        params.applyTo(t.mTransaction);
-    }
-
-    /**
-     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
-     * attached if necessary.
-     */
-    public static void create(final View targetView,
-            final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
-        if (targetView == null) {
-            // No target view, no applier
-            callback.accept(null);
-        } else if (targetView.getViewRootImpl() != null) {
-            // Already attached, we're good to go
-            callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-        } else {
-            // Haven't been attached before we can get the view root
-            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    targetView.removeOnAttachStateChangeListener(this);
-                    callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                    // Do nothing
-                }
-            });
-        }
-    }
-
-    public static class SurfaceParams {
-        public static class Builder {
-            final SurfaceControl surface;
-            int flags;
-            float alpha;
-            float cornerRadius;
-            int backgroundBlurRadius;
-            Matrix matrix;
-            Rect windowCrop;
-            int layer;
-            SurfaceControl relativeTo;
-            int relativeLayer;
-            boolean visible;
-            float shadowRadius;
-
-            /**
-             * @param surface The surface to modify.
-             */
-            public Builder(SurfaceControl surface) {
-                this.surface = surface;
-            }
-
-            /**
-             * @param alpha The alpha value to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withAlpha(float alpha) {
-                this.alpha = alpha;
-                flags |= FLAG_ALPHA;
-                return this;
-            }
-
-            /**
-             * @param matrix The matrix to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withMatrix(Matrix matrix) {
-                this.matrix = new Matrix(matrix);
-                flags |= FLAG_MATRIX;
-                return this;
-            }
-
-            /**
-             * @param windowCrop The window crop to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withWindowCrop(Rect windowCrop) {
-                this.windowCrop = new Rect(windowCrop);
-                flags |= FLAG_WINDOW_CROP;
-                return this;
-            }
-
-            /**
-             * @param layer The layer to assign the surface.
-             * @return this Builder
-             */
-            public Builder withLayer(int layer) {
-                this.layer = layer;
-                flags |= FLAG_LAYER;
-                return this;
-            }
-
-            /**
-             * @param relativeTo The surface that's set relative layer to.
-             * @param relativeLayer The relative layer.
-             * @return this Builder
-             */
-            public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
-                this.relativeTo = relativeTo;
-                this.relativeLayer = relativeLayer;
-                flags |= FLAG_RELATIVE_LAYER;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for rounded corners to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withCornerRadius(float radius) {
-                this.cornerRadius = radius;
-                flags |= FLAG_CORNER_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for the shadows to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withShadowRadius(float radius) {
-                this.shadowRadius = radius;
-                flags |= FLAG_SHADOW_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for blur to apply to the background surfaces.
-             * @return this Builder
-             */
-            public Builder withBackgroundBlur(int radius) {
-                this.backgroundBlurRadius = radius;
-                flags |= FLAG_BACKGROUND_BLUR_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param visible The visibility to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withVisibility(boolean visible) {
-                this.visible = visible;
-                flags |= FLAG_VISIBILITY;
-                return this;
-            }
-
-            /**
-             * @return a new SurfaceParams instance
-             */
-            public SurfaceParams build() {
-                return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
-                        relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
-                        shadowRadius);
-            }
-        }
-
-        private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
-                Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
-                float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
-            this.flags = flags;
-            this.surface = surface;
-            this.alpha = alpha;
-            this.matrix = matrix;
-            this.windowCrop = windowCrop;
-            this.layer = layer;
-            this.relativeTo = relativeTo;
-            this.relativeLayer = relativeLayer;
-            this.cornerRadius = cornerRadius;
-            this.backgroundBlurRadius = backgroundBlurRadius;
-            this.visible = visible;
-            this.shadowRadius = shadowRadius;
-        }
-
-        private final int flags;
-        private final float[] mTmpValues = new float[9];
-
-        public final SurfaceControl surface;
-        public final float alpha;
-        public final float cornerRadius;
-        public final int backgroundBlurRadius;
-        public final Matrix matrix;
-        public final Rect windowCrop;
-        public final int layer;
-        public final SurfaceControl relativeTo;
-        public final int relativeLayer;
-        public final boolean visible;
-        public final float shadowRadius;
-
-        public void applyTo(SurfaceControl.Transaction t) {
-            if ((flags & FLAG_MATRIX) != 0) {
-                t.setMatrix(surface, matrix, mTmpValues);
-            }
-            if ((flags & FLAG_WINDOW_CROP) != 0) {
-                t.setWindowCrop(surface, windowCrop);
-            }
-            if ((flags & FLAG_ALPHA) != 0) {
-                t.setAlpha(surface, alpha);
-            }
-            if ((flags & FLAG_LAYER) != 0) {
-                t.setLayer(surface, layer);
-            }
-            if ((flags & FLAG_CORNER_RADIUS) != 0) {
-                t.setCornerRadius(surface, cornerRadius);
-            }
-            if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
-                t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
-            }
-            if ((flags & FLAG_VISIBILITY) != 0) {
-                if (visible) {
-                    t.show(surface);
-                } else {
-                    t.hide(surface);
-                }
-            }
-            if ((flags & FLAG_RELATIVE_LAYER) != 0) {
-                t.setRelativeLayer(surface, relativeTo, relativeLayer);
-            }
-            if ((flags & FLAG_SHADOW_RADIUS) != 0) {
-                t.setShadowRadius(surface, shadowRadius);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
deleted file mode 100644
index 43a882a5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-
-public class TransactionCompat {
-
-    final Transaction mTransaction;
-
-    final float[] mTmpValues = new float[9];
-
-    public TransactionCompat() {
-        mTransaction = new Transaction();
-    }
-
-    public void apply() {
-        mTransaction.apply();
-    }
-
-    public TransactionCompat show(SurfaceControl surfaceControl) {
-        mTransaction.show(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat hide(SurfaceControl surfaceControl) {
-        mTransaction.hide(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
-        mTransaction.setPosition(surfaceControl, x, y);
-        return this;
-    }
-
-    public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
-        mTransaction.setBufferSize(surfaceControl, w, h);
-        return this;
-    }
-
-    public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
-        mTransaction.setLayer(surfaceControl, z);
-        return this;
-    }
-
-    public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
-        mTransaction.setAlpha(surfaceControl, alpha);
-        return this;
-    }
-
-    public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
-        mTransaction.setOpaque(surfaceControl, opaque);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
-            float dtdy, float dsdy) {
-        mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
-        mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
-        return this;
-    }
-
-    public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
-        mTransaction.setWindowCrop(surfaceControl, crop);
-        return this;
-    }
-
-    public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
-        mTransaction.setCornerRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
-        mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
-        mTransaction.setColor(surfaceControl, color);
-        return this;
-    }
-
-    public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
-            SurfaceControl relativeTo, int z) {
-        t.setRelativeLayer(surfaceControl, relativeTo, z);
-    }
-}
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
new file mode 100644
index 0000000..05372fe
--- /dev/null
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsFactory.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.annotation.BoolRes
+
+object FlagsFactory {
+    private val flagMap = mutableMapOf<String, Flag<*>>()
+
+    val knownFlags: Map<String, Flag<*>>
+        get() {
+            // We need to access Flags in order to initialize our map.
+            assert(flagMap.contains(Flags.TEAMFOOD.name)) { "Where is teamfood?" }
+            return flagMap
+        }
+
+    fun unreleasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): UnreleasedFlag {
+        val flag = UnreleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun releasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ReleasedFlag {
+        val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun resourceBooleanFlag(
+        id: Int,
+        @BoolRes resourceId: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ResourceBooleanFlag {
+        val flag =
+            ResourceBooleanFlag(
+                id = id,
+                name = name,
+                namespace = namespace,
+                resourceId = resourceId,
+                teamfood = teamfood
+            )
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    fun sysPropBooleanFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        default: Boolean = false
+    ): SysPropBooleanFlag {
+        val flag =
+            SysPropBooleanFlag(id = id, name = name, namespace = "systemui", default = default)
+        FlagsFactory.checkForDupesAndAdd(flag)
+        return flag
+    }
+
+    private fun checkForDupesAndAdd(flag: Flag<*>) {
+        if (flagMap.containsKey(flag.name)) {
+            throw IllegalArgumentException("Name {flag.name} is already registered")
+        }
+        flagMap.forEach {
+            if (it.value.id == flag.id) {
+                throw IllegalArgumentException("Name {flag.id} is already registered")
+            }
+        }
+        flagMap[flag.name] = flag
+    }
+}
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..8323d09 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,
 ])
@@ -36,6 +34,9 @@
     @Binds
     abstract fun bindsFeatureFlagDebug(impl: FeatureFlagsDebug): FeatureFlags
 
+    @Binds
+    abstract fun bindsRestarter(debugRestarter: FeatureFlagsDebugRestarter): Restarter
+
     @Module
     companion object {
         @JvmStatic
@@ -43,20 +44,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/FlagsFactory.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt
new file mode 100644
index 0000000..27c5699
--- /dev/null
+++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsFactory.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags
+
+import android.annotation.BoolRes
+
+object FlagsFactory {
+    private val flagMap = mutableMapOf<String, Flag<*>>()
+
+    val knownFlags: Map<String, Flag<*>>
+        get() {
+            // We need to access Flags in order to initialize our map.
+            assert(flagMap.contains(Flags.TEAMFOOD.name)) { "Where is teamfood?" }
+            return flagMap
+        }
+
+    fun unreleasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): UnreleasedFlag {
+        // Unreleased flags are always false in this build.
+        val flag = UnreleasedFlag(id = id, name = "", namespace = "", teamfood = false)
+        return flag
+    }
+
+    fun releasedFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ReleasedFlag {
+        val flag = ReleasedFlag(id = id, name = name, namespace = namespace, teamfood = teamfood)
+        flagMap[name] = flag
+        return flag
+    }
+
+    fun resourceBooleanFlag(
+        id: Int,
+        @BoolRes resourceId: Int,
+        name: String,
+        namespace: String = "systemui",
+        teamfood: Boolean = false
+    ): ResourceBooleanFlag {
+        val flag =
+            ResourceBooleanFlag(
+                id = id,
+                name = name,
+                namespace = namespace,
+                resourceId = resourceId,
+                teamfood = teamfood
+            )
+        flagMap[name] = flag
+        return flag
+    }
+
+    fun sysPropBooleanFlag(
+        id: Int,
+        name: String,
+        namespace: String = "systemui",
+        default: Boolean = false
+    ): SysPropBooleanFlag {
+        val flag =
+            SysPropBooleanFlag(id = id, name = name, namespace = namespace, default = default)
+        flagMap[name] = flag
+        return flag
+    }
+}
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..87beff7 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,18 @@
 
 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()
-                }
-            }
-        }
-    }
+    @Binds
+    abstract fun bindsRestarter(debugRestarter: FeatureFlagsReleaseRestarter): Restarter
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 0075ddd..450784e 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -16,19 +16,29 @@
 
 package com.android.keyguard
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
 import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.TypedArray
 import android.graphics.Color
 import android.util.AttributeSet
+import android.view.View
 import com.android.settingslib.Utils
+import com.android.systemui.animation.Interpolators
 
 /** Displays security messages for the keyguard bouncer. */
-class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
+open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
     KeyguardMessageArea(context, attrs) {
     private val DEFAULT_COLOR = -1
     private var mDefaultColorState: ColorStateList? = null
     private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR)
+    private val animatorSet = AnimatorSet()
+    private var textAboutToShow: CharSequence? = null
+    protected open val SHOW_DURATION_MILLIS = 150L
+    protected open val HIDE_DURATION_MILLIS = 200L
 
     override fun updateTextColor() {
         var colorState = mDefaultColorState
@@ -58,4 +68,46 @@
         mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary)
         super.reloadColor()
     }
+
+    override fun setMessage(msg: CharSequence?) {
+        if ((msg == textAboutToShow && msg != null) || msg == text) {
+            return
+        }
+        textAboutToShow = msg
+
+        if (animatorSet.isRunning) {
+            animatorSet.cancel()
+            textAboutToShow = null
+        }
+
+        val hideAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply {
+                duration = HIDE_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_ACCELERATE
+            }
+
+        hideAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    super@BouncerKeyguardMessageArea.setMessage(msg)
+                }
+            }
+        )
+        val showAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply {
+                duration = SHOW_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_DECELERATE
+            }
+
+        showAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    textAboutToShow = null
+                }
+            }
+        )
+
+        animatorSet.playSequentially(hideAnimator, showAnimator)
+        animatorSet.start()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 9151238..87e9d56 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -23,13 +23,24 @@
 import android.text.format.DateFormat
 import android.util.TypedValue
 import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
+import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.dagger.KeyguardClockLog
 import com.android.systemui.plugins.ClockController
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shared.regionsampling.RegionSamplingInstance
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.shared.regionsampling.RegionSampler
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -38,13 +49,21 @@
 import java.util.TimeZone
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
  */
 open class ClockEventController @Inject constructor(
-    private val statusBarStateController: StatusBarStateController,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val batteryController: BatteryController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -53,14 +72,20 @@
     private val context: Context,
     @Main private val mainExecutor: Executor,
     @Background private val bgExecutor: Executor,
-    private val featureFlags: FeatureFlags,
+    @KeyguardClockLog private val logBuffer: LogBuffer?,
+    private val featureFlags: FeatureFlags
 ) {
     var clock: ClockController? = null
         set(value) {
             field = value
             if (value != null) {
+                if (logBuffer != null) {
+                    value.setLogBuffer(logBuffer)
+                }
+
                 value.initialize(resources, dozeAmount, 0f)
                 updateRegionSamplers(value)
+                updateFontSizes()
             }
         }
 
@@ -70,9 +95,9 @@
     private var isCharging = false
     private var dozeAmount = 0f
     private var isKeyguardVisible = false
-
-    private val regionSamplingEnabled =
-            featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING)
+    private var isRegistered = false
+    private var disposableHandle: DisposableHandle? = null
+    private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
 
     private fun updateColors() {
         if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
@@ -121,21 +146,17 @@
             bgExecutor: Executor?,
             regionSamplingEnabled: Boolean,
             updateColors: () -> Unit
-    ): RegionSamplingInstance {
-        return RegionSamplingInstance(
+    ): RegionSampler {
+        return RegionSampler(
             sampledView,
             mainExecutor,
             bgExecutor,
             regionSamplingEnabled,
-            object : RegionSamplingInstance.UpdateColorCallback {
-                override fun updateColors() {
-                    updateColors()
-                }
-            })
+            updateColors)
     }
 
-    var smallRegionSampler: RegionSamplingInstance? = null
-    var largeRegionSampler: RegionSamplingInstance? = null
+    var smallRegionSampler: RegionSampler? = null
+    var largeRegionSampler: RegionSampler? = null
 
     private var smallClockIsDark = true
     private var largeClockIsDark = true
@@ -143,10 +164,11 @@
     private val configListener = object : ConfigurationController.ConfigurationListener {
         override fun onThemeChanged() {
             clock?.events?.onColorPaletteChanged(resources)
+            updateColors()
         }
 
         override fun onDensityOrFontScaleChanged() {
-            clock?.events?.onFontSettingChanged()
+            updateFontSizes()
         }
     }
 
@@ -165,20 +187,13 @@
         }
     }
 
-    private val statusBarStateListener = object : StatusBarStateController.StateListener {
-        override fun onDozeAmountChanged(linear: Float, eased: Float) {
-            clock?.animations?.doze(linear)
-
-            isDozing = linear > dozeAmount
-            dozeAmount = linear
-        }
-    }
-
     private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
         override fun onKeyguardVisibilityChanged(visible: Boolean) {
             isKeyguardVisible = visible
-            if (!isKeyguardVisible) {
-                clock?.animations?.doze(if (isDozing) 1f else 0f)
+            if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                if (!isKeyguardVisible) {
+                    clock?.animations?.doze(if (isDozing) 1f else 0f)
+                }
             }
         }
 
@@ -195,13 +210,11 @@
         }
     }
 
-    init {
-        isDozing = statusBarStateController.isDozing
-    }
-
-    fun registerListeners() {
-        dozeAmount = statusBarStateController.dozeAmount
-        isDozing = statusBarStateController.isDozing || dozeAmount != 0f
+    fun registerListeners(parent: View) {
+        if (isRegistered) {
+            return
+        }
+        isRegistered = true
 
         broadcastDispatcher.registerReceiver(
             localeBroadcastReceiver,
@@ -210,21 +223,43 @@
         configurationController.addCallback(configListener)
         batteryController.addCallback(batteryCallback)
         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.addCallback(statusBarStateListener)
         smallRegionSampler?.startRegionSampler()
         largeRegionSampler?.startRegionSampler()
+        disposableHandle = parent.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                listenForDozing(this)
+                if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                    listenForDozeAmountTransition(this)
+                    listenForAnyStateToAodTransition(this)
+                } else {
+                    listenForDozeAmount(this)
+                }
+            }
+        }
     }
 
     fun unregisterListeners() {
+        if (!isRegistered) {
+            return
+        }
+        isRegistered = false
+
+        disposableHandle?.dispose()
         broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
         configurationController.removeCallback(configListener)
         batteryController.removeCallback(batteryCallback)
         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.removeCallback(statusBarStateListener)
         smallRegionSampler?.stopRegionSampler()
         largeRegionSampler?.stopRegionSampler()
     }
 
+    private fun updateFontSizes() {
+        clock?.smallClock?.events?.onFontSettingChanged(
+            resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat())
+        clock?.largeClock?.events?.onFontSettingChanged(
+            resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat())
+    }
+
     /**
      * Dump information for debugging
      */
@@ -235,8 +270,54 @@
         largeRegionSampler?.dump(pw)
     }
 
-    companion object {
-        private val TAG = ClockEventController::class.simpleName
-        private const val FORMAT_NUMBER = 1234567890
+    @VisibleForTesting
+    internal fun listenForDozeAmount(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardInteractor.dozeAmount.collect {
+                dozeAmount = it
+                clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor.dozeAmountTransition.collect {
+                dozeAmount = it.value
+                clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    /**
+     * When keyguard is displayed again after being gone, the clock must be reset to full
+     * dozing.
+     */
+    @VisibleForTesting
+    internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor.anyStateToAodTransition.filter {
+                it.transitionState == TransitionState.FINISHED
+            }.collect {
+                dozeAmount = 1f
+                clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozing(scope: CoroutineScope): Job {
+        return scope.launch {
+            combine (
+                keyguardInteractor.dozeAmount,
+                keyguardInteractor.isDozing,
+            ) { localDozeAmount, localIsDozing ->
+                localDozeAmount > dozeAmount || localIsDozing
+            }
+            .collect { localIsDozing ->
+                isDozing = localIsDozing
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index 6fcb6f5..4a41b3f 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -17,6 +17,7 @@
 package com.android.keyguard
 
 import android.annotation.StringDef
+import android.os.PowerManager
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
@@ -122,122 +123,93 @@
         "Face auth started/stopped because biometric is enabled on keyguard"
 }
 
-/** UiEvents that are logged to identify why face auth is being triggered. */
-enum class FaceAuthUiEvent constructor(private val id: Int, val reason: String) :
+/**
+ * UiEvents that are logged to identify why face auth is being triggered.
+ * @param extraInfo is logged as the position. See [UiEventLogger#logWithInstanceIdAndPosition]
+ */
+enum class FaceAuthUiEvent
+constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) :
     UiEventLogger.UiEventEnum {
     @UiEvent(doc = OCCLUDING_APP_REQUESTED)
     FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED(1146, OCCLUDING_APP_REQUESTED),
-
     @UiEvent(doc = UDFPS_POINTER_DOWN)
     FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN(1147, UDFPS_POINTER_DOWN),
-
     @UiEvent(doc = SWIPE_UP_ON_BOUNCER)
     FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER(1148, SWIPE_UP_ON_BOUNCER),
-
     @UiEvent(doc = DEVICE_WOKEN_UP_ON_REACH_GESTURE)
     FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD(1149, DEVICE_WOKEN_UP_ON_REACH_GESTURE),
-
     @UiEvent(doc = FACE_LOCKOUT_RESET)
     FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET(1150, FACE_LOCKOUT_RESET),
-
-    @UiEvent(doc = QS_EXPANDED)
-    FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
-
+    @UiEvent(doc = QS_EXPANDED) FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
     @UiEvent(doc = NOTIFICATION_PANEL_CLICKED)
     FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED(1152, NOTIFICATION_PANEL_CLICKED),
-
     @UiEvent(doc = PICK_UP_GESTURE_TRIGGERED)
     FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED(1153, PICK_UP_GESTURE_TRIGGERED),
-
     @UiEvent(doc = ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
-    FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154,
-        ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
-
+    FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
     @UiEvent(doc = PRIMARY_BOUNCER_SHOWN)
     FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN(1155, PRIMARY_BOUNCER_SHOWN),
-
     @UiEvent(doc = PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN)
     FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN(
         1197,
         PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN
     ),
-
     @UiEvent(doc = RETRY_AFTER_HW_UNAVAILABLE)
     FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE(1156, RETRY_AFTER_HW_UNAVAILABLE),
-
-    @UiEvent(doc = TRUST_DISABLED)
-    FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
-
-    @UiEvent(doc = TRUST_ENABLED)
-    FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
-
+    @UiEvent(doc = TRUST_DISABLED) FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
+    @UiEvent(doc = TRUST_ENABLED) FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
     @UiEvent(doc = KEYGUARD_OCCLUSION_CHANGED)
     FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED(1159, KEYGUARD_OCCLUSION_CHANGED),
-
     @UiEvent(doc = ASSISTANT_VISIBILITY_CHANGED)
     FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED(1160, ASSISTANT_VISIBILITY_CHANGED),
-
     @UiEvent(doc = STARTED_WAKING_UP)
-    FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP),
-
+    FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP) {
+        override fun extraInfoToString(): String {
+            return PowerManager.wakeReasonToString(extraInfo)
+        }
+    },
+    @Deprecated(
+        "Not a face auth trigger.",
+        ReplaceWith(
+            "FACE_AUTH_UPDATED_STARTED_WAKING_UP, " +
+                "extraInfo=PowerManager.WAKE_REASON_DREAM_FINISHED"
+        )
+    )
     @UiEvent(doc = DREAM_STOPPED)
     FACE_AUTH_TRIGGERED_DREAM_STOPPED(1162, DREAM_STOPPED),
-
     @UiEvent(doc = ALL_AUTHENTICATORS_REGISTERED)
     FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED(1163, ALL_AUTHENTICATORS_REGISTERED),
-
     @UiEvent(doc = ENROLLMENTS_CHANGED)
     FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED(1164, ENROLLMENTS_CHANGED),
-
     @UiEvent(doc = KEYGUARD_VISIBILITY_CHANGED)
     FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED(1165, KEYGUARD_VISIBILITY_CHANGED),
-
     @UiEvent(doc = FACE_CANCEL_NOT_RECEIVED)
     FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED(1174, FACE_CANCEL_NOT_RECEIVED),
-
     @UiEvent(doc = AUTH_REQUEST_DURING_CANCELLATION)
     FACE_AUTH_TRIGGERED_DURING_CANCELLATION(1175, AUTH_REQUEST_DURING_CANCELLATION),
-
-    @UiEvent(doc = DREAM_STARTED)
-    FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
-
-    @UiEvent(doc = FP_LOCKED_OUT)
-    FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
-
+    @UiEvent(doc = DREAM_STARTED) FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
+    @UiEvent(doc = FP_LOCKED_OUT) FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
     @UiEvent(doc = FACE_AUTH_STOPPED_ON_USER_INPUT)
     FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER(1178, FACE_AUTH_STOPPED_ON_USER_INPUT),
-
     @UiEvent(doc = KEYGUARD_GOING_AWAY)
     FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY(1179, KEYGUARD_GOING_AWAY),
-
-    @UiEvent(doc = CAMERA_LAUNCHED)
-    FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
-
-    @UiEvent(doc = FP_AUTHENTICATED)
-    FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
-
-    @UiEvent(doc = GOING_TO_SLEEP)
-    FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
-
+    @UiEvent(doc = CAMERA_LAUNCHED) FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
+    @UiEvent(doc = FP_AUTHENTICATED) FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
+    @UiEvent(doc = GOING_TO_SLEEP) FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
     @UiEvent(doc = FINISHED_GOING_TO_SLEEP)
     FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP(1183, FINISHED_GOING_TO_SLEEP),
-
-    @UiEvent(doc = KEYGUARD_INIT)
-    FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
-
-    @UiEvent(doc = KEYGUARD_RESET)
-    FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
-
-    @UiEvent(doc = USER_SWITCHING)
-    FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
-
+    @UiEvent(doc = KEYGUARD_INIT) FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
+    @UiEvent(doc = KEYGUARD_RESET) FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
+    @UiEvent(doc = USER_SWITCHING) FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
     @UiEvent(doc = FACE_AUTHENTICATED)
     FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED(1187, FACE_AUTHENTICATED),
-
     @UiEvent(doc = BIOMETRIC_ENABLED)
     FACE_AUTH_UPDATED_BIOMETRIC_ENABLED_ON_KEYGUARD(1188, BIOMETRIC_ENABLED);
 
     override fun getId(): Int = this.id
+
+    /** Convert [extraInfo] to a human-readable string. By default, this is empty. */
+    open fun extraInfoToString(): String = ""
 }
 
 private val apiRequestReasonToUiEvent =
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
new file mode 100644
index 0000000..a0c43fb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.res.Resources
+import android.os.Build
+import android.os.PowerManager
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.GlobalSettings
+import java.io.PrintWriter
+import java.util.stream.Collectors
+import javax.inject.Inject
+
+/** Determines which device wake-ups should trigger face authentication. */
+@SysUISingleton
+class FaceWakeUpTriggersConfig
+@Inject
+constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) :
+    Dumpable {
+    private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> =
+        resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet()
+    private val triggerFaceAuthOnWakeUpFrom: Set<Int>
+
+    init {
+        triggerFaceAuthOnWakeUpFrom =
+            if (Build.IS_DEBUGGABLE) {
+                // Update face wake triggers via adb on debuggable builds:
+                // ie: adb shell settings put global face_wake_triggers "1\|4" &&
+                //     adb shell am crash com.android.systemui
+                processStringArray(
+                    globalSettings.getString("face_wake_triggers"),
+                    defaultTriggerFaceAuthOnWakeUpFrom
+                )
+            } else {
+                defaultTriggerFaceAuthOnWakeUpFrom
+            }
+        dumpManager.registerDumpable(this)
+    }
+
+    fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean {
+        return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("FaceWakeUpTriggers:")
+        for (pmWakeReason in triggerFaceAuthOnWakeUpFrom) {
+            pw.println("    ${PowerManager.wakeReasonToString(pmWakeReason)}")
+        }
+    }
+
+    /** Convert a pipe-separated set of integers into a set of ints. */
+    private fun processStringArray(stringSetting: String?, default: Set<Int>): Set<Int> {
+        return stringSetting?.let {
+            stringSetting.split("|").stream().map(Integer::parseInt).collect(Collectors.toSet())
+        }
+            ?: default
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d03ef98..40423cd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -5,6 +5,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -22,6 +23,7 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+
 /**
  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
  */
@@ -46,6 +48,7 @@
      */
     private FrameLayout mSmallClockFrame;
     private FrameLayout mLargeClockFrame;
+    private ClockController mClock;
 
     private View mStatusArea;
     private int mSmartspaceTopOffset;
@@ -95,6 +98,8 @@
     }
 
     void setClock(ClockController clock, int statusBarState) {
+        mClock = clock;
+
         // Disconnect from existing plugin.
         mSmallClockFrame.removeAllViews();
         mLargeClockFrame.removeAllViews();
@@ -108,6 +113,35 @@
         Log.i(TAG, "Attached new clock views to switch");
         mSmallClockFrame.addView(clock.getSmallClock().getView());
         mLargeClockFrame.addView(clock.getLargeClock().getView());
+        updateClockTargetRegions();
+    }
+
+    void updateClockTargetRegions() {
+        if (mClock != null) {
+            if (mSmallClockFrame.isLaidOut()) {
+                int targetHeight =  getResources()
+                        .getDimensionPixelSize(R.dimen.small_clock_text_size);
+                mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect(
+                        mSmallClockFrame.getLeft(),
+                        mSmallClockFrame.getTop(),
+                        mSmallClockFrame.getRight(),
+                        mSmallClockFrame.getTop() + targetHeight));
+            }
+
+            if (mLargeClockFrame.isLaidOut()) {
+                int largeClockTopMargin = getResources()
+                        .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
+                int targetHeight = getResources()
+                        .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2;
+                int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2
+                        + largeClockTopMargin / 2;
+                mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect(
+                        mLargeClockFrame.getLeft(),
+                        top,
+                        mLargeClockFrame.getRight(),
+                        top + targetHeight));
+            }
+        }
     }
 
     private void updateClockViews(boolean useLargeClock, boolean animate) {
@@ -127,7 +161,7 @@
         if (useLargeClock) {
             out = mSmallClockFrame;
             in = mLargeClockFrame;
-            if (indexOfChild(in) == -1) addView(in);
+            if (indexOfChild(in) == -1) addView(in, 0);
             direction = -1;
             statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
                     + mSmartspaceTopOffset;
@@ -214,6 +248,10 @@
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
 
+        if (changed) {
+            post(() -> updateClockTargetRegions());
+        }
+
         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
             post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index b450ec3..e6aae9b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -37,9 +37,8 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.ClockRegistry;
@@ -78,7 +77,7 @@
     @KeyguardClockSwitch.ClockSize
     private int mCurrentClockSize = SMALL;
 
-    private int mKeyguardClockTopMargin = 0;
+    private int mKeyguardSmallClockTopMargin = 0;
     private final ClockRegistry.ClockChangeListener mClockChangedListener;
 
     private ViewGroup mStatusArea;
@@ -119,8 +118,7 @@
             SecureSettings secureSettings,
             @Main Executor uiExecutor,
             DumpManager dumpManager,
-            ClockEventController clockEventController,
-            FeatureFlags featureFlags) {
+            ClockEventController clockEventController) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
@@ -133,7 +131,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
 
-        mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS));
         mClockChangedListener = () -> {
             setClock(mClockRegistry.createCurrentClock());
         };
@@ -164,8 +161,8 @@
     protected void onViewAttached() {
         mClockRegistry.registerClockChangeListener(mClockChangedListener);
         setClock(mClockRegistry.createCurrentClock());
-        mClockEventController.registerListeners();
-        mKeyguardClockTopMargin =
+        mClockEventController.registerListeners(mView);
+        mKeyguardSmallClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
         if (mOnlyClock) {
@@ -247,10 +244,12 @@
      */
     public void onDensityOrFontScaleChanged() {
         mView.onDensityOrFontScaleChanged();
-        mKeyguardClockTopMargin =
+        mKeyguardSmallClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
+        mView.updateClockTargetRegions();
     }
 
+
     /**
      * Set which clock should be displayed on the keyguard. The other one will be automatically
      * hidden.
@@ -330,7 +329,7 @@
             return frameHeight / 2 + clockHeight / 2;
         } else {
             int clockHeight = clock.getSmallClock().getView().getHeight();
-            return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin;
+            return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
         }
     }
 
@@ -404,5 +403,9 @@
             clock.dump(pw);
         }
     }
-}
 
+    /** Gets the animations for the current clock. */
+    public ClockAnimations getClockAnimations() {
+        return getClock().getAnimations();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
index db64f05..2b660de 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources;
 import android.media.AudioManager;
 import android.os.SystemClock;
-import android.service.trust.TrustAgentService;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 import android.util.MathUtils;
@@ -68,30 +67,24 @@
     private final KeyguardUpdateMonitorCallback mUpdateCallback =
             new KeyguardUpdateMonitorCallback() {
                 @Override
-                public void onTrustGrantedWithFlags(int flags, int userId) {
-                    if (userId != KeyguardUpdateMonitor.getCurrentUser()) return;
-                    boolean bouncerVisible = mView.isVisibleToUser();
-                    boolean temporaryAndRenewable =
-                            (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)
-                            != 0;
-                    boolean initiatedByUser =
-                            (flags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0;
-                    boolean dismissKeyguard =
-                            (flags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0;
-
-                    if (initiatedByUser || dismissKeyguard) {
-                        if ((mViewMediatorCallback.isScreenOn() || temporaryAndRenewable)
-                                && (bouncerVisible || dismissKeyguard)) {
-                            if (!bouncerVisible) {
-                                // The trust agent dismissed the keyguard without the user proving
-                                // that they are present (by swiping up to show the bouncer). That's
-                                // fine if the user proved presence via some other way to the trust
-                                //agent.
-                                Log.i(TAG, "TrustAgent dismissed Keyguard.");
-                            }
-                            mSecurityCallback.dismiss(false /* authenticated */, userId,
-                                    /* bypassSecondaryLockScreen */ false, SecurityMode.Invalid);
-                        } else {
+                public void onTrustGrantedForCurrentUser(boolean dismissKeyguard,
+                        TrustGrantFlags flags, String message) {
+                    if (dismissKeyguard) {
+                        if (!mView.isVisibleToUser()) {
+                            // The trust agent dismissed the keyguard without the user proving
+                            // that they are present (by swiping up to show the bouncer). That's
+                            // fine if the user proved presence via some other way to the trust
+                            // agent.
+                            Log.i(TAG, "TrustAgent dismissed Keyguard.");
+                        }
+                        mSecurityCallback.dismiss(
+                                false /* authenticated */,
+                                KeyguardUpdateMonitor.getCurrentUser(),
+                                /* bypassSecondaryLockScreen */ false,
+                                SecurityMode.Invalid
+                        );
+                    } else {
+                        if (flags.isInitiatedByUser() || flags.dismissKeyguardRequested()) {
                             mViewMediatorCallback.playTrustedSound();
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index f26b905..faaba63 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -21,6 +21,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.inputmethod.InputMethodManager;
 
@@ -152,6 +153,9 @@
     }
 
     public void startAppearAnimation() {
+        if (TextUtils.isEmpty(mMessageAreaController.getMessage())) {
+            mMessageAreaController.setMessage(getInitialMessageResId());
+        }
         mView.startAppearAnimation();
     }
 
@@ -169,6 +173,11 @@
         return view.indexOfChild(mView);
     }
 
+    /** Determines the message to show in the bouncer when it first appears. */
+    protected int getInitialMessageResId() {
+        return 0;
+    }
+
     /** Factory for a {@link KeyguardInputViewController}. */
     public static class Factory {
         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index 71470e8..8197685 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -35,6 +35,7 @@
     val keyguardOccluded: Boolean,
     val occludingAppRequestingFp: Boolean,
     val primaryUser: Boolean,
+    val shouldListenSfpsState: Boolean,
     val shouldListenForFingerprintAssistant: Boolean,
     val switchingUser: Boolean,
     val udfps: Boolean,
@@ -49,10 +50,9 @@
     override val listening: Boolean,
     // keep sorted
     val authInterruptActive: Boolean,
-    val becauseCannotSkipBouncer: Boolean,
     val biometricSettingEnabledForUser: Boolean,
     val bouncerFullyShown: Boolean,
-    val faceAuthenticated: Boolean,
+    val faceAndFpNotAuthenticated: Boolean,
     val faceDisabled: Boolean,
     val faceLockedOut: Boolean,
     val fpLockedOut: Boolean,
@@ -66,7 +66,9 @@
     val secureCameraLaunched: Boolean,
     val switchingUser: Boolean,
     val udfpsBouncerShowing: Boolean,
-) : KeyguardListenModel()
+    val udfpsFingerDown: Boolean,
+    val userNotTrustedOrDetectionIsNeeded: Boolean,
+    ) : KeyguardListenModel()
 /**
  * Verbose debug information associated with [KeyguardUpdateMonitor.shouldTriggerActiveUnlock].
  */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index c2802f7..db986e0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -18,7 +18,6 @@
 
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
-import android.text.TextUtils;
 
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -100,19 +99,15 @@
         mView.setMessage(resId);
     }
 
-    /**
-     * Set Text if KeyguardMessageArea is empty.
-     */
-    public void setMessageIfEmpty(int resId) {
-        if (TextUtils.isEmpty(mView.getText())) {
-            setMessage(resId);
-        }
-    }
-
     public void setNextMessageColor(ColorStateList colorState) {
         mView.setNextMessageColor(colorState);
     }
 
+    /** Returns the message of the underlying TextView. */
+    public CharSequence getMessage() {
+        return mView.getText();
+    }
+
     /**
      * Reload colors from resources.
      **/
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 29e912f..0025986 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -187,7 +187,7 @@
     @Override
     void resetState() {
         mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser()));
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
         final boolean wasDisabled = mPasswordEntry.isEnabled();
         mView.setPasswordEntryEnabled(true);
         mView.setPasswordEntryInputEnabled(true);
@@ -207,7 +207,6 @@
         if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
             showInput();
         }
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password);
     }
 
     private void showInput() {
@@ -324,4 +323,9 @@
                 //enabled input method subtype (The current IME should be LatinIME.)
                 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_password;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 9871645..1f0bd54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -298,12 +298,6 @@
     }
 
     @Override
-    public void onResume(int reason) {
-        super.onResume(reason);
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern);
-    }
-
-    @Override
     public boolean needsInput() {
         return false;
     }
@@ -361,7 +355,7 @@
     }
 
     private void displayDefaultSecurityMessage() {
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
     }
 
     private void handleAttemptLockout(long elapsedRealtimeDeadline) {
@@ -392,4 +386,9 @@
 
         }.start();
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pattern;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 59a018a..f7423ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -127,7 +127,6 @@
     public void onResume(int reason) {
         super.onResume(reason);
         mPasswordEntry.requestFocus();
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 89fcc47..7876f07 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -76,20 +76,13 @@
     }
 
     @Override
-    void resetState() {
-        super.resetState();
-        mMessageAreaController.setMessage("");
-    }
-
-    @Override
-    public void startAppearAnimation() {
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        super.startAppearAnimation();
-    }
-
-    @Override
     public boolean startDisappearAnimation(Runnable finishRunnable) {
         return mView.startDisappearAnimation(
                 mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable);
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pin;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 93ee151..5c4126e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -89,6 +89,7 @@
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
+import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
@@ -105,8 +106,9 @@
     static final int USER_TYPE_WORK_PROFILE = 2;
     static final int USER_TYPE_SECONDARY_USER = 3;
 
-    @IntDef({MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
+    @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
     public @interface Mode {}
+    static final int MODE_UNINITIALIZED = -1;
     static final int MODE_DEFAULT = 0;
     static final int MODE_ONE_HANDED = 1;
     static final int MODE_USER_SWITCHER = 2;
@@ -136,6 +138,7 @@
     private GlobalSettings mGlobalSettings;
     private FalsingManager mFalsingManager;
     private UserSwitcherController mUserSwitcherController;
+    private FalsingA11yDelegate mFalsingA11yDelegate;
     private AlertDialog mAlertDialog;
     private boolean mSwipeUpToRetry;
 
@@ -152,7 +155,11 @@
     private boolean mDisappearAnimRunning;
     private SwipeListener mSwipeListener;
     private ViewMode mViewMode = new DefaultViewMode();
-    private @Mode int mCurrentMode = MODE_DEFAULT;
+    /*
+     * Using MODE_UNINITIALIZED to mean the view mode is set to DefaultViewMode, but init() has not
+     * yet been called on it. This will happen when the ViewController is initialized.
+     */
+    private @Mode int mCurrentMode = MODE_UNINITIALIZED;
     private int mWidth = -1;
 
     private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
@@ -318,7 +325,8 @@
 
     void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager,
             UserSwitcherController userSwitcherController,
-            UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback) {
+            UserSwitcherViewMode.UserSwitcherCallback userSwitcherCallback,
+            FalsingA11yDelegate falsingA11yDelegate) {
         if (mCurrentMode == mode) return;
         Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to "
                 + modeToString(mode));
@@ -337,12 +345,15 @@
         }
         mGlobalSettings = globalSettings;
         mFalsingManager = falsingManager;
+        mFalsingA11yDelegate = falsingA11yDelegate;
         mUserSwitcherController = userSwitcherController;
         setupViewMode();
     }
 
     private String modeToString(@Mode int mode) {
         switch (mode) {
+            case MODE_UNINITIALIZED:
+                return "Uninitialized";
             case MODE_DEFAULT:
                 return "Default";
             case MODE_ONE_HANDED:
@@ -361,7 +372,7 @@
         }
 
         mViewMode.init(this, mGlobalSettings, mSecurityViewFlipper, mFalsingManager,
-                mUserSwitcherController);
+                mUserSwitcherController, mFalsingA11yDelegate);
     }
 
     @Mode int getMode() {
@@ -716,6 +727,11 @@
         mViewMode.reloadColors();
     }
 
+    /** Handles density or font scale changes. */
+    void onDensityOrFontScaleChanged() {
+        mViewMode.onDensityOrFontScaleChanged();
+    }
+
     /**
      * Enscapsulates the differences between bouncer modes for the container.
      */
@@ -723,7 +739,8 @@
         default void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
-                @NonNull UserSwitcherController userSwitcherController) {};
+                @NonNull UserSwitcherController userSwitcherController,
+                @NonNull FalsingA11yDelegate falsingA11yDelegate) {};
 
         /** Reinitialize the location */
         default void updateSecurityViewLocation() {};
@@ -740,6 +757,9 @@
         /** Refresh colors */
         default void reloadColors() {};
 
+        /** Handles density or font scale changes. */
+        default void onDensityOrFontScaleChanged() {}
+
         /** On a successful auth, optionally handle how the view disappears */
         default void startDisappearAnimation(SecurityMode securityMode) {};
 
@@ -828,7 +848,8 @@
         public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
-                @NonNull UserSwitcherController userSwitcherController) {
+                @NonNull UserSwitcherController userSwitcherController,
+                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
             mView = v;
             mViewFlipper = viewFlipper;
 
@@ -865,6 +886,7 @@
                 this::setupUserSwitcher;
 
         private UserSwitcherCallback mUserSwitcherCallback;
+        private FalsingA11yDelegate mFalsingA11yDelegate;
 
         UserSwitcherViewMode(UserSwitcherCallback userSwitcherCallback) {
             mUserSwitcherCallback = userSwitcherCallback;
@@ -874,23 +896,20 @@
         public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
-                @NonNull UserSwitcherController userSwitcherController) {
+                @NonNull UserSwitcherController userSwitcherController,
+                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
             init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false);
             mView = v;
             mViewFlipper = viewFlipper;
             mFalsingManager = falsingManager;
             mUserSwitcherController = userSwitcherController;
             mResources = v.getContext().getResources();
+            mFalsingA11yDelegate = falsingA11yDelegate;
 
             if (mUserSwitcherViewGroup == null) {
-                LayoutInflater.from(v.getContext()).inflate(
-                        R.layout.keyguard_bouncer_user_switcher,
-                        mView,
-                        true);
-                mUserSwitcherViewGroup =  mView.findViewById(R.id.keyguard_bouncer_user_switcher);
+                inflateUserSwitcher();
             }
             updateSecurityViewLocation();
-            mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
             setupUserSwitcher();
             mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
         }
@@ -921,6 +940,12 @@
         }
 
         @Override
+        public void onDensityOrFontScaleChanged() {
+            mView.removeView(mUserSwitcherViewGroup);
+            inflateUserSwitcher();
+        }
+
+        @Override
         public void onDestroy() {
             mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
         }
@@ -978,6 +1003,7 @@
             mUserSwitcher.setText(currentUserName);
 
             KeyguardUserSwitcherAnchor anchor = mView.findViewById(R.id.user_switcher_anchor);
+            anchor.setAccessibilityDelegate(mFalsingA11yDelegate);
 
             BaseUserSwitcherAdapter adapter = new BaseUserSwitcherAdapter(mUserSwitcherController) {
                 @Override
@@ -1048,7 +1074,7 @@
 
             anchor.setOnClickListener((v) -> {
                 if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
-                mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager);
+                mPopup = new KeyguardUserSwitcherPopupMenu(mView.getContext(), mFalsingManager);
                 mPopup.setAnchorView(anchor);
                 mPopup.setAdapter(adapter);
                 mPopup.setOnItemClickListener((parent, view, pos, id) -> {
@@ -1080,11 +1106,19 @@
                         new KeyguardSecurityViewTransition());
             }
             int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            int viewFlipperBottomMargin = mResources.getDimensionPixelSize(
+                    R.dimen.bouncer_user_switcher_view_mode_view_flipper_bottom_margin);
+            int userSwitcherBottomMargin = mResources.getDimensionPixelSize(
+                    R.dimen.bouncer_user_switcher_view_mode_user_switcher_bottom_margin);
             if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                 ConstraintSet constraintSet = new ConstraintSet();
                 constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP, yTrans);
-                constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP);
-                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM);
+                constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, mViewFlipper.getId(),
+                        TOP, userSwitcherBottomMargin);
+                constraintSet.connect(mViewFlipper.getId(), TOP, mUserSwitcherViewGroup.getId(),
+                        BOTTOM);
+                constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM,
+                        viewFlipperBottomMargin);
                 constraintSet.centerHorizontally(mViewFlipper.getId(), PARENT_ID);
                 constraintSet.centerHorizontally(mUserSwitcherViewGroup.getId(), PARENT_ID);
                 constraintSet.setVerticalChainStyle(mViewFlipper.getId(), CHAIN_SPREAD);
@@ -1120,6 +1154,15 @@
             }
         }
 
+        private void inflateUserSwitcher() {
+            LayoutInflater.from(mView.getContext()).inflate(
+                    R.layout.keyguard_bouncer_user_switcher,
+                    mView,
+                    true);
+            mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher);
+            mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
+        }
+
         interface UserSwitcherCallback {
             void showUnlockToContinueMessage();
         }
@@ -1137,7 +1180,8 @@
         public void init(@NonNull ConstraintLayout v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
-                @NonNull UserSwitcherController userSwitcherController) {
+                @NonNull UserSwitcherController userSwitcherController,
+                @NonNull FalsingA11yDelegate falsingA11yDelegate) {
             init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true);
             mView = v;
             mViewFlipper = viewFlipper;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index bcd1a1e..01be33e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -58,7 +58,9 @@
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.R;
-import com.android.systemui.biometrics.SidefpsController;
+import com.android.systemui.biometrics.SideFpsController;
+import com.android.systemui.biometrics.SideFpsUiRequestSource;
+import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -99,7 +101,8 @@
     private final GlobalSettings mGlobalSettings;
     private final FeatureFlags mFeatureFlags;
     private final SessionTracker mSessionTracker;
-    private final Optional<SidefpsController> mSidefpsController;
+    private final Optional<SideFpsController> mSideFpsController;
+    private final FalsingA11yDelegate mFalsingA11yDelegate;
 
     private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
 
@@ -219,13 +222,16 @@
     };
 
 
-    private SwipeListener mSwipeListener = new SwipeListener() {
+    private final SwipeListener mSwipeListener = new SwipeListener() {
         @Override
         public void onSwipeUp() {
             if (!mUpdateMonitor.isFaceDetectionRunning()) {
-                mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+                boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(
+                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
                 mKeyguardSecurityCallback.userActivity();
-                showMessage(null, null);
+                if (didFaceAuthRun) {
+                    showMessage(null, null);
+                }
             }
             if (mUpdateMonitor.isFaceEnrolled()) {
                 mUpdateMonitor.requestActiveUnlock(
@@ -234,7 +240,7 @@
             }
         }
     };
-    private ConfigurationController.ConfigurationListener mConfigurationListener =
+    private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
                 @Override
                 public void onThemeChanged() {
@@ -245,6 +251,11 @@
                 public void onUiModeChanged() {
                     reloadColors();
                 }
+
+                @Override
+                public void onDensityOrFontScaleChanged() {
+                    KeyguardSecurityContainerController.this.onDensityOrFontScaleChanged();
+                }
             };
     private boolean mBouncerVisible = false;
     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
@@ -285,7 +296,8 @@
             FeatureFlags featureFlags,
             GlobalSettings globalSettings,
             SessionTracker sessionTracker,
-            Optional<SidefpsController> sidefpsController) {
+            Optional<SideFpsController> sideFpsController,
+            FalsingA11yDelegate falsingA11yDelegate) {
         super(view);
         mLockPatternUtils = lockPatternUtils;
         mUpdateMonitor = keyguardUpdateMonitor;
@@ -305,12 +317,14 @@
         mFeatureFlags = featureFlags;
         mGlobalSettings = globalSettings;
         mSessionTracker = sessionTracker;
-        mSidefpsController = sidefpsController;
+        mSideFpsController = sideFpsController;
+        mFalsingA11yDelegate = falsingA11yDelegate;
     }
 
     @Override
     public void onInit() {
         mSecurityViewFlipperController.init();
+        configureMode();
     }
 
     @Override
@@ -343,16 +357,27 @@
     }
 
     private void updateSideFpsVisibility() {
-        if (!mSidefpsController.isPresent()) {
+        if (!mSideFpsController.isPresent()) {
             return;
         }
-        if (mBouncerVisible
-                && getResources().getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
-                && mUpdateMonitor.isFingerprintDetectionRunning()
-                && !mUpdateMonitor.userNeedsStrongAuth()) {
-            mSidefpsController.get().show();
+        final boolean sfpsEnabled = getResources().getBoolean(
+                R.bool.config_show_sidefps_hint_on_bouncer);
+        final boolean fpsDetectionRunning = mUpdateMonitor.isFingerprintDetectionRunning();
+        final boolean needsStrongAuth = mUpdateMonitor.userNeedsStrongAuth();
+
+        boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning && !needsStrongAuth;
+
+        if (DEBUG) {
+            Log.d(TAG, "sideFpsToShow=" + toShow + ", "
+                    + "mBouncerVisible=" + mBouncerVisible + ", "
+                    + "configEnabled=" + sfpsEnabled + ", "
+                    + "fpsDetectionRunning=" + fpsDetectionRunning + ", "
+                    + "needsStrongAuth=" + needsStrongAuth);
+        }
+        if (toShow) {
+            mSideFpsController.get().show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
         } else {
-            mSidefpsController.get().hide();
+            mSideFpsController.get().hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
         }
     }
 
@@ -622,7 +647,7 @@
 
         mView.initMode(mode, mGlobalSettings, mFalsingManager, mUserSwitcherController,
                 () -> showMessage(getContext().getString(R.string.keyguard_unlock_to_continue),
-                        null));
+                        null), mFalsingA11yDelegate);
     }
 
     public void reportFailedUnlockAttempt(int userId, int timeoutMs) {
@@ -707,6 +732,14 @@
         mView.reloadColors();
     }
 
+    /** Handles density or font scale changes. */
+    private void onDensityOrFontScaleChanged() {
+        mSecurityViewFlipperController.onDensityOrFontScaleChanged();
+        mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
+                mKeyguardSecurityCallback);
+        mView.onDensityOrFontScaleChanged();
+    }
+
     static class Factory {
 
         private final KeyguardSecurityContainer mView;
@@ -726,7 +759,8 @@
         private final FeatureFlags mFeatureFlags;
         private final UserSwitcherController mUserSwitcherController;
         private final SessionTracker mSessionTracker;
-        private final Optional<SidefpsController> mSidefpsController;
+        private final Optional<SideFpsController> mSidefpsController;
+        private final FalsingA11yDelegate mFalsingA11yDelegate;
 
         @Inject
         Factory(KeyguardSecurityContainer view,
@@ -746,7 +780,8 @@
                 FeatureFlags featureFlags,
                 GlobalSettings globalSettings,
                 SessionTracker sessionTracker,
-                Optional<SidefpsController> sidefpsController) {
+                Optional<SideFpsController> sidefpsController,
+                FalsingA11yDelegate falsingA11yDelegate) {
             mView = view;
             mAdminSecondaryLockScreenControllerFactory = adminSecondaryLockScreenControllerFactory;
             mLockPatternUtils = lockPatternUtils;
@@ -764,6 +799,7 @@
             mUserSwitcherController = userSwitcherController;
             mSessionTracker = sessionTracker;
             mSidefpsController = sidefpsController;
+            mFalsingA11yDelegate = falsingA11yDelegate;
         }
 
         public KeyguardSecurityContainerController create(
@@ -774,7 +810,7 @@
                     mKeyguardStateController, securityCallback, mSecurityViewFlipperController,
                     mConfigurationController, mFalsingCollector, mFalsingManager,
                     mUserSwitcherController, mFeatureFlags, mGlobalSettings, mSessionTracker,
-                    mSidefpsController);
+                    mSidefpsController, mFalsingA11yDelegate);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index bddf4b0..25afe11 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -83,6 +83,13 @@
         }
     }
 
+    /** Handles density or font scale changes. */
+    public void onDensityOrFontScaleChanged() {
+        mView.removeAllViews();
+        mChildren.clear();
+    }
+
+
     @VisibleForTesting
     KeyguardInputViewController<KeyguardInputView> getSecurityView(SecurityMode securityMode,
             KeyguardSecurityCallback keyguardSecurityCallback) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 83e23bd..8b9823b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -17,6 +17,7 @@
 package com.android.keyguard;
 
 import android.content.Context;
+import android.os.Trace;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
@@ -112,4 +113,11 @@
             mKeyguardSlice.dump(pw, args);
         }
     }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Trace.beginSection("KeyguardStatusView#onMeasure");
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        Trace.endSection();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index e9f06ed..7849747 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,7 @@
 import android.util.Slog;
 
 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
@@ -232,4 +233,9 @@
             mView.setClipBounds(null);
         }
     }
+
+    /** Gets the animations for the current clock. */
+    public ClockAnimations getClockAnimations() {
+        return mKeyguardClockSwitchController.getClockAnimations();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f558276..ce22a81 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -44,7 +44,6 @@
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN;
-import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DREAM_STOPPED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DURING_CANCELLATION;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET;
@@ -106,7 +105,6 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -146,12 +144,14 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.Assert;
+import com.android.systemui.util.settings.SecureSettings;
 
 import com.google.android.collect.Lists;
 
@@ -264,6 +264,7 @@
             "com.android.settings", "com.android.settings.FallbackHome");
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final KeyguardUpdateMonitorLogger mLogger;
     private final boolean mIsPrimaryUser;
     private final AuthController mAuthController;
@@ -288,6 +289,7 @@
             }
         }
     };
+    private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
 
     HashMap<Integer, SimData> mSimDatas = new HashMap<>();
     HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
@@ -298,8 +300,8 @@
     private boolean mCredentialAttempted;
     private boolean mKeyguardGoingAway;
     private boolean mGoingToSleep;
-    private boolean mBouncerFullyShown;
-    private boolean mBouncerIsOrWillBeShowing;
+    private boolean mPrimaryBouncerFullyShown;
+    private boolean mPrimaryBouncerIsOrWillBeShowing;
     private boolean mUdfpsBouncerShowing;
     private boolean mAuthInterruptActive;
     private boolean mNeedsSlowUnlockTransition;
@@ -322,17 +324,20 @@
     private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
             mCallbacks = Lists.newArrayList();
     private ContentObserver mDeviceProvisionedObserver;
+    private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver;
     private final ContentObserver mTimeFormatChangeObserver;
 
     private boolean mSwitchingUser;
 
     private boolean mDeviceInteractive;
+    private boolean mSfpsRequireScreenOnToAuthPrefEnabled;
     private final SubscriptionManager mSubscriptionManager;
     private final TelephonyListenerManager mTelephonyListenerManager;
     private final TrustManager mTrustManager;
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final SecureSettings mSecureSettings;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final LatencyTracker mLatencyTracker;
     private final StatusBarStateController mStatusBarStateController;
@@ -381,6 +386,7 @@
     protected Handler getHandler() {
         return mHandler;
     }
+
     private final Handler mHandler;
 
     private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
@@ -466,21 +472,12 @@
                     FACE_AUTH_TRIGGERED_TRUST_DISABLED);
         }
 
-        mLogger.logTrustChanged(wasTrusted, enabled, userId);
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-            if (cb != null) {
-                cb.onTrustChanged(userId);
-                if (enabled && flags != 0) {
-                    cb.onTrustGrantedWithFlags(flags, userId);
-                }
-            }
-        }
-
-        if (KeyguardUpdateMonitor.getCurrentUser() == userId) {
-            CharSequence message = null;
-            final boolean userHasTrust = getUserHasTrust(userId);
-            if (userHasTrust && trustGrantedMessages != null) {
+        if (enabled) {
+            String message = null;
+            if (KeyguardUpdateMonitor.getCurrentUser() == userId
+                    && trustGrantedMessages != null) {
+                // Show the first non-empty string provided by a trust agent OR intentionally pass
+                // an empty string through (to prevent the default trust agent string from showing)
                 for (String msg : trustGrantedMessages) {
                     message = msg;
                     if (!TextUtils.isEmpty(message)) {
@@ -489,17 +486,38 @@
                 }
             }
 
-            if (message != null) {
-                mLogger.logShowTrustGrantedMessage(message.toString());
-            }
-            for (int i = 0; i < mCallbacks.size(); i++) {
-                KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-                if (cb != null) {
-                    cb.showTrustGrantedMessage(message);
+            mLogger.logTrustGrantedWithFlags(flags, userId, message);
+            if (userId == getCurrentUser()) {
+                final TrustGrantFlags trustGrantFlags = new TrustGrantFlags(flags);
+                for (int i = 0; i < mCallbacks.size(); i++) {
+                    KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+                    if (cb != null) {
+                        cb.onTrustGrantedForCurrentUser(
+                                shouldDismissKeyguardOnTrustGrantedWithCurrentUser(trustGrantFlags),
+                                trustGrantFlags, message);
+                    }
                 }
             }
         }
 
+        mLogger.logTrustChanged(wasTrusted, enabled, userId);
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onTrustChanged(userId);
+            }
+        }
+    }
+
+    /**
+     * Whether the trust granted call with its passed flags should dismiss keyguard.
+     * It's assumed that the trust was granted for the current user.
+     */
+    private boolean shouldDismissKeyguardOnTrustGrantedWithCurrentUser(TrustGrantFlags flags) {
+        final boolean isBouncerShowing = mPrimaryBouncerIsOrWillBeShowing || mUdfpsBouncerShowing;
+        return (flags.isInitiatedByUser() || flags.dismissKeyguardRequested())
+                && (mDeviceInteractive || flags.temporaryAndRenewable())
+                && (isBouncerShowing || flags.dismissKeyguardRequested());
     }
 
     @Override
@@ -705,6 +723,7 @@
 
     /**
      * Request to listen for face authentication when an app is occluding keyguard.
+     *
      * @param request if true and mKeyguardOccluded, request face auth listening, else default
      *                to normal behavior.
      *                See {@link KeyguardUpdateMonitor#shouldListenForFace()}
@@ -717,6 +736,7 @@
 
     /**
      * Request to listen for fingerprint when an app is occluding keyguard.
+     *
      * @param request if true and mKeyguardOccluded, request fingerprint listening, else default
      *                to normal behavior.
      *                See {@link KeyguardUpdateMonitor#shouldListenForFingerprint(boolean)}
@@ -774,9 +794,9 @@
         }
         // Don't send cancel if authentication succeeds
         mFingerprintCancelSignal = null;
+        mLogger.logFingerprintSuccess(userId, isStrongBiometric);
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
                 FACE_AUTH_UPDATED_FP_AUTHENTICATED);
-        mLogger.d("onFingerprintAuthenticated");
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -850,13 +870,7 @@
             mHandler.removeCallbacks(mFpCancelNotReceived);
         }
         try {
-            final int userId;
-            try {
-                userId = ActivityManager.getService().getCurrentUser().id;
-            } catch (RemoteException e) {
-                mLogger.logException(e, "Failed to get current user id");
-                return;
-            }
+            final int userId = mUserTracker.getUserId();
             if (userId != authUserId) {
                 mLogger.logFingerprintAuthForWrongUser(authUserId);
                 return;
@@ -1074,13 +1088,7 @@
                 mLogger.d("Aborted successful auth because device is going to sleep.");
                 return;
             }
-            final int userId;
-            try {
-                userId = ActivityManager.getService().getCurrentUser().id;
-            } catch (RemoteException e) {
-                mLogger.logException(e, "Failed to get current user id");
-                return;
-            }
+            final int userId = mUserTracker.getUserId();
             if (userId != authUserId) {
                 mLogger.logFaceAuthForWrongUser(authUserId);
                 return;
@@ -1616,7 +1624,7 @@
                 @Override
                 public void onUdfpsPointerDown(int sensorId) {
                     mLogger.logUdfpsPointerDown(sensorId);
-                    requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+                    requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
                 }
 
                 /**
@@ -1642,8 +1650,9 @@
                 public void onAuthenticationFailed() {
                         String reason =
                                 mKeyguardBypassController.canBypass() ? "bypass"
-                                        : mUdfpsBouncerShowing ? "udfpsBouncer" :
-                                                mBouncerFullyShown ? "bouncer" : "udfpsFpDown";
+                                        : mUdfpsBouncerShowing ? "udfpsBouncer"
+                                                : mPrimaryBouncerFullyShown ? "bouncer"
+                                                        : "udfpsFpDown";
                         requestActiveUnlock(
                                 ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
                                 "faceFailure-" + reason);
@@ -1808,11 +1817,21 @@
         }
     }
 
-    protected void handleStartedWakingUp() {
+    protected void handleStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
         Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp");
         Assert.isMainThread();
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_STARTED_WAKING_UP);
-        requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp");
+
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+        if (mFaceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason)) {
+            FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
+            updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+                    FACE_AUTH_UPDATED_STARTED_WAKING_UP);
+            requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
+                    + PowerManager.wakeReasonToString(pmWakeReason));
+        } else {
+            mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
+        }
+
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -1864,12 +1883,9 @@
                 cb.onDreamingStateChanged(mIsDreaming);
             }
         }
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         if (mIsDreaming) {
-            updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
             updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_DREAM_STARTED);
-        } else {
-            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FACE_AUTH_TRIGGERED_DREAM_STOPPED);
         }
     }
 
@@ -1922,8 +1938,10 @@
     @Inject
     protected KeyguardUpdateMonitor(
             Context context,
+            UserTracker userTracker,
             @Main Looper mainLooper,
             BroadcastDispatcher broadcastDispatcher,
+            SecureSettings secureSettings,
             DumpManager dumpManager,
             @Background Executor backgroundExecutor,
             @Main Executor mainExecutor,
@@ -1949,9 +1967,11 @@
             PackageManager packageManager,
             @Nullable FaceManager faceManager,
             @Nullable FingerprintManager fingerprintManager,
-            @Nullable BiometricManager biometricManager) {
+            @Nullable BiometricManager biometricManager,
+            FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) {
         mContext = context;
         mSubscriptionManager = subscriptionManager;
+        mUserTracker = userTracker;
         mTelephonyListenerManager = telephonyListenerManager;
         mDeviceProvisioned = isDeviceProvisionedInSettingsDb();
         mStrongAuthTracker = new StrongAuthTracker(context, this::notifyStrongAuthStateChanged,
@@ -1965,6 +1985,7 @@
         mStatusBarState = mStatusBarStateController.getState();
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
+        mSecureSettings = secureSettings;
         dumpManager.registerDumpable(getClass().getName(), this);
         mSensorPrivacyManager = sensorPrivacyManager;
         mActiveUnlockConfig = activeUnlockConfiguration;
@@ -1988,6 +2009,7 @@
                         R.array.config_face_acquire_device_entry_ignorelist))
                 .boxed()
                 .collect(Collectors.toSet());
+        mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -2024,7 +2046,7 @@
                         handleKeyguardReset();
                         break;
                     case MSG_KEYGUARD_BOUNCER_CHANGED:
-                        handleKeyguardBouncerChanged(msg.arg1, msg.arg2);
+                        handlePrimaryBouncerChanged(msg.arg1, msg.arg2);
                         break;
                     case MSG_REPORT_EMERGENCY_CALL_ACTION:
                         handleReportEmergencyCallAction();
@@ -2037,7 +2059,7 @@
                         break;
                     case MSG_STARTED_WAKING_UP:
                         Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP");
-                        handleStartedWakingUp();
+                        handleStartedWakingUp(msg.arg1);
                         Trace.endSection();
                         break;
                     case MSG_SIM_SUBSCRIPTION_INFO_CHANGED:
@@ -2182,7 +2204,7 @@
 
         TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
         mIsPrimaryUser = mUserManager.isPrimaryUser();
-        int user = ActivityManager.getCurrentUser();
+        int user = mUserTracker.getUserId();
         mUserIsUnlocked.put(user, mUserManager.isUserUnlocked(user));
         mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled();
         updateSecondaryLockscreenRequirement(user);
@@ -2206,9 +2228,35 @@
                                 Settings.System.TIME_12_24)));
             }
         };
+
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.TIME_12_24),
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
+
+        updateSfpsRequireScreenOnToAuthPref();
+        mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateSfpsRequireScreenOnToAuthPref();
+            }
+        };
+
+        mContext.getContentResolver().registerContentObserver(
+                mSecureSettings.getUriFor(
+                        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED),
+                false,
+                mSfpsRequireScreenOnToAuthPrefObserver,
+                getCurrentUser());
+    }
+
+    protected void updateSfpsRequireScreenOnToAuthPref() {
+        final int defaultSfpsRequireScreenOnToAuthValue =
+                mContext.getResources().getBoolean(
+                        com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0;
+        mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser(
+                Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
+                defaultSfpsRequireScreenOnToAuthValue,
+                getCurrentUser()) != 0;
     }
 
     private void initializeSimState() {
@@ -2228,8 +2276,8 @@
     private void updateFaceEnrolled(int userId) {
         mIsFaceEnrolled = whitelistIpcs(
                 () -> mFaceManager != null && mFaceManager.isHardwareDetected()
-                        && mFaceManager.hasEnrolledTemplates(userId)
-                        && mBiometricEnabledForUser.get(userId));
+                        && mBiometricEnabledForUser.get(userId))
+                && mAuthController.isFaceAuthEnrolled(userId);
     }
 
     public boolean isFaceSupported() {
@@ -2253,6 +2301,22 @@
     }
 
     /**
+     * @return true if there's at least one sfps enrollment for the current user.
+     */
+    public boolean isSfpsEnrolled() {
+        return mAuthController.isSfpsEnrolled(getCurrentUser());
+    }
+
+    /**
+     * @return true if sfps HW is supported on this device. Can return true even if the user has
+     * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered.
+     */
+    public boolean isSfpsSupported() {
+        return mAuthController.getSfpsProps() != null
+                && !mAuthController.getSfpsProps().isEmpty();
+    }
+
+    /**
      * @return true if there's at least one face enrolled
      */
     public boolean isFaceEnrolled() {
@@ -2349,14 +2413,14 @@
     /**
      * 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 void 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();
     }
 
     /**
@@ -2366,10 +2430,6 @@
         stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER);
     }
 
-    public boolean isFaceScanning() {
-        return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
-    }
-
     private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) {
         // If this message exists, we should not authenticate again until this message is
         // consumed by the handler
@@ -2417,7 +2477,7 @@
      * Attempts to trigger active unlock from trust agent.
      */
     private void requestActiveUnlock(
-            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
             String reason,
             boolean dismissKeyguard
     ) {
@@ -2447,7 +2507,7 @@
      * Only dismisses the keyguard under certain conditions.
      */
     public void requestActiveUnlock(
-            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
             String extraReason
     ) {
         final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
@@ -2456,7 +2516,7 @@
                 requestOrigin,
                 extraReason, canFaceBypass
                         || mUdfpsBouncerShowing
-                        || mBouncerFullyShown
+                        || mPrimaryBouncerFullyShown
                         || mAuthController.isUdfpsFingerDown());
     }
 
@@ -2477,7 +2537,7 @@
     private boolean shouldTriggerActiveUnlock() {
         // Triggers:
         final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
-        final boolean awakeKeyguard = mBouncerFullyShown || mUdfpsBouncerShowing
+        final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mUdfpsBouncerShowing
                 || (isKeyguardVisible() && !mGoingToSleep
                 && mStatusBarState != StatusBarState.SHADE_LOCKED);
 
@@ -2556,7 +2616,7 @@
         final boolean shouldListenKeyguardState =
                 isKeyguardVisible()
                         || !mDeviceInteractive
-                        || (mBouncerIsOrWillBeShowing && !mKeyguardGoingAway)
+                        || (mPrimaryBouncerIsOrWillBeShowing && !mKeyguardGoingAway)
                         || mGoingToSleep
                         || shouldListenForFingerprintAssistant
                         || (mKeyguardOccluded && mIsDreaming)
@@ -2575,17 +2635,25 @@
                         && mIsPrimaryUser
                         && biometricEnabledForUser;
 
-        final boolean shouldListenBouncerState =
-                !(mFingerprintLockedOut && mBouncerIsOrWillBeShowing && mCredentialAttempted);
+        final boolean shouldListenBouncerState = !(mFingerprintLockedOut
+                && mPrimaryBouncerIsOrWillBeShowing && mCredentialAttempted);
 
         final boolean isEncryptedOrLockdownForUser = isEncryptedOrLockdown(user);
+
         final boolean shouldListenUdfpsState = !isUdfps
                 || (!userCanSkipBouncer
-                    && !isEncryptedOrLockdownForUser
-                    && userDoesNotHaveTrust);
+                && !isEncryptedOrLockdownForUser
+                && userDoesNotHaveTrust);
 
-        final boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
-                && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut();
+        boolean shouldListenSideFpsState = true;
+        if (isSfpsSupported() && isSfpsEnrolled()) {
+            shouldListenSideFpsState =
+                    mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true;
+        }
+
+        boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
+                && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut()
+                && shouldListenSideFpsState;
 
         maybeLogListenerModelData(
                 new KeyguardFingerprintListenModel(
@@ -2593,7 +2661,7 @@
                     user,
                     shouldListen,
                     biometricEnabledForUser,
-                    mBouncerIsOrWillBeShowing,
+                        mPrimaryBouncerIsOrWillBeShowing,
                     userCanSkipBouncer,
                     mCredentialAttempted,
                     mDeviceInteractive,
@@ -2607,6 +2675,7 @@
                     mKeyguardOccluded,
                     mOccludingAppRequestingFp,
                     mIsPrimaryUser,
+                    shouldListenSideFpsState,
                     shouldListenForFingerprintAssistant,
                     mSwitchingUser,
                     isUdfps,
@@ -2637,19 +2706,19 @@
                         || containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT);
 
         // TODO: always disallow when fp is already locked out?
-        final boolean fpLockedout = mFingerprintLockedOut || mFingerprintLockedOutPermanent;
+        final boolean fpLockedOut = mFingerprintLockedOut || mFingerprintLockedOutPermanent;
 
         final boolean canBypass = mKeyguardBypassController != null
                 && mKeyguardBypassController.canBypass();
         // There's no reason to ask the HAL for authentication when the user can dismiss the
-        // bouncer, unless we're bypassing and need to auto-dismiss the lock screen even when
-        // TrustAgents or biometrics are keeping the device unlocked.
-        final boolean becauseCannotSkipBouncer = !getUserCanSkipBouncer(user) || canBypass;
+        // bouncer because the user is trusted, unless we're bypassing and need to auto-dismiss
+        // the lock screen even when TrustAgents are keeping the device unlocked.
+        final boolean userNotTrustedOrDetectionIsNeeded = !getUserHasTrust(user) || canBypass;
 
         // Scan even when encrypted or timeout to show a preemptive bouncer when bypassing.
         // Lock-down mode shouldn't scan, since it is more explicit.
         boolean strongAuthAllowsScanning = (!isEncryptedOrTimedOut || canBypass
-                && !mBouncerFullyShown);
+                && !mPrimaryBouncerFullyShown);
 
         // If the device supports face detection (without authentication) and bypass is enabled,
         // allow face scanning to happen if the device is in lockdown mode.
@@ -2661,30 +2730,33 @@
             strongAuthAllowsScanning = false;
         }
 
-        // If the face has recently been authenticated do not attempt to authenticate again.
-        final boolean faceAuthenticated = getIsFaceAuthenticated();
+        // If the face or fp has recently been authenticated do not attempt to authenticate again.
+        final boolean faceAndFpNotAuthenticated = !getUserUnlockedWithBiometric(user);
         final boolean faceDisabledForUser = isFaceDisabled(user);
         final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user);
         final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant();
-        final boolean fpOrFaceIsLockedOut = isFaceLockedOut() || fpLockedout;
+        final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown();
 
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         final boolean shouldListen =
-                (mBouncerFullyShown
+                (mPrimaryBouncerFullyShown
                         || mAuthInterruptActive
                         || mOccludingAppRequestingFace
                         || awakeKeyguard
                         || shouldListenForFaceAssistant
-                        || mAuthController.isUdfpsFingerDown()
+                        || isUdfpsFingerDown
                         || mUdfpsBouncerShowing)
-                && !mSwitchingUser && !faceDisabledForUser && becauseCannotSkipBouncer
+                && !mSwitchingUser && !faceDisabledForUser && userNotTrustedOrDetectionIsNeeded
                 && !mKeyguardGoingAway && biometricEnabledForUser
                 && strongAuthAllowsScanning && mIsPrimaryUser
                 && (!mSecureCameraLaunched || mOccludingAppRequestingFace)
-                && !faceAuthenticated
+                && faceAndFpNotAuthenticated
                 && !mGoingToSleep
-                && !fpOrFaceIsLockedOut;
+                // We only care about fp locked out state and not face because we still trigger
+                // face auth even when face is locked out to show the user a message that face
+                // unlock was supposed to run but didn't
+                && !fpLockedOut;
 
         // Aggregate relevant fields for debug logging.
         maybeLogListenerModelData(
@@ -2693,13 +2765,12 @@
                     user,
                     shouldListen,
                     mAuthInterruptActive,
-                    becauseCannotSkipBouncer,
                     biometricEnabledForUser,
-                    mBouncerFullyShown,
-                    faceAuthenticated,
+                        mPrimaryBouncerFullyShown,
+                    faceAndFpNotAuthenticated,
                     faceDisabledForUser,
                     isFaceLockedOut(),
-                    fpLockedout,
+                    fpLockedOut,
                     mGoingToSleep,
                     awakeKeyguard,
                     mKeyguardGoingAway,
@@ -2709,12 +2780,14 @@
                     strongAuthAllowsScanning,
                     mSecureCameraLaunched,
                     mSwitchingUser,
-                    mUdfpsBouncerShowing));
+                    mUdfpsBouncerShowing,
+                    isUdfpsFingerDown,
+                    userNotTrustedOrDetectionIsNeeded));
 
         return shouldListen;
     }
 
-    private void maybeLogListenerModelData(KeyguardListenModel model) {
+    private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) {
         mLogger.logKeyguardListenerModel(model);
 
         if (model instanceof KeyguardActiveUnlockModel) {
@@ -2787,8 +2860,14 @@
             // Waiting for ERROR_CANCELED before requesting auth again
             return;
         }
-        mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason());
-        mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId());
+        mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent);
+        mUiEventLogger.logWithInstanceIdAndPosition(
+                faceAuthUiEvent,
+                0,
+                null,
+                getKeyguardSessionId(),
+                faceAuthUiEvent.getExtraInfo()
+        );
 
         if (unlockPossible) {
             mFaceCancelSignal = new CancellationSignal();
@@ -3219,17 +3298,19 @@
     /**
      * Handle {@link #MSG_KEYGUARD_BOUNCER_CHANGED}
      *
-     * @see #sendKeyguardBouncerChanged(boolean, boolean)
+     * @see #sendPrimaryBouncerChanged(boolean, boolean)
      */
-    private void handleKeyguardBouncerChanged(int bouncerIsOrWillBeShowing, int bouncerFullyShown) {
+    private void handlePrimaryBouncerChanged(int primaryBouncerIsOrWillBeShowing,
+            int primaryBouncerFullyShown) {
         Assert.isMainThread();
-        final boolean wasBouncerIsOrWillBeShowing = mBouncerIsOrWillBeShowing;
-        final boolean wasBouncerFullyShown = mBouncerFullyShown;
-        mBouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing == 1;
-        mBouncerFullyShown = bouncerFullyShown == 1;
-        mLogger.logKeyguardBouncerChanged(mBouncerIsOrWillBeShowing, mBouncerFullyShown);
+        final boolean wasPrimaryBouncerIsOrWillBeShowing = mPrimaryBouncerIsOrWillBeShowing;
+        final boolean wasPrimaryBouncerFullyShown = mPrimaryBouncerFullyShown;
+        mPrimaryBouncerIsOrWillBeShowing = primaryBouncerIsOrWillBeShowing == 1;
+        mPrimaryBouncerFullyShown = primaryBouncerFullyShown == 1;
+        mLogger.logPrimaryKeyguardBouncerChanged(mPrimaryBouncerIsOrWillBeShowing,
+                mPrimaryBouncerFullyShown);
 
-        if (mBouncerFullyShown) {
+        if (mPrimaryBouncerFullyShown) {
             // If the bouncer is shown, always clear this flag. This can happen in the following
             // situations: 1) Default camera with SHOW_WHEN_LOCKED is not chosen yet. 2) Secure
             // camera requests dismiss keyguard (tapping on photos for example). When these happen,
@@ -3239,18 +3320,18 @@
             mCredentialAttempted = false;
         }
 
-        if (wasBouncerIsOrWillBeShowing != mBouncerIsOrWillBeShowing) {
+        if (wasPrimaryBouncerIsOrWillBeShowing != mPrimaryBouncerIsOrWillBeShowing) {
             for (int i = 0; i < mCallbacks.size(); i++) {
                 KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
                 if (cb != null) {
-                    cb.onKeyguardBouncerStateChanged(mBouncerIsOrWillBeShowing);
+                    cb.onKeyguardBouncerStateChanged(mPrimaryBouncerIsOrWillBeShowing);
                 }
             }
             updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         }
 
-        if (wasBouncerFullyShown != mBouncerFullyShown) {
-            if (mBouncerFullyShown) {
+        if (wasPrimaryBouncerFullyShown != mPrimaryBouncerFullyShown) {
+            if (mPrimaryBouncerFullyShown) {
                 requestActiveUnlock(
                         ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
                         "bouncerFullyShown");
@@ -3258,7 +3339,7 @@
             for (int i = 0; i < mCallbacks.size(); i++) {
                 KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
                 if (cb != null) {
-                    cb.onKeyguardBouncerFullyShowingChanged(mBouncerFullyShown);
+                    cb.onKeyguardBouncerFullyShowingChanged(mPrimaryBouncerFullyShown);
                 }
             }
             updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
@@ -3409,14 +3490,15 @@
     }
 
     /**
-     * @see #handleKeyguardBouncerChanged(int, int)
+     * @see #handlePrimaryBouncerChanged(int, int)
      */
-    public void sendKeyguardBouncerChanged(boolean bouncerIsOrWillBeShowing,
-            boolean bouncerFullyShown) {
-        mLogger.logSendKeyguardBouncerChanged(bouncerIsOrWillBeShowing, bouncerFullyShown);
+    public void sendPrimaryBouncerChanged(boolean primaryBouncerIsOrWillBeShowing,
+            boolean primaryBouncerFullyShown) {
+        mLogger.logSendPrimaryBouncerChanged(primaryBouncerIsOrWillBeShowing,
+                primaryBouncerFullyShown);
         Message message = mHandler.obtainMessage(MSG_KEYGUARD_BOUNCER_CHANGED);
-        message.arg1 = bouncerIsOrWillBeShowing ? 1 : 0;
-        message.arg2 = bouncerFullyShown ? 1 : 0;
+        message.arg1 = primaryBouncerIsOrWillBeShowing ? 1 : 0;
+        message.arg2 = primaryBouncerFullyShown ? 1 : 0;
         message.sendToTarget();
     }
 
@@ -3567,11 +3649,16 @@
 
     // TODO: use these callbacks elsewhere in place of the existing notifyScreen*()
     // (KeyguardViewMediator, KeyguardHostView)
-    public void dispatchStartedWakingUp() {
+    /**
+     * Dispatch wakeup events to:
+     *  - update biometric listening states
+     *  - send to registered KeyguardUpdateMonitorCallbacks
+     */
+    public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
         synchronized (this) {
             mDeviceInteractive = true;
         }
-        mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP);
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0));
     }
 
     public void dispatchStartedGoingToSleep(int why) {
@@ -3697,6 +3784,11 @@
             mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);
         }
 
+        if (mSfpsRequireScreenOnToAuthPrefObserver != null) {
+            mContext.getContentResolver().unregisterContentObserver(
+                    mSfpsRequireScreenOnToAuthPrefObserver);
+        }
+
         try {
             ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
         } catch (RemoteException e) {
@@ -3740,7 +3832,7 @@
             pw.println("    " + subId + "=" + mServiceStates.get(subId));
         }
         if (mFpm != null && mFpm.isHardwareDetected()) {
-            final int userId = ActivityManager.getCurrentUser();
+            final int userId = mUserTracker.getUserId();
             final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId);
             BiometricAuthenticated fingerprint = mUserFingerprintAuthenticated.get(userId);
             pw.println("  Fingerprint state (user=" + userId + ")");
@@ -3766,13 +3858,21 @@
             if (isUdfpsSupported()) {
                 pw.println("        udfpsEnrolled=" + isUdfpsEnrolled());
                 pw.println("        shouldListenForUdfps=" + shouldListenForFingerprint(true));
-                pw.println("        mBouncerIsOrWillBeShowing=" + mBouncerIsOrWillBeShowing);
+                pw.println("        mPrimaryBouncerIsOrWillBeShowing="
+                        + mPrimaryBouncerIsOrWillBeShowing);
                 pw.println("        mStatusBarState=" + StatusBarState.toString(mStatusBarState));
                 pw.println("        mUdfpsBouncerShowing=" + mUdfpsBouncerShowing);
+            } else if (isSfpsSupported()) {
+                pw.println("        sfpsEnrolled=" + isSfpsEnrolled());
+                pw.println("        shouldListenForSfps=" + shouldListenForFingerprint(false));
+                if (isSfpsEnrolled()) {
+                    pw.println("        mSfpsRequireScreenOnToAuthPrefEnabled="
+                        + mSfpsRequireScreenOnToAuthPrefEnabled);
+                }
             }
         }
         if (mFaceManager != null && mFaceManager.isHardwareDetected()) {
-            final int userId = ActivityManager.getCurrentUser();
+            final int userId = mUserTracker.getUserId();
             final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId);
             BiometricAuthenticated face = mUserFaceAuthenticated.get(userId);
             pw.println("  Face authentication state (user=" + userId + ")");
@@ -3791,9 +3891,22 @@
             pw.println("    mFaceLockedOutPermanent=" + mFaceLockedOutPermanent);
             pw.println("    enabledByUser=" + mBiometricEnabledForUser.get(userId));
             pw.println("    mSecureCameraLaunched=" + mSecureCameraLaunched);
-            pw.println("    mBouncerFullyShown=" + mBouncerFullyShown);
+            pw.println("    mPrimaryBouncerFullyShown=" + mPrimaryBouncerFullyShown);
             pw.println("    mNeedsSlowUnlockTransition=" + mNeedsSlowUnlockTransition);
         }
         mListenModels.print(pw);
     }
+
+    /**
+     * Schedules a watchdog for the face and fingerprint BiometricScheduler.
+     * Cancels all operations in the scheduler if it is hung for 10 seconds.
+     */
+    public void startBiometricWatchdog() {
+        if (mFaceManager != null) {
+            mFaceManager.scheduleWatchdog();
+        }
+        if (mFpm != null) {
+            mFpm.scheduleWatchdog();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index c06e1dc..1d58fc9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -19,6 +19,7 @@
 import android.telephony.TelephonyManager;
 import android.view.WindowManagerPolicyConstants;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settingslib.fuelgauge.BatteryStatus;
@@ -174,14 +175,14 @@
     public void onTrustManagedChanged(int userId) { }
 
     /**
-     * Called after trust was granted with non-zero flags.
+     * Called after trust was granted.
+     * @param dismissKeyguard whether the keyguard should be dismissed as a result of the
+     *                        trustGranted
+     * @param message optional message the trust agent has provided to show that should indicate
+     *                why trust was granted.
      */
-    public void onTrustGrantedWithFlags(int flags, int userId) { }
-
-    /**
-     * Called when setting the trust granted message.
-     */
-    public void showTrustGrantedMessage(@Nullable CharSequence message) { }
+    public void onTrustGrantedForCurrentUser(boolean dismissKeyguard,
+            @NonNull TrustGrantFlags flags, @Nullable String message) { }
 
     /**
      * Called when a biometric has been acquired.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index 90f0446..6c3c246 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -50,16 +50,11 @@
 
     /**
      * Resets the state of Keyguard View.
-     * @param hideBouncerWhenShowing
+     * @param hideBouncerWhenShowing when true, hides the primary and alternate bouncers if showing.
      */
     void reset(boolean hideBouncerWhenShowing);
 
     /**
-     * Stop showing any alternate auth methods.
-     */
-    void resetAlternateAuth(boolean forceUpdateScrim);
-
-    /**
      * Called when the device started going to sleep.
      */
     default void onStartedGoingToSleep() {};
@@ -156,20 +151,24 @@
     void notifyKeyguardAuthenticated(boolean strongAuth);
 
     /**
-     * Shows the Bouncer.
-     *
+     * Shows the primary bouncer.
      */
-    void showBouncer(boolean scrimmed);
+    void showPrimaryBouncer(boolean scrimmed);
 
     /**
-     * Returns {@code true} when the bouncer is currently showing
+     * When the primary bouncer is fully visible or is showing but animation didn't finish yet.
+     */
+    boolean primaryBouncerIsOrWillBeShowing();
+
+    /**
+     * Returns {@code true} when the primary bouncer or alternate bouncer is currently showing
      */
     boolean isBouncerShowing();
 
     /**
-     * When bouncer is fully visible or it is showing but animation didn't finish yet.
+     * Stop showing the alternate bouncer, if showing.
      */
-    boolean bouncerIsOrWillBeShowing();
+    void hideAlternateBouncer(boolean forceUpdateScrim);
 
     // TODO: Deprecate registerStatusBar in KeyguardViewController interface. It is currently
     //  only used for testing purposes in StatusBarKeyguardViewManager, and it prevents us from
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconView.java b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
index 0a82968..34a5ef7 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconView.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconView.java
@@ -158,6 +158,10 @@
         return mLockIconCenter.y - mRadius;
     }
 
+    float getLocationBottom() {
+        return mLockIconCenter.y + mRadius;
+    }
+
     /**
      * Updates the icon its default state where no visual is shown.
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 70758df..cf246b6 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -24,6 +24,8 @@
 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -46,6 +48,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 
 import com.android.systemui.Dumpable;
@@ -55,6 +58,10 @@
 import com.android.systemui.biometrics.UdfpsController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -67,6 +74,7 @@
 
 import java.io.PrintWriter;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -103,6 +111,9 @@
     @NonNull private CharSequence mLockedLabel;
     @NonNull private final VibratorHelper mVibrator;
     @Nullable private final AuthRippleController mAuthRippleController;
+    @NonNull private final FeatureFlags mFeatureFlags;
+    @NonNull private final KeyguardTransitionInteractor mTransitionInteractor;
+    @NonNull private final KeyguardInteractor mKeyguardInteractor;
 
     // Tracks the velocity of a touch to help filter out the touches that move too fast.
     private VelocityTracker mVelocityTracker;
@@ -139,6 +150,20 @@
     private boolean mDownDetected;
     private final Rect mSensorTouchLocation = new Rect();
 
+    @VisibleForTesting
+    final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> {
+        mInterpolatedDarkAmount = step.getValue();
+        mView.setDozeAmount(step.getValue());
+        updateBurnInOffsets();
+    };
+
+    @VisibleForTesting
+    final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> {
+        mIsDozing = isDozing;
+        updateBurnInOffsets();
+        updateVisibility();
+    };
+
     @Inject
     public LockIconViewController(
             @Nullable LockIconView view,
@@ -154,7 +179,10 @@
             @NonNull @Main DelayableExecutor executor,
             @NonNull VibratorHelper vibrator,
             @Nullable AuthRippleController authRippleController,
-            @NonNull @Main Resources resources
+            @NonNull @Main Resources resources,
+            @NonNull KeyguardTransitionInteractor transitionInteractor,
+            @NonNull KeyguardInteractor keyguardInteractor,
+            @NonNull FeatureFlags featureFlags
     ) {
         super(view);
         mStatusBarStateController = statusBarStateController;
@@ -168,6 +196,9 @@
         mExecutor = executor;
         mVibrator = vibrator;
         mAuthRippleController = authRippleController;
+        mTransitionInteractor = transitionInteractor;
+        mKeyguardInteractor = keyguardInteractor;
+        mFeatureFlags = featureFlags;
 
         mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
         mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
@@ -184,6 +215,12 @@
     @Override
     protected void onInit() {
         mView.setAccessibilityDelegate(mAccessibilityDelegate);
+
+        if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+            collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(),
+                    mDozeTransitionCallback);
+            collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback);
+        }
     }
 
     @Override
@@ -245,6 +282,10 @@
         return mView.getLocationTop();
     }
 
+    public float getBottom() {
+        return mView.getLocationBottom();
+    }
+
     private void updateVisibility() {
         if (mCancelDelayedUpdateVisibilityRunnable != null) {
             mCancelDelayedUpdateVisibilityRunnable.run();
@@ -379,14 +420,17 @@
         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
         pw.println(" mShowLockIcon: " + mShowLockIcon);
         pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon);
-        pw.println("  mIsDozing: " + mIsDozing);
-        pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
-        pw.println("  mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
-        pw.println("  mRunningFPS: " + mRunningFPS);
-        pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
-        pw.println("  mStatusBarState: " + StatusBarState.toString(mStatusBarState));
-        pw.println("  mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
-        pw.println("  mSensorTouchLocation: " + mSensorTouchLocation);
+        pw.println();
+        pw.println(" mIsDozing: " + mIsDozing);
+        pw.println(" isFlagEnabled(DOZING_MIGRATION_1): "
+                + mFeatureFlags.isEnabled(DOZING_MIGRATION_1));
+        pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
+        pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
+        pw.println(" mRunningFPS: " + mRunningFPS);
+        pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
+        pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
+        pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
+        pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
 
         if (mView != null) {
             mView.dump(pw, args);
@@ -427,16 +471,20 @@
             new StatusBarStateController.StateListener() {
                 @Override
                 public void onDozeAmountChanged(float linear, float eased) {
-                    mInterpolatedDarkAmount = eased;
-                    mView.setDozeAmount(eased);
-                    updateBurnInOffsets();
+                    if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                        mInterpolatedDarkAmount = eased;
+                        mView.setDozeAmount(eased);
+                        updateBurnInOffsets();
+                    }
                 }
 
                 @Override
                 public void onDozingChanged(boolean isDozing) {
-                    mIsDozing = isDozing;
-                    updateBurnInOffsets();
-                    updateVisibility();
+                    if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                        mIsDozing = isDozing;
+                        updateBurnInOffsets();
+                        updateVisibility();
+                    }
                 }
 
                 @Override
@@ -653,7 +701,7 @@
                 "lock-screen-lock-icon-longpress",
                 TOUCH_VIBRATION_ATTRIBUTES);
 
-        mKeyguardViewController.showBouncer(/* scrim */ true);
+        mKeyguardViewController.showPrimaryBouncer(/* scrim */ true);
     }
 
 
diff --git a/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java b/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java
new file mode 100644
index 0000000..d33732c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/TrustGrantFlags.java
@@ -0,0 +1,98 @@
+/*
+ * 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.keyguard;
+
+import android.service.trust.TrustAgentService;
+
+import java.util.Objects;
+
+/**
+ * Translating {@link android.service.trust.TrustAgentService.GrantTrustFlags} to a more
+ * parsable object. These flags are requested by a TrustAgent.
+ */
+public class TrustGrantFlags {
+    final int mFlags;
+
+    public TrustGrantFlags(int flags) {
+        this.mFlags = flags;
+    }
+
+    /** {@link TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER} */
+    public boolean isInitiatedByUser() {
+        return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0;
+    }
+
+    /**
+     * Trust agent is requesting to dismiss the keyguard.
+     * See {@link TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD}.
+     *
+     * This does not guarantee that the keyguard is dismissed.
+     * KeyguardUpdateMonitor makes the final determination whether the keyguard should be dismissed.
+     * {@link KeyguardUpdateMonitorCallback#onTrustGrantedForCurrentUser(
+     *      boolean, TrustGrantFlags, String).
+     */
+    public boolean dismissKeyguardRequested() {
+        return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0;
+    }
+
+    /** {@link TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE} */
+    public boolean temporaryAndRenewable() {
+        return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0;
+    }
+
+    /** {@link TrustAgentService.FLAG_GRANT_TRUST_DISPLAY_MESSAGE} */
+    public boolean displayMessage() {
+        return (mFlags & TrustAgentService.FLAG_GRANT_TRUST_DISPLAY_MESSAGE) != 0;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof TrustGrantFlags)) {
+            return false;
+        }
+
+        return ((TrustGrantFlags) o).mFlags == this.mFlags;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(mFlags);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        sb.append(mFlags);
+        sb.append("]=");
+
+        if (isInitiatedByUser()) {
+            sb.append("initiatedByUser|");
+        }
+        if (dismissKeyguardRequested()) {
+            sb.append("dismissKeyguard|");
+        }
+        if (temporaryAndRenewable()) {
+            sb.append("temporaryAndRenewable|");
+        }
+        if (displayMessage()) {
+            sb.append("displayMessage|");
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
new file mode 100644
index 0000000..72a44bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package com.android.keyguard.clock;
+
+import java.util.List;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger Module for clock package.
+ *
+ * @deprecated Migrate to ClockRegistry
+ */
+@Module
+@Deprecated
+public abstract class ClockInfoModule {
+
+    /** */
+    @Provides
+    public static List<ClockInfo> provideClockInfoList(ClockManager clockManager) {
+        return clockManager.getClockInfos();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
index 9a0bfc1..ad9609f 100644
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
@@ -29,17 +29,17 @@
 import android.util.DisplayMetrics;
 import android.view.LayoutInflater;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.Observer;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManager.DockEventListener;
 import com.android.systemui.plugins.ClockPlugin;
 import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.settings.CurrentUserObservable;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 
 import java.util.ArrayList;
@@ -47,6 +47,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 import javax.inject.Inject;
@@ -69,7 +70,8 @@
     private final ContentResolver mContentResolver;
     private final SettingsWrapper mSettingsWrapper;
     private final Handler mMainHandler = new Handler(Looper.getMainLooper());
-    private final CurrentUserObservable mCurrentUserObservable;
+    private final UserTracker mUserTracker;
+    private final Executor mMainExecutor;
 
     /**
      * Observe settings changes to know when to switch the clock face.
@@ -80,7 +82,7 @@
                 public void onChange(boolean selfChange, Collection<Uri> uris,
                         int flags, int userId) {
                     if (Objects.equals(userId,
-                            mCurrentUserObservable.getCurrentUser().getValue())) {
+                            mUserTracker.getUserId())) {
                         reload();
                     }
                 }
@@ -89,7 +91,13 @@
     /**
      * Observe user changes and react by potentially loading the custom clock for the new user.
      */
-    private final Observer<Integer> mCurrentUserObserver = (newUserId) -> reload();
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    reload();
+                }
+            };
 
     private final PluginManager mPluginManager;
     @Nullable private final DockManager mDockManager;
@@ -129,22 +137,24 @@
     @Inject
     public ClockManager(Context context, LayoutInflater layoutInflater,
             PluginManager pluginManager, SysuiColorExtractor colorExtractor,
-            @Nullable DockManager dockManager, BroadcastDispatcher broadcastDispatcher) {
+            @Nullable DockManager dockManager, UserTracker userTracker,
+            @Main Executor mainExecutor) {
         this(context, layoutInflater, pluginManager, colorExtractor,
-                context.getContentResolver(), new CurrentUserObservable(broadcastDispatcher),
+                context.getContentResolver(), userTracker, mainExecutor,
                 new SettingsWrapper(context.getContentResolver()), dockManager);
     }
 
     @VisibleForTesting
     ClockManager(Context context, LayoutInflater layoutInflater,
             PluginManager pluginManager, SysuiColorExtractor colorExtractor,
-            ContentResolver contentResolver, CurrentUserObservable currentUserObservable,
+            ContentResolver contentResolver, UserTracker userTracker, Executor mainExecutor,
             SettingsWrapper settingsWrapper, DockManager dockManager) {
         mContext = context;
         mPluginManager = pluginManager;
         mContentResolver = contentResolver;
         mSettingsWrapper = settingsWrapper;
-        mCurrentUserObservable = currentUserObservable;
+        mUserTracker = userTracker;
+        mMainExecutor = mainExecutor;
         mDockManager = dockManager;
         mPreviewClocks = new AvailableClocks();
 
@@ -226,7 +236,7 @@
         mContentResolver.registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.DOCKED_CLOCK_FACE),
                 false, mContentObserver, UserHandle.USER_ALL);
-        mCurrentUserObservable.getCurrentUser().observeForever(mCurrentUserObserver);
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
         if (mDockManager != null) {
             mDockManager.addListener(mDockEventListener);
         }
@@ -235,7 +245,7 @@
     private void unregister() {
         mPluginManager.removePluginListener(mPreviewClocks);
         mContentResolver.unregisterContentObserver(mContentObserver);
-        mCurrentUserObservable.getCurrentUser().removeObserver(mCurrentUserObserver);
+        mUserTracker.removeCallback(mUserChangedCallback);
         if (mDockManager != null) {
             mDockManager.removeListener(mDockEventListener);
         }
@@ -363,7 +373,7 @@
             ClockPlugin plugin = null;
             if (ClockManager.this.isDocked()) {
                 final String name = mSettingsWrapper.getDockedClockFace(
-                        mCurrentUserObservable.getCurrentUser().getValue());
+                        mUserTracker.getUserId());
                 if (name != null) {
                     plugin = mClocks.get(name);
                     if (plugin != null) {
@@ -372,7 +382,7 @@
                 }
             }
             final String name = mSettingsWrapper.getLockScreenCustomClockFace(
-                    mCurrentUserObservable.getCurrentUser().getValue());
+                    mUserTracker.getUserId());
             if (name != null) {
                 plugin = mClocks.get(name);
             }
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
deleted file mode 100644
index c4be1ba535..0000000
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
+++ /dev/null
@@ -1,33 +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.
- */
-
-package com.android.keyguard.clock;
-
-import java.util.List;
-
-import dagger.Module;
-import dagger.Provides;
-
-/** Dagger Module for clock package. */
-@Module
-public abstract class ClockModule {
-
-    /** */
-    @Provides
-    public static List<ClockInfo> provideClockInfoList(ClockManager clockManager) {
-        return clockManager.getClockInfos();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
new file mode 100644
index 0000000..b514f60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -0,0 +1,57 @@
+/*
+ * 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.keyguard.dagger;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.clocks.DefaultClockProvider;
+import com.android.systemui.shared.plugins.PluginManager;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger Module for clocks. */
+@Module
+public abstract class ClockRegistryModule {
+    /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */
+    @Provides
+    @SysUISingleton
+    public static ClockRegistry getClockRegistry(
+            @Application Context context,
+            PluginManager pluginManager,
+            @Main Handler handler,
+            DefaultClockProvider defaultClockProvider,
+            FeatureFlags featureFlags) {
+        return new ClockRegistry(
+                context,
+                pluginManager,
+                handler,
+                featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
+                UserHandle.USER_ALL,
+                defaultClockProvider,
+                context.getString(R.string.lockscreen_clock_id_fallback));
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
index 49e9783..ef067b8 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardBouncerModule.java
@@ -16,7 +16,7 @@
 
 package com.android.keyguard.dagger;
 
-import static com.android.systemui.biometrics.SidefpsControllerKt.hasSideFpsSensor;
+import static com.android.systemui.biometrics.SideFpsControllerKt.hasSideFpsSensor;
 
 import android.annotation.Nullable;
 import android.hardware.fingerprint.FingerprintManager;
@@ -27,7 +27,7 @@
 import com.android.keyguard.KeyguardSecurityContainer;
 import com.android.keyguard.KeyguardSecurityViewFlipper;
 import com.android.systemui.R;
-import com.android.systemui.biometrics.SidefpsController;
+import com.android.systemui.biometrics.SideFpsController;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 
@@ -70,12 +70,12 @@
         return containerView.findViewById(R.id.view_flipper);
     }
 
-    /** Provides {@link SidefpsController} if the device has the side fingerprint sensor. */
+    /** Provides {@link SideFpsController} if the device has the side fingerprint sensor. */
     @Provides
     @KeyguardBouncerScope
-    static Optional<SidefpsController> providesOptionalSidefpsController(
+    static Optional<SideFpsController> providesOptionalSidefpsController(
             @Nullable FingerprintManager fingerprintManager,
-            Provider<SidefpsController> sidefpsControllerProvider) {
+            Provider<SideFpsController> sidefpsControllerProvider) {
         if (!hasSideFpsSensor(fingerprintManager)) {
             return Optional.empty();
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
index 8fc8600..a7d4455 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
@@ -21,10 +21,7 @@
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl;
 
-import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 
@@ -50,10 +47,4 @@
     static StatusBarUserSwitcherContainer getUserSwitcherContainer(KeyguardStatusBarView view) {
         return view.findViewById(R.id.user_switcher_container);
     }
-
-    /** */
-    @Binds
-    @KeyguardStatusBarViewScope
-    abstract StatusBarUserSwitcherController bindStatusBarUserSwitcherController(
-            StatusBarUserSwitcherControllerImpl controller);
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index 2c2ab7b..6264ce7 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -17,9 +17,9 @@
 package com.android.keyguard.logging
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.dagger.BiometricMessagesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import javax.inject.Inject
 
 /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 50012a5..9e58500 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,15 +16,13 @@
 
 package com.android.keyguard.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.MessageInitializer
-import com.android.systemui.log.MessagePrinter
 import com.android.systemui.log.dagger.KeyguardLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -36,33 +34,46 @@
  * an overkill.
  */
 class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) {
-    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+    fun d(@CompileTimeConstant msg: String) = buffer.log(TAG, DEBUG, msg)
 
-    fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+    fun e(@CompileTimeConstant msg: String) = buffer.log(TAG, ERROR, msg)
 
-    fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+    fun v(@CompileTimeConstant msg: String) = buffer.log(TAG, VERBOSE, msg)
 
-    fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+    fun w(@CompileTimeConstant msg: String) = buffer.log(TAG, WARNING, msg)
 
-    fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg)
+    fun logException(ex: Exception, @CompileTimeConstant logMsg: String) {
+        buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex)
+    }
 
-    private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) {
-        buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
+    fun v(msg: String, arg: Any) {
+        buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" })
+    }
+
+    fun i(msg: String, arg: Any) {
+        buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
     }
 
     // TODO: remove after b/237743330 is fixed
     fun logStatusBarCalculatedAlpha(alpha: Float) {
-        debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
+        buffer.log(TAG, DEBUG, { double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })
     }
 
     // TODO: remove after b/237743330 is fixed
     fun logStatusBarExplicitAlpha(alpha: Float) {
-        debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" })
+        buffer.log(
+            TAG,
+            DEBUG,
+            { double1 = alpha.toDouble() },
+            { "new mExplicitAlpha value: $double1" }
+        )
     }
 
     // TODO: remove after b/237743330 is fixed
     fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) {
-        debugLog(
+        buffer.log(
+            TAG,
+            DEBUG,
             {
                 int1 = visibility
                 double1 = alpha.toDouble()
@@ -71,4 +82,22 @@
             { "changing visibility to $int1 with alpha $double1 in state: $str1" }
         )
     }
+
+    @JvmOverloads
+    fun logBiometricMessage(
+        @CompileTimeConstant context: String,
+        msgId: Int? = null,
+        msg: String? = null
+    ) {
+        buffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = context
+                str2 = "$msgId"
+                str3 = msg
+            },
+            { "$str1 msgId: $str2 msg: $str3" }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2eee957..6763700 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -17,18 +17,22 @@
 package com.android.keyguard.logging
 
 import android.hardware.biometrics.BiometricConstants.LockoutMode
+import android.os.PowerManager
+import android.os.PowerManager.WakeReason
 import android.telephony.ServiceState
 import android.telephony.SubscriptionInfo
 import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.FaceAuthUiEvent
 import com.android.keyguard.KeyguardListenModel
 import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.keyguard.TrustGrantFlags
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
@@ -51,7 +55,7 @@
 
     fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
 
-    fun logActiveUnlockTriggered(reason: String) {
+    fun logActiveUnlockTriggered(reason: String?) {
         logBuffer.log("ActiveUnlock", DEBUG,
                 { str1 = reason },
                 { "initiate active unlock triggerReason=$str1" })
@@ -101,18 +105,17 @@
                 { "Face authenticated for wrong user: $int1" })
     }
 
-    fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) {
+    fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) {
         logBuffer.log(TAG, DEBUG, {
                     int1 = msgId
                     str1 = helpMsg
                 }, { "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) {
@@ -151,19 +154,29 @@
                 { "fingerprintRunningState: $int1" })
     }
 
+    fun logFingerprintSuccess(userId: Int, isStrongBiometric: Boolean) {
+        logBuffer.log(TAG, DEBUG, {
+            int1 = userId
+            bool1 = isStrongBiometric
+        }, {"Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1"})
+    }
+
     fun logInvalidSubId(subId: Int) {
         logBuffer.log(TAG, INFO,
                 { int1 = subId },
                 { "Previously active sub id $int1 is now invalid, will remove" })
     }
 
-    fun logKeyguardBouncerChanged(bouncerIsOrWillBeShowing: Boolean, bouncerFullyShown: Boolean) {
+    fun logPrimaryKeyguardBouncerChanged(
+            primaryBouncerIsOrWillBeShowing: Boolean,
+            primaryBouncerFullyShown: Boolean
+    ) {
         logBuffer.log(TAG, DEBUG, {
-            bool1 = bouncerIsOrWillBeShowing
-            bool2 = bouncerFullyShown
+            bool1 = primaryBouncerIsOrWillBeShowing
+            bool2 = primaryBouncerFullyShown
         }, {
-            "handleKeyguardBouncerChanged " +
-                    "bouncerIsOrWillBeShowing=$bool1 bouncerFullyShowing=$bool2"
+            "handlePrimaryBouncerChanged " +
+                    "primaryBouncerIsOrWillBeShowing=$bool1 primaryBouncerFullyShown=$bool2"
         })
     }
 
@@ -187,7 +200,7 @@
                 { "No Profile Owner or Device Owner supervision app found for User $int1" })
     }
 
-    fun logPhoneStateChanged(newState: String) {
+    fun logPhoneStateChanged(newState: String?) {
         logBuffer.log(TAG, DEBUG,
                 { str1 = newState },
                 { "handlePhoneStateChanged($str1)" })
@@ -220,16 +233,16 @@
                 { "Retrying fingerprint attempt: $int1" })
     }
 
-    fun logSendKeyguardBouncerChanged(
-        bouncerIsOrWillBeShowing: Boolean,
-        bouncerFullyShown: Boolean,
+    fun logSendPrimaryBouncerChanged(
+        primaryBouncerIsOrWillBeShowing: Boolean,
+        primaryBouncerFullyShown: Boolean,
     ) {
         logBuffer.log(TAG, DEBUG, {
-            bool1 = bouncerIsOrWillBeShowing
-            bool2 = bouncerFullyShown
+            bool1 = primaryBouncerIsOrWillBeShowing
+            bool2 = primaryBouncerFullyShown
         }, {
-            "sendKeyguardBouncerChanged bouncerIsOrWillBeShowing=$bool1 " +
-                    "bouncerFullyShown=$bool2"
+            "sendPrimaryBouncerChanged primaryBouncerIsOrWillBeShowing=$bool1 " +
+                    "primaryBouncerFullyShown=$bool2"
         })
     }
 
@@ -240,7 +253,7 @@
         }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" })
     }
 
-    fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) {
+    fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
         logBuffer.log(TAG, VERBOSE, {
             str1 = action
             str2 = "$serviceState"
@@ -256,7 +269,7 @@
         }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" })
     }
 
-    fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) {
+    fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
         logBuffer.log(TAG, VERBOSE, {
             str1 = action
             str2 = extraSimState
@@ -269,11 +282,19 @@
         logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
     }
 
-    fun logStartedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
+    fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) {
         logBuffer.log(TAG, VERBOSE, {
             int1 = faceRunningState
-            str1 = faceAuthReason
-        }, { "startListeningForFace(): $int1, reason: $str1" })
+            str1 = faceAuthUiEvent.reason
+            str2 = faceAuthUiEvent.extraInfoToString()
+        }, { "startListeningForFace(): $int1, reason: $str1 $str2" })
+    }
+
+    fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            int1 = faceRunningState
+            str1 = PowerManager.wakeReasonToString(pmWakeReason)
+        }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" })
     }
 
     fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
@@ -289,7 +310,7 @@
                 { "SubInfo:$str1" })
     }
 
-    fun logTimeFormatChanged(newTimeFormat: String) {
+    fun logTimeFormatChanged(newTimeFormat: String?) {
         logBuffer.log(TAG, DEBUG,
                 { str1 = newTimeFormat },
                 { "handleTimeFormatUpdate timeFormat=$str1" })
@@ -338,22 +359,26 @@
 
     fun logUserRequestedUnlock(
         requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
-        reason: String,
+        reason: String?,
         dismissKeyguard: Boolean
     ) {
         logBuffer.log("ActiveUnlock", DEBUG, {
-                    str1 = requestOrigin.name
+                    str1 = requestOrigin?.name
                     str2 = reason
                     bool1 = dismissKeyguard
                 }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" })
     }
 
-    fun logShowTrustGrantedMessage(
-            message: String
+    fun logTrustGrantedWithFlags(
+            flags: Int,
+            userId: Int,
+            message: String?
     ) {
         logBuffer.log(TAG, DEBUG, {
+            int1 = flags
+            int2 = userId
             str1 = message
-        }, { "showTrustGrantedMessage message$str1" })
+        }, { "trustGrantedWithFlags[user=$int2] flags=${TrustGrantFlags(int1)} message=$str1" })
     }
 
     fun logTrustChanged(
@@ -383,4 +408,10 @@
         }, { "#update secure=$bool1 canDismissKeyguard=$bool2" +
                 " trusted=$bool3 trustManaged=$bool4" })
     }
+
+    fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            str1 = PowerManager.wakeReasonToString(pmWakeReason)
+        }, { "Skip updating face listening state on wakeup from $str1"})
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
index 3015710..eee705d 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
@@ -26,8 +26,6 @@
 
 import kotlin.math.roundToInt
 
-const val TAG = "CameraAvailabilityListener"
-
 /**
  * Listens for usage of the Camera and controls the ScreenDecorations transition to show extra
  * protection around a display cutout based on config_frontBuiltInDisplayCutoutProtection and
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
index a89cbf5..9ac45b3 100644
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -4,30 +4,32 @@
 import android.content.Context
 import android.content.pm.PackageManager
 import android.util.Log
+import com.android.internal.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.FlagListenable
 import com.android.systemui.flags.Flags
-import javax.inject.Inject
+import com.android.systemui.settings.UserTracker
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
+import javax.inject.Inject
 
 @SysUISingleton
 class ChooserSelector @Inject constructor(
         private val context: Context,
+        private val userTracker: UserTracker,
         private val featureFlags: FeatureFlags,
         @Application private val coroutineScope: CoroutineScope,
-        @Background private val bgDispatcher: CoroutineDispatcher
+        @Background private val bgDispatcher: CoroutineDispatcher,
 ) : CoreStartable {
 
-    private val packageManager = context.packageManager
     private val chooserComponent = ComponentName.unflattenFromString(
-            context.resources.getString(ChooserSelectorResourceHelper.CONFIG_CHOOSER_ACTIVITY))
+            context.resources.getString(R.string.config_chooserActivity))
 
     override fun start() {
         coroutineScope.launch {
@@ -56,10 +58,17 @@
         } else {
             PackageManager.COMPONENT_ENABLED_STATE_DISABLED
         }
-        try {
-            packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
-        } catch (e: IllegalArgumentException) {
-            Log.w("ChooserSelector", "Unable to set IntentResolver enabled=" + enabled, e)
+        userTracker.userProfiles.forEach {
+            try {
+                context.createContextAsUser(it.userHandle, /* flags = */ 0).packageManager
+                        .setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
+            } catch (e: IllegalArgumentException) {
+                Log.w(
+                        "ChooserSelector",
+                        "Unable to set IntentResolver enabled=$enabled for user ${it.id}",
+                        e,
+                )
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
deleted file mode 100644
index 7a2de7b..0000000
--- a/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
+++ /dev/null
@@ -1,31 +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;
-
-import androidx.annotation.StringRes;
-
-import com.android.internal.R;
-
-/** Helper class for referencing resources */
-class ChooserSelectorResourceHelper {
-
-    private ChooserSelectorResourceHelper() {
-    }
-
-    @StringRes
-    static final int CONFIG_CHOOSER_ACTIVITY = R.string.config_chooserActivity;
-}
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index a5fdc68..51bcd6b 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -70,6 +70,7 @@
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -357,6 +358,7 @@
     @Inject Lazy<GroupExpansionManager> mGroupExpansionManagerLazy;
     @Inject Lazy<SystemUIDialogManager> mSystemUIDialogManagerLazy;
     @Inject Lazy<DialogLaunchAnimator> mDialogLaunchAnimatorLazy;
+    @Inject Lazy<UserTracker> mUserTrackerLazy;
 
     @Inject
     public Dependency() {
@@ -564,6 +566,7 @@
         mProviders.put(GroupExpansionManager.class, mGroupExpansionManagerLazy::get);
         mProviders.put(SystemUIDialogManager.class, mSystemUIDialogManagerLazy::get);
         mProviders.put(DialogLaunchAnimator.class, mDialogLaunchAnimatorLazy::get);
+        mProviders.put(UserTracker.class, mUserTrackerLazy::get);
 
         Dependency.setInstance(this);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
index a3351e1..90ecb46 100644
--- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
+++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
@@ -86,30 +86,38 @@
         onUpdate()
     }
 
-    fun onDisplayChanged(newDisplayUniqueId: String?) {
+    fun updateConfiguration(newDisplayUniqueId: String?) {
+        val info = DisplayInfo()
+        context.display?.getDisplayInfo(info)
         val oldMode: Display.Mode? = displayMode
-        val display: Display? = context.display
-        displayMode = display?.mode
+        displayMode = info.mode
 
-        if (displayUniqueId != display?.uniqueId) {
-            displayUniqueId = display?.uniqueId
-            shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout(
-                context.resources, displayUniqueId
-            )
-        }
+        updateDisplayUniqueId(info.uniqueId)
 
         // Skip if display mode or cutout hasn't changed.
         if (!displayModeChanged(oldMode, displayMode) &&
-                display?.cutout == displayInfo.displayCutout) {
+                displayInfo.displayCutout == info.displayCutout &&
+                displayRotation == info.rotation) {
             return
         }
-        if (newDisplayUniqueId == display?.uniqueId) {
+        if (newDisplayUniqueId == info.uniqueId) {
+            displayRotation = info.rotation
             updateCutout()
             updateProtectionBoundingPath()
             onUpdate()
         }
     }
 
+    open fun updateDisplayUniqueId(newDisplayUniqueId: String?) {
+        if (displayUniqueId != newDisplayUniqueId) {
+            displayUniqueId = newDisplayUniqueId
+            shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout(
+                    context.resources, displayUniqueId
+            )
+            invalidate()
+        }
+    }
+
     open fun updateRotation(rotation: Int) {
         displayRotation = rotation
         updateCutout()
@@ -161,7 +169,7 @@
             return
         }
         cutoutPath.reset()
-        display.getDisplayInfo(displayInfo)
+        context.display?.getDisplayInfo(displayInfo)
         displayInfo.displayCutout?.cutoutPath?.let { path -> cutoutPath.set(path) }
         invalidate()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java
index 6525951..73fdce6 100644
--- a/packages/SystemUI/src/com/android/systemui/Dumpable.java
+++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java
@@ -30,7 +30,6 @@
 
     /**
      * Called when it's time to dump the internal state
-     * @param fd A file descriptor.
      * @param pw Where to write your dump to.
      * @param args Arguments.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index c595586..3e0fa45 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -19,6 +19,7 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
+import android.animation.TimeInterpolator
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Canvas
@@ -55,7 +56,7 @@
     private val rimRect = RectF()
     private var cameraProtectionColor = Color.BLACK
     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
-            com.android.systemui.R.attr.wallpaperTextColorAccent)
+            R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
     var hideOverlayRunnable: Runnable? = null
     var faceAuthSucceeded = false
@@ -84,46 +85,19 @@
     }
 
     override fun drawCutoutProtection(canvas: Canvas) {
-        if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) {
-            val rimPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val rimBounds = RectF()
-                rimPath.computeBounds(rimBounds, true)
-                setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY())
-            }
-            rimPath.transform(scaleMatrix)
-            rimPaint.style = Paint.Style.FILL
-            val rimPaintAlpha = rimPaint.alpha
-            rimPaint.color = ColorUtils.blendARGB(
-                    faceScanningAnimColor,
-                    Color.WHITE,
-                    statusBarStateController.dozeAmount)
-            rimPaint.alpha = rimPaintAlpha
-            canvas.drawPath(rimPath, rimPaint)
+        if (protectionRect.isEmpty) {
+            return
         }
-
-        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE &&
-                !protectionRect.isEmpty) {
-            val scaledProtectionPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val protectionPathRect = RectF()
-                scaledProtectionPath.computeBounds(protectionPathRect, true)
-                setScale(cameraProtectionProgress, cameraProtectionProgress,
-                        protectionPathRect.centerX(), protectionPathRect.centerY())
-            }
-            scaledProtectionPath.transform(scaleMatrix)
-            paint.style = Paint.Style.FILL
-            paint.color = cameraProtectionColor
-            canvas.drawPath(scaledProtectionPath, paint)
+        if (rimProgress > HIDDEN_RIM_SCALE) {
+            drawFaceScanningRim(canvas)
         }
-    }
-
-    override fun updateVisOnUpdateCutout(): Boolean {
-        return false // instead, we always update the visibility whenever face scanning starts/ends
+        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
+            drawCameraProtection(canvas)
+        }
     }
 
     override fun enableShowProtection(show: Boolean) {
-        val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show
+        val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show
         if (showScanningAnimNow == showScanningAnim) {
             return
         }
@@ -152,91 +126,26 @@
                     if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
                     else if (faceAuthSucceeded) Interpolators.STANDARD
                     else Interpolators.STANDARD_DECELERATE
-            addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                animation: ValueAnimator ->
-                cameraProtectionProgress = animation.animatedValue as Float
-                invalidate()
-            })
+            addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress)
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     cameraProtectionAnimator = null
                     if (!showScanningAnim) {
-                        visibility = View.INVISIBLE
-                        hideOverlayRunnable?.run()
-                        hideOverlayRunnable = null
-                        requestLayout()
+                        hide()
                     }
                 }
             })
         }
 
         rimAnimator?.cancel()
-        rimAnimator = AnimatorSet().apply {
-            if (showScanningAnim) {
-                val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
-                        PULSE_RADIUS_OUT).apply {
-                    duration = PULSE_APPEAR_DURATION
-                    interpolator = Interpolators.STANDARD_DECELERATE
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                }
-
-                // animate in camera protection, rim, and then pulse in/out
-                playSequentially(cameraProtectionAnimator, rimAppearAnimator,
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator())
-            } else {
-                val rimDisappearAnimator = ValueAnimator.ofFloat(
-                        rimProgress,
-                        if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
-                        else SHOW_CAMERA_PROTECTION_SCALE
-                ).apply {
-                    duration =
-                            if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
-                            else PULSE_ERROR_DISAPPEAR_DURATION
-                    interpolator =
-                            if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
-                            else Interpolators.STANDARD
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                    addListener(object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator) {
-                            rimProgress = HIDDEN_RIM_SCALE
-                            invalidate()
-                        }
-                    })
-                }
-                if (faceAuthSucceeded) {
-                    val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
-                        duration = PULSE_SUCCESS_DISAPPEAR_DURATION
-                        interpolator = Interpolators.LINEAR
-                        addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                            animation: ValueAnimator ->
-                            rimPaint.alpha = animation.animatedValue as Int
-                            invalidate()
-                        })
-                        addListener(object : AnimatorListenerAdapter() {
-                            override fun onAnimationEnd(animation: Animator) {
-                                rimPaint.alpha = 255
-                                invalidate()
-                            }
-                        })
-                    }
-                    val rimSuccessAnimator = AnimatorSet()
-                    rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
-                    playTogether(rimSuccessAnimator, cameraProtectionAnimator)
-                } else {
-                    playTogether(rimDisappearAnimator, cameraProtectionAnimator)
-                }
-            }
-
+        rimAnimator = if (showScanningAnim) {
+            createFaceScanningRimAnimator()
+        } else if (faceAuthSucceeded) {
+            createFaceSuccessRimAnimator()
+        } else {
+            createFaceNotSuccessRimAnimator()
+        }
+        rimAnimator?.apply {
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     rimAnimator = null
@@ -245,34 +154,12 @@
                     }
                 }
             })
-            start()
         }
+        rimAnimator?.start()
     }
 
-    fun createPulseAnimator(): AnimatorSet {
-        return AnimatorSet().apply {
-            val pulseInwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
-                duration = PULSE_DURATION_INWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            val pulseOutwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply {
-                duration = PULSE_DURATION_OUTWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            playSequentially(pulseInwards, pulseOutwards)
-        }
+    override fun updateVisOnUpdateCutout(): Boolean {
+        return false // instead, we always update the visibility whenever face scanning starts/ends
     }
 
     override fun updateProtectionBoundingPath() {
@@ -290,17 +177,153 @@
             // Make sure that our measured height encompasses the extra space for the animation
             mTotalBounds.union(mBoundingRect)
             mTotalBounds.union(
-                    rimRect.left.toInt(),
-                    rimRect.top.toInt(),
-                    rimRect.right.toInt(),
-                    rimRect.bottom.toInt())
+                rimRect.left.toInt(),
+                rimRect.top.toInt(),
+                rimRect.right.toInt(),
+                rimRect.bottom.toInt())
             setMeasuredDimension(
-                    resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
         } else {
             setMeasuredDimension(
-                    resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+        }
+    }
+
+    private fun drawFaceScanningRim(canvas: Canvas) {
+        val rimPath = Path(protectionPath)
+        scalePath(rimPath, rimProgress)
+        rimPaint.style = Paint.Style.FILL
+        val rimPaintAlpha = rimPaint.alpha
+        rimPaint.color = ColorUtils.blendARGB(
+            faceScanningAnimColor,
+            Color.WHITE,
+            statusBarStateController.dozeAmount
+        )
+        rimPaint.alpha = rimPaintAlpha
+        canvas.drawPath(rimPath, rimPaint)
+    }
+
+    private fun drawCameraProtection(canvas: Canvas) {
+        val scaledProtectionPath = Path(protectionPath)
+        scalePath(scaledProtectionPath, cameraProtectionProgress)
+        paint.style = Paint.Style.FILL
+        paint.color = cameraProtectionColor
+        canvas.drawPath(scaledProtectionPath, paint)
+    }
+
+    private fun createFaceSuccessRimAnimator(): AnimatorSet {
+        val rimSuccessAnimator = AnimatorSet()
+        rimSuccessAnimator.playTogether(
+            createRimDisappearAnimator(
+                PULSE_RADIUS_SUCCESS,
+                PULSE_SUCCESS_DISAPPEAR_DURATION,
+                Interpolators.STANDARD_DECELERATE
+            ),
+            createSuccessOpacityAnimator(),
+        )
+        return AnimatorSet().apply {
+            playTogether(rimSuccessAnimator, cameraProtectionAnimator)
+        }
+    }
+
+    private fun createFaceNotSuccessRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playTogether(
+                createRimDisappearAnimator(
+                    SHOW_CAMERA_PROTECTION_SCALE,
+                    PULSE_ERROR_DISAPPEAR_DURATION,
+                    Interpolators.STANDARD
+                ),
+                cameraProtectionAnimator,
+            )
+        }
+    }
+
+    private fun createRimDisappearAnimator(
+        endValue: Float,
+        animDuration: Long,
+        timeInterpolator: TimeInterpolator
+    ): ValueAnimator {
+        return ValueAnimator.ofFloat(rimProgress, endValue).apply {
+            duration = animDuration
+            interpolator = timeInterpolator
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimProgress = HIDDEN_RIM_SCALE
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createSuccessOpacityAnimator(): ValueAnimator {
+        return ValueAnimator.ofInt(255, 0).apply {
+            duration = PULSE_SUCCESS_DISAPPEAR_DURATION
+            interpolator = Interpolators.LINEAR
+            addUpdateListener(this@FaceScanningOverlay::updateRimAlpha)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimPaint.alpha = 255
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createFaceScanningRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playSequentially(
+                cameraProtectionAnimator,
+                createRimAppearAnimator(),
+                createPulseAnimator()
+            )
+        }
+    }
+
+    private fun createRimAppearAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+            SHOW_CAMERA_PROTECTION_SCALE,
+            PULSE_RADIUS_OUT
+        ).apply {
+            duration = PULSE_APPEAR_DURATION
+            interpolator = Interpolators.STANDARD_DECELERATE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+        }
+    }
+
+    private fun hide() {
+        visibility = INVISIBLE
+        hideOverlayRunnable?.run()
+        hideOverlayRunnable = null
+        requestLayout()
+    }
+
+    private fun updateRimProgress(animator: ValueAnimator) {
+        rimProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateCameraProtectionProgress(animator: ValueAnimator) {
+        cameraProtectionProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateRimAlpha(animator: ValueAnimator) {
+        rimPaint.alpha = animator.animatedValue as Int
+        invalidate()
+    }
+
+    private fun createPulseAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+                PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
+            duration = HALF_PULSE_DURATION
+            interpolator = Interpolators.STANDARD
+            repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times
+            repeatMode = ValueAnimator.REVERSE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
         }
     }
 
@@ -363,13 +386,24 @@
         private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
         private const val PULSE_APPEAR_DURATION = 250L // without start delay
 
-        private const val PULSE_DURATION_INWARDS = 500L
-        private const val PULSE_DURATION_OUTWARDS = 500L
+        private const val HALF_PULSE_DURATION = 500L
 
         private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
         private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
 
         private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
         private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
+
+        private fun scalePath(path: Path, scalingFactor: Float) {
+            val scaleMatrix = Matrix().apply {
+                val boundingRectangle = RectF()
+                path.computeBounds(boundingRectangle, true)
+                setScale(
+                    scalingFactor, scalingFactor,
+                    boundingRectangle.centerX(), boundingRectangle.centerY()
+                )
+            }
+            path.transform(scaleMatrix)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
new file mode 100644
index 0000000..4c3a7ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
@@ -0,0 +1,7 @@
+package com.android.systemui
+
+import com.android.systemui.dump.nano.SystemUIProtoDump
+
+interface ProtoDumpable : Dumpable {
+    fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index b5f42a1..7e3b1389 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -26,7 +26,6 @@
 import android.annotation.IdRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -170,6 +169,7 @@
     private Display.Mode mDisplayMode;
     @VisibleForTesting
     protected DisplayInfo mDisplayInfo = new DisplayInfo();
+    private DisplayCutout mDisplayCutout;
 
     @VisibleForTesting
     protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
@@ -384,6 +384,7 @@
         mRotation = mDisplayInfo.rotation;
         mDisplayMode = mDisplayInfo.getMode();
         mDisplayUniqueId = mDisplayInfo.uniqueId;
+        mDisplayCutout = mDisplayInfo.displayCutout;
         mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(),
                 mDisplayUniqueId);
         mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
@@ -456,7 +457,6 @@
                     }
                 }
 
-                boolean needToUpdateProviderViews = false;
                 final String newUniqueId = mDisplayInfo.uniqueId;
                 if (!Objects.equals(newUniqueId, mDisplayUniqueId)) {
                     mDisplayUniqueId = newUniqueId;
@@ -474,37 +474,6 @@
                         setupDecorations();
                         return;
                     }
-
-                    if (mScreenDecorHwcLayer != null) {
-                        updateHwLayerRoundedCornerDrawable();
-                        updateHwLayerRoundedCornerExistAndSize();
-                    }
-                    needToUpdateProviderViews = true;
-                }
-
-                final float newRatio = getPhysicalPixelDisplaySizeRatio();
-                if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) {
-                    mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio);
-                    if (mScreenDecorHwcLayer != null) {
-                        updateHwLayerRoundedCornerExistAndSize();
-                    }
-                    needToUpdateProviderViews = true;
-                }
-
-                if (needToUpdateProviderViews) {
-                    updateOverlayProviderViews(null);
-                } else {
-                    updateOverlayProviderViews(new Integer[] {
-                            mFaceScanningViewId,
-                            R.id.display_cutout,
-                            R.id.display_cutout_left,
-                            R.id.display_cutout_right,
-                            R.id.display_cutout_bottom,
-                    });
-                }
-
-                if (mScreenDecorHwcLayer != null) {
-                    mScreenDecorHwcLayer.onDisplayChanged(newUniqueId);
                 }
             }
         };
@@ -931,7 +900,7 @@
     private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            int newUserId = ActivityManager.getCurrentUser();
+            int newUserId = mUserTracker.getUserId();
             if (DEBUG) {
                 Log.d(TAG, "UserSwitched newUserId=" + newUserId);
             }
@@ -1054,7 +1023,8 @@
         mRoundedCornerResDelegate.dump(pw, args);
     }
 
-    private void updateConfiguration() {
+    @VisibleForTesting
+    void updateConfiguration() {
         Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
                 "must call on " + mHandler.getLooper().getThread()
                         + ", but was " + Thread.currentThread());
@@ -1065,14 +1035,19 @@
             mDotViewController.setNewRotation(newRotation);
         }
         final Display.Mode newMod = mDisplayInfo.getMode();
+        final DisplayCutout newCutout = mDisplayInfo.displayCutout;
 
         if (!mPendingConfigChange
-                && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) {
+                && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod)
+                || !Objects.equals(newCutout, mDisplayCutout))) {
             mRotation = newRotation;
             mDisplayMode = newMod;
+            mDisplayCutout = newCutout;
+            mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
+                    getPhysicalPixelDisplaySizeRatio());
             if (mScreenDecorHwcLayer != null) {
                 mScreenDecorHwcLayer.pendingConfigChange = false;
-                mScreenDecorHwcLayer.updateRotation(mRotation);
+                mScreenDecorHwcLayer.updateConfiguration(mDisplayUniqueId);
                 updateHwLayerRoundedCornerExistAndSize();
                 updateHwLayerRoundedCornerDrawable();
             }
@@ -1111,7 +1086,8 @@
                 context.getResources(), context.getDisplay().getUniqueId());
     }
 
-    private void updateOverlayProviderViews(@Nullable Integer[] filterIds) {
+    @VisibleForTesting
+    void updateOverlayProviderViews(@Nullable Integer[] filterIds) {
         if (mOverlays == null) {
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index d9f44cd..0e7deeb 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -43,6 +43,7 @@
 import com.android.systemui.dagger.GlobalRootComponent;
 import com.android.systemui.dagger.SysUIComponent;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.NotificationChannels;
 
 import java.util.Comparator;
@@ -137,7 +138,7 @@
                     if (mServicesStarted) {
                         final int N = mServices.length;
                         for (int i = 0; i < N; i++) {
-                            mServices[i].onBootCompleted();
+                            notifyBootCompleted(mServices[i]);
                         }
                     }
                 }
@@ -256,7 +257,7 @@
 
         for (i = 0; i < mServices.length; i++) {
             if (mBootCompleteCache.isBootComplete()) {
-                mServices[i].onBootCompleted();
+                notifyBootCompleted(mServices[i]);
             }
 
             mDumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]);
@@ -267,7 +268,17 @@
         mServicesStarted = true;
     }
 
-    private void timeInitialization(String clsName, Runnable init, TimingsTraceLog log,
+    private static void notifyBootCompleted(CoreStartable coreStartable) {
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP,
+                    coreStartable.getClass().getSimpleName() + ".onBootCompleted()");
+        }
+        coreStartable.onBootCompleted();
+        Trace.endSection();
+    }
+
+    private static void timeInitialization(String clsName, Runnable init, TimingsTraceLog log,
             String metricsPrefix) {
         long ti = System.currentTimeMillis();
         log.traceBegin(metricsPrefix + " " + clsName);
@@ -281,28 +292,45 @@
         }
     }
 
-    private CoreStartable startAdditionalStartable(String clsName) {
+    private static CoreStartable startAdditionalStartable(String clsName) {
         CoreStartable startable;
         if (DEBUG) Log.d(TAG, "loading: " + clsName);
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP, clsName + ".newInstance()");
+        }
         try {
             startable = (CoreStartable) Class.forName(clsName).newInstance();
         } catch (ClassNotFoundException
                 | IllegalAccessException
                 | InstantiationException ex) {
             throw new RuntimeException(ex);
+        } finally {
+            Trace.endSection();
         }
 
         return startStartable(startable);
     }
 
-    private CoreStartable startStartable(String clsName, Provider<CoreStartable> provider) {
+    private static CoreStartable startStartable(String clsName, Provider<CoreStartable> provider) {
         if (DEBUG) Log.d(TAG, "loading: " + clsName);
-        return startStartable(provider.get());
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP, "Provider<" + clsName + ">.get()");
+        }
+        CoreStartable startable = provider.get();
+        Trace.endSection();
+        return startStartable(startable);
     }
 
-    private CoreStartable startStartable(CoreStartable startable) {
+    private static CoreStartable startStartable(CoreStartable startable) {
         if (DEBUG) Log.d(TAG, "running: " + startable);
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP, startable.getClass().getSimpleName() + ".start()");
+        }
         startable.start();
+        Trace.endSection();
 
         return startable;
     }
@@ -340,11 +368,25 @@
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         if (mServicesStarted) {
-            mSysUIComponent.getConfigurationController().onConfigurationChanged(newConfig);
+            ConfigurationController configController = mSysUIComponent.getConfigurationController();
+            if (Trace.isEnabled()) {
+                Trace.traceBegin(
+                        Trace.TRACE_TAG_APP,
+                        configController.getClass().getSimpleName() + ".onConfigurationChanged()");
+            }
+            configController.onConfigurationChanged(newConfig);
+            Trace.endSection();
             int len = mServices.length;
             for (int i = 0; i < len; i++) {
                 if (mServices[i] != null) {
+                    if (Trace.isEnabled()) {
+                        Trace.traceBegin(
+                                Trace.TRACE_TAG_APP,
+                                mServices[i].getClass().getSimpleName()
+                                        + ".onConfigurationChanged()");
+                    }
                     mServices[i].onConfigurationChanged(newConfig);
+                    Trace.endSection();
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index a21f45f..632fcdc 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -97,7 +97,6 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
                     .setBackAnimation(mWMComponent.getBackAnimation())
-                    .setFloatingTasks(mWMComponent.getFloatingTasks())
                     .setDesktopMode(mWMComponent.getDesktopMode());
 
             // Only initialize when not starting from tests since this currently initializes some
@@ -118,7 +117,6 @@
                     .setStartingSurface(Optional.ofNullable(null))
                     .setRecentTasks(Optional.ofNullable(null))
                     .setBackAnimation(Optional.ofNullable(null))
-                    .setFloatingTasks(Optional.ofNullable(null))
                     .setDesktopMode(Optional.ofNullable(null));
         }
         mSysUIComponent = builder.build();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
index 8920c92..8aa3040 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
@@ -17,7 +17,7 @@
 package com.android.systemui
 
 import android.content.Context
-import com.android.systemui.dagger.DaggerGlobalRootComponent
+import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent
 import com.android.systemui.dagger.GlobalRootComponent
 
 /**
@@ -25,6 +25,6 @@
  */
 class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) {
     override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder {
-        return DaggerGlobalRootComponent.builder()
+        return DaggerReferenceGlobalRootComponent.builder()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 7bcba3c..50e0399 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -121,6 +121,6 @@
                     DumpHandler.PRIORITY_ARG_CRITICAL};
         }
 
-        mDumpHandler.dump(pw, massagedArgs);
+        mDumpHandler.dump(fd, pw, massagedArgs);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 9f1c9b4..d60cc75 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -291,8 +291,11 @@
         mA11yManager.registerSystemAction(actionBack, SYSTEM_ACTION_ID_BACK);
         mA11yManager.registerSystemAction(actionHome, SYSTEM_ACTION_ID_HOME);
         mA11yManager.registerSystemAction(actionRecents, SYSTEM_ACTION_ID_RECENTS);
-        mA11yManager.registerSystemAction(actionNotifications, SYSTEM_ACTION_ID_NOTIFICATIONS);
-        mA11yManager.registerSystemAction(actionQuickSettings, SYSTEM_ACTION_ID_QUICK_SETTINGS);
+        if (mCentralSurfacesOptionalLazy.get().isPresent()) {
+            // These two actions require the CentralSurfaces instance.
+            mA11yManager.registerSystemAction(actionNotifications, SYSTEM_ACTION_ID_NOTIFICATIONS);
+            mA11yManager.registerSystemAction(actionQuickSettings, SYSTEM_ACTION_ID_QUICK_SETTINGS);
+        }
         mA11yManager.registerSystemAction(actionPowerDialog, SYSTEM_ACTION_ID_POWER_DIALOG);
         mA11yManager.registerSystemAction(actionLockScreen, SYSTEM_ACTION_ID_LOCK_SCREEN);
         mA11yManager.registerSystemAction(actionTakeScreenshot, SYSTEM_ACTION_ID_TAKE_SCREENSHOT);
@@ -597,6 +600,7 @@
                 case INTENT_ACTION_DPAD_CENTER: {
                     Intent intent = new Intent(intentAction);
                     intent.setPackage(context.getPackageName());
+                    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
                     return PendingIntent.getBroadcast(context, 0, intent,
                             PendingIntent.FLAG_IMMUTABLE);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index a6e767c..ec15d1a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -158,6 +158,10 @@
     private View mTopDrag;
     private View mRightDrag;
     private View mBottomDrag;
+    private ImageView mTopLeftCornerView;
+    private ImageView mTopRightCornerView;
+    private ImageView mBottomLeftCornerView;
+    private ImageView mBottomRightCornerView;
     private final Configuration mConfiguration;
 
     @NonNull
@@ -357,13 +361,15 @@
         return false;
     }
 
-    private void changeMagnificationSize(@MagnificationSize int index) {
+    @VisibleForTesting
+    void changeMagnificationSize(@MagnificationSize int index) {
         final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3;
         int size = (int) (initSize * MAGNIFICATION_SCALE_OPTIONS[index]);
         setWindowSize(size, size);
     }
 
-    private void setEditMagnifierSizeMode(boolean enable) {
+    @VisibleForTesting
+    void setEditMagnifierSizeMode(boolean enable) {
         mEditSizeEnable = enable;
         applyResourcesValues();
 
@@ -639,10 +645,37 @@
         Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
                 mMirrorView.getWidth() - mBorderDragSize,
                 mMirrorView.getHeight() - mBorderDragSize);
+
+        Region tapExcludeRegion = new Region();
+
         Rect dragArea = new Rect();
         mDragView.getHitRect(dragArea);
 
-        regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE);
+        Rect topLeftArea = new Rect();
+        mTopLeftCornerView.getHitRect(topLeftArea);
+
+        Rect topRightArea = new Rect();
+        mTopRightCornerView.getHitRect(topRightArea);
+
+        Rect bottomLeftArea = new Rect();
+        mBottomLeftCornerView.getHitRect(bottomLeftArea);
+
+        Rect bottomRightArea = new Rect();
+        mBottomRightCornerView.getHitRect(bottomRightArea);
+
+        Rect closeArea = new Rect();
+        mCloseView.getHitRect(closeArea);
+
+        // add tapExcludeRegion for Drag or close
+        tapExcludeRegion.op(dragArea, Region.Op.UNION);
+        tapExcludeRegion.op(topLeftArea, Region.Op.UNION);
+        tapExcludeRegion.op(topRightArea, Region.Op.UNION);
+        tapExcludeRegion.op(bottomLeftArea, Region.Op.UNION);
+        tapExcludeRegion.op(bottomRightArea, Region.Op.UNION);
+        tapExcludeRegion.op(closeArea, Region.Op.UNION);
+
+        regionInsideDragBorder.op(tapExcludeRegion, Region.Op.DIFFERENCE);
+
         return regionInsideDragBorder;
     }
 
@@ -756,6 +789,10 @@
         mRightDrag = mMirrorView.findViewById(R.id.right_handle);
         mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle);
         mCloseView = mMirrorView.findViewById(R.id.close_button);
+        mTopRightCornerView = mMirrorView.findViewById(R.id.top_right_corner);
+        mTopLeftCornerView = mMirrorView.findViewById(R.id.top_left_corner);
+        mBottomRightCornerView = mMirrorView.findViewById(R.id.bottom_right_corner);
+        mBottomLeftCornerView = mMirrorView.findViewById(R.id.bottom_left_corner);
 
         mDragView.setOnTouchListener(this);
         mLeftDrag.setOnTouchListener(this);
@@ -763,6 +800,10 @@
         mRightDrag.setOnTouchListener(this);
         mBottomDrag.setOnTouchListener(this);
         mCloseView.setOnTouchListener(this);
+        mTopLeftCornerView.setOnTouchListener(this);
+        mTopRightCornerView.setOnTouchListener(this);
+        mBottomLeftCornerView.setOnTouchListener(this);
+        mBottomRightCornerView.setOnTouchListener(this);
     }
 
     /**
@@ -831,8 +872,16 @@
 
     @Override
     public boolean onTouch(View v, MotionEvent event) {
-        if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag
-                || v == mBottomDrag || v == mCloseView) {
+        if (v == mDragView
+                || v == mLeftDrag
+                || v == mTopDrag
+                || v == mRightDrag
+                || v == mBottomDrag
+                || v == mTopLeftCornerView
+                || v == mTopRightCornerView
+                || v == mBottomLeftCornerView
+                || v == mBottomRightCornerView
+                || v == mCloseView) {
             return mGestureDetector.onTouch(v, event);
         }
         return false;
@@ -1195,7 +1244,7 @@
     @Override
     public boolean onDrag(View view, float offsetX, float offsetY) {
         if (mEditSizeEnable) {
-            changeWindowSize(view, offsetX, offsetY);
+            return changeWindowSize(view, offsetX, offsetY);
         } else {
             move((int) offsetX, (int) offsetY);
         }
@@ -1220,13 +1269,47 @@
         if (mEditSizeEnable) {
             mDragView.setVisibility(View.GONE);
             mCloseView.setVisibility(View.VISIBLE);
+            mTopRightCornerView.setVisibility(View.VISIBLE);
+            mTopLeftCornerView.setVisibility(View.VISIBLE);
+            mBottomRightCornerView.setVisibility(View.VISIBLE);
+            mBottomLeftCornerView.setVisibility(View.VISIBLE);
         } else {
             mDragView.setVisibility(View.VISIBLE);
             mCloseView.setVisibility(View.GONE);
+            mTopRightCornerView.setVisibility(View.GONE);
+            mTopLeftCornerView.setVisibility(View.GONE);
+            mBottomRightCornerView.setVisibility(View.GONE);
+            mBottomLeftCornerView.setVisibility(View.GONE);
         }
     }
 
-    public boolean changeWindowSize(View view, float offsetX, float offsetY) {
+    private boolean changeWindowSize(View view, float offsetX, float offsetY) {
+        if (view == mLeftDrag) {
+            changeMagnificationFrameSize(offsetX, 0, 0, 0);
+        } else if (view == mRightDrag) {
+            changeMagnificationFrameSize(0, 0, offsetX, 0);
+        } else if (view == mTopDrag) {
+            changeMagnificationFrameSize(0, offsetY, 0, 0);
+        } else if (view == mBottomDrag) {
+            changeMagnificationFrameSize(0, 0, 0, offsetY);
+        } else if (view == mTopLeftCornerView) {
+            changeMagnificationFrameSize(offsetX, offsetY, 0, 0);
+        } else if (view == mTopRightCornerView) {
+            changeMagnificationFrameSize(0, offsetY, offsetX, 0);
+        } else if (view == mBottomLeftCornerView) {
+            changeMagnificationFrameSize(offsetX, 0, 0, offsetY);
+        } else if (view == mBottomRightCornerView) {
+            changeMagnificationFrameSize(0, 0, offsetX, offsetY);
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void changeMagnificationFrameSize(
+            float leftOffset, float topOffset, float rightOffset,
+            float bottomOffset) {
         boolean bRTL = isRTL(mContext);
         final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3;
 
@@ -1236,54 +1319,26 @@
         Rect tempRect = new Rect();
         tempRect.set(mMagnificationFrame);
 
-        if (view == mLeftDrag) {
-            if (bRTL) {
-                tempRect.right += offsetX;
-                if (tempRect.right > mWindowBounds.width()) {
-                    return false;
-                }
-            } else {
-                tempRect.left += offsetX;
-                if (tempRect.left < 0) {
-                    return false;
-                }
-            }
-        } else if (view == mRightDrag) {
-            if (bRTL) {
-                tempRect.left += offsetX;
-                if (tempRect.left < 0) {
-                    return false;
-                }
-            } else {
-                tempRect.right += offsetX;
-                if (tempRect.right > mWindowBounds.width()) {
-                    return false;
-                }
-            }
-        } else if (view == mTopDrag) {
-            tempRect.top += offsetY;
-            if (tempRect.top < 0) {
-                return false;
-            }
-        } else if (view == mBottomDrag) {
-            tempRect.bottom += offsetY;
-            if (tempRect.bottom > mWindowBounds.height()) {
-                return false;
-            }
+        if (bRTL) {
+            tempRect.left += (int) (rightOffset);
+            tempRect.right += (int) (leftOffset);
+        } else {
+            tempRect.right += (int) (rightOffset);
+            tempRect.left += (int) (leftOffset);
         }
+        tempRect.top += (int) (topOffset);
+        tempRect.bottom += (int) (bottomOffset);
 
         if (tempRect.width() < initSize || tempRect.height() < initSize
                 || tempRect.width() > maxWidthSize || tempRect.height() > maxHeightSize) {
-            return false;
+            return;
         }
-
         mMagnificationFrame.set(tempRect);
 
         computeBounceAnimationScale();
         calculateMagnificationFrameBoundary();
 
         modifyWindowMagnification(true);
-        return true;
     }
 
     private static boolean isRTL(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 9cffd5d..069c0f6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -103,7 +103,7 @@
             MagnificationSize.LARGE,
     })
     /** Denotes the Magnification size type. */
-    @interface MagnificationSize {
+    public @interface MagnificationSize {
         int NONE = 0;
         int SMALL  = 1;
         int MEDIUM = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
index 9af8300..de351ec 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java
@@ -57,7 +57,7 @@
     @FloatRange(from = 0.0, to = 1.0)
     private static final float DEFAULT_POSITION_X_PERCENT = 1.0f;
     @FloatRange(from = 0.0, to = 1.0)
-    private static final float DEFAULT_POSITION_Y_PERCENT = 0.9f;
+    private static final float DEFAULT_POSITION_Y_PERCENT = 0.77f;
 
     private final Context mContext;
     private final AccessibilityFloatingMenuView mMenuView;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index ea334b2..777d10c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -28,6 +28,7 @@
 import android.text.TextUtils;
 import android.view.Display;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.MainThread;
 
@@ -56,6 +57,7 @@
     private Context mContext;
     private final WindowManager mWindowManager;
     private final DisplayManager mDisplayManager;
+    private final AccessibilityManager mAccessibilityManager;
     private final FeatureFlags mFeatureFlags;
     @VisibleForTesting
     IAccessibilityFloatingMenu mFloatingMenu;
@@ -96,6 +98,7 @@
     public AccessibilityFloatingMenuController(Context context,
             WindowManager windowManager,
             DisplayManager displayManager,
+            AccessibilityManager accessibilityManager,
             AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
             AccessibilityButtonModeObserver accessibilityButtonModeObserver,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -103,6 +106,7 @@
         mContext = context;
         mWindowManager = windowManager;
         mDisplayManager = displayManager;
+        mAccessibilityManager = accessibilityManager;
         mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
         mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -180,7 +184,8 @@
                 final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
                 mFloatingMenu = new MenuViewLayerController(
                         mContext.createWindowContext(defaultDisplay,
-                                TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager);
+                                TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager,
+                        mAccessibilityManager);
             } else {
                 mFloatingMenu = new AccessibilityFloatingMenu(mContext);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
new file mode 100644
index 0000000..ee048e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
@@ -0,0 +1,179 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.ComponentCallbacks;
+import android.content.res.Configuration;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.systemui.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls the interaction between {@link MagnetizedObject} and
+ * {@link MagnetizedObject.MagneticTarget}.
+ */
+class DismissAnimationController implements ComponentCallbacks {
+    private static final float COMPLETELY_OPAQUE = 1.0f;
+    private static final float COMPLETELY_TRANSPARENT = 0.0f;
+    private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f;
+    private static final float ANIMATING_MAX_ALPHA = 0.7f;
+
+    private final DismissView mDismissView;
+    private final MenuView mMenuView;
+    private final ValueAnimator mDismissAnimator;
+    private final MagnetizedObject<?> mMagnetizedObject;
+    private float mMinDismissSize;
+    private float mSizePercent;
+
+    DismissAnimationController(DismissView dismissView, MenuView menuView) {
+        mDismissView = dismissView;
+        mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
+        mDismissView.setPivotY(dismissView.getHeight() / 2.0f);
+        mMenuView = menuView;
+
+        updateResources();
+
+        mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT);
+        mDismissAnimator.addUpdateListener(dismissAnimation -> {
+            final float animatedValue = (float) dismissAnimation.getAnimatedValue();
+            final float scaleValue = Math.max(animatedValue, mSizePercent);
+            dismissView.getCircle().setScaleX(scaleValue);
+            dismissView.getCircle().setScaleY(scaleValue);
+
+            menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA));
+        });
+
+        mDismissAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
+                super.onAnimationEnd(animation, isReverse);
+
+                if (isReverse) {
+                    mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE);
+                    mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE);
+                    mMenuView.setAlpha(COMPLETELY_OPAQUE);
+                }
+            }
+        });
+
+        mMagnetizedObject =
+                new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView,
+                        new MenuAnimationController.MenuPositionProperty(
+                                DynamicAnimation.TRANSLATION_X),
+                        new MenuAnimationController.MenuPositionProperty(
+                                DynamicAnimation.TRANSLATION_Y)) {
+                    @Override
+                    public void getLocationOnScreen(MenuView underlyingObject, int[] loc) {
+                        underlyingObject.getLocationOnScreen(loc);
+                    }
+
+                    @Override
+                    public float getHeight(MenuView underlyingObject) {
+                        return underlyingObject.getHeight();
+                    }
+
+                    @Override
+                    public float getWidth(MenuView underlyingObject) {
+                        return underlyingObject.getWidth();
+                    }
+                };
+
+        final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget(
+                dismissView.getCircle(), (int) mMinDismissSize);
+        mMagnetizedObject.addTarget(magneticTarget);
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        updateResources();
+    }
+
+    @Override
+    public void onLowMemory() {
+        // Do nothing
+    }
+
+    void showDismissView(boolean show) {
+        if (show) {
+            mDismissView.show();
+        } else {
+            mDismissView.hide();
+        }
+    }
+
+    void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) {
+        mMagnetizedObject.setMagnetListener(magnetListener);
+    }
+
+    void maybeConsumeDownMotionEvent(MotionEvent event) {
+        mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    /**
+     * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was
+     * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
+     *
+     * @param event that move the magnetized object which is also the menu list view.
+     * @return true if the location of the motion events moves within the magnetic field of a
+     * target, but false if didn't set
+     * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+     */
+    boolean maybeConsumeMoveMotionEvent(MotionEvent event) {
+        return mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    /**
+     * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was
+     * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
+     *
+     * @param event that move the magnetized object which is also the menu list view.
+     * @return true if the location of the motion events moves within the magnetic field of a
+     * target, but false if didn't set
+     * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+     */
+    boolean maybeConsumeUpMotionEvent(MotionEvent event) {
+        return mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
+    void animateDismissMenu(boolean scaleUp) {
+        if (scaleUp) {
+            mDismissAnimator.start();
+        } else {
+            mDismissAnimator.reverse();
+        }
+    }
+
+    private void updateResources() {
+        final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize(
+                R.dimen.dismiss_circle_size);
+        mMinDismissSize = mDismissView.getResources().getDimensionPixelSize(
+                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
new file mode 100644
index 0000000..396f584
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -0,0 +1,418 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static android.util.MathUtils.constrain;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FlingAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.HashMap;
+
+/**
+ * Controls the interaction animations of the {@link MenuView}. Also, it will use the relative
+ * coordinate based on the {@link MenuViewLayer} to compute the offset of the {@link MenuView}.
+ */
+class MenuAnimationController {
+    private static final String TAG = "MenuAnimationController";
+    private static final boolean DEBUG = false;
+    private static final float MIN_PERCENT = 0.0f;
+    private static final float MAX_PERCENT = 1.0f;
+    private static final float COMPLETELY_OPAQUE = 1.0f;
+    private static final float COMPLETELY_TRANSPARENT = 0.0f;
+    private static final float SCALE_SHRINK = 0.0f;
+    private static final float SCALE_GROW = 1.0f;
+    private static final float FLING_FRICTION_SCALAR = 1.9f;
+    private static final float DEFAULT_FRICTION = 4.2f;
+    private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
+    private static final float SPRING_STIFFNESS = 700f;
+    private static final float ESCAPE_VELOCITY = 750f;
+
+    private static final int FADE_OUT_DURATION_MS = 1000;
+    private static final int FADE_EFFECT_DURATION_MS = 3000;
+
+    private final MenuView mMenuView;
+    private final ValueAnimator mFadeOutAnimator;
+    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
+    private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
+            new HashMap<>();
+
+    MenuAnimationController(MenuView menuView) {
+        mMenuView = menuView;
+
+        mHandler = createUiHandler();
+        mFadeOutAnimator = new ValueAnimator();
+        mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
+        mFadeOutAnimator.addUpdateListener(
+                (animation) -> menuView.setAlpha((float) animation.getAnimatedValue()));
+    }
+
+    void moveToPosition(PointF position) {
+        moveToPositionX(position.x);
+        moveToPositionY(position.y);
+    }
+
+    void moveToPositionX(float positionX) {
+        DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+    }
+
+    private void moveToPositionY(float positionY) {
+        DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+    }
+
+    void moveToPositionYIfNeeded(float positionY) {
+        // If the list view was out of screen bounds, it would allow users to nest scroll inside
+        // and avoid conflicting with outer scroll.
+        final RecyclerView listView = (RecyclerView) mMenuView.getChildAt(/* index= */ 0);
+        if (listView.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
+            moveToPositionY(positionY);
+        }
+    }
+
+    void setDismissCallback(
+            DismissAnimationController.DismissCallback dismissCallback) {
+        mDismissCallback = dismissCallback;
+    }
+
+    void moveToTopLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top));
+    }
+
+    void moveToTopRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top));
+    }
+
+    void moveToBottomLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom));
+    }
+
+    void moveToBottomRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom));
+    }
+
+    void moveAndPersistPosition(PointF position) {
+        moveToPosition(position);
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        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
+                : velocityX < -ESCAPE_VELOCITY;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float finalPositionX = shouldMenuFlingLeft
+                ? draggableBounds.left : draggableBounds.right;
+
+        final float minimumVelocityToReachEdge =
+                (finalPositionX - x) * (FLING_FRICTION_SCALAR * DEFAULT_FRICTION);
+
+        final float startXVelocity = shouldMenuFlingLeft
+                ? Math.min(minimumVelocityToReachEdge, velocityX)
+                : Math.max(minimumVelocityToReachEdge, velocityX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
+                startXVelocity,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                finalPositionX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
+                velocityY,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                /* finalPosition= */ null);
+    }
+
+    private void flingThenSpringMenuWith(DynamicAnimation.ViewProperty property, float velocity,
+            float friction, SpringForce spring, Float finalPosition) {
+
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final float currentValue = menuPositionProperty.getValue(mMenuView);
+        final Rect bounds = mMenuView.getMenuDraggableBounds();
+        final float min =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.left
+                        : bounds.top;
+        final float max =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.right
+                        : bounds.bottom;
+
+        final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty);
+        flingAnimation.setFriction(friction)
+                .setStartVelocity(velocity)
+                .setMinValue(Math.min(currentValue, min))
+                .setMaxValue(Math.max(currentValue, max))
+                .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                    if (canceled) {
+                        if (DEBUG) {
+                            Log.d(TAG, "The fling animation was canceled.");
+                        }
+
+                        return;
+                    }
+
+                    final float endPosition = finalPosition != null
+                            ? finalPosition
+                            : Math.max(min, Math.min(max, endValue));
+                    springMenuWith(property, spring, endVelocity, endPosition);
+                });
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, flingAnimation);
+        flingAnimation.start();
+    }
+
+    private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
+            float velocity, float finalPosition) {
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final SpringAnimation springAnimation =
+                new SpringAnimation(mMenuView, menuPositionProperty)
+                        .setSpring(spring)
+                        .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                            if (canceled || endValue != finalPosition) {
+                                return;
+                            }
+
+                            onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(),
+                                    mMenuView.getTranslationY()));
+                        })
+                        .setStartVelocity(velocity);
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, springAnimation);
+        springAnimation.animateToFinalPosition(finalPosition);
+    }
+
+    /**
+     * Determines whether to hide the menu to the edge of the screen with the given current
+     * translation x of the menu view. It should be used when receiving the action up touch event.
+     *
+     * @param currentXTranslation the current translation x of the menu view.
+     * @return true if the menu would be hidden to the edge, otherwise false.
+     */
+    boolean maybeMoveToEdgeAndHide(float currentXTranslation) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+
+        // If the translation x is zero, it should be at the left of the bound.
+        if (currentXTranslation < draggableBounds.left
+                || currentXTranslation > draggableBounds.right) {
+            moveToEdgeAndHide();
+            return true;
+        }
+
+        fadeOutIfEnabled();
+        return false;
+    }
+
+    private boolean isOnLeftSide() {
+        return mMenuView.getTranslationX() < mMenuView.getMenuDraggableBounds().centerX();
+    }
+
+    boolean isMovedToEdge() {
+        return mIsMovedToEdge;
+    }
+
+    void moveToEdgeAndHide() {
+        mIsMovedToEdge = true;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float endY = constrain(mMenuView.getTranslationY(), draggableBounds.top,
+                draggableBounds.bottom);
+        final float menuHalfWidth = mMenuView.getWidth() / 2.0f;
+        final float endX = isOnLeftSide()
+                ? draggableBounds.left - menuHalfWidth
+                : draggableBounds.right + menuHalfWidth;
+        moveAndPersistPosition(new PointF(endX, endY));
+
+        // Keep the touch region let users could click extra space to pop up the menu view
+        // from the screen edge
+        mMenuView.onBoundsInParentChanged(isOnLeftSide()
+                ? draggableBounds.left
+                : draggableBounds.right, (int) mMenuView.getTranslationY());
+
+        fadeOutIfEnabled();
+    }
+
+    void moveOutEdgeAndShow() {
+        mIsMovedToEdge = false;
+
+        mMenuView.onPositionChanged();
+        mMenuView.onEdgeChangedIfNeeded();
+    }
+
+    void cancelAnimations() {
+        cancelAnimation(DynamicAnimation.TRANSLATION_X);
+        cancelAnimation(DynamicAnimation.TRANSLATION_Y);
+    }
+
+    private void cancelAnimation(DynamicAnimation.ViewProperty property) {
+        if (!mPositionAnimations.containsKey(property)) {
+            return;
+        }
+
+        mPositionAnimations.get(property).cancel();
+    }
+
+    void onDraggingStart() {
+        mMenuView.onDraggingStart();
+    }
+
+    void startShrinkAnimation(Runnable endAction) {
+        mMenuView.animate().cancel();
+
+        mMenuView.animate()
+                .scaleX(SCALE_SHRINK)
+                .scaleY(SCALE_SHRINK)
+                .alpha(COMPLETELY_TRANSPARENT)
+                .translationY(mMenuView.getTranslationY())
+                .withEndAction(endAction).start();
+    }
+
+    void startGrowAnimation() {
+        mMenuView.animate().cancel();
+
+        mMenuView.animate()
+                .scaleX(SCALE_GROW)
+                .scaleY(SCALE_GROW)
+                .alpha(COMPLETELY_OPAQUE)
+                .translationY(mMenuView.getTranslationY())
+                .start();
+    }
+
+    private void onSpringAnimationEnd(PointF position) {
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        constrainPositionAndUpdate(position);
+
+        fadeOutIfEnabled();
+    }
+
+    private void constrainPositionAndUpdate(PointF position) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        // Have the space gap margin between the top bound and the menu view, so actually the
+        // position y range needs to cut the margin.
+        position.offset(-draggableBounds.left, -draggableBounds.top);
+
+        final float percentageX = position.x < draggableBounds.centerX()
+                ? MIN_PERCENT : MAX_PERCENT;
+
+        final float percentageY = position.y < 0 || draggableBounds.height() == 0
+                ? MIN_PERCENT
+                : Math.min(MAX_PERCENT, position.y / draggableBounds.height());
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+    }
+
+    void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
+        mIsFadeEffectEnabled = isFadeEffectEnabled;
+
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        mFadeOutAnimator.cancel();
+        mFadeOutAnimator.setFloatValues(COMPLETELY_OPAQUE, newOpacityValue);
+        mHandler.post(() -> mMenuView.setAlpha(
+                mIsFadeEffectEnabled ? newOpacityValue : COMPLETELY_OPAQUE));
+    }
+
+    void fadeInNowIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.post(() -> mMenuView.setAlpha(COMPLETELY_OPAQUE));
+    }
+
+    void fadeOutIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.postDelayed(mFadeOutAnimator::start, FADE_EFFECT_DURATION_MS);
+    }
+
+    private void cancelAndRemoveCallbacksAndMessages() {
+        mFadeOutAnimator.cancel();
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+    }
+
+    private Handler createUiHandler() {
+        return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
+    }
+
+    static class MenuPositionProperty
+            extends FloatPropertyCompat<MenuView> {
+        private final DynamicAnimation.ViewProperty mProperty;
+
+        MenuPositionProperty(DynamicAnimation.ViewProperty property) {
+            super(property.toString());
+            mProperty = property;
+        }
+
+        @Override
+        public float getValue(MenuView menuView) {
+            return mProperty.getValue(menuView);
+        }
+
+        @Override
+        public void setValue(MenuView menuView, float value) {
+            mProperty.setValue(menuView, value);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
new file mode 100644
index 0000000..83c344c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.accessibility.floatingmenu
+
+import android.annotation.FloatRange
+
+@FloatRange(from = 0.0, to = 1.0) const val DEFAULT_OPACITY_VALUE = 0.55f
+const val DEFAULT_FADE_EFFECT_IS_ENABLED = 1
+
+/** The data class for the fade effect info of the accessibility floating menu view. */
+data class MenuFadeEffectInfo(val isFadeEffectEnabled: Boolean, val opacity: Float)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 698d60a..4c52b33 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -16,22 +16,29 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
 import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_OPACITY_VALUE;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
+import android.annotation.FloatRange;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Prefs;
 
 import java.util.List;
 
@@ -39,9 +46,16 @@
  * Stores and observe the settings contents for the menu view.
  */
 class MenuInfoRepository {
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f;
+
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.77f;
+
     private final Context mContext;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final OnSettingsContentsChanged mSettingsContentsCallback;
+    private Position mPercentagePosition;
 
     private final ContentObserver mMenuTargetFeaturesContentObserver =
             new ContentObserver(mHandler) {
@@ -62,9 +76,24 @@
                 }
             };
 
+    @VisibleForTesting
+    final ContentObserver mMenuFadeOutContentObserver =
+            new ContentObserver(mHandler) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mSettingsContentsCallback.onFadeEffectInfoChanged(getMenuFadeEffectInfo());
+                }
+            };
+
     MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) {
         mContext = context;
         mSettingsContentsCallback = settingsContentsChanged;
+
+        mPercentagePosition = getStartPosition();
+    }
+
+    void loadMenuPosition(OnInfoReady<Position> callback) {
+        callback.onReady(mPercentagePosition);
     }
 
     void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) {
@@ -75,6 +104,30 @@
         callback.onReady(getMenuSizeTypeFromSettings(mContext));
     }
 
+    void loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback) {
+        callback.onReady(getMenuFadeEffectInfo());
+    }
+
+    private MenuFadeEffectInfo getMenuFadeEffectInfo() {
+        return new MenuFadeEffectInfo(isMenuFadeEffectEnabledFromSettings(mContext),
+                getMenuOpacityFromSettings(mContext));
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mPercentagePosition = percentagePosition;
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+                percentagePosition.toString());
+    }
+
+    private Position getStartPosition() {
+        final String absolutePositionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+
+        return TextUtils.isEmpty(absolutePositionString)
+                ? new Position(DEFAULT_MENU_POSITION_X_PERCENT, DEFAULT_MENU_POSITION_Y_PERCENT)
+                : Position.fromString(absolutePositionString);
+    }
+
     void registerContentObservers() {
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
@@ -88,17 +141,28 @@
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
                 /* notifyForDescendants */ false, mMenuSizeContentObserver,
                 UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
     }
 
     void unregisterContentObservers() {
         mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);
         mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);
+        mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);
     }
 
     interface OnSettingsContentsChanged {
         void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures);
 
         void onSizeTypeChanged(int newSizeType);
+
+        void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo);
     }
 
     interface OnInfoReady<T> {
@@ -109,4 +173,16 @@
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 ACCESSIBILITY_FLOATING_MENU_SIZE, SMALL, UserHandle.USER_CURRENT);
     }
+
+    private static boolean isMenuFadeEffectEnabledFromSettings(Context context) {
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+                DEFAULT_FADE_EFFECT_IS_ENABLED, UserHandle.USER_CURRENT) == /* enabled */ 1;
+    }
+
+    private static float getMenuOpacityFromSettings(Context context) {
+        return Settings.Secure.getFloatForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE,
+                UserHandle.USER_CURRENT);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
new file mode 100644
index 0000000..ac5736b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
@@ -0,0 +1,142 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+
+import com.android.systemui.R;
+
+/**
+ * An accessibility item delegate for the individual items of the list view in the
+ * {@link MenuView}.
+ */
+class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate {
+    private final MenuAnimationController mAnimationController;
+
+    MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
+            MenuAnimationController animationController) {
+        super(recyclerViewDelegate);
+        mAnimationController = animationController;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(host, info);
+
+        final Resources res = host.getResources();
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_left));
+        info.addAction(moveTopLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_top_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_right));
+        info.addAction(moveTopRight);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_left));
+        info.addAction(moveBottomLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_right));
+        info.addAction(moveBottomRight);
+
+        final int moveEdgeId = mAnimationController.isMovedToEdge()
+                ? R.id.action_move_out_edge_and_show
+                : R.id.action_move_to_edge_and_hide;
+        final int moveEdgeTextResId = mAnimationController.isMovedToEdge()
+                ? R.string.accessibility_floating_button_action_move_out_edge_and_show
+                : R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half;
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
+                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
+    public boolean performAccessibilityAction(View host, int action, Bundle args) {
+        if (action == ACTION_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeInNowIfEnabled();
+        }
+
+        if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeOutIfEnabled();
+        }
+
+        if (action == R.id.action_move_top_left) {
+            mAnimationController.moveToTopLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_top_right) {
+            mAnimationController.moveToTopRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_left) {
+            mAnimationController.moveToBottomLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_right) {
+            mAnimationController.moveToBottomRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_to_edge_and_hide) {
+            mAnimationController.moveToEdgeAndHide();
+            return true;
+        }
+
+        if (action == R.id.action_move_out_edge_and_show) {
+            mAnimationController.moveOutEdgeAndShow();
+            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/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
new file mode 100644
index 0000000..bc3cf0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
@@ -0,0 +1,138 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Controls the all touch events of the accessibility target features view{@link RecyclerView} in
+ * the {@link MenuView}. And then compute the gestures' velocity for fling and spring
+ * animations.
+ */
+class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
+    private static final int VELOCITY_UNIT_SECONDS = 1000;
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private final MenuAnimationController mMenuAnimationController;
+    private final PointF mDown = new PointF();
+    private final PointF mMenuTranslationDown = new PointF();
+    private boolean mIsDragging = false;
+    private float mTouchSlop;
+    private final DismissAnimationController mDismissAnimationController;
+
+    MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
+            DismissAnimationController dismissAnimationController) {
+        mMenuAnimationController = menuAnimationController;
+        mDismissAnimationController = dismissAnimationController;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+
+        final View menuView = (View) recyclerView.getParent();
+        addMovement(motionEvent);
+
+        final float dx = motionEvent.getRawX() - mDown.x;
+        final float dy = motionEvent.getRawY() - mDown.y;
+
+        switch (motionEvent.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mMenuAnimationController.fadeInNowIfEnabled();
+                mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop();
+                mDown.set(motionEvent.getRawX(), motionEvent.getRawY());
+                mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
+
+                mMenuAnimationController.cancelAnimations();
+                mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
+                    if (!mIsDragging) {
+                        mIsDragging = true;
+                        mMenuAnimationController.onDraggingStart();
+                    }
+
+                    mDismissAnimationController.showDismissView(/* show= */ true);
+
+                    if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) {
+                        mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
+                        mMenuAnimationController.moveToPositionYIfNeeded(
+                                mMenuTranslationDown.y + dy);
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsDragging) {
+                    final float endX = mMenuTranslationDown.x + dx;
+                    mIsDragging = false;
+
+                    if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+                        mDismissAnimationController.showDismissView(/* show= */ false);
+                        mMenuAnimationController.fadeOutIfEnabled();
+
+                        return true;
+                    }
+
+                    if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) {
+                        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
+                        mMenuAnimationController.flingMenuThenSpringToEdge(endX,
+                                mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+                        mDismissAnimationController.showDismissView(/* show= */ false);
+                    }
+
+                    // Avoid triggering the listener of the item.
+                    return true;
+                }
+
+                break;
+            default: // Do nothing
+        }
+
+        // not consume all the events here because keeping the scroll behavior of list view.
+        return false;
+    }
+
+    @Override
+    public void onTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+        // Do nothing
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean b) {
+        // Do nothing
+    }
+
+    /**
+     * Adds a movement to the velocity tracker using raw screen coordinates.
+     */
+    private void addMovement(MotionEvent motionEvent) {
+        final float deltaX = motionEvent.getRawX() - motionEvent.getX();
+        final float deltaY = motionEvent.getRawY() - motionEvent.getY();
+        motionEvent.offsetLocation(deltaX, deltaY);
+        mVelocityTracker.addMovement(motionEvent);
+        motionEvent.offsetLocation(-deltaX, -deltaY);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
new file mode 100644
index 0000000..9875ad0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -0,0 +1,162 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The message view with the action prompt to whether to undo operation for users when removing
+ * the {@link MenuView}.
+ */
+class MenuMessageView extends LinearLayout implements
+        ViewTreeObserver.OnComputeInternalInsetsListener {
+    private final TextView mTextView;
+    private final Button mUndoButton;
+
+    @IntDef({
+            Index.TEXT_VIEW,
+            Index.UNDO_BUTTON
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface Index {
+        int TEXT_VIEW = 0;
+        int UNDO_BUTTON = 1;
+    }
+
+    MenuMessageView(Context context) {
+        super(context);
+
+        setVisibility(GONE);
+
+        mTextView = new TextView(context);
+        mUndoButton = new Button(context);
+
+        addView(mTextView, Index.TEXT_VIEW,
+                new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1));
+        addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        updateResources();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        final FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(WRAP_CONTENT,
+                WRAP_CONTENT);
+        containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+        setLayoutParams(containerParams);
+        setGravity(Gravity.CENTER_VERTICAL);
+
+        mUndoButton.setBackground(null);
+
+        updateResources();
+
+        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+    }
+
+    @Override
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+
+        if (getVisibility() == VISIBLE) {
+            final int x = (int) getX();
+            final int y = (int) getY();
+            inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight()));
+        }
+    }
+
+    /**
+     * Registers a listener to be invoked when this undo action button is clicked. It should be
+     * called after {@link View#onAttachedToWindow()}.
+     *
+     * @param listener The listener that will run
+     */
+    void setUndoListener(OnClickListener listener) {
+        mUndoButton.setOnClickListener(listener);
+    }
+
+    private void updateResources() {
+        final Resources res = getResources();
+
+        final int containerPadding =
+                res.getDimensionPixelSize(
+                        R.dimen.accessibility_floating_menu_message_container_horizontal_padding);
+        final int margin = res.getDimensionPixelSize(
+                R.dimen.accessibility_floating_menu_message_margin);
+        final FrameLayout.LayoutParams containerParams =
+                (FrameLayout.LayoutParams) getLayoutParams();
+        containerParams.setMargins(margin, margin, margin, margin);
+        setLayoutParams(containerParams);
+        setBackground(res.getDrawable(R.drawable.accessibility_floating_message_background));
+        setPadding(containerPadding, /* top= */ 0, containerPadding, /* bottom= */ 0);
+        setMinimumWidth(
+                res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_width));
+        setMinimumHeight(
+                res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_height));
+        setElevation(
+                res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_elevation));
+
+        final int textPadding =
+                res.getDimensionPixelSize(
+                        R.dimen.accessibility_floating_menu_message_text_vertical_padding);
+        final int textColor = res.getColor(R.color.accessibility_floating_menu_message_text);
+        final int textSize = res.getDimensionPixelSize(
+                R.dimen.accessibility_floating_menu_message_text_size);
+        mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding);
+        mTextView.setTextSize(COMPLEX_UNIT_PX, textSize);
+        mTextView.setTextColor(textColor);
+
+        final ColorStateList colorAccent = Utils.getColorAccent(getContext());
+        mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo));
+        mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize);
+        mUndoButton.setTextColor(colorAccent);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 576f23e..6a14af5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -21,55 +21,106 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.lifecycle.Observer;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
  * The container view displays the accessibility features.
  */
 @SuppressLint("ViewConstructor")
-class MenuView extends FrameLayout {
+class MenuView extends FrameLayout implements
+        ViewTreeObserver.OnComputeInternalInsetsListener {
     private static final int INDEX_MENU_ITEM = 0;
     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
     private final AccessibilityTargetAdapter mAdapter;
     private final MenuViewModel mMenuViewModel;
+    private final MenuAnimationController mMenuAnimationController;
+    private final Rect mBoundsInParent = new Rect();
     private final RecyclerView mTargetFeaturesView;
+    private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
+            this::updateSystemGestureExcludeRects;
+    private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver =
+            this::onMenuFadeEffectInfoChanged;
+    private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition;
     private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged;
     private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver =
             this::onTargetFeaturesChanged;
     private final MenuViewAppearance mMenuViewAppearance;
 
+    private OnTargetFeaturesChangeListener mFeaturesChangeListener;
+
     MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
         super(context);
 
         mMenuViewModel = menuViewModel;
         mMenuViewAppearance = menuViewAppearance;
+        mMenuAnimationController = new MenuAnimationController(this);
         mAdapter = new AccessibilityTargetAdapter(mTargetFeatures);
         mTargetFeaturesView = new RecyclerView(context);
         mTargetFeaturesView.setAdapter(mAdapter);
         mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context));
+        mTargetFeaturesView.setAccessibilityDelegateCompat(
+                new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) {
+                    @NonNull
+                    @Override
+                    public AccessibilityDelegateCompat getItemDelegate() {
+                        return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this,
+                                mMenuAnimationController);
+                    }
+                });
         setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
         // Avoid drawing out of bounds of the parent view
         setClipToOutline(true);
+
         loadLayoutResources();
 
         addView(mTargetFeaturesView);
     }
 
     @Override
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        if (getVisibility() == VISIBLE) {
+            inoutInfo.touchableRegion.union(mBoundsInParent);
+        }
+    }
+
+    @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
         loadLayoutResources();
+
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
+    }
+
+    void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) {
+        mFeaturesChangeListener = listener;
+    }
+
+    void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
+        mTargetFeaturesView.addOnItemTouchListener(listener);
+    }
+
+    MenuAnimationController getMenuAnimationController() {
+        return mMenuAnimationController;
     }
 
     @SuppressLint("NotifyDataSetChanged")
@@ -80,12 +131,26 @@
     }
 
     private void onSizeChanged() {
+        mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top,
+                mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(),
+                mBoundsInParent.top + mMenuViewAppearance.getMenuHeight());
+
         final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
         layoutParams.height = mMenuViewAppearance.getMenuHeight();
         setLayoutParams(layoutParams);
     }
 
-    private void onEdgeChanged() {
+    void onEdgeChangedIfNeeded() {
+        final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds();
+        if (getTranslationX() != draggableBounds.left
+                && getTranslationX() != draggableBounds.right) {
+            return;
+        }
+
+        onEdgeChanged();
+    }
+
+    void onEdgeChanged() {
         final int[] insets = mMenuViewAppearance.getMenuInsets();
         getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
                 insets[3]);
@@ -96,8 +161,22 @@
                 mMenuViewAppearance.getMenuStrokeColor());
     }
 
+    private void onPercentagePosition(Position percentagePosition) {
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onPositionChanged();
+    }
+
+    void onPositionChanged() {
+        final PointF position = mMenuViewAppearance.getMenuPosition();
+        mMenuAnimationController.moveToPosition(position);
+        onBoundsInParentChanged((int) position.x, (int) position.y);
+    }
+
     @SuppressLint("NotifyDataSetChanged")
     private void onSizeTypeChanged(int newSizeType) {
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mMenuViewAppearance.setSizeType(newSizeType);
 
         mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
@@ -106,41 +185,120 @@
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        mMenuAnimationController.fadeOutIfEnabled();
     }
 
     private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) {
         // TODO(b/252756133): Should update specific item instead of the whole list
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mTargetFeatures.clear();
         mTargetFeatures.addAll(newTargetFeatures);
         mMenuViewAppearance.setTargetFeaturesSize(mTargetFeatures.size());
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
         mAdapter.notifyDataSetChanged();
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        if (mFeaturesChangeListener != null) {
+            mFeaturesChangeListener.onChange(newTargetFeatures);
+        }
+        mMenuAnimationController.fadeOutIfEnabled();
+    }
+
+    private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(),
+                fadeEffectInfo.getOpacity());
+    }
+
+    Rect getMenuDraggableBounds() {
+        return mMenuViewAppearance.getMenuDraggableBounds();
+    }
+
+    void persistPositionAndUpdateEdge(Position percentagePosition) {
+        mMenuViewModel.updateMenuSavingPosition(percentagePosition);
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onEdgeChangedIfNeeded();
+    }
+
+    /**
+     * Uses the touch events from the parent view to identify if users clicked the extra
+     * space of the menu view. If yes, will use the percentage position and update the
+     * translations of the menu view to meet the effect of moving out from the edge. It’s only
+     * used when the menu view is hidden to the screen edge.
+     *
+     * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @return true if consume the touch event, otherwise false.
+     */
+    boolean maybeMoveOutEdgeAndShow(int x, int y) {
+        // Utilizes the touch region of the parent view to implement that users could tap extra
+        // the space region to show the menu from the edge.
+        if (!mMenuAnimationController.isMovedToEdge() || !mBoundsInParent.contains(x, y)) {
+            return false;
+        }
+
+        mMenuAnimationController.fadeInNowIfEnabled();
+
+        mMenuAnimationController.moveOutEdgeAndShow();
+
+        mMenuAnimationController.fadeOutIfEnabled();
+        return true;
     }
 
     void show() {
+        mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver);
         setVisibility(VISIBLE);
         mMenuViewModel.registerContentObservers();
+        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
 
     void hide() {
         setVisibility(GONE);
+        mBoundsInParent.setEmpty();
+        mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver);
         mMenuViewModel.unregisterContentObservers();
+        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
+    }
+
+    void onDraggingStart() {
+        final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets();
+        getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
+                insets[3]);
+
+        final GradientDrawable gradientDrawable = getContainerViewGradient();
+        gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuMovingStateRadii());
+    }
+
+    void onBoundsInParentChanged(int newLeft, int newTop) {
+        mBoundsInParent.offsetTo(newLeft, newTop);
     }
 
     void loadLayoutResources() {
         mMenuViewAppearance.update();
 
+        mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription());
         setBackground(mMenuViewAppearance.getMenuBackground());
         setElevation(mMenuViewAppearance.getMenuElevation());
         onItemSizeChanged();
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
     }
 
     private InstantInsetLayerDrawable getContainerViewInsetLayer() {
@@ -150,4 +308,22 @@
     private GradientDrawable getContainerViewGradient() {
         return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
     }
+
+    private void updateSystemGestureExcludeRects() {
+        final ViewGroup parentView = (ViewGroup) getParent();
+        parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent));
+    }
+
+    /**
+     * Interface definition for the {@link AccessibilityTarget} list changes.
+     */
+    interface OnTargetFeaturesChangeListener {
+        /**
+         * Called when the list of accessibility target features was updated. This will be
+         * invoked when the end of {@code onTargetFeaturesChanged}.
+         *
+         * @param newTargetFeatures the list related to the current accessibility features.
+         */
+        void onChange(List<AccessibilityTarget> newTargetFeatures);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
index b9b7732..4a9807f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
@@ -16,12 +16,21 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.view.View.OVER_SCROLL_ALWAYS;
+import static android.view.View.OVER_SCROLL_NEVER;
+
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.annotation.DimenRes;
 
@@ -34,9 +43,13 @@
  * Provides the layout resources information of the {@link MenuView}.
  */
 class MenuViewAppearance {
+    private final WindowManager mWindowManager;
     private final Resources mRes;
+    private final Position mPercentagePosition = new Position(/* percentageX= */
+            0f, /* percentageY= */ 0f);
     private int mTargetFeaturesSize;
     private int mSizeType;
+    private int mMargin;
     private int mSmallPadding;
     private int mLargePadding;
     private int mSmallIconSize;
@@ -51,6 +64,7 @@
     private int mElevation;
     private float[] mRadii;
     private Drawable mBackgroundDrawable;
+    private String mContentDescription;
 
     @IntDef({
             SMALL,
@@ -62,13 +76,15 @@
         int LARGE = 1;
     }
 
-    MenuViewAppearance(Context context) {
+    MenuViewAppearance(Context context, WindowManager windowManager) {
+        mWindowManager = windowManager;
         mRes = context.getResources();
 
         update();
     }
 
     void update() {
+        mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
         mSmallPadding =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
         mLargePadding =
@@ -81,7 +97,7 @@
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius);
         mSmallMultipleRadius = mRes.getDimensionPixelSize(
                 R.dimen.accessibility_floating_menu_small_multiple_radius);
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
         mLargeSingleRadius =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius);
         mLargeMultipleRadius = mRes.getDimensionPixelSize(
@@ -93,18 +109,55 @@
         final Drawable drawable =
                 mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
         mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
+        mContentDescription = mRes.getString(
+                com.android.internal.R.string.accessibility_select_shortcut_menu_title);
     }
 
     void setSizeType(int sizeType) {
         mSizeType = sizeType;
 
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
     }
 
     void setTargetFeaturesSize(int targetFeaturesSize) {
         mTargetFeaturesSize = targetFeaturesSize;
 
-        mRadii = createRadii(getMenuRadius(targetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize));
+    }
+
+    void setPercentagePosition(Position percentagePosition) {
+        mPercentagePosition.update(percentagePosition);
+
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
+    }
+
+    Rect getMenuDraggableBounds() {
+        final int margin = getMenuMargin();
+        final Rect draggableBounds = getWindowAvailableBounds();
+
+        // Initializes start position for mapping the translation of the menu view.
+        draggableBounds.offsetTo(/* newLeft= */ 0, /* newTop= */ 0);
+
+        draggableBounds.top += margin;
+        draggableBounds.right -= getMenuWidth();
+        draggableBounds.bottom -= Math.min(
+                getWindowAvailableBounds().height() - draggableBounds.top,
+                calculateActualMenuHeight() + margin);
+        return draggableBounds;
+    }
+
+    PointF getMenuPosition() {
+        final Rect draggableBounds = getMenuDraggableBounds();
+
+        return new PointF(
+                draggableBounds.left
+                        + draggableBounds.width() * mPercentagePosition.getPercentageX(),
+                draggableBounds.top
+                        + draggableBounds.height() * mPercentagePosition.getPercentageY());
+    }
+
+    String getContentDescription() {
+        return mContentDescription;
     }
 
     Drawable getMenuBackground() {
@@ -115,20 +168,41 @@
         return mElevation;
     }
 
+    int getMenuWidth() {
+        return getMenuPadding() * 2 + getMenuIconSize();
+    }
+
     int getMenuHeight() {
-        return calculateActualMenuHeight();
+        return Math.min(getWindowAvailableBounds().height() - mMargin * 2,
+                calculateActualMenuHeight());
     }
 
     int getMenuIconSize() {
         return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize;
     }
 
+    private int getMenuMargin() {
+        return mMargin;
+    }
+
     int getMenuPadding() {
         return mSizeType == SMALL ? mSmallPadding : mLargePadding;
     }
 
     int[] getMenuInsets() {
-        return new int[]{mInset, 0, 0, 0};
+        final int left = isMenuOnLeftSide() ? mInset : 0;
+        final int right = isMenuOnLeftSide() ? 0 : mInset;
+
+        return new int[]{left, 0, right, 0};
+    }
+
+    int[] getMenuMovingStateInsets() {
+        return new int[]{0, 0, 0, 0};
+    }
+
+    float[] getMenuMovingStateRadii() {
+        final float radius = getMenuRadius(mTargetFeaturesSize);
+        return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
     }
 
     int getMenuStrokeWidth() {
@@ -147,6 +221,14 @@
         return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount);
     }
 
+    int getMenuScrollMode() {
+        return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER;
+    }
+
+    private boolean hasExceededMaxWindowHeight() {
+        return calculateActualMenuHeight() > getWindowAvailableBounds().height();
+    }
+
     @DimenRes
     private int getSmallSize(int itemCount) {
         return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius;
@@ -157,8 +239,29 @@
         return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius;
     }
 
-    private static float[] createRadii(float radius) {
-        return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
+    private static float[] createRadii(boolean isMenuOnLeftSide, float radius) {
+        return isMenuOnLeftSide
+                ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}
+                : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
+    }
+
+    private Rect getWindowAvailableBounds() {
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets insets = windowInsets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+
+        final Rect bounds = new Rect(windowMetrics.getBounds());
+        bounds.left += insets.left;
+        bounds.right -= insets.right;
+        bounds.top += insets.top;
+        bounds.bottom -= insets.bottom;
+
+        return bounds;
+    }
+
+    private boolean isMenuOnLeftSide() {
+        return mPercentagePosition.getPercentageX() < 0.5f;
     }
 
     private int calculateActualMenuHeight() {
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 4ea2f77..b8f14ae 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -16,39 +16,183 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE;
+import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
+import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
+import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.PluralsMessageFormatter;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 import android.widget.FrameLayout;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.accessibility.dialog.AccessibilityTarget;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.systemui.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
- * The basic interactions with the child view {@link MenuView}.
+ * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and
+ * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the
+ * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu
+ * message view would be shown and allowed users to undo it.
  */
 @SuppressLint("ViewConstructor")
 class MenuViewLayer extends FrameLayout {
+    private static final int SHOW_MESSAGE_DELAY_MS = 3000;
+
     private final MenuView mMenuView;
+    private final MenuMessageView mMessageView;
+    private final DismissView mDismissView;
+    private final MenuAnimationController mMenuAnimationController;
+    private final AccessibilityManager mAccessibilityManager;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final IAccessibilityFloatingMenu mFloatingMenu;
+    private final DismissAnimationController mDismissAnimationController;
 
     @IntDef({
-            LayerIndex.MENU_VIEW
+            LayerIndex.MENU_VIEW,
+            LayerIndex.DISMISS_VIEW,
+            LayerIndex.MESSAGE_VIEW,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface LayerIndex {
         int MENU_VIEW = 0;
+        int DISMISS_VIEW = 1;
+        int MESSAGE_VIEW = 2;
     }
 
-    MenuViewLayer(@NonNull Context context) {
+    @VisibleForTesting
+    final Runnable mDismissMenuAction = new Runnable() {
+        @Override
+        public void run() {
+            Settings.Secure.putStringForUser(getContext().getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ "",
+                    UserHandle.USER_CURRENT);
+
+            // Should disable the corresponding service when the fragment type is
+            // INVISIBLE_TOGGLE, which will enable service when the shortcut is on.
+            final List<AccessibilityServiceInfo> serviceInfoList =
+                    mAccessibilityManager.getEnabledAccessibilityServiceList(
+                            AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+            serviceInfoList.forEach(info -> {
+                if (getAccessibilityServiceFragmentType(info) == INVISIBLE_TOGGLE) {
+                    setAccessibilityServiceState(mContext, info.getComponentName(), /* enabled= */
+                            false);
+                }
+            });
+
+            mFloatingMenu.hide();
+        }
+    };
+
+    MenuViewLayer(@NonNull Context context, WindowManager windowManager,
+            AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) {
         super(context);
 
+        mAccessibilityManager = accessibilityManager;
+        mFloatingMenu = floatingMenu;
+
         final MenuViewModel menuViewModel = new MenuViewModel(context);
-        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context);
+        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context,
+                windowManager);
         mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
+        mMenuAnimationController = mMenuView.getMenuAnimationController();
+        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+
+        mDismissView = new DismissView(context);
+        mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
+        mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
+            @Override
+            public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true);
+            }
+
+            @Override
+            public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                    float velocityX, float velocityY, boolean wasFlungOut) {
+                mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+            }
+
+            @Override
+            public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                hideMenuAndShowMessage();
+                mDismissView.hide();
+                mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+            }
+        });
+
+        final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler(
+                mMenuAnimationController, mDismissAnimationController);
+        mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler);
+
+        mMessageView = new MenuMessageView(context);
+
+        mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
+            if (newTargetFeatures.size() < 1) {
+                return;
+            }
+
+            // During the undo action period, the pending action will be canceled and undo back
+            // to the previous state if users did any action related to the accessibility features.
+            if (mMessageView.getVisibility() == VISIBLE) {
+                undo();
+            }
+
+            final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
+            messageText.setText(getMessageText(newTargetFeatures));
+        });
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
+        addView(mDismissView, LayerIndex.DISMISS_VIEW);
+        addView(mMessageView, LayerIndex.MESSAGE_VIEW);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDismissView.updateResources();
+    }
+
+    private String getMessageText(List<AccessibilityTarget> newTargetFeatures) {
+        Preconditions.checkArgument(newTargetFeatures.size() > 0,
+                "The list should at least have one feature.");
+
+        final Map<String, Object> arguments = new HashMap<>();
+        arguments.put("count", newTargetFeatures.size());
+        arguments.put("label", newTargetFeatures.get(0).getLabel());
+        return PluralsMessageFormatter.format(getResources(), arguments,
+                R.string.accessibility_floating_button_undo_message_text);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) {
+            return true;
+        }
+
+        return super.onInterceptTouchEvent(event);
     }
 
     @Override
@@ -56,6 +200,8 @@
         super.onAttachedToWindow();
 
         mMenuView.show();
+        mMessageView.setUndoListener(view -> undo());
+        mContext.registerComponentCallbacks(mDismissAnimationController);
     }
 
     @Override
@@ -63,5 +209,26 @@
         super.onDetachedFromWindow();
 
         mMenuView.hide();
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        mContext.unregisterComponentCallbacks(mDismissAnimationController);
+    }
+
+    private void hideMenuAndShowMessage() {
+        final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
+                SHOW_MESSAGE_DELAY_MS,
+                AccessibilityManager.FLAG_CONTENT_TEXT
+                        | AccessibilityManager.FLAG_CONTENT_CONTROLS);
+        mHandler.postDelayed(mDismissMenuAction, delayTime);
+        mMessageView.setVisibility(VISIBLE);
+        mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
+    }
+
+    private void undo() {
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        mMessageView.setVisibility(GONE);
+        mMenuView.onEdgeChanged();
+        mMenuView.onPositionChanged();
+        mMenuView.setVisibility(VISIBLE);
+        mMenuAnimationController.startGrowAnimation();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index 1e15a59..c7be907 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -20,7 +20,9 @@
 
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 /**
  * Controls the {@link MenuViewLayer} whether to be attached to the window via the interface
@@ -31,9 +33,10 @@
     private final MenuViewLayer mMenuViewLayer;
     private boolean mIsShowing;
 
-    MenuViewLayerController(Context context, WindowManager windowManager) {
+    MenuViewLayerController(Context context, WindowManager windowManager,
+            AccessibilityManager accessibilityManager) {
         mWindowManager = windowManager;
-        mMenuViewLayer = new MenuViewLayer(context);
+        mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this);
     }
 
     @Override
@@ -68,9 +71,11 @@
                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                 PixelFormat.TRANSLUCENT);
+        params.receiveInsetsIgnoringZOrder = true;
         params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
         params.windowAnimations = android.R.style.Animation_Translucent;
-
+        params.setFitInsetsTypes(
+                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
         return params;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
index c3ba439..e8a2b6e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
@@ -33,6 +33,9 @@
     private final MutableLiveData<List<AccessibilityTarget>> mTargetFeaturesData =
             new MutableLiveData<>();
     private final MutableLiveData<Integer> mSizeTypeData = new MutableLiveData<>();
+    private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData =
+            new MutableLiveData<>();
+    private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
     private final MenuInfoRepository mInfoRepository;
 
     MenuViewModel(Context context) {
@@ -49,11 +52,30 @@
         mSizeTypeData.setValue(newSizeType);
     }
 
+    @Override
+    public void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mFadeEffectInfoData.setValue(fadeEffectInfo);
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mInfoRepository.updateMenuSavingPosition(percentagePosition);
+    }
+
+    LiveData<Position> getPercentagePositionData() {
+        mInfoRepository.loadMenuPosition(mPercentagePositionData::setValue);
+        return mPercentagePositionData;
+    }
+
     LiveData<Integer> getSizeTypeData() {
         mInfoRepository.loadMenuSizeType(mSizeTypeData::setValue);
         return mSizeTypeData;
     }
 
+    LiveData<MenuFadeEffectInfo> getFadeEffectInfoData() {
+        mInfoRepository.loadMenuFadeEffectInfo(mFadeEffectInfoData::setValue);
+        return mFadeEffectInfoData;
+    }
+
     LiveData<List<AccessibilityTarget>> getTargetFeaturesData() {
         mInfoRepository.loadMenuTargetFeatures(mTargetFeaturesData::setValue);
         return mTargetFeaturesData;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
index 7b7eda8..fc21be2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import android.annotation.FloatRange;
+import android.annotation.NonNull;
 import android.text.TextUtils;
 
 /**
@@ -62,6 +63,13 @@
     }
 
     /**
+     * Updates the position with {@code percentagePosition}.
+     */
+    public void update(@NonNull Position percentagePosition) {
+        update(percentagePosition.getPercentageX(), percentagePosition.getPercentageY());
+    }
+
+    /**
      * Updates the position with {@code percentageX} and {@code percentageY}.
      *
      * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0.
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index 6b85976..6785a43 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -103,6 +103,7 @@
             AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
             AppOpsManager.OP_RECORD_AUDIO,
             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+            AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
             AppOpsManager.OP_COARSE_LOCATION,
             AppOpsManager.OP_FINE_LOCATION
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
new file mode 100644
index 0000000..b52ddc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.battery
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.DrawableWrapper
+import android.util.PathParser
+import com.android.settingslib.graph.ThemedBatteryDrawable
+import com.android.systemui.R
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.SHIELD_LEFT_OFFSET
+import com.android.systemui.battery.BatterySpecs.SHIELD_STROKE
+import com.android.systemui.battery.BatterySpecs.SHIELD_TOP_OFFSET
+
+/**
+ * A battery drawable that accessorizes [ThemedBatteryDrawable] with additional information if
+ * necessary.
+ *
+ * For now, it adds a shield in the bottom-right corner when [displayShield] is true.
+ */
+class AccessorizedBatteryDrawable(
+    private val context: Context,
+    frameColor: Int,
+) : DrawableWrapper(ThemedBatteryDrawable(context, frameColor)) {
+    private val mainBatteryDrawable: ThemedBatteryDrawable
+        get() = drawable as ThemedBatteryDrawable
+
+    private val shieldPath = Path()
+    private val scaledShield = Path()
+    private val scaleMatrix = Matrix()
+
+    private var shieldLeftOffsetScaled = SHIELD_LEFT_OFFSET
+    private var shieldTopOffsetScaled = SHIELD_TOP_OFFSET
+
+    private var density = context.resources.displayMetrics.density
+
+    private val dualTone =
+        context.resources.getBoolean(com.android.internal.R.bool.config_batterymeterDualTone)
+
+    private val shieldTransparentOutlinePaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+            p.color = Color.TRANSPARENT
+            p.strokeWidth = ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+            p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
+            p.style = Paint.Style.FILL_AND_STROKE
+        }
+
+    private val shieldPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
+            p.color = Color.MAGENTA
+            p.style = Paint.Style.FILL
+            p.isDither = true
+        }
+
+    init {
+        loadPaths()
+    }
+
+    override fun onBoundsChange(bounds: Rect) {
+        super.onBoundsChange(bounds)
+        updateSizes()
+    }
+
+    var displayShield: Boolean = false
+
+    private fun updateSizes() {
+        val b = bounds
+        if (b.isEmpty) {
+            return
+        }
+
+        val mainWidth = BatterySpecs.getMainBatteryWidth(b.width().toFloat(), displayShield)
+        val mainHeight = BatterySpecs.getMainBatteryHeight(b.height().toFloat(), displayShield)
+
+        drawable?.setBounds(
+            b.left,
+            b.top,
+            /* right= */ b.left + mainWidth.toInt(),
+            /* bottom= */ b.top + mainHeight.toInt()
+        )
+
+        if (displayShield) {
+            val sx = b.right / BATTERY_WIDTH_WITH_SHIELD
+            val sy = b.bottom / BATTERY_HEIGHT_WITH_SHIELD
+            scaleMatrix.setScale(sx, sy)
+            shieldPath.transform(scaleMatrix, scaledShield)
+
+            shieldLeftOffsetScaled = sx * SHIELD_LEFT_OFFSET
+            shieldTopOffsetScaled = sy * SHIELD_TOP_OFFSET
+
+            val scaledStrokeWidth =
+                (sx * SHIELD_STROKE).coerceAtLeast(
+                    ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH
+                )
+            shieldTransparentOutlinePaint.strokeWidth = scaledStrokeWidth
+        }
+    }
+
+    override fun getIntrinsicHeight(): Int {
+        val height =
+            if (displayShield) {
+                BATTERY_HEIGHT_WITH_SHIELD
+            } else {
+                BATTERY_HEIGHT
+            }
+        return (height * density).toInt()
+    }
+
+    override fun getIntrinsicWidth(): Int {
+        val width =
+            if (displayShield) {
+                BATTERY_WIDTH_WITH_SHIELD
+            } else {
+                BATTERY_WIDTH
+            }
+        return (width * density).toInt()
+    }
+
+    override fun draw(c: Canvas) {
+        c.saveLayer(null, null)
+        // Draw the main battery icon
+        super.draw(c)
+
+        if (displayShield) {
+            c.translate(shieldLeftOffsetScaled, shieldTopOffsetScaled)
+            // We need a transparent outline around the shield, so first draw the transparent-ness
+            // then draw the shield
+            c.drawPath(scaledShield, shieldTransparentOutlinePaint)
+            c.drawPath(scaledShield, shieldPaint)
+        }
+        c.restore()
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.OPAQUE
+    }
+
+    override fun setAlpha(p0: Int) {
+        // Unused internally -- see [ThemedBatteryDrawable.setAlpha].
+    }
+
+    override fun setColorFilter(colorfilter: ColorFilter?) {
+        super.setColorFilter(colorFilter)
+        shieldPaint.colorFilter = colorFilter
+    }
+
+    /** Sets whether the battery is currently charging. */
+    fun setCharging(charging: Boolean) {
+        mainBatteryDrawable.charging = charging
+    }
+
+    /** Sets the current level (out of 100) of the battery. */
+    fun setBatteryLevel(level: Int) {
+        mainBatteryDrawable.setBatteryLevel(level)
+    }
+
+    /** Sets whether power save is enabled. */
+    fun setPowerSaveEnabled(powerSaveEnabled: Boolean) {
+        mainBatteryDrawable.powerSaveEnabled = powerSaveEnabled
+    }
+
+    /** Returns whether power save is currently enabled. */
+    fun getPowerSaveEnabled(): Boolean {
+        return mainBatteryDrawable.powerSaveEnabled
+    }
+
+    /** Sets the colors to use for the icon. */
+    fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
+        shieldPaint.color = if (dualTone) fgColor else singleToneColor
+        mainBatteryDrawable.setColors(fgColor, bgColor, singleToneColor)
+    }
+
+    /** Notifies this drawable that the density might have changed. */
+    fun notifyDensityChanged() {
+        density = context.resources.displayMetrics.density
+    }
+
+    private fun loadPaths() {
+        val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
+        shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 6a10d4a..03d999f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -45,7 +45,6 @@
 import androidx.annotation.StyleRes;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.settingslib.graph.ThemedBatteryDrawable;
 import com.android.systemui.DualToneHandler;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -68,7 +67,7 @@
     public static final int MODE_OFF = 2;
     public static final int MODE_ESTIMATE = 3;
 
-    private final ThemedBatteryDrawable mDrawable;
+    private final AccessorizedBatteryDrawable mDrawable;
     private final ImageView mBatteryIconView;
     private TextView mBatteryPercentView;
 
@@ -77,7 +76,10 @@
     private int mLevel;
     private int mShowPercentMode = MODE_DEFAULT;
     private boolean mShowPercentAvailable;
+    private String mEstimateText = null;
     private boolean mCharging;
+    private boolean mIsOverheated;
+    private boolean mDisplayShieldEnabled;
     // Error state where we know nothing about the current battery state
     private boolean mBatteryStateUnknown;
     // Lazily-loaded since this is expected to be a rare-if-ever state
@@ -106,7 +108,7 @@
         final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor,
                 context.getColor(R.color.meter_background_color));
         mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0);
-        mDrawable = new ThemedBatteryDrawable(context, frameColor);
+        mDrawable = new AccessorizedBatteryDrawable(context, frameColor);
         atts.recycle();
 
         mShowPercentAvailable = context.getResources().getBoolean(
@@ -170,12 +172,14 @@
         if (mode == mShowPercentMode) return;
         mShowPercentMode = mode;
         updateShowPercent();
+        updatePercentText();
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updatePercentView();
+        mDrawable.notifyDensityChanged();
     }
 
     public void setColorsFromContext(Context context) {
@@ -203,6 +207,17 @@
         mDrawable.setPowerSaveEnabled(isPowerSave);
     }
 
+    void onIsOverheatedChanged(boolean isOverheated) {
+        boolean valueChanged = mIsOverheated != isOverheated;
+        mIsOverheated = isOverheated;
+        if (valueChanged) {
+            updateContentDescription();
+            // The battery drawable is a different size depending on whether it's currently
+            // overheated or not, so we need to re-scale the view when overheated changes.
+            scaleBatteryMeterViews();
+        }
+    }
+
     private TextView loadPercentView() {
         return (TextView) LayoutInflater.from(getContext())
                 .inflate(R.layout.battery_percentage_view, null);
@@ -227,13 +242,17 @@
         mBatteryEstimateFetcher = fetcher;
     }
 
+    void setDisplayShieldEnabled(boolean displayShieldEnabled) {
+        mDisplayShieldEnabled = displayShieldEnabled;
+    }
+
     void updatePercentText() {
         if (mBatteryStateUnknown) {
-            setContentDescription(getContext().getString(R.string.accessibility_battery_unknown));
             return;
         }
 
         if (mBatteryEstimateFetcher == null) {
+            setPercentTextAtCurrentLevel();
             return;
         }
 
@@ -245,10 +264,9 @@
                         return;
                     }
                     if (estimate != null && mShowPercentMode == MODE_ESTIMATE) {
+                        mEstimateText = estimate;
                         mBatteryPercentView.setText(estimate);
-                        setContentDescription(getContext().getString(
-                                R.string.accessibility_battery_level_with_estimate,
-                                mLevel, estimate));
+                        updateContentDescription();
                     } else {
                         setPercentTextAtCurrentLevel();
                     }
@@ -257,28 +275,49 @@
                 setPercentTextAtCurrentLevel();
             }
         } else {
-            setContentDescription(
-                    getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
-                            : R.string.accessibility_battery_level, mLevel));
+            updateContentDescription();
         }
     }
 
     private void setPercentTextAtCurrentLevel() {
-        if (mBatteryPercentView == null) {
-            return;
+        if (mBatteryPercentView != null) {
+            mEstimateText = null;
+            String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
+            // Setting text actually triggers a layout pass (because the text view is set to
+            // wrap_content width and TextView always relayouts for this). Avoid needless
+            // relayout if the text didn't actually change.
+            if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
+                mBatteryPercentView.setText(percentText);
+            }
         }
 
-        String percentText = NumberFormat.getPercentInstance().format(mLevel / 100f);
-        // Setting text actually triggers a layout pass (because the text view is set to
-        // wrap_content width and TextView always relayouts for this). Avoid needless
-        // relayout if the text didn't actually change.
-        if (!TextUtils.equals(mBatteryPercentView.getText(), percentText)) {
-            mBatteryPercentView.setText(percentText);
+        updateContentDescription();
+    }
+
+    private void updateContentDescription() {
+        Context context = getContext();
+
+        String contentDescription;
+        if (mBatteryStateUnknown) {
+            contentDescription = context.getString(R.string.accessibility_battery_unknown);
+        } else if (mShowPercentMode == MODE_ESTIMATE && !TextUtils.isEmpty(mEstimateText)) {
+            contentDescription = context.getString(
+                    mIsOverheated
+                            ? R.string.accessibility_battery_level_charging_paused_with_estimate
+                            : R.string.accessibility_battery_level_with_estimate,
+                    mLevel,
+                    mEstimateText);
+        } else if (mIsOverheated) {
+            contentDescription =
+                    context.getString(R.string.accessibility_battery_level_charging_paused, mLevel);
+        } else if (mCharging) {
+            contentDescription =
+                    context.getString(R.string.accessibility_battery_level_charging, mLevel);
+        } else {
+            contentDescription = context.getString(R.string.accessibility_battery_level, mLevel);
         }
 
-        setContentDescription(
-                getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
-                        : R.string.accessibility_battery_level, mLevel));
+        setContentDescription(contentDescription);
     }
 
     void updateShowPercent() {
@@ -329,6 +368,7 @@
         }
 
         mBatteryStateUnknown = isUnknown;
+        updateContentDescription();
 
         if (mBatteryStateUnknown) {
             mBatteryIconView.setImageDrawable(getUnknownStateDrawable());
@@ -349,15 +389,43 @@
         res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
         float iconScaleFactor = typedValue.getFloat();
 
-        int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height);
-        int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width);
+        float mainBatteryHeight =
+                res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor;
+        float mainBatteryWidth =
+                res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
+
+        // If the battery is marked as overheated, we should display a shield indicating that the
+        // battery is being "defended".
+        boolean displayShield = mDisplayShieldEnabled && mIsOverheated;
+        float fullBatteryIconHeight =
+                BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
+        float fullBatteryIconWidth =
+                BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield);
+
+        int marginTop;
+        if (displayShield) {
+            // If the shield is displayed, we need some extra marginTop so that the bottom of the
+            // main icon is still aligned with the bottom of all the other system icons.
+            int shieldHeightAddition = Math.round(fullBatteryIconHeight - mainBatteryHeight);
+            // However, the other system icons have some embedded bottom padding that the battery
+            // doesn't have, so we shouldn't move the battery icon down by the full amount.
+            // See b/258672854.
+            marginTop = shieldHeightAddition
+                    - res.getDimensionPixelSize(R.dimen.status_bar_battery_extra_vertical_spacing);
+        } else {
+            marginTop = 0;
+        }
+
         int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom);
 
         LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams(
-                (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor));
-        scaledLayoutParams.setMargins(0, 0, 0, marginBottom);
+                Math.round(fullBatteryIconWidth),
+                Math.round(fullBatteryIconHeight));
+        scaledLayoutParams.setMargins(0, marginTop, 0, marginBottom);
 
+        mDrawable.setDisplayShield(displayShield);
         mBatteryIconView.setLayoutParams(scaledLayoutParams);
+        mBatteryIconView.invalidateDrawable(mDrawable);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index ae9a323..f4ec33a 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -17,19 +17,23 @@
 
 import static android.provider.Settings.System.SHOW_BATTERY_PERCENT;
 
-import android.app.ActivityManager;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.view.View;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
+import androidx.annotation.NonNull;
+
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -42,12 +46,13 @@
 public class BatteryMeterViewController extends ViewController<BatteryMeterView> {
     private final ConfigurationController mConfigurationController;
     private final TunerService mTunerService;
+    private final Handler mMainHandler;
     private final ContentResolver mContentResolver;
     private final BatteryController mBatteryController;
 
     private final String mSlotBattery;
     private final SettingObserver mSettingObserver;
-    private final CurrentUserTracker mCurrentUserTracker;
+    private final UserTracker mUserTracker;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -84,6 +89,21 @@
                 public void onBatteryUnknownStateChanged(boolean isUnknown) {
                     mView.onBatteryUnknownStateChanged(isUnknown);
                 }
+
+                @Override
+                public void onIsOverheatedChanged(boolean isOverheated) {
+                    mView.onIsOverheatedChanged(isOverheated);
+                }
+            };
+
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    mContentResolver.unregisterContentObserver(mSettingObserver);
+                    registerShowBatteryPercentObserver(newUser);
+                    mView.updateShowPercent();
+                }
             };
 
     // Some places may need to show the battery conditionally, and not obey the tuner
@@ -93,30 +113,26 @@
     @Inject
     public BatteryMeterViewController(
             BatteryMeterView view,
+            UserTracker userTracker,
             ConfigurationController configurationController,
             TunerService tunerService,
-            BroadcastDispatcher broadcastDispatcher,
             @Main Handler mainHandler,
             ContentResolver contentResolver,
+            FeatureFlags featureFlags,
             BatteryController batteryController) {
         super(view);
+        mUserTracker = userTracker;
         mConfigurationController = configurationController;
         mTunerService = tunerService;
+        mMainHandler = mainHandler;
         mContentResolver = contentResolver;
         mBatteryController = batteryController;
 
         mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
+        mView.setDisplayShieldEnabled(featureFlags.isEnabled(Flags.BATTERY_SHIELD_ICON));
 
         mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
-        mSettingObserver = new SettingObserver(mainHandler);
-        mCurrentUserTracker = new CurrentUserTracker(broadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                contentResolver.unregisterContentObserver(mSettingObserver);
-                registerShowBatteryPercentObserver(newUserId);
-                mView.updateShowPercent();
-            }
-        };
+        mSettingObserver = new SettingObserver(mMainHandler);
     }
 
     @Override
@@ -125,9 +141,9 @@
         subscribeForTunerUpdates();
         mBatteryController.addCallback(mBatteryStateChangeCallback);
 
-        registerShowBatteryPercentObserver(ActivityManager.getCurrentUser());
+        registerShowBatteryPercentObserver(mUserTracker.getUserId());
         registerGlobalBatteryUpdateObserver();
-        mCurrentUserTracker.startTracking();
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
 
         mView.updateShowPercent();
     }
@@ -138,7 +154,7 @@
         unsubscribeFromTunerUpdates();
         mBatteryController.removeCallback(mBatteryStateChangeCallback);
 
-        mCurrentUserTracker.stopTracking();
+        mUserTracker.removeCallback(mUserChangedCallback);
         mContentResolver.unregisterContentObserver(mSettingObserver);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
new file mode 100644
index 0000000..6455a96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt
@@ -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.systemui.battery
+
+import com.android.settingslib.graph.ThemedBatteryDrawable
+
+/** An object storing specs related to the battery icon in the status bar. */
+object BatterySpecs {
+
+    /** Width of the main battery icon, not including the shield. */
+    const val BATTERY_WIDTH = ThemedBatteryDrawable.WIDTH
+    /** Height of the main battery icon, not including the shield. */
+    const val BATTERY_HEIGHT = ThemedBatteryDrawable.HEIGHT
+
+    private const val SHIELD_WIDTH = 10f
+    private const val SHIELD_HEIGHT = 13f
+
+    /**
+     * Amount that the left side of the shield should be offset from the left side of the battery.
+     */
+    const val SHIELD_LEFT_OFFSET = 8f
+    /** Amount that the top of the shield should be offset from the top of the battery. */
+    const val SHIELD_TOP_OFFSET = 10f
+
+    const val SHIELD_STROKE = 4f
+
+    /** The full width of the battery icon, including the main battery icon *and* the shield. */
+    const val BATTERY_WIDTH_WITH_SHIELD = SHIELD_LEFT_OFFSET + SHIELD_WIDTH
+    /** The full height of the battery icon, including the main battery icon *and* the shield. */
+    const val BATTERY_HEIGHT_WITH_SHIELD = SHIELD_TOP_OFFSET + SHIELD_HEIGHT
+
+    /**
+     * Given the desired height of the main battery icon in pixels, returns the height that the full
+     * battery icon will take up in pixels.
+     *
+     * If there's no shield, this will just return [mainBatteryHeight]. Otherwise, the shield
+     * extends slightly below the bottom of the main battery icon so we need some extra height.
+     */
+    @JvmStatic
+    fun getFullBatteryHeight(mainBatteryHeight: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            mainBatteryHeight
+        } else {
+            val verticalScaleFactor = mainBatteryHeight / BATTERY_HEIGHT
+            verticalScaleFactor * BATTERY_HEIGHT_WITH_SHIELD
+        }
+    }
+
+    /**
+     * Given the desired width of the main battery icon in pixels, returns the width that the full
+     * battery icon will take up in pixels.
+     *
+     * If there's no shield, this will just return [mainBatteryWidth]. Otherwise, the shield extends
+     * past the right side of the main battery icon so we need some extra width.
+     */
+    @JvmStatic
+    fun getFullBatteryWidth(mainBatteryWidth: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            mainBatteryWidth
+        } else {
+            val horizontalScaleFactor = mainBatteryWidth / BATTERY_WIDTH
+            horizontalScaleFactor * BATTERY_WIDTH_WITH_SHIELD
+        }
+    }
+
+    /**
+     * Given the height of the full battery icon, return how tall the main battery icon should be.
+     *
+     * If there's no shield, this will just return [fullBatteryHeight]. Otherwise, the shield takes
+     * up some of the view's height so the main battery width will be just a portion of
+     * [fullBatteryHeight].
+     */
+    @JvmStatic
+    fun getMainBatteryHeight(fullBatteryHeight: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            fullBatteryHeight
+        } else {
+            return (BATTERY_HEIGHT / BATTERY_HEIGHT_WITH_SHIELD) * fullBatteryHeight
+        }
+    }
+
+    /**
+     * Given the width of the full battery icon, return how wide the main battery icon should be.
+     *
+     * If there's no shield, this will just return [fullBatteryWidth]. Otherwise, the shield takes
+     * up some of the view's width so the main battery width will be just a portion of
+     * [fullBatteryWidth].
+     */
+    @JvmStatic
+    fun getMainBatteryWidth(fullBatteryWidth: Float, displayShield: Boolean): Float {
+        return if (!displayShield) {
+            fullBatteryWidth
+        } else {
+            return (BATTERY_WIDTH / BATTERY_WIDTH_WITH_SHIELD) * fullBatteryWidth
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index b40b356..b2a2a67 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.RawRes
 import android.content.Context
+import android.content.res.Configuration
 import android.hardware.fingerprint.FingerprintManager
 import android.view.DisplayInfo
 import android.view.Surface
@@ -33,15 +34,19 @@
 import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
 import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE
 import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
+import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
+import com.android.systemui.unfold.updates.FoldProvider
 
 /** Fingerprint only icon animator for BiometricPrompt.  */
 open class AuthBiometricFingerprintIconController(
         context: Context,
         iconView: LottieAnimationView,
         protected val iconViewOverlay: LottieAnimationView
-) : AuthIconController(context, iconView) {
+) : AuthIconController(context, iconView), FoldProvider.FoldCallback {
 
+    private var isDeviceFolded: Boolean = false
     private val isSideFps: Boolean
+    private val screenSizeFoldProvider: ScreenSizeFoldProvider = ScreenSizeFoldProvider(context)
     var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1)
         set(value) {
             if (field == value) {
@@ -74,6 +79,8 @@
         if (isSideFps && displayInfo.rotation == Surface.ROTATION_180) {
             iconView.rotation = 180f
         }
+        screenSizeFoldProvider.registerCallback(this, context.mainExecutor)
+        screenSizeFoldProvider.onConfigurationChange(context.resources.configuration)
     }
 
     private fun updateIconSideFps(@BiometricState lastState: Int, @BiometricState newState: Int) {
@@ -124,6 +131,10 @@
         LottieColorUtils.applyDynamicColors(context, iconView)
     }
 
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        screenSizeFoldProvider.onConfigurationChange(newConfig)
+    }
+
     override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
         if (isSideFps) {
             updateIconSideFps(lastState, newState)
@@ -191,11 +202,21 @@
 
     @RawRes
     private fun getSideFpsAnimationForTransition(rotation: Int): Int = when (rotation) {
-        Surface.ROTATION_0 -> R.raw.biometricprompt_landscape_base
-        Surface.ROTATION_90 -> R.raw.biometricprompt_portrait_base_topleft
-        Surface.ROTATION_180 -> R.raw.biometricprompt_landscape_base
-        Surface.ROTATION_270 -> R.raw.biometricprompt_portrait_base_bottomright
-        else -> R.raw.biometricprompt_landscape_base
+        Surface.ROTATION_90 -> if (isDeviceFolded) {
+            R.raw.biometricprompt_folded_base_topleft
+        } else {
+            R.raw.biometricprompt_portrait_base_topleft
+        }
+        Surface.ROTATION_270 -> if (isDeviceFolded) {
+            R.raw.biometricprompt_folded_base_bottomright
+        } else {
+            R.raw.biometricprompt_portrait_base_bottomright
+        }
+        else -> if (isDeviceFolded) {
+            R.raw.biometricprompt_folded_base_default
+        } else {
+            R.raw.biometricprompt_landscape_base
+        }
     }
 
     @RawRes
@@ -273,4 +294,8 @@
         }
         else -> null
     }
+
+    override fun onFoldUpdated(isFolded: Boolean) {
+        isDeviceFolded = isFolded
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt
index 15f487b..b3b6fa2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.DrawableRes
 import android.content.Context
+import android.content.res.Configuration
 import android.graphics.drawable.Animatable2
 import android.graphics.drawable.AnimatedVectorDrawable
 import android.graphics.drawable.Drawable
@@ -91,4 +92,6 @@
 
     /** Called during [onAnimationEnd] if the controller is not [deactivated]. */
     open fun handleAnimationEnd(drawable: Drawable) {}
+
+    open fun onConfigurationChanged(newConfig: Configuration) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 0ac71c4..e12c170 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -27,6 +27,7 @@
 import android.annotation.Nullable;
 import android.annotation.StringRes;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.PromptInfo;
@@ -654,6 +655,12 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mIconController.onConfigurationChanged(newConfig);
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 3e796cd0..815ac68 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,28 +118,30 @@
     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;
     private final ScrollView mBiometricScrollView;
     private final View mPanelView;
     private final float mTranslationY;
-    @ContainerState private int mContainerState = STATE_UNKNOWN;
+    @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
     private final @Background DelayableExecutor mBackgroundExecutor;
-    private int mOrientation;
-    private boolean mSkipFirstLostFocus = false;
+    private boolean mIsOrientationChanged = false;
 
     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
     @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
@@ -229,11 +237,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 +281,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 +334,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 +351,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 +362,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 +454,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 +472,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);
     }
 
@@ -444,6 +490,7 @@
     @Override
     public void onOrientationChanged() {
         maybeUpdatePositionForUdfps(true /* invalidate */);
+        mIsOrientationChanged = true;
     }
 
     @Override
@@ -452,8 +499,8 @@
         if (!hasWindowFocus) {
             //it's a workaround to avoid closing BP incorrectly
             //BP gets a onWindowFocusChanged(false) and then gets a onWindowFocusChanged(true)
-            if (mSkipFirstLostFocus) {
-                mSkipFirstLostFocus = false;
+            if (mIsOrientationChanged) {
+                mIsOrientationChanged = false;
                 return;
             }
             Log.v(TAG, "Lost window focus, dismissing the dialog");
@@ -465,9 +512,6 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
-        //save the first orientation
-        mOrientation = getResources().getConfiguration().orientation;
-
         mWakefulnessLifecycle.addObserver(this);
 
         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
@@ -623,18 +667,32 @@
         }
 
         if (savedState != null) {
-            mSkipFirstLostFocus = savedState.getBoolean(
+            mIsOrientationChanged = savedState.getBoolean(
                     AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED);
         }
 
         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
     }
 
+    private void forceExecuteAnimatedIn() {
+        if (mContainerState == STATE_ANIMATING_IN) {
+            //clear all animators
+            if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+                mCredentialView.animate().cancel();
+            }
+            mPanelView.animate().cancel();
+            mBiometricView.animate().cancel();
+            animate().cancel();
+            onDialogAnimatedIn();
+        }
+    }
+
     @Override
     public void dismissWithoutCallback(boolean animate) {
         if (animate) {
             animateAway(false /* sendReason */, 0 /* reason */);
         } else {
+            forceExecuteAnimatedIn();
             removeWindowIfAttached();
         }
     }
@@ -703,9 +761,7 @@
                 mBiometricView != null && mCredentialView == null);
         outState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, mCredentialView != null);
 
-        if (mOrientation != getResources().getConfiguration().orientation) {
-            outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, true);
-        }
+        outState.putBoolean(AuthDialog.KEY_BIOMETRIC_ORIENTATION_CHANGED, mIsOrientationChanged);
 
         if (mBiometricView != null) {
             mBiometricView.onSaveState(outState);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 242a598..a7519cf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -52,7 +52,7 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -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;
@@ -120,7 +122,11 @@
     @Nullable private final FingerprintManager mFingerprintManager;
     @Nullable private final FaceManager mFaceManager;
     private final Provider<UdfpsController> mUdfpsControllerFactory;
-    private final Provider<SidefpsController> mSidefpsControllerFactory;
+    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;
@@ -141,8 +147,8 @@
     @NonNull private final WindowManager mWindowManager;
     @NonNull private final DisplayManager mDisplayManager;
     @Nullable private UdfpsController mUdfpsController;
-    @Nullable private IUdfpsHbmListener mUdfpsHbmListener;
-    @Nullable private SidefpsController mSidefpsController;
+    @Nullable private IUdfpsRefreshRateRequestCallback mUdfpsRefreshRateRequestCallback;
+    @Nullable private SideFpsController mSideFpsController;
     @Nullable private IBiometricContextListener mBiometricContextListener;
     @Nullable private UdfpsLogger mUdfpsLogger;
     @VisibleForTesting IBiometricSysuiReceiver mReceiver;
@@ -153,6 +159,8 @@
     @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
 
     @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
+    @NonNull private final SparseBooleanArray mFaceEnrolledForUser;
+    @NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
     @NonNull private final SensorPrivacyManager mSensorPrivacyManager;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private boolean mAllFingerprintAuthenticatorsRegistered;
@@ -298,7 +306,7 @@
 
         mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null;
         if (mSidefpsProps != null) {
-            mSidefpsController = mSidefpsControllerFactory.get();
+            mSideFpsController = mSidefpsControllerFactory.get();
         }
 
         mFingerprintManager.registerBiometricStateListener(new BiometricStateListener() {
@@ -349,6 +357,24 @@
                 }
             }
         }
+        if (mFaceProps == null) {
+            Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null");
+        } else {
+            for (FaceSensorPropertiesInternal prop : mFaceProps) {
+                if (prop.sensorId == sensorId) {
+                    mFaceEnrolledForUser.put(userId, hasEnrollments);
+                }
+            }
+        }
+        if (mSidefpsProps == null) {
+            Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null");
+        } else {
+            for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) {
+                if (prop.sensorId == sensorId) {
+                    mSfpsEnrolledForUser.put(userId, hasEnrollments);
+                }
+            }
+        }
         for (Callback cb : mCallbacks) {
             cb.onEnrollmentsChanged(modality);
         }
@@ -652,17 +678,6 @@
         mUdfpsController.onAodInterrupt(screenX, screenY, major, minor);
     }
 
-    /**
-     * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps
-     * sensor area even if the user hasn't explicitly lifted their finger yet.
-     */
-    public void onCancelUdfps() {
-        if (mUdfpsController == null) {
-            return;
-        }
-        mUdfpsController.onCancelUdfps();
-    }
-
     private void sendResultAndCleanUp(@DismissedReason int reason,
             @Nullable byte[] credentialAttestation) {
         if (mReceiver == null) {
@@ -687,13 +702,15 @@
             @Nullable FingerprintManager fingerprintManager,
             @Nullable FaceManager faceManager,
             Provider<UdfpsController> udfpsControllerFactory,
-            Provider<SidefpsController> sidefpsControllerFactory,
+            Provider<SideFpsController> sidefpsControllerFactory,
             @NonNull DisplayManager displayManager,
             @NonNull WakefulnessLifecycle wakefulnessLifecycle,
             @NonNull UserManager userManager,
             @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,
@@ -715,8 +732,13 @@
         mWindowManager = windowManager;
         mInteractionJankMonitor = jankMonitor;
         mUdfpsEnrolledForUser = new SparseBooleanArray();
+        mSfpsEnrolledForUser = new SparseBooleanArray();
+        mFaceEnrolledForUser = new SparseBooleanArray();
         mVibratorHelper = vibrator;
 
+        mBiometricPromptInteractor = biometricPromptInteractor;
+        mCredentialViewModelProvider = credentialViewModelProvider;
+
         mOrientationListener = new BiometricDisplayListener(
                 context,
                 mDisplayManager,
@@ -788,13 +810,26 @@
     private void updateUdfpsLocation() {
         if (mUdfpsController != null) {
             final FingerprintSensorPropertiesInternal udfpsProp = mUdfpsProps.get(0);
+
             final Rect previousUdfpsBounds = mUdfpsBounds;
             mUdfpsBounds = udfpsProp.getLocation().getRect();
             mUdfpsBounds.scale(mScaleFactor);
-            mUdfpsController.updateOverlayParams(udfpsProp.sensorId,
-                    new UdfpsOverlayParams(mUdfpsBounds, mCachedDisplayInfo.getNaturalWidth(),
-                            mCachedDisplayInfo.getNaturalHeight(), mScaleFactor,
-                            mCachedDisplayInfo.rotation));
+
+            final Rect overlayBounds = new Rect(
+                    0, /* left */
+                    mCachedDisplayInfo.getNaturalHeight() / 2, /* top */
+                    mCachedDisplayInfo.getNaturalWidth(), /* right */
+                    mCachedDisplayInfo.getNaturalHeight() /* botom */);
+
+            final UdfpsOverlayParams overlayParams = new UdfpsOverlayParams(
+                    mUdfpsBounds,
+                    overlayBounds,
+                    mCachedDisplayInfo.getNaturalWidth(),
+                    mCachedDisplayInfo.getNaturalHeight(),
+                    mScaleFactor,
+                    mCachedDisplayInfo.rotation);
+
+            mUdfpsController.updateOverlayParams(udfpsProp, overlayParams);
             if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds)) {
                 for (Callback cb : mCallbacks) {
                     cb.onUdfpsLocationChanged();
@@ -857,21 +892,22 @@
     }
 
     /**
-     * Stores the listener received from {@link com.android.server.display.DisplayModeDirector}.
+     * Stores the callback received from {@link com.android.server.display.DisplayModeDirector}.
      *
-     * DisplayModeDirector implements {@link IUdfpsHbmListener} and registers it with this class by
-     * calling {@link CommandQueue#setUdfpsHbmListener(IUdfpsHbmListener)}.
+     * DisplayModeDirector implements {@link IUdfpsRefreshRateRequestCallback}
+     * and registers it with this class by calling
+     * {@link CommandQueue#setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback)}.
      */
     @Override
-    public void setUdfpsHbmListener(IUdfpsHbmListener listener) {
-        mUdfpsHbmListener = listener;
+    public void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback) {
+        mUdfpsRefreshRateRequestCallback = callback;
     }
 
     /**
-     * @return IUdfpsHbmListener that can be set by DisplayModeDirector.
+     * @return IUdfpsRefreshRateRequestCallback that can be set by DisplayModeDirector.
      */
-    @Nullable public IUdfpsHbmListener getUdfpsHbmListener() {
-        return mUdfpsHbmListener;
+    @Nullable public IUdfpsRefreshRateRequestCallback getUdfpsRefreshRateCallback() {
+        return mUdfpsRefreshRateRequestCallback;
     }
 
     @Override
@@ -949,6 +985,11 @@
         return mUdfpsProps;
     }
 
+    @Nullable
+    public List<FingerprintSensorPropertiesInternal> getSfpsProps() {
+        return mSidefpsProps;
+    }
+
     private String getErrorString(@Modality int modality, int error, int vendorCode) {
         switch (modality) {
             case TYPE_FACE:
@@ -1017,8 +1058,6 @@
         } else {
             Log.w(TAG, "onBiometricError callback but dialog is gone");
         }
-
-        onCancelUdfps();
     }
 
     @Override
@@ -1063,7 +1102,7 @@
             return false;
         }
 
-        return mFaceManager.hasEnrolledTemplates(userId);
+        return mFaceEnrolledForUser.get(userId);
     }
 
     /**
@@ -1077,6 +1116,22 @@
         return mUdfpsEnrolledForUser.get(userId);
     }
 
+    /**
+     * Whether the passed userId has enrolled SFPS.
+     */
+    public boolean isSfpsEnrolled(int userId) {
+        if (mSideFpsController == null) {
+            return false;
+        }
+
+        return mSfpsEnrolledForUser.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;
 
@@ -1208,7 +1263,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 5ed8986..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
+++ /dev/null
@@ -1,210 +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.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 || mPasswordField == null || mErrorView == null) {
-            return;
-        }
-
-        // b/157910732 In AuthContainerView#getLayoutParams() we used to prevent jank risk when
-        // resizing by IME show or hide, we used to setFitInsetsTypes `~WindowInsets.Type.ime()` to
-        // LP. As a result this view needs to listen onApplyWindowInsets() and handle onLayout.
-        int inputLeftBound;
-        int inputTopBound;
-        int headerRightBound = right;
-        if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            inputTopBound = (bottom - (mPasswordField.getHeight() + mErrorView.getHeight())) / 2;
-            inputLeftBound = (right - left) / 2;
-            headerRightBound = inputLeftBound;
-        } else {
-            inputTopBound = mSubtitleView.getBottom() + (bottom - mSubtitleView.getBottom()) / 2;
-            inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2;
-        }
-
-        mAuthCredentialHeader.layout(left, top, headerRightBound, bottom);
-        mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset;
-
-        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), newHeight);
-
-        measureChildren(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.AT_MOST));
-    }
-
-    @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;
-            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 11498db..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
+++ /dev/null
@@ -1,111 +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();
-        mLockPatternView.setEnabled(true);
-    }
-
-    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 d4176ac..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;
-
-    private final AccessibilityManager mAccessibilityManager;
-    private final UserManager mUserManager;
-    private final DevicePolicyManager mDevicePolicyManager;
-
-    private PromptInfo mPromptInfo;
-    private AuthPanelController mPanelController;
-    private boolean mShouldAnimatePanel;
-    private boolean mShouldAnimateContents;
-
-    private TextView mTitleView;
-    protected TextView mSubtitleView;
-    private TextView mDescriptionView;
-    private 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/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index c93fe6a..4b57d45 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -29,7 +29,7 @@
 import android.view.animation.PathInterpolator
 import com.android.internal.graphics.ColorUtils
 import com.android.systemui.animation.Interpolators
-import com.android.systemui.ripple.RippleShader
+import com.android.systemui.surfaceeffects.ripple.RippleShader
 
 private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
new file mode 100644
index 0000000..1c3dd45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -0,0 +1,397 @@
+/*
+ * 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.
+ */
+package com.android.systemui.biometrics
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.ActivityTaskManager
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
+import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.display.DisplayManager
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.hardware.fingerprint.ISidefpsController
+import android.os.Handler
+import android.util.Log
+import android.util.RotationUtils
+import android.view.Display
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.View
+import android.view.View.AccessibilityDelegate
+import android.view.ViewPropertyAnimator
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RawRes
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+private const val TAG = "SideFpsController"
+
+/**
+ * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
+ */
+@SysUISingleton
+class SideFpsController
+@Inject
+constructor(
+    private val context: Context,
+    private val layoutInflater: LayoutInflater,
+    fingerprintManager: FingerprintManager?,
+    private val windowManager: WindowManager,
+    private val activityTaskManager: ActivityTaskManager,
+    overviewProxyService: OverviewProxyService,
+    displayManager: DisplayManager,
+    @Main private val mainExecutor: DelayableExecutor,
+    @Main private val handler: Handler,
+    dumpManager: DumpManager
+) : Dumpable {
+    val requests: HashSet<SideFpsUiRequestSource> = HashSet()
+
+    @VisibleForTesting
+    val sensorProps: FingerprintSensorPropertiesInternal =
+        fingerprintManager?.sideFpsSensorProperties
+            ?: throw IllegalStateException("no side fingerprint sensor")
+
+    @VisibleForTesting
+    val orientationListener =
+        BiometricDisplayListener(
+            context,
+            displayManager,
+            handler,
+            BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
+        ) { onOrientationChanged() }
+
+    @VisibleForTesting
+    val overviewProxyListener =
+        object : OverviewProxyService.OverviewProxyListener {
+            override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
+                overlayView?.let { view ->
+                    handler.postDelayed({ updateOverlayVisibility(view) }, 500)
+                }
+            }
+        }
+
+    private val animationDuration =
+        context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
+
+    private var overlayHideAnimator: ViewPropertyAnimator? = null
+
+    private var overlayView: View? = null
+        set(value) {
+            field?.let { oldView ->
+                windowManager.removeView(oldView)
+                orientationListener.disable()
+            }
+            overlayHideAnimator?.cancel()
+            overlayHideAnimator = null
+
+            field = value
+            field?.let { newView ->
+                windowManager.addView(newView, overlayViewParams)
+                updateOverlayVisibility(newView)
+                orientationListener.enable()
+            }
+        }
+    @VisibleForTesting
+    internal var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT
+
+    private val overlayViewParams =
+        WindowManager.LayoutParams(
+                WindowManager.LayoutParams.WRAP_CONTENT,
+                WindowManager.LayoutParams.WRAP_CONTENT,
+                WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
+                Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
+                PixelFormat.TRANSLUCENT
+            )
+            .apply {
+                title = TAG
+                fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
+                gravity = Gravity.TOP or Gravity.LEFT
+                layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
+            }
+
+    init {
+        fingerprintManager?.setSidefpsController(
+            object : ISidefpsController.Stub() {
+                override fun show(
+                    sensorId: Int,
+                    @BiometricOverlayConstants.ShowReason reason: Int
+                ) =
+                    if (reason.isReasonToAutoShow(activityTaskManager)) {
+                        show(SideFpsUiRequestSource.AUTO_SHOW)
+                    } else {
+                        hide(SideFpsUiRequestSource.AUTO_SHOW)
+                    }
+
+                override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW)
+            }
+        )
+        overviewProxyService.addCallback(overviewProxyListener)
+        dumpManager.registerDumpable(this)
+    }
+
+    /** Shows the side fps overlay if not already shown. */
+    fun show(request: SideFpsUiRequestSource) {
+        requests.add(request)
+        mainExecutor.execute {
+            if (overlayView == null) {
+                createOverlayForDisplay()
+            } else {
+                Log.v(TAG, "overlay already shown")
+            }
+        }
+    }
+
+    /** Hides the fps overlay if shown. */
+    fun hide(request: SideFpsUiRequestSource) {
+        requests.remove(request)
+        mainExecutor.execute {
+            if (requests.isEmpty()) {
+                overlayView = null
+            }
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("requests:")
+        for (requestSource in requests) {
+            pw.println("     $requestSource.name")
+        }
+    }
+
+    private fun onOrientationChanged() {
+        if (overlayView != null) {
+            createOverlayForDisplay()
+        }
+    }
+
+    private fun createOverlayForDisplay() {
+        val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
+        overlayView = view
+        val display = context.display!!
+        val offsets =
+            sensorProps.getLocation(display.uniqueId).let { location ->
+                if (location == null) {
+                    Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+                }
+                location ?: sensorProps.location
+            }
+        overlayOffsets = offsets
+
+        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+        view.rotation = display.asSideFpsAnimationRotation(offsets.isYAligned())
+        lottie.setAnimation(display.asSideFpsAnimation(offsets.isYAligned()))
+        lottie.addLottieOnCompositionLoadedListener {
+            // Check that view is not stale, and that overlayView has not been hidden/removed
+            if (overlayView != null && overlayView == view) {
+                updateOverlayParams(display, it.bounds)
+            }
+        }
+        lottie.addOverlayDynamicColor(context)
+
+        /**
+         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
+         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
+         * in focus
+         */
+        view.setAccessibilityDelegate(
+            object : AccessibilityDelegate() {
+                override fun dispatchPopulateAccessibilityEvent(
+                    host: View,
+                    event: AccessibilityEvent
+                ): Boolean {
+                    return if (
+                        event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+                    ) {
+                        true
+                    } else {
+                        super.dispatchPopulateAccessibilityEvent(host, event)
+                    }
+                }
+            }
+        )
+    }
+
+    @VisibleForTesting
+    internal fun updateOverlayParams(display: Display, bounds: Rect) {
+        val isNaturalOrientation = display.isNaturalOrientation()
+        val size = windowManager.maximumWindowMetrics.bounds
+        val displayWidth = if (isNaturalOrientation) size.width() else size.height()
+        val displayHeight = if (isNaturalOrientation) size.height() else size.width()
+        val boundsWidth = if (isNaturalOrientation) bounds.width() else bounds.height()
+        val boundsHeight = if (isNaturalOrientation) bounds.height() else bounds.width()
+        val sensorBounds =
+            if (overlayOffsets.isYAligned()) {
+                Rect(
+                    displayWidth - boundsWidth,
+                    overlayOffsets.sensorLocationY,
+                    displayWidth,
+                    overlayOffsets.sensorLocationY + boundsHeight
+                )
+            } else {
+                Rect(
+                    overlayOffsets.sensorLocationX,
+                    0,
+                    overlayOffsets.sensorLocationX + boundsWidth,
+                    boundsHeight
+                )
+            }
+
+        RotationUtils.rotateBounds(
+            sensorBounds,
+            Rect(0, 0, displayWidth, displayHeight),
+            display.rotation
+        )
+
+        overlayViewParams.x = sensorBounds.left
+        overlayViewParams.y = sensorBounds.top
+        windowManager.updateViewLayout(overlayView, overlayViewParams)
+    }
+
+    private fun updateOverlayVisibility(view: View) {
+        if (view != overlayView) {
+            return
+        }
+        // hide after a few seconds if the sensor is oriented down and there are
+        // large overlapping system bars
+        val rotation = context.display?.rotation
+        if (
+            windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar() &&
+                ((rotation == Surface.ROTATION_270 && overlayOffsets.isYAligned()) ||
+                    (rotation == Surface.ROTATION_180 && !overlayOffsets.isYAligned()))
+        ) {
+            overlayHideAnimator =
+                view
+                    .animate()
+                    .alpha(0f)
+                    .setStartDelay(3_000)
+                    .setDuration(animationDuration)
+                    .setListener(
+                        object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator) {
+                                view.visibility = View.GONE
+                                overlayHideAnimator = null
+                            }
+                        }
+                    )
+        } else {
+            overlayHideAnimator?.cancel()
+            overlayHideAnimator = null
+            view.alpha = 1f
+            view.visibility = View.VISIBLE
+        }
+    }
+}
+
+private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
+    get() = this?.sensorPropertiesInternal?.firstOrNull { it.isAnySidefpsType }
+
+/** Returns [True] when the device has a side fingerprint sensor. */
+fun FingerprintManager?.hasSideFpsSensor(): Boolean = this?.sideFpsSensorProperties != null
+
+@BiometricOverlayConstants.ShowReason
+private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Boolean =
+    when (this) {
+        REASON_AUTH_KEYGUARD -> false
+        REASON_AUTH_SETTINGS ->
+            when (activityTaskManager.topClass()) {
+                // TODO(b/186176653): exclude fingerprint overlays from this list view
+                "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
+                else -> true
+            }
+        else -> true
+    }
+
+private fun ActivityTaskManager.topClass(): String =
+    getTasks(1).firstOrNull()?.topActivity?.className ?: ""
+
+@RawRes
+private fun Display.asSideFpsAnimation(yAligned: Boolean): Int =
+    when (rotation) {
+        Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+        Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+        else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
+    }
+
+private fun Display.asSideFpsAnimationRotation(yAligned: Boolean): Float =
+    when (rotation) {
+        Surface.ROTATION_90 -> if (yAligned) 0f else 180f
+        Surface.ROTATION_180 -> 180f
+        Surface.ROTATION_270 -> if (yAligned) 180f else 0f
+        else -> 0f
+    }
+
+private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
+
+private fun Display.isNaturalOrientation(): Boolean =
+    rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+
+private fun WindowInsets.hasBigNavigationBar(): Boolean =
+    getInsets(WindowInsets.Type.navigationBars()).bottom >= 70
+
+private fun LottieAnimationView.addOverlayDynamicColor(context: Context) {
+    fun update() {
+        val c = context.getColor(R.color.biometric_dialog_accent)
+        for (key in listOf(".blue600", ".blue400")) {
+            addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+                PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP)
+            }
+        }
+    }
+
+    if (composition != null) {
+        update()
+    } else {
+        addLottieOnCompositionLoadedListener { update() }
+    }
+}
+
+/**
+ * The source of a request to show the side fps visual indicator. This is distinct from
+ * [BiometricOverlayConstants] which corrresponds with the reason fingerprint authentication is
+ * requested.
+ */
+enum class SideFpsUiRequestSource {
+    /** see [isReasonToAutoShow] */
+    AUTO_SHOW,
+    /** Pin, pattern or password bouncer */
+    PRIMARY_BOUNCER,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
deleted file mode 100644
index d03106b..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
+++ /dev/null
@@ -1,337 +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.
- */
-package com.android.systemui.biometrics
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.app.ActivityTaskManager
-import android.content.Context
-import android.graphics.PixelFormat
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManager
-import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
-import android.hardware.fingerprint.ISidefpsController
-import android.os.Handler
-import android.util.Log
-import android.util.RotationUtils
-import android.view.Display
-import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.Surface
-import android.view.View
-import android.view.View.AccessibilityDelegate
-import android.view.ViewPropertyAnimator
-import android.view.WindowInsets
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
-import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
-import android.view.accessibility.AccessibilityEvent
-import androidx.annotation.RawRes
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.recents.OverviewProxyService
-import com.android.systemui.util.concurrency.DelayableExecutor
-import javax.inject.Inject
-
-private const val TAG = "SidefpsController"
-
-/**
- * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
- */
-@SysUISingleton
-class SidefpsController @Inject constructor(
-    private val context: Context,
-    private val layoutInflater: LayoutInflater,
-    fingerprintManager: FingerprintManager?,
-    private val windowManager: WindowManager,
-    private val activityTaskManager: ActivityTaskManager,
-    overviewProxyService: OverviewProxyService,
-    displayManager: DisplayManager,
-    @Main private val mainExecutor: DelayableExecutor,
-    @Main private val handler: Handler
-) {
-    @VisibleForTesting
-    val sensorProps: FingerprintSensorPropertiesInternal = fingerprintManager
-        ?.sideFpsSensorProperties
-        ?: throw IllegalStateException("no side fingerprint sensor")
-
-    @VisibleForTesting
-    val orientationListener = BiometricDisplayListener(
-        context,
-        displayManager,
-        handler,
-        BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
-    ) { onOrientationChanged() }
-
-    @VisibleForTesting
-    val overviewProxyListener = object : OverviewProxyService.OverviewProxyListener {
-        override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
-            overlayView?.let { view ->
-                handler.postDelayed({ updateOverlayVisibility(view) }, 500)
-            }
-        }
-    }
-
-    private val animationDuration =
-        context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
-
-    private var overlayHideAnimator: ViewPropertyAnimator? = null
-
-    private var overlayView: View? = null
-        set(value) {
-            field?.let { oldView ->
-                windowManager.removeView(oldView)
-                orientationListener.disable()
-            }
-            overlayHideAnimator?.cancel()
-            overlayHideAnimator = null
-
-            field = value
-            field?.let { newView ->
-                windowManager.addView(newView, overlayViewParams)
-                updateOverlayVisibility(newView)
-                orientationListener.enable()
-            }
-        }
-    @VisibleForTesting
-    internal var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT
-
-    private val overlayViewParams = WindowManager.LayoutParams(
-        WindowManager.LayoutParams.WRAP_CONTENT,
-        WindowManager.LayoutParams.WRAP_CONTENT,
-        WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
-        Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
-        PixelFormat.TRANSLUCENT
-    ).apply {
-        title = TAG
-        fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
-        gravity = Gravity.TOP or Gravity.LEFT
-        layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-        privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
-    }
-
-    init {
-        fingerprintManager?.setSidefpsController(
-            object : ISidefpsController.Stub() {
-                override fun show(
-                    sensorId: Int,
-                    @BiometricOverlayConstants.ShowReason reason: Int
-                ) = if (reason.isReasonToShow(activityTaskManager)) show() else hide()
-
-                override fun hide(sensorId: Int) = hide()
-            })
-        overviewProxyService.addCallback(overviewProxyListener)
-    }
-
-    /** Shows the side fps overlay if not already shown. */
-    fun show() {
-        mainExecutor.execute {
-            if (overlayView == null) {
-                createOverlayForDisplay()
-            } else {
-                Log.v(TAG, "overlay already shown")
-            }
-        }
-    }
-
-    /** Hides the fps overlay if shown. */
-    fun hide() {
-        mainExecutor.execute { overlayView = null }
-    }
-
-    private fun onOrientationChanged() {
-        if (overlayView != null) {
-            createOverlayForDisplay()
-        }
-    }
-
-    private fun createOverlayForDisplay() {
-        val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
-        overlayView = view
-        val display = context.display!!
-        val offsets = sensorProps.getLocation(display.uniqueId).let { location ->
-            if (location == null) {
-                Log.w(TAG, "No location specified for display: ${display.uniqueId}")
-            }
-            location ?: sensorProps.location
-        }
-        overlayOffsets = offsets
-
-        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
-        view.rotation = display.asSideFpsAnimationRotation(offsets.isYAligned())
-        lottie.setAnimation(display.asSideFpsAnimation(offsets.isYAligned()))
-        lottie.addLottieOnCompositionLoadedListener {
-            // Check that view is not stale, and that overlayView has not been hidden/removed
-            if (overlayView != null && overlayView == view) {
-                updateOverlayParams(display, it.bounds)
-            }
-        }
-        lottie.addOverlayDynamicColor(context)
-
-        /**
-         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
-         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator
-         * is in focus
-         */
-        view.setAccessibilityDelegate(object : AccessibilityDelegate() {
-            override fun dispatchPopulateAccessibilityEvent(
-                host: View,
-                event: AccessibilityEvent
-            ): Boolean {
-                return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
-                    true
-                } else {
-                    super.dispatchPopulateAccessibilityEvent(host, event)
-                }
-            }
-        })
-    }
-
-    @VisibleForTesting
-    internal fun updateOverlayParams(display: Display, bounds: Rect) {
-        val isNaturalOrientation = display.isNaturalOrientation()
-        val size = windowManager.maximumWindowMetrics.bounds
-        val displayWidth = if (isNaturalOrientation) size.width() else size.height()
-        val displayHeight = if (isNaturalOrientation) size.height() else size.width()
-        val boundsWidth = if (isNaturalOrientation) bounds.width() else bounds.height()
-        val boundsHeight = if (isNaturalOrientation) bounds.height() else bounds.width()
-        val sensorBounds = if (overlayOffsets.isYAligned()) {
-            Rect(
-                displayWidth - boundsWidth,
-                overlayOffsets.sensorLocationY,
-                displayWidth,
-                overlayOffsets.sensorLocationY + boundsHeight
-            )
-        } else {
-            Rect(
-                overlayOffsets.sensorLocationX,
-                0,
-                overlayOffsets.sensorLocationX + boundsWidth,
-                boundsHeight
-            )
-        }
-
-        RotationUtils.rotateBounds(
-            sensorBounds,
-            Rect(0, 0, displayWidth, displayHeight),
-            display.rotation
-        )
-
-        overlayViewParams.x = sensorBounds.left
-        overlayViewParams.y = sensorBounds.top
-        windowManager.updateViewLayout(overlayView, overlayViewParams)
-    }
-
-    private fun updateOverlayVisibility(view: View) {
-        if (view != overlayView) {
-            return
-        }
-        // hide after a few seconds if the sensor is oriented down and there are
-        // large overlapping system bars
-        val rotation = context.display?.rotation
-        if (windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar() &&
-            ((rotation == Surface.ROTATION_270 && overlayOffsets.isYAligned()) ||
-                    (rotation == Surface.ROTATION_180 && !overlayOffsets.isYAligned()))) {
-            overlayHideAnimator = view.animate()
-                .alpha(0f)
-                .setStartDelay(3_000)
-                .setDuration(animationDuration)
-                .setListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        view.visibility = View.GONE
-                        overlayHideAnimator = null
-                    }
-                })
-        } else {
-            overlayHideAnimator?.cancel()
-            overlayHideAnimator = null
-            view.alpha = 1f
-            view.visibility = View.VISIBLE
-        }
-    }
-}
-
-private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
-    get() = this?.sensorPropertiesInternal?.firstOrNull { it.isAnySidefpsType }
-
-/** Returns [True] when the device has a side fingerprint sensor. */
-fun FingerprintManager?.hasSideFpsSensor(): Boolean = this?.sideFpsSensorProperties != null
-
-@BiometricOverlayConstants.ShowReason
-private fun Int.isReasonToShow(activityTaskManager: ActivityTaskManager): Boolean = when (this) {
-    REASON_AUTH_KEYGUARD -> false
-    REASON_AUTH_SETTINGS -> when (activityTaskManager.topClass()) {
-        // TODO(b/186176653): exclude fingerprint overlays from this list view
-        "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
-        else -> true
-    }
-    else -> true
-}
-
-private fun ActivityTaskManager.topClass(): String =
-    getTasks(1).firstOrNull()?.topActivity?.className ?: ""
-
-@RawRes
-private fun Display.asSideFpsAnimation(yAligned: Boolean): Int = when (rotation) {
-    Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
-    Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
-    else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
-}
-
-private fun Display.asSideFpsAnimationRotation(yAligned: Boolean): Float = when (rotation) {
-    Surface.ROTATION_90 -> if (yAligned) 0f else 180f
-    Surface.ROTATION_180 -> 180f
-    Surface.ROTATION_270 -> if (yAligned) 180f else 0f
-    else -> 0f
-}
-
-private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
-
-private fun Display.isNaturalOrientation(): Boolean =
-    rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
-private fun WindowInsets.hasBigNavigationBar(): Boolean =
-    getInsets(WindowInsets.Type.navigationBars()).bottom >= 70
-
-private fun LottieAnimationView.addOverlayDynamicColor(context: Context) {
-    fun update() {
-        val c = context.getColor(R.color.biometric_dialog_accent)
-        for (key in listOf(".blue600", ".blue400")) {
-            addValueCallback(
-                KeyPath(key, "**"),
-                LottieProperty.COLOR_FILTER
-            ) { PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP) }
-        }
-    }
-
-    if (composition != null) {
-        update()
-    } else {
-        addLottieOnCompositionLoadedListener { update() }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
new file mode 100644
index 0000000..794eba4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
@@ -0,0 +1,16 @@
+{
+  "presubmit": [
+    {
+      // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
+      "name": "SystemUIGoogleBiometricsScreenshotTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java
index ad96612..bdad413 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java
@@ -37,6 +37,9 @@
     private float mDialogSuggestedAlpha = 1f;
     private float mNotificationShadeExpansion = 0f;
 
+    // Used for Udfps ellipse detection when flag is true, set by AnimationViewController
+    boolean mUseExpandedOverlay = false;
+
     // mAlpha takes into consideration the status bar expansion amount and dialog suggested alpha
     private int mAlpha;
     boolean mPauseAuth;
@@ -118,6 +121,24 @@
     }
 
     /**
+     * Converts coordinates of RectF relative to the screen to coordinates relative to this view.
+     *
+     * @param bounds RectF based off screen coordinates in current orientation
+     */
+    RectF getBoundsRelativeToView(RectF bounds) {
+        int[] pos = getLocationOnScreen();
+
+        RectF output = new RectF(
+                bounds.left - pos[0],
+                bounds.top - pos[1],
+                bounds.right - pos[0],
+                bounds.bottom - pos[1]
+        );
+
+        return output;
+    }
+
+    /**
      * Set the suggested alpha based on whether a dialog was recently shown or hidden.
      * @param dialogSuggestedAlpha value from 0f to 1f.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 0f5a99c..1d4281f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -23,16 +23,18 @@
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
 import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.SensorProperties;
 import android.hardware.display.DisplayManager;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
 import android.os.Handler;
@@ -50,17 +52,24 @@
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.biometrics.dagger.BiometricsBackground;
 import com.android.systemui.dagger.SysUISingleton;
 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.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -75,6 +84,8 @@
 import com.android.systemui.util.concurrency.Execution;
 import com.android.systemui.util.time.SystemClock;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Optional;
 import java.util.Set;
@@ -97,7 +108,7 @@
  */
 @SuppressWarnings("deprecation")
 @SysUISingleton
-public class UdfpsController implements DozeReceiver {
+public class UdfpsController implements DozeReceiver, Dumpable {
     private static final String TAG = "UdfpsController";
     private static final long AOD_INTERRUPT_TIMEOUT_MILLIS = 1000;
 
@@ -119,6 +130,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;
@@ -130,10 +142,11 @@
     @NonNull private final LatencyTracker mLatencyTracker;
     @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
     @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
+    @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
-    @VisibleForTesting int mSensorId;
+    @VisibleForTesting @NonNull FingerprintSensorPropertiesInternal mSensorProps;
     @VisibleForTesting @NonNull UdfpsOverlayParams mOverlayParams = new UdfpsOverlayParams();
     // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this.
     @Nullable private Runnable mAuthControllerUpdateUdfpsLocation;
@@ -153,6 +166,7 @@
 
     // The current request from FingerprintService. Null if no current request.
     @Nullable UdfpsControllerOverlay mOverlay;
+    @Nullable private UdfpsEllipseDetection mUdfpsEllipseDetection;
 
     // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when
     // to turn off high brightness mode. To get around this limitation, the state of the AOD
@@ -198,10 +212,19 @@
         }
     };
 
+    @Override
+    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("mSensorProps=(" + mSensorProps + ")");
+    }
+
     public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
         @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,
@@ -212,11 +235,16 @@
                             mUnlockedScreenOffAnimationController,
                             mUdfpsDisplayMode, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
-                                    fromUdfpsView), mActivityLaunchAnimator)));
+                                    fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
+                            mPrimaryBouncerInteractor)));
         }
 
         @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
@@ -244,7 +272,7 @@
                     }
                     mAcquiredReceived = true;
                     final UdfpsView view = mOverlay.getOverlayView();
-                    if (view != null) {
+                    if (view != null && isOptical()) {
                         unconfigureDisplay(view);
                     }
                     if (acquiredGood) {
@@ -285,31 +313,60 @@
                 mOverlay.getOverlayView().setDebugMessage(message);
             });
         }
+
+        public Rect getSensorBounds() {
+            return mOverlayParams.getSensorBounds();
+        }
+
+        /**
+         * Passes a mocked MotionEvent to OnTouch.
+         *
+         * @param event MotionEvent to simulate in onTouch
+         */
+        public void debugOnTouch(long requestId, MotionEvent event) {
+            UdfpsController.this.onTouch(requestId, event, false);
+        }
+
+        /**
+         * Debug to run onUiReady
+         */
+        public void debugOnUiReady(long requestId, int sensorId) {
+            if (UdfpsController.this.mAlternateTouchProvider != null) {
+                UdfpsController.this.mAlternateTouchProvider.onUiReady();
+            } else {
+                UdfpsController.this.mFingerprintManager.onUiReady(requestId, sensorId);
+            }
+        }
     }
 
     /**
      * Updates the overlay parameters and reconstructs or redraws the overlay, if necessary.
      *
-     * @param sensorId      sensor for which the overlay is getting updated.
+     * @param sensorProps   sensor for which the overlay is getting updated.
      * @param overlayParams See {@link UdfpsOverlayParams}.
      */
-    public void updateOverlayParams(int sensorId, @NonNull UdfpsOverlayParams overlayParams) {
-        if (sensorId != mSensorId) {
-            mSensorId = sensorId;
+    public void updateOverlayParams(@NonNull FingerprintSensorPropertiesInternal sensorProps,
+            @NonNull UdfpsOverlayParams overlayParams) {
+        if (mSensorProps.sensorId != sensorProps.sensorId) {
+            mSensorProps = sensorProps;
             Log.w(TAG, "updateUdfpsParams | sensorId has changed");
         }
 
         if (!mOverlayParams.equals(overlayParams)) {
             mOverlayParams = overlayParams;
 
-            final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateAuth();
+            if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+                mUdfpsEllipseDetection.updateOverlayParams(overlayParams);
+            }
+
+            final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer();
 
             // When the bounds change it's always necessary to re-create the overlay's window with
             // new LayoutParams. If the overlay needs to be shown, this will re-create and show the
             // overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden.
             redrawOverlay();
             if (wasShowingAltAuth) {
-                mKeyguardViewManager.showGenericBouncer(true);
+                mKeyguardViewManager.showBouncer(true);
             }
         }
     }
@@ -319,7 +376,7 @@
         mAuthControllerUpdateUdfpsLocation = r;
     }
 
-    public void setUdfpsDisplayMode(UdfpsDisplayModeProvider udfpsDisplayMode) {
+    public void setUdfpsDisplayMode(@NonNull UdfpsDisplayModeProvider udfpsDisplayMode) {
         mUdfpsDisplayMode = udfpsDisplayMode;
     }
 
@@ -423,7 +480,6 @@
         }
 
         final UdfpsView udfpsView = mOverlay.getOverlayView();
-        final boolean isDisplayConfigured = udfpsView.isDisplayConfigured();
         boolean handled = false;
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_OUTSIDE:
@@ -442,8 +498,23 @@
                     mVelocityTracker.clear();
                 }
 
-                boolean withinSensorArea =
+                boolean withinSensorArea;
+                if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+                    if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+                        // Ellipse detection
+                        withinSensorArea = mUdfpsEllipseDetection.isGoodEllipseOverlap(event);
+                    } else {
+                        // Centroid with expanded overlay
+                        withinSensorArea =
+                            isWithinSensorArea(udfpsView, event.getRawX(),
+                                        event.getRawY(), fromUdfpsView);
+                    }
+                } else {
+                    // Centroid with sensor sized view
+                    withinSensorArea =
                         isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView);
+                }
+
                 if (withinSensorArea) {
                     Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0);
                     Log.v(TAG, "onTouch | action down");
@@ -474,9 +545,25 @@
                         ? event.getPointerId(0)
                         : event.findPointerIndex(mActivePointerId);
                 if (idx == event.getActionIndex()) {
-                    boolean actionMoveWithinSensorArea =
-                            isWithinSensorArea(udfpsView, event.getX(idx), event.getY(idx),
-                                    fromUdfpsView);
+                    boolean actionMoveWithinSensorArea;
+                    if (mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+                        if (mFeatureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+                            // Ellipse detection
+                            actionMoveWithinSensorArea =
+                                    mUdfpsEllipseDetection.isGoodEllipseOverlap(event);
+                        } else {
+                            // Centroid with expanded overlay
+                            actionMoveWithinSensorArea =
+                                isWithinSensorArea(udfpsView, event.getRawX(idx),
+                                        event.getRawY(idx), fromUdfpsView);
+                        }
+                    } else {
+                        // Centroid with sensor sized view
+                        actionMoveWithinSensorArea =
+                            isWithinSensorArea(udfpsView, event.getX(idx),
+                                    event.getY(idx), fromUdfpsView);
+                    }
+
                     if ((fromUdfpsView || actionMoveWithinSensorArea)
                             && shouldTryToDismissKeyguard()) {
                         Log.v(TAG, "onTouch | dismiss keyguard ACTION_MOVE");
@@ -507,15 +594,14 @@
                                 "minor: %.1f, major: %.1f, v: %.1f, exceedsVelocityThreshold: %b",
                                 minor, major, v, exceedsVelocityThreshold);
                         final long sinceLastLog = mSystemClock.elapsedRealtime() - mTouchLogTime;
-                        if (!isDisplayConfigured && !mAcquiredReceived
-                                && !exceedsVelocityThreshold) {
 
+                        if (!mOnFingerDown && !mAcquiredReceived && !exceedsVelocityThreshold) {
                             final float scale = mOverlayParams.getScaleFactor();
                             float scaledMinor = minor / scale;
                             float scaledMajor = major / scale;
-
                             onFingerDown(requestId, scaledTouch.x, scaledTouch.y, scaledMinor,
                                     scaledMajor);
+
                             Log.v(TAG, "onTouch | finger down: " + touchInfo);
                             mTouchLogTime = mSystemClock.elapsedRealtime();
                             handled = true;
@@ -590,6 +676,7 @@
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull DumpManager dumpManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
+            @NonNull FeatureFlags featureFlags,
             @NonNull FalsingManager falsingManager,
             @NonNull PowerManager powerManager,
             @NonNull AccessibilityManager accessibilityManager,
@@ -608,7 +695,8 @@
             @NonNull LatencyTracker latencyTracker,
             @NonNull ActivityLaunchAnimator activityLaunchAnimator,
             @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider,
-            @BiometricsBackground Executor biometricsExecutor) {
+            @NonNull @BiometricsBackground Executor biometricsExecutor,
+            @NonNull PrimaryBouncerInteractor primaryBouncerInteractor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -625,6 +713,7 @@
         mDumpManager = dumpManager;
         mDialogManager = dialogManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mFeatureFlags = featureFlags;
         mFalsingManager = falsingManager;
         mPowerManager = powerManager;
         mAccessibilityManager = accessibilityManager;
@@ -637,7 +726,18 @@
         mLatencyTracker = latencyTracker;
         mActivityLaunchAnimator = activityLaunchAnimator;
         mAlternateTouchProvider = alternateTouchProvider.orElse(null);
+        mSensorProps = new FingerprintSensorPropertiesInternal(
+                -1 /* sensorId */,
+                SensorProperties.STRENGTH_CONVENIENCE,
+                0 /* maxEnrollmentsPerUser */,
+                new ArrayList<>() /* componentInfo */,
+                FingerprintSensorProperties.TYPE_UNKNOWN,
+                false /* resetLockoutRequiresHardwareAuthToken */);
+
         mBiometricExecutor = biometricsExecutor;
+        mPrimaryBouncerInteractor = primaryBouncerInteractor;
+
+        mDumpManager.registerDumpable(TAG, this);
 
         mOrientationListener = new BiometricDisplayListener(
                 context,
@@ -661,6 +761,10 @@
 
         udfpsHapticsSimulator.setUdfpsController(this);
         udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController);
+
+        if (featureFlags.isEnabled(Flags.UDFPS_ELLIPSE_DETECTION)) {
+            mUdfpsEllipseDetection = new UdfpsEllipseDetection(mOverlayParams);
+        }
     }
 
     /**
@@ -727,8 +831,8 @@
                 onFingerUp(mOverlay.getRequestId(), oldView);
             }
             final boolean removed = mOverlay.hide();
-            if (mKeyguardViewManager.isShowingAlternateAuth()) {
-                mKeyguardViewManager.resetAlternateAuth(true);
+            if (mKeyguardViewManager.isShowingAlternateBouncer()) {
+                mKeyguardViewManager.hideAlternateBouncer(true);
             }
             Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed);
         } else {
@@ -768,7 +872,7 @@
                 Log.v(TAG, "aod lock icon long-press rejected by the falsing manager.");
                 return;
             }
-            mKeyguardViewManager.showBouncer(true);
+            mKeyguardViewManager.showPrimaryBouncer(true);
 
             // play the same haptic as the LockIconViewController longpress
             mVibrator.vibrate(
@@ -788,7 +892,7 @@
             // ACTION_UP/ACTION_CANCEL,  we need to be careful about not letting the screen
             // accidentally remain in high brightness mode. As a mitigation, queue a call to
             // cancel the fingerprint scan.
-            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps,
+            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt,
                     AOD_INTERRUPT_TIMEOUT_MILLIS);
             // using a hard-coded value for major and minor until it is available from the sensor
             onFingerDown(requestId, screenX, screenY, minor, major);
@@ -815,29 +919,29 @@
     }
 
     /**
-     * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly
-     * lifts their finger. Generally, this should be called on errors in the authentication flow.
-     *
-     * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give
-     * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually.
+     * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL
+     * events, which means the fingerprint gesture created by the AOD interrupt needs to be
+     * cancelled manually.
      * This should be called when authentication either succeeds or fails. Failing to cancel the
      * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical
      * sensors, this can result in illumination persisting for longer than necessary.
      */
-    void onCancelUdfps() {
+    @VisibleForTesting
+    void cancelAodInterrupt() {
         if (!mIsAodInterruptActive) {
             return;
         }
         if (mOverlay != null && mOverlay.getOverlayView() != null) {
             onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
         }
-        if (mCancelAodTimeoutAction != null) {
-            mCancelAodTimeoutAction.run();
-            mCancelAodTimeoutAction = null;
-        }
+        mCancelAodTimeoutAction = null;
         mIsAodInterruptActive = false;
     }
 
+    private boolean isOptical() {
+        return mSensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
+    }
+
     public boolean isFingerDown() {
         return mOnFingerDown;
     }
@@ -854,7 +958,9 @@
                     + " current: " + mOverlay.getRequestId());
             return;
         }
-        mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+        if (isOptical()) {
+            mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+        }
         // Refresh screen timeout and boost process priority if possible.
         mPowerManager.userActivity(mSystemClock.uptimeMillis(),
                 PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0);
@@ -863,9 +969,7 @@
             playStartHaptic();
 
             if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) {
-                mKeyguardUpdateMonitor.requestFaceAuth(
-                        /* userInitiatedRequest */ false,
-                        FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+                mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
             }
         }
         mOnFingerDown = true;
@@ -879,11 +983,11 @@
                 }
             });
         } else {
-            mFingerprintManager.onPointerDown(requestId, mSensorId, x, y, minor, major);
+            mFingerprintManager.onPointerDown(requestId, mSensorProps.sensorId, x, y, minor, major);
         }
         Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0);
         final UdfpsView view = mOverlay.getOverlayView();
-        if (view != null) {
+        if (view != null && isOptical()) {
             view.configureDisplay(() -> {
                 if (mAlternateTouchProvider != null) {
                     mBiometricExecutor.execute(() -> {
@@ -891,7 +995,7 @@
                         mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
                     });
                 } else {
-                    mFingerprintManager.onUiReady(requestId, mSensorId);
+                    mFingerprintManager.onUiReady(requestId, mSensorProps.sensorId);
                     mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
                 }
             });
@@ -917,15 +1021,16 @@
                     }
                 });
             } else {
-                mFingerprintManager.onPointerUp(requestId, mSensorId);
+                mFingerprintManager.onPointerUp(requestId, mSensorProps.sensorId);
             }
             for (Callback cb : mCallbacks) {
                 cb.onFingerUp();
             }
         }
         mOnFingerDown = false;
-        unconfigureDisplay(view);
-
+        if (isOptical()) {
+            unconfigureDisplay(view);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 66a521c..8db4927 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -21,13 +21,18 @@
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.os.Build
 import android.os.RemoteException
+import android.provider.Settings
 import android.util.Log
 import android.util.RotationUtils
 import android.view.LayoutInflater
@@ -38,10 +43,14 @@
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
 import androidx.annotation.LayoutRes
+import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -54,41 +63,48 @@
 
 private const val TAG = "UdfpsControllerOverlay"
 
+@VisibleForTesting
+const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
+
 /**
  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
  * request. This state can persist across configuration changes via the [show] and [hide]
  * methods.
  */
 @UiThread
-class UdfpsControllerOverlay(
-    private val context: Context,
-    fingerprintManager: FingerprintManager,
-    private val inflater: LayoutInflater,
-    private val windowManager: WindowManager,
-    private val accessibilityManager: AccessibilityManager,
-    private val statusBarStateController: StatusBarStateController,
-    private val shadeExpansionStateManager: ShadeExpansionStateManager,
-    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val dialogManager: SystemUIDialogManager,
-    private val dumpManager: DumpManager,
-    private val transitionController: LockscreenShadeTransitionController,
-    private val configurationController: ConfigurationController,
-    private val systemClock: SystemClock,
-    private val keyguardStateController: KeyguardStateController,
-    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
-    private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
-    val requestId: Long,
-    @ShowReason val requestReason: Int,
-    private val controllerCallback: IUdfpsOverlayControllerCallback,
-    private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-    private val activityLaunchAnimator: ActivityLaunchAnimator
+class UdfpsControllerOverlay @JvmOverloads constructor(
+        private val context: Context,
+        fingerprintManager: FingerprintManager,
+        private val inflater: LayoutInflater,
+        private val windowManager: WindowManager,
+        private val accessibilityManager: AccessibilityManager,
+        private val statusBarStateController: StatusBarStateController,
+        private val shadeExpansionStateManager: ShadeExpansionStateManager,
+        private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val dialogManager: SystemUIDialogManager,
+        private val dumpManager: DumpManager,
+        private val transitionController: LockscreenShadeTransitionController,
+        private val configurationController: ConfigurationController,
+        private val systemClock: SystemClock,
+        private val keyguardStateController: KeyguardStateController,
+        private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+        private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+        val requestId: Long,
+        @ShowReason val requestReason: Int,
+        private val controllerCallback: IUdfpsOverlayControllerCallback,
+        private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
+        private val activityLaunchAnimator: ActivityLaunchAnimator,
+        private val featureFlags: FeatureFlags,
+        private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+        private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
         private set
 
     private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
+    private var sensorBounds: Rect = Rect()
 
     private var overlayTouchListener: TouchExplorationStateChangeListener? = null
 
@@ -102,18 +118,23 @@
         gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
         flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
-          WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
         privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
         // Avoid announcing window title.
         accessibilityTitle = " "
+
+        if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+            inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
+        }
     }
 
     /** A helper if the [requestReason] was due to enrollment. */
-    val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) {
-        UdfpsEnrollHelper(context, fingerprintManager, requestReason)
-    } else {
-        null
-    }
+    val enrollHelper: UdfpsEnrollHelper? =
+        if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
+            UdfpsEnrollHelper(context, fingerprintManager, requestReason)
+        } else {
+            null
+        }
 
     /** If the overlay is currently showing. */
     val isShowing: Boolean
@@ -129,11 +150,23 @@
 
     private var touchExplorationEnabled = false
 
+    private fun shouldRemoveEnrollmentUi(): Boolean {
+        if (isDebuggable) {
+            return Settings.Global.getInt(
+                context.contentResolver,
+                SETTING_REMOVE_ENROLLMENT_UI,
+                0 /* def */
+            ) != 0
+        }
+        return false
+    }
+
     /** Show the overlay or return false and do nothing if it is already showing. */
     @SuppressLint("ClickableViewAccessibility")
     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
         if (overlayView == null) {
             overlayParams = params
+            sensorBounds = Rect(params.sensorBounds)
             try {
                 overlayView = (inflater.inflate(
                     R.layout.udfps_view, null, false
@@ -152,6 +185,7 @@
                     }
 
                     windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
+                    sensorRect = sensorBounds
                     touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
                     overlayTouchListener = TouchExplorationStateChangeListener {
                         if (accessibilityManager.isTouchExplorationEnabled) {
@@ -168,6 +202,7 @@
                         overlayTouchListener!!
                     )
                     overlayTouchListener?.onTouchExplorationStateChanged(true)
+                    useExpandedOverlay = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
                 }
             } catch (e: RuntimeException) {
                 Log.e(TAG, "showUdfpsOverlay | failed to add window", e)
@@ -183,22 +218,34 @@
         view: UdfpsView,
         controller: UdfpsController
     ): UdfpsAnimationViewController<*>? {
-        return when (requestReason) {
+        val isEnrollment = when (requestReason) {
+            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
+            else -> false
+        }
+
+        val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
+            REASON_AUTH_OTHER
+        } else {
+            requestReason
+        }
+
+        return when (filteredRequestReason) {
             REASON_ENROLL_FIND_SENSOR,
             REASON_ENROLL_ENROLLING -> {
                 UdfpsEnrollViewController(
                     view.addUdfpsView(R.layout.udfps_enroll_view) {
-                        updateSensorLocation(overlayParams.sensorBounds)
+                        updateSensorLocation(sensorBounds)
                     },
                     enrollHelper ?: throw IllegalStateException("no enrollment helper"),
                     statusBarStateController,
                     shadeExpansionStateManager,
                     dialogManager,
                     dumpManager,
+                    featureFlags,
                     overlayParams.scaleFactor
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> {
+            REASON_AUTH_KEYGUARD -> {
                 UdfpsKeyguardViewController(
                     view.addUdfpsView(R.layout.udfps_keyguard_view),
                     statusBarStateController,
@@ -213,10 +260,12 @@
                     unlockedScreenOffAnimationController,
                     dialogManager,
                     controller,
-                    activityLaunchAnimator
+                    activityLaunchAnimator,
+                    featureFlags,
+                    primaryBouncerInteractor
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_BP -> {
+            REASON_AUTH_BP -> {
                 // note: empty controller, currently shows no visual affordance
                 UdfpsBpViewController(
                     view.addUdfpsView(R.layout.udfps_bp_view),
@@ -226,8 +275,8 @@
                     dumpManager
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_OTHER,
-            BiometricOverlayConstants.REASON_AUTH_SETTINGS -> {
+            REASON_AUTH_OTHER,
+            REASON_AUTH_SETTINGS -> {
                 UdfpsFpmOtherViewController(
                     view.addUdfpsView(R.layout.udfps_fpm_other_view),
                     statusBarStateController,
@@ -381,7 +430,12 @@
         }
 
         // Original sensorBounds assume portrait mode.
-        val rotatedSensorBounds = Rect(overlayParams.sensorBounds)
+        var rotatedBounds =
+            if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+                Rect(overlayParams.overlayBounds)
+            } else {
+                Rect(overlayParams.sensorBounds)
+            }
 
         val rot = overlayParams.rotation
         if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
@@ -395,18 +449,27 @@
             } else {
                 Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot))
                 RotationUtils.rotateBounds(
-                    rotatedSensorBounds,
+                    rotatedBounds,
                     overlayParams.naturalDisplayWidth,
                     overlayParams.naturalDisplayHeight,
                     rot
                 )
+
+                if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+                    RotationUtils.rotateBounds(
+                            sensorBounds,
+                            overlayParams.naturalDisplayWidth,
+                            overlayParams.naturalDisplayHeight,
+                            rot
+                    )
+                }
             }
         }
 
-        x = rotatedSensorBounds.left - paddingX
-        y = rotatedSensorBounds.top - paddingY
-        height = rotatedSensorBounds.height() + 2 * paddingX
-        width = rotatedSensorBounds.width() + 2 * paddingY
+        x = rotatedBounds.left - paddingX
+        y = rotatedBounds.top - paddingY
+        height = rotatedBounds.height() + 2 * paddingX
+        width = rotatedBounds.width() + 2 * paddingY
 
         return this
     }
@@ -440,4 +503,4 @@
 private fun Int.isImportantForAccessibility() =
     this == REASON_ENROLL_FIND_SENSOR ||
             this == REASON_ENROLL_ENROLLING ||
-            this == BiometricOverlayConstants.REASON_AUTH_BP
+            this == REASON_AUTH_BP
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
index b80b8a0..670a8e6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
@@ -46,7 +46,7 @@
             logger.e(TAG, "enable | already requested")
             return
         }
-        if (authController.udfpsHbmListener == null) {
+        if (authController.udfpsRefreshRateCallback == null) {
             logger.e(TAG, "enable | mDisplayManagerCallback is null")
             return
         }
@@ -60,7 +60,7 @@
         try {
             // This method is a misnomer. It has nothing to do with HBM, its purpose is to set
             // the appropriate display refresh rate.
-            authController.udfpsHbmListener!!.onHbmEnabled(request.displayId)
+            authController.udfpsRefreshRateCallback!!.onRequestEnabled(request.displayId)
             logger.v(TAG, "enable | requested optimal refresh rate for UDFPS")
         } catch (e: RemoteException) {
             logger.e(TAG, "enable", e)
@@ -84,7 +84,7 @@
 
         try {
             // Allow DisplayManager to unset the UDFPS refresh rate.
-            authController.udfpsHbmListener!!.onHbmDisabled(request.displayId)
+            authController.udfpsRefreshRateCallback!!.onRequestDisabled(request.displayId)
             logger.v(TAG, "disable | removed the UDFPS refresh rate request")
         } catch (e: RemoteException) {
             logger.e(TAG, "disable", e)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt
new file mode 100644
index 0000000..8ae4775
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEllipseDetection.kt
@@ -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.systemui.biometrics
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.util.RotationUtils
+import android.view.MotionEvent
+import kotlin.math.cos
+import kotlin.math.pow
+import kotlin.math.sin
+
+private const val TAG = "UdfpsEllipseDetection"
+
+private const val NEEDED_POINTS = 2
+
+class UdfpsEllipseDetection(overlayParams: UdfpsOverlayParams) {
+    var sensorRect = Rect()
+    var points: Array<Point> = emptyArray()
+
+    init {
+        sensorRect = Rect(overlayParams.sensorBounds)
+
+        points = calculateSensorPoints(sensorRect)
+    }
+
+    fun updateOverlayParams(params: UdfpsOverlayParams) {
+        sensorRect = Rect(params.sensorBounds)
+
+        val rot = params.rotation
+        RotationUtils.rotateBounds(
+            sensorRect,
+            params.naturalDisplayWidth,
+            params.naturalDisplayHeight,
+            rot
+        )
+
+        points = calculateSensorPoints(sensorRect)
+    }
+
+    fun isGoodEllipseOverlap(event: MotionEvent): Boolean {
+        return points.count { checkPoint(event, it) } >= NEEDED_POINTS
+    }
+
+    private 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 calculateSensorPoints(sensorRect: Rect): Array<Point> {
+    val sensorX = sensorRect.centerX()
+    val sensorY = sensorRect.centerY()
+    val cornerOffset: Int = sensorRect.width() / 4
+    val sideOffset: Int = sensorRect.width() / 3
+
+    return 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/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 49e378e..af7e0b6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -99,12 +99,11 @@
         mProgressColor = context.getColor(R.color.udfps_enroll_progress);
         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
         mIsAccessibilityEnabled = am.isTouchExplorationEnabled();
+        mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error);
         if (!mIsAccessibilityEnabled) {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
-            mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error);
         } else {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help_with_talkback);
-            mOnFirstBucketFailedColor = mHelpColor;
         }
         mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
         mCheckmarkDrawable.mutate();
@@ -197,6 +196,7 @@
             }
         }
 
+        mShowingHelp = showingHelp;
         mRemainingSteps = remainingSteps;
         mTotalSteps = totalSteps;
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index 69c37b2..87be42c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
@@ -41,6 +42,9 @@
     @NonNull private ImageView mFingerprintView;
     @NonNull private ImageView mFingerprintProgressView;
 
+    private LayoutParams mProgressParams;
+    private float mProgressBarRadius;
+
     public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         mFingerprintDrawable = new UdfpsEnrollDrawable(mContext);
@@ -57,6 +61,32 @@
     }
 
     @Override
+    void onSensorRectUpdated(RectF bounds) {
+        if (mUseExpandedOverlay) {
+            RectF converted = getBoundsRelativeToView(bounds);
+
+            mProgressParams = new LayoutParams(
+                    (int) (converted.width() + mProgressBarRadius * 2),
+                    (int) (converted.height() + mProgressBarRadius * 2));
+            mProgressParams.setMargins(
+                    (int) (converted.left - mProgressBarRadius),
+                    (int) (converted.top - mProgressBarRadius),
+                    (int) (converted.right + mProgressBarRadius),
+                    (int) (converted.bottom + mProgressBarRadius)
+            );
+
+            mFingerprintProgressView.setLayoutParams(mProgressParams);
+            super.onSensorRectUpdated(converted);
+        } else {
+            super.onSensorRectUpdated(bounds);
+        }
+    }
+
+    void setProgressBarRadius(float radius) {
+        mProgressBarRadius = radius;
+    }
+
+    @Override
     public UdfpsDrawable getDrawable() {
         return mFingerprintDrawable;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index e01273f..4017665 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -21,6 +21,8 @@
 
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -57,6 +59,7 @@
             @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
             @NonNull SystemUIDialogManager systemUIDialogManager,
             @NonNull DumpManager dumpManager,
+            @NonNull FeatureFlags featureFlags,
             float scaleFactor) {
         super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
                 dumpManager);
@@ -64,6 +67,11 @@
                 R.integer.config_udfpsEnrollProgressBar));
         mEnrollHelper = enrollHelper;
         mView.setEnrollHelper(mEnrollHelper);
+        mView.setProgressBarRadius(mEnrollProgressBarRadius);
+
+        if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
+            mView.mUseExpandedOverlay = true;
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java
index bc274a0..339b8ca 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java
@@ -26,6 +26,7 @@
 import android.content.Context;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
+import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.util.MathUtils;
 import android.view.View;
@@ -75,6 +76,8 @@
     private int mAnimationType = ANIMATION_NONE;
     private boolean mFullyInflated;
 
+    private LayoutParams mParams;
+
     public UdfpsKeyguardView(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         mFingerprintDrawable = new UdfpsFpDrawable(context);
@@ -239,6 +242,22 @@
         updateAlpha();
     }
 
+    @Override
+    void onSensorRectUpdated(RectF bounds) {
+        super.onSensorRectUpdated(bounds);
+
+        if (mUseExpandedOverlay) {
+            mParams = new LayoutParams((int) bounds.width(), (int) bounds.height());
+            RectF converted = getBoundsRelativeToView(bounds);
+            mParams.setMargins(
+                    (int) converted.left,
+                    (int) converted.top,
+                    (int) converted.right,
+                    (int) converted.bottom
+            );
+        }
+    }
+
     /**
      * Animates in the bg protection circle behind the fp icon to highlight the icon.
      */
@@ -277,6 +296,7 @@
         pw.println("    mUdfpsRequested=" + mUdfpsRequested);
         pw.println("    mInterpolatedDarkAmount=" + mInterpolatedDarkAmount);
         pw.println("    mAnimationType=" + mAnimationType);
+        pw.println("    mUseExpandedOverlay=" + mUseExpandedOverlay);
     }
 
     private final AsyncLayoutInflater.OnInflateFinishedListener mLayoutInflaterFinishListener =
@@ -291,7 +311,12 @@
             updatePadding();
             updateColor();
             updateAlpha();
-            parent.addView(view);
+
+            if (mUseExpandedOverlay) {
+                parent.addView(view, mParams);
+            } else {
+                parent.addView(view);
+            }
 
             // requires call to invalidate to update the color
             mLockScreenFp.addValueCallback(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
deleted file mode 100644
index 4d7f89d..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ /dev/null
@@ -1,548 +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.
- */
-
-package com.android.systemui.biometrics;
-
-import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
-
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.res.Configuration;
-import android.util.MathUtils;
-import android.view.MotionEvent;
-
-import com.android.keyguard.BouncerPanelExpansionCalculator;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
-import com.android.systemui.shade.ShadeExpansionListener;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.LockscreenShadeTransitionController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.time.SystemClock;
-
-import java.io.PrintWriter;
-
-/**
- * Class that coordinates non-HBM animations during keyguard authentication.
- */
-public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<UdfpsKeyguardView> {
-    public static final String TAG = "UdfpsKeyguardViewCtrl";
-    @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
-    @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @NonNull private final LockscreenShadeTransitionController mLockScreenShadeTransitionController;
-    @NonNull private final ConfigurationController mConfigurationController;
-    @NonNull private final SystemClock mSystemClock;
-    @NonNull private final KeyguardStateController mKeyguardStateController;
-    @NonNull private final UdfpsController mUdfpsController;
-    @NonNull private final UnlockedScreenOffAnimationController
-            mUnlockedScreenOffAnimationController;
-    @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
-    private final ValueAnimator mUnlockedScreenOffDozeAnimator = ValueAnimator.ofFloat(0f, 1f);
-
-    private boolean mShowingUdfpsBouncer;
-    private boolean mUdfpsRequested;
-    private float mQsExpansion;
-    private boolean mFaceDetectRunning;
-    private int mStatusBarState;
-    private float mTransitionToFullShadeProgress;
-    private float mLastDozeAmount;
-    private long mLastUdfpsBouncerShowTime = -1;
-    private float mPanelExpansionFraction;
-    private boolean mLaunchTransitionFadingAway;
-    private boolean mIsLaunchingActivity;
-    private float mActivityLaunchProgress;
-
-    /**
-     * hidden amount of pin/pattern/password bouncer
-     * {@link KeyguardBouncer#EXPANSION_VISIBLE} (0f) to
-     * {@link KeyguardBouncer#EXPANSION_HIDDEN} (1f)
-     */
-    private float mInputBouncerHiddenAmount;
-    private boolean mIsGenericBouncerShowing; // whether UDFPS bouncer or input bouncer is visible
-
-    protected UdfpsKeyguardViewController(
-            @NonNull UdfpsKeyguardView view,
-            @NonNull StatusBarStateController statusBarStateController,
-            @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
-            @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
-            @NonNull DumpManager dumpManager,
-            @NonNull LockscreenShadeTransitionController transitionController,
-            @NonNull ConfigurationController configurationController,
-            @NonNull SystemClock systemClock,
-            @NonNull KeyguardStateController keyguardStateController,
-            @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            @NonNull SystemUIDialogManager systemUIDialogManager,
-            @NonNull UdfpsController udfpsController,
-            @NonNull ActivityLaunchAnimator activityLaunchAnimator) {
-        super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
-                dumpManager);
-        mKeyguardViewManager = statusBarKeyguardViewManager;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mLockScreenShadeTransitionController = transitionController;
-        mConfigurationController = configurationController;
-        mSystemClock = systemClock;
-        mKeyguardStateController = keyguardStateController;
-        mUdfpsController = udfpsController;
-        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
-        mActivityLaunchAnimator = activityLaunchAnimator;
-
-        mUnlockedScreenOffDozeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-        mUnlockedScreenOffDozeAnimator.setInterpolator(Interpolators.ALPHA_IN);
-        mUnlockedScreenOffDozeAnimator.addUpdateListener(
-                new ValueAnimator.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animation) {
-                        mView.onDozeAmountChanged(
-                                animation.getAnimatedFraction(),
-                                (float) animation.getAnimatedValue(),
-                                UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF);
-                    }
-                });
-    }
-
-    @Override
-    @NonNull protected String getTag() {
-        return "UdfpsKeyguardViewController";
-    }
-
-    @Override
-    public void onInit() {
-        super.onInit();
-        mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-    }
-
-    @Override
-    protected void onViewAttached() {
-        super.onViewAttached();
-        final float dozeAmount = getStatusBarStateController().getDozeAmount();
-        mLastDozeAmount = dozeAmount;
-        mStateListener.onDozeAmountChanged(dozeAmount, dozeAmount);
-        getStatusBarStateController().addCallback(mStateListener);
-
-        mUdfpsRequested = false;
-
-        mLaunchTransitionFadingAway = mKeyguardStateController.isLaunchTransitionFadingAway();
-        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
-        mStatusBarState = getStatusBarStateController().getState();
-        mQsExpansion = mKeyguardViewManager.getQsExpansion();
-        updateGenericBouncerVisibility();
-        mConfigurationController.addCallback(mConfigurationListener);
-        getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener);
-        updateScaleFactor();
-        mView.updatePadding();
-        updateAlpha();
-        updatePauseAuth();
-
-        mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(this);
-        mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener);
-    }
-
-    @Override
-    protected void onViewDetached() {
-        super.onViewDetached();
-        mFaceDetectRunning = false;
-
-        mKeyguardStateController.removeCallback(mKeyguardStateControllerCallback);
-        getStatusBarStateController().removeCallback(mStateListener);
-        mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
-        mConfigurationController.removeCallback(mConfigurationListener);
-        getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener);
-        if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
-            mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
-        }
-        mActivityLaunchAnimator.removeListener(mActivityLaunchAnimatorListener);
-    }
-
-    @Override
-    public void dump(PrintWriter pw, String[] args) {
-        super.dump(pw, args);
-        pw.println("mShowingUdfpsBouncer=" + mShowingUdfpsBouncer);
-        pw.println("mFaceDetectRunning=" + mFaceDetectRunning);
-        pw.println("mStatusBarState=" + StatusBarState.toString(mStatusBarState));
-        pw.println("mTransitionToFullShadeProgress=" + mTransitionToFullShadeProgress);
-        pw.println("mQsExpansion=" + mQsExpansion);
-        pw.println("mIsGenericBouncerShowing=" + mIsGenericBouncerShowing);
-        pw.println("mInputBouncerHiddenAmount=" + mInputBouncerHiddenAmount);
-        pw.println("mPanelExpansionFraction=" + mPanelExpansionFraction);
-        pw.println("unpausedAlpha=" + mView.getUnpausedAlpha());
-        pw.println("mUdfpsRequested=" + mUdfpsRequested);
-        pw.println("mLaunchTransitionFadingAway=" + mLaunchTransitionFadingAway);
-        pw.println("mLastDozeAmount=" + mLastDozeAmount);
-
-        mView.dump(pw);
-    }
-
-    /**
-     * Overrides non-bouncer show logic in shouldPauseAuth to still show icon.
-     * @return whether the udfpsBouncer has been newly shown or hidden
-     */
-    private boolean showUdfpsBouncer(boolean show) {
-        if (mShowingUdfpsBouncer == show) {
-            return false;
-        }
-
-        boolean udfpsAffordanceWasNotShowing = shouldPauseAuth();
-        mShowingUdfpsBouncer = show;
-        if (mShowingUdfpsBouncer) {
-            mLastUdfpsBouncerShowTime = mSystemClock.uptimeMillis();
-        }
-        if (mShowingUdfpsBouncer) {
-            if (udfpsAffordanceWasNotShowing) {
-                mView.animateInUdfpsBouncer(null);
-            }
-
-            if (mKeyguardStateController.isOccluded()) {
-                mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true);
-            }
-
-            mView.announceForAccessibility(mView.getContext().getString(
-                    R.string.accessibility_fingerprint_bouncer));
-        } else {
-            mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
-        }
-
-        updateGenericBouncerVisibility();
-        updateAlpha();
-        updatePauseAuth();
-        return true;
-    }
-
-    /**
-     * Returns true if the fingerprint manager is running but we want to temporarily pause
-     * authentication. On the keyguard, we may want to show udfps when the shade
-     * is expanded, so this can be overridden with the showBouncer method.
-     */
-    public boolean shouldPauseAuth() {
-        if (mShowingUdfpsBouncer) {
-            return false;
-        }
-
-        if (mUdfpsRequested && !getNotificationShadeVisible()
-                && (!mIsGenericBouncerShowing
-                || mInputBouncerHiddenAmount != KeyguardBouncer.EXPANSION_VISIBLE)
-                && mKeyguardStateController.isShowing()) {
-            return false;
-        }
-
-        if (mLaunchTransitionFadingAway) {
-            return true;
-        }
-
-        // Only pause auth if we're not on the keyguard AND we're not transitioning to doze
-        // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is
-        // delayed. However, we still animate in the UDFPS affordance with the 
-        // mUnlockedScreenOffDozeAnimator.
-        if (mStatusBarState != KEYGUARD && mLastDozeAmount == 0f) {
-            return true;
-        }
-
-        if (mInputBouncerHiddenAmount < .5f) {
-            return true;
-        }
-
-        if (mView.getUnpausedAlpha() < (255 * .1)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    @Override
-    public boolean listenForTouchesOutsideView() {
-        return true;
-    }
-
-    @Override
-    public void onTouchOutsideView() {
-        maybeShowInputBouncer();
-    }
-
-    /**
-     * If we were previously showing the udfps bouncer, hide it and instead show the regular
-     * (pin/pattern/password) bouncer.
-     *
-     * Does nothing if we weren't previously showing the UDFPS bouncer.
-     */
-    private void maybeShowInputBouncer() {
-        if (mShowingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
-            mKeyguardViewManager.showBouncer(true);
-        }
-    }
-
-    /**
-     * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside
-     * of the udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password
-     * bouncer.
-     */
-    private boolean hasUdfpsBouncerShownWithMinTime() {
-        return (mSystemClock.uptimeMillis() - mLastUdfpsBouncerShowTime) > 200;
-    }
-
-    /**
-     * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
-     * transitioning yet, while 1.0f means we've fully dragged down.
-     *
-     * For example, start swiping down to expand the notification shade from the empty space in
-     * the middle of the lock screen.
-     */
-    public void setTransitionToFullShadeProgress(float progress) {
-        mTransitionToFullShadeProgress = progress;
-        updateAlpha();
-    }
-
-    /**
-     * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's
-     * alpha is based on the doze amount.
-     */
-    @Override
-    public void updateAlpha() {
-        // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested,
-        // then the keyguard is occluded by some application - so instead use the input bouncer
-        // hidden amount to determine the fade.
-        float expansion = mUdfpsRequested ? mInputBouncerHiddenAmount : mPanelExpansionFraction;
-
-        int alpha = mShowingUdfpsBouncer ? 255
-                : (int) MathUtils.constrain(
-                    MathUtils.map(.5f, .9f, 0f, 255f, expansion),
-                    0f, 255f);
-
-        if (!mShowingUdfpsBouncer) {
-            // swipe from top of the lockscreen to expand full QS:
-            alpha *= (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(mQsExpansion));
-
-            // swipe from the middle (empty space) of lockscreen to expand the notification shade:
-            alpha *= (1.0f - mTransitionToFullShadeProgress);
-
-            // Fade out the icon if we are animating an activity launch over the lockscreen and the
-            // activity didn't request the UDFPS.
-            if (mIsLaunchingActivity && !mUdfpsRequested) {
-                alpha *= (1.0f - mActivityLaunchProgress);
-            }
-
-            // Fade out alpha when a dialog is shown
-            // Fade in alpha when a dialog is hidden
-            alpha *= mView.getDialogSuggestedAlpha();
-        }
-        mView.setUnpausedAlpha(alpha);
-    }
-
-    /**
-     * Updates mIsGenericBouncerShowing (whether any bouncer is showing) and updates the
-     * mInputBouncerHiddenAmount to reflect whether the input bouncer is fully showing or not.
-     */
-    private void updateGenericBouncerVisibility() {
-        mIsGenericBouncerShowing = mKeyguardViewManager.isBouncerShowing(); // includes altBouncer
-        final boolean altBouncerShowing = mKeyguardViewManager.isShowingAlternateAuth();
-        if (altBouncerShowing || !mKeyguardViewManager.bouncerIsOrWillBeShowing()) {
-            mInputBouncerHiddenAmount = 1f;
-        } else if (mIsGenericBouncerShowing) {
-            // input bouncer is fully showing
-            mInputBouncerHiddenAmount = 0f;
-        }
-    }
-
-    /**
-     * Update the scale factor based on the device's resolution.
-     */
-    private void updateScaleFactor() {
-        if (mUdfpsController != null && mUdfpsController.mOverlayParams != null) {
-            mView.setScaleFactor(mUdfpsController.mOverlayParams.getScaleFactor());
-        }
-    }
-
-    private final StatusBarStateController.StateListener mStateListener =
-            new StatusBarStateController.StateListener() {
-        @Override
-        public void onDozeAmountChanged(float linear, float eased) {
-            if (mLastDozeAmount < linear) {
-                showUdfpsBouncer(false);
-            }
-            mUnlockedScreenOffDozeAnimator.cancel();
-            final boolean animatingFromUnlockedScreenOff =
-                    mUnlockedScreenOffAnimationController.isAnimationPlaying();
-            if (animatingFromUnlockedScreenOff && linear != 0f) {
-                // we manually animate the fade in of the UDFPS icon since the unlocked
-                // screen off animation prevents the doze amounts to be incrementally eased in
-                mUnlockedScreenOffDozeAnimator.start();
-            } else {
-                mView.onDozeAmountChanged(linear, eased,
-                        UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
-            }
-
-            mLastDozeAmount = linear;
-            updatePauseAuth();
-        }
-
-        @Override
-        public void onStateChanged(int statusBarState) {
-            mStatusBarState = statusBarState;
-            updateAlpha();
-            updatePauseAuth();
-        }
-    };
-
-    private final StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor =
-            new StatusBarKeyguardViewManager.AlternateAuthInterceptor() {
-                @Override
-                public boolean showAlternateAuthBouncer() {
-                    return showUdfpsBouncer(true);
-                }
-
-                @Override
-                public boolean hideAlternateAuthBouncer() {
-                    return showUdfpsBouncer(false);
-                }
-
-                @Override
-                public boolean isShowingAlternateAuthBouncer() {
-                    return mShowingUdfpsBouncer;
-                }
-
-                @Override
-                public void requestUdfps(boolean request, int color) {
-                    mUdfpsRequested = request;
-                    mView.requestUdfps(request, color);
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public boolean isAnimating() {
-                    return false;
-                }
-
-                /**
-                 * Set the amount qs is expanded. Forxample, swipe down from the top of the
-                 * lock screen to start the full QS expansion.
-                 */
-                @Override
-                public void setQsExpansion(float qsExpansion) {
-                    mQsExpansion = qsExpansion;
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public boolean onTouch(MotionEvent event) {
-                    if (mTransitionToFullShadeProgress != 0) {
-                        return false;
-                    }
-                    return mUdfpsController.onTouch(event);
-                }
-
-                @Override
-                public void setBouncerExpansionChanged(float expansion) {
-                    mInputBouncerHiddenAmount = expansion;
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                /**
-                 * Only called on primary auth bouncer changes, not on whether the UDFPS bouncer
-                 * visibility changes.
-                 */
-                @Override
-                public void onBouncerVisibilityChanged() {
-                    updateGenericBouncerVisibility();
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public void dump(PrintWriter pw) {
-                    pw.println(getTag());
-                }
-            };
-
-    private final ConfigurationController.ConfigurationListener mConfigurationListener =
-            new ConfigurationController.ConfigurationListener() {
-                @Override
-                public void onUiModeChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
-                public void onThemeChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
-                public void onConfigChanged(Configuration newConfig) {
-                    updateScaleFactor();
-                    mView.updatePadding();
-                    mView.updateColor();
-                }
-            };
-
-    private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() {
-        @Override
-        public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
-            float fraction = event.getFraction();
-            mPanelExpansionFraction =
-                    mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator
-                            .aboutToShowBouncerProgress(fraction) : fraction;
-            updateAlpha();
-            updatePauseAuth();
-        }
-    };
-
-    private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
-            new KeyguardStateController.Callback() {
-                @Override
-                public void onLaunchTransitionFadingAwayChanged() {
-                    mLaunchTransitionFadingAway =
-                            mKeyguardStateController.isLaunchTransitionFadingAway();
-                    updatePauseAuth();
-                }
-            };
-
-    private final ActivityLaunchAnimator.Listener mActivityLaunchAnimatorListener =
-            new ActivityLaunchAnimator.Listener() {
-                @Override
-                public void onLaunchAnimationStart() {
-                    mIsLaunchingActivity = true;
-                    mActivityLaunchProgress = 0f;
-                    updateAlpha();
-                }
-
-                @Override
-                public void onLaunchAnimationEnd() {
-                    mIsLaunchingActivity = false;
-                    updateAlpha();
-                }
-
-                @Override
-                public void onLaunchAnimationProgress(float linearProgress) {
-                    mActivityLaunchProgress = linearProgress;
-                    updateAlpha();
-                }
-            };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
new file mode 100644
index 0000000..63144fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
@@ -0,0 +1,560 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.biometrics
+
+import android.animation.ValueAnimator
+import android.content.res.Configuration
+import android.util.MathUtils
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.LockscreenShadeTransitionController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateBouncer
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/** Class that coordinates non-HBM animations during keyguard authentication. */
+open class UdfpsKeyguardViewController
+constructor(
+    private val view: UdfpsKeyguardView,
+    statusBarStateController: StatusBarStateController,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val keyguardViewManager: StatusBarKeyguardViewManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    dumpManager: DumpManager,
+    private val lockScreenShadeTransitionController: LockscreenShadeTransitionController,
+    private val configurationController: ConfigurationController,
+    private val systemClock: SystemClock,
+    private val keyguardStateController: KeyguardStateController,
+    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+    systemUIDialogManager: SystemUIDialogManager,
+    private val udfpsController: UdfpsController,
+    private val activityLaunchAnimator: ActivityLaunchAnimator,
+    featureFlags: FeatureFlags,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor
+) :
+    UdfpsAnimationViewController<UdfpsKeyguardView>(
+        view,
+        statusBarStateController,
+        shadeExpansionStateManager,
+        systemUIDialogManager,
+        dumpManager
+    ) {
+    private val useExpandedOverlay: Boolean =
+        featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
+    private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER)
+    private var showingUdfpsBouncer = false
+    private var udfpsRequested = false
+    private var qsExpansion = 0f
+    private var faceDetectRunning = false
+    private var statusBarState = 0
+    private var transitionToFullShadeProgress = 0f
+    private var lastDozeAmount = 0f
+    private var lastUdfpsBouncerShowTime: Long = -1
+    private var panelExpansionFraction = 0f
+    private var launchTransitionFadingAway = false
+    private var isLaunchingActivity = false
+    private var activityLaunchProgress = 0f
+    private val unlockedScreenOffDozeAnimator =
+        ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong()
+            interpolator = Interpolators.ALPHA_IN
+            addUpdateListener { animation ->
+                view.onDozeAmountChanged(
+                    animation.animatedFraction,
+                    animation.animatedValue as Float,
+                    UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF
+                )
+            }
+        }
+    /**
+     * Hidden amount of input (pin/pattern/password) bouncer. This is used
+     * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only
+     * used for the non-modernBouncer.
+     */
+    private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN
+    private var inputBouncerExpansion = 0f // only used for modernBouncer
+
+    private val stateListener: StatusBarStateController.StateListener =
+        object : StatusBarStateController.StateListener {
+            override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                if (lastDozeAmount < linear) {
+                    showUdfpsBouncer(false)
+                }
+                unlockedScreenOffDozeAnimator.cancel()
+                val animatingFromUnlockedScreenOff =
+                    unlockedScreenOffAnimationController.isAnimationPlaying()
+                if (animatingFromUnlockedScreenOff && linear != 0f) {
+                    // we manually animate the fade in of the UDFPS icon since the unlocked
+                    // screen off animation prevents the doze amounts to be incrementally eased in
+                    unlockedScreenOffDozeAnimator.start()
+                } else {
+                    view.onDozeAmountChanged(
+                        linear,
+                        eased,
+                        UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
+                    )
+                }
+                lastDozeAmount = linear
+                updatePauseAuth()
+            }
+
+            override fun onStateChanged(statusBarState: Int) {
+                this@UdfpsKeyguardViewController.statusBarState = statusBarState
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+
+    private val mPrimaryBouncerExpansionCallback: PrimaryBouncerExpansionCallback =
+        object : PrimaryBouncerExpansionCallback {
+            override fun onExpansionChanged(expansion: Float) {
+                inputBouncerHiddenAmount = expansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            override fun onVisibilityChanged(isVisible: Boolean) {
+                updateBouncerHiddenAmount()
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+
+    private val configurationListener: ConfigurationController.ConfigurationListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onUiModeChanged() {
+                view.updateColor()
+            }
+
+            override fun onThemeChanged() {
+                view.updateColor()
+            }
+
+            override fun onConfigChanged(newConfig: Configuration) {
+                updateScaleFactor()
+                view.updatePadding()
+                view.updateColor()
+            }
+        }
+
+    private val shadeExpansionListener = ShadeExpansionListener { (fraction) ->
+        panelExpansionFraction =
+            if (keyguardViewManager.isPrimaryBouncerInTransit) {
+                aboutToShowBouncerProgress(fraction)
+            } else {
+                fraction
+            }
+        updateAlpha()
+        updatePauseAuth()
+    }
+
+    private val keyguardStateControllerCallback: KeyguardStateController.Callback =
+        object : KeyguardStateController.Callback {
+            override fun onLaunchTransitionFadingAwayChanged() {
+                launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
+                updatePauseAuth()
+            }
+        }
+
+    private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener =
+        object : ActivityLaunchAnimator.Listener {
+            override fun onLaunchAnimationStart() {
+                isLaunchingActivity = true
+                activityLaunchProgress = 0f
+                updateAlpha()
+            }
+
+            override fun onLaunchAnimationEnd() {
+                isLaunchingActivity = false
+                updateAlpha()
+            }
+
+            override fun onLaunchAnimationProgress(linearProgress: Float) {
+                activityLaunchProgress = linearProgress
+                updateAlpha()
+            }
+        }
+
+    private val statusBarKeyguardViewManagerCallback: KeyguardViewManagerCallback =
+        object : KeyguardViewManagerCallback {
+            override fun onQSExpansionChanged(qsExpansion: Float) {
+                this@UdfpsKeyguardViewController.qsExpansion = qsExpansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            /**
+             * Forward touches to the UdfpsController. This allows the touch to start from outside
+             * the sensor area and then slide their finger into the sensor area.
+             */
+            override fun onTouch(event: MotionEvent) {
+                // Don't forward touches if the shade has already started expanding.
+                if (transitionToFullShadeProgress != 0f) {
+                    return
+                }
+
+                // Forwarding touches not needed with expanded overlay
+                if (useExpandedOverlay) {
+                    return
+                } else {
+                    udfpsController.onTouch(event)
+                }
+            }
+        }
+
+    private val mAlternateBouncer: AlternateBouncer =
+        object : AlternateBouncer {
+            override fun showAlternateBouncer(): Boolean {
+                return showUdfpsBouncer(true)
+            }
+
+            override fun hideAlternateBouncer(): Boolean {
+                return showUdfpsBouncer(false)
+            }
+
+            override fun isShowingAlternateBouncer(): Boolean {
+                return showingUdfpsBouncer
+            }
+
+            override fun requestUdfps(request: Boolean, color: Int) {
+                udfpsRequested = request
+                view.requestUdfps(request, color)
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            override fun dump(pw: PrintWriter) {
+                pw.println(tag)
+            }
+        }
+
+    override val tag: String
+        get() = TAG
+
+    override fun onInit() {
+        super.onInit()
+        keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+    }
+
+    init {
+        if (isModernBouncerEnabled) {
+            view.repeatWhenAttached {
+                // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
+                // can make the view not visible; and we still want to listen for events
+                // that may make the view visible again.
+                repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
+        return scope.launch {
+            primaryBouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
+                inputBouncerExpansion = bouncerExpansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+    }
+
+    public override fun onViewAttached() {
+        super.onViewAttached()
+        val dozeAmount = statusBarStateController.dozeAmount
+        lastDozeAmount = dozeAmount
+        stateListener.onDozeAmountChanged(dozeAmount, dozeAmount)
+        statusBarStateController.addCallback(stateListener)
+        udfpsRequested = false
+        launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
+        keyguardStateController.addCallback(keyguardStateControllerCallback)
+        statusBarState = statusBarStateController.state
+        qsExpansion = keyguardViewManager.qsExpansion
+        keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback)
+        if (!isModernBouncerEnabled) {
+            val bouncer = keyguardViewManager.primaryBouncer
+            bouncer?.expansion?.let {
+                mPrimaryBouncerExpansionCallback.onExpansionChanged(it)
+                bouncer.addBouncerExpansionCallback(mPrimaryBouncerExpansionCallback)
+            }
+            updateBouncerHiddenAmount()
+        }
+        configurationController.addCallback(configurationListener)
+        shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
+        updateScaleFactor()
+        view.updatePadding()
+        updateAlpha()
+        updatePauseAuth()
+        keyguardViewManager.setAlternateBouncer(mAlternateBouncer)
+        lockScreenShadeTransitionController.udfpsKeyguardViewController = this
+        activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
+        view.mUseExpandedOverlay = useExpandedOverlay
+    }
+
+    override fun onViewDetached() {
+        super.onViewDetached()
+        faceDetectRunning = false
+        keyguardStateController.removeCallback(keyguardStateControllerCallback)
+        statusBarStateController.removeCallback(stateListener)
+        keyguardViewManager.removeAlternateAuthInterceptor(mAlternateBouncer)
+        keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
+        configurationController.removeCallback(configurationListener)
+        shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
+        if (lockScreenShadeTransitionController.udfpsKeyguardViewController === this) {
+            lockScreenShadeTransitionController.udfpsKeyguardViewController = null
+        }
+        activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
+        keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
+        if (!isModernBouncerEnabled) {
+            keyguardViewManager.primaryBouncer?.removeBouncerExpansionCallback(
+                mPrimaryBouncerExpansionCallback
+            )
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<String>) {
+        super.dump(pw, args)
+        pw.println("isModernBouncerEnabled=$isModernBouncerEnabled")
+        pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer")
+        pw.println("faceDetectRunning=$faceDetectRunning")
+        pw.println("statusBarState=" + StatusBarState.toString(statusBarState))
+        pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress")
+        pw.println("qsExpansion=$qsExpansion")
+        pw.println("panelExpansionFraction=$panelExpansionFraction")
+        pw.println("unpausedAlpha=" + view.unpausedAlpha)
+        pw.println("udfpsRequestedByApp=$udfpsRequested")
+        pw.println("launchTransitionFadingAway=$launchTransitionFadingAway")
+        pw.println("lastDozeAmount=$lastDozeAmount")
+        if (isModernBouncerEnabled) {
+            pw.println("inputBouncerExpansion=$inputBouncerExpansion")
+        } else {
+            pw.println("inputBouncerHiddenAmount=$inputBouncerHiddenAmount")
+        }
+        view.dump(pw)
+    }
+
+    /**
+     * Overrides non-bouncer show logic in shouldPauseAuth to still show icon.
+     * @return whether the udfpsBouncer has been newly shown or hidden
+     */
+    private fun showUdfpsBouncer(show: Boolean): Boolean {
+        if (showingUdfpsBouncer == show) {
+            return false
+        }
+        val udfpsAffordanceWasNotShowing = shouldPauseAuth()
+        showingUdfpsBouncer = show
+        if (showingUdfpsBouncer) {
+            lastUdfpsBouncerShowTime = systemClock.uptimeMillis()
+        }
+        if (showingUdfpsBouncer) {
+            if (udfpsAffordanceWasNotShowing) {
+                view.animateInUdfpsBouncer(null)
+            }
+            if (keyguardStateController.isOccluded) {
+                keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true)
+            }
+            view.announceForAccessibility(
+                view.context.getString(R.string.accessibility_fingerprint_bouncer)
+            )
+        } else {
+            keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
+        }
+        updateBouncerHiddenAmount()
+        updateAlpha()
+        updatePauseAuth()
+        return true
+    }
+
+    /**
+     * Returns true if the fingerprint manager is running but we want to temporarily pause
+     * authentication. On the keyguard, we may want to show udfps when the shade is expanded, so
+     * this can be overridden with the showBouncer method.
+     */
+    override fun shouldPauseAuth(): Boolean {
+        if (showingUdfpsBouncer) {
+            return false
+        }
+        if (
+            udfpsRequested &&
+                !notificationShadeVisible &&
+                !isInputBouncerFullyVisible() &&
+                keyguardStateController.isShowing
+        ) {
+            return false
+        }
+        if (launchTransitionFadingAway) {
+            return true
+        }
+
+        // Only pause auth if we're not on the keyguard AND we're not transitioning to doze
+        // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is
+        // delayed. However, we still animate in the UDFPS affordance with the
+        // mUnlockedScreenOffDozeAnimator.
+        if (statusBarState != StatusBarState.KEYGUARD && lastDozeAmount == 0f) {
+            return true
+        }
+        if (isBouncerExpansionGreaterThan(.5f)) {
+            return true
+        }
+        return view.unpausedAlpha < 255 * .1
+    }
+
+    fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean {
+        return if (isModernBouncerEnabled) {
+            inputBouncerExpansion >= bouncerExpansionThreshold
+        } else {
+            inputBouncerHiddenAmount < bouncerExpansionThreshold
+        }
+    }
+
+    fun isInputBouncerFullyVisible(): Boolean {
+        return if (isModernBouncerEnabled) {
+            inputBouncerExpansion == 1f
+        } else {
+            keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateBouncer
+        }
+    }
+
+    override fun listenForTouchesOutsideView(): Boolean {
+        return true
+    }
+
+    override fun onTouchOutsideView() {
+        maybeShowInputBouncer()
+    }
+
+    /**
+     * If we were previously showing the udfps bouncer, hide it and instead show the regular
+     * (pin/pattern/password) bouncer.
+     *
+     * Does nothing if we weren't previously showing the UDFPS bouncer.
+     */
+    private fun maybeShowInputBouncer() {
+        if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
+            keyguardViewManager.showPrimaryBouncer(true)
+        }
+    }
+
+    /**
+     * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the
+     * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer.
+     */
+    private fun hasUdfpsBouncerShownWithMinTime(): Boolean {
+        return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200
+    }
+
+    /**
+     * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
+     * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down
+     * to expand the notification shade from the empty space in the middle of the lock screen.
+     */
+    fun setTransitionToFullShadeProgress(progress: Float) {
+        transitionToFullShadeProgress = progress
+        updateAlpha()
+    }
+
+    /**
+     * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's alpha is
+     * based on the doze amount.
+     */
+    override fun updateAlpha() {
+        // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested,
+        // then the keyguard is occluded by some application - so instead use the input bouncer
+        // hidden amount to determine the fade.
+        val expansion = if (udfpsRequested) getInputBouncerHiddenAmt() else panelExpansionFraction
+        var alpha: Int =
+            if (showingUdfpsBouncer) 255
+            else MathUtils.constrain(MathUtils.map(.5f, .9f, 0f, 255f, expansion), 0f, 255f).toInt()
+        if (!showingUdfpsBouncer) {
+            // swipe from top of the lockscreen to expand full QS:
+            alpha =
+                (alpha * (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(qsExpansion)))
+                    .toInt()
+
+            // swipe from the middle (empty space) of lockscreen to expand the notification shade:
+            alpha = (alpha * (1.0f - transitionToFullShadeProgress)).toInt()
+
+            // Fade out the icon if we are animating an activity launch over the lockscreen and the
+            // activity didn't request the UDFPS.
+            if (isLaunchingActivity && !udfpsRequested) {
+                alpha = (alpha * (1.0f - activityLaunchProgress)).toInt()
+            }
+
+            // Fade out alpha when a dialog is shown
+            // Fade in alpha when a dialog is hidden
+            alpha = (alpha * view.dialogSuggestedAlpha).toInt()
+        }
+        view.unpausedAlpha = alpha
+    }
+
+    private fun getInputBouncerHiddenAmt(): Float {
+        return if (isModernBouncerEnabled) {
+            1f - inputBouncerExpansion
+        } else {
+            inputBouncerHiddenAmount
+        }
+    }
+
+    /** Update the scale factor based on the device's resolution. */
+    private fun updateScaleFactor() {
+        udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) }
+    }
+
+    private fun updateBouncerHiddenAmount() {
+        if (isModernBouncerEnabled) {
+            return
+        }
+        val altBouncerShowing = keyguardViewManager.isShowingAlternateBouncer
+        if (altBouncerShowing || !keyguardViewManager.primaryBouncerIsOrWillBeShowing()) {
+            inputBouncerHiddenAmount = 1f
+        } else if (keyguardViewManager.isBouncerShowing) {
+            // input bouncer is fully showing
+            inputBouncerHiddenAmount = 0f
+        }
+    }
+
+    companion object {
+        const val TAG = "UdfpsKeyguardViewController"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
index 39199d1..0d08b43 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.biometrics
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.UdfpsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
new file mode 100644
index 0000000..142642a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.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.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.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
+@Inject
+constructor(
+    private val context: Context,
+    private val execution: Execution,
+    private val windowManager: WindowManager,
+    private val fingerprintManager: FingerprintManager?,
+    private val handler: Handler,
+    private val biometricExecutor: Executor,
+    private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
+    @Main private val fgExecutor: DelayableExecutor,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val authController: AuthController,
+    private val udfpsLogger: UdfpsLogger,
+    private var featureFlags: FeatureFlags
+) : CoreStartable {
+
+    /** The view, when [isShowing], or null. */
+    var overlayView: UdfpsOverlayView? = null
+        private set
+
+    private var requestId: Long = 0
+    private var onFingerDown = false
+    val size = windowManager.maximumWindowMetrics.bounds
+
+    val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf()
+    var points: Array<Point> = emptyArray()
+    var processedMotionEvent = false
+    var isShowing = false
+
+    private var params: UdfpsOverlayParams = UdfpsOverlayParams()
+
+    private val coreLayoutParams =
+        WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
+                0 /* flags set in computeLayoutParams() */,
+                PixelFormat.TRANSLUCENT
+            )
+            .apply {
+                title = TAG
+                fitInsetsTypes = 0
+                gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
+                layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS
+                privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+                // Avoid announcing window title.
+                accessibilityTitle = " "
+                inputFeatures = INPUT_FEATURE_SPY
+            }
+
+    fun onTouch(event: MotionEvent): Boolean {
+        val view = overlayView!!
+
+        return when (event.action) {
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE -> {
+                onFingerDown = true
+                if (!view.isDisplayConfigured && alternateTouchProvider.isPresent) {
+                    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.invalidate()
+                }
+                true
+            }
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> {
+                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)
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt())
+                        }
+                    }
+                }
+
+                if (overlayView!!.isDisplayConfigured) {
+                    overlayView!!.unconfigureDisplay()
+                }
+
+                overlayView?.apply {
+                    windowManager.removeView(this)
+                    setOnTouchListener(null)
+                }
+
+                isShowing = false
+                overlayView = null
+                processedMotionEvent = false
+            }
+        }
+    }
+
+    @Override
+    override fun start() {
+        fingerprintManager?.addAuthenticatorsRegisteredCallback(
+            object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+                override fun onAllAuthenticatorsRegistered(
+                    sensors: List<FingerprintSensorPropertiesInternal>
+                ) {
+                    handler.post { handleAllFingerprintAuthenticatorsRegistered(sensors) }
+                }
+            }
+        )
+
+        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(
+        sensors: List<FingerprintSensorPropertiesInternal>
+    ) {
+        for (props in sensors) {
+            if (props.isAnyUdfpsType) {
+                udfpsProps.add(props)
+            }
+        }
+
+        // Setup param size
+        if (udfpsProps.isNotEmpty()) {
+            params =
+                UdfpsOverlayParams(
+                    sensorBounds = udfpsProps[0].location.rect,
+                    overlayBounds = Rect(0, size.height() / 2, size.width(), size.height()),
+                    naturalDisplayWidth = size.width(),
+                    naturalDisplayHeight = size.height(),
+                    scaleFactor = 1f
+                )
+
+            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/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
index d725dfb..c23b0f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
@@ -20,6 +20,7 @@
 
 data class UdfpsOverlayParams(
     val sensorBounds: Rect = Rect(),
+    val overlayBounds: Rect = Rect(),
     val naturalDisplayWidth: Int = 0,
     val naturalDisplayHeight: Int = 0,
     val scaleFactor: Float = 1f,
@@ -40,4 +41,4 @@
         } else {
             naturalDisplayHeight
         }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
new file mode 100644
index 0000000..4e6a06b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.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) {
+    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)
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        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)
+
+        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?) {
+        mUdfpsDisplayMode = udfpsDisplayMode
+    }
+
+    fun configureDisplay(onDisplayConfigured: Runnable) {
+        isDisplayConfigured = true
+        mUdfpsDisplayMode?.enable(onDisplayConfigured)
+    }
+
+    fun unconfigureDisplay() {
+        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 b1d6e00..f48cfd3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.biometrics
 
+import android.content.Context
+import android.graphics.Rect
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
@@ -23,9 +25,14 @@
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
-import android.hardware.fingerprint.IUdfpsOverlayController
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.util.Log
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -35,20 +42,25 @@
 private const val TAG = "UdfpsShell"
 private const val REQUEST_ID = 2L
 private const val SENSOR_ID = 0
+private const val MINOR = 10F
+private const val MAJOR = 10F
 
 /**
  * Used to show and hide the UDFPS overlay with statusbar commands.
  */
 @SysUISingleton
 class UdfpsShell @Inject constructor(
-    commandRegistry: CommandRegistry
+    commandRegistry: CommandRegistry,
+    private val udfpsOverlay: UdfpsOverlay
 ) : Command {
 
     /**
      * Set in [UdfpsController.java] constructor, used to show and hide the UDFPS overlay.
      * TODO: inject after b/229290039 is resolved
      */
-    var udfpsOverlayController: IUdfpsOverlayController? = null
+    var udfpsOverlayController: UdfpsController.UdfpsOverlayController? = null
+    var context: Context? = null
+    var inflater: LayoutInflater? = null
 
     init {
         commandRegistry.registerCommand("udfps") { this }
@@ -57,8 +69,18 @@
     override fun execute(pw: PrintWriter, args: List<String>) {
         if (args.size == 1 && args[0] == "hide") {
             hideOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "show") {
+            showUdfpsOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "hide") {
+            hideUdfpsOverlay()
         } else if (args.size == 2 && args[0] == "show") {
             showOverlay(getEnrollmentReason(args[1]))
+        } else if (args.size == 1 && args[0] == "onUiReady") {
+            onUiReady()
+        } else if (args.size == 1 && args[0] == "simFingerDown") {
+            simFingerDown()
+        } else if (args.size == 1 && args[0] == "simFingerUp") {
+            simFingerUp()
         } else {
             invalidCommand(pw)
         }
@@ -72,6 +94,11 @@
                             "auth-keyguard, auth-other, auth-settings]")
         pw.println("    -> reason otherwise defaults to unknown")
         pw.println("  - hide")
+        pw.println("  - onUiReady")
+        pw.println("  - simFingerDown")
+        pw.println("    -> Simulates onFingerDown on sensor")
+        pw.println("  - simFingerUp")
+        pw.println("    -> Simulates onFingerUp on sensor")
     }
 
     private fun invalidCommand(pw: PrintWriter) {
@@ -104,7 +131,67 @@
         )
     }
 
+    private fun showUdfpsOverlay() {
+        Log.v(TAG, "showUdfpsOverlay")
+        udfpsOverlay.show(REQUEST_ID)
+    }
+
+    private fun hideUdfpsOverlay() {
+        Log.v(TAG, "hideUdfpsOverlay")
+        udfpsOverlay.hide()
+    }
+
     private fun hideOverlay() {
         udfpsOverlayController?.hideUdfpsOverlay(SENSOR_ID)
     }
-}
\ No newline at end of file
+
+
+    @VisibleForTesting
+    fun onUiReady() {
+        udfpsOverlayController?.debugOnUiReady(REQUEST_ID, SENSOR_ID)
+    }
+
+    @VisibleForTesting
+    fun simFingerDown() {
+        val sensorBounds: Rect = udfpsOverlayController!!.sensorBounds
+
+        val downEvent: MotionEvent? = obtainMotionEvent(ACTION_DOWN, sensorBounds.exactCenterX(),
+                sensorBounds.exactCenterY(), MINOR, MAJOR)
+        udfpsOverlayController?.debugOnTouch(REQUEST_ID, downEvent)
+
+        val moveEvent: MotionEvent? = obtainMotionEvent(ACTION_MOVE, sensorBounds.exactCenterX(),
+                sensorBounds.exactCenterY(), MINOR, MAJOR)
+        udfpsOverlayController?.debugOnTouch(REQUEST_ID, moveEvent)
+
+        downEvent?.recycle()
+        moveEvent?.recycle()
+    }
+
+    @VisibleForTesting
+    fun simFingerUp() {
+        val sensorBounds: Rect = udfpsOverlayController!!.sensorBounds
+
+        val upEvent: MotionEvent? = obtainMotionEvent(ACTION_UP, sensorBounds.exactCenterX(),
+                sensorBounds.exactCenterY(), MINOR, MAJOR)
+        udfpsOverlayController?.debugOnTouch(REQUEST_ID, upEvent)
+        upEvent?.recycle()
+    }
+
+    private fun obtainMotionEvent(
+            action: Int,
+            x: Float,
+            y: Float,
+            minor: Float,
+            major: Float
+    ): MotionEvent? {
+        val pp = MotionEvent.PointerProperties()
+        pp.id = 1
+        val pc = MotionEvent.PointerCoords()
+        pc.x = x
+        pc.y = y
+        pc.touchMinor = minor
+        pc.touchMajor = major
+        return MotionEvent.obtain(0, 0, action, 1, arrayOf(pp), arrayOf(pc),
+                0, 0, 1f, 1f, 0, 0, 0, 0)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index a15456d..4a8877e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -20,6 +20,7 @@
 import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.PointF
+import android.graphics.Rect
 import android.graphics.RectF
 import android.util.AttributeSet
 import android.util.Log
@@ -38,9 +39,12 @@
     attrs: AttributeSet?
 ) : FrameLayout(context, attrs), DozeReceiver {
 
+    // Use expanded overlay when feature flag is true, set by UdfpsViewController
+    var useExpandedOverlay: Boolean = false
+
     // sensorRect may be bigger than the sensor. True sensor dimensions are defined in
     // overlayParams.sensorBounds
-    private val sensorRect = RectF()
+    var sensorRect = Rect()
     private var mUdfpsDisplayMode: UdfpsDisplayModeProvider? = null
     private val debugTextPaint = Paint().apply {
         isAntiAlias = true
@@ -92,13 +96,19 @@
         val paddingX = animationViewController?.paddingX ?: 0
         val paddingY = animationViewController?.paddingY ?: 0
 
-        sensorRect.set(
-            paddingX.toFloat(),
-            paddingY.toFloat(),
-            (overlayParams.sensorBounds.width() + paddingX).toFloat(),
-            (overlayParams.sensorBounds.height() + paddingY).toFloat()
-        )
-        animationViewController?.onSensorRectUpdated(RectF(sensorRect))
+        // Updates sensor rect in relation to the overlay view
+        if (useExpandedOverlay) {
+            animationViewController?.onSensorRectUpdated(RectF(sensorRect))
+        } else {
+            sensorRect.set(
+                    paddingX,
+                    paddingY,
+                    (overlayParams.sensorBounds.width() + paddingX),
+                    (overlayParams.sensorBounds.height() + paddingY)
+            )
+
+            animationViewController?.onSensorRectUpdated(RectF(sensorRect))
+        }
     }
 
     fun onTouchOutsideView() {
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/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
new file mode 100644
index 0000000..e82646f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.data.model
+
+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..6fb8e34
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
@@ -0,0 +1,126 @@
+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 android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+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.awaitCancellation
+import kotlinx.coroutines.launch
+
+/** Sub-binder for the [CredentialPasswordView]. */
+object CredentialPasswordViewBinder {
+
+    /** Bind the view. */
+    fun bind(
+        view: CredentialPasswordView,
+        host: CredentialView.Host,
+        viewModel: CredentialViewModel,
+        requestFocusForInput: Boolean,
+    ) {
+        val imeManager = view.context.getSystemService(InputMethodManager::class.java)!!
+
+        val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword)
+
+        val onBackInvokedCallback = OnBackInvokedCallback { host.onCredentialAborted() }
+
+        view.repeatWhenAttached {
+            if (requestFocusForInput) {
+                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(onBackInvokedCallback))
+                    }
+                }
+
+                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("")
+                        }
+                    }
+                }
+
+                val onBackInvokedDispatcher = view.findOnBackInvokedDispatcher()
+                if (onBackInvokedDispatcher != null) {
+                    launch {
+                            onBackInvokedDispatcher.registerOnBackInvokedCallback(
+                                OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                                onBackInvokedCallback
+                            )
+                            awaitCancellation()
+                        }
+                        .invokeOnCompletion {
+                            onBackInvokedDispatcher.unregisterOnBackInvokedCallback(
+                                onBackInvokedCallback
+                            )
+                        }
+                }
+            }
+        }
+    }
+}
+
+private class OnBackButtonListener(private val onBackInvokedCallback: OnBackInvokedCallback) :
+    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) {
+            onBackInvokedCallback.onBackInvoked()
+        }
+        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..b692ad3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
@@ -0,0 +1,74 @@
+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.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.isPatternTooShort()) {
+                                    // 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>.isPatternTooShort(): 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..e2d36dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -0,0 +1,141 @@
+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.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,
+        requestFocusForInput: Boolean = true,
+    ) {
+        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, requestFocusForInput)
+            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/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
index 96af42b..d99625a 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.bluetooth
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.BluetoothLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Helper class for logging bluetooth events. */
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
index 9b7d498..8e062bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
@@ -17,15 +17,11 @@
 package com.android.systemui.bluetooth;
 
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.Window;
-import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -33,7 +29,7 @@
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
-import com.android.systemui.media.MediaDataUtils;
+import com.android.systemui.media.controls.util.MediaDataUtils;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
index 22dc94a..08c7c0f 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.os.Handler
 import android.os.Looper
+import android.os.Trace
 import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.ArraySet
@@ -126,6 +127,10 @@
                 action,
                 userId,
                 {
+                    if (Trace.isEnabled()) {
+                        Trace.traceBegin(
+                                Trace.TRACE_TAG_APP, "registerReceiver act=$action user=$userId")
+                    }
                     context.registerReceiverAsUser(
                             this,
                             UserHandle.of(userId),
@@ -134,11 +139,18 @@
                             workerHandler,
                             flags
                     )
+                    Trace.endSection()
                     logger.logContextReceiverRegistered(userId, flags, it)
                 },
                 {
                     try {
+                        if (Trace.isEnabled()) {
+                            Trace.traceBegin(
+                                    Trace.TRACE_TAG_APP,
+                                    "unregisterReceiver act=$action user=$userId")
+                        }
                         context.unregisterReceiver(this)
+                        Trace.endSection()
                         logger.logContextReceiverUnregistered(userId, action)
                     } catch (e: IllegalArgumentException) {
                         Log.e(TAG, "Trying to unregister unregistered receiver for user $userId, " +
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
index 5b3a982..d27708f 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
@@ -20,11 +20,11 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogMessage
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.log.dagger.BroadcastDispatcherLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index dec3d6b..1454210 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.ripple.RippleView
+import com.android.systemui.surfaceeffects.ripple.RippleView
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
@@ -149,7 +149,7 @@
     }
 
     fun startRipple() {
-        if (rippleView.rippleInProgress || rippleView.parent != null) {
+        if (rippleView.rippleInProgress() || rippleView.parent != null) {
             // Skip if ripple is still playing, or not playing but already added the parent
             // (which might happen just before the animation starts or right after
             // the animation ends.)
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
index e82d0ea..3808ab7 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java
@@ -30,7 +30,7 @@
 
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.ripple.RippleShader.RippleShape;
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
 
 /**
  * A WirelessChargingAnimation is a view containing view + animation for wireless charging.
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index c0cc6b4..36103f8 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -33,9 +33,9 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
-import com.android.systemui.ripple.RippleShader.RippleShape;
-import com.android.systemui.ripple.RippleView;
-import com.android.systemui.ripple.RippleViewKt;
+import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
+import com.android.systemui.surfaceeffects.ripple.RippleView;
 
 import java.text.NumberFormat;
 
@@ -150,7 +150,7 @@
             mRippleView.setColor(color, 28);
         } else {
             mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
-            mRippleView.setColor(color, RippleViewKt.RIPPLE_DEFAULT_ALPHA);
+            mRippleView.setColor(color, RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA);
         }
 
         OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index 500f280..e8e1f2e 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -34,6 +34,8 @@
 import com.android.systemui.classifier.FalsingDataProvider.SessionListener;
 import com.android.systemui.classifier.HistoryTracker.BeliefListener;
 import com.android.systemui.dagger.qualifiers.TestHarness;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -65,6 +67,7 @@
     private static final double FALSE_BELIEF_THRESHOLD = 0.9;
 
     private final FalsingDataProvider mDataProvider;
+    private final LongTapClassifier mLongTapClassifier;
     private final SingleTapClassifier mSingleTapClassifier;
     private final DoubleTapClassifier mDoubleTapClassifier;
     private final HistoryTracker mHistoryTracker;
@@ -73,6 +76,7 @@
     private final boolean mTestHarness;
     private final MetricsLogger mMetricsLogger;
     private int mIsFalseTouchCalls;
+    private FeatureFlags mFeatureFlags;
     private static final Queue<String> RECENT_INFO_LOG =
             new ArrayDeque<>(RECENT_INFO_LOG_SIZE + 1);
     private static final Queue<DebugSwipeRecord> RECENT_SWIPES =
@@ -175,19 +179,23 @@
     public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider,
             MetricsLogger metricsLogger,
             @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers,
-            SingleTapClassifier singleTapClassifier, DoubleTapClassifier doubleTapClassifier,
-            HistoryTracker historyTracker, KeyguardStateController keyguardStateController,
+            SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier,
+            DoubleTapClassifier doubleTapClassifier, HistoryTracker historyTracker,
+            KeyguardStateController keyguardStateController,
             AccessibilityManager accessibilityManager,
-            @TestHarness boolean testHarness) {
+            @TestHarness boolean testHarness,
+            FeatureFlags featureFlags) {
         mDataProvider = falsingDataProvider;
         mMetricsLogger = metricsLogger;
         mClassifiers = classifiers;
         mSingleTapClassifier = singleTapClassifier;
+        mLongTapClassifier = longTapClassifier;
         mDoubleTapClassifier = doubleTapClassifier;
         mHistoryTracker = historyTracker;
         mKeyguardStateController = keyguardStateController;
         mAccessibilityManager = accessibilityManager;
         mTestHarness = testHarness;
+        mFeatureFlags = featureFlags;
 
         mDataProvider.addSessionListener(mSessionListener);
         mDataProvider.addGestureCompleteListener(mGestureFinalizedListener);
@@ -223,7 +231,8 @@
 
         // check for false tap if it is a seekbar interaction
         if (interactionType == MEDIA_SEEKBAR) {
-            localResult[0] &= isFalseTap(LOW_PENALTY);
+            localResult[0] &= isFalseTap(mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY)
+                    ? FalsingManager.MODERATE_PENALTY : FalsingManager.LOW_PENALTY);
         }
 
         logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]);
@@ -313,6 +322,58 @@
     }
 
     @Override
+    public boolean isFalseLongTap(@Penalty int penalty) {
+        if (!mFeatureFlags.isEnabled(Flags.FALSING_FOR_LONG_TAPS)) {
+            return false;
+        }
+
+        checkDestroyed();
+
+        if (skipFalsing(GENERIC)) {
+            mPriorResults = getPassedResult(1);
+            logDebug("Skipped falsing");
+            return false;
+        }
+
+        double falsePenalty = 0;
+        switch(penalty) {
+            case NO_PENALTY:
+                falsePenalty = 0;
+                break;
+            case LOW_PENALTY:
+                falsePenalty = 0.1;
+                break;
+            case MODERATE_PENALTY:
+                falsePenalty = 0.3;
+                break;
+            case HIGH_PENALTY:
+                falsePenalty = 0.6;
+                break;
+        }
+
+        FalsingClassifier.Result longTapResult =
+                mLongTapClassifier.isTap(mDataProvider.getRecentMotionEvents().isEmpty()
+                        ? mDataProvider.getPriorMotionEvents()
+                        : mDataProvider.getRecentMotionEvents(), falsePenalty);
+        mPriorResults = Collections.singleton(longTapResult);
+
+        if (!longTapResult.isFalse()) {
+            if (mDataProvider.isJustUnlockedWithFace()) {
+                // Immediately pass if a face is detected.
+                mPriorResults = getPassedResult(1);
+                logDebug("False Long Tap: false (face detected)");
+            } else {
+                mPriorResults = getPassedResult(0.1);
+                logDebug("False Long Tap: false (default)");
+            }
+            return false;
+        } else {
+            logDebug("False Long Tap: " + longTapResult.isFalse() + " (simple)");
+            return longTapResult.isFalse();
+        }
+    }
+
+    @Override
     public boolean isFalseDoubleTap() {
         checkDestroyed();
 
@@ -337,7 +398,8 @@
                 || mTestHarness
                 || mDataProvider.isJustUnlockedWithFace()
                 || mDataProvider.isDocked()
-                || mAccessibilityManager.isTouchExplorationEnabled();
+                || mAccessibilityManager.isTouchExplorationEnabled()
+                || mDataProvider.isA11yAction();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt
new file mode 100644
index 0000000..63d57cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingA11yDelegate.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.classifier
+
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK
+import javax.inject.Inject
+
+/**
+ * Class that injects an artificial tap into the falsing collector.
+ *
+ * This is used for views that can be interacted with by A11y services and have falsing checks, as
+ * the gestures made by the A11y framework do not propagate motion events down the view hierarchy.
+ */
+class FalsingA11yDelegate @Inject constructor(private val falsingCollector: FalsingCollector) :
+    View.AccessibilityDelegate() {
+    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+        if (action == ACTION_CLICK) {
+            falsingCollector.onA11yAction()
+        }
+        return super.performAccessibilityAction(host, action, args)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
index 3871248..6670108 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
@@ -44,9 +44,6 @@
     void onQsDown();
 
     /** */
-    void setQsExpanded(boolean expanded);
-
-    /** */
     boolean shouldEnforceBouncer();
 
     /** */
@@ -135,5 +132,8 @@
 
     /** */
     void updateFalseConfidence(FalsingClassifier.Result result);
+
+    /** Indicates an a11y action was made. */
+    void onA11yAction();
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
index 28aac05..cc25368 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
@@ -49,10 +49,6 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-    }
-
-    @Override
     public boolean shouldEnforceBouncer() {
         return false;
     }
@@ -161,4 +157,8 @@
     @Override
     public void updateFalseConfidence(FalsingClassifier.Result result) {
     }
+
+    @Override
+    public void onA11yAction() {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index f5f9655..8bdef13 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -23,6 +23,8 @@
 import android.util.Log;
 import android.view.MotionEvent;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.dagger.SysUISingleton;
@@ -30,6 +32,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -133,6 +136,7 @@
             ProximitySensor proximitySensor,
             StatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             BatteryController batteryController,
             DockManager dockManager,
             @Main DelayableExecutor mainExecutor,
@@ -157,6 +161,8 @@
 
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
 
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+
         mBatteryController.addCallback(mBatteryListener);
         mDockManager.addListener(mDockEventListener);
     }
@@ -193,8 +199,8 @@
     public void onQsDown() {
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    @VisibleForTesting
+    void onQsExpansionChanged(Boolean expanded) {
         if (expanded) {
             unregisterSensors();
         } else if (mSessionStarted) {
@@ -369,6 +375,15 @@
         mHistoryTracker.addResults(Collections.singleton(result), mSystemClock.uptimeMillis());
     }
 
+    @Override
+    public void onA11yAction() {
+        if (mPendingDownEvent != null) {
+            mPendingDownEvent.recycle();
+            mPendingDownEvent = null;
+        }
+        mFalsingDataProvider.onA11yAction();
+    }
+
     private boolean shouldSessionBeActive() {
         return mScreenOn && (mState == StatusBarState.KEYGUARD) && !mShowingAod;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index 3991a35..09ebeea 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -59,6 +59,7 @@
     private MotionEvent mFirstRecentMotionEvent;
     private MotionEvent mLastMotionEvent;
     private boolean mJustUnlockedWithFace;
+    private boolean mA11YAction;
 
     @Inject
     public FalsingDataProvider(
@@ -124,6 +125,7 @@
             mPriorMotionEvents = mRecentMotionEvents;
             mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS);
         }
+        mA11YAction = false;
     }
 
     /** Returns screen width in pixels. */
@@ -334,6 +336,17 @@
         mGestureFinalizedListeners.remove(listener);
     }
 
+    /** Return whether last gesture was an A11y action. */
+    public boolean isA11yAction() {
+        return mA11YAction;
+    }
+
+    /** Set whether last gesture was an A11y action. */
+    public void onA11yAction() {
+        completePriorGesture();
+        this.mA11YAction = true;
+    }
+
     void onSessionStarted() {
         mSessionListeners.forEach(SessionListener::onSessionStarted);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
index 5d04b5f..c4723e8 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
@@ -139,6 +139,11 @@
     }
 
     @Override
+    public boolean isFalseLongTap(int penalty) {
+        return mInternalFalsingManager.isFalseLongTap(penalty);
+    }
+
+    @Override
     public boolean isFalseDoubleTap() {
         return mInternalFalsingManager.isFalseDoubleTap();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
index 7b7f17e..5302af9 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingModule.java
@@ -40,6 +40,7 @@
 public interface FalsingModule {
     String BRIGHT_LINE_GESTURE_CLASSIFERS = "bright_line_gesture_classifiers";
     String SINGLE_TAP_TOUCH_SLOP = "falsing_single_tap_touch_slop";
+    String LONG_TAP_TOUCH_SLOP = "falsing_long_tap_slop";
     String DOUBLE_TAP_TOUCH_SLOP = "falsing_double_tap_touch_slop";
     String DOUBLE_TAP_TIMEOUT_MS = "falsing_double_tap_timeout_ms";
 
@@ -81,4 +82,11 @@
     static float providesSingleTapTouchSlop(ViewConfiguration viewConfiguration) {
         return viewConfiguration.getScaledTouchSlop();
     }
+
+    /** */
+    @Provides
+    @Named(LONG_TAP_TOUCH_SLOP)
+    static float providesLongTapTouchSlop(ViewConfiguration viewConfiguration) {
+        return viewConfiguration.getScaledTouchSlop() * 1.25f;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/LongTapClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/LongTapClassifier.java
new file mode 100644
index 0000000..1963e69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/LongTapClassifier.java
@@ -0,0 +1,34 @@
+/*
+ * 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.classifier;
+
+import static com.android.systemui.classifier.FalsingModule.LONG_TAP_TOUCH_SLOP;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * Falsing classifier that accepts or rejects a gesture as a long tap.
+ */
+public class LongTapClassifier extends TapClassifier{
+
+    @Inject
+    LongTapClassifier(FalsingDataProvider dataProvider,
+            @Named(LONG_TAP_TOUCH_SLOP) float touchSlop) {
+        super(dataProvider, touchSlop);
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/SingleTapClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/SingleTapClassifier.java
index bd6fbfb..7a7401d 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/SingleTapClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/SingleTapClassifier.java
@@ -18,57 +18,17 @@
 
 import static com.android.systemui.classifier.FalsingModule.SINGLE_TAP_TOUCH_SLOP;
 
-import android.view.MotionEvent;
-
-import java.util.List;
-
 import javax.inject.Inject;
 import javax.inject.Named;
 
 /**
- * Falsing classifier that accepts or rejects a single gesture as a tap.
+ * Falsing classifier that accepts or rejects a gesture as a single tap.
  */
-public class SingleTapClassifier extends FalsingClassifier {
-    private final float mTouchSlop;
+public class SingleTapClassifier extends TapClassifier {
 
     @Inject
     SingleTapClassifier(FalsingDataProvider dataProvider,
             @Named(SINGLE_TAP_TOUCH_SLOP) float touchSlop) {
-        super(dataProvider);
-        mTouchSlop = touchSlop;
-    }
-
-    @Override
-    Result calculateFalsingResult(
-            @Classifier.InteractionType int interactionType,
-            double historyBelief, double historyConfidence) {
-        return isTap(getRecentMotionEvents(), 0.5);
-    }
-
-    /** Given a list of {@link android.view.MotionEvent}'s, returns true if the look like a tap. */
-    public Result isTap(List<MotionEvent> motionEvents, double falsePenalty) {
-        if (motionEvents.isEmpty()) {
-            return falsed(0, "no motion events");
-        }
-        float downX = motionEvents.get(0).getX();
-        float downY = motionEvents.get(0).getY();
-
-        for (MotionEvent event : motionEvents) {
-            String reason;
-            if (Math.abs(event.getX() - downX) >= mTouchSlop) {
-                reason = "dX too big for a tap: "
-                        + Math.abs(event.getX() - downX)
-                        + "vs "
-                        + mTouchSlop;
-                return falsed(falsePenalty, reason);
-            } else if (Math.abs(event.getY() - downY) >= mTouchSlop) {
-                reason = "dY too big for a tap: "
-                        + Math.abs(event.getY() - downY)
-                        + " vs "
-                        + mTouchSlop;
-                return falsed(falsePenalty, reason);
-            }
-        }
-        return Result.passed(0);
+        super(dataProvider, touchSlop);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TapClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TapClassifier.java
new file mode 100644
index 0000000..e24cfaa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/TapClassifier.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.systemui.classifier;
+
+import android.view.MotionEvent;
+
+import java.util.List;
+
+/**
+ * Falsing classifier that accepts or rejects a gesture as a tap.
+ */
+public abstract class TapClassifier extends FalsingClassifier{
+    private final float mTouchSlop;
+
+    TapClassifier(FalsingDataProvider dataProvider,
+            float touchSlop) {
+        super(dataProvider);
+        mTouchSlop = touchSlop;
+    }
+
+    @Override
+    Result calculateFalsingResult(
+            @Classifier.InteractionType int interactionType,
+            double historyBelief, double historyConfidence) {
+        return isTap(getRecentMotionEvents(), 0.5);
+    }
+
+    /** Given a list of {@link android.view.MotionEvent}'s, returns true if the look like a tap. */
+    public Result isTap(List<MotionEvent> motionEvents, double falsePenalty) {
+        if (motionEvents.isEmpty()) {
+            return falsed(0, "no motion events");
+        }
+        float downX = motionEvents.get(0).getX();
+        float downY = motionEvents.get(0).getY();
+
+        for (MotionEvent event : motionEvents) {
+            String reason;
+            if (Math.abs(event.getX() - downX) >= mTouchSlop) {
+                reason = "dX too big for a tap: "
+                        + Math.abs(event.getX() - downX)
+                        + "vs "
+                        + mTouchSlop;
+                return falsed(falsePenalty, reason);
+            } else if (Math.abs(event.getY() - downY) >= mTouchSlop) {
+                reason = "dY too big for a tap: "
+                        + Math.abs(event.getY() - downY)
+                        + " vs "
+                        + mTouchSlop;
+                return falsed(falsePenalty, reason);
+            }
+        }
+        return Result.passed(0);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bfb27a4..c853671 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -31,6 +31,7 @@
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
+import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
 
 import static java.util.Objects.requireNonNull;
 
@@ -73,6 +74,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.screenshot.TimeoutHandler;
 
 import java.io.IOException;
@@ -101,6 +103,8 @@
     private final ClipboardOverlayWindow mWindow;
     private final TimeoutHandler mTimeoutHandler;
     private final TextClassifier mTextClassifier;
+    private final ClipboardOverlayUtils mClipboardUtils;
+    private final FeatureFlags mFeatureFlags;
 
     private final ClipboardOverlayView mView;
 
@@ -119,11 +123,15 @@
     private Animator mExitAnimator;
     private Animator mEnterAnimator;
 
+    private Runnable mOnUiUpdate;
+
     private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
             new ClipboardOverlayView.ClipboardOverlayCallbacks() {
                 @Override
                 public void onInteraction() {
-                    mTimeoutHandler.resetTimeout();
+                    if (mOnUiUpdate != null) {
+                        mOnUiUpdate.run();
+                    }
                 }
 
                 @Override
@@ -178,7 +186,10 @@
             ClipboardOverlayWindow clipboardOverlayWindow,
             BroadcastDispatcher broadcastDispatcher,
             BroadcastSender broadcastSender,
-            TimeoutHandler timeoutHandler, UiEventLogger uiEventLogger) {
+            TimeoutHandler timeoutHandler,
+            FeatureFlags featureFlags,
+            ClipboardOverlayUtils clipboardUtils,
+            UiEventLogger uiEventLogger) {
         mBroadcastDispatcher = broadcastDispatcher;
         mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
         final Context displayContext = context.createDisplayContext(getDefaultDisplay());
@@ -199,6 +210,9 @@
         mTimeoutHandler = timeoutHandler;
         mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
 
+        mFeatureFlags = featureFlags;
+        mClipboardUtils = clipboardUtils;
+
         mView.setCallbacks(mClipboardCallbacks);
 
 
@@ -257,11 +271,13 @@
         boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
                 && clipData.getDescription().getExtras()
                 .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
+        boolean isRemote = mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)
+                && mClipboardUtils.isRemoteCopy(mContext, clipData, clipSource);
         if (clipData == null || clipData.getItemCount() == 0) {
             mView.showDefaultTextPreview();
         } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
             ClipData.Item item = clipData.getItemAt(0);
-            if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+            if (isRemote || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
                     CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
                 if (item.getTextLinks() != null) {
                     AsyncTask.execute(() -> classifyText(clipData.getItemAt(0), clipSource));
@@ -287,7 +303,13 @@
         maybeShowRemoteCopy(clipData);
         animateIn();
         mView.announceForAccessibility(accessibilityAnnouncement);
-        mTimeoutHandler.resetTimeout();
+        if (isRemote) {
+            mTimeoutHandler.cancelTimeout();
+            mOnUiUpdate = null;
+        } else {
+            mOnUiUpdate = mTimeoutHandler::resetTimeout;
+            mOnUiUpdate.run();
+        }
     }
 
     private void maybeShowRemoteCopy(ClipData clipData) {
@@ -427,7 +449,9 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
-                mTimeoutHandler.resetTimeout();
+                if (mOnUiUpdate != null) {
+                    mOnUiUpdate.run();
+                }
             }
         });
         mEnterAnimator.start();
@@ -459,7 +483,7 @@
         anim.start();
     }
 
-    private void hideImmediate() {
+    void hideImmediate() {
         // Note this may be called multiple times if multiple dismissal events happen at the same
         // time.
         mTimeoutHandler.cancelTimeout();
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.java
new file mode 100644
index 0000000..cece764
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtils.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.systemui.clipboardoverlay;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+class ClipboardOverlayUtils {
+
+    @Inject
+    ClipboardOverlayUtils() {
+    }
+
+    boolean isRemoteCopy(Context context, ClipData clipData, String clipSource) {
+        if (clipData != null && clipData.getDescription().getExtras() != null
+                && clipData.getDescription().getExtras().getBoolean(
+                ClipDescription.EXTRA_IS_REMOTE_DEVICE)) {
+            ComponentName remoteComponent = ComponentName.unflattenFromString(
+                    context.getResources().getString(R.string.config_remoteCopyPackage));
+            if (remoteComponent != null) {
+                return remoteComponent.getPackageName().equals(clipSource);
+            }
+        }
+        return false;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
index bebade0..08e8293 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or
@@ -30,4 +31,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : ContentDescription()
+
+    companion object {
+        /**
+         * Returns the loaded content description string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over
+         * this method. This should only be used for testing or concatenation purposes.
+         */
+        fun ContentDescription?.loadContentDescription(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.description
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
index 5d0e08f..4a56932 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference]
@@ -31,4 +32,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : Text()
+
+    companion object {
+        /**
+         * Returns the loaded test string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This
+         * should only be used for testing or concatenation purposes.
+         */
+        fun Text?.loadText(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.text
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
index 588ef5c..4dfcd63 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt
@@ -16,16 +16,120 @@
 
 package com.android.systemui.controls
 
+import android.Manifest
+import android.content.ComponentName
 import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE
+import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
+import android.os.UserHandle
+import android.service.controls.ControlsProviderService
+import androidx.annotation.WorkerThread
 import com.android.settingslib.applications.DefaultAppInfo
+import java.util.Objects
 
 class ControlsServiceInfo(
-    context: Context,
+    private val context: Context,
     val serviceInfo: ServiceInfo
 ) : DefaultAppInfo(
     context,
     context.packageManager,
     context.userId,
     serviceInfo.componentName
-)
\ No newline at end of file
+) {
+    private val _panelActivity: ComponentName?
+
+    init {
+        val metadata = serviceInfo.metaData
+                ?.getString(ControlsProviderService.META_DATA_PANEL_ACTIVITY) ?: ""
+        val unflatenned = ComponentName.unflattenFromString(metadata)
+        if (unflatenned != null && unflatenned.packageName == componentName.packageName) {
+            _panelActivity = unflatenned
+        } else {
+            _panelActivity = null
+        }
+    }
+
+    /**
+     * Component name of an activity that will be shown embedded in the device controls space
+     * instead of using the controls rendered by SystemUI.
+     *
+     * The activity must be in the same package, exported, enabled and protected by the
+     * [Manifest.permission.BIND_CONTROLS] permission.
+     */
+    var panelActivity: ComponentName? = null
+        private set
+
+    private var resolved: Boolean = false
+
+    @WorkerThread
+    fun resolvePanelActivity() {
+        if (resolved) return
+        resolved = true
+        panelActivity = _panelActivity?.let {
+            val resolveInfos = mPm.queryIntentActivitiesAsUser(
+                    Intent().setComponent(it),
+                    PackageManager.ResolveInfoFlags.of(
+                            MATCH_DIRECT_BOOT_AWARE.toLong() or
+                                    MATCH_DIRECT_BOOT_UNAWARE.toLong()
+                    ),
+                    UserHandle.of(userId)
+            )
+            if (resolveInfos.isNotEmpty() && verifyResolveInfo(resolveInfos[0])) {
+                it
+            } else {
+                null
+            }
+        }
+    }
+
+    /**
+     * Verifies that the panel activity is enabled, exported and protected by the correct
+     * permission. This last check is to prevent apps from forgetting to protect the activity, as
+     * they won't be able to see the panel until they do.
+     */
+    @WorkerThread
+    private fun verifyResolveInfo(resolveInfo: ResolveInfo): Boolean {
+        return resolveInfo.activityInfo?.let {
+            it.permission == Manifest.permission.BIND_CONTROLS &&
+                    it.exported && isComponentActuallyEnabled(it)
+        } ?: false
+    }
+
+    @WorkerThread
+    private fun isComponentActuallyEnabled(activityInfo: ActivityInfo): Boolean {
+        return when (mPm.getComponentEnabledSetting(activityInfo.componentName)) {
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false
+            PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> activityInfo.enabled
+            else -> false
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ControlsServiceInfo &&
+                userId == other.userId &&
+                componentName == other.componentName &&
+                panelActivity == other.panelActivity
+    }
+
+    override fun hashCode(): Int {
+        return Objects.hash(userId, componentName, panelActivity)
+    }
+
+    fun copy(): ControlsServiceInfo {
+        return ControlsServiceInfo(context, serviceInfo).also {
+            it.panelActivity = this.panelActivity
+        }
+    }
+
+    override fun toString(): String {
+        return """
+            ControlsServiceInfo(serviceInfo=$serviceInfo, panelActivity=$panelActivity, resolved=$resolved)
+        """.trimIndent()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
index 5e8ce6d..7df0865 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt
@@ -18,40 +18,47 @@
 
 import android.app.ActivityOptions
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.os.Bundle
+import android.util.Log
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.widget.Button
 import android.widget.TextView
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
 import androidx.activity.ComponentActivity
 import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.RecyclerView
 import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.CustomIconCache
 import com.android.systemui.controls.controller.ControlsControllerImpl
 import com.android.systemui.controls.controller.StructureInfo
 import com.android.systemui.controls.ui.ControlsActivity
 import com.android.systemui.controls.ui.ControlsUiController
-import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
 /**
  * Activity for rearranging and removing controls for a given structure
  */
-class ControlsEditingActivity @Inject constructor(
+open class ControlsEditingActivity @Inject constructor(
+    @Main private val mainExecutor: Executor,
     private val controller: ControlsControllerImpl,
-    private val broadcastDispatcher: BroadcastDispatcher,
+    private val userTracker: UserTracker,
     private val customIconCache: CustomIconCache,
     private val uiController: ControlsUiController
 ) : ComponentActivity() {
 
     companion object {
+        private const val DEBUG = false
         private const val TAG = "ControlsEditingActivity"
-        private const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE
+        const val EXTRA_STRUCTURE = ControlsFavoritingActivity.EXTRA_STRUCTURE
         private val SUBTITLE_ID = R.string.controls_favorite_rearrange
         private val EMPTY_TEXT_ID = R.string.controls_favorite_removed
     }
@@ -62,17 +69,24 @@
     private lateinit var subtitle: TextView
     private lateinit var saveButton: View
 
-    private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+    private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback {
         private val startingUser = controller.currentUserId
 
-        override fun onUserSwitched(newUserId: Int) {
-            if (newUserId != startingUser) {
-                stopTracking()
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            if (newUser != startingUser) {
+                userTracker.removeCallback(this)
                 finish()
             }
         }
     }
 
+    private val mOnBackInvokedCallback = OnBackInvokedCallback {
+        if (DEBUG) {
+            Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback")
+        }
+        onBackPressed()
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
@@ -93,12 +107,23 @@
         super.onStart()
         setUpList()
 
-        currentUserTracker.startTracking()
+        userTracker.addCallback(userTrackerCallback, mainExecutor)
+
+        if (DEBUG) {
+            Log.d(TAG, "Registered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback)
     }
 
     override fun onStop() {
         super.onStop()
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
+
+        if (DEBUG) {
+            Log.d(TAG, "Unregistered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback)
     }
 
     override fun onBackPressed() {
@@ -226,7 +251,7 @@
     }
 
     override fun onDestroy() {
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
         super.onDestroy()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
index be572c5..3e97d31 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt
@@ -20,10 +20,12 @@
 import android.animation.AnimatorListenerAdapter
 import android.app.ActivityOptions
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.content.res.Configuration
 import android.os.Bundle
 import android.text.TextUtils
+import android.util.Log
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
@@ -32,11 +34,12 @@
 import android.widget.FrameLayout
 import android.widget.TextView
 import android.widget.Toast
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
 import androidx.activity.ComponentActivity
 import androidx.viewpager2.widget.ViewPager2
 import com.android.systemui.Prefs
 import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.TooltipManager
 import com.android.systemui.controls.controller.ControlsControllerImpl
@@ -44,21 +47,22 @@
 import com.android.systemui.controls.ui.ControlsActivity
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.settings.UserTracker
 import java.text.Collator
 import java.util.concurrent.Executor
 import java.util.function.Consumer
 import javax.inject.Inject
 
-class ControlsFavoritingActivity @Inject constructor(
+open class ControlsFavoritingActivity @Inject constructor(
     @Main private val executor: Executor,
     private val controller: ControlsControllerImpl,
     private val listingController: ControlsListingController,
-    private val broadcastDispatcher: BroadcastDispatcher,
+    private val userTracker: UserTracker,
     private val uiController: ControlsUiController
 ) : ComponentActivity() {
 
     companion object {
+        private const val DEBUG = false
         private const val TAG = "ControlsFavoritingActivity"
 
         // If provided and no structure is available, use as the title
@@ -67,7 +71,7 @@
         // If provided, show this structure page first
         const val EXTRA_STRUCTURE = "extra_structure"
         const val EXTRA_SINGLE_STRUCTURE = "extra_single_structure"
-        internal const val EXTRA_FROM_PROVIDER_SELECTOR = "extra_from_provider_selector"
+        const val EXTRA_FROM_PROVIDER_SELECTOR = "extra_from_provider_selector"
         private const val TOOLTIP_PREFS_KEY = Prefs.Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
         private const val TOOLTIP_MAX_SHOWN = 2
     }
@@ -91,17 +95,24 @@
     private var cancelLoadRunnable: Runnable? = null
     private var isPagerLoaded = false
 
-    private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+    private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback {
         private val startingUser = controller.currentUserId
 
-        override fun onUserSwitched(newUserId: Int) {
-            if (newUserId != startingUser) {
-                stopTracking()
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            if (newUser != startingUser) {
+                userTracker.removeCallback(this)
                 finish()
             }
         }
     }
 
+    private val mOnBackInvokedCallback = OnBackInvokedCallback {
+        if (DEBUG) {
+            Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback")
+        }
+        onBackPressed()
+    }
+
     private val listingCallback = object : ControlsListingController.ControlsListingCallback {
 
         override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
@@ -346,13 +357,19 @@
     override fun onPause() {
         super.onPause()
         mTooltipManager?.hide(false)
-    }
+   }
 
     override fun onStart() {
         super.onStart()
 
         listingController.addCallback(listingCallback)
-        currentUserTracker.startTracking()
+        userTracker.addCallback(userTrackerCallback, executor)
+
+        if (DEBUG) {
+            Log.d(TAG, "Registered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback)
     }
 
     override fun onResume() {
@@ -365,13 +382,19 @@
             loadControls()
             isPagerLoaded = true
         }
-    }
+   }
 
     override fun onStop() {
         super.onStop()
 
         listingController.removeCallback(listingCallback)
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
+
+        if (DEBUG) {
+            Log.d(TAG, "Unregistered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.unregisterOnBackInvokedCallback(
+                mOnBackInvokedCallback)
     }
 
     override fun onConfigurationChanged(newConfig: Configuration) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
index 2d76ff2..c6428ef 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
@@ -18,17 +18,23 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.content.pm.ServiceInfo
 import android.os.UserHandle
 import android.service.controls.ControlsProviderService
 import android.util.Log
 import com.android.internal.annotations.VisibleForTesting
 import com.android.settingslib.applications.ServiceListing
 import com.android.settingslib.widget.CandidateInfo
+import com.android.systemui.Dumpable
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.indentIfPossible
+import java.io.PrintWriter
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicInteger
 import javax.inject.Inject
@@ -57,16 +63,19 @@
     private val context: Context,
     @Background private val backgroundExecutor: Executor,
     private val serviceListingBuilder: (Context) -> ServiceListing,
-    userTracker: UserTracker
-) : ControlsListingController {
+    private val userTracker: UserTracker,
+    dumpManager: DumpManager,
+    featureFlags: FeatureFlags
+) : ControlsListingController, Dumpable {
 
     @Inject
-    constructor(context: Context, executor: Executor, userTracker: UserTracker): this(
-            context,
-            executor,
-            ::createServiceListing,
-            userTracker
-    )
+    constructor(
+            context: Context,
+            @Background executor: Executor,
+            userTracker: UserTracker,
+            dumpManager: DumpManager,
+            featureFlags: FeatureFlags
+    ) : this(context, executor, ::createServiceListing, userTracker, dumpManager, featureFlags)
 
     private var serviceListing = serviceListingBuilder(context)
     // All operations in background thread
@@ -76,27 +85,26 @@
         private const val TAG = "ControlsListingControllerImpl"
     }
 
-    private var availableComponents = emptySet<ComponentName>()
-    private var availableServices = emptyList<ServiceInfo>()
+    private var availableServices = emptyList<ControlsServiceInfo>()
     private var userChangeInProgress = AtomicInteger(0)
 
     override var currentUserId = userTracker.userId
         private set
 
-    private val serviceListingCallback = ServiceListing.Callback {
-        val newServices = it.toList()
-        val newComponents =
-            newServices.mapTo(mutableSetOf<ComponentName>(), { s -> s.getComponentName() })
-
+    private val serviceListingCallback = ServiceListing.Callback { list ->
+        Log.d(TAG, "ServiceConfig reloaded, count: ${list.size}")
+        val newServices = list.map { ControlsServiceInfo(userTracker.userContext, it) }
+        // After here, `list` is not captured, so we don't risk modifying it outside of the callback
         backgroundExecutor.execute {
             if (userChangeInProgress.get() > 0) return@execute
-            if (!newComponents.equals(availableComponents)) {
-                Log.d(TAG, "ServiceConfig reloaded, count: ${newComponents.size}")
-                availableComponents = newComponents
+            if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) {
+                newServices.forEach(ControlsServiceInfo::resolvePanelActivity)
+            }
+
+            if (newServices != availableServices) {
                 availableServices = newServices
-                val currentServices = getCurrentServices()
                 callbacks.forEach {
-                    it.onServicesUpdated(currentServices)
+                    it.onServicesUpdated(getCurrentServices())
                 }
             }
         }
@@ -104,6 +112,7 @@
 
     init {
         Log.d(TAG, "Initializing")
+        dumpManager.registerDumpable(TAG, this)
         serviceListing.addCallback(serviceListingCallback)
         serviceListing.setListening(true)
         serviceListing.reload()
@@ -165,7 +174,7 @@
      *         [ControlsProviderService]
      */
     override fun getCurrentServices(): List<ControlsServiceInfo> =
-            availableServices.map { ControlsServiceInfo(context, it) }
+            availableServices.map(ControlsServiceInfo::copy)
 
     /**
      * Get the localized label for the component.
@@ -174,7 +183,15 @@
      * @return a label as returned by [CandidateInfo.loadLabel] or `null`.
      */
     override fun getAppLabel(name: ComponentName): CharSequence? {
-        return getCurrentServices().firstOrNull { it.componentName == name }
+        return availableServices.firstOrNull { it.componentName == name }
                 ?.loadLabel()
     }
+
+    override fun dump(writer: PrintWriter, args: Array<out String>) {
+        writer.println("ControlsListingController:")
+        writer.asIndenting().indentIfPossible {
+            println("Callbacks: $callbacks")
+            println("Services: ${getCurrentServices()}")
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
index b26615f..90bc5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt
@@ -18,58 +18,68 @@
 
 import android.app.ActivityOptions
 import android.content.ComponentName
+import android.content.Context
 import android.content.Intent
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.widget.Button
 import android.widget.TextView
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
 import androidx.activity.ComponentActivity
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
 import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.ui.ControlsActivity
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.settings.UserTracker
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
 /**
  * Activity to select an application to favorite the [Control] provided by them.
  */
-class ControlsProviderSelectorActivity @Inject constructor(
+open class ControlsProviderSelectorActivity @Inject constructor(
     @Main private val executor: Executor,
     @Background private val backExecutor: Executor,
     private val listingController: ControlsListingController,
     private val controlsController: ControlsController,
-    private val broadcastDispatcher: BroadcastDispatcher,
+    private val userTracker: UserTracker,
     private val uiController: ControlsUiController
 ) : ComponentActivity() {
 
     companion object {
+        private const val DEBUG = false
         private const val TAG = "ControlsProviderSelectorActivity"
         const val BACK_SHOULD_EXIT = "back_should_exit"
     }
     private var backShouldExit = false
     private lateinit var recyclerView: RecyclerView
-    private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+    private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback {
         private val startingUser = listingController.currentUserId
 
-        override fun onUserSwitched(newUserId: Int) {
-            if (newUserId != startingUser) {
-                stopTracking()
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            if (newUser != startingUser) {
+                userTracker.removeCallback(this)
                 finish()
             }
         }
     }
 
+    private val mOnBackInvokedCallback = OnBackInvokedCallback {
+        if (DEBUG) {
+            Log.d(TAG, "Predictive Back dispatcher called mOnBackInvokedCallback")
+        }
+        onBackPressed()
+    }
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
@@ -119,7 +129,7 @@
 
     override fun onStart() {
         super.onStart()
-        currentUserTracker.startTracking()
+        userTracker.addCallback(userTrackerCallback, executor)
 
         recyclerView.alpha = 0.0f
         recyclerView.adapter = AppAdapter(
@@ -141,11 +151,22 @@
                 }
             })
         }
+
+        if (DEBUG) {
+            Log.d(TAG, "Registered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback)
     }
 
     override fun onStop() {
         super.onStop()
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
+
+        if (DEBUG) {
+            Log.d(TAG, "Unregistered onBackInvokedCallback")
+        }
+        onBackInvokedDispatcher.unregisterOnBackInvokedCallback(mOnBackInvokedCallback)
     }
 
     /**
@@ -169,7 +190,7 @@
     }
 
     override fun onDestroy() {
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
         super.onDestroy()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
index b376455..86bde5c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestDialog.kt
@@ -19,6 +19,7 @@
 import android.app.AlertDialog
 import android.app.Dialog
 import android.content.ComponentName
+import android.content.Context
 import android.content.DialogInterface
 import android.content.Intent
 import android.os.Bundle
@@ -32,18 +33,20 @@
 import android.widget.TextView
 import androidx.activity.ComponentActivity
 import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.controller.ControlInfo
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.ui.RenderInfo
-import com.android.systemui.settings.CurrentUserTracker
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
 open class ControlsRequestDialog @Inject constructor(
+    @Main private val mainExecutor: Executor,
     private val controller: ControlsController,
-    private val broadcastDispatcher: BroadcastDispatcher,
+    private val userTracker: UserTracker,
     private val controlsListingController: ControlsListingController
 ) : ComponentActivity(), DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
 
@@ -58,12 +61,12 @@
         override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {}
     }
 
-    private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) {
+    private val userTrackerCallback: UserTracker.Callback = object : UserTracker.Callback {
         private val startingUser = controller.currentUserId
 
-        override fun onUserSwitched(newUserId: Int) {
-            if (newUserId != startingUser) {
-                stopTracking()
+        override fun onUserChanged(newUser: Int, userContext: Context) {
+            if (newUser != startingUser) {
+                userTracker.removeCallback(this)
                 finish()
             }
         }
@@ -72,7 +75,7 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        currentUserTracker.startTracking()
+        userTracker.addCallback(userTrackerCallback, mainExecutor)
         controlsListingController.addCallback(callback)
 
         val requestUser = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
@@ -118,7 +121,7 @@
 
     override fun onDestroy() {
         dialog?.dismiss()
-        currentUserTracker.stopTracking()
+        userTracker.removeCallback(userTrackerCallback)
         controlsListingController.removeCallback(callback)
         super.onDestroy()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index 77b6523..d3b5d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -21,6 +21,8 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.os.Bundle
+import android.os.RemoteException
+import android.service.dreams.IDreamManager
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowInsets
@@ -40,11 +42,13 @@
  */
 class ControlsActivity @Inject constructor(
     private val uiController: ControlsUiController,
-    private val broadcastDispatcher: BroadcastDispatcher
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val dreamManager: IDreamManager,
 ) : ComponentActivity() {
 
     private lateinit var parent: ViewGroup
     private lateinit var broadcastReceiver: BroadcastReceiver
+    private var mExitToDream: Boolean = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -81,17 +85,36 @@
 
         parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
         parent.alpha = 0f
-        uiController.show(parent, { finish() }, this)
+        uiController.show(parent, { finishOrReturnToDream() }, this)
 
         ControlsAnimations.enterAnimation(parent).start()
     }
 
-    override fun onBackPressed() {
+    override fun onResume() {
+        super.onResume()
+        mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false)
+    }
+
+    fun finishOrReturnToDream() {
+        if (mExitToDream) {
+            try {
+                mExitToDream = false
+                dreamManager.dream()
+                return
+            } catch (e: RemoteException) {
+                // Fall through
+            }
+        }
         finish()
     }
 
+    override fun onBackPressed() {
+        finishOrReturnToDream()
+    }
+
     override fun onStop() {
         super.onStop()
+        mExitToDream = false
 
         uiController.hide()
     }
@@ -106,7 +129,8 @@
         broadcastReceiver = object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 val action = intent.getAction()
-                if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                if (action == Intent.ACTION_SCREEN_OFF ||
+                    action == Intent.ACTION_DREAMING_STARTED) {
                     finish()
                 }
             }
@@ -114,6 +138,7 @@
 
         val filter = IntentFilter()
         filter.addAction(Intent.ACTION_SCREEN_OFF)
+        filter.addAction(Intent.ACTION_DREAMING_STARTED)
         broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index 822f8f2..c1cfbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -27,6 +27,7 @@
     companion object {
         public const val TAG = "ControlsUiController"
         public const val EXTRA_ANIMATE = "extra_animate"
+        public const val EXIT_TO_DREAM = "extra_exit_to_dream"
     }
 
     fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index bf7d716..6cb0e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -24,7 +24,6 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.SharedPreferences
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.service.controls.Control
@@ -59,7 +58,10 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.globalactions.GlobalActionsPopupMenu
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.shade.ShadeController
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
@@ -76,13 +78,14 @@
         @Main val uiExecutor: DelayableExecutor,
         @Background val bgExecutor: DelayableExecutor,
         val controlsListingController: Lazy<ControlsListingController>,
-        @Main val sharedPreferences: SharedPreferences,
         val controlActionCoordinator: ControlActionCoordinator,
         private val activityStarter: ActivityStarter,
         private val shadeController: ShadeController,
         private val iconCache: CustomIconCache,
         private val controlsMetricsLogger: ControlsMetricsLogger,
-        private val keyguardStateController: KeyguardStateController
+        private val keyguardStateController: KeyguardStateController,
+        private val userFileManager: UserFileManager,
+        private val userTracker: UserTracker,
 ) : ControlsUiController {
 
     companion object {
@@ -110,6 +113,12 @@
     private lateinit var onDismiss: Runnable
     private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow)
     private var retainCache = false
+    private val sharedPreferences
+        get() = userFileManager.getSharedPreferences(
+            fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+            mode = 0,
+            userId = userTracker.userId
+        )
 
     private val collator = Collator.getInstance(context.resources.configuration.locales[0])
     private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index fb01691..4eb444e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -25,6 +25,7 @@
 import com.android.systemui.people.widget.LaunchConversationActivity;
 import com.android.systemui.screenshot.LongScreenshotActivity;
 import com.android.systemui.sensorprivacy.SensorUseStartedActivity;
+import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity;
 import com.android.systemui.sensorprivacy.television.TvUnblockSensorActivity;
 import com.android.systemui.settings.brightness.BrightnessDialog;
 import com.android.systemui.statusbar.tv.notifications.TvNotificationPanelActivity;
@@ -142,4 +143,11 @@
     @ClassKey(HdmiCecSetMenuLanguageActivity.class)
     public abstract Activity bindHdmiCecSetMenuLanguageActivity(
             HdmiCecSetMenuLanguageActivity activity);
+
+    /** Inject into TvSensorPrivacyChangedActivity. */
+    @Binds
+    @IntoMap
+    @ClassKey(TvSensorPrivacyChangedActivity.class)
+    public abstract Activity bindTvSensorPrivacyChangedActivity(
+            TvSensorPrivacyChangedActivity activity);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 139a8b7..0664e9f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -17,9 +17,11 @@
 package com.android.systemui.dagger;
 
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
+import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.IActivityTaskManager;
 import android.app.INotificationManager;
@@ -68,6 +70,7 @@
 import android.os.ServiceManager;
 import android.os.UserManager;
 import android.os.Vibrator;
+import android.os.storage.StorageManager;
 import android.permission.PermissionManager;
 import android.safetycenter.SafetyCenterManager;
 import android.service.dreams.DreamService;
@@ -109,6 +112,7 @@
 /**
  * Provides Non-SystemUI, Framework-Owned instances to the dependency graph.
  */
+@SuppressLint("NonInjectedService")
 @Module
 public class FrameworkServicesModule {
     @Provides
@@ -137,6 +141,12 @@
 
     @Provides
     @Singleton
+    static AppOpsManager provideAppOpsManager(Context context) {
+        return context.getSystemService(AppOpsManager.class);
+    }
+
+    @Provides
+    @Singleton
     static AudioManager provideAudioManager(Context context) {
         return context.getSystemService(AudioManager.class);
     }
@@ -462,7 +472,13 @@
 
     @Provides
     @Singleton
-    static SubscriptionManager provideSubcriptionManager(Context context) {
+    static StorageManager provideStorageManager(Context context) {
+        return context.getSystemService(StorageManager.class);
+    }
+
+    @Provides
+    @Singleton
+    static SubscriptionManager provideSubscriptionManager(Context context) {
         return context.getSystemService(SubscriptionManager.class);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
index 9e33ee1..9e8c0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/GlobalRootComponent.java
@@ -21,22 +21,21 @@
 import com.android.systemui.dagger.qualifiers.InstrumentationTest;
 import com.android.systemui.util.InitializationChecker;
 
-import javax.inject.Singleton;
-
 import dagger.BindsInstance;
-import dagger.Component;
 
 /**
- * Root component for Dagger injection.
+ * Base root component for Dagger injection.
+ *
+ * This class is not actually annotated as a Dagger component, since it is not used directly as one.
+ * Doing so generates unnecessary code bloat.
+ *
+ * See {@link ReferenceGlobalRootComponent} for the one actually used by AOSP.
  */
-@Singleton
-@Component(modules = {GlobalModule.class})
 public interface GlobalRootComponent {
 
     /**
      * Builder for a GlobalRootComponent.
      */
-    @Component.Builder
     interface Builder {
         @BindsInstance
         Builder context(Context context);
@@ -51,7 +50,7 @@
     WMComponent.Builder getWMComponentBuilder();
 
     /**
-     * Builder for a {@link SysUIComponent}, which makes it a subcomponent of this class.
+     * Builder for a {@link ReferenceSysUIComponent}, which makes it a subcomponent of this class.
      */
     SysUIComponent.Builder getSysUIComponent();
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java
new file mode 100644
index 0000000..be93c9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java
@@ -0,0 +1,42 @@
+/*
+ * 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.dagger;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+/**
+ * Root component for Dagger injection used in AOSP.
+ */
+@Singleton
+@Component(modules = {GlobalModule.class})
+public interface ReferenceGlobalRootComponent extends GlobalRootComponent {
+
+    /**
+     * Builder for a ReferenceGlobalRootComponent.
+     */
+    @Component.Builder
+    interface Builder extends GlobalRootComponent.Builder {
+        ReferenceGlobalRootComponent build();
+    }
+
+    /**
+     * Builder for a {@link ReferenceSysUIComponent}, which makes it a subcomponent of this class.
+     */
+    ReferenceSysUIComponent.Builder getSysUIComponent();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
new file mode 100644
index 0000000..b30e0c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java
@@ -0,0 +1,53 @@
+/*
+ * 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.dagger;
+
+import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
+import com.android.systemui.statusbar.NotificationInsetsModule;
+import com.android.systemui.statusbar.QsFrameTranslateModule;
+
+import dagger.Subcomponent;
+
+/**
+ * Dagger Subcomponent for Core SysUI used in AOSP.
+ */
+@SysUISingleton
+@Subcomponent(modules = {
+        DefaultComponentBinder.class,
+        DependencyProvider.class,
+        NotificationInsetsModule.class,
+        QsFrameTranslateModule.class,
+        SystemUIBinder.class,
+        SystemUIModule.class,
+        SystemUICoreStartableModule.class,
+        ReferenceSystemUIModule.class})
+public interface ReferenceSysUIComponent extends SysUIComponent {
+
+    /**
+     * Builder for a ReferenceSysUIComponent.
+     */
+    @SysUISingleton
+    @Subcomponent.Builder
+    interface Builder extends SysUIComponent.Builder {
+        ReferenceSysUIComponent build();
+    }
+
+    /**
+     * Member injection into the supplied argument.
+     */
+    void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 48bef97..d0c5007 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -41,9 +41,11 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
 import com.android.systemui.screenshot.ReferenceScreenshotModule;
+import com.android.systemui.settings.dagger.MultiUserUtilsModule;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
@@ -93,6 +95,7 @@
         AospPolicyModule.class,
         GestureModule.class,
         MediaModule.class,
+        MultiUserUtilsModule.class,
         PowerModule.class,
         QSModule.class,
         ReferenceScreenshotModule.class,
@@ -162,7 +165,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -173,7 +177,8 @@
                 configurationController,
                 handler,
                 accessibilityManagerWrapper,
-                uiEventLogger
+                uiEventLogger,
+                shadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index d05bd51..6dc4f5c 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -28,6 +28,7 @@
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.statusbar.NotificationInsetsModule;
 import com.android.systemui.statusbar.QsFrameTranslateModule;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.FoldStateLogger;
@@ -40,7 +41,6 @@
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
-import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
@@ -58,12 +58,15 @@
 import dagger.Subcomponent;
 
 /**
- * Dagger Subcomponent for Core SysUI.
+ * An example Dagger Subcomponent for Core SysUI.
+ *
+ * See {@link ReferenceSysUIComponent} for the one actually used by AOSP.
  */
 @SysUISingleton
 @Subcomponent(modules = {
         DefaultComponentBinder.class,
         DependencyProvider.class,
+        NotificationInsetsModule.class,
         QsFrameTranslateModule.class,
         SystemUIBinder.class,
         SystemUIModule.class,
@@ -111,9 +114,6 @@
         Builder setBackAnimation(Optional<BackAnimation> b);
 
         @BindsInstance
-        Builder setFloatingTasks(Optional<FloatingTasks> f);
-
-        @BindsInstance
         Builder setDesktopMode(Optional<DesktopMode> d);
 
         SysUIComponent build();
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 721c0ba..09743ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.accessibility.SystemActions
 import com.android.systemui.accessibility.WindowMagnification
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.UdfpsOverlay
 import com.android.systemui.clipboardoverlay.ClipboardListener
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.globalactions.GlobalActionsComponent
@@ -218,6 +219,12 @@
     @ClassKey(KeyguardLiftController::class)
     abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
 
+    /** Inject into UdfpsOverlay.  */
+    @Binds
+    @IntoMap
+    @ClassKey(UdfpsOverlay::class)
+    abstract fun bindUdfpsOverlay(sysui: UdfpsOverlay): CoreStartable
+
     /** Inject into MediaTttSenderCoordinator. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d7638d6..bcf5e7a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -23,7 +23,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.clock.ClockModule;
+import com.android.keyguard.clock.ClockInfoModule;
+import com.android.keyguard.dagger.ClockRegistryModule;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
@@ -40,13 +41,16 @@
 import com.android.systemui.doze.dagger.DozeComponent;
 import com.android.systemui.dreams.dagger.DreamModule;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.FlagsModule;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.data.BouncerViewModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.motiontool.MotionToolModule;
 import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.notetask.NoteTaskModule;
 import com.android.systemui.people.PeopleModule;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 import com.android.systemui.privacy.PrivacyModule;
@@ -56,7 +60,6 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.screenshot.dagger.ScreenshotModule;
 import com.android.systemui.security.data.repository.SecurityRepositoryModule;
-import com.android.systemui.settings.dagger.MultiUserUtilsModule;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.smartspace.dagger.SmartspaceModule;
 import com.android.systemui.statusbar.CommandQueue;
@@ -82,6 +85,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;
@@ -120,7 +124,8 @@
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
-            ClockModule.class,
+            ClockInfoModule.class,
+            ClockRegistryModule.class,
             CoroutinesModule.class,
             DreamModule.class,
             ControlsModule.class,
@@ -130,13 +135,13 @@
             FooterActionsModule.class,
             LogModule.class,
             MediaProjectionModule.class,
+            MotionToolModule.class,
             PeopleHubModule.class,
             PeopleModule.class,
             PluginModule.class,
             PrivacyModule.class,
             ScreenshotModule.class,
             SensorModule.class,
-            MultiUserUtilsModule.class,
             SecurityRepositoryModule.class,
             SettingsUtilModule.class,
             SmartRepliesInflationModule.class,
@@ -147,9 +152,11 @@
             SysUIConcurrencyModule.class,
             SysUIUnfoldModule.class,
             TelephonyRepositoryModule.class,
+            TemporaryDisplayModule.class,
             TunerModule.class,
             UserModule.class,
             UtilModule.class,
+            NoteTaskModule.class,
             WalletModule.class
         },
         subcomponents = {
@@ -234,6 +241,7 @@
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
+            FeatureFlags featureFlags,
             @Main Executor sysuiMainExecutor) {
         return Optional.ofNullable(BubblesManager.create(context,
                 bubblesOptional,
@@ -250,6 +258,7 @@
                 notifCollection,
                 notifPipeline,
                 sysUiState,
+                featureFlags,
                 sysuiMainExecutor));
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index 096f969..d756f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -32,7 +32,6 @@
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
-import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
@@ -111,9 +110,6 @@
     @WMSingleton
     Optional<BackAnimation> getBackAnimation();
 
-    @WMSingleton
-    Optional<FloatingTasks> getFloatingTasks();
-
     /**
      * Optional {@link DesktopMode} component for interacting with desktop mode.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
index 991b54e..ded0fb7 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderImpl.kt
@@ -59,7 +59,7 @@
         (view as? DisplayCutoutView)?.let { cutoutView ->
             cutoutView.setColor(tintColor)
             cutoutView.updateRotation(rotation)
-            cutoutView.onDisplayChanged(displayUniqueId)
+            cutoutView.updateConfiguration(displayUniqueId)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index ec0013b..976afd4 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,8 +34,6 @@
 import com.android.systemui.biometrics.AuthController
 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.plugins.statusbar.StatusBarStateController
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -47,15 +45,13 @@
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Main private val mainExecutor: Executor,
-    private val featureFlags: FeatureFlags
 ) : DecorProviderFactory() {
     private val display = context.display
     private val displayInfo = DisplayInfo()
 
     override val hasProviders: Boolean
         get() {
-            if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) ||
-                    authController.faceSensorLocation == null) {
+            if (authController.faceSensorLocation == null) {
                 return false
             }
 
@@ -99,7 +95,7 @@
     }
 
     fun shouldShowFaceScanningAnim(): Boolean {
-        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning
+        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning
     }
 }
 
@@ -124,7 +120,7 @@
             view.layoutParams = it
             (view as? FaceScanningOverlay)?.let { overlay ->
                 overlay.setColor(tintColor)
-                overlay.onDisplayChanged(displayUniqueId)
+                overlay.updateConfiguration(displayUniqueId)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
index e18c0e1..8cfd391 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
@@ -33,7 +33,7 @@
  * of the privacy dot views are controlled by the PrivacyDotViewController.
  */
 @SysUISingleton
-class PrivacyDotDecorProviderFactory @Inject constructor(
+open class PrivacyDotDecorProviderFactory @Inject constructor(
     @Main private val res: Resources
 ) : DecorProviderFactory() {
 
diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
index a252864..8b4aeef 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerResDelegate.kt
@@ -78,23 +78,18 @@
         reloadMeasures()
     }
 
-    private fun reloadAll(newReloadToken: Int) {
-        if (reloadToken == newReloadToken) {
-            return
-        }
-        reloadToken = newReloadToken
-        reloadRes()
-        reloadMeasures()
-    }
-
     fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
         if (displayUniqueId != newDisplayUniqueId) {
             displayUniqueId = newDisplayUniqueId
             newReloadToken ?.let { reloadToken = it }
             reloadRes()
             reloadMeasures()
-        } else {
-            newReloadToken?.let { reloadAll(it) }
+        } else if (newReloadToken != null) {
+            if (reloadToken == newReloadToken) {
+                return
+            }
+            reloadToken = newReloadToken
+            reloadMeasures()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
index d537d4b..000bbe6 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
@@ -54,6 +54,9 @@
     private val receiverMap: Map<String, MutableList<DemoMode>>
 
     init {
+        // Don't persist demo mode across restarts.
+        requestFinishDemoMode()
+
         val m = mutableMapOf<String, MutableList<DemoMode>>()
         DemoMode.COMMANDS.map { command ->
             m.put(command, mutableListOf())
@@ -74,7 +77,6 @@
         // content changes to know if the setting turned on or off
         tracker.startTracking()
 
-        // TODO: We should probably exit demo mode if we booted up with it on
         isInDemoMode = tracker.isInDemoMode
 
         val demoFilter = IntentFilter()
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 2e51b51..0c14ed5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -133,9 +133,9 @@
     /**
      * Appends fling event to the logs
      */
-    public void traceFling(boolean expand, boolean aboveThreshold, boolean thresholdNeeded,
+    public void traceFling(boolean expand, boolean aboveThreshold,
             boolean screenOnFromTouch) {
-        mLogger.logFling(expand, aboveThreshold, thresholdNeeded, screenOnFromTouch);
+        mLogger.logFling(expand, aboveThreshold, screenOnFromTouch);
     }
 
     /**
@@ -287,8 +287,8 @@
     /**
      * Appends sensor event dropped event to logs
      */
-    public void traceSensorEventDropped(int sensorEvent, String reason) {
-        mLogger.logSensorEventDropped(sensorEvent, reason);
+    public void traceSensorEventDropped(@Reason int pulseReason, String reason) {
+        mLogger.logSensorEventDropped(pulseReason, reason);
     }
 
     /**
@@ -386,6 +386,47 @@
         mLogger.logSetAodDimmingScrim((long) scrimOpacity);
     }
 
+    /**
+     * Appends sensor attempted to register and whether it was a successful registration.
+     */
+    public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) {
+        mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration);
+    }
+
+    /**
+     * Appends sensor attempted to unregister and whether it was successfully unregistered.
+     */
+    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) {
+        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered);
+    }
+
+    /**
+     * Appends sensor attempted to unregister and whether it was successfully unregistered
+     * with a reason the sensor is being unregistered.
+     */
+    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered,
+            String reason) {
+        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason);
+    }
+
+    /**
+     * Appends the event of skipping a sensor registration since it's already registered.
+     */
+    public void traceSkipRegisterSensor(String sensorInfo) {
+        mLogger.logSkipSensorRegistration(sensorInfo);
+    }
+
+    /**
+     * Appends a plugin sensor was registered or unregistered event.
+     */
+    public void tracePluginSensorUpdate(boolean registered) {
+        if (registered) {
+            mLogger.log("register plugin sensor");
+        } else {
+            mLogger.log("unregister plugin sensor");
+        }
+    }
+
     private class SummaryStats {
         private int mCount;
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index cc57662..b5dbe21 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -19,12 +19,13 @@
 import android.view.Display
 import com.android.systemui.doze.DozeLog.Reason
 import com.android.systemui.doze.DozeLog.reasonToString
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
 import com.android.systemui.statusbar.policy.DevicePostureController
+import com.google.errorprone.annotations.CompileTimeConstant
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
@@ -95,13 +96,11 @@
     fun logFling(
         expand: Boolean,
         aboveThreshold: Boolean,
-        thresholdNeeded: Boolean,
         screenOnFromTouch: Boolean
     ) {
         buffer.log(TAG, DEBUG, {
             bool1 = expand
             bool2 = aboveThreshold
-            bool3 = thresholdNeeded
             bool4 = screenOnFromTouch
         }, {
             "Fling expand=$bool1 aboveThreshold=$bool2 thresholdNeeded=$bool3 " +
@@ -224,10 +223,14 @@
         })
     }
 
-    fun logPulseDropped(from: String, state: DozeMachine.State) {
+    /**
+     * Log why a pulse was dropped and the current doze machine state. The state can be null
+     * if the DozeMachine is the middle of transitioning between states.
+     */
+    fun logPulseDropped(from: String, state: DozeMachine.State?) {
         buffer.log(TAG, INFO, {
             str1 = from
-            str2 = state.name
+            str2 = state?.name
         }, {
             "Pulse dropped, cannot pulse from=$str1 state=$str2"
         })
@@ -320,6 +323,50 @@
             "Doze car mode started"
         })
     }
+
+    fun logSensorRegisterAttempt(sensorInfo: String, successfulRegistration: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulRegistration
+        }, {
+            "Register sensor. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSensorUnregisterAttempt(sensorInfo: String, successfulUnregister: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulUnregister
+        }, {
+            "Unregister sensor. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSensorUnregisterAttempt(
+            sensorInfo: String,
+            successfulUnregister: Boolean,
+            reason: String
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulUnregister
+            str2 = reason
+        }, {
+            "Unregister sensor. reason=$str2. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSkipSensorRegistration(sensor: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = sensor
+        }, {
+            "Skipping sensor registration because its already registered. sensor=$str1"
+        })
+    }
+
+    fun log(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, DEBUG, msg)
+    }
 }
 
 private const val TAG = "DozeLog"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index ae41215..96c35d4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -20,7 +20,6 @@
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
 import android.annotation.MainThread;
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Trace;
@@ -145,10 +144,9 @@
 
     private final Service mDozeService;
     private final WakeLock mWakeLock;
-    private final AmbientDisplayConfiguration mConfig;
+    private final AmbientDisplayConfiguration mAmbientDisplayConfig;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final DozeHost mDozeHost;
-    private final UiModeManager mUiModeManager;
     private final DockManager mDockManager;
     private final Part[] mParts;
 
@@ -156,18 +154,18 @@
     private State mState = State.UNINITIALIZED;
     private int mPulseReason;
     private boolean mWakeLockHeldForCurrentState = false;
+    private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL;
 
     @Inject
-    public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config,
+    public DozeMachine(@WrappedService Service service,
+            AmbientDisplayConfiguration ambientDisplayConfig,
             WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
-            UiModeManager uiModeManager,
             DozeLog dozeLog, DockManager dockManager,
             DozeHost dozeHost, Part[] parts) {
         mDozeService = service;
-        mConfig = config;
+        mAmbientDisplayConfig = ambientDisplayConfig;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mWakeLock = wakeLock;
-        mUiModeManager = uiModeManager;
         mDozeLog = dozeLog;
         mDockManager = dockManager;
         mDozeHost = dozeHost;
@@ -187,6 +185,18 @@
     }
 
     /**
+     * Notifies the {@link DozeMachine} that {@link Configuration} has changed.
+     */
+    public void onConfigurationChanged(Configuration newConfiguration) {
+        int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK;
+        if (mUiModeType == newUiModeType) return;
+        mUiModeType = newUiModeType;
+        for (Part part : mParts) {
+            part.onUiModeTypeChanged(mUiModeType);
+        }
+    }
+
+    /**
      * Requests transitioning to {@code requestedState}.
      *
      * This can be called during a state transition, in which case it will be queued until all
@@ -211,6 +221,14 @@
         requestState(State.DOZE_REQUEST_PULSE, pulseReason);
     }
 
+    /**
+     * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED}
+     *  or {@link State#FINISH}
+     */
+    public boolean isUninitializedOrFinished() {
+        return mState == State.UNINITIALIZED || mState == State.FINISH;
+    }
+
     void onScreenState(int state) {
         mDozeLog.traceDisplayState(state);
         for (Part part : mParts) {
@@ -360,7 +378,7 @@
         if (mState == State.FINISH) {
             return State.FINISH;
         }
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+        if (mUiModeType == Configuration.UI_MODE_TYPE_CAR
                 && (requestedState.canPulse() || requestedState.staysAwake())) {
             Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active");
             mDozeLog.traceCarModeStarted();
@@ -411,7 +429,7 @@
                     nextState = State.FINISH;
                 } else if (mDockManager.isDocked()) {
                     nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
-                } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+                } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
                     nextState = State.DOZE_AOD;
                 } else {
                     nextState = State.DOZE;
@@ -427,6 +445,7 @@
     /** Dumps the current state */
     public void dump(PrintWriter pw) {
         pw.print(" state="); pw.println(mState);
+        pw.print(" mUiModeType="); pw.println(mUiModeType);
         pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState);
         pw.print(" wakeLock="); pw.println(mWakeLock);
         pw.println("Parts:");
@@ -459,6 +478,19 @@
 
         /** Sets the {@link DozeMachine} when this Part is associated with one. */
         default void setDozeMachine(DozeMachine dozeMachine) {}
+
+        /**
+         * Notifies the Part about a change in {@link Configuration#uiMode}.
+         *
+         * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL},
+         *                   {@link Configuration#UI_MODE_TYPE_DESK},
+         *                   {@link Configuration#UI_MODE_TYPE_CAR},
+         *                   {@link Configuration#UI_MODE_TYPE_TELEVISION},
+         *                   {@link Configuration#UI_MODE_TYPE_APPLIANCE},
+         *                   {@link Configuration#UI_MODE_TYPE_WATCH},
+         *                   or {@link Configuration#UI_MODE_TYPE_VR_HEADSET}
+         */
+        default void onUiModeTypeChanged(int newUiModeType) {}
     }
 
     /** A wrapper interface for {@link android.service.dreams.DreamService} */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
index 60227ee..937884c 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java
@@ -171,7 +171,10 @@
 
     @Override
     public void onSensorChanged(SensorEvent event) {
-        Trace.beginSection("DozeScreenBrightness.onSensorChanged" + event.values[0]);
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP, "DozeScreenBrightness.onSensorChanged" + event.values[0]);
+        }
         try {
             if (mRegistered) {
                 mLastSensorValue = (int) event.values[0];
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 997a6e5..f64d918 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -24,8 +24,6 @@
 import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
 
 import android.annotation.AnyThread;
-import android.app.ActivityManager;
-import android.content.Context;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
@@ -40,7 +38,6 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.IndentingPrintWriter;
-import android.util.Log;
 import android.view.Display;
 
 import androidx.annotation.NonNull;
@@ -52,6 +49,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.plugins.SensorManagerPlugin;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -91,12 +89,9 @@
  * trigger callbacks on the provided {@link mProxCallback}.
  */
 public class DozeSensors {
-
-    private static final boolean DEBUG = DozeService.DEBUG;
     private static final String TAG = "DozeSensors";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
 
-    private final Context mContext;
     private final AsyncSensorManager mSensorManager;
     private final AmbientDisplayConfiguration mConfig;
     private final WakeLock mWakeLock;
@@ -104,6 +99,7 @@
     private final SecureSettings mSecureSettings;
     private final DevicePostureController mDevicePostureController;
     private final AuthController mAuthController;
+    private final UserTracker mUserTracker;
     private final boolean mScreenOffUdfpsEnabled;
 
     // Sensors
@@ -147,7 +143,6 @@
     }
 
     DozeSensors(
-            Context context,
             AsyncSensorManager sensorManager,
             DozeParameters dozeParameters,
             AmbientDisplayConfiguration config,
@@ -158,9 +153,9 @@
             ProximitySensor proximitySensor,
             SecureSettings secureSettings,
             AuthController authController,
-            DevicePostureController devicePostureController
+            DevicePostureController devicePostureController,
+            UserTracker userTracker
     ) {
-        mContext = context;
         mSensorManager = sensorManager;
         mConfig = config;
         mWakeLock = wakeLock;
@@ -177,6 +172,7 @@
         mDevicePostureController = devicePostureController;
         mDevicePosture = mDevicePostureController.getDevicePosture();
         mAuthController = authController;
+        mUserTracker = userTracker;
 
         mUdfpsEnrolled =
                 mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser());
@@ -448,7 +444,7 @@
     private final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
         @Override
         public void onChange(boolean selfChange, Collection<Uri> uris, int flags, int userId) {
-            if (userId != ActivityManager.getCurrentUser()) {
+            if (userId != mUserTracker.getUserId()) {
                 return;
             }
             for (TriggerSensor s : mTriggerSensors) {
@@ -608,10 +604,7 @@
             // cancel the previous sensor:
             if (mRegistered) {
                 final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor);
-                if (DEBUG) {
-                    Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] "
-                            + rt);
-                }
+                mDozeLog.traceSensorUnregisterAttempt(oldSensor.toString(), rt, "posture changed");
                 mRegistered = false;
             }
 
@@ -657,19 +650,13 @@
             if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) {
                 if (!mRegistered) {
                     mRegistered = mSensorManager.requestTriggerSensor(this, sensor);
-                    if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered);
-                    }
+                    mDozeLog.traceSensorRegisterAttempt(sensor.toString(), mRegistered);
                 } else {
-                    if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered");
-                    }
+                    mDozeLog.traceSkipRegisterSensor(sensor.toString());
                 }
             } else if (mRegistered) {
                 final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor);
-                if (DEBUG) {
-                    Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt);
-                }
+                mDozeLog.traceSensorUnregisterAttempt(sensor.toString(), rt);
                 mRegistered = false;
             }
         }
@@ -707,7 +694,6 @@
             final Sensor sensor = mSensors[mPosture];
             mDozeLog.traceSensor(mPulseReason);
             mHandler.post(mWakeLock.wrap(() -> {
-                if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event));
                 if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
                     UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP);
                 }
@@ -779,11 +765,11 @@
                     && !mRegistered) {
                 asyncSensorManager.registerPluginListener(mPluginSensor, this);
                 mRegistered = true;
-                if (DEBUG) Log.d(TAG, "registerPluginListener");
+                mDozeLog.tracePluginSensorUpdate(true /* registered */);
             } else if (mRegistered) {
                 asyncSensorManager.unregisterPluginListener(mPluginSensor, this);
                 mRegistered = false;
-                if (DEBUG) Log.d(TAG, "unregisterPluginListener");
+                mDozeLog.tracePluginSensorUpdate(false /* registered */);
             }
         }
 
@@ -816,10 +802,9 @@
             mHandler.post(mWakeLock.wrap(() -> {
                 final long now = SystemClock.uptimeMillis();
                 if (now < mDebounceFrom + mDebounce) {
-                    Log.d(TAG, "onSensorEvent dropped: " + triggerEventToString(event));
+                    mDozeLog.traceSensorEventDropped(mPulseReason, "debounce");
                     return;
                 }
-                if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event));
                 mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues());
             }));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index a2eb4e3..e8d7e46 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.service.dreams.DreamService;
@@ -59,6 +60,7 @@
         mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */);
         DozeComponent dozeComponent = mDozeComponentBuilder.build(this);
         mDozeMachine = dozeComponent.getDozeMachine();
+        mDozeMachine.onConfigurationChanged(getResources().getConfiguration());
     }
 
     @Override
@@ -127,6 +129,12 @@
     }
 
     @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDozeMachine.onConfigurationChanged(newConfig);
+    }
+
+    @Override
     public void onRequestHideDoze() {
         if (mDozeMachine != null) {
             mDozeMachine.requestState(DozeMachine.State.DOZE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index 7ed4b35..e6d9865 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -16,21 +16,13 @@
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.text.TextUtils;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
@@ -43,7 +35,9 @@
 /**
  * Handles suppressing doze on:
  * 1. INITIALIZED, don't allow dozing at all when:
- *      - in CAR_MODE
+ *      - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers
+ *      to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only
+ *      temporary and stops when the device exits CAR_MODE
  *      - device is NOT provisioned
  *      - there's a pending authentication
  * 2. PowerSaveMode active
@@ -57,35 +51,47 @@
  */
 @DozeScope
 public class DozeSuppressor implements DozeMachine.Part {
-    private static final String TAG = "DozeSuppressor";
 
     private DozeMachine mMachine;
     private final DozeHost mDozeHost;
     private final AmbientDisplayConfiguration mConfig;
     private final DozeLog mDozeLog;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final UiModeManager mUiModeManager;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
 
-    private boolean mBroadcastReceiverRegistered;
+    private boolean mIsCarModeEnabled = false;
 
     @Inject
     public DozeSuppressor(
             DozeHost dozeHost,
             AmbientDisplayConfiguration config,
             DozeLog dozeLog,
-            BroadcastDispatcher broadcastDispatcher,
-            UiModeManager uiModeManager,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
         mDozeHost = dozeHost;
         mConfig = config;
         mDozeLog = dozeLog;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mUiModeManager = uiModeManager;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
     }
 
     @Override
+    public void onUiModeTypeChanged(int newUiModeType) {
+        boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR;
+        if (mIsCarModeEnabled == isCarModeEnabled) {
+            return;
+        }
+        mIsCarModeEnabled = isCarModeEnabled;
+        // Do not handle the event if doze machine is not initialized yet.
+        // It will be handled upon initialization.
+        if (mMachine.isUninitializedOrFinished()) {
+            return;
+        }
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
+        } else {
+            handleCarModeExited();
+        }
+    }
+
+    @Override
     public void setDozeMachine(DozeMachine dozeMachine) {
         mMachine = dozeMachine;
     }
@@ -94,7 +100,6 @@
     public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
         switch (newState) {
             case INITIALIZED:
-                registerBroadcastReceiver();
                 mDozeHost.addCallback(mHostCallback);
                 checkShouldImmediatelyEndDoze();
                 checkShouldImmediatelySuspendDoze();
@@ -108,14 +113,12 @@
 
     @Override
     public void destroy() {
-        unregisterBroadcastReceiver();
         mDozeHost.removeCallback(mHostCallback);
     }
 
     private void checkShouldImmediatelySuspendDoze() {
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
-            mDozeLog.traceCarModeStarted();
-            mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
         }
     }
 
@@ -135,7 +138,7 @@
 
     @Override
     public void dump(PrintWriter pw) {
-        pw.println(" uiMode=" + mUiModeManager.getCurrentModeType());
+        pw.println(" isCarModeEnabled=" + mIsCarModeEnabled);
         pw.println(" hasPendingAuth="
                 + mBiometricUnlockControllerLazy.get().hasPendingAuthentication());
         pw.println(" isProvisioned=" + mDozeHost.isProvisioned());
@@ -143,40 +146,18 @@
         pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive());
     }
 
-    private void registerBroadcastReceiver() {
-        if (mBroadcastReceiverRegistered) {
-            return;
-        }
-        IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE);
-        filter.addAction(ACTION_EXIT_CAR_MODE);
-        mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
-        mBroadcastReceiverRegistered = true;
+    private void handleCarModeExited() {
+        mDozeLog.traceCarModeEnded();
+        mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+                ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
     }
 
-    private void unregisterBroadcastReceiver() {
-        if (!mBroadcastReceiverRegistered) {
-            return;
-        }
-        mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
-        mBroadcastReceiverRegistered = false;
+    private void handleCarModeStarted() {
+        mDozeLog.traceCarModeStarted();
+        mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
     }
 
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (ACTION_ENTER_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeStarted();
-                mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
-            } else if (ACTION_EXIT_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeEnded();
-                mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
-                        ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
-            }
-        }
-    };
-
-    private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
+    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
         @Override
         public void onPowerSaveChanged(boolean active) {
             // handles suppression changes, while DozeMachine#transitionPolicy handles gating
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index ef454ff..0b69b80 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -45,6 +45,7 @@
 import com.android.systemui.doze.DozeMachine.State;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -188,7 +189,8 @@
             UiEventLogger uiEventLogger,
             SessionTracker sessionTracker,
             KeyguardStateController keyguardStateController,
-            DevicePostureController devicePostureController) {
+            DevicePostureController devicePostureController,
+            UserTracker userTracker) {
         mContext = context;
         mDozeHost = dozeHost;
         mConfig = config;
@@ -198,9 +200,9 @@
         mAllowPulseTriggers = true;
         mSessionTracker = sessionTracker;
 
-        mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters,
+        mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
                 config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
-                secureSettings, authController, devicePostureController);
+                secureSettings, authController, devicePostureController, userTracker);
         mDockManager = dockManager;
         mProxCheck = proxCheck;
         mDozeLog = dozeLog;
@@ -536,13 +538,13 @@
             return;
         }
 
-        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) {
+        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) {
             if (!mAllowPulseTriggers) {
                 mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers");
             } else if (mDozeHost.isPulsePending()) {
                 mDozeLog.tracePulseDropped("requestPulse - pulsePending");
-            } else if (!canPulse()) {
-                mDozeLog.tracePulseDropped("requestPulse", dozeState);
+            } else if (!canPulse(dozeState)) {
+                mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState);
             }
             runIfNotNull(onPulseSuppressedListener);
             return;
@@ -559,15 +561,16 @@
                 // not in pocket, continue pulsing
                 final boolean isPulsePending = mDozeHost.isPulsePending();
                 mDozeHost.setPulsePending(false);
-                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) {
+                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) {
                     if (!isPulsePending) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer"
                                 + " pending, pulse was cancelled before it could start"
                                 + " transitioning to pulsing state.");
                     } else if (mDozeHost.isPulsingBlocked()) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked");
-                    } else if (!canPulse()) {
-                        mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState());
+                    } else if (!canPulse(dozeState)) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest"
+                                + " - doze state cannot pulse", dozeState);
                     }
                     runIfNotNull(onPulseSuppressedListener);
                     return;
@@ -582,10 +585,10 @@
                 .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId()));
     }
 
-    private boolean canPulse() {
-        return mMachine.getState() == DozeMachine.State.DOZE
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED;
+    private boolean canPulse(DozeMachine.State dozeState) {
+        return dozeState == DozeMachine.State.DOZE
+                || dozeState == DozeMachine.State.DOZE_AOD
+                || dozeState == DozeMachine.State.DOZE_AOD_DOCKED;
     }
 
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
new file mode 100644
index 0000000..0087c84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.animation.Interpolator
+import androidx.annotation.FloatRange
+import androidx.core.animation.doOnEnd
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.dreams.complication.ComplicationLayoutParams
+import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position
+import com.android.systemui.dreams.dagger.DreamOverlayModule
+import com.android.systemui.statusbar.BlurUtils
+import com.android.systemui.statusbar.CrossFadeHelper
+import javax.inject.Inject
+import javax.inject.Named
+
+/** Controller for dream overlay animations. */
+class DreamOverlayAnimationsController
+@Inject
+constructor(
+    private val mBlurUtils: BlurUtils,
+    private val mComplicationHostViewController: ComplicationHostViewController,
+    private val mStatusBarViewController: DreamOverlayStatusBarViewController,
+    private val mOverlayStateController: DreamOverlayStateController,
+    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
+    private val mDreamInBlurAnimDurationMs: Long,
+    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY)
+    private val mDreamInBlurAnimDelayMs: Long,
+    @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
+    private val mDreamInComplicationsAnimDurationMs: Long,
+    @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
+    private val mDreamInTopComplicationsAnimDelayMs: Long,
+    @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
+    private val mDreamInBottomComplicationsAnimDelayMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE)
+    private val mDreamOutTranslationYDistance: Int,
+    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION)
+    private val mDreamOutTranslationYDurationMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
+    private val mDreamOutTranslationYDelayBottomMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
+    private val mDreamOutTranslationYDelayTopMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DURATION) private val mDreamOutAlphaDurationMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_BOTTOM)
+    private val mDreamOutAlphaDelayBottomMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_TOP) private val mDreamOutAlphaDelayTopMs: Long,
+    @Named(DreamOverlayModule.DREAM_OUT_BLUR_DURATION) private val mDreamOutBlurDurationMs: Long
+) {
+
+    private var mAnimator: Animator? = null
+
+    /**
+     * Store the current alphas at the various positions. This is so that we may resume an animation
+     * at the current alpha.
+     */
+    private var mCurrentAlphaAtPosition = mutableMapOf<Int, Float>()
+
+    @FloatRange(from = 0.0, to = 1.0) private var mBlurProgress: Float = 0f
+
+    /** Starts the dream content and dream overlay entry animations. */
+    @JvmOverloads
+    fun startEntryAnimations(view: View, animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) {
+        cancelAnimations()
+
+        mAnimator =
+            animatorBuilder().apply {
+                playTogether(
+                    blurAnimator(
+                        view = view,
+                        from = 1f,
+                        to = 0f,
+                        durationMs = mDreamInBlurAnimDurationMs,
+                        delayMs = mDreamInBlurAnimDelayMs
+                    ),
+                    alphaAnimator(
+                        from = 0f,
+                        to = 1f,
+                        durationMs = mDreamInComplicationsAnimDurationMs,
+                        delayMs = mDreamInTopComplicationsAnimDelayMs,
+                        position = ComplicationLayoutParams.POSITION_TOP
+                    ),
+                    alphaAnimator(
+                        from = 0f,
+                        to = 1f,
+                        durationMs = mDreamInComplicationsAnimDurationMs,
+                        delayMs = mDreamInBottomComplicationsAnimDelayMs,
+                        position = ComplicationLayoutParams.POSITION_BOTTOM
+                    )
+                )
+                doOnEnd {
+                    mAnimator = null
+                    mOverlayStateController.setEntryAnimationsFinished(true)
+                }
+                start()
+            }
+    }
+
+    /** Starts the dream content and dream overlay exit animations. */
+    @JvmOverloads
+    fun startExitAnimations(
+        view: View,
+        doneCallback: () -> Unit,
+        animatorBuilder: () -> AnimatorSet = { AnimatorSet() }
+    ) {
+        cancelAnimations()
+
+        mAnimator =
+            animatorBuilder().apply {
+                playTogether(
+                    blurAnimator(
+                        view = view,
+                        // Start the blurring wherever the entry animation ended, in
+                        // case it was cancelled early.
+                        from = mBlurProgress,
+                        to = 1f,
+                        durationMs = mDreamOutBlurDurationMs
+                    ),
+                    translationYAnimator(
+                        from = 0f,
+                        to = mDreamOutTranslationYDistance.toFloat(),
+                        durationMs = mDreamOutTranslationYDurationMs,
+                        delayMs = mDreamOutTranslationYDelayBottomMs,
+                        position = ComplicationLayoutParams.POSITION_BOTTOM,
+                        animInterpolator = Interpolators.EMPHASIZED_ACCELERATE
+                    ),
+                    translationYAnimator(
+                        from = 0f,
+                        to = mDreamOutTranslationYDistance.toFloat(),
+                        durationMs = mDreamOutTranslationYDurationMs,
+                        delayMs = mDreamOutTranslationYDelayTopMs,
+                        position = ComplicationLayoutParams.POSITION_TOP,
+                        animInterpolator = Interpolators.EMPHASIZED_ACCELERATE
+                    ),
+                    alphaAnimator(
+                        from =
+                            mCurrentAlphaAtPosition.getOrDefault(
+                                key = ComplicationLayoutParams.POSITION_BOTTOM,
+                                defaultValue = 1f
+                            ),
+                        to = 0f,
+                        durationMs = mDreamOutAlphaDurationMs,
+                        delayMs = mDreamOutAlphaDelayBottomMs,
+                        position = ComplicationLayoutParams.POSITION_BOTTOM
+                    ),
+                    alphaAnimator(
+                        from =
+                            mCurrentAlphaAtPosition.getOrDefault(
+                                key = ComplicationLayoutParams.POSITION_TOP,
+                                defaultValue = 1f
+                            ),
+                        to = 0f,
+                        durationMs = mDreamOutAlphaDurationMs,
+                        delayMs = mDreamOutAlphaDelayTopMs,
+                        position = ComplicationLayoutParams.POSITION_TOP
+                    )
+                )
+                doOnEnd {
+                    mAnimator = null
+                    mOverlayStateController.setExitAnimationsRunning(false)
+                    doneCallback()
+                }
+                start()
+            }
+        mOverlayStateController.setExitAnimationsRunning(true)
+    }
+
+    /** Cancels the dream content and dream overlay animations, if they're currently running. */
+    fun cancelAnimations() {
+        mAnimator =
+            mAnimator?.let {
+                it.cancel()
+                null
+            }
+    }
+
+    private fun blurAnimator(
+        view: View,
+        from: Float,
+        to: Float,
+        durationMs: Long,
+        delayMs: Long = 0
+    ): Animator {
+        return ValueAnimator.ofFloat(from, to).apply {
+            duration = durationMs
+            startDelay = delayMs
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { animator: ValueAnimator ->
+                mBlurProgress = animator.animatedValue as Float
+                mBlurUtils.applyBlur(
+                    viewRootImpl = view.viewRootImpl,
+                    radius = mBlurUtils.blurRadiusOfRatio(mBlurProgress).toInt(),
+                    opaque = false
+                )
+            }
+        }
+    }
+
+    private fun alphaAnimator(
+        from: Float,
+        to: Float,
+        durationMs: Long,
+        delayMs: Long,
+        @Position position: Int
+    ): Animator {
+        return ValueAnimator.ofFloat(from, to).apply {
+            duration = durationMs
+            startDelay = delayMs
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { va: ValueAnimator ->
+                setElementsAlphaAtPosition(
+                    alpha = va.animatedValue as Float,
+                    position = position,
+                    fadingOut = to < from
+                )
+            }
+        }
+    }
+
+    private fun translationYAnimator(
+        from: Float,
+        to: Float,
+        durationMs: Long,
+        delayMs: Long,
+        @Position position: Int,
+        animInterpolator: Interpolator
+    ): Animator {
+        return ValueAnimator.ofFloat(from, to).apply {
+            duration = durationMs
+            startDelay = delayMs
+            interpolator = animInterpolator
+            addUpdateListener { va: ValueAnimator ->
+                setElementsTranslationYAtPosition(va.animatedValue as Float, position)
+            }
+        }
+    }
+
+    /** Sets alpha of complications at the specified position. */
+    private fun setElementsAlphaAtPosition(alpha: Float, position: Int, fadingOut: Boolean) {
+        mCurrentAlphaAtPosition[position] = alpha
+        mComplicationHostViewController.getViewsAtPosition(position).forEach { view ->
+            if (fadingOut) {
+                CrossFadeHelper.fadeOut(view, 1 - alpha, /* remap= */ false)
+            } else {
+                CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false)
+            }
+        }
+        if (position == ComplicationLayoutParams.POSITION_TOP) {
+            mStatusBarViewController.setFadeAmount(alpha, fadingOut)
+        }
+    }
+
+    /** Sets y translation of complications at the specified position. */
+    private fun setElementsTranslationYAtPosition(translationY: Float, position: Int) {
+        mComplicationHostViewController.getViewsAtPosition(position).forEach { v ->
+            v.translationY = translationY
+        }
+        if (position == ComplicationLayoutParams.POSITION_TOP) {
+            mStatusBarViewController.setTranslationY(translationY)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 733a80d..9d7ad30 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -29,19 +29,22 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
+
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayModule;
-import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.util.ViewController;
 
 import java.util.Arrays;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -54,6 +57,8 @@
     private final DreamOverlayStatusBarViewController mStatusBarViewController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final BlurUtils mBlurUtils;
+    private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
+    private final DreamOverlayStateController mStateController;
 
     private final ComplicationHostViewController mComplicationHostViewController;
 
@@ -74,14 +79,14 @@
     // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
     private final Handler mHandler;
     private final int mDreamOverlayMaxTranslationY;
-    private final BouncerCallbackInteractor mBouncerCallbackInteractor;
+    private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
 
     private long mJitterStartTimeMillis;
 
     private boolean mBouncerAnimating;
 
-    private final KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback =
-            new KeyguardBouncer.BouncerExpansionCallback() {
+    private final KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback =
+            new KeyguardBouncer.PrimaryBouncerExpansionCallback() {
 
                 @Override
                 public void onStartingToShow() {
@@ -134,12 +139,16 @@
             @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
                     burnInProtectionUpdateInterval,
             @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter,
-            BouncerCallbackInteractor bouncerCallbackInteractor) {
+            PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
+            DreamOverlayAnimationsController animationsController,
+            DreamOverlayStateController stateController) {
         super(containerView);
         mDreamOverlayContentView = contentView;
         mStatusBarViewController = statusBarViewController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mBlurUtils = blurUtils;
+        mDreamOverlayAnimationsController = animationsController;
+        mStateController = stateController;
 
         mComplicationHostViewController = complicationHostViewController;
         mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
@@ -154,7 +163,7 @@
         mMaxBurnInOffset = maxBurnInOffset;
         mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
         mMillisUntilFullJitter = millisUntilFullJitter;
-        mBouncerCallbackInteractor = bouncerCallbackInteractor;
+        mPrimaryBouncerCallbackInteractor = primaryBouncerCallbackInteractor;
     }
 
     @Override
@@ -167,21 +176,28 @@
     protected void onViewAttached() {
         mJitterStartTimeMillis = System.currentTimeMillis();
         mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
-        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getBouncer();
+        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
         if (bouncer != null) {
             bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback);
         }
-        mBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
+        mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
+
+        // Start dream entry animations. Skip animations for low light clock.
+        if (!mStateController.isLowLightActive()) {
+            mDreamOverlayAnimationsController.startEntryAnimations(mView);
+        }
     }
 
     @Override
     protected void onViewDetached() {
         mHandler.removeCallbacks(this::updateBurnInOffsets);
-        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getBouncer();
+        final KeyguardBouncer bouncer = mStatusBarKeyguardViewManager.getPrimaryBouncer();
         if (bouncer != null) {
             bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback);
         }
-        mBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
+        mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
+
+        mDreamOverlayAnimationsController.cancelAnimations();
     }
 
     View getContainerView() {
@@ -238,4 +254,17 @@
                         : aboutToShowBouncerProgress(expansion + 0.03f));
         return MathUtils.lerp(-mDreamOverlayMaxTranslationY, 0, fraction);
     }
+
+    /**
+     * Handle the dream waking up and run any necessary animations.
+     *
+     * @param onAnimationEnd Callback to trigger once animations are finished.
+     * @param callbackExecutor Executor to execute the callback on.
+     */
+    public void wakeUp(@NonNull Runnable onAnimationEnd, @NonNull Executor callbackExecutor) {
+        mDreamOverlayAnimationsController.startExitAnimations(mView, () -> {
+            callbackExecutor.execute(onAnimationEnd);
+            return null;
+        });
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index d1b7368..e76d5b3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -90,13 +90,15 @@
             new KeyguardUpdateMonitorCallback() {
                 @Override
                 public void onShadeExpandedChanged(boolean expanded) {
-                    if (mLifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED
-                            && mLifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) {
-                        return;
-                    }
+                    mExecutor.execute(() -> {
+                        if (getCurrentStateLocked() != Lifecycle.State.RESUMED
+                                && getCurrentStateLocked() != Lifecycle.State.STARTED) {
+                            return;
+                        }
 
-                    mLifecycleRegistry.setCurrentState(
-                            expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+                        setCurrentStateLocked(
+                                expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+                    });
                 }
             };
 
@@ -146,29 +148,30 @@
                 () -> mExecutor.execute(DreamOverlayService.this::requestExit);
         mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host);
         mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
-        setCurrentState(Lifecycle.State.CREATED);
-    }
 
-    private void setCurrentState(Lifecycle.State state) {
-        mExecutor.execute(() -> mLifecycleRegistry.setCurrentState(state));
+        mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
     }
 
     @Override
     public void onDestroy() {
         mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
-        setCurrentState(Lifecycle.State.DESTROYED);
 
-        resetCurrentDreamOverlay();
+        mExecutor.execute(() -> {
+            setCurrentStateLocked(Lifecycle.State.DESTROYED);
 
-        mDestroyed = true;
+            resetCurrentDreamOverlayLocked();
+
+            mDestroyed = true;
+        });
+
         super.onDestroy();
     }
 
     @Override
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
-        setCurrentState(Lifecycle.State.STARTED);
-
         mExecutor.execute(() -> {
+            setCurrentStateLocked(Lifecycle.State.STARTED);
+
             mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
 
             if (mDestroyed) {
@@ -181,7 +184,7 @@
                 // Reset the current dream overlay before starting a new one. This can happen
                 // when two dreams overlap (briefly, for a smoother dream transition) and both
                 // dreams are bound to the dream overlay service.
-                resetCurrentDreamOverlay();
+                resetCurrentDreamOverlayLocked();
             }
 
             mDreamOverlayContainerViewController =
@@ -191,7 +194,7 @@
 
             mStateController.setShouldShowComplications(shouldShowComplications());
             addOverlayWindowLocked(layoutParams);
-            setCurrentState(Lifecycle.State.RESUMED);
+            setCurrentStateLocked(Lifecycle.State.RESUMED);
             mStateController.setOverlayActive(true);
             final ComponentName dreamComponent = getDreamComponent();
             mStateController.setLowLightActive(
@@ -202,6 +205,23 @@
         });
     }
 
+    private Lifecycle.State getCurrentStateLocked() {
+        return mLifecycleRegistry.getCurrentState();
+    }
+
+    private void setCurrentStateLocked(Lifecycle.State state) {
+        mLifecycleRegistry.setCurrentState(state);
+    }
+
+    @Override
+    public void onWakeUp(@NonNull Runnable onCompletedCallback) {
+        mExecutor.execute(() -> {
+            if (mDreamOverlayContainerViewController != null) {
+                mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
+            }
+        });
+    }
+
     /**
      * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be
      * called from the main executing thread. The window attributes closely mirror those that are
@@ -231,13 +251,13 @@
         // Make extra sure the container view has been removed from its old parent (otherwise we
         // risk an IllegalStateException in some cases when setting the container view as the
         // window's content view and the container view hasn't been properly removed previously).
-        removeContainerViewFromParent();
+        removeContainerViewFromParentLocked();
         mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView());
 
         mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes());
     }
 
-    private void removeContainerViewFromParent() {
+    private void removeContainerViewFromParentLocked() {
         View containerView = mDreamOverlayContainerViewController.getContainerView();
         if (containerView == null) {
             return;
@@ -250,13 +270,14 @@
         parentView.removeView(containerView);
     }
 
-    private void resetCurrentDreamOverlay() {
+    private void resetCurrentDreamOverlayLocked() {
         if (mStarted && mWindow != null) {
             mWindowManager.removeView(mWindow.getDecorView());
         }
 
         mStateController.setOverlayActive(false);
         mStateController.setLowLightActive(false);
+        mStateController.setEntryAnimationsFinished(false);
 
         mDreamOverlayContainerViewController = null;
         mDreamOverlayTouchMonitor = null;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 72feaca..5f942b6 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -51,6 +51,8 @@
 
     public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0;
     public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
+    public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2;
+    public static final int STATE_DREAM_EXIT_ANIMATIONS_RUNNING = 1 << 3;
 
     private static final int OP_CLEAR_STATE = 1;
     private static final int OP_SET_STATE = 2;
@@ -202,6 +204,22 @@
         return containsState(STATE_LOW_LIGHT_ACTIVE);
     }
 
+    /**
+     * Returns whether the dream content and dream overlay entry animations are finished.
+     * @return {@code true} if animations are finished, {@code false} otherwise.
+     */
+    public boolean areEntryAnimationsFinished() {
+        return containsState(STATE_DREAM_ENTRY_ANIMATIONS_FINISHED);
+    }
+
+    /**
+     * Returns whether the dream content and dream overlay exit animations are running.
+     * @return {@code true} if animations are running, {@code false} otherwise.
+     */
+    public boolean areExitAnimationsRunning() {
+        return containsState(STATE_DREAM_EXIT_ANIMATIONS_RUNNING);
+    }
+
     private boolean containsState(int state) {
         return (mState & state) != 0;
     }
@@ -218,7 +236,7 @@
         }
 
         if (existingState != mState) {
-            notifyCallbacks(callback -> callback.onStateChanged());
+            notifyCallbacks(Callback::onStateChanged);
         }
     }
 
@@ -239,6 +257,24 @@
     }
 
     /**
+     * Sets whether dream content and dream overlay entry animations are finished.
+     * @param finished {@code true} if entry animations are finished, {@code false} otherwise.
+     */
+    public void setEntryAnimationsFinished(boolean finished) {
+        modifyState(finished ? OP_SET_STATE : OP_CLEAR_STATE,
+                STATE_DREAM_ENTRY_ANIMATIONS_FINISHED);
+    }
+
+    /**
+     * Sets whether dream content and dream overlay exit animations are running.
+     * @param running {@code true} if exit animations are running, {@code false} otherwise.
+     */
+    public void setExitAnimationsRunning(boolean running) {
+        modifyState(running ? OP_SET_STATE : OP_CLEAR_STATE,
+                STATE_DREAM_EXIT_ANIMATIONS_RUNNING);
+    }
+
+    /**
      * Returns the available complication types.
      */
     @Complication.ComplicationType
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index bb1c430..f1bb156 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -16,10 +16,6 @@
 
 package com.android.systemui.dreams;
 
-import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
-import static android.app.StatusBarManager.WINDOW_STATE_HIDING;
-import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-
 import android.app.AlarmManager;
 import android.app.StatusBarManager;
 import android.content.res.Resources;
@@ -41,6 +37,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -83,6 +80,9 @@
 
     private boolean mIsAttached;
 
+    // Whether dream entry animations are finished.
+    private boolean mEntryAnimationsFinished = false;
+
     private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
             .clearCapabilities()
             .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
@@ -109,7 +109,9 @@
             new DreamOverlayStateController.Callback() {
                 @Override
                 public void onStateChanged() {
-                    updateLowLightState();
+                    mEntryAnimationsFinished =
+                            mDreamOverlayStateController.areEntryAnimationsFinished();
+                    updateVisibility();
                 }
             };
 
@@ -195,7 +197,6 @@
         mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback);
 
         mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
-        updateLowLightState();
 
         mTouchInsetSession.addViewToTracking(mView);
     }
@@ -216,6 +217,37 @@
         mIsAttached = false;
     }
 
+    /**
+     * Sets fade of the dream overlay status bar.
+     *
+     * No-op if the dream overlay status bar should not be shown.
+     */
+    protected void setFadeAmount(float fadeAmount, boolean fadingOut) {
+        updateVisibility();
+
+        if (mView.getVisibility() != View.VISIBLE) {
+            return;
+        }
+
+        if (fadingOut) {
+            CrossFadeHelper.fadeOut(mView, 1 - fadeAmount, /* remap= */ false);
+        } else {
+            CrossFadeHelper.fadeIn(mView, fadeAmount, /* remap= */ false);
+        }
+    }
+
+    /**
+     * Sets the y translation of the dream overlay status bar.
+     */
+    public void setTranslationY(float translationY) {
+        mView.setTranslationY(translationY);
+    }
+
+    private boolean shouldShowStatusBar() {
+        return !mDreamOverlayStateController.isLowLightActive()
+                && !mStatusBarWindowStateController.windowIsShowing();
+    }
+
     private void updateWifiUnavailableStatusIcon() {
         final NetworkCapabilities capabilities =
                 mConnectivityManager.getNetworkCapabilities(
@@ -235,13 +267,12 @@
                 hasAlarm ? buildAlarmContentDescription(alarm) : null);
     }
 
-    private void updateLowLightState() {
-        int visibility = View.VISIBLE;
-        if (mDreamOverlayStateController.isLowLightActive()
-                || mStatusBarWindowStateController.windowIsShowing()) {
-            visibility = View.INVISIBLE;
+    private void updateVisibility() {
+        if (shouldShowStatusBar()) {
+            mView.setVisibility(View.VISIBLE);
+        } else {
+            mView.setVisibility(View.INVISIBLE);
         }
-        mView.setVisibility(visibility);
     }
 
     private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {
@@ -298,21 +329,11 @@
     }
 
     private void onSystemStatusBarStateChanged(@StatusBarManager.WindowVisibleState int state) {
-        mMainExecutor.execute(() -> {
-            if (!mIsAttached || mDreamOverlayStateController.isLowLightActive()) {
-                return;
-            }
+        if (!mIsAttached || !mEntryAnimationsFinished) {
+            return;
+        }
 
-            switch (state) {
-                case WINDOW_STATE_SHOWING:
-                    mView.setVisibility(View.INVISIBLE);
-                    break;
-                case WINDOW_STATE_HIDING:
-                case WINDOW_STATE_HIDDEN:
-                    mView.setVisibility(View.VISIBLE);
-                    break;
-            }
-        });
+        mMainExecutor.execute(this::updateVisibility);
     }
 
     private void onStatusBarItemsChanged(List<StatusBarItem> newItems) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
index 29bb2f4..b07efdf 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
@@ -164,7 +164,8 @@
             COMPLICATION_TYPE_AIR_QUALITY,
             COMPLICATION_TYPE_CAST_INFO,
             COMPLICATION_TYPE_HOME_CONTROLS,
-            COMPLICATION_TYPE_SMARTSPACE
+            COMPLICATION_TYPE_SMARTSPACE,
+            COMPLICATION_TYPE_MEDIA_ENTRY
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ComplicationType {}
@@ -177,6 +178,7 @@
     int COMPLICATION_TYPE_CAST_INFO = 1 << 4;
     int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5;
     int COMPLICATION_TYPE_SMARTSPACE = 1 << 6;
+    int COMPLICATION_TYPE_MEDIA_ENTRY = 1 << 7;
 
     /**
      * The {@link Host} interface specifies a way a {@link Complication} to communicate with its
@@ -195,11 +197,11 @@
      */
     interface VisibilityController {
         /**
-         * Called to set the visibility of all shown and future complications.
+         * Called to set the visibility of all shown and future complications. Changes in visibility
+         * will always be animated.
          * @param visibility The desired future visibility.
-         * @param animate whether the change should be animated.
          */
-        void setVisibility(@View.Visibility int visibility, boolean animate);
+        void setVisibility(@View.Visibility int visibility);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
index fd6cfc0..100ccc3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -28,6 +28,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.lifecycle.LifecycleOwner;
 
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.util.ViewController;
 
 import java.util.Collection;
@@ -49,20 +50,34 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final ComplicationLayoutEngine mLayoutEngine;
+    private final DreamOverlayStateController mDreamOverlayStateController;
     private final LifecycleOwner mLifecycleOwner;
     private final ComplicationCollectionViewModel mComplicationCollectionViewModel;
     private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>();
 
+    // Whether dream entry animations are finished.
+    private boolean mEntryAnimationsFinished = false;
+
     @Inject
     protected ComplicationHostViewController(
             @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view,
             ComplicationLayoutEngine layoutEngine,
+            DreamOverlayStateController dreamOverlayStateController,
             LifecycleOwner lifecycleOwner,
             @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) {
         super(view);
         mLayoutEngine = layoutEngine;
         mLifecycleOwner = lifecycleOwner;
         mComplicationCollectionViewModel = viewModel;
+        mDreamOverlayStateController = dreamOverlayStateController;
+
+        mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() {
+            @Override
+            public void onStateChanged() {
+                mEntryAnimationsFinished =
+                        mDreamOverlayStateController.areEntryAnimationsFinished();
+            }
+        });
     }
 
     @Override
@@ -123,6 +138,11 @@
                     final ComplicationId id = complication.getId();
                     final Complication.ViewHolder viewHolder = complication.getComplication()
                             .createView(complication);
+                    // Complications to be added before dream entry animations are finished are set
+                    // to invisible and are animated in.
+                    if (!mEntryAnimationsFinished) {
+                        viewHolder.getView().setVisibility(View.INVISIBLE);
+                    }
                     mComplications.put(id, viewHolder);
                     if (viewHolder.getView().getParent() != null) {
                         Log.e(TAG, "View for complication "
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
index 5694f6d..48159ae 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -21,12 +21,9 @@
 import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATION_MARGIN_DEFAULT;
 import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.SCOPED_COMPLICATIONS_LAYOUT;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.constraintlayout.widget.Constraints;
@@ -34,6 +31,7 @@
 import com.android.systemui.R;
 import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.touch.TouchInsetManager;
 
 import java.util.ArrayList;
@@ -194,7 +192,9 @@
                         break;
                 }
 
-                if (!isRoot) {
+                // Add margin if specified by the complication. Otherwise add default margin
+                // between complications.
+                if (mLayoutParams.isMarginSpecified() || !isRoot) {
                     final int margin = mLayoutParams.getMargin(mDefaultMargin);
                     switch(direction) {
                         case ComplicationLayoutParams.DIRECTION_DOWN:
@@ -479,7 +479,6 @@
     private final TouchInsetManager.TouchInsetSession mSession;
     private final int mFadeInDuration;
     private final int mFadeOutDuration;
-    private ViewPropertyAnimator mViewPropertyAnimator;
 
     /** */
     @Inject
@@ -496,26 +495,16 @@
     }
 
     @Override
-    public void setVisibility(int visibility, boolean animate) {
-        final boolean appearing = visibility == View.VISIBLE;
-
-        if (mViewPropertyAnimator != null) {
-            mViewPropertyAnimator.cancel();
+    public void setVisibility(int visibility) {
+        if (visibility == View.VISIBLE) {
+            CrossFadeHelper.fadeIn(mLayout, mFadeInDuration, /* delay= */ 0);
+        } else {
+            CrossFadeHelper.fadeOut(
+                    mLayout,
+                    mFadeOutDuration,
+                    /* delay= */ 0,
+                    /* endRunnable= */ null);
         }
-
-        if (appearing) {
-            mLayout.setVisibility(View.VISIBLE);
-        }
-
-        mViewPropertyAnimator = mLayout.animate()
-                .alpha(appearing ? 1f : 0f)
-                .setDuration(appearing ? mFadeInDuration : mFadeOutDuration)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mLayout.setVisibility(visibility);
-                    }
-                });
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
index a21eb19..4fae68d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java
@@ -38,7 +38,7 @@
             POSITION_START,
     })
 
-    @interface Position {}
+    public @interface Position {}
     /** Align view with the top of parent or bottom of preceding {@link Complication}. */
     public static final int POSITION_TOP = 1 << 0;
     /** Align view with the bottom of parent or top of preceding {@link Complication}. */
@@ -261,6 +261,13 @@
     }
 
     /**
+     * Returns whether margin has been specified by the complication.
+     */
+    public boolean isMarginSpecified() {
+        return mMargin != MARGIN_UNSPECIFIED;
+    }
+
+    /**
      * Returns the margin to apply between complications, or the given default if no margin is
      * specified.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
index 75a97de..18aacd2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
@@ -54,6 +55,8 @@
                 return COMPLICATION_TYPE_HOME_CONTROLS;
             case DreamBackend.COMPLICATION_TYPE_SMARTSPACE:
                 return COMPLICATION_TYPE_SMARTSPACE;
+            case DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY:
+                return COMPLICATION_TYPE_MEDIA_ENTRY;
             default:
                 return COMPLICATION_TYPE_NONE;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 0ccb222..c01cf43 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -33,6 +33,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.controls.ControlsServiceInfo;
 import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsActivity;
@@ -42,6 +43,8 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.util.ViewController;
 
+import java.util.List;
+
 import javax.inject.Inject;
 import javax.inject.Named;
 
@@ -76,16 +79,25 @@
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final ControlsComponent mControlsComponent;
 
-        private boolean mControlServicesAvailable = false;
+        private boolean mOverlayActive = false;
 
         // Callback for when the home controls service availability changes.
         private final ControlsListingController.ControlsListingCallback mControlsCallback =
-                serviceInfos -> {
-                    boolean available = !serviceInfos.isEmpty();
+                services -> updateHomeControlsComplication();
 
-                    if (available != mControlServicesAvailable) {
-                        mControlServicesAvailable = available;
-                        updateComplicationAvailability();
+        private final DreamOverlayStateController.Callback mOverlayStateCallback =
+                new DreamOverlayStateController.Callback() {
+                    @Override
+                    public void onStateChanged() {
+                        if (mOverlayActive == mDreamOverlayStateController.isOverlayActive()) {
+                            return;
+                        }
+
+                        mOverlayActive = !mOverlayActive;
+
+                        if (mOverlayActive) {
+                            updateHomeControlsComplication();
+                        }
                     }
                 };
 
@@ -102,18 +114,29 @@
         public void start() {
             mControlsComponent.getControlsListingController().ifPresent(
                     c -> c.addCallback(mControlsCallback));
+            mDreamOverlayStateController.addCallback(mOverlayStateCallback);
         }
 
-        private void updateComplicationAvailability() {
+        private void updateHomeControlsComplication() {
+            mControlsComponent.getControlsListingController().ifPresent(c -> {
+                if (isHomeControlsAvailable(c.getCurrentServices())) {
+                    mDreamOverlayStateController.addComplication(mComplication);
+                } else {
+                    mDreamOverlayStateController.removeComplication(mComplication);
+                }
+            });
+        }
+
+        private boolean isHomeControlsAvailable(List<ControlsServiceInfo> controlsServices) {
+            if (controlsServices.isEmpty()) {
+                return false;
+            }
+
             final boolean hasFavorites = mControlsComponent.getControlsController()
                     .map(c -> !c.getFavorites().isEmpty())
                     .orElse(false);
-            if (!hasFavorites || !mControlServicesAvailable
-                    || mControlsComponent.getVisibility() == UNAVAILABLE) {
-                mDreamOverlayStateController.removeComplication(mComplication);
-            } else {
-                mDreamOverlayStateController.addComplication(mComplication);
-            }
+            final ControlsComponent.Visibility visibility = mControlsComponent.getVisibility();
+            return hasFavorites && visibility != UNAVAILABLE;
         }
     }
 
@@ -210,7 +233,8 @@
 
             final Intent intent = new Intent(mContext, ControlsActivity.class)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true);
+                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+                    .putExtra(ControlsUiController.EXIT_TO_DREAM, true);
 
             final ActivityLaunchAnimator.Controller controller =
                     v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index c07d402..deff060 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -28,7 +28,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaCarouselController;
+import com.android.systemui.media.controls.ui.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -55,6 +55,11 @@
         return mComponentFactory.create().getViewHolder();
     }
 
+    @Override
+    public int getRequiredTypeAvailability() {
+        return COMPLICATION_TYPE_MEDIA_ENTRY;
+    }
+
     /**
      * Contains values/logic associated with the dream complication view.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
index c9fecc9..09cc7c5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
@@ -41,6 +41,7 @@
     public static final String COMPLICATIONS_FADE_OUT_DURATION = "complications_fade_out_duration";
     public static final String COMPLICATIONS_FADE_IN_DURATION = "complications_fade_in_duration";
     public static final String COMPLICATIONS_RESTORE_TIMEOUT = "complication_restore_timeout";
+    public static final String COMPLICATIONS_FADE_OUT_DELAY = "complication_fade_out_delay";
 
     /**
      * Generates a {@link ConstraintLayout}, which can host
@@ -75,6 +76,16 @@
     }
 
     /**
+     * Provides the delay to wait for before fading out complications.
+     */
+    @Provides
+    @Named(COMPLICATIONS_FADE_OUT_DELAY)
+    @DreamOverlayComponent.DreamOverlayScope
+    static int providesComplicationsFadeOutDelay(@Main Resources resources) {
+        return resources.getInteger(R.integer.complicationFadeOutDelayMs);
+    }
+
+    /**
      * Provides the fade in duration for complications.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
index 7d2ce51..69b85b5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
@@ -48,9 +48,9 @@
 
     int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1;
     int DREAM_SMARTSPACE_COMPLICATION_WEIGHT = 0;
-    int DREAM_MEDIA_COMPLICATION_WEIGHT = -1;
-    int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 1;
-    int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 0;
+    int DREAM_MEDIA_COMPLICATION_WEIGHT = 0;
+    int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 2;
+    int DREAM_MEDIA_ENTRY_COMPLICATION_WEIGHT = 1;
 
     /**
      * Provides layout parameters for the clock time complication.
@@ -60,10 +60,11 @@
     static ComplicationLayoutParams provideClockTimeLayoutParams() {
         return new ComplicationLayoutParams(0,
                 ViewGroup.LayoutParams.WRAP_CONTENT,
-                ComplicationLayoutParams.POSITION_TOP
+                ComplicationLayoutParams.POSITION_BOTTOM
                         | ComplicationLayoutParams.POSITION_START,
-                ComplicationLayoutParams.DIRECTION_DOWN,
-                DREAM_CLOCK_TIME_COMPLICATION_WEIGHT);
+                ComplicationLayoutParams.DIRECTION_UP,
+                DREAM_CLOCK_TIME_COMPLICATION_WEIGHT,
+                0 /*margin*/);
     }
 
     /**
@@ -77,8 +78,10 @@
                 res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
                 ComplicationLayoutParams.POSITION_BOTTOM
                         | ComplicationLayoutParams.POSITION_START,
-                ComplicationLayoutParams.DIRECTION_END,
-                DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT);
+                ComplicationLayoutParams.DIRECTION_UP,
+                DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT,
+                // Add margin to the bottom of home controls to horizontally align with smartspace.
+                res.getDimensionPixelSize(R.dimen.dream_overlay_complication_clock_time_padding));
     }
 
     /**
@@ -101,14 +104,13 @@
      */
     @Provides
     @Named(DREAM_SMARTSPACE_LAYOUT_PARAMS)
-    static ComplicationLayoutParams provideSmartspaceLayoutParams() {
+    static ComplicationLayoutParams provideSmartspaceLayoutParams(@Main Resources res) {
         return new ComplicationLayoutParams(0,
                 ViewGroup.LayoutParams.WRAP_CONTENT,
-                ComplicationLayoutParams.POSITION_TOP
+                ComplicationLayoutParams.POSITION_BOTTOM
                         | ComplicationLayoutParams.POSITION_START,
-                ComplicationLayoutParams.DIRECTION_DOWN,
+                ComplicationLayoutParams.DIRECTION_END,
                 DREAM_SMARTSPACE_COMPLICATION_WEIGHT,
-                0,
-                true /*snapToGuide*/);
+                res.getDimensionPixelSize(R.dimen.dream_overlay_complication_smartspace_padding));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index f9dca08..101f4a4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -44,7 +44,7 @@
             DreamOverlayComponent.class,
         })
 public interface DreamModule {
-    String DREAM_ONLY_ENABLED_FOR_SYSTEM_USER = "dream_only_enabled_for_system_user";
+    String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
 
     String DREAM_SUPPORTED = "dream_supported";
 
@@ -70,10 +70,10 @@
 
     /** */
     @Provides
-    @Named(DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
-    static boolean providesDreamOnlyEnabledForSystemUser(@Main Resources resources) {
+    @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+    static boolean providesDreamOnlyEnabledForDockUser(@Main Resources resources) {
         return resources.getBoolean(
-                com.android.internal.R.bool.config_dreamsOnlyEnabledForSystemUser);
+                com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 4fe1622..ed0e1d9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -47,6 +47,30 @@
     public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL =
             "burn_in_protection_update_interval";
     public static final String MILLIS_UNTIL_FULL_JITTER = "millis_until_full_jitter";
+    public static final String DREAM_IN_BLUR_ANIMATION_DURATION = "dream_in_blur_anim_duration";
+    public static final String DREAM_IN_BLUR_ANIMATION_DELAY = "dream_in_blur_anim_delay";
+    public static final String DREAM_IN_COMPLICATIONS_ANIMATION_DURATION =
+            "dream_in_complications_anim_duration";
+    public static final String DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY =
+            "dream_in_top_complications_anim_delay";
+    public static final String DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY =
+            "dream_in_bottom_complications_anim_delay";
+    public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE =
+            "dream_out_complications_translation_y";
+    public static final String DREAM_OUT_TRANSLATION_Y_DURATION =
+            "dream_out_complications_translation_y_duration";
+    public static final String DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM =
+            "dream_out_complications_translation_y_delay_bottom";
+    public static final String DREAM_OUT_TRANSLATION_Y_DELAY_TOP =
+            "dream_out_complications_translation_y_delay_top";
+    public static final String DREAM_OUT_ALPHA_DURATION =
+            "dream_out_complications_alpha_duration";
+    public static final String DREAM_OUT_ALPHA_DELAY_BOTTOM =
+            "dream_out_complications_alpha_delay_bottom";
+    public static final String DREAM_OUT_ALPHA_DELAY_TOP =
+            "dream_out_complications_alpha_delay_top";
+    public static final String DREAM_OUT_BLUR_DURATION =
+            "dream_out_blur_duration";
 
     /** */
     @Provides
@@ -114,6 +138,112 @@
         return resources.getInteger(R.integer.config_dreamOverlayMillisUntilFullJitter);
     }
 
+    /**
+     * Duration in milliseconds of the dream in un-blur animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BLUR_ANIMATION_DURATION)
+    static long providesDreamInBlurAnimationDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDurationMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in un-blur animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BLUR_ANIMATION_DELAY)
+    static long providesDreamInBlurAnimationDelay(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDelayMs);
+    }
+
+    /**
+     * Duration in milliseconds of the dream in complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
+    static long providesDreamInComplicationsAnimationDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayInComplicationsDurationMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in top complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
+    static long providesDreamInTopComplicationsAnimationDelay(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayInTopComplicationsDelayMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in bottom complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
+    static long providesDreamInBottomComplicationsAnimationDelay(@Main Resources resources) {
+        return (long) resources.getInteger(
+                R.integer.config_dreamOverlayInBottomComplicationsDelayMs);
+    }
+
+    /**
+     * Provides the number of pixels to translate complications when waking up from dream.
+     */
+    @Provides
+    @Named(DREAM_OUT_TRANSLATION_Y_DISTANCE)
+    @DreamOverlayComponent.DreamOverlayScope
+    static int providesDreamOutComplicationsTranslationY(@Main Resources resources) {
+        return resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_TRANSLATION_Y_DURATION)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsTranslationYDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDurationMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsTranslationYDelayBottom(@Main Resources resources) {
+        return (long) resources.getInteger(
+                R.integer.config_dreamOverlayOutTranslationYDelayBottomMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsTranslationYDelayTop(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDelayTopMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_ALPHA_DURATION)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsAlphaDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDurationMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_ALPHA_DELAY_BOTTOM)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsAlphaDelayBottom(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayBottomMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_ALPHA_DELAY_TOP)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutComplicationsAlphaDelayTop(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayTopMs);
+    }
+
+    @Provides
+    @Named(DREAM_OUT_BLUR_DURATION)
+    @DreamOverlayComponent.DreamOverlayScope
+    static long providesDreamOutBlurDuration(@Main Resources resources) {
+        return (long) resources.getInteger(R.integer.config_dreamOverlayOutBlurDurationMs);
+    }
+
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
     static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
index 0dba4ff..92cdcf9 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/BouncerSwipeTouchHandler.java
@@ -116,7 +116,7 @@
 
                         if (mCapture) {
                             // Since the user is dragging the bouncer up, set scrimmed to false.
-                            mStatusBarKeyguardViewManager.showBouncer(false);
+                            mStatusBarKeyguardViewManager.showPrimaryBouncer(false);
                         }
                     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
index 3087cdf..e276e0c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
@@ -16,22 +16,26 @@
 
 package com.android.systemui.dreams.touch;
 
+import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DELAY;
 import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_RESTORE_TIMEOUT;
 
-import android.os.Handler;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.Complication;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.touch.TouchInsetManager;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.ArrayDeque;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -49,33 +53,58 @@
     private static final String TAG = "HideComplicationHandler";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private final Complication.VisibilityController mVisibilityController;
     private final int mRestoreTimeout;
+    private final int mFadeOutDelay;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    private final Handler mHandler;
-    private final Executor mExecutor;
+    private final DelayableExecutor mExecutor;
+    private final DreamOverlayStateController mOverlayStateController;
     private final TouchInsetManager mTouchInsetManager;
+    private final Complication.VisibilityController mVisibilityController;
+    private boolean mHidden = false;
+    @Nullable
+    private Runnable mHiddenCallback;
+    private final ArrayDeque<Runnable> mCancelCallbacks = new ArrayDeque<>();
+
 
     private final Runnable mRestoreComplications = new Runnable() {
         @Override
         public void run() {
-            mVisibilityController.setVisibility(View.VISIBLE, true);
+            mVisibilityController.setVisibility(View.VISIBLE);
+            mHidden = false;
+        }
+    };
+
+    private final Runnable mHideComplications = new Runnable() {
+        @Override
+        public void run() {
+            if (mOverlayStateController.areExitAnimationsRunning()) {
+                // Avoid interfering with the exit animations.
+                return;
+            }
+            mVisibilityController.setVisibility(View.INVISIBLE);
+            mHidden = true;
+            if (mHiddenCallback != null) {
+                mHiddenCallback.run();
+                mHiddenCallback = null;
+            }
         }
     };
 
     @Inject
     HideComplicationTouchHandler(Complication.VisibilityController visibilityController,
             @Named(COMPLICATIONS_RESTORE_TIMEOUT) int restoreTimeout,
+            @Named(COMPLICATIONS_FADE_OUT_DELAY) int fadeOutDelay,
             TouchInsetManager touchInsetManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            @Main Executor executor,
-            @Main Handler handler) {
+            @Main DelayableExecutor executor,
+            DreamOverlayStateController overlayStateController) {
         mVisibilityController = visibilityController;
         mRestoreTimeout = restoreTimeout;
+        mFadeOutDelay = fadeOutDelay;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-        mHandler = handler;
         mTouchInsetManager = touchInsetManager;
         mExecutor = executor;
+        mOverlayStateController = overlayStateController;
     }
 
     @Override
@@ -87,7 +116,8 @@
         final boolean bouncerShowing = mStatusBarKeyguardViewManager.isBouncerShowing();
 
         // If other sessions are interested in this touch, do not fade out elements.
-        if (session.getActiveSessionCount() > 1 || bouncerShowing) {
+        if (session.getActiveSessionCount() > 1 || bouncerShowing
+                || mOverlayStateController.areExitAnimationsRunning()) {
             if (DEBUG) {
                 Log.d(TAG, "not fading. Active session count: " + session.getActiveSessionCount()
                         + ". Bouncer showing: " + bouncerShowing);
@@ -115,8 +145,11 @@
                 touchCheck.addListener(() -> {
                     try {
                         if (!touchCheck.get()) {
-                            mHandler.removeCallbacks(mRestoreComplications);
-                            mVisibilityController.setVisibility(View.INVISIBLE, true);
+                            // Cancel all pending callbacks.
+                            while (!mCancelCallbacks.isEmpty()) mCancelCallbacks.pop().run();
+                            mCancelCallbacks.add(
+                                    mExecutor.executeDelayed(
+                                            mHideComplications, mFadeOutDelay));
                         } else {
                             // If a touch occurred inside the dream overlay touch insets, do not
                             // handle the touch.
@@ -130,7 +163,23 @@
                     || motionEvent.getAction() == MotionEvent.ACTION_UP) {
                 // End session and initiate delayed reappearance of the complications.
                 session.pop();
-                mHandler.postDelayed(mRestoreComplications, mRestoreTimeout);
+                runAfterHidden(() -> mCancelCallbacks.add(
+                        mExecutor.executeDelayed(mRestoreComplications,
+                                mRestoreTimeout)));
+            }
+        });
+    }
+
+    /**
+     * Triggers a runnable after complications have been hidden. Will override any previously set
+     * runnable currently waiting for hide to happen.
+     */
+    private void runAfterHidden(Runnable runnable) {
+        mExecutor.execute(() -> {
+            if (mHidden) {
+                runnable.run();
+            } else {
+                mHiddenCallback = runnable;
             }
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 08ef8f3..609bd76 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -24,8 +24,13 @@
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.google.protobuf.nano.MessageNano
+import java.io.BufferedOutputStream
+import java.io.FileDescriptor
+import java.io.FileOutputStream
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
@@ -100,7 +105,7 @@
     /**
      * Dump the diagnostics! Behavior can be controlled via [args].
      */
-    fun dump(pw: PrintWriter, args: Array<String>) {
+    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
         Trace.beginSection("DumpManager#dump()")
         val start = SystemClock.uptimeMillis()
 
@@ -111,10 +116,12 @@
             return
         }
 
-        when (parsedArgs.dumpPriority) {
-            PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
-            PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
-            else -> dumpParameterized(pw, parsedArgs)
+        when {
+            parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
+            parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> {
+                dumpNormal(pw, parsedArgs)
+            }
+            else -> dumpParameterized(fd, pw, parsedArgs)
         }
 
         pw.println()
@@ -122,7 +129,7 @@
         Trace.endSection()
     }
 
-    private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) {
+    private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
         when (args.command) {
             "bugreport-critical" -> dumpCritical(pw, args)
             "bugreport-normal" -> dumpNormal(pw, args)
@@ -130,7 +137,13 @@
             "buffers" -> dumpBuffers(pw, args)
             "config" -> dumpConfig(pw)
             "help" -> dumpHelp(pw)
-            else -> dumpTargets(args.nonFlagArgs, pw, args)
+            else -> {
+                if (args.proto) {
+                    dumpProtoTargets(args.nonFlagArgs, fd, args)
+                } else {
+                    dumpTargets(args.nonFlagArgs, pw, args)
+                }
+            }
         }
     }
 
@@ -160,6 +173,26 @@
         }
     }
 
+    private fun dumpProtoTargets(
+            targets: List<String>,
+            fd: FileDescriptor,
+            args: ParsedArgs
+    ) {
+        val systemUIProto = SystemUIProtoDump()
+        if (targets.isNotEmpty()) {
+            for (target in targets) {
+                dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs)
+            }
+        } else {
+            dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs)
+        }
+        val buffer = BufferedOutputStream(FileOutputStream(fd))
+        buffer.use {
+            it.write(MessageNano.toByteArray(systemUIProto))
+            it.flush()
+        }
+    }
+
     private fun dumpTargets(
         targets: List<String>,
         pw: PrintWriter,
@@ -235,6 +268,7 @@
         pw.println("$ <invocation> buffers")
         pw.println("$ <invocation> bugreport-critical")
         pw.println("$ <invocation> bugreport-normal")
+        pw.println("$ <invocation> config")
         pw.println()
 
         pw.println("Targets can be listed:")
@@ -266,6 +300,7 @@
                             }
                         }
                     }
+                    PROTO -> pArgs.proto = true
                     "-t", "--tail" -> {
                         pArgs.tailLength = readArgument(iterator, arg) {
                             it.toInt()
@@ -277,6 +312,9 @@
                     "-h", "--help" -> {
                         pArgs.command = "help"
                     }
+                    // This flag is passed as part of the proto dump in Bug reports, we can ignore
+                    // it because this is our default behavior.
+                    "-a" -> {}
                     else -> {
                         throw ArgParseException("Unknown flag: $arg")
                     }
@@ -313,13 +351,21 @@
         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
         const val PRIORITY_ARG_HIGH = "HIGH"
         const val PRIORITY_ARG_NORMAL = "NORMAL"
+        const val PROTO = "--proto"
     }
 }
 
 private val PRIORITY_OPTIONS =
         arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
 
-private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
+private val COMMANDS = arrayOf(
+        "bugreport-critical",
+        "bugreport-normal",
+        "buffers",
+        "dumpables",
+        "config",
+        "help"
+)
 
 private class ParsedArgs(
     val rawArgs: Array<String>,
@@ -329,6 +375,7 @@
     var tailLength: Int = 0
     var command: String? = null
     var listOnly = false
+    var proto = false
 }
 
 class ArgParseException(message: String) : Exception(message)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index cca04da..ae78089 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,7 +18,9 @@
 
 import android.util.ArrayMap
 import com.android.systemui.Dumpable
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.ProtoDumpable
+import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -90,7 +92,7 @@
         target: String,
         pw: PrintWriter,
         args: Array<String>,
-        tailLength: Int
+        tailLength: Int,
     ) {
         for (dumpable in dumpables.values) {
             if (dumpable.name.endsWith(target)) {
@@ -107,6 +109,36 @@
         }
     }
 
+    @Synchronized
+    fun dumpProtoTarget(
+        target: String,
+        protoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) {
+                dumpProtoDumpable(dumpable.dumpable, protoDump, args)
+                return
+            }
+        }
+    }
+
+    @Synchronized
+    fun dumpProtoDumpables(
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable) {
+                dumpProtoDumpable(
+                    dumpable.dumpable,
+                    systemUIProtoDump,
+                    args
+                )
+            }
+        }
+    }
+
     /**
      * Dumps all registered dumpables to [pw]
      */
@@ -184,6 +216,14 @@
         buffer.dumpable.dump(pw, tailLength)
     }
 
+    private fun dumpProtoDumpable(
+        protoDumpable: ProtoDumpable,
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        protoDumpable.dumpProto(systemUIProtoDump, args)
+    }
+
     private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
         val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
         return existingDumpable == null || newDumpable == existingDumpable
@@ -195,4 +235,4 @@
     val dumpable: T
 )
 
-private const val TAG = "DumpManager"
\ No newline at end of file
+private const val TAG = "DumpManager"
diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
index 0eab1af..8299b13 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
@@ -19,7 +19,7 @@
 import android.content.Context
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.util.io.Files
 import com.android.systemui.util.time.SystemClock
 import java.io.IOException
diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 0a41a56..da983ab 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
+++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
@@ -51,6 +51,7 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         // Simulate the NORMAL priority arg being passed to us
         mDumpHandler.dump(
+                fd,
                 pw,
                 new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dump/sysui.proto b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
new file mode 100644
index 0000000..cd8c08a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+syntax = "proto3";
+
+package com.android.systemui.dump;
+
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto";
+
+option java_multiple_files = true;
+
+message SystemUIProtoDump {
+  repeated com.android.systemui.qs.QsTileState tiles = 1;
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
index fb4fc92..95e7ad96 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
@@ -34,9 +34,6 @@
     fun isEnabled(flag: ResourceBooleanFlag): Boolean
 
     /** Returns a boolean value for the given flag.  */
-    fun isEnabled(flag: DeviceConfigBooleanFlag): Boolean
-
-    /** Returns a boolean value for the given flag.  */
     fun isEnabled(flag: SysPropBooleanFlag): Boolean
 
     /** Returns a string value for the given flag.  */
@@ -44,4 +41,10 @@
 
     /** Returns a string value for the given flag.  */
     fun getString(flag: ResourceStringFlag): String
+
+    /** Returns an int value for a given flag/ */
+    fun getInt(flag: IntFlag): Int
+
+    /** Returns an int value for a given flag/ */
+    fun getInt(flag: ResourceIntFlag): Int
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index 3adeeac..81df4ed 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;
 
@@ -39,7 +40,6 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.settings.SecureSettings;
 
 import org.jetbrains.annotations.NotNull;
@@ -59,30 +59,38 @@
  *
  * 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;
-    private final DeviceConfigProxy mDeviceConfigProxy;
     private final ServerFlagReader mServerFlagReader;
     private final Map<Integer, Flag<?>> mAllFlags;
     private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
     private final Map<Integer, String> mStringFlagCache = new TreeMap<>();
+    private final Map<Integer, Integer> mIntFlagCache = 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,
@@ -90,26 +98,29 @@
             SecureSettings secureSettings,
             SystemPropertiesHelper systemProperties,
             @Main Resources resources,
-            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
@@ -126,7 +137,7 @@
         int id = flag.getId();
         if (!mBooleanFlagCache.containsKey(id)) {
             mBooleanFlagCache.put(id,
-                    readFlagValue(id, flag.getDefault()));
+                    readBooleanFlagInternal(flag, flag.getDefault()));
         }
 
         return mBooleanFlagCache.get(id);
@@ -137,19 +148,7 @@
         int id = flag.getId();
         if (!mBooleanFlagCache.containsKey(id)) {
             mBooleanFlagCache.put(id,
-                    readFlagValue(id, mResources.getBoolean(flag.getResourceId())));
-        }
-
-        return mBooleanFlagCache.get(id);
-    }
-
-    @Override
-    public boolean isEnabled(@NonNull DeviceConfigBooleanFlag flag) {
-        int id = flag.getId();
-        if (!mBooleanFlagCache.containsKey(id)) {
-            boolean deviceConfigValue = mDeviceConfigProxy.getBoolean(flag.getNamespace(),
-                    flag.getName(), flag.getDefault());
-            mBooleanFlagCache.put(id, readFlagValue(id, deviceConfigValue));
+                    readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId())));
         }
 
         return mBooleanFlagCache.get(id);
@@ -165,7 +164,7 @@
                     id,
                     mSystemProperties.getBoolean(
                             flag.getName(),
-                            readFlagValue(id, flag.getDefault())));
+                            readBooleanFlagInternal(flag, flag.getDefault())));
         }
 
         return mBooleanFlagCache.get(id);
@@ -177,7 +176,7 @@
         int id = flag.getId();
         if (!mStringFlagCache.containsKey(id)) {
             mStringFlagCache.put(id,
-                    readFlagValue(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
+                    readFlagValueInternal(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
         }
 
         return mStringFlagCache.get(id);
@@ -189,27 +188,57 @@
         int id = flag.getId();
         if (!mStringFlagCache.containsKey(id)) {
             mStringFlagCache.put(id,
-                    readFlagValue(id, mResources.getString(flag.getResourceId()),
+                    readFlagValueInternal(id, mResources.getString(flag.getResourceId()),
                             StringFlagSerializer.INSTANCE));
         }
 
         return mStringFlagCache.get(id);
     }
 
+
+    @NonNull
+    @Override
+    public int getInt(@NonNull IntFlag flag) {
+        int id = flag.getId();
+        if (!mIntFlagCache.containsKey(id)) {
+            mIntFlagCache.put(id,
+                    readFlagValueInternal(id, flag.getDefault(), IntFlagSerializer.INSTANCE));
+        }
+
+        return mIntFlagCache.get(id);
+    }
+
+    @NonNull
+    @Override
+    public int getInt(@NonNull ResourceIntFlag flag) {
+        int id = flag.getId();
+        if (!mIntFlagCache.containsKey(id)) {
+            mIntFlagCache.put(id,
+                    readFlagValueInternal(id, mResources.getInteger(flag.getResourceId()),
+                            IntFlagSerializer.INSTANCE));
+        }
+
+        return mIntFlagCache.get(id);
+    }
+
     /** 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);
+    private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) {
+        Boolean result = readBooleanFlagOverride(flag.getId());
+        boolean hasServerOverride = mServerFlagReader.hasOverride(
+                flag.getNamespace(), flag.getName());
 
         // Only check for teamfood if the default is false
         // and there is no server override.
-        if (!hasServerOverride && !defaultValue && result == null && id != Flags.TEAMFOOD.getId()) {
-            if (mAllFlags.containsKey(id) && mAllFlags.get(id).getTeamfood()) {
-                return isEnabled(Flags.TEAMFOOD);
-            }
+        if (!hasServerOverride
+                && !defaultValue
+                && result == null
+                && flag.getId() != Flags.TEAMFOOD.getId()
+                && flag.getTeamfood()) {
+            return isEnabled(Flags.TEAMFOOD);
         }
 
-        return result == null ? mServerFlagReader.readServerOverride(id, defaultValue) : result;
+        return result == null ? mServerFlagReader.readServerOverride(
+                flag.getNamespace(), flag.getName(), defaultValue) : result;
     }
 
     private Boolean readBooleanFlagOverride(int id) {
@@ -217,7 +246,8 @@
     }
 
     @NonNull
-    private <T> T readFlagValue(int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
+    private <T> T readFlagValueInternal(
+            int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
         requireNonNull(defaultValue, "defaultValue");
         T result = readFlagValueInternal(id, serializer);
         return result == null ? defaultValue : result;
@@ -273,6 +303,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!
@@ -306,7 +337,6 @@
             Log.i(TAG, "Android Restart Suppressed");
             return;
         }
-        Log.i(TAG, "Restarting Android");
         mRestarter.restart();
     }
 
@@ -315,8 +345,6 @@
             setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof ResourceBooleanFlag) {
             setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
-        } else if (flag instanceof DeviceConfigBooleanFlag) {
-            setFlagValue(flag.getId(), value, BooleanFlagSerializer.INSTANCE);
         } else if (flag instanceof SysPropBooleanFlag) {
             // Store SysProp flags in SystemProperties where they can read by outside parties.
             mSystemProperties.setBoolean(((SysPropBooleanFlag) flag).getName(), value);
@@ -337,6 +365,16 @@
         }
     }
 
+    void setIntFlagInternal(Flag<?> flag, int value) {
+        if (flag instanceof IntFlag) {
+            setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+        } else if (flag instanceof ResourceIntFlag) {
+            setFlagValue(flag.getId(), value, IntFlagSerializer.INSTANCE);
+        } else {
+            throw new IllegalArgumentException("Unknown flag type");
+        }
+    }
+
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -358,7 +396,7 @@
                     }
                 }
 
-                Bundle extras =  getResultExtras(true);
+                Bundle extras = getResultExtras(true);
                 if (extras != null) {
                     extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
                 }
@@ -424,9 +462,6 @@
             } else if (f instanceof ResourceBooleanFlag) {
                 enabled = isEnabled((ResourceBooleanFlag) f);
                 overridden = readBooleanFlagOverride(f.getId()) != null;
-            } else if (f instanceof DeviceConfigBooleanFlag) {
-                enabled = isEnabled((DeviceConfigBooleanFlag) f);
-                overridden = false;
             } else if (f instanceof SysPropBooleanFlag) {
                 // TODO(b/223379190): Teamfood not supported for sysprop flags yet.
                 enabled = isEnabled((SysPropBooleanFlag) f);
@@ -439,9 +474,11 @@
             }
 
             if (enabled) {
-                return new ReleasedFlag(f.getId(), teamfood, overridden);
+                return new ReleasedFlag(
+                        f.getId(), f.getName(), f.getNamespace(), teamfood, overridden);
             } else {
-                return new UnreleasedFlag(f.getId(), teamfood, overridden);
+                return new UnreleasedFlag(
+                        f.getId(), f.getName(), f.getNamespace(), teamfood, overridden);
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
new file mode 100644
index 0000000..3d9f627
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.util.Log
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import javax.inject.Inject
+
+/** Restarts SystemUI when the screen is locked. */
+class FeatureFlagsDebugRestarter
+@Inject
+constructor(
+    private val wakefulnessLifecycle: WakefulnessLifecycle,
+    private val systemExitRestarter: SystemExitRestarter,
+) : Restarter {
+
+    val observer =
+        object : WakefulnessLifecycle.Observer {
+            override fun onFinishedGoingToSleep() {
+                Log.d(FeatureFlagsDebug.TAG, "Restarting due to systemui flag change")
+                restartNow()
+            }
+        }
+
+    override fun restart() {
+        Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting on next screen off.")
+        if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) {
+            restartNow()
+        } else {
+            wakefulnessLifecycle.addObserver(observer)
+        }
+    }
+
+    private fun restartNow() {
+        systemExitRestarter.restart()
+    }
+}
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..3c83682 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) {
@@ -77,7 +101,7 @@
 
     @Override
     public boolean isEnabled(@NotNull ReleasedFlag flag) {
-        return mServerFlagReader.readServerOverride(flag.getId(), true);
+        return mServerFlagReader.readServerOverride(flag.getNamespace(), flag.getName(), true);
     }
 
     @Override
@@ -91,18 +115,6 @@
     }
 
     @Override
-    public boolean isEnabled(@NonNull DeviceConfigBooleanFlag flag) {
-        int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
-        if (cacheIndex < 0) {
-            boolean deviceConfigValue = mDeviceConfigProxy.getBoolean(flag.getNamespace(),
-                    flag.getName(), flag.getDefault());
-            return isEnabled(flag.getId(), deviceConfigValue);
-        }
-
-        return mBooleanCache.valueAt(cacheIndex);
-    }
-
-    @Override
     public boolean isEnabled(SysPropBooleanFlag flag) {
         int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
         if (cacheIndex < 0) {
@@ -141,13 +153,25 @@
         return defaultValue;
     }
 
+    @NonNull
+    @Override
+    public int getInt(@NonNull IntFlag flag) {
+        return flag.getDefault();
+    }
+
+    @NonNull
+    @Override
+    public int getInt(@NonNull ResourceIntFlag flag) {
+        return mResources.getInteger(flag.getResourceId());
+    }
+
     @Override
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("can override: false");
-        Map<Integer, Flag<?>> knownFlags = Flags.collectFlags();
-        for (Map.Entry<Integer, Flag<?>> idToFlag : knownFlags.entrySet()) {
-            int id = idToFlag.getKey();
-            Flag<?> flag = idToFlag.getValue();
+        Map<String, Flag<?>> knownFlags = FlagsFactory.INSTANCE.getKnownFlags();
+        for (Map.Entry<String, Flag<?>> nameToFlag : knownFlags.entrySet()) {
+            Flag<?> flag = nameToFlag.getValue();
+            int id = flag.getId();
             boolean def = false;
             if (mBooleanCache.indexOfKey(flag.getId()) < 0) {
                 if (flag instanceof SysPropBooleanFlag) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt
new file mode 100644
index 0000000..a3f0f66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.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.flags
+
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+/** Restarts SystemUI when the device appears idle. */
+class FeatureFlagsReleaseRestarter
+@Inject
+constructor(
+    private val wakefulnessLifecycle: WakefulnessLifecycle,
+    private val batteryController: BatteryController,
+    @Background private val bgExecutor: DelayableExecutor,
+    private val systemExitRestarter: SystemExitRestarter
+) : Restarter {
+    var shouldRestart = false
+    var pendingRestart: Runnable? = null
+
+    val observer =
+        object : WakefulnessLifecycle.Observer {
+            override fun onFinishedGoingToSleep() {
+                maybeScheduleRestart()
+            }
+        }
+
+    val batteryCallback =
+        object : BatteryController.BatteryStateChangeCallback {
+            override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
+                maybeScheduleRestart()
+            }
+        }
+
+    override fun restart() {
+        Log.d(FeatureFlagsDebug.TAG, "Restart requested. Restarting when plugged in and idle.")
+        if (!shouldRestart) {
+            // Don't bother scheduling twice.
+            shouldRestart = true
+            wakefulnessLifecycle.addObserver(observer)
+            batteryController.addCallback(batteryCallback)
+            maybeScheduleRestart()
+        }
+    }
+
+    private fun maybeScheduleRestart() {
+        if (
+            wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn
+        ) {
+            if (pendingRestart == null) {
+                pendingRestart = bgExecutor.executeDelayed(this::restartNow, 30L, TimeUnit.SECONDS)
+            }
+        } else if (pendingRestart != null) {
+            pendingRestart?.run()
+            pendingRestart = null
+        }
+    }
+
+    private fun restartNow() {
+        Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change")
+        systemExitRestarter.restart()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
index 4d25431..b7fc0e4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
@@ -16,12 +16,13 @@
 
 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;
 
 import java.io.PrintWriter;
-import java.lang.reflect.Field;
 import java.util.List;
 import java.util.Map;
 
@@ -36,13 +37,14 @@
 
     private final List<String> mOnCommands = List.of("true", "on", "1", "enabled");
     private final List<String> mOffCommands = List.of("false", "off", "0", "disable");
+    private final List<String> mSetCommands = List.of("set", "put");
     private final FeatureFlagsDebug mFeatureFlags;
     private final Map<Integer, Flag<?>> mAllFlags;
 
     @Inject
     FlagCommand(
             FeatureFlagsDebug featureFlags,
-            @Named(FeatureFlagsDebug.ALL_FLAGS) Map<Integer, Flag<?>> allFlags
+            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags
     ) {
         mFeatureFlags = featureFlags;
         mAllFlags = allFlags;
@@ -58,12 +60,6 @@
             return;
         }
 
-        if (args.size() > 2) {
-            pw.println("Invalid number of arguments.");
-            help(pw);
-            return;
-        }
-
         int id = 0;
         try {
             id = Integer.parseInt(args.get(0));
@@ -83,48 +79,113 @@
         Flag<?> flag = mAllFlags.get(id);
 
         String cmd = "";
-        if (args.size() == 2) {
+        if (args.size() > 1) {
             cmd = args.get(1).toLowerCase();
         }
 
         if ("erase".equals(cmd) || "reset".equals(cmd)) {
+            if (args.size() > 2) {
+                pw.println("Invalid number of arguments to reset a flag.");
+                help(pw);
+                return;
+            }
+
             mFeatureFlags.eraseFlag(flag);
             return;
         }
 
-        boolean newValue = true;
-        if (args.size() == 1 || "toggle".equals(cmd)) {
-            boolean enabled = isBooleanFlagEnabled(flag);
-
-            if (args.size() == 1) {
-                pw.println("Flag " + id + " is " + enabled);
+        boolean shouldSet = true;
+        if (args.size() == 1) {
+            shouldSet = false;
+        }
+        if (isBooleanFlag(flag)) {
+            if (args.size() > 2) {
+                pw.println("Invalid number of arguments for a boolean flag.");
+                help(pw);
                 return;
             }
-
-            newValue = !enabled;
-        } else {
-            newValue = mOnCommands.contains(cmd);
-            if (!newValue && !mOffCommands.contains(cmd)) {
+            boolean newValue = isBooleanFlagEnabled(flag);
+            if ("toggle".equals(cmd)) {
+                newValue = !newValue;
+            } else if (mOnCommands.contains(cmd)) {
+                newValue = true;
+            } else if (mOffCommands.contains(cmd)) {
+                newValue = false;
+            } else if (shouldSet) {
                 pw.println("Invalid on/off argument supplied");
                 help(pw);
                 return;
             }
-        }
 
-        pw.flush();  // Next command will restart sysui, so flush before we do so.
-        mFeatureFlags.setBooleanFlagInternal(flag, newValue);
+            pw.println("Flag " + id + " is " + newValue);
+            pw.flush();  // Next command will restart sysui, so flush before we do so.
+            if (shouldSet) {
+                mFeatureFlags.setBooleanFlagInternal(flag, newValue);
+            }
+            return;
+
+        } else if (isStringFlag(flag)) {
+            if (shouldSet) {
+                if (args.size() != 3) {
+                    pw.println("Invalid number of arguments a StringFlag.");
+                    help(pw);
+                    return;
+                } else if (!mSetCommands.contains(cmd)) {
+                    pw.println("Unknown command: " + cmd);
+                    help(pw);
+                    return;
+                }
+                String value = args.get(2);
+                pw.println("Setting Flag " + id + " to " + value);
+                pw.flush();  // Next command will restart sysui, so flush before we do so.
+                mFeatureFlags.setStringFlagInternal(flag, args.get(2));
+            } else {
+                pw.println("Flag " + id + " is " + getStringFlag(flag));
+            }
+            return;
+        } else if (isIntFlag(flag)) {
+            if (shouldSet) {
+                if (args.size() != 3) {
+                    pw.println("Invalid number of arguments for an IntFlag.");
+                    help(pw);
+                    return;
+                } else if (!mSetCommands.contains(cmd)) {
+                    pw.println("Unknown command: " + cmd);
+                    help(pw);
+                    return;
+                }
+                int value = Integer.parseInt(args.get(2));
+                pw.println("Setting Flag " + id + " to " + value);
+                pw.flush();  // Next command will restart sysui, so flush before we do so.
+                mFeatureFlags.setIntFlagInternal(flag, value);
+            } else {
+                pw.println("Flag " + id + " is " + getIntFlag(flag));
+            }
+            return;
+        }
     }
 
     @Override
     public void help(PrintWriter pw) {
-        pw.println(
-                "Usage: adb shell cmd statusbar flag <id> "
+        pw.println("Usage: adb shell cmd statusbar flag <id> [options]");
+        pw.println();
+        pw.println("  Boolean Flag Options: "
                         + "[true|false|1|0|on|off|enable|disable|toggle|erase|reset]");
+        pw.println("  String Flag Options: [set|put \"<value>\"]");
+        pw.println("  Int Flag Options: [set|put <value>]");
+        pw.println();
         pw.println("The id can either be a numeric integer or the corresponding field name");
         pw.println(
                 "If no argument is supplied after the id, the flags runtime value is output");
     }
 
+    private boolean isBooleanFlag(Flag<?> flag) {
+        return (flag instanceof BooleanFlag)
+                || (flag instanceof ResourceBooleanFlag)
+                || (flag instanceof SysPropFlag)
+                || (flag instanceof DeviceConfigBooleanFlag);
+    }
+
     private boolean isBooleanFlagEnabled(Flag<?> flag) {
         if (flag instanceof ReleasedFlag) {
             return mFeatureFlags.isEnabled((ReleasedFlag) flag);
@@ -139,34 +200,51 @@
         return false;
     }
 
+    private boolean isStringFlag(Flag<?> flag) {
+        return (flag instanceof StringFlag) || (flag instanceof ResourceStringFlag);
+    }
+
+    private String getStringFlag(Flag<?> flag) {
+        if (flag instanceof StringFlag) {
+            return mFeatureFlags.getString((StringFlag) flag);
+        } else if (flag instanceof ResourceStringFlag) {
+            return mFeatureFlags.getString((ResourceStringFlag) flag);
+        }
+
+        return "";
+    }
+
+    private boolean isIntFlag(Flag<?> flag) {
+        return (flag instanceof IntFlag) || (flag instanceof ResourceIntFlag);
+    }
+
+    private int getIntFlag(Flag<?> flag) {
+        if (flag instanceof IntFlag) {
+            return mFeatureFlags.getInt((IntFlag) flag);
+        } else if (flag instanceof ResourceIntFlag) {
+            return mFeatureFlags.getInt((ResourceIntFlag) flag);
+        }
+
+        return 0;
+    }
+
     private int flagNameToId(String flagName) {
-        List<Field> fields = Flags.getFlagFields();
-        for (Field field : fields) {
-            if (flagName.equals(field.getName())) {
-                return fieldToId(field);
+        Map<String, Flag<?>> flagFields = FlagsFactory.INSTANCE.getKnownFlags();
+        for (String fieldName : flagFields.keySet()) {
+            if (flagName.equals(fieldName)) {
+                return flagFields.get(fieldName).getId();
             }
         }
 
         return 0;
     }
 
-    private int fieldToId(Field field) {
-        try {
-            Flag<?> flag = (Flag<?>) field.get(null);
-            return flag.getId();
-        } catch (IllegalAccessException e) {
-            // no-op
-        }
-
-        return 0;
-    }
-
     private void printKnownFlags(PrintWriter pw) {
-        List<Field> fields = Flags.getFlagFields();
+        Map<String, Flag<?>> fields = FlagsFactory.INSTANCE.getKnownFlags();
 
         int longestFieldName = 0;
-        for (Field field : fields) {
-            longestFieldName = Math.max(longestFieldName, field.getName().length());
+        for (String fieldName : fields.keySet()) {
+            longestFieldName = Math.max(longestFieldName, fieldName.length());
         }
 
         pw.println("Known Flags:");
@@ -174,23 +252,32 @@
         for (int i = 0; i < longestFieldName - "Flag Name".length() + 1; i++) {
             pw.print(" ");
         }
-        pw.println("ID   Enabled?");
+        pw.println("ID   Value");
         for (int i = 0; i < longestFieldName; i++) {
             pw.print("=");
         }
         pw.println(" ==== ========");
-        for (Field field : fields) {
-            int id = fieldToId(field);
+        for (String fieldName : fields.keySet()) {
+            Flag<?> flag = fields.get(fieldName);
+            int id = flag.getId();
             if (id == 0 || !mAllFlags.containsKey(id)) {
                 continue;
             }
-            pw.print(field.getName());
-            int fieldWidth = field.getName().length();
+            pw.print(fieldName);
+            int fieldWidth = fieldName.length();
             for (int i = 0; i < longestFieldName - fieldWidth + 1; i++) {
                 pw.print(" ");
             }
             pw.printf("%-4d ", id);
-            pw.println(isBooleanFlagEnabled(mAllFlags.get(id)));
+            if (isBooleanFlag(flag)) {
+                pw.println(isBooleanFlagEnabled(mAllFlags.get(id)));
+            } else if (isStringFlag(flag)) {
+                pw.println(getStringFlag(flag));
+            } else if (isIntFlag(flag)) {
+                pw.println(getIntFlag(flag));
+            } else {
+                pw.println("<unknown flag type>");
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
deleted file mode 100644
index 9beb1e9..0000000
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ /dev/null
@@ -1,358 +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.
- */
-
-package com.android.systemui.flags;
-
-import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
-
-import com.android.internal.annotations.Keep;
-import com.android.systemui.R;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * List of {@link Flag} objects for use in SystemUI.
- *
- * Flag Ids are integers.
- * Ids must be unique. This is enforced in a unit test.
- * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with
- * a comment. This is purely for organizational purposes.
- *
- * On public release builds, flags will always return their default value. There is no way to
- * change their value on release builds.
- *
- * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb.
- */
-public class Flags {
-    public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1);
-
-    /***************************************/
-    // 100 - notification
-    public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
-            new UnreleasedFlag(103);
-
-    public static final UnreleasedFlag NSSL_DEBUG_LINES =
-            new UnreleasedFlag(105);
-
-    public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION =
-            new UnreleasedFlag(106);
-
-    public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE =
-            new UnreleasedFlag(107);
-
-    public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
-            new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
-
-    public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
-            new ReleasedFlag(109);
-
-    public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD =
-            new UnreleasedFlag(110, true);
-
-    public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
-
-    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
-            false);
-
-    public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true);
-
-    // next id: 114
-
-    /***************************************/
-    // 200 - keyguard/lockscreen
-
-    // ** Flag retired **
-    // public static final BooleanFlag KEYGUARD_LAYOUT =
-    //         new BooleanFlag(200, true);
-
-    public static final ReleasedFlag LOCKSCREEN_ANIMATIONS =
-            new ReleasedFlag(201);
-
-    public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION =
-            new ReleasedFlag(202);
-
-    public static final ResourceBooleanFlag CHARGING_RIPPLE =
-            new ResourceBooleanFlag(203, R.bool.flag_charging_ripple);
-
-    public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER =
-            new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher);
-
-    public static final ResourceBooleanFlag FACE_SCANNING_ANIM =
-            new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation);
-
-    public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207);
-  
-    /**
-     * Flag to enable the usage of the new bouncer data source. This is a refactor of and
-     * eventual replacement of KeyguardBouncer.java.
-     */
-    public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);
-
-    /**
-     * Whether the user interactor and repository should use `UserSwitcherController`.
-     *
-     * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
-     * the framework APIs.
-     */
-    public static final ReleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
-            new ReleasedFlag(210);
-
-    /**
-     * Whether `UserSwitcherController` should use the user interactor.
-     *
-     * <p>When this is {@code true}, the controller does not directly access framework APIs.
-     * Instead, it goes through the interactor.
-     *
-     * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
-     * {@code true} as it would created a cycle between controller -> interactor -> controller.
-     */
-    public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = new UnreleasedFlag(211);
-
-    /***************************************/
-    // 300 - power menu
-    public static final ReleasedFlag POWER_MENU_LITE =
-            new ReleasedFlag(300);
-
-    /***************************************/
-    // 400 - smartspace
-    public static final ReleasedFlag SMARTSPACE_DEDUPING =
-            new ReleasedFlag(400);
-
-    public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
-            new ReleasedFlag(401);
-
-    public static final ResourceBooleanFlag SMARTSPACE =
-            new ResourceBooleanFlag(402, R.bool.flag_smartspace);
-
-    /***************************************/
-    // 500 - quick settings
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_USER_SWITCHER =
-            new ReleasedFlag(500);
-
-    public static final UnreleasedFlag COMBINED_QS_HEADERS =
-            new UnreleasedFlag(501, true);
-
-    public static final ResourceBooleanFlag PEOPLE_TILE =
-            new ResourceBooleanFlag(502, R.bool.flag_conversations);
-
-    public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT =
-            new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut);
-
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504);
-
-    public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true);
-    public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
-            new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
-
-    public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
-
-    /***************************************/
-    // 600- status bar
-    public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
-            new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip);
-
-    public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
-            new ReleasedFlag(603, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND =
-            new UnreleasedFlag(604, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND =
-            new UnreleasedFlag(605, false);
-
-    /***************************************/
-    // 700 - dialer/calls
-    public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP =
-            new ReleasedFlag(700);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE =
-            new ReleasedFlag(701);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP =
-            new ReleasedFlag(702);
-
-    /***************************************/
-    // 800 - general visual/theme
-    public static final ResourceBooleanFlag MONET =
-            new ResourceBooleanFlag(800, R.bool.flag_monet);
-
-    /***************************************/
-    // 801 - region sampling
-    public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801);
-
-    // 802 - wallpaper rendering
-    public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true);
-
-    // 803 - screen contents translation
-    public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803);
-
-    /***************************************/
-    // 900 - media
-    public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900);
-    public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
-    public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
-    public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
-    public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905);
-    public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906);
-    public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907);
-
-    // 1000 - dock
-    public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
-            new ReleasedFlag(1000);
-    public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001);
-
-    public static final UnreleasedFlag ROUNDED_BOX_RIPPLE =
-            new UnreleasedFlag(1002, /* teamfood= */ true);
-
-    public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true);
-
-    // 1100 - windowing
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
-            new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
-
-    /**
-     * b/170163464: animate bubbles expanded view collapse with home gesture
-     */
-    @Keep
-    public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE =
-            new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true);
-
-    @Keep
-    public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING =
-            new DeviceConfigBooleanFlag(1102, "record_task_content",
-                    NAMESPACE_WINDOW_MANAGER, false, true);
-
-    @Keep
-    public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
-            new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING =
-            new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
-            new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
-
-    @Keep
-    public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
-            new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
-
-    @Keep
-    public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
-            new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
-            new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
-            new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-            new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false);
-
-    // 1200 - predictive back
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
-            1200, "persist.wm.debug.predictive_back", true);
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag(
-            1201, "persist.wm.debug.predictive_back_anim", false);
-    @Keep
-    public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
-            new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false);
-
-    public static final UnreleasedFlag NEW_BACK_AFFORDANCE =
-            new UnreleasedFlag(1203, false /* teamfood */);
-
-    // 1300 - screenshots
-
-    public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
-    public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
-
-    // 1400 - columbus
-    public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400);
-
-    // 1500 - chooser
-    public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
-
-    // 1600 - accessibility
-    public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
-            new UnreleasedFlag(1600);
-
-    // 1700 - clipboard
-    public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
-
-    // Pay no attention to the reflection behind the curtain.
-    // ========================== Curtain ==========================
-    // |                                                           |
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    private static Map<Integer, Flag<?>> sFlagMap;
-    static Map<Integer, Flag<?>> collectFlags() {
-        if (sFlagMap != null) {
-            return sFlagMap;
-        }
-
-        Map<Integer, Flag<?>> flags = new HashMap<>();
-        List<Field> flagFields = getFlagFields();
-
-        for (Field field : flagFields) {
-            try {
-                Flag<?> flag = (Flag<?>) field.get(null);
-                flags.put(flag.getId(), flag);
-            } catch (IllegalAccessException e) {
-                // no-op
-            }
-        }
-
-        sFlagMap = flags;
-
-        return sFlagMap;
-    }
-
-    static List<Field> getFlagFields() {
-        Field[] fields = Flags.class.getFields();
-        List<Field> result = new ArrayList<>();
-
-        for (Field field : fields) {
-            Class<?> t = field.getType();
-            if (Flag.class.isAssignableFrom(t)) {
-                result.add(field);
-            }
-        }
-
-        return result;
-    }
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    // |                                                           |
-    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
-
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
new file mode 100644
index 0000000..51691c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -0,0 +1,413 @@
+/*
+ * 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.provider.DeviceConfig
+import com.android.internal.annotations.Keep
+import com.android.systemui.R
+import com.android.systemui.flags.FlagsFactory.releasedFlag
+import com.android.systemui.flags.FlagsFactory.resourceBooleanFlag
+import com.android.systemui.flags.FlagsFactory.sysPropBooleanFlag
+import com.android.systemui.flags.FlagsFactory.unreleasedFlag
+
+/**
+ * List of [Flag] objects for use in SystemUI.
+ *
+ * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be
+ * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This
+ * is purely for organizational purposes.
+ *
+ * On public release builds, flags will always return their default value. There is no way to change
+ * their value on release builds.
+ *
+ * See [FeatureFlagsDebug] for instructions on flipping the flags via adb.
+ */
+object Flags {
+    @JvmField val TEAMFOOD = unreleasedFlag(1, "teamfood")
+
+    // 100 - notification
+    // TODO(b/254512751): Tracking Bug
+    val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
+        unreleasedFlag(103, "notification_pipeline_developer_logging")
+
+    // TODO(b/254512732): Tracking Bug
+    @JvmField val NSSL_DEBUG_LINES = unreleasedFlag(105, "nssl_debug_lines")
+
+    // TODO(b/254512505): Tracking Bug
+    @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = unreleasedFlag(106, "nssl_debug_remove_animation")
+
+    // TODO(b/254512624): Tracking Bug
+    @JvmField
+    val NOTIFICATION_DRAG_TO_CONTENTS =
+        resourceBooleanFlag(
+            108,
+            R.bool.config_notificationToContents,
+            "notification_drag_to_contents"
+        )
+
+    // TODO(b/254512517): Tracking Bug
+    val FSI_REQUIRES_KEYGUARD = unreleasedFlag(110, "fsi_requires_keyguard", teamfood = true)
+
+    // TODO(b/259130119): Tracking Bug
+    val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true)
+
+    // TODO(b/254512538): Tracking Bug
+    val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
+
+    // TODO(b/254512425): Tracking Bug
+    val NOTIFICATION_MEMORY_MONITOR_ENABLED =
+        releasedFlag(112, "notification_memory_monitor_enabled")
+
+    // TODO(b/254512731): Tracking Bug
+    @JvmField
+    val NOTIFICATION_DISMISSAL_FADE =
+        unreleasedFlag(113, "notification_dismissal_fade", teamfood = true)
+    val STABILITY_INDEX_FIX = unreleasedFlag(114, "stability_index_fix", teamfood = true)
+    val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
+
+    @JvmField
+    val NOTIFICATION_GROUP_CORNER =
+        unreleasedFlag(116, "notification_group_corner", teamfood = true)
+
+    // TODO(b/259217907)
+    @JvmField
+    val NOTIFICATION_GROUP_DISMISSAL_ANIMATION =
+        unreleasedFlag(259217907, "notification_group_dismissal_animation", teamfood = true)
+
+    // TODO(b/257506350): Tracking Bug
+    val FSI_CHROME = unreleasedFlag(117, "fsi_chrome")
+
+    // TODO(b/257315550): Tracking Bug
+    val NO_HUN_FOR_OLD_WHEN = unreleasedFlag(118, "no_hun_for_old_when")
+
+    val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
+        unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
+
+    // 200 - keyguard/lockscreen
+    // ** Flag retired **
+    // public static final BooleanFlag KEYGUARD_LAYOUT =
+    //         new BooleanFlag(200, true);
+    // TODO(b/254512713): Tracking Bug
+    @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations")
+
+    // TODO(b/254512750): Tracking Bug
+    val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation")
+    val CHARGING_RIPPLE = resourceBooleanFlag(203, R.bool.flag_charging_ripple, "charging_ripple")
+
+    // TODO(b/254512281): Tracking Bug
+    @JvmField
+    val BOUNCER_USER_SWITCHER =
+        resourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher, "bouncer_user_switcher")
+
+    // TODO(b/254512676): Tracking Bug
+    @JvmField
+    val LOCKSCREEN_CUSTOM_CLOCKS = unreleasedFlag(207, "lockscreen_custom_clocks", teamfood = true)
+
+    /**
+     * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
+     * replacement of KeyguardBouncer.java.
+     */
+    // TODO(b/254512385): Tracking Bug
+    @JvmField val MODERN_BOUNCER = releasedFlag(208, "modern_bouncer")
+
+    /**
+     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
+     * the digits when the clock moves.
+     */
+    @JvmField val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation")
+
+    /**
+     * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
+     * will occur in stages. This is one stage of many to come.
+     */
+    // TODO(b/255607168): Tracking Bug
+    @JvmField val DOZING_MIGRATION_1 = unreleasedFlag(213, "dozing_migration_1")
+
+    // TODO(b/252897742): Tracking Bug
+    @JvmField val NEW_ELLIPSE_DETECTION = unreleasedFlag(214, "new_ellipse_detection")
+
+    // TODO(b/252897742): Tracking Bug
+    @JvmField val NEW_UDFPS_OVERLAY = unreleasedFlag(215, "new_udfps_overlay")
+
+    /**
+     * Whether to enable the code powering customizable lock screen quick affordances.
+     *
+     * This flag enables any new prebuilt quick affordances as well.
+     */
+    // TODO(b/255618149): Tracking Bug
+    @JvmField
+    val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES =
+        unreleasedFlag(216, "customizable_lock_screen_quick_affordances", teamfood = false)
+
+    /** Shows chipbar UI whenever the device is unlocked by ActiveUnlock (watch). */
+    // TODO(b/240196500): Tracking Bug
+    @JvmField val ACTIVE_UNLOCK_CHIPBAR = unreleasedFlag(217, "active_unlock_chipbar")
+
+    // 300 - power menu
+    // TODO(b/254512600): Tracking Bug
+    @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
+
+    // 400 - smartspace
+
+    // TODO(b/254513100): Tracking Bug
+    val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
+        releasedFlag(401, "smartspace_shared_element_transition_enabled")
+    val SMARTSPACE = resourceBooleanFlag(402, R.bool.flag_smartspace, "smartspace")
+
+    // 500 - quick settings
+
+    // TODO(b/254512321): Tracking Bug
+    @JvmField val COMBINED_QS_HEADERS = releasedFlag(501, "combined_qs_headers")
+    val PEOPLE_TILE = resourceBooleanFlag(502, R.bool.flag_conversations, "people_tile")
+
+    @JvmField
+    val QS_USER_DETAIL_SHORTCUT =
+        resourceBooleanFlag(
+            503,
+            R.bool.flag_lockscreen_qs_user_detail_shortcut,
+            "qs_user_detail_shortcut"
+        )
+
+    // TODO(b/254512747): Tracking Bug
+    val NEW_HEADER = releasedFlag(505, "new_header")
+
+    // TODO(b/254512383): Tracking Bug
+    @JvmField
+    val FULL_SCREEN_USER_SWITCHER =
+        resourceBooleanFlag(
+            506,
+            R.bool.config_enableFullscreenUserSwitcher,
+            "full_screen_user_switcher"
+        )
+
+    // TODO(b/254512678): Tracking Bug
+    @JvmField val NEW_FOOTER_ACTIONS = releasedFlag(507, "new_footer_actions")
+
+    // TODO(b/244064524): Tracking Bug
+    @JvmField val QS_SECONDARY_DATA_SUB_INFO = releasedFlag(508, "qs_secondary_data_sub_info")
+
+    // 600- status bar
+    // TODO(b/254513246): Tracking Bug
+    val STATUS_BAR_USER_SWITCHER =
+        resourceBooleanFlag(602, R.bool.flag_user_switcher_chip, "status_bar_user_switcher")
+
+    // TODO(b/254512623): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_BACKEND =
+        unreleasedFlag(604, "new_status_bar_pipeline_backend", teamfood = false)
+
+    // TODO(b/254512660): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_FRONTEND =
+        unreleasedFlag(605, "new_status_bar_pipeline_frontend", teamfood = false)
+
+    // TODO(b/256614753): Tracking Bug
+    val NEW_STATUS_BAR_MOBILE_ICONS = unreleasedFlag(606, "new_status_bar_mobile_icons")
+
+    // TODO(b/256614210): Tracking Bug
+    val NEW_STATUS_BAR_WIFI_ICON = unreleasedFlag(607, "new_status_bar_wifi_icon")
+
+    // TODO(b/256614751): Tracking Bug
+    val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
+        unreleasedFlag(608, "new_status_bar_mobile_icons_backend")
+
+    // TODO(b/256613548): Tracking Bug
+    val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend")
+
+    // TODO(b/256623670): Tracking Bug
+    @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon")
+
+    // 700 - dialer/calls
+    // TODO(b/254512734): Tracking Bug
+    val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
+
+    // TODO(b/254512681): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE = releasedFlag(701, "ongoing_call_in_immersive")
+
+    // TODO(b/254512753): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = releasedFlag(702, "ongoing_call_in_immersive_chip_tap")
+
+    // 800 - general visual/theme
+    @JvmField val MONET = resourceBooleanFlag(800, R.bool.flag_monet, "monet")
+
+    // 801 - region sampling
+    // TODO(b/254512848): Tracking Bug
+    val REGION_SAMPLING = unreleasedFlag(801, "region_sampling")
+
+    // 802 - wallpaper rendering
+    // TODO(b/254512923): Tracking Bug
+    @JvmField val USE_CANVAS_RENDERER = unreleasedFlag(802, "use_canvas_renderer")
+
+    // 803 - screen contents translation
+    // TODO(b/254513187): Tracking Bug
+    val SCREEN_CONTENTS_TRANSLATION = unreleasedFlag(803, "screen_contents_translation")
+
+    // 804 - monochromatic themes
+    @JvmField
+    val MONOCHROMATIC_THEMES =
+        sysPropBooleanFlag(804, "persist.sysui.monochromatic", default = false)
+
+    // 900 - media
+    // TODO(b/254512697): Tracking Bug
+    val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer")
+
+    // TODO(b/254512502): Tracking Bug
+    val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
+
+    // TODO(b/254512726): Tracking Bug
+    val MEDIA_NEARBY_DEVICES = releasedFlag(903, "media_nearby_devices")
+
+    // TODO(b/254512695): Tracking Bug
+    val MEDIA_MUTE_AWAIT = releasedFlag(904, "media_mute_await")
+
+    // TODO(b/254512654): Tracking Bug
+    @JvmField val DREAM_MEDIA_COMPLICATION = unreleasedFlag(905, "dream_media_complication")
+
+    // TODO(b/254512673): Tracking Bug
+    @JvmField val DREAM_MEDIA_TAP_TO_OPEN = unreleasedFlag(906, "dream_media_tap_to_open")
+
+    // TODO(b/254513168): Tracking Bug
+    @JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
+
+    @JvmField val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media")
+
+    // 1000 - dock
+    val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
+
+    // TODO(b/254512758): Tracking Bug
+    @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
+
+    // 1100 - windowing
+    @Keep
+    @JvmField
+    val WM_ENABLE_SHELL_TRANSITIONS =
+        sysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", default = false)
+
+    // TODO(b/254513207): Tracking Bug
+    @Keep
+    @JvmField
+    val WM_ENABLE_PARTIAL_SCREEN_SHARING =
+        unreleasedFlag(
+            1102,
+            name = "record_task_content",
+            namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+            teamfood = true
+        )
+
+    // TODO(b/254512674): Tracking Bug
+    @Keep
+    @JvmField
+    val HIDE_NAVBAR_WINDOW =
+        sysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", default = false)
+
+    @Keep
+    @JvmField
+    val WM_DESKTOP_WINDOWING =
+        sysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", default = false)
+
+    @Keep
+    @JvmField
+    val WM_CAPTION_ON_SHELL =
+        sysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", default = false)
+
+    @Keep
+    @JvmField
+    val ENABLE_FLING_TO_DISMISS_BUBBLE =
+        sysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", default = true)
+
+    @Keep
+    @JvmField
+    val ENABLE_FLING_TO_DISMISS_PIP =
+        sysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", default = true)
+
+    @Keep
+    @JvmField
+    val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
+        sysPropBooleanFlag(
+            1110,
+            "persist.wm.debug.enable_pip_keep_clear_algorithm",
+            default = false
+        )
+
+    // TODO(b/256873975): Tracking Bug
+    @JvmField @Keep val WM_BUBBLE_BAR = unreleasedFlag(1111, "wm_bubble_bar")
+
+    // 1200 - predictive back
+    @Keep
+    @JvmField
+    val WM_ENABLE_PREDICTIVE_BACK =
+        sysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", default = true)
+
+    @Keep
+    @JvmField
+    val WM_ENABLE_PREDICTIVE_BACK_ANIM =
+        sysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", default = false)
+
+    @Keep
+    @JvmField
+    val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
+        sysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", default = false)
+
+    // TODO(b/254512728): Tracking Bug
+    @JvmField
+    val NEW_BACK_AFFORDANCE = unreleasedFlag(1203, "new_back_affordance", teamfood = false)
+
+    // 1300 - screenshots
+    // TODO(b/254512719): Tracking Bug
+    @JvmField val SCREENSHOT_REQUEST_PROCESSOR = releasedFlag(1300, "screenshot_request_processor")
+
+    // TODO(b/254513155): Tracking Bug
+    @JvmField
+    val SCREENSHOT_WORK_PROFILE_POLICY =
+        unreleasedFlag(1301, "screenshot_work_profile_policy", teamfood = true)
+
+    // 1400 - columbus
+    // TODO(b/254512756): Tracking Bug
+    val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
+
+    // 1500 - chooser
+    // TODO(b/254512507): Tracking Bug
+    val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
+
+    // 1600 - accessibility
+    @JvmField
+    val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
+        unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
+
+    // 1700 - clipboard
+    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor")
+    @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = unreleasedFlag(1701, "clipboard_remote_behavior")
+
+    // 1800 - shade container
+    @JvmField
+    val LEAVE_SHADE_OPEN_FOR_BUGREPORT =
+        unreleasedFlag(1800, "leave_shade_open_for_bugreport", teamfood = true)
+
+    // 1900 - note task
+    @JvmField val NOTE_TASKS = sysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
+
+    // 2000 - device controls
+    @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
+
+    // 2100 - Falsing Manager
+    @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
+
+    // 2200 - udfps
+    // TODO(b/259264861): Tracking Bug
+    @JvmField val UDFPS_NEW_TOUCH_DETECTION = unreleasedFlag(2200, "udfps_new_touch_detection")
+    @JvmField val UDFPS_ELLIPSE_DEBUG_UI = unreleasedFlag(2201, "udfps_ellipse_debug")
+    @JvmField val UDFPS_ELLIPSE_DETECTION = unreleasedFlag(2202, "udfps_ellipse_detection")
+}
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..8442230
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 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 FlagsFactory.knownFlags.map { it.value.id to it.value }.toMap()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/OWNERS b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
new file mode 100644
index 0000000..c9d2db1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
@@ -0,0 +1,12 @@
+set noparent
+
+# Bug component: 1203176
+
+mankoff@google.com # send reviews here
+
+pixel@google.com
+juliacr@google.com
+cinek@google.com
+alexflo@google.com
+dsandler@android.com
+adamcohen@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
index fc5b9f4..ae05c46 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
@@ -16,34 +16,83 @@
 
 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 {
     /** Returns true if there is a server-side setting stored. */
-    fun hasOverride(flagId: Int): Boolean
+    fun hasOverride(namespace: String, name: String): Boolean
 
     /** Returns any stored server-side setting or the default if not set. */
-    fun readServerOverride(flagId: Int, default: Boolean): Boolean
+    fun readServerOverride(namespace: String, name: String, 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 {
-    override fun hasOverride(flagId: Int): Boolean =
-        deviceConfig.getProperty(
-            SYSUI_NAMESPACE,
-            getServerOverrideName(flagId)
+
+    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(namespace: String, name: String): Boolean =
+        !namespace.isBlank() && !name.isBlank() && deviceConfig.getProperty(
+            namespace,
+            name
         ) != null
 
-    override fun readServerOverride(flagId: Int, default: Boolean): Boolean {
-        return deviceConfig.getBoolean(
-            SYSUI_NAMESPACE,
-            getServerOverrideName(flagId),
+
+    override fun readServerOverride(namespace: String, name: String, default: Boolean): Boolean =
+        !namespace.isBlank() && !name.isBlank() && deviceConfig.getBoolean(
+            namespace,
+            name,
             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 {
@@ -51,30 +100,58 @@
     }
 }
 
-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 flagMap: MutableMap<String, Boolean> = mutableMapOf()
+    private val listeners =
+        mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>()
 
-    override fun hasOverride(flagId: Int): Boolean {
-        return flagMap.containsKey(flagId)
+    override fun hasOverride(namespace: String, name: String): Boolean {
+        return flagMap.containsKey(name)
     }
 
-    override fun readServerOverride(flagId: Int, default: Boolean): Boolean {
-        return flagMap.getOrDefault(flagId, default)
+    override fun readServerOverride(namespace: String, name: String, default: Boolean): Boolean {
+        return flagMap.getOrDefault(name, default)
     }
 
-    fun setFlagValue(flagId: Int, value: Boolean) {
-        flagMap.put(flagId, value)
+    fun setFlagValue(namespace: String, name: String, value: Boolean) {
+        flagMap.put(name, value)
+
+        for ((listener, flags) in listeners) {
+            flagLoop@ for (flag in flags) {
+                if (name == flag.name) {
+                    listener.onChange()
+                    break@flagLoop
+                }
+            }
+        }
     }
 
-    fun eraseFlag(flagId: Int) {
-        flagMap.remove(flagId)
+    fun eraseFlag(namespace: String, name: String) {
+        flagMap.remove(name)
+    }
+
+    override fun listenForChanges(
+        flags: Collection<Flag<*>>,
+        listener: ServerFlagReader.ChangeListener
+    ) {
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
new file mode 100644
index 0000000..f1b1be4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemExitRestarter.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 javax.inject.Inject
+
+class SystemExitRestarter @Inject constructor() : Restarter {
+    override fun restart() {
+        System.exit(0)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
index 18fb423..d9bcb50 100644
--- a/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
+++ b/packages/SystemUI/src/com/android/systemui/fragments/ExtensionFragmentListener.java
@@ -50,13 +50,12 @@
 
     @Override
     public void accept(T extension) {
-        try {
-            Fragment.class.cast(extension);
+        if (Fragment.class.isInstance(extension)) {
             mFragmentHostManager.getExtensionManager().setCurrentExtension(mId, mTag,
                     mOldClass, extension.getClass().getName(), mExtension.getContext());
             mOldClass = extension.getClass().getName();
-        } catch (ClassCastException e) {
-            Log.e(TAG, extension.getClass().getName() + " must be a Fragment", e);
+        } else {
+            Log.e(TAG, extension.getClass().getName() + " must be a Fragment");
         }
         mExtension.clearItem(true);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index da5819a..db2cd91 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -116,6 +116,7 @@
 import com.android.systemui.MultiListLayout.MultiListAdapter;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -124,6 +125,7 @@
 import com.android.systemui.plugins.GlobalActions.GlobalActionsManager;
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
 import com.android.systemui.scrim.ScrimDrawable;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -200,6 +202,7 @@
     protected final SecureSettings mSecureSettings;
     protected final Resources mResources;
     private final ConfigurationController mConfigurationController;
+    private final UserTracker mUserTracker;
     private final UserManager mUserManager;
     private final TrustManager mTrustManager;
     private final IActivityManager mIActivityManager;
@@ -338,6 +341,7 @@
             @NonNull VibratorHelper vibrator,
             @Main Resources resources,
             ConfigurationController configurationController,
+            UserTracker userTracker,
             KeyguardStateController keyguardStateController,
             UserManager userManager,
             TrustManager trustManager,
@@ -369,6 +373,7 @@
         mSecureSettings = secureSettings;
         mResources = resources;
         mConfigurationController = configurationController;
+        mUserTracker = userTracker;
         mUserManager = userManager;
         mTrustManager = trustManager;
         mIActivityManager = iActivityManager;
@@ -448,10 +453,11 @@
      *
      * @param keyguardShowing     True if keyguard is showing
      * @param isDeviceProvisioned True if device is provisioned
-     * @param view                The view from which we should animate the dialog when showing it
+     * @param expandable          The expandable from which we should animate the dialog when
+     *                            showing it
      */
     public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
-            @Nullable View view) {
+            @Nullable Expandable expandable) {
         mKeyguardShowing = keyguardShowing;
         mDeviceProvisioned = isDeviceProvisioned;
         if (mDialog != null && mDialog.isShowing()) {
@@ -463,7 +469,7 @@
             mDialog.dismiss();
             mDialog = null;
         } else {
-            handleShow(view);
+            handleShow(expandable);
         }
     }
 
@@ -495,7 +501,7 @@
         }
     }
 
-    protected void handleShow(@Nullable View view) {
+    protected void handleShow(@Nullable Expandable expandable) {
         awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
@@ -507,10 +513,12 @@
         // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports
         mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
 
-        if (view != null) {
-            mDialogLaunchAnimator.showFromView(mDialog, view,
-                    new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG));
+        DialogLaunchAnimator.Controller controller =
+                expandable != null ? expandable.dialogLaunchController(
+                        new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG)) : null;
+        if (controller != null) {
+            mDialogLaunchAnimator.show(mDialog, controller);
         } else {
             mDialog.show();
         }
@@ -1016,8 +1024,9 @@
                             Log.w(TAG, "Bugreport handler could not be launched");
                             mIActivityManager.requestInteractiveBugReport();
                         }
-                        // Close shade so user sees the activity
-                        mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                        // Maybe close shade (depends on a flag) so user sees the activity
+                        mCentralSurfacesOptional.ifPresent(
+                                CentralSurfaces::collapseShadeForBugreport);
                     } catch (RemoteException e) {
                     }
                 }
@@ -1036,8 +1045,8 @@
                 mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL);
                 mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
                 mIActivityManager.requestFullBugReport();
-                // Close shade so user sees the activity
-                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                // Maybe close shade (depends on a flag) so user sees the activity
+                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport);
             } catch (RemoteException e) {
             }
             return false;
@@ -1193,11 +1202,7 @@
     }
 
     protected UserInfo getCurrentUser() {
-        try {
-            return mIActivityManager.getCurrentUser();
-        } catch (RemoteException re) {
-            return null;
-        }
+        return mUserTracker.getUserInfo();
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
index 1f52fc6..9b2e6b8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
@@ -375,7 +375,7 @@
     public static final int INDICATION_TYPE_ALIGNMENT = 4;
     public static final int INDICATION_TYPE_TRANSIENT = 5;
     public static final int INDICATION_TYPE_TRUST = 6;
-    public static final int INDICATION_TYPE_RESTING = 7;
+    public static final int INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE = 7;
     public static final int INDICATION_TYPE_USER_LOCKED = 8;
     public static final int INDICATION_TYPE_REVERSE_CHARGING = 10;
     public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11;
@@ -390,7 +390,7 @@
             INDICATION_TYPE_ALIGNMENT,
             INDICATION_TYPE_TRANSIENT,
             INDICATION_TYPE_TRUST,
-            INDICATION_TYPE_RESTING,
+            INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE,
             INDICATION_TYPE_USER_LOCKED,
             INDICATION_TYPE_REVERSE_CHARGING,
             INDICATION_TYPE_BIOMETRIC_MESSAGE,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
new file mode 100644
index 0000000..1f1ed00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
@@ -0,0 +1,326 @@
+/*
+ * 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
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.systemui.SystemUIAppComponentFactoryBase
+import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import javax.inject.Inject
+
+class KeyguardQuickAffordanceProvider :
+    ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
+
+    @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
+
+    private lateinit var contextAvailableCallback: ContextAvailableCallback
+
+    private val uriMatcher =
+        UriMatcher(UriMatcher.NO_MATCH).apply {
+            addURI(
+                Contract.AUTHORITY,
+                Contract.SlotTable.TABLE_NAME,
+                MATCH_CODE_ALL_SLOTS,
+            )
+            addURI(
+                Contract.AUTHORITY,
+                Contract.AffordanceTable.TABLE_NAME,
+                MATCH_CODE_ALL_AFFORDANCES,
+            )
+            addURI(
+                Contract.AUTHORITY,
+                Contract.SelectionTable.TABLE_NAME,
+                MATCH_CODE_ALL_SELECTIONS,
+            )
+            addURI(
+                Contract.AUTHORITY,
+                Contract.FlagsTable.TABLE_NAME,
+                MATCH_CODE_ALL_FLAGS,
+            )
+        }
+
+    override fun onCreate(): Boolean {
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        contextAvailableCallback.onContextAvailable(checkNotNull(context))
+        super.attachInfo(context, info)
+    }
+
+    override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
+        contextAvailableCallback = callback
+    }
+
+    override fun getType(uri: Uri): String? {
+        val prefix =
+            when (uriMatcher.match(uri)) {
+                MATCH_CODE_ALL_SLOTS,
+                MATCH_CODE_ALL_AFFORDANCES,
+                MATCH_CODE_ALL_FLAGS,
+                MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
+                else -> null
+            }
+
+        val tableName =
+            when (uriMatcher.match(uri)) {
+                MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
+                MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
+                MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
+                MATCH_CODE_ALL_FLAGS -> Contract.FlagsTable.TABLE_NAME
+                else -> null
+            }
+
+        if (prefix == null || tableName == null) {
+            return null
+        }
+
+        return "$prefix${Contract.AUTHORITY}.$tableName"
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
+            throw UnsupportedOperationException()
+        }
+
+        return insertSelection(values)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<out String>?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+        sortOrder: String?,
+    ): Cursor? {
+        return when (uriMatcher.match(uri)) {
+            MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
+            MATCH_CODE_ALL_SLOTS -> querySlots()
+            MATCH_CODE_ALL_SELECTIONS -> querySelections()
+            MATCH_CODE_ALL_FLAGS -> queryFlags()
+            else -> null
+        }
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+    ): Int {
+        Log.e(TAG, "Update is not supported!")
+        return 0
+    }
+
+    override fun delete(
+        uri: Uri,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+    ): Int {
+        if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
+            throw UnsupportedOperationException()
+        }
+
+        return deleteSelection(uri, selectionArgs)
+    }
+
+    private fun insertSelection(values: ContentValues?): Uri? {
+        if (values == null) {
+            throw IllegalArgumentException("Cannot insert selection, no values passed in!")
+        }
+
+        if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) {
+            throw IllegalArgumentException(
+                "Cannot insert selection, " +
+                    "\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!"
+            )
+        }
+
+        if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) {
+            throw IllegalArgumentException(
+                "Cannot insert selection, " +
+                    "\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
+            )
+        }
+
+        val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID)
+        val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID)
+
+        if (slotId.isNullOrEmpty()) {
+            throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
+        }
+
+        if (affordanceId.isNullOrEmpty()) {
+            throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
+        }
+
+        val success =
+            interactor.select(
+                slotId = slotId,
+                affordanceId = affordanceId,
+            )
+
+        return if (success) {
+            Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
+            context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null)
+            Contract.SelectionTable.URI
+        } else {
+            Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
+            null
+        }
+    }
+
+    private fun querySelections(): Cursor {
+        return MatrixCursor(
+                arrayOf(
+                    Contract.SelectionTable.Columns.SLOT_ID,
+                    Contract.SelectionTable.Columns.AFFORDANCE_ID,
+                )
+            )
+            .apply {
+                val affordanceIdsBySlotId = interactor.getSelections()
+                affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
+                    affordanceIds.forEach { affordanceId ->
+                        addRow(
+                            arrayOf(
+                                slotId,
+                                affordanceId,
+                            )
+                        )
+                    }
+                }
+            }
+    }
+
+    private fun queryAffordances(): Cursor {
+        return MatrixCursor(
+                arrayOf(
+                    Contract.AffordanceTable.Columns.ID,
+                    Contract.AffordanceTable.Columns.NAME,
+                    Contract.AffordanceTable.Columns.ICON,
+                )
+            )
+            .apply {
+                interactor.getAffordancePickerRepresentations().forEach { representation ->
+                    addRow(
+                        arrayOf(
+                            representation.id,
+                            representation.name,
+                            representation.iconResourceId,
+                        )
+                    )
+                }
+            }
+    }
+
+    private fun querySlots(): Cursor {
+        return MatrixCursor(
+                arrayOf(
+                    Contract.SlotTable.Columns.ID,
+                    Contract.SlotTable.Columns.CAPACITY,
+                )
+            )
+            .apply {
+                interactor.getSlotPickerRepresentations().forEach { representation ->
+                    addRow(
+                        arrayOf(
+                            representation.id,
+                            representation.maxSelectedAffordances,
+                        )
+                    )
+                }
+            }
+    }
+
+    private fun queryFlags(): Cursor {
+        return MatrixCursor(
+                arrayOf(
+                    Contract.FlagsTable.Columns.NAME,
+                    Contract.FlagsTable.Columns.VALUE,
+                )
+            )
+            .apply {
+                interactor.getPickerFlags().forEach { flag ->
+                    addRow(
+                        arrayOf(
+                            flag.name,
+                            if (flag.value) {
+                                1
+                            } else {
+                                0
+                            },
+                        )
+                    )
+                }
+            }
+    }
+
+    private fun deleteSelection(
+        uri: Uri,
+        selectionArgs: Array<out String>?,
+    ): Int {
+        if (selectionArgs == null) {
+            throw IllegalArgumentException(
+                "Cannot delete selection, selection arguments not included!"
+            )
+        }
+
+        val (slotId, affordanceId) =
+            when (selectionArgs.size) {
+                1 -> Pair(selectionArgs[0], null)
+                2 -> Pair(selectionArgs[0], selectionArgs[1])
+                else ->
+                    throw IllegalArgumentException(
+                        "Cannot delete selection, selection arguments has wrong size, expected to" +
+                            " have 1 or 2 arguments, had ${selectionArgs.size} instead!"
+                    )
+            }
+
+        val deleted =
+            interactor.unselect(
+                slotId = slotId,
+                affordanceId = affordanceId,
+            )
+
+        return if (deleted) {
+            Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
+            context?.contentResolver?.notifyChange(uri, null)
+            1
+        } else {
+            Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
+            0
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardQuickAffordanceProvider"
+        private const val MATCH_CODE_ALL_SLOTS = 1
+        private const val MATCH_CODE_ALL_AFFORDANCES = 2
+        private const val MATCH_CODE_ALL_SELECTIONS = 3
+        private const val MATCH_CODE_ALL_FLAGS = 4
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 1c6cec2..0214313 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -518,7 +518,7 @@
                 @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
             Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
             checkPermission();
-            mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered);
+            mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
             mKeyguardLifecyclesDispatcher.dispatch(
                     KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason);
             Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 5d564f7..bafd2e7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard;
 
 import android.annotation.AnyThread;
-import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -52,6 +51,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SystemUIAppComponentFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -140,6 +140,8 @@
     public KeyguardBypassController mKeyguardBypassController;
     @Inject
     public KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Inject
+    UserTracker mUserTracker;
     private CharSequence mMediaTitle;
     private CharSequence mMediaArtist;
     protected boolean mDozing;
@@ -355,7 +357,7 @@
         synchronized (this) {
             if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) {
                 String pattern = android.text.format.DateFormat.is24HourFormat(getContext(),
-                        ActivityManager.getCurrentUser()) ? "HH:mm" : "h:mm";
+                        mUserTracker.getUserId()) ? "HH:mm" : "h:mm";
                 mNextAlarm = android.text.format.DateFormat.format(pattern,
                         mNextAlarmInfo.getTriggerTime()).toString();
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 84bd8ce..dd222c0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -36,7 +36,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
@@ -91,6 +90,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
@@ -123,7 +123,9 @@
 import com.android.systemui.keyguard.dagger.KeyguardModule;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
@@ -135,6 +137,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -261,10 +264,12 @@
     private AlarmManager mAlarmManager;
     private AudioManager mAudioManager;
     private StatusBarManager mStatusBarManager;
+    private final UserTracker mUserTracker;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final Executor mUiBgExecutor;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
+    private final Lazy<ShadeController> mShadeController;
 
     private boolean mSystemReady;
     private boolean mBootCompleted;
@@ -322,6 +327,12 @@
     // true if the keyguard is hidden by another window
     private boolean mOccluded = false;
 
+    /**
+     * Whether the {@link #mOccludeAnimationController} is currently playing the occlusion
+     * animation.
+     */
+    private boolean mOccludeAnimationPlaying = false;
+
     private boolean mWakeAndUnlocking = false;
 
     /**
@@ -335,12 +346,6 @@
      */
     private int mDelayedProfileShowingSequence;
 
-    /**
-     * If the user has disabled the keyguard, then requests to exit, this is
-     * how we'll ultimately let them know whether it was successful.  We use this
-     * var being non-null as an indicator that there is an in progress request.
-     */
-    private IKeyguardExitCallback mExitSecureCallback;
     private final DismissCallbackRegistry mDismissCallbackRegistry;
 
     // the properties of the keyguard
@@ -401,6 +406,16 @@
     private final float mWindowCornerRadius;
 
     /**
+     * The duration in milliseconds of the dream open animation.
+     */
+    private final int mDreamOpenAnimationDuration;
+
+    /**
+     * The duration in milliseconds of the dream close animation.
+     */
+    private final int mDreamCloseAnimationDuration;
+
+    /**
      * The animation used for hiding keyguard. This is used to fetch the animation timings if
      * WindowManager is not providing us with them.
      */
@@ -707,7 +722,7 @@
 
         @Override
         public void keyguardDone(boolean strongAuth, int targetUserId) {
-            if (targetUserId != ActivityManager.getCurrentUser()) {
+            if (targetUserId != mUserTracker.getUserId()) {
                 return;
             }
             if (DEBUG) Log.d(TAG, "keyguardDone");
@@ -730,7 +745,7 @@
         public void keyguardDonePending(boolean strongAuth, int targetUserId) {
             Trace.beginSection("KeyguardViewMediator.mViewMediatorCallback#keyguardDonePending");
             if (DEBUG) Log.d(TAG, "keyguardDonePending");
-            if (targetUserId != ActivityManager.getCurrentUser()) {
+            if (targetUserId != mUserTracker.getUserId()) {
                 Trace.endSection();
                 return;
             }
@@ -751,6 +766,7 @@
             if (DEBUG) Log.d(TAG, "keyguardGone");
             mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
             mKeyguardDisplayManager.hide();
+            mUpdateMonitor.startBiometricWatchdog();
             Trace.endSection();
         }
 
@@ -831,15 +847,24 @@
     /**
      * Animation launch controller for activities that occlude the keyguard.
      */
-    private final ActivityLaunchAnimator.Controller mOccludeAnimationController =
+    @VisibleForTesting
+    final ActivityLaunchAnimator.Controller mOccludeAnimationController =
             new ActivityLaunchAnimator.Controller() {
                 @Override
-                public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {}
+                public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
+                    mOccludeAnimationPlaying = true;
+                    mScrimControllerLazy.get().setOccludeAnimationPlaying(true);
+                }
 
                 @Override
                 public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) {
                     Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
                             + mOccluded);
+                    mOccludeAnimationPlaying = false;
+
+                    // Ensure keyguard state is set correctly if we're cancelled.
+                    mCentralSurfaces.updateIsKeyguard();
+                    mScrimControllerLazy.get().setOccludeAnimationPlaying(false);
                 }
 
                 @Override
@@ -848,6 +873,13 @@
                         mCentralSurfaces.instantCollapseNotificationPanel();
                     }
 
+                    mOccludeAnimationPlaying = false;
+
+                    // Hide the keyguard now that we're done launching the occluding activity over
+                    // it.
+                    mCentralSurfaces.updateIsKeyguard();
+                    mScrimControllerLazy.get().setOccludeAnimationPlaying(false);
+
                     mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
@@ -946,8 +978,7 @@
                         }
 
                         mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f);
-                        // Use the same duration as for the UNOCCLUDE.
-                        mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION);
+                        mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration);
                         mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR);
                         mOccludeByDreamAnimator.addUpdateListener(
                                 animation -> {
@@ -1034,7 +1065,8 @@
                         }
 
                         mUnoccludeAnimator = ValueAnimator.ofFloat(1f, 0f);
-                        mUnoccludeAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION);
+                        mUnoccludeAnimator.setDuration(isDream ? mDreamCloseAnimationDuration
+                                : UNOCCLUDE_ANIMATION_DURATION);
                         mUnoccludeAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE);
                         mUnoccludeAnimator.addUpdateListener(
                                 animation -> {
@@ -1104,12 +1136,14 @@
     private ScreenOnCoordinator mScreenOnCoordinator;
 
     private Lazy<ActivityLaunchAnimator> mActivityLaunchAnimator;
+    private Lazy<ScrimController> mScrimControllerLazy;
 
     /**
      * Injected constructor. See {@link KeyguardModule}.
      */
     public KeyguardViewMediator(
             Context context,
+            UserTracker userTracker,
             FalsingCollector falsingCollector,
             LockPatternUtils lockPatternUtils,
             BroadcastDispatcher broadcastDispatcher,
@@ -1131,9 +1165,12 @@
             ScreenOnCoordinator screenOnCoordinator,
             InteractionJankMonitor interactionJankMonitor,
             DreamOverlayStateController dreamOverlayStateController,
+            Lazy<ShadeController> shadeControllerLazy,
             Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
-            Lazy<ActivityLaunchAnimator> activityLaunchAnimator) {
+            Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
+            Lazy<ScrimController> scrimControllerLazy) {
         mContext = context;
+        mUserTracker = userTracker;
         mFalsingCollector = falsingCollector;
         mLockPatternUtils = lockPatternUtils;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -1146,6 +1183,7 @@
         mTrustManager = trustManager;
         mUserSwitcherController = userSwitcherController;
         mKeyguardDisplayManager = keyguardDisplayManager;
+        mShadeController = shadeControllerLazy;
         dumpManager.registerDumpable(getClass().getName(), this);
         mDeviceConfig = deviceConfig;
         mScreenOnCoordinator = screenOnCoordinator;
@@ -1175,10 +1213,16 @@
         mDreamOverlayStateController = dreamOverlayStateController;
 
         mActivityLaunchAnimator = activityLaunchAnimator;
+        mScrimControllerLazy = scrimControllerLazy;
 
         mPowerButtonY = context.getResources().getDimensionPixelSize(
                 R.dimen.physical_power_button_center_screen_location_y);
         mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+
+        mDreamOpenAnimationDuration = context.getResources().getInteger(
+                com.android.internal.R.integer.config_dreamOpenAnimationDuration);
+        mDreamCloseAnimationDuration = context.getResources().getInteger(
+                com.android.internal.R.integer.config_dreamCloseAnimationDuration);
     }
 
     public void userActivity() {
@@ -1208,7 +1252,7 @@
 
         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
 
-        KeyguardUpdateMonitor.setCurrentUser(ActivityManager.getCurrentUser());
+        KeyguardUpdateMonitor.setCurrentUser(mUserTracker.getUserId());
 
         // Assume keyguard is showing (unless it's disabled) until we know for sure, unless Keyguard
         // is disabled.
@@ -1314,18 +1358,7 @@
                             || !mLockPatternUtils.isSecure(currentUser);
             long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser());
             mLockLater = false;
-            if (mExitSecureCallback != null) {
-                if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled");
-                try {
-                    mExitSecureCallback.onKeyguardExitResult(false);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                }
-                mExitSecureCallback = null;
-                if (!mExternallyEnabled) {
-                    hideLocked();
-                }
-            } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
+            if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
                 // If we are going to sleep but the keyguard is showing (and will continue to be
                 // showing, not in the process of going away) then reset its state. Otherwise, let
                 // this fall through and explicitly re-lock the keyguard.
@@ -1567,7 +1600,8 @@
     /**
      * It will let us know when the device is waking up.
      */
-    public void onStartedWakingUp(boolean cameraGestureTriggered) {
+    public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason,
+            boolean cameraGestureTriggered) {
         Trace.beginSection("KeyguardViewMediator#onStartedWakingUp");
 
         // TODO: Rename all screen off/on references to interactive/sleeping
@@ -1582,7 +1616,7 @@
             if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence);
             notifyStartedWakingUp();
         }
-        mUpdateMonitor.dispatchStartedWakingUp();
+        mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason);
         maybeSendUserPresentBroadcast();
         Trace.endSection();
     }
@@ -1644,13 +1678,6 @@
             mExternallyEnabled = enabled;
 
             if (!enabled && mShowing) {
-                if (mExitSecureCallback != null) {
-                    if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring");
-                    // we're in the process of handling a request to verify the user
-                    // can get past the keyguard. ignore extraneous requests to disable / re-enable
-                    return;
-                }
-
                 // hiding keyguard that is showing, remember to reshow later
                 if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, "
                         + "disabling status bar expansion");
@@ -1664,33 +1691,23 @@
                 mNeedToReshowWhenReenabled = false;
                 updateInputRestrictedLocked();
 
-                if (mExitSecureCallback != null) {
-                    if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting");
-                    try {
-                        mExitSecureCallback.onKeyguardExitResult(false);
-                    } catch (RemoteException e) {
-                        Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                    }
-                    mExitSecureCallback = null;
-                    resetStateLocked();
-                } else {
-                    showLocked(null);
+                showLocked(null);
 
-                    // block until we know the keyguard is done drawing (and post a message
-                    // to unblock us after a timeout, so we don't risk blocking too long
-                    // and causing an ANR).
-                    mWaitingUntilKeyguardVisible = true;
-                    mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
-                    if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
-                    while (mWaitingUntilKeyguardVisible) {
-                        try {
-                            wait();
-                        } catch (InterruptedException e) {
-                            Thread.currentThread().interrupt();
-                        }
+                // block until we know the keyguard is done drawing (and post a message
+                // to unblock us after a timeout, so we don't risk blocking too long
+                // and causing an ANR).
+                mWaitingUntilKeyguardVisible = true;
+                mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING,
+                        KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
+                if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
+                while (mWaitingUntilKeyguardVisible) {
+                    try {
+                        wait();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
                     }
-                    if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
                 }
+                if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
             }
         }
     }
@@ -1720,13 +1737,6 @@
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
                 }
-            } else if (mExitSecureCallback != null) {
-                // already in progress with someone else
-                try {
-                    callback.onKeyguardExitResult(false);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                }
             } else if (!isSecure()) {
 
                 // Keyguard is not secure, no need to do anything, and we don't need to reshow
@@ -1737,7 +1747,7 @@
                 try {
                     callback.onKeyguardExitResult(true);
                 } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
+                    Slog.w(TAG, "Failed to call onKeyguardExitResult(true)", e);
                 }
             } else {
 
@@ -1760,6 +1770,10 @@
         return mShowing && !mOccluded;
     }
 
+    public boolean isOccludeAnimationPlaying() {
+        return mOccludeAnimationPlaying;
+    }
+
     /**
      * Notify us when the keyguard is occluded by another window
      */
@@ -2257,21 +2271,6 @@
             return;
         }
         setPendingLock(false); // user may have authenticated during the screen off animation
-        if (mExitSecureCallback != null) {
-            try {
-                mExitSecureCallback.onKeyguardExitResult(true /* authenciated */);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to call onKeyguardExitResult()", e);
-            }
-
-            mExitSecureCallback = null;
-
-            // after successfully exiting securely, no need to reshow
-            // the keyguard when they've released the lock
-            mExternallyEnabled = true;
-            mNeedToReshowWhenReenabled = false;
-            updateInputRestricted();
-        }
 
         handleHide();
         mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser);
@@ -3079,7 +3078,6 @@
         pw.print("  mInputRestricted: "); pw.println(mInputRestricted);
         pw.print("  mOccluded: "); pw.println(mOccluded);
         pw.print("  mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence);
-        pw.print("  mExitSecureCallback: "); pw.println(mExitSecureCallback);
         pw.print("  mDeviceInteractive: "); pw.println(mDeviceInteractive);
         pw.print("  mGoingToSleep: "); pw.println(mGoingToSleep);
         pw.print("  mHiding: "); pw.println(mHiding);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
index 546a409..450fa14 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
@@ -33,6 +33,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.widget.ImageView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
@@ -61,6 +63,7 @@
     private UserManager mUserManager;
     private PackageManager mPackageManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
     @Inject
     public WorkLockActivity(BroadcastDispatcher broadcastDispatcher, UserManager userManager,
@@ -95,6 +98,10 @@
         if (badgedIcon != null) {
             ((ImageView) findViewById(R.id.icon)).setImageDrawable(badgedIcon);
         }
+
+        getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                mBackCallback);
     }
 
     @VisibleForTesting
@@ -134,11 +141,16 @@
     @Override
     public void onDestroy() {
         unregisterBroadcastReceiver();
+        getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback);
         super.onDestroy();
     }
 
     @Override
     public void onBackPressed() {
+        onBackInvoked();
+    }
+
+    private void onBackInvoked() {
         // Ignore back presses.
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 56f1ac4..47ef0fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,14 +42,19 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
+import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.util.DeviceConfigProxy;
@@ -70,8 +75,10 @@
         KeyguardUserSwitcherComponent.class},
         includes = {
             FalsingModule.class,
+            KeyguardDataQuickAffordanceModule.class,
             KeyguardQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
+            StartKeyguardTransitionModule.class,
         })
 public class KeyguardModule {
     /**
@@ -81,6 +88,7 @@
     @SysUISingleton
     public static KeyguardViewMediator newKeyguardViewMediator(
             Context context,
+            UserTracker userTracker,
             FalsingCollector falsingCollector,
             LockPatternUtils lockPatternUtils,
             BroadcastDispatcher broadcastDispatcher,
@@ -104,10 +112,13 @@
             ScreenOnCoordinator screenOnCoordinator,
             InteractionJankMonitor interactionJankMonitor,
             DreamOverlayStateController dreamOverlayStateController,
+            Lazy<ShadeController> shadeController,
             Lazy<NotificationShadeWindowController> notificationShadeWindowController,
-            Lazy<ActivityLaunchAnimator> activityLaunchAnimator) {
+            Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
+            Lazy<ScrimController> scrimControllerLazy) {
         return new KeyguardViewMediator(
                 context,
+                userTracker,
                 falsingCollector,
                 lockPatternUtils,
                 broadcastDispatcher,
@@ -131,8 +142,10 @@
                 screenOnCoordinator,
                 interactionJankMonitor,
                 dreamOverlayStateController,
+                shadeController,
                 notificationShadeWindowController,
-                activityLaunchAnimator);
+                activityLaunchAnimator,
+                scrimControllerLazy);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
index 99ae85d..80c6130 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
@@ -18,6 +18,7 @@
 
 import android.view.KeyEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
 import java.lang.ref.WeakReference
 import javax.inject.Inject
 
@@ -45,4 +46,9 @@
     fun dispatchBackKeyEventPreIme(): Boolean
     fun showNextSecurityScreenOrFinish(): Boolean
     fun resume()
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?,
+    )
+    fun willDismissWithActions(): Boolean
 }
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..f5220b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 CAMERA = "camera"
+    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/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
new file mode 100644
index 0000000..3c09aab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt
@@ -0,0 +1,62 @@
+/*
+ *  Copyright (C) 2022 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.app.StatusBarManager
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.camera.CameraGestureHelper
+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.dagger.qualifiers.Application
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import javax.inject.Inject
+
+@SysUISingleton
+class CameraQuickAffordanceConfig @Inject constructor(
+        @Application private val context: Context,
+        private val cameraGestureHelper: CameraGestureHelper,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String
+        get() = BuiltInKeyguardQuickAffordanceKeys.CAMERA
+
+    override val pickerName: String
+        get() = context.getString(R.string.accessibility_camera_button)
+
+    override val pickerIconResourceId: Int
+        get() = com.android.internal.R.drawable.perm_group_camera
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
+        get() = flowOf(
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                    icon = Icon.Resource(
+                            com.android.internal.R.drawable.perm_group_camera,
+                            ContentDescription.Resource(R.string.accessibility_camera_button)
+                    )
+            )
+        )
+
+    override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d6f521c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.Context
+import android.content.Intent
+import androidx.annotation.DrawableRes
+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.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.ControlsActivity
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+
+/** Home controls quick affordance data source. */
+@SysUISingleton
+class HomeControlsKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    @Application context: Context,
+    private val component: ControlsComponent,
+) : KeyguardQuickAffordanceConfig {
+
+    private val appContext = context.applicationContext
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+
+    override val pickerName: String by lazy { context.getString(component.getTileTitleId()) }
+
+    override val pickerIconResourceId: Int by lazy { component.getTileImageId() }
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
+            if (canShowWhileLocked) {
+                stateInternal(component.getControlsListingController().getOrNull())
+            } else {
+                flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+            }
+        }
+
+    override fun onTriggered(
+        expandable: Expandable?,
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
+            intent =
+                Intent(appContext, ControlsActivity::class.java)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .putExtra(
+                        ControlsUiController.EXTRA_ANIMATE,
+                        true,
+                    ),
+            canShowWhileLocked = component.canShowWhileLockedSetting.value,
+        )
+    }
+
+    private fun stateInternal(
+        listingController: ControlsListingController?,
+    ): Flow<KeyguardQuickAffordanceConfig.LockScreenState> {
+        if (listingController == null) {
+            return flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+        }
+
+        return conflatedCallbackFlow {
+            val callback =
+                object : ControlsListingController.ControlsListingCallback {
+                    override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                        val favorites: List<StructureInfo>? =
+                            component.getControlsController().getOrNull()?.getFavorites()
+
+                        trySendWithFailureLogging(
+                            state(
+                                isFeatureEnabled = component.isEnabled(),
+                                hasFavorites = favorites?.isNotEmpty() == true,
+                                hasServiceInfos = serviceInfos.isNotEmpty(),
+                                iconResourceId = component.getTileImageId(),
+                                visibility = component.getVisibility(),
+                            ),
+                            TAG,
+                        )
+                    }
+                }
+
+            listingController.addCallback(callback)
+
+            awaitClose { listingController.removeCallback(callback) }
+        }
+    }
+
+    private fun state(
+        isFeatureEnabled: Boolean,
+        hasFavorites: Boolean,
+        hasServiceInfos: Boolean,
+        visibility: ControlsComponent.Visibility,
+        @DrawableRes iconResourceId: Int?,
+    ): KeyguardQuickAffordanceConfig.LockScreenState {
+        return if (
+            isFeatureEnabled &&
+                hasFavorites &&
+                hasServiceInfos &&
+                iconResourceId != null &&
+                visibility == ControlsComponent.Visibility.AVAILABLE
+        ) {
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                icon =
+                    Icon.Resource(
+                        res = iconResourceId,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = component.getTileTitleId(),
+                            ),
+                    ),
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
new file mode 100644
index 0000000..f7225a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -0,0 +1,41 @@
+/*
+ * 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 dagger.Module
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+
+@Module
+object KeyguardDataQuickAffordanceModule {
+    @Provides
+    @ElementsIntoSet
+    fun quickAffordanceConfigs(
+        home: HomeControlsKeyguardQuickAffordanceConfig,
+        quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
+        qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
+        camera: CameraQuickAffordanceConfig,
+    ): Set<KeyguardQuickAffordanceConfig> {
+        return setOf(
+            camera,
+            home,
+            quickAccessWallet,
+            qrCodeScanner,
+        )
+    }
+}
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..fd40d1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,86 @@
+/*
+ * 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
+
+    val pickerName: String
+
+    val pickerIconResourceId: Int
+
+    /**
+     * 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/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
new file mode 100644
index 0000000..766096f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
@@ -0,0 +1,214 @@
+/*
+ * 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.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer.Companion.BINDINGS
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Keeps quick affordance selections and legacy user settings in sync.
+ *
+ * "Legacy user settings" are user settings like: Settings > Display > Lock screen > "Show device
+ * controls" Settings > Display > Lock screen > "Show wallet"
+ *
+ * Quick affordance selections are the ones available through the new custom lock screen experience
+ * from Settings > Wallpaper & Style.
+ *
+ * This class keeps these in sync, mostly for backwards compatibility purposes and in order to not
+ * "forget" an existing legacy user setting when the device gets updated with a version of System UI
+ * that has the new customizable lock screen feature.
+ *
+ * The way it works is that, when [startSyncing] is called, the syncer starts coroutines to listen
+ * for changes in both legacy user settings and their respective affordance selections. Whenever one
+ * of each pair is changed, the other member of that pair is also updated to match. For example, if
+ * the user turns on "Show device controls", we automatically select the home controls affordance
+ * for the preferred slot. Conversely, when the home controls affordance is unselected by the user,
+ * we set the "Show device controls" setting to "off".
+ *
+ * The class can be configured by updating its list of triplets in the code under [BINDINGS].
+ */
+@SysUISingleton
+class KeyguardQuickAffordanceLegacySettingSyncer
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val secureSettings: SecureSettings,
+    private val selectionsManager: KeyguardQuickAffordanceSelectionManager,
+) {
+    companion object {
+        private val BINDINGS =
+            listOf(
+                Binding(
+                    settingsKey = Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                    slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                    affordanceId = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
+                ),
+                Binding(
+                    settingsKey = Settings.Secure.LOCKSCREEN_SHOW_WALLET,
+                    slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                    affordanceId = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET,
+                ),
+                Binding(
+                    settingsKey = Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER,
+                    slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                    affordanceId = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER,
+                ),
+            )
+    }
+
+    fun startSyncing(
+        bindings: List<Binding> = BINDINGS,
+    ): Job {
+        return scope.launch { bindings.forEach { binding -> startSyncing(this, binding) } }
+    }
+
+    private fun startSyncing(
+        scope: CoroutineScope,
+        binding: Binding,
+    ) {
+        secureSettings
+            .observerFlow(
+                names = arrayOf(binding.settingsKey),
+                userId = UserHandle.USER_ALL,
+            )
+            .map {
+                isSet(
+                    settingsKey = binding.settingsKey,
+                )
+            }
+            .distinctUntilChanged()
+            .onEach { isSet ->
+                if (isSelected(binding.affordanceId) != isSet) {
+                    if (isSet) {
+                        select(
+                            slotId = binding.slotId,
+                            affordanceId = binding.affordanceId,
+                        )
+                    } else {
+                        unselect(
+                            affordanceId = binding.affordanceId,
+                        )
+                    }
+                }
+            }
+            .flowOn(backgroundDispatcher)
+            .launchIn(scope)
+
+        selectionsManager.selections
+            .map { it.values.flatten().toSet() }
+            .map { it.contains(binding.affordanceId) }
+            .distinctUntilChanged()
+            .onEach { isSelected ->
+                if (isSet(binding.settingsKey) != isSelected) {
+                    set(binding.settingsKey, isSelected)
+                }
+            }
+            .flowOn(backgroundDispatcher)
+            .launchIn(scope)
+    }
+
+    private fun isSelected(
+        affordanceId: String,
+    ): Boolean {
+        return selectionsManager
+            .getSelections() // Map<String, List<String>>
+            .values // Collection<List<String>>
+            .flatten() // List<String>
+            .toSet() // Set<String>
+            .contains(affordanceId)
+    }
+
+    private fun select(
+        slotId: String,
+        affordanceId: String,
+    ) {
+        val affordanceIdsAtSlotId = selectionsManager.getSelections()[slotId] ?: emptyList()
+        selectionsManager.setSelections(
+            slotId = slotId,
+            affordanceIds = affordanceIdsAtSlotId + listOf(affordanceId),
+        )
+    }
+
+    private fun unselect(
+        affordanceId: String,
+    ) {
+        val currentSelections = selectionsManager.getSelections()
+        val slotIdsContainingAffordanceId =
+            currentSelections
+                .filter { (_, affordanceIds) -> affordanceIds.contains(affordanceId) }
+                .map { (slotId, _) -> slotId }
+
+        slotIdsContainingAffordanceId.forEach { slotId ->
+            val currentAffordanceIds = currentSelections[slotId] ?: emptyList()
+            val affordanceIdsAfterUnselecting =
+                currentAffordanceIds.toMutableList().apply { remove(affordanceId) }
+
+            selectionsManager.setSelections(
+                slotId = slotId,
+                affordanceIds = affordanceIdsAfterUnselecting,
+            )
+        }
+    }
+
+    private fun isSet(
+        settingsKey: String,
+    ): Boolean {
+        return secureSettings.getIntForUser(
+            settingsKey,
+            0,
+            UserHandle.USER_CURRENT,
+        ) != 0
+    }
+
+    private suspend fun set(
+        settingsKey: String,
+        isSet: Boolean,
+    ) {
+        withContext(backgroundDispatcher) {
+            secureSettings.putInt(
+                settingsKey,
+                if (isSet) 1 else 0,
+            )
+        }
+    }
+
+    data class Binding(
+        val settingsKey: String,
+        val slotId: String,
+        val affordanceId: String,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
new file mode 100644
index 0000000..b29cf45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.Context
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.R
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+
+/**
+ * Manages and provides access to the current "selections" of keyguard quick affordances, answering
+ * the question "which affordances should the keyguard show?".
+ */
+@SysUISingleton
+class KeyguardQuickAffordanceSelectionManager
+@Inject
+constructor(
+    @Application context: Context,
+    private val userFileManager: UserFileManager,
+    private val userTracker: UserTracker,
+) {
+
+    private val sharedPrefs: SharedPreferences
+        get() =
+            userFileManager.getSharedPreferences(
+                FILE_NAME,
+                Context.MODE_PRIVATE,
+                userTracker.userId,
+            )
+
+    private val userId: Flow<Int> = conflatedCallbackFlow {
+        val callback =
+            object : UserTracker.Callback {
+                override fun onUserChanged(newUser: Int, userContext: Context) {
+                    trySendWithFailureLogging(newUser, TAG)
+                }
+            }
+
+        userTracker.addCallback(callback) { it.run() }
+        trySendWithFailureLogging(userTracker.userId, TAG)
+
+        awaitClose { userTracker.removeCallback(callback) }
+    }
+    private val defaults: Map<String, List<String>> by lazy {
+        context.resources
+            .getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
+            .associate { item ->
+                val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER)
+                check(splitUp.size == 2)
+                val slotId = splitUp[0]
+                val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER)
+                slotId to affordanceIds
+            }
+    }
+
+    /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
+    val selections: Flow<Map<String, List<String>>> =
+        userId.flatMapLatest {
+            conflatedCallbackFlow {
+                val listener =
+                    SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
+                        trySend(getSelections())
+                    }
+
+                sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
+                send(getSelections())
+
+                awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
+            }
+        }
+
+    /**
+     * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
+     * descending priority order.
+     */
+    fun getSelections(): Map<String, List<String>> {
+        val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
+        val result =
+            slotKeys
+                .associate { key ->
+                    val slotId = key.substring(KEY_PREFIX_SLOT.length)
+                    val value = sharedPrefs.getString(key, null)
+                    val affordanceIds =
+                        if (!value.isNullOrEmpty()) {
+                            value.split(AFFORDANCE_DELIMITER)
+                        } else {
+                            emptyList()
+                        }
+                    slotId to affordanceIds
+                }
+                .toMutableMap()
+
+        // If the result map is missing keys, it means that the system has never set anything for
+        // those slots. This is where we need examine our defaults and see if there should be a
+        // default value for the affordances in the slot IDs that are missing from the result.
+        //
+        // Once the user makes any selection for a slot, even when they select "None", this class
+        // will persist a key for that slot ID. In the case of "None", it will have a value of the
+        // empty string. This is why this system works.
+        defaults.forEach { (slotId, affordanceIds) ->
+            if (!result.containsKey(slotId)) {
+                result[slotId] = affordanceIds
+            }
+        }
+
+        return result
+    }
+
+    /**
+     * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
+     * IDs should be descending priority order.
+     */
+    fun setSelections(
+        slotId: String,
+        affordanceIds: List<String>,
+    ) {
+        val key = "$KEY_PREFIX_SLOT$slotId"
+        val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
+        sharedPrefs.edit().putString(key, value).apply()
+    }
+
+    companion object {
+        private const val TAG = "KeyguardQuickAffordanceSelectionManager"
+        @VisibleForTesting const val FILE_NAME = "quick_affordance_selections"
+        private const val KEY_PREFIX_SLOT = "slot_"
+        private const val SLOT_AFFORDANCES_DELIMITER = ":"
+        private const val AFFORDANCE_DELIMITER = ","
+    }
+}
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..11f72ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.Context
+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.dagger.qualifiers.Application
+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(
+    @Application context: Context,
+    private val controller: QRCodeScannerController,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+
+    override val pickerName = context.getString(R.string.qr_code_scanner_title)
+
+    override val pickerIconResourceId = R.drawable.ic_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..303e6a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import android.content.Context
+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.dagger.qualifiers.Application
+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(
+    @Application context: Context,
+    private val walletController: QuickAccessWalletController,
+    private val activityStarter: ActivityStarter,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+
+    override val pickerName = context.getString(R.string.accessibility_wallet_button)
+
+    override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen
+
+    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/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 543389e..9a90fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -16,20 +16,20 @@
 
 package com.android.systemui.keyguard.data.repository
 
-import android.hardware.biometrics.BiometricSourceType
 import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
+import com.android.systemui.statusbar.phone.KeyguardBouncer
 import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-/** Encapsulates app state for the lock screen bouncer. */
+/** Encapsulates app state for the lock screen primary and alternate bouncer. */
 @SysUISingleton
 class KeyguardBouncerRepository
 @Inject
@@ -37,95 +37,83 @@
     private val viewMediatorCallback: ViewMediatorCallback,
     keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) {
-    var bouncerPromptReason: Int? = null
-    /** Determines if we want to instantaneously show the bouncer instead of translating. */
-    private val _isScrimmed = MutableStateFlow(false)
-    val isScrimmed = _isScrimmed.asStateFlow()
-    /** Set amount of how much of the bouncer is showing on the screen */
-    private val _expansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
-    val expansionAmount = _expansionAmount.asStateFlow()
-    private val _isVisible = MutableStateFlow(false)
-    val isVisible = _isVisible.asStateFlow()
-    private val _show = MutableStateFlow<KeyguardBouncerModel?>(null)
-    val show = _show.asStateFlow()
-    private val _showingSoon = MutableStateFlow(false)
-    val showingSoon = _showingSoon.asStateFlow()
-    private val _hide = MutableStateFlow(false)
-    val hide = _hide.asStateFlow()
-    private val _startingToHide = MutableStateFlow(false)
-    val startingToHide = _startingToHide.asStateFlow()
-    private val _onDismissAction = MutableStateFlow<BouncerCallbackActionsModel?>(null)
-    val onDismissAction = _onDismissAction.asStateFlow()
-    private val _disappearAnimation = MutableStateFlow<Runnable?>(null)
-    val startingDisappearAnimation = _disappearAnimation.asStateFlow()
+    /** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
+    private val _primaryBouncerVisible = MutableStateFlow(false)
+    val primaryBouncerVisible = _primaryBouncerVisible.asStateFlow()
+    private val _primaryBouncerShow = MutableStateFlow<KeyguardBouncerModel?>(null)
+    val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
+    private val _primaryBouncerShowingSoon = MutableStateFlow(false)
+    val primaryBouncerShowingSoon = _primaryBouncerShowingSoon.asStateFlow()
+    private val _primaryBouncerHide = MutableStateFlow(false)
+    val primaryBouncerHide = _primaryBouncerHide.asStateFlow()
+    private val _primaryBouncerStartingToHide = MutableStateFlow(false)
+    val primaryBouncerStartingToHide = _primaryBouncerStartingToHide.asStateFlow()
+    private val _primaryBouncerDisappearAnimation = MutableStateFlow<Runnable?>(null)
+    val primaryBouncerStartingDisappearAnimation = _primaryBouncerDisappearAnimation.asStateFlow()
+    /** Determines if we want to instantaneously show the primary bouncer instead of translating. */
+    private val _primaryBouncerScrimmed = MutableStateFlow(false)
+    val primaryBouncerScrimmed = _primaryBouncerScrimmed.asStateFlow()
+    /**
+     * Set how much of the notification panel is showing on the screen.
+     * ```
+     *      0f = panel fully hidden = bouncer fully showing
+     *      1f = panel fully showing = bouncer fully hidden
+     * ```
+     */
+    private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN)
+    val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
     private val _keyguardPosition = MutableStateFlow(0f)
     val keyguardPosition = _keyguardPosition.asStateFlow()
-    private val _resourceUpdateRequests = MutableStateFlow(false)
-    val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
-    private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
-    val showMessage = _showMessage.asStateFlow()
+    private val _onScreenTurnedOff = MutableStateFlow(false)
+    val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+    val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
     private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
     /** Determines if user is already unlocked */
     val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
-    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
-    val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
-    private val _onScreenTurnedOff = MutableStateFlow(false)
-    val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
-
+    private val _showMessage =
+        MutableSharedFlow<BouncerShowMessageModel?>(
+            replay = 1,
+            onBufferOverflow = BufferOverflow.DROP_OLDEST
+        )
+    val showMessage = _showMessage.asSharedFlow()
+    private val _resourceUpdateRequests = MutableStateFlow(false)
+    val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+    val bouncerPromptReason: Int
+        get() = viewMediatorCallback.bouncerPromptReason
     val bouncerErrorMessage: CharSequence?
         get() = viewMediatorCallback.consumeCustomMessage()
 
-    init {
-        val callback =
-            object : KeyguardUpdateMonitorCallback() {
-                override fun onStrongAuthStateChanged(userId: Int) {
-                    bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
-                }
-
-                override fun onLockedOutStateChanged(type: BiometricSourceType) {
-                    if (type == BiometricSourceType.FINGERPRINT) {
-                        bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
-                    }
-                }
-            }
-
-        keyguardUpdateMonitor.registerCallback(callback)
+    fun setPrimaryScrimmed(isScrimmed: Boolean) {
+        _primaryBouncerScrimmed.value = isScrimmed
     }
 
-    fun setScrimmed(isScrimmed: Boolean) {
-        _isScrimmed.value = isScrimmed
+    fun setPrimaryVisible(isVisible: Boolean) {
+        _primaryBouncerVisible.value = isVisible
     }
 
-    fun setExpansion(expansion: Float) {
-        _expansionAmount.value = expansion
+    fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+        _primaryBouncerShow.value = keyguardBouncerModel
     }
 
-    fun setVisible(isVisible: Boolean) {
-        _isVisible.value = isVisible
+    fun setPrimaryShowingSoon(showingSoon: Boolean) {
+        _primaryBouncerShowingSoon.value = showingSoon
     }
 
-    fun setShow(keyguardBouncerModel: KeyguardBouncerModel?) {
-        _show.value = keyguardBouncerModel
+    fun setPrimaryHide(hide: Boolean) {
+        _primaryBouncerHide.value = hide
     }
 
-    fun setShowingSoon(showingSoon: Boolean) {
-        _showingSoon.value = showingSoon
+    fun setPrimaryStartingToHide(startingToHide: Boolean) {
+        _primaryBouncerStartingToHide.value = startingToHide
     }
 
-    fun setHide(hide: Boolean) {
-        _hide.value = hide
+    fun setPrimaryStartDisappearAnimation(runnable: Runnable?) {
+        _primaryBouncerDisappearAnimation.value = runnable
     }
 
-    fun setStartingToHide(startingToHide: Boolean) {
-        _startingToHide.value = startingToHide
-    }
-
-    fun setOnDismissAction(bouncerCallbackActionsModel: BouncerCallbackActionsModel?) {
-        _onDismissAction.value = bouncerCallbackActionsModel
-    }
-
-    fun setStartDisappearAnimation(runnable: Runnable?) {
-        _disappearAnimation.value = runnable
+    fun setPanelExpansion(panelExpansion: Float) {
+        _panelExpansionAmount.value = panelExpansion
     }
 
     fun setKeyguardPosition(keyguardPosition: Float) {
@@ -137,7 +125,7 @@
     }
 
     fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
-        _showMessage.value = bouncerShowMessageModel
+        _showMessage.tryEmit(bouncerShowMessageModel)
     }
 
     fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
new file mode 100644
index 0000000..533b3ab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.repository
+
+import android.content.Context
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Abstracts access to application state related to keyguard quick affordances. */
+@SysUISingleton
+class KeyguardQuickAffordanceRepository
+@Inject
+constructor(
+    @Application private val appContext: Context,
+    @Application private val scope: CoroutineScope,
+    private val selectionManager: KeyguardQuickAffordanceSelectionManager,
+    legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
+    private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
+) {
+    /**
+     * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the
+     * given ID. The configs are sorted in descending priority order.
+     */
+    val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> =
+        selectionManager.selections
+            .map { selectionsBySlotId ->
+                selectionsBySlotId.mapValues { (_, selections) ->
+                    configs.filter { selections.contains(it.key) }
+                }
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.Eagerly,
+                initialValue = emptyMap(),
+            )
+
+    private val _slotPickerRepresentations: List<KeyguardSlotPickerRepresentation> by lazy {
+        fun parseSlot(unparsedSlot: String): Pair<String, Int> {
+            val split = unparsedSlot.split(SLOT_CONFIG_DELIMITER)
+            check(split.size == 2)
+            val slotId = split[0]
+            val slotCapacity = split[1].toInt()
+            return slotId to slotCapacity
+        }
+
+        val unparsedSlots =
+            appContext.resources.getStringArray(R.array.config_keyguardQuickAffordanceSlots)
+
+        val seenSlotIds = mutableSetOf<String>()
+        unparsedSlots.mapNotNull { unparsedSlot ->
+            val (slotId, slotCapacity) = parseSlot(unparsedSlot)
+            check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" }
+            seenSlotIds.add(slotId)
+            KeyguardSlotPickerRepresentation(
+                id = slotId,
+                maxSelectedAffordances = slotCapacity,
+            )
+        }
+    }
+
+    init {
+        legacySettingSyncer.startSyncing()
+    }
+
+    /**
+     * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
+     * slot with the given ID. The configs are sorted in descending priority order.
+     */
+    fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
+        val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList())
+        return configs.filter { selections.contains(it.key) }
+    }
+
+    /**
+     * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
+     * are sorted in descending priority order.
+     */
+    fun getSelections(): Map<String, List<String>> {
+        return selectionManager.getSelections()
+    }
+
+    /**
+     * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
+     * IDs should be descending priority order.
+     */
+    fun setSelections(
+        slotId: String,
+        affordanceIds: List<String>,
+    ) {
+        selectionManager.setSelections(
+            slotId = slotId,
+            affordanceIds = affordanceIds,
+        )
+    }
+
+    /**
+     * Returns the list of representation objects for all known affordances, regardless of what is
+     * selected. This is useful for building experiences like the picker/selector or user settings
+     * so the user can see everything that can be selected in a menu.
+     */
+    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
+        return configs.map { config ->
+            KeyguardQuickAffordancePickerRepresentation(
+                id = config.key,
+                name = config.pickerName,
+                iconResourceId = config.pickerIconResourceId,
+            )
+        }
+    }
+
+    /**
+     * Returns the list of representation objects for all available slots on the keyguard. This is
+     * useful for building experiences like the picker/selector or user settings so the user can see
+     * each slot and select which affordance(s) is/are installed in each slot on the keyguard.
+     */
+    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
+        return _slotPickerRepresentations
+    }
+
+    companion object {
+        private const val SLOT_CONFIG_DELIMITER = ":"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 45b668e..9d5d8bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -16,12 +16,21 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.doze.DozeHost
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.phone.BiometricUnlockController
+import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
@@ -61,6 +70,12 @@
      */
     val isKeyguardShowing: Flow<Boolean>
 
+    /** Observable for the signal that keyguard is about to go away. */
+    val isKeyguardGoingAway: Flow<Boolean>
+
+    /** Observable for whether the bouncer is showing. */
+    val isBouncerShowing: Flow<Boolean>
+
     /**
      * Observable for whether we are in doze state.
      *
@@ -74,6 +89,14 @@
     val isDozing: Flow<Boolean>
 
     /**
+     * Observable for whether the device is dreaming.
+     *
+     * Dozing/AOD is a specific type of dream, but it is also possible for other non-systemui dreams
+     * to be active, such as screensavers.
+     */
+    val isDreaming: Flow<Boolean>
+
+    /**
      * Observable for the amount of doze we are currently in.
      *
      * While in doze state, this amount can change - driving a cycle of animations designed to avoid
@@ -85,6 +108,15 @@
      */
     val dozeAmount: Flow<Float>
 
+    /** Observable for the [StatusBarState] */
+    val statusBarState: Flow<StatusBarState>
+
+    /** Observable for device wake/sleep state */
+    val wakefulnessState: Flow<WakefulnessModel>
+
+    /** Observable for biometric unlock modes */
+    val biometricUnlockState: Flow<BiometricUnlockModel>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -104,6 +136,11 @@
      * Sets the relative offset of the lock-screen clock from its natural position on the screen.
      */
     fun setClockPosition(x: Int, y: Int)
+
+    /**
+     * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
+     */
+    fun isUdfpsSupported(): Boolean
 }
 
 /** Encapsulates application state for the keyguard. */
@@ -112,8 +149,11 @@
 @Inject
 constructor(
     statusBarStateController: StatusBarStateController,
-    private val keyguardStateController: KeyguardStateController,
     dozeHost: DozeHost,
+    wakefulnessLifecycle: WakefulnessLifecycle,
+    biometricUnlockController: BiometricUnlockController,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
     override val animateBottomAreaDozingTransitions =
@@ -148,6 +188,52 @@
         awaitClose { keyguardStateController.removeCallback(callback) }
     }
 
+    override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardStateController.Callback {
+                override fun onKeyguardGoingAwayChanged() {
+                    trySendWithFailureLogging(
+                        keyguardStateController.isKeyguardGoingAway,
+                        TAG,
+                        "updated isKeyguardGoingAway"
+                    )
+                }
+            }
+
+        keyguardStateController.addCallback(callback)
+        // Adding the callback does not send an initial update.
+        trySendWithFailureLogging(
+            keyguardStateController.isKeyguardGoingAway,
+            TAG,
+            "initial isKeyguardGoingAway"
+        )
+
+        awaitClose { keyguardStateController.removeCallback(callback) }
+    }
+
+    override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardStateController.Callback {
+                override fun onBouncerShowingChanged() {
+                    trySendWithFailureLogging(
+                        keyguardStateController.isBouncerShowing,
+                        TAG,
+                        "updated isBouncerShowing"
+                    )
+                }
+            }
+
+        keyguardStateController.addCallback(callback)
+        // Adding the callback does not send an initial update.
+        trySendWithFailureLogging(
+            keyguardStateController.isBouncerShowing,
+            TAG,
+            "initial isBouncerShowing"
+        )
+
+        awaitClose { keyguardStateController.removeCallback(callback) }
+    }
+
     override val isDozing: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
@@ -167,6 +253,25 @@
             }
             .distinctUntilChanged()
 
+    override val isDreaming: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onDreamingStateChanged(isDreaming: Boolean) {
+                            trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                trySendWithFailureLogging(
+                    keyguardUpdateMonitor.isDreaming,
+                    TAG,
+                    "initial isDreaming",
+                )
+
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+
     override val dozeAmount: Flow<Float> = conflatedCallbackFlow {
         val callback =
             object : StatusBarStateController.StateListener {
@@ -185,6 +290,76 @@
         return keyguardStateController.isShowing
     }
 
+    override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
+        val callback =
+            object : StatusBarStateController.StateListener {
+                override fun onStateChanged(state: Int) {
+                    trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state")
+                }
+            }
+
+        statusBarStateController.addCallback(callback)
+        trySendWithFailureLogging(
+            statusBarStateIntToObject(statusBarStateController.getState()),
+            TAG,
+            "initial state"
+        )
+
+        awaitClose { statusBarStateController.removeCallback(callback) }
+    }
+
+    override val wakefulnessState: Flow<WakefulnessModel> = conflatedCallbackFlow {
+        val callback =
+            object : WakefulnessLifecycle.Observer {
+                override fun onStartedWakingUp() {
+                    trySendWithFailureLogging(
+                        WakefulnessModel.STARTING_TO_WAKE,
+                        TAG,
+                        "Wakefulness: starting to wake"
+                    )
+                }
+                override fun onFinishedWakingUp() {
+                    trySendWithFailureLogging(WakefulnessModel.AWAKE, TAG, "Wakefulness: awake")
+                }
+                override fun onStartedGoingToSleep() {
+                    trySendWithFailureLogging(
+                        WakefulnessModel.STARTING_TO_SLEEP,
+                        TAG,
+                        "Wakefulness: starting to sleep"
+                    )
+                }
+                override fun onFinishedGoingToSleep() {
+                    trySendWithFailureLogging(WakefulnessModel.ASLEEP, TAG, "Wakefulness: asleep")
+                }
+            }
+        wakefulnessLifecycle.addObserver(callback)
+        trySendWithFailureLogging(
+            wakefulnessIntToObject(wakefulnessLifecycle.getWakefulness()),
+            TAG,
+            "initial wakefulness state"
+        )
+
+        awaitClose { wakefulnessLifecycle.removeObserver(callback) }
+    }
+
+    override val biometricUnlockState: Flow<BiometricUnlockModel> = conflatedCallbackFlow {
+        val callback =
+            object : BiometricUnlockController.BiometricModeListener {
+                override fun onModeChanged(@WakeAndUnlockMode mode: Int) {
+                    trySendWithFailureLogging(biometricModeIntToObject(mode), TAG, "biometric mode")
+                }
+            }
+
+        biometricUnlockController.addBiometricModeListener(callback)
+        trySendWithFailureLogging(
+            biometricModeIntToObject(biometricUnlockController.getMode()),
+            TAG,
+            "initial biometric mode"
+        )
+
+        awaitClose { biometricUnlockController.removeBiometricModeListener(callback) }
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
@@ -197,6 +372,41 @@
         _clockPosition.value = Position(x, y)
     }
 
+    override fun isUdfpsSupported(): Boolean = keyguardUpdateMonitor.isUdfpsSupported
+
+    private fun statusBarStateIntToObject(value: Int): StatusBarState {
+        return when (value) {
+            0 -> StatusBarState.SHADE
+            1 -> StatusBarState.KEYGUARD
+            2 -> StatusBarState.SHADE_LOCKED
+            else -> throw IllegalArgumentException("Invalid StatusBarState value: $value")
+        }
+    }
+
+    private fun wakefulnessIntToObject(@Wakefulness value: Int): WakefulnessModel {
+        return when (value) {
+            0 -> WakefulnessModel.ASLEEP
+            1 -> WakefulnessModel.STARTING_TO_WAKE
+            2 -> WakefulnessModel.AWAKE
+            3 -> WakefulnessModel.STARTING_TO_SLEEP
+            else -> throw IllegalArgumentException("Invalid Wakefulness value: $value")
+        }
+    }
+
+    private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockModel {
+        return when (value) {
+            0 -> BiometricUnlockModel.NONE
+            1 -> BiometricUnlockModel.WAKE_AND_UNLOCK
+            2 -> BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
+            3 -> BiometricUnlockModel.SHOW_BOUNCER
+            4 -> BiometricUnlockModel.ONLY_WAKE
+            5 -> BiometricUnlockModel.UNLOCK_COLLAPSING
+            6 -> BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+            7 -> BiometricUnlockModel.DISMISS_BOUNCER
+            else -> throw IllegalArgumentException("Invalid BiometricUnlockModel value: $value")
+        }
+    }
+
     companion object {
         private const val TAG = "KeyguardRepositoryImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index d15d7f2..0c72520 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -22,4 +22,9 @@
 @Module
 interface KeyguardRepositoryModule {
     @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository
+
+    @Binds
+    fun keyguardTransitionRepository(
+        impl: KeyguardTransitionRepositoryImpl
+    ): KeyguardTransitionRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
new file mode 100644
index 0000000..bce7d92
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.repository
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.annotation.FloatRange
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+
+/**
+ * The source of truth for all keyguard transitions.
+ *
+ * While the keyguard component is visible, it can undergo a number of transitions between different
+ * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState].
+ * These UI elements should listen to events emitted by [transitions], to ensure a centrally
+ * coordinated experience.
+ *
+ * To create or modify logic that controls when and how transitions get created, look at
+ * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
+ * this repository.
+ */
+interface KeyguardTransitionRepository {
+    /**
+     * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is
+     * a float between [0, 1] representing progress towards completion. If this is a user driven
+     * transition, that value may not be a monotonic progression, as the user may swipe in any
+     * direction.
+     */
+    val transitions: Flow<TransitionStep>
+
+    /**
+     * Interactors that require information about changes between [KeyguardState]s will call this to
+     * register themselves for flowable [TransitionStep]s when that transition occurs.
+     */
+    fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
+        return transitions.filter { step -> step.from == from && step.to == to }
+    }
+
+    /**
+     * Begin a transition from one state to another. Will not start if another transition is in
+     * progress.
+     */
+    fun startTransition(info: TransitionInfo): UUID?
+
+    /**
+     * Allows manual control of a transition. When calling [startTransition], the consumer must pass
+     * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
+     * further updates.
+     *
+     * When the transition is over, TransitionState.FINISHED must be passed into the [state]
+     * parameter.
+     */
+    fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    )
+}
+
+@SysUISingleton
+class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitionRepository {
+    /*
+     * Each transition between [KeyguardState]s will have an associated Flow.
+     * In order to collect these events, clients should call [transition].
+     */
+    private val _transitions =
+        MutableSharedFlow<TransitionStep>(
+            replay = 2,
+            extraBufferCapacity = 10,
+            onBufferOverflow = BufferOverflow.DROP_OLDEST,
+        )
+    override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
+    private var lastStep: TransitionStep = TransitionStep()
+    private var lastAnimator: ValueAnimator? = null
+
+    /*
+     * When manual control of the transition is requested, a unique [UUID] is used as the handle
+     * to permit calls to [updateTransition]
+     */
+    private var updateTransitionId: UUID? = null
+
+    init {
+        // Seed with transitions signaling a boot into lockscreen state
+        emitTransition(
+            TransitionStep(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+                0f,
+                TransitionState.STARTED,
+            )
+        )
+        emitTransition(
+            TransitionStep(
+                KeyguardState.OFF,
+                KeyguardState.LOCKSCREEN,
+                1f,
+                TransitionState.FINISHED,
+            )
+        )
+    }
+
+    override fun startTransition(info: TransitionInfo): UUID? {
+        if (lastStep.transitionState != TransitionState.FINISHED) {
+            Log.i(TAG, "Transition still active: $lastStep, canceling")
+        }
+
+        val startingValue = 1f - lastStep.value
+        lastAnimator?.cancel()
+        lastAnimator = info.animator
+
+        info.animator?.let { animator ->
+            // An animator was provided, so use it to run the transition
+            animator.setFloatValues(startingValue, 1f)
+            animator.duration = ((1f - startingValue) * animator.duration).toLong()
+            val updateListener =
+                object : AnimatorUpdateListener {
+                    override fun onAnimationUpdate(animation: ValueAnimator) {
+                        emitTransition(
+                            TransitionStep(
+                                info,
+                                (animation.getAnimatedValue() as Float),
+                                TransitionState.RUNNING
+                            )
+                        )
+                    }
+                }
+            val adapter =
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator) {
+                        emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
+                    }
+                    override fun onAnimationCancel(animation: Animator) {
+                        endAnimation(animation, lastStep.value, TransitionState.CANCELED)
+                    }
+                    override fun onAnimationEnd(animation: Animator) {
+                        endAnimation(animation, 1f, TransitionState.FINISHED)
+                    }
+
+                    private fun endAnimation(
+                        animation: Animator,
+                        value: Float,
+                        state: TransitionState
+                    ) {
+                        emitTransition(TransitionStep(info, value, state))
+                        animator.removeListener(this)
+                        animator.removeUpdateListener(updateListener)
+                        lastAnimator = null
+                    }
+                }
+            animator.addListener(adapter)
+            animator.addUpdateListener(updateListener)
+            animator.start()
+            return@startTransition null
+        }
+            ?: run {
+                emitTransition(TransitionStep(info, 0f, TransitionState.STARTED))
+
+                // No animator, so it's manual. Provide a mechanism to callback
+                updateTransitionId = UUID.randomUUID()
+                return@startTransition updateTransitionId
+            }
+    }
+
+    override fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) {
+        if (updateTransitionId != transitionId) {
+            Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+            return
+        }
+
+        if (state == TransitionState.FINISHED) {
+            updateTransitionId = null
+        }
+
+        val nextStep = lastStep.copy(value = value, transitionState = state)
+        emitTransition(nextStep, isManual = true)
+    }
+
+    private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) {
+        trace(nextStep, isManual)
+        val emitted = _transitions.tryEmit(nextStep)
+        if (!emitted) {
+            Log.w(TAG, "Failed to emit next value without suspending")
+        }
+        lastStep = nextStep
+    }
+
+    private fun trace(step: TransitionStep, isManual: Boolean) {
+        if (
+            step.transitionState != TransitionState.STARTED &&
+                step.transitionState != TransitionState.FINISHED
+        ) {
+            return
+        }
+        val traceName =
+            "Transition: ${step.from} -> ${step.to} " +
+                if (isManual) {
+                    "(manual)"
+                } else {
+                    ""
+                }
+        val traceCookie = traceName.hashCode()
+        if (step.transitionState == TransitionState.STARTED) {
+            Trace.beginAsyncSection(traceName, traceCookie)
+        } else if (step.transitionState == TransitionState.FINISHED) {
+            Trace.endAsyncSection(traceName, traceCookie)
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..e5521c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodLockscreenTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("AOD<->LOCKSCREEN") {
+
+    override fun start() {
+        scope.launch {
+            /*
+             * Listening to the startedKeyguardTransitionStep (last started step) allows this code
+             * to interrupt an active transition, as long as they were either going to LOCKSCREEN or
+             * AOD state. One example is when the user presses the power button in the middle of an
+             * active transition.
+             */
+            keyguardInteractor.wakefulnessState
+                .sample(
+                    keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                    { a, b -> Pair(a, b) }
+                )
+                .collect { pair ->
+                    val (wakefulnessState, lastStartedStep) = pair
+                    if (
+                        isSleepingOrStartingToSleep(wakefulnessState) &&
+                            lastStartedStep.to == KeyguardState.LOCKSCREEN
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                KeyguardState.AOD,
+                                getAnimator(),
+                            )
+                        )
+                    } else if (
+                        isWakingOrStartingToWake(wakefulnessState) &&
+                            lastStartedStep.to == KeyguardState.AOD
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.AOD,
+                                KeyguardState.LOCKSCREEN,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
new file mode 100644
index 0000000..7e01db3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodToGoneTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("AOD->GONE") {
+
+    private val wakeAndUnlockModes =
+        setOf(WAKE_AND_UNLOCK, WAKE_AND_UNLOCK_FROM_DREAM, WAKE_AND_UNLOCK_PULSING)
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.biometricUnlockState
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (biometricUnlockState, keyguardState) = pair
+                    if (
+                        keyguardState == KeyguardState.AOD &&
+                            wakeAndUnlockModes.contains(biometricUnlockState)
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.AOD,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt
deleted file mode 100644
index 10c7a37..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt
+++ /dev/null
@@ -1,102 +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.interactor
-
-import android.view.View
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.phone.KeyguardBouncer
-import com.android.systemui.util.ListenerSet
-import javax.inject.Inject
-
-/** Interactor to add and remove callbacks for the bouncer. */
-@SysUISingleton
-class BouncerCallbackInteractor @Inject constructor() {
-    private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>()
-    private var expansionCallbacks = ArrayList<KeyguardBouncer.BouncerExpansionCallback>()
-    /** Add a KeyguardResetCallback. */
-    fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
-        resetCallbacks.addIfAbsent(callback)
-    }
-
-    /** Remove a KeyguardResetCallback. */
-    fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
-        resetCallbacks.remove(callback)
-    }
-
-    /** Adds a callback to listen to bouncer expansion updates. */
-    fun addBouncerExpansionCallback(callback: KeyguardBouncer.BouncerExpansionCallback) {
-        if (!expansionCallbacks.contains(callback)) {
-            expansionCallbacks.add(callback)
-        }
-    }
-
-    /**
-     * Removes a previously added callback. If the callback was never added, this method does
-     * nothing.
-     */
-    fun removeBouncerExpansionCallback(callback: KeyguardBouncer.BouncerExpansionCallback) {
-        expansionCallbacks.remove(callback)
-    }
-
-    /** Propagate fully shown to bouncer expansion callbacks. */
-    fun dispatchFullyShown() {
-        for (callback in expansionCallbacks) {
-            callback.onFullyShown()
-        }
-    }
-
-    /** Propagate starting to hide to bouncer expansion callbacks. */
-    fun dispatchStartingToHide() {
-        for (callback in expansionCallbacks) {
-            callback.onStartingToHide()
-        }
-    }
-
-    /** Propagate starting to show to bouncer expansion callbacks. */
-    fun dispatchStartingToShow() {
-        for (callback in expansionCallbacks) {
-            callback.onStartingToShow()
-        }
-    }
-
-    /** Propagate fully hidden to bouncer expansion callbacks. */
-    fun dispatchFullyHidden() {
-        for (callback in expansionCallbacks) {
-            callback.onFullyHidden()
-        }
-    }
-
-    /** Propagate expansion changes to bouncer expansion callbacks. */
-    fun dispatchExpansionChanged(expansion: Float) {
-        for (callback in expansionCallbacks) {
-            callback.onExpansionChanged(expansion)
-        }
-    }
-    /** Propagate visibility changes to bouncer expansion callbacks. */
-    fun dispatchVisibilityChanged(visibility: Int) {
-        for (callback in expansionCallbacks) {
-            callback.onVisibilityChanged(visibility == View.VISIBLE)
-        }
-    }
-
-    /** Propagate keyguard reset. */
-    fun dispatchReset() {
-        for (callback in resetCallbacks) {
-            callback.onKeyguardReset()
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
deleted file mode 100644
index 7d4db37..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
+++ /dev/null
@@ -1,324 +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.interactor
-
-import android.content.res.ColorStateList
-import android.os.Handler
-import android.os.Trace
-import android.os.UserHandle
-import android.os.UserManager
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.DejankUtils
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.BouncerView
-import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
-import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
-import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.phone.KeyguardBouncer
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-
-/** Encapsulates business logic for interacting with the lock-screen bouncer. */
-@SysUISingleton
-class BouncerInteractor
-@Inject
-constructor(
-    private val repository: KeyguardBouncerRepository,
-    private val bouncerView: BouncerView,
-    @Main private val mainHandler: Handler,
-    private val keyguardStateController: KeyguardStateController,
-    private val keyguardSecurityModel: KeyguardSecurityModel,
-    private val callbackInteractor: BouncerCallbackInteractor,
-    private val falsingCollector: FalsingCollector,
-    private val dismissCallbackRegistry: DismissCallbackRegistry,
-    keyguardBypassController: KeyguardBypassController,
-    keyguardUpdateMonitor: KeyguardUpdateMonitor,
-) {
-    /** Whether we want to wait for face auth. */
-    private val bouncerFaceDelay =
-        keyguardStateController.isFaceAuthEnabled &&
-            !keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
-                KeyguardUpdateMonitor.getCurrentUser()
-            ) &&
-            !needsFullscreenBouncer() &&
-            !keyguardUpdateMonitor.userNeedsStrongAuth() &&
-            !keyguardBypassController.bypassEnabled
-
-    /** Runnable to show the bouncer. */
-    val showRunnable = Runnable {
-        repository.setVisible(true)
-        repository.setShow(
-            KeyguardBouncerModel(
-                promptReason = repository.bouncerPromptReason ?: 0,
-                errorMessage = repository.bouncerErrorMessage,
-                expansionAmount = repository.expansionAmount.value
-            )
-        )
-        repository.setShowingSoon(false)
-    }
-
-    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
-    val screenTurnedOff: Flow<Unit> = repository.onScreenTurnedOff.filter { it }.map {}
-    val show: Flow<KeyguardBouncerModel> = repository.show.filterNotNull()
-    val hide: Flow<Unit> = repository.hide.filter { it }.map {}
-    val startingToHide: Flow<Unit> = repository.startingToHide.filter { it }.map {}
-    val isVisible: Flow<Boolean> = repository.isVisible
-    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
-    val expansionAmount: Flow<Float> = repository.expansionAmount
-    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
-    val startingDisappearAnimation: Flow<Runnable> =
-        repository.startingDisappearAnimation.filterNotNull()
-    val onDismissAction: Flow<BouncerCallbackActionsModel> =
-        repository.onDismissAction.filterNotNull()
-    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
-    val keyguardPosition: Flow<Float> = repository.keyguardPosition
-
-    // TODO(b/243685699): Move isScrimmed logic to data layer.
-    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
-    /** Show the bouncer if necessary and set the relevant states. */
-    @JvmOverloads
-    fun show(isScrimmed: Boolean) {
-        // Reset some states as we show the bouncer.
-        repository.setShowMessage(null)
-        repository.setOnScreenTurnedOff(false)
-        repository.setKeyguardAuthenticated(null)
-        repository.setHide(false)
-        repository.setStartingToHide(false)
-
-        val resumeBouncer =
-            (repository.isVisible.value || repository.showingSoon.value) && needsFullscreenBouncer()
-
-        if (!resumeBouncer && repository.show.value != null) {
-            // If bouncer is visible, the bouncer is already showing.
-            return
-        }
-
-        val keyguardUserId = KeyguardUpdateMonitor.getCurrentUser()
-        if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
-            // In split system user mode, we never unlock system user.
-            return
-        }
-
-        Trace.beginSection("KeyguardBouncer#show")
-        repository.setScrimmed(isScrimmed)
-        if (isScrimmed) {
-            setExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
-        }
-
-        if (resumeBouncer) {
-            bouncerView.delegate?.resume()
-            // Bouncer is showing the next security screen and we just need to prompt a resume.
-            return
-        }
-        if (bouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
-            // Keyguard is done.
-            return
-        }
-
-        repository.setShowingSoon(true)
-        if (bouncerFaceDelay) {
-            mainHandler.postDelayed(showRunnable, 1200L)
-        } else {
-            DejankUtils.postAfterTraversal(showRunnable)
-        }
-        keyguardStateController.notifyBouncerShowing(true)
-        callbackInteractor.dispatchStartingToShow()
-
-        Trace.endSection()
-    }
-
-    /** Sets the correct bouncer states to hide the bouncer. */
-    fun hide() {
-        Trace.beginSection("KeyguardBouncer#hide")
-        if (isFullyShowing()) {
-            SysUiStatsLog.write(
-                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
-                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
-            )
-            dismissCallbackRegistry.notifyDismissCancelled()
-        }
-
-        falsingCollector.onBouncerHidden()
-        keyguardStateController.notifyBouncerShowing(false /* showing */)
-        cancelShowRunnable()
-        repository.setShowingSoon(false)
-        repository.setOnDismissAction(null)
-        repository.setVisible(false)
-        repository.setHide(true)
-        repository.setShow(null)
-        Trace.endSection()
-    }
-
-    /**
-     * Sets the panel expansion which is calculated further upstream. Expansion is from 0f to 1f
-     * where 0f => showing and 1f => hiding
-     */
-    fun setExpansion(expansion: Float) {
-        val oldExpansion = repository.expansionAmount.value
-        val expansionChanged = oldExpansion != expansion
-        if (repository.startingDisappearAnimation.value == null) {
-            repository.setExpansion(expansion)
-        }
-
-        if (
-            expansion == KeyguardBouncer.EXPANSION_VISIBLE &&
-                oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE
-        ) {
-            falsingCollector.onBouncerShown()
-            callbackInteractor.dispatchFullyShown()
-        } else if (
-            expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
-                oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
-        ) {
-            repository.setVisible(false)
-            repository.setShow(null)
-            falsingCollector.onBouncerHidden()
-            DejankUtils.postAfterTraversal { callbackInteractor.dispatchReset() }
-            callbackInteractor.dispatchFullyHidden()
-        } else if (
-            expansion != KeyguardBouncer.EXPANSION_VISIBLE &&
-                oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE
-        ) {
-            callbackInteractor.dispatchStartingToHide()
-            repository.setStartingToHide(true)
-        }
-        if (expansionChanged) {
-            callbackInteractor.dispatchExpansionChanged(expansion)
-        }
-    }
-
-    /** Set the initial keyguard message to show when bouncer is shown. */
-    fun showMessage(message: String?, colorStateList: ColorStateList?) {
-        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
-    }
-
-    /**
-     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
-     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
-     * call cancelAction.
-     */
-    fun setDismissAction(
-        onDismissAction: ActivityStarter.OnDismissAction?,
-        cancelAction: Runnable?
-    ) {
-        repository.setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
-    }
-
-    /** Update the resources of the views. */
-    fun updateResources() {
-        repository.setResourceUpdateRequests(true)
-    }
-
-    /** Tell the bouncer that keyguard is authenticated. */
-    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
-        repository.setKeyguardAuthenticated(strongAuth)
-    }
-
-    /** Tell the bouncer the screen has turned off. */
-    fun onScreenTurnedOff() {
-        repository.setOnScreenTurnedOff(true)
-    }
-
-    /** Update the position of the bouncer when showing. */
-    fun setKeyguardPosition(position: Float) {
-        repository.setKeyguardPosition(position)
-    }
-
-    /** Notifies that the state change was handled. */
-    fun notifyKeyguardAuthenticatedHandled() {
-        repository.setKeyguardAuthenticated(null)
-    }
-
-    /** Notify that view visibility has changed. */
-    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
-        callbackInteractor.dispatchVisibilityChanged(visibility)
-    }
-
-    /** Notify that the resources have been updated */
-    fun notifyUpdatedResources() {
-        repository.setResourceUpdateRequests(false)
-    }
-
-    /** Set whether back button is enabled when on the bouncer screen. */
-    fun setBackButtonEnabled(enabled: Boolean) {
-        repository.setIsBackButtonEnabled(enabled)
-    }
-
-    /** Tell the bouncer to start the pre hide animation. */
-    fun startDisappearAnimation(runnable: Runnable) {
-        val finishRunnable = Runnable {
-            repository.setStartDisappearAnimation(null)
-            runnable.run()
-        }
-        repository.setStartDisappearAnimation(finishRunnable)
-    }
-
-    /** Returns whether bouncer is fully showing. */
-    fun isFullyShowing(): Boolean {
-        return (repository.showingSoon.value || repository.isVisible.value) &&
-            repository.expansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
-            repository.startingDisappearAnimation.value == null
-    }
-
-    /** Returns whether bouncer is scrimmed. */
-    fun isScrimmed(): Boolean {
-        return repository.isScrimmed.value
-    }
-
-    /** If bouncer expansion is between 0f and 1f non-inclusive. */
-    fun isInTransit(): Boolean {
-        return repository.showingSoon.value ||
-            repository.expansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
-                repository.expansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
-    }
-
-    /** Return whether bouncer is animating away. */
-    fun isAnimatingAway(): Boolean {
-        return repository.startingDisappearAnimation.value != null
-    }
-
-    /** Return whether bouncer will dismiss with actions */
-    fun willDismissWithAction(): Boolean {
-        return repository.onDismissAction.value?.onDismissAction != null
-    }
-
-    /** Returns whether the bouncer should be full screen. */
-    private fun needsFullscreenBouncer(): Boolean {
-        val mode: KeyguardSecurityModel.SecurityMode =
-            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
-        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
-            mode == KeyguardSecurityModel.SecurityMode.SimPuk
-    }
-
-    /** Remove the show runnable from the main handler queue to improve performance. */
-    private fun cancelShowRunnable() {
-        DejankUtils.removeCallbacks(showRunnable)
-        mainHandler.removeCallbacks(showRunnable)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
new file mode 100644
index 0000000..dd29673
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class BouncerToGoneTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val shadeRepository: ShadeRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor
+) : TransitionInteractor("BOUNCER->GONE") {
+
+    private var transitionId: UUID? = null
+
+    override fun start() {
+        listenForKeyguardGoingAway()
+    }
+
+    private fun listenForKeyguardGoingAway() {
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isKeyguardGoingAway, keyguardState) = pair
+                    if (isKeyguardGoingAway && keyguardState == KeyguardState.BOUNCER) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.BOUNCER,
+                                to = KeyguardState.GONE,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..c44cda4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingLockscreenTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class DreamingLockscreenTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("DREAMING<->LOCKSCREEN") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.isDreaming
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isDreaming, keyguardState) = pair
+                    if (isDreaming && keyguardState == KeyguardState.LOCKSCREEN) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                KeyguardState.DREAMING,
+                                getAnimator(),
+                            )
+                        )
+                    } else if (!isDreaming && keyguardState == KeyguardState.DREAMING) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DREAMING,
+                                KeyguardState.LOCKSCREEN,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt
new file mode 100644
index 0000000..9e2b724
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isSleepingOrStartingToSleep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class DreamingToAodTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("DREAMING->AOD") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.wakefulnessState
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (wakefulnessState, keyguardState) = pair
+                    if (
+                        isSleepingOrStartingToSleep(wakefulnessState) &&
+                            keyguardState == KeyguardState.DREAMING
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.DREAMING,
+                                KeyguardState.AOD,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
new file mode 100644
index 0000000..0e2a54c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class GoneAodTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("GONE->AOD") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.wakefulnessState
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (wakefulnessState, keyguardState) = pair
+                    if (
+                        keyguardState == KeyguardState.GONE &&
+                            wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.GONE,
+                                KeyguardState.AOD,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
index ede50b0..d2a7486 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt
@@ -48,4 +48,9 @@
     fun setAnimateDozingTransitions(animate: Boolean) {
         repository.setAnimateDozingTransitions(animate)
     }
+
+    /**
+     * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
+     */
+    fun shouldConstrainToTopOfLockIcon(): Boolean = repository.isUdfpsSupported()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 192919e..5a1c702 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,6 +19,9 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 
@@ -38,8 +41,26 @@
     val dozeAmount: Flow<Float> = repository.dozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
-    /** Whether the keyguard is showing ot not. */
+    /**
+     * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
+     * but not vice-versa.
+     */
+    val isDreaming: Flow<Boolean> = repository.isDreaming
+    /** Whether the keyguard is showing or not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+    /** Whether the keyguard is going away. */
+    val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
+    /** Whether the bouncer is showing or not. */
+    val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing
+    /** The device wake/sleep state */
+    val wakefulnessState: Flow<WakefulnessModel> = repository.wakefulnessState
+    /** Observable for the [StatusBarState] */
+    val statusBarState: Flow<StatusBarState> = repository.statusBarState
+    /**
+     * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear,
+     * side, under display) is used to unlock the device.
+     */
+    val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState
 
     fun isKeyguardShowing(): Boolean {
         return repository.isKeyguardShowing()
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..45eb6f5 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
@@ -18,20 +18,32 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Intent
+import android.util.Log
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 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.model.KeyguardPickerFlag
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import dagger.Lazy
 import javax.inject.Inject
-import kotlin.reflect.KClass
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
 @SysUISingleton
@@ -44,7 +56,12 @@
     private val keyguardStateController: KeyguardStateController,
     private val userTracker: UserTracker,
     private val activityStarter: ActivityStarter,
+    private val featureFlags: FeatureFlags,
+    private val repository: Lazy<KeyguardQuickAffordanceRepository>,
 ) {
+    private val isUsingRepository: Boolean
+        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
+
     /** Returns an observable for the quick affordance at the given position. */
     fun quickAffordance(
         position: KeyguardQuickAffordancePosition
@@ -63,48 +80,174 @@
     }
 
     /**
-     * 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 =
+            if (isUsingRepository) {
+                val (slotId, decodedConfigKey) = configKey.decode()
+                repository.get().selections.value[slotId]?.find { it.key == decodedConfigKey }
+            } else {
+                registry.get(configKey)
+            }
+        if (config == null) {
+            Log.e(TAG, "Affordance config with key of \"$configKey\" not found!")
+            return
+        }
+
+        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
+        }
+    }
+
+    /**
+     * Selects an affordance with the given ID on the slot with the given ID.
+     *
+     * @return `true` if the affordance was selected successfully; `false` otherwise.
+     */
+    fun select(slotId: String, affordanceId: String): Boolean {
+        check(isUsingRepository)
+
+        val slots = repository.get().getSlotPickerRepresentations()
+        val slot = slots.find { it.id == slotId } ?: return false
+        val selections =
+            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+        val alreadySelected = selections.remove(affordanceId)
+        if (!alreadySelected) {
+            while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
+                selections.removeAt(0)
+            }
+        }
+
+        selections.add(affordanceId)
+
+        repository
+            .get()
+            .setSelections(
+                slotId = slotId,
+                affordanceIds = selections,
+            )
+
+        return true
+    }
+
+    /**
+     * Unselects one or all affordances from the slot with the given ID.
+     *
+     * @param slotId The ID of the slot.
+     * @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances
+     * from the slot.
+     * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
+     * the affordance was not on the slot to begin with).
+     */
+    fun unselect(slotId: String, affordanceId: String?): Boolean {
+        check(isUsingRepository)
+
+        val slots = repository.get().getSlotPickerRepresentations()
+        if (slots.find { it.id == slotId } == null) {
+            return false
+        }
+
+        if (affordanceId.isNullOrEmpty()) {
+            return if (
+                repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
+            ) {
+                false
+            } else {
+                repository.get().setSelections(slotId = slotId, affordanceIds = emptyList())
+                true
+            }
+        }
+
+        val selections =
+            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+        return if (selections.remove(affordanceId)) {
+            repository
+                .get()
+                .setSelections(
+                    slotId = slotId,
+                    affordanceIds = selections,
+                )
+            true
+        } else {
+            false
+        }
+    }
+
+    /** Returns affordance IDs indexed by slot ID, for all known slots. */
+    fun getSelections(): Map<String, List<String>> {
+        check(isUsingRepository)
+
+        val selections = repository.get().getSelections()
+        return repository.get().getSlotPickerRepresentations().associate { slotRepresentation ->
+            slotRepresentation.id to (selections[slotRepresentation.id] ?: emptyList())
         }
     }
 
     private fun quickAffordanceInternal(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceModel> {
-        val configs = registry.getAll(position)
+        return if (isUsingRepository) {
+            repository
+                .get()
+                .selections
+                .map { it[position.toSlotId()] ?: emptyList() }
+                .flatMapLatest { configs -> combinedConfigs(position, configs) }
+        } else {
+            combinedConfigs(position, registry.getAll(position))
+        }
+    }
+
+    private fun combinedConfigs(
+        position: KeyguardQuickAffordancePosition,
+        configs: List<KeyguardQuickAffordanceConfig>,
+    ): Flow<KeyguardQuickAffordanceModel> {
+        if (configs.isEmpty()) {
+            return flowOf(KeyguardQuickAffordanceModel.Hidden)
+        }
+
         return combine(
             configs.map { config ->
-                // We emit an initial "Hidden" value to make sure that there's always an initial
-                // 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) }
+                // We emit an initial "Hidden" value to make sure that there's always an
+                // initial 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.lockScreenState.onStart {
+                    emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+                }
             }
         ) { states ->
-            val index = states.indexOfFirst { it is KeyguardQuickAffordanceConfig.State.Visible }
+            val index =
+                states.indexOfFirst { state ->
+                    state is KeyguardQuickAffordanceConfig.LockScreenState.Visible
+                }
             if (index != -1) {
-                val visibleState = states[index] as KeyguardQuickAffordanceConfig.State.Visible
+                val visibleState =
+                    states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible
+                val configKey = configs[index].key
                 KeyguardQuickAffordanceModel.Visible(
-                    configKey = configs[index]::class,
+                    configKey =
+                        if (isUsingRepository) {
+                            configKey.encode(position.toSlotId())
+                        } else {
+                            configKey
+                        },
                     icon = visibleState.icon,
-                    toggle = visibleState.toggle,
+                    activationState = visibleState.activationState,
                 )
             } else {
                 KeyguardQuickAffordanceModel.Hidden
@@ -142,4 +285,48 @@
             )
         }
     }
+
+    private fun KeyguardQuickAffordancePosition.toSlotId(): String {
+        return when (this) {
+            KeyguardQuickAffordancePosition.BOTTOM_START ->
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+            KeyguardQuickAffordancePosition.BOTTOM_END ->
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
+        }
+    }
+
+    private fun String.encode(slotId: String): String {
+        return "$slotId$DELIMITER$this"
+    }
+
+    private fun String.decode(): Pair<String, String> {
+        val splitUp = this.split(DELIMITER)
+        return Pair(splitUp[0], splitUp[1])
+    }
+
+    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
+        check(isUsingRepository)
+
+        return repository.get().getAffordancePickerRepresentations()
+    }
+
+    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
+        check(isUsingRepository)
+
+        return repository.get().getSlotPickerRepresentations()
+    }
+
+    fun getPickerFlags(): List<KeyguardPickerFlag> {
+        return listOf(
+            KeyguardPickerFlag(
+                name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED,
+                value = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
+            )
+        )
+    }
+
+    companion object {
+        private const val TAG = "KeyguardQuickAffordanceInteractor"
+        private const val DELIMITER = "::"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
new file mode 100644
index 0000000..58a8093
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.interactor
+
+import com.android.keyguard.logging.KeyguardLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Collect flows of interest for auditing keyguard transitions. */
+@SysUISingleton
+class KeyguardTransitionAuditLogger
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val interactor: KeyguardTransitionInteractor,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val logger: KeyguardLogger,
+) {
+
+    fun start() {
+        scope.launch {
+            keyguardInteractor.wakefulnessState.collect { logger.v("WakefulnessState", it) }
+        }
+
+        scope.launch {
+            keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) }
+        }
+
+        scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } }
+
+        scope.launch {
+            interactor.finishedKeyguardTransitionStep.collect {
+                logger.i("Finished transition", it)
+            }
+        }
+
+        scope.launch {
+            interactor.canceledKeyguardTransitionStep.collect {
+                logger.i("Canceled transition", it)
+            }
+        }
+
+        scope.launch {
+            interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
new file mode 100644
index 0000000..43dd358e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.interactor
+
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import java.util.Set
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardTransitionCoreStartable
+@Inject
+constructor(
+    private val interactors: Set<TransitionInteractor>,
+    private val auditLogger: KeyguardTransitionAuditLogger,
+) : CoreStartable {
+
+    override fun start() {
+        // By listing the interactors in a when, the compiler will help enforce all classes
+        // extending the sealed class [TransitionInteractor] will be initialized.
+        interactors.forEach {
+            // `when` needs to be an expression in order for the compiler to enforce it being
+            // exhaustive
+            val ret =
+                when (it) {
+                    is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is DreamingLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is DreamingToAodTransitionInteractor -> Log.d(TAG, "Started $it")
+                }
+            it.start()
+        }
+        auditLogger.start()
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionCoreStartable"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
new file mode 100644
index 0000000..54a4f49
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -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.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+/** Encapsulates business-logic related to the keyguard transitions. */
+@SysUISingleton
+class KeyguardTransitionInteractor
+@Inject
+constructor(
+    repository: KeyguardTransitionRepository,
+) {
+    /** AOD->LOCKSCREEN transition information. */
+    val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+
+    /** LOCKSCREEN->AOD transition information. */
+    val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+    /** (any)->AOD transition information */
+    val anyStateToAodTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == KeyguardState.AOD }
+
+    /**
+     * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
+     * Lockscreen (0f).
+     */
+    val dozeAmountTransition: Flow<TransitionStep> =
+        merge(
+            aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
+            lockscreenToAodTransition,
+        )
+
+    /* The last [TransitionStep] with a [TransitionState] of STARTED */
+    val startedKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
+
+    /* The last [TransitionStep] with a [TransitionState] of CANCELED */
+    val canceledKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.CANCELED }
+
+    /* The last [TransitionStep] with a [TransitionState] of FINISHED */
+    val finishedKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
+
+    /* The last completed [KeyguardState] transition */
+    val finishedKeyguardState: Flow<KeyguardState> =
+        finishedKeyguardTransitionStep.map { step -> step.to }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..cca2d56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class LockscreenBouncerTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val shadeRepository: ShadeRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor
+) : TransitionInteractor("LOCKSCREEN<->BOUNCER") {
+
+    private var transitionId: UUID? = null
+
+    override fun start() {
+        listenForDraggingUpToBouncer()
+        listenForBouncerHiding()
+    }
+
+    private fun listenForBouncerHiding() {
+        scope.launch {
+            keyguardInteractor.isBouncerShowing
+                .sample(
+                    combine(
+                        keyguardInteractor.wakefulnessState,
+                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
+                    ) { a, b ->
+                        Pair(a, b)
+                    },
+                    { a, bc -> Triple(a, bc.first, bc.second) }
+                )
+                .collect { triple ->
+                    val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
+                    if (
+                        !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
+                    ) {
+                        val to =
+                            if (
+                                wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP ||
+                                    wakefulnessState == WakefulnessModel.ASLEEP
+                            ) {
+                                KeyguardState.AOD
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.BOUNCER,
+                                to = to,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
+    private fun listenForDraggingUpToBouncer() {
+        scope.launch {
+            shadeRepository.shadeModel
+                .sample(
+                    combine(
+                        keyguardTransitionInteractor.finishedKeyguardState,
+                        keyguardInteractor.statusBarState,
+                    ) { a, b ->
+                        Pair(a, b)
+                    },
+                    { a, bc -> Triple(a, bc.first, bc.second) }
+                )
+                .collect { triple ->
+                    val (shadeModel, keyguardState, statusBarState) = triple
+
+                    val id = transitionId
+                    if (id != null) {
+                        // An existing `id` means a transition is started, and calls to
+                        // `updateTransition` will control it until FINISHED
+                        keyguardTransitionRepository.updateTransition(
+                            id,
+                            shadeModel.expansionAmount,
+                            if (
+                                shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
+                            ) {
+                                transitionId = null
+                                TransitionState.FINISHED
+                            } else {
+                                TransitionState.RUNNING
+                            }
+                        )
+                    } else {
+                        // TODO (b/251849525): Remove statusbarstate check when that state is
+                        // integrated into KeyguardTransitionRepository
+                        if (
+                            keyguardState == KeyguardState.LOCKSCREEN &&
+                                shadeModel.isUserDragging &&
+                                statusBarState != SHADE_LOCKED
+                        ) {
+                            transitionId =
+                                keyguardTransitionRepository.startTransition(
+                                    TransitionInfo(
+                                        ownerName = name,
+                                        from = KeyguardState.LOCKSCREEN,
+                                        to = KeyguardState.BOUNCER,
+                                        animator = null,
+                                    )
+                                )
+                        }
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
new file mode 100644
index 0000000..4100f7a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class LockscreenGoneTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("LOCKSCREEN->GONE") {
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.isKeyguardGoingAway
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isKeyguardGoingAway, keyguardState) = pair
+                    if (!isKeyguardGoingAway && keyguardState == KeyguardState.LOCKSCREEN) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.LOCKSCREEN,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 10L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
new file mode 100644
index 0000000..c5e49c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.interactor
+
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.util.ListenerSet
+import javax.inject.Inject
+
+/** Interactor to add and remove callbacks for the bouncer. */
+@SysUISingleton
+class PrimaryBouncerCallbackInteractor @Inject constructor() {
+    private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>()
+    private var expansionCallbacks = ArrayList<KeyguardBouncer.PrimaryBouncerExpansionCallback>()
+    /** Add a KeyguardResetCallback. */
+    fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+        resetCallbacks.addIfAbsent(callback)
+    }
+
+    /** Remove a KeyguardResetCallback. */
+    fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+        resetCallbacks.remove(callback)
+    }
+
+    /** Adds a callback to listen to bouncer expansion updates. */
+    fun addBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+        if (!expansionCallbacks.contains(callback)) {
+            expansionCallbacks.add(callback)
+        }
+    }
+
+    /**
+     * Removes a previously added callback. If the callback was never added, this method does
+     * nothing.
+     */
+    fun removeBouncerExpansionCallback(callback: KeyguardBouncer.PrimaryBouncerExpansionCallback) {
+        expansionCallbacks.remove(callback)
+    }
+
+    /** Propagate fully shown to bouncer expansion callbacks. */
+    fun dispatchFullyShown() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyShown()
+        }
+    }
+
+    /** Propagate starting to hide to bouncer expansion callbacks. */
+    fun dispatchStartingToHide() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToHide()
+        }
+    }
+
+    /** Propagate starting to show to bouncer expansion callbacks. */
+    fun dispatchStartingToShow() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToShow()
+        }
+    }
+
+    /** Propagate fully hidden to bouncer expansion callbacks. */
+    fun dispatchFullyHidden() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyHidden()
+        }
+    }
+
+    /** Propagate expansion changes to bouncer expansion callbacks. */
+    fun dispatchExpansionChanged(expansion: Float) {
+        for (callback in expansionCallbacks) {
+            callback.onExpansionChanged(expansion)
+        }
+    }
+    /** Propagate visibility changes to bouncer expansion callbacks. */
+    fun dispatchVisibilityChanged(visibility: Int) {
+        for (callback in expansionCallbacks) {
+            callback.onVisibilityChanged(visibility == View.VISIBLE)
+        }
+    }
+
+    /** Propagate keyguard reset. */
+    fun dispatchReset() {
+        for (callback in resetCallbacks) {
+            callback.onKeyguardReset()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
new file mode 100644
index 0000000..910cdf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt
@@ -0,0 +1,341 @@
+/*
+ * 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.interactor
+
+import android.content.res.ColorStateList
+import android.os.Handler
+import android.os.Trace
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.DejankUtils
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/**
+ * Encapsulates business logic for interacting with the lock-screen primary (pin/pattern/password)
+ * bouncer.
+ */
+@SysUISingleton
+class PrimaryBouncerInteractor
+@Inject
+constructor(
+    private val repository: KeyguardBouncerRepository,
+    private val primaryBouncerView: BouncerView,
+    @Main private val mainHandler: Handler,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardSecurityModel: KeyguardSecurityModel,
+    private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
+    private val falsingCollector: FalsingCollector,
+    private val dismissCallbackRegistry: DismissCallbackRegistry,
+    keyguardBypassController: KeyguardBypassController,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+    /** Whether we want to wait for face auth. */
+    private val primaryBouncerFaceDelay =
+        keyguardStateController.isFaceAuthEnabled &&
+            !keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
+                KeyguardUpdateMonitor.getCurrentUser()
+            ) &&
+            !needsFullscreenBouncer() &&
+            !keyguardUpdateMonitor.userNeedsStrongAuth() &&
+            !keyguardBypassController.bypassEnabled
+
+    /** Runnable to show the primary bouncer. */
+    val showRunnable = Runnable {
+        repository.setPrimaryVisible(true)
+        repository.setPrimaryShow(
+            KeyguardBouncerModel(
+                promptReason = repository.bouncerPromptReason ?: 0,
+                errorMessage = repository.bouncerErrorMessage,
+                expansionAmount = repository.panelExpansionAmount.value
+            )
+        )
+        repository.setPrimaryShowingSoon(false)
+    }
+
+    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
+    val screenTurnedOff: Flow<Unit> = repository.onScreenTurnedOff.filter { it }.map {}
+    val show: Flow<KeyguardBouncerModel> = repository.primaryBouncerShow.filterNotNull()
+    val hide: Flow<Unit> = repository.primaryBouncerHide.filter { it }.map {}
+    val startingToHide: Flow<Unit> = repository.primaryBouncerStartingToHide.filter { it }.map {}
+    val isVisible: Flow<Boolean> = repository.primaryBouncerVisible
+    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
+    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
+    val startingDisappearAnimation: Flow<Runnable> =
+        repository.primaryBouncerStartingDisappearAnimation.filterNotNull()
+    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
+    val keyguardPosition: Flow<Float> = repository.keyguardPosition
+    val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
+    /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
+    val bouncerExpansion: Flow<Float> =
+        combine(repository.panelExpansionAmount, repository.primaryBouncerVisible) {
+            panelExpansion,
+            primaryBouncerVisible ->
+            if (primaryBouncerVisible) {
+                1f - panelExpansion
+            } else {
+                0f
+            }
+        }
+
+    // TODO(b/243685699): Move isScrimmed logic to data layer.
+    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
+    /** Show the bouncer if necessary and set the relevant states. */
+    @JvmOverloads
+    fun show(isScrimmed: Boolean) {
+        // Reset some states as we show the bouncer.
+        repository.setOnScreenTurnedOff(false)
+        repository.setKeyguardAuthenticated(null)
+        repository.setPrimaryHide(false)
+        repository.setPrimaryStartingToHide(false)
+
+        val resumeBouncer =
+            (repository.primaryBouncerVisible.value ||
+                repository.primaryBouncerShowingSoon.value) && needsFullscreenBouncer()
+
+        if (!resumeBouncer && repository.primaryBouncerShow.value != null) {
+            // If bouncer is visible, the bouncer is already showing.
+            return
+        }
+
+        val keyguardUserId = KeyguardUpdateMonitor.getCurrentUser()
+        if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
+            // In split system user mode, we never unlock system user.
+            return
+        }
+
+        Trace.beginSection("KeyguardBouncer#show")
+        repository.setPrimaryScrimmed(isScrimmed)
+        if (isScrimmed) {
+            setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+        }
+
+        if (resumeBouncer) {
+            primaryBouncerView.delegate?.resume()
+            // Bouncer is showing the next security screen and we just need to prompt a resume.
+            return
+        }
+        if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
+            // Keyguard is done.
+            return
+        }
+
+        repository.setPrimaryShowingSoon(true)
+        if (primaryBouncerFaceDelay) {
+            mainHandler.postDelayed(showRunnable, 1200L)
+        } else {
+            DejankUtils.postAfterTraversal(showRunnable)
+        }
+        keyguardStateController.notifyBouncerShowing(true)
+        primaryBouncerCallbackInteractor.dispatchStartingToShow()
+        Trace.endSection()
+    }
+
+    /** Sets the correct bouncer states to hide the bouncer. */
+    fun hide() {
+        Trace.beginSection("KeyguardBouncer#hide")
+        if (isFullyShowing()) {
+            SysUiStatsLog.write(
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
+            )
+            dismissCallbackRegistry.notifyDismissCancelled()
+        }
+
+        falsingCollector.onBouncerHidden()
+        keyguardStateController.notifyBouncerShowing(false /* showing */)
+        cancelShowRunnable()
+        repository.setPrimaryShowingSoon(false)
+        repository.setPrimaryVisible(false)
+        repository.setPrimaryHide(true)
+        repository.setPrimaryShow(null)
+        Trace.endSection()
+    }
+
+    /**
+     * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
+     * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
+     * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
+     * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
+     * of 0f represents the bouncer fully showing.
+     */
+    fun setPanelExpansion(expansion: Float) {
+        val oldExpansion = repository.panelExpansionAmount.value
+        val expansionChanged = oldExpansion != expansion
+        if (repository.primaryBouncerStartingDisappearAnimation.value == null) {
+            repository.setPanelExpansion(expansion)
+        }
+
+        if (
+            expansion == KeyguardBouncer.EXPANSION_VISIBLE &&
+                oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE
+        ) {
+            falsingCollector.onBouncerShown()
+            primaryBouncerCallbackInteractor.dispatchFullyShown()
+        } else if (
+            expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
+                oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
+        ) {
+            /*
+             * There are cases where #hide() was not invoked, such as when
+             * NotificationPanelViewController controls the hide animation. Make sure the state gets
+             * updated by calling #hide() directly.
+             */
+            hide()
+            DejankUtils.postAfterTraversal { primaryBouncerCallbackInteractor.dispatchReset() }
+            primaryBouncerCallbackInteractor.dispatchFullyHidden()
+        } else if (
+            expansion != KeyguardBouncer.EXPANSION_VISIBLE &&
+                oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE
+        ) {
+            primaryBouncerCallbackInteractor.dispatchStartingToHide()
+            repository.setPrimaryStartingToHide(true)
+        }
+        if (expansionChanged) {
+            primaryBouncerCallbackInteractor.dispatchExpansionChanged(expansion)
+        }
+    }
+
+    /** Set the initial keyguard message to show when bouncer is shown. */
+    fun showMessage(message: String?, colorStateList: ColorStateList?) {
+        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
+    }
+
+    /**
+     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
+     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
+     * call cancelAction.
+     */
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?
+    ) {
+        primaryBouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
+    }
+
+    /** Update the resources of the views. */
+    fun updateResources() {
+        repository.setResourceUpdateRequests(true)
+    }
+
+    /** Tell the bouncer that keyguard is authenticated. */
+    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
+        repository.setKeyguardAuthenticated(strongAuth)
+    }
+
+    /** Tell the bouncer the screen has turned off. */
+    fun onScreenTurnedOff() {
+        repository.setOnScreenTurnedOff(true)
+    }
+
+    /** Update the position of the bouncer when showing. */
+    fun setKeyguardPosition(position: Float) {
+        repository.setKeyguardPosition(position)
+    }
+
+    /** Notifies that the state change was handled. */
+    fun notifyKeyguardAuthenticatedHandled() {
+        repository.setKeyguardAuthenticated(null)
+    }
+
+    /** Notify that view visibility has changed. */
+    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
+        primaryBouncerCallbackInteractor.dispatchVisibilityChanged(visibility)
+    }
+
+    /** Notify that the resources have been updated */
+    fun notifyUpdatedResources() {
+        repository.setResourceUpdateRequests(false)
+    }
+
+    /** Set whether back button is enabled when on the bouncer screen. */
+    fun setBackButtonEnabled(enabled: Boolean) {
+        repository.setIsBackButtonEnabled(enabled)
+    }
+
+    /** Tell the bouncer to start the pre hide animation. */
+    fun startDisappearAnimation(runnable: Runnable) {
+        val finishRunnable = Runnable {
+            runnable.run()
+            repository.setPrimaryStartDisappearAnimation(null)
+        }
+        repository.setPrimaryStartDisappearAnimation(finishRunnable)
+    }
+
+    /** Returns whether bouncer is fully showing. */
+    fun isFullyShowing(): Boolean {
+        return (repository.primaryBouncerShowingSoon.value ||
+            repository.primaryBouncerVisible.value) &&
+            repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
+            repository.primaryBouncerStartingDisappearAnimation.value == null
+    }
+
+    /** Returns whether bouncer is scrimmed. */
+    fun isScrimmed(): Boolean {
+        return repository.primaryBouncerScrimmed.value
+    }
+
+    /** If bouncer expansion is between 0f and 1f non-inclusive. */
+    fun isInTransit(): Boolean {
+        return repository.primaryBouncerShowingSoon.value ||
+            repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
+                repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
+    }
+
+    /** Return whether bouncer is animating away. */
+    fun isAnimatingAway(): Boolean {
+        return repository.primaryBouncerStartingDisappearAnimation.value != null
+    }
+
+    /** Return whether bouncer will dismiss with actions */
+    fun willDismissWithAction(): Boolean {
+        return primaryBouncerView.delegate?.willDismissWithActions() == true
+    }
+
+    /** Returns whether the bouncer should be full screen. */
+    private fun needsFullscreenBouncer(): Boolean {
+        val mode: KeyguardSecurityModel.SecurityMode =
+            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
+        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
+            mode == KeyguardSecurityModel.SecurityMode.SimPuk
+    }
+
+    /** Remove the show runnable from the main handler queue to improve performance. */
+    private fun cancelShowRunnable() {
+        DejankUtils.removeCallbacks(showRunnable)
+        mainHandler.removeCallbacks(showRunnable)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
new file mode 100644
index 0000000..dbffeab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.interactor
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+abstract class StartKeyguardTransitionModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardTransitionCoreStartable::class)
+    abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenBouncer(
+        impl: LockscreenBouncerTransitionInteractor
+    ): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+
+    @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
+
+    @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun dreamingLockscreen(
+        impl: DreamingLockscreenTransitionInteractor
+    ): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun dreamingToAod(impl: DreamingToAodTransitionInteractor): TransitionInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
new file mode 100644
index 0000000..a2a46d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.interactor
+/**
+ * Each TransitionInteractor is responsible for determining under which conditions to notify
+ * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is
+ * determined by [KeyguardTransitionRepository].
+ *
+ * [name] field should be a unique identifiable string representing this state, used primarily for
+ * logging
+ *
+ * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the
+ * 'when' clause of [KeyguardTransitionCoreStartable]
+ */
+sealed class TransitionInteractor(val name: String) {
+
+    abstract fun start()
+}
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/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
deleted file mode 100644
index 581dafa3..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ /dev/null
@@ -1,23 +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.model
-
-/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
-enum class KeyguardQuickAffordancePosition {
-    BOTTOM_START,
-    BOTTOM_END,
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
deleted file mode 100644
index 8384260..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,143 +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.Context
-import android.content.Intent
-import androidx.annotation.DrawableRes
-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.controls.ControlsServiceInfo
-import com.android.systemui.controls.controller.StructureInfo
-import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.controls.ui.ControlsActivity
-import com.android.systemui.controls.ui.ControlsUiController
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.util.kotlin.getOrNull
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-
-/** Home controls quick affordance data source. */
-@SysUISingleton
-class HomeControlsKeyguardQuickAffordanceConfig
-@Inject
-constructor(
-    @Application context: Context,
-    private val component: ControlsComponent,
-) : KeyguardQuickAffordanceConfig {
-
-    private val appContext = context.applicationContext
-
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> =
-        component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
-            if (canShowWhileLocked) {
-                stateInternal(component.getControlsListingController().getOrNull())
-            } else {
-                flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
-            }
-        }
-
-    override fun onQuickAffordanceClicked(
-        expandable: Expandable?,
-    ): KeyguardQuickAffordanceConfig.OnClickedResult {
-        return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
-            intent =
-                Intent(appContext, ControlsActivity::class.java)
-                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(
-                        ControlsUiController.EXTRA_ANIMATE,
-                        true,
-                    ),
-            canShowWhileLocked = component.canShowWhileLockedSetting.value,
-        )
-    }
-
-    private fun stateInternal(
-        listingController: ControlsListingController?,
-    ): Flow<KeyguardQuickAffordanceConfig.State> {
-        if (listingController == null) {
-            return flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
-        }
-
-        return conflatedCallbackFlow {
-            val callback =
-                object : ControlsListingController.ControlsListingCallback {
-                    override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
-                        val favorites: List<StructureInfo>? =
-                            component.getControlsController().getOrNull()?.getFavorites()
-
-                        trySendWithFailureLogging(
-                            state(
-                                isFeatureEnabled = component.isEnabled(),
-                                hasFavorites = favorites?.isNotEmpty() == true,
-                                hasServiceInfos = serviceInfos.isNotEmpty(),
-                                iconResourceId = component.getTileImageId(),
-                                visibility = component.getVisibility(),
-                            ),
-                            TAG,
-                        )
-                    }
-                }
-
-            listingController.addCallback(callback)
-
-            awaitClose { listingController.removeCallback(callback) }
-        }
-    }
-
-    private fun state(
-        isFeatureEnabled: Boolean,
-        hasFavorites: Boolean,
-        hasServiceInfos: Boolean,
-        visibility: ControlsComponent.Visibility,
-        @DrawableRes iconResourceId: Int?,
-    ): KeyguardQuickAffordanceConfig.State {
-        return if (
-            isFeatureEnabled &&
-                hasFavorites &&
-                hasServiceInfos &&
-                iconResourceId != null &&
-                visibility == ControlsComponent.Visibility.AVAILABLE
-        ) {
-            KeyguardQuickAffordanceConfig.State.Visible(
-                icon =
-                    Icon.Resource(
-                        res = iconResourceId,
-                        contentDescription =
-                            ContentDescription.Resource(
-                                res = component.getTileTitleId(),
-                            ),
-                    ),
-            )
-        } else {
-            KeyguardQuickAffordanceConfig.State.Hidden
-        }
-    }
-
-    companion object {
-        private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig"
-    }
-}
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/model/BiometricUnlockModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt
new file mode 100644
index 0000000..db709b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.shared.model
+
+/** Model device wakefulness states. */
+enum class BiometricUnlockModel {
+    /** Mode in which we don't need to wake up the device when we authenticate. */
+    NONE,
+    /**
+     * Mode in which we wake up the device, and directly dismiss Keyguard. Active when we acquire a
+     * fingerprint while the screen is off and the device was sleeping.
+     */
+    WAKE_AND_UNLOCK,
+    /**
+     * Mode in which we wake the device up, and fade out the Keyguard contents because they were
+     * already visible while pulsing in doze mode.
+     */
+    WAKE_AND_UNLOCK_PULSING,
+    /**
+     * Mode in which we wake up the device, but play the normal dismiss animation. Active when we
+     * acquire a fingerprint pulsing in doze mode.
+     */
+    SHOW_BOUNCER,
+    /**
+     * Mode in which we only wake up the device, and keyguard was not showing when we authenticated.
+     */
+    ONLY_WAKE,
+    /**
+     * Mode in which fingerprint unlocks the device or passive auth (ie face auth) unlocks the
+     * device while being requested when keyguard is occluded or showing.
+     */
+    UNLOCK_COLLAPSING,
+    /** When bouncer is visible and will be dismissed. */
+    DISMISS_BOUNCER,
+    /** Mode in which fingerprint wakes and unlocks the device from a dream. */
+    WAKE_AND_UNLOCK_FROM_DREAM,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt
new file mode 100644
index 0000000..a7a5957
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardPickerFlag.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.shared.model
+
+/** Represents a flag that's consumed by the settings or wallpaper picker app. */
+data class KeyguardPickerFlag(
+    val name: String,
+    val value: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
new file mode 100644
index 0000000..a56bc90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.shared.model
+
+import androidx.annotation.DrawableRes
+
+/**
+ * Representation of a quick affordance for use to build "picker", "selector", or "settings"
+ * experiences.
+ */
+data class KeyguardQuickAffordancePickerRepresentation(
+    val id: String,
+    val name: String,
+    @DrawableRes val iconResourceId: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt
new file mode 100644
index 0000000..86f2756
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.shared.model
+
+/**
+ * Representation of a quick affordance slot (or position) for use to build "picker", "selector", or
+ * "settings" experiences.
+ */
+data class KeyguardSlotPickerRepresentation(
+    val id: String,
+    /** The maximum number of selected affordances that can be present on this slot. */
+    val maxSelectedAffordances: Int = 1,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
new file mode 100644
index 0000000..dd908c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.shared.model
+
+/** List of all possible states to transition to/from */
+enum class KeyguardState {
+    /*
+     * The display is completely off, as well as any sensors that would trigger the device to wake
+     * up.
+     */
+    OFF,
+    /**
+     * The device has entered a special low-power mode within SystemUI. Doze is technically a
+     * special dream service implementation. No UI is visible. In this state, a least some
+     * low-powered sensors such as lift to wake or tap to wake are enabled, or wake screen for
+     * notifications is enabled, allowing the device to quickly wake up.
+     */
+    DOZING,
+    /*
+     * A device state after the device times out, which can be from both LOCKSCREEN or GONE states.
+     * DOZING is an example of special version of this state. Dreams may be implemented by third
+     * parties to present their own UI over keyguard, like a screensaver.
+     */
+    DREAMING,
+    /**
+     * The device has entered a special low-power mode within SystemUI, also called the Always-on
+     * Display (AOD). A minimal UI is presented to show critical information. If the device is in
+     * low-power mode without a UI, then it is DOZING.
+     */
+    AOD,
+    /*
+     * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
+     * (Fingerprint Sensor) variations, for the user to verify their credentials
+     */
+    BOUNCER,
+    /*
+     * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
+     * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
+     */
+    LOCKSCREEN,
+    /*
+     * Keyguard is no longer visible. In most cases the user has just authenticated and keyguard
+     * is being removed, but there are other cases where the user is swiping away keyguard, such as
+     * with SWIPE security method or face unlock without bypass.
+     */
+    GONE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
new file mode 100644
index 0000000..bb95347
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** See [com.android.systemui.statusbar.StatusBarState] for definitions */
+enum class StatusBarState {
+    SHADE,
+    KEYGUARD,
+    SHADE_LOCKED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
new file mode 100644
index 0000000..bfccf3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.shared.model
+
+import android.animation.ValueAnimator
+
+/** Tracks who is controlling the current transition, and how to run it. */
+data class TransitionInfo(
+    val ownerName: String,
+    val from: KeyguardState,
+    val to: KeyguardState,
+    val animator: ValueAnimator?, // 'null' animator signal manual control
+) {
+    override fun toString(): String =
+        "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " +
+            (if (animator != null) {
+                "animated"
+            } else {
+                "manual"
+            }) +
+            ")"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
new file mode 100644
index 0000000..38a93b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.shared.model
+
+/** Possible states for a running transition between [State] */
+enum class TransitionState {
+    /* Transition has begun. */
+    STARTED,
+    /* Transition is actively running. */
+    RUNNING,
+    /* Transition has completed successfully. */
+    FINISHED,
+    /* Transition has been interrupted, and not completed successfully. */
+    CANCELED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
new file mode 100644
index 0000000..767fd58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.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.shared.model
+
+/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
+data class TransitionStep(
+    val from: KeyguardState = KeyguardState.OFF,
+    val to: KeyguardState = KeyguardState.OFF,
+    val value: Float = 0f, // constrained [0.0, 1.0]
+    val transitionState: TransitionState = TransitionState.FINISHED,
+    val ownerName: String = "",
+) {
+    constructor(
+        info: TransitionInfo,
+        value: Float,
+        transitionState: TransitionState,
+    ) : this(info.from, info.to, value, transitionState, info.ownerName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
new file mode 100644
index 0000000..92040f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.shared.model
+
+/** Model device wakefulness states. */
+enum class WakefulnessModel {
+    /** The device is asleep and not interactive. */
+    ASLEEP,
+    /** Received a signal that the device is beginning to wake up. */
+    STARTING_TO_WAKE,
+    /** Device is now fully awake and interactive. */
+    AWAKE,
+    /** Signal that the device is now going to sleep. */
+    STARTING_TO_SLEEP;
+
+    companion object {
+        fun isSleepingOrStartingToSleep(model: WakefulnessModel): Boolean {
+            return model == ASLEEP || model == STARTING_TO_SLEEP
+        }
+
+        fun isWakingOrStartingToWake(model: WakefulnessModel): Boolean {
+            return model == AWAKE || model == STARTING_TO_WAKE
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt
new file mode 100644
index 0000000..a68d190
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.shared.quickaffordance
+
+/** 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 Active : ActivationState()
+    /** The quick affordance is off. */
+    object Inactive : ActivationState()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
new file mode 100644
index 0000000..a18b036
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared.quickaffordance
+
+/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
+enum class KeyguardQuickAffordancePosition {
+    BOTTOM_START,
+    BOTTOM_END,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
deleted file mode 100644
index 55d38a4..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
+++ /dev/null
@@ -1,28 +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.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()
-    /** The quick affordance is on. */
-    object On : KeyguardQuickAffordanceToggleState()
-    /** The quick affordance is off. */
-    object Off : KeyguardQuickAffordanceToggleState()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index 2c99ca5..3276b6d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -27,6 +27,8 @@
 import androidx.core.view.updateLayoutParams
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.LockIconViewController
 import com.android.settingslib.Utils
 import com.android.systemui.R
 import com.android.systemui.animation.Expandable
@@ -69,6 +71,11 @@
 
         /** Notifies that device configuration has changed. */
         fun onConfigurationChanged()
+
+        /**
+         * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
+         */
+        fun shouldConstrainToTopOfLockIcon(): Boolean
     }
 
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
@@ -208,6 +215,9 @@
             override fun onConfigurationChanged() {
                 configurationBasedDimensions.value = loadFromResources(view)
             }
+
+            override fun shouldConstrainToTopOfLockIcon(): Boolean =
+                    viewModel.shouldConstrainToTopOfLockIcon()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index df26014..7739a45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.keyguard.data.BouncerViewDelegate
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.collect
@@ -75,6 +76,17 @@
                     hostViewController.showPrimarySecurityScreen()
                     hostViewController.onResume()
                 }
+
+                override fun setDismissAction(
+                    onDismissAction: ActivityStarter.OnDismissAction?,
+                    cancelAction: Runnable?
+                ) {
+                    hostViewController.setOnDismissAction(onDismissAction, cancelAction)
+                }
+
+                override fun willDismissWithActions(): Boolean {
+                    return hostViewController.hasDismissActions()
+                }
             }
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -82,6 +94,10 @@
                     viewModel.setBouncerViewDelegate(delegate)
                     launch {
                         viewModel.show.collect {
+                            hostViewController.showPromptReason(it.promptReason)
+                            it.errorMessage?.let { errorMessage ->
+                                hostViewController.showErrorMessage(errorMessage)
+                            }
                             hostViewController.showPrimarySecurityScreen()
                             hostViewController.appear(
                                 SystemBarUtils.getStatusBarHeight(view.context)
@@ -90,18 +106,6 @@
                     }
 
                     launch {
-                        viewModel.showPromptReason.collect { prompt ->
-                            hostViewController.showPromptReason(prompt)
-                        }
-                    }
-
-                    launch {
-                        viewModel.showBouncerErrorMessage.collect { errorMessage ->
-                            hostViewController.showErrorMessage(errorMessage)
-                        }
-                    }
-
-                    launch {
                         viewModel.showWithFullExpansion.collect { model ->
                             hostViewController.resetSecurityContainer()
                             hostViewController.showPromptReason(model.promptReason)
@@ -122,15 +126,6 @@
                     }
 
                     launch {
-                        viewModel.setDismissAction.collect {
-                            hostViewController.setOnDismissAction(
-                                it.onDismissAction,
-                                it.cancelAction
-                            )
-                        }
-                    }
-
-                    launch {
                         viewModel.startDisappearAnimation.collect {
                             hostViewController.startDisappearAnimation(it)
                         }
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..227796f 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
@@ -90,6 +90,12 @@
             .distinctUntilChanged()
     }
 
+    /**
+     * Returns whether the keyguard bottom area should be constrained to the top of the lock icon
+     */
+    fun shouldConstrainToTopOfLockIcon(): Boolean =
+            bottomAreaInteractor.shouldConstrainToTopOfLockIcon()
+
     private fun button(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceViewModel> {
@@ -118,13 +124,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/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 9ad5211..526ae74 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -19,15 +19,13 @@
 import android.view.View
 import com.android.systemui.keyguard.data.BouncerView
 import com.android.systemui.keyguard.data.BouncerViewDelegate
-import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
-import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
 import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 
 /** Models UI state for the lock screen bouncer; handles user input. */
@@ -35,10 +33,10 @@
 @Inject
 constructor(
     private val view: BouncerView,
-    private val interactor: BouncerInteractor,
+    private val interactor: PrimaryBouncerInteractor,
 ) {
     /** Observe on bouncer expansion amount. */
-    val bouncerExpansionAmount: Flow<Float> = interactor.expansionAmount
+    val bouncerExpansionAmount: Flow<Float> = interactor.panelExpansionAmount
 
     /** Observe on bouncer visibility. */
     val isBouncerVisible: Flow<Boolean> = interactor.isVisible
@@ -46,13 +44,6 @@
     /** Observe whether bouncer is showing. */
     val show: Flow<KeyguardBouncerModel> = interactor.show
 
-    /** Observe bouncer prompt when bouncer is showing. */
-    val showPromptReason: Flow<Int> = interactor.show.map { it.promptReason }
-
-    /** Observe bouncer error message when bouncer is showing. */
-    val showBouncerErrorMessage: Flow<CharSequence> =
-        interactor.show.map { it.errorMessage }.filterNotNull()
-
     /** Observe visible expansion when bouncer is showing. */
     val showWithFullExpansion: Flow<KeyguardBouncerModel> =
         interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
@@ -63,9 +54,6 @@
     /** Observe whether bouncer is starting to hide. */
     val startingToHide: Flow<Unit> = interactor.startingToHide
 
-    /** Observe whether we want to set the dismiss action to the bouncer. */
-    val setDismissAction: Flow<BouncerCallbackActionsModel> = interactor.onDismissAction
-
     /** Observe whether we want to start the disappear animation. */
     val startDisappearAnimation: Flow<Runnable> = interactor.startingDisappearAnimation
 
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/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
deleted file mode 100644
index 6124e10..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import android.os.Trace
-import android.util.Log
-import com.android.systemui.log.dagger.LogModule
-import com.android.systemui.util.collection.RingBuffer
-import com.google.errorprone.annotations.CompileTimeConstant
-import java.io.PrintWriter
-import java.util.concurrent.ArrayBlockingQueue
-import java.util.concurrent.BlockingQueue
-import kotlin.concurrent.thread
-import kotlin.math.max
-
-/**
- * A simple ring buffer of recyclable log messages
- *
- * The goal of this class is to enable logging that is both extremely chatty and extremely
- * lightweight. If done properly, logging a message will not result in any heap allocations or
- * string generation. Messages are only converted to strings if the log is actually dumped (usually
- * as the result of taking a bug report).
- *
- * You can dump the entire buffer at any time by running:
- *
- * ```
- * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService <bufferName>
- * ```
- *
- * ...where `bufferName` is the (case-sensitive) [name] passed to the constructor.
- *
- * By default, only messages of WARN level or higher are echoed to logcat, but this can be adjusted
- * locally (usually for debugging purposes).
- *
- * To enable logcat echoing for an entire buffer:
- *
- * ```
- * $ adb shell settings put global systemui/buffer/<bufferName> <level>
- * ```
- *
- * To enable logcat echoing for a specific tag:
- *
- * ```
- * $ adb shell settings put global systemui/tag/<tag> <level>
- * ```
- *
- * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
- * the first letter of any of the previous.
- *
- * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
- *
- * @param name The name of this buffer, printed when the buffer is dumped and in some other
- * situations.
- * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
- * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
- * the maximum, it behaves like a ring buffer.
- */
-class LogBuffer @JvmOverloads constructor(
-    private val name: String,
-    private val maxSize: Int,
-    private val logcatEchoTracker: LogcatEchoTracker,
-    private val systrace: Boolean = true,
-) {
-    private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
-
-    private val echoMessageQueue: BlockingQueue<LogMessage>? =
-            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
-
-    init {
-        if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
-            thread(start = true, name = "LogBuffer-$name", priority = Thread.NORM_PRIORITY) {
-                try {
-                    while (true) {
-                        echoToDesiredEndpoints(echoMessageQueue.take())
-                    }
-                } catch (e: InterruptedException) {
-                    Thread.currentThread().interrupt()
-                }
-            }
-        }
-    }
-
-    var frozen = false
-        private set
-
-    private val mutable
-        get() = !frozen && maxSize > 0
-
-    /**
-     * Logs a message to the log buffer
-     *
-     * May also log the message to logcat if echoing is enabled for this buffer or tag.
-     *
-     * The actual string of the log message is not constructed until it is needed. To accomplish
-     * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is
-     * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data
-     * on the message's fields. The message is then inserted into the buffer where it waits until it
-     * is either pushed out by newer messages or it needs to printed. If and when this latter moment
-     * occurs, the [messagePrinter] function is called on the message. It reads whatever data the
-     * initializer stored and converts it to a human-readable log message.
-     *
-     * @param tag A string of at most 23 characters, used for grouping logs into categories or
-     * subjects. If this message is echoed to logcat, this will be the tag that is used.
-     * @param level Which level to log the message at, both to the buffer and to logcat if it's
-     * echoed. In general, a module should split most of its logs into either INFO or DEBUG level.
-     * INFO level should be reserved for information that other parts of the system might care
-     * about, leaving the specifics of code's day-to-day operations to DEBUG.
-     * @param messageInitializer A function that will be called immediately to store relevant data
-     * on the log message. The value of `this` will be the LogMessage to be initialized.
-     * @param messagePrinter A function that will be called if and when the message needs to be
-     * dumped to logcat or a bug report. It should read the data stored by the initializer and
-     * convert it to a human-readable string. The value of `this` will be the LogMessage to be
-     * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and
-     * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a
-     * new instance of the printer for each call, thwarting our attempts at avoiding any sort of
-     * allocation.
-     * @param exception Provide any exception that need to be logged. This is saved as
-     * [LogMessage.exception]
-     */
-    @JvmOverloads
-    inline fun log(
-            tag: String,
-            level: LogLevel,
-            messageInitializer: MessageInitializer,
-            noinline messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
-    ) {
-        val message = obtain(tag, level, messagePrinter, exception)
-        messageInitializer(message)
-        commit(message)
-    }
-
-    /**
-     * Logs a compile-time string constant [message] to the log buffer. Use sparingly.
-     *
-     * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for
-     * simpler use-cases where [message] is a compile time string constant. For use-cases where the
-     * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
-     * an initializer and a message printer.
-     *
-     * Log buffers are limited by the number of entries, so logging more frequently
-     * will limit the time window that the LogBuffer covers in a bug report.  Richer logs, on the
-     * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to
-     * add more detail to every log may do more to improve overall logging than adding more logs
-     * with this method.
-     */
-    fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
-            log(tag, level, {str1 = message}, { str1!! })
-
-    /**
-     * You should call [log] instead of this method.
-     *
-     * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
-     * grows the buffer by one.
-     *
-     * After calling [obtain], the message will now be at the end of the buffer. The caller must
-     * store any relevant data on the message and then call [commit].
-     */
-    @Synchronized
-    fun obtain(
-            tag: String,
-            level: LogLevel,
-            messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
-    ): LogMessage {
-        if (!mutable) {
-            return FROZEN_MESSAGE
-        }
-        val message = buffer.advance()
-        message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception)
-        return message
-    }
-
-    /**
-     * You should call [log] instead of this method.
-     *
-     * After acquiring a message via [obtain], call this method to signal to the buffer that you
-     * have finished filling in its data fields. The message will be echoed to logcat if
-     * necessary.
-     */
-    @Synchronized
-    fun commit(message: LogMessage) {
-        if (!mutable) {
-            return
-        }
-        // Log in the background thread only if echoMessageQueue exists and has capacity (checking
-        // capacity avoids the possibility of blocking this thread)
-        if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
-            try {
-                echoMessageQueue.put(message)
-            } catch (e: InterruptedException) {
-                // the background thread has been shut down, so just log on this one
-                echoToDesiredEndpoints(message)
-            }
-        } else {
-            echoToDesiredEndpoints(message)
-        }
-    }
-
-    /** Sends message to echo after determining whether to use Logcat and/or systrace. */
-    private fun echoToDesiredEndpoints(message: LogMessage) {
-        val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
-                logcatEchoTracker.isTagLoggable(message.tag, message.level)
-        echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
-    }
-
-    /** Converts the entire buffer to a newline-delimited string */
-    @Synchronized
-    fun dump(pw: PrintWriter, tailLength: Int) {
-        val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
-
-        for (i in iterationStart until buffer.size) {
-            buffer[i].dump(pw)
-        }
-    }
-
-    /**
-     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
-     * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
-     * values if necessary.
-     */
-    @Synchronized
-    fun freeze() {
-        if (!frozen) {
-            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 frozen" })
-            frozen = true
-        }
-    }
-
-    /**
-     * Undoes the effects of calling [freeze].
-     */
-    @Synchronized
-    fun unfreeze() {
-        if (frozen) {
-            log(TAG, LogLevel.DEBUG, { str1 = name }, { "$str1 unfrozen" })
-            frozen = false
-        }
-    }
-
-    private fun echo(message: LogMessage, toLogcat: Boolean, toSystrace: Boolean) {
-        if (toLogcat || toSystrace) {
-            val strMessage = message.messagePrinter(message)
-            if (toSystrace) {
-                echoToSystrace(message, strMessage)
-            }
-            if (toLogcat) {
-                echoToLogcat(message, strMessage)
-            }
-        }
-    }
-
-    private fun echoToSystrace(message: LogMessage, strMessage: String) {
-        Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
-            "$name - ${message.level.shortString} ${message.tag}: $strMessage")
-    }
-
-    private fun echoToLogcat(message: LogMessage, strMessage: String) {
-        when (message.level) {
-            LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception)
-            LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception)
-            LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception)
-            LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception)
-            LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception)
-            LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception)
-        }
-    }
-}
-
-/**
- * A function that will be called immediately to store relevant data on the log message. The value
- * of `this` will be the LogMessage to be initialized.
- */
-typealias MessageInitializer = LogMessage.() -> Unit
-
-private const val TAG = "LogBuffer"
-private val FROZEN_MESSAGE = LogMessageImpl.create()
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index 5651399..f9e341c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -19,6 +19,9 @@
 import android.app.ActivityManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+
 import javax.inject.Inject
 
 @SysUISingleton
@@ -26,7 +29,7 @@
     private val dumpManager: DumpManager,
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
-    /* limit the size of maxPoolSize for low ram (Go) devices */
+    /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
     private fun adjustMaxSize(requestedMaxSize: Int): Int {
         return if (ActivityManager.isLowRamDeviceStatic()) {
             minOf(requestedMaxSize, 20) /* low ram max log size*/
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
deleted file mode 100644
index 53f231c..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import android.util.Log
-
-/**
- * Enum version of @Log.Level
- */
-enum class LogLevel(
-    @Log.Level val nativeLevel: Int,
-    val shortString: String
-) {
-    VERBOSE(Log.VERBOSE, "V"),
-    DEBUG(Log.DEBUG, "D"),
-    INFO(Log.INFO, "I"),
-    WARNING(Log.WARN, "W"),
-    ERROR(Log.ERROR, "E"),
-    WTF(Log.ASSERT, "WTF")
-}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
deleted file mode 100644
index dae2592..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import java.io.PrintWriter
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-/**
- * Generic data class for storing messages logged to a [LogBuffer]
- *
- * Each LogMessage has a few standard fields ([level], [tag], and [timestamp]). The rest are generic
- * data slots that may or may not be used, depending on the nature of the specific message being
- * logged.
- *
- * When a message is logged, the code doing the logging stores data in one or more of the generic
- * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [messagePrinter] function reads the data stored in the generic fields and converts that to a human-
- * readable string. Thus, for every log type there must be a specialized initializer function that
- * stores data specific to that log type and a specialized printer function that prints that data.
- *
- * See [LogBuffer.log] for more information.
- */
-interface LogMessage {
-    val level: LogLevel
-    val tag: String
-    val timestamp: Long
-    val messagePrinter: MessagePrinter
-    val exception: Throwable?
-
-    var str1: String?
-    var str2: String?
-    var str3: String?
-    var int1: Int
-    var int2: Int
-    var long1: Long
-    var long2: Long
-    var double1: Double
-    var bool1: Boolean
-    var bool2: Boolean
-    var bool3: Boolean
-    var bool4: Boolean
-
-    /**
-     * Function that dumps the [LogMessage] to the provided [writer].
-     */
-    fun dump(writer: PrintWriter) {
-        val formattedTimestamp = DATE_FORMAT.format(timestamp)
-        val shortLevel = level.shortString
-        val messageToPrint = messagePrinter(this)
-        printLikeLogcat(writer, formattedTimestamp, shortLevel, tag, messageToPrint)
-        exception?.printStackTrace(writer)
-    }
-}
-
-/**
- * A function that will be called if and when the message needs to be dumped to
- * logcat or a bug report. It should read the data stored by the initializer and convert it to
- * a human-readable string. The value of `this` will be the LogMessage to be printed.
- * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
- * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
- * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
- */
-typealias MessagePrinter = LogMessage.() -> String
-
-private fun printLikeLogcat(
-    pw: PrintWriter,
-    formattedTimestamp: String,
-    shortLogLevel: String,
-    tag: String,
-    message: String
-) {
-    pw.print(formattedTimestamp)
-    pw.print(" ")
-    pw.print(shortLogLevel)
-    pw.print(" ")
-    pw.print(tag)
-    pw.print(": ")
-    pw.println(message)
-}
-
-private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
deleted file mode 100644
index 4dd6f65..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-/**
- * Recyclable implementation of [LogMessage].
- */
-data class LogMessageImpl(
-    override var level: LogLevel,
-    override var tag: String,
-    override var timestamp: Long,
-    override var messagePrinter: MessagePrinter,
-    override var exception: Throwable?,
-    override var str1: String?,
-    override var str2: String?,
-    override var str3: String?,
-    override var int1: Int,
-    override var int2: Int,
-    override var long1: Long,
-    override var long2: Long,
-    override var double1: Double,
-    override var bool1: Boolean,
-    override var bool2: Boolean,
-    override var bool3: Boolean,
-    override var bool4: Boolean,
-) : LogMessage {
-
-    fun reset(
-        tag: String,
-        level: LogLevel,
-        timestamp: Long,
-        renderer: MessagePrinter,
-        exception: Throwable? = null,
-    ) {
-        this.level = level
-        this.tag = tag
-        this.timestamp = timestamp
-        this.messagePrinter = renderer
-        this.exception = exception
-        str1 = null
-        str2 = null
-        str3 = null
-        int1 = 0
-        int2 = 0
-        long1 = 0
-        long2 = 0
-        double1 = 0.0
-        bool1 = false
-        bool2 = false
-        bool3 = false
-        bool4 = false
-    }
-
-    companion object Factory {
-        fun create(): LogMessageImpl {
-            return LogMessageImpl(
-                    LogLevel.DEBUG,
-                    DEFAULT_TAG,
-                    0,
-                    DEFAULT_PRINTER,
-                    null,
-                    null,
-                    null,
-                    null,
-                    0,
-                    0,
-                    0,
-                    0,
-                    0.0,
-                    false,
-                    false,
-                    false,
-                    false)
-        }
-    }
-}
-
-private const val DEFAULT_TAG = "UnknownTag"
-private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
deleted file mode 100644
index 8cda423..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-/**
- * Keeps track of which [LogBuffer] messages should also appear in logcat.
- */
-interface LogcatEchoTracker {
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
-    fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
-
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
-    fun isTagLoggable(tagName: String, level: LogLevel): Boolean
-
-    /**
-     * Whether to log messages in a background thread.
-     */
-    val logInBackgroundThread: Boolean
-}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
deleted file mode 100644
index 40b0cdc..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-import android.content.ContentResolver
-import android.database.ContentObserver
-import android.net.Uri
-import android.os.Handler
-import android.os.Looper
-import android.provider.Settings
-
-/**
- * Version of [LogcatEchoTracker] for debuggable builds
- *
- * The log level of individual buffers or tags can be controlled via global settings:
- *
- * ```
- * # Echo any message to <bufferName> of <level> or higher
- * $ adb shell settings put global systemui/buffer/<bufferName> <level>
- *
- * # Echo any message of <tag> and of <level> or higher
- * $ adb shell settings put global systemui/tag/<tag> <level>
- * ```
- */
-class LogcatEchoTrackerDebug private constructor(
-    private val contentResolver: ContentResolver
-) : LogcatEchoTracker {
-    private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
-    private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
-    override val logInBackgroundThread = true
-
-    companion object Factory {
-        @JvmStatic
-        fun create(
-            contentResolver: ContentResolver,
-            mainLooper: Looper
-        ): LogcatEchoTrackerDebug {
-            val tracker = LogcatEchoTrackerDebug(contentResolver)
-            tracker.attach(mainLooper)
-            return tracker
-        }
-    }
-
-    private fun attach(mainLooper: Looper) {
-        contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(BUFFER_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedBufferLevels.clear()
-                    }
-                })
-
-        contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(TAG_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedTagLevels.clear()
-                    }
-                })
-    }
-
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
-    @Synchronized
-    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
-        return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
-    }
-
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
-    @Synchronized
-    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
-        return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
-    }
-
-    private fun getLogLevel(
-        name: String,
-        path: String,
-        cache: MutableMap<String, LogLevel>
-    ): LogLevel {
-        return cache[name] ?: readSetting("$path/$name").also { cache[name] = it }
-    }
-
-    private fun readSetting(path: String): LogLevel {
-        return try {
-            parseProp(Settings.Global.getString(contentResolver, path))
-        } catch (_: Settings.SettingNotFoundException) {
-            DEFAULT_LEVEL
-        }
-    }
-
-    private fun parseProp(propValue: String?): LogLevel {
-        return when (propValue?.lowercase()) {
-            "verbose" -> LogLevel.VERBOSE
-            "v" -> LogLevel.VERBOSE
-            "debug" -> LogLevel.DEBUG
-            "d" -> LogLevel.DEBUG
-            "info" -> LogLevel.INFO
-            "i" -> LogLevel.INFO
-            "warning" -> LogLevel.WARNING
-            "warn" -> LogLevel.WARNING
-            "w" -> LogLevel.WARNING
-            "error" -> LogLevel.ERROR
-            "e" -> LogLevel.ERROR
-            "assert" -> LogLevel.WTF
-            "wtf" -> LogLevel.WTF
-            else -> DEFAULT_LEVEL
-        }
-    }
-}
-
-private val DEFAULT_LEVEL = LogLevel.WARNING
-private const val BUFFER_PATH = "systemui/buffer"
-private const val TAG_PATH = "systemui/tag"
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
deleted file mode 100644
index 1a4ad19..0000000
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.log
-
-/**
- * Production version of [LogcatEchoTracker] that isn't configurable.
- */
-class LogcatEchoTrackerProd : LogcatEchoTracker {
-    override val logInBackgroundThread = false
-
-    override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
-        return level >= LogLevel.WARNING
-    }
-
-    override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
-        return level >= LogLevel.WARNING
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
index 7f1ad6d..eeadf40 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
@@ -23,7 +23,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as
+ * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as
  * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}
  */
 @Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
index 7d1f1c2..5cca1ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
index 9ca0293..1d016d8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
index 7c5f402..c9f78bc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
new file mode 100644
index 0000000..0645236
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
index 08d969b..76d20be 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 28aa19e..ff291bf 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -22,11 +22,11 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.log.LogcatEchoTracker;
-import com.android.systemui.log.LogcatEchoTrackerDebug;
-import com.android.systemui.log.LogcatEchoTrackerProd;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
+import com.android.systemui.plugins.log.LogcatEchoTrackerDebug;
+import com.android.systemui.plugins.log.LogcatEchoTrackerProd;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.util.Compile;
 
@@ -43,7 +43,7 @@
     @SysUISingleton
     @DozeLog
     public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) {
-        return factory.create("DozeLog", 120);
+        return factory.create("DozeLog", 150);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -250,7 +250,7 @@
     /**
      * Provides a buffer for our connections and disconnections to MediaBrowserService.
      *
-     * See {@link com.android.systemui.media.ResumeMediaBrowser}.
+     * See {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}.
      */
     @Provides
     @SysUISingleton
@@ -262,7 +262,7 @@
     /**
      * Provides a buffer for updates to the media carousel.
      *
-     * See {@link com.android.systemui.media.MediaCarouselController}.
+     * See {@link com.android.systemui.media.controls.ui.MediaCarouselController}.
      */
     @Provides
     @SysUISingleton
@@ -316,6 +316,16 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for keyguard clock logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardClockLog
+    public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
+        return factory.create("KeyguardClockLog", 500);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
index 1d7ba94..af43347 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.ResumeMediaBrowser}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
index b03655a..f4dac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaCarouselController}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
index c67d8be..73690ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 53963fc..0c2cd92 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaTimeoutLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.pipeline.MediaTimeoutLogger}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
index 5c572e8..1570d43 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
index edab8c3..bf216c6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 75a34fc..5b7f4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaViewLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaViewLogger}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
index b1c6dcf..6d91f0c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
index 20fc6ff..26af496 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
index fcc184a..61daf9c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
index 760fbf3..a59afa0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
index a0b6864..6f8ea7f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
index 8c8753a..835d349 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
index 7259eeb..6e2bd7b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
index e96e532..77b1bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
index 557a254..9fd166b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
index dd5010c..dd168ba 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
index bd0d298..d24bfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
index b237f2d..67cdb72 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
index f26b316..af0f7c5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
index dd68375..4c276e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
index 8671dbf..ba8b27c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java b/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java
new file mode 100644
index 0000000..a88a4ca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/logcat/LogAccessDialogActivity.java
@@ -0,0 +1,254 @@
+/*
+ * 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.logcat;
+
+import android.annotation.StyleRes;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.Html;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.util.Slog;
+import android.view.ContextThemeWrapper;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.internal.app.ILogAccessDialogCallback;
+import com.android.systemui.R;
+
+
+/**
+ * Dialog responsible for obtaining user consent per-use log access
+ */
+public class LogAccessDialogActivity extends Activity implements
+        View.OnClickListener {
+    private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
+    public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
+
+
+    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
+    private static final int MSG_DISMISS_DIALOG = 0;
+
+    private String mPackageName;
+    private int mUid;
+    private ILogAccessDialogCallback mCallback;
+
+    private String mAlertTitle;
+    private String mAlertBody;
+    private String mAlertLearnMore;
+    private AlertDialog.Builder mAlertDialog;
+    private AlertDialog mAlert;
+    private View mAlertView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // retrieve Intent extra information
+        if (!readIntentInfo(getIntent())) {
+            Slog.e(TAG, "Invalid Intent extras, finishing");
+            finish();
+            return;
+        }
+
+        // retrieve the title string from passed intent extra
+        try {
+            mAlertTitle = getTitleString(this, mPackageName, mUid);
+        } catch (NameNotFoundException e) {
+            Slog.e(TAG, "Unable to fetch label of package " + mPackageName, e);
+            declineLogAccess();
+            finish();
+            return;
+        }
+
+        mAlertBody = getResources().getString(R.string.log_access_confirmation_body);
+        mAlertLearnMore = getResources().getString(R.string.log_access_confirmation_learn_more);
+
+        // create View
+        int themeId = R.style.LogAccessDialogTheme;
+        mAlertView = createView(themeId);
+
+        // create AlertDialog
+        mAlertDialog = new AlertDialog.Builder(this, themeId);
+        mAlertDialog.setView(mAlertView);
+        mAlertDialog.setOnCancelListener(dialog -> declineLogAccess());
+        mAlertDialog.setOnDismissListener(dialog -> finish());
+
+        // show Alert
+        mAlert = mAlertDialog.create();
+        mAlert.getWindow().setHideOverlayWindows(true);
+        mAlert.show();
+
+        // set Alert Timeout
+        mHandler.sendEmptyMessageDelayed(MSG_DISMISS_DIALOG, DIALOG_TIME_OUT);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (!isChangingConfigurations() && mAlert != null && mAlert.isShowing()) {
+            mAlert.dismiss();
+        }
+        mAlert = null;
+    }
+
+    private boolean readIntentInfo(Intent intent) {
+        if (intent == null) {
+            Slog.e(TAG, "Intent is null");
+            return false;
+        }
+
+        mCallback = ILogAccessDialogCallback.Stub.asInterface(
+                intent.getExtras().getBinder(EXTRA_CALLBACK));
+        if (mCallback == null) {
+            Slog.e(TAG, "Missing callback");
+            return false;
+        }
+
+        mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        if (mPackageName == null || mPackageName.length() == 0) {
+            Slog.e(TAG, "Missing package name extra");
+            return false;
+        }
+
+        if (!intent.hasExtra(Intent.EXTRA_UID)) {
+            Slog.e(TAG, "Missing EXTRA_UID");
+            return false;
+        }
+
+        mUid = intent.getIntExtra(Intent.EXTRA_UID, 0);
+
+        return true;
+    }
+
+    private Handler mHandler = new Handler() {
+        public void handleMessage(android.os.Message msg) {
+            switch (msg.what) {
+                case MSG_DISMISS_DIALOG:
+                    if (mAlert != null) {
+                        mAlert.dismiss();
+                        mAlert = null;
+                        declineLogAccess();
+                    }
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    };
+
+    private String getTitleString(Context context, String callingPackage, int uid)
+            throws NameNotFoundException {
+        PackageManager pm = context.getPackageManager();
+
+        CharSequence appLabel = pm.getApplicationInfoAsUser(callingPackage,
+                PackageManager.MATCH_DIRECT_BOOT_AUTO,
+                UserHandle.getUserId(uid)).loadLabel(pm);
+
+        String titleString = context.getString(R.string.log_access_confirmation_title, appLabel);
+
+        return titleString;
+    }
+
+    private Spannable styleFont(String text) {
+        Spannable s = (Spannable) Html.fromHtml(text);
+        for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
+            TypefaceSpan typefaceSpan = new TypefaceSpan("google-sans");
+            s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
+        }
+        return s;
+    }
+
+    /**
+     * Returns the dialog view.
+     * If we cannot retrieve the package name, it returns null and we decline the full device log
+     * access
+     */
+    private View createView(@StyleRes int themeId) {
+        Context themedContext = new ContextThemeWrapper(this, themeId);
+        final View view = LayoutInflater.from(themedContext).inflate(
+                R.layout.log_access_user_consent_dialog_permission, null /*root*/);
+
+        if (view == null) {
+            throw new InflateException();
+        }
+
+        ((TextView) view.findViewById(R.id.log_access_dialog_title))
+            .setText(mAlertTitle);
+
+        if (!TextUtils.isEmpty(mAlertLearnMore)) {
+            Spannable mSpannableLearnMore = styleFont(mAlertLearnMore);
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(TextUtils.concat(mAlertBody, "\n\n", mSpannableLearnMore));
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setMovementMethod(LinkMovementMethod.getInstance());
+        } else {
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(mAlertBody);
+        }
+
+        Button button_allow = (Button) view.findViewById(R.id.log_access_dialog_allow_button);
+        button_allow.setOnClickListener(this);
+
+        Button button_deny = (Button) view.findViewById(R.id.log_access_dialog_deny_button);
+        button_deny.setOnClickListener(this);
+
+        return view;
+
+    }
+
+    @Override
+    public void onClick(View view) {
+        try {
+            if (view.getId() == R.id.log_access_dialog_allow_button) {
+                mCallback.approveAccessForClient(mUid, mPackageName);
+                finish();
+            } else if (view.getId() == R.id.log_access_dialog_allow_button) {
+                declineLogAccess();
+                finish();
+            }
+        } catch (RemoteException e) {
+            finish();
+        }
+    }
+
+    private void declineLogAccess() {
+        try {
+            mCallback.declineAccessForClient(mUid, mPackageName);
+        } catch (RemoteException e) {
+            finish();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/logcat/OWNERS b/packages/SystemUI/src/com/android/systemui/logcat/OWNERS
new file mode 100644
index 0000000..9c0c414
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/logcat/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 1218649
+file:platform/frameworks/base:/services/core/java/com/android/server/logcat/OWNERS
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt b/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
deleted file mode 100644
index 013683e..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
+++ /dev/null
@@ -1,81 +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.media
-
-import android.graphics.drawable.Animatable2
-import android.graphics.drawable.Drawable
-
-/**
- * AnimationBindHandler is responsible for tracking the bound animation state and preventing
- * jank and conflicts due to media notifications arriving at any time during an animation. It
- * does this in two parts.
- *  - Exit animations fired as a result of user input are tracked. When these are running, any
- *      bind actions are delayed until the animation completes (and then fired in sequence).
- *  - Continuous animations are tracked using their rebind id. Later calls using the same
- *      rebind id will be totally ignored to prevent the continuous animation from restarting.
- */
-internal class AnimationBindHandler : Animatable2.AnimationCallback() {
-    private val onAnimationsComplete = mutableListOf<() -> Unit>()
-    private val registrations = mutableListOf<Animatable2>()
-    private var rebindId: Int? = null
-
-    val isAnimationRunning: Boolean
-        get() = registrations.any { it.isRunning }
-
-    /**
-     * This check prevents rebinding to the action button if the identifier has not changed. A
-     * null value is always considered to be changed. This is used to prevent the connecting
-     * animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by
-     * an application in a row.
-     */
-    fun updateRebindId(newRebindId: Int?): Boolean {
-        if (rebindId == null || newRebindId == null || rebindId != newRebindId) {
-            rebindId = newRebindId
-            return true
-        }
-        return false
-    }
-
-    fun tryRegister(drawable: Drawable?) {
-        if (drawable is Animatable2) {
-            val anim = drawable as Animatable2
-            anim.registerAnimationCallback(this)
-            registrations.add(anim)
-        }
-    }
-
-    fun unregisterAll() {
-        registrations.forEach { it.unregisterAnimationCallback(this) }
-        registrations.clear()
-    }
-
-    fun tryExecute(action: () -> Unit) {
-        if (isAnimationRunning) {
-            onAnimationsComplete.add(action)
-        } else {
-            action()
-        }
-    }
-
-    override fun onAnimationEnd(drawable: Drawable) {
-        super.onAnimationEnd(drawable)
-        if (!isAnimationRunning) {
-            onAnimationsComplete.forEach { it() }
-            onAnimationsComplete.clear()
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
deleted file mode 100644
index 556560c..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
+++ /dev/null
@@ -1,209 +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.media
-
-import android.animation.ArgbEvaluator
-import android.animation.ValueAnimator
-import android.animation.ValueAnimator.AnimatorUpdateListener
-import android.content.Context
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import android.graphics.drawable.RippleDrawable
-import com.android.internal.R
-import com.android.internal.annotations.VisibleForTesting
-import com.android.settingslib.Utils
-import com.android.systemui.monet.ColorScheme
-
-/**
- * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
- * is triggered.
- */
-interface ColorTransition {
-    fun updateColorScheme(scheme: ColorScheme?): Boolean
-}
-
-/**
- * A [ColorTransition] that animates between two specific colors.
- * It uses a ValueAnimator to execute the animation and interpolate between the source color and
- * the target color.
- *
- * Selection of the target color from the scheme, and application of the interpolated color
- * are delegated to callbacks.
- */
-open class AnimatingColorTransition(
-    private val defaultColor: Int,
-    private val extractColor: (ColorScheme) -> Int,
-    private val applyColor: (Int) -> Unit
-) : AnimatorUpdateListener, ColorTransition {
-
-    private val argbEvaluator = ArgbEvaluator()
-    private val valueAnimator = buildAnimator()
-    var sourceColor: Int = defaultColor
-    var currentColor: Int = defaultColor
-    var targetColor: Int = defaultColor
-
-    override fun onAnimationUpdate(animation: ValueAnimator) {
-        currentColor = argbEvaluator.evaluate(
-            animation.animatedFraction, sourceColor, targetColor
-        ) as Int
-        applyColor(currentColor)
-    }
-
-    override fun updateColorScheme(scheme: ColorScheme?): Boolean {
-        val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
-        if (newTargetColor != targetColor) {
-            sourceColor = currentColor
-            targetColor = newTargetColor
-            valueAnimator.cancel()
-            valueAnimator.start()
-            return true
-        }
-        return false
-    }
-
-    init {
-        applyColor(defaultColor)
-    }
-
-    @VisibleForTesting
-    open fun buildAnimator(): ValueAnimator {
-        val animator = ValueAnimator.ofFloat(0f, 1f)
-        animator.duration = 333
-        animator.addUpdateListener(this)
-        return animator
-    }
-}
-
-typealias AnimatingColorTransitionFactory =
-            (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
-
-/**
- * ColorSchemeTransition constructs a ColorTransition for each color in the scheme
- * that needs to be transitioned when changed. It also sets up the assignment functions for sending
- * the sending the interpolated colors to the appropriate views.
- */
-class ColorSchemeTransition internal constructor(
-    private val context: Context,
-    private val mediaViewHolder: MediaViewHolder,
-    animatingColorTransitionFactory: AnimatingColorTransitionFactory
-) {
-    constructor(context: Context, mediaViewHolder: MediaViewHolder) :
-        this(context, mediaViewHolder, ::AnimatingColorTransition)
-
-    val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
-    val surfaceColor = animatingColorTransitionFactory(
-        bgColor,
-        ::surfaceFromScheme
-    ) { surfaceColor ->
-        val colorList = ColorStateList.valueOf(surfaceColor)
-        mediaViewHolder.seamlessIcon.imageTintList = colorList
-        mediaViewHolder.seamlessText.setTextColor(surfaceColor)
-        mediaViewHolder.albumView.backgroundTintList = colorList
-        mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
-    }
-
-    val accentPrimary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::accentPrimaryFromScheme
-    ) { accentPrimary ->
-        val accentColorList = ColorStateList.valueOf(accentPrimary)
-        mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
-        mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
-    }
-
-    val accentSecondary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::accentSecondaryFromScheme
-    ) { accentSecondary ->
-        val colorList = ColorStateList.valueOf(accentSecondary)
-        (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
-            it.setColor(colorList)
-            it.effectColor = colorList
-        }
-    }
-
-    val colorSeamless = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        { colorScheme: ColorScheme ->
-            // A1-100 dark in dark theme, A1-200 in light theme
-            if (context.resources.configuration.uiMode and
-                    Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES)
-                        colorScheme.accent1[2]
-                        else colorScheme.accent1[3]
-        }, { seamlessColor: Int ->
-            val accentColorList = ColorStateList.valueOf(seamlessColor)
-            mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
-    })
-
-    val textPrimary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::textPrimaryFromScheme
-    ) { textPrimary ->
-        mediaViewHolder.titleText.setTextColor(textPrimary)
-        val textColorList = ColorStateList.valueOf(textPrimary)
-        mediaViewHolder.seekBar.thumb.setTintList(textColorList)
-        mediaViewHolder.seekBar.progressTintList = textColorList
-        mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
-        mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
-        for (button in mediaViewHolder.getTransparentActionButtons()) {
-            button.imageTintList = textColorList
-        }
-        mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
-    }
-
-    val textPrimaryInverse = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimaryInverse),
-        ::textPrimaryInverseFromScheme
-    ) { textPrimaryInverse ->
-        mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse)
-    }
-
-    val textSecondary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorSecondary),
-        ::textSecondaryFromScheme
-    ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
-
-    val textTertiary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorTertiary),
-        ::textTertiaryFromScheme
-    ) { textTertiary ->
-        mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary)
-    }
-
-    val colorTransitions = arrayOf(
-        surfaceColor,
-        colorSeamless,
-        accentPrimary,
-        accentSecondary,
-        textPrimary,
-        textPrimaryInverse,
-        textSecondary,
-        textTertiary,
-    )
-
-    private fun loadDefaultColor(id: Int): Int {
-        return Utils.getColorAttr(context, id).defaultColor
-    }
-
-    fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
-        var anyChanged = false
-        colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
-        colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
-        return anyChanged
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
deleted file mode 100644
index 73240b5..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
+++ /dev/null
@@ -1,105 +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.media
-
-import android.content.res.ColorStateList
-import android.util.Log
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageButton
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.monet.ColorScheme
-
-/**
- * A view holder for the guts menu of a media player. The guts are shown when the user long-presses
- * on the media player.
- *
- * Both [MediaViewHolder] and [RecommendationViewHolder] use the same guts menu layout, so this
- * class helps share logic between the two.
- */
-class GutsViewHolder constructor(itemView: View) {
-    val gutsText: TextView = itemView.requireViewById(R.id.remove_text)
-    val cancel: View = itemView.requireViewById(R.id.cancel)
-    val cancelText: TextView = itemView.requireViewById(R.id.cancel_text)
-    val dismiss: ViewGroup = itemView.requireViewById(R.id.dismiss)
-    val dismissText: TextView = itemView.requireViewById(R.id.dismiss_text)
-    val settings: ImageButton = itemView.requireViewById(R.id.settings)
-
-    private var isDismissible: Boolean = true
-    var colorScheme: ColorScheme? = null
-
-    /** Marquees the main text of the guts menu. */
-    fun marquee(start: Boolean, delay: Long, tag: String) {
-        val gutsTextHandler = gutsText.handler
-        if (gutsTextHandler == null) {
-            Log.d(tag, "marquee while longPressText.getHandler() is null", Exception())
-            return
-        }
-        gutsTextHandler.postDelayed({ gutsText.isSelected = start }, delay)
-    }
-
-    /** Set whether this control can be dismissed, and update appearance to match */
-    fun setDismissible(dismissible: Boolean) {
-        if (isDismissible == dismissible) return
-
-        isDismissible = dismissible
-        colorScheme?.let { setColors(it) }
-    }
-
-    /** Sets the right colors on all the guts views based on the given [ColorScheme]. */
-    fun setColors(scheme: ColorScheme) {
-        colorScheme = scheme
-        setSurfaceColor(surfaceFromScheme(scheme))
-        setTextPrimaryColor(textPrimaryFromScheme(scheme))
-        setAccentPrimaryColor(accentPrimaryFromScheme(scheme))
-    }
-
-    /** Sets the surface color on all guts views that use it. */
-    fun setSurfaceColor(surfaceColor: Int) {
-        dismissText.setTextColor(surfaceColor)
-        if (!isDismissible) {
-            cancelText.setTextColor(surfaceColor)
-        }
-    }
-
-    /** Sets the primary accent color on all guts views that use it. */
-    fun setAccentPrimaryColor(accentPrimary: Int) {
-        val accentColorList = ColorStateList.valueOf(accentPrimary)
-        settings.imageTintList = accentColorList
-        cancelText.backgroundTintList = accentColorList
-        dismissText.backgroundTintList = accentColorList
-    }
-
-    /** Sets the primary text color on all guts views that use it. */
-    fun setTextPrimaryColor(textPrimary: Int) {
-        val textColorList = ColorStateList.valueOf(textPrimary)
-        gutsText.setTextColor(textColorList)
-        if (isDismissible) {
-            cancelText.setTextColor(textColorList)
-        }
-    }
-
-    companion object {
-        val ids = setOf(
-            R.id.remove_text,
-            R.id.cancel,
-            R.id.dismiss,
-            R.id.settings
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
deleted file mode 100644
index 121ddd4..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.content.res.ColorStateList
-import android.content.res.Resources
-import android.content.res.TypedArray
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.ColorFilter
-import android.graphics.Outline
-import android.graphics.Paint
-import android.graphics.PixelFormat
-import android.graphics.Xfermode
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.util.MathUtils
-import android.view.View
-import androidx.annotation.Keep
-import com.android.internal.graphics.ColorUtils
-import com.android.internal.graphics.ColorUtils.blendARGB
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import org.xmlpull.v1.XmlPullParser
-
-private const val BACKGROUND_ANIM_DURATION = 370L
-
-/**
- * Drawable that can draw an animated gradient when tapped.
- */
-@Keep
-class IlluminationDrawable : Drawable() {
-
-    private var themeAttrs: IntArray? = null
-    private var cornerRadiusOverride = -1f
-    var cornerRadius = 0f
-    get() {
-        return if (cornerRadiusOverride >= 0) {
-            cornerRadiusOverride
-        } else {
-            field
-        }
-    }
-    private var highlightColor = Color.TRANSPARENT
-    private var tmpHsl = floatArrayOf(0f, 0f, 0f)
-    private var paint = Paint()
-    private var highlight = 0f
-    private val lightSources = arrayListOf<LightSourceDrawable>()
-
-    private var backgroundColor = Color.TRANSPARENT
-    set(value) {
-        if (value == field) {
-            return
-        }
-        field = value
-        animateBackground()
-    }
-
-    private var backgroundAnimation: ValueAnimator? = null
-
-    /**
-     * Draw background and gradient.
-     */
-    override fun draw(canvas: Canvas) {
-        canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(),
-                cornerRadius, cornerRadius, paint)
-    }
-
-    override fun getOutline(outline: Outline) {
-        outline.setRoundRect(bounds, cornerRadius)
-    }
-
-    override fun getOpacity(): Int {
-        return PixelFormat.TRANSPARENT
-    }
-
-    override fun inflate(
-        r: Resources,
-        parser: XmlPullParser,
-        attrs: AttributeSet,
-        theme: Resources.Theme?
-    ) {
-        val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
-        themeAttrs = a.extractThemeAttrs()
-        updateStateFromTypedArray(a)
-        a.recycle()
-    }
-
-    private fun updateStateFromTypedArray(a: TypedArray) {
-        if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) {
-            cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius,
-                    cornerRadius)
-        }
-        if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
-            highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
-                    100f
-        }
-    }
-
-    override fun canApplyTheme(): Boolean {
-        return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
-    }
-
-    override fun applyTheme(t: Resources.Theme) {
-        super.applyTheme(t)
-        themeAttrs?.let {
-            val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
-            updateStateFromTypedArray(a)
-            a.recycle()
-        }
-    }
-
-    override fun setColorFilter(p0: ColorFilter?) {
-        throw UnsupportedOperationException("Color filters are not supported")
-    }
-
-    override fun setAlpha(alpha: Int) {
-        if (alpha == paint.alpha) {
-            return
-        }
-
-        paint.alpha = alpha
-        invalidateSelf()
-
-        lightSources.forEach { it.alpha = alpha }
-    }
-
-    override fun getAlpha(): Int {
-        return paint.alpha
-    }
-
-    override fun setXfermode(mode: Xfermode?) {
-        if (mode == paint.xfermode) {
-            return
-        }
-
-        paint.xfermode = mode
-        invalidateSelf()
-    }
-
-    /**
-     * Cross fade background.
-     * @see setTintList
-     * @see backgroundColor
-     */
-    private fun animateBackground() {
-        ColorUtils.colorToHSL(backgroundColor, tmpHsl)
-        val L = tmpHsl[2]
-        tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) {
-            L + highlight
-        } else {
-            L - highlight
-        }, 0f, 1f)
-
-        val initialBackground = paint.color
-        val initialHighlight = highlightColor
-        val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
-
-        backgroundAnimation?.cancel()
-        backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply {
-            duration = BACKGROUND_ANIM_DURATION
-            interpolator = Interpolators.FAST_OUT_LINEAR_IN
-            addUpdateListener {
-                val progress = it.animatedValue as Float
-                paint.color = blendARGB(initialBackground, backgroundColor, progress)
-                highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
-                lightSources.forEach { it.highlightColor = highlightColor }
-                invalidateSelf()
-            }
-            addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
-                    backgroundAnimation = null
-                }
-            })
-            start()
-        }
-    }
-
-    override fun setTintList(tint: ColorStateList?) {
-        super.setTintList(tint)
-        backgroundColor = tint!!.defaultColor
-    }
-
-    fun registerLightSource(lightSource: View) {
-        if (lightSource.background is LightSourceDrawable) {
-            registerLightSource(lightSource.background as LightSourceDrawable)
-        } else if (lightSource.foreground is LightSourceDrawable) {
-            registerLightSource(lightSource.foreground as LightSourceDrawable)
-        }
-    }
-
-    private fun registerLightSource(lightSource: LightSourceDrawable) {
-        lightSource.alpha = paint.alpha
-        lightSources.add(lightSource)
-    }
-
-    /** Set or remove the corner radius override. This is typically set during animations. */
-    fun setCornerRadiusOverride(cornerRadius: Float?) {
-        cornerRadiusOverride = cornerRadius ?: -1f
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
deleted file mode 100644
index 32600fb..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-import android.content.res.Configuration
-import android.database.ContentObserver
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.dagger.MediaModule.KEYGUARD
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.MediaContainerView
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.LargeScreenUtils
-import com.android.systemui.util.settings.SecureSettings
-import javax.inject.Inject
-import javax.inject.Named
-
-/**
- * Controls the media notifications on the lock screen, handles its visibility and placement -
- * switches media player positioning between split pane container vs single pane container
- */
-@SysUISingleton
-class KeyguardMediaController @Inject constructor(
-    @param:Named(KEYGUARD) private val mediaHost: MediaHost,
-    private val bypassController: KeyguardBypassController,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val context: Context,
-    private val secureSettings: SecureSettings,
-    @Main private val handler: Handler,
-    configurationController: ConfigurationController,
-) {
-
-    init {
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onStateChanged(newState: Int) {
-                refreshMediaPosition()
-            }
-        })
-        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateResources()
-            }
-        })
-
-        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?) {
-                if (uri == lockScreenMediaPlayerUri) {
-                    allowMediaPlayerOnLockScreen =
-                            secureSettings.getBoolForUser(
-                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                                    true,
-                                    UserHandle.USER_CURRENT
-                            )
-                    refreshMediaPosition()
-                }
-            }
-        }
-        secureSettings.registerContentObserverForUser(
-                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                settingsObserver,
-                UserHandle.USER_ALL)
-
-        // First let's set the desired state that we want for this host
-        mediaHost.expansion = MediaHostState.EXPANDED
-        mediaHost.showsOnlyActiveMedia = true
-        mediaHost.falsingProtectionNeeded = true
-
-        // Let's now initialize this view, which also creates the host view for us.
-        mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
-        updateResources()
-    }
-
-    private fun updateResources() {
-        useSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
-    }
-
-    @VisibleForTesting
-    var useSplitShade = false
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            reattachHostView()
-            refreshMediaPosition()
-        }
-
-    /**
-     * Is the media player visible?
-     */
-    var visible = false
-        private set
-
-    var visibilityChangedListener: ((Boolean) -> Unit)? = null
-
-    /**
-     * single pane media container placed at the top of the notifications list
-     */
-    var singlePaneContainer: MediaContainerView? = null
-        private set
-    private var splitShadeContainer: ViewGroup? = null
-
-    /**
-     * Track the media player setting status on lock screen.
-     */
-    private var allowMediaPlayerOnLockScreen: Boolean = true
-    private val lockScreenMediaPlayerUri =
-            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
-
-    /**
-     * Attaches media container in single pane mode, situated at the top of the notifications list
-     */
-    fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
-        val needsListener = singlePaneContainer == null
-        singlePaneContainer = mediaView
-        if (needsListener) {
-            // On reinflation we don't want to add another listener
-            mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
-        }
-        reattachHostView()
-        onMediaHostVisibilityChanged(mediaHost.visible)
-    }
-
-    /**
-     * Called whenever the media hosts visibility changes
-     */
-    private fun onMediaHostVisibilityChanged(visible: Boolean) {
-        refreshMediaPosition()
-        if (visible) {
-            mediaHost.hostView.layoutParams.apply {
-                height = ViewGroup.LayoutParams.WRAP_CONTENT
-                width = ViewGroup.LayoutParams.MATCH_PARENT
-            }
-        }
-    }
-
-    /**
-     * Attaches media container in split shade mode, situated to the left of notifications
-     */
-    fun attachSplitShadeContainer(container: ViewGroup) {
-        splitShadeContainer = container
-        reattachHostView()
-        refreshMediaPosition()
-    }
-
-    private fun reattachHostView() {
-        val inactiveContainer: ViewGroup?
-        val activeContainer: ViewGroup?
-        if (useSplitShade) {
-            activeContainer = splitShadeContainer
-            inactiveContainer = singlePaneContainer
-        } else {
-            inactiveContainer = splitShadeContainer
-            activeContainer = singlePaneContainer
-        }
-        if (inactiveContainer?.childCount == 1) {
-            inactiveContainer.removeAllViews()
-        }
-        if (activeContainer?.childCount == 0) {
-            // Detach the hostView from its parent view if exists
-            mediaHost.hostView.parent?.let {
-                (it as? ViewGroup)?.removeView(mediaHost.hostView)
-            }
-            activeContainer.addView(mediaHost.hostView)
-        }
-    }
-
-    fun refreshMediaPosition() {
-        val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
-        // mediaHost.visible required for proper animations handling
-        visible = mediaHost.visible &&
-                !bypassController.bypassEnabled &&
-                keyguardOrUserSwitcher &&
-                allowMediaPlayerOnLockScreen
-        if (visible) {
-            showMediaPlayer()
-        } else {
-            hideMediaPlayer()
-        }
-    }
-
-    private fun showMediaPlayer() {
-        if (useSplitShade) {
-            setVisibility(splitShadeContainer, View.VISIBLE)
-            setVisibility(singlePaneContainer, View.GONE)
-        } else {
-            setVisibility(singlePaneContainer, View.VISIBLE)
-            setVisibility(splitShadeContainer, View.GONE)
-        }
-    }
-
-    private fun hideMediaPlayer() {
-        // always hide splitShadeContainer as it's initially visible and may influence layout
-        setVisibility(splitShadeContainer, View.GONE)
-        setVisibility(singlePaneContainer, View.GONE)
-    }
-
-    private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
-        val previousVisibility = view?.visibility
-        view?.visibility = newVisibility
-        if (previousVisibility != newVisibility) {
-            visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
deleted file mode 100644
index 711cb36..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
-import android.content.res.Resources
-import android.content.res.TypedArray
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.ColorFilter
-import android.graphics.Outline
-import android.graphics.Paint
-import android.graphics.PixelFormat
-import android.graphics.RadialGradient
-import android.graphics.Rect
-import android.graphics.Shader
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.util.MathUtils.lerp
-import androidx.annotation.Keep
-import com.android.internal.graphics.ColorUtils
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import org.xmlpull.v1.XmlPullParser
-
-private const val RIPPLE_ANIM_DURATION = 800L
-private const val RIPPLE_DOWN_PROGRESS = 0.05f
-private const val RIPPLE_CANCEL_DURATION = 200L
-private val GRADIENT_STOPS = floatArrayOf(0.2f, 1f)
-
-private data class RippleData(
-    var x: Float,
-    var y: Float,
-    var alpha: Float,
-    var progress: Float,
-    var minSize: Float,
-    var maxSize: Float,
-    var highlight: Float
-)
-
-/**
- * Drawable that can draw an animated gradient when tapped.
- */
-@Keep
-class LightSourceDrawable : Drawable() {
-
-    private var pressed = false
-    private var themeAttrs: IntArray? = null
-    private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f)
-    private var paint = Paint()
-
-    var highlightColor = Color.WHITE
-    set(value) {
-        if (field == value) {
-            return
-        }
-        field = value
-        invalidateSelf()
-    }
-
-    /**
-     * Draw a small highlight under the finger before expanding (or cancelling) it.
-     */
-    private var active: Boolean = false
-        set(value) {
-            if (value == field) {
-                return
-            }
-            field = value
-
-            if (value) {
-                rippleAnimation?.cancel()
-                rippleData.alpha = 1f
-                rippleData.progress = RIPPLE_DOWN_PROGRESS
-            } else {
-                rippleAnimation?.cancel()
-                rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
-                    duration = RIPPLE_CANCEL_DURATION
-                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                    addUpdateListener {
-                        rippleData.alpha = it.animatedValue as Float
-                        invalidateSelf()
-                    }
-                    addListener(object : AnimatorListenerAdapter() {
-                        var cancelled = false
-                        override fun onAnimationCancel(animation: Animator?) {
-                            cancelled = true
-                        }
-
-                        override fun onAnimationEnd(animation: Animator?) {
-                            if (cancelled) {
-                                return
-                            }
-                            rippleData.progress = 0f
-                            rippleData.alpha = 0f
-                            rippleAnimation = null
-                            invalidateSelf()
-                        }
-                    })
-                    start()
-                }
-            }
-            invalidateSelf()
-        }
-
-    private var rippleAnimation: Animator? = null
-
-    /**
-     * Draw background and gradient.
-     */
-    override fun draw(canvas: Canvas) {
-        val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
-        val centerColor =
-                ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
-        paint.shader = RadialGradient(rippleData.x, rippleData.y, radius,
-                intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP)
-        canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
-    }
-
-    override fun getOutline(outline: Outline) {
-        // No bounds, parent will clip it
-    }
-
-    override fun getOpacity(): Int {
-        return PixelFormat.TRANSPARENT
-    }
-
-    override fun inflate(
-        r: Resources,
-        parser: XmlPullParser,
-        attrs: AttributeSet,
-        theme: Resources.Theme?
-    ) {
-        val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
-        themeAttrs = a.extractThemeAttrs()
-        updateStateFromTypedArray(a)
-        a.recycle()
-    }
-
-    private fun updateStateFromTypedArray(a: TypedArray) {
-        if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) {
-            rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f)
-        }
-        if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) {
-            rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
-        }
-        if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
-            rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
-                    100f
-        }
-    }
-
-    override fun canApplyTheme(): Boolean {
-        return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
-    }
-
-    override fun applyTheme(t: Resources.Theme) {
-        super.applyTheme(t)
-        themeAttrs?.let {
-            val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
-            updateStateFromTypedArray(a)
-            a.recycle()
-        }
-    }
-
-    override fun setColorFilter(p0: ColorFilter?) {
-        throw UnsupportedOperationException("Color filters are not supported")
-    }
-
-    override fun setAlpha(alpha: Int) {
-        if (alpha == paint.alpha) {
-            return
-        }
-
-        paint.alpha = alpha
-        invalidateSelf()
-    }
-
-    /**
-     * Draws an animated ripple that expands fading away.
-     */
-    private fun illuminate() {
-        rippleData.alpha = 1f
-        invalidateSelf()
-
-        rippleAnimation?.cancel()
-        rippleAnimation = AnimatorSet().apply {
-            playTogether(ValueAnimator.ofFloat(1f, 0f).apply {
-                startDelay = 133
-                duration = RIPPLE_ANIM_DURATION - startDelay
-                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                addUpdateListener {
-                    rippleData.alpha = it.animatedValue as Float
-                    invalidateSelf()
-                }
-            }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
-                duration = RIPPLE_ANIM_DURATION
-                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                addUpdateListener {
-                    rippleData.progress = it.animatedValue as Float
-                    invalidateSelf()
-                }
-            })
-            addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
-                    rippleData.progress = 0f
-                    rippleAnimation = null
-                    invalidateSelf()
-                }
-            })
-            start()
-        }
-    }
-
-    override fun setHotspot(x: Float, y: Float) {
-        rippleData.x = x
-        rippleData.y = y
-        if (active) {
-            invalidateSelf()
-        }
-    }
-
-    override fun isStateful(): Boolean {
-        return true
-    }
-
-    override fun hasFocusStateSpecified(): Boolean {
-        return true
-    }
-
-    override fun isProjected(): Boolean {
-        return true
-    }
-
-    override fun getDirtyBounds(): Rect {
-        val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
-        val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(),
-                (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt())
-        bounds.union(super.getDirtyBounds())
-        return bounds
-    }
-
-    override fun onStateChange(stateSet: IntArray?): Boolean {
-        val changed = super.onStateChange(stateSet)
-        if (stateSet == null) {
-            return changed
-        }
-
-        val wasPressed = pressed
-        var enabled = false
-        pressed = false
-        var focused = false
-        var hovered = false
-
-        for (state in stateSet) {
-            when (state) {
-                com.android.internal.R.attr.state_enabled -> {
-                    enabled = true
-                }
-                com.android.internal.R.attr.state_focused -> {
-                    focused = true
-                }
-                com.android.internal.R.attr.state_pressed -> {
-                    pressed = true
-                }
-                com.android.internal.R.attr.state_hovered -> {
-                    hovered = true
-                }
-            }
-        }
-
-        active = enabled && (pressed || focused || hovered)
-        if (wasPressed && !pressed) {
-            illuminate()
-        }
-
-        return changed
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
deleted file mode 100644
index 94a0835..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.media.InfoMediaManager
-import com.android.settingslib.media.LocalMediaManager
-
-import javax.inject.Inject
-
-/**
- * Factory to create [LocalMediaManager] objects.
- */
-class LocalMediaManagerFactory @Inject constructor(
-    private val context: Context,
-    private val localBluetoothManager: LocalBluetoothManager?
-) {
-    /** Creates a [LocalMediaManager] for the given package. */
-    fun create(packageName: String): LocalMediaManager {
-        return InfoMediaManager(context, packageName, null, localBluetoothManager).run {
-            LocalMediaManager(context, localBluetoothManager, this, packageName)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
deleted file mode 100644
index aca033e..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
-
-import javax.inject.Inject;
-
-/**
- * Testable wrapper around {@link MediaBrowser} constructor
- */
-public class MediaBrowserFactory {
-    private final Context mContext;
-
-    @Inject
-    public MediaBrowserFactory(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Creates a new MediaBrowser
-     *
-     * @param serviceComponent
-     * @param callback
-     * @param rootHints
-     * @return
-     */
-    public MediaBrowser create(ComponentName serviceComponent,
-            MediaBrowser.ConnectionCallback callback, Bundle rootHints) {
-        return new MediaBrowser(mContext, serviceComponent, callback, rootHints);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
deleted file mode 100644
index 5977ed0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ /dev/null
@@ -1,1193 +0,0 @@
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
-import android.util.Log
-import android.util.MathUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.animation.PathInterpolator
-import android.widget.LinearLayout
-import androidx.annotation.VisibleForTesting
-import com.android.internal.logging.InstanceId
-import com.android.systemui.Dumpable
-import com.android.systemui.R
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.PageIndicator
-import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.Utils
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.animation.requiresRemeasuring
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.util.traceSection
-import java.io.PrintWriter
-import java.util.TreeMap
-import javax.inject.Inject
-import javax.inject.Provider
-
-private const val TAG = "MediaCarouselController"
-private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
-private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
-
-/**
- * Class that is responsible for keeping the view carousel up to date.
- * This also handles changes in state and applies them to the media carousel like the expansion.
- */
-@SysUISingleton
-class MediaCarouselController @Inject constructor(
-    private val context: Context,
-    private val mediaControlPanelFactory: Provider<MediaControlPanel>,
-    private val visualStabilityProvider: VisualStabilityProvider,
-    private val mediaHostStatesManager: MediaHostStatesManager,
-    private val activityStarter: ActivityStarter,
-    private val systemClock: SystemClock,
-    @Main executor: DelayableExecutor,
-    private val mediaManager: MediaDataManager,
-    configurationController: ConfigurationController,
-    falsingCollector: FalsingCollector,
-    falsingManager: FalsingManager,
-    dumpManager: DumpManager,
-    private val logger: MediaUiEventLogger,
-    private val debugLogger: MediaCarouselControllerLogger
-) : Dumpable {
-    /**
-     * The current width of the carousel
-     */
-    private var currentCarouselWidth: Int = 0
-
-    /**
-     * The current height of the carousel
-     */
-    private var currentCarouselHeight: Int = 0
-
-    /**
-     * Are we currently showing only active players
-     */
-    private var currentlyShowingOnlyActive: Boolean = false
-
-    /**
-     * Is the player currently visible (at the end of the transformation
-     */
-    private var playersVisible: Boolean = false
-    /**
-     * The desired location where we'll be at the end of the transformation. Usually this matches
-     * the end location, except when we're still waiting on a state update call.
-     */
-    @MediaLocation
-    private var desiredLocation: Int = -1
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    @VisibleForTesting
-    var currentEndLocation: Int = -1
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    private var currentStartLocation: Int = -1
-
-    /**
-     * The progress of the transition or 1.0 if there is no transition happening
-     */
-    private var currentTransitionProgress: Float = 1.0f
-
-    /**
-     * The measured width of the carousel
-     */
-    private var carouselMeasureWidth: Int = 0
-
-    /**
-     * The measured height of the carousel
-     */
-    private var carouselMeasureHeight: Int = 0
-    private var desiredHostState: MediaHostState? = null
-    private val mediaCarousel: MediaScrollView
-    val mediaCarouselScrollHandler: MediaCarouselScrollHandler
-    val mediaFrame: ViewGroup
-    @VisibleForTesting
-    lateinit var settingsButton: View
-        private set
-    private val mediaContent: ViewGroup
-    @VisibleForTesting
-    val pageIndicator: PageIndicator
-    private val visualStabilityCallback: OnReorderingAllowedListener
-    private var needsReordering: Boolean = false
-    private var keysNeedRemoval = mutableSetOf<String>()
-    var shouldScrollToKey: Boolean = false
-    private var isRtl: Boolean = false
-        set(value) {
-            if (value != field) {
-                field = value
-                mediaFrame.layoutDirection =
-                        if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
-                mediaCarouselScrollHandler.scrollToStart()
-            }
-        }
-    private var currentlyExpanded = true
-        set(value) {
-            if (field != value) {
-                field = value
-                for (player in MediaPlayerData.players()) {
-                    player.setListening(field)
-                }
-            }
-        }
-
-    companion object {
-        const val ANIMATION_BASE_DURATION = 2200f
-        const val DURATION = 167f
-        const val DETAILS_DELAY = 1067f
-        const val CONTROLS_DELAY = 1400f
-        const val PAGINATION_DELAY = 1900f
-        const val MEDIATITLES_DELAY = 1000f
-        const val MEDIACONTAINERS_DELAY = 967f
-        val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F)
-        val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F)
-
-        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
-            val transformStartFraction = delay / ANIMATION_BASE_DURATION
-            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
-            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
-            return MathUtils.constrain((squishinessToTime - transformStartFraction) /
-                    transformDurationFraction, 0F, 1F)
-        }
-    }
-
-    private val configListener = object : ConfigurationController.ConfigurationListener {
-        override fun onDensityOrFontScaleChanged() {
-            // System font changes should only happen when UMO is offscreen or a flicker may occur
-            updatePlayers(recreateMedia = true)
-            inflateSettingsButton()
-        }
-
-        override fun onThemeChanged() {
-            updatePlayers(recreateMedia = false)
-            inflateSettingsButton()
-        }
-
-        override fun onConfigChanged(newConfig: Configuration?) {
-            if (newConfig == null) return
-            isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
-        }
-
-        override fun onUiModeChanged() {
-            updatePlayers(recreateMedia = false)
-            inflateSettingsButton()
-        }
-    }
-
-    /**
-     * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
-     * It will be called when the container is out of view.
-     */
-    lateinit var updateUserVisibility: () -> Unit
-    lateinit var updateHostVisibility: () -> Unit
-
-    private val isReorderingAllowed: Boolean
-        get() = visualStabilityProvider.isReorderingAllowed
-
-    init {
-        dumpManager.registerDumpable(TAG, this)
-        mediaFrame = inflateMediaCarousel()
-        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
-        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
-        mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
-                executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
-                this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression,
-                logger)
-        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
-        inflateSettingsButton()
-        mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
-        configurationController.addCallback(configListener)
-        visualStabilityCallback = OnReorderingAllowedListener {
-            if (needsReordering) {
-                needsReordering = false
-                reorderAllPlayers(previousVisiblePlayerKey = null)
-            }
-
-            keysNeedRemoval.forEach {
-                removePlayer(it)
-            }
-            if (keysNeedRemoval.size > 0) {
-                // Carousel visibility may need to be updated after late removals
-                updateHostVisibility()
-            }
-            keysNeedRemoval.clear()
-
-            // Update user visibility so that no extra impression will be logged when
-            // activeMediaIndex resets to 0
-            if (this::updateUserVisibility.isInitialized) {
-                updateUserVisibility()
-            }
-
-            // Let's reset our scroll position
-            mediaCarouselScrollHandler.scrollToStart()
-        }
-        visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
-        mediaManager.addListener(object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(
-                key: String,
-                oldKey: String?,
-                data: MediaData,
-                immediately: Boolean,
-                receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
-            ) {
-                debugLogger.logMediaLoaded(key)
-                if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
-                    // Log card received if a new resumable media card is added
-                    MediaPlayerData.getMediaPlayer(key)?.let {
-                        /* ktlint-disable max-line-length */
-                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                it.mSmartspaceId,
-                                it.mUid,
-                                surfaces = intArrayOf(
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                rank = MediaPlayerData.getMediaPlayerIndex(key))
-                        /* ktlint-disable max-line-length */
-                    }
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            mediaCarouselScrollHandler.visibleMediaIndex
-                            == MediaPlayerData.getMediaPlayerIndex(key)) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                } else if (receivedSmartspaceCardLatency != 0) {
-                    // Log resume card received if resumable media card is reactivated and
-                    // resume card is ranked first
-                    MediaPlayerData.players().forEachIndexed { index, it ->
-                        if (it.recommendationViewHolder == null) {
-                            it.mSmartspaceId = SmallHash.hash(it.mUid +
-                                    systemClock.currentTimeMillis().toInt())
-                            it.mIsImpressed = false
-                            /* ktlint-disable max-line-length */
-                            logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                    it.mSmartspaceId,
-                                    it.mUid,
-                                    surfaces = intArrayOf(
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                    rank = index,
-                                    receivedLatencyMillis = receivedSmartspaceCardLatency)
-                            /* ktlint-disable max-line-length */
-                        }
-                    }
-                    // If media container area already visible to the user, log impression for
-                    // reactivated card.
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            !mediaCarouselScrollHandler.qsExpanded) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                }
-
-                val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
-                if (canRemove && !Utils.useMediaResumption(context)) {
-                    // This view isn't playing, let's remove this! This happens e.g when
-                    // dismissing/timing out a view. We still have the data around because
-                    // resumption could be on, but we should save the resources and release this.
-                    if (isReorderingAllowed) {
-                        onMediaDataRemoved(key)
-                    } else {
-                        keysNeedRemoval.add(key)
-                    }
-                } else {
-                    keysNeedRemoval.remove(key)
-                }
-            }
-
-            override fun onSmartspaceMediaDataLoaded(
-                key: String,
-                data: SmartspaceMediaData,
-                shouldPrioritize: Boolean
-            ) {
-                debugLogger.logRecommendationLoaded(key)
-                // Log the case where the hidden media carousel with the existed inactive resume
-                // media is shown by the Smartspace signal.
-                if (data.isActive) {
-                    val hasActivatedExistedResumeMedia =
-                            !mediaManager.hasActiveMedia() &&
-                                    mediaManager.hasAnyMedia() &&
-                                    shouldPrioritize
-                    if (hasActivatedExistedResumeMedia) {
-                        // Log resume card received if resumable media card is reactivated and
-                        // recommendation card is valid and ranked first
-                        MediaPlayerData.players().forEachIndexed { index, it ->
-                            if (it.recommendationViewHolder == null) {
-                                it.mSmartspaceId = SmallHash.hash(it.mUid +
-                                        systemClock.currentTimeMillis().toInt())
-                                it.mIsImpressed = false
-                                /* ktlint-disable max-line-length */
-                                logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                        it.mSmartspaceId,
-                                        it.mUid,
-                                        surfaces = intArrayOf(
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                        rank = index,
-                                        receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
-                                /* ktlint-disable max-line-length */
-                            }
-                        }
-                    }
-                    addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
-                    MediaPlayerData.getMediaPlayer(key)?.let {
-                        /* ktlint-disable max-line-length */
-                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                it.mSmartspaceId,
-                                it.mUid,
-                                surfaces = intArrayOf(
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                rank = MediaPlayerData.getMediaPlayerIndex(key),
-                                receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
-                        /* ktlint-disable max-line-length */
-                    }
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            mediaCarouselScrollHandler.visibleMediaIndex
-                            == MediaPlayerData.getMediaPlayerIndex(key)) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                } else {
-                    onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
-                }
-            }
-
-            override fun onMediaDataRemoved(key: String) {
-                debugLogger.logMediaRemoved(key)
-                removePlayer(key)
-            }
-
-            override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-                debugLogger.logRecommendationRemoved(key, immediately)
-                if (immediately || isReorderingAllowed) {
-                    removePlayer(key)
-                    if (!immediately) {
-                        // Although it wasn't requested, we were able to process the removal
-                        // immediately since reordering is allowed. So, notify hosts to update
-                        if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
-                            updateHostVisibility()
-                        }
-                    }
-                } else {
-                    keysNeedRemoval.add(key)
-                }
-            }
-        })
-        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
-            // The pageIndicator is not laid out yet when we get the current state update,
-            // Lets make sure we have the right dimensions
-            updatePageIndicatorLocation()
-        }
-        mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
-            override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
-                if (location == desiredLocation) {
-                    onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
-                }
-            }
-        })
-    }
-
-    private fun inflateSettingsButton() {
-        val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
-                mediaFrame, false) as View
-        if (this::settingsButton.isInitialized) {
-            mediaFrame.removeView(settingsButton)
-        }
-        settingsButton = settings
-        mediaFrame.addView(settingsButton)
-        mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
-        settingsButton.setOnClickListener {
-            logger.logCarouselSettings()
-            activityStarter.startActivity(settingsIntent, true /* dismissShade */)
-        }
-    }
-
-    private fun inflateMediaCarousel(): ViewGroup {
-        val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
-                UniqueObjectHostView(context), false) as ViewGroup
-        // Because this is inflated when not attached to the true view hierarchy, it resolves some
-        // potential issues to force that the layout direction is defined by the locale
-        // (rather than inherited from the parent, which would resolve to LTR when unattached).
-        mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-        return mediaCarousel
-    }
-
-    private fun reorderAllPlayers(
-            previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
-            key: String? = null
-    ) {
-        mediaContent.removeAllViews()
-        for (mediaPlayer in MediaPlayerData.players()) {
-            mediaPlayer.mediaViewHolder?.let {
-                mediaContent.addView(it.player)
-            } ?: mediaPlayer.recommendationViewHolder?.let {
-                mediaContent.addView(it.recommendations)
-            }
-        }
-        mediaCarouselScrollHandler.onPlayersChanged()
-        MediaPlayerData.updateVisibleMediaPlayers()
-        // Automatically scroll to the active player if needed
-        if (shouldScrollToKey) {
-            shouldScrollToKey = false
-            val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
-            if (mediaIndex != -1) {
-                previousVisiblePlayerKey?.let {
-                    val previousVisibleIndex = MediaPlayerData.playerKeys()
-                            .indexOfFirst { key -> it == key }
-                    mediaCarouselScrollHandler
-                            .scrollToPlayer(previousVisibleIndex, mediaIndex)
-                } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
-            }
-        }
-    }
-
-    // Returns true if new player is added
-    private fun addOrUpdatePlayer(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        isSsReactivated: Boolean
-    ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") {
-        MediaPlayerData.moveIfExists(oldKey, key)
-        val existingPlayer = MediaPlayerData.getMediaPlayer(key)
-        val curVisibleMediaKey = MediaPlayerData.visiblePlayerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-        if (existingPlayer == null) {
-            val newPlayer = mediaControlPanelFactory.get()
-            newPlayer.attachPlayer(MediaViewHolder.create(
-                    LayoutInflater.from(context), mediaContent))
-            newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-            val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT)
-            newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-            newPlayer.bindPlayer(data, key)
-            newPlayer.setListening(currentlyExpanded)
-            MediaPlayerData.addMediaPlayer(
-                key, data, newPlayer, systemClock, isSsReactivated, debugLogger
-            )
-            updatePlayerToState(newPlayer, noAnimation = true)
-            // Media data added from a recommendation card should starts playing.
-            if ((shouldScrollToKey && data.isPlaying == true) ||
-                    (!shouldScrollToKey && data.active)) {
-                reorderAllPlayers(curVisibleMediaKey, key)
-            } else {
-                needsReordering = true
-            }
-        } else {
-            existingPlayer.bindPlayer(data, key)
-            MediaPlayerData.addMediaPlayer(
-                key, data, existingPlayer, systemClock, isSsReactivated, debugLogger
-            )
-            val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
-            // In case of recommendations hits.
-            // Check the playing status of media player and the package name.
-            // To make sure we scroll to the right app's media player.
-            if (isReorderingAllowed ||
-                    shouldScrollToKey &&
-                    data.isPlaying == true &&
-                    packageName == data.packageName
-            ) {
-                reorderAllPlayers(curVisibleMediaKey, key)
-            } else {
-                needsReordering = true
-            }
-        }
-        updatePageIndicator()
-        mediaCarouselScrollHandler.onPlayersChanged()
-        mediaFrame.requiresRemeasuring = true
-        // Check postcondition: mediaContent should have the same number of children as there are
-        // elements in mediaPlayers.
-        if (MediaPlayerData.players().size != mediaContent.childCount) {
-            Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
-        }
-        return existingPlayer == null
-    }
-
-    private fun addSmartspaceMediaRecommendations(
-        key: String,
-        data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
-    ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
-        if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
-        if (MediaPlayerData.getMediaPlayer(key) != null) {
-            Log.w(TAG, "Skip adding smartspace target in carousel")
-            return
-        }
-
-        val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
-        existingSmartspaceMediaKey?.let {
-            val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true)
-            removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) }
-        }
-
-        val newRecs = mediaControlPanelFactory.get()
-        newRecs.attachRecommendation(
-                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
-        newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-        val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.WRAP_CONTENT)
-        newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
-        newRecs.bindRecommendation(data)
-        val curVisibleMediaKey = MediaPlayerData.visiblePlayerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-        MediaPlayerData.addMediaRecommendation(
-            key, data, newRecs, shouldPrioritize, systemClock, debugLogger
-        )
-        updatePlayerToState(newRecs, noAnimation = true)
-        reorderAllPlayers(curVisibleMediaKey)
-        updatePageIndicator()
-        mediaFrame.requiresRemeasuring = true
-        // Check postcondition: mediaContent should have the same number of children as there are
-        // elements in mediaPlayers.
-        if (MediaPlayerData.players().size != mediaContent.childCount) {
-            Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
-        }
-    }
-
-    fun removePlayer(
-        key: String,
-        dismissMediaData: Boolean = true,
-        dismissRecommendation: Boolean = true
-    ) {
-        if (key == MediaPlayerData.smartspaceMediaKey()) {
-            MediaPlayerData.smartspaceMediaData?.let {
-                logger.logRecommendationRemoved(it.packageName, it.instanceId)
-            }
-        }
-        val removed = MediaPlayerData.removeMediaPlayer(
-                key,
-                dismissMediaData || dismissRecommendation
-        )
-        removed?.apply {
-            mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
-            mediaContent.removeView(removed.mediaViewHolder?.player)
-            mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
-            removed.onDestroy()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            updatePageIndicator()
-
-            if (dismissMediaData) {
-                // Inform the media manager of a potentially late dismissal
-                mediaManager.dismissMediaData(key, delay = 0L)
-            }
-            if (dismissRecommendation) {
-                // Inform the media manager of a potentially late dismissal
-                mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
-            }
-        }
-    }
-
-    private fun updatePlayers(recreateMedia: Boolean) {
-        pageIndicator.tintList = ColorStateList.valueOf(
-            context.getColor(R.color.media_paging_indicator)
-        )
-
-        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
-            if (isSsMediaRec) {
-                val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
-                removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
-                smartspaceMediaData?.let {
-                    addSmartspaceMediaRecommendations(
-                            it.targetId, it, MediaPlayerData.shouldPrioritizeSs)
-                }
-            } else {
-                val isSsReactivated = MediaPlayerData.isSsReactivated(key)
-                if (recreateMedia) {
-                    removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
-                }
-                addOrUpdatePlayer(
-                        key = key, oldKey = null, data = data, isSsReactivated = isSsReactivated)
-            }
-        }
-    }
-
-    private fun updatePageIndicator() {
-        val numPages = mediaContent.getChildCount()
-        pageIndicator.setNumPages(numPages)
-        if (numPages == 1) {
-            pageIndicator.setLocation(0f)
-        }
-        updatePageIndicatorAlpha()
-    }
-
-    /**
-     * Set a new interpolated state for all players. This is a state that is usually controlled
-     * by a finger movement where the user drags from one state to the next.
-     *
-     * @param startLocation the start location of our state or -1 if this is directly set
-     * @param endLocation the ending location of our state.
-     * @param progress the progress of the transition between startLocation and endlocation. If
-     *                 this is not a guided transformation, this will be 1.0f
-     * @param immediately should this state be applied immediately, canceling all animations?
-     */
-    fun setCurrentState(
-        @MediaLocation startLocation: Int,
-        @MediaLocation endLocation: Int,
-        progress: Float,
-        immediately: Boolean
-    ) {
-        if (startLocation != currentStartLocation ||
-                endLocation != currentEndLocation ||
-                progress != currentTransitionProgress ||
-                immediately
-        ) {
-            currentStartLocation = startLocation
-            currentEndLocation = endLocation
-            currentTransitionProgress = progress
-            for (mediaPlayer in MediaPlayerData.players()) {
-                updatePlayerToState(mediaPlayer, immediately)
-            }
-            maybeResetSettingsCog()
-            updatePageIndicatorAlpha()
-        }
-    }
-
-    @VisibleForTesting
-    fun updatePageIndicatorAlpha() {
-        val hostStates = mediaHostStatesManager.mediaHostStates
-        val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
-        val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
-        val startAlpha = if (startIsVisible) 1.0f else 0.0f
-        // when squishing in split shade, only use endState, which keeps changing
-        // to provide squishFraction
-        val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
-        val endAlpha = (if (endIsVisible) 1.0f else 0.0f) *
-                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
-        var alpha = 1.0f
-        if (!endIsVisible || !startIsVisible) {
-            var progress = currentTransitionProgress
-            if (!endIsVisible) {
-                progress = 1.0f - progress
-            }
-            // Let's fade in quickly at the end where the view is visible
-            progress = MathUtils.constrain(
-                    MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress),
-                    0.0f,
-                    1.0f)
-            alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
-        }
-        pageIndicator.alpha = alpha
-    }
-
-    private fun updatePageIndicatorLocation() {
-        // Update the location of the page indicator, carousel clipping
-        val translationX = if (isRtl) {
-            (pageIndicator.width - currentCarouselWidth) / 2.0f
-        } else {
-            (currentCarouselWidth - pageIndicator.width) / 2.0f
-        }
-        pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
-        val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
-        pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
-                layoutParams.bottomMargin).toFloat()
-    }
-
-    /**
-     * Update the dimension of this carousel.
-     */
-    private fun updateCarouselDimensions() {
-        var width = 0
-        var height = 0
-        for (mediaPlayer in MediaPlayerData.players()) {
-            val controller = mediaPlayer.mediaViewController
-            // When transitioning the view to gone, the view gets smaller, but the translation
-            // Doesn't, let's add the translation
-            width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
-            height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
-        }
-        if (width != currentCarouselWidth || height != currentCarouselHeight) {
-            currentCarouselWidth = width
-            currentCarouselHeight = height
-            mediaCarouselScrollHandler.setCarouselBounds(
-                    currentCarouselWidth, currentCarouselHeight)
-            updatePageIndicatorLocation()
-            updatePageIndicatorAlpha()
-        }
-    }
-
-    private fun maybeResetSettingsCog() {
-        val hostStates = mediaHostStatesManager.mediaHostStates
-        val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
-                ?: true
-        val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
-                ?: endShowsActive
-        if (currentlyShowingOnlyActive != endShowsActive ||
-                ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
-                        startShowsActive != endShowsActive)) {
-            // Whenever we're transitioning from between differing states or the endstate differs
-            // we reset the translation
-            currentlyShowingOnlyActive = endShowsActive
-            mediaCarouselScrollHandler.resetTranslation(animate = true)
-        }
-    }
-
-    private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
-        mediaPlayer.mediaViewController.setCurrentState(
-                startLocation = currentStartLocation,
-                endLocation = currentEndLocation,
-                transitionProgress = currentTransitionProgress,
-                applyImmediately = noAnimation)
-    }
-
-    /**
-     * The desired location of this view has changed. We should remeasure the view to match
-     * the new bounds and kick off bounds animations if necessary.
-     * If an animation is happening, an animation is kicked of externally, which sets a new
-     * current state until we reach the targetState.
-     *
-     * @param desiredLocation the location we're going to
-     * @param desiredHostState the target state we're transitioning to
-     * @param animate should this be animated
-     */
-    fun onDesiredLocationChanged(
-        desiredLocation: Int,
-        desiredHostState: MediaHostState?,
-        animate: Boolean,
-        duration: Long = 200,
-        startDelay: Long = 0
-    ) = traceSection("MediaCarouselController#onDesiredLocationChanged") {
-        desiredHostState?.let {
-            if (this.desiredLocation != desiredLocation) {
-                // Only log an event when location changes
-                logger.logCarouselPosition(desiredLocation)
-            }
-
-            // This is a hosting view, let's remeasure our players
-            this.desiredLocation = desiredLocation
-            this.desiredHostState = it
-            currentlyExpanded = it.expansion > 0
-
-            val shouldCloseGuts = !currentlyExpanded &&
-                    !mediaManager.hasActiveMediaOrRecommendation() &&
-                    desiredHostState.showsOnlyActiveMedia
-
-            for (mediaPlayer in MediaPlayerData.players()) {
-                if (animate) {
-                    mediaPlayer.mediaViewController.animatePendingStateChange(
-                            duration = duration,
-                            delay = startDelay)
-                }
-                if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
-                    mediaPlayer.closeGuts(!animate)
-                }
-
-                mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
-            }
-            mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
-            mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
-            val nowVisible = it.visible
-            if (nowVisible != playersVisible) {
-                playersVisible = nowVisible
-                if (nowVisible) {
-                    mediaCarouselScrollHandler.resetTranslation()
-                }
-            }
-            updateCarouselSize()
-        }
-    }
-
-    fun closeGuts(immediate: Boolean = true) {
-        MediaPlayerData.players().forEach {
-            it.closeGuts(immediate)
-        }
-    }
-
-    /**
-     * Update the size of the carousel, remeasuring it if necessary.
-     */
-    private fun updateCarouselSize() {
-        val width = desiredHostState?.measurementInput?.width ?: 0
-        val height = desiredHostState?.measurementInput?.height ?: 0
-        if (width != carouselMeasureWidth && width != 0 ||
-                height != carouselMeasureHeight && height != 0) {
-            carouselMeasureWidth = width
-            carouselMeasureHeight = height
-            val playerWidthPlusPadding = carouselMeasureWidth +
-                    context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
-            // Let's remeasure the carousel
-            val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
-            val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
-            mediaCarousel.measure(widthSpec, heightSpec)
-            mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
-            // Update the padding after layout; view widths are used in RTL to calculate scrollX
-            mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
-        }
-    }
-
-    /**
-     * Log the user impression for media card at visibleMediaIndex.
-     */
-    fun logSmartspaceImpression(qsExpanded: Boolean) {
-        val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
-        if (MediaPlayerData.players().size > visibleMediaIndex) {
-            val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
-            val hasActiveMediaOrRecommendationCard =
-                    MediaPlayerData.hasActiveMediaOrRecommendationCard()
-            if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
-                // Skip logging if on LS or QQS, and there is no active media card
-                return
-            }
-            mediaControlPanel?.let {
-                logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
-                        it.mSmartspaceId,
-                        it.mUid,
-                        intArrayOf(it.surfaceForSmartspaceLogging))
-                it.mIsImpressed = true
-            }
-        }
-    }
-
-    @JvmOverloads
-    /**
-     * Log Smartspace events
-     *
-     * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
-     * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
-     * instanceId
-     * @param uid uid for the application that media comes from
-     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
-     * the event happened
-     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
-     * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
-     * @param interactedSubcardCardinality how many media items were shown to the user when there
-     * is user interaction
-     * @param rank the rank for media card in the media carousel, starting from 0
-     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
-     * between headphone connection to sysUI displays media recommendation card
-     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
-     *
-     */
-    fun logSmartspaceCardReported(
-        eventId: Int,
-        instanceId: Int,
-        uid: Int,
-        surfaces: IntArray,
-        interactedSubcardRank: Int = 0,
-        interactedSubcardCardinality: Int = 0,
-        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
-        receivedLatencyMillis: Int = 0,
-        isSwipeToDismiss: Boolean = false
-    ) {
-        if (MediaPlayerData.players().size <= rank) {
-            return
-        }
-
-        val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
-        // Only log media resume card when Smartspace data is available
-        if (!mediaControlKey.isSsMediaRec &&
-                !mediaManager.smartspaceMediaData.isActive &&
-                MediaPlayerData.smartspaceMediaData == null) {
-            return
-        }
-
-        val cardinality = mediaContent.getChildCount()
-        surfaces.forEach { surface ->
-            /* ktlint-disable max-line-length */
-            SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
-                    eventId,
-                    instanceId,
-                    // Deprecated, replaced with AiAi feature type so we don't need to create logging
-                    // card type for each new feature.
-                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
-                    surface,
-                    // Use -1 as rank value to indicate user swipe to dismiss the card
-                    if (isSwipeToDismiss) -1 else rank,
-                    cardinality,
-                    if (mediaControlKey.isSsMediaRec)
-                        15 // MEDIA_RECOMMENDATION
-                    else if (mediaControlKey.isSsReactivated)
-                        43 // MEDIA_RESUME_SS_ACTIVATED
-                    else
-                        31, // MEDIA_RESUME
-                    uid,
-                    interactedSubcardRank,
-                    interactedSubcardCardinality,
-                    receivedLatencyMillis,
-                    null, // Media cards cannot have subcards.
-                    null // Media cards don't have dimensions today.
-            )
-            /* ktlint-disable max-line-length */
-            if (DEBUG) {
-                Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" +
-                        " surface: $surface rank: $rank cardinality: $cardinality " +
-                        "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
-                        "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
-                        "uid: $uid " +
-                        "interactedSubcardRank: $interactedSubcardRank " +
-                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
-                        "received_latency_millis: $receivedLatencyMillis")
-            }
-        }
-    }
-
-    private fun onSwipeToDismiss() {
-        MediaPlayerData.players().forEachIndexed {
-            index, it ->
-            if (it.mIsImpressed) {
-                logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
-                        it.mSmartspaceId,
-                        it.mUid,
-                        intArrayOf(it.surfaceForSmartspaceLogging),
-                        rank = index,
-                        isSwipeToDismiss = true)
-                // Reset card impressed state when swipe to dismissed
-                it.mIsImpressed = false
-            }
-        }
-        logger.logSwipeDismiss()
-        mediaManager.onSwipeToDismiss()
-    }
-
-    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
-        return MediaPlayerData.playerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("keysNeedRemoval: $keysNeedRemoval")
-            println("dataKeys: ${MediaPlayerData.dataKeys()}")
-            println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
-            println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
-            println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
-            println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
-            println("current size: $currentCarouselWidth x $currentCarouselHeight")
-            println("location: $desiredLocation")
-            println("state: ${desiredHostState?.expansion}, " +
-                "only active ${desiredHostState?.showsOnlyActiveMedia}")
-        }
-    }
-}
-
-@VisibleForTesting
-internal object MediaPlayerData {
-    private val EMPTY = MediaData(
-            userId = -1,
-            initialized = false,
-            app = null,
-            appIcon = null,
-            artist = null,
-            song = null,
-            artwork = null,
-            actions = emptyList(),
-            actionsToShowInCompact = emptyList(),
-            packageName = "INVALID",
-            token = null,
-            clickIntent = null,
-            device = null,
-            active = true,
-            resumeAction = null,
-            instanceId = InstanceId.fakeInstanceId(-1),
-            appUid = -1)
-    // Whether should prioritize Smartspace card.
-    internal var shouldPrioritizeSs: Boolean = false
-        private set
-    internal var smartspaceMediaData: SmartspaceMediaData? = null
-        private set
-
-    data class MediaSortKey(
-        val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
-        val data: MediaData,
-        val key: String,
-        val updateTime: Long = 0,
-        val isSsReactivated: Boolean = false
-    )
-
-    private val comparator = compareByDescending<MediaSortKey> {
-            it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL }
-        .thenByDescending {
-            it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL }
-        .thenByDescending { it.data.active }
-        .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
-        .thenByDescending { !it.data.resumption }
-        .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
-        .thenByDescending { it.data.lastActive }
-        .thenByDescending { it.updateTime }
-        .thenByDescending { it.data.notificationKey }
-
-    private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
-    private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
-    // A map that tracks order of visible media players before they get reordered.
-    private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
-
-    fun addMediaPlayer(
-        key: String,
-        data: MediaData,
-        player: MediaControlPanel,
-        clock: SystemClock,
-        isSsReactivated: Boolean,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        val removedPlayer = removeMediaPlayer(key)
-        if (removedPlayer != null && removedPlayer != player) {
-            debugLogger?.logPotentialMemoryLeak(key)
-        }
-        val sortKey = MediaSortKey(isSsMediaRec = false,
-                data, key, clock.currentTimeMillis(), isSsReactivated = isSsReactivated)
-        mediaData.put(key, sortKey)
-        mediaPlayers.put(sortKey, player)
-        visibleMediaPlayers.put(key, sortKey)
-    }
-
-    fun addMediaRecommendation(
-        key: String,
-        data: SmartspaceMediaData,
-        player: MediaControlPanel,
-        shouldPrioritize: Boolean,
-        clock: SystemClock,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        shouldPrioritizeSs = shouldPrioritize
-        val removedPlayer = removeMediaPlayer(key)
-        if (removedPlayer != null && removedPlayer != player) {
-            debugLogger?.logPotentialMemoryLeak(key)
-        }
-        val sortKey = MediaSortKey(
-            isSsMediaRec = true,
-            EMPTY.copy(isPlaying = false),
-            key,
-            clock.currentTimeMillis(),
-            isSsReactivated = true
-        )
-        mediaData.put(key, sortKey)
-        mediaPlayers.put(sortKey, player)
-        visibleMediaPlayers.put(key, sortKey)
-        smartspaceMediaData = data
-    }
-
-    fun moveIfExists(
-        oldKey: String?,
-        newKey: String,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        if (oldKey == null || oldKey == newKey) {
-            return
-        }
-
-        mediaData.remove(oldKey)?.let {
-            // MediaPlayer should not be visible
-            // no need to set isDismissed flag.
-            val removedPlayer = removeMediaPlayer(newKey)
-            removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
-            mediaData.put(newKey, it)
-        }
-    }
-
-    fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
-        return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
-    }
-
-    fun getMediaPlayer(key: String): MediaControlPanel? {
-        return mediaData.get(key)?.let { mediaPlayers.get(it) }
-    }
-
-    fun getMediaPlayerIndex(key: String): Int {
-        val sortKey = mediaData.get(key)
-        mediaPlayers.entries.forEachIndexed { index, e ->
-            if (e.key == sortKey) {
-                return index
-            }
-        }
-        return -1
-    }
-
-    /**
-     * Removes media player given the key.
-     * @param isDismissed determines whether the media player is removed from the carousel.
-     */
-    fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = mediaData.remove(key)?.let {
-        if (it.isSsMediaRec) {
-            smartspaceMediaData = null
-        }
-        if (isDismissed) {
-            visibleMediaPlayers.remove(key)
-        }
-        mediaPlayers.remove(it)
-    }
-
-    fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
-
-    fun dataKeys() = mediaData.keys
-
-    fun players() = mediaPlayers.values
-
-    fun playerKeys() = mediaPlayers.keys
-
-    fun visiblePlayerKeys() = visibleMediaPlayers.values
-
-    /** Returns the index of the first non-timeout media. */
-    fun firstActiveMediaIndex(): Int {
-        mediaPlayers.entries.forEachIndexed { index, e ->
-            if (!e.key.isSsMediaRec && e.key.data.active) {
-                return index
-            }
-        }
-        return -1
-    }
-
-    /** Returns the existing Smartspace target id. */
-    fun smartspaceMediaKey(): String? {
-        mediaData.entries.forEach { e ->
-            if (e.value.isSsMediaRec) {
-                return e.key
-            }
-        }
-        return null
-    }
-
-    @VisibleForTesting
-    fun clear() {
-        mediaData.clear()
-        mediaPlayers.clear()
-        visibleMediaPlayers.clear()
-    }
-
-    /* Returns true if there is active media player card or recommendation card */
-    fun hasActiveMediaOrRecommendationCard(): Boolean {
-        if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
-            return true
-        }
-        if (firstActiveMediaIndex() != -1) {
-            return true
-        }
-        return false
-    }
-
-    fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
-
-    /**
-     * This method is called when media players are reordered.
-     * To make sure we have the new version of the order of
-     * media players visible to user.
-     */
-    fun updateVisibleMediaPlayers() {
-        visibleMediaPlayers.clear()
-        playerKeys().forEach {
-            visibleMediaPlayers.put(it.key, it)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
deleted file mode 100644
index b1018f9..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
+++ /dev/null
@@ -1,76 +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.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaCarouselControllerLog
-import javax.inject.Inject
-
-/** A debug logger for [MediaCarouselController]. */
-@SysUISingleton
-class MediaCarouselControllerLogger @Inject constructor(
-    @MediaCarouselControllerLog private val buffer: LogBuffer
-) {
-    /**
-     * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
-     * [MediaViewController] related to [key].
-     */
-    fun logPotentialMemoryLeak(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        {
-            "Potential memory leak: " +
-                    "Removing control panel for $str1 from map without calling #onDestroy"
-        }
-    )
-
-    fun logMediaLoaded(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "add player $str1" }
-    )
-
-    fun logMediaRemoved(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "removing player $str1" }
-    )
-
-    fun logRecommendationLoaded(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "add recommendation $str1" }
-    )
-
-    fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = immediately
-        },
-        { "removing recommendation $str1, immediate=$bool1" }
-    )
-}
-
-private const val TAG = "MediaCarouselCtlrLog"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
deleted file mode 100644
index a776897..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
+++ /dev/null
@@ -1,601 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.graphics.Outline
-import android.util.MathUtils
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewOutlineProvider
-import androidx.core.view.GestureDetectorCompat
-import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.SpringForce
-import com.android.settingslib.Utils
-import com.android.systemui.Gefingerpoken
-import com.android.systemui.R
-import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.PageIndicator
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.wm.shell.animation.PhysicsAnimator
-
-private const val FLING_SLOP = 1000000
-private const val DISMISS_DELAY = 100L
-private const val SCROLL_DELAY = 100L
-private const val RUBBERBAND_FACTOR = 0.2f
-private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
-
-/**
- * Default spring configuration to use for animations where stiffness and/or damping ratio
- * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
- */
-private val translationConfig = PhysicsAnimator.SpringConfig(
-        SpringForce.STIFFNESS_LOW,
-        SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-
-/**
- * A controller class for the media scrollview, responsible for touch handling
- */
-class MediaCarouselScrollHandler(
-    private val scrollView: MediaScrollView,
-    private val pageIndicator: PageIndicator,
-    private val mainExecutor: DelayableExecutor,
-    val dismissCallback: () -> Unit,
-    private var translationChangedListener: () -> Unit,
-    private val closeGuts: (immediate: Boolean) -> Unit,
-    private val falsingCollector: FalsingCollector,
-    private val falsingManager: FalsingManager,
-    private val logSmartspaceImpression: (Boolean) -> Unit,
-    private val logger: MediaUiEventLogger
-) {
-    /**
-     * Is the view in RTL
-     */
-    val isRtl: Boolean get() = scrollView.isLayoutRtl
-    /**
-     * Do we need falsing protection?
-     */
-    var falsingProtectionNeeded: Boolean = false
-    /**
-     * The width of the carousel
-     */
-    private var carouselWidth: Int = 0
-
-    /**
-     * The height of the carousel
-     */
-    private var carouselHeight: Int = 0
-
-    /**
-     * How much are we scrolled into the current media?
-     */
-    private var cornerRadius: Int = 0
-
-    /**
-     * The content where the players are added
-     */
-    private var mediaContent: ViewGroup
-    /**
-     * The gesture detector to detect touch gestures
-     */
-    private val gestureDetector: GestureDetectorCompat
-
-    /**
-     * The settings button view
-     */
-    private lateinit var settingsButton: View
-
-    /**
-     * What's the currently visible player index?
-     */
-    var visibleMediaIndex: Int = 0
-        private set
-
-    /**
-     * How much are we scrolled into the current media?
-     */
-    private var scrollIntoCurrentMedia: Int = 0
-
-    /**
-     * how much is the content translated in X
-     */
-    var contentTranslation = 0.0f
-        private set(value) {
-            field = value
-            mediaContent.translationX = value
-            updateSettingsPresentation()
-            translationChangedListener.invoke()
-            updateClipToOutline()
-        }
-
-    /**
-     * The width of a player including padding
-     */
-    var playerWidthPlusPadding: Int = 0
-        set(value) {
-            field = value
-            // The player width has changed, let's update the scroll position to make sure
-            // it's still at the same place
-            var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding
-            if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
-                newRelativeScroll += playerWidthPlusPadding -
-                        (scrollIntoCurrentMedia - playerWidthPlusPadding)
-            } else {
-                newRelativeScroll += scrollIntoCurrentMedia
-            }
-            scrollView.relativeScrollX = newRelativeScroll
-        }
-
-    /**
-     * Does the dismiss currently show the setting cog?
-     */
-    var showsSettingsButton: Boolean = false
-
-    /**
-     * A utility to detect gestures, used in the touch listener
-     */
-    private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
-        override fun onFling(
-            eStart: MotionEvent?,
-            eCurrent: MotionEvent?,
-            vX: Float,
-            vY: Float
-        ) = onFling(vX, vY)
-
-        override fun onScroll(
-            down: MotionEvent?,
-            lastMotion: MotionEvent?,
-            distanceX: Float,
-            distanceY: Float
-        ) = onScroll(down!!, lastMotion!!, distanceX)
-
-        override fun onDown(e: MotionEvent?): Boolean {
-            if (falsingProtectionNeeded) {
-                falsingCollector.onNotificationStartDismissing()
-            }
-            return false
-        }
-    }
-
-    /**
-     * The touch listener for the scroll view
-     */
-    private val touchListener = object : Gefingerpoken {
-        override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
-        override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
-    }
-
-    /**
-     * A listener that is invoked when the scrolling changes to update player visibilities
-     */
-    private val scrollChangedListener = object : View.OnScrollChangeListener {
-        override fun onScrollChange(
-            v: View?,
-            scrollX: Int,
-            scrollY: Int,
-            oldScrollX: Int,
-            oldScrollY: Int
-        ) {
-            if (playerWidthPlusPadding == 0) {
-                return
-            }
-
-            val relativeScrollX = scrollView.relativeScrollX
-            onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding,
-                    relativeScrollX % playerWidthPlusPadding)
-        }
-    }
-
-    /**
-     * Whether the media card is visible to user if any
-     */
-    var visibleToUser: Boolean = false
-
-    /**
-     * Whether the quick setting is expanded or not
-     */
-    var qsExpanded: Boolean = false
-
-    init {
-        gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener)
-        scrollView.touchListener = touchListener
-        scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
-        mediaContent = scrollView.contentContainer
-        scrollView.setOnScrollChangeListener(scrollChangedListener)
-        scrollView.outlineProvider = object : ViewOutlineProvider() {
-            override fun getOutline(view: View?, outline: Outline?) {
-                outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat())
-            }
-        }
-    }
-
-    fun onSettingsButtonUpdated(button: View) {
-        settingsButton = button
-        // We don't have a context to resolve, lets use the settingsbuttons one since that is
-        // reinflated appropriately
-        cornerRadius = settingsButton.resources.getDimensionPixelSize(
-                Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius))
-        updateSettingsPresentation()
-        scrollView.invalidateOutline()
-    }
-
-    private fun updateSettingsPresentation() {
-        if (showsSettingsButton && settingsButton.width > 0) {
-            val settingsOffset = MathUtils.map(
-                    0.0f,
-                    getMaxTranslation().toFloat(),
-                    0.0f,
-                    1.0f,
-                    Math.abs(contentTranslation))
-            val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width *
-                    SETTINGS_BUTTON_TRANSLATION_FRACTION
-            val newTranslationX = if (isRtl) {
-                // In RTL, the 0-placement is on the right side of the view, not the left...
-                if (contentTranslation > 0) {
-                    -(scrollView.width - settingsTranslation - settingsButton.width)
-                } else {
-                    -settingsTranslation
-                }
-            } else {
-                if (contentTranslation > 0) {
-                    settingsTranslation
-                } else {
-                    scrollView.width - settingsTranslation - settingsButton.width
-                }
-            }
-            val rotation = (1.0f - settingsOffset) * 50
-            settingsButton.rotation = rotation * -Math.signum(contentTranslation)
-            val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset))
-            settingsButton.alpha = alpha
-            settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE
-            settingsButton.translationX = newTranslationX
-            settingsButton.translationY = (scrollView.height - settingsButton.height) / 2.0f
-        } else {
-            settingsButton.visibility = View.INVISIBLE
-        }
-    }
-
-    private fun onTouch(motionEvent: MotionEvent): Boolean {
-        val isUp = motionEvent.action == MotionEvent.ACTION_UP
-        if (isUp && falsingProtectionNeeded) {
-            falsingCollector.onNotificationStopDismissing()
-        }
-        if (gestureDetector.onTouchEvent(motionEvent)) {
-            if (isUp) {
-                // If this is an up and we're flinging, we don't want to have this touch reach
-                // the view, otherwise that would scroll, while we are trying to snap to the
-                // new page. Let's dispatch a cancel instead.
-                scrollView.cancelCurrentScroll()
-                return true
-            } else {
-                // Pass touches to the scrollView
-                return false
-            }
-        }
-        if (motionEvent.action == MotionEvent.ACTION_MOVE) {
-            // cancel on going animation if there is any.
-            PhysicsAnimator.getInstance(this).cancel()
-        } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
-            // It's an up and the fling didn't take it above
-            val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
-            val scrollXAmount: Int
-            if (relativePos > playerWidthPlusPadding / 2) {
-                scrollXAmount = playerWidthPlusPadding - relativePos
-            } else {
-                scrollXAmount = -1 * relativePos
-            }
-            if (scrollXAmount != 0) {
-                val dx = if (isRtl) -scrollXAmount else scrollXAmount
-                val newScrollX = scrollView.relativeScrollX + dx
-                // Delay the scrolling since scrollView calls springback which cancels
-                // the animation again..
-                mainExecutor.execute {
-                    scrollView.smoothScrollTo(newScrollX, scrollView.scrollY)
-                }
-            }
-            val currentTranslation = scrollView.getContentTranslation()
-            if (currentTranslation != 0.0f) {
-                // We started a Swipe but didn't end up with a fling. Let's either go to the
-                // dismissed position or go back.
-                val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 ||
-                        isFalseTouch()
-                val newTranslation: Float
-                if (springBack) {
-                    newTranslation = 0.0f
-                } else {
-                    newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
-                    if (!showsSettingsButton) {
-                        // Delay the dismiss a bit to avoid too much overlap. Waiting until the
-                        // animation has finished also feels a bit too slow here.
-                        mainExecutor.executeDelayed({
-                            dismissCallback.invoke()
-                        }, DISMISS_DELAY)
-                    }
-                }
-                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
-                scrollView.animationTargetX = newTranslation
-            }
-        }
-        // Always pass touches to the scrollView
-        return false
-    }
-
-    private fun isFalseTouch() = falsingProtectionNeeded &&
-            falsingManager.isFalseTouch(NOTIFICATION_DISMISS)
-
-    private fun getMaxTranslation() = if (showsSettingsButton) {
-            settingsButton.width
-        } else {
-            playerWidthPlusPadding
-        }
-
-    private fun onInterceptTouch(motionEvent: MotionEvent): Boolean {
-        return gestureDetector.onTouchEvent(motionEvent)
-    }
-
-    fun onScroll(
-        down: MotionEvent,
-        lastMotion: MotionEvent,
-        distanceX: Float
-    ): Boolean {
-        val totalX = lastMotion.x - down.x
-        val currentTranslation = scrollView.getContentTranslation()
-        if (currentTranslation != 0.0f ||
-                !scrollView.canScrollHorizontally((-totalX).toInt())) {
-            var newTranslation = currentTranslation - distanceX
-            val absTranslation = Math.abs(newTranslation)
-            if (absTranslation > getMaxTranslation()) {
-                // Rubberband all translation above the maximum
-                if (Math.signum(distanceX) != Math.signum(currentTranslation)) {
-                    // The movement is in the same direction as our translation,
-                    // Let's rubberband it.
-                    if (Math.abs(currentTranslation) > getMaxTranslation()) {
-                        // we were already overshooting before. Let's add the distance
-                        // fully rubberbanded.
-                        newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
-                    } else {
-                        // We just crossed the boundary, let's rubberband it all
-                        newTranslation = Math.signum(newTranslation) * (getMaxTranslation() +
-                                (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
-                    }
-                } // Otherwise we don't have do do anything, and will remove the unrubberbanded
-                // translation
-            }
-            if (Math.signum(newTranslation) != Math.signum(currentTranslation) &&
-                    currentTranslation != 0.0f) {
-                // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
-                // to scroll into the new direction
-                if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
-                    // We can actually scroll in the direction where we want to translate,
-                    // Let's make sure to stop at 0
-                    newTranslation = 0.0f
-                }
-            }
-            val physicsAnimator = PhysicsAnimator.getInstance(this)
-            if (physicsAnimator.isRunning()) {
-                physicsAnimator.spring(CONTENT_TRANSLATION,
-                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
-            } else {
-                contentTranslation = newTranslation
-            }
-            scrollView.animationTargetX = newTranslation
-            return true
-        }
-        return false
-    }
-
-    private fun onFling(
-        vX: Float,
-        vY: Float
-    ): Boolean {
-        if (vX * vX < 0.5 * vY * vY) {
-            return false
-        }
-        if (vX * vX < FLING_SLOP) {
-            return false
-        }
-        val currentTranslation = scrollView.getContentTranslation()
-        if (currentTranslation != 0.0f) {
-            // We're translated and flung. Let's see if the fling is in the same direction
-            val newTranslation: Float
-            if (Math.signum(vX) != Math.signum(currentTranslation) || isFalseTouch()) {
-                // The direction of the fling isn't the same as the translation, let's go to 0
-                newTranslation = 0.0f
-            } else {
-                newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
-                // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
-                // has finished also feels a bit too slow here.
-                if (!showsSettingsButton) {
-                    mainExecutor.executeDelayed({
-                        dismissCallback.invoke()
-                    }, DISMISS_DELAY)
-                }
-            }
-            PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                    newTranslation, startVelocity = vX, config = translationConfig).start()
-            scrollView.animationTargetX = newTranslation
-        } else {
-            // We're flinging the player! Let's go either to the previous or to the next player
-            val pos = scrollView.relativeScrollX
-            val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
-            val flungTowardEnd = if (isRtl) vX > 0 else vX < 0
-            var destIndex = if (flungTowardEnd) currentIndex + 1 else currentIndex
-            destIndex = Math.max(0, destIndex)
-            destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
-            val view = mediaContent.getChildAt(destIndex)
-            // We need to post this since we're dispatching a touch to the underlying view to cancel
-            // but canceling will actually abort the animation.
-            mainExecutor.execute {
-                scrollView.smoothScrollTo(view.left, scrollView.scrollY)
-            }
-        }
-        return true
-    }
-
-    /**
-     * Reset the translation of the players when swiped
-     */
-    fun resetTranslation(animate: Boolean = false) {
-        if (scrollView.getContentTranslation() != 0.0f) {
-            if (animate) {
-                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                        0.0f, config = translationConfig).start()
-                scrollView.animationTargetX = 0.0f
-            } else {
-                PhysicsAnimator.getInstance(this).cancel()
-                contentTranslation = 0.0f
-            }
-        }
-    }
-
-    private fun updateClipToOutline() {
-        val clip = contentTranslation != 0.0f || scrollIntoCurrentMedia != 0
-        scrollView.clipToOutline = clip
-    }
-
-    private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
-        val wasScrolledIn = scrollIntoCurrentMedia != 0
-        scrollIntoCurrentMedia = scrollInAmount
-        val nowScrolledIn = scrollIntoCurrentMedia != 0
-        if (newIndex != visibleMediaIndex || wasScrolledIn != nowScrolledIn) {
-            val oldIndex = visibleMediaIndex
-            visibleMediaIndex = newIndex
-            if (oldIndex != visibleMediaIndex && visibleToUser) {
-                logSmartspaceImpression(qsExpanded)
-                logger.logMediaCarouselPage(newIndex)
-            }
-            closeGuts(false)
-            updatePlayerVisibilities()
-        }
-        val relativeLocation = visibleMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
-            scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
-        // Fix the location, because PageIndicator does not handle RTL internally
-        val location = if (isRtl) {
-            mediaContent.childCount - relativeLocation - 1
-        } else {
-            relativeLocation
-        }
-        pageIndicator.setLocation(location)
-        updateClipToOutline()
-    }
-
-    /**
-     * Notified whenever the players or their order has changed
-     */
-    fun onPlayersChanged() {
-        updatePlayerVisibilities()
-        updateMediaPaddings()
-    }
-
-    private fun updateMediaPaddings() {
-        val padding = scrollView.context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
-        val childCount = mediaContent.childCount
-        for (i in 0 until childCount) {
-            val mediaView = mediaContent.getChildAt(i)
-            val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
-            val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
-            if (layoutParams.marginEnd != desiredPaddingEnd) {
-                layoutParams.marginEnd = desiredPaddingEnd
-                mediaView.layoutParams = layoutParams
-            }
-        }
-    }
-
-    private fun updatePlayerVisibilities() {
-        val scrolledIn = scrollIntoCurrentMedia != 0
-        for (i in 0 until mediaContent.childCount) {
-            val view = mediaContent.getChildAt(i)
-            val visible = (i == visibleMediaIndex) || ((i == (visibleMediaIndex + 1)) && scrolledIn)
-            view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
-        }
-    }
-
-    /**
-     * Notify that a player will be removed right away. This gives us the opporunity to look
-     * where it was and update our scroll position.
-     */
-    fun onPrePlayerRemoved(removed: MediaControlPanel) {
-        val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player)
-        // If the removed index is less than the visibleMediaIndex, then we need to decrement it.
-        // RTL has no effect on this, because indices are always relative (start-to-end).
-        // Update the index 'manually' since we won't always get a call to onMediaScrollingChanged
-        val beforeActive = removedIndex <= visibleMediaIndex
-        if (beforeActive) {
-            visibleMediaIndex = Math.max(0, visibleMediaIndex - 1)
-        }
-        // If the removed media item is "left of" the active one (in an absolute sense), we need to
-        // scroll the view to keep that player in view.  This is because scroll position is always
-        // calculated from left to right.
-        val leftOfActive = if (isRtl) !beforeActive else beforeActive
-        if (leftOfActive) {
-            scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0)
-        }
-    }
-
-    /**
-     * Update the bounds of the carousel
-     */
-    fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
-        if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
-            carouselWidth = currentCarouselWidth
-            carouselHeight = currentCarouselHeight
-            scrollView.invalidateOutline()
-        }
-    }
-
-    /**
-     * Reset the MediaScrollView to the start.
-     */
-    fun scrollToStart() {
-        scrollView.relativeScrollX = 0
-    }
-
-    /**
-     * Smooth scroll to the destination player.
-     *
-     * @param sourceIndex optional source index to indicate where the scroll should begin.
-     * @param destIndex destination index to indicate where the scroll should end.
-     */
-    fun scrollToPlayer(sourceIndex: Int = -1, destIndex: Int) {
-        if (sourceIndex >= 0 && sourceIndex < mediaContent.childCount) {
-            scrollView.relativeScrollX = sourceIndex * playerWidthPlusPadding
-        }
-        val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
-        val view = mediaContent.getChildAt(destIndex)
-        // We need to post this to wait for the active player becomes visible.
-        mainExecutor.executeDelayed({
-            scrollView.smoothScrollTo(view.left, scrollView.scrollY)
-        }, SCROLL_DELAY)
-    }
-
-    companion object {
-        private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>(
-                "contentTranslation") {
-            override fun getValue(handler: MediaCarouselScrollHandler): Float {
-                return handler.contentTranslation
-            }
-
-            override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
-                handler.contentTranslation = value
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt
deleted file mode 100644
index 208766d..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt
+++ /dev/null
@@ -1,46 +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.media
-
-import com.android.systemui.monet.ColorScheme
-
-/** Returns the surface color for media controls based on the scheme. */
-internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2[9] // A2-800
-
-/** Returns the primary accent color for media controls based on the scheme. */
-internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1[2] // A1-100
-
-/** Returns the secondary accent color for media controls based on the scheme. */
-internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1[3] // A1-200
-
-/** Returns the primary text color for media controls based on the scheme. */
-internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1[1] // N1-50
-
-/** Returns the inverse of the primary text color for media controls based on the scheme. */
-internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1[10] // N1-900
-
-/** Returns the secondary text color for media controls based on the scheme. */
-internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2[3] // N2-200
-
-/** Returns the tertiary text color for media controls based on the scheme. */
-internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2[5] // N2-400
-
-/** Returns the color for the start of the background gradient based on the scheme. */
-internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2[8] // A2-700
-
-/** Returns the color for the end of the background gradient based on the scheme. */
-internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1[8] // A1-700
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
deleted file mode 100644
index fba51dd..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ /dev/null
@@ -1,1503 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
-
-import static com.android.systemui.media.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
-
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorSet;
-import android.app.PendingIntent;
-import android.app.WallpaperColors;
-import android.app.smartspace.SmartspaceAction;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Rect;
-import android.graphics.drawable.Animatable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.Icon;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.TransitionDrawable;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Process;
-import android.os.Trace;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Interpolator;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.constraintlayout.widget.ConstraintSet;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.InstanceId;
-import com.android.settingslib.widget.AdaptiveIcon;
-import com.android.systemui.ActivityIntentHelper;
-import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.bluetooth.BroadcastDialogController;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.monet.ColorScheme;
-import com.android.systemui.monet.Style;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.ColorUtilKt;
-import com.android.systemui.util.animation.TransitionLayout;
-import com.android.systemui.util.time.SystemClock;
-
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-
-import dagger.Lazy;
-import kotlin.Unit;
-
-/**
- * A view controller used for Media Playback.
- */
-public class MediaControlPanel {
-    protected static final String TAG = "MediaControlPanel";
-
-    private static final float DISABLED_ALPHA = 0.38f;
-    private static final String EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = "com.google"
-            + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity";
-    private static final String EXTRAS_SMARTSPACE_INTENT =
-            "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
-    private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
-    private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
-    protected static final String KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME";
-
-    // Event types logged by smartspace
-    private static final int SMARTSPACE_CARD_CLICK_EVENT = 760;
-    protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761;
-
-    private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
-
-    // Buttons to show in small player when using semantic actions
-    private static final List<Integer> SEMANTIC_ACTIONS_COMPACT = List.of(
-            R.id.actionPlayPause,
-            R.id.actionPrev,
-            R.id.actionNext
-    );
-
-    // Buttons that should get hidden when we're scrubbing (they will be replaced with the views
-    // showing scrubbing time)
-    private static final List<Integer> SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = List.of(
-            R.id.actionPrev,
-            R.id.actionNext
-    );
-
-    // Buttons to show in small player when using semantic actions
-    private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
-            R.id.actionPlayPause,
-            R.id.actionPrev,
-            R.id.actionNext,
-            R.id.action0,
-            R.id.action1
-    );
-
-    private final SeekBarViewModel mSeekBarViewModel;
-    private SeekBarObserver mSeekBarObserver;
-    protected final Executor mBackgroundExecutor;
-    private final Executor mMainExecutor;
-    private final ActivityStarter mActivityStarter;
-    private final BroadcastSender mBroadcastSender;
-
-    private Context mContext;
-    private MediaViewHolder mMediaViewHolder;
-    private RecommendationViewHolder mRecommendationViewHolder;
-    private String mKey;
-    private MediaData mMediaData;
-    private SmartspaceMediaData mRecommendationData;
-    private MediaViewController mMediaViewController;
-    private MediaSession.Token mToken;
-    private MediaController mController;
-    private Lazy<MediaDataManager> mMediaDataManagerLazy;
-    // Uid for the media app.
-    protected int mUid = Process.INVALID_UID;
-    private int mSmartspaceMediaItemsCount;
-    private MediaCarouselController mMediaCarouselController;
-    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
-    private final FalsingManager mFalsingManager;
-    private MetadataAnimationHandler mMetadataAnimationHandler;
-    private ColorSchemeTransition mColorSchemeTransition;
-    private Drawable mPrevArtwork = null;
-    private boolean mIsArtworkBound = false;
-    private int mArtworkBoundId = 0;
-    private int mArtworkNextBindRequestId = 0;
-
-    private final KeyguardStateController mKeyguardStateController;
-    private final ActivityIntentHelper mActivityIntentHelper;
-    private final NotificationLockscreenUserManager mLockscreenUserManager;
-
-    // Used for logging.
-    protected boolean mIsImpressed = false;
-    private SystemClock mSystemClock;
-    private MediaUiEventLogger mLogger;
-    private InstanceId mInstanceId;
-    protected int mSmartspaceId = -1;
-    private String mPackageName;
-
-    private boolean mIsScrubbing = false;
-    private boolean mIsSeekBarEnabled = false;
-
-    private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
-            this::setIsScrubbing;
-    private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
-            this::setIsSeekBarEnabled;
-
-    private final BroadcastDialogController mBroadcastDialogController;
-    private boolean mIsCurrentBroadcastedApp = false;
-    private boolean mShowBroadcastDialogButton = false;
-    private String mSwitchBroadcastApp;
-
-    /**
-     * Initialize a new control panel
-     *
-     * @param backgroundExecutor background executor, used for processing artwork
-     * @param mainExecutor main thread executor, used if we receive callbacks on the background
-     *                     thread that then trigger UI changes.
-     * @param activityStarter    activity starter
-     */
-    @Inject
-    public MediaControlPanel(
-            Context context,
-            @Background Executor backgroundExecutor,
-            @Main Executor mainExecutor,
-            ActivityStarter activityStarter,
-            BroadcastSender broadcastSender,
-            MediaViewController mediaViewController,
-            SeekBarViewModel seekBarViewModel,
-            Lazy<MediaDataManager> lazyMediaDataManager,
-            MediaOutputDialogFactory mediaOutputDialogFactory,
-            MediaCarouselController mediaCarouselController,
-            FalsingManager falsingManager,
-            SystemClock systemClock,
-            MediaUiEventLogger logger,
-            KeyguardStateController keyguardStateController,
-            ActivityIntentHelper activityIntentHelper,
-            NotificationLockscreenUserManager lockscreenUserManager,
-            BroadcastDialogController broadcastDialogController) {
-        mContext = context;
-        mBackgroundExecutor = backgroundExecutor;
-        mMainExecutor = mainExecutor;
-        mActivityStarter = activityStarter;
-        mBroadcastSender = broadcastSender;
-        mSeekBarViewModel = seekBarViewModel;
-        mMediaViewController = mediaViewController;
-        mMediaDataManagerLazy = lazyMediaDataManager;
-        mMediaOutputDialogFactory = mediaOutputDialogFactory;
-        mMediaCarouselController = mediaCarouselController;
-        mFalsingManager = falsingManager;
-        mSystemClock = systemClock;
-        mLogger = logger;
-        mKeyguardStateController = keyguardStateController;
-        mActivityIntentHelper = activityIntentHelper;
-        mLockscreenUserManager = lockscreenUserManager;
-        mBroadcastDialogController = broadcastDialogController;
-
-        mSeekBarViewModel.setLogSeek(() -> {
-            if (mPackageName != null && mInstanceId != null) {
-                mLogger.logSeek(mUid, mPackageName, mInstanceId);
-            }
-            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
-            return Unit.INSTANCE;
-        });
-    }
-
-    public void onDestroy() {
-        if (mSeekBarObserver != null) {
-            mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
-        }
-        mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
-        mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener);
-        mSeekBarViewModel.onDestroy();
-        mMediaViewController.onDestroy();
-    }
-
-    /**
-     * Get the view holder used to display media controls.
-     *
-     * @return the media view holder
-     */
-    @Nullable
-    public MediaViewHolder getMediaViewHolder() {
-        return mMediaViewHolder;
-    }
-
-    /**
-     * Get the recommendation view holder used to display Smartspace media recs.
-     * @return the recommendation view holder
-     */
-    @Nullable
-    public RecommendationViewHolder getRecommendationViewHolder() {
-        return mRecommendationViewHolder;
-    }
-
-    /**
-     * Get the view controller used to display media controls
-     *
-     * @return the media view controller
-     */
-    @NonNull
-    public MediaViewController getMediaViewController() {
-        return mMediaViewController;
-    }
-
-    /**
-     * Sets the listening state of the player.
-     * <p>
-     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
-     * unnecessary work when the QS panel is closed.
-     *
-     * @param listening True when player should be active. Otherwise, false.
-     */
-    public void setListening(boolean listening) {
-        mSeekBarViewModel.setListening(listening);
-    }
-
-    /** Sets whether the user is touching the seek bar to change the track position. */
-    private void setIsScrubbing(boolean isScrubbing) {
-        if (mMediaData == null || mMediaData.getSemanticActions() == null) {
-            return;
-        }
-        if (isScrubbing == this.mIsScrubbing) {
-            return;
-        }
-        this.mIsScrubbing = isScrubbing;
-        mMainExecutor.execute(() ->
-                updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
-    }
-
-    private void setIsSeekBarEnabled(boolean isSeekBarEnabled) {
-        if (isSeekBarEnabled == this.mIsSeekBarEnabled) {
-            return;
-        }
-        this.mIsSeekBarEnabled = isSeekBarEnabled;
-        updateSeekBarVisibility();
-    }
-
-    /**
-     * Get the context
-     *
-     * @return context
-     */
-    public Context getContext() {
-        return mContext;
-    }
-
-    /** Attaches the player to the player view holder. */
-    public void attachPlayer(MediaViewHolder vh) {
-        mMediaViewHolder = vh;
-        TransitionLayout player = vh.getPlayer();
-
-        mSeekBarObserver = new SeekBarObserver(vh);
-        mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
-        mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
-        mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
-        mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener);
-        mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
-
-        vh.getPlayer().setOnLongClickListener(v -> {
-            if (!mMediaViewController.isGutsVisible()) {
-                openGuts();
-                return true;
-            } else {
-                closeGuts();
-                return true;
-            }
-        });
-
-        // AlbumView uses a hardware layer so that clipping of the foreground is handled
-        // with clipping the album art. Otherwise album art shows through at the edges.
-        mMediaViewHolder.getAlbumView().setLayerType(View.LAYER_TYPE_HARDWARE, null);
-
-        TextView titleText = mMediaViewHolder.getTitleText();
-        TextView artistText = mMediaViewHolder.getArtistText();
-        AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
-                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText);
-        AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
-                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
-
-        mColorSchemeTransition = new ColorSchemeTransition(mContext, mMediaViewHolder);
-        mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
-    }
-
-    @VisibleForTesting
-    protected AnimatorSet loadAnimator(int animId, Interpolator motionInterpolator,
-            View... targets) {
-        ArrayList<Animator> animators = new ArrayList<>();
-        for (View target : targets) {
-            AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(mContext, animId);
-            animator.getChildAnimations().get(0).setInterpolator(motionInterpolator);
-            animator.setTarget(target);
-            animators.add(animator);
-        }
-
-        AnimatorSet result = new AnimatorSet();
-        result.playTogether(animators);
-        return result;
-    }
-
-    /** Attaches the recommendations to the recommendation view holder. */
-    public void attachRecommendation(RecommendationViewHolder vh) {
-        mRecommendationViewHolder = vh;
-        TransitionLayout recommendations = vh.getRecommendations();
-
-        mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
-
-        mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> {
-            if (!mMediaViewController.isGutsVisible()) {
-                openGuts();
-                return true;
-            } else {
-                closeGuts();
-                return true;
-            }
-        });
-    }
-
-    /** Bind this player view based on the data given. */
-    public void bindPlayer(@NonNull MediaData data, String key) {
-        if (mMediaViewHolder == null) {
-            return;
-        }
-        Trace.beginSection("MediaControlPanel#bindPlayer<" + key + ">");
-        mKey = key;
-        mMediaData = data;
-        MediaSession.Token token = data.getToken();
-        mPackageName = data.getPackageName();
-        mUid = data.getAppUid();
-        // Only assigns instance id if it's unassigned.
-        if (mSmartspaceId == -1) {
-            mSmartspaceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
-        }
-        mInstanceId = data.getInstanceId();
-
-        if (mToken == null || !mToken.equals(token)) {
-            mToken = token;
-        }
-
-        if (mToken != null) {
-            mController = new MediaController(mContext, mToken);
-        } else {
-            mController = null;
-        }
-
-        // Click action
-        PendingIntent clickIntent = data.getClickIntent();
-        if (clickIntent != null) {
-            mMediaViewHolder.getPlayer().setOnClickListener(v -> {
-                if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
-                if (mMediaViewController.isGutsVisible()) return;
-                mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
-                logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
-
-                // See StatusBarNotificationActivityStarter#onNotificationClicked
-                boolean showOverLockscreen = mKeyguardStateController.isShowing()
-                        && mActivityIntentHelper.wouldShowOverLockscreen(clickIntent.getIntent(),
-                        mLockscreenUserManager.getCurrentUserId());
-
-                if (showOverLockscreen) {
-                    mActivityStarter.startActivity(clickIntent.getIntent(),
-                            /* dismissShade */ true,
-                            /* animationController */ null,
-                            /* showOverLockscreenWhenLocked */ true);
-                } else {
-                    mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
-                            buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
-                }
-            });
-        }
-
-        // Seek Bar
-        final MediaController controller = getController();
-        mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
-
-        // Show the broadcast dialog button only when the le audio is enabled.
-        mShowBroadcastDialogButton =
-                data.getDevice() != null && data.getDevice().getShowBroadcastButton();
-        bindOutputSwitcherAndBroadcastButton(mShowBroadcastDialogButton, data);
-        bindGutsMenuForPlayer(data);
-        bindPlayerContentDescription(data);
-        bindScrubbingTime(data);
-        bindActionButtons(data);
-
-        boolean isSongUpdated = bindSongMetadata(data);
-        bindArtworkAndColors(data, key, isSongUpdated);
-
-        // TODO: We don't need to refresh this state constantly, only if the state actually changed
-        // to something which might impact the measurement
-        // State refresh interferes with the translation animation, only run it if it's not running.
-        if (!mMetadataAnimationHandler.isRunning()) {
-            mMediaViewController.refreshState();
-        }
-        Trace.endSection();
-    }
-
-    private void bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data) {
-        ViewGroup seamlessView = mMediaViewHolder.getSeamless();
-        seamlessView.setVisibility(View.VISIBLE);
-        ImageView iconView = mMediaViewHolder.getSeamlessIcon();
-        TextView deviceName = mMediaViewHolder.getSeamlessText();
-        final MediaDeviceData device = data.getDevice();
-
-        final boolean isTapEnabled;
-        final boolean useDisabledAlpha;
-        final int iconResource;
-        CharSequence deviceString;
-        if (showBroadcastButton) {
-            // TODO(b/233698402): Use the package name instead of app label to avoid the
-            // unexpected result.
-            mIsCurrentBroadcastedApp = device != null
-                    && TextUtils.equals(device.getName(),
-                    MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString(
-                            R.string.bt_le_audio_broadcast_dialog_unknown_name)));
-            useDisabledAlpha = !mIsCurrentBroadcastedApp;
-            // Always be enabled if the broadcast button is shown
-            isTapEnabled = true;
-
-            // Defaults for broadcasting state
-            deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
-            iconResource = R.drawable.settings_input_antenna;
-        } else {
-            // Disable clicking on output switcher for invalid devices and resumption controls
-            useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption();
-            isTapEnabled = !useDisabledAlpha;
-
-            // Defaults for non-broadcasting state
-            deviceString = mContext.getString(R.string.media_seamless_other_device);
-            iconResource = R.drawable.ic_media_home_devices;
-        }
-
-        mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f);
-        seamlessView.setEnabled(isTapEnabled);
-
-        if (device != null) {
-            Drawable icon = device.getIcon();
-            if (icon instanceof AdaptiveIcon) {
-                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
-                aIcon.setBackgroundColor(mColorSchemeTransition.getBgColor());
-                iconView.setImageDrawable(aIcon);
-            } else {
-                iconView.setImageDrawable(icon);
-            }
-            if (device.getName() != null) {
-                deviceString = device.getName();
-            }
-        } else {
-            // Set to default icon
-            iconView.setImageResource(iconResource);
-        }
-        deviceName.setText(deviceString);
-        seamlessView.setContentDescription(deviceString);
-        seamlessView.setOnClickListener(
-                v -> {
-                    if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                        return;
-                    }
-
-                    if (showBroadcastButton) {
-                        // If the current media app is not broadcasted and users press the outputer
-                        // button, we should pop up the broadcast dialog to check do they want to
-                        // switch broadcast to the other media app, otherwise we still pop up the
-                        // media output dialog.
-                        if (!mIsCurrentBroadcastedApp) {
-                            mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
-                            mSwitchBroadcastApp = device.getName().toString();
-                            mBroadcastDialogController.createBroadcastDialog(mSwitchBroadcastApp,
-                                    mPackageName, true, mMediaViewHolder.getSeamlessButton());
-                        } else {
-                            mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
-                            mMediaOutputDialogFactory.create(mPackageName, true,
-                                    mMediaViewHolder.getSeamlessButton());
-                        }
-                    } else {
-                        mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
-                        if (device.getIntent() != null) {
-                            if (device.getIntent().isActivity()) {
-                                mActivityStarter.startActivity(
-                                        device.getIntent().getIntent(), true);
-                            } else {
-                                try {
-                                    device.getIntent().send();
-                                } catch (PendingIntent.CanceledException e) {
-                                    Log.e(TAG, "Device pending intent was canceled");
-                                }
-                            }
-                        } else {
-                            mMediaOutputDialogFactory.create(mPackageName, true,
-                                    mMediaViewHolder.getSeamlessButton());
-                        }
-                    }
-                });
-    }
-
-    private void bindGutsMenuForPlayer(MediaData data) {
-        Runnable onDismissClickedRunnable = () -> {
-            if (mKey != null) {
-                closeGuts();
-                if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
-                        MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
-                    Log.w(TAG, "Manager failed to dismiss media " + mKey);
-                    // Remove directly from carousel so user isn't stuck with defunct controls
-                    mMediaCarouselController.removePlayer(mKey, false, false);
-                }
-            } else {
-                Log.w(TAG, "Dismiss media with null notification. Token uid="
-                        + data.getToken().getUid());
-            }
-        };
-
-        bindGutsMenuCommon(
-                /* isDismissible= */ data.isClearable(),
-                data.getApp(),
-                mMediaViewHolder.getGutsViewHolder(),
-                onDismissClickedRunnable);
-    }
-
-    private boolean bindSongMetadata(MediaData data) {
-        TextView titleText = mMediaViewHolder.getTitleText();
-        TextView artistText = mMediaViewHolder.getArtistText();
-        return mMetadataAnimationHandler.setNext(
-            Pair.create(data.getSong(), data.getArtist()),
-            () -> {
-                titleText.setText(data.getSong());
-                artistText.setText(data.getArtist());
-
-                // refreshState is required here to resize the text views (and prevent ellipsis)
-                mMediaViewController.refreshState();
-                return Unit.INSTANCE;
-            },
-            () -> {
-                // After finishing the enter animation, we refresh state. This could pop if
-                // something is incorrectly bound, but needs to be run if other elements were
-                // updated while the enter animation was running
-                mMediaViewController.refreshState();
-                return Unit.INSTANCE;
-            });
-    }
-
-    // We may want to look into unifying this with bindRecommendationContentDescription if/when we
-    // do a refactor of this class.
-    private void bindPlayerContentDescription(MediaData data) {
-        if (mMediaViewHolder == null) {
-            return;
-        }
-
-        CharSequence contentDescription;
-        if (mMediaViewController.isGutsVisible()) {
-            contentDescription = mMediaViewHolder.getGutsViewHolder().getGutsText().getText();
-        } else if (data != null) {
-            contentDescription = mContext.getString(
-                    R.string.controls_media_playing_item_description,
-                    data.getSong(),
-                    data.getArtist(),
-                    data.getApp());
-        } else {
-            contentDescription = null;
-        }
-        mMediaViewHolder.getPlayer().setContentDescription(contentDescription);
-    }
-
-    private void bindRecommendationContentDescription(SmartspaceMediaData data) {
-        if (mRecommendationViewHolder == null) {
-            return;
-        }
-
-       CharSequence contentDescription;
-        if (mMediaViewController.isGutsVisible()) {
-            contentDescription =
-                    mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
-        } else if (data != null) {
-            contentDescription = mContext.getString(
-                    R.string.controls_media_smartspace_rec_description,
-                    data.getAppName(mContext));
-        } else {
-            contentDescription = null;
-        }
-
-        mRecommendationViewHolder.getRecommendations().setContentDescription(contentDescription);
-    }
-
-    private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) {
-        final int traceCookie = data.hashCode();
-        final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">";
-        Trace.beginAsyncSection(traceName, traceCookie);
-
-        final int reqId = mArtworkNextBindRequestId++;
-        if (updateBackground) {
-            mIsArtworkBound = false;
-        }
-
-        // Capture width & height from views in foreground for artwork scaling in background
-        int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
-        int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
-
-        // WallpaperColors.fromBitmap takes a good amount of time. We do that work
-        // on the background executor to avoid stalling animations on the UI Thread.
-        mBackgroundExecutor.execute(() -> {
-            // Album art
-            ColorScheme mutableColorScheme = null;
-            Drawable artwork;
-            boolean isArtworkBound;
-            Icon artworkIcon = data.getArtwork();
-            WallpaperColors wallpaperColors = null;
-            if (artworkIcon != null) {
-                if (artworkIcon.getType() == Icon.TYPE_BITMAP
-                        || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
-                    // Avoids extra processing if this is already a valid bitmap
-                    wallpaperColors = WallpaperColors
-                            .fromBitmap(artworkIcon.getBitmap());
-                } else {
-                    Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
-                    if (artworkDrawable != null) {
-                        wallpaperColors = WallpaperColors
-                                .fromDrawable(artworkIcon.loadDrawable(mContext));
-                    }
-                }
-            }
-            if (wallpaperColors != null) {
-                mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
-                Drawable albumArt = getScaledBackground(artworkIcon, width, height);
-                GradientDrawable gradient = (GradientDrawable) mContext
-                        .getDrawable(R.drawable.qs_media_scrim);
-                gradient.setColors(new int[] {
-                        ColorUtilKt.getColorWithAlpha(
-                                MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
-                                0.25f),
-                        ColorUtilKt.getColorWithAlpha(
-                                MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
-                                0.9f),
-                });
-                artwork = new LayerDrawable(new Drawable[] { albumArt, gradient });
-                isArtworkBound = true;
-            } else {
-                // If there's no artwork, use colors from the app icon
-                artwork = new ColorDrawable(Color.TRANSPARENT);
-                isArtworkBound = false;
-                try {
-                    Drawable icon = mContext.getPackageManager()
-                            .getApplicationIcon(data.getPackageName());
-                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true,
-                            Style.CONTENT);
-                } catch (PackageManager.NameNotFoundException e) {
-                    Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
-                }
-            }
-
-            final ColorScheme colorScheme = mutableColorScheme;
-            mMainExecutor.execute(() -> {
-                // Cancel the request if a later one arrived first
-                if (reqId < mArtworkBoundId) {
-                    Trace.endAsyncSection(traceName, traceCookie);
-                    return;
-                }
-                mArtworkBoundId = reqId;
-
-                // Transition Colors to current color scheme
-                boolean colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme);
-
-                // Bind the album view to the artwork or a transition drawable
-                ImageView albumView = mMediaViewHolder.getAlbumView();
-                albumView.setPadding(0, 0, 0, 0);
-                if (updateBackground || colorSchemeChanged
-                        || (!mIsArtworkBound && isArtworkBound)) {
-                    if (mPrevArtwork == null) {
-                        albumView.setImageDrawable(artwork);
-                    } else {
-                        // Since we throw away the last transition, this'll pop if you backgrounds
-                        // are cycled too fast (or the correct background arrives very soon after
-                        // the metadata changes).
-                        TransitionDrawable transitionDrawable = new TransitionDrawable(
-                                new Drawable[]{mPrevArtwork, artwork});
-
-                        scaleTransitionDrawableLayer(transitionDrawable, 0, width, height);
-                        scaleTransitionDrawableLayer(transitionDrawable, 1, width, height);
-                        transitionDrawable.setLayerGravity(0, Gravity.CENTER);
-                        transitionDrawable.setLayerGravity(1, Gravity.CENTER);
-                        transitionDrawable.setCrossFadeEnabled(!isArtworkBound);
-
-                        albumView.setImageDrawable(transitionDrawable);
-                        transitionDrawable.startTransition(isArtworkBound ? 333 : 80);
-                    }
-                    mPrevArtwork = artwork;
-                    mIsArtworkBound = isArtworkBound;
-                }
-
-                // App icon - use notification icon
-                ImageView appIconView = mMediaViewHolder.getAppIcon();
-                appIconView.clearColorFilter();
-                if (data.getAppIcon() != null && !data.getResumption()) {
-                    appIconView.setImageIcon(data.getAppIcon());
-                    appIconView.setColorFilter(
-                            mColorSchemeTransition.getAccentPrimary().getTargetColor());
-                } else {
-                    // Resume players use launcher icon
-                    appIconView.setColorFilter(getGrayscaleFilter());
-                    try {
-                        Drawable icon = mContext.getPackageManager()
-                                .getApplicationIcon(data.getPackageName());
-                        appIconView.setImageDrawable(icon);
-                    } catch (PackageManager.NameNotFoundException e) {
-                        Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
-                        appIconView.setImageResource(R.drawable.ic_music_note);
-                    }
-                }
-                Trace.endAsyncSection(traceName, traceCookie);
-            });
-        });
-    }
-
-    private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
-            int targetWidth, int targetHeight) {
-        Drawable drawable = transitionDrawable.getDrawable(layer);
-        if (drawable == null) {
-            return;
-        }
-
-        int width = drawable.getIntrinsicWidth();
-        int height = drawable.getIntrinsicHeight();
-        if (width == 0 || height == 0 || targetWidth == 0 || targetHeight == 0) {
-            return;
-        }
-
-        float scale;
-        if ((width / (float) height) > (targetWidth / (float) targetHeight)) {
-            // Drawable is wider than target view, scale to match height
-            scale = targetHeight / (float) height;
-        } else {
-            // Drawable is taller than target view, scale to match width
-            scale = targetWidth / (float) width;
-        }
-        transitionDrawable.setLayerSize(layer, (int) (scale * width), (int) (scale * height));
-    }
-
-    private void bindActionButtons(MediaData data) {
-        MediaButton semanticActions = data.getSemanticActions();
-
-        List<ImageButton> genericButtons = new ArrayList<>();
-        for (int id : MediaViewHolder.Companion.getGenericButtonIds()) {
-            genericButtons.add(mMediaViewHolder.getAction(id));
-        }
-
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
-        if (semanticActions != null) {
-            // Hide all the generic buttons
-            for (ImageButton b: genericButtons) {
-                setVisibleAndAlpha(collapsedSet, b.getId(), false);
-                setVisibleAndAlpha(expandedSet, b.getId(), false);
-            }
-
-            for (int id : SEMANTIC_ACTIONS_ALL) {
-                ImageButton button = mMediaViewHolder.getAction(id);
-                MediaAction action = semanticActions.getActionById(id);
-                setSemanticButton(button, action, semanticActions);
-            }
-        } else {
-            // Hide buttons that only appear for semantic actions
-            for (int id : SEMANTIC_ACTIONS_COMPACT) {
-                setVisibleAndAlpha(collapsedSet, id, false);
-                setVisibleAndAlpha(expandedSet, id, false);
-            }
-
-            // Set all the generic buttons
-            List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
-            List<MediaAction> actions = data.getActions();
-            int i = 0;
-            for (; i < actions.size() && i < genericButtons.size(); i++) {
-                boolean showInCompact = actionsWhenCollapsed.contains(i);
-                setGenericButton(
-                        genericButtons.get(i),
-                        actions.get(i),
-                        collapsedSet,
-                        expandedSet,
-                        showInCompact);
-            }
-            for (; i < genericButtons.size(); i++) {
-                // Hide any unused buttons
-                setGenericButton(
-                        genericButtons.get(i),
-                        /* mediaAction= */ null,
-                        collapsedSet,
-                        expandedSet,
-                        /* showInCompact= */ false);
-            }
-        }
-
-        updateSeekBarVisibility();
-    }
-
-    private void updateSeekBarVisibility() {
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
-        expandedSet.setAlpha(R.id.media_progress_bar, mIsSeekBarEnabled ? 1.0f : 0.0f);
-    }
-
-    private int getSeekBarVisibility() {
-        if (mIsSeekBarEnabled) {
-            return ConstraintSet.VISIBLE;
-        }
-        // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE
-        // so layout weights still work.
-        return areAnyExpandedBottomActionsVisible() ? ConstraintSet.INVISIBLE : ConstraintSet.GONE;
-    }
-
-    private boolean areAnyExpandedBottomActionsVisible() {
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) {
-            if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void setGenericButton(
-            final ImageButton button,
-            @Nullable MediaAction mediaAction,
-            ConstraintSet collapsedSet,
-            ConstraintSet expandedSet,
-            boolean showInCompact) {
-        bindButtonCommon(button, mediaAction);
-        boolean visible = mediaAction != null;
-        setVisibleAndAlpha(expandedSet, button.getId(), visible);
-        setVisibleAndAlpha(collapsedSet, button.getId(), visible && showInCompact);
-    }
-
-    private void setSemanticButton(
-            final ImageButton button,
-            @Nullable MediaAction mediaAction,
-            MediaButton semanticActions) {
-        AnimationBindHandler animHandler;
-        if (button.getTag() == null) {
-            animHandler = new AnimationBindHandler();
-            button.setTag(animHandler);
-        } else {
-            animHandler = (AnimationBindHandler) button.getTag();
-        }
-
-        animHandler.tryExecute(() -> {
-            bindButtonWithAnimations(button, mediaAction, animHandler);
-            setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction, semanticActions);
-            return Unit.INSTANCE;
-        });
-    }
-
-    private void bindButtonWithAnimations(
-            final ImageButton button,
-            @Nullable MediaAction mediaAction,
-            @NonNull AnimationBindHandler animHandler) {
-        if (mediaAction != null) {
-            if (animHandler.updateRebindId(mediaAction.getRebindId())) {
-                animHandler.unregisterAll();
-                animHandler.tryRegister(mediaAction.getIcon());
-                animHandler.tryRegister(mediaAction.getBackground());
-                bindButtonCommon(button, mediaAction);
-            }
-        } else {
-            animHandler.unregisterAll();
-            clearButton(button);
-        }
-    }
-
-    private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
-        if (mediaAction != null) {
-            final Drawable icon = mediaAction.getIcon();
-            button.setImageDrawable(icon);
-            button.setContentDescription(mediaAction.getContentDescription());
-            final Drawable bgDrawable = mediaAction.getBackground();
-            button.setBackground(bgDrawable);
-
-            Runnable action = mediaAction.getAction();
-            if (action == null) {
-                button.setEnabled(false);
-            } else {
-                button.setEnabled(true);
-                button.setOnClickListener(v -> {
-                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                        mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
-                        logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
-                        action.run();
-
-                        if (icon instanceof Animatable) {
-                            ((Animatable) icon).start();
-                        }
-                        if (bgDrawable instanceof Animatable) {
-                            ((Animatable) bgDrawable).start();
-                        }
-                    }
-                });
-            }
-        } else {
-            clearButton(button);
-        }
-    }
-
-    private void clearButton(final ImageButton button) {
-        button.setImageDrawable(null);
-        button.setContentDescription(null);
-        button.setEnabled(false);
-        button.setBackground(null);
-    }
-
-    private void setSemanticButtonVisibleAndAlpha(
-            int buttonId,
-            @Nullable MediaAction mediaAction,
-            MediaButton semanticActions) {
-        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(buttonId);
-        boolean hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId);
-        boolean shouldBeHiddenDueToScrubbing =
-                scrubbingTimeViewsEnabled(semanticActions) && hideWhenScrubbing && mIsScrubbing;
-        boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
-
-        int notVisibleValue;
-        if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
-                || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) {
-            notVisibleValue = ConstraintSet.INVISIBLE;
-        } else {
-            notVisibleValue = ConstraintSet.GONE;
-        }
-        setVisibleAndAlpha(expandedSet, buttonId, visible, notVisibleValue);
-        setVisibleAndAlpha(collapsedSet, buttonId, visible && showInCompact);
-    }
-
-    /** Updates all the views that might change due to a scrubbing state change. */
-    private void updateDisplayForScrubbingChange(@NonNull MediaButton semanticActions) {
-        // Update visibilities of the scrubbing time views and the scrubbing-dependent buttons.
-        bindScrubbingTime(mMediaData);
-        SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach((id) -> setSemanticButtonVisibleAndAlpha(
-                id, semanticActions.getActionById(id), semanticActions));
-        if (!mMetadataAnimationHandler.isRunning()) {
-            // Trigger a state refresh so that we immediately update visibilities.
-            mMediaViewController.refreshState();
-        }
-    }
-
-    private void bindScrubbingTime(MediaData data) {
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        int elapsedTimeId = mMediaViewHolder.getScrubbingElapsedTimeView().getId();
-        int totalTimeId = mMediaViewHolder.getScrubbingTotalTimeView().getId();
-
-        boolean visible = scrubbingTimeViewsEnabled(data.getSemanticActions()) && mIsScrubbing;
-        setVisibleAndAlpha(expandedSet, elapsedTimeId, visible);
-        setVisibleAndAlpha(expandedSet, totalTimeId, visible);
-        // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically
-    }
-
-    private boolean scrubbingTimeViewsEnabled(@Nullable MediaButton semanticActions) {
-        // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views,
-        // so we should only allow scrubbing times to be shown if those action views are present.
-        return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch(
-                id -> semanticActions.getActionById(id) != null
-        );
-    }
-
-    @Nullable
-    private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
-            TransitionLayout player) {
-        if (!(player.getParent() instanceof ViewGroup)) {
-            // TODO(b/192194319): Throw instead of just logging.
-            Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
-                    new Exception());
-            return null;
-        }
-
-        // TODO(b/174236650): Make sure that the carousel indicator also fades out.
-        // TODO(b/174236650): Instrument the animation to measure jank.
-        return new GhostedViewLaunchAnimatorController(player,
-                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
-            @Override
-            protected float getCurrentTopCornerRadius() {
-                return mContext.getResources().getDimension(R.dimen.notification_corner_radius);
-            }
-
-            @Override
-            protected float getCurrentBottomCornerRadius() {
-                // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
-                return getCurrentTopCornerRadius();
-            }
-        };
-    }
-
-    /** Bind this recommendation view based on the given data. */
-    public void bindRecommendation(@NonNull SmartspaceMediaData data) {
-        if (mRecommendationViewHolder == null) {
-            return;
-        }
-
-        if (!data.isValid()) {
-            Log.e(TAG, "Received an invalid recommendation list; returning");
-            return;
-        }
-
-        Trace.beginSection(
-                "MediaControlPanel#bindRecommendation<" + data.getPackageName() + ">");
-
-        mRecommendationData = data;
-        mSmartspaceId = SmallHash.hash(data.getTargetId());
-        mPackageName = data.getPackageName();
-        mInstanceId = data.getInstanceId();
-
-        // Set up recommendation card's header.
-        ApplicationInfo applicationInfo;
-        try {
-            applicationInfo = mContext.getPackageManager()
-                    .getApplicationInfo(data.getPackageName(), 0 /* flags */);
-            mUid = applicationInfo.uid;
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Fail to get media recommendation's app info", e);
-            Trace.endSection();
-            return;
-        }
-
-        CharSequence appName = data.getAppName(mContext);
-        if (appName == null) {
-            Log.w(TAG, "Fail to get media recommendation's app name");
-            Trace.endSection();
-            return;
-        }
-
-        PackageManager packageManager = mContext.getPackageManager();
-        // Set up media source app's logo.
-        Drawable icon = packageManager.getApplicationIcon(applicationInfo);
-        ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
-        headerLogoImageView.setImageDrawable(icon);
-        fetchAndUpdateRecommendationColors(icon);
-
-        // Set up media rec card's tap action if applicable.
-        TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
-        setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction(),
-                /* interactedSubcardRank */ -1);
-        bindRecommendationContentDescription(data);
-
-        List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
-        List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
-        List<SmartspaceAction> recommendations = data.getValidRecommendations();
-
-        boolean hasTitle = false;
-        boolean hasSubtitle = false;
-        for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
-            SmartspaceAction recommendation = recommendations.get(itemIndex);
-
-            // Set up media item cover.
-            ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
-            mediaCoverImageView.setImageIcon(recommendation.getIcon());
-
-            // Set up the media item's click listener if applicable.
-            ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
-            setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation, itemIndex);
-            // Bubble up the long-click event to the card.
-            mediaCoverContainer.setOnLongClickListener(v -> {
-                View parent = (View) v.getParent();
-                if (parent != null) {
-                    parent.performLongClick();
-                }
-                return true;
-            });
-
-            // Set up the accessibility label for the media item.
-            String artistName = recommendation.getExtras()
-                    .getString(KEY_SMARTSPACE_ARTIST_NAME, "");
-            if (artistName.isEmpty()) {
-                mediaCoverImageView.setContentDescription(
-                        mContext.getString(
-                                R.string.controls_media_smartspace_rec_item_no_artist_description,
-                                recommendation.getTitle(), appName));
-            } else {
-                mediaCoverImageView.setContentDescription(
-                        mContext.getString(
-                                R.string.controls_media_smartspace_rec_item_description,
-                                recommendation.getTitle(), artistName, appName));
-            }
-
-
-            // Set up title
-            CharSequence title = recommendation.getTitle();
-            hasTitle |= !TextUtils.isEmpty(title);
-            TextView titleView = mRecommendationViewHolder.getMediaTitles().get(itemIndex);
-            titleView.setText(title);
-
-            // Set up subtitle
-            // It would look awkward to show a subtitle if we don't have a title.
-            boolean shouldShowSubtitleText = !TextUtils.isEmpty(title);
-            CharSequence subtitle = shouldShowSubtitleText ? recommendation.getSubtitle() : "";
-            hasSubtitle |= !TextUtils.isEmpty(subtitle);
-            TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
-            subtitleView.setText(subtitle);
-        }
-        mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
-
-        // If there's no subtitles and/or titles for any of the albums, hide those views.
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        final boolean titlesVisible = hasTitle;
-        final boolean subtitlesVisible = hasSubtitle;
-        mRecommendationViewHolder.getMediaTitles().forEach((titleView) ->
-                setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible));
-        mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) ->
-                setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible));
-
-        // Guts
-        Runnable onDismissClickedRunnable = () -> {
-            closeGuts();
-            mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
-                    data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
-
-            Intent dismissIntent = data.getDismissIntent();
-            if (dismissIntent == null) {
-                Log.w(TAG, "Cannot create dismiss action click action: "
-                        + "extras missing dismiss_intent.");
-                return;
-            }
-
-            if (dismissIntent.getComponent() != null
-                    && dismissIntent.getComponent().getClassName()
-                    .equals(EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)) {
-                // Dismiss the card Smartspace data through Smartspace trampoline activity.
-                mContext.startActivity(dismissIntent);
-            } else {
-                mBroadcastSender.sendBroadcast(dismissIntent);
-            }
-        };
-        bindGutsMenuCommon(
-                /* isDismissible= */ true,
-                appName.toString(),
-                mRecommendationViewHolder.getGutsViewHolder(),
-                onDismissClickedRunnable);
-
-        mController = null;
-        if (mMetadataAnimationHandler == null || !mMetadataAnimationHandler.isRunning()) {
-            mMediaViewController.refreshState();
-        }
-        Trace.endSection();
-    }
-
-    private void fetchAndUpdateRecommendationColors(Drawable appIcon) {
-        mBackgroundExecutor.execute(() -> {
-            ColorScheme colorScheme = new ColorScheme(
-                    WallpaperColors.fromDrawable(appIcon), /* darkTheme= */ true);
-            mMainExecutor.execute(() -> setRecommendationColors(colorScheme));
-        });
-    }
-
-    private void setRecommendationColors(ColorScheme colorScheme) {
-        if (mRecommendationViewHolder == null) {
-            return;
-        }
-
-        int backgroundColor = MediaColorSchemesKt.surfaceFromScheme(colorScheme);
-        int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
-        int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
-
-        mRecommendationViewHolder.getRecommendations()
-                .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
-        mRecommendationViewHolder.getMediaTitles().forEach(
-                (title) -> title.setTextColor(textPrimaryColor));
-        mRecommendationViewHolder.getMediaSubtitles().forEach(
-                (subtitle) -> subtitle.setTextColor(textSecondaryColor));
-
-        mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
-    }
-
-    private void bindGutsMenuCommon(
-            boolean isDismissible,
-            String appName,
-            GutsViewHolder gutsViewHolder,
-            Runnable onDismissClickedRunnable) {
-        // Text
-        String text;
-        if (isDismissible) {
-            text = mContext.getString(R.string.controls_media_close_session, appName);
-        } else {
-            text = mContext.getString(R.string.controls_media_active_session);
-        }
-        gutsViewHolder.getGutsText().setText(text);
-
-        // Dismiss button
-        gutsViewHolder.getDismissText().setVisibility(isDismissible ? View.VISIBLE : View.GONE);
-        gutsViewHolder.getDismiss().setEnabled(isDismissible);
-        gutsViewHolder.getDismiss().setOnClickListener(v -> {
-            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
-            logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT);
-            mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
-
-            onDismissClickedRunnable.run();
-        });
-
-        // Cancel button
-        TextView cancelText = gutsViewHolder.getCancelText();
-        if (isDismissible) {
-            cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_outline_button));
-        } else {
-            cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_solid_button));
-        }
-        gutsViewHolder.getCancel().setOnClickListener(v -> {
-            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                closeGuts();
-            }
-        });
-        gutsViewHolder.setDismissible(isDismissible);
-
-        // Settings button
-        gutsViewHolder.getSettings().setOnClickListener(v -> {
-            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
-                mActivityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */true);
-            }
-        });
-    }
-
-    /**
-     * Close the guts for this player.
-     *
-     * @param immediate {@code true} if it should be closed without animation
-     */
-    public void closeGuts(boolean immediate) {
-        if (mMediaViewHolder != null) {
-            mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
-        } else if (mRecommendationViewHolder != null) {
-            mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
-        }
-        mMediaViewController.closeGuts(immediate);
-        if (mMediaViewHolder != null) {
-            bindPlayerContentDescription(mMediaData);
-        } else if (mRecommendationViewHolder != null) {
-            bindRecommendationContentDescription(mRecommendationData);
-        }
-    }
-
-    private void closeGuts() {
-        closeGuts(false);
-    }
-
-    private void openGuts() {
-        if (mMediaViewHolder != null) {
-            mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
-        } else if (mRecommendationViewHolder != null) {
-            mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
-        }
-        mMediaViewController.openGuts();
-        if (mMediaViewHolder != null) {
-            bindPlayerContentDescription(mMediaData);
-        } else if (mRecommendationViewHolder != null) {
-            bindRecommendationContentDescription(mRecommendationData);
-        }
-        mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId);
-    }
-
-    /**
-     * Scale artwork to fill the background of the panel
-     */
-    @UiThread
-    private Drawable getScaledBackground(Icon icon, int width, int height) {
-        if (icon == null) {
-            return null;
-        }
-        Drawable drawable = icon.loadDrawable(mContext);
-        Rect bounds = new Rect(0, 0, width, height);
-        if (bounds.width() > width || bounds.height() > height) {
-            float offsetX = (bounds.width() - width) / 2.0f;
-            float offsetY = (bounds.height() - height) / 2.0f;
-            bounds.offset((int) -offsetX, (int) -offsetY);
-        }
-        drawable.setBounds(bounds);
-        return drawable;
-    }
-
-    /**
-     * Get the current media controller
-     *
-     * @return the controller
-     */
-    public MediaController getController() {
-        return mController;
-    }
-
-    /**
-     * Check whether the media controlled by this player is currently playing
-     *
-     * @return whether it is playing, or false if no controller information
-     */
-    public boolean isPlaying() {
-        return isPlaying(mController);
-    }
-
-    /**
-     * Check whether the given controller is currently playing
-     *
-     * @param controller media controller to check
-     * @return whether it is playing, or false if no controller information
-     */
-    protected boolean isPlaying(MediaController controller) {
-        if (controller == null) {
-            return false;
-        }
-
-        PlaybackState state = controller.getPlaybackState();
-        if (state == null) {
-            return false;
-        }
-
-        return (state.getState() == PlaybackState.STATE_PLAYING);
-    }
-
-    private ColorMatrixColorFilter getGrayscaleFilter() {
-        ColorMatrix matrix = new ColorMatrix();
-        matrix.setSaturation(0);
-        return new ColorMatrixColorFilter(matrix);
-    }
-
-    private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
-        setVisibleAndAlpha(set, actionId, visible, ConstraintSet.GONE);
-    }
-
-    private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible,
-            int notVisibleValue) {
-        set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : notVisibleValue);
-        set.setAlpha(actionId, visible ? 1.0f : 0.0f);
-    }
-
-    private void setSmartspaceRecItemOnClickListener(
-            @NonNull View view,
-            @NonNull SmartspaceAction action,
-            int interactedSubcardRank) {
-        if (view == null || action == null || action.getIntent() == null
-                || action.getIntent().getExtras() == null) {
-            Log.e(TAG, "No tap action can be set up");
-            return;
-        }
-
-        view.setOnClickListener(v -> {
-            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
-
-            if (interactedSubcardRank == -1) {
-                mLogger.logRecommendationCardTap(mPackageName, mInstanceId);
-            } else {
-                mLogger.logRecommendationItemTap(mPackageName, mInstanceId, interactedSubcardRank);
-            }
-            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
-                    interactedSubcardRank,
-                    mSmartspaceMediaItemsCount);
-
-            if (shouldSmartspaceRecItemOpenInForeground(action)) {
-                // Request to unlock the device if the activity needs to be opened in foreground.
-                mActivityStarter.postStartActivityDismissingKeyguard(
-                        action.getIntent(),
-                        0 /* delay */,
-                        buildLaunchAnimatorController(
-                                mRecommendationViewHolder.getRecommendations()));
-            } else {
-                // Otherwise, open the activity in background directly.
-                view.getContext().startActivity(action.getIntent());
-            }
-
-            // Automatically scroll to the active player once the media is loaded.
-            mMediaCarouselController.setShouldScrollToKey(true);
-        });
-    }
-
-    /** Returns if the Smartspace action will open the activity in foreground. */
-    private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) {
-        if (action == null || action.getIntent() == null
-                || action.getIntent().getExtras() == null) {
-            return false;
-        }
-
-        String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT);
-        if (intentString == null) {
-            return false;
-        }
-
-        try {
-            Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
-            return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false);
-        } catch (URISyntaxException e) {
-            Log.wtf(TAG, "Failed to create intent from URI: " + intentString);
-            e.printStackTrace();
-        }
-
-        return false;
-    }
-
-    /**
-     * Get the surface given the current end location for MediaViewController
-     * @return surface used for Smartspace logging
-     */
-    protected int getSurfaceForSmartspaceLogging() {
-        int currentEndLocation = mMediaViewController.getCurrentEndLocation();
-        if (currentEndLocation == MediaHierarchyManager.LOCATION_QQS
-                || currentEndLocation == MediaHierarchyManager.LOCATION_QS) {
-            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE;
-        } else if (currentEndLocation == MediaHierarchyManager.LOCATION_LOCKSCREEN) {
-            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN;
-        } else if (currentEndLocation == MediaHierarchyManager.LOCATION_DREAM_OVERLAY) {
-            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY;
-        }
-        return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE;
-    }
-
-    private void logSmartspaceCardReported(int eventId) {
-        logSmartspaceCardReported(eventId,
-                /* interactedSubcardRank */ 0,
-                /* interactedSubcardCardinality */ 0);
-    }
-
-    private void logSmartspaceCardReported(int eventId,
-            int interactedSubcardRank, int interactedSubcardCardinality) {
-        mMediaCarouselController.logSmartspaceCardReported(eventId,
-                mSmartspaceId,
-                mUid,
-                new int[]{getSurfaceForSmartspaceLogging()},
-                interactedSubcardRank,
-                interactedSubcardCardinality);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java
deleted file mode 100644
index ed3e109..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-
-import javax.inject.Inject;
-
-/**
- * Testable wrapper around {@link MediaController} constructor.
- */
-public class MediaControllerFactory {
-
-    private final Context mContext;
-
-    @Inject
-    public MediaControllerFactory(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Creates a new MediaController from a session's token.
-     *
-     * @param token The token for the session. This value must never be null.
-     */
-    public MediaController create(@NonNull MediaSession.Token token) {
-        return new MediaController(mContext, token);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
deleted file mode 100644
index d0fc3d06..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.Icon
-import android.media.session.MediaSession
-import com.android.internal.logging.InstanceId
-import com.android.systemui.R
-
-/** State of a media view. */
-data class MediaData(
-    val userId: Int,
-    val initialized: Boolean = false,
-    /**
-     * App name that will be displayed on the player.
-     */
-    val app: String?,
-    /**
-     * App icon shown on player.
-     */
-    val appIcon: Icon?,
-    /**
-     * Artist name.
-     */
-    val artist: CharSequence?,
-    /**
-     * Song name.
-     */
-    val song: CharSequence?,
-    /**
-     * Album artwork.
-     */
-    val artwork: Icon?,
-    /**
-     * List of generic action buttons for the media player, based on notification actions
-     */
-    val actions: List<MediaAction>,
-    /**
-     * Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
-     */
-    val actionsToShowInCompact: List<Int>,
-    /**
-     * Semantic actions buttons, based on the PlaybackState of the media session.
-     * If present, these actions will be preferred in the UI over [actions]
-     */
-    val semanticActions: MediaButton? = null,
-    /**
-     * Package name of the app that's posting the media.
-     */
-    val packageName: String,
-    /**
-     * Unique media session identifier.
-     */
-    val token: MediaSession.Token?,
-    /**
-     * Action to perform when the player is tapped.
-     * This is unrelated to {@link #actions}.
-     */
-    val clickIntent: PendingIntent?,
-    /**
-     * Where the media is playing: phone, headphones, ear buds, remote session.
-     */
-    val device: MediaDeviceData?,
-    /**
-     * When active, a player will be displayed on keyguard and quick-quick settings.
-     * This is unrelated to the stream being playing or not, a player will not be active if
-     * timed out, or in resumption mode.
-     */
-    var active: Boolean,
-    /**
-     * Action that should be performed to restart a non active session.
-     */
-    var resumeAction: Runnable?,
-    /**
-     * Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE
-     */
-    var playbackLocation: Int = PLAYBACK_LOCAL,
-    /**
-     * Indicates that this player is a resumption player (ie. It only shows a play actions which
-     * will start the app and start playing).
-     */
-    var resumption: Boolean = false,
-    /**
-     * Notification key for cancelling a media player after a timeout (when not using resumption.)
-     */
-    val notificationKey: String? = null,
-    var hasCheckedForResume: Boolean = false,
-
-    /**
-     * If apps do not report PlaybackState, set as null to imply 'undetermined'
-     */
-    val isPlaying: Boolean? = null,
-
-    /**
-     * Set from the notification and used as fallback when PlaybackState cannot be determined
-     */
-    val isClearable: Boolean = true,
-
-    /**
-     * Timestamp when this player was last active.
-     */
-    var lastActive: Long = 0L,
-
-    /**
-     * Instance ID for logging purposes
-     */
-    val instanceId: InstanceId,
-
-    /**
-     * The UID of the app, used for logging
-     */
-    val appUid: Int
-) {
-    companion object {
-        /** Media is playing on the local device */
-        const val PLAYBACK_LOCAL = 0
-        /** Media is cast but originated on the local device */
-        const val PLAYBACK_CAST_LOCAL = 1
-        /** Media is from a remote cast notification */
-        const val PLAYBACK_CAST_REMOTE = 2
-    }
-
-    fun isLocalSession(): Boolean {
-        return playbackLocation == PLAYBACK_LOCAL
-    }
-}
-
-/**
- * Contains [MediaAction] objects which represent specific buttons in the UI
- */
-data class MediaButton(
-    /**
-     * Play/pause button
-     */
-    val playOrPause: MediaAction? = null,
-    /**
-     * Next button, or custom action
-     */
-    val nextOrCustom: MediaAction? = null,
-    /**
-     * Previous button, or custom action
-     */
-    val prevOrCustom: MediaAction? = null,
-    /**
-     * First custom action space
-     */
-    val custom0: MediaAction? = null,
-    /**
-     * Second custom action space
-     */
-    val custom1: MediaAction? = null,
-    /**
-     * Whether to reserve the empty space when the nextOrCustom is null
-     */
-    val reserveNext: Boolean = false,
-    /**
-     * Whether to reserve the empty space when the prevOrCustom is null
-     */
-    val reservePrev: Boolean = false
-) {
-    fun getActionById(id: Int): MediaAction? {
-        return when (id) {
-            R.id.actionPlayPause -> playOrPause
-            R.id.actionNext -> nextOrCustom
-            R.id.actionPrev -> prevOrCustom
-            R.id.action0 -> custom0
-            R.id.action1 -> custom1
-            else -> null
-        }
-    }
-}
-
-/** State of a media action. */
-data class MediaAction(
-    val icon: Drawable?,
-    val action: Runnable?,
-    val contentDescription: CharSequence?,
-    val background: Drawable?,
-
-    // Rebind Id is used to detect identical rebinds and ignore them. It is intended
-    // to prevent continuously looping animations from restarting due to the arrival
-    // of repeated media notifications that are visually identical.
-    val rebindId: Int? = null
-)
-
-/** State of the media device. */
-data class MediaDeviceData
-@JvmOverloads constructor(
-    /** Whether or not to enable the chip */
-    val enabled: Boolean,
-
-    /** Device icon to show in the chip */
-    val icon: Drawable?,
-
-    /** Device display name */
-    val name: CharSequence?,
-
-    /** Optional intent to override the default output switcher for this control */
-    val intent: PendingIntent? = null,
-
-    /** Unique id for this device */
-    val id: String? = null,
-
-    /** Whether or not to show the broadcast button */
-    val showBroadcastButton: Boolean
-) {
-    /**
-     * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon
-     * is ignored because it can change by reference frequently depending on the device type's
-     * implementation, but this is not usually relevant unless other info has changed
-     */
-    fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {
-        if (other == null) {
-            return false
-        }
-
-        return enabled == other.enabled &&
-            name == other.name &&
-            intent == other.intent &&
-            id == other.id
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
deleted file mode 100644
index 311973a..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import javax.inject.Inject
-
-/**
- * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events.
- */
-class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
-        MediaDeviceManager.Listener {
-
-    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
-    private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
-
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
-            entries[key] = data to entries.remove(oldKey)?.second
-            update(key, oldKey)
-        } else {
-            entries[key] = data to entries[key]?.second
-            update(key, key)
-        }
-    }
-
-    override fun onSmartspaceMediaDataLoaded(
-        key: String,
-        data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
-    ) {
-        listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
-    }
-
-    override fun onMediaDataRemoved(key: String) {
-        remove(key)
-    }
-
-    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
-    }
-
-    override fun onMediaDeviceChanged(
-        key: String,
-        oldKey: String?,
-        data: MediaDeviceData?
-    ) {
-        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
-            entries[key] = entries.remove(oldKey)?.first to data
-            update(key, oldKey)
-        } else {
-            entries[key] = entries[key]?.first to data
-            update(key, key)
-        }
-    }
-
-    override fun onKeyRemoved(key: String) {
-        remove(key)
-    }
-
-    /**
-     * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
-     */
-    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
-
-    /**
-     * Remove a listener registered with addListener.
-     */
-    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
-
-    private fun update(key: String, oldKey: String?) {
-        val (entry, device) = entries[key] ?: null to null
-        if (entry != null && device != null) {
-            val data = entry.copy(device = device)
-            val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataLoaded(key, oldKey, data)
-            }
-        }
-    }
-
-    private fun remove(key: String) {
-        entries.remove(key)?.let {
-            val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataRemoved(key)
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
deleted file mode 100644
index e0c8d66..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-import android.os.SystemProperties
-import android.util.Log
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.settings.CurrentUserTracker
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.util.time.SystemClock
-import java.util.SortedMap
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-import kotlin.collections.LinkedHashMap
-
-private const val TAG = "MediaDataFilter"
-private const val DEBUG = true
-private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = ("com.google" +
-        ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
-private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
-
-/**
- * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
- * available within this time window, smartspace recommendations will be shown instead.
- */
-@VisibleForTesting
-internal val SMARTSPACE_MAX_AGE = SystemProperties
-        .getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
-
-/**
- * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
- * switches (removing entries for the previous user, adding back entries for the current user). Also
- * filters out smartspace updates in favor of local recent media, when avaialble.
- *
- * This is added at the end of the pipeline since we may still need to handle callbacks from
- * background users (e.g. timeouts).
- */
-class MediaDataFilter @Inject constructor(
-    private val context: Context,
-    private val broadcastDispatcher: BroadcastDispatcher,
-    private val broadcastSender: BroadcastSender,
-    private val lockscreenUserManager: NotificationLockscreenUserManager,
-    @Main private val executor: Executor,
-    private val systemClock: SystemClock,
-    private val logger: MediaUiEventLogger
-) : MediaDataManager.Listener {
-    private val userTracker: CurrentUserTracker
-    private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
-    internal val listeners: Set<MediaDataManager.Listener>
-        get() = _listeners.toSet()
-    internal lateinit var mediaDataManager: MediaDataManager
-
-    private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
-    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-    private var reactivatedKey: String? = null
-
-    init {
-        userTracker = object : CurrentUserTracker(broadcastDispatcher) {
-            override fun onUserSwitched(newUserId: Int) {
-                // Post this so we can be sure lockscreenUserManager already got the broadcast
-                executor.execute { handleUserSwitched(newUserId) }
-            }
-        }
-        userTracker.startTracking()
-    }
-
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        if (oldKey != null && oldKey != key) {
-            allEntries.remove(oldKey)
-        }
-        allEntries.put(key, data)
-
-        if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
-            return
-        }
-
-        if (oldKey != null && oldKey != key) {
-            userEntries.remove(oldKey)
-        }
-        userEntries.put(key, data)
-
-        // Notify listeners
-        listeners.forEach {
-            it.onMediaDataLoaded(key, oldKey, data)
-        }
-    }
-
-    override fun onSmartspaceMediaDataLoaded(
-        key: String,
-        data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
-    ) {
-        if (!data.isActive) {
-            Log.d(TAG, "Inactive recommendation data. Skip triggering.")
-            return
-        }
-
-        // Override the pass-in value here, as the order of Smartspace card is only determined here.
-        var shouldPrioritizeMutable = false
-        smartspaceMediaData = data
-
-        // Before forwarding the smartspace target, first check if we have recently inactive media
-        val sorted = userEntries.toSortedMap(compareBy {
-            userEntries.get(it)?.lastActive ?: -1
-        })
-        val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
-        var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
-        data.cardAction?.let {
-            val smartspaceMaxAgeSeconds =
-                it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
-            if (smartspaceMaxAgeSeconds > 0) {
-                smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
-            }
-        }
-
-        val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
-
-        if (timeSinceActive < smartspaceMaxAgeMillis) {
-            // It could happen there are existing active media resume cards, then we don't need to
-            // reactivate.
-            if (shouldReactivate) {
-                val lastActiveKey = sorted.lastKey() // most recently active
-                // Notify listeners to consider this media active
-                Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
-                reactivatedKey = lastActiveKey
-                val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
-                logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName,
-                    mediaData.instanceId)
-                listeners.forEach {
-                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData,
-                            receivedSmartspaceCardLatency =
-                            (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
-                                    .toInt(), isSsReactivated = true)
-                }
-            }
-        } else {
-            // Mark to prioritize Smartspace card if no recent media.
-            shouldPrioritizeMutable = true
-        }
-
-        if (!data.isValid()) {
-            Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
-            return
-        }
-        logger.logRecommendationAdded(smartspaceMediaData.packageName,
-            smartspaceMediaData.instanceId)
-        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
-    }
-
-    override fun onMediaDataRemoved(key: String) {
-        allEntries.remove(key)
-        userEntries.remove(key)?.let {
-            // Only notify listeners if something actually changed
-            listeners.forEach {
-                it.onMediaDataRemoved(key)
-            }
-        }
-    }
-
-    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        // First check if we had reactivated media instead of forwarding smartspace
-        reactivatedKey?.let {
-            val lastActiveKey = it
-            reactivatedKey = null
-            Log.d(TAG, "expiring reactivated key $lastActiveKey")
-            // Notify listeners to update with actual active value
-            userEntries.get(lastActiveKey)?.let { mediaData ->
-                listeners.forEach {
-                    it.onMediaDataLoaded(
-                            lastActiveKey, lastActiveKey, mediaData, immediately)
-                }
-            }
-        }
-
-        if (smartspaceMediaData.isActive) {
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
-        }
-        listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
-    }
-
-    @VisibleForTesting
-    internal fun handleUserSwitched(id: Int) {
-        // If the user changes, remove all current MediaData objects and inform listeners
-        val listenersCopy = listeners
-        val keyCopy = userEntries.keys.toMutableList()
-        // Clear the list first, to make sure callbacks from listeners if we have any entries
-        // are up to date
-        userEntries.clear()
-        keyCopy.forEach {
-            if (DEBUG) Log.d(TAG, "Removing $it after user change")
-            listenersCopy.forEach { listener ->
-                listener.onMediaDataRemoved(it)
-            }
-        }
-
-        allEntries.forEach { (key, data) ->
-            if (lockscreenUserManager.isCurrentProfile(data.userId)) {
-                if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
-                userEntries.put(key, data)
-                listenersCopy.forEach { listener ->
-                    listener.onMediaDataLoaded(key, null, data)
-                }
-            }
-        }
-    }
-
-    /**
-     * Invoked when the user has dismissed the media carousel
-     */
-    fun onSwipeToDismiss() {
-        if (DEBUG) Log.d(TAG, "Media carousel swiped away")
-        val mediaKeys = userEntries.keys.toSet()
-        mediaKeys.forEach {
-            // Force updates to listeners, needed for re-activated card
-            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
-        }
-        if (smartspaceMediaData.isActive) {
-            val dismissIntent = smartspaceMediaData.dismissIntent
-            if (dismissIntent == null) {
-                Log.w(TAG, "Cannot create dismiss action click action: " +
-                        "extras missing dismiss_intent.")
-            } else if (dismissIntent.getComponent() != null &&
-                    dismissIntent.getComponent().getClassName()
-                    == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) {
-                // Dismiss the card Smartspace data through Smartspace trampoline activity.
-                context.startActivity(dismissIntent)
-            } else {
-                broadcastSender.sendBroadcast(dismissIntent)
-            }
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
-            mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId,
-                delay = 0L)
-        }
-    }
-
-    /**
-     * Are there any active media entries, including the recommendation?
-     */
-    fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } ||
-            (smartspaceMediaData.isActive &&
-                (smartspaceMediaData.isValid() || reactivatedKey != null))
-
-    /**
-     * Are there any media entries we should display?
-     */
-    fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() ||
-            (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
-
-    /**
-     * Are there any media notifications active (excluding the recommendation)?
-     */
-    fun hasActiveMedia() = userEntries.any { it.value.active }
-
-    /**
-     * Are there any media entries we should display (excluding the recommendation)?
-     */
-    fun hasAnyMedia() = userEntries.isNotEmpty()
-
-    /**
-     * Add a listener for filtered [MediaData] changes
-     */
-    fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
-
-    /**
-     * Remove a listener that was registered with addListener
-     */
-    fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
-
-    /**
-     * Return the time since last active for the most-recent media.
-     *
-     * @param sortedEntries userEntries sorted from the earliest to the most-recent.
-     *
-     * @return The duration in milliseconds from the most-recent media's last active timestamp to
-     * the present. MAX_VALUE will be returned if there is no media.
-     */
-    private fun timeSinceActiveForMostRecentMedia(
-        sortedEntries: SortedMap<String, MediaData>
-    ): Long {
-        if (sortedEntries.isEmpty()) {
-            return Long.MAX_VALUE
-        }
-
-        val now = systemClock.elapsedRealtime()
-        val lastActiveKey = sortedEntries.lastKey() // most recently active
-        return sortedEntries.get(lastActiveKey)?.let {
-            now - it.lastActive
-        } ?: Long.MAX_VALUE
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
deleted file mode 100644
index 896fb47..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ /dev/null
@@ -1,1328 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.app.Notification
-import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
-import android.app.PendingIntent
-import android.app.smartspace.SmartspaceConfig
-import android.app.smartspace.SmartspaceManager
-import android.app.smartspace.SmartspaceSession
-import android.app.smartspace.SmartspaceTarget
-import android.content.BroadcastReceiver
-import android.content.ContentResolver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.ImageDecoder
-import android.graphics.drawable.Animatable
-import android.graphics.drawable.Icon
-import android.media.MediaDescription
-import android.media.MediaMetadata
-import android.media.session.MediaController
-import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.net.Uri
-import android.os.Parcelable
-import android.os.Process
-import android.os.UserHandle
-import android.provider.Settings
-import android.service.notification.StatusBarNotification
-import android.text.TextUtils
-import android.util.Log
-import androidx.media.utils.MediaConstants
-import com.android.internal.annotations.VisibleForTesting
-import com.android.internal.logging.InstanceId
-import com.android.systemui.Dumpable
-import com.android.systemui.R
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
-import com.android.systemui.statusbar.notification.row.HybridGroupManager
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.Assert
-import com.android.systemui.util.Utils
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.util.traceSection
-import java.io.IOException
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import java.util.concurrent.Executors
-import javax.inject.Inject
-
-// URI fields to try loading album art from
-private val ART_URIS = arrayOf(
-        MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
-        MediaMetadata.METADATA_KEY_ART_URI,
-        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
-)
-
-private const val TAG = "MediaDataManager"
-private const val DEBUG = true
-private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
-
-private val LOADING = MediaData(
-        userId = -1,
-        initialized = false,
-        app = null,
-        appIcon = null,
-        artist = null,
-        song = null,
-        artwork = null,
-        actions = emptyList(),
-        actionsToShowInCompact = emptyList(),
-        packageName = "INVALID",
-        token = null,
-        clickIntent = null,
-        device = null,
-        active = true,
-        resumeAction = null,
-        instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID)
-
-@VisibleForTesting
-internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData(
-    targetId = "INVALID",
-    isActive = false,
-    packageName = "INVALID",
-    cardAction = null,
-    recommendations = emptyList(),
-    dismissIntent = null,
-    headphoneConnectionTimeMillis = 0,
-    instanceId = InstanceId.fakeInstanceId(-1))
-
-fun isMediaNotification(sbn: StatusBarNotification): Boolean {
-    return sbn.notification.isMediaNotification()
-}
-
-/**
- * Allow recommendations from smartspace to show in media controls.
- * Requires [Utils.useQsMediaPlayer] to be enabled.
- * On by default, but can be disabled by setting to 0
- */
-private fun allowMediaRecommendations(context: Context): Boolean {
-    val flag = Settings.Secure.getInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
-    return Utils.useQsMediaPlayer(context) && flag > 0
-}
-
-/**
- * A class that facilitates management and loading of Media Data, ready for binding.
- */
-@SysUISingleton
-class MediaDataManager(
-    private val context: Context,
-    @Background private val backgroundExecutor: Executor,
-    @Main private val foregroundExecutor: DelayableExecutor,
-    private val mediaControllerFactory: MediaControllerFactory,
-    private val broadcastDispatcher: BroadcastDispatcher,
-    dumpManager: DumpManager,
-    mediaTimeoutListener: MediaTimeoutListener,
-    mediaResumeListener: MediaResumeListener,
-    mediaSessionBasedFilter: MediaSessionBasedFilter,
-    mediaDeviceManager: MediaDeviceManager,
-    mediaDataCombineLatest: MediaDataCombineLatest,
-    private val mediaDataFilter: MediaDataFilter,
-    private val activityStarter: ActivityStarter,
-    private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
-    private var useMediaResumption: Boolean,
-    private val useQsMediaPlayer: Boolean,
-    private val systemClock: SystemClock,
-    private val tunerService: TunerService,
-    private val mediaFlags: MediaFlags,
-    private val logger: MediaUiEventLogger
-) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
-
-    companion object {
-        // UI surface label for subscribing Smartspace updates.
-        @JvmField
-        val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
-
-        // Smartspace package name's extra key.
-        @JvmField
-        val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
-
-        // Maximum number of actions allowed in compact view
-        @JvmField
-        val MAX_COMPACT_ACTIONS = 3
-
-        // Maximum number of actions allowed in expanded view
-        @JvmField
-        val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
-    }
-
-    private val themeText = com.android.settingslib.Utils.getColorAttr(context,
-            com.android.internal.R.attr.textColorPrimary).defaultColor
-
-    // Internal listeners are part of the internal pipeline. External listeners (those registered
-    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
-    // the internal pipeline.
-    // Another way to think of the distinction between internal and external listeners is the
-    // following. Internal listeners are listeners that MediaDataManager depends on, and external
-    // listeners are listeners that depend on MediaDataManager.
-    // TODO(b/159539991#comment5): Move internal listeners to separate package.
-    private val internalListeners: MutableSet<Listener> = mutableSetOf()
-    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
-    // There should ONLY be at most one Smartspace media recommendation.
-    var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-    private var smartspaceSession: SmartspaceSession? = null
-    private var allowMediaRecommendations = allowMediaRecommendations(context)
-
-    /**
-     * Check whether this notification is an RCN
-     */
-    private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
-        return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
-    }
-
-    @Inject
-    constructor(
-        context: Context,
-        @Background backgroundExecutor: Executor,
-        @Main foregroundExecutor: DelayableExecutor,
-        mediaControllerFactory: MediaControllerFactory,
-        dumpManager: DumpManager,
-        broadcastDispatcher: BroadcastDispatcher,
-        mediaTimeoutListener: MediaTimeoutListener,
-        mediaResumeListener: MediaResumeListener,
-        mediaSessionBasedFilter: MediaSessionBasedFilter,
-        mediaDeviceManager: MediaDeviceManager,
-        mediaDataCombineLatest: MediaDataCombineLatest,
-        mediaDataFilter: MediaDataFilter,
-        activityStarter: ActivityStarter,
-        smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
-        clock: SystemClock,
-        tunerService: TunerService,
-        mediaFlags: MediaFlags,
-        logger: MediaUiEventLogger
-    ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
-            broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
-            mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
-            activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context),
-            Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags, logger)
-
-    private val appChangeReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            when (intent.action) {
-                Intent.ACTION_PACKAGES_SUSPENDED -> {
-                    val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-                    packages?.forEach {
-                        removeAllForPackage(it)
-                    }
-                }
-                Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> {
-                    intent.data?.encodedSchemeSpecificPart?.let {
-                        removeAllForPackage(it)
-                    }
-                }
-            }
-        }
-    }
-
-    init {
-        dumpManager.registerDumpable(TAG, this)
-
-        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
-        // are set as internal listeners so that they receive events. From there, events are
-        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
-        // so it is responsible for dispatching events to external listeners. To achieve this,
-        // external listeners that are registered with [MediaDataManager.addListener] are actually
-        // registered as listeners to mediaDataFilter.
-        addInternalListener(mediaTimeoutListener)
-        addInternalListener(mediaResumeListener)
-        addInternalListener(mediaSessionBasedFilter)
-        mediaSessionBasedFilter.addListener(mediaDeviceManager)
-        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
-        mediaDeviceManager.addListener(mediaDataCombineLatest)
-        mediaDataCombineLatest.addListener(mediaDataFilter)
-
-        // Set up links back into the pipeline for listeners that need to send events upstream.
-        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
-            setTimedOut(key, timedOut) }
-        mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
-            updateState(key, state) }
-        mediaResumeListener.setManager(this)
-        mediaDataFilter.mediaDataManager = this
-
-        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
-        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
-
-        val uninstallFilter = IntentFilter().apply {
-            addAction(Intent.ACTION_PACKAGE_REMOVED)
-            addAction(Intent.ACTION_PACKAGE_RESTARTED)
-            addDataScheme("package")
-        }
-        // BroadcastDispatcher does not allow filters with data schemes
-        context.registerReceiver(appChangeReceiver, uninstallFilter)
-
-        // Register for Smartspace data updates.
-        smartspaceMediaDataProvider.registerListener(this)
-        val smartspaceManager: SmartspaceManager =
-            context.getSystemService(SmartspaceManager::class.java)
-        smartspaceSession = smartspaceManager.createSmartspaceSession(
-            SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build())
-        smartspaceSession?.let {
-            it.addOnTargetsAvailableListener(
-                // Use a new thread listening to Smartspace updates instead of using the existing
-                // backgroundExecutor. SmartspaceSession has scheduled routine updates which can be
-                // unpredictable on test simulators, using the backgroundExecutor makes it's hard to
-                // test the threads numbers.
-                // Switch to use backgroundExecutor when SmartspaceSession has a good way to be
-                // mocked.
-                Executors.newCachedThreadPool(),
-                SmartspaceSession.OnTargetsAvailableListener { targets ->
-                    smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                })
-        }
-        smartspaceSession?.let { it.requestSmartspaceUpdate() }
-        tunerService.addTunable(object : TunerService.Tunable {
-            override fun onTuningChanged(key: String?, newValue: String?) {
-                allowMediaRecommendations = allowMediaRecommendations(context)
-                if (!allowMediaRecommendations) {
-                    dismissSmartspaceRecommendation(key = smartspaceMediaData.targetId, delay = 0L)
-                }
-            }
-        }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
-    }
-
-    fun destroy() {
-        smartspaceMediaDataProvider.unregisterListener(this)
-        context.unregisterReceiver(appChangeReceiver)
-    }
-
-    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
-        if (useQsMediaPlayer && isMediaNotification(sbn)) {
-            var logEvent = false
-            Assert.isMainThread()
-            val oldKey = findExistingEntry(key, sbn.packageName)
-            if (oldKey == null) {
-                val instanceId = logger.getNewInstanceId()
-                val temp = LOADING.copy(
-                    packageName = sbn.packageName,
-                    instanceId = instanceId
-                )
-                mediaEntries.put(key, temp)
-                logEvent = true
-            } else if (oldKey != key) {
-                // Resume -> active conversion; move to new key
-                val oldData = mediaEntries.remove(oldKey)!!
-                logEvent = true
-                mediaEntries.put(key, oldData)
-            }
-            loadMediaData(key, sbn, oldKey, logEvent)
-        } else {
-            onNotificationRemoved(key)
-        }
-    }
-
-    private fun removeAllForPackage(packageName: String) {
-        Assert.isMainThread()
-        val toRemove = mediaEntries.filter { it.value.packageName == packageName }
-        toRemove.forEach {
-            removeEntry(it.key)
-        }
-    }
-
-    fun setResumeAction(key: String, action: Runnable?) {
-        mediaEntries.get(key)?.let {
-            it.resumeAction = action
-            it.hasCheckedForResume = true
-        }
-    }
-
-    fun addResumptionControls(
-        userId: Int,
-        desc: MediaDescription,
-        action: Runnable,
-        token: MediaSession.Token,
-        appName: String,
-        appIntent: PendingIntent,
-        packageName: String
-    ) {
-        // Resume controls don't have a notification key, so store by package name instead
-        if (!mediaEntries.containsKey(packageName)) {
-            val instanceId = logger.getNewInstanceId()
-            val appUid = try {
-                context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
-            } catch (e: PackageManager.NameNotFoundException) {
-                Log.w(TAG, "Could not get app UID for $packageName", e)
-                Process.INVALID_UID
-            }
-
-            val resumeData = LOADING.copy(
-                packageName = packageName,
-                resumeAction = action,
-                hasCheckedForResume = true,
-                instanceId = instanceId,
-                appUid = appUid
-            )
-            mediaEntries.put(packageName, resumeData)
-            logger.logResumeMediaAdded(appUid, packageName, instanceId)
-        }
-        backgroundExecutor.execute {
-            loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
-                packageName)
-        }
-    }
-
-    /**
-     * Check if there is an existing entry that matches the key or package name.
-     * Returns the key that matches, or null if not found.
-     */
-    private fun findExistingEntry(key: String, packageName: String): String? {
-        if (mediaEntries.containsKey(key)) {
-            return key
-        }
-        // Check if we already had a resume player
-        if (mediaEntries.containsKey(packageName)) {
-            return packageName
-        }
-        return null
-    }
-
-    private fun loadMediaData(
-        key: String,
-        sbn: StatusBarNotification,
-        oldKey: String?,
-        logEvent: Boolean = false
-    ) {
-        backgroundExecutor.execute {
-            loadMediaDataInBg(key, sbn, oldKey, logEvent)
-        }
-    }
-
-    /**
-     * Add a listener for changes in this class
-     */
-    fun addListener(listener: Listener) {
-        // mediaDataFilter is the current end of the internal pipeline. Register external
-        // listeners as listeners to it.
-        mediaDataFilter.addListener(listener)
-    }
-
-    /**
-     * Remove a listener for changes in this class
-     */
-    fun removeListener(listener: Listener) {
-        // Since mediaDataFilter is the current end of the internal pipelie, external listeners
-        // have been registered to it. So, they need to be removed from it too.
-        mediaDataFilter.removeListener(listener)
-    }
-
-    /**
-     * Add a listener for internal events.
-     */
-    private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
-
-    /**
-     * Notify internal listeners of media loaded event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
-        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
-    }
-
-    /**
-     * Notify internal listeners of Smartspace media loaded event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
-        internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
-    }
-
-    /**
-     * Notify internal listeners of media removed event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     */
-    private fun notifyMediaDataRemoved(key: String) {
-        internalListeners.forEach { it.onMediaDataRemoved(key) }
-    }
-
-    /**
-     * Notify internal listeners of Smartspace media removed event.
-     *
-     * External listeners registered with [addListener] will be notified after the event propagates
-     * through the internal listener pipeline.
-     *
-     * @param immediately indicates should apply the UI changes immediately, otherwise wait until
-     * the next refresh-round before UI becomes visible. Should only be true if the update is
-     * initiated by user's interaction.
-     */
-    private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
-    }
-
-    /**
-     * Called whenever the player has been paused or stopped for a while, or swiped from QQS.
-     * This will make the player not active anymore, hiding it from QQS and Keyguard.
-     * @see MediaData.active
-     */
-    internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
-        mediaEntries[key]?.let {
-            if (timedOut && !forceUpdate) {
-                // Only log this event when media expires on its own
-                logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
-            }
-            if (it.active == !timedOut && !forceUpdate) {
-                if (it.resumption) {
-                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
-                    dismissMediaData(key, 0L /* delay */)
-                }
-                return
-            }
-            it.active = !timedOut
-            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
-            onMediaDataLoaded(key, key, it)
-        }
-    }
-
-    /**
-     * Called when the player's [PlaybackState] has been updated with new actions and/or state
-     */
-    private fun updateState(key: String, state: PlaybackState) {
-        mediaEntries.get(key)?.let {
-            val token = it.token
-            if (token == null) {
-                if (DEBUG) Log.d(TAG, "State updated, but token was null")
-                return
-            }
-            val actions = createActionsFromState(it.packageName,
-                    mediaControllerFactory.create(it.token), UserHandle(it.userId))
-
-            // Control buttons
-            // If flag is enabled and controller has a PlaybackState,
-            // create actions from session info
-            // otherwise, no need to update semantic actions.
-            val data = if (actions != null) {
-                it.copy(
-                        semanticActions = actions,
-                        isPlaying = isPlayingState(state.state))
-            } else {
-                it.copy(
-                        isPlaying = isPlayingState(state.state)
-                )
-            }
-            if (DEBUG) Log.d(TAG, "State updated outside of notification")
-            onMediaDataLoaded(key, key, data)
-        }
-    }
-
-    private fun removeEntry(key: String) {
-        mediaEntries.remove(key)?.let {
-            logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
-        }
-        notifyMediaDataRemoved(key)
-    }
-
-    /**
-     * Dismiss a media entry. Returns false if the key was not found.
-     */
-    fun dismissMediaData(key: String, delay: Long): Boolean {
-        val existed = mediaEntries[key] != null
-        backgroundExecutor.execute {
-            mediaEntries[key]?.let { mediaData ->
-                if (mediaData.isLocalSession()) {
-                    mediaData.token?.let {
-                        val mediaController = mediaControllerFactory.create(it)
-                        mediaController.transportControls.stop()
-                    }
-                }
-            }
-        }
-        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
-        return existed
-    }
-
-    /**
-     * Called whenever the recommendation has been expired, or swiped from QQS.
-     * This will make the recommendation view to not be shown anymore during this headphone
-     * connection session.
-     */
-    fun dismissSmartspaceRecommendation(key: String, delay: Long) {
-        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
-            // If this doesn't match, or we've already invalidated the data, no action needed
-            return
-        }
-
-        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
-        if (smartspaceMediaData.isActive) {
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
-        }
-        foregroundExecutor.executeDelayed(
-            { notifySmartspaceMediaDataRemoved(
-                smartspaceMediaData.targetId, immediately = true) }, delay)
-    }
-
-    private fun loadMediaDataInBgForResumption(
-        userId: Int,
-        desc: MediaDescription,
-        resumeAction: Runnable,
-        token: MediaSession.Token,
-        appName: String,
-        appIntent: PendingIntent,
-        packageName: String
-    ) {
-        if (TextUtils.isEmpty(desc.title)) {
-            Log.e(TAG, "Description incomplete")
-            // Delete the placeholder entry
-            mediaEntries.remove(packageName)
-            return
-        }
-
-        if (DEBUG) {
-            Log.d(TAG, "adding track for $userId from browser: $desc")
-        }
-
-        // Album art
-        var artworkBitmap = desc.iconBitmap
-        if (artworkBitmap == null && desc.iconUri != null) {
-            artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
-        }
-        val artworkIcon = if (artworkBitmap != null) {
-            Icon.createWithBitmap(artworkBitmap)
-        } else {
-            null
-        }
-
-        val currentEntry = mediaEntries.get(packageName)
-        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-        val appUid = currentEntry?.appUid ?: Process.INVALID_UID
-
-        val mediaAction = getResumeMediaAction(resumeAction)
-        val lastActive = systemClock.elapsedRealtime()
-        foregroundExecutor.execute {
-            onMediaDataLoaded(packageName, null, MediaData(userId, true, appName,
-                    null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
-                    MediaButton(playOrPause = mediaAction), packageName, token, appIntent,
-                    device = null, active = false,
-                    resumeAction = resumeAction, resumption = true, notificationKey = packageName,
-                    hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId,
-                    appUid = appUid))
-        }
-    }
-
-    fun loadMediaDataInBg(
-        key: String,
-        sbn: StatusBarNotification,
-        oldKey: String?,
-        logEvent: Boolean = false
-    ) {
-        val token = sbn.notification.extras.getParcelable(
-                Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java)
-        if (token == null) {
-            return
-        }
-        val mediaController = mediaControllerFactory.create(token)
-        val metadata = mediaController.metadata
-        val notif: Notification = sbn.notification
-
-        val appInfo = notif.extras.getParcelable(
-            Notification.EXTRA_BUILDER_APPLICATION_INFO,
-            ApplicationInfo::class.java
-        ) ?: getAppInfoFromPackage(sbn.packageName)
-
-        // Album art
-        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
-        if (artworkBitmap == null) {
-            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
-        }
-        if (artworkBitmap == null) {
-            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
-        }
-        val artWorkIcon = if (artworkBitmap == null) {
-            notif.getLargeIcon()
-        } else {
-            Icon.createWithBitmap(artworkBitmap)
-        }
-
-        // App name
-        val appName = getAppName(sbn, appInfo)
-
-        // App Icon
-        val smallIcon = sbn.notification.smallIcon
-
-        // Song name
-        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
-        if (song == null) {
-            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
-        }
-        if (song == null) {
-            song = HybridGroupManager.resolveTitle(notif)
-        }
-
-        // Artist name
-        var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
-        if (artist == null) {
-            artist = HybridGroupManager.resolveText(notif)
-        }
-
-        // Device name (used for remote cast notifications)
-        var device: MediaDeviceData? = null
-        if (isRemoteCastNotification(sbn)) {
-            val extras = sbn.notification.extras
-            val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
-            val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
-            val deviceIntent = extras.getParcelable(
-                    Notification.EXTRA_MEDIA_REMOTE_INTENT, PendingIntent::class.java)
-            Log.d(TAG, "$key is RCN for $deviceName")
-
-            if (deviceName != null && deviceIcon > -1) {
-                // Name and icon must be present, but intent may be null
-                val enabled = deviceIntent != null && deviceIntent.isActivity
-                val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon)
-                        .loadDrawable(sbn.getPackageContext(context))
-                device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent,
-                        showBroadcastButton = false)
-            }
-        }
-
-        // Control buttons
-        // If flag is enabled and controller has a PlaybackState, create actions from session info
-        // Otherwise, use the notification actions
-        var actionIcons: List<MediaAction> = emptyList()
-        var actionsToShowCollapsed: List<Int> = emptyList()
-        val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
-        if (semanticActions == null) {
-            val actions = createActionsFromNotification(sbn)
-            actionIcons = actions.first
-            actionsToShowCollapsed = actions.second
-        }
-
-        val playbackLocation =
-                if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
-                else if (mediaController.playbackInfo?.playbackType ==
-                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
-                else MediaData.PLAYBACK_CAST_LOCAL
-        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
-
-        val currentEntry = mediaEntries.get(key)
-        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-        val appUid = appInfo?.uid ?: Process.INVALID_UID
-
-        if (logEvent) {
-            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
-        } else if (playbackLocation != currentEntry?.playbackLocation) {
-            logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
-        }
-
-        val lastActive = systemClock.elapsedRealtime()
-        foregroundExecutor.execute {
-            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
-            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
-            val active = mediaEntries[key]?.active ?: true
-            onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, appName,
-                    smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed,
-                    semanticActions, sbn.packageName, token, notif.contentIntent, device,
-                    active, resumeAction = resumeAction, playbackLocation = playbackLocation,
-                    notificationKey = key, hasCheckedForResume = hasCheckedForResume,
-                    isPlaying = isPlaying, isClearable = sbn.isClearable(),
-                    lastActive = lastActive, instanceId = instanceId, appUid = appUid))
-        }
-    }
-
-    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
-        try {
-            return context.packageManager.getApplicationInfo(packageName, 0)
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.w(TAG, "Could not get app info for $packageName", e)
-        }
-        return null
-    }
-
-    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
-        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
-        if (name != null) {
-            return name
-        }
-
-        return if (appInfo != null) {
-            context.packageManager.getApplicationLabel(appInfo).toString()
-        } else {
-            sbn.packageName
-        }
-    }
-
-    /**
-     * Generate action buttons based on notification actions
-     */
-    private fun createActionsFromNotification(sbn: StatusBarNotification):
-            Pair<List<MediaAction>, List<Int>> {
-        val notif = sbn.notification
-        val actionIcons: MutableList<MediaAction> = ArrayList()
-        val actions = notif.actions
-        var actionsToShowCollapsed = notif.extras.getIntArray(
-            Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf()
-        if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
-            Log.e(TAG, "Too many compact actions for ${sbn.key}," +
-                "limiting to first $MAX_COMPACT_ACTIONS")
-            actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
-        }
-
-        if (actions != null) {
-            for ((index, action) in actions.withIndex()) {
-                if (index == MAX_NOTIFICATION_ACTIONS) {
-                    Log.w(TAG, "Too many notification actions for ${sbn.key}," +
-                        " limiting to first $MAX_NOTIFICATION_ACTIONS")
-                    break
-                }
-                if (action.getIcon() == null) {
-                    if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
-                    actionsToShowCollapsed.remove(index)
-                    continue
-                }
-                val runnable = if (action.actionIntent != null) {
-                    Runnable {
-                        if (action.actionIntent.isActivity) {
-                            activityStarter.startPendingIntentDismissingKeyguard(
-                                action.actionIntent)
-                        } else if (action.isAuthenticationRequired()) {
-                            activityStarter.dismissKeyguardThenExecute({
-                                var result = sendPendingIntent(action.actionIntent)
-                                result
-                            }, {}, true)
-                        } else {
-                            sendPendingIntent(action.actionIntent)
-                        }
-                    }
-                } else {
-                    null
-                }
-                val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
-                    Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
-                } else {
-                    action.getIcon()
-                }.setTint(themeText).loadDrawable(context)
-                val mediaAction = MediaAction(
-                    mediaActionIcon,
-                    runnable,
-                    action.title,
-                    null)
-                actionIcons.add(mediaAction)
-            }
-        }
-        return Pair(actionIcons, actionsToShowCollapsed)
-    }
-
-    /**
-     * Generates action button info for this media session based on the PlaybackState
-     *
-     * @param packageName Package name for the media app
-     * @param controller MediaController for the current session
-     * @return a Pair consisting of a list of media actions, and a list of ints representing which
-     *      of those actions should be shown in the compact player
-     */
-    private fun createActionsFromState(
-        packageName: String,
-        controller: MediaController,
-        user: UserHandle
-    ): MediaButton? {
-        val state = controller.playbackState
-        if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
-            return null
-        }
-
-        // First, check for standard actions
-        val playOrPause = if (isConnectingState(state.state)) {
-            // Spinner needs to be animating to render anything. Start it here.
-            val drawable = context.getDrawable(
-                com.android.internal.R.drawable.progress_small_material)
-            (drawable as Animatable).start()
-            MediaAction(
-                drawable,
-                null, // no action to perform when clicked
-                context.getString(R.string.controls_media_button_connecting),
-                context.getDrawable(R.drawable.ic_media_connecting_container),
-                // Specify a rebind id to prevent the spinner from restarting on later binds.
-                com.android.internal.R.drawable.progress_small_material
-            )
-        } else if (isPlayingState(state.state)) {
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
-        } else {
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
-        }
-        val prevButton = getStandardAction(controller, state.actions,
-            PlaybackState.ACTION_SKIP_TO_PREVIOUS)
-        val nextButton = getStandardAction(controller, state.actions,
-            PlaybackState.ACTION_SKIP_TO_NEXT)
-
-        // Then, create a way to build any custom actions that will be needed
-        val customActions = state.customActions.asSequence().filterNotNull().map {
-            getCustomAction(state, packageName, controller, it)
-        }.iterator()
-        fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
-
-        // Finally, assign the remaining button slots: play/pause A B C D
-        // A = previous, else custom action (if not reserved)
-        // B = next, else custom action (if not reserved)
-        // C and D are always custom actions
-        val reservePrev = controller.extras?.getBoolean(
-            MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV) == true
-        val reserveNext = controller.extras?.getBoolean(
-            MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT) == true
-
-        val prevOrCustom = if (prevButton != null) {
-            prevButton
-        } else if (!reservePrev) {
-            nextCustomAction()
-        } else {
-            null
-        }
-
-        val nextOrCustom = if (nextButton != null) {
-            nextButton
-        } else if (!reserveNext) {
-            nextCustomAction()
-        } else {
-            null
-        }
-
-        return MediaButton(
-            playOrPause,
-            nextOrCustom,
-            prevOrCustom,
-            nextCustomAction(),
-            nextCustomAction(),
-            reserveNext,
-            reservePrev
-        )
-    }
-
-    /**
-     * Create a [MediaAction] for a given action and media session
-     *
-     * @param controller MediaController for the session
-     * @param stateActions The actions included with the session's [PlaybackState]
-     * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
-     *      [PlaybackState.ACTION_PLAY]
-     *      [PlaybackState.ACTION_PAUSE]
-     *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
-     *      [PlaybackState.ACTION_SKIP_TO_NEXT]
-     * @return A [MediaAction] with correct values set, or null if the state doesn't support it
-     */
-    private fun getStandardAction(
-        controller: MediaController,
-        stateActions: Long,
-        @PlaybackState.Actions action: Long
-    ): MediaAction? {
-        if (!includesAction(stateActions, action)) {
-            return null
-        }
-
-        return when (action) {
-            PlaybackState.ACTION_PLAY -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_play),
-                    { controller.transportControls.play() },
-                    context.getString(R.string.controls_media_button_play),
-                    context.getDrawable(R.drawable.ic_media_play_container)
-                )
-            }
-            PlaybackState.ACTION_PAUSE -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_pause),
-                    { controller.transportControls.pause() },
-                    context.getString(R.string.controls_media_button_pause),
-                    context.getDrawable(R.drawable.ic_media_pause_container)
-                )
-            }
-            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_prev),
-                    { controller.transportControls.skipToPrevious() },
-                    context.getString(R.string.controls_media_button_prev),
-                    null
-                )
-            }
-            PlaybackState.ACTION_SKIP_TO_NEXT -> {
-                MediaAction(
-                    context.getDrawable(R.drawable.ic_media_next),
-                    { controller.transportControls.skipToNext() },
-                    context.getString(R.string.controls_media_button_next),
-                    null
-                )
-            }
-            else -> null
-        }
-    }
-
-    /**
-     * Check whether the actions from a [PlaybackState] include a specific action
-     */
-    private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
-        if ((action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
-                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)) {
-            return true
-        }
-        return (stateActions and action != 0L)
-    }
-
-    /**
-     * Get a [MediaAction] representing a [PlaybackState.CustomAction]
-     */
-    private fun getCustomAction(
-        state: PlaybackState,
-        packageName: String,
-        controller: MediaController,
-        customAction: PlaybackState.CustomAction
-    ): MediaAction {
-        return MediaAction(
-            Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
-            { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
-            customAction.name,
-            null
-        )
-    }
-
-    /**
-     * Load a bitmap from the various Art metadata URIs
-     */
-    private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
-        for (uri in ART_URIS) {
-            val uriString = metadata.getString(uri)
-            if (!TextUtils.isEmpty(uriString)) {
-                val albumArt = loadBitmapFromUri(Uri.parse(uriString))
-                if (albumArt != null) {
-                    if (DEBUG) Log.d(TAG, "loaded art from $uri")
-                    return albumArt
-                }
-            }
-        }
-        return null
-    }
-
-    private fun sendPendingIntent(intent: PendingIntent): Boolean {
-        return try {
-            intent.send()
-            true
-        } catch (e: PendingIntent.CanceledException) {
-            Log.d(TAG, "Intent canceled", e)
-            false
-        }
-    }
-    /**
-     * Load a bitmap from a URI
-     * @param uri the uri to load
-     * @return bitmap, or null if couldn't be loaded
-     */
-    private fun loadBitmapFromUri(uri: Uri): Bitmap? {
-        // ImageDecoder requires a scheme of the following types
-        if (uri.scheme == null) {
-            return null
-        }
-
-        if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
-                !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
-                !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
-            return null
-        }
-
-        val source = ImageDecoder.createSource(context.getContentResolver(), uri)
-        return try {
-            ImageDecoder.decodeBitmap(source) {
-                decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
-            }
-        } catch (e: IOException) {
-            Log.e(TAG, "Unable to load bitmap", e)
-            null
-        } catch (e: RuntimeException) {
-            Log.e(TAG, "Unable to load bitmap", e)
-            null
-        }
-    }
-
-    private fun getResumeMediaAction(action: Runnable): MediaAction {
-        return MediaAction(
-            Icon.createWithResource(context, R.drawable.ic_media_play)
-                .setTint(themeText).loadDrawable(context),
-            action,
-            context.getString(R.string.controls_media_resume),
-            context.getDrawable(R.drawable.ic_media_play_container)
-        )
-    }
-
-    fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData
-    ) = traceSection("MediaDataManager#onMediaDataLoaded") {
-        Assert.isMainThread()
-        if (mediaEntries.containsKey(key)) {
-            // Otherwise this was removed already
-            mediaEntries.put(key, data)
-            notifyMediaDataLoaded(key, oldKey, data)
-        }
-    }
-
-    override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
-        if (!allowMediaRecommendations) {
-            if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
-            return
-        }
-
-        val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
-        when (mediaTargets.size) {
-            0 -> {
-                if (!smartspaceMediaData.isActive) {
-                    return
-                }
-                if (DEBUG) {
-                    Log.d(TAG, "Set Smartspace media to be inactive for the data update")
-                }
-                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                    targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId)
-                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
-            }
-            1 -> {
-                val newMediaTarget = mediaTargets.get(0)
-                if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
-                    // The same Smartspace updates can be received. Skip the duplicate updates.
-                    return
-                }
-                if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
-                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
-                notifySmartspaceMediaDataLoaded(
-                    smartspaceMediaData.targetId, smartspaceMediaData)
-            }
-            else -> {
-                // There should NOT be more than 1 Smartspace media update. When it happens, it
-                // indicates a bad state or an error. Reset the status accordingly.
-                Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
-                notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId, false /* immediately */)
-                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
-            }
-        }
-    }
-
-    fun onNotificationRemoved(key: String) {
-        Assert.isMainThread()
-        val removed = mediaEntries.remove(key)
-        if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
-            Log.d(TAG, "Not removing $key because resumable")
-            // Move to resume key (aka package name) if that key doesn't already exist.
-            val resumeAction = getResumeMediaAction(removed.resumeAction!!)
-            val updated = removed.copy(token = null, actions = listOf(resumeAction),
-                    semanticActions = MediaButton(playOrPause = resumeAction),
-                    actionsToShowInCompact = listOf(0), active = false, resumption = true,
-                    isPlaying = false, isClearable = true)
-            val pkg = removed.packageName
-            val migrate = mediaEntries.put(pkg, updated) == null
-            // Notify listeners of "new" controls when migrating or removed and update when not
-            if (migrate) {
-                notifyMediaDataLoaded(pkg, key, updated)
-            } else {
-                // Since packageName is used for the key of the resumption controls, it is
-                // possible that another notification has already been reused for the resumption
-                // controls of this package. In this case, rather than renaming this player as
-                // packageName, just remove it and then send a update to the existing resumption
-                // controls.
-                notifyMediaDataRemoved(key)
-                notifyMediaDataLoaded(pkg, pkg, updated)
-            }
-            logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
-            return
-        }
-        if (removed != null) {
-            notifyMediaDataRemoved(key)
-            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
-        }
-    }
-
-    fun setMediaResumptionEnabled(isEnabled: Boolean) {
-        if (useMediaResumption == isEnabled) {
-            return
-        }
-
-        useMediaResumption = isEnabled
-
-        if (!useMediaResumption) {
-            // Remove any existing resume controls
-            val filtered = mediaEntries.filter { !it.value.active }
-            filtered.forEach {
-                mediaEntries.remove(it.key)
-                notifyMediaDataRemoved(it.key)
-                logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
-            }
-        }
-    }
-
-    /**
-     * Invoked when the user has dismissed the media carousel
-     */
-    fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
-
-    /**
-     * Are there any media notifications active, including the recommendations?
-     */
-    fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
-
-    /**
-     * Are there any media entries we should display, including the recommendations?
-     * If resumption is enabled, this will include inactive players
-     * If resumption is disabled, we only want to show active players
-     */
-    fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
-
-    /**
-     * Are there any resume media notifications active, excluding the recommendations?
-     */
-    fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
-
-    /**
-    * Are there any resume media notifications active, excluding the recommendations?
-    * If resumption is enabled, this will include inactive players
-    * If resumption is disabled, we only want to show active players
-    */
-    fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
-
-    interface Listener {
-
-        /**
-         * Called whenever there's new MediaData Loaded for the consumption in views.
-         *
-         * oldKey is provided to check whether the view has changed keys, which can happen when a
-         * player has gone from resume state (key is package name) to active state (key is
-         * notification key) or vice versa.
-         *
-         * @param immediately indicates should apply the UI changes immediately, otherwise wait
-         * until the next refresh-round before UI becomes visible. True by default to take in place
-         * immediately.
-         *
-         * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
-         * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
-         * signal.
-         *
-         * @param isSsReactivated indicates resume media card is reactivated by Smartspace
-         * recommendation signal
-         */
-        fun onMediaDataLoaded(
-            key: String,
-            oldKey: String?,
-            data: MediaData,
-            immediately: Boolean = true,
-            receivedSmartspaceCardLatency: Int = 0,
-            isSsReactivated: Boolean = false
-        ) {}
-
-        /**
-         * Called whenever there's new Smartspace media data loaded.
-         *
-         * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
-         * it will be prioritized as the first card. Otherwise, it will show up as the last card as
-         * default.
-         */
-        fun onSmartspaceMediaDataLoaded(
-            key: String,
-            data: SmartspaceMediaData,
-            shouldPrioritize: Boolean = false
-        ) {}
-
-        /** Called whenever a previously existing Media notification was removed. */
-        fun onMediaDataRemoved(key: String) {}
-
-        /**
-         * Called whenever a previously existing Smartspace media data was removed.
-         *
-         * @param immediately indicates should apply the UI changes immediately, otherwise wait
-         * until the next refresh-round before UI becomes visible. True by default to take in place
-         * immediately.
-         */
-        fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
-    }
-
-    /**
-     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
-     *
-     * @return An empty SmartspaceMediaData with the valid target Id is returned if the
-     * SmartspaceTarget's data is invalid.
-     */
-    private fun toSmartspaceMediaData(
-        target: SmartspaceTarget,
-        isActive: Boolean
-    ): SmartspaceMediaData {
-        var dismissIntent: Intent? = null
-        if (target.baseAction != null && target.baseAction.extras != null) {
-            dismissIntent = target
-                .baseAction
-                .extras
-                .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
-        }
-        packageName(target)?.let {
-            return SmartspaceMediaData(
-                targetId = target.smartspaceTargetId,
-                isActive = isActive,
-                packageName = it,
-                cardAction = target.baseAction,
-                recommendations = target.iconGrid,
-                dismissIntent = dismissIntent,
-                headphoneConnectionTimeMillis = target.creationTimeMillis,
-                instanceId = logger.getNewInstanceId())
-        }
-        return EMPTY_SMARTSPACE_MEDIA_DATA
-            .copy(targetId = target.smartspaceTargetId,
-                    isActive = isActive,
-                    dismissIntent = dismissIntent,
-                    headphoneConnectionTimeMillis = target.creationTimeMillis,
-                    instanceId = logger.getNewInstanceId())
-    }
-
-    private fun packageName(target: SmartspaceTarget): String? {
-        val recommendationList = target.iconGrid
-        if (recommendationList == null || recommendationList.isEmpty()) {
-            Log.w(TAG, "Empty or null media recommendation list.")
-            return null
-        }
-        for (recommendation in recommendationList) {
-            val extras = recommendation.extras
-            extras?.let {
-                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let {
-                    packageName -> return packageName }
-            }
-        }
-        Log.w(TAG, "No valid package name is provided.")
-        return null
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("internalListeners: $internalListeners")
-            println("externalListeners: ${mediaDataFilter.listeners}")
-            println("mediaEntries: $mediaEntries")
-            println("useMediaResumption: $useMediaResumption")
-            println("allowMediaRecommendations: $allowMediaRecommendations")
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java
deleted file mode 100644
index b8185b9..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java
+++ /dev/null
@@ -1,43 +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.media;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.text.TextUtils;
-
-public class MediaDataUtils {
-
-    public static String getAppLabel(Context context, String packageName, String unknownName) {
-        if (TextUtils.isEmpty(packageName)) {
-            return null;
-        }
-        final PackageManager packageManager = context.getPackageManager();
-        ApplicationInfo applicationInfo;
-        try {
-            applicationInfo = packageManager.getApplicationInfo(packageName, 0);
-        } catch (PackageManager.NameNotFoundException e) {
-            applicationInfo = null;
-        }
-        final String applicationName =
-                (String) (applicationInfo != null
-                        ? packageManager.getApplicationLabel(applicationInfo)
-                        : unknownName);
-        return applicationName;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
deleted file mode 100644
index b3a4ddf..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.bluetooth.BluetoothLeBroadcast
-import android.bluetooth.BluetoothLeBroadcastMetadata
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.media.MediaRouter2Manager
-import android.media.session.MediaController
-import android.text.TextUtils
-import android.util.Log
-import androidx.annotation.AnyThread
-import androidx.annotation.MainThread
-import androidx.annotation.WorkerThread
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.media.LocalMediaManager
-import com.android.settingslib.media.MediaDevice
-import com.android.systemui.Dumpable
-import com.android.systemui.R
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
-import com.android.systemui.statusbar.policy.ConfigurationController
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-private const val PLAYBACK_TYPE_UNKNOWN = 0
-private const val TAG = "MediaDeviceManager"
-private const val DEBUG = true
-
-/**
- * Provides information about the route (ie. device) where playback is occurring.
- */
-class MediaDeviceManager @Inject constructor(
-    private val context: Context,
-    private val controllerFactory: MediaControllerFactory,
-    private val localMediaManagerFactory: LocalMediaManagerFactory,
-    private val mr2manager: MediaRouter2Manager,
-    private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
-    private val configurationController: ConfigurationController,
-    private val localBluetoothManager: LocalBluetoothManager?,
-    @Main private val fgExecutor: Executor,
-    @Background private val bgExecutor: Executor,
-    dumpManager: DumpManager
-) : MediaDataManager.Listener, Dumpable {
-
-    private val listeners: MutableSet<Listener> = mutableSetOf()
-    private val entries: MutableMap<String, Entry> = mutableMapOf()
-
-    init {
-        dumpManager.registerDumpable(javaClass.name, this)
-    }
-
-    /**
-     * Add a listener for changes to the media route (ie. device).
-     */
-    fun addListener(listener: Listener) = listeners.add(listener)
-
-    /**
-     * Remove a listener that has been registered with addListener.
-     */
-    fun removeListener(listener: Listener) = listeners.remove(listener)
-
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        if (oldKey != null && oldKey != key) {
-            val oldEntry = entries.remove(oldKey)
-            oldEntry?.stop()
-        }
-        var entry = entries[key]
-        if (entry == null || entry.token != data.token) {
-            entry?.stop()
-            if (data.device != null) {
-                // If we were already provided device info (e.g. from RCN), keep that and don't
-                // listen for updates, but process once to push updates to listeners
-                processDevice(key, oldKey, data.device)
-                return
-            }
-            val controller = data.token?.let {
-                controllerFactory.create(it)
-            }
-            val localMediaManager = localMediaManagerFactory.create(data.packageName)
-            val muteAwaitConnectionManager =
-                    muteAwaitConnectionManagerFactory.create(localMediaManager)
-            entry = Entry(
-                key,
-                oldKey,
-                controller,
-                localMediaManager,
-                muteAwaitConnectionManager
-            )
-            entries[key] = entry
-            entry.start()
-        }
-    }
-
-    override fun onMediaDataRemoved(key: String) {
-        val token = entries.remove(key)
-        token?.stop()
-        token?.let {
-            listeners.forEach {
-                it.onKeyRemoved(key)
-            }
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<String>) {
-        with(pw) {
-            println("MediaDeviceManager state:")
-            entries.forEach { (key, entry) ->
-                println("  key=$key")
-                entry.dump(pw)
-            }
-        }
-    }
-
-    @MainThread
-    private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
-        listeners.forEach {
-            it.onMediaDeviceChanged(key, oldKey, device)
-        }
-    }
-
-    interface Listener {
-        /** Called when the route has changed for a given notification. */
-        fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
-        /** Called when the notification was removed. */
-        fun onKeyRemoved(key: String)
-    }
-
-    private inner class Entry(
-        val key: String,
-        val oldKey: String?,
-        val controller: MediaController?,
-        val localMediaManager: LocalMediaManager,
-        val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
-    ) : LocalMediaManager.DeviceCallback, MediaController.Callback(),
-            BluetoothLeBroadcast.Callback {
-
-        val token
-            get() = controller?.sessionToken
-        private var started = false
-        private var playbackType = PLAYBACK_TYPE_UNKNOWN
-        private var current: MediaDeviceData? = null
-            set(value) {
-                val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
-                if (!started || !sameWithoutIcon) {
-                    field = value
-                    fgExecutor.execute {
-                        processDevice(key, oldKey, value)
-                    }
-                }
-            }
-        // A device that is not yet connected but is expected to connect imminently. Because it's
-        // expected to connect imminently, it should be displayed as the current device.
-        private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
-        private var broadcastDescription: String? = null
-        private val configListener = object : ConfigurationController.ConfigurationListener {
-            override fun onLocaleListChanged() {
-                updateCurrent()
-            }
-        }
-
-        @AnyThread
-        fun start() = bgExecutor.execute {
-            if (!started) {
-                localMediaManager.registerCallback(this)
-                localMediaManager.startScan()
-                muteAwaitConnectionManager?.startListening()
-                playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
-                controller?.registerCallback(this)
-                updateCurrent()
-                started = true
-                configurationController.addCallback(configListener)
-            }
-        }
-
-        @AnyThread
-        fun stop() = bgExecutor.execute {
-            if (started) {
-                started = false
-                controller?.unregisterCallback(this)
-                localMediaManager.stopScan()
-                localMediaManager.unregisterCallback(this)
-                muteAwaitConnectionManager?.stopListening()
-                configurationController.removeCallback(configListener)
-            }
-        }
-
-        fun dump(pw: PrintWriter) {
-            val routingSession = controller?.let {
-                mr2manager.getRoutingSessionForMediaController(it)
-            }
-            val selectedRoutes = routingSession?.let {
-                mr2manager.getSelectedRoutes(it)
-            }
-            with(pw) {
-                println("    current device is ${current?.name}")
-                val type = controller?.playbackInfo?.playbackType
-                println("    PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType")
-                println("    routingSession=$routingSession")
-                println("    selectedRoutes=$selectedRoutes")
-            }
-        }
-
-        @WorkerThread
-        override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
-            val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
-            if (newPlaybackType == playbackType) {
-                return
-            }
-            playbackType = newPlaybackType
-            updateCurrent()
-        }
-
-        override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute {
-            updateCurrent()
-        }
-
-        override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
-            bgExecutor.execute {
-                updateCurrent()
-            }
-        }
-
-        override fun onAboutToConnectDeviceAdded(
-            deviceAddress: String,
-            deviceName: String,
-            deviceIcon: Drawable?
-        ) {
-            aboutToConnectDeviceOverride = AboutToConnectDevice(
-                fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
-                backupMediaDeviceData = MediaDeviceData(
-                        /* enabled */ enabled = true,
-                        /* icon */ deviceIcon,
-                        /* name */ deviceName,
-                        /* showBroadcastButton */ showBroadcastButton = false)
-            )
-            updateCurrent()
-        }
-
-        override fun onAboutToConnectDeviceRemoved() {
-            aboutToConnectDeviceOverride = null
-            updateCurrent()
-        }
-
-        override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
-            }
-            updateCurrent()
-        }
-
-        override fun onBroadcastStartFailed(reason: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastStartFailed(), reason = $reason")
-            }
-        }
-
-        override fun onBroadcastMetadataChanged(
-            broadcastId: Int,
-            metadata: BluetoothLeBroadcastMetadata
-        ) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
-                        "metadata = $metadata")
-            }
-            updateCurrent()
-        }
-
-        override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
-            }
-            updateCurrent()
-        }
-
-        override fun onBroadcastStopFailed(reason: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastStopFailed(), reason = $reason")
-            }
-        }
-
-        override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId")
-            }
-            updateCurrent()
-        }
-
-        override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
-            if (DEBUG) {
-                Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " +
-                        "broadcastId = $broadcastId")
-            }
-        }
-
-        override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
-
-        override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
-
-        @WorkerThread
-        private fun updateCurrent() {
-            if (isLeAudioBroadcastEnabled()) {
-                current = MediaDeviceData(
-                        /* enabled */ true,
-                        /* icon */ context.getDrawable(R.drawable.settings_input_antenna),
-                        /* name */ broadcastDescription,
-                        /* intent */ null,
-                        /* showBroadcastButton */ showBroadcastButton = true)
-            } else {
-                val aboutToConnect = aboutToConnectDeviceOverride
-                if (aboutToConnect != null &&
-                        aboutToConnect.fullMediaDevice == null &&
-                        aboutToConnect.backupMediaDeviceData != null) {
-                    // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
-                    current = aboutToConnect.backupMediaDeviceData
-                    return
-                }
-                val device = aboutToConnect?.fullMediaDevice
-                        ?: localMediaManager.currentConnectedDevice
-                val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
-
-                // If we have a controller but get a null route, then don't trust the device
-                val enabled = device != null && (controller == null || route != null)
-                val name = if (controller == null || route != null) {
-                    route?.name?.toString() ?: device?.name
-                } else {
-                    null
-                }
-                current = MediaDeviceData(enabled, device?.iconWithoutBackground, name,
-                        id = device?.id, showBroadcastButton = false)
-            }
-        }
-
-        private fun isLeAudioBroadcastEnabled(): Boolean {
-            if (localBluetoothManager != null) {
-                val profileManager = localBluetoothManager.profileManager
-                if (profileManager != null) {
-                    val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile
-                    if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) {
-                        getBroadcastingInfo(bluetoothLeBroadcast)
-                        return true
-                    } else if (DEBUG) {
-                        Log.d(TAG, "Can not get LocalBluetoothLeBroadcast")
-                    }
-                } else if (DEBUG) {
-                    Log.d(TAG, "Can not get LocalBluetoothProfileManager")
-                }
-            } else if (DEBUG) {
-                Log.d(TAG, "Can not get LocalBluetoothManager")
-            }
-            return false
-        }
-
-        private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
-            var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
-            // TODO(b/233698402): Use the package name instead of app label to avoid the
-            // unexpected result.
-            // Check the current media app's name is the same with current broadcast app's name
-            // or not.
-            var mediaApp = MediaDataUtils.getAppLabel(
-                    context, localMediaManager.packageName,
-                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name))
-            var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
-            if (isCurrentBroadcastedApp) {
-                broadcastDescription = context.getString(
-                        R.string.broadcasting_description_is_broadcasting)
-            } else {
-                broadcastDescription = currentBroadcastedApp
-            }
-        }
-    }
-}
-
-/**
- * A class storing information for the about-to-connect device. See
- * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
- *
- * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
- *   non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
- * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
- *   information required to display the device. Only use if [fullMediaDevice] is null.
- */
-private data class AboutToConnectDevice(
-    val fullMediaDevice: MediaDevice? = null,
-    val backupMediaDeviceData: MediaDeviceData? = null
-)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt b/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
deleted file mode 100644
index 75eb33d..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-import com.android.systemui.util.Utils
-import javax.inject.Inject
-
-/**
- * Provides access to the current value of the feature flag.
- */
-class MediaFeatureFlag @Inject constructor(private val context: Context) {
-    val enabled
-        get() = Utils.useQsMediaPlayer(context)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
deleted file mode 100644
index b85ae48..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
+++ /dev/null
@@ -1,47 +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.
- */
-
-package com.android.systemui.media
-
-import android.app.StatusBarManager
-import android.os.UserHandle
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import javax.inject.Inject
-
-@SysUISingleton
-class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
-    /**
-     * Check whether media control actions should be based on PlaybackState instead of notification
-     */
-    fun areMediaSessionActionsEnabled(packageName: String, user: UserHandle): Boolean {
-        val enabled = StatusBarManager.useMediaSessionActionsForApp(packageName, user)
-        // Allow global override with flag
-        return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
-    }
-
-    /**
-     * Check whether we support displaying information about mute await connections.
-     */
-    fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
-
-    /**
-     * Check whether we enable support for nearby media devices. See
-     * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
-     */
-    fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
deleted file mode 100644
index e0b6d1f..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ /dev/null
@@ -1,1243 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.annotation.IntDef
-import android.content.Context
-import android.content.res.Configuration
-import android.database.ContentObserver
-import android.graphics.Rect
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.util.Log
-import android.util.MathUtils
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroupOverlay
-import androidx.annotation.VisibleForTesting
-import com.android.keyguard.KeyguardViewController
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dreams.DreamOverlayStateController
-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.statusbar.CrossFadeHelper
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.LargeScreenUtils
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-private val TAG: String = MediaHierarchyManager::class.java.simpleName
-
-/**
- * Similarly to isShown but also excludes views that have 0 alpha
- */
-val View.isShownNotFaded: Boolean
-    get() {
-        var current: View = this
-        while (true) {
-            if (current.visibility != View.VISIBLE) {
-                return false
-            }
-            if (current.alpha == 0.0f) {
-                return false
-            }
-            val parent = current.parent ?: return false // We are not attached to the view root
-            if (parent !is View) {
-                // we reached the viewroot, hurray
-                return true
-            }
-            current = parent
-        }
-    }
-
-/**
- * This manager is responsible for placement of the unique media view between the different hosts
- * and animate the positions of the views to achieve seamless transitions.
- */
-@SysUISingleton
-class MediaHierarchyManager @Inject constructor(
-    private val context: Context,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val keyguardStateController: KeyguardStateController,
-    private val bypassController: KeyguardBypassController,
-    private val mediaCarouselController: MediaCarouselController,
-    private val keyguardViewController: KeyguardViewController,
-    private val dreamOverlayStateController: DreamOverlayStateController,
-    configurationController: ConfigurationController,
-    wakefulnessLifecycle: WakefulnessLifecycle,
-    panelEventsEvents: NotifPanelEvents,
-    private val secureSettings: SecureSettings,
-    @Main private val handler: Handler,
-) {
-
-    /**
-     * Track the media player setting status on lock screen.
-     */
-    private var allowMediaPlayerOnLockScreen: Boolean = true
-    private val lockScreenMediaPlayerUri =
-            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
-
-    /**
-     * Whether we "skip" QQS during panel expansion.
-     *
-     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
-     * start closing the panel, it fully collapses instead of going to QQS.
-     */
-    private var skipQqsOnExpansion: Boolean = false
-
-    /**
-     * The root overlay of the hierarchy. This is where the media notification is attached to
-     * whenever the view is transitioning from one host to another. It also make sure that the
-     * view is always in its final state when it is attached to a view host.
-     */
-    private var rootOverlay: ViewGroupOverlay? = null
-
-    private var rootView: View? = null
-    private var currentBounds = Rect()
-    private var animationStartBounds: Rect = Rect()
-
-    private var animationStartClipping = Rect()
-    private var currentClipping = Rect()
-    private var targetClipping = Rect()
-
-    /**
-     * The cross fade progress at the start of the animation. 0.5f means it's just switching between
-     * the start and the end location and the content is fully faded, while 0.75f means that we're
-     * halfway faded in again in the target state.
-     */
-    private var animationStartCrossFadeProgress = 0.0f
-
-    /**
-     * The starting alpha of the animation
-     */
-    private var animationStartAlpha = 0.0f
-
-    /**
-     * The starting location of the cross fade if an animation is running right now.
-     */
-    @MediaLocation
-    private var crossFadeAnimationStartLocation = -1
-
-    /**
-     * The end location of the cross fade if an animation is running right now.
-     */
-    @MediaLocation
-    private var crossFadeAnimationEndLocation = -1
-    private var targetBounds: Rect = Rect()
-    private val mediaFrame
-        get() = mediaCarouselController.mediaFrame
-    private var statusbarState: Int = statusBarStateController.state
-    private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
-        interpolator = Interpolators.FAST_OUT_SLOW_IN
-        addUpdateListener {
-            updateTargetState()
-            val currentAlpha: Float
-            var boundsProgress = animatedFraction
-            if (isCrossFadeAnimatorRunning) {
-                animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f,
-                    animatedFraction)
-                // When crossfading, let's keep the bounds at the right location during fading
-                boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
-                currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress)
-            } else {
-                // If we're not crossfading, let's interpolate from the start alpha to 1.0f
-                currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
-            }
-            interpolateBounds(animationStartBounds, targetBounds, boundsProgress,
-                    result = currentBounds)
-            resolveClipping(currentClipping)
-            applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
-        }
-        addListener(object : AnimatorListenerAdapter() {
-            private var cancelled: Boolean = false
-
-            override fun onAnimationCancel(animation: Animator?) {
-                cancelled = true
-                animationPending = false
-                rootView?.removeCallbacks(startAnimation)
-            }
-
-            override fun onAnimationEnd(animation: Animator?) {
-                isCrossFadeAnimatorRunning = false
-                if (!cancelled) {
-                    applyTargetStateIfNotAnimating()
-                }
-            }
-
-            override fun onAnimationStart(animation: Animator?) {
-                cancelled = false
-                animationPending = false
-            }
-        })
-    }
-
-    private fun resolveClipping(result: Rect) {
-        if (animationStartClipping.isEmpty) result.set(targetClipping)
-        else if (targetClipping.isEmpty) result.set(animationStartClipping)
-        else result.setIntersect(animationStartClipping, targetClipping)
-    }
-
-    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1)
-    /**
-     * The last location where this view was at before going to the desired location. This is
-     * useful for guided transitions.
-     */
-    @MediaLocation
-    private var previousLocation = -1
-    /**
-     * The desired location where the view will be at the end of the transition.
-     */
-    @MediaLocation
-    private var desiredLocation = -1
-
-    /**
-     * The current attachment location where the view is currently attached.
-     * Usually this matches the desired location except for animations whenever a view moves
-     * to the new desired location, during which it is in [IN_OVERLAY].
-     */
-    @MediaLocation
-    private var currentAttachmentLocation = -1
-
-    private var inSplitShade = false
-
-    /**
-     * Is there any active media in the carousel?
-     */
-    private var hasActiveMedia: Boolean = false
-        get() = mediaHosts.get(LOCATION_QQS)?.visible == true
-
-    /**
-     * Are we currently waiting on an animation to start?
-     */
-    private var animationPending: Boolean = false
-    private val startAnimation: Runnable = Runnable { animator.start() }
-
-    /**
-     * The expansion of quick settings
-     */
-    var qsExpansion: Float = 0.0f
-        set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation()
-                if (getQSTransformationProgress() >= 0) {
-                    updateTargetState()
-                    applyTargetStateIfNotAnimating()
-                }
-            }
-        }
-
-    /**
-     * Is quick setting expanded?
-     */
-    var qsExpanded: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
-            }
-            // qs is expanded on LS shade and HS shade
-            if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
-                mediaCarouselController.logSmartspaceImpression(value)
-            }
-            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-        }
-
-    /**
-     * distance that the full shade transition takes in order for media to fully transition to the
-     * shade
-     */
-    private var distanceForFullShadeTransition = 0
-
-    /**
-     * The amount of progress we are currently in if we're transitioning to the full shade.
-     * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
-     * shade.
-     */
-    private var fullShadeTransitionProgress = 0f
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
-                // No need to do all the calculations / updates below if we're not on the lockscreen
-                // or if we're bypassing.
-                return
-            }
-            updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
-            if (value >= 0) {
-                updateTargetState()
-                // Setting the alpha directly, as the below call will use it to update the alpha
-                carouselAlpha = calculateAlphaFromCrossFade(field)
-                applyTargetStateIfNotAnimating()
-            }
-        }
-
-    /**
-     * Is there currently a cross-fade animation running driven by an animator?
-     */
-    private var isCrossFadeAnimatorRunning = false
-
-    /**
-     * Are we currently transitionioning from the lockscreen to the full shade
-     * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
-     * the transition starts, this will no longer return true.
-     */
-    private val isTransitioningToFullShade: Boolean
-        get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled &&
-            statusbarState == StatusBarState.KEYGUARD
-
-    /**
-     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
-     * shade. 0.0f means we're not transitioning yet.
-     */
-    fun setTransitionToFullShadeAmount(value: Float) {
-        // If we're transitioning starting on the shade_locked, we don't want any delay and rather
-        // have it aligned with the rest of the animation
-        val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
-        fullShadeTransitionProgress = progress
-    }
-
-    /**
-     * Returns the amount of translationY of the media container, during the current guided
-     * transformation, if running. If there is no guided transformation running, it will return 0.
-     */
-    fun getGuidedTransformationTranslationY(): Int {
-        if (!isCurrentlyInGuidedTransformation()) {
-            return -1
-        }
-        val startHost = getHost(previousLocation) ?: return 0
-        return targetBounds.top - startHost.currentBounds.top
-    }
-
-    /**
-     * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
-     * we wouldn't want to transition in that case.
-     */
-    var collapsingShadeFromQS: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * Are location changes currently blocked?
-     */
-    private val blockLocationChanges: Boolean
-        get() {
-            return goingToSleep || dozeAnimationRunning
-        }
-
-    /**
-     * Are we currently going to sleep
-     */
-    private var goingToSleep: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                if (!value) {
-                    updateDesiredLocation()
-                }
-            }
-        }
-
-    /**
-     * Are we currently fullyAwake
-     */
-    private var fullyAwake: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                if (value) {
-                    updateDesiredLocation(forceNoAnimation = true)
-                }
-            }
-        }
-
-    /**
-     * Is the doze animation currently Running
-     */
-    private var dozeAnimationRunning: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                if (!value) {
-                    updateDesiredLocation()
-                }
-            }
-        }
-
-    /**
-     * Is the dream overlay currently active
-     */
-    private var dreamOverlayActive: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * Is the dream media complication currently active
-     */
-    private var dreamMediaComplicationActive: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * The current cross fade progress. 0.5f means it's just switching
-     * between the start and the end location and the content is fully faded, while 0.75f means
-     * that we're halfway faded in again in the target state.
-     * This is only valid while [isCrossFadeAnimatorRunning] is true.
-     */
-    private var animationCrossFadeProgress = 1.0f
-
-    /**
-     * The current carousel Alpha.
-     */
-    private var carouselAlpha: Float = 1.0f
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            CrossFadeHelper.fadeIn(mediaFrame, value)
-        }
-
-    /**
-     * Calculate the alpha of the view when given a cross-fade progress.
-     *
-     * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
-     * between the start and the end location and the content is fully faded, while 0.75f means
-     * that we're halfway faded in again in the target state.
-     */
-    private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
-        if (crossFadeProgress <= 0.5f) {
-            return 1.0f - crossFadeProgress / 0.5f
-        } else {
-            return (crossFadeProgress - 0.5f) / 0.5f
-        }
-    }
-
-    init {
-        updateConfiguration()
-        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateConfiguration()
-                updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
-            }
-        })
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onStatePreChange(oldState: Int, newState: Int) {
-                // We're updating the location before the state change happens, since we want the
-                // location of the previous state to still be up to date when the animation starts
-                statusbarState = newState
-                updateDesiredLocation()
-            }
-
-            override fun onStateChanged(newState: Int) {
-                updateTargetState()
-                // Enters shade from lock screen
-                if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) {
-                    mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-
-            override fun onDozeAmountChanged(linear: Float, eased: Float) {
-                dozeAnimationRunning = linear != 0.0f && linear != 1.0f
-            }
-
-            override fun onDozingChanged(isDozing: Boolean) {
-                if (!isDozing) {
-                    dozeAnimationRunning = false
-                    // Enters lock screen from screen off
-                    if (isLockScreenVisibleToUser()) {
-                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                    }
-                } else {
-                    updateDesiredLocation()
-                    qsExpanded = false
-                    closeGuts()
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-
-            override fun onExpandedChanged(isExpanded: Boolean) {
-                // Enters shade from home screen
-                if (isHomeScreenShadeVisibleToUser()) {
-                    mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-        })
-
-        dreamOverlayStateController.addCallback(object : DreamOverlayStateController.Callback {
-            override fun onComplicationsChanged() {
-                dreamMediaComplicationActive = dreamOverlayStateController.complications.any {
-                    it is MediaDreamComplication
-                }
-            }
-
-            override fun onStateChanged() {
-                dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
-            }
-        })
-
-        wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
-            override fun onFinishedGoingToSleep() {
-                goingToSleep = false
-            }
-
-            override fun onStartedGoingToSleep() {
-                goingToSleep = true
-                fullyAwake = false
-            }
-
-            override fun onFinishedWakingUp() {
-                goingToSleep = false
-                fullyAwake = true
-            }
-
-            override fun onStartedWakingUp() {
-                goingToSleep = false
-            }
-        })
-
-        mediaCarouselController.updateUserVisibility = {
-            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-        }
-        mediaCarouselController.updateHostVisibility = {
-            mediaHosts.forEach {
-                it?.updateViewVisibility()
-            }
-        }
-
-        panelEventsEvents.registerListener(object : NotifPanelEvents.Listener {
-            override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
-                skipQqsOnExpansion = isExpandImmediateEnabled
-                updateDesiredLocation()
-            }
-        })
-
-        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?) {
-                if (uri == lockScreenMediaPlayerUri) {
-                    allowMediaPlayerOnLockScreen =
-                            secureSettings.getBoolForUser(
-                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                                    true,
-                                    UserHandle.USER_CURRENT
-                            )
-                }
-            }
-        }
-        secureSettings.registerContentObserverForUser(
-                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                settingsObserver,
-                UserHandle.USER_ALL)
-    }
-
-    private fun updateConfiguration() {
-        distanceForFullShadeTransition = context.resources.getDimensionPixelSize(
-                R.dimen.lockscreen_shade_media_transition_distance)
-        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
-    }
-
-    /**
-     * Register a media host and create a view can be attached to a view hierarchy
-     * and where the players will be placed in when the host is the currently desired state.
-     *
-     * @return the hostView associated with this location
-     */
-    fun register(mediaObject: MediaHost): UniqueObjectHostView {
-        val viewHost = createUniqueObjectHost()
-        mediaObject.hostView = viewHost
-        mediaObject.addVisibilityChangeListener {
-            // If QQS changes visibility, we need to force an update to ensure the transition
-            // goes into the correct state
-            val stateUpdate = mediaObject.location == LOCATION_QQS
-
-            // Never animate because of a visibility change, only state changes should do that
-            updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
-        }
-        mediaHosts[mediaObject.location] = mediaObject
-        if (mediaObject.location == desiredLocation) {
-            // In case we are overriding a view that is already visible, make sure we attach it
-            // to this new host view in the below call
-            desiredLocation = -1
-        }
-        if (mediaObject.location == currentAttachmentLocation) {
-            currentAttachmentLocation = -1
-        }
-        updateDesiredLocation()
-        return viewHost
-    }
-
-    /**
-     * Close the guts in all players in [MediaCarouselController].
-     */
-    fun closeGuts() {
-        mediaCarouselController.closeGuts()
-    }
-
-    private fun createUniqueObjectHost(): UniqueObjectHostView {
-        val viewHost = UniqueObjectHostView(context)
-        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(p0: View?) {
-                if (rootOverlay == null) {
-                    rootView = viewHost.viewRootImpl.view
-                    rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
-                }
-                viewHost.removeOnAttachStateChangeListener(this)
-            }
-
-            override fun onViewDetachedFromWindow(p0: View?) {
-            }
-        })
-        return viewHost
-    }
-
-    /**
-     * Updates the location that the view should be in. If it changes, an animation may be triggered
-     * going from the old desired location to the new one.
-     *
-     * @param forceNoAnimation optional parameter telling the system not to animate
-     * @param forceStateUpdate optional parameter telling the system to update transition state
-     *                         even if location did not change
-     */
-    private fun updateDesiredLocation(
-        forceNoAnimation: Boolean = false,
-        forceStateUpdate: Boolean = false
-    ) = traceSection("MediaHierarchyManager#updateDesiredLocation") {
-        val desiredLocation = calculateLocation()
-        if (desiredLocation != this.desiredLocation || forceStateUpdate) {
-            if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
-                // Only update previous location when it actually changes
-                previousLocation = this.desiredLocation
-            } else if (forceStateUpdate) {
-                val onLockscreen = (!bypassController.bypassEnabled &&
-                        (statusbarState == StatusBarState.KEYGUARD))
-                if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN &&
-                        !onLockscreen) {
-                    // If media active state changed and the device is now unlocked, update the
-                    // previous location so we animate between the correct hosts
-                    previousLocation = LOCATION_QQS
-                }
-            }
-            val isNewView = this.desiredLocation == -1
-            this.desiredLocation = desiredLocation
-            // Let's perform a transition
-            val animate = !forceNoAnimation &&
-                    shouldAnimateTransition(desiredLocation, previousLocation)
-            val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
-            val host = getHost(desiredLocation)
-            val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
-            if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
-                // if we're fading, we want the desired location / measurement only to change
-                // once fully faded. This is happening in the host attachment
-                mediaCarouselController.onDesiredLocationChanged(desiredLocation, host,
-                    animate, animDuration, delay)
-            }
-            performTransitionToNewLocation(isNewView, animate)
-        }
-    }
-
-    private fun performTransitionToNewLocation(
-        isNewView: Boolean,
-        animate: Boolean
-    ) = traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
-        if (previousLocation < 0 || isNewView) {
-            cancelAnimationAndApplyDesiredState()
-            return
-        }
-        val currentHost = getHost(desiredLocation)
-        val previousHost = getHost(previousLocation)
-        if (currentHost == null || previousHost == null) {
-            cancelAnimationAndApplyDesiredState()
-            return
-        }
-        updateTargetState()
-        if (isCurrentlyInGuidedTransformation()) {
-            applyTargetStateIfNotAnimating()
-        } else if (animate) {
-            val wasCrossFading = isCrossFadeAnimatorRunning
-            val previewsCrossFadeProgress = animationCrossFadeProgress
-            animator.cancel()
-            if (currentAttachmentLocation != previousLocation ||
-                    !previousHost.hostView.isAttachedToWindow) {
-                // Let's animate to the new position, starting from the current position
-                // We also go in here in case the view was detached, since the bounds wouldn't
-                // be correct anymore
-                animationStartBounds.set(currentBounds)
-                animationStartClipping.set(currentClipping)
-            } else {
-                // otherwise, let's take the freshest state, since the current one could
-                // be outdated
-                animationStartBounds.set(previousHost.currentBounds)
-                animationStartClipping.set(previousHost.currentClipping)
-            }
-            val transformationType = calculateTransformationType()
-            var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
-            var crossFadeStartProgress = 0.0f
-            // The alpha is only relevant when not cross fading
-            var newCrossFadeStartLocation = previousLocation
-            if (wasCrossFading) {
-                if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
-                    if (needsCrossFade) {
-                        // We were previously crossFading and we've already reached
-                        // the end view, Let's start crossfading from the same position there
-                        crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
-                    }
-                    // Otherwise let's fade in from the current alpha, but not cross fade
-                } else {
-                    // We haven't reached the previous location yet, let's still cross fade from
-                    // where we were.
-                    newCrossFadeStartLocation = crossFadeAnimationStartLocation
-                    if (newCrossFadeStartLocation == desiredLocation) {
-                        // we're crossFading back to where we were, let's start at the end position
-                        crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
-                    } else {
-                        // Let's start from where we are right now
-                        crossFadeStartProgress = previewsCrossFadeProgress
-                        // We need to force cross fading as we haven't reached the end location yet
-                        needsCrossFade = true
-                    }
-                }
-            } else if (needsCrossFade) {
-                // let's not flicker and start with the same alpha
-                crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
-            }
-            isCrossFadeAnimatorRunning = needsCrossFade
-            crossFadeAnimationStartLocation = newCrossFadeStartLocation
-            crossFadeAnimationEndLocation = desiredLocation
-            animationStartAlpha = carouselAlpha
-            animationStartCrossFadeProgress = crossFadeStartProgress
-            adjustAnimatorForTransition(desiredLocation, previousLocation)
-            if (!animationPending) {
-                rootView?.let {
-                    // Let's delay the animation start until we finished laying out
-                    animationPending = true
-                    it.postOnAnimation(startAnimation)
-                }
-            }
-        } else {
-            cancelAnimationAndApplyDesiredState()
-        }
-    }
-
-    private fun shouldAnimateTransition(
-        @MediaLocation currentLocation: Int,
-        @MediaLocation previousLocation: Int
-    ): Boolean {
-        if (isCurrentlyInGuidedTransformation()) {
-            return false
-        }
-        if (skipQqsOnExpansion) {
-            return false
-        }
-        // This is an invalid transition, and can happen when using the camera gesture from the
-        // lock screen. Disallow.
-        if (previousLocation == LOCATION_LOCKSCREEN &&
-            desiredLocation == LOCATION_QQS &&
-            statusbarState == StatusBarState.SHADE) {
-            return false
-        }
-
-        if (currentLocation == LOCATION_QQS &&
-                previousLocation == LOCATION_LOCKSCREEN &&
-                (statusBarStateController.leaveOpenOnKeyguardHide() ||
-                        statusbarState == StatusBarState.SHADE_LOCKED)) {
-            // Usually listening to the isShown is enough to determine this, but there is some
-            // non-trivial reattaching logic happening that will make the view not-shown earlier
-            return true
-        }
-
-        if (statusbarState == StatusBarState.KEYGUARD && (currentLocation == LOCATION_LOCKSCREEN ||
-                        previousLocation == LOCATION_LOCKSCREEN)) {
-            // We're always fading from lockscreen to keyguard in situations where the player
-            // is already fully hidden
-            return false
-        }
-        return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
-    }
-
-    private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
-        val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
-        animator.apply {
-            duration = animDuration
-            startDelay = delay
-        }
-    }
-
-    private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
-        var animDuration = 200L
-        var delay = 0L
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
-            // Going to the full shade, let's adjust the animation duration
-            if (statusbarState == StatusBarState.SHADE &&
-                    keyguardStateController.isKeyguardFadingAway) {
-                delay = keyguardStateController.keyguardFadingAwayDelay
-            }
-            animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
-        } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
-            animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
-        }
-        return animDuration to delay
-    }
-
-    private fun applyTargetStateIfNotAnimating() {
-        if (!animator.isRunning) {
-            // Let's immediately apply the target state (which is interpolated) if there is
-            // no animation running. Otherwise the animation update will already update
-            // the location
-            applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
-        }
-    }
-
-    /**
-     * Updates the bounds that the view wants to be in at the end of the animation.
-     */
-    private fun updateTargetState() {
-        var starthost = getHost(previousLocation)
-        var endHost = getHost(desiredLocation)
-        if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading() && starthost != null &&
-            endHost != null) {
-            val progress = getTransformationProgress()
-            // If either of the hosts are invisible, let's keep them at the other host location to
-            // have a nicer disappear animation. Otherwise the currentBounds of the state might
-            // be undefined
-            if (!endHost.visible) {
-                endHost = starthost
-            } else if (!starthost.visible) {
-                starthost = endHost
-            }
-            val newBounds = endHost.currentBounds
-            val previousBounds = starthost.currentBounds
-            targetBounds = interpolateBounds(previousBounds, newBounds, progress)
-            targetClipping = endHost.currentClipping
-        } else if (endHost != null) {
-            val bounds = endHost.currentBounds
-            targetBounds.set(bounds)
-            targetClipping = endHost.currentClipping
-        }
-    }
-
-    private fun interpolateBounds(
-        startBounds: Rect,
-        endBounds: Rect,
-        progress: Float,
-        result: Rect? = null
-    ): Rect {
-        val left = MathUtils.lerp(startBounds.left.toFloat(),
-                endBounds.left.toFloat(), progress).toInt()
-        val top = MathUtils.lerp(startBounds.top.toFloat(),
-                endBounds.top.toFloat(), progress).toInt()
-        val right = MathUtils.lerp(startBounds.right.toFloat(),
-                endBounds.right.toFloat(), progress).toInt()
-        val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
-                endBounds.bottom.toFloat(), progress).toInt()
-        val resultBounds = result ?: Rect()
-        resultBounds.set(left, top, right, bottom)
-        return resultBounds
-    }
-
-    /** @return true if this transformation is guided by an external progress like a finger */
-    fun isCurrentlyInGuidedTransformation(): Boolean {
-        return hasValidStartAndEndLocations() &&
-                getTransformationProgress() >= 0 &&
-                areGuidedTransitionHostsVisible()
-    }
-
-    private fun hasValidStartAndEndLocations(): Boolean {
-        return previousLocation != -1 && desiredLocation != -1
-    }
-
-    /**
-     * Calculate the transformation type for the current animation
-     */
-    @VisibleForTesting
-    @TransformationType
-    fun calculateTransformationType(): Int {
-        if (isTransitioningToFullShade) {
-            if (inSplitShade && areGuidedTransitionHostsVisible()) {
-                return TRANSFORMATION_TYPE_TRANSITION
-            }
-            return TRANSFORMATION_TYPE_FADE
-        }
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
-            previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) {
-            // animating between ls and qs should fade, as QS is clipped.
-            return TRANSFORMATION_TYPE_FADE
-        }
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
-            // animating between ls and qqs should fade when dragging down via e.g. expand button
-            return TRANSFORMATION_TYPE_FADE
-        }
-        return TRANSFORMATION_TYPE_TRANSITION
-    }
-
-    private fun areGuidedTransitionHostsVisible(): Boolean {
-        return getHost(previousLocation)?.visible == true &&
-                getHost(desiredLocation)?.visible == true
-    }
-
-    /**
-     * @return the current transformation progress if we're in a guided transformation and -1
-     * otherwise
-     */
-    private fun getTransformationProgress(): Float {
-        if (skipQqsOnExpansion) {
-            return -1.0f
-        }
-        val progress = getQSTransformationProgress()
-        if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
-            return progress
-        }
-        if (isTransitioningToFullShade) {
-            return fullShadeTransitionProgress
-        }
-        return -1.0f
-    }
-
-    private fun getQSTransformationProgress(): Float {
-        val currentHost = getHost(desiredLocation)
-        val previousHost = getHost(previousLocation)
-        if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
-            if (previousHost?.location == LOCATION_QQS) {
-                if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
-                    return qsExpansion
-                }
-            }
-        }
-        return -1.0f
-    }
-
-    private fun getHost(@MediaLocation location: Int): MediaHost? {
-        if (location < 0) {
-            return null
-        }
-        return mediaHosts[location]
-    }
-
-    private fun cancelAnimationAndApplyDesiredState() {
-        animator.cancel()
-        getHost(desiredLocation)?.let {
-            applyState(it.currentBounds, alpha = 1.0f, immediately = true)
-        }
-    }
-
-    /**
-     * Apply the current state to the view, updating it's bounds and desired state
-     */
-    private fun applyState(
-        bounds: Rect,
-        alpha: Float,
-        immediately: Boolean = false,
-        clipBounds: Rect = EMPTY_RECT
-    ) = traceSection("MediaHierarchyManager#applyState") {
-        currentBounds.set(bounds)
-        currentClipping = clipBounds
-        carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
-        val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
-        val startLocation = if (onlyUseEndState) -1 else previousLocation
-        val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
-        val endLocation = resolveLocationForFading()
-        mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
-        updateHostAttachment()
-        if (currentAttachmentLocation == IN_OVERLAY) {
-            // Setting the clipping on the hierarchy of `mediaFrame` does not work
-            if (!currentClipping.isEmpty) {
-                currentBounds.intersect(currentClipping)
-            }
-            mediaFrame.setLeftTopRightBottom(
-                    currentBounds.left,
-                    currentBounds.top,
-                    currentBounds.right,
-                    currentBounds.bottom)
-        }
-    }
-
-    private fun updateHostAttachment() = traceSection(
-        "MediaHierarchyManager#updateHostAttachment"
-    ) {
-        var newLocation = resolveLocationForFading()
-        var canUseOverlay = !isCurrentlyFading()
-        if (isCrossFadeAnimatorRunning) {
-            if (getHost(newLocation)?.visible == true &&
-                getHost(newLocation)?.hostView?.isShown == false &&
-                newLocation != desiredLocation) {
-                // We're crossfading but the view is already hidden. Let's move to the overlay
-                // instead. This happens when animating to the full shade using a button click.
-                canUseOverlay = true
-            }
-        }
-        val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
-        newLocation = if (inOverlay) IN_OVERLAY else newLocation
-        if (currentAttachmentLocation != newLocation) {
-            currentAttachmentLocation = newLocation
-
-            // Remove the carousel from the old host
-            (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
-
-            // Add it to the new one
-            if (inOverlay) {
-                rootOverlay!!.add(mediaFrame)
-            } else {
-                val targetHost = getHost(newLocation)!!.hostView
-                // When adding back to the host, let's make sure to reset the bounds.
-                // Usually adding the view will trigger a layout that does this automatically,
-                // but we sometimes suppress this.
-                targetHost.addView(mediaFrame)
-                val left = targetHost.paddingLeft
-                val top = targetHost.paddingTop
-                mediaFrame.setLeftTopRightBottom(
-                        left,
-                        top,
-                        left + currentBounds.width(),
-                        top + currentBounds.height())
-
-                if (mediaFrame.childCount > 0) {
-                    val child = mediaFrame.getChildAt(0)
-                    if (mediaFrame.height < child.height) {
-                        Log.wtf(TAG, "mediaFrame height is too small for child: " +
-                            "${mediaFrame.height} vs ${child.height}")
-                    }
-                }
-            }
-            if (isCrossFadeAnimatorRunning) {
-                // When cross-fading with an animation, we only notify the media carousel of the
-                // location change, once the view is reattached to the new place and not immediately
-                // when the desired location changes. This callback will update the measurement
-                // of the carousel, only once we've faded out at the old location and then reattach
-                // to fade it in at the new location.
-                mediaCarouselController.onDesiredLocationChanged(
-                    newLocation,
-                    getHost(newLocation),
-                    animate = false
-                )
-            }
-        }
-    }
-
-    /**
-     * Calculate the location when cross fading between locations. While fading out,
-     * the content should remain in the previous location, while after the switch it should
-     * be at the desired location.
-     */
-    private fun resolveLocationForFading(): Int {
-        if (isCrossFadeAnimatorRunning) {
-            // When animating between two hosts with a fade, let's keep ourselves in the old
-            // location for the first half, and then switch over to the end location
-            if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
-                return crossFadeAnimationEndLocation
-            } else {
-                return crossFadeAnimationStartLocation
-            }
-        }
-        return desiredLocation
-    }
-
-    private fun isTransitionRunning(): Boolean {
-        return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
-                animator.isRunning || animationPending
-    }
-
-    @MediaLocation
-    private fun calculateLocation(): Int {
-        if (blockLocationChanges) {
-            // Keep the current location until we're allowed to again
-            return desiredLocation
-        }
-        val onLockscreen = (!bypassController.bypassEnabled &&
-            (statusbarState == StatusBarState.KEYGUARD))
-        val location = when {
-            dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
-            (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
-            qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
-            !hasActiveMedia -> LOCATION_QS
-            onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
-            onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-            onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
-            else -> LOCATION_QQS
-        }
-        // When we're on lock screen and the player is not active, we should keep it in QS.
-        // Otherwise it will try to animate a transition that doesn't make sense.
-        if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
-            !statusBarStateController.isDozing) {
-            return LOCATION_QS
-        }
-        if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
-            collapsingShadeFromQS) {
-            // When collapsing on the lockscreen, we want to remain in QS
-            return LOCATION_QS
-        }
-        if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN &&
-            !fullyAwake) {
-            // When unlocking from dozing / while waking up, the media shouldn't be transitioning
-            // in an animated way. Let's keep it in the lockscreen until we're fully awake and
-            // reattach it without an animation
-            return LOCATION_LOCKSCREEN
-        }
-        if (skipQqsOnExpansion) {
-            // When doing an immediate expand or collapse, we want to keep it in QS.
-            return LOCATION_QS
-        }
-        return location
-    }
-
-    private fun isSplitShadeExpanding(): Boolean {
-        return inSplitShade && isTransitioningToFullShade
-    }
-
-    /**
-     * Are we currently transforming to the full shade and already in QQS
-     */
-    private fun isTransformingToFullShadeAndInQQS(): Boolean {
-        if (!isTransitioningToFullShade) {
-            return false
-        }
-        if (inSplitShade) {
-            // Split shade doesn't use QQS.
-            return false
-        }
-        return fullShadeTransitionProgress > 0.5f
-    }
-
-    /**
-     * Is the current transformationType fading
-     */
-    private fun isCurrentlyFading(): Boolean {
-        if (isSplitShadeExpanding()) {
-            // Split shade always uses transition instead of fade.
-            return false
-        }
-        if (isTransitioningToFullShade) {
-            return true
-        }
-        return isCrossFadeAnimatorRunning
-    }
-
-    /**
-     * Returns true when the media card could be visible to the user if existed.
-     */
-    private fun isVisibleToUser(): Boolean {
-        return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() ||
-                isHomeScreenShadeVisibleToUser()
-    }
-
-    private fun isLockScreenVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                !keyguardViewController.isBouncerShowing &&
-                statusBarStateController.state == StatusBarState.KEYGUARD &&
-                allowMediaPlayerOnLockScreen &&
-                statusBarStateController.isExpanded &&
-                !qsExpanded
-    }
-
-    private fun isLockScreenShadeVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                !keyguardViewController.isBouncerShowing &&
-                (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
-                        (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
-    }
-
-    private fun isHomeScreenShadeVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                statusBarStateController.state == StatusBarState.SHADE &&
-                statusBarStateController.isExpanded
-    }
-
-    companion object {
-        /**
-         * Attached in expanded quick settings
-         */
-        const val LOCATION_QS = 0
-
-        /**
-         * Attached in the collapsed QS
-         */
-        const val LOCATION_QQS = 1
-
-        /**
-         * Attached on the lock screen
-         */
-        const val LOCATION_LOCKSCREEN = 2
-
-        /**
-         * Attached on the dream overlay
-         */
-        const val LOCATION_DREAM_OVERLAY = 3
-
-        /**
-         * Attached at the root of the hierarchy in an overlay
-         */
-        const val IN_OVERLAY = -1000
-
-        /**
-         * The default transformation type where the hosts transform into each other using a direct
-         * transition
-         */
-        const val TRANSFORMATION_TYPE_TRANSITION = 0
-
-        /**
-         * A transformation type where content fades from one place to another instead of
-         * transitioning
-         */
-        const val TRANSFORMATION_TYPE_FADE = 1
-    }
-}
-private val EMPTY_RECT = Rect()
-
-@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [
-    MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
-    MediaHierarchyManager.TRANSFORMATION_TYPE_FADE])
-@Retention(AnnotationRetention.SOURCE)
-private annotation class TransformationType
-
-@IntDef(prefix = ["LOCATION_"], value = [
-    MediaHierarchyManager.LOCATION_QS,
-    MediaHierarchyManager.LOCATION_QQS,
-    MediaHierarchyManager.LOCATION_LOCKSCREEN,
-    MediaHierarchyManager.LOCATION_DREAM_OVERLAY])
-@Retention(AnnotationRetention.SOURCE)
-annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
deleted file mode 100644
index 8645922..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ /dev/null
@@ -1,375 +0,0 @@
-package com.android.systemui.media
-
-import android.graphics.Rect
-import android.util.ArraySet
-import android.view.View
-import android.view.View.OnAttachStateChangeListener
-import com.android.systemui.util.animation.DisappearParameters
-import com.android.systemui.util.animation.MeasurementInput
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.animation.UniqueObjectHostView
-import java.util.Objects
-import javax.inject.Inject
-
-class MediaHost constructor(
-    private val state: MediaHostStateHolder,
-    private val mediaHierarchyManager: MediaHierarchyManager,
-    private val mediaDataManager: MediaDataManager,
-    private val mediaHostStatesManager: MediaHostStatesManager
-) : MediaHostState by state {
-    lateinit var hostView: UniqueObjectHostView
-    var location: Int = -1
-        private set
-    private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
-
-    private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
-
-    private var inited: Boolean = false
-
-    /**
-     * Are we listening to media data changes?
-     */
-    private var listeningToMediaData = false
-
-    /**
-     * Get the current bounds on the screen. This makes sure the state is fresh and up to date
-     */
-    val currentBounds: Rect = Rect()
-        get() {
-            hostView.getLocationOnScreen(tmpLocationOnScreen)
-            var left = tmpLocationOnScreen[0] + hostView.paddingLeft
-            var top = tmpLocationOnScreen[1] + hostView.paddingTop
-            var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
-            var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
-            // Handle cases when the width or height is 0 but it has padding. In those cases
-            // the above could return negative widths, which is wrong
-            if (right < left) {
-                left = 0
-                right = 0
-            }
-            if (bottom < top) {
-                bottom = 0
-                top = 0
-            }
-            field.set(left, top, right, bottom)
-            return field
-        }
-
-    /**
-     * Set the clipping that this host should use, based on its parent's bounds.
-     *
-     * Use [Rect.set].
-     */
-    val currentClipping = Rect()
-
-    private val listener = object : MediaDataManager.Listener {
-        override fun onMediaDataLoaded(
-            key: String,
-            oldKey: String?,
-            data: MediaData,
-            immediately: Boolean,
-            receivedSmartspaceCardLatency: Int,
-            isSsReactivated: Boolean
-        ) {
-            if (immediately) {
-                updateViewVisibility()
-            }
-        }
-
-        override fun onSmartspaceMediaDataLoaded(
-            key: String,
-            data: SmartspaceMediaData,
-            shouldPrioritize: Boolean
-        ) {
-            updateViewVisibility()
-        }
-
-        override fun onMediaDataRemoved(key: String) {
-            updateViewVisibility()
-        }
-
-        override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-            if (immediately) {
-                updateViewVisibility()
-            }
-        }
-    }
-
-    fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
-        visibleChangedListeners.add(listener)
-    }
-
-    fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
-        visibleChangedListeners.remove(listener)
-    }
-
-    /**
-     * Initialize this MediaObject and create a host view.
-     * All state should already be set on this host before calling this method in order to avoid
-     * unnecessary state changes which lead to remeasurings later on.
-     *
-     * @param location the location this host name has. Used to identify the host during
-     *                 transitions.
-     */
-    fun init(@MediaLocation location: Int) {
-        if (inited) {
-            return
-        }
-        inited = true
-
-        this.location = location
-        hostView = mediaHierarchyManager.register(this)
-        // Listen by default, as the host might not be attached by our clients, until
-        // they get a visibility change. We still want to stay up to date in that case!
-        setListeningToMediaData(true)
-        hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(v: View?) {
-                setListeningToMediaData(true)
-                updateViewVisibility()
-            }
-
-            override fun onViewDetachedFromWindow(v: View?) {
-                setListeningToMediaData(false)
-            }
-        })
-
-        // Listen to measurement updates and update our state with it
-        hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
-            override fun onMeasure(input: MeasurementInput): MeasurementOutput {
-                // Modify the measurement to exactly match the dimensions
-                if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
-                    input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
-                            View.MeasureSpec.getSize(input.widthMeasureSpec),
-                            View.MeasureSpec.EXACTLY)
-                }
-                // This will trigger a state change that ensures that we now have a state available
-                state.measurementInput = input
-                return mediaHostStatesManager.updateCarouselDimensions(location, state)
-            }
-        }
-
-        // Whenever the state changes, let our state manager know
-        state.changedListener = {
-            mediaHostStatesManager.updateHostState(location, state)
-        }
-
-        updateViewVisibility()
-    }
-
-    private fun setListeningToMediaData(listen: Boolean) {
-        if (listen != listeningToMediaData) {
-            listeningToMediaData = listen
-            if (listen) {
-                mediaDataManager.addListener(listener)
-            } else {
-                mediaDataManager.removeListener(listener)
-            }
-        }
-    }
-
-    /**
-     * Updates this host's state based on the current media data's status, and invokes listeners if
-     * the visibility has changed
-     */
-    fun updateViewVisibility() {
-        state.visible = if (showsOnlyActiveMedia) {
-            mediaDataManager.hasActiveMediaOrRecommendation()
-        } else {
-            mediaDataManager.hasAnyMediaOrRecommendation()
-        }
-        val newVisibility = if (visible) View.VISIBLE else View.GONE
-        if (newVisibility != hostView.visibility) {
-            hostView.visibility = newVisibility
-            visibleChangedListeners.forEach {
-                it.invoke(visible)
-            }
-        }
-    }
-
-    class MediaHostStateHolder @Inject constructor() : MediaHostState {
-        override var measurementInput: MeasurementInput? = null
-            set(value) {
-                if (value?.equals(field) != true) {
-                    field = value
-                    changedListener?.invoke()
-                }
-            }
-
-        override var expansion: Float = 0.0f
-            set(value) {
-                if (!value.equals(field)) {
-                    field = value
-                    changedListener?.invoke()
-                }
-            }
-
-        override var squishFraction: Float = 1.0f
-            set(value) {
-                if (!value.equals(field)) {
-                    field = value
-                    changedListener?.invoke()
-                }
-            }
-
-        override var showsOnlyActiveMedia: Boolean = false
-            set(value) {
-                if (!value.equals(field)) {
-                    field = value
-                    changedListener?.invoke()
-                }
-            }
-
-        override var visible: Boolean = true
-            set(value) {
-                if (field == value) {
-                    return
-                }
-                field = value
-                changedListener?.invoke()
-            }
-
-        override var falsingProtectionNeeded: Boolean = false
-            set(value) {
-                if (field == value) {
-                    return
-                }
-                field = value
-                changedListener?.invoke()
-            }
-
-        override var disappearParameters: DisappearParameters = DisappearParameters()
-            set(value) {
-                val newHash = value.hashCode()
-                if (lastDisappearHash.equals(newHash)) {
-                    return
-                }
-                field = value
-                lastDisappearHash = newHash
-                changedListener?.invoke()
-            }
-
-        private var lastDisappearHash = disappearParameters.hashCode()
-
-        /**
-         * A listener for all changes. This won't be copied over when invoking [copy]
-         */
-        var changedListener: (() -> Unit)? = null
-
-        /**
-         * Get a copy of this state. This won't copy any listeners it may have set
-         */
-        override fun copy(): MediaHostState {
-            val mediaHostState = MediaHostStateHolder()
-            mediaHostState.expansion = expansion
-            mediaHostState.squishFraction = squishFraction
-            mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
-            mediaHostState.measurementInput = measurementInput?.copy()
-            mediaHostState.visible = visible
-            mediaHostState.disappearParameters = disappearParameters.deepCopy()
-            mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
-            return mediaHostState
-        }
-
-        override fun equals(other: Any?): Boolean {
-            if (!(other is MediaHostState)) {
-                return false
-            }
-            if (!Objects.equals(measurementInput, other.measurementInput)) {
-                return false
-            }
-            if (expansion != other.expansion) {
-                return false
-            }
-            if (squishFraction != other.squishFraction) {
-                return false
-            }
-            if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
-                return false
-            }
-            if (visible != other.visible) {
-                return false
-            }
-            if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
-                return false
-            }
-            if (!disappearParameters.equals(other.disappearParameters)) {
-                return false
-            }
-            return true
-        }
-
-        override fun hashCode(): Int {
-            var result = measurementInput?.hashCode() ?: 0
-            result = 31 * result + expansion.hashCode()
-            result = 31 * result + squishFraction.hashCode()
-            result = 31 * result + falsingProtectionNeeded.hashCode()
-            result = 31 * result + showsOnlyActiveMedia.hashCode()
-            result = 31 * result + if (visible) 1 else 2
-            result = 31 * result + disappearParameters.hashCode()
-            return result
-        }
-    }
-}
-
-/**
- * A description of a media host state that describes the behavior whenever the media carousel
- * is hosted. The HostState notifies the media players of changes to their properties, who
- * in turn will create view states from it.
- * When adding a new property to this, make sure to update the listener and notify them
- * about the changes.
- * In case you need to have a different rendering based on the state, you can add a new
- * constraintState to the [MediaViewController]. Otherwise, similar host states will resolve
- * to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update
- * that key if the underlying view needs to have a different measurement.
- */
-interface MediaHostState {
-
-    companion object {
-        const val EXPANDED: Float = 1.0f
-        const val COLLAPSED: Float = 0.0f
-    }
-
-    /**
-     * The last measurement input that this state was measured with. Infers width and height of
-     * the players.
-     */
-    var measurementInput: MeasurementInput?
-
-    /**
-     * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions),
-     * [EXPANDED] for fully expanded (up to 5 actions).
-     */
-    var expansion: Float
-
-    /**
-     * Fraction of the height animation.
-     */
-    var squishFraction: Float
-
-    /**
-     * Is this host only showing active media or is it showing all of them including resumption?
-     */
-    var showsOnlyActiveMedia: Boolean
-
-    /**
-     * If the view should be VISIBLE or GONE.
-     */
-    val visible: Boolean
-
-    /**
-     * Does this host need any falsing protection?
-     */
-    var falsingProtectionNeeded: Boolean
-
-    /**
-     * The parameters how the view disappears from this location when going to a host that's not
-     * visible. If modified, make sure to set this value again on the host to ensure the values
-     * are propagated
-     */
-    var disappearParameters: DisappearParameters
-
-    /**
-     * Get a copy of this view state, deepcopying all appropriate members
-     */
-    fun copy(): MediaHostState
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
deleted file mode 100644
index aea2934..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-/**
- * A class responsible for managing all media host states of the various host locations and
- * coordinating the heights among different players. This class can be used to get the most up to
- * date state for any location.
- */
-@SysUISingleton
-class MediaHostStatesManager @Inject constructor() {
-
-    private val callbacks: MutableSet<Callback> = mutableSetOf()
-    private val controllers: MutableSet<MediaViewController> = mutableSetOf()
-
-    /**
-     * The overall sizes of the carousel. This is needed to make sure all players in the carousel
-     * have equal size.
-     */
-    val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
-
-    /**
-     * A map with all media states of all locations.
-     */
-    val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
-
-    /**
-     * Notify that a media state for a given location has changed. Should only be called from
-     * Media hosts themselves.
-     */
-    fun updateHostState(
-        @MediaLocation location: Int,
-        hostState: MediaHostState
-    ) = traceSection("MediaHostStatesManager#updateHostState") {
-        val currentState = mediaHostStates.get(location)
-        if (!hostState.equals(currentState)) {
-            val newState = hostState.copy()
-            mediaHostStates.put(location, newState)
-            updateCarouselDimensions(location, hostState)
-            // First update all the controllers to ensure they get the chance to measure
-            for (controller in controllers) {
-                controller.stateCallback.onHostStateChanged(location, newState)
-            }
-
-            // Then update all other callbacks which may depend on the controllers above
-            for (callback in callbacks) {
-                callback.onHostStateChanged(location, newState)
-            }
-        }
-    }
-
-    /**
-     * Get the dimensions of all players combined, which determines the overall height of the
-     * media carousel and the media hosts.
-     */
-    fun updateCarouselDimensions(
-        @MediaLocation location: Int,
-        hostState: MediaHostState
-    ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") {
-        val result = MeasurementOutput(0, 0)
-        for (controller in controllers) {
-            val measurement = controller.getMeasurementsForState(hostState)
-            measurement?.let {
-                if (it.measuredHeight > result.measuredHeight) {
-                    result.measuredHeight = it.measuredHeight
-                }
-                if (it.measuredWidth > result.measuredWidth) {
-                    result.measuredWidth = it.measuredWidth
-                }
-            }
-        }
-        carouselSizes[location] = result
-        return result
-    }
-
-    /**
-     * Add a callback to be called when a MediaState has updated
-     */
-    fun addCallback(callback: Callback) {
-        callbacks.add(callback)
-    }
-
-    /**
-     * Remove a callback that listens to media states
-     */
-    fun removeCallback(callback: Callback) {
-        callbacks.remove(callback)
-    }
-
-    /**
-     * Register a controller that listens to media states and is used to determine the size of
-     * the media carousel
-     */
-    fun addController(controller: MediaViewController) {
-        controllers.add(controller)
-    }
-
-    /**
-     * Notify the manager about the removal of a controller.
-     */
-    fun removeController(controller: MediaViewController) {
-        controllers.remove(controller)
-    }
-
-    interface Callback {
-        /**
-         * Notify the callbacks that a media state for a host has changed, and that the
-         * corresponding view states should be updated and applied
-         */
-        fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 1ac2a07..ceb4845 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -79,8 +79,7 @@
         val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
         intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
 
-        // TODO(b/240939253): update copies
-        val title = getString(R.string.media_projection_dialog_service_title)
+        val title = getString(R.string.media_projection_permission_app_selector_title)
         intent.putExtra(Intent.EXTRA_TITLE, title)
         super.onCreate(bundle)
         controller.init()
@@ -182,8 +181,7 @@
 
     override fun shouldGetOnlyDefaultActivities() = false
 
-    // TODO(b/240924732) flip the flag when the recents selector is ready
-    override fun shouldShowContentPreview() = false
+    override fun shouldShowContentPreview() = true
 
     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
         recentsViewController.createView(parent)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index 397bffc..22f91f3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -18,6 +18,9 @@
 
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN;
+import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP;
+
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
@@ -44,6 +47,8 @@
 import com.android.systemui.R;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
+import com.android.systemui.screenrecord.ScreenShareOption;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.Utils;
 
@@ -102,7 +107,9 @@
 
         CharSequence dialogText = null;
         CharSequence dialogTitle = null;
+        String appName = null;
         if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) {
+            // TODO(b/253438807): handle special app name
             dialogText = getString(R.string.media_projection_dialog_service_text);
             dialogTitle = getString(R.string.media_projection_dialog_service_title);
         } else {
@@ -132,7 +139,7 @@
 
             String unsanitizedAppName = TextUtils.ellipsize(label,
                     paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();
-            String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);
+            appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);
 
             String actionText = getString(R.string.media_projection_dialog_text, appName);
             SpannableString message = new SpannableString(actionText);
@@ -146,27 +153,28 @@
             dialogTitle = getString(R.string.media_projection_dialog_title, appName);
         }
 
-        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this,
-                R.style.Theme_SystemUI_Dialog)
-                .setTitle(dialogTitle)
-                .setIcon(R.drawable.ic_media_projection_permission)
-                .setMessage(dialogText)
-                .setPositiveButton(R.string.media_projection_action_text, this)
-                .setNeutralButton(android.R.string.cancel, this)
-                .setOnCancelListener(this);
-
         if (isPartialScreenSharingEnabled()) {
-            // This is a temporary entry point before we have a new permission dialog
-            // TODO(b/233183090): this activity should be redesigned to have a dropdown selector
-            dialogBuilder.setNegativeButton("App", this);
+            mDialog = new MediaProjectionPermissionDialog(this, () -> {
+                ScreenShareOption selectedOption =
+                        ((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption();
+                grantMediaProjectionPermission(selectedOption.getMode());
+            }, appName);
+        } else {
+            AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this,
+                    R.style.Theme_SystemUI_Dialog)
+                    .setTitle(dialogTitle)
+                    .setIcon(R.drawable.ic_media_projection_permission)
+                    .setMessage(dialogText)
+                    .setPositiveButton(R.string.media_projection_action_text, this)
+                    .setNeutralButton(android.R.string.cancel, this);
+            mDialog = dialogBuilder.create();
         }
 
-        mDialog = dialogBuilder.create();
-
         SystemUIDialog.registerDismissListener(mDialog);
         SystemUIDialog.applyFlags(mDialog);
         SystemUIDialog.setDialogSize(mDialog);
 
+        mDialog.setOnCancelListener(this);
         mDialog.create();
         mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
 
@@ -186,12 +194,17 @@
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
+        if (which == AlertDialog.BUTTON_POSITIVE) {
+            grantMediaProjectionPermission(ENTIRE_SCREEN);
+        }
+    }
+
+    private void grantMediaProjectionPermission(int screenShareMode) {
         try {
-            if (which == AlertDialog.BUTTON_POSITIVE) {
+            if (screenShareMode == ENTIRE_SCREEN) {
                 setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
             }
-
-            if (isPartialScreenSharingEnabled() && which == AlertDialog.BUTTON_NEGATIVE) {
+            if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) {
                 IMediaProjection projection = createProjection(mUid, mPackageName);
                 final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class);
                 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
deleted file mode 100644
index b52565d..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.BroadcastReceiver
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageManager
-import android.media.MediaDescription
-import android.os.UserHandle
-import android.provider.Settings
-import android.service.media.MediaBrowserService
-import android.util.Log
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.Dumpable
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.Utils
-import com.android.systemui.util.time.SystemClock
-import java.io.PrintWriter
-import java.util.concurrent.ConcurrentLinkedQueue
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-private const val TAG = "MediaResumeListener"
-
-private const val MEDIA_PREFERENCES = "media_control_prefs"
-private const val MEDIA_PREFERENCE_KEY = "browser_components_"
-
-@SysUISingleton
-class MediaResumeListener @Inject constructor(
-    private val context: Context,
-    private val broadcastDispatcher: BroadcastDispatcher,
-    @Background private val backgroundExecutor: Executor,
-    private val tunerService: TunerService,
-    private val mediaBrowserFactory: ResumeMediaBrowserFactory,
-    dumpManager: DumpManager,
-    private val systemClock: SystemClock
-) : MediaDataManager.Listener, Dumpable {
-
-    private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
-    private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
-            ConcurrentLinkedQueue()
-
-    private lateinit var mediaDataManager: MediaDataManager
-
-    private var mediaBrowser: ResumeMediaBrowser? = null
-        set(value) {
-            // Always disconnect the old browser -- see b/225403871.
-            field?.disconnect()
-            field = value
-        }
-    private var currentUserId: Int = context.userId
-
-    @VisibleForTesting
-    val userChangeReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            if (Intent.ACTION_USER_UNLOCKED == intent.action) {
-                loadMediaResumptionControls()
-            } else if (Intent.ACTION_USER_SWITCHED == intent.action) {
-                currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
-                loadSavedComponents()
-            }
-        }
-    }
-
-    private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
-        override fun addTrack(
-            desc: MediaDescription,
-            component: ComponentName,
-            browser: ResumeMediaBrowser
-        ) {
-            val token = browser.token
-            val appIntent = browser.appIntent
-            val pm = context.getPackageManager()
-            var appName: CharSequence = component.packageName
-            val resumeAction = getResumeAction(component)
-            try {
-                appName = pm.getApplicationLabel(
-                        pm.getApplicationInfo(component.packageName, 0))
-            } catch (e: PackageManager.NameNotFoundException) {
-                Log.e(TAG, "Error getting package information", e)
-            }
-
-            Log.d(TAG, "Adding resume controls $desc")
-            mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
-                appName.toString(), appIntent, component.packageName)
-        }
-    }
-
-    init {
-        if (useMediaResumption) {
-            dumpManager.registerDumpable(TAG, this)
-            val unlockFilter = IntentFilter()
-            unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
-            unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
-            broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null,
-                UserHandle.ALL)
-            loadSavedComponents()
-        }
-    }
-
-    fun setManager(manager: MediaDataManager) {
-        mediaDataManager = manager
-
-        // Add listener for resumption setting changes
-        tunerService.addTunable(object : TunerService.Tunable {
-            override fun onTuningChanged(key: String?, newValue: String?) {
-                useMediaResumption = Utils.useMediaResumption(context)
-                mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
-            }
-        }, Settings.Secure.MEDIA_CONTROLS_RESUME)
-    }
-
-    private fun loadSavedComponents() {
-        // Make sure list is empty (if we switched users)
-        resumeComponents.clear()
-        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
-        val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
-        val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
-            ?.dropLastWhile { it.isEmpty() }
-        var needsUpdate = false
-        components?.forEach {
-            val info = it.split("/")
-            val packageName = info[0]
-            val className = info[1]
-            val component = ComponentName(packageName, className)
-
-            val lastPlayed = if (info.size == 3) {
-                try {
-                    info[2].toLong()
-                } catch (e: NumberFormatException) {
-                    needsUpdate = true
-                    systemClock.currentTimeMillis()
-                }
-            } else {
-                needsUpdate = true
-                systemClock.currentTimeMillis()
-            }
-            resumeComponents.add(component to lastPlayed)
-        }
-        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
-
-        if (needsUpdate) {
-            // Save any missing times that we had to fill in
-            writeSharedPrefs()
-        }
-    }
-
-    /**
-     * Load controls for resuming media, if available
-     */
-    private fun loadMediaResumptionControls() {
-        if (!useMediaResumption) {
-            return
-        }
-
-        val now = systemClock.currentTimeMillis()
-        resumeComponents.forEach {
-            if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
-                val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
-                browser.findRecentMedia()
-            }
-        }
-    }
-
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        if (useMediaResumption) {
-            // If this had been started from a resume state, disconnect now that it's live
-            if (!key.equals(oldKey)) {
-                mediaBrowser = null
-            }
-            // If we don't have a resume action, check if we haven't already
-            if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
-                // TODO also check for a media button receiver intended for restarting (b/154127084)
-                Log.d(TAG, "Checking for service component for " + data.packageName)
-                val pm = context.packageManager
-                val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
-                val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
-
-                val inf = resumeInfo?.filter {
-                    it.serviceInfo.packageName == data.packageName
-                }
-                if (inf != null && inf.size > 0) {
-                    backgroundExecutor.execute {
-                        tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
-                    }
-                } else {
-                    // No service found
-                    mediaDataManager.setResumeAction(key, null)
-                }
-            }
-        }
-    }
-
-    /**
-     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
-     * component to the list of resumption components
-     */
-    private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
-        Log.d(TAG, "Testing if we can connect to $componentName")
-        // Set null action to prevent additional attempts to connect
-        mediaDataManager.setResumeAction(key, null)
-        mediaBrowser = mediaBrowserFactory.create(
-                object : ResumeMediaBrowser.Callback() {
-                    override fun onConnected() {
-                        Log.d(TAG, "Connected to $componentName")
-                    }
-
-                    override fun onError() {
-                        Log.e(TAG, "Cannot resume with $componentName")
-                        mediaBrowser = null
-                    }
-
-                    override fun addTrack(
-                        desc: MediaDescription,
-                        component: ComponentName,
-                        browser: ResumeMediaBrowser
-                    ) {
-                        // Since this is a test, just save the component for later
-                        Log.d(TAG, "Can get resumable media from $componentName")
-                        mediaDataManager.setResumeAction(key, getResumeAction(componentName))
-                        updateResumptionList(componentName)
-                        mediaBrowser = null
-                    }
-                },
-                componentName)
-        mediaBrowser?.testConnection()
-    }
-
-    /**
-     * Add the component to the saved list of media browser services, checking for duplicates and
-     * removing older components that exceed the maximum limit
-     * @param componentName
-     */
-    private fun updateResumptionList(componentName: ComponentName) {
-        // Remove if exists
-        resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
-        // Insert at front of queue
-        val currentTime = systemClock.currentTimeMillis()
-        resumeComponents.add(componentName to currentTime)
-        // Remove old components if over the limit
-        if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
-            resumeComponents.remove()
-        }
-
-        writeSharedPrefs()
-    }
-
-    private fun writeSharedPrefs() {
-        val sb = StringBuilder()
-        resumeComponents.forEach {
-            sb.append(it.first.flattenToString())
-            sb.append("/")
-            sb.append(it.second)
-            sb.append(ResumeMediaBrowser.DELIMITER)
-        }
-        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
-        prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
-    }
-
-    /**
-     * Get a runnable which will resume media playback
-     */
-    private fun getResumeAction(componentName: ComponentName): Runnable {
-        return Runnable {
-            mediaBrowser = mediaBrowserFactory.create(null, componentName)
-            mediaBrowser?.restart()
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("resumeComponents: $resumeComponents")
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
deleted file mode 100644
index 00273bc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.android.systemui.media
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewGroup
-import android.widget.HorizontalScrollView
-import com.android.systemui.Gefingerpoken
-import com.android.wm.shell.animation.physicsAnimator
-
-/**
- * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful
- * when only measuring children but not the parent, when trying to apply a new scroll position
- */
-class MediaScrollView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0
-)
-    : HorizontalScrollView(context, attrs, defStyleAttr) {
-
-    lateinit var contentContainer: ViewGroup
-        private set
-    var touchListener: Gefingerpoken? = null
-
-    /**
-     * The target value of the translation X animation. Only valid if the physicsAnimator is running
-     */
-    var animationTargetX = 0.0f
-
-    /**
-     * Get the current content translation. This is usually the normal translationX of the content,
-     * but when animating, it might differ
-     */
-    fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) {
-        animationTargetX
-    } else {
-        contentContainer.translationX
-    }
-
-    /**
-     * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
-     * carousel.  The player indices are always relative (start-to-end) and the scrollView.scrollX
-     * is always absolute.  This function is its own inverse.
-     */
-    private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) {
-        contentContainer.width - width - scrollX
-    } else {
-        scrollX
-    }
-
-    /**
-     * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel.
-     */
-    var relativeScrollX: Int
-        get() = transformScrollX(scrollX)
-        set(value) {
-            scrollX = transformScrollX(value)
-        }
-
-    /**
-     * Allow all scrolls to go through, use base implementation
-     */
-    override fun scrollTo(x: Int, y: Int) {
-        if (mScrollX != x || mScrollY != y) {
-            val oldX: Int = mScrollX
-            val oldY: Int = mScrollY
-            mScrollX = x
-            mScrollY = y
-            invalidateParentCaches()
-            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
-            if (!awakenScrollBars()) {
-                postInvalidateOnAnimation()
-            }
-        }
-    }
-
-    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
-        var intercept = false
-        touchListener?.let {
-            intercept = it.onInterceptTouchEvent(ev)
-        }
-        return super.onInterceptTouchEvent(ev) || intercept
-    }
-
-    override fun onTouchEvent(ev: MotionEvent?): Boolean {
-        var touch = false
-        touchListener?.let {
-            touch = it.onTouchEvent(ev)
-        }
-        return super.onTouchEvent(ev) || touch
-    }
-
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-        contentContainer = getChildAt(0) as ViewGroup
-    }
-
-    override fun overScrollBy(
-        deltaX: Int,
-        deltaY: Int,
-        scrollX: Int,
-        scrollY: Int,
-        scrollRangeX: Int,
-        scrollRangeY: Int,
-        maxOverScrollX: Int,
-        maxOverScrollY: Int,
-        isTouchEvent: Boolean
-    ): Boolean {
-        if (getContentTranslation() != 0.0f) {
-            // When we're dismissing we ignore all the scrolling
-            return false
-        }
-        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
-                scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
-    }
-
-    /**
-     * Cancel the current touch event going on.
-     */
-    fun cancelCurrentScroll() {
-        val now = SystemClock.uptimeMillis()
-        val event = MotionEvent.obtain(now, now,
-                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
-        event.source = InputDevice.SOURCE_TOUCHSCREEN
-        super.onTouchEvent(event)
-        event.recycle()
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
deleted file mode 100644
index 3179296..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.ComponentName
-import android.content.Context
-import android.media.session.MediaController
-import android.media.session.MediaController.PlaybackInfo
-import android.media.session.MediaSession
-import android.media.session.MediaSessionManager
-import android.util.Log
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-private const val TAG = "MediaSessionBasedFilter"
-
-/**
- * Filters media loaded events for local media sessions while an app is casting.
- *
- * When an app is casting there can be one remote media sessions and potentially more local media
- * sessions. In this situation, there should only be a media object for the remote session. To
- * achieve this, update events for the local session need to be filtered.
- */
-class MediaSessionBasedFilter @Inject constructor(
-    context: Context,
-    private val sessionManager: MediaSessionManager,
-    @Main private val foregroundExecutor: Executor,
-    @Background private val backgroundExecutor: Executor
-) : MediaDataManager.Listener {
-
-    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
-
-    // Keep track of MediaControllers for a given package to check if an app is casting and it
-    // filter loaded events for local sessions.
-    private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
-            LinkedHashMap()
-
-    // Keep track of the key used for the session tokens. This information is used to know when to
-    // dispatch a removed event so that a media object for a local session will be removed.
-    private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
-
-    // Keep track of which media session tokens have associated notifications.
-    private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
-
-    private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener {
-        override fun onActiveSessionsChanged(controllers: List<MediaController>) {
-            handleControllersChanged(controllers)
-        }
-    }
-
-    init {
-        backgroundExecutor.execute {
-            val name = ComponentName(context, NotificationListenerWithPlugins::class.java)
-            sessionManager.addOnActiveSessionsChangedListener(sessionListener, name)
-            handleControllersChanged(sessionManager.getActiveSessions(name))
-        }
-    }
-
-    /**
-     * Add a listener for filtered [MediaData] changes
-     */
-    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
-
-    /**
-     * Remove a listener that was registered with addListener
-     */
-    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
-
-    /**
-     * May filter loaded events by not passing them along to listeners.
-     *
-     * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that
-     * the app is casting. Sometimes apps will send redundant updates to a local session with
-     * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability
-     * of the media controls.
-     */
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        backgroundExecutor.execute {
-            data.token?.let {
-                tokensWithNotifications.add(it)
-            }
-            val isMigration = oldKey != null && key != oldKey
-            if (isMigration) {
-                keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
-            }
-            if (data.token != null) {
-                keyedTokens.get(key)?.let {
-                    tokens ->
-                    tokens.add(data.token)
-                } ?: run {
-                    val tokens = mutableSetOf(data.token)
-                    keyedTokens.put(key, tokens)
-                }
-            }
-            // Determine if an app is casting by checking if it has a session with playback type
-            // PLAYBACK_TYPE_REMOTE.
-            val remoteControllers = packageControllers.get(data.packageName)?.filter {
-                it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
-            }
-            // Limiting search to only apps with a single remote session.
-            val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
-            if (isMigration || remote == null || remote.sessionToken == data.token ||
-                    !tokensWithNotifications.contains(remote.sessionToken)) {
-                // Not filtering in this case. Passing the event along to listeners.
-                dispatchMediaDataLoaded(key, oldKey, data, immediately)
-            } else {
-                // Filtering this event because the app is casting and the loaded events is for a
-                // local session.
-                Log.d(TAG, "filtering key=$key local=${data.token} remote=${remote?.sessionToken}")
-                // If the local session uses a different notification key, then lets go a step
-                // farther and dismiss the media data so that media controls for the local session
-                // don't hang around while casting.
-                if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
-                    dispatchMediaDataRemoved(key)
-                }
-            }
-        }
-    }
-
-    override fun onSmartspaceMediaDataLoaded(
-        key: String,
-        data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
-    ) {
-        backgroundExecutor.execute {
-            dispatchSmartspaceMediaDataLoaded(key, data)
-        }
-    }
-
-    override fun onMediaDataRemoved(key: String) {
-        // Queue on background thread to ensure ordering of loaded and removed events is maintained.
-        backgroundExecutor.execute {
-            keyedTokens.remove(key)
-            dispatchMediaDataRemoved(key)
-        }
-    }
-
-    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        backgroundExecutor.execute {
-            dispatchSmartspaceMediaDataRemoved(key, immediately)
-        }
-    }
-
-    private fun dispatchMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        info: MediaData,
-        immediately: Boolean
-    ) {
-        foregroundExecutor.execute {
-            listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) }
-        }
-    }
-
-    private fun dispatchMediaDataRemoved(key: String) {
-        foregroundExecutor.execute {
-            listeners.toSet().forEach { it.onMediaDataRemoved(key) }
-        }
-    }
-
-    private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
-        foregroundExecutor.execute {
-            listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) }
-        }
-    }
-
-    private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        foregroundExecutor.execute {
-            listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
-        }
-    }
-
-    private fun handleControllersChanged(controllers: List<MediaController>) {
-        packageControllers.clear()
-        controllers.forEach {
-            controller ->
-            packageControllers.get(controller.packageName)?.let {
-                tokens ->
-                tokens.add(controller)
-            } ?: run {
-                val tokens = mutableListOf(controller)
-                packageControllers.put(controller.packageName, tokens)
-            }
-        }
-        tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
deleted file mode 100644
index 93a29ef..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.session.MediaController
-import android.media.session.PlaybackState
-import android.os.SystemProperties
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@VisibleForTesting
-val PAUSED_MEDIA_TIMEOUT = SystemProperties
-        .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
-
-@VisibleForTesting
-val RESUME_MEDIA_TIMEOUT = SystemProperties
-        .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
-
-/**
- * Controller responsible for keeping track of playback states and expiring inactive streams.
- */
-@SysUISingleton
-class MediaTimeoutListener @Inject constructor(
-    private val mediaControllerFactory: MediaControllerFactory,
-    @Main private val mainExecutor: DelayableExecutor,
-    private val logger: MediaTimeoutLogger,
-    statusBarStateController: SysuiStatusBarStateController,
-    private val systemClock: SystemClock
-) : MediaDataManager.Listener {
-
-    private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
-
-    /**
-     * Callback representing that a media object is now expired:
-     * @param key Media control unique identifier
-     * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
-     *                 or {@code RESUME_MEDIA_TIMEOUT} for resume media
-     */
-    lateinit var timeoutCallback: (String, Boolean) -> Unit
-
-    /**
-     * Callback representing that a media object [PlaybackState] has changed.
-     * @param key Media control unique identifier
-     * @param state The new [PlaybackState]
-     */
-    lateinit var stateCallback: (String, PlaybackState) -> Unit
-
-    init {
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onDozingChanged(isDozing: Boolean) {
-                if (!isDozing) {
-                    // Check whether any timeouts should have expired
-                    mediaListeners.forEach { (key, listener) ->
-                        if (listener.cancellation != null &&
-                                listener.expiration <= systemClock.elapsedRealtime()) {
-                            // We dozed too long - timeout now, and cancel the pending one
-                            listener.expireMediaTimeout(key, "timeout happened while dozing")
-                            listener.doTimeout()
-                        }
-                    }
-                }
-            }
-        })
-    }
-
-    override fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        immediately: Boolean,
-        receivedSmartspaceCardLatency: Int,
-        isSsReactivated: Boolean
-    ) {
-        var reusedListener: PlaybackStateListener? = null
-
-        // First check if we already have a listener
-        mediaListeners.get(key)?.let {
-            if (!it.destroyed) {
-                return
-            }
-
-            // If listener was destroyed previously, we'll need to re-register it
-            logger.logReuseListener(key)
-            reusedListener = it
-        }
-
-        // Having an old key means that we're migrating from/to resumption. We should update
-        // the old listener to make sure that events will be dispatched to the new location.
-        val migrating = oldKey != null && key != oldKey
-        if (migrating) {
-            reusedListener = mediaListeners.remove(oldKey)
-            logger.logMigrateListener(oldKey, key, reusedListener != null)
-        }
-
-        reusedListener?.let {
-            val wasPlaying = it.isPlaying()
-            logger.logUpdateListener(key, wasPlaying)
-            it.mediaData = data
-            it.key = key
-            mediaListeners[key] = it
-            if (wasPlaying != it.isPlaying()) {
-                // If a player becomes active because of a migration, we'll need to broadcast
-                // its state. Doing it now would lead to reentrant callbacks, so let's wait
-                // until we're done.
-                mainExecutor.execute {
-                    if (mediaListeners[key]?.isPlaying() == true) {
-                        logger.logDelayedUpdate(key)
-                        timeoutCallback.invoke(key, false /* timedOut */)
-                    }
-                }
-            }
-            return
-        }
-
-        mediaListeners[key] = PlaybackStateListener(key, data)
-    }
-
-    override fun onMediaDataRemoved(key: String) {
-        mediaListeners.remove(key)?.destroy()
-    }
-
-    fun isTimedOut(key: String): Boolean {
-        return mediaListeners[key]?.timedOut ?: false
-    }
-
-    private inner class PlaybackStateListener(
-        var key: String,
-        data: MediaData
-    ) : MediaController.Callback() {
-
-        var timedOut = false
-        var lastState: PlaybackState? = null
-        var resumption: Boolean? = null
-        var destroyed = false
-        var expiration = Long.MAX_VALUE
-
-        var mediaData: MediaData = data
-            set(value) {
-                destroyed = false
-                mediaController?.unregisterCallback(this)
-                field = value
-                val token = field.token
-                mediaController = if (token != null) {
-                    mediaControllerFactory.create(token)
-                } else {
-                    null
-                }
-                mediaController?.registerCallback(this)
-                // Let's register the cancellations, but not dispatch events now.
-                // Timeouts didn't happen yet and reentrant events are troublesome.
-                processState(mediaController?.playbackState, dispatchEvents = false)
-            }
-
-        // Resume controls may have null token
-        private var mediaController: MediaController? = null
-        var cancellation: Runnable? = null
-            private set
-
-        fun Int.isPlaying() = isPlayingState(this)
-        fun isPlaying() = lastState?.state?.isPlaying() ?: false
-
-        init {
-            mediaData = data
-        }
-
-        fun destroy() {
-            mediaController?.unregisterCallback(this)
-            cancellation?.run()
-            destroyed = true
-        }
-
-        override fun onPlaybackStateChanged(state: PlaybackState?) {
-            processState(state, dispatchEvents = true)
-        }
-
-        override fun onSessionDestroyed() {
-            logger.logSessionDestroyed(key)
-            if (resumption == true) {
-                // Some apps create a session when MBS is queried. We should unregister the
-                // controller since it will no longer be valid, but don't cancel the timeout
-                mediaController?.unregisterCallback(this)
-            } else {
-                // For active controls, if the session is destroyed, clean up everything since we
-                // will need to recreate it if this key is updated later
-                destroy()
-            }
-        }
-
-        private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
-            logger.logPlaybackState(key, state)
-
-            val playingStateSame = (state?.state?.isPlaying() == isPlaying())
-            val actionsSame = (lastState?.actions == state?.actions) &&
-                    areCustomActionListsEqual(lastState?.customActions, state?.customActions)
-            val resumptionChanged = resumption != mediaData.resumption
-
-            lastState = state
-
-            if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) {
-                logger.logStateCallback(key)
-                stateCallback.invoke(key, state)
-            }
-
-            if (playingStateSame && !resumptionChanged) {
-                return
-            }
-            resumption = mediaData.resumption
-
-            val playing = isPlaying()
-            if (!playing) {
-                logger.logScheduleTimeout(key, playing, resumption!!)
-                if (cancellation != null && !resumptionChanged) {
-                    // if the media changed resume state, we'll need to adjust the timeout length
-                    logger.logCancelIgnored(key)
-                    return
-                }
-                expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
-                val timeout = if (mediaData.resumption) {
-                    RESUME_MEDIA_TIMEOUT
-                } else {
-                    PAUSED_MEDIA_TIMEOUT
-                }
-                expiration = systemClock.elapsedRealtime() + timeout
-                cancellation = mainExecutor.executeDelayed({
-                    doTimeout()
-                }, timeout)
-            } else {
-                expireMediaTimeout(key, "playback started - $state, $key")
-                timedOut = false
-                if (dispatchEvents) {
-                    timeoutCallback(key, timedOut)
-                }
-            }
-        }
-
-        fun doTimeout() {
-            cancellation = null
-            logger.logTimeout(key)
-            timedOut = true
-            expiration = Long.MAX_VALUE
-            // this event is async, so it's safe even when `dispatchEvents` is false
-            timeoutCallback(key, timedOut)
-        }
-
-        fun expireMediaTimeout(mediaKey: String, reason: String) {
-            cancellation?.apply {
-                logger.logTimeoutCancelled(mediaKey, reason)
-                run()
-            }
-            expiration = Long.MAX_VALUE
-            cancellation = null
-        }
-    }
-
-    private fun areCustomActionListsEqual(
-        first: List<PlaybackState.CustomAction>?,
-        second: List<PlaybackState.CustomAction>?
-    ): Boolean {
-        // Same object, or both null
-        if (first === second) {
-            return true
-        }
-
-        // Only one null, or different number of actions
-        if ((first == null || second == null) || (first.size != second.size)) {
-            return false
-        }
-
-        // Compare individual actions
-        first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
-            if (!areCustomActionsEqual(firstAction, secondAction)) {
-                return false
-            }
-        }
-        return true
-    }
-
-    private fun areCustomActionsEqual(
-        firstAction: PlaybackState.CustomAction,
-        secondAction: PlaybackState.CustomAction
-    ): Boolean {
-        if (firstAction.action != secondAction.action ||
-                firstAction.name != secondAction.name ||
-                firstAction.icon != secondAction.icon) {
-            return false
-        }
-
-        if ((firstAction.extras == null) != (secondAction.extras == null)) {
-            return false
-        }
-        if (firstAction.extras != null) {
-            firstAction.extras.keySet().forEach { key ->
-                if (firstAction.extras.get(key) != secondAction.extras.get(key)) {
-                    return false
-                }
-            }
-        }
-        return true
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
deleted file mode 100644
index d9c58c0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ /dev/null
@@ -1,162 +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.media
-
-import android.media.session.PlaybackState
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaTimeoutListenerLog
-import javax.inject.Inject
-
-private const val TAG = "MediaTimeout"
-
-/**
- * A buffered log for [MediaTimeoutListener] events
- */
-@SysUISingleton
-class MediaTimeoutLogger @Inject constructor(
-    @MediaTimeoutListenerLog private val buffer: LogBuffer
-) {
-    fun logReuseListener(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "reuse listener: $str1"
-        }
-    )
-
-    fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = oldKey
-            str2 = newKey
-            bool1 = hadListener
-        },
-        {
-            "migrate from $str1 to $str2, had listener? $bool1"
-        }
-    )
-
-    fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = wasPlaying
-        },
-        {
-            "updating $str1, was playing? $bool1"
-        }
-    )
-
-    fun logDelayedUpdate(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "deliver delayed playback state for $str1"
-        }
-    )
-
-    fun logSessionDestroyed(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "session destroyed $str1"
-        }
-    )
-
-    fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log(
-        TAG,
-        LogLevel.VERBOSE,
-        {
-            str1 = key
-            str2 = state?.toString()
-        },
-        {
-            "state update: key=$str1 state=$str2"
-        }
-    )
-
-    fun logStateCallback(key: String) = buffer.log(
-            TAG,
-            LogLevel.VERBOSE,
-            {
-                str1 = key
-            },
-            {
-                "dispatching state update for $key"
-            }
-    )
-
-    fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = playing
-            bool2 = resumption
-        },
-        {
-            "schedule timeout $str1, playing=$bool1 resumption=$bool2"
-        }
-    )
-
-    fun logCancelIgnored(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "cancellation already exists for $str1"
-        }
-    )
-
-    fun logTimeout(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "execute timeout for $str1"
-        }
-    )
-
-    fun logTimeoutCancelled(key: String, reason: String) = buffer.log(
-        TAG,
-        LogLevel.VERBOSE,
-        {
-            str1 = key
-            str2 = reason
-        },
-        {
-            "media timeout cancelled for $str1, reason: $str2"
-        }
-    )
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
deleted file mode 100644
index 0baf01e..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
+++ /dev/null
@@ -1,287 +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.media
-
-import com.android.internal.logging.InstanceId
-import com.android.internal.logging.InstanceIdSequence
-import com.android.internal.logging.UiEvent
-import com.android.internal.logging.UiEventLogger
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import java.lang.IllegalArgumentException
-import javax.inject.Inject
-
-private const val INSTANCE_ID_MAX = 1 shl 20
-
-/**
- * A helper class to log events related to the media controls
- */
-@SysUISingleton
-class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
-
-    private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
-
-    /**
-     * Get a new instance ID for a new media control
-     */
-    fun getNewInstanceId(): InstanceId {
-        return instanceIdSequence.newInstanceId()
-    }
-
-    fun logActiveMediaAdded(
-        uid: Int,
-        packageName: String,
-        instanceId: InstanceId,
-        playbackLocation: Int
-    ) {
-        val event = when (playbackLocation) {
-            MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
-            MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
-            MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
-            else -> throw IllegalArgumentException("Unknown playback location")
-        }
-        logger.logWithInstanceId(event, uid, packageName, instanceId)
-    }
-
-    fun logPlaybackLocationChange(
-        uid: Int,
-        packageName: String,
-        instanceId: InstanceId,
-        playbackLocation: Int
-    ) {
-        val event = when (playbackLocation) {
-            MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
-            MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
-            MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
-            else -> throw IllegalArgumentException("Unknown playback location")
-        }
-        logger.logWithInstanceId(event, uid, packageName, instanceId)
-    }
-
-    fun logResumeMediaAdded(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.RESUME_MEDIA_ADDED, uid, packageName, instanceId)
-    }
-
-    fun logActiveConvertedToResume(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.ACTIVE_TO_RESUME, uid, packageName, instanceId)
-    }
-
-    fun logMediaTimeout(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_TIMEOUT, uid, packageName, instanceId)
-    }
-
-    fun logMediaRemoved(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_REMOVED, uid, packageName, instanceId)
-    }
-
-    fun logMediaCarouselPage(position: Int) {
-        // Since this operation is on the carousel, we don't include package information
-        logger.logWithPosition(MediaUiEvent.CAROUSEL_PAGE, 0, null, position)
-    }
-
-    fun logSwipeDismiss() {
-        // Since this operation is on the carousel, we don't include package information
-        logger.log(MediaUiEvent.DISMISS_SWIPE)
-    }
-
-    fun logLongPressOpen(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.OPEN_LONG_PRESS, uid, packageName, instanceId)
-    }
-
-    fun logLongPressDismiss(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.DISMISS_LONG_PRESS, uid, packageName, instanceId)
-    }
-
-    fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName,
-            instanceId)
-    }
-
-    fun logCarouselSettings() {
-        // Since this operation is on the carousel, we don't include package information
-        logger.log(MediaUiEvent.OPEN_SETTINGS_CAROUSEL)
-    }
-
-    fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
-        val event = when (buttonId) {
-            R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
-            R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
-            R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
-            else -> MediaUiEvent.TAP_ACTION_OTHER
-        }
-
-        logger.logWithInstanceId(event, uid, packageName, instanceId)
-    }
-
-    fun logSeek(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.ACTION_SEEK, uid, packageName, instanceId)
-    }
-
-    fun logOpenOutputSwitcher(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.OPEN_OUTPUT_SWITCHER, uid, packageName, instanceId)
-    }
-
-    fun logTapContentView(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_TAP_CONTENT_VIEW, uid, packageName, instanceId)
-    }
-
-    fun logCarouselPosition(@MediaLocation location: Int) {
-        val event = when (location) {
-            MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
-            MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
-            MediaHierarchyManager.LOCATION_LOCKSCREEN ->
-                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
-            MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
-                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
-            else -> throw IllegalArgumentException("Unknown media carousel location $location")
-        }
-        logger.log(event)
-    }
-
-    fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName,
-            instanceId)
-    }
-
-    fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName,
-            instanceId)
-    }
-
-    fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName,
-            instanceId)
-    }
-
-    fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
-        logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0,
-            packageName, instanceId, position)
-    }
-
-    fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName,
-            instanceId)
-    }
-
-    fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName,
-            instanceId)
-    }
-}
-
-enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
-    @UiEvent(doc = "A new media control was added for media playing locally on the device")
-    LOCAL_MEDIA_ADDED(1029),
-
-    @UiEvent(doc = "A new media control was added for media cast from the device")
-    CAST_MEDIA_ADDED(1030),
-
-    @UiEvent(doc = "A new media control was added for media playing remotely")
-    REMOTE_MEDIA_ADDED(1031),
-
-    @UiEvent(doc = "The media for an existing control was transferred to local playback")
-    TRANSFER_TO_LOCAL(1032),
-
-    @UiEvent(doc = "The media for an existing control was transferred to a cast device")
-    TRANSFER_TO_CAST(1033),
-
-    @UiEvent(doc = "The media for an existing control was transferred to a remote device")
-    TRANSFER_TO_REMOTE(1034),
-
-    @UiEvent(doc = "A new resumable media control was added")
-    RESUME_MEDIA_ADDED(1013),
-
-    @UiEvent(doc = "An existing active media control was converted into resumable media")
-    ACTIVE_TO_RESUME(1014),
-
-    @UiEvent(doc = "A media control timed out")
-    MEDIA_TIMEOUT(1015),
-
-    @UiEvent(doc = "A media control was removed from the carousel")
-    MEDIA_REMOVED(1016),
-
-    @UiEvent(doc = "User swiped to another control within the media carousel")
-    CAROUSEL_PAGE(1017),
-
-    @UiEvent(doc = "The user swiped away the media carousel")
-    DISMISS_SWIPE(1018),
-
-    @UiEvent(doc = "The user long pressed on a media control")
-    OPEN_LONG_PRESS(1019),
-
-    @UiEvent(doc = "The user dismissed a media control via its long press menu")
-    DISMISS_LONG_PRESS(1020),
-
-    @UiEvent(doc = "The user opened media settings from a media control's long press menu")
-    OPEN_SETTINGS_LONG_PRESS(1021),
-
-    @UiEvent(doc = "The user opened media settings from the media carousel")
-    OPEN_SETTINGS_CAROUSEL(1022),
-
-    @UiEvent(doc = "The play/pause button on a media control was tapped")
-    TAP_ACTION_PLAY_PAUSE(1023),
-
-    @UiEvent(doc = "The previous button on a media control was tapped")
-    TAP_ACTION_PREV(1024),
-
-    @UiEvent(doc = "The next button on a media control was tapped")
-    TAP_ACTION_NEXT(1025),
-
-    @UiEvent(doc = "A custom or generic action button on a media control was tapped")
-    TAP_ACTION_OTHER(1026),
-
-    @UiEvent(doc = "The user seeked on a media control using the seekbar")
-    ACTION_SEEK(1027),
-
-    @UiEvent(doc = "The user opened the output switcher from a media control")
-    OPEN_OUTPUT_SWITCHER(1028),
-
-    @UiEvent(doc = "The user tapped on a media control view")
-    MEDIA_TAP_CONTENT_VIEW(1036),
-
-    @UiEvent(doc = "The media carousel moved to QQS")
-    MEDIA_CAROUSEL_LOCATION_QQS(1037),
-
-    @UiEvent(doc = "THe media carousel moved to QS")
-    MEDIA_CAROUSEL_LOCATION_QS(1038),
-
-    @UiEvent(doc = "The media carousel moved to the lockscreen")
-    MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
-
-    @UiEvent(doc = "The media carousel moved to the dream state")
-    MEDIA_CAROUSEL_LOCATION_DREAM(1040),
-
-    @UiEvent(doc = "A media recommendation card was added to the media carousel")
-    MEDIA_RECOMMENDATION_ADDED(1041),
-
-    @UiEvent(doc = "A media recommendation card was removed from the media carousel")
-    MEDIA_RECOMMENDATION_REMOVED(1042),
-
-    @UiEvent(doc = "An existing media control was made active as a recommendation")
-    MEDIA_RECOMMENDATION_ACTIVATED(1043),
-
-    @UiEvent(doc = "User tapped on an item in a media recommendation card")
-    MEDIA_RECOMMENDATION_ITEM_TAP(1044),
-
-    @UiEvent(doc = "User tapped on a media recommendation card")
-    MEDIA_RECOMMENDATION_CARD_TAP(1045),
-
-    @UiEvent(doc = "User opened the broadcast dialog from a media control")
-    MEDIA_OPEN_BROADCAST_DIALOG(1079);
-
-    override fun getId() = metricId
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
deleted file mode 100644
index faa7aae..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ /dev/null
@@ -1,615 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-import android.content.res.Configuration
-import androidx.annotation.VisibleForTesting
-import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.R
-import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.animation.TransitionLayout
-import com.android.systemui.util.animation.TransitionLayoutController
-import com.android.systemui.util.animation.TransitionViewState
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-/**
- * A class responsible for controlling a single instance of a media player handling interactions
- * with the view instance and keeping the media view states up to date.
- */
-class MediaViewController @Inject constructor(
-    private val context: Context,
-    private val configurationController: ConfigurationController,
-    private val mediaHostStatesManager: MediaHostStatesManager,
-    private val logger: MediaViewLogger
-) {
-
-    /**
-     * Indicating that the media view controller is for a notification-based player,
-     * session-based player, or recommendation
-     */
-    enum class TYPE {
-        PLAYER, RECOMMENDATION
-    }
-
-    companion object {
-        @JvmField
-        val GUTS_ANIMATION_DURATION = 500L
-        val controlIds = setOf(
-                R.id.media_progress_bar,
-                R.id.actionNext,
-                R.id.actionPrev,
-                R.id.action0,
-                R.id.action1,
-                R.id.action2,
-                R.id.action3,
-                R.id.action4,
-                R.id.media_scrubbing_elapsed_time,
-                R.id.media_scrubbing_total_time
-        )
-
-        val detailIds = setOf(
-                R.id.header_title,
-                R.id.header_artist,
-                R.id.actionPlayPause,
-        )
-    }
-
-    /**
-     * A listener when the current dimensions of the player change
-     */
-    lateinit var sizeChangedListener: () -> Unit
-    private var firstRefresh: Boolean = true
-    @VisibleForTesting
-    private var transitionLayout: TransitionLayout? = null
-    private val layoutController = TransitionLayoutController()
-    private var animationDelay: Long = 0
-    private var animationDuration: Long = 0
-    private var animateNextStateChange: Boolean = false
-    private val measurement = MeasurementOutput(0, 0)
-    private var type: TYPE = TYPE.PLAYER
-
-    /**
-     * A map containing all viewStates for all locations of this mediaState
-     */
-    private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    var currentEndLocation: Int = -1
-
-    /**
-     * The starting location of the view where it starts for all animations and transitions
-     */
-    @MediaLocation
-    private var currentStartLocation: Int = -1
-
-    /**
-     * The progress of the transition or 1.0 if there is no transition happening
-     */
-    private var currentTransitionProgress: Float = 1.0f
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState = TransitionViewState()
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState2 = TransitionViewState()
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState3 = TransitionViewState()
-
-    /**
-     * A temporary cache key to be used to look up cache entries
-     */
-    private val tmpKey = CacheKey()
-
-    /**
-     * The current width of the player. This might not factor in case the player is animating
-     * to the current state, but represents the end state
-     */
-    var currentWidth: Int = 0
-    /**
-     * The current height of the player. This might not factor in case the player is animating
-     * to the current state, but represents the end state
-     */
-    var currentHeight: Int = 0
-
-    /**
-     * Get the translationX of the layout
-     */
-    var translationX: Float = 0.0f
-        private set
-        get() {
-            return transitionLayout?.translationX ?: 0.0f
-        }
-
-    /**
-     * Get the translationY of the layout
-     */
-    var translationY: Float = 0.0f
-        private set
-        get() {
-            return transitionLayout?.translationY ?: 0.0f
-        }
-
-    /**
-     * A callback for RTL config changes
-     */
-    private val configurationListener = object : ConfigurationController.ConfigurationListener {
-        override fun onConfigChanged(newConfig: Configuration?) {
-            // Because the TransitionLayout is not always attached (and calculates/caches layout
-            // results regardless of attach state), we have to force the layoutDirection of the view
-            // to the correct value for the user's current locale to ensure correct recalculation
-            // when/after calling refreshState()
-            newConfig?.apply {
-                if (transitionLayout?.rawLayoutDirection != layoutDirection) {
-                    transitionLayout?.layoutDirection = layoutDirection
-                    refreshState()
-                }
-            }
-        }
-    }
-
-    /**
-     * A callback for media state changes
-     */
-    val stateCallback = object : MediaHostStatesManager.Callback {
-        override fun onHostStateChanged(
-            @MediaLocation location: Int,
-            mediaHostState: MediaHostState
-        ) {
-            if (location == currentEndLocation || location == currentStartLocation) {
-                setCurrentState(currentStartLocation,
-                        currentEndLocation,
-                        currentTransitionProgress,
-                        applyImmediately = false)
-            }
-        }
-    }
-
-    /**
-     * The expanded constraint set used to render a expanded player. If it is modified, make sure
-     * to call [refreshState]
-     */
-    val collapsedLayout = ConstraintSet()
-
-    /**
-     * The expanded constraint set used to render a collapsed player. If it is modified, make sure
-     * to call [refreshState]
-     */
-    val expandedLayout = ConstraintSet()
-
-    /**
-     * Whether the guts are visible for the associated player.
-     */
-    var isGutsVisible = false
-        private set
-
-    init {
-        mediaHostStatesManager.addController(this)
-        layoutController.sizeChangedListener = { width: Int, height: Int ->
-            currentWidth = width
-            currentHeight = height
-            sizeChangedListener.invoke()
-        }
-        configurationController.addCallback(configurationListener)
-    }
-
-    /**
-     * Notify this controller that the view has been removed and all listeners should be destroyed
-     */
-    fun onDestroy() {
-        mediaHostStatesManager.removeController(this)
-        configurationController.removeCallback(configurationListener)
-    }
-
-    /**
-     * Show guts with an animated transition.
-     */
-    fun openGuts() {
-        if (isGutsVisible) return
-        isGutsVisible = true
-        animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
-        setCurrentState(currentStartLocation,
-                currentEndLocation,
-                currentTransitionProgress,
-                applyImmediately = false)
-    }
-
-    /**
-     * Close the guts for the associated player.
-     *
-     * @param immediate if `false`, it will animate the transition.
-     */
-    @JvmOverloads
-    fun closeGuts(immediate: Boolean = false) {
-        if (!isGutsVisible) return
-        isGutsVisible = false
-        if (!immediate) {
-            animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
-        }
-        setCurrentState(currentStartLocation,
-                currentEndLocation,
-                currentTransitionProgress,
-                applyImmediately = immediate)
-    }
-
-    private fun ensureAllMeasurements() {
-        val mediaStates = mediaHostStatesManager.mediaHostStates
-        for (entry in mediaStates) {
-            obtainViewState(entry.value)
-        }
-    }
-
-    /**
-     * Get the constraintSet for a given expansion
-     */
-    private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
-            if (expansion > 0) expandedLayout else collapsedLayout
-
-    /**
-     * Set the views to be showing/hidden based on the [isGutsVisible] for a given
-     * [TransitionViewState].
-     */
-    private fun setGutsViewState(viewState: TransitionViewState) {
-        val controlsIds = when (type) {
-            TYPE.PLAYER -> MediaViewHolder.controlsIds
-            TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
-        }
-        val gutsIds = GutsViewHolder.ids
-        controlsIds.forEach { id ->
-            viewState.widgetStates.get(id)?.let { state ->
-                // Make sure to use the unmodified state if guts are not visible.
-                state.alpha = if (isGutsVisible) 0f else state.alpha
-                state.gone = if (isGutsVisible) true else state.gone
-            }
-        }
-        gutsIds.forEach { id ->
-            viewState.widgetStates.get(id)?.let { state ->
-                // Make sure to use the unmodified state if guts are visible
-                state.alpha = if (isGutsVisible) state.alpha else 0f
-                state.gone = if (isGutsVisible) state.gone else true
-            }
-        }
-    }
-
-    /**
-     * Apply squishFraction to a copy of viewState such that the cached version is untouched.
-    */
-    internal fun squishViewState(
-        viewState: TransitionViewState,
-        squishFraction: Float
-    ): TransitionViewState {
-        val squishedViewState = viewState.copy()
-        squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
-        controlIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
-            }
-        }
-
-        detailIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
-            }
-        }
-
-        RecommendationViewHolder.mediaContainersIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
-            }
-        }
-
-        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
-            }
-        }
-
-        return squishedViewState
-    }
-
-    /**
-     * Obtain a new viewState for a given media state. This usually returns a cached state, but if
-     * it's not available, it will recreate one by measuring, which may be expensive.
-     */
-     @VisibleForTesting
-     fun obtainViewState(state: MediaHostState?): TransitionViewState? {
-        if (state == null || state.measurementInput == null) {
-            return null
-        }
-        // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
-        var cacheKey = getKey(state, isGutsVisible, tmpKey)
-        val viewState = viewStates[cacheKey]
-        if (viewState != null) {
-            // we already have cached this measurement, let's continue
-            if (state.squishFraction <= 1f) {
-                return squishViewState(viewState, state.squishFraction)
-            }
-            return viewState
-        }
-        // Copy the key since this might call recursively into it and we're using tmpKey
-        cacheKey = cacheKey.copy()
-        val result: TransitionViewState?
-
-        if (transitionLayout == null) {
-            return null
-        }
-        // Let's create a new measurement
-        if (state.expansion == 0.0f || state.expansion == 1.0f) {
-            result = transitionLayout!!.calculateViewState(
-                    state.measurementInput!!,
-                    constraintSetForExpansion(state.expansion),
-                    TransitionViewState())
-
-            setGutsViewState(result)
-            // We don't want to cache interpolated or null states as this could quickly fill up
-            // our cache. We only cache the start and the end states since the interpolation
-            // is cheap
-            viewStates[cacheKey] = result
-        } else {
-            // This is an interpolated state
-            val startState = state.copy().also { it.expansion = 0.0f }
-
-            // Given that we have a measurement and a view, let's get (guaranteed) viewstates
-            // from the start and end state and interpolate them
-            val startViewState = obtainViewState(startState) as TransitionViewState
-            val endState = state.copy().also { it.expansion = 1.0f }
-            val endViewState = obtainViewState(endState) as TransitionViewState
-            result = layoutController.getInterpolatedState(
-                    startViewState,
-                    endViewState,
-                    state.expansion)
-        }
-        if (state.squishFraction <= 1f) {
-            return squishViewState(result, state.squishFraction)
-        }
-        return result
-    }
-
-    private fun getKey(
-        state: MediaHostState,
-        guts: Boolean,
-        result: CacheKey
-    ): CacheKey {
-        result.apply {
-            heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
-            widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
-            expansion = state.expansion
-            gutsVisible = guts
-        }
-        return result
-    }
-
-    /**
-     * Attach a view to this controller. This may perform measurements if it's not available yet
-     * and should therefore be done carefully.
-     */
-    fun attach(
-        transitionLayout: TransitionLayout,
-        type: TYPE
-    ) = traceSection("MediaViewController#attach") {
-        updateMediaViewControllerType(type)
-        logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
-        this.transitionLayout = transitionLayout
-        layoutController.attach(transitionLayout)
-        if (currentEndLocation == -1) {
-            return
-        }
-        // Set the previously set state immediately to the view, now that it's finally attached
-        setCurrentState(
-                startLocation = currentStartLocation,
-                endLocation = currentEndLocation,
-                transitionProgress = currentTransitionProgress,
-                applyImmediately = true)
-    }
-
-    /**
-     * Obtain a measurement for a given location. This makes sure that the state is up to date
-     * and all widgets know their location. Calling this method may create a measurement if we
-     * don't have a cached value available already.
-     */
-    fun getMeasurementsForState(
-        hostState: MediaHostState
-    ): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") {
-        val viewState = obtainViewState(hostState) ?: return null
-        measurement.measuredWidth = viewState.width
-        measurement.measuredHeight = viewState.height
-        return measurement
-    }
-
-    /**
-     * Set a new state for the controlled view which can be an interpolation between multiple
-     * locations.
-     */
-    fun setCurrentState(
-        @MediaLocation startLocation: Int,
-        @MediaLocation endLocation: Int,
-        transitionProgress: Float,
-        applyImmediately: Boolean
-    ) = traceSection("MediaViewController#setCurrentState") {
-        currentEndLocation = endLocation
-        currentStartLocation = startLocation
-        currentTransitionProgress = transitionProgress
-        logger.logMediaLocation("setCurrentState", startLocation, endLocation)
-
-        val shouldAnimate = animateNextStateChange && !applyImmediately
-
-        val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
-        val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
-
-        // Obtain the view state that we'd want to be at the end
-        // The view might not be bound yet or has never been measured and in that case will be
-        // reset once the state is fully available
-        var endViewState = obtainViewState(endHostState) ?: return
-        endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
-        layoutController.setMeasureState(endViewState)
-
-        // If the view isn't bound, we can drop the animation, otherwise we'll execute it
-        animateNextStateChange = false
-        if (transitionLayout == null) {
-            return
-        }
-
-        val result: TransitionViewState
-        var startViewState = obtainViewState(startHostState)
-        startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
-
-        if (!endHostState.visible) {
-            // Let's handle the case where the end is gone first. In this case we take the
-            // start viewState and will make it gone
-            if (startViewState == null || startHostState == null || !startHostState.visible) {
-                // the start isn't a valid state, let's use the endstate directly
-                result = endViewState
-            } else {
-                // Let's get the gone presentation from the start state
-                result = layoutController.getGoneState(startViewState,
-                        startHostState.disappearParameters,
-                        transitionProgress,
-                        tmpState)
-            }
-        } else if (startHostState != null && !startHostState.visible) {
-            // We have a start state and it is gone.
-            // Let's get presentation from the endState
-            result = layoutController.getGoneState(endViewState, endHostState.disappearParameters,
-                    1.0f - transitionProgress,
-                    tmpState)
-        } else if (transitionProgress == 1.0f || startViewState == null) {
-            // We're at the end. Let's use that state
-            result = endViewState
-        } else if (transitionProgress == 0.0f) {
-            // We're at the start. Let's use that state
-            result = startViewState
-        } else {
-            result = layoutController.getInterpolatedState(startViewState, endViewState,
-                    transitionProgress, tmpState)
-        }
-        logger.logMediaSize("setCurrentState", result.width, result.height)
-        layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
-                animationDelay)
-    }
-
-    private fun updateViewStateToCarouselSize(
-        viewState: TransitionViewState?,
-        location: Int,
-        outState: TransitionViewState
-    ): TransitionViewState? {
-        val result = viewState?.copy(outState) ?: return null
-        val overrideSize = mediaHostStatesManager.carouselSizes[location]
-        overrideSize?.let {
-            // To be safe we're using a maximum here. The override size should always be set
-            // properly though.
-            result.height = Math.max(it.measuredHeight, result.height)
-            result.width = Math.max(it.measuredWidth, result.width)
-        }
-        logger.logMediaSize("update to carousel", result.width, result.height)
-        return result
-    }
-
-    private fun updateMediaViewControllerType(type: TYPE) {
-        this.type = type
-
-        // These XML resources contain ConstraintSets that will apply to this player type's layout
-        when (type) {
-            TYPE.PLAYER -> {
-                collapsedLayout.load(context, R.xml.media_session_collapsed)
-                expandedLayout.load(context, R.xml.media_session_expanded)
-            }
-            TYPE.RECOMMENDATION -> {
-                collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
-                expandedLayout.load(context, R.xml.media_recommendation_expanded)
-            }
-        }
-        refreshState()
-    }
-
-    /**
-     * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
-     * In the event of [location] not being visible, [locationWhenHidden] will be used instead.
-     *
-     * @param location Target
-     * @param locationWhenHidden Location that will be used when the target is not
-     * [MediaHost.visible]
-     * @return State require for executing a transition, and also the respective [MediaHost].
-     */
-    private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
-        val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
-        return obtainViewState(mediaHostState)
-    }
-
-    /**
-     * Notify that the location is changing right now and a [setCurrentState] change is imminent.
-     * This updates the width the view will me measured with.
-     */
-    fun onLocationPreChange(@MediaLocation newLocation: Int) {
-        obtainViewStateForLocation(newLocation)?.let {
-            layoutController.setMeasureState(it)
-        }
-    }
-
-    /**
-     * Request that the next state change should be animated with the given parameters.
-     */
-    fun animatePendingStateChange(duration: Long, delay: Long) {
-        animateNextStateChange = true
-        animationDuration = duration
-        animationDelay = delay
-    }
-
-    /**
-     * Clear all existing measurements and refresh the state to match the view.
-     */
-    fun refreshState() = traceSection("MediaViewController#refreshState") {
-        // Let's clear all of our measurements and recreate them!
-        viewStates.clear()
-        if (firstRefresh) {
-            // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
-            // We'll just load these on demand.
-            ensureAllMeasurements()
-            firstRefresh = false
-        }
-        setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
-                applyImmediately = true)
-    }
-}
-
-/**
- * An internal key for the cache of mediaViewStates. This is a subset of the full host state.
- */
-private data class CacheKey(
-    var widthMeasureSpec: Int = -1,
-    var heightMeasureSpec: Int = -1,
-    var expansion: Float = 0.0f,
-    var gutsVisible: Boolean = false
-)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
deleted file mode 100644
index fc9515c..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
+++ /dev/null
@@ -1,168 +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.
- */
-
-package com.android.systemui.media
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.SeekBar
-import android.widget.TextView
-import androidx.constraintlayout.widget.Barrier
-import com.android.systemui.R
-import com.android.systemui.util.animation.TransitionLayout
-
-private const val TAG = "MediaViewHolder"
-
-/**
- * Holder class for media player view
- */
-class MediaViewHolder constructor(itemView: View) {
-    val player = itemView as TransitionLayout
-
-    // Player information
-    val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
-    val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
-    val titleText = itemView.requireViewById<TextView>(R.id.header_title)
-    val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
-
-    // Output switcher
-    val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
-    val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
-    val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
-    val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
-
-    // Seekbar views
-    val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
-    // These views are only shown while the user is actively scrubbing
-    val scrubbingElapsedTimeView: TextView =
-        itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
-    val scrubbingTotalTimeView: TextView =
-        itemView.requireViewById(R.id.media_scrubbing_total_time)
-
-    val gutsViewHolder = GutsViewHolder(itemView)
-
-    // Action Buttons
-    val actionPlayPause = itemView.requireViewById<ImageButton>(R.id.actionPlayPause)
-    val actionNext = itemView.requireViewById<ImageButton>(R.id.actionNext)
-    val actionPrev = itemView.requireViewById<ImageButton>(R.id.actionPrev)
-    val action0 = itemView.requireViewById<ImageButton>(R.id.action0)
-    val action1 = itemView.requireViewById<ImageButton>(R.id.action1)
-    val action2 = itemView.requireViewById<ImageButton>(R.id.action2)
-    val action3 = itemView.requireViewById<ImageButton>(R.id.action3)
-    val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
-
-    val actionsTopBarrier = itemView.requireViewById<Barrier>(R.id.media_action_barrier_top)
-
-    fun getAction(id: Int): ImageButton {
-        return when (id) {
-            R.id.actionPlayPause -> actionPlayPause
-            R.id.actionNext -> actionNext
-            R.id.actionPrev -> actionPrev
-            R.id.action0 -> action0
-            R.id.action1 -> action1
-            R.id.action2 -> action2
-            R.id.action3 -> action3
-            R.id.action4 -> action4
-            else -> {
-                throw IllegalArgumentException()
-            }
-        }
-    }
-
-    fun getTransparentActionButtons(): List<ImageButton> {
-        return listOf(
-                actionNext,
-                actionPrev,
-                action0,
-                action1,
-                action2,
-                action3,
-                action4
-        )
-    }
-
-    fun marquee(start: Boolean, delay: Long) {
-        gutsViewHolder.marquee(start, delay, TAG)
-    }
-
-    companion object {
-        /**
-         * Creates a MediaViewHolder.
-         *
-         * @param inflater LayoutInflater to use to inflate the layout.
-         * @param parent Parent of inflated view.
-         */
-        @JvmStatic fun create(
-            inflater: LayoutInflater,
-            parent: ViewGroup
-        ): MediaViewHolder {
-            val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
-            mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
-            // Because this media view (a TransitionLayout) is used to measure and layout the views
-            // in various states before being attached to its parent, we can't depend on the default
-            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
-            mediaView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            return MediaViewHolder(mediaView).apply {
-                // Media playback is in the direction of tape, not time, so it stays LTR
-                seekBar.layoutDirection = View.LAYOUT_DIRECTION_LTR
-            }
-        }
-
-        val controlsIds = setOf(
-                R.id.icon,
-                R.id.app_name,
-                R.id.header_title,
-                R.id.header_artist,
-                R.id.media_seamless,
-                R.id.media_progress_bar,
-                R.id.actionPlayPause,
-                R.id.actionNext,
-                R.id.actionPrev,
-                R.id.action0,
-                R.id.action1,
-                R.id.action2,
-                R.id.action3,
-                R.id.action4,
-                R.id.icon,
-                R.id.media_scrubbing_elapsed_time,
-                R.id.media_scrubbing_total_time
-        )
-
-        // Buttons used for notification-based actions
-        val genericButtonIds = setOf(
-            R.id.action0,
-            R.id.action1,
-            R.id.action2,
-            R.id.action3,
-            R.id.action4
-        )
-
-        val expandedBottomActionIds = setOf(
-            R.id.actionPrev,
-            R.id.actionNext,
-            R.id.action0,
-            R.id.action1,
-            R.id.action2,
-            R.id.action3,
-            R.id.action4,
-            R.id.media_scrubbing_elapsed_time,
-            R.id.media_scrubbing_total_time
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
deleted file mode 100644
index 73868189..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
+++ /dev/null
@@ -1,63 +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.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaViewLog
-import javax.inject.Inject
-
-private const val TAG = "MediaView"
-
-/**
- * A buffered log for media view events that are too noisy for regular logging
- */
-@SysUISingleton
-class MediaViewLogger @Inject constructor(
-    @MediaViewLog private val buffer: LogBuffer
-) {
-    fun logMediaSize(reason: String, width: Int, height: Int) {
-        buffer.log(
-                TAG,
-                LogLevel.DEBUG,
-                {
-                    str1 = reason
-                    int1 = width
-                    int2 = height
-                },
-                {
-                    "size ($str1): $int1 x $int2"
-                }
-        )
-    }
-
-    fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
-        buffer.log(
-                TAG,
-                LogLevel.DEBUG,
-                {
-                    str1 = reason
-                    int1 = startLocation
-                    int2 = endLocation
-                },
-                {
-                    "location ($str1): $int1 -> $int2"
-                }
-        )
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
deleted file mode 100644
index 48f4a16..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
+++ /dev/null
@@ -1,76 +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.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-
-/**
- * MetadataAnimationHandler controls the current state of the MediaControlPanel's transition motion.
- *
- * It checks for a changed data object (artist & title from MediaControlPanel) and runs the
- * animation if necessary. When the motion has fully transitioned the elements out, it runs the
- * update callback to modify the view data, before the enter animation runs.
- */
-internal open class MetadataAnimationHandler(
-    private val exitAnimator: Animator,
-    private val enterAnimator: Animator
-) : AnimatorListenerAdapter() {
-
-    private var postExitUpdate: (() -> Unit)? = null
-    private var postEnterUpdate: (() -> Unit)? = null
-    private var targetData: Any? = null
-
-    val isRunning: Boolean
-        get() = enterAnimator.isRunning || exitAnimator.isRunning
-
-    fun setNext(targetData: Any, postExit: () -> Unit, postEnter: () -> Unit): Boolean {
-        if (targetData != this.targetData) {
-            this.targetData = targetData
-            postExitUpdate = postExit
-            postEnterUpdate = postEnter
-            if (!isRunning) {
-                exitAnimator.start()
-            }
-            return true
-        }
-        return false
-    }
-
-    override fun onAnimationEnd(anim: Animator) {
-        if (anim === exitAnimator) {
-            postExitUpdate?.let { it() }
-            postExitUpdate = null
-            enterAnimator.start()
-        }
-
-        if (anim === enterAnimator) {
-            // Another new update appeared while entering
-            if (postExitUpdate != null) {
-                exitAnimator.start()
-            } else {
-                postEnterUpdate?.let { it() }
-                postEnterUpdate = null
-            }
-        }
-    }
-
-    init {
-        exitAnimator.addListener(this)
-        enterAnimator.addListener(this)
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
deleted file mode 100644
index 8ae75fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.util.animation.TransitionLayout
-
-private const val TAG = "RecommendationViewHolder"
-
-/** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View) {
-
-    val recommendations = itemView as TransitionLayout
-
-    // Recommendation screen
-    val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
-    val mediaCoverItems = listOf<ImageView>(
-        itemView.requireViewById(R.id.media_cover1),
-        itemView.requireViewById(R.id.media_cover2),
-        itemView.requireViewById(R.id.media_cover3)
-    )
-    val mediaCoverContainers = listOf<ViewGroup>(
-        itemView.requireViewById(R.id.media_cover1_container),
-        itemView.requireViewById(R.id.media_cover2_container),
-        itemView.requireViewById(R.id.media_cover3_container)
-    )
-    val mediaTitles: List<TextView> = listOf(
-        itemView.requireViewById(R.id.media_title1),
-        itemView.requireViewById(R.id.media_title2),
-        itemView.requireViewById(R.id.media_title3)
-    )
-    val mediaSubtitles: List<TextView> = listOf(
-        itemView.requireViewById(R.id.media_subtitle1),
-        itemView.requireViewById(R.id.media_subtitle2),
-        itemView.requireViewById(R.id.media_subtitle3)
-    )
-
-    val gutsViewHolder = GutsViewHolder(itemView)
-
-    init {
-        (recommendations.background as IlluminationDrawable).let { background ->
-            mediaCoverContainers.forEach { background.registerLightSource(it) }
-            background.registerLightSource(gutsViewHolder.cancel)
-            background.registerLightSource(gutsViewHolder.dismiss)
-            background.registerLightSource(gutsViewHolder.settings)
-        }
-    }
-
-    fun marquee(start: Boolean, delay: Long) {
-        gutsViewHolder.marquee(start, delay, TAG)
-    }
-
-    companion object {
-        /**
-         * Creates a RecommendationViewHolder.
-         *
-         * @param inflater LayoutInflater to use to inflate the layout.
-         * @param parent Parent of inflated view.
-         */
-        @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup):
-            RecommendationViewHolder {
-            val itemView =
-                inflater.inflate(
-                    R.layout.media_smartspace_recommendations,
-                    parent,
-                    false /* attachToRoot */)
-            // Because this media view (a TransitionLayout) is used to measure and layout the views
-            // in various states before being attached to its parent, we can't depend on the default
-            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
-            itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            return RecommendationViewHolder(itemView)
-        }
-
-        // Res Ids for the control components on the recommendation view.
-        val controlsIds = setOf(
-            R.id.recommendation_card_icon,
-            R.id.media_cover1,
-            R.id.media_cover2,
-            R.id.media_cover3,
-            R.id.media_cover1_container,
-            R.id.media_cover2_container,
-            R.id.media_cover3_container,
-            R.id.media_title1,
-            R.id.media_title2,
-            R.id.media_title3,
-            R.id.media_subtitle1,
-            R.id.media_subtitle2,
-            R.id.media_subtitle3
-        )
-
-        val mediaTitlesAndSubtitlesIds = setOf(
-            R.id.media_title1,
-            R.id.media_title2,
-            R.id.media_title3,
-            R.id.media_subtitle1,
-            R.id.media_subtitle2,
-            R.id.media_subtitle3
-        )
-
-        val mediaContainersIds = setOf(
-            R.id.media_cover1_container,
-            R.id.media_cover2_container,
-            R.id.media_cover3_container
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
deleted file mode 100644
index 40a5653..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import android.annotation.Nullable;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.media.MediaDescription;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.os.Bundle;
-import android.service.media.MediaBrowserService;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.List;
-
-/**
- * Media browser for managing resumption in media controls
- */
-public class ResumeMediaBrowser {
-
-    /** Maximum number of controls to show on boot */
-    public static final int MAX_RESUMPTION_CONTROLS = 5;
-
-    /** Delimiter for saved component names */
-    public static final String DELIMITER = ":";
-
-    private static final String TAG = "ResumeMediaBrowser";
-    private final Context mContext;
-    @Nullable private final Callback mCallback;
-    private final MediaBrowserFactory mBrowserFactory;
-    private final ResumeMediaBrowserLogger mLogger;
-    private final ComponentName mComponentName;
-    private final MediaController.Callback mMediaControllerCallback = new SessionDestroyCallback();
-
-    private MediaBrowser mMediaBrowser;
-    @Nullable private MediaController mMediaController;
-
-    /**
-     * Initialize a new media browser
-     * @param context the context
-     * @param callback used to report media items found
-     * @param componentName Component name of the MediaBrowserService this browser will connect to
-     */
-    public ResumeMediaBrowser(
-            Context context,
-            @Nullable Callback callback,
-            ComponentName componentName,
-            MediaBrowserFactory browserFactory,
-            ResumeMediaBrowserLogger logger) {
-        mContext = context;
-        mCallback = callback;
-        mComponentName = componentName;
-        mBrowserFactory = browserFactory;
-        mLogger = logger;
-    }
-
-    /**
-     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
-     * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
-     * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
-     * called when the initial connection is successful, or an error occurs.
-     * Note that it is possible for the service to connect but for no playable tracks to be found.
-     * ResumeMediaBrowser#disconnect will be called automatically with this function.
-     */
-    public void findRecentMedia() {
-        disconnect();
-        Bundle rootHints = new Bundle();
-        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
-        mMediaBrowser = mBrowserFactory.create(
-                mComponentName,
-                mConnectionCallback,
-                rootHints);
-        updateMediaController();
-        mLogger.logConnection(mComponentName, "findRecentMedia");
-        mMediaBrowser.connect();
-    }
-
-    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
-            new MediaBrowser.SubscriptionCallback() {
-        @Override
-        public void onChildrenLoaded(String parentId,
-                List<MediaBrowser.MediaItem> children) {
-            if (children.size() == 0) {
-                Log.d(TAG, "No children found for " + mComponentName);
-                if (mCallback != null) {
-                    mCallback.onError();
-                }
-            } else {
-                // We ask apps to return a playable item as the first child when sending
-                // a request with EXTRA_RECENT; if they don't, no resume controls
-                MediaBrowser.MediaItem child = children.get(0);
-                MediaDescription desc = child.getDescription();
-                if (child.isPlayable() && mMediaBrowser != null) {
-                    if (mCallback != null) {
-                        mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
-                                ResumeMediaBrowser.this);
-                    }
-                } else {
-                    Log.d(TAG, "Child found but not playable for " + mComponentName);
-                    if (mCallback != null) {
-                        mCallback.onError();
-                    }
-                }
-            }
-            disconnect();
-        }
-
-        @Override
-        public void onError(String parentId) {
-            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
-            if (mCallback != null) {
-                mCallback.onError();
-            }
-            disconnect();
-        }
-
-        @Override
-        public void onError(String parentId, Bundle options) {
-            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
-                    + ", options: " + options);
-            if (mCallback != null) {
-                mCallback.onError();
-            }
-            disconnect();
-        }
-    };
-
-    private final MediaBrowser.ConnectionCallback mConnectionCallback =
-            new MediaBrowser.ConnectionCallback() {
-        /**
-         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
-         * For resumption controls, apps are expected to return a playable media item as the first
-         * child. If there are no children or it isn't playable it will be ignored.
-         */
-        @Override
-        public void onConnected() {
-            Log.d(TAG, "Service connected for " + mComponentName);
-            updateMediaController();
-            if (isBrowserConnected()) {
-                String root = mMediaBrowser.getRoot();
-                if (!TextUtils.isEmpty(root)) {
-                    if (mCallback != null) {
-                        mCallback.onConnected();
-                    }
-                    if (mMediaBrowser != null) {
-                        mMediaBrowser.subscribe(root, mSubscriptionCallback);
-                    }
-                    return;
-                }
-            }
-            if (mCallback != null) {
-                mCallback.onError();
-            }
-            disconnect();
-        }
-
-        /**
-         * Invoked when the client is disconnected from the media browser.
-         */
-        @Override
-        public void onConnectionSuspended() {
-            Log.d(TAG, "Connection suspended for " + mComponentName);
-            if (mCallback != null) {
-                mCallback.onError();
-            }
-            disconnect();
-        }
-
-        /**
-         * Invoked when the connection to the media browser failed.
-         */
-        @Override
-        public void onConnectionFailed() {
-            Log.d(TAG, "Connection failed for " + mComponentName);
-            if (mCallback != null) {
-                mCallback.onError();
-            }
-            disconnect();
-        }
-    };
-
-    /**
-     * Disconnect the media browser. This should be done after callbacks have completed to
-     * disconnect from the media browser service.
-     */
-    protected void disconnect() {
-        if (mMediaBrowser != null) {
-            mLogger.logDisconnect(mComponentName);
-            mMediaBrowser.disconnect();
-        }
-        mMediaBrowser = null;
-        updateMediaController();
-    }
-
-    /**
-     * Connects to the MediaBrowserService and starts playback.
-     * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
-     * depending on whether it was successful.
-     * If the connection is successful, the listener should call ResumeMediaBrowser#disconnect after
-     * getting a media update from the app
-     */
-    public void restart() {
-        disconnect();
-        Bundle rootHints = new Bundle();
-        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
-        mMediaBrowser = mBrowserFactory.create(mComponentName,
-                new MediaBrowser.ConnectionCallback() {
-                    @Override
-                    public void onConnected() {
-                        Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
-                        updateMediaController();
-                        if (!isBrowserConnected()) {
-                            if (mCallback != null) {
-                                mCallback.onError();
-                            }
-                            disconnect();
-                            return;
-                        }
-                        MediaSession.Token token = mMediaBrowser.getSessionToken();
-                        MediaController controller = createMediaController(token);
-                        controller.getTransportControls();
-                        controller.getTransportControls().prepare();
-                        controller.getTransportControls().play();
-                        if (mCallback != null) {
-                            mCallback.onConnected();
-                        }
-                        // listener should disconnect after media player update
-                    }
-
-                    @Override
-                    public void onConnectionFailed() {
-                        if (mCallback != null) {
-                            mCallback.onError();
-                        }
-                        disconnect();
-                    }
-
-                    @Override
-                    public void onConnectionSuspended() {
-                        if (mCallback != null) {
-                            mCallback.onError();
-                        }
-                        disconnect();
-                    }
-                }, rootHints);
-        updateMediaController();
-        mLogger.logConnection(mComponentName, "restart");
-        mMediaBrowser.connect();
-    }
-
-    @VisibleForTesting
-    protected MediaController createMediaController(MediaSession.Token token) {
-        return new MediaController(mContext, token);
-    }
-
-    /**
-     * Get the media session token
-     * @return the token, or null if the MediaBrowser is null or disconnected
-     */
-    public MediaSession.Token getToken() {
-        if (!isBrowserConnected()) {
-            return null;
-        }
-        return mMediaBrowser.getSessionToken();
-    }
-
-    /**
-     * Get an intent to launch the app associated with this browser service
-     * @return
-     */
-    public PendingIntent getAppIntent() {
-        PackageManager pm = mContext.getPackageManager();
-        Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
-        return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
-    }
-
-    /**
-     * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
-     * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
-     * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more
-     * detailed logging if the service has issues. If it cannot connect, or cannot find valid media,
-     * then ResumeMediaBrowser.Callback#onError will be called.
-     * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
-     */
-    public void testConnection() {
-        disconnect();
-        Bundle rootHints = new Bundle();
-        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
-        mMediaBrowser = mBrowserFactory.create(
-                mComponentName,
-                mConnectionCallback,
-                rootHints);
-        updateMediaController();
-        mLogger.logConnection(mComponentName, "testConnection");
-        mMediaBrowser.connect();
-    }
-
-    /** Updates mMediaController based on our current browser values. */
-    private void updateMediaController() {
-        MediaSession.Token controllerToken =
-                mMediaController != null ? mMediaController.getSessionToken() : null;
-        MediaSession.Token currentToken = getToken();
-        boolean areEqual = (controllerToken == null && currentToken == null)
-                || (controllerToken != null && controllerToken.equals(currentToken));
-        if (areEqual) {
-            return;
-        }
-
-        // Whenever the token changes, un-register the callback on the old controller (if we have
-        // one) and create a new controller with the callback attached.
-        if (mMediaController != null) {
-            mMediaController.unregisterCallback(mMediaControllerCallback);
-        }
-        if (currentToken != null) {
-            mMediaController = createMediaController(currentToken);
-            mMediaController.registerCallback(mMediaControllerCallback);
-        } else {
-            mMediaController = null;
-        }
-    }
-
-    private boolean isBrowserConnected() {
-        return mMediaBrowser != null && mMediaBrowser.isConnected();
-    }
-
-    /**
-     * Interface to handle results from ResumeMediaBrowser
-     */
-    public static class Callback {
-        /**
-         * Called when the browser has successfully connected to the service
-         */
-        public void onConnected() {
-        }
-
-        /**
-         * Called when the browser encountered an error connecting to the service
-         */
-        public void onError() {
-        }
-
-        /**
-         * Called when the browser finds a suitable track to add to the media carousel
-         * @param track media info for the item
-         * @param component component of the MediaBrowserService which returned this
-         * @param browser reference to the browser
-         */
-        public void addTrack(MediaDescription track, ComponentName component,
-                ResumeMediaBrowser browser) {
-        }
-    }
-
-    private class SessionDestroyCallback extends MediaController.Callback {
-        @Override
-        public void onSessionDestroyed() {
-            mLogger.logSessionDestroyed(isBrowserConnected(), mComponentName);
-            disconnect();
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
deleted file mode 100644
index 3d1380b..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-
-import javax.inject.Inject;
-
-/**
- * Testable wrapper around {@link ResumeMediaBrowser} constructor
- */
-public class ResumeMediaBrowserFactory {
-    private final Context mContext;
-    private final MediaBrowserFactory mBrowserFactory;
-    private final ResumeMediaBrowserLogger mLogger;
-
-    @Inject
-    public ResumeMediaBrowserFactory(
-            Context context, MediaBrowserFactory browserFactory, ResumeMediaBrowserLogger logger) {
-        mContext = context;
-        mBrowserFactory = browserFactory;
-        mLogger = logger;
-    }
-
-    /**
-     * Creates a new ResumeMediaBrowser.
-     *
-     * @param callback will be called on connection or error, and addTrack when media item found
-     * @param componentName component to browse
-     * @return
-     */
-    public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
-            ComponentName componentName) {
-        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory, mLogger);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
deleted file mode 100644
index 41f7354..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
+++ /dev/null
@@ -1,74 +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.media
-
-import android.content.ComponentName
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaBrowserLog
-import javax.inject.Inject
-
-/** A logger for events in [ResumeMediaBrowser]. */
-@SysUISingleton
-class ResumeMediaBrowserLogger @Inject constructor(
-    @MediaBrowserLog private val buffer: LogBuffer
-) {
-    /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */
-    fun logConnection(componentName: ComponentName, reason: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = componentName.toShortString()
-            str2 = reason
-        },
-        { "Connecting browser for component $str1 due to $str2" }
-    )
-
-    /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */
-    fun logDisconnect(componentName: ComponentName) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = componentName.toShortString()
-        },
-        { "Disconnecting browser for component $str1" }
-    )
-
-    /**
-     * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed]
-     * event.
-     *
-     * @param isBrowserConnected true if there's a currently connected
-     *     [android.media.browse.MediaBrowser] and false otherwise.
-     * @param componentName the component name for the [ResumeMediaBrowser] that triggered this log.
-     */
-    fun logSessionDestroyed(
-        isBrowserConnected: Boolean,
-        componentName: ComponentName
-    ) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            bool1 = isBrowserConnected
-            str1 = componentName.toShortString()
-        },
-        { "Session destroyed. Active browser = $bool1. Browser component = $str1." }
-    )
-}
-
-private const val TAG = "MediaBrowser"
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
deleted file mode 100644
index 121021f..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.ObjectAnimator
-import android.text.format.DateUtils
-import androidx.annotation.UiThread
-import androidx.lifecycle.Observer
-import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-
-/**
- * Observer for changes from SeekBarViewModel.
- *
- * <p>Updates the seek bar views in response to changes to the model.
- */
-open class SeekBarObserver(
-    private val holder: MediaViewHolder
-) : Observer<SeekBarViewModel.Progress> {
-
-    companion object {
-        @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
-        @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
-    }
-
-    val seekBarEnabledMaxHeight = holder.seekBar.context.resources
-        .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height)
-    val seekBarDisabledHeight = holder.seekBar.context.resources
-        .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height)
-    val seekBarEnabledVerticalPadding = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding)
-    val seekBarDisabledVerticalPadding = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding)
-    var seekBarResetAnimator: Animator? = null
-
-    init {
-        val seekBarProgressWavelength = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat()
-        val seekBarProgressAmplitude = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).toFloat()
-        val seekBarProgressPhase = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase).toFloat()
-        val seekBarProgressStrokeWidth = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width).toFloat()
-        val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
-        progressDrawable?.let {
-            it.waveLength = seekBarProgressWavelength
-            it.lineAmplitude = seekBarProgressAmplitude
-            it.phaseSpeed = seekBarProgressPhase
-            it.strokeWidth = seekBarProgressStrokeWidth
-        }
-    }
-
-    /** Updates seek bar views when the data model changes. */
-    @UiThread
-    override fun onChanged(data: SeekBarViewModel.Progress) {
-        val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
-        if (!data.enabled) {
-            if (holder.seekBar.maxHeight != seekBarDisabledHeight) {
-                holder.seekBar.maxHeight = seekBarDisabledHeight
-                setVerticalPadding(seekBarDisabledVerticalPadding)
-            }
-            holder.seekBar.isEnabled = false
-            progressDrawable?.animate = false
-            holder.seekBar.thumb.alpha = 0
-            holder.seekBar.progress = 0
-            holder.seekBar.contentDescription = ""
-            holder.scrubbingElapsedTimeView.text = ""
-            holder.scrubbingTotalTimeView.text = ""
-            return
-        }
-
-        holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0
-        holder.seekBar.isEnabled = data.seekAvailable
-        progressDrawable?.animate = data.playing && !data.scrubbing
-        progressDrawable?.transitionEnabled = !data.seekAvailable
-
-        if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) {
-            holder.seekBar.maxHeight = seekBarEnabledMaxHeight
-            setVerticalPadding(seekBarEnabledVerticalPadding)
-        }
-
-        holder.seekBar.setMax(data.duration)
-        val totalTimeString = DateUtils.formatElapsedTime(
-            data.duration / DateUtils.SECOND_IN_MILLIS)
-        if (data.scrubbing) {
-            holder.scrubbingTotalTimeView.text = totalTimeString
-        }
-
-        data.elapsedTime?.let {
-            if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
-                if (it <= RESET_ANIMATION_THRESHOLD_MS &&
-                        holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) {
-                    // This animation resets for every additional update to zero.
-                    val animator = buildResetAnimator(it)
-                    animator.start()
-                    seekBarResetAnimator = animator
-                } else {
-                    holder.seekBar.progress = it
-                }
-            }
-
-            val elapsedTimeString = DateUtils.formatElapsedTime(
-                it / DateUtils.SECOND_IN_MILLIS)
-            if (data.scrubbing) {
-                holder.scrubbingElapsedTimeView.text = elapsedTimeString
-            }
-
-            holder.seekBar.contentDescription = holder.seekBar.context.getString(
-                R.string.controls_media_seekbar_description,
-                elapsedTimeString,
-                totalTimeString
-            )
-        }
-    }
-
-    @VisibleForTesting
-    open fun buildResetAnimator(targetTime: Int): Animator {
-        val animator = ObjectAnimator.ofInt(holder.seekBar, "progress",
-                holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS)
-        animator.setAutoCancel(true)
-        animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
-        animator.interpolator = Interpolators.EMPHASIZED
-        return animator
-    }
-
-    @UiThread
-    fun setVerticalPadding(padding: Int) {
-        val leftPadding = holder.seekBar.paddingLeft
-        val rightPadding = holder.seekBar.paddingRight
-        val bottomPadding = holder.seekBar.paddingBottom
-        holder.seekBar.setPadding(leftPadding, padding, rightPadding, bottomPadding)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
deleted file mode 100644
index 0f78a1e..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.MediaMetadata
-import android.media.session.MediaController
-import android.media.session.PlaybackState
-import android.os.SystemClock
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewConfiguration
-import android.widget.SeekBar
-import androidx.annotation.AnyThread
-import androidx.annotation.WorkerThread
-import androidx.core.view.GestureDetectorCompat
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.NotificationMediaManager
-import com.android.systemui.util.concurrency.RepeatableExecutor
-import javax.inject.Inject
-
-private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
-private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10
-
-private fun PlaybackState.isInMotion(): Boolean {
-    return this.state == PlaybackState.STATE_PLAYING ||
-            this.state == PlaybackState.STATE_FAST_FORWARDING ||
-            this.state == PlaybackState.STATE_REWINDING
-}
-
-/**
- * Gets the playback position while accounting for the time since the [PlaybackState] was last
- * retrieved.
- *
- * This method closely follows the implementation of
- * [MediaSessionRecord#getStateWithUpdatedPosition].
- */
-private fun PlaybackState.computePosition(duration: Long): Long {
-    var currentPosition = this.position
-    if (this.isInMotion()) {
-        val updateTime = this.getLastPositionUpdateTime()
-        val currentTime = SystemClock.elapsedRealtime()
-        if (updateTime > 0) {
-            var position = (this.playbackSpeed * (currentTime - updateTime)).toLong() +
-                    this.getPosition()
-            if (duration >= 0 && position > duration) {
-                position = duration.toLong()
-            } else if (position < 0) {
-                position = 0
-            }
-            currentPosition = position
-        }
-    }
-    return currentPosition
-}
-
-/** ViewModel for seek bar in QS media player. */
-class SeekBarViewModel @Inject constructor(
-    @Background private val bgExecutor: RepeatableExecutor,
-    private val falsingManager: FalsingManager,
-) {
-    private var _data = Progress(false, false, false, false, null, 0)
-        set(value) {
-            val enabledChanged = value.enabled != field.enabled
-            field = value
-            if (enabledChanged) {
-                enabledChangeListener?.onEnabledChanged(value.enabled)
-            }
-            _progress.postValue(value)
-        }
-    private val _progress = MutableLiveData<Progress>().apply {
-        postValue(_data)
-    }
-    val progress: LiveData<Progress>
-        get() = _progress
-    private var controller: MediaController? = null
-        set(value) {
-            if (field?.sessionToken != value?.sessionToken) {
-                field?.unregisterCallback(callback)
-                value?.registerCallback(callback)
-                field = value
-            }
-        }
-    private var playbackState: PlaybackState? = null
-    private var callback = object : MediaController.Callback() {
-        override fun onPlaybackStateChanged(state: PlaybackState?) {
-            playbackState = state
-            if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) {
-                clearController()
-            } else {
-                checkIfPollingNeeded()
-            }
-        }
-
-        override fun onSessionDestroyed() {
-            clearController()
-        }
-    }
-    private var cancel: Runnable? = null
-
-    /** Indicates if the seek interaction is considered a false guesture. */
-    private var isFalseSeek = false
-
-    /** Listening state (QS open or closed) is used to control polling of progress. */
-    var listening = true
-        set(value) = bgExecutor.execute {
-            if (field != value) {
-                field = value
-                checkIfPollingNeeded()
-            }
-        }
-
-    private var scrubbingChangeListener: ScrubbingChangeListener? = null
-    private var enabledChangeListener: EnabledChangeListener? = null
-
-    /** Set to true when the user is touching the seek bar to change the position. */
-    private var scrubbing = false
-        set(value) {
-            if (field != value) {
-                field = value
-                checkIfPollingNeeded()
-                scrubbingChangeListener?.onScrubbingChanged(value)
-                _data = _data.copy(scrubbing = value)
-            }
-        }
-
-    lateinit var logSeek: () -> Unit
-
-    /**
-     * Event indicating that the user has started interacting with the seek bar.
-     */
-    @AnyThread
-    fun onSeekStarting() = bgExecutor.execute {
-        scrubbing = true
-        isFalseSeek = false
-    }
-
-    /**
-     * Event indicating that the user has moved the seek bar.
-     *
-     * @param position Current location in the track.
-     */
-    @AnyThread
-    fun onSeekProgress(position: Long) = bgExecutor.execute {
-        if (scrubbing) {
-            // The user hasn't yet finished their touch gesture, so only update the data for visual
-            // feedback and don't update [controller] yet.
-            _data = _data.copy(elapsedTime = position.toInt())
-        } else {
-            // The seek progress came from an a11y action and we should immediately update to the
-            // new position. (a11y actions to change the seekbar position don't trigger
-            // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.)
-            onSeek(position)
-        }
-    }
-
-    /**
-     * Event indicating that the seek interaction is a false gesture and it should be ignored.
-     */
-    @AnyThread
-    fun onSeekFalse() = bgExecutor.execute {
-        if (scrubbing) {
-            isFalseSeek = true
-        }
-    }
-
-    /**
-     * Handle request to change the current position in the media track.
-     * @param position Place to seek to in the track.
-     */
-    @AnyThread
-    fun onSeek(position: Long) = bgExecutor.execute {
-        if (isFalseSeek) {
-            scrubbing = false
-            checkPlaybackPosition()
-        } else {
-            logSeek()
-            controller?.transportControls?.seekTo(position)
-            // Invalidate the cached playbackState to avoid the thumb jumping back to the previous
-            // position.
-            playbackState = null
-            scrubbing = false
-        }
-    }
-
-    /**
-     * Updates media information.
-     *
-     * This function makes a binder call, so it must happen on a worker thread.
-     *
-     * @param mediaController controller for media session
-     */
-    @WorkerThread
-    fun updateController(mediaController: MediaController?) {
-        controller = mediaController
-        playbackState = controller?.playbackState
-        val mediaMetadata = controller?.metadata
-        val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
-        val position = playbackState?.position?.toInt()
-        val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
-        val playing = NotificationMediaManager
-                .isPlayingState(playbackState?.state ?: PlaybackState.STATE_NONE)
-        val enabled = if (playbackState == null ||
-                playbackState?.getState() == PlaybackState.STATE_NONE ||
-                (duration <= 0)) false else true
-        _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration)
-        checkIfPollingNeeded()
-    }
-
-    /**
-     * Puts the seek bar into a resumption state.
-     *
-     * This should be called when the media session behind the controller has been destroyed.
-     */
-    @AnyThread
-    fun clearController() = bgExecutor.execute {
-        controller = null
-        playbackState = null
-        cancel?.run()
-        cancel = null
-        _data = _data.copy(enabled = false)
-    }
-
-    /**
-     * Call to clean up any resources.
-     */
-    @AnyThread
-    fun onDestroy() = bgExecutor.execute {
-        controller = null
-        playbackState = null
-        cancel?.run()
-        cancel = null
-        scrubbingChangeListener = null
-        enabledChangeListener = null
-    }
-
-    @WorkerThread
-    private fun checkPlaybackPosition() {
-        val duration = _data.duration ?: -1
-        val currentPosition = playbackState?.computePosition(duration.toLong())?.toInt()
-        if (currentPosition != null && _data.elapsedTime != currentPosition) {
-            _data = _data.copy(elapsedTime = currentPosition)
-        }
-    }
-
-    @WorkerThread
-    private fun checkIfPollingNeeded() {
-        val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false
-        if (needed) {
-            if (cancel == null) {
-                cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L,
-                        POSITION_UPDATE_INTERVAL_MILLIS)
-            }
-        } else {
-            cancel?.run()
-            cancel = null
-        }
-    }
-
-    /** Gets a listener to attach to the seek bar to handle seeking. */
-    val seekBarListener: SeekBar.OnSeekBarChangeListener
-        get() {
-            return SeekBarChangeListener(this, falsingManager)
-        }
-
-    /** Attach touch handlers to the seek bar view. */
-    fun attachTouchHandlers(bar: SeekBar) {
-        bar.setOnSeekBarChangeListener(seekBarListener)
-        bar.setOnTouchListener(SeekBarTouchListener(this, bar))
-    }
-
-    fun setScrubbingChangeListener(listener: ScrubbingChangeListener) {
-        scrubbingChangeListener = listener
-    }
-
-    fun removeScrubbingChangeListener(listener: ScrubbingChangeListener) {
-        if (listener == scrubbingChangeListener) {
-            scrubbingChangeListener = null
-        }
-    }
-
-    fun setEnabledChangeListener(listener: EnabledChangeListener) {
-        enabledChangeListener = listener
-    }
-
-    fun removeEnabledChangeListener(listener: EnabledChangeListener) {
-        if (listener == enabledChangeListener) {
-            enabledChangeListener = null
-        }
-    }
-
-    /** Listener interface to be notified when the user starts or stops scrubbing. */
-    interface ScrubbingChangeListener {
-        fun onScrubbingChanged(scrubbing: Boolean)
-    }
-
-    /** Listener interface to be notified when the seekbar's enabled status changes. */
-    interface EnabledChangeListener {
-        fun onEnabledChanged(enabled: Boolean)
-    }
-
-    private class SeekBarChangeListener(
-        val viewModel: SeekBarViewModel,
-        val falsingManager: FalsingManager,
-    ) : SeekBar.OnSeekBarChangeListener {
-        override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
-            if (fromUser) {
-                viewModel.onSeekProgress(progress.toLong())
-            }
-        }
-
-        override fun onStartTrackingTouch(bar: SeekBar) {
-            viewModel.onSeekStarting()
-        }
-
-        override fun onStopTrackingTouch(bar: SeekBar) {
-            if (falsingManager.isFalseTouch(MEDIA_SEEKBAR)) {
-                viewModel.onSeekFalse()
-            }
-            viewModel.onSeek(bar.progress.toLong())
-        }
-    }
-
-    /**
-     * Responsible for intercepting touch events before they reach the seek bar.
-     *
-     * This reduces the gestures seen by the seek bar so that users don't accidentially seek when
-     * they intend to scroll the carousel.
-     */
-    private class SeekBarTouchListener(
-        private val viewModel: SeekBarViewModel,
-        private val bar: SeekBar,
-    ) : View.OnTouchListener, GestureDetector.OnGestureListener {
-
-        // Gesture detector helps decide which touch events to intercept.
-        private val detector = GestureDetectorCompat(bar.context, this)
-        // Velocity threshold used to decide when a fling is considered a false gesture.
-        private val flingVelocity: Int = ViewConfiguration.get(bar.context).run {
-            getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR
-        }
-        // Indicates if the gesture should go to the seek bar or if it should be intercepted.
-        private var shouldGoToSeekBar = false
-
-        /**
-         * Decide which touch events to intercept before they reach the seek bar.
-         *
-         * Based on the gesture detected, we decide whether we want the event to reach the seek bar.
-         * If we want the seek bar to see the event, then we return false so that the event isn't
-         * handled here and it will be passed along. If, however, we don't want the seek bar to see
-         * the event, then return true so that the event is handled here.
-         *
-         * When the seek bar is contained in the carousel, the carousel still has the ability to
-         * intercept the touch event. So, even though we may handle the event here, the carousel can
-         * still intercept the event. This way, gestures that we consider falses on the seek bar can
-         * still be used by the carousel for paging.
-         *
-         * Returns true for events that we don't want dispatched to the seek bar.
-         */
-        override fun onTouch(view: View, event: MotionEvent): Boolean {
-            if (view != bar) {
-                return false
-            }
-            detector.onTouchEvent(event)
-            return !shouldGoToSeekBar
-        }
-
-        /**
-         * Handle down events that press down on the thumb.
-         *
-         * On the down action, determine a target box around the thumb to know when a scroll
-         * gesture starts by clicking on the thumb. The target box will be used by subsequent
-         * onScroll events.
-         *
-         * Returns true when the down event hits within the target box of the thumb.
-         */
-        override fun onDown(event: MotionEvent): Boolean {
-            val padL = bar.paddingLeft
-            val padR = bar.paddingRight
-            // Compute the X location of the thumb as a function of the seek bar progress.
-            // TODO: account for thumb offset
-            val progress = bar.getProgress()
-            val range = bar.max - bar.min
-            val widthFraction = if (range > 0) {
-                (progress - bar.min).toDouble() / range
-            } else {
-                0.0
-            }
-            val availableWidth = bar.width - padL - padR
-            val thumbX = if (bar.isLayoutRtl()) {
-                padL + availableWidth * (1 - widthFraction)
-            } else {
-                padL + availableWidth * widthFraction
-            }
-            // Set the min, max boundaries of the thumb box.
-            // I'm cheating by using the height of the seek bar as the width of the box.
-            val halfHeight: Int = bar.height / 2
-            val targetBoxMinX = (Math.round(thumbX) - halfHeight).toInt()
-            val targetBoxMaxX = (Math.round(thumbX) + halfHeight).toInt()
-            // If the x position of the down event is within the box, then request that the parent
-            // not intercept the event.
-            val x = Math.round(event.x)
-            shouldGoToSeekBar = x >= targetBoxMinX && x <= targetBoxMaxX
-            if (shouldGoToSeekBar) {
-                bar.parent?.requestDisallowInterceptTouchEvent(true)
-            }
-            return shouldGoToSeekBar
-        }
-
-        /**
-         * Always handle single tap up.
-         *
-         * This enables the user to single tap anywhere on the seek bar to seek to that position.
-         */
-        override fun onSingleTapUp(event: MotionEvent): Boolean {
-            shouldGoToSeekBar = true
-            return true
-        }
-
-        /**
-         * Handle scroll events when the down event is on the thumb.
-         *
-         * Returns true when the down event of the scroll hits within the target box of the thumb.
-         */
-        override fun onScroll(
-            eventStart: MotionEvent,
-            event: MotionEvent,
-            distanceX: Float,
-            distanceY: Float
-        ): Boolean {
-            return shouldGoToSeekBar
-        }
-
-        /**
-         * Handle fling events when the down event is on the thumb.
-         *
-         * Gestures that include a fling are considered a false gesture on the seek bar.
-         */
-        override fun onFling(
-            eventStart: MotionEvent,
-            event: MotionEvent,
-            velocityX: Float,
-            velocityY: Float
-        ): Boolean {
-            if (Math.abs(velocityX) > flingVelocity || Math.abs(velocityY) > flingVelocity) {
-                viewModel.onSeekFalse()
-            }
-            return shouldGoToSeekBar
-        }
-
-        override fun onShowPress(event: MotionEvent) {}
-
-        override fun onLongPress(event: MotionEvent) {}
-    }
-
-    /** State seen by seek bar UI. */
-    data class Progress(
-        val enabled: Boolean,
-        val seekAvailable: Boolean,
-        val playing: Boolean,
-        val scrubbing: Boolean,
-        val elapsedTime: Int?,
-        val duration: Int
-    )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java b/packages/SystemUI/src/com/android/systemui/media/SmallHash.java
deleted file mode 100644
index de7aac6..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java
+++ /dev/null
@@ -1,44 +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.
- */
-
-package com.android.systemui.media;
-
-import java.util.Objects;
-
-/**
- * A simple hash function for use in privacy-sensitive logging.
- */
-public final class SmallHash {
-    // Hashes will be in the range [0, MAX_HASH).
-    public static final int MAX_HASH = (1 << 13);
-
-    /** Return Small hash of the string, if non-null, or 0 otherwise. */
-    public static int hash(String in) {
-        return hash(Objects.hashCode(in));
-    }
-
-    /**
-     * Maps in to the range [0, MAX_HASH), keeping similar values distinct.
-     *
-     * @param in An arbitrary integer.
-     * @return in mod MAX_HASH, signs chosen to stay in the range [0, MAX_HASH).
-     */
-    public static int hash(int in) {
-        return Math.abs(Math.floorMod(in, MAX_HASH));
-    }
-
-    private SmallHash() {}
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
deleted file mode 100644
index c8f17d9..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.media
-
-import android.app.smartspace.SmartspaceAction
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.text.TextUtils
-import android.util.Log
-import com.android.internal.logging.InstanceId
-import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME
-
-/** State of a Smartspace media recommendations view. */
-data class SmartspaceMediaData(
-    /**
-     * Unique id of a Smartspace media target.
-     */
-    val targetId: String,
-    /**
-     * Indicates if the status is active.
-     */
-    val isActive: Boolean,
-    /**
-     * Package name of the media recommendations' provider-app.
-     */
-    val packageName: String,
-    /**
-     * Action to perform when the card is tapped. Also contains the target's extra info.
-     */
-    val cardAction: SmartspaceAction?,
-    /**
-     * List of media recommendations.
-     */
-    val recommendations: List<SmartspaceAction>,
-    /**
-     * Intent for the user's initiated dismissal.
-     */
-    val dismissIntent: Intent?,
-    /**
-     * The timestamp in milliseconds that headphone is connected.
-     */
-    val headphoneConnectionTimeMillis: Long,
-    /**
-     * Instance ID for [MediaUiEventLogger]
-     */
-    val instanceId: InstanceId
-) {
-    /**
-     * Indicates if all the data is valid.
-     *
-     * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
-     *     [NUM_REQUIRED_RECOMMENDATIONS].
-     */
-    fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS
-
-    /**
-     * Returns the list of [recommendations] that have valid data.
-     */
-    fun getValidRecommendations() = recommendations.filter { it.icon != null }
-
-    /** Returns the upstream app name if available. */
-    fun getAppName(context: Context): CharSequence? {
-        val nameFromAction = cardAction?.intent?.extras?.getString(KEY_SMARTSPACE_APP_NAME)
-        if (!TextUtils.isEmpty(nameFromAction)) {
-            return nameFromAction
-        }
-
-        val packageManager = context.packageManager
-        packageManager.getLaunchIntentForPackage(packageName)?.let {
-            val launchActivity = it.resolveActivityInfo(packageManager, 0)
-            return launchActivity.loadLabel(packageManager)
-        }
-
-        Log.w(
-            TAG,
-            "Package $packageName does not have a main launcher activity. " +
-                    "Fallback to full app name")
-        return try {
-            val applicationInfo = packageManager.getApplicationInfo(packageName,  /* flags= */ 0)
-            packageManager.getApplicationLabel(applicationInfo)
-        } catch (e: PackageManager.NameNotFoundException) {
-            null
-        }
-    }
-}
-
-const val NUM_REQUIRED_RECOMMENDATIONS = 3
-private val TAG = SmartspaceMediaData::class.simpleName!!
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
deleted file mode 100644
index 140a1fe..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.android.systemui.media
-
-import android.app.smartspace.SmartspaceTarget
-import android.util.Log
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
-import javax.inject.Inject
-
-private const val TAG = "SsMediaDataProvider"
-
-/** Provides SmartspaceTargets of media types for SystemUI media control. */
-class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin {
-
-    private val smartspaceMediaTargetListeners: MutableList<SmartspaceTargetListener> =
-        mutableListOf()
-    private var smartspaceMediaTargets: List<SmartspaceTarget> = listOf()
-
-    override fun registerListener(smartspaceTargetListener: SmartspaceTargetListener) {
-        smartspaceMediaTargetListeners.add(smartspaceTargetListener)
-    }
-
-    override fun unregisterListener(smartspaceTargetListener: SmartspaceTargetListener?) {
-        smartspaceMediaTargetListeners.remove(smartspaceTargetListener)
-    }
-
-    /** Updates Smartspace data and propagates it to any listeners.  */
-    override fun onTargetsAvailable(targets: List<SmartspaceTarget>) {
-        // Filter out non-media targets.
-        val mediaTargets = mutableListOf<SmartspaceTarget>()
-        for (target in targets) {
-            val smartspaceTarget = target
-            if (smartspaceTarget.featureType == SmartspaceTarget.FEATURE_MEDIA) {
-                mediaTargets.add(smartspaceTarget)
-            }
-        }
-
-        if (!mediaTargets.isEmpty()) {
-            Log.d(TAG, "Forwarding Smartspace media updates $mediaTargets")
-        }
-
-        smartspaceMediaTargets = mediaTargets
-        smartspaceMediaTargetListeners.forEach {
-            it.onSmartspaceTargetsUpdated(smartspaceMediaTargets)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
deleted file mode 100644
index 6bc94cd..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.content.res.ColorStateList
-import android.graphics.Canvas
-import android.graphics.ColorFilter
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.PixelFormat
-import android.graphics.drawable.Drawable
-import android.os.SystemClock
-import android.util.MathUtils.lerp
-import android.util.MathUtils.lerpInv
-import android.util.MathUtils.lerpInvSat
-import androidx.annotation.VisibleForTesting
-import com.android.internal.graphics.ColorUtils
-import com.android.systemui.animation.Interpolators
-import kotlin.math.abs
-import kotlin.math.cos
-
-private const val TAG = "Squiggly"
-
-private const val TWO_PI = (Math.PI * 2f).toFloat()
-@VisibleForTesting
-internal const val DISABLED_ALPHA = 77
-
-class SquigglyProgress : Drawable() {
-
-    private val wavePaint = Paint()
-    private val linePaint = Paint()
-    private val path = Path()
-    private var heightFraction = 0f
-    private var heightAnimator: ValueAnimator? = null
-    private var phaseOffset = 0f
-    private var lastFrameTime = -1L
-
-    /* distance over which amplitude drops to zero, measured in wavelengths */
-    private val transitionPeriods = 1.5f
-    /* wave endpoint as percentage of bar when play position is zero */
-    private val minWaveEndpoint = 0.2f
-    /* wave endpoint as percentage of bar when play position matches wave endpoint */
-    private val matchedWaveEndpoint = 0.6f
-
-    // Horizontal length of the sine wave
-    var waveLength = 0f
-    // Height of each peak of the sine wave
-    var lineAmplitude = 0f
-    // Line speed in px per second
-    var phaseSpeed = 0f
-    // Progress stroke width, both for wave and solid line
-    var strokeWidth = 0f
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            wavePaint.strokeWidth = value
-            linePaint.strokeWidth = value
-        }
-
-    // Enables a transition region where the amplitude
-    // of the wave is reduced linearly across it.
-    var transitionEnabled = true
-        set(value) {
-            field = value
-            invalidateSelf()
-        }
-
-    init {
-        wavePaint.strokeCap = Paint.Cap.ROUND
-        linePaint.strokeCap = Paint.Cap.ROUND
-        linePaint.style = Paint.Style.STROKE
-        wavePaint.style = Paint.Style.STROKE
-        linePaint.alpha = DISABLED_ALPHA
-    }
-
-    var animate: Boolean = false
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            if (field) {
-                lastFrameTime = SystemClock.uptimeMillis()
-            }
-            heightAnimator?.cancel()
-            heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
-                if (animate) {
-                    startDelay = 60
-                    duration = 800
-                    interpolator = Interpolators.EMPHASIZED_DECELERATE
-                } else {
-                    duration = 550
-                    interpolator = Interpolators.STANDARD_DECELERATE
-                }
-                addUpdateListener {
-                    heightFraction = it.animatedValue as Float
-                    invalidateSelf()
-                }
-                addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
-                        heightAnimator = null
-                    }
-                })
-                start()
-            }
-        }
-
-    override fun draw(canvas: Canvas) {
-        if (animate) {
-            invalidateSelf()
-            val now = SystemClock.uptimeMillis()
-            phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed
-            phaseOffset %= waveLength
-            lastFrameTime = now
-        }
-
-        val progress = level / 10_000f
-        val totalWidth = bounds.width().toFloat()
-        val totalProgressPx = totalWidth * progress
-        val waveProgressPx = totalWidth * (
-            if (!transitionEnabled || progress > matchedWaveEndpoint) progress else
-            lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress)))
-
-        // Build Wiggly Path
-        val waveStart = -phaseOffset - waveLength / 2f
-        val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx
-
-        // helper function, computes amplitude for wave segment
-        val computeAmplitude: (Float, Float) -> Float = { x, sign ->
-            if (transitionEnabled) {
-                val length = transitionPeriods * waveLength
-                val coeff = lerpInvSat(
-                    waveProgressPx + length / 2f,
-                    waveProgressPx - length / 2f,
-                    x)
-                sign * heightFraction * lineAmplitude * coeff
-            } else {
-                sign * heightFraction * lineAmplitude
-            }
-        }
-
-        // Reset path object to the start
-        path.rewind()
-        path.moveTo(waveStart, 0f)
-
-        // Build the wave, incrementing by half the wavelength each time
-        var currentX = waveStart
-        var waveSign = 1f
-        var currentAmp = computeAmplitude(currentX, waveSign)
-        val dist = waveLength / 2f
-        while (currentX < waveEnd) {
-            waveSign = -waveSign
-            val nextX = currentX + dist
-            val midX = currentX + dist / 2
-            val nextAmp = computeAmplitude(nextX, waveSign)
-            path.cubicTo(
-                midX, currentAmp,
-                midX, nextAmp,
-                nextX, nextAmp)
-            currentAmp = nextAmp
-            currentX = nextX
-        }
-
-        // translate to the start position of the progress bar for all draw commands
-        val clipTop = lineAmplitude + strokeWidth
-        canvas.save()
-        canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())
-
-        // Draw path up to progress position
-        canvas.save()
-        canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop)
-        canvas.drawPath(path, wavePaint)
-        canvas.restore()
-
-        if (transitionEnabled) {
-            // If there's a smooth transition, we draw the rest of the
-            // path in a different color (using different clip params)
-            canvas.save()
-            canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop)
-            canvas.drawPath(path, linePaint)
-            canvas.restore()
-        } else {
-            // No transition, just draw a flat line to the end of the region.
-            // The discontinuity is hidden by the progress bar thumb shape.
-            canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint)
-        }
-
-        // Draw round line cap at the beginning of the wave
-        val startAmp = cos(abs(waveStart) / waveLength * TWO_PI)
-        canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint)
-
-        canvas.restore()
-    }
-
-    override fun getOpacity(): Int {
-        return PixelFormat.TRANSLUCENT
-    }
-
-    override fun setColorFilter(colorFilter: ColorFilter?) {
-        wavePaint.colorFilter = colorFilter
-        linePaint.colorFilter = colorFilter
-    }
-
-    override fun setAlpha(alpha: Int) {
-        updateColors(wavePaint.color, alpha)
-    }
-
-    override fun getAlpha(): Int {
-        return wavePaint.alpha
-    }
-
-    override fun setTint(tintColor: Int) {
-        updateColors(tintColor, alpha)
-    }
-
-    override fun onLevelChange(level: Int): Boolean {
-        return animate
-    }
-
-    override fun setTintList(tint: ColorStateList?) {
-        if (tint == null) {
-            return
-        }
-        updateColors(tint.defaultColor, alpha)
-    }
-
-    private fun updateColors(tintColor: Int, alpha: Int) {
-        wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
-        linePaint.color = ColorUtils.setAlphaComponent(tintColor,
-                (DISABLED_ALPHA * (alpha / 255f)).toInt())
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
new file mode 100644
index 0000000..5315067
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.media.controls.models
+
+import android.content.res.ColorStateList
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.media.controls.ui.accentPrimaryFromScheme
+import com.android.systemui.media.controls.ui.surfaceFromScheme
+import com.android.systemui.media.controls.ui.textPrimaryFromScheme
+import com.android.systemui.monet.ColorScheme
+
+/**
+ * A view holder for the guts menu of a media player. The guts are shown when the user long-presses
+ * on the media player.
+ *
+ * Both [MediaViewHolder] and [RecommendationViewHolder] use the same guts menu layout, so this
+ * class helps share logic between the two.
+ */
+class GutsViewHolder constructor(itemView: View) {
+    val gutsText: TextView = itemView.requireViewById(R.id.remove_text)
+    val cancel: View = itemView.requireViewById(R.id.cancel)
+    val cancelText: TextView = itemView.requireViewById(R.id.cancel_text)
+    val dismiss: ViewGroup = itemView.requireViewById(R.id.dismiss)
+    val dismissText: TextView = itemView.requireViewById(R.id.dismiss_text)
+    val settings: ImageButton = itemView.requireViewById(R.id.settings)
+
+    private var isDismissible: Boolean = true
+    var colorScheme: ColorScheme? = null
+
+    /** Marquees the main text of the guts menu. */
+    fun marquee(start: Boolean, delay: Long, tag: String) {
+        val gutsTextHandler = gutsText.handler
+        if (gutsTextHandler == null) {
+            Log.d(tag, "marquee while longPressText.getHandler() is null", Exception())
+            return
+        }
+        gutsTextHandler.postDelayed({ gutsText.isSelected = start }, delay)
+    }
+
+    /** Set whether this control can be dismissed, and update appearance to match */
+    fun setDismissible(dismissible: Boolean) {
+        if (isDismissible == dismissible) return
+
+        isDismissible = dismissible
+        colorScheme?.let { setColors(it) }
+    }
+
+    /** Sets the right colors on all the guts views based on the given [ColorScheme]. */
+    fun setColors(scheme: ColorScheme) {
+        colorScheme = scheme
+        setSurfaceColor(surfaceFromScheme(scheme))
+        setTextPrimaryColor(textPrimaryFromScheme(scheme))
+        setAccentPrimaryColor(accentPrimaryFromScheme(scheme))
+    }
+
+    /** Sets the surface color on all guts views that use it. */
+    fun setSurfaceColor(surfaceColor: Int) {
+        dismissText.setTextColor(surfaceColor)
+        if (!isDismissible) {
+            cancelText.setTextColor(surfaceColor)
+        }
+    }
+
+    /** Sets the primary accent color on all guts views that use it. */
+    fun setAccentPrimaryColor(accentPrimary: Int) {
+        val accentColorList = ColorStateList.valueOf(accentPrimary)
+        settings.imageTintList = accentColorList
+        cancelText.backgroundTintList = accentColorList
+        dismissText.backgroundTintList = accentColorList
+    }
+
+    /** Sets the primary text color on all guts views that use it. */
+    fun setTextPrimaryColor(textPrimary: Int) {
+        val textColorList = ColorStateList.valueOf(textPrimary)
+        gutsText.setTextColor(textColorList)
+        if (isDismissible) {
+            cancelText.setTextColor(textColorList)
+        }
+    }
+
+    companion object {
+        val ids = setOf(R.id.remove_text, R.id.cancel, R.id.dismiss, R.id.settings)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
new file mode 100644
index 0000000..f006442
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.app.PendingIntent
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.media.session.MediaSession
+import com.android.internal.logging.InstanceId
+import com.android.systemui.R
+
+/** State of a media view. */
+data class MediaData(
+    val userId: Int,
+    val initialized: Boolean = false,
+    /** App name that will be displayed on the player. */
+    val app: String?,
+    /** App icon shown on player. */
+    val appIcon: Icon?,
+    /** Artist name. */
+    val artist: CharSequence?,
+    /** Song name. */
+    val song: CharSequence?,
+    /** Album artwork. */
+    val artwork: Icon?,
+    /** List of generic action buttons for the media player, based on notification actions */
+    val actions: List<MediaAction>,
+    /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
+    val actionsToShowInCompact: List<Int>,
+    /**
+     * Semantic actions buttons, based on the PlaybackState of the media session. If present, these
+     * actions will be preferred in the UI over [actions]
+     */
+    val semanticActions: MediaButton? = null,
+    /** Package name of the app that's posting the media. */
+    val packageName: String,
+    /** Unique media session identifier. */
+    val token: MediaSession.Token?,
+    /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
+    val clickIntent: PendingIntent?,
+    /** Where the media is playing: phone, headphones, ear buds, remote session. */
+    val device: MediaDeviceData?,
+    /**
+     * When active, a player will be displayed on keyguard and quick-quick settings. This is
+     * unrelated to the stream being playing or not, a player will not be active if timed out, or in
+     * resumption mode.
+     */
+    var active: Boolean,
+    /** Action that should be performed to restart a non active session. */
+    var resumeAction: Runnable?,
+    /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
+    var playbackLocation: Int = PLAYBACK_LOCAL,
+    /**
+     * Indicates that this player is a resumption player (ie. It only shows a play actions which
+     * will start the app and start playing).
+     */
+    var resumption: Boolean = false,
+    /**
+     * Notification key for cancelling a media player after a timeout (when not using resumption.)
+     */
+    val notificationKey: String? = null,
+    var hasCheckedForResume: Boolean = false,
+
+    /** If apps do not report PlaybackState, set as null to imply 'undetermined' */
+    val isPlaying: Boolean? = null,
+
+    /** Set from the notification and used as fallback when PlaybackState cannot be determined */
+    val isClearable: Boolean = true,
+
+    /** Timestamp when this player was last active. */
+    var lastActive: Long = 0L,
+
+    /** Instance ID for logging purposes */
+    val instanceId: InstanceId,
+
+    /** The UID of the app, used for logging */
+    val appUid: Int
+) {
+    companion object {
+        /** Media is playing on the local device */
+        const val PLAYBACK_LOCAL = 0
+        /** Media is cast but originated on the local device */
+        const val PLAYBACK_CAST_LOCAL = 1
+        /** Media is from a remote cast notification */
+        const val PLAYBACK_CAST_REMOTE = 2
+    }
+
+    fun isLocalSession(): Boolean {
+        return playbackLocation == PLAYBACK_LOCAL
+    }
+}
+
+/** Contains [MediaAction] objects which represent specific buttons in the UI */
+data class MediaButton(
+    /** Play/pause button */
+    val playOrPause: MediaAction? = null,
+    /** Next button, or custom action */
+    val nextOrCustom: MediaAction? = null,
+    /** Previous button, or custom action */
+    val prevOrCustom: MediaAction? = null,
+    /** First custom action space */
+    val custom0: MediaAction? = null,
+    /** Second custom action space */
+    val custom1: MediaAction? = null,
+    /** Whether to reserve the empty space when the nextOrCustom is null */
+    val reserveNext: Boolean = false,
+    /** Whether to reserve the empty space when the prevOrCustom is null */
+    val reservePrev: Boolean = false
+) {
+    fun getActionById(id: Int): MediaAction? {
+        return when (id) {
+            R.id.actionPlayPause -> playOrPause
+            R.id.actionNext -> nextOrCustom
+            R.id.actionPrev -> prevOrCustom
+            R.id.action0 -> custom0
+            R.id.action1 -> custom1
+            else -> null
+        }
+    }
+}
+
+/** State of a media action. */
+data class MediaAction(
+    val icon: Drawable?,
+    val action: Runnable?,
+    val contentDescription: CharSequence?,
+    val background: Drawable?,
+
+    // Rebind Id is used to detect identical rebinds and ignore them. It is intended
+    // to prevent continuously looping animations from restarting due to the arrival
+    // of repeated media notifications that are visually identical.
+    val rebindId: Int? = null
+)
+
+/** State of the media device. */
+data class MediaDeviceData
+@JvmOverloads
+constructor(
+    /** Whether or not to enable the chip */
+    val enabled: Boolean,
+
+    /** Device icon to show in the chip */
+    val icon: Drawable?,
+
+    /** Device display name */
+    val name: CharSequence?,
+
+    /** Optional intent to override the default output switcher for this control */
+    val intent: PendingIntent? = null,
+
+    /** Unique id for this device */
+    val id: String? = null,
+
+    /** Whether or not to show the broadcast button */
+    val showBroadcastButton: Boolean
+) {
+    /**
+     * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is
+     * ignored because it can change by reference frequently depending on the device type's
+     * implementation, but this is not usually relevant unless other info has changed
+     */
+    fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {
+        if (other == null) {
+            return false
+        }
+
+        return enabled == other.enabled &&
+            name == other.name &&
+            intent == other.intent &&
+            id == other.id &&
+            showBroadcastButton == other.showBroadcastButton
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
new file mode 100644
index 0000000..a8f39fa9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.constraintlayout.widget.Barrier
+import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.surfaceeffects.ripple.MultiRippleView
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
+import com.android.systemui.util.animation.TransitionLayout
+
+private const val TAG = "MediaViewHolder"
+
+/** Holder class for media player view */
+class MediaViewHolder constructor(itemView: View) {
+    val player = itemView as TransitionLayout
+
+    // Player information
+    val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
+    val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view)
+    val turbulenceNoiseView =
+        itemView.requireViewById<TurbulenceNoiseView>(R.id.turbulence_noise_view)
+    val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
+    val titleText = itemView.requireViewById<TextView>(R.id.header_title)
+    val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
+
+    // Output switcher
+    val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
+    val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
+    val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
+    val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
+
+    // Seekbar views
+    val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
+    // These views are only shown while the user is actively scrubbing
+    val scrubbingElapsedTimeView: TextView =
+        itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
+    val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time)
+
+    val gutsViewHolder = GutsViewHolder(itemView)
+
+    // Action Buttons
+    val actionPlayPause = itemView.requireViewById<ImageButton>(R.id.actionPlayPause)
+    val actionNext = itemView.requireViewById<ImageButton>(R.id.actionNext)
+    val actionPrev = itemView.requireViewById<ImageButton>(R.id.actionPrev)
+    val action0 = itemView.requireViewById<ImageButton>(R.id.action0)
+    val action1 = itemView.requireViewById<ImageButton>(R.id.action1)
+    val action2 = itemView.requireViewById<ImageButton>(R.id.action2)
+    val action3 = itemView.requireViewById<ImageButton>(R.id.action3)
+    val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
+
+    val actionsTopBarrier = itemView.requireViewById<Barrier>(R.id.media_action_barrier_top)
+
+    fun getAction(id: Int): ImageButton {
+        return when (id) {
+            R.id.actionPlayPause -> actionPlayPause
+            R.id.actionNext -> actionNext
+            R.id.actionPrev -> actionPrev
+            R.id.action0 -> action0
+            R.id.action1 -> action1
+            R.id.action2 -> action2
+            R.id.action3 -> action3
+            R.id.action4 -> action4
+            else -> {
+                throw IllegalArgumentException()
+            }
+        }
+    }
+
+    fun getTransparentActionButtons(): List<ImageButton> {
+        return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4)
+    }
+
+    fun marquee(start: Boolean, delay: Long) {
+        gutsViewHolder.marquee(start, delay, TAG)
+    }
+
+    companion object {
+        /**
+         * Creates a MediaViewHolder.
+         *
+         * @param inflater LayoutInflater to use to inflate the layout.
+         * @param parent Parent of inflated view.
+         */
+        @JvmStatic
+        fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder {
+            val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
+            mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+            // Because this media view (a TransitionLayout) is used to measure and layout the views
+            // in various states before being attached to its parent, we can't depend on the default
+            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
+            mediaView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+            return MediaViewHolder(mediaView).apply {
+                // Media playback is in the direction of tape, not time, so it stays LTR
+                seekBar.layoutDirection = View.LAYOUT_DIRECTION_LTR
+            }
+        }
+
+        val controlsIds =
+            setOf(
+                R.id.icon,
+                R.id.app_name,
+                R.id.header_title,
+                R.id.header_artist,
+                R.id.media_seamless,
+                R.id.media_progress_bar,
+                R.id.actionPlayPause,
+                R.id.actionNext,
+                R.id.actionPrev,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.icon,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+            )
+
+        // Buttons used for notification-based actions
+        val genericButtonIds =
+            setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4)
+
+        val expandedBottomActionIds =
+            setOf(
+                R.id.actionPrev,
+                R.id.actionNext,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
new file mode 100644
index 0000000..37d956b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.text.format.DateUtils
+import androidx.annotation.UiThread
+import androidx.lifecycle.Observer
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.media.controls.ui.SquigglyProgress
+
+/**
+ * Observer for changes from SeekBarViewModel.
+ *
+ * <p>Updates the seek bar views in response to changes to the model.
+ */
+open class SeekBarObserver(private val holder: MediaViewHolder) :
+    Observer<SeekBarViewModel.Progress> {
+
+    companion object {
+        @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
+        @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
+    }
+
+    val seekBarEnabledMaxHeight =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_enabled_seekbar_height
+        )
+    val seekBarDisabledHeight =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_disabled_seekbar_height
+        )
+    val seekBarEnabledVerticalPadding =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_session_enabled_seekbar_vertical_padding
+        )
+    val seekBarDisabledVerticalPadding =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_session_disabled_seekbar_vertical_padding
+        )
+    var seekBarResetAnimator: Animator? = null
+
+    init {
+        val seekBarProgressWavelength =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength)
+                .toFloat()
+        val seekBarProgressAmplitude =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude)
+                .toFloat()
+        val seekBarProgressPhase =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase)
+                .toFloat()
+        val seekBarProgressStrokeWidth =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width)
+                .toFloat()
+        val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
+        progressDrawable?.let {
+            it.waveLength = seekBarProgressWavelength
+            it.lineAmplitude = seekBarProgressAmplitude
+            it.phaseSpeed = seekBarProgressPhase
+            it.strokeWidth = seekBarProgressStrokeWidth
+        }
+    }
+
+    /** Updates seek bar views when the data model changes. */
+    @UiThread
+    override fun onChanged(data: SeekBarViewModel.Progress) {
+        val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
+        if (!data.enabled) {
+            if (holder.seekBar.maxHeight != seekBarDisabledHeight) {
+                holder.seekBar.maxHeight = seekBarDisabledHeight
+                setVerticalPadding(seekBarDisabledVerticalPadding)
+            }
+            holder.seekBar.isEnabled = false
+            progressDrawable?.animate = false
+            holder.seekBar.thumb.alpha = 0
+            holder.seekBar.progress = 0
+            holder.seekBar.contentDescription = ""
+            holder.scrubbingElapsedTimeView.text = ""
+            holder.scrubbingTotalTimeView.text = ""
+            return
+        }
+
+        holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0
+        holder.seekBar.isEnabled = data.seekAvailable
+        progressDrawable?.animate = data.playing && !data.scrubbing
+        progressDrawable?.transitionEnabled = !data.seekAvailable
+
+        if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) {
+            holder.seekBar.maxHeight = seekBarEnabledMaxHeight
+            setVerticalPadding(seekBarEnabledVerticalPadding)
+        }
+
+        holder.seekBar.setMax(data.duration)
+        val totalTimeString =
+            DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
+        if (data.scrubbing) {
+            holder.scrubbingTotalTimeView.text = totalTimeString
+        }
+
+        data.elapsedTime?.let {
+            if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
+                if (
+                    it <= RESET_ANIMATION_THRESHOLD_MS &&
+                        holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS
+                ) {
+                    // This animation resets for every additional update to zero.
+                    val animator = buildResetAnimator(it)
+                    animator.start()
+                    seekBarResetAnimator = animator
+                } else {
+                    holder.seekBar.progress = it
+                }
+            }
+
+            val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
+            if (data.scrubbing) {
+                holder.scrubbingElapsedTimeView.text = elapsedTimeString
+            }
+
+            holder.seekBar.contentDescription =
+                holder.seekBar.context.getString(
+                    R.string.controls_media_seekbar_description,
+                    elapsedTimeString,
+                    totalTimeString
+                )
+        }
+    }
+
+    @VisibleForTesting
+    open fun buildResetAnimator(targetTime: Int): Animator {
+        val animator =
+            ObjectAnimator.ofInt(
+                holder.seekBar,
+                "progress",
+                holder.seekBar.progress,
+                targetTime + RESET_ANIMATION_DURATION_MS
+            )
+        animator.setAutoCancel(true)
+        animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
+        animator.interpolator = Interpolators.EMPHASIZED
+        return animator
+    }
+
+    @UiThread
+    fun setVerticalPadding(padding: Int) {
+        val leftPadding = holder.seekBar.paddingLeft
+        val rightPadding = holder.seekBar.paddingRight
+        val bottomPadding = holder.seekBar.paddingBottom
+        holder.seekBar.setPadding(leftPadding, padding, rightPadding, bottomPadding)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
new file mode 100644
index 0000000..bba5f35
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.os.SystemClock
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import android.widget.SeekBar
+import androidx.annotation.AnyThread
+import androidx.annotation.WorkerThread
+import androidx.core.view.GestureDetectorCompat
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.util.concurrency.RepeatableExecutor
+import javax.inject.Inject
+
+private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
+private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10
+
+private fun PlaybackState.isInMotion(): Boolean {
+    return this.state == PlaybackState.STATE_PLAYING ||
+        this.state == PlaybackState.STATE_FAST_FORWARDING ||
+        this.state == PlaybackState.STATE_REWINDING
+}
+
+/**
+ * Gets the playback position while accounting for the time since the [PlaybackState] was last
+ * retrieved.
+ *
+ * This method closely follows the implementation of
+ * [MediaSessionRecord#getStateWithUpdatedPosition].
+ */
+private fun PlaybackState.computePosition(duration: Long): Long {
+    var currentPosition = this.position
+    if (this.isInMotion()) {
+        val updateTime = this.getLastPositionUpdateTime()
+        val currentTime = SystemClock.elapsedRealtime()
+        if (updateTime > 0) {
+            var position =
+                (this.playbackSpeed * (currentTime - updateTime)).toLong() + this.getPosition()
+            if (duration >= 0 && position > duration) {
+                position = duration.toLong()
+            } else if (position < 0) {
+                position = 0
+            }
+            currentPosition = position
+        }
+    }
+    return currentPosition
+}
+
+/** ViewModel for seek bar in QS media player. */
+class SeekBarViewModel
+@Inject
+constructor(
+    @Background private val bgExecutor: RepeatableExecutor,
+    private val falsingManager: FalsingManager,
+) {
+    private var _data = Progress(false, false, false, false, null, 0)
+        set(value) {
+            val enabledChanged = value.enabled != field.enabled
+            field = value
+            if (enabledChanged) {
+                enabledChangeListener?.onEnabledChanged(value.enabled)
+            }
+            _progress.postValue(value)
+        }
+    private val _progress = MutableLiveData<Progress>().apply { postValue(_data) }
+    val progress: LiveData<Progress>
+        get() = _progress
+    private var controller: MediaController? = null
+        set(value) {
+            if (field?.sessionToken != value?.sessionToken) {
+                field?.unregisterCallback(callback)
+                value?.registerCallback(callback)
+                field = value
+            }
+        }
+    private var playbackState: PlaybackState? = null
+    private var callback =
+        object : MediaController.Callback() {
+            override fun onPlaybackStateChanged(state: PlaybackState?) {
+                playbackState = state
+                if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) {
+                    clearController()
+                } else {
+                    checkIfPollingNeeded()
+                }
+            }
+
+            override fun onSessionDestroyed() {
+                clearController()
+            }
+        }
+    private var cancel: Runnable? = null
+
+    /** Indicates if the seek interaction is considered a false guesture. */
+    private var isFalseSeek = false
+
+    /** Listening state (QS open or closed) is used to control polling of progress. */
+    var listening = true
+        set(value) =
+            bgExecutor.execute {
+                if (field != value) {
+                    field = value
+                    checkIfPollingNeeded()
+                }
+            }
+
+    private var scrubbingChangeListener: ScrubbingChangeListener? = null
+    private var enabledChangeListener: EnabledChangeListener? = null
+
+    /** Set to true when the user is touching the seek bar to change the position. */
+    private var scrubbing = false
+        set(value) {
+            if (field != value) {
+                field = value
+                checkIfPollingNeeded()
+                scrubbingChangeListener?.onScrubbingChanged(value)
+                _data = _data.copy(scrubbing = value)
+            }
+        }
+
+    lateinit var logSeek: () -> Unit
+
+    /** Event indicating that the user has started interacting with the seek bar. */
+    @AnyThread
+    fun onSeekStarting() =
+        bgExecutor.execute {
+            scrubbing = true
+            isFalseSeek = false
+        }
+
+    /**
+     * Event indicating that the user has moved the seek bar.
+     *
+     * @param position Current location in the track.
+     */
+    @AnyThread
+    fun onSeekProgress(position: Long) =
+        bgExecutor.execute {
+            if (scrubbing) {
+                // The user hasn't yet finished their touch gesture, so only update the data for
+                // visual
+                // feedback and don't update [controller] yet.
+                _data = _data.copy(elapsedTime = position.toInt())
+            } else {
+                // The seek progress came from an a11y action and we should immediately update to
+                // the
+                // new position. (a11y actions to change the seekbar position don't trigger
+                // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.)
+                onSeek(position)
+            }
+        }
+
+    /** Event indicating that the seek interaction is a false gesture and it should be ignored. */
+    @AnyThread
+    fun onSeekFalse() =
+        bgExecutor.execute {
+            if (scrubbing) {
+                isFalseSeek = true
+            }
+        }
+
+    /**
+     * Handle request to change the current position in the media track.
+     * @param position Place to seek to in the track.
+     */
+    @AnyThread
+    fun onSeek(position: Long) =
+        bgExecutor.execute {
+            if (isFalseSeek) {
+                scrubbing = false
+                checkPlaybackPosition()
+            } else {
+                logSeek()
+                controller?.transportControls?.seekTo(position)
+                // Invalidate the cached playbackState to avoid the thumb jumping back to the
+                // previous
+                // position.
+                playbackState = null
+                scrubbing = false
+            }
+        }
+
+    /**
+     * Updates media information.
+     *
+     * This function makes a binder call, so it must happen on a worker thread.
+     *
+     * @param mediaController controller for media session
+     */
+    @WorkerThread
+    fun updateController(mediaController: MediaController?) {
+        controller = mediaController
+        playbackState = controller?.playbackState
+        val mediaMetadata = controller?.metadata
+        val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
+        val position = playbackState?.position?.toInt()
+        val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
+        val playing =
+            NotificationMediaManager.isPlayingState(
+                playbackState?.state ?: PlaybackState.STATE_NONE
+            )
+        val enabled =
+            if (
+                playbackState == null ||
+                    playbackState?.getState() == PlaybackState.STATE_NONE ||
+                    (duration <= 0)
+            )
+                false
+            else true
+        _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration)
+        checkIfPollingNeeded()
+    }
+
+    /**
+     * Puts the seek bar into a resumption state.
+     *
+     * This should be called when the media session behind the controller has been destroyed.
+     */
+    @AnyThread
+    fun clearController() =
+        bgExecutor.execute {
+            controller = null
+            playbackState = null
+            cancel?.run()
+            cancel = null
+            _data = _data.copy(enabled = false)
+        }
+
+    /** Call to clean up any resources. */
+    @AnyThread
+    fun onDestroy() =
+        bgExecutor.execute {
+            controller = null
+            playbackState = null
+            cancel?.run()
+            cancel = null
+            scrubbingChangeListener = null
+            enabledChangeListener = null
+        }
+
+    @WorkerThread
+    private fun checkPlaybackPosition() {
+        val duration = _data.duration ?: -1
+        val currentPosition = playbackState?.computePosition(duration.toLong())?.toInt()
+        if (currentPosition != null && _data.elapsedTime != currentPosition) {
+            _data = _data.copy(elapsedTime = currentPosition)
+        }
+    }
+
+    @WorkerThread
+    private fun checkIfPollingNeeded() {
+        val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false
+        if (needed) {
+            if (cancel == null) {
+                cancel =
+                    bgExecutor.executeRepeatedly(
+                        this::checkPlaybackPosition,
+                        0L,
+                        POSITION_UPDATE_INTERVAL_MILLIS
+                    )
+            }
+        } else {
+            cancel?.run()
+            cancel = null
+        }
+    }
+
+    /** Gets a listener to attach to the seek bar to handle seeking. */
+    val seekBarListener: SeekBar.OnSeekBarChangeListener
+        get() {
+            return SeekBarChangeListener(this, falsingManager)
+        }
+
+    /** Attach touch handlers to the seek bar view. */
+    fun attachTouchHandlers(bar: SeekBar) {
+        bar.setOnSeekBarChangeListener(seekBarListener)
+        bar.setOnTouchListener(SeekBarTouchListener(this, bar))
+    }
+
+    fun setScrubbingChangeListener(listener: ScrubbingChangeListener) {
+        scrubbingChangeListener = listener
+    }
+
+    fun removeScrubbingChangeListener(listener: ScrubbingChangeListener) {
+        if (listener == scrubbingChangeListener) {
+            scrubbingChangeListener = null
+        }
+    }
+
+    fun setEnabledChangeListener(listener: EnabledChangeListener) {
+        enabledChangeListener = listener
+    }
+
+    fun removeEnabledChangeListener(listener: EnabledChangeListener) {
+        if (listener == enabledChangeListener) {
+            enabledChangeListener = null
+        }
+    }
+
+    /** Listener interface to be notified when the user starts or stops scrubbing. */
+    interface ScrubbingChangeListener {
+        fun onScrubbingChanged(scrubbing: Boolean)
+    }
+
+    /** Listener interface to be notified when the seekbar's enabled status changes. */
+    interface EnabledChangeListener {
+        fun onEnabledChanged(enabled: Boolean)
+    }
+
+    private class SeekBarChangeListener(
+        val viewModel: SeekBarViewModel,
+        val falsingManager: FalsingManager,
+    ) : SeekBar.OnSeekBarChangeListener {
+        override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
+            if (fromUser) {
+                viewModel.onSeekProgress(progress.toLong())
+            }
+        }
+
+        override fun onStartTrackingTouch(bar: SeekBar) {
+            viewModel.onSeekStarting()
+        }
+
+        override fun onStopTrackingTouch(bar: SeekBar) {
+            if (falsingManager.isFalseTouch(MEDIA_SEEKBAR)) {
+                viewModel.onSeekFalse()
+            }
+            viewModel.onSeek(bar.progress.toLong())
+        }
+    }
+
+    /**
+     * Responsible for intercepting touch events before they reach the seek bar.
+     *
+     * This reduces the gestures seen by the seek bar so that users don't accidentially seek when
+     * they intend to scroll the carousel.
+     */
+    private class SeekBarTouchListener(
+        private val viewModel: SeekBarViewModel,
+        private val bar: SeekBar,
+    ) : View.OnTouchListener, GestureDetector.OnGestureListener {
+
+        // Gesture detector helps decide which touch events to intercept.
+        private val detector = GestureDetectorCompat(bar.context, this)
+        // Velocity threshold used to decide when a fling is considered a false gesture.
+        private val flingVelocity: Int =
+            ViewConfiguration.get(bar.context).run {
+                getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR
+            }
+        // Indicates if the gesture should go to the seek bar or if it should be intercepted.
+        private var shouldGoToSeekBar = false
+
+        /**
+         * Decide which touch events to intercept before they reach the seek bar.
+         *
+         * Based on the gesture detected, we decide whether we want the event to reach the seek bar.
+         * If we want the seek bar to see the event, then we return false so that the event isn't
+         * handled here and it will be passed along. If, however, we don't want the seek bar to see
+         * the event, then return true so that the event is handled here.
+         *
+         * When the seek bar is contained in the carousel, the carousel still has the ability to
+         * intercept the touch event. So, even though we may handle the event here, the carousel can
+         * still intercept the event. This way, gestures that we consider falses on the seek bar can
+         * still be used by the carousel for paging.
+         *
+         * Returns true for events that we don't want dispatched to the seek bar.
+         */
+        override fun onTouch(view: View, event: MotionEvent): Boolean {
+            if (view != bar) {
+                return false
+            }
+            detector.onTouchEvent(event)
+            return !shouldGoToSeekBar
+        }
+
+        /**
+         * Handle down events that press down on the thumb.
+         *
+         * On the down action, determine a target box around the thumb to know when a scroll gesture
+         * starts by clicking on the thumb. The target box will be used by subsequent onScroll
+         * events.
+         *
+         * Returns true when the down event hits within the target box of the thumb.
+         */
+        override fun onDown(event: MotionEvent): Boolean {
+            val padL = bar.paddingLeft
+            val padR = bar.paddingRight
+            // Compute the X location of the thumb as a function of the seek bar progress.
+            // TODO: account for thumb offset
+            val progress = bar.getProgress()
+            val range = bar.max - bar.min
+            val widthFraction =
+                if (range > 0) {
+                    (progress - bar.min).toDouble() / range
+                } else {
+                    0.0
+                }
+            val availableWidth = bar.width - padL - padR
+            val thumbX =
+                if (bar.isLayoutRtl()) {
+                    padL + availableWidth * (1 - widthFraction)
+                } else {
+                    padL + availableWidth * widthFraction
+                }
+            // Set the min, max boundaries of the thumb box.
+            // I'm cheating by using the height of the seek bar as the width of the box.
+            val halfHeight: Int = bar.height / 2
+            val targetBoxMinX = (Math.round(thumbX) - halfHeight).toInt()
+            val targetBoxMaxX = (Math.round(thumbX) + halfHeight).toInt()
+            // If the x position of the down event is within the box, then request that the parent
+            // not intercept the event.
+            val x = Math.round(event.x)
+            shouldGoToSeekBar = x >= targetBoxMinX && x <= targetBoxMaxX
+            if (shouldGoToSeekBar) {
+                bar.parent?.requestDisallowInterceptTouchEvent(true)
+            }
+            return shouldGoToSeekBar
+        }
+
+        /**
+         * Always handle single tap up.
+         *
+         * This enables the user to single tap anywhere on the seek bar to seek to that position.
+         */
+        override fun onSingleTapUp(event: MotionEvent): Boolean {
+            shouldGoToSeekBar = true
+            return true
+        }
+
+        /**
+         * Handle scroll events when the down event is on the thumb.
+         *
+         * Returns true when the down event of the scroll hits within the target box of the thumb.
+         */
+        override fun onScroll(
+            eventStart: MotionEvent,
+            event: MotionEvent,
+            distanceX: Float,
+            distanceY: Float
+        ): Boolean {
+            return shouldGoToSeekBar
+        }
+
+        /**
+         * Handle fling events when the down event is on the thumb.
+         *
+         * Gestures that include a fling are considered a false gesture on the seek bar.
+         */
+        override fun onFling(
+            eventStart: MotionEvent,
+            event: MotionEvent,
+            velocityX: Float,
+            velocityY: Float
+        ): Boolean {
+            if (Math.abs(velocityX) > flingVelocity || Math.abs(velocityY) > flingVelocity) {
+                viewModel.onSeekFalse()
+            }
+            return shouldGoToSeekBar
+        }
+
+        override fun onShowPress(event: MotionEvent) {}
+
+        override fun onLongPress(event: MotionEvent) {}
+    }
+
+    /** State seen by seek bar UI. */
+    data class Progress(
+        val enabled: Boolean,
+        val seekAvailable: Boolean,
+        val playing: Boolean,
+        val scrubbing: Boolean,
+        val elapsedTime: Int?,
+        val duration: Int
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
new file mode 100644
index 0000000..1a10b18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.recommendation
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.ui.IlluminationDrawable
+import com.android.systemui.util.animation.TransitionLayout
+
+private const val TAG = "RecommendationViewHolder"
+
+/** ViewHolder for a Smartspace media recommendation. */
+class RecommendationViewHolder private constructor(itemView: View) {
+
+    val recommendations = itemView as TransitionLayout
+
+    // Recommendation screen
+    val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
+    val mediaCoverItems =
+        listOf<ImageView>(
+            itemView.requireViewById(R.id.media_cover1),
+            itemView.requireViewById(R.id.media_cover2),
+            itemView.requireViewById(R.id.media_cover3)
+        )
+    val mediaCoverContainers =
+        listOf<ViewGroup>(
+            itemView.requireViewById(R.id.media_cover1_container),
+            itemView.requireViewById(R.id.media_cover2_container),
+            itemView.requireViewById(R.id.media_cover3_container)
+        )
+    val mediaTitles: List<TextView> =
+        listOf(
+            itemView.requireViewById(R.id.media_title1),
+            itemView.requireViewById(R.id.media_title2),
+            itemView.requireViewById(R.id.media_title3)
+        )
+    val mediaSubtitles: List<TextView> =
+        listOf(
+            itemView.requireViewById(R.id.media_subtitle1),
+            itemView.requireViewById(R.id.media_subtitle2),
+            itemView.requireViewById(R.id.media_subtitle3)
+        )
+
+    val gutsViewHolder = GutsViewHolder(itemView)
+
+    init {
+        (recommendations.background as IlluminationDrawable).let { background ->
+            mediaCoverContainers.forEach { background.registerLightSource(it) }
+            background.registerLightSource(gutsViewHolder.cancel)
+            background.registerLightSource(gutsViewHolder.dismiss)
+            background.registerLightSource(gutsViewHolder.settings)
+        }
+    }
+
+    fun marquee(start: Boolean, delay: Long) {
+        gutsViewHolder.marquee(start, delay, TAG)
+    }
+
+    companion object {
+        /**
+         * Creates a RecommendationViewHolder.
+         *
+         * @param inflater LayoutInflater to use to inflate the layout.
+         * @param parent Parent of inflated view.
+         */
+        @JvmStatic
+        fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
+            val itemView =
+                inflater.inflate(
+                    R.layout.media_smartspace_recommendations,
+                    parent,
+                    false /* attachToRoot */
+                )
+            // Because this media view (a TransitionLayout) is used to measure and layout the views
+            // in various states before being attached to its parent, we can't depend on the default
+            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
+            itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+            return RecommendationViewHolder(itemView)
+        }
+
+        // Res Ids for the control components on the recommendation view.
+        val controlsIds =
+            setOf(
+                R.id.recommendation_card_icon,
+                R.id.media_cover1,
+                R.id.media_cover2,
+                R.id.media_cover3,
+                R.id.media_cover1_container,
+                R.id.media_cover2_container,
+                R.id.media_cover3_container,
+                R.id.media_title1,
+                R.id.media_title2,
+                R.id.media_title3,
+                R.id.media_subtitle1,
+                R.id.media_subtitle2,
+                R.id.media_subtitle3
+            )
+
+        val mediaTitlesAndSubtitlesIds =
+            setOf(
+                R.id.media_title1,
+                R.id.media_title2,
+                R.id.media_title3,
+                R.id.media_subtitle1,
+                R.id.media_subtitle2,
+                R.id.media_subtitle3
+            )
+
+        val mediaContainersIds =
+            setOf(
+                R.id.media_cover1_container,
+                R.id.media_cover2_container,
+                R.id.media_cover3_container
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
new file mode 100644
index 0000000..1df42c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.media.controls.models.recommendation
+
+import android.app.smartspace.SmartspaceAction
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.text.TextUtils
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.InstanceId
+
+@VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
+
+/** State of a Smartspace media recommendations view. */
+data class SmartspaceMediaData(
+    /** Unique id of a Smartspace media target. */
+    val targetId: String,
+    /** Indicates if the status is active. */
+    val isActive: Boolean,
+    /** Package name of the media recommendations' provider-app. */
+    val packageName: String,
+    /** Action to perform when the card is tapped. Also contains the target's extra info. */
+    val cardAction: SmartspaceAction?,
+    /** List of media recommendations. */
+    val recommendations: List<SmartspaceAction>,
+    /** Intent for the user's initiated dismissal. */
+    val dismissIntent: Intent?,
+    /** The timestamp in milliseconds that headphone is connected. */
+    val headphoneConnectionTimeMillis: Long,
+    /** Instance ID for [MediaUiEventLogger] */
+    val instanceId: InstanceId
+) {
+    /**
+     * Indicates if all the data is valid.
+     *
+     * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
+     * ```
+     *     [NUM_REQUIRED_RECOMMENDATIONS].
+     * ```
+     */
+    fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS
+
+    /** Returns the list of [recommendations] that have valid data. */
+    fun getValidRecommendations() = recommendations.filter { it.icon != null }
+
+    /** Returns the upstream app name if available. */
+    fun getAppName(context: Context): CharSequence? {
+        val nameFromAction = cardAction?.intent?.extras?.getString(KEY_SMARTSPACE_APP_NAME)
+        if (!TextUtils.isEmpty(nameFromAction)) {
+            return nameFromAction
+        }
+
+        val packageManager = context.packageManager
+        packageManager.getLaunchIntentForPackage(packageName)?.let {
+            val launchActivity = it.resolveActivityInfo(packageManager, 0)
+            return launchActivity.loadLabel(packageManager)
+        }
+
+        Log.w(
+            TAG,
+            "Package $packageName does not have a main launcher activity. " +
+                "Fallback to full app name"
+        )
+        return try {
+            val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
+            packageManager.getApplicationLabel(applicationInfo)
+        } catch (e: PackageManager.NameNotFoundException) {
+            null
+        }
+    }
+}
+
+const val NUM_REQUIRED_RECOMMENDATIONS = 3
+private val TAG = SmartspaceMediaData::class.simpleName!!
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt
new file mode 100644
index 0000000..cacb3e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.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.media.controls.models.recommendation
+
+import android.app.smartspace.SmartspaceTarget
+import android.util.Log
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
+import javax.inject.Inject
+
+private const val TAG = "SsMediaDataProvider"
+
+/** Provides SmartspaceTargets of media types for SystemUI media control. */
+class SmartspaceMediaDataProvider @Inject constructor() : BcSmartspaceDataPlugin {
+
+    private val smartspaceMediaTargetListeners: MutableList<SmartspaceTargetListener> =
+        mutableListOf()
+
+    override fun registerListener(smartspaceTargetListener: SmartspaceTargetListener) {
+        smartspaceMediaTargetListeners.add(smartspaceTargetListener)
+    }
+
+    override fun unregisterListener(smartspaceTargetListener: SmartspaceTargetListener?) {
+        smartspaceMediaTargetListeners.remove(smartspaceTargetListener)
+    }
+
+    /** Updates Smartspace data and propagates it to any listeners. */
+    override fun onTargetsAvailable(targets: List<SmartspaceTarget>) {
+        Log.d(TAG, "Forwarding Smartspace updates $targets")
+        smartspaceMediaTargetListeners.forEach { it.onSmartspaceTargetsUpdated(targets) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
new file mode 100644
index 0000000..ff763d8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.content.Context
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.media.InfoMediaManager
+import com.android.settingslib.media.LocalMediaManager
+import javax.inject.Inject
+
+/** Factory to create [LocalMediaManager] objects. */
+class LocalMediaManagerFactory
+@Inject
+constructor(
+    private val context: Context,
+    private val localBluetoothManager: LocalBluetoothManager?
+) {
+    /** Creates a [LocalMediaManager] for the given package. */
+    fun create(packageName: String): LocalMediaManager {
+        return InfoMediaManager(context, packageName, null, localBluetoothManager).run {
+            LocalMediaManager(context, localBluetoothManager, this, packageName)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
new file mode 100644
index 0000000..789ef40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import javax.inject.Inject
+
+/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */
+class MediaDataCombineLatest @Inject constructor() :
+    MediaDataManager.Listener, MediaDeviceManager.Listener {
+
+    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+    private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+            entries[key] = data to entries.remove(oldKey)?.second
+            update(key, oldKey)
+        } else {
+            entries[key] = data to entries[key]?.second
+            update(key, key)
+        }
+    }
+
+    override fun onSmartspaceMediaDataLoaded(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) {
+        listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        remove(key)
+    }
+
+    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) {
+        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+            entries[key] = entries.remove(oldKey)?.first to data
+            update(key, oldKey)
+        } else {
+            entries[key] = entries[key]?.first to data
+            update(key, key)
+        }
+    }
+
+    override fun onKeyRemoved(key: String) {
+        remove(key)
+    }
+
+    /**
+     * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
+     */
+    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
+
+    /** Remove a listener registered with addListener. */
+    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
+
+    private fun update(key: String, oldKey: String?) {
+        val (entry, device) = entries[key] ?: null to null
+        if (entry != null && device != null) {
+            val data = entry.copy(device = device)
+            val listenersCopy = listeners.toSet()
+            listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) }
+        }
+    }
+
+    private fun remove(key: String) {
+        entries.remove(key)?.let {
+            val listenersCopy = listeners.toSet()
+            listenersCopy.forEach { it.onMediaDataRemoved(key) }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
new file mode 100644
index 0000000..cf71d67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.content.Context
+import android.os.SystemProperties
+import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.util.time.SystemClock
+import java.util.SortedMap
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import kotlin.collections.LinkedHashMap
+
+private const val TAG = "MediaDataFilter"
+private const val DEBUG = true
+private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
+    ("com.google" +
+        ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
+private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
+
+/**
+ * Maximum age of a media control to re-activate on smartspace signal. If there is no media control
+ * available within this time window, smartspace recommendations will be shown instead.
+ */
+@VisibleForTesting
+internal val SMARTSPACE_MAX_AGE =
+    SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
+
+/**
+ * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
+ * switches (removing entries for the previous user, adding back entries for the current user). Also
+ * filters out smartspace updates in favor of local recent media, when avaialble.
+ *
+ * This is added at the end of the pipeline since we may still need to handle callbacks from
+ * background users (e.g. timeouts).
+ */
+class MediaDataFilter
+@Inject
+constructor(
+    private val context: Context,
+    private val userTracker: UserTracker,
+    private val broadcastSender: BroadcastSender,
+    private val lockscreenUserManager: NotificationLockscreenUserManager,
+    @Main private val executor: Executor,
+    private val systemClock: SystemClock,
+    private val logger: MediaUiEventLogger
+) : MediaDataManager.Listener {
+    private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+    internal val listeners: Set<MediaDataManager.Listener>
+        get() = _listeners.toSet()
+    internal lateinit var mediaDataManager: MediaDataManager
+
+    private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
+    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+    private var reactivatedKey: String? = null
+
+    private val userTrackerCallback =
+        object : UserTracker.Callback {
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                handleUserSwitched(newUser)
+            }
+        }
+
+    init {
+        userTracker.addCallback(userTrackerCallback, executor)
+    }
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        if (oldKey != null && oldKey != key) {
+            allEntries.remove(oldKey)
+        }
+        allEntries.put(key, data)
+
+        if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
+            return
+        }
+
+        if (oldKey != null && oldKey != key) {
+            userEntries.remove(oldKey)
+        }
+        userEntries.put(key, data)
+
+        // Notify listeners
+        listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
+    }
+
+    override fun onSmartspaceMediaDataLoaded(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) {
+        if (!data.isActive) {
+            Log.d(TAG, "Inactive recommendation data. Skip triggering.")
+            return
+        }
+
+        // Override the pass-in value here, as the order of Smartspace card is only determined here.
+        var shouldPrioritizeMutable = false
+        smartspaceMediaData = data
+
+        // Before forwarding the smartspace target, first check if we have recently inactive media
+        val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
+        val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
+        var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
+        data.cardAction?.let {
+            val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
+            if (smartspaceMaxAgeSeconds > 0) {
+                smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
+            }
+        }
+
+        val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
+
+        if (timeSinceActive < smartspaceMaxAgeMillis) {
+            // It could happen there are existing active media resume cards, then we don't need to
+            // reactivate.
+            if (shouldReactivate) {
+                val lastActiveKey = sorted.lastKey() // most recently active
+                // Notify listeners to consider this media active
+                Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
+                reactivatedKey = lastActiveKey
+                val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
+                logger.logRecommendationActivated(
+                    mediaData.appUid,
+                    mediaData.packageName,
+                    mediaData.instanceId
+                )
+                listeners.forEach {
+                    it.onMediaDataLoaded(
+                        lastActiveKey,
+                        lastActiveKey,
+                        mediaData,
+                        receivedSmartspaceCardLatency =
+                            (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
+                                .toInt(),
+                        isSsReactivated = true
+                    )
+                }
+            }
+        } else {
+            // Mark to prioritize Smartspace card if no recent media.
+            shouldPrioritizeMutable = true
+        }
+
+        if (!data.isValid()) {
+            Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
+            return
+        }
+        logger.logRecommendationAdded(
+            smartspaceMediaData.packageName,
+            smartspaceMediaData.instanceId
+        )
+        listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        allEntries.remove(key)
+        userEntries.remove(key)?.let {
+            // Only notify listeners if something actually changed
+            listeners.forEach { it.onMediaDataRemoved(key) }
+        }
+    }
+
+    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        // First check if we had reactivated media instead of forwarding smartspace
+        reactivatedKey?.let {
+            val lastActiveKey = it
+            reactivatedKey = null
+            Log.d(TAG, "expiring reactivated key $lastActiveKey")
+            // Notify listeners to update with actual active value
+            userEntries.get(lastActiveKey)?.let { mediaData ->
+                listeners.forEach {
+                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
+                }
+            }
+        }
+
+        if (smartspaceMediaData.isActive) {
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
+        }
+        listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    @VisibleForTesting
+    internal fun handleUserSwitched(id: Int) {
+        // If the user changes, remove all current MediaData objects and inform listeners
+        val listenersCopy = listeners
+        val keyCopy = userEntries.keys.toMutableList()
+        // Clear the list first, to make sure callbacks from listeners if we have any entries
+        // are up to date
+        userEntries.clear()
+        keyCopy.forEach {
+            if (DEBUG) Log.d(TAG, "Removing $it after user change")
+            listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
+        }
+
+        allEntries.forEach { (key, data) ->
+            if (lockscreenUserManager.isCurrentProfile(data.userId)) {
+                if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
+                userEntries.put(key, data)
+                listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
+            }
+        }
+    }
+
+    /** Invoked when the user has dismissed the media carousel */
+    fun onSwipeToDismiss() {
+        if (DEBUG) Log.d(TAG, "Media carousel swiped away")
+        val mediaKeys = userEntries.keys.toSet()
+        mediaKeys.forEach {
+            // Force updates to listeners, needed for re-activated card
+            mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true)
+        }
+        if (smartspaceMediaData.isActive) {
+            val dismissIntent = smartspaceMediaData.dismissIntent
+            if (dismissIntent == null) {
+                Log.w(
+                    TAG,
+                    "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
+                )
+            } else if (
+                dismissIntent.getComponent() != null &&
+                    dismissIntent.getComponent().getClassName() ==
+                        EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
+            ) {
+                // Dismiss the card Smartspace data through Smartspace trampoline activity.
+                context.startActivity(dismissIntent)
+            } else {
+                broadcastSender.sendBroadcast(dismissIntent)
+            }
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
+            mediaDataManager.dismissSmartspaceRecommendation(
+                smartspaceMediaData.targetId,
+                delay = 0L
+            )
+        }
+    }
+
+    /** Are there any active media entries, including the recommendation? */
+    fun hasActiveMediaOrRecommendation() =
+        userEntries.any { it.value.active } ||
+            (smartspaceMediaData.isActive &&
+                (smartspaceMediaData.isValid() || reactivatedKey != null))
+
+    /** Are there any media entries we should display? */
+    fun hasAnyMediaOrRecommendation() =
+        userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+
+    /** Are there any media notifications active (excluding the recommendation)? */
+    fun hasActiveMedia() = userEntries.any { it.value.active }
+
+    /** Are there any media entries we should display (excluding the recommendation)? */
+    fun hasAnyMedia() = userEntries.isNotEmpty()
+
+    /** Add a listener for filtered [MediaData] changes */
+    fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
+
+    /** Remove a listener that was registered with addListener */
+    fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
+
+    /**
+     * Return the time since last active for the most-recent media.
+     *
+     * @param sortedEntries userEntries sorted from the earliest to the most-recent.
+     *
+     * @return The duration in milliseconds from the most-recent media's last active timestamp to
+     * the present. MAX_VALUE will be returned if there is no media.
+     */
+    private fun timeSinceActiveForMostRecentMedia(
+        sortedEntries: SortedMap<String, MediaData>
+    ): Long {
+        if (sortedEntries.isEmpty()) {
+            return Long.MAX_VALUE
+        }
+
+        val now = systemClock.elapsedRealtime()
+        val lastActiveKey = sortedEntries.lastKey() // most recently active
+        return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
new file mode 100644
index 0000000..3012bb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -0,0 +1,1459 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
+import android.app.PendingIntent
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceSession
+import android.app.smartspace.SmartspaceTarget
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.net.Uri
+import android.os.Parcelable
+import android.os.Process
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.text.TextUtils
+import android.util.Log
+import androidx.media.utils.MediaConstants
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.logging.InstanceId
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaAction
+import com.android.systemui.media.controls.models.player.MediaButton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.notification.row.HybridGroupManager
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.Assert
+import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.traceSection
+import java.io.IOException
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+// URI fields to try loading album art from
+private val ART_URIS =
+    arrayOf(
+        MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+        MediaMetadata.METADATA_KEY_ART_URI,
+        MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
+    )
+
+private const val TAG = "MediaDataManager"
+private const val DEBUG = true
+private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
+
+private val LOADING =
+    MediaData(
+        userId = -1,
+        initialized = false,
+        app = null,
+        appIcon = null,
+        artist = null,
+        song = null,
+        artwork = null,
+        actions = emptyList(),
+        actionsToShowInCompact = emptyList(),
+        packageName = "INVALID",
+        token = null,
+        clickIntent = null,
+        device = null,
+        active = true,
+        resumeAction = null,
+        instanceId = InstanceId.fakeInstanceId(-1),
+        appUid = Process.INVALID_UID
+    )
+
+@VisibleForTesting
+internal val EMPTY_SMARTSPACE_MEDIA_DATA =
+    SmartspaceMediaData(
+        targetId = "INVALID",
+        isActive = false,
+        packageName = "INVALID",
+        cardAction = null,
+        recommendations = emptyList(),
+        dismissIntent = null,
+        headphoneConnectionTimeMillis = 0,
+        instanceId = InstanceId.fakeInstanceId(-1)
+    )
+
+fun isMediaNotification(sbn: StatusBarNotification): Boolean {
+    return sbn.notification.isMediaNotification()
+}
+
+/**
+ * Allow recommendations from smartspace to show in media controls. Requires
+ * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
+ */
+private fun allowMediaRecommendations(context: Context): Boolean {
+    val flag =
+        Settings.Secure.getInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
+    return Utils.useQsMediaPlayer(context) && flag > 0
+}
+
+/** A class that facilitates management and loading of Media Data, ready for binding. */
+@SysUISingleton
+class MediaDataManager(
+    private val context: Context,
+    @Background private val backgroundExecutor: Executor,
+    @Main private val uiExecutor: Executor,
+    @Main private val foregroundExecutor: DelayableExecutor,
+    private val mediaControllerFactory: MediaControllerFactory,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    dumpManager: DumpManager,
+    mediaTimeoutListener: MediaTimeoutListener,
+    mediaResumeListener: MediaResumeListener,
+    mediaSessionBasedFilter: MediaSessionBasedFilter,
+    mediaDeviceManager: MediaDeviceManager,
+    mediaDataCombineLatest: MediaDataCombineLatest,
+    private val mediaDataFilter: MediaDataFilter,
+    private val activityStarter: ActivityStarter,
+    private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+    private var useMediaResumption: Boolean,
+    private val useQsMediaPlayer: Boolean,
+    private val systemClock: SystemClock,
+    private val tunerService: TunerService,
+    private val mediaFlags: MediaFlags,
+    private val logger: MediaUiEventLogger,
+    private val smartspaceManager: SmartspaceManager,
+) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
+
+    companion object {
+        // UI surface label for subscribing Smartspace updates.
+        @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+
+        // Smartspace package name's extra key.
+        @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+
+        // Maximum number of actions allowed in compact view
+        @JvmField val MAX_COMPACT_ACTIONS = 3
+
+        // Maximum number of actions allowed in expanded view
+        @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
+    }
+
+    private val themeText =
+        com.android.settingslib.Utils.getColorAttr(
+                context,
+                com.android.internal.R.attr.textColorPrimary
+            )
+            .defaultColor
+
+    // Internal listeners are part of the internal pipeline. External listeners (those registered
+    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+    // the internal pipeline.
+    // Another way to think of the distinction between internal and external listeners is the
+    // following. Internal listeners are listeners that MediaDataManager depends on, and external
+    // listeners are listeners that depend on MediaDataManager.
+    // TODO(b/159539991#comment5): Move internal listeners to separate package.
+    private val internalListeners: MutableSet<Listener> = mutableSetOf()
+    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    // There should ONLY be at most one Smartspace media recommendation.
+    var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+    private var smartspaceSession: SmartspaceSession? = null
+    private var allowMediaRecommendations = allowMediaRecommendations(context)
+
+    /** Check whether this notification is an RCN */
+    private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
+        return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
+    }
+
+    @Inject
+    constructor(
+        context: Context,
+        @Background backgroundExecutor: Executor,
+        @Main uiExecutor: Executor,
+        @Main foregroundExecutor: DelayableExecutor,
+        mediaControllerFactory: MediaControllerFactory,
+        dumpManager: DumpManager,
+        broadcastDispatcher: BroadcastDispatcher,
+        mediaTimeoutListener: MediaTimeoutListener,
+        mediaResumeListener: MediaResumeListener,
+        mediaSessionBasedFilter: MediaSessionBasedFilter,
+        mediaDeviceManager: MediaDeviceManager,
+        mediaDataCombineLatest: MediaDataCombineLatest,
+        mediaDataFilter: MediaDataFilter,
+        activityStarter: ActivityStarter,
+        smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
+        clock: SystemClock,
+        tunerService: TunerService,
+        mediaFlags: MediaFlags,
+        logger: MediaUiEventLogger,
+        smartspaceManager: SmartspaceManager,
+    ) : this(
+        context,
+        backgroundExecutor,
+        uiExecutor,
+        foregroundExecutor,
+        mediaControllerFactory,
+        broadcastDispatcher,
+        dumpManager,
+        mediaTimeoutListener,
+        mediaResumeListener,
+        mediaSessionBasedFilter,
+        mediaDeviceManager,
+        mediaDataCombineLatest,
+        mediaDataFilter,
+        activityStarter,
+        smartspaceMediaDataProvider,
+        Utils.useMediaResumption(context),
+        Utils.useQsMediaPlayer(context),
+        clock,
+        tunerService,
+        mediaFlags,
+        logger,
+        smartspaceManager,
+    )
+
+    private val appChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                when (intent.action) {
+                    Intent.ACTION_PACKAGES_SUSPENDED -> {
+                        val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+                        packages?.forEach { removeAllForPackage(it) }
+                    }
+                    Intent.ACTION_PACKAGE_REMOVED,
+                    Intent.ACTION_PACKAGE_RESTARTED -> {
+                        intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
+                    }
+                }
+            }
+        }
+
+    init {
+        dumpManager.registerDumpable(TAG, this)
+
+        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+        // are set as internal listeners so that they receive events. From there, events are
+        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+        // so it is responsible for dispatching events to external listeners. To achieve this,
+        // external listeners that are registered with [MediaDataManager.addListener] are actually
+        // registered as listeners to mediaDataFilter.
+        addInternalListener(mediaTimeoutListener)
+        addInternalListener(mediaResumeListener)
+        addInternalListener(mediaSessionBasedFilter)
+        mediaSessionBasedFilter.addListener(mediaDeviceManager)
+        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+        mediaDeviceManager.addListener(mediaDataCombineLatest)
+        mediaDataCombineLatest.addListener(mediaDataFilter)
+
+        // Set up links back into the pipeline for listeners that need to send events upstream.
+        mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
+            setTimedOut(key, timedOut)
+        }
+        mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
+            updateState(key, state)
+        }
+        mediaResumeListener.setManager(this)
+        mediaDataFilter.mediaDataManager = this
+
+        val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
+        broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
+
+        val uninstallFilter =
+            IntentFilter().apply {
+                addAction(Intent.ACTION_PACKAGE_REMOVED)
+                addAction(Intent.ACTION_PACKAGE_RESTARTED)
+                addDataScheme("package")
+            }
+        // BroadcastDispatcher does not allow filters with data schemes
+        context.registerReceiver(appChangeReceiver, uninstallFilter)
+
+        // Register for Smartspace data updates.
+        smartspaceMediaDataProvider.registerListener(this)
+        smartspaceSession =
+            smartspaceManager.createSmartspaceSession(
+                SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+            )
+        smartspaceSession?.let {
+            it.addOnTargetsAvailableListener(
+                // Use a main uiExecutor thread listening to Smartspace updates instead of using
+                // the existing background executor.
+                // SmartspaceSession has scheduled routine updates which can be unpredictable on
+                // test simulators, using the backgroundExecutor makes it's hard to test the threads
+                // numbers.
+                uiExecutor,
+                SmartspaceSession.OnTargetsAvailableListener { targets ->
+                    smartspaceMediaDataProvider.onTargetsAvailable(targets)
+                }
+            )
+        }
+        smartspaceSession?.let { it.requestSmartspaceUpdate() }
+        tunerService.addTunable(
+            object : TunerService.Tunable {
+                override fun onTuningChanged(key: String?, newValue: String?) {
+                    allowMediaRecommendations = allowMediaRecommendations(context)
+                    if (!allowMediaRecommendations) {
+                        dismissSmartspaceRecommendation(
+                            key = smartspaceMediaData.targetId,
+                            delay = 0L
+                        )
+                    }
+                }
+            },
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+        )
+    }
+
+    fun destroy() {
+        smartspaceMediaDataProvider.unregisterListener(this)
+        context.unregisterReceiver(appChangeReceiver)
+    }
+
+    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
+        if (useQsMediaPlayer && isMediaNotification(sbn)) {
+            var logEvent = false
+            Assert.isMainThread()
+            val oldKey = findExistingEntry(key, sbn.packageName)
+            if (oldKey == null) {
+                val instanceId = logger.getNewInstanceId()
+                val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId)
+                mediaEntries.put(key, temp)
+                logEvent = true
+            } else if (oldKey != key) {
+                // Resume -> active conversion; move to new key
+                val oldData = mediaEntries.remove(oldKey)!!
+                logEvent = true
+                mediaEntries.put(key, oldData)
+            }
+            loadMediaData(key, sbn, oldKey, logEvent)
+        } else {
+            onNotificationRemoved(key)
+        }
+    }
+
+    private fun removeAllForPackage(packageName: String) {
+        Assert.isMainThread()
+        val toRemove = mediaEntries.filter { it.value.packageName == packageName }
+        toRemove.forEach { removeEntry(it.key) }
+    }
+
+    fun setResumeAction(key: String, action: Runnable?) {
+        mediaEntries.get(key)?.let {
+            it.resumeAction = action
+            it.hasCheckedForResume = true
+        }
+    }
+
+    fun addResumptionControls(
+        userId: Int,
+        desc: MediaDescription,
+        action: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        // Resume controls don't have a notification key, so store by package name instead
+        if (!mediaEntries.containsKey(packageName)) {
+            val instanceId = logger.getNewInstanceId()
+            val appUid =
+                try {
+                    context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.w(TAG, "Could not get app UID for $packageName", e)
+                    Process.INVALID_UID
+                }
+
+            val resumeData =
+                LOADING.copy(
+                    packageName = packageName,
+                    resumeAction = action,
+                    hasCheckedForResume = true,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
+            mediaEntries.put(packageName, resumeData)
+            logger.logResumeMediaAdded(appUid, packageName, instanceId)
+        }
+        backgroundExecutor.execute {
+            loadMediaDataInBgForResumption(
+                userId,
+                desc,
+                action,
+                token,
+                appName,
+                appIntent,
+                packageName
+            )
+        }
+    }
+
+    /**
+     * Check if there is an existing entry that matches the key or package name. Returns the key
+     * that matches, or null if not found.
+     */
+    private fun findExistingEntry(key: String, packageName: String): String? {
+        if (mediaEntries.containsKey(key)) {
+            return key
+        }
+        // Check if we already had a resume player
+        if (mediaEntries.containsKey(packageName)) {
+            return packageName
+        }
+        return null
+    }
+
+    private fun loadMediaData(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        logEvent: Boolean = false
+    ) {
+        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) }
+    }
+
+    /** Add a listener for changes in this class */
+    fun addListener(listener: Listener) {
+        // mediaDataFilter is the current end of the internal pipeline. Register external
+        // listeners as listeners to it.
+        mediaDataFilter.addListener(listener)
+    }
+
+    /** Remove a listener for changes in this class */
+    fun removeListener(listener: Listener) {
+        // Since mediaDataFilter is the current end of the internal pipelie, external listeners
+        // have been registered to it. So, they need to be removed from it too.
+        mediaDataFilter.removeListener(listener)
+    }
+
+    /** Add a listener for internal events. */
+    private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
+
+    /**
+     * Notify internal listeners of media loaded event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media loaded event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+        internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+    }
+
+    /**
+     * Notify internal listeners of media removed event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataRemoved(key: String) {
+        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    }
+
+    /**
+     * Notify internal listeners of Smartspace media removed event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     *
+     * @param immediately indicates should apply the UI changes immediately, otherwise wait until
+     * the next refresh-round before UI becomes visible. Should only be true if the update is
+     * initiated by user's interaction.
+     */
+    private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    /**
+     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+     * will make the player not active anymore, hiding it from QQS and Keyguard.
+     * @see MediaData.active
+     */
+    internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
+        mediaEntries[key]?.let {
+            if (timedOut && !forceUpdate) {
+                // Only log this event when media expires on its own
+                logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId)
+            }
+            if (it.active == !timedOut && !forceUpdate) {
+                if (it.resumption) {
+                    if (DEBUG) Log.d(TAG, "timing out resume player $key")
+                    dismissMediaData(key, 0L /* delay */)
+                }
+                return
+            }
+            it.active = !timedOut
+            if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
+            onMediaDataLoaded(key, key, it)
+        }
+    }
+
+    /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
+    private fun updateState(key: String, state: PlaybackState) {
+        mediaEntries.get(key)?.let {
+            val token = it.token
+            if (token == null) {
+                if (DEBUG) Log.d(TAG, "State updated, but token was null")
+                return
+            }
+            val actions =
+                createActionsFromState(
+                    it.packageName,
+                    mediaControllerFactory.create(it.token),
+                    UserHandle(it.userId)
+                )
+
+            // Control buttons
+            // If flag is enabled and controller has a PlaybackState,
+            // create actions from session info
+            // otherwise, no need to update semantic actions.
+            val data =
+                if (actions != null) {
+                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                } else {
+                    it.copy(isPlaying = isPlayingState(state.state))
+                }
+            if (DEBUG) Log.d(TAG, "State updated outside of notification")
+            onMediaDataLoaded(key, key, data)
+        }
+    }
+
+    private fun removeEntry(key: String) {
+        mediaEntries.remove(key)?.let {
+            logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId)
+        }
+        notifyMediaDataRemoved(key)
+    }
+
+    /** Dismiss a media entry. Returns false if the key was not found. */
+    fun dismissMediaData(key: String, delay: Long): Boolean {
+        val existed = mediaEntries[key] != null
+        backgroundExecutor.execute {
+            mediaEntries[key]?.let { mediaData ->
+                if (mediaData.isLocalSession()) {
+                    mediaData.token?.let {
+                        val mediaController = mediaControllerFactory.create(it)
+                        mediaController.transportControls.stop()
+                    }
+                }
+            }
+        }
+        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
+        return existed
+    }
+
+    /**
+     * Called whenever the recommendation has been expired, or swiped from QQS. This will make the
+     * recommendation view to not be shown anymore during this headphone connection session.
+     */
+    fun dismissSmartspaceRecommendation(key: String, delay: Long) {
+        if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
+            // If this doesn't match, or we've already invalidated the data, no action needed
+            return
+        }
+
+        if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
+        if (smartspaceMediaData.isActive) {
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
+        }
+        foregroundExecutor.executeDelayed(
+            { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
+            delay
+        )
+    }
+
+    private fun loadMediaDataInBgForResumption(
+        userId: Int,
+        desc: MediaDescription,
+        resumeAction: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        if (TextUtils.isEmpty(desc.title)) {
+            Log.e(TAG, "Description incomplete")
+            // Delete the placeholder entry
+            mediaEntries.remove(packageName)
+            return
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "adding track for $userId from browser: $desc")
+        }
+
+        // Album art
+        var artworkBitmap = desc.iconBitmap
+        if (artworkBitmap == null && desc.iconUri != null) {
+            artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
+        }
+        val artworkIcon =
+            if (artworkBitmap != null) {
+                Icon.createWithBitmap(artworkBitmap)
+            } else {
+                null
+            }
+
+        val currentEntry = mediaEntries.get(packageName)
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val appUid = currentEntry?.appUid ?: Process.INVALID_UID
+
+        val mediaAction = getResumeMediaAction(resumeAction)
+        val lastActive = systemClock.elapsedRealtime()
+        foregroundExecutor.execute {
+            onMediaDataLoaded(
+                packageName,
+                null,
+                MediaData(
+                    userId,
+                    true,
+                    appName,
+                    null,
+                    desc.subtitle,
+                    desc.title,
+                    artworkIcon,
+                    listOf(mediaAction),
+                    listOf(0),
+                    MediaButton(playOrPause = mediaAction),
+                    packageName,
+                    token,
+                    appIntent,
+                    device = null,
+                    active = false,
+                    resumeAction = resumeAction,
+                    resumption = true,
+                    notificationKey = packageName,
+                    hasCheckedForResume = true,
+                    lastActive = lastActive,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
+            )
+        }
+    }
+
+    fun loadMediaDataInBg(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?,
+        logEvent: Boolean = false
+    ) {
+        val token =
+            sbn.notification.extras.getParcelable(
+                Notification.EXTRA_MEDIA_SESSION,
+                MediaSession.Token::class.java
+            )
+        if (token == null) {
+            return
+        }
+        val mediaController = mediaControllerFactory.create(token)
+        val metadata = mediaController.metadata
+        val notif: Notification = sbn.notification
+
+        val appInfo =
+            notif.extras.getParcelable(
+                Notification.EXTRA_BUILDER_APPLICATION_INFO,
+                ApplicationInfo::class.java
+            )
+                ?: getAppInfoFromPackage(sbn.packageName)
+
+        // Album art
+        var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
+        }
+        if (artworkBitmap == null) {
+            artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
+        }
+        val artWorkIcon =
+            if (artworkBitmap == null) {
+                notif.getLargeIcon()
+            } else {
+                Icon.createWithBitmap(artworkBitmap)
+            }
+
+        // App name
+        val appName = getAppName(sbn, appInfo)
+
+        // App Icon
+        val smallIcon = sbn.notification.smallIcon
+
+        // Song name
+        var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
+        if (song == null) {
+            song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE)
+        }
+        if (song == null) {
+            song = HybridGroupManager.resolveTitle(notif)
+        }
+
+        // Artist name
+        var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST)
+        if (artist == null) {
+            artist = HybridGroupManager.resolveText(notif)
+        }
+
+        // Device name (used for remote cast notifications)
+        var device: MediaDeviceData? = null
+        if (isRemoteCastNotification(sbn)) {
+            val extras = sbn.notification.extras
+            val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
+            val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
+            val deviceIntent =
+                extras.getParcelable(
+                    Notification.EXTRA_MEDIA_REMOTE_INTENT,
+                    PendingIntent::class.java
+                )
+            Log.d(TAG, "$key is RCN for $deviceName")
+
+            if (deviceName != null && deviceIcon > -1) {
+                // Name and icon must be present, but intent may be null
+                val enabled = deviceIntent != null && deviceIntent.isActivity
+                val deviceDrawable =
+                    Icon.createWithResource(sbn.packageName, deviceIcon)
+                        .loadDrawable(sbn.getPackageContext(context))
+                device =
+                    MediaDeviceData(
+                        enabled,
+                        deviceDrawable,
+                        deviceName,
+                        deviceIntent,
+                        showBroadcastButton = false
+                    )
+            }
+        }
+
+        // Control buttons
+        // If flag is enabled and controller has a PlaybackState, create actions from session info
+        // Otherwise, use the notification actions
+        var actionIcons: List<MediaAction> = emptyList()
+        var actionsToShowCollapsed: List<Int> = emptyList()
+        val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
+        if (semanticActions == null) {
+            val actions = createActionsFromNotification(sbn)
+            actionIcons = actions.first
+            actionsToShowCollapsed = actions.second
+        }
+
+        val playbackLocation =
+            if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+            else if (
+                mediaController.playbackInfo?.playbackType ==
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+            )
+                MediaData.PLAYBACK_LOCAL
+            else MediaData.PLAYBACK_CAST_LOCAL
+        val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
+
+        val currentEntry = mediaEntries.get(key)
+        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val appUid = appInfo?.uid ?: Process.INVALID_UID
+
+        if (logEvent) {
+            logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
+        } else if (playbackLocation != currentEntry?.playbackLocation) {
+            logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
+        }
+
+        val lastActive = systemClock.elapsedRealtime()
+        foregroundExecutor.execute {
+            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
+            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+            val active = mediaEntries[key]?.active ?: true
+            onMediaDataLoaded(
+                key,
+                oldKey,
+                MediaData(
+                    sbn.normalizedUserId,
+                    true,
+                    appName,
+                    smallIcon,
+                    artist,
+                    song,
+                    artWorkIcon,
+                    actionIcons,
+                    actionsToShowCollapsed,
+                    semanticActions,
+                    sbn.packageName,
+                    token,
+                    notif.contentIntent,
+                    device,
+                    active,
+                    resumeAction = resumeAction,
+                    playbackLocation = playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = isPlaying,
+                    isClearable = sbn.isClearable(),
+                    lastActive = lastActive,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
+            )
+        }
+    }
+
+    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+        try {
+            return context.packageManager.getApplicationInfo(packageName, 0)
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "Could not get app info for $packageName", e)
+        }
+        return null
+    }
+
+    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+        if (name != null) {
+            return name
+        }
+
+        return if (appInfo != null) {
+            context.packageManager.getApplicationLabel(appInfo).toString()
+        } else {
+            sbn.packageName
+        }
+    }
+
+    /** Generate action buttons based on notification actions */
+    private fun createActionsFromNotification(
+        sbn: StatusBarNotification
+    ): Pair<List<MediaAction>, List<Int>> {
+        val notif = sbn.notification
+        val actionIcons: MutableList<MediaAction> = ArrayList()
+        val actions = notif.actions
+        var actionsToShowCollapsed =
+            notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+                ?: mutableListOf()
+        if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
+            Log.e(
+                TAG,
+                "Too many compact actions for ${sbn.key}," +
+                    "limiting to first $MAX_COMPACT_ACTIONS"
+            )
+            actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
+        }
+
+        if (actions != null) {
+            for ((index, action) in actions.withIndex()) {
+                if (index == MAX_NOTIFICATION_ACTIONS) {
+                    Log.w(
+                        TAG,
+                        "Too many notification actions for ${sbn.key}," +
+                            " limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    )
+                    break
+                }
+                if (action.getIcon() == null) {
+                    if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}")
+                    actionsToShowCollapsed.remove(index)
+                    continue
+                }
+                val runnable =
+                    if (action.actionIntent != null) {
+                        Runnable {
+                            if (action.actionIntent.isActivity) {
+                                activityStarter.startPendingIntentDismissingKeyguard(
+                                    action.actionIntent
+                                )
+                            } else if (action.isAuthenticationRequired()) {
+                                activityStarter.dismissKeyguardThenExecute(
+                                    {
+                                        var result = sendPendingIntent(action.actionIntent)
+                                        result
+                                    },
+                                    {},
+                                    true
+                                )
+                            } else {
+                                sendPendingIntent(action.actionIntent)
+                            }
+                        }
+                    } else {
+                        null
+                    }
+                val mediaActionIcon =
+                    if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+                            Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+                        } else {
+                            action.getIcon()
+                        }
+                        .setTint(themeText)
+                        .loadDrawable(context)
+                val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+                actionIcons.add(mediaAction)
+            }
+        }
+        return Pair(actionIcons, actionsToShowCollapsed)
+    }
+
+    /**
+     * Generates action button info for this media session based on the PlaybackState
+     *
+     * @param packageName Package name for the media app
+     * @param controller MediaController for the current session
+     * @return a Pair consisting of a list of media actions, and a list of ints representing which
+     * ```
+     *      of those actions should be shown in the compact player
+     * ```
+     */
+    private fun createActionsFromState(
+        packageName: String,
+        controller: MediaController,
+        user: UserHandle
+    ): MediaButton? {
+        val state = controller.playbackState
+        if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) {
+            return null
+        }
+
+        // First, check for standard actions
+        val playOrPause =
+            if (isConnectingState(state.state)) {
+                // Spinner needs to be animating to render anything. Start it here.
+                val drawable =
+                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+                (drawable as Animatable).start()
+                MediaAction(
+                    drawable,
+                    null, // no action to perform when clicked
+                    context.getString(R.string.controls_media_button_connecting),
+                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    // Specify a rebind id to prevent the spinner from restarting on later binds.
+                    com.android.internal.R.drawable.progress_small_material
+                )
+            } else if (isPlayingState(state.state)) {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+            } else {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+            }
+        val prevButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+        val nextButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
+
+        // Then, create a way to build any custom actions that will be needed
+        val customActions =
+            state.customActions
+                .asSequence()
+                .filterNotNull()
+                .map { getCustomAction(state, packageName, controller, it) }
+                .iterator()
+        fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
+
+        // Finally, assign the remaining button slots: play/pause A B C D
+        // A = previous, else custom action (if not reserved)
+        // B = next, else custom action (if not reserved)
+        // C and D are always custom actions
+        val reservePrev =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+            ) == true
+        val reserveNext =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+            ) == true
+
+        val prevOrCustom =
+            if (prevButton != null) {
+                prevButton
+            } else if (!reservePrev) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        val nextOrCustom =
+            if (nextButton != null) {
+                nextButton
+            } else if (!reserveNext) {
+                nextCustomAction()
+            } else {
+                null
+            }
+
+        return MediaButton(
+            playOrPause,
+            nextOrCustom,
+            prevOrCustom,
+            nextCustomAction(),
+            nextCustomAction(),
+            reserveNext,
+            reservePrev
+        )
+    }
+
+    /**
+     * Create a [MediaAction] for a given action and media session
+     *
+     * @param controller MediaController for the session
+     * @param stateActions The actions included with the session's [PlaybackState]
+     * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+     * ```
+     *      [PlaybackState.ACTION_PLAY]
+     *      [PlaybackState.ACTION_PAUSE]
+     *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
+     *      [PlaybackState.ACTION_SKIP_TO_NEXT]
+     * @return
+     * ```
+     * A [MediaAction] with correct values set, or null if the state doesn't support it
+     */
+    private fun getStandardAction(
+        controller: MediaController,
+        stateActions: Long,
+        @PlaybackState.Actions action: Long
+    ): MediaAction? {
+        if (!includesAction(stateActions, action)) {
+            return null
+        }
+
+        return when (action) {
+            PlaybackState.ACTION_PLAY -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_play),
+                    { controller.transportControls.play() },
+                    context.getString(R.string.controls_media_button_play),
+                    context.getDrawable(R.drawable.ic_media_play_container)
+                )
+            }
+            PlaybackState.ACTION_PAUSE -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_pause),
+                    { controller.transportControls.pause() },
+                    context.getString(R.string.controls_media_button_pause),
+                    context.getDrawable(R.drawable.ic_media_pause_container)
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_prev),
+                    { controller.transportControls.skipToPrevious() },
+                    context.getString(R.string.controls_media_button_prev),
+                    null
+                )
+            }
+            PlaybackState.ACTION_SKIP_TO_NEXT -> {
+                MediaAction(
+                    context.getDrawable(R.drawable.ic_media_next),
+                    { controller.transportControls.skipToNext() },
+                    context.getString(R.string.controls_media_button_next),
+                    null
+                )
+            }
+            else -> null
+        }
+    }
+
+    /** Check whether the actions from a [PlaybackState] include a specific action */
+    private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
+        if (
+            (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+        ) {
+            return true
+        }
+        return (stateActions and action != 0L)
+    }
+
+    /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
+    private fun getCustomAction(
+        state: PlaybackState,
+        packageName: String,
+        controller: MediaController,
+        customAction: PlaybackState.CustomAction
+    ): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
+            { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
+            customAction.name,
+            null
+        )
+    }
+
+    /** Load a bitmap from the various Art metadata URIs */
+    private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
+        for (uri in ART_URIS) {
+            val uriString = metadata.getString(uri)
+            if (!TextUtils.isEmpty(uriString)) {
+                val albumArt = loadBitmapFromUri(Uri.parse(uriString))
+                if (albumArt != null) {
+                    if (DEBUG) Log.d(TAG, "loaded art from $uri")
+                    return albumArt
+                }
+            }
+        }
+        return null
+    }
+
+    private fun sendPendingIntent(intent: PendingIntent): Boolean {
+        return try {
+            intent.send()
+            true
+        } catch (e: PendingIntent.CanceledException) {
+            Log.d(TAG, "Intent canceled", e)
+            false
+        }
+    }
+    /**
+     * Load a bitmap from a URI
+     * @param uri the uri to load
+     * @return bitmap, or null if couldn't be loaded
+     */
+    private fun loadBitmapFromUri(uri: Uri): Bitmap? {
+        // ImageDecoder requires a scheme of the following types
+        if (uri.scheme == null) {
+            return null
+        }
+
+        if (
+            !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
+                !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+        ) {
+            return null
+        }
+
+        val source = ImageDecoder.createSource(context.getContentResolver(), uri)
+        return try {
+            ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            }
+        } catch (e: IOException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        } catch (e: RuntimeException) {
+            Log.e(TAG, "Unable to load bitmap", e)
+            null
+        }
+    }
+
+    private fun getResumeMediaAction(action: Runnable): MediaAction {
+        return MediaAction(
+            Icon.createWithResource(context, R.drawable.ic_media_play)
+                .setTint(themeText)
+                .loadDrawable(context),
+            action,
+            context.getString(R.string.controls_media_resume),
+            context.getDrawable(R.drawable.ic_media_play_container)
+        )
+    }
+
+    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+        traceSection("MediaDataManager#onMediaDataLoaded") {
+            Assert.isMainThread()
+            if (mediaEntries.containsKey(key)) {
+                // Otherwise this was removed already
+                mediaEntries.put(key, data)
+                notifyMediaDataLoaded(key, oldKey, data)
+            }
+        }
+
+    override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
+        if (!allowMediaRecommendations) {
+            if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.")
+            return
+        }
+
+        val mediaTargets = targets.filterIsInstance<SmartspaceTarget>()
+        when (mediaTargets.size) {
+            0 -> {
+                if (!smartspaceMediaData.isActive) {
+                    return
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "Set Smartspace media to be inactive for the data update")
+                }
+                smartspaceMediaData =
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = smartspaceMediaData.targetId,
+                        instanceId = smartspaceMediaData.instanceId
+                    )
+                notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
+            }
+            1 -> {
+                val newMediaTarget = mediaTargets.get(0)
+                if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) {
+                    // The same Smartspace updates can be received. Skip the duplicate updates.
+                    return
+                }
+                if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
+                smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
+                notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
+            }
+            else -> {
+                // There should NOT be more than 1 Smartspace media update. When it happens, it
+                // indicates a bad state or an error. Reset the status accordingly.
+                Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
+                notifySmartspaceMediaDataRemoved(
+                    smartspaceMediaData.targetId,
+                    false /* immediately */
+                )
+                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
+            }
+        }
+    }
+
+    fun onNotificationRemoved(key: String) {
+        Assert.isMainThread()
+        val removed = mediaEntries.remove(key)
+        if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) {
+            Log.d(TAG, "Not removing $key because resumable")
+            // Move to resume key (aka package name) if that key doesn't already exist.
+            val resumeAction = getResumeMediaAction(removed.resumeAction!!)
+            val updated =
+                removed.copy(
+                    token = null,
+                    actions = listOf(resumeAction),
+                    semanticActions = MediaButton(playOrPause = resumeAction),
+                    actionsToShowInCompact = listOf(0),
+                    active = false,
+                    resumption = true,
+                    isPlaying = false,
+                    isClearable = true
+                )
+            val pkg = removed.packageName
+            val migrate = mediaEntries.put(pkg, updated) == null
+            // Notify listeners of "new" controls when migrating or removed and update when not
+            if (migrate) {
+                notifyMediaDataLoaded(pkg, key, updated)
+            } else {
+                // Since packageName is used for the key of the resumption controls, it is
+                // possible that another notification has already been reused for the resumption
+                // controls of this package. In this case, rather than renaming this player as
+                // packageName, just remove it and then send a update to the existing resumption
+                // controls.
+                notifyMediaDataRemoved(key)
+                notifyMediaDataLoaded(pkg, pkg, updated)
+            }
+            logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+            return
+        }
+        if (removed != null) {
+            notifyMediaDataRemoved(key)
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        }
+    }
+
+    fun setMediaResumptionEnabled(isEnabled: Boolean) {
+        if (useMediaResumption == isEnabled) {
+            return
+        }
+
+        useMediaResumption = isEnabled
+
+        if (!useMediaResumption) {
+            // Remove any existing resume controls
+            val filtered = mediaEntries.filter { !it.value.active }
+            filtered.forEach {
+                mediaEntries.remove(it.key)
+                notifyMediaDataRemoved(it.key)
+                logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId)
+            }
+        }
+    }
+
+    /** Invoked when the user has dismissed the media carousel */
+    fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+
+    /** Are there any media notifications active, including the recommendations? */
+    fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
+
+    /**
+     * Are there any media entries we should display, including the recommendations?
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
+     */
+    fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
+
+    /** Are there any resume media notifications active, excluding the recommendations? */
+    fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+
+    /**
+     * Are there any resume media notifications active, excluding the recommendations?
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
+     */
+    fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+
+    interface Listener {
+
+        /**
+         * Called whenever there's new MediaData Loaded for the consumption in views.
+         *
+         * oldKey is provided to check whether the view has changed keys, which can happen when a
+         * player has gone from resume state (key is package name) to active state (key is
+         * notification key) or vice versa.
+         *
+         * @param immediately indicates should apply the UI changes immediately, otherwise wait
+         * until the next refresh-round before UI becomes visible. True by default to take in place
+         * immediately.
+         *
+         * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI
+         * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace
+         * signal.
+         *
+         * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+         * recommendation signal
+         */
+        fun onMediaDataLoaded(
+            key: String,
+            oldKey: String?,
+            data: MediaData,
+            immediately: Boolean = true,
+            receivedSmartspaceCardLatency: Int = 0,
+            isSsReactivated: Boolean = false
+        ) {}
+
+        /**
+         * Called whenever there's new Smartspace media data loaded.
+         *
+         * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true,
+         * it will be prioritized as the first card. Otherwise, it will show up as the last card as
+         * default.
+         */
+        fun onSmartspaceMediaDataLoaded(
+            key: String,
+            data: SmartspaceMediaData,
+            shouldPrioritize: Boolean = false
+        ) {}
+
+        /** Called whenever a previously existing Media notification was removed. */
+        fun onMediaDataRemoved(key: String) {}
+
+        /**
+         * Called whenever a previously existing Smartspace media data was removed.
+         *
+         * @param immediately indicates should apply the UI changes immediately, otherwise wait
+         * until the next refresh-round before UI becomes visible. True by default to take in place
+         * immediately.
+         */
+        fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+    }
+
+    /**
+     * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
+     *
+     * @return An empty SmartspaceMediaData with the valid target Id is returned if the
+     * SmartspaceTarget's data is invalid.
+     */
+    private fun toSmartspaceMediaData(
+        target: SmartspaceTarget,
+        isActive: Boolean
+    ): SmartspaceMediaData {
+        var dismissIntent: Intent? = null
+        if (target.baseAction != null && target.baseAction.extras != null) {
+            dismissIntent =
+                target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
+                    as Intent?
+        }
+        packageName(target)?.let {
+            return SmartspaceMediaData(
+                targetId = target.smartspaceTargetId,
+                isActive = isActive,
+                packageName = it,
+                cardAction = target.baseAction,
+                recommendations = target.iconGrid,
+                dismissIntent = dismissIntent,
+                headphoneConnectionTimeMillis = target.creationTimeMillis,
+                instanceId = logger.getNewInstanceId()
+            )
+        }
+        return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+            targetId = target.smartspaceTargetId,
+            isActive = isActive,
+            dismissIntent = dismissIntent,
+            headphoneConnectionTimeMillis = target.creationTimeMillis,
+            instanceId = logger.getNewInstanceId()
+        )
+    }
+
+    private fun packageName(target: SmartspaceTarget): String? {
+        val recommendationList = target.iconGrid
+        if (recommendationList == null || recommendationList.isEmpty()) {
+            Log.w(TAG, "Empty or null media recommendation list.")
+            return null
+        }
+        for (recommendation in recommendationList) {
+            val extras = recommendation.extras
+            extras?.let {
+                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+                    return packageName
+                }
+            }
+        }
+        Log.w(TAG, "No valid package name is provided.")
+        return null
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("internalListeners: $internalListeners")
+            println("externalListeners: ${mediaDataFilter.listeners}")
+            println("mediaEntries: $mediaEntries")
+            println("useMediaResumption: $useMediaResumption")
+            println("allowMediaRecommendations: $allowMediaRecommendations")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
new file mode 100644
index 0000000..6a512be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.bluetooth.BluetoothLeBroadcast
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.media.MediaRouter2Manager
+import android.media.session.MediaController
+import android.text.TextUtils
+import android.util.Log
+import androidx.annotation.AnyThread
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.media.LocalMediaManager
+import com.android.settingslib.media.MediaDevice
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
+import com.android.systemui.statusbar.policy.ConfigurationController
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val PLAYBACK_TYPE_UNKNOWN = 0
+private const val TAG = "MediaDeviceManager"
+private const val DEBUG = true
+
+/** Provides information about the route (ie. device) where playback is occurring. */
+class MediaDeviceManager
+@Inject
+constructor(
+    private val context: Context,
+    private val controllerFactory: MediaControllerFactory,
+    private val localMediaManagerFactory: LocalMediaManagerFactory,
+    private val mr2manager: MediaRouter2Manager,
+    private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
+    private val configurationController: ConfigurationController,
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Main private val fgExecutor: Executor,
+    @Background private val bgExecutor: Executor,
+    dumpManager: DumpManager
+) : MediaDataManager.Listener, Dumpable {
+
+    private val listeners: MutableSet<Listener> = mutableSetOf()
+    private val entries: MutableMap<String, Entry> = mutableMapOf()
+
+    init {
+        dumpManager.registerDumpable(javaClass.name, this)
+    }
+
+    /** Add a listener for changes to the media route (ie. device). */
+    fun addListener(listener: Listener) = listeners.add(listener)
+
+    /** Remove a listener that has been registered with addListener. */
+    fun removeListener(listener: Listener) = listeners.remove(listener)
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        if (oldKey != null && oldKey != key) {
+            val oldEntry = entries.remove(oldKey)
+            oldEntry?.stop()
+        }
+        var entry = entries[key]
+        if (entry == null || entry.token != data.token) {
+            entry?.stop()
+            if (data.device != null) {
+                // If we were already provided device info (e.g. from RCN), keep that and don't
+                // listen for updates, but process once to push updates to listeners
+                processDevice(key, oldKey, data.device)
+                return
+            }
+            val controller = data.token?.let { controllerFactory.create(it) }
+            val localMediaManager = localMediaManagerFactory.create(data.packageName)
+            val muteAwaitConnectionManager =
+                muteAwaitConnectionManagerFactory.create(localMediaManager)
+            entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
+            entries[key] = entry
+            entry.start()
+        }
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        val token = entries.remove(key)
+        token?.stop()
+        token?.let { listeners.forEach { it.onKeyRemoved(key) } }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<String>) {
+        with(pw) {
+            println("MediaDeviceManager state:")
+            entries.forEach { (key, entry) ->
+                println("  key=$key")
+                entry.dump(pw)
+            }
+        }
+    }
+
+    @MainThread
+    private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
+        listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
+    }
+
+    interface Listener {
+        /** Called when the route has changed for a given notification. */
+        fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
+        /** Called when the notification was removed. */
+        fun onKeyRemoved(key: String)
+    }
+
+    private inner class Entry(
+        val key: String,
+        val oldKey: String?,
+        val controller: MediaController?,
+        val localMediaManager: LocalMediaManager,
+        val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
+    ) :
+        LocalMediaManager.DeviceCallback,
+        MediaController.Callback(),
+        BluetoothLeBroadcast.Callback {
+
+        val token
+            get() = controller?.sessionToken
+        private var started = false
+        private var playbackType = PLAYBACK_TYPE_UNKNOWN
+        private var current: MediaDeviceData? = null
+            set(value) {
+                val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
+                if (!started || !sameWithoutIcon) {
+                    field = value
+                    fgExecutor.execute { processDevice(key, oldKey, value) }
+                }
+            }
+        // A device that is not yet connected but is expected to connect imminently. Because it's
+        // expected to connect imminently, it should be displayed as the current device.
+        private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
+        private var broadcastDescription: String? = null
+        private val configListener =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onLocaleListChanged() {
+                    updateCurrent()
+                }
+            }
+
+        @AnyThread
+        fun start() =
+            bgExecutor.execute {
+                if (!started) {
+                    localMediaManager.registerCallback(this)
+                    localMediaManager.startScan()
+                    muteAwaitConnectionManager?.startListening()
+                    playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
+                    controller?.registerCallback(this)
+                    updateCurrent()
+                    started = true
+                    configurationController.addCallback(configListener)
+                }
+            }
+
+        @AnyThread
+        fun stop() =
+            bgExecutor.execute {
+                if (started) {
+                    started = false
+                    controller?.unregisterCallback(this)
+                    localMediaManager.stopScan()
+                    localMediaManager.unregisterCallback(this)
+                    muteAwaitConnectionManager?.stopListening()
+                    configurationController.removeCallback(configListener)
+                }
+            }
+
+        fun dump(pw: PrintWriter) {
+            val routingSession =
+                controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
+            val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
+            with(pw) {
+                println("    current device is ${current?.name}")
+                val type = controller?.playbackInfo?.playbackType
+                println("    PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType")
+                println("    routingSession=$routingSession")
+                println("    selectedRoutes=$selectedRoutes")
+            }
+        }
+
+        @WorkerThread
+        override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
+            val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
+            if (newPlaybackType == playbackType) {
+                return
+            }
+            playbackType = newPlaybackType
+            updateCurrent()
+        }
+
+        override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
+            bgExecutor.execute { updateCurrent() }
+
+        override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
+            bgExecutor.execute { updateCurrent() }
+        }
+
+        override fun onAboutToConnectDeviceAdded(
+            deviceAddress: String,
+            deviceName: String,
+            deviceIcon: Drawable?
+        ) {
+            aboutToConnectDeviceOverride =
+                AboutToConnectDevice(
+                    fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
+                    backupMediaDeviceData =
+                        MediaDeviceData(
+                            /* enabled */ enabled = true,
+                            /* icon */ deviceIcon,
+                            /* name */ deviceName,
+                            /* showBroadcastButton */ showBroadcastButton = false
+                        )
+                )
+            updateCurrent()
+        }
+
+        override fun onAboutToConnectDeviceRemoved() {
+            aboutToConnectDeviceOverride = null
+            updateCurrent()
+        }
+
+        override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
+            if (DEBUG) {
+                Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
+            }
+            updateCurrent()
+        }
+
+        override fun onBroadcastStartFailed(reason: Int) {
+            if (DEBUG) {
+                Log.d(TAG, "onBroadcastStartFailed(), reason = $reason")
+            }
+        }
+
+        override fun onBroadcastMetadataChanged(
+            broadcastId: Int,
+            metadata: BluetoothLeBroadcastMetadata
+        ) {
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
+                        "metadata = $metadata"
+                )
+            }
+            updateCurrent()
+        }
+
+        override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
+            if (DEBUG) {
+                Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
+            }
+            updateCurrent()
+        }
+
+        override fun onBroadcastStopFailed(reason: Int) {
+            if (DEBUG) {
+                Log.d(TAG, "onBroadcastStopFailed(), reason = $reason")
+            }
+        }
+
+        override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {
+            if (DEBUG) {
+                Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId")
+            }
+            updateCurrent()
+        }
+
+        override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
+                )
+            }
+        }
+
+        override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
+
+        override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
+
+        @WorkerThread
+        private fun updateCurrent() {
+            if (isLeAudioBroadcastEnabled()) {
+                current =
+                    MediaDeviceData(
+                        /* enabled */ true,
+                        /* icon */ context.getDrawable(R.drawable.settings_input_antenna),
+                        /* name */ broadcastDescription,
+                        /* intent */ null,
+                        /* showBroadcastButton */ showBroadcastButton = true
+                    )
+            } else {
+                val aboutToConnect = aboutToConnectDeviceOverride
+                if (
+                    aboutToConnect != null &&
+                        aboutToConnect.fullMediaDevice == null &&
+                        aboutToConnect.backupMediaDeviceData != null
+                ) {
+                    // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
+                    current = aboutToConnect.backupMediaDeviceData
+                    return
+                }
+                val device =
+                    aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
+                val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
+
+                // If we have a controller but get a null route, then don't trust the device
+                val enabled = device != null && (controller == null || route != null)
+                val name =
+                    if (controller == null || route != null) {
+                        route?.name?.toString() ?: device?.name
+                    } else {
+                        null
+                    }
+                current =
+                    MediaDeviceData(
+                        enabled,
+                        device?.iconWithoutBackground,
+                        name,
+                        id = device?.id,
+                        showBroadcastButton = false
+                    )
+            }
+        }
+
+        private fun isLeAudioBroadcastEnabled(): Boolean {
+            if (localBluetoothManager != null) {
+                val profileManager = localBluetoothManager.profileManager
+                if (profileManager != null) {
+                    val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile
+                    if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) {
+                        getBroadcastingInfo(bluetoothLeBroadcast)
+                        return true
+                    } else if (DEBUG) {
+                        Log.d(TAG, "Can not get LocalBluetoothLeBroadcast")
+                    }
+                } else if (DEBUG) {
+                    Log.d(TAG, "Can not get LocalBluetoothProfileManager")
+                }
+            } else if (DEBUG) {
+                Log.d(TAG, "Can not get LocalBluetoothManager")
+            }
+            return false
+        }
+
+        private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
+            var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
+            // TODO(b/233698402): Use the package name instead of app label to avoid the
+            // unexpected result.
+            // Check the current media app's name is the same with current broadcast app's name
+            // or not.
+            var mediaApp =
+                MediaDataUtils.getAppLabel(
+                    context,
+                    localMediaManager.packageName,
+                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+                )
+            var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
+            if (isCurrentBroadcastedApp) {
+                broadcastDescription =
+                    context.getString(R.string.broadcasting_description_is_broadcasting)
+            } else {
+                broadcastDescription = currentBroadcastedApp
+            }
+        }
+    }
+}
+
+/**
+ * A class storing information for the about-to-connect device. See
+ * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
+ *
+ * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
+ * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
+ * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
+ * information required to display the device. Only use if [fullMediaDevice] is null.
+ */
+private data class AboutToConnectDevice(
+    val fullMediaDevice: MediaDevice? = null,
+    val backupMediaDeviceData: MediaDeviceData? = null
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
new file mode 100644
index 0000000..ab93b29
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.content.ComponentName
+import android.content.Context
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "MediaSessionBasedFilter"
+
+/**
+ * Filters media loaded events for local media sessions while an app is casting.
+ *
+ * When an app is casting there can be one remote media sessions and potentially more local media
+ * sessions. In this situation, there should only be a media object for the remote session. To
+ * achieve this, update events for the local session need to be filtered.
+ */
+class MediaSessionBasedFilter
+@Inject
+constructor(
+    context: Context,
+    private val sessionManager: MediaSessionManager,
+    @Main private val foregroundExecutor: Executor,
+    @Background private val backgroundExecutor: Executor
+) : MediaDataManager.Listener {
+
+    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+
+    // Keep track of MediaControllers for a given package to check if an app is casting and it
+    // filter loaded events for local sessions.
+    private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
+        LinkedHashMap()
+
+    // Keep track of the key used for the session tokens. This information is used to know when to
+    // dispatch a removed event so that a media object for a local session will be removed.
+    private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
+
+    // Keep track of which media session tokens have associated notifications.
+    private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
+
+    private val sessionListener =
+        object : MediaSessionManager.OnActiveSessionsChangedListener {
+            override fun onActiveSessionsChanged(controllers: List<MediaController>) {
+                handleControllersChanged(controllers)
+            }
+        }
+
+    init {
+        backgroundExecutor.execute {
+            val name = ComponentName(context, NotificationListenerWithPlugins::class.java)
+            sessionManager.addOnActiveSessionsChangedListener(sessionListener, name)
+            handleControllersChanged(sessionManager.getActiveSessions(name))
+        }
+    }
+
+    /** Add a listener for filtered [MediaData] changes */
+    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
+
+    /** Remove a listener that was registered with addListener */
+    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
+
+    /**
+     * May filter loaded events by not passing them along to listeners.
+     *
+     * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that
+     * the app is casting. Sometimes apps will send redundant updates to a local session with
+     * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability
+     * of the media controls.
+     */
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        backgroundExecutor.execute {
+            data.token?.let { tokensWithNotifications.add(it) }
+            val isMigration = oldKey != null && key != oldKey
+            if (isMigration) {
+                keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
+            }
+            if (data.token != null) {
+                keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+                    ?: run {
+                        val tokens = mutableSetOf(data.token)
+                        keyedTokens.put(key, tokens)
+                    }
+            }
+            // Determine if an app is casting by checking if it has a session with playback type
+            // PLAYBACK_TYPE_REMOTE.
+            val remoteControllers =
+                packageControllers.get(data.packageName)?.filter {
+                    it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+                }
+            // Limiting search to only apps with a single remote session.
+            val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
+            if (
+                isMigration ||
+                    remote == null ||
+                    remote.sessionToken == data.token ||
+                    !tokensWithNotifications.contains(remote.sessionToken)
+            ) {
+                // Not filtering in this case. Passing the event along to listeners.
+                dispatchMediaDataLoaded(key, oldKey, data, immediately)
+            } else {
+                // Filtering this event because the app is casting and the loaded events is for a
+                // local session.
+                Log.d(TAG, "filtering key=$key local=${data.token} remote=${remote?.sessionToken}")
+                // If the local session uses a different notification key, then lets go a step
+                // farther and dismiss the media data so that media controls for the local session
+                // don't hang around while casting.
+                if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+                    dispatchMediaDataRemoved(key)
+                }
+            }
+        }
+    }
+
+    override fun onSmartspaceMediaDataLoaded(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) {
+        backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        // Queue on background thread to ensure ordering of loaded and removed events is maintained.
+        backgroundExecutor.execute {
+            keyedTokens.remove(key)
+            dispatchMediaDataRemoved(key)
+        }
+    }
+
+    override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
+    }
+
+    private fun dispatchMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        info: MediaData,
+        immediately: Boolean
+    ) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) }
+        }
+    }
+
+    private fun dispatchMediaDataRemoved(key: String) {
+        foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } }
+    }
+
+    private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) }
+        }
+    }
+
+    private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
+        }
+    }
+
+    private fun handleControllersChanged(controllers: List<MediaController>) {
+        packageControllers.clear()
+        controllers.forEach { controller ->
+            packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) }
+                ?: run {
+                    val tokens = mutableListOf(controller)
+                    packageControllers.put(controller.packageName, tokens)
+                }
+        }
+        tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
new file mode 100644
index 0000000..7f5c82f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.os.SystemProperties
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@VisibleForTesting
+val PAUSED_MEDIA_TIMEOUT =
+    SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
+
+@VisibleForTesting
+val RESUME_MEDIA_TIMEOUT =
+    SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
+
+/** Controller responsible for keeping track of playback states and expiring inactive streams. */
+@SysUISingleton
+class MediaTimeoutListener
+@Inject
+constructor(
+    private val mediaControllerFactory: MediaControllerFactory,
+    @Main private val mainExecutor: DelayableExecutor,
+    private val logger: MediaTimeoutLogger,
+    statusBarStateController: SysuiStatusBarStateController,
+    private val systemClock: SystemClock
+) : MediaDataManager.Listener {
+
+    private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
+
+    /**
+     * Callback representing that a media object is now expired:
+     * @param key Media control unique identifier
+     * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
+     * ```
+     *                 or {@code RESUME_MEDIA_TIMEOUT} for resume media
+     * ```
+     */
+    lateinit var timeoutCallback: (String, Boolean) -> Unit
+
+    /**
+     * Callback representing that a media object [PlaybackState] has changed.
+     * @param key Media control unique identifier
+     * @param state The new [PlaybackState]
+     */
+    lateinit var stateCallback: (String, PlaybackState) -> Unit
+
+    init {
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onDozingChanged(isDozing: Boolean) {
+                    if (!isDozing) {
+                        // Check whether any timeouts should have expired
+                        mediaListeners.forEach { (key, listener) ->
+                            if (
+                                listener.cancellation != null &&
+                                    listener.expiration <= systemClock.elapsedRealtime()
+                            ) {
+                                // We dozed too long - timeout now, and cancel the pending one
+                                listener.expireMediaTimeout(key, "timeout happened while dozing")
+                                listener.doTimeout()
+                            }
+                        }
+                    }
+                }
+            }
+        )
+    }
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        var reusedListener: PlaybackStateListener? = null
+
+        // First check if we already have a listener
+        mediaListeners.get(key)?.let {
+            if (!it.destroyed) {
+                return
+            }
+
+            // If listener was destroyed previously, we'll need to re-register it
+            logger.logReuseListener(key)
+            reusedListener = it
+        }
+
+        // Having an old key means that we're migrating from/to resumption. We should update
+        // the old listener to make sure that events will be dispatched to the new location.
+        val migrating = oldKey != null && key != oldKey
+        if (migrating) {
+            reusedListener = mediaListeners.remove(oldKey)
+            logger.logMigrateListener(oldKey, key, reusedListener != null)
+        }
+
+        reusedListener?.let {
+            val wasPlaying = it.isPlaying()
+            logger.logUpdateListener(key, wasPlaying)
+            it.mediaData = data
+            it.key = key
+            mediaListeners[key] = it
+            if (wasPlaying != it.isPlaying()) {
+                // If a player becomes active because of a migration, we'll need to broadcast
+                // its state. Doing it now would lead to reentrant callbacks, so let's wait
+                // until we're done.
+                mainExecutor.execute {
+                    if (mediaListeners[key]?.isPlaying() == true) {
+                        logger.logDelayedUpdate(key)
+                        timeoutCallback.invoke(key, false /* timedOut */)
+                    }
+                }
+            }
+            return
+        }
+
+        mediaListeners[key] = PlaybackStateListener(key, data)
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        mediaListeners.remove(key)?.destroy()
+    }
+
+    fun isTimedOut(key: String): Boolean {
+        return mediaListeners[key]?.timedOut ?: false
+    }
+
+    private inner class PlaybackStateListener(var key: String, data: MediaData) :
+        MediaController.Callback() {
+
+        var timedOut = false
+        var lastState: PlaybackState? = null
+        var resumption: Boolean? = null
+        var destroyed = false
+        var expiration = Long.MAX_VALUE
+
+        var mediaData: MediaData = data
+            set(value) {
+                destroyed = false
+                mediaController?.unregisterCallback(this)
+                field = value
+                val token = field.token
+                mediaController =
+                    if (token != null) {
+                        mediaControllerFactory.create(token)
+                    } else {
+                        null
+                    }
+                mediaController?.registerCallback(this)
+                // Let's register the cancellations, but not dispatch events now.
+                // Timeouts didn't happen yet and reentrant events are troublesome.
+                processState(mediaController?.playbackState, dispatchEvents = false)
+            }
+
+        // Resume controls may have null token
+        private var mediaController: MediaController? = null
+        var cancellation: Runnable? = null
+            private set
+
+        fun Int.isPlaying() = isPlayingState(this)
+        fun isPlaying() = lastState?.state?.isPlaying() ?: false
+
+        init {
+            mediaData = data
+        }
+
+        fun destroy() {
+            mediaController?.unregisterCallback(this)
+            cancellation?.run()
+            destroyed = true
+        }
+
+        override fun onPlaybackStateChanged(state: PlaybackState?) {
+            processState(state, dispatchEvents = true)
+        }
+
+        override fun onSessionDestroyed() {
+            logger.logSessionDestroyed(key)
+            if (resumption == true) {
+                // Some apps create a session when MBS is queried. We should unregister the
+                // controller since it will no longer be valid, but don't cancel the timeout
+                mediaController?.unregisterCallback(this)
+            } else {
+                // For active controls, if the session is destroyed, clean up everything since we
+                // will need to recreate it if this key is updated later
+                destroy()
+            }
+        }
+
+        private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
+            logger.logPlaybackState(key, state)
+
+            val playingStateSame = (state?.state?.isPlaying() == isPlaying())
+            val actionsSame =
+                (lastState?.actions == state?.actions) &&
+                    areCustomActionListsEqual(lastState?.customActions, state?.customActions)
+            val resumptionChanged = resumption != mediaData.resumption
+
+            lastState = state
+
+            if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) {
+                logger.logStateCallback(key)
+                stateCallback.invoke(key, state)
+            }
+
+            if (playingStateSame && !resumptionChanged) {
+                return
+            }
+            resumption = mediaData.resumption
+
+            val playing = isPlaying()
+            if (!playing) {
+                logger.logScheduleTimeout(key, playing, resumption!!)
+                if (cancellation != null && !resumptionChanged) {
+                    // if the media changed resume state, we'll need to adjust the timeout length
+                    logger.logCancelIgnored(key)
+                    return
+                }
+                expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
+                val timeout =
+                    if (mediaData.resumption) {
+                        RESUME_MEDIA_TIMEOUT
+                    } else {
+                        PAUSED_MEDIA_TIMEOUT
+                    }
+                expiration = systemClock.elapsedRealtime() + timeout
+                cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
+            } else {
+                expireMediaTimeout(key, "playback started - $state, $key")
+                timedOut = false
+                if (dispatchEvents) {
+                    timeoutCallback(key, timedOut)
+                }
+            }
+        }
+
+        fun doTimeout() {
+            cancellation = null
+            logger.logTimeout(key)
+            timedOut = true
+            expiration = Long.MAX_VALUE
+            // this event is async, so it's safe even when `dispatchEvents` is false
+            timeoutCallback(key, timedOut)
+        }
+
+        fun expireMediaTimeout(mediaKey: String, reason: String) {
+            cancellation?.apply {
+                logger.logTimeoutCancelled(mediaKey, reason)
+                run()
+            }
+            expiration = Long.MAX_VALUE
+            cancellation = null
+        }
+    }
+
+    private fun areCustomActionListsEqual(
+        first: List<PlaybackState.CustomAction>?,
+        second: List<PlaybackState.CustomAction>?
+    ): Boolean {
+        // Same object, or both null
+        if (first === second) {
+            return true
+        }
+
+        // Only one null, or different number of actions
+        if ((first == null || second == null) || (first.size != second.size)) {
+            return false
+        }
+
+        // Compare individual actions
+        first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
+            if (!areCustomActionsEqual(firstAction, secondAction)) {
+                return false
+            }
+        }
+        return true
+    }
+
+    private fun areCustomActionsEqual(
+        firstAction: PlaybackState.CustomAction,
+        secondAction: PlaybackState.CustomAction
+    ): Boolean {
+        if (
+            firstAction.action != secondAction.action ||
+                firstAction.name != secondAction.name ||
+                firstAction.icon != secondAction.icon
+        ) {
+            return false
+        }
+
+        if ((firstAction.extras == null) != (secondAction.extras == null)) {
+            return false
+        }
+        if (firstAction.extras != null) {
+            firstAction.extras.keySet().forEach { key ->
+                if (firstAction.extras.get(key) != secondAction.extras.get(key)) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
new file mode 100644
index 0000000..8f3f054
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.media.controls.pipeline
+
+import android.media.session.PlaybackState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaTimeoutListenerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+private const val TAG = "MediaTimeout"
+
+/** A buffered log for [MediaTimeoutListener] events */
+@SysUISingleton
+class MediaTimeoutLogger
+@Inject
+constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) {
+    fun logReuseListener(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" })
+
+    fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = oldKey
+                str2 = newKey
+                bool1 = hadListener
+            },
+            { "migrate from $str1 to $str2, had listener? $bool1" }
+        )
+
+    fun logUpdateListener(key: String, wasPlaying: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = wasPlaying
+            },
+            { "updating $str1, was playing? $bool1" }
+        )
+
+    fun logDelayedUpdate(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            { "deliver delayed playback state for $str1" }
+        )
+
+    fun logSessionDestroyed(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" })
+
+    fun logPlaybackState(key: String, state: PlaybackState?) =
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = key
+                str2 = state?.toString()
+            },
+            { "state update: key=$str1 state=$str2" }
+        )
+
+    fun logStateCallback(key: String) =
+        buffer.log(TAG, LogLevel.VERBOSE, { str1 = key }, { "dispatching state update for $key" })
+
+    fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = playing
+                bool2 = resumption
+            },
+            { "schedule timeout $str1, playing=$bool1 resumption=$bool2" }
+        )
+
+    fun logCancelIgnored(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" })
+
+    fun logTimeout(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" })
+
+    fun logTimeoutCancelled(key: String, reason: String) =
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = key
+                str2 = reason
+            },
+            { "media timeout cancelled for $str1, reason: $str2" }
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
new file mode 100644
index 0000000..00620b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link MediaBrowser} constructor
+ */
+public class MediaBrowserFactory {
+    private final Context mContext;
+
+    @Inject
+    public MediaBrowserFactory(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Creates a new MediaBrowser
+     *
+     * @param serviceComponent
+     * @param callback
+     * @param rootHints
+     * @return
+     */
+    public MediaBrowser create(ComponentName serviceComponent,
+            MediaBrowser.ConnectionCallback callback, Bundle rootHints) {
+        return new MediaBrowser(mContext, serviceComponent, callback, rootHints);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
new file mode 100644
index 0000000..4891297
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.media.MediaDescription
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.media.MediaBrowserService
+import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.Utils
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "MediaResumeListener"
+
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val MEDIA_PREFERENCE_KEY = "browser_components_"
+
+@SysUISingleton
+class MediaResumeListener
+@Inject
+constructor(
+    private val context: Context,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    @Background private val backgroundExecutor: Executor,
+    private val tunerService: TunerService,
+    private val mediaBrowserFactory: ResumeMediaBrowserFactory,
+    dumpManager: DumpManager,
+    private val systemClock: SystemClock
+) : MediaDataManager.Listener, Dumpable {
+
+    private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
+    private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
+        ConcurrentLinkedQueue()
+
+    private lateinit var mediaDataManager: MediaDataManager
+
+    private var mediaBrowser: ResumeMediaBrowser? = null
+        set(value) {
+            // Always disconnect the old browser -- see b/225403871.
+            field?.disconnect()
+            field = value
+        }
+    private var currentUserId: Int = context.userId
+
+    @VisibleForTesting
+    val userChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                if (Intent.ACTION_USER_UNLOCKED == intent.action) {
+                    loadMediaResumptionControls()
+                } else if (Intent.ACTION_USER_SWITCHED == intent.action) {
+                    currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+                    loadSavedComponents()
+                }
+            }
+        }
+
+    private val mediaBrowserCallback =
+        object : ResumeMediaBrowser.Callback() {
+            override fun addTrack(
+                desc: MediaDescription,
+                component: ComponentName,
+                browser: ResumeMediaBrowser
+            ) {
+                val token = browser.token
+                val appIntent = browser.appIntent
+                val pm = context.getPackageManager()
+                var appName: CharSequence = component.packageName
+                val resumeAction = getResumeAction(component)
+                try {
+                    appName =
+                        pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0))
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.e(TAG, "Error getting package information", e)
+                }
+
+                Log.d(TAG, "Adding resume controls $desc")
+                mediaDataManager.addResumptionControls(
+                    currentUserId,
+                    desc,
+                    resumeAction,
+                    token,
+                    appName.toString(),
+                    appIntent,
+                    component.packageName
+                )
+            }
+        }
+
+    init {
+        if (useMediaResumption) {
+            dumpManager.registerDumpable(TAG, this)
+            val unlockFilter = IntentFilter()
+            unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
+            unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
+            broadcastDispatcher.registerReceiver(
+                userChangeReceiver,
+                unlockFilter,
+                null,
+                UserHandle.ALL
+            )
+            loadSavedComponents()
+        }
+    }
+
+    fun setManager(manager: MediaDataManager) {
+        mediaDataManager = manager
+
+        // Add listener for resumption setting changes
+        tunerService.addTunable(
+            object : TunerService.Tunable {
+                override fun onTuningChanged(key: String?, newValue: String?) {
+                    useMediaResumption = Utils.useMediaResumption(context)
+                    mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
+                }
+            },
+            Settings.Secure.MEDIA_CONTROLS_RESUME
+        )
+    }
+
+    private fun loadSavedComponents() {
+        // Make sure list is empty (if we switched users)
+        resumeComponents.clear()
+        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+        val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
+        val components =
+            listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile {
+                it.isEmpty()
+            }
+        var needsUpdate = false
+        components?.forEach {
+            val info = it.split("/")
+            val packageName = info[0]
+            val className = info[1]
+            val component = ComponentName(packageName, className)
+
+            val lastPlayed =
+                if (info.size == 3) {
+                    try {
+                        info[2].toLong()
+                    } catch (e: NumberFormatException) {
+                        needsUpdate = true
+                        systemClock.currentTimeMillis()
+                    }
+                } else {
+                    needsUpdate = true
+                    systemClock.currentTimeMillis()
+                }
+            resumeComponents.add(component to lastPlayed)
+        }
+        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
+
+        if (needsUpdate) {
+            // Save any missing times that we had to fill in
+            writeSharedPrefs()
+        }
+    }
+
+    /** Load controls for resuming media, if available */
+    private fun loadMediaResumptionControls() {
+        if (!useMediaResumption) {
+            return
+        }
+
+        val now = systemClock.currentTimeMillis()
+        resumeComponents.forEach {
+            if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) {
+                val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first)
+                browser.findRecentMedia()
+            }
+        }
+    }
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        if (useMediaResumption) {
+            // If this had been started from a resume state, disconnect now that it's live
+            if (!key.equals(oldKey)) {
+                mediaBrowser = null
+            }
+            // If we don't have a resume action, check if we haven't already
+            if (data.resumeAction == null && !data.hasCheckedForResume && data.isLocalSession()) {
+                // TODO also check for a media button receiver intended for restarting (b/154127084)
+                Log.d(TAG, "Checking for service component for " + data.packageName)
+                val pm = context.packageManager
+                val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+                val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
+
+                val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
+                if (inf != null && inf.size > 0) {
+                    backgroundExecutor.execute {
+                        tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
+                    }
+                } else {
+                    // No service found
+                    mediaDataManager.setResumeAction(key, null)
+                }
+            }
+        }
+    }
+
+    /**
+     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
+     * component to the list of resumption components
+     */
+    private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
+        Log.d(TAG, "Testing if we can connect to $componentName")
+        // Set null action to prevent additional attempts to connect
+        mediaDataManager.setResumeAction(key, null)
+        mediaBrowser =
+            mediaBrowserFactory.create(
+                object : ResumeMediaBrowser.Callback() {
+                    override fun onConnected() {
+                        Log.d(TAG, "Connected to $componentName")
+                    }
+
+                    override fun onError() {
+                        Log.e(TAG, "Cannot resume with $componentName")
+                        mediaBrowser = null
+                    }
+
+                    override fun addTrack(
+                        desc: MediaDescription,
+                        component: ComponentName,
+                        browser: ResumeMediaBrowser
+                    ) {
+                        // Since this is a test, just save the component for later
+                        Log.d(TAG, "Can get resumable media from $componentName")
+                        mediaDataManager.setResumeAction(key, getResumeAction(componentName))
+                        updateResumptionList(componentName)
+                        mediaBrowser = null
+                    }
+                },
+                componentName
+            )
+        mediaBrowser?.testConnection()
+    }
+
+    /**
+     * Add the component to the saved list of media browser services, checking for duplicates and
+     * removing older components that exceed the maximum limit
+     * @param componentName
+     */
+    private fun updateResumptionList(componentName: ComponentName) {
+        // Remove if exists
+        resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) })
+        // Insert at front of queue
+        val currentTime = systemClock.currentTimeMillis()
+        resumeComponents.add(componentName to currentTime)
+        // Remove old components if over the limit
+        if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            resumeComponents.remove()
+        }
+
+        writeSharedPrefs()
+    }
+
+    private fun writeSharedPrefs() {
+        val sb = StringBuilder()
+        resumeComponents.forEach {
+            sb.append(it.first.flattenToString())
+            sb.append("/")
+            sb.append(it.second)
+            sb.append(ResumeMediaBrowser.DELIMITER)
+        }
+        val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+        prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
+    }
+
+    /** Get a runnable which will resume media playback */
+    private fun getResumeAction(componentName: ComponentName): Runnable {
+        return Runnable {
+            mediaBrowser = mediaBrowserFactory.create(null, componentName)
+            mediaBrowser?.restart()
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply { println("resumeComponents: $resumeComponents") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
new file mode 100644
index 0000000..3493b24
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.MediaDescription;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * Media browser for managing resumption in media controls
+ */
+public class ResumeMediaBrowser {
+
+    /** Maximum number of controls to show on boot */
+    public static final int MAX_RESUMPTION_CONTROLS = 5;
+
+    /** Delimiter for saved component names */
+    public static final String DELIMITER = ":";
+
+    private static final String TAG = "ResumeMediaBrowser";
+    private final Context mContext;
+    @Nullable private final Callback mCallback;
+    private final MediaBrowserFactory mBrowserFactory;
+    private final ResumeMediaBrowserLogger mLogger;
+    private final ComponentName mComponentName;
+    private final MediaController.Callback mMediaControllerCallback = new SessionDestroyCallback();
+
+    private MediaBrowser mMediaBrowser;
+    @Nullable private MediaController mMediaController;
+
+    /**
+     * Initialize a new media browser
+     * @param context the context
+     * @param callback used to report media items found
+     * @param componentName Component name of the MediaBrowserService this browser will connect to
+     */
+    public ResumeMediaBrowser(
+            Context context,
+            @Nullable Callback callback,
+            ComponentName componentName,
+            MediaBrowserFactory browserFactory,
+            ResumeMediaBrowserLogger logger) {
+        mContext = context;
+        mCallback = callback;
+        mComponentName = componentName;
+        mBrowserFactory = browserFactory;
+        mLogger = logger;
+    }
+
+    /**
+     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
+     * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
+     * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
+     * called when the initial connection is successful, or an error occurs.
+     * Note that it is possible for the service to connect but for no playable tracks to be found.
+     * ResumeMediaBrowser#disconnect will be called automatically with this function.
+     */
+    public void findRecentMedia() {
+        disconnect();
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = mBrowserFactory.create(
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+        updateMediaController();
+        mLogger.logConnection(mComponentName, "findRecentMedia");
+        mMediaBrowser.connect();
+    }
+
+    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
+            new MediaBrowser.SubscriptionCallback() {
+        @Override
+        public void onChildrenLoaded(String parentId,
+                List<MediaBrowser.MediaItem> children) {
+            if (children.size() == 0) {
+                Log.d(TAG, "No children found for " + mComponentName);
+                if (mCallback != null) {
+                    mCallback.onError();
+                }
+            } else {
+                // We ask apps to return a playable item as the first child when sending
+                // a request with EXTRA_RECENT; if they don't, no resume controls
+                MediaBrowser.MediaItem child = children.get(0);
+                MediaDescription desc = child.getDescription();
+                if (child.isPlayable() && mMediaBrowser != null) {
+                    if (mCallback != null) {
+                        mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
+                                ResumeMediaBrowser.this);
+                    }
+                } else {
+                    Log.d(TAG, "Child found but not playable for " + mComponentName);
+                    if (mCallback != null) {
+                        mCallback.onError();
+                    }
+                }
+            }
+            disconnect();
+        }
+
+        @Override
+        public void onError(String parentId) {
+            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
+            if (mCallback != null) {
+                mCallback.onError();
+            }
+            disconnect();
+        }
+
+        @Override
+        public void onError(String parentId, Bundle options) {
+            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+                    + ", options: " + options);
+            if (mCallback != null) {
+                mCallback.onError();
+            }
+            disconnect();
+        }
+    };
+
+    private final MediaBrowser.ConnectionCallback mConnectionCallback =
+            new MediaBrowser.ConnectionCallback() {
+        /**
+         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+         * For resumption controls, apps are expected to return a playable media item as the first
+         * child. If there are no children or it isn't playable it will be ignored.
+         */
+        @Override
+        public void onConnected() {
+            Log.d(TAG, "Service connected for " + mComponentName);
+            updateMediaController();
+            if (isBrowserConnected()) {
+                String root = mMediaBrowser.getRoot();
+                if (!TextUtils.isEmpty(root)) {
+                    if (mCallback != null) {
+                        mCallback.onConnected();
+                    }
+                    if (mMediaBrowser != null) {
+                        mMediaBrowser.subscribe(root, mSubscriptionCallback);
+                    }
+                    return;
+                }
+            }
+            if (mCallback != null) {
+                mCallback.onError();
+            }
+            disconnect();
+        }
+
+        /**
+         * Invoked when the client is disconnected from the media browser.
+         */
+        @Override
+        public void onConnectionSuspended() {
+            Log.d(TAG, "Connection suspended for " + mComponentName);
+            if (mCallback != null) {
+                mCallback.onError();
+            }
+            disconnect();
+        }
+
+        /**
+         * Invoked when the connection to the media browser failed.
+         */
+        @Override
+        public void onConnectionFailed() {
+            Log.d(TAG, "Connection failed for " + mComponentName);
+            if (mCallback != null) {
+                mCallback.onError();
+            }
+            disconnect();
+        }
+    };
+
+    /**
+     * Disconnect the media browser. This should be done after callbacks have completed to
+     * disconnect from the media browser service.
+     */
+    protected void disconnect() {
+        if (mMediaBrowser != null) {
+            mLogger.logDisconnect(mComponentName);
+            mMediaBrowser.disconnect();
+        }
+        mMediaBrowser = null;
+        updateMediaController();
+    }
+
+    /**
+     * Connects to the MediaBrowserService and starts playback.
+     * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
+     * depending on whether it was successful.
+     * If the connection is successful, the listener should call ResumeMediaBrowser#disconnect after
+     * getting a media update from the app
+     */
+    public void restart() {
+        disconnect();
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = mBrowserFactory.create(mComponentName,
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
+                        updateMediaController();
+                        if (!isBrowserConnected()) {
+                            if (mCallback != null) {
+                                mCallback.onError();
+                            }
+                            disconnect();
+                            return;
+                        }
+                        MediaSession.Token token = mMediaBrowser.getSessionToken();
+                        MediaController controller = createMediaController(token);
+                        controller.getTransportControls();
+                        controller.getTransportControls().prepare();
+                        controller.getTransportControls().play();
+                        if (mCallback != null) {
+                            mCallback.onConnected();
+                        }
+                        // listener should disconnect after media player update
+                    }
+
+                    @Override
+                    public void onConnectionFailed() {
+                        if (mCallback != null) {
+                            mCallback.onError();
+                        }
+                        disconnect();
+                    }
+
+                    @Override
+                    public void onConnectionSuspended() {
+                        if (mCallback != null) {
+                            mCallback.onError();
+                        }
+                        disconnect();
+                    }
+                }, rootHints);
+        updateMediaController();
+        mLogger.logConnection(mComponentName, "restart");
+        mMediaBrowser.connect();
+    }
+
+    @VisibleForTesting
+    protected MediaController createMediaController(MediaSession.Token token) {
+        return new MediaController(mContext, token);
+    }
+
+    /**
+     * Get the media session token
+     * @return the token, or null if the MediaBrowser is null or disconnected
+     */
+    public MediaSession.Token getToken() {
+        if (!isBrowserConnected()) {
+            return null;
+        }
+        return mMediaBrowser.getSessionToken();
+    }
+
+    /**
+     * Get an intent to launch the app associated with this browser service
+     * @return
+     */
+    public PendingIntent getAppIntent() {
+        PackageManager pm = mContext.getPackageManager();
+        Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
+        return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    /**
+     * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
+     * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
+     * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more
+     * detailed logging if the service has issues. If it cannot connect, or cannot find valid media,
+     * then ResumeMediaBrowser.Callback#onError will be called.
+     * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
+     */
+    public void testConnection() {
+        disconnect();
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = mBrowserFactory.create(
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+        updateMediaController();
+        mLogger.logConnection(mComponentName, "testConnection");
+        mMediaBrowser.connect();
+    }
+
+    /** Updates mMediaController based on our current browser values. */
+    private void updateMediaController() {
+        MediaSession.Token controllerToken =
+                mMediaController != null ? mMediaController.getSessionToken() : null;
+        MediaSession.Token currentToken = getToken();
+        boolean areEqual = (controllerToken == null && currentToken == null)
+                || (controllerToken != null && controllerToken.equals(currentToken));
+        if (areEqual) {
+            return;
+        }
+
+        // Whenever the token changes, un-register the callback on the old controller (if we have
+        // one) and create a new controller with the callback attached.
+        if (mMediaController != null) {
+            mMediaController.unregisterCallback(mMediaControllerCallback);
+        }
+        if (currentToken != null) {
+            mMediaController = createMediaController(currentToken);
+            mMediaController.registerCallback(mMediaControllerCallback);
+        } else {
+            mMediaController = null;
+        }
+    }
+
+    private boolean isBrowserConnected() {
+        return mMediaBrowser != null && mMediaBrowser.isConnected();
+    }
+
+    /**
+     * Interface to handle results from ResumeMediaBrowser
+     */
+    public static class Callback {
+        /**
+         * Called when the browser has successfully connected to the service
+         */
+        public void onConnected() {
+        }
+
+        /**
+         * Called when the browser encountered an error connecting to the service
+         */
+        public void onError() {
+        }
+
+        /**
+         * Called when the browser finds a suitable track to add to the media carousel
+         * @param track media info for the item
+         * @param component component of the MediaBrowserService which returned this
+         * @param browser reference to the browser
+         */
+        public void addTrack(MediaDescription track, ComponentName component,
+                ResumeMediaBrowser browser) {
+        }
+    }
+
+    private class SessionDestroyCallback extends MediaController.Callback {
+        @Override
+        public void onSessionDestroyed() {
+            mLogger.logSessionDestroyed(isBrowserConnected(), mComponentName);
+            disconnect();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
new file mode 100644
index 0000000..c558227
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link ResumeMediaBrowser} constructor
+ */
+public class ResumeMediaBrowserFactory {
+    private final Context mContext;
+    private final MediaBrowserFactory mBrowserFactory;
+    private final ResumeMediaBrowserLogger mLogger;
+
+    @Inject
+    public ResumeMediaBrowserFactory(
+            Context context, MediaBrowserFactory browserFactory, ResumeMediaBrowserLogger logger) {
+        mContext = context;
+        mBrowserFactory = browserFactory;
+        mLogger = logger;
+    }
+
+    /**
+     * Creates a new ResumeMediaBrowser.
+     *
+     * @param callback will be called on connection or error, and addTrack when media item found
+     * @param componentName component to browse
+     * @return
+     */
+    public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
+            ComponentName componentName) {
+        return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory, mLogger);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
new file mode 100644
index 0000000..335ce1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.media.controls.resume
+
+import android.content.ComponentName
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaBrowserLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** A logger for events in [ResumeMediaBrowser]. */
+@SysUISingleton
+class ResumeMediaBrowserLogger @Inject constructor(@MediaBrowserLog private val buffer: LogBuffer) {
+    /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */
+    fun logConnection(componentName: ComponentName, reason: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = componentName.toShortString()
+                str2 = reason
+            },
+            { "Connecting browser for component $str1 due to $str2" }
+        )
+
+    /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */
+    fun logDisconnect(componentName: ComponentName) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = componentName.toShortString() },
+            { "Disconnecting browser for component $str1" }
+        )
+
+    /**
+     * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed]
+     * event.
+     *
+     * @param isBrowserConnected true if there's a currently connected
+     * ```
+     *     [android.media.browse.MediaBrowser] and false otherwise.
+     * @param componentName
+     * ```
+     * the component name for the [ResumeMediaBrowser] that triggered this log.
+     */
+    fun logSessionDestroyed(isBrowserConnected: Boolean, componentName: ComponentName) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = isBrowserConnected
+                str1 = componentName.toShortString()
+            },
+            { "Session destroyed. Active browser = $bool1. Browser component = $str1." }
+        )
+}
+
+private const val TAG = "MediaBrowser"
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
new file mode 100644
index 0000000..d2793bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.media.controls.ui
+
+import android.graphics.drawable.Animatable2
+import android.graphics.drawable.Drawable
+
+/**
+ * AnimationBindHandler is responsible for tracking the bound animation state and preventing jank
+ * and conflicts due to media notifications arriving at any time during an animation. It does this
+ * in two parts.
+ * - Exit animations fired as a result of user input are tracked. When these are running, any
+ * ```
+ *      bind actions are delayed until the animation completes (and then fired in sequence).
+ * ```
+ * - Continuous animations are tracked using their rebind id. Later calls using the same
+ * ```
+ *      rebind id will be totally ignored to prevent the continuous animation from restarting.
+ * ```
+ */
+internal class AnimationBindHandler : Animatable2.AnimationCallback() {
+    private val onAnimationsComplete = mutableListOf<() -> Unit>()
+    private val registrations = mutableListOf<Animatable2>()
+    private var rebindId: Int? = null
+
+    val isAnimationRunning: Boolean
+        get() = registrations.any { it.isRunning }
+
+    /**
+     * This check prevents rebinding to the action button if the identifier has not changed. A null
+     * value is always considered to be changed. This is used to prevent the connecting animation
+     * from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by an
+     * application in a row.
+     */
+    fun updateRebindId(newRebindId: Int?): Boolean {
+        if (rebindId == null || newRebindId == null || rebindId != newRebindId) {
+            rebindId = newRebindId
+            return true
+        }
+        return false
+    }
+
+    fun tryRegister(drawable: Drawable?) {
+        if (drawable is Animatable2) {
+            val anim = drawable as Animatable2
+            anim.registerAnimationCallback(this)
+            registrations.add(anim)
+        }
+    }
+
+    fun unregisterAll() {
+        registrations.forEach { it.unregisterAnimationCallback(this) }
+        registrations.clear()
+    }
+
+    fun tryExecute(action: () -> Unit) {
+        if (isAnimationRunning) {
+            onAnimationsComplete.add(action)
+        } else {
+            action()
+        }
+    }
+
+    override fun onAnimationEnd(drawable: Drawable) {
+        super.onAnimationEnd(drawable)
+        if (!isAnimationRunning) {
+            onAnimationsComplete.forEach { it() }
+            onAnimationsComplete.clear()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
new file mode 100644
index 0000000..93be6a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.media.controls.ui
+
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.graphics.drawable.RippleDrawable
+import com.android.internal.R
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settingslib.Utils
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
+
+/**
+ * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
+ * is triggered.
+ */
+interface ColorTransition {
+    fun updateColorScheme(scheme: ColorScheme?): Boolean
+}
+
+/**
+ * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
+ * the animation and interpolate between the source color and the target color.
+ *
+ * Selection of the target color from the scheme, and application of the interpolated color are
+ * delegated to callbacks.
+ */
+open class AnimatingColorTransition(
+    private val defaultColor: Int,
+    private val extractColor: (ColorScheme) -> Int,
+    private val applyColor: (Int) -> Unit
+) : AnimatorUpdateListener, ColorTransition {
+
+    private val argbEvaluator = ArgbEvaluator()
+    private val valueAnimator = buildAnimator()
+    var sourceColor: Int = defaultColor
+    var currentColor: Int = defaultColor
+    var targetColor: Int = defaultColor
+
+    override fun onAnimationUpdate(animation: ValueAnimator) {
+        currentColor =
+            argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
+        applyColor(currentColor)
+    }
+
+    override fun updateColorScheme(scheme: ColorScheme?): Boolean {
+        val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
+        if (newTargetColor != targetColor) {
+            sourceColor = currentColor
+            targetColor = newTargetColor
+            valueAnimator.cancel()
+            valueAnimator.start()
+            return true
+        }
+        return false
+    }
+
+    init {
+        applyColor(defaultColor)
+    }
+
+    @VisibleForTesting
+    open fun buildAnimator(): ValueAnimator {
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = 333
+        animator.addUpdateListener(this)
+        return animator
+    }
+}
+
+typealias AnimatingColorTransitionFactory =
+    (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
+
+/**
+ * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
+ * transitioned when changed. It also sets up the assignment functions for sending the sending the
+ * interpolated colors to the appropriate views.
+ */
+class ColorSchemeTransition
+internal constructor(
+    private val context: Context,
+    private val mediaViewHolder: MediaViewHolder,
+    private val multiRippleController: MultiRippleController,
+    private val turbulenceNoiseController: TurbulenceNoiseController,
+    animatingColorTransitionFactory: AnimatingColorTransitionFactory
+) {
+    constructor(
+        context: Context,
+        mediaViewHolder: MediaViewHolder,
+        multiRippleController: MultiRippleController,
+        turbulenceNoiseController: TurbulenceNoiseController
+    ) : this(
+        context,
+        mediaViewHolder,
+        multiRippleController,
+        turbulenceNoiseController,
+        ::AnimatingColorTransition
+    )
+
+    val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
+    val surfaceColor =
+        animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
+            val colorList = ColorStateList.valueOf(surfaceColor)
+            mediaViewHolder.seamlessIcon.imageTintList = colorList
+            mediaViewHolder.seamlessText.setTextColor(surfaceColor)
+            mediaViewHolder.albumView.backgroundTintList = colorList
+            mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
+        }
+
+    val accentPrimary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::accentPrimaryFromScheme
+        ) { accentPrimary ->
+            val accentColorList = ColorStateList.valueOf(accentPrimary)
+            mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
+            mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
+            multiRippleController.updateColor(accentPrimary)
+            turbulenceNoiseController.updateNoiseColor(accentPrimary)
+        }
+
+    val accentSecondary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::accentSecondaryFromScheme
+        ) { accentSecondary ->
+            val colorList = ColorStateList.valueOf(accentSecondary)
+            (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
+                it.setColor(colorList)
+                it.effectColor = colorList
+            }
+        }
+
+    val colorSeamless =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            { colorScheme: ColorScheme ->
+                // A1-100 dark in dark theme, A1-200 in light theme
+                if (
+                    context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+                        UI_MODE_NIGHT_YES
+                )
+                    colorScheme.accent1[2]
+                else colorScheme.accent1[3]
+            },
+            { seamlessColor: Int ->
+                val accentColorList = ColorStateList.valueOf(seamlessColor)
+                mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
+            }
+        )
+
+    val textPrimary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::textPrimaryFromScheme
+        ) { textPrimary ->
+            mediaViewHolder.titleText.setTextColor(textPrimary)
+            val textColorList = ColorStateList.valueOf(textPrimary)
+            mediaViewHolder.seekBar.thumb.setTintList(textColorList)
+            mediaViewHolder.seekBar.progressTintList = textColorList
+            mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
+            mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
+            for (button in mediaViewHolder.getTransparentActionButtons()) {
+                button.imageTintList = textColorList
+            }
+            mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
+        }
+
+    val textPrimaryInverse =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimaryInverse),
+            ::textPrimaryInverseFromScheme
+        ) { textPrimaryInverse ->
+            mediaViewHolder.actionPlayPause.imageTintList =
+                ColorStateList.valueOf(textPrimaryInverse)
+        }
+
+    val textSecondary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorSecondary),
+            ::textSecondaryFromScheme
+        ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
+
+    val textTertiary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorTertiary),
+            ::textTertiaryFromScheme
+        ) { textTertiary ->
+            mediaViewHolder.seekBar.progressBackgroundTintList =
+                ColorStateList.valueOf(textTertiary)
+        }
+
+    val colorTransitions =
+        arrayOf(
+            surfaceColor,
+            colorSeamless,
+            accentPrimary,
+            accentSecondary,
+            textPrimary,
+            textPrimaryInverse,
+            textSecondary,
+            textTertiary,
+        )
+
+    private fun loadDefaultColor(id: Int): Int {
+        return Utils.getColorAttr(context, id).defaultColor
+    }
+
+    fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
+        var anyChanged = false
+        colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
+        colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
+        return anyChanged
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
new file mode 100644
index 0000000..9f86cd8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.Xfermode
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.MathUtils
+import android.view.View
+import androidx.annotation.Keep
+import com.android.internal.graphics.ColorUtils
+import com.android.internal.graphics.ColorUtils.blendARGB
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import org.xmlpull.v1.XmlPullParser
+
+private const val BACKGROUND_ANIM_DURATION = 370L
+
+/** Drawable that can draw an animated gradient when tapped. */
+@Keep
+class IlluminationDrawable : Drawable() {
+
+    private var themeAttrs: IntArray? = null
+    private var cornerRadiusOverride = -1f
+    var cornerRadius = 0f
+        get() {
+            return if (cornerRadiusOverride >= 0) {
+                cornerRadiusOverride
+            } else {
+                field
+            }
+        }
+    private var highlightColor = Color.TRANSPARENT
+    private var tmpHsl = floatArrayOf(0f, 0f, 0f)
+    private var paint = Paint()
+    private var highlight = 0f
+    private val lightSources = arrayListOf<LightSourceDrawable>()
+
+    private var backgroundColor = Color.TRANSPARENT
+        set(value) {
+            if (value == field) {
+                return
+            }
+            field = value
+            animateBackground()
+        }
+
+    private var backgroundAnimation: ValueAnimator? = null
+
+    /** Draw background and gradient. */
+    override fun draw(canvas: Canvas) {
+        canvas.drawRoundRect(
+            0f,
+            0f,
+            bounds.width().toFloat(),
+            bounds.height().toFloat(),
+            cornerRadius,
+            cornerRadius,
+            paint
+        )
+    }
+
+    override fun getOutline(outline: Outline) {
+        outline.setRoundRect(bounds, cornerRadius)
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.TRANSPARENT
+    }
+
+    override fun inflate(
+        r: Resources,
+        parser: XmlPullParser,
+        attrs: AttributeSet,
+        theme: Resources.Theme?
+    ) {
+        val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
+        themeAttrs = a.extractThemeAttrs()
+        updateStateFromTypedArray(a)
+        a.recycle()
+    }
+
+    private fun updateStateFromTypedArray(a: TypedArray) {
+        if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) {
+            cornerRadius =
+                a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius)
+        }
+        if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
+            highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
+        }
+    }
+
+    override fun canApplyTheme(): Boolean {
+        return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
+    }
+
+    override fun applyTheme(t: Resources.Theme) {
+        super.applyTheme(t)
+        themeAttrs?.let {
+            val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
+            updateStateFromTypedArray(a)
+            a.recycle()
+        }
+    }
+
+    override fun setColorFilter(p0: ColorFilter?) {
+        throw UnsupportedOperationException("Color filters are not supported")
+    }
+
+    override fun setAlpha(alpha: Int) {
+        if (alpha == paint.alpha) {
+            return
+        }
+
+        paint.alpha = alpha
+        invalidateSelf()
+
+        lightSources.forEach { it.alpha = alpha }
+    }
+
+    override fun getAlpha(): Int {
+        return paint.alpha
+    }
+
+    override fun setXfermode(mode: Xfermode?) {
+        if (mode == paint.xfermode) {
+            return
+        }
+
+        paint.xfermode = mode
+        invalidateSelf()
+    }
+
+    /**
+     * Cross fade background.
+     * @see setTintList
+     * @see backgroundColor
+     */
+    private fun animateBackground() {
+        ColorUtils.colorToHSL(backgroundColor, tmpHsl)
+        val L = tmpHsl[2]
+        tmpHsl[2] =
+            MathUtils.constrain(
+                if (L < 1f - highlight) {
+                    L + highlight
+                } else {
+                    L - highlight
+                },
+                0f,
+                1f
+            )
+
+        val initialBackground = paint.color
+        val initialHighlight = highlightColor
+        val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
+
+        backgroundAnimation?.cancel()
+        backgroundAnimation =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                duration = BACKGROUND_ANIM_DURATION
+                interpolator = Interpolators.FAST_OUT_LINEAR_IN
+                addUpdateListener {
+                    val progress = it.animatedValue as Float
+                    paint.color = blendARGB(initialBackground, backgroundColor, progress)
+                    highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
+                    lightSources.forEach { it.highlightColor = highlightColor }
+                    invalidateSelf()
+                }
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator?) {
+                            backgroundAnimation = null
+                        }
+                    }
+                )
+                start()
+            }
+    }
+
+    override fun setTintList(tint: ColorStateList?) {
+        super.setTintList(tint)
+        backgroundColor = tint!!.defaultColor
+    }
+
+    fun registerLightSource(lightSource: View) {
+        if (lightSource.background is LightSourceDrawable) {
+            registerLightSource(lightSource.background as LightSourceDrawable)
+        } else if (lightSource.foreground is LightSourceDrawable) {
+            registerLightSource(lightSource.foreground as LightSourceDrawable)
+        }
+    }
+
+    private fun registerLightSource(lightSource: LightSourceDrawable) {
+        lightSource.alpha = paint.alpha
+        lightSources.add(lightSource)
+    }
+
+    /** Set or remove the corner radius override. This is typically set during animations. */
+    fun setCornerRadiusOverride(cornerRadius: Float?) {
+        cornerRadiusOverride = cornerRadius ?: -1f
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
new file mode 100644
index 0000000..899148b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.content.Context
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.dagger.MediaModule.KEYGUARD
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.stack.MediaContainerView
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.settings.SecureSettings
+import javax.inject.Inject
+import javax.inject.Named
+
+/**
+ * Controls the media notifications on the lock screen, handles its visibility and placement -
+ * switches media player positioning between split pane container vs single pane container
+ */
+@SysUISingleton
+class KeyguardMediaController
+@Inject
+constructor(
+    @param:Named(KEYGUARD) private val mediaHost: MediaHost,
+    private val bypassController: KeyguardBypassController,
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val context: Context,
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
+    configurationController: ConfigurationController,
+) {
+
+    init {
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onStateChanged(newState: Int) {
+                    refreshMediaPosition()
+                }
+            }
+        )
+        configurationController.addCallback(
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration?) {
+                    updateResources()
+                }
+            }
+        )
+
+        val settingsObserver: ContentObserver =
+            object : ContentObserver(handler) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    if (uri == lockScreenMediaPlayerUri) {
+                        allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                true,
+                                UserHandle.USER_CURRENT
+                            )
+                        refreshMediaPosition()
+                    }
+                }
+            }
+        secureSettings.registerContentObserverForUser(
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            settingsObserver,
+            UserHandle.USER_ALL
+        )
+
+        // First let's set the desired state that we want for this host
+        mediaHost.expansion = MediaHostState.EXPANDED
+        mediaHost.showsOnlyActiveMedia = true
+        mediaHost.falsingProtectionNeeded = true
+
+        // Let's now initialize this view, which also creates the host view for us.
+        mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
+        updateResources()
+    }
+
+    private fun updateResources() {
+        useSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
+    }
+
+    @VisibleForTesting
+    var useSplitShade = false
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            reattachHostView()
+            refreshMediaPosition()
+        }
+
+    /** Is the media player visible? */
+    var visible = false
+        private set
+
+    var visibilityChangedListener: ((Boolean) -> Unit)? = null
+
+    /** single pane media container placed at the top of the notifications list */
+    var singlePaneContainer: MediaContainerView? = null
+        private set
+    private var splitShadeContainer: ViewGroup? = null
+
+    /** Track the media player setting status on lock screen. */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+        secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
+     * Attaches media container in single pane mode, situated at the top of the notifications list
+     */
+    fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
+        val needsListener = singlePaneContainer == null
+        singlePaneContainer = mediaView
+        if (needsListener) {
+            // On reinflation we don't want to add another listener
+            mediaHost.addVisibilityChangeListener(this::onMediaHostVisibilityChanged)
+        }
+        reattachHostView()
+        onMediaHostVisibilityChanged(mediaHost.visible)
+    }
+
+    /** Called whenever the media hosts visibility changes */
+    private fun onMediaHostVisibilityChanged(visible: Boolean) {
+        refreshMediaPosition()
+        if (visible) {
+            mediaHost.hostView.layoutParams.apply {
+                height = ViewGroup.LayoutParams.WRAP_CONTENT
+                width = ViewGroup.LayoutParams.MATCH_PARENT
+            }
+        }
+    }
+
+    /** Attaches media container in split shade mode, situated to the left of notifications */
+    fun attachSplitShadeContainer(container: ViewGroup) {
+        splitShadeContainer = container
+        reattachHostView()
+        refreshMediaPosition()
+    }
+
+    private fun reattachHostView() {
+        val inactiveContainer: ViewGroup?
+        val activeContainer: ViewGroup?
+        if (useSplitShade) {
+            activeContainer = splitShadeContainer
+            inactiveContainer = singlePaneContainer
+        } else {
+            inactiveContainer = splitShadeContainer
+            activeContainer = singlePaneContainer
+        }
+        if (inactiveContainer?.childCount == 1) {
+            inactiveContainer.removeAllViews()
+        }
+        if (activeContainer?.childCount == 0) {
+            // Detach the hostView from its parent view if exists
+            mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
+            activeContainer.addView(mediaHost.hostView)
+        }
+    }
+
+    fun refreshMediaPosition() {
+        val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
+        // mediaHost.visible required for proper animations handling
+        visible =
+            mediaHost.visible &&
+                !bypassController.bypassEnabled &&
+                keyguardOrUserSwitcher &&
+                allowMediaPlayerOnLockScreen
+        if (visible) {
+            showMediaPlayer()
+        } else {
+            hideMediaPlayer()
+        }
+    }
+
+    private fun showMediaPlayer() {
+        if (useSplitShade) {
+            setVisibility(splitShadeContainer, View.VISIBLE)
+            setVisibility(singlePaneContainer, View.GONE)
+        } else {
+            setVisibility(singlePaneContainer, View.VISIBLE)
+            setVisibility(splitShadeContainer, View.GONE)
+        }
+    }
+
+    private fun hideMediaPlayer() {
+        // always hide splitShadeContainer as it's initially visible and may influence layout
+        setVisibility(splitShadeContainer, View.GONE)
+        setVisibility(singlePaneContainer, View.GONE)
+    }
+
+    private fun setVisibility(view: ViewGroup?, newVisibility: Int) {
+        val previousVisibility = view?.visibility
+        view?.visibility = newVisibility
+        if (previousVisibility != newVisibility) {
+            visibilityChangedListener?.invoke(newVisibility == View.VISIBLE)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
new file mode 100644
index 0000000..dd5c2bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Outline
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.RadialGradient
+import android.graphics.Rect
+import android.graphics.Shader
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.MathUtils.lerp
+import androidx.annotation.Keep
+import com.android.internal.graphics.ColorUtils
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import org.xmlpull.v1.XmlPullParser
+
+private const val RIPPLE_ANIM_DURATION = 800L
+private const val RIPPLE_DOWN_PROGRESS = 0.05f
+private const val RIPPLE_CANCEL_DURATION = 200L
+private val GRADIENT_STOPS = floatArrayOf(0.2f, 1f)
+
+private data class RippleData(
+    var x: Float,
+    var y: Float,
+    var alpha: Float,
+    var progress: Float,
+    var minSize: Float,
+    var maxSize: Float,
+    var highlight: Float
+)
+
+/** Drawable that can draw an animated gradient when tapped. */
+@Keep
+class LightSourceDrawable : Drawable() {
+
+    private var pressed = false
+    private var themeAttrs: IntArray? = null
+    private val rippleData = RippleData(0f, 0f, 0f, 0f, 0f, 0f, 0f)
+    private var paint = Paint()
+
+    var highlightColor = Color.WHITE
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            invalidateSelf()
+        }
+
+    /** Draw a small highlight under the finger before expanding (or cancelling) it. */
+    private var active: Boolean = false
+        set(value) {
+            if (value == field) {
+                return
+            }
+            field = value
+
+            if (value) {
+                rippleAnimation?.cancel()
+                rippleData.alpha = 1f
+                rippleData.progress = RIPPLE_DOWN_PROGRESS
+            } else {
+                rippleAnimation?.cancel()
+                rippleAnimation =
+                    ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
+                        duration = RIPPLE_CANCEL_DURATION
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.alpha = it.animatedValue as Float
+                            invalidateSelf()
+                        }
+                        addListener(
+                            object : AnimatorListenerAdapter() {
+                                var cancelled = false
+                                override fun onAnimationCancel(animation: Animator?) {
+                                    cancelled = true
+                                }
+
+                                override fun onAnimationEnd(animation: Animator?) {
+                                    if (cancelled) {
+                                        return
+                                    }
+                                    rippleData.progress = 0f
+                                    rippleData.alpha = 0f
+                                    rippleAnimation = null
+                                    invalidateSelf()
+                                }
+                            }
+                        )
+                        start()
+                    }
+            }
+            invalidateSelf()
+        }
+
+    private var rippleAnimation: Animator? = null
+
+    /** Draw background and gradient. */
+    override fun draw(canvas: Canvas) {
+        val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
+        val centerColor =
+            ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
+        paint.shader =
+            RadialGradient(
+                rippleData.x,
+                rippleData.y,
+                radius,
+                intArrayOf(centerColor, Color.TRANSPARENT),
+                GRADIENT_STOPS,
+                Shader.TileMode.CLAMP
+            )
+        canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
+    }
+
+    override fun getOutline(outline: Outline) {
+        // No bounds, parent will clip it
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.TRANSPARENT
+    }
+
+    override fun inflate(
+        r: Resources,
+        parser: XmlPullParser,
+        attrs: AttributeSet,
+        theme: Resources.Theme?
+    ) {
+        val a = obtainAttributes(r, theme, attrs, R.styleable.IlluminationDrawable)
+        themeAttrs = a.extractThemeAttrs()
+        updateStateFromTypedArray(a)
+        a.recycle()
+    }
+
+    private fun updateStateFromTypedArray(a: TypedArray) {
+        if (a.hasValue(R.styleable.IlluminationDrawable_rippleMinSize)) {
+            rippleData.minSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMinSize, 0f)
+        }
+        if (a.hasValue(R.styleable.IlluminationDrawable_rippleMaxSize)) {
+            rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
+        }
+        if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
+            rippleData.highlight =
+                a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
+        }
+    }
+
+    override fun canApplyTheme(): Boolean {
+        return themeAttrs != null && themeAttrs!!.size > 0 || super.canApplyTheme()
+    }
+
+    override fun applyTheme(t: Resources.Theme) {
+        super.applyTheme(t)
+        themeAttrs?.let {
+            val a = t.resolveAttributes(it, R.styleable.IlluminationDrawable)
+            updateStateFromTypedArray(a)
+            a.recycle()
+        }
+    }
+
+    override fun setColorFilter(p0: ColorFilter?) {
+        throw UnsupportedOperationException("Color filters are not supported")
+    }
+
+    override fun setAlpha(alpha: Int) {
+        if (alpha == paint.alpha) {
+            return
+        }
+
+        paint.alpha = alpha
+        invalidateSelf()
+    }
+
+    /** Draws an animated ripple that expands fading away. */
+    private fun illuminate() {
+        rippleData.alpha = 1f
+        invalidateSelf()
+
+        rippleAnimation?.cancel()
+        rippleAnimation =
+            AnimatorSet().apply {
+                playTogether(
+                    ValueAnimator.ofFloat(1f, 0f).apply {
+                        startDelay = 133
+                        duration = RIPPLE_ANIM_DURATION - startDelay
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.alpha = it.animatedValue as Float
+                            invalidateSelf()
+                        }
+                    },
+                    ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
+                        duration = RIPPLE_ANIM_DURATION
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.progress = it.animatedValue as Float
+                            invalidateSelf()
+                        }
+                    }
+                )
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator?) {
+                            rippleData.progress = 0f
+                            rippleAnimation = null
+                            invalidateSelf()
+                        }
+                    }
+                )
+                start()
+            }
+    }
+
+    override fun setHotspot(x: Float, y: Float) {
+        rippleData.x = x
+        rippleData.y = y
+        if (active) {
+            invalidateSelf()
+        }
+    }
+
+    override fun isStateful(): Boolean {
+        return true
+    }
+
+    override fun hasFocusStateSpecified(): Boolean {
+        return true
+    }
+
+    override fun isProjected(): Boolean {
+        return true
+    }
+
+    override fun getDirtyBounds(): Rect {
+        val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
+        val bounds =
+            Rect(
+                (rippleData.x - radius).toInt(),
+                (rippleData.y - radius).toInt(),
+                (rippleData.x + radius).toInt(),
+                (rippleData.y + radius).toInt()
+            )
+        bounds.union(super.getDirtyBounds())
+        return bounds
+    }
+
+    override fun onStateChange(stateSet: IntArray?): Boolean {
+        val changed = super.onStateChange(stateSet)
+        if (stateSet == null) {
+            return changed
+        }
+
+        val wasPressed = pressed
+        var enabled = false
+        pressed = false
+        var focused = false
+        var hovered = false
+
+        for (state in stateSet) {
+            when (state) {
+                com.android.internal.R.attr.state_enabled -> {
+                    enabled = true
+                }
+                com.android.internal.R.attr.state_focused -> {
+                    focused = true
+                }
+                com.android.internal.R.attr.state_pressed -> {
+                    pressed = true
+                }
+                com.android.internal.R.attr.state_hovered -> {
+                    hovered = true
+                }
+            }
+        }
+
+        active = enabled && (pressed || focused || hovered)
+        if (wasPressed && !pressed) {
+            illuminate()
+        }
+
+        return changed
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
new file mode 100644
index 0000000..8aaee81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -0,0 +1,1337 @@
+/*
+ * 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.media.controls.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
+import android.util.Log
+import android.util.MathUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.PathInterpolator
+import android.widget.LinearLayout
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.InstanceId
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.media.controls.util.SmallHash
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PageIndicator
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.Utils
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.animation.requiresRemeasuring
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.traceSection
+import java.io.PrintWriter
+import java.util.TreeMap
+import javax.inject.Inject
+import javax.inject.Provider
+
+private const val TAG = "MediaCarouselController"
+private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
+private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+/**
+ * Class that is responsible for keeping the view carousel up to date. This also handles changes in
+ * state and applies them to the media carousel like the expansion.
+ */
+@SysUISingleton
+class MediaCarouselController
+@Inject
+constructor(
+    private val context: Context,
+    private val mediaControlPanelFactory: Provider<MediaControlPanel>,
+    private val visualStabilityProvider: VisualStabilityProvider,
+    private val mediaHostStatesManager: MediaHostStatesManager,
+    private val activityStarter: ActivityStarter,
+    private val systemClock: SystemClock,
+    @Main executor: DelayableExecutor,
+    private val mediaManager: MediaDataManager,
+    configurationController: ConfigurationController,
+    falsingCollector: FalsingCollector,
+    falsingManager: FalsingManager,
+    dumpManager: DumpManager,
+    private val logger: MediaUiEventLogger,
+    private val debugLogger: MediaCarouselControllerLogger
+) : Dumpable {
+    /** The current width of the carousel */
+    private var currentCarouselWidth: Int = 0
+
+    /** The current height of the carousel */
+    private var currentCarouselHeight: Int = 0
+
+    /** Are we currently showing only active players */
+    private var currentlyShowingOnlyActive: Boolean = false
+
+    /** Is the player currently visible (at the end of the transformation */
+    private var playersVisible: Boolean = false
+    /**
+     * The desired location where we'll be at the end of the transformation. Usually this matches
+     * the end location, except when we're still waiting on a state update call.
+     */
+    @MediaLocation private var desiredLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation @VisibleForTesting var currentEndLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation private var currentStartLocation: Int = -1
+
+    /** The progress of the transition or 1.0 if there is no transition happening */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /** The measured width of the carousel */
+    private var carouselMeasureWidth: Int = 0
+
+    /** The measured height of the carousel */
+    private var carouselMeasureHeight: Int = 0
+    private var desiredHostState: MediaHostState? = null
+    private val mediaCarousel: MediaScrollView
+    val mediaCarouselScrollHandler: MediaCarouselScrollHandler
+    val mediaFrame: ViewGroup
+    @VisibleForTesting
+    lateinit var settingsButton: View
+        private set
+    private val mediaContent: ViewGroup
+    @VisibleForTesting val pageIndicator: PageIndicator
+    private val visualStabilityCallback: OnReorderingAllowedListener
+    private var needsReordering: Boolean = false
+    private var keysNeedRemoval = mutableSetOf<String>()
+    var shouldScrollToKey: Boolean = false
+    private var isRtl: Boolean = false
+        set(value) {
+            if (value != field) {
+                field = value
+                mediaFrame.layoutDirection =
+                    if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
+                mediaCarouselScrollHandler.scrollToStart()
+            }
+        }
+    private var currentlyExpanded = true
+        set(value) {
+            if (field != value) {
+                field = value
+                for (player in MediaPlayerData.players()) {
+                    player.setListening(field)
+                }
+            }
+        }
+
+    companion object {
+        const val ANIMATION_BASE_DURATION = 2200f
+        const val DURATION = 167f
+        const val DETAILS_DELAY = 1067f
+        const val CONTROLS_DELAY = 1400f
+        const val PAGINATION_DELAY = 1900f
+        const val MEDIATITLES_DELAY = 1000f
+        const val MEDIACONTAINERS_DELAY = 967f
+        val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
+        val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F)
+
+        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
+            val transformStartFraction = delay / ANIMATION_BASE_DURATION
+            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
+            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
+            return MathUtils.constrain(
+                (squishinessToTime - transformStartFraction) / transformDurationFraction,
+                0F,
+                1F
+            )
+        }
+    }
+
+    private val configListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onDensityOrFontScaleChanged() {
+                // System font changes should only happen when UMO is offscreen or a flicker may
+                // occur
+                updatePlayers(recreateMedia = true)
+                inflateSettingsButton()
+            }
+
+            override fun onThemeChanged() {
+                updatePlayers(recreateMedia = false)
+                inflateSettingsButton()
+            }
+
+            override fun onConfigChanged(newConfig: Configuration?) {
+                if (newConfig == null) return
+                isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
+            }
+
+            override fun onUiModeChanged() {
+                updatePlayers(recreateMedia = false)
+                inflateSettingsButton()
+            }
+        }
+
+    /**
+     * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
+     * It will be called when the container is out of view.
+     */
+    lateinit var updateUserVisibility: () -> Unit
+    lateinit var updateHostVisibility: () -> Unit
+
+    private val isReorderingAllowed: Boolean
+        get() = visualStabilityProvider.isReorderingAllowed
+
+    init {
+        dumpManager.registerDumpable(TAG, this)
+        mediaFrame = inflateMediaCarousel()
+        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
+        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
+        mediaCarouselScrollHandler =
+            MediaCarouselScrollHandler(
+                mediaCarousel,
+                pageIndicator,
+                executor,
+                this::onSwipeToDismiss,
+                this::updatePageIndicatorLocation,
+                this::closeGuts,
+                falsingCollector,
+                falsingManager,
+                this::logSmartspaceImpression,
+                logger
+            )
+        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
+        inflateSettingsButton()
+        mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
+        configurationController.addCallback(configListener)
+        visualStabilityCallback = OnReorderingAllowedListener {
+            if (needsReordering) {
+                needsReordering = false
+                reorderAllPlayers(previousVisiblePlayerKey = null)
+            }
+
+            keysNeedRemoval.forEach { removePlayer(it) }
+            if (keysNeedRemoval.size > 0) {
+                // Carousel visibility may need to be updated after late removals
+                updateHostVisibility()
+            }
+            keysNeedRemoval.clear()
+
+            // Update user visibility so that no extra impression will be logged when
+            // activeMediaIndex resets to 0
+            if (this::updateUserVisibility.isInitialized) {
+                updateUserVisibility()
+            }
+
+            // Let's reset our scroll position
+            mediaCarouselScrollHandler.scrollToStart()
+        }
+        visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
+        mediaManager.addListener(
+            object : MediaDataManager.Listener {
+                override fun onMediaDataLoaded(
+                    key: String,
+                    oldKey: String?,
+                    data: MediaData,
+                    immediately: Boolean,
+                    receivedSmartspaceCardLatency: Int,
+                    isSsReactivated: Boolean
+                ) {
+                    debugLogger.logMediaLoaded(key)
+                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
+                        // Log card received if a new resumable media card is added
+                        MediaPlayerData.getMediaPlayer(key)?.let {
+                            /* ktlint-disable max-line-length */
+                            logSmartspaceCardReported(
+                                759, // SMARTSPACE_CARD_RECEIVED
+                                it.mSmartspaceId,
+                                it.mUid,
+                                surfaces =
+                                    intArrayOf(
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                    ),
+                                rank = MediaPlayerData.getMediaPlayerIndex(key)
+                            )
+                            /* ktlint-disable max-line-length */
+                        }
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                mediaCarouselScrollHandler.visibleMediaIndex ==
+                                    MediaPlayerData.getMediaPlayerIndex(key)
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    } else if (receivedSmartspaceCardLatency != 0) {
+                        // Log resume card received if resumable media card is reactivated and
+                        // resume card is ranked first
+                        MediaPlayerData.players().forEachIndexed { index, it ->
+                            if (it.recommendationViewHolder == null) {
+                                it.mSmartspaceId =
+                                    SmallHash.hash(
+                                        it.mUid + systemClock.currentTimeMillis().toInt()
+                                    )
+                                it.mIsImpressed = false
+                                /* ktlint-disable max-line-length */
+                                logSmartspaceCardReported(
+                                    759, // SMARTSPACE_CARD_RECEIVED
+                                    it.mSmartspaceId,
+                                    it.mUid,
+                                    surfaces =
+                                        intArrayOf(
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                        ),
+                                    rank = index,
+                                    receivedLatencyMillis = receivedSmartspaceCardLatency
+                                )
+                                /* ktlint-disable max-line-length */
+                            }
+                        }
+                        // If media container area already visible to the user, log impression for
+                        // reactivated card.
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                !mediaCarouselScrollHandler.qsExpanded
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    }
+
+                    val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
+                    if (canRemove && !Utils.useMediaResumption(context)) {
+                        // This view isn't playing, let's remove this! This happens e.g. when
+                        // dismissing/timing out a view. We still have the data around because
+                        // resumption could be on, but we should save the resources and release
+                        // this.
+                        if (isReorderingAllowed) {
+                            onMediaDataRemoved(key)
+                        } else {
+                            keysNeedRemoval.add(key)
+                        }
+                    } else {
+                        keysNeedRemoval.remove(key)
+                    }
+                }
+
+                override fun onSmartspaceMediaDataLoaded(
+                    key: String,
+                    data: SmartspaceMediaData,
+                    shouldPrioritize: Boolean
+                ) {
+                    debugLogger.logRecommendationLoaded(key)
+                    // Log the case where the hidden media carousel with the existed inactive resume
+                    // media is shown by the Smartspace signal.
+                    if (data.isActive) {
+                        val hasActivatedExistedResumeMedia =
+                            !mediaManager.hasActiveMedia() &&
+                                mediaManager.hasAnyMedia() &&
+                                shouldPrioritize
+                        if (hasActivatedExistedResumeMedia) {
+                            // Log resume card received if resumable media card is reactivated and
+                            // recommendation card is valid and ranked first
+                            MediaPlayerData.players().forEachIndexed { index, it ->
+                                if (it.recommendationViewHolder == null) {
+                                    it.mSmartspaceId =
+                                        SmallHash.hash(
+                                            it.mUid + systemClock.currentTimeMillis().toInt()
+                                        )
+                                    it.mIsImpressed = false
+                                    /* ktlint-disable max-line-length */
+                                    logSmartspaceCardReported(
+                                        759, // SMARTSPACE_CARD_RECEIVED
+                                        it.mSmartspaceId,
+                                        it.mUid,
+                                        surfaces =
+                                            intArrayOf(
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                            ),
+                                        rank = index,
+                                        receivedLatencyMillis =
+                                            (systemClock.currentTimeMillis() -
+                                                    data.headphoneConnectionTimeMillis)
+                                                .toInt()
+                                    )
+                                    /* ktlint-disable max-line-length */
+                                }
+                            }
+                        }
+                        addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
+                        MediaPlayerData.getMediaPlayer(key)?.let {
+                            /* ktlint-disable max-line-length */
+                            logSmartspaceCardReported(
+                                759, // SMARTSPACE_CARD_RECEIVED
+                                it.mSmartspaceId,
+                                it.mUid,
+                                surfaces =
+                                    intArrayOf(
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                    ),
+                                rank = MediaPlayerData.getMediaPlayerIndex(key),
+                                receivedLatencyMillis =
+                                    (systemClock.currentTimeMillis() -
+                                            data.headphoneConnectionTimeMillis)
+                                        .toInt()
+                            )
+                            /* ktlint-disable max-line-length */
+                        }
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                mediaCarouselScrollHandler.visibleMediaIndex ==
+                                    MediaPlayerData.getMediaPlayerIndex(key)
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    } else {
+                        onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+                    }
+                }
+
+                override fun onMediaDataRemoved(key: String) {
+                    debugLogger.logMediaRemoved(key)
+                    removePlayer(key)
+                }
+
+                override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+                    debugLogger.logRecommendationRemoved(key, immediately)
+                    if (immediately || isReorderingAllowed) {
+                        removePlayer(key)
+                        if (!immediately) {
+                            // Although it wasn't requested, we were able to process the removal
+                            // immediately since reordering is allowed. So, notify hosts to update
+                            if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
+                                updateHostVisibility()
+                            }
+                        }
+                    } else {
+                        keysNeedRemoval.add(key)
+                    }
+                }
+            }
+        )
+        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+            // The pageIndicator is not laid out yet when we get the current state update,
+            // Lets make sure we have the right dimensions
+            updatePageIndicatorLocation()
+        }
+        mediaHostStatesManager.addCallback(
+            object : MediaHostStatesManager.Callback {
+                override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+                    if (location == desiredLocation) {
+                        onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
+                    }
+                }
+            }
+        )
+    }
+
+    private fun inflateSettingsButton() {
+        val settings =
+            LayoutInflater.from(context)
+                .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View
+        if (this::settingsButton.isInitialized) {
+            mediaFrame.removeView(settingsButton)
+        }
+        settingsButton = settings
+        mediaFrame.addView(settingsButton)
+        mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
+        settingsButton.setOnClickListener {
+            logger.logCarouselSettings()
+            activityStarter.startActivity(settingsIntent, true /* dismissShade */)
+        }
+    }
+
+    private fun inflateMediaCarousel(): ViewGroup {
+        val mediaCarousel =
+            LayoutInflater.from(context)
+                .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
+        // Because this is inflated when not attached to the true view hierarchy, it resolves some
+        // potential issues to force that the layout direction is defined by the locale
+        // (rather than inherited from the parent, which would resolve to LTR when unattached).
+        mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+        return mediaCarousel
+    }
+
+    private fun reorderAllPlayers(
+        previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
+        key: String? = null
+    ) {
+        mediaContent.removeAllViews()
+        for (mediaPlayer in MediaPlayerData.players()) {
+            mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) }
+                ?: mediaPlayer.recommendationViewHolder?.let {
+                    mediaContent.addView(it.recommendations)
+                }
+        }
+        mediaCarouselScrollHandler.onPlayersChanged()
+        MediaPlayerData.updateVisibleMediaPlayers()
+        // Automatically scroll to the active player if needed
+        if (shouldScrollToKey) {
+            shouldScrollToKey = false
+            val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
+            if (mediaIndex != -1) {
+                previousVisiblePlayerKey?.let {
+                    val previousVisibleIndex =
+                        MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
+                    mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
+                }
+                    ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
+            }
+        }
+    }
+
+    // Returns true if new player is added
+    private fun addOrUpdatePlayer(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        isSsReactivated: Boolean
+    ): Boolean =
+        traceSection("MediaCarouselController#addOrUpdatePlayer") {
+            MediaPlayerData.moveIfExists(oldKey, key)
+            val existingPlayer = MediaPlayerData.getMediaPlayer(key)
+            val curVisibleMediaKey =
+                MediaPlayerData.visiblePlayerKeys()
+                    .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            if (existingPlayer == null) {
+                val newPlayer = mediaControlPanelFactory.get()
+                newPlayer.attachPlayer(
+                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+                )
+                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
+                val lp =
+                    LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                    )
+                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+                newPlayer.bindPlayer(data, key)
+                newPlayer.setListening(currentlyExpanded)
+                MediaPlayerData.addMediaPlayer(
+                    key,
+                    data,
+                    newPlayer,
+                    systemClock,
+                    isSsReactivated,
+                    debugLogger
+                )
+                updatePlayerToState(newPlayer, noAnimation = true)
+                // Media data added from a recommendation card should starts playing.
+                if (
+                    (shouldScrollToKey && data.isPlaying == true) ||
+                        (!shouldScrollToKey && data.active)
+                ) {
+                    reorderAllPlayers(curVisibleMediaKey, key)
+                } else {
+                    needsReordering = true
+                }
+            } else {
+                existingPlayer.bindPlayer(data, key)
+                MediaPlayerData.addMediaPlayer(
+                    key,
+                    data,
+                    existingPlayer,
+                    systemClock,
+                    isSsReactivated,
+                    debugLogger
+                )
+                val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
+                // In case of recommendations hits.
+                // Check the playing status of media player and the package name.
+                // To make sure we scroll to the right app's media player.
+                if (
+                    isReorderingAllowed ||
+                        shouldScrollToKey &&
+                            data.isPlaying == true &&
+                            packageName == data.packageName
+                ) {
+                    reorderAllPlayers(curVisibleMediaKey, key)
+                } else {
+                    needsReordering = true
+                }
+            }
+            updatePageIndicator()
+            mediaCarouselScrollHandler.onPlayersChanged()
+            mediaFrame.requiresRemeasuring = true
+            // Check postcondition: mediaContent should have the same number of children as there
+            // are
+            // elements in mediaPlayers.
+            if (MediaPlayerData.players().size != mediaContent.childCount) {
+                Log.e(
+                    TAG,
+                    "Size of players list and number of views in carousel are out of sync. " +
+                        "Players size is ${MediaPlayerData.players().size}. " +
+                        "View count is ${mediaContent.childCount}."
+                )
+            }
+            return existingPlayer == null
+        }
+
+    private fun addSmartspaceMediaRecommendations(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) =
+        traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
+            if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
+            if (MediaPlayerData.getMediaPlayer(key) != null) {
+                Log.w(TAG, "Skip adding smartspace target in carousel")
+                return
+            }
+
+            val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
+            existingSmartspaceMediaKey?.let {
+                val removedPlayer =
+                    MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true)
+                removedPlayer?.run {
+                    debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey)
+                }
+            }
+
+            val newRecs = mediaControlPanelFactory.get()
+            newRecs.attachRecommendation(
+                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
+            )
+            newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
+            val lp =
+                LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT
+                )
+            newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
+            newRecs.bindRecommendation(data)
+            val curVisibleMediaKey =
+                MediaPlayerData.visiblePlayerKeys()
+                    .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            MediaPlayerData.addMediaRecommendation(
+                key,
+                data,
+                newRecs,
+                shouldPrioritize,
+                systemClock,
+                debugLogger
+            )
+            updatePlayerToState(newRecs, noAnimation = true)
+            reorderAllPlayers(curVisibleMediaKey)
+            updatePageIndicator()
+            mediaFrame.requiresRemeasuring = true
+            // Check postcondition: mediaContent should have the same number of children as there
+            // are
+            // elements in mediaPlayers.
+            if (MediaPlayerData.players().size != mediaContent.childCount) {
+                Log.e(
+                    TAG,
+                    "Size of players list and number of views in carousel are out of sync. " +
+                        "Players size is ${MediaPlayerData.players().size}. " +
+                        "View count is ${mediaContent.childCount}."
+                )
+            }
+        }
+
+    fun removePlayer(
+        key: String,
+        dismissMediaData: Boolean = true,
+        dismissRecommendation: Boolean = true
+    ) {
+        if (key == MediaPlayerData.smartspaceMediaKey()) {
+            MediaPlayerData.smartspaceMediaData?.let {
+                logger.logRecommendationRemoved(it.packageName, it.instanceId)
+            }
+        }
+        val removed =
+            MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation)
+        removed?.apply {
+            mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
+            mediaContent.removeView(removed.mediaViewHolder?.player)
+            mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
+            removed.onDestroy()
+            mediaCarouselScrollHandler.onPlayersChanged()
+            updatePageIndicator()
+
+            if (dismissMediaData) {
+                // Inform the media manager of a potentially late dismissal
+                mediaManager.dismissMediaData(key, delay = 0L)
+            }
+            if (dismissRecommendation) {
+                // Inform the media manager of a potentially late dismissal
+                mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
+            }
+        }
+    }
+
+    private fun updatePlayers(recreateMedia: Boolean) {
+        pageIndicator.tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+
+        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
+            if (isSsMediaRec) {
+                val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
+                removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
+                smartspaceMediaData?.let {
+                    addSmartspaceMediaRecommendations(
+                        it.targetId,
+                        it,
+                        MediaPlayerData.shouldPrioritizeSs
+                    )
+                }
+            } else {
+                val isSsReactivated = MediaPlayerData.isSsReactivated(key)
+                if (recreateMedia) {
+                    removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
+                }
+                addOrUpdatePlayer(
+                    key = key,
+                    oldKey = null,
+                    data = data,
+                    isSsReactivated = isSsReactivated
+                )
+            }
+        }
+    }
+
+    private fun updatePageIndicator() {
+        val numPages = mediaContent.getChildCount()
+        pageIndicator.setNumPages(numPages)
+        if (numPages == 1) {
+            pageIndicator.setLocation(0f)
+        }
+        updatePageIndicatorAlpha()
+    }
+
+    /**
+     * Set a new interpolated state for all players. This is a state that is usually controlled by a
+     * finger movement where the user drags from one state to the next.
+     *
+     * @param startLocation the start location of our state or -1 if this is directly set
+     * @param endLocation the ending location of our state.
+     * @param progress the progress of the transition between startLocation and endlocation. If
+     * ```
+     *                 this is not a guided transformation, this will be 1.0f
+     * @param immediately
+     * ```
+     * should this state be applied immediately, canceling all animations?
+     */
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        progress: Float,
+        immediately: Boolean
+    ) {
+        if (
+            startLocation != currentStartLocation ||
+                endLocation != currentEndLocation ||
+                progress != currentTransitionProgress ||
+                immediately
+        ) {
+            currentStartLocation = startLocation
+            currentEndLocation = endLocation
+            currentTransitionProgress = progress
+            for (mediaPlayer in MediaPlayerData.players()) {
+                updatePlayerToState(mediaPlayer, immediately)
+            }
+            maybeResetSettingsCog()
+            updatePageIndicatorAlpha()
+        }
+    }
+
+    @VisibleForTesting
+    fun updatePageIndicatorAlpha() {
+        val hostStates = mediaHostStatesManager.mediaHostStates
+        val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
+        val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
+        val startAlpha = if (startIsVisible) 1.0f else 0.0f
+        // when squishing in split shade, only use endState, which keeps changing
+        // to provide squishFraction
+        val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
+        val endAlpha =
+            (if (endIsVisible) 1.0f else 0.0f) *
+                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
+        var alpha = 1.0f
+        if (!endIsVisible || !startIsVisible) {
+            var progress = currentTransitionProgress
+            if (!endIsVisible) {
+                progress = 1.0f - progress
+            }
+            // Let's fade in quickly at the end where the view is visible
+            progress =
+                MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f)
+            alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
+        }
+        pageIndicator.alpha = alpha
+    }
+
+    private fun updatePageIndicatorLocation() {
+        // Update the location of the page indicator, carousel clipping
+        val translationX =
+            if (isRtl) {
+                (pageIndicator.width - currentCarouselWidth) / 2.0f
+            } else {
+                (currentCarouselWidth - pageIndicator.width) / 2.0f
+            }
+        pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
+        val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
+        pageIndicator.translationY =
+            (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat()
+    }
+
+    /** Update the dimension of this carousel. */
+    private fun updateCarouselDimensions() {
+        var width = 0
+        var height = 0
+        for (mediaPlayer in MediaPlayerData.players()) {
+            val controller = mediaPlayer.mediaViewController
+            // When transitioning the view to gone, the view gets smaller, but the translation
+            // Doesn't, let's add the translation
+            width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
+            height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
+        }
+        if (width != currentCarouselWidth || height != currentCarouselHeight) {
+            currentCarouselWidth = width
+            currentCarouselHeight = height
+            mediaCarouselScrollHandler.setCarouselBounds(
+                currentCarouselWidth,
+                currentCarouselHeight
+            )
+            updatePageIndicatorLocation()
+            updatePageIndicatorAlpha()
+        }
+    }
+
+    private fun maybeResetSettingsCog() {
+        val hostStates = mediaHostStatesManager.mediaHostStates
+        val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true
+        val startShowsActive =
+            hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive
+        if (
+            currentlyShowingOnlyActive != endShowsActive ||
+                ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
+                    startShowsActive != endShowsActive)
+        ) {
+            // Whenever we're transitioning from between differing states or the endstate differs
+            // we reset the translation
+            currentlyShowingOnlyActive = endShowsActive
+            mediaCarouselScrollHandler.resetTranslation(animate = true)
+        }
+    }
+
+    private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
+        mediaPlayer.mediaViewController.setCurrentState(
+            startLocation = currentStartLocation,
+            endLocation = currentEndLocation,
+            transitionProgress = currentTransitionProgress,
+            applyImmediately = noAnimation
+        )
+    }
+
+    /**
+     * The desired location of this view has changed. We should remeasure the view to match the new
+     * bounds and kick off bounds animations if necessary. If an animation is happening, an
+     * animation is kicked of externally, which sets a new current state until we reach the
+     * targetState.
+     *
+     * @param desiredLocation the location we're going to
+     * @param desiredHostState the target state we're transitioning to
+     * @param animate should this be animated
+     */
+    fun onDesiredLocationChanged(
+        desiredLocation: Int,
+        desiredHostState: MediaHostState?,
+        animate: Boolean,
+        duration: Long = 200,
+        startDelay: Long = 0
+    ) =
+        traceSection("MediaCarouselController#onDesiredLocationChanged") {
+            desiredHostState?.let {
+                if (this.desiredLocation != desiredLocation) {
+                    // Only log an event when location changes
+                    logger.logCarouselPosition(desiredLocation)
+                }
+
+                // This is a hosting view, let's remeasure our players
+                this.desiredLocation = desiredLocation
+                this.desiredHostState = it
+                currentlyExpanded = it.expansion > 0
+
+                val shouldCloseGuts =
+                    !currentlyExpanded &&
+                        !mediaManager.hasActiveMediaOrRecommendation() &&
+                        desiredHostState.showsOnlyActiveMedia
+
+                for (mediaPlayer in MediaPlayerData.players()) {
+                    if (animate) {
+                        mediaPlayer.mediaViewController.animatePendingStateChange(
+                            duration = duration,
+                            delay = startDelay
+                        )
+                    }
+                    if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
+                        mediaPlayer.closeGuts(!animate)
+                    }
+
+                    mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
+                }
+                mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
+                mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
+                val nowVisible = it.visible
+                if (nowVisible != playersVisible) {
+                    playersVisible = nowVisible
+                    if (nowVisible) {
+                        mediaCarouselScrollHandler.resetTranslation()
+                    }
+                }
+                updateCarouselSize()
+            }
+        }
+
+    fun closeGuts(immediate: Boolean = true) {
+        MediaPlayerData.players().forEach { it.closeGuts(immediate) }
+    }
+
+    /** Update the size of the carousel, remeasuring it if necessary. */
+    private fun updateCarouselSize() {
+        val width = desiredHostState?.measurementInput?.width ?: 0
+        val height = desiredHostState?.measurementInput?.height ?: 0
+        if (
+            width != carouselMeasureWidth && width != 0 ||
+                height != carouselMeasureHeight && height != 0
+        ) {
+            carouselMeasureWidth = width
+            carouselMeasureHeight = height
+            val playerWidthPlusPadding =
+                carouselMeasureWidth +
+                    context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+            // Let's remeasure the carousel
+            val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
+            val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
+            mediaCarousel.measure(widthSpec, heightSpec)
+            mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
+            // Update the padding after layout; view widths are used in RTL to calculate scrollX
+            mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
+        }
+    }
+
+    /** Log the user impression for media card at visibleMediaIndex. */
+    fun logSmartspaceImpression(qsExpanded: Boolean) {
+        val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
+        if (MediaPlayerData.players().size > visibleMediaIndex) {
+            val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
+            val hasActiveMediaOrRecommendationCard =
+                MediaPlayerData.hasActiveMediaOrRecommendationCard()
+            if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
+                // Skip logging if on LS or QQS, and there is no active media card
+                return
+            }
+            mediaControlPanel?.let {
+                logSmartspaceCardReported(
+                    800, // SMARTSPACE_CARD_SEEN
+                    it.mSmartspaceId,
+                    it.mUid,
+                    intArrayOf(it.surfaceForSmartspaceLogging)
+                )
+                it.mIsImpressed = true
+            }
+        }
+    }
+
+    @JvmOverloads
+    /**
+     * Log Smartspace events
+     *
+     * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
+     * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
+     * instanceId
+     * @param uid uid for the application that media comes from
+     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
+     * the event happened
+     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
+     * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
+     * @param interactedSubcardCardinality how many media items were shown to the user when there is
+     * user interaction
+     * @param rank the rank for media card in the media carousel, starting from 0
+     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
+     * between headphone connection to sysUI displays media recommendation card
+     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
+     */
+    fun logSmartspaceCardReported(
+        eventId: Int,
+        instanceId: Int,
+        uid: Int,
+        surfaces: IntArray,
+        interactedSubcardRank: Int = 0,
+        interactedSubcardCardinality: Int = 0,
+        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
+        receivedLatencyMillis: Int = 0,
+        isSwipeToDismiss: Boolean = false
+    ) {
+        if (MediaPlayerData.players().size <= rank) {
+            return
+        }
+
+        val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
+        // Only log media resume card when Smartspace data is available
+        if (
+            !mediaControlKey.isSsMediaRec &&
+                !mediaManager.smartspaceMediaData.isActive &&
+                MediaPlayerData.smartspaceMediaData == null
+        ) {
+            return
+        }
+
+        val cardinality = mediaContent.getChildCount()
+        surfaces.forEach { surface ->
+            /* ktlint-disable max-line-length */
+            SysUiStatsLog.write(
+                SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
+                eventId,
+                instanceId,
+                // Deprecated, replaced with AiAi feature type so we don't need to create logging
+                // card type for each new feature.
+                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
+                surface,
+                // Use -1 as rank value to indicate user swipe to dismiss the card
+                if (isSwipeToDismiss) -1 else rank,
+                cardinality,
+                if (mediaControlKey.isSsMediaRec) 15 // MEDIA_RECOMMENDATION
+                else if (mediaControlKey.isSsReactivated) 43 // MEDIA_RESUME_SS_ACTIVATED
+                else 31, // MEDIA_RESUME
+                uid,
+                interactedSubcardRank,
+                interactedSubcardCardinality,
+                receivedLatencyMillis,
+                null, // Media cards cannot have subcards.
+                null // Media cards don't have dimensions today.
+            )
+            /* ktlint-disable max-line-length */
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Log Smartspace card event id: $eventId instance id: $instanceId" +
+                        " surface: $surface rank: $rank cardinality: $cardinality " +
+                        "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
+                        "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
+                        "uid: $uid " +
+                        "interactedSubcardRank: $interactedSubcardRank " +
+                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
+                        "received_latency_millis: $receivedLatencyMillis"
+                )
+            }
+        }
+    }
+
+    private fun onSwipeToDismiss() {
+        MediaPlayerData.players().forEachIndexed { index, it ->
+            if (it.mIsImpressed) {
+                logSmartspaceCardReported(
+                    SMARTSPACE_CARD_DISMISS_EVENT,
+                    it.mSmartspaceId,
+                    it.mUid,
+                    intArrayOf(it.surfaceForSmartspaceLogging),
+                    rank = index,
+                    isSwipeToDismiss = true
+                )
+                // Reset card impressed state when swipe to dismissed
+                it.mIsImpressed = false
+            }
+        }
+        logger.logSwipeDismiss()
+        mediaManager.onSwipeToDismiss()
+    }
+
+    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
+        return MediaPlayerData.playerKeys()
+            .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            ?.data
+            ?.clickIntent
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("keysNeedRemoval: $keysNeedRemoval")
+            println("dataKeys: ${MediaPlayerData.dataKeys()}")
+            println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
+            println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
+            println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
+            println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
+            println("current size: $currentCarouselWidth x $currentCarouselHeight")
+            println("location: $desiredLocation")
+            println(
+                "state: ${desiredHostState?.expansion}, " +
+                    "only active ${desiredHostState?.showsOnlyActiveMedia}"
+            )
+        }
+    }
+}
+
+@VisibleForTesting
+internal object MediaPlayerData {
+    private val EMPTY =
+        MediaData(
+            userId = -1,
+            initialized = false,
+            app = null,
+            appIcon = null,
+            artist = null,
+            song = null,
+            artwork = null,
+            actions = emptyList(),
+            actionsToShowInCompact = emptyList(),
+            packageName = "INVALID",
+            token = null,
+            clickIntent = null,
+            device = null,
+            active = true,
+            resumeAction = null,
+            instanceId = InstanceId.fakeInstanceId(-1),
+            appUid = -1
+        )
+    // Whether should prioritize Smartspace card.
+    internal var shouldPrioritizeSs: Boolean = false
+        private set
+    internal var smartspaceMediaData: SmartspaceMediaData? = null
+        private set
+
+    data class MediaSortKey(
+        val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
+        val data: MediaData,
+        val key: String,
+        val updateTime: Long = 0,
+        val isSsReactivated: Boolean = false
+    )
+
+    private val comparator =
+        compareByDescending<MediaSortKey> {
+                it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL
+            }
+            .thenByDescending {
+                it.data.isPlaying == true &&
+                    it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
+            }
+            .thenByDescending { it.data.active }
+            .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
+            .thenByDescending { !it.data.resumption }
+            .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
+            .thenByDescending { it.data.lastActive }
+            .thenByDescending { it.updateTime }
+            .thenByDescending { it.data.notificationKey }
+
+    private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
+    private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
+    // A map that tracks order of visible media players before they get reordered.
+    private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
+
+    fun addMediaPlayer(
+        key: String,
+        data: MediaData,
+        player: MediaControlPanel,
+        clock: SystemClock,
+        isSsReactivated: Boolean,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
+        val sortKey =
+            MediaSortKey(
+                isSsMediaRec = false,
+                data,
+                key,
+                clock.currentTimeMillis(),
+                isSsReactivated = isSsReactivated
+            )
+        mediaData.put(key, sortKey)
+        mediaPlayers.put(sortKey, player)
+        visibleMediaPlayers.put(key, sortKey)
+    }
+
+    fun addMediaRecommendation(
+        key: String,
+        data: SmartspaceMediaData,
+        player: MediaControlPanel,
+        shouldPrioritize: Boolean,
+        clock: SystemClock,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        shouldPrioritizeSs = shouldPrioritize
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
+        val sortKey =
+            MediaSortKey(
+                isSsMediaRec = true,
+                EMPTY.copy(isPlaying = false),
+                key,
+                clock.currentTimeMillis(),
+                isSsReactivated = true
+            )
+        mediaData.put(key, sortKey)
+        mediaPlayers.put(sortKey, player)
+        visibleMediaPlayers.put(key, sortKey)
+        smartspaceMediaData = data
+    }
+
+    fun moveIfExists(
+        oldKey: String?,
+        newKey: String,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        if (oldKey == null || oldKey == newKey) {
+            return
+        }
+
+        mediaData.remove(oldKey)?.let {
+            // MediaPlayer should not be visible
+            // no need to set isDismissed flag.
+            val removedPlayer = removeMediaPlayer(newKey)
+            removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
+            mediaData.put(newKey, it)
+        }
+    }
+
+    fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
+        return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
+    }
+
+    fun getMediaPlayer(key: String): MediaControlPanel? {
+        return mediaData.get(key)?.let { mediaPlayers.get(it) }
+    }
+
+    fun getMediaPlayerIndex(key: String): Int {
+        val sortKey = mediaData.get(key)
+        mediaPlayers.entries.forEachIndexed { index, e ->
+            if (e.key == sortKey) {
+                return index
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Removes media player given the key.
+     * @param isDismissed determines whether the media player is removed from the carousel.
+     */
+    fun removeMediaPlayer(key: String, isDismissed: Boolean = false) =
+        mediaData.remove(key)?.let {
+            if (it.isSsMediaRec) {
+                smartspaceMediaData = null
+            }
+            if (isDismissed) {
+                visibleMediaPlayers.remove(key)
+            }
+            mediaPlayers.remove(it)
+        }
+
+    fun mediaData() =
+        mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
+
+    fun dataKeys() = mediaData.keys
+
+    fun players() = mediaPlayers.values
+
+    fun playerKeys() = mediaPlayers.keys
+
+    fun visiblePlayerKeys() = visibleMediaPlayers.values
+
+    /** Returns the index of the first non-timeout media. */
+    fun firstActiveMediaIndex(): Int {
+        mediaPlayers.entries.forEachIndexed { index, e ->
+            if (!e.key.isSsMediaRec && e.key.data.active) {
+                return index
+            }
+        }
+        return -1
+    }
+
+    /** Returns the existing Smartspace target id. */
+    fun smartspaceMediaKey(): String? {
+        mediaData.entries.forEach { e ->
+            if (e.value.isSsMediaRec) {
+                return e.key
+            }
+        }
+        return null
+    }
+
+    @VisibleForTesting
+    fun clear() {
+        mediaData.clear()
+        mediaPlayers.clear()
+        visibleMediaPlayers.clear()
+    }
+
+    /* Returns true if there is active media player card or recommendation card */
+    fun hasActiveMediaOrRecommendationCard(): Boolean {
+        if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
+            return true
+        }
+        if (firstActiveMediaIndex() != -1) {
+            return true
+        }
+        return false
+    }
+
+    fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
+
+    /**
+     * This method is called when media players are reordered. To make sure we have the new version
+     * of the order of media players visible to user.
+     */
+    fun updateVisibleMediaPlayers() {
+        visibleMediaPlayers.clear()
+        playerKeys().forEach { visibleMediaPlayers.put(it.key, it) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
new file mode 100644
index 0000000..eed1bd7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaCarouselControllerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** A debug logger for [MediaCarouselController]. */
+@SysUISingleton
+class MediaCarouselControllerLogger
+@Inject
+constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) {
+    /**
+     * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
+     * [MediaViewController] related to [key].
+     */
+    fun logPotentialMemoryLeak(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            {
+                "Potential memory leak: " +
+                    "Removing control panel for $str1 from map without calling #onDestroy"
+            }
+        )
+
+    fun logMediaLoaded(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add player $str1" })
+
+    fun logMediaRemoved(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
+
+    fun logRecommendationLoaded(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
+
+    fun logRecommendationRemoved(key: String, immediately: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = immediately
+            },
+            { "removing recommendation $str1, immediate=$bool1" }
+        )
+}
+
+private const val TAG = "MediaCarouselCtlrLog"
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
new file mode 100644
index 0000000..36b2eda
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.graphics.Outline
+import android.util.MathUtils
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import androidx.core.view.GestureDetectorCompat
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.settingslib.Utils
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.R
+import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PageIndicator
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.wm.shell.animation.PhysicsAnimator
+
+private const val FLING_SLOP = 1000000
+private const val DISMISS_DELAY = 100L
+private const val SCROLL_DELAY = 100L
+private const val RUBBERBAND_FACTOR = 0.2f
+private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
+
+/**
+ * Default spring configuration to use for animations where stiffness and/or damping ratio were not
+ * provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
+ */
+private val translationConfig =
+    PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+
+/** A controller class for the media scrollview, responsible for touch handling */
+class MediaCarouselScrollHandler(
+    private val scrollView: MediaScrollView,
+    private val pageIndicator: PageIndicator,
+    private val mainExecutor: DelayableExecutor,
+    val dismissCallback: () -> Unit,
+    private var translationChangedListener: () -> Unit,
+    private val closeGuts: (immediate: Boolean) -> Unit,
+    private val falsingCollector: FalsingCollector,
+    private val falsingManager: FalsingManager,
+    private val logSmartspaceImpression: (Boolean) -> Unit,
+    private val logger: MediaUiEventLogger
+) {
+    /** Is the view in RTL */
+    val isRtl: Boolean
+        get() = scrollView.isLayoutRtl
+    /** Do we need falsing protection? */
+    var falsingProtectionNeeded: Boolean = false
+    /** The width of the carousel */
+    private var carouselWidth: Int = 0
+
+    /** The height of the carousel */
+    private var carouselHeight: Int = 0
+
+    /** How much are we scrolled into the current media? */
+    private var cornerRadius: Int = 0
+
+    /** The content where the players are added */
+    private var mediaContent: ViewGroup
+    /** The gesture detector to detect touch gestures */
+    private val gestureDetector: GestureDetectorCompat
+
+    /** The settings button view */
+    private lateinit var settingsButton: View
+
+    /** What's the currently visible player index? */
+    var visibleMediaIndex: Int = 0
+        private set
+
+    /** How much are we scrolled into the current media? */
+    private var scrollIntoCurrentMedia: Int = 0
+
+    /** how much is the content translated in X */
+    var contentTranslation = 0.0f
+        private set(value) {
+            field = value
+            mediaContent.translationX = value
+            updateSettingsPresentation()
+            translationChangedListener.invoke()
+            updateClipToOutline()
+        }
+
+    /** The width of a player including padding */
+    var playerWidthPlusPadding: Int = 0
+        set(value) {
+            field = value
+            // The player width has changed, let's update the scroll position to make sure
+            // it's still at the same place
+            var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding
+            if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
+                newRelativeScroll +=
+                    playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding)
+            } else {
+                newRelativeScroll += scrollIntoCurrentMedia
+            }
+            scrollView.relativeScrollX = newRelativeScroll
+        }
+
+    /** Does the dismiss currently show the setting cog? */
+    var showsSettingsButton: Boolean = false
+
+    /** A utility to detect gestures, used in the touch listener */
+    private val gestureListener =
+        object : GestureDetector.SimpleOnGestureListener() {
+            override fun onFling(
+                eStart: MotionEvent?,
+                eCurrent: MotionEvent?,
+                vX: Float,
+                vY: Float
+            ) = onFling(vX, vY)
+
+            override fun onScroll(
+                down: MotionEvent?,
+                lastMotion: MotionEvent?,
+                distanceX: Float,
+                distanceY: Float
+            ) = onScroll(down!!, lastMotion!!, distanceX)
+
+            override fun onDown(e: MotionEvent?): Boolean {
+                if (falsingProtectionNeeded) {
+                    falsingCollector.onNotificationStartDismissing()
+                }
+                return false
+            }
+        }
+
+    /** The touch listener for the scroll view */
+    private val touchListener =
+        object : Gefingerpoken {
+            override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
+            override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
+        }
+
+    /** A listener that is invoked when the scrolling changes to update player visibilities */
+    private val scrollChangedListener =
+        object : View.OnScrollChangeListener {
+            override fun onScrollChange(
+                v: View?,
+                scrollX: Int,
+                scrollY: Int,
+                oldScrollX: Int,
+                oldScrollY: Int
+            ) {
+                if (playerWidthPlusPadding == 0) {
+                    return
+                }
+
+                val relativeScrollX = scrollView.relativeScrollX
+                onMediaScrollingChanged(
+                    relativeScrollX / playerWidthPlusPadding,
+                    relativeScrollX % playerWidthPlusPadding
+                )
+            }
+        }
+
+    /** Whether the media card is visible to user if any */
+    var visibleToUser: Boolean = false
+
+    /** Whether the quick setting is expanded or not */
+    var qsExpanded: Boolean = false
+
+    init {
+        gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener)
+        scrollView.touchListener = touchListener
+        scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
+        mediaContent = scrollView.contentContainer
+        scrollView.setOnScrollChangeListener(scrollChangedListener)
+        scrollView.outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View?, outline: Outline?) {
+                    outline?.setRoundRect(
+                        0,
+                        0,
+                        carouselWidth,
+                        carouselHeight,
+                        cornerRadius.toFloat()
+                    )
+                }
+            }
+    }
+
+    fun onSettingsButtonUpdated(button: View) {
+        settingsButton = button
+        // We don't have a context to resolve, lets use the settingsbuttons one since that is
+        // reinflated appropriately
+        cornerRadius =
+            settingsButton.resources.getDimensionPixelSize(
+                Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)
+            )
+        updateSettingsPresentation()
+        scrollView.invalidateOutline()
+    }
+
+    private fun updateSettingsPresentation() {
+        if (showsSettingsButton && settingsButton.width > 0) {
+            val settingsOffset =
+                MathUtils.map(
+                    0.0f,
+                    getMaxTranslation().toFloat(),
+                    0.0f,
+                    1.0f,
+                    Math.abs(contentTranslation)
+                )
+            val settingsTranslation =
+                (1.0f - settingsOffset) *
+                    -settingsButton.width *
+                    SETTINGS_BUTTON_TRANSLATION_FRACTION
+            val newTranslationX =
+                if (isRtl) {
+                    // In RTL, the 0-placement is on the right side of the view, not the left...
+                    if (contentTranslation > 0) {
+                        -(scrollView.width - settingsTranslation - settingsButton.width)
+                    } else {
+                        -settingsTranslation
+                    }
+                } else {
+                    if (contentTranslation > 0) {
+                        settingsTranslation
+                    } else {
+                        scrollView.width - settingsTranslation - settingsButton.width
+                    }
+                }
+            val rotation = (1.0f - settingsOffset) * 50
+            settingsButton.rotation = rotation * -Math.signum(contentTranslation)
+            val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset))
+            settingsButton.alpha = alpha
+            settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE
+            settingsButton.translationX = newTranslationX
+            settingsButton.translationY = (scrollView.height - settingsButton.height) / 2.0f
+        } else {
+            settingsButton.visibility = View.INVISIBLE
+        }
+    }
+
+    private fun onTouch(motionEvent: MotionEvent): Boolean {
+        val isUp = motionEvent.action == MotionEvent.ACTION_UP
+        if (isUp && falsingProtectionNeeded) {
+            falsingCollector.onNotificationStopDismissing()
+        }
+        if (gestureDetector.onTouchEvent(motionEvent)) {
+            if (isUp) {
+                // If this is an up and we're flinging, we don't want to have this touch reach
+                // the view, otherwise that would scroll, while we are trying to snap to the
+                // new page. Let's dispatch a cancel instead.
+                scrollView.cancelCurrentScroll()
+                return true
+            } else {
+                // Pass touches to the scrollView
+                return false
+            }
+        }
+        if (motionEvent.action == MotionEvent.ACTION_MOVE) {
+            // cancel on going animation if there is any.
+            PhysicsAnimator.getInstance(this).cancel()
+        } else if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
+            // It's an up and the fling didn't take it above
+            val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
+            val scrollXAmount: Int
+            if (relativePos > playerWidthPlusPadding / 2) {
+                scrollXAmount = playerWidthPlusPadding - relativePos
+            } else {
+                scrollXAmount = -1 * relativePos
+            }
+            if (scrollXAmount != 0) {
+                val dx = if (isRtl) -scrollXAmount else scrollXAmount
+                val newScrollX = scrollView.relativeScrollX + dx
+                // Delay the scrolling since scrollView calls springback which cancels
+                // the animation again..
+                mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) }
+            }
+            val currentTranslation = scrollView.getContentTranslation()
+            if (currentTranslation != 0.0f) {
+                // We started a Swipe but didn't end up with a fling. Let's either go to the
+                // dismissed position or go back.
+                val springBack =
+                    Math.abs(currentTranslation) < getMaxTranslation() / 2 || isFalseTouch()
+                val newTranslation: Float
+                if (springBack) {
+                    newTranslation = 0.0f
+                } else {
+                    newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
+                    if (!showsSettingsButton) {
+                        // Delay the dismiss a bit to avoid too much overlap. Waiting until the
+                        // animation has finished also feels a bit too slow here.
+                        mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
+                    }
+                }
+                PhysicsAnimator.getInstance(this)
+                    .spring(
+                        CONTENT_TRANSLATION,
+                        newTranslation,
+                        startVelocity = 0.0f,
+                        config = translationConfig
+                    )
+                    .start()
+                scrollView.animationTargetX = newTranslation
+            }
+        }
+        // Always pass touches to the scrollView
+        return false
+    }
+
+    private fun isFalseTouch() =
+        falsingProtectionNeeded && falsingManager.isFalseTouch(NOTIFICATION_DISMISS)
+
+    private fun getMaxTranslation() =
+        if (showsSettingsButton) {
+            settingsButton.width
+        } else {
+            playerWidthPlusPadding
+        }
+
+    private fun onInterceptTouch(motionEvent: MotionEvent): Boolean {
+        return gestureDetector.onTouchEvent(motionEvent)
+    }
+
+    fun onScroll(down: MotionEvent, lastMotion: MotionEvent, distanceX: Float): Boolean {
+        val totalX = lastMotion.x - down.x
+        val currentTranslation = scrollView.getContentTranslation()
+        if (currentTranslation != 0.0f || !scrollView.canScrollHorizontally((-totalX).toInt())) {
+            var newTranslation = currentTranslation - distanceX
+            val absTranslation = Math.abs(newTranslation)
+            if (absTranslation > getMaxTranslation()) {
+                // Rubberband all translation above the maximum
+                if (Math.signum(distanceX) != Math.signum(currentTranslation)) {
+                    // The movement is in the same direction as our translation,
+                    // Let's rubberband it.
+                    if (Math.abs(currentTranslation) > getMaxTranslation()) {
+                        // we were already overshooting before. Let's add the distance
+                        // fully rubberbanded.
+                        newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
+                    } else {
+                        // We just crossed the boundary, let's rubberband it all
+                        newTranslation =
+                            Math.signum(newTranslation) *
+                                (getMaxTranslation() +
+                                    (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
+                    }
+                } // Otherwise we don't have do do anything, and will remove the unrubberbanded
+                // translation
+            }
+            if (
+                Math.signum(newTranslation) != Math.signum(currentTranslation) &&
+                    currentTranslation != 0.0f
+            ) {
+                // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
+                // to scroll into the new direction
+                if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
+                    // We can actually scroll in the direction where we want to translate,
+                    // Let's make sure to stop at 0
+                    newTranslation = 0.0f
+                }
+            }
+            val physicsAnimator = PhysicsAnimator.getInstance(this)
+            if (physicsAnimator.isRunning()) {
+                physicsAnimator
+                    .spring(
+                        CONTENT_TRANSLATION,
+                        newTranslation,
+                        startVelocity = 0.0f,
+                        config = translationConfig
+                    )
+                    .start()
+            } else {
+                contentTranslation = newTranslation
+            }
+            scrollView.animationTargetX = newTranslation
+            return true
+        }
+        return false
+    }
+
+    private fun onFling(vX: Float, vY: Float): Boolean {
+        if (vX * vX < 0.5 * vY * vY) {
+            return false
+        }
+        if (vX * vX < FLING_SLOP) {
+            return false
+        }
+        val currentTranslation = scrollView.getContentTranslation()
+        if (currentTranslation != 0.0f) {
+            // We're translated and flung. Let's see if the fling is in the same direction
+            val newTranslation: Float
+            if (Math.signum(vX) != Math.signum(currentTranslation) || isFalseTouch()) {
+                // The direction of the fling isn't the same as the translation, let's go to 0
+                newTranslation = 0.0f
+            } else {
+                newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
+                // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
+                // has finished also feels a bit too slow here.
+                if (!showsSettingsButton) {
+                    mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
+                }
+            }
+            PhysicsAnimator.getInstance(this)
+                .spring(
+                    CONTENT_TRANSLATION,
+                    newTranslation,
+                    startVelocity = vX,
+                    config = translationConfig
+                )
+                .start()
+            scrollView.animationTargetX = newTranslation
+        } else {
+            // We're flinging the player! Let's go either to the previous or to the next player
+            val pos = scrollView.relativeScrollX
+            val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
+            val flungTowardEnd = if (isRtl) vX > 0 else vX < 0
+            var destIndex = if (flungTowardEnd) currentIndex + 1 else currentIndex
+            destIndex = Math.max(0, destIndex)
+            destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
+            val view = mediaContent.getChildAt(destIndex)
+            // We need to post this since we're dispatching a touch to the underlying view to cancel
+            // but canceling will actually abort the animation.
+            mainExecutor.execute { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }
+        }
+        return true
+    }
+
+    /** Reset the translation of the players when swiped */
+    fun resetTranslation(animate: Boolean = false) {
+        if (scrollView.getContentTranslation() != 0.0f) {
+            if (animate) {
+                PhysicsAnimator.getInstance(this)
+                    .spring(CONTENT_TRANSLATION, 0.0f, config = translationConfig)
+                    .start()
+                scrollView.animationTargetX = 0.0f
+            } else {
+                PhysicsAnimator.getInstance(this).cancel()
+                contentTranslation = 0.0f
+            }
+        }
+    }
+
+    private fun updateClipToOutline() {
+        val clip = contentTranslation != 0.0f || scrollIntoCurrentMedia != 0
+        scrollView.clipToOutline = clip
+    }
+
+    private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
+        val wasScrolledIn = scrollIntoCurrentMedia != 0
+        scrollIntoCurrentMedia = scrollInAmount
+        val nowScrolledIn = scrollIntoCurrentMedia != 0
+        if (newIndex != visibleMediaIndex || wasScrolledIn != nowScrolledIn) {
+            val oldIndex = visibleMediaIndex
+            visibleMediaIndex = newIndex
+            if (oldIndex != visibleMediaIndex && visibleToUser) {
+                logSmartspaceImpression(qsExpanded)
+                logger.logMediaCarouselPage(newIndex)
+            }
+            closeGuts(false)
+            updatePlayerVisibilities()
+        }
+        val relativeLocation =
+            visibleMediaIndex.toFloat() +
+                if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding
+                else 0f
+        // Fix the location, because PageIndicator does not handle RTL internally
+        val location =
+            if (isRtl) {
+                mediaContent.childCount - relativeLocation - 1
+            } else {
+                relativeLocation
+            }
+        pageIndicator.setLocation(location)
+        updateClipToOutline()
+    }
+
+    /** Notified whenever the players or their order has changed */
+    fun onPlayersChanged() {
+        updatePlayerVisibilities()
+        updateMediaPaddings()
+    }
+
+    private fun updateMediaPaddings() {
+        val padding = scrollView.context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+        val childCount = mediaContent.childCount
+        for (i in 0 until childCount) {
+            val mediaView = mediaContent.getChildAt(i)
+            val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
+            val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
+            if (layoutParams.marginEnd != desiredPaddingEnd) {
+                layoutParams.marginEnd = desiredPaddingEnd
+                mediaView.layoutParams = layoutParams
+            }
+        }
+    }
+
+    private fun updatePlayerVisibilities() {
+        val scrolledIn = scrollIntoCurrentMedia != 0
+        for (i in 0 until mediaContent.childCount) {
+            val view = mediaContent.getChildAt(i)
+            val visible = (i == visibleMediaIndex) || ((i == (visibleMediaIndex + 1)) && scrolledIn)
+            view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
+        }
+    }
+
+    /**
+     * Notify that a player will be removed right away. This gives us the opporunity to look where
+     * it was and update our scroll position.
+     */
+    fun onPrePlayerRemoved(removed: MediaControlPanel) {
+        val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player)
+        // If the removed index is less than the visibleMediaIndex, then we need to decrement it.
+        // RTL has no effect on this, because indices are always relative (start-to-end).
+        // Update the index 'manually' since we won't always get a call to onMediaScrollingChanged
+        val beforeActive = removedIndex <= visibleMediaIndex
+        if (beforeActive) {
+            visibleMediaIndex = Math.max(0, visibleMediaIndex - 1)
+        }
+        // If the removed media item is "left of" the active one (in an absolute sense), we need to
+        // scroll the view to keep that player in view.  This is because scroll position is always
+        // calculated from left to right.
+        val leftOfActive = if (isRtl) !beforeActive else beforeActive
+        if (leftOfActive) {
+            scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0)
+        }
+    }
+
+    /** Update the bounds of the carousel */
+    fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
+        if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
+            carouselWidth = currentCarouselWidth
+            carouselHeight = currentCarouselHeight
+            scrollView.invalidateOutline()
+        }
+    }
+
+    /** Reset the MediaScrollView to the start. */
+    fun scrollToStart() {
+        scrollView.relativeScrollX = 0
+    }
+
+    /**
+     * Smooth scroll to the destination player.
+     *
+     * @param sourceIndex optional source index to indicate where the scroll should begin.
+     * @param destIndex destination index to indicate where the scroll should end.
+     */
+    fun scrollToPlayer(sourceIndex: Int = -1, destIndex: Int) {
+        if (sourceIndex >= 0 && sourceIndex < mediaContent.childCount) {
+            scrollView.relativeScrollX = sourceIndex * playerWidthPlusPadding
+        }
+        val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
+        val view = mediaContent.getChildAt(destIndex)
+        // We need to post this to wait for the active player becomes visible.
+        mainExecutor.executeDelayed(
+            { scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
+            SCROLL_DELAY
+        )
+    }
+
+    companion object {
+        private val CONTENT_TRANSLATION =
+            object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
+                override fun getValue(handler: MediaCarouselScrollHandler): Float {
+                    return handler.contentTranslation
+                }
+
+                override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
+                    handler.contentTranslation = value
+                }
+            }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
new file mode 100644
index 0000000..82abf9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.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.media.controls.ui
+
+import com.android.systemui.monet.ColorScheme
+
+/** Returns the surface color for media controls based on the scheme. */
+internal fun surfaceFromScheme(scheme: ColorScheme) = scheme.accent2[9] // A2-800
+
+/** Returns the primary accent color for media controls based on the scheme. */
+internal fun accentPrimaryFromScheme(scheme: ColorScheme) = scheme.accent1[2] // A1-100
+
+/** Returns the secondary accent color for media controls based on the scheme. */
+internal fun accentSecondaryFromScheme(scheme: ColorScheme) = scheme.accent1[3] // A1-200
+
+/** Returns the primary text color for media controls based on the scheme. */
+internal fun textPrimaryFromScheme(scheme: ColorScheme) = scheme.neutral1[1] // N1-50
+
+/** Returns the inverse of the primary text color for media controls based on the scheme. */
+internal fun textPrimaryInverseFromScheme(scheme: ColorScheme) = scheme.neutral1[10] // N1-900
+
+/** Returns the secondary text color for media controls based on the scheme. */
+internal fun textSecondaryFromScheme(scheme: ColorScheme) = scheme.neutral2[3] // N2-200
+
+/** Returns the tertiary text color for media controls based on the scheme. */
+internal fun textTertiaryFromScheme(scheme: ColorScheme) = scheme.neutral2[5] // N2-400
+
+/** Returns the color for the start of the background gradient based on the scheme. */
+internal fun backgroundStartFromScheme(scheme: ColorScheme) = scheme.accent2[8] // A2-700
+
+/** Returns the color for the end of the background gradient based on the scheme. */
+internal fun backgroundEndFromScheme(scheme: ColorScheme) = scheme.accent1[8] // A1-700
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
new file mode 100644
index 0000000..21e64e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -0,0 +1,1603 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui;
+
+import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
+
+import static com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorSet;
+import android.app.PendingIntent;
+import android.app.WallpaperColors;
+import android.app.smartspace.SmartspaceAction;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.graphics.BlendMode;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Process;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.constraintlayout.widget.ConstraintSet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.ColorUtils;
+import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.InstanceId;
+import com.android.settingslib.widget.AdaptiveIcon;
+import com.android.systemui.ActivityIntentHelper;
+import com.android.systemui.R;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
+import com.android.systemui.animation.Interpolators;
+import com.android.systemui.bluetooth.BroadcastDialogController;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.media.controls.models.GutsViewHolder;
+import com.android.systemui.media.controls.models.player.MediaAction;
+import com.android.systemui.media.controls.models.player.MediaButton;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.player.MediaDeviceData;
+import com.android.systemui.media.controls.models.player.MediaViewHolder;
+import com.android.systemui.media.controls.models.player.SeekBarObserver;
+import com.android.systemui.media.controls.models.player.SeekBarViewModel;
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.util.MediaDataUtils;
+import com.android.systemui.media.controls.util.MediaUiEventLogger;
+import com.android.systemui.media.controls.util.SmallHash;
+import com.android.systemui.media.dialog.MediaOutputDialogFactory;
+import com.android.systemui.monet.ColorScheme;
+import com.android.systemui.monet.Style;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController;
+import com.android.systemui.surfaceeffects.ripple.MultiRippleView;
+import com.android.systemui.surfaceeffects.ripple.RippleAnimation;
+import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
+import com.android.systemui.surfaceeffects.ripple.RippleShader;
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig;
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController;
+import com.android.systemui.util.ColorUtilKt;
+import com.android.systemui.util.animation.TransitionLayout;
+import com.android.systemui.util.time.SystemClock;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import dagger.Lazy;
+import kotlin.Unit;
+
+/**
+ * A view controller used for Media Playback.
+ */
+public class MediaControlPanel {
+    protected static final String TAG = "MediaControlPanel";
+
+    private static final float DISABLED_ALPHA = 0.38f;
+    private static final String EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = "com.google"
+            + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity";
+    private static final String EXTRAS_SMARTSPACE_INTENT =
+            "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
+    private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
+    private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
+
+    // Event types logged by smartspace
+    private static final int SMARTSPACE_CARD_CLICK_EVENT = 760;
+    protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761;
+
+    private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
+
+    // Buttons to show in small player when using semantic actions
+    private static final List<Integer> SEMANTIC_ACTIONS_COMPACT = List.of(
+            R.id.actionPlayPause,
+            R.id.actionPrev,
+            R.id.actionNext
+    );
+
+    // Buttons that should get hidden when we're scrubbing (they will be replaced with the views
+    // showing scrubbing time)
+    private static final List<Integer> SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = List.of(
+            R.id.actionPrev,
+            R.id.actionNext
+    );
+
+    // Buttons to show in small player when using semantic actions
+    private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
+            R.id.actionPlayPause,
+            R.id.actionPrev,
+            R.id.actionNext,
+            R.id.action0,
+            R.id.action1
+    );
+
+    private final SeekBarViewModel mSeekBarViewModel;
+    private SeekBarObserver mSeekBarObserver;
+    protected final Executor mBackgroundExecutor;
+    private final Executor mMainExecutor;
+    private final ActivityStarter mActivityStarter;
+    private final BroadcastSender mBroadcastSender;
+
+    private Context mContext;
+    private MediaViewHolder mMediaViewHolder;
+    private RecommendationViewHolder mRecommendationViewHolder;
+    private String mKey;
+    private MediaData mMediaData;
+    private SmartspaceMediaData mRecommendationData;
+    private MediaViewController mMediaViewController;
+    private MediaSession.Token mToken;
+    private MediaController mController;
+    private Lazy<MediaDataManager> mMediaDataManagerLazy;
+    // Uid for the media app.
+    protected int mUid = Process.INVALID_UID;
+    private int mSmartspaceMediaItemsCount;
+    private MediaCarouselController mMediaCarouselController;
+    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final FalsingManager mFalsingManager;
+    private MetadataAnimationHandler mMetadataAnimationHandler;
+    private ColorSchemeTransition mColorSchemeTransition;
+    private Drawable mPrevArtwork = null;
+    private boolean mIsArtworkBound = false;
+    private int mArtworkBoundId = 0;
+    private int mArtworkNextBindRequestId = 0;
+
+    private final KeyguardStateController mKeyguardStateController;
+    private final ActivityIntentHelper mActivityIntentHelper;
+    private final NotificationLockscreenUserManager mLockscreenUserManager;
+
+    // Used for logging.
+    protected boolean mIsImpressed = false;
+    private SystemClock mSystemClock;
+    private MediaUiEventLogger mLogger;
+    private InstanceId mInstanceId;
+    protected int mSmartspaceId = -1;
+    private String mPackageName;
+
+    private boolean mIsScrubbing = false;
+    private boolean mIsSeekBarEnabled = false;
+
+    private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
+            this::setIsScrubbing;
+    private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
+            this::setIsSeekBarEnabled;
+
+    private final BroadcastDialogController mBroadcastDialogController;
+    private boolean mIsCurrentBroadcastedApp = false;
+    private boolean mShowBroadcastDialogButton = false;
+    private String mSwitchBroadcastApp;
+    private MultiRippleController mMultiRippleController;
+    private TurbulenceNoiseController mTurbulenceNoiseController;
+    private FeatureFlags mFeatureFlags;
+    private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null;
+
+    /**
+     * Initialize a new control panel
+     *
+     * @param backgroundExecutor background executor, used for processing artwork
+     * @param mainExecutor main thread executor, used if we receive callbacks on the background
+     *                     thread that then trigger UI changes.
+     * @param activityStarter    activity starter
+     */
+    @Inject
+    public MediaControlPanel(
+            Context context,
+            @Background Executor backgroundExecutor,
+            @Main Executor mainExecutor,
+            ActivityStarter activityStarter,
+            BroadcastSender broadcastSender,
+            MediaViewController mediaViewController,
+            SeekBarViewModel seekBarViewModel,
+            Lazy<MediaDataManager> lazyMediaDataManager,
+            MediaOutputDialogFactory mediaOutputDialogFactory,
+            MediaCarouselController mediaCarouselController,
+            FalsingManager falsingManager,
+            SystemClock systemClock,
+            MediaUiEventLogger logger,
+            KeyguardStateController keyguardStateController,
+            ActivityIntentHelper activityIntentHelper,
+            NotificationLockscreenUserManager lockscreenUserManager,
+            BroadcastDialogController broadcastDialogController,
+            FeatureFlags featureFlags
+    ) {
+        mContext = context;
+        mBackgroundExecutor = backgroundExecutor;
+        mMainExecutor = mainExecutor;
+        mActivityStarter = activityStarter;
+        mBroadcastSender = broadcastSender;
+        mSeekBarViewModel = seekBarViewModel;
+        mMediaViewController = mediaViewController;
+        mMediaDataManagerLazy = lazyMediaDataManager;
+        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mMediaCarouselController = mediaCarouselController;
+        mFalsingManager = falsingManager;
+        mSystemClock = systemClock;
+        mLogger = logger;
+        mKeyguardStateController = keyguardStateController;
+        mActivityIntentHelper = activityIntentHelper;
+        mLockscreenUserManager = lockscreenUserManager;
+        mBroadcastDialogController = broadcastDialogController;
+
+        mSeekBarViewModel.setLogSeek(() -> {
+            if (mPackageName != null && mInstanceId != null) {
+                mLogger.logSeek(mUid, mPackageName, mInstanceId);
+            }
+            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+            return Unit.INSTANCE;
+        });
+
+        mFeatureFlags = featureFlags;
+    }
+
+    /**
+     * Clean up seekbar and controller when panel is destroyed
+     */
+    public void onDestroy() {
+        if (mSeekBarObserver != null) {
+            mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
+        }
+        mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
+        mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener);
+        mSeekBarViewModel.onDestroy();
+        mMediaViewController.onDestroy();
+    }
+
+    /**
+     * Get the view holder used to display media controls.
+     *
+     * @return the media view holder
+     */
+    @Nullable
+    public MediaViewHolder getMediaViewHolder() {
+        return mMediaViewHolder;
+    }
+
+    /**
+     * Get the recommendation view holder used to display Smartspace media recs.
+     * @return the recommendation view holder
+     */
+    @Nullable
+    public RecommendationViewHolder getRecommendationViewHolder() {
+        return mRecommendationViewHolder;
+    }
+
+    /**
+     * Get the view controller used to display media controls
+     *
+     * @return the media view controller
+     */
+    @NonNull
+    public MediaViewController getMediaViewController() {
+        return mMediaViewController;
+    }
+
+    /**
+     * Sets the listening state of the player.
+     * <p>
+     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
+     * unnecessary work when the QS panel is closed.
+     *
+     * @param listening True when player should be active. Otherwise, false.
+     */
+    public void setListening(boolean listening) {
+        mSeekBarViewModel.setListening(listening);
+    }
+
+    /** Sets whether the user is touching the seek bar to change the track position. */
+    private void setIsScrubbing(boolean isScrubbing) {
+        if (mMediaData == null || mMediaData.getSemanticActions() == null) {
+            return;
+        }
+        if (isScrubbing == this.mIsScrubbing) {
+            return;
+        }
+        this.mIsScrubbing = isScrubbing;
+        mMainExecutor.execute(() ->
+                updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
+    }
+
+    private void setIsSeekBarEnabled(boolean isSeekBarEnabled) {
+        if (isSeekBarEnabled == this.mIsSeekBarEnabled) {
+            return;
+        }
+        this.mIsSeekBarEnabled = isSeekBarEnabled;
+        updateSeekBarVisibility();
+    }
+
+    /**
+     * Get the context
+     *
+     * @return context
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    /** Attaches the player to the player view holder. */
+    public void attachPlayer(MediaViewHolder vh) {
+        mMediaViewHolder = vh;
+        TransitionLayout player = vh.getPlayer();
+
+        mSeekBarObserver = new SeekBarObserver(vh);
+        mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
+        mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
+        mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
+        mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener);
+        mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
+
+        vh.getPlayer().setOnLongClickListener(v -> {
+            if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
+            if (!mMediaViewController.isGutsVisible()) {
+                openGuts();
+                return true;
+            } else {
+                closeGuts();
+                return true;
+            }
+        });
+
+        // AlbumView uses a hardware layer so that clipping of the foreground is handled
+        // with clipping the album art. Otherwise album art shows through at the edges.
+        mMediaViewHolder.getAlbumView().setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+        TextView titleText = mMediaViewHolder.getTitleText();
+        TextView artistText = mMediaViewHolder.getArtistText();
+        AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
+                Interpolators.EMPHASIZED_DECELERATE, titleText, artistText);
+        AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
+                Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText);
+
+        MultiRippleView multiRippleView = vh.getMultiRippleView();
+        mMultiRippleController = new MultiRippleController(multiRippleView);
+        mTurbulenceNoiseController = new TurbulenceNoiseController(vh.getTurbulenceNoiseView());
+        multiRippleView.addRipplesFinishedListener(
+                () -> {
+                    if (mTurbulenceNoiseAnimationConfig == null) {
+                        mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation();
+                    }
+                    // Color will be correctly updated in ColorSchemeTransition.
+                    mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig);
+                }
+        );
+        mColorSchemeTransition = new ColorSchemeTransition(
+                mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController);
+        mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
+    }
+
+    @VisibleForTesting
+    protected AnimatorSet loadAnimator(int animId, Interpolator motionInterpolator,
+            View... targets) {
+        ArrayList<Animator> animators = new ArrayList<>();
+        for (View target : targets) {
+            AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(mContext, animId);
+            animator.getChildAnimations().get(0).setInterpolator(motionInterpolator);
+            animator.setTarget(target);
+            animators.add(animator);
+        }
+
+        AnimatorSet result = new AnimatorSet();
+        result.playTogether(animators);
+        return result;
+    }
+
+    /** Attaches the recommendations to the recommendation view holder. */
+    public void attachRecommendation(RecommendationViewHolder vh) {
+        mRecommendationViewHolder = vh;
+        TransitionLayout recommendations = vh.getRecommendations();
+
+        mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
+
+        mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> {
+            if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
+            if (!mMediaViewController.isGutsVisible()) {
+                openGuts();
+                return true;
+            } else {
+                closeGuts();
+                return true;
+            }
+        });
+    }
+
+    /** Bind this player view based on the data given. */
+    public void bindPlayer(@NonNull MediaData data, String key) {
+        if (mMediaViewHolder == null) {
+            return;
+        }
+        Trace.beginSection("MediaControlPanel#bindPlayer<" + key + ">");
+        mKey = key;
+        mMediaData = data;
+        MediaSession.Token token = data.getToken();
+        mPackageName = data.getPackageName();
+        mUid = data.getAppUid();
+        // Only assigns instance id if it's unassigned.
+        if (mSmartspaceId == -1) {
+            mSmartspaceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
+        }
+        mInstanceId = data.getInstanceId();
+
+        if (mToken == null || !mToken.equals(token)) {
+            mToken = token;
+        }
+
+        if (mToken != null) {
+            mController = new MediaController(mContext, mToken);
+        } else {
+            mController = null;
+        }
+
+        // Click action
+        PendingIntent clickIntent = data.getClickIntent();
+        if (clickIntent != null) {
+            mMediaViewHolder.getPlayer().setOnClickListener(v -> {
+                if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+                if (mMediaViewController.isGutsVisible()) return;
+                mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
+                logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+
+                // See StatusBarNotificationActivityStarter#onNotificationClicked
+                boolean showOverLockscreen = mKeyguardStateController.isShowing()
+                        && mActivityIntentHelper.wouldShowOverLockscreen(clickIntent.getIntent(),
+                        mLockscreenUserManager.getCurrentUserId());
+
+                if (showOverLockscreen) {
+                    mActivityStarter.startActivity(clickIntent.getIntent(),
+                            /* dismissShade */ true,
+                            /* animationController */ null,
+                            /* showOverLockscreenWhenLocked */ true);
+                } else {
+                    mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
+                            buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
+                }
+            });
+        }
+
+        // Seek Bar
+        final MediaController controller = getController();
+        mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
+
+        // Show the broadcast dialog button only when the le audio is enabled.
+        mShowBroadcastDialogButton =
+                data.getDevice() != null && data.getDevice().getShowBroadcastButton();
+        bindOutputSwitcherAndBroadcastButton(mShowBroadcastDialogButton, data);
+        bindGutsMenuForPlayer(data);
+        bindPlayerContentDescription(data);
+        bindScrubbingTime(data);
+        bindActionButtons(data);
+
+        boolean isSongUpdated = bindSongMetadata(data);
+        bindArtworkAndColors(data, key, isSongUpdated);
+
+        // TODO: We don't need to refresh this state constantly, only if the state actually changed
+        // to something which might impact the measurement
+        // State refresh interferes with the translation animation, only run it if it's not running.
+        if (!mMetadataAnimationHandler.isRunning()) {
+            mMediaViewController.refreshState();
+        }
+        Trace.endSection();
+    }
+
+    private void bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data) {
+        ViewGroup seamlessView = mMediaViewHolder.getSeamless();
+        seamlessView.setVisibility(View.VISIBLE);
+        ImageView iconView = mMediaViewHolder.getSeamlessIcon();
+        TextView deviceName = mMediaViewHolder.getSeamlessText();
+        final MediaDeviceData device = data.getDevice();
+
+        final boolean isTapEnabled;
+        final boolean useDisabledAlpha;
+        final int iconResource;
+        CharSequence deviceString;
+        if (showBroadcastButton) {
+            // TODO(b/233698402): Use the package name instead of app label to avoid the
+            // unexpected result.
+            mIsCurrentBroadcastedApp = device != null
+                    && TextUtils.equals(device.getName(),
+                    MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString(
+                            R.string.bt_le_audio_broadcast_dialog_unknown_name)));
+            useDisabledAlpha = !mIsCurrentBroadcastedApp;
+            // Always be enabled if the broadcast button is shown
+            isTapEnabled = true;
+
+            // Defaults for broadcasting state
+            deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
+            iconResource = R.drawable.settings_input_antenna;
+        } else {
+            // Disable clicking on output switcher for invalid devices and resumption controls
+            useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption();
+            isTapEnabled = !useDisabledAlpha;
+
+            // Defaults for non-broadcasting state
+            deviceString = mContext.getString(R.string.media_seamless_other_device);
+            iconResource = R.drawable.ic_media_home_devices;
+        }
+
+        mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f);
+        seamlessView.setEnabled(isTapEnabled);
+
+        if (device != null) {
+            Drawable icon = device.getIcon();
+            if (icon instanceof AdaptiveIcon) {
+                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
+                aIcon.setBackgroundColor(mColorSchemeTransition.getBgColor());
+                iconView.setImageDrawable(aIcon);
+            } else {
+                iconView.setImageDrawable(icon);
+            }
+            if (device.getName() != null) {
+                deviceString = device.getName();
+            }
+        } else {
+            // Set to default icon
+            iconView.setImageResource(iconResource);
+        }
+        deviceName.setText(deviceString);
+        seamlessView.setContentDescription(deviceString);
+        seamlessView.setOnClickListener(
+                v -> {
+                    if (mFalsingManager.isFalseTap(
+                            mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY)
+                                    ? FalsingManager.MODERATE_PENALTY :
+                                    FalsingManager.LOW_PENALTY)) {
+                        return;
+                    }
+
+                    if (showBroadcastButton) {
+                        // If the current media app is not broadcasted and users press the outputer
+                        // button, we should pop up the broadcast dialog to check do they want to
+                        // switch broadcast to the other media app, otherwise we still pop up the
+                        // media output dialog.
+                        if (!mIsCurrentBroadcastedApp) {
+                            mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
+                            mSwitchBroadcastApp = device.getName().toString();
+                            mBroadcastDialogController.createBroadcastDialog(mSwitchBroadcastApp,
+                                    mPackageName, true, mMediaViewHolder.getSeamlessButton());
+                        } else {
+                            mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
+                            mMediaOutputDialogFactory.create(mPackageName, true,
+                                    mMediaViewHolder.getSeamlessButton());
+                        }
+                    } else {
+                        mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
+                        if (device.getIntent() != null) {
+                            if (device.getIntent().isActivity()) {
+                                mActivityStarter.startActivity(
+                                        device.getIntent().getIntent(), true);
+                            } else {
+                                try {
+                                    device.getIntent().send();
+                                } catch (PendingIntent.CanceledException e) {
+                                    Log.e(TAG, "Device pending intent was canceled");
+                                }
+                            }
+                        } else {
+                            mMediaOutputDialogFactory.create(mPackageName, true,
+                                    mMediaViewHolder.getSeamlessButton());
+                        }
+                    }
+                });
+    }
+
+    private void bindGutsMenuForPlayer(MediaData data) {
+        Runnable onDismissClickedRunnable = () -> {
+            if (mKey != null) {
+                closeGuts();
+                if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
+                        MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
+                    Log.w(TAG, "Manager failed to dismiss media " + mKey);
+                    // Remove directly from carousel so user isn't stuck with defunct controls
+                    mMediaCarouselController.removePlayer(mKey, false, false);
+                }
+            } else {
+                Log.w(TAG, "Dismiss media with null notification. Token uid="
+                        + data.getToken().getUid());
+            }
+        };
+
+        bindGutsMenuCommon(
+                /* isDismissible= */ data.isClearable(),
+                data.getApp(),
+                mMediaViewHolder.getGutsViewHolder(),
+                onDismissClickedRunnable);
+    }
+
+    private boolean bindSongMetadata(MediaData data) {
+        TextView titleText = mMediaViewHolder.getTitleText();
+        TextView artistText = mMediaViewHolder.getArtistText();
+        return mMetadataAnimationHandler.setNext(
+            Pair.create(data.getSong(), data.getArtist()),
+            () -> {
+                titleText.setText(data.getSong());
+                artistText.setText(data.getArtist());
+
+                // refreshState is required here to resize the text views (and prevent ellipsis)
+                mMediaViewController.refreshState();
+                return Unit.INSTANCE;
+            },
+            () -> {
+                // After finishing the enter animation, we refresh state. This could pop if
+                // something is incorrectly bound, but needs to be run if other elements were
+                // updated while the enter animation was running
+                mMediaViewController.refreshState();
+                return Unit.INSTANCE;
+            });
+    }
+
+    // We may want to look into unifying this with bindRecommendationContentDescription if/when we
+    // do a refactor of this class.
+    private void bindPlayerContentDescription(MediaData data) {
+        if (mMediaViewHolder == null) {
+            return;
+        }
+
+        CharSequence contentDescription;
+        if (mMediaViewController.isGutsVisible()) {
+            contentDescription = mMediaViewHolder.getGutsViewHolder().getGutsText().getText();
+        } else if (data != null) {
+            contentDescription = mContext.getString(
+                    R.string.controls_media_playing_item_description,
+                    data.getSong(),
+                    data.getArtist(),
+                    data.getApp());
+        } else {
+            contentDescription = null;
+        }
+        mMediaViewHolder.getPlayer().setContentDescription(contentDescription);
+    }
+
+    private void bindRecommendationContentDescription(SmartspaceMediaData data) {
+        if (mRecommendationViewHolder == null) {
+            return;
+        }
+
+        CharSequence contentDescription;
+        if (mMediaViewController.isGutsVisible()) {
+            contentDescription =
+                    mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
+        } else if (data != null) {
+            contentDescription = mContext.getString(
+                    R.string.controls_media_smartspace_rec_description,
+                    data.getAppName(mContext));
+        } else {
+            contentDescription = null;
+        }
+
+        mRecommendationViewHolder.getRecommendations().setContentDescription(contentDescription);
+    }
+
+    private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) {
+        final int traceCookie = data.hashCode();
+        final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">";
+        Trace.beginAsyncSection(traceName, traceCookie);
+
+        final int reqId = mArtworkNextBindRequestId++;
+        if (updateBackground) {
+            mIsArtworkBound = false;
+        }
+
+        // Capture width & height from views in foreground for artwork scaling in background
+        int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
+        int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
+
+        // WallpaperColors.fromBitmap takes a good amount of time. We do that work
+        // on the background executor to avoid stalling animations on the UI Thread.
+        mBackgroundExecutor.execute(() -> {
+            // Album art
+            ColorScheme mutableColorScheme = null;
+            Drawable artwork;
+            boolean isArtworkBound;
+            Icon artworkIcon = data.getArtwork();
+            WallpaperColors wallpaperColors = null;
+            if (artworkIcon != null) {
+                if (artworkIcon.getType() == Icon.TYPE_BITMAP
+                        || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
+                    // Avoids extra processing if this is already a valid bitmap
+                    wallpaperColors = WallpaperColors
+                            .fromBitmap(artworkIcon.getBitmap());
+                } else {
+                    Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
+                    if (artworkDrawable != null) {
+                        wallpaperColors = WallpaperColors
+                                .fromDrawable(artworkIcon.loadDrawable(mContext));
+                    }
+                }
+            }
+            if (wallpaperColors != null) {
+                mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
+                Drawable albumArt = getScaledBackground(artworkIcon, width, height);
+                GradientDrawable gradient = (GradientDrawable) mContext
+                        .getDrawable(R.drawable.qs_media_scrim);
+                gradient.setColors(new int[] {
+                        ColorUtilKt.getColorWithAlpha(
+                                MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
+                                0.25f),
+                        ColorUtilKt.getColorWithAlpha(
+                                MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
+                                0.9f),
+                });
+                artwork = new LayerDrawable(new Drawable[] { albumArt, gradient });
+                isArtworkBound = true;
+            } else {
+                // If there's no artwork, use colors from the app icon
+                artwork = new ColorDrawable(Color.TRANSPARENT);
+                isArtworkBound = false;
+                try {
+                    Drawable icon = mContext.getPackageManager()
+                            .getApplicationIcon(data.getPackageName());
+                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true,
+                            Style.CONTENT);
+                } catch (PackageManager.NameNotFoundException e) {
+                    Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
+                }
+            }
+
+            final ColorScheme colorScheme = mutableColorScheme;
+            mMainExecutor.execute(() -> {
+                // Cancel the request if a later one arrived first
+                if (reqId < mArtworkBoundId) {
+                    Trace.endAsyncSection(traceName, traceCookie);
+                    return;
+                }
+                mArtworkBoundId = reqId;
+
+                // Transition Colors to current color scheme
+                boolean colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme);
+
+                // Bind the album view to the artwork or a transition drawable
+                ImageView albumView = mMediaViewHolder.getAlbumView();
+                albumView.setPadding(0, 0, 0, 0);
+                if (updateBackground || colorSchemeChanged
+                        || (!mIsArtworkBound && isArtworkBound)) {
+                    if (mPrevArtwork == null) {
+                        albumView.setImageDrawable(artwork);
+                    } else {
+                        // Since we throw away the last transition, this'll pop if you backgrounds
+                        // are cycled too fast (or the correct background arrives very soon after
+                        // the metadata changes).
+                        TransitionDrawable transitionDrawable = new TransitionDrawable(
+                                new Drawable[]{mPrevArtwork, artwork});
+
+                        scaleTransitionDrawableLayer(transitionDrawable, 0, width, height);
+                        scaleTransitionDrawableLayer(transitionDrawable, 1, width, height);
+                        transitionDrawable.setLayerGravity(0, Gravity.CENTER);
+                        transitionDrawable.setLayerGravity(1, Gravity.CENTER);
+                        transitionDrawable.setCrossFadeEnabled(!isArtworkBound);
+
+                        albumView.setImageDrawable(transitionDrawable);
+                        transitionDrawable.startTransition(isArtworkBound ? 333 : 80);
+                    }
+                    mPrevArtwork = artwork;
+                    mIsArtworkBound = isArtworkBound;
+                }
+
+                // App icon - use notification icon
+                ImageView appIconView = mMediaViewHolder.getAppIcon();
+                appIconView.clearColorFilter();
+                if (data.getAppIcon() != null && !data.getResumption()) {
+                    appIconView.setImageIcon(data.getAppIcon());
+                    appIconView.setColorFilter(
+                            mColorSchemeTransition.getAccentPrimary().getTargetColor());
+                } else {
+                    // Resume players use launcher icon
+                    appIconView.setColorFilter(getGrayscaleFilter());
+                    try {
+                        Drawable icon = mContext.getPackageManager()
+                                .getApplicationIcon(data.getPackageName());
+                        appIconView.setImageDrawable(icon);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
+                        appIconView.setImageResource(R.drawable.ic_music_note);
+                    }
+                }
+                Trace.endAsyncSection(traceName, traceCookie);
+            });
+        });
+    }
+
+    private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
+            int targetWidth, int targetHeight) {
+        Drawable drawable = transitionDrawable.getDrawable(layer);
+        if (drawable == null) {
+            return;
+        }
+
+        int width = drawable.getIntrinsicWidth();
+        int height = drawable.getIntrinsicHeight();
+        if (width == 0 || height == 0 || targetWidth == 0 || targetHeight == 0) {
+            return;
+        }
+
+        float scale;
+        if ((width / (float) height) > (targetWidth / (float) targetHeight)) {
+            // Drawable is wider than target view, scale to match height
+            scale = targetHeight / (float) height;
+        } else {
+            // Drawable is taller than target view, scale to match width
+            scale = targetWidth / (float) width;
+        }
+        transitionDrawable.setLayerSize(layer, (int) (scale * width), (int) (scale * height));
+    }
+
+    private void bindActionButtons(MediaData data) {
+        MediaButton semanticActions = data.getSemanticActions();
+
+        List<ImageButton> genericButtons = new ArrayList<>();
+        for (int id : MediaViewHolder.Companion.getGenericButtonIds()) {
+            genericButtons.add(mMediaViewHolder.getAction(id));
+        }
+
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
+        if (semanticActions != null) {
+            // Hide all the generic buttons
+            for (ImageButton b: genericButtons) {
+                setVisibleAndAlpha(collapsedSet, b.getId(), false);
+                setVisibleAndAlpha(expandedSet, b.getId(), false);
+            }
+
+            for (int id : SEMANTIC_ACTIONS_ALL) {
+                ImageButton button = mMediaViewHolder.getAction(id);
+                MediaAction action = semanticActions.getActionById(id);
+                setSemanticButton(button, action, semanticActions);
+            }
+        } else {
+            // Hide buttons that only appear for semantic actions
+            for (int id : SEMANTIC_ACTIONS_COMPACT) {
+                setVisibleAndAlpha(collapsedSet, id, false);
+                setVisibleAndAlpha(expandedSet, id, false);
+            }
+
+            // Set all the generic buttons
+            List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
+            List<MediaAction> actions = data.getActions();
+            int i = 0;
+            for (; i < actions.size() && i < genericButtons.size(); i++) {
+                boolean showInCompact = actionsWhenCollapsed.contains(i);
+                setGenericButton(
+                        genericButtons.get(i),
+                        actions.get(i),
+                        collapsedSet,
+                        expandedSet,
+                        showInCompact);
+            }
+            for (; i < genericButtons.size(); i++) {
+                // Hide any unused buttons
+                setGenericButton(
+                        genericButtons.get(i),
+                        /* mediaAction= */ null,
+                        collapsedSet,
+                        expandedSet,
+                        /* showInCompact= */ false);
+            }
+        }
+
+        updateSeekBarVisibility();
+    }
+
+    private void updateSeekBarVisibility() {
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
+        expandedSet.setAlpha(R.id.media_progress_bar, mIsSeekBarEnabled ? 1.0f : 0.0f);
+    }
+
+    private int getSeekBarVisibility() {
+        if (mIsSeekBarEnabled) {
+            return ConstraintSet.VISIBLE;
+        }
+        // If disabled and "neighbours" are visible, set progress bar to INVISIBLE instead of GONE
+        // so layout weights still work.
+        return areAnyExpandedBottomActionsVisible() ? ConstraintSet.INVISIBLE : ConstraintSet.GONE;
+    }
+
+    private boolean areAnyExpandedBottomActionsVisible() {
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        for (int id : MediaViewHolder.Companion.getExpandedBottomActionIds()) {
+            if (expandedSet.getVisibility(id) == ConstraintSet.VISIBLE) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void setGenericButton(
+            final ImageButton button,
+            @Nullable MediaAction mediaAction,
+            ConstraintSet collapsedSet,
+            ConstraintSet expandedSet,
+            boolean showInCompact) {
+        bindButtonCommon(button, mediaAction);
+        boolean visible = mediaAction != null;
+        setVisibleAndAlpha(expandedSet, button.getId(), visible);
+        setVisibleAndAlpha(collapsedSet, button.getId(), visible && showInCompact);
+    }
+
+    private void setSemanticButton(
+            final ImageButton button,
+            @Nullable MediaAction mediaAction,
+            MediaButton semanticActions) {
+        AnimationBindHandler animHandler;
+        if (button.getTag() == null) {
+            animHandler = new AnimationBindHandler();
+            button.setTag(animHandler);
+        } else {
+            animHandler = (AnimationBindHandler) button.getTag();
+        }
+
+        animHandler.tryExecute(() -> {
+            bindButtonWithAnimations(button, mediaAction, animHandler);
+            setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction, semanticActions);
+            return Unit.INSTANCE;
+        });
+    }
+
+    private void bindButtonWithAnimations(
+            final ImageButton button,
+            @Nullable MediaAction mediaAction,
+            @NonNull AnimationBindHandler animHandler) {
+        if (mediaAction != null) {
+            if (animHandler.updateRebindId(mediaAction.getRebindId())) {
+                animHandler.unregisterAll();
+                animHandler.tryRegister(mediaAction.getIcon());
+                animHandler.tryRegister(mediaAction.getBackground());
+                bindButtonCommon(button, mediaAction);
+            }
+        } else {
+            animHandler.unregisterAll();
+            clearButton(button);
+        }
+    }
+
+    private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
+        if (mediaAction != null) {
+            final Drawable icon = mediaAction.getIcon();
+            button.setImageDrawable(icon);
+            button.setContentDescription(mediaAction.getContentDescription());
+            final Drawable bgDrawable = mediaAction.getBackground();
+            button.setBackground(bgDrawable);
+
+            Runnable action = mediaAction.getAction();
+            if (action == null) {
+                button.setEnabled(false);
+            } else {
+                button.setEnabled(true);
+                button.setOnClickListener(v -> {
+                    if (!mFalsingManager.isFalseTap(
+                            mFeatureFlags.isEnabled(Flags.MEDIA_FALSING_PENALTY)
+                                    ? FalsingManager.MODERATE_PENALTY :
+                                    FalsingManager.LOW_PENALTY)) {
+                        mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+                        logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+                        action.run();
+                        if (mFeatureFlags.isEnabled(Flags.UMO_SURFACE_RIPPLE)) {
+                            mMultiRippleController.play(createTouchRippleAnimation(button));
+                        }
+
+                        if (icon instanceof Animatable) {
+                            ((Animatable) icon).start();
+                        }
+                        if (bgDrawable instanceof Animatable) {
+                            ((Animatable) bgDrawable).start();
+                        }
+                    }
+                });
+            }
+        } else {
+            clearButton(button);
+        }
+    }
+
+    private RippleAnimation createTouchRippleAnimation(ImageButton button) {
+        float maxSize = mMediaViewHolder.getMultiRippleView().getWidth() * 2;
+        return new RippleAnimation(
+                new RippleAnimationConfig(
+                        RippleShader.RippleShape.CIRCLE,
+                        /* duration= */ 1500L,
+                        /* centerX= */ button.getX() + button.getWidth() * 0.5f,
+                        /* centerY= */ button.getY() + button.getHeight() * 0.5f,
+                        /* maxWidth= */ maxSize,
+                        /* maxHeight= */ maxSize,
+                        /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
+                        mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
+                        /* opacity= */ 100,
+                        /* shouldFillRipple= */ false,
+                        /* sparkleStrength= */ 0f,
+                        /* shouldDistort= */ false
+                )
+        );
+    }
+
+    private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() {
+        return new TurbulenceNoiseAnimationConfig(
+                TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT,
+                TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
+                /* noiseMoveSpeedX= */ 0f,
+                /* noiseMoveSpeedY= */ 0f,
+                TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
+                /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
+                // We want to add (BlendMode.PLUS) the turbulence noise on top of the album art.
+                // Thus, set the background color with alpha 0.
+                /* backgroundColor= */ ColorUtils.setAlphaComponent(Color.BLACK, 0),
+                TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY,
+                /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(),
+                /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(),
+                TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_DURATION_IN_MILLIS,
+                this.getContext().getResources().getDisplayMetrics().density,
+                BlendMode.PLUS,
+                /* onAnimationEnd= */ null
+        );
+    }
+    private void clearButton(final ImageButton button) {
+        button.setImageDrawable(null);
+        button.setContentDescription(null);
+        button.setEnabled(false);
+        button.setBackground(null);
+    }
+
+    private void setSemanticButtonVisibleAndAlpha(
+            int buttonId,
+            @Nullable MediaAction mediaAction,
+            MediaButton semanticActions) {
+        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(buttonId);
+        boolean hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId);
+        boolean shouldBeHiddenDueToScrubbing =
+                scrubbingTimeViewsEnabled(semanticActions) && hideWhenScrubbing && mIsScrubbing;
+        boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
+
+        int notVisibleValue;
+        if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
+                || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) {
+            notVisibleValue = ConstraintSet.INVISIBLE;
+        } else {
+            notVisibleValue = ConstraintSet.GONE;
+        }
+        setVisibleAndAlpha(expandedSet, buttonId, visible, notVisibleValue);
+        setVisibleAndAlpha(collapsedSet, buttonId, visible && showInCompact);
+    }
+
+    /** Updates all the views that might change due to a scrubbing state change. */
+    private void updateDisplayForScrubbingChange(@NonNull MediaButton semanticActions) {
+        // Update visibilities of the scrubbing time views and the scrubbing-dependent buttons.
+        bindScrubbingTime(mMediaData);
+        SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach((id) -> setSemanticButtonVisibleAndAlpha(
+                id, semanticActions.getActionById(id), semanticActions));
+        if (!mMetadataAnimationHandler.isRunning()) {
+            // Trigger a state refresh so that we immediately update visibilities.
+            mMediaViewController.refreshState();
+        }
+    }
+
+    private void bindScrubbingTime(MediaData data) {
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        int elapsedTimeId = mMediaViewHolder.getScrubbingElapsedTimeView().getId();
+        int totalTimeId = mMediaViewHolder.getScrubbingTotalTimeView().getId();
+
+        boolean visible = scrubbingTimeViewsEnabled(data.getSemanticActions()) && mIsScrubbing;
+        setVisibleAndAlpha(expandedSet, elapsedTimeId, visible);
+        setVisibleAndAlpha(expandedSet, totalTimeId, visible);
+        // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically
+    }
+
+    private boolean scrubbingTimeViewsEnabled(@Nullable MediaButton semanticActions) {
+        // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views,
+        // so we should only allow scrubbing times to be shown if those action views are present.
+        return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch(
+                id -> semanticActions.getActionById(id) != null
+        );
+    }
+
+    @Nullable
+    private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
+            TransitionLayout player) {
+        if (!(player.getParent() instanceof ViewGroup)) {
+            // TODO(b/192194319): Throw instead of just logging.
+            Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
+                    new Exception());
+            return null;
+        }
+
+        // TODO(b/174236650): Make sure that the carousel indicator also fades out.
+        // TODO(b/174236650): Instrument the animation to measure jank.
+        return new GhostedViewLaunchAnimatorController(player,
+                InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
+            @Override
+            protected float getCurrentTopCornerRadius() {
+                return mContext.getResources().getDimension(R.dimen.notification_corner_radius);
+            }
+
+            @Override
+            protected float getCurrentBottomCornerRadius() {
+                // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
+                return getCurrentTopCornerRadius();
+            }
+        };
+    }
+
+    /** Bind this recommendation view based on the given data. */
+    public void bindRecommendation(@NonNull SmartspaceMediaData data) {
+        if (mRecommendationViewHolder == null) {
+            return;
+        }
+
+        if (!data.isValid()) {
+            Log.e(TAG, "Received an invalid recommendation list; returning");
+            return;
+        }
+
+        Trace.beginSection(
+                "MediaControlPanel#bindRecommendation<" + data.getPackageName() + ">");
+
+        mRecommendationData = data;
+        mSmartspaceId = SmallHash.hash(data.getTargetId());
+        mPackageName = data.getPackageName();
+        mInstanceId = data.getInstanceId();
+
+        // Set up recommendation card's header.
+        ApplicationInfo applicationInfo;
+        try {
+            applicationInfo = mContext.getPackageManager()
+                    .getApplicationInfo(data.getPackageName(), 0 /* flags */);
+            mUid = applicationInfo.uid;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Fail to get media recommendation's app info", e);
+            Trace.endSection();
+            return;
+        }
+
+        CharSequence appName = data.getAppName(mContext);
+        if (appName == null) {
+            Log.w(TAG, "Fail to get media recommendation's app name");
+            Trace.endSection();
+            return;
+        }
+
+        PackageManager packageManager = mContext.getPackageManager();
+        // Set up media source app's logo.
+        Drawable icon = packageManager.getApplicationIcon(applicationInfo);
+        ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
+        headerLogoImageView.setImageDrawable(icon);
+        fetchAndUpdateRecommendationColors(icon);
+
+        // Set up media rec card's tap action if applicable.
+        TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
+        setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction(),
+                /* interactedSubcardRank */ -1);
+        bindRecommendationContentDescription(data);
+
+        List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
+        List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
+        List<SmartspaceAction> recommendations = data.getValidRecommendations();
+
+        boolean hasTitle = false;
+        boolean hasSubtitle = false;
+        for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
+            SmartspaceAction recommendation = recommendations.get(itemIndex);
+
+            // Set up media item cover.
+            ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
+            mediaCoverImageView.setImageIcon(recommendation.getIcon());
+
+            // Set up the media item's click listener if applicable.
+            ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
+            setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation, itemIndex);
+            // Bubble up the long-click event to the card.
+            mediaCoverContainer.setOnLongClickListener(v -> {
+                if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
+                View parent = (View) v.getParent();
+                if (parent != null) {
+                    parent.performLongClick();
+                }
+                return true;
+            });
+
+            // Set up the accessibility label for the media item.
+            String artistName = recommendation.getExtras()
+                    .getString(KEY_SMARTSPACE_ARTIST_NAME, "");
+            if (artistName.isEmpty()) {
+                mediaCoverImageView.setContentDescription(
+                        mContext.getString(
+                                R.string.controls_media_smartspace_rec_item_no_artist_description,
+                                recommendation.getTitle(), appName));
+            } else {
+                mediaCoverImageView.setContentDescription(
+                        mContext.getString(
+                                R.string.controls_media_smartspace_rec_item_description,
+                                recommendation.getTitle(), artistName, appName));
+            }
+
+
+            // Set up title
+            CharSequence title = recommendation.getTitle();
+            hasTitle |= !TextUtils.isEmpty(title);
+            TextView titleView = mRecommendationViewHolder.getMediaTitles().get(itemIndex);
+            titleView.setText(title);
+
+            // Set up subtitle
+            // It would look awkward to show a subtitle if we don't have a title.
+            boolean shouldShowSubtitleText = !TextUtils.isEmpty(title);
+            CharSequence subtitle = shouldShowSubtitleText ? recommendation.getSubtitle() : "";
+            hasSubtitle |= !TextUtils.isEmpty(subtitle);
+            TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+            subtitleView.setText(subtitle);
+        }
+        mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
+
+        // If there's no subtitles and/or titles for any of the albums, hide those views.
+        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+        final boolean titlesVisible = hasTitle;
+        final boolean subtitlesVisible = hasSubtitle;
+        mRecommendationViewHolder.getMediaTitles().forEach((titleView) ->
+                setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible));
+        mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) ->
+                setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible));
+
+        // Guts
+        Runnable onDismissClickedRunnable = () -> {
+            closeGuts();
+            mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
+                    data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
+
+            Intent dismissIntent = data.getDismissIntent();
+            if (dismissIntent == null) {
+                Log.w(TAG, "Cannot create dismiss action click action: "
+                        + "extras missing dismiss_intent.");
+                return;
+            }
+
+            if (dismissIntent.getComponent() != null
+                    && dismissIntent.getComponent().getClassName()
+                    .equals(EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)) {
+                // Dismiss the card Smartspace data through Smartspace trampoline activity.
+                mContext.startActivity(dismissIntent);
+            } else {
+                mBroadcastSender.sendBroadcast(dismissIntent);
+            }
+        };
+        bindGutsMenuCommon(
+                /* isDismissible= */ true,
+                appName.toString(),
+                mRecommendationViewHolder.getGutsViewHolder(),
+                onDismissClickedRunnable);
+
+        mController = null;
+        if (mMetadataAnimationHandler == null || !mMetadataAnimationHandler.isRunning()) {
+            mMediaViewController.refreshState();
+        }
+        Trace.endSection();
+    }
+
+    private void fetchAndUpdateRecommendationColors(Drawable appIcon) {
+        mBackgroundExecutor.execute(() -> {
+            ColorScheme colorScheme = new ColorScheme(
+                    WallpaperColors.fromDrawable(appIcon), /* darkTheme= */ true);
+            mMainExecutor.execute(() -> setRecommendationColors(colorScheme));
+        });
+    }
+
+    private void setRecommendationColors(ColorScheme colorScheme) {
+        if (mRecommendationViewHolder == null) {
+            return;
+        }
+
+        int backgroundColor = MediaColorSchemesKt.surfaceFromScheme(colorScheme);
+        int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
+        int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
+
+        mRecommendationViewHolder.getRecommendations()
+                .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
+        mRecommendationViewHolder.getMediaTitles().forEach(
+                (title) -> title.setTextColor(textPrimaryColor));
+        mRecommendationViewHolder.getMediaSubtitles().forEach(
+                (subtitle) -> subtitle.setTextColor(textSecondaryColor));
+
+        mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
+    }
+
+    private void bindGutsMenuCommon(
+            boolean isDismissible,
+            String appName,
+            GutsViewHolder gutsViewHolder,
+            Runnable onDismissClickedRunnable) {
+        // Text
+        String text;
+        if (isDismissible) {
+            text = mContext.getString(R.string.controls_media_close_session, appName);
+        } else {
+            text = mContext.getString(R.string.controls_media_active_session);
+        }
+        gutsViewHolder.getGutsText().setText(text);
+
+        // Dismiss button
+        gutsViewHolder.getDismissText().setVisibility(isDismissible ? View.VISIBLE : View.GONE);
+        gutsViewHolder.getDismiss().setEnabled(isDismissible);
+        gutsViewHolder.getDismiss().setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+            logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT);
+            mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
+
+            onDismissClickedRunnable.run();
+        });
+
+        // Cancel button
+        TextView cancelText = gutsViewHolder.getCancelText();
+        if (isDismissible) {
+            cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_outline_button));
+        } else {
+            cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_solid_button));
+        }
+        gutsViewHolder.getCancel().setOnClickListener(v -> {
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                closeGuts();
+            }
+        });
+        gutsViewHolder.setDismissible(isDismissible);
+
+        // Settings button
+        gutsViewHolder.getSettings().setOnClickListener(v -> {
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
+                mActivityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */true);
+            }
+        });
+    }
+
+    /**
+     * Close the guts for this player.
+     *
+     * @param immediate {@code true} if it should be closed without animation
+     */
+    public void closeGuts(boolean immediate) {
+        if (mMediaViewHolder != null) {
+            mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
+        } else if (mRecommendationViewHolder != null) {
+            mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
+        }
+        mMediaViewController.closeGuts(immediate);
+        if (mMediaViewHolder != null) {
+            bindPlayerContentDescription(mMediaData);
+        } else if (mRecommendationViewHolder != null) {
+            bindRecommendationContentDescription(mRecommendationData);
+        }
+    }
+
+    private void closeGuts() {
+        closeGuts(false);
+    }
+
+    private void openGuts() {
+        if (mMediaViewHolder != null) {
+            mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
+        } else if (mRecommendationViewHolder != null) {
+            mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
+        }
+        mMediaViewController.openGuts();
+        if (mMediaViewHolder != null) {
+            bindPlayerContentDescription(mMediaData);
+        } else if (mRecommendationViewHolder != null) {
+            bindRecommendationContentDescription(mRecommendationData);
+        }
+        mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId);
+    }
+
+    /**
+     * Scale artwork to fill the background of the panel
+     */
+    @UiThread
+    private Drawable getScaledBackground(Icon icon, int width, int height) {
+        if (icon == null) {
+            return null;
+        }
+        Drawable drawable = icon.loadDrawable(mContext);
+        Rect bounds = new Rect(0, 0, width, height);
+        if (bounds.width() > width || bounds.height() > height) {
+            float offsetX = (bounds.width() - width) / 2.0f;
+            float offsetY = (bounds.height() - height) / 2.0f;
+            bounds.offset((int) -offsetX, (int) -offsetY);
+        }
+        drawable.setBounds(bounds);
+        return drawable;
+    }
+
+    /**
+     * Get the current media controller
+     *
+     * @return the controller
+     */
+    public MediaController getController() {
+        return mController;
+    }
+
+    /**
+     * Check whether the media controlled by this player is currently playing
+     *
+     * @return whether it is playing, or false if no controller information
+     */
+    public boolean isPlaying() {
+        return isPlaying(mController);
+    }
+
+    /**
+     * Check whether the given controller is currently playing
+     *
+     * @param controller media controller to check
+     * @return whether it is playing, or false if no controller information
+     */
+    protected boolean isPlaying(MediaController controller) {
+        if (controller == null) {
+            return false;
+        }
+
+        PlaybackState state = controller.getPlaybackState();
+        if (state == null) {
+            return false;
+        }
+
+        return (state.getState() == PlaybackState.STATE_PLAYING);
+    }
+
+    private ColorMatrixColorFilter getGrayscaleFilter() {
+        ColorMatrix matrix = new ColorMatrix();
+        matrix.setSaturation(0);
+        return new ColorMatrixColorFilter(matrix);
+    }
+
+    private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
+        setVisibleAndAlpha(set, actionId, visible, ConstraintSet.GONE);
+    }
+
+    private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible,
+            int notVisibleValue) {
+        set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : notVisibleValue);
+        set.setAlpha(actionId, visible ? 1.0f : 0.0f);
+    }
+
+    private void setSmartspaceRecItemOnClickListener(
+            @NonNull View view,
+            @NonNull SmartspaceAction action,
+            int interactedSubcardRank) {
+        if (view == null || action == null || action.getIntent() == null
+                || action.getIntent().getExtras() == null) {
+            Log.e(TAG, "No tap action can be set up");
+            return;
+        }
+
+        view.setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
+            if (interactedSubcardRank == -1) {
+                mLogger.logRecommendationCardTap(mPackageName, mInstanceId);
+            } else {
+                mLogger.logRecommendationItemTap(mPackageName, mInstanceId, interactedSubcardRank);
+            }
+            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
+                    interactedSubcardRank,
+                    mSmartspaceMediaItemsCount);
+
+            if (shouldSmartspaceRecItemOpenInForeground(action)) {
+                // Request to unlock the device if the activity needs to be opened in foreground.
+                mActivityStarter.postStartActivityDismissingKeyguard(
+                        action.getIntent(),
+                        0 /* delay */,
+                        buildLaunchAnimatorController(
+                                mRecommendationViewHolder.getRecommendations()));
+            } else {
+                // Otherwise, open the activity in background directly.
+                view.getContext().startActivity(action.getIntent());
+            }
+
+            // Automatically scroll to the active player once the media is loaded.
+            mMediaCarouselController.setShouldScrollToKey(true);
+        });
+    }
+
+    /** Returns if the Smartspace action will open the activity in foreground. */
+    private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) {
+        if (action == null || action.getIntent() == null
+                || action.getIntent().getExtras() == null) {
+            return false;
+        }
+
+        String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT);
+        if (intentString == null) {
+            return false;
+        }
+
+        try {
+            Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
+            return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false);
+        } catch (URISyntaxException e) {
+            Log.wtf(TAG, "Failed to create intent from URI: " + intentString);
+            e.printStackTrace();
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the surface given the current end location for MediaViewController
+     * @return surface used for Smartspace logging
+     */
+    protected int getSurfaceForSmartspaceLogging() {
+        int currentEndLocation = mMediaViewController.getCurrentEndLocation();
+        if (currentEndLocation == MediaHierarchyManager.LOCATION_QQS
+                || currentEndLocation == MediaHierarchyManager.LOCATION_QS) {
+            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE;
+        } else if (currentEndLocation == MediaHierarchyManager.LOCATION_LOCKSCREEN) {
+            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN;
+        } else if (currentEndLocation == MediaHierarchyManager.LOCATION_DREAM_OVERLAY) {
+            return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY;
+        }
+        return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE;
+    }
+
+    private void logSmartspaceCardReported(int eventId) {
+        logSmartspaceCardReported(eventId,
+                /* interactedSubcardRank */ 0,
+                /* interactedSubcardCardinality */ 0);
+    }
+
+    private void logSmartspaceCardReported(int eventId,
+            int interactedSubcardRank, int interactedSubcardCardinality) {
+        mMediaCarouselController.logSmartspaceCardReported(eventId,
+                mSmartspaceId,
+                mUid,
+                new int[]{getSurfaceForSmartspaceLogging()},
+                interactedSubcardRank,
+                interactedSubcardCardinality);
+    }
+}
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
new file mode 100644
index 0000000..cbb670e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -0,0 +1,1273 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.annotation.IntDef
+import android.content.Context
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.util.MathUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import androidx.annotation.VisibleForTesting
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dreams.DreamOverlayStateController
+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.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
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+private val TAG: String = MediaHierarchyManager::class.java.simpleName
+
+/** Similarly to isShown but also excludes views that have 0 alpha */
+val View.isShownNotFaded: Boolean
+    get() {
+        var current: View = this
+        while (true) {
+            if (current.visibility != View.VISIBLE) {
+                return false
+            }
+            if (current.alpha == 0.0f) {
+                return false
+            }
+            val parent = current.parent ?: return false // We are not attached to the view root
+            if (parent !is View) {
+                // we reached the viewroot, hurray
+                return true
+            }
+            current = parent
+        }
+    }
+
+/**
+ * This manager is responsible for placement of the unique media view between the different hosts
+ * and animate the positions of the views to achieve seamless transitions.
+ */
+@SysUISingleton
+class MediaHierarchyManager
+@Inject
+constructor(
+    private val context: Context,
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val keyguardStateController: KeyguardStateController,
+    private val bypassController: KeyguardBypassController,
+    private val mediaCarouselController: MediaCarouselController,
+    private val keyguardViewController: KeyguardViewController,
+    private val dreamOverlayStateController: DreamOverlayStateController,
+    configurationController: ConfigurationController,
+    wakefulnessLifecycle: WakefulnessLifecycle,
+    panelEventsEvents: ShadeStateEvents,
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
+) {
+
+    /** Track the media player setting status on lock screen. */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+        secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
+     * Whether we "skip" QQS during panel expansion.
+     *
+     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
+     * start closing the panel, it fully collapses instead of going to QQS.
+     */
+    private var skipQqsOnExpansion: Boolean = false
+
+    /**
+     * The root overlay of the hierarchy. This is where the media notification is attached to
+     * whenever the view is transitioning from one host to another. It also make sure that the view
+     * is always in its final state when it is attached to a view host.
+     */
+    private var rootOverlay: ViewGroupOverlay? = null
+
+    private var rootView: View? = null
+    private var currentBounds = Rect()
+    private var animationStartBounds: Rect = Rect()
+
+    private var animationStartClipping = Rect()
+    private var currentClipping = Rect()
+    private var targetClipping = Rect()
+
+    /**
+     * The cross fade progress at the start of the animation. 0.5f means it's just switching between
+     * the start and the end location and the content is fully faded, while 0.75f means that we're
+     * halfway faded in again in the target state.
+     */
+    private var animationStartCrossFadeProgress = 0.0f
+
+    /** The starting alpha of the animation */
+    private var animationStartAlpha = 0.0f
+
+    /** The starting location of the cross fade if an animation is running right now. */
+    @MediaLocation private var crossFadeAnimationStartLocation = -1
+
+    /** The end location of the cross fade if an animation is running right now. */
+    @MediaLocation private var crossFadeAnimationEndLocation = -1
+    private var targetBounds: Rect = Rect()
+    private val mediaFrame
+        get() = mediaCarouselController.mediaFrame
+    private var statusbarState: Int = statusBarStateController.state
+    private var animator =
+        ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+            interpolator = Interpolators.FAST_OUT_SLOW_IN
+            addUpdateListener {
+                updateTargetState()
+                val currentAlpha: Float
+                var boundsProgress = animatedFraction
+                if (isCrossFadeAnimatorRunning) {
+                    animationCrossFadeProgress =
+                        MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction)
+                    // When crossfading, let's keep the bounds at the right location during fading
+                    boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
+                    currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress)
+                } else {
+                    // If we're not crossfading, let's interpolate from the start alpha to 1.0f
+                    currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
+                }
+                interpolateBounds(
+                    animationStartBounds,
+                    targetBounds,
+                    boundsProgress,
+                    result = currentBounds
+                )
+                resolveClipping(currentClipping)
+                applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
+            }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    private var cancelled: Boolean = false
+
+                    override fun onAnimationCancel(animation: Animator?) {
+                        cancelled = true
+                        animationPending = false
+                        rootView?.removeCallbacks(startAnimation)
+                    }
+
+                    override fun onAnimationEnd(animation: Animator?) {
+                        isCrossFadeAnimatorRunning = false
+                        if (!cancelled) {
+                            applyTargetStateIfNotAnimating()
+                        }
+                    }
+
+                    override fun onAnimationStart(animation: Animator?) {
+                        cancelled = false
+                        animationPending = false
+                    }
+                }
+            )
+        }
+
+    private fun resolveClipping(result: Rect) {
+        if (animationStartClipping.isEmpty) result.set(targetClipping)
+        else if (targetClipping.isEmpty) result.set(animationStartClipping)
+        else result.setIntersect(animationStartClipping, targetClipping)
+    }
+
+    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1)
+    /**
+     * The last location where this view was at before going to the desired location. This is useful
+     * for guided transitions.
+     */
+    @MediaLocation private var previousLocation = -1
+    /** The desired location where the view will be at the end of the transition. */
+    @MediaLocation private var desiredLocation = -1
+
+    /**
+     * The current attachment location where the view is currently attached. Usually this matches
+     * the desired location except for animations whenever a view moves to the new desired location,
+     * during which it is in [IN_OVERLAY].
+     */
+    @MediaLocation private var currentAttachmentLocation = -1
+
+    private var inSplitShade = false
+
+    /** Is there any active media in the carousel? */
+    private var hasActiveMedia: Boolean = false
+        get() = mediaHosts.get(LOCATION_QQS)?.visible == true
+
+    /** Are we currently waiting on an animation to start? */
+    private var animationPending: Boolean = false
+    private val startAnimation: Runnable = Runnable { animator.start() }
+
+    /** The expansion of quick settings */
+    var qsExpansion: Float = 0.0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation()
+                if (getQSTransformationProgress() >= 0) {
+                    updateTargetState()
+                    applyTargetStateIfNotAnimating()
+                }
+            }
+        }
+
+    /** Is quick setting expanded? */
+    var qsExpanded: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
+            }
+            // qs is expanded on LS shade and HS shade
+            if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
+                mediaCarouselController.logSmartspaceImpression(value)
+            }
+            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+        }
+
+    /**
+     * distance that the full shade transition takes in order for media to fully transition to the
+     * shade
+     */
+    private var distanceForFullShadeTransition = 0
+
+    /**
+     * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f
+     * means we're not transitioning yet, while 1 means we're all the way in the full shade.
+     */
+    private var fullShadeTransitionProgress = 0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
+                // No need to do all the calculations / updates below if we're not on the lockscreen
+                // or if we're bypassing.
+                return
+            }
+            updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
+            if (value >= 0) {
+                updateTargetState()
+                // Setting the alpha directly, as the below call will use it to update the alpha
+                carouselAlpha = calculateAlphaFromCrossFade(field)
+                applyTargetStateIfNotAnimating()
+            }
+        }
+
+    /** Is there currently a cross-fade animation running driven by an animator? */
+    private var isCrossFadeAnimatorRunning = false
+
+    /**
+     * Are we currently transitionioning from the lockscreen to the full shade
+     * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
+     * the transition starts, this will no longer return true.
+     */
+    private val isTransitioningToFullShade: Boolean
+        get() =
+            fullShadeTransitionProgress != 0f &&
+                !bypassController.bypassEnabled &&
+                statusbarState == StatusBarState.KEYGUARD
+
+    /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    fun setTransitionToFullShadeAmount(value: Float) {
+        // If we're transitioning starting on the shade_locked, we don't want any delay and rather
+        // have it aligned with the rest of the animation
+        val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
+        fullShadeTransitionProgress = progress
+    }
+
+    /**
+     * Returns the amount of translationY of the media container, during the current guided
+     * transformation, if running. If there is no guided transformation running, it will return 0.
+     */
+    fun getGuidedTransformationTranslationY(): Int {
+        if (!isCurrentlyInGuidedTransformation()) {
+            return -1
+        }
+        val startHost = getHost(previousLocation) ?: return 0
+        return targetBounds.top - startHost.currentBounds.top
+    }
+
+    /**
+     * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
+     * we wouldn't want to transition in that case.
+     */
+    var collapsingShadeFromQS: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /** Are location changes currently blocked? */
+    private val blockLocationChanges: Boolean
+        get() {
+            return goingToSleep || dozeAnimationRunning
+        }
+
+    /** Are we currently going to sleep */
+    private var goingToSleep: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
+    /** Are we currently fullyAwake */
+    private var fullyAwake: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                if (value) {
+                    updateDesiredLocation(forceNoAnimation = true)
+                }
+            }
+        }
+
+    /** Is the doze animation currently Running */
+    private var dozeAnimationRunning: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
+    /** Is the dream overlay currently active */
+    private var dreamOverlayActive: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /** Is the dream media complication currently active */
+    private var dreamMediaComplicationActive: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /**
+     * The current cross fade progress. 0.5f means it's just switching between the start and the end
+     * location and the content is fully faded, while 0.75f means that we're halfway faded in again
+     * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true.
+     */
+    private var animationCrossFadeProgress = 1.0f
+
+    /** The current carousel Alpha. */
+    private var carouselAlpha: Float = 1.0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            CrossFadeHelper.fadeIn(mediaFrame, value)
+        }
+
+    /**
+     * Calculate the alpha of the view when given a cross-fade progress.
+     *
+     * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
+     * between the start and the end location and the content is fully faded, while 0.75f means that
+     * we're halfway faded in again in the target state.
+     */
+    private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
+        if (crossFadeProgress <= 0.5f) {
+            return 1.0f - crossFadeProgress / 0.5f
+        } else {
+            return (crossFadeProgress - 0.5f) / 0.5f
+        }
+    }
+
+    init {
+        updateConfiguration()
+        configurationController.addCallback(
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration?) {
+                    updateConfiguration()
+                    updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
+                }
+            }
+        )
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onStatePreChange(oldState: Int, newState: Int) {
+                    // We're updating the location before the state change happens, since we want
+                    // the
+                    // location of the previous state to still be up to date when the animation
+                    // starts
+                    statusbarState = newState
+                    updateDesiredLocation()
+                }
+
+                override fun onStateChanged(newState: Int) {
+                    updateTargetState()
+                    // Enters shade from lock screen
+                    if (
+                        newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()
+                    ) {
+                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+
+                override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                    dozeAnimationRunning = linear != 0.0f && linear != 1.0f
+                }
+
+                override fun onDozingChanged(isDozing: Boolean) {
+                    if (!isDozing) {
+                        dozeAnimationRunning = false
+                        // Enters lock screen from screen off
+                        if (isLockScreenVisibleToUser()) {
+                            mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                        }
+                    } else {
+                        updateDesiredLocation()
+                        qsExpanded = false
+                        closeGuts()
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+
+                override fun onExpandedChanged(isExpanded: Boolean) {
+                    // Enters shade from home screen
+                    if (isHomeScreenShadeVisibleToUser()) {
+                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+            }
+        )
+
+        dreamOverlayStateController.addCallback(
+            object : DreamOverlayStateController.Callback {
+                override fun onComplicationsChanged() {
+                    dreamMediaComplicationActive =
+                        dreamOverlayStateController.complications.any {
+                            it is MediaDreamComplication
+                        }
+                }
+
+                override fun onStateChanged() {
+                    dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
+                }
+            }
+        )
+
+        wakefulnessLifecycle.addObserver(
+            object : WakefulnessLifecycle.Observer {
+                override fun onFinishedGoingToSleep() {
+                    goingToSleep = false
+                }
+
+                override fun onStartedGoingToSleep() {
+                    goingToSleep = true
+                    fullyAwake = false
+                }
+
+                override fun onFinishedWakingUp() {
+                    goingToSleep = false
+                    fullyAwake = true
+                }
+
+                override fun onStartedWakingUp() {
+                    goingToSleep = false
+                }
+            }
+        )
+
+        mediaCarouselController.updateUserVisibility = {
+            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+        }
+        mediaCarouselController.updateHostVisibility = {
+            mediaHosts.forEach { it?.updateViewVisibility() }
+        }
+
+        panelEventsEvents.addShadeStateEventsListener(
+            object : ShadeStateEventsListener {
+                override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
+                    skipQqsOnExpansion = isExpandImmediateEnabled
+                    updateDesiredLocation()
+                }
+            }
+        )
+
+        val settingsObserver: ContentObserver =
+            object : ContentObserver(handler) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    if (uri == lockScreenMediaPlayerUri) {
+                        allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                true,
+                                UserHandle.USER_CURRENT
+                            )
+                    }
+                }
+            }
+        secureSettings.registerContentObserverForUser(
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            settingsObserver,
+            UserHandle.USER_ALL
+        )
+    }
+
+    private fun updateConfiguration() {
+        distanceForFullShadeTransition =
+            context.resources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_media_transition_distance
+            )
+        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
+    }
+
+    /**
+     * Register a media host and create a view can be attached to a view hierarchy and where the
+     * players will be placed in when the host is the currently desired state.
+     *
+     * @return the hostView associated with this location
+     */
+    fun register(mediaObject: MediaHost): UniqueObjectHostView {
+        val viewHost = createUniqueObjectHost()
+        mediaObject.hostView = viewHost
+        mediaObject.addVisibilityChangeListener {
+            // If QQS changes visibility, we need to force an update to ensure the transition
+            // goes into the correct state
+            val stateUpdate = mediaObject.location == LOCATION_QQS
+
+            // Never animate because of a visibility change, only state changes should do that
+            updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
+        }
+        mediaHosts[mediaObject.location] = mediaObject
+        if (mediaObject.location == desiredLocation) {
+            // In case we are overriding a view that is already visible, make sure we attach it
+            // to this new host view in the below call
+            desiredLocation = -1
+        }
+        if (mediaObject.location == currentAttachmentLocation) {
+            currentAttachmentLocation = -1
+        }
+        updateDesiredLocation()
+        return viewHost
+    }
+
+    /** Close the guts in all players in [MediaCarouselController]. */
+    fun closeGuts() {
+        mediaCarouselController.closeGuts()
+    }
+
+    private fun createUniqueObjectHost(): UniqueObjectHostView {
+        val viewHost = UniqueObjectHostView(context)
+        viewHost.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(p0: View?) {
+                    if (rootOverlay == null) {
+                        rootView = viewHost.viewRootImpl.view
+                        rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
+                    }
+                    viewHost.removeOnAttachStateChangeListener(this)
+                }
+
+                override fun onViewDetachedFromWindow(p0: View?) {}
+            }
+        )
+        return viewHost
+    }
+
+    /**
+     * Updates the location that the view should be in. If it changes, an animation may be triggered
+     * going from the old desired location to the new one.
+     *
+     * @param forceNoAnimation optional parameter telling the system not to animate
+     * @param forceStateUpdate optional parameter telling the system to update transition state
+     * ```
+     *                         even if location did not change
+     * ```
+     */
+    private fun updateDesiredLocation(
+        forceNoAnimation: Boolean = false,
+        forceStateUpdate: Boolean = false
+    ) =
+        traceSection("MediaHierarchyManager#updateDesiredLocation") {
+            val desiredLocation = calculateLocation()
+            if (desiredLocation != this.desiredLocation || forceStateUpdate) {
+                if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
+                    // Only update previous location when it actually changes
+                    previousLocation = this.desiredLocation
+                } else if (forceStateUpdate) {
+                    val onLockscreen =
+                        (!bypassController.bypassEnabled &&
+                            (statusbarState == StatusBarState.KEYGUARD))
+                    if (
+                        desiredLocation == LOCATION_QS &&
+                            previousLocation == LOCATION_LOCKSCREEN &&
+                            !onLockscreen
+                    ) {
+                        // If media active state changed and the device is now unlocked, update the
+                        // previous location so we animate between the correct hosts
+                        previousLocation = LOCATION_QQS
+                    }
+                }
+                val isNewView = this.desiredLocation == -1
+                this.desiredLocation = desiredLocation
+                // Let's perform a transition
+                val animate =
+                    !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation)
+                val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+                val host = getHost(desiredLocation)
+                val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
+                if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
+                    // if we're fading, we want the desired location / measurement only to change
+                    // once fully faded. This is happening in the host attachment
+                    mediaCarouselController.onDesiredLocationChanged(
+                        desiredLocation,
+                        host,
+                        animate,
+                        animDuration,
+                        delay
+                    )
+                }
+                performTransitionToNewLocation(isNewView, animate)
+            }
+        }
+
+    private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) =
+        traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
+            if (previousLocation < 0 || isNewView) {
+                cancelAnimationAndApplyDesiredState()
+                return
+            }
+            val currentHost = getHost(desiredLocation)
+            val previousHost = getHost(previousLocation)
+            if (currentHost == null || previousHost == null) {
+                cancelAnimationAndApplyDesiredState()
+                return
+            }
+            updateTargetState()
+            if (isCurrentlyInGuidedTransformation()) {
+                applyTargetStateIfNotAnimating()
+            } else if (animate) {
+                val wasCrossFading = isCrossFadeAnimatorRunning
+                val previewsCrossFadeProgress = animationCrossFadeProgress
+                animator.cancel()
+                if (
+                    currentAttachmentLocation != previousLocation ||
+                        !previousHost.hostView.isAttachedToWindow
+                ) {
+                    // Let's animate to the new position, starting from the current position
+                    // We also go in here in case the view was detached, since the bounds wouldn't
+                    // be correct anymore
+                    animationStartBounds.set(currentBounds)
+                    animationStartClipping.set(currentClipping)
+                } else {
+                    // otherwise, let's take the freshest state, since the current one could
+                    // be outdated
+                    animationStartBounds.set(previousHost.currentBounds)
+                    animationStartClipping.set(previousHost.currentClipping)
+                }
+                val transformationType = calculateTransformationType()
+                var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
+                var crossFadeStartProgress = 0.0f
+                // The alpha is only relevant when not cross fading
+                var newCrossFadeStartLocation = previousLocation
+                if (wasCrossFading) {
+                    if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
+                        if (needsCrossFade) {
+                            // We were previously crossFading and we've already reached
+                            // the end view, Let's start crossfading from the same position there
+                            crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+                        }
+                        // Otherwise let's fade in from the current alpha, but not cross fade
+                    } else {
+                        // We haven't reached the previous location yet, let's still cross fade from
+                        // where we were.
+                        newCrossFadeStartLocation = crossFadeAnimationStartLocation
+                        if (newCrossFadeStartLocation == desiredLocation) {
+                            // we're crossFading back to where we were, let's start at the end
+                            // position
+                            crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+                        } else {
+                            // Let's start from where we are right now
+                            crossFadeStartProgress = previewsCrossFadeProgress
+                            // We need to force cross fading as we haven't reached the end location
+                            // yet
+                            needsCrossFade = true
+                        }
+                    }
+                } else if (needsCrossFade) {
+                    // let's not flicker and start with the same alpha
+                    crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
+                }
+                isCrossFadeAnimatorRunning = needsCrossFade
+                crossFadeAnimationStartLocation = newCrossFadeStartLocation
+                crossFadeAnimationEndLocation = desiredLocation
+                animationStartAlpha = carouselAlpha
+                animationStartCrossFadeProgress = crossFadeStartProgress
+                adjustAnimatorForTransition(desiredLocation, previousLocation)
+                if (!animationPending) {
+                    rootView?.let {
+                        // Let's delay the animation start until we finished laying out
+                        animationPending = true
+                        it.postOnAnimation(startAnimation)
+                    }
+                }
+            } else {
+                cancelAnimationAndApplyDesiredState()
+            }
+        }
+
+    private fun shouldAnimateTransition(
+        @MediaLocation currentLocation: Int,
+        @MediaLocation previousLocation: Int
+    ): Boolean {
+        if (isCurrentlyInGuidedTransformation()) {
+            return false
+        }
+        if (skipQqsOnExpansion) {
+            return false
+        }
+        // This is an invalid transition, and can happen when using the camera gesture from the
+        // lock screen. Disallow.
+        if (
+            previousLocation == LOCATION_LOCKSCREEN &&
+                desiredLocation == LOCATION_QQS &&
+                statusbarState == StatusBarState.SHADE
+        ) {
+            return false
+        }
+
+        if (
+            currentLocation == LOCATION_QQS &&
+                previousLocation == LOCATION_LOCKSCREEN &&
+                (statusBarStateController.leaveOpenOnKeyguardHide() ||
+                    statusbarState == StatusBarState.SHADE_LOCKED)
+        ) {
+            // Usually listening to the isShown is enough to determine this, but there is some
+            // non-trivial reattaching logic happening that will make the view not-shown earlier
+            return true
+        }
+
+        if (
+            statusbarState == StatusBarState.KEYGUARD &&
+                (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN)
+        ) {
+            // We're always fading from lockscreen to keyguard in situations where the player
+            // is already fully hidden
+            return false
+        }
+        return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
+    }
+
+    private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
+        val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+        animator.apply {
+            duration = animDuration
+            startDelay = delay
+        }
+    }
+
+    private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
+        var animDuration = 200L
+        var delay = 0L
+        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+            // Going to the full shade, let's adjust the animation duration
+            if (
+                statusbarState == StatusBarState.SHADE &&
+                    keyguardStateController.isKeyguardFadingAway
+            ) {
+                delay = keyguardStateController.keyguardFadingAwayDelay
+            }
+            animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
+        } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
+            animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
+        }
+        return animDuration to delay
+    }
+
+    private fun applyTargetStateIfNotAnimating() {
+        if (!animator.isRunning) {
+            // Let's immediately apply the target state (which is interpolated) if there is
+            // no animation running. Otherwise the animation update will already update
+            // the location
+            applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
+        }
+    }
+
+    /** Updates the bounds that the view wants to be in at the end of the animation. */
+    private fun updateTargetState() {
+        var starthost = getHost(previousLocation)
+        var endHost = getHost(desiredLocation)
+        if (
+            isCurrentlyInGuidedTransformation() &&
+                !isCurrentlyFading() &&
+                starthost != null &&
+                endHost != null
+        ) {
+            val progress = getTransformationProgress()
+            // If either of the hosts are invisible, let's keep them at the other host location to
+            // have a nicer disappear animation. Otherwise the currentBounds of the state might
+            // be undefined
+            if (!endHost.visible) {
+                endHost = starthost
+            } else if (!starthost.visible) {
+                starthost = endHost
+            }
+            val newBounds = endHost.currentBounds
+            val previousBounds = starthost.currentBounds
+            targetBounds = interpolateBounds(previousBounds, newBounds, progress)
+            targetClipping = endHost.currentClipping
+        } else if (endHost != null) {
+            val bounds = endHost.currentBounds
+            targetBounds.set(bounds)
+            targetClipping = endHost.currentClipping
+        }
+    }
+
+    private fun interpolateBounds(
+        startBounds: Rect,
+        endBounds: Rect,
+        progress: Float,
+        result: Rect? = null
+    ): Rect {
+        val left =
+            MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt()
+        val top =
+            MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt()
+        val right =
+            MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt()
+        val bottom =
+            MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress)
+                .toInt()
+        val resultBounds = result ?: Rect()
+        resultBounds.set(left, top, right, bottom)
+        return resultBounds
+    }
+
+    /** @return true if this transformation is guided by an external progress like a finger */
+    fun isCurrentlyInGuidedTransformation(): Boolean {
+        return hasValidStartAndEndLocations() &&
+            getTransformationProgress() >= 0 &&
+            areGuidedTransitionHostsVisible()
+    }
+
+    private fun hasValidStartAndEndLocations(): Boolean {
+        return previousLocation != -1 && desiredLocation != -1
+    }
+
+    /** Calculate the transformation type for the current animation */
+    @VisibleForTesting
+    @TransformationType
+    fun calculateTransformationType(): Int {
+        if (isTransitioningToFullShade) {
+            if (inSplitShade && areGuidedTransitionHostsVisible()) {
+                return TRANSFORMATION_TYPE_TRANSITION
+            }
+            return TRANSFORMATION_TYPE_FADE
+        }
+        if (
+            previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
+                previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN
+        ) {
+            // animating between ls and qs should fade, as QS is clipped.
+            return TRANSFORMATION_TYPE_FADE
+        }
+        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+            // animating between ls and qqs should fade when dragging down via e.g. expand button
+            return TRANSFORMATION_TYPE_FADE
+        }
+        return TRANSFORMATION_TYPE_TRANSITION
+    }
+
+    private fun areGuidedTransitionHostsVisible(): Boolean {
+        return getHost(previousLocation)?.visible == true &&
+            getHost(desiredLocation)?.visible == true
+    }
+
+    /**
+     * @return the current transformation progress if we're in a guided transformation and -1
+     * otherwise
+     */
+    private fun getTransformationProgress(): Float {
+        if (skipQqsOnExpansion) {
+            return -1.0f
+        }
+        val progress = getQSTransformationProgress()
+        if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
+            return progress
+        }
+        if (isTransitioningToFullShade) {
+            return fullShadeTransitionProgress
+        }
+        return -1.0f
+    }
+
+    private fun getQSTransformationProgress(): Float {
+        val currentHost = getHost(desiredLocation)
+        val previousHost = getHost(previousLocation)
+        if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
+            if (previousHost?.location == LOCATION_QQS) {
+                if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
+                    return qsExpansion
+                }
+            }
+        }
+        return -1.0f
+    }
+
+    private fun getHost(@MediaLocation location: Int): MediaHost? {
+        if (location < 0) {
+            return null
+        }
+        return mediaHosts[location]
+    }
+
+    private fun cancelAnimationAndApplyDesiredState() {
+        animator.cancel()
+        getHost(desiredLocation)?.let {
+            applyState(it.currentBounds, alpha = 1.0f, immediately = true)
+        }
+    }
+
+    /** Apply the current state to the view, updating it's bounds and desired state */
+    private fun applyState(
+        bounds: Rect,
+        alpha: Float,
+        immediately: Boolean = false,
+        clipBounds: Rect = EMPTY_RECT
+    ) =
+        traceSection("MediaHierarchyManager#applyState") {
+            currentBounds.set(bounds)
+            currentClipping = clipBounds
+            carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
+            val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
+            val startLocation = if (onlyUseEndState) -1 else previousLocation
+            val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
+            val endLocation = resolveLocationForFading()
+            mediaCarouselController.setCurrentState(
+                startLocation,
+                endLocation,
+                progress,
+                immediately
+            )
+            updateHostAttachment()
+            if (currentAttachmentLocation == IN_OVERLAY) {
+                // Setting the clipping on the hierarchy of `mediaFrame` does not work
+                if (!currentClipping.isEmpty) {
+                    currentBounds.intersect(currentClipping)
+                }
+                mediaFrame.setLeftTopRightBottom(
+                    currentBounds.left,
+                    currentBounds.top,
+                    currentBounds.right,
+                    currentBounds.bottom
+                )
+            }
+        }
+
+    private fun updateHostAttachment() =
+        traceSection("MediaHierarchyManager#updateHostAttachment") {
+            var newLocation = resolveLocationForFading()
+            var canUseOverlay = !isCurrentlyFading()
+            if (isCrossFadeAnimatorRunning) {
+                if (
+                    getHost(newLocation)?.visible == true &&
+                        getHost(newLocation)?.hostView?.isShown == false &&
+                        newLocation != desiredLocation
+                ) {
+                    // We're crossfading but the view is already hidden. Let's move to the overlay
+                    // instead. This happens when animating to the full shade using a button click.
+                    canUseOverlay = true
+                }
+            }
+            val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
+            newLocation = if (inOverlay) IN_OVERLAY else newLocation
+            if (currentAttachmentLocation != newLocation) {
+                currentAttachmentLocation = newLocation
+
+                // Remove the carousel from the old host
+                (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
+
+                // Add it to the new one
+                if (inOverlay) {
+                    rootOverlay!!.add(mediaFrame)
+                } else {
+                    val targetHost = getHost(newLocation)!!.hostView
+                    // When adding back to the host, let's make sure to reset the bounds.
+                    // Usually adding the view will trigger a layout that does this automatically,
+                    // but we sometimes suppress this.
+                    targetHost.addView(mediaFrame)
+                    val left = targetHost.paddingLeft
+                    val top = targetHost.paddingTop
+                    mediaFrame.setLeftTopRightBottom(
+                        left,
+                        top,
+                        left + currentBounds.width(),
+                        top + currentBounds.height()
+                    )
+
+                    if (mediaFrame.childCount > 0) {
+                        val child = mediaFrame.getChildAt(0)
+                        if (mediaFrame.height < child.height) {
+                            Log.wtf(
+                                TAG,
+                                "mediaFrame height is too small for child: " +
+                                    "${mediaFrame.height} vs ${child.height}"
+                            )
+                        }
+                    }
+                }
+                if (isCrossFadeAnimatorRunning) {
+                    // When cross-fading with an animation, we only notify the media carousel of the
+                    // location change, once the view is reattached to the new place and not
+                    // immediately
+                    // when the desired location changes. This callback will update the measurement
+                    // of the carousel, only once we've faded out at the old location and then
+                    // reattach
+                    // to fade it in at the new location.
+                    mediaCarouselController.onDesiredLocationChanged(
+                        newLocation,
+                        getHost(newLocation),
+                        animate = false
+                    )
+                }
+            }
+        }
+
+    /**
+     * Calculate the location when cross fading between locations. While fading out, the content
+     * should remain in the previous location, while after the switch it should be at the desired
+     * location.
+     */
+    private fun resolveLocationForFading(): Int {
+        if (isCrossFadeAnimatorRunning) {
+            // When animating between two hosts with a fade, let's keep ourselves in the old
+            // location for the first half, and then switch over to the end location
+            if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
+                return crossFadeAnimationEndLocation
+            } else {
+                return crossFadeAnimationStartLocation
+            }
+        }
+        return desiredLocation
+    }
+
+    private fun isTransitionRunning(): Boolean {
+        return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
+            animator.isRunning ||
+            animationPending
+    }
+
+    @MediaLocation
+    private fun calculateLocation(): Int {
+        if (blockLocationChanges) {
+            // Keep the current location until we're allowed to again
+            return desiredLocation
+        }
+        val onLockscreen =
+            (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD))
+        val location =
+            when {
+                dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
+                (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
+                qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
+                !hasActiveMedia -> LOCATION_QS
+                onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
+                onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
+                onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
+                else -> LOCATION_QQS
+            }
+        // When we're on lock screen and the player is not active, we should keep it in QS.
+        // Otherwise it will try to animate a transition that doesn't make sense.
+        if (
+            location == LOCATION_LOCKSCREEN &&
+                getHost(location)?.visible != true &&
+                !statusBarStateController.isDozing
+        ) {
+            return LOCATION_QS
+        }
+        if (
+            location == LOCATION_LOCKSCREEN &&
+                desiredLocation == LOCATION_QS &&
+                collapsingShadeFromQS
+        ) {
+            // When collapsing on the lockscreen, we want to remain in QS
+            return LOCATION_QS
+        }
+        if (
+            location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake
+        ) {
+            // When unlocking from dozing / while waking up, the media shouldn't be transitioning
+            // in an animated way. Let's keep it in the lockscreen until we're fully awake and
+            // reattach it without an animation
+            return LOCATION_LOCKSCREEN
+        }
+        if (skipQqsOnExpansion) {
+            // When doing an immediate expand or collapse, we want to keep it in QS.
+            return LOCATION_QS
+        }
+        return location
+    }
+
+    private fun isSplitShadeExpanding(): Boolean {
+        return inSplitShade && isTransitioningToFullShade
+    }
+
+    /** Are we currently transforming to the full shade and already in QQS */
+    private fun isTransformingToFullShadeAndInQQS(): Boolean {
+        if (!isTransitioningToFullShade) {
+            return false
+        }
+        if (inSplitShade) {
+            // Split shade doesn't use QQS.
+            return false
+        }
+        return fullShadeTransitionProgress > 0.5f
+    }
+
+    /** Is the current transformationType fading */
+    private fun isCurrentlyFading(): Boolean {
+        if (isSplitShadeExpanding()) {
+            // Split shade always uses transition instead of fade.
+            return false
+        }
+        if (isTransitioningToFullShade) {
+            return true
+        }
+        return isCrossFadeAnimatorRunning
+    }
+
+    /** Returns true when the media card could be visible to the user if existed. */
+    private fun isVisibleToUser(): Boolean {
+        return isLockScreenVisibleToUser() ||
+            isLockScreenShadeVisibleToUser() ||
+            isHomeScreenShadeVisibleToUser()
+    }
+
+    private fun isLockScreenVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            !keyguardViewController.isBouncerShowing &&
+            statusBarStateController.state == StatusBarState.KEYGUARD &&
+            allowMediaPlayerOnLockScreen &&
+            statusBarStateController.isExpanded &&
+            !qsExpanded
+    }
+
+    private fun isLockScreenShadeVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            !keyguardViewController.isBouncerShowing &&
+            (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
+                (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
+    }
+
+    private fun isHomeScreenShadeVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            statusBarStateController.state == StatusBarState.SHADE &&
+            statusBarStateController.isExpanded
+    }
+
+    companion object {
+        /** Attached in expanded quick settings */
+        const val LOCATION_QS = 0
+
+        /** Attached in the collapsed QS */
+        const val LOCATION_QQS = 1
+
+        /** Attached on the lock screen */
+        const val LOCATION_LOCKSCREEN = 2
+
+        /** Attached on the dream overlay */
+        const val LOCATION_DREAM_OVERLAY = 3
+
+        /** Attached at the root of the hierarchy in an overlay */
+        const val IN_OVERLAY = -1000
+
+        /**
+         * The default transformation type where the hosts transform into each other using a direct
+         * transition
+         */
+        const val TRANSFORMATION_TYPE_TRANSITION = 0
+
+        /**
+         * A transformation type where content fades from one place to another instead of
+         * transitioning
+         */
+        const val TRANSFORMATION_TYPE_FADE = 1
+    }
+}
+
+private val EMPTY_RECT = Rect()
+
+@IntDef(
+    prefix = ["TRANSFORMATION_TYPE_"],
+    value =
+        [
+            MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
+            MediaHierarchyManager.TRANSFORMATION_TYPE_FADE
+        ]
+)
+@Retention(AnnotationRetention.SOURCE)
+private annotation class TransformationType
+
+@IntDef(
+    prefix = ["LOCATION_"],
+    value =
+        [
+            MediaHierarchyManager.LOCATION_QS,
+            MediaHierarchyManager.LOCATION_QQS,
+            MediaHierarchyManager.LOCATION_LOCKSCREEN,
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY
+        ]
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
new file mode 100644
index 0000000..455b7de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
@@ -0,0 +1,383 @@
+/*
+ * 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.media.controls.ui
+
+import android.graphics.Rect
+import android.util.ArraySet
+import android.view.View
+import android.view.View.OnAttachStateChangeListener
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.util.animation.DisappearParameters
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.UniqueObjectHostView
+import java.util.Objects
+import javax.inject.Inject
+
+class MediaHost
+constructor(
+    private val state: MediaHostStateHolder,
+    private val mediaHierarchyManager: MediaHierarchyManager,
+    private val mediaDataManager: MediaDataManager,
+    private val mediaHostStatesManager: MediaHostStatesManager
+) : MediaHostState by state {
+    lateinit var hostView: UniqueObjectHostView
+    var location: Int = -1
+        private set
+    private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
+
+    private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
+
+    private var inited: Boolean = false
+
+    /** Are we listening to media data changes? */
+    private var listeningToMediaData = false
+
+    /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */
+    val currentBounds: Rect = Rect()
+        get() {
+            hostView.getLocationOnScreen(tmpLocationOnScreen)
+            var left = tmpLocationOnScreen[0] + hostView.paddingLeft
+            var top = tmpLocationOnScreen[1] + hostView.paddingTop
+            var right = tmpLocationOnScreen[0] + hostView.width - hostView.paddingRight
+            var bottom = tmpLocationOnScreen[1] + hostView.height - hostView.paddingBottom
+            // Handle cases when the width or height is 0 but it has padding. In those cases
+            // the above could return negative widths, which is wrong
+            if (right < left) {
+                left = 0
+                right = 0
+            }
+            if (bottom < top) {
+                bottom = 0
+                top = 0
+            }
+            field.set(left, top, right, bottom)
+            return field
+        }
+
+    /**
+     * Set the clipping that this host should use, based on its parent's bounds.
+     *
+     * Use [Rect.set].
+     */
+    val currentClipping = Rect()
+
+    private val listener =
+        object : MediaDataManager.Listener {
+            override fun onMediaDataLoaded(
+                key: String,
+                oldKey: String?,
+                data: MediaData,
+                immediately: Boolean,
+                receivedSmartspaceCardLatency: Int,
+                isSsReactivated: Boolean
+            ) {
+                if (immediately) {
+                    updateViewVisibility()
+                }
+            }
+
+            override fun onSmartspaceMediaDataLoaded(
+                key: String,
+                data: SmartspaceMediaData,
+                shouldPrioritize: Boolean
+            ) {
+                updateViewVisibility()
+            }
+
+            override fun onMediaDataRemoved(key: String) {
+                updateViewVisibility()
+            }
+
+            override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+                if (immediately) {
+                    updateViewVisibility()
+                }
+            }
+        }
+
+    fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
+        visibleChangedListeners.add(listener)
+    }
+
+    fun removeVisibilityChangeListener(listener: (Boolean) -> Unit) {
+        visibleChangedListeners.remove(listener)
+    }
+
+    /**
+     * Initialize this MediaObject and create a host view. All state should already be set on this
+     * host before calling this method in order to avoid unnecessary state changes which lead to
+     * remeasurings later on.
+     *
+     * @param location the location this host name has. Used to identify the host during
+     * ```
+     *                 transitions.
+     * ```
+     */
+    fun init(@MediaLocation location: Int) {
+        if (inited) {
+            return
+        }
+        inited = true
+
+        this.location = location
+        hostView = mediaHierarchyManager.register(this)
+        // Listen by default, as the host might not be attached by our clients, until
+        // they get a visibility change. We still want to stay up to date in that case!
+        setListeningToMediaData(true)
+        hostView.addOnAttachStateChangeListener(
+            object : OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View?) {
+                    setListeningToMediaData(true)
+                    updateViewVisibility()
+                }
+
+                override fun onViewDetachedFromWindow(v: View?) {
+                    setListeningToMediaData(false)
+                }
+            }
+        )
+
+        // Listen to measurement updates and update our state with it
+        hostView.measurementManager =
+            object : UniqueObjectHostView.MeasurementManager {
+                override fun onMeasure(input: MeasurementInput): MeasurementOutput {
+                    // Modify the measurement to exactly match the dimensions
+                    if (
+                        View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST
+                    ) {
+                        input.widthMeasureSpec =
+                            View.MeasureSpec.makeMeasureSpec(
+                                View.MeasureSpec.getSize(input.widthMeasureSpec),
+                                View.MeasureSpec.EXACTLY
+                            )
+                    }
+                    // This will trigger a state change that ensures that we now have a state
+                    // available
+                    state.measurementInput = input
+                    return mediaHostStatesManager.updateCarouselDimensions(location, state)
+                }
+            }
+
+        // Whenever the state changes, let our state manager know
+        state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
+
+        updateViewVisibility()
+    }
+
+    private fun setListeningToMediaData(listen: Boolean) {
+        if (listen != listeningToMediaData) {
+            listeningToMediaData = listen
+            if (listen) {
+                mediaDataManager.addListener(listener)
+            } else {
+                mediaDataManager.removeListener(listener)
+            }
+        }
+    }
+
+    /**
+     * Updates this host's state based on the current media data's status, and invokes listeners if
+     * the visibility has changed
+     */
+    fun updateViewVisibility() {
+        state.visible =
+            if (showsOnlyActiveMedia) {
+                mediaDataManager.hasActiveMediaOrRecommendation()
+            } else {
+                mediaDataManager.hasAnyMediaOrRecommendation()
+            }
+        val newVisibility = if (visible) View.VISIBLE else View.GONE
+        if (newVisibility != hostView.visibility) {
+            hostView.visibility = newVisibility
+            visibleChangedListeners.forEach { it.invoke(visible) }
+        }
+    }
+
+    class MediaHostStateHolder @Inject constructor() : MediaHostState {
+        override var measurementInput: MeasurementInput? = null
+            set(value) {
+                if (value?.equals(field) != true) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var expansion: Float = 0.0f
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var squishFraction: Float = 1.0f
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var showsOnlyActiveMedia: Boolean = false
+            set(value) {
+                if (!value.equals(field)) {
+                    field = value
+                    changedListener?.invoke()
+                }
+            }
+
+        override var visible: Boolean = true
+            set(value) {
+                if (field == value) {
+                    return
+                }
+                field = value
+                changedListener?.invoke()
+            }
+
+        override var falsingProtectionNeeded: Boolean = false
+            set(value) {
+                if (field == value) {
+                    return
+                }
+                field = value
+                changedListener?.invoke()
+            }
+
+        override var disappearParameters: DisappearParameters = DisappearParameters()
+            set(value) {
+                val newHash = value.hashCode()
+                if (lastDisappearHash.equals(newHash)) {
+                    return
+                }
+                field = value
+                lastDisappearHash = newHash
+                changedListener?.invoke()
+            }
+
+        private var lastDisappearHash = disappearParameters.hashCode()
+
+        /** A listener for all changes. This won't be copied over when invoking [copy] */
+        var changedListener: (() -> Unit)? = null
+
+        /** Get a copy of this state. This won't copy any listeners it may have set */
+        override fun copy(): MediaHostState {
+            val mediaHostState = MediaHostStateHolder()
+            mediaHostState.expansion = expansion
+            mediaHostState.squishFraction = squishFraction
+            mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
+            mediaHostState.measurementInput = measurementInput?.copy()
+            mediaHostState.visible = visible
+            mediaHostState.disappearParameters = disappearParameters.deepCopy()
+            mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
+            return mediaHostState
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (!(other is MediaHostState)) {
+                return false
+            }
+            if (!Objects.equals(measurementInput, other.measurementInput)) {
+                return false
+            }
+            if (expansion != other.expansion) {
+                return false
+            }
+            if (squishFraction != other.squishFraction) {
+                return false
+            }
+            if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
+                return false
+            }
+            if (visible != other.visible) {
+                return false
+            }
+            if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
+                return false
+            }
+            if (!disappearParameters.equals(other.disappearParameters)) {
+                return false
+            }
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = measurementInput?.hashCode() ?: 0
+            result = 31 * result + expansion.hashCode()
+            result = 31 * result + squishFraction.hashCode()
+            result = 31 * result + falsingProtectionNeeded.hashCode()
+            result = 31 * result + showsOnlyActiveMedia.hashCode()
+            result = 31 * result + if (visible) 1 else 2
+            result = 31 * result + disappearParameters.hashCode()
+            return result
+        }
+    }
+}
+
+/**
+ * A description of a media host state that describes the behavior whenever the media carousel is
+ * hosted. The HostState notifies the media players of changes to their properties, who in turn will
+ * create view states from it. When adding a new property to this, make sure to update the listener
+ * and notify them about the changes. In case you need to have a different rendering based on the
+ * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
+ * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure
+ * to only update that key if the underlying view needs to have a different measurement.
+ */
+interface MediaHostState {
+
+    companion object {
+        const val EXPANDED: Float = 1.0f
+        const val COLLAPSED: Float = 0.0f
+    }
+
+    /**
+     * The last measurement input that this state was measured with. Infers width and height of the
+     * players.
+     */
+    var measurementInput: MeasurementInput?
+
+    /**
+     * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
+     * for fully expanded (up to 5 actions).
+     */
+    var expansion: Float
+
+    /** Fraction of the height animation. */
+    var squishFraction: Float
+
+    /** Is this host only showing active media or is it showing all of them including resumption? */
+    var showsOnlyActiveMedia: Boolean
+
+    /** If the view should be VISIBLE or GONE. */
+    val visible: Boolean
+
+    /** Does this host need any falsing protection? */
+    var falsingProtectionNeeded: Boolean
+
+    /**
+     * The parameters how the view disappears from this location when going to a host that's not
+     * visible. If modified, make sure to set this value again on the host to ensure the values are
+     * propagated
+     */
+    var disappearParameters: DisappearParameters
+
+    /** Get a copy of this view state, deepcopying all appropriate members */
+    fun copy(): MediaHostState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
new file mode 100644
index 0000000..ae3ce33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+/**
+ * A class responsible for managing all media host states of the various host locations and
+ * coordinating the heights among different players. This class can be used to get the most up to
+ * date state for any location.
+ */
+@SysUISingleton
+class MediaHostStatesManager @Inject constructor() {
+
+    private val callbacks: MutableSet<Callback> = mutableSetOf()
+    private val controllers: MutableSet<MediaViewController> = mutableSetOf()
+
+    /**
+     * The overall sizes of the carousel. This is needed to make sure all players in the carousel
+     * have equal size.
+     */
+    val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
+
+    /** A map with all media states of all locations. */
+    val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
+
+    /**
+     * Notify that a media state for a given location has changed. Should only be called from Media
+     * hosts themselves.
+     */
+    fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) =
+        traceSection("MediaHostStatesManager#updateHostState") {
+            val currentState = mediaHostStates.get(location)
+            if (!hostState.equals(currentState)) {
+                val newState = hostState.copy()
+                mediaHostStates.put(location, newState)
+                updateCarouselDimensions(location, hostState)
+                // First update all the controllers to ensure they get the chance to measure
+                for (controller in controllers) {
+                    controller.stateCallback.onHostStateChanged(location, newState)
+                }
+
+                // Then update all other callbacks which may depend on the controllers above
+                for (callback in callbacks) {
+                    callback.onHostStateChanged(location, newState)
+                }
+            }
+        }
+
+    /**
+     * Get the dimensions of all players combined, which determines the overall height of the media
+     * carousel and the media hosts.
+     */
+    fun updateCarouselDimensions(
+        @MediaLocation location: Int,
+        hostState: MediaHostState
+    ): MeasurementOutput =
+        traceSection("MediaHostStatesManager#updateCarouselDimensions") {
+            val result = MeasurementOutput(0, 0)
+            for (controller in controllers) {
+                val measurement = controller.getMeasurementsForState(hostState)
+                measurement?.let {
+                    if (it.measuredHeight > result.measuredHeight) {
+                        result.measuredHeight = it.measuredHeight
+                    }
+                    if (it.measuredWidth > result.measuredWidth) {
+                        result.measuredWidth = it.measuredWidth
+                    }
+                }
+            }
+            carouselSizes[location] = result
+            return result
+        }
+
+    /** Add a callback to be called when a MediaState has updated */
+    fun addCallback(callback: Callback) {
+        callbacks.add(callback)
+    }
+
+    /** Remove a callback that listens to media states */
+    fun removeCallback(callback: Callback) {
+        callbacks.remove(callback)
+    }
+
+    /**
+     * Register a controller that listens to media states and is used to determine the size of the
+     * media carousel
+     */
+    fun addController(controller: MediaViewController) {
+        controllers.add(controller)
+    }
+
+    /** Notify the manager about the removal of a controller. */
+    fun removeController(controller: MediaViewController) {
+        controllers.remove(controller)
+    }
+
+    interface Callback {
+        /**
+         * Notify the callbacks that a media state for a host has changed, and that the
+         * corresponding view states should be updated and applied
+         */
+        fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
new file mode 100644
index 0000000..0e07465
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.media.controls.ui
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.HorizontalScrollView
+import com.android.systemui.Gefingerpoken
+import com.android.wm.shell.animation.physicsAnimator
+
+/**
+ * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
+ * only measuring children but not the parent, when trying to apply a new scroll position
+ */
+class MediaScrollView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    HorizontalScrollView(context, attrs, defStyleAttr) {
+
+    lateinit var contentContainer: ViewGroup
+        private set
+    var touchListener: Gefingerpoken? = null
+
+    /**
+     * The target value of the translation X animation. Only valid if the physicsAnimator is running
+     */
+    var animationTargetX = 0.0f
+
+    /**
+     * Get the current content translation. This is usually the normal translationX of the content,
+     * but when animating, it might differ
+     */
+    fun getContentTranslation() =
+        if (contentContainer.physicsAnimator.isRunning()) {
+            animationTargetX
+        } else {
+            contentContainer.translationX
+        }
+
+    /**
+     * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
+     * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is
+     * always absolute. This function is its own inverse.
+     */
+    private fun transformScrollX(scrollX: Int): Int =
+        if (isLayoutRtl) {
+            contentContainer.width - width - scrollX
+        } else {
+            scrollX
+        }
+
+    /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */
+    var relativeScrollX: Int
+        get() = transformScrollX(scrollX)
+        set(value) {
+            scrollX = transformScrollX(value)
+        }
+
+    /** Allow all scrolls to go through, use base implementation */
+    override fun scrollTo(x: Int, y: Int) {
+        if (mScrollX != x || mScrollY != y) {
+            val oldX: Int = mScrollX
+            val oldY: Int = mScrollY
+            mScrollX = x
+            mScrollY = y
+            invalidateParentCaches()
+            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
+            if (!awakenScrollBars()) {
+                postInvalidateOnAnimation()
+            }
+        }
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+        var intercept = false
+        touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
+        return super.onInterceptTouchEvent(ev) || intercept
+    }
+
+    override fun onTouchEvent(ev: MotionEvent?): Boolean {
+        var touch = false
+        touchListener?.let { touch = it.onTouchEvent(ev) }
+        return super.onTouchEvent(ev) || touch
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        contentContainer = getChildAt(0) as ViewGroup
+    }
+
+    override fun overScrollBy(
+        deltaX: Int,
+        deltaY: Int,
+        scrollX: Int,
+        scrollY: Int,
+        scrollRangeX: Int,
+        scrollRangeY: Int,
+        maxOverScrollX: Int,
+        maxOverScrollY: Int,
+        isTouchEvent: Boolean
+    ): Boolean {
+        if (getContentTranslation() != 0.0f) {
+            // When we're dismissing we ignore all the scrolling
+            return false
+        }
+        return super.overScrollBy(
+            deltaX,
+            deltaY,
+            scrollX,
+            scrollY,
+            scrollRangeX,
+            scrollRangeY,
+            maxOverScrollX,
+            maxOverScrollY,
+            isTouchEvent
+        )
+    }
+
+    /** Cancel the current touch event going on. */
+    fun cancelCurrentScroll() {
+        val now = SystemClock.uptimeMillis()
+        val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
+        event.source = InputDevice.SOURCE_TOUCHSCREEN
+        super.onTouchEvent(event)
+        event.recycle()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
new file mode 100644
index 0000000..4bf3031
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.content.Context
+import android.content.res.Configuration
+import androidx.annotation.VisibleForTesting
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionLayoutController
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+/**
+ * A class responsible for controlling a single instance of a media player handling interactions
+ * with the view instance and keeping the media view states up to date.
+ */
+class MediaViewController
+@Inject
+constructor(
+    private val context: Context,
+    private val configurationController: ConfigurationController,
+    private val mediaHostStatesManager: MediaHostStatesManager,
+    private val logger: MediaViewLogger
+) {
+
+    /**
+     * Indicating that the media view controller is for a notification-based player, session-based
+     * player, or recommendation
+     */
+    enum class TYPE {
+        PLAYER,
+        RECOMMENDATION
+    }
+
+    companion object {
+        @JvmField val GUTS_ANIMATION_DURATION = 500L
+        val controlIds =
+            setOf(
+                R.id.media_progress_bar,
+                R.id.actionNext,
+                R.id.actionPrev,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+            )
+
+        val detailIds =
+            setOf(
+                R.id.header_title,
+                R.id.header_artist,
+                R.id.actionPlayPause,
+            )
+    }
+
+    /** A listener when the current dimensions of the player change */
+    lateinit var sizeChangedListener: () -> Unit
+    private var firstRefresh: Boolean = true
+    @VisibleForTesting private var transitionLayout: TransitionLayout? = null
+    private val layoutController = TransitionLayoutController()
+    private var animationDelay: Long = 0
+    private var animationDuration: Long = 0
+    private var animateNextStateChange: Boolean = false
+    private val measurement = MeasurementOutput(0, 0)
+    private var type: TYPE = TYPE.PLAYER
+
+    /** A map containing all viewStates for all locations of this mediaState */
+    private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation var currentEndLocation: Int = -1
+
+    /** The starting location of the view where it starts for all animations and transitions */
+    @MediaLocation private var currentStartLocation: Int = -1
+
+    /** The progress of the transition or 1.0 if there is no transition happening */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState = TransitionViewState()
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState2 = TransitionViewState()
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState3 = TransitionViewState()
+
+    /** A temporary cache key to be used to look up cache entries */
+    private val tmpKey = CacheKey()
+
+    /**
+     * The current width of the player. This might not factor in case the player is animating to the
+     * current state, but represents the end state
+     */
+    var currentWidth: Int = 0
+    /**
+     * The current height of the player. This might not factor in case the player is animating to
+     * the current state, but represents the end state
+     */
+    var currentHeight: Int = 0
+
+    /** Get the translationX of the layout */
+    var translationX: Float = 0.0f
+        private set
+        get() {
+            return transitionLayout?.translationX ?: 0.0f
+        }
+
+    /** Get the translationY of the layout */
+    var translationY: Float = 0.0f
+        private set
+        get() {
+            return transitionLayout?.translationY ?: 0.0f
+        }
+
+    /** A callback for RTL config changes */
+    private val configurationListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onConfigChanged(newConfig: Configuration?) {
+                // Because the TransitionLayout is not always attached (and calculates/caches layout
+                // results regardless of attach state), we have to force the layoutDirection of the
+                // view
+                // to the correct value for the user's current locale to ensure correct
+                // recalculation
+                // when/after calling refreshState()
+                newConfig?.apply {
+                    if (transitionLayout?.rawLayoutDirection != layoutDirection) {
+                        transitionLayout?.layoutDirection = layoutDirection
+                        refreshState()
+                    }
+                }
+            }
+        }
+
+    /** A callback for media state changes */
+    val stateCallback =
+        object : MediaHostStatesManager.Callback {
+            override fun onHostStateChanged(
+                @MediaLocation location: Int,
+                mediaHostState: MediaHostState
+            ) {
+                if (location == currentEndLocation || location == currentStartLocation) {
+                    setCurrentState(
+                        currentStartLocation,
+                        currentEndLocation,
+                        currentTransitionProgress,
+                        applyImmediately = false
+                    )
+                }
+            }
+        }
+
+    /**
+     * The expanded constraint set used to render a expanded player. If it is modified, make sure to
+     * call [refreshState]
+     */
+    val collapsedLayout = ConstraintSet()
+
+    /**
+     * The expanded constraint set used to render a collapsed player. If it is modified, make sure
+     * to call [refreshState]
+     */
+    val expandedLayout = ConstraintSet()
+
+    /** Whether the guts are visible for the associated player. */
+    var isGutsVisible = false
+        private set
+
+    init {
+        mediaHostStatesManager.addController(this)
+        layoutController.sizeChangedListener = { width: Int, height: Int ->
+            currentWidth = width
+            currentHeight = height
+            sizeChangedListener.invoke()
+        }
+        configurationController.addCallback(configurationListener)
+    }
+
+    /**
+     * Notify this controller that the view has been removed and all listeners should be destroyed
+     */
+    fun onDestroy() {
+        mediaHostStatesManager.removeController(this)
+        configurationController.removeCallback(configurationListener)
+    }
+
+    /** Show guts with an animated transition. */
+    fun openGuts() {
+        if (isGutsVisible) return
+        isGutsVisible = true
+        animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+        setCurrentState(
+            currentStartLocation,
+            currentEndLocation,
+            currentTransitionProgress,
+            applyImmediately = false
+        )
+    }
+
+    /**
+     * Close the guts for the associated player.
+     *
+     * @param immediate if `false`, it will animate the transition.
+     */
+    @JvmOverloads
+    fun closeGuts(immediate: Boolean = false) {
+        if (!isGutsVisible) return
+        isGutsVisible = false
+        if (!immediate) {
+            animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+        }
+        setCurrentState(
+            currentStartLocation,
+            currentEndLocation,
+            currentTransitionProgress,
+            applyImmediately = immediate
+        )
+    }
+
+    private fun ensureAllMeasurements() {
+        val mediaStates = mediaHostStatesManager.mediaHostStates
+        for (entry in mediaStates) {
+            obtainViewState(entry.value)
+        }
+    }
+
+    /** Get the constraintSet for a given expansion */
+    private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
+        if (expansion > 0) expandedLayout else collapsedLayout
+
+    /**
+     * Set the views to be showing/hidden based on the [isGutsVisible] for a given
+     * [TransitionViewState].
+     */
+    private fun setGutsViewState(viewState: TransitionViewState) {
+        val controlsIds =
+            when (type) {
+                TYPE.PLAYER -> MediaViewHolder.controlsIds
+                TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
+            }
+        val gutsIds = GutsViewHolder.ids
+        controlsIds.forEach { id ->
+            viewState.widgetStates.get(id)?.let { state ->
+                // Make sure to use the unmodified state if guts are not visible.
+                state.alpha = if (isGutsVisible) 0f else state.alpha
+                state.gone = if (isGutsVisible) true else state.gone
+            }
+        }
+        gutsIds.forEach { id ->
+            viewState.widgetStates.get(id)?.let { state ->
+                // Make sure to use the unmodified state if guts are visible
+                state.alpha = if (isGutsVisible) state.alpha else 0f
+                state.gone = if (isGutsVisible) state.gone else true
+            }
+        }
+    }
+
+    /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */
+    internal fun squishViewState(
+        viewState: TransitionViewState,
+        squishFraction: Float
+    ): TransitionViewState {
+        val squishedViewState = viewState.copy()
+        squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+        controlIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+            }
+        }
+
+        detailIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaContainersIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+            }
+        }
+
+        return squishedViewState
+    }
+
+    /**
+     * Obtain a new viewState for a given media state. This usually returns a cached state, but if
+     * it's not available, it will recreate one by measuring, which may be expensive.
+     */
+    @VisibleForTesting
+    fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+        if (state == null || state.measurementInput == null) {
+            return null
+        }
+        // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
+        var cacheKey = getKey(state, isGutsVisible, tmpKey)
+        val viewState = viewStates[cacheKey]
+        if (viewState != null) {
+            // we already have cached this measurement, let's continue
+            if (state.squishFraction <= 1f) {
+                return squishViewState(viewState, state.squishFraction)
+            }
+            return viewState
+        }
+        // Copy the key since this might call recursively into it and we're using tmpKey
+        cacheKey = cacheKey.copy()
+        val result: TransitionViewState?
+
+        if (transitionLayout == null) {
+            return null
+        }
+        // Let's create a new measurement
+        if (state.expansion == 0.0f || state.expansion == 1.0f) {
+            result =
+                transitionLayout!!.calculateViewState(
+                    state.measurementInput!!,
+                    constraintSetForExpansion(state.expansion),
+                    TransitionViewState()
+                )
+
+            setGutsViewState(result)
+            // We don't want to cache interpolated or null states as this could quickly fill up
+            // our cache. We only cache the start and the end states since the interpolation
+            // is cheap
+            viewStates[cacheKey] = result
+        } else {
+            // This is an interpolated state
+            val startState = state.copy().also { it.expansion = 0.0f }
+
+            // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+            // from the start and end state and interpolate them
+            val startViewState = obtainViewState(startState) as TransitionViewState
+            val endState = state.copy().also { it.expansion = 1.0f }
+            val endViewState = obtainViewState(endState) as TransitionViewState
+            result =
+                layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
+        }
+        if (state.squishFraction <= 1f) {
+            return squishViewState(result, state.squishFraction)
+        }
+        return result
+    }
+
+    private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
+        result.apply {
+            heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
+            widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
+            expansion = state.expansion
+            gutsVisible = guts
+        }
+        return result
+    }
+
+    /**
+     * Attach a view to this controller. This may perform measurements if it's not available yet and
+     * should therefore be done carefully.
+     */
+    fun attach(transitionLayout: TransitionLayout, type: TYPE) =
+        traceSection("MediaViewController#attach") {
+            updateMediaViewControllerType(type)
+            logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
+            this.transitionLayout = transitionLayout
+            layoutController.attach(transitionLayout)
+            if (currentEndLocation == -1) {
+                return
+            }
+            // Set the previously set state immediately to the view, now that it's finally attached
+            setCurrentState(
+                startLocation = currentStartLocation,
+                endLocation = currentEndLocation,
+                transitionProgress = currentTransitionProgress,
+                applyImmediately = true
+            )
+        }
+
+    /**
+     * Obtain a measurement for a given location. This makes sure that the state is up to date and
+     * all widgets know their location. Calling this method may create a measurement if we don't
+     * have a cached value available already.
+     */
+    fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? =
+        traceSection("MediaViewController#getMeasurementsForState") {
+            val viewState = obtainViewState(hostState) ?: return null
+            measurement.measuredWidth = viewState.width
+            measurement.measuredHeight = viewState.height
+            return measurement
+        }
+
+    /**
+     * Set a new state for the controlled view which can be an interpolation between multiple
+     * locations.
+     */
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        transitionProgress: Float,
+        applyImmediately: Boolean
+    ) =
+        traceSection("MediaViewController#setCurrentState") {
+            currentEndLocation = endLocation
+            currentStartLocation = startLocation
+            currentTransitionProgress = transitionProgress
+            logger.logMediaLocation("setCurrentState", startLocation, endLocation)
+
+            val shouldAnimate = animateNextStateChange && !applyImmediately
+
+            val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
+            val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
+
+            // Obtain the view state that we'd want to be at the end
+            // The view might not be bound yet or has never been measured and in that case will be
+            // reset once the state is fully available
+            var endViewState = obtainViewState(endHostState) ?: return
+            endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
+            layoutController.setMeasureState(endViewState)
+
+            // If the view isn't bound, we can drop the animation, otherwise we'll execute it
+            animateNextStateChange = false
+            if (transitionLayout == null) {
+                return
+            }
+
+            val result: TransitionViewState
+            var startViewState = obtainViewState(startHostState)
+            startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
+
+            if (!endHostState.visible) {
+                // Let's handle the case where the end is gone first. In this case we take the
+                // start viewState and will make it gone
+                if (startViewState == null || startHostState == null || !startHostState.visible) {
+                    // the start isn't a valid state, let's use the endstate directly
+                    result = endViewState
+                } else {
+                    // Let's get the gone presentation from the start state
+                    result =
+                        layoutController.getGoneState(
+                            startViewState,
+                            startHostState.disappearParameters,
+                            transitionProgress,
+                            tmpState
+                        )
+                }
+            } else if (startHostState != null && !startHostState.visible) {
+                // We have a start state and it is gone.
+                // Let's get presentation from the endState
+                result =
+                    layoutController.getGoneState(
+                        endViewState,
+                        endHostState.disappearParameters,
+                        1.0f - transitionProgress,
+                        tmpState
+                    )
+            } else if (transitionProgress == 1.0f || startViewState == null) {
+                // We're at the end. Let's use that state
+                result = endViewState
+            } else if (transitionProgress == 0.0f) {
+                // We're at the start. Let's use that state
+                result = startViewState
+            } else {
+                result =
+                    layoutController.getInterpolatedState(
+                        startViewState,
+                        endViewState,
+                        transitionProgress,
+                        tmpState
+                    )
+            }
+            logger.logMediaSize("setCurrentState", result.width, result.height)
+            layoutController.setState(
+                result,
+                applyImmediately,
+                shouldAnimate,
+                animationDuration,
+                animationDelay
+            )
+        }
+
+    private fun updateViewStateToCarouselSize(
+        viewState: TransitionViewState?,
+        location: Int,
+        outState: TransitionViewState
+    ): TransitionViewState? {
+        val result = viewState?.copy(outState) ?: return null
+        val overrideSize = mediaHostStatesManager.carouselSizes[location]
+        overrideSize?.let {
+            // To be safe we're using a maximum here. The override size should always be set
+            // properly though.
+            result.height = Math.max(it.measuredHeight, result.height)
+            result.width = Math.max(it.measuredWidth, result.width)
+        }
+        logger.logMediaSize("update to carousel", result.width, result.height)
+        return result
+    }
+
+    private fun updateMediaViewControllerType(type: TYPE) {
+        this.type = type
+
+        // These XML resources contain ConstraintSets that will apply to this player type's layout
+        when (type) {
+            TYPE.PLAYER -> {
+                collapsedLayout.load(context, R.xml.media_session_collapsed)
+                expandedLayout.load(context, R.xml.media_session_expanded)
+            }
+            TYPE.RECOMMENDATION -> {
+                collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
+                expandedLayout.load(context, R.xml.media_recommendation_expanded)
+            }
+        }
+        refreshState()
+    }
+
+    /**
+     * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event
+     * of [location] not being visible, [locationWhenHidden] will be used instead.
+     *
+     * @param location Target
+     * @param locationWhenHidden Location that will be used when the target is not
+     * [MediaHost.visible]
+     * @return State require for executing a transition, and also the respective [MediaHost].
+     */
+    private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
+        val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
+        return obtainViewState(mediaHostState)
+    }
+
+    /**
+     * Notify that the location is changing right now and a [setCurrentState] change is imminent.
+     * This updates the width the view will me measured with.
+     */
+    fun onLocationPreChange(@MediaLocation newLocation: Int) {
+        obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) }
+    }
+
+    /** Request that the next state change should be animated with the given parameters. */
+    fun animatePendingStateChange(duration: Long, delay: Long) {
+        animateNextStateChange = true
+        animationDuration = duration
+        animationDelay = delay
+    }
+
+    /** Clear all existing measurements and refresh the state to match the view. */
+    fun refreshState() =
+        traceSection("MediaViewController#refreshState") {
+            // Let's clear all of our measurements and recreate them!
+            viewStates.clear()
+            if (firstRefresh) {
+                // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
+                // We'll just load these on demand.
+                ensureAllMeasurements()
+                firstRefresh = false
+            }
+            setCurrentState(
+                currentStartLocation,
+                currentEndLocation,
+                currentTransitionProgress,
+                applyImmediately = true
+            )
+        }
+}
+
+/** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
+private data class CacheKey(
+    var widthMeasureSpec: Int = -1,
+    var heightMeasureSpec: Int = -1,
+    var expansion: Float = 0.0f,
+    var gutsVisible: Boolean = false
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
new file mode 100644
index 0000000..fdac33a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaViewLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+private const val TAG = "MediaView"
+
+/** A buffered log for media view events that are too noisy for regular logging */
+@SysUISingleton
+class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) {
+    fun logMediaSize(reason: String, width: Int, height: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                int1 = width
+                int2 = height
+            },
+            { "size ($str1): $int1 x $int2" }
+        )
+    }
+
+    fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                int1 = startLocation
+                int2 = endLocation
+            },
+            { "location ($str1): $int1 -> $int2" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
new file mode 100644
index 0000000..1cdcf5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+
+/**
+ * MetadataAnimationHandler controls the current state of the MediaControlPanel's transition motion.
+ *
+ * It checks for a changed data object (artist & title from MediaControlPanel) and runs the
+ * animation if necessary. When the motion has fully transitioned the elements out, it runs the
+ * update callback to modify the view data, before the enter animation runs.
+ */
+internal open class MetadataAnimationHandler(
+    private val exitAnimator: Animator,
+    private val enterAnimator: Animator
+) : AnimatorListenerAdapter() {
+
+    private var postExitUpdate: (() -> Unit)? = null
+    private var postEnterUpdate: (() -> Unit)? = null
+    private var targetData: Any? = null
+
+    val isRunning: Boolean
+        get() = enterAnimator.isRunning || exitAnimator.isRunning
+
+    fun setNext(targetData: Any, postExit: () -> Unit, postEnter: () -> Unit): Boolean {
+        if (targetData != this.targetData) {
+            this.targetData = targetData
+            postExitUpdate = postExit
+            postEnterUpdate = postEnter
+            if (!isRunning) {
+                exitAnimator.start()
+            }
+            return true
+        }
+        return false
+    }
+
+    override fun onAnimationEnd(anim: Animator) {
+        if (anim === exitAnimator) {
+            postExitUpdate?.let { it() }
+            postExitUpdate = null
+            enterAnimator.start()
+        }
+
+        if (anim === enterAnimator) {
+            // Another new update appeared while entering
+            if (postExitUpdate != null) {
+                exitAnimator.start()
+            } else {
+                postEnterUpdate?.let { it() }
+                postEnterUpdate = null
+            }
+        }
+    }
+
+    init {
+        exitAnimator.addListener(this)
+        enterAnimator.addListener(this)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
new file mode 100644
index 0000000..e9b2cf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
@@ -0,0 +1,254 @@
+/*
+ * 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.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+import android.os.SystemClock
+import android.util.MathUtils.lerp
+import android.util.MathUtils.lerpInv
+import android.util.MathUtils.lerpInvSat
+import androidx.annotation.VisibleForTesting
+import com.android.internal.graphics.ColorUtils
+import com.android.systemui.animation.Interpolators
+import kotlin.math.abs
+import kotlin.math.cos
+
+private const val TAG = "Squiggly"
+
+private const val TWO_PI = (Math.PI * 2f).toFloat()
+@VisibleForTesting internal const val DISABLED_ALPHA = 77
+
+class SquigglyProgress : Drawable() {
+
+    private val wavePaint = Paint()
+    private val linePaint = Paint()
+    private val path = Path()
+    private var heightFraction = 0f
+    private var heightAnimator: ValueAnimator? = null
+    private var phaseOffset = 0f
+    private var lastFrameTime = -1L
+
+    /* distance over which amplitude drops to zero, measured in wavelengths */
+    private val transitionPeriods = 1.5f
+    /* wave endpoint as percentage of bar when play position is zero */
+    private val minWaveEndpoint = 0.2f
+    /* wave endpoint as percentage of bar when play position matches wave endpoint */
+    private val matchedWaveEndpoint = 0.6f
+
+    // Horizontal length of the sine wave
+    var waveLength = 0f
+    // Height of each peak of the sine wave
+    var lineAmplitude = 0f
+    // Line speed in px per second
+    var phaseSpeed = 0f
+    // Progress stroke width, both for wave and solid line
+    var strokeWidth = 0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            wavePaint.strokeWidth = value
+            linePaint.strokeWidth = value
+        }
+
+    // Enables a transition region where the amplitude
+    // of the wave is reduced linearly across it.
+    var transitionEnabled = true
+        set(value) {
+            field = value
+            invalidateSelf()
+        }
+
+    init {
+        wavePaint.strokeCap = Paint.Cap.ROUND
+        linePaint.strokeCap = Paint.Cap.ROUND
+        linePaint.style = Paint.Style.STROKE
+        wavePaint.style = Paint.Style.STROKE
+        linePaint.alpha = DISABLED_ALPHA
+    }
+
+    var animate: Boolean = false
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            if (field) {
+                lastFrameTime = SystemClock.uptimeMillis()
+            }
+            heightAnimator?.cancel()
+            heightAnimator =
+                ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
+                    if (animate) {
+                        startDelay = 60
+                        duration = 800
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE
+                    } else {
+                        duration = 550
+                        interpolator = Interpolators.STANDARD_DECELERATE
+                    }
+                    addUpdateListener {
+                        heightFraction = it.animatedValue as Float
+                        invalidateSelf()
+                    }
+                    addListener(
+                        object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator?) {
+                                heightAnimator = null
+                            }
+                        }
+                    )
+                    start()
+                }
+        }
+
+    override fun draw(canvas: Canvas) {
+        if (animate) {
+            invalidateSelf()
+            val now = SystemClock.uptimeMillis()
+            phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed
+            phaseOffset %= waveLength
+            lastFrameTime = now
+        }
+
+        val progress = level / 10_000f
+        val totalWidth = bounds.width().toFloat()
+        val totalProgressPx = totalWidth * progress
+        val waveProgressPx =
+            totalWidth *
+                (if (!transitionEnabled || progress > matchedWaveEndpoint) progress
+                else
+                    lerp(
+                        minWaveEndpoint,
+                        matchedWaveEndpoint,
+                        lerpInv(0f, matchedWaveEndpoint, progress)
+                    ))
+
+        // Build Wiggly Path
+        val waveStart = -phaseOffset - waveLength / 2f
+        val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx
+
+        // helper function, computes amplitude for wave segment
+        val computeAmplitude: (Float, Float) -> Float = { x, sign ->
+            if (transitionEnabled) {
+                val length = transitionPeriods * waveLength
+                val coeff =
+                    lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x)
+                sign * heightFraction * lineAmplitude * coeff
+            } else {
+                sign * heightFraction * lineAmplitude
+            }
+        }
+
+        // Reset path object to the start
+        path.rewind()
+        path.moveTo(waveStart, 0f)
+
+        // Build the wave, incrementing by half the wavelength each time
+        var currentX = waveStart
+        var waveSign = 1f
+        var currentAmp = computeAmplitude(currentX, waveSign)
+        val dist = waveLength / 2f
+        while (currentX < waveEnd) {
+            waveSign = -waveSign
+            val nextX = currentX + dist
+            val midX = currentX + dist / 2
+            val nextAmp = computeAmplitude(nextX, waveSign)
+            path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp)
+            currentAmp = nextAmp
+            currentX = nextX
+        }
+
+        // translate to the start position of the progress bar for all draw commands
+        val clipTop = lineAmplitude + strokeWidth
+        canvas.save()
+        canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())
+
+        // Draw path up to progress position
+        canvas.save()
+        canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop)
+        canvas.drawPath(path, wavePaint)
+        canvas.restore()
+
+        if (transitionEnabled) {
+            // If there's a smooth transition, we draw the rest of the
+            // path in a different color (using different clip params)
+            canvas.save()
+            canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop)
+            canvas.drawPath(path, linePaint)
+            canvas.restore()
+        } else {
+            // No transition, just draw a flat line to the end of the region.
+            // The discontinuity is hidden by the progress bar thumb shape.
+            canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint)
+        }
+
+        // Draw round line cap at the beginning of the wave
+        val startAmp = cos(abs(waveStart) / waveLength * TWO_PI)
+        canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint)
+
+        canvas.restore()
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.TRANSLUCENT
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        wavePaint.colorFilter = colorFilter
+        linePaint.colorFilter = colorFilter
+    }
+
+    override fun setAlpha(alpha: Int) {
+        updateColors(wavePaint.color, alpha)
+    }
+
+    override fun getAlpha(): Int {
+        return wavePaint.alpha
+    }
+
+    override fun setTint(tintColor: Int) {
+        updateColors(tintColor, alpha)
+    }
+
+    override fun onLevelChange(level: Int): Boolean {
+        return animate
+    }
+
+    override fun setTintList(tint: ColorStateList?) {
+        if (tint == null) {
+            return
+        }
+        updateColors(tint.defaultColor, alpha)
+    }
+
+    private fun updateColors(tintColor: Int, alpha: Int) {
+        wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
+        linePaint.color =
+            ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
new file mode 100644
index 0000000..6caf5c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.util;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link MediaController} constructor.
+ */
+public class MediaControllerFactory {
+
+    private final Context mContext;
+
+    @Inject
+    public MediaControllerFactory(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Creates a new MediaController from a session's token.
+     *
+     * @param token The token for the session. This value must never be null.
+     */
+    public MediaController create(@NonNull MediaSession.Token token) {
+        return new MediaController(mContext, token);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
new file mode 100644
index 0000000..bcfceaa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.util;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+
+/**
+ * Utility class with common methods for media controls
+ */
+public class MediaDataUtils {
+
+    /**
+     * Get the application label for a given package
+     * @param context the context to use
+     * @param packageName Package to check
+     * @param unknownName Fallback string if application is not found
+     * @return The label or fallback string
+     */
+    public static String getAppLabel(Context context, String packageName, String unknownName) {
+        if (TextUtils.isEmpty(packageName)) {
+            return null;
+        }
+        final PackageManager packageManager = context.getPackageManager();
+        ApplicationInfo applicationInfo;
+        try {
+            applicationInfo = packageManager.getApplicationInfo(packageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            applicationInfo = null;
+        }
+        final String applicationName =
+                (String) (applicationInfo != null
+                        ? packageManager.getApplicationLabel(applicationInfo)
+                        : unknownName);
+        return applicationName;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
new file mode 100644
index 0000000..91dac6f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.util
+
+import android.content.Context
+import com.android.systemui.util.Utils
+import javax.inject.Inject
+
+/** Provides access to the current value of the feature flag. */
+class MediaFeatureFlag @Inject constructor(private val context: Context) {
+    val enabled
+        get() = Utils.useQsMediaPlayer(context)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
new file mode 100644
index 0000000..8d4931a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.media.controls.util
+
+import android.app.StatusBarManager
+import android.os.UserHandle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+@SysUISingleton
+class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
+    /**
+     * Check whether media control actions should be based on PlaybackState instead of notification
+     */
+    fun areMediaSessionActionsEnabled(packageName: String, user: UserHandle): Boolean {
+        val enabled = StatusBarManager.useMediaSessionActionsForApp(packageName, user)
+        // Allow global override with flag
+        return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
+    }
+
+    /** Check whether we support displaying information about mute await connections. */
+    fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
+
+    /**
+     * Check whether we enable support for nearby media devices. See
+     * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
+     */
+    fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
new file mode 100644
index 0000000..3ad8c21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -0,0 +1,275 @@
+/*
+ * 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.media.controls.util
+
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaLocation
+import java.lang.IllegalArgumentException
+import javax.inject.Inject
+
+private const val INSTANCE_ID_MAX = 1 shl 20
+
+/** A helper class to log events related to the media controls */
+@SysUISingleton
+class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
+
+    private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
+
+    /** Get a new instance ID for a new media control */
+    fun getNewInstanceId(): InstanceId {
+        return instanceIdSequence.newInstanceId()
+    }
+
+    fun logActiveMediaAdded(
+        uid: Int,
+        packageName: String,
+        instanceId: InstanceId,
+        playbackLocation: Int
+    ) {
+        val event =
+            when (playbackLocation) {
+                MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
+                MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
+                MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
+                else -> throw IllegalArgumentException("Unknown playback location")
+            }
+        logger.logWithInstanceId(event, uid, packageName, instanceId)
+    }
+
+    fun logPlaybackLocationChange(
+        uid: Int,
+        packageName: String,
+        instanceId: InstanceId,
+        playbackLocation: Int
+    ) {
+        val event =
+            when (playbackLocation) {
+                MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
+                MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
+                MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
+                else -> throw IllegalArgumentException("Unknown playback location")
+            }
+        logger.logWithInstanceId(event, uid, packageName, instanceId)
+    }
+
+    fun logResumeMediaAdded(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.RESUME_MEDIA_ADDED, uid, packageName, instanceId)
+    }
+
+    fun logActiveConvertedToResume(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.ACTIVE_TO_RESUME, uid, packageName, instanceId)
+    }
+
+    fun logMediaTimeout(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.MEDIA_TIMEOUT, uid, packageName, instanceId)
+    }
+
+    fun logMediaRemoved(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.MEDIA_REMOVED, uid, packageName, instanceId)
+    }
+
+    fun logMediaCarouselPage(position: Int) {
+        // Since this operation is on the carousel, we don't include package information
+        logger.logWithPosition(MediaUiEvent.CAROUSEL_PAGE, 0, null, position)
+    }
+
+    fun logSwipeDismiss() {
+        // Since this operation is on the carousel, we don't include package information
+        logger.log(MediaUiEvent.DISMISS_SWIPE)
+    }
+
+    fun logLongPressOpen(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.OPEN_LONG_PRESS, uid, packageName, instanceId)
+    }
+
+    fun logLongPressDismiss(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.DISMISS_LONG_PRESS, uid, packageName, instanceId)
+    }
+
+    fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.OPEN_SETTINGS_LONG_PRESS,
+            uid,
+            packageName,
+            instanceId
+        )
+    }
+
+    fun logCarouselSettings() {
+        // Since this operation is on the carousel, we don't include package information
+        logger.log(MediaUiEvent.OPEN_SETTINGS_CAROUSEL)
+    }
+
+    fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
+        val event =
+            when (buttonId) {
+                R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
+                R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
+                R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
+                else -> MediaUiEvent.TAP_ACTION_OTHER
+            }
+
+        logger.logWithInstanceId(event, uid, packageName, instanceId)
+    }
+
+    fun logSeek(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.ACTION_SEEK, uid, packageName, instanceId)
+    }
+
+    fun logOpenOutputSwitcher(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.OPEN_OUTPUT_SWITCHER, uid, packageName, instanceId)
+    }
+
+    fun logTapContentView(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.MEDIA_TAP_CONTENT_VIEW, uid, packageName, instanceId)
+    }
+
+    fun logCarouselPosition(@MediaLocation location: Int) {
+        val event =
+            when (location) {
+                MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
+                MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
+                MediaHierarchyManager.LOCATION_LOCKSCREEN ->
+                    MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
+                MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
+                    MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
+                else -> throw IllegalArgumentException("Unknown media carousel location $location")
+            }
+        logger.log(event)
+    }
+
+    fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
+            0,
+            packageName,
+            instanceId
+        )
+    }
+
+    fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
+            0,
+            packageName,
+            instanceId
+        )
+    }
+
+    fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED,
+            uid,
+            packageName,
+            instanceId
+        )
+    }
+
+    fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
+        logger.logWithInstanceIdAndPosition(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP,
+            0,
+            packageName,
+            instanceId,
+            position
+        )
+    }
+
+    fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP,
+            0,
+            packageName,
+            instanceId
+        )
+    }
+
+    fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG,
+            uid,
+            packageName,
+            instanceId
+        )
+    }
+}
+
+enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
+    @UiEvent(doc = "A new media control was added for media playing locally on the device")
+    LOCAL_MEDIA_ADDED(1029),
+    @UiEvent(doc = "A new media control was added for media cast from the device")
+    CAST_MEDIA_ADDED(1030),
+    @UiEvent(doc = "A new media control was added for media playing remotely")
+    REMOTE_MEDIA_ADDED(1031),
+    @UiEvent(doc = "The media for an existing control was transferred to local playback")
+    TRANSFER_TO_LOCAL(1032),
+    @UiEvent(doc = "The media for an existing control was transferred to a cast device")
+    TRANSFER_TO_CAST(1033),
+    @UiEvent(doc = "The media for an existing control was transferred to a remote device")
+    TRANSFER_TO_REMOTE(1034),
+    @UiEvent(doc = "A new resumable media control was added") RESUME_MEDIA_ADDED(1013),
+    @UiEvent(doc = "An existing active media control was converted into resumable media")
+    ACTIVE_TO_RESUME(1014),
+    @UiEvent(doc = "A media control timed out") MEDIA_TIMEOUT(1015),
+    @UiEvent(doc = "A media control was removed from the carousel") MEDIA_REMOVED(1016),
+    @UiEvent(doc = "User swiped to another control within the media carousel") CAROUSEL_PAGE(1017),
+    @UiEvent(doc = "The user swiped away the media carousel") DISMISS_SWIPE(1018),
+    @UiEvent(doc = "The user long pressed on a media control") OPEN_LONG_PRESS(1019),
+    @UiEvent(doc = "The user dismissed a media control via its long press menu")
+    DISMISS_LONG_PRESS(1020),
+    @UiEvent(doc = "The user opened media settings from a media control's long press menu")
+    OPEN_SETTINGS_LONG_PRESS(1021),
+    @UiEvent(doc = "The user opened media settings from the media carousel")
+    OPEN_SETTINGS_CAROUSEL(1022),
+    @UiEvent(doc = "The play/pause button on a media control was tapped")
+    TAP_ACTION_PLAY_PAUSE(1023),
+    @UiEvent(doc = "The previous button on a media control was tapped") TAP_ACTION_PREV(1024),
+    @UiEvent(doc = "The next button on a media control was tapped") TAP_ACTION_NEXT(1025),
+    @UiEvent(doc = "A custom or generic action button on a media control was tapped")
+    TAP_ACTION_OTHER(1026),
+    @UiEvent(doc = "The user seeked on a media control using the seekbar") ACTION_SEEK(1027),
+    @UiEvent(doc = "The user opened the output switcher from a media control")
+    OPEN_OUTPUT_SWITCHER(1028),
+    @UiEvent(doc = "The user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036),
+    @UiEvent(doc = "The media carousel moved to QQS") MEDIA_CAROUSEL_LOCATION_QQS(1037),
+    @UiEvent(doc = "THe media carousel moved to QS") MEDIA_CAROUSEL_LOCATION_QS(1038),
+    @UiEvent(doc = "The media carousel moved to the lockscreen")
+    MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
+    @UiEvent(doc = "The media carousel moved to the dream state")
+    MEDIA_CAROUSEL_LOCATION_DREAM(1040),
+    @UiEvent(doc = "A media recommendation card was added to the media carousel")
+    MEDIA_RECOMMENDATION_ADDED(1041),
+    @UiEvent(doc = "A media recommendation card was removed from the media carousel")
+    MEDIA_RECOMMENDATION_REMOVED(1042),
+    @UiEvent(doc = "An existing media control was made active as a recommendation")
+    MEDIA_RECOMMENDATION_ACTIVATED(1043),
+    @UiEvent(doc = "User tapped on an item in a media recommendation card")
+    MEDIA_RECOMMENDATION_ITEM_TAP(1044),
+    @UiEvent(doc = "User tapped on a media recommendation card")
+    MEDIA_RECOMMENDATION_CARD_TAP(1045),
+    @UiEvent(doc = "User opened the broadcast dialog from a media control")
+    MEDIA_OPEN_BROADCAST_DIALOG(1079);
+
+    override fun getId() = metricId
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
new file mode 100644
index 0000000..97483a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.media.controls.util;
+
+import java.util.Objects;
+
+/**
+ * A simple hash function for use in privacy-sensitive logging.
+ */
+public final class SmallHash {
+    // Hashes will be in the range [0, MAX_HASH).
+    public static final int MAX_HASH = (1 << 13);
+
+    /** Return Small hash of the string, if non-null, or 0 otherwise. */
+    public static int hash(String in) {
+        return hash(Objects.hashCode(in));
+    }
+
+    /**
+     * Maps in to the range [0, MAX_HASH), keeping similar values distinct.
+     *
+     * @param in An arbitrary integer.
+     * @return in mod MAX_HASH, signs chosen to stay in the range [0, MAX_HASH).
+     */
+    public static int hash(int in) {
+        return Math.abs(Math.floorMod(in, MAX_HASH));
+    }
+
+    private SmallHash() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index a8a8433..3e5d337 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,14 +17,13 @@
 package com.android.systemui.media.dagger;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
 import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaFlags;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostStatesManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostStatesManager;
+import com.android.systemui.media.controls.util.MediaFlags;
 import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -33,6 +32,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.util.Optional;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
index dd9d35b..55fce59 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
@@ -35,25 +35,40 @@
     private val mediaOutputBroadcastDialogFactory: MediaOutputBroadcastDialogFactory
 ) : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
-        if (TextUtils.equals(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG,
-                        intent.action)) {
-            val packageName: String? =
-                    intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
-            if (!TextUtils.isEmpty(packageName)) {
-                mediaOutputDialogFactory.create(packageName!!, false)
-            } else if (DEBUG) {
-                Log.e(TAG, "Unable to launch media output dialog. Package name is empty.")
+        when {
+            TextUtils.equals(Intent.ACTION_SHOW_OUTPUT_SWITCHER, intent.action) -> {
+                val packageName: String? = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
+                launchMediaOutputDialogIfPossible(packageName)
             }
-        } else if (TextUtils.equals(
-                    MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG,
-                    intent.action)) {
-            val packageName: String? =
+            TextUtils.equals(
+                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG, intent.action) -> {
+                val packageName: String? =
                     intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
-            if (!TextUtils.isEmpty(packageName)) {
-                mediaOutputBroadcastDialogFactory.create(packageName!!, false)
-            } else if (DEBUG) {
-                Log.e(TAG, "Unable to launch media output broadcast dialog. Package name is empty.")
+                launchMediaOutputDialogIfPossible(packageName)
             }
+            TextUtils.equals(
+                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG,
+                intent.action) -> {
+                val packageName: String? =
+                    intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
+                launchMediaOutputBroadcastDialogIfPossible(packageName)
+            }
+        }
+    }
+
+    private fun launchMediaOutputDialogIfPossible(packageName: String?) {
+        if (!packageName.isNullOrEmpty()) {
+            mediaOutputDialogFactory.create(packageName, false)
+        } else if (DEBUG) {
+            Log.e(TAG, "Unable to launch media output dialog. Package name is empty.")
+        }
+    }
+
+    private fun launchMediaOutputBroadcastDialogIfPossible(packageName: String?) {
+        if (!packageName.isNullOrEmpty()) {
+            mediaOutputBroadcastDialogFactory.create(packageName, false)
+        } else if (DEBUG) {
+            Log.e(TAG, "Unable to launch media output broadcast dialog. Package name is empty.")
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
index 65c5bc7..69b5698 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
@@ -21,9 +21,9 @@
 
 import android.widget.FrameLayout;
 
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostState;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostState;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index 91e7b49..20e8ae6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -27,9 +27,9 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.DreamMediaEntryComplication;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.SmartspaceMediaData;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 
 import javax.inject.Inject;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
index ffcc1f7..e260894 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
@@ -21,7 +21,7 @@
 import com.android.settingslib.media.LocalMediaManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.MediaFlags
+import com.android.systemui.media.controls.util.MediaFlags
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
index 78f4e01..5ace3ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.muteawait
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaMuteAwaitLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [MediaMuteAwaitConnectionManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
index 46b2cc14..78408fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.nearby
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NearbyMediaDevicesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [NearbyMediaDevicesManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
index a4a96806..647beb9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt
@@ -61,7 +61,7 @@
             @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
             val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
                     as StatusBarManager
-            val routeInfo = MediaRoute2Info.Builder("id", args[0])
+            val routeInfo = MediaRoute2Info.Builder(if (args.size >= 4) args[3] else "id", args[0])
                     .addFeature("feature")
             val useAppIcon = !(args.size >= 3 && args[2] == "useAppIcon=false")
             if (useAppIcon) {
@@ -107,7 +107,7 @@
 
         override fun help(pw: PrintWriter) {
             pw.println("Usage: adb shell cmd statusbar $SENDER_COMMAND " +
-                    "<deviceName> <chipState> useAppIcon=[true|false]")
+                    "<deviceName> <chipState> useAppIcon=[true|false] <id>")
         }
     }
 
@@ -127,8 +127,10 @@
             @SuppressLint("WrongConstant") // sysui is allowed to call STATUS_BAR_SERVICE
             val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE)
                     as StatusBarManager
-            val routeInfo = MediaRoute2Info.Builder("id", "Test Name")
-                .addFeature("feature")
+            val routeInfo = MediaRoute2Info.Builder(
+                if (args.size >= 3) args[2] else "id",
+                "Test Name"
+            ).addFeature("feature")
             val useAppIcon = !(args.size >= 2 && args[1] == "useAppIcon=false")
             if (useAppIcon) {
                 routeInfo.setClientPackageName(TEST_PACKAGE_NAME)
@@ -144,7 +146,7 @@
 
         override fun help(pw: PrintWriter) {
             pw.println("Usage: adb shell cmd statusbar $RECEIVER_COMMAND " +
-                    "<chipState> useAppIcon=[true|false]")
+                    "<chipState> useAppIcon=[true|false] <id>")
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index b565f3c..120f7d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.media.taptotransfer.common
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.temporarydisplay.TemporaryViewLogger
 
 /**
@@ -43,6 +43,21 @@
         )
     }
 
+    /**
+     * Logs an error in trying to update to [displayState].
+     *
+     * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
+     * a [android.app.StatusBarManager.MediaTransferReceiverState].
+     */
+    fun logStateChangeError(displayState: Int) {
+        buffer.log(
+            tag,
+            LogLevel.ERROR,
+            { int1 = displayState },
+            { "Cannot display state=$int1; aborting" }
+        )
+    }
+
     /** Logs that we couldn't find information for [packageName]. */
     fun logPackageNotFound(packageName: String) {
         buffer.log(
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 c3de94f..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
@@ -21,14 +21,34 @@
 import android.graphics.drawable.Drawable
 import com.android.settingslib.Utils
 import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 
 /** 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.
+         *
+         * See [getIconInfoFromPackageName].
+         */
+        fun getIconFromPackageName(
+            context: Context,
+            appPackageName: String?,
+            logger: MediaTttLogger,
+        ): Icon {
+            val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger)
+            return Icon.Loaded(
+                iconInfo.drawable,
+                ContentDescription.Loaded(iconInfo.contentDescription)
+            )
+        }
 
         /**
          * Returns the information needed to display the icon.
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 089625c..691953a 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
@@ -25,7 +25,6 @@
 import android.media.MediaRoute2Info
 import android.os.Handler
 import android.os.PowerManager
-import android.util.Log
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
@@ -41,12 +40,12 @@
 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
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
+import com.android.systemui.util.wakelock.WakeLock
 import javax.inject.Inject
 
 /**
@@ -70,6 +69,7 @@
         private val mediaTttFlags: MediaTttFlags,
         private val uiEventLogger: MediaTttReceiverUiEventLogger,
         private val viewUtil: ViewUtil,
+        wakeLockBuilder: WakeLock.Builder,
 ) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>(
         context,
         logger,
@@ -79,8 +79,7 @@
         configurationController,
         powerManager,
         R.layout.media_ttt_chip_receiver,
-        MediaTttUtils.WINDOW_TITLE,
-        MediaTttUtils.WAKE_REASON,
+        wakeLockBuilder,
 ) {
     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
     override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -116,24 +115,38 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logReceiverStateChange(chipState)
 
         if (chipState == ChipStateReceiver.FAR_FROM_SENDER) {
-            removeView(removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
+            removeView(routeInfo.id, removalReason = ChipStateReceiver.FAR_FROM_SENDER.name)
             return
         }
         if (appIcon == null) {
-            displayView(ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appName))
+            displayView(
+                ChipReceiverInfo(
+                    routeInfo,
+                    appIconDrawableOverride = null,
+                    appName,
+                    id = routeInfo.id,
+                )
+            )
             return
         }
 
         appIcon.loadDrawableAsync(
                 context,
                 Icon.OnDrawableLoadedListener { drawable ->
-                    displayView(ChipReceiverInfo(routeInfo, drawable, appName))
+                    displayView(
+                        ChipReceiverInfo(
+                            routeInfo,
+                            drawable,
+                            appName,
+                            id = routeInfo.id,
+                        )
+                    )
                 },
                 // Notify the listener on the main handler since the listener will update
                 // the UI.
@@ -193,7 +206,7 @@
     }
 
     private fun startRipple(rippleView: ReceiverChipRippleView) {
-        if (rippleView.rippleInProgress) {
+        if (rippleView.rippleInProgress()) {
             // Skip if ripple is still playing
             return
         }
@@ -232,9 +245,8 @@
 data class ChipReceiverInfo(
     val routeInfo: MediaRoute2Info,
     val appIconDrawableOverride: Drawable?,
-    val appNameOverride: CharSequence?
-) : TemporaryViewInfo {
-    override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS
-}
-
-private const val RECEIVER_TAG = "MediaTapToTransferRcvr"
+    val appNameOverride: CharSequence?,
+    override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
+    override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
+    override val id: String,
+) : TemporaryViewInfo()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index e354a03..1ea2025 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -18,8 +18,8 @@
 
 import android.content.Context
 import android.util.AttributeSet
-import com.android.systemui.ripple.RippleShader
-import com.android.systemui.ripple.RippleView
+import com.android.systemui.surfaceeffects.ripple.RippleShader
+import com.android.systemui.surfaceeffects.ripple.RippleView
 
 /**
  * An expanding ripple effect for the media tap-to-transfer receiver chip.
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 c24b030..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
@@ -18,17 +18,12 @@
 
 import android.app.StatusBarManager
 import android.content.Context
-import android.media.MediaRoute2Info
 import android.util.Log
-import android.view.View
 import androidx.annotation.StringRes
 import com.android.internal.logging.UiEventLogger
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
-import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
-import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 
 /**
  * A class enumerating all the possible states of the media tap-to-transfer chip on the sender
@@ -38,6 +33,7 @@
  * @property stringResId the res ID of the string that should be displayed in the chip. Null if the
  *   state should not have the chip be displayed.
  * @property transferStatus the transfer status that the chip state represents.
+ * @property endItem the item that should be displayed in the end section of the chip.
  * @property timeout the amount of time this chip should display on the screen before it times out
  *   and disappears.
  */
@@ -46,7 +42,8 @@
     val uiEvent: UiEventLogger.UiEventEnum,
     @StringRes val stringResId: Int?,
     val transferStatus: TransferStatus,
-    val timeout: Long = DEFAULT_TIMEOUT_MILLIS
+    val endItem: SenderEndItem?,
+    val timeout: Int = DEFAULT_TIMEOUT_MILLIS,
 ) {
     /**
      * A state representing that the two devices are close but not close enough to *start* a cast to
@@ -58,6 +55,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST,
         R.string.media_move_closer_to_start_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -71,6 +69,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST,
         R.string.media_move_closer_to_end_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -82,6 +81,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -94,6 +94,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -105,36 +106,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToThisDeviceTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        ),
+    ),
 
     /**
      * A state representing that a transfer back to this device has been successfully completed.
@@ -144,36 +122,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToReceiverTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED
+        ),
+    ),
 
     /** A state representing that a transfer to the receiver device has failed. */
     TRANSFER_TO_RECEIVER_FAILED(
@@ -181,6 +136,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that a transfer back to this device has failed. */
@@ -189,6 +145,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that this device is far away from any receiver device. */
@@ -197,37 +154,27 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER,
         stringResId = null,
         transferStatus = TransferStatus.TOO_FAR,
-    );
+        // We shouldn't be displaying the chipbar anyway
+        endItem = null,
+    ) {
+        override fun getChipTextString(context: Context, otherDeviceName: String): Text {
+            // TODO(b/245610654): Better way to handle this.
+            throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " +
+                "so its string should never be fetched")
+        }
+    };
 
     /**
      * Returns a fully-formed string with the text that the chip should display.
      *
+     * Throws an NPE if [stringResId] is null.
+     *
      * @param otherDeviceName the name of the other device involved in the transfer.
      */
-    fun getChipTextString(context: Context, otherDeviceName: String): String? {
-        if (stringResId == null) {
-            return null
-        }
-        return context.getString(stringResId, otherDeviceName)
+    open fun getChipTextString(context: Context, otherDeviceName: String): Text {
+        return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
     }
 
-    /**
-     * Returns a click listener for the undo button on the chip. Returns null if this chip state
-     * doesn't have an undo button.
-     *
-     * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar
-     *   when undo is clicked.
-     * @param undoCallback if present, the callback that should be called when the user clicks the
-     *   undo button. The undo button will only be shown if this is non-null.
-     */
-    open fun undoClickListener(
-        chipbarCoordinator: ChipbarCoordinator,
-        routeInfo: MediaRoute2Info,
-        undoCallback: IUndoMediaTransferCallback?,
-        uiEventLogger: MediaTttSenderUiEventLogger,
-        falsingManager: FalsingManager,
-    ): View.OnClickListener? = null
-
     companion object {
         /**
          * Returns the sender state enum associated with the given [displayState] from
@@ -253,9 +200,29 @@
     }
 }
 
+/** Represents the item that should be displayed in the end section of the chip. */
+sealed class SenderEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : SenderEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : SenderEndItem()
+
+    /**
+     * An undo button should be displayed.
+     *
+     * @property uiEventOnClick the UI event to log when this button is clicked.
+     * @property newState the state that should immediately be transitioned to.
+     */
+    data class UndoButton(
+        val uiEventOnClick: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState val newState: Int,
+    ) : SenderEndItem()
+}
+
 // 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 224303a..bb7bc6f 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
@@ -19,16 +19,20 @@
 import android.app.StatusBarManager
 import android.content.Context
 import android.media.MediaRoute2Info
-import android.util.Log
+import android.view.View
+import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
-import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG
+import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
 import javax.inject.Inject
 
 /**
@@ -80,7 +84,7 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logSenderStateChange(chipState)
@@ -104,10 +108,97 @@
             }
 
             displayedState = null
-            chipbarCoordinator.removeView(removalReason)
+            chipbarCoordinator.removeView(routeInfo.id, removalReason)
         } else {
             displayedState = chipState
-            chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
+            chipbarCoordinator.displayView(
+                createChipbarInfo(
+                    chipState,
+                    routeInfo,
+                    undoCallback,
+                    context,
+                    logger,
+                )
+            )
         }
     }
+
+    /**
+     * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
+     */
+    private fun createChipbarInfo(
+        chipStateSender: ChipStateSender,
+        routeInfo: MediaRoute2Info,
+        undoCallback: IUndoMediaTransferCallback?,
+        context: Context,
+        logger: MediaTttLogger,
+    ): ChipbarInfo {
+        val packageName = routeInfo.clientPackageName
+        val otherDeviceName = routeInfo.name.toString()
+
+        return ChipbarInfo(
+            // Display the app's icon as the start icon
+            startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger),
+            text = chipStateSender.getChipTextString(context, otherDeviceName),
+            endItem =
+                when (chipStateSender.endItem) {
+                    null -> null
+                    is SenderEndItem.Loading -> ChipbarEndItem.Loading
+                    is SenderEndItem.Error -> ChipbarEndItem.Error
+                    is SenderEndItem.UndoButton -> {
+                        if (undoCallback != null) {
+                            getUndoButton(
+                                undoCallback,
+                                chipStateSender.endItem.uiEventOnClick,
+                                chipStateSender.endItem.newState,
+                                routeInfo,
+                            )
+                        } else {
+                            null
+                        }
+                    }
+                },
+            vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+            windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
+            wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
+            timeoutMs = chipStateSender.timeout,
+            id = routeInfo.id,
+        )
+    }
+
+    /**
+     * Returns an undo button for the chip.
+     *
+     * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and
+     * this coordinator will transition to [newState].
+     */
+    private fun getUndoButton(
+        undoCallback: IUndoMediaTransferCallback,
+        uiEvent: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState newState: Int,
+        routeInfo: MediaRoute2Info,
+    ): ChipbarEndItem.Button {
+        val onClickListener =
+            View.OnClickListener {
+                uiEventLogger.logUndoClicked(uiEvent)
+                undoCallback.onUndoTriggered()
+
+                // The external service should eventually send us a new TransferTriggered state, but
+                // but that may take too long to go through the binder and the user may be confused
+                // as to why the UI hasn't changed yet. So, we immediately change the UI here.
+                updateMediaTapToTransferSenderDisplay(
+                    newState,
+                    routeInfo,
+                    // Since we're force-updating the UI, we don't have any [undoCallback] from the
+                    // external service (and TransferTriggered states don't have undo callbacks
+                    // anyway).
+                    undoCallback = null,
+                )
+            }
+
+        return ChipbarEndItem.Button(
+            Text.Resource(R.string.media_transfer_undo),
+            onClickListener,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
index f15720d..b963809 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
@@ -16,16 +16,36 @@
 
 package com.android.systemui.media.taptotransfer.sender
 
-/** Represents the different possible transfer states that we could be in. */
-enum class TransferStatus {
+import android.os.VibrationEffect
+
+/**
+ * Represents the different possible transfer states that we could be in and the vibration effects
+ * that come with updating transfer states.
+ *
+ * @property vibrationEffect an optional vibration effect when the transfer status is changed.
+ */
+enum class TransferStatus(
+    val vibrationEffect: VibrationEffect? = null,
+) {
     /** The transfer hasn't started yet. */
-    NOT_STARTED,
+    NOT_STARTED(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0)
+                .compose()
+    ),
     /** The transfer is currently ongoing but hasn't completed yet. */
-    IN_PROGRESS,
+    IN_PROGRESS(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70)
+                .compose(),
+    ),
     /** The transfer has completed successfully. */
     SUCCEEDED,
     /** The transfer has completed with a failure. */
-    FAILED,
+    FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)),
     /** The device is too far away to do a transfer. */
     TOO_FAR,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index 7fd100f..6c41caa 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.content.ComponentName
 import android.content.Context
+import com.android.launcher3.icons.IconFactory
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.MediaProjectionAppSelectorActivity
 import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
@@ -92,6 +93,11 @@
         ): ConfigurationController = ConfigurationControllerImpl(activity)
 
         @Provides
+        fun bindIconFactory(
+            context: Context
+        ): IconFactory = IconFactory.obtain(context)
+
+        @Provides
         @MediaProjectionAppSelector
         @MediaProjectionAppSelectorScope
         fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope =
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt
index 0927f3b..b85d628 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt
@@ -19,13 +19,14 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ComponentInfoFlags
 import android.graphics.drawable.Drawable
 import android.os.UserHandle
 import com.android.launcher3.icons.BaseIconFactory.IconOptions
 import com.android.launcher3.icons.IconFactory
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.system.PackageManagerWrapper
 import javax.inject.Inject
+import javax.inject.Provider
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.withContext
 
@@ -38,14 +39,18 @@
 constructor(
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val context: Context,
-    private val packageManager: PackageManager
+    // Use wrapper to access hidden API that allows to get ActivityInfo for any user id
+    private val packageManagerWrapper: PackageManagerWrapper,
+    private val packageManager: PackageManager,
+    private val iconFactoryProvider: Provider<IconFactory>
 ) : AppIconLoader {
 
     override suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? =
         withContext(backgroundDispatcher) {
-            IconFactory.obtain(context).use<IconFactory, Drawable?> { iconFactory ->
-                val activityInfo = packageManager
-                        .getActivityInfo(component, ComponentInfoFlags.of(0))
+            iconFactoryProvider.get().use<IconFactory, Drawable?> { iconFactory ->
+                val activityInfo =
+                    packageManagerWrapper.getActivityInfo(component, userId)
+                        ?: return@withContext null
                 val icon = activityInfo.loadIcon(packageManager) ?: return@withContext null
                 val userHandler = UserHandle.of(userId)
                 val options = IconOptions().apply { setUser(userHandler) }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt
index e8b49cd..7a77c47 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt
@@ -16,19 +16,19 @@
 
 package com.android.systemui.mediaprojection.appselector.data
 
-import android.app.ActivityManager
 import android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.kotlin.getOrNull
 import com.android.wm.shell.recents.RecentTasks
 import com.android.wm.shell.util.GroupedRecentTaskInfo
 import java.util.Optional
+import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.withContext
-import java.util.concurrent.Executor
 
 interface RecentTaskListProvider {
     /** Loads recent tasks, the returned task list is from the most-recent to least-recent order */
@@ -40,7 +40,8 @@
 constructor(
     @Background private val coroutineDispatcher: CoroutineDispatcher,
     @Background private val backgroundExecutor: Executor,
-    private val recentTasks: Optional<RecentTasks>
+    private val recentTasks: Optional<RecentTasks>,
+    private val userTracker: UserTracker
 ) : RecentTaskListProvider {
 
     private val recents by lazy { recentTasks.getOrNull() }
@@ -67,10 +68,8 @@
             getRecentTasks(
                 Integer.MAX_VALUE,
                 RECENT_IGNORE_UNAVAILABLE,
-                ActivityManager.getCurrentUser(),
+                userTracker.userId,
                 backgroundExecutor
-            ) { tasks ->
-                continuation.resume(tasks)
-            }
+            ) { tasks -> continuation.resume(tasks) }
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
index b682bd1..d4991f9 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
@@ -148,6 +148,7 @@
 
         val currentRotation: Int = display.rotation
         val displayWidthPx = windowMetrics.bounds.width()
+        val displayHeightPx = windowMetrics.bounds.height()
         val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
         val isTablet = isTablet(context)
         val taskbarSize =
@@ -163,6 +164,7 @@
             measuredWidth,
             measuredHeight,
             displayWidthPx,
+            displayHeightPx,
             taskbarSize,
             isTablet,
             currentRotation,
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
new file mode 100644
index 0000000..1324d2c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.motiontool
+
+import android.view.WindowManagerGlobal
+import com.android.app.motiontool.DdmHandleMotionTool
+import com.android.app.motiontool.MotionToolManager
+import com.android.app.viewcapture.ViewCapture
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface MotionToolModule {
+
+    companion object {
+
+        @Provides
+        fun provideDdmHandleMotionTool(motionToolManager: MotionToolManager): DdmHandleMotionTool {
+            return DdmHandleMotionTool.getInstance(motionToolManager)
+        }
+
+        @Provides
+        fun provideMotionToolManager(
+            viewCapture: ViewCapture,
+            windowManagerGlobal: WindowManagerGlobal
+        ): MotionToolManager {
+            return MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+        }
+
+        @Provides
+        fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance()
+
+        @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance()
+    }
+
+    @Binds
+    @IntoMap
+    @ClassKey(MotionToolStartable::class)
+    fun bindMotionToolStartable(impl: MotionToolStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt
new file mode 100644
index 0000000..fbb9538
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.motiontool
+
+import com.android.app.motiontool.DdmHandleMotionTool
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class MotionToolStartable
+@Inject
+internal constructor(private val ddmHandleMotionTool: DdmHandleMotionTool) : CoreStartable {
+
+    override fun start() {
+        ddmHandleMotionTool.register()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index c089511..b9f5859 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -25,11 +25,16 @@
 import static android.app.StatusBarManager.windowStateToString;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES;
+import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
+import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
 import static android.view.InsetsState.containsType;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 
@@ -77,18 +82,23 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Display;
+import android.view.DisplayCutout;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.SurfaceChangedCallback;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.InternalInsetsInfo;
 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 import android.view.WindowManager;
@@ -240,6 +250,12 @@
 
     private boolean mTransientShown;
     private boolean mTransientShownFromGestureOnSystemBar;
+    /**
+     * This is to indicate whether the navigation bar button is forced visible. This is true
+     * when the setup wizard is on display. When that happens, the window frame should be provided
+     * as insets size directly.
+     */
+    private boolean mIsButtonForceVisible;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
     private LightBarController mLightBarController;
     private final LightBarController mMainLightBarController;
@@ -354,15 +370,6 @@
         }
 
         @Override
-        public void onQuickStepStarted() {
-            // Use navbar dragging as a signal to hide the rotate button
-            mView.getRotationButtonController().setRotateSuggestionButtonState(false);
-
-            // Hide the notifications panel when quick step starts
-            mShadeController.collapsePanel(true /* animate */);
-        }
-
-        @Override
         public void onPrioritizedRotation(@Surface.Rotation int rotation) {
             mStartingQuickSwitchRotation = rotation;
             if (rotation == -1) {
@@ -475,6 +482,24 @@
                 }
             };
 
+    private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback =
+            new SurfaceChangedCallback() {
+            @Override
+            public void surfaceCreated(Transaction t) {
+                notifyNavigationBarSurface();
+            }
+
+            @Override
+            public void surfaceDestroyed() {
+                notifyNavigationBarSurface();
+            }
+
+            @Override
+            public void surfaceReplaced(Transaction t) {
+                notifyNavigationBarSurface();
+            }
+    };
+
     @Inject
     NavigationBar(
             NavigationBarView navigationBarView,
@@ -623,6 +648,10 @@
         mView.setTouchHandler(mTouchHandler);
         setNavBarMode(mNavBarMode);
         mEdgeBackGestureHandler.setStateChangeCallback(mView::updateStates);
+        mEdgeBackGestureHandler.setButtonForceVisibleChangeCallback((forceVisible) -> {
+            mIsButtonForceVisible = forceVisible;
+            repositionNavigationBar(mCurrentRotation);
+        });
         mNavigationBarTransitions.addListener(this::onBarTransition);
         mView.updateRotationButton();
 
@@ -680,7 +709,8 @@
         final Display display = mView.getDisplay();
         mView.setComponents(mRecentsOptional);
         if (mCentralSurfacesOptionalLazy.get().isPresent()) {
-            mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController());
+            mView.setComponents(
+                    mCentralSurfacesOptionalLazy.get().get().getNotificationPanelViewController());
         }
         mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
         mView.setOnVerticalChangedListener(this::onVerticalChanged);
@@ -701,6 +731,8 @@
 
         mView.getViewTreeObserver().addOnComputeInternalInsetsListener(
                 mOnComputeInternalInsetsListener);
+        mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback);
+        notifyNavigationBarSurface();
 
         mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
 
@@ -779,6 +811,10 @@
         mHandler.removeCallbacks(mEnableLayoutTransitions);
         mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
         mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener);
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
+        }
         mFrame = null;
         mOrientationHandle = null;
     }
@@ -810,7 +846,6 @@
             mLayoutDirection = ld;
             refreshLayout(ld);
         }
-
         repositionNavigationBar(rotation);
         if (canShowSecondaryHandle()) {
             if (rotation != mCurrentRotation) {
@@ -932,6 +967,12 @@
         }
     }
 
+    private void notifyNavigationBarSurface() {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        SurfaceControl surface = viewRoot != null ? viewRoot.getSurfaceControl() : null;
+        mOverviewProxyService.onNavigationBarSurfaceChanged(surface);
+    }
+
     private int deltaRotation(int oldRotation, int newRotation) {
         int delta = newRotation - oldRotation;
         if (delta < 0) delta += 4;
@@ -1044,7 +1085,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName,
+            @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails) {
         if (displayId != mDisplayId) {
             return;
@@ -1257,8 +1298,8 @@
     }
 
     private void onVerticalChanged(boolean isVertical) {
-        mCentralSurfacesOptionalLazy.get().ifPresent(
-                statusBar -> statusBar.setQsScrimEnabled(!isVertical));
+        mCentralSurfacesOptionalLazy.get().ifPresent(statusBar ->
+                statusBar.getNotificationPanelViewController().setQsScrimEnabled(!isVertical));
     }
 
     private boolean onNavigationTouch(View v, MotionEvent event) {
@@ -1599,23 +1640,15 @@
                 width,
                 height,
                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
-                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
-                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                         | WindowManager.LayoutParams.FLAG_SLIPPERY,
                 PixelFormat.TRANSLUCENT);
         lp.gravity = gravity;
-        if (insetsHeight != -1) {
-            lp.providedInsets = new InsetsFrameProvider[] {
-                new InsetsFrameProvider(ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, insetsHeight))
-            };
-        } else {
-            lp.providedInsets = new InsetsFrameProvider[] {
-                    new InsetsFrameProvider(ITYPE_NAVIGATION_BAR)
-            };
-        }
+        lp.providedInsets = getInsetsFrameProvider(insetsHeight, userContext);
+
         lp.token = new Binder();
         lp.accessibilityTitle = userContext.getString(R.string.nav_bar);
         lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC
@@ -1628,6 +1661,68 @@
         return lp;
     }
 
+    private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) {
+        final InsetsFrameProvider navBarProvider;
+        if (insetsHeight != -1 && !mIsButtonForceVisible) {
+            navBarProvider = new InsetsFrameProvider(
+                    ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, insetsHeight));
+            // Use window frame for IME.
+            navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[] {
+                    new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null)
+            };
+        } else {
+            navBarProvider = new InsetsFrameProvider(ITYPE_NAVIGATION_BAR);
+            navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[]{
+                    new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null)
+            };
+        }
+        final boolean navBarTapThrough = userContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_navBarTapThrough);
+        final InsetsFrameProvider bottomTappableProvider;
+        if (navBarTapThrough) {
+            bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT,
+                    Insets.of(0, 0, 0, 0));
+        } else {
+            bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT);
+        }
+
+        if (!mEdgeBackGestureHandler.isHandlingGestures()) {
+            // 2/3 button navigation is on. Do not provide any gesture insets here. But need to keep
+            // the provider to support runtime update.
+            return new InsetsFrameProvider[] {
+                    navBarProvider,
+                    new InsetsFrameProvider(
+                            ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.NONE),
+                    new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY,
+                            Insets.NONE, null),
+                    new InsetsFrameProvider(ITYPE_RIGHT_GESTURES,
+                            InsetsFrameProvider.SOURCE_DISPLAY,
+                            Insets.NONE, null),
+                    bottomTappableProvider
+            };
+        } else {
+            // Gesture navigation
+            final int gestureHeight = userContext.getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.navigation_bar_gesture_height);
+            final DisplayCutout cutout = userContext.getDisplay().getCutout();
+            final int safeInsetsLeft = cutout != null ? cutout.getSafeInsetLeft() : 0;
+            final int safeInsetsRight = cutout != null ? cutout.getSafeInsetRight() : 0;
+            return new InsetsFrameProvider[] {
+                    navBarProvider,
+                    new InsetsFrameProvider(
+                            ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.of(0, 0, 0, gestureHeight)),
+                    new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY,
+                            Insets.of(safeInsetsLeft
+                                    + mEdgeBackGestureHandler.getEdgeWidthLeft(), 0, 0, 0), null),
+                    new InsetsFrameProvider(ITYPE_RIGHT_GESTURES,
+                            InsetsFrameProvider.SOURCE_DISPLAY,
+                            Insets.of(0, 0, safeInsetsRight
+                                    + mEdgeBackGestureHandler.getEdgeWidthRight(), 0), null),
+                    bottomTappableProvider
+            };
+        }
+    }
+
     private boolean canShowSecondaryHandle() {
         return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java
index 59bb2278e..2a7704f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java
@@ -45,11 +45,10 @@
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.util.Objects;
 
-public class NavigationBarInflaterView extends FrameLayout
-        implements NavigationModeController.ModeChangedListener {
-
+public class NavigationBarInflaterView extends FrameLayout {
     private static final String TAG = "NavBarInflater";
 
     public static final String NAV_BAR_VIEWS = "sysui_nav_bar";
@@ -83,6 +82,24 @@
     private static final String ABSOLUTE_SUFFIX = "A";
     private static final String ABSOLUTE_VERTICAL_CENTERED_SUFFIX = "C";
 
+    private static class Listener implements NavigationModeController.ModeChangedListener {
+        private final WeakReference<NavigationBarInflaterView> mSelf;
+
+        Listener(NavigationBarInflaterView self) {
+            mSelf = new WeakReference<>(self);
+        }
+
+        @Override
+        public void onNavigationModeChanged(int mode) {
+            NavigationBarInflaterView self = mSelf.get();
+            if (self != null) {
+                self.onNavigationModeChanged(mode);
+            }
+        }
+    }
+
+    private final Listener mListener;
+
     protected LayoutInflater mLayoutInflater;
     protected LayoutInflater mLandscapeInflater;
 
@@ -106,7 +123,8 @@
         super(context, attrs);
         createInflaters();
         mOverviewProxyService = Dependency.get(OverviewProxyService.class);
-        mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this);
+        mListener = new Listener(this);
+        mNavBarMode = Dependency.get(NavigationModeController.class).addListener(mListener);
     }
 
     @VisibleForTesting
@@ -146,14 +164,13 @@
         return getContext().getString(defaultResource);
     }
 
-    @Override
-    public void onNavigationModeChanged(int mode) {
+    private void onNavigationModeChanged(int mode) {
         mNavBarMode = mode;
     }
 
     @Override
     protected void onDetachedFromWindow() {
-        Dependency.get(NavigationModeController.class).removeListener(this);
+        Dependency.get(NavigationModeController.class).removeListener(mListener);
         super.onDetachedFromWindow();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index 73fc21e..eb87ff0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -49,8 +49,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.Display;
-import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -355,7 +355,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, int behavior,
-            InsetsVisibilities requestedVisibilities, String packageName,
+            @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails) {
         mOverviewProxyService.onSystemBarAttributesChanged(displayId, behavior);
         boolean nbModeChanged = false;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index 622f5a2..83c2a5d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -412,10 +412,6 @@
         logSomePresses(action, flags);
         if (mCode == KeyEvent.KEYCODE_BACK && flags != KeyEvent.FLAG_LONG_PRESS) {
             Log.i(TAG, "Back button event: " + KeyEvent.actionToString(action));
-            if (action == MotionEvent.ACTION_UP) {
-                mOverviewProxyService.notifyBackAction((flags & KeyEvent.FLAG_CANCELED) == 0,
-                        -1, -1, true /* isButton */, false /* gestureSwipeLeft */);
-            }
         }
         final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
         final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 709467f..4e3831c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -59,7 +59,6 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
@@ -71,7 +70,7 @@
 import com.android.systemui.plugins.NavigationEdgeBackPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputChannelCompat;
@@ -102,8 +101,8 @@
 /**
  * Utility class to handle edge swipes for back gesture
  */
-public class EdgeBackGestureHandler extends CurrentUserTracker
-        implements PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> {
+public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBackPlugin>,
+        ProtoTraceable<SystemUiTraceProto> {
 
     private static final String TAG = "EdgeBackGestureHandler";
     private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
@@ -172,9 +171,11 @@
 
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final OverviewProxyService mOverviewProxyService;
     private final SysUiState mSysUiState;
     private Runnable mStateChangeCallback;
+    private Consumer<Boolean> mButtonForceVisibleCallback;
 
     private final PluginManager mPluginManager;
     private final ProtoTracer mProtoTracer;
@@ -240,6 +241,7 @@
     private boolean mIsBackGestureAllowed;
     private boolean mGestureBlockingActivityRunning;
     private boolean mIsNewBackAffordanceEnabled;
+    private boolean mIsButtonForceVisible;
 
     private InputMonitor mInputMonitor;
     private InputChannelCompat.InputEventReceiver mInputEventReceiver;
@@ -287,8 +289,6 @@
                         mBackAnimation.setTriggerBack(true);
                     }
 
-                    mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x,
-                            (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
                     logGesture(mInRejectedExclusion
                             ? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED
                             : SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED);
@@ -300,8 +300,6 @@
                         mBackAnimation.setTriggerBack(false);
                     }
                     logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE);
-                    mOverviewProxyService.notifyBackAction(false, (int) mDownPoint.x,
-                            (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
                 }
 
                 @Override
@@ -323,6 +321,15 @@
     private final Consumer<Boolean> mOnIsInPipStateChangedListener =
             (isInPip) -> mIsInPip = isInPip;
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    updateIsEnabled();
+                    updateCurrentUserResources();
+                }
+            };
+
     EdgeBackGestureHandler(
             Context context,
             OverviewProxyService overviewProxyService,
@@ -330,7 +337,7 @@
             PluginManager pluginManager,
             @Main Executor executor,
             @Background Executor backgroundExecutor,
-            BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
             ProtoTracer protoTracer,
             NavigationModeController navigationModeController,
             BackPanelController.Factory backPanelControllerFactory,
@@ -342,11 +349,11 @@
             Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider,
             Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
             FeatureFlags featureFlags) {
-        super(broadcastDispatcher);
         mContext = context;
         mDisplayId = context.getDisplayId();
         mMainExecutor = executor;
         mBackgroundExecutor = backgroundExecutor;
+        mUserTracker = userTracker;
         mOverviewProxyService = overviewProxyService;
         mSysUiState = sysUiState;
         mPluginManager = pluginManager;
@@ -402,12 +409,29 @@
         mStateChangeCallback = callback;
     }
 
+    public void setButtonForceVisibleChangeCallback(Consumer<Boolean> callback) {
+        mButtonForceVisibleCallback = callback;
+    }
+
+    public int getEdgeWidthLeft() {
+        return mEdgeWidthLeft;
+    }
+
+    public int getEdgeWidthRight() {
+        return mEdgeWidthRight;
+    }
+
     public void updateCurrentUserResources() {
         Resources res = mNavigationModeController.getCurrentUserContext().getResources();
         mEdgeWidthLeft = mGestureNavigationSettingsObserver.getLeftSensitivity(res);
         mEdgeWidthRight = mGestureNavigationSettingsObserver.getRightSensitivity(res);
-        mIsBackGestureAllowed =
-                !mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
+        final boolean previousForceVisible = mIsButtonForceVisible;
+        mIsButtonForceVisible =
+                mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
+        if (previousForceVisible != mIsButtonForceVisible && mButtonForceVisibleCallback != null) {
+            mButtonForceVisibleCallback.accept(mIsButtonForceVisible);
+        }
+        mIsBackGestureAllowed = !mIsButtonForceVisible;
 
         final DisplayMetrics dm = res.getDisplayMetrics();
         final float defaultGestureHeight = res.getDimension(
@@ -448,12 +472,6 @@
         }
     }
 
-    @Override
-    public void onUserSwitched(int newUserId) {
-        updateIsEnabled();
-        updateCurrentUserResources();
-    }
-
     /**
      * @see NavigationBarView#onAttachedToWindow()
      */
@@ -463,7 +481,7 @@
         mOverviewProxyService.addCallback(mQuickSwitchListener);
         mSysUiState.addCallback(mSysUiStateCallback);
         updateIsEnabled();
-        startTracking();
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     /**
@@ -475,7 +493,7 @@
         mOverviewProxyService.removeCallback(mQuickSwitchListener);
         mSysUiState.removeCallback(mSysUiStateCallback);
         updateIsEnabled();
-        stopTracking();
+        mUserTracker.removeCallback(mUserChangedCallback);
     }
 
     /**
@@ -785,9 +803,6 @@
 
         if (mExcludeRegion.contains(x, y)) {
             if (withinRange) {
-                // Log as exclusion only if it is in acceptable range in the first place.
-                mOverviewProxyService.notifyBackAction(
-                        false /* completed */, -1, -1, false /* isButton */, !mIsOnLeftEdge);
                 // We don't have the end point for logging purposes.
                 mEndPoint.x = -1;
                 mEndPoint.y = -1;
@@ -1081,7 +1096,7 @@
         private final PluginManager mPluginManager;
         private final Executor mExecutor;
         private final Executor mBackgroundExecutor;
-        private final BroadcastDispatcher mBroadcastDispatcher;
+        private final UserTracker mUserTracker;
         private final ProtoTracer mProtoTracer;
         private final NavigationModeController mNavigationModeController;
         private final BackPanelController.Factory mBackPanelControllerFactory;
@@ -1101,7 +1116,7 @@
                        PluginManager pluginManager,
                        @Main Executor executor,
                        @Background Executor backgroundExecutor,
-                       BroadcastDispatcher broadcastDispatcher,
+                       UserTracker userTracker,
                        ProtoTracer protoTracer,
                        NavigationModeController navigationModeController,
                        BackPanelController.Factory backPanelControllerFactory,
@@ -1119,7 +1134,7 @@
             mPluginManager = pluginManager;
             mExecutor = executor;
             mBackgroundExecutor = backgroundExecutor;
-            mBroadcastDispatcher = broadcastDispatcher;
+            mUserTracker = userTracker;
             mProtoTracer = protoTracer;
             mNavigationModeController = navigationModeController;
             mBackPanelControllerFactory = backPanelControllerFactory;
@@ -1142,7 +1157,7 @@
                     mPluginManager,
                     mExecutor,
                     mBackgroundExecutor,
-                    mBroadcastDispatcher,
+                    mUserTracker,
                     mProtoTracer,
                     mNavigationModeController,
                     mBackPanelControllerFactory,
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
new file mode 100644
index 0000000..b964b76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import android.view.KeyEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import javax.inject.Inject
+
+/**
+ * Entry point for creating and managing note.
+ *
+ * The controller decides how a note is launched based in the device state: locked or unlocked.
+ *
+ * Currently, we only support a single task per time.
+ */
+@SysUISingleton
+internal class NoteTaskController
+@Inject
+constructor(
+    private val context: Context,
+    private val intentResolver: NoteTaskIntentResolver,
+    private val optionalBubbles: Optional<Bubbles>,
+    private val optionalKeyguardManager: Optional<KeyguardManager>,
+    private val optionalUserManager: Optional<UserManager>,
+    @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+    fun handleSystemKey(keyCode: Int) {
+        if (!isEnabled) return
+
+        if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+            showNoteTask()
+        }
+    }
+
+    private fun showNoteTask() {
+        val bubbles = optionalBubbles.getOrNull() ?: return
+        val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
+        val userManager = optionalUserManager.getOrNull() ?: return
+        val intent = intentResolver.resolveIntent() ?: return
+
+        // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
+        if (!userManager.isUserUnlocked) return
+
+        if (keyguardManager.isKeyguardLocked) {
+            context.startActivity(intent)
+        } else {
+            // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
+            bubbles.showAppBubble(intent)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
new file mode 100644
index 0000000..e0bf1da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.notetask
+
+import javax.inject.Qualifier
+
+/** Key associated with a [Boolean] flag that enables or disables the note task feature. */
+@Qualifier internal annotation class NoteTaskEnabledKey
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
new file mode 100644
index 0000000..0a5b600
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.notetask
+
+import com.android.systemui.statusbar.CommandQueue
+import com.android.wm.shell.bubbles.Bubbles
+import dagger.Lazy
+import java.util.Optional
+import javax.inject.Inject
+
+/** Class responsible to "glue" all note task dependencies. */
+internal class NoteTaskInitializer
+@Inject
+constructor(
+    private val optionalBubbles: Optional<Bubbles>,
+    private val lazyNoteTaskController: Lazy<NoteTaskController>,
+    private val commandQueue: CommandQueue,
+    @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+    private val callbacks =
+        object : CommandQueue.Callbacks {
+            override fun handleSystemKey(keyCode: Int) {
+                lazyNoteTaskController.get().handleSystemKey(keyCode)
+            }
+        }
+
+    fun initialize() {
+        if (isEnabled && optionalBubbles.isPresent) {
+            commandQueue.addCallback(callbacks)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
new file mode 100644
index 0000000..98d6991
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import javax.inject.Inject
+
+/**
+ * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
+ * [Intent] ready for be launched will be returned. Otherwise, returns null.
+ *
+ * TODO(b/248274123): should be revisited once the notes role is implemented.
+ */
+internal class NoteTaskIntentResolver
+@Inject
+constructor(
+    private val packageManager: PackageManager,
+) {
+
+    fun resolveIntent(): Intent? {
+        val intent = Intent(NOTES_ACTION)
+        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+        val infoList = packageManager.queryIntentActivities(intent, flags)
+
+        for (info in infoList) {
+            val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
+            val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
+
+            return Intent(NOTES_ACTION)
+                .setComponent(ComponentName(packageName, activityName))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+
+        return null
+    }
+
+    private fun resolveActivityNameForNotesAction(packageName: String): String? {
+        val intent = Intent(NOTES_ACTION).setPackage(packageName)
+        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+        val resolveInfo = packageManager.resolveActivity(intent, flags)
+
+        val activityInfo = resolveInfo?.activityInfo ?: return null
+        if (activityInfo.name.isNullOrBlank()) return null
+        if (!activityInfo.exported) return null
+        if (!activityInfo.enabled) return null
+        if (!activityInfo.showWhenLocked) return null
+        if (!activityInfo.turnScreenOn) return null
+
+        return activityInfo.name
+    }
+
+    companion object {
+        // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
+        const val NOTES_ACTION = "android.intent.action.NOTES"
+    }
+}
+
+private val ActivityInfo.showWhenLocked: Boolean
+    get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0
+
+private val ActivityInfo.turnScreenOn: Boolean
+    get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
new file mode 100644
index 0000000..035396a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import androidx.core.content.getSystemService
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import dagger.Module
+import dagger.Provides
+import java.util.*
+
+/** Compose all dependencies required by Note Task feature. */
+@Module
+internal class NoteTaskModule {
+
+    @[Provides NoteTaskEnabledKey]
+    fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
+        return featureFlags.isEnabled(Flags.NOTE_TASKS)
+    }
+
+    @Provides
+    fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
+        return Optional.ofNullable(context.getSystemService())
+    }
+
+    @Provides
+    fun provideOptionalUserManager(context: Context): Optional<UserManager> {
+        return Optional.ofNullable(context.getSystemService())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
index be82b1f..67e9664 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
@@ -1096,7 +1096,7 @@
             Pair<Integer, Integer> first = emojiIndices.get(i - 1);
 
             // Check if second emoji starts right after first starts
-            if (second.first == first.second) {
+            if (Objects.equals(second.first, first.second)) {
                 // Check if emojis in sequence are the same
                 if (Objects.equals(emojiTexts.get(i), emojiTexts.get(i - 1))) {
                     if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
index de34cd6..88b8676 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
@@ -56,7 +56,8 @@
         val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
                 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
                 AppOpsManager.OP_PHONE_CALL_MICROPHONE,
-                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
+                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+                AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO)
         val OPS_LOCATION = intArrayOf(
                 AppOpsManager.OP_COARSE_LOCATION,
                 AppOpsManager.OP_FINE_LOCATION)
@@ -210,6 +211,7 @@
             AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+            AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
             AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
             else -> return null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index 1ea9347..03503fd 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.privacy.logging
 
 import android.permission.PermissionGroupUsage
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.PrivacyLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.privacy.PrivacyDialog
 import com.android.systemui.privacy.PrivacyItem
 import java.text.SimpleDateFormat
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 482a139..bb2b441 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -98,10 +99,10 @@
     fun init()
 
     /**
-     * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if
+     * Show the foreground services dialog. The dialog will be expanded from [expandable] if
      * it's not `null`.
      */
-    fun showDialog(viewLaunchedFrom: View?)
+    fun showDialog(expandable: Expandable?)
 
     /** Add a [OnNumberOfPackagesChangedListener]. */
     fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
@@ -367,7 +368,7 @@
 
     override fun shouldUpdateFooterVisibility() = dialog == null
 
-    override fun showDialog(viewLaunchedFrom: View?) {
+    override fun showDialog(expandable: Expandable?) {
         synchronized(lock) {
             if (dialog == null) {
 
@@ -403,16 +404,18 @@
                 }
 
                 mainExecutor.execute {
-                    viewLaunchedFrom
-                        ?.let {
-                            dialogLaunchAnimator.showFromView(
-                                dialog, it,
-                                cuj = DialogCuj(
-                                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                    INTERACTION_JANK_TAG
-                                )
+                    val controller =
+                        expandable?.dialogLaunchController(
+                            DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG,
                             )
-                        } ?: dialog.show()
+                        )
+                    if (controller != null) {
+                        dialogLaunchAnimator.show(dialog, controller)
+                    } else {
+                        dialog.show()
+                    }
                 }
 
                 backgroundExecutor.execute {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index 9d64781..a9943e8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -32,6 +32,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -156,7 +157,7 @@
             startSettingsActivity()
         } else if (v === powerMenuLite) {
             uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
-            globalActionsDialog?.showOrHideDialog(false, true, v)
+            globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite))
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index dc79f40..6f645b5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -26,6 +26,7 @@
 import javax.inject.Inject
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
 
 interface ChipVisibilityListener {
     fun onChipVisibilityRefreshed(visible: Boolean)
@@ -54,7 +55,8 @@
     private val activityStarter: ActivityStarter,
     private val appOpsController: AppOpsController,
     private val broadcastDispatcher: BroadcastDispatcher,
-    private val safetyCenterManager: SafetyCenterManager
+    private val safetyCenterManager: SafetyCenterManager,
+    private val deviceProvisionedController: DeviceProvisionedController
 ) {
 
     var chipVisibilityListener: ChipVisibilityListener? = null
@@ -134,6 +136,8 @@
 
     fun onParentVisible() {
         privacyChip.setOnClickListener {
+            // Do not expand dialog while device is not provisioned
+            if (!deviceProvisionedController.isDeviceProvisioned) return@setOnClickListener
             // If the privacy chip is visible, it means there were some indicators
             uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
             if (safetyCenterEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 0697133..f92bbf7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -364,13 +364,18 @@
     private void distributeTiles() {
         emptyAndInflateOrRemovePages();
 
-        final int tileCount = mPages.get(0).maxTiles();
-        if (DEBUG) Log.d(TAG, "Distributing tiles");
+        final int tilesPerPageCount = mPages.get(0).maxTiles();
         int index = 0;
-        final int NT = mTiles.size();
-        for (int i = 0; i < NT; i++) {
+        final int totalTilesCount = mTiles.size();
+        if (DEBUG) {
+            Log.d(TAG, "Distributing tiles: "
+                    + "[tilesPerPageCount=" + tilesPerPageCount + "]"
+                    + "[totalTilesCount=" + totalTilesCount + "]"
+            );
+        }
+        for (int i = 0; i < totalTilesCount; i++) {
             TileRecord tile = mTiles.get(i);
-            if (mPages.get(index).mRecords.size() == tileCount) index++;
+            if (mPages.get(index).mRecords.size() == tilesPerPageCount) index++;
             if (DEBUG) {
                 Log.d(TAG, "Adding " + tile.tile.getClass().getSimpleName() + " to "
                         + index);
@@ -577,8 +582,8 @@
         });
         setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
         int dx = getWidth() * lastPageNumber;
-        mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx  : dx, 0,
-            REVEAL_SCROLL_DURATION_MILLIS);
+        mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
+                REVEAL_SCROLL_DURATION_MILLIS);
         postInvalidateOnAnimation();
     }
 
@@ -738,6 +743,7 @@
 
     public interface PageListener {
         int INVALID_PAGE = -1;
+
         void onPageChanged(boolean isFirst, int pageNumber);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index ef87fb4..dc9dcc2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -29,6 +29,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.util.LargeScreenUtils;
 
 import java.io.PrintWriter;
 
@@ -52,6 +53,7 @@
     private boolean mQsDisabled;
     private int mContentHorizontalPadding = -1;
     private boolean mClippingEnabled;
+    private boolean mUseCombinedHeaders;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -66,6 +68,10 @@
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
     }
 
+    void setUseCombinedHeaders(boolean useCombinedHeaders) {
+        mUseCombinedHeaders = useCombinedHeaders;
+    }
+
     @Override
     public boolean hasOverlappingRendering() {
         return false;
@@ -143,9 +149,15 @@
 
     void updateResources(QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
+        int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
+        if (mUseCombinedHeaders
+                && !LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) {
+            topPadding = mContext.getResources()
+                    .getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
+        }
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
-                QSUtils.getQsHeaderSystemIconsAreaHeight(mContext),
+                topPadding,
                 mQSPanelContainer.getPaddingEnd(),
                 mQSPanelContainer.getPaddingBottom());
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index dea7bb5..28b4c822 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -22,6 +22,8 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -37,7 +39,6 @@
     private final ConfigurationController mConfigurationController;
     private final FalsingManager mFalsingManager;
     private final NonInterceptingScrollView mQSPanelContainer;
-
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
         @Override
@@ -65,13 +66,15 @@
             QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
             ConfigurationController configurationController,
-            FalsingManager falsingManager) {
+            FalsingManager falsingManager,
+            FeatureFlags featureFlags) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
         mFalsingManager = falsingManager;
         mQSPanelContainer = mView.getQSPanelContainer();
+        view.setUseCombinedHeaders(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
index 7511278e..b1b9dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
@@ -29,6 +29,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.dagger.QSScope;
@@ -130,7 +131,7 @@
 
     @Override
     public void onClick(View view) {
-        mFgsManagerController.showDialog(mRootView);
+        mFgsManagerController.showDialog(Expandable.fromView(view));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 498a98b..d9be281 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -49,7 +49,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
@@ -515,7 +515,13 @@
     public void setExpanded(boolean expanded) {
         if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
         mQsExpanded = expanded;
-        updateQsPanelControllerListening();
+        if (mInSplitShade && mQsExpanded) {
+            // in split shade QS is expanded immediately when shade expansion starts and then we
+            // also need to listen to changes - otherwise QS is updated only once its fully expanded
+            setListening(true);
+        } else {
+            updateQsPanelControllerListening();
+        }
         updateQsState();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
index e5d86cc..025fb22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.qs
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.QSFragmentDisableLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index f6db775..64962b4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -29,9 +29,9 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostState;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostState;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
@@ -237,7 +237,7 @@
      * @return if bouncer is in transit
      */
     public boolean isBouncerInTransit() {
-        return mStatusBarKeyguardViewManager.isBouncerInTransit();
+        return mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit();
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 2727c83..dd88c83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -25,6 +25,7 @@
 import android.content.res.Configuration;
 import android.content.res.Configuration.Orientation;
 import android.metrics.LogMaker;
+import android.util.Log;
 import android.view.View;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -32,12 +33,13 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileViewImpl;
 import com.android.systemui.util.LargeScreenUtils;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.animation.DisappearParameters;
@@ -237,6 +239,16 @@
     private void addTile(final QSTile tile, boolean collapsedView) {
         final TileRecord r =
                 new TileRecord(tile, mHost.createTileView(getContext(), tile, collapsedView));
+        // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of
+        // b/250618218.
+        try {
+            QSTileViewImpl qsTileView = (QSTileViewImpl) (r.tileView);
+            if (qsTileView != null) {
+                qsTileView.setQsLogger(mQSLogger);
+            }
+        } catch (ClassCastException e) {
+            Log.e(TAG, "Failed to cast QSTileView to QSTileViewImpl", e);
+        }
         mView.addTile(r);
         mRecords.add(r);
         mCachedSpecs = getTilesSpecs();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
index 67bf300..6c1e956 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
@@ -39,6 +39,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.common.shared.model.Icon;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -169,7 +170,7 @@
 
     // TODO(b/242040009): Remove this.
     public void showDeviceMonitoringDialog() {
-        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
+        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
index ae6ed20..67bc769 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
@@ -75,6 +75,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+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.dagger.SysUISingleton;
@@ -190,8 +191,9 @@
     }
 
     /** Show the device monitoring dialog. */
-    public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) {
-        createDialog(quickSettingsContext, view);
+    public void showDeviceMonitoringDialog(Context quickSettingsContext,
+            @Nullable Expandable expandable) {
+        createDialog(quickSettingsContext, expandable);
     }
 
     /**
@@ -440,7 +442,7 @@
         }
     }
 
-    private void createDialog(Context quickSettingsContext, @Nullable View view) {
+    private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) {
         mShouldUseSettingsButton.set(false);
         mBgHandler.post(() -> {
             String settingsButtonText = getSettingsButton();
@@ -453,9 +455,12 @@
                         ? settingsButtonText : getNegativeButton(), this);
 
                 mDialog.setView(dialogView);
-                if (view != null && view.isAggregatedVisible()) {
-                    mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
+                DialogLaunchAnimator.Controller controller =
+                        expandable != null ? expandable.dialogLaunchController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG))
+                                : null;
+                if (controller != null) {
+                    mDialogLaunchAnimator.show(mDialog, controller);
                 } else {
                     mDialog.show();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index ac46c85..f37d668 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -34,10 +34,12 @@
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
+import com.android.systemui.ProtoDumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -48,6 +50,7 @@
 import com.android.systemui.qs.external.TileServiceKey;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.nano.QsTileState;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
@@ -59,16 +62,20 @@
 import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -82,7 +89,7 @@
  * This class also provides the interface for adding/removing/changing tiles.
  */
 @SysUISingleton
-public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable {
+public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable {
     private static final String TAG = "QSTileHost";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int MAX_QS_INSTANCE_ID = 1 << 20;
@@ -671,4 +678,15 @@
         mTiles.values().stream().filter(obj -> obj instanceof Dumpable)
                 .forEach(o -> ((Dumpable) o).dump(pw, args));
     }
+
+    @Override
+    public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) {
+        List<QsTileState> data = mTiles.values().stream()
+                .map(QSTile::getState)
+                .map(TileStateToProtoKt::toProto)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        systemUIProtoDump.tiles = data.toArray(new QsTileState[0]);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 9739974..6aabe3b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -26,8 +26,8 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 27d9da6..946fe54 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -288,8 +288,15 @@
         }
 
         MarginLayoutParams qqsLP = (MarginLayoutParams) mHeaderQsPanel.getLayoutParams();
-        qqsLP.topMargin = largeScreenHeaderActive || !mUseCombinedQSHeader ? mContext.getResources()
-                .getDimensionPixelSize(R.dimen.qqs_layout_margin_top) : qsOffsetHeight;
+        if (largeScreenHeaderActive) {
+            qqsLP.topMargin = mContext.getResources()
+                    .getDimensionPixelSize(R.dimen.qqs_layout_margin_top);
+        } else if (!mUseCombinedQSHeader) {
+            qqsLP.topMargin = qsOffsetHeight;
+        } else {
+            qqsLP.topMargin = mContext.getResources()
+                    .getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height);
+        }
         mHeaderQsPanel.setLayoutParams(qqsLP);
 
         updateBatteryMode();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
index 6b0abd4..7794fa0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.qs;
 
-import android.app.ActivityManager;
 import android.database.ContentObserver;
 import android.os.Handler;
 
@@ -47,10 +46,6 @@
         this(settingsProxy, handler, settingName, userId, 0);
     }
 
-    public SettingObserver(SettingsProxy settingsProxy, Handler handler, String settingName) {
-        this(settingsProxy, handler, settingName, ActivityManager.getCurrentUser());
-    }
-
     public SettingObserver(SettingsProxy settingsProxy, Handler handler, String settingName,
             int userId, int defaultValue) {
         super(handler);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index 3d00dd4..7ee4047 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -123,7 +123,6 @@
     public boolean updateResources() {
         final Resources res = mContext.getResources();
         mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
-        updateColumns();
         mMaxCellHeight = mContext.getResources().getDimensionPixelSize(mCellHeightResId);
         mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal);
         mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
new file mode 100644
index 0000000..2c8a5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.qs
+
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.android.systemui.qs.nano.QsTileState
+import com.android.systemui.util.nano.ComponentNameProto
+
+fun QSTile.State.toProto(): QsTileState? {
+    if (TextUtils.isEmpty(spec)) return null
+    val state = QsTileState()
+    if (spec.startsWith(CustomTile.PREFIX)) {
+        val protoComponentName = ComponentNameProto()
+        val tileComponentName = CustomTile.getComponentFromSpec(spec)
+        protoComponentName.packageName = tileComponentName.packageName
+        protoComponentName.className = tileComponentName.className
+        state.componentName = protoComponentName
+    } else {
+        state.spec = spec
+    }
+    state.state =
+        when (this.state) {
+            Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE
+            Tile.STATE_INACTIVE -> QsTileState.INACTIVE
+            Tile.STATE_ACTIVE -> QsTileState.ACTIVE
+            else -> QsTileState.UNAVAILABLE
+        }
+    label?.let { state.label = it.toString() }
+    secondaryLabel?.let { state.secondaryLabel = it.toString() }
+    if (this is QSTile.BooleanState) {
+        state.booleanState = value
+    }
+    return state
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
index 703b95a..b5ceeae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
@@ -19,6 +19,7 @@
 import android.annotation.StyleRes;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.content.res.Configuration;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
@@ -33,6 +34,7 @@
 import com.android.settingslib.graph.SignalDrawable;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
+import com.android.systemui.util.LargeScreenUtils;
 
 import java.util.Objects;
 
@@ -72,6 +74,7 @@
         mMobileSignal = findViewById(R.id.mobile_signal);
         mCarrierText = findViewById(R.id.qs_carrier_text);
         mSpacer = findViewById(R.id.spacer);
+        updateResources();
     }
 
     /**
@@ -142,4 +145,20 @@
     public void updateTextAppearance(@StyleRes int resId) {
         FontSizeUtils.updateFontSizeFromStyle(mCarrierText, resId);
     }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateResources();
+    }
+
+    private void updateResources() {
+        boolean useLargeScreenHeader =
+                LargeScreenUtils.shouldUseLargeScreenShadeHeader(getResources());
+        mCarrierText.setMaxEms(
+                useLargeScreenHeader
+                        ? Integer.MAX_VALUE
+                        : getResources().getInteger(R.integer.qs_carrier_max_em)
+        );
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index cf10c79..79fcc7d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -82,12 +82,12 @@
         DefaultItemAnimator animator = new DefaultItemAnimator();
         animator.setMoveDuration(TileAdapter.MOVE_DURATION);
         mRecyclerView.setItemAnimator(animator);
+
+        updateTransparentViewHeight();
     }
 
     void updateResources() {
-        LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
-        lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
-        mTransparentView.setLayoutParams(lp);
+        updateTransparentViewHeight();
         mRecyclerView.getAdapter().notifyItemChanged(0);
     }
 
@@ -236,4 +236,10 @@
     public boolean isOpening() {
         return mOpening;
     }
+
+    private void updateTransparentViewHeight() {
+        LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
+        lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext);
+        mTransparentView.setLayoutParams(lp);
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 4cacbba..5d03da3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -35,6 +35,7 @@
 
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.settings.UserTracker;
@@ -53,6 +54,7 @@
 /**
  * Runs the day-to-day operations of which tiles should be bound and when.
  */
+@SysUISingleton
 public class TileServices extends IQSService.Stub {
     static final int DEFAULT_MAX_BOUND = 3;
     static final int REDUCED_MAX_BOUND = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index cf9b41c..03bb7a0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -23,19 +23,15 @@
 import android.content.IntentFilter
 import android.os.UserHandle
 import android.provider.Settings
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.util.FrameworkStatsLog
-import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.FgsManagerController
@@ -44,10 +40,9 @@
 import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
 import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
 import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
-import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.security.data.repository.SecurityRepository
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.user.UserSwitcherActivity
+import com.android.systemui.user.domain.interactor.UserInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
@@ -74,37 +69,27 @@
     val deviceMonitoringDialogRequests: Flow<Unit>
 
     /**
-     * Show the device monitoring dialog, expanded from [view].
-     *
-     * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment]
-     * [com.android.systemui.qs.QSFragment].
-     */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(view: View)
-
-    /**
-     * Show the device monitoring dialog.
+     * Show the device monitoring dialog, expanded from [expandable] if it's not null.
      *
      * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings
      * fragment][com.android.systemui.qs.QSFragment].
      */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(quickSettingsContext: Context)
+    fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?)
 
     /** Show the foreground services dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showForegroundServicesDialog(view: View)
+    fun showForegroundServicesDialog(expandable: Expandable)
 
     /** Show the power menu dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View)
+    fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    )
 
     /** Show the settings. */
     fun showSettings(expandable: Expandable)
 
     /** Show the user switcher. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showUserSwitcher(view: View)
+    fun showUserSwitcher(context: Context, expandable: Expandable)
 }
 
 @SysUISingleton
@@ -112,13 +97,12 @@
 @Inject
 constructor(
     private val activityStarter: ActivityStarter,
-    private val featureFlags: FeatureFlags,
     private val metricsLogger: MetricsLogger,
     private val uiEventLogger: UiEventLogger,
     private val deviceProvisionedController: DeviceProvisionedController,
     private val qsSecurityFooterUtils: QSSecurityFooterUtils,
     private val fgsManagerController: FgsManagerController,
-    private val userSwitchDialogController: UserSwitchDialogController,
+    private val userInteractor: UserInteractor,
     securityRepository: SecurityRepository,
     foregroundServicesRepository: ForegroundServicesRepository,
     userSwitcherRepository: UserSwitcherRepository,
@@ -147,28 +131,32 @@
             null,
         )
 
-    override fun showDeviceMonitoringDialog(view: View) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view)
-        DevicePolicyEventLogger.createEvent(
-                FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
-            )
-            .write()
+    override fun showDeviceMonitoringDialog(
+        quickSettingsContext: Context,
+        expandable: Expandable?,
+    ) {
+        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        if (expandable != null) {
+            DevicePolicyEventLogger.createEvent(
+                    FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
+                )
+                .write()
+        }
     }
 
-    override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
+    override fun showForegroundServicesDialog(expandable: Expandable) {
+        fgsManagerController.showDialog(expandable)
     }
 
-    override fun showForegroundServicesDialog(view: View) {
-        fgsManagerController.showDialog(view)
-    }
-
-    override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
+    override fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    ) {
         uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
         globalActionsDialogLite.showOrHideDialog(
             /* keyguardShowing= */ false,
             /* isDeviceProvisioned= */ true,
-            view,
+            expandable,
         )
     }
 
@@ -189,23 +177,7 @@
         )
     }
 
-    override fun showUserSwitcher(view: View) {
-        if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
-            userSwitchDialogController.showDialog(view)
-            return
-        }
-
-        val intent =
-            Intent(view.context, UserSwitcherActivity::class.java).apply {
-                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
-            }
-
-        activityStarter.startActivity(
-            intent,
-            true /* dismissShade */,
-            ActivityLaunchAnimator.Controller.fromView(view, null),
-            true /* showOverlockscreenwhenlocked */,
-            UserHandle.SYSTEM,
-        )
+    override fun showUserSwitcher(context: Context, expandable: Expandable) {
+        userInteractor.showUserSwitcher(context, expandable)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index dd1ffcc..3e39c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -31,6 +31,7 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.R
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.people.ui.view.PeopleViewBinder.bind
@@ -125,7 +126,7 @@
                 launch {
                     viewModel.security.collect { security ->
                         if (previousSecurity != security) {
-                            bindSecurity(securityHolder, security)
+                            bindSecurity(view.context, securityHolder, security)
                             previousSecurity = security
                         }
                     }
@@ -159,6 +160,7 @@
     }
 
     private fun bindSecurity(
+        quickSettingsContext: Context,
         securityHolder: TextButtonViewHolder,
         security: FooterActionsSecurityButtonViewModel?,
     ) {
@@ -171,9 +173,12 @@
         // Make sure that the chevron is visible and that the button is clickable if there is a
         // listener.
         val chevron = securityHolder.chevron
-        if (security.onClick != null) {
+        val onClick = security.onClick
+        if (onClick != null) {
             securityView.isClickable = true
-            securityView.setOnClickListener(security.onClick)
+            securityView.setOnClickListener {
+                onClick(quickSettingsContext, Expandable.fromView(securityView))
+            }
             chevron.isVisible = true
         } else {
             securityView.isClickable = false
@@ -205,7 +210,9 @@
             foregroundServicesWithNumberView.isVisible = false
 
             foregroundServicesWithTextView.isVisible = true
-            foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithTextView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithTextHolder.text.text = foregroundServices.text
             foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
         } else {
@@ -213,7 +220,9 @@
             foregroundServicesWithTextView.isVisible = false
 
             foregroundServicesWithNumberView.visibility = View.VISIBLE
-            foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithNumberView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
             foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
@@ -229,7 +238,7 @@
         }
 
         buttonView.setBackgroundResource(model.background)
-        buttonView.setOnClickListener(model.onClick)
+        buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
 
         val icon = model.icon
         val iconView = button.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 9b5f683..8d819da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.footer.ui.viewmodel
 
 import android.annotation.DrawableRes
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /**
@@ -29,7 +29,5 @@
     val icon: Icon,
     val iconTint: Int?,
     @DrawableRes val background: Int,
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
index 98b53cb..ff8130d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 
 /** A ViewModel for the foreground services button. */
 data class FooterActionsForegroundServicesButtonViewModel(
@@ -24,5 +24,5 @@
     val text: String,
     val displayText: Boolean,
     val hasNewChanges: Boolean,
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
index 98ab129..3450505 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
@@ -16,12 +16,13 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import android.content.Context
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /** A ViewModel for the security button. */
 data class FooterActionsSecurityButtonViewModel(
     val icon: Icon,
     val text: String,
-    val onClick: ((View) -> Unit)?,
+    val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index d3c06f6..dee6fad 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.View
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -199,50 +198,51 @@
      */
     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
-            footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext)
+            footerActionsInteractor.showDeviceMonitoringDialog(
+                quickSettingsContext,
+                expandable = null,
+            )
         }
     }
 
-    private fun onSecurityButtonClicked(view: View) {
+    private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showDeviceMonitoringDialog(view)
+        footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
-    private fun onForegroundServiceButtonClicked(view: View) {
+    private fun onForegroundServiceButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showForegroundServicesDialog(view)
+        footerActionsInteractor.showForegroundServicesDialog(expandable)
     }
 
-    private fun onUserSwitcherClicked(view: View) {
+    private fun onUserSwitcherClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showUserSwitcher(view)
+        footerActionsInteractor.showUserSwitcher(context, expandable)
     }
 
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    private fun onSettingsButtonClicked(view: View) {
+    private fun onSettingsButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showSettings(Expandable.fromView(view))
+        footerActionsInteractor.showSettings(expandable)
     }
 
-    private fun onPowerButtonClicked(view: View) {
+    private fun onPowerButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view)
+        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
     }
 
     private fun userSwitcherButton(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 6038006..9f6317f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.qs.logging
 
 import android.service.quicksettings.Tile
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.StatusBarState
 import javax.inject.Inject
@@ -129,12 +129,36 @@
         })
     }
 
-    fun logInternetTileUpdate(lastType: Int, callback: String) {
+    fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) {
         log(VERBOSE, {
+            str1 = tileSpec
             int1 = lastType
-            str1 = callback
+            str2 = callback
         }, {
-            "mLastTileState=$int1, Callback=$str1."
+            "[$str1] mLastTileState=$int1, Callback=$str2."
+        })
+    }
+
+    // TODO(b/250618218): Remove this method once we know the root cause of b/250618218.
+    fun logTileBackgroundColorUpdateIfInternetTile(
+        tileSpec: String,
+        state: Int,
+        disabledByPolicy: Boolean,
+        color: Int
+    ) {
+        // This method is added to further debug b/250618218 which has only been observed from the
+        // InternetTile, so we are only logging the background color change for the InternetTile
+        // to avoid spamming the QSLogger.
+        if (tileSpec != "internet") {
+            return
+        }
+        log(VERBOSE, {
+            str1 = tileSpec
+            int1 = state
+            bool1 = disabledByPolicy
+            int2 = color
+        }, {
+            "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2."
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
new file mode 100644
index 0000000..2a61033
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package com.android.systemui.qs;
+
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto";
+
+option java_multiple_files = true;
+
+message QsTileState {
+  oneof identifier {
+    string spec = 1;
+    com.android.systemui.util.ComponentNameProto component_name = 2;
+  }
+
+  enum State {
+    UNAVAILABLE = 0;
+    INACTIVE = 1;
+    ACTIVE = 2;
+  }
+
+  State state = 3;
+  oneof optional_boolean_state {
+    bool boolean_state = 4;
+  }
+  oneof optional_label {
+    string label = 5;
+  }
+  oneof optional_secondary_label {
+    string secondary_label = 6;
+  }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index e9a6c25..cd69f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -143,13 +143,18 @@
             if (d instanceof Animatable2) {
                 Animatable2 a = (Animatable2) d;
                 a.start();
-                if (state.isTransient) {
-                    a.registerAnimationCallback(new AnimationCallback() {
-                        @Override
-                        public void onAnimationEnd(Drawable drawable) {
-                            a.start();
-                        }
-                    });
+                if (shouldAnimate) {
+                    if (state.isTransient) {
+                        a.registerAnimationCallback(new AnimationCallback() {
+                            @Override
+                            public void onAnimationEnd(Drawable drawable) {
+                                a.start();
+                            }
+                        });
+                    }
+                } else {
+                    // Sends animator to end of animation. Needs to be called after calling start.
+                    a.stop();
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 972b243..b355d4b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTile.BooleanState
 import com.android.systemui.plugins.qs.QSTileView
+import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
 import java.util.Objects
 
@@ -116,7 +117,7 @@
     protected lateinit var sideView: ViewGroup
     private lateinit var customDrawableView: ImageView
     private lateinit var chevronView: ImageView
-
+    private var mQsLogger: QSLogger? = null
     protected var showRippleEffect = true
 
     private lateinit var ripple: RippleDrawable
@@ -188,6 +189,10 @@
         updateHeight()
     }
 
+    fun setQsLogger(qsLogger: QSLogger) {
+        mQsLogger = qsLogger
+    }
+
     fun updateResources() {
         FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
         FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
@@ -493,6 +498,11 @@
         // Colors
         if (state.state != lastState || state.disabledByPolicy || lastDisabledByPolicy) {
             singleAnimator.cancel()
+            mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
+                    state.spec,
+                    state.state,
+                    state.disabledByPolicy,
+                    getBackgroundColorForState(state.state, state.disabledByPolicy))
             if (allowAnimations) {
                 singleAnimator.setValues(
                         colorValuesHolder(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index 86d4fa3..033dbe0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -48,6 +48,7 @@
 import com.android.systemui.qs.SettingObserver;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.GlobalSettings;
 
 import javax.inject.Inject;
@@ -74,14 +75,16 @@
             QSLogger qsLogger,
             BroadcastDispatcher broadcastDispatcher,
             Lazy<ConnectivityManager> lazyConnectivityManager,
-            GlobalSettings globalSettings
+            GlobalSettings globalSettings,
+            UserTracker userTracker
     ) {
         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mBroadcastDispatcher = broadcastDispatcher;
         mLazyConnectivityManager = lazyConnectivityManager;
 
-        mSetting = new SettingObserver(globalSettings, mHandler, Global.AIRPLANE_MODE_ON) {
+        mSetting = new SettingObserver(globalSettings, mHandler, Global.AIRPLANE_MODE_ON,
+                userTracker.getUserId()) {
             @Override
             protected void handleValueChanged(int value, boolean observedChange) {
                 // mHandler is the background handler so calling this is OK
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index bebd580..5bc209a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -70,7 +70,7 @@
     private final SettingObserver mDreamSettingObserver;
     private final UserTracker mUserTracker;
     private final boolean mDreamSupported;
-    private final boolean mDreamOnlyEnabledForSystemUser;
+    private final boolean mDreamOnlyEnabledForDockUser;
 
     private boolean mIsDocked = false;
 
@@ -100,22 +100,22 @@
             BroadcastDispatcher broadcastDispatcher,
             UserTracker userTracker,
             @Named(DreamModule.DREAM_SUPPORTED) boolean dreamSupported,
-            @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_SYSTEM_USER)
-                    boolean dreamOnlyEnabledForSystemUser
+            @Named(DreamModule.DREAM_ONLY_ENABLED_FOR_DOCK_USER)
+                    boolean dreamOnlyEnabledForDockUser
     ) {
         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mDreamManager = dreamManager;
         mBroadcastDispatcher = broadcastDispatcher;
         mEnabledSettingObserver = new SettingObserver(secureSettings, mHandler,
-                Settings.Secure.SCREENSAVER_ENABLED) {
+                Settings.Secure.SCREENSAVER_ENABLED, userTracker.getUserId()) {
             @Override
             protected void handleValueChanged(int value, boolean observedChange) {
                 refreshState();
             }
         };
         mDreamSettingObserver = new SettingObserver(secureSettings, mHandler,
-                Settings.Secure.SCREENSAVER_COMPONENTS) {
+                Settings.Secure.SCREENSAVER_COMPONENTS, userTracker.getUserId()) {
             @Override
             protected void handleValueChanged(int value, boolean observedChange) {
                 refreshState();
@@ -123,7 +123,7 @@
         };
         mUserTracker = userTracker;
         mDreamSupported = dreamSupported;
-        mDreamOnlyEnabledForSystemUser = dreamOnlyEnabledForSystemUser;
+        mDreamOnlyEnabledForDockUser = dreamOnlyEnabledForDockUser;
     }
 
     @Override
@@ -203,7 +203,8 @@
         // For now, restrict to debug users.
         return Build.isDebuggable()
                 && mDreamSupported
-                && (!mDreamOnlyEnabledForSystemUser || mUserTracker.getUserHandle().isSystem());
+                // TODO(b/257333623): Allow the Dock User to be non-SystemUser user in HSUM.
+                && (!mDreamOnlyEnabledForDockUser || mUserTracker.getUserHandle().isSystem());
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index ae46477..350d8b0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -387,7 +387,8 @@
 
     @Override
     protected void handleUpdateState(SignalState state, Object arg) {
-        mQSLogger.logInternetTileUpdate(mLastTileState, arg == null ? "null" : arg.toString());
+        mQSLogger.logInternetTileUpdate(
+                getTileSpec(), mLastTileState, arg == null ? "null" : arg.toString());
         if (arg instanceof CellularCallbackInfo) {
             mLastTileState = LAST_STATE_CELLULAR;
             handleUpdateCellularState(state, arg);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index b415022..376d3d8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -115,7 +115,7 @@
         state.label = mContext.getString(R.string.qr_code_scanner_title);
         state.contentDescription = state.label;
         state.icon = ResourceIcon.get(R.drawable.ic_qr_code_scanner);
-        state.state = mQRCodeScannerController.isEnabledForQuickSettings() ? Tile.STATE_ACTIVE
+        state.state = mQRCodeScannerController.isEnabledForQuickSettings() ? Tile.STATE_INACTIVE
                 : Tile.STATE_UNAVAILABLE;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index f63f044..64a8a14 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles;
 
+import android.app.Dialog;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.Looper;
@@ -43,7 +44,6 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.screenrecord.RecordingController;
-import com.android.systemui.screenrecord.ScreenRecordDialog;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -170,9 +170,9 @@
             mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
             getHost().collapsePanels();
         };
-        ScreenRecordDialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
-                mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
 
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
+                mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
         ActivityStarter.OnDismissAction dismissAction = () -> {
             if (shouldAnimateFromView) {
                 mDialogLaunchAnimator.showFromView(dialog, view, new DialogCuj(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index d2d5063..57a00c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -26,6 +26,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
@@ -43,6 +44,9 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.data.source.UserRecord;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import javax.inject.Inject;
 
 /**
@@ -83,6 +87,13 @@
         private final FalsingManager mFalsingManager;
         private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
 
+        @NonNull
+        @Override
+        protected List<UserRecord> getUsers() {
+            return super.getUsers().stream().filter(
+                    userRecord -> !userRecord.isManageUsers).collect(Collectors.toList());
+        }
+
         @Inject
         public Adapter(Context context, UserSwitcherController controller,
                 UiEventLogger uiEventLogger, FalsingManager falsingManager) {
@@ -193,6 +204,15 @@
             Trace.endSection();
         }
 
+        @Override
+        public void onUserListItemClicked(@NonNull UserRecord record,
+                @Nullable UserSwitchDialogController.DialogShower dialogShower) {
+            if (dialogShower != null) {
+                mDialogShower.dismiss();
+            }
+            super.onUserListItemClicked(record, dialogShower);
+        }
+
         public void linkToViewGroup(ViewGroup viewGroup) {
             PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 24c4723..9743c3e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -39,6 +39,7 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewStub;
 import android.view.Window;
 import android.view.WindowManager;
 import android.widget.Button;
@@ -62,6 +63,7 @@
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.accessibility.floatingmenu.AnnotationLinkSpan;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -72,6 +74,8 @@
 import java.util.List;
 import java.util.concurrent.Executor;
 
+import javax.inject.Inject;
+
 /**
  * Dialog for showing mobile network, connected Wi-Fi network and Wi-Fi networks.
  */
@@ -86,6 +90,7 @@
 
     private final Handler mHandler;
     private final Executor mBackgroundExecutor;
+    private final DialogLaunchAnimator mDialogLaunchAnimator;
 
     @VisibleForTesting
     protected InternetAdapter mAdapter;
@@ -109,6 +114,7 @@
     private LinearLayout mInternetDialogLayout;
     private LinearLayout mConnectedWifListLayout;
     private LinearLayout mMobileNetworkLayout;
+    private LinearLayout mSecondaryMobileNetworkLayout;
     private LinearLayout mTurnWifiOnLayout;
     private LinearLayout mEthernetLayout;
     private TextView mWifiToggleTitleText;
@@ -123,6 +129,8 @@
     private ImageView mSignalIcon;
     private TextView mMobileTitleText;
     private TextView mMobileSummaryText;
+    private TextView mSecondaryMobileTitleText;
+    private TextView  mSecondaryMobileSummaryText;
     private TextView mAirplaneModeSummaryText;
     private Switch mMobileDataToggle;
     private View mMobileToggleDivider;
@@ -158,9 +166,11 @@
         mInternetDialogSubTitle.setText(getSubtitleText());
     };
 
+    @Inject
     public InternetDialog(Context context, InternetDialogFactory internetDialogFactory,
             InternetDialogController internetDialogController, boolean canConfigMobileData,
             boolean canConfigWifi, boolean aboveStatusBar, UiEventLogger uiEventLogger,
+            DialogLaunchAnimator dialogLaunchAnimator,
             @Main Handler handler, @Background Executor executor,
             KeyguardStateController keyguardStateController) {
         super(context);
@@ -183,6 +193,7 @@
         mKeyguard = keyguardStateController;
 
         mUiEventLogger = uiEventLogger;
+        mDialogLaunchAnimator = dialogLaunchAnimator;
         mAdapter = new InternetAdapter(mInternetDialogController);
         if (!aboveStatusBar) {
             getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
@@ -238,15 +249,7 @@
         mBackgroundOn = mContext.getDrawable(R.drawable.settingslib_switch_bar_bg_on);
         mInternetDialogTitle.setText(getDialogTitleText());
         mInternetDialogTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
-
-        TypedArray typedArray = mContext.obtainStyledAttributes(
-                new int[]{android.R.attr.selectableItemBackground});
-        try {
-            mBackgroundOff = typedArray.getDrawable(0 /* index */);
-        } finally {
-            typedArray.recycle();
-        }
-
+        mBackgroundOff = mContext.getDrawable(R.drawable.internet_dialog_selected_effect);
         setOnClickListener();
         mTurnWifiOnLayout.setBackground(null);
         mAirplaneModeButton.setVisibility(
@@ -287,6 +290,9 @@
         mMobileNetworkLayout.setOnClickListener(null);
         mMobileDataToggle.setOnCheckedChangeListener(null);
         mConnectedWifListLayout.setOnClickListener(null);
+        if (mSecondaryMobileNetworkLayout != null) {
+            mSecondaryMobileNetworkLayout.setOnClickListener(null);
+        }
         mSeeAllLayout.setOnClickListener(null);
         mWiFiToggle.setOnCheckedChangeListener(null);
         mDoneButton.setOnClickListener(null);
@@ -341,6 +347,10 @@
 
     private void setOnClickListener() {
         mMobileNetworkLayout.setOnClickListener(v -> {
+            int autoSwitchNonDdsSubId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+            if (autoSwitchNonDdsSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+                showTurnOffAutoDataSwitchDialog(autoSwitchNonDdsSubId);
+            }
             mInternetDialogController.connectCarrierNetwork();
         });
         mMobileDataToggle.setOnCheckedChangeListener(
@@ -385,11 +395,14 @@
         if (!mInternetDialogController.hasActiveSubId()
                 && (!isWifiEnabled || !isCarrierNetworkActive)) {
             mMobileNetworkLayout.setVisibility(View.GONE);
+            if (mSecondaryMobileNetworkLayout != null) {
+                mSecondaryMobileNetworkLayout.setVisibility(View.GONE);
+            }
         } else {
             mMobileNetworkLayout.setVisibility(View.VISIBLE);
             mMobileDataToggle.setChecked(mInternetDialogController.isMobileDataEnabled());
-            mMobileTitleText.setText(getMobileNetworkTitle());
-            String summary = getMobileNetworkSummary();
+            mMobileTitleText.setText(getMobileNetworkTitle(mDefaultDataSubId));
+            String summary = getMobileNetworkSummary(mDefaultDataSubId);
             if (!TextUtils.isEmpty(summary)) {
                 mMobileSummaryText.setText(
                         Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY));
@@ -399,28 +412,11 @@
                 mMobileSummaryText.setVisibility(View.GONE);
             }
             mBackgroundExecutor.execute(() -> {
-                Drawable drawable = getSignalStrengthDrawable();
+                Drawable drawable = getSignalStrengthDrawable(mDefaultDataSubId);
                 mHandler.post(() -> {
                     mSignalIcon.setImageDrawable(drawable);
                 });
             });
-            mMobileTitleText.setTextAppearance(isNetworkConnected
-                    ? R.style.TextAppearance_InternetDialog_Active
-                    : R.style.TextAppearance_InternetDialog);
-            int secondaryRes = isNetworkConnected
-                    ? R.style.TextAppearance_InternetDialog_Secondary_Active
-                    : R.style.TextAppearance_InternetDialog_Secondary;
-            mMobileSummaryText.setTextAppearance(secondaryRes);
-            // Set airplane mode to the summary for carrier network
-            if (mInternetDialogController.isAirplaneModeEnabled()) {
-                mAirplaneModeSummaryText.setVisibility(View.VISIBLE);
-                mAirplaneModeSummaryText.setText(mContext.getText(R.string.airplane_mode));
-                mAirplaneModeSummaryText.setTextAppearance(secondaryRes);
-            } else {
-                mAirplaneModeSummaryText.setVisibility(View.GONE);
-            }
-            mMobileNetworkLayout.setBackground(
-                    isNetworkConnected ? mBackgroundOn : mBackgroundOff);
 
             TypedArray array = mContext.obtainStyledAttributes(
                     R.style.InternetDialog_Divider_Active, new int[]{android.R.attr.background});
@@ -433,6 +429,86 @@
             mMobileDataToggle.setVisibility(mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
             mMobileToggleDivider.setVisibility(
                     mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
+
+            // Display the info for the non-DDS if it's actively being used
+            int autoSwitchNonDdsSubId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+            int nonDdsVisibility = autoSwitchNonDdsSubId
+                    != SubscriptionManager.INVALID_SUBSCRIPTION_ID ? View.VISIBLE : View.GONE;
+
+            int secondaryRes = isNetworkConnected
+                    ? R.style.TextAppearance_InternetDialog_Secondary_Active
+                    : R.style.TextAppearance_InternetDialog_Secondary;
+            if (nonDdsVisibility == View.VISIBLE) {
+                // non DDS is the currently active sub, set primary visual for it
+                ViewStub stub = mDialogView.findViewById(R.id.secondary_mobile_network_stub);
+                if (stub != null) {
+                    stub.inflate();
+                }
+                mSecondaryMobileNetworkLayout = findViewById(R.id.secondary_mobile_network_layout);
+                mSecondaryMobileNetworkLayout.setOnClickListener(
+                        this::onClickConnectedSecondarySub);
+                mSecondaryMobileNetworkLayout.setBackground(mBackgroundOn);
+
+                mSecondaryMobileTitleText = mDialogView.requireViewById(
+                        R.id.secondary_mobile_title);
+                mSecondaryMobileTitleText.setText(getMobileNetworkTitle(autoSwitchNonDdsSubId));
+                mSecondaryMobileTitleText.setTextAppearance(
+                        R.style.TextAppearance_InternetDialog_Active);
+
+                mSecondaryMobileSummaryText =
+                        mDialogView.requireViewById(R.id.secondary_mobile_summary);
+                summary = getMobileNetworkSummary(autoSwitchNonDdsSubId);
+                if (!TextUtils.isEmpty(summary)) {
+                    mSecondaryMobileSummaryText.setText(
+                            Html.fromHtml(summary, Html.FROM_HTML_MODE_LEGACY));
+                    mSecondaryMobileSummaryText.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
+                    mSecondaryMobileSummaryText.setTextAppearance(
+                            R.style.TextAppearance_InternetDialog_Active);
+                }
+
+                ImageView mSecondarySignalIcon =
+                        mDialogView.requireViewById(R.id.secondary_signal_icon);
+                mBackgroundExecutor.execute(() -> {
+                    Drawable drawable = getSignalStrengthDrawable(autoSwitchNonDdsSubId);
+                    mHandler.post(() -> {
+                        mSecondarySignalIcon.setImageDrawable(drawable);
+                    });
+                });
+
+                ImageView mSecondaryMobileSettingsIcon =
+                        mDialogView.requireViewById(R.id.secondary_settings_icon);
+                mSecondaryMobileSettingsIcon.setColorFilter(
+                        mContext.getColor(R.color.connected_network_primary_color));
+
+                // set secondary visual for default data sub
+                mMobileNetworkLayout.setBackground(mBackgroundOff);
+                mMobileTitleText.setTextAppearance(R.style.TextAppearance_InternetDialog);
+                mMobileSummaryText.setTextAppearance(
+                        R.style.TextAppearance_InternetDialog_Secondary);
+                mSignalIcon.setColorFilter(
+                        mContext.getColor(R.color.connected_network_secondary_color));
+            } else {
+                mMobileNetworkLayout.setBackground(
+                        isNetworkConnected ? mBackgroundOn : mBackgroundOff);
+                mMobileTitleText.setTextAppearance(isNetworkConnected
+                        ?
+                        R.style.TextAppearance_InternetDialog_Active
+                        : R.style.TextAppearance_InternetDialog);
+                mMobileSummaryText.setTextAppearance(secondaryRes);
+            }
+
+            if (mSecondaryMobileNetworkLayout != null) {
+                mSecondaryMobileNetworkLayout.setVisibility(nonDdsVisibility);
+            }
+
+            // Set airplane mode to the summary for carrier network
+            if (mInternetDialogController.isAirplaneModeEnabled()) {
+                mAirplaneModeSummaryText.setVisibility(View.VISIBLE);
+                mAirplaneModeSummaryText.setText(mContext.getText(R.string.airplane_mode));
+                mAirplaneModeSummaryText.setTextAppearance(secondaryRes);
+            } else {
+                mAirplaneModeSummaryText.setVisibility(View.GONE);
+            }
         }
     }
 
@@ -471,6 +547,10 @@
                 mInternetDialogController.getInternetWifiDrawable(mConnectedWifiEntry));
         mWifiSettingsIcon.setColorFilter(
                 mContext.getColor(R.color.connected_network_primary_color));
+
+        if (mSecondaryMobileNetworkLayout != null) {
+            mSecondaryMobileNetworkLayout.setVisibility(View.GONE);
+        }
     }
 
     @MainThread
@@ -541,6 +621,11 @@
         mInternetDialogController.launchWifiDetailsSetting(mConnectedWifiEntry.getKey(), view);
     }
 
+    /** For DSDS auto data switch **/
+    void onClickConnectedSecondarySub(View view) {
+        mInternetDialogController.launchMobileNetworkSettings(view);
+    }
+
     void onClickSeeMoreButton(View view) {
         mInternetDialogController.launchNetworkSetting(view);
     }
@@ -555,16 +640,16 @@
                 mIsProgressBarVisible && !mIsSearchingHidden);
     }
 
-    private Drawable getSignalStrengthDrawable() {
-        return mInternetDialogController.getSignalStrengthDrawable();
+    private Drawable getSignalStrengthDrawable(int subId) {
+        return mInternetDialogController.getSignalStrengthDrawable(subId);
     }
 
-    CharSequence getMobileNetworkTitle() {
-        return mInternetDialogController.getMobileNetworkTitle();
+    CharSequence getMobileNetworkTitle(int subId) {
+        return mInternetDialogController.getMobileNetworkTitle(subId);
     }
 
-    String getMobileNetworkSummary() {
-        return mInternetDialogController.getMobileNetworkSummary();
+    String getMobileNetworkSummary(int subId) {
+        return mInternetDialogController.getMobileNetworkSummary(subId);
     }
 
     protected void showProgressBar() {
@@ -602,8 +687,8 @@
     }
 
     private void showTurnOffMobileDialog() {
-        CharSequence carrierName = getMobileNetworkTitle();
-        boolean isInService = mInternetDialogController.isVoiceStateInService();
+        CharSequence carrierName = getMobileNetworkTitle(mDefaultDataSubId);
+        boolean isInService = mInternetDialogController.isVoiceStateInService(mDefaultDataSubId);
         if (TextUtils.isEmpty(carrierName) || !isInService) {
             carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier);
         }
@@ -627,7 +712,33 @@
         SystemUIDialog.setShowForAllUsers(mAlertDialog, true);
         SystemUIDialog.registerDismissListener(mAlertDialog);
         SystemUIDialog.setWindowOnTop(mAlertDialog, mKeyguard.isShowing());
-        mAlertDialog.show();
+        mDialogLaunchAnimator.showFromDialog(mAlertDialog, this, null, false);
+    }
+
+    private void showTurnOffAutoDataSwitchDialog(int subId) {
+        CharSequence carrierName = getMobileNetworkTitle(mDefaultDataSubId);
+        if (TextUtils.isEmpty(carrierName)) {
+            carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier);
+        }
+        mAlertDialog = new Builder(mContext)
+                .setTitle(mContext.getString(R.string.auto_data_switch_disable_title, carrierName))
+                .setMessage(R.string.auto_data_switch_disable_message)
+                .setNegativeButton(R.string.auto_data_switch_dialog_negative_button,
+                        (d, w) -> {})
+                .setPositiveButton(R.string.auto_data_switch_dialog_positive_button,
+                        (d, w) -> {
+                            mInternetDialogController
+                                    .setAutoDataSwitchMobileDataPolicy(subId, false);
+                            if (mSecondaryMobileNetworkLayout != null) {
+                                mSecondaryMobileNetworkLayout.setVisibility(View.GONE);
+                            }
+                        })
+                .create();
+        mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+        SystemUIDialog.setShowForAllUsers(mAlertDialog, true);
+        SystemUIDialog.registerDismissListener(mAlertDialog);
+        SystemUIDialog.setWindowOnTop(mAlertDialog, mKeyguard.isShowing());
+        mDialogLaunchAnimator.showFromDialog(mAlertDialog, this, null, false);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 0e00c46..aa6e678 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -37,6 +37,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.wifi.WifiManager;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -78,6 +79,8 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.connectivity.AccessPointController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -90,6 +93,7 @@
 import com.android.wifitrackerlib.WifiEntry;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -113,6 +117,17 @@
             "android.settings.NETWORK_PROVIDER_SETTINGS";
     private static final String ACTION_WIFI_SCANNING_SETTINGS =
             "android.settings.WIFI_SCANNING_SETTINGS";
+    /**
+     * Fragment "key" argument passed thru {@link #SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS}
+     */
+    private static final String SETTINGS_EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+    /**
+     * When starting this activity, this extra can also be specified to supply a Bundle of arguments
+     * to pass to that fragment when it is instantiated during the initial creation of the activity.
+     */
+    private static final String SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS =
+            ":settings:show_fragment_args";
+    private static final String AUTO_DATA_SWITCH_SETTING_R_ID = "auto_data_switch";
     public static final Drawable EMPTY_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
     public static final int NO_CELL_DATA_TYPE_ICON = 0;
     private static final int SUBTITLE_TEXT_WIFI_IS_OFF = R.string.wifi_is_off;
@@ -130,9 +145,12 @@
 
     static final int MAX_WIFI_ENTRY_COUNT = 3;
 
+    private final FeatureFlags mFeatureFlags;
+
     private WifiManager mWifiManager;
     private Context mContext;
     private SubscriptionManager mSubscriptionManager;
+    private Map<Integer, TelephonyManager> mSubIdTelephonyManagerMap = new HashMap<>();
     private TelephonyManager mTelephonyManager;
     private ConnectivityManager mConnectivityManager;
     private CarrierConfigTracker mCarrierConfigTracker;
@@ -155,6 +173,7 @@
     private WindowManager mWindowManager;
     private ToastFactory mToastFactory;
     private SignalDrawable mSignalDrawable;
+    private SignalDrawable mSecondarySignalDrawable; // For the secondary mobile data sub in DSDS
     private LocationController mLocationController;
     private DialogLaunchAnimator mDialogLaunchAnimator;
     private boolean mHasWifiEntries;
@@ -213,7 +232,8 @@
             CarrierConfigTracker carrierConfigTracker,
             LocationController locationController,
             DialogLaunchAnimator dialogLaunchAnimator,
-            WifiStateWorker wifiStateWorker
+            WifiStateWorker wifiStateWorker,
+            FeatureFlags featureFlags
     ) {
         if (DEBUG) {
             Log.d(TAG, "Init InternetDialogController");
@@ -242,10 +262,12 @@
         mWindowManager = windowManager;
         mToastFactory = toastFactory;
         mSignalDrawable = new SignalDrawable(mContext);
+        mSecondarySignalDrawable = new SignalDrawable(mContext);
         mLocationController = locationController;
         mDialogLaunchAnimator = dialogLaunchAnimator;
         mConnectedWifiInternetMonitor = new ConnectedWifiInternetMonitor();
         mWifiStateWorker = wifiStateWorker;
+        mFeatureFlags = featureFlags;
     }
 
     void onStart(@NonNull InternetDialogCallback callback, boolean canConfigWifi) {
@@ -267,6 +289,7 @@
         }
         mConfig = MobileMappings.Config.readConfig(mContext);
         mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
+        mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
         mInternetTelephonyCallback = new InternetTelephonyCallback();
         mTelephonyManager.registerTelephonyCallback(mExecutor, mInternetTelephonyCallback);
         // Listen the connectivity changes
@@ -280,7 +303,9 @@
             Log.d(TAG, "onStop");
         }
         mBroadcastDispatcher.unregisterReceiver(mConnectionStateReceiver);
-        mTelephonyManager.unregisterTelephonyCallback(mInternetTelephonyCallback);
+        for (TelephonyManager tm : mSubIdTelephonyManagerMap.values()) {
+            tm.unregisterTelephonyCallback(mInternetTelephonyCallback);
+        }
         mSubscriptionManager.removeOnSubscriptionsChangedListener(
                 mOnSubscriptionsChangedListener);
         mAccessPointController.removeAccessPointCallback(this);
@@ -371,7 +396,10 @@
         if (DEBUG) {
             Log.d(TAG, "No Wi-Fi item.");
         }
-        if (!hasActiveSubId() || (!isVoiceStateInService() && !isDataStateInService())) {
+        boolean isActiveOnNonDds = getActiveAutoSwitchNonDdsSubId() != SubscriptionManager
+                .INVALID_SUBSCRIPTION_ID;
+        if (!hasActiveSubId() || (!isVoiceStateInService(mDefaultDataSubId)
+                && !isDataStateInService(mDefaultDataSubId) && !isActiveOnNonDds)) {
             if (DEBUG) {
                 Log.d(TAG, "No carrier or service is out of service.");
             }
@@ -412,7 +440,7 @@
         return drawable;
     }
 
-    Drawable getSignalStrengthDrawable() {
+    Drawable getSignalStrengthDrawable(int subId) {
         Drawable drawable = mContext.getDrawable(
                 R.drawable.ic_signal_strength_zero_bar_no_internet);
         try {
@@ -424,9 +452,10 @@
             }
 
             boolean isCarrierNetworkActive = isCarrierNetworkActive();
-            if (isDataStateInService() || isVoiceStateInService() || isCarrierNetworkActive) {
+            if (isDataStateInService(subId) || isVoiceStateInService(subId)
+                    || isCarrierNetworkActive) {
                 AtomicReference<Drawable> shared = new AtomicReference<>();
-                shared.set(getSignalStrengthDrawableWithLevel(isCarrierNetworkActive));
+                shared.set(getSignalStrengthDrawableWithLevel(isCarrierNetworkActive, subId));
                 drawable = shared.get();
             }
 
@@ -447,24 +476,30 @@
      *
      * @return The Drawable which is a signal bar icon with level.
      */
-    Drawable getSignalStrengthDrawableWithLevel(boolean isCarrierNetworkActive) {
-        final SignalStrength strength = mTelephonyManager.getSignalStrength();
+    Drawable getSignalStrengthDrawableWithLevel(boolean isCarrierNetworkActive, int subId) {
+        TelephonyManager tm = mSubIdTelephonyManagerMap.getOrDefault(subId, mTelephonyManager);
+        final SignalStrength strength = tm.getSignalStrength();
         int level = (strength == null) ? 0 : strength.getLevel();
         int numLevels = SignalStrength.NUM_SIGNAL_STRENGTH_BINS;
         if (isCarrierNetworkActive) {
             level = getCarrierNetworkLevel();
             numLevels = WifiEntry.WIFI_LEVEL_MAX + 1;
-        } else if (mSubscriptionManager != null && shouldInflateSignalStrength(mDefaultDataSubId)) {
+        } else if (mSubscriptionManager != null && shouldInflateSignalStrength(subId)) {
             level += 1;
             numLevels += 1;
         }
-        return getSignalStrengthIcon(mContext, level, numLevels, NO_CELL_DATA_TYPE_ICON,
+        return getSignalStrengthIcon(subId, mContext, level, numLevels, NO_CELL_DATA_TYPE_ICON,
                 !isMobileDataEnabled());
     }
 
-    Drawable getSignalStrengthIcon(Context context, int level, int numLevels,
+    Drawable getSignalStrengthIcon(int subId, Context context, int level, int numLevels,
             int iconType, boolean cutOut) {
-        mSignalDrawable.setLevel(SignalDrawable.getState(level, numLevels, cutOut));
+        boolean isForDds = subId == mDefaultDataSubId;
+        if (isForDds) {
+            mSignalDrawable.setLevel(SignalDrawable.getState(level, numLevels, cutOut));
+        } else {
+            mSecondarySignalDrawable.setLevel(SignalDrawable.getState(level, numLevels, cutOut));
+        }
 
         // Make the network type drawable
         final Drawable networkDrawable =
@@ -473,7 +508,8 @@
                         : context.getResources().getDrawable(iconType, context.getTheme());
 
         // Overlay the two drawables
-        final Drawable[] layers = {networkDrawable, mSignalDrawable};
+        final Drawable[] layers = {networkDrawable, isForDds
+                ? mSignalDrawable : mSecondarySignalDrawable};
         final int iconSize =
                 context.getResources().getDimensionPixelSize(R.dimen.signal_strength_icon_size);
 
@@ -571,14 +607,39 @@
                 info -> info.uniqueName));
     }
 
-    CharSequence getMobileNetworkTitle() {
-        return getUniqueSubscriptionDisplayName(mDefaultDataSubId, mContext);
+    /**
+     * @return the subId of the visible non-DDS if it's actively being used for data, otherwise
+     * return {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+     */
+    int getActiveAutoSwitchNonDdsSubId() {
+        if (!mFeatureFlags.isEnabled(Flags.QS_SECONDARY_DATA_SUB_INFO)) {
+            // sets the non-DDS to be not found to hide its visual
+            return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+        }
+        SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(
+                SubscriptionManager.getActiveDataSubscriptionId());
+        if (subInfo != null && subInfo.getSubscriptionId() != mDefaultDataSubId
+                && !subInfo.isOpportunistic()) {
+            int subId = subInfo.getSubscriptionId();
+            if (mSubIdTelephonyManagerMap.get(subId) == null) {
+                TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId);
+                secondaryTm.registerTelephonyCallback(mExecutor, mInternetTelephonyCallback);
+                mSubIdTelephonyManagerMap.put(subId, secondaryTm);
+            }
+            return subId;
+        }
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
     }
 
-    String getMobileNetworkSummary() {
+    CharSequence getMobileNetworkTitle(int subId) {
+        return getUniqueSubscriptionDisplayName(subId, mContext);
+    }
+
+    String getMobileNetworkSummary(int subId) {
         String description = getNetworkTypeDescription(mContext, mConfig,
-                mTelephonyDisplayInfo, mDefaultDataSubId);
-        return getMobileSummary(mContext, description);
+                mTelephonyDisplayInfo, subId);
+        return getMobileSummary(mContext, description, subId);
     }
 
     /**
@@ -606,22 +667,28 @@
                 ? SubscriptionManager.getResourcesForSubId(context, subId).getString(resId) : "";
     }
 
-    private String getMobileSummary(Context context, String networkTypeDescription) {
+    private String getMobileSummary(Context context, String networkTypeDescription, int subId) {
         if (!isMobileDataEnabled()) {
             return context.getString(R.string.mobile_data_off_summary);
         }
 
         String summary = networkTypeDescription;
+        boolean isForDds = subId == mDefaultDataSubId;
+        int activeSubId = getActiveAutoSwitchNonDdsSubId();
+        boolean isOnNonDds = activeSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID;
         // Set network description for the carrier network when connecting to the carrier network
         // under the airplane mode ON.
         if (activeNetworkIsCellular() || isCarrierNetworkActive()) {
             summary = context.getString(R.string.preference_summary_default_combination,
-                    context.getString(R.string.mobile_data_connection_active),
+                    context.getString(
+                            isForDds // if nonDds is active, explains Dds status as poor connection
+                                    ? (isOnNonDds ? R.string.mobile_data_poor_connection
+                                            : R.string.mobile_data_connection_active)
+                            : R.string.mobile_data_temp_connection_active),
                     networkTypeDescription);
-        } else if (!isDataStateInService()) {
+        } else if (!isDataStateInService(subId)) {
             summary = context.getString(R.string.mobile_data_no_connection);
         }
-
         return summary;
     }
 
@@ -647,6 +714,26 @@
         }
     }
 
+    void launchMobileNetworkSettings(View view) {
+        final int subId = getActiveAutoSwitchNonDdsSubId();
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            Log.w(TAG, "launchMobileNetworkSettings fail, invalid subId:" + subId);
+            return;
+        }
+        startActivity(getSubSettingIntent(subId), view);
+    }
+
+    Intent getSubSettingIntent(int subId) {
+        final Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
+
+        final Bundle fragmentArgs = new Bundle();
+        // Special contract for Settings to highlight permission row
+        fragmentArgs.putString(SETTINGS_EXTRA_FRAGMENT_ARG_KEY, AUTO_DATA_SWITCH_SETTING_R_ID);
+        fragmentArgs.putInt(Settings.EXTRA_SUB_ID, subId);
+        intent.putExtra(SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs);
+        return intent;
+    }
+
     void launchWifiScanningSetting(View view) {
         final Intent intent = new Intent(ACTION_WIFI_SCANNING_SETTINGS);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -824,8 +911,20 @@
         mWorkerHandler.post(() -> setMergedCarrierWifiEnabledIfNeed(subId, enabled));
     }
 
-    boolean isDataStateInService() {
-        final ServiceState serviceState = mTelephonyManager.getServiceState();
+    void setAutoDataSwitchMobileDataPolicy(int subId, boolean enable) {
+        TelephonyManager tm = mSubIdTelephonyManagerMap.getOrDefault(subId, mTelephonyManager);
+        if (tm == null) {
+            if (DEBUG) {
+                Log.d(TAG, "TelephonyManager is null, can not set mobile data.");
+            }
+            return;
+        }
+        tm.setMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH, enable);
+    }
+
+    boolean isDataStateInService(int subId) {
+        TelephonyManager tm = mSubIdTelephonyManagerMap.getOrDefault(subId, mTelephonyManager);
+        final ServiceState serviceState = tm.getServiceState();
         NetworkRegistrationInfo regInfo =
                 (serviceState == null) ? null : serviceState.getNetworkRegistrationInfo(
                         NetworkRegistrationInfo.DOMAIN_PS,
@@ -833,7 +932,7 @@
         return (regInfo == null) ? false : regInfo.isRegistered();
     }
 
-    boolean isVoiceStateInService() {
+    boolean isVoiceStateInService(int subId) {
         if (mTelephonyManager == null) {
             if (DEBUG) {
                 Log.d(TAG, "TelephonyManager is null, can not detect voice state.");
@@ -841,7 +940,8 @@
             return false;
         }
 
-        final ServiceState serviceState = mTelephonyManager.getServiceState();
+        TelephonyManager tm = mSubIdTelephonyManagerMap.getOrDefault(subId, mTelephonyManager);
+        final ServiceState serviceState = tm.getServiceState();
         return serviceState != null
                 && serviceState.getState() == serviceState.STATE_IN_SERVICE;
     }
@@ -1132,6 +1232,7 @@
         if (SubscriptionManager.isUsableSubscriptionId(mDefaultDataSubId)) {
             mTelephonyManager.unregisterTelephonyCallback(mInternetTelephonyCallback);
             mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
+            mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
             mTelephonyManager.registerTelephonyCallback(mHandler::post,
                     mInternetTelephonyCallback);
             mCallback.onSubscriptionsChanged(mDefaultDataSubId);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogFactory.kt
index 8566ca3..796672d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogFactory.kt
@@ -66,7 +66,8 @@
         } else {
             internetDialog = InternetDialog(
                 context, this, internetDialogController,
-                canConfigMobileData, canConfigWifi, aboveStatusBar, uiEventLogger, handler,
+                canConfigMobileData, canConfigWifi, aboveStatusBar, uiEventLogger,
+                    dialogLaunchAnimator, handler,
                 executor, keyguardStateController
             )
             if (view != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index bdcc6b0..314252b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,13 +23,13 @@
 import android.content.Intent
 import android.provider.Settings
 import android.view.LayoutInflater
-import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -77,10 +77,10 @@
      * Show a [UserDialog].
      *
      * Populate the dialog with information from and adapter obtained from
-     * [userDetailViewAdapterProvider] and show it as launched from [view].
+     * [userDetailViewAdapterProvider] and show it as launched from [expandable].
      */
-    fun showDialog(view: View) {
-        with(dialogFactory(view.context)) {
+    fun showDialog(context: Context, expandable: Expandable) {
+        with(dialogFactory(context)) {
             setShowForAllUsers(true)
             setCanceledOnTouchOutside(true)
 
@@ -112,13 +112,19 @@
 
             adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
 
-            dialogLaunchAnimator.showFromView(
-                this, view,
-                cuj = DialogCuj(
-                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                    INTERACTION_JANK_TAG
+            val controller =
+                expandable.dialogLaunchController(
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
                 )
-            )
+            if (controller != null) {
+                dialogLaunchAnimator.show(
+                    this,
+                    controller,
+                )
+            } else {
+                show()
+            }
+
             uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
             adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 66be00d..547b496 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -64,6 +64,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
 import android.view.inputmethod.InputMethodManager;
 
@@ -77,8 +78,8 @@
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.util.ScreenshotHelper;
 import com.android.systemui.Dumpable;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.ScreenLifecycle;
@@ -89,12 +90,11 @@
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -107,21 +107,19 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.function.BiConsumer;
+import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 import javax.inject.Inject;
 
 import dagger.Lazy;
 
-
 /**
  * Class to send information from overview to launcher with a binder.
  */
 @SysUISingleton
-public class OverviewProxyService extends CurrentUserTracker implements
-        CallbackController<OverviewProxyListener>, NavigationModeController.ModeChangedListener,
-        Dumpable {
+public class OverviewProxyService implements CallbackController<OverviewProxyListener>,
+        NavigationModeController.ModeChangedListener, Dumpable {
 
     private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE";
 
@@ -133,6 +131,7 @@
     private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
 
     private final Context mContext;
+    private final Executor mMainExecutor;
     private final ShellInterface mShellInterface;
     private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
     private SysUiState mSysUiState;
@@ -145,10 +144,12 @@
     private final Intent mQuickStepIntent;
     private final ScreenshotHelper mScreenshotHelper;
     private final CommandQueue mCommandQueue;
+    private final UserTracker mUserTracker;
     private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
     private final UiEventLogger mUiEventLogger;
 
     private Region mActiveNavBarRegion;
+    private SurfaceControl mNavigationBarSurface;
 
     private IOverviewProxy mOverviewProxy;
     private int mConnectionBackoffAttempts;
@@ -190,7 +191,8 @@
                 // TODO move this logic to message queue
                 mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
                     if (event.getActionMasked() == ACTION_DOWN) {
-                        centralSurfaces.getPanelController().startExpandLatencyTracking();
+                        centralSurfaces.getNotificationPanelViewController()
+                                        .startExpandLatencyTracking();
                     }
                     mHandler.post(() -> {
                         int action = event.getActionMasked();
@@ -217,17 +219,15 @@
         }
 
         @Override
-        public void onBackPressed() throws RemoteException {
+        public void onBackPressed() {
             verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
                 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
                 sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
-
-                notifyBackAction(true, -1, -1, true, false);
             });
         }
 
         @Override
-        public void onImeSwitcherPressed() throws RemoteException {
+        public void onImeSwitcherPressed() {
             // TODO(b/204901476) We're intentionally using DEFAULT_DISPLAY for now since
             // Launcher/Taskbar isn't display aware.
             mContext.getSystemService(InputMethodManager.class)
@@ -316,12 +316,6 @@
         }
 
         @Override
-        public void notifySwipeUpGestureStarted() {
-            verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
-                    notifySwipeUpGestureStartedInternal());
-        }
-
-        @Override
         public void notifyPrioritizedRotation(@Surface.Rotation int rotation) {
             verifyCallerAndClearCallingIdentityPostMain("notifyPrioritizedRotation", () ->
                     notifyPrioritizedRotationInternal(rotation));
@@ -423,7 +417,7 @@
                 return;
             }
 
-            mCurrentBoundedUserId = getCurrentUserId();
+            mCurrentBoundedUserId = mUserTracker.getUserId();
             mOverviewProxy = IOverviewProxy.Stub.asInterface(service);
 
             Bundle params = new Bundle();
@@ -443,6 +437,7 @@
                 Log.e(TAG_OPS, "Failed to call onInitialize()", e);
             }
             dispatchNavButtonBounds();
+            dispatchNavigationBarSurface();
 
             // Force-update the systemui state flags
             updateSystemUiStateFlags();
@@ -474,8 +469,6 @@
     };
 
     private final StatusBarWindowCallback mStatusBarWindowCallback = this::onStatusBarStateChanged;
-    private final BiConsumer<Rect, Rect> mSplitScreenBoundsChangeListener =
-            this::notifySplitScreenBoundsChanged;
 
     // This is the death handler for the binder from the launcher service
     private final IBinder.DeathRecipient mOverviewServiceDeathRcpt
@@ -505,34 +498,44 @@
         }
     };
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    mConnectionBackoffAttempts = 0;
+                    internalConnectToCurrentUser();
+                }
+            };
+
     @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
     @Inject
     public OverviewProxyService(Context context,
+            @Main Executor mainExecutor,
             CommandQueue commandQueue,
             ShellInterface shellInterface,
             Lazy<NavigationBarController> navBarControllerLazy,
             Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
             NavigationModeController navModeController,
             NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
-            BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
             ScreenLifecycle screenLifecycle,
             UiEventLogger uiEventLogger,
             KeyguardUnlockAnimationController sysuiUnlockAnimationController,
             AssistUtils assistUtils,
             DumpManager dumpManager) {
-        super(broadcastDispatcher);
-
         // b/241601880: This component shouldn't be running for a non-primary user
         if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) {
             Log.e(TAG_OPS, "Unexpected initialization for non-primary user", new Throwable());
         }
 
         mContext = context;
+        mMainExecutor = mainExecutor;
         mShellInterface = shellInterface;
         mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
         mHandler = new Handler();
         mNavBarControllerLazy = navBarControllerLazy;
         mStatusBarWinController = statusBarWinController;
+        mUserTracker = userTracker;
         mConnectionBackoffAttempts = 0;
         mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
                 com.android.internal.R.string.config_recentsComponentName));
@@ -573,7 +576,7 @@
         mCommandQueue = commandQueue;
 
         // Listen for user setup
-        startTracking();
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
 
         screenLifecycle.addObserver(mLifecycleObserver);
 
@@ -586,22 +589,23 @@
         assistUtils.registerVoiceInteractionSessionListener(mVoiceInteractionSessionListener);
     }
 
-    @Override
-    public void onUserSwitched(int newUserId) {
-        mConnectionBackoffAttempts = 0;
-        internalConnectToCurrentUser();
-    }
-
     public void onVoiceSessionWindowVisibilityChanged(boolean visible) {
         mSysUiState.setFlag(SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, visible)
                 .commitUpdate(mContext.getDisplayId());
     }
 
-    public void notifyBackAction(boolean completed, int downX, int downY, boolean isButton,
-            boolean gestureSwipeLeft) {
+    /**
+     * Called when the navigation bar surface is created or changed
+     */
+    public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) {
+        mNavigationBarSurface = navbarSurface;
+        dispatchNavigationBarSurface();
+    }
+
+    private void dispatchNavigationBarSurface() {
         try {
             if (mOverviewProxy != null) {
-                mOverviewProxy.onBackAction(completed, downX, downY, isButton, gestureSwipeLeft);
+                mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface);
             }
         } catch (RemoteException e) {
             Log.e(TAG_OPS, "Failed to notify back action", e);
@@ -614,7 +618,7 @@
         final NavigationBarView navBarView =
                 mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
         final NotificationPanelViewController panelController =
-                mCentralSurfacesOptionalLazy.get().get().getPanelController();
+                mCentralSurfacesOptionalLazy.get().get().getNotificationPanelViewController();
         if (SysUiState.DEBUG) {
             Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
                     + " navBarView=" + navBarView + " panelController=" + panelController);
@@ -712,7 +716,7 @@
             mBound = mContext.bindServiceAsUser(launcherServiceIntent,
                     mOverviewServiceConnection,
                     Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
-                    UserHandle.of(getCurrentUserId()));
+                    UserHandle.of(mUserTracker.getUserId()));
         } catch (SecurityException e) {
             Log.e(TAG_OPS, "Unable to bind because of security error", e);
         }
@@ -800,24 +804,12 @@
         }
     }
 
-    public void notifyQuickStepStarted() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onQuickStepStarted();
-        }
-    }
-
     private void notifyPrioritizedRotationInternal(@Surface.Rotation int rotation) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onPrioritizedRotation(rotation);
         }
     }
 
-    public void notifyQuickScrubStarted() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onQuickScrubStarted();
-        }
-    }
-
     private void notifyAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onAssistantProgress(progress);
@@ -836,12 +828,6 @@
         }
     }
 
-    private void notifySwipeUpGestureStartedInternal() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onSwipeUpGestureStarted();
-        }
-    }
-
     public void notifyAssistantVisibilityChanged(float visibility) {
         try {
             if (mOverviewProxy != null) {
@@ -854,26 +840,6 @@
         }
     }
 
-    /**
-     * Notifies the Launcher of split screen size changes
-     *
-     * @param secondaryWindowBounds Bounds of the secondary window including the insets
-     * @param secondaryWindowInsets stable insets received by the secondary window
-     */
-    public void notifySplitScreenBoundsChanged(
-            Rect secondaryWindowBounds, Rect secondaryWindowInsets) {
-        try {
-            if (mOverviewProxy != null) {
-                mOverviewProxy.onSplitScreenSecondaryBoundsChanged(
-                        secondaryWindowBounds, secondaryWindowInsets);
-            } else {
-                Log.e(TAG_OPS, "Failed to get overview proxy for split screen bounds.");
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG_OPS, "Failed to call onSplitScreenSecondaryBoundsChanged()", e);
-        }
-    }
-
     private final ScreenLifecycle.Observer mLifecycleObserver = new ScreenLifecycle.Observer() {
         /**
          * Notifies the Launcher that screen turned on and ready to use
@@ -979,7 +945,7 @@
     }
 
     private void updateEnabledState() {
-        final int currentUser = ActivityManagerWrapper.getInstance().getCurrentUserId();
+        final int currentUser = mUserTracker.getUserId();
         mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent,
                 MATCH_SYSTEM_ONLY, currentUser) != null;
     }
@@ -1005,23 +971,20 @@
         pw.print("  mWindowCornerRadius="); pw.println(mWindowCornerRadius);
         pw.print("  mSupportsRoundedCornersOnWindows="); pw.println(mSupportsRoundedCornersOnWindows);
         pw.print("  mActiveNavBarRegion="); pw.println(mActiveNavBarRegion);
+        pw.print("  mNavigationBarSurface="); pw.println(mNavigationBarSurface);
         pw.print("  mNavBarMode="); pw.println(mNavBarMode);
         mSysUiState.dump(pw, args);
     }
 
     public interface OverviewProxyListener {
         default void onConnectionChanged(boolean isConnected) {}
-        default void onQuickStepStarted() {}
-        default void onSwipeUpGestureStarted() {}
         default void onPrioritizedRotation(@Surface.Rotation int rotation) {}
         default void onOverviewShown(boolean fromHome) {}
-        default void onQuickScrubStarted() {}
         /** Notify the recents app (overview) is started by 3-button navigation. */
         default void onToggleRecentApps() {}
         default void onHomeRotationEnabled(boolean enabled) {}
         default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {}
         default void onTaskbarAutohideSuspend(boolean suspend) {}
-        default void onSystemUiStateChanged(int sysuiStateFlags) {}
         default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {}
         default void onAssistantGestureCompletion(float velocity) {}
         default void startAssistant(Bundle bundle) {}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
deleted file mode 100644
index d2f3a6a..0000000
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ /dev/null
@@ -1,228 +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.
- */
-package com.android.systemui.ripple
-
-import android.graphics.PointF
-import android.graphics.RuntimeShader
-import android.util.MathUtils
-
-/**
- * Shader class that renders an expanding ripple effect. The ripple contains three elements:
- *
- * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away
- * 2. an expanding ring that appears throughout the effect
- * 3. an expanding ring-shaped area that reveals noise over #2.
- *
- * The ripple shader will be default to the circle shape if not specified.
- *
- * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
- */
-class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
-        RuntimeShader(buildShader(rippleShape)) {
-
-    /** Shapes that the [RippleShader] supports. */
-    enum class RippleShape {
-        CIRCLE,
-        ROUNDED_BOX,
-        ELLIPSE
-    }
-    //language=AGSL
-    companion object {
-        private const val SHADER_UNIFORMS = """uniform vec2 in_center;
-                uniform vec2 in_size;
-                uniform float in_progress;
-                uniform float in_cornerRadius;
-                uniform float in_thickness;
-                uniform float in_time;
-                uniform float in_distort_radial;
-                uniform float in_distort_xy;
-                uniform float in_fadeSparkle;
-                uniform float in_fadeFill;
-                uniform float in_fadeRing;
-                uniform float in_blur;
-                uniform float in_pixelDensity;
-                layout(color) uniform vec4 in_color;
-                uniform float in_sparkle_strength;"""
-
-        private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) {
-                vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
-                float radius = in_size.x * 0.5;
-                float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur);
-                float inside = soften(sdCircle(p_distorted-in_center, radius * 1.2), in_blur);
-                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
-                    * (1.-sparkleRing) * in_fadeSparkle;
-
-                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
-                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
-                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
-                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
-            }
-        """
-
-        private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) {
-                float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
-                    in_thickness), in_blur);
-                float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius),
-                    in_blur);
-                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
-                    * (1.-sparkleRing) * in_fadeSparkle;
-
-                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
-                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
-                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
-                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
-            }
-        """
-
-        private const val SHADER_ELLIPSE_MAIN = """vec4 main(vec2 p) {
-                vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
-
-                float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur);
-                float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur);
-                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
-                    * (1.-sparkleRing) * in_fadeSparkle;
-
-                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
-                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
-                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
-                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
-            }
-        """
-
-        private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
-                SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF +
-                SHADER_CIRCLE_MAIN
-        private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS +
-                RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
-                SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN
-        private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
-                SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF +
-                SHADER_ELLIPSE_MAIN
-
-        private fun buildShader(rippleShape: RippleShape): String =
-                when (rippleShape) {
-                    RippleShape.CIRCLE -> CIRCLE_SHADER
-                    RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER
-                    RippleShape.ELLIPSE -> ELLIPSE_SHADER
-                }
-
-        private fun subProgress(start: Float, end: Float, progress: Float): Float {
-            val min = Math.min(start, end)
-            val max = Math.max(start, end)
-            val sub = Math.min(Math.max(progress, min), max)
-            return (sub - start) / (end - start)
-        }
-    }
-
-    /**
-     * Sets the center position of the ripple.
-     */
-    fun setCenter(x: Float, y: Float) {
-        setFloatUniform("in_center", x, y)
-    }
-
-    /** Max width of the ripple. */
-    private var maxSize: PointF = PointF()
-    fun setMaxSize(width: Float, height: Float) {
-        maxSize.x = width
-        maxSize.y = height
-    }
-
-    /**
-     * Progress of the ripple. Float value between [0, 1].
-     */
-    var progress: Float = 0.0f
-        set(value) {
-            field = value
-            setFloatUniform("in_progress", value)
-            val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
-
-            setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg,
-                    /* height= */ maxSize.y * curvedProg)
-            setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
-            // radius should not exceed width and height values.
-            setFloatUniform("in_cornerRadius",
-                    Math.min(maxSize.x, maxSize.y) * curvedProg)
-
-            setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
-
-            val fadeIn = subProgress(0f, 0.1f, value)
-            val fadeOutNoise = subProgress(0.4f, 1f, value)
-            var fadeOutRipple = 0f
-            var fadeFill = 0f
-            if (!rippleFill) {
-                fadeFill = subProgress(0f, 0.6f, value)
-                fadeOutRipple = subProgress(0.3f, 1f, value)
-            }
-            setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
-            setFloatUniform("in_fadeFill", 1 - fadeFill)
-            setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
-        }
-
-    /**
-     * Play time since the start of the effect.
-     */
-    var time: Float = 0.0f
-        set(value) {
-            field = value
-            setFloatUniform("in_time", value)
-        }
-
-    /**
-     * A hex value representing the ripple color, in the format of ARGB
-     */
-    var color: Int = 0xffffff
-        set(value) {
-            field = value
-            setColorUniform("in_color", value)
-        }
-
-    /**
-     * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus
-     * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1
-     * it's opaque white and looks the most grainy.
-     */
-    var sparkleStrength: Float = 0.0f
-        set(value) {
-            field = value
-            setFloatUniform("in_sparkle_strength", value)
-        }
-
-    /**
-     * Distortion strength of the ripple. Expected value between[0, 1].
-     */
-    var distortionStrength: Float = 0.0f
-        set(value) {
-            field = value
-            setFloatUniform("in_distort_radial", 75 * progress * value)
-            setFloatUniform("in_distort_xy", 75 * value)
-        }
-
-    var pixelDensity: Float = 1.0f
-        set(value) {
-            field = value
-            setFloatUniform("in_pixelDensity", value)
-        }
-
-    /**
-     * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
-     * False for a ring effect.
-     */
-    var rippleFill: Boolean = false
-}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
deleted file mode 100644
index 6de4648..0000000
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
+++ /dev/null
@@ -1,53 +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.ripple
-
-/** A common utility functions that are used for computing [RippleShader]. */
-class RippleShaderUtilLibrary {
-    //language=AGSL
-    companion object {
-        const val SHADER_LIB = """
-            float triangleNoise(vec2 n) {
-                    n  = fract(n * vec2(5.3987, 5.4421));
-                    n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
-                    float xy = n.x * n.y;
-                    return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
-                }
-                const float PI = 3.1415926535897932384626;
-
-                float sparkles(vec2 uv, float t) {
-                    float n = triangleNoise(uv);
-                    float s = 0.0;
-                    for (float i = 0; i < 4; i += 1) {
-                        float l = i * 0.01;
-                        float h = l + 0.1;
-                        float o = smoothstep(n - l, h, n);
-                        o *= abs(sin(PI * o * (t + 0.55 * i)));
-                        s += o;
-                    }
-                    return s;
-                }
-
-                vec2 distort(vec2 p, float time, float distort_amount_radial,
-                    float distort_amount_xy) {
-                        float angle = atan(p.y, p.x);
-                          return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
-                                    cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
-                             + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
-                                    cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
-            }"""
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
deleted file mode 100644
index 1e51ffa..0000000
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ /dev/null
@@ -1,166 +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.
- */
-
-package com.android.systemui.ripple
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Canvas
-import android.graphics.Paint
-import android.util.AttributeSet
-import android.view.View
-import androidx.core.graphics.ColorUtils
-import com.android.systemui.ripple.RippleShader.RippleShape
-
-private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
-private const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
-const val RIPPLE_DEFAULT_ALPHA: Int = 45
-
-/**
- * A generic expanding ripple effect.
- *
- * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
- * then call [startRipple] to trigger the ripple expansion.
- */
-open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
-
-    private lateinit var rippleShader: RippleShader
-    lateinit var rippleShape: RippleShape
-        private set
-
-    private val ripplePaint = Paint()
-
-    var rippleInProgress: Boolean = false
-    var duration: Long = 1750
-
-    private var maxWidth: Float = 0.0f
-    private var maxHeight: Float = 0.0f
-    fun setMaxSize(maxWidth: Float, maxHeight: Float) {
-        this.maxWidth = maxWidth
-        this.maxHeight = maxHeight
-        rippleShader.setMaxSize(maxWidth, maxHeight)
-    }
-
-    private var centerX: Float = 0.0f
-    private var centerY: Float = 0.0f
-    fun setCenter(x: Float, y: Float) {
-        this.centerX = x
-        this.centerY = y
-        rippleShader.setCenter(x, y)
-    }
-
-    override fun onConfigurationChanged(newConfig: Configuration?) {
-        rippleShader.pixelDensity = resources.displayMetrics.density
-        super.onConfigurationChanged(newConfig)
-    }
-
-    override fun onAttachedToWindow() {
-        rippleShader.pixelDensity = resources.displayMetrics.density
-        super.onAttachedToWindow()
-    }
-
-    /** Initializes the shader. Must be called before [startRipple]. */
-    fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
-        this.rippleShape = rippleShape
-        rippleShader = RippleShader(rippleShape)
-
-        rippleShader.color = RIPPLE_DEFAULT_COLOR
-        rippleShader.progress = 0f
-        rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
-        rippleShader.pixelDensity = resources.displayMetrics.density
-
-        ripplePaint.shader = rippleShader
-    }
-
-    @JvmOverloads
-    fun startRipple(onAnimationEnd: Runnable? = null) {
-        if (rippleInProgress) {
-            return // Ignore if ripple effect is already playing
-        }
-        val animator = ValueAnimator.ofFloat(0f, 1f)
-        animator.duration = duration
-        animator.addUpdateListener { updateListener ->
-            val now = updateListener.currentPlayTime
-            val progress = updateListener.animatedValue as Float
-            rippleShader.progress = progress
-            rippleShader.distortionStrength = 1 - progress
-            rippleShader.time = now.toFloat()
-            invalidate()
-        }
-        animator.addListener(object : AnimatorListenerAdapter() {
-            override fun onAnimationEnd(animation: Animator?) {
-                rippleInProgress = false
-                onAnimationEnd?.run()
-            }
-        })
-        animator.start()
-        rippleInProgress = true
-    }
-
-    /** Set the color to be used for the ripple.
-     *
-     * The alpha value of the color will be applied to the ripple. The alpha range is [0-100].
-     */
-    fun setColor(color: Int, alpha: Int = RIPPLE_DEFAULT_ALPHA) {
-        rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
-    }
-
-    /**
-     * Set whether the ripple should remain filled as the ripple expands.
-     *
-     * See [RippleShader.rippleFill].
-     */
-    fun setRippleFill(rippleFill: Boolean) {
-        rippleShader.rippleFill = rippleFill
-    }
-
-    /**
-     * Set the intensity of the sparkles.
-     */
-    fun setSparkleStrength(strength: Float) {
-        rippleShader.sparkleStrength = strength
-    }
-
-    override fun onDraw(canvas: Canvas?) {
-        if (canvas == null || !canvas.isHardwareAccelerated) {
-            // Drawing with the ripple shader requires hardware acceleration, so skip
-            // if it's unsupported.
-            return
-        }
-        // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
-        // active effect area. Values here should be kept in sync with the animation implementation
-        // in the ripple shader.
-        if (rippleShape == RippleShape.CIRCLE) {
-            val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
-                    (1 - rippleShader.progress)) * maxWidth
-            canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
-        } else {
-            val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
-                    (1 - rippleShader.progress)) * maxWidth * 2
-            val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
-                    (1 - rippleShader.progress)) * maxHeight * 2
-            canvas.drawRect(
-                    /* left= */ centerX - maskWidth,
-                    /* top= */ centerY - maskHeight,
-                    /* right= */ centerX + maskWidth,
-                    /* bottom= */ centerY + maskHeight,
-                    ripplePaint)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
deleted file mode 100644
index 5e256c6..0000000
--- a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
+++ /dev/null
@@ -1,112 +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.ripple
-
-/** Library class that contains 2D signed distance functions. */
-class SdfShaderLibrary {
-    //language=AGSL
-    companion object {
-        const val CIRCLE_SDF = """
-            float sdCircle(vec2 p, float r) {
-                return (length(p)-r) / r;
-            }
-
-            float circleRing(vec2 p, float radius) {
-                float thicknessHalf = radius * 0.25;
-
-                float outerCircle = sdCircle(p, radius + thicknessHalf);
-                float innerCircle = sdCircle(p, radius);
-
-                return subtract(outerCircle, innerCircle);
-            }
-        """
-
-        const val ROUNDED_BOX_SDF = """
-            float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
-                size *= 0.5;
-                cornerRadius *= 0.5;
-                vec2 d = abs(p)-size+cornerRadius;
-
-                float outside = length(max(d, 0.0));
-                float inside = min(max(d.x, d.y), 0.0);
-
-                return (outside+inside-cornerRadius)/size.y;
-            }
-
-            float roundedBoxRing(vec2 p, vec2 size, float cornerRadius,
-                float borderThickness) {
-                float outerRoundBox = sdRoundedBox(p, size, cornerRadius);
-                float innerRoundBox = sdRoundedBox(p, size - vec2(borderThickness),
-                    cornerRadius - borderThickness);
-                return subtract(outerRoundBox, innerRoundBox);
-            }
-        """
-
-        // Used non-trigonometry parametrization and Halley's method (iterative) for root finding.
-        // This is more expensive than the regular circle SDF, recommend to use the circle SDF if
-        // possible.
-        const val ELLIPSE_SDF = """float sdEllipse(vec2 p, vec2 wh) {
-            wh *= 0.5;
-
-            // symmetry
-            (wh.x > wh.y) ? wh = wh.yx, p = abs(p.yx) : p = abs(p);
-
-            vec2 u = wh*p, v = wh*wh;
-
-            float U1 = u.y/2.0;  float U5 = 4.0*U1;
-            float U2 = v.y-v.x;  float U6 = 6.0*U1;
-            float U3 = u.x-U2;   float U7 = 3.0*U3;
-            float U4 = u.x+U2;
-
-            float t = 0.5;
-            for (int i = 0; i < 3; i ++) {
-                float F1 = t*(t*t*(U1*t+U3)+U4)-U1;
-                float F2 = t*t*(U5*t+U7)+U4;
-                float F3 = t*(U6*t+U7);
-
-                t += (F1*F2)/(F1*F3-F2*F2);
-            }
-
-            t = clamp(t, 0.0, 1.0);
-
-            float d = distance(p, wh*vec2(1.0-t*t,2.0*t)/(t*t+1.0));
-            d /= wh.y;
-
-            return (dot(p/wh,p/wh)>1.0) ? d : -d;
-        }
-
-        float ellipseRing(vec2 p, vec2 wh) {
-            vec2 thicknessHalf = wh * 0.25;
-
-            float outerEllipse = sdEllipse(p, wh + thicknessHalf);
-            float innerEllipse = sdEllipse(p, wh);
-
-            return subtract(outerEllipse, innerEllipse);
-        }
-        """
-
-        const val SHADER_SDF_OPERATION_LIB = """
-            float soften(float d, float blur) {
-                float blurHalf = blur * 0.5;
-                return smoothstep(-blurHalf, blurHalf, d);
-            }
-
-            float subtract(float outer, float inner) {
-                return max(outer, -inner);
-            }
-        """
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
new file mode 100644
index 0000000..f4d59a8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.screenrecord
+
+import android.content.Context
+import android.os.Bundle
+import android.view.Gravity
+import android.view.View
+import android.view.ViewStub
+import android.view.WindowManager
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Base permission dialog for screen share and recording */
+open class BaseScreenSharePermissionDialog(
+    context: Context?,
+    private val screenShareOptions: List<ScreenShareOption>,
+    private val appName: String?
+) : SystemUIDialog(context), AdapterView.OnItemSelectedListener {
+    private lateinit var dialogTitle: TextView
+    private lateinit var startButton: TextView
+    private lateinit var warning: TextView
+    private lateinit var screenShareModeSpinner: Spinner
+    var selectedScreenShareOption: ScreenShareOption = screenShareOptions.first()
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        window.apply {
+            addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
+            setGravity(Gravity.CENTER)
+        }
+        setContentView(R.layout.screen_share_dialog)
+        dialogTitle = findViewById(R.id.screen_share_dialog_title)
+        warning = findViewById(R.id.text_warning)
+        startButton = findViewById(R.id.button_start)
+        findViewById<TextView>(R.id.button_cancel).setOnClickListener { dismiss() }
+        initScreenShareOptions()
+        createOptionsView(getOptionsViewLayoutId())
+    }
+
+    protected fun initScreenShareOptions() {
+        selectedScreenShareOption = screenShareOptions.first()
+        warning.text = warningText
+        initScreenShareSpinner()
+    }
+
+    private val warningText: String
+        get() = context.getString(selectedScreenShareOption.warningText, appName)
+
+    private fun initScreenShareSpinner() {
+        val options = screenShareOptions.map { context.getString(it.spinnerText) }.toTypedArray()
+        val adapter =
+            ArrayAdapter(context.applicationContext, android.R.layout.simple_spinner_item, options)
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        screenShareModeSpinner = findViewById(R.id.screen_share_mode_spinner)
+        screenShareModeSpinner.adapter = adapter
+        screenShareModeSpinner.onItemSelectedListener = this
+    }
+
+    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
+        selectedScreenShareOption = screenShareOptions[pos]
+        warning.text = warningText
+    }
+
+    override fun onNothingSelected(parent: AdapterView<*>?) {}
+
+    /** Protected methods for the text updates & functionality */
+    protected fun setDialogTitle(@StringRes stringId: Int) {
+        val title = context.getString(stringId, appName)
+        dialogTitle.text = title
+    }
+
+    protected fun setStartButtonText(@StringRes stringId: Int) {
+        startButton.setText(stringId)
+    }
+
+    protected fun setStartButtonOnClickListener(listener: View.OnClickListener?) {
+        startButton.setOnClickListener(listener)
+    }
+
+    // Create additional options that is shown under the share mode spinner
+    // Eg. the audio and tap toggles in SysUI Recorder
+    @LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null
+
+    private fun createOptionsView(@LayoutRes layoutId: Int?) {
+        if (layoutId == null) return
+        val stub = findViewById<View>(R.id.options_stub) as ViewStub
+        stub.layoutResource = layoutId
+        stub.inflate()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
new file mode 100644
index 0000000..15b0bc4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.screenrecord
+
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.R
+
+/** Dialog to select screen recording options */
+class MediaProjectionPermissionDialog(
+    context: Context?,
+    private val onStartRecordingClicked: Runnable,
+    appName: String?
+) : BaseScreenSharePermissionDialog(context, createOptionList(), appName) {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setDialogTitle(R.string.media_projection_permission_dialog_title)
+        setStartButtonText(R.string.media_projection_permission_dialog_continue)
+        setStartButtonOnClickListener {
+            // Note that it is important to run this callback before dismissing, so that the
+            // callback can disable the dialog exit animation if it wants to.
+            onStartRecordingClicked.run()
+            dismiss()
+        }
+    }
+
+    companion object {
+        private fun createOptionList(): List<ScreenShareOption> {
+            return listOf(
+                ScreenShareOption(
+                    SINGLE_APP,
+                    R.string.media_projection_permission_dialog_option_single_app,
+                    R.string.media_projection_permission_dialog_warning_single_app
+                ),
+                ScreenShareOption(
+                    ENTIRE_SCREEN,
+                    R.string.media_projection_permission_dialog_option_entire_screen,
+                    R.string.media_projection_permission_dialog_warning_entire_screen
+                )
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index 1083f22..ce4e0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord;
 
+import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -33,6 +34,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.policy.CallbackController;
@@ -97,11 +99,15 @@
     }
 
     /** Create a dialog to show screen recording options to the user. */
-    public ScreenRecordDialog createScreenRecordDialog(Context context, FeatureFlags flags,
-            DialogLaunchAnimator dialogLaunchAnimator, ActivityStarter activityStarter,
-            @Nullable Runnable onStartRecordingClicked) {
-        return new ScreenRecordDialog(context, this, activityStarter, mUserContextProvider,
-                flags, dialogLaunchAnimator, onStartRecordingClicked);
+    public Dialog createScreenRecordDialog(Context context, FeatureFlags flags,
+                                           DialogLaunchAnimator dialogLaunchAnimator,
+                                           ActivityStarter activityStarter,
+                                           @Nullable Runnable onStartRecordingClicked) {
+        return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
+                ? new ScreenRecordPermissionDialog(context, this, activityStarter,
+                        dialogLaunchAnimator, mUserContextProvider, onStartRecordingClicked)
+                : new ScreenRecordDialog(context, this, activityStarter,
+                mUserContextProvider, flags, dialogLaunchAnimator, onStartRecordingClicked);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
new file mode 100644
index 0000000..19bb15a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.screenrecord
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.ResultReceiver
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import android.widget.Switch
+import androidx.annotation.LayoutRes
+import com.android.systemui.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.media.MediaProjectionAppSelectorActivity
+import com.android.systemui.media.MediaProjectionCaptureTarget
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserContextProvider
+
+/** Dialog to select screen recording options */
+class ScreenRecordPermissionDialog(
+    context: Context?,
+    private val controller: RecordingController,
+    private val activityStarter: ActivityStarter,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val userContextProvider: UserContextProvider,
+    private val onStartRecordingClicked: Runnable?
+) : BaseScreenSharePermissionDialog(context, createOptionList(), null) {
+    private lateinit var tapsSwitch: Switch
+    private lateinit var tapsView: View
+    private lateinit var audioSwitch: Switch
+    private lateinit var options: Spinner
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setDialogTitle(R.string.screenrecord_start_label)
+        setStartButtonText(R.string.screenrecord_start_recording)
+        setStartButtonOnClickListener { v: View? ->
+            onStartRecordingClicked?.run()
+            if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
+                requestScreenCapture(/* captureTarget= */ null)
+            }
+            if (selectedScreenShareOption.mode == SINGLE_APP) {
+                val intent = Intent(context, MediaProjectionAppSelectorActivity::class.java)
+                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+                // We can't start activity for result here so we use result receiver to get
+                // the selected target to capture
+                intent.putExtra(
+                    MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
+                    CaptureTargetResultReceiver()
+                )
+                val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
+                if (animationController == null) {
+                    dismiss()
+                }
+                activityStarter.startActivity(intent, /* dismissShade= */ true, animationController)
+            }
+            dismiss()
+        }
+        initRecordOptionsView()
+    }
+
+    @LayoutRes override fun getOptionsViewLayoutId(): Int = R.layout.screen_record_options
+
+    private fun initRecordOptionsView() {
+        audioSwitch = findViewById(R.id.screenrecord_audio_switch)
+        tapsSwitch = findViewById(R.id.screenrecord_taps_switch)
+        tapsView = findViewById(R.id.show_taps)
+        updateTapsViewVisibility()
+        options = findViewById(R.id.screen_recording_options)
+        val a: ArrayAdapter<*> =
+            ScreenRecordingAdapter(context, android.R.layout.simple_spinner_dropdown_item, MODES)
+        a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        options.adapter = a
+        options.setOnItemClickListenerInt { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
+            audioSwitch.isChecked = true
+        }
+    }
+
+    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
+        super.onItemSelected(adapterView, view, pos, id)
+        updateTapsViewVisibility()
+    }
+
+    private fun updateTapsViewVisibility() {
+        tapsView.visibility = if (selectedScreenShareOption.mode == SINGLE_APP) GONE else VISIBLE
+    }
+
+    /**
+     * Starts screen capture after some countdown
+     * @param captureTarget target to capture (could be e.g. a task) or null to record the whole
+     * screen
+     */
+    private fun requestScreenCapture(captureTarget: MediaProjectionCaptureTarget?) {
+        val userContext = userContextProvider.userContext
+        val showTaps = selectedScreenShareOption.mode != SINGLE_APP && tapsSwitch.isChecked
+        val audioMode =
+            if (audioSwitch.isChecked) options.selectedItem as ScreenRecordingAudioSource
+            else ScreenRecordingAudioSource.NONE
+        val startIntent =
+            PendingIntent.getForegroundService(
+                userContext,
+                RecordingService.REQUEST_CODE,
+                RecordingService.getStartIntent(
+                    userContext,
+                    Activity.RESULT_OK,
+                    audioMode.ordinal,
+                    showTaps,
+                    captureTarget
+                ),
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+        val stopIntent =
+            PendingIntent.getService(
+                userContext,
+                RecordingService.REQUEST_CODE,
+                RecordingService.getStopIntent(userContext),
+                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+        controller.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent)
+    }
+
+    private inner class CaptureTargetResultReceiver() :
+        ResultReceiver(Handler(Looper.getMainLooper())) {
+        override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
+            if (resultCode == Activity.RESULT_OK) {
+                val captureTarget =
+                    resultData.getParcelable(
+                        MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET,
+                        MediaProjectionCaptureTarget::class.java
+                    )
+
+                // Start recording of the selected target
+                requestScreenCapture(captureTarget)
+            }
+        }
+    }
+
+    companion object {
+        private val MODES =
+            listOf(
+                ScreenRecordingAudioSource.INTERNAL,
+                ScreenRecordingAudioSource.MIC,
+                ScreenRecordingAudioSource.MIC_AND_INTERNAL
+            )
+        private const val DELAY_MS: Long = 3000
+        private const val INTERVAL_MS: Long = 1000
+        private fun createOptionList(): List<ScreenShareOption> {
+            return listOf(
+                ScreenShareOption(
+                    SINGLE_APP,
+                    R.string.screenrecord_option_single_app,
+                    R.string.screenrecord_warning_single_app
+                ),
+                ScreenShareOption(
+                    ENTIRE_SCREEN,
+                    R.string.screenrecord_option_entire_screen,
+                    R.string.screenrecord_warning_entire_screen
+                )
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt
new file mode 100644
index 0000000..914d29a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenShareOption.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.screenrecord
+
+import androidx.annotation.IntDef
+import androidx.annotation.StringRes
+import kotlin.annotation.Retention
+
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(SINGLE_APP, ENTIRE_SCREEN)
+annotation class ScreenShareMode
+
+const val SINGLE_APP = 0
+const val ENTIRE_SCREEN = 1
+
+class ScreenShareOption(
+    @ScreenShareMode val mode: Int,
+    @StringRes val spinnerText: Int,
+    @StringRes val warningText: Int
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 5961635..01e32b7a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -32,7 +32,7 @@
 import com.android.internal.infra.ServiceConnector
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineDispatcher
@@ -45,7 +45,7 @@
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
-    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val context: Context,
 ) {
     /**
@@ -70,23 +70,21 @@
         userId: Int,
         overrideTransition: Boolean,
     ) {
-        withContext(bgDispatcher) {
-            dismissKeyguard()
+        dismissKeyguard()
 
-            if (userId == UserHandle.myUserId()) {
-                context.startActivity(intent, bundle)
-            } else {
-                launchCrossProfileIntent(userId, intent, bundle)
-            }
+        if (userId == UserHandle.myUserId()) {
+            withContext(mainDispatcher) { context.startActivity(intent, bundle) }
+        } else {
+            launchCrossProfileIntent(userId, intent, bundle)
+        }
 
-            if (overrideTransition) {
-                val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
-                try {
-                    WindowManagerGlobal.getWindowManagerService()
-                        .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
-                } catch (e: Exception) {
-                    Log.e(TAG, "Error overriding screenshot app transition", e)
-                }
+        if (overrideTransition) {
+            val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
+            try {
+                WindowManagerGlobal.getWindowManagerService()
+                    .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+            } catch (e: Exception) {
+                Log.e(TAG, "Error overriding screenshot app transition", e)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
index e3658de..c8c1337 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
@@ -38,6 +38,8 @@
 import androidx.exifinterface.media.ExifInterface;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -85,10 +87,12 @@
     private final ContentResolver mResolver;
     private CompressFormat mCompressFormat = CompressFormat.PNG;
     private int mQuality = 100;
+    private final FeatureFlags mFlags;
 
     @Inject
-    ImageExporter(ContentResolver resolver) {
+    ImageExporter(ContentResolver resolver, FeatureFlags flags) {
         mResolver = resolver;
+        mFlags = flags;
     }
 
     /**
@@ -161,7 +165,7 @@
             ZonedDateTime captureTime, UserHandle owner) {
 
         final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
-                mQuality, /* publish */ true, owner);
+                mQuality, /* publish */ true, owner, mFlags);
 
         return CallbackToFutureAdapter.getFuture(
                 (completer) -> {
@@ -209,9 +213,11 @@
         private final UserHandle mOwner;
         private final String mFileName;
         private final boolean mPublish;
+        private final FeatureFlags mFlags;
 
         Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
-                CompressFormat format, int quality, boolean publish, UserHandle owner) {
+                CompressFormat format, int quality, boolean publish, UserHandle owner,
+                FeatureFlags flags) {
             mResolver = resolver;
             mRequestId = requestId;
             mBitmap = bitmap;
@@ -221,6 +227,7 @@
             mOwner = owner;
             mFileName = createFilename(mCaptureTime, mFormat);
             mPublish = publish;
+            mFlags = flags;
         }
 
         public Result execute() throws ImageExportException, InterruptedException {
@@ -234,7 +241,7 @@
                     start = Instant.now();
                 }
 
-                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
+                uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner, mFlags);
                 throwIfInterrupted();
 
                 writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -278,13 +285,15 @@
     }
 
     private static Uri createEntry(ContentResolver resolver, CompressFormat format,
-            ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
+            ZonedDateTime time, String fileName, UserHandle owner, FeatureFlags flags)
+            throws ImageExportException {
         Trace.beginSection("ImageExporter_createEntry");
         try {
             final ContentValues values = createMetadata(time, format, fileName);
 
             Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
-            if (UserHandle.myUserId() != owner.getIdentifier()) {
+            if (flags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+                    && UserHandle.myUserId() != owner.getIdentifier()) {
                 baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
             }
             Uri uri = resolver.insert(baseUri, values);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 8bf956b..5450db9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -46,6 +46,8 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -67,6 +69,7 @@
     private static final String TAG = LogConfig.logTag(LongScreenshotActivity.class);
 
     public static final String EXTRA_CAPTURE_RESPONSE = "capture-response";
+    public static final String EXTRA_SCREENSHOT_USER_HANDLE = "screenshot-userhandle";
     private static final String KEY_SAVED_IMAGE_PATH = "saved-image-path";
 
     private final UiEventLogger mUiEventLogger;
@@ -74,6 +77,8 @@
     private final Executor mBackgroundExecutor;
     private final ImageExporter mImageExporter;
     private final LongScreenshotData mLongScreenshotHolder;
+    private final ActionIntentExecutor mActionExecutor;
+    private final FeatureFlags mFeatureFlags;
 
     private ImageView mPreview;
     private ImageView mTransitionView;
@@ -85,6 +90,7 @@
     private CropView mCropView;
     private MagnifierView mMagnifierView;
     private ScrollCaptureResponse mScrollCaptureResponse;
+    private UserHandle mScreenshotUserHandle;
     private File mSavedImagePath;
 
     private ListenableFuture<File> mCacheSaveFuture;
@@ -103,12 +109,15 @@
     @Inject
     public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter,
             @Main Executor mainExecutor, @Background Executor bgExecutor,
-            LongScreenshotData longScreenshotHolder) {
+            LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor,
+            FeatureFlags featureFlags) {
         mUiEventLogger = uiEventLogger;
         mUiExecutor = mainExecutor;
         mBackgroundExecutor = bgExecutor;
         mImageExporter = imageExporter;
         mLongScreenshotHolder = longScreenshotHolder;
+        mActionExecutor = actionExecutor;
+        mFeatureFlags = featureFlags;
     }
 
 
@@ -139,6 +148,11 @@
 
         Intent intent = getIntent();
         mScrollCaptureResponse = intent.getParcelableExtra(EXTRA_CAPTURE_RESPONSE);
+        mScreenshotUserHandle = intent.getParcelableExtra(EXTRA_SCREENSHOT_USER_HANDLE,
+                UserHandle.class);
+        if (mScreenshotUserHandle == null) {
+            mScreenshotUserHandle = Process.myUserHandle();
+        }
 
         if (savedInstanceState != null) {
             String savedImagePath = savedInstanceState.getString(KEY_SAVED_IMAGE_PATH);
@@ -318,36 +332,51 @@
     }
 
     private void doEdit(Uri uri) {
-        String editorPackage = getString(R.string.config_screenshotEditor);
-        Intent intent = new Intent(Intent.ACTION_EDIT);
-        if (!TextUtils.isEmpty(editorPackage)) {
-            intent.setComponent(ComponentName.unflattenFromString(editorPackage));
-        }
-        intent.setDataAndType(uri, "image/png");
-        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
-                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) && mScreenshotUserHandle
+                != Process.myUserHandle()) {
+            // TODO: Fix transition for work profile. Omitting it in the meantime.
+            mActionExecutor.launchIntentAsync(
+                    ActionIntentCreator.INSTANCE.createEditIntent(uri, this),
+                    null,
+                    mScreenshotUserHandle.getIdentifier(), false);
+        } else {
+            String editorPackage = getString(R.string.config_screenshotEditor);
+            Intent intent = new Intent(Intent.ACTION_EDIT);
+            if (!TextUtils.isEmpty(editorPackage)) {
+                intent.setComponent(ComponentName.unflattenFromString(editorPackage));
+            }
+            intent.setDataAndType(uri, "image/png");
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
 
-        mTransitionView.setImageBitmap(mOutputBitmap);
-        mTransitionView.setVisibility(View.VISIBLE);
-        mTransitionView.setTransitionName(
-                ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
-        // TODO: listen for transition completing instead of finishing onStop
-        mTransitionStarted = true;
-        startActivity(intent,
-                ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView,
-                        ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle());
+            mTransitionView.setImageBitmap(mOutputBitmap);
+            mTransitionView.setVisibility(View.VISIBLE);
+            mTransitionView.setTransitionName(
+                    ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+            // TODO: listen for transition completing instead of finishing onStop
+            mTransitionStarted = true;
+            startActivity(intent,
+                    ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView,
+                            ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle());
+        }
     }
 
     private void doShare(Uri uri) {
-        Intent intent = new Intent(Intent.ACTION_SEND);
-        intent.setType("image/png");
-        intent.putExtra(Intent.EXTRA_STREAM, uri);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
-                | Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        Intent sharingChooserIntent = Intent.createChooser(intent, null)
-                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+            Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri, null);
+            mActionExecutor.launchIntentAsync(shareIntent, null,
+                    mScreenshotUserHandle.getIdentifier(), false);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_SEND);
+            intent.setType("image/png");
+            intent.putExtra(Intent.EXTRA_STREAM, uri);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
+                    | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            Intent sharingChooserIntent = Intent.createChooser(intent, null)
+                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
-        startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+            startActivityAsUser(sharingChooserIntent, UserHandle.CURRENT);
+        }
     }
 
     private void onClicked(View v) {
@@ -389,8 +418,8 @@
         mOutputBitmap = renderBitmap(drawable, bounds);
         ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
                 mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
-                // TODO: Owner must match the owner of the captured window.
-                Process.myUserHandle());
+                mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+                        ? mScreenshotUserHandle : Process.myUserHandle());
         exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 7143ba2..b4934cf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -38,6 +38,7 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -81,7 +82,6 @@
 
     private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
     private String mScreenshotId;
-    private final boolean mSmartActionsEnabled;
     private final Random mRandom = new Random();
     private final Supplier<ActionTransition> mSharedElementTransition;
     private final ImageExporter mImageExporter;
@@ -109,8 +109,6 @@
         mParams = data;
 
         // Initialize screenshot notification smart actions provider.
-        mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true);
         mSmartActionsProvider = screenshotNotificationSmartActionsProvider;
     }
 
@@ -131,8 +129,16 @@
 
         Bitmap image = mParams.image;
         mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId);
+
+        boolean savingToOtherUser = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+                && (user != Process.myUserHandle());
+        // Smart actions don't yet work for cross-user saves.
+        boolean smartActionsEnabled = !savingToOtherUser
+                && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
+                true);
         try {
-            if (mSmartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) {
+            if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) {
                 // Since Quick Share target recommendation does not rely on image URL, it is
                 // queried and surfaced before image compress/export. Action intent would not be
                 // used, because it does not contain image URL.
@@ -150,10 +156,9 @@
             CompletableFuture<List<Notification.Action>> smartActionsFuture =
                     mScreenshotSmartActions.getSmartActionsFuture(
                             mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS,
-                            mSmartActionsEnabled, user);
-
+                            smartActionsEnabled, user);
             List<Notification.Action> smartActions = new ArrayList<>();
-            if (mSmartActionsEnabled) {
+            if (smartActionsEnabled) {
                 int timeoutMs = DeviceConfig.getInt(
                         DeviceConfig.NAMESPACE_SYSTEMUI,
                         SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
@@ -168,9 +173,12 @@
             mImageData.uri = uri;
             mImageData.owner = user;
             mImageData.smartActions = smartActions;
-            mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
-            mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
-            mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
+            mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri,
+                    smartActionsEnabled);
+            mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri,
+                    smartActionsEnabled);
+            mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri,
+                    smartActionsEnabled);
             mImageData.quickShareAction = createQuickShareAction(mContext,
                     mQuickShareData.quickShareAction, uri);
             mImageData.subject = getSubjectString();
@@ -228,7 +236,8 @@
      * Assumes that the action intent is sent immediately after being supplied.
      */
     @VisibleForTesting
-    Supplier<ActionTransition> createShareAction(Context context, Resources r, Uri uri) {
+    Supplier<ActionTransition> createShareAction(Context context, Resources r, Uri uri,
+            boolean smartActionsEnabled) {
         return () -> {
             ActionTransition transition = mSharedElementTransition.get();
 
@@ -274,7 +283,7 @@
                             .putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true)
                             .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId)
                             .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED,
-                                    mSmartActionsEnabled)
+                                    smartActionsEnabled)
                             .setAction(Intent.ACTION_SEND)
                             .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
@@ -290,7 +299,8 @@
     }
 
     @VisibleForTesting
-    Supplier<ActionTransition> createEditAction(Context context, Resources r, Uri uri) {
+    Supplier<ActionTransition> createEditAction(Context context, Resources r, Uri uri,
+            boolean smartActionsEnabled) {
         return () -> {
             ActionTransition transition = mSharedElementTransition.get();
             // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
@@ -323,7 +333,7 @@
                             .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
                             .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId)
                             .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED,
-                                    mSmartActionsEnabled)
+                                    smartActionsEnabled)
                             .putExtra(ScreenshotController.EXTRA_OVERRIDE_TRANSITION, true)
                             .setAction(Intent.ACTION_EDIT)
                             .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
@@ -339,7 +349,8 @@
     }
 
     @VisibleForTesting
-    Notification.Action createDeleteAction(Context context, Resources r, Uri uri) {
+    Notification.Action createDeleteAction(Context context, Resources r, Uri uri,
+            boolean smartActionsEnabled) {
         // Make sure pending intents for the system user are still unique across users
         // by setting the (otherwise unused) request code to the current user id.
         int requestCode = mContext.getUserId();
@@ -350,7 +361,7 @@
                         .putExtra(ScreenshotController.SCREENSHOT_URI_ID, uri.toString())
                         .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId)
                         .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED,
-                                mSmartActionsEnabled)
+                                smartActionsEnabled)
                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
                 PendingIntent.FLAG_CANCEL_CURRENT
                         | PendingIntent.FLAG_ONE_SHOT
@@ -391,7 +402,7 @@
             Intent intent = new Intent(context, SmartActionsReceiver.class)
                     .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent)
                     .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-            addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled);
+            addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
             PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
                     mRandom.nextInt(),
                     intent,
@@ -445,7 +456,9 @@
         Intent intent = new Intent(context, SmartActionsReceiver.class)
                 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent)
                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled);
+        // We only query for quick share actions when smart actions are enabled, so we can assert
+        // that it's true here.
+        addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
         PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
                 mRandom.nextInt(),
                 intent,
@@ -464,7 +477,7 @@
                 mScreenshotSmartActions.getSmartActionsFuture(
                         mScreenshotId, null, image, mSmartActionsProvider,
                         QUICK_SHARE_ACTION,
-                        mSmartActionsEnabled, user);
+                        true /* smartActionsEnabled */, user);
         int timeoutMs = DeviceConfig.getInt(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 231e415..d94c827 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,6 +20,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
@@ -62,6 +63,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -275,6 +277,7 @@
             mScreenshotNotificationSmartActionsProvider;
     private final TimeoutHandler mScreenshotHandler;
     private final ActionIntentExecutor mActionExecutor;
+    private final UserManager mUserManager;
 
     private ScreenshotView mScreenshotView;
     private Bitmap mScreenBitmap;
@@ -313,7 +316,8 @@
             TimeoutHandler timeoutHandler,
             BroadcastSender broadcastSender,
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
-            ActionIntentExecutor actionExecutor
+            ActionIntentExecutor actionExecutor,
+            UserManager userManager
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
         mNotificationsController = screenshotNotificationsController;
@@ -344,6 +348,7 @@
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mFlags = flags;
         mActionExecutor = actionExecutor;
+        mUserManager = userManager;
 
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
 
@@ -586,7 +591,7 @@
         // Wait until this window is attached to request because it is
         // the reference used to locate the target window (below).
         withWindowAttached(() -> {
-            requestScrollCapture();
+            requestScrollCapture(owner);
             mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
                     new ViewRootImpl.ActivityConfigCallback() {
                         @Override
@@ -598,11 +603,11 @@
                                 mScreenshotView.hideScrollChip();
                                 // Delay scroll capture eval a bit to allow the underlying activity
                                 // to set up in the new orientation.
-                                mScreenshotHandler.postDelayed(
-                                        ScreenshotController.this::requestScrollCapture, 150);
+                                mScreenshotHandler.postDelayed(() -> {
+                                    requestScrollCapture(owner);
+                                }, 150);
                                 mScreenshotView.updateInsets(
-                                        mWindowManager.getCurrentWindowMetrics()
-                                                .getWindowInsets());
+                                        mWindowManager.getCurrentWindowMetrics().getWindowInsets());
                                 // Screenshot animation calculations won't be valid anymore,
                                 // so just end
                                 if (mScreenshotAnimation != null
@@ -634,6 +639,11 @@
                         return true;
                     }
                 });
+
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mScreenshotView.badgeScreenshot(
+                    mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
+        }
         mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mScreenshotView);
@@ -645,7 +655,7 @@
         mScreenshotHandler.cancelTimeout(); // restarted after animation
     }
 
-    private void requestScrollCapture() {
+    private void requestScrollCapture(UserHandle owner) {
         if (!allowLongScreenshots()) {
             Log.d(TAG, "Long screenshots not supported on this device");
             return;
@@ -658,10 +668,11 @@
                 mScrollCaptureClient.request(DEFAULT_DISPLAY);
         mLastScrollCaptureRequest = future;
         mLastScrollCaptureRequest.addListener(() ->
-                onScrollCaptureResponseReady(future), mMainExecutor);
+                onScrollCaptureResponseReady(future, owner), mMainExecutor);
     }
 
-    private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture) {
+    private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture,
+            UserHandle owner) {
         try {
             if (mLastScrollCaptureResponse != null) {
                 mLastScrollCaptureResponse.close();
@@ -691,7 +702,7 @@
                 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
                         mScreenshotTakenInPortrait);
                 // delay starting scroll capture to make sure the scrim is up before the app moves
-                mScreenshotView.post(() -> runBatchScrollCapture(response));
+                mScreenshotView.post(() -> runBatchScrollCapture(response, owner));
             });
         } catch (InterruptedException | ExecutionException e) {
             Log.e(TAG, "requestScrollCapture failed", e);
@@ -700,7 +711,7 @@
 
     ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture;
 
-    private void runBatchScrollCapture(ScrollCaptureResponse response) {
+    private void runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
         // Clear the reference to prevent close() in dismissScreenshot
         mLastScrollCaptureResponse = null;
 
@@ -734,6 +745,8 @@
                                     longScreenshot));
 
             final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
+            intent.putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE,
+                    owner);
             intent.setFlags(
                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 
@@ -969,16 +982,25 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                             super.onAnimationEnd(animation);
-                            mScreenshotView.setChipIntents(imageData);
+                            doPostAnimation(imageData);
                         }
                     });
                 } else {
-                    mScreenshotView.setChipIntents(imageData);
+                    doPostAnimation(imageData);
                 }
             });
         }
     }
 
+    private void doPostAnimation(ScreenshotController.SavedImageData imageData) {
+        mScreenshotView.setChipIntents(imageData);
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+                && mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
+            // TODO: Read app from configuration
+            mScreenshotView.showWorkProfileMessage("Files");
+        }
+    }
+
     /**
      * Sets up the action shade and its entrance animation, once we get the Quick Share action data.
      */
@@ -1037,8 +1059,13 @@
     }
 
     private boolean isUserSetupComplete(UserHandle owner) {
-        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
-                        .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
+                    .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+        } else {
+            return Settings.Secure.getInt(mContext.getContentResolver(),
+                    SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
index 8b5a24c..c891686 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java
@@ -89,7 +89,9 @@
     @UiEvent(doc = "User has saved a long screenshot to a file")
     SCREENSHOT_LONG_SCREENSHOT_SAVED(910),
     @UiEvent(doc = "User has discarded the result of a long screenshot")
-    SCREENSHOT_LONG_SCREENSHOT_EXIT(911);
+    SCREENSHOT_LONG_SCREENSHOT_EXIT(911),
+    @UiEvent(doc = "A screenshot has been taken and saved to work profile")
+    SCREENSHOT_SAVED_TO_WORK_PROFILE(1240);
 
     private final int mId;
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index c41e2bc..4cb91e1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -15,12 +15,17 @@
  */
 package com.android.systemui.screenshot
 
-import android.app.Service
 import android.content.Intent
 import android.os.IBinder
 import android.util.Log
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.phone.CentralSurfaces
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import java.util.Optional
 import javax.inject.Inject
 
@@ -30,7 +35,8 @@
 internal class ScreenshotProxyService @Inject constructor(
     private val mExpansionMgr: ShadeExpansionStateManager,
     private val mCentralSurfacesOptional: Optional<CentralSurfaces>,
-) : Service() {
+    @Main private val mMainDispatcher: CoroutineDispatcher,
+) : LifecycleService() {
 
     private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
         /**
@@ -43,20 +49,28 @@
         }
 
         override fun dismissKeyguard(callback: IOnDoneCallback) {
-            if (mCentralSurfacesOptional.isPresent) {
-                mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard(
-                    Runnable {
-                        callback.onDone(true)
-                    }, null,
-                    true /* dismissShade */, true /* afterKeyguardGone */,
-                    true /* deferred */
-                )
-            } else {
-                callback.onDone(false)
+            lifecycleScope.launch {
+                executeAfterDismissing(callback)
             }
         }
     }
 
+    private suspend fun executeAfterDismissing(callback: IOnDoneCallback) =
+        withContext(mMainDispatcher) {
+            mCentralSurfacesOptional.ifPresentOrElse(
+                    {
+                        it.executeRunnableDismissingKeyguard(
+                                Runnable {
+                                    callback.onDone(true)
+                                }, null,
+                                true /* dismissShade */, true /* afterKeyguardGone */,
+                                true /* deferred */
+                        )
+                    },
+                    { callback.onDone(false) }
+            )
+        }
+
     override fun onBind(intent: Intent): IBinder? {
         Log.d(TAG, "onBind: $intent")
         return mBinder
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 26cbcbf..0a4b550 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -74,13 +74,13 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 import android.widget.HorizontalScrollView;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
@@ -122,15 +122,9 @@
     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
-    private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
-    private static final float ROUNDED_CORNER_RADIUS = .25f;
     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
 
-    private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
-
     private final Resources mResources;
     private final Interpolator mFastOutSlowIn;
     private final DisplayMetrics mDisplayMetrics;
@@ -144,7 +138,10 @@
 
     private ImageView mScrollingScrim;
     private DraggableConstraintLayout mScreenshotStatic;
+    private ViewGroup mMessageContainer;
+    private TextView mMessageContent;
     private ImageView mScreenshotPreview;
+    private ImageView mScreenshotBadge;
     private View mScreenshotPreviewBorder;
     private ImageView mScrollablePreview;
     private ImageView mScreenshotFlash;
@@ -346,15 +343,32 @@
         }
     }
 
+    /**
+     * Show a notification under the screenshot view indicating that a work profile screenshot has
+     * been taken and which app can be used to view it.
+     *
+     * @param appName The name of the app to use to view screenshots
+     */
+    void showWorkProfileMessage(String appName) {
+        mMessageContent.setText(
+                mContext.getString(R.string.screenshot_work_profile_notification, appName));
+        mMessageContainer.setVisibility(VISIBLE);
+    }
+
     @Override // View
     protected void onFinishInflate() {
         mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
         mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static));
+        mMessageContainer =
+                requireNonNull(mScreenshotStatic.findViewById(R.id.screenshot_message_container));
+        mMessageContent =
+                requireNonNull(mMessageContainer.findViewById(R.id.screenshot_message_content));
         mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview));
 
         mScreenshotPreviewBorder = requireNonNull(
                 findViewById(R.id.screenshot_preview_border));
         mScreenshotPreview.setClipToOutline(true);
+        mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge));
 
         mActionsContainerBackground = requireNonNull(findViewById(
                 R.id.actions_container_background));
@@ -595,8 +609,11 @@
 
         ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
         borderFadeIn.setDuration(100);
-        borderFadeIn.addUpdateListener((animation) ->
-                mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
+        borderFadeIn.addUpdateListener((animation) -> {
+            float borderAlpha = animation.getAnimatedFraction();
+            mScreenshotPreviewBorder.setAlpha(borderAlpha);
+            mScreenshotBadge.setAlpha(borderAlpha);
+        });
 
         if (showFlash) {
             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
@@ -763,11 +780,18 @@
         return animator;
     }
 
+    void badgeScreenshot(Drawable badge) {
+        mScreenshotBadge.setImageDrawable(badge);
+        mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
+    }
+
     void setChipIntents(ScreenshotController.SavedImageData imageData) {
         mShareChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
-                mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent(
+                prepareSharedTransition();
+                mActionExecutor.launchIntentAsync(
+                        ActionIntentCreator.INSTANCE.createShareIntent(
                                 imageData.uri, imageData.subject),
                         imageData.shareTransition.get().bundle,
                         imageData.owner.getIdentifier(), false);
@@ -778,6 +802,7 @@
         mEditChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -789,6 +814,7 @@
         mScreenshotPreview.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -1023,6 +1049,9 @@
         mScreenshotPreview.setVisibility(View.INVISIBLE);
         mScreenshotPreview.setAlpha(1f);
         mScreenshotPreviewBorder.setAlpha(0);
+        mScreenshotBadge.setAlpha(0f);
+        mScreenshotBadge.setVisibility(View.GONE);
+        mScreenshotBadge.setImageDrawable(null);
         mPendingSharedTransition = false;
         mActionsContainerBackground.setVisibility(View.GONE);
         mActionsContainer.setVisibility(View.GONE);
@@ -1064,6 +1093,12 @@
         }
     }
 
+    private void prepareSharedTransition() {
+        mPendingSharedTransition = true;
+        // fade out non-preview UI
+        createScreenshotFadeDismissAnimation().start();
+    }
+
     ValueAnimator createScreenshotFadeDismissAnimation() {
         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
         alphaAnim.addUpdateListener(animation -> {
@@ -1072,6 +1107,7 @@
             mActionsContainerBackground.setAlpha(alpha);
             mActionsContainer.setAlpha(alpha);
             mScreenshotPreviewBorder.setAlpha(alpha);
+            mScreenshotBadge.setAlpha(alpha);
         });
         alphaAnim.setDuration(600);
         return alphaAnim;
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
index bbba007..b36f0d7 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
@@ -33,13 +33,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 
 /**
  * Drawable used on SysUI scrims.
  */
 public class ScrimDrawable extends Drawable {
     private static final String TAG = "ScrimDrawable";
-    private static final long COLOR_ANIMATION_DURATION = 2000;
 
     private final Paint mPaint;
     private int mAlpha = 255;
@@ -76,7 +76,7 @@
             final int mainFrom = mMainColor;
 
             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
-            anim.setDuration(COLOR_ANIMATION_DURATION);
+            anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
             anim.addUpdateListener(animation -> {
                 float ratio = (float) animation.getAnimatedValue();
                 mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvSensorPrivacyChangedActivity.java b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvSensorPrivacyChangedActivity.java
new file mode 100644
index 0000000..731b177
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvSensorPrivacyChangedActivity.java
@@ -0,0 +1,233 @@
+/*
+ * 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.sensorprivacy.television;
+
+import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
+import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
+
+import android.annotation.DimenRes;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.Drawable;
+import android.hardware.SensorPrivacyManager;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
+import com.android.systemui.tv.TvBottomSheetActivity;
+import com.android.systemui.util.settings.GlobalSettings;
+
+import javax.inject.Inject;
+
+/**
+ * Bottom sheet that is shown when the camera/mic sensors privacy state changed
+ * by the global software toggle or physical privacy switch.
+ */
+public class TvSensorPrivacyChangedActivity extends TvBottomSheetActivity {
+
+    private static final String TAG = TvSensorPrivacyChangedActivity.class.getSimpleName();
+
+    private static final int ALL_SENSORS = Integer.MAX_VALUE;
+
+    private int mSensor = -1;
+    private int mToggleType = -1;
+
+    private final GlobalSettings mGlobalSettings;
+    private final IndividualSensorPrivacyController mSensorPrivacyController;
+    private IndividualSensorPrivacyController.Callback mSensorPrivacyCallback;
+    private TextView mTitle;
+    private TextView mContent;
+    private ImageView mIcon;
+    private ImageView mSecondIcon;
+    private Button mPositiveButton;
+    private Button mCancelButton;
+
+    @Inject
+    public TvSensorPrivacyChangedActivity(
+            IndividualSensorPrivacyController individualSensorPrivacyController,
+            GlobalSettings globalSettings) {
+        mSensorPrivacyController = individualSensorPrivacyController;
+        mGlobalSettings = globalSettings;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addSystemFlags(
+                WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+
+        boolean allSensors = getIntent().getBooleanExtra(SensorPrivacyManager.EXTRA_ALL_SENSORS,
+                false);
+        if (allSensors) {
+            mSensor = ALL_SENSORS;
+        } else {
+            mSensor = getIntent().getIntExtra(SensorPrivacyManager.EXTRA_SENSOR, -1);
+        }
+
+        mToggleType = getIntent().getIntExtra(SensorPrivacyManager.EXTRA_TOGGLE_TYPE, -1);
+
+        if (mSensor == -1 || mToggleType == -1) {
+            Log.v(TAG, "Invalid extras");
+            finish();
+            return;
+        }
+
+        // Do not show for software toggles
+        if (mToggleType == SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE) {
+            finish();
+            return;
+        }
+
+        mSensorPrivacyCallback = (sensor, blocked) -> {
+            updateUI();
+        };
+
+        initUI();
+    }
+
+    private void initUI() {
+        mTitle = findViewById(R.id.bottom_sheet_title);
+        mContent = findViewById(R.id.bottom_sheet_body);
+        mIcon = findViewById(R.id.bottom_sheet_icon);
+        // mic icon if both icons are shown
+        mSecondIcon = findViewById(R.id.bottom_sheet_second_icon);
+        mPositiveButton = findViewById(R.id.bottom_sheet_positive_button);
+        mCancelButton = findViewById(R.id.bottom_sheet_negative_button);
+
+        mCancelButton.setText(android.R.string.cancel);
+        mCancelButton.setOnClickListener(v -> finish());
+
+        updateUI();
+    }
+
+    private void updateUI() {
+        final Resources resources = getResources();
+        setIconTint(resources.getBoolean(R.bool.config_unblockHwSensorIconEnableTint));
+        setIconSize(R.dimen.unblock_hw_sensor_icon_width, R.dimen.unblock_hw_sensor_icon_height);
+
+        switch (mSensor) {
+            case CAMERA:
+                updateUiForCameraUpdate(
+                        mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA));
+                break;
+            case MICROPHONE:
+            default:
+                updateUiForMicUpdate(
+                        mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE));
+                break;
+        }
+
+        // Start animation if drawable is animated
+        Drawable iconDrawable = mIcon.getDrawable();
+        if (iconDrawable instanceof Animatable) {
+            ((Animatable) iconDrawable).start();
+        }
+
+        mPositiveButton.setVisibility(View.GONE);
+        mCancelButton.setText(android.R.string.ok);
+    }
+
+    private void updateUiForMicUpdate(boolean blocked) {
+        if (blocked) {
+            mTitle.setText(R.string.sensor_privacy_mic_turned_off_dialog_title);
+            if (isExplicitUserInteractionAudioBypassAllowed()) {
+                mContent.setText(R.string.sensor_privacy_mic_blocked_with_exception_dialog_content);
+            } else {
+                mContent.setText(R.string.sensor_privacy_mic_blocked_no_exception_dialog_content);
+            }
+            mIcon.setImageResource(R.drawable.unblock_hw_sensor_microphone);
+            mSecondIcon.setVisibility(View.GONE);
+        } else {
+            mTitle.setText(R.string.sensor_privacy_mic_turned_on_dialog_title);
+            mContent.setText(R.string.sensor_privacy_mic_unblocked_dialog_content);
+            mIcon.setImageResource(com.android.internal.R.drawable.ic_mic_allowed);
+            mSecondIcon.setVisibility(View.GONE);
+        }
+    }
+
+    private void updateUiForCameraUpdate(boolean blocked) {
+        if (blocked) {
+            mTitle.setText(R.string.sensor_privacy_camera_turned_off_dialog_title);
+            mContent.setText(R.string.sensor_privacy_camera_blocked_dialog_content);
+            mIcon.setImageResource(R.drawable.unblock_hw_sensor_camera);
+            mSecondIcon.setVisibility(View.GONE);
+        } else {
+            mTitle.setText(R.string.sensor_privacy_camera_turned_on_dialog_title);
+            mContent.setText(R.string.sensor_privacy_camera_unblocked_dialog_content);
+            mIcon.setImageResource(com.android.internal.R.drawable.ic_camera_allowed);
+            mSecondIcon.setVisibility(View.GONE);
+        }
+    }
+
+    private void setIconTint(boolean enableTint) {
+        final Resources resources = getResources();
+
+        if (enableTint) {
+            final ColorStateList iconTint = resources.getColorStateList(
+                    R.color.bottom_sheet_icon_color, getTheme());
+            mIcon.setImageTintList(iconTint);
+            mSecondIcon.setImageTintList(iconTint);
+        } else {
+            mIcon.setImageTintList(null);
+            mSecondIcon.setImageTintList(null);
+        }
+
+        mIcon.invalidate();
+        mSecondIcon.invalidate();
+    }
+
+    private void setIconSize(@DimenRes int widthRes, @DimenRes int heightRes) {
+        final Resources resources = getResources();
+        final int iconWidth = resources.getDimensionPixelSize(widthRes);
+        final int iconHeight = resources.getDimensionPixelSize(heightRes);
+
+        mIcon.getLayoutParams().width = iconWidth;
+        mIcon.getLayoutParams().height = iconHeight;
+        mIcon.invalidate();
+
+        mSecondIcon.getLayoutParams().width = iconWidth;
+        mSecondIcon.getLayoutParams().height = iconHeight;
+        mSecondIcon.invalidate();
+    }
+
+    private boolean isExplicitUserInteractionAudioBypassAllowed() {
+        return mGlobalSettings.getInt(
+                Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED, 1) == 1;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateUI();
+        mSensorPrivacyController.addCallback(mSensorPrivacyCallback);
+    }
+
+    @Override
+    public void onPause() {
+        mSensorPrivacyController.removeCallback(mSensorPrivacyCallback);
+        super.onPause();
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java
index d543eb2..1b9657f 100644
--- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java
@@ -16,17 +16,23 @@
 
 package com.android.systemui.sensorprivacy.television;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
 import static android.hardware.SensorPrivacyManager.Sources.OTHER;
 
 import android.annotation.DimenRes;
+import android.app.AppOpsManager;
+import android.app.role.RoleManager;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.hardware.SensorPrivacyManager;
 import android.os.Bundle;
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
 import android.view.WindowManager;
@@ -39,6 +45,8 @@
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.tv.TvBottomSheetActivity;
 
+import java.util.List;
+
 import javax.inject.Inject;
 
 /**
@@ -57,6 +65,8 @@
 
     private int mSensor = -1;
 
+    private final AppOpsManager mAppOpsManager;
+    private final RoleManager mRoleManager;
     private final IndividualSensorPrivacyController mSensorPrivacyController;
     private IndividualSensorPrivacyController.Callback mSensorPrivacyCallback;
     private TextView mTitle;
@@ -68,8 +78,11 @@
 
     @Inject
     public TvUnblockSensorActivity(
-            IndividualSensorPrivacyController individualSensorPrivacyController) {
+            IndividualSensorPrivacyController individualSensorPrivacyController,
+            AppOpsManager appOpsManager, RoleManager roleManager) {
         mSensorPrivacyController = individualSensorPrivacyController;
+        mAppOpsManager = appOpsManager;
+        mRoleManager = roleManager;
     }
 
     @Override
@@ -120,6 +133,10 @@
                 toastMsgResId = R.string.sensor_privacy_mic_camera_unblocked_toast_content;
                 break;
         }
+        showToastAndFinish(toastMsgResId);
+    }
+
+    private void showToastAndFinish(int toastMsgResId) {
         Toast.makeText(this, toastMsgResId, Toast.LENGTH_SHORT).show();
         finish();
     }
@@ -149,7 +166,9 @@
     }
 
     private void updateUI() {
-        if (isBlockedByHardwareToggle()) {
+        if (isHTTAccessDisabled()) {
+            updateUiForHTT();
+        } else if (isBlockedByHardwareToggle()) {
             updateUiForHardwareToggle();
         } else {
             updateUiForSoftwareToggle();
@@ -208,20 +227,20 @@
 
         switch (mSensor) {
             case MICROPHONE:
-                mTitle.setText(R.string.sensor_privacy_start_use_mic_dialog_title);
+                mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title);
                 mContent.setText(R.string.sensor_privacy_start_use_mic_dialog_content);
                 mIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone);
                 mSecondIcon.setVisibility(View.GONE);
                 break;
             case CAMERA:
-                mTitle.setText(R.string.sensor_privacy_start_use_camera_dialog_title);
+                mTitle.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_title);
                 mContent.setText(R.string.sensor_privacy_start_use_camera_dialog_content);
                 mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera);
                 mSecondIcon.setVisibility(View.GONE);
                 break;
             case ALL_SENSORS:
             default:
-                mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_title);
+                mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_title);
                 mContent.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_content);
                 mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera);
                 mSecondIcon.setImageResource(
@@ -241,6 +260,29 @@
         });
     }
 
+    private void updateUiForHTT() {
+        setIconTint(true);
+        setIconSize(R.dimen.bottom_sheet_icon_size, R.dimen.bottom_sheet_icon_size);
+
+        mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title);
+        mContent.setText(R.string.sensor_privacy_htt_blocked_dialog_content);
+        mIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone);
+        mSecondIcon.setVisibility(View.GONE);
+
+        mPositiveButton.setText(R.string.sensor_privacy_dialog_open_settings);
+        mPositiveButton.setOnClickListener(v -> {
+            Intent openPrivacySettings = new Intent(ACTION_MANAGE_MICROPHONE_PRIVACY);
+            ActivityInfo activityInfo = openPrivacySettings.resolveActivityInfo(getPackageManager(),
+                    MATCH_SYSTEM_ONLY);
+            if (activityInfo == null) {
+                showToastAndFinish(com.android.internal.R.string.noApplications);
+            } else {
+                startActivity(openPrivacySettings);
+                finish();
+            }
+        });
+    }
+
     private void setIconTint(boolean enableTint) {
         final Resources resources = getResources();
 
@@ -272,6 +314,18 @@
         mSecondIcon.invalidate();
     }
 
+    private boolean isHTTAccessDisabled() {
+        String pkg = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        List<String> assistantPkgs = mRoleManager.getRoleHolders(RoleManager.ROLE_ASSISTANT);
+        if (!assistantPkgs.contains(pkg)) {
+            return false;
+        }
+
+        return (mAppOpsManager.checkOpNoThrow(
+                AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, UserHandle.myUserId(),
+                pkg) != AppOpsManager.MODE_ALLOWED);
+    }
+
     @Override
     public void onResume() {
         super.onResume();
diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserObservable.java b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserObservable.java
deleted file mode 100644
index dea8c32..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserObservable.java
+++ /dev/null
@@ -1,63 +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.settings;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-
-import com.android.systemui.broadcast.BroadcastDispatcher;
-
-/**
- * A class that has an observable for the current user.
- */
-public class CurrentUserObservable {
-
-    private final CurrentUserTracker mTracker;
-
-    private final MutableLiveData<Integer> mCurrentUser = new MutableLiveData<Integer>() {
-        @Override
-        protected void onActive() {
-            super.onActive();
-            mTracker.startTracking();
-        }
-
-        @Override
-        protected void onInactive() {
-            super.onInactive();
-            mTracker.stopTracking();
-        }
-    };
-
-    public CurrentUserObservable(BroadcastDispatcher broadcastDispatcher) {
-        mTracker = new CurrentUserTracker(broadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                mCurrentUser.setValue(newUserId);
-            }
-        };
-    }
-
-    /**
-     * Returns the current user that can be observed.
-     */
-    public LiveData<Integer> getCurrentUser() {
-        if (mCurrentUser.getValue() == null) {
-            mCurrentUser.setValue(mTracker.getCurrentUserId());
-        }
-        return mCurrentUser;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java
deleted file mode 100644
index 9599d77..0000000
--- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserTracker.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2013 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.settings;
-
-import android.app.ActivityManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.UserHandle;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-
-public abstract class CurrentUserTracker {
-    private final UserReceiver mUserReceiver;
-
-    private Consumer<Integer> mCallback = this::onUserSwitched;
-
-    public CurrentUserTracker(BroadcastDispatcher broadcastDispatcher) {
-        this(UserReceiver.getInstance(broadcastDispatcher));
-    }
-
-    @VisibleForTesting
-    CurrentUserTracker(UserReceiver receiver) {
-        mUserReceiver = receiver;
-    }
-
-    public int getCurrentUserId() {
-        return mUserReceiver.getCurrentUserId();
-    }
-
-    public void startTracking() {
-        mUserReceiver.addTracker(mCallback);
-    }
-
-    public void stopTracking() {
-        mUserReceiver.removeTracker(mCallback);
-    }
-
-    public abstract void onUserSwitched(int newUserId);
-
-    @VisibleForTesting
-    static class UserReceiver extends BroadcastReceiver {
-        private static UserReceiver sInstance;
-
-        private boolean mReceiverRegistered;
-        private int mCurrentUserId;
-        private final BroadcastDispatcher mBroadcastDispatcher;
-
-        private List<Consumer<Integer>> mCallbacks = new ArrayList<>();
-
-        @VisibleForTesting
-        UserReceiver(BroadcastDispatcher broadcastDispatcher) {
-            mBroadcastDispatcher = broadcastDispatcher;
-        }
-
-        static UserReceiver getInstance(BroadcastDispatcher broadcastDispatcher) {
-            if (sInstance == null) {
-                sInstance = new UserReceiver(broadcastDispatcher);
-            }
-            return sInstance;
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
-                notifyUserSwitched(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
-            }
-        }
-
-        public int getCurrentUserId() {
-            return mCurrentUserId;
-        }
-
-        private void addTracker(Consumer<Integer> callback) {
-            if (!mCallbacks.contains(callback)) {
-                mCallbacks.add(callback);
-            }
-            if (!mReceiverRegistered) {
-                mCurrentUserId = ActivityManager.getCurrentUser();
-                IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
-                mBroadcastDispatcher.registerReceiver(this, filter, null,
-                        UserHandle.ALL);
-                mReceiverRegistered = true;
-            }
-        }
-
-        private void removeTracker(Consumer<Integer> callback) {
-            if (mCallbacks.contains(callback)) {
-                mCallbacks.remove(callback);
-                if (mCallbacks.size() == 0 && mReceiverRegistered) {
-                    mBroadcastDispatcher.unregisterReceiver(this);
-                    mReceiverRegistered = false;
-                }
-            }
-        }
-
-        private void notifyUserSwitched(int newUserId) {
-            if (mCurrentUserId != newUserId) {
-                mCurrentUserId = newUserId;
-                List<Consumer<Integer>> callbacks = new ArrayList<>(mCallbacks);
-                for (Consumer<Integer> consumer : callbacks) {
-                    // Accepting may modify this list
-                    if (mCallbacks.contains(consumer)) {
-                        consumer.accept(newUserId);
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 6711734..200288b 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -30,7 +30,6 @@
 import androidx.annotation.WorkerThread
 import com.android.systemui.Dumpable
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE
 import com.android.systemui.util.Assert
 import java.io.PrintWriter
 import java.lang.ref.WeakReference
@@ -53,7 +52,7 @@
  *
  * Class constructed and initialized in [SettingsModule].
  */
-class UserTrackerImpl internal constructor(
+open class UserTrackerImpl internal constructor(
     private val context: Context,
     private val userManager: UserManager,
     private val dumpManager: DumpManager,
@@ -70,13 +69,13 @@
     private val mutex = Any()
 
     override var userId: Int by SynchronizedDelegate(context.userId)
-        private set
+        protected set
 
     override var userHandle: UserHandle by SynchronizedDelegate(context.user)
-        private set
+        protected set
 
     override var userContext: Context by SynchronizedDelegate(context)
-        private set
+        protected set
 
     override val userContentResolver: ContentResolver
         get() = userContext.contentResolver
@@ -94,7 +93,7 @@
      * modified.
      */
     override var userProfiles: List<UserInfo> by SynchronizedDelegate(emptyList())
-        private set
+        protected set
 
     @GuardedBy("callbacks")
     private val callbacks: MutableList<DataItem> = ArrayList()
@@ -108,6 +107,7 @@
 
         val filter = IntentFilter().apply {
             addAction(Intent.ACTION_USER_SWITCHED)
+            addAction(Intent.ACTION_USER_INFO_CHANGED)
             // These get called when a managed profile goes in or out of quiet mode.
             addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
             addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
@@ -125,6 +125,7 @@
             Intent.ACTION_USER_SWITCHED -> {
                 handleSwitchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL))
             }
+            Intent.ACTION_USER_INFO_CHANGED,
             Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
             Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
             Intent.ACTION_MANAGED_PROFILE_REMOVED,
@@ -155,7 +156,7 @@
     }
 
     @WorkerThread
-    private fun handleSwitchUser(newUser: Int) {
+    protected open fun handleSwitchUser(newUser: Int) {
         Assert.isNotMainThread()
         if (newUser == UserHandle.USER_NULL) {
             Log.w(TAG, "handleSwitchUser - Couldn't get new id from intent")
@@ -174,7 +175,7 @@
     }
 
     @WorkerThread
-    private fun handleProfilesChanged() {
+    protected open fun handleProfilesChanged() {
         Assert.isNotMainThread()
 
         val profiles = userManager.getProfiles(userId)
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 7801c68..5880003 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -21,6 +21,7 @@
 import static com.android.settingslib.display.BrightnessUtils.convertLinearToGammaFloat;
 
 import android.animation.ValueAnimator;
+import android.annotation.NonNull;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -46,11 +47,13 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.settingslib.RestrictedLockUtilsInternal;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 
+import java.util.concurrent.Executor;
+
 import javax.inject.Inject;
 
 public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
@@ -74,9 +77,10 @@
     private final Context mContext;
     private final ToggleSlider mControl;
     private final DisplayManager mDisplayManager;
-    private final CurrentUserTracker mUserTracker;
+    private final UserTracker mUserTracker;
     private final IVrManager mVrManager;
 
+    private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
     private final BrightnessObserver mBrightnessObserver;
 
@@ -169,7 +173,7 @@
             }
 
             mBrightnessObserver.startObserving();
-            mUserTracker.startTracking();
+            mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
 
             // Update the slider and mode before attaching the listener so we don't
             // receive the onChanged notifications for the initial values.
@@ -197,7 +201,7 @@
             }
 
             mBrightnessObserver.stopObserving();
-            mUserTracker.stopTracking();
+            mUserTracker.removeCallback(mUserChangedCallback);
 
             mHandler.sendEmptyMessage(MSG_DETACH_LISTENER);
         }
@@ -275,22 +279,27 @@
         }
     };
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    mBackgroundHandler.post(mUpdateModeRunnable);
+                    mBackgroundHandler.post(mUpdateSliderRunnable);
+                }
+            };
+
     public BrightnessController(
             Context context,
             ToggleSlider control,
-            BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
+            @Main Executor mainExecutor,
             @Background Handler bgHandler) {
         mContext = context;
         mControl = control;
         mControl.setMax(GAMMA_SPACE_MAX);
+        mMainExecutor = mainExecutor;
         mBackgroundHandler = bgHandler;
-        mUserTracker = new CurrentUserTracker(broadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                mBackgroundHandler.post(mUpdateModeRunnable);
-                mBackgroundHandler.post(mUpdateSliderRunnable);
-            }
-        };
+        mUserTracker = userTracker;
         mBrightnessObserver = new BrightnessObserver(mHandler);
 
         mDisplayId = mContext.getDisplayId();
@@ -364,7 +373,7 @@
                 mControl.setEnforcedAdmin(
                         RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
                                 UserManager.DISALLOW_CONFIG_BRIGHTNESS,
-                                mUserTracker.getCurrentUserId()));
+                                mUserTracker.getUserId()));
             }
         });
     }
@@ -440,16 +449,19 @@
     /** Factory for creating a {@link BrightnessController}. */
     public static class Factory {
         private final Context mContext;
-        private final BroadcastDispatcher mBroadcastDispatcher;
+        private final UserTracker mUserTracker;
+        private final Executor mMainExecutor;
         private final Handler mBackgroundHandler;
 
         @Inject
         public Factory(
                 Context context,
-                BroadcastDispatcher broadcastDispatcher,
+                UserTracker userTracker,
+                @Main Executor mainExecutor,
                 @Background Handler bgHandler) {
             mContext = context;
-            mBroadcastDispatcher = broadcastDispatcher;
+            mUserTracker = userTracker;
+            mMainExecutor = mainExecutor;
             mBackgroundHandler = bgHandler;
         }
 
@@ -458,7 +470,8 @@
             return new BrightnessController(
                     mContext,
                     toggleSlider,
-                    mBroadcastDispatcher,
+                    mUserTracker,
+                    mMainExecutor,
                     mBackgroundHandler);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 6e9f859..e208be9 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -20,6 +20,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.app.Activity;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.Gravity;
@@ -33,8 +34,12 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.UserTracker;
+
+import java.util.List;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -43,16 +48,19 @@
 
     private BrightnessController mBrightnessController;
     private final BrightnessSliderController.Factory mToggleSliderFactory;
-    private final BroadcastDispatcher mBroadcastDispatcher;
+    private final UserTracker mUserTracker;
+    private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
 
     @Inject
     public BrightnessDialog(
-            BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
             BrightnessSliderController.Factory factory,
+            @Main Executor mainExecutor,
             @Background Handler bgHandler) {
-        mBroadcastDispatcher = broadcastDispatcher;
+        mUserTracker = userTracker;
         mToggleSliderFactory = factory;
+        mMainExecutor = mainExecutor;
         mBackgroundHandler = bgHandler;
     }
 
@@ -83,13 +91,22 @@
         lp.leftMargin = horizontalMargin;
         lp.rightMargin = horizontalMargin;
         frame.setLayoutParams(lp);
+        Rect bounds = new Rect();
+        frame.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    // Exclude this view (and its horizontal margins) from triggering gestures.
+                    // This prevents back gesture from being triggered by dragging close to the
+                    // edge of the slider (0% or 100%).
+                    bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top);
+                    v.setSystemGestureExclusionRects(List.of(bounds));
+                });
 
         BrightnessSliderController controller = mToggleSliderFactory.create(this, frame);
         controller.init();
         frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
 
         mBrightnessController = new BrightnessController(
-                this, controller, mBroadcastDispatcher, mBackgroundHandler);
+                this, controller, mUserTracker, mMainExecutor, mBackgroundHandler);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
new file mode 100644
index 0000000..fc61e90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import com.android.systemui.camera.CameraGestureHelper;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+
+import javax.inject.Inject;
+
+/** Handles launching camera from Shade. */
+@SysUISingleton
+public class CameraLauncher {
+    private final CameraGestureHelper mCameraGestureHelper;
+    private final KeyguardBypassController mKeyguardBypassController;
+
+    private boolean mLaunchingAffordance;
+
+    @Inject
+    public CameraLauncher(
+            CameraGestureHelper cameraGestureHelper,
+            KeyguardBypassController keyguardBypassController
+    ) {
+        mCameraGestureHelper = cameraGestureHelper;
+        mKeyguardBypassController = keyguardBypassController;
+    }
+
+    /** Launches the camera. */
+    public void launchCamera(int source, boolean isShadeFullyCollapsed) {
+        if (!isShadeFullyCollapsed) {
+            setLaunchingAffordance(true);
+        }
+
+        mCameraGestureHelper.launchCamera(source);
+    }
+
+    /**
+     * Set whether we are currently launching an affordance. This is currently only set when
+     * launched via a camera gesture.
+     */
+    public void setLaunchingAffordance(boolean launchingAffordance) {
+        mLaunchingAffordance = launchingAffordance;
+        mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
+    }
+
+    /**
+     * Return true when a bottom affordance is launching an occluded activity with a splash screen.
+     */
+    public boolean isLaunchingAffordance() {
+        return mLaunchingAffordance;
+    }
+
+    /**
+     * Whether the camera application can be launched for the camera launch gesture.
+     */
+    public boolean canCameraGestureBeLaunched(int barState) {
+        return mCameraGestureHelper.canCameraGestureBeLaunched(barState);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
index 4063af3..5011227 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/CombinedShadeHeadersConstraintManagerImpl.kt
@@ -51,6 +51,8 @@
                 connect(R.id.statusIcons, ConstraintSet.START, R.id.date, ConstraintSet.END)
                 connect(R.id.privacy_container, ConstraintSet.START, R.id.date, ConstraintSet.END)
                 constrainWidth(R.id.statusIcons, ViewGroup.LayoutParams.WRAP_CONTENT)
+                constrainedWidth(R.id.date, true)
+                constrainedWidth(R.id.statusIcons, true)
             }
         )
     }
@@ -92,7 +94,8 @@
                     centerEnd,
                     ConstraintSet.END
                 )
-                constrainWidth(R.id.statusIcons, 0)
+                constrainedWidth(R.id.date, true)
+                constrainedWidth(R.id.statusIcons, true)
             },
             qsConstraintsChanges = {
                 setGuidelineBegin(centerStart, offsetFromEdge)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
new file mode 100644
index 0000000..ae303eb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+import com.android.keyguard.LockIconViewController;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Drawable for NotificationPanelViewController.
+ */
+public class DebugDrawable extends Drawable {
+
+    private final NotificationPanelViewController mNotificationPanelViewController;
+    private final NotificationPanelView mView;
+    private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
+    private final LockIconViewController mLockIconViewController;
+    private final Set<Integer> mDebugTextUsedYPositions;
+    private final Paint mDebugPaint;
+
+    public DebugDrawable(
+            NotificationPanelViewController notificationPanelViewController,
+            NotificationPanelView notificationPanelView,
+            NotificationStackScrollLayoutController notificationStackScrollLayoutController,
+            LockIconViewController lockIconViewController
+    ) {
+        mNotificationPanelViewController = notificationPanelViewController;
+        mView = notificationPanelView;
+        mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
+        mLockIconViewController = lockIconViewController;
+        mDebugTextUsedYPositions = new HashSet<>();
+        mDebugPaint = new Paint();
+    }
+
+    @Override
+    public void draw(@androidx.annotation.NonNull @NonNull Canvas canvas) {
+        mDebugTextUsedYPositions.clear();
+
+        mDebugPaint.setColor(Color.RED);
+        mDebugPaint.setStrokeWidth(2);
+        mDebugPaint.setStyle(Paint.Style.STROKE);
+        mDebugPaint.setTextSize(24);
+        String headerDebugInfo = mNotificationPanelViewController.getHeaderDebugInfo();
+        if (headerDebugInfo != null) canvas.drawText(headerDebugInfo, 50, 100, mDebugPaint);
+
+        drawDebugInfo(canvas, mNotificationPanelViewController.getMaxPanelHeight(),
+                Color.RED, "getMaxPanelHeight()");
+        drawDebugInfo(canvas, (int) mNotificationPanelViewController.getExpandedHeight(),
+                Color.BLUE, "getExpandedHeight()");
+        drawDebugInfo(canvas, mNotificationPanelViewController.calculatePanelHeightQsExpanded(),
+                Color.GREEN, "calculatePanelHeightQsExpanded()");
+        drawDebugInfo(canvas, mNotificationPanelViewController.calculatePanelHeightQsExpanded(),
+                Color.YELLOW, "calculatePanelHeightShade()");
+        drawDebugInfo(canvas,
+                (int) mNotificationPanelViewController.calculateNotificationsTopPadding(),
+                Color.MAGENTA, "calculateNotificationsTopPadding()");
+        drawDebugInfo(canvas, mNotificationPanelViewController.getClockPositionResult().clockY,
+                Color.GRAY, "mClockPositionResult.clockY");
+        drawDebugInfo(canvas, (int) mLockIconViewController.getTop(), Color.GRAY,
+                "mLockIconViewController.getTop()");
+
+        if (mNotificationPanelViewController.getKeyguardShowing()) {
+            // Notifications have the space between those two lines.
+            drawDebugInfo(canvas,
+                    mNotificationStackScrollLayoutController.getTop()
+                            + (int) mNotificationPanelViewController
+                            .getKeyguardNotificationTopPadding(),
+                    Color.RED, "NSSL.getTop() + mKeyguardNotificationTopPadding");
+
+            drawDebugInfo(canvas, mNotificationStackScrollLayoutController.getBottom()
+                            - (int) mNotificationPanelViewController
+                            .getKeyguardNotificationBottomPadding(),
+                    Color.RED, "NSSL.getBottom() - mKeyguardNotificationBottomPadding");
+        }
+
+        mDebugPaint.setColor(Color.CYAN);
+        canvas.drawLine(0,
+                mNotificationPanelViewController.getClockPositionResult().stackScrollerPadding,
+                mView.getWidth(), mNotificationStackScrollLayoutController.getTopPadding(),
+                mDebugPaint);
+    }
+
+    private void drawDebugInfo(Canvas canvas, int y, int color, String label) {
+        mDebugPaint.setColor(color);
+        canvas.drawLine(/* startX= */ 0, /* startY= */ y, /* stopX= */ mView.getWidth(),
+                /* stopY= */ y, mDebugPaint);
+        canvas.drawText(label + " = " + y + "px", /* x= */ 0,
+                /* y= */ computeDebugYTextPosition(y), mDebugPaint);
+    }
+
+    private int computeDebugYTextPosition(int lineY) {
+        if (lineY - mDebugPaint.getTextSize() < 0) {
+            // Avoiding drawing out of bounds
+            lineY += mDebugPaint.getTextSize();
+        }
+        int textY = lineY;
+        while (mDebugTextUsedYPositions.contains(textY)) {
+            textY = (int) (textY + mDebugPaint.getTextSize());
+        }
+        mDebugTextUsedYPositions.add(textY);
+        return textY;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.UNKNOWN;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index a494f42..63d0d16 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -246,6 +246,8 @@
             qsCarrierGroup.updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers)
             if (header is MotionLayout) {
                 loadConstraints()
+                header.minHeight = resources
+                        .getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height)
                 lastInsets?.let { updateConstraintsForInsets(header, it) }
             }
             updateResources()
@@ -292,6 +294,7 @@
             clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
                 val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
                 v.pivotX = newPivot
+                v.pivotY = v.height.toFloat() / 2
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
index 07e8b9f..754036d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
@@ -16,7 +16,7 @@
 import android.view.MotionEvent
 import com.android.systemui.dump.DumpsysTableLogger
 import com.android.systemui.dump.Row
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
 import java.text.SimpleDateFormat
 import java.util.Locale
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
deleted file mode 100644
index 4558061..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
+++ /dev/null
@@ -1,53 +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
-
-/** Provides certain notification panel events.  */
-interface NotifPanelEvents {
-
-    /** Registers callbacks to be invoked when notification panel events occur.  */
-    fun registerListener(listener: Listener)
-
-    /** Unregisters callbacks previously registered via [registerListener]  */
-    fun unregisterListener(listener: Listener)
-
-    /** Callbacks for certain notification panel events. */
-    interface Listener {
-
-        /** Invoked when the notification panel starts or stops collapsing. */
-        @JvmDefault
-        fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
-
-        /**
-         * Invoked when the notification panel starts or stops launching an [android.app.Activity].
-         */
-        @JvmDefault
-        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
-
-        /**
-         * Invoked when the "expand immediate" attribute changes.
-         *
-         * An example of expanding immediately is when swiping down from the top with two fingers.
-         * Instead of going to QQS, we immediately expand to full QS.
-         *
-         * 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) {}
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java
deleted file mode 100644
index 6772384..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java
+++ /dev/null
@@ -1,30 +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;
-
-import com.android.systemui.dagger.SysUISingleton;
-
-import dagger.Binds;
-import dagger.Module;
-
-/** Provides a {@link NotifPanelEvents} in {@link SysUISingleton} scope. */
-@Module
-public abstract class NotifPanelEventsModule {
-    @Binds
-    abstract NotifPanelEvents bindPanelEvents(
-            NotificationPanelViewController.PanelEventsEmitter impl);
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index a49b7f0..b92cf5a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -33,7 +33,6 @@
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
-import static com.android.systemui.shade.NotificationPanelView.DEBUG;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPENING;
@@ -41,9 +40,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
-import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
 import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
-import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
 
@@ -53,21 +50,16 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Fragment;
 import android.app.StatusBarManager;
 import android.content.ContentResolver;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
-import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.ColorFilter;
 import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.graphics.Region;
-import android.graphics.drawable.Drawable;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Bundle;
@@ -79,7 +71,10 @@
 import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.transition.ChangeBounds;
+import android.transition.Transition;
 import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.transition.TransitionValues;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
@@ -101,7 +96,6 @@
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
-import androidx.annotation.Nullable;
 import androidx.constraintlayout.widget.ConstraintSet;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -124,18 +118,18 @@
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
 import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
 import com.android.systemui.DejankUtils;
+import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
-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;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -144,10 +138,12 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.media.KeyguardMediaController;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -171,19 +167,17 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.events.PrivacyDotViewController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
-import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -206,7 +200,6 @@
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
@@ -226,7 +219,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;
@@ -234,17 +226,15 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
 @CentralSurfacesComponent.CentralSurfacesScope
-public final class NotificationPanelViewController {
+public final class NotificationPanelViewController implements Dumpable {
 
     public static final String TAG = NotificationPanelView.class.getSimpleName();
     public static final float FLING_MAX_LENGTH_SECONDS = 0.6f;
@@ -254,29 +244,18 @@
     private static final boolean DEBUG_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
     private static final boolean SPEW_LOGCAT = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private static final boolean DEBUG_DRAWABLE = false;
-
     private static final VibrationEffect ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
             VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false);
-
-    /**
-     * The parallax amount of the quick settings translation when dragging down the panel
-     */
+    /** The parallax amount of the quick settings translation when dragging down the panel. */
     private static final float QS_PARALLAX_AMOUNT = 0.175f;
-
-    /**
-     * Fling expanding QS.
-     */
+    /** Fling expanding QS. */
     public static final int FLING_EXPAND = 0;
-
-    /**
-     * Fling collapsing QS, potentially stopping when QS becomes QQS.
-     */
+    /** Fling collapsing QS, potentially stopping when QS becomes QQS. */
     private static final int FLING_COLLAPSE = 1;
-
-    /**
-     * Fling until QS is completely hidden.
-     */
+    /** Fling until QS is completely hidden. */
     private static final int FLING_HIDE = 2;
+    /** The delay to reset the hint text when the hint animation is finished running. */
+    private static final int HINT_RESET_DELAY_MS = 1200;
     private static final long ANIMATION_DELAY_ICON_FADE_IN =
             ActivityLaunchAnimator.TIMINGS.getTotalDuration()
                     - CollapsedStatusBarFragment.FADE_IN_DURATION
@@ -289,6 +268,18 @@
      * when flinging. A low value will make it that most flings will reach the maximum overshoot.
      */
     private static final float FACTOR_OF_HIGH_VELOCITY_FOR_MAX_OVERSHOOT = 0.5f;
+    /**
+     * Maximum time before which we will expand the panel even for slow motions when getting a
+     * touch passed over from launcher.
+     */
+    private static final int MAX_TIME_TO_OPEN_WHEN_FLINGING_FROM_LAUNCHER = 300;
+    private static final int MAX_DOWN_EVENT_BUFFER_SIZE = 50;
+    private static final String COUNTER_PANEL_OPEN = "panel_open";
+    private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
+    private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
+    private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1);
+    private static final Rect EMPTY_RECT = new Rect();
+
     private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
     private final Resources mResources;
     private final KeyguardStateController mKeyguardStateController;
@@ -297,49 +288,24 @@
     private final LockscreenGestureLogger mLockscreenGestureLogger;
     private final SystemClock mSystemClock;
     private final ShadeLogger mShadeLog;
-
     private final DozeParameters mDozeParameters;
-    private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
-    private final Runnable mCollapseExpandAction = new CollapseExpandAction();
-    private final OnOverscrollTopChangedListener
-            mOnOverscrollTopChangedListener =
-            new OnOverscrollTopChangedListener();
-    private final OnEmptySpaceClickListener
-            mOnEmptySpaceClickListener =
-            new OnEmptySpaceClickListener();
-    private final MyOnHeadsUpChangedListener
-            mOnHeadsUpChangedListener =
-            new MyOnHeadsUpChangedListener();
-    private final HeightListener mHeightListener = new HeightListener();
+    private final Runnable mCollapseExpandAction = this::collapseOrExpand;
+    private final NsslOverscrollTopChangedListener mOnOverscrollTopChangedListener =
+            new NsslOverscrollTopChangedListener();
+    private final NotificationStackScrollLayout.OnEmptySpaceClickListener
+            mOnEmptySpaceClickListener = (x, y) -> onEmptySpaceClick();
+    private final ShadeHeadsUpChangedListener mOnHeadsUpChangedListener =
+            new ShadeHeadsUpChangedListener();
+    private final QS.HeightListener mHeightListener = this::onQsHeightChanged;
     private final ConfigurationListener mConfigurationListener = new ConfigurationListener();
     private final SettingsChangeObserver mSettingsChangeObserver;
-
-    @VisibleForTesting
-    final StatusBarStateListener mStatusBarStateListener =
-            new StatusBarStateListener();
+    private final StatusBarStateListener mStatusBarStateListener = new StatusBarStateListener();
     private final NotificationPanelView mView;
     private final VibratorHelper mVibratorHelper;
     private final MetricsLogger mMetricsLogger;
     private final ConfigurationController mConfigurationController;
     private final Provider<FlingAnimationUtils.Builder> mFlingAnimationUtilsBuilder;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
-    private final NotificationIconAreaController mNotificationIconAreaController;
-
-    /**
-     * Maximum time before which we will expand the panel even for slow motions when getting a
-     * touch passed over from launcher.
-     */
-    private static final int MAX_TIME_TO_OPEN_WHEN_FLINGING_FROM_LAUNCHER = 300;
-
-    private static final int MAX_DOWN_EVENT_BUFFER_SIZE = 50;
-
-    private static final String COUNTER_PANEL_OPEN = "panel_open";
-    private static final String COUNTER_PANEL_OPEN_QS = "panel_open_qs";
-    private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek";
-
-    private static final Rect M_DUMMY_DIRTY_RECT = new Rect(0, 0, 1, 1);
-    private static final Rect EMPTY_RECT = new Rect();
-
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final LayoutInflater mLayoutInflater;
     private final FeatureFlags mFeatureFlags;
@@ -359,15 +325,12 @@
     private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
     private final FragmentService mFragmentService;
     private final ScrimController mScrimController;
-    private final PrivacyDotViewController mPrivacyDotViewController;
     private final NotificationRemoteInputManager mRemoteInputManager;
-
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final ShadeTransitionController mShadeTransitionController;
     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;
@@ -379,6 +342,12 @@
     private final Interpolator mBounceInterpolator;
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
+    private final QS.ScrollListener mQsScrollListener = this::onQsPanelScrollChanged;
+    private final FalsingTapListener mFalsingTapListener = this::falsingAdditionalTapRequired;
+    private final FragmentListener mQsFragmentListener = new QsFragmentListener();
+    private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate();
+    private final NotificationGutsManager mGutsManager;
+
     private long mDownTime;
     private boolean mTouchSlopExceededBeforeDown;
     private boolean mIsLaunchAnimationRunning;
@@ -400,13 +369,11 @@
     private float mKeyguardNotificationTopPadding;
     /** Current max allowed keyguard notifications determined by measuring the panel. */
     private int mMaxAllowedKeyguardNotifications;
-
     private KeyguardQsUserSwitchController mKeyguardQsUserSwitchController;
     private KeyguardUserSwitcherController mKeyguardUserSwitcherController;
     private KeyguardStatusBarView mKeyguardStatusBar;
     private KeyguardStatusBarViewController mKeyguardStatusBarViewController;
-    @VisibleForTesting
-    QS mQs;
+    private QS mQs;
     private FrameLayout mQsFrame;
     private final QsFrameTranslateController mQsFrameTranslateController;
     private KeyguardStatusViewController mKeyguardStatusViewController;
@@ -419,18 +386,11 @@
     private float mQuickQsHeaderHeight;
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-
     private int mQsTrackingPointer;
     private VelocityTracker mQsVelocityTracker;
     private boolean mQsTracking;
-
-    /**
-     * If set, the ongoing touch gesture might both trigger the expansion in {@link
-     * NotificationPanelView} and
-     * the expansion for quick settings.
-     */
+    /** Whether the ongoing gesture might both trigger the expansion in both the view and QS. */
     private boolean mConflictingQsExpansionGesture;
-
     private boolean mPanelExpanded;
 
     /**
@@ -485,19 +445,15 @@
      * Used for split shade, two finger gesture as well as accessibility shortcut to QS.
      * It needs to be set when movement starts as it resets at the end of expansion/collapse.
      */
-    @VisibleForTesting
-    boolean mQsExpandImmediate;
+    private boolean mQsExpandImmediate;
     private boolean mTwoFingerQsExpandPossible;
     private String mHeaderDebugInfo;
-
     /**
      * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
      * need to take this into account in our panel height calculation.
      */
     private boolean mQsAnimatorExpand;
-    private boolean mIsLaunchTransitionFinished;
     private ValueAnimator mQsSizeChangeAnimator;
-
     private boolean mQsScrimEnabled = true;
     private boolean mQsTouchAboveFalsingThreshold;
     private int mQsFalsingThreshold;
@@ -511,43 +467,30 @@
     private boolean mCollapsedOnDown;
     private boolean mClosingWithAlphaFadeOut;
     private boolean mHeadsUpAnimatingAway;
-    private boolean mLaunchingAffordance;
     private final FalsingManager mFalsingManager;
     private final FalsingCollector mFalsingCollector;
 
-    private final Runnable mHeadsUpExistenceChangedRunnable = () -> {
-        setHeadsUpAnimatingAway(false);
-        updatePanelExpansionAndVisibility();
-    };
     private boolean mShowIconsWhenExpanded;
     private int mIndicationBottomPadding;
     private int mAmbientIndicationBottomPadding;
+    /** Whether the notifications are displayed full width (no margins on the side). */
     private boolean mIsFullWidth;
     private boolean mBlockingExpansionForCurrentTouch;
+     // Following variables maintain state of events when input focus transfer may occur.
+    private boolean mExpectingSynthesizedDown;
+    private boolean mLastEventSynthesizedDown;
 
-    /**
-     * Following variables maintain state of events when input focus transfer may occur.
-     */
-    private boolean mExpectingSynthesizedDown; // expecting to see synthesized DOWN event
-    private boolean mLastEventSynthesizedDown; // last event was synthesized DOWN event
-
-    /**
-     * Current dark amount that follows regular interpolation curve of animation.
-     */
+    /** Current dark amount that follows regular interpolation curve of animation. */
     private float mInterpolatedDarkAmount;
-
     /**
      * Dark amount that animates from 0 to 1 or vice-versa in linear manner, even if the
      * interpolation curve is different.
      */
     private float mLinearDarkAmount;
-
     private boolean mPulsing;
     private boolean mHideIconsDuringLaunchAnimation = true;
     private int mStackScrollerMeasuringPass;
-    /**
-     * Non-null if there's a heads-up notification that we're currently tracking the position of.
-     */
+    /** Non-null if a heads-up notification's position is being tracked. */
     @Nullable
     private ExpandableNotificationRow mTrackedHeadsUpNotification;
     private final ArrayList<Consumer<ExpandableNotificationRow>>
@@ -577,16 +520,19 @@
     private final CommandQueue mCommandQueue;
     private final UserManager mUserManager;
     private final MediaDataManager mMediaDataManager;
+    @PanelState
+    private int mCurrentPanelState = STATE_CLOSED;
     private final SysUiState mSysUiState;
-
     private final NotificationShadeDepthController mDepthController;
+    private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
 
-    private KeyguardIndicationController mKeyguardIndicationController;
+    private final KeyguardIndicationController mKeyguardIndicationController;
     private int mHeadsUpInset;
     private boolean mHeadsUpPinnedMode;
     private boolean mAllowExpandForSmallExpansion;
     private Runnable mExpandAfterLayoutRunnable;
+    private Runnable mHideExpandedRunnable;
 
     /**
      * The padding between the start of notifications and the qs boundary on the lockscreen.
@@ -594,94 +540,51 @@
      * qs boundary to be padded.
      */
     private int mLockscreenNotificationQSPadding;
-
     /**
      * The amount of progress we are currently in if we're transitioning to the full shade.
      * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
      * shade. This value can also go beyond 1.1 when we're overshooting!
      */
     private float mTransitioningToFullShadeProgress;
-
     /**
      * Position of the qs bottom during the full shade transition. This is needed as the toppadding
      * can change during state changes, which makes it much harder to do animations
      */
     private int mTransitionToFullShadeQSPosition;
-
-    /**
-     * Distance that the full shade transition takes in order for qs to fully transition to the
-     * shade.
-     */
+    /** Distance a full shade transition takes in order for qs to fully transition to the shade. */
     private int mDistanceForQSFullShadeTransition;
-
-    /**
-     * The translation amount for QS for the full shade transition
-     */
+    /** The translation amount for QS for the full shade transition. */
     private float mQsTranslationForFullShadeTransition;
 
-    /**
-     * The maximum overshoot allowed for the top padding for the full shade transition
-     */
+    /** The maximum overshoot allowed for the top padding for the full shade transition. */
     private int mMaxOverscrollAmountForPulse;
-
-    /**
-     * Should we animate the next bounds update
-     */
+    /** Should we animate the next bounds update. */
     private boolean mAnimateNextNotificationBounds;
-    /**
-     * The delay for the next bounds animation
-     */
+    /** The delay for the next bounds animation. */
     private long mNotificationBoundsAnimationDelay;
-
-    /**
-     * The duration of the notification bounds animation
-     */
+    /** The duration of the notification bounds animation. */
     private long mNotificationBoundsAnimationDuration;
 
-    /**
-     * Is this a collapse that started on the panel where we should allow the panel to intercept
-     */
+    /** Whether a collapse that started on the panel should allow the panel to intercept. */
     private boolean mIsPanelCollapseOnQQS;
-
     private boolean mAnimatingQS;
-
-    /**
-     * The end bounds of a clipping animation.
-     */
+    /** The end bounds of a clipping animation. */
     private final Rect mQsClippingAnimationEndBounds = new Rect();
-
-    /**
-     * The animator for the qs clipping bounds.
-     */
+    /** The animator for the qs clipping bounds. */
     private ValueAnimator mQsClippingAnimation = null;
-
-    /**
-     * Is the current animator resetting the qs translation.
-     */
+    /** Whether the current animator is resetting the qs translation. */
     private boolean mIsQsTranslationResetAnimator;
 
-    /**
-     * Is the current animator resetting the pulse expansion after a drag down
-     */
+    /** Whether the current animator is resetting the pulse expansion after a drag down. */
     private boolean mIsPulseExpansionResetAnimator;
-    private final Rect mKeyguardStatusAreaClipBounds = new Rect();
+    private final Rect mLastQsClipBounds = new Rect();
     private final Region mQsInterceptRegion = new Region();
-
-    /**
-     * The alpha of the views which only show on the keyguard but not in shade / shade locked
-     */
+    /** Alpha of the views which only show on the keyguard but not in shade / shade locked. */
     private float mKeyguardOnlyContentAlpha = 1.0f;
-
-    /**
-     * The translationY of the views which only show on the keyguard but in shade / shade locked.
-     */
+    /** Y translation of the views that only show on the keyguard but in shade / shade locked. */
     private int mKeyguardOnlyTransitionTranslationY = 0;
-
     private float mUdfpsMaxYBurnInOffset;
-
-    /**
-     * Are we currently in gesture navigation
-     */
+    /** Are we currently in gesture navigation. */
     private boolean mIsGestureNavigation;
     private int mOldLayoutDirection;
     private NotificationShelfController mNotificationShelfController;
@@ -689,10 +592,12 @@
     private int mScreenCornerRadius;
     private boolean mQSAnimatingHiddenFromCollapsed;
     private boolean mUseLargeScreenShadeHeader;
+    private boolean mEnableQsClipping;
 
     private int mQsClipTop;
     private int mQsClipBottom;
     private boolean mQsVisible;
+
     private final ContentResolver mContentResolver;
     private float mMinFraction;
 
@@ -711,56 +616,7 @@
 
     private final NotificationListContainer mNotificationListContainer;
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
-
     private final NPVCDownEventState.Buffer mLastDownEvents;
-
-    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable =
-            () -> mKeyguardBottomArea.setVisibility(View.GONE);
-
-    private final AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
-        @Override
-        public void onInitializeAccessibilityNodeInfo(View host,
-                AccessibilityNodeInfo info) {
-            super.onInitializeAccessibilityNodeInfo(host, info);
-            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
-            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
-        }
-
-        @Override
-        public boolean performAccessibilityAction(View host, int action, Bundle args) {
-            if (action
-                    == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()
-                    || action
-                    == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) {
-                mStatusBarKeyguardViewManager.showBouncer(true);
-                return true;
-            }
-            return super.performAccessibilityAction(host, action, args);
-        }
-    };
-
-    private final FalsingTapListener mFalsingTapListener = new FalsingTapListener() {
-        @Override
-        public void onAdditionalTapRequired() {
-            if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
-                mTapAgainViewController.show();
-            } else {
-                mKeyguardIndicationController.showTransientIndication(
-                        R.string.notification_tap_again);
-            }
-
-            if (!mStatusBarStateController.isDozing()) {
-                mVibratorHelper.vibrate(
-                        Process.myUid(),
-                        mView.getContext().getPackageName(),
-                        ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
-                        "falsing-additional-tap-required",
-                        TOUCH_VIBRATION_ATTRIBUTES);
-            }
-        }
-    };
-
-    private final CameraGestureHelper mCameraGestureHelper;
     private final KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     private final KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     private float mMinExpandHeight;
@@ -773,7 +629,6 @@
     private float mLastGesturedOverExpansion = -1;
     /** Whether the current animator is the spring back animation. */
     private boolean mIsSpringBackAnimation;
-    private boolean mInSplitShade;
     private float mHintDistance;
     private float mInitialOffsetOnTouch;
     private boolean mCollapsedAndHeadsUpOnDown;
@@ -809,8 +664,20 @@
     private boolean mGestureWaitForTouchSlop;
     private boolean mIgnoreXTouchSlop;
     private boolean mExpandLatencyTracking;
+
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
+    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable =
+            () -> mKeyguardBottomArea.setVisibility(View.GONE);
+    private final Runnable mHeadsUpExistenceChangedRunnable = () -> {
+        setHeadsUpAnimatingAway(false);
+        updatePanelExpansionAndVisibility();
+    };
+    private final Runnable mMaybeHideExpandedRunnable = () -> {
+        if (getExpansionFraction() == 0.0f) {
+            getView().post(mHideExpandedRunnable);
+        }
+    };
 
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
@@ -838,6 +705,7 @@
             ConversationNotificationManager conversationNotificationManager,
             MediaHierarchyManager mediaHierarchyManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            NotificationGutsManager gutsManager,
             NotificationsQSContainerController notificationsQSContainerController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
@@ -845,7 +713,6 @@
             KeyguardUserSwitcherComponent.Factory keyguardUserSwitcherComponentFactory,
             KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory,
             LockscreenShadeTransitionController lockscreenShadeTransitionController,
-            NotificationIconAreaController notificationIconAreaController,
             AuthController authController,
             ScrimController scrimController,
             UserManager userManager,
@@ -854,9 +721,9 @@
             AmbientState ambientState,
             LockIconViewController lockIconViewController,
             KeyguardMediaController keyguardMediaController,
-            PrivacyDotViewController privacyDotViewController,
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
+            NavigationBarController navigationBarController,
             FragmentService fragmentService,
             ContentResolver contentResolver,
             RecordingController recordingController,
@@ -871,15 +738,15 @@
             SysUiState sysUiState,
             Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            KeyguardIndicationController keyguardIndicationController,
             NotificationListContainer notificationListContainer,
-            PanelEventsEmitter panelEventsEmitter,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
             SystemClock systemClock,
-            CameraGestureHelper cameraGestureHelper,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
-            KeyguardBottomAreaInteractor keyguardBottomAreaInteractor) {
+            KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+            DumpManager dumpManager) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
             public void onKeyguardFadingAwayChanged() {
@@ -892,7 +759,7 @@
         mLockscreenGestureLogger = lockscreenGestureLogger;
         mShadeExpansionStateManager = shadeExpansionStateManager;
         mShadeLog = shadeLogger;
-        TouchHandler touchHandler = createTouchHandler();
+        mGutsManager = gutsManager;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -900,16 +767,16 @@
             }
 
             @Override
-            public void onViewDetachedFromWindow(View v) {
-            }
+            public void onViewDetachedFromWindow(View v) {}
         });
 
-        mView.addOnLayoutChangeListener(createLayoutChangeListener());
-        mView.setOnTouchListener(touchHandler);
-        mView.setOnConfigurationChangedListener(createOnConfigurationChangedListener());
+        mView.addOnLayoutChangeListener(new ShadeLayoutChangeListener());
+        mView.setOnTouchListener(createTouchHandler());
+        mView.setOnConfigurationChangedListener(config -> loadDimens());
 
         mResources = mView.getResources();
         mKeyguardStateController = keyguardStateController;
+        mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
         mNotificationShadeWindowController = notificationShadeWindowController;
         FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get();
@@ -942,7 +809,6 @@
         mInteractionJankMonitor = interactionJankMonitor;
         mSystemClock = systemClock;
         mKeyguardMediaController = keyguardMediaController;
-        mPrivacyDotViewController = privacyDotViewController;
         mMetricsLogger = metricsLogger;
         mConfigurationController = configurationController;
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
@@ -950,10 +816,10 @@
         mNotificationsQSContainerController = notificationsQSContainerController;
         mNotificationListContainer = notificationListContainer;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
+        mNavigationBarController = navigationBarController;
         mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
         mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
-        mNotificationIconAreaController = notificationIconAreaController;
         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
         mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory;
         mDepthController = notificationShadeDepthController;
@@ -984,7 +850,6 @@
         mMediaDataManager = mediaDataManager;
         mTapAgainViewController = tapAgainViewController;
         mSysUiState = sysUiState;
-        mPanelEventsEmitter = panelEventsEmitter;
         pulseExpansionHandler.setPulseExpandAbortListener(() -> {
             if (mQs != null) {
                 mQs.animateHeaderSlidingOut();
@@ -997,10 +862,7 @@
         mShadeTransitionController = shadeTransitionController;
         lockscreenShadeTransitionController.setNotificationPanelController(this);
         shadeTransitionController.setNotificationPanelViewController(this);
-        DynamicPrivacyControlListener
-                dynamicPrivacyControlListener =
-                new DynamicPrivacyControlListener();
-        dynamicPrivacyController.addListener(dynamicPrivacyControlListener);
+        dynamicPrivacyController.addListener(this::onDynamicPrivacyChanged);
 
         shadeExpansionStateManager.addStateListener(this::onPanelStateChanged);
 
@@ -1024,16 +886,18 @@
         mIsGestureNavigation = QuickStepContract.isGesturalMode(currentMode);
 
         mView.setBackgroundColor(Color.TRANSPARENT);
-        OnAttachStateChangeListener onAttachStateChangeListener = new OnAttachStateChangeListener();
+        ShadeAttachStateChangeListener
+                onAttachStateChangeListener = new ShadeAttachStateChangeListener();
         mView.addOnAttachStateChangeListener(onAttachStateChangeListener);
         if (mView.isAttachedToWindow()) {
             onAttachStateChangeListener.onViewAttachedToWindow(mView);
         }
 
-        mView.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener());
+        mView.setOnApplyWindowInsetsListener((v, insets) -> onApplyShadeWindowInsets(insets));
 
         if (DEBUG_DRAWABLE) {
-            mView.getOverlay().add(new DebugDrawable());
+            mView.getOverlay().add(new DebugDrawable(this, mView,
+                    mNotificationStackScrollLayoutController, mLockIconViewController));
         }
 
         mKeyguardUnfoldTransition = unfoldComponent.map(
@@ -1049,55 +913,66 @@
                 new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
                     @Override
                     public void onUnlockAnimationFinished() {
-                        // Make sure the clock is in the correct position after the unlock animation
-                        // so that it's not in the wrong place when we show the keyguard again.
-                        positionClockAndNotifications(true /* forceClockUpdate */);
+                        unlockAnimationFinished();
                     }
 
                     @Override
                     public void onUnlockAnimationStarted(
                             boolean playingCannedAnimation,
                             boolean isWakeAndUnlock,
-                            long unlockAnimationStartDelay,
+                            long startDelay,
                             long unlockAnimationDuration) {
-                        // Disable blurs while we're unlocking so that panel expansion does not
-                        // cause blurring. This will eventually be re-enabled by the panel view on
-                        // ACTION_UP, since the user's finger might still be down after a swipe to
-                        // unlock gesture, and we don't want that to cause blurring either.
-                        mDepthController.setBlursDisabledForUnlock(mTracking);
-
-                        if (playingCannedAnimation && !isWakeAndUnlock) {
-                            // Hide the panel so it's not in the way or the surface behind the
-                            // keyguard, which will be appearing. If we're wake and unlocking, the
-                            // lock screen is hidden instantly so should not be flung away.
-                            if (isTracking() || isFlinging()) {
-                                // Instant collpase the notification panel since the notification
-                                // panel is already in the middle animating
-                                onTrackingStopped(false);
-                                instantCollapse();
-                            } else {
-                                mView.animate()
-                                        .alpha(0f)
-                                        .setStartDelay(0)
-                                        // Translate up by 4%.
-                                        .translationY(mView.getHeight() * -0.04f)
-                                        // This start delay is to give us time to animate out before
-                                        // the launcher icons animation starts, so use that as our
-                                        // duration.
-                                        .setDuration(unlockAnimationStartDelay)
-                                        .setInterpolator(EMPHASIZED_ACCELERATE)
-                                        .withEndAction(() -> {
-                                            instantCollapse();
-                                            mView.setAlpha(1f);
-                                            mView.setTranslationY(0f);
-                                        })
-                                        .start();
-                            }
-                        }
+                        unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay);
                     }
                 });
-        mCameraGestureHelper = cameraGestureHelper;
         mKeyguardBottomAreaInteractor = keyguardBottomAreaInteractor;
+        dumpManager.registerDumpable(this);
+    }
+
+    private void unlockAnimationFinished() {
+        // Make sure the clock is in the correct position after the unlock animation
+        // so that it's not in the wrong place when we show the keyguard again.
+        positionClockAndNotifications(true /* forceClockUpdate */);
+    }
+
+    private void unlockAnimationStarted(
+            boolean playingCannedAnimation,
+            boolean isWakeAndUnlock,
+            long unlockAnimationStartDelay) {
+        // Disable blurs while we're unlocking so that panel expansion does not
+        // cause blurring. This will eventually be re-enabled by the panel view on
+        // ACTION_UP, since the user's finger might still be down after a swipe to
+        // unlock gesture, and we don't want that to cause blurring either.
+        mDepthController.setBlursDisabledForUnlock(mTracking);
+
+        if (playingCannedAnimation && !isWakeAndUnlock) {
+            // Hide the panel so it's not in the way or the surface behind the
+            // keyguard, which will be appearing. If we're wake and unlocking, the
+            // lock screen is hidden instantly so should not be flung away.
+            if (isTracking() || mIsFlinging) {
+                // Instant collapse the notification panel since the notification
+                // panel is already in the middle animating
+                onTrackingStopped(false);
+                instantCollapse();
+            } else {
+                mView.animate()
+                        .alpha(0f)
+                        .setStartDelay(0)
+                        // Translate up by 4%.
+                        .translationY(mView.getHeight() * -0.04f)
+                        // This start delay is to give us time to animate out before
+                        // the launcher icons animation starts, so use that as our
+                        // duration.
+                        .setDuration(unlockAnimationStartDelay)
+                        .setInterpolator(EMPHASIZED_ACCELERATE)
+                        .withEndAction(() -> {
+                            instantCollapse();
+                            mView.setAlpha(1f);
+                            mView.setTranslationY(0f);
+                        })
+                        .start();
+            }
+        }
     }
 
     @VisibleForTesting
@@ -1136,7 +1011,7 @@
                 R.id.notification_stack_scroller);
         mNotificationStackScrollLayoutController.attach(stackScrollLayout);
         mNotificationStackScrollLayoutController.setOnHeightChangedListener(
-                mOnHeightChangedListener);
+                new NsslHeightChangedListener());
         mNotificationStackScrollLayoutController.setOverscrollTopChangedListener(
                 mOnOverscrollTopChangedListener);
         mNotificationStackScrollLayoutController.setOnScrollListener(this::onNotificationScrolled);
@@ -1144,7 +1019,7 @@
         mNotificationStackScrollLayoutController.setOnEmptySpaceClickListener(
                 mOnEmptySpaceClickListener);
         addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        mKeyguardBottomArea = mView.findViewById(R.id.keyguard_bottom_area);
+        setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
 
         initBottomArea();
 
@@ -1191,7 +1066,6 @@
         mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
         mHintDistance = mResources.getDimension(R.dimen.hint_move_distance);
         mPanelFlingOvershootAmount = mResources.getDimension(R.dimen.panel_overshoot_amount);
-        mInSplitShade = mResources.getBoolean(R.bool.config_use_split_notification_shade);
         mFlingAnimationUtils = mFlingAnimationUtilsBuilder.get()
                 .setMaxLengthSeconds(0.4f).build();
         mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mView.getContext());
@@ -1257,11 +1131,6 @@
         }
     }
 
-    private void setCentralSurfaces(CentralSurfaces centralSurfaces) {
-        // TODO: this can be injected.
-        mCentralSurfaces = centralSurfaces;
-    }
-
     public void updateResources() {
         mSplitShadeNotificationsScrimMarginBottom =
                 mResources.getDimensionPixelSize(
@@ -1280,8 +1149,15 @@
 
         mLargeScreenShadeHeaderHeight =
                 mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
-        mQuickQsHeaderHeight = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight :
-                SystemBarUtils.getQuickQsOffsetHeight(mView.getContext());
+        // TODO: When the flag is eventually removed, it means that we have a single view that is
+        // the same height in QQS and in Large Screen (large_screen_shade_header_height). Eventually
+        // the concept of largeScreenHeader or quickQsHeader will disappear outside of the class
+        // that controls the view as the offset needs to be the same regardless.
+        if (mUseLargeScreenShadeHeader || mFeatureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)) {
+            mQuickQsHeaderHeight = mLargeScreenShadeHeaderHeight;
+        } else {
+            mQuickQsHeaderHeight = SystemBarUtils.getQuickQsOffsetHeight(mView.getContext());
+        }
         int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight :
                 mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top);
         mLargeScreenShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader);
@@ -1298,6 +1174,8 @@
 
         mSplitShadeFullTransitionDistance =
                 mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance);
+
+        mEnableQsClipping = mResources.getBoolean(R.bool.qs_enable_clipping);
     }
 
     private void onSplitShadeEnabledChanged() {
@@ -1345,7 +1223,7 @@
 
     @VisibleForTesting
     void reInflateViews() {
-        if (DEBUG_LOGCAT) Log.d(TAG, "reInflateViews");
+        debugLog("reInflateViews");
         // Re-inflate the status view group.
         KeyguardStatusView keyguardStatusView =
                 mNotificationContainerParent.findViewById(R.id.keyguard_status_view);
@@ -1391,7 +1269,7 @@
         int index = mView.indexOfChild(mKeyguardBottomArea);
         mView.removeView(mKeyguardBottomArea);
         KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
-        mKeyguardBottomArea = mKeyguardBottomAreaViewControllerProvider.get().getView();
+        setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView());
         mKeyguardBottomArea.initFrom(oldBottomArea);
         mView.addView(mKeyguardBottomArea, index);
         initBottomArea();
@@ -1424,12 +1302,21 @@
         mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
     }
 
+    @VisibleForTesting
+    void setQs(QS qs) {
+        mQs = qs;
+    }
+
     private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
         mKeyguardMediaController.attachSplitShadeContainer(container);
     }
 
     private void initBottomArea() {
-        mKeyguardBottomArea.init(mKeyguardBottomAreaViewModel, mFalsingManager);
+        mKeyguardBottomArea.init(
+                mKeyguardBottomAreaViewModel,
+                mFalsingManager,
+                mLockIconViewController
+        );
     }
 
     @VisibleForTesting
@@ -1437,6 +1324,11 @@
         mMaxAllowedKeyguardNotifications = maxAllowed;
     }
 
+    @VisibleForTesting
+    boolean isFlinging() {
+        return mIsFlinging;
+    }
+
     private void updateMaxDisplayedNotifications(boolean recompute) {
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
@@ -1460,8 +1352,8 @@
         return mHintAnimationRunning || mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
-    public void setKeyguardIndicationController(KeyguardIndicationController indicationController) {
-        mKeyguardIndicationController = indicationController;
+    private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
+        mKeyguardBottomArea = keyguardBottomArea;
         mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
     }
 
@@ -1621,6 +1513,10 @@
         updateClock();
     }
 
+    public KeyguardClockPositionAlgorithm.Result getClockPositionResult() {
+        return mClockPositionResult;
+    }
+
     @ClockSize
     private int computeDesiredClockSize() {
         if (mSplitShadeEnabled) {
@@ -1664,9 +1560,48 @@
                     // horizontally properly.
                     transition.excludeTarget(R.id.status_view_media_container, true);
                 }
+
                 transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
                 transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-                TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition);
+
+                boolean customClockAnimation =
+                            mKeyguardStatusViewController.getClockAnimations() != null
+                            && mKeyguardStatusViewController.getClockAnimations()
+                                    .getHasCustomPositionUpdatedAnimation();
+
+                if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) {
+                    // Find the clock, so we can exclude it from this transition.
+                    FrameLayout clockContainerView =
+                            mView.findViewById(R.id.lockscreen_clock_view_large);
+
+                    // The clock container can sometimes be null. If it is, just fall back to the
+                    // old animation rather than setting up the custom animations.
+                    if (clockContainerView == null || clockContainerView.getChildCount() == 0) {
+                        TransitionManager.beginDelayedTransition(
+                                mNotificationContainerParent, transition);
+                    } else {
+                        View clockView = clockContainerView.getChildAt(0);
+
+                        transition.excludeTarget(clockView, /* exclude= */ true);
+
+                        TransitionSet set = new TransitionSet();
+                        set.addTransition(transition);
+
+                        SplitShadeTransitionAdapter adapter =
+                                new SplitShadeTransitionAdapter(mKeyguardStatusViewController);
+
+                        // Use linear here, so the actual clock can pick its own interpolator.
+                        adapter.setInterpolator(Interpolators.LINEAR);
+                        adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                        adapter.addTarget(clockView);
+                        set.addTransition(adapter);
+
+                        TransitionManager.beginDelayedTransition(mNotificationContainerParent, set);
+                    }
+                } else {
+                    TransitionManager.beginDelayedTransition(
+                            mNotificationContainerParent, transition);
+                }
             }
 
             constraintSet.applyTo(mNotificationContainerParent);
@@ -1837,8 +1772,7 @@
     }
 
     public void resetViews(boolean animate) {
-        mIsLaunchTransitionFinished = false;
-        mCentralSurfaces.getGutsManager().closeAndSaveGuts(true /* leavebehind */, true /* force */,
+        mGutsManager.closeAndSaveGuts(true /* leavebehind */, true /* force */,
                 true /* controls */, -1 /* x */, -1 /* y */, true /* resetMenu */);
         if (animate && !isFullyCollapsed()) {
             animateCloseQs(true /* animateAway */);
@@ -1877,13 +1811,13 @@
             setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
-        if (DEBUG) this.logf("collapse: " + this);
+        debugLog("collapse: %s", this);
         if (canPanelBeCollapsed()) {
             cancelHeightAnimator();
             notifyExpandingStarted();
 
             // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
-            setIsClosing(true);
+            setClosing(true);
             if (delayed) {
                 mNextCollapseSpeedUpFactor = speedUpFactor;
                 this.mView.postDelayed(mFlingCollapseRunnable, 120);
@@ -1893,13 +1827,19 @@
         }
     }
 
-    private void setQsExpandImmediate(boolean expandImmediate) {
+    @VisibleForTesting
+    void setQsExpandImmediate(boolean expandImmediate) {
         if (expandImmediate != mQsExpandImmediate) {
             mQsExpandImmediate = expandImmediate;
-            mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+            mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate);
         }
     }
 
+    @VisibleForTesting
+    boolean isQsExpandImmediate() {
+        return mQsExpandImmediate;
+    }
+
     private void setShowShelfOnly(boolean shelfOnly) {
         mNotificationStackScrollLayoutController.setShouldShowShelfOnly(
                 shelfOnly && !mSplitShadeEnabled);
@@ -1907,7 +1847,11 @@
 
     public void closeQs() {
         cancelQsAnimation();
-        setQsExpansion(mQsMinExpansionHeight);
+        setQsExpansionHeight(mQsMinExpansionHeight);
+        // qsExpandImmediate is a safety latch in case we're calling closeQS while we're in the
+        // middle of animation - we need to make sure that value is always false when shade if
+        // fully collapsed or expanded
+        setQsExpandImmediate(false);
     }
 
     @VisibleForTesting
@@ -1945,7 +1889,7 @@
             }
             float height = mQsExpansionHeight;
             mQsExpansionAnimator.cancel();
-            setQsExpansion(height);
+            setQsExpansionHeight(height);
         }
         flingSettings(0 /* vel */, animateAway ? FLING_HIDE : FLING_COLLAPSE);
     }
@@ -1969,7 +1913,7 @@
             // case but currently motion in portrait looks worse than when using flingSettings.
             // TODO: make below function transitioning smoothly also in portrait with null target
             mLockscreenShadeTransitionController.goToLockedShade(
-                    /* expandedView= */null, /* needsQSAnimation= */false);
+                    /* expandedView= */null, /* needsQSAnimation= */true);
         } else if (isFullyCollapsed()) {
             expand(true /* animate */);
         } else {
@@ -1986,12 +1930,12 @@
         }
     }
 
-    public void fling(float vel, boolean expand) {
+    private void fling(float vel) {
         GestureRecorder gr = mCentralSurfaces.getGestureRecorder();
         if (gr != null) {
             gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
         }
-        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
+        fling(vel, true, 1.0f /* collapseSpeedUpFactor */, false);
     }
 
     @VisibleForTesting
@@ -2010,7 +1954,7 @@
         // we want to perform an overshoot animation when flinging open
         final boolean addOverscroll =
                 expand
-                        && !mInSplitShade // Split shade has its own overscroll logic
+                        && !mSplitShadeEnabled // Split shade has its own overscroll logic
                         && mStatusBarStateController.getState() != KEYGUARD
                         && mOverExpansion == 0.0f
                         && vel >= 0;
@@ -2078,7 +2022,7 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 if (shouldSpringBack && !mCancelled) {
-                    // After the shade is flinged open to an overscrolled state, spring back
+                    // After the shade is flung open to an overscrolled state, spring back
                     // the shade by reducing section padding to 0.
                     springBack();
                 } else {
@@ -2090,7 +2034,8 @@
         animator.start();
     }
 
-    private void onFlingEnd(boolean cancelled) {
+    @VisibleForTesting
+    void onFlingEnd(boolean cancelled) {
         mIsFlinging = false;
         // No overshoot when the animation ends
         setOverExpansionInternal(0, false /* isFromGesture */);
@@ -2107,7 +2052,7 @@
     }
 
     private boolean onQsIntercept(MotionEvent event) {
-        if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept");
+        debugLog("onQsIntercept");
         int pointerIndex = event.findPointerIndex(mQsTrackingPointer);
         if (pointerIndex < 0) {
             pointerIndex = 0;
@@ -2157,7 +2102,7 @@
                     // Already tracking because onOverscrolled was called. We need to update here
                     // so we don't stop for a frame until the next touch event gets handled in
                     // onTouchEvent.
-                    setQsExpansion(h + mInitialHeightOnTouch);
+                    setQsExpansionHeight(h + mInitialHeightOnTouch);
                     trackMovement(event);
                     return true;
                 } else {
@@ -2168,7 +2113,7 @@
                 if ((h > touchSlop || (h < -touchSlop && mQsExpanded))
                         && Math.abs(h) > Math.abs(x - mInitialTouchX)
                         && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
-                    if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion");
+                    debugLog("onQsIntercept - start tracking expansion");
                     mView.getParent().requestDisallowInterceptTouchEvent(true);
                     mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
                     mQsTracking = true;
@@ -2227,7 +2172,7 @@
     private void initDownStates(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
             mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
-            mDozingOnDown = isDozing();
+            mDozingOnDown = mDozing;
             mDownX = event.getX();
             mDownY = event.getY();
             mCollapsedOnDown = isFullyCollapsed();
@@ -2277,7 +2222,7 @@
         float vel = getCurrentQSVelocity();
         boolean expandsQs = flingExpandsQs(vel);
         if (expandsQs) {
-            if (mFalsingManager.isUnlockingDisabled() || isFalseTouch(QUICK_SETTINGS)) {
+            if (mFalsingManager.isUnlockingDisabled() || isFalseTouch()) {
                 expandsQs = false;
             } else {
                 logQsSwipeDown(y);
@@ -2316,9 +2261,9 @@
         }
     }
 
-    private boolean isFalseTouch(@Classifier.InteractionType int interactionType) {
+    private boolean isFalseTouch() {
         if (mFalsingManager.isClassifierEnabled()) {
-            return mFalsingManager.isFalseTouch(interactionType);
+            return mFalsingManager.isFalseTouch(Classifier.QUICK_SETTINGS);
         }
         return !mQsTouchAboveFalsingThreshold;
     }
@@ -2444,7 +2389,7 @@
     private void handleQsDown(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN && shouldQuickSettingsIntercept(
                 event.getX(), event.getY(), -1)) {
-            if (DEBUG_LOGCAT) Log.d(TAG, "handleQsDown");
+            debugLog("handleQsDown");
             mFalsingCollector.onQsDown();
             mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled");
             mQsTracking = true;
@@ -2458,9 +2403,7 @@
         }
     }
 
-    /**
-     * Input focus transfer is about to happen.
-     */
+    /** Input focus transfer is about to happen. */
     public void startWaitingForOpenPanelGesture() {
         if (!isFullyCollapsed()) {
             return;
@@ -2492,7 +2435,7 @@
             } else {
                 // Window never will receive touch events that typically trigger haptic on open.
                 maybeVibrateOnOpening(false /* openingWithTouch */);
-                fling(velocity > 1f ? 1000f * velocity : 0, true /* expand */);
+                fling(velocity > 1f ? 1000f * velocity : 0  /* expand */);
             }
             onTrackingStopped(false);
         }
@@ -2566,9 +2509,9 @@
                 break;
 
             case MotionEvent.ACTION_MOVE:
-                if (DEBUG_LOGCAT) Log.d(TAG, "onQSTouch move");
+                debugLog("onQSTouch move");
                 mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion");
-                setQsExpansion(h + mInitialHeightOnTouch);
+                setQsExpansionHeight(h + mInitialHeightOnTouch);
                 if (h >= getFalsingThreshold()) {
                     mQsTouchAboveFalsingThreshold = true;
                 }
@@ -2615,14 +2558,14 @@
 
         // Reset scroll position and apply that position to the expanded height.
         float height = mQsExpansionHeight;
-        setQsExpansion(height);
+        setQsExpansionHeight(height);
         updateExpandedHeightToMaxHeight();
         mNotificationStackScrollLayoutController.checkSnoozeLeavebehind();
 
         // 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);
         }
     }
 
@@ -2633,12 +2576,19 @@
             mQsExpanded = expanded;
             updateQsState();
             updateExpandedHeightToMaxHeight();
-            mFalsingCollector.setQsExpanded(expanded);
-            mCentralSurfaces.setQsExpanded(expanded);
-            mNotificationsQSContainerController.setQsExpanded(expanded);
-            mPulseExpansionHandler.setQsExpanded(expanded);
-            mKeyguardBypassController.setQSExpanded(expanded);
-            mPrivacyDotViewController.setQsExpanded(expanded);
+            setStatusAccessibilityImportance(expanded
+                    ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                    : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            updateSystemUiStateFlags();
+            NavigationBarView navigationBarView =
+                    mNavigationBarController.getNavigationBarView(mDisplayId);
+            if (navigationBarView != null) {
+                navigationBarView.onStatusBarPanelStateChanged();
+            }
+            mShadeExpansionStateManager.onQsExpansionChanged(expanded);
+            mShadeLog.logQsExpansionChanged("QS Expansion Changed.", expanded,
+                    mQsMinExpansionHeight, mQsMaxExpansionHeight, mStackScrollerOverscrolling,
+                    mDozing, mQsAnimatorExpand, mAnimatingQS);
         }
     }
 
@@ -2683,7 +2633,7 @@
         mQs.setExpanded(mQsExpanded);
     }
 
-    void setQsExpansion(float height) {
+    void setQsExpansionHeight(float height) {
         height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
         mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
         boolean qsAnimatingAway = !mQsAnimatorExpand && mAnimatingQS;
@@ -2793,8 +2743,10 @@
      * as well based on the bounds of the shade and QS state.
      */
     private void setQSClippingBounds() {
-        final int qsPanelBottomY = calculateQsBottomPosition(computeQsExpansionFraction());
-        final boolean qsVisible = (computeQsExpansionFraction() > 0 || qsPanelBottomY > 0);
+        float qsExpansionFraction = computeQsExpansionFraction();
+        final int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
+        final boolean qsVisible = (qsExpansionFraction > 0 || qsPanelBottomY > 0);
+        checkCorrectScrimVisibility(qsExpansionFraction);
 
         int top = calculateTopQsClippingBound(qsPanelBottomY);
         int bottom = calculateBottomQsClippingBound(top);
@@ -2805,6 +2757,19 @@
         applyQSClippingBounds(left, top, right, bottom, qsVisible);
     }
 
+    private void checkCorrectScrimVisibility(float expansionFraction) {
+        // issues with scrims visible on keyguard occur only in split shade
+        if (mSplitShadeEnabled) {
+            boolean keyguardViewsVisible = mBarState == KEYGUARD && mKeyguardOnlyContentAlpha == 1;
+            // expansionFraction == 1 means scrims are fully visible as their size/visibility depend
+            // on QS expansion
+            if (expansionFraction == 1 && keyguardViewsVisible) {
+                Log.wtf(TAG,
+                        "Incorrect state, scrim is visible at the same time when clock is visible");
+            }
+        }
+    }
+
     private int calculateTopQsClippingBound(int qsPanelBottomY) {
         int top;
         if (mSplitShadeEnabled) {
@@ -2850,7 +2815,7 @@
     }
 
     private int calculateLeftQsClippingBound() {
-        if (isFullWidth()) {
+        if (mIsFullWidth) {
             // left bounds can ignore insets, it should always reach the edge of the screen
             return 0;
         } else {
@@ -2859,7 +2824,7 @@
     }
 
     private int calculateRightQsClippingBound() {
-        if (isFullWidth()) {
+        if (mIsFullWidth) {
             return getView().getRight() + mDisplayRightInset;
         } else {
             return mNotificationStackScrollLayoutController.getRight();
@@ -2875,7 +2840,7 @@
      */
     private void applyQSClippingBounds(int left, int top, int right, int bottom,
             boolean qsVisible) {
-        if (!mAnimateNextNotificationBounds || mKeyguardStatusAreaClipBounds.isEmpty()) {
+        if (!mAnimateNextNotificationBounds || mLastQsClipBounds.isEmpty()) {
             if (mQsClippingAnimation != null) {
                 // update the end position of the animator
                 mQsClippingAnimationEndBounds.set(left, top, right, bottom);
@@ -2884,10 +2849,10 @@
             }
         } else {
             mQsClippingAnimationEndBounds.set(left, top, right, bottom);
-            final int startLeft = mKeyguardStatusAreaClipBounds.left;
-            final int startTop = mKeyguardStatusAreaClipBounds.top;
-            final int startRight = mKeyguardStatusAreaClipBounds.right;
-            final int startBottom = mKeyguardStatusAreaClipBounds.bottom;
+            final int startLeft = mLastQsClipBounds.left;
+            final int startTop = mLastQsClipBounds.top;
+            final int startRight = mLastQsClipBounds.right;
+            final int startBottom = mLastQsClipBounds.bottom;
             if (mQsClippingAnimation != null) {
                 mQsClippingAnimation.cancel();
             }
@@ -2924,12 +2889,10 @@
 
     private void applyQSClippingImmediately(int left, int top, int right, int bottom,
             boolean qsVisible) {
-        // Fancy clipping for quick settings
         int radius = mScrimCornerRadius;
         boolean clipStatusView = false;
-        if (isFullWidth()) {
-            // The padding on this area is large enough that we can use a cheaper clipping strategy
-            mKeyguardStatusAreaClipBounds.set(left, top, right, bottom);
+        mLastQsClipBounds.set(left, top, right, bottom);
+        if (mIsFullWidth) {
             clipStatusView = qsVisible;
             float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius;
             radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius,
@@ -2952,8 +2915,10 @@
             mQsTranslationForFullShadeTransition = qsTranslation;
             updateQsFrameTranslation();
             float currentTranslation = mQsFrame.getTranslationY();
-            mQsClipTop = (int) (top - currentTranslation - mQsFrame.getTop());
-            mQsClipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop());
+            mQsClipTop = mEnableQsClipping
+                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
+            mQsClipBottom = mEnableQsClipping
+                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
             mQsVisible = qsVisible;
             mQs.setQsVisible(mQsVisible);
             mQs.setFancyClipping(
@@ -2962,8 +2927,8 @@
                     radius,
                     qsVisible && !mSplitShadeEnabled);
         }
-        mKeyguardStatusViewController.setClipBounds(
-                clipStatusView ? mKeyguardStatusAreaClipBounds : null);
+        // The padding on this area is large enough that we can use a cheaper clipping strategy
+        mKeyguardStatusViewController.setClipBounds(clipStatusView ? mLastQsClipBounds : null);
         if (!qsVisible && mSplitShadeEnabled) {
             // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
             // be visible, otherwise you can see the bounds once swiping up to see bouncer
@@ -2988,11 +2953,23 @@
         // relative to NotificationStackScrollLayout
         int nsslLeft = left - mNotificationStackScrollLayoutController.getLeft();
         int nsslRight = right - mNotificationStackScrollLayoutController.getLeft();
-        int nsslTop = top - mNotificationStackScrollLayoutController.getTop();
+        int nsslTop = getNotificationsClippingTopBounds(top);
         int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
         int bottomRadius = mSplitShadeEnabled ? radius : 0;
+        int topRadius = mSplitShadeEnabled && mExpandingFromHeadsUp ? 0 : radius;
         mNotificationStackScrollLayoutController.setRoundedClippingBounds(
-                nsslLeft, nsslTop, nsslRight, nsslBottom, radius, bottomRadius);
+                nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+    }
+
+    private int getNotificationsClippingTopBounds(int qsTop) {
+        if (mSplitShadeEnabled && mExpandingFromHeadsUp) {
+            // in split shade nssl has extra top margin so clipping at top 0 is not enough, we need
+            // to set top clipping bound to negative value to allow HUN to go up to the top edge of
+            // the screen without clipping.
+            return -mAmbientState.getStackTopMargin();
+        } else {
+            return qsTop - mNotificationStackScrollLayoutController.getTop();
+        }
     }
 
     private float getQSEdgePosition() {
@@ -3029,7 +3006,7 @@
         }
     }
 
-    private float calculateNotificationsTopPadding() {
+    float calculateNotificationsTopPadding() {
         if (mSplitShadeEnabled) {
             return mKeyguardShowing ? getKeyguardNotificationStaticPadding() : 0;
         }
@@ -3063,10 +3040,19 @@
         }
     }
 
-    /**
-     * @return the topPadding of notifications when on keyguard not respecting quick settings
-     * expansion
-     */
+    public boolean getKeyguardShowing() {
+        return mKeyguardShowing;
+    }
+
+    public float getKeyguardNotificationTopPadding() {
+        return mKeyguardNotificationTopPadding;
+    }
+
+    public float getKeyguardNotificationBottomPadding() {
+        return mKeyguardNotificationBottomPadding;
+    }
+
+    /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */
     private int getKeyguardNotificationStaticPadding() {
         if (!mKeyguardShowing) {
             return 0;
@@ -3098,17 +3084,18 @@
      * shade. 0.0f means we're not transitioning yet.
      */
     public void setTransitionToFullShadeAmount(float pxAmount, boolean animate, long delay) {
-        if (animate && isFullWidth()) {
+        if (animate && mIsFullWidth) {
             animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
                     delay);
             mIsQsTranslationResetAnimator = mQsTranslationForFullShadeTransition > 0.0f;
         }
-
-        if (mSplitShadeEnabled) {
-            updateQsExpansionForLockscreenToShadeTransition(pxAmount);
-        }
         float endPosition = 0;
         if (pxAmount > 0.0f) {
+            if (mSplitShadeEnabled) {
+                float qsHeight = MathUtils.lerp(mQsMinExpansionHeight, mQsMaxExpansionHeight,
+                        mLockscreenShadeTransitionController.getQSDragProgress());
+                setQsExpansionHeight(qsHeight);
+            }
             if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
                     && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
                 // No notifications are visible, let's animate to the height of qs instead
@@ -3146,22 +3133,7 @@
         updateQsExpansion();
     }
 
-    private void updateQsExpansionForLockscreenToShadeTransition(float pxAmount) {
-        float qsExpansion = 0;
-        if (pxAmount > 0.0f) {
-            qsExpansion = MathUtils.lerp(mQsMinExpansionHeight, mQsMaxExpansionHeight,
-                    mLockscreenShadeTransitionController.getQSDragProgress());
-        }
-        // SHADE_LOCKED means transition is over and we don't want further updates
-        if (mBarState != SHADE_LOCKED) {
-            setQsExpansion(qsExpansion);
-        }
-    }
-
-    /**
-     * Notify the panel that the pulse expansion has finished and that we're going to the full
-     * shade
-     */
+    /** Called when pulse expansion has finished and this is going to the full shade. */
     public void onPulseExpansionFinished() {
         animateNextNotificationBounds(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 0);
         mIsPulseExpansionResetAnimator = true;
@@ -3216,9 +3188,7 @@
         }
     }
 
-    /**
-     * @see #flingSettings(float, int, Runnable, boolean)
-     */
+    /** @see #flingSettings(float, int, Runnable, boolean) */
     public void flingSettings(float vel, int type) {
         flingSettings(vel, type, null /* onFinishRunnable */, false /* isClick */);
     }
@@ -3275,7 +3245,7 @@
             animator.setDuration(350);
         }
         animator.addUpdateListener(
-                animation -> setQsExpansion((Float) animation.getAnimatedValue()));
+                animation -> setQsExpansionHeight((Float) animation.getAnimatedValue()));
         animator.addListener(new AnimatorListenerAdapter() {
             private boolean mIsCanceled;
 
@@ -3351,7 +3321,7 @@
         return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
     }
 
-    public int getMaxPanelHeight() {
+    int getMaxPanelHeight() {
         int min = mStatusBarMinHeight;
         if (!(mBarState == KEYGUARD)
                 && mNotificationStackScrollLayoutController.getNotGoneChildCount() == 0) {
@@ -3385,19 +3355,35 @@
     }
 
     private void onHeightUpdated(float expandedHeight) {
+        if (expandedHeight <= 0) {
+            mShadeLog.logExpansionChanged("onHeightUpdated: fully collapsed.",
+                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+        } else if (isFullyExpanded()) {
+            mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.",
+                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+        }
         if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) {
             // Updating the clock position will set the top padding which might
             // trigger a new panel height and re-position the clock.
             // This is a circular dependency and should be avoided, otherwise we'll have
             // a stack overflow.
             if (mStackScrollerMeasuringPass > 2) {
-                if (DEBUG_LOGCAT) Log.d(TAG, "Unstable notification panel height. Aborting.");
+                debugLog("Unstable notification panel height. Aborting.");
             } else {
                 positionClockAndNotifications();
             }
         }
-        if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
-                && !mQsExpansionFromOverscroll)) {
+        // Below is true when QS are expanded and we swipe up from the same bottom of panel to
+        // close the whole shade with one motion. Also this will be always true when closing
+        // split shade as there QS are always expanded so every collapsing motion is motion from
+        // expanded QS to closed panel
+        boolean collapsingShadeFromExpandedQs = mQsExpanded && !mQsTracking
+                && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll;
+        boolean goingBetweenClosedShadeAndExpandedQs =
+                mQsExpandImmediate || collapsingShadeFromExpandedQs;
+        // we don't want to update QS expansion when HUN is visible because then the whole shade is
+        // initially hidden, even though it has non-zero height
+        if (goingBetweenClosedShadeAndExpandedQs && !mHeadsUpManager.isTrackingHeadsUp()) {
             float qsExpansionFraction;
             if (mSplitShadeEnabled) {
                 qsExpansionFraction = 1;
@@ -3416,7 +3402,7 @@
             }
             float targetHeight = mQsMinExpansionHeight
                     + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
-            setQsExpansion(targetHeight);
+            setQsExpansionHeight(targetHeight);
         }
         updateExpandedHeight(expandedHeight);
         updateHeader();
@@ -3432,11 +3418,7 @@
         boolean isExpanded = !isFullyCollapsed() || mExpectingSynthesizedDown;
         if (mPanelExpanded != isExpanded) {
             mPanelExpanded = isExpanded;
-
-            mHeadsUpManager.setIsPanelExpanded(isExpanded);
-            mStatusBarTouchableRegionManager.setPanelExpanded(isExpanded);
-            mCentralSurfaces.setPanelExpanded(isExpanded);
-
+            mShadeExpansionStateManager.onShadeExpansionFullyChanged(isExpanded);
             if (!isExpanded && mQs != null && mQs.isCustomizing()) {
                 mQs.closeCustomizer();
             }
@@ -3460,7 +3442,7 @@
         }
     }
 
-    private int calculatePanelHeightQsExpanded() {
+    int calculatePanelHeightQsExpanded() {
         float
                 notificationHeight =
                 mNotificationStackScrollLayoutController.getHeight()
@@ -3518,9 +3500,7 @@
         return alpha;
     }
 
-    /**
-     * Hides the header when notifications are colliding with it.
-     */
+    /** Hides the header when notifications are colliding with it. */
     private void updateHeader() {
         if (mBarState == KEYGUARD) {
             mKeyguardStatusBarViewController.updateViewState();
@@ -3663,7 +3643,7 @@
                                 if (mAnimateAfterExpanding) {
                                     notifyExpandingStarted();
                                     beginJankMonitoring();
-                                    fling(0, true /* expand */);
+                                    fling(0  /* expand */);
                                 } else {
                                     setExpandedFraction(1f);
                                 }
@@ -3678,6 +3658,11 @@
         setListening(true);
     }
 
+    @VisibleForTesting
+    void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
+        mTouchSlopExceeded = isTouchSlopExceeded;
+    }
+
     public void setOverExpansion(float overExpansion) {
         if (overExpansion == mOverExpansion) {
             return;
@@ -3695,6 +3680,24 @@
 
     }
 
+    private void falsingAdditionalTapRequired() {
+        if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
+            mTapAgainViewController.show();
+        } else {
+            mKeyguardIndicationController.showTransientIndication(
+                    R.string.notification_tap_again);
+        }
+
+        if (!mStatusBarStateController.isDozing()) {
+            mVibratorHelper.vibrate(
+                    Process.myUid(),
+                    mView.getContext().getPackageName(),
+                    ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
+                    "falsing-additional-tap-required",
+                    TOUCH_VIBRATION_ATTRIBUTES);
+        }
+    }
+
     private void onTrackingStarted() {
         mFalsingCollector.onTrackingStarted(!mKeyguardStateController.canDismissLockScreen());
         endClosing();
@@ -3714,7 +3717,6 @@
     private void onTrackingStopped(boolean expand) {
         mFalsingCollector.onTrackingStopped();
         mTracking = false;
-        mCentralSurfaces.onTrackingStopped(expand);
         updatePanelExpansionAndVisibility();
         if (expand) {
             mNotificationStackScrollLayoutController.setOverScrollAmount(0.0f, true /* onTop */,
@@ -3729,7 +3731,7 @@
 
     private void updateMaxHeadsUpTranslation() {
         mNotificationStackScrollLayoutController.setHeadsUpBoundaries(
-                getHeight(), mNavigationBarBottomHeight);
+                mView.getHeight(), mNavigationBarBottomHeight);
     }
 
     @VisibleForTesting
@@ -3757,14 +3759,16 @@
 
     @VisibleForTesting
     void onUnlockHintFinished() {
-        mCentralSurfaces.onHintFinished();
+        // Delay the reset a bit so the user can read the text.
+        mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS);
         mScrimController.setExpansionAffectsAlpha(true);
         mNotificationStackScrollLayoutController.setUnlockHintRunning(false);
     }
 
     @VisibleForTesting
     void onUnlockHintStarted() {
-        mCentralSurfaces.onUnlockHintStarted();
+        mFalsingCollector.onUnlockHintStarted();
+        mKeyguardIndicationController.showActionToUnlock();
         mScrimController.setExpansionAffectsAlpha(false);
         mNotificationStackScrollLayoutController.setUnlockHintRunning(true);
     }
@@ -3774,7 +3778,8 @@
                 || !isTracking());
     }
 
-    public int getMaxPanelTransitionDistance() {
+    @VisibleForTesting
+    int getMaxPanelTransitionDistance() {
         // Traditionally the value is based on the number of notifications. On split-shade, we want
         // the required distance to be a specific and constant value, to make sure the expansion
         // motion has the expected speed. We also only want this on non-lockscreen for now.
@@ -3817,24 +3822,21 @@
         mQs.closeCustomizer();
     }
 
-    public boolean isLaunchTransitionFinished() {
-        return mIsLaunchTransitionFinished;
-    }
-
     public void setIsLaunchAnimationRunning(boolean running) {
         boolean wasRunning = mIsLaunchAnimationRunning;
         mIsLaunchAnimationRunning = running;
         if (wasRunning != mIsLaunchAnimationRunning) {
-            mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
+            mShadeExpansionStateManager.notifyLaunchingActivityChanged(running);
         }
     }
 
-    private void setIsClosing(boolean isClosing) {
-        boolean wasClosing = isClosing();
-        mClosing = isClosing;
-        if (wasClosing != isClosing) {
-            mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
+    @VisibleForTesting
+    void setClosing(boolean isClosing) {
+        if (mClosing != isClosing) {
+            mClosing = isClosing;
+            mShadeExpansionStateManager.notifyPanelCollapsingChanged(isClosing);
         }
+        mAmbientState.setIsClosing(isClosing);
     }
 
     private void updateDozingVisibilities(boolean animate) {
@@ -3844,10 +3846,6 @@
         }
     }
 
-    public boolean isDozing() {
-        return mDozing;
-    }
-
     public void setQsScrimEnabled(boolean qsScrimEnabled) {
         boolean changed = mQsScrimEnabled != qsScrimEnabled;
         mQsScrimEnabled = qsScrimEnabled;
@@ -3860,16 +3858,20 @@
         mKeyguardStatusViewController.dozeTimeTick();
     }
 
-    private boolean onMiddleClicked() {
+    private void onMiddleClicked() {
         switch (mBarState) {
             case KEYGUARD:
                 if (!mDozingOnDown) {
-                    if (mUpdateMonitor.isFaceEnrolled()
-                            && !mUpdateMonitor.isFaceDetectionRunning()
-                            && !mUpdateMonitor.getUserCanSkipBouncer(
-                            KeyguardUpdateMonitor.getCurrentUser())) {
-                        mUpdateMonitor.requestFaceAuth(true,
-                                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+                    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(
+                            FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+
+                    if (didFaceAuthRun) {
+                        mUpdateMonitor.requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                                "lockScreenEmptySpaceTap");
                     } else {
                         mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
                                 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
@@ -3877,20 +3879,13 @@
                                 .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
                         startUnlockHintAnimation();
                     }
-                    if (mUpdateMonitor.isFaceEnrolled()) {
-                        mUpdateMonitor.requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
-                                "lockScreenEmptySpaceTap");
-                    }
                 }
-                return true;
+                break;
             case StatusBarState.SHADE_LOCKED:
                 if (!mQsExpanded) {
                     mStatusBarStateController.setState(KEYGUARD);
                 }
-                return true;
-            default:
-                return true;
+                break;
         }
     }
 
@@ -3926,6 +3921,7 @@
 
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         mHeadsUpManager = headsUpManager;
+        mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
         mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
                 mNotificationStackScrollLayoutController.getHeadsUpCallback(),
                 NotificationPanelViewController.this);
@@ -3964,17 +3960,9 @@
         updateStatusBarIcons();
     }
 
-    /**
-     * @return whether the notifications are displayed full width and don't have any margins on
-     * the side.
-     */
-    public boolean isFullWidth() {
-        return mIsFullWidth;
-    }
-
     private void updateStatusBarIcons() {
         boolean showIconsWhenExpanded =
-                (isPanelVisibleBecauseOfHeadsUp() || isFullWidth())
+                (isPanelVisibleBecauseOfHeadsUp() || mIsFullWidth)
                         && getExpandedHeight() < getOpeningHeight();
         if (showIconsWhenExpanded && isOnKeyguard()) {
             showIconsWhenExpanded = false;
@@ -3985,14 +3973,15 @@
         }
     }
 
+    public int getBarState() {
+        return mBarState;
+    }
+
     private boolean isOnKeyguard() {
         return mBarState == KEYGUARD;
     }
 
-    /**
-     * Called when heads-up notification is being dragged up or down to indicate what's the starting
-     * height for shade motion
-     */
+    /** Called when a HUN is dragged up or down to indicate the starting height for shade motion. */
     public void setHeadsUpDraggingStartingHeight(int startHeight) {
         mHeadsUpStartHeight = startHeight;
         float scrimMinFraction;
@@ -4033,42 +4022,6 @@
                 && mBarState == StatusBarState.SHADE;
     }
 
-    /** Launches the camera. */
-    public void launchCamera(int source) {
-        if (!isFullyCollapsed()) {
-            setLaunchingAffordance(true);
-        }
-
-        mCameraGestureHelper.launchCamera(source);
-    }
-
-    public void onAffordanceLaunchEnded() {
-        setLaunchingAffordance(false);
-    }
-
-    /**
-     * Set whether we are currently launching an affordance. This is currently only set when
-     * launched via a camera gesture.
-     */
-    private void setLaunchingAffordance(boolean launchingAffordance) {
-        mLaunchingAffordance = launchingAffordance;
-        mKeyguardBypassController.setLaunchingAffordance(launchingAffordance);
-    }
-
-    /**
-     * Return true when a bottom affordance is launching an occluded activity with a splash screen.
-     */
-    public boolean isLaunchingAffordanceWithPreview() {
-        return mLaunchingAffordance;
-    }
-
-    /**
-     * Whether the camera application can be launched for the camera launch gesture.
-     */
-    public boolean canCameraGestureBeLaunched() {
-        return mCameraGestureHelper.canCameraGestureBeLaunched(mBarState);
-    }
-
     public boolean hideStatusBarIconsWhenExpanded() {
         if (mIsLaunchAnimationRunning) {
             return mHideIconsDuringLaunchAnimation;
@@ -4077,22 +4030,19 @@
                 && mHeadsUpAppearanceController.shouldBeVisible()) {
             return false;
         }
-        return !isFullWidth() || !mShowIconsWhenExpanded;
+        return !mIsFullWidth || !mShowIconsWhenExpanded;
     }
 
-    public final QS.ScrollListener mScrollListener = new QS.ScrollListener() {
-        @Override
-        public void onQsPanelScrollChanged(int scrollY) {
-            mLargeScreenShadeHeaderController.setQsScrollY(scrollY);
-            if (scrollY > 0 && !mQsFullyExpanded) {
-                if (DEBUG_LOGCAT) Log.d(TAG, "Scrolling while not expanded. Forcing expand");
-                // If we are scrolling QS, we should be fully expanded.
-                expandWithQs();
-            }
+    private void onQsPanelScrollChanged(int scrollY) {
+        mLargeScreenShadeHeaderController.setQsScrollY(scrollY);
+        if (scrollY > 0 && !mQsFullyExpanded) {
+            debugLog("Scrolling while not expanded. Forcing expand");
+            // If we are scrolling QS, we should be fully expanded.
+            expandWithQs();
         }
-    };
+    }
 
-    private final FragmentListener mFragmentListener = new FragmentListener() {
+    private final class QsFragmentListener implements FragmentListener {
         @Override
         public void onFragmentViewCreated(String tag, Fragment fragment) {
             mQs = (QS) fragment;
@@ -4109,7 +4059,7 @@
                         final int height = bottom - top;
                         final int oldHeight = oldBottom - oldTop;
                         if (height != oldHeight) {
-                            mHeightListener.onQsHeightChanged();
+                            onQsHeightChanged();
                         }
                     });
             mQs.setCollapsedMediaVisibilityChangedListener((visible) -> {
@@ -4122,7 +4072,7 @@
             mLockscreenShadeTransitionController.setQS(mQs);
             mShadeTransitionController.setQs(mQs);
             mNotificationStackScrollLayoutController.setQsHeader((ViewGroup) mQs.getHeader());
-            mQs.setScrollListener(mScrollListener);
+            mQs.setScrollListener(mQsScrollListener);
             updateQsExpansion();
         }
 
@@ -4135,7 +4085,7 @@
                 mQs = null;
             }
         }
-    };
+    }
 
     private void animateNextNotificationBounds(long duration, long delay) {
         mAnimateNextNotificationBounds = true;
@@ -4158,8 +4108,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;
@@ -4225,13 +4175,7 @@
         mKeyguardStatusViewController.setStatusAccessibilityImportance(mode);
     }
 
-    /**
-     * TODO: this should be removed.
-     * It's not correct to pass this view forward because other classes will end up adding
-     * children to it. Theme will be out of sync.
-     *
-     * @return bottom area view
-     */
+    //TODO(b/254875405): this should be removed.
     public KeyguardBottomAreaView getKeyguardBottomAreaView() {
         return mKeyguardBottomArea;
     }
@@ -4260,11 +4204,8 @@
         mHeadsUpAppearanceController = headsUpAppearanceController;
     }
 
-    /**
-     * Starts the animation before we dismiss Keyguard, i.e. an disappearing animation on the
-     * security view of the bouncer.
-     */
-    public void onBouncerPreHideAnimation() {
+    /** Called before animating Keyguard dismissal, i.e. the animation dismissing the bouncer. */
+    public void startBouncerPreHideAnimation() {
         if (mKeyguardQsUserSwitchController != null) {
             mKeyguardQsUserSwitchController.setKeyguardQsUserSwitchVisibility(
                     mBarState,
@@ -4281,9 +4222,7 @@
         }
     }
 
-    /**
-     * Updates the views to the initial state for the fold to AOD animation
-     */
+    /** Updates the views to the initial state for the fold to AOD animation. */
     public void prepareFoldToAodAnimation() {
         // Force show AOD UI even if we are not locked
         showAodUi();
@@ -4299,40 +4238,37 @@
     /**
      * 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();
     }
 
-    /**
-     * Cancels fold to AOD transition and resets view state
-     */
+    /** Cancels fold to AOD transition and resets view state. */
     public void cancelFoldToAodAnimation() {
         cancelAnimation();
         resetAlpha();
@@ -4351,67 +4287,191 @@
         mBlockingExpansionForCurrentTouch = mTracking;
     }
 
+    @Override
     public void dump(PrintWriter pw, String[] args) {
-        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
-                        + " tracking=%s timeAnim=%s%s "
-                        + "touchDisabled=%s" + "]",
-                this.getClass().getSimpleName(), getExpandedHeight(), getMaxPanelHeight(),
-                mClosing ? "T" : "f", mTracking ? "T" : "f", mHeightAnimator,
-                ((mHeightAnimator != null && mHeightAnimator.isStarted()) ? " (started)" : ""),
-                mTouchDisabled ? "T" : "f"));
+        pw.println(TAG + ":");
         IndentingPrintWriter ipw = asIndenting(pw);
         ipw.increaseIndent();
+
+        ipw.print("mDownTime="); ipw.println(mDownTime);
+        ipw.print("mTouchSlopExceededBeforeDown="); ipw.println(mTouchSlopExceededBeforeDown);
+        ipw.print("mIsLaunchAnimationRunning="); ipw.println(mIsLaunchAnimationRunning);
+        ipw.print("mOverExpansion="); ipw.println(mOverExpansion);
+        ipw.print("mExpandedHeight="); ipw.println(mExpandedHeight);
+        ipw.print("mTracking="); ipw.println(mTracking);
+        ipw.print("mHintAnimationRunning="); ipw.println(mHintAnimationRunning);
+        ipw.print("mExpanding="); ipw.println(mExpanding);
+        ipw.print("mSplitShadeEnabled="); ipw.println(mSplitShadeEnabled);
+        ipw.print("mKeyguardNotificationBottomPadding=");
+        ipw.println(mKeyguardNotificationBottomPadding);
+        ipw.print("mKeyguardNotificationTopPadding="); ipw.println(mKeyguardNotificationTopPadding);
+        ipw.print("mMaxAllowedKeyguardNotifications=");
+        ipw.println(mMaxAllowedKeyguardNotifications);
+        ipw.print("mAnimateNextPositionUpdate="); ipw.println(mAnimateNextPositionUpdate);
+        ipw.print("mQuickQsHeaderHeight="); ipw.println(mQuickQsHeaderHeight);
+        ipw.print("mQsTrackingPointer="); ipw.println(mQsTrackingPointer);
+        ipw.print("mQsTracking="); ipw.println(mQsTracking);
+        ipw.print("mConflictingQsExpansionGesture="); ipw.println(mConflictingQsExpansionGesture);
+        ipw.print("mPanelExpanded="); ipw.println(mPanelExpanded);
+        ipw.print("mQsExpanded="); ipw.println(mQsExpanded);
+        ipw.print("mQsExpandedWhenExpandingStarted="); ipw.println(mQsExpandedWhenExpandingStarted);
+        ipw.print("mQsFullyExpanded="); ipw.println(mQsFullyExpanded);
+        ipw.print("mKeyguardShowing="); ipw.println(mKeyguardShowing);
+        ipw.print("mKeyguardQsUserSwitchEnabled="); ipw.println(mKeyguardQsUserSwitchEnabled);
+        ipw.print("mKeyguardUserSwitcherEnabled="); ipw.println(mKeyguardUserSwitcherEnabled);
+        ipw.print("mDozing="); ipw.println(mDozing);
+        ipw.print("mDozingOnDown="); ipw.println(mDozingOnDown);
+        ipw.print("mBouncerShowing="); ipw.println(mBouncerShowing);
+        ipw.print("mBarState="); ipw.println(mBarState);
+        ipw.print("mInitialHeightOnTouch="); ipw.println(mInitialHeightOnTouch);
+        ipw.print("mInitialTouchX="); ipw.println(mInitialTouchX);
+        ipw.print("mInitialTouchY="); ipw.println(mInitialTouchY);
+        ipw.print("mQsExpansionHeight="); ipw.println(mQsExpansionHeight);
+        ipw.print("mQsMinExpansionHeight="); ipw.println(mQsMinExpansionHeight);
+        ipw.print("mQsMaxExpansionHeight="); ipw.println(mQsMaxExpansionHeight);
+        ipw.print("mQsPeekHeight="); ipw.println(mQsPeekHeight);
+        ipw.print("mStackScrollerOverscrolling="); ipw.println(mStackScrollerOverscrolling);
+        ipw.print("mQsExpansionFromOverscroll="); ipw.println(mQsExpansionFromOverscroll);
+        ipw.print("mLastOverscroll="); ipw.println(mLastOverscroll);
+        ipw.print("mQsExpansionEnabledPolicy="); ipw.println(mQsExpansionEnabledPolicy);
+        ipw.print("mQsExpansionEnabledAmbient="); ipw.println(mQsExpansionEnabledAmbient);
+        ipw.print("mStatusBarMinHeight="); ipw.println(mStatusBarMinHeight);
+        ipw.print("mStatusBarHeaderHeightKeyguard="); ipw.println(mStatusBarHeaderHeightKeyguard);
+        ipw.print("mOverStretchAmount="); ipw.println(mOverStretchAmount);
+        ipw.print("mDownX="); ipw.println(mDownX);
+        ipw.print("mDownY="); ipw.println(mDownY);
+        ipw.print("mDisplayTopInset="); ipw.println(mDisplayTopInset);
+        ipw.print("mDisplayRightInset="); ipw.println(mDisplayRightInset);
+        ipw.print("mLargeScreenShadeHeaderHeight="); ipw.println(mLargeScreenShadeHeaderHeight);
+        ipw.print("mSplitShadeNotificationsScrimMarginBottom=");
+        ipw.println(mSplitShadeNotificationsScrimMarginBottom);
+        ipw.print("mIsExpanding="); ipw.println(mIsExpanding);
+        ipw.print("mQsExpandImmediate="); ipw.println(mQsExpandImmediate);
+        ipw.print("mTwoFingerQsExpandPossible="); ipw.println(mTwoFingerQsExpandPossible);
+        ipw.print("mHeaderDebugInfo="); ipw.println(mHeaderDebugInfo);
+        ipw.print("mQsAnimatorExpand="); ipw.println(mQsAnimatorExpand);
+        ipw.print("mQsScrimEnabled="); ipw.println(mQsScrimEnabled);
+        ipw.print("mQsTouchAboveFalsingThreshold="); ipw.println(mQsTouchAboveFalsingThreshold);
+        ipw.print("mQsFalsingThreshold="); ipw.println(mQsFalsingThreshold);
+        ipw.print("mHeadsUpStartHeight="); ipw.println(mHeadsUpStartHeight);
+        ipw.print("mListenForHeadsUp="); ipw.println(mListenForHeadsUp);
+        ipw.print("mNavigationBarBottomHeight="); ipw.println(mNavigationBarBottomHeight);
+        ipw.print("mExpandingFromHeadsUp="); ipw.println(mExpandingFromHeadsUp);
+        ipw.print("mCollapsedOnDown="); ipw.println(mCollapsedOnDown);
+        ipw.print("mClosingWithAlphaFadeOut="); ipw.println(mClosingWithAlphaFadeOut);
+        ipw.print("mHeadsUpAnimatingAway="); ipw.println(mHeadsUpAnimatingAway);
+        ipw.print("mShowIconsWhenExpanded="); ipw.println(mShowIconsWhenExpanded);
+        ipw.print("mIndicationBottomPadding="); ipw.println(mIndicationBottomPadding);
+        ipw.print("mAmbientIndicationBottomPadding="); ipw.println(mAmbientIndicationBottomPadding);
+        ipw.print("mIsFullWidth="); ipw.println(mIsFullWidth);
+        ipw.print("mBlockingExpansionForCurrentTouch=");
+        ipw.println(mBlockingExpansionForCurrentTouch);
+        ipw.print("mExpectingSynthesizedDown="); ipw.println(mExpectingSynthesizedDown);
+        ipw.print("mLastEventSynthesizedDown="); ipw.println(mLastEventSynthesizedDown);
+        ipw.print("mInterpolatedDarkAmount="); ipw.println(mInterpolatedDarkAmount);
+        ipw.print("mLinearDarkAmount="); ipw.println(mLinearDarkAmount);
+        ipw.print("mPulsing="); ipw.println(mPulsing);
+        ipw.print("mHideIconsDuringLaunchAnimation="); ipw.println(mHideIconsDuringLaunchAnimation);
+        ipw.print("mStackScrollerMeasuringPass="); ipw.println(mStackScrollerMeasuringPass);
+        ipw.print("mPanelAlpha="); ipw.println(mPanelAlpha);
+        ipw.print("mBottomAreaShadeAlpha="); ipw.println(mBottomAreaShadeAlpha);
+        ipw.print("mHeadsUpInset="); ipw.println(mHeadsUpInset);
+        ipw.print("mHeadsUpPinnedMode="); ipw.println(mHeadsUpPinnedMode);
+        ipw.print("mAllowExpandForSmallExpansion="); ipw.println(mAllowExpandForSmallExpansion);
+        ipw.print("mLockscreenNotificationQSPadding=");
+        ipw.println(mLockscreenNotificationQSPadding);
+        ipw.print("mTransitioningToFullShadeProgress=");
+        ipw.println(mTransitioningToFullShadeProgress);
+        ipw.print("mTransitionToFullShadeQSPosition=");
+        ipw.println(mTransitionToFullShadeQSPosition);
+        ipw.print("mDistanceForQSFullShadeTransition=");
+        ipw.println(mDistanceForQSFullShadeTransition);
+        ipw.print("mQsTranslationForFullShadeTransition=");
+        ipw.println(mQsTranslationForFullShadeTransition);
+        ipw.print("mMaxOverscrollAmountForPulse="); ipw.println(mMaxOverscrollAmountForPulse);
+        ipw.print("mAnimateNextNotificationBounds="); ipw.println(mAnimateNextNotificationBounds);
+        ipw.print("mNotificationBoundsAnimationDelay=");
+        ipw.println(mNotificationBoundsAnimationDelay);
+        ipw.print("mNotificationBoundsAnimationDuration=");
+        ipw.println(mNotificationBoundsAnimationDuration);
+        ipw.print("mIsPanelCollapseOnQQS="); ipw.println(mIsPanelCollapseOnQQS);
+        ipw.print("mAnimatingQS="); ipw.println(mAnimatingQS);
+        ipw.print("mIsQsTranslationResetAnimator="); ipw.println(mIsQsTranslationResetAnimator);
+        ipw.print("mIsPulseExpansionResetAnimator="); ipw.println(mIsPulseExpansionResetAnimator);
+        ipw.print("mKeyguardOnlyContentAlpha="); ipw.println(mKeyguardOnlyContentAlpha);
+        ipw.print("mKeyguardOnlyTransitionTranslationY=");
+        ipw.println(mKeyguardOnlyTransitionTranslationY);
+        ipw.print("mUdfpsMaxYBurnInOffset="); ipw.println(mUdfpsMaxYBurnInOffset);
+        ipw.print("mIsGestureNavigation="); ipw.println(mIsGestureNavigation);
+        ipw.print("mOldLayoutDirection="); ipw.println(mOldLayoutDirection);
+        ipw.print("mScrimCornerRadius="); ipw.println(mScrimCornerRadius);
+        ipw.print("mScreenCornerRadius="); ipw.println(mScreenCornerRadius);
+        ipw.print("mQSAnimatingHiddenFromCollapsed="); ipw.println(mQSAnimatingHiddenFromCollapsed);
+        ipw.print("mUseLargeScreenShadeHeader="); ipw.println(mUseLargeScreenShadeHeader);
+        ipw.print("mEnableQsClipping="); ipw.println(mEnableQsClipping);
+        ipw.print("mQsClipTop="); ipw.println(mQsClipTop);
+        ipw.print("mQsClipBottom="); ipw.println(mQsClipBottom);
+        ipw.print("mQsVisible="); ipw.println(mQsVisible);
+        ipw.print("mMinFraction="); ipw.println(mMinFraction);
+        ipw.print("mStatusViewCentered="); ipw.println(mStatusViewCentered);
+        ipw.print("mSplitShadeFullTransitionDistance=");
+        ipw.println(mSplitShadeFullTransitionDistance);
+        ipw.print("mSplitShadeScrimTransitionDistance=");
+        ipw.println(mSplitShadeScrimTransitionDistance);
+        ipw.print("mMinExpandHeight="); ipw.println(mMinExpandHeight);
+        ipw.print("mPanelUpdateWhenAnimatorEnds="); ipw.println(mPanelUpdateWhenAnimatorEnds);
+        ipw.print("mHasVibratedOnOpen="); ipw.println(mHasVibratedOnOpen);
+        ipw.print("mFixedDuration="); ipw.println(mFixedDuration);
+        ipw.print("mPanelFlingOvershootAmount="); ipw.println(mPanelFlingOvershootAmount);
+        ipw.print("mLastGesturedOverExpansion="); ipw.println(mLastGesturedOverExpansion);
+        ipw.print("mIsSpringBackAnimation="); ipw.println(mIsSpringBackAnimation);
+        ipw.print("mSplitShadeEnabled="); ipw.println(mSplitShadeEnabled);
+        ipw.print("mHintDistance="); ipw.println(mHintDistance);
+        ipw.print("mInitialOffsetOnTouch="); ipw.println(mInitialOffsetOnTouch);
+        ipw.print("mCollapsedAndHeadsUpOnDown="); ipw.println(mCollapsedAndHeadsUpOnDown);
+        ipw.print("mExpandedFraction="); ipw.println(mExpandedFraction);
+        ipw.print("mExpansionDragDownAmountPx="); ipw.println(mExpansionDragDownAmountPx);
+        ipw.print("mPanelClosedOnDown="); ipw.println(mPanelClosedOnDown);
+        ipw.print("mHasLayoutedSinceDown="); ipw.println(mHasLayoutedSinceDown);
+        ipw.print("mUpdateFlingVelocity="); ipw.println(mUpdateFlingVelocity);
+        ipw.print("mUpdateFlingOnLayout="); ipw.println(mUpdateFlingOnLayout);
+        ipw.print("mClosing="); ipw.println(mClosing);
+        ipw.print("mTouchSlopExceeded="); ipw.println(mTouchSlopExceeded);
+        ipw.print("mTrackingPointer="); ipw.println(mTrackingPointer);
+        ipw.print("mTouchSlop="); ipw.println(mTouchSlop);
+        ipw.print("mSlopMultiplier="); ipw.println(mSlopMultiplier);
+        ipw.print("mTouchAboveFalsingThreshold="); ipw.println(mTouchAboveFalsingThreshold);
+        ipw.print("mTouchStartedInEmptyArea="); ipw.println(mTouchStartedInEmptyArea);
+        ipw.print("mMotionAborted="); ipw.println(mMotionAborted);
+        ipw.print("mUpwardsWhenThresholdReached="); ipw.println(mUpwardsWhenThresholdReached);
+        ipw.print("mAnimatingOnDown="); ipw.println(mAnimatingOnDown);
+        ipw.print("mHandlingPointerUp="); ipw.println(mHandlingPointerUp);
+        ipw.print("mInstantExpanding="); ipw.println(mInstantExpanding);
+        ipw.print("mAnimateAfterExpanding="); ipw.println(mAnimateAfterExpanding);
+        ipw.print("mIsFlinging="); ipw.println(mIsFlinging);
+        ipw.print("mViewName="); ipw.println(mViewName);
+        ipw.print("mInitialExpandY="); ipw.println(mInitialExpandY);
+        ipw.print("mInitialExpandX="); ipw.println(mInitialExpandX);
+        ipw.print("mTouchDisabled="); ipw.println(mTouchDisabled);
+        ipw.print("mInitialTouchFromKeyguard="); ipw.println(mInitialTouchFromKeyguard);
+        ipw.print("mNextCollapseSpeedUpFactor="); ipw.println(mNextCollapseSpeedUpFactor);
+        ipw.print("mGestureWaitForTouchSlop="); ipw.println(mGestureWaitForTouchSlop);
+        ipw.print("mIgnoreXTouchSlop="); ipw.println(mIgnoreXTouchSlop);
+        ipw.print("mExpandLatencyTracking="); ipw.println(mExpandLatencyTracking);
+        ipw.print("mExpandLatencyTracking="); ipw.println(mExpandLatencyTracking);
         ipw.println("gestureExclusionRect:" + calculateGestureExclusionRect());
-        ipw.println("applyQSClippingImmediately: top(" + mQsClipTop + ") bottom(" + mQsClipBottom
-                + ")");
-        ipw.println("qsVisible:" + mQsVisible);
         new DumpsysTableLogger(
                 TAG,
                 NPVCDownEventState.TABLE_HEADERS,
                 mLastDownEvents.toList()
         ).printTableData(ipw);
-        ipw.decreaseIndent();
-        if (mKeyguardStatusBarViewController != null) {
-            mKeyguardStatusBarViewController.dump(pw, args);
-        }
     }
 
-    public boolean hasActiveClearableNotifications() {
-        return mNotificationStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL);
-    }
 
     public RemoteInputController.Delegate createRemoteInputDelegate() {
         return mNotificationStackScrollLayoutController.createDelegate();
     }
 
-    /**
-     * Updates the notification views' sections and status bar icons. This is
-     * triggered by the NotificationPresenter whenever there are changes to the underlying
-     * notification data being displayed. In the new notification pipeline, this is handled in
-     * {@link ShadeViewManager}.
-     */
-    public void updateNotificationViews() {
-        mNotificationStackScrollLayoutController.updateFooter();
-
-        mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList());
-    }
-
-    private List<ListEntry> createVisibleEntriesList() {
-        List<ListEntry> entries = new ArrayList<>(
-                mNotificationStackScrollLayoutController.getChildCount());
-        for (int i = 0; i < mNotificationStackScrollLayoutController.getChildCount(); i++) {
-            View view = mNotificationStackScrollLayoutController.getChildAt(i);
-            if (view instanceof ExpandableNotificationRow) {
-                entries.add(((ExpandableNotificationRow) view).getEntry());
-            }
-        }
-        return entries;
-    }
-
-    public void onUpdateRowStates() {
-        mNotificationStackScrollLayoutController.onUpdateRowStates();
-    }
-
     public boolean hasPulsingNotifications() {
         return mNotificationListContainer.hasPulsingNotifications();
     }
@@ -4428,16 +4488,6 @@
         mNotificationStackScrollLayoutController.runAfterAnimationFinished(r);
     }
 
-    private Runnable mHideExpandedRunnable;
-    private final Runnable mMaybeHideExpandedRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (getExpansionFraction() == 0.0f) {
-                mView.post(mHideExpandedRunnable);
-            }
-        }
-    };
-
     /**
      * Initialize objects instead of injecting to avoid circular dependencies.
      *
@@ -4447,7 +4497,9 @@
             CentralSurfaces centralSurfaces,
             Runnable hideExpandedRunnable,
             NotificationShelfController notificationShelfController) {
-        setCentralSurfaces(centralSurfaces);
+        // TODO(b/254859580): this can be injected.
+        mCentralSurfaces = centralSurfaces;
+
         mHideExpandedRunnable = hideExpandedRunnable;
         mNotificationStackScrollLayoutController.setShelfController(notificationShelfController);
         mNotificationShelfController = notificationShelfController;
@@ -4455,10 +4507,6 @@
         updateMaxDisplayedNotifications(true);
     }
 
-    public void setAlpha(float alpha) {
-        mView.setAlpha(alpha);
-    }
-
     public void resetTranslation() {
         mView.setTranslationX(0f);
     }
@@ -4477,32 +4525,24 @@
         ViewGroupFadeHelper.reset(mView);
     }
 
-    public void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
+    void addOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
         mView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
     }
 
-    public void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
+    void removeOnGlobalLayoutListener(ViewTreeObserver.OnGlobalLayoutListener listener) {
         mView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
     }
 
-    public MyOnHeadsUpChangedListener getOnHeadsUpChangedListener() {
-        return mOnHeadsUpChangedListener;
-    }
-
-    public int getHeight() {
-        return mView.getHeight();
-    }
-
     public void setHeaderDebugInfo(String text) {
         if (DEBUG_DRAWABLE) mHeaderDebugInfo = text;
     }
 
-    public void onThemeChanged() {
-        mConfigurationListener.onThemeChanged();
+    public String getHeaderDebugInfo() {
+        return mHeaderDebugInfo;
     }
 
-    private OnLayoutChangeListener createLayoutChangeListener() {
-        return new OnLayoutChangeListener();
+    public void onThemeChanged() {
+        mConfigurationListener.onThemeChanged();
     }
 
     @VisibleForTesting
@@ -4559,10 +4599,6 @@
                 }
             };
 
-    private OnConfigurationChangedListener createOnConfigurationChangedListener() {
-        return new OnConfigurationChangedListener();
-    }
-
     public NotificationStackScrollLayoutController getNotificationStackScrollLayoutController() {
         return mNotificationStackScrollLayoutController;
     }
@@ -4603,13 +4639,7 @@
         );
     }
 
-    private void unregisterSettingsChangeListener() {
-        mContentResolver.unregisterContentObserver(mSettingsChangeObserver);
-    }
-
-    /**
-     * Updates notification panel-specific flags on {@link SysUiState}.
-     */
+    /** Updates notification panel-specific flags on {@link SysUiState}. */
     public void updateSystemUiStateFlags() {
         if (SysUiState.DEBUG) {
             Log.d(TAG, "Updating panel sysui state flags: fullyExpanded="
@@ -4621,18 +4651,22 @@
                 .commitUpdate(mDisplayId);
     }
 
-    private void logf(String fmt, Object... args) {
-        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+    private void debugLog(String fmt, Object... args) {
+        if (DEBUG_LOGCAT) {
+            Log.d(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
+        }
     }
 
-    private void notifyExpandingStarted() {
+    @VisibleForTesting
+    void notifyExpandingStarted() {
         if (!mExpanding) {
             mExpanding = true;
             onExpandingStarted();
         }
     }
 
-    private void notifyExpandingFinished() {
+    @VisibleForTesting
+    void notifyExpandingFinished() {
         endClosing();
         if (mExpanding) {
             mExpanding = false;
@@ -4667,8 +4701,6 @@
 
     private void startOpening(MotionEvent event) {
         updatePanelExpansionAndVisibility();
-        // Reset at start so haptic can be triggered as soon as panel starts to open.
-        mHasVibratedOnOpen = false;
         //TODO: keyguard opens QS a different way; log that too?
 
         // Log the position of the swipe that opened the panel
@@ -4685,8 +4717,9 @@
     /**
      * 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) {
@@ -4732,6 +4765,7 @@
         mAmbientState.setSwipingUp(false);
         if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
                 || Math.abs(y - mInitialExpandY) > mTouchSlop
+                || (!isFullyExpanded() && !isFullyCollapsed())
                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
             mVelocityTracker.computeCurrentVelocity(1000);
             float vel = mVelocityTracker.getYVelocity();
@@ -4760,7 +4794,6 @@
             }
 
             mDozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
-                    mCentralSurfaces.isFalsingThresholdNeeded(),
                     mCentralSurfaces.isWakeUpComingFromTouch());
             // Log collapse gesture if on lock screen.
             if (!expand && onKeyguard) {
@@ -4782,10 +4815,10 @@
                 mUpdateFlingVelocity = vel;
             }
         } else if (!mCentralSurfaces.isBouncerShowing()
-                && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
+                && !mStatusBarKeyguardViewManager.isShowingAlternateBouncer()
                 && !mKeyguardStateController.isKeyguardGoingAway()) {
-            boolean expands = onEmptySpaceClick();
-            onTrackingStopped(expands);
+            onEmptySpaceClick();
+            onTrackingStopped(true);
         }
         mVelocityTracker.clear();
     }
@@ -4797,7 +4830,7 @@
 
     private void endClosing() {
         if (mClosing) {
-            setIsClosing(false);
+            setClosing(false);
             onClosingFinished();
         }
     }
@@ -4809,9 +4842,6 @@
      */
     private boolean isFalseTouch(float x, float y,
             @Classifier.InteractionType int interactionType) {
-        if (!mCentralSurfaces.isFalsingThresholdNeeded()) {
-            return false;
-        }
         if (mFalsingManager.isClassifierEnabled()) {
             return mFalsingManager.isFalseTouch(interactionType);
         }
@@ -4832,7 +4862,7 @@
             boolean expandBecauseOfFalsing) {
         float target = expand ? getMaxPanelHeight() : 0;
         if (!expand) {
-            setIsClosing(true);
+            setClosing(true);
         }
         flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
     }
@@ -4851,10 +4881,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;
@@ -4865,13 +4897,9 @@
         animator.start();
     }
 
-    public String getName() {
-        return mViewName;
-    }
-
     @VisibleForTesting
     void setExpandedHeight(float height) {
-        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
+        debugLog("setExpandedHeight(%.1f)", height);
         setExpandedHeightInternal(height);
     }
 
@@ -4902,7 +4930,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));
@@ -4911,7 +4939,7 @@
             float maxPanelHeight = getMaxPanelTransitionDistance();
             if (mHeightAnimator == null) {
                 // Split shade has its own overscroll logic
-                if (mTracking && !mInSplitShade) {
+                if (mTracking && !mSplitShadeEnabled) {
                     float overExpansionPixels = Math.max(0, h - maxPanelHeight);
                     setOverExpansionInternal(overExpansionPixels, true /* isFromGesture */);
                 }
@@ -4958,17 +4986,16 @@
         setExpandedHeight(getMaxPanelTransitionDistance() * frac);
     }
 
-    @VisibleForTesting
     float getExpandedHeight() {
         return mExpandedHeight;
     }
 
-    public float getExpandedFraction() {
+    private float getExpandedFraction() {
         return mExpandedFraction;
     }
 
     public boolean isFullyExpanded() {
-        return mExpandedHeight >= getMaxPanelHeight();
+        return mExpandedHeight >= getMaxPanelTransitionDistance();
     }
 
     public boolean isFullyCollapsed() {
@@ -4979,10 +5006,6 @@
         return mClosing || mIsLaunchAnimationRunning;
     }
 
-    public boolean isFlinging() {
-        return mIsFlinging;
-    }
-
     public boolean isTracking() {
         return mTracking;
     }
@@ -5093,7 +5116,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) {
@@ -5142,16 +5165,11 @@
                 && !mIsSpringBackAnimation;
     }
 
-    /**
-     * Gets called when the user performs a click anywhere in the empty area of the panel.
-     *
-     * @return whether the panel will be expanded after the action performed by this method
-     */
-    private boolean onEmptySpaceClick() {
-        if (mHintAnimationRunning) {
-            return true;
+    /** Called when the user performs a click anywhere in the empty area of the panel. */
+    private void onEmptySpaceClick() {
+        if (!mHintAnimationRunning)  {
+            onMiddleClicked();
         }
-        return onMiddleClicked();
     }
 
     @VisibleForTesting
@@ -5168,7 +5186,7 @@
 
     /** Returns the NotificationPanelView. */
     public ViewGroup getView() {
-        // TODO: remove this method, or at least reduce references to it.
+        // TODO(b/254878364): remove this method, or at least reduce references to it.
         return mView;
     }
 
@@ -5208,12 +5226,11 @@
         return mShadeExpansionStateManager;
     }
 
-    private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
+    private final class NsslHeightChangedListener implements
+            ExpandableView.OnHeightChangedListener {
         @Override
         public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
-
-            // Block update if we are in quick settings and just the top padding changed
-            // (i.e. view == null).
+            // Block update if we are in QS and just the top padding changed (i.e. view == null).
             if (view == null && mQsExpanded) {
                 return;
             }
@@ -5237,26 +5254,22 @@
         }
 
         @Override
-        public void onReset(ExpandableView view) {
+        public void onReset(ExpandableView view) {}
+    }
+
+    private void collapseOrExpand() {
+        onQsExpansionStarted();
+        if (mQsExpanded) {
+            flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */,
+                    true /* isClick */);
+        } else if (isQsExpansionEnabled()) {
+            mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
+            flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */,
+                    true /* isClick */);
         }
     }
 
-    private class CollapseExpandAction implements Runnable {
-        @Override
-        public void run() {
-            onQsExpansionStarted();
-            if (mQsExpanded) {
-                flingSettings(0 /* vel */, FLING_COLLAPSE, null /* onFinishRunnable */,
-                        true /* isClick */);
-            } else if (isQsExpansionEnabled()) {
-                mLockscreenGestureLogger.write(MetricsEvent.ACTION_SHADE_QS_TAP, 0, 0);
-                flingSettings(0 /* vel */, FLING_EXPAND, null /* onFinishRunnable */,
-                        true /* isClick */);
-            }
-        }
-    }
-
-    private class OnOverscrollTopChangedListener implements
+    private final class NsslOverscrollTopChangedListener implements
             NotificationStackScrollLayout.OnOverscrollTopChangedListener {
         @Override
         public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
@@ -5273,7 +5286,7 @@
             mQsExpansionFromOverscroll = rounded != 0f;
             mLastOverscroll = rounded;
             updateQsState();
-            setQsExpansion(mQsMinExpansionHeight + rounded);
+            setQsExpansionHeight(mQsMinExpansionHeight + rounded);
         }
 
         @Override
@@ -5290,7 +5303,7 @@
                 // make sure we can expand
                 setOverScrolling(false);
             }
-            setQsExpansion(mQsExpansionHeight);
+            setQsExpansionHeight(mQsExpansionHeight);
             boolean canExpand = isQsExpansionEnabled();
             flingSettings(!canExpand && open ? 0f : velocity,
                     open && canExpand ? FLING_EXPAND : FLING_COLLAPSE, () -> {
@@ -5300,27 +5313,16 @@
         }
     }
 
-    private class DynamicPrivacyControlListener implements DynamicPrivacyController.Listener {
-        @Override
-        public void onDynamicPrivacyChanged() {
-            // Do not request animation when pulsing or waking up, otherwise the clock wiill be out
-            // of sync with the notification panel.
-            if (mLinearDarkAmount != 0) {
-                return;
-            }
-            mAnimateNextPositionUpdate = true;
+    private void onDynamicPrivacyChanged() {
+        // Do not request animation when pulsing or waking up, otherwise the clock will be out
+        // of sync with the notification panel.
+        if (mLinearDarkAmount != 0) {
+            return;
         }
+        mAnimateNextPositionUpdate = true;
     }
 
-    private class OnEmptySpaceClickListener implements
-            NotificationStackScrollLayout.OnEmptySpaceClickListener {
-        @Override
-        public void onEmptySpaceClicked(float x, float y) {
-            onEmptySpaceClick();
-        }
-    }
-
-    private class MyOnHeadsUpChangedListener implements OnHeadsUpChangedListener {
+    private final class ShadeHeadsUpChangedListener implements OnHeadsUpChangedListener {
         @Override
         public void onHeadsUpPinnedModeChanged(final boolean inPinnedMode) {
             if (inPinnedMode) {
@@ -5360,32 +5362,31 @@
         }
     }
 
-    private class HeightListener implements QS.HeightListener {
-        public void onQsHeightChanged() {
-            mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
-            if (mQsExpanded && mQsFullyExpanded) {
-                mQsExpansionHeight = mQsMaxExpansionHeight;
-                requestScrollerTopPaddingUpdate(false /* animate */);
-                updateExpandedHeightToMaxHeight();
-            }
-            if (mAccessibilityManager.isEnabled()) {
-                mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
-            }
-            mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight);
+    private void onQsHeightChanged() {
+        mQsMaxExpansionHeight = mQs != null ? mQs.getDesiredHeight() : 0;
+        if (mQsExpanded && mQsFullyExpanded) {
+            mQsExpansionHeight = mQsMaxExpansionHeight;
+            requestScrollerTopPaddingUpdate(false /* animate */);
+            updateExpandedHeightToMaxHeight();
         }
+        if (mAccessibilityManager.isEnabled()) {
+            mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
+        }
+        mNotificationStackScrollLayoutController.setMaxTopPadding(mQsMaxExpansionHeight);
     }
 
-    private class ConfigurationListener implements ConfigurationController.ConfigurationListener {
+    private final class ConfigurationListener implements
+            ConfigurationController.ConfigurationListener {
         @Override
         public void onThemeChanged() {
-            if (DEBUG_LOGCAT) Log.d(TAG, "onThemeChanged");
+            debugLog("onThemeChanged");
             reInflateViews();
         }
 
         @Override
         public void onSmallestScreenWidthChanged() {
             Trace.beginSection("onSmallestScreenWidthChanged");
-            if (DEBUG_LOGCAT) Log.d(TAG, "onSmallestScreenWidthChanged");
+            debugLog("onSmallestScreenWidthChanged");
 
             // Can affect multi-user switcher visibility as it depends on screen size by default:
             // it is enabled only for devices with large screens (see config_keyguardUserSwitcher)
@@ -5402,27 +5403,26 @@
 
         @Override
         public void onDensityOrFontScaleChanged() {
-            if (DEBUG_LOGCAT) Log.d(TAG, "onDensityOrFontScaleChanged");
+            debugLog("onDensityOrFontScaleChanged");
             reInflateViews();
         }
     }
 
-    private class SettingsChangeObserver extends ContentObserver {
-
+    private final class SettingsChangeObserver extends ContentObserver {
         SettingsChangeObserver(Handler handler) {
             super(handler);
         }
 
         @Override
         public void onChange(boolean selfChange) {
-            if (DEBUG_LOGCAT) Log.d(TAG, "onSettingsChanged");
+            debugLog("onSettingsChanged");
 
             // Can affect multi-user switcher visibility
             reInflateViews();
         }
     }
 
-    private class StatusBarStateListener implements StateListener {
+    private final class StatusBarStateListener implements StateListener {
         @Override
         public void onStateChanged(int statusBarState) {
             boolean goingToFullShade = mStatusBarStateController.goingToFullShade();
@@ -5483,10 +5483,16 @@
                 }
             } else {
                 // this else branch means we are doing one of:
-                //  - from KEYGUARD and SHADE (but not expanded shade)
+                //  - from KEYGUARD to SHADE (but not fully expanded as when swiping from the top)
                 //  - from SHADE to KEYGUARD
                 //  - from SHADE_LOCKED to SHADE
                 //  - getting notified again about the current SHADE or KEYGUARD state
+                if (mSplitShadeEnabled && oldState == SHADE && statusBarState == KEYGUARD) {
+                    // user can go to keyguard from different shade states and closing animation
+                    // may not fully run - we always want to make sure we close QS when that happens
+                    // as we never need QS open in fresh keyguard state
+                    closeQs();
+                }
                 final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
                         && statusBarState == KEYGUARD
                         && mScreenOffAnimationController.isKeyguardShowDelayed();
@@ -5578,21 +5584,19 @@
         setExpandedFraction(1f);
     }
 
-    /**
-     * Sets the overstretch amount in raw pixels when dragging down.
-     */
-    public void setOverStrechAmount(float amount) {
+    /** Sets the overstretch amount in raw pixels when dragging down. */
+    public void setOverStretchAmount(float amount) {
         float progress = amount / mView.getHeight();
-        float overstretch = Interpolators.getOvershootInterpolation(progress);
-        mOverStretchAmount = overstretch * mMaxOverscrollAmountForPulse;
+        float overStretch = Interpolators.getOvershootInterpolation(progress);
+        mOverStretchAmount = overStretch * mMaxOverscrollAmountForPulse;
         positionClockAndNotifications(true /* forceUpdate */);
     }
 
-    private class OnAttachStateChangeListener implements View.OnAttachStateChangeListener {
+    private final class ShadeAttachStateChangeListener implements View.OnAttachStateChangeListener {
         @Override
         public void onViewAttachedToWindow(View v) {
             mFragmentService.getFragmentHostManager(mView)
-                    .addTagListener(QS.TAG, mFragmentListener);
+                    .addTagListener(QS.TAG, mQsFragmentListener);
             mStatusBarStateController.addCallback(mStatusBarStateListener);
             mStatusBarStateListener.onStateChanged(mStatusBarStateController.getState());
             mConfigurationController.addCallback(mConfigurationListener);
@@ -5607,16 +5611,16 @@
 
         @Override
         public void onViewDetachedFromWindow(View v) {
-            unregisterSettingsChangeListener();
+            mContentResolver.unregisterContentObserver(mSettingsChangeObserver);
             mFragmentService.getFragmentHostManager(mView)
-                    .removeTagListener(QS.TAG, mFragmentListener);
+                    .removeTagListener(QS.TAG, mQsFragmentListener);
             mStatusBarStateController.removeCallback(mStatusBarStateListener);
             mConfigurationController.removeCallback(mConfigurationListener);
             mFalsingManager.removeTapListener(mFalsingTapListener);
         }
     }
 
-    private final class OnLayoutChangeListener implements View.OnLayoutChangeListener {
+    private final class ShadeLayoutChangeListener implements View.OnLayoutChangeListener {
         @Override
         public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
                 int oldTop, int oldRight, int oldBottom) {
@@ -5625,7 +5629,7 @@
             mHasLayoutedSinceDown = true;
             if (mUpdateFlingOnLayout) {
                 abortAnimations();
-                fling(mUpdateFlingVelocity, true /* expands */);
+                fling(mUpdateFlingVelocity);
                 mUpdateFlingOnLayout = false;
             }
             updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount());
@@ -5652,21 +5656,18 @@
                     startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight);
                 }
             } else if (!mQsExpanded && mQsExpansionAnimator == null) {
-                setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
+                setQsExpansionHeight(mQsMinExpansionHeight + mLastOverscroll);
             } else {
                 mShadeLog.v("onLayoutChange: qs expansion not set");
             }
             updateExpandedHeight(getExpandedHeight());
             updateHeader();
 
-            // If we are running a size change animation, the animation takes care of the height of
-            // the container. However, if we are not animating, we always need to make the QS
-            // container
-            // the desired height so when closing the QS detail, it stays smaller after the size
-            // change
-            // animation is finished but the detail view is still being animated away (this
-            // animation
-            // takes longer than the size change animation).
+            // If we are running a size change animation, the animation takes care of the height
+            // of the container. However, if we are not animating, we always need to make the QS
+            // container the desired height so when closing the QS detail, it stays smaller after
+            // the size change animation is finished but the detail view is still being animated
+            // away (this animation takes longer than the size change animation).
             if (mQsSizeChangeAnimator == null && mQs != null) {
                 mQs.setHeightOverride(mQs.getDesiredHeight());
             }
@@ -5692,102 +5693,17 @@
         }
     }
 
-    private class DebugDrawable extends Drawable {
+    @NonNull
+    private WindowInsets onApplyShadeWindowInsets(WindowInsets insets) {
+        // the same types of insets that are handled in NotificationShadeWindowView
+        int insetTypes = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout();
+        Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes);
+        mDisplayTopInset = combinedInsets.top;
+        mDisplayRightInset = combinedInsets.right;
 
-        private final Set<Integer> mDebugTextUsedYPositions = new HashSet<>();
-        private final Paint mDebugPaint = new Paint();
-
-        @Override
-        public void draw(@androidx.annotation.NonNull @NonNull Canvas canvas) {
-            mDebugTextUsedYPositions.clear();
-
-            mDebugPaint.setColor(Color.RED);
-            mDebugPaint.setStrokeWidth(2);
-            mDebugPaint.setStyle(Paint.Style.STROKE);
-            mDebugPaint.setTextSize(24);
-            if (mHeaderDebugInfo != null) canvas.drawText(mHeaderDebugInfo, 50, 100, mDebugPaint);
-
-            drawDebugInfo(canvas, getMaxPanelHeight(), Color.RED, "getMaxPanelHeight()");
-            drawDebugInfo(canvas, (int) getExpandedHeight(), Color.BLUE, "getExpandedHeight()");
-            drawDebugInfo(canvas, calculatePanelHeightQsExpanded(), Color.GREEN,
-                    "calculatePanelHeightQsExpanded()");
-            drawDebugInfo(canvas, calculatePanelHeightShade(), Color.YELLOW,
-                    "calculatePanelHeightShade()");
-            drawDebugInfo(canvas, (int) calculateNotificationsTopPadding(), Color.MAGENTA,
-                    "calculateNotificationsTopPadding()");
-            drawDebugInfo(canvas, mClockPositionResult.clockY, Color.GRAY,
-                    "mClockPositionResult.clockY");
-            drawDebugInfo(canvas, (int) mLockIconViewController.getTop(), Color.GRAY,
-                    "mLockIconViewController.getTop()");
-
-            if (mKeyguardShowing) {
-                // Notifications have the space between those two lines.
-                drawDebugInfo(canvas,
-                        mNotificationStackScrollLayoutController.getTop() +
-                                (int) mKeyguardNotificationTopPadding,
-                        Color.RED,
-                        "NSSL.getTop() + mKeyguardNotificationTopPadding");
-
-                drawDebugInfo(canvas, mNotificationStackScrollLayoutController.getBottom() -
-                                (int) mKeyguardNotificationBottomPadding,
-                        Color.RED,
-                        "NSSL.getBottom() - mKeyguardNotificationBottomPadding");
-            }
-
-            mDebugPaint.setColor(Color.CYAN);
-            canvas.drawLine(0, mClockPositionResult.stackScrollerPadding, mView.getWidth(),
-                    mNotificationStackScrollLayoutController.getTopPadding(), mDebugPaint);
-        }
-
-        private void drawDebugInfo(Canvas canvas, int y, int color, String label) {
-            mDebugPaint.setColor(color);
-            canvas.drawLine(/* startX= */ 0, /* startY= */ y, /* stopX= */ mView.getWidth(),
-                    /* stopY= */ y, mDebugPaint);
-            canvas.drawText(label + " = " + y + "px", /* x= */ 0,
-                    /* y= */ computeDebugYTextPosition(y), mDebugPaint);
-        }
-
-        private int computeDebugYTextPosition(int lineY) {
-            if (lineY - mDebugPaint.getTextSize() < 0) {
-                // Avoiding drawing out of bounds
-                lineY += mDebugPaint.getTextSize();
-            }
-            int textY = lineY;
-            while (mDebugTextUsedYPositions.contains(textY)) {
-                textY = (int) (textY + mDebugPaint.getTextSize());
-            }
-            mDebugTextUsedYPositions.add(textY);
-            return textY;
-        }
-
-        @Override
-        public void setAlpha(int alpha) {
-
-        }
-
-        @Override
-        public void setColorFilter(ColorFilter colorFilter) {
-
-        }
-
-        @Override
-        public int getOpacity() {
-            return PixelFormat.UNKNOWN;
-        }
-    }
-
-    private class OnApplyWindowInsetsListener implements View.OnApplyWindowInsetsListener {
-        public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
-            // the same types of insets that are handled in NotificationShadeWindowView
-            int insetTypes = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout();
-            Insets combinedInsets = insets.getInsetsIgnoringVisibility(insetTypes);
-            mDisplayTopInset = combinedInsets.top;
-            mDisplayRightInset = combinedInsets.right;
-
-            mNavigationBarBottomHeight = insets.getStableInsetBottom();
-            updateMaxHeadsUpTranslation();
-            return insets;
-        }
+        mNavigationBarBottomHeight = insets.getStableInsetBottom();
+        updateMaxHeadsUpTranslation();
+        return insets;
     }
 
     /** Removes any pending runnables that would collapse the panel. */
@@ -5795,9 +5711,6 @@
         mView.removeCallbacks(mMaybeHideExpandedRunnable);
     }
 
-    @PanelState
-    private int mCurrentPanelState = STATE_CLOSED;
-
     private void onPanelStateChanged(@PanelState int state) {
         updateQSExpansionEnabledAmbient();
 
@@ -5834,6 +5747,11 @@
     }
 
     @VisibleForTesting
+    StateListener getStatusBarStateListener() {
+        return mStatusBarStateListener;
+    }
+
+    @VisibleForTesting
     boolean isHintAnimationRunning() {
         return mHintAnimationRunning;
     }
@@ -5848,50 +5766,13 @@
         }
     }
 
-    @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) {
+            mShadeLog.logMotionEvent(event, "NPVC onInterceptTouchEvent");
             if (SPEW_LOGCAT) {
                 Log.v(TAG,
                         "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
@@ -5904,6 +5785,8 @@
             // Do not let touches go to shade or QS if the bouncer is visible,
             // but still let user swipe down to expand the panel, dismissing the bouncer.
             if (mCentralSurfaces.isBouncerShowing()) {
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "bouncer is showing");
                 return true;
             }
             if (mCommandQueue.panelsEnabled()
@@ -5911,15 +5794,21 @@
                     && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                 mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
                 mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "HeadsUpTouchHelper");
                 return true;
             }
             if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
                     && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "PulseExpansionHandler");
                 return true;
             }
 
             if (!isFullyCollapsed() && onQsIntercept(event)) {
-                if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept true");
+                debugLog("onQsIntercept true");
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "QsIntercept");
                 return true;
             }
             if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
@@ -5950,6 +5839,9 @@
                     if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
                         cancelHeightAnimator();
                         mTouchSlopExceeded = true;
+                        mShadeLog.v("NotificationPanelViewController MotionEvent intercepted:"
+                                + " mAnimatingOnDown: true, mClosing: true, mHintAnimationRunning:"
+                                + " false");
                         return true;
                     }
                     mInitialExpandY = y;
@@ -5994,6 +5886,8 @@
                                 && hAbs > Math.abs(x - mInitialExpandX)) {
                             cancelHeightAnimator();
                             startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+                            mShadeLog.v("NotificationPanelViewController MotionEvent"
+                                    + " intercepted: startExpandMotion");
                             return true;
                         }
                     }
@@ -6089,7 +5983,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;
             }
@@ -6122,7 +6016,6 @@
              *
              * Flinging is also enabled in order to open or close the shade.
              */
-
             int pointerIndex = event.findPointerIndex(mTrackingPointer);
             if (pointerIndex < 0) {
                 pointerIndex = 0;
@@ -6138,6 +6031,7 @@
 
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
+                    mShadeLog.logMotionEvent(event, "onTouch: down action");
                     startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                     mMinExpandHeight = 0.0f;
                     mPanelClosedOnDown = isFullyCollapsed();
@@ -6184,6 +6078,10 @@
                     }
                     break;
                 case MotionEvent.ACTION_MOVE:
+                    if (isFullyCollapsed()) {
+                        // If panel is fully collapsed, reset haptic effect before adding movement.
+                        mHasVibratedOnOpen = false;
+                    }
                     addMovement(event);
                     if (!isFullyCollapsed()) {
                         maybeVibrateOnOpening(true /* openingWithTouch */);
@@ -6222,6 +6120,7 @@
 
                 case MotionEvent.ACTION_UP:
                 case MotionEvent.ACTION_CANCEL:
+                    mShadeLog.logMotionEvent(event, "onTouch: up/cancel action");
                     addMovement(event);
                     endMotionEvent(event, x, y, false /* forceCancel */);
                     // mHeightAnimator is null, there is no remaining frame, ends instrumenting.
@@ -6238,12 +6137,76 @@
         }
     }
 
-    /** Listens for config changes. */
-    public class OnConfigurationChangedListener implements
-            NotificationPanelView.OnConfigurationChangedListener {
+    static class SplitShadeTransitionAdapter extends Transition {
+        private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds";
+        private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS };
+
+        private final KeyguardStatusViewController mController;
+
+        SplitShadeTransitionAdapter(KeyguardStatusViewController controller) {
+            mController = controller;
+        }
+
+        private void captureValues(TransitionValues transitionValues) {
+            Rect boundsRect = new Rect();
+            boundsRect.left = transitionValues.view.getLeft();
+            boundsRect.top = transitionValues.view.getTop();
+            boundsRect.right = transitionValues.view.getRight();
+            boundsRect.bottom = transitionValues.view.getBottom();
+            transitionValues.values.put(PROP_BOUNDS, boundsRect);
+        }
+
         @Override
-        public void onConfigurationChanged(Configuration newConfig) {
-            loadDimens();
+        public void captureEndValues(TransitionValues transitionValues) {
+            captureValues(transitionValues);
+        }
+
+        @Override
+        public void captureStartValues(TransitionValues transitionValues) {
+            captureValues(transitionValues);
+        }
+
+        @Override
+        public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+                TransitionValues endValues) {
+            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+
+            Rect from = (Rect) startValues.values.get(PROP_BOUNDS);
+            Rect to = (Rect) endValues.values.get(PROP_BOUNDS);
+
+            anim.addUpdateListener(
+                    animation -> mController.getClockAnimations().onPositionUpdated(
+                            from, to, animation.getAnimatedFraction()));
+
+            return anim;
+        }
+
+        @Override
+        public String[] getTransitionProperties() {
+            return TRANSITION_PROPERTIES;
+        }
+    }
+
+    private final class ShadeAccessibilityDelegate extends AccessibilityDelegate {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host,
+                AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle args) {
+            if (action
+                    == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()
+                    || action
+                    == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId()) {
+                mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
+                return true;
+            }
+            return super.performAccessibilityAction(host, action, args);
         }
     }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 1d92105..b719177 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -44,6 +44,7 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManagerGlobal;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
@@ -135,7 +136,8 @@
             DumpManager dumpManager,
             KeyguardStateController keyguardStateController,
             ScreenOffAnimationController screenOffAnimationController,
-            AuthController authController) {
+            AuthController authController,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         mContext = context;
         mWindowManager = windowManager;
         mActivityManager = activityManager;
@@ -156,6 +158,8 @@
                 .addCallback(mStateListener,
                         SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER);
         configurationController.addCallback(this);
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         float desiredPreferredRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
@@ -202,6 +206,14 @@
         }
     }
 
+    @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mCurrentState.mPanelExpanded != isExpanded) {
+            mCurrentState.mPanelExpanded = isExpanded;
+            apply(mCurrentState);
+        }
+    }
+
     /**
      * Register a listener to monitor scrims visibility
      * @param listener A listener to monitor scrims visibility
@@ -607,8 +619,7 @@
         apply(mCurrentState);
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    private void onQsExpansionChanged(Boolean expanded) {
         mCurrentState.mQsExpanded = expanded;
         apply(mCurrentState);
     }
@@ -698,15 +709,6 @@
     }
 
     @Override
-    public void setPanelExpanded(boolean isExpanded) {
-        if (mCurrentState.mPanelExpanded == isExpanded) {
-            return;
-        }
-        mCurrentState.mPanelExpanded = isExpanded;
-        apply(mCurrentState);
-    }
-
-    @Override
     public void onRemoteInputActive(boolean remoteInputActive) {
         mCurrentState.mRemoteInputActive = remoteInputActive;
         apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index e52170e..6acf417 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade;
 
+import static android.os.Trace.TRACE_TAG_ALWAYS;
 import static android.view.WindowInsets.Type.systemBars;
 
 import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
@@ -23,6 +24,7 @@
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.LayoutRes;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
@@ -33,7 +35,9 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Trace;
 import android.util.AttributeSet;
+import android.util.Pair;
 import android.view.ActionMode;
 import android.view.DisplayCutout;
 import android.view.InputQueue;
@@ -72,6 +76,7 @@
     private ViewTreeObserver.OnPreDrawListener mFloatingToolbarPreDrawListener;
 
     private InteractionEventHandler mInteractionEventHandler;
+    private LayoutInsetsController mLayoutInsetProvider;
 
     public NotificationShadeWindowView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -106,12 +111,10 @@
         mLeftInset = 0;
         mRightInset = 0;
         DisplayCutout displayCutout = getRootWindowInsets().getDisplayCutout();
-        if (displayCutout != null) {
-            mLeftInset = displayCutout.getSafeInsetLeft();
-            mRightInset = displayCutout.getSafeInsetRight();
-        }
-        mLeftInset = Math.max(insets.left, mLeftInset);
-        mRightInset = Math.max(insets.right, mRightInset);
+        Pair<Integer, Integer> pairInsets = mLayoutInsetProvider
+                .getinsets(windowInsets, displayCutout);
+        mLeftInset = pairInsets.first;
+        mRightInset = pairInsets.second;
         applyMargins();
         return windowInsets;
     }
@@ -170,6 +173,10 @@
         mInteractionEventHandler = listener;
     }
 
+    protected void setLayoutInsetsController(LayoutInsetsController provider) {
+        mLayoutInsetProvider = provider;
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         Boolean result = mInteractionEventHandler.handleDispatchTouchEvent(ev);
@@ -299,6 +306,19 @@
         return mode;
     }
 
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Trace.beginSection("NotificationShadeWindowView#onMeasure");
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        Trace.endSection();
+    }
+
+    @Override
+    public void requestLayout() {
+        Trace.instant(TRACE_TAG_ALWAYS, "NotificationShadeWindowView#requestLayout");
+        super.requestLayout();
+    }
+
     private class ActionModeCallback2Wrapper extends ActionMode.Callback2 {
         private final ActionMode.Callback mWrapped;
 
@@ -338,6 +358,18 @@
         }
     }
 
+    /**
+     * Controller responsible for calculating insets for the shade window.
+     */
+    public interface LayoutInsetsController {
+
+        /**
+         * Update the insets and calculate them accordingly.
+         */
+        Pair<Integer, Integer> getinsets(@Nullable WindowInsets windowInsets,
+                @Nullable DisplayCutout displayCutout);
+    }
+
     interface InteractionEventHandler {
         /**
          * Returns a result for {@link ViewGroup#dispatchTouchEvent(MotionEvent)} or null to defer
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 65bd58d..8379e51 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.NotificationInsetsController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -76,6 +77,7 @@
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final AmbientState mAmbientState;
     private final PulsingGestureListener mPulsingGestureListener;
+    private final NotificationInsetsController mNotificationInsetsController;
 
     private GestureDetector mPulsingWakeupGestureHandler;
     private View mBrightnessMirror;
@@ -111,6 +113,7 @@
             CentralSurfaces centralSurfaces,
             NotificationShadeWindowController controller,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            NotificationInsetsController notificationInsetsController,
             AmbientState ambientState,
             PulsingGestureListener pulsingGestureListener,
             FeatureFlags featureFlags,
@@ -134,6 +137,7 @@
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mAmbientState = ambientState;
         mPulsingGestureListener = pulsingGestureListener;
+        mNotificationInsetsController = notificationInsetsController;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -165,6 +169,7 @@
         mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
                 mPulsingGestureListener);
 
+        mView.setLayoutInsetsController(mNotificationInsetsController);
         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
             @Override
             public Boolean handleDispatchTouchEvent(MotionEvent ev) {
@@ -284,7 +289,7 @@
                     return true;
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) {
+                if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
                     // capture all touches if the alt auth bouncer is showing
                     return true;
                 }
@@ -322,7 +327,7 @@
                     handled = !mService.isPulsing();
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) {
+                if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
                     // eat the touch
                     handled = true;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index d6f0de8..85b259e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade
 
 import android.view.View
@@ -36,17 +52,12 @@
     private val navigationModeController: NavigationModeController,
     private val overviewProxyService: OverviewProxyService,
     private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
+    private val shadeExpansionStateManager: ShadeExpansionStateManager,
     private val featureFlags: FeatureFlags,
     @Main private val delayableExecutor: DelayableExecutor
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
-    var qsExpanded = false
-        set(value) {
-            if (field != value) {
-                field = value
-                mView.invalidate()
-            }
-        }
+    private var qsExpanded = false
     private var splitShadeEnabled = false
     private var isQSDetailShowing = false
     private var isQSCustomizing = false
@@ -71,6 +82,13 @@
             taskbarVisible = visible
         }
     }
+    private val shadeQsExpansionListener: ShadeQsExpansionListener =
+        ShadeQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+                mView.invalidate()
+            }
+        }
 
     // With certain configuration changes (like light/dark changes), the nav bar will disappear
     // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
@@ -106,6 +124,7 @@
     public override fun onViewAttached() {
         updateResources()
         overviewProxyService.addCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener)
         mView.setInsetsChangedListener(delayedInsetSetter)
         mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
         mView.setConfigurationChangedListener { updateResources() }
@@ -113,6 +132,7 @@
 
     override fun onViewDetached() {
         overviewProxyService.removeCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener)
         mView.removeOnInsetsChangedListener()
         mView.removeQSFragmentAttachedListener()
         mView.setConfigurationChangedListener(null)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index 084b7dc..bf622c9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -52,6 +52,7 @@
         private val centralSurfaces: CentralSurfaces,
         private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
         private val statusBarStateController: StatusBarStateController,
+        private val shadeLogger: ShadeLogger,
         tunerService: TunerService,
         dumpManager: DumpManager
 ) : GestureDetector.SimpleOnGestureListener(), Dumpable {
@@ -77,18 +78,23 @@
     }
 
     override fun onSingleTapUp(e: MotionEvent): Boolean {
-        if (statusBarStateController.isDozing &&
-                singleTapEnabled &&
-                !dockManager.isDocked &&
-                !falsingManager.isProximityNear &&
-                !falsingManager.isFalseTap(LOW_PENALTY)
-        ) {
-            centralSurfaces.wakeUpIfDozing(
+        val isNotDocked = !dockManager.isDocked
+        shadeLogger.logSingleTapUp(statusBarStateController.isDozing, singleTapEnabled, isNotDocked)
+        if (statusBarStateController.isDozing && singleTapEnabled && isNotDocked) {
+            val proximityIsNotNear = !falsingManager.isProximityNear
+            val isNotAFalseTap = !falsingManager.isFalseTap(LOW_PENALTY)
+            shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
+            if (proximityIsNotNear && isNotAFalseTap) {
+                shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
+                centralSurfaces.wakeUpIfDozing(
                     SystemClock.uptimeMillis(),
                     notificationShadeWindowView,
-                    "PULSING_SINGLE_TAP")
+                    "PULSING_SINGLE_TAP"
+                )
+            }
             return true
         }
+        shadeLogger.d("onSingleTapUp event ignored")
         return false
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index f389dd9..eaf7fae 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -224,6 +224,6 @@
     }
 
     private NotificationPanelViewController getNotificationPanelViewController() {
-        return getCentralSurfaces().getPanelController();
+        return getCentralSurfaces().getNotificationPanelViewController();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
new file mode 100644
index 0000000..959c339
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import com.android.systemui.dagger.SysUISingleton;
+
+import dagger.Binds;
+import dagger.Module;
+
+/** Provides a {@link ShadeStateEvents} in {@link SysUISingleton} scope. */
+@Module
+public abstract class ShadeEventsModule {
+    @Binds
+    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 f617d47..a1767cc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -20,7 +20,9 @@
 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
 
 /**
@@ -29,14 +31,18 @@
  * 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 = mutableListOf<ShadeExpansionListener>()
-    private val stateListeners = mutableListOf<ShadeStateListener>()
+    private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
+    private val fullExpansionListeners = CopyOnWriteArrayList<ShadeFullExpansionListener>()
+    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
     private var expanded: Boolean = false
+    private var qsExpanded: Boolean = false
     private var tracking: Boolean = false
     private var dragDownPxAmount: Float = 0f
 
@@ -57,6 +63,24 @@
         expansionListeners.remove(listener)
     }
 
+    fun addFullExpansionListener(listener: ShadeFullExpansionListener) {
+        fullExpansionListeners.add(listener)
+        listener.onShadeExpansionFullyChanged(qsExpanded)
+    }
+
+    fun removeFullExpansionListener(listener: ShadeFullExpansionListener) {
+        fullExpansionListeners.remove(listener)
+    }
+
+    fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.add(listener)
+        listener.onQsExpansionChanged(qsExpanded)
+    }
+
+    fun removeQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.remove(listener)
+    }
+
     /** Adds a listener that will be notified when the panel state has changed. */
     fun addStateListener(listener: ShadeStateListener) {
         stateListeners.add(listener)
@@ -67,6 +91,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
 
@@ -126,6 +158,21 @@
         expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) }
     }
 
+    /** Called when the quick settings expansion changes to fully expanded or collapsed. */
+    fun onQsExpansionChanged(qsExpanded: Boolean) {
+        this.qsExpanded = qsExpanded
+
+        debugLog("qsExpanded=$qsExpanded")
+        qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
+    }
+
+    fun onShadeExpansionFullyChanged(isExpanded: Boolean) {
+        this.expanded = isExpanded
+
+        debugLog("expanded=$isExpanded")
+        fullExpansionListeners.forEach { it.onShadeExpansionFullyChanged(isExpanded) }
+    }
+
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
@@ -142,6 +189,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/ShadeFullExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
new file mode 100644
index 0000000..6d13e19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+/** A listener interface to be notified of expansion events for the notification shade. */
+fun interface ShadeFullExpansionListener {
+    /** Invoked whenever the shade expansion changes, when it is fully collapsed or expanded */
+    fun onShadeExpansionFullyChanged(isExpanded: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 7bee0ba..40ed40a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -1,10 +1,26 @@
+/*
+ * 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
 
 import android.view.MotionEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ShadeLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -12,64 +28,140 @@
 
 /** Lightweight logging utility for the Shade. */
 class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) {
-  fun v(@CompileTimeConstant msg: String) {
-    buffer.log(TAG, LogLevel.VERBOSE, msg)
-  }
+    fun v(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.VERBOSE, msg)
+    }
 
-  private inline fun log(
-      logLevel: LogLevel,
-      initializer: LogMessage.() -> Unit,
-      noinline printer: LogMessage.() -> String
-  ) {
-    buffer.log(TAG, logLevel, initializer, printer)
-  }
+    fun d(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.DEBUG, msg)
+    }
 
-  fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
-    log(
-        LogLevel.VERBOSE,
-        { double1 = h.toDouble() },
-        { "onQsIntercept: move action, QS tracking enabled. h = $double1" })
-  }
+    private inline fun log(
+        logLevel: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        buffer.log(TAG, logLevel, initializer, printer)
+    }
 
-  fun logQsTrackingNotStarted(
-      initialTouchY: Float,
-      y: Float,
-      h: Float,
-      touchSlop: Float,
-      qsExpanded: Boolean,
-      collapsedOnDown: Boolean,
-      keyguardShowing: Boolean,
-      qsExpansionEnabled: Boolean
-  ) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          int1 = initialTouchY.toInt()
-          int2 = y.toInt()
-          long1 = h.toLong()
-          double1 = touchSlop.toDouble()
-          bool1 = qsExpanded
-          bool2 = collapsedOnDown
-          bool3 = keyguardShowing
-          bool4 = qsExpansionEnabled
-        },
-        {
-          "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded=" +
-              "$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+    fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
+        log(
+            LogLevel.VERBOSE,
+            { double1 = h.toDouble() },
+            { "onQsIntercept: move action, QS tracking enabled. h = $double1" }
+        )
+    }
+
+    fun logQsTrackingNotStarted(
+        initialTouchY: Float,
+        y: Float,
+        h: Float,
+        touchSlop: Float,
+        qsExpanded: Boolean,
+        collapsedOnDown: Boolean,
+        keyguardShowing: Boolean,
+        qsExpansionEnabled: Boolean
+    ) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                int1 = initialTouchY.toInt()
+                int2 = y.toInt()
+                long1 = h.toLong()
+                double1 = touchSlop.toDouble()
+                bool1 = qsExpanded
+                bool2 = collapsedOnDown
+                bool3 = keyguardShowing
+                bool4 = qsExpansionEnabled
+            },
+            {
+                "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" +
+                    "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+            }
+        )
+    }
+
+    fun logMotionEvent(event: MotionEvent, message: String) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                long1 = event.eventTime
+                long2 = event.downTime
+                int1 = event.action
+                int2 = event.classification
+                double1 = event.y.toDouble()
+            },
+            {
+                "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2"
+            }
+        )
+    }
+
+    fun logExpansionChanged(
+            message: String,
+            fraction: Float,
+            expanded: Boolean,
+            tracking: Boolean,
+            dragDownPxAmount: Float,
+    ) {
+        log(LogLevel.VERBOSE, {
+            str1 = message
+            double1 = fraction.toDouble()
+            bool1 = expanded
+            bool2 = tracking
+            long1 = dragDownPxAmount.toLong()
+        }, {
+            "$str1 fraction=$double1,expanded=$bool1," +
+                    "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount"
         })
-  }
+    }
 
-  fun logMotionEvent(event: MotionEvent, message: String) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          str1 = message
-          long1 = event.eventTime
-          long2 = event.downTime
-          int1 = event.action
-          int2 = event.classification
-          double1 = event.y.toDouble()
-        },
-        { "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" })
-  }
+    fun logQsExpansionChanged(
+            message: String,
+            qsExpanded: Boolean,
+            qsMinExpansionHeight: Int,
+            qsMaxExpansionHeight: Int,
+            stackScrollerOverscrolling: Boolean,
+            dozing: Boolean,
+            qsAnimatorExpand: Boolean,
+            animatingQs: Boolean
+    ) {
+        log(LogLevel.VERBOSE, {
+            str1 = message
+            bool1 = qsExpanded
+            int1 = qsMinExpansionHeight
+            int2 = qsMaxExpansionHeight
+            bool2 = stackScrollerOverscrolling
+            bool3 = dozing
+            bool4 = qsAnimatorExpand
+            // 0 = false, 1 = true
+            long1 = animatingQs.compareTo(false).toLong()
+        }, {
+            "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," +
+                    "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," +
+                    "animatingQs=$long1"
+        })
+    }
+
+    fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) {
+        log(LogLevel.DEBUG, {
+            bool1 = isDozing
+            bool2 = singleTapEnabled
+            bool3 = isNotDocked
+        }, {
+            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+              "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
+        })
+    }
+
+    fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) {
+        log(LogLevel.DEBUG, {
+            bool1 = proximityIsNotNear
+            bool2 = isNotFalseTap
+        }, {
+            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+                    "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2"
+        })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
new file mode 100644
index 0000000..14882b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+/** A listener interface to be notified of expansion events for the quick settings panel. */
+fun interface ShadeQsExpansionListener {
+    /**
+     * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded
+     */
+    fun onQsExpansionChanged(isQsExpanded: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
new file mode 100644
index 0000000..56bb1a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+/** Provides certain notification panel events. */
+interface ShadeStateEvents {
+
+    /** Registers callbacks to be invoked when notification panel events occur. */
+    fun addShadeStateEventsListener(listener: ShadeStateEventsListener)
+
+    /** Unregisters callbacks previously registered via [addShadeStateEventsListener] */
+    fun removeShadeStateEventsListener(listener: ShadeStateEventsListener)
+
+    /** Callbacks for certain notification panel events. */
+    interface ShadeStateEventsListener {
+
+        /** Invoked when the notification panel starts or stops collapsing. */
+        @JvmDefault fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
+
+        /**
+         * Invoked when the notification panel starts or stops launching an [android.app.Activity].
+         */
+        @JvmDefault fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
+
+        /**
+         * Invoked when the "expand immediate" attribute changes.
+         *
+         * An example of expanding immediately is when swiping down from the top with two fingers.
+         * Instead of going to QQS, we immediately expand to full QS.
+         *
+         * 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) {}
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
new file mode 100644
index 0000000..09019a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.shade.data.repository
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/** Business logic for shade interactions */
+@SysUISingleton
+class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) {
+
+    val shadeModel: Flow<ShadeModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : ShadeExpansionListener {
+                        override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+                            // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field.
+                            // It is too noisy and produces extra events that consumers won't care
+                            // about
+                            val info =
+                                ShadeModel(
+                                    expansionAmount = event.fraction,
+                                    isExpanded = event.expanded,
+                                    isUserDragging = event.tracking
+                                )
+                            trySendWithFailureLogging(info, TAG, "updated shade expansion info")
+                        }
+                    }
+
+                shadeExpansionStateManager.addExpansionListener(callback)
+                trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info")
+
+                awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) }
+            }
+            .distinctUntilChanged()
+
+    companion object {
+        private const val TAG = "ShadeRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
new file mode 100644
index 0000000..ce0f4283
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.domain.model
+
+import android.annotation.FloatRange
+
+/** Information about shade (NotificationPanel) expansion */
+data class ShadeModel(
+    /** 0 when collapsed, 1 when fully expanded. */
+    @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f,
+    /** Whether the panel should be considered expanded */
+    val isExpanded: Boolean = false,
+    /** Whether the user is actively dragging the panel. */
+    val isUserDragging: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/NoOpOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/NoOpOverScroller.kt
index f4db3ab..8847dbd 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/NoOpOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/NoOpOverScroller.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade.transition
 
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index a77c21a..218e897 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade.transition
 
 import android.content.res.Configuration
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
index 22e847d..a4642e0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeOverScroller.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade.transition
 
 import com.android.systemui.shade.PanelState
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
index 1e8208f..1054aa5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade.transition
 
 import android.content.Context
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
index 8c57194..fde08ee 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeOverScroller.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.systemui.shade.transition
 
 import android.animation.Animator
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 7f7ff9cf..90c52bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 9d4a27c..f786ced 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -39,7 +39,7 @@
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.display.DisplayManager;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.inputmethodservice.InputMethodService.BackDispositionMode;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
@@ -53,7 +53,7 @@
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -67,12 +67,15 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.GcUtils;
 import com.android.internal.view.AppearanceRegion;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.systemui.tracing.ProtoTracer;
 
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
@@ -151,7 +154,7 @@
     //TODO(b/169175022) Update name and when feature name is locked.
     private static final int MSG_EMERGENCY_ACTION_LAUNCH_GESTURE      = 58 << MSG_SHIFT;
     private static final int MSG_SET_NAVIGATION_BAR_LUMA_SAMPLING_ENABLED = 59 << MSG_SHIFT;
-    private static final int MSG_SET_UDFPS_HBM_LISTENER = 60 << MSG_SHIFT;
+    private static final int MSG_SET_UDFPS_REFRESH_RATE_CALLBACK = 60 << MSG_SHIFT;
     private static final int MSG_TILE_SERVICE_REQUEST_ADD = 61 << MSG_SHIFT;
     private static final int MSG_TILE_SERVICE_REQUEST_CANCEL = 62 << MSG_SHIFT;
     private static final int MSG_SET_BIOMETRICS_LISTENER = 63 << MSG_SHIFT;
@@ -182,6 +185,7 @@
     private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;
     private ProtoTracer mProtoTracer;
     private final @Nullable CommandRegistry mRegistry;
+    private final @Nullable DumpHandler mDumpHandler;
 
     /**
      * These methods are called back on the main thread.
@@ -329,9 +333,9 @@
         }
 
         /**
-         * @see IStatusBar#setUdfpsHbmListener(IUdfpsHbmListener)
+         * @see IStatusBar#setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback)
          */
-        default void setUdfpsHbmListener(IUdfpsHbmListener listener) {
+        default void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback) {
         }
 
         /**
@@ -352,11 +356,11 @@
         default void onRecentsAnimationStateChanged(boolean running) { }
 
         /**
-         * @see IStatusBar#onSystemBarAttributesChanged.
+         * @see IStatusBar#onSystemBarAttributesChanged
          */
         default void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                @Behavior int behavior, @InsetsType int requestedVisibleTypes,
                 String packageName, LetterboxDetails[] letterboxDetails) { }
 
         /**
@@ -471,12 +475,18 @@
     }
 
     public CommandQueue(Context context) {
-        this(context, null, null);
+        this(context, null, null, null);
     }
 
-    public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) {
+    public CommandQueue(
+            Context context,
+            ProtoTracer protoTracer,
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
         mProtoTracer = protoTracer;
         mRegistry = registry;
+        mDumpHandler = dumpHandler;
         context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
         // We always have default display.
         setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
@@ -1007,9 +1017,9 @@
     }
 
     @Override
-    public void setUdfpsHbmListener(IUdfpsHbmListener listener) {
+    public void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback) {
         synchronized (mLock) {
-            mHandler.obtainMessage(MSG_SET_UDFPS_HBM_LISTENER, listener).sendToTarget();
+            mHandler.obtainMessage(MSG_SET_UDFPS_REFRESH_RATE_CALLBACK, callback).sendToTarget();
         }
     }
 
@@ -1080,7 +1090,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName,
+            @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails) {
         synchronized (mLock) {
             SomeArgs args = SomeArgs.obtain();
@@ -1089,7 +1099,7 @@
             args.argi3 = navbarColorManagedByIme ? 1 : 0;
             args.arg1 = appearanceRegions;
             args.argi4 = behavior;
-            args.arg2 = requestedVisibilities;
+            args.argi5 = requestedVisibleTypes;
             args.arg3 = packageName;
             args.arg4 = letterboxDetails;
             mHandler.obtainMessage(MSG_SYSTEM_BAR_CHANGED, args).sendToTarget();
@@ -1175,6 +1185,35 @@
     }
 
     @Override
+    public void dumpProto(String[] args, ParcelFileDescriptor pfd) {
+        final FileDescriptor fd = pfd.getFileDescriptor();
+        // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible
+        // to just throw this work onto the handler just like the other messages
+        Thread thr = new Thread("Sysui.dumpProto") {
+            public void run() {
+                try {
+                    if (mDumpHandler == null) {
+                        return;
+                    }
+                    // We won't be using the PrintWriter.
+                    OutputStream o = new OutputStream() {
+                        @Override
+                        public void write(int b) {}
+                    };
+                    mDumpHandler.dump(fd, new PrintWriter(o), args);
+                } finally {
+                    try {
+                        // Close the file descriptor so the TransferPipe finishes its thread
+                        pfd.close();
+                    } catch (Exception e) {
+                    }
+                }
+            }
+        };
+        thr.start();
+    }
+
+    @Override
     public void runGcForTest() {
         // Gc sysui
         GcUtils.runGcAndFinalizersSync();
@@ -1507,9 +1546,10 @@
                                 (IBiometricContextListener) msg.obj);
                     }
                     break;
-                case MSG_SET_UDFPS_HBM_LISTENER:
+                case MSG_SET_UDFPS_REFRESH_RATE_CALLBACK:
                     for (int i = 0; i < mCallbacks.size(); i++) {
-                        mCallbacks.get(i).setUdfpsHbmListener((IUdfpsHbmListener) msg.obj);
+                        mCallbacks.get(i).setUdfpsRefreshRateCallback(
+                                (IUdfpsRefreshRateRequestCallback) msg.obj);
                     }
                     break;
                 case MSG_SHOW_CHARGING_ANIMATION:
@@ -1542,8 +1582,7 @@
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).onSystemBarAttributesChanged(args.argi1, args.argi2,
                                 (AppearanceRegion[]) args.arg1, args.argi3 == 1, args.argi4,
-                                (InsetsVisibilities) args.arg2, (String) args.arg3,
-                                (LetterboxDetails[]) args.arg4);
+                                args.argi5, (String) args.arg3, (LetterboxDetails[]) args.arg4);
                     }
                     args.recycle();
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 073ab8b..0f27420 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -21,6 +21,7 @@
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
 import static android.hardware.biometrics.BiometricSourceType.FACE;
+import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
@@ -35,7 +36,7 @@
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_DISCLOSURE;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_LOGOUT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_OWNER_INFO;
-import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_RESTING;
+import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
@@ -50,12 +51,10 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
-import android.hardware.biometrics.BiometricFaceConstants;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.BatteryManager;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -64,22 +63,24 @@
 import android.os.UserManager;
 import android.text.TextUtils;
 import android.text.format.Formatter;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.ViewClippingUtil;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.TrustGrantFlags;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.settingslib.Utils;
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.R;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.FaceHelpMessageDeferral;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -123,7 +124,6 @@
 
     private static final String TAG = "KeyguardIndication";
     private static final boolean DEBUG_CHARGING_SPEED = false;
-    private static final boolean DEBUG = Build.IS_DEBUGGABLE;
 
     private static final int MSG_HIDE_TRANSIENT = 1;
     private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2;
@@ -138,6 +138,8 @@
     private final KeyguardStateController mKeyguardStateController;
     protected final StatusBarStateController mStatusBarStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final AuthController mAuthController;
+    private final KeyguardLogger mKeyguardLogger;
     private ViewGroup mIndicationArea;
     private KeyguardIndicationTextView mTopIndicationView;
     private KeyguardIndicationTextView mLockScreenIndicationView;
@@ -154,11 +156,12 @@
     private final AccessibilityManager mAccessibilityManager;
     private final Handler mHandler;
 
-    protected KeyguardIndicationRotateTextViewController mRotateTextViewController;
+    @VisibleForTesting
+    public KeyguardIndicationRotateTextViewController mRotateTextViewController;
     private BroadcastReceiver mBroadcastReceiver;
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
-    private String mRestingIndication;
+    private String mPersistentUnlockMessage;
     private String mAlignmentIndication;
     private CharSequence mTrustGrantedIndication;
     private CharSequence mTransientIndication;
@@ -188,27 +191,23 @@
     private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
 
     private boolean mDozing;
-    private final ViewClippingUtil.ClippingParameters mClippingParams =
-            new ViewClippingUtil.ClippingParameters() {
-                @Override
-                public boolean shouldFinish(View view) {
-                    return view == mIndicationArea;
-                }
-            };
-    private ScreenLifecycle mScreenLifecycle;
+    private final ScreenLifecycle mScreenLifecycle;
     private final ScreenLifecycle.Observer mScreenObserver =
             new ScreenLifecycle.Observer() {
         @Override
         public void onScreenTurnedOn() {
             mHandler.removeMessages(MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON);
             if (mBiometricErrorMessageToShowOnScreenOn != null) {
-                showBiometricMessage(mBiometricErrorMessageToShowOnScreenOn);
+                String followUpMessage = mFaceLockedOutThisAuthSession
+                        ? faceLockedOutFollowupMessage() : null;
+                showBiometricMessage(mBiometricErrorMessageToShowOnScreenOn, followUpMessage);
                 // We want to keep this message around in case the screen was off
                 hideBiometricMessageDelayed(DEFAULT_HIDE_DELAY_MS);
                 mBiometricErrorMessageToShowOnScreenOn = null;
             }
         }
     };
+    private boolean mFaceLockedOutThisAuthSession;
 
     /**
      * Creates a new KeyguardIndicationController and registers callbacks.
@@ -229,11 +228,13 @@
             @Main DelayableExecutor executor,
             @Background DelayableExecutor bgExecutor,
             FalsingManager falsingManager,
+            AuthController authController,
             LockPatternUtils lockPatternUtils,
             ScreenLifecycle screenLifecycle,
             KeyguardBypassController keyguardBypassController,
             AccessibilityManager accessibilityManager,
-            FaceHelpMessageDeferral faceHelpMessageDeferral) {
+            FaceHelpMessageDeferral faceHelpMessageDeferral,
+            KeyguardLogger keyguardLogger) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mDevicePolicyManager = devicePolicyManager;
@@ -248,10 +249,12 @@
         mExecutor = executor;
         mBackgroundExecutor = bgExecutor;
         mLockPatternUtils = lockPatternUtils;
+        mAuthController = authController;
         mFalsingManager = falsingManager;
         mKeyguardBypassController = keyguardBypassController;
         mAccessibilityManager = accessibilityManager;
         mScreenLifecycle = screenLifecycle;
+        mKeyguardLogger = keyguardLogger;
         mScreenLifecycle.addObserver(mScreenObserver);
 
         mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
@@ -376,7 +379,7 @@
         updateLockScreenTrustMsg(userId, getTrustGrantedIndication(), getTrustManagedIndication());
         updateLockScreenAlignmentMsg();
         updateLockScreenLogoutView();
-        updateLockScreenRestingMsg();
+        updateLockScreenPersistentUnlockMsg();
     }
 
     private void updateOrganizedOwnedDevice() {
@@ -482,7 +485,8 @@
     }
 
     private void updateLockScreenUserLockedMsg(int userId) {
-        if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)) {
+        if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)
+                || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) {
             mRotateTextViewController.updateIndication(
                     INDICATION_TYPE_USER_LOCKED,
                     new KeyguardIndication.Builder()
@@ -587,18 +591,17 @@
         }
     }
 
-    private void updateLockScreenRestingMsg() {
-        if (!TextUtils.isEmpty(mRestingIndication)
-                && !mRotateTextViewController.hasIndications()) {
+    private void updateLockScreenPersistentUnlockMsg() {
+        if (!TextUtils.isEmpty(mPersistentUnlockMessage)) {
             mRotateTextViewController.updateIndication(
-                    INDICATION_TYPE_RESTING,
+                    INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE,
                     new KeyguardIndication.Builder()
-                            .setMessage(mRestingIndication)
+                            .setMessage(mPersistentUnlockMessage)
                             .setTextColor(mInitialTextColorState)
                             .build(),
-                    false);
+                    true);
         } else {
-            mRotateTextViewController.hideIndication(INDICATION_TYPE_RESTING);
+            mRotateTextViewController.hideIndication(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE);
         }
     }
 
@@ -619,7 +622,6 @@
                                 if (mFalsingManager.isFalseTap(LOW_PENALTY)) {
                                     return;
                                 }
-                                int currentUserId = getCurrentUser();
                                 mDevicePolicyManager.logoutUser();
                             })
                             .build(),
@@ -676,17 +678,14 @@
                 hideTransientIndication();
             }
             updateDeviceEntryIndication(false);
-        } else if (!visible) {
+        } else {
             // If we unlock and return to keyguard quickly, previous error should not be shown
             hideTransientIndication();
         }
     }
 
-    /**
-     * Sets the indication that is shown if nothing else is showing.
-     */
-    public void setRestingIndication(String restingIndication) {
-        mRestingIndication = restingIndication;
+    private void setPersistentUnlockMessage(String persistentUnlockMessage) {
+        mPersistentUnlockMessage = persistentUnlockMessage;
         updateDeviceEntryIndication(false);
     }
 
@@ -764,7 +763,7 @@
      * logic.
      */
     private void showBiometricMessage(CharSequence biometricMessage,
-            CharSequence biometricMessageFollowUp) {
+            @Nullable CharSequence biometricMessageFollowUp) {
         if (TextUtils.equals(biometricMessage, mBiometricMessage)) {
             return;
         }
@@ -929,7 +928,7 @@
         }
 
         if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
-            if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
+            if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
                 return; // udfps affordance is highlighted, no need to show action to unlock
             } else if (mKeyguardUpdateMonitor.isFaceEnrolled()) {
                 String message = mContext.getString(R.string.keyguard_retry);
@@ -1028,7 +1027,7 @@
                 mChargingTimeRemaining = mPowerPluggedIn
                         ? mBatteryInfo.computeChargeTimeRemaining() : -1;
             } catch (RemoteException e) {
-                Log.e(TAG, "Error calling IBatteryStats: ", e);
+                mKeyguardLogger.logException(e, "Error calling IBatteryStats");
                 mChargingTimeRemaining = -1;
             }
             updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired);
@@ -1072,17 +1071,14 @@
                     && msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
             final boolean faceAuthFailed = biometricSourceType == FACE
                     && msgId == BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; // ran through matcher & failed
-            final boolean isUnlockWithFingerprintPossible =
-                    mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
-                            getCurrentUser());
+            final boolean isUnlockWithFingerprintPossible = canUnlockWithFingerprint();
             final boolean isCoExFaceAcquisitionMessage =
                     faceAuthSoftError && isUnlockWithFingerprintPossible;
             if (isCoExFaceAcquisitionMessage && !mCoExFaceAcquisitionMsgIdsToShow.contains(msgId)) {
-                if (DEBUG) {
-                    Log.d(TAG, "skip showing msgId=" + msgId + " helpString=" + helpString
-                            + ", due to co-ex logic");
-                }
-                return;
+                mKeyguardLogger.logBiometricMessage(
+                        "skipped showing help message due to co-ex logic",
+                        msgId,
+                        helpString);
             } else if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                 mStatusBarKeyguardViewManager.setKeyguardMessage(helpString,
                         mInitialTextColorState);
@@ -1120,74 +1116,49 @@
         }
 
         @Override
-        public void onBiometricError(int msgId, String errString,
-                BiometricSourceType biometricSourceType) {
-            CharSequence deferredFaceMessage = null;
-            if (biometricSourceType == FACE) {
-                if (msgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT) {
-                    deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
-                    if (DEBUG) {
-                        Log.d(TAG, "showDeferredFaceMessage msgId=" + deferredFaceMessage);
-                    }
-                }
-                mFaceAcquiredMessageDeferral.reset();
-            }
-
-            if (shouldSuppressBiometricError(msgId, biometricSourceType, mKeyguardUpdateMonitor)) {
-                if (DEBUG) {
-                    Log.d(TAG, "suppressingBiometricError msgId=" + msgId
-                            + " source=" + biometricSourceType);
-                }
-            } else if (biometricSourceType == FACE && msgId == FaceManager.FACE_ERROR_TIMEOUT) {
-                // Co-ex: show deferred message OR nothing
-                if (mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
-                        KeyguardUpdateMonitor.getCurrentUser())) {
-                    // if we're on the lock screen (bouncer isn't showing), show the deferred msg
-                    if (deferredFaceMessage != null
-                            && !mStatusBarKeyguardViewManager.isBouncerShowing()) {
-                        showBiometricMessage(
-                                deferredFaceMessage,
-                                mContext.getString(R.string.keyguard_suggest_fingerprint)
-                        );
-                        return;
-                    }
-
-                    // otherwise, don't show any message
-                    if (DEBUG) {
-                        Log.d(TAG, "skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
-                    }
-                    return;
-                }
-
-                // Face-only: The face timeout message is not very actionable, let's ask the user to
-                // manually retry.
-                if (deferredFaceMessage != null) {
-                    showBiometricMessage(
-                            deferredFaceMessage,
-                            mContext.getString(R.string.keyguard_unlock)
-                    );
-                } else {
-                    // suggest swiping up to unlock (try face auth again or swipe up to bouncer)
-                    showActionToUnlock();
-                }
-            } else if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
-                mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState);
-            } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
-                showBiometricMessage(errString);
-            } else {
-                mBiometricErrorMessageToShowOnScreenOn = errString;
+        public void onLockedOutStateChanged(BiometricSourceType biometricSourceType) {
+            if (biometricSourceType == FACE && !mKeyguardUpdateMonitor.isFaceLockedOut()) {
+                mFaceLockedOutThisAuthSession = false;
+            } else if (biometricSourceType == FINGERPRINT) {
+                setPersistentUnlockMessage(mKeyguardUpdateMonitor.isFingerprintLockedOut()
+                        ? mContext.getString(R.string.keyguard_unlock) : "");
             }
         }
 
-        private boolean shouldSuppressBiometricError(int msgId,
-                BiometricSourceType biometricSourceType, KeyguardUpdateMonitor updateMonitor) {
-            if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
-                return shouldSuppressFingerprintError(msgId, updateMonitor);
-            }
+        @Override
+        public void onBiometricError(int msgId, String errString,
+                BiometricSourceType biometricSourceType) {
             if (biometricSourceType == FACE) {
-                return shouldSuppressFaceError(msgId, updateMonitor);
+                onFaceAuthError(msgId, errString);
+            } else if (biometricSourceType == FINGERPRINT) {
+                onFingerprintAuthError(msgId, errString);
             }
-            return false;
+        }
+
+        private void onFaceAuthError(int msgId, String errString) {
+            CharSequence deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
+            mFaceAcquiredMessageDeferral.reset();
+            if (shouldSuppressFaceError(msgId, mKeyguardUpdateMonitor)) {
+                mKeyguardLogger.logBiometricMessage("suppressingFaceError", msgId, errString);
+                return;
+            }
+            if (msgId == FaceManager.FACE_ERROR_TIMEOUT) {
+                handleFaceAuthTimeoutError(deferredFaceMessage);
+            } else if (isLockoutError(msgId)) {
+                handleFaceLockoutError(errString);
+            } else {
+                showErrorMessageNowOrLater(errString, null);
+            }
+        }
+
+        private void onFingerprintAuthError(int msgId, String errString) {
+            if (shouldSuppressFingerprintError(msgId, mKeyguardUpdateMonitor)) {
+                mKeyguardLogger.logBiometricMessage("suppressingFingerprintError",
+                        msgId,
+                        errString);
+            } else {
+                showErrorMessageNowOrLater(errString, null);
+            }
         }
 
         private boolean shouldSuppressFingerprintError(int msgId,
@@ -1197,7 +1168,7 @@
             // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the
             // check of whether non-strong biometric is allowed
             return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
-                    && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
+                    && !isLockoutError(msgId))
                     || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
                     || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED
                     || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED);
@@ -1217,16 +1188,14 @@
 
         @Override
         public void onTrustChanged(int userId) {
-            if (getCurrentUser() != userId) {
-                return;
-            }
+            if (!isCurrentUser(userId)) return;
             updateDeviceEntryIndication(false);
         }
 
         @Override
-        public void showTrustGrantedMessage(CharSequence message) {
-            mTrustGrantedIndication = message;
-            updateDeviceEntryIndication(false);
+        public void onTrustGrantedForCurrentUser(boolean dismissKeyguard,
+                @NonNull TrustGrantFlags flags, @Nullable String message) {
+            showTrustGrantedMessage(dismissKeyguard, message);
         }
 
         @Override
@@ -1286,7 +1255,92 @@
         }
     }
 
-    private StatusBarStateController.StateListener mStatusBarStateListener =
+    private boolean isCurrentUser(int userId) {
+        return getCurrentUser() == userId;
+    }
+
+    protected void showTrustGrantedMessage(boolean dismissKeyguard, @Nullable String message) {
+        mTrustGrantedIndication = message;
+        updateDeviceEntryIndication(false);
+    }
+
+    private void handleFaceLockoutError(String errString) {
+        String followupMessage = faceLockedOutFollowupMessage();
+        // Lockout error can happen multiple times in a session because we trigger face auth
+        // even when it is locked out so that the user is aware that face unlock would have
+        // triggered but didn't because it is locked out.
+
+        // On first lockout we show the error message from FaceManager, which tells the user they
+        // had too many unsuccessful attempts.
+        if (!mFaceLockedOutThisAuthSession) {
+            mFaceLockedOutThisAuthSession = true;
+            showErrorMessageNowOrLater(errString, followupMessage);
+        } else if (!mAuthController.isUdfpsFingerDown()) {
+            // On subsequent lockouts, we show a more generic locked out message.
+            showErrorMessageNowOrLater(
+                    mContext.getString(R.string.keyguard_face_unlock_unavailable),
+                    followupMessage);
+        }
+    }
+
+    private String faceLockedOutFollowupMessage() {
+        int followupMsgId = canUnlockWithFingerprint() ? R.string.keyguard_suggest_fingerprint
+                : R.string.keyguard_unlock;
+        return mContext.getString(followupMsgId);
+    }
+
+    private static boolean isLockoutError(int msgId) {
+        return msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
+                || msgId == FaceManager.FACE_ERROR_LOCKOUT;
+    }
+
+    private void handleFaceAuthTimeoutError(@Nullable CharSequence deferredFaceMessage) {
+        mKeyguardLogger.logBiometricMessage("deferred message after face auth timeout",
+                null, String.valueOf(deferredFaceMessage));
+        if (canUnlockWithFingerprint()) {
+            // Co-ex: show deferred message OR nothing
+            // if we're on the lock screen (bouncer isn't showing), show the deferred msg
+            if (deferredFaceMessage != null
+                    && !mStatusBarKeyguardViewManager.isBouncerShowing()) {
+                showBiometricMessage(
+                        deferredFaceMessage,
+                        mContext.getString(R.string.keyguard_suggest_fingerprint)
+                );
+            } else {
+                // otherwise, don't show any message
+                mKeyguardLogger.logBiometricMessage(
+                        "skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
+            }
+        } else if (deferredFaceMessage != null) {
+            // Face-only: The face timeout message is not very actionable, let's ask the
+            // user to manually retry.
+            showBiometricMessage(
+                    deferredFaceMessage,
+                    mContext.getString(R.string.keyguard_unlock)
+            );
+        } else {
+            // Face-only
+            // suggest swiping up to unlock (try face auth again or swipe up to bouncer)
+            showActionToUnlock();
+        }
+    }
+
+    private boolean canUnlockWithFingerprint() {
+        return mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
+                KeyguardUpdateMonitor.getCurrentUser());
+    }
+
+    private void showErrorMessageNowOrLater(String errString, @Nullable String followUpMsg) {
+        if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+            mStatusBarKeyguardViewManager.setKeyguardMessage(errString, mInitialTextColorState);
+        } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
+            showBiometricMessage(errString, followUpMsg);
+        } else {
+            mBiometricErrorMessageToShowOnScreenOn = errString;
+        }
+    }
+
+    private final StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
         @Override
         public void onStateChanged(int newState) {
@@ -1307,7 +1361,7 @@
         }
     };
 
-    private KeyguardStateController.Callback mKeyguardStateCallback =
+    private final KeyguardStateController.Callback mKeyguardStateCallback =
             new KeyguardStateController.Callback() {
         @Override
         public void onUnlockedChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 9d2750f..bc456d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -18,6 +18,8 @@
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold
 import com.android.systemui.util.getColorWithAlpha
+import com.android.systemui.util.leak.RotationUtils
+import com.android.systemui.util.leak.RotationUtils.Rotation
 import java.util.function.Consumer
 
 /**
@@ -67,22 +69,19 @@
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         val interpolatedAmount = INTERPOLATOR.getInterpolation(amount)
         val ovalWidthIncreaseAmount =
-                getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
+            getPercentPastThreshold(interpolatedAmount, WIDEN_OVAL_THRESHOLD)
 
         val initialWidthMultiplier = (1f - OVAL_INITIAL_WIDTH_PERCENT) / 2f
 
         with(scrim) {
-            revealGradientEndColorAlpha = 1f - getPercentPastThreshold(
-                    amount, FADE_END_COLOR_OUT_THRESHOLD)
+            revealGradientEndColorAlpha =
+                1f - getPercentPastThreshold(amount, FADE_END_COLOR_OUT_THRESHOLD)
             setRevealGradientBounds(
-                    scrim.width * initialWidthMultiplier +
-                            -scrim.width * ovalWidthIncreaseAmount,
-                    scrim.height * OVAL_INITIAL_TOP_PERCENT -
-                            scrim.height * interpolatedAmount,
-                    scrim.width * (1f - initialWidthMultiplier) +
-                            scrim.width * ovalWidthIncreaseAmount,
-                    scrim.height * OVAL_INITIAL_BOTTOM_PERCENT +
-                            scrim.height * interpolatedAmount)
+                scrim.width * initialWidthMultiplier + -scrim.width * ovalWidthIncreaseAmount,
+                scrim.height * OVAL_INITIAL_TOP_PERCENT - scrim.height * interpolatedAmount,
+                scrim.width * (1f - initialWidthMultiplier) + scrim.width * ovalWidthIncreaseAmount,
+                scrim.height * OVAL_INITIAL_BOTTOM_PERCENT + scrim.height * interpolatedAmount
+            )
         }
     }
 }
@@ -97,12 +96,17 @@
         scrim.interpolatedRevealAmount = interpolatedAmount
 
         scrim.startColorAlpha =
-            getPercentPastThreshold(1 - interpolatedAmount,
-                threshold = 1 - START_COLOR_REVEAL_PERCENTAGE)
+            getPercentPastThreshold(
+                1 - interpolatedAmount,
+                threshold = 1 - START_COLOR_REVEAL_PERCENTAGE
+            )
 
         scrim.revealGradientEndColorAlpha =
-            1f - getPercentPastThreshold(interpolatedAmount,
-                threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE)
+            1f -
+                getPercentPastThreshold(
+                    interpolatedAmount,
+                    threshold = REVEAL_GRADIENT_END_COLOR_ALPHA_START_PERCENTAGE
+                )
 
         // Start changing gradient bounds later to avoid harsh gradient in the beginning
         val gradientBoundsAmount = lerp(GRADIENT_START_BOUNDS_PERCENTAGE, 1.0f, interpolatedAmount)
@@ -179,7 +183,7 @@
      */
     private val OFF_SCREEN_START_AMOUNT = 0.05f
 
-    private val WIDTH_INCREASE_MULTIPLIER = 1.25f
+    private val INCREASE_MULTIPLIER = 1.25f
 
     override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
         val interpolatedAmount = Interpolators.FAST_OUT_SLOW_IN_REVERSE.getInterpolation(amount)
@@ -188,15 +192,36 @@
         with(scrim) {
             revealGradientEndColorAlpha = 1f - fadeAmount
             interpolatedRevealAmount = interpolatedAmount
-            setRevealGradientBounds(
+            @Rotation val rotation = RotationUtils.getRotation(scrim.getContext())
+            if (rotation == RotationUtils.ROTATION_NONE) {
+                setRevealGradientBounds(
                     width * (1f + OFF_SCREEN_START_AMOUNT) -
-                            width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
-                    powerButtonY -
-                            height * interpolatedAmount,
+                        width * INCREASE_MULTIPLIER * interpolatedAmount,
+                    powerButtonY - height * interpolatedAmount,
                     width * (1f + OFF_SCREEN_START_AMOUNT) +
-                            width * WIDTH_INCREASE_MULTIPLIER * interpolatedAmount,
-                    powerButtonY +
-                            height * interpolatedAmount)
+                        width * INCREASE_MULTIPLIER * interpolatedAmount,
+                    powerButtonY + height * interpolatedAmount
+                )
+            } else if (rotation == RotationUtils.ROTATION_LANDSCAPE) {
+                setRevealGradientBounds(
+                    powerButtonY - width * interpolatedAmount,
+                    (-height * OFF_SCREEN_START_AMOUNT) -
+                        height * INCREASE_MULTIPLIER * interpolatedAmount,
+                    powerButtonY + width * interpolatedAmount,
+                    (-height * OFF_SCREEN_START_AMOUNT) +
+                        height * INCREASE_MULTIPLIER * interpolatedAmount
+                )
+            } else {
+                // RotationUtils.ROTATION_SEASCAPE
+                setRevealGradientBounds(
+                    (width - powerButtonY) - width * interpolatedAmount,
+                    height * (1f + OFF_SCREEN_START_AMOUNT) -
+                        height * INCREASE_MULTIPLIER * interpolatedAmount,
+                    (width - powerButtonY) + width * interpolatedAmount,
+                    height * (1f + OFF_SCREEN_START_AMOUNT) +
+                        height * INCREASE_MULTIPLIER * interpolatedAmount
+                )
+            }
         }
     }
 }
@@ -208,9 +233,7 @@
  */
 class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
 
-    /**
-     * Listener that is called if the scrim's opaqueness changes
-     */
+    /** Listener that is called if the scrim's opaqueness changes */
     lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
 
     /**
@@ -224,8 +247,11 @@
 
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
-                Trace.traceCounter(Trace.TRACE_TAG_APP, "light_reveal_amount",
-                        (field * 100).toInt())
+                Trace.traceCounter(
+                    Trace.TRACE_TAG_APP,
+                    "light_reveal_amount",
+                    (field * 100).toInt()
+                )
                 invalidate()
             }
         }
@@ -250,10 +276,10 @@
 
     /**
      * Alpha of the fill that can be used in the beginning of the animation to hide the content.
-     * Normally the gradient bounds are animated from small size so the content is not visible,
-     * but if the start gradient bounds allow to see some content this could be used to make the
-     * reveal smoother. It can help to add fade in effect in the beginning of the animation.
-     * The color of the fill is determined by [revealGradientEndColor].
+     * Normally the gradient bounds are animated from small size so the content is not visible, but
+     * if the start gradient bounds allow to see some content this could be used to make the reveal
+     * smoother. It can help to add fade in effect in the beginning of the animation. The color of
+     * the fill is determined by [revealGradientEndColor].
      *
      * 0 - no fill and content is visible, 1 - the content is covered with the start color
      */
@@ -281,9 +307,7 @@
             }
         }
 
-    /**
-     * Is the scrim currently fully opaque
-     */
+    /** Is the scrim currently fully opaque */
     var isScrimOpaque = false
         private set(value) {
             if (field != value) {
@@ -318,16 +342,22 @@
      * Paint used to draw a transparent-to-white radial gradient. This will be scaled and translated
      * via local matrix in [onDraw] so we never need to construct a new shader.
      */
-    private val gradientPaint = Paint().apply {
-        shader = RadialGradient(
-                0f, 0f, 1f,
-                intArrayOf(Color.TRANSPARENT, Color.WHITE), floatArrayOf(0f, 1f),
-                Shader.TileMode.CLAMP)
+    private val gradientPaint =
+        Paint().apply {
+            shader =
+                RadialGradient(
+                    0f,
+                    0f,
+                    1f,
+                    intArrayOf(Color.TRANSPARENT, Color.WHITE),
+                    floatArrayOf(0f, 1f),
+                    Shader.TileMode.CLAMP
+                )
 
-        // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same
-        // window, rather than outright replacing them.
-        xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
-    }
+            // SRC_OVER ensures that we draw the semitransparent pixels over other views in the same
+            // window, rather than outright replacing them.
+            xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
+        }
 
     /**
      * Matrix applied to [gradientPaint]'s RadialGradient shader to move the gradient to
@@ -347,8 +377,8 @@
      * simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
      * [revealGradientHeight] for you.
      *
-     * This method does not call [invalidate] - you should do so once you're done changing
-     * properties.
+     * This method does not call [invalidate]
+     * - you should do so once you're done changing properties.
      */
     fun setRevealGradientBounds(left: Float, top: Float, right: Float, bottom: Float) {
         revealGradientWidth = right - left
@@ -359,8 +389,12 @@
     }
 
     override fun onDraw(canvas: Canvas?) {
-        if (canvas == null || revealGradientWidth <= 0 || revealGradientHeight <= 0 ||
-            revealAmount == 0f) {
+        if (
+            canvas == null ||
+                revealGradientWidth <= 0 ||
+                revealGradientHeight <= 0 ||
+                revealAmount == 0f
+        ) {
             if (revealAmount < 1f) {
                 canvas?.drawColor(revealGradientEndColor)
             }
@@ -383,8 +417,10 @@
     }
 
     private fun setPaintColorFilter() {
-        gradientPaint.colorFilter = PorterDuffColorFilter(
-            getColorWithAlpha(revealGradientEndColor, revealGradientEndColorAlpha),
-            PorterDuff.Mode.MULTIPLY)
+        gradientPaint.colorFilter =
+            PorterDuffColorFilter(
+                getColorWithAlpha(revealGradientEndColor, revealGradientEndColorAlpha),
+                PorterDuff.Mode.MULTIPLY
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
index 886ad68..5fb5002 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
@@ -5,7 +5,7 @@
 import android.util.MathUtils
 import com.android.systemui.R
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.shade.NotificationPanelViewController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import dagger.assisted.Assisted
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 8006931..b8302d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
@@ -663,7 +663,7 @@
         } else {
             pulseHeight = height
             val overflow = nsslController.setPulseHeight(height)
-            notificationPanelController.setOverStrechAmount(overflow)
+            notificationPanelController.setOverStretchAmount(overflow)
             val transitionHeight = if (keyguardBypassController.bypassEnabled) height else 0.0f
             transitionToShadeAmountCommon(transitionHeight)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java
new file mode 100644
index 0000000..39d7d66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsController.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+import com.android.systemui.shade.NotificationShadeWindowView;
+
+/**
+ * Calculates insets for the notification shade window view.
+ */
+public abstract class NotificationInsetsController
+        implements NotificationShadeWindowView.LayoutInsetsController {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java
new file mode 100644
index 0000000..1ed704e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsImpl.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import static android.view.WindowInsets.Type.systemBars;
+
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.util.Pair;
+import android.view.DisplayCutout;
+import android.view.WindowInsets;
+
+import com.android.systemui.dagger.SysUISingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Default implementation of NotificationsInsetsController.
+ */
+@SysUISingleton
+public class NotificationInsetsImpl extends NotificationInsetsController {
+
+    @Inject
+    public NotificationInsetsImpl() {
+
+    }
+
+    @Override
+    public Pair<Integer, Integer> getinsets(@Nullable WindowInsets windowInsets,
+            @Nullable DisplayCutout displayCutout) {
+        final Insets insets = windowInsets.getInsetsIgnoringVisibility(systemBars());
+        int leftInset = 0;
+        int rightInset = 0;
+
+        if (displayCutout != null) {
+            leftInset = displayCutout.getSafeInsetLeft();
+            rightInset = displayCutout.getSafeInsetRight();
+        }
+        leftInset = Math.max(insets.left, leftInset);
+        rightInset = Math.max(insets.right, rightInset);
+
+        return new Pair(leftInset, rightInset);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java
new file mode 100644
index 0000000..614bc0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInsetsModule.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.statusbar;
+
+import com.android.systemui.dagger.SysUISingleton;
+
+import dagger.Binds;
+import dagger.Module;
+
+@Module
+public interface NotificationInsetsModule {
+
+    @Binds
+    @SysUISingleton
+    NotificationInsetsController bindNotificationInsetsController(NotificationInsetsImpl impl);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 184dc25..cdefae6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -19,7 +19,6 @@
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
 
-import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.admin.DevicePolicyManager;
@@ -50,6 +49,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
@@ -93,6 +93,7 @@
     private final SparseBooleanArray mUsersInLockdownLatestResult = new SparseBooleanArray();
     private final SparseBooleanArray mShouldHideNotifsLatestResult = new SparseBooleanArray();
     private final UserManager mUserManager;
+    private final UserTracker mUserTracker;
     private final List<UserChangedListener> mListeners = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final NotificationClickNotifier mClickNotifier;
@@ -195,6 +196,7 @@
             BroadcastDispatcher broadcastDispatcher,
             DevicePolicyManager devicePolicyManager,
             UserManager userManager,
+            UserTracker userTracker,
             Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
             Lazy<CommonNotifCollection> commonNotifCollectionLazy,
             NotificationClickNotifier clickNotifier,
@@ -210,7 +212,8 @@
         mMainHandler = mainHandler;
         mDevicePolicyManager = devicePolicyManager;
         mUserManager = userManager;
-        mCurrentUserId = ActivityManager.getCurrentUser();
+        mUserTracker = userTracker;
+        mCurrentUserId = mUserTracker.getUserId();
         mVisibilityProviderLazy = visibilityProviderLazy;
         mCommonNotifCollectionLazy = commonNotifCollectionLazy;
         mClickNotifier = clickNotifier;
@@ -295,7 +298,7 @@
         mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null,
                 Context.RECEIVER_EXPORTED_UNAUDITED);
 
-        mCurrentUserId = ActivityManager.getCurrentUser(); // in case we reg'd receiver too late
+        mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
         updateCurrentProfilesCache();
 
         mSettingsObserver.onChange(false);  // set up
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 4be5a1a..ced725e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -48,9 +48,9 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.SmartspaceMediaData;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 0c9e1ec..0b1807d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -92,9 +92,6 @@
     /** Sets the state of whether the keyguard is fading away or not. */
     default void setKeyguardFadingAway(boolean keyguardFadingAway) {}
 
-    /** Sets the state of whether the quick settings is expanded or not. */
-    default void setQsExpanded(boolean expanded) {}
-
     /** Sets the state of whether the user activities are forced or not. */
     default void setForceUserActivity(boolean forceUserActivity) {}
 
@@ -126,9 +123,6 @@
     /** Sets whether the window was collapsed by force or not. */
     default void setForceWindowCollapsed(boolean force) {}
 
-    /** Sets whether panel is expanded or not. */
-    default void setPanelExpanded(boolean isExpanded) {}
-
     /** Gets whether the panel is expanded or not. */
     default boolean getPanelExpanded() {
         return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index f961984..d7eddf5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -40,6 +40,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -70,6 +71,7 @@
     private int[] mTmp = new int[2];
     private boolean mHideBackground;
     private int mStatusBarHeight;
+    private boolean mEnableNotificationClipping;
     private AmbientState mAmbientState;
     private NotificationStackScrollLayoutController mHostLayoutController;
     private int mPaddingBetweenElements;
@@ -110,13 +112,13 @@
         setClipChildren(false);
         setClipToPadding(false);
         mShelfIcons.setIsStaticLayout(false);
-        setBottomRoundness(1.0f, false /* animate */);
-        setTopRoundness(1f, false /* animate */);
+        requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
+        requestTopRoundness(1f, false, SourceType.DefaultValue);
 
         // Setting this to first in section to get the clipping to the top roundness correct. This
         // value determines the way we are clipping to the top roundness of the overall shade
         setFirstInSection(true);
-        initDimens();
+        updateResources();
     }
 
     public void bind(AmbientState ambientState,
@@ -125,14 +127,17 @@
         mHostLayoutController = hostLayoutController;
     }
 
-    private void initDimens() {
+    private void updateResources() {
         Resources res = getResources();
         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
 
         ViewGroup.LayoutParams layoutParams = getLayoutParams();
-        layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
-        setLayoutParams(layoutParams);
+        final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
+        if (newShelfHeight != layoutParams.height) {
+            layoutParams.height = newShelfHeight;
+            setLayoutParams(layoutParams);
+        }
 
         final int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
         mShelfIcons.setPadding(padding, 0, padding, 0);
@@ -140,6 +145,7 @@
         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
         mCornerAnimationDistance = res.getDimensionPixelSize(
                 R.dimen.notification_corner_animation_distance);
+        mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
 
         mShelfIcons.setInNotificationIconShelf(true);
         if (!mShowNotificationShelf) {
@@ -150,7 +156,7 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        initDimens();
+        updateResources();
     }
 
     @Override
@@ -413,7 +419,7 @@
                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
                         // only if the first icon is fully in the shelf we want to clip to it!
                         backgroundTop = (int) (child.getTranslationY() - getTranslationY());
-                        firstElementRoundness = expandableRow.getCurrentTopRoundness();
+                        firstElementRoundness = expandableRow.getTopRoundness();
                     }
                 }
 
@@ -495,9 +501,6 @@
             return;
         }
 
-        final float smallCornerRadius =
-                getResources().getDimension(R.dimen.notification_corner_radius_small)
-                /  getResources().getDimension(R.dimen.notification_corner_radius);
         final float viewEnd = viewStart + anv.getActualHeight();
         final float cornerAnimationDistance = mCornerAnimationDistance
                 * mAmbientState.getExpansionFraction();
@@ -507,28 +510,36 @@
             // Round bottom corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    /* value = */ anv.isLastInSection() ? 1f : changeFraction,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
 
         } else if (viewEnd < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    /* value = */ anv.isLastInSection() ? 1f : 0f,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
         }
 
         if (viewStart >= cornerAnimationTop) {
             // Round top corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    /* value = */ anv.isFirstInSection() ? 1f : changeFraction,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
 
         } else if (viewStart < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    /* value = */ anv.isFirstInSection() ? 1f : 0f,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
         }
     }
 
@@ -630,7 +641,8 @@
         }
         if (!isPinned) {
             if (viewEnd > notificationClipEnd && !shouldClipOwnTop) {
-                int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
+                int clipBottomAmount =
+                        mEnableNotificationClipping ? (int) (viewEnd - notificationClipEnd) : 0;
                 view.setClipBottomAmount(clipBottomAmount);
             } else {
                 view.setClipBottomAmount(0);
@@ -799,7 +811,7 @@
         iconState.hidden = isAppearing
                 || (view instanceof ExpandableNotificationRow
                 && ((ExpandableNotificationRow) view).isLowPriority()
-                && mShelfIcons.hasMaxNumDot())
+                && mShelfIcons.areIconsOverflowing())
                 || (transitionAmount == 0.0f && !iconState.isAnimating(icon))
                 || row.isAboveShelf()
                 || row.showingPulsing()
@@ -967,6 +979,16 @@
         mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf);
     }
 
+    /**
+     * This method resets the OnScroll roundness of a view to 0f
+     *
+     * Note: This should be the only class that handles roundness {@code SourceType.OnScroll}
+     */
+    public static void resetOnScrollRoundness(ExpandableView expandableView) {
+        expandableView.requestTopRoundness(0f, false, SourceType.OnScroll);
+        expandableView.requestBottomRoundness(0f, false, SourceType.OnScroll);
+    }
+
     public class ShelfState extends ExpandableViewState {
         private boolean hasItemsInStableShelf;
         private ExpandableView firstViewInShelf;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index 8222c9d..c630feb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -68,6 +69,7 @@
     configurationController: ConfigurationController,
     private val statusBarStateController: StatusBarStateController,
     private val falsingManager: FalsingManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
     private val falsingCollector: FalsingCollector,
     dumpManager: DumpManager
@@ -126,6 +128,13 @@
                 initResources(context)
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+            }
+        }
+
         mPowerManager = context.getSystemService(PowerManager::class.java)
         dumpManager.registerDumpable(this)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index c04bc82..58ce447 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar;
 
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
@@ -34,9 +32,10 @@
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InsetsFlags;
-import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.ViewDebug;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 import android.view.animation.Interpolator;
@@ -54,6 +53,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.policy.CallbackController;
 
@@ -153,13 +153,18 @@
     private Interpolator mDozeInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
     @Inject
-    public StatusBarStateControllerImpl(UiEventLogger uiEventLogger, DumpManager dumpManager,
-            InteractionJankMonitor interactionJankMonitor) {
+    public StatusBarStateControllerImpl(
+            UiEventLogger uiEventLogger,
+            DumpManager dumpManager,
+            InteractionJankMonitor interactionJankMonitor,
+            ShadeExpansionStateManager shadeExpansionStateManager
+    ) {
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitor = interactionJankMonitor;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         dumpManager.registerDumpable(this);
     }
@@ -263,21 +268,6 @@
     }
 
     @Override
-    public boolean setPanelExpanded(boolean expanded) {
-        if (mIsExpanded == expanded) {
-            return false;
-        }
-        mIsExpanded = expanded;
-        String tag = getClass().getSimpleName() + "#setIsExpanded";
-        DejankUtils.startDetectingBlockingIpcs(tag);
-        for (RankedListener rl : new ArrayList<>(mListeners)) {
-            rl.mListener.onExpandedChanged(mIsExpanded);
-        }
-        DejankUtils.stopDetectingBlockingIpcs(tag);
-        return true;
-    }
-
-    @Override
     public float getInterpolatedDozeAmount() {
         return mDozeInterpolator.getInterpolation(mDozeAmount);
     }
@@ -325,6 +315,18 @@
         }
     }
 
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mIsExpanded != isExpanded) {
+            mIsExpanded = isExpanded;
+            String tag = getClass().getSimpleName() + "#setIsExpanded";
+            DejankUtils.startDetectingBlockingIpcs(tag);
+            for (RankedListener rl : new ArrayList<>(mListeners)) {
+                rl.mListener.onExpandedChanged(mIsExpanded);
+            }
+            DejankUtils.stopDetectingBlockingIpcs(tag);
+        }
+    }
+
     private void startDozeAnimation() {
         if (mDozeAmount == 0f || mDozeAmount == 1f) {
             mDozeInterpolator = mIsDozing
@@ -497,9 +499,9 @@
 
     @Override
     public void setSystemBarAttributes(@Appearance int appearance, @Behavior int behavior,
-            InsetsVisibilities requestedVisibilities, String packageName) {
-        boolean isFullscreen = !requestedVisibilities.getVisibility(ITYPE_STATUS_BAR)
-                || !requestedVisibilities.getVisibility(ITYPE_NAVIGATION_BAR);
+            @InsetsType int requestedVisibleTypes, String packageName) {
+        boolean isFullscreen = (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0
+                || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0;
         if (mIsFullscreen != isFullscreen) {
             mIsFullscreen = isFullscreen;
             synchronized (mListeners) {
@@ -514,12 +516,12 @@
         if (DEBUG_IMMERSIVE_APPS) {
             boolean dim = (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0;
             String behaviorName = ViewDebug.flagsToString(InsetsFlags.class, "behavior", behavior);
-            String requestedVisibilityString = requestedVisibilities.toString();
-            if (requestedVisibilityString.isEmpty()) {
-                requestedVisibilityString = "none";
+            String requestedVisibleTypesString = WindowInsets.Type.toString(requestedVisibleTypes);
+            if (requestedVisibleTypesString.isEmpty()) {
+                requestedVisibleTypesString = "none";
             }
             Log.d(TAG, packageName + " dim=" + dim + " behavior=" + behaviorName
-                    + " requested visibilities: " + requestedVisibilityString);
+                    + " requested visible types: " + requestedVisibleTypesString);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index 2cc7738..5a392a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -19,8 +19,8 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.IntDef;
-import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -109,13 +109,6 @@
     void setAndInstrumentDozeAmount(View view, float dozeAmount, boolean animated);
 
     /**
-     * Update the expanded state from {@link CentralSurfaces}'s perspective
-     * @param expanded are we expanded?
-     * @return {@code true} if the state changed, else {@code false}
-     */
-    boolean setPanelExpanded(boolean expanded);
-
-    /**
      * Sets whether to leave status bar open when hiding keyguard
      */
     void setLeaveOpenOnKeyguardHide(boolean leaveOpen);
@@ -154,7 +147,7 @@
      * Set the system bar attributes
      */
     void setSystemBarAttributes(@Appearance int appearance, @Behavior int behavior,
-            InsetsVisibilities requestedVisibilities, String packageName);
+            @InsetsType int requestedVisibleTypes, String packageName);
 
     /**
      * Set pulsing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index c070fcc..324e972 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -24,17 +24,21 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 import javax.inject.Inject;
 
 /**
- *
+ * A Helper class that offloads {@link Vibrator} calls to a different thread.
+ * {@link Vibrator} makes blocking calls that may cause SysUI to ANR.
+ * TODO(b/245528624): Use regular Vibrator instance once new APIs are available.
  */
 @SysUISingleton
 public class VibratorHelper {
@@ -53,10 +57,18 @@
     private final Executor mExecutor;
 
     /**
-     *
+     * Creates a vibrator helper on a new single threaded {@link Executor}.
      */
     @Inject
-    public VibratorHelper(@Nullable Vibrator vibrator, @Background Executor executor) {
+    public VibratorHelper(@Nullable Vibrator vibrator) {
+        this(vibrator, Executors.newSingleThreadExecutor());
+    }
+
+    /**
+     * Creates new vibrator helper on a specific {@link Executor}.
+     */
+    @VisibleForTesting
+    public VibratorHelper(@Nullable Vibrator vibrator, Executor executor) {
         mExecutor = executor;
         mVibrator = vibrator;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
index ec221b7..c523d22 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java
@@ -15,9 +15,7 @@
  */
 package com.android.systemui.statusbar.connectivity;
 
-import static com.android.settingslib.mobile.MobileMappings.getDefaultIcons;
-import static com.android.settingslib.mobile.MobileMappings.getIconKey;
-import static com.android.settingslib.mobile.MobileMappings.mapIconSets;
+import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID;
 
 import android.content.Context;
 import android.content.Intent;
@@ -46,6 +44,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.SignalStrengthUtil;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import java.io.PrintWriter;
@@ -63,6 +62,7 @@
     private final TelephonyManager mPhone;
     private final CarrierConfigTracker mCarrierConfigTracker;
     private final SubscriptionDefaults mDefaults;
+    private final MobileMappingsProxy mMobileMappingsProxy;
     private final String mNetworkNameDefault;
     private final String mNetworkNameSeparator;
     private final ContentObserver mObserver;
@@ -121,6 +121,7 @@
             TelephonyManager phone,
             CallbackHandler callbackHandler,
             NetworkControllerImpl networkController,
+            MobileMappingsProxy mobileMappingsProxy,
             SubscriptionInfo info,
             SubscriptionDefaults defaults,
             Looper receiverLooper,
@@ -135,13 +136,14 @@
         mPhone = phone;
         mDefaults = defaults;
         mSubscriptionInfo = info;
+        mMobileMappingsProxy = mobileMappingsProxy;
         mNetworkNameSeparator = getTextIfExists(
                 R.string.status_bar_network_name_separator).toString();
         mNetworkNameDefault = getTextIfExists(
                 com.android.internal.R.string.lockscreen_carrier_default).toString();
 
-        mNetworkToIconLookup = mapIconSets(mConfig);
-        mDefaultIcons = getDefaultIcons(mConfig);
+        mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig);
+        mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig);
 
         String networkName = info.getCarrierName() != null ? info.getCarrierName().toString()
                 : mNetworkNameDefault;
@@ -161,8 +163,8 @@
     void setConfiguration(Config config) {
         mConfig = config;
         updateInflateSignalStrength();
-        mNetworkToIconLookup = mapIconSets(mConfig);
-        mDefaultIcons = getDefaultIcons(mConfig);
+        mNetworkToIconLookup = mMobileMappingsProxy.mapIconSets(mConfig);
+        mDefaultIcons = mMobileMappingsProxy.getDefaultIcons(mConfig);
         updateTelephony();
     }
 
@@ -271,8 +273,9 @@
             dataContentDescription = mContext.getString(R.string.data_connection_no_internet);
         }
 
-        final QsInfo qsInfo = getQsInfo(contentDescription, icons.dataType);
-        final SbInfo sbInfo = getSbInfo(contentDescription, icons.dataType);
+        int iconId = mCurrentState.getNetworkTypeIcon(mContext);
+        final QsInfo qsInfo = getQsInfo(contentDescription, iconId);
+        final SbInfo sbInfo = getSbInfo(contentDescription, iconId);
 
         MobileDataIndicators mobileDataIndicators = new MobileDataIndicators(
                 sbInfo.icon,
@@ -373,6 +376,10 @@
         } else if (action.equals(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) {
             updateDataSim();
             notifyListenersIfNecessary();
+        } else if (action.equals(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED)) {
+            int carrierId = intent.getIntExtra(
+                    TelephonyManager.EXTRA_CARRIER_ID, UNKNOWN_CARRIER_ID);
+            mCurrentState.setCarrierId(carrierId);
         }
     }
 
@@ -477,7 +484,8 @@
             mCurrentState.level = getSignalLevel(mCurrentState.signalStrength);
         }
 
-        String iconKey = getIconKey(mCurrentState.telephonyDisplayInfo);
+        mCurrentState.setCarrierId(mPhone.getSimCarrierId());
+        String iconKey = mMobileMappingsProxy.getIconKey(mCurrentState.telephonyDisplayInfo);
         if (mNetworkToIconLookup.get(iconKey) != null) {
             mCurrentState.iconGroup = mNetworkToIconLookup.get(iconKey);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
index 7938179..a323454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalControllerFactory.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.mobile.MobileMappings
 import com.android.settingslib.mobile.MobileStatusTracker
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
 
@@ -33,6 +34,7 @@
     val context: Context,
     val callbackHandler: CallbackHandler,
     val carrierConfigTracker: CarrierConfigTracker,
+    val mobileMappings: MobileMappingsProxy,
 ) {
     fun createMobileSignalController(
         config: MobileMappings.Config,
@@ -56,6 +58,7 @@
             phone,
             callbackHandler,
             networkController,
+            mobileMappings,
             subscriptionInfo,
             subscriptionDefaults,
             receiverLooper,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
index f20d206..1fb6a98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileState.kt
@@ -16,10 +16,14 @@
 
 package com.android.systemui.statusbar.connectivity
 
+import android.annotation.DrawableRes
+import android.content.Context
 import android.telephony.ServiceState
 import android.telephony.SignalStrength
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.Utils
 import com.android.settingslib.mobile.MobileStatusTracker.MobileStatus
 import com.android.settingslib.mobile.TelephonyIcons
@@ -41,7 +45,7 @@
     @JvmField var roaming: Boolean = false,
     @JvmField var dataState: Int = TelephonyManager.DATA_DISCONNECTED,
     // Tracks the on/off state of the defaultDataSubscription
-    @JvmField var defaultDataOff: Boolean = false
+    @JvmField var defaultDataOff: Boolean = false,
 ) : ConnectivityState() {
 
     @JvmField var telephonyDisplayInfo = TelephonyDisplayInfo(TelephonyManager.NETWORK_TYPE_UNKNOWN,
@@ -49,6 +53,11 @@
     @JvmField var serviceState: ServiceState? = null
     @JvmField var signalStrength: SignalStrength? = null
 
+    var carrierId = TelephonyManager.UNKNOWN_CARRIER_ID
+
+    @VisibleForTesting
+    var networkTypeResIdCache: NetworkTypeResIdCache = NetworkTypeResIdCache()
+
     /** @return true if this state is disabled or not default data */
     val isDataDisabledOrNotDefault: Boolean
         get() = (iconGroup === TelephonyIcons.DATA_DISABLED ||
@@ -125,6 +134,21 @@
         return serviceState != null && serviceState!!.roaming
     }
 
+    /**
+     *
+     * Load the (potentially customized) icon resource id for the current network type. Note that
+     * this operation caches the result. Note that reading the [MobileIconGroup.dataType] field
+     * directly will not yield correct results in cases where the carrierId has an associated
+     * override. This is the preferred method for getting the network type indicator.
+     *
+     * @return a drawable res id appropriate for the current (carrierId, networkType) pair
+     */
+    @DrawableRes
+    fun getNetworkTypeIcon(context: Context): Int {
+        val icon = (iconGroup as MobileIconGroup)
+        return networkTypeResIdCache.get(icon, carrierId, context)
+    }
+
     fun setFromMobileStatus(mobileStatus: MobileStatus) {
         activityIn = mobileStatus.activityIn
         activityOut = mobileStatus.activityOut
@@ -140,6 +164,7 @@
         super.toString(builder)
         builder.append(',')
         builder.append("dataSim=$dataSim,")
+        builder.append("carrierId=$carrierId")
         builder.append("networkName=$networkName,")
         builder.append("networkNameData=$networkNameData,")
         builder.append("dataConnected=$dataConnected,")
@@ -157,6 +182,8 @@
         builder.append("voiceServiceState=${getVoiceServiceState()},")
         builder.append("isInService=${isInService()},")
 
+        builder.append("networkTypeIconCache=$networkTypeResIdCache")
+
         builder.append("serviceState=${serviceState?.minLog() ?: "(null)"},")
         builder.append("signalStrength=${signalStrength?.minLog() ?: "(null)"},")
         builder.append("displayInfo=$telephonyDisplayInfo")
@@ -164,6 +191,7 @@
 
     override fun tableColumns(): List<String> {
         val columns = listOf("dataSim",
+            "carrierId",
             "networkName",
             "networkNameData",
             "dataConnected",
@@ -178,6 +206,7 @@
             "showQuickSettingsRatIcon",
             "voiceServiceState",
             "isInService",
+            "networkTypeIconCache",
             "serviceState",
             "signalStrength",
             "displayInfo")
@@ -187,6 +216,7 @@
 
     override fun tableData(): List<String> {
         val columns = listOf(dataSim,
+                carrierId,
                 networkName,
                 networkNameData,
                 dataConnected,
@@ -201,6 +231,7 @@
                 showQuickSettingsRatIcon(),
                 getVoiceServiceState(),
                 isInService(),
+                networkTypeResIdCache,
                 serviceState?.minLog() ?: "(null)",
                 signalStrength?.minLog() ?: "(null)",
                 telephonyDisplayInfo).map { it.toString() }
@@ -217,6 +248,7 @@
 
         if (networkName != other.networkName) return false
         if (networkNameData != other.networkNameData) return false
+        if (carrierId != other.carrierId) return false
         if (dataSim != other.dataSim) return false
         if (dataConnected != other.dataConnected) return false
         if (isEmergency != other.isEmergency) return false
@@ -238,6 +270,7 @@
         var result = super.hashCode()
         result = 31 * result + (networkName?.hashCode() ?: 0)
         result = 31 * result + (networkNameData?.hashCode() ?: 0)
+        result = 31 * result + (carrierId.hashCode())
         result = 31 * result + dataSim.hashCode()
         result = 31 * result + dataConnected.hashCode()
         result = 31 * result + isEmergency.hashCode()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index ea7ec4f..99ff06a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -22,6 +22,7 @@
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE;
 import static android.net.wifi.WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
@@ -38,6 +39,7 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
@@ -71,11 +73,11 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DataSaverController;
 import com.android.systemui.statusbar.policy.DataSaverControllerImpl;
@@ -127,7 +129,7 @@
     private final boolean mHasMobileDataFeature;
     private final SubscriptionDefaults mSubDefaults;
     private final DataSaverController mDataSaverController;
-    private final CurrentUserTracker mUserTracker;
+    private final UserTracker mUserTracker;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final DemoModeController mDemoModeController;
     private final Object mLock = new Object();
@@ -138,7 +140,7 @@
     private final MobileSignalControllerFactory mMobileFactory;
 
     private TelephonyCallback.ActiveDataSubscriptionIdListener mPhoneStateListener;
-    private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    private int mActiveMobileDataSubscription = INVALID_SUBSCRIPTION_ID;
 
     // Subcontrollers.
     @VisibleForTesting
@@ -211,6 +213,14 @@
                 }
             };
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    NetworkControllerImpl.this.onUserSwitched(newUser);
+                }
+            };
+
     /**
      * Construct this controller object and register for updates.
      */
@@ -223,6 +233,7 @@
             CallbackHandler callbackHandler,
             DeviceProvisionedController deviceProvisionedController,
             BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
             ConnectivityManager connectivityManager,
             TelephonyManager telephonyManager,
             TelephonyListenerManager telephonyListenerManager,
@@ -250,6 +261,7 @@
                 new SubscriptionDefaults(),
                 deviceProvisionedController,
                 broadcastDispatcher,
+                userTracker,
                 demoModeController,
                 carrierConfigTracker,
                 trackerFactory,
@@ -276,6 +288,7 @@
             SubscriptionDefaults defaultsHandler,
             DeviceProvisionedController deviceProvisionedController,
             BroadcastDispatcher broadcastDispatcher,
+            UserTracker userTracker,
             DemoModeController demoModeController,
             CarrierConfigTracker carrierConfigTracker,
             WifiStatusTrackerFactory trackerFactory,
@@ -332,13 +345,9 @@
 
         // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
         updateAirplaneMode(true /* force callback */);
-        mUserTracker = new CurrentUserTracker(broadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                NetworkControllerImpl.this.onUserSwitched(newUserId);
-            }
-        };
-        mUserTracker.startTracking();
+        mUserTracker = userTracker;
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
+
         deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
             @Override
             public void onUserSetupChanged() {
@@ -502,6 +511,7 @@
         filter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
         filter.addAction(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED);
         filter.addAction(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED);
+        filter.addAction(TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
         filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
         mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler);
         mListening = true;
@@ -792,6 +802,20 @@
                 mConfig = Config.readConfig(mContext);
                 mReceiverHandler.post(this::handleConfigurationChanged);
                 break;
+
+            case TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED: {
+                // Notify the relevant MobileSignalController of the change
+                int subId = intent.getIntExtra(
+                        TelephonyManager.EXTRA_SUBSCRIPTION_ID,
+                        INVALID_SUBSCRIPTION_ID
+                );
+                if (SubscriptionManager.isValidSubscriptionId(subId)) {
+                    if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
+                        mMobileSignalControllers.get(subId).handleBroadcast(intent);
+                    }
+                }
+            }
+            break;
             case Intent.ACTION_SIM_STATE_CHANGED:
                 // Avoid rebroadcast because SysUI is direct boot aware.
                 if (intent.getBooleanExtra(Intent.EXTRA_REBROADCAST_ON_UNLOCK, false)) {
@@ -819,7 +843,7 @@
                 break;
             default:
                 int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
-                        SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+                        INVALID_SUBSCRIPTION_ID);
                 if (SubscriptionManager.isValidSubscriptionId(subId)) {
                     if (mMobileSignalControllers.indexOfKey(subId) >= 0) {
                         mMobileSignalControllers.get(subId).handleBroadcast(intent);
@@ -1335,6 +1359,9 @@
             String slotString = args.getString("slot");
             int slot = TextUtils.isEmpty(slotString) ? 0 : Integer.parseInt(slotString);
             slot = MathUtils.constrain(slot, 0, 8);
+            String carrierIdString = args.getString("carrierid");
+            int carrierId = TextUtils.isEmpty(carrierIdString) ? 0
+                    : Integer.parseInt(carrierIdString);
             // Ensure we have enough sim slots
             List<SubscriptionInfo> subs = new ArrayList<>();
             while (mMobileSignalControllers.size() <= slot) {
@@ -1346,6 +1373,9 @@
             }
             // Hack to index linearly for easy use.
             MobileSignalController controller = mMobileSignalControllers.valueAt(slot);
+            if (carrierId != 0) {
+                controller.getState().setCarrierId(carrierId);
+            }
             controller.getState().dataSim = datatype != null;
             controller.getState().isDefault = datatype != null;
             controller.getState().dataConnected = datatype != null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt
new file mode 100644
index 0000000..9be7ee9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCache.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.connectivity
+
+import android.annotation.DrawableRes
+import android.content.Context
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
+import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
+
+/**
+ * Cache for network type resource IDs.
+ *
+ * The default framework behavior is to have a statically defined icon per network type. See
+ * [MobileIconGroup] for the standard mapping.
+ *
+ * For the case of carrierId-defined overrides, we want to check [MobileIconCarrierIdOverrides] for
+ * an existing icon override, and cache the result of the operation
+ */
+class NetworkTypeResIdCache(
+    private val overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
+) {
+    @DrawableRes private var cachedResId: Int = 0
+    private var lastCarrierId: Int? = null
+    private var lastIconGroup: MobileIconGroup? = null
+    private var isOverridden: Boolean = false
+
+    @DrawableRes
+    fun get(iconGroup: MobileIconGroup, carrierId: Int, context: Context): Int {
+        if (lastCarrierId != carrierId || lastIconGroup != iconGroup) {
+            lastCarrierId = carrierId
+            lastIconGroup = iconGroup
+
+            val maybeOverride = calculateOverriddenIcon(iconGroup, carrierId, context)
+            if (maybeOverride > 0) {
+                cachedResId = maybeOverride
+                isOverridden = true
+            } else {
+                cachedResId = iconGroup.dataType
+                isOverridden = false
+            }
+        }
+
+        return cachedResId
+    }
+
+    override fun toString(): String {
+        return "networkTypeResIdCache={id=$cachedResId, isOverridden=$isOverridden}"
+    }
+
+    @DrawableRes
+    private fun calculateOverriddenIcon(
+        iconGroup: MobileIconGroup,
+        carrierId: Int,
+        context: Context,
+    ): Int {
+        val name = iconGroup.name
+        if (!overrides.carrierIdEntryExists(carrierId)) {
+            return 0
+        }
+
+        return overrides.getOverrideFor(carrierId, name, context.resources)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
index a02dd34..42b874f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ui/MobileContextProvider.kt
@@ -37,6 +37,13 @@
  * own [Configuration] and track resources based on the full set of available mcc-mnc combinations.
  *
  * (for future reference: b/240555502 is the initiating bug for this)
+ *
+ * NOTE: MCC/MNC qualifiers are not sufficient to fully describe a network type icon qualified by
+ * network type + carrier ID. This class exists to keep the legacy behavior of using the MCC/MNC
+ * resource qualifiers working, but if a carrier-specific icon is requested, then the override
+ * provided by [MobileIconCarrierIdOverrides] will take precedence.
+ *
+ * TODO(b/258503704): consider removing this class in favor of the `carrierId` overrides
  */
 @SysUISingleton
 class MobileContextProvider
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 11e3d17..14d0d7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -29,8 +29,9 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaDataManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.carrier.QSCarrierGroupController;
@@ -181,8 +182,10 @@
     static CommandQueue provideCommandQueue(
             Context context,
             ProtoTracer protoTracer,
-            CommandRegistry registry) {
-        return new CommandQueue(context, protoTracer, registry);
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
+        return new CommandQueue(context, protoTracer, registry, dumpHandler);
     }
 
     /**
@@ -297,7 +300,7 @@
 
             @Override
             public boolean isShowingAlternateAuthOnUnlock() {
-                return statusBarKeyguardViewManager.get().shouldShowAltAuth();
+                return statusBarKeyguardViewManager.get().canShowAlternateBouncer();
             }
         };
         return new DialogLaunchAnimator(callback, interactionJankMonitor);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index d88f07c..143c697 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -25,11 +25,12 @@
 import android.view.View
 import android.widget.FrameLayout
 import com.android.internal.annotations.GuardedBy
-import com.android.systemui.animation.Interpolators
 import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
@@ -42,7 +43,6 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
-
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -62,12 +62,13 @@
  */
 
 @SysUISingleton
-class PrivacyDotViewController @Inject constructor(
+open class PrivacyDotViewController @Inject constructor(
     @Main private val mainExecutor: Executor,
     private val stateController: StatusBarStateController,
     private val configurationController: ConfigurationController,
     private val contentInsetsProvider: StatusBarContentInsetsProvider,
-    private val animationScheduler: SystemStatusAnimationScheduler
+    private val animationScheduler: SystemStatusAnimationScheduler,
+    shadeExpansionStateManager: ShadeExpansionStateManager
 ) {
     private lateinit var tl: View
     private lateinit var tr: View
@@ -75,7 +76,8 @@
     private lateinit var br: View
 
     // Only can be modified on @UiThread
-    private var currentViewState: ViewState = ViewState()
+    var currentViewState: ViewState = ViewState()
+        get() = field
 
     @GuardedBy("lock")
     private var nextViewState: ViewState = currentViewState.copy()
@@ -128,21 +130,25 @@
                 updateStatusBarState()
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            dlog("setQsExpanded $isQsExpanded")
+            synchronized(lock) {
+                nextViewState = nextViewState.copy(qsExpanded = isQsExpanded)
+            }
+        }
     }
 
     fun setUiExecutor(e: DelayableExecutor) {
         uiExecutor = e
     }
 
-    fun setShowingListener(l: ShowingListener?) {
-        showingListener = l
+    fun getUiExecutor(): DelayableExecutor? {
+        return uiExecutor
     }
 
-    fun setQsExpanded(expanded: Boolean) {
-        dlog("setQsExpanded $expanded")
-        synchronized(lock) {
-            nextViewState = nextViewState.copy(qsExpanded = expanded)
-        }
+    fun setShowingListener(l: ShowingListener?) {
+        showingListener = l
     }
 
     @UiThread
@@ -175,7 +181,7 @@
     }
 
     @UiThread
-    private fun hideDotView(dot: View, animate: Boolean) {
+    fun hideDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.animate()
@@ -194,7 +200,7 @@
     }
 
     @UiThread
-    private fun showDotView(dot: View, animate: Boolean) {
+    fun showDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.visibility = View.VISIBLE
@@ -507,6 +513,13 @@
             state.designatedCorner?.contentDescription = state.contentDescription
         }
 
+        updateDotView(state)
+
+        currentViewState = state
+    }
+
+    @UiThread
+    open fun updateDotView(state: ViewState) {
         val shouldShow = state.shouldShowDot()
         if (shouldShow != currentViewState.shouldShowDot()) {
             if (shouldShow && state.designatedCorner != null) {
@@ -515,8 +528,6 @@
                 hideDotView(state.designatedCorner, true)
             }
         }
-
-        currentViewState = state
     }
 
     private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
@@ -620,7 +631,7 @@
     }
 }
 
-private data class ViewState(
+data class ViewState(
     val viewInitialized: Boolean = false,
 
     val systemPrivacyEventIsActive: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
index 17feaa8..9bdff92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.gesture
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [SwipeStatusBarAwayGestureHandler]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index dfba8cd..6bd9502 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -48,7 +48,8 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.shared.regionsampling.RegionSamplingInstance
+import com.android.systemui.shared.regionsampling.RegionSampler
+import com.android.systemui.shared.regionsampling.UpdateColorCallback
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -90,8 +91,8 @@
 
     // Smartspace can be used on multiple displays, such as when the user casts their screen
     private var smartspaceViews = mutableSetOf<SmartspaceView>()
-    private var regionSamplingInstances =
-            mutableMapOf<SmartspaceView, RegionSamplingInstance>()
+    private var regionSamplers =
+            mutableMapOf<SmartspaceView, RegionSampler>()
 
     private val regionSamplingEnabled =
             featureFlags.isEnabled(Flags.REGION_SAMPLING)
@@ -101,26 +102,23 @@
     private var showSensitiveContentForManagedUser = false
     private var managedUserHandle: UserHandle? = null
 
-    private val updateFun = object : RegionSamplingInstance.UpdateColorCallback {
-        override fun updateColors() {
-            updateTextColorFromRegionSampler()
-        }
-    }
+    private val updateFun: UpdateColorCallback = { updateTextColorFromRegionSampler() }
 
     // TODO: Move logic into SmartspaceView
     var stateChangeListener = object : View.OnAttachStateChangeListener {
         override fun onViewAttachedToWindow(v: View) {
             smartspaceViews.add(v as SmartspaceView)
 
-            var regionSamplingInstance = RegionSamplingInstance(
+            var regionSampler = RegionSampler(
                     v,
                     uiExecutor,
                     bgExecutor,
                     regionSamplingEnabled,
                     updateFun
             )
-            regionSamplingInstance.startRegionSampler()
-            regionSamplingInstances.put(v, regionSamplingInstance)
+            initializeTextColors(regionSampler)
+            regionSampler.startRegionSampler()
+            regionSamplers.put(v, regionSampler)
             connectSession()
 
             updateTextColorFromWallpaper()
@@ -130,9 +128,9 @@
         override fun onViewDetachedFromWindow(v: View) {
             smartspaceViews.remove(v as SmartspaceView)
 
-            var regionSamplingInstance = regionSamplingInstances.getValue(v)
-            regionSamplingInstance.stopRegionSampler()
-            regionSamplingInstances.remove(v)
+            var regionSampler = regionSamplers.getValue(v)
+            regionSampler.stopRegionSampler()
+            regionSamplers.remove(v)
 
             if (smartspaceViews.isEmpty()) {
                 disconnect()
@@ -237,19 +235,24 @@
 
         ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
             override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) {
-                activityStarter.startActivity(
-                    intent,
-                    true, /* dismissShade */
-                    null, /* launch animator - looks bad with the transparent smartspace bg */
-                    showOnLockscreen
-                )
+                if (showOnLockscreen) {
+                    activityStarter.startActivity(
+                            intent,
+                            true, /* dismissShade */
+                            // launch animator - looks bad with the transparent smartspace bg
+                            null,
+                            true
+                    )
+                } else {
+                    activityStarter.postStartActivityDismissingKeyguard(intent, 0)
+                }
             }
 
             override fun startPendingIntent(pi: PendingIntent, showOnLockscreen: Boolean) {
                 if (showOnLockscreen) {
                     pi.send()
                 } else {
-                    activityStarter.startPendingIntentDismissingKeyguard(pi)
+                    activityStarter.postStartActivityDismissingKeyguard(pi)
                 }
             }
         })
@@ -362,18 +365,20 @@
         }
     }
 
+    private fun initializeTextColors(regionSampler: RegionSampler) {
+        val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper)
+        val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor)
+
+        val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI)
+        val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor)
+
+        regionSampler.setForegroundColors(lightColor, darkColor)
+    }
+
     private fun updateTextColorFromRegionSampler() {
         smartspaceViews.forEach {
-            val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness()
-            val themeID = if (isRegionDark.isDark) {
-                R.style.Theme_SystemUI
-            } else {
-                R.style.Theme_SystemUI_LightWallpaper
-            }
-            val themedContext = ContextThemeWrapper(context, themeID)
-            val wallpaperTextColor =
-                    Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor)
-            it.setPrimaryTextColor(wallpaperTextColor)
+            val textColor = regionSamplers.getValue(it).currentForegroundColor()
+            it.setPrimaryTextColor(textColor)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
index 822840d..0a5e986 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java
@@ -290,7 +290,7 @@
                             .setComponent(aiaComponent)
                             .setAction(Intent.ACTION_VIEW)
                             .addCategory(Intent.CATEGORY_BROWSABLE)
-                            .addCategory("unique:" + System.currentTimeMillis())
+                            .setIdentifier("unique:" + System.currentTimeMillis())
                             .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
                             .putExtra(
                                     Intent.EXTRA_VERSION_CODE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 7fbdd35..7eb8906 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -17,40 +17,31 @@
 package com.android.systemui.statusbar.notification
 
 import android.content.Context
-import android.util.Log
-import android.widget.Toast
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NotifPipelineFlags @Inject constructor(
     val context: Context,
     val featureFlags: FeatureFlags
 ) {
-    fun checkLegacyPipelineEnabled(): Boolean {
-        if (Compile.IS_DEBUG) {
-            Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show()
-        }
-        if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) {
-            throw RuntimeException("Old pipeline code running with new pipeline enabled")
-        } else {
-            Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled",
-                    Exception())
-        }
-        return false
-    }
-
     fun isDevLoggingEnabled(): Boolean =
         featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
 
-    fun isSmartspaceDedupingEnabled(): Boolean =
-            featureFlags.isEnabled(Flags.SMARTSPACE) &&
-                    featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-
-    fun removeUnrankedNotifs(): Boolean =
-        featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+    fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
 
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+
+    val isStabilityIndexFixEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
+    }
+
+    val isSemiStableSortEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
+    }
+
+    val shouldFilterUnseenNotifsOnKeyguard: Boolean by lazy {
+        featureFlags.isEnabled(Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index ad3dfed..3058fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 553826d..0d35fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -70,8 +70,8 @@
         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
         val location = notification.locationOnScreen
 
-        val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
-        val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
+        val clipStartLocation = notificationListContainer.topClippingStartLocation
+        val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
         val windowTop = location[1] + roundedTopClipping
         val topCornerRadius = if (roundedTopClipping > 0) {
             // Because the rounded Rect clipping is complex, we start the top rounding at
@@ -80,7 +80,7 @@
             // if we'd like to have this perfect, but this is close enough.
             0f
         } else {
-            notification.currentBackgroundRadiusTop
+            notification.topCornerRadius
         }
         val params = LaunchAnimationParameters(
             top = windowTop,
@@ -88,7 +88,7 @@
             left = location[0],
             right = location[0] + notification.width,
             topCornerRadius = topCornerRadius,
-            bottomCornerRadius = notification.currentBackgroundRadiusBottom
+            bottomCornerRadius = notification.bottomCornerRadius
         )
 
         params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@
         params.startClipTopAmount = notification.clipTopAmount
         if (notification.isChildInGroup) {
             params.startNotificationTop += notification.notificationParent.translationY
-            val parentRoundedClip = Math.max(
-                clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
+            val locationOnScreen = notification.notificationParent.locationOnScreen[1]
+            val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
             params.parentStartRoundedTopClipping = parentRoundedClip
 
             val parentClip = notification.notificationParent.clipTopAmount
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 7242506..d97b712 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -18,8 +18,10 @@
 
 import android.animation.ObjectAnimator
 import android.util.FloatProperty
+import com.android.systemui.Dumpable
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
@@ -32,17 +34,20 @@
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlin.math.min
 
 @SysUISingleton
 class NotificationWakeUpCoordinator @Inject constructor(
+    dumpManager: DumpManager,
     private val mHeadsUpManager: HeadsUpManager,
     private val statusBarStateController: StatusBarStateController,
     private val bypassController: KeyguardBypassController,
     private val dozeParameters: DozeParameters,
     private val screenOffAnimationController: ScreenOffAnimationController
-) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener {
+) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener,
+    Dumpable {
 
     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
         "notificationVisibility") {
@@ -60,6 +65,7 @@
 
     private var mLinearDozeAmount: Float = 0.0f
     private var mDozeAmount: Float = 0.0f
+    private var mDozeAmountSource: String = "init"
     private var mNotificationVisibleAmount = 0.0f
     private var mNotificationsVisible = false
     private var mNotificationsVisibleForExpansion = false
@@ -142,6 +148,7 @@
         }
 
     init {
+        dumpManager.registerDumpable(this)
         mHeadsUpManager.addListener(this)
         statusBarStateController.addCallback(this)
         addListener(object : WakeUpListener {
@@ -248,13 +255,14 @@
             // Let's notify the scroller that an animation started
             notifyAnimationStart(mLinearDozeAmount == 1.0f)
         }
-        setDozeAmount(linear, eased)
+        setDozeAmount(linear, eased, source = "StatusBar")
     }
 
-    fun setDozeAmount(linear: Float, eased: Float) {
+    fun setDozeAmount(linear: Float, eased: Float, source: String) {
         val changed = linear != mLinearDozeAmount
         mLinearDozeAmount = linear
         mDozeAmount = eased
+        mDozeAmountSource = source
         mStackScrollerController.setDozeAmount(mDozeAmount)
         updateHideAmount()
         if (changed && linear == 0.0f) {
@@ -271,7 +279,7 @@
             // undefined state, so it's an indication that we should do state cleanup. We override
             // the doze amount to 0f (not dozing) so that the notifications are no longer hidden.
             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
-            setDozeAmount(0f, 0f)
+            setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)")
         }
 
         if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) {
@@ -311,12 +319,11 @@
      */
     private fun overrideDozeAmountIfBypass(): Boolean {
         if (bypassController.bypassEnabled) {
-            var amount = 1.0f
-            if (statusBarStateController.state == StatusBarState.SHADE ||
-                statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
-                amount = 0.0f
+            if (statusBarStateController.state == StatusBarState.KEYGUARD) {
+                setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
+            } else {
+                setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
             }
-            setDozeAmount(amount, amount)
             return true
         }
         return false
@@ -332,7 +339,7 @@
      */
     private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean {
         if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
-            setDozeAmount(1f, 1f)
+            setDozeAmount(1f, 1f, source = "Override: animating screen off")
             return true
         }
 
@@ -414,6 +421,26 @@
     private fun shouldAnimateVisibility() =
             dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
 
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("mLinearDozeAmount: $mLinearDozeAmount")
+        pw.println("mDozeAmount: $mDozeAmount")
+        pw.println("mDozeAmountSource: $mDozeAmountSource")
+        pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
+        pw.println("mNotificationsVisible: $mNotificationsVisible")
+        pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
+        pw.println("mVisibilityAmount: $mVisibilityAmount")
+        pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
+        pw.println("pulseExpanding: $pulseExpanding")
+        pw.println("state: ${StatusBarState.toString(state)}")
+        pw.println("fullyAwake: $fullyAwake")
+        pw.println("wakingUp: $wakingUp")
+        pw.println("willWakeUp: $willWakeUp")
+        pw.println("collapsedEnoughToHide: $collapsedEnoughToHide")
+        pw.println("pulsing: $pulsing")
+        pw.println("notificationsFullyHidden: $notificationsFullyHidden")
+        pw.println("canShowPulsingHuns: $canShowPulsingHuns")
+    }
+
     interface WakeUpListener {
         /**
          * Called whenever the notifications are fully hidden or shown
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
new file mode 100644
index 0000000..ed7f648
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -0,0 +1,284 @@
+package com.android.systemui.statusbar.notification
+
+import android.util.FloatProperty
+import android.view.View
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import kotlin.math.abs
+
+/**
+ * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
+ *
+ * To request a roundness value, an [SourceType] must be specified. In case more origins require
+ * different roundness, for the same property, the maximum value will always be chosen.
+ *
+ * It also returns the current radius for all corners ([updatedRadii]).
+ */
+interface Roundable {
+    /** Properties required for a Roundable */
+    val roundableState: RoundableState
+
+    /** Current top roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val topRoundness: Float
+        get() = roundableState.topRoundness
+
+    /** Current bottom roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val bottomRoundness: Float
+        get() = roundableState.bottomRoundness
+
+    /** Max radius in pixel */
+    @JvmDefault
+    val maxRadius: Float
+        get() = roundableState.maxRadius
+
+    /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
+    @JvmDefault
+    val topCornerRadius: Float
+        get() = topRoundness * maxRadius
+
+    /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
+    @JvmDefault
+    val bottomCornerRadius: Float
+        get() = bottomRoundness * maxRadius
+
+    /** Get and update the current radii */
+    @JvmDefault
+    val updatedRadii: FloatArray
+        get() =
+            roundableState.radiiBuffer.also { radii ->
+                updateRadii(
+                    topCornerRadius = topCornerRadius,
+                    bottomCornerRadius = bottomCornerRadius,
+                    radii = radii,
+                )
+            }
+
+    /**
+     * Request the top roundness [value] for a specific [sourceType].
+     *
+     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value a value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestTopRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.topRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isTopAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Request the bottom roundness [value] for a specific [sourceType].
+     *
+     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestBottomRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.bottomRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isBottomAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
+    @JvmDefault
+    fun applyRoundness() {
+        roundableState.targetView.invalidate()
+    }
+
+    /** @return true if top or bottom roundness is not zero. */
+    @JvmDefault
+    fun hasRoundedCorner(): Boolean {
+        return topRoundness != 0f || bottomRoundness != 0f
+    }
+
+    /**
+     * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
+     * [android.graphics.Path.addRoundRect].
+     *
+     * This method reuses the previous [radii] for performance reasons.
+     */
+    @JvmDefault
+    fun updateRadii(
+        topCornerRadius: Float,
+        bottomCornerRadius: Float,
+        radii: FloatArray,
+    ) {
+        if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")
+
+        if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
+            (0..3).forEach { radii[it] = topCornerRadius }
+            (4..7).forEach { radii[it] = bottomCornerRadius }
+        }
+    }
+}
+
+/**
+ * State object for a `Roundable` class.
+ * @param targetView Will handle the [AnimatableProperty]
+ * @param roundable Target of the radius animation
+ * @param maxRadius Max corner radius in pixels
+ */
+class RoundableState(
+    internal val targetView: View,
+    roundable: Roundable,
+    internal val maxRadius: Float,
+) {
+    /** Animatable for top roundness */
+    private val topAnimatable = topAnimatable(roundable)
+
+    /** Animatable for bottom roundness */
+    private val bottomAnimatable = bottomAnimatable(roundable)
+
+    /** Current top roundness. Use [setTopRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var topRoundness = 0f
+        private set
+
+    /** Current bottom roundness. Use [setBottomRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var bottomRoundness = 0f
+        private set
+
+    /** Last requested top roundness associated by [SourceType] */
+    internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last requested bottom roundness associated by [SourceType] */
+    internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last cached radii */
+    internal val radiiBuffer = FloatArray(8)
+
+    /** Is top roundness animation in progress? */
+    internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)
+
+    /** Is bottom roundness animation in progress? */
+    internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)
+
+    /** Set the current top roundness */
+    internal fun setTopRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
+    }
+
+    /** Set the current bottom roundness */
+    internal fun setBottomRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
+    }
+
+    companion object {
+        private val DURATION: AnimationProperties =
+            AnimationProperties()
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())
+
+        private fun topAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("topRoundness") {
+                    override fun get(view: View): Float = roundable.topRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.topRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.top_roundess_animator_tag,
+                R.id.top_roundess_animator_end_tag,
+                R.id.top_roundess_animator_start_tag,
+            )
+
+        private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("bottomRoundness") {
+                    override fun get(view: View): Float = roundable.bottomRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.bottomRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.bottom_roundess_animator_tag,
+                R.id.bottom_roundess_animator_end_tag,
+                R.id.bottom_roundess_animator_start_tag,
+            )
+    }
+}
+
+enum class SourceType {
+    DefaultValue,
+    OnDismissAnimation,
+    OnScroll,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index f8449ae..84ab0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,6 +68,9 @@
      */
     var stableIndex: Int = -1
 
+    /** Access the index of the [section] or -1 if the entry does not have one */
+    val sectionIndex: Int get() = section?.index ?: -1
+
     /** Copies the state of another instance. */
     fun clone(other: ListAttachState) {
         parent = other.parent
@@ -95,11 +98,13 @@
      * This can happen if the entry is removed from a group that was broken up or if the entry was
      * filtered out during any of the filtering steps.
      */
-    fun detach() {
+    fun detach(includingStableIndex: Boolean) {
         parent = null
         section = null
         promoter = null
-        // stableIndex = -1  // TODO(b/241229236): Clear this once we fix the stability fragility
+        if (includingStableIndex) {
+            stableIndex = -1
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 2887f97..df35c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -602,7 +602,7 @@
 
         mInconsistencyTracker.logNewMissingNotifications(rankingMap);
         mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap);
-        if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
+        if (currentEntriesWithoutRankings != null) {
             for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
                 entry.mCancellationReason = REASON_UNKNOWN;
                 tryRemoveNotification(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index e129ee4..3ae2545 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -54,6 +54,9 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,11 +99,14 @@
     // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
     // TODO replace temp with collection pool for readability
     private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
+    private NotifPipelineFlags mFlags;
     private final boolean mAlwaysLogList;
 
     private List<ListEntry> mNotifList = new ArrayList<>();
     private List<ListEntry> mNewNotifList = new ArrayList<>();
 
+    private final SemiStableSort mSemiStableSort = new SemiStableSort();
+    private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
     private final PipelineState mPipelineState = new PipelineState();
     private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
     private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -141,6 +147,7 @@
     ) {
         mSystemClock = systemClock;
         mLogger = logger;
+        mFlags = flags;
         mAlwaysLogList = flags.isDevLoggingEnabled();
         mInteractionTracker = interactionTracker;
         mChoreographer = pipelineChoreographer;
@@ -527,7 +534,7 @@
             List<NotifFilter> filters) {
         Trace.beginSection("ShadeListBuilder.filterNotifs");
         final long now = mSystemClock.uptimeMillis();
-        for (ListEntry entry : entries)  {
+        for (ListEntry entry : entries) {
             if (entry instanceof GroupEntry) {
                 final GroupEntry groupEntry = (GroupEntry) entry;
 
@@ -958,7 +965,8 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
-        entry.getAttachState().detach();
+        // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
+        entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
     }
 
     private void assignSections() {
@@ -978,7 +986,16 @@
 
     private void sortListAndGroups() {
         Trace.beginSection("ShadeListBuilder.sortListAndGroups");
-        // Assign sections to top-level elements and sort their children
+        if (mFlags.isSemiStableSortEnabled()) {
+            sortWithSemiStableSort();
+        } else {
+            sortWithLegacyStability();
+        }
+        Trace.endSection();
+    }
+
+    private void sortWithLegacyStability() {
+        // Sort all groups and the top level list
         for (ListEntry entry : mNotifList) {
             if (entry instanceof GroupEntry) {
                 GroupEntry parent = (GroupEntry) entry;
@@ -991,16 +1008,15 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isShadeSorted();
+            boolean isSorted = isShadeSortedLegacy();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
             }
         }
-        Trace.endSection();
     }
 
-    private boolean isShadeSorted() {
+    private boolean isShadeSortedLegacy() {
         if (!isSorted(mNotifList, mTopLevelComparator)) {
             return false;
         }
@@ -1014,6 +1030,43 @@
         return true;
     }
 
+    private void sortWithSemiStableSort() {
+        // Sort each group's children
+        boolean allSorted = true;
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                GroupEntry parent = (GroupEntry) entry;
+                allSorted &= sortGroupChildren(parent.getRawChildren());
+            }
+        }
+        // Sort each section within the top level list
+        mNotifList.sort(mTopLevelComparator);
+        if (!getStabilityManager().isEveryChangeAllowed()) {
+            for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
+                allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
+            }
+            applyNewNotifList();
+        }
+        assignIndexes(mNotifList);
+        if (!allSorted) {
+            // Report suppressed order changes
+            getStabilityManager().onEntryReorderSuppressed();
+        }
+    }
+
+    private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
+        return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
+    }
+
+    private boolean sortGroupChildren(List<NotificationEntry> entries) {
+        if (getStabilityManager().isEveryChangeAllowed()) {
+            entries.sort(mGroupChildrenComparator);
+            return true;
+        } else {
+            return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
+        }
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
     public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1036,27 +1089,41 @@
     /**
      * Assign the index of each notification relative to the total order
      */
-    private static void assignIndexes(List<ListEntry> notifList) {
+    private void assignIndexes(List<ListEntry> notifList) {
         if (notifList.size() == 0) return;
         NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
         int sectionMemberIndex = 0;
         for (int i = 0; i < notifList.size(); i++) {
-            ListEntry entry = notifList.get(i);
+            final ListEntry entry = notifList.get(i);
             NotifSection section = requireNonNull(entry.getSection());
             if (section.getIndex() != currentSection.getIndex()) {
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            entry.getAttachState().setStableIndex(sectionMemberIndex);
-            if (entry instanceof GroupEntry) {
-                GroupEntry parent = (GroupEntry) entry;
-                for (int j = 0; j < parent.getChildren().size(); j++) {
-                    entry = parent.getChildren().get(j);
-                    entry.getAttachState().setStableIndex(sectionMemberIndex);
-                    sectionMemberIndex++;
+            if (mFlags.isStabilityIndexFixEnabled()) {
+                entry.getAttachState().setStableIndex(sectionMemberIndex++);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    final NotificationEntry summary = parent.getSummary();
+                    if (summary != null) {
+                        summary.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
                 }
+            } else {
+                // This old implementation uses the same index number for the group as the first
+                // child, and fails to assign an index to the summary.  Remove once tested.
+                entry.getAttachState().setStableIndex(sectionMemberIndex);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                }
+                sectionMemberIndex++;
             }
-            sectionMemberIndex++;
         }
     }
 
@@ -1196,7 +1263,7 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
+        cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1225,7 +1292,7 @@
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = Integer.compare(
+        int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1256,9 +1323,25 @@
             // let the stability manager constrain or allow reordering
             return -1;
         }
+        // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
         return entry.getPreviousAttachState().getStableIndex();
     }
 
+    @Nullable
+    private Integer getStableOrderRank(ListEntry entry) {
+        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
+            // let the stability manager constrain or allow reordering
+            return null;
+        }
+        if (entry.getAttachState().getSectionIndex()
+                != entry.getPreviousAttachState().getSectionIndex()) {
+            // stable index is only valid within the same section; otherwise we allow reordering
+            return null;
+        }
+        final int stableIndex = entry.getPreviousAttachState().getStableIndex();
+        return stableIndex == -1 ? null : stableIndex;
+    }
+
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
         final NotifFilter filter = findRejectingFilter(entry, now, filters);
         entry.getAttachState().setExcludingFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
index 211e374..68d1319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coalescer
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 class GroupCoalescerLogger @Inject constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
index e8f352f..2919def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.NotificationGuts
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8f3eb4f..5dbb4f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -18,6 +18,8 @@
 import android.app.Notification
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.util.ArrayMap
+import android.util.ArraySet
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -31,6 +33,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
@@ -66,10 +69,12 @@
     private val mHeadsUpViewBinder: HeadsUpViewBinder,
     private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
     private val mRemoteInputManager: NotificationRemoteInputManager,
+    private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
     @IncomingHeader private val mIncomingHeaderController: NodeController,
     @Main private val mExecutor: DelayableExecutor,
 ) : Coordinator {
     private val mEntriesBindingUntil = ArrayMap<String, Long>()
+    private val mEntriesUpdateTimes = ArrayMap<String, Long>()
     private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
     private lateinit var mNotifPipeline: NotifPipeline
     private var mNow: Long = -1
@@ -195,6 +200,13 @@
             // At this point we just need to initiate the transfer
             val summaryUpdate = mPostedEntries[logicalSummary.key]
 
+            // Because we now know for certain that some child is going to alert for this summary
+            // (as we have found a child to transfer the alert to), mark the group as having
+            // interrupted. This will allow us to know in the future that the "should heads up"
+            // state of this group has already been handled, just not via the summary entry itself.
+            logicalSummary.setInterruption()
+            mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentAlert.key)
+
             // If the summary was not attached, then remove the alert from the detached summary.
             // Otherwise we can simply ignore its posted update.
             if (!isSummaryAttached) {
@@ -264,6 +276,9 @@
         }
         // After this method runs, all posted entries should have been handled (or skipped).
         mPostedEntries.clear()
+
+        // Also take this opportunity to clean up any stale entry update times
+        cleanUpEntryUpdateTimes()
     }
 
     /**
@@ -367,6 +382,12 @@
          * Notification was just added and if it should heads up, bind the view and then show it.
          */
         override fun onEntryAdded(entry: NotificationEntry) {
+            // First check whether this notification should launch a full screen intent, and
+            // launch it if needed.
+            if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
+                mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+            }
+
             // shouldHeadsUp includes check for whether this notification should be filtered
             val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
             mPostedEntries[entry.key] = PostedEntry(
@@ -378,6 +399,9 @@
                 isAlerting = false,
                 isBinding = false,
             )
+
+            // Record the last updated time for this key
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -419,6 +443,9 @@
                     cancelHeadsUpBind(posted.entry)
                 }
             }
+
+            // Update last updated time for this entry
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -426,6 +453,7 @@
          */
         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
             mPostedEntries.remove(entry.key)
+            mEntriesUpdateTimes.remove(entry.key)
             cancelHeadsUpBind(entry)
             val entryKey = entry.key
             if (mHeadsUpManager.isAlerting(entryKey)) {
@@ -454,7 +482,12 @@
             // never) in mPostedEntries to need to alert, we need to check every notification
             // known to the pipeline.
             for (entry in mNotifPipeline.allNotifs) {
-                // The only entries we can consider alerting for here are entries that have never
+                // Only consider entries that are recent enough, since we want to apply a fairly
+                // strict threshold for when an entry should be updated via only ranking and not an
+                // app-provided notification update.
+                if (!isNewEnoughForRankingUpdate(entry)) continue
+
+                // The only entries we consider alerting for here are entries that have never
                 // interrupted and that now say they should heads up; if they've alerted in the
                 // past, we don't want to incorrectly alert a second time if there wasn't an
                 // explicit notification update.
@@ -486,6 +519,41 @@
                 (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
     }
 
+    /**
+     * Sets the updated time for the given entry to the specified time.
+     */
+    @VisibleForTesting
+    fun setUpdateTime(entry: NotificationEntry, time: Long) {
+        mEntriesUpdateTimes[entry.key] = time
+    }
+
+    /**
+     * Checks whether the entry is new enough to be updated via ranking update.
+     * We want to avoid updating an entry too long after it was originally posted/updated when we're
+     * only reacting to a ranking change, as relevant ranking updates are expected to come in
+     * fairly soon after the posting of a notification.
+     */
+    private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
+        // If we don't have an update time for this key, default to "too old"
+        if (!mEntriesUpdateTimes.containsKey(entry.key)) return false
+
+        val updateTime = mEntriesUpdateTimes[entry.key] ?: return false
+        return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
+    }
+
+    private fun cleanUpEntryUpdateTimes() {
+        // Because we won't update entries that are older than this amount of time anyway, clean
+        // up any entries that are too old to notify.
+        val toRemove = ArraySet<String>()
+        for ((key, updateTime) in mEntriesUpdateTimes) {
+            if (updateTime == null ||
+                    (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
+                toRemove.add(key)
+            }
+        }
+        mEntriesUpdateTimes.removeAll(toRemove)
+    }
+
     /** When an action is pressed on a notification, end HeadsUp lifetime extension. */
     private val mActionPressListener = Consumer<NotificationEntry> { entry ->
         if (mNotifsExtendingLifetime.contains(entry)) {
@@ -597,6 +665,9 @@
     companion object {
         private const val TAG = "HeadsUpCoordinator"
         private const val BIND_TIMEOUT = 1000L
+
+        // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord.
+        private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000
     }
 
     data class PostedEntry(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 8625cdb..473c35d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -1,9 +1,10 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.util.Log
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "HeadsUpCoordinator"
@@ -68,4 +69,13 @@
             "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
         })
     }
+
+    fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = summaryKey
+            str2 = childKey
+        }, {
+            "marked group summary as interrupted: $str1 for alert transfer to child: $str2"
+        })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
deleted file mode 100644
index e3d71c8..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
+++ /dev/null
@@ -1,88 +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.statusbar.notification.collection.coordinator;
-
-import androidx.annotation.NonNull;
-
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider;
-import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
-
-import javax.inject.Inject;
-
-/**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen.
- */
-@CoordinatorScope
-public class KeyguardCoordinator implements Coordinator {
-    private static final String TAG = "KeyguardCoordinator";
-    private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
-    private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
-    private final StatusBarStateController mStatusBarStateController;
-
-    @Inject
-    public KeyguardCoordinator(
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
-            StatusBarStateController statusBarStateController) {
-        mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
-        mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
-        mStatusBarStateController = statusBarStateController;
-    }
-
-    @Override
-    public void attach(NotifPipeline pipeline) {
-
-        setupInvalidateNotifListCallbacks();
-        // Filter at the "finalize" stage so that views remain bound by PreparationCoordinator
-        pipeline.addFinalizeFilter(mNotifFilter);
-        mKeyguardNotificationVisibilityProvider
-                .addOnStateChangedListener(this::invalidateListFromFilter);
-        updateSectionHeadersVisibility();
-    }
-
-    private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
-        @Override
-        public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
-            return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry);
-        }
-    };
-
-    // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
-    // these same updates
-    private void setupInvalidateNotifListCallbacks() {
-
-    }
-
-    private void invalidateListFromFilter(String reason) {
-        updateSectionHeadersVisibility();
-        mNotifFilter.invalidateList(reason);
-    }
-
-    private void updateSectionHeadersVisibility() {
-        boolean onKeyguard = mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
-        boolean neverShowSections = mSectionHeaderVisibilityProvider.getNeverShowSectionHeaders();
-        boolean showSections = !onKeyguard && !neverShowSections;
-        mSectionHeaderVisibilityProvider.setSectionHeadersVisible(showSections);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
new file mode 100644
index 0000000..6e5fceb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
+import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
+ * headers on the lockscreen.
+ */
+@CoordinatorScope
+class KeyguardCoordinator
+@Inject
+constructor(
+    private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
+    private val keyguardRepository: KeyguardRepository,
+    private val notifPipelineFlags: NotifPipelineFlags,
+    @Application private val scope: CoroutineScope,
+    private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
+    private val statusBarStateController: StatusBarStateController,
+) : Coordinator {
+
+    private val unseenNotifications = mutableSetOf<NotificationEntry>()
+
+    override fun attach(pipeline: NotifPipeline) {
+        setupInvalidateNotifListCallbacks()
+        // Filter at the "finalize" stage so that views remain bound by PreparationCoordinator
+        pipeline.addFinalizeFilter(notifFilter)
+        keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter)
+        updateSectionHeadersVisibility()
+        if (notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard) {
+            attachUnseenFilter(pipeline)
+        }
+    }
+
+    private fun attachUnseenFilter(pipeline: NotifPipeline) {
+        pipeline.addFinalizeFilter(unseenNotifFilter)
+        pipeline.addCollectionListener(collectionListener)
+        scope.launch { clearUnseenWhenKeyguardIsDismissed() }
+    }
+
+    private suspend fun clearUnseenWhenKeyguardIsDismissed() {
+        // Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes
+        // during the timeout period
+        keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing ->
+            if (!isKeyguardShowing) {
+                unseenNotifFilter.invalidateList("keyguard no longer showing")
+                delay(SEEN_TIMEOUT)
+                unseenNotifications.clear()
+            }
+        }
+    }
+
+    private val collectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryAdded(entry: NotificationEntry) {
+                if (keyguardRepository.isKeyguardShowing()) {
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                if (keyguardRepository.isKeyguardShowing()) {
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                unseenNotifications.remove(entry)
+            }
+        }
+
+    @VisibleForTesting
+    internal val unseenNotifFilter =
+        object : NotifFilter("$TAG-unseen") {
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
+                when {
+                    // Don't apply filter if the keyguard isn't currently showing
+                    !keyguardRepository.isKeyguardShowing() -> false
+                    // Don't apply the filter if the notification is unseen
+                    unseenNotifications.contains(entry) -> false
+                    // Don't apply the filter to (non-promoted) group summaries
+                    //  - summary will be pruned if necessary, depending on if children are filtered
+                    entry.parent?.summary == entry -> false
+                    else -> true
+                }
+        }
+
+    private val notifFilter: NotifFilter =
+        object : NotifFilter(TAG) {
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
+                keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
+        }
+
+    // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
+    //  these same updates
+    private fun setupInvalidateNotifListCallbacks() {}
+
+    private fun invalidateListFromFilter(reason: String) {
+        updateSectionHeadersVisibility()
+        notifFilter.invalidateList(reason)
+    }
+
+    private fun updateSectionHeadersVisibility() {
+        val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
+        val neverShowSections = sectionHeaderVisibilityProvider.neverShowSectionHeaders
+        val showSections = !onKeyguard && !neverShowSections
+        sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections
+    }
+
+    companion object {
+        private const val TAG = "KeyguardCoordinator"
+        private val SEEN_TIMEOUT = 5.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 2480ff6..0be4bde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -16,14 +16,14 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification;
+import static com.android.systemui.media.controls.pipeline.MediaDataManagerKt.isMediaNotification;
 
 import android.os.RemoteException;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 93146f9..d2db622 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -410,7 +410,7 @@
         // Only delay release if the summary is not inflated.
         // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been
         //  done by this point, we can revert back to checking for mInflatingNotifs.contains(...)
-        if (!isInflated(group.getSummary())) {
+        if (group.getSummary() != null && !isInflated(group.getSummary())) {
             mLogger.logDelayingGroupRelease(group, group.getSummary());
             return true;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index c4f4ed5..9558f47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
index c687e1b..d804454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "ShadeEventCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index d3bc257..a2379b2 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,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.statusbar.notification.VisibilityLocationProvider;
 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,13 +56,14 @@
 // 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 VisibilityLocationProvider mVisibilityLocationProvider;
     private final VisualStabilityProvider mVisualStabilityProvider;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
 
@@ -92,16 +94,18 @@
             DelayableExecutor delayableExecutor,
             DumpManager dumpManager,
             HeadsUpManager headsUpManager,
-            NotifPanelEvents notifPanelEvents,
+            ShadeStateEvents shadeStateEvents,
             StatusBarStateController statusBarStateController,
+            VisibilityLocationProvider visibilityLocationProvider,
             VisualStabilityProvider visualStabilityProvider,
             WakefulnessLifecycle wakefulnessLifecycle) {
         mHeadsUpManager = headsUpManager;
+        mVisibilityLocationProvider = visibilityLocationProvider;
         mVisualStabilityProvider = visualStabilityProvider;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mStatusBarStateController = statusBarStateController;
         mDelayableExecutor = delayableExecutor;
-        mNotifPanelEvents = notifPanelEvents;
+        mShadeStateEvents = shadeStateEvents;
 
         dumpManager.registerDumpable(this);
     }
@@ -114,7 +118,7 @@
 
         mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
         mPulsing = mStatusBarStateController.isPulsing();
-        mNotifPanelEvents.registerListener(this);
+        mShadeStateEvents.addShadeStateEventsListener(this);
 
         pipeline.setVisualStabilityManager(mNotifStabilityManager);
     }
@@ -123,6 +127,11 @@
     //  HUNs to the top of the shade
     private final NotifStabilityManager mNotifStabilityManager =
             new NotifStabilityManager("VisualStabilityCoordinator") {
+                private boolean canMoveForHeadsUp(NotificationEntry entry) {
+                    return entry != null && mHeadsUpManager.isAlerting(entry.getKey())
+                            && !mVisibilityLocationProvider.isInVisibleLocation(entry);
+                }
+
                 @Override
                 public void onBeginRun() {
                     mIsSuppressingPipelineRun = false;
@@ -140,7 +149,7 @@
                 @Override
                 public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) {
                     final boolean isGroupChangeAllowedForEntry =
-                            mReorderingAllowed || mHeadsUpManager.isAlerting(entry.getKey());
+                            mReorderingAllowed || canMoveForHeadsUp(entry);
                     mIsSuppressingGroupChange |= !isGroupChangeAllowedForEntry;
                     return isGroupChangeAllowedForEntry;
                 }
@@ -156,7 +165,7 @@
                 public boolean isSectionChangeAllowed(@NonNull NotificationEntry entry) {
                     final boolean isSectionChangeAllowedForEntry =
                             mReorderingAllowed
-                                    || mHeadsUpManager.isAlerting(entry.getKey())
+                                    || canMoveForHeadsUp(entry)
                                     || mEntriesThatCanChangeSection.containsKey(entry.getKey());
                     if (!isSectionChangeAllowedForEntry) {
                         mEntriesWithSuppressedSectionChange.add(entry.getKey());
@@ -165,8 +174,8 @@
                 }
 
                 @Override
-                public boolean isEntryReorderingAllowed(@NonNull ListEntry section) {
-                    return mReorderingAllowed;
+                public boolean isEntryReorderingAllowed(@NonNull ListEntry entry) {
+                    return mReorderingAllowed || canMoveForHeadsUp(entry.getRepresentativeEntry());
                 }
 
                 @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
new file mode 100644
index 0000000..9ec8e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.sign
+
+class SemiStableSort {
+    val preallocatedWorkspace by lazy { ArrayList<Any>() }
+    val preallocatedAdditions by lazy { ArrayList<Any>() }
+    val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
+    val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
+        Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
+    }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be placed into the original [items] list.
+     */
+    fun <T : Any> sort(
+        items: MutableList<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+    ): Boolean =
+        withWorkspace<T, Boolean> { workspace ->
+            val ordered =
+                sortTo(
+                    items,
+                    stableOrder,
+                    comparator,
+                    workspace,
+                )
+            items.clear()
+            items.addAll(workspace)
+            return ordered
+        }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be put into [output].
+     */
+    fun <T : Any> sortTo(
+        items: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
+        // If array already has elements, use subList to ensure we only append
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        items.filterTo(result) { stableOrder.getRank(it) != null }
+        result.sortBy { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(comparator)
+        withAdditions<T> { additions ->
+            items.filterTo(additions) { stableOrder.getRank(it) == null }
+            additions.sortWith(comparator)
+            insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+        }
+        return isOrdered
+    }
+
+    /**
+     * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
+     * result in [output]. Items with a [stableOrder] will be in that order, items without a
+     * [stableOrder] will remain in same relative order as the input, and the two sets of items will
+     * be combined to have the fewest elements moved from their locations in the original.
+     */
+    fun <T : Any> stabilizeTo(
+        sortedItems: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        // Append to the output array if present
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
+        val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(stableRankComparator)
+        if (!isOrdered) {
+            result.sortWith(stableRankComparator)
+        }
+        if (result.isEmpty()) {
+            sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
+            return isOrdered
+        }
+        withAdditions<T> { additions ->
+            sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
+            if (additions.isNotEmpty()) {
+                withIndexOfComparator(sortedItems) { comparator ->
+                    insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+                }
+            }
+        }
+        return isOrdered
+    }
+
+    private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
+        preallocatedWorkspace.clear()
+        val result = block(preallocatedWorkspace as ArrayList<T>)
+        preallocatedWorkspace.clear()
+        return result
+    }
+
+    private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
+        preallocatedAdditions.clear()
+        block(preallocatedAdditions as ArrayList<T>)
+        preallocatedAdditions.clear()
+    }
+
+    private inline fun <T : Any> withIndexOfComparator(
+        sortedItems: Iterable<T>,
+        block: (Comparator<in T>) -> Unit
+    ) {
+        preallocatedMapToIndex.clear()
+        sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
+        block(preallocatedMapToIndexComparator as Comparator<in T>)
+        preallocatedMapToIndex.clear()
+    }
+
+    companion object {
+
+        /**
+         * This is the core of the algorithm.
+         *
+         * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
+         * changing the relative order of any elements already in [existing], even though those
+         * elements may be mis-ordered relative to the [comparator], such that the total number of
+         * elements which are ordered incorrectly according to the [comparator] is fewest.
+         */
+        private fun <T> insertPreSortedElementsWithFewestMisOrderings(
+            existing: MutableList<T>,
+            preSortedAdditions: Iterable<T>,
+            comparator: Comparator<in T>,
+        ) {
+            if (DEBUG) println("  To $existing insert $preSortedAdditions with fewest misordering")
+            var iStart = 0
+            preSortedAdditions.forEach { toAdd ->
+                if (DEBUG) println("    need to add $toAdd to $existing, starting at $iStart")
+                var cmpSum = 0
+                var cmpSumMax = 0
+                var iCmpSumMax = iStart
+                if (DEBUG) print("      ")
+                for (i in iCmpSumMax until existing.size) {
+                    val cmp = comparator.compare(toAdd, existing[i]).sign
+                    cmpSum += cmp
+                    if (cmpSum > cmpSumMax) {
+                        cmpSumMax = cmpSum
+                        iCmpSumMax = i + 1
+                    }
+                    if (DEBUG) print("sum[$i]=$cmpSum, ")
+                }
+                if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
+                existing.add(iCmpSumMax, toAdd)
+                iStart = iCmpSumMax + 1
+            }
+        }
+
+        /** Determines if a list is correctly sorted according to the given comparator */
+        @VisibleForTesting
+        fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
+            if (this.size <= 1) {
+                return true
+            }
+            val iterator = this.iterator()
+            var previous = iterator.next()
+            var current: T?
+            while (iterator.hasNext()) {
+                current = iterator.next()
+                if (comparator.compare(previous, current) > 0) {
+                    return false
+                }
+                previous = current
+            }
+            return true
+        }
+    }
+
+    fun interface StableOrder<T> {
+        fun getRank(item: T): Int?
+    }
+}
+
+val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
new file mode 100644
index 0000000..d8f75f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+object ShadeListBuilderHelper {
+    fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
+        getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
+
+    inline fun <T : Any, K : Any> getContiguousSubLists(
+        itemList: List<T>,
+        minLength: Int = 1,
+        key: (T) -> K,
+    ): Iterable<List<T>> {
+        val subLists = mutableListOf<List<T>>()
+        val numEntries = itemList.size
+        var currentSectionStartIndex = 0
+        var currentSectionKey: K? = null
+        for (i in 0 until numEntries) {
+            val sectionKey = key(itemList[i])
+            if (currentSectionKey == null) {
+                currentSectionKey = sectionKey
+            } else if (currentSectionKey != sectionKey) {
+                val length = i - currentSectionStartIndex
+                if (length >= minLength) {
+                    subLists.add(itemList.subList(currentSectionStartIndex, i))
+                }
+                currentSectionStartIndex = i
+                currentSectionKey = sectionKey
+            }
+        }
+        val length = numEntries - currentSectionStartIndex
+        if (length >= minLength) {
+            subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
+        }
+        return subLists
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index d8dae5d..8e052c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.collection.listbuilder
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index aa27e1e..911a2d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -20,13 +20,13 @@
 import android.service.notification.NotificationListenerService
 import android.service.notification.NotificationListenerService.RankingMap
 import android.service.notification.StatusBarNotification
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.LogLevel.WTF
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogLevel.WTF
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
 import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt
new file mode 100644
index 0000000..74ff78e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.provider
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.ListenerSet
+import javax.inject.Inject
+
+/**
+ * A class that enables communication of decisions to launch a notification's full screen intent.
+ */
+@SysUISingleton
+class LaunchFullScreenIntentProvider @Inject constructor() {
+    companion object {
+        private const val TAG = "LaunchFullScreenIntentProvider"
+    }
+    private val listeners = ListenerSet<Listener>()
+
+    /**
+     * Registers a listener with this provider. These listeners will be alerted whenever a full
+     * screen intent should be launched for a notification entry.
+     */
+    fun registerListener(listener: Listener) {
+        listeners.addIfAbsent(listener)
+    }
+
+    /** Removes the specified listener. */
+    fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    /**
+     * Sends a request to launch full screen intent for the given notification entry to all
+     * registered listeners.
+     */
+    fun launchFullScreenIntent(entry: NotificationEntry) {
+        if (listeners.isEmpty()) {
+            // This should never happen, but we should definitely know if it does because having
+            // no listeners would indicate that FSIs are getting entirely dropped on the floor.
+            Log.wtf(TAG, "no listeners found when launchFullScreenIntent requested")
+        }
+        for (listener in listeners) {
+            listener.onFullScreenIntentRequested(entry)
+        }
+    }
+
+    /** Listener interface for passing full screen intent launch decisions. */
+    fun interface Listener {
+        /**
+         * Invoked whenever a full screen intent launch is requested for the given notification
+         * entry.
+         */
+        fun onFullScreenIntentRequested(entry: NotificationEntry)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt
new file mode 100644
index 0000000..4bc4ecf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisibilityLocationProviderDelegator.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.provider
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import javax.inject.Inject
+
+/**
+ * An injectable component which delegates the visibility location computation to a delegate which
+ * can be initialized after the initial injection, generally because it's provided by a view.
+ */
+@SysUISingleton
+class VisibilityLocationProviderDelegator @Inject constructor() : VisibilityLocationProvider {
+    private var delegate: VisibilityLocationProvider? = null
+
+    fun setDelegate(provider: VisibilityLocationProvider) {
+        delegate = provider
+    }
+
+    override fun isInVisibleLocation(entry: NotificationEntry): Boolean =
+        requireNotNull(this.delegate) { "delegate not initialized" }.isInVisibleLocation(entry)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 38e3d49..9c71e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index 6d1071c..b4b9438 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import java.lang.RuntimeException
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index da4cced..a7b7a23 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,10 +32,12 @@
 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.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
+import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl;
@@ -50,6 +52,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl;
+import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -93,7 +96,7 @@
 @Module(includes = {
         CoordinatorsModule.class,
         KeyguardNotificationVisibilityProviderModule.class,
-        NotifPanelEventsModule.class,
+        ShadeEventsModule.class,
         NotifPipelineChoreographerModule.class,
         NotificationSectionHeadersModule.class,
 })
@@ -150,6 +153,11 @@
     @Binds
     NotifGutsViewManager bindNotifGutsViewManager(NotificationGutsManager notificationGutsManager);
 
+    /** Provides an instance of {@link VisibilityLocationProvider} */
+    @Binds
+    VisibilityLocationProvider bindVisibilityLocationProvider(
+            VisibilityLocationProviderDelegator visibilityLocationProviderDelegator);
+
     /** Provides an instance of {@link NotificationLogger} */
     @SysUISingleton
     @Provides
@@ -160,6 +168,7 @@
             NotificationVisibilityProvider visibilityProvider,
             NotifPipeline notifPipeline,
             StatusBarStateController statusBarStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             NotificationLogger.ExpansionStateLogger expansionStateLogger,
             NotificationPanelLogger notificationPanelLogger) {
         return new NotificationLogger(
@@ -169,6 +178,7 @@
                 visibilityProvider,
                 notifPipeline,
                 statusBarStateController,
+                shadeExpansionStateManager,
                 expansionStateLogger,
                 notificationPanelLogger);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index 5dbec8d..d4f11fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 99d320d..073b6b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index c5a6921..c4f5a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.interruption;
 
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;
 
 import android.app.NotificationManager;
 import android.content.ContentResolver;
@@ -32,6 +34,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -68,10 +72,30 @@
     private final NotificationInterruptLogger mLogger;
     private final NotifPipelineFlags mFlags;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    private final UiEventLogger mUiEventLogger;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
 
+    public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior")
+        FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235),
+
+        @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard")
+        FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236);
+
+        private final int mId;
+
+        NotificationInterruptEvent(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+
     @Inject
     public NotificationInterruptStateProviderImpl(
             ContentResolver contentResolver,
@@ -85,7 +109,8 @@
             NotificationInterruptLogger logger,
             @Main Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         mContentResolver = contentResolver;
         mPowerManager = powerManager;
         mDreamManager = dreamManager;
@@ -97,6 +122,7 @@
         mLogger = logger;
         mFlags = flags;
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+        mUiEventLogger = uiEventLogger;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -203,7 +229,9 @@
             // b/231322873: Detect and report an event when a notification has both an FSI and a
             // suppressive groupAlertBehavior, and now correctly block the FSI from firing.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior");
+            mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
             return false;
         }
@@ -249,7 +277,9 @@
             // Detect the case determined by b/231322873 to launch FSI while device is in use,
             // as blocked by the correct implementation, and report the event.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard");
+            mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 6391877..5f6a5cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -36,6 +36,7 @@
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
@@ -52,6 +53,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -92,25 +94,6 @@
     private Boolean mPanelExpanded = null;  // Use null to indicate state is not yet known
     private boolean mLogging = false;
 
-    protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
-            new OnChildLocationsChangedListener() {
-                @Override
-                public void onChildLocationsChanged() {
-                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
-                        // Visibilities will be reported when the existing
-                        // callback is executed.
-                        return;
-                    }
-                    // Calculate when we're allowed to run the visibility
-                    // reporter. Note that this timestamp might already have
-                    // passed. That's OK, the callback will just be executed
-                    // ASAP.
-                    long nextReportUptimeMs =
-                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
-                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
-                }
-            };
-
     // Tracks notifications currently visible in mNotificationStackScroller and
     // emits visibility events via NoMan on changes.
     protected Runnable mVisibilityReporter = new Runnable() {
@@ -219,6 +202,7 @@
             NotificationVisibilityProvider visibilityProvider,
             NotifPipeline notifPipeline,
             StatusBarStateController statusBarStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             ExpansionStateLogger expansionStateLogger,
             NotificationPanelLogger notificationPanelLogger) {
         mNotificationListener = notificationListener;
@@ -232,6 +216,7 @@
         mNotificationPanelLogger = notificationPanelLogger;
         // Not expected to be destroyed, don't need to unsubscribe
         statusBarStateController.addCallback(this);
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         registerNewPipelineListener();
     }
@@ -278,14 +263,14 @@
             if (DEBUG) {
                 Log.i(TAG, "startNotificationLogging");
             }
-            mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
+            mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged);
             // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
             // cause the scroller to emit child location events. Hence generate
             // one ourselves to guarantee that we're reporting visible
             // notifications.
             // (Note that in cases where the scroller does emit events, this
             // additional event doesn't break anything.)
-            mNotificationLocationsChangedListener.onChildLocationsChanged();
+            onChildLocationsChanged();
         }
     }
 
@@ -411,21 +396,6 @@
     }
 
     /**
-     * Called by CentralSurfaces to notify the logger that the panel expansion has changed.
-     * The panel may be showing any of the normal notification panel, the AOD, or the bouncer.
-     * @param isExpanded True if the panel is expanded.
-     */
-    public void onPanelExpandedChanged(boolean isExpanded) {
-        if (DEBUG) {
-            Log.i(TAG, "onPanelExpandedChanged: new=" + isExpanded);
-        }
-        mPanelExpanded = isExpanded;
-        synchronized (mDozingLock) {
-            maybeUpdateLoggingStatus();
-        }
-    }
-
-    /**
      * Called when the notification is expanded / collapsed.
      */
     public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) {
@@ -434,6 +404,36 @@
     }
 
     @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        // mPanelExpanded is initialized as null
+        if (mPanelExpanded == null || !mPanelExpanded.equals(isExpanded)) {
+            if (DEBUG) {
+                Log.i(TAG, "onPanelExpandedChanged: new=" + isExpanded);
+            }
+            mPanelExpanded = isExpanded;
+            synchronized (mDozingLock) {
+                maybeUpdateLoggingStatus();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void onChildLocationsChanged() {
+        if (mHandler.hasCallbacks(mVisibilityReporter)) {
+            // Visibilities will be reported when the existing
+            // callback is executed.
+            return;
+        }
+        // Calculate when we're allowed to run the visibility
+        // reporter. Note that this timestamp might already have
+        // passed. That's OK, the callback will just be executed
+        // ASAP.
+        long nextReportUptimeMs =
+                mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+        mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+    }
+
+    @VisibleForTesting
     public void setVisibilityReporter(Runnable visibilityReporter) {
         mVisibilityReporter = visibilityReporter;
     }
@@ -535,7 +535,7 @@
                 return;
             }
             if (loggedExpansionState != null
-                    && state.mIsExpanded == loggedExpansionState) {
+                    && Objects.equals(state.mIsExpanded, loggedExpansionState)) {
                 return;
             }
             mLoggedExpansionState.put(key, state.mIsExpanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
index 832a739..0380fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -20,8 +20,9 @@
 /** Describes usage of a notification. */
 data class NotificationMemoryUsage(
     val packageName: String,
-    val notificationId: String,
+    val notificationKey: String,
     val objectUsage: NotificationObjectUsage,
+    val viewUsage: List<NotificationViewUsage>
 )
 
 /**
@@ -39,3 +40,26 @@
     val extender: Int,
     val hasCustomView: Boolean,
 )
+
+enum class ViewType {
+    PUBLIC_VIEW,
+    PRIVATE_CONTRACTED_VIEW,
+    PRIVATE_EXPANDED_VIEW,
+    PRIVATE_HEADS_UP_VIEW,
+    TOTAL
+}
+
+/**
+ * Describes current memory of a notification view hierarchy.
+ *
+ * The values are in bytes.
+ */
+data class NotificationViewUsage(
+    val viewType: ViewType,
+    val smallIcon: Int,
+    val largeIcon: Int,
+    val systemIcons: Int,
+    val style: Int,
+    val customViews: Int,
+    val softwareBitmapsPenalty: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
new file mode 100644
index 0000000..7d39e18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
@@ -0,0 +1,212 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.annotation.WorkerThread
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */
+internal object NotificationMemoryMeter {
+
+    private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+    private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+    private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+    private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+    private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+
+    /** Returns a list of memory use entries for currently shown notifications. */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notifications: Collection<NotificationEntry>,
+    ): List<NotificationMemoryUsage> {
+        return notifications
+            .asSequence()
+            .map { entry ->
+                val packageName = entry.sbn.packageName
+                val notificationObjectUsage =
+                    notificationMemoryUse(entry.sbn.notification, hashSetOf())
+                val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row)
+                NotificationMemoryUsage(
+                    packageName,
+                    NotificationUtils.logKey(entry.sbn.key),
+                    notificationObjectUsage,
+                    notificationViewUsage
+                )
+            }
+            .toList()
+    }
+
+    @WorkerThread
+    fun notificationMemoryUse(
+        entry: NotificationEntry,
+        seenBitmaps: HashSet<Int> = hashSetOf(),
+    ): NotificationMemoryUsage {
+        return NotificationMemoryUsage(
+            entry.sbn.packageName,
+            NotificationUtils.logKey(entry.sbn.key),
+            notificationMemoryUse(entry.sbn.notification, seenBitmaps),
+            NotificationMemoryViewWalker.getViewUsage(entry.row)
+        )
+    }
+
+    /**
+     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+     * inspect Bitmaps in the object and provide summary of memory usage.
+     */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notification: Notification,
+        seenBitmaps: HashSet<Int> = hashSetOf(),
+    ): NotificationObjectUsage {
+        val extras = notification.extras
+        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+        // Collect memory usage of extra styles
+
+        // Big Picture
+        val bigPictureIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+        val bigPictureUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+        // People
+        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+        val peopleUse =
+            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+        // Calling
+        val callingPersonUse =
+            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+        val verificationIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+        // Messages
+        val messages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+            )
+        val messagesUse =
+            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+        val historicMessages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+            )
+        val historyicMessagesUse =
+            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+        // Extenders
+        val carExtender = extras.getBundle(CAR_EXTENSIONS)
+        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+        val carExtenderIcon =
+            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+        val tvExtender = extras.getBundle(TV_EXTENSIONS)
+        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+        val wearExtenderBackground =
+            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+        val style = notification.notificationStyle
+        val hasCustomView = notification.contentView != null || notification.bigContentView != null
+        val extrasSize = computeBundleSize(extras)
+
+        return NotificationObjectUsage(
+            smallIcon = smallIconUse,
+            largeIcon = largeIconUse,
+            extras = extrasSize,
+            style = style?.simpleName,
+            styleIcon =
+                bigPictureIconUse +
+                    peopleUse +
+                    callingPersonUse +
+                    verificationIconUse +
+                    messagesUse +
+                    historyicMessagesUse,
+            bigPicture = bigPictureUse,
+            extender =
+                carExtenderSize +
+                    carExtenderIcon +
+                    tvExtenderSize +
+                    wearExtenderSize +
+                    wearExtenderBackground,
+            hasCustomView = hasCustomView
+        )
+    }
+
+    /**
+     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+     * bitmaps). Can be slow.
+     */
+    private fun computeBundleSize(extras: Bundle): Int {
+        val parcel = Parcel.obtain()
+        try {
+            extras.writeToParcel(parcel, 0)
+            return parcel.dataSize()
+        } finally {
+            parcel.recycle()
+        }
+    }
+
+    /**
+     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+     * if the key does not exist in extras.
+     */
+    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+            is Icon -> computeIconUse(parcelable, seenBitmaps)
+            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+     * defined via Uri or a resource.
+     *
+     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+     */
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+        when (icon?.type) {
+            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+            else -> 0
+        }
+
+    /**
+     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+     * seenBitmaps set, this method returns 0 to avoid double counting.
+     *
+     * @return memory usage of the bitmap in bytes
+     */
+    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+        val refId = System.identityHashCode(bitmap)
+        if (seenBitmaps?.contains(refId) == true) {
+            return 0
+        }
+
+        seenBitmaps?.add(refId)
+        return bitmap.allocationByteCount
+    }
+
+    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+        val refId = System.identityHashCode(icon.dataBytes)
+        if (seenBitmaps.contains(refId)) {
+            return 0
+        }
+
+        seenBitmaps.add(refId)
+        return icon.dataLength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
index 958978e..c09cc43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -17,22 +17,11 @@
 
 package com.android.systemui.statusbar.notification.logging
 
-import android.app.Notification
-import android.app.Person
-import android.graphics.Bitmap
-import android.graphics.drawable.Icon
-import android.os.Bundle
-import android.os.Parcel
-import android.os.Parcelable
 import android.util.Log
-import androidx.annotation.WorkerThread
-import androidx.core.util.contains
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.NotificationUtils
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -46,12 +35,7 @@
 ) : Dumpable {
 
     companion object {
-        private const val TAG = "NotificationMemMonitor"
-        private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
-        private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
-        private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
-        private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
-        private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+        private const val TAG = "NotificationMemory"
     }
 
     fun init() {
@@ -60,184 +44,123 @@
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs)
+                .sortedWith(compareBy({ it.packageName }, { it.notificationKey }))
+        dumpNotificationObjects(pw, memoryUse)
+        dumpNotificationViewUsage(pw, memoryUse)
     }
 
-    @WorkerThread
-    fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
-        return notificationMemoryUse(notificationPipeline.allNotifs)
-    }
-
-    /** Returns a list of memory use entries for currently shown notifications. */
-    @WorkerThread
-    fun notificationMemoryUse(
-        notifications: Collection<NotificationEntry>
-    ): List<NotificationMemoryUsage> {
-        return notifications
-            .asSequence()
-            .map { entry ->
-                val packageName = entry.sbn.packageName
-                val notificationObjectUsage =
-                    computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
-                NotificationMemoryUsage(
-                    packageName,
-                    NotificationUtils.logKey(entry.sbn.key),
-                    notificationObjectUsage
-                )
-            }
-            .toList()
-    }
-
-    /**
-     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
-     * inspect Bitmaps in the object and provide summary of memory usage.
-     */
-    private fun computeNotificationObjectUse(
-        notification: Notification,
-        seenBitmaps: HashSet<Int>
-    ): NotificationObjectUsage {
-        val extras = notification.extras
-        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
-        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
-
-        // Collect memory usage of extra styles
-
-        // Big Picture
-        val bigPictureIconUse =
-            computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
-                computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
-        val bigPictureUse =
-            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
-                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
-
-        // People
-        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
-        val peopleUse =
-            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
-
-        // Calling
-        val callingPersonUse =
-            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
-        val verificationIconUse =
-            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
-
-        // Messages
-        val messages =
-            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
-                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
-            )
-        val messagesUse =
-            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
-        val historicMessages =
-            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
-                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
-            )
-        val historyicMessagesUse =
-            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
-
-        // Extenders
-        val carExtender = extras.getBundle(CAR_EXTENSIONS)
-        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
-        val carExtenderIcon =
-            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
-
-        val tvExtender = extras.getBundle(TV_EXTENSIONS)
-        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
-
-        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
-        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
-        val wearExtenderBackground =
-            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
-
-        val style = notification.notificationStyle
-        val hasCustomView = notification.contentView != null || notification.bigContentView != null
-        val extrasSize = computeBundleSize(extras)
-
-        return NotificationObjectUsage(
-            smallIconUse,
-            largeIconUse,
-            extrasSize,
-            style?.simpleName,
-            bigPictureIconUse +
-                peopleUse +
-                callingPersonUse +
-                verificationIconUse +
-                messagesUse +
-                historyicMessagesUse,
-            bigPictureUse,
-            carExtenderSize +
-                carExtenderIcon +
-                tvExtenderSize +
-                wearExtenderSize +
-                wearExtenderBackground,
-            hasCustomView
+    /** Renders a table of notification object usage into passed [PrintWriter]. */
+    private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
+        pw.println("Notification Object Usage")
+        pw.println("-----------")
+        pw.println(
+            "Package".padEnd(35) +
+                "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
         )
+        pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
+        pw.println()
+
+        memoryUse.forEach { use ->
+            pw.println(
+                use.packageName.padEnd(35) +
+                    "\t\t" +
+                    "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
+                    (use.objectUsage.style?.take(15) ?: "").padEnd(15) +
+                    "\t\t${use.objectUsage.styleIcon}\t" +
+                    "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
+                    "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
+                    use.notificationKey
+            )
+        }
+
+        // Calculate totals for easily glanceable summary.
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var styleIcon: Int = 0,
+            var bigPicture: Int = 0,
+            var extender: Int = 0,
+            var extras: Int = 0,
+        )
+
+        val totals =
+            memoryUse.fold(Totals()) { t, usage ->
+                t.smallIcon += usage.objectUsage.smallIcon
+                t.largeIcon += usage.objectUsage.largeIcon
+                t.styleIcon += usage.objectUsage.styleIcon
+                t.bigPicture += usage.objectUsage.bigPicture
+                t.extender += usage.objectUsage.extender
+                t.extras += usage.objectUsage.extras
+                t
+            }
+
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "".padEnd(35) +
+                "\t\t" +
+                "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
+                "".padEnd(15) +
+                "\t\t${toKb(totals.styleIcon)}\t" +
+                "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
+                toKb(totals.extras)
+        )
+        pw.println()
     }
 
-    /**
-     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
-     * bitmaps). Can be slow.
-     */
-    private fun computeBundleSize(extras: Bundle): Int {
-        val parcel = Parcel.obtain()
-        try {
-            extras.writeToParcel(parcel, 0)
-            return parcel.dataSize()
-        } finally {
-            parcel.recycle()
-        }
+    /** Renders a table of notification view usage into passed [PrintWriter] */
+    private fun dumpNotificationViewUsage(
+        pw: PrintWriter,
+        memoryUse: List<NotificationMemoryUsage>,
+    ) {
+
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var style: Int = 0,
+            var customViews: Int = 0,
+            var softwareBitmapsPenalty: Int = 0,
+        )
+
+        val totals = Totals()
+        pw.println("Notification View Usage")
+        pw.println("-----------")
+        pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
+        pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
+        pw.println()
+        memoryUse
+            .filter { it.viewUsage.isNotEmpty() }
+            .forEach { use ->
+                pw.println(use.packageName + " " + use.notificationKey)
+                use.viewUsage.forEach { view ->
+                    pw.println(
+                        "  ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
+                            "\t${view.largeIcon}\t${view.style}" +
+                            "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
+                    )
+
+                    if (view.viewType == ViewType.TOTAL) {
+                        totals.smallIcon += view.smallIcon
+                        totals.largeIcon += view.largeIcon
+                        totals.style += view.style
+                        totals.customViews += view.customViews
+                        totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
+                    }
+                }
+            }
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "  ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
+                "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
+                "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
+        )
+        pw.println()
     }
 
-    /**
-     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
-     * if the key does not exist in extras.
-     */
-    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
-        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
-            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
-            is Icon -> computeIconUse(parcelable, seenBitmaps)
-            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
-            else -> 0
-        }
-    }
-
-    /**
-     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
-     * defined via Uri or a resource.
-     *
-     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
-     */
-    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
-        when (icon?.type) {
-            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
-            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
-            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
-            else -> 0
-        }
-
-    /**
-     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
-     * seenBitmaps set, this method returns 0 to avoid double counting.
-     *
-     * @return memory usage of the bitmap in bytes
-     */
-    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
-        val refId = System.identityHashCode(bitmap)
-        if (seenBitmaps?.contains(refId) == true) {
-            return 0
-        }
-
-        seenBitmaps?.add(refId)
-        return bitmap.allocationByteCount
-    }
-
-    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
-        val refId = System.identityHashCode(icon.dataBytes)
-        if (seenBitmaps.contains(refId)) {
-            return 0
-        }
-
-        seenBitmaps.add(refId)
-        return icon.dataLength
+    private fun toKb(bytes: Int): String {
+        return (bytes / 1024).toString() + " KB"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
new file mode 100644
index 0000000..a0bee15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -0,0 +1,173 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.android.internal.R
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.util.children
+
+/** Walks view hiearchy of a given notification to estimate its memory use. */
+internal object NotificationMemoryViewWalker {
+
+    private const val TAG = "NotificationMemory"
+
+    /** Builder for [NotificationViewUsage] objects. */
+    private class UsageBuilder {
+        private var smallIcon: Int = 0
+        private var largeIcon: Int = 0
+        private var systemIcons: Int = 0
+        private var style: Int = 0
+        private var customViews: Int = 0
+        private var softwareBitmaps = 0
+
+        fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+        fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+        fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+        fun addStyle(styleUse: Int) = apply { style += styleUse }
+        fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
+            softwareBitmaps += softwareBitmapUse
+        }
+
+        fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse }
+
+        fun build(viewType: ViewType): NotificationViewUsage {
+            return NotificationViewUsage(
+                viewType = viewType,
+                smallIcon = smallIcon,
+                largeIcon = largeIcon,
+                systemIcons = systemIcons,
+                style = style,
+                customViews = customViews,
+                softwareBitmapsPenalty = softwareBitmaps,
+            )
+        }
+    }
+
+    /**
+     * Returns memory usage of public and private views contained in passed
+     * [ExpandableNotificationRow]
+     */
+    fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> {
+        if (row == null) {
+            return listOf()
+        }
+
+        // The ordering here is significant since it determines deduplication of seen drawables.
+        return listOf(
+            getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
+            getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild),
+            getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
+            getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout),
+            getTotalUsage(row)
+        )
+    }
+
+    /**
+     * Calculate total usage of all views - we need to do a separate traversal to make sure we don't
+     * double count fields.
+     */
+    private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage {
+        val totalUsage = UsageBuilder()
+        val seenObjects = hashSetOf<Int>()
+
+        row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) }
+        row.privateLayout?.let { child ->
+            for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) {
+                (view as? ViewGroup)?.let { v ->
+                    computeViewHierarchyUse(v, totalUsage, seenObjects)
+                }
+            }
+        }
+        return totalUsage.build(ViewType.TOTAL)
+    }
+
+    private fun getViewUsage(
+        type: ViewType,
+        rootView: View?,
+        seenObjects: HashSet<Int> = hashSetOf()
+    ): NotificationViewUsage {
+        val usageBuilder = UsageBuilder()
+        (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) }
+        return usageBuilder.build(type)
+    }
+
+    private fun computeViewHierarchyUse(
+        rootView: ViewGroup,
+        builder: UsageBuilder,
+        seenObjects: HashSet<Int> = hashSetOf(),
+    ) {
+        for (child in rootView.children) {
+            if (child is ViewGroup) {
+                computeViewHierarchyUse(child, builder, seenObjects)
+            } else {
+                computeViewUse(child, builder, seenObjects)
+            }
+        }
+    }
+
+    private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) {
+        if (view !is ImageView) return
+        val drawable = view.drawable ?: return
+        val drawableRef = System.identityHashCode(drawable)
+        if (seenObjects.contains(drawableRef)) return
+        val drawableUse = computeDrawableUse(drawable, seenObjects)
+        // TODO(b/235451049): We need to make sure we traverse large icon before small icon -
+        // sometimes the large icons are assigned to small icon views and we want to
+        // attribute them to large view in those cases.
+        when (view.id) {
+            R.id.left_icon,
+            R.id.icon,
+            R.id.conversation_icon -> builder.addSmallIcon(drawableUse)
+            R.id.right_icon -> builder.addLargeIcon(drawableUse)
+            R.id.big_picture -> builder.addStyle(drawableUse)
+            // Elements that are part of platform with resources
+            R.id.phishing_alert,
+            R.id.feedback,
+            R.id.alerted_icon,
+            R.id.expand_button_icon,
+            R.id.remote_input_send -> builder.addSystem(drawableUse)
+            // Custom view ImageViews
+            else -> {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Custom view: ${identifierForView(view)}")
+                }
+                builder.addCustomViews(drawableUse)
+            }
+        }
+
+        if (isDrawableSoftwareBitmap(drawable)) {
+            builder.addSoftwareBitmapPenalty(drawableUse)
+        }
+
+        seenObjects.add(drawableRef)
+    }
+
+    private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
+        when (drawable) {
+            is BitmapDrawable -> {
+                val ref = System.identityHashCode(drawable.bitmap)
+                if (seenObjects.contains(ref)) {
+                    0
+                } else {
+                    seenObjects.add(ref)
+                    drawable.bitmap.allocationByteCount
+                }
+            }
+            else -> 0
+        }
+
+    private fun isDrawableSoftwareBitmap(drawable: Drawable) =
+        drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+
+    private fun identifierForView(view: View) =
+        if (view.id == View.NO_ID) {
+            "no-id"
+        } else {
+            view.resources.getResourceName(view.id)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index fe03b2a..10197a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationRenderLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationSection
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 755e3e1..d29298a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -613,22 +613,21 @@
     protected void resetAllContentAlphas() {}
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
-        applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
-                getCurrentBackgroundRadiusBottom());
+        applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
     }
 
     @Override
-    public float getCurrentBackgroundRadiusTop() {
+    public float getTopCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction);
+        return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
     }
 
     @Override
-    public float getCurrentBackgroundRadiusBottom() {
+    public float getBottomCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction);
+        return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
     }
 
     private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 087dc71..b93e150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -93,6 +93,7 @@
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -134,6 +135,8 @@
 
     private static final String TAG = "ExpandableNotifRow";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean DEBUG_ONMEASURE =
+            Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
     private static final int COLORED_DIVIDER_ALPHA = 0x7B;
     private static final int MENU_VIEW_INDEX = 0;
@@ -154,7 +157,9 @@
         void onLayout();
     }
 
-    /** Listens for changes to the expansion state of this row. */
+    /**
+     * Listens for changes to the expansion state of this row.
+     */
     public interface OnExpansionChangedListener {
         void onExpansionChanged(boolean isExpanded);
     }
@@ -183,22 +188,34 @@
     private int mNotificationLaunchHeight;
     private boolean mMustStayOnScreen;
 
-    /** Does this row contain layouts that can adapt to row expansion */
+    /**
+     * Does this row contain layouts that can adapt to row expansion
+     */
     private boolean mExpandable;
-    /** Has the user actively changed the expansion state of this row */
+    /**
+     * Has the user actively changed the expansion state of this row
+     */
     private boolean mHasUserChangedExpansion;
-    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+    /**
+     * If {@link #mHasUserChangedExpansion}, has the user expanded this row
+     */
     private boolean mUserExpanded;
-    /** Whether the blocking helper is showing on this notification (even if dismissed) */
+    /**
+     * Whether the blocking helper is showing on this notification (even if dismissed)
+     */
     private boolean mIsBlockingHelperShowing;
 
     /**
      * Has this notification been expanded while it was pinned
      */
     private boolean mExpandedWhenPinned;
-    /** Is the user touching this row */
+    /**
+     * Is the user touching this row
+     */
     private boolean mUserLocked;
-    /** Are we showing the "public" version */
+    /**
+     * Are we showing the "public" version
+     */
     private boolean mShowingPublic;
     private boolean mSensitive;
     private boolean mSensitiveHiddenInGeneral;
@@ -351,11 +368,14 @@
     private boolean mWasChildInGroupWhenRemoved;
     private NotificationInlineImageResolver mImageResolver;
     private NotificationMediaManager mMediaManager;
-    @Nullable private OnExpansionChangedListener mExpansionChangedListener;
-    @Nullable private Runnable mOnIntrinsicHeightReachedRunnable;
+    @Nullable
+    private OnExpansionChangedListener mExpansionChangedListener;
+    @Nullable
+    private Runnable mOnIntrinsicHeightReachedRunnable;
 
     private float mTopRoundnessDuringLaunchAnimation;
     private float mBottomRoundnessDuringLaunchAnimation;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     /**
      * Returns whether the given {@code statusBarNotification} is a system notification.
@@ -574,14 +594,18 @@
         }
     }
 
-    /** Called when the notification's ranking was changed (but nothing else changed). */
+    /**
+     * Called when the notification's ranking was changed (but nothing else changed).
+     */
     public void onNotificationRankingUpdated() {
         if (mMenuRow != null) {
             mMenuRow.onNotificationUpdated(mEntry.getSbn());
         }
     }
 
-    /** Call when bubble state has changed and the button on the notification should be updated. */
+    /**
+     * Call when bubble state has changed and the button on the notification should be updated.
+     */
     public void updateBubbleButton() {
         for (NotificationContentView l : mLayouts) {
             l.updateBubbleButton(mEntry);
@@ -620,6 +644,7 @@
 
     /**
      * Sets a supplier that can determine whether the keyguard is secure or not.
+     *
      * @param secureStateProvider A function that returns true if keyguard is secure.
      */
     public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
@@ -781,7 +806,9 @@
         mChildrenContainer.setUntruncatedChildCount(childCount);
     }
 
-    /** Called after children have been attached to set the expansion states */
+    /**
+     * Called after children have been attached to set the expansion states
+     */
     public void resetChildSystemExpandedStates() {
         if (isSummaryWithChildren()) {
             mChildrenContainer.updateExpansionStates();
@@ -791,7 +818,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
@@ -809,10 +836,12 @@
         }
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(false, null);
-        row.setBottomRoundness(0.0f, false /* animate */);
+        row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
     }
 
-    /** Returns the child notification at [index], or null if no such child. */
+    /**
+     * Returns the child notification at [index], or null if no such child.
+     */
     @Nullable
     public ExpandableNotificationRow getChildNotificationAt(int index) {
         if (mChildrenContainer == null
@@ -834,7 +863,7 @@
 
     /**
      * @param isChildInGroup Is this notification now in a group
-     * @param parent the new parent notification
+     * @param parent         the new parent notification
      */
     public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {
         if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) {
@@ -898,7 +927,9 @@
         return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren();
     }
 
-    /** Updates states of all children. */
+    /**
+     * Updates states of all children.
+     */
     public void updateChildrenStates(AmbientState ambientState) {
         if (mIsSummaryWithChildren) {
             ExpandableViewState parentState = getViewState();
@@ -906,21 +937,27 @@
         }
     }
 
-    /** Applies children states. */
+    /**
+     * Applies children states.
+     */
     public void applyChildrenState() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.applyState();
         }
     }
 
-    /** Prepares expansion changed. */
+    /**
+     * Prepares expansion changed.
+     */
     public void prepareExpansionChanged() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.prepareExpansionChanged();
         }
     }
 
-    /** Starts child animations. */
+    /**
+     * Starts child animations.
+     */
     public void startChildAnimation(AnimationProperties properties) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.startAnimationToState(properties);
@@ -984,7 +1021,7 @@
         if (mIsSummaryWithChildren) {
             return mChildrenContainer.getIntrinsicHeight();
         }
-        if(mExpandedWhenPinned) {
+        if (mExpandedWhenPinned) {
             return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
         } else if (atLeastMinHeight) {
             return Math.max(getCollapsedHeight(), getHeadsUpHeight());
@@ -1079,18 +1116,22 @@
         updateClickAndFocus();
     }
 
-    /** The click listener for the bubble button. */
+    /**
+     * The click listener for the bubble button.
+     */
     public View.OnClickListener getBubbleClickListener() {
         return v -> {
             if (mBubblesManagerOptional.isPresent()) {
                 mBubblesManagerOptional.get()
-                    .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
+                        .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
             }
             mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
         };
     }
 
-    /** The click listener for the snooze button. */
+    /**
+     * The click listener for the snooze button.
+     */
     public View.OnClickListener getSnoozeClickListener(MenuItem item) {
         return v -> {
             // Dismiss a snoozed notification if one is still left behind
@@ -1252,7 +1293,7 @@
     }
 
     public void setContentBackground(int customBackgroundColor, boolean animate,
-            NotificationContentView notificationContentView) {
+                                     NotificationContentView notificationContentView) {
         if (getShowingLayout() == notificationContentView) {
             setTintColor(customBackgroundColor, animate);
         }
@@ -1302,21 +1343,6 @@
         return mOnKeyguard;
     }
 
-    public void removeAllChildren() {
-        List<ExpandableNotificationRow> notificationChildren =
-                mChildrenContainer.getAttachedChildren();
-        ArrayList<ExpandableNotificationRow> clonedList = new ArrayList<>(notificationChildren);
-        for (int i = 0; i < clonedList.size(); i++) {
-            ExpandableNotificationRow row = clonedList.get(i);
-            if (row.keepInParent()) {
-                continue;
-            }
-            mChildrenContainer.removeNotification(row);
-            row.setIsChildInGroup(false, null);
-        }
-        onAttachedChildrenCountChanged();
-    }
-
     @Override
     public void dismiss(boolean refocusOnDismiss) {
         super.dismiss(refocusOnDismiss);
@@ -1487,7 +1513,7 @@
             l.setAlpha(alpha);
         }
         if (mChildrenContainer != null) {
-            mChildrenContainer.setAlpha(alpha);
+            mChildrenContainer.setContentAlpha(alpha);
         }
     }
 
@@ -1637,7 +1663,9 @@
         setTargetPoint(null);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.setFeedbackIcon(icon);
@@ -1646,7 +1674,9 @@
         mPublicLayout.setFeedbackIcon(icon);
     }
 
-    /** Sets the last time the notification being displayed audibly alerted the user. */
+    /**
+     * Sets the last time the notification being displayed audibly alerted the user.
+     */
     public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) {
         long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs;
         boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS;
@@ -1696,11 +1726,18 @@
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         Trace.beginSection(appendTraceStyleTag("ExpNotRow#onMeasure"));
+        if (DEBUG_ONMEASURE) {
+            Log.d(TAG, "onMeasure("
+                    + "widthMeasureSpec=" + MeasureSpec.toString(widthMeasureSpec) + ", "
+                    + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
+        }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         Trace.endSection();
     }
 
-    /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */
+    /**
+     * Generates and appends "(MessagingStyle)" type tag to passed string for tracing.
+     */
     @NonNull
     private String appendTraceStyleTag(@NonNull String traceTag) {
         if (!Trace.isEnabled()) {
@@ -1721,7 +1758,7 @@
         super.onFinishInflate();
         mPublicLayout = findViewById(R.id.expandedPublic);
         mPrivateLayout = findViewById(R.id.expanded);
-        mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
 
         for (NotificationContentView l : mLayouts) {
             l.setExpandClickListener(mExpandClickListener);
@@ -1740,6 +1777,7 @@
             mChildrenContainer.setIsLowPriority(mIsLowPriority);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
 
             mTranslateableViews.add(mChildrenContainer);
         });
@@ -1796,6 +1834,7 @@
     /**
      * Perform a smart action which triggers a longpress (expose guts).
      * Based on the semanticAction passed, may update the state of the guts view.
+     *
      * @param semanticAction associated with this smart action click
      */
     public void doSmartActionClick(int x, int y, int semanticAction) {
@@ -1939,9 +1978,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     @Override
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
@@ -1955,6 +1995,14 @@
             if (previousTranslation != 0) {
                 setTranslation(previousTranslation);
             }
+            if (mChildrenContainer != null) {
+                List<ExpandableNotificationRow> notificationChildren =
+                        mChildrenContainer.getAttachedChildren();
+                for (int i = 0; i < notificationChildren.size(); i++) {
+                    ExpandableNotificationRow child = notificationChildren.get(i);
+                    child.setDismissUsingRowTranslationX(usingRowTranslationX);
+                }
+            }
         }
     }
 
@@ -2009,7 +2057,7 @@
     }
 
     public Animator getTranslateViewAnimator(final float leftTarget,
-            AnimatorUpdateListener listener) {
+                                             AnimatorUpdateListener listener) {
         if (mTranslateAnim != null) {
             mTranslateAnim.cancel();
         }
@@ -2115,7 +2163,7 @@
                             NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
             float startTop = params.getStartNotificationTop();
             top = (int) Math.min(MathUtils.lerp(startTop,
-                    params.getTop(), expandProgress),
+                            params.getTop(), expandProgress),
                     startTop);
         } else {
             top = params.getTop();
@@ -2151,29 +2199,30 @@
         }
         setTranslationY(top);
 
-        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius;
-        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius;
+        final float maxRadius = getMaxRadius();
+        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius;
+        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius;
         invalidateOutline();
 
         mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
     }
 
     @Override
-    public float getCurrentTopRoundness() {
+    public float getTopRoundness() {
         if (mExpandAnimationRunning) {
             return mTopRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentTopRoundness();
+        return super.getTopRoundness();
     }
 
     @Override
-    public float getCurrentBottomRoundness() {
+    public float getBottomRoundness() {
         if (mExpandAnimationRunning) {
             return mBottomRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentBottomRoundness();
+        return super.getBottomRoundness();
     }
 
     public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -2284,7 +2333,7 @@
     /**
      * Set this notification to be expanded by the user
      *
-     * @param userExpanded whether the user wants this notification to be expanded
+     * @param userExpanded        whether the user wants this notification to be expanded
      * @param allowChildExpansion whether a call to this method allows expanding children
      */
     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
@@ -2434,7 +2483,7 @@
 
     /**
      * @return {@code true} if the notification can show it's heads up layout. This is mostly true
-     *         except for legacy use cases.
+     * except for legacy use cases.
      */
     public boolean canShowHeadsUp() {
         if (mOnKeyguard && !isDozing() && !isBypassEnabled()) {
@@ -2625,7 +2674,7 @@
 
     @Override
     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
-            long duration) {
+                                 long duration) {
         if (getVisibility() == GONE) {
             // If we are GONE, the hideSensitive parameter will not be calculated and always be
             // false, which is incorrect, let's wait until a real call comes in later.
@@ -2658,9 +2707,9 @@
 
     private void animateShowingPublic(long delay, long duration, boolean showingPublic) {
         View[] privateViews = mIsSummaryWithChildren
-                ? new View[] {mChildrenContainer}
-                : new View[] {mPrivateLayout};
-        View[] publicViews = new View[] {mPublicLayout};
+                ? new View[]{mChildrenContainer}
+                : new View[]{mPrivateLayout};
+        View[] publicViews = new View[]{mPublicLayout};
         View[] hiddenChildren = showingPublic ? privateViews : publicViews;
         View[] shownChildren = showingPublic ? publicViews : privateViews;
         for (final View hiddenView : hiddenChildren) {
@@ -2693,8 +2742,8 @@
 
     /**
      * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
-     *         otherwise some state might not be updated. To request about the general clearability
-     *         see {@link NotificationEntry#isDismissable()}.
+     * otherwise some state might not be updated. To request about the general clearability
+     * see {@link NotificationEntry#isDismissable()}.
      */
     public boolean canViewBeDismissed() {
         return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
@@ -2777,8 +2826,13 @@
     }
 
     @Override
-    public long performRemoveAnimation(long duration, long delay, float translationDirection,
-            boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable,
+    public long performRemoveAnimation(
+            long duration,
+            long delay,
+            float translationDirection,
+            boolean isHeadsUpAnimation,
+            float endLocation,
+            Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         if (mMenuRow != null && mMenuRow.isMenuVisible()) {
             Animator anim = getTranslateViewAnimator(0f, null /* listener */);
@@ -2828,7 +2882,9 @@
         }
     }
 
-    /** Gets the last value set with {@link #setNotificationFaded(boolean)} */
+    /**
+     * Gets the last value set with {@link #setNotificationFaded(boolean)}
+     */
     @Override
     public boolean isNotificationFaded() {
         return mIsFaded;
@@ -2843,7 +2899,7 @@
      * notifications return false from {@link #hasOverlappingRendering()} and delegate the
      * layerType to child views which really need it in order to render correctly, such as icon
      * views or the conversation face pile.
-     *
+     * <p>
      * Another compounding factor for notifications is that we change clipping on each frame of the
      * animation, so the hardware layer isn't able to do any caching at the top level, but the
      * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we
@@ -2869,7 +2925,9 @@
         }
     }
 
-    /** Private helper for iterating over the layouts and children containers to set faded state */
+    /**
+     * Private helper for iterating over the layouts and children containers to set faded state
+     */
     private void setNotificationFadedOnChildren(boolean faded) {
         delegateNotificationFaded(mChildrenContainer, faded);
         for (NotificationContentView layout : mLayouts) {
@@ -2897,7 +2955,7 @@
      * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the
      * row should require overlapping rendering to ensure that the overlapped view doesn't bleed
      * through when alpha fading.
-     *
+     * <p>
      * Note that this currently works for top-level notifications which squish their height down
      * while collapsing the shade, but does not work for children inside groups, because the
      * accordion affect does not apply to those views, so super.hasOverlappingRendering() will
@@ -2976,7 +3034,7 @@
             return mGuts.getIntrinsicHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp
                 && mHeadsUpManager.isTrackingHeadsUp()) {
-                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+            return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
         } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) {
             return mChildrenContainer.getMinHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) {
@@ -3218,8 +3276,8 @@
             MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext());
             if (snoozeMenu != null) {
                 AccessibilityAction action = new AccessibilityAction(R.id.action_snooze,
-                    getContext().getResources()
-                        .getString(R.string.notification_menu_snooze_action));
+                        getContext().getResources()
+                                .getString(R.string.notification_menu_snooze_action));
                 info.addAction(action);
             }
         }
@@ -3280,17 +3338,17 @@
             NotificationContentView contentView = (NotificationContentView) child;
             if (isClippingNeeded()) {
                 return true;
-            } else if (!hasNoRounding()
-                    && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f,
-                    getCurrentBottomRoundness() != 0.0f)) {
+            } else if (hasRoundedCorner()
+                    && contentView.shouldClipToRounding(getTopRoundness() != 0.0f,
+                    getBottomRoundness() != 0.0f)) {
                 return true;
             }
         } else if (child == mChildrenContainer) {
-            if (isClippingNeeded() || !hasNoRounding()) {
+            if (isClippingNeeded() || hasRoundedCorner()) {
                 return true;
             }
         } else if (child instanceof NotificationGuts) {
-            return !hasNoRounding();
+            return hasRoundedCorner();
         }
         return super.childNeedsClipping(child);
     }
@@ -3316,14 +3374,17 @@
     }
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
         applyChildrenRoundness();
     }
 
     private void applyChildrenRoundness() {
         if (mIsSummaryWithChildren) {
-            mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness());
+            mChildrenContainer.requestBottomRoundness(
+                    getBottomRoundness(),
+                    /* animate = */ false,
+                    SourceType.DefaultValue);
         }
     }
 
@@ -3335,10 +3396,6 @@
         return super.getCustomClipPath(child);
     }
 
-    private boolean hasNoRounding() {
-        return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f;
-    }
-
     public boolean isMediaRow() {
         return mEntry.getSbn().getNotification().isMediaNotification();
     }
@@ -3434,6 +3491,7 @@
     public interface LongPressListener {
         /**
          * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
+         *
          * @return whether the longpress was handled
          */
         boolean onLongPress(View v, int x, int y, MenuItem item);
@@ -3455,6 +3513,7 @@
     public interface CoordinateOnClickListener {
         /**
          * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates
+         *
          * @return whether the click was handled
          */
         boolean onClick(View v, int x, int y, MenuItem item);
@@ -3511,7 +3570,19 @@
     private void setTargetPoint(Point p) {
         mTargetPoint = p;
     }
+
     public Point getTargetPoint() {
         return mTargetPoint;
     }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+        if (mChildrenContainer != null) {
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a493a67..842526e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -231,6 +231,8 @@
                 mStatusBarStateController.removeCallback(mStatusBarStateListener);
             }
         });
+        mView.enableNotificationGroupCorner(
+                mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
     }
 
     private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index d58fe3b..4fde5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,46 +28,21 @@
 import android.view.ViewOutlineProvider;
 
 import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 
 /**
  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
  */
 public abstract class ExpandableOutlineView extends ExpandableView {
 
-    private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
-            "topRoundness",
-            ExpandableOutlineView::setTopRoundnessInternal,
-            ExpandableOutlineView::getCurrentTopRoundness,
-            R.id.top_roundess_animator_tag,
-            R.id.top_roundess_animator_end_tag,
-            R.id.top_roundess_animator_start_tag);
-    private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
-            "bottomRoundness",
-            ExpandableOutlineView::setBottomRoundnessInternal,
-            ExpandableOutlineView::getCurrentBottomRoundness,
-            R.id.bottom_roundess_animator_tag,
-            R.id.bottom_roundess_animator_end_tag,
-            R.id.bottom_roundess_animator_start_tag);
-    private static final AnimationProperties ROUNDNESS_PROPERTIES =
-            new AnimationProperties().setDuration(
-                    StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS);
+    private RoundableState mRoundableState;
     private static final Path EMPTY_PATH = new Path();
-
     private final Rect mOutlineRect = new Rect();
-    private final Path mClipPath = new Path();
     private boolean mCustomOutline;
     private float mOutlineAlpha = -1f;
-    protected float mOutlineRadius;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    private float mCurrentBottomRoundness;
-    private float mCurrentTopRoundness;
-    private float mBottomRoundness;
-    private float mTopRoundness;
     private int mBackgroundTop;
 
     /**
@@ -80,8 +55,7 @@
     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
         @Override
         public void getOutline(View view, Outline outline) {
-            if (!mCustomOutline && getCurrentTopRoundness() == 0.0f
-                    && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) {
+            if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) {
                 // Only when translating just the contents, does the outline need to be shifted.
                 int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
                 int left = Math.max(translation, 0);
@@ -99,14 +73,18 @@
         }
     };
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected Path getClipPath(boolean ignoreTranslation) {
         int left;
         int top;
         int right;
         int bottom;
         int height;
-        float topRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusTop();
+        float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius();
         if (!mCustomOutline) {
             // The outline just needs to be shifted if we're translating the contents. Otherwise
             // it's already in the right place.
@@ -130,12 +108,11 @@
         if (height == 0) {
             return EMPTY_PATH;
         }
-        float bottomRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
+        float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
         if (topRoundness + bottomRoundness > height) {
             float overShoot = topRoundness + bottomRoundness - height;
-            float currentTopRoundness = getCurrentTopRoundness();
-            float currentBottomRoundness = getCurrentBottomRoundness();
+            float currentTopRoundness = getTopRoundness();
+            float currentBottomRoundness = getBottomRoundness();
             topRoundness -= overShoot * currentTopRoundness
                     / (currentTopRoundness + currentBottomRoundness);
             bottomRoundness -= overShoot * currentBottomRoundness
@@ -145,8 +122,18 @@
         return mTmpPath;
     }
 
-    public void getRoundedRectPath(int left, int top, int right, int bottom,
-            float topRoundness, float bottomRoundness, Path outPath) {
+    /**
+     * Add a round rect in {@code outPath}
+     * @param outPath destination path
+     */
+    public void getRoundedRectPath(
+            int left,
+            int top,
+            int right,
+            int bottom,
+            float topRoundness,
+            float bottomRoundness,
+            Path outPath) {
         outPath.reset();
         mTmpCornerRadii[0] = topRoundness;
         mTmpCornerRadii[1] = topRoundness;
@@ -168,15 +155,28 @@
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         canvas.save();
+        Path clipPath = null;
+        Path childClipPath = null;
         if (childNeedsClipping(child)) {
-            Path clipPath = getCustomClipPath(child);
+            clipPath = getCustomClipPath(child);
             if (clipPath == null) {
                 clipPath = getClipPath(false /* ignoreTranslation */);
             }
-            if (clipPath != null) {
-                canvas.clipPath(clipPath);
+            // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the
+            // children instead.
+            if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) {
+                childClipPath = clipPath;
+                clipPath = null;
             }
         }
+
+        if (child instanceof NotificationChildrenContainer) {
+            ((NotificationChildrenContainer) child).setChildClipPath(childClipPath);
+        }
+        if (clipPath != null) {
+            canvas.clipPath(clipPath);
+        }
+
         boolean result = super.drawChild(canvas, child, drawingTime);
         canvas.restore();
         return result;
@@ -207,73 +207,21 @@
 
     private void initDimens() {
         Resources res = getResources();
-        mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
-        if (!mAlwaysRoundBothCorners) {
-            mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
+        float maxRadius;
+        if (mAlwaysRoundBothCorners) {
+            maxRadius = res.getDimension(R.dimen.notification_shadow_radius);
+        } else {
+            maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
         }
+        mRoundableState = new RoundableState(this, this, maxRadius);
         setClipToOutline(mAlwaysRoundBothCorners);
     }
 
     @Override
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        if (mTopRoundness != topRoundness) {
-            float diff = Math.abs(topRoundness - mTopRoundness);
-            mTopRoundness = topRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
-    }
-
-    protected void applyRoundness() {
+    public void applyRoundness() {
         invalidateOutline();
-        invalidate();
-    }
-
-    public float getCurrentBackgroundRadiusTop() {
-        return getCurrentTopRoundness() * mOutlineRadius;
-    }
-
-    public float getCurrentTopRoundness() {
-        return mCurrentTopRoundness;
-    }
-
-    public float getCurrentBottomRoundness() {
-        return mCurrentBottomRoundness;
-    }
-
-    public float getCurrentBackgroundRadiusBottom() {
-        return getCurrentBottomRoundness() * mOutlineRadius;
-    }
-
-    @Override
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        if (mBottomRoundness != bottomRoundness) {
-            float diff = Math.abs(bottomRoundness - mBottomRoundness);
-            mBottomRoundness = bottomRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
+        super.applyRoundness();
     }
 
     protected void setBackgroundTop(int backgroundTop) {
@@ -283,16 +231,6 @@
         }
     }
 
-    private void setTopRoundnessInternal(float topRoundness) {
-        mCurrentTopRoundness = topRoundness;
-        applyRoundness();
-    }
-
-    private void setBottomRoundnessInternal(float bottomRoundness) {
-        mCurrentBottomRoundness = bottomRoundness;
-        applyRoundness();
-    }
-
     public void onDensityOrFontScaleChanged() {
         initDimens();
         applyRoundness();
@@ -348,9 +286,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
         mDismissUsingRowTranslationX = usingRowTranslationX;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 38f0c55..955d7c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -36,6 +36,8 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.util.DumpUtilsKt;
@@ -47,9 +49,10 @@
 /**
  * An abstract view for expandable views.
  */
-public abstract class ExpandableView extends FrameLayout implements Dumpable {
+public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
     private static final String TAG = "ExpandableView";
 
+    private RoundableState mRoundableState = null;
     protected OnHeightChangedListener mOnHeightChangedListener;
     private int mActualHeight;
     protected int mClipTopAmount;
@@ -78,6 +81,14 @@
         initDimens();
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        if (mRoundableState == null) {
+            mRoundableState = new RoundableState(this, this, 0f);
+        }
+        return mRoundableState;
+    }
+
     private void initDimens() {
         mContentShift = getResources().getDimensionPixelSize(
                 R.dimen.shelf_transform_content_shift);
@@ -440,8 +451,7 @@
             int top = getClipTopAmount();
             int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding()
                     - mClipBottomAmount, top), mMinimumHeightForClipping);
-            int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
-            mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom);
+            mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
             setClipBounds(mClipRect);
         } else {
             setClipBounds(null);
@@ -455,7 +465,6 @@
 
     public void setExtraWidthForClipping(float extraWidthForClipping) {
         mExtraWidthForClipping = extraWidthForClipping;
-        updateClipping();
     }
 
     public float getHeaderVisibleAmount() {
@@ -844,22 +853,6 @@
         return mFirstInSection;
     }
 
-    /**
-     * Set the topRoundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        return false;
-    }
-
-    /**
-     * Set the bottom roundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        return false;
-    }
-
     public int getHeadsUpHeightWithoutHeader() {
         return getHeight();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index ab91926..46fef3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 4c69304..c534860 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -40,7 +40,7 @@
 import com.android.internal.widget.ImageMessageConsumer;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8de0365..277ad8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1374,13 +1374,8 @@
         if (bubbleButton == null || actionContainer == null) {
             return;
         }
-        boolean isPersonWithShortcut =
-                mPeopleIdentifier.getPeopleNotificationType(entry)
-                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
-        boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
-                && isPersonWithShortcut
-                && entry.getBubbleMetadata() != null;
-        if (showButton) {
+
+        if (shouldShowBubbleButton(entry)) {
             // explicitly resolve drawable resource using SystemUI's theme
             Drawable d = mContext.getDrawable(entry.isBubble()
                     ? R.drawable.bubble_ic_stop_bubble
@@ -1410,6 +1405,16 @@
         }
     }
 
+    @VisibleForTesting
+    boolean shouldShowBubbleButton(NotificationEntry entry) {
+        boolean isPersonWithShortcut =
+                mPeopleIdentifier.getPeopleNotificationType(entry)
+                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
+        return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+                && isPersonWithShortcut
+                && entry.getBubbleMetadata() != null;
+    }
+
     private void applySnoozeAction(View layout) {
         if (layout == null || mContainingNotification == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index f9923b2..8a5d29a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 7a65436..f13e48d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -35,12 +35,15 @@
 
 import com.android.internal.widget.CachingIconView;
 import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.TransformableView;
 import com.android.systemui.statusbar.ViewTransformationHelper;
 import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.ImageTransformState;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.TransformState;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
@@ -49,13 +52,12 @@
 /**
  * Wraps a notification view which may or may not include a header.
  */
-public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable {
 
+    private final RoundableState mRoundableState;
     private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
             = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
-
     protected final ViewTransformationHelper mTransformationHelper;
-
     private CachingIconView mIcon;
     private NotificationExpandButton mExpandButton;
     private View mAltExpandTarget;
@@ -67,12 +69,16 @@
     private ImageView mWorkProfileImage;
     private View mAudiblyAlertedIcon;
     private View mFeedbackIcon;
-
     private boolean mIsLowPriority;
     private boolean mTransformLowPriorityTitle;
 
     protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
         super(ctx, view, row);
+        mRoundableState = new RoundableState(
+                mView,
+                this,
+                ctx.getResources().getDimension(R.dimen.notification_corner_radius)
+        );
         mTransformationHelper = new ViewTransformationHelper();
 
         // we want to avoid that the header clashes with the other text when transforming
@@ -81,7 +87,8 @@
                 new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
 
                     @Override
-                    public Interpolator getCustomInterpolator(int interpolationType,
+                    public Interpolator getCustomInterpolator(
+                            int interpolationType,
                             boolean isFrom) {
                         boolean isLowPriority = mView instanceof NotificationHeaderView;
                         if (interpolationType == TRANSFORM_Y) {
@@ -99,11 +106,17 @@
                     protected boolean hasCustomTransformation() {
                         return mIsLowPriority && mTransformLowPriorityTitle;
                     }
-                }, TRANSFORMING_VIEW_TITLE);
+                },
+                TRANSFORMING_VIEW_TITLE);
         resolveHeaderViews();
         addFeedbackOnClickListener(row);
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected void resolveHeaderViews() {
         mIcon = mView.findViewById(com.android.internal.R.id.icon);
         mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -128,7 +141,9 @@
         }
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     @Override
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mFeedbackIcon != null) {
@@ -193,7 +208,7 @@
                     // its animation
                     && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
                 ((ImageView) child).setCropToPadding(true);
-            } else if (child instanceof ViewGroup){
+            } else if (child instanceof ViewGroup) {
                 ViewGroup group = (ViewGroup) child;
                 for (int i = 0; i < group.getChildCount(); i++) {
                     stack.push(group.getChildAt(i));
@@ -215,7 +230,9 @@
     }
 
     @Override
-    public void updateExpandability(boolean expandable, View.OnClickListener onClickListener,
+    public void updateExpandability(
+            boolean expandable,
+            View.OnClickListener onClickListener,
             boolean requestLayout) {
         mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
         mExpandButton.setOnClickListener(expandable ? onClickListener : null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 2719dd8..6f4d6d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -142,6 +142,11 @@
      */
     private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false;
 
+    /**
+     * Whether the shade is currently closing.
+     */
+    private boolean mIsClosing;
+
     @VisibleForTesting
     public boolean isFlingRequiredAfterLockScreenSwipeUp() {
         return mIsFlingRequiredAfterLockScreenSwipeUp;
@@ -714,7 +719,21 @@
      */
     public boolean isBouncerInTransit() {
         return mStatusBarKeyguardViewManager != null
-                && mStatusBarKeyguardViewManager.isBouncerInTransit();
+                && mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit();
+    }
+
+    /**
+     * @param isClosing Whether the shade is currently closing.
+     */
+    public void setIsClosing(boolean isClosing) {
+        mIsClosing = isClosing;
+    }
+
+    /**
+     * @return Whether the shade is currently closing.
+     */
+    public boolean isClosing() {
+        return mIsClosing;
     }
 
     @Override
@@ -761,5 +780,6 @@
                 + mIsFlingRequiredAfterLockScreenSwipeUp);
         pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements);
         pw.println("mBaseZHeight=" + mBaseZHeight);
+        pw.println("mIsClosing=" + mIsClosing);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..d43ca823 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -21,7 +21,11 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
 import android.graphics.drawable.ColorDrawable;
+import android.os.Trace;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -33,6 +37,7 @@
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -40,13 +45,18 @@
 import com.android.systemui.R;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.NotificationGroupingUtil;
+import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.HybridGroupManager;
 import com.android.systemui.statusbar.notification.row.HybridNotificationView;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
 
 import java.util.ArrayList;
@@ -56,7 +66,7 @@
  * A container containing child notifications
  */
 public class NotificationChildrenContainer extends ViewGroup
-        implements NotificationFadeAware {
+        implements NotificationFadeAware, Roundable {
 
     private static final String TAG = "NotificationChildrenContainer";
 
@@ -100,9 +110,9 @@
     private boolean mEnableShadowOnChildNotifications;
 
     private NotificationHeaderView mNotificationHeader;
-    private NotificationViewWrapper mNotificationHeaderWrapper;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
     private NotificationHeaderView mNotificationHeaderLowPriority;
-    private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
     private NotificationGroupingUtil mGroupingUtil;
     private ViewState mHeaderViewState;
     private int mClipBottomAmount;
@@ -110,7 +120,8 @@
     private OnClickListener mHeaderClickListener;
     private ViewGroup mCurrentHeader;
     private boolean mIsConversation;
-
+    private Path mChildClipPath = null;
+    private final Path mHeaderPath = new Path();
     private boolean mShowGroupCountInExpander;
     private boolean mShowDividersWhenExpanded;
     private boolean mHideDividersDuringExpand;
@@ -119,6 +130,8 @@
     private float mHeaderVisibleAmount = 1.0f;
     private int mUntruncatedChildCount;
     private boolean mContainingNotificationIsFaded = false;
+    private RoundableState mRoundableState;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -132,10 +145,14 @@
         this(context, attrs, defStyleAttr, 0);
     }
 
-    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+    public NotificationChildrenContainer(
+            Context context,
+            AttributeSet attrs,
+            int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         mHybridGroupManager = new HybridGroupManager(getContext());
+        mRoundableState = new RoundableState(this, this, 0f);
         initDimens();
         setClipChildren(false);
     }
@@ -167,6 +184,12 @@
         mHybridGroupManager.initDimens();
     }
 
+    @NonNull
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         int childCount =
@@ -197,6 +220,7 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Trace.beginSection("NotificationChildrenContainer#onMeasure");
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
@@ -245,6 +269,7 @@
         }
 
         setMeasuredDimension(width, height);
+        Trace.endSection();
     }
 
     @Override
@@ -271,7 +296,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addNotification(ExpandableNotificationRow row, int childIndex) {
@@ -287,6 +312,11 @@
 
         row.setContentTransformationAmount(0, false /* isLastChild */);
         row.setNotificationFaded(mContainingNotificationIsFaded);
+
+        // This is a workaround, the NotificationShelf should be the owner of `OnScroll` roundness.
+        // Here we should reset the `OnScroll` roundness only on top-level rows.
+        NotificationShelf.resetOnScrollRoundness(row);
+
         // It doesn't make sense to keep old animations around, lets cancel them!
         ExpandableViewState viewState = row.getViewState();
         if (viewState != null) {
@@ -347,8 +377,11 @@
             mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
                     .setVisibility(VISIBLE);
             mNotificationHeader.setOnClickListener(mHeaderClickListener);
-            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
-                    mNotificationHeader, mContainingNotification);
+            mNotificationHeaderWrapper =
+                    (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                            getContext(),
+                            mNotificationHeader,
+                            mContainingNotification);
             addView(mNotificationHeader, 0);
             invalidate();
         } else {
@@ -381,8 +414,11 @@
                 mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
                         .setVisibility(VISIBLE);
                 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
-                mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
-                        mNotificationHeaderLowPriority, mContainingNotification);
+                mNotificationHeaderWrapperLowPriority =
+                        (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                                getContext(),
+                                mNotificationHeaderLowPriority,
+                                mContainingNotification);
                 addView(mNotificationHeaderLowPriority, 0);
                 invalidate();
             } else {
@@ -461,7 +497,23 @@
         return mAttachedChildren;
     }
 
-    /** To be called any time the rows have been updated */
+    /**
+     * Sets the alpha on the content, while leaving the background of the container itself as is.
+     *
+     * @param alpha alpha value to apply to the content
+     */
+    public void setContentAlpha(float alpha) {
+        for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
+            mNotificationHeader.getChildAt(i).setAlpha(alpha);
+        }
+        for (ExpandableNotificationRow child : getAttachedChildren()) {
+            child.setContentAlpha(alpha);
+        }
+    }
+
+    /**
+     * To be called any time the rows have been updated
+     */
     public void updateExpansionStates() {
         if (mChildrenExpanded || mUserLocked) {
             // we don't modify it the group is expanded or if we are expanding it
@@ -475,7 +527,6 @@
     }
 
     /**
-     *
      * @return the intrinsic size of this children container, i.e the natural fully expanded state
      */
     public int getIntrinsicHeight() {
@@ -485,7 +536,7 @@
 
     /**
      * @return the intrinsic height with a number of children given
-     *         in @param maxAllowedVisibleChildren
+     * in @param maxAllowedVisibleChildren
      */
     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
         if (showingAsLowPriority()) {
@@ -539,7 +590,8 @@
 
     /**
      * Update the state of all its children based on a linear layout algorithm.
-     * @param parentState the state of the parent
+     *
+     * @param parentState  the state of the parent
      * @param ambientState the ambient state containing ambient information
      */
     public void updateState(ExpandableViewState parentState, AmbientState ambientState) {
@@ -655,14 +707,17 @@
      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
      * height, children in the group after this are gone.
      *
-     * @param child the child who's height to adjust.
+     * @param child        the child who's height to adjust.
      * @param parentHeight the height of the parent.
-     * @param childState the state to update.
-     * @param yPosition the yPosition of the view.
+     * @param childState   the state to update.
+     * @param yPosition    the yPosition of the view.
      * @return true if children after this one should be hidden.
      */
-    private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
-            int parentHeight, ExpandableViewState childState, int yPosition) {
+    private boolean updateChildStateForExpandedGroup(
+            ExpandableNotificationRow child,
+            int parentHeight,
+            ExpandableViewState childState,
+            int yPosition) {
         final int top = yPosition + child.getClipTopAmount();
         final int intrinsicHeight = child.getIntrinsicHeight();
         final int bottom = top + intrinsicHeight;
@@ -690,13 +745,15 @@
         if (mIsLowPriority
                 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
                 || (mContainingNotification.isHeadsUpState()
-                        && mContainingNotification.canShowHeadsUp())) {
+                && mContainingNotification.canShowHeadsUp())) {
             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
         }
         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
     }
 
-    /** Applies state to children. */
+    /**
+     * Applies state to children.
+     */
     public void applyState() {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -768,17 +825,73 @@
         }
     }
 
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        boolean isCanvasChanged = false;
+
+        Path clipPath = mChildClipPath;
+        if (clipPath != null) {
+            final float translation;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
+                translation = notificationRow.getTranslation();
+            } else {
+                translation = child.getTranslationX();
+            }
+
+            isCanvasChanged = true;
+            canvas.save();
+            if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+                clipPath.offset(translation, 0f);
+                canvas.clipPath(clipPath);
+                clipPath.offset(-translation, 0f);
+            } else {
+                canvas.clipPath(clipPath);
+            }
+        }
+
+        if (child instanceof NotificationHeaderView
+                && mNotificationHeaderWrapper.hasRoundedCorner()) {
+            float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+            mHeaderPath.reset();
+            mHeaderPath.addRoundRect(
+                    child.getLeft(),
+                    child.getTop(),
+                    child.getRight(),
+                    child.getBottom(),
+                    radii,
+                    Direction.CW
+            );
+            if (!isCanvasChanged) {
+                isCanvasChanged = true;
+                canvas.save();
+            }
+            canvas.clipPath(mHeaderPath);
+        }
+
+        if (isCanvasChanged) {
+            boolean result = super.drawChild(canvas, child, drawingTime);
+            canvas.restore();
+            return result;
+        } else {
+            // If there have been no changes to the canvas we can proceed as usual
+            return super.drawChild(canvas, child, drawingTime);
+        }
+    }
+
+
     /**
      * This is called when the children expansion has changed and positions the children properly
      * for an appear animation.
-     *
      */
     public void prepareExpansionChanged() {
         // TODO: do something that makes sense, like placing the invisible views correctly
         return;
     }
 
-    /** Animate to a given state. */
+    /**
+     * Animate to a given state.
+     */
     public void startAnimationToState(AnimationProperties properties) {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -1102,7 +1215,8 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
      */
     private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
         return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
@@ -1112,10 +1226,13 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
-     * @param headerTranslation the translation amount of the header
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
+     * @param headerTranslation         the translation amount of the header
      */
-    private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority,
+    private int getMinHeight(
+            int maxAllowedVisibleChildren,
+            boolean likeHighPriority,
             int headerTranslation) {
         if (!likeHighPriority && showingAsLowPriority()) {
             if (mNotificationHeaderLowPriority == null) {
@@ -1274,16 +1391,23 @@
         return mUserLocked;
     }
 
-    public void setCurrentBottomRoundness(float currentBottomRoundness) {
+    @Override
+    public void applyRoundness() {
+        Roundable.super.applyRoundness();
         boolean last = true;
         for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mAttachedChildren.get(i);
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
-            float bottomRoundness = last ? currentBottomRoundness : 0.0f;
-            child.setBottomRoundness(bottomRoundness, isShown() /* animate */);
-            child.setTopRoundness(0.0f, false /* animate */);
+            child.requestTopRoundness(
+                    /* value = */ 0f,
+                    /* animate = */ isShown(),
+                    SourceType.DefaultValue);
+            child.requestBottomRoundness(
+                    /* value = */ last ? getBottomRoundness() : 0f,
+                    /* animate = */ isShown(),
+                    SourceType.DefaultValue);
             last = false;
         }
     }
@@ -1293,7 +1417,9 @@
         mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mNotificationHeaderWrapper != null) {
             mNotificationHeaderWrapper.setFeedbackIcon(icon);
@@ -1325,4 +1451,26 @@
             child.setNotificationFaded(faded);
         }
     }
+
+    /**
+     * Allow to define a path the clip the children in #drawChild()
+     *
+     * @param childClipPath path used to clip the children
+     */
+    public void setChildClipPath(@Nullable Path childClipPath) {
+        mChildClipPath = childClipPath;
+        invalidate();
+    }
+
+    public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
+        return mNotificationHeaderWrapper;
+    }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 2015c87..6810055 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -26,6 +26,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -59,8 +61,8 @@
     private boolean mIsClearAllInProgress;
 
     private ExpandableView mSwipedView = null;
-    private ExpandableView mViewBeforeSwipedView = null;
-    private ExpandableView mViewAfterSwipedView = null;
+    private Roundable mViewBeforeSwipedView = null;
+    private Roundable mViewAfterSwipedView = null;
 
     @Inject
     NotificationRoundnessManager(
@@ -101,11 +103,12 @@
     public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
         return expandableView != null
                 && (expandableView == mSwipedView
-                    || expandableView == mViewBeforeSwipedView
-                    || expandableView == mViewAfterSwipedView);
+                || expandableView == mViewBeforeSwipedView
+                || expandableView == mViewAfterSwipedView);
     }
 
-    boolean updateViewWithoutCallback(ExpandableView view,
+    boolean updateViewWithoutCallback(
+            ExpandableView view,
             boolean animate) {
         if (view == null
                 || view == mViewBeforeSwipedView
@@ -113,11 +116,15 @@
             return false;
         }
 
-        final float topRoundness = getRoundnessFraction(view, true /* top */);
-        final float bottomRoundness = getRoundnessFraction(view, false /* top */);
+        final boolean isTopChanged = view.requestTopRoundness(
+                getRoundnessDefaultValue(view, true /* top */),
+                animate,
+                SourceType.DefaultValue);
 
-        final boolean topChanged = view.setTopRoundness(topRoundness, animate);
-        final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
+        final boolean isBottomChanged = view.requestBottomRoundness(
+                getRoundnessDefaultValue(view, /* top = */ false),
+                animate,
+                SourceType.DefaultValue);
 
         final boolean isFirstInSection = isFirstInSection(view);
         final boolean isLastInSection = isLastInSection(view);
@@ -126,9 +133,9 @@
         view.setLastInSection(isLastInSection);
 
         mNotifLogger.onCornersUpdated(view, isFirstInSection,
-                isLastInSection, topChanged, bottomChanged);
+                isLastInSection, isTopChanged, isBottomChanged);
 
-        return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged);
+        return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged);
     }
 
     private boolean isFirstInSection(ExpandableView view) {
@@ -150,42 +157,46 @@
     }
 
     void setViewsAffectedBySwipe(
-            ExpandableView viewBefore,
+            Roundable viewBefore,
             ExpandableView viewSwiped,
-            ExpandableView viewAfter) {
+            Roundable viewAfter) {
         final boolean animate = true;
+        final SourceType source = SourceType.OnDismissAnimation;
 
-        ExpandableView oldViewBefore = mViewBeforeSwipedView;
+        // This method requires you to change the roundness of the current View targets and reset
+        // the roundness of the old View targets (if any) to 0f.
+        // To avoid conflicts, it generates a set of old Views and removes the current Views
+        // from this set.
+        HashSet<Roundable> oldViews = new HashSet<>();
+        if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
+        if (mSwipedView != null) oldViews.add(mSwipedView);
+        if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
+
         mViewBeforeSwipedView = viewBefore;
-        if (oldViewBefore != null) {
-            final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */);
-            oldViewBefore.setBottomRoundness(bottomRoundness,  animate);
-        }
         if (viewBefore != null) {
-            viewBefore.setBottomRoundness(1f, animate);
+            oldViews.remove(viewBefore);
+            viewBefore.requestTopRoundness(0f, animate, source);
+            viewBefore.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldSwipedview = mSwipedView;
         mSwipedView = viewSwiped;
-        if (oldSwipedview != null) {
-            final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */);
-            final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */);
-            oldSwipedview.setTopRoundness(topRoundness, animate);
-            oldSwipedview.setBottomRoundness(bottomRoundness, animate);
-        }
         if (viewSwiped != null) {
-            viewSwiped.setTopRoundness(1f, animate);
-            viewSwiped.setBottomRoundness(1f, animate);
+            oldViews.remove(viewSwiped);
+            viewSwiped.requestTopRoundness(1f, animate, source);
+            viewSwiped.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldViewAfter = mViewAfterSwipedView;
         mViewAfterSwipedView = viewAfter;
-        if (oldViewAfter != null) {
-            final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */);
-            oldViewAfter.setTopRoundness(topRoundness, animate);
-        }
         if (viewAfter != null) {
-            viewAfter.setTopRoundness(1f, animate);
+            oldViews.remove(viewAfter);
+            viewAfter.requestTopRoundness(1f, animate, source);
+            viewAfter.requestBottomRoundness(0f, animate, source);
+        }
+
+        // After setting the current Views, reset the views that are still present in the set.
+        for (Roundable oldView : oldViews) {
+            oldView.requestTopRoundness(0f, animate, source);
+            oldView.requestBottomRoundness(0f, animate, source);
         }
     }
 
@@ -193,7 +204,7 @@
         mIsClearAllInProgress = isClearingAll;
     }
 
-    private float getRoundnessFraction(ExpandableView view, boolean top) {
+    private float getRoundnessDefaultValue(Roundable view, boolean top) {
         if (view == null) {
             return 0f;
         }
@@ -207,28 +218,35 @@
                 && mIsClearAllInProgress) {
             return 1.0f;
         }
-        if ((view.isPinned()
-                || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
-            return 1.0f;
-        }
-        if (isFirstInSection(view) && top) {
-            return 1.0f;
-        }
-        if (isLastInSection(view) && !top) {
-            return 1.0f;
-        }
+        if (view instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) view;
+            if ((expandableView.isPinned()
+                    || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) {
+                return 1.0f;
+            }
+            if (isFirstInSection(expandableView) && top) {
+                return 1.0f;
+            }
+            if (isLastInSection(expandableView) && !top) {
+                return 1.0f;
+            }
 
-        if (view == mTrackedHeadsUp) {
-            // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
-            // rounded.
-            return MathUtils.saturate(1.0f - mAppearFraction);
+            if (view == mTrackedHeadsUp) {
+                // If we're pushing up on a headsup the appear fraction is < 0 and it needs to
+                // still be rounded.
+                return MathUtils.saturate(1.0f - mAppearFraction);
+            }
+            if (expandableView.showingPulsing() && mRoundForPulsingViews) {
+                return 1.0f;
+            }
+            if (expandableView.isChildInGroup()) {
+                return 0f;
+            }
+            final Resources resources = expandableView.getResources();
+            return resources.getDimension(R.dimen.notification_corner_radius_small)
+                    / resources.getDimension(R.dimen.notification_corner_radius);
         }
-        if (view.showingPulsing() && mRoundForPulsingViews) {
-            return 1.0f;
-        }
-        final Resources resources = view.getResources();
-        return resources.getDimension(R.dimen.notification_corner_radius_small)
-                / resources.getDimension(R.dimen.notification_corner_radius);
+        return 0f;
     }
 
     public void setExpanded(float expandedHeight, float appearFraction) {
@@ -258,8 +276,10 @@
         mNotifLogger.onSectionCornersUpdated(sections, anyChanged);
     }
 
-    private boolean handleRemovedOldViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleRemovedOldViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (ExpandableView oldView : oldViews) {
             if (oldView != null) {
@@ -289,8 +309,10 @@
         return anyChanged;
     }
 
-    private boolean handleAddedNewViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleAddedNewViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (NotificationSection section : sections) {
             ExpandableView newView =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
index cb7dfe8..b61c55e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar.notification.stack
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationSectionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "NotifSections"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 91a2813..a1b77ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -19,11 +19,10 @@
 import android.util.Log
 import android.view.View
 import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.media.KeyguardMediaController
+import com.android.systemui.media.controls.ui.KeyguardMediaController
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
 import com.android.systemui.statusbar.notification.collection.render.MediaContainerController
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
-import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager
 import com.android.systemui.statusbar.notification.dagger.AlertingHeader
 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
 import com.android.systemui.statusbar.notification.dagger.PeopleHeader
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 55c577f..41dbf1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static android.os.Trace.TRACE_TAG_ALWAYS;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
@@ -44,6 +46,7 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.Trace;
 import android.provider.Settings;
 import android.util.AttributeSet;
 import android.util.IndentingPrintWriter;
@@ -114,6 +117,8 @@
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.LargeScreenUtils;
 
+import com.google.errorprone.annotations.CompileTimeConstant;
+
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.util.ArrayList;
@@ -255,7 +260,6 @@
     private boolean mClearAllInProgress;
     private FooterClearAllListener mFooterClearAllListener;
     private boolean mFlingAfterUpEvent;
-
     /**
      * Was the scroller scrolled to the top when the down motion was observed?
      */
@@ -1073,6 +1077,12 @@
     @Override
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Trace.beginSection("NotificationStackScrollLayout#onMeasure");
+        if (SPEW) {
+            Log.d(TAG, "onMeasure("
+                    + "widthMeasureSpec=" + MeasureSpec.toString(widthMeasureSpec) + ", "
+                    + "heightMeasureSpec=" + MeasureSpec.toString(heightMeasureSpec) + ")");
+        }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
         int width = MeasureSpec.getSize(widthMeasureSpec);
@@ -1089,6 +1099,13 @@
         for (int i = 0; i < size; i++) {
             measureChild(getChildAt(i), childWidthSpec, childHeightSpec);
         }
+        Trace.endSection();
+    }
+
+    @Override
+    public void requestLayout() {
+        Trace.instant(TRACE_TAG_ALWAYS, "NotificationStackScrollLayout#requestLayout");
+        super.requestLayout();
     }
 
     @Override
@@ -1189,7 +1206,7 @@
             return;
         }
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (mChildrenToAddAnimated.contains(child)) {
                 final int startingPosition = getPositionInLinearLayout(child);
                 final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements;
@@ -1384,7 +1401,7 @@
             if (height < minExpansionHeight) {
                 mClipRect.left = 0;
                 mClipRect.right = getWidth();
-                mClipRect.top = 0;
+                mClipRect.top = getNotificationsClippingTopBound();
                 mClipRect.bottom = (int) height;
                 height = minExpansionHeight;
                 setRequestedClipBounds(mClipRect);
@@ -1445,6 +1462,17 @@
         notifyAppearChangedListeners();
     }
 
+    private int getNotificationsClippingTopBound() {
+        if (isHeadsUpTransition()) {
+            // HUN in split shade can go higher than bottom of NSSL when swiping up so we want
+            // to give it extra clipping margin. Because clipping has rounded corners, we also
+            // need to account for that corner clipping.
+            return -mAmbientState.getStackTopMargin() - mCornerRadius;
+        } else {
+            return 0;
+        }
+    }
+
     private void notifyAppearChangedListeners() {
         float appear;
         float expandAmount;
@@ -1483,7 +1511,6 @@
     public void updateClipping() {
         boolean clipped = mRequestedClipBounds != null && !mInHeadsUpPinnedMode
                 && !mHeadsUpAnimatingAway;
-        boolean clipToOutline = false;
         if (mIsClipped != clipped) {
             mIsClipped = clipped;
         }
@@ -1499,7 +1526,7 @@
             setClipBounds(null);
         }
 
-        setClipToOutline(clipToOutline);
+        setClipToOutline(false);
     }
 
     /**
@@ -1659,7 +1686,7 @@
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+            ExpandableView slidingChild = getChildAtIndex(childIdx);
             if (slidingChild.getVisibility() != VISIBLE
                     || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) {
                 continue;
@@ -1692,6 +1719,10 @@
         return null;
     }
 
+    private ExpandableView getChildAtIndex(int index) {
+        return (ExpandableView) getChildAt(index);
+    }
+
     public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
         return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
@@ -2277,7 +2308,7 @@
         int childCount = getChildCount();
         int count = 0;
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
                 count++;
             }
@@ -2497,7 +2528,7 @@
     private ExpandableView getLastChildWithBackground() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2510,7 +2541,7 @@
     private ExpandableView getFirstChildWithBackground() {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2524,7 +2555,7 @@
         ArrayList<ExpandableView> children = new ArrayList<>();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE
                     && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
@@ -2883,7 +2914,7 @@
         }
         int position = 0;
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             boolean notGone = child.getVisibility() != View.GONE;
             if (notGone && !child.hasNoContentHeight()) {
                 if (position != 0) {
@@ -2937,7 +2968,7 @@
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         // TODO: Refactor SectionManager and put the RoundnessManager there.
-        mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+        mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
         mAnimateBottomOnLayout = false;
         invalidate();
     }
@@ -3680,6 +3711,8 @@
 
     @ShadeViewRefactor(RefactorComponent.INPUT)
     void handleEmptySpaceClick(MotionEvent ev) {
+        logEmptySpaceClick(ev, isBelowLastNotification(mInitialTouchX, mInitialTouchY),
+                mStatusBarState, mTouchIsClick);
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_MOVE:
                 final float touchSlop = getTouchSlop(ev);
@@ -3691,12 +3724,34 @@
             case MotionEvent.ACTION_UP:
                 if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
                         isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
+                    debugLog("handleEmptySpaceClick: touch event propagated further");
                     mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
                 }
                 break;
+            default:
+                debugLog("handleEmptySpaceClick: MotionEvent ignored");
         }
     }
 
+    private void debugLog(@CompileTimeConstant String s) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.d(s);
+    }
+
+    private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification,
+            int statusBarState, boolean touchIsClick) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.logEmptySpaceClick(
+                isTouchBelowLastNotification,
+                statusBarState,
+                touchIsClick,
+                MotionEvent.actionToString(ev.getActionMasked()));
+    }
+
     @ShadeViewRefactor(RefactorComponent.INPUT)
     void initDownStates(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
@@ -3969,7 +4024,7 @@
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void clearUserLockedViews() {
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 row.setUserLocked(false);
@@ -3982,7 +4037,7 @@
         // lets make sure nothing is transient anymore
         clearTemporaryViewsInGroup(this);
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 clearTemporaryViewsInGroup(row.getChildrenContainer());
@@ -4020,8 +4075,9 @@
         setOwnScrollY(0);
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setIsExpanded(boolean isExpanded) {
+    void setIsExpanded(boolean isExpanded) {
         boolean changed = isExpanded != mIsExpanded;
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
@@ -4230,7 +4286,7 @@
         if (hideSensitive != mAmbientState.isHideSensitive()) {
             int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
-                ExpandableView v = (ExpandableView) getChildAt(i);
+                ExpandableView v = getChildAtIndex(i);
                 v.setHideSensitiveForIntrinsicHeight(hideSensitive);
             }
             mAmbientState.setHideSensitive(hideSensitive);
@@ -4265,7 +4321,7 @@
     private void applyCurrentState() {
         int numChildren = getChildCount();
         for (int i = 0; i < numChildren; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             child.applyViewState();
         }
 
@@ -4285,7 +4341,7 @@
 
         // Lefts first sort by Z difference
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != GONE) {
                 mTmpSortedChildren.add(child);
             }
@@ -4512,7 +4568,7 @@
     public void setClearAllInProgress(boolean clearAllInProgress) {
         mClearAllInProgress = clearAllInProgress;
         mAmbientState.setClearAllInProgress(clearAllInProgress);
-        mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+        mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
     }
 
     boolean getClearAllInProgress() {
@@ -4555,7 +4611,7 @@
         final int count = getChildCount();
         float max = 0;
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView child = (ExpandableView) getChildAt(childIdx);
+            ExpandableView child = getChildAtIndex(childIdx);
             if (child.getVisibility() == GONE) {
                 continue;
             }
@@ -4586,7 +4642,7 @@
     public boolean isBelowLastNotification(float touchX, float touchY) {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE) {
                 float childTop = child.getY();
                 if (childTop > touchY) {
@@ -4842,13 +4898,21 @@
         }
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setOwnScrollY(int ownScrollY) {
+    void setOwnScrollY(int ownScrollY) {
         setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */);
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+        // Avoid Flicking during clear all
+        // when the shade finishes closing, onExpansionStopped will call
+        // resetScrollPosition to setOwnScrollY to 0
+        if (mAmbientState.isClosing()) {
+            return;
+        }
+
         if (ownScrollY != mOwnScrollY) {
             // We still want to call the normal scrolled changed for accessibility reasons
             onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
@@ -5044,7 +5108,7 @@
             pw.println();
 
             for (int i = 0; i < childCount; i++) {
-                ExpandableView child = (ExpandableView) getChildAt(i);
+                ExpandableView child = getChildAtIndex(i);
                 child.dump(pw, args);
                 pw.println();
             }
@@ -5333,7 +5397,7 @@
         float wakeUplocation = -1f;
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView view = (ExpandableView) getChildAt(i);
+            ExpandableView view = getChildAtIndex(i);
             if (view.getVisibility() == View.GONE) {
                 continue;
             }
@@ -5372,7 +5436,7 @@
     public void setController(
             NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
         mController = notificationStackScrollLayoutController;
-        mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+        mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
     }
 
     void addSwipedOutView(View v) {
@@ -5383,31 +5447,22 @@
         if (!(viewSwiped instanceof ExpandableNotificationRow)) {
             return;
         }
-        final int indexOfSwipedView = indexOfChild(viewSwiped);
-        if (indexOfSwipedView < 0) {
-            return;
-        }
         mSectionsManager.updateFirstAndLastViewsForAllSections(
-                mSections, getChildrenWithBackground());
-        View viewBefore = null;
-        if (indexOfSwipedView > 0) {
-            viewBefore = getChildAt(indexOfSwipedView - 1);
-            if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) {
-                viewBefore = null;
-            }
-        }
-        View viewAfter = null;
-        if (indexOfSwipedView < getChildCount()) {
-            viewAfter = getChildAt(indexOfSwipedView + 1);
-            if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) {
-                viewAfter = null;
-            }
-        }
-        mController.getNoticationRoundessManager()
+                mSections,
+                getChildrenWithBackground()
+        );
+
+        RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets(
+                (ExpandableNotificationRow) viewSwiped,
+                this,
+                mSectionsManager
+        );
+
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(
-                        (ExpandableView) viewBefore,
-                        (ExpandableView) viewSwiped,
-                        (ExpandableView) viewAfter);
+                        targets.getBefore(),
+                        targets.getSwiped(),
+                        targets.getAfter());
 
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
@@ -5418,7 +5473,7 @@
 
     void onSwipeEnd() {
         updateFirstAndLastBackgroundViews();
-        mController.getNoticationRoundessManager()
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(null, null, null);
         // Round bottom corners for notification right before shelf.
         mShelf.updateAppearance();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5c09d61..0240bbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -63,7 +63,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
@@ -89,6 +89,7 @@
 import com.android.systemui.statusbar.notification.collection.PipelineDumper;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
+import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
 import com.android.systemui.statusbar.notification.collection.render.NotifStats;
@@ -157,6 +158,7 @@
     private final NotifCollection mNotifCollection;
     private final UiEventLogger mUiEventLogger;
     private final NotificationRemoteInputManager mRemoteInputManager;
+    private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     private final ShadeController mShadeController;
     private final KeyguardMediaController mKeyguardMediaController;
     private final SysuiStatusBarStateController mStatusBarStateController;
@@ -180,6 +182,7 @@
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
     private final FeatureFlags mFeatureFlags;
+    private final NotificationTargetsHelper mNotificationTargetsHelper;
 
     private View mLongPressedView;
 
@@ -637,12 +640,14 @@
             ShadeTransitionController shadeTransitionController,
             UiEventLogger uiEventLogger,
             NotificationRemoteInputManager remoteInputManager,
+            VisibilityLocationProviderDelegator visibilityLocationProviderDelegator,
             ShadeController shadeController,
             InteractionJankMonitor jankMonitor,
             StackStateLogger stackLogger,
             NotificationStackScrollLogger logger,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            NotificationTargetsHelper notificationTargetsHelper) {
         mStackStateLogger = stackLogger;
         mLogger = logger;
         mAllowLongPress = allowLongPress;
@@ -677,8 +682,10 @@
         mNotifCollection = notifCollection;
         mUiEventLogger = uiEventLogger;
         mRemoteInputManager = remoteInputManager;
+        mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator;
         mShadeController = shadeController;
         mFeatureFlags = featureFlags;
+        mNotificationTargetsHelper = notificationTargetsHelper;
         updateResources();
     }
 
@@ -747,6 +754,8 @@
         mNotificationRoundnessManager.setOnRoundingChangedCallback(mView::invalidate);
         mView.addOnExpandedHeightChangedListener(mNotificationRoundnessManager::setExpanded);
 
+        mVisibilityLocationProviderDelegator.setDelegate(this::isInVisibleLocation);
+
         mTunerService.addTunable(
                 (key, newValue) -> {
                     switch (key) {
@@ -1380,7 +1389,7 @@
         return mView.calculateGapHeight(previousView, child, count);
     }
 
-    NotificationRoundnessManager getNoticationRoundessManager() {
+    NotificationRoundnessManager getNotificationRoundnessManager() {
         return mNotificationRoundnessManager;
     }
 
@@ -1537,6 +1546,10 @@
         mNotificationActivityStarter = activityStarter;
     }
 
+    public NotificationTargetsHelper getNotificationTargetsHelper() {
+        return mNotificationTargetsHelper;
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 5f79c0e..64dd6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -1,8 +1,9 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
@@ -10,6 +11,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER
+import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
 class NotificationStackScrollLogger @Inject constructor(
@@ -56,6 +58,25 @@
                     "key: $str1 expected: $bool1 actual: $bool2"
         })
     }
+
+    fun d(@CompileTimeConstant msg: String) = buffer.log(TAG, DEBUG, msg)
+
+    fun logEmptySpaceClick(
+        isBelowLastNotification: Boolean,
+        statusBarState: Int,
+        touchIsClick: Boolean,
+        motionEventDesc: String
+    ) {
+        buffer.log(TAG, DEBUG, {
+            int1 = statusBarState
+            bool1 = touchIsClick
+            bool2 = isBelowLastNotification
+            str1 = motionEventDesc
+        }, {
+            "handleEmptySpaceClick: statusBarState: $int1 isTouchAClick: $bool1 " +
+                    "isTouchBelowNotification: $bool2 motionEvent: $str1"
+        })
+    }
 }
 
 private const val TAG = "NotificationStackScroll"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 0000000..991a14b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.Roundable
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import javax.inject.Inject
+
+/**
+ * Utility class that helps us find the targets of an animation, often used to find the notification
+ * ([Roundable]) above and below the current one (see [findRoundableTargets]).
+ */
+@SysUISingleton
+class NotificationTargetsHelper
+@Inject
+constructor(
+    featureFlags: FeatureFlags,
+) {
+    private val isNotificationGroupCornerEnabled =
+        featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+
+    /**
+     * This method looks for views that can be rounded (and implement [Roundable]) during a
+     * notification swipe.
+     * @return The [Roundable] targets above/below the [viewSwiped] (if available). The
+     * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is
+     * no above/below notification or the notification is not part of the same section.
+     */
+    fun findRoundableTargets(
+        viewSwiped: ExpandableNotificationRow,
+        stackScrollLayout: NotificationStackScrollLayout,
+        sectionsManager: NotificationSectionsManager,
+    ): RoundableTargets {
+        val viewBefore: Roundable?
+        val viewAfter: Roundable?
+
+        val notificationParent = viewSwiped.notificationParent
+        val childrenContainer = notificationParent?.childrenContainer
+        val visibleStackChildren =
+            stackScrollLayout.children
+                .filterIsInstance<ExpandableView>()
+                .filter { it.isVisible }
+                .toList()
+        if (notificationParent != null && childrenContainer != null) {
+            // We are inside a notification group
+
+            if (!isNotificationGroupCornerEnabled) {
+                return RoundableTargets(null, null, null)
+            }
+
+            val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible }
+            val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1)
+                    ?: childrenContainer.notificationHeaderWrapper
+
+            viewAfter =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1)
+                    ?: visibleStackChildren.indexOf(notificationParent).let {
+                        visibleStackChildren.getOrNull(it + 1)
+                    }
+        } else {
+            // Assumption: we are inside the NotificationStackScrollLayout
+
+            val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf {
+                    !sectionsManager.beginsSection(viewSwiped, it)
+                }
+
+            viewAfter =
+                visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf {
+                    !sectionsManager.beginsSection(it, viewSwiped)
+                }
+        }
+
+        return RoundableTargets(
+            before = viewBefore,
+            swiped = viewSwiped,
+            after = viewAfter,
+        )
+    }
+}
+
+/**
+ * This object contains targets above/below the [swiped] (if available). The [before] and [after]
+ * parameters can be `null` if there is no above/below notification or the notification is not part
+ * of the same section.
+ */
+data class RoundableTargets(
+    val before: Roundable?,
+    val swiped: ExpandableNotificationRow?,
+    val after: Roundable?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 0502159..d8c6878 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -31,6 +31,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -56,11 +57,13 @@
     private float mGapHeight;
     private float mGapHeightOnLockscreen;
     private int mCollapsedSize;
+    private boolean mEnableNotificationClipping;
 
     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
     private boolean mIsExpanded;
     private boolean mClipNotificationScrollToTop;
-    @VisibleForTesting float mHeadsUpInset;
+    @VisibleForTesting
+    float mHeadsUpInset;
     private int mPinnedZTranslationExtra;
     private float mNotificationScrimPadding;
     private int mMarginBottom;
@@ -84,6 +87,7 @@
         mPaddingBetweenElements = res.getDimensionPixelSize(
                 R.dimen.notification_divider_height);
         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
@@ -288,7 +292,7 @@
                 // The bottom of this view is peeking out from under the previous view.
                 // Clip the part that is peeking out.
                 float overlapAmount = newNotificationEnd - firstHeadsUpEnd;
-                state.clipBottomAmount = (int) overlapAmount;
+                state.clipBottomAmount = mEnableNotificationClipping ? (int) overlapAmount : 0;
             } else {
                 state.clipBottomAmount = 0;
             }
@@ -453,7 +457,7 @@
 
     /**
      * @return Fraction to apply to view height and gap between views.
-     *         Does not include shelf height even if shelf is showing.
+     * Does not include shelf height even if shelf is showing.
      */
     protected float getExpansionFractionWithoutShelf(
             StackScrollAlgorithmState algorithmState,
@@ -467,7 +471,7 @@
                 && (!ambientState.isBypassEnabled() || !ambientState.isPulseExpanding())
                 ? 0 : mNotificationScrimPadding;
 
-        final float stackHeight = ambientState.getStackHeight()  - shelfHeight - scrimPadding;
+        final float stackHeight = ambientState.getStackHeight() - shelfHeight - scrimPadding;
         final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding;
         if (stackEndHeight == 0f) {
             // This should not happen, since even when the shade is empty we show EmptyShadeView
@@ -501,13 +505,14 @@
     }
 
     // TODO(b/172289889) polish shade open from HUN
+
     /**
      * Populates the {@link ExpandableViewState} for a single child.
      *
-     * @param i                The index of the child in
-     * {@link StackScrollAlgorithmState#visibleChildren}.
-     * @param algorithmState   The overall output state of the algorithm.
-     * @param ambientState     The input state provided to the algorithm.
+     * @param i              The index of the child in
+     *                       {@link StackScrollAlgorithmState#visibleChildren}.
+     * @param algorithmState The overall output state of the algorithm.
+     * @param ambientState   The input state provided to the algorithm.
      */
     protected void updateChild(
             int i,
@@ -581,8 +586,8 @@
                     final float stackBottom = !ambientState.isShadeExpanded()
                             || ambientState.getDozeAmount() == 1f
                             || bypassPulseNotExpanding
-                                    ? ambientState.getInnerHeight()
-                                    : ambientState.getStackHeight();
+                            ? ambientState.getInnerHeight()
+                            : ambientState.getStackHeight();
                     final float shelfStart = stackBottom
                             - ambientState.getShelf().getIntrinsicHeight()
                             - mPaddingBetweenElements;
@@ -618,9 +623,9 @@
      * Get the gap height needed for before a view
      *
      * @param sectionProvider the sectionProvider used to understand the sections
-     * @param visibleIndex the visible index of this view in the list
-     * @param child the child asked about
-     * @param previousChild the child right before it or null if none
+     * @param visibleIndex    the visible index of this view in the list
+     * @param child           the child asked about
+     * @param previousChild   the child right before it or null if none
      * @return the size of the gap needed or 0 if none is needed
      */
     public float getGapHeightForChild(
@@ -654,9 +659,9 @@
      * Does a given child need a gap, i.e spacing before a view?
      *
      * @param sectionProvider the sectionProvider used to understand the sections
-     * @param visibleIndex the visible index of this view in the list
-     * @param child the child asked about
-     * @param previousChild the child right before it or null if none
+     * @param visibleIndex    the visible index of this view in the list
+     * @param child           the child asked about
+     * @param previousChild   the child right before it or null if none
      * @return if the child needs a gap height
      */
     private boolean childNeedsGapHeight(
@@ -804,7 +809,7 @@
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
         final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                 ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
-        row.setBottomRoundness(roundness, /* animate= */ false);
+        row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
     }
 
     @VisibleForTesting
@@ -859,30 +864,53 @@
         }
     }
 
+    /**
+     * Calculate and update the Z positions for a given child. We currently only give shadows to
+     * HUNs to distinguish a HUN from its surroundings.
+     *
+     * @param isTopHun      Whether the child is a top HUN. A top HUN means a HUN that shows on the
+     *                      vertically top of screen. Top HUNs should have drop shadows
+     * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
+     * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
+     *                      that overlaps with QQS Panel. The integer part represents the count of
+     *                      previous HUNs whose Z positions are greater than 0.
+     */
     protected float updateChildZValue(int i, float childrenOnTop,
             StackScrollAlgorithmState algorithmState,
             AmbientState ambientState,
-            boolean shouldElevateHun) {
+            boolean isTopHun) {
         ExpandableView child = algorithmState.visibleChildren.get(i);
         ExpandableViewState childViewState = child.getViewState();
-        int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
         float baseZ = ambientState.getBaseZHeight();
+
+        // Handles HUN shadow when Shade is opened
+
         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
                 && !ambientState.isDozingAndNotPulsing(child)
                 && childViewState.getYTranslation() < ambientState.getTopPadding()
                 + ambientState.getStackTranslation()) {
+            // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
+            // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
+            // When scrolling down shade to make HUN back to in-position in Notification Panel,
+            // The over-lapping fraction goes to 0, and shadows hides gradually.
             if (childrenOnTop != 0.0f) {
+                // To elevate the later HUN over previous HUN
                 childrenOnTop++;
             } else {
                 float overlap = ambientState.getTopPadding()
                         + ambientState.getStackTranslation() - childViewState.getYTranslation();
-                childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
+                // To prevent over-shadow during HUN entry
+                childrenOnTop += Math.min(
+                        1.0f,
+                        overlap / childViewState.height
+                );
+                MathUtils.saturate(childrenOnTop);
             }
             childViewState.setZTranslation(baseZ
-                    + childrenOnTop * zDistanceBetweenElements);
-        } else if (shouldElevateHun) {
+                    + childrenOnTop * mPinnedZTranslationExtra);
+        } else if (isTopHun) {
             // In case this is a new view that has never been measured before, we don't want to
-            // elevate if we are currently expanded more then the notification
+            // elevate if we are currently expanded more than the notification
             int shelfHeight = ambientState.getShelf() == null ? 0 :
                     ambientState.getShelf().getIntrinsicHeight();
             float shelfStart = ambientState.getInnerHeight()
@@ -891,23 +919,28 @@
             float notificationEnd = childViewState.getYTranslation() + child.getIntrinsicHeight()
                     + mPaddingBetweenElements;
             if (shelfStart > notificationEnd) {
+                // When the notification doesn't overlap with Notification Shelf, there's no shadow
                 childViewState.setZTranslation(baseZ);
             } else {
+                // Give shadow to the notification if it overlaps with Notification Shelf
                 float factor = (notificationEnd - shelfStart) / shelfHeight;
                 if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0.
                     factor = 1.0f;
                 }
                 factor = Math.min(factor, 1.0f);
-                childViewState.setZTranslation(baseZ + factor * zDistanceBetweenElements);
+                childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra);
             }
         } else {
             childViewState.setZTranslation(baseZ);
         }
 
-        // We need to scrim the notification more from its surrounding content when we are pinned,
-        // and we therefore elevate it higher.
-        // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when
-        // expanding after which we have a normal elevation again.
+        // Handles HUN shadow when shade is closed.
+        // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays.
+        // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes
+        // gradually from 0 to 1, shadow hides gradually.
+        // Header visibility is a deprecated concept, we are using headerVisibleAmount only because
+        // this value nicely goes from 0 to 1 during the HUN-to-Shade process.
+
         childViewState.setZTranslation(childViewState.getZTranslation()
                 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra);
         return childrenOnTop;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index cb4a088..f5de678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 9900e41..9dcbe20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -64,8 +64,10 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -163,7 +165,7 @@
     private PendingAuthenticated mPendingAuthenticated = null;
     private boolean mHasScreenTurnedOnSinceAuthenticating;
     private boolean mFadedAwayAfterWakeAndUnlock;
-    private BiometricModeListener mBiometricModeListener;
+    private Set<BiometricModeListener> mBiometricModeListeners = new HashSet<>();
 
     private final MetricsLogger mMetricsLogger;
     private final AuthController mAuthController;
@@ -305,9 +307,14 @@
         mKeyguardViewController = keyguardViewController;
     }
 
-    /** Sets a {@link BiometricModeListener}. */
-    public void setBiometricModeListener(BiometricModeListener biometricModeListener) {
-        mBiometricModeListener = biometricModeListener;
+    /** Adds a {@link BiometricModeListener}. */
+    public void addBiometricModeListener(BiometricModeListener listener) {
+        mBiometricModeListeners.add(listener);
+    }
+
+    /** Removes a {@link BiometricModeListener}. */
+    public void removeBiometricModeListener(BiometricModeListener listener) {
+        mBiometricModeListeners.remove(listener);
     }
 
     private final Runnable mReleaseBiometricWakeLockRunnable = new Runnable() {
@@ -455,7 +462,7 @@
                 break;
             case MODE_SHOW_BOUNCER:
                 Trace.beginSection("MODE_SHOW_BOUNCER");
-                mKeyguardViewController.showBouncer(true);
+                mKeyguardViewController.showPrimaryBouncer(true);
                 Trace.endSection();
                 break;
             case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
@@ -481,15 +488,12 @@
                 break;
         }
         onModeChanged(mMode);
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.notifyBiometricAuthModeChanged();
-        }
         Trace.endSection();
     }
 
     private void onModeChanged(@WakeAndUnlockMode int mode) {
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.onModeChanged(mode);
+        for (BiometricModeListener listener : mBiometricModeListeners) {
+            listener.onModeChanged(mode);
         }
     }
 
@@ -538,7 +542,7 @@
             return MODE_WAKE_AND_UNLOCK_FROM_DREAM;
         }
         if (mKeyguardStateController.isShowing()) {
-            if (mKeyguardViewController.bouncerIsOrWillBeShowing() && unlockingAllowed) {
+            if (mKeyguardViewController.primaryBouncerIsOrWillBeShowing() && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
             } else if (unlockingAllowed) {
                 return MODE_UNLOCK_COLLAPSING;
@@ -581,7 +585,7 @@
             return MODE_UNLOCK_COLLAPSING;
         }
         if (mKeyguardStateController.isShowing()) {
-            if ((mKeyguardViewController.bouncerIsOrWillBeShowing()
+            if ((mKeyguardViewController.primaryBouncerIsOrWillBeShowing()
                     || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) {
                 return MODE_DISMISS_BOUNCER;
             } else if (unlockingAllowed && (bypass || mAuthController.isUdfpsFingerDown())) {
@@ -696,9 +700,8 @@
         mMode = MODE_NONE;
         mBiometricType = null;
         mNotificationShadeWindowController.setForceDozeBrightness(false);
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.onResetMode();
-            mBiometricModeListener.notifyBiometricAuthModeChanged();
+        for (BiometricModeListener listener : mBiometricModeListeners) {
+            listener.onResetMode();
         }
         mNumConsecutiveFpFailures = 0;
         mLastFpFailureUptimeMillis = 0;
@@ -807,10 +810,8 @@
     /** An interface to interact with the {@link BiometricUnlockController}. */
     public interface BiometricModeListener {
         /** Called when {@code mMode} is reset to {@link #MODE_NONE}. */
-        void onResetMode();
+        default void onResetMode() {}
         /** Called when {@code mMode} has changed in {@link #startWakeAndUnlock(int)}. */
-        void onModeChanged(@WakeAndUnlockMode int mode);
-        /** Called after processing {@link #onModeChanged(int)}. */
-        void notifyBiometricAuthModeChanged();
+        default void onModeChanged(@WakeAndUnlockMode int mode) {}
     }
 }
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 25fd483..be08183 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;
@@ -55,7 +54,6 @@
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.LightRevealScrim;
 import com.android.systemui.statusbar.NotificationPresenter;
-import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 
 import java.io.PrintWriter;
 
@@ -197,8 +195,6 @@
 
     void collapsePanelOnMainThread();
 
-    void collapsePanelWithDuration(int duration);
-
     void togglePanel();
 
     void start();
@@ -230,13 +226,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);
@@ -262,16 +251,10 @@
     @Override
     void startActivity(Intent intent, boolean dismissShade, Callback callback);
 
-    void setQsExpanded(boolean expanded);
-
     boolean isWakeUpComingFromTouch();
 
-    boolean isFalsingThresholdNeeded();
-
     void onKeyguardViewManagerStatesUpdated();
 
-    void setPanelExpanded(boolean isExpanded);
-
     ViewGroup getNotificationScrollLayout();
 
     boolean isPulsing();
@@ -315,9 +298,6 @@
 
     void checkBarModes();
 
-    // Called by NavigationBarFragment
-    void setQsScrimEnabled(boolean scrimEnabled);
-
     void updateBubblesVisibility();
 
     void setInteracting(int barWindow, boolean interacting);
@@ -389,8 +369,6 @@
 
     void showKeyguardImpl();
 
-    boolean isInLaunchTransition();
-
     void fadeKeyguardAfterLaunchTransition(Runnable beforeFading,
             Runnable endRunnable, Runnable cancelRunnable);
 
@@ -432,12 +410,6 @@
 
     void onClosingFinished();
 
-    void onUnlockHintStarted();
-
-    void onHintFinished();
-
-    void onTrackingStopped(boolean expand);
-
     // TODO: Figure out way to remove these.
     NavigationBarView getNavigationBarView();
 
@@ -447,14 +419,15 @@
 
     void showPinningEscapeToast();
 
-    KeyguardBottomAreaView getKeyguardBottomAreaView();
-
     void setBouncerShowing(boolean bouncerShowing);
 
     void setBouncerShowingOverDream(boolean bouncerShowingOverDream);
 
     void collapseShade();
 
+    /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */
+    void collapseShadeForBugreport();
+
     int getWakefulnessState();
 
     boolean isScreenFullyOff();
@@ -472,7 +445,11 @@
 
     void setTransitionToFullShadeProgress(float transitionToFullShadeProgress);
 
-    void setBouncerHiddenFraction(float expansion);
+    /**
+     * Sets the amount of progress to the bouncer being fully hidden/visible. 1 means the bouncer
+     * is fully hidden, while 0 means the bouncer is visible.
+     */
+    void setPrimaryBouncerHiddenFraction(float expansion);
 
     @VisibleForTesting
     void updateScrimController();
@@ -512,14 +489,8 @@
 
     boolean isBouncerShowingOverDream();
 
-    void onBouncerPreHideAnimation();
-
     boolean isKeyguardSecure();
 
-    NotificationPanelViewController getPanelController();
-
-    NotificationGutsManager getGutsManager();
-
     void updateNotificationPanelTouchState();
 
     void makeExpandedVisible(boolean force);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index d6fadca..f3482f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -38,8 +38,8 @@
 import android.util.Log;
 import android.util.Slog;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -55,6 +55,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -71,6 +72,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /** */
 @CentralSurfacesComponent.CentralSurfacesScope
 public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callbacks {
@@ -99,6 +102,7 @@
     private final boolean mVibrateOnOpening;
     private final VibrationEffect mCameraLaunchGestureVibrationEffect;
     private final SystemBarAttributesListener mSystemBarAttributesListener;
+    private final Lazy<CameraLauncher> mCameraLauncherLazy;
 
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
@@ -128,8 +132,8 @@
             Optional<Vibrator> vibratorOptional,
             DisableFlagsLogger disableFlagsLogger,
             @DisplayId int displayId,
-            SystemBarAttributesListener systemBarAttributesListener) {
-
+            SystemBarAttributesListener systemBarAttributesListener,
+            Lazy<CameraLauncher> cameraLauncherLazy) {
         mCentralSurfaces = centralSurfaces;
         mContext = context;
         mShadeController = shadeController;
@@ -152,6 +156,7 @@
         mVibratorOptional = vibratorOptional;
         mDisableFlagsLogger = disableFlagsLogger;
         mDisplayId = displayId;
+        mCameraLauncherLazy = cameraLauncherLazy;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -346,7 +351,8 @@
             mCentralSurfaces.setLaunchCameraOnFinishedGoingToSleep(true);
             return;
         }
-        if (!mNotificationPanelViewController.canCameraGestureBeLaunched()) {
+        if (!mCameraLauncherLazy.get().canCameraGestureBeLaunched(
+                mNotificationPanelViewController.getBarState())) {
             if (CentralSurfaces.DEBUG_CAMERA_LIFT) {
                 Slog.d(CentralSurfaces.TAG, "Can't launch camera right now");
             }
@@ -383,7 +389,8 @@
                 if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
                     mStatusBarKeyguardViewManager.reset(true /* hide */);
                 }
-                mNotificationPanelViewController.launchCamera(source);
+                mCameraLauncherLazy.get().launchCamera(source,
+                        mNotificationPanelViewController.isFullyCollapsed());
                 mCentralSurfaces.updateScrimController();
             } else {
                 // We need to defer the camera launch until the screen comes on, since otherwise
@@ -458,7 +465,7 @@
     @Override
     public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName,
+            @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails) {
         if (displayId != mDisplayId) {
             return;
@@ -471,7 +478,7 @@
                 appearanceRegions,
                 navbarColorManagedByIme,
                 behavior,
-                requestedVisibilities,
+                requestedVisibleTypes,
                 packageName,
                 letterboxDetails
         );
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 2c834cf..4562e69 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;
@@ -173,9 +172,9 @@
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.recents.ScreenPinningRequest;
-import com.android.systemui.ripple.RippleShader.RippleShape;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -232,6 +231,7 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.WallpaperController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -279,6 +279,7 @@
     // 1020-1040 reserved for BaseStatusBar
 
     /**
+     * TODO(b/249277686) delete this
      * The delay to reset the hint text when the hint animation is finished running.
      */
     private static final int HINT_RESET_DELAY_MS = 1200;
@@ -467,8 +468,9 @@
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     private final CentralSurfacesComponent.Factory mCentralSurfacesComponentFactory;
     private final PluginManager mPluginManager;
-    private final com.android.systemui.shade.ShadeController mShadeController;
+    private final ShadeController mShadeController;
     private final InitController mInitController;
+    private final Lazy<CameraLauncher> mCameraLauncherLazy;
 
     private final PluginDependencyProvider mPluginDependencyProvider;
     private final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -480,9 +482,9 @@
     private final StatusBarSignalPolicy mStatusBarSignalPolicy;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
 
-    // expanded notifications
-    // the sliding/resizing panel within the notification window
-    protected NotificationPanelViewController mNotificationPanelViewController;
+    /** Controller for the Shade. */
+    @VisibleForTesting
+    NotificationPanelViewController mNotificationPanelViewController;
 
     // settings
     private QSPanelController mQSPanelController;
@@ -601,6 +603,7 @@
 
     private Runnable mLaunchTransitionEndRunnable;
     private Runnable mLaunchTransitionCancelRunnable;
+    private boolean mLaunchingAffordance;
     private boolean mLaunchCameraWhenFinishedWaking;
     private boolean mLaunchCameraOnFinishedGoingToSleep;
     private boolean mLaunchEmergencyActionWhenFinishedWaking;
@@ -745,7 +748,8 @@
             InteractionJankMonitor jankMonitor,
             DeviceStateManager deviceStateManager,
             WiredChargingRippleController wiredChargingRippleController,
-            IDreamManager dreamManager) {
+            IDreamManager dreamManager,
+            Lazy<CameraLauncher> cameraLauncherLazy) {
         mContext = context;
         mNotificationsController = notificationsController;
         mFragmentService = fragmentService;
@@ -822,6 +826,7 @@
         mMessageRouter = messageRouter;
         mWallpaperManager = wallpaperManager;
         mJankMonitor = jankMonitor;
+        mCameraLauncherLazy = cameraLauncherLazy;
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
@@ -832,6 +837,7 @@
         mScreenOffAnimationController = screenOffAnimationController;
 
         mShadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
+        mShadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
 
         mBubbleExpandListener = (isExpanding, key) ->
                 mContext.getMainExecutor().execute(this::updateScrimController);
@@ -868,6 +874,11 @@
             mBubblesOptional.get().setExpandListener(mBubbleExpandListener);
         }
 
+        // Do not restart System UI when the bugreport flag changes.
+        mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> {
+            event.requestNoRestart();
+        });
+
         mStatusBarSignalPolicy.init();
         mKeyguardIndicationController.init();
 
@@ -924,7 +935,7 @@
         }
         mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
                 result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior,
-                result.mRequestedVisibilities, result.mPackageName, result.mLetterboxDetails);
+                result.mRequestedVisibleTypes, result.mPackageName, result.mLetterboxDetails);
 
         // StatusBarManagerService has a back up of IME token and it's restored here.
         mCommandQueueCallbacks.setImeWindowStatus(mDisplayId, result.mImeToken,
@@ -1121,7 +1132,6 @@
         // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
         mNotificationIconAreaController.setupShelf(mNotificationShelfController);
         mShadeExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
-        mUserSwitcherController.init(mNotificationShadeWindowView);
 
         // Allow plugins to reference DarkIconDispatcher and StatusBarStateController
         mPluginDependencyProvider.allowPluginDependency(DarkIconDispatcher.class);
@@ -1148,7 +1158,6 @@
         initializer.initializeStatusBar(mCentralSurfacesComponent);
 
         mStatusBarTouchableRegionManager.setup(this, mNotificationShadeWindowView);
-        mHeadsUpManager.addListener(mNotificationPanelViewController.getOnHeadsUpChangedListener());
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
 
         createNavigationBar(result);
@@ -1157,9 +1166,6 @@
             mLockscreenWallpaper = mLockscreenWallpaperLazy.get();
         }
 
-        mNotificationPanelViewController.setKeyguardIndicationController(
-                mKeyguardIndicationController);
-
         mAmbientIndicationContainer = mNotificationShadeWindowView.findViewById(
                 R.id.ambient_indication_container);
 
@@ -1359,6 +1365,7 @@
     private void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
         float fraction = event.getFraction();
         boolean tracking = event.getTracking();
+        boolean isExpanded = event.getExpanded();
         dispatchPanelExpansionForKeyguardDismiss(fraction, tracking);
 
         if (fraction == 0 || fraction == 1) {
@@ -1371,6 +1378,23 @@
         }
     }
 
+    @VisibleForTesting
+    void onShadeExpansionFullyChanged(Boolean isExpanded) {
+        if (mPanelExpanded != isExpanded) {
+            mPanelExpanded = isExpanded;
+            if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
+                if (DEBUG) {
+                    Log.v(TAG, "clearing notification effects from Height");
+                }
+                clearNotificationEffects();
+            }
+
+            if (!isExpanded) {
+                mRemoteInputManager.onPanelCollapsed();
+            }
+        }
+    }
+
     @NonNull
     @Override
     public Lifecycle getLifecycle() {
@@ -1510,11 +1534,12 @@
     protected void startKeyguard() {
         Trace.beginSection("CentralSurfaces#startKeyguard");
         mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
-        mBiometricUnlockController.setBiometricModeListener(
+        mBiometricUnlockController.addBiometricModeListener(
                 new BiometricUnlockController.BiometricModeListener() {
                     @Override
                     public void onResetMode() {
                         setWakeAndUnlocking(false);
+                        notifyBiometricAuthModeChanged();
                     }
 
                     @Override
@@ -1525,11 +1550,7 @@
                             case BiometricUnlockController.MODE_WAKE_AND_UNLOCK:
                                 setWakeAndUnlocking(true);
                         }
-                    }
-
-                    @Override
-                    public void notifyBiometricAuthModeChanged() {
-                        CentralSurfacesImpl.this.notifyBiometricAuthModeChanged();
+                        notifyBiometricAuthModeChanged();
                     }
 
                     private void setWakeAndUnlocking(boolean wakeAndUnlocking) {
@@ -1609,18 +1630,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;
@@ -1772,27 +1781,10 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-        mNotificationShadeWindowController.setQsExpanded(expanded);
-        mNotificationPanelViewController.setStatusAccessibilityImportance(expanded
-                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        mNotificationPanelViewController.updateSystemUiStateFlags();
-        if (getNavigationBarView() != null) {
-            getNavigationBarView().onStatusBarPanelStateChanged();
-        }
-    }
-
-    @Override
     public boolean isWakeUpComingFromTouch() {
         return mWakeUpComingFromTouch;
     }
 
-    @Override
-    public boolean isFalsingThresholdNeeded() {
-        return true;
-    }
-
     /**
      * To be called when there's a state change in StatusBarKeyguardViewManager.
      */
@@ -1802,27 +1794,6 @@
     }
 
     @Override
-    public void setPanelExpanded(boolean isExpanded) {
-        if (mPanelExpanded != isExpanded) {
-            mNotificationLogger.onPanelExpandedChanged(isExpanded);
-        }
-        mPanelExpanded = isExpanded;
-        mStatusBarHideIconsForBouncerManager.setPanelExpandedAndTriggerUpdate(isExpanded);
-        mNotificationShadeWindowController.setPanelExpanded(isExpanded);
-        mStatusBarStateController.setPanelExpanded(isExpanded);
-        if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
-            if (DEBUG) {
-                Log.v(TAG, "clearing notification effects from Height");
-            }
-            clearNotificationEffects();
-        }
-
-        if (!isExpanded) {
-            mRemoteInputManager.onPanelCollapsed();
-        }
-    }
-
-    @Override
     public ViewGroup getNotificationScrollLayout() {
         return mStackScroller;
     }
@@ -2021,8 +1992,7 @@
     }
 
     void makeExpandedInvisible() {
-        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible
-                + " mExpandedVisible=" + mExpandedVisible);
+        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible);
 
         if (!mExpandedVisible || mNotificationShadeWindowView == null) {
             return;
@@ -2198,12 +2168,6 @@
         mNoAnimationOnNextBarModeChange = false;
     }
 
-    // Called by NavigationBarFragment
-    @Override
-    public void setQsScrimEnabled(boolean scrimEnabled) {
-        mNotificationPanelViewController.setQsScrimEnabled(scrimEnabled);
-    }
-
     /** Temporarily hides Bubbles if the status bar is hidden. */
     @Override
     public void updateBubblesVisibility() {
@@ -2281,13 +2245,6 @@
         }
 
         pw.println("  Panels: ");
-        if (mNotificationPanelViewController != null) {
-            pw.println("    mNotificationPanel="
-                    + mNotificationPanelViewController.getView() + " params="
-                    + mNotificationPanelViewController.getView().getLayoutParams().debug(""));
-            pw.print  ("      ");
-            mNotificationPanelViewController.dump(pw, args);
-        }
         pw.println("  mStackScroller: " + mStackScroller + " (dump moved)");
         pw.println("  Theme:");
         String nightMode = mUiModeManager == null ? "null" : mUiModeManager.getNightMode() + "";
@@ -2579,19 +2536,10 @@
                                 CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
                                 true /* force */, true /* delayed*/);
                     } else {
-
                         // Do it after DismissAction has been processed to conserve the needed
                         // ordering.
                         mMainExecutor.execute(mShadeController::runPostCollapseRunnables);
                     }
-                } else if (CentralSurfacesImpl.this.isInLaunchTransition()
-                        && mNotificationPanelViewController.isLaunchTransitionFinished()) {
-
-                    // We are not dismissing the shade, but the launch transition is already
-                    // finished,
-                    // so nobody will call readyForKeyguardDone anymore. Post it such that
-                    // keyguardDonePending gets called first.
-                    mMainExecutor.execute(mStatusBarKeyguardViewManager::readyForKeyguardDone);
                 }
                 return deferred;
             }
@@ -2967,7 +2915,10 @@
             //  * When phone is unlocked: we still don't want to execute hiding of the keyguard
             //    as the animation could prepare 'fake AOD' interface (without actually
             //    transitioning to keyguard state) and this might reset the view states
-            if (!mScreenOffAnimationController.isKeyguardHideDelayed()) {
+            if (!mScreenOffAnimationController.isKeyguardHideDelayed()
+                    // If we're animating occluded, there's an activity launching over the keyguard
+                    // UI. Wait to hide it until after the animation concludes.
+                    && !mKeyguardViewMediator.isOccludeAnimationPlaying()) {
                 return hideKeyguardImpl(forceStateChange);
             }
         }
@@ -2998,18 +2949,13 @@
 
     private void onLaunchTransitionFadingEnded() {
         mNotificationPanelViewController.resetAlpha();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         releaseGestureWakeLock();
         runLaunchTransitionEndRunnable();
         mKeyguardStateController.setLaunchTransitionFadingAway(false);
         mPresenter.updateMediaMetaData(true /* metaDataChanged */, true);
     }
 
-    @Override
-    public boolean isInLaunchTransition() {
-        return mNotificationPanelViewController.isLaunchTransitionFinished();
-    }
-
     /**
      * Fades the content of the keyguard away after the launch transition is done.
      *
@@ -3073,7 +3019,7 @@
 
     private void onLaunchTransitionTimeout() {
         Log.w(TAG, "Launch transition: Timeout!");
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         releaseGestureWakeLock();
         mNotificationPanelViewController.resetViews(false /* animate */);
     }
@@ -3126,7 +3072,7 @@
         }
         mMessageRouter.cancelMessages(MSG_LAUNCH_TRANSITION_TIMEOUT);
         releaseGestureWakeLock();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
         mNotificationPanelViewController.resetAlpha();
         mNotificationPanelViewController.resetTranslation();
         mNotificationPanelViewController.resetViewGroupFade();
@@ -3284,7 +3230,7 @@
     @Override
     public void endAffordanceLaunch() {
         releaseGestureWakeLock();
-        mNotificationPanelViewController.onAffordanceLaunchEnded();
+        mCameraLauncherLazy.get().setLaunchingAffordance(false);
     }
 
     /**
@@ -3347,9 +3293,9 @@
                 // lock screen where users can use the UDFPS affordance to enter the device
                 mStatusBarKeyguardViewManager.reset(true);
             } else if (mState == StatusBarState.KEYGUARD
-                    && !mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()
+                    && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
                     && isKeyguardSecure()) {
-                mStatusBarKeyguardViewManager.showGenericBouncer(true /* scrimmed */);
+                mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
             }
         }
     }
@@ -3390,12 +3336,6 @@
         }
     }
 
-    /** Collapse the panel. The collapsing will be animated for the given {@code duration}. */
-    @Override
-    public void collapsePanelWithDuration(int duration) {
-        mNotificationPanelViewController.collapseWithDuration(duration);
-    }
-
     /**
      * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example,
      * from the power button).
@@ -3448,22 +3388,6 @@
         }
     }
 
-    @Override
-    public void onUnlockHintStarted() {
-        mFalsingCollector.onUnlockHintStarted();
-        mKeyguardIndicationController.showActionToUnlock();
-    }
-
-    @Override
-    public void onHintFinished() {
-        // Delay the reset a bit so the user can read the text.
-        mKeyguardIndicationController.hideTransientIndicationDelayed(HINT_RESET_DELAY_MS);
-    }
-
-    @Override
-    public void onTrackingStopped(boolean expand) {
-    }
-
     // TODO: Figure out way to remove these.
     @Override
     public NavigationBarView getNavigationBarView() {
@@ -3485,15 +3409,6 @@
         mNavigationBarController.showPinningEscapeToast(mDisplayId);
     }
 
-    /**
-     * TODO: Remove this method. Views should not be passed forward. Will cause theme issues.
-     * @return bottom area view
-     */
-    @Override
-    public KeyguardBottomAreaView getKeyguardBottomAreaView() {
-        return mNotificationPanelViewController.getKeyguardBottomAreaView();
-    }
-
     protected ViewRootImpl getViewRootImpl()  {
         NotificationShadeWindowView nswv = getNotificationShadeWindowView();
         if (nswv != null) return nswv.getViewRootImpl();
@@ -3561,11 +3476,18 @@
         }
     }
 
+    @Override
+    public void collapseShadeForBugreport() {
+        if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) {
+            collapseShade();
+        }
+    }
+
     @VisibleForTesting
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
         public void onFinishedGoingToSleep() {
-            mNotificationPanelViewController.onAffordanceLaunchEnded();
+            mCameraLauncherLazy.get().setLaunchingAffordance(false);
             releaseGestureWakeLock();
             mLaunchCameraWhenFinishedWaking = false;
             mDeviceInteractive = false;
@@ -3666,7 +3588,8 @@
                         .updateSensitivenessForOccludedWakeup();
             }
             if (mLaunchCameraWhenFinishedWaking) {
-                mNotificationPanelViewController.launchCamera(mLastCameraLaunchSource);
+                mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource,
+                        mNotificationPanelViewController.isFullyCollapsed());
                 mLaunchCameraWhenFinishedWaking = false;
             }
             if (mLaunchEmergencyActionWhenFinishedWaking) {
@@ -3839,7 +3762,7 @@
      * is fully hidden, while 0 means the bouncer is visible.
      */
     @Override
-    public void setBouncerHiddenFraction(float expansion) {
+    public void setPrimaryBouncerHiddenFraction(float expansion) {
         mScrimController.setBouncerHiddenFraction(expansion);
     }
 
@@ -3857,11 +3780,10 @@
 
         mScrimController.setExpansionAffectsAlpha(!unlocking);
 
-        boolean launchingAffordanceWithPreview =
-                mNotificationPanelViewController.isLaunchingAffordanceWithPreview();
+        boolean launchingAffordanceWithPreview = mLaunchingAffordance;
         mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview);
 
-        if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
+        if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
             if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED
                     || mTransitionToFullShadeProgress > 0f) {
                 mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
@@ -3872,7 +3794,7 @@
             // Bouncer needs the front scrim when it's on top of an activity,
             // tapping on a notification, editing QS or being dismissed by
             // FLAG_DISMISS_KEYGUARD_ACTIVITY.
-            ScrimState state = mStatusBarKeyguardViewManager.bouncerNeedsScrimming()
+            ScrimState state = mStatusBarKeyguardViewManager.primaryBouncerNeedsScrimming()
                     ? ScrimState.BOUNCER_SCRIMMED : ScrimState.BOUNCER;
             mScrimController.transitionTo(state);
         } else if (launchingAffordanceWithPreview) {
@@ -4181,7 +4103,7 @@
      */
     @Override
     public boolean isBouncerShowingScrimmed() {
-        return isBouncerShowing() && mStatusBarKeyguardViewManager.bouncerNeedsScrimming();
+        return isBouncerShowing() && mStatusBarKeyguardViewManager.primaryBouncerNeedsScrimming();
     }
 
     @Override
@@ -4189,29 +4111,12 @@
         return mBouncerShowingOverDream;
     }
 
-    /**
-     * When {@link KeyguardBouncer} starts to be dismissed, playing its animation.
-     */
-    @Override
-    public void onBouncerPreHideAnimation() {
-        mNotificationPanelViewController.onBouncerPreHideAnimation();
-
-    }
-
     @Override
     public boolean isKeyguardSecure() {
         return mStatusBarKeyguardViewManager.isSecure();
     }
-    @Override
-    public NotificationPanelViewController getPanelController() {
-        return mNotificationPanelViewController;
-    }
-    // End Extra BaseStatusBarMethods.
 
-    @Override
-    public NotificationGutsManager getGutsManager() {
-        return mGutsManager;
-    }
+    // End Extra BaseStatusBarMethods.
 
     boolean isTransientShown() {
         return mTransientShown;
@@ -4335,7 +4240,6 @@
             }
             // TODO: Bring these out of CentralSurfaces.
             mUserInfoControllerImpl.onDensityOrFontScaleChanged();
-            mUserSwitcherController.onDensityOrFontScaleChanged();
             mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
             mHeadsUpManager.onDensityOrFontScaleChanged();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index 34cd1ce..7dcdc0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -33,7 +33,7 @@
     private val lastConfig = Configuration()
     private var density: Int = 0
     private var smallestScreenWidth: Int = 0
-    private var maxBounds: Rect? = null
+    private var maxBounds = Rect()
     private var fontScale: Float = 0.toFloat()
     private val inCarMode: Boolean
     private var uiMode: Int = 0
@@ -47,6 +47,7 @@
         fontScale = currentConfig.fontScale
         density = currentConfig.densityDpi
         smallestScreenWidth = currentConfig.smallestScreenWidthDp
+        maxBounds.set(currentConfig.windowConfiguration.maxBounds)
         inCarMode = currentConfig.uiMode and Configuration.UI_MODE_TYPE_MASK ==
                 Configuration.UI_MODE_TYPE_CAR
         uiMode = currentConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
@@ -92,7 +93,11 @@
 
         val maxBounds = newConfig.windowConfiguration.maxBounds
         if (maxBounds != this.maxBounds) {
-            this.maxBounds = maxBounds
+            // Update our internal rect to have the same bounds, instead of using
+            // `this.maxBounds = maxBounds` directly. Setting it directly means that `maxBounds`
+            // would be a direct reference to windowConfiguration.maxBounds, so the if statement
+            // above would always fail. See b/245799099 for more information.
+            this.maxBounds.set(maxBounds)
             listeners.filterForEach({ this.listeners.contains(it) }) {
                 it.onMaxBoundsChanged()
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 103e4f6..3743fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -34,6 +34,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -111,7 +112,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger);
         Resources resources = mContext.getResources();
         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
@@ -132,6 +134,8 @@
                 updateResources();
             }
         });
+
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
     }
 
     public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -221,13 +225,7 @@
         mTrackingHeadsUp = trackingHeadsUp;
     }
 
-    /**
-     * Notify that the status bar panel gets expanded or collapsed.
-     *
-     * @param isExpanded True to notify expanded, false to notify collapsed.
-     * TODO(b/237811427) replace with a listener
-     */
-    public void setIsPanelExpanded(boolean isExpanded) {
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
         if (isExpanded != mIsExpanded) {
             mIsExpanded = isExpanded;
             if (isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index 4897c52..78b28d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -23,6 +23,8 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import android.widget.FrameLayout
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.LockIconViewController
 import com.android.systemui.R
 import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
@@ -51,13 +53,20 @@
 
     private var ambientIndicationArea: View? = null
     private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
+    private lateinit var lockIconViewController: LockIconViewController
 
     /** Initializes the view. */
     fun init(
         viewModel: KeyguardBottomAreaViewModel,
         falsingManager: FalsingManager,
+        lockIconViewController: LockIconViewController,
     ) {
-        binding = bind(this, viewModel, falsingManager)
+        binding = bind(
+                this,
+                viewModel,
+                falsingManager,
+        )
+        this.lockIconViewController = lockIconViewController
     }
 
     /**
@@ -114,4 +123,29 @@
         }
         return insets
     }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        findViewById<View>(R.id.ambient_indication_container)?.let {
+            val (ambientLeft, ambientTop) = it.locationOnScreen
+            if (binding.shouldConstrainToTopOfLockIcon()) {
+                //make top of ambient indication view the bottom of the lock icon
+                it.layout(
+                        ambientLeft,
+                        lockIconViewController.bottom.toInt(),
+                        right - ambientLeft,
+                        ambientTop + it.measuredHeight
+                )
+            } else {
+                //make bottom of ambient indication view the top of the lock icon
+                val lockLocationTop = lockIconViewController.top
+                it.layout(
+                        ambientLeft,
+                        lockLocationTop.toInt() - it.measuredHeight,
+                        right - ambientLeft,
+                        lockLocationTop.toInt()
+                )
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 37f04bb..aa0757e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -55,15 +55,20 @@
 import javax.inject.Inject;
 
 /**
- * A class which manages the bouncer on the lockscreen.
+ * A class which manages the primary (pin/pattern/password) bouncer on the lockscreen.
  * @deprecated Use KeyguardBouncerRepository
  */
 @Deprecated
 public class KeyguardBouncer {
 
-    private static final String TAG = "KeyguardBouncer";
+    private static final String TAG = "PrimaryKeyguardBouncer";
     static final long BOUNCER_FACE_DELAY = 1200;
     public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f;
+    /**
+     * Values for the bouncer expansion represented as the panel expansion.
+     * Panel expansion 1f = panel fully showing = bouncer fully hidden
+     * Panel expansion 0f = panel fully hiding = bouncer fully showing
+     */
     public static final float EXPANSION_HIDDEN = 1f;
     public static final float EXPANSION_VISIBLE = 0f;
 
@@ -73,7 +78,7 @@
     private final FalsingCollector mFalsingCollector;
     private final DismissCallbackRegistry mDismissCallbackRegistry;
     private final Handler mHandler;
-    private final List<BouncerExpansionCallback> mExpansionCallbacks = new ArrayList<>();
+    private final List<PrimaryBouncerExpansionCallback> mExpansionCallbacks = new ArrayList<>();
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final KeyguardStateController mKeyguardStateController;
     private final KeyguardSecurityModel mKeyguardSecurityModel;
@@ -121,7 +126,7 @@
     private KeyguardBouncer(Context context, ViewMediatorCallback callback,
             ViewGroup container,
             DismissCallbackRegistry dismissCallbackRegistry, FalsingCollector falsingCollector,
-            BouncerExpansionCallback expansionCallback,
+            PrimaryBouncerExpansionCallback expansionCallback,
             KeyguardStateController keyguardStateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             KeyguardBypassController keyguardBypassController, @Main Handler handler,
@@ -143,6 +148,14 @@
     }
 
     /**
+     * Get the KeyguardBouncer expansion
+     * @return 1=HIDDEN, 0=SHOWING, in between 0 and 1 means the bouncer is in transition.
+     */
+    public float getExpansion() {
+        return mExpansion;
+    }
+
+    /**
      * Enable/disable only the back button
      */
     public void setBackButtonEnabled(boolean enabled) {
@@ -266,10 +279,7 @@
      * @see #onFullyShown()
      */
     private void onFullyHidden() {
-        cancelShowRunnable();
-        setVisibility(View.INVISIBLE);
-        mFalsingCollector.onBouncerHidden();
-        DejankUtils.postAfterTraversal(mResetRunnable);
+
     }
 
     private void setVisibility(@View.Visibility int visibility) {
@@ -446,7 +456,13 @@
             onFullyShown();
             dispatchFullyShown();
         } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) {
-            onFullyHidden();
+            DejankUtils.postAfterTraversal(mResetRunnable);
+            /*
+             * There are cases where #hide() was not invoked, such as when
+             * NotificationPanelViewController controls the hide animation. Make sure the state gets
+             * updated by calling #hide() directly.
+             */
+            hide(false /* destroyView */);
             dispatchFullyHidden();
         } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
             dispatchStartingToHide();
@@ -558,37 +574,37 @@
     }
 
     private void dispatchFullyShown() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onFullyShown();
         }
     }
 
     private void dispatchStartingToHide() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onStartingToHide();
         }
     }
 
     private void dispatchStartingToShow() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onStartingToShow();
         }
     }
 
     private void dispatchFullyHidden() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onFullyHidden();
         }
     }
 
     private void dispatchExpansionChanged() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onExpansionChanged(mExpansion);
         }
     }
 
     private void dispatchVisibilityChanged() {
-        for (BouncerExpansionCallback callback : mExpansionCallbacks) {
+        for (PrimaryBouncerExpansionCallback callback : mExpansionCallbacks) {
             callback.onVisibilityChanged(mContainer.getVisibility() == View.VISIBLE);
         }
     }
@@ -634,7 +650,7 @@
     /**
      * Adds a callback to listen to bouncer expansion updates.
      */
-    public void addBouncerExpansionCallback(BouncerExpansionCallback callback) {
+    public void addBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
         if (!mExpansionCallbacks.contains(callback)) {
             mExpansionCallbacks.add(callback);
         }
@@ -644,11 +660,14 @@
      * Removes a previously added callback. If the callback was never added, this methood
      * does nothing.
      */
-    public void removeBouncerExpansionCallback(BouncerExpansionCallback callback) {
+    public void removeBouncerExpansionCallback(PrimaryBouncerExpansionCallback callback) {
         mExpansionCallbacks.remove(callback);
     }
 
-    public interface BouncerExpansionCallback {
+    /**
+     * Callback updated when the primary bouncer's show and hide states change.
+     */
+    public interface PrimaryBouncerExpansionCallback {
         /**
          * Invoked when the bouncer expansion reaches {@link KeyguardBouncer#EXPANSION_VISIBLE}.
          * This is NOT called each time the bouncer is shown, but rather only when the fully
@@ -732,7 +751,7 @@
          * Construct a KeyguardBouncer that will exist in the given container.
          */
         public KeyguardBouncer create(ViewGroup container,
-                BouncerExpansionCallback expansionCallback) {
+                PrimaryBouncerExpansionCallback expansionCallback) {
             return new KeyguardBouncer(mContext, mCallback, container,
                     mDismissCallbackRegistry, mFalsingCollector, expansionCallback,
                     mKeyguardStateController, mKeyguardUpdateMonitor,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index b987f68..b965ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm
@@ -95,14 +96,7 @@
     var bouncerShowing: Boolean = false
     var altBouncerShowing: Boolean = false
     var launchingAffordance: Boolean = false
-    var qSExpanded = false
-        set(value) {
-            val changed = field != value
-            field = value
-            if (changed && !value) {
-                maybePerformPendingUnlock()
-            }
-        }
+    var qsExpanded = false
 
     @Inject
     constructor(
@@ -111,6 +105,7 @@
         statusBarStateController: StatusBarStateController,
         lockscreenUserManager: NotificationLockscreenUserManager,
         keyguardStateController: KeyguardStateController,
+        shadeExpansionStateManager: ShadeExpansionStateManager,
         dumpManager: DumpManager
     ) {
         this.mKeyguardStateController = keyguardStateController
@@ -132,6 +127,14 @@
             }
         })
 
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            val changed = qsExpanded != isQsExpanded
+            qsExpanded = isQsExpanded
+            if (changed && !isQsExpanded) {
+                maybePerformPendingUnlock()
+            }
+        }
+
         val dismissByDefault = if (context.resources.getBoolean(
                         com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0
         tunerService.addTunable(object : TunerService.Tunable {
@@ -160,7 +163,7 @@
     ): Boolean {
         if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) {
             val can = canBypass()
-            if (!can && (isPulseExpanding || qSExpanded)) {
+            if (!can && (isPulseExpanding || qsExpanded)) {
                 pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric)
             }
             return can
@@ -189,7 +192,7 @@
                 altBouncerShowing -> true
                 statusBarStateController.state != StatusBarState.KEYGUARD -> false
                 launchingAffordance -> false
-                isPulseExpanding || qSExpanded -> false
+                isPulseExpanding || qsExpanded -> false
                 else -> true
             }
         }
@@ -214,7 +217,7 @@
         pw.println("  altBouncerShowing: $altBouncerShowing")
         pw.println("  isPulseExpanding: $isPulseExpanding")
         pw.println("  launchingAffordance: $launchingAffordance")
-        pw.println("  qSExpanded: $qSExpanded")
+        pw.println("  qSExpanded: $qsExpanded")
         pw.println("  hasFaceFeature: $hasFaceFeature")
     }
 
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/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 18877f9..13566ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -26,6 +26,7 @@
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Trace;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.util.TypedValue;
@@ -47,6 +48,9 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
+import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -69,7 +73,7 @@
     private ImageView mMultiUserAvatar;
     private BatteryMeterView mBatteryView;
     private StatusIconContainer mStatusIconContainer;
-    private ViewGroup mUserSwitcherContainer;
+    private StatusBarUserSwitcherContainer mUserSwitcherContainer;
 
     private boolean mKeyguardUserSwitcherEnabled;
     private boolean mKeyguardUserAvatarEnabled;
@@ -120,8 +124,12 @@
         loadDimens();
     }
 
-    public ViewGroup getUserSwitcherContainer() {
-        return mUserSwitcherContainer;
+    /**
+     * Should only be called from {@link KeyguardStatusBarViewController}
+     * @param viewModel view model for the status bar user chip
+     */
+    void init(StatusBarUserChipViewModel viewModel) {
+        StatusBarUserChipViewBinder.bind(mUserSwitcherContainer, viewModel);
     }
 
     @Override
@@ -303,10 +311,7 @@
         lp = (LayoutParams) mStatusIconArea.getLayoutParams();
         lp.removeRule(RelativeLayout.RIGHT_OF);
         lp.width = LayoutParams.WRAP_CONTENT;
-
-        LinearLayout.LayoutParams llp =
-                (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams();
-        llp.setMarginStart(getResources().getDimensionPixelSize(
+        lp.setMarginStart(getResources().getDimensionPixelSize(
                 R.dimen.system_icons_super_container_margin_start));
         return true;
     }
@@ -338,10 +343,7 @@
         lp = (LayoutParams) mStatusIconArea.getLayoutParams();
         lp.addRule(RelativeLayout.RIGHT_OF, R.id.cutout_space_view);
         lp.width = LayoutParams.MATCH_PARENT;
-
-        LinearLayout.LayoutParams llp =
-                (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams();
-        llp.setMarginStart(0);
+        lp.setMarginStart(0);
         return true;
     }
 
@@ -527,4 +529,11 @@
         mClipRect.set(0, mTopClipping, getWidth(), getHeight());
         setClipBounds(mClipRect);
     }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Trace.beginSection("KeyguardStatusBarView#onMeasure");
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        Trace.endSection();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 14cebf4..d4dc1dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -59,13 +59,11 @@
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt;
 import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.settings.SecureSettings;
 
@@ -110,9 +108,7 @@
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final StatusBarContentInsetsProvider mInsetsProvider;
     private final UserManager mUserManager;
-    private final StatusBarUserSwitcherFeatureController mFeatureController;
-    private final StatusBarUserSwitcherController mUserSwitcherController;
-    private final StatusBarUserInfoTracker mStatusBarUserInfoTracker;
+    private final StatusBarUserChipViewModel mStatusBarUserChipViewModel;
     private final SecureSettings mSecureSettings;
     private final CommandQueue mCommandQueue;
     private final Executor mMainExecutor;
@@ -276,9 +272,7 @@
             SysuiStatusBarStateController statusBarStateController,
             StatusBarContentInsetsProvider statusBarContentInsetsProvider,
             UserManager userManager,
-            StatusBarUserSwitcherFeatureController featureController,
-            StatusBarUserSwitcherController userSwitcherController,
-            StatusBarUserInfoTracker statusBarUserInfoTracker,
+            StatusBarUserChipViewModel userChipViewModel,
             SecureSettings secureSettings,
             CommandQueue commandQueue,
             @Main Executor mainExecutor,
@@ -301,9 +295,7 @@
         mStatusBarStateController = statusBarStateController;
         mInsetsProvider = statusBarContentInsetsProvider;
         mUserManager = userManager;
-        mFeatureController = featureController;
-        mUserSwitcherController = userSwitcherController;
-        mStatusBarUserInfoTracker = statusBarUserInfoTracker;
+        mStatusBarUserChipViewModel = userChipViewModel;
         mSecureSettings = secureSettings;
         mCommandQueue = commandQueue;
         mMainExecutor = mainExecutor;
@@ -328,8 +320,7 @@
                 R.dimen.header_notifications_collide_distance);
 
         mView.setKeyguardUserAvatarEnabled(
-                !mFeatureController.isStatusBarUserSwitcherFeatureEnabled());
-        mFeatureController.addCallback(enabled -> mView.setKeyguardUserAvatarEnabled(!enabled));
+                !mStatusBarUserChipViewModel.getChipEnabled());
         mSystemEventAnimator = new StatusBarSystemEventAnimator(mView, r);
 
         mDisableStateTracker = new DisableStateTracker(
@@ -344,11 +335,11 @@
         super.onInit();
         mCarrierTextController.init();
         mBatteryMeterViewController.init();
-        mUserSwitcherController.init();
     }
 
     @Override
     protected void onViewAttached() {
+        mView.init(mStatusBarUserChipViewModel);
         mConfigurationController.addCallback(mConfigurationListener);
         mAnimationScheduler.addCallback(mAnimationCallback);
         mUserInfoController.addCallback(mOnUserInfoChangedListener);
@@ -394,9 +385,6 @@
     /** Sets whether user switcher is enabled. */
     public void setKeyguardUserSwitcherEnabled(boolean enabled) {
         mView.setKeyguardUserSwitcherEnabled(enabled);
-        // We don't have a listener for when the user switcher setting changes, so this is
-        // where we re-check the state
-        mStatusBarUserInfoTracker.checkEnabled();
     }
 
     /** Sets whether this controller should listen to battery updates. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 02b2354..4839fe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -19,9 +19,9 @@
 import android.util.DisplayMetrics
 import android.view.View
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.LSShadeTransitionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
index 16fddb42..6bf5443 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarTransitionsController.java
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 
 import dagger.assisted.Assisted;
 import dagger.assisted.AssistedFactory;
@@ -41,12 +42,54 @@
 /**
  * Class to control all aspects about light bar changes.
  */
-public class LightBarTransitionsController implements Dumpable, Callbacks,
-        StatusBarStateController.StateListener {
+public class LightBarTransitionsController implements Dumpable {
 
     public static final int DEFAULT_TINT_ANIMATION_DURATION = 120;
     private static final String EXTRA_DARK_INTENSITY = "dark_intensity";
 
+    private static class Callback implements Callbacks, StatusBarStateController.StateListener {
+        private final WeakReference<LightBarTransitionsController> mSelf;
+
+        Callback(LightBarTransitionsController self) {
+            mSelf = new WeakReference<>(self);
+        }
+
+        @Override
+        public void appTransitionPending(int displayId, boolean forced) {
+            LightBarTransitionsController self = mSelf.get();
+            if (self != null) {
+                self.appTransitionPending(displayId, forced);
+            }
+        }
+
+        @Override
+        public void appTransitionCancelled(int displayId) {
+            LightBarTransitionsController self = mSelf.get();
+            if (self != null) {
+                self.appTransitionCancelled(displayId);
+            }
+        }
+
+        @Override
+        public void appTransitionStarting(int displayId, long startTime, long duration,
+                boolean forced) {
+            LightBarTransitionsController self = mSelf.get();
+            if (self != null) {
+                self.appTransitionStarting(displayId, startTime, duration, forced);
+            }
+        }
+
+        @Override
+        public void onDozeAmountChanged(float linear, float eased) {
+            LightBarTransitionsController self = mSelf.get();
+            if (self != null) {
+                self.onDozeAmountChanged(linear, eased);
+            }
+        }
+    }
+
+    private final Callback mCallback;
+
     private final Handler mHandler;
     private final DarkIntensityApplier mApplier;
     private final KeyguardStateController mKeyguardStateController;
@@ -86,8 +129,9 @@
         mKeyguardStateController = keyguardStateController;
         mStatusBarStateController = statusBarStateController;
         mCommandQueue = commandQueue;
-        mCommandQueue.addCallback(this);
-        mStatusBarStateController.addCallback(this);
+        mCallback = new Callback(this);
+        mCommandQueue.addCallback(mCallback);
+        mStatusBarStateController.addCallback(mCallback);
         mDozeAmount = mStatusBarStateController.getDozeAmount();
         mContext = context;
         mDisplayId = mContext.getDisplayId();
@@ -95,8 +139,8 @@
 
     /** Call to cleanup the LightBarTransitionsController when done with it. */
     public void destroy() {
-        mCommandQueue.removeCallback(this);
-        mStatusBarStateController.removeCallback(this);
+        mCommandQueue.removeCallback(mCallback);
+        mStatusBarStateController.removeCallback(mCallback);
     }
 
     public void saveState(Bundle outState) {
@@ -110,16 +154,14 @@
         mNextDarkIntensity = mDarkIntensity;
     }
 
-    @Override
-    public void appTransitionPending(int displayId, boolean forced) {
+    private void appTransitionPending(int displayId, boolean forced) {
         if (mDisplayId != displayId || mKeyguardStateController.isKeyguardGoingAway() && !forced) {
             return;
         }
         mTransitionPending = true;
     }
 
-    @Override
-    public void appTransitionCancelled(int displayId) {
+    private void appTransitionCancelled(int displayId) {
         if (mDisplayId != displayId) {
             return;
         }
@@ -131,9 +173,7 @@
         mTransitionPending = false;
     }
 
-    @Override
-    public void appTransitionStarting(int displayId, long startTime, long duration,
-            boolean forced) {
+    private void appTransitionStarting(int displayId, long startTime, long duration, boolean forced) {
         if (mDisplayId != displayId || mKeyguardStateController.isKeyguardGoingAway() && !forced) {
             return;
         }
@@ -230,10 +270,6 @@
         pw.print(" mNextDarkIntensity="); pw.println(mNextDarkIntensity);
     }
 
-    @Override
-    public void onStateChanged(int newState) { }
-
-    @Override
     public void onDozeAmountChanged(float linear, float eased) {
         mDozeAmount = eased;
         dispatchDark();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
index 6e98c49..eba7fe0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
@@ -22,8 +22,8 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.view.InsetsVisibilities;
 import android.view.View;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 import android.view.WindowManager;
@@ -144,7 +144,7 @@
         @Override
         public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                @Behavior int behavior, @InsetsType int requestedVisibleTypes,
                 String packageName, LetterboxDetails[] letterboxDetails) {
             if (displayId != mDisplayId) {
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
index 8793a57..1d7dfe1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.phone;
 
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.app.IWallpaperManager;
 import android.app.IWallpaperManagerCallback;
 import android.app.WallpaperColors;
@@ -45,6 +44,7 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationMediaManager;
 
 import libcore.io.IoUtils;
@@ -82,10 +82,11 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             DumpManager dumpManager,
             NotificationMediaManager mediaManager,
-            @Main Handler mainHandler) {
+            @Main Handler mainHandler,
+            UserTracker userTracker) {
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
         mWallpaperManager = wallpaperManager;
-        mCurrentUserId = ActivityManager.getCurrentUser();
+        mCurrentUserId = userTracker.getUserId();
         mUpdateMonitor = keyguardUpdateMonitor;
         mMediaManager = mediaManager;
         mH = mainHandler;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 94d1bf4..26e6db6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.statusbar.phone;
 
-import android.app.ActivityManager;
 import android.app.StatusBarManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -28,6 +27,7 @@
 
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.settings.UserTracker;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -44,6 +44,7 @@
 
     private final Context mContext;
     private final UserManager mUserManager;
+    private final UserTracker mUserTracker;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final LinkedList<UserInfo> mProfiles;
     private boolean mListening;
@@ -52,9 +53,11 @@
     /**
      */
     @Inject
-    public ManagedProfileControllerImpl(Context context, BroadcastDispatcher broadcastDispatcher) {
+    public ManagedProfileControllerImpl(Context context, UserTracker userTracker,
+            BroadcastDispatcher broadcastDispatcher) {
         mContext = context;
         mUserManager = UserManager.get(mContext);
+        mUserTracker = userTracker;
         mBroadcastDispatcher = broadcastDispatcher;
         mProfiles = new LinkedList<UserInfo>();
     }
@@ -90,7 +93,7 @@
     private void reloadManagedProfiles() {
         synchronized (mProfiles) {
             boolean hadProfile = mProfiles.size() > 0;
-            int user = ActivityManager.getCurrentUser();
+            int user = mUserTracker.getUserId();
             mProfiles.clear();
 
             for (UserInfo ui : mUserManager.getEnabledProfiles(user)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 00c3e8f..5e2a7c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
@@ -26,6 +26,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
@@ -67,7 +68,7 @@
                         ActivityLaunchAnimator.Controller.fromView(v, null),
                         true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM);
             } else {
-                mUserSwitchDialogController.showDialog(v);
+                mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v));
             }
         }
     };
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..4ee2de1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -141,7 +141,6 @@
     /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */
     public static final int MAX_ICONS_ON_LOCKSCREEN = 3;
     public static final int MAX_STATIC_ICONS = 4;
-    private static final int MAX_DOTS = 1;
 
     private boolean mIsStaticLayout = true;
     private final HashMap<View, IconState> mIconStates = new HashMap<>();
@@ -166,8 +165,7 @@
     private IconState mLastVisibleIconState;
     private IconState mFirstVisibleIconState;
     private float mVisualOverflowStart;
-    // Keep track of overflow in range [0, 3]
-    private int mNumDots;
+    private boolean mIsShowingOverflowDot;
     private StatusBarIconView mIsolatedIcon;
     private Rect mIsolatedIconLocation;
     private int[] mAbsolutePosition = new int[2];
@@ -387,8 +385,8 @@
         }
     }
 
-    public boolean hasMaxNumDot() {
-        return mNumDots >= MAX_DOTS;
+    public boolean areIconsOverflowing() {
+        return mIsShowingOverflowDot;
     }
 
     private boolean areAnimationsEnabled(StatusBarIconView icon) {
@@ -494,7 +492,7 @@
                     : 1f;
             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
         }
-        mNumDots = 0;
+        mIsShowingOverflowDot = false;
         if (firstOverflowIndex != -1) {
             translationX = mVisualOverflowStart;
             for (int i = firstOverflowIndex; i < childCount; i++) {
@@ -502,15 +500,14 @@
                 IconState iconState = mIconStates.get(view);
                 int dotWidth = mStaticDotDiameter + mDotPadding;
                 iconState.setXTranslation(translationX);
-                if (mNumDots < MAX_DOTS) {
-                    if (mNumDots == 0 && iconState.iconAppearAmount < 0.8f) {
+                if (!mIsShowingOverflowDot) {
+                    if (iconState.iconAppearAmount < 0.8f) {
                         iconState.visibleState = StatusBarIconView.STATE_ICON;
                     } else {
                         iconState.visibleState = StatusBarIconView.STATE_DOT;
-                        mNumDots++;
+                        mIsShowingOverflowDot = true;
                     }
-                    translationX += (mNumDots == MAX_DOTS ? MAX_DOTS * dotWidth : dotWidth)
-                            * iconState.iconAppearAmount;
+                    translationX += dotWidth * iconState.iconAppearAmount;
                     mLastVisibleIconState = iconState;
                 } else {
                     iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
@@ -618,10 +615,6 @@
         return Math.min(getWidth(), translation);
     }
 
-    private float getMaxOverflowStart() {
-        return getLayoutEnd() - mIconSize;
-    }
-
     public void setChangingViewPositions(boolean changingViewPositions) {
         mChangingViewPositions = changingViewPositions;
     }
@@ -645,56 +638,6 @@
         mSpeedBumpIndex = speedBumpIndex;
     }
 
-    public boolean hasOverflow() {
-        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) {
-            return 0;
-        }
-
-        int collapsedPadding = mIconSize;
-
-        if (collapsedPadding + getFinalTranslationX() > getWidth()) {
-            collapsedPadding = getWidth() - getFinalTranslationX();
-        }
-
-        return collapsedPadding;
-    }
-
     public int getIconSize() {
         return mIconSize;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 7aeb08d..28bc64d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -38,6 +38,9 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
+import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
 import com.android.systemui.util.leak.RotationUtils;
 
 import java.util.Objects;
@@ -73,6 +76,11 @@
         mTouchEventHandler = handler;
     }
 
+    void init(StatusBarUserChipViewModel viewModel) {
+        StatusBarUserSwitcherContainer container = findViewById(R.id.user_switcher_container);
+        StatusBarUserChipViewBinder.bind(container, viewModel);
+    }
+
     @Override
     public void onFinishInflate() {
         super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index f9c4c8f..a6c2b2c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -23,11 +23,11 @@
 import android.view.ViewTreeObserver
 import com.android.systemui.R
 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.unfold.SysUIUnfoldComponent
 import com.android.systemui.unfold.UNFOLD_STATUS_BAR
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.kotlin.getOrNull
 import com.android.systemui.util.view.ViewUtil
@@ -40,7 +40,7 @@
     view: PhoneStatusBarView,
     @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
     private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
-    private val userSwitcherController: StatusBarUserSwitcherController,
+    private val userChipViewModel: StatusBarUserChipViewModel,
     private val viewUtil: ViewUtil,
     touchEventHandler: PhoneStatusBarView.TouchEventHandler,
     private val configurationController: ConfigurationController
@@ -91,10 +91,10 @@
 
     init {
         mView.setTouchEventHandler(touchEventHandler)
+        mView.init(userChipViewModel)
     }
 
     override fun onInit() {
-        userSwitcherController.init()
     }
 
     fun setImportantForAccessibility(mode: Int) {
@@ -156,9 +156,9 @@
         private val unfoldComponent: Optional<SysUIUnfoldComponent>,
         @Named(UNFOLD_STATUS_BAR)
         private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
-        private val userSwitcherController: StatusBarUserSwitcherController,
+        private val userChipViewModel: StatusBarUserChipViewModel,
         private val viewUtil: ViewUtil,
-        private val configurationController: ConfigurationController
+        private val configurationController: ConfigurationController,
     ) {
         fun create(
             view: PhoneStatusBarView,
@@ -168,7 +168,7 @@
                 view,
                 progressProvider.getOrNull(),
                 unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController(),
-                userSwitcherController,
+                userChipViewModel,
                 viewUtil,
                 touchEventHandler,
                 configurationController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 9f93223..fb0d3e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -110,6 +110,12 @@
     private boolean mClipsQsScrim;
 
     /**
+     * Whether an activity is launching over the lockscreen. During the launch animation, we want to
+     * delay certain scrim changes until after the animation ends.
+     */
+    private boolean mOccludeAnimationPlaying = false;
+
+    /**
      * The amount of progress we are currently in if we're transitioning to the full shade.
      * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
      * shade.
@@ -249,6 +255,7 @@
     private Callback mCallback;
     private boolean mWallpaperSupportsAmbientMode;
     private boolean mScreenOn;
+    private boolean mTransparentScrimBackground;
 
     // Scrim blanking callbacks
     private Runnable mPendingFrameCallback;
@@ -341,6 +348,8 @@
         mScrimBehind.setDefaultFocusHighlightEnabled(false);
         mNotificationsScrim.setDefaultFocusHighlightEnabled(false);
         mScrimInFront.setDefaultFocusHighlightEnabled(false);
+        mTransparentScrimBackground = notificationsScrim.getResources()
+                .getBoolean(R.bool.notification_scrim_transparent);
         updateScrims();
         mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
     }
@@ -730,6 +739,11 @@
         return mClipsQsScrim;
     }
 
+    public void setOccludeAnimationPlaying(boolean occludeAnimationPlaying) {
+        mOccludeAnimationPlaying = occludeAnimationPlaying;
+        applyAndDispatchState();
+    }
+
     private void setOrAdaptCurrentAnimation(@Nullable View scrim) {
         if (scrim == null) {
             return;
@@ -769,21 +783,28 @@
         }
 
         if (mState == ScrimState.UNLOCKED || mState == ScrimState.DREAMING) {
-            // Darken scrim as you pull down the shade when unlocked, unless the shade is expanding
-            // because we're doing the screen off animation OR the shade is collapsing because
-            // we're playing the unlock animation
+            final boolean occluding =
+                    mOccludeAnimationPlaying || mState.mLaunchingAffordanceWithPreview;
+
+            // Darken scrim as it's pulled down while unlocked. If we're unlocked but playing the
+            // screen off/occlusion animations, ignore expansion changes while those animations
+            // play.
             if (!mScreenOffAnimationController.shouldExpandNotifications()
-                    && !mAnimatingPanelExpansionOnUnlock) {
+                    && !mAnimatingPanelExpansionOnUnlock
+                    && !occluding) {
                 float behindFraction = getInterpolatedFraction();
                 behindFraction = (float) Math.pow(behindFraction, 0.8f);
                 if (mClipsQsScrim) {
-                    mBehindAlpha = 1;
-                    mNotificationsAlpha = behindFraction * mDefaultScrimAlpha;
+                    mBehindAlpha = mTransparentScrimBackground ? 0 : 1;
+                    mNotificationsAlpha =
+                            mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
                 } else {
-                    mBehindAlpha = behindFraction * mDefaultScrimAlpha;
+                    mBehindAlpha =
+                            mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha;
                     // Delay fade-in of notification scrim a bit further, to coincide with the
                     // view fade in. Otherwise the empty panel can be quite jarring.
-                    mNotificationsAlpha = MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
+                    mNotificationsAlpha = mTransparentScrimBackground
+                            ? 0 : MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f,
                             mPanelExpansionFraction);
                 }
                 mBehindTint = mState.getBehindTint();
@@ -802,15 +823,7 @@
                         interpolatedFraction);
             }
         } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
-            float behindFraction = getInterpolatedFraction();
-            behindFraction = (float) Math.pow(behindFraction, 0.8f);
-
-            mBehindAlpha = behindFraction * mDefaultScrimAlpha;
-            mNotificationsAlpha = mBehindAlpha;
-            if (mClipsQsScrim) {
-                mBehindAlpha = 1;
-                mBehindTint = Color.BLACK;
-            }
+            mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f);
         } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED
                 || mState == ScrimState.PULSING) {
             Pair<Integer, Float> result = calculateBackStateForState(mState);
@@ -885,7 +898,7 @@
 
         float stateBehind = mClipsQsScrim ? state.getNotifAlpha() : state.getBehindAlpha();
         float behindAlpha;
-        int behindTint;
+        int behindTint = state.getBehindTint();
         if (mDarkenWhileDragging) {
             behindAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind,
                     interpolatedFract);
@@ -893,17 +906,19 @@
             behindAlpha = MathUtils.lerp(0 /* start */, stateBehind,
                     interpolatedFract);
         }
-        if (mClipsQsScrim) {
-            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
+        if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) {
+            if (mClipsQsScrim) {
+                behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getNotifTint(),
                     state.getNotifTint(), interpolatedFract);
-        } else {
-            behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
+            } else {
+                behindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
                     state.getBehindTint(), interpolatedFract);
+            }
         }
         if (mQsExpansion > 0) {
             behindAlpha = MathUtils.lerp(behindAlpha, mDefaultScrimAlpha, mQsExpansion);
             float tintProgress = mQsExpansion;
-            if (mStatusBarKeyguardViewManager.isBouncerInTransit()) {
+            if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) {
                 // this is case of - on lockscreen - going from expanded QS to bouncer.
                 // Because mQsExpansion is already interpolated and transition between tints
                 // is too slow, we want to speed it up and make it more aligned to bouncer
@@ -1086,7 +1101,7 @@
     }
 
     private float getInterpolatedFraction() {
-        if (mStatusBarKeyguardViewManager.isBouncerInTransit()) {
+        if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) {
             return BouncerPanelExpansionCalculator
                     .aboutToShowBouncerProgress(mPanelExpansionFraction);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index b447f0d..52430d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -90,11 +90,14 @@
     AUTH_SCRIMMED_SHADE {
         @Override
         public void prepare(ScrimState previousState) {
-            // notif & behind scrim alpha values are determined by ScrimController#applyState
+            // notif scrim alpha values are determined by ScrimController#applyState
             // based on the shade expansion
 
             mFrontTint = Color.BLACK;
             mFrontAlpha = .66f;
+
+            mBehindTint = Color.BLACK;
+            mBehindAlpha = 1f;
         }
     },
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
index 5113191..4d9de09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
@@ -5,6 +5,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -24,10 +25,11 @@
  */
 @SysUISingleton
 class StatusBarHideIconsForBouncerManager @Inject constructor(
-    private val commandQueue: CommandQueue,
-    @Main private val mainExecutor: DelayableExecutor,
-    statusBarWindowStateController: StatusBarWindowStateController,
-    dumpManager: DumpManager
+        private val commandQueue: CommandQueue,
+        @Main private val mainExecutor: DelayableExecutor,
+        statusBarWindowStateController: StatusBarWindowStateController,
+        shadeExpansionStateManager: ShadeExpansionStateManager,
+        dumpManager: DumpManager
 ) : Dumpable {
     // State variables set by external classes.
     private var panelExpanded: Boolean = false
@@ -47,6 +49,12 @@
         statusBarWindowStateController.addListener {
                 state -> setStatusBarStateAndTriggerUpdate(state)
         }
+        shadeExpansionStateManager.addFullExpansionListener { isExpanded ->
+            if (panelExpanded != isExpanded) {
+                panelExpanded = isExpanded
+                updateHideIconsForBouncer(animate = false)
+            }
+        }
     }
 
     /** Returns true if the status bar icons should be hidden in the bouncer. */
@@ -63,11 +71,6 @@
         this.displayId = displayId
     }
 
-    fun setPanelExpandedAndTriggerUpdate(panelExpanded: Boolean) {
-        this.panelExpanded = panelExpanded
-        updateHideIconsForBouncer(animate = false)
-    }
-
     fun setIsOccludedAndTriggerUpdate(isOccluded: Boolean) {
         this.isOccluded = isOccluded
         updateHideIconsForBouncer(animate = false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index ece7ee0..0a0ded2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -18,6 +18,7 @@
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI;
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -53,8 +54,9 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
 import com.android.systemui.util.Assert;
 
 import java.util.ArrayList;
@@ -84,7 +86,18 @@
     /** */
     void setIcon(String slot, StatusBarIcon icon);
     /** */
-    void setSignalIcon(String slot, WifiIconState state);
+    void setWifiIcon(String slot, WifiIconState state);
+
+    /**
+     * Sets up a wifi icon using the new data pipeline. No effect if the wifi icon has already been
+     * set up (inflated and added to the view hierarchy).
+     *
+     * This method completely replaces {@link #setWifiIcon} with the information from the new wifi
+     * data pipeline. Icons will automatically keep their state up to date, so we don't have to
+     * worry about funneling state objects through anymore.
+     */
+    void setNewWifiIcon();
+
     /** */
     void setMobileIcons(String slot, List<MobileIconState> states);
 
@@ -151,14 +164,14 @@
                 LinearLayout linearLayout,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(linearLayout,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     mobileContextProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
@@ -218,7 +231,7 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final WifiViewModel mWifiViewModel;
+            private final WifiUiAdapter mWifiUiAdapter;
             private final MobileContextProvider mMobileContextProvider;
             private final MobileUiAdapter mMobileUiAdapter;
             private final DarkIconDispatcher mDarkIconDispatcher;
@@ -226,12 +239,12 @@
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    WifiViewModel wifiViewModel,
+                    WifiUiAdapter wifiUiAdapter,
                     MobileContextProvider mobileContextProvider,
                     MobileUiAdapter mobileUiAdapter,
                     DarkIconDispatcher darkIconDispatcher) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModel = wifiViewModel;
+                mWifiUiAdapter = wifiUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
                 mMobileUiAdapter = mobileUiAdapter;
                 mDarkIconDispatcher = darkIconDispatcher;
@@ -242,7 +255,7 @@
                         group,
                         location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModel,
+                        mWifiUiAdapter,
                         mMobileUiAdapter,
                         mMobileContextProvider,
                         mDarkIconDispatcher);
@@ -260,14 +273,14 @@
                 ViewGroup group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider
         ) {
             super(group,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     mobileContextProvider);
         }
@@ -302,19 +315,19 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final WifiViewModel mWifiViewModel;
+            private final WifiUiAdapter mWifiUiAdapter;
             private final MobileContextProvider mMobileContextProvider;
             private final MobileUiAdapter mMobileUiAdapter;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    WifiViewModel wifiViewModel,
+                    WifiUiAdapter wifiUiAdapter,
                     MobileUiAdapter mobileUiAdapter,
                     MobileContextProvider mobileContextProvider
             ) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModel = wifiViewModel;
+                mWifiUiAdapter = wifiUiAdapter;
                 mMobileUiAdapter = mobileUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
             }
@@ -324,7 +337,7 @@
                         group,
                         location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModel,
+                        mWifiUiAdapter,
                         mMobileUiAdapter,
                         mMobileContextProvider);
             }
@@ -336,10 +349,9 @@
      */
     class IconManager implements DemoModeCommandReceiver {
         protected final ViewGroup mGroup;
-        private final StatusBarLocation mLocation;
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-        private final WifiViewModel mWifiViewModel;
         private final MobileContextProvider mMobileContextProvider;
+        private final LocationBasedWifiViewModel mWifiViewModel;
         private final MobileIconsViewModel mMobileIconsViewModel;
 
         protected final Context mContext;
@@ -359,26 +371,33 @@
                 ViewGroup group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider
         ) {
             mGroup = group;
-            mLocation = location;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
-            mWifiViewModel = wifiViewModel;
             mMobileContextProvider = mobileContextProvider;
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
 
-            if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
-                // This starts the flow for the new pipeline, and will notify us of changes
+            if (statusBarPipelineFlags.runNewMobileIconsBackend()) {
+                // This starts the flow for the new pipeline, and will notify us of changes if
+                // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true.
                 mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
                 MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
             } else {
                 mMobileIconsViewModel = null;
             }
+
+            if (statusBarPipelineFlags.runNewWifiIconBackend()) {
+                // This starts the flow for the new pipeline, and will notify us of changes if
+                // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true.
+                mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location);
+            } else {
+                mWifiViewModel = null;
+            }
         }
 
         public boolean isDemoable() {
@@ -429,6 +448,9 @@
                 case TYPE_WIFI:
                     return addWifiIcon(index, slot, holder.getWifiState());
 
+                case TYPE_WIFI_NEW:
+                    return addNewWifiIcon(index, slot);
+
                 case TYPE_MOBILE:
                     return addMobileIcon(index, slot, holder.getMobileState());
 
@@ -450,16 +472,13 @@
 
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
-            final BaseStatusBarFrameLayout view;
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
-                view = onCreateModernStatusBarWifiView(slot);
-                // When [ModernStatusBarWifiView] is created, it will automatically apply the
-                // correct view state so we don't need to call applyWifiState.
-            } else {
-                StatusBarWifiView wifiView = onCreateStatusBarWifiView(slot);
-                wifiView.applyWifiState(state);
-                view = wifiView;
+            if (mStatusBarPipelineFlags.useNewWifiIcon()) {
+                throw new IllegalStateException("Attempting to add a mobile icon while the new "
+                        + "icons are enabled is not supported");
             }
+
+            final StatusBarWifiView view = onCreateStatusBarWifiView(slot);
+            view.applyWifiState(state);
             mGroup.addView(view, index, onCreateLayoutParams());
 
             if (mIsInDemoMode) {
@@ -468,15 +487,26 @@
             return view;
         }
 
+        protected StatusIconDisplayable addNewWifiIcon(int index, String slot) {
+            if (!mStatusBarPipelineFlags.useNewWifiIcon()) {
+                throw new IllegalStateException("Attempting to add a wifi icon using the new"
+                        + "pipeline, but the enabled flag is false.");
+            }
+
+            ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
+            mGroup.addView(view, index, onCreateLayoutParams());
+            return view;
+        }
+
         @VisibleForTesting
         protected StatusIconDisplayable addMobileIcon(
                 int index,
                 String slot,
                 MobileIconState state
         ) {
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon while the new "
-                        + "pipeline is enabled is not supported");
+                        + "icons are enabled is not supported");
             }
 
             // Use the `subId` field as a key to query for the correct context
@@ -497,7 +527,7 @@
                 String slot,
                 int subId
         ) {
-            if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon using the new"
                         + "pipeline, but the enabled flag is false.");
             }
@@ -523,8 +553,7 @@
         }
 
         private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
-            return ModernStatusBarWifiView.constructAndBind(
-                    mContext, slot, mWifiViewModel, mLocation);
+            return ModernStatusBarWifiView.constructAndBind(mContext, slot, mWifiViewModel);
         }
 
         private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) {
@@ -600,7 +629,8 @@
                     onSetMobileIcon(viewIndex, holder.getMobileState());
                     return;
                 case TYPE_MOBILE_NEW:
-                    // Nothing, the icon updates itself now
+                case TYPE_WIFI_NEW:
+                    // Nothing, the new icons update themselves
                     return;
                 default:
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index e106b9e..674e574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -195,12 +195,13 @@
         }
     }
 
-    /**
-     * Signal icons need to be handled differently, because they can be
-     * composite views
-     */
     @Override
-    public void setSignalIcon(String slot, WifiIconState state) {
+    public void setWifiIcon(String slot, WifiIconState state) {
+        if (mStatusBarPipelineFlags.useNewWifiIcon()) {
+            Log.d(TAG, "ignoring old pipeline callback because the new wifi icon is enabled");
+            return;
+        }
+
         if (state == null) {
             removeIcon(slot, 0);
             return;
@@ -216,6 +217,24 @@
         }
     }
 
+
+    @Override
+    public void setNewWifiIcon() {
+        if (!mStatusBarPipelineFlags.useNewWifiIcon()) {
+            Log.d(TAG, "ignoring new pipeline callback because the new wifi icon is disabled");
+            return;
+        }
+
+        String slot = mContext.getString(com.android.internal.R.string.status_bar_wifi);
+        StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, /* tag= */ 0);
+        if (holder == null) {
+            holder = StatusBarIconHolder.forNewWifiIcon();
+            setIcon(slot, holder);
+        } else {
+            // Don't have to do anything in the new world
+        }
+    }
+
     /**
      * Accept a list of MobileIconStates, which all live in the same slot(?!), and then are sorted
      * by subId. Don't worry this definitely makes sense and works.
@@ -224,9 +243,9 @@
      */
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
-        if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
-            Log.d(TAG, "ignoring old pipeline callbacks, because the new "
-                    + "pipeline frontend is enabled");
+        if (mStatusBarPipelineFlags.useNewMobileIcons()) {
+            Log.d(TAG, "ignoring old pipeline callbacks, because the new mobile "
+                    + "icons are enabled");
             return;
         }
         Slot mobileSlot = mStatusBarIconList.getSlot(slot);
@@ -249,12 +268,13 @@
 
     @Override
     public void setNewMobileIconSubIds(List<Integer> subIds) {
-        if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+        if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring new pipeline callback, "
-                    + "since the frontend is disabled");
+                    + "since the new mobile icons are disabled");
             return;
         }
-        Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
+        String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile);
+        Slot mobileSlot = mStatusBarIconList.getSlot(slotName);
 
         Collections.reverse(subIds);
 
@@ -262,7 +282,7 @@
             StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId);
             if (holder == null) {
                 holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId);
-                setIcon("mobile", holder);
+                setIcon(slotName, holder);
             } else {
                 // Don't have to do anything in the new world
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
index 68a203e..f6c0da8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
@@ -51,11 +51,24 @@
     @Deprecated
     public static final int TYPE_MOBILE_NEW = 3;
 
+    /**
+     * TODO (b/238425913): address this once the new pipeline is in place
+     * This type exists so that the new wifi pipeline can be used to inform the old view system
+     * about the existence of the wifi icon. The design of the new pipeline should allow for removal
+     * of this icon holder type, and obsolete the need for this entire class.
+     *
+     * @deprecated This field only exists so the new status bar pipeline can interface with the
+     * view holder system.
+     */
+    @Deprecated
+    public static final int TYPE_WIFI_NEW = 4;
+
     @IntDef({
             TYPE_ICON,
             TYPE_WIFI,
             TYPE_MOBILE,
-            TYPE_MOBILE_NEW
+            TYPE_MOBILE_NEW,
+            TYPE_WIFI_NEW
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface IconType {}
@@ -95,6 +108,13 @@
         return holder;
     }
 
+    /** Creates a new holder with for the new wifi icon. */
+    public static StatusBarIconHolder forNewWifiIcon() {
+        StatusBarIconHolder holder = new StatusBarIconHolder();
+        holder.mType = TYPE_WIFI_NEW;
+        return holder;
+    }
+
     /** */
     public static StatusBarIconHolder fromMobileIconState(MobileIconState state) {
         StatusBarIconHolder holder = new StatusBarIconHolder();
@@ -172,9 +192,10 @@
             case TYPE_MOBILE:
                 return mMobileState.visible;
             case TYPE_MOBILE_NEW:
-                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+            case TYPE_WIFI_NEW:
+                // The new pipeline controls visibilities via the view model and view binder, so
+                // this is effectively an unused return value.
                 return true;
-
             default:
                 return true;
         }
@@ -199,7 +220,9 @@
                 break;
 
             case TYPE_MOBILE_NEW:
-                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+            case TYPE_WIFI_NEW:
+                // The new pipeline controls visibilities via the view model and view binder, so
+                // ignore setVisible.
                 break;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5f5ec68..9e075e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -60,9 +60,8 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.data.BouncerViewDelegate;
-import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
-import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -79,7 +78,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
+import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.unfold.FoldAodAnimationController;
@@ -87,8 +86,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -134,69 +135,64 @@
     @Nullable
     private final FoldAodAnimationController mFoldAodAnimationController;
     private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
-    private final BouncerCallbackInteractor mBouncerCallbackInteractor;
-    private final BouncerInteractor mBouncerInteractor;
-    private final BouncerViewDelegate mBouncerViewDelegate;
+    private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
+    private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    private final BouncerView mPrimaryBouncerView;
     private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
 
-    private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() {
-        private boolean mBouncerAnimating;
+    private final PrimaryBouncerExpansionCallback mExpansionCallback =
+            new PrimaryBouncerExpansionCallback() {
+            private boolean mPrimaryBouncerAnimating;
 
-        @Override
-        public void onFullyShown() {
-            mBouncerAnimating = false;
-            updateStates();
-        }
-
-        @Override
-        public void onStartingToHide() {
-            mBouncerAnimating = true;
-            updateStates();
-        }
-
-        @Override
-        public void onStartingToShow() {
-            mBouncerAnimating = true;
-            updateStates();
-        }
-
-        @Override
-        public void onFullyHidden() {
-            mBouncerAnimating = false;
-        }
-
-        @Override
-        public void onExpansionChanged(float expansion) {
-            if (mAlternateAuthInterceptor != null) {
-                mAlternateAuthInterceptor.setBouncerExpansionChanged(expansion);
-            }
-            if (mBouncerAnimating) {
-                mCentralSurfaces.setBouncerHiddenFraction(expansion);
-            }
-            updateStates();
-        }
-
-        @Override
-        public void onVisibilityChanged(boolean isVisible) {
-            mCentralSurfaces
-                    .setBouncerShowingOverDream(
-                            isVisible && mDreamOverlayStateController.isOverlayActive());
-
-            if (!isVisible) {
-                mCentralSurfaces.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN);
-            }
-            if (mAlternateAuthInterceptor != null) {
-                mAlternateAuthInterceptor.onBouncerVisibilityChanged();
+            @Override
+            public void onFullyShown() {
+                mPrimaryBouncerAnimating = false;
+                updateStates();
             }
 
-            /* Register predictive back callback when keyguard becomes visible, and unregister
-            when it's hidden. */
-            if (isVisible) {
-                registerBackCallback();
-            } else {
-                unregisterBackCallback();
+            @Override
+            public void onStartingToHide() {
+                mPrimaryBouncerAnimating = true;
+                updateStates();
             }
-        }
+
+            @Override
+            public void onStartingToShow() {
+                mPrimaryBouncerAnimating = true;
+                updateStates();
+            }
+
+            @Override
+            public void onFullyHidden() {
+                mPrimaryBouncerAnimating = false;
+                updateStates();
+            }
+
+            @Override
+            public void onExpansionChanged(float expansion) {
+                if (mPrimaryBouncerAnimating) {
+                    mCentralSurfaces.setPrimaryBouncerHiddenFraction(expansion);
+                }
+            }
+
+            @Override
+            public void onVisibilityChanged(boolean isVisible) {
+                mCentralSurfaces.setBouncerShowingOverDream(
+                        isVisible && mDreamOverlayStateController.isOverlayActive());
+
+                if (!isVisible) {
+                    mCentralSurfaces.setPrimaryBouncerHiddenFraction(
+                            KeyguardBouncer.EXPANSION_HIDDEN);
+                }
+
+                /* Register predictive back callback when keyguard becomes visible, and unregister
+                when it's hidden. */
+                if (isVisible) {
+                    registerBackCallback();
+                } else {
+                    unregisterBackCallback();
+                }
+            }
     };
 
     private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
@@ -229,7 +225,7 @@
 
     private View mNotificationContainer;
 
-    @Nullable protected KeyguardBouncer mBouncer;
+    @Nullable protected KeyguardBouncer mPrimaryBouncer;
     protected boolean mRemoteInputActive;
     private boolean mGlobalActionsVisible = false;
     private boolean mLastGlobalActionsVisible = false;
@@ -242,8 +238,8 @@
     protected boolean mFirstUpdate = true;
     protected boolean mLastShowing;
     protected boolean mLastOccluded;
-    private boolean mLastBouncerShowing;
-    private boolean mLastBouncerIsOrWillBeShowing;
+    private boolean mLastPrimaryBouncerShowing;
+    private boolean mLastPrimaryBouncerIsOrWillBeShowing;
     private boolean mLastBouncerDismissible;
     protected boolean mLastRemoteInputActive;
     private boolean mLastDozing;
@@ -253,6 +249,7 @@
     private int mLastBiometricMode;
     private boolean mLastScreenOffAnimationPlaying;
     private float mQsExpansion;
+    final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
     private boolean mIsModernBouncerEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
@@ -270,7 +267,7 @@
     private final LatencyTracker mLatencyTracker;
     private final KeyguardSecurityModel mKeyguardSecurityModel;
     private KeyguardBypassController mBypassController;
-    @Nullable private AlternateAuthInterceptor mAlternateAuthInterceptor;
+    @Nullable private AlternateBouncer mAlternateBouncer;
 
     private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback =
             new KeyguardUpdateMonitorCallback() {
@@ -305,9 +302,9 @@
             LatencyTracker latencyTracker,
             KeyguardSecurityModel keyguardSecurityModel,
             FeatureFlags featureFlags,
-            BouncerCallbackInteractor bouncerCallbackInteractor,
-            BouncerInteractor bouncerInteractor,
-            BouncerView bouncerView) {
+            PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
+            PrimaryBouncerInteractor primaryBouncerInteractor,
+            BouncerView primaryBouncerView) {
         mContext = context;
         mViewMediatorCallback = callback;
         mLockPatternUtils = lockPatternUtils;
@@ -325,9 +322,9 @@
         mShadeController = shadeController;
         mLatencyTracker = latencyTracker;
         mKeyguardSecurityModel = keyguardSecurityModel;
-        mBouncerCallbackInteractor = bouncerCallbackInteractor;
-        mBouncerInteractor = bouncerInteractor;
-        mBouncerViewDelegate = bouncerView.getDelegate();
+        mPrimaryBouncerCallbackInteractor = primaryBouncerCallbackInteractor;
+        mPrimaryBouncerInteractor = primaryBouncerInteractor;
+        mPrimaryBouncerView = primaryBouncerView;
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
@@ -345,9 +342,9 @@
 
         ViewGroup container = mCentralSurfaces.getBouncerContainer();
         if (mIsModernBouncerEnabled) {
-            mBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
+            mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
         } else {
-            mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
+            mPrimaryBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         }
         mNotificationPanelViewController = notificationPanelViewController;
         if (shadeExpansionStateManager != null) {
@@ -366,20 +363,20 @@
      * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else,
      * does nothing.
      */
-    public void removeAlternateAuthInterceptor(@NonNull AlternateAuthInterceptor authInterceptor) {
-        if (Objects.equals(mAlternateAuthInterceptor, authInterceptor)) {
-            mAlternateAuthInterceptor = null;
-            resetAlternateAuth(true);
+    public void removeAlternateAuthInterceptor(@NonNull AlternateBouncer authInterceptor) {
+        if (Objects.equals(mAlternateBouncer, authInterceptor)) {
+            mAlternateBouncer = null;
+            hideAlternateBouncer(true);
         }
     }
 
     /**
      * Sets a new alt auth interceptor.
      */
-    public void setAlternateAuthInterceptor(@NonNull AlternateAuthInterceptor authInterceptor) {
-        if (!Objects.equals(mAlternateAuthInterceptor, authInterceptor)) {
-            mAlternateAuthInterceptor = authInterceptor;
-            resetAlternateAuth(false);
+    public void setAlternateBouncer(@NonNull AlternateBouncer authInterceptor) {
+        if (!Objects.equals(mAlternateBouncer, authInterceptor)) {
+            mAlternateBouncer = authInterceptor;
+            hideAlternateBouncer(false);
         }
     }
 
@@ -463,49 +460,48 @@
         if (mDozing && !mPulsing) {
             return;
         } else if (mNotificationPanelViewController.isUnlockHintRunning()) {
-            if (mBouncer != null) {
-                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+                mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             }
         } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
             // Don't expand to the bouncer. Instead transition back to the lock screen (see
             // CentralSurfaces#showBouncerOrLockScreenIfKeyguard)
             return;
-        } else if (bouncerNeedsScrimming()) {
-            if (mBouncer != null) {
-                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
+        } else if (needsFullscreenBouncer()) {
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
+                mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             }
         } else if (mKeyguardStateController.isShowing()  && !hideBouncerOverDream) {
             if (!isWakeAndUnlocking()
                     && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
-                    && !mCentralSurfaces.isInLaunchTransition()
                     && !isUnlockCollapsing()) {
-                if (mBouncer != null) {
-                    mBouncer.setExpansion(fraction);
+                if (mPrimaryBouncer != null) {
+                    mPrimaryBouncer.setExpansion(fraction);
                 } else {
-                    mBouncerInteractor.setExpansion(fraction);
+                    mPrimaryBouncerInteractor.setPanelExpansion(fraction);
                 }
             }
             if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking
                     && !mKeyguardStateController.canDismissLockScreen()
-                    && !bouncerIsShowing()
+                    && !primaryBouncerIsShowing()
                     && !bouncerIsAnimatingAway()) {
-                if (mBouncer != null) {
-                    mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
+                if (mPrimaryBouncer != null) {
+                    mPrimaryBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
                 } else {
-                    mBouncerInteractor.show(/* isScrimmed= */false);
+                    mPrimaryBouncerInteractor.show(/* isScrimmed= */false);
                 }
             }
-        } else if (!mKeyguardStateController.isShowing()  && isBouncerInTransit()) {
+        } else if (!mKeyguardStateController.isShowing()  && isPrimaryBouncerInTransit()) {
             // Keyguard is not visible anymore, but expansion animation was still running.
             // We need to hide the bouncer, otherwise it will be stuck in transit.
-            if (mBouncer != null) {
-                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+                mPrimaryBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             }
         } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
@@ -549,17 +545,17 @@
         if (needsFullscreenBouncer() && !mDozing) {
             // The keyguard might be showing (already). So we need to hide it.
             mCentralSurfaces.hideKeyguard();
-            if (mBouncer != null) {
-                mBouncer.show(true /* resetSecuritySelection */);
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.show(true /* resetSecuritySelection */);
             } else {
-                mBouncerInteractor.show(true);
+                mPrimaryBouncerInteractor.show(true);
             }
         } else {
             mCentralSurfaces.showKeyguard();
             if (hideBouncerWhenShowing) {
                 hideBouncer(false /* destroyView */);
-                if (mBouncer != null) {
-                    mBouncer.prepare();
+                if (mPrimaryBouncer != null) {
+                    mPrimaryBouncer.prepare();
                 }
             }
         }
@@ -567,23 +563,25 @@
     }
 
     /**
-     * If applicable, shows the alternate authentication bouncer. Else, shows the input
-     * (pin/password/pattern) bouncer.
-     * @param scrimmed true when the input bouncer should show scrimmed, false when the user will be
-     * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
+     *
+     * If possible, shows the alternate bouncer. Else, shows the primary (pin/pattern/password)
+     * bouncer.
+     * @param scrimmed true when the primary bouncer should show scrimmed,
+     *                 false when the user will be dragging it and translation should be deferred
+     *                 {@see KeyguardBouncer#show(boolean, boolean)}
      */
-    public void showGenericBouncer(boolean scrimmed) {
-        if (shouldShowAltAuth()) {
-            updateAlternateAuthShowing(mAlternateAuthInterceptor.showAlternateAuthBouncer());
+    public void showBouncer(boolean scrimmed) {
+        if (canShowAlternateBouncer()) {
+            updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
             return;
         }
 
-        showBouncer(scrimmed);
+        showPrimaryBouncer(scrimmed);
     }
 
-    /** Whether we should show the alternate authentication instead of the traditional bouncer. */
-    public boolean shouldShowAltAuth() {
-        return mAlternateAuthInterceptor != null
+    /** Whether we can show the alternate bouncer instead of the primary bouncer. */
+    public boolean canShowAlternateBouncer() {
+        return mAlternateBouncer != null
                 && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true);
     }
 
@@ -592,10 +590,10 @@
      */
     @VisibleForTesting
     void hideBouncer(boolean destroyView) {
-        if (mBouncer != null) {
-            mBouncer.hide(destroyView);
+        if (mPrimaryBouncer != null) {
+            mPrimaryBouncer.hide(destroyView);
         } else {
-            mBouncerInteractor.hide();
+            mPrimaryBouncerInteractor.hide();
         }
         if (mKeyguardStateController.isShowing()) {
             // If we were showing the bouncer and then aborting, we need to also clear out any
@@ -606,19 +604,19 @@
     }
 
     /**
-     * Shows the keyguard input bouncer - the password challenge on the lock screen
+     * Shows the primary bouncer - the pin/pattern/password challenge on the lock screen.
      *
      * @param scrimmed true when the bouncer should show scrimmed, false when the user will be
      * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
      */
-    public void showBouncer(boolean scrimmed) {
-        resetAlternateAuth(false);
+    public void showPrimaryBouncer(boolean scrimmed) {
+        hideAlternateBouncer(false);
 
         if (mKeyguardStateController.isShowing()  && !isBouncerShowing()) {
-            if (mBouncer != null) {
-                mBouncer.show(false /* resetSecuritySelection */, scrimmed);
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.show(false /* resetSecuritySelection */, scrimmed);
             } else {
-                mBouncerInteractor.show(scrimmed);
+                mPrimaryBouncerInteractor.show(scrimmed);
             }
         }
         updateStates();
@@ -650,42 +648,41 @@
 
                 // If there is an an alternate auth interceptor (like the UDFPS), show that one
                 // instead of the bouncer.
-                if (shouldShowAltAuth()) {
+                if (canShowAlternateBouncer()) {
                     if (!afterKeyguardGone) {
-                        if (mBouncer != null) {
-                            mBouncer.setDismissAction(mAfterKeyguardGoneAction,
+                        if (mPrimaryBouncer != null) {
+                            mPrimaryBouncer.setDismissAction(mAfterKeyguardGoneAction,
                                     mKeyguardGoneCancelAction);
                         } else {
-                            mBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
+                            mPrimaryBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
                                     mKeyguardGoneCancelAction);
                         }
                         mAfterKeyguardGoneAction = null;
                         mKeyguardGoneCancelAction = null;
                     }
 
-                    updateAlternateAuthShowing(
-                            mAlternateAuthInterceptor.showAlternateAuthBouncer());
+                    updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer());
                     return;
                 }
 
                 if (afterKeyguardGone) {
                     // we'll handle the dismiss action after keyguard is gone, so just show the
                     // bouncer
-                    if (mBouncer != null) {
-                        mBouncer.show(false /* resetSecuritySelection */);
+                    if (mPrimaryBouncer != null) {
+                        mPrimaryBouncer.show(false /* resetSecuritySelection */);
                     } else {
-                        mBouncerInteractor.show(/* isScrimmed= */true);
+                        mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
                     }
                 } else {
                     // after authentication success, run dismiss action with the option to defer
                     // hiding the keyguard based on the return value of the OnDismissAction
-                    if (mBouncer != null) {
-                        mBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
+                    if (mPrimaryBouncer != null) {
+                        mPrimaryBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
                                 mKeyguardGoneCancelAction);
                     } else {
-                        mBouncerInteractor.setDismissAction(
+                        mPrimaryBouncerInteractor.setDismissAction(
                                 mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
-                        mBouncerInteractor.show(/* isScrimmed= */true);
+                        mPrimaryBouncerInteractor.show(/* isScrimmed= */true);
                     }
                     // bouncer will handle the dismiss action, so we no longer need to track it here
                     mAfterKeyguardGoneAction = null;
@@ -730,28 +727,28 @@
             } else {
                 showBouncerOrKeyguard(hideBouncerWhenShowing);
             }
-            resetAlternateAuth(false);
+            hideAlternateBouncer(false);
             mKeyguardUpdateManager.sendKeyguardReset();
             updateStates();
         }
     }
 
     @Override
-    public void resetAlternateAuth(boolean forceUpdateScrim) {
-        final boolean updateScrim = (mAlternateAuthInterceptor != null
-                && mAlternateAuthInterceptor.hideAlternateAuthBouncer())
+    public void hideAlternateBouncer(boolean forceUpdateScrim) {
+        final boolean updateScrim = (mAlternateBouncer != null
+                && mAlternateBouncer.hideAlternateBouncer())
                 || forceUpdateScrim;
-        updateAlternateAuthShowing(updateScrim);
+        updateAlternateBouncerShowing(updateScrim);
     }
 
-    private void updateAlternateAuthShowing(boolean updateScrim) {
-        final boolean isShowingAltAuth = isShowingAlternateAuth();
+    private void updateAlternateBouncerShowing(boolean updateScrim) {
+        final boolean isShowingAlternateBouncer = isShowingAlternateBouncer();
         if (mKeyguardMessageAreaController != null) {
-            mKeyguardMessageAreaController.setIsVisible(isShowingAltAuth);
+            mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer);
             mKeyguardMessageAreaController.setMessage("");
         }
-        mBypassController.setAltBouncerShowing(isShowingAltAuth);
-        mKeyguardUpdateManager.setUdfpsBouncerShowing(isShowingAltAuth);
+        mBypassController.setAltBouncerShowing(isShowingAlternateBouncer);
+        mKeyguardUpdateManager.setUdfpsBouncerShowing(isShowingAlternateBouncer);
 
         if (updateScrim) {
             mCentralSurfaces.updateScrimController();
@@ -788,10 +785,10 @@
 
     @Override
     public void onFinishedGoingToSleep() {
-        if (mBouncer != null) {
-            mBouncer.onScreenTurnedOff();
+        if (mPrimaryBouncer != null) {
+            mPrimaryBouncer.onScreenTurnedOff();
         } else {
-            mBouncerInteractor.onScreenTurnedOff();
+            mPrimaryBouncerInteractor.onScreenTurnedOff();
         }
     }
 
@@ -804,7 +801,7 @@
     private void setDozing(boolean dozing) {
         if (mDozing != dozing) {
             mDozing = dozing;
-            if (dozing || mBouncer.needsFullscreenBouncer()
+            if (dozing || needsFullscreenBouncer()
                     || mKeyguardStateController.isOccluded()) {
                 reset(dozing /* hideBouncerWhenShowing */);
             }
@@ -850,21 +847,6 @@
         if (isShowing && isOccluding) {
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
-            if (mCentralSurfaces.isInLaunchTransition()) {
-                final Runnable endRunnable = new Runnable() {
-                    @Override
-                    public void run() {
-                        mNotificationShadeWindowController.setKeyguardOccluded(isOccluded);
-                        reset(true /* hideBouncerWhenShowing */);
-                    }
-                };
-                mCentralSurfaces.fadeKeyguardAfterLaunchTransition(
-                        null /* beforeFading */,
-                        endRunnable,
-                        endRunnable);
-                return;
-            }
-
             if (mCentralSurfaces.isLaunchingActivityOverLockscreen()) {
                 // When isLaunchingActivityOverLockscreen() is true, we know for sure that the post
                 // collapse runnables will be run.
@@ -891,20 +873,20 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (animate && !isOccluded && isShowing && !bouncerIsShowing()) {
+        if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
             mCentralSurfaces.animateKeyguardUnoccluding();
         }
     }
 
     @Override
     public void startPreHideAnimation(Runnable finishRunnable) {
-        if (bouncerIsShowing()) {
-            if (mBouncer != null) {
-                mBouncer.startPreHideAnimation(finishRunnable);
+        if (primaryBouncerIsShowing()) {
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.startPreHideAnimation(finishRunnable);
             } else {
-                mBouncerInteractor.startDisappearAnimation(finishRunnable);
+                mPrimaryBouncerInteractor.startDisappearAnimation(finishRunnable);
             }
-            mCentralSurfaces.onBouncerPreHideAnimation();
+            mNotificationPanelViewController.startBouncerPreHideAnimation();
 
             // We update the state (which will show the keyguard) only if an animation will run on
             // the keyguard. If there is no animation, we wait before updating the state so that we
@@ -936,8 +918,7 @@
         long uptimeMillis = SystemClock.uptimeMillis();
         long delay = Math.max(0, startTime + HIDE_TIMING_CORRECTION_MS - uptimeMillis);
 
-        if (mCentralSurfaces.isInLaunchTransition()
-                || mKeyguardStateController.isFlingingToDismissKeyguard()) {
+        if (mKeyguardStateController.isFlingingToDismissKeyguard()) {
             final boolean wasFlingingToDismissKeyguard =
                     mKeyguardStateController.isFlingingToDismissKeyguard();
             mCentralSurfaces.fadeKeyguardAfterLaunchTransition(new Runnable() {
@@ -1016,13 +997,13 @@
             updateResources();
             return;
         }
-        boolean wasShowing = bouncerIsShowing();
-        boolean wasScrimmed = bouncerIsScrimmed();
+        boolean wasShowing = primaryBouncerIsShowing();
+        boolean wasScrimmed = primaryBouncerIsScrimmed();
 
         hideBouncer(true /* destroyView */);
-        mBouncer.prepare();
+        mPrimaryBouncer.prepare();
 
-        if (wasShowing) showBouncer(wasScrimmed);
+        if (wasShowing) showPrimaryBouncer(wasScrimmed);
     }
 
     public void onKeyguardFadedAway() {
@@ -1066,8 +1047,8 @@
      * WARNING: This method might cause Binder calls.
      */
     public boolean isSecure() {
-        if (mBouncer != null) {
-            return mBouncer.isSecure();
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.isSecure();
         }
 
         return mKeyguardSecurityModel.getSecurityMode(
@@ -1081,7 +1062,7 @@
      * @return whether a back press can be handled right now.
      */
     public boolean canHandleBackPressed() {
-        return mBouncer.isShowing();
+        return primaryBouncerIsShowing();
     }
 
     /**
@@ -1094,7 +1075,7 @@
 
         mCentralSurfaces.endAffordanceLaunch();
         // The second condition is for SIM card locked bouncer
-        if (bouncerIsScrimmed() && needsFullscreenBouncer()) {
+        if (primaryBouncerIsScrimmed() && !needsFullscreenBouncer()) {
             hideBouncer(false);
             updateStates();
         } else {
@@ -1115,27 +1096,27 @@
 
     @Override
     public boolean isBouncerShowing() {
-        return bouncerIsShowing() || isShowingAlternateAuth();
+        return primaryBouncerIsShowing() || isShowingAlternateBouncer();
     }
 
     @Override
-    public boolean bouncerIsOrWillBeShowing() {
-        return isBouncerShowing() || isBouncerInTransit();
+    public boolean primaryBouncerIsOrWillBeShowing() {
+        return isBouncerShowing() || isPrimaryBouncerInTransit();
     }
 
     public boolean isFullscreenBouncer() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.isFullScreenBouncer();
+        if (mPrimaryBouncerView.getDelegate() != null) {
+            return mPrimaryBouncerView.getDelegate().isFullScreenBouncer();
         }
-        return mBouncer != null && mBouncer.isFullscreenBouncer();
+        return mPrimaryBouncer != null && mPrimaryBouncer.isFullscreenBouncer();
     }
 
     /**
      * Clear out any potential actions that were saved to run when the device is unlocked
      */
     public void cancelPostAuthActions() {
-        if (bouncerIsOrWillBeShowing()) {
-            return; // allow bouncer to trigger saved actions
+        if (primaryBouncerIsOrWillBeShowing()) {
+            return; // allow the primary bouncer to trigger saved actions
         }
         mAfterKeyguardGoneAction = null;
         mDismissActionWillAnimateOnKeyguard = false;
@@ -1174,25 +1155,25 @@
         }
         boolean showing = mKeyguardStateController.isShowing();
         boolean occluded = mKeyguardStateController.isOccluded();
-        boolean bouncerShowing = bouncerIsShowing();
-        boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing();
-        boolean bouncerDismissible = !isFullscreenBouncer();
+        boolean primaryBouncerShowing = primaryBouncerIsShowing();
+        boolean primaryBouncerIsOrWillBeShowing = primaryBouncerIsOrWillBeShowing();
+        boolean primaryBouncerDismissible = !isFullscreenBouncer();
         boolean remoteInputActive = mRemoteInputActive;
 
-        if ((bouncerDismissible || !showing || remoteInputActive) !=
-                (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
+        if ((primaryBouncerDismissible || !showing || remoteInputActive)
+                != (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
                 || mFirstUpdate) {
-            if (bouncerDismissible || !showing || remoteInputActive) {
-                if (mBouncer != null) {
-                    mBouncer.setBackButtonEnabled(true);
+            if (primaryBouncerDismissible || !showing || remoteInputActive) {
+                if (mPrimaryBouncer != null) {
+                    mPrimaryBouncer.setBackButtonEnabled(true);
                 } else {
-                    mBouncerInteractor.setBackButtonEnabled(true);
+                    mPrimaryBouncerInteractor.setBackButtonEnabled(true);
                 }
             } else {
-                if (mBouncer != null) {
-                    mBouncer.setBackButtonEnabled(false);
+                if (mPrimaryBouncer != null) {
+                    mPrimaryBouncer.setBackButtonEnabled(false);
                 } else {
-                    mBouncerInteractor.setBackButtonEnabled(false);
+                    mPrimaryBouncerInteractor.setBackButtonEnabled(false);
                 }
             }
         }
@@ -1203,23 +1184,26 @@
             updateNavigationBarVisibility(navBarVisible);
         }
 
-        if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
-            mNotificationShadeWindowController.setBouncerShowing(bouncerShowing);
-            mCentralSurfaces.setBouncerShowing(bouncerShowing);
+        boolean isPrimaryBouncerShowingChanged =
+            primaryBouncerShowing != mLastPrimaryBouncerShowing;
+        mLastPrimaryBouncerShowing = primaryBouncerShowing;
+
+        if (isPrimaryBouncerShowingChanged || mFirstUpdate) {
+            mNotificationShadeWindowController.setBouncerShowing(primaryBouncerShowing);
+            mCentralSurfaces.setBouncerShowing(primaryBouncerShowing);
         }
-        if (bouncerIsOrWillBeShowing != mLastBouncerIsOrWillBeShowing || mFirstUpdate
-                || bouncerShowing != mLastBouncerShowing) {
-            mKeyguardUpdateManager.sendKeyguardBouncerChanged(bouncerIsOrWillBeShowing,
-                    bouncerShowing);
+        if (primaryBouncerIsOrWillBeShowing != mLastPrimaryBouncerIsOrWillBeShowing || mFirstUpdate
+                || isPrimaryBouncerShowingChanged) {
+            mKeyguardUpdateManager.sendPrimaryBouncerChanged(primaryBouncerIsOrWillBeShowing,
+                    primaryBouncerShowing);
         }
 
         mFirstUpdate = false;
         mLastShowing = showing;
         mLastGlobalActionsVisible = mGlobalActionsVisible;
         mLastOccluded = occluded;
-        mLastBouncerShowing = bouncerShowing;
-        mLastBouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing;
-        mLastBouncerDismissible = bouncerDismissible;
+        mLastPrimaryBouncerIsOrWillBeShowing = primaryBouncerIsOrWillBeShowing;
+        mLastBouncerDismissible = primaryBouncerDismissible;
         mLastRemoteInputActive = remoteInputActive;
         mLastDozing = mDozing;
         mLastPulsing = mPulsing;
@@ -1263,7 +1247,7 @@
                 || mPulsing && !mIsDocked)
                 && mGesturalNav;
         return (!keyguardVisible && !hideWhileDozing && !mScreenOffAnimationPlaying
-                || bouncerIsShowing()
+                || primaryBouncerIsShowing()
                 || mRemoteInputActive
                 || keyguardWithGestureNav
                 || mGlobalActionsVisible);
@@ -1279,32 +1263,32 @@
                 && !mLastScreenOffAnimationPlaying || mLastPulsing && !mLastIsDocked)
                 && mLastGesturalNav;
         return (!keyguardShowing && !hideWhileDozing && !mLastScreenOffAnimationPlaying
-                || mLastBouncerShowing || mLastRemoteInputActive || keyguardWithGestureNav
+                || mLastPrimaryBouncerShowing || mLastRemoteInputActive || keyguardWithGestureNav
                 || mLastGlobalActionsVisible);
     }
 
     public boolean shouldDismissOnMenuPressed() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.shouldDismissOnMenuPressed();
+        if (mPrimaryBouncerView.getDelegate() != null) {
+            return mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
         }
-        return mBouncer != null && mBouncer.shouldDismissOnMenuPressed();
+        return mPrimaryBouncer != null && mPrimaryBouncer.shouldDismissOnMenuPressed();
     }
 
     public boolean interceptMediaKey(KeyEvent event) {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.interceptMediaKey(event);
+        if (mPrimaryBouncerView.getDelegate() != null) {
+            return mPrimaryBouncerView.getDelegate().interceptMediaKey(event);
         }
-        return mBouncer != null && mBouncer.interceptMediaKey(event);
+        return mPrimaryBouncer != null && mPrimaryBouncer.interceptMediaKey(event);
     }
 
     /**
      * @return true if the pre IME back event should be handled
      */
     public boolean dispatchBackKeyEventPreIme() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.dispatchBackKeyEventPreIme();
+        if (mPrimaryBouncerView.getDelegate() != null) {
+            return mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme();
         }
-        return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme();
+        return mPrimaryBouncer != null && mPrimaryBouncer.dispatchBackKeyEventPreIme();
     }
 
     public void readyForKeyguardDone() {
@@ -1313,7 +1297,7 @@
 
     @Override
     public boolean shouldDisableWindowAnimationsForUnlock() {
-        return mCentralSurfaces.isInLaunchTransition();
+        return false;
     }
 
     @Override
@@ -1350,29 +1334,29 @@
      * fingerprint.
      */
     public void notifyKeyguardAuthenticated(boolean strongAuth) {
-        if (mBouncer != null) {
-            mBouncer.notifyKeyguardAuthenticated(strongAuth);
+        if (mPrimaryBouncer != null) {
+            mPrimaryBouncer.notifyKeyguardAuthenticated(strongAuth);
         } else {
-            mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
+            mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
         }
 
-        if (mAlternateAuthInterceptor != null && isShowingAlternateAuthOrAnimating()) {
-            resetAlternateAuth(false);
+        if (mAlternateBouncer != null && isShowingAlternateBouncer()) {
+            hideAlternateBouncer(false);
             executeAfterKeyguardGoneAction();
         }
     }
 
     /** Display security message to relevant KeyguardMessageArea. */
     public void setKeyguardMessage(String message, ColorStateList colorState) {
-        if (isShowingAlternateAuth()) {
+        if (isShowingAlternateBouncer()) {
             if (mKeyguardMessageAreaController != null) {
                 mKeyguardMessageAreaController.setMessage(message);
             }
         } else {
-            if (mBouncer != null) {
-                mBouncer.showMessage(message, colorState);
+            if (mPrimaryBouncer != null) {
+                mPrimaryBouncer.showMessage(message, colorState);
             } else {
-                mBouncerInteractor.showMessage(message, colorState);
+                mPrimaryBouncerInteractor.showMessage(message, colorState);
             }
         }
     }
@@ -1411,12 +1395,15 @@
         }
     }
 
-    public boolean bouncerNeedsScrimming() {
+    /**
+     * Whether the primary bouncer requires scrimming.
+     */
+    public boolean primaryBouncerNeedsScrimming() {
         // When a dream overlay is active, scrimming will cause any expansion to immediately expand.
         return (mKeyguardStateController.isOccluded()
                 && !mDreamOverlayStateController.isOverlayActive())
-                || bouncerWillDismissWithAction()
-                || (bouncerIsShowing() && bouncerIsScrimmed())
+                || primaryBouncerWillDismissWithAction()
+                || (primaryBouncerIsShowing() && primaryBouncerIsScrimmed())
                 || isFullscreenBouncer();
     }
 
@@ -1426,10 +1413,10 @@
      * configuration.
      */
     public void updateResources() {
-        if (mBouncer != null) {
-            mBouncer.updateResources();
+        if (mPrimaryBouncer != null) {
+            mPrimaryBouncer.updateResources();
         } else {
-            mBouncerInteractor.updateResources();
+            mPrimaryBouncerInteractor.updateResources();
         }
     }
 
@@ -1441,15 +1428,20 @@
         pw.println("  mAfterKeyguardGoneRunnables: " + mAfterKeyguardGoneRunnables);
         pw.println("  mPendingWakeupAction: " + mPendingWakeupAction);
         pw.println("  isBouncerShowing(): " + isBouncerShowing());
-        pw.println("  bouncerIsOrWillBeShowing(): " + bouncerIsOrWillBeShowing());
-
-        if (mBouncer != null) {
-            mBouncer.dump(pw);
+        pw.println("  bouncerIsOrWillBeShowing(): " + primaryBouncerIsOrWillBeShowing());
+        pw.println("  Registered KeyguardViewManagerCallbacks:");
+        for (KeyguardViewManagerCallback callback : mCallbacks) {
+            pw.println("      " + callback);
         }
 
-        if (mAlternateAuthInterceptor != null) {
-            pw.println("AltAuthInterceptor: ");
-            mAlternateAuthInterceptor.dump(pw);
+        if (mPrimaryBouncer != null) {
+            pw.println("PrimaryBouncer:");
+            mPrimaryBouncer.dump(pw);
+        }
+
+        if (mAlternateBouncer != null) {
+            pw.println("AlternateBouncer:");
+            mAlternateBouncer.dump(pw);
         }
     }
 
@@ -1466,6 +1458,20 @@
     }
 
     /**
+     * Add a callback to listen for changes
+     */
+    public void addCallback(KeyguardViewManagerCallback callback) {
+        mCallbacks.add(callback);
+    }
+
+    /**
+     * Removes callback to stop receiving updates
+     */
+    public void removeCallback(KeyguardViewManagerCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    /**
      * Whether qs is currently expanded.
      */
     public float getQsExpansion() {
@@ -1477,44 +1483,35 @@
      */
     public void setQsExpansion(float qsExpansion) {
         mQsExpansion = qsExpansion;
-        if (mAlternateAuthInterceptor != null) {
-            mAlternateAuthInterceptor.setQsExpansion(qsExpansion);
+        for (KeyguardViewManagerCallback callback : mCallbacks) {
+            callback.onQSExpansionChanged(mQsExpansion);
         }
     }
 
     @Nullable
-    public KeyguardBouncer getBouncer() {
-        return mBouncer;
+    public KeyguardBouncer getPrimaryBouncer() {
+        return mPrimaryBouncer;
     }
 
-    public boolean isShowingAlternateAuth() {
-        return mAlternateAuthInterceptor != null
-                && mAlternateAuthInterceptor.isShowingAlternateAuthBouncer();
-    }
-
-    public boolean isShowingAlternateAuthOrAnimating() {
-        return mAlternateAuthInterceptor != null
-                && (mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()
-                || mAlternateAuthInterceptor.isAnimating());
+    public boolean isShowingAlternateBouncer() {
+        return mAlternateBouncer != null && mAlternateBouncer.isShowingAlternateBouncer();
     }
 
     /**
-     * Forward touches to any alternate authentication affordances.
+     * Forward touches to callbacks.
      */
-    public boolean onTouch(MotionEvent event) {
-        if (mAlternateAuthInterceptor == null) {
-            return false;
+    public void onTouch(MotionEvent event) {
+        for (KeyguardViewManagerCallback callback: mCallbacks) {
+            callback.onTouch(event);
         }
-
-        return mAlternateAuthInterceptor.onTouch(event);
     }
 
     /** Update keyguard position based on a tapped X coordinate. */
     public void updateKeyguardPosition(float x) {
-        if (mBouncer != null) {
-            mBouncer.updateKeyguardPosition(x);
+        if (mPrimaryBouncer != null) {
+            mPrimaryBouncer.updateKeyguardPosition(x);
         } else {
-            mBouncerInteractor.setKeyguardPosition(x);
+            mPrimaryBouncerInteractor.setKeyguardPosition(x);
         }
     }
 
@@ -1546,41 +1543,41 @@
      */
     public void requestFp(boolean request, int udfpsColor) {
         mKeyguardUpdateManager.requestFingerprintAuthOnOccludingApp(request);
-        if (mAlternateAuthInterceptor != null) {
-            mAlternateAuthInterceptor.requestUdfps(request, udfpsColor);
+        if (mAlternateBouncer != null) {
+            mAlternateBouncer.requestUdfps(request, udfpsColor);
         }
     }
 
     /**
      * Returns if bouncer expansion is between 0 and 1 non-inclusive.
      */
-    public boolean isBouncerInTransit() {
-        if (mBouncer != null) {
-            return mBouncer.inTransit();
+    public boolean isPrimaryBouncerInTransit() {
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.inTransit();
         } else {
-            return mBouncerInteractor.isInTransit();
+            return mPrimaryBouncerInteractor.isInTransit();
         }
     }
 
     /**
      * Returns if bouncer is showing
      */
-    public boolean bouncerIsShowing() {
-        if (mBouncer != null) {
-            return mBouncer.isShowing();
+    public boolean primaryBouncerIsShowing() {
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.isShowing();
         } else {
-            return mBouncerInteractor.isFullyShowing();
+            return mPrimaryBouncerInteractor.isFullyShowing();
         }
     }
 
     /**
      * Returns if bouncer is scrimmed
      */
-    public boolean bouncerIsScrimmed() {
-        if (mBouncer != null) {
-            return mBouncer.isScrimmed();
+    public boolean primaryBouncerIsScrimmed() {
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.isScrimmed();
         } else {
-            return mBouncerInteractor.isScrimmed();
+            return mPrimaryBouncerInteractor.isScrimmed();
         }
     }
 
@@ -1588,10 +1585,10 @@
      * Returns if bouncer is animating away
      */
     public boolean bouncerIsAnimatingAway() {
-        if (mBouncer != null) {
-            return mBouncer.isAnimatingAway();
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.isAnimatingAway();
         } else {
-            return mBouncerInteractor.isAnimatingAway();
+            return mPrimaryBouncerInteractor.isAnimatingAway();
         }
 
     }
@@ -1599,11 +1596,11 @@
     /**
      * Returns if bouncer will dismiss with action
      */
-    public boolean bouncerWillDismissWithAction() {
-        if (mBouncer != null) {
-            return mBouncer.willDismissWithAction();
+    public boolean primaryBouncerWillDismissWithAction() {
+        if (mPrimaryBouncer != null) {
+            return mPrimaryBouncer.willDismissWithAction();
         } else {
-            return mBouncerInteractor.willDismissWithAction();
+            return mPrimaryBouncerInteractor.willDismissWithAction();
         }
     }
 
@@ -1618,58 +1615,26 @@
     }
 
     /**
-     * Delegate used to send show/reset events to an alternate authentication method instead of the
-     * regular pin/pattern/password bouncer.
+     * Delegate used to send show and hide events to an alternate authentication method instead of
+     * the regular pin/pattern/password bouncer.
      */
-    public interface AlternateAuthInterceptor {
+    public interface AlternateBouncer {
         /**
          * Show alternate authentication bouncer.
          * @return whether alternate auth method was newly shown
          */
-        boolean showAlternateAuthBouncer();
+        boolean showAlternateBouncer();
 
         /**
          * Hide alternate authentication bouncer
          * @return whether the alternate auth method was newly hidden
          */
-        boolean hideAlternateAuthBouncer();
+        boolean hideAlternateBouncer();
 
         /**
          * @return true if the alternate auth bouncer is showing
          */
-        boolean isShowingAlternateAuthBouncer();
-
-        /**
-         * print information for the alternate auth interceptor registered
-         */
-        void dump(PrintWriter pw);
-
-        /**
-         * @return true if the new auth method bouncer is currently animating in or out.
-         */
-        boolean isAnimating();
-
-        /**
-         * How much QS is fully expanded where 0f is not showing and 1f is fully expanded.
-         */
-        void setQsExpansion(float qsExpansion);
-
-        /**
-         * Forward potential touches to authentication interceptor
-         * @return true if event was handled
-         */
-        boolean onTouch(MotionEvent event);
-
-        /**
-         * Update pin/pattern/password bouncer expansion amount where 0 is visible and 1 is fully
-         * hidden
-         */
-        void setBouncerExpansionChanged(float expansion);
-
-        /**
-         *  called when the bouncer view visibility has changed.
-         */
-        void onBouncerVisibilityChanged();
+        boolean isShowingAlternateBouncer();
 
         /**
          * Use when an app occluding the keyguard would like to give the user ability to
@@ -1680,5 +1645,25 @@
          */
         void requestUdfps(boolean requestUdfps, int color);
 
+        /**
+         * print information for the alternate bouncer registered
+         */
+        void dump(PrintWriter pw);
+    }
+
+    /**
+     * Callback for KeyguardViewManager state changes.
+     */
+    public interface KeyguardViewManagerCallback {
+        /**
+         * Set the amount qs is expanded. For example, swipe down from the top of the
+         * lock screen to start the full QS expansion.
+         */
+        default void onQSExpansionChanged(float qsExpansion) { }
+
+        /**
+         * Forward touch events to callbacks
+         */
+        default void onTouch(MotionEvent event) { }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index ee948c0..b1642d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -31,7 +31,7 @@
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
         centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            centralSurfaces.collapsePanelWithDuration(
+            centralSurfaces.notificationPanelViewController.collapseWithDuration(
                 ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 5cd2ba1..b6ae4a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -23,7 +23,6 @@
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.TaskStackBuilder;
 import android.content.Context;
@@ -60,9 +59,8 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
+import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -126,7 +124,6 @@
             Context context,
             Handler mainThreadHandler,
             Executor uiBgExecutor,
-            NotifPipeline notifPipeline,
             NotificationVisibilityProvider visibilityProvider,
             HeadsUpManagerPhone headsUpManager,
             ActivityStarter activityStarter,
@@ -151,7 +148,8 @@
             NotificationPresenter presenter,
             NotificationPanelViewController panel,
             ActivityLaunchAnimator activityLaunchAnimator,
-            NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) {
+            NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
+            LaunchFullScreenIntentProvider launchFullScreenIntentProvider) {
         mContext = context;
         mMainThreadHandler = mainThreadHandler;
         mUiBgExecutor = uiBgExecutor;
@@ -182,12 +180,7 @@
         mActivityLaunchAnimator = activityLaunchAnimator;
         mNotificationAnimationProvider = notificationAnimationProvider;
 
-        notifPipeline.addCollectionListener(new NotifCollectionListener() {
-            @Override
-            public void onEntryAdded(NotificationEntry entry) {
-                handleFullScreenIntent(entry);
-            }
-        });
+        launchFullScreenIntentProvider.registerListener(entry -> launchFullScreenIntent(entry));
     }
 
     /**
@@ -549,38 +542,36 @@
     }
 
     @VisibleForTesting
-    void handleFullScreenIntent(NotificationEntry entry) {
-        if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
-            if (shouldSuppressFullScreenIntent(entry)) {
-                mLogger.logFullScreenIntentSuppressedByDnD(entry);
-            } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
-                mLogger.logFullScreenIntentNotImportantEnough(entry);
-            } else {
-                // Stop screensaver if the notification has a fullscreen intent.
-                // (like an incoming phone call)
-                mUiBgExecutor.execute(() -> {
-                    try {
-                        mDreamManager.awaken();
-                    } catch (RemoteException e) {
-                        e.printStackTrace();
-                    }
-                });
+    void launchFullScreenIntent(NotificationEntry entry) {
+        // Skip if device is in VR mode.
+        if (mPresenter.isDeviceInVrMode()) {
+            mLogger.logFullScreenIntentSuppressedByVR(entry);
+            return;
+        }
 
-                // not immersive & a fullscreen alert should be shown
-                final PendingIntent fullscreenIntent =
-                        entry.getSbn().getNotification().fullScreenIntent;
-                mLogger.logSendingFullScreenIntent(entry, fullscreenIntent);
-                try {
-                    EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
-                            entry.getKey());
-                    mCentralSurfaces.wakeUpForFullScreenIntent();
-                    fullscreenIntent.send();
-                    entry.notifyFullScreenIntentLaunched();
-                    mMetricsLogger.count("note_fullscreen", 1);
-                } catch (PendingIntent.CanceledException e) {
-                    // ignore
-                }
+        // Stop screensaver if the notification has a fullscreen intent.
+        // (like an incoming phone call)
+        mUiBgExecutor.execute(() -> {
+            try {
+                mDreamManager.awaken();
+            } catch (RemoteException e) {
+                e.printStackTrace();
             }
+        });
+
+        // not immersive & a fullscreen alert should be shown
+        final PendingIntent fullscreenIntent =
+                entry.getSbn().getNotification().fullScreenIntent;
+        mLogger.logSendingFullScreenIntent(entry, fullscreenIntent);
+        try {
+            EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
+                    entry.getKey());
+            mCentralSurfaces.wakeUpForFullScreenIntent();
+            fullscreenIntent.send();
+            entry.notifyFullScreenIntentLaunched();
+            mMetricsLogger.count("note_fullscreen", 1);
+        } catch (PendingIntent.CanceledException e) {
+            // ignore
         }
     }
 
@@ -607,12 +598,4 @@
             mMainThreadHandler.post(mShadeController::collapsePanel);
         }
     }
-
-    private boolean shouldSuppressFullScreenIntent(NotificationEntry entry) {
-        if (mPresenter.isDeviceInVrMode()) {
-            return true;
-        }
-
-        return entry.shouldSuppressFullScreenIntent();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index b9a1413..1f0b96a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
@@ -96,19 +96,11 @@
         })
     }
 
-    fun logFullScreenIntentSuppressedByDnD(entry: NotificationEntry) {
+    fun logFullScreenIntentSuppressedByVR(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "No Fullscreen intent: suppressed by DND: $str1"
-        })
-    }
-
-    fun logFullScreenIntentNotImportantEnough(entry: NotificationEntry) {
-        buffer.log(TAG, DEBUG, {
-            str1 = entry.logKey
-        }, {
-            "No Fullscreen intent: not important enough: $str1"
+            "No Fullscreen intent: suppressed by VR mode: $str1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index 70af77e..8a49850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -133,7 +133,7 @@
         if (!row.isPinned()) {
             mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
         }
-        mStatusBarKeyguardViewManager.showGenericBouncer(true /* scrimmed */);
+        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
         mPendingRemoteInputView = clicked;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index 492734e..de7bf3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -212,7 +212,7 @@
     private void updateWifiIconWithState(WifiIconState state) {
         if (DEBUG) Log.d(TAG, "WifiIconState: " + state == null ? "" : state.toString());
         if (state.visible && state.resId > 0) {
-            mIconController.setSignalIcon(mSlotWifi, state);
+            mIconController.setWifiIcon(mSlotWifi, state);
             mIconController.setIconVisibility(mSlotWifi, true);
         } else {
             mIconController.setIconVisibility(mSlotWifi, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index d9c0293..2a039da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -34,6 +34,7 @@
 import com.android.systemui.R;
 import com.android.systemui.ScreenDecorations;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -68,12 +69,15 @@
     private int mDisplayCutoutTouchableRegionSize;
     private int mStatusBarHeight;
 
+    private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener;
+
     @Inject
     public StatusBarTouchableRegionManager(
             Context context,
             NotificationShadeWindowController notificationShadeWindowController,
             ConfigurationController configurationController,
             HeadsUpManagerPhone headsUpManager,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
     ) {
         mContext = context;
@@ -101,17 +105,7 @@
                         updateTouchableRegion();
                     }
                 });
-        mHeadsUpManager.addHeadsUpPhoneListener(
-                new HeadsUpManagerPhone.OnHeadsUpPhoneListenerChange() {
-                    @Override
-                    public void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
-                        if (!headsUpGoingAway) {
-                            updateTouchableRegionAfterLayout();
-                        } else {
-                            updateTouchableRegion();
-                        }
-                    }
-                });
+        mHeadsUpManager.addHeadsUpPhoneListener(this::onHeadsUpGoingAwayStateChanged);
 
         mNotificationShadeWindowController = notificationShadeWindowController;
         mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> {
@@ -119,6 +113,9 @@
         });
 
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
+        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+
+        mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
     }
 
     protected void setup(
@@ -136,17 +133,11 @@
         pw.println(mTouchableRegion);
     }
 
-    /**
-     * Notify that the status bar panel gets expanded or collapsed.
-     *
-     * @param isExpanded True to notify expanded, false to notify collapsed.
-     * TODO(b/237811427) replace with a listener
-     */
-    public void setPanelExpanded(boolean isExpanded) {
+    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
         if (isExpanded != mIsStatusBarExpanded) {
             mIsStatusBarExpanded = isExpanded;
             if (isExpanded) {
-                // make sure our state is sane
+                // make sure our state is sensible
                 mForceCollapsedUntilLayout = false;
             }
             updateTouchableRegion();
@@ -260,18 +251,22 @@
                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
-    private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener =
-            new OnComputeInternalInsetsListener() {
-        @Override
-        public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
-            if (shouldMakeEntireScreenTouchable()) {
-                return;
-            }
-
-            // Update touch insets to include any area needed for touching features that live in
-            // the status bar (ie: heads up notifications)
-            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-            info.touchableRegion.set(calculateTouchableRegion());
+    private void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) {
+        if (!headsUpGoingAway) {
+            updateTouchableRegionAfterLayout();
+        } else {
+            updateTouchableRegion();
         }
-    };
+    }
+
+    private void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
+        if (shouldMakeEntireScreenTouchable()) {
+            return;
+        }
+
+        // Update touch insets to include any area needed for touching features that live in
+        // the status bar (ie: heads up notifications)
+        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        info.touchableRegion.set(calculateTouchableRegion());
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
index a0415f2..08599c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -16,14 +16,12 @@
 
 package com.android.systemui.statusbar.phone
 
-import android.view.InsetsVisibilities
+import android.view.WindowInsets.Type.InsetsType
 import android.view.WindowInsetsController.Appearance
 import android.view.WindowInsetsController.Behavior
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
@@ -42,7 +40,6 @@
 @Inject
 internal constructor(
     private val centralSurfaces: CentralSurfaces,
-    private val featureFlags: FeatureFlags,
     private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val lightBarController: LightBarController,
@@ -69,21 +66,21 @@
                 params.appearanceRegionsArray,
                 params.navbarColorManagedByIme,
                 params.behavior,
-                params.requestedVisibilities,
+                params.requestedVisibleTypes,
                 params.packageName,
                 params.letterboxesArray)
         }
     }
 
     fun onSystemBarAttributesChanged(
-        displayId: Int,
-        @Appearance originalAppearance: Int,
-        originalAppearanceRegions: Array<AppearanceRegion>,
-        navbarColorManagedByIme: Boolean,
-        @Behavior behavior: Int,
-        requestedVisibilities: InsetsVisibilities,
-        packageName: String,
-        letterboxDetails: Array<LetterboxDetails>
+            displayId: Int,
+            @Appearance originalAppearance: Int,
+            originalAppearanceRegions: Array<AppearanceRegion>,
+            navbarColorManagedByIme: Boolean,
+            @Behavior behavior: Int,
+            @InsetsType requestedVisibleTypes: Int,
+            packageName: String,
+            letterboxDetails: Array<LetterboxDetails>
     ) {
         lastSystemBarAttributesParams =
             SystemBarAttributesParams(
@@ -92,7 +89,7 @@
                 originalAppearanceRegions.toList(),
                 navbarColorManagedByIme,
                 behavior,
-                requestedVisibilities,
+                requestedVisibleTypes,
                 packageName,
                 letterboxDetails.toList())
 
@@ -107,7 +104,7 @@
 
         centralSurfaces.updateBubblesVisibility()
         statusBarStateController.setSystemBarAttributes(
-            appearance, behavior, requestedVisibilities, packageName)
+            appearance, behavior, requestedVisibleTypes, packageName)
     }
 
     private fun modifyAppearanceIfNeeded(
@@ -127,15 +124,11 @@
         }
 
     private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) =
-        isLetterboxAppearanceFlagEnabled() && letterboxDetails.isNotEmpty()
-
-    private fun isLetterboxAppearanceFlagEnabled() =
-        featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)
+        letterboxDetails.isNotEmpty()
 
     private fun dump(printWriter: PrintWriter, strings: Array<String>) {
         printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams")
         printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance")
-        printWriter.println("letterbox appearance flag: ${isLetterboxAppearanceFlagEnabled()}")
     }
 }
 
@@ -144,14 +137,14 @@
  * [SystemBarAttributesListener.onSystemBarAttributesChanged].
  */
 private data class SystemBarAttributesParams(
-    val displayId: Int,
-    @Appearance val appearance: Int,
-    val appearanceRegions: List<AppearanceRegion>,
-    val navbarColorManagedByIme: Boolean,
-    @Behavior val behavior: Int,
-    val requestedVisibilities: InsetsVisibilities,
-    val packageName: String,
-    val letterboxes: List<LetterboxDetails>,
+        val displayId: Int,
+        @Appearance val appearance: Int,
+        val appearanceRegions: List<AppearanceRegion>,
+        val navbarColorManagedByIme: Boolean,
+        @Behavior val behavior: Int,
+        @InsetsType val requestedVisibleTypes: Int,
+        val packageName: String,
+        val letterboxes: List<LetterboxDetails>,
 ) {
     val letterboxesArray = letterboxes.toTypedArray()
     val appearanceRegionsArray = appearanceRegions.toTypedArray()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManager.java
index e7d9221..678c2d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManager.java
@@ -87,7 +87,7 @@
 
     private void updateDialogListeners() {
         if (shouldHideAffordance()) {
-            mKeyguardViewController.resetAlternateAuth(true);
+            mKeyguardViewController.hideAlternateBouncer(true);
         }
 
         for (Listener listener : mListeners) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 0369845..344d233 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -28,13 +28,13 @@
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.biometrics.AuthRippleView;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.privacy.OngoingPrivacyChip;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.CombinedShadeHeadersConstraintManager;
 import com.android.systemui.shade.CombinedShadeHeadersConstraintManagerImpl;
 import com.android.systemui.shade.NotificationPanelView;
@@ -220,20 +220,22 @@
     @Named(LARGE_SCREEN_BATTERY_CONTROLLER)
     static BatteryMeterViewController getBatteryMeterViewController(
             @Named(SPLIT_SHADE_BATTERY_VIEW) BatteryMeterView batteryMeterView,
+            UserTracker userTracker,
             ConfigurationController configurationController,
             TunerService tunerService,
-            BroadcastDispatcher broadcastDispatcher,
             @Main Handler mainHandler,
             ContentResolver contentResolver,
+            FeatureFlags featureFlags,
             BatteryController batteryController
     ) {
         return new BatteryMeterViewController(
                 batteryMeterView,
+                userTracker,
                 configurationController,
                 tunerService,
-                broadcastDispatcher,
                 mainHandler,
                 contentResolver,
+                featureFlags,
                 batteryController);
 
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 28ed080..d64bc58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.phone.fragment
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.CollapsedSbFragmentLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index 41f1f95..efec270 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -29,8 +29,6 @@
 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherControllerImpl;
 import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 
@@ -39,7 +37,6 @@
 
 import javax.inject.Named;
 
-import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.Multibinds;
@@ -126,12 +123,6 @@
     }
 
     /** */
-    @Binds
-    @StatusBarFragmentScope
-    StatusBarUserSwitcherController bindStatusBarUserSwitcherController(
-            StatusBarUserSwitcherControllerImpl controller);
-
-    /** */
     @Provides
     @StatusBarFragmentScope
     static PhoneStatusBarViewController providePhoneStatusBarViewController(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt
deleted file mode 100644
index f6b8cb0..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserInfoTracker.kt
+++ /dev/null
@@ -1,134 +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.statusbar.phone.userswitcher
-
-import android.graphics.drawable.Drawable
-import android.os.UserManager
-import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.policy.CallbackController
-import com.android.systemui.statusbar.policy.UserInfoController
-import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-/**
- * Since every user switcher chip will user the exact same information and logic on whether or not
- * to show, and what data to show, it makes sense to create a single tracker here
- */
-@SysUISingleton
-class StatusBarUserInfoTracker @Inject constructor(
-    private val userInfoController: UserInfoController,
-    private val userManager: UserManager,
-    private val dumpManager: DumpManager,
-    @Main private val mainExecutor: Executor,
-    @Background private val backgroundExecutor: Executor
-) : CallbackController<CurrentUserChipInfoUpdatedListener>, Dumpable {
-    var currentUserName: String? = null
-        private set
-    var currentUserAvatar: Drawable? = null
-        private set
-    var userSwitcherEnabled = false
-        private set
-    private var listening = false
-
-    private val listeners = mutableListOf<CurrentUserChipInfoUpdatedListener>()
-
-    private val userInfoChangedListener = OnUserInfoChangedListener { name, picture, _ ->
-        currentUserAvatar = picture
-        currentUserName = name
-        notifyListenersUserInfoChanged()
-    }
-
-    init {
-        dumpManager.registerDumpable(TAG, this)
-    }
-
-    override fun addCallback(listener: CurrentUserChipInfoUpdatedListener) {
-        if (listeners.isEmpty()) {
-            startListening()
-        }
-
-        if (!listeners.contains(listener)) {
-            listeners.add(listener)
-        }
-    }
-
-    override fun removeCallback(listener: CurrentUserChipInfoUpdatedListener) {
-        listeners.remove(listener)
-
-        if (listeners.isEmpty()) {
-            stopListening()
-        }
-    }
-
-    private fun notifyListenersUserInfoChanged() {
-        listeners.forEach {
-            it.onCurrentUserChipInfoUpdated()
-        }
-    }
-
-    private fun notifyListenersSettingChanged() {
-        listeners.forEach {
-            it.onStatusBarUserSwitcherSettingChanged(userSwitcherEnabled)
-        }
-    }
-
-    private fun startListening() {
-        listening = true
-        userInfoController.addCallback(userInfoChangedListener)
-    }
-
-    private fun stopListening() {
-        listening = false
-        userInfoController.removeCallback(userInfoChangedListener)
-    }
-
-    /**
-     * Force a check to [UserManager.isUserSwitcherEnabled], and update listeners if the value has
-     * changed
-     */
-    fun checkEnabled() {
-        backgroundExecutor.execute {
-            // Check on a background thread to avoid main thread Binder calls
-            val wasEnabled = userSwitcherEnabled
-            userSwitcherEnabled = userManager.isUserSwitcherEnabled
-
-            if (wasEnabled != userSwitcherEnabled) {
-                mainExecutor.execute {
-                    notifyListenersSettingChanged()
-                }
-            }
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.println("  userSwitcherEnabled=$userSwitcherEnabled")
-        pw.println("  listening=$listening")
-    }
-}
-
-interface CurrentUserChipInfoUpdatedListener {
-    fun onCurrentUserChipInfoUpdated()
-    fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) {}
-}
-
-private const val TAG = "StatusBarUserInfoTracker"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
deleted file mode 100644
index 0d52f46..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
+++ /dev/null
@@ -1,111 +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.statusbar.phone.userswitcher
-
-import android.content.Intent
-import android.os.UserHandle
-import android.view.View
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-
-import com.android.systemui.qs.user.UserSwitchDialogController
-import com.android.systemui.user.UserSwitcherActivity
-import com.android.systemui.util.ViewController
-
-import javax.inject.Inject
-
-/**
- * ViewController for [StatusBarUserSwitcherContainer]
- */
-class StatusBarUserSwitcherControllerImpl @Inject constructor(
-    view: StatusBarUserSwitcherContainer,
-    private val tracker: StatusBarUserInfoTracker,
-    private val featureController: StatusBarUserSwitcherFeatureController,
-    private val userSwitcherDialogController: UserSwitchDialogController,
-    private val featureFlags: FeatureFlags,
-    private val activityStarter: ActivityStarter,
-    private val falsingManager: FalsingManager
-) : ViewController<StatusBarUserSwitcherContainer>(view),
-        StatusBarUserSwitcherController {
-    private val listener = object : CurrentUserChipInfoUpdatedListener {
-        override fun onCurrentUserChipInfoUpdated() {
-            updateChip()
-        }
-
-        override fun onStatusBarUserSwitcherSettingChanged(enabled: Boolean) {
-            updateEnabled()
-        }
-    }
-
-    private val featureFlagListener = object : OnUserSwitcherPreferenceChangeListener {
-        override fun onUserSwitcherPreferenceChange(enabled: Boolean) {
-            updateEnabled()
-        }
-    }
-
-    public override fun onViewAttached() {
-        tracker.addCallback(listener)
-        featureController.addCallback(featureFlagListener)
-        mView.setOnClickListener { view: View ->
-            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                return@setOnClickListener
-            }
-
-            if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
-                val intent = Intent(context, UserSwitcherActivity::class.java)
-                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
-
-                activityStarter.startActivity(intent, true /* dismissShade */,
-                        null /* ActivityLaunchAnimator.Controller */,
-                        true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM)
-            } else {
-                userSwitcherDialogController.showDialog(view)
-            }
-        }
-
-        updateEnabled()
-    }
-
-    override fun onViewDetached() {
-        tracker.removeCallback(listener)
-        featureController.removeCallback(featureFlagListener)
-        mView.setOnClickListener(null)
-    }
-
-    private fun updateChip() {
-        mView.text.text = tracker.currentUserName
-        mView.avatar.setImageDrawable(tracker.currentUserAvatar)
-    }
-
-    private fun updateEnabled() {
-        if (featureController.isStatusBarUserSwitcherFeatureEnabled() &&
-                tracker.userSwitcherEnabled) {
-            mView.visibility = View.VISIBLE
-            updateChip()
-        } else {
-            mView.visibility = View.GONE
-        }
-    }
-}
-
-interface StatusBarUserSwitcherController {
-    fun init()
-}
-
-private const val TAG = "SbUserSwitcherController"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt
deleted file mode 100644
index 7bae9ff..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherFeatureController.kt
+++ /dev/null
@@ -1,63 +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.statusbar.phone.userswitcher
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.statusbar.policy.CallbackController
-
-import javax.inject.Inject
-
-@SysUISingleton
-class StatusBarUserSwitcherFeatureController @Inject constructor(
-    private val flags: FeatureFlags
-) : CallbackController<OnUserSwitcherPreferenceChangeListener> {
-    private val listeners = mutableListOf<OnUserSwitcherPreferenceChangeListener>()
-
-    init {
-        flags.addListener(Flags.STATUS_BAR_USER_SWITCHER) {
-            it.requestNoRestart()
-            notifyListeners()
-        }
-    }
-
-    fun isStatusBarUserSwitcherFeatureEnabled(): Boolean {
-        return flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER)
-    }
-
-    override fun addCallback(listener: OnUserSwitcherPreferenceChangeListener) {
-        if (!listeners.contains(listener)) {
-            listeners.add(listener)
-        }
-    }
-
-    override fun removeCallback(listener: OnUserSwitcherPreferenceChangeListener) {
-        listeners.remove(listener)
-    }
-
-    private fun notifyListeners() {
-        val enabled = flags.isEnabled(Flags.STATUS_BAR_USER_SWITCHER)
-        listeners.forEach {
-            it.onUserSwitcherPreferenceChange(enabled)
-        }
-    }
-}
-
-interface OnUserSwitcherPreferenceChangeListener {
-    fun onUserSwitcherPreferenceChange(enabled: Boolean)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 9b8b643..946d7e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -24,29 +24,34 @@
 /** All flagging methods related to the new status bar pipeline (see b/238425913). */
 @SysUISingleton
 class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
-    /**
-     * Returns true if we should run the new pipeline backend.
-     *
-     * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs,
-     * and logs the output state.
-     */
-    fun isNewPipelineBackendEnabled(): Boolean =
-        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND)
+    /** True if we should display the mobile icons using the new status bar data pipeline. */
+    fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS)
 
     /**
-     * Returns true if we should run the new pipeline frontend *and* backend.
+     * True if we should run the new mobile icons backend to get the logging.
      *
-     * The new pipeline frontend will use the outputted state from the new backend and will make the
-     * correct changes to the UI.
+     * Does *not* affect whether we render the mobile icons using the new backend data. See
+     * [useNewMobileIcons] for that.
      */
-    fun isNewPipelineFrontendEnabled(): Boolean =
-        isNewPipelineBackendEnabled() &&
-            featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND)
+    fun runNewMobileIconsBackend(): Boolean =
+        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS_BACKEND) || useNewMobileIcons()
+
+    /** True if we should display the wifi icon using the new status bar data pipeline. */
+    fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON)
 
     /**
-     * Returns true if we should apply some coloring to icons that were rendered with the new
+     * True if we should run the new wifi icon backend to get the logging.
+     *
+     * Does *not* affect whether we render the wifi icon using the new backend data. See
+     * [useNewWifiIcon] for that.
+     */
+    fun runNewWifiIconBackend(): Boolean =
+        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON_BACKEND) || useNewWifiIcon()
+
+    /**
+     * Returns true if we should apply some coloring to the wifi icon that was rendered with the new
      * pipeline to help with debugging.
      */
-    // For now, just always apply the debug coloring if we've enabled frontend rendering.
-    fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled()
+    // For now, just always apply the debug coloring if we've enabled the new icon.
+    fun useWifiDebugColoring(): Boolean = useNewWifiIcon()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
new file mode 100644
index 0000000..7aa5ee1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.airplane.data.repository
+
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings.Global
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.SettingObserver
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides data related to airplane mode.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is
+ * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel]
+ * determine what parts of the wifi icon view should be shown.
+ *
+ * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo.
+ */
+interface AirplaneModeRepository {
+    /** Observable for whether the device is currently in airplane mode. */
+    val isAirplaneMode: StateFlow<Boolean>
+}
+
+@SysUISingleton
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class AirplaneModeRepositoryImpl
+@Inject
+constructor(
+    @Background private val bgHandler: Handler,
+    private val globalSettings: GlobalSettings,
+    logger: ConnectivityPipelineLogger,
+    @Application scope: CoroutineScope,
+) : AirplaneModeRepository {
+    // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it.
+    override val isAirplaneMode: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val observer =
+                    object :
+                        SettingObserver(
+                            globalSettings,
+                            bgHandler,
+                            Global.AIRPLANE_MODE_ON,
+                            UserHandle.USER_ALL
+                        ) {
+                        override fun handleValueChanged(value: Int, observedChange: Boolean) {
+                            trySend(value == 1)
+                        }
+                    }
+
+                observer.isListening = true
+                trySend(observer.value == 1)
+                awaitClose { observer.isListening = false }
+            }
+            .distinctUntilChanged()
+            .logInputChange(logger, "isAirplaneMode")
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                // When the observer starts listening, the flow will emit the current value so the
+                // initialValue here is irrelevant.
+                initialValue = false,
+            )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt
new file mode 100644
index 0000000..3e9b2c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.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.statusbar.pipeline.airplane.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * The business logic layer for airplane mode.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See
+ * [AirplaneModeRepository] for more details.
+ */
+@SysUISingleton
+class AirplaneModeInteractor
+@Inject
+constructor(
+    airplaneModeRepository: AirplaneModeRepository,
+    connectivityRepository: ConnectivityRepository,
+) {
+    /** True if the device is currently in airplane mode. */
+    val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode
+
+    /** True if we're configured to force-hide the airplane mode icon and false otherwise. */
+    val isForceHidden: Flow<Boolean> =
+        connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
new file mode 100644
index 0000000..fe30c01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.airplane.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Models the UI state for the status bar airplane mode icon.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See
+ * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for
+ * more details.
+ */
+@SysUISingleton
+class AirplaneModeViewModel
+@Inject
+constructor(
+    interactor: AirplaneModeInteractor,
+    logger: ConnectivityPipelineLogger,
+    @Application private val scope: CoroutineScope,
+) {
+    /** True if the airplane mode icon is currently visible in the status bar. */
+    val isAirplaneModeIconVisible: StateFlow<Boolean> =
+        combine(interactor.isAirplaneMode, interactor.isForceHidden) {
+                isAirplaneMode,
+                isAirplaneIconForceHidden ->
+                isAirplaneMode && !isAirplaneIconForceHidden
+            }
+            .distinctUntilChanged()
+            .logOutputChange(logger, "isAirplaneModeIconVisible")
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 06d5542..fcd1b8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,10 +16,16 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -30,16 +36,25 @@
 @Module
 abstract class StatusBarPipelineModule {
     @Binds
+    abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository
+
+    @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
     @Binds
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 
     @Binds
-    abstract fun mobileSubscriptionRepository(
-        impl: MobileSubscriptionRepositoryImpl
-    ): MobileSubscriptionRepository
+    abstract fun mobileConnectionsRepository(
+        impl: MobileConnectionsRepositoryImpl
+    ): MobileConnectionsRepository
 
     @Binds
     abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+
+    @Binds
+    abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+
+    @Binds
+    abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
 }
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/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
new file mode 100644
index 0000000..e618905
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.net.NetworkCapabilities
+
+/** Provides information about a mobile network connection */
+data class MobileConnectivityModel(
+    /** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */
+    val isConnected: Boolean = false,
+    /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
+    val isValidated: Boolean = false,
+)
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 46ccf32c..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
@@ -27,6 +27,8 @@
 import android.telephony.TelephonyCallback.SignalStrengthsListener
 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
@@ -48,15 +50,20 @@
     @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] */
-    val displayInfo: TelephonyDisplayInfo? = null
+    /**
+     * From [DisplayInfoListener.onDisplayInfoChanged].
+     *
+     * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
+     * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
+     */
+    val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
new file mode 100644
index 0000000..f385806
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.Annotation.NetworkType
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+/**
+ * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending
+ * on whether or not the display info contains an override type, we may have to call different
+ * methods on [MobileMappingsProxy] to generate an icon lookup key.
+ */
+sealed interface ResolvedNetworkType {
+    @NetworkType val type: Int
+}
+
+data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
+
+data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
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
new file mode 100644
index 0000000..581842b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -0,0 +1,262 @@
+/*
+ * 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.repository
+
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings.Global
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+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 com.android.systemui.util.settings.GlobalSettings
+import java.lang.IllegalStateException
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
+ * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this
+ * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId
+ *
+ * There should only ever be one [MobileConnectionRepository] per subscription, since
+ * [TelephonyManager] limits the number of callbacks that can be registered per process.
+ *
+ * This repository should have all of the relevant information for a single line of service, which
+ * eventually becomes a single icon in the status bar.
+ */
+interface MobileConnectionRepository {
+    /**
+     * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
+     * listener + model.
+     */
+    val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+    /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
+    val dataEnabled: StateFlow<Boolean>
+    /**
+     * True if this connection represents the default subscription per
+     * [SubscriptionManager.getDefaultDataSubscriptionId]
+     */
+    val isDefaultDataSubscription: StateFlow<Boolean>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+    private val context: Context,
+    private val subId: Int,
+    private val telephonyManager: TelephonyManager,
+    private val globalSettings: GlobalSettings,
+    defaultDataSubId: StateFlow<Int>,
+    globalMobileDataSettingChangedEvent: Flow<Unit>,
+    bgDispatcher: CoroutineDispatcher,
+    logger: ConnectivityPipelineLogger,
+    scope: CoroutineScope,
+) : MobileConnectionRepository {
+    init {
+        if (telephonyManager.subscriptionId != subId) {
+            throw IllegalStateException(
+                "TelephonyManager should be created with subId($subId). " +
+                    "Found ${telephonyManager.subscriptionId} instead."
+            )
+        }
+    }
+
+    private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
+    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        TelephonyCallback.ServiceStateListener,
+                        TelephonyCallback.SignalStrengthsListener,
+                        TelephonyCallback.DataConnectionStateListener,
+                        TelephonyCallback.DataActivityListener,
+                        TelephonyCallback.CarrierNetworkListener,
+                        TelephonyCallback.DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state =
+                                state.copy(dataConnectionState = dataState.toDataConnectionType())
+                            trySend(state)
+                        }
+
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            val networkType =
+                                if (
+                                    telephonyDisplayInfo.overrideNetworkType ==
+                                        OVERRIDE_NETWORK_TYPE_NONE
+                                ) {
+                                    DefaultNetworkType(telephonyDisplayInfo.networkType)
+                                } else {
+                                    OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
+                                }
+                            state = state.copy(resolvedNetworkType = networkType)
+                            trySend(state)
+                        }
+                    }
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .onEach { telephonyCallbackEvent.tryEmit(Unit) }
+            .logOutputChange(logger, "MobileSubscriptionModel")
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    /** Produces whenever the mobile data setting changes for this subId */
+    private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+        val observer =
+            object : ContentObserver(null) {
+                override fun onChange(selfChange: Boolean) {
+                    trySend(Unit)
+                }
+            }
+
+        globalSettings.registerContentObserver(
+            globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
+            /* notifyForDescendants */ true,
+            observer
+        )
+
+        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+    }
+
+    /**
+     * 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> =
+        merge(
+            telephonyCallbackEvent,
+            localMobileDataSettingChangedEvent,
+            globalMobileDataSettingChangedEvent,
+        )
+
+    override val dataEnabled: StateFlow<Boolean> =
+        telephonyPollingEvent
+            .mapLatest { dataConnectionAllowed() }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
+
+    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
+
+    override val isDefaultDataSubscription: StateFlow<Boolean> =
+        defaultDataSubId
+            .mapLatest { it == subId }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+
+    class Factory
+    @Inject
+    constructor(
+        private val context: Context,
+        private val telephonyManager: TelephonyManager,
+        private val logger: ConnectivityPipelineLogger,
+        private val globalSettings: GlobalSettings,
+        @Background private val bgDispatcher: CoroutineDispatcher,
+        @Application private val scope: CoroutineScope,
+    ) {
+        fun build(
+            subId: Int,
+            defaultDataSubId: StateFlow<Int>,
+            globalMobileDataSettingChangedEvent: Flow<Unit>,
+        ): MobileConnectionRepository {
+            return MobileConnectionRepositoryImpl(
+                context,
+                subId,
+                telephonyManager.createForSubscriptionId(subId),
+                globalSettings,
+                defaultDataSubId,
+                globalMobileDataSettingChangedEvent,
+                bgDispatcher,
+                logger,
+                scope,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
new file mode 100644
index 0000000..c3c1f14
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -0,0 +1,291 @@
+/*
+ * 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.repository
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings
+import android.provider.Settings.Global.MOBILE_DATA
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileConnectionsRepository {
+    /** Observable list of current mobile subscriptions */
+    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+    /** Observable for the subscriptionId of the current mobile data connection */
+    val activeMobileDataSubscriptionId: StateFlow<Int>
+
+    /** Observable for [MobileMappings.Config] tracking the defaults */
+    val defaultDataSubRatConfig: StateFlow<Config>
+
+    /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
+    val defaultDataSubId: StateFlow<Int>
+
+    /** The current connectivity status for the default mobile network connection */
+    val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
+
+    /** Get or create a repository for the line of service for the given subscription ID */
+    fun getRepoForSubId(subId: Int): MobileConnectionRepository
+
+    /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
+    val globalMobileDataSettingChangedEvent: Flow<Unit>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+    private val connectivityManager: ConnectivityManager,
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    private val logger: ConnectivityPipelineLogger,
+    broadcastDispatcher: BroadcastDispatcher,
+    private val globalSettings: GlobalSettings,
+    private val context: Context,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+    private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
+
+    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+
+    override val defaultDataSubId: StateFlow<Int> =
+        broadcastDispatcher
+            .broadcastFlow(
+                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+            ) { intent, _ ->
+                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+            }
+            .distinctUntilChanged()
+            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                SubscriptionManager.getDefaultDataSubscriptionId()
+            )
+
+    private val carrierConfigChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+        )
+
+    /**
+     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+     * config, so this will apply to every icon that we care about.
+     *
+     * Relevant bits in the config are things like
+     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+     *
+     * This flow will produce whenever the default data subscription or the carrier config changes.
+     */
+    override val defaultDataSubRatConfig: StateFlow<Config> =
+        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+            .mapLatest { Config.readConfig(context) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                initialValue = Config.readConfig(context)
+            )
+
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        if (!isValidSubId(subId)) {
+            throw IllegalArgumentException(
+                "subscriptionId $subId is not in the list of valid subscriptions"
+            )
+        }
+
+        return subIdRepositoryCache[subId]
+            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+    }
+
+    /**
+     * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
+     * connection repositories also observe the URI for [MOBILE_DATA] + subId.
+     */
+    override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+        val observer =
+            object : ContentObserver(null) {
+                override fun onChange(selfChange: Boolean) {
+                    trySend(Unit)
+                }
+            }
+
+        globalSettings.registerContentObserver(
+            globalSettings.getUriFor(MOBILE_DATA),
+            true,
+            observer
+        )
+
+        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+    }
+
+    @SuppressLint("MissingPermission")
+    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+                        override fun onLost(network: Network) {
+                            // Send a disconnected model when lost. Maybe should create a sealed
+                            // type or null here?
+                            trySend(MobileConnectivityModel())
+                        }
+
+                        override fun onCapabilitiesChanged(
+                            network: Network,
+                            caps: NetworkCapabilities
+                        ) {
+                            trySend(
+                                MobileConnectivityModel(
+                                    isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
+                                    isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
+                                )
+                            )
+                        }
+                    }
+
+                connectivityManager.registerDefaultNetworkCallback(callback)
+
+                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
+
+    private fun isValidSubId(subId: Int): Boolean {
+        subscriptionsFlow.value.forEach {
+            if (it.subscriptionId == subId) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+        return mobileConnectionRepositoryFactory.build(
+            subId,
+            defaultDataSubId,
+            globalMobileDataSettingChangedEvent,
+        )
+    }
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+        subIdRepositoryCache.keys.forEach {
+            if (!currentValidSubscriptionIds.contains(it)) {
+                subIdRepositoryCache.remove(it)
+            }
+        }
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
deleted file mode 100644
index 36de2a2..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
+++ /dev/null
@@ -1,210 +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.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-
-/**
- * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
- * on various policy
- */
-interface MobileSubscriptionRepository {
-    /** Observable list of current mobile subscriptions */
-    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
-
-    /** Observable for the subscriptionId of the current mobile data connection */
-    val activeMobileDataSubscriptionId: Flow<Int>
-
-    /** Get or create an observable for the given subscription ID */
-    fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
-}
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileSubscriptionRepositoryImpl
-@Inject
-constructor(
-    private val subscriptionManager: SubscriptionManager,
-    private val telephonyManager: TelephonyManager,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @Application private val scope: CoroutineScope,
-) : MobileSubscriptionRepository {
-    private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
-
-    /**
-     * State flow that emits the set of mobile data subscriptions, each represented by its own
-     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
-     * info object, but for now we keep track of the infos themselves.
-     */
-    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
-            .mapLatest { fetchSubscriptionsList() }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
-    /** StateFlow that keeps track of the current active mobile data subscription */
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
-                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
-                            trySend(subId)
-                        }
-                    }
-
-                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
-            }
-            .stateIn(
-                scope,
-                started = SharingStarted.WhileSubscribed(),
-                SubscriptionManager.INVALID_SUBSCRIPTION_ID
-            )
-
-    /**
-     * Each mobile subscription needs its own flow, which comes from registering listeners on the
-     * system. Use this method to create those flows and cache them for reuse
-     */
-    override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
-        return subIdFlowCache[subId]
-            ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
-    }
-
-    @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
-
-    private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
-        var state = MobileSubscriptionModel()
-        conflatedCallbackFlow {
-                val phony = telephonyManager.createForSubscriptionId(subId)
-                // TODO (b/240569788): log all of these into the connectivity logger
-                val callback =
-                    object :
-                        TelephonyCallback(),
-                        ServiceStateListener,
-                        SignalStrengthsListener,
-                        DataConnectionStateListener,
-                        DataActivityListener,
-                        CarrierNetworkListener,
-                        DisplayInfoListener {
-                        override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
-                            trySend(state)
-                        }
-                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
-                            val cdmaLevel =
-                                signalStrength
-                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
-                                    .let { strengths ->
-                                        if (!strengths.isEmpty()) {
-                                            strengths[0].level
-                                        } else {
-                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
-                                        }
-                                    }
-
-                            val primaryLevel = signalStrength.level
-
-                            state =
-                                state.copy(
-                                    cdmaLevel = cdmaLevel,
-                                    primaryLevel = primaryLevel,
-                                    isGsm = signalStrength.isGsm,
-                                )
-                            trySend(state)
-                        }
-                        override fun onDataConnectionStateChanged(
-                            dataState: Int,
-                            networkType: Int
-                        ) {
-                            state = state.copy(dataConnectionState = dataState)
-                            trySend(state)
-                        }
-                        override fun onDataActivity(direction: Int) {
-                            state = state.copy(dataActivityDirection = direction)
-                            trySend(state)
-                        }
-                        override fun onCarrierNetworkChange(active: Boolean) {
-                            state = state.copy(carrierNetworkChangeActive = active)
-                            trySend(state)
-                        }
-                        override fun onDisplayInfoChanged(
-                            telephonyDisplayInfo: TelephonyDisplayInfo
-                        ) {
-                            state = state.copy(displayInfo = telephonyDisplayInfo)
-                            trySend(state)
-                        }
-                    }
-                phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose {
-                    phony.unregisterTelephonyCallback(callback)
-                    // Release the cached flow
-                    subIdFlowCache.remove(subId)
-                }
-            }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
-    }
-
-    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
-        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
index 77de849..91886bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
@@ -26,7 +26,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.mapLatest
@@ -40,7 +39,7 @@
  */
 interface UserSetupRepository {
     /** Observable tracking [DeviceProvisionedController.isUserSetup] */
-    val isUserSetupFlow: Flow<Boolean>
+    val isUserSetupFlow: StateFlow<Boolean>
 }
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
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 40fe0f3..0da84f0 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
@@ -17,51 +17,110 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.telephony.CarrierConfigManager
-import com.android.settingslib.SignalIcon
-import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
 
 interface MobileIconInteractor {
-    /** Identifier for RAT type indicator */
-    val iconGroup: Flow<SignalIcon.MobileIconGroup>
+    /** Only true if mobile is the default transport but is not validated, otherwise false */
+    val isDefaultConnectionFailed: StateFlow<Boolean>
+
+    /** True when telephony tells us that the data state is CONNECTED */
+    val isDataConnected: StateFlow<Boolean>
+
+    // TODO(b/256839546): clarify naming of default vs active
+    /** True if we want to consider the data connection enabled */
+    val isDefaultDataEnabled: StateFlow<Boolean>
+
+    /** Observable for the data enabled state of this connection */
+    val isDataEnabled: StateFlow<Boolean>
+
+    /** Observable for RAT type (network type) indicator */
+    val networkTypeIconGroup: StateFlow<MobileIconGroup>
+
     /** True if this line of service is emergency-only */
-    val isEmergencyOnly: Flow<Boolean>
+    val isEmergencyOnly: StateFlow<Boolean>
+
     /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
-    val level: Flow<Int>
+    val level: StateFlow<Int>
+
     /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
-    val numberOfLevels: Flow<Int>
-    /** True when we want to draw an icon that makes room for the exclamation mark */
-    val cutOut: Flow<Boolean>
+    val numberOfLevels: StateFlow<Int>
 }
 
 /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
 class MobileIconInteractorImpl(
-    mobileStatusInfo: Flow<MobileSubscriptionModel>,
+    @Application scope: CoroutineScope,
+    defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
+    defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
+    defaultMobileIconGroup: StateFlow<MobileIconGroup>,
+    override val isDefaultConnectionFailed: StateFlow<Boolean>,
+    mobileMappingsProxy: MobileMappingsProxy,
+    connectionRepository: MobileConnectionRepository,
 ) : MobileIconInteractor {
-    override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
-    override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
+    private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
 
-    override val level: Flow<Int> =
-        mobileStatusInfo.map { mobileModel ->
-            // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
-            if (mobileModel.isGsm) {
-                mobileModel.primaryLevel
-            } else {
-                mobileModel.cdmaLevel
+    override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
+
+    override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
+
+    /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
+    override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
+        combine(
+                mobileStatusInfo,
+                defaultMobileIconMapping,
+                defaultMobileIconGroup,
+            ) { info, mapping, defaultGroup ->
+                val lookupKey =
+                    when (val resolved = info.resolvedNetworkType) {
+                        is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
+                        is OverrideNetworkType ->
+                            mobileMappingsProxy.toIconKeyOverride(resolved.type)
+                    }
+                mapping[lookupKey] ?: defaultGroup
             }
-        }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
+
+    override val isEmergencyOnly: StateFlow<Boolean> =
+        mobileStatusInfo
+            .mapLatest { it.isEmergencyOnly }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val level: StateFlow<Int> =
+        mobileStatusInfo
+            .mapLatest { mobileModel ->
+                // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
+                if (mobileModel.isGsm) {
+                    mobileModel.primaryLevel
+                } else {
+                    mobileModel.cdmaLevel
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     /**
      * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
      * once it's wired up inside of [CarrierConfigTracker]
      */
-    override val numberOfLevels: Flow<Int> = flowOf(4)
+    override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4)
 
-    /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
-    // TODO: find a better name for this?
-    override val cutOut: Flow<Boolean> = flowOf(false)
+    override val isDataConnected: StateFlow<Boolean> =
+        mobileStatusInfo
+            .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 }
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 8e67e19..a4175c3 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
@@ -19,34 +19,91 @@
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+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
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
 
 /**
- * Business layer logic for mobile subscription icons
+ * Business layer logic for the set of mobile subscription icons.
  *
- * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
- * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
+ * The list of subscriptions is filtered based on the opportunistic flags on the infos.
+ *
+ * It provides the default mapping between the telephony display info and the icon group that
+ * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
+ * icon
  */
+interface MobileIconsInteractor {
+    /** List of subscriptions, potentially filtered for CBRS */
+    val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+    /** True if the active mobile data subscription has data enabled */
+    val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
+    /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
+    val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
+    /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
+    val defaultMobileIconGroup: StateFlow<MobileIconGroup>
+    /** True only if the default network is mobile, and validation also failed */
+    val isDefaultConnectionFailed: StateFlow<Boolean>
+    /** True once the user has been set up */
+    val isUserSetup: StateFlow<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 MobileIconsInteractor
+class MobileIconsInteractorImpl
 @Inject
 constructor(
-    private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+    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 activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> =
+        activeMobileDataSubscriptionId
+            .mapLatest { activeId ->
+                if (activeId == INVALID_SUBSCRIPTION_ID) {
+                    null
+                } else {
+                    mobileConnectionsRepo.getRepoForSubId(activeId)
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+    override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
+        activeMobileDataConnectionRepo
+            .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
-        mobileSubscriptionRepo.subscriptionsFlow
+        mobileConnectionsRepo.subscriptionsFlow
 
     /**
      * Generally, SystemUI wants to show iconography for each subscription that is listed by
@@ -61,7 +118,7 @@
      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
      * and by checking which subscription is opportunistic, or which one is active.
      */
-    val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+    override val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
         combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
             ->
             // Based on the old logic,
@@ -92,15 +149,47 @@
             }
         }
 
-    val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+    /**
+     * Mapping from network type to [MobileIconGroup] using the config generated for the default
+     * subscription Id. This mapping is the same for every subscription.
+     */
+    override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
+        mobileConnectionsRepo.defaultDataSubRatConfig
+            .mapLatest { mobileMappingsProxy.mapIconSets(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
 
-    /** Vends out new [MobileIconInteractor] for a particular subId */
-    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
-        MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
+    /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
+    override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
+        mobileConnectionsRepo.defaultDataSubRatConfig
+            .mapLatest { mobileMappingsProxy.getDefaultIcons(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
 
     /**
-     * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
+     * We want to show an error state when cellular has actually failed to validate, but not if some
+     * other transport type is active, because then we expect there not to be validation.
      */
-    private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
-        mobileSubscriptionRepo.getFlowForSubId(subId)
+    override val isDefaultConnectionFailed: StateFlow<Boolean> =
+        mobileConnectionsRepo.defaultMobileNetworkConnectivity
+            .mapLatest { connectivityModel ->
+                if (!connectivityModel.isConnected) {
+                    false
+                } else {
+                    !connectivityModel.isValidated
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
+
+    /** Vends out new [MobileIconInteractor] for a particular subId */
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+        MobileIconInteractorImpl(
+            scope,
+            activeDataConnectionHasDataEnabled,
+            defaultMobileIconMapping,
+            defaultMobileIconGroup,
+            isDefaultConnectionFailed,
+            mobileMappingsProxy,
+            mobileConnectionsRepo.getRepoForSubId(subId),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 380017c..c7e0ce1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import javax.inject.Inject
@@ -50,6 +51,7 @@
     private val iconController: StatusBarIconController,
     private val iconsViewModelFactory: MobileIconsViewModel.Factory,
     @Application scope: CoroutineScope,
+    private val statusBarPipelineFlags: StatusBarPipelineFlags,
 ) {
     private val mobileSubIds: Flow<List<Int>> =
         interactor.filteredSubscriptions.mapLatest { infos ->
@@ -66,8 +68,14 @@
     private val mobileSubIdsState: StateFlow<List<Int>> =
         mobileSubIds
             .onEach {
-                // Notify the icon controller here so that it knows to add icons
-                iconController.setNewMobileIconSubIds(it)
+                // Only notify the icon controller if we want to *render* the new icons.
+                // Note that this flow may still run if
+                // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
+                // get the logging data without rendering.
+                if (statusBarPipelineFlags.useNewMobileIcons()) {
+                    // Notify the icon controller here so that it knows to add icons
+                    iconController.setNewMobileIconSubIds(it)
+                }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 1405b05..67ea139 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.binder
 
 import android.content.res.ColorStateList
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
 import androidx.core.view.isVisible
@@ -24,6 +26,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
 import kotlinx.coroutines.flow.collect
@@ -37,6 +40,7 @@
         view: ViewGroup,
         viewModel: MobileIconViewModel,
     ) {
+        val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
 
@@ -52,10 +56,20 @@
                     }
                 }
 
+                // Set the network type icon
+                launch {
+                    viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
+                        dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+                        networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE
+                    }
+                }
+
                 // Set the tint
                 launch {
                     viewModel.tint.collect { tint ->
-                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                        val tintList = ColorStateList.valueOf(tint)
+                        iconView.imageTintList = tintList
+                        networkTypeView.imageTintList = tintList
                     }
                 }
             }
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 cfabeba..7869021 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
@@ -18,14 +18,18 @@
 
 import android.graphics.Color
 import com.android.settingslib.graph.SignalDrawable
+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.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
 
 /**
  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -37,22 +41,47 @@
  *
  * TODO: figure out where carrier merged and VCN models go (probably here?)
  */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
 class MobileIconViewModel
 constructor(
     val subscriptionId: Int,
     iconInteractor: MobileIconInteractor,
     logger: ConnectivityPipelineLogger,
 ) {
+    /** Whether or not to show the error state of [SignalDrawable] */
+    private val showExclamationMark: Flow<Boolean> =
+        iconInteractor.isDefaultDataEnabled.mapLatest { !it }
+
     /** An int consumable by [SignalDrawable] for display */
-    var iconId: Flow<Int> =
-        combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
+    val iconId: Flow<Int> =
+        combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
                 level,
                 numberOfLevels,
-                cutOut ->
-                SignalDrawable.getState(level, numberOfLevels, cutOut)
+                showExclamationMark ->
+                SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
             }
             .distinctUntilChanged()
             .logOutputChange(logger, "iconId($subscriptionId)")
 
-    var tint: Flow<Int> = flowOf(Color.CYAN)
+    /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+    val networkTypeIcon: Flow<Icon?> =
+        combine(
+            iconInteractor.networkTypeIconGroup,
+            iconInteractor.isDataConnected,
+            iconInteractor.isDataEnabled,
+            iconInteractor.isDefaultConnectionFailed,
+        ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection ->
+            if (!dataConnected || !dataEnabled || failedConnection) {
+                null
+            } else {
+                val desc =
+                    if (networkTypeIconGroup.dataContentDescription != 0)
+                        ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
+                    else null
+                Icon.Resource(networkTypeIconGroup.dataType, desc)
+            }
+        }
+
+    val tint: Flow<Int> = flowOf(Color.CYAN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
new file mode 100644
index 0000000..501467f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.util
+
+import android.telephony.Annotation.NetworkType
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import javax.inject.Inject
+
+/**
+ * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to
+ * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This
+ * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake
+ * them out in tests
+ */
+interface MobileMappingsProxy {
+    fun mapIconSets(config: Config): Map<String, MobileIconGroup>
+    fun getDefaultIcons(config: Config): MobileIconGroup
+    fun getIconKey(displayInfo: TelephonyDisplayInfo): String
+    fun toIconKey(@NetworkType networkType: Int): String
+    fun toIconKeyOverride(@NetworkType networkType: Int): String
+}
+
+/** Injectable wrapper class for [MobileMappings] */
+class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy {
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> =
+        MobileMappings.mapIconSets(config)
+
+    override fun getDefaultIcons(config: Config): MobileIconGroup =
+        MobileMappings.getDefaultIcons(config)
+
+    override fun getIconKey(displayInfo: TelephonyDisplayInfo): String =
+        MobileMappings.getIconKey(displayInfo)
+
+    override fun toIconKey(@NetworkType networkType: Int): String =
+        MobileMappings.toIconKey(networkType)
+
+    override fun toIconKeyOverride(networkType: Int): String =
+        MobileMappings.toDisplayIconKey(networkType)
+}
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 118b94c..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/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index dbb1aa5..d3cf32f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,10 +18,10 @@
 
 import android.net.Network
 import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/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/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 681cf72..93448c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import java.util.concurrent.Executor
@@ -64,6 +63,9 @@
     /** Observable for the current wifi enabled status. */
     val isWifiEnabled: StateFlow<Boolean>
 
+    /** Observable for the current wifi default status. */
+    val isWifiDefault: StateFlow<Boolean>
+
     /** Observable for the current wifi network. */
     val wifiNetwork: StateFlow<WifiNetworkModel>
 
@@ -103,7 +105,7 @@
             merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
                 .mapLatest { wifiManager.isWifiEnabled }
                 .distinctUntilChanged()
-                .logOutputChange(logger, "enabled")
+                .logInputChange(logger, "enabled")
                 .stateIn(
                     scope = scope,
                     started = SharingStarted.WhileSubscribed(),
@@ -111,6 +113,39 @@
                 )
         }
 
+    override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow {
+        // Note: This callback doesn't do any logging because we already log every network change
+        // in the [wifiNetwork] callback.
+        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                networkCapabilities: NetworkCapabilities
+            ) {
+                // This method will always be called immediately after the network becomes the
+                // default, in addition to any time the capabilities change while the network is
+                // the default.
+                // If this network contains valid wifi info, then wifi is the default network.
+                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                trySend(wifiInfo != null)
+            }
+
+            override fun onLost(network: Network) {
+                // The system no longer has a default network, so wifi is definitely not default.
+                trySend(false)
+            }
+        }
+
+        connectivityManager.registerDefaultNetworkCallback(callback)
+        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+    }
+        .distinctUntilChanged()
+        .logInputChange(logger, "isWifiDefault")
+        .stateIn(
+            scope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = false
+        )
+
     override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow {
         var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 04b17ed..3a3e611 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -59,6 +59,9 @@
     /** Our current enabled status. */
     val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
 
+    /** Our current default status. */
+    val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault
+
     /** Our current wifi network. See [WifiNetworkModel]. */
     val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
 
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/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
new file mode 100644
index 0000000..b816364
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.wifi.ui
+
+import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/**
+ * This class serves as a bridge between the old UI classes and the new data pipeline.
+ *
+ * Once the new pipeline notifies [wifiViewModel] that the wifi icon should be visible, this class
+ * notifies [iconController] to inflate the wifi icon (if needed). After that, the [wifiViewModel]
+ * has sole responsibility for updating the wifi icon drawable, visibility, etc. and the
+ * [iconController] will not do any updates to the icon.
+ */
+@SysUISingleton
+class WifiUiAdapter
+@Inject
+constructor(
+    private val iconController: StatusBarIconController,
+    private val wifiViewModel: WifiViewModel,
+    private val statusBarPipelineFlags: StatusBarPipelineFlags,
+) {
+    /**
+     * Binds the container for all the status bar icons to a view model, so that we inflate the wifi
+     * view once we receive a valid icon from the data pipeline.
+     *
+     * NOTE: This should go away as we better integrate the data pipeline with the UI.
+     *
+     * @return the view model used for this particular group in the given [location].
+     */
+    fun bindGroup(
+        statusBarIconGroup: ViewGroup,
+        location: StatusBarLocation,
+    ): LocationBasedWifiViewModel {
+        val locationViewModel =
+            when (location) {
+                StatusBarLocation.HOME -> wifiViewModel.home
+                StatusBarLocation.KEYGUARD -> wifiViewModel.keyguard
+                StatusBarLocation.QS -> wifiViewModel.qs
+            }
+
+        statusBarIconGroup.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    locationViewModel.wifiIcon.collect { wifiIcon ->
+                        // Only notify the icon controller if we want to *render* the new icon.
+                        // Note that this flow may still run if
+                        // [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may
+                        // want to get the logging data without rendering.
+                        if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) {
+                            iconController.setNewWifiIcon()
+                        }
+                    }
+                }
+            }
+        }
+
+        return locationViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 273be63..345f8cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -30,9 +30,7 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collect
@@ -62,26 +60,9 @@
         fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
     }
 
-    /**
-     * Binds the view to the appropriate view-model based on the given location. The view will
-     * continue to be updated following updates from the view-model.
-     */
-    @JvmStatic
-    fun bind(
-        view: ViewGroup,
-        wifiViewModel: WifiViewModel,
-        location: StatusBarLocation,
-    ): Binding {
-        return when (location) {
-            StatusBarLocation.HOME -> bind(view, wifiViewModel.home)
-            StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard)
-            StatusBarLocation.QS -> bind(view, wifiViewModel.qs)
-        }
-    }
-
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
     @JvmStatic
-    private fun bind(
+    fun bind(
         view: ViewGroup,
         viewModel: LocationBasedWifiViewModel,
     ): Binding {
@@ -91,6 +72,7 @@
         val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)
         val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
         val activityContainerView = view.requireViewById<View>(R.id.inout_container)
+        val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer)
 
         view.isVisible = true
         iconView.isVisible = true
@@ -142,6 +124,12 @@
                         activityContainerView.isVisible = visible
                     }
                 }
+
+                launch {
+                    viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible ->
+                        airplaneSpacer.isVisible = visible
+                    }
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index 0cd9bd7..a45076b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -26,9 +26,8 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 
 /**
  * A new and more modern implementation of [com.android.systemui.statusbar.StatusBarWifiView] that
@@ -81,12 +80,11 @@
 
     private fun initView(
         slotName: String,
-        wifiViewModel: WifiViewModel,
-        location: StatusBarLocation,
+        wifiViewModel: LocationBasedWifiViewModel,
     ) {
         slot = slotName
         initDotView()
-        binding = WifiViewBinder.bind(this, wifiViewModel, location)
+        binding = WifiViewBinder.bind(this, wifiViewModel)
     }
 
     // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView].
@@ -116,14 +114,13 @@
         fun constructAndBind(
             context: Context,
             slot: String,
-            wifiViewModel: WifiViewModel,
-            location: StatusBarLocation,
+            wifiViewModel: LocationBasedWifiViewModel,
         ): ModernStatusBarWifiView {
             return (
                 LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
                     as ModernStatusBarWifiView
                 ).also {
-                    it.initView(slot, wifiViewModel, location)
+                    it.initView(slot, wifiViewModel)
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
index 40f948f..95ab251 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
@@ -32,6 +32,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -40,4 +41,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
index 9642ac4..86535d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
@@ -29,6 +29,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index e23f8c7..7cbdf5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -44,11 +44,14 @@
 
     /** True if the activity container view should be visible. */
     val isActivityContainerVisible: Flow<Boolean>,
+
+    /** True if the airplane spacer view should be visible. */
+    val isAirplaneSpacerVisible: Flow<Boolean>,
 ) {
     /** The color that should be used to tint the icon. */
     val tint: Flow<Int> =
         flowOf(
-            if (statusBarPipelineFlags.useNewPipelineDebugColoring()) {
+            if (statusBarPipelineFlags.useWifiDebugColoring()) {
                 debugTint
             } else {
                 DEFAULT_TINT
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
index 0ddf90e..fd54c5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
@@ -29,6 +29,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ebbd77b..0782bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
@@ -66,6 +67,7 @@
 class WifiViewModel
 @Inject
 constructor(
+    airplaneModeViewModel: AirplaneModeViewModel,
     connectivityConstants: ConnectivityConstants,
     private val context: Context,
     logger: ConnectivityPipelineLogger,
@@ -124,9 +126,10 @@
     private val wifiIcon: StateFlow<Icon.Resource?> =
         combine(
             interactor.isEnabled,
+            interactor.isDefault,
             interactor.isForceHidden,
             interactor.wifiNetwork,
-        ) { isEnabled, isForceHidden, wifiNetwork ->
+        ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
             if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
                 return@combine null
             }
@@ -135,13 +138,15 @@
             val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
 
             return@combine when {
+                isDefault -> icon
                 wifiConstants.alwaysShowIconIfEnabled -> icon
                 !connectivityConstants.hasDataCapabilities -> icon
                 wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
                 else -> null
             }
         }
-        .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
+                .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() }
+                .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
     private val activity: Flow<WifiActivityModel?> =
@@ -175,6 +180,12 @@
                 }
              .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
+    // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the
+    //  airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel
+    //  that appropriately knows about both icons and sets the padding appropriately.
+    private val isAirplaneSpacerVisible: Flow<Boolean> =
+        airplaneModeViewModel.isAirplaneModeIconVisible
+
     /** A view model for the status bar on the home screen. */
     val home: HomeWifiViewModel =
         HomeWifiViewModel(
@@ -183,6 +194,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar on keyguard. */
@@ -193,6 +205,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar in quick settings. */
@@ -203,6 +216,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 2f0ebf7..68d30d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -21,7 +21,6 @@
 import android.graphics.ColorMatrix
 import android.graphics.ColorMatrixColorFilter
 import android.graphics.drawable.Drawable
-import android.os.UserHandle
 import android.widget.BaseAdapter
 import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
 import com.android.systemui.user.data.source.UserRecord
@@ -43,11 +42,7 @@
     }
 
     override fun getCount(): Int {
-        return if (controller.isKeyguardShowing) {
-            users.count { !it.isRestricted }
-        } else {
-            users.size
-        }
+        return users.size
     }
 
     override fun getItem(position: Int): UserRecord {
@@ -65,7 +60,7 @@
      * animation to and from the parent dialog.
      */
     @JvmOverloads
-    fun onUserListItemClicked(
+    open fun onUserListItemClicked(
         record: UserRecord,
         dialogShower: DialogShower? = null,
     ) {
@@ -88,7 +83,7 @@
     }
 
     fun refresh() {
-        controller.refreshUsers(UserHandle.USER_NULL)
+        controller.refreshUsers()
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 149ed0a..d10d7cf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -155,6 +155,9 @@
 
         default void onWirelessChargingChanged(boolean isWirlessCharging) {
         }
+
+        default void onIsOverheatedChanged(boolean isOverheated) {
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index c7ad767..2ee5232 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT;
+import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN;
+import static android.os.BatteryManager.EXTRA_HEALTH;
 import static android.os.BatteryManager.EXTRA_PRESENT;
 
 import android.annotation.WorkerThread;
@@ -87,6 +90,7 @@
     protected boolean mPowerSave;
     private boolean mAodPowerSave;
     private boolean mWirelessCharging;
+    private boolean mIsOverheated = false;
     private boolean mTestMode = false;
     @VisibleForTesting
     boolean mHasReceivedBattery = false;
@@ -152,6 +156,7 @@
         pw.print("  mPluggedIn="); pw.println(mPluggedIn);
         pw.print("  mCharging="); pw.println(mCharging);
         pw.print("  mCharged="); pw.println(mCharged);
+        pw.print("  mIsOverheated="); pw.println(mIsOverheated);
         pw.print("  mPowerSave="); pw.println(mPowerSave);
         pw.print("  mStateUnknown="); pw.println(mStateUnknown);
     }
@@ -184,6 +189,7 @@
         cb.onPowerSaveChanged(mPowerSave);
         cb.onBatteryUnknownStateChanged(mStateUnknown);
         cb.onWirelessChargingChanged(mWirelessCharging);
+        cb.onIsOverheatedChanged(mIsOverheated);
     }
 
     @Override
@@ -222,6 +228,13 @@
                 fireBatteryUnknownStateChanged();
             }
 
+            int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN);
+            boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT;
+            if (isOverheated != mIsOverheated) {
+                mIsOverheated = isOverheated;
+                fireIsOverheatedChanged();
+            }
+
             fireBatteryLevelChanged();
         } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) {
             updatePowerSave();
@@ -292,6 +305,10 @@
         return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
     }
 
+    public boolean isOverheated() {
+        return mIsOverheated;
+    }
+
     @Override
     public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) {
         // Need to fetch or refresh the estimate, but it may involve binder calls so offload the
@@ -402,6 +419,15 @@
         }
     }
 
+    private void fireIsOverheatedChanged() {
+        synchronized (mChangeCallbacks) {
+            final int n = mChangeCallbacks.size();
+            for (int i = 0; i < n; i++) {
+                mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated);
+            }
+        }
+    }
+
     @Override
     public void dispatchDemoCommand(String command, Bundle args) {
         if (!mDemoModeController.isInDemoMode()) {
@@ -412,6 +438,7 @@
         String plugged = args.getString("plugged");
         String powerSave = args.getString("powersave");
         String present = args.getString("present");
+        String overheated = args.getString("overheated");
         if (level != null) {
             mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100);
         }
@@ -426,6 +453,10 @@
             mStateUnknown = !present.equals("true");
             fireBatteryUnknownStateChanged();
         }
+        if (overheated != null) {
+            mIsOverheated = overheated.equals("true");
+            fireIsOverheatedChanged();
+        }
         fireBatteryLevelChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index aae0f93..acdf0d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
@@ -41,6 +40,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -83,6 +83,7 @@
     @Inject
     public BluetoothControllerImpl(
             Context context,
+            UserTracker userTracker,
             DumpManager dumpManager,
             BluetoothLogger logger,
             @Background Looper bgLooper,
@@ -100,7 +101,7 @@
                     mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState());
         }
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-        mCurrentUser = ActivityManager.getCurrentUser();
+        mCurrentUser = userTracker.getUserId();
         mDumpManager.registerDumpable(TAG, this);
     }
 
@@ -145,7 +146,13 @@
     }
 
     private String getDeviceString(CachedBluetoothDevice device) {
-        return device.getName() + " " + device.getBondState() + " " + device.isConnected();
+        return device.getName()
+                + " bondState=" + device.getBondState()
+                + " connected=" + device.isConnected()
+                + " active[A2DP]=" + device.isActiveDevice(BluetoothProfile.A2DP)
+                + " active[HEADSET]=" + device.isActiveDevice(BluetoothProfile.HEADSET)
+                + " active[HEARING_AID]=" + device.isActiveDevice(BluetoothProfile.HEARING_AID)
+                + " active[LE_AUDIO]=" + device.isActiveDevice(BluetoothProfile.LE_AUDIO);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index 576962d..d84cbcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import android.annotation.NonNull;
 import android.app.StatusBarManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -49,7 +50,7 @@
 import com.android.systemui.demomode.DemoModeCommandReceiver;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -79,7 +80,7 @@
     private static final String SHOW_SECONDS = "show_seconds";
     private static final String VISIBILITY = "visibility";
 
-    private final CurrentUserTracker mCurrentUserTracker;
+    private final UserTracker mUserTracker;
     private final CommandQueue mCommandQueue;
     private int mCurrentUserId;
 
@@ -114,6 +115,14 @@
 
     private final BroadcastDispatcher mBroadcastDispatcher;
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    mCurrentUserId = newUser;
+                }
+            };
+
     public Clock(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -132,12 +141,7 @@
             a.recycle();
         }
         mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
-        mCurrentUserTracker = new CurrentUserTracker(mBroadcastDispatcher) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                mCurrentUserId = newUserId;
-            }
-        };
+        mUserTracker = Dependency.get(UserTracker.class);
     }
 
     @Override
@@ -196,8 +200,8 @@
             Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
                     StatusBarIconController.ICON_HIDE_LIST);
             mCommandQueue.addCallback(this);
-            mCurrentUserTracker.startTracking();
-            mCurrentUserId = mCurrentUserTracker.getCurrentUserId();
+            mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+            mCurrentUserId = mUserTracker.getUserId();
         }
 
         // The time zone may have changed while the receiver wasn't registered, so update the Time
@@ -227,7 +231,7 @@
             mAttached = false;
             Dependency.get(TunerService.class).removeTunable(this);
             mCommandQueue.removeCallback(this);
-            mCurrentUserTracker.stopTracking();
+            mUserTracker.removeCallback(mUserChangedCallback);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
index bc2ae64..e326611 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -67,7 +67,7 @@
         internal const val QS_DEFAULT_POSITION = 7
 
         internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted"
-        internal const val PREFS_CONTROLS_FILE = "controls_prefs"
+        const val PREFS_CONTROLS_FILE = "controls_prefs"
         internal const val PREFS_SETTINGS_DIALOG_ATTEMPTS = "show_settings_attempts"
         private const val SEEDING_MAX = 2
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
index 1d414745..7acdaff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
@@ -118,7 +118,10 @@
 
     private void updateDeviceState(int state) {
         Log.v(TAG, "updateDeviceState [state=" + state + "]");
-        Trace.beginSection("updateDeviceState [state=" + state + "]");
+        if (Trace.isEnabled()) {
+            Trace.traceBegin(
+                    Trace.TRACE_TAG_APP, "updateDeviceState [state=" + state + "]");
+        }
         try {
             if (mDeviceState == state) {
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index d7c81af..df1e80b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.statusbar.policy
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
index bd2123a..a4821e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -18,7 +18,6 @@
 
 import static android.net.TetheringManager.TETHERING_WIFI;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.TetheringManager;
@@ -33,10 +32,12 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.ConcurrentUtils;
+import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -58,11 +59,13 @@
     private final WifiManager mWifiManager;
     private final Handler mMainHandler;
     private final Context mContext;
+    private final UserTracker mUserTracker;
 
     private int mHotspotState;
     private volatile int mNumConnectedDevices;
     // Assume tethering is available until told otherwise
     private volatile boolean mIsTetheringSupported = true;
+    private final boolean mIsTetheringSupportedConfig;
     private volatile boolean mHasTetherableWifiRegexs = true;
     private boolean mWaitingForTerminalState;
 
@@ -93,31 +96,39 @@
     @Inject
     public HotspotControllerImpl(
             Context context,
+            UserTracker userTracker,
             @Main Handler mainHandler,
             @Background Handler backgroundHandler,
             DumpManager dumpManager) {
         mContext = context;
+        mUserTracker = userTracker;
         mTetheringManager = context.getSystemService(TetheringManager.class);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mMainHandler = mainHandler;
-        mTetheringManager.registerTetheringEventCallback(
-                new HandlerExecutor(backgroundHandler), mTetheringCallback);
+        mIsTetheringSupportedConfig = context.getResources()
+                .getBoolean(R.bool.config_show_wifi_tethering);
+        if (mIsTetheringSupportedConfig) {
+            mTetheringManager.registerTetheringEventCallback(
+                    new HandlerExecutor(backgroundHandler), mTetheringCallback);
+        }
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     /**
      * Whether hotspot is currently supported.
      *
-     * This will return {@code true} immediately on creation of the controller, but may be updated
-     * later. Callbacks from this controllers will notify if the state changes.
+     * This may return {@code true} immediately on creation of the controller, but may be updated
+     * later as capabilities are collected from System Server.
+     *
+     * Callbacks from this controllers will notify if the state changes.
      *
      * @return {@code true} if hotspot is supported (or we haven't been told it's not)
      * @see #addCallback
      */
     @Override
     public boolean isHotspotSupported() {
-        return mIsTetheringSupported && mHasTetherableWifiRegexs
-                && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser());
+        return mIsTetheringSupportedConfig && mIsTetheringSupported && mHasTetherableWifiRegexs
+                && UserManager.get(mContext).isUserAdmin(mUserTracker.getUserId());
     }
 
     public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index dc73d1f..f63d652 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -36,6 +36,7 @@
 import com.android.keyguard.dagger.KeyguardUserSwitcherScope;
 import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -190,7 +191,8 @@
             mUiEventLogger.log(
                     LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP);
 
-            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground);
+            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(),
+                    Expandable.fromView(mUserAvatarViewWithBackground));
         });
 
         mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index da6d455..dd400b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
 import android.view.View;
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
+import android.view.ViewRootImpl;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsController;
@@ -61,6 +62,8 @@
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -88,6 +91,7 @@
  */
 public class RemoteInputView extends LinearLayout implements View.OnClickListener {
 
+    private static final boolean DEBUG = false;
     private static final String TAG = "RemoteInput";
 
     // A marker object that let's us easily find views of this class.
@@ -124,6 +128,7 @@
     // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
     //  that need the controller shouldn't have access to the view
     private RemoteInputViewController mViewController;
+    private ViewRootImpl mTestableViewRootImpl;
 
     /**
      * Enum for logged notification remote input UiEvents.
@@ -430,10 +435,20 @@
         }
     }
 
+    @VisibleForTesting
+    protected void setViewRootImpl(ViewRootImpl viewRoot) {
+        mTestableViewRootImpl = viewRoot;
+    }
+
+    @VisibleForTesting
+    protected void setEditTextReferenceToSelf() {
+        mEditText.mRemoteInputView = this;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mEditText.mRemoteInputView = this;
+        setEditTextReferenceToSelf();
         mEditText.setOnEditorActionListener(mEditorActionHandler);
         mEditText.addTextChangedListener(mTextWatcher);
         if (mEntry.getRow().isChangingPosition()) {
@@ -457,7 +472,50 @@
     }
 
     @Override
+    public ViewRootImpl getViewRootImpl() {
+        if (mTestableViewRootImpl != null) {
+            return mTestableViewRootImpl;
+        }
+        return super.getViewRootImpl();
+    }
+
+    private void registerBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "registering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback);
+    }
+
+    private void unregisterBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "unregistering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(
+                mEditText.mOnBackInvokedCallback);
+    }
+
+    @Override
     public void onVisibilityAggregated(boolean isVisible) {
+        if (isVisible) {
+            registerBackCallback();
+        } else {
+            unregisterBackCallback();
+        }
         super.onVisibilityAggregated(isVisible);
         mEditText.setEnabled(isVisible && !mSending);
     }
@@ -822,10 +880,21 @@
             return super.onKeyDown(keyCode, event);
         }
 
+        private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+            if (DEBUG) {
+                Log.d(TAG, "Predictive Back Callback dispatched");
+            }
+            respondToKeycodeBack();
+        };
+
+        private void respondToKeycodeBack() {
+            defocusIfNeeded(true /* animate */);
+        }
+
         @Override
         public boolean onKeyUp(int keyCode, KeyEvent event) {
             if (keyCode == KeyEvent.KEYCODE_BACK) {
-                defocusIfNeeded(true /* animate */);
+                respondToKeycodeBack();
                 return true;
             }
             return super.onKeyUp(keyCode, event);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index cc241d9..ba94714 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -16,7 +16,6 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManager.DeviceOwnerType;
@@ -55,8 +54,9 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -70,7 +70,7 @@
 /**
  */
 @SysUISingleton
-public class SecurityControllerImpl extends CurrentUserTracker implements SecurityController {
+public class SecurityControllerImpl implements SecurityController {
 
     private static final String TAG = "SecurityController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -84,11 +84,13 @@
     private static final int CA_CERT_LOADING_RETRY_TIME_IN_MS = 30_000;
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final ConnectivityManager mConnectivityManager;
     private final VpnManager mVpnManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final PackageManager mPackageManager;
     private final UserManager mUserManager;
+    private final Executor mMainExecutor;
     private final Executor mBgExecutor;
 
     @GuardedBy("mCallbacks")
@@ -102,18 +104,28 @@
     // Needs to be cached here since the query has to be asynchronous
     private ArrayMap<Integer, Boolean> mHasCACerts = new ArrayMap<Integer, Boolean>();
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    onUserSwitched(newUser);
+                }
+            };
+
     /**
      */
     @Inject
     public SecurityControllerImpl(
             Context context,
+            UserTracker userTracker,
             @Background Handler bgHandler,
             BroadcastDispatcher broadcastDispatcher,
+            @Main Executor mainExecutor,
             @Background Executor bgExecutor,
             DumpManager dumpManager
     ) {
-        super(broadcastDispatcher);
         mContext = context;
+        mUserTracker = userTracker;
         mDevicePolicyManager = (DevicePolicyManager)
                 context.getSystemService(Context.DEVICE_POLICY_SERVICE);
         mConnectivityManager = (ConnectivityManager)
@@ -121,6 +133,7 @@
         mVpnManager = context.getSystemService(VpnManager.class);
         mPackageManager = context.getPackageManager();
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mMainExecutor = mainExecutor;
         mBgExecutor = bgExecutor;
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
@@ -133,8 +146,8 @@
 
         // TODO: re-register network callback on user change.
         mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
-        onUserSwitched(ActivityManager.getCurrentUser());
-        startTracking();
+        onUserSwitched(mUserTracker.getUserId());
+        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
     }
 
     public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
index 29285f8..a593d51 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -28,7 +27,6 @@
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.ContactsContract;
@@ -40,8 +38,11 @@
 import com.android.settingslib.drawable.UserIconDrawable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.settings.UserTracker;
 
 import java.util.ArrayList;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -53,6 +54,7 @@
     private static final String TAG = "UserInfoController";
 
     private final Context mContext;
+    private final UserTracker mUserTracker;
     private final ArrayList<OnUserInfoChangedListener> mCallbacks =
             new ArrayList<OnUserInfoChangedListener>();
     private AsyncTask<Void, Void, UserInfoQueryResult> mUserInfoTask;
@@ -64,11 +66,11 @@
     /**
      */
     @Inject
-    public UserInfoControllerImpl(Context context) {
+    public UserInfoControllerImpl(Context context, @Main Executor mainExecutor,
+            UserTracker userTracker) {
         mContext = context;
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_USER_SWITCHED);
-        mContext.registerReceiver(mReceiver, filter);
+        mUserTracker = userTracker;
+        mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
 
         IntentFilter profileFilter = new IntentFilter();
         profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
@@ -88,15 +90,13 @@
         mCallbacks.remove(callback);
     }
 
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            if (Intent.ACTION_USER_SWITCHED.equals(action)) {
-                reloadUserInfo();
-            }
-        }
-    };
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, @NonNull Context userContext) {
+                    reloadUserInfo();
+                }
+            };
 
     private final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() {
         @Override
@@ -104,15 +104,11 @@
             final String action = intent.getAction();
             if (ContactsContract.Intents.ACTION_PROFILE_CHANGED.equals(action) ||
                     Intent.ACTION_USER_INFO_CHANGED.equals(action)) {
-                try {
-                    final int currentUser = ActivityManager.getService().getCurrentUser().id;
-                    final int changedUser =
-                            intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId());
-                    if (changedUser == currentUser) {
-                        reloadUserInfo();
-                    }
-                } catch (RemoteException e) {
-                    Log.e(TAG, "Couldn't get current user id for profile change", e);
+                final int currentUser = mUserTracker.getUserId();
+                final int changedUser =
+                        intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId());
+                if (changedUser == currentUser) {
+                    reloadUserInfo();
                 }
             }
         }
@@ -130,15 +126,12 @@
         Context currentUserContext;
         UserInfo userInfo;
         try {
-            userInfo = ActivityManager.getService().getCurrentUser();
+            userInfo = mUserTracker.getUserInfo();
             currentUserContext = mContext.createPackageContextAsUser("android", 0,
                     new UserHandle(userInfo.id));
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Couldn't create user context", e);
             throw new RuntimeException(e);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Couldn't get user info", e);
-            throw new RuntimeException(e);
         }
         final int userId = userInfo.id;
         final boolean isGuest = userInfo.isGuest();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
index 146b222..bdb656b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -14,35 +14,74 @@
  * limitations under the License.
  *
  */
+
 package com.android.systemui.statusbar.policy
 
-import android.annotation.UserIdInt
+import android.content.Context
 import android.content.Intent
 import android.view.View
-import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
 import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
+import dagger.Lazy
+import java.io.PrintWriter
 import java.lang.ref.WeakReference
-import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
 
-/** Defines interface for a class that provides user switching functionality and state. */
-interface UserSwitcherController : Dumpable {
+/** Access point into multi-user switching logic. */
+@Deprecated("Use UserInteractor or GuestUserInteractor instead.")
+@SysUISingleton
+class UserSwitcherController
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    private val userInteractorLazy: Lazy<UserInteractor>,
+    private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
+    private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
+    private val activityStarter: ActivityStarter,
+) {
+
+    /** Defines interface for classes that can be called back when the user is switched. */
+    fun interface UserSwitchCallback {
+        /** Notifies that the user has switched. */
+        fun onUserSwitched()
+    }
+
+    private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
+    private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
+    private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
+
+    private val callbackCompatMap = mutableMapOf<UserSwitchCallback, UserInteractor.UserCallback>()
 
     /** The current list of [UserRecord]. */
     val users: ArrayList<UserRecord>
+        get() = userInteractor.userRecords.value
 
     /** Whether the user switcher experience should use the simple experience. */
     val isSimpleUserSwitcher: Boolean
-
-    /** Require a view for jank detection */
-    fun init(view: View)
+        get() = userInteractor.isSimpleUserSwitcher
 
     /** The [UserRecord] of the current user or `null` when none. */
     val currentUserRecord: UserRecord?
+        get() = userInteractor.selectedUserRecord.value
 
     /** The name of the current user of the device or `null`, when none is selected. */
     val currentUserName: String?
+        get() =
+            currentUserRecord?.let {
+                LegacyUserUiHelper.getUserRecordName(
+                    context = applicationContext,
+                    record = it,
+                    isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+                    isGuestUserResetting = userInteractor.isGuestUserResetting,
+                )
+            }
 
     /**
      * Notifies that a user has been selected.
@@ -55,34 +94,40 @@
      * @param userId The ID of the user to switch to.
      * @param dialogShower An optional [DialogShower] in case we need to show dialogs.
      */
-    fun onUserSelected(userId: Int, dialogShower: DialogShower?)
-
-    /** Whether it is allowed to add users while the device is locked. */
-    val isAddUsersFromLockScreenEnabled: Flow<Boolean>
+    fun onUserSelected(userId: Int, dialogShower: DialogShower?) {
+        userInteractor.selectUser(userId, dialogShower)
+    }
 
     /** Whether the guest user is configured to always be present on the device. */
     val isGuestUserAutoCreated: Boolean
+        get() = userInteractor.isGuestUserAutoCreated
 
     /** Whether the guest user is currently being reset. */
     val isGuestUserResetting: Boolean
-
-    /** Creates and switches to the guest user. */
-    fun createAndSwitchToGuestUser(dialogShower: DialogShower?)
-
-    /** Shows the add user dialog. */
-    fun showAddUserDialog(dialogShower: DialogShower?)
-
-    /** Starts an activity to add a supervised user to the device. */
-    fun startSupervisedUserActivity()
-
-    /** Notifies when the display density or font scale has changed. */
-    fun onDensityOrFontScaleChanged()
+        get() = userInteractor.isGuestUserResetting
 
     /** Registers an adapter to notify when the users change. */
-    fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>)
+    fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
+        userInteractor.addCallback(
+            object : UserInteractor.UserCallback {
+                override fun isEvictable(): Boolean {
+                    return adapter.get() == null
+                }
+
+                override fun onUserStateChanged() {
+                    adapter.get()?.notifyDataSetChanged()
+                }
+            }
+        )
+    }
 
     /** Notifies the item for a user has been clicked. */
-    fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?)
+    fun onUserListItemClicked(
+        record: UserRecord,
+        dialogShower: DialogShower?,
+    ) {
+        userInteractor.onRecordSelected(record, dialogShower)
+    }
 
     /**
      * Removes guest user and switches to target user. The guest must be the current user and its id
@@ -103,7 +148,12 @@
      * @param targetUserId id of the user to switch to after guest is removed. If
      * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user.
      */
-    fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int)
+    fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
+        userInteractor.removeGuestUser(
+            guestUserId = guestUserId,
+            targetUserId = targetUserId,
+        )
+    }
 
     /**
      * Exits guest user and switches to previous non-guest user. The guest must be the current user.
@@ -114,43 +164,58 @@
      * @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest
      * only if its ephemeral, else keep guest
      */
-    fun exitGuestUser(
-        @UserIdInt guestUserId: Int,
-        @UserIdInt targetUserId: Int,
-        forceRemoveGuestOnExit: Boolean
-    )
+    fun exitGuestUser(guestUserId: Int, targetUserId: Int, forceRemoveGuestOnExit: Boolean) {
+        userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
+    }
 
     /**
      * Guarantee guest is present only if the device is provisioned. Otherwise, create a content
      * observer to wait until the device is provisioned, then schedule the guest creation.
      */
-    fun schedulePostBootGuestCreation()
+    fun schedulePostBootGuestCreation() {
+        guestUserInteractor.onDeviceBootCompleted()
+    }
 
     /** Whether keyguard is showing. */
     val isKeyguardShowing: Boolean
+        get() = keyguardInteractor.isKeyguardShowing()
 
     /** Starts an activity with the given [Intent]. */
-    fun startActivity(intent: Intent)
+    fun startActivity(intent: Intent) {
+        activityStarter.startActivity(intent, /* dismissShade= */ true)
+    }
 
     /**
      * Refreshes users from UserManager.
      *
      * The pictures are only loaded if they have not been loaded yet.
-     *
-     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
      */
-    fun refreshUsers(forcePictureLoadForId: Int)
+    fun refreshUsers() {
+        userInteractor.refreshUsers()
+    }
 
     /** Adds a subscriber to when user switches. */
-    fun addUserSwitchCallback(callback: UserSwitchCallback)
+    fun addUserSwitchCallback(callback: UserSwitchCallback) {
+        val interactorCallback =
+            object : UserInteractor.UserCallback {
+                override fun onUserStateChanged() {
+                    callback.onUserSwitched()
+                }
+            }
+        callbackCompatMap[callback] = interactorCallback
+        userInteractor.addCallback(interactorCallback)
+    }
 
     /** Removes a previously-added subscriber. */
-    fun removeUserSwitchCallback(callback: UserSwitchCallback)
+    fun removeUserSwitchCallback(callback: UserSwitchCallback) {
+        val interactorCallback = callbackCompatMap.remove(callback)
+        if (interactorCallback != null) {
+            userInteractor.removeCallback(interactorCallback)
+        }
+    }
 
-    /** Defines interface for classes that can be called back when the user is switched. */
-    fun interface UserSwitchCallback {
-        /** Notifies that the user has switched. */
-        fun onUserSwitched()
+    fun dump(pw: PrintWriter, args: Array<out String>) {
+        userInteractor.dump(pw)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
deleted file mode 100644
index 935fc7f..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ /dev/null
@@ -1,299 +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.statusbar.policy
-
-import android.content.Context
-import android.content.Intent
-import android.view.View
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.qs.user.UserSwitchDialogController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.domain.interactor.GuestUserInteractor
-import com.android.systemui.user.domain.interactor.UserInteractor
-import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
-import dagger.Lazy
-import java.io.PrintWriter
-import java.lang.ref.WeakReference
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-
-/** Implementation of [UserSwitcherController]. */
-@SysUISingleton
-class UserSwitcherControllerImpl
-@Inject
-constructor(
-    @Application private val applicationContext: Context,
-    flags: FeatureFlags,
-    @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
-    private val userInteractorLazy: Lazy<UserInteractor>,
-    private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
-    private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
-    private val activityStarter: ActivityStarter,
-) : UserSwitcherController {
-
-    private val useInteractor: Boolean =
-        flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
-            !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
-    private val _oldImpl: UserSwitcherControllerOldImpl
-        get() = oldImpl.get()
-    private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
-    private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
-    private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
-
-    private val callbackCompatMap =
-        mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
-
-    private fun notSupported(): Nothing {
-        error("Not supported in the new implementation!")
-    }
-
-    override val users: ArrayList<UserRecord>
-        get() =
-            if (useInteractor) {
-                userInteractor.userRecords.value
-            } else {
-                _oldImpl.users
-            }
-
-    override val isSimpleUserSwitcher: Boolean
-        get() =
-            if (useInteractor) {
-                userInteractor.isSimpleUserSwitcher
-            } else {
-                _oldImpl.isSimpleUserSwitcher
-            }
-
-    override fun init(view: View) {
-        if (!useInteractor) {
-            _oldImpl.init(view)
-        }
-    }
-
-    override val currentUserRecord: UserRecord?
-        get() =
-            if (useInteractor) {
-                userInteractor.selectedUserRecord.value
-            } else {
-                _oldImpl.currentUserRecord
-            }
-
-    override val currentUserName: String?
-        get() =
-            if (useInteractor) {
-                currentUserRecord?.let {
-                    LegacyUserUiHelper.getUserRecordName(
-                        context = applicationContext,
-                        record = it,
-                        isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
-                        isGuestUserResetting = userInteractor.isGuestUserResetting,
-                    )
-                }
-            } else {
-                _oldImpl.currentUserName
-            }
-
-    override fun onUserSelected(
-        userId: Int,
-        dialogShower: UserSwitchDialogController.DialogShower?
-    ) {
-        if (useInteractor) {
-            userInteractor.selectUser(userId, dialogShower)
-        } else {
-            _oldImpl.onUserSelected(userId, dialogShower)
-        }
-    }
-
-    override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
-        get() =
-            if (useInteractor) {
-                notSupported()
-            } else {
-                _oldImpl.isAddUsersFromLockScreenEnabled
-            }
-
-    override val isGuestUserAutoCreated: Boolean
-        get() =
-            if (useInteractor) {
-                userInteractor.isGuestUserAutoCreated
-            } else {
-                _oldImpl.isGuestUserAutoCreated
-            }
-
-    override val isGuestUserResetting: Boolean
-        get() =
-            if (useInteractor) {
-                userInteractor.isGuestUserResetting
-            } else {
-                _oldImpl.isGuestUserResetting
-            }
-
-    override fun createAndSwitchToGuestUser(
-        dialogShower: UserSwitchDialogController.DialogShower?,
-    ) {
-        if (useInteractor) {
-            notSupported()
-        } else {
-            _oldImpl.createAndSwitchToGuestUser(dialogShower)
-        }
-    }
-
-    override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
-        if (useInteractor) {
-            notSupported()
-        } else {
-            _oldImpl.showAddUserDialog(dialogShower)
-        }
-    }
-
-    override fun startSupervisedUserActivity() {
-        if (useInteractor) {
-            notSupported()
-        } else {
-            _oldImpl.startSupervisedUserActivity()
-        }
-    }
-
-    override fun onDensityOrFontScaleChanged() {
-        if (!useInteractor) {
-            _oldImpl.onDensityOrFontScaleChanged()
-        }
-    }
-
-    override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
-        if (useInteractor) {
-            userInteractor.addCallback(
-                object : UserInteractor.UserCallback {
-                    override fun isEvictable(): Boolean {
-                        return adapter.get() == null
-                    }
-
-                    override fun onUserStateChanged() {
-                        adapter.get()?.notifyDataSetChanged()
-                    }
-                }
-            )
-        } else {
-            _oldImpl.addAdapter(adapter)
-        }
-    }
-
-    override fun onUserListItemClicked(
-        record: UserRecord,
-        dialogShower: UserSwitchDialogController.DialogShower?,
-    ) {
-        if (useInteractor) {
-            userInteractor.onRecordSelected(record, dialogShower)
-        } else {
-            _oldImpl.onUserListItemClicked(record, dialogShower)
-        }
-    }
-
-    override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
-        if (useInteractor) {
-            userInteractor.removeGuestUser(
-                guestUserId = guestUserId,
-                targetUserId = targetUserId,
-            )
-        } else {
-            _oldImpl.removeGuestUser(guestUserId, targetUserId)
-        }
-    }
-
-    override fun exitGuestUser(
-        guestUserId: Int,
-        targetUserId: Int,
-        forceRemoveGuestOnExit: Boolean
-    ) {
-        if (useInteractor) {
-            userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
-        } else {
-            _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
-        }
-    }
-
-    override fun schedulePostBootGuestCreation() {
-        if (useInteractor) {
-            guestUserInteractor.onDeviceBootCompleted()
-        } else {
-            _oldImpl.schedulePostBootGuestCreation()
-        }
-    }
-
-    override val isKeyguardShowing: Boolean
-        get() =
-            if (useInteractor) {
-                keyguardInteractor.isKeyguardShowing()
-            } else {
-                _oldImpl.isKeyguardShowing
-            }
-
-    override fun startActivity(intent: Intent) {
-        if (useInteractor) {
-            activityStarter.startActivity(intent, /* dismissShade= */ true)
-        } else {
-            _oldImpl.startActivity(intent)
-        }
-    }
-
-    override fun refreshUsers(forcePictureLoadForId: Int) {
-        if (useInteractor) {
-            userInteractor.refreshUsers()
-        } else {
-            _oldImpl.refreshUsers(forcePictureLoadForId)
-        }
-    }
-
-    override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
-        if (useInteractor) {
-            val interactorCallback =
-                object : UserInteractor.UserCallback {
-                    override fun onUserStateChanged() {
-                        callback.onUserSwitched()
-                    }
-                }
-            callbackCompatMap[callback] = interactorCallback
-            userInteractor.addCallback(interactorCallback)
-        } else {
-            _oldImpl.addUserSwitchCallback(callback)
-        }
-    }
-
-    override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
-        if (useInteractor) {
-            val interactorCallback = callbackCompatMap.remove(callback)
-            if (interactorCallback != null) {
-                userInteractor.removeCallback(interactorCallback)
-            }
-        } else {
-            _oldImpl.removeUserSwitchCallback(callback)
-        }
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        if (useInteractor) {
-            userInteractor.dump(pw)
-        } else {
-            _oldImpl.dump(pw, args)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
deleted file mode 100644
index c294c37..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ /dev/null
@@ -1,1063 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.statusbar.policy;
-
-import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
-
-import android.annotation.UserIdInt;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.IActivityManager;
-import android.app.admin.DevicePolicyManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.UserInfo;
-import android.database.ContentObserver;
-import android.graphics.Bitmap;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.telephony.TelephonyCallback;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.view.View;
-import android.view.WindowManagerGlobal;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.util.LatencyTracker;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.settingslib.users.UserCreatingDialog;
-import com.android.systemui.GuestResetOrExitSessionReceiver;
-import com.android.systemui.GuestResumeSessionReceiver;
-import com.android.systemui.SystemUISecondaryUserService;
-import com.android.systemui.animation.DialogCuj;
-import com.android.systemui.animation.DialogLaunchAnimator;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.LongRunning;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.qs.QSUserSwitcherEvent;
-import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.user.data.source.UserRecord;
-import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
-import com.android.systemui.user.shared.model.UserActionModel;
-import com.android.systemui.user.ui.dialog.AddUserDialog;
-import com.android.systemui.user.ui.dialog.ExitGuestDialog;
-import com.android.systemui.util.settings.GlobalSettings;
-import com.android.systemui.util.settings.SecureSettings;
-
-import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-import javax.inject.Inject;
-
-import kotlinx.coroutines.flow.Flow;
-import kotlinx.coroutines.flow.MutableStateFlow;
-import kotlinx.coroutines.flow.StateFlowKt;
-
-/**
- * Old implementation. Keeps a list of all users on the device for user switching.
- *
- * @deprecated This is the old implementation. Please depend on {@link UserSwitcherController}
- * instead.
- */
-@Deprecated
-@SysUISingleton
-public class UserSwitcherControllerOldImpl implements UserSwitcherController {
-
-    private static final String TAG = "UserSwitcherController";
-    private static final boolean DEBUG = false;
-    private static final String SIMPLE_USER_SWITCHER_GLOBAL_SETTING =
-            "lockscreenSimpleUserSwitcher";
-    private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000;
-
-    private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
-    private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000L;
-
-    private static final String INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user";
-    private static final String INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode";
-
-    protected final Context mContext;
-    protected final UserTracker mUserTracker;
-    protected final UserManager mUserManager;
-    private final ContentObserver mSettingsObserver;
-    private final ArrayList<WeakReference<BaseUserSwitcherAdapter>> mAdapters = new ArrayList<>();
-    @VisibleForTesting
-    final GuestResumeSessionReceiver mGuestResumeSessionReceiver;
-    @VisibleForTesting
-    final GuestResetOrExitSessionReceiver mGuestResetOrExitSessionReceiver;
-    private final KeyguardStateController mKeyguardStateController;
-    private final DeviceProvisionedController mDeviceProvisionedController;
-    private final DevicePolicyManager mDevicePolicyManager;
-    protected final Handler mHandler;
-    private final ActivityStarter mActivityStarter;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final BroadcastSender mBroadcastSender;
-    private final TelephonyListenerManager mTelephonyListenerManager;
-    private final InteractionJankMonitor mInteractionJankMonitor;
-    private final LatencyTracker mLatencyTracker;
-    private final DialogLaunchAnimator mDialogLaunchAnimator;
-
-    private ArrayList<UserRecord> mUsers = new ArrayList<>();
-    @VisibleForTesting
-    AlertDialog mExitGuestDialog;
-    @VisibleForTesting
-    Dialog mAddUserDialog;
-    private int mLastNonGuestUser = UserHandle.USER_SYSTEM;
-    private boolean mSimpleUserSwitcher;
-    // When false, there won't be any visual affordance to add a new user from the keyguard even if
-    // the user is unlocked
-    private final MutableStateFlow<Boolean> mAddUsersFromLockScreen =
-            StateFlowKt.MutableStateFlow(false);
-    private boolean mUserSwitcherEnabled;
-    @VisibleForTesting
-    boolean mPauseRefreshUsers;
-    private int mSecondaryUser = UserHandle.USER_NULL;
-    private Intent mSecondaryUserServiceIntent;
-    private SparseBooleanArray mForcePictureLoadForUserId = new SparseBooleanArray(2);
-    private final UiEventLogger mUiEventLogger;
-    private final IActivityManager mActivityManager;
-    private final Executor mBgExecutor;
-    private final Executor mUiExecutor;
-    private final Executor mLongRunningExecutor;
-    private final boolean mGuestUserAutoCreated;
-    private final AtomicBoolean mGuestIsResetting;
-    private final AtomicBoolean mGuestCreationScheduled;
-    private FalsingManager mFalsingManager;
-    @Nullable
-    private View mView;
-    private String mCreateSupervisedUserPackage;
-    private GlobalSettings mGlobalSettings;
-    private List<UserSwitchCallback> mUserSwitchCallbacks =
-            Collections.synchronizedList(new ArrayList<>());
-
-    @Inject
-    public UserSwitcherControllerOldImpl(
-            Context context,
-            IActivityManager activityManager,
-            UserManager userManager,
-            UserTracker userTracker,
-            KeyguardStateController keyguardStateController,
-            DeviceProvisionedController deviceProvisionedController,
-            DevicePolicyManager devicePolicyManager,
-            @Main Handler handler,
-            ActivityStarter activityStarter,
-            BroadcastDispatcher broadcastDispatcher,
-            BroadcastSender broadcastSender,
-            UiEventLogger uiEventLogger,
-            FalsingManager falsingManager,
-            TelephonyListenerManager telephonyListenerManager,
-            SecureSettings secureSettings,
-            GlobalSettings globalSettings,
-            @Background Executor bgExecutor,
-            @LongRunning Executor longRunningExecutor,
-            @Main Executor uiExecutor,
-            InteractionJankMonitor interactionJankMonitor,
-            LatencyTracker latencyTracker,
-            DumpManager dumpManager,
-            DialogLaunchAnimator dialogLaunchAnimator,
-            GuestResumeSessionReceiver guestResumeSessionReceiver,
-            GuestResetOrExitSessionReceiver guestResetOrExitSessionReceiver) {
-        mContext = context;
-        mActivityManager = activityManager;
-        mUserTracker = userTracker;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mBroadcastSender = broadcastSender;
-        mTelephonyListenerManager = telephonyListenerManager;
-        mUiEventLogger = uiEventLogger;
-        mFalsingManager = falsingManager;
-        mInteractionJankMonitor = interactionJankMonitor;
-        mLatencyTracker = latencyTracker;
-        mGlobalSettings = globalSettings;
-        mGuestResumeSessionReceiver = guestResumeSessionReceiver;
-        mGuestResetOrExitSessionReceiver = guestResetOrExitSessionReceiver;
-        mBgExecutor = bgExecutor;
-        mLongRunningExecutor = longRunningExecutor;
-        mUiExecutor = uiExecutor;
-        mGuestResumeSessionReceiver.register();
-        mGuestResetOrExitSessionReceiver.register();
-        mGuestUserAutoCreated = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_guestUserAutoCreated);
-        mGuestIsResetting = new AtomicBoolean();
-        mGuestCreationScheduled = new AtomicBoolean();
-        mKeyguardStateController = keyguardStateController;
-        mDeviceProvisionedController = deviceProvisionedController;
-        mDevicePolicyManager = devicePolicyManager;
-        mHandler = handler;
-        mActivityStarter = activityStarter;
-        mUserManager = userManager;
-        mDialogLaunchAnimator = dialogLaunchAnimator;
-
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_USER_ADDED);
-        filter.addAction(Intent.ACTION_USER_REMOVED);
-        filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
-        filter.addAction(Intent.ACTION_USER_SWITCHED);
-        filter.addAction(Intent.ACTION_USER_STOPPED);
-        filter.addAction(Intent.ACTION_USER_UNLOCKED);
-        mBroadcastDispatcher.registerReceiver(
-                mReceiver, filter, null /* executor */,
-                UserHandle.SYSTEM, Context.RECEIVER_EXPORTED, null /* permission */);
-
-        mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
-
-        mSecondaryUserServiceIntent = new Intent(context, SystemUISecondaryUserService.class);
-
-        filter = new IntentFilter();
-        mContext.registerReceiverAsUser(mReceiver, UserHandle.SYSTEM, filter,
-                PERMISSION_SELF, null /* scheduler */,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
-
-        mSettingsObserver = new ContentObserver(mHandler) {
-            @Override
-            public void onChange(boolean selfChange) {
-                mSimpleUserSwitcher = shouldUseSimpleUserSwitcher();
-                mAddUsersFromLockScreen.setValue(
-                        mGlobalSettings.getIntForUser(
-                                Settings.Global.ADD_USERS_WHEN_LOCKED,
-                                0,
-                                UserHandle.USER_SYSTEM) != 0);
-                mUserSwitcherEnabled = mGlobalSettings.getIntForUser(
-                        Settings.Global.USER_SWITCHER_ENABLED, 0, UserHandle.USER_SYSTEM) != 0;
-                refreshUsers(UserHandle.USER_NULL);
-            };
-        };
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(SIMPLE_USER_SWITCHER_GLOBAL_SETTING), true,
-                mSettingsObserver);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.USER_SWITCHER_ENABLED), true,
-                mSettingsObserver);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.ADD_USERS_WHEN_LOCKED), true,
-                mSettingsObserver);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(
-                        Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED),
-                true, mSettingsObserver);
-        // Fetch initial values.
-        mSettingsObserver.onChange(false);
-
-        keyguardStateController.addCallback(mCallback);
-        listenForCallState();
-
-        mCreateSupervisedUserPackage = mContext.getString(
-                com.android.internal.R.string.config_supervisedUserCreationPackage);
-
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-
-        refreshUsers(UserHandle.USER_NULL);
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public void refreshUsers(int forcePictureLoadForId) {
-        if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId + ")");
-        if (forcePictureLoadForId != UserHandle.USER_NULL) {
-            mForcePictureLoadForUserId.put(forcePictureLoadForId, true);
-        }
-
-        if (mPauseRefreshUsers) {
-            return;
-        }
-
-        boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL);
-        SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size());
-        final int userCount = mUsers.size();
-        for (int i = 0; i < userCount; i++) {
-            UserRecord r = mUsers.get(i);
-            if (r == null || r.picture == null || r.info == null || forceAllUsers
-                    || mForcePictureLoadForUserId.get(r.info.id)) {
-                continue;
-            }
-            bitmaps.put(r.info.id, r.picture);
-        }
-        mForcePictureLoadForUserId.clear();
-
-        mBgExecutor.execute(() ->  {
-            List<UserInfo> infos = mUserManager.getAliveUsers();
-            if (infos == null) {
-                return;
-            }
-            ArrayList<UserRecord> records = new ArrayList<>(infos.size());
-            int currentId = mUserTracker.getUserId();
-            // Check user switchability of the foreground user since SystemUI is running in
-            // User 0
-            boolean canSwitchUsers = mUserManager.getUserSwitchability(
-                    UserHandle.of(mUserTracker.getUserId())) == SWITCHABILITY_STATUS_OK;
-            UserRecord guestRecord = null;
-
-            for (UserInfo info : infos) {
-                boolean isCurrent = currentId == info.id;
-                if (!mUserSwitcherEnabled && !info.isPrimary()) {
-                    continue;
-                }
-
-                if (info.isEnabled()) {
-                    if (info.isGuest()) {
-                        // Tapping guest icon triggers remove and a user switch therefore
-                        // the icon shouldn't be enabled even if the user is current
-                        guestRecord = LegacyUserDataHelper.createRecord(
-                                mContext,
-                                mUserManager,
-                                null /* picture */,
-                                info,
-                                isCurrent,
-                                canSwitchUsers);
-                    } else if (info.supportsSwitchToByUser()) {
-                        records.add(
-                                LegacyUserDataHelper.createRecord(
-                                        mContext,
-                                        mUserManager,
-                                        bitmaps.get(info.id),
-                                        info,
-                                        isCurrent,
-                                        canSwitchUsers));
-                    }
-                }
-            }
-
-            if (guestRecord == null) {
-                if (mGuestUserAutoCreated) {
-                    // If mGuestIsResetting=true, the switch should be disabled since
-                    // we will just use it as an indicator for "Resetting guest...".
-                    // Otherwise, default to canSwitchUsers.
-                    boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
-                    guestRecord = LegacyUserDataHelper.createRecord(
-                            mContext,
-                            currentId,
-                            UserActionModel.ENTER_GUEST_MODE,
-                            false /* isRestricted */,
-                            isSwitchToGuestEnabled);
-                    records.add(guestRecord);
-                } else if (canCreateGuest(guestRecord != null)) {
-                    guestRecord = LegacyUserDataHelper.createRecord(
-                            mContext,
-                            currentId,
-                            UserActionModel.ENTER_GUEST_MODE,
-                            false /* isRestricted */,
-                            canSwitchUsers);
-                    records.add(guestRecord);
-                }
-            } else {
-                records.add(guestRecord);
-            }
-
-            if (canCreateUser()) {
-                final UserRecord userRecord = LegacyUserDataHelper.createRecord(
-                        mContext,
-                        currentId,
-                        UserActionModel.ADD_USER,
-                        createIsRestricted(),
-                        canSwitchUsers);
-                records.add(userRecord);
-            }
-
-            if (canCreateSupervisedUser()) {
-                final UserRecord userRecord = LegacyUserDataHelper.createRecord(
-                        mContext,
-                        currentId,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        createIsRestricted(),
-                        canSwitchUsers);
-                records.add(userRecord);
-            }
-
-            if (canManageUsers()) {
-                records.add(LegacyUserDataHelper.createRecord(
-                        mContext,
-                        KeyguardUpdateMonitor.getCurrentUser(),
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                        /* isRestricted= */ false,
-                        /* isSwitchToEnabled= */ true
-                ));
-            }
-
-            mUiExecutor.execute(() -> {
-                if (records != null) {
-                    mUsers = records;
-                    notifyAdapters();
-                }
-            });
-        });
-    }
-
-    private boolean systemCanCreateUsers() {
-        return !mUserManager.hasBaseUserRestriction(
-                UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM);
-    }
-
-    private boolean currentUserCanCreateUsers() {
-        UserInfo currentUser = mUserTracker.getUserInfo();
-        return currentUser != null
-                && (currentUser.isAdmin() || mUserTracker.getUserId() == UserHandle.USER_SYSTEM)
-                && systemCanCreateUsers();
-    }
-
-    private boolean anyoneCanCreateUsers() {
-        return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue();
-    }
-
-    @VisibleForTesting
-    boolean canCreateGuest(boolean hasExistingGuest) {
-        return mUserSwitcherEnabled
-                && (currentUserCanCreateUsers() || anyoneCanCreateUsers())
-                && !hasExistingGuest;
-    }
-
-    @VisibleForTesting
-    boolean canCreateUser() {
-        return mUserSwitcherEnabled
-                && (currentUserCanCreateUsers() || anyoneCanCreateUsers())
-                && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY);
-    }
-
-    @VisibleForTesting
-    boolean canManageUsers() {
-        UserInfo currentUser = mUserTracker.getUserInfo();
-        return mUserSwitcherEnabled
-                && ((currentUser != null && currentUser.isAdmin())
-                || mAddUsersFromLockScreen.getValue());
-    }
-
-    private boolean createIsRestricted() {
-        return !mAddUsersFromLockScreen.getValue();
-    }
-
-    @VisibleForTesting
-    boolean canCreateSupervisedUser() {
-        return !TextUtils.isEmpty(mCreateSupervisedUserPackage) && canCreateUser();
-    }
-
-    private void pauseRefreshUsers() {
-        if (!mPauseRefreshUsers) {
-            mHandler.postDelayed(mUnpauseRefreshUsers, PAUSE_REFRESH_USERS_TIMEOUT_MS);
-            mPauseRefreshUsers = true;
-        }
-    }
-
-    private void notifyAdapters() {
-        for (int i = mAdapters.size() - 1; i >= 0; i--) {
-            BaseUserSwitcherAdapter adapter = mAdapters.get(i).get();
-            if (adapter != null) {
-                adapter.notifyDataSetChanged();
-            } else {
-                mAdapters.remove(i);
-            }
-        }
-    }
-
-    @Override
-    public boolean isSimpleUserSwitcher() {
-        return mSimpleUserSwitcher;
-    }
-
-    /**
-     * Returns whether the current user is a system user.
-     */
-    @VisibleForTesting
-    boolean isSystemUser() {
-        return mUserTracker.getUserId() == UserHandle.USER_SYSTEM;
-    }
-
-    @Override
-    public @Nullable UserRecord getCurrentUserRecord() {
-        for (int i = 0; i < mUsers.size(); ++i) {
-            UserRecord userRecord = mUsers.get(i);
-            if (userRecord.isCurrent) {
-                return userRecord;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void onUserSelected(int userId, @Nullable DialogShower dialogShower) {
-        UserRecord userRecord = mUsers.stream()
-                .filter(x -> x.resolveId() == userId)
-                .findFirst()
-                .orElse(null);
-        if (userRecord == null) {
-            return;
-        }
-
-        onUserListItemClicked(userRecord, dialogShower);
-    }
-
-    @Override
-    public Flow<Boolean> isAddUsersFromLockScreenEnabled() {
-        return mAddUsersFromLockScreen;
-    }
-
-    @Override
-    public boolean isGuestUserAutoCreated() {
-        return mGuestUserAutoCreated;
-    }
-
-    @Override
-    public boolean isGuestUserResetting() {
-        return mGuestIsResetting.get();
-    }
-
-    @Override
-    public void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
-        if (record.isGuest && record.info == null) {
-            createAndSwitchToGuestUser(dialogShower);
-        } else if (record.isAddUser) {
-            showAddUserDialog(dialogShower);
-        } else if (record.isAddSupervisedUser) {
-            startSupervisedUserActivity();
-        } else if (record.isManageUsers) {
-            startActivity(new Intent(Settings.ACTION_USER_SETTINGS));
-        } else {
-            onUserListItemClicked(record.info.id, record, dialogShower);
-        }
-    }
-
-    private void onUserListItemClicked(int id, UserRecord record, DialogShower dialogShower) {
-        int currUserId = mUserTracker.getUserId();
-        // If switching from guest and guest is ephemeral, then follow the flow
-        // of showExitGuestDialog to remove current guest,
-        // and switch to selected user
-        UserInfo currUserInfo = mUserTracker.getUserInfo();
-        if (currUserId == id) {
-            if (record.isGuest) {
-                showExitGuestDialog(id, currUserInfo.isEphemeral(), dialogShower);
-            }
-            return;
-        }
-
-        if (currUserInfo != null && currUserInfo.isGuest()) {
-            showExitGuestDialog(currUserId, currUserInfo.isEphemeral(),
-                    record.resolveId(), dialogShower);
-            return;
-        }
-
-        if (dialogShower != null) {
-            // If we haven't morphed into another dialog, it means we have just switched users.
-            // Then, dismiss the dialog.
-            dialogShower.dismiss();
-        }
-        switchToUserId(id);
-    }
-
-    private void switchToUserId(int id) {
-        try {
-            if (mView != null) {
-                mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
-                        .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView)
-                        .setTimeout(MULTI_USER_JOURNEY_TIMEOUT));
-            }
-            mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH);
-            pauseRefreshUsers();
-            mActivityManager.switchUser(id);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Couldn't switch user.", e);
-        }
-    }
-
-    private void showExitGuestDialog(int id, boolean isGuestEphemeral, DialogShower dialogShower) {
-        int newId = UserHandle.USER_SYSTEM;
-        if (mLastNonGuestUser != UserHandle.USER_SYSTEM) {
-            UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
-            if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
-                newId = info.id;
-            }
-        }
-        showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
-    }
-
-    private void showExitGuestDialog(
-            int id,
-            boolean isGuestEphemeral,
-            int targetId,
-            DialogShower dialogShower) {
-        if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
-            mExitGuestDialog.cancel();
-        }
-        mExitGuestDialog = new ExitGuestDialog(
-                mContext,
-                id,
-                isGuestEphemeral,
-                targetId,
-                mKeyguardStateController.isShowing(),
-                mFalsingManager,
-                mDialogLaunchAnimator,
-                this::exitGuestUser);
-        if (dialogShower != null) {
-            dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
-                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
-                    INTERACTION_JANK_EXIT_GUEST_MODE_TAG));
-        } else {
-            mExitGuestDialog.show();
-        }
-    }
-
-    @Override
-    public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) {
-        createGuestAsync(guestId -> {
-            // guestId may be USER_NULL if we haven't reloaded the user list yet.
-            if (guestId != UserHandle.USER_NULL) {
-                mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD);
-                onUserListItemClicked(guestId, UserRecord.createForGuest(), dialogShower);
-            }
-        });
-    }
-
-    @Override
-    public void showAddUserDialog(@Nullable DialogShower dialogShower) {
-        if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
-            mAddUserDialog.cancel();
-        }
-        final UserInfo currentUser = mUserTracker.getUserInfo();
-        mAddUserDialog = new AddUserDialog(
-                mContext,
-                currentUser.getUserHandle(),
-                mKeyguardStateController.isShowing(),
-                /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
-                mFalsingManager,
-                mBroadcastSender,
-                mDialogLaunchAnimator);
-        if (dialogShower != null) {
-            dialogShower.showDialog(mAddUserDialog,
-                    new DialogCuj(
-                            InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
-                            INTERACTION_JANK_ADD_NEW_USER_TAG
-                    ));
-        } else {
-            mAddUserDialog.show();
-        }
-    }
-
-    @Override
-    public void startSupervisedUserActivity() {
-        final Intent intent = new Intent()
-                .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
-                .setPackage(mCreateSupervisedUserPackage)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-        mContext.startActivity(intent);
-    }
-
-    private void listenForCallState() {
-        mTelephonyListenerManager.addCallStateListener(mPhoneStateListener);
-    }
-
-    private final TelephonyCallback.CallStateListener mPhoneStateListener =
-            new TelephonyCallback.CallStateListener() {
-        private int mCallState;
-
-        @Override
-        public void onCallStateChanged(int state) {
-            if (mCallState == state) return;
-            if (DEBUG) Log.v(TAG, "Call state changed: " + state);
-            mCallState = state;
-            refreshUsers(UserHandle.USER_NULL);
-        }
-    };
-
-    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (DEBUG) {
-                Log.v(TAG, "Broadcast: a=" + intent.getAction()
-                        + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
-            }
-
-            boolean unpauseRefreshUsers = false;
-            int forcePictureLoadForId = UserHandle.USER_NULL;
-
-            if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) {
-                if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
-                    mExitGuestDialog.cancel();
-                    mExitGuestDialog = null;
-                }
-
-                final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
-                final UserInfo userInfo = mUserManager.getUserInfo(currentId);
-                final int userCount = mUsers.size();
-                for (int i = 0; i < userCount; i++) {
-                    UserRecord record = mUsers.get(i);
-                    if (record.info == null) continue;
-                    boolean shouldBeCurrent = record.info.id == currentId;
-                    if (record.isCurrent != shouldBeCurrent) {
-                        mUsers.set(i, record.copyWithIsCurrent(shouldBeCurrent));
-                    }
-                    if (shouldBeCurrent && !record.isGuest) {
-                        mLastNonGuestUser = record.info.id;
-                    }
-                    if ((userInfo == null || !userInfo.isAdmin()) && record.isRestricted) {
-                        // Immediately remove restricted records in case the AsyncTask is too slow.
-                        mUsers.remove(i);
-                        i--;
-                    }
-                }
-                notifyUserSwitchCallbacks();
-                notifyAdapters();
-
-                // Disconnect from the old secondary user's service
-                if (mSecondaryUser != UserHandle.USER_NULL) {
-                    context.stopServiceAsUser(mSecondaryUserServiceIntent,
-                            UserHandle.of(mSecondaryUser));
-                    mSecondaryUser = UserHandle.USER_NULL;
-                }
-                // Connect to the new secondary user's service (purely to ensure that a persistent
-                // SystemUI application is created for that user)
-                if (userInfo != null && userInfo.id != UserHandle.USER_SYSTEM) {
-                    context.startServiceAsUser(mSecondaryUserServiceIntent,
-                            UserHandle.of(userInfo.id));
-                    mSecondaryUser = userInfo.id;
-                }
-                unpauseRefreshUsers = true;
-                if (mGuestUserAutoCreated) {
-                    // Guest user must be scheduled for creation AFTER switching to the target user.
-                    // This avoids lock contention which will produce UX bugs on the keyguard
-                    // (b/193933686).
-                    // TODO(b/191067027): Move guest user recreation to system_server
-                    guaranteeGuestPresent();
-                }
-            } else if (Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) {
-                forcePictureLoadForId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
-                        UserHandle.USER_NULL);
-            } else if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
-                // Unlocking the system user may require a refresh
-                int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
-                if (userId != UserHandle.USER_SYSTEM) {
-                    return;
-                }
-            }
-            refreshUsers(forcePictureLoadForId);
-            if (unpauseRefreshUsers) {
-                mUnpauseRefreshUsers.run();
-            }
-        }
-    };
-
-    private final Runnable mUnpauseRefreshUsers = new Runnable() {
-        @Override
-        public void run() {
-            mHandler.removeCallbacks(this);
-            mPauseRefreshUsers = false;
-            refreshUsers(UserHandle.USER_NULL);
-        }
-    };
-
-    @Override
-    public void dump(PrintWriter pw, String[] args) {
-        pw.println("UserSwitcherController state:");
-        pw.println("  mLastNonGuestUser=" + mLastNonGuestUser);
-        pw.print("  mUsers.size="); pw.println(mUsers.size());
-        for (int i = 0; i < mUsers.size(); i++) {
-            final UserRecord u = mUsers.get(i);
-            pw.print("    "); pw.println(u.toString());
-        }
-        pw.println("mSimpleUserSwitcher=" + mSimpleUserSwitcher);
-        pw.println("mGuestUserAutoCreated=" + mGuestUserAutoCreated);
-    }
-
-    @Override
-    public String getCurrentUserName() {
-        if (mUsers.isEmpty()) return null;
-        UserRecord item = mUsers.stream().filter(x -> x.isCurrent).findFirst().orElse(null);
-        if (item == null || item.info == null) return null;
-        if (item.isGuest) return mContext.getString(com.android.internal.R.string.guest_name);
-        return item.info.name;
-    }
-
-    @Override
-    public void onDensityOrFontScaleChanged() {
-        refreshUsers(UserHandle.USER_ALL);
-    }
-
-    @Override
-    public void addAdapter(WeakReference<BaseUserSwitcherAdapter> adapter) {
-        mAdapters.add(adapter);
-    }
-
-    @Override
-    public ArrayList<UserRecord> getUsers() {
-        return mUsers;
-    }
-
-    @Override
-    public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) {
-        UserInfo currentUser = mUserTracker.getUserInfo();
-        if (currentUser.id != guestUserId) {
-            Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")"
-                    + " is not current user (" + currentUser.id + ")");
-            return;
-        }
-        if (!currentUser.isGuest()) {
-            Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")"
-                    + " is not a guest");
-            return;
-        }
-
-        boolean marked = mUserManager.markGuestForDeletion(currentUser.id);
-        if (!marked) {
-            Log.w(TAG, "Couldn't mark the guest for deletion for user " + guestUserId);
-            return;
-        }
-
-        if (targetUserId == UserHandle.USER_NULL) {
-            // Create a new guest in the foreground, and then immediately switch to it
-            createGuestAsync(newGuestId -> {
-                if (newGuestId == UserHandle.USER_NULL) {
-                    Log.e(TAG, "Could not create new guest, switching back to system user");
-                    switchToUserId(UserHandle.USER_SYSTEM);
-                    mUserManager.removeUser(currentUser.id);
-                    try {
-                        WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Couldn't remove guest because ActivityManager "
-                                + "or WindowManager is dead");
-                    }
-                    return;
-                }
-                switchToUserId(newGuestId);
-                mUserManager.removeUser(currentUser.id);
-            });
-        } else {
-            if (mGuestUserAutoCreated) {
-                mGuestIsResetting.set(true);
-            }
-            switchToUserId(targetUserId);
-            mUserManager.removeUser(currentUser.id);
-        }
-    }
-
-    @Override
-    public void exitGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId,
-            boolean forceRemoveGuestOnExit) {
-        UserInfo currentUser = mUserTracker.getUserInfo();
-        if (currentUser.id != guestUserId) {
-            Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")"
-                    + " is not current user (" + currentUser.id + ")");
-            return;
-        }
-        if (!currentUser.isGuest()) {
-            Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")"
-                    + " is not a guest");
-            return;
-        }
-
-        int newUserId = UserHandle.USER_SYSTEM;
-        if (targetUserId == UserHandle.USER_NULL) {
-            // when target user is not specified switch to last non guest user
-            if (mLastNonGuestUser != UserHandle.USER_SYSTEM) {
-                UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
-                if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
-                    newUserId = info.id;
-                }
-            }
-        } else {
-            newUserId = targetUserId;
-        }
-
-        if (currentUser.isEphemeral() || forceRemoveGuestOnExit) {
-            mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE);
-            removeGuestUser(currentUser.id, newUserId);
-        } else {
-            mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
-            switchToUserId(newUserId);
-        }
-    }
-
-    private void scheduleGuestCreation() {
-        if (!mGuestCreationScheduled.compareAndSet(false, true)) {
-            return;
-        }
-
-        mLongRunningExecutor.execute(() -> {
-            int newGuestId = createGuest();
-            mGuestCreationScheduled.set(false);
-            mGuestIsResetting.set(false);
-            if (newGuestId == UserHandle.USER_NULL) {
-                Log.w(TAG, "Could not create new guest while exiting existing guest");
-                // Refresh users so that we still display "Guest" if
-                // config_guestUserAutoCreated=true
-                refreshUsers(UserHandle.USER_NULL);
-            }
-        });
-
-    }
-
-    @Override
-    public void schedulePostBootGuestCreation() {
-        if (isDeviceAllowedToAddGuest()) {
-            guaranteeGuestPresent();
-        } else {
-            mDeviceProvisionedController.addCallback(mGuaranteeGuestPresentAfterProvisioned);
-        }
-    }
-
-    private boolean isDeviceAllowedToAddGuest() {
-        return mDeviceProvisionedController.isDeviceProvisioned()
-                && !mDevicePolicyManager.isDeviceManaged();
-    }
-
-    /**
-     * If there is no guest on the device, schedule creation of a new guest user in the background.
-     */
-    private void guaranteeGuestPresent() {
-        if (isDeviceAllowedToAddGuest() && mUserManager.findCurrentGuestUser() == null) {
-            scheduleGuestCreation();
-        }
-    }
-
-    private void createGuestAsync(Consumer<Integer> callback) {
-        final Dialog guestCreationProgressDialog =
-                new UserCreatingDialog(mContext, /* isGuest= */true);
-        guestCreationProgressDialog.show();
-
-        // userManager.createGuest will block the thread so post is needed for the dialog to show
-        mBgExecutor.execute(() -> {
-            final int guestId = createGuest();
-            mUiExecutor.execute(() -> {
-                guestCreationProgressDialog.dismiss();
-                if (guestId == UserHandle.USER_NULL) {
-                    Toast.makeText(mContext,
-                            com.android.settingslib.R.string.add_guest_failed,
-                            Toast.LENGTH_SHORT).show();
-                }
-                callback.accept(guestId);
-            });
-        });
-    }
-
-    /**
-     * Creates a guest user and return its multi-user user ID.
-     *
-     * This method does not check if a guest already exists before it makes a call to
-     * {@link UserManager} to create a new one.
-     *
-     * @return The multi-user user ID of the newly created guest user, or
-     * {@link UserHandle#USER_NULL} if the guest couldn't be created.
-     */
-    private @UserIdInt int createGuest() {
-        UserInfo guest;
-        try {
-            guest = mUserManager.createGuest(mContext);
-        } catch (UserManager.UserOperationException e) {
-            Log.e(TAG, "Couldn't create guest user", e);
-            return UserHandle.USER_NULL;
-        }
-        if (guest == null) {
-            Log.e(TAG, "Couldn't create guest, most likely because there already exists one");
-            return UserHandle.USER_NULL;
-        }
-        return guest.id;
-    }
-
-    @Override
-    public void init(View view) {
-        mView = view;
-    }
-
-    @Override
-    public boolean isKeyguardShowing() {
-        return mKeyguardStateController.isShowing();
-    }
-
-    private boolean shouldUseSimpleUserSwitcher() {
-        int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
-        return mGlobalSettings.getIntForUser(SIMPLE_USER_SWITCHER_GLOBAL_SETTING,
-                defaultSimpleUserSwitcher, UserHandle.USER_SYSTEM) != 0;
-    }
-
-    @Override
-    public void startActivity(Intent intent) {
-        mActivityStarter.startActivity(intent, /* dismissShade= */ true);
-    }
-
-    @Override
-    public void addUserSwitchCallback(UserSwitchCallback callback) {
-        mUserSwitchCallbacks.add(callback);
-    }
-
-    @Override
-    public void removeUserSwitchCallback(UserSwitchCallback callback) {
-        mUserSwitchCallbacks.remove(callback);
-    }
-
-    /**
-     *  Notify user switch callbacks that user has switched.
-     */
-    private void notifyUserSwitchCallbacks() {
-        List<UserSwitchCallback> temp;
-        synchronized (mUserSwitchCallbacks) {
-            temp = new ArrayList<>(mUserSwitchCallbacks);
-        }
-        for (UserSwitchCallback callback : temp) {
-            callback.onUserSwitched();
-        }
-    }
-
-    private final KeyguardStateController.Callback mCallback =
-            new KeyguardStateController.Callback() {
-                @Override
-                public void onKeyguardShowingChanged() {
-
-                    // When Keyguard is going away, we don't need to update our items immediately
-                    // which
-                    // helps making the transition faster.
-                    if (!mKeyguardStateController.isShowing()) {
-                        mHandler.post(UserSwitcherControllerOldImpl.this::notifyAdapters);
-                    } else {
-                        notifyAdapters();
-                    }
-                }
-            };
-
-    private final DeviceProvisionedController.DeviceProvisionedListener
-            mGuaranteeGuestPresentAfterProvisioned =
-            new DeviceProvisionedController.DeviceProvisionedListener() {
-                @Override
-                public void onDeviceProvisionedChanged() {
-                    if (isDeviceAllowedToAddGuest()) {
-                        mBgExecutor.execute(
-                                () -> mDeviceProvisionedController.removeCallback(
-                                        mGuaranteeGuestPresentAfterProvisioned));
-                        guaranteeGuestPresent();
-                    }
-                }
-            };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index 9866389..b135d0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
@@ -28,6 +27,7 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings.Global;
@@ -46,7 +46,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.qs.SettingObserver;
-import com.android.systemui.settings.CurrentUserTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.Utils;
 import com.android.systemui.util.settings.GlobalSettings;
 
@@ -58,14 +58,15 @@
 
 /** Platform implementation of the zen mode controller. **/
 @SysUISingleton
-public class ZenModeControllerImpl extends CurrentUserTracker
-        implements ZenModeController, Dumpable {
+public class ZenModeControllerImpl implements ZenModeController, Dumpable {
     private static final String TAG = "ZenModeController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
     private final Object mCallbacksLock = new Object();
     private final Context mContext;
+    private final UserTracker mUserTracker;
+    private final BroadcastDispatcher mBroadcastDispatcher;
     private final SettingObserver mModeSetting;
     private final SettingObserver mConfigSetting;
     private final NotificationManager mNoMan;
@@ -80,23 +81,45 @@
     private long mZenUpdateTime;
     private NotificationManager.Policy mConsolidatedNotificationPolicy;
 
+    private final UserTracker.Callback mUserChangedCallback =
+            new UserTracker.Callback() {
+                @Override
+                public void onUserChanged(int newUser, Context userContext) {
+                    mUserId = newUser;
+                    if (mRegistered) {
+                        mBroadcastDispatcher.unregisterReceiver(mReceiver);
+                    }
+                    final IntentFilter filter = new IntentFilter(
+                            AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+                    filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
+                    mBroadcastDispatcher.registerReceiver(mReceiver, filter, null,
+                            UserHandle.of(mUserId));
+                    mRegistered = true;
+                    mSetupObserver.register();
+                }
+            };
+
     @Inject
     public ZenModeControllerImpl(
             Context context,
             @Main Handler handler,
             BroadcastDispatcher broadcastDispatcher,
             DumpManager dumpManager,
-            GlobalSettings globalSettings) {
-        super(broadcastDispatcher);
+            GlobalSettings globalSettings,
+            UserTracker userTracker) {
         mContext = context;
-        mModeSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE) {
+        mBroadcastDispatcher = broadcastDispatcher;
+        mUserTracker = userTracker;
+        mModeSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE,
+                userTracker.getUserId()) {
             @Override
             protected void handleValueChanged(int value, boolean observedChange) {
                 updateZenMode(value);
                 fireZenChanged(value);
             }
         };
-        mConfigSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE_CONFIG_ETAG) {
+        mConfigSetting = new SettingObserver(globalSettings, handler, Global.ZEN_MODE_CONFIG_ETAG,
+                userTracker.getUserId()) {
             @Override
             protected void handleValueChanged(int value, boolean observedChange) {
                 updateZenModeConfig();
@@ -112,7 +135,7 @@
         mSetupObserver = new SetupObserver(handler);
         mSetupObserver.register();
         mUserManager = context.getSystemService(UserManager.class);
-        startTracking();
+        mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler));
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
@@ -120,7 +143,7 @@
     @Override
     public boolean isVolumeRestricted() {
         return mUserManager.hasUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
-                new UserHandle(mUserId));
+                UserHandle.of(mUserId));
     }
 
     @Override
@@ -183,19 +206,6 @@
     }
 
     @Override
-    public void onUserSwitched(int userId) {
-        mUserId = userId;
-        if (mRegistered) {
-            mContext.unregisterReceiver(mReceiver);
-        }
-        final IntentFilter filter = new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
-        filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
-        mContext.registerReceiverAsUser(mReceiver, new UserHandle(mUserId), filter, null, null);
-        mRegistered = true;
-        mSetupObserver.register();
-    }
-
-    @Override
     public ComponentName getEffectsSuppressor() {
         return NotificationManager.from(mContext).getEffectsSuppressor();
     }
@@ -208,7 +218,7 @@
 
     @Override
     public int getCurrentUser() {
-        return ActivityManager.getCurrentUser();
+        return mUserTracker.getUserId();
     }
 
     private void fireNextAlarmChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index b1b45b5..1b73539 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -58,8 +58,6 @@
 import com.android.systemui.statusbar.policy.SecurityControllerImpl;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
-import com.android.systemui.statusbar.policy.UserSwitcherControllerImpl;
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.statusbar.policy.WalletControllerImpl;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -198,8 +196,4 @@
     static DataSaverController provideDataSaverController(NetworkController networkController) {
         return networkController.getDataSaverController();
     }
-
-    /** Binds {@link UserSwitcherController} to its implementation. */
-    @Binds
-    UserSwitcherController bindUserSwitcherController(UserSwitcherControllerImpl impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
new file mode 100644
index 0000000..93e78ac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import androidx.annotation.VisibleForTesting
+
+/** Controller that handles playing [RippleAnimation]. */
+class MultiRippleController(private val multipleRippleView: MultiRippleView) {
+
+    companion object {
+        /** Max number of ripple animations at a time. */
+        @VisibleForTesting const val MAX_RIPPLE_NUMBER = 10
+    }
+
+    /** Updates all the ripple colors during the animation. */
+    fun updateColor(color: Int) {
+        multipleRippleView.ripples.forEach { anim -> anim.updateColor(color) }
+    }
+
+    fun play(rippleAnimation: RippleAnimation) {
+        if (multipleRippleView.ripples.size >= MAX_RIPPLE_NUMBER) {
+            return
+        }
+
+        multipleRippleView.ripples.add(rippleAnimation)
+
+        // Remove ripple once the animation is done
+        rippleAnimation.play { multipleRippleView.ripples.remove(rippleAnimation) }
+
+        // Trigger drawing
+        multipleRippleView.invalidate()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
new file mode 100644
index 0000000..f558fee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+
+/**
+ * A view that allows multiple ripples to play.
+ *
+ * Use [MultiRippleController] to play ripple animations.
+ */
+class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+
+    internal val ripples = ArrayList<RippleAnimation>()
+    private val listeners = ArrayList<RipplesFinishedListener>()
+    private val ripplePaint = Paint()
+    private var isWarningLogged = false
+
+    companion object {
+        private const val TAG = "MultiRippleView"
+
+        interface RipplesFinishedListener {
+            /** Triggered when all the ripples finish running. */
+            fun onRipplesFinish()
+        }
+    }
+
+    fun addRipplesFinishedListener(listener: RipplesFinishedListener) {
+        listeners.add(listener)
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        if (canvas == null || !canvas.isHardwareAccelerated) {
+            // Drawing with the ripple shader requires hardware acceleration, so skip
+            // if it's unsupported.
+            if (!isWarningLogged) {
+                // Only log once to not spam.
+                Log.w(
+                    TAG,
+                    "Can't draw ripple shader. $canvas does not support hardware acceleration."
+                )
+                isWarningLogged = true
+            }
+            return
+        }
+
+        var shouldInvalidate = false
+
+        ripples.forEach { anim ->
+            ripplePaint.shader = anim.rippleShader
+            canvas.drawPaint(ripplePaint)
+
+            shouldInvalidate = shouldInvalidate || anim.isPlaying()
+        }
+
+        if (shouldInvalidate) {
+            invalidate()
+        } else { // Nothing is playing.
+            listeners.forEach { listener -> listener.onRipplesFinish() }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
new file mode 100644
index 0000000..b2f8994
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import androidx.core.graphics.ColorUtils
+
+/** A single ripple animation. */
+class RippleAnimation(private val config: RippleAnimationConfig) {
+    internal val rippleShader: RippleShader = RippleShader(config.rippleShape)
+    private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
+
+    init {
+        applyConfigToShader()
+    }
+
+    /** Updates the ripple color during the animation. */
+    fun updateColor(color: Int) {
+        config.apply { config.color = color }
+        applyConfigToShader()
+    }
+
+    @JvmOverloads
+    fun play(onAnimationEnd: Runnable? = null) {
+        if (isPlaying()) {
+            return // Ignore if ripple effect is already playing
+        }
+
+        animator.duration = config.duration
+        animator.addUpdateListener { updateListener ->
+            val now = updateListener.currentPlayTime
+            val progress = updateListener.animatedValue as Float
+            rippleShader.progress = progress
+            rippleShader.distortionStrength = if (config.shouldDistort) 1 - progress else 0f
+            rippleShader.time = now.toFloat()
+        }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    onAnimationEnd?.run()
+                }
+            }
+        )
+        animator.start()
+    }
+
+    /** Indicates whether the animation is playing. */
+    fun isPlaying(): Boolean = animator.isRunning
+
+    private fun applyConfigToShader() {
+        rippleShader.setCenter(config.centerX, config.centerY)
+        rippleShader.setMaxSize(config.maxWidth, config.maxHeight)
+        rippleShader.rippleFill = config.shouldFillRipple
+        rippleShader.pixelDensity = config.pixelDensity
+        rippleShader.color = ColorUtils.setAlphaComponent(config.color, config.opacity)
+        rippleShader.sparkleStrength = config.sparkleStrength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
new file mode 100644
index 0000000..773ac55
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
@@ -0,0 +1,32 @@
+package com.android.systemui.surfaceeffects.ripple
+
+import android.graphics.Color
+
+/**
+ * A struct that holds the ripple animation configurations.
+ *
+ * <p>This configuration is designed to play a SINGLE animation. Do not reuse or modify the
+ * configuration parameters to play different animations, unless the value has to change within the
+ * single animation (e.g. Change color or opacity during the animation). Note that this data class
+ * is pulled out to make the [RippleAnimation] constructor succinct.
+ */
+data class RippleAnimationConfig(
+    val rippleShape: RippleShader.RippleShape = RippleShader.RippleShape.CIRCLE,
+    val duration: Long = 0L,
+    val centerX: Float = 0f,
+    val centerY: Float = 0f,
+    val maxWidth: Float = 0f,
+    val maxHeight: Float = 0f,
+    val pixelDensity: Float = 1f,
+    var color: Int = Color.WHITE,
+    val opacity: Int = RIPPLE_DEFAULT_ALPHA,
+    val shouldFillRipple: Boolean = false,
+    val sparkleStrength: Float = RIPPLE_SPARKLE_STRENGTH,
+    val shouldDistort: Boolean = true
+) {
+    companion object {
+        const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
+        const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
+        const val RIPPLE_DEFAULT_ALPHA: Int = 115 // full opacity is 255.
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
new file mode 100644
index 0000000..a950d34
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -0,0 +1,240 @@
+/*
+ * 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.
+ */
+package com.android.systemui.surfaceeffects.ripple
+
+import android.graphics.PointF
+import android.graphics.RuntimeShader
+import android.util.MathUtils
+import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary
+import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
+
+/**
+ * Shader class that renders an expanding ripple effect. The ripple contains three elements:
+ *
+ * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away
+ * 2. an expanding ring that appears throughout the effect
+ * 3. an expanding ring-shaped area that reveals noise over #2.
+ *
+ * The ripple shader will be default to the circle shape if not specified.
+ *
+ * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
+ */
+class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
+    RuntimeShader(buildShader(rippleShape)) {
+
+    /** Shapes that the [RippleShader] supports. */
+    enum class RippleShape {
+        CIRCLE,
+        ROUNDED_BOX,
+        ELLIPSE
+    }
+    // language=AGSL
+    companion object {
+        private const val SHADER_UNIFORMS =
+            """
+            uniform vec2 in_center;
+            uniform vec2 in_size;
+            uniform float in_progress;
+            uniform float in_cornerRadius;
+            uniform float in_thickness;
+            uniform float in_time;
+            uniform float in_distort_radial;
+            uniform float in_distort_xy;
+            uniform float in_fadeSparkle;
+            uniform float in_fadeFill;
+            uniform float in_fadeRing;
+            uniform float in_blur;
+            uniform float in_pixelDensity;
+            layout(color) uniform vec4 in_color;
+            uniform float in_sparkle_strength;
+        """
+
+        private const val SHADER_CIRCLE_MAIN =
+            """
+            vec4 main(vec2 p) {
+                vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
+                float radius = in_size.x * 0.5;
+                float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur);
+                float inside = soften(sdCircle(p_distorted-in_center, radius * 1.2), in_blur);
+                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
+                    * (1.-sparkleRing) * in_fadeSparkle;
+
+                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
+                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
+                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
+                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
+            }
+        """
+
+        private const val SHADER_ROUNDED_BOX_MAIN =
+            """
+            vec4 main(vec2 p) {
+                float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
+                    in_thickness), in_blur);
+                float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius),
+                    in_blur);
+                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
+                    * (1.-sparkleRing) * in_fadeSparkle;
+
+                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
+                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
+                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
+                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
+            }
+        """
+
+        private const val SHADER_ELLIPSE_MAIN =
+            """
+            vec4 main(vec2 p) {
+                vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
+
+                float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur);
+                float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur);
+                float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
+                    * (1.-sparkleRing) * in_fadeSparkle;
+
+                float rippleInsideAlpha = (1.-inside) * in_fadeFill;
+                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
+                vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
+                return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
+            }
+        """
+
+        private const val CIRCLE_SHADER =
+            SHADER_UNIFORMS +
+                ShaderUtilLibrary.SHADER_LIB +
+                SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+                SdfShaderLibrary.CIRCLE_SDF +
+                SHADER_CIRCLE_MAIN
+        private const val ROUNDED_BOX_SHADER =
+            SHADER_UNIFORMS +
+                ShaderUtilLibrary.SHADER_LIB +
+                SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+                SdfShaderLibrary.ROUNDED_BOX_SDF +
+                SHADER_ROUNDED_BOX_MAIN
+        private const val ELLIPSE_SHADER =
+            SHADER_UNIFORMS +
+                ShaderUtilLibrary.SHADER_LIB +
+                SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+                SdfShaderLibrary.ELLIPSE_SDF +
+                SHADER_ELLIPSE_MAIN
+
+        private fun buildShader(rippleShape: RippleShape): String =
+            when (rippleShape) {
+                RippleShape.CIRCLE -> CIRCLE_SHADER
+                RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER
+                RippleShape.ELLIPSE -> ELLIPSE_SHADER
+            }
+
+        private fun subProgress(start: Float, end: Float, progress: Float): Float {
+            val min = Math.min(start, end)
+            val max = Math.max(start, end)
+            val sub = Math.min(Math.max(progress, min), max)
+            return (sub - start) / (end - start)
+        }
+    }
+
+    /** Sets the center position of the ripple. */
+    fun setCenter(x: Float, y: Float) {
+        setFloatUniform("in_center", x, y)
+    }
+
+    /** Max width of the ripple. */
+    private var maxSize: PointF = PointF()
+    fun setMaxSize(width: Float, height: Float) {
+        maxSize.x = width
+        maxSize.y = height
+    }
+
+    /** Progress of the ripple. Float value between [0, 1]. */
+    var progress: Float = 0.0f
+        set(value) {
+            field = value
+            setFloatUniform("in_progress", value)
+            val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
+
+            setFloatUniform(
+                "in_size",
+                /* width= */ maxSize.x * curvedProg,
+                /* height= */ maxSize.y * curvedProg
+            )
+            setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
+            // radius should not exceed width and height values.
+            setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
+
+            setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
+
+            val fadeIn = subProgress(0f, 0.1f, value)
+            val fadeOutNoise = subProgress(0.4f, 1f, value)
+            var fadeOutRipple = 0f
+            var fadeFill = 0f
+            if (!rippleFill) {
+                fadeFill = subProgress(0f, 0.6f, value)
+                fadeOutRipple = subProgress(0.3f, 1f, value)
+            }
+            setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
+            setFloatUniform("in_fadeFill", 1 - fadeFill)
+            setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
+        }
+
+    /** Play time since the start of the effect. */
+    var time: Float = 0.0f
+        set(value) {
+            field = value
+            setFloatUniform("in_time", value)
+        }
+
+    /** A hex value representing the ripple color, in the format of ARGB */
+    var color: Int = 0xffffff
+        set(value) {
+            field = value
+            setColorUniform("in_color", value)
+        }
+
+    /**
+     * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with
+     * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's
+     * opaque white and looks the most grainy.
+     */
+    var sparkleStrength: Float = 0.0f
+        set(value) {
+            field = value
+            setFloatUniform("in_sparkle_strength", value)
+        }
+
+    /** Distortion strength of the ripple. Expected value between[0, 1]. */
+    var distortionStrength: Float = 0.0f
+        set(value) {
+            field = value
+            setFloatUniform("in_distort_radial", 75 * progress * value)
+            setFloatUniform("in_distort_xy", 75 * value)
+        }
+
+    var pixelDensity: Float = 1.0f
+        set(value) {
+            field = value
+            setFloatUniform("in_pixelDensity", value)
+        }
+
+    /**
+     * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
+     * False for a ring effect.
+     */
+    var rippleFill: Boolean = false
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
new file mode 100644
index 0000000..2ad8243
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.surfaceeffects.ripple
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import androidx.core.graphics.ColorUtils
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape
+
+/**
+ * A generic expanding ripple effect.
+ *
+ * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
+ * then call [startRipple] to trigger the ripple expansion.
+ */
+open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+
+    private lateinit var rippleShader: RippleShader
+    lateinit var rippleShape: RippleShape
+        private set
+
+    private val ripplePaint = Paint()
+    private val animator = ValueAnimator.ofFloat(0f, 1f)
+
+    var duration: Long = 1750
+
+    private var maxWidth: Float = 0.0f
+    private var maxHeight: Float = 0.0f
+    fun setMaxSize(maxWidth: Float, maxHeight: Float) {
+        this.maxWidth = maxWidth
+        this.maxHeight = maxHeight
+        rippleShader.setMaxSize(maxWidth, maxHeight)
+    }
+
+    private var centerX: Float = 0.0f
+    private var centerY: Float = 0.0f
+    fun setCenter(x: Float, y: Float) {
+        this.centerX = x
+        this.centerY = y
+        rippleShader.setCenter(x, y)
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration?) {
+        rippleShader.pixelDensity = resources.displayMetrics.density
+        super.onConfigurationChanged(newConfig)
+    }
+
+    override fun onAttachedToWindow() {
+        rippleShader.pixelDensity = resources.displayMetrics.density
+        super.onAttachedToWindow()
+    }
+
+    /** Initializes the shader. Must be called before [startRipple]. */
+    fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
+        this.rippleShape = rippleShape
+        rippleShader = RippleShader(rippleShape)
+
+        rippleShader.color = RippleAnimationConfig.RIPPLE_DEFAULT_COLOR
+        rippleShader.progress = 0f
+        rippleShader.sparkleStrength = RippleAnimationConfig.RIPPLE_SPARKLE_STRENGTH
+        rippleShader.pixelDensity = resources.displayMetrics.density
+
+        ripplePaint.shader = rippleShader
+    }
+
+    @JvmOverloads
+    fun startRipple(onAnimationEnd: Runnable? = null) {
+        if (animator.isRunning) {
+            return // Ignore if ripple effect is already playing
+        }
+        animator.duration = duration
+        animator.addUpdateListener { updateListener ->
+            val now = updateListener.currentPlayTime
+            val progress = updateListener.animatedValue as Float
+            rippleShader.progress = progress
+            rippleShader.distortionStrength = 1 - progress
+            rippleShader.time = now.toFloat()
+            invalidate()
+        }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    onAnimationEnd?.run()
+                }
+            }
+        )
+        animator.start()
+    }
+
+    /**
+     * Set the color to be used for the ripple.
+     *
+     * The alpha value of the color will be applied to the ripple. The alpha range is [0-255].
+     */
+    fun setColor(color: Int, alpha: Int = RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA) {
+        rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
+    }
+
+    /**
+     * Set whether the ripple should remain filled as the ripple expands.
+     *
+     * See [RippleShader.rippleFill].
+     */
+    fun setRippleFill(rippleFill: Boolean) {
+        rippleShader.rippleFill = rippleFill
+    }
+
+    /** Set the intensity of the sparkles. */
+    fun setSparkleStrength(strength: Float) {
+        rippleShader.sparkleStrength = strength
+    }
+
+    /** Indicates whether the ripple animation is playing. */
+    fun rippleInProgress(): Boolean = animator.isRunning
+
+    override fun onDraw(canvas: Canvas?) {
+        if (canvas == null || !canvas.isHardwareAccelerated) {
+            // Drawing with the ripple shader requires hardware acceleration, so skip
+            // if it's unsupported.
+            return
+        }
+        // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
+        // active effect area. Values here should be kept in sync with the animation implementation
+        // in the ripple shader.
+        if (rippleShape == RippleShape.CIRCLE) {
+            val maskRadius =
+                (1 -
+                    (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress)) * maxWidth
+            canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
+        } else {
+            val maskWidth =
+                (1 -
+                    (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress)) * maxWidth * 2
+            val maskHeight =
+                (1 -
+                    (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress) *
+                        (1 - rippleShader.progress)) * maxHeight * 2
+            canvas.drawRect(
+                /* left= */ centerX - maskWidth,
+                /* top= */ centerY - maskHeight,
+                /* right= */ centerX + maskWidth,
+                /* bottom= */ centerY + maskHeight,
+                ripplePaint
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
new file mode 100644
index 0000000..8b2f466
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.surfaceeffects.shaderutil
+
+/** Library class that contains 2D signed distance functions. */
+class SdfShaderLibrary {
+    // language=AGSL
+    companion object {
+        const val CIRCLE_SDF =
+            """
+            float sdCircle(vec2 p, float r) {
+                return (length(p)-r) / r;
+            }
+
+            float circleRing(vec2 p, float radius) {
+                float thicknessHalf = radius * 0.25;
+
+                float outerCircle = sdCircle(p, radius + thicknessHalf);
+                float innerCircle = sdCircle(p, radius);
+
+                return subtract(outerCircle, innerCircle);
+            }
+        """
+
+        const val ROUNDED_BOX_SDF =
+            """
+            float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
+                size *= 0.5;
+                cornerRadius *= 0.5;
+                vec2 d = abs(p)-size+cornerRadius;
+
+                float outside = length(max(d, 0.0));
+                float inside = min(max(d.x, d.y), 0.0);
+
+                return (outside+inside-cornerRadius)/size.y;
+            }
+
+            float roundedBoxRing(vec2 p, vec2 size, float cornerRadius,
+                float borderThickness) {
+                float outerRoundBox = sdRoundedBox(p, size, cornerRadius);
+                float innerRoundBox = sdRoundedBox(p, size - vec2(borderThickness),
+                    cornerRadius - borderThickness);
+                return subtract(outerRoundBox, innerRoundBox);
+            }
+        """
+
+        // Used non-trigonometry parametrization and Halley's method (iterative) for root finding.
+        // This is more expensive than the regular circle SDF, recommend to use the circle SDF if
+        // possible.
+        const val ELLIPSE_SDF =
+            """float sdEllipse(vec2 p, vec2 wh) {
+            wh *= 0.5;
+
+            // symmetry
+            (wh.x > wh.y) ? wh = wh.yx, p = abs(p.yx) : p = abs(p);
+
+            vec2 u = wh*p, v = wh*wh;
+
+            float U1 = u.y/2.0;  float U5 = 4.0*U1;
+            float U2 = v.y-v.x;  float U6 = 6.0*U1;
+            float U3 = u.x-U2;   float U7 = 3.0*U3;
+            float U4 = u.x+U2;
+
+            float t = 0.5;
+            for (int i = 0; i < 3; i ++) {
+                float F1 = t*(t*t*(U1*t+U3)+U4)-U1;
+                float F2 = t*t*(U5*t+U7)+U4;
+                float F3 = t*(U6*t+U7);
+
+                t += (F1*F2)/(F1*F3-F2*F2);
+            }
+
+            t = clamp(t, 0.0, 1.0);
+
+            float d = distance(p, wh*vec2(1.0-t*t,2.0*t)/(t*t+1.0));
+            d /= wh.y;
+
+            return (dot(p/wh,p/wh)>1.0) ? d : -d;
+        }
+
+        float ellipseRing(vec2 p, vec2 wh) {
+            vec2 thicknessHalf = wh * 0.25;
+
+            float outerEllipse = sdEllipse(p, wh + thicknessHalf);
+            float innerEllipse = sdEllipse(p, wh);
+
+            return subtract(outerEllipse, innerEllipse);
+        }
+        """
+
+        const val SHADER_SDF_OPERATION_LIB =
+            """
+            float soften(float d, float blur) {
+                float blurHalf = blur * 0.5;
+                return smoothstep(-blurHalf, blurHalf, d);
+            }
+
+            float subtract(float outer, float inner) {
+                return max(outer, -inner);
+            }
+        """
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
new file mode 100644
index 0000000..d78e0c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.surfaceeffects.shaderutil
+
+/** A common utility functions that are used for computing shaders. */
+class ShaderUtilLibrary {
+    // language=AGSL
+    companion object {
+        const val SHADER_LIB =
+            """
+            float triangleNoise(vec2 n) {
+                n  = fract(n * vec2(5.3987, 5.4421));
+                n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
+                float xy = n.x * n.y;
+                // compute in [0..2[ and remap to [-1.0..1.0[
+                return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
+            }
+
+            const float PI = 3.1415926535897932384626;
+
+            float sparkles(vec2 uv, float t) {
+                float n = triangleNoise(uv);
+                float s = 0.0;
+                for (float i = 0; i < 4; i += 1) {
+                    float l = i * 0.01;
+                    float h = l + 0.1;
+                    float o = smoothstep(n - l, h, n);
+                    o *= abs(sin(PI * o * (t + 0.55 * i)));
+                    s += o;
+                }
+                return s;
+            }
+
+            vec2 distort(vec2 p, float time, float distort_amount_radial,
+                float distort_amount_xy) {
+                    float angle = atan(p.y, p.x);
+                      return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
+                                cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
+                         + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
+                                cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
+            }
+
+            // Return range [-1, 1].
+            vec3 hash(vec3 p) {
+                p = fract(p * vec3(.3456, .1234, .9876));
+                p += dot(p, p.yxz + 43.21);
+                p = (p.xxy + p.yxx) * p.zyx;
+                return (fract(sin(p) * 4567.1234567) - .5) * 2.;
+            }
+
+            // Skew factors (non-uniform).
+            const float SKEW = 0.3333333;  // 1/3
+            const float UNSKEW = 0.1666667;  // 1/6
+
+            // Return range roughly [-1,1].
+            // It's because the hash function (that returns a random gradient vector) returns
+            // different magnitude of vectors. Noise doesn't have to be in the precise range thus
+            // skipped normalize.
+            float simplex3d(vec3 p) {
+                // Skew the input coordinate, so that we get squashed cubical grid
+                vec3 s = floor(p + (p.x + p.y + p.z) * SKEW);
+
+                // Unskew back
+                vec3 u = s - (s.x + s.y + s.z) * UNSKEW;
+
+                // Unskewed coordinate that is relative to p, to compute the noise contribution
+                // based on the distance.
+                vec3 c0 = p - u;
+
+                // We have six simplices (in this case tetrahedron, since we are in 3D) that we
+                // could possibly in.
+                // Here, we are finding the correct tetrahedron (simplex shape), and traverse its
+                // four vertices (c0..3) when computing noise contribution.
+                // The way we find them is by comparing c0's x,y,z values.
+                // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in
+                // by comparing x and y values. i.e. x>y lower, x<y, upper triangle.
+                // Same applies in 3D.
+                //
+                // Below indicates the offsets (or offset directions) when c0=(x0,y0,z0)
+                // x0>y0>z0: (1,0,0), (1,1,0), (1,1,1)
+                // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1)
+                // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1)
+                // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1)
+                // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1)
+                // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1)
+                //
+                // The rule is:
+                // * For offset1, set 1 at the max component, otherwise 0.
+                // * For offset2, set 0 at the min component, otherwise 1.
+                // * For offset3, set 1 for all.
+                //
+                // Encode x0-y0, y0-z0, z0-x0 in a vec3
+                vec3 en = c0 - c0.yzx;
+                // Each represents whether x0>y0, y0>z0, z0>x0
+                en = step(vec3(0.), en);
+                // en.zxy encodes z0>x0, x0>y0, y0>x0
+                vec3 offset1 = en * (1. - en.zxy); // find max
+                vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min)
+                vec3 offset3 = vec3(1.);
+
+                vec3 c1 = c0 - offset1 + UNSKEW;
+                vec3 c2 = c0 - offset2 + UNSKEW * 2.;
+                vec3 c3 = c0 - offset3 + UNSKEW * 3.;
+
+                // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution)
+                //
+                // First compute d^2, squared distance to the point.
+                vec4 w; // w = max(0, r^2 - d^2))
+                w.x = dot(c0, c0);
+                w.y = dot(c1, c1);
+                w.z = dot(c2, c2);
+                w.w = dot(c3, c3);
+
+                // Noise contribution should decay to zero before they cross the simplex boundary.
+                // Usually r^2 is 0.5 or 0.6;
+                // 0.5 ensures continuity but 0.6 increases the visual quality for the application
+                // where discontinuity isn't noticeable.
+                w = max(0.6 - w, 0.);
+
+                // Noise contribution from each point.
+                vec4 nc;
+                nc.x = dot(hash(s), c0);
+                nc.y = dot(hash(s + offset1), c1);
+                nc.z = dot(hash(s + offset2), c2);
+                nc.w = dot(hash(s + offset3), c3);
+
+                nc *= w*w*w*w;
+
+                // Add all the noise contributions.
+                // Should multiply by the possible max contribution to adjust the range in [-1,1].
+                return dot(vec4(32.), nc);
+            }
+            """
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
new file mode 100644
index 0000000..5ac3aad7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.surfaceeffects.turbulencenoise
+
+import android.graphics.BlendMode
+import android.graphics.Color
+
+/** Turbulence noise animation configuration. */
+data class TurbulenceNoiseAnimationConfig(
+    /** The number of grids that is used to generate noise. */
+    val gridCount: Float = DEFAULT_NOISE_GRID_COUNT,
+
+    /** Multiplier for the noise luma matte. Increase this for brighter effects. */
+    val luminosityMultiplier: Float = DEFAULT_LUMINOSITY_MULTIPLIER,
+
+    /**
+     * Noise move speed variables.
+     *
+     * Its sign determines the direction; magnitude determines the speed. <ul>
+     * ```
+     *     <li> [noiseMoveSpeedX] positive: right to left; negative: left to right.
+     *     <li> [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom.
+     *     <li> [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it
+     *     to add turbulence in place.
+     * ```
+     * </ul>
+     */
+    val noiseMoveSpeedX: Float = 0f,
+    val noiseMoveSpeedY: Float = 0f,
+    val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z,
+
+    /** Color of the effect. */
+    var color: Int = DEFAULT_COLOR,
+    /** Background color of the effect. */
+    val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR,
+    val opacity: Int = DEFAULT_OPACITY,
+    val width: Float = 0f,
+    val height: Float = 0f,
+    val duration: Float = DEFAULT_NOISE_DURATION_IN_MILLIS,
+    val pixelDensity: Float = 1f,
+    val blendMode: BlendMode = DEFAULT_BLEND_MODE,
+    val onAnimationEnd: Runnable? = null
+) {
+    companion object {
+        const val DEFAULT_NOISE_DURATION_IN_MILLIS = 7500F
+        const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f
+        const val DEFAULT_NOISE_GRID_COUNT = 1.2f
+        const val DEFAULT_NOISE_SPEED_Z = 0.3f
+        const val DEFAULT_OPACITY = 150 // full opacity is 255.
+        const val DEFAULT_COLOR = Color.WHITE
+        const val DEFAULT_BACKGROUND_COLOR = Color.BLACK
+        val DEFAULT_BLEND_MODE = BlendMode.SRC_OVER
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
new file mode 100644
index 0000000..4c7e5f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.surfaceeffects.turbulencenoise
+
+/** A controller that plays [TurbulenceNoiseView]. */
+class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) {
+    /** Updates the color of the noise. */
+    fun updateNoiseColor(color: Int) {
+        turbulenceNoiseView.updateColor(color)
+    }
+
+    // TODO: add cancel and/ or pause once design requirements become clear.
+    /** Plays [TurbulenceNoiseView] with the given config. */
+    fun play(turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig) {
+        turbulenceNoiseView.play(turbulenceNoiseAnimationConfig)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
new file mode 100644
index 0000000..19c114d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.surfaceeffects.turbulencenoise
+
+import android.graphics.RuntimeShader
+import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
+import java.lang.Float.max
+
+/** Shader that renders turbulence simplex noise, with no octave. */
+class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) {
+    // language=AGSL
+    companion object {
+        private const val UNIFORMS =
+            """
+            uniform float in_gridNum;
+            uniform vec3 in_noiseMove;
+            uniform vec2 in_size;
+            uniform float in_aspectRatio;
+            uniform float in_opacity;
+            uniform float in_pixelDensity;
+            layout(color) uniform vec4 in_color;
+            layout(color) uniform vec4 in_backgroundColor;
+        """
+
+        private const val SHADER_LIB =
+            """
+            float getLuminosity(vec3 c) {
+                return 0.3*c.r + 0.59*c.g + 0.11*c.b;
+            }
+
+            vec3 maskLuminosity(vec3 dest, float lum) {
+                dest.rgb *= vec3(lum);
+                // Clip back into the legal range
+                dest = clamp(dest, vec3(0.), vec3(1.0));
+                return dest;
+            }
+        """
+
+        private const val MAIN_SHADER =
+            """
+            vec4 main(vec2 p) {
+                vec2 uv = p / in_size.xy;
+                uv.x *= in_aspectRatio;
+
+                vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
+                float luma = simplex3d(noiseP) * in_opacity;
+                vec3 mask = maskLuminosity(in_color.rgb, luma);
+                vec3 color = in_backgroundColor.rgb + mask * 0.6;
+
+                // Add dither with triangle distribution to avoid color banding. Ok to dither in the
+                // shader here as we are in gamma space.
+                float dither = triangleNoise(p * in_pixelDensity) / 255.;
+
+                // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to
+                // multiply rgb with a to get the correct result.
+                color = (color + dither.rrr) * in_color.a;
+                return vec4(color, in_color.a);
+            }
+        """
+
+        private const val TURBULENCE_NOISE_SHADER =
+            ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SHADER_LIB + MAIN_SHADER
+    }
+
+    /** Sets the number of grid for generating noise. */
+    fun setGridCount(gridNumber: Float = 1.0f) {
+        setFloatUniform("in_gridNum", gridNumber)
+    }
+
+    /**
+     * Sets the pixel density of the screen.
+     *
+     * Used it for noise dithering.
+     */
+    fun setPixelDensity(pixelDensity: Float) {
+        setFloatUniform("in_pixelDensity", pixelDensity)
+    }
+
+    /** Sets the noise color of the effect. */
+    fun setColor(color: Int) {
+        setColorUniform("in_color", color)
+    }
+
+    /** Sets the background color of the effect. */
+    fun setBackgroundColor(color: Int) {
+        setColorUniform("in_backgroundColor", color)
+    }
+
+    /**
+     * Sets the opacity to achieve fade in/ out of the animation.
+     *
+     * Expected value range is [1, 0].
+     */
+    fun setOpacity(opacity: Float) {
+        setFloatUniform("in_opacity", opacity)
+    }
+
+    /** Sets the size of the shader. */
+    fun setSize(width: Float, height: Float) {
+        setFloatUniform("in_size", width, height)
+        setFloatUniform("in_aspectRatio", width / max(height, 0.001f))
+    }
+
+    /** Sets noise move speed in x, y, and z direction. */
+    fun setNoiseMove(x: Float, y: Float, z: Float) {
+        setFloatUniform("in_noiseMove", x, y, z)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
new file mode 100644
index 0000000..8649d59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.surfaceeffects.turbulencenoise
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.core.graphics.ColorUtils
+import java.util.Random
+import kotlin.math.sin
+
+/** View that renders turbulence noise effect. */
+class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+
+    companion object {
+        private const val MS_TO_SEC = 0.001f
+        private const val TWO_PI = Math.PI.toFloat() * 2f
+    }
+
+    @VisibleForTesting val turbulenceNoiseShader = TurbulenceNoiseShader()
+    private val paint = Paint().apply { this.shader = turbulenceNoiseShader }
+    private val random = Random()
+    private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
+    private var config: TurbulenceNoiseAnimationConfig? = null
+
+    val isPlaying: Boolean
+        get() = animator.isRunning
+
+    init {
+        // Only visible during the animation.
+        visibility = INVISIBLE
+    }
+
+    /** Updates the color during the animation. No-op if there's no animation playing. */
+    fun updateColor(color: Int) {
+        config?.let {
+            it.color = color
+            applyConfig(it)
+        }
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        if (canvas == null || !canvas.isHardwareAccelerated) {
+            // Drawing with the turbulence noise shader requires hardware acceleration, so skip
+            // if it's unsupported.
+            return
+        }
+
+        canvas.drawPaint(paint)
+    }
+
+    internal fun play(config: TurbulenceNoiseAnimationConfig) {
+        if (isPlaying) {
+            return // Ignore if the animation is playing.
+        }
+        visibility = VISIBLE
+        applyConfig(config)
+
+        // Add random offset to avoid same patterned noise.
+        val offsetX = random.nextFloat()
+        val offsetY = random.nextFloat()
+
+        animator.duration = config.duration.toLong()
+        animator.addUpdateListener { updateListener ->
+            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+            // Remap [0,1] to [0, 2*PI]
+            val progress = TWO_PI * updateListener.animatedValue as Float
+
+            turbulenceNoiseShader.setNoiseMove(
+                offsetX + timeInSec * config.noiseMoveSpeedX,
+                offsetY + timeInSec * config.noiseMoveSpeedY,
+                timeInSec * config.noiseMoveSpeedZ
+            )
+
+            // Fade in and out the noise as the animation progress.
+            // TODO: replace it with a better curve
+            turbulenceNoiseShader.setOpacity(sin(TWO_PI - progress) * config.luminosityMultiplier)
+
+            invalidate()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    visibility = INVISIBLE
+                    config.onAnimationEnd?.run()
+                }
+            }
+        )
+        animator.start()
+    }
+
+    private fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
+        this.config = config
+        with(turbulenceNoiseShader) {
+            setGridCount(config.gridCount)
+            setColor(ColorUtils.setAlphaComponent(config.color, config.opacity))
+            setBackgroundColor(config.backgroundColor)
+            setSize(config.width, config.height)
+            setPixelDensity(config.pixelDensity)
+        }
+        paint.blendMode = config.blendMode
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index d5d904c..a9d05d1 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -17,13 +17,11 @@
 package com.android.systemui.temporarydisplay
 
 import android.annotation.LayoutRes
-import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.os.PowerManager
-import android.os.SystemClock
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -36,6 +34,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.wakelock.WakeLock
 
 /**
  * A generic controller that can temporarily display a new view in a new window.
@@ -45,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,
@@ -60,21 +54,18 @@
     private val configurationController: ConfigurationController,
     private val powerManager: PowerManager,
     @LayoutRes private val viewLayoutRes: Int,
-    private val windowTitle: String,
-    private val wakeReason: String,
+    private val wakeLockBuilder: WakeLock.Builder,
 ) : CoreStartable {
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
      * all subclasses.
      */
-    @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
     internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
         width = WindowManager.LayoutParams.WRAP_CONTENT
         height = WindowManager.LayoutParams.WRAP_CONTENT
-        type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
+        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()
     }
@@ -94,6 +85,22 @@
     private var cancelViewTimeout: Runnable? = null
 
     /**
+     * A wakelock that is acquired when view is displayed and screen off,
+     * then released when view is removed.
+     */
+    private var wakeLock: WakeLock? = null
+
+    /** A string that keeps track of wakelock reason once it is acquired till it gets released */
+    private var wakeReasonAcquired: String? = null
+
+    /**
+     * A stack of pairs of device id and temporary view info. This is used when there may be
+     * multiple devices in range, and we want to always display the chip for the most recently
+     * active device.
+     */
+    internal val activeViews: ArrayDeque<Pair<String, T>> = ArrayDeque()
+
+    /**
      * Displays the view with the provided [newInfo].
      *
      * This method handles inflating and attaching the view, then delegates to [updateView] to
@@ -102,36 +109,71 @@
     fun displayView(newInfo: T) {
         val currentDisplayInfo = displayInfo
 
-        if (currentDisplayInfo != null) {
+        // Update our list of active devices by removing it if necessary, then adding back at the
+        // front of the list
+        val id = newInfo.id
+        val position = findAndRemoveFromActiveViewsList(id)
+        activeViews.addFirst(Pair(id, newInfo))
+
+        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
-            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",
+            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(
+                    id,
+                    removalReason = "New info has new window title: ${newInfo.windowTitle}"
                 )
             }
-            logger.logChipAddition()
+
+            // 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)
+
+            wakeLock = if (!powerManager.isScreenOn) {
+                // If the screen is off, fully wake it so the user can see the view.
+                wakeLockBuilder
+                    .setTag(newInfo.windowTitle)
+                    .setLevelsAndFlags(
+                            PowerManager.FULL_WAKE_LOCK or
+                                PowerManager.ACQUIRE_CAUSES_WAKEUP
+                    )
+                    .build()
+            } else {
+                // Per b/239426653, we want the view to show over the dream state.
+                // If the screen is on, using screen bright level will leave screen on the dream
+                // state but ensure the screen will not go off before wake lock is released.
+                wakeLockBuilder
+                    .setTag(newInfo.windowTitle)
+                    .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK)
+                    .build()
+            }
+            wakeLock?.acquire(newInfo.wakeReason)
+            wakeReasonAcquired = newInfo.wakeReason
+            logger.logViewAddition(id, 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
        )
-        cancelViewTimeout?.run()
+
+        // Only cancel timeout of the most recent view displayed, as it will be reset.
+        if (position == 0) {
+            cancelViewTimeout?.run()
+        }
         cancelViewTimeout = mainExecutor.executeDelayed(
-            { removeView(REMOVAL_REASON_TIMEOUT) },
+            { removeView(id, REMOVAL_REASON_TIMEOUT) },
             timeout.toLong()
         )
     }
@@ -149,7 +191,13 @@
         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
+        }
+        newView.keepScreenOn = true
+        windowManager.addView(newView, paramsWithTitle)
         animateViewIn(newView)
     }
 
@@ -168,25 +216,67 @@
     }
 
     /**
-     * Hides the view.
+     * Hides the view given its [id].
      *
+     * @param id the id of the device responsible of displaying the temp view.
      * @param removalReason a short string describing why the view was removed (timeout, state
      *     change, etc.)
      */
-    fun removeView(removalReason: String) {
+    fun removeView(id: String, removalReason: String) {
         val currentDisplayInfo = displayInfo ?: return
 
-        val currentView = currentDisplayInfo.view
-        animateViewOut(currentView) { windowManager.removeView(currentView) }
+        val removalPosition = findAndRemoveFromActiveViewsList(id)
+        if (removalPosition == null) {
+            logger.logViewRemovalIgnored(id, "view not found in the list")
+            return
+        }
+        if (removalPosition != 0) {
+            logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
+            return
+        }
+        logger.logViewRemoval(id, removalReason)
 
-        logger.logChipRemoval(removalReason)
+        val newViewToDisplay = if (activeViews.isEmpty()) {
+            null
+        } else {
+            activeViews[0].second
+        }
+
+        val currentView = currentDisplayInfo.view
+        animateViewOut(currentView) {
+            windowManager.removeView(currentView)
+            wakeLock?.release(wakeReasonAcquired)
+        }
+
         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
-        // new view appropriately.
+        // that if a new view event comes in while this view is animating out, we still display
+        // the new view appropriately.
         displayInfo = null
         // No need to time the view out since it's already gone
         cancelViewTimeout?.run()
+
+        if (newViewToDisplay != null) {
+            mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY)
+        }
+    }
+
+    /**
+     * Finds and removes the active view with the given [id] from the stack, or null if there is no
+     * active view with that ID
+     *
+     * @param id that temporary view belonged to.
+     *
+     * @return index of the view in the stack , otherwise null.
+     */
+    private fun findAndRemoveFromActiveViewsList(id: String): Int? {
+        for (i in 0 until activeViews.size) {
+            if (activeViews[i].first == id) {
+                activeViews.removeAt(i)
+                return i
+            }
+        }
+        return null
     }
 
     /**
@@ -227,6 +317,7 @@
 }
 
 private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
+const val DISPLAY_VIEW_DELAY = 50L
 
 private data class IconInfo(
     val iconName: String,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
index 4fe753a..df83960 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
@@ -19,12 +19,29 @@
 /**
  * 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
+
+    /**
+     * The id of the temporary view.
+     */
+    abstract val id: String
 }
 
-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 606a11a..133a384 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -16,21 +16,50 @@
 
 package com.android.systemui.temporarydisplay
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 
 /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
 open class TemporaryViewLogger(
     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 with the given [id] in a window titled [windowTitle]. */
+    fun logViewAddition(id: String, windowTitle: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = windowTitle
+                str2 = id
+            },
+            { "View added. window=$str1 id=$str2" }
+        )
     }
 
-    /** 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" })
+    /** Logs that we removed the view with the given [id] for the given [reason]. */
+    fun logViewRemoval(id: String, reason: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                str2 = id
+            },
+            { "View with id=$str2 is removed due to: $str1" }
+        )
+    }
+
+    /** Logs that we ignored removal of the view with the given [id]. */
+    fun logViewRemovalIgnored(id: String, reason: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                str2 = id
+            },
+            { "Removal of view with id=$str2 is ignored because $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 1a25e4d..fb17b69 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.graphics.Rect
-import android.media.MediaRoute2Info
 import android.os.PowerManager
 import android.view.Gravity
 import android.view.MotionEvent
@@ -27,27 +26,25 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.ViewHierarchyAnimator
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.common.ui.binder.IconViewBinder
+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.ChipStateSender
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.TransferStatus
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
+import com.android.systemui.util.wakelock.WakeLock
 import javax.inject.Inject
 
 /**
@@ -65,24 +62,22 @@
  * 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,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
-        private val uiEventLogger: MediaTttSenderUiEventLogger,
         private val falsingManager: FalsingManager,
         private val falsingCollector: FalsingCollector,
         private val viewUtil: ViewUtil,
-) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
+        private val vibratorHelper: VibratorHelper,
+        wakeLockBuilder: WakeLock.Builder,
+) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
         context,
         logger,
         windowManager,
@@ -91,8 +86,7 @@
         configurationController,
         powerManager,
         R.layout.chipbar,
-        MediaTttUtils.WINDOW_TITLE,
-        MediaTttUtils.WAKE_REASON,
+        wakeLockBuilder,
 ) {
 
     private lateinit var parent: ChipbarRootView
@@ -101,18 +95,23 @@
         gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
     }
 
-    override fun start() {}
-
     override fun updateView(
-        newInfo: ChipSenderInfo,
+        newInfo: ChipbarInfo,
         currentView: ViewGroup
     ) {
-        // TODO(b/245610654): Adding logging here.
-
-        val chipState = newInfo.state
+        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.media_ttt_sender_chip)
+        parent = currentView.requireViewById(R.id.chipbar_root_view)
         parent.touchHandler = object : Gefingerpoken {
             override fun onTouchEvent(ev: MotionEvent?): Boolean {
                 falsingCollector.onTouchEvent(ev)
@@ -120,52 +119,62 @@
             }
         }
 
-        // App icon
-        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
-            context, newInfo.routeInfo.clientPackageName, logger
-        )
-        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
-        iconView.setImageDrawable(iconInfo.drawable)
-        iconView.contentDescription = iconInfo.contentDescription
+        // ---- Start icon ----
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon)
+        IconViewBinder.bind(newInfo.startIcon, iconView)
 
-        // Text
-        val otherDeviceName = newInfo.routeInfo.name.toString()
-        val chipText = chipState.getChipTextString(context, otherDeviceName)
-        currentView.requireViewById<TextView>(R.id.text).text = chipText
+        // ---- Text ----
+        val textView = currentView.requireViewById<TextView>(R.id.text)
+        TextViewBinder.bind(textView, newInfo.text)
+        // Updates text view bounds to make sure it perfectly fits the new text
+        // (If the new text is smaller than the previous text) see b/253228632.
+        textView.requestLayout()
 
+        // ---- End item ----
         // Loading
         currentView.requireViewById<View>(R.id.loading).visibility =
-            (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue()
+            (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
 
-        // Undo
-        val undoView = currentView.requireViewById<View>(R.id.undo)
-        val undoClickListener = chipState.undoClickListener(
-                this,
-                newInfo.routeInfo,
-                newInfo.undoCallback,
-                uiEventLogger,
-                falsingManager,
-        )
-        undoView.setOnClickListener(undoClickListener)
-        undoView.visibility = (undoClickListener != null).visibleIfTrue()
+        // Error
+        currentView.requireViewById<View>(R.id.error).visibility =
+            (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue()
 
-        // Failure
-        currentView.requireViewById<View>(R.id.failure_icon).visibility =
-            (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue()
+        // Button
+        val buttonView = currentView.requireViewById<TextView>(R.id.end_button)
+        if (newInfo.endItem is ChipbarEndItem.Button) {
+            TextViewBinder.bind(buttonView, newInfo.endItem.text)
 
-        // For accessibility
+            val onClickListener = View.OnClickListener { clickedView ->
+                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+                newInfo.endItem.onClickListener.onClick(clickedView)
+            }
+
+            buttonView.setOnClickListener(onClickListener)
+            buttonView.visibility = View.VISIBLE
+        } else {
+            buttonView.visibility = View.GONE
+        }
+
+        // ---- Overall accessibility ----
         currentView.requireViewById<ViewGroup>(
-                R.id.media_ttt_sender_chip_inner
-        ).contentDescription = "${iconInfo.contentDescription} $chipText"
+                R.id.chipbar_inner
+        ).contentDescription =
+            "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " +
+                "${newInfo.text.loadText(context)}"
+
+        // ---- Haptics ----
+        newInfo.vibrationEffect?.let {
+            vibratorHelper.vibrate(it)
+        }
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner)
+        val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner)
         ViewHierarchyAnimator.animateAddition(
             chipInnerView,
             ViewHierarchyAnimator.Hotspot.TOP,
             Interpolators.EMPHASIZED_DECELERATE,
-            duration = ANIMATION_DURATION,
+            duration = ANIMATION_IN_DURATION,
             includeMargins = true,
             includeFadeIn = true,
             // We can only request focus once the animation finishes.
@@ -175,15 +184,17 @@
 
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         ViewHierarchyAnimator.animateRemoval(
-            view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+            view.requireViewById<ViewGroup>(R.id.chipbar_inner),
             ViewHierarchyAnimator.Hotspot.TOP,
             Interpolators.EMPHASIZED_ACCELERATE,
-            ANIMATION_DURATION,
+            ANIMATION_OUT_DURATION,
             includeMargins = true,
             onAnimationEnd,
         )
     }
 
+    override fun start() {}
+
     override fun getTouchableRegion(view: View, outRect: Rect) {
         viewUtil.setRectToViewWindowLocation(view, outRect)
     }
@@ -197,13 +208,5 @@
     }
 }
 
-data class ChipSenderInfo(
-    val state: ChipStateSender,
-    val routeInfo: MediaRoute2Info,
-    val undoCallback: IUndoMediaTransferCallback? = null
-) : TemporaryViewInfo {
-    override fun getTimeoutMs() = state.timeout
-}
-
-const val SENDER_TAG = "MediaTapToTransferSender"
-private const val ANIMATION_DURATION = 500L
+private const val ANIMATION_IN_DURATION = 500L
+private const val ANIMATION_OUT_DURATION = 250L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
new file mode 100644
index 0000000..b92e0ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -0,0 +1,60 @@
+/*
+ * 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 android.os.VibrationEffect
+import android.view.View
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
+
+/**
+ * A container for all the state needed to display a chipbar via [ChipbarCoordinator].
+ *
+ * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales;
+ * on the right in RTL locales).
+ * @property text the text to display.
+ * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
+ * locales; on the left in RTL locales).
+ * @property vibrationEffect an optional vibration effect when the chipbar is displayed
+ */
+data class ChipbarInfo(
+    val startIcon: Icon,
+    val text: Text,
+    val endItem: ChipbarEndItem?,
+    val vibrationEffect: VibrationEffect? = null,
+    override val windowTitle: String,
+    override val wakeReason: String,
+    override val timeoutMs: Int,
+    override val id: String,
+) : TemporaryViewInfo()
+
+/** The possible items to display at the end of the chipbar. */
+sealed class ChipbarEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : ChipbarEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : ChipbarEndItem()
+
+    /**
+     * A button with the provided [text] and [onClickListener] functionality should be displayed.
+     */
+    data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem()
+
+    // TODO(b/245610654): Add support for a generic icon.
+}
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/temporarydisplay/dagger/ChipbarLog.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt
new file mode 100644
index 0000000..5f101f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 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/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 3d56f23..5894fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -79,6 +79,7 @@
 import org.json.JSONObject;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -665,8 +666,10 @@
         // Allow-list of Style objects that can be created from a setting string, i.e. can be
         // used as a system-wide theme.
         // - Content intentionally excluded, intended for media player, not system-wide
-        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
-                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
+        List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ,
+                Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT,
+                Style.MONOCHROMATIC));
+
         Style style = mThemeStyle;
         final String overlayPackageJson = mSecureSettings.getStringForUser(
                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
index 51541bd..fda5114 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.toast
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ToastLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogMessage
 import javax.inject.Inject
 
 private const val TAG = "ToastLog"
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 10a09dd1..5ea4399 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -44,9 +44,11 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsImplementation;
 import com.android.systemui.screenshot.ReferenceScreenshotModule;
+import com.android.systemui.settings.dagger.MultiUserUtilsModule;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeControllerImpl;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -89,6 +91,7 @@
         includes = {
                 AospPolicyModule.class,
                 GestureModule.class,
+                MultiUserUtilsModule.class,
                 PowerModule.class,
                 QSModule.class,
                 ReferenceScreenshotModule.class,
@@ -157,7 +160,8 @@
             ConfigurationController configurationController,
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -168,7 +172,8 @@
                 configurationController,
                 handler,
                 accessibilityManagerWrapper,
-                uiEventLogger
+                uiEventLogger,
+                shadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index bf70673..095718b 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -46,6 +46,7 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.SystemUIApplication;
+import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.util.NotificationChannels;
 
@@ -61,15 +62,24 @@
     private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME";
     private static final String ACTION_FINISH_WIZARD = "com.android.systemui.action.FINISH_WIZARD";
     private final Context mContext;
+    private final BroadcastDispatcher mBroadcastDispatcher;
 
     // TODO: delay some notifications to avoid bumpy fast operations
 
-    private NotificationManager mNotificationManager;
-    private StorageManager mStorageManager;
+    private final NotificationManager mNotificationManager;
+    private final StorageManager mStorageManager;
 
     @Inject
-    public StorageNotification(Context context) {
+    public StorageNotification(
+            Context context,
+            BroadcastDispatcher broadcastDispatcher,
+            NotificationManager notificationManager,
+            StorageManager storageManager
+    ) {
         mContext = context;
+        mBroadcastDispatcher = broadcastDispatcher;
+        mNotificationManager = notificationManager;
+        mStorageManager = storageManager;
     }
 
     private static class MoveInfo {
@@ -168,17 +178,22 @@
 
     @Override
     public void start() {
-        mNotificationManager = mContext.getSystemService(NotificationManager.class);
-
-        mStorageManager = mContext.getSystemService(StorageManager.class);
         mStorageManager.registerListener(mListener);
 
-        mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME),
-                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
-        mContext.registerReceiver(mFinishReceiver, new IntentFilter(ACTION_FINISH_WIZARD),
-                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
+        mBroadcastDispatcher.registerReceiver(
+                mSnoozeReceiver,
+                new IntentFilter(ACTION_SNOOZE_VOLUME),
+                null,
+                null,
+                Context.RECEIVER_EXPORTED_UNAUDITED,
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+        mBroadcastDispatcher.registerReceiver(
+                mFinishReceiver,
+                new IntentFilter(ACTION_FINISH_WIZARD),
+                null,
+                null,
+                Context.RECEIVER_EXPORTED_UNAUDITED,
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
 
         // Kick current state into place
         final List<DiskInfo> disks = mStorageManager.getDisks();
@@ -381,7 +396,7 @@
 
         // Don't annoy when user dismissed in past.  (But make sure the disk is adoptable; we
         // used to allow snoozing non-adoptable disks too.)
-        if (rec.isSnoozed() && disk.isAdoptable()) {
+        if (rec == null || (rec.isSnoozed() && disk.isAdoptable())) {
             return null;
         }
         if (disk.isAdoptable() && !rec.isInited() && rec.getType() != VolumeInfo.TYPE_PUBLIC
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index f017126..b56c403 100644
--- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -25,6 +25,8 @@
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -59,6 +61,7 @@
     private final ActivityStarter mActivityStarter;
 
     private Dialog mSetupUserDialog;
+    private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
     @Inject
     public CreateUserActivity(UserCreator userCreator,
@@ -82,6 +85,10 @@
 
         mSetupUserDialog = createDialog();
         mSetupUserDialog.show();
+
+        getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                        OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                        mBackCallback);
     }
 
     @Override
@@ -125,10 +132,20 @@
 
     @Override
     public void onBackPressed() {
-        super.onBackPressed();
+        onBackInvoked();
+    }
+
+    private void onBackInvoked() {
         if (mSetupUserDialog != null) {
             mSetupUserDialog.dismiss();
         }
+        finish();
+    }
+
+    @Override
+    protected void onDestroy() {
+        getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback);
+        super.onDestroy();
     }
 
     private void addUserNow(String userName, Drawable userIcon) {
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
index ee785b6..088cd93 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
@@ -36,9 +36,7 @@
     private var adapter: ListAdapter? = null
 
     init {
-        setBackgroundDrawable(
-            res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme())
-        )
+        setBackgroundDrawable(null)
         setModal(false)
         setOverlapAnchor(true)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 919e699..4c9b8e4 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -19,31 +19,19 @@
 
 import android.content.Context
 import android.content.pm.UserInfo
-import android.graphics.drawable.BitmapDrawable
-import android.graphics.drawable.Drawable
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
-import androidx.appcompat.content.res.AppCompatResources
-import com.android.internal.util.UserIcons
 import com.android.systemui.R
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import java.util.concurrent.atomic.AtomicBoolean
@@ -55,13 +43,13 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 
 /**
@@ -71,15 +59,6 @@
  * upstream changes.
  */
 interface UserRepository {
-    /** List of all users on the device. */
-    val users: Flow<List<UserModel>>
-
-    /** The currently-selected user. */
-    val selectedUser: Flow<UserModel>
-
-    /** List of available user-related actions. */
-    val actions: Flow<List<UserActionModel>>
-
     /** User switcher related settings. */
     val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
 
@@ -92,9 +71,6 @@
     /** User ID of the last non-guest selected user. */
     val lastSelectedNonGuestUserId: Int
 
-    /** Whether actions are available even when locked. */
-    val isActionableWhenLocked: Flow<Boolean>
-
     /** Whether the device is configured to always have a guest user available. */
     val isGuestUserAutoCreated: Boolean
 
@@ -104,6 +80,9 @@
     /** Whether we've scheduled the creation of a guest user. */
     val isGuestUserCreationScheduled: AtomicBoolean
 
+    /** Whether to enable the status bar user chip (which launches the user switcher) */
+    val isStatusBarUserChipEnabled: Boolean
+
     /** The user of the secondary service. */
     var secondaryUserId: Int
 
@@ -124,19 +103,14 @@
 constructor(
     @Application private val appContext: Context,
     private val manager: UserManager,
-    private val controller: UserSwitcherController,
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val globalSettings: GlobalSettings,
     private val tracker: UserTracker,
-    private val featureFlags: FeatureFlags,
 ) : UserRepository {
 
-    private val isNewImpl: Boolean
-        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
-
-    private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
+    private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
     override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
         _userSwitcherSettings.asStateFlow().filterNotNull()
 
@@ -149,70 +123,24 @@
     override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
         private set
 
-    private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
-        fun send() {
-            trySendWithFailureLogging(
-                controller.users,
-                TAG,
-            )
-        }
-
-        val callback = UserSwitcherController.UserSwitchCallback { send() }
-
-        controller.addUserSwitchCallback(callback)
-        send()
-
-        awaitClose { controller.removeUserSwitchCallback(callback) }
-    }
-
-    override val users: Flow<List<UserModel>> =
-        userRecords.map { records -> records.filter { it.isUser() }.map { it.toUserModel() } }
-
-    override val selectedUser: Flow<UserModel> =
-        users.map { users -> users.first { user -> user.isSelected } }
-
-    override val actions: Flow<List<UserActionModel>> =
-        userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
-
-    override val isActionableWhenLocked: Flow<Boolean> =
-        if (isNewImpl) {
-            emptyFlow()
-        } else {
-            controller.isAddUsersFromLockScreenEnabled
-        }
-
     override val isGuestUserAutoCreated: Boolean =
-        if (isNewImpl) {
-            appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
-        } else {
-            controller.isGuestUserAutoCreated
-        }
+        appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
 
     private var _isGuestUserResetting: Boolean = false
-    override var isGuestUserResetting: Boolean =
-        if (isNewImpl) {
-            _isGuestUserResetting
-        } else {
-            controller.isGuestUserResetting
-        }
-        set(value) =
-            if (isNewImpl) {
-                _isGuestUserResetting = value
-            } else {
-                error("Not supported in the old implementation!")
-            }
+    override var isGuestUserResetting: Boolean = _isGuestUserResetting
 
     override val isGuestUserCreationScheduled = AtomicBoolean()
 
+    override val isStatusBarUserChipEnabled: Boolean =
+        appContext.resources.getBoolean(R.bool.flag_user_switcher_chip)
+
     override var secondaryUserId: Int = UserHandle.USER_NULL
 
     override var isRefreshUsersPaused: Boolean = false
 
     init {
-        if (isNewImpl) {
-            observeSelectedUser()
-            observeUserSettings()
-        }
+        observeSelectedUser()
+        observeUserSettings()
     }
 
     override fun refreshUsers() {
@@ -220,7 +148,12 @@
             val result = withContext(backgroundDispatcher) { manager.aliveUsers }
 
             if (result != null) {
-                _userInfos.value = result.sortedBy { it.creationTime }
+                _userInfos.value =
+                    result
+                        // Users should be sorted by ascending creation time.
+                        .sortedBy { it.creationTime }
+                        // The guest user is always last, regardless of creation time.
+                        .sortedBy { it.isGuest }
             }
         }
     }
@@ -230,7 +163,7 @@
     }
 
     override fun isSimpleUserSwitcher(): Boolean {
-        return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
+        return _userSwitcherSettings.value.isSimpleUserSwitcher
     }
 
     private fun observeSelectedUser() {
@@ -244,6 +177,10 @@
                         override fun onUserChanged(newUser: Int, userContext: Context) {
                             send()
                         }
+
+                        override fun onProfilesChanged(profiles: List<UserInfo>) {
+                            send()
+                        }
                     }
 
                 tracker.addCallback(callback, mainDispatcher.asExecutor())
@@ -317,62 +254,6 @@
         }
     }
 
-    private fun UserRecord.isUser(): Boolean {
-        return when {
-            isAddUser -> false
-            isAddSupervisedUser -> false
-            isGuest -> info != null
-            else -> true
-        }
-    }
-
-    private fun UserRecord.isNotUser(): Boolean {
-        return !isUser()
-    }
-
-    private fun UserRecord.toUserModel(): UserModel {
-        return UserModel(
-            id = resolveId(),
-            name = getUserName(this),
-            image = getUserImage(this),
-            isSelected = isCurrent,
-            isSelectable = isSwitchToEnabled || isGuest,
-            isGuest = isGuest,
-        )
-    }
-
-    private fun UserRecord.toActionModel(): UserActionModel {
-        return when {
-            isAddUser -> UserActionModel.ADD_USER
-            isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
-            isGuest -> UserActionModel.ENTER_GUEST_MODE
-            else -> error("Don't know how to convert to UserActionModel: $this")
-        }
-    }
-
-    private fun getUserName(record: UserRecord): Text {
-        val resourceId: Int? = LegacyUserUiHelper.getGuestUserRecordNameResourceId(record)
-        return if (resourceId != null) {
-            Text.Resource(resourceId)
-        } else {
-            Text.Loaded(checkNotNull(record.info).name)
-        }
-    }
-
-    private fun getUserImage(record: UserRecord): Drawable {
-        if (record.isGuest) {
-            return checkNotNull(
-                AppCompatResources.getDrawable(appContext, R.drawable.ic_account_circle)
-            )
-        }
-
-        val userId = checkNotNull(record.info?.id)
-        return manager.getUserIcon(userId)?.let { userSelectedIcon ->
-            BitmapDrawable(userSelectedIcon)
-        }
-            ?: UserIcons.getDefaultUserIcon(appContext.resources, userId, /* light= */ false)
-    }
-
     companion object {
         private const val TAG = "UserRepository"
         @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
index 07e5cf9..a374885 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -28,6 +28,8 @@
 import android.view.WindowManagerGlobal
 import android.widget.Toast
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -58,6 +60,8 @@
     private val devicePolicyManager: DevicePolicyManager,
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val uiEventLogger: UiEventLogger,
+    resumeSessionReceiver: GuestResumeSessionReceiver,
+    resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver,
 ) {
     /** Whether the device is configured to always have a guest user available. */
     val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
@@ -65,6 +69,11 @@
     /** Whether the guest user is currently being reset. */
     val isGuestUserResetting: Boolean = repository.isGuestUserResetting
 
+    init {
+        resumeSessionReceiver.register()
+        resetOrExitSessionReceiver.register()
+    }
+
     /** Notifies that the device has finished booting. */
     fun onDeviceBootCompleted() {
         applicationScope.launch {
@@ -208,7 +217,12 @@
             if (newGuestId == UserHandle.USER_NULL) {
                 Log.e(TAG, "Could not create new guest, switching back to system user")
                 switchUser(UserHandle.USER_SYSTEM)
-                withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+                withContext(backgroundDispatcher) {
+                    manager.removeUserWhenPossible(
+                        UserHandle.of(currentUser.id),
+                        /* overrideDevicePolicy= */ false
+                    )
+                }
                 try {
                     WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
                 } catch (e: RemoteException) {
@@ -222,13 +236,21 @@
 
             switchUser(newGuestId)
 
-            withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+            withContext(backgroundDispatcher) {
+                manager.removeUserWhenPossible(
+                    UserHandle.of(currentUser.id),
+                    /* overrideDevicePolicy= */ false
+                )
+            }
         } else {
             if (repository.isGuestUserAutoCreated) {
                 repository.isGuestUserResetting = true
             }
             switchUser(targetUserId)
-            manager.removeUser(currentUser.id)
+            manager.removeUserWhenPossible(
+                UserHandle.of(currentUser.id),
+                /* overrideDevicePolicy= */ false
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index ba5a82a..83f0711 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -34,6 +34,7 @@
 import com.android.internal.util.UserIcons
 import com.android.systemui.R
 import com.android.systemui.SystemUISecondaryUserService
+import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
@@ -44,8 +45,9 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
-import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.UserSwitcherActivity
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
@@ -64,8 +66,7 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -82,7 +83,6 @@
 constructor(
     @Application private val applicationContext: Context,
     private val repository: UserRepository,
-    private val controller: UserSwitcherController,
     private val activityStarter: ActivityStarter,
     private val keyguardInteractor: KeyguardInteractor,
     private val featureFlags: FeatureFlags,
@@ -107,9 +107,6 @@
         fun onUserStateChanged()
     }
 
-    private val isNewImpl: Boolean
-        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
-
     private val supervisedUserPackageName: String?
         get() =
             applicationContext.getString(
@@ -118,196 +115,145 @@
 
     private val callbackMutex = Mutex()
     private val callbacks = mutableSetOf<UserCallback>()
+    private val userInfos =
+        combine(repository.userSwitcherSettings, repository.userInfos) { settings, userInfos ->
+            userInfos.filter { !it.isGuest || canCreateGuestUser(settings) }
+        }
 
     /** List of current on-device users to select from. */
     val users: Flow<List<UserModel>>
         get() =
-            if (isNewImpl) {
-                combine(
-                    repository.userInfos,
-                    repository.selectedUserInfo,
-                    repository.userSwitcherSettings,
-                ) { userInfos, selectedUserInfo, settings ->
-                    toUserModels(
-                        userInfos = userInfos,
-                        selectedUserId = selectedUserInfo.id,
-                        isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
-                    )
-                }
-            } else {
-                repository.users
+            combine(
+                userInfos,
+                repository.selectedUserInfo,
+                repository.userSwitcherSettings,
+            ) { userInfos, selectedUserInfo, settings ->
+                toUserModels(
+                    userInfos = userInfos,
+                    selectedUserId = selectedUserInfo.id,
+                    isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+                )
             }
 
     /** The currently-selected user. */
     val selectedUser: Flow<UserModel>
         get() =
-            if (isNewImpl) {
-                combine(
-                    repository.selectedUserInfo,
-                    repository.userSwitcherSettings,
-                ) { selectedUserInfo, settings ->
-                    val selectedUserId = selectedUserInfo.id
-                    checkNotNull(
-                        toUserModel(
-                            userInfo = selectedUserInfo,
-                            selectedUserId = selectedUserId,
-                            canSwitchUsers = canSwitchUsers(selectedUserId),
-                            isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
-                        )
+            combine(
+                repository.selectedUserInfo,
+                repository.userSwitcherSettings,
+            ) { selectedUserInfo, settings ->
+                val selectedUserId = selectedUserInfo.id
+                checkNotNull(
+                    toUserModel(
+                        userInfo = selectedUserInfo,
+                        selectedUserId = selectedUserId,
+                        canSwitchUsers = canSwitchUsers(selectedUserId),
+                        isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
                     )
-                }
-            } else {
-                repository.selectedUser
+                )
             }
 
     /** List of user-switcher related actions that are available. */
     val actions: Flow<List<UserActionModel>>
         get() =
-            if (isNewImpl) {
-                combine(
-                    repository.selectedUserInfo,
-                    repository.userInfos,
-                    repository.userSwitcherSettings,
-                    keyguardInteractor.isKeyguardShowing,
-                ) { _, userInfos, settings, isDeviceLocked ->
-                    buildList {
-                        val hasGuestUser = userInfos.any { it.isGuest }
-                        if (
-                            !hasGuestUser &&
-                                (guestUserInteractor.isGuestUserAutoCreated ||
-                                    UserActionsUtil.canCreateGuest(
-                                        manager,
-                                        repository,
-                                        settings.isUserSwitcherEnabled,
-                                        settings.isAddUsersFromLockscreen,
-                                    ))
-                        ) {
-                            add(UserActionModel.ENTER_GUEST_MODE)
-                        }
+            combine(
+                repository.selectedUserInfo,
+                userInfos,
+                repository.userSwitcherSettings,
+                keyguardInteractor.isKeyguardShowing,
+            ) { _, userInfos, settings, isDeviceLocked ->
+                buildList {
+                    val hasGuestUser = userInfos.any { it.isGuest }
+                    if (!hasGuestUser && canCreateGuestUser(settings)) {
+                        add(UserActionModel.ENTER_GUEST_MODE)
+                    }
 
-                        if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
-                            // The device is locked and our setting to allow actions that add users
-                            // from the lock-screen is not enabled. The guest action from above is
-                            // always allowed, even when the device is locked, but the various "add
-                            // user" actions below are not. We can finish building the list here.
+                    if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
+                        // The device is locked and our setting to allow actions that add users
+                        // from the lock-screen is not enabled. The guest action from above is
+                        // always allowed, even when the device is locked, but the various "add
+                        // user" actions below are not. We can finish building the list here.
 
-                            val canCreateUsers =
-                                UserActionsUtil.canCreateUser(
-                                    manager,
-                                    repository,
-                                    settings.isUserSwitcherEnabled,
-                                    settings.isAddUsersFromLockscreen,
-                                )
-
-                            if (canCreateUsers) {
-                                add(UserActionModel.ADD_USER)
-                            }
-
-                            if (
-                                UserActionsUtil.canCreateSupervisedUser(
-                                    manager,
-                                    repository,
-                                    settings.isUserSwitcherEnabled,
-                                    settings.isAddUsersFromLockscreen,
-                                    supervisedUserPackageName,
-                                )
-                            ) {
-                                add(UserActionModel.ADD_SUPERVISED_USER)
-                            }
-                        }
-
-                        if (
-                            UserActionsUtil.canManageUsers(
+                        val canCreateUsers =
+                            UserActionsUtil.canCreateUser(
+                                manager,
                                 repository,
                                 settings.isUserSwitcherEnabled,
                                 settings.isAddUsersFromLockscreen,
                             )
-                        ) {
-                            add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+                        if (canCreateUsers) {
+                            add(UserActionModel.ADD_USER)
                         }
+
+                        if (
+                            UserActionsUtil.canCreateSupervisedUser(
+                                manager,
+                                repository,
+                                settings.isUserSwitcherEnabled,
+                                settings.isAddUsersFromLockscreen,
+                                supervisedUserPackageName,
+                            )
+                        ) {
+                            add(UserActionModel.ADD_SUPERVISED_USER)
+                        }
+                    }
+
+                    if (
+                        UserActionsUtil.canManageUsers(
+                            repository,
+                            settings.isUserSwitcherEnabled,
+                            settings.isAddUsersFromLockscreen,
+                        )
+                    ) {
+                        add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
                     }
                 }
-            } else {
-                combine(
-                        repository.isActionableWhenLocked,
-                        keyguardInteractor.isKeyguardShowing,
-                    ) { isActionableWhenLocked, isLocked ->
-                        isActionableWhenLocked || !isLocked
-                    }
-                    .flatMapLatest { isActionable ->
-                        if (isActionable) {
-                            repository.actions.map { actions ->
-                                actions +
-                                    if (actions.isNotEmpty()) {
-                                        // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
-                                        // because that's a user switcher specific action that is
-                                        // not known to the our data source or other features.
-                                        listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                                    } else {
-                                        // If no actions, don't add the navigate action.
-                                        emptyList()
-                                    }
-                            }
-                        } else {
-                            // If not actionable it means that we're not allowed to show actions
-                            // when
-                            // locked and we are locked. Therefore, we should show no actions.
-                            flowOf(emptyList())
-                        }
-                    }
             }
 
     val userRecords: StateFlow<ArrayList<UserRecord>> =
-        if (isNewImpl) {
-            combine(
-                    repository.userInfos,
-                    repository.selectedUserInfo,
-                    actions,
-                    repository.userSwitcherSettings,
-                ) { userInfos, selectedUserInfo, actionModels, settings ->
-                    ArrayList(
-                        userInfos.map {
+        combine(
+                userInfos,
+                repository.selectedUserInfo,
+                actions,
+                repository.userSwitcherSettings,
+            ) { userInfos, selectedUserInfo, actionModels, settings ->
+                ArrayList(
+                    userInfos.map {
+                        toRecord(
+                            userInfo = it,
+                            selectedUserId = selectedUserInfo.id,
+                        )
+                    } +
+                        actionModels.map {
                             toRecord(
-                                userInfo = it,
+                                action = it,
                                 selectedUserId = selectedUserInfo.id,
+                                isRestricted =
+                                    it != UserActionModel.ENTER_GUEST_MODE &&
+                                        it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
+                                        !settings.isAddUsersFromLockscreen,
                             )
-                        } +
-                            actionModels.map {
-                                toRecord(
-                                    action = it,
-                                    selectedUserId = selectedUserInfo.id,
-                                    isRestricted =
-                                        it != UserActionModel.ENTER_GUEST_MODE &&
-                                            it != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT &&
-                                            !settings.isAddUsersFromLockscreen,
-                                )
-                            }
-                    )
-                }
-                .onEach { notifyCallbacks() }
-                .stateIn(
-                    scope = applicationScope,
-                    started = SharingStarted.Eagerly,
-                    initialValue = ArrayList(),
+                        }
                 )
-        } else {
-            MutableStateFlow(ArrayList())
-        }
+            }
+            .onEach { notifyCallbacks() }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = ArrayList(),
+            )
 
     val selectedUserRecord: StateFlow<UserRecord?> =
-        if (isNewImpl) {
-            repository.selectedUserInfo
-                .map { selectedUserInfo ->
-                    toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
-                }
-                .stateIn(
-                    scope = applicationScope,
-                    started = SharingStarted.Eagerly,
-                    initialValue = null,
-                )
-        } else {
-            MutableStateFlow(null)
-        }
+        repository.selectedUserInfo
+            .map { selectedUserInfo ->
+                toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = null,
+            )
 
     /** Whether the device is configured to always have a guest user available. */
     val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
@@ -315,6 +261,9 @@
     /** Whether the guest user is currently being reset. */
     val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
 
+    /** Whether to enable the user chip in the status bar */
+    val isStatusBarUserChipEnabled: Boolean = repository.isStatusBarUserChipEnabled
+
     private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
     val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
 
@@ -322,44 +271,37 @@
     val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
 
     val isSimpleUserSwitcher: Boolean
-        get() =
-            if (isNewImpl) {
-                repository.isSimpleUserSwitcher()
-            } else {
-                error("Not supported in the old implementation!")
-            }
+        get() = repository.isSimpleUserSwitcher()
 
     init {
-        if (isNewImpl) {
-            refreshUsersScheduler.refreshIfNotPaused()
-            telephonyInteractor.callState
-                .distinctUntilChanged()
-                .onEach { refreshUsersScheduler.refreshIfNotPaused() }
-                .launchIn(applicationScope)
+        refreshUsersScheduler.refreshIfNotPaused()
+        telephonyInteractor.callState
+            .distinctUntilChanged()
+            .onEach { refreshUsersScheduler.refreshIfNotPaused() }
+            .launchIn(applicationScope)
 
-            combine(
-                    broadcastDispatcher.broadcastFlow(
-                        filter =
-                            IntentFilter().apply {
-                                addAction(Intent.ACTION_USER_ADDED)
-                                addAction(Intent.ACTION_USER_REMOVED)
-                                addAction(Intent.ACTION_USER_INFO_CHANGED)
-                                addAction(Intent.ACTION_USER_SWITCHED)
-                                addAction(Intent.ACTION_USER_STOPPED)
-                                addAction(Intent.ACTION_USER_UNLOCKED)
-                            },
-                        user = UserHandle.SYSTEM,
-                        map = { intent, _ -> intent },
-                    ),
-                    repository.selectedUserInfo.pairwise(null),
-                ) { intent, selectedUserChange ->
-                    Pair(intent, selectedUserChange.previousValue)
-                }
-                .onEach { (intent, previousSelectedUser) ->
-                    onBroadcastReceived(intent, previousSelectedUser)
-                }
-                .launchIn(applicationScope)
-        }
+        combine(
+                broadcastDispatcher.broadcastFlow(
+                    filter =
+                        IntentFilter().apply {
+                            addAction(Intent.ACTION_USER_ADDED)
+                            addAction(Intent.ACTION_USER_REMOVED)
+                            addAction(Intent.ACTION_USER_INFO_CHANGED)
+                            addAction(Intent.ACTION_USER_SWITCHED)
+                            addAction(Intent.ACTION_USER_STOPPED)
+                            addAction(Intent.ACTION_USER_UNLOCKED)
+                        },
+                    user = UserHandle.SYSTEM,
+                    map = { intent, _ -> intent },
+                ),
+                repository.selectedUserInfo.pairwise(null),
+            ) { intent, selectedUserChange ->
+                Pair(intent, selectedUserChange.previousValue)
+            }
+            .onEach { (intent, previousSelectedUser) ->
+                onBroadcastReceived(intent, previousSelectedUser)
+            }
+            .launchIn(applicationScope)
     }
 
     fun addCallback(callback: UserCallback) {
@@ -425,46 +367,43 @@
         newlySelectedUserId: Int,
         dialogShower: UserSwitchDialogController.DialogShower? = null,
     ) {
-        if (isNewImpl) {
-            val currentlySelectedUserInfo = repository.getSelectedUserInfo()
-            if (
-                newlySelectedUserId == currentlySelectedUserInfo.id &&
-                    currentlySelectedUserInfo.isGuest
-            ) {
-                // Here when clicking on the currently-selected guest user to leave guest mode
-                // and return to the previously-selected non-guest user.
-                showDialog(
-                    ShowDialogRequestModel.ShowExitGuestDialog(
-                        guestUserId = currentlySelectedUserInfo.id,
-                        targetUserId = repository.lastSelectedNonGuestUserId,
-                        isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
-                        isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
-                        onExitGuestUser = this::exitGuestUser,
-                    )
+        val currentlySelectedUserInfo = repository.getSelectedUserInfo()
+        if (
+            newlySelectedUserId == currentlySelectedUserInfo.id && currentlySelectedUserInfo.isGuest
+        ) {
+            // Here when clicking on the currently-selected guest user to leave guest mode
+            // and return to the previously-selected non-guest user.
+            showDialog(
+                ShowDialogRequestModel.ShowExitGuestDialog(
+                    guestUserId = currentlySelectedUserInfo.id,
+                    targetUserId = repository.lastSelectedNonGuestUserId,
+                    isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+                    isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                    onExitGuestUser = this::exitGuestUser,
+                    dialogShower = dialogShower,
                 )
-                return
-            }
-
-            if (currentlySelectedUserInfo.isGuest) {
-                // Here when switching from guest to a non-guest user.
-                showDialog(
-                    ShowDialogRequestModel.ShowExitGuestDialog(
-                        guestUserId = currentlySelectedUserInfo.id,
-                        targetUserId = newlySelectedUserId,
-                        isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
-                        isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
-                        onExitGuestUser = this::exitGuestUser,
-                    )
-                )
-                return
-            }
-
-            dialogShower?.dismiss()
-
-            switchUser(newlySelectedUserId)
-        } else {
-            controller.onUserSelected(newlySelectedUserId, dialogShower)
+            )
+            return
         }
+
+        if (currentlySelectedUserInfo.isGuest) {
+            // Here when switching from guest to a non-guest user.
+            showDialog(
+                ShowDialogRequestModel.ShowExitGuestDialog(
+                    guestUserId = currentlySelectedUserInfo.id,
+                    targetUserId = newlySelectedUserId,
+                    isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+                    isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                    onExitGuestUser = this::exitGuestUser,
+                    dialogShower = dialogShower,
+                )
+            )
+            return
+        }
+
+        dialogShower?.dismiss()
+
+        switchUser(newlySelectedUserId)
     }
 
     /** Executes the given action. */
@@ -472,50 +411,38 @@
         action: UserActionModel,
         dialogShower: UserSwitchDialogController.DialogShower? = null,
     ) {
-        if (isNewImpl) {
-            when (action) {
-                UserActionModel.ENTER_GUEST_MODE ->
-                    guestUserInteractor.createAndSwitchTo(
-                        this::showDialog,
-                        this::dismissDialog,
-                    ) { userId ->
-                        selectUser(userId, dialogShower)
-                    }
-                UserActionModel.ADD_USER -> {
-                    val currentUser = repository.getSelectedUserInfo()
-                    showDialog(
-                        ShowDialogRequestModel.ShowAddUserDialog(
-                            userHandle = currentUser.userHandle,
-                            isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
-                            showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
-                        )
-                    )
+        when (action) {
+            UserActionModel.ENTER_GUEST_MODE ->
+                guestUserInteractor.createAndSwitchTo(
+                    this::showDialog,
+                    this::dismissDialog,
+                ) { userId ->
+                    selectUser(userId, dialogShower)
                 }
-                UserActionModel.ADD_SUPERVISED_USER ->
-                    activityStarter.startActivity(
-                        Intent()
-                            .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
-                            .setPackage(supervisedUserPackageName)
-                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
-                        /* dismissShade= */ true,
+            UserActionModel.ADD_USER -> {
+                val currentUser = repository.getSelectedUserInfo()
+                showDialog(
+                    ShowDialogRequestModel.ShowAddUserDialog(
+                        userHandle = currentUser.userHandle,
+                        isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+                        showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+                        dialogShower = dialogShower,
                     )
-                UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
-                    activityStarter.startActivity(
-                        Intent(Settings.ACTION_USER_SETTINGS),
-                        /* dismissShade= */ true,
-                    )
+                )
             }
-        } else {
-            when (action) {
-                UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
-                UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
-                UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
-                UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
-                    activityStarter.startActivity(
-                        Intent(Settings.ACTION_USER_SETTINGS),
-                        /* dismissShade= */ false,
-                    )
-            }
+            UserActionModel.ADD_SUPERVISED_USER ->
+                activityStarter.startActivity(
+                    Intent()
+                        .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
+                        .setPackage(supervisedUserPackageName)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+                    /* dismissShade= */ true,
+                )
+            UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+                activityStarter.startActivity(
+                    Intent(Settings.ACTION_USER_SETTINGS),
+                    /* dismissShade= */ true,
+                )
         }
     }
 
@@ -549,6 +476,26 @@
         }
     }
 
+    fun showUserSwitcher(context: Context, expandable: Expandable) {
+        if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
+            showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog)
+            return
+        }
+
+        val intent =
+            Intent(context, UserSwitcherActivity::class.java).apply {
+                addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+            }
+
+        activityStarter.startActivity(
+            intent,
+            true /* dismissShade */,
+            expandable.activityLaunchController(),
+            true /* showOverlockscreenwhenlocked */,
+            UserHandle.SYSTEM,
+        )
+    }
+
     private fun showDialog(request: ShowDialogRequestModel) {
         _dialogShowRequests.value = request
     }
@@ -765,6 +712,16 @@
         )
     }
 
+    private fun canCreateGuestUser(settings: UserSwitcherSettingsModel): Boolean {
+        return guestUserInteractor.isGuestUserAutoCreated ||
+            UserActionsUtil.canCreateGuest(
+                manager,
+                repository,
+                settings.isUserSwitcherEnabled,
+                settings.isAddUsersFromLockscreen,
+            )
+    }
+
     companion object {
         private const val TAG = "UserInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
index 08d7c5a..85c2964 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -18,14 +18,18 @@
 package com.android.systemui.user.domain.model
 
 import android.os.UserHandle
+import com.android.systemui.qs.user.UserSwitchDialogController
 
 /** Encapsulates a request to show a dialog. */
-sealed class ShowDialogRequestModel {
+sealed class ShowDialogRequestModel(
+    open val dialogShower: UserSwitchDialogController.DialogShower? = null,
+) {
     data class ShowAddUserDialog(
         val userHandle: UserHandle,
         val isKeyguardShowing: Boolean,
         val showEphemeralMessage: Boolean,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
 
     data class ShowUserCreationDialog(
         val isGuest: Boolean,
@@ -37,5 +41,9 @@
         val isGuestEphemeral: Boolean,
         val isKeyguardShowing: Boolean,
         val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
+
+    /** Show the user switcher dialog */
+    object ShowUserSwitcherDialog : ShowDialogRequestModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt
new file mode 100644
index 0000000..8e40f68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/StatusBarUserChipViewBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.user.ui.binder
+
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.ui.binder.TextViewBinder
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@OptIn(InternalCoroutinesApi::class)
+object StatusBarUserChipViewBinder {
+    /** Binds the status bar user chip view model to the given view */
+    @JvmStatic
+    fun bind(
+        view: StatusBarUserSwitcherContainer,
+        viewModel: StatusBarUserChipViewModel,
+    ) {
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.isChipVisible.collect { isVisible -> view.isVisible = isVisible }
+                }
+
+                launch {
+                    viewModel.userName.collect { name -> TextViewBinder.bind(view.text, name) }
+                }
+
+                launch {
+                    viewModel.userAvatar.collect { avatar -> view.avatar.setImageDrawable(avatar) }
+                }
+
+                bindButton(view, viewModel)
+            }
+        }
+    }
+
+    private fun bindButton(
+        view: StatusBarUserSwitcherContainer,
+        viewModel: StatusBarUserChipViewModel,
+    ) {
+        view.setOnClickListener { viewModel.onClick(Expandable.fromView(view)) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index 938417f..e137107 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.user.ui.binder
 
 import android.content.Context
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.widget.BaseAdapter
 import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE
 import android.widget.TextView
 import androidx.constraintlayout.helper.widget.Flow as FlowWidget
 import androidx.core.view.isVisible
@@ -36,6 +39,7 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.user.UserSwitcherPopupMenu
 import com.android.systemui.user.UserSwitcherRootView
+import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.util.children
@@ -57,14 +61,15 @@
         falsingCollector: FalsingCollector,
         onFinish: () -> Unit,
     ) {
-        val rootView: UserSwitcherRootView = view.requireViewById(R.id.user_switcher_root)
-        val flowWidget: FlowWidget = view.requireViewById(R.id.flow)
+        val gridContainerView: UserSwitcherRootView =
+            view.requireViewById(R.id.user_switcher_grid_container)
+        val flowWidget: FlowWidget = gridContainerView.requireViewById(R.id.flow)
         val addButton: View = view.requireViewById(R.id.add)
         val cancelButton: View = view.requireViewById(R.id.cancel)
         val popupMenuAdapter = MenuAdapter(layoutInflater)
         var popupMenu: UserSwitcherPopupMenu? = null
 
-        rootView.touchHandler =
+        gridContainerView.touchHandler =
             object : Gefingerpoken {
                 override fun onTouchEvent(ev: MotionEvent?): Boolean {
                     falsingCollector.onTouchEvent(ev)
@@ -128,9 +133,11 @@
                 launch {
                     viewModel.users.collect { users ->
                         val viewPool =
-                            view.children.filter { it.tag == USER_VIEW_TAG }.toMutableList()
+                            gridContainerView.children
+                                .filter { it.tag == USER_VIEW_TAG }
+                                .toMutableList()
                         viewPool.forEach {
-                            view.removeView(it)
+                            gridContainerView.removeView(it)
                             flowWidget.removeView(it)
                         }
                         users.forEach { userViewModel ->
@@ -148,7 +155,7 @@
                                     inflatedView
                                 }
                             userView.id = View.generateViewId()
-                            view.addView(userView)
+                            gridContainerView.addView(userView)
                             flowWidget.addView(userView)
                             UserViewBinder.bind(
                                 view = userView,
@@ -168,15 +175,10 @@
         onDismissed: () -> Unit,
     ): UserSwitcherPopupMenu {
         return UserSwitcherPopupMenu(context).apply {
+            this.setDropDownGravity(Gravity.END)
             this.anchorView = anchorView
             setAdapter(adapter)
             setOnDismissListener { onDismissed() }
-            setOnItemClickListener { _, _, position, _ ->
-                val itemPositionExcludingHeader = position - 1
-                adapter.getItem(itemPositionExcludingHeader).onClicked()
-                dismiss()
-            }
-
             show()
         }
     }
@@ -186,38 +188,67 @@
         private val layoutInflater: LayoutInflater,
     ) : BaseAdapter() {
 
-        private val items = mutableListOf<UserActionViewModel>()
+        private var sections = listOf<List<UserActionViewModel>>()
 
         override fun getCount(): Int {
-            return items.size
+            return sections.size
         }
 
-        override fun getItem(position: Int): UserActionViewModel {
-            return items[position]
+        override fun getItem(position: Int): List<UserActionViewModel> {
+            return sections[position]
         }
 
         override fun getItemId(position: Int): Long {
-            return getItem(position).viewKey
+            return position.toLong()
         }
 
         override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
-            val view =
-                convertView
-                    ?: layoutInflater.inflate(
+            val section = getItem(position)
+            val context = parent.context
+            val sectionView =
+                convertView as? LinearLayout
+                    ?: LinearLayout(context, null).apply {
+                        this.orientation = LinearLayout.VERTICAL
+                        this.background =
+                            parent.resources.getDrawable(
+                                R.drawable.bouncer_user_switcher_popup_bg,
+                                context.theme
+                            )
+                        this.showDividers = SHOW_DIVIDER_MIDDLE
+                        this.dividerDrawable =
+                            context.getDrawable(
+                                R.drawable.fullscreen_userswitcher_menu_item_divider
+                            )
+                    }
+            sectionView.removeAllViewsInLayout()
+
+            for (viewModel in section) {
+                val view =
+                    layoutInflater.inflate(
                         R.layout.user_switcher_fullscreen_popup_item,
-                        parent,
-                        false
+                        /* parent= */ null
                     )
-            val viewModel = getItem(position)
-            view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId)
-            view.requireViewById<TextView>(R.id.text).text =
-                view.resources.getString(viewModel.textResourceId)
-            return view
+                view
+                    .requireViewById<ImageView>(R.id.icon)
+                    .setImageResource(viewModel.iconResourceId)
+                view.requireViewById<TextView>(R.id.text).text =
+                    view.resources.getString(viewModel.textResourceId)
+                view.setOnClickListener { viewModel.onClicked() }
+                sectionView.addView(view)
+            }
+            return sectionView
         }
 
         fun setItems(items: List<UserActionViewModel>) {
-            this.items.clear()
-            this.items.addAll(items)
+            val primarySection =
+                items.filter {
+                    it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            val secondarySection =
+                items.filter {
+                    it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            this.sections = listOf(primarySection, secondarySection)
             notifyDataSetChanged()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
new file mode 100644
index 0000000..ed25898
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitchDialog.kt
@@ -0,0 +1,68 @@
+package com.android.systemui.user.ui.dialog
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.view.LayoutInflater
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.tiles.UserDetailView
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/**
+ * Extracted from the old UserSwitchDialogController. This is the dialog version of the full-screen
+ * user switcher. See config_enableFullscreenUserSwitcher
+ */
+class UserSwitchDialog(
+    context: Context,
+    adapter: UserDetailView.Adapter,
+    uiEventLogger: UiEventLogger,
+    falsingManager: FalsingManager,
+    activityStarter: ActivityStarter,
+    dialogLaunchAnimator: DialogLaunchAnimator,
+) : SystemUIDialog(context) {
+    init {
+        setShowForAllUsers(true)
+        setCanceledOnTouchOutside(true)
+        setTitle(R.string.qs_user_switch_dialog_title)
+        setPositiveButton(R.string.quick_settings_done) { _, _ ->
+            uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE)
+        }
+        setNeutralButton(
+            R.string.quick_settings_more_user_settings,
+            { _, _ ->
+                if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                    uiEventLogger.log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS)
+                    val controller =
+                        dialogLaunchAnimator.createActivityLaunchController(
+                            getButton(BUTTON_NEUTRAL)
+                        )
+
+                    if (controller == null) {
+                        dismiss()
+                    }
+
+                    activityStarter.postStartActivityDismissingKeyguard(
+                        USER_SETTINGS_INTENT,
+                        0,
+                        controller
+                    )
+                }
+            },
+            false /* dismissOnClick */
+        )
+        val gridFrame =
+            LayoutInflater.from(this.context).inflate(R.layout.qs_user_dialog_content, null)
+        setView(gridFrame)
+
+        adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
+    }
+
+    companion object {
+        private val USER_SETTINGS_INTENT = Intent(Settings.ACTION_USER_SETTINGS)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 91c5921..4141054 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -19,20 +19,24 @@
 
 import android.app.Dialog
 import android.content.Context
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.users.UserCreatingDialog
 import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.tiles.UserDetailView
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import dagger.Lazy
 import javax.inject.Inject
+import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
 
@@ -41,82 +45,121 @@
 class UserSwitcherDialogCoordinator
 @Inject
 constructor(
-    @Application private val context: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val falsingManager: FalsingManager,
-    private val broadcastSender: BroadcastSender,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val interactor: UserInteractor,
-    private val featureFlags: FeatureFlags,
+    @Application private val context: Lazy<Context>,
+    @Application private val applicationScope: Lazy<CoroutineScope>,
+    private val falsingManager: Lazy<FalsingManager>,
+    private val broadcastSender: Lazy<BroadcastSender>,
+    private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
+    private val interactor: Lazy<UserInteractor>,
+    private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>,
+    private val eventLogger: Lazy<UiEventLogger>,
+    private val activityStarter: Lazy<ActivityStarter>,
 ) : CoreStartable {
 
     private var currentDialog: Dialog? = null
 
     override fun start() {
-        if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
-            return
-        }
-
         startHandlingDialogShowRequests()
         startHandlingDialogDismissRequests()
     }
 
     private fun startHandlingDialogShowRequests() {
-        applicationScope.launch {
-            interactor.dialogShowRequests.filterNotNull().collect { request ->
+        applicationScope.get().launch {
+            interactor.get().dialogShowRequests.filterNotNull().collect { request ->
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                currentDialog =
+                val (dialog, dialogCuj) =
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
-                            AddUserDialog(
-                                context = context,
-                                userHandle = request.userHandle,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                showEphemeralMessage = request.showEphemeralMessage,
-                                falsingManager = falsingManager,
-                                broadcastSender = broadcastSender,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
+                            Pair(
+                                AddUserDialog(
+                                    context = context.get(),
+                                    userHandle = request.userHandle,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    showEphemeralMessage = request.showEphemeralMessage,
+                                    falsingManager = falsingManager.get(),
+                                    broadcastSender = broadcastSender.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_ADD_NEW_USER_TAG,
+                                ),
                             )
                         is ShowDialogRequestModel.ShowUserCreationDialog ->
-                            UserCreatingDialog(
-                                context,
-                                request.isGuest,
+                            Pair(
+                                UserCreatingDialog(
+                                    context.get(),
+                                    request.isGuest,
+                                ),
+                                null,
                             )
                         is ShowDialogRequestModel.ShowExitGuestDialog ->
-                            ExitGuestDialog(
-                                context = context,
-                                guestUserId = request.guestUserId,
-                                isGuestEphemeral = request.isGuestEphemeral,
-                                targetUserId = request.targetUserId,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                falsingManager = falsingManager,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
-                                onExitGuestUserListener = request.onExitGuestUser,
+                            Pair(
+                                ExitGuestDialog(
+                                    context = context.get(),
+                                    guestUserId = request.guestUserId,
+                                    isGuestEphemeral = request.isGuestEphemeral,
+                                    targetUserId = request.targetUserId,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    falsingManager = falsingManager.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                    onExitGuestUserListener = request.onExitGuestUser,
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
+                                ),
+                            )
+                        is ShowDialogRequestModel.ShowUserSwitcherDialog ->
+                            Pair(
+                                UserSwitchDialog(
+                                    context = context.get(),
+                                    adapter = userDetailAdapterProvider.get(),
+                                    uiEventLogger = eventLogger.get(),
+                                    falsingManager = falsingManager.get(),
+                                    activityStarter = activityStarter.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
+                                ),
                             )
                     }
+                currentDialog = dialog
 
-                currentDialog?.show()
-                interactor.onDialogShown()
+                if (request.dialogShower != null && dialogCuj != null) {
+                    request.dialogShower?.showDialog(dialog, dialogCuj)
+                } else {
+                    dialog.show()
+                }
+
+                interactor.get().onDialogShown()
             }
         }
     }
 
     private fun startHandlingDialogDismissRequests() {
-        applicationScope.launch {
-            interactor.dialogDismissRequests.filterNotNull().collect {
+        applicationScope.get().launch {
+            interactor.get().dialogDismissRequests.filterNotNull().collect {
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                interactor.onDialogDismissed()
+                interactor.get().onDialogDismissed()
             }
         }
     }
+
+    companion object {
+        private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"
+        private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt
new file mode 100644
index 0000000..3300e8e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.user.ui.viewmodel
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.user.domain.interactor.UserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StatusBarUserChipViewModel
+@Inject
+constructor(
+    @Application private val context: Context,
+    interactor: UserInteractor,
+) {
+    /** Whether the status bar chip ui should be available */
+    val chipEnabled: Boolean = interactor.isStatusBarUserChipEnabled
+
+    /** Whether or not the chip should be showing, based on the number of users */
+    val isChipVisible: Flow<Boolean> =
+        if (!chipEnabled) {
+            flowOf(false)
+        } else {
+            interactor.users.mapLatest { users -> users.size > 1 }
+        }
+
+    /** The display name of the current user */
+    val userName: Flow<Text> = interactor.selectedUser.mapLatest { userModel -> userModel.name }
+
+    /** Avatar for the current user */
+    val userAvatar: Flow<Drawable> =
+        interactor.selectedUser.mapLatest { userModel -> userModel.image }
+
+    /** Action to execute on click. Should launch the user switcher */
+    val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 219dae2..0910ea3 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -20,8 +20,6 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.android.systemui.common.ui.drawable.CircularDrawable
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
 import com.android.systemui.user.domain.interactor.UserInteractor
@@ -41,12 +39,8 @@
     private val userInteractor: UserInteractor,
     private val guestUserInteractor: GuestUserInteractor,
     private val powerInteractor: PowerInteractor,
-    private val featureFlags: FeatureFlags,
 ) : ViewModel() {
 
-    private val isNewImpl: Boolean
-        get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
-
     /** On-device users. */
     val users: Flow<List<UserViewModel>> =
         userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -62,17 +56,7 @@
     val isMenuVisible: Flow<Boolean> = _isMenuVisible
     /** The user action menu. */
     val menu: Flow<List<UserActionViewModel>> =
-        userInteractor.actions.map { actions ->
-            if (isNewImpl && actions.isNotEmpty()) {
-                    // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
-                    // switcher specific action that is not known to the our data source or other
-                    // features.
-                    actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                } else {
-                    actions
-                }
-                .map { action -> toViewModel(action) }
-        }
+        userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
 
     /** Whether the button to open the user action menu is visible. */
     val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
@@ -226,7 +210,6 @@
         private val userInteractor: UserInteractor,
         private val guestUserInteractor: GuestUserInteractor,
         private val powerInteractor: PowerInteractor,
-        private val featureFlags: FeatureFlags,
     ) : ViewModelProvider.Factory {
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
             @Suppress("UNCHECKED_CAST")
@@ -234,7 +217,6 @@
                 userInteractor = userInteractor,
                 guestUserInteractor = guestUserInteractor,
                 powerInteractor = powerInteractor,
-                featureFlags = featureFlags,
             )
                 as T
         }
diff --git a/packages/SystemUI/src/com/android/systemui/util/BrightnessProgressDrawable.kt b/packages/SystemUI/src/com/android/systemui/util/BrightnessProgressDrawable.kt
new file mode 100644
index 0000000..12a0c03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/BrightnessProgressDrawable.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.util
+
+import android.content.pm.ActivityInfo
+import android.content.res.Resources
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.DrawableWrapper
+import android.graphics.drawable.InsetDrawable
+
+/**
+ * [DrawableWrapper] to use in the progress of brightness slider.
+ *
+ * This drawable is used to change the bounds of the enclosed drawable depending on the level to
+ * simulate a sliding progress, instead of using clipping or scaling. That way, the shape of the
+ * edges is maintained.
+ *
+ * Meant to be used with a rounded ends background, it will also prevent deformation when the slider
+ * is meant to be smaller than the rounded corner. The background should have rounded corners that
+ * are half of the height.
+ *
+ * This class also assumes that a "thumb" icon exists within the end's edge of the progress
+ * drawable, and the slider's width, when interacted on, if offset by half the size of the thumb
+ * icon which puts the icon directly underneath the user's finger.
+ */
+class BrightnessProgressDrawable @JvmOverloads constructor(drawable: Drawable? = null) :
+    InsetDrawable(drawable, 0) {
+
+    companion object {
+        private const val MAX_LEVEL = 10000 // Taken from Drawable
+    }
+
+    override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
+        onLevelChange(level)
+        return super.onLayoutDirectionChanged(layoutDirection)
+    }
+
+    override fun onBoundsChange(bounds: Rect) {
+        super.onBoundsChange(bounds)
+        onLevelChange(level)
+    }
+
+    override fun onLevelChange(level: Int): Boolean {
+        val db = drawable?.bounds!!
+
+        // The thumb offset shifts the sun icon directly under the user's thumb
+        val thumbOffset = bounds.height() / 2
+        val width = bounds.width() * level / MAX_LEVEL + thumbOffset
+
+        // On 0, the width is bounds.height (a circle), and on MAX_LEVEL, the width is bounds.width
+        drawable?.setBounds(
+            bounds.left,
+            db.top,
+            width.coerceAtMost(bounds.width()).coerceAtLeast(bounds.height()),
+            db.bottom
+        )
+        return super.onLevelChange(level)
+    }
+
+    override fun getConstantState(): ConstantState {
+        // This should not be null as it was created with a state in the constructor.
+        return RoundedCornerState(super.getConstantState()!!)
+    }
+
+    override fun getChangingConfigurations(): Int {
+        return super.getChangingConfigurations() or ActivityInfo.CONFIG_DENSITY
+    }
+
+    override fun canApplyTheme(): Boolean {
+        return (drawable?.canApplyTheme() ?: false) || super.canApplyTheme()
+    }
+
+    private class RoundedCornerState(private val wrappedState: ConstantState) : ConstantState() {
+        override fun newDrawable(): Drawable {
+            return newDrawable(null, null)
+        }
+
+        override fun newDrawable(res: Resources?, theme: Resources.Theme?): Drawable {
+            val wrapper = wrappedState.newDrawable(res, theme) as DrawableWrapper
+            return BrightnessProgressDrawable(wrapper.drawable)
+        }
+
+        override fun getChangingConfigurations(): Int {
+            return wrappedState.changingConfigurations
+        }
+
+        override fun canApplyTheme(): Boolean {
+            return true
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
index 6b5556b..0f3eddf 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/util/DeviceConfigProxy.java
@@ -19,7 +19,6 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.Context;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 
@@ -53,8 +52,8 @@
     /**
      * Wrapped version of {@link DeviceConfig#enforceReadPermission}.
      */
-    public void enforceReadPermission(Context context, String namespace) {
-        DeviceConfig.enforceReadPermission(context, namespace);
+    public void enforceReadPermission(String namespace) {
+        DeviceConfig.enforceReadPermission(namespace);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
index 99eb03b..1059d6c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/RoundedCornerProgressDrawable.kt
@@ -33,11 +33,6 @@
  * Meant to be used with a rounded ends background, it will also prevent deformation when the slider
  * is meant to be smaller than the rounded corner. The background should have rounded corners that
  * are half of the height.
- *
- * This class also assumes that a "thumb" icon exists within the end's edge of the progress
- * drawable, and the slider's width, when interacted on, if offset by half the size of the thumb
- * icon which puts the icon directly underneath the user's finger.
- *
  */
 class RoundedCornerProgressDrawable @JvmOverloads constructor(
     drawable: Drawable? = null
@@ -59,16 +54,9 @@
 
     override fun onLevelChange(level: Int): Boolean {
         val db = drawable?.bounds!!
-
-        // The thumb offset shifts the sun icon directly under the user's thumb
-        val thumbOffset = bounds.height() / 2
-        val width = bounds.width() * level / MAX_LEVEL + thumbOffset
-
         // On 0, the width is bounds.height (a circle), and on MAX_LEVEL, the width is bounds.width
-        drawable?.setBounds(
-            bounds.left, db.top,
-            width.coerceAtMost(bounds.width()).coerceAtLeast(bounds.height()), db.bottom
-        )
+        val width = bounds.height() + (bounds.width() - bounds.height()) * level / MAX_LEVEL
+        drawable?.setBounds(bounds.left, db.top, bounds.left + width, db.bottom)
         return super.onLevelChange(level)
     }
 
@@ -103,4 +91,4 @@
             return true
         }
     }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
deleted file mode 100644
index 97dc842..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.collection
-
-import kotlin.math.max
-
-/**
- * A simple ring buffer implementation
- *
- * Use [advance] to get the least recent item in the buffer (and then presumably fill it with
- * appropriate data). This will cause it to become the most recent item.
- *
- * As the buffer is used, it will grow, allocating new instances of T using [factory] until it
- * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
- * instances will be recycled from the back of the buffer and placed at the front.
- *
- * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
- * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
- * growing to [maxSize].
- */
-class RingBuffer<T>(
-    private val maxSize: Int,
-    private val factory: () -> T
-) : Iterable<T> {
-
-    private val buffer = MutableList<T?>(maxSize) { null }
-
-    /**
-     * An abstract representation that points to the "end" of the buffer. Increments every time
-     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
-     * the backing array. Always points to the "next" available slot in the buffer. Before the
-     * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
-     * value at the "beginning" of the buffer.
-     *
-     * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
-     * omega will overflow after a little under three million years of continuous operation.
-     */
-    private var omega: Long = 0
-
-    /**
-     * The number of items currently stored in the buffer. Calls to [advance] will cause this value
-     * to increase by one until it reaches [maxSize].
-     */
-    val size: Int
-        get() = if (omega < maxSize) omega.toInt() else maxSize
-
-    /**
-     * Advances the buffer's position by one and returns the value that is now present at the "end"
-     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
-     * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
-     *
-     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
-     * was previously stored on it.
-     */
-    fun advance(): T {
-        val index = indexOf(omega)
-        omega += 1
-        val entry = buffer[index] ?: factory().also {
-            buffer[index] = it
-        }
-        return entry
-    }
-
-    /**
-     * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
-     * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
-     */
-    operator fun get(index: Int): T {
-        if (index < 0 || index >= size) {
-            throw IndexOutOfBoundsException("Index $index is out of bounds")
-        }
-
-        // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
-        // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
-        // not yet full and our start should be 0. However, in modspace, maxSize and 0 are
-        // equivalent, so we can get away with using it as the start value instead.
-        val start = max(omega, maxSize.toLong())
-
-        return buffer[indexOf(start + index)]!!
-    }
-
-    inline fun forEach(action: (T) -> Unit) {
-        for (i in 0 until size) {
-            action(get(i))
-        }
-    }
-
-    override fun iterator(): Iterator<T> {
-        return object : Iterator<T> {
-            private var position: Int = 0
-
-            override fun next(): T {
-                if (position >= size) {
-                    throw NoSuchElementException()
-                }
-                return get(position).also { position += 1 }
-            }
-
-            override fun hasNext(): Boolean {
-                return position < size
-            }
-        }
-    }
-
-    private fun indexOf(position: Long): Int {
-        return (position % maxSize).toInt()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
index ecb365f..2c317dd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
@@ -172,10 +172,14 @@
         return Boolean.TRUE.equals(mIsConditionMet);
     }
 
-    private boolean shouldLog() {
+    protected final boolean shouldLog() {
         return Log.isLoggable(mTag, Log.DEBUG);
     }
 
+    protected final String getTag() {
+        return mTag;
+    }
+
     /**
      * Callback that receives updates about whether the condition has been fulfilled.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index 4824f67..cb430ba 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -117,6 +117,7 @@
         final SubscriptionState state = new SubscriptionState(subscription);
 
         mExecutor.execute(() -> {
+            if (shouldLog()) Log.d(mTag, "adding subscription");
             mSubscriptions.put(token, state);
 
             // Add and associate conditions.
@@ -143,7 +144,7 @@
      */
     public void removeSubscription(@NotNull Subscription.Token token) {
         mExecutor.execute(() -> {
-            if (shouldLog()) Log.d(mTag, "removing callback");
+            if (shouldLog()) Log.d(mTag, "removing subscription");
             if (!mSubscriptions.containsKey(token)) {
                 Log.e(mTag, "subscription not present:" + token);
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
new file mode 100644
index 0000000..9653985
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.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.util.kotlin
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import java.util.function.Consumer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
+ * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
+ * during onViewAttached() and removing during onViewRemoved()
+ */
+@JvmOverloads
+fun <T> collectFlow(
+    view: View,
+    flow: Flow<T>,
+    consumer: Consumer<T>,
+    state: Lifecycle.State = Lifecycle.State.CREATED,
+) {
+    view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
new file mode 100644
index 0000000..b7166d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package com.android.systemui.util;
+
+option java_multiple_files = true;
+
+message ComponentNameProto {
+  string package_name = 1;
+  string class_name = 2;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java b/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java
index d7c4e93..3c57081 100644
--- a/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/util/time/DateFormatUtil.java
@@ -16,10 +16,11 @@
 
 package com.android.systemui.util.time;
 
-import android.app.ActivityManager;
 import android.content.Context;
 import android.text.format.DateFormat;
 
+import com.android.systemui.settings.UserTracker;
+
 import javax.inject.Inject;
 
 /**
@@ -27,14 +28,16 @@
  */
 public class DateFormatUtil {
     private final Context mContext;
+    private final UserTracker mUserTracker;
 
     @Inject
-    public DateFormatUtil(Context context) {
+    public DateFormatUtil(Context context, UserTracker userTracker) {
         mContext = context;
+        mUserTracker = userTracker;
     }
 
     /** Returns true if the phone is in 24 hour format. */
     public boolean is24HourFormat() {
-        return DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
+        return DateFormat.is24HourFormat(mContext, mUserTracker.getUserId());
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java
index 8d77c4a..f320d07 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java
+++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java
@@ -38,6 +38,11 @@
     long DEFAULT_MAX_TIMEOUT = 20000;
 
     /**
+     * Default wake-lock levels and flags.
+     */
+    int DEFAULT_LEVELS_AND_FLAGS = PowerManager.PARTIAL_WAKE_LOCK;
+
+    /**
      * @param why A tag that will be saved for sysui dumps.
      * @see android.os.PowerManager.WakeLock#acquire()
      **/
@@ -60,13 +65,21 @@
      * Creates a {@link WakeLock} that has a default release timeout.
      * @see android.os.PowerManager.WakeLock#acquire(long) */
     static WakeLock createPartial(Context context, String tag, long maxTimeout) {
-        return wrap(createPartialInner(context, tag), maxTimeout);
+        return wrap(createWakeLockInner(context, tag, DEFAULT_LEVELS_AND_FLAGS), maxTimeout);
+    }
+
+    /**
+     * Creates a {@link WakeLock} that has a default release timeout and flags.
+     */
+    static WakeLock createWakeLock(Context context, String tag, int flags, long maxTimeout) {
+        return wrap(createWakeLockInner(context, tag, flags), maxTimeout);
     }
 
     @VisibleForTesting
-    static PowerManager.WakeLock createPartialInner(Context context, String tag) {
+    static PowerManager.WakeLock createWakeLockInner(
+            Context context, String tag, int levelsAndFlags) {
         return context.getSystemService(PowerManager.class)
-                    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag);
+                    .newWakeLock(levelsAndFlags, tag);
     }
 
     static Runnable wrapImpl(WakeLock w, Runnable r) {
@@ -131,6 +144,7 @@
     class Builder {
         private final Context mContext;
         private String mTag;
+        private int mLevelsAndFlags = DEFAULT_LEVELS_AND_FLAGS;
         private long mMaxTimeout = DEFAULT_MAX_TIMEOUT;
 
         @Inject
@@ -143,13 +157,18 @@
             return this;
         }
 
+        public Builder setLevelsAndFlags(int levelsAndFlags) {
+            this.mLevelsAndFlags = levelsAndFlags;
+            return this;
+        }
+
         public Builder setMaxTimeout(long maxTimeout) {
             this.mMaxTimeout = maxTimeout;
             return this;
         }
 
         public WakeLock build() {
-            return WakeLock.createPartial(mContext, mTag, mMaxTimeout);
+            return WakeLock.createWakeLock(mContext, mTag, mLevelsAndFlags, mMaxTimeout);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 903aba1..2c64fe1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -1230,6 +1230,9 @@
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
                 break;
             case RINGER_MODE_VIBRATE:
+                // Feedback handled by onStateChange, for feedback both when user toggles
+                // directly in volume dialog, or drags slider to a value of 0 in settings.
+                break;
             default:
                 effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
         }
@@ -1630,9 +1633,8 @@
                 && mState.ringerModeInternal != -1
                 && mState.ringerModeInternal != state.ringerModeInternal
                 && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) {
-            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK));
+            mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
         }
-
         mState = state;
         mDynamic.clear();
         // add any new dynamic rows
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 42d7d52..ad97ef4 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -31,6 +31,7 @@
 import android.os.HandlerThread;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.service.wallpaper.WallpaperService;
 import android.util.ArraySet;
 import android.util.Log;
@@ -47,7 +48,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor;
+import com.android.systemui.wallpapers.canvas.WallpaperLocalColorExtractor;
 import com.android.systemui.wallpapers.gl.EglHelper;
 import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
 
@@ -521,7 +522,7 @@
 
     class CanvasEngine extends WallpaperService.Engine implements DisplayListener {
         private WallpaperManager mWallpaperManager;
-        private final WallpaperColorExtractor mWallpaperColorExtractor;
+        private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor;
         private SurfaceHolder mSurfaceHolder;
         @VisibleForTesting
         static final int MIN_SURFACE_WIDTH = 128;
@@ -543,9 +544,9 @@
             super();
             setFixedSizeAllowed(true);
             setShowForAllUsers(true);
-            mWallpaperColorExtractor = new WallpaperColorExtractor(
+            mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor(
                     mBackgroundExecutor,
-                    new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+                    new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
                         @Override
                         public void onColorsProcessed(List<RectF> regions,
                                 List<WallpaperColors> colors) {
@@ -570,7 +571,7 @@
 
             // if the number of pages is already computed, transmit it to the color extractor
             if (mPagesComputed) {
-                mWallpaperColorExtractor.onPageChanged(mPages);
+                mWallpaperLocalColorExtractor.onPageChanged(mPages);
             }
         }
 
@@ -597,8 +598,7 @@
         public void onDestroy() {
             getDisplayContext().getSystemService(DisplayManager.class)
                     .unregisterDisplayListener(this);
-            mWallpaperColorExtractor.cleanUp();
-            unloadBitmap();
+            mWallpaperLocalColorExtractor.cleanUp();
         }
 
         @Override
@@ -676,9 +676,14 @@
         void drawFrameOnCanvas(Bitmap bitmap) {
             Trace.beginSection("ImageWallpaper.CanvasEngine#drawFrame");
             Surface surface = mSurfaceHolder.getSurface();
-            Canvas canvas = mWideColorGamut
-                    ? surface.lockHardwareWideColorGamutCanvas()
-                    : surface.lockHardwareCanvas();
+            Canvas canvas = null;
+            try {
+                canvas = mWideColorGamut
+                        ? surface.lockHardwareWideColorGamutCanvas()
+                        : surface.lockHardwareCanvas();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Unable to lock canvas", e);
+            }
             if (canvas != null) {
                 Rect dest = mSurfaceHolder.getSurfaceFrame();
                 try {
@@ -709,17 +714,6 @@
             }
         }
 
-        private void unloadBitmap() {
-            mBackgroundExecutor.execute(this::unloadBitmapSynchronized);
-        }
-
-        private void unloadBitmapSynchronized() {
-            synchronized (mLock) {
-                mBitmapUsages = 0;
-                unloadBitmapInternal();
-            }
-        }
-
         private void unloadBitmapInternal() {
             Trace.beginSection("ImageWallpaper.CanvasEngine#unloadBitmap");
             if (mBitmap != null) {
@@ -738,7 +732,7 @@
             boolean loadSuccess = false;
             Bitmap bitmap;
             try {
-                bitmap = mWallpaperManager.getBitmap(false);
+                bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
                 if (bitmap != null
                         && bitmap.getByteCount() > RecordingCanvas.MAX_BITMAP_SIZE) {
                     throw new RuntimeException("Wallpaper is too large to draw!");
@@ -757,7 +751,7 @@
                 }
 
                 try {
-                    bitmap = mWallpaperManager.getBitmap(false);
+                    bitmap = mWallpaperManager.getBitmapAsUser(UserHandle.USER_CURRENT, false);
                 } catch (RuntimeException | OutOfMemoryError e) {
                     Log.w(TAG, "Unable to load default wallpaper!", e);
                     bitmap = null;
@@ -770,9 +764,6 @@
                 Log.e(TAG, "Attempt to load a recycled bitmap");
             } else if (mBitmap == bitmap) {
                 Log.e(TAG, "Loaded a bitmap that was already loaded");
-            } else if (bitmap.getWidth() < 1 || bitmap.getHeight() < 1) {
-                Log.e(TAG, "Attempt to load an invalid wallpaper of length "
-                        + bitmap.getWidth() + "x" + bitmap.getHeight());
             } else {
                 // at this point, loading is done correctly.
                 loadSuccess = true;
@@ -813,7 +804,7 @@
 
         @VisibleForTesting
         void recomputeColorExtractorMiniBitmap() {
-            mWallpaperColorExtractor.onBitmapChanged(mBitmap);
+            mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap);
         }
 
         @VisibleForTesting
@@ -830,14 +821,14 @@
         public void addLocalColorsAreas(@NonNull List<RectF> regions) {
             // this call will activate the offset notifications
             // if no colors were being processed before
-            mWallpaperColorExtractor.addLocalColorsAreas(regions);
+            mWallpaperLocalColorExtractor.addLocalColorsAreas(regions);
         }
 
         @Override
         public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
             // this call will deactivate the offset notifications
             // if we are no longer processing colors
-            mWallpaperColorExtractor.removeLocalColorAreas(regions);
+            mWallpaperLocalColorExtractor.removeLocalColorAreas(regions);
         }
 
         @Override
@@ -853,7 +844,7 @@
             if (pages != mPages || !mPagesComputed) {
                 mPages = pages;
                 mPagesComputed = true;
-                mWallpaperColorExtractor.onPageChanged(mPages);
+                mWallpaperLocalColorExtractor.onPageChanged(mPages);
             }
         }
 
@@ -881,7 +872,7 @@
                     .getSystemService(WindowManager.class)
                     .getCurrentWindowMetrics()
                     .getBounds();
-            mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height());
+            mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height());
         }
 
 
@@ -902,7 +893,7 @@
                     : mBitmap.isRecycled() ? "recycled"
                     : mBitmap.getWidth() + "x" + mBitmap.getHeight());
 
-            mWallpaperColorExtractor.dump(prefix, fd, out, args);
+            mWallpaperLocalColorExtractor.dump(prefix, fd, out, args);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
deleted file mode 100644
index e2e4555..0000000
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
+++ /dev/null
@@ -1,400 +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.wallpapers.canvas;
-
-import android.app.WallpaperColors;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Trace;
-import android.util.ArraySet;
-import android.util.Log;
-import android.util.MathUtils;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.util.Assert;
-import com.android.systemui.wallpapers.ImageWallpaper;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.Executor;
-
-/**
- * This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper.
- * It uses a background executor, and uses callbacks to inform that the work is done.
- * It uses  a downscaled version of the wallpaper to extract the colors.
- */
-public class WallpaperColorExtractor {
-
-    private Bitmap mMiniBitmap;
-
-    @VisibleForTesting
-    static final int SMALL_SIDE = 128;
-
-    private static final String TAG = WallpaperColorExtractor.class.getSimpleName();
-    private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
-            new RectF(0, 0, 1, 1);
-
-    private int mDisplayWidth = -1;
-    private int mDisplayHeight = -1;
-    private int mPages = -1;
-    private int mBitmapWidth = -1;
-    private int mBitmapHeight = -1;
-
-    private final Object mLock = new Object();
-
-    private final List<RectF> mPendingRegions = new ArrayList<>();
-    private final Set<RectF> mProcessedRegions = new ArraySet<>();
-
-    @Background
-    private final Executor mBackgroundExecutor;
-
-    private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback;
-
-    /**
-     * Interface to handle the callbacks after the different steps of the color extraction
-     */
-    public interface WallpaperColorExtractorCallback {
-        /**
-         * Callback after the colors of new regions have been extracted
-         * @param regions the list of new regions that have been processed
-         * @param colors the resulting colors for these regions, in the same order as the regions
-         */
-        void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors);
-
-        /**
-         * Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is
-         * no longer used by the color extractor and can be safely recycled
-         */
-        void onMiniBitmapUpdated();
-
-        /**
-         * Callback to inform that the extractor has started processing colors
-         */
-        void onActivated();
-
-        /**
-         * Callback to inform that no more colors are being processed
-         */
-        void onDeactivated();
-    }
-
-    /**
-     * Creates a new color extractor.
-     * @param backgroundExecutor the executor on which the color extraction will be performed
-     * @param wallpaperColorExtractorCallback an interface to handle the callbacks from
-     *                                        the color extractor.
-     */
-    public WallpaperColorExtractor(@Background Executor backgroundExecutor,
-            WallpaperColorExtractorCallback wallpaperColorExtractorCallback) {
-        mBackgroundExecutor = backgroundExecutor;
-        mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback;
-    }
-
-    /**
-     * Used by the outside to inform that the display size has changed.
-     * The new display size will be used in the next computations, but the current colors are
-     * not recomputed.
-     */
-    public void setDisplayDimensions(int displayWidth, int displayHeight) {
-        mBackgroundExecutor.execute(() ->
-                setDisplayDimensionsSynchronized(displayWidth, displayHeight));
-    }
-
-    private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) {
-        synchronized (mLock) {
-            if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return;
-            mDisplayWidth = displayWidth;
-            mDisplayHeight = displayHeight;
-            processColorsInternal();
-        }
-    }
-
-    /**
-     * @return whether color extraction is currently in use
-     */
-    private boolean isActive() {
-        return mPendingRegions.size() + mProcessedRegions.size() > 0;
-    }
-
-    /**
-     * Should be called when the wallpaper is changed.
-     * This will recompute the mini bitmap
-     * and restart the extraction of all areas
-     * @param bitmap the new wallpaper
-     */
-    public void onBitmapChanged(@NonNull Bitmap bitmap) {
-        mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap));
-    }
-
-    private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) {
-        synchronized (mLock) {
-            if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
-                Log.e(TAG, "Attempt to extract colors from an invalid bitmap");
-                return;
-            }
-            mBitmapWidth = bitmap.getWidth();
-            mBitmapHeight = bitmap.getHeight();
-            mMiniBitmap = createMiniBitmap(bitmap);
-            mWallpaperColorExtractorCallback.onMiniBitmapUpdated();
-            recomputeColors();
-        }
-    }
-
-    /**
-     * Should be called when the number of pages is changed
-     * This will restart the extraction of all areas
-     * @param pages the total number of pages of the launcher
-     */
-    public void onPageChanged(int pages) {
-        mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages));
-    }
-
-    private void onPageChangedSynchronized(int pages) {
-        synchronized (mLock) {
-            if (mPages == pages) return;
-            mPages = pages;
-            if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) {
-                recomputeColors();
-            }
-        }
-    }
-
-    // helper to recompute colors, to be called in synchronized methods
-    private void recomputeColors() {
-        mPendingRegions.addAll(mProcessedRegions);
-        mProcessedRegions.clear();
-        processColorsInternal();
-    }
-
-    /**
-     * Add new regions to extract
-     * This will trigger the color extraction and call the callback only for these new regions
-     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
-     */
-    public void addLocalColorsAreas(@NonNull List<RectF> regions) {
-        if (regions.size() > 0) {
-            mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions));
-        } else {
-            Log.w(TAG, "Attempt to add colors with an empty list");
-        }
-    }
-
-    private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) {
-        synchronized (mLock) {
-            boolean wasActive = isActive();
-            mPendingRegions.addAll(regions);
-            if (!wasActive && isActive()) {
-                mWallpaperColorExtractorCallback.onActivated();
-            }
-            processColorsInternal();
-        }
-    }
-
-    /**
-     * Remove regions to extract. If a color extraction is ongoing does not stop it.
-     * But if there are subsequent changes that restart the extraction, the removed regions
-     * will not be recomputed.
-     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
-     */
-    public void removeLocalColorAreas(@NonNull List<RectF> regions) {
-        mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions));
-    }
-
-    private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) {
-        synchronized (mLock) {
-            boolean wasActive = isActive();
-            mPendingRegions.removeAll(regions);
-            regions.forEach(mProcessedRegions::remove);
-            if (wasActive && !isActive()) {
-                mWallpaperColorExtractorCallback.onDeactivated();
-            }
-        }
-    }
-
-    /**
-     * Clean up the memory (in particular, the mini bitmap) used by this class.
-     */
-    public void cleanUp() {
-        mBackgroundExecutor.execute(this::cleanUpSynchronized);
-    }
-
-    private void cleanUpSynchronized() {
-        synchronized (mLock) {
-            if (mMiniBitmap != null) {
-                mMiniBitmap.recycle();
-                mMiniBitmap = null;
-            }
-            mProcessedRegions.clear();
-            mPendingRegions.clear();
-        }
-    }
-
-    private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
-        Trace.beginSection("WallpaperColorExtractor#createMiniBitmap");
-        // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
-        int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
-        float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
-        Bitmap result = createMiniBitmap(bitmap,
-                (int) (scale * bitmap.getWidth()),
-                (int) (scale * bitmap.getHeight()));
-        Trace.endSection();
-        return result;
-    }
-
-    @VisibleForTesting
-    Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) {
-        return Bitmap.createScaledBitmap(bitmap, width, height, false);
-    }
-
-    private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) {
-        RectF imageArea = pageToImgRect(area);
-        if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) {
-            return null;
-        }
-        Rect subImage = new Rect(
-                (int) Math.floor(imageArea.left * mMiniBitmap.getWidth()),
-                (int) Math.floor(imageArea.top * mMiniBitmap.getHeight()),
-                (int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()),
-                (int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight()));
-        if (subImage.isEmpty()) {
-            // Do not notify client. treat it as too small to sample
-            return null;
-        }
-        return getLocalWallpaperColors(subImage);
-    }
-
-    @VisibleForTesting
-    WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) {
-        Assert.isNotMainThread();
-        Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap,
-                subImage.left, subImage.top, subImage.width(), subImage.height());
-        return WallpaperColors.fromBitmap(colorImg);
-    }
-
-    /**
-     * Transform the logical coordinates into wallpaper coordinates.
-     *
-     * Logical coordinates are organised such that the various pages are non-overlapping. So,
-     * if there are n pages, the first page will have its X coordinate on the range [0-1/n].
-     *
-     * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width
-     * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of
-     * pages increase.
-     * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the
-     * last page is at position (1-Wr) and the others are regularly spread on the range [0-
-     * (1-Wr)].
-     */
-    private RectF pageToImgRect(RectF area) {
-        // Width of a page for the caller of this API.
-        float virtualPageWidth = 1f / (float) mPages;
-        float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth;
-        float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth;
-        int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth);
-
-        if (mDisplayWidth <= 0 || mDisplayHeight <= 0) {
-            Log.e(TAG, "Trying to extract colors with invalid display dimensions");
-            return null;
-        }
-
-        RectF imgArea = new RectF();
-        imgArea.bottom = area.bottom;
-        imgArea.top = area.top;
-
-        float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1);
-        float mappedScreenWidth = mDisplayWidth * imageScale;
-        float pageWidth = Math.min(1.0f,
-                mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f);
-        float pageOffset = (1 - pageWidth) / (float) (mPages - 1);
-
-        imgArea.left = MathUtils.constrain(
-                leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
-        imgArea.right = MathUtils.constrain(
-                rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
-        if (imgArea.left > imgArea.right) {
-            // take full page
-            imgArea.left = 0;
-            imgArea.right = 1;
-        }
-        return imgArea;
-    }
-
-    /**
-     * Extract the colors from the pending regions,
-     * then notify the callback with the resulting colors for these regions
-     * This method should only be called synchronously
-     */
-    private void processColorsInternal() {
-        /*
-         * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been
-         * called, and thus the wallpaper is not yet loaded. In that case, exit, the function
-         * will be called again when the bitmap is loaded and the miniBitmap is computed.
-         */
-        if (mMiniBitmap == null || mMiniBitmap.isRecycled())  return;
-
-        /*
-         * if the screen size or number of pages is not yet known, exit
-         * the function will be called again once the screen size and page are known
-         */
-        if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
-
-        Trace.beginSection("WallpaperColorExtractor#processColorsInternal");
-        List<WallpaperColors> processedColors = new ArrayList<>();
-        for (int i = 0; i < mPendingRegions.size(); i++) {
-            RectF nextArea = mPendingRegions.get(i);
-            WallpaperColors colors = getLocalWallpaperColors(nextArea);
-
-            mProcessedRegions.add(nextArea);
-            processedColors.add(colors);
-        }
-        List<RectF> processedRegions = new ArrayList<>(mPendingRegions);
-        mPendingRegions.clear();
-        Trace.endSection();
-
-        mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
-    }
-
-    /**
-     * Called to dump current state.
-     * @param prefix prefix.
-     * @param fd fd.
-     * @param out out.
-     * @param args args.
-     */
-    public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
-        out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight);
-        out.print(prefix); out.print("mPages="); out.println(mPages);
-
-        out.print(prefix); out.print("bitmap dimensions=");
-        out.println(mBitmapWidth + "x" + mBitmapHeight);
-
-        out.print(prefix); out.print("bitmap=");
-        out.println(mMiniBitmap == null ? "null"
-                : mMiniBitmap.isRecycled() ? "recycled"
-                : mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight());
-
-        out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size());
-        out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size());
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
new file mode 100644
index 0000000..6cac5c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
@@ -0,0 +1,400 @@
+/*
+ * 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.wallpapers.canvas;
+
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Trace;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.MathUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.util.Assert;
+import com.android.systemui.wallpapers.ImageWallpaper;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * This class is used by the {@link ImageWallpaper} to extract colors from areas of a wallpaper.
+ * It uses a background executor, and uses callbacks to inform that the work is done.
+ * It uses  a downscaled version of the wallpaper to extract the colors.
+ */
+public class WallpaperLocalColorExtractor {
+
+    private Bitmap mMiniBitmap;
+
+    @VisibleForTesting
+    static final int SMALL_SIDE = 128;
+
+    private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName();
+    private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
+            new RectF(0, 0, 1, 1);
+
+    private int mDisplayWidth = -1;
+    private int mDisplayHeight = -1;
+    private int mPages = -1;
+    private int mBitmapWidth = -1;
+    private int mBitmapHeight = -1;
+
+    private final Object mLock = new Object();
+
+    private final List<RectF> mPendingRegions = new ArrayList<>();
+    private final Set<RectF> mProcessedRegions = new ArraySet<>();
+
+    @Background
+    private final Executor mBackgroundExecutor;
+
+    private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback;
+
+    /**
+     * Interface to handle the callbacks after the different steps of the color extraction
+     */
+    public interface WallpaperLocalColorExtractorCallback {
+        /**
+         * Callback after the colors of new regions have been extracted
+         * @param regions the list of new regions that have been processed
+         * @param colors the resulting colors for these regions, in the same order as the regions
+         */
+        void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors);
+
+        /**
+         * Callback after the mini bitmap is computed, to indicate that the wallpaper bitmap is
+         * no longer used by the color extractor and can be safely recycled
+         */
+        void onMiniBitmapUpdated();
+
+        /**
+         * Callback to inform that the extractor has started processing colors
+         */
+        void onActivated();
+
+        /**
+         * Callback to inform that no more colors are being processed
+         */
+        void onDeactivated();
+    }
+
+    /**
+     * Creates a new color extractor.
+     * @param backgroundExecutor the executor on which the color extraction will be performed
+     * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from
+     *                                        the color extractor.
+     */
+    public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor,
+            WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) {
+        mBackgroundExecutor = backgroundExecutor;
+        mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback;
+    }
+
+    /**
+     * Used by the outside to inform that the display size has changed.
+     * The new display size will be used in the next computations, but the current colors are
+     * not recomputed.
+     */
+    public void setDisplayDimensions(int displayWidth, int displayHeight) {
+        mBackgroundExecutor.execute(() ->
+                setDisplayDimensionsSynchronized(displayWidth, displayHeight));
+    }
+
+    private void setDisplayDimensionsSynchronized(int displayWidth, int displayHeight) {
+        synchronized (mLock) {
+            if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return;
+            mDisplayWidth = displayWidth;
+            mDisplayHeight = displayHeight;
+            processColorsInternal();
+        }
+    }
+
+    /**
+     * @return whether color extraction is currently in use
+     */
+    private boolean isActive() {
+        return mPendingRegions.size() + mProcessedRegions.size() > 0;
+    }
+
+    /**
+     * Should be called when the wallpaper is changed.
+     * This will recompute the mini bitmap
+     * and restart the extraction of all areas
+     * @param bitmap the new wallpaper
+     */
+    public void onBitmapChanged(@NonNull Bitmap bitmap) {
+        mBackgroundExecutor.execute(() -> onBitmapChangedSynchronized(bitmap));
+    }
+
+    private void onBitmapChangedSynchronized(@NonNull Bitmap bitmap) {
+        synchronized (mLock) {
+            if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+                Log.e(TAG, "Attempt to extract colors from an invalid bitmap");
+                return;
+            }
+            mBitmapWidth = bitmap.getWidth();
+            mBitmapHeight = bitmap.getHeight();
+            mMiniBitmap = createMiniBitmap(bitmap);
+            mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated();
+            recomputeColors();
+        }
+    }
+
+    /**
+     * Should be called when the number of pages is changed
+     * This will restart the extraction of all areas
+     * @param pages the total number of pages of the launcher
+     */
+    public void onPageChanged(int pages) {
+        mBackgroundExecutor.execute(() -> onPageChangedSynchronized(pages));
+    }
+
+    private void onPageChangedSynchronized(int pages) {
+        synchronized (mLock) {
+            if (mPages == pages) return;
+            mPages = pages;
+            if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) {
+                recomputeColors();
+            }
+        }
+    }
+
+    // helper to recompute colors, to be called in synchronized methods
+    private void recomputeColors() {
+        mPendingRegions.addAll(mProcessedRegions);
+        mProcessedRegions.clear();
+        processColorsInternal();
+    }
+
+    /**
+     * Add new regions to extract
+     * This will trigger the color extraction and call the callback only for these new regions
+     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
+     */
+    public void addLocalColorsAreas(@NonNull List<RectF> regions) {
+        if (regions.size() > 0) {
+            mBackgroundExecutor.execute(() -> addLocalColorsAreasSynchronized(regions));
+        } else {
+            Log.w(TAG, "Attempt to add colors with an empty list");
+        }
+    }
+
+    private void addLocalColorsAreasSynchronized(@NonNull List<RectF> regions) {
+        synchronized (mLock) {
+            boolean wasActive = isActive();
+            mPendingRegions.addAll(regions);
+            if (!wasActive && isActive()) {
+                mWallpaperLocalColorExtractorCallback.onActivated();
+            }
+            processColorsInternal();
+        }
+    }
+
+    /**
+     * Remove regions to extract. If a color extraction is ongoing does not stop it.
+     * But if there are subsequent changes that restart the extraction, the removed regions
+     * will not be recomputed.
+     * @param regions The areas of interest in our wallpaper (in screen pixel coordinates)
+     */
+    public void removeLocalColorAreas(@NonNull List<RectF> regions) {
+        mBackgroundExecutor.execute(() -> removeLocalColorAreasSynchronized(regions));
+    }
+
+    private void removeLocalColorAreasSynchronized(@NonNull List<RectF> regions) {
+        synchronized (mLock) {
+            boolean wasActive = isActive();
+            mPendingRegions.removeAll(regions);
+            regions.forEach(mProcessedRegions::remove);
+            if (wasActive && !isActive()) {
+                mWallpaperLocalColorExtractorCallback.onDeactivated();
+            }
+        }
+    }
+
+    /**
+     * Clean up the memory (in particular, the mini bitmap) used by this class.
+     */
+    public void cleanUp() {
+        mBackgroundExecutor.execute(this::cleanUpSynchronized);
+    }
+
+    private void cleanUpSynchronized() {
+        synchronized (mLock) {
+            if (mMiniBitmap != null) {
+                mMiniBitmap.recycle();
+                mMiniBitmap = null;
+            }
+            mProcessedRegions.clear();
+            mPendingRegions.clear();
+        }
+    }
+
+    private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
+        Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap");
+        // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
+        int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
+        float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
+        Bitmap result = createMiniBitmap(bitmap,
+                (int) (scale * bitmap.getWidth()),
+                (int) (scale * bitmap.getHeight()));
+        Trace.endSection();
+        return result;
+    }
+
+    @VisibleForTesting
+    Bitmap createMiniBitmap(@NonNull Bitmap bitmap, int width, int height) {
+        return Bitmap.createScaledBitmap(bitmap, width, height, false);
+    }
+
+    private WallpaperColors getLocalWallpaperColors(@NonNull RectF area) {
+        RectF imageArea = pageToImgRect(area);
+        if (imageArea == null || !LOCAL_COLOR_BOUNDS.contains(imageArea)) {
+            return null;
+        }
+        Rect subImage = new Rect(
+                (int) Math.floor(imageArea.left * mMiniBitmap.getWidth()),
+                (int) Math.floor(imageArea.top * mMiniBitmap.getHeight()),
+                (int) Math.ceil(imageArea.right * mMiniBitmap.getWidth()),
+                (int) Math.ceil(imageArea.bottom * mMiniBitmap.getHeight()));
+        if (subImage.isEmpty()) {
+            // Do not notify client. treat it as too small to sample
+            return null;
+        }
+        return getLocalWallpaperColors(subImage);
+    }
+
+    @VisibleForTesting
+    WallpaperColors getLocalWallpaperColors(@NonNull Rect subImage) {
+        Assert.isNotMainThread();
+        Bitmap colorImg = Bitmap.createBitmap(mMiniBitmap,
+                subImage.left, subImage.top, subImage.width(), subImage.height());
+        return WallpaperColors.fromBitmap(colorImg);
+    }
+
+    /**
+     * Transform the logical coordinates into wallpaper coordinates.
+     *
+     * Logical coordinates are organised such that the various pages are non-overlapping. So,
+     * if there are n pages, the first page will have its X coordinate on the range [0-1/n].
+     *
+     * The real pages are overlapping. If the Wallpaper are a width Ww and the screen a width
+     * Ws, the relative width of a page Wr is Ws/Ww. This does not change if the number of
+     * pages increase.
+     * If there are n pages, the page k starts at the offset k * (1 - Wr) / (n - 1), as the
+     * last page is at position (1-Wr) and the others are regularly spread on the range [0-
+     * (1-Wr)].
+     */
+    private RectF pageToImgRect(RectF area) {
+        // Width of a page for the caller of this API.
+        float virtualPageWidth = 1f / (float) mPages;
+        float leftPosOnPage = (area.left % virtualPageWidth) / virtualPageWidth;
+        float rightPosOnPage = (area.right % virtualPageWidth) / virtualPageWidth;
+        int currentPage = (int) Math.floor(area.centerX() / virtualPageWidth);
+
+        if (mDisplayWidth <= 0 || mDisplayHeight <= 0) {
+            Log.e(TAG, "Trying to extract colors with invalid display dimensions");
+            return null;
+        }
+
+        RectF imgArea = new RectF();
+        imgArea.bottom = area.bottom;
+        imgArea.top = area.top;
+
+        float imageScale = Math.min(((float) mBitmapHeight) / mDisplayHeight, 1);
+        float mappedScreenWidth = mDisplayWidth * imageScale;
+        float pageWidth = Math.min(1.0f,
+                mBitmapWidth > 0 ? mappedScreenWidth / (float) mBitmapWidth : 1.f);
+        float pageOffset = (1 - pageWidth) / (float) (mPages - 1);
+
+        imgArea.left = MathUtils.constrain(
+                leftPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
+        imgArea.right = MathUtils.constrain(
+                rightPosOnPage * pageWidth + currentPage * pageOffset, 0, 1);
+        if (imgArea.left > imgArea.right) {
+            // take full page
+            imgArea.left = 0;
+            imgArea.right = 1;
+        }
+        return imgArea;
+    }
+
+    /**
+     * Extract the colors from the pending regions,
+     * then notify the callback with the resulting colors for these regions
+     * This method should only be called synchronously
+     */
+    private void processColorsInternal() {
+        /*
+         * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been
+         * called, and thus the wallpaper is not yet loaded. In that case, exit, the function
+         * will be called again when the bitmap is loaded and the miniBitmap is computed.
+         */
+        if (mMiniBitmap == null || mMiniBitmap.isRecycled())  return;
+
+        /*
+         * if the screen size or number of pages is not yet known, exit
+         * the function will be called again once the screen size and page are known
+         */
+        if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
+
+        Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal");
+        List<WallpaperColors> processedColors = new ArrayList<>();
+        for (int i = 0; i < mPendingRegions.size(); i++) {
+            RectF nextArea = mPendingRegions.get(i);
+            WallpaperColors colors = getLocalWallpaperColors(nextArea);
+
+            mProcessedRegions.add(nextArea);
+            processedColors.add(colors);
+        }
+        List<RectF> processedRegions = new ArrayList<>(mPendingRegions);
+        mPendingRegions.clear();
+        Trace.endSection();
+
+        mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
+    }
+
+    /**
+     * Called to dump current state.
+     * @param prefix prefix.
+     * @param fd fd.
+     * @param out out.
+     * @param args args.
+     */
+    public void dump(String prefix, FileDescriptor fd, PrintWriter out, String[] args) {
+        out.print(prefix); out.print("display="); out.println(mDisplayWidth + "x" + mDisplayHeight);
+        out.print(prefix); out.print("mPages="); out.println(mPages);
+
+        out.print(prefix); out.print("bitmap dimensions=");
+        out.println(mBitmapWidth + "x" + mBitmapHeight);
+
+        out.print(prefix); out.print("bitmap=");
+        out.println(mMiniBitmap == null ? "null"
+                : mMiniBitmap.isRecycled() ? "recycled"
+                : mMiniBitmap.getWidth() + "x" + mMiniBitmap.getHeight());
+
+        out.print(prefix); out.print("PendingRegions size="); out.print(mPendingRegions.size());
+        out.print(prefix); out.print("ProcessedRegions size="); out.print(mProcessedRegions.size());
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 4e77514..a4384d5 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -25,6 +25,7 @@
 import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
+import static com.android.systemui.flags.Flags.WM_BUBBLE_BAR;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
 
@@ -51,6 +52,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -129,6 +131,7 @@
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
+            FeatureFlags featureFlags,
             Executor sysuiMainExecutor) {
         if (bubblesOptional.isPresent()) {
             return new BubblesManager(context,
@@ -146,6 +149,7 @@
                     notifCollection,
                     notifPipeline,
                     sysUiState,
+                    featureFlags,
                     sysuiMainExecutor);
         } else {
             return null;
@@ -168,6 +172,7 @@
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
+            FeatureFlags featureFlags,
             Executor sysuiMainExecutor) {
         mContext = context;
         mBubbles = bubbles;
@@ -352,6 +357,7 @@
                 });
             }
         };
+        mBubbles.setBubbleBarEnabled(featureFlags.isEnabled(WM_BUBBLE_BAR));
         mBubbles.setSysuiProxy(mSysuiProxy);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index fbc6a58..02738d5 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -48,6 +49,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.statusbar.CommandQueue;
@@ -55,7 +57,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
-import com.android.wm.shell.floating.FloatingTasks;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.nano.WmShellTraceProto;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -110,7 +113,7 @@
     private final Optional<Pip> mPipOptional;
     private final Optional<SplitScreen> mSplitScreenOptional;
     private final Optional<OneHanded> mOneHandedOptional;
-    private final Optional<FloatingTasks> mFloatingTasksOptional;
+    private final Optional<DesktopMode> mDesktopModeOptional;
 
     private final CommandQueue mCommandQueue;
     private final ConfigurationController mConfigurationController;
@@ -121,6 +124,7 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final ProtoTracer mProtoTracer;
     private final UserTracker mUserTracker;
+    private final NoteTaskInitializer mNoteTaskInitializer;
     private final Executor mSysUiMainExecutor;
 
     // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
@@ -172,7 +176,7 @@
             Optional<Pip> pipOptional,
             Optional<SplitScreen> splitScreenOptional,
             Optional<OneHanded> oneHandedOptional,
-            Optional<FloatingTasks> floatingTasksOptional,
+            Optional<DesktopMode> desktopMode,
             CommandQueue commandQueue,
             ConfigurationController configurationController,
             KeyguardStateController keyguardStateController,
@@ -182,6 +186,7 @@
             ProtoTracer protoTracer,
             WakefulnessLifecycle wakefulnessLifecycle,
             UserTracker userTracker,
+            NoteTaskInitializer noteTaskInitializer,
             @Main Executor sysUiMainExecutor) {
         mContext = context;
         mShell = shell;
@@ -194,10 +199,11 @@
         mPipOptional = pipOptional;
         mSplitScreenOptional = splitScreenOptional;
         mOneHandedOptional = oneHandedOptional;
+        mDesktopModeOptional = desktopMode;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mUserTracker = userTracker;
-        mFloatingTasksOptional = floatingTasksOptional;
+        mNoteTaskInitializer = noteTaskInitializer;
         mSysUiMainExecutor = sysUiMainExecutor;
     }
 
@@ -219,6 +225,9 @@
         mPipOptional.ifPresent(this::initPip);
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
+        mDesktopModeOptional.ifPresent(this::initDesktopMode);
+
+        mNoteTaskInitializer.initialize();
     }
 
     @VisibleForTesting
@@ -326,6 +335,16 @@
         });
     }
 
+    void initDesktopMode(DesktopMode desktopMode) {
+        desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() {
+            @Override
+            public void onVisibilityChanged(boolean hasFreeformTasks) {
+                mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
+                        .commitUpdate(DEFAULT_DISPLAY);
+            }
+        }, mSysUiMainExecutor);
+    }
+
     @Override
     public void writeToProto(SystemUiTraceProto proto) {
         if (proto.wmShell == null) {
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 0fa3d36..4891339 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -88,6 +88,26 @@
                   android:excludeFromRecents="true"
                   />
 
+        <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
+        <activity android:name="com.android.systemui.controls.management.ControlsEditingActivityTest$TestableControlsEditingActivity"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
+        <activity android:name="com.android.systemui.controls.management.ControlsFavoritingActivityTest$TestableControlsFavoritingActivity"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
+        <activity android:name="com.android.systemui.controls.management.ControlsProviderSelectorActivityTest$TestableControlsProviderSelectorActivity"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
         <activity android:name="com.android.systemui.screenshot.ScrollViewActivity"
                   android:exported="false" />
 
@@ -101,6 +121,12 @@
                   android:finishOnCloseSystemDialogs="true"
                   android:excludeFromRecents="true" />
 
+        <activity android:name=".user.CreateUserActivityTest$CreateUserActivityTestable"
+            android:exported="false"
+            android:theme="@style/Theme.SystemUI.Dialog.Alert"
+            android:finishOnCloseSystemDialogs="true"
+            android:excludeFromRecents="true" />
+
         <provider
             android:name="androidx.startup.InitializationProvider"
             tools:replace="android:authorities"
@@ -114,6 +140,12 @@
             tools:replace="android:authorities"
             tools:node="remove" />
 
+        <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
+            android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="com.android.systemui.test.fileprovider"
diff --git a/packages/SystemUI/tests/res/layout/custom_view_dark.xml b/packages/SystemUI/tests/res/layout/custom_view_dark.xml
index 9e460a5..112d73d2 100644
--- a/packages/SystemUI/tests/res/layout/custom_view_dark.xml
+++ b/packages/SystemUI/tests/res/layout/custom_view_dark.xml
@@ -14,6 +14,7 @@
     limitations under the License.
 -->
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/custom_view_dark_image"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="#ff000000"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
new file mode 100644
index 0000000..7b9b39f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.AttributeSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BouncerKeyguardMessageAreaTest : SysuiTestCase() {
+    class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) :
+        BouncerKeyguardMessageArea(context, attrs) {
+        override val SHOW_DURATION_MILLIS = 0L
+        override val HIDE_DURATION_MILLIS = 0L
+    }
+    lateinit var underTest: BouncerKeyguardMessageArea
+
+    @Before
+    fun setup() {
+        underTest = FakeBouncerKeyguardMessageArea(context, null)
+    }
+
+    @Test
+    fun testSetSameMessage() {
+        val underTestSpy = spy(underTest)
+        underTestSpy.setMessage("abc")
+        underTestSpy.setMessage("abc")
+        verify(underTestSpy, times(1)).text = "abc"
+    }
+
+    @Test
+    fun testSetDifferentMessage() {
+        underTest.setMessage("abc")
+        underTest.setMessage("def")
+        assertThat(underTest.text).isEqualTo("def")
+    }
+
+    @Test
+    fun testSetNullMessage() {
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
+
+    @Test
+    fun testSetNullClearsPreviousMessage() {
+        underTest.setMessage("something not null")
+        assertThat(underTest.text).isEqualTo("something not null")
+
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 8a2c354..e8f8e25 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -17,17 +17,22 @@
 
 import android.content.BroadcastReceiver
 import android.testing.AndroidTestingRunner
+import android.view.View
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.ClockAnimations
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.mockito.any
@@ -37,6 +42,9 @@
 import com.android.systemui.util.mockito.mock
 import java.util.TimeZone
 import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -57,7 +65,6 @@
 class ClockEventControllerTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var batteryController: BatteryController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -72,8 +79,11 @@
     @Mock private lateinit var largeClockController: ClockFaceController
     @Mock private lateinit var smallClockEvents: ClockFaceEvents
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
-
-    private lateinit var clockEventController: ClockEventController
+    @Mock private lateinit var parentView: View
+    @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+    private lateinit var repository: FakeKeyguardRepository
+    @Mock private lateinit var logBuffer: LogBuffer
+    private lateinit var underTest: ClockEventController
 
     @Before
     fun setUp() {
@@ -86,8 +96,11 @@
         whenever(clock.events).thenReturn(events)
         whenever(clock.animations).thenReturn(animations)
 
-        clockEventController = ClockEventController(
-            statusBarStateController,
+        repository = FakeKeyguardRepository()
+
+        underTest = ClockEventController(
+            KeyguardInteractor(repository = repository),
+            KeyguardTransitionInteractor(repository = transitionRepository),
             broadcastDispatcher,
             batteryController,
             keyguardUpdateMonitor,
@@ -96,33 +109,36 @@
             context,
             mainExecutor,
             bgExecutor,
+            logBuffer,
             featureFlags
         )
+        underTest.clock = clock
+
+        runBlocking(IMMEDIATE) {
+            underTest.registerListeners(parentView)
+
+            repository.setDozing(true)
+            repository.setDozeAmount(1f)
+        }
     }
 
     @Test
     fun clockSet_validateInitialization() {
-        clockEventController.clock = clock
-
         verify(clock).initialize(any(), anyFloat(), anyFloat())
     }
 
     @Test
     fun clockUnset_validateState() {
-        clockEventController.clock = clock
-        clockEventController.clock = null
+        underTest.clock = null
 
-        assertEquals(clockEventController.clock, null)
+        assertEquals(underTest.clock, null)
     }
 
     @Test
-    fun themeChanged_verifyClockPaletteUpdated() {
-        clockEventController.clock = clock
+    fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onThemeChanged()
@@ -131,25 +147,20 @@
     }
 
     @Test
-    fun fontChanged_verifyFontSizeUpdated() {
-        clockEventController.clock = clock
+    fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onDensityOrFontScaleChanged()
 
-        verify(events).onFontSettingChanged()
+        verify(smallClockEvents, times(2)).onFontSettingChanged(anyFloat())
+        verify(largeClockEvents, times(2)).onFontSettingChanged(anyFloat())
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -161,26 +172,21 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-
-        verify(animations, times(1)).charge()
-    }
+            verify(animations, times(1)).charge()
+        }
 
     @Test
-    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -192,25 +198,20 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, false)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, false)
-
-        verify(animations, never()).charge()
-    }
+            verify(animations, never()).charge()
+        }
 
     @Test
-    fun localeCallback_verifyClockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<BroadcastReceiver>()
         verify(broadcastDispatcher).registerReceiver(
             capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
@@ -221,10 +222,7 @@
     }
 
     @Test
-    fun keyguardCallback_visibilityChanged_clockDozeCalled() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
 
@@ -236,10 +234,7 @@
     }
 
     @Test
-    fun keyguardCallback_timeFormat_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeFormatChanged("12h")
@@ -248,11 +243,8 @@
     }
 
     @Test
-    fun keyguardCallback_timezoneChanged_clockNotified() {
+    fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) {
         val mockTimeZone = mock<TimeZone>()
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeZoneChanged(mockTimeZone)
@@ -261,10 +253,7 @@
     }
 
     @Test
-    fun keyguardCallback_userSwitched_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onUserSwitchComplete(10)
@@ -273,25 +262,27 @@
     }
 
     @Test
-    fun keyguardCallback_verifyKeyguardChanged() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) {
+        val job = underTest.listenForDozeAmount(this)
+        repository.setDozeAmount(0.4f)
 
-        val captor = argumentCaptor<StatusBarStateController.StateListener>()
-        verify(statusBarStateController).addCallback(capture(captor))
-        captor.value.onDozeAmountChanged(0.4f, 0.6f)
+        yield()
 
         verify(animations).doze(0.4f)
+
+        job.cancel()
     }
 
     @Test
-    fun unregisterListeners_validate() {
-        clockEventController.clock = clock
-        clockEventController.unregisterListeners()
+    fun unregisterListeners_validate() = runBlocking(IMMEDIATE) {
+        underTest.unregisterListeners()
         verify(broadcastDispatcher).unregisterReceiver(any())
         verify(configurationController).removeCallback(any())
         verify(batteryController).removeCallback(any())
         verify(keyguardUpdateMonitor).removeCallback(any())
-        verify(statusBarStateController).removeCallback(any())
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
new file mode 100644
index 0000000..6c5620d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.GlobalSettings
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FaceWakeUpTriggersConfigTest : SysuiTestCase() {
+    @Mock lateinit var globalSettings: GlobalSettings
+    @Mock lateinit var dumpManager: DumpManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testShouldTriggerFaceAuthOnWakeUpFrom_inConfig_returnsTrue() {
+        val faceWakeUpTriggersConfig =
+            createFaceWakeUpTriggersConfig(
+                intArrayOf(PowerManager.WAKE_REASON_POWER_BUTTON, PowerManager.WAKE_REASON_GESTURE)
+            )
+
+        assertTrue(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_POWER_BUTTON
+            )
+        )
+        assertTrue(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_GESTURE
+            )
+        )
+        assertFalse(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_APPLICATION
+            )
+        )
+    }
+
+    private fun createFaceWakeUpTriggersConfig(wakeUpTriggers: IntArray): FaceWakeUpTriggersConfig {
+        overrideResource(
+            com.android.systemui.R.array.config_face_auth_wake_up_triggers,
+            wakeUpTriggers
+        )
+
+        return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 400caa3..61c7bb5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,6 +29,7 @@
 
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -43,8 +44,8 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -103,8 +104,6 @@
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
-    @Mock
-    private FeatureFlags mFeatureFlags;
 
     private final View mFakeSmartspaceView = new View(mContext);
 
@@ -141,8 +140,7 @@
                 mSecureSettings,
                 mExecutor,
                 mDumpManager,
-                mClockEventController,
-                mFeatureFlags
+                mClockEventController
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
@@ -262,9 +260,22 @@
         verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true);
     }
 
+    @Test
+    public void testGetClockAnimationsForwardsToClock() {
+        ClockController mockClockController = mock(ClockController.class);
+        ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
+        when(mClockEventController.getClock()).thenReturn(mockClockController);
+        when(mockClockController.getAnimations()).thenReturn(mockClockAnimations);
+
+        Rect r1 = new Rect(1, 2, 3, 4);
+        Rect r2 = new Rect(5, 6, 7, 8);
+        mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f);
+        verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f);
+    }
+
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
-        verify(mClockEventController, times).registerListeners();
+        verify(mClockEventController, times).registerListeners(mView);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index aca60c0..8839662 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -72,6 +72,7 @@
     keyguardOccluded = false,
     occludingAppRequestingFp = false,
     primaryUser = false,
+    shouldListenSfpsState = false,
     shouldListenForFingerprintAssistant = false,
     switchingUser = false,
     udfps = false,
@@ -83,10 +84,9 @@
     userId = user,
     listening = false,
     authInterruptActive = false,
-    becauseCannotSkipBouncer = false,
     biometricSettingEnabledForUser = false,
     bouncerFullyShown = false,
-    faceAuthenticated = false,
+    faceAndFpNotAuthenticated = false,
     faceDisabled = false,
     faceLockedOut = false,
     fpLockedOut = false,
@@ -100,4 +100,6 @@
     secureCameraLaunched = false,
     switchingUser = false,
     udfpsBouncerShowing = false,
+    udfpsFingerDown = false,
+    userNotTrustedOrDetectionIsNeeded = false
 )
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 69524e5..8290084 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -16,14 +16,15 @@
 
 package com.android.keyguard;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
-import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -94,17 +95,9 @@
     }
 
     @Test
-    public void testSetMessageIfEmpty_empty() {
-        mMessageAreaController.setMessage("");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin);
-    }
-
-    @Test
-    public void testSetMessageIfEmpty_notEmpty() {
-        mMessageAreaController.setMessage("abc");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea, never()).setMessage(getContext()
-                .getResources().getText(R.string.keyguard_enter_your_pin));
+    public void testGetMessage() {
+        String msg = "abc";
+        when(mKeyguardMessageArea.getText()).thenReturn(msg);
+        assertThat(mMessageAreaController.getMessage()).isEqualTo(msg);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index b89dbd9..ffd95f4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -31,6 +31,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
@@ -114,9 +115,18 @@
     }
 
     @Test
-    fun onResume_testSetInitialText() {
-        keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_password)
+    fun startAppearAnimation() {
+        keyguardPasswordViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
+    }
+
+    @Test
+    fun startAppearAnimation_withExistingMessage() {
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        keyguardPasswordViewController.startAppearAnimation()
+        verify(
+            mKeyguardMessageAreaController,
+            never()
+        ).setMessage(R.string.keyguard_enter_your_password)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 3262a77..b3d1c8f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -33,6 +33,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.never
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -100,16 +101,26 @@
     }
 
     @Test
-    fun onPause_clearsTextField() {
+    fun onPause_resetsText() {
         mKeyguardPatternViewController.init()
         mKeyguardPatternViewController.onPause()
-        verify(mKeyguardMessageAreaController).setMessage("")
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
+    }
+
+
+    @Test
+    fun startAppearAnimation() {
+        mKeyguardPatternViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
 
     @Test
-    fun onResume_setInitialText() {
-        mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_pattern)
+    fun startAppearAnimation_withExistingMessage() {
+        `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        mKeyguardPatternViewController.startAppearAnimation()
+        verify(
+            mKeyguardMessageAreaController,
+            never()
+        ).setMessage(R.string.keyguard_enter_your_password)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index 97d556b..ce1101f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -113,11 +113,4 @@
         mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
         verify(mPasswordEntry).requestFocus();
     }
-
-    @Test
-    public void onResume_setInitialText() {
-        mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
-        verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-    }
 }
-
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 9e5bfe5..8bcfe6f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -98,6 +98,14 @@
     @Test
     fun startAppearAnimation() {
         pinViewController.startAppearAnimation()
-        verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin)
+        verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
+    }
+
+    @Test
+    fun startAppearAnimation_withExistingMessage() {
+        Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+        pinViewController.startAppearAnimation()
+        verify(keyguardMessageAreaController, Mockito.never())
+            .setMessage(R.string.keyguard_enter_your_password)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 48e8239..4d58b09 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -54,7 +54,9 @@
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.SidefpsController;
+import com.android.systemui.biometrics.SideFpsController;
+import com.android.systemui.biometrics.SideFpsUiRequestSource;
+import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.log.SessionTracker;
@@ -140,12 +142,16 @@
     @Mock
     private KeyguardViewController mKeyguardViewController;
     @Mock
-    private SidefpsController mSidefpsController;
+    private SideFpsController mSideFpsController;
     @Mock
     private KeyguardPasswordViewController mKeyguardPasswordViewControllerMock;
+    @Mock
+    private FalsingA11yDelegate mFalsingA11yDelegate;
 
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback;
+    @Captor
+    private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor;
 
     private Configuration mConfiguration;
 
@@ -184,7 +190,17 @@
                 mKeyguardStateController, mKeyguardSecurityViewFlipperController,
                 mConfigurationController, mFalsingCollector, mFalsingManager,
                 mUserSwitcherController, mFeatureFlags, mGlobalSettings,
-                mSessionTracker, Optional.of(mSidefpsController)).create(mSecurityCallback);
+                mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate).create(
+                mSecurityCallback);
+    }
+
+    @Test
+    public void onInitConfiguresViewMode() {
+        mKeyguardSecurityContainerController.onInit();
+        verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+                eq(mUserSwitcherController),
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
     }
 
     @Test
@@ -223,7 +239,8 @@
         mKeyguardSecurityContainerController.updateResources();
         verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
                 eq(mUserSwitcherController),
-                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
 
         // Update rotation. Should trigger update
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
@@ -231,7 +248,8 @@
         mKeyguardSecurityContainerController.updateResources();
         verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
                 eq(mUserSwitcherController),
-                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
     }
 
     private void touchDown() {
@@ -267,7 +285,8 @@
         mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
         verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
                 eq(mUserSwitcherController),
-                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
     }
 
     @Test
@@ -280,7 +299,8 @@
         mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern);
         verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager),
                 eq(mUserSwitcherController),
-                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
     }
 
     @Test
@@ -291,7 +311,8 @@
         mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
         verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
                 eq(mUserSwitcherController),
-                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class));
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
     }
 
     @Test
@@ -305,7 +326,8 @@
         mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Password);
         verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class),
                 any(UserSwitcherController.class),
-                captor.capture());
+                captor.capture(),
+                eq(mFalsingA11yDelegate));
         captor.getValue().showUnlockToContinueMessage();
         verify(mKeyguardPasswordViewControllerMock).showMessage(
                 getContext().getString(R.string.keyguard_unlock_to_continue), null);
@@ -324,48 +346,48 @@
     @Test
     public void onBouncerVisibilityChanged_allConditionsGood_sideFpsHintShown() {
         setupConditionsToEnableSideFpsHint();
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
 
-        verify(mSidefpsController).show();
-        verify(mSidefpsController, never()).hide();
+        verify(mSideFpsController).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).hide(any());
     }
 
     @Test
     public void onBouncerVisibilityChanged_fpsSensorNotRunning_sideFpsHintHidden() {
         setupConditionsToEnableSideFpsHint();
         setFingerprintDetectionRunning(false);
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
     public void onBouncerVisibilityChanged_withoutSidedSecurity_sideFpsHintHidden() {
         setupConditionsToEnableSideFpsHint();
         setSideFpsHintEnabledFromResources(false);
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
     public void onBouncerVisibilityChanged_needsStrongAuth_sideFpsHintHidden() {
         setupConditionsToEnableSideFpsHint();
         setNeedsStrongAuth(true);
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
@@ -373,13 +395,13 @@
         setupGetSecurityView();
         setupConditionsToEnableSideFpsHint();
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSidefpsController, atLeastOnce()).show();
-        reset(mSidefpsController);
+        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.INVISIBLE);
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
@@ -387,13 +409,13 @@
         setupGetSecurityView();
         setupConditionsToEnableSideFpsHint();
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSidefpsController, atLeastOnce()).show();
-        reset(mSidefpsController);
+        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onStartingToHide();
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
@@ -401,13 +423,13 @@
         setupGetSecurityView();
         setupConditionsToEnableSideFpsHint();
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        verify(mSidefpsController, atLeastOnce()).show();
-        reset(mSidefpsController);
+        verify(mSideFpsController, atLeastOnce()).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onPause();
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
@@ -415,12 +437,12 @@
         setupGetSecurityView();
         setupConditionsToEnableSideFpsHint();
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onResume(0);
 
-        verify(mSidefpsController).show();
-        verify(mSidefpsController, never()).hide();
+        verify(mSideFpsController).show(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).hide(any());
     }
 
     @Test
@@ -429,12 +451,12 @@
         setupConditionsToEnableSideFpsHint();
         setSideFpsHintEnabledFromResources(false);
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
-        reset(mSidefpsController);
+        reset(mSideFpsController);
 
         mKeyguardSecurityContainerController.onResume(0);
 
-        verify(mSidefpsController).hide();
-        verify(mSidefpsController, never()).show();
+        verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER);
+        verify(mSideFpsController, never()).show(any());
     }
 
     @Test
@@ -475,6 +497,79 @@
         verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt());
     }
 
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor).requestFaceAuth(
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true);
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor, never())
+                .requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER))
+                .thenReturn(true);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock).showMessage(null, null);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER))
+                .thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null);
+    }
+
+    @Test
+    public void onDensityorFontScaleChanged() {
+        ArgumentCaptor<ConfigurationController.ConfigurationListener>
+                configurationListenerArgumentCaptor = ArgumentCaptor.forClass(
+                ConfigurationController.ConfigurationListener.class);
+        mKeyguardSecurityContainerController.onViewAttached();
+        verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+        configurationListenerArgumentCaptor.getValue().onDensityOrFontScaleChanged();
+
+        verify(mView).onDensityOrFontScaleChanged();
+        verify(mKeyguardSecurityViewFlipperController).onDensityOrFontScaleChanged();
+        verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
+                any(KeyguardSecurityCallback.class));
+    }
+
+
+    private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() {
+        mKeyguardSecurityContainerController.onViewAttached();
+        verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture());
+        return mSwipeListenerArgumentCaptor.getValue();
+    }
+
     private void setupConditionsToEnableSideFpsHint() {
         attachView();
         setSideFpsHintEnabledFromResources(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 82d3ca7..36ed669 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -31,6 +31,7 @@
 
 import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT;
 import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED;
+import static com.android.keyguard.KeyguardSecurityContainer.MODE_USER_SWITCHER;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -54,6 +55,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingA11yDelegate;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.data.source.UserRecord;
@@ -87,6 +89,8 @@
     private FalsingManager mFalsingManager;
     @Mock
     private UserSwitcherController mUserSwitcherController;
+    @Mock
+    private FalsingA11yDelegate mFalsingA11yDelegate;
 
     private KeyguardSecurityContainer mKeyguardSecurityContainer;
 
@@ -111,15 +115,14 @@
         when(mUserSwitcherController.getCurrentUserName()).thenReturn("Test User");
         when(mUserSwitcherController.isKeyguardShowing()).thenReturn(true);
     }
+
     @Test
     public void testOnApplyWindowInsets() {
         int paddingBottom = getContext().getResources()
                 .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin);
         int imeInsetAmount = paddingBottom + 1;
         int systemBarInsetAmount = 0;
-
-        mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
-                mUserSwitcherController, () -> {});
+        initMode(MODE_DEFAULT);
 
         Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
         Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -140,8 +143,7 @@
                 .getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin);
         int systemBarInsetAmount = paddingBottom + 1;
 
-        mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
-                mUserSwitcherController, () -> {});
+        initMode(MODE_DEFAULT);
 
         Insets imeInset = Insets.of(0, 0, 0, imeInsetAmount);
         Insets systemBarInset = Insets.of(0, 0, 0, systemBarInsetAmount);
@@ -157,11 +159,8 @@
 
     @Test
     public void testDefaultViewMode() {
-        mKeyguardSecurityContainer.initMode(MODE_ONE_HANDED, mGlobalSettings, mFalsingManager,
-                mUserSwitcherController, () -> {
-                });
-        mKeyguardSecurityContainer.initMode(MODE_DEFAULT, mGlobalSettings, mFalsingManager,
-                mUserSwitcherController, () -> {});
+        initMode(MODE_ONE_HANDED);
+        initMode(MODE_DEFAULT);
         ConstraintSet.Constraint viewFlipperConstraint =
                 getViewConstraint(mSecurityViewFlipper.getId());
         assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID);
@@ -263,9 +262,12 @@
         ConstraintSet.Constraint userSwitcherConstraint =
                 getViewConstraint(R.id.keyguard_bouncer_user_switcher);
 
-        assertThat(viewFlipperConstraint.layout.topToTop).isEqualTo(PARENT_ID);
+        assertThat(viewFlipperConstraint.layout.topToBottom).isEqualTo(
+                R.id.keyguard_bouncer_user_switcher);
         assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID);
         assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID);
+        assertThat(userSwitcherConstraint.layout.bottomToTop).isEqualTo(
+                mSecurityViewFlipper.getId());
         assertThat(userSwitcherConstraint.layout.topMargin).isEqualTo(
                 getContext().getResources().getDimensionPixelSize(
                         R.dimen.bouncer_user_switcher_y_trans));
@@ -309,6 +311,17 @@
     }
 
     @Test
+    public void testOnDensityOrFontScaleChanged() {
+        setupUserSwitcher();
+        View oldUserSwitcher = mKeyguardSecurityContainer.findViewById(
+                R.id.keyguard_bouncer_user_switcher);
+        mKeyguardSecurityContainer.onDensityOrFontScaleChanged();
+        View newUserSwitcher = mKeyguardSecurityContainer.findViewById(
+                R.id.keyguard_bouncer_user_switcher);
+        assertThat(oldUserSwitcher).isNotEqualTo(newUserSwitcher);
+    }
+
+    @Test
     public void testTouchesAreRecognizedAsBeingOnTheOtherSideOfSecurity() {
         setupUserSwitcher();
         setViewWidth(VIEW_WIDTH);
@@ -377,8 +390,7 @@
 
     private void setupUserSwitcher() {
         when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT);
-        mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER,
-                mGlobalSettings, mFalsingManager, mUserSwitcherController, () -> {});
+        initMode(MODE_USER_SWITCHER);
     }
 
     private ArrayList<UserRecord> buildUserRecords(int count) {
@@ -396,8 +408,7 @@
 
     private void setupForUpdateKeyguardPosition(boolean oneHandedMode) {
         int mode = oneHandedMode ? MODE_ONE_HANDED : MODE_DEFAULT;
-        mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager,
-                mUserSwitcherController, () -> {});
+        initMode(mode);
     }
 
     /** Get the ConstraintLayout constraint of the view. */
@@ -406,4 +417,10 @@
         constraintSet.clone(mKeyguardSecurityContainer);
         return constraintSet.getConstraint(viewId);
     }
+
+    private void initMode(int mode) {
+        mKeyguardSecurityContainer.initMode(mode, mGlobalSettings, mFalsingManager,
+                mUserSwitcherController, () -> {
+                }, mFalsingA11yDelegate);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
index 9296d3d..fd02ac9 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
@@ -106,4 +106,10 @@
             }
         }
     }
+
+    @Test
+    public void onDensityOrFontScaleChanged() {
+        mKeyguardSecurityViewFlipperController.onDensityOrFontScaleChanged();
+        verify(mView).removeAllViews();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 4dcaa7c..c94c97c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -16,12 +16,16 @@
 
 package com.android.keyguard;
 
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.graphics.Rect;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -108,4 +112,16 @@
         configurationListenerArgumentCaptor.getValue().onLocaleListChanged();
         verify(mKeyguardClockSwitchController).onLocaleListChanged();
     }
+
+    @Test
+    public void getClockAnimations_forwardsToClockSwitch() {
+        ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
+        when(mKeyguardClockSwitchController.getClockAnimations()).thenReturn(mockClockAnimations);
+
+        Rect r1 = new Rect(1, 2, 3, 4);
+        Rect r2 = new Rect(5, 6, 7, 8);
+        mController.getClockAnimations().onPositionUpdated(r1, r2, 0.3f);
+
+        verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.3f);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 7281bc8..7231b34 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -21,12 +21,15 @@
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
 import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE;
 import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT;
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
+import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -34,10 +37,12 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyObject;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -53,6 +58,7 @@
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -60,18 +66,21 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
+import android.database.ContentObserver;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
+import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.net.Uri;
 import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -82,6 +91,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.service.dreams.IDreamManager;
+import android.service.trust.TrustAgentService;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -106,9 +116,12 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.SessionTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
+import com.android.systemui.util.settings.GlobalSettings;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -148,6 +161,8 @@
     private static final int FINGERPRINT_SENSOR_ID = 1;
 
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private DumpManager mDumpManager;
     @Mock
     private KeyguardUpdateMonitor.StrongAuthTracker mStrongAuthTracker;
@@ -180,6 +195,8 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
+    private SecureSettings mSecureSettings;
+    @Mock
     private TelephonyManager mTelephonyManager;
     @Mock
     private SensorPrivacyManager mSensorPrivacyManager;
@@ -209,6 +226,10 @@
     private UiEventLogger mUiEventLogger;
     @Mock
     private PowerManager mPowerManager;
+    @Mock
+    private GlobalSettings mGlobalSettings;
+    private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
+
 
     private final int mCurrentUserId = 100;
     private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
@@ -219,6 +240,9 @@
     @Captor
     private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor;
 
+    @Mock
+    private Uri mURI;
+
     // Direct executor
     private final Executor mBackgroundExecutor = Runnable::run;
     private final Executor mMainExecutor = Runnable::run;
@@ -236,8 +260,7 @@
         when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo);
         when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId);
         when(mFaceManager.isHardwareDetected()).thenReturn(true);
-        when(mFaceManager.hasEnrolledTemplates()).thenReturn(true);
-        when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+        when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true);
         when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties);
         when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId);
 
@@ -289,12 +312,26 @@
         ExtendedMockito.doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
                 .when(SubscriptionManager::getDefaultSubscriptionId);
         KeyguardUpdateMonitor.setCurrentUser(mCurrentUserId);
-        ExtendedMockito.doReturn(KeyguardUpdateMonitor.getCurrentUser())
-                .when(ActivityManager::getCurrentUser);
+        when(mUserTracker.getUserId()).thenReturn(mCurrentUserId);
         ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService);
 
+        mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig(
+                mContext.getResources(),
+                mGlobalSettings,
+                mDumpManager
+        );
+
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
+
+        when(mSecureSettings.getUriFor(anyString())).thenReturn(mURI);
+
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        ExtendedMockito.spyOn(contentResolver);
+        doNothing().when(contentResolver)
+                .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class),
+                        anyInt());
+
         mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
 
         verify(mBiometricManager)
@@ -319,7 +356,9 @@
 
     @After
     public void tearDown() {
-        mMockitoSession.finishMocking();
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
         cleanupKeyguardUpdateMonitor();
     }
 
@@ -592,12 +631,12 @@
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
         verify(mFaceManager).isHardwareDetected();
-        verify(mFaceManager).hasEnrolledTemplates(anyInt());
+        verify(mFaceManager, never()).hasEnrolledTemplates(anyInt());
     }
 
     @Test
     public void testNoStartAuthenticate_whenAboutToShowBouncer() {
-        mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(
+        mKeyguardUpdateMonitor.sendPrimaryBouncerChanged(
                 /* bouncerIsOrWillBeShowing */ true, /* bouncerFullyShown */ false);
 
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
@@ -606,16 +645,22 @@
 
     @Test
     public void testTriesToAuthenticate_whenKeyguard() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
-        mTestableLooper.processAllMessages();
         keyguardIsVisible();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mTestableLooper.processAllMessages();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verify(mUiEventLogger).logWithInstanceIdAndPosition(
+                eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP),
+                eq(0),
+                eq(null),
+                any(),
+                eq(PowerManager.WAKE_REASON_POWER_BUTTON));
     }
 
     @Test
     public void skipsAuthentication_whenStatusBarShadeLocked() {
         mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
 
         keyguardIsVisible();
@@ -629,7 +674,7 @@
                 STRONG_AUTH_REQUIRED_AFTER_BOOT);
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
@@ -648,12 +693,42 @@
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT);
     }
 
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException {
+        // This satisfies all the preconditions to run face auth.
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        bouncerFullyVisibleAndNotGoingToSleep();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isTrue();
+    }
+
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException {
+        // This ensures face auth won't run.
+        biometricsDisabledForCurrentUser();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isFalse();
+    }
+
     private void testStrongAuthExceptOnBouncer(int strongAuth) {
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
@@ -661,8 +736,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());
     }
@@ -678,7 +752,7 @@
     @Test
     public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() {
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
@@ -690,7 +764,7 @@
 
     @Test
     public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>());
@@ -701,7 +775,7 @@
 
     @Test
     public void testIgnoresAuth_whenLockdown() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
@@ -713,7 +787,7 @@
 
     @Test
     public void testTriesToAuthenticate_whenLockout() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
@@ -737,7 +811,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout_onlyFace() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -748,7 +822,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout_onlyFingerprint() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -760,7 +834,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -859,7 +933,7 @@
         when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser)))
                 .thenReturn(faceLockoutMode);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -965,6 +1039,7 @@
     @Test
     public void testSecondaryLockscreenRequirement() {
         KeyguardUpdateMonitor.setCurrentUser(UserHandle.myUserId());
+        when(mUserTracker.getUserId()).thenReturn(UserHandle.myUserId());
         int user = KeyguardUpdateMonitor.getCurrentUser();
         String packageName = "fake.test.package";
         String cls = "FakeService";
@@ -1033,7 +1108,7 @@
     @Test
     public void testOccludingAppFingerprintListeningState() {
         // GIVEN keyguard isn't visible (app occluding)
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
         when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
 
@@ -1048,7 +1123,7 @@
     @Test
     public void testOccludingAppRequestsFingerprint() {
         // GIVEN keyguard isn't visible (app occluding)
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
 
         // WHEN an occluding app requests fp
@@ -1091,6 +1166,63 @@
     }
 
     @Test
+    public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled()
+            throws RemoteException {
+        // SFPS supported and enrolled
+        final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+        props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+        when(mAuthController.getSfpsProps()).thenReturn(props);
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN require screen on to auth is disabled, and keyguard is not awake
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0);
+        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true);
+
+        // Preconditions for sfps auth to run
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+
+        statusBarShadeIsLocked();
+        mTestableLooper.processAllMessages();
+
+        // THEN we should listen for sfps when screen off, because require screen on is disabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+
+        // WHEN require screen on to auth is enabled, and keyguard is not awake
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1);
+        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+
+        // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
+
+        // Device now awake & keyguard is now interactive
+        deviceNotGoingToSleep();
+        deviceIsInteractive();
+        keyguardIsVisible();
+
+        // THEN we should listen for sfps when screen on, and require screen on is enabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+    }
+
+    private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal(
+            @FingerprintSensorProperties.SensorType int sensorType) {
+        return new FingerprintSensorPropertiesInternal(
+                0 /* sensorId */,
+                SensorProperties.STRENGTH_STRONG,
+                1 /* maxEnrollmentsPerUser */,
+                new ArrayList<ComponentInfoInternal>(),
+                sensorType,
+                true /* resetLockoutRequiresHardwareAuthToken */);
+    }
+
+    @Test
     public void testShouldNotListenForUdfps_whenTrustEnabled() {
         // GIVEN a "we should listen for udfps" state
         mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD);
@@ -1139,7 +1271,7 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
         setKeyguardBouncerVisibility(false /* isVisible */);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         keyguardIsVisible();
 
@@ -1189,7 +1321,10 @@
                 Arrays.asList("Unlocked by wearable"));
 
         // THEN the showTrustGrantedMessage should be called with the first message
-        verify(mTestCallback).showTrustGrantedMessage("Unlocked by wearable");
+        verify(mTestCallback).onTrustGrantedForCurrentUser(
+                anyBoolean(),
+                eq(new TrustGrantFlags(0)),
+                eq("Unlocked by wearable"));
     }
 
     @Test
@@ -1246,6 +1381,29 @@
     }
 
     @Test
+    public void testShouldListenForFace_whenFpIsAlreadyAuthenticated_returnsFalse()
+            throws RemoteException {
+        // Face auth should run when the following is true.
+        bouncerFullyVisibleAndNotGoingToSleep();
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        strongAuthNotRequired();
+        biometricsEnabledForCurrentUser();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        userNotCurrentlySwitching();
+
+        mTestableLooper.processAllMessages();
+
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
+
+        successfulFingerprintAuth();
+        mTestableLooper.processAllMessages();
+
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
+    }
+
+    @Test
     public void testShouldListenForFace_whenUserIsNotPrimary_returnsFalse() throws RemoteException {
         cleanupKeyguardUpdateMonitor();
         // This disables face auth
@@ -1496,7 +1654,7 @@
     }
 
     @Test
-    public void testShouldListenForFace_whenFaceIsLockedOut_returnsFalse()
+    public void testShouldListenForFace_whenFaceIsLockedOut_returnsTrue()
             throws RemoteException {
         // Preconditions for face auth to run
         keyguardNotGoingAway();
@@ -1513,12 +1671,14 @@
         faceAuthLockedOut();
         mTestableLooper.processAllMessages();
 
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
+        // This is needed beccause we want to show face locked out error message whenever face auth
+        // is supposed to run.
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
     }
 
     @Test
     public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -1567,6 +1727,185 @@
         verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
     }
 
+    @Test
+    public void testDreamingStopped_faceDoesNotRun() {
+        mKeyguardUpdateMonitor.dispatchDreamingStopped();
+        mTestableLooper.processAllMessages();
+
+        verify(mFaceManager, never()).authenticate(
+                any(), any(), any(), any(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testFaceWakeupTrigger_runFaceAuth_onlyOnConfiguredTriggers() {
+        // keyguard is visible
+        keyguardIsVisible();
+
+        // WHEN device wakes up from an application
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_APPLICATION);
+        mTestableLooper.processAllMessages();
+
+        // THEN face auth isn't triggered
+        verify(mFaceManager, never()).authenticate(
+                any(), any(), any(), any(), anyInt(), anyBoolean());
+
+        // WHEN device wakes up from the power button
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mTestableLooper.processAllMessages();
+
+        // THEN face auth is triggered
+        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+    }
+
+
+    @Test
+    public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_deviceInteractive() {
+        // GIVEN device is interactive
+        deviceIsInteractive();
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD flag
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                getCurrentUser() /* userId */,
+                TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback).onTrustGrantedForCurrentUser(
+                eq(true) /* dismissKeyguard */,
+                eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD)),
+                eq(null) /* message */
+        );
+    }
+
+    @Test
+    public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_doesNotDismiss() {
+        // GIVEN device is NOT interactive
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD flag
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                getCurrentUser() /* userId */,
+                TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback).onTrustGrantedForCurrentUser(
+                eq(false) /* dismissKeyguard */,
+                eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD)),
+                eq(null) /* message */
+        );
+    }
+
+    @Test
+    public void testOnTrustGrantedForCurrentUser_dismissKeyguardRequested_temporaryAndRenewable() {
+        // GIVEN device is interactive
+        deviceIsInteractive();
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged for a different user
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                546 /* userId, not the current userId */,
+                0 /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback, never()).onTrustGrantedForCurrentUser(
+                anyBoolean() /* dismissKeyguard */,
+                anyObject() /* flags */,
+                anyString() /* message */
+        );
+    }
+
+    @Test
+    public void testOnTrustGranted_differentUser_noCallback() {
+        // GIVEN device is interactive
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged with TRUST_DISMISS_KEYGUARD AND TRUST_TEMPORARY_AND_RENEWABLE
+        // flags (temporary & rewable is active unlock)
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                getCurrentUser() /* userId */,
+                TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD
+                        | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback).onTrustGrantedForCurrentUser(
+                eq(true) /* dismissKeyguard */,
+                eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD
+                        | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)),
+                eq(null) /* message */
+        );
+    }
+
+    @Test
+    public void testOnTrustGrantedForCurrentUser_bouncerShowing_initiatedByUser() {
+        // GIVEN device is interactive & bouncer is showing
+        deviceIsInteractive();
+        bouncerFullyVisible();
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged with INITIATED_BY_USER flag
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                getCurrentUser() /* userId, not the current userId */,
+                TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback, never()).onTrustGrantedForCurrentUser(
+                eq(true) /* dismissKeyguard */,
+                eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER)),
+                anyString() /* message */
+        );
+    }
+
+    @Test
+    public void testOnTrustGrantedForCurrentUser_bouncerShowing_temporaryRenewable() {
+        // GIVEN device is NOT interactive & bouncer is showing
+        bouncerFullyVisible();
+
+        // GIVEN callback is registered
+        KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(callback);
+
+        // WHEN onTrustChanged with INITIATED_BY_USER flag
+        mKeyguardUpdateMonitor.onTrustChanged(
+                true /* enabled */,
+                getCurrentUser() /* userId, not the current userId */,
+                TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER
+                        | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE /* flags */,
+                null /* trustGrantedMessages */);
+
+        // THEN onTrustGrantedForCurrentUser callback called
+        verify(callback, never()).onTrustGrantedForCurrentUser(
+                eq(true) /* dismissKeyguard */,
+                eq(new TrustGrantFlags(TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER
+                        | TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)),
+                anyString() /* message */
+        );
+    }
+
     private void cleanupKeyguardUpdateMonitor() {
         if (mKeyguardUpdateMonitor != null) {
             mKeyguardUpdateMonitor.removeCallback(mTestCallback);
@@ -1618,8 +1957,17 @@
                 .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START);
     }
 
+    private void successfulFingerprintAuth() {
+        mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback
+                .onAuthenticationSucceeded(
+                        new FingerprintManager.AuthenticationResult(null,
+                                null,
+                                mCurrentUserId,
+                                true));
+    }
+
     private void triggerSuccessfulFaceAuth() {
-        mKeyguardUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+        mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
         verify(mFaceManager).authenticate(any(),
                 any(),
                 mAuthenticationCallbackCaptor.capture(),
@@ -1687,7 +2035,7 @@
     }
 
     private void deviceIsInteractive() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
     }
 
     private void bouncerFullyVisible() {
@@ -1695,7 +2043,7 @@
     }
 
     private void setKeyguardBouncerVisibility(boolean isVisible) {
-        mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(isVisible, isVisible);
+        mKeyguardUpdateMonitor.sendPrimaryBouncerChanged(isVisible, isVisible);
         mTestableLooper.processAllMessages();
     }
 
@@ -1727,9 +2075,9 @@
         AtomicBoolean mSimStateChanged = new AtomicBoolean(false);
 
         protected TestableKeyguardUpdateMonitor(Context context) {
-            super(context,
+            super(context, mUserTracker,
                     TestableLooper.get(KeyguardUpdateMonitorTest.this).getLooper(),
-                    mBroadcastDispatcher, mDumpManager,
+                    mBroadcastDispatcher, mSecureSettings, mDumpManager,
                     mBackgroundExecutor, mMainExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager,
@@ -1737,7 +2085,8 @@
                     mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
                     mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
                     mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
-                    mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager);
+                    mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
+                    mFaceWakeUpTriggersConfig);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
new file mode 100644
index 0000000..ae8f419
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.keyguard;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.util.Pair;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.doze.util.BurnInHelperKt;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class LockIconViewControllerBaseTest extends SysuiTestCase {
+    protected static final String UNLOCKED_LABEL = "unlocked";
+    protected static final int PADDING = 10;
+
+    protected MockitoSession mStaticMockSession;
+
+    protected @Mock LockIconView mLockIconView;
+    protected @Mock AnimatedStateListDrawable mIconDrawable;
+    protected @Mock Context mContext;
+    protected @Mock Resources mResources;
+    protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
+    protected @Mock StatusBarStateController mStatusBarStateController;
+    protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    protected @Mock KeyguardViewController mKeyguardViewController;
+    protected @Mock KeyguardStateController mKeyguardStateController;
+    protected @Mock FalsingManager mFalsingManager;
+    protected @Mock AuthController mAuthController;
+    protected @Mock DumpManager mDumpManager;
+    protected @Mock AccessibilityManager mAccessibilityManager;
+    protected @Mock ConfigurationController mConfigurationController;
+    protected @Mock VibratorHelper mVibrator;
+    protected @Mock AuthRippleController mAuthRippleController;
+    protected @Mock FeatureFlags mFeatureFlags;
+    protected @Mock KeyguardTransitionRepository mTransitionRepository;
+    protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
+
+    protected LockIconViewController mUnderTest;
+
+    // Capture listeners so that they can be used to send events
+    @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+
+    @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
+            ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+    protected KeyguardStateController.Callback mKeyguardStateCallback;
+
+    @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
+            ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+    protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+    @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
+    protected AuthController.Callback mAuthControllerCallback;
+
+    @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback>
+            mKeyguardUpdateMonitorCallbackCaptor =
+            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+    protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
+    @Captor protected ArgumentCaptor<Point> mPointCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        mStaticMockSession = mockitoSession()
+                .mockStatic(BurnInHelperKt.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        MockitoAnnotations.initMocks(this);
+
+        setupLockIconViewMocks();
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
+        Rect windowBounds = new Rect(0, 0, 800, 1200);
+        when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
+        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+        when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
+        when(mAuthController.getScaleFactor()).thenReturn(1f);
+
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+
+        mUnderTest = new LockIconViewController(
+                mLockIconView,
+                mStatusBarStateController,
+                mKeyguardUpdateMonitor,
+                mKeyguardViewController,
+                mKeyguardStateController,
+                mFalsingManager,
+                mAuthController,
+                mDumpManager,
+                mAccessibilityManager,
+                mConfigurationController,
+                mDelayableExecutor,
+                mVibrator,
+                mAuthRippleController,
+                mResources,
+                new KeyguardTransitionInteractor(mTransitionRepository),
+                new KeyguardInteractor(new FakeKeyguardRepository()),
+                mFeatureFlags
+        );
+    }
+
+    @After
+    public void tearDown() {
+        mStaticMockSession.finishMocking();
+    }
+
+    protected Pair<Float, Point> setupUdfps() {
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+        final Point udfpsLocation = new Point(50, 75);
+        final float radius = 33f;
+        when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
+        when(mAuthController.getUdfpsRadius()).thenReturn(radius);
+
+        return new Pair(radius, udfpsLocation);
+    }
+
+    protected void setupShowLockIcon() {
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+    }
+
+    protected void captureAuthControllerCallback() {
+        verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
+        mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
+    }
+
+    protected void captureKeyguardStateCallback() {
+        verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
+        mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
+    }
+
+    protected void captureStatusBarStateListener() {
+        verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
+        mStatusBarStateListener = mStatusBarStateCaptor.getValue();
+    }
+
+    protected void captureKeyguardUpdateMonitorCallback() {
+        verify(mKeyguardUpdateMonitor).registerCallback(
+                mKeyguardUpdateMonitorCallbackCaptor.capture());
+        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+    }
+
+    protected void setupLockIconViewMocks() {
+        when(mLockIconView.getResources()).thenReturn(mResources);
+        when(mLockIconView.getContext()).thenReturn(mContext);
+    }
+
+    protected void resetLockIconView() {
+        reset(mLockIconView);
+        setupLockIconViewMocks();
+    }
+
+    protected void init(boolean useMigrationFlag) {
+        when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag);
+        mUnderTest.init();
+
+        verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
+        mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
new file mode 100644
index 0000000..f4c2284
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.
+ */
+
+package com.android.keyguard;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.hardware.biometrics.BiometricSourceType;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.doze.util.BurnInHelperKt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class LockIconViewControllerTest extends LockIconViewControllerBaseTest {
+
+    @Test
+    public void testUpdateFingerprintLocationOnInit() {
+        // GIVEN fp sensor location is available pre-attached
+        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+
+        // WHEN lock icon view controller is initialized and attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN lock icon view location is updated to the udfps location with UDFPS radius
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testUpdatePaddingBasedOnResolutionScale() {
+        // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
+        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+        when(mAuthController.getScaleFactor()).thenReturn(5f);
+
+        // WHEN lock icon view controller is initialized and attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN lock icon view location is updated with the scaled radius
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING * 5));
+    }
+
+    @Test
+    public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        init(/* useMigrationFlag= */false);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+        // GIVEN fp sensor location is available post-attached
+        captureAuthControllerCallback();
+        Pair<Float, Point> udfps = setupUdfps();
+
+        // WHEN all authenticators are registered
+        mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
+        mDelayableExecutor.runAllReady();
+
+        // THEN lock icon view location is updated with the same coordinates as auth controller vals
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        init(/* useMigrationFlag= */false);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+        // GIVEN fp sensor location is available post-attached
+        captureAuthControllerCallback();
+        Pair<Float, Point> udfps = setupUdfps();
+
+        // WHEN udfps location changes
+        mAuthControllerCallback.onUdfpsLocationChanged();
+        mDelayableExecutor.runAllReady();
+
+        // THEN lock icon view location is updated with the same coordinates as auth controller vals
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
+        // GIVEN Udpfs sensor location is available
+        setupUdfps();
+
+        // WHEN the view is attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon view background should be enabled
+        verify(mLockIconView).setUseBackground(true);
+    }
+
+    @Test
+    public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
+        // GIVEN Udfps sensor location is not supported
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+
+        // WHEN the view is attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon view background should be disabled
+        verify(mLockIconView).setUseBackground(false);
+    }
+
+    @Test
+    public void testUnlockIconShows_biometricUnlockedTrue() {
+        // GIVEN UDFPS sensor location is available
+        setupUdfps();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureKeyguardUpdateMonitorCallback();
+
+        // GIVEN user has unlocked with a biometric auth (ie: face auth)
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+        reset(mLockIconView);
+
+        // WHEN face auth's biometric running state changes
+        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+                BiometricSourceType.FACE);
+
+        // THEN the unlock icon is shown
+        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+    }
+
+    @Test
+    public void testLockIconStartState() {
+        // GIVEN lock icon state
+        setupShowLockIcon();
+
+        // WHEN lock icon controller is initialized
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_updateToUnlock() {
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureKeyguardStateCallback();
+        reset(mLockIconView);
+
+        // WHEN the unlocked state changes to canDismissLockScreen=true
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+        mKeyguardStateCallback.onUnlockedChanged();
+
+        // THEN the unlock should show
+        verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
+        // GIVEN udfps not enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the icon is cleared
+        verify(mLockIconView).clearIcon();
+    }
+
+    @Test
+    public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
+        // GIVEN udfps enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the AOD lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, true);
+    }
+
+    @Test
+    public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
+        // GIVEN udfps enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        // GIVEN burn-in offset = 5
+        int burnInOffset = 5;
+        when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
+
+        // GIVEN starting state for the lock icon (keyguard)
+        setupShowLockIcon();
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN dozing updates
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+        mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+
+        // THEN the view's translation is updated to use the AoD burn-in offsets
+        verify(mLockIconView).setTranslationY(burnInOffset);
+        verify(mLockIconView).setTranslationX(burnInOffset);
+        reset(mLockIconView);
+
+        // WHEN the device is no longer dozing
+        mStatusBarStateListener.onDozingChanged(false /* isDozing */);
+        mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
+
+        // THEN the view is updated to NO translation (no burn-in offsets anymore)
+        verify(mLockIconView).setTranslationY(0);
+        verify(mLockIconView).setTranslationX(0);
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..d2c54b4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.keyguard
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.keyguard.LockIconView.ICON_LOCK
+import com.android.systemui.doze.util.getBurnInOffset
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() {
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps not enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false)
+
+            // GIVEN starting state for the lock icon
+            setupShowLockIcon()
+
+            // GIVEN lock icon controller is initialized and view is attached
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN the dozing state changes
+            mUnderTest.mIsDozingCallback.accept(true)
+
+            // THEN the icon is cleared
+            verify(mLockIconView).clearIcon()
+        }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+            // GIVEN starting state for the lock icon
+            setupShowLockIcon()
+
+            // GIVEN lock icon controller is initialized and view is attached
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN the dozing state changes
+            mUnderTest.mIsDozingCallback.accept(true)
+
+            // THEN the AOD lock icon should show
+            verify(mLockIconView).updateIcon(ICON_LOCK, true)
+        }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testBurnInOffsetsUpdated_onDozeAmountChanged() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+            // GIVEN burn-in offset = 5
+            val burnInOffset = 5
+            whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset)
+
+            // GIVEN starting state for the lock icon (keyguard)
+            setupShowLockIcon()
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN dozing updates
+            mUnderTest.mIsDozingCallback.accept(true)
+            mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+
+            // THEN the view's translation is updated to use the AoD burn-in offsets
+            verify(mLockIconView).setTranslationY(burnInOffset.toFloat())
+            verify(mLockIconView).setTranslationX(burnInOffset.toFloat())
+            reset(mLockIconView)
+
+            // WHEN the device is no longer dozing
+            mUnderTest.mIsDozingCallback.accept(false)
+            mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED))
+
+            // THEN the view is updated to NO translation (no burn-in offsets anymore)
+            verify(mLockIconView).setTranslationY(0f)
+            verify(mLockIconView).setTranslationX(0f)
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java
index ff4412e9..27701be 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockManagerTest.java
@@ -17,6 +17,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -30,15 +31,15 @@
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.LayoutInflater;
 
-import androidx.lifecycle.MutableLiveData;
-
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.settings.CurrentUserObservable;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.After;
 import org.junit.Before;
@@ -52,8 +53,7 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-// Need to run tests on main looper because LiveData operations such as setData, observe,
-// removeObserver cannot be invoked on a background thread.
+// Need to run tests on main looper to allow for onClockChanged operation to happen synchronously.
 @RunWithLooper(setAsMainLooper = true)
 public final class ClockManagerTest extends SysuiTestCase {
 
@@ -63,14 +63,16 @@
     private static final int SECONDARY_USER_ID = 11;
     private static final Uri SETTINGS_URI = null;
 
+    private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
     private ClockManager mClockManager;
     private ContentObserver mContentObserver;
     private DockManagerFake mFakeDockManager;
-    private MutableLiveData<Integer> mCurrentUser;
+    private ArgumentCaptor<UserTracker.Callback> mUserTrackerCallbackCaptor;
     @Mock PluginManager mMockPluginManager;
     @Mock SysuiColorExtractor mMockColorExtractor;
     @Mock ContentResolver mMockContentResolver;
-    @Mock CurrentUserObservable mMockCurrentUserObserable;
+    @Mock UserTracker mUserTracker;
     @Mock SettingsWrapper mMockSettingsWrapper;
     @Mock ClockManager.ClockChangedListener mMockListener1;
     @Mock ClockManager.ClockChangedListener mMockListener2;
@@ -83,18 +85,18 @@
 
         mFakeDockManager = new DockManagerFake();
 
-        mCurrentUser = new MutableLiveData<>();
-        mCurrentUser.setValue(MAIN_USER_ID);
-        when(mMockCurrentUserObserable.getCurrentUser()).thenReturn(mCurrentUser);
+        when(mUserTracker.getUserId()).thenReturn(MAIN_USER_ID);
+        mUserTrackerCallbackCaptor = ArgumentCaptor.forClass(UserTracker.Callback.class);
 
         mClockManager = new ClockManager(getContext(), inflater,
                 mMockPluginManager, mMockColorExtractor, mMockContentResolver,
-                mMockCurrentUserObserable, mMockSettingsWrapper, mFakeDockManager);
+                mUserTracker, mMainExecutor, mMockSettingsWrapper, mFakeDockManager);
 
         mClockManager.addBuiltinClock(() -> new BubbleClockController(
                 getContext().getResources(), inflater, mMockColorExtractor));
         mClockManager.addOnClockChangedListener(mMockListener1);
         mClockManager.addOnClockChangedListener(mMockListener2);
+        verify(mUserTracker).addCallback(mUserTrackerCallbackCaptor.capture(), any());
         reset(mMockListener1, mMockListener2);
 
         mContentObserver = mClockManager.getContentObserver();
@@ -221,7 +223,7 @@
     @Test
     public void onUserChanged_defaultClock() {
         // WHEN the user changes
-        mCurrentUser.setValue(SECONDARY_USER_ID);
+        switchUser(SECONDARY_USER_ID);
         // THEN the plugin is null for the default clock face
         assertThat(mClockManager.getCurrentClock()).isNull();
     }
@@ -232,7 +234,7 @@
         when(mMockSettingsWrapper.getLockScreenCustomClockFace(SECONDARY_USER_ID)).thenReturn(
                 BUBBLE_CLOCK);
         // WHEN the user changes
-        mCurrentUser.setValue(SECONDARY_USER_ID);
+        switchUser(SECONDARY_USER_ID);
         // THEN the plugin is the bubble clock face.
         assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
     }
@@ -244,8 +246,13 @@
         // AND the second user as selected the bubble clock for the dock
         when(mMockSettingsWrapper.getDockedClockFace(SECONDARY_USER_ID)).thenReturn(BUBBLE_CLOCK);
         // WHEN the user changes
-        mCurrentUser.setValue(SECONDARY_USER_ID);
+        switchUser(SECONDARY_USER_ID);
         // THEN the plugin is the bubble clock face.
         assertThat(mClockManager.getCurrentClock()).isInstanceOf(BUBBLE_CLOCK_CLASS);
     }
+
+    private void switchUser(int newUser) {
+        when(mUserTracker.getUserId()).thenReturn(newUser);
+        mUserTrackerCallbackCaptor.getValue().onUserChanged(newUser, mContext);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 6b1ef38..81d0034 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -3,6 +3,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.PackageManager
+import android.content.pm.UserInfo
 import android.content.res.Resources
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
@@ -11,9 +12,11 @@
 import com.android.systemui.flags.FlagListenable
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
@@ -26,9 +29,9 @@
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -44,6 +47,8 @@
     private lateinit var chooserSelector: ChooserSelector
 
     @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockProfileContext: Context
+    @Mock private lateinit var mockUserTracker: UserTracker
     @Mock private lateinit var mockPackageManager: PackageManager
     @Mock private lateinit var mockResources: Resources
     @Mock private lateinit var mockFeatureFlags: FeatureFlags
@@ -52,12 +57,20 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(mockContext.packageManager).thenReturn(mockPackageManager)
-        `when`(mockContext.resources).thenReturn(mockResources)
-        `when`(mockResources.getString(anyInt())).thenReturn(
+        whenever(mockContext.createContextAsUser(any(), anyInt())).thenReturn(mockProfileContext)
+        whenever(mockContext.resources).thenReturn(mockResources)
+        whenever(mockProfileContext.packageManager).thenReturn(mockPackageManager)
+        whenever(mockResources.getString(anyInt())).thenReturn(
                 ComponentName("TestPackage", "TestClass").flattenToString())
+        whenever(mockUserTracker.userProfiles).thenReturn(listOf(UserInfo(), UserInfo()))
 
-        chooserSelector = ChooserSelector(mockContext, mockFeatureFlags, testScope, testDispatcher)
+        chooserSelector = ChooserSelector(
+                mockContext,
+                mockUserTracker,
+                mockFeatureFlags,
+                testScope,
+                testDispatcher,
+        )
     }
 
     @After
@@ -74,7 +87,9 @@
 
         // Assert
         verify(mockFeatureFlags).addListener(
-                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
+                flagListener.capture(),
+        )
         verify(mockFeatureFlags, never()).removeListener(any())
 
         // Act
@@ -87,86 +102,102 @@
     @Test
     fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
         // Arrange
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
 
         // Act
         chooserSelector.start()
 
         // Assert
-        verify(mockPackageManager).setComponentEnabledSetting(
+        verify(mockPackageManager, times(2)).setComponentEnabledSetting(
                 eq(ComponentName("TestPackage", "TestClass")),
                 eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
-                anyInt())
+                anyInt(),
+        )
     }
 
     @Test
     fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
         // Arrange
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
 
         // Act
         chooserSelector.start()
 
         // Assert
-        verify(mockPackageManager).setComponentEnabledSetting(
+        verify(mockPackageManager, times(2)).setComponentEnabledSetting(
                 eq(ComponentName("TestPackage", "TestClass")),
                 eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
-                anyInt())
+                anyInt(),
+        )
     }
 
     @Test
     fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
         // Arrange
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
-                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
+                flagListener.capture(),
+        )
         verify(mockPackageManager, never()).setComponentEnabledSetting(
-                any(), eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), anyInt())
+                any(),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                anyInt(),
+        )
 
         // Act
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
         flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
 
         // Assert
-        verify(mockPackageManager).setComponentEnabledSetting(
+        verify(mockPackageManager, times(2)).setComponentEnabledSetting(
                 eq(ComponentName("TestPackage", "TestClass")),
                 eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
-                anyInt())
+                anyInt(),
+        )
     }
 
     @Test
     fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
         // Arrange
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
-                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
+                flagListener.capture(),
+        )
         verify(mockPackageManager, never()).setComponentEnabledSetting(
-                any(), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), anyInt())
+                any(),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                anyInt(),
+        )
 
         // Act
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
         flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
 
         // Assert
-        verify(mockPackageManager).setComponentEnabledSetting(
+        verify(mockPackageManager, times(2)).setComponentEnabledSetting(
                 eq(ComponentName("TestPackage", "TestClass")),
                 eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
-                anyInt())
+                anyInt(),
+        )
     }
 
     @Test
     fun doesNothing_whenAnotherFlagChanges() {
         // Arrange
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
-                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
+                flagListener.capture(),
+        )
         clearInvocations(mockPackageManager)
 
         // Act
-        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
         flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1))
 
         // Assert
diff --git a/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt
index a4e0825..5886206 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/DisplayCutoutBaseViewTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui
 
+import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Insets
 import android.graphics.Path
@@ -48,6 +49,7 @@
     @Mock private lateinit var mockCanvas: Canvas
     @Mock private lateinit var mockRootView: View
     @Mock private lateinit var mockDisplay: Display
+    @Mock private lateinit var mockContext: Context
 
     private lateinit var cutoutBaseView: DisplayCutoutBaseView
     private val cutout: DisplayCutout = DisplayCutout.Builder()
@@ -168,7 +170,9 @@
                 R.bool.config_fillMainBuiltInDisplayCutout, fillCutout)
 
         cutoutBaseView = spy(DisplayCutoutBaseView(mContext))
-        whenever(cutoutBaseView.display).thenReturn(mockDisplay)
+
+        whenever(cutoutBaseView.context).thenReturn(mockContext)
+        whenever(mockContext.display).thenReturn(mockDisplay)
         whenever(mockDisplay.uniqueId).thenReturn("mockDisplayUniqueId")
         whenever(cutoutBaseView.rootView).thenReturn(mockRootView)
         whenever(cutoutBaseView.getPhysicalPixelDisplaySizeRatio()).thenReturn(1f)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
index 054650b..8207fa6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorHwcLayerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui
 
+import android.content.Context
 import android.graphics.Insets
 import android.graphics.PixelFormat
 import android.graphics.Rect
@@ -44,6 +45,7 @@
 
     @Mock private lateinit var mockDisplay: Display
     @Mock private lateinit var mockRootView: View
+    @Mock private lateinit var mockContext: Context
 
     private val displayWidth = 100
     private val displayHeight = 200
@@ -75,7 +77,8 @@
         decorHwcLayer = Mockito.spy(ScreenDecorHwcLayer(mContext, decorationSupport))
         whenever(decorHwcLayer.width).thenReturn(displayWidth)
         whenever(decorHwcLayer.height).thenReturn(displayHeight)
-        whenever(decorHwcLayer.display).thenReturn(mockDisplay)
+        whenever(decorHwcLayer.context).thenReturn(mockContext)
+        whenever(mockContext.display).thenReturn(mockDisplay)
         whenever(decorHwcLayer.rootView).thenReturn(mockRootView)
         whenever(mockRootView.left).thenReturn(0)
         whenever(mockRootView.top).thenReturn(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 2319f43..181839a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -255,6 +256,7 @@
         });
         mScreenDecorations.mDisplayInfo = mDisplayInfo;
         doReturn(1f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
+        doNothing().when(mScreenDecorations).updateOverlayProviderViews(any());
         reset(mTunerService);
 
         try {
@@ -1005,18 +1007,13 @@
         assertEquals(new Size(3, 3), resDelegate.getTopRoundedSize());
         assertEquals(new Size(4, 4), resDelegate.getBottomRoundedSize());
 
-        setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
-                getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px)
-                /* roundedTopDrawable */,
-                getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px)
-                /* roundedBottomDrawable */,
-                0 /* roundedPadding */, true /* privacyDot */, false /* faceScanning*/);
+        doReturn(2f).when(mScreenDecorations).getPhysicalPixelDisplaySizeRatio();
         mDisplayInfo.rotation = Surface.ROTATION_270;
 
         mScreenDecorations.onConfigurationChanged(null);
 
-        assertEquals(new Size(4, 4), resDelegate.getTopRoundedSize());
-        assertEquals(new Size(5, 5), resDelegate.getBottomRoundedSize());
+        assertEquals(new Size(6, 6), resDelegate.getTopRoundedSize());
+        assertEquals(new Size(8, 8), resDelegate.getBottomRoundedSize());
     }
 
     @Test
@@ -1293,51 +1290,6 @@
     }
 
     @Test
-    public void testOnDisplayChanged_hwcLayer() {
-        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
-                null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
-                0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
-        final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport();
-        decorationSupport.format = PixelFormat.R_8;
-        doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport();
-
-        // top cutout
-        mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
-
-        mScreenDecorations.start();
-
-        final ScreenDecorHwcLayer hwcLayer = mScreenDecorations.mScreenDecorHwcLayer;
-        spyOn(hwcLayer);
-        doReturn(mDisplay).when(hwcLayer).getDisplay();
-
-        mScreenDecorations.mDisplayListener.onDisplayChanged(1);
-
-        verify(hwcLayer, times(1)).onDisplayChanged(any());
-    }
-
-    @Test
-    public void testOnDisplayChanged_nonHwcLayer() {
-        setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
-                null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
-                0 /* roundedPadding */, false /* privacyDot */, false /* faceScanning */);
-
-        // top cutout
-        mMockCutoutList.add(new CutoutDecorProviderImpl(BOUNDS_POSITION_TOP));
-
-        mScreenDecorations.start();
-
-        final ScreenDecorations.DisplayCutoutView cutoutView = (ScreenDecorations.DisplayCutoutView)
-                mScreenDecorations.getOverlayView(R.id.display_cutout);
-        assertNotNull(cutoutView);
-        spyOn(cutoutView);
-        doReturn(mDisplay).when(cutoutView).getDisplay();
-
-        mScreenDecorations.mDisplayListener.onDisplayChanged(1);
-
-        verify(cutoutView, times(1)).onDisplayChanged(any());
-    }
-
-    @Test
     public void testHasSameProvidersWithNullOverlays() {
         setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */,
                 null /* roundedTopDrawable */, null /* roundedBottomDrawable */,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 69ccc8b..57ca9c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -903,6 +903,97 @@
     }
 
     @Test
+    public void changeMagnificationSize_expectedWindowSize() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final float magnificationScaleLarge = 2.5f;
+        final int initSize = Math.min(bounds.width(), bounds.height()) / 3;
+        final int magnificationSize = (int) (initSize * magnificationScaleLarge);
+
+        final int expectedWindowHeight = magnificationSize;
+        final int expectedWindowWidth = magnificationSize;
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.changeMagnificationSize(
+                            WindowMagnificationSettings.MagnificationSize.LARGE);
+                    actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+                    actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+                });
+
+        assertEquals(expectedWindowHeight, actualWindowHeight.get());
+        assertEquals(expectedWindowWidth, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragCorner_resizesWindow() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                });
+
+        waitForIdleSync();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f);
+                    actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+                    actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+                });
+
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize + 2, actualWindowWidth.get());
+    }
+
+    @Test
+    public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() {
+        final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+
+        final int startingSize = (int) (bounds.width() / 2f);
+
+        mInstrumentation.runOnMainSync(
+                () ->
+                        mWindowMagnificationController.enableWindowMagnificationInternal(
+                                Float.NaN, Float.NaN, Float.NaN));
+
+        final AtomicInteger actualWindowHeight = new AtomicInteger();
+        final AtomicInteger actualWindowWidth = new AtomicInteger();
+
+        mInstrumentation.runOnMainSync(
+                () -> {
+                    mWindowMagnificationController.setWindowSize(startingSize, startingSize);
+                    mWindowMagnificationController.setEditMagnifierSizeMode(true);
+                    mWindowMagnificationController
+                            .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f);
+                    actualWindowHeight.set(mWindowManager.getLayoutParamsFromAttachedView().height);
+                    actualWindowWidth.set(mWindowManager.getLayoutParamsFromAttachedView().width);
+                });
+        assertEquals(startingSize + 1, actualWindowHeight.get());
+        assertEquals(startingSize, actualWindowWidth.get());
+    }
+
+    @Test
     public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() {
 
         final int minimumWindowSize = mResources.getDimensionPixelSize(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 19a6c66..77d38c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -35,6 +35,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -68,6 +69,7 @@
     public MockitoRule mockito = MockitoJUnit.rule();
 
     private Context mContextWrapper;
+    private AccessibilityManager mAccessibilityManager;
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private AccessibilityFloatingMenuController mController;
     private AccessibilityButtonTargetsObserver mTargetsObserver;
@@ -87,6 +89,7 @@
             }
         };
 
+        mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
         mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
         mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(),
@@ -348,8 +351,8 @@
         mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
         final AccessibilityFloatingMenuController controller =
                 new AccessibilityFloatingMenuController(mContextWrapper, windowManager,
-                        displayManager, mTargetsObserver, mModeObserver, mKeyguardUpdateMonitor,
-                        featureFlags);
+                        displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver,
+                        mKeyguardUpdateMonitor, featureFlags);
         controller.init();
 
         return controller;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
new file mode 100644
index 0000000..a36105e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.wm.shell.bubbles.DismissView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DismissAnimationController}. */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class DismissAnimationControllerTest extends SysuiTestCase {
+    private DismissAnimationController mDismissAnimationController;
+    private DismissView mDismissView;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel,
+                stubMenuViewAppearance);
+        mDismissView = spy(new DismissView(mContext));
+        mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView);
+    }
+
+    @Test
+    public void showDismissView_success() {
+        mDismissAnimationController.showDismissView(true);
+
+        verify(mDismissView).show();
+    }
+
+    @Test
+    public void hideDismissView_success() {
+        mDismissAnimationController.showDismissView(false);
+
+        verify(mDismissView).hide();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
new file mode 100644
index 0000000..d0bd4f7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.PointF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MenuAnimationController}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class MenuAnimationControllerTest extends SysuiTestCase {
+
+    private ViewPropertyAnimator mViewPropertyAnimator;
+    private MenuView mMenuView;
+    private MenuAnimationController mMenuAnimationController;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        mViewPropertyAnimator = spy(mMenuView.animate());
+        doReturn(mViewPropertyAnimator).when(mMenuView).animate();
+
+        mMenuAnimationController = new MenuAnimationController(mMenuView);
+    }
+
+    @Test
+    public void moveToPosition_matchPosition() {
+        final PointF destination = new PointF(50, 60);
+
+        mMenuAnimationController.moveToPosition(destination);
+
+        assertThat(mMenuView.getTranslationX()).isEqualTo(50);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(60);
+    }
+
+    @Test
+    public void startShrinkAnimation_verifyAnimationEndAction() {
+        mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE));
+
+        verify(mViewPropertyAnimator).withEndAction(any(Runnable.class));
+    }
+
+    @Test
+    public void startGrowAnimation_menuCompletelyOpaque() {
+        mMenuAnimationController.startShrinkAnimation(null);
+
+        mMenuAnimationController.startGrowAnimation();
+
+        assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
index d8b10e0..e62a329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.verify;
 
 import android.testing.AndroidTestingRunner;
@@ -25,6 +26,7 @@
 
 import com.android.systemui.SysuiTestCase;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,13 +44,24 @@
     @Mock
     private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged;
 
+    private MenuInfoRepository mMenuInfoRepository;
+
+    @Before
+    public void setUp() {
+        mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged);
+    }
+
     @Test
     public void menuSizeTypeChanged_verifyOnSizeTypeChanged() {
-        final MenuInfoRepository menuInfoRepository =
-                new MenuInfoRepository(mContext,  mMockSettingsContentsChanged);
-
-        menuInfoRepository.mMenuSizeContentObserver.onChange(true);
+        mMenuInfoRepository.mMenuSizeContentObserver.onChange(true);
 
         verify(mMockSettingsContentsChanged).onSizeTypeChanged(anyInt());
     }
+
+    @Test
+    public void menuOpacityChanged_verifyOnFadeEffectChanged() {
+        mMenuInfoRepository.mMenuFadeOutContentObserver.onChange(true);
+
+        verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class));
+    }
 }
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
new file mode 100644
index 0000000..78ee627
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+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;
+
+/** Tests for {@link MenuItemAccessibilityDelegate}. */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private DismissAnimationController.DismissCallback mStubDismissCallback;
+
+    private RecyclerView mStubListView;
+    private MenuView mMenuView;
+    private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
+    private MenuAnimationController mMenuAnimationController;
+    private final Rect mDraggableBounds = new Rect(100, 200, 300, 400);
+
+    @Before
+    public void setUp() {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+
+        final int halfScreenHeight =
+                stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2;
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        mMenuView.setTranslationY(halfScreenHeight);
+
+        doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds();
+        mStubListView = new RecyclerView(mContext);
+        mMenuAnimationController = spy(new MenuAnimationController(mMenuView));
+        mMenuItemAccessibilityDelegate =
+                new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate(
+                        mStubListView), mMenuAnimationController);
+    }
+
+    @Test
+    public void getAccessibilityActionList_matchSize() {
+        final AccessibilityNodeInfoCompat info =
+                new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
+
+        mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
+
+        assertThat(info.getActionList().size()).isEqualTo(6);
+    }
+
+    @Test
+    public void performMoveTopLeftAction_matchPosition() {
+        final boolean moveTopLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_left,
+                        null);
+
+        assertThat(moveTopLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveTopRightAction_matchPosition() {
+        final boolean moveTopRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_right, null);
+
+        assertThat(moveTopRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveBottomLeftAction_matchPosition() {
+        final boolean moveBottomLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_left, null);
+
+        assertThat(moveBottomLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveBottomRightAction_matchPosition() {
+        final boolean moveBottomRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_right, null);
+
+        assertThat(moveBottomRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveToEdgeAndHideAction_success() {
+        final boolean moveToEdgeAndHideAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_to_edge_and_hide, null);
+
+        assertThat(moveToEdgeAndHideAction).isTrue();
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @Test
+    public void performMoveOutFromEdgeAction_success() {
+        final boolean moveOutEdgeAndShowAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_out_edge_and_show, null);
+
+        assertThat(moveOutEdgeAndShowAction).isTrue();
+        verify(mMenuAnimationController).moveOutEdgeAndShow();
+    }
+
+    @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);
+
+        verify(mMenuAnimationController).fadeInNowIfEnabled();
+    }
+
+    @Test
+    public void performClearFocusAction_fadeOut() {
+        mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
+
+        verify(mMenuAnimationController).fadeOutIfEnabled();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
new file mode 100644
index 0000000..4acb394
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.accessibility.floatingmenu;
+
+import static android.view.View.OVER_SCROLL_NEVER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.accessibility.dialog.AccessibilityTarget;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.accessibility.MotionEventHelper;
+import com.android.wm.shell.bubbles.DismissView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MenuListViewTouchHandler}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class MenuListViewTouchHandlerTest extends SysuiTestCase {
+    private final List<AccessibilityTarget> mStubTargets = new ArrayList<>(
+            Collections.singletonList(mock(AccessibilityTarget.class)));
+    private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+    private MenuView mStubMenuView;
+    private MenuListViewTouchHandler mTouchHandler;
+    private MenuAnimationController mMenuAnimationController;
+    private DismissAnimationController mDismissAnimationController;
+    private RecyclerView mStubListView;
+    private DismissView mDismissView;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                windowManager);
+        mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+        mStubMenuView.setTranslationX(0);
+        mStubMenuView.setTranslationY(0);
+        mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView));
+        mDismissView = spy(new DismissView(mContext));
+        mDismissAnimationController =
+                spy(new DismissAnimationController(mDismissView, mStubMenuView));
+        mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
+                mDismissAnimationController);
+        final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets);
+        mStubListView = (RecyclerView) mStubMenuView.getChildAt(0);
+        mStubListView.setAdapter(stubAdapter);
+    }
+
+    @Test
+    public void onActionDownEvent_shouldCancelAnimations() {
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+
+        verify(mMenuAnimationController).cancelAnimations();
+    }
+
+    @Test
+    public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() {
+        doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent(
+                any(MotionEvent.class));
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mStubListView.setOverScrollMode(OVER_SCROLL_NEVER);
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+
+        assertThat(mStubMenuView.getTranslationX()).isEqualTo(offset);
+        assertThat(mStubMenuView.getTranslationY()).isEqualTo(offset);
+    }
+
+    @Test
+    public void onActionMoveEvent_shouldShowDismissView() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+
+        verify(mDismissView).show();
+    }
+
+    @Test
+    public void dragAndDrop_shouldFlingMenuThenSpringToEdge() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).flingMenuThenSpringToEdge(anyFloat(), anyFloat(),
+                anyFloat());
+    }
+
+    @Test
+    public void dragMenuOutOfBoundsAndDrop_moveToLeftEdge_shouldMoveToEdgeAndHide() {
+        final int offset = -100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @After
+    public void tearDown() {
+        mMotionEventHelper.recycleEvents();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
index f782a44..dd7ce0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
@@ -16,13 +16,25 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.systemBars;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -38,6 +50,7 @@
 
 /** Tests for {@link MenuViewLayerController}. */
 @RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class MenuViewLayerControllerTest extends SysuiTestCase {
     @Rule
@@ -46,11 +59,25 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private AccessibilityManager mAccessibilityManager;
+
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private MenuViewLayerController mMenuViewLayerController;
 
     @Before
     public void setUp() throws Exception {
-        mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager);
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
+                mWindowManager).getMaximumWindowMetrics();
+        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340));
+        when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets());
+        mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager,
+                mAccessibilityManager);
     }
 
     @Test
@@ -68,4 +95,14 @@
 
         verify(mWindowManager).removeView(any(View.class));
     }
+
+    private WindowInsets stubDisplayInsets() {
+        final int stubStatusBarHeight = 118;
+        final int stubNavigationBarHeight = 125;
+        return new WindowInsets.Builder()
+                .setVisible(systemBars() | displayCutout(), true)
+                .setInsets(systemBars() | displayCutout(),
+                        Insets.of(0, stubStatusBarHeight, 0, stubNavigationBarHeight))
+                .build();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 8883cb7..2d5188f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -19,32 +19,90 @@
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
+import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.UserHandle;
+import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
 
+import org.junit.After;
 import org.junit.Before;
+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;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /** Tests for {@link MenuViewLayer}. */
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class MenuViewLayerTest extends SysuiTestCase {
+    private static final String SELECT_TO_SPEAK_PACKAGE_NAME = "com.google.android.marvin.talkback";
+    private static final String SELECT_TO_SPEAK_SERVICE_NAME =
+            "com.google.android.accessibility.selecttospeak.SelectToSpeakService";
+    private static final ComponentName TEST_SELECT_TO_SPEAK_COMPONENT_NAME = new ComponentName(
+            SELECT_TO_SPEAK_PACKAGE_NAME, SELECT_TO_SPEAK_SERVICE_NAME);
+
     private MenuViewLayer mMenuViewLayer;
+    private String mLastAccessibilityButtonTargets;
+    private String mLastEnabledAccessibilityServices;
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IAccessibilityFloatingMenu mFloatingMenu;
+
+    @Mock
+    private AccessibilityManager mStubAccessibilityManager;
 
     @Before
     public void setUp() throws Exception {
-        mMenuViewLayer = new MenuViewLayer(mContext);
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mStubAccessibilityManager,
+                mFloatingMenu);
+
+        mLastAccessibilityButtonTargets =
+                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
+        mLastEnabledAccessibilityServices =
+                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets,
+                UserHandle.USER_CURRENT);
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
+                UserHandle.USER_CURRENT);
     }
 
     @Test
@@ -62,4 +120,52 @@
 
         assertThat(menuView.getVisibility()).isEqualTo(GONE);
     }
+
+    @Test
+    public void tiggerDismissMenuAction_hideFloatingMenu() {
+        mMenuViewLayer.mDismissMenuAction.run();
+
+        verify(mFloatingMenu).hide();
+    }
+
+    @Test
+    public void tiggerDismissMenuAction_matchA11yButtonTargetsResult() {
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                MAGNIFICATION_COMPONENT_NAME.flattenToString(), UserHandle.USER_CURRENT);
+
+        mMenuViewLayer.mDismissMenuAction.run();
+        final String value =
+                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                        Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
+
+        assertThat(value).isEqualTo("");
+    }
+
+    @Test
+    public void tiggerDismissMenuAction_matchEnabledA11yServicesResult() {
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+                TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
+        final ResolveInfo resolveInfo = new ResolveInfo();
+        final ServiceInfo serviceInfo = new ServiceInfo();
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        resolveInfo.serviceInfo = serviceInfo;
+        serviceInfo.applicationInfo = applicationInfo;
+        applicationInfo.targetSdkVersion = Build.VERSION_CODES.R;
+        final AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
+        accessibilityServiceInfo.setResolveInfo(resolveInfo);
+        accessibilityServiceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
+        final List<AccessibilityServiceInfo> serviceInfoList = new ArrayList<>();
+        accessibilityServiceInfo.setComponentName(TEST_SELECT_TO_SPEAK_COMPONENT_NAME);
+        serviceInfoList.add(accessibilityServiceInfo);
+        when(mStubAccessibilityManager.getEnabledAccessibilityServiceList(
+                AccessibilityServiceInfo.FEEDBACK_ALL_MASK)).thenReturn(serviceInfoList);
+
+        mMenuViewLayer.mDismissMenuAction.run();
+        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+
+        assertThat(value).isEqualTo("");
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index 513044d..742ee53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -24,11 +24,15 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.UiModeManager;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.After;
@@ -45,6 +49,8 @@
     private int mNightMode;
     private UiModeManager mUiModeManager;
     private MenuView mMenuView;
+    private String mLastPosition;
+    private MenuViewAppearance mStubMenuViewAppearance;
 
     @Before
     public void setUp() throws Exception {
@@ -52,8 +58,11 @@
         mNightMode = mUiModeManager.getNightMode();
         mUiModeManager.setNightMode(MODE_NIGHT_YES);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
-        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext);
-        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager);
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance));
+        mLastPosition = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
     }
 
     @Test
@@ -74,8 +83,58 @@
         assertThat(areInsetsMatched).isTrue();
     }
 
+    @Test
+    public void onDraggingStart_matchInsets() {
+        mMenuView.onDraggingStart();
+        final InstantInsetLayerDrawable insetLayerDrawable =
+                (InstantInsetLayerDrawable) mMenuView.getBackground();
+
+        assertThat(insetLayerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetTop(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetRight(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetBottom(INDEX_MENU_ITEM)).isEqualTo(0);
+    }
+
+    @Test
+    public void onAnimationend_updatePositionForSharedPreference() {
+        final float percentageX = 0.0f;
+        final float percentageY = 0.5f;
+
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+        final String positionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+        final Position position = Position.fromString(positionString);
+
+        assertThat(position.getPercentageX()).isEqualTo(percentageX);
+        assertThat(position.getPercentageY()).isEqualTo(percentageY);
+    }
+
+    @Test
+    public void onEdgeChangedIfNeeded_moveToLeftEdge_matchRadii() {
+        final Rect draggableBounds = mStubMenuViewAppearance.getMenuDraggableBounds();
+        mMenuView.setTranslationX(draggableBounds.right);
+
+        mMenuView.setTranslationX(draggableBounds.left);
+        mMenuView.onEdgeChangedIfNeeded();
+        final float[] radii = getMenuViewGradient().getCornerRadii();
+
+        assertThat(radii[0]).isEqualTo(0.0f);
+        assertThat(radii[1]).isEqualTo(0.0f);
+        assertThat(radii[6]).isEqualTo(0.0f);
+        assertThat(radii[7]).isEqualTo(0.0f);
+    }
+
+    private InstantInsetLayerDrawable getMenuViewInsetLayer() {
+        return (InstantInsetLayerDrawable) mMenuView.getBackground();
+    }
+
+    private GradientDrawable getMenuViewGradient() {
+        return (GradientDrawable) getMenuViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
+    }
+
     @After
     public void tearDown() throws Exception {
         mUiModeManager.setNightMode(mNightMode);
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, mLastPosition);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
new file mode 100644
index 0000000..982f033
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class AccessorizedBatteryDrawableTest : SysuiTestCase() {
+    @Test
+    fun intrinsicSize_shieldFalse_isBatterySize() {
+        val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+        drawable.displayShield = false
+
+        val density = context.resources.displayMetrics.density
+        assertThat(drawable.intrinsicHeight).isEqualTo((BATTERY_HEIGHT * density).toInt())
+        assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH * density).toInt())
+    }
+
+    @Test
+    fun intrinsicSize_shieldTrue_isBatteryPlusShieldSize() {
+        val drawable = AccessorizedBatteryDrawable(context, frameColor = 0)
+        drawable.displayShield = true
+
+        val density = context.resources.displayMetrics.density
+        assertThat(drawable.intrinsicHeight)
+            .isEqualTo((BATTERY_HEIGHT_WITH_SHIELD * density).toInt())
+        assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH_WITH_SHIELD * density).toInt())
+    }
+
+    // TODO(b/255625888): Screenshot tests for this drawable would be amazing!
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1d038a4..1482f29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -34,7 +34,9 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
@@ -50,15 +52,16 @@
     private BatteryMeterView mBatteryMeterView;
 
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private ConfigurationController mConfigurationController;
     @Mock
     private TunerService mTunerService;
     @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
     private Handler mHandler;
     @Mock
     private ContentResolver mContentResolver;
+    private FakeFeatureFlags mFeatureFlags;
     @Mock
     private BatteryController mBatteryController;
 
@@ -71,19 +74,13 @@
         when(mBatteryMeterView.getContext()).thenReturn(mContext);
         when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
 
-        mController = new BatteryMeterViewController(
-                mBatteryMeterView,
-                mConfigurationController,
-                mTunerService,
-                mBroadcastDispatcher,
-                mHandler,
-                mContentResolver,
-                mBatteryController
-        );
+        mFeatureFlags = new FakeFeatureFlags();
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
     }
 
     @Test
     public void onViewAttached_callbacksRegistered() {
+        initController();
         mController.onViewAttached();
 
         verify(mConfigurationController).addCallback(any());
@@ -101,6 +98,7 @@
 
     @Test
     public void onViewDetached_callbacksUnregistered() {
+        initController();
         // Set everything up first.
         mController.onViewAttached();
 
@@ -114,6 +112,7 @@
 
     @Test
     public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() {
+        initController();
         // Start out receiving tuner updates
         mController.onViewAttached();
 
@@ -124,10 +123,43 @@
 
     @Test
     public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() {
+        initController();
+
         mController.ignoreTunerUpdates();
 
         mController.onViewAttached();
 
         verify(mTunerService, never()).addTunable(any(), any());
     }
+
+    @Test
+    public void shieldFlagDisabled_viewNotified() {
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false);
+
+        initController();
+
+        verify(mBatteryMeterView).setDisplayShieldEnabled(false);
+    }
+
+    @Test
+    public void shieldFlagEnabled_viewNotified() {
+        mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, true);
+
+        initController();
+
+        verify(mBatteryMeterView).setDisplayShieldEnabled(true);
+    }
+
+    private void initController() {
+        mController = new BatteryMeterViewController(
+                mBatteryMeterView,
+                mUserTracker,
+                mConfigurationController,
+                mTunerService,
+                mHandler,
+                mContentResolver,
+                mFeatureFlags,
+                mBatteryController
+        );
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index b4ff2a5..eb7d9c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -17,7 +17,9 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.widget.ImageView
 import androidx.test.filters.SmallTest
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
 import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
@@ -58,6 +60,182 @@
         // No assert needed
     }
 
+    @Test
+    fun contentDescription_unknown() {
+        mBatteryMeterView.onBatteryUnknownStateChanged(true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_unknown)
+        )
+    }
+
+    @Test
+    fun contentDescription_estimate() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+                )
+        )
+    }
+
+    @Test
+    fun contentDescription_estimateAndOverheated() {
+        mBatteryMeterView.onBatteryLevelChanged(17, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_charging_paused_with_estimate,
+                        17,
+                        ESTIMATE,
+                )
+        )
+    }
+
+    @Test
+    fun contentDescription_overheated() {
+        mBatteryMeterView.onBatteryLevelChanged(90, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+        )
+    }
+
+    @Test
+    fun contentDescription_charging() {
+        mBatteryMeterView.onBatteryLevelChanged(45, true)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging, 45)
+        )
+    }
+
+    @Test
+    fun contentDescription_notCharging() {
+        mBatteryMeterView.onBatteryLevelChanged(45, false)
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 45)
+        )
+    }
+
+    @Test
+    fun changesFromEstimateToPercent_textAndContentDescriptionChanges() {
+        mBatteryMeterView.onBatteryLevelChanged(15, false)
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+
+        mBatteryMeterView.updatePercentText()
+
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+                )
+        )
+
+        // Update the show mode from estimate to percent
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+
+        assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 15)
+        )
+    }
+
+    @Test
+    fun contentDescription_manyUpdates_alwaysUpdated() {
+        // Overheated
+        mBatteryMeterView.onBatteryLevelChanged(90, false)
+        mBatteryMeterView.onIsOverheatedChanged(true)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
+        )
+
+        // Overheated & estimate
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
+        mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
+        mBatteryMeterView.updatePercentText()
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_charging_paused_with_estimate,
+                        90,
+                        ESTIMATE,
+                )
+        )
+
+        // Just estimate
+        mBatteryMeterView.onIsOverheatedChanged(false)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(
+                        R.string.accessibility_battery_level_with_estimate,
+                        90,
+                        ESTIMATE,
+                )
+        )
+
+        // Just percent
+        mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level, 90)
+        )
+
+        // Charging
+        mBatteryMeterView.onBatteryLevelChanged(90, true)
+        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+                context.getString(R.string.accessibility_battery_level_charging, 90)
+        )
+    }
+
+    @Test
+    fun isOverheatedChanged_true_drawableGetsTrue() {
+        mBatteryMeterView.setDisplayShieldEnabled(true)
+        val drawable = getBatteryDrawable()
+
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(drawable.displayShield).isTrue()
+    }
+
+    @Test
+    fun isOverheatedChanged_false_drawableGetsFalse() {
+        mBatteryMeterView.setDisplayShieldEnabled(true)
+        val drawable = getBatteryDrawable()
+
+        // Start as true
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        // Update to false
+        mBatteryMeterView.onIsOverheatedChanged(false)
+
+        assertThat(drawable.displayShield).isFalse()
+    }
+
+    @Test
+    fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() {
+        mBatteryMeterView.setDisplayShieldEnabled(false)
+        val drawable = getBatteryDrawable()
+
+        mBatteryMeterView.onIsOverheatedChanged(true)
+
+        assertThat(drawable.displayShield).isFalse()
+    }
+
+    private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
+        return (mBatteryMeterView.getChildAt(0) as ImageView)
+                .drawable as AccessorizedBatteryDrawable
+    }
+
     private class Fetcher : BatteryEstimateFetcher {
         override fun fetchBatteryTimeRemainingEstimate(
                 completion: EstimateFetchCompletion) {
@@ -68,4 +246,4 @@
     private companion object {
         const val ESTIMATE = "2 hours 2 minutes"
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
new file mode 100644
index 0000000..39cb047
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt
@@ -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 com.android.systemui.battery
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT
+import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH
+import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class BatterySpecsTest : SysuiTestCase() {
+    @Test
+    fun getFullBatteryHeight_shieldFalse_returnsMainHeight() {
+        val fullHeight = BatterySpecs.getFullBatteryHeight(56f, displayShield = false)
+
+        assertThat(fullHeight).isEqualTo(56f)
+    }
+
+    @Test
+    fun getFullBatteryHeight_shieldTrue_returnsMainHeightPlusShield() {
+        val mainHeight = BATTERY_HEIGHT * 5
+        val fullHeight = BatterySpecs.getFullBatteryHeight(mainHeight, displayShield = true)
+
+        // Since the main battery was scaled 5x, the output height should also be scaled 5x
+        val expectedFullHeight = BATTERY_HEIGHT_WITH_SHIELD * 5
+
+        assertThat(fullHeight).isWithin(.0001f).of(expectedFullHeight)
+    }
+
+    @Test
+    fun getFullBatteryWidth_shieldFalse_returnsMainWidth() {
+        val fullWidth = BatterySpecs.getFullBatteryWidth(33f, displayShield = false)
+
+        assertThat(fullWidth).isEqualTo(33f)
+    }
+
+    @Test
+    fun getFullBatteryWidth_shieldTrue_returnsMainWidthPlusShield() {
+        val mainWidth = BATTERY_WIDTH * 3.3f
+
+        val fullWidth = BatterySpecs.getFullBatteryWidth(mainWidth, displayShield = true)
+
+        // Since the main battery was scaled 3.3x, the output width should also be scaled 5x
+        val expectedFullWidth = BATTERY_WIDTH_WITH_SHIELD * 3.3f
+        assertThat(fullWidth).isWithin(.0001f).of(expectedFullWidth)
+    }
+
+    @Test
+    fun getMainBatteryHeight_shieldFalse_returnsFullHeight() {
+        val mainHeight = BatterySpecs.getMainBatteryHeight(89f, displayShield = false)
+
+        assertThat(mainHeight).isEqualTo(89f)
+    }
+
+    @Test
+    fun getMainBatteryHeight_shieldTrue_returnsNotFullHeight() {
+        val fullHeight = BATTERY_HEIGHT_WITH_SHIELD * 7.7f
+
+        val mainHeight = BatterySpecs.getMainBatteryHeight(fullHeight, displayShield = true)
+
+        // Since the full height was scaled 7.7x, the main height should also be scaled 7.7x.
+        val expectedHeight = BATTERY_HEIGHT * 7.7f
+        assertThat(mainHeight).isWithin(.0001f).of(expectedHeight)
+    }
+
+    @Test
+    fun getMainBatteryWidth_shieldFalse_returnsFullWidth() {
+        val mainWidth = BatterySpecs.getMainBatteryWidth(2345f, displayShield = false)
+
+        assertThat(mainWidth).isEqualTo(2345f)
+    }
+
+    @Test
+    fun getMainBatteryWidth_shieldTrue_returnsNotFullWidth() {
+        val fullWidth = BATTERY_WIDTH_WITH_SHIELD * 0.6f
+
+        val mainWidth = BatterySpecs.getMainBatteryWidth(fullWidth, displayShield = true)
+
+        // Since the full width was scaled 0.6x, the main height should also be scaled 0.6x.
+        val expectedWidth = BATTERY_WIDTH * 0.6f
+        assertThat(mainWidth).isWithin(.0001f).of(expectedWidth)
+    }
+}
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 f2ae7a1..898f370 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
@@ -54,6 +59,7 @@
 import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
@@ -79,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
@@ -110,6 +125,21 @@
     }
 
     @Test
+    fun testCredentialPasswordDismissesOnBack() {
+        val container = initializeCredentialPasswordContainer(addToView = true)
+        assertThat(container.parent).isNotNull()
+        val root = container.rootView
+
+        // Simulate back invocation
+        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
+        container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
+        waitForIdleSync()
+
+        assertThat(container.parent).isNull()
+        assertThat(root.isAttachedToWindow).isFalse()
+    }
+
+    @Test
     fun testIgnoresAnimatedInWhenDismissed() {
         val container = initializeFingerprintContainer(addToView = false)
         container.dismissFromSystemServer()
@@ -125,6 +155,21 @@
     }
 
     @Test
+    fun testDismissBeforeIntroEnd() {
+        val container = initializeFingerprintContainer()
+        waitForIdleSync()
+
+        // STATE_ANIMATING_IN = 1
+        container?.mContainerState = 1
+
+        container.dismissWithoutCallback(false)
+
+        // the first time is triggered by initializeFingerprintContainer()
+        // the second time was triggered by dismissWithoutCallback()
+        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+    }
+
+    @Test
     fun testDismissesOnFocusLoss() {
         val container = initializeFingerprintContainer()
         waitForIdleSync()
@@ -145,6 +190,25 @@
     }
 
     @Test
+    fun testFocusLossAfterRotating() {
+        val container = initializeFingerprintContainer()
+        waitForIdleSync()
+
+        val requestID = authContainer?.requestId ?: 0L
+
+        verify(callback).onDialogAnimatedIn(requestID)
+        container.onOrientationChanged()
+        container.onWindowFocusChanged(false)
+        waitForIdleSync()
+
+        verify(callback, never()).onDismissed(
+                eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
+                eq<ByteArray?>(null), /* credentialAttestation */
+                eq(requestID)
+        )
+    }
+
+    @Test
     fun testDismissesOnFocusLoss_hidesKeyboardWhenVisible() {
         val container = initializeFingerprintContainer(
             authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
@@ -320,20 +384,7 @@
 
     @Test
     fun testCredentialUI_disablesClickingOnBackground() {
-        whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
-        whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
-            DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
-        )
-
-        // In the credential view, clicking on the background (to cancel authentication) is not
-        // valid. Thus, the listener should be null, and it should not be in the accessibility
-        // hierarchy.
-        val container = initializeFingerprintContainer(
-            authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
-        )
-        waitForIdleSync()
-
-        assertThat(container.hasCredentialPasswordView()).isTrue()
+        val container = initializeCredentialPasswordContainer()
         assertThat(container.hasBiometricPrompt()).isFalse()
         assertThat(
             container.findViewById<View>(R.id.background)?.isImportantForAccessibility
@@ -393,6 +444,27 @@
         verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L)
     }
 
+    private fun initializeCredentialPasswordContainer(
+            addToView: Boolean = true,
+    ): TestAuthContainerView {
+        whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
+        whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
+            DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+        )
+
+        // In the credential view, clicking on the background (to cancel authentication) is not
+        // valid. Thus, the listener should be null, and it should not be in the accessibility
+        // hierarchy.
+        val container = initializeFingerprintContainer(
+                authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
+                addToView = addToView,
+        )
+        waitForIdleSync()
+
+        assertThat(container.hasCredentialPasswordView()).isTrue()
+        return container
+    }
+
     private fun initializeFingerprintContainer(
         authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
         addToView: Boolean = true
@@ -450,6 +522,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 8e45067..40b2cdf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -25,7 +25,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -87,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;
@@ -148,7 +152,7 @@
     @Mock
     private UdfpsController mUdfpsController;
     @Mock
-    private SidefpsController mSidefpsController;
+    private SideFpsController mSideFpsController;
     @Mock
     private DisplayManager mDisplayManager;
     @Mock
@@ -163,6 +167,11 @@
     private UdfpsLogger mUdfpsLogger;
     @Mock
     private InteractionJankMonitor mInteractionJankMonitor;
+    @Mock
+    private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+    @Mock
+    private CredentialViewModel mCredentialViewModel;
+
     @Captor
     private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
     @Captor
@@ -236,7 +245,7 @@
                         2 /* sensorId */,
                         SensorProperties.STRENGTH_STRONG,
                         1 /* maxEnrollmentsPerUser */,
-                        fpComponentInfo,
+                        faceComponentInfo,
                         FaceSensorProperties.TYPE_RGB,
                         true /* supportsFaceDetection */,
                         true /* supportsSelfIllumination */,
@@ -246,7 +255,7 @@
 
         mAuthController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue,
                 mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager,
-                () -> mUdfpsController, () -> mSidefpsController, mStatusBarStateController,
+                () -> mUdfpsController, () -> mSideFpsController, mStatusBarStateController,
                 mVibratorHelper);
 
         mAuthController.start();
@@ -276,12 +285,10 @@
         reset(mFingerprintManager);
         reset(mFaceManager);
 
-        when(mVibratorHelper.hasVibrator()).thenReturn(true);
-
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
                 mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
-                mFaceManager, () -> mUdfpsController, () -> mSidefpsController,
+                mFaceManager, () -> mUdfpsController, () -> mSideFpsController,
                 mStatusBarStateController, mVibratorHelper);
         authController.start();
 
@@ -308,12 +315,10 @@
         reset(mFingerprintManager);
         reset(mFaceManager);
 
-        when(mVibratorHelper.hasVibrator()).thenReturn(true);
-
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
                 mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
-                mFaceManager, () -> mUdfpsController, () -> mSidefpsController,
+                mFaceManager, () -> mUdfpsController, () -> mSideFpsController,
                 mStatusBarStateController, mVibratorHelper);
         authController.start();
 
@@ -343,6 +348,36 @@
     }
 
     @Test
+    public void testFaceAuthEnrollmentStatus() throws RemoteException {
+        final int userId = 0;
+
+        reset(mFaceManager);
+        mAuthController.start();
+
+        verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+                mFaceAuthenticatorsRegisteredCaptor.capture());
+
+        mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(
+                mFaceManager.getSensorPropertiesInternal());
+        mTestableLooper.processAllMessages();
+
+        verify(mFaceManager).registerBiometricStateListener(
+                mBiometricStateCaptor.capture());
+
+        assertFalse(mAuthController.isFaceAuthEnrolled(userId));
+
+        // Enrollments changed for an unknown sensor.
+        for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) {
+            listener.onEnrollmentsChanged(userId,
+                    2 /* sensorId */, true /* hasEnrollments */);
+        }
+        mTestableLooper.processAllMessages();
+
+        assertTrue(mAuthController.isFaceAuthEnrolled(userId));
+    }
+
+
+    @Test
     public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception {
         showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */);
         mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED,
@@ -974,13 +1009,14 @@
                 FingerprintManager fingerprintManager,
                 FaceManager faceManager,
                 Provider<UdfpsController> udfpsControllerFactory,
-                Provider<SidefpsController> sidefpsControllerFactory,
+                Provider<SideFpsController> sidefpsControllerFactory,
                 StatusBarStateController statusBarStateController,
                 VibratorHelper vibratorHelper) {
             super(context, execution, commandQueue, activityTaskManager, windowManager,
                     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/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
new file mode 100644
index 0000000..e7d5632
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -0,0 +1,472 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.biometrics
+
+import android.animation.Animator
+import android.app.ActivityManager
+import android.app.ActivityTaskManager
+import android.content.ComponentName
+import android.graphics.Insets
+import android.graphics.Rect
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.biometrics.SensorProperties
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManagerGlobal
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.hardware.fingerprint.ISidefpsController
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Display
+import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+import android.view.DisplayInfo
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.View
+import android.view.ViewPropertyAnimator
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowMetrics
+import androidx.test.filters.SmallTest
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.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.junit.MockitoJUnit
+
+private const val DISPLAY_ID = 2
+private const val SENSOR_ID = 1
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SideFpsControllerTest : SysuiTestCase() {
+
+    @JvmField @Rule var rule = MockitoJUnit.rule()
+
+    @Mock lateinit var layoutInflater: LayoutInflater
+    @Mock lateinit var fingerprintManager: FingerprintManager
+    @Mock lateinit var windowManager: WindowManager
+    @Mock lateinit var activityTaskManager: ActivityTaskManager
+    @Mock lateinit var sideFpsView: View
+    @Mock lateinit var displayManager: DisplayManager
+    @Mock lateinit var overviewProxyService: OverviewProxyService
+    @Mock lateinit var handler: Handler
+    @Mock lateinit var dumpManager: DumpManager
+    @Captor lateinit var overlayCaptor: ArgumentCaptor<View>
+    @Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
+
+    private val executor = FakeExecutor(FakeSystemClock())
+    private lateinit var overlayController: ISidefpsController
+    private lateinit var sideFpsController: SideFpsController
+
+    enum class DeviceConfig {
+        X_ALIGNED,
+        Y_ALIGNED_UNFOLDED,
+        Y_ALIGNED_FOLDED
+    }
+
+    private lateinit var deviceConfig: DeviceConfig
+    private lateinit var indicatorBounds: Rect
+    private lateinit var displayBounds: Rect
+    private lateinit var sensorLocation: SensorLocationInternal
+    private var displayWidth: Int = 0
+    private var displayHeight: Int = 0
+    private var boundsWidth: Int = 0
+    private var boundsHeight: Int = 0
+
+    @Before
+    fun setup() {
+        context.addMockSystemService(DisplayManager::class.java, displayManager)
+        context.addMockSystemService(WindowManager::class.java, windowManager)
+
+        whenEver(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
+        whenEver(sideFpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
+            .thenReturn(mock(LottieAnimationView::class.java))
+        with(mock(ViewPropertyAnimator::class.java)) {
+            whenEver(sideFpsView.animate()).thenReturn(this)
+            whenEver(alpha(anyFloat())).thenReturn(this)
+            whenEver(setStartDelay(anyLong())).thenReturn(this)
+            whenEver(setDuration(anyLong())).thenReturn(this)
+            whenEver(setListener(any())).thenAnswer {
+                (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
+                    mock(Animator::class.java)
+                )
+                this
+            }
+        }
+    }
+
+    private fun testWithDisplay(
+        deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
+        initInfo: DisplayInfo.() -> Unit = {},
+        windowInsets: WindowInsets = insetsForSmallNavbar(),
+        block: () -> Unit
+    ) {
+        this.deviceConfig = deviceConfig
+
+        when (deviceConfig) {
+            DeviceConfig.X_ALIGNED -> {
+                displayWidth = 2560
+                displayHeight = 1600
+                sensorLocation = SensorLocationInternal("", 2325, 0, 0)
+                boundsWidth = 160
+                boundsHeight = 84
+            }
+            DeviceConfig.Y_ALIGNED_UNFOLDED -> {
+                displayWidth = 2208
+                displayHeight = 1840
+                sensorLocation = SensorLocationInternal("", 0, 510, 0)
+                boundsWidth = 110
+                boundsHeight = 210
+            }
+            DeviceConfig.Y_ALIGNED_FOLDED -> {
+                displayWidth = 1080
+                displayHeight = 2100
+                sensorLocation = SensorLocationInternal("", 0, 590, 0)
+                boundsWidth = 110
+                boundsHeight = 210
+            }
+        }
+        indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
+        displayBounds = Rect(0, 0, displayWidth, displayHeight)
+        var locations = listOf(sensorLocation)
+
+        whenEver(fingerprintManager.sensorPropertiesInternal)
+            .thenReturn(
+                listOf(
+                    FingerprintSensorPropertiesInternal(
+                        SENSOR_ID,
+                        SensorProperties.STRENGTH_STRONG,
+                        5 /* maxEnrollmentsPerUser */,
+                        listOf() /* componentInfo */,
+                        FingerprintSensorProperties.TYPE_POWER_BUTTON,
+                        true /* halControlsIllumination */,
+                        true /* resetLockoutRequiresHardwareAuthToken */,
+                        locations
+                    )
+                )
+            )
+
+        val displayInfo = DisplayInfo()
+        displayInfo.initInfo()
+        val dmGlobal = mock(DisplayManagerGlobal::class.java)
+        val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS)
+        whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
+        whenEver(windowManager.defaultDisplay).thenReturn(display)
+        whenEver(windowManager.maximumWindowMetrics)
+            .thenReturn(WindowMetrics(displayBounds, WindowInsets.CONSUMED))
+        whenEver(windowManager.currentWindowMetrics)
+            .thenReturn(WindowMetrics(displayBounds, windowInsets))
+
+        sideFpsController =
+            SideFpsController(
+                context.createDisplayContext(display),
+                layoutInflater,
+                fingerprintManager,
+                windowManager,
+                activityTaskManager,
+                overviewProxyService,
+                displayManager,
+                executor,
+                handler,
+                dumpManager
+            )
+
+        overlayController =
+            ArgumentCaptor.forClass(ISidefpsController::class.java)
+                .apply { verify(fingerprintManager).setSidefpsController(capture()) }
+                .value
+
+        block()
+    }
+
+    @Test
+    fun testSubscribesToOrientationChangesWhenShowingOverlay() = testWithDisplay {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(displayManager).registerDisplayListener(any(), eq(handler), anyLong())
+
+        overlayController.hide(SENSOR_ID)
+        executor.runAllReady()
+        verify(displayManager).unregisterDisplayListener(any())
+    }
+
+    @Test
+    fun testShowsAndHides() = testWithDisplay {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(windowManager).addView(overlayCaptor.capture(), any())
+
+        reset(windowManager)
+        overlayController.hide(SENSOR_ID)
+        executor.runAllReady()
+
+        verify(windowManager, never()).addView(any(), any())
+        verify(windowManager).removeView(eq(overlayCaptor.value))
+    }
+
+    @Test
+    fun testShowsOnce() = testWithDisplay {
+        repeat(5) {
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+        }
+
+        verify(windowManager).addView(any(), any())
+        verify(windowManager, never()).removeView(any())
+    }
+
+    @Test
+    fun testHidesOnce() = testWithDisplay {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        repeat(5) {
+            overlayController.hide(SENSOR_ID)
+            executor.runAllReady()
+        }
+
+        verify(windowManager).addView(any(), any())
+        verify(windowManager).removeView(any())
+    }
+
+    @Test fun testIgnoredForKeyguard() = testWithDisplay { testIgnoredFor(REASON_AUTH_KEYGUARD) }
+
+    @Test
+    fun testShowsForMostSettings() = testWithDisplay {
+        whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpEnrollTask()))
+        testIgnoredFor(REASON_AUTH_SETTINGS, ignored = false)
+    }
+
+    @Test
+    fun testIgnoredForVerySpecificSettings() = testWithDisplay {
+        whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpSettingsTask()))
+        testIgnoredFor(REASON_AUTH_SETTINGS)
+    }
+
+    private fun testIgnoredFor(reason: Int, ignored: Boolean = true) {
+        overlayController.show(SENSOR_ID, reason)
+        executor.runAllReady()
+
+        verify(windowManager, if (ignored) never() else times(1)).addView(any(), any())
+    }
+
+    @Test
+    fun showsWithTaskbar() =
+        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_0 }) {
+            hidesWithTaskbar(visible = true)
+        }
+
+    @Test
+    fun showsWithTaskbarOnY() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            { rotation = Surface.ROTATION_0 }
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun showsWithTaskbar90() =
+        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED, { rotation = Surface.ROTATION_90 }) {
+            hidesWithTaskbar(visible = true)
+        }
+
+    @Test
+    fun showsWithTaskbar90OnY() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            { rotation = Surface.ROTATION_90 }
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun showsWithTaskbar180() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            { rotation = Surface.ROTATION_180 }
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun showsWithTaskbar270OnY() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            { rotation = Surface.ROTATION_270 }
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun showsWithTaskbarCollapsedDown() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            { rotation = Surface.ROTATION_270 },
+            windowInsets = insetsForSmallNavbar()
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun showsWithTaskbarCollapsedDownOnY() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForSmallNavbar()
+        ) { hidesWithTaskbar(visible = true) }
+
+    @Test
+    fun hidesWithTaskbarDown() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.X_ALIGNED,
+            { rotation = Surface.ROTATION_180 },
+            windowInsets = insetsForLargeNavbar()
+        ) { hidesWithTaskbar(visible = false) }
+
+    @Test
+    fun hidesWithTaskbarDownOnY() =
+        testWithDisplay(
+            deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
+            { rotation = Surface.ROTATION_270 },
+            windowInsets = insetsForLargeNavbar()
+        ) { hidesWithTaskbar(visible = true) }
+
+    private fun hidesWithTaskbar(visible: Boolean) {
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false)
+        executor.runAllReady()
+
+        verify(windowManager).addView(any(), any())
+        verify(windowManager, never()).removeView(any())
+        verify(sideFpsView).visibility = if (visible) View.VISIBLE else View.GONE
+    }
+
+    @Test
+    fun testIndicatorPlacementForXAlignedSensor() =
+        testWithDisplay(deviceConfig = DeviceConfig.X_ALIGNED) {
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+        }
+
+    @Test
+    fun testIndicatorPlacementForYAlignedSensor() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+            assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+            assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+        }
+
+    @Test
+    fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
+        // By default all those tests assume the side fps sensor is available.
+
+        assertThat(fingerprintManager.hasSideFpsSensor()).isTrue()
+    }
+
+    @Test
+    fun hasSideFpsSensor_withoutSensorProps_returnsFalse() {
+        whenEver(fingerprintManager.sensorPropertiesInternal).thenReturn(null)
+
+        assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
+    }
+
+    @Test
+    fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+            assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+        }
+
+    @Test
+    fun testLayoutParams_hasTrustedOverlayWindowFlag() =
+        testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED) {
+            sideFpsController.overlayOffsets = sensorLocation
+            sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+            executor.runAllReady()
+
+            verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+            val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+            assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+        }
+}
+
+private fun insetsForSmallNavbar() = insetsWithBottom(60)
+
+private fun insetsForLargeNavbar() = insetsWithBottom(100)
+
+private fun insetsWithBottom(bottom: Int) =
+    WindowInsets.Builder()
+        .setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, bottom))
+        .build()
+
+private fun fpEnrollTask() = settingsTask(".biometrics.fingerprint.FingerprintEnrollEnrolling")
+
+private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
+
+private fun settingsTask(cls: String) =
+    ActivityManager.RunningTaskInfo().apply {
+        topActivity = ComponentName.createRelative("com.android.settings", cls)
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
deleted file mode 100644
index 8d969d0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
+++ /dev/null
@@ -1,493 +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.
- */
-
-package com.android.systemui.biometrics
-
-import android.animation.Animator
-import android.app.ActivityManager
-import android.app.ActivityTaskManager
-import android.content.ComponentName
-import android.graphics.Insets
-import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
-import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
-import android.hardware.display.DisplayManager
-import android.hardware.display.DisplayManagerGlobal
-import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorProperties
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
-import android.hardware.fingerprint.ISidefpsController
-import android.os.Handler
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.Display
-import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
-import android.view.DisplayInfo
-import android.view.LayoutInflater
-import android.view.Surface
-import android.view.View
-import android.view.ViewPropertyAnimator
-import android.view.WindowInsets
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
-import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
-import android.view.WindowMetrics
-import androidx.test.filters.SmallTest
-import com.airbnb.lottie.LottieAnimationView
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.recents.OverviewProxyService
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyFloat
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.anyLong
-import org.mockito.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.junit.MockitoJUnit
-
-private const val DISPLAY_ID = 2
-private const val SENSOR_ID = 1
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class SidefpsControllerTest : SysuiTestCase() {
-
-    @JvmField @Rule
-    var rule = MockitoJUnit.rule()
-
-    @Mock
-    lateinit var layoutInflater: LayoutInflater
-    @Mock
-    lateinit var fingerprintManager: FingerprintManager
-    @Mock
-    lateinit var windowManager: WindowManager
-    @Mock
-    lateinit var activityTaskManager: ActivityTaskManager
-    @Mock
-    lateinit var sidefpsView: View
-    @Mock
-    lateinit var displayManager: DisplayManager
-    @Mock
-    lateinit var overviewProxyService: OverviewProxyService
-    @Mock
-    lateinit var handler: Handler
-    @Captor
-    lateinit var overlayCaptor: ArgumentCaptor<View>
-    @Captor
-    lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
-
-    private val executor = FakeExecutor(FakeSystemClock())
-    private lateinit var overlayController: ISidefpsController
-    private lateinit var sideFpsController: SidefpsController
-
-    enum class DeviceConfig { X_ALIGNED, Y_ALIGNED_UNFOLDED, Y_ALIGNED_FOLDED }
-
-    private lateinit var deviceConfig: DeviceConfig
-    private lateinit var indicatorBounds: Rect
-    private lateinit var displayBounds: Rect
-    private lateinit var sensorLocation: SensorLocationInternal
-    private var displayWidth: Int = 0
-    private var displayHeight: Int = 0
-    private var boundsWidth: Int = 0
-    private var boundsHeight: Int = 0
-
-    @Before
-    fun setup() {
-        context.addMockSystemService(DisplayManager::class.java, displayManager)
-        context.addMockSystemService(WindowManager::class.java, windowManager)
-
-        whenEver(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sidefpsView)
-        whenEver(sidefpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
-            .thenReturn(mock(LottieAnimationView::class.java))
-        with(mock(ViewPropertyAnimator::class.java)) {
-            whenEver(sidefpsView.animate()).thenReturn(this)
-            whenEver(alpha(anyFloat())).thenReturn(this)
-            whenEver(setStartDelay(anyLong())).thenReturn(this)
-            whenEver(setDuration(anyLong())).thenReturn(this)
-            whenEver(setListener(any())).thenAnswer {
-                (it.arguments[0] as Animator.AnimatorListener)
-                    .onAnimationEnd(mock(Animator::class.java))
-                this
-            }
-        }
-    }
-
-    private fun testWithDisplay(
-        deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
-        initInfo: DisplayInfo.() -> Unit = {},
-        windowInsets: WindowInsets = insetsForSmallNavbar(),
-        block: () -> Unit
-    ) {
-        this.deviceConfig = deviceConfig
-
-        when (deviceConfig) {
-            DeviceConfig.X_ALIGNED -> {
-                displayWidth = 2560
-                displayHeight = 1600
-                sensorLocation = SensorLocationInternal("", 2325, 0, 0)
-                boundsWidth = 160
-                boundsHeight = 84
-            }
-            DeviceConfig.Y_ALIGNED_UNFOLDED -> {
-                displayWidth = 2208
-                displayHeight = 1840
-                sensorLocation = SensorLocationInternal("", 0, 510, 0)
-                boundsWidth = 110
-                boundsHeight = 210
-            }
-            DeviceConfig.Y_ALIGNED_FOLDED -> {
-                displayWidth = 1080
-                displayHeight = 2100
-                sensorLocation = SensorLocationInternal("", 0, 590, 0)
-                boundsWidth = 110
-                boundsHeight = 210
-            }
-        }
-        indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
-        displayBounds = Rect(0, 0, displayWidth, displayHeight)
-        var locations = listOf(sensorLocation)
-
-        whenEver(fingerprintManager.sensorPropertiesInternal).thenReturn(
-            listOf(
-                FingerprintSensorPropertiesInternal(
-                    SENSOR_ID,
-                    SensorProperties.STRENGTH_STRONG,
-                    5 /* maxEnrollmentsPerUser */,
-                    listOf() /* componentInfo */,
-                    FingerprintSensorProperties.TYPE_POWER_BUTTON,
-                    true /* halControlsIllumination */,
-                    true /* resetLockoutRequiresHardwareAuthToken */,
-                    locations
-                )
-            )
-        )
-
-        val displayInfo = DisplayInfo()
-        displayInfo.initInfo()
-        val dmGlobal = mock(DisplayManagerGlobal::class.java)
-        val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS)
-        whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
-        whenEver(windowManager.defaultDisplay).thenReturn(display)
-        whenEver(windowManager.maximumWindowMetrics).thenReturn(
-                WindowMetrics(displayBounds, WindowInsets.CONSUMED)
-        )
-        whenEver(windowManager.currentWindowMetrics).thenReturn(
-            WindowMetrics(displayBounds, windowInsets)
-        )
-
-        sideFpsController = SidefpsController(
-            context.createDisplayContext(display), layoutInflater, fingerprintManager,
-            windowManager, activityTaskManager, overviewProxyService, displayManager, executor,
-            handler
-        )
-
-        overlayController = ArgumentCaptor.forClass(ISidefpsController::class.java).apply {
-            verify(fingerprintManager).setSidefpsController(capture())
-        }.value
-
-        block()
-    }
-
-    @Test
-    fun testSubscribesToOrientationChangesWhenShowingOverlay() = testWithDisplay {
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        verify(displayManager).registerDisplayListener(any(), eq(handler), anyLong())
-
-        overlayController.hide(SENSOR_ID)
-        executor.runAllReady()
-        verify(displayManager).unregisterDisplayListener(any())
-    }
-
-    @Test
-    fun testShowsAndHides() = testWithDisplay {
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        verify(windowManager).addView(overlayCaptor.capture(), any())
-
-        reset(windowManager)
-        overlayController.hide(SENSOR_ID)
-        executor.runAllReady()
-
-        verify(windowManager, never()).addView(any(), any())
-        verify(windowManager).removeView(eq(overlayCaptor.value))
-    }
-
-    @Test
-    fun testShowsOnce() = testWithDisplay {
-        repeat(5) {
-            overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-            executor.runAllReady()
-        }
-
-        verify(windowManager).addView(any(), any())
-        verify(windowManager, never()).removeView(any())
-    }
-
-    @Test
-    fun testHidesOnce() = testWithDisplay {
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        repeat(5) {
-            overlayController.hide(SENSOR_ID)
-            executor.runAllReady()
-        }
-
-        verify(windowManager).addView(any(), any())
-        verify(windowManager).removeView(any())
-    }
-
-    @Test
-    fun testIgnoredForKeyguard() = testWithDisplay {
-        testIgnoredFor(REASON_AUTH_KEYGUARD)
-    }
-
-    @Test
-    fun testShowsForMostSettings() = testWithDisplay {
-        whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpEnrollTask()))
-        testIgnoredFor(REASON_AUTH_SETTINGS, ignored = false)
-    }
-
-    @Test
-    fun testIgnoredForVerySpecificSettings() = testWithDisplay {
-        whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpSettingsTask()))
-        testIgnoredFor(REASON_AUTH_SETTINGS)
-    }
-
-    private fun testIgnoredFor(reason: Int, ignored: Boolean = true) {
-        overlayController.show(SENSOR_ID, reason)
-        executor.runAllReady()
-
-        verify(windowManager, if (ignored) never() else times(1)).addView(any(), any())
-    }
-
-    @Test
-    fun showsWithTaskbar() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED,
-        { rotation = Surface.ROTATION_0 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbarOnY() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-        { rotation = Surface.ROTATION_0 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbar90() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED,
-        { rotation = Surface.ROTATION_90 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbar90OnY() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-        { rotation = Surface.ROTATION_90 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbar180() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED,
-        { rotation = Surface.ROTATION_180 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbar270OnY() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-        { rotation = Surface.ROTATION_270 }
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbarCollapsedDown() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED,
-        { rotation = Surface.ROTATION_270 },
-        windowInsets = insetsForSmallNavbar()
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun showsWithTaskbarCollapsedDownOnY() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-        { rotation = Surface.ROTATION_180 },
-        windowInsets = insetsForSmallNavbar()
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    @Test
-    fun hidesWithTaskbarDown() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED,
-        { rotation = Surface.ROTATION_180 },
-        windowInsets = insetsForLargeNavbar()
-    ) {
-        hidesWithTaskbar(visible = false)
-    }
-
-    @Test
-    fun hidesWithTaskbarDownOnY() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED,
-        { rotation = Surface.ROTATION_270 },
-        windowInsets = insetsForLargeNavbar()
-    ) {
-        hidesWithTaskbar(visible = true)
-    }
-
-    private fun hidesWithTaskbar(visible: Boolean) {
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false)
-        executor.runAllReady()
-
-        verify(windowManager).addView(any(), any())
-        verify(windowManager, never()).removeView(any())
-        verify(sidefpsView).visibility = if (visible) View.VISIBLE else View.GONE
-    }
-
-    @Test
-    fun testIndicatorPlacementForXAlignedSensor() = testWithDisplay(
-        deviceConfig = DeviceConfig.X_ALIGNED
-    ) {
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        sideFpsController.overlayOffsets = sensorLocation
-        sideFpsController.updateOverlayParams(
-            windowManager.defaultDisplay,
-            indicatorBounds
-        )
-        executor.runAllReady()
-
-        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-
-        assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
-        assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
-    }
-
-    @Test
-    fun testIndicatorPlacementForYAlignedSensor() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED
-    ) {
-        sideFpsController.overlayOffsets = sensorLocation
-        sideFpsController.updateOverlayParams(
-            windowManager.defaultDisplay,
-            indicatorBounds
-        )
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-        assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
-        assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
-    }
-
-    @Test
-    fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
-        // By default all those tests assume the side fps sensor is available.
-
-        assertThat(fingerprintManager.hasSideFpsSensor()).isTrue()
-    }
-
-    @Test
-    fun hasSideFpsSensor_withoutSensorProps_returnsFalse() {
-        whenEver(fingerprintManager.sensorPropertiesInternal).thenReturn(null)
-
-        assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
-    }
-
-    @Test
-    fun testLayoutParams_hasNoMoveAnimationWindowFlag() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED
-    ) {
-        sideFpsController.overlayOffsets = sensorLocation
-        sideFpsController.updateOverlayParams(
-            windowManager.defaultDisplay,
-            indicatorBounds
-        )
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-
-        val lpFlags = overlayViewParamsCaptor.value.privateFlags
-
-        assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
-    }
-
-    @Test
-    fun testLayoutParams_hasTrustedOverlayWindowFlag() = testWithDisplay(
-        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED
-    ) {
-        sideFpsController.overlayOffsets = sensorLocation
-        sideFpsController.updateOverlayParams(
-            windowManager.defaultDisplay,
-            indicatorBounds
-        )
-        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
-        executor.runAllReady()
-
-        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
-
-        val lpFlags = overlayViewParamsCaptor.value.privateFlags
-
-        assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
-    }
-}
-
-private fun insetsForSmallNavbar() = insetsWithBottom(60)
-private fun insetsForLargeNavbar() = insetsWithBottom(100)
-private fun insetsWithBottom(bottom: Int) = WindowInsets.Builder()
-    .setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, bottom))
-    .build()
-
-private fun fpEnrollTask() = settingsTask(".biometrics.fingerprint.FingerprintEnrollEnrolling")
-private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
-private fun settingsTask(cls: String) = ActivityManager.RunningTaskInfo().apply {
-    topActivity = ComponentName.createRelative("com.android.settings", cls)
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index baeabc5..4b459c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -26,6 +26,7 @@
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.LayoutInflater
@@ -41,6 +42,8 @@
 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.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -102,6 +105,8 @@
     @Mock private lateinit var udfpsView: UdfpsView
     @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
+    @Mock private lateinit var featureFlags: FeatureFlags
+    @Mock private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -124,14 +129,19 @@
         whenever(udfpsEnrollView.context).thenReturn(context)
     }
 
-    private fun withReason(@ShowReason reason: Int, block: () -> Unit) {
+    private fun withReason(
+        @ShowReason reason: Int,
+        isDebuggable: Boolean = false,
+        block: () -> Unit
+    ) {
         controllerOverlay = UdfpsControllerOverlay(
             context, fingerprintManager, inflater, windowManager, accessibilityManager,
             statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator
+            controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
+            mPrimaryBouncerInteractor, isDebuggable
         )
         block()
     }
@@ -151,11 +161,29 @@
     }
 
     @Test
+    fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) {
         showUdfpsOverlay(isEnrollUseCase = true)
     }
 
     @Test
+    fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() }
 
     private fun withRotation(@Rotation rotation: Int, block: () -> Unit) {
@@ -163,6 +191,7 @@
         val sensorBounds = Rect(0, 0, SENSOR_WIDTH, SENSOR_HEIGHT)
         overlayParams = UdfpsOverlayParams(
             sensorBounds,
+            sensorBounds,
             DISPLAY_WIDTH,
             DISPLAY_HEIGHT,
             scaleFactor = 1f,
@@ -372,21 +401,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_0
         // touch at 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
     }
 
     fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -394,21 +435,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_90
         // touch at 0 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 90 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 180 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 270 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
     }
 
     fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -416,21 +469,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_270
         // touch at 0 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 90 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 180 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 270 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index f210708..acdafe3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -20,6 +20,8 @@
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.internal.util.FunctionalUtils.ThrowingConsumer;
+
 import static junit.framework.Assert.assertEquals;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -69,7 +71,9 @@
 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.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -106,20 +110,14 @@
 @RunWithLooper(setAsMainLooper = true)
 public class UdfpsControllerTest extends SysuiTestCase {
 
-    // Use this for inputs going into SystemUI. Use UdfpsController.mUdfpsSensorId for things
-    // leaving SystemUI.
-    private static final int TEST_UDFPS_SENSOR_ID = 1;
     private static final long TEST_REQUEST_ID = 70;
 
     @Rule
     public MockitoRule rule = MockitoJUnit.rule();
-
     // Unit under test
     private UdfpsController mUdfpsController;
-
     // Dependencies
     private FakeExecutor mBiometricsExecutor;
-    private Execution mExecution;
     @Mock
     private LayoutInflater mLayoutInflater;
     @Mock
@@ -169,7 +167,8 @@
     private FakeExecutor mFgExecutor;
     @Mock
     private UdfpsDisplayMode mUdfpsDisplayMode;
-
+    @Mock
+    private FeatureFlags mFeatureFlags;
     // Stuff for configuring mocks
     @Mock
     private UdfpsView mUdfpsView;
@@ -189,19 +188,28 @@
     private ActivityLaunchAnimator mActivityLaunchAnimator;
     @Mock
     private AlternateUdfpsTouchProvider mAlternateTouchProvider;
+    @Mock
+    private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
 
     // Capture listeners so that they can be used to send events
-    @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor;
+    @Captor
+    private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor;
     private IUdfpsOverlayController mOverlayController;
-    @Captor private ArgumentCaptor<UdfpsView.OnTouchListener> mTouchListenerCaptor;
-    @Captor private ArgumentCaptor<View.OnHoverListener> mHoverListenerCaptor;
-    @Captor private ArgumentCaptor<Runnable> mOnDisplayConfiguredCaptor;
-    @Captor private ArgumentCaptor<ScreenLifecycle.Observer> mScreenObserverCaptor;
+    @Captor
+    private ArgumentCaptor<UdfpsView.OnTouchListener> mTouchListenerCaptor;
+    @Captor
+    private ArgumentCaptor<View.OnHoverListener> mHoverListenerCaptor;
+    @Captor
+    private ArgumentCaptor<Runnable> mOnDisplayConfiguredCaptor;
+    @Captor
+    private ArgumentCaptor<ScreenLifecycle.Observer> mScreenObserverCaptor;
     private ScreenLifecycle.Observer mScreenObserver;
+    private FingerprintSensorPropertiesInternal mOpticalProps;
+    private FingerprintSensorPropertiesInternal mUltrasonicProps;
 
     @Before
     public void setUp() {
-        mExecution = new FakeExecution();
+        Execution execution = new FakeExecution();
 
         when(mLayoutInflater.inflate(R.layout.udfps_view, null, false))
                 .thenReturn(mUdfpsView);
@@ -214,9 +222,7 @@
         when(mLayoutInflater.inflate(R.layout.udfps_fpm_other_view, null))
                 .thenReturn(mFpmOtherView);
         when(mEnrollView.getContext()).thenReturn(mContext);
-        when(mKeyguardStateController.isOccluded()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
-        final List<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
 
         final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
         componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */,
@@ -226,60 +232,62 @@
                 "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
                 "vendor/version/revision" /* softwareVersion */));
 
-        props.add(new FingerprintSensorPropertiesInternal(TEST_UDFPS_SENSOR_ID,
+        mOpticalProps = new FingerprintSensorPropertiesInternal(1 /* sensorId */,
                 SensorProperties.STRENGTH_STRONG,
                 5 /* maxEnrollmentsPerUser */,
                 componentInfo,
                 FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                true /* resetLockoutRequiresHardwareAuthToken */));
-        when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
+                true /* resetLockoutRequiresHardwareAuthToken */);
+
+        mUltrasonicProps = new FingerprintSensorPropertiesInternal(2 /* sensorId */,
+                SensorProperties.STRENGTH_STRONG,
+                5 /* maxEnrollmentsPerUser */,
+                componentInfo,
+                FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
+                true /* resetLockoutRequiresHardwareAuthToken */);
+
         mFgExecutor = new FakeExecutor(new FakeSystemClock());
 
         // Create a fake background executor.
         mBiometricsExecutor = new FakeExecutor(new FakeSystemClock());
 
-        mUdfpsController = new UdfpsController(
-                mContext,
-                mExecution,
-                mLayoutInflater,
-                mFingerprintManager,
-                mWindowManager,
-                mStatusBarStateController,
-                mFgExecutor,
-                new ShadeExpansionStateManager(),
-                mStatusBarKeyguardViewManager,
-                mDumpManager,
-                mKeyguardUpdateMonitor,
-                mFalsingManager,
-                mPowerManager,
-                mAccessibilityManager,
-                mLockscreenShadeTransitionController,
-                mScreenLifecycle,
-                mVibrator,
-                mUdfpsHapticsSimulator,
-                mUdfpsShell,
-                mKeyguardStateController,
-                mDisplayManager,
-                mHandler,
-                mConfigurationController,
-                mSystemClock,
-                mUnlockedScreenOffAnimationController,
-                mSystemUIDialogManager,
-                mLatencyTracker,
-                mActivityLaunchAnimator,
-                Optional.of(mAlternateTouchProvider),
-                mBiometricsExecutor);
+        initUdfpsController(true /* hasAlternateTouchProvider */);
+    }
+
+    private void initUdfpsController(boolean hasAlternateTouchProvider) {
+        initUdfpsController(mOpticalProps, hasAlternateTouchProvider);
+    }
+
+    private void initUdfpsController(FingerprintSensorPropertiesInternal sensorProps,
+            boolean hasAlternateTouchProvider) {
+        reset(mFingerprintManager);
+        reset(mScreenLifecycle);
+
+        final Optional<AlternateUdfpsTouchProvider> alternateTouchProvider =
+                hasAlternateTouchProvider ? Optional.of(mAlternateTouchProvider) : Optional.empty();
+
+        mUdfpsController = new UdfpsController(mContext, new FakeExecution(), mLayoutInflater,
+                mFingerprintManager, mWindowManager, mStatusBarStateController, mFgExecutor,
+                new ShadeExpansionStateManager(), mStatusBarKeyguardViewManager, mDumpManager,
+                mKeyguardUpdateMonitor, mFeatureFlags, mFalsingManager, mPowerManager,
+                mAccessibilityManager, mLockscreenShadeTransitionController, mScreenLifecycle,
+                mVibrator, mUdfpsHapticsSimulator, mUdfpsShell, mKeyguardStateController,
+                mDisplayManager, mHandler, mConfigurationController, mSystemClock,
+                mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
+                mActivityLaunchAnimator, alternateTouchProvider, mBiometricsExecutor,
+                mPrimaryBouncerInteractor);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
         mScreenObserver = mScreenObserverCaptor.getValue();
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams());
+
+        mUdfpsController.updateOverlayParams(sensorProps, new UdfpsOverlayParams());
         mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode);
     }
 
     @Test
     public void dozeTimeTick() throws RemoteException {
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         mUdfpsController.dozeTimeTick();
@@ -294,7 +302,7 @@
         when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController);
 
         // GIVEN that the overlay is showing
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
@@ -310,8 +318,7 @@
     }
 
     @Test
-    public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice()
-            throws RemoteException {
+    public void onActionMoveTouch_whenCanDismissLockScreen_entersDevice() throws RemoteException {
         onActionMoveTouch_whenCanDismissLockScreen_entersDevice(false /* stale */);
     }
 
@@ -329,7 +336,7 @@
         when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController);
 
         // GIVEN that the overlay is showing
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
@@ -337,7 +344,7 @@
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
         MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
         if (stale) {
-            mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
+            mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId);
             mFgExecutor.runAllReady();
         }
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
@@ -357,7 +364,7 @@
         when(mUdfpsView.getAnimationViewController()).thenReturn(mUdfpsKeyguardViewController);
 
         // GIVEN that the overlay is showing
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
@@ -380,27 +387,27 @@
     @Test
     public void hideUdfpsOverlay_resetsAltAuthBouncerWhenShowing() throws RemoteException {
         // GIVEN overlay was showing and the udfps bouncer is showing
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
 
         // WHEN the overlay is hidden
-        mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
+        mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId);
         mFgExecutor.runAllReady();
 
         // THEN the udfps bouncer is reset
-        verify(mStatusBarKeyguardViewManager).resetAlternateAuth(eq(true));
+        verify(mStatusBarKeyguardViewManager).hideAlternateBouncer(eq(true));
     }
 
     @Test
     public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception {
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
         verify(mDisplayManager).registerDisplayListener(any(), eq(mHandler), anyLong());
 
-        mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
+        mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId);
         mFgExecutor.runAllReady();
 
         verify(mDisplayManager).unregisterDisplayListener(any());
@@ -414,7 +421,7 @@
         final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]};
         final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90};
         final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0],
-                displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
+                sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
 
         for (int i1 = 0; i1 <= 1; ++i1) {
             for (int i2 = 0; i2 <= 1; ++i2) {
@@ -422,20 +429,20 @@
                     for (int i4 = 0; i4 <= 1; ++i4) {
                         for (int i5 = 0; i5 <= 1; ++i5) {
                             final UdfpsOverlayParams newParams = new UdfpsOverlayParams(
-                                    sensorBounds[i1], displayWidth[i2], displayHeight[i3],
-                                    scaleFactor[i4], rotation[i5]);
+                                    sensorBounds[i1], sensorBounds[i1], displayWidth[i2],
+                                    displayHeight[i3], scaleFactor[i4], rotation[i5]);
 
                             if (newParams.equals(oldParams)) {
                                 continue;
                             }
 
                             // Initialize the overlay with old parameters.
-                            mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, oldParams);
+                            mUdfpsController.updateOverlayParams(mOpticalProps, oldParams);
 
                             // Show the overlay.
                             reset(mWindowManager);
                             mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID,
-                                    TEST_UDFPS_SENSOR_ID,
+                                    mOpticalProps.sensorId,
                                     BiometricOverlayConstants.REASON_ENROLL_ENROLLING,
                                     mUdfpsOverlayControllerCallback);
                             mFgExecutor.runAllReady();
@@ -443,7 +450,7 @@
 
                             // Update overlay parameters.
                             reset(mWindowManager);
-                            mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, newParams);
+                            mUdfpsController.updateOverlayParams(mOpticalProps, newParams);
                             mFgExecutor.runAllReady();
 
                             // Ensure the overlay was recreated.
@@ -465,20 +472,20 @@
         final int rotation = Surface.ROTATION_0;
 
         // Initialize the overlay.
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+        mUdfpsController.updateOverlayParams(mOpticalProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
 
         // Show the overlay.
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mWindowManager).addView(any(), any());
 
         // Update overlay with the same parameters.
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+        mUdfpsController.updateOverlayParams(mOpticalProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
         mFgExecutor.runAllReady();
 
         // Ensure the overlay was not recreated.
@@ -498,8 +505,37 @@
                 new MotionEvent.PointerCoords[]{pc}, 0, 0, 1f, 1f, 0, 0, 0, 0);
     }
 
+    private static class TestParams {
+        public final FingerprintSensorPropertiesInternal sensorProps;
+        public final boolean hasAlternateTouchProvider;
+
+        TestParams(FingerprintSensorPropertiesInternal sensorProps,
+                boolean hasAlternateTouchProvider) {
+            this.sensorProps = sensorProps;
+            this.hasAlternateTouchProvider = hasAlternateTouchProvider;
+        }
+    }
+
+    private void runWithAllParams(ThrowingConsumer<TestParams> testParamsConsumer) {
+        for (FingerprintSensorPropertiesInternal sensorProps : List.of(mOpticalProps,
+                mUltrasonicProps)) {
+            for (boolean hasAlternateTouchProvider : new boolean[]{false, true}) {
+                initUdfpsController(sensorProps, hasAlternateTouchProvider);
+                testParamsConsumer.accept(new TestParams(sensorProps, hasAlternateTouchProvider));
+            }
+        }
+    }
+
     @Test
-    public void onTouch_propagatesTouchInNativeOrientationAndResolution() throws RemoteException {
+    public void onTouch_propagatesTouchInNativeOrientationAndResolution() {
+        runWithAllParams(
+                this::onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized);
+    }
+
+    private void onTouch_propagatesTouchInNativeOrientationAndResolutionParameterized(
+            TestParams testParams) throws RemoteException {
+        reset(mUdfpsView);
+
         final Rect sensorBounds = new Rect(1000, 1900, 1080, 1920); // Bottom right corner.
         final int displayWidth = 1080;
         final int displayHeight = 1920;
@@ -518,15 +554,15 @@
         when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
 
         // Show the overlay.
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
 
         // Test ROTATION_0
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_0));
+        mUdfpsController.updateOverlayParams(testParams.sensorProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_0));
         MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor,
                 touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -536,14 +572,21 @@
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
         event.recycle();
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
-                eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
+                    eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY),
+                    eq(expectedMinor), eq(expectedMajor));
+        }
 
         // Test ROTATION_90
         reset(mAlternateTouchProvider);
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_90));
+        reset(mFingerprintManager);
+        mUdfpsController.updateOverlayParams(testParams.sensorProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_90));
         event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -552,14 +595,21 @@
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
         event.recycle();
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
-                eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
+                    eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY),
+                    eq(expectedMinor), eq(expectedMajor));
+        }
 
         // Test ROTATION_270
         reset(mAlternateTouchProvider);
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_270));
+        reset(mFingerprintManager);
+        mUdfpsController.updateOverlayParams(testParams.sensorProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_270));
         event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -568,14 +618,21 @@
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
         event.recycle();
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
-                eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
+                    eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY),
+                    eq(expectedMinor), eq(expectedMajor));
+        }
 
         // Test ROTATION_180
         reset(mAlternateTouchProvider);
-        mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_180));
+        reset(mFingerprintManager);
+        mUdfpsController.updateOverlayParams(testParams.sensorProps,
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_180));
         // ROTATION_180 is not supported. It should be treated like ROTATION_0.
         event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -585,124 +642,228 @@
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
         event.recycle();
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
-                eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
+                    eq(expectedY), eq(expectedMinor), eq(expectedMajor));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(expectedX), eq(expectedY),
+                    eq(expectedMinor), eq(expectedMajor));
+        }
     }
 
     @Test
-    public void fingerDown() throws RemoteException {
+    public void fingerDown() {
+        runWithAllParams(this::fingerDownParameterized);
+    }
+
+    private void fingerDownParameterized(TestParams testParams) throws RemoteException {
+        reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mLatencyTracker,
+                mKeyguardUpdateMonitor);
+
         // Configure UdfpsView to accept the ACTION_DOWN event
         when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
         when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
         when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
 
         // GIVEN that the overlay is showing
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
-        // WHEN ACTION_DOWN is received
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
+
+        // WHEN ACTION_DOWN is received
         MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
         mBiometricsExecutor.runAllReady();
         downEvent.recycle();
-        MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
 
-        // FIX THIS TEST
+        MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
         mBiometricsExecutor.runAllReady();
         moveEvent.recycle();
+
         mFgExecutor.runAllReady();
-        // THEN FingerprintManager is notified about onPointerDown
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f),
-                eq(0f));
-        verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(),
-                anyFloat(), anyFloat());
-        verify(mLatencyTracker).onActionStart(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+
+        // THEN the touch provider is notified about onPointerDown.
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f),
+                    eq(0f));
+            verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(),
+                    anyInt(), anyFloat(), anyFloat());
+            verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(0f), eq(0f));
+            verify(mAlternateTouchProvider, never()).onPointerDown(anyInt(), anyInt(), anyInt(),
+                    anyFloat(), anyFloat());
+        }
+
         // AND display configuration begins
-        verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture());
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            verify(mLatencyTracker).onActionStart(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+            verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture());
+        } else {
+            verify(mLatencyTracker, never()).onActionStart(
+                    eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+            verify(mUdfpsView, never()).configureDisplay(any());
+        }
         verify(mLatencyTracker, never()).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
-        verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID));
-        // AND onDisplayConfigured notifies FingerprintManager about onUiReady
-        mOnDisplayConfiguredCaptor.getValue().run();
-        mBiometricsExecutor.runAllReady();
-        InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker);
-        inOrder.verify(mAlternateTouchProvider).onUiReady();
-        inOrder.verify(mLatencyTracker).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // AND onDisplayConfigured notifies FingerprintManager about onUiReady
+            mOnDisplayConfiguredCaptor.getValue().run();
+            mBiometricsExecutor.runAllReady();
+            if (testParams.hasAlternateTouchProvider) {
+                InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker);
+                inOrder.verify(mAlternateTouchProvider).onUiReady();
+                inOrder.verify(mLatencyTracker).onActionEnd(
+                        eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+                verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt());
+            } else {
+                InOrder inOrder = inOrder(mFingerprintManager, mLatencyTracker);
+                inOrder.verify(mFingerprintManager).onUiReady(eq(TEST_REQUEST_ID),
+                        eq(testParams.sensorProps.sensorId));
+                inOrder.verify(mLatencyTracker).onActionEnd(
+                        eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+                verify(mAlternateTouchProvider, never()).onUiReady();
+            }
+        } else {
+            verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt());
+            verify(mAlternateTouchProvider, never()).onUiReady();
+            verify(mLatencyTracker, never()).onActionEnd(
+                    eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
+        }
     }
 
     @Test
-    public void aodInterrupt() throws RemoteException {
+    public void aodInterrupt() {
+        runWithAllParams(this::aodInterruptParameterized);
+    }
+
+    private void aodInterruptParameterized(TestParams testParams) throws RemoteException {
+        mUdfpsController.cancelAodInterrupt();
+        reset(mUdfpsView, mAlternateTouchProvider, mFingerprintManager, mKeyguardUpdateMonitor);
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
+
         // GIVEN that the overlay is showing and screen is on and fp is running
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         // WHEN fingerprint is requested because of AOD interrupt
         mUdfpsController.onAodInterrupt(0, 0, 2f, 3f);
         mFgExecutor.runAllReady();
-        // THEN display configuration begins
-        // AND onDisplayConfigured notifies FingerprintManager about onUiReady
-        verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture());
-        mOnDisplayConfiguredCaptor.getValue().run();
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // THEN display configuration begins
+            // AND onDisplayConfigured notifies FingerprintManager about onUiReady
+            verify(mUdfpsView).configureDisplay(mOnDisplayConfiguredCaptor.capture());
+            mOnDisplayConfiguredCaptor.getValue().run();
+        } else {
+            verify(mUdfpsView, never()).configureDisplay(mOnDisplayConfiguredCaptor.capture());
+        }
         mBiometricsExecutor.runAllReady();
-        verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID),
-                eq(0), eq(0), eq(3f) /* minor */, eq(2f) /* major */);
-        verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(),
-                anyFloat(), anyFloat());
-        verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID));
+
+        if (testParams.hasAlternateTouchProvider) {
+            verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0),
+                    eq(3f) /* minor */, eq(2f) /* major */);
+            verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(),
+                    anyInt(), anyFloat(), anyFloat());
+            verify(mKeyguardUpdateMonitor).onUdfpsPointerDown(eq((int) TEST_REQUEST_ID));
+        } else {
+            verify(mFingerprintManager).onPointerDown(eq(TEST_REQUEST_ID),
+                    eq(testParams.sensorProps.sensorId), eq(0), eq(0), eq(3f) /* minor */,
+                    eq(2f) /* major */);
+            verify(mAlternateTouchProvider, never()).onPointerDown(anyLong(), anyInt(), anyInt(),
+                    anyFloat(), anyFloat());
+        }
     }
 
     @Test
-    public void cancelAodInterrupt() throws RemoteException {
+    public void cancelAodInterrupt() {
+        runWithAllParams(this::cancelAodInterruptParameterized);
+    }
+
+    private void cancelAodInterruptParameterized(TestParams testParams) throws RemoteException {
+        reset(mUdfpsView);
+
         // GIVEN AOD interrupt
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
-        // WHEN it is cancelled
-        mUdfpsController.onCancelUdfps();
-        // THEN the display is unconfigured
-        verify(mUdfpsView).unconfigureDisplay();
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+            // WHEN it is cancelled
+            mUdfpsController.cancelAodInterrupt();
+            // THEN the display is unconfigured
+            verify(mUdfpsView).unconfigureDisplay();
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+            // WHEN it is cancelled
+            mUdfpsController.cancelAodInterrupt();
+            // THEN the display configuration is unchanged.
+            verify(mUdfpsView, never()).unconfigureDisplay();
+        }
     }
 
     @Test
-    public void aodInterruptTimeout() throws RemoteException {
+    public void aodInterruptTimeout() {
+        runWithAllParams(this::aodInterruptTimeoutParameterized);
+    }
+
+    private void aodInterruptTimeoutParameterized(TestParams testParams) throws RemoteException {
+        reset(mUdfpsView);
+
         // GIVEN AOD interrupt
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         mFgExecutor.runAllReady();
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+        }
         // WHEN it times out
         mFgExecutor.advanceClockToNext();
         mFgExecutor.runAllReady();
-        // THEN the display is unconfigured
-        verify(mUdfpsView).unconfigureDisplay();
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // THEN the display is unconfigured.
+            verify(mUdfpsView).unconfigureDisplay();
+        } else {
+            // THEN the display configuration is unchanged.
+            verify(mUdfpsView, never()).unconfigureDisplay();
+        }
     }
 
     @Test
-    public void aodInterruptCancelTimeoutActionOnFingerUp() throws RemoteException {
+    public void aodInterruptCancelTimeoutActionOnFingerUp() {
+        runWithAllParams(this::aodInterruptCancelTimeoutActionOnFingerUpParameterized);
+    }
+
+    private void aodInterruptCancelTimeoutActionOnFingerUpParameterized(TestParams testParams)
+            throws RemoteException {
+        reset(mUdfpsView);
         when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
 
         // GIVEN AOD interrupt
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         mFgExecutor.runAllReady();
 
-        // Configure UdfpsView to accept the ACTION_UP event
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // Configure UdfpsView to accept the ACTION_UP event
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+        }
 
         // WHEN ACTION_UP is received
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
@@ -727,37 +888,54 @@
         moveEvent.recycle();
         mFgExecutor.runAllReady();
 
-        // Configure UdfpsView to accept the finger up event
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // Configure UdfpsView to accept the finger up event
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+        }
 
         // WHEN it times out
         mFgExecutor.advanceClockToNext();
         mFgExecutor.runAllReady();
 
-        // THEN the display should be unconfigured once. If the timeout action is not
-        // cancelled, the display would be unconfigured twice which would cause two
-        // FP attempts.
-        verify(mUdfpsView, times(1)).unconfigureDisplay();
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // THEN the display should be unconfigured once. If the timeout action is not
+            // cancelled, the display would be unconfigured twice which would cause two
+            // FP attempts.
+            verify(mUdfpsView, times(1)).unconfigureDisplay();
+        } else {
+            verify(mUdfpsView, never()).unconfigureDisplay();
+        }
     }
 
     @Test
-    public void aodInterruptCancelTimeoutActionOnAcquired() throws RemoteException {
+    public void aodInterruptCancelTimeoutActionOnAcquired() {
+        runWithAllParams(this::aodInterruptCancelTimeoutActionOnAcquiredParameterized);
+    }
+
+    private void aodInterruptCancelTimeoutActionOnAcquiredParameterized(TestParams testParams)
+            throws RemoteException {
+        reset(mUdfpsView);
         when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true);
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
 
         // GIVEN AOD interrupt
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         mFgExecutor.runAllReady();
 
-        // Configure UdfpsView to accept the acquired event
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // Configure UdfpsView to accept the acquired event
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+        }
 
         // WHEN acquired is received
-        mOverlayController.onAcquired(TEST_UDFPS_SENSOR_ID,
+        mOverlayController.onAcquired(testParams.sensorProps.sensorId,
                 BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD);
 
         // Configure UdfpsView to accept the ACTION_DOWN event
@@ -777,29 +955,42 @@
         moveEvent.recycle();
         mFgExecutor.runAllReady();
 
-        // Configure UdfpsView to accept the finger up event
-        when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // Configure UdfpsView to accept the finger up event
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
+        } else {
+            when(mUdfpsView.isDisplayConfigured()).thenReturn(false);
+        }
 
         // WHEN it times out
         mFgExecutor.advanceClockToNext();
         mFgExecutor.runAllReady();
 
-        // THEN the display should be unconfigured once. If the timeout action is not
-        // cancelled, the display would be unconfigured twice which would cause two
-        // FP attempts.
-        verify(mUdfpsView, times(1)).unconfigureDisplay();
+        if (testParams.sensorProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) {
+            // THEN the display should be unconfigured once. If the timeout action is not
+            // cancelled, the display would be unconfigured twice which would cause two
+            // FP attempts.
+            verify(mUdfpsView, times(1)).unconfigureDisplay();
+        } else {
+            verify(mUdfpsView, never()).unconfigureDisplay();
+        }
     }
 
     @Test
-    public void aodInterruptScreenOff() throws RemoteException {
+    public void aodInterruptScreenOff() {
+        runWithAllParams(this::aodInterruptScreenOffParameterized);
+    }
+
+    private void aodInterruptScreenOffParameterized(TestParams testParams) throws RemoteException {
+        reset(mUdfpsView);
+
         // GIVEN screen off
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOff();
         mFgExecutor.runAllReady();
 
         // WHEN aod interrupt is received
-        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
 
         // THEN display doesn't get configured because it's off
@@ -807,11 +998,17 @@
     }
 
     @Test
-    public void aodInterrupt_fingerprintNotRunning() throws RemoteException {
+    public void aodInterrupt_fingerprintNotRunning() {
+        runWithAllParams(this::aodInterrupt_fingerprintNotRunningParameterized);
+    }
+
+    private void aodInterrupt_fingerprintNotRunningParameterized(TestParams testParams)
+            throws RemoteException {
+        reset(mUdfpsView);
+
         // GIVEN showing overlay
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
-                mUdfpsOverlayControllerCallback);
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, testParams.sensorProps.sensorId,
+                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mScreenObserver.onScreenTurnedOn();
         mFgExecutor.runAllReady();
 
@@ -831,7 +1028,7 @@
 
         // GIVEN that the overlay is showing and a11y touch exploration enabled
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
@@ -866,7 +1063,7 @@
 
         // GIVEN that the overlay is showing and a11y touch exploration NOT enabled
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false);
-        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
+        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
index 7864f21b..1bc237d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
@@ -23,7 +23,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
@@ -48,7 +48,7 @@
     @Mock
     private AuthController mAuthController;
     @Mock
-    private IUdfpsHbmListener mDisplayCallback;
+    private IUdfpsRefreshRateRequestCallback mDisplayCallback;
     @Mock
     private UdfpsLogger mUdfpsLogger;
     @Mock
@@ -68,7 +68,7 @@
         when(contextSpy.getDisplayId()).thenReturn(DISPLAY_ID);
 
         // Set up mocks.
-        when(mAuthController.getUdfpsHbmListener()).thenReturn(mDisplayCallback);
+        when(mAuthController.getUdfpsRefreshRateCallback()).thenReturn(mDisplayCallback);
 
         // Create a real controller with mock dependencies.
         mHbmController = new UdfpsDisplayMode(contextSpy, mExecution, mAuthController,
@@ -81,7 +81,7 @@
         mHbmController.enable(mOnEnabled);
 
         // Should set the appropriate refresh rate for UDFPS and notify the caller.
-        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mDisplayCallback).onRequestEnabled(eq(DISPLAY_ID));
         verify(mOnEnabled).run();
 
         // Disable the UDFPS mode.
@@ -89,7 +89,7 @@
 
         // Should unset the refresh rate and notify the caller.
         verify(mOnDisabled).run();
-        verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+        verify(mDisplayCallback).onRequestDisabled(eq(DISPLAY_ID));
     }
 
     @Test
@@ -98,7 +98,7 @@
         mHbmController.enable(mOnEnabled);
 
         // Should set the appropriate refresh rate for UDFPS and notify the caller.
-        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mDisplayCallback).onRequestEnabled(eq(DISPLAY_ID));
         verify(mOnEnabled).run();
 
         // Second request to enable the UDFPS mode, while it's still enabled.
@@ -115,7 +115,7 @@
         mHbmController.enable(mOnEnabled);
 
         // Should set the appropriate refresh rate for UDFPS and notify the caller.
-        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mDisplayCallback).onRequestEnabled(eq(DISPLAY_ID));
         verify(mOnEnabled).run();
 
         // First request to disable the UDFPS mode.
@@ -123,7 +123,7 @@
 
         // Should unset the refresh rate and notify the caller.
         verify(mOnDisabled).run();
-        verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+        verify(mDisplayCallback).onRequestDisabled(eq(DISPLAY_ID));
 
         // Second request to disable the UDFPS mode, when it's already disabled.
         mHbmController.disable(mOnDisabled);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
new file mode 100644
index 0000000..3c61382
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.SystemUIDialogManager;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase {
+    // Dependencies
+    protected @Mock UdfpsKeyguardView mView;
+    protected @Mock Context mResourceContext;
+    protected @Mock StatusBarStateController mStatusBarStateController;
+    protected @Mock ShadeExpansionStateManager mShadeExpansionStateManager;
+    protected @Mock StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    protected @Mock LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    protected @Mock DumpManager mDumpManager;
+    protected @Mock DelayableExecutor mExecutor;
+    protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    protected @Mock KeyguardStateController mKeyguardStateController;
+    protected @Mock KeyguardViewMediator mKeyguardViewMediator;
+    protected @Mock ConfigurationController mConfigurationController;
+    protected @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    protected @Mock SystemUIDialogManager mDialogManager;
+    protected @Mock UdfpsController mUdfpsController;
+    protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
+    protected @Mock KeyguardBouncer mBouncer;
+    protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+
+    protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    protected FakeSystemClock mSystemClock = new FakeSystemClock();
+
+    protected UdfpsKeyguardViewController mController;
+
+    // Capture listeners so that they can be used to send events
+    private @Captor ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
+    protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+    private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
+    protected List<ShadeExpansionListener> mExpansionListeners;
+
+    private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateBouncer>
+            mAlternateBouncerCaptor;
+    protected StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
+
+    private @Captor ArgumentCaptor<KeyguardStateController.Callback>
+            mKeyguardStateControllerCallbackCaptor;
+    protected KeyguardStateController.Callback mKeyguardStateControllerCallback;
+
+    private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.KeyguardViewManagerCallback>
+            mKeyguardViewManagerCallbackArgumentCaptor;
+    protected StatusBarKeyguardViewManager.KeyguardViewManagerCallback mKeyguardViewManagerCallback;
+
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mView.getContext()).thenReturn(mResourceContext);
+        when(mResourceContext.getString(anyInt())).thenReturn("test string");
+        when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false);
+        when(mView.getUnpausedAlpha()).thenReturn(255);
+        mController = createUdfpsKeyguardViewController();
+    }
+
+    protected void sendStatusBarStateChanged(int statusBarState) {
+        mStatusBarStateListener.onStateChanged(statusBarState);
+    }
+
+    protected void captureStatusBarStateListeners() {
+        verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture());
+        mStatusBarStateListener = mStateListenerCaptor.getValue();
+    }
+
+    protected void captureStatusBarExpansionListeners() {
+        verify(mShadeExpansionStateManager, times(2))
+                .addExpansionListener(mExpansionListenerCaptor.capture());
+        // first (index=0) is from super class, UdfpsAnimationViewController.
+        // second (index=1) is from UdfpsKeyguardViewController
+        mExpansionListeners = mExpansionListenerCaptor.getAllValues();
+    }
+
+    protected void updateStatusBarExpansion(float fraction, boolean expanded) {
+        ShadeExpansionChangeEvent event =
+                new ShadeExpansionChangeEvent(
+                        fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
+        for (ShadeExpansionListener listener : mExpansionListeners) {
+            listener.onPanelExpansionChanged(event);
+        }
+    }
+
+    protected void captureAltAuthInterceptor() {
+        verify(mStatusBarKeyguardViewManager).setAlternateBouncer(
+                mAlternateBouncerCaptor.capture());
+        mAlternateBouncer = mAlternateBouncerCaptor.getValue();
+    }
+
+    protected void captureKeyguardStateControllerCallback() {
+        verify(mKeyguardStateController).addCallback(
+                mKeyguardStateControllerCallbackCaptor.capture());
+        mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue();
+    }
+
+    public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
+        return createUdfpsKeyguardViewController(false, false);
+    }
+
+    public void captureKeyGuardViewManagerCallback() {
+        verify(mStatusBarKeyguardViewManager).addCallback(
+                mKeyguardViewManagerCallbackArgumentCaptor.capture());
+        mKeyguardViewManagerCallback = mKeyguardViewManagerCallbackArgumentCaptor.getValue();
+    }
+
+    protected UdfpsKeyguardViewController createUdfpsKeyguardViewController(
+            boolean useModernBouncer, boolean useExpandedOverlay) {
+        mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer);
+        mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay);
+        when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(
+                useModernBouncer ? null : mBouncer);
+        UdfpsKeyguardViewController controller = new UdfpsKeyguardViewController(
+                mView,
+                mStatusBarStateController,
+                mShadeExpansionStateManager,
+                mStatusBarKeyguardViewManager,
+                mKeyguardUpdateMonitor,
+                mDumpManager,
+                mLockscreenShadeTransitionController,
+                mConfigurationController,
+                mSystemClock,
+                mKeyguardStateController,
+                mUnlockedScreenOffAnimationController,
+                mDialogManager,
+                mUdfpsController,
+                mActivityLaunchAnimator,
+                mFeatureFlags,
+                mPrimaryBouncerInteractor);
+        return controller;
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index c0f9c82..babe533 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.atLeast;
@@ -25,126 +26,55 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
+import android.view.MotionEvent;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.statusbar.phone.KeyguardBouncer;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.List;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
-public class UdfpsKeyguardViewControllerTest extends SysuiTestCase {
-    // Dependencies
-    @Mock
-    private UdfpsKeyguardView mView;
-    @Mock
-    private Context mResourceContext;
-    @Mock
-    private StatusBarStateController mStatusBarStateController;
-    @Mock
-    private ShadeExpansionStateManager mShadeExpansionStateManager;
-    @Mock
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    @Mock
-    private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Mock
-    private DumpManager mDumpManager;
-    @Mock
-    private DelayableExecutor mExecutor;
-    @Mock
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock
-    private KeyguardStateController mKeyguardStateController;
-    @Mock
-    private KeyguardViewMediator mKeyguardViewMediator;
-    @Mock
-    private ConfigurationController mConfigurationController;
-    @Mock
-    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    @Mock
-    private SystemUIDialogManager mDialogManager;
-    @Mock
-    private UdfpsController mUdfpsController;
-    @Mock
-    private ActivityLaunchAnimator mActivityLaunchAnimator;
-    private FakeSystemClock mSystemClock = new FakeSystemClock();
+public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
+    private @Captor ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback>
+            mBouncerExpansionCallbackCaptor;
+    private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
 
-    private UdfpsKeyguardViewController mController;
-
-    // Capture listeners so that they can be used to send events
-    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
-    private StatusBarStateController.StateListener mStatusBarStateListener;
-
-    @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
-    private List<ShadeExpansionListener> mExpansionListeners;
-
-    @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
-            mAltAuthInterceptorCaptor;
-    private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor;
-
-    @Captor private ArgumentCaptor<KeyguardStateController.Callback>
-            mKeyguardStateControllerCallbackCaptor;
-    private KeyguardStateController.Callback mKeyguardStateControllerCallback;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mView.getContext()).thenReturn(mResourceContext);
-        when(mResourceContext.getString(anyInt())).thenReturn("test string");
-        when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false);
-        when(mView.getUnpausedAlpha()).thenReturn(255);
-        mController = new UdfpsKeyguardViewController(
-                mView,
-                mStatusBarStateController,
-                mShadeExpansionStateManager,
-                mStatusBarKeyguardViewManager,
-                mKeyguardUpdateMonitor,
-                mDumpManager,
-                mLockscreenShadeTransitionController,
-                mConfigurationController,
-                mSystemClock,
-                mKeyguardStateController,
-                mUnlockedScreenOffAnimationController,
-                mDialogManager,
-                mUdfpsController,
-                mActivityLaunchAnimator);
+    @Override
+    public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
+        return createUdfpsKeyguardViewController(/* useModernBouncer */ false,
+                /* useExpandedOverlay */ false);
     }
 
     @Test
+    public void testShouldPauseAuth_bouncerShowing() {
+        mController.onViewAttached();
+        captureStatusBarStateListeners();
+        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
+
+        captureBouncerExpansionCallback();
+        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
+        mBouncerExpansionCallback.onVisibilityChanged(true);
+
+        assertTrue(mController.shouldPauseAuth());
+    }
+
+
+
+    @Test
     public void testRegistersExpansionChangedListenerOnAttached() {
         mController.onViewAttached();
         captureStatusBarExpansionListeners();
@@ -202,20 +132,6 @@
     }
 
     @Test
-    public void testShouldPauseAuthBouncerShowing() {
-        mController.onViewAttached();
-        captureStatusBarStateListeners();
-        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
-
-        captureAltAuthInterceptor();
-        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
-        mAltAuthInterceptor.onBouncerVisibilityChanged();
-
-        assertTrue(mController.shouldPauseAuth());
-    }
-
-    @Test
     public void testShouldPauseAuthUnpausedAlpha0() {
         mController.onViewAttached();
         captureStatusBarStateListeners();
@@ -326,13 +242,13 @@
         sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED);
         assertTrue(mController.shouldPauseAuth());
 
-        mAltAuthInterceptor.showAlternateAuthBouncer(); // force show
+        mAlternateBouncer.showAlternateBouncer(); // force show
         assertFalse(mController.shouldPauseAuth());
-        assertTrue(mAltAuthInterceptor.isShowingAlternateAuthBouncer());
+        assertTrue(mAlternateBouncer.isShowingAlternateBouncer());
 
-        mAltAuthInterceptor.hideAlternateAuthBouncer(); // stop force show
+        mAlternateBouncer.hideAlternateBouncer(); // stop force show
         assertTrue(mController.shouldPauseAuth());
-        assertFalse(mAltAuthInterceptor.isShowingAlternateAuthBouncer());
+        assertFalse(mAlternateBouncer.isShowingAlternateBouncer());
     }
 
     @Test
@@ -345,7 +261,7 @@
         mController.onViewDetached();
 
         // THEN remove alternate auth interceptor
-        verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAltAuthInterceptor);
+        verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAlternateBouncer);
     }
 
     @Test
@@ -355,14 +271,15 @@
         captureAltAuthInterceptor();
 
         // GIVEN udfps bouncer isn't showing
-        mAltAuthInterceptor.hideAlternateAuthBouncer();
+        mAlternateBouncer.hideAlternateBouncer();
 
         // WHEN touch is observed outside the view
         mController.onTouchOutsideView();
 
         // THEN bouncer / alt auth methods are never called
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
         verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).resetAlternateAuth(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
     }
 
     @Test
@@ -372,32 +289,33 @@
         captureAltAuthInterceptor();
 
         // GIVEN udfps bouncer is showing
-        mAltAuthInterceptor.showAlternateAuthBouncer();
+        mAlternateBouncer.showAlternateBouncer();
 
         // WHEN touch is observed outside the view 200ms later (just within threshold)
         mSystemClock.advanceTime(200);
         mController.onTouchOutsideView();
 
         // THEN bouncer / alt auth methods are never called because not enough time has passed
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
         verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
-        verify(mStatusBarKeyguardViewManager, never()).resetAlternateAuth(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean());
     }
 
     @Test
-    public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showInputBouncer() {
+    public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showPrimaryBouncer() {
         // GIVEN view is attached
         mController.onViewAttached();
         captureAltAuthInterceptor();
 
         // GIVEN udfps bouncer is showing
-        mAltAuthInterceptor.showAlternateAuthBouncer();
+        mAlternateBouncer.showAlternateBouncer();
 
         // WHEN touch is observed outside the view 205ms later
         mSystemClock.advanceTime(205);
         mController.onTouchOutsideView();
 
         // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showBouncer(eq(true));
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(eq(true));
     }
 
     @Test
@@ -428,7 +346,7 @@
         when(mResourceContext.getString(anyInt())).thenReturn("test string");
 
         // WHEN status bar expansion is 0 but udfps bouncer is requested
-        mAltAuthInterceptor.showAlternateAuthBouncer();
+        mAlternateBouncer.showAlternateBouncer();
 
         // THEN alpha is 255
         verify(mView).setUnpausedAlpha(255);
@@ -459,7 +377,7 @@
         captureKeyguardStateControllerCallback();
         captureAltAuthInterceptor();
         updateStatusBarExpansion(1f, true);
-        mAltAuthInterceptor.showAlternateAuthBouncer();
+        mAlternateBouncer.showAlternateBouncer();
         reset(mView);
 
         // WHEN we're transitioning to the full shade
@@ -503,41 +421,41 @@
         verify(mView, atLeastOnce()).setPauseAuth(false);
     }
 
-    private void sendStatusBarStateChanged(int statusBarState) {
-        mStatusBarStateListener.onStateChanged(statusBarState);
+    private void captureBouncerExpansionCallback() {
+        verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture());
+        mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue();
     }
 
-    private void captureStatusBarStateListeners() {
-        verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture());
-        mStatusBarStateListener = mStateListenerCaptor.getValue();
+    @Test
+    // TODO(b/259264861): Tracking Bug
+    public void testUdfpsExpandedOverlayOn() {
+        // GIVEN view is attached and useExpandedOverlay is true
+        mController = createUdfpsKeyguardViewController(false, true);
+        mController.onViewAttached();
+        captureKeyGuardViewManagerCallback();
+
+        // WHEN a touch is received
+        mKeyguardViewManagerCallback.onTouch(
+                MotionEvent.obtain(0, 0, 0, 0, 0, 0));
+
+        // THEN udfpsController onTouch is not called
+        assertTrue(mView.mUseExpandedOverlay);
+        verify(mUdfpsController, never()).onTouch(any());
     }
 
-    private void captureStatusBarExpansionListeners() {
-        verify(mShadeExpansionStateManager, times(2))
-                .addExpansionListener(mExpansionListenerCaptor.capture());
-        // first (index=0) is from super class, UdfpsAnimationViewController.
-        // second (index=1) is from UdfpsKeyguardViewController
-        mExpansionListeners = mExpansionListenerCaptor.getAllValues();
-    }
+    @Test
+    // TODO(b/259264861): Tracking Bug
+    public void testUdfpsExpandedOverlayOff() {
+        // GIVEN view is attached and useExpandedOverlay is false
+        mController.onViewAttached();
+        captureKeyGuardViewManagerCallback();
 
-    private void updateStatusBarExpansion(float fraction, boolean expanded) {
-        ShadeExpansionChangeEvent event =
-                new ShadeExpansionChangeEvent(
-                        fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
-        for (ShadeExpansionListener listener : mExpansionListeners) {
-            listener.onPanelExpansionChanged(event);
-        }
-    }
+        // WHEN a touch is received
+        mKeyguardViewManagerCallback.onTouch(
+                MotionEvent.obtain(0, 0, 0, 0, 0, 0));
 
-    private void captureAltAuthInterceptor() {
-        verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor(
-                mAltAuthInterceptorCaptor.capture());
-        mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue();
-    }
-
-    private void captureKeyguardStateControllerCallback() {
-        verify(mKeyguardStateController).addCallback(
-                mKeyguardStateControllerCallbackCaptor.capture());
-        mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue();
+        // THEN udfpsController onTouch is called
+        assertFalse(mView.mUseExpandedOverlay);
+        verify(mUdfpsController).onTouch(any());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..517e27a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControllerBaseTest() {
+    lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
+
+    @Before
+    override fun setUp() {
+        allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
+        MockitoAnnotations.initMocks(this)
+        keyguardBouncerRepository =
+            KeyguardBouncerRepository(
+                mock(com.android.keyguard.ViewMediatorCallback::class.java),
+                mKeyguardUpdateMonitor
+            )
+        super.setUp()
+    }
+
+    override fun createUdfpsKeyguardViewController(): UdfpsKeyguardViewController? {
+        mPrimaryBouncerInteractor =
+            PrimaryBouncerInteractor(
+                keyguardBouncerRepository,
+                mock(BouncerView::class.java),
+                mock(Handler::class.java),
+                mKeyguardStateController,
+                mock(KeyguardSecurityModel::class.java),
+                mock(PrimaryBouncerCallbackInteractor::class.java),
+                mock(FalsingCollector::class.java),
+                mock(DismissCallbackRegistry::class.java),
+                mock(KeyguardBypassController::class.java),
+                mKeyguardUpdateMonitor
+            )
+        return createUdfpsKeyguardViewController(
+            /* useModernBouncer */ true, /* useExpandedOverlay */
+            false
+        )
+    }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testShouldPauseAuthBouncerShowing() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN view attached and we're on the keyguard
+            mController.onViewAttached()
+            captureStatusBarStateListeners()
+            sendStatusBarStateChanged(StatusBarState.KEYGUARD)
+
+            // WHEN the bouncer expansion is VISIBLE
+            val job = mController.listenForBouncerExpansion(this)
+            keyguardBouncerRepository.setPrimaryVisible(true)
+            keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+            yield()
+
+            // THEN UDFPS shouldPauseAuth == true
+            assertTrue(mController.shouldPauseAuth())
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt
new file mode 100644
index 0000000..54f20db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.biometrics
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.UdfpsController.UdfpsOverlayController
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.util.mockito.any
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenEver
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class UdfpsShellTest : SysuiTestCase() {
+
+    @JvmField @Rule var rule = MockitoJUnit.rule()
+
+    // Unit under test
+    private lateinit var udfpsShell: UdfpsShell
+
+    @Mock lateinit var commandRegistry: CommandRegistry
+    @Mock lateinit var udfpsOverlay: UdfpsOverlay
+    @Mock lateinit var udfpsOverlayController: UdfpsOverlayController
+
+    @Captor private lateinit var motionEvent: ArgumentCaptor<MotionEvent>
+
+    private val sensorBounds = Rect()
+
+    @Before
+    fun setup() {
+        whenEver(udfpsOverlayController.sensorBounds).thenReturn(sensorBounds)
+
+        udfpsShell = UdfpsShell(commandRegistry, udfpsOverlay)
+        udfpsShell.udfpsOverlayController = udfpsOverlayController
+    }
+
+    @Test
+    fun testSimFingerDown() {
+        udfpsShell.simFingerDown()
+
+        verify(udfpsOverlayController, times(2)).debugOnTouch(any(), motionEvent.capture())
+
+        assertEquals(motionEvent.allValues[0].action, MotionEvent.ACTION_DOWN) // ACTION_MOVE
+        assertEquals(motionEvent.allValues[1].action, MotionEvent.ACTION_MOVE) // ACTION_MOVE
+    }
+
+    @Test
+    fun testSimFingerUp() {
+        udfpsShell.simFingerUp()
+
+        verify(udfpsOverlayController).debugOnTouch(any(), motionEvent.capture())
+
+        assertEquals(motionEvent.value.action, MotionEvent.ACTION_UP)
+    }
+
+    @Test
+    fun testOnUiReady() {
+        udfpsShell.onUiReady()
+
+        verify(udfpsOverlayController).debugOnUiReady(any(), any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index b78c063..ac936e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -68,7 +68,8 @@
         view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView
         view.animationViewController = animationViewController
         val sensorBounds = SensorLocationInternal("", SENSOR_X, SENSOR_Y, SENSOR_RADIUS).rect
-        view.overlayParams = UdfpsOverlayParams(sensorBounds, 1920, 1080, 1f, Surface.ROTATION_0)
+        view.overlayParams = UdfpsOverlayParams(sensorBounds, sensorBounds, 1920,
+            1080, 1f, Surface.ROTATION_0)
         view.setUdfpsDisplayModeProvider(hbmProvider)
         ViewUtils.attachView(view)
     }
@@ -133,7 +134,8 @@
     @Test
     fun isNotWithinSensorArea() {
         whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f))
-        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())).isFalse()
+        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat()))
+            .isFalse()
         assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/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/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index 2af0557..d159714 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.ripple.RippleView
+import com.android.systemui.surfaceeffects.ripple.RippleView
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
index 6bc7308..0fadc13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
@@ -39,6 +39,8 @@
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingDataProvider.GestureFinalizedListener;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -66,6 +68,8 @@
     @Mock
     private SingleTapClassifier mSingleTapClassfier;
     @Mock
+    private LongTapClassifier mLongTapClassifier;
+    @Mock
     private DoubleTapClassifier mDoubleTapClassifier;
     @Mock
     private FalsingClassifier mClassifierA;
@@ -80,6 +84,7 @@
     private AccessibilityManager mAccessibilityManager;
 
     private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    private final FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags();
 
     private final FalsingClassifier.Result mFalsedResult =
             FalsingClassifier.Result.falsed(1, getClass().getSimpleName(), "");
@@ -94,6 +99,7 @@
         when(mClassifierB.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mPassedResult);
         when(mSingleTapClassfier.isTap(any(List.class), anyDouble())).thenReturn(mPassedResult);
+        when(mLongTapClassifier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
         when(mDoubleTapClassifier.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mPassedResult);
         mClassifiers.add(mClassifierA);
@@ -101,9 +107,9 @@
         when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
-                mMetricsLogger, mClassifiers, mSingleTapClassfier, mDoubleTapClassifier,
-                mHistoryTracker, mKeyguardStateController, mAccessibilityManager,
-                false);
+                mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier,
+                mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
+                mAccessibilityManager, false, mFakeFeatureFlags);
 
 
         ArgumentCaptor<GestureFinalizedListener> gestureCompleteListenerCaptor =
@@ -113,6 +119,8 @@
                 gestureCompleteListenerCaptor.capture());
 
         mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue();
+        mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
+        mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true);
     }
 
     @Test
@@ -212,7 +220,7 @@
     }
 
     @Test
-    public void testIsFalseTap_EmptyRecentEvents() {
+    public void testIsFalseSingleTap_EmptyRecentEvents() {
         // Ensure we look at prior events if recent events has already been emptied.
         when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(new ArrayList<>());
         when(mFalsingDataProvider.getPriorMotionEvents()).thenReturn(mMotionEventList);
@@ -223,7 +231,7 @@
 
 
     @Test
-    public void testIsFalseTap_RobustCheck_NoFaceAuth() {
+    public void testIsFalseSingleTap_RobustCheck_NoFaceAuth() {
         when(mSingleTapClassfier.isTap(mMotionEventList, 0)).thenReturn(mPassedResult);
         when(mDoubleTapClassifier.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mFalsedResult);
@@ -233,13 +241,50 @@
     }
 
     @Test
-    public void testIsFalseTap_RobustCheck_FaceAuth() {
+    public void testIsFalseSingleTap_RobustCheck_FaceAuth() {
         when(mSingleTapClassfier.isTap(mMotionEventList, 0)).thenReturn(mPassedResult);
         when(mFalsingDataProvider.isJustUnlockedWithFace()).thenReturn(true);
         assertThat(mBrightLineFalsingManager.isFalseTap(NO_PENALTY)).isFalse();
     }
 
     @Test
+    public void testIsFalseLongTap_EmptyRecentEvents() {
+        // Ensure we look at prior events if recent events has already been emptied.
+        when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(new ArrayList<>());
+        when(mFalsingDataProvider.getPriorMotionEvents()).thenReturn(mMotionEventList);
+
+        mBrightLineFalsingManager.isFalseLongTap(0);
+        verify(mLongTapClassifier).isTap(mMotionEventList, 0);
+    }
+
+    @Test
+    public void testIsFalseLongTap_FalseLongTap_NotFlagged() {
+        mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, false);
+        when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mFalsedResult);
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isFalse();
+    }
+
+    @Test
+    public void testIsFalseLongTap_FalseLongTap() {
+        when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mFalsedResult);
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isTrue();
+    }
+
+    @Test
+    public void testIsFalseLongTap_RobustCheck_NoFaceAuth() {
+        when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mPassedResult);
+        when(mFalsingDataProvider.isJustUnlockedWithFace()).thenReturn(false);
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isFalse();
+    }
+
+    @Test
+    public void testIsFalseLongTap_RobustCheck_FaceAuth() {
+        when(mLongTapClassifier.isTap(mMotionEventList, 0)).thenReturn(mPassedResult);
+        when(mFalsingDataProvider.isJustUnlockedWithFace()).thenReturn(true);
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(NO_PENALTY)).isFalse();
+    }
+
+    @Test
     public void testIsFalseDoubleTap() {
         when(mDoubleTapClassifier.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mPassedResult);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
index 9481349..4281ee0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java
@@ -32,6 +32,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -57,6 +59,8 @@
     @Mock
     private SingleTapClassifier mSingleTapClassifier;
     @Mock
+    private LongTapClassifier mLongTapClassifier;
+    @Mock
     private DoubleTapClassifier mDoubleTapClassifier;
     @Mock
     private FalsingClassifier mClassifierA;
@@ -71,6 +75,7 @@
     private final FalsingClassifier.Result mPassedResult = FalsingClassifier.Result.passed(1);
     private final FalsingClassifier.Result mFalsedResult =
             FalsingClassifier.Result.falsed(1, getClass().getSimpleName(), "");
+    private final FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags();
 
     @Before
     public void setup() {
@@ -78,15 +83,17 @@
         when(mClassifierA.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mFalsedResult);
         when(mSingleTapClassifier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
+        when(mLongTapClassifier.isTap(any(List.class), anyDouble())).thenReturn(mFalsedResult);
         when(mDoubleTapClassifier.classifyGesture(anyInt(), anyDouble(), anyDouble()))
                 .thenReturn(mFalsedResult);
         mClassifiers.add(mClassifierA);
         when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList);
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider,
-                mMetricsLogger, mClassifiers, mSingleTapClassifier, mDoubleTapClassifier,
-                mHistoryTracker, mKeyguardStateController, mAccessibilityManager,
-                false);
+                mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier,
+                mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController,
+                mAccessibilityManager, false, mFakeFeatureFlags);
+        mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true);
     }
 
     @Test
@@ -96,7 +103,6 @@
         assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
     }
 
-
     @Test
     public void testA11yDisablesTap() {
         assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue();
@@ -106,6 +112,13 @@
 
 
     @Test
+    public void testA11yDisablesLongTap() {
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(1)).isTrue();
+        when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
+        assertThat(mBrightLineFalsingManager.isFalseLongTap(1)).isFalse();
+    }
+
+    @Test
     public void testA11yDisablesDoubleTap() {
         assertThat(mBrightLineFalsingManager.isFalseDoubleTap()).isTrue();
         when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true);
@@ -159,4 +172,11 @@
         });
         assertThat(mBrightLineFalsingManager.isProximityNear()).isFalse();
     }
+
+    @Test
+    public void testA11yAction() {
+        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue();
+        when(mFalsingDataProvider.isA11yAction()).thenReturn(true);
+        assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt
new file mode 100644
index 0000000..2c904e7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingA11yDelegateTest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.classifier
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK
+import android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FalsingA11yDelegateTest : SysuiTestCase() {
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var view: View
+    lateinit var underTest: FalsingA11yDelegate
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        underTest = FalsingA11yDelegate(falsingCollector)
+    }
+
+    @Test
+    fun testPerformAccessibilityAction_ACTION_CLICK() {
+        underTest.performAccessibilityAction(view, ACTION_CLICK, null)
+        verify(falsingCollector).onA11yAction()
+    }
+
+    @Test
+    fun testPerformAccessibilityAction_not_ACTION_CLICK() {
+        underTest.performAccessibilityAction(view, ACTION_LONG_CLICK, null)
+        verify(falsingCollector, never()).onA11yAction()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
index 3e9cf1e..442bf91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -71,6 +72,8 @@
     @Mock
     private KeyguardStateController mKeyguardStateController;
     @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
+    @Mock
     private BatteryController mBatteryController;
     private final DockManagerFake mDockManager = new DockManagerFake();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -85,7 +88,8 @@
 
         mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager,
                 mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor,
-                mStatusBarStateController, mKeyguardStateController, mBatteryController,
+                mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager,
+                mBatteryController,
                 mDockManager, mFakeExecutor, mFakeSystemClock);
     }
 
@@ -137,9 +141,9 @@
     public void testUnregisterSensor_QS() {
         mFalsingCollector.onScreenTurningOn();
         reset(mProximitySensor);
-        mFalsingCollector.setQsExpanded(true);
+        mFalsingCollector.onQsExpansionChanged(true);
         verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class));
-        mFalsingCollector.setQsExpanded(false);
+        mFalsingCollector.onQsExpansionChanged(false);
         verify(mProximitySensor).register(any(ThresholdSensor.Listener.class));
     }
 
@@ -263,4 +267,10 @@
         mFalsingCollector.onTouchEvent(up);
         verify(mFalsingDataProvider, times(2)).onMotionEvent(any(MotionEvent.class));
     }
+
+    @Test
+    public void testOnA11yAction() {
+        mFalsingCollector.onA11yAction();
+        verify(mFalsingDataProvider).onA11yAction();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
index 5dc607f..d315c2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java
@@ -310,4 +310,10 @@
         // an empty array.
         assertThat(mDataProvider.getPriorMotionEvents()).isNotNull();
     }
+
+    @Test
+    public void test_MotionEventComplete_A11yAction() {
+        mDataProvider.onA11yAction();
+        assertThat(mDataProvider.isA11yAction()).isTrue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index d96ca91..a872e4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -19,7 +19,9 @@
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
+import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -37,8 +39,10 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.screenshot.TimeoutHandler;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -61,7 +65,10 @@
     @Mock
     private TimeoutHandler mTimeoutHandler;
     @Mock
+    private ClipboardOverlayUtils mClipboardUtils;
+    @Mock
     private UiEventLogger mUiEventLogger;
+    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     @Mock
     private Animator mAnimator;
@@ -72,7 +79,6 @@
     private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor;
     private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks;
 
-
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -83,6 +89,8 @@
         mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
                 new ClipData.Item("Test Item"));
 
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, false);
+
         mOverlayController = new ClipboardOverlayController(
                 mContext,
                 mClipboardOverlayView,
@@ -90,11 +98,18 @@
                 getFakeBroadcastDispatcher(),
                 mBroadcastSender,
                 mTimeoutHandler,
+                mFeatureFlags,
+                mClipboardUtils,
                 mUiEventLogger);
         verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture());
         mCallbacks = mOverlayCallbacksCaptor.getValue();
     }
 
+    @After
+    public void tearDown() {
+        mOverlayController.hideImmediate();
+    }
+
     @Test
     public void test_setClipData_nullData() {
         ClipData clipData = null;
@@ -180,4 +195,33 @@
         verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
         verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
     }
+
+    @Test
+    public void test_remoteCopy_withFlagOn() {
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        verify(mTimeoutHandler, never()).resetTimeout();
+    }
+
+    @Test
+    public void test_remoteCopy_withFlagOff() {
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true);
+
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        verify(mTimeoutHandler).resetTimeout();
+    }
+
+    @Test
+    public void test_nonRemoteCopy() {
+        mFeatureFlags.set(CLIPBOARD_REMOTE_BEHAVIOR, true);
+        when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false);
+
+        mOverlayController.setClipData(mSampleClipData, "");
+
+        verify(mTimeoutHandler).resetTimeout();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java
new file mode 100644
index 0000000..09b1699
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.clipboardoverlay;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.os.PersistableBundle;
+import android.testing.TestableResources;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ClipboardOverlayUtilsTest extends SysuiTestCase {
+
+    private ClipboardOverlayUtils mClipboardUtils;
+
+    @Before
+    public void setUp() {
+        mClipboardUtils = new ClipboardOverlayUtils();
+    }
+
+    @Test
+    public void test_extra_withPackage_returnsTrue() {
+        PersistableBundle b = new PersistableBundle();
+        b.putBoolean(ClipDescription.EXTRA_IS_REMOTE_DEVICE, true);
+        ClipData data = constructClipData(
+                new String[]{"text/plain"}, new ClipData.Item("6175550000"), b);
+        TestableResources res = mContext.getOrCreateTestableResources();
+        res.addOverride(
+                R.string.config_remoteCopyPackage, "com.android.remote/.RemoteActivity");
+
+        assertTrue(mClipboardUtils.isRemoteCopy(mContext, data, "com.android.remote"));
+    }
+
+    @Test
+    public void test_noExtra_returnsFalse() {
+        ClipData data = constructClipData(
+                new String[]{"text/plain"}, new ClipData.Item("6175550000"), null);
+        TestableResources res = mContext.getOrCreateTestableResources();
+        res.addOverride(
+                R.string.config_remoteCopyPackage, "com.android.remote/.RemoteActivity");
+
+        assertFalse(mClipboardUtils.isRemoteCopy(mContext, data, "com.android.remote"));
+    }
+
+    @Test
+    public void test_falseExtra_returnsFalse() {
+        PersistableBundle b = new PersistableBundle();
+        b.putBoolean(ClipDescription.EXTRA_IS_REMOTE_DEVICE, false);
+        ClipData data = constructClipData(
+                new String[]{"text/plain"}, new ClipData.Item("6175550000"), b);
+        TestableResources res = mContext.getOrCreateTestableResources();
+        res.addOverride(
+                R.string.config_remoteCopyPackage, "com.android.remote/.RemoteActivity");
+
+        assertFalse(mClipboardUtils.isRemoteCopy(mContext, data, "com.android.remote"));
+    }
+
+    @Test
+    public void test_wrongPackage_returnsFalse() {
+        PersistableBundle b = new PersistableBundle();
+        b.putBoolean(ClipDescription.EXTRA_IS_REMOTE_DEVICE, true);
+        ClipData data = constructClipData(
+                new String[]{"text/plain"}, new ClipData.Item("6175550000"), b);
+
+        assertFalse(mClipboardUtils.isRemoteCopy(mContext, data, ""));
+    }
+
+    static ClipData constructClipData(String[] mimeTypes, ClipData.Item item,
+            PersistableBundle extras) {
+        ClipDescription description = new ClipDescription("Test", mimeTypes);
+        if (extras != null) {
+            description.setExtras(extras);
+        }
+        return new ClipData(description, item);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
new file mode 100644
index 0000000..3b6f7d1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsEditingActivityTest.kt
@@ -0,0 +1,118 @@
+package com.android.systemui.controls.management
+
+import android.content.ComponentName
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.CustomIconCache
+import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.concurrent.CountDownLatch
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ControlsEditingActivityTest : SysuiTestCase() {
+    private val uiExecutor = FakeExecutor(FakeSystemClock())
+
+    @Mock lateinit var controller: ControlsControllerImpl
+
+    @Mock lateinit var userTracker: UserTracker
+
+    @Mock lateinit var customIconCache: CustomIconCache
+
+    @Mock lateinit var uiController: ControlsUiController
+
+    private lateinit var controlsEditingActivity: ControlsEditingActivity_Factory
+    private var latch: CountDownLatch = CountDownLatch(1)
+
+    @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
+    @Captor private lateinit var captureCallback: ArgumentCaptor<OnBackInvokedCallback>
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object :
+                SingleActivityFactory<TestableControlsEditingActivity>(
+                    TestableControlsEditingActivity::class.java
+                ) {
+                override fun create(intent: Intent?): TestableControlsEditingActivity {
+                    return TestableControlsEditingActivity(
+                        uiExecutor,
+                        controller,
+                        userTracker,
+                        customIconCache,
+                        uiController,
+                        mockDispatcher,
+                        latch
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val intent = Intent()
+        intent.putExtra(ControlsEditingActivity.EXTRA_STRUCTURE, "TestTitle")
+        val cname = ComponentName("TestPackageName", "TestClassName")
+        intent.putExtra(Intent.EXTRA_COMPONENT_NAME, cname)
+        activityRule.launchActivity(intent)
+    }
+
+    @Test
+    fun testBackCallbackRegistrationAndUnregistration() {
+        // 1. ensure that launching the activity results in it registering a callback
+        verify(mockDispatcher)
+            .registerOnBackInvokedCallback(
+                ArgumentMatchers.eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
+                captureCallback.capture()
+            )
+        activityRule.finishActivity()
+        latch.await() // ensure activity is finished
+        // 2. ensure that when the activity is finished, it unregisters the same callback
+        verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
+    }
+
+    public class TestableControlsEditingActivity(
+        private val executor: FakeExecutor,
+        private val controller: ControlsControllerImpl,
+        private val userTracker: UserTracker,
+        private val customIconCache: CustomIconCache,
+        private val uiController: ControlsUiController,
+        private val mockDispatcher: OnBackInvokedDispatcher,
+        private val latch: CountDownLatch
+    ) : ControlsEditingActivity(executor, controller, userTracker, customIconCache, uiController) {
+        override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+            return mockDispatcher
+        }
+
+        override fun onStop() {
+            super.onStop()
+            // ensures that test runner thread does not proceed until ui thread is done
+            latch.countDown()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
new file mode 100644
index 0000000..0f06de2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsFavoritingActivityTest.kt
@@ -0,0 +1,122 @@
+package com.android.systemui.controls.management
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsControllerImpl
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ControlsFavoritingActivityTest : SysuiTestCase() {
+    @Main private val executor: Executor = MoreExecutors.directExecutor()
+
+    @Mock lateinit var controller: ControlsControllerImpl
+
+    @Mock lateinit var listingController: ControlsListingController
+
+    @Mock lateinit var userTracker: UserTracker
+
+    @Mock lateinit var uiController: ControlsUiController
+
+    private lateinit var controlsFavoritingActivity: ControlsFavoritingActivity_Factory
+    private var latch: CountDownLatch = CountDownLatch(1)
+
+    @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
+    @Captor private lateinit var captureCallback: ArgumentCaptor<OnBackInvokedCallback>
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object :
+                SingleActivityFactory<TestableControlsFavoritingActivity>(
+                    TestableControlsFavoritingActivity::class.java
+                ) {
+                override fun create(intent: Intent?): TestableControlsFavoritingActivity {
+                    return TestableControlsFavoritingActivity(
+                        executor,
+                        controller,
+                        listingController,
+                        userTracker,
+                        uiController,
+                        mockDispatcher,
+                        latch
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val intent = Intent()
+        intent.putExtra(ControlsFavoritingActivity.EXTRA_FROM_PROVIDER_SELECTOR, true)
+        activityRule.launchActivity(intent)
+    }
+
+    @Test
+    fun testBackCallbackRegistrationAndUnregistration() {
+        // 1. ensure that launching the activity results in it registering a callback
+        verify(mockDispatcher)
+            .registerOnBackInvokedCallback(
+                ArgumentMatchers.eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
+                captureCallback.capture()
+            )
+        activityRule.finishActivity()
+        latch.await() // ensure activity is finished
+        // 2. ensure that when the activity is finished, it unregisters the same callback
+        verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
+    }
+
+    public class TestableControlsFavoritingActivity(
+        executor: Executor,
+        controller: ControlsControllerImpl,
+        listingController: ControlsListingController,
+        userTracker: UserTracker,
+        uiController: ControlsUiController,
+        private val mockDispatcher: OnBackInvokedDispatcher,
+        private val latch: CountDownLatch
+    ) :
+        ControlsFavoritingActivity(
+            executor,
+            controller,
+            listingController,
+            userTracker,
+            uiController
+        ) {
+        override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+            return mockDispatcher
+        }
+
+        override fun onStop() {
+            super.onStop()
+            // ensures that test runner thread does not proceed until ui thread is done
+            latch.countDown()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
index db41d8d..98ff8d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt
@@ -16,27 +16,43 @@
 
 package com.android.systemui.controls.management
 
+import android.Manifest
 import android.content.ComponentName
 import android.content.Context
 import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
 import android.content.pm.ServiceInfo
+import android.os.Bundle
 import android.os.UserHandle
+import android.service.controls.ControlsProviderService
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.settingslib.applications.ServiceListing
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.USE_APP_PANELS
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argThat
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
@@ -51,10 +67,8 @@
 class ControlsListingControllerImplTest : SysuiTestCase() {
 
     companion object {
-        private const val TEST_LABEL = "TEST_LABEL"
-        private const val TEST_PERMISSION = "permission"
-        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-        fun <T> any(): T = Mockito.any<T>()
+        private const val FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or
+                PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong()
     }
 
     @Mock
@@ -63,15 +77,17 @@
     private lateinit var mockCallback: ControlsListingController.ControlsListingCallback
     @Mock
     private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback
-    @Mock
-    private lateinit var serviceInfo: ServiceInfo
-    @Mock
-    private lateinit var serviceInfo2: ServiceInfo
     @Mock(stubOnly = true)
     private lateinit var userTracker: UserTracker
+    @Mock(stubOnly = true)
+    private lateinit var dumpManager: DumpManager
+    @Mock
+    private lateinit var packageManager: PackageManager
+    @Mock
+    private lateinit var featureFlags: FeatureFlags
 
-    private var componentName = ComponentName("pkg1", "class1")
-    private var componentName2 = ComponentName("pkg2", "class2")
+    private var componentName = ComponentName("pkg", "class1")
+    private var activityName = ComponentName("pkg", "activity")
 
     private val executor = FakeExecutor(FakeSystemClock())
 
@@ -87,9 +103,15 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(serviceInfo.componentName).thenReturn(componentName)
-        `when`(serviceInfo2.componentName).thenReturn(componentName2)
         `when`(userTracker.userId).thenReturn(user)
+        `when`(userTracker.userContext).thenReturn(context)
+        // Return disabled by default
+        `when`(packageManager.getComponentEnabledSetting(any()))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED)
+        mContext.setMockPackageManager(packageManager)
+
+        // Return true by default, we'll test the false path
+        `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true)
 
         val wrapper = object : ContextWrapper(mContext) {
             override fun createContextAsUser(user: UserHandle, flags: Int): Context {
@@ -97,7 +119,14 @@
             }
         }
 
-        controller = ControlsListingControllerImpl(wrapper, executor, { mockSL }, userTracker)
+        controller = ControlsListingControllerImpl(
+                wrapper,
+                executor,
+                { mockSL },
+                userTracker,
+                dumpManager,
+                featureFlags
+        )
         verify(mockSL).addCallback(capture(serviceListingCallbackCaptor))
     }
 
@@ -123,9 +152,16 @@
             Unit
         }
         `when`(mockServiceListing.reload()).then {
-            callback?.onServicesReloaded(listOf(serviceInfo))
+            callback?.onServicesReloaded(listOf(ServiceInfo(componentName)))
         }
-        ControlsListingControllerImpl(mContext, exec, { mockServiceListing }, userTracker)
+        ControlsListingControllerImpl(
+                mContext,
+                exec,
+                { mockServiceListing },
+                userTracker,
+                dumpManager,
+                featureFlags
+        )
     }
 
     @Test
@@ -148,7 +184,7 @@
 
     @Test
     fun testCallbackGetsList() {
-        val list = listOf(serviceInfo)
+        val list = listOf(ServiceInfo(componentName))
         controller.addCallback(mockCallback)
         controller.addCallback(mockCallbackOther)
 
@@ -188,6 +224,8 @@
 
     @Test
     fun testChangeUserSendsCorrectServiceUpdate() {
+        val serviceInfo = ServiceInfo(componentName)
+
         val list = listOf(serviceInfo)
         controller.addCallback(mockCallback)
 
@@ -223,4 +261,297 @@
         verify(mockCallback).onServicesUpdated(capture(captor))
         assertEquals(0, captor.value.size)
     }
+
+    @Test
+    fun test_nullPanelActivity() {
+        val list = listOf(ServiceInfo(componentName))
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testNoActivity_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityWithoutPermission_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(ActivityInfo(activityName)))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityPermissionNotExported_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(
+                ActivityInfo(activityName, permission = Manifest.permission.BIND_CONTROLS)
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDisabled_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityEnabled_correctPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertEquals(activityName, controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultEnabled_correctPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertEquals(activityName, controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultDisabled_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = false,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDefaultEnabled_flagDisabled_nullPanel() {
+        `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(false)
+        val serviceInfo = ServiceInfo(
+                componentName,
+                activityName,
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testActivityDifferentPackage_nullPanel() {
+        val serviceInfo = ServiceInfo(
+                componentName,
+                ComponentName("other_package", "cls")
+        )
+
+        `when`(packageManager.getComponentEnabledSetting(eq(activityName)))
+                .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+
+        setUpQueryResult(listOf(
+                ActivityInfo(
+                        activityName,
+                        enabled = true,
+                        exported = true,
+                        permission = Manifest.permission.BIND_CONTROLS
+                )
+        ))
+
+        val list = listOf(serviceInfo)
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        executor.runAllReady()
+
+        assertNull(controller.getCurrentServices()[0].panelActivity)
+    }
+
+    @Test
+    fun testListingsNotModifiedByCallback() {
+        // This test checks that if the list passed to the callback is modified, it has no effect
+        // in the resulting services
+        val list = mutableListOf<ServiceInfo>()
+        serviceListingCallbackCaptor.value.onServicesReloaded(list)
+
+        list.add(ServiceInfo(ComponentName("a", "b")))
+        executor.runAllReady()
+
+        assertTrue(controller.getCurrentServices().isEmpty())
+    }
+
+    private fun ServiceInfo(
+            componentName: ComponentName,
+            panelActivityComponentName: ComponentName? = null
+    ): ServiceInfo {
+        return ServiceInfo().apply {
+            packageName = componentName.packageName
+            name = componentName.className
+            panelActivityComponentName?.let {
+                metaData = Bundle().apply {
+                    putString(
+                            ControlsProviderService.META_DATA_PANEL_ACTIVITY,
+                            it.flattenToShortString()
+                    )
+                }
+            }
+        }
+    }
+
+    private fun ActivityInfo(
+        componentName: ComponentName,
+        exported: Boolean = false,
+        enabled: Boolean = true,
+        permission: String? = null
+    ): ActivityInfo {
+        return ActivityInfo().apply {
+            packageName = componentName.packageName
+            name = componentName.className
+            this.permission = permission
+            this.exported = exported
+            this.enabled = enabled
+        }
+    }
+
+    private fun setUpQueryResult(infos: List<ActivityInfo>) {
+        `when`(
+                packageManager.queryIntentActivitiesAsUser(
+                        argThat(IntentMatcher(activityName)),
+                        argThat(FlagsMatcher(FLAGS)),
+                        eq(UserHandle.of(user))
+                )
+        ).thenReturn(infos.map {
+            ResolveInfo().apply { activityInfo = it }
+        })
+    }
+
+    private class IntentMatcher(
+            private val componentName: ComponentName
+    ) : ArgumentMatcher<Intent> {
+        override fun matches(argument: Intent?): Boolean {
+            return argument?.component == componentName
+        }
+    }
+
+    private class FlagsMatcher(
+            private val flags: Long
+    ) : ArgumentMatcher<PackageManager.ResolveInfoFlags> {
+        override fun matches(argument: PackageManager.ResolveInfoFlags?): Boolean {
+            return flags == argument?.value
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
new file mode 100644
index 0000000..56c3efe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsProviderSelectorActivityTest.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.controls.management
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ControlsProviderSelectorActivityTest : SysuiTestCase() {
+    @Main private val executor: Executor = MoreExecutors.directExecutor()
+
+    @Background private val backExecutor: Executor = MoreExecutors.directExecutor()
+
+    @Mock lateinit var listingController: ControlsListingController
+
+    @Mock lateinit var controlsController: ControlsController
+
+    @Mock lateinit var userTracker: UserTracker
+
+    @Mock lateinit var uiController: ControlsUiController
+
+    private lateinit var controlsProviderSelectorActivity: ControlsProviderSelectorActivity_Factory
+    private var latch: CountDownLatch = CountDownLatch(1)
+
+    @Mock private lateinit var mockDispatcher: OnBackInvokedDispatcher
+    @Captor private lateinit var captureCallback: ArgumentCaptor<OnBackInvokedCallback>
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object :
+                SingleActivityFactory<TestableControlsProviderSelectorActivity>(
+                    TestableControlsProviderSelectorActivity::class.java
+                ) {
+                override fun create(intent: Intent?): TestableControlsProviderSelectorActivity {
+                    return TestableControlsProviderSelectorActivity(
+                        executor,
+                        backExecutor,
+                        listingController,
+                        controlsController,
+                        userTracker,
+                        uiController,
+                        mockDispatcher,
+                        latch
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val intent = Intent()
+        intent.putExtra(ControlsProviderSelectorActivity.BACK_SHOULD_EXIT, true)
+        activityRule.launchActivity(intent)
+    }
+
+    @Test
+    fun testBackCallbackRegistrationAndUnregistration() {
+        // 1. ensure that launching the activity results in it registering a callback
+        verify(mockDispatcher)
+            .registerOnBackInvokedCallback(
+                ArgumentMatchers.eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT),
+                captureCallback.capture()
+            )
+        activityRule.finishActivity()
+        latch.await() // ensure activity is finished
+        // 2. ensure that when the activity is finished, it unregisters the same callback
+        verify(mockDispatcher).unregisterOnBackInvokedCallback(captureCallback.value)
+    }
+
+    public class TestableControlsProviderSelectorActivity(
+        executor: Executor,
+        backExecutor: Executor,
+        listingController: ControlsListingController,
+        controlsController: ControlsController,
+        userTracker: UserTracker,
+        uiController: ControlsUiController,
+        private val mockDispatcher: OnBackInvokedDispatcher,
+        private val latch: CountDownLatch
+    ) :
+        ControlsProviderSelectorActivity(
+            executor,
+            backExecutor,
+            listingController,
+            controlsController,
+            userTracker,
+            uiController
+        ) {
+        override fun getOnBackInvokedDispatcher(): OnBackInvokedDispatcher {
+            return mockDispatcher
+        }
+
+        override fun onStop() {
+            super.onStop()
+            // ensures that test runner thread does not proceed until ui thread is done
+            latch.countDown()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
index efb3db7..314b176 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestDialogTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.controller.ControlInfo
 import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import org.junit.After
@@ -46,9 +47,10 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
+import java.util.concurrent.Executor
 
 @MediumTest
 @RunWith(AndroidTestingRunner::class)
@@ -67,6 +69,10 @@
     private lateinit var controller: ControlsController
 
     @Mock
+    private lateinit var mainExecutor: Executor
+    @Mock
+    private lateinit var userTracker: UserTracker
+    @Mock
     private lateinit var listingController: ControlsListingController
     @Mock
     private lateinit var iIntentSender: IIntentSender
@@ -81,8 +87,9 @@
             ) {
                     override fun create(intent: Intent?): TestControlsRequestDialog {
                         return TestControlsRequestDialog(
+                                mainExecutor,
                                 controller,
-                                fakeBroadcastDispatcher,
+                                userTracker,
                                 listingController
                         )
                     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt
index 3f6308b..ec239f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/TestControlsRequestDialog.kt
@@ -16,11 +16,13 @@
 
 package com.android.systemui.controls.management
 
-import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.settings.UserTracker
+import java.util.concurrent.Executor
 
 class TestControlsRequestDialog(
+    mainExecutor: Executor,
     controller: ControlsController,
-    dispatcher: BroadcastDispatcher,
+    userTracker: UserTracker,
     listingController: ControlsListingController
-) : ControlsRequestDialog(controller, dispatcher, listingController)
\ No newline at end of file
+) : ControlsRequestDialog(mainExecutor, controller, userTracker, listingController)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
new file mode 100644
index 0000000..49c7442
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.controls.ui
+
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsMetricsLogger
+import com.android.systemui.controls.CustomIconCache
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ControlsUiControllerImplTest : SysuiTestCase() {
+    @Mock lateinit var controlsController: ControlsController
+    @Mock lateinit var controlsListingController: ControlsListingController
+    @Mock lateinit var controlActionCoordinator: ControlActionCoordinator
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock lateinit var shadeController: ShadeController
+    @Mock lateinit var iconCache: CustomIconCache
+    @Mock lateinit var controlsMetricsLogger: ControlsMetricsLogger
+    @Mock lateinit var keyguardStateController: KeyguardStateController
+    @Mock lateinit var userFileManager: UserFileManager
+    @Mock lateinit var userTracker: UserTracker
+    val sharedPreferences = FakeSharedPreferences()
+
+    var uiExecutor = FakeExecutor(FakeSystemClock())
+    var bgExecutor = FakeExecutor(FakeSystemClock())
+    lateinit var underTest: ControlsUiControllerImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            ControlsUiControllerImpl(
+                Lazy { controlsController },
+                context,
+                uiExecutor,
+                bgExecutor,
+                Lazy { controlsListingController },
+                controlActionCoordinator,
+                activityStarter,
+                shadeController,
+                iconCache,
+                controlsMetricsLogger,
+                keyguardStateController,
+                userFileManager,
+                userTracker
+            )
+        `when`(
+                userFileManager.getSharedPreferences(
+                    DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+                    0,
+                    0
+                )
+            )
+            .thenReturn(sharedPreferences)
+        `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt()))
+            .thenReturn(sharedPreferences)
+        `when`(userTracker.userId).thenReturn(0)
+    }
+
+    @Test
+    fun testGetPreferredStructure() {
+        val structureInfo = mock(StructureInfo::class.java)
+        underTest.getPreferredStructure(listOf(structureInfo))
+        verify(userFileManager, times(2))
+            .getSharedPreferences(
+                fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+                mode = 0,
+                userId = 0
+            )
+    }
+
+    @Test
+    fun testGetPreferredStructure_differentUserId() {
+        val structureInfo =
+            listOf(
+                StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()),
+                StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()),
+            )
+        sharedPreferences
+            .edit()
+            .putString("controls_component", structureInfo[0].componentName.flattenToString())
+            .putString("controls_structure", structureInfo[0].structure.toString())
+            .commit()
+
+        val differentSharedPreferences = FakeSharedPreferences()
+        differentSharedPreferences
+            .edit()
+            .putString("controls_component", structureInfo[1].componentName.flattenToString())
+            .putString("controls_structure", structureInfo[1].structure.toString())
+            .commit()
+
+        val previousPreferredStructure = underTest.getPreferredStructure(structureInfo)
+
+        `when`(
+                userFileManager.getSharedPreferences(
+                    DeviceControlsControllerImpl.PREFS_CONTROLS_FILE,
+                    0,
+                    1
+                )
+            )
+            .thenReturn(differentSharedPreferences)
+        `when`(userTracker.userId).thenReturn(1)
+
+        val currentPreferredStructure = underTest.getPreferredStructure(structureInfo)
+
+        assertThat(previousPreferredStructure).isEqualTo(structureInfo[0])
+        assertThat(currentPreferredStructure).isEqualTo(structureInfo[1])
+        assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
index f933361..93a1868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/decor/RoundedCornerResDelegateTest.kt
@@ -24,12 +24,11 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.R as InternalR
 import com.android.systemui.R as SystemUIR
-import com.android.systemui.tests.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.tests.R
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
-
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
@@ -102,14 +101,11 @@
         assertEquals(Size(3, 3), roundedCornerResDelegate.topRoundedSize)
         assertEquals(Size(4, 4), roundedCornerResDelegate.bottomRoundedSize)
 
-        setupResources(radius = 100,
-                roundedTopDrawable = getTestsDrawable(R.drawable.rounded4px),
-                roundedBottomDrawable = getTestsDrawable(R.drawable.rounded5px))
-
+        roundedCornerResDelegate.physicalPixelDisplaySizeRatio = 2f
         roundedCornerResDelegate.updateDisplayUniqueId(null, 1)
 
-        assertEquals(Size(4, 4), roundedCornerResDelegate.topRoundedSize)
-        assertEquals(Size(5, 5), roundedCornerResDelegate.bottomRoundedSize)
+        assertEquals(Size(6, 6), roundedCornerResDelegate.topRoundedSize)
+        assertEquals(Size(8, 8), roundedCornerResDelegate.bottomRoundedSize)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 6a55a60..5bbd810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.doze;
 
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED;
@@ -38,16 +41,17 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
 import android.view.Display;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -78,25 +82,30 @@
     @Mock
     private DozeHost mHost;
     @Mock
-    private UiModeManager mUiModeManager;
+    private DozeMachine.Part mPartMock;
+    @Mock
+    private DozeMachine.Part mAnotherPartMock;
     private DozeServiceFake mServiceFake;
     private WakeLockFake mWakeLockFake;
-    private AmbientDisplayConfiguration mConfigMock;
-    private DozeMachine.Part mPartMock;
+    private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mServiceFake = new DozeServiceFake();
         mWakeLockFake = new WakeLockFake();
-        mConfigMock = mock(AmbientDisplayConfiguration.class);
-        mPartMock = mock(DozeMachine.Part.class);
+        mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
         when(mDockManager.isDocked()).thenReturn(false);
         when(mDockManager.isHidden()).thenReturn(false);
 
-        mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake,
-                mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager,
-                mHost, new DozeMachine.Part[]{mPartMock});
+        mMachine = new DozeMachine(mServiceFake,
+                mAmbientDisplayConfigMock,
+                mWakeLockFake,
+                mWakefulnessLifecycle,
+                mDozeLog,
+                mDockManager,
+                mHost,
+                new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
     }
 
     @Test
@@ -108,7 +117,7 @@
 
     @Test
     public void testInitialize_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -118,7 +127,7 @@
 
     @Test
     public void testInitialize_goesToAod() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -138,7 +147,7 @@
 
     @Test
     public void testInitialize_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -151,7 +160,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -162,7 +171,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -184,7 +193,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -197,7 +206,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -209,7 +218,7 @@
 
     @Test
     public void testPulseDone_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -222,7 +231,7 @@
 
     @Test
     public void testPulseDone_goesToAoD() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -236,7 +245,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -287,7 +296,7 @@
 
     @Test
     public void testPulseDone_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -303,7 +312,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -471,7 +480,9 @@
 
     @Test
     public void testTransitionToInitialized_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
 
         verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
@@ -481,7 +492,9 @@
 
     @Test
     public void testTransitionToFinish_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(FINISH);
 
@@ -490,7 +503,9 @@
 
     @Test
     public void testDozeToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE);
 
@@ -499,7 +514,9 @@
 
     @Test
     public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD);
 
@@ -508,7 +525,9 @@
 
     @Test
     public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_PULSING_BRIGHT);
 
@@ -517,7 +536,9 @@
 
     @Test
     public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD_DOCKED);
 
@@ -525,7 +546,35 @@
     }
 
     @Test
+    public void testOnConfigurationChanged_propagatesUiModeTypeToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
+    public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
     public void testDozeSuppressTriggers_screenState() {
         assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null));
     }
+
+    @NonNull
+    private Configuration configWithCarNightUiMode() {
+        Configuration configuration = Configuration.EMPTY;
+        configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES;
+        return configuration;
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index b33f9a7..07d7e79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -51,6 +51,7 @@
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.doze.DozeSensors.TriggerSensor;
 import com.android.systemui.plugins.SensorManagerPlugin;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -99,6 +100,8 @@
     @Mock
     private DevicePostureController mDevicePostureController;
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private ProximitySensor mProximitySensor;
 
     // Capture listeners so that they can be used to send events
@@ -425,10 +428,10 @@
 
     @Test
     public void testGesturesAllInitiallyRespectSettings() {
-        DozeSensors dozeSensors = new DozeSensors(getContext(), mSensorManager, mDozeParameters,
+        DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
                 mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                 mProximitySensor, mFakeSettings, mAuthController,
-                mDevicePostureController);
+                mDevicePostureController, mUserTracker);
 
         for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
             assertFalse(sensor.mIgnoresSetting);
@@ -437,10 +440,10 @@
 
     private class TestableDozeSensors extends DozeSensors {
         TestableDozeSensors() {
-            super(getContext(), mSensorManager, mDozeParameters,
+            super(mSensorManager, mDozeParameters,
                     mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                     mProximitySensor, mFakeSettings, mAuthController,
-                    mDevicePostureController);
+                    mDevicePostureController, mUserTracker);
             for (TriggerSensor sensor : mTriggerSensors) {
                 if (sensor instanceof PluginSensor
                         && ((PluginSensor) sensor).mPluginSensor.getType()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 0f29dcd..32b9945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -10,14 +10,14 @@
  * 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 andatest
+ * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL;
 
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
@@ -26,17 +26,16 @@
 import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
 import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
 
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
@@ -44,13 +43,13 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -71,10 +70,6 @@
     @Mock
     private AmbientDisplayConfiguration mConfig;
     @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
-    private UiModeManager mUiModeManager;
-    @Mock
     private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock
     private BiometricUnlockController mBiometricUnlockController;
@@ -83,13 +78,6 @@
     private DozeMachine mDozeMachine;
 
     @Captor
-    private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
-    @Captor
-    private ArgumentCaptor<IntentFilter> mIntentFilterCaptor;
-    private BroadcastReceiver mBroadcastReceiver;
-    private IntentFilter mIntentFilter;
-
-    @Captor
     private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor;
     private DozeHost.Callback mDozeHostCallback;
 
@@ -106,8 +94,6 @@
                 mDozeHost,
                 mConfig,
                 mDozeLog,
-                mBroadcastDispatcher,
-                mUiModeManager,
                 mBiometricUnlockControllerLazy);
 
         mDozeSuppressor.setDozeMachine(mDozeMachine);
@@ -122,36 +108,35 @@
     public void testRegistersListenersOnInitialized_unregisteredOnFinish() {
         // check that receivers and callbacks registered
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         captureDozeHostCallback();
 
         // check that receivers and callbacks are unregistered
         mDozeSuppressor.transitionTo(INITIALIZED, FINISH);
-        verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver);
         verify(mDozeHost).removeCallback(mDozeHostCallback);
     }
 
     @Test
     public void testSuspendTriggersDoze_carMode() {
         // GIVEN car mode
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // WHEN dozing begins
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
 
         // THEN doze continues with all doze triggers disabled.
-        verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
     }
 
     @Test
     public void testSuspendTriggersDoze_enterCarMode() {
         // GIVEN currently dozing
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE);
 
         // WHEN car mode entered
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // THEN doze continues with all doze triggers disabled.
         verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
@@ -160,13 +145,13 @@
     @Test
     public void testDozeResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze is resumed
         verify(mDozeMachine).requestState(DOZE);
@@ -175,19 +160,53 @@
     @Test
     public void testDozeAoDResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze AOD is resumed
         verify(mDozeMachine).requestState(DOZE_AOD);
     }
 
     @Test
+    public void testUiModeDoesNotChange_noStateTransition() {
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        clearInvocations(mDozeMachine);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
+    }
+
+    @Test
+    public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, never()).requestState(any());
+    }
+
+    @Test
+    public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mDozeMachine, never()).requestState(any());
+
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+    }
+
+    @Test
     public void testEndDoze_unprovisioned() {
         // GIVEN device unprovisioned
         when(mDozeHost.isProvisioned()).thenReturn(false);
@@ -276,14 +295,4 @@
         verify(mDozeHost).addCallback(mDozeHostCaptor.capture());
         mDozeHostCallback = mDozeHostCaptor.getValue();
     }
-
-    private void captureBroadcastReceiver() {
-        verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(),
-                mIntentFilterCaptor.capture());
-        mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
-        mIntentFilter = mIntentFilterCaptor.getValue();
-        assertEquals(2, mIntentFilter.countActions());
-        org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(),
-                containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE));
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 781dc15..82432ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -23,10 +23,10 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -49,6 +49,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.DozeTriggers.DozingUpdateUiEvent;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -88,6 +89,8 @@
     @Mock
     private ProximityCheck mProximityCheck;
     @Mock
+    private DozeLog mDozeLog;
+    @Mock
     private AuthController mAuthController;
     @Mock
     private UiEventLogger mUiEventLogger;
@@ -96,6 +99,8 @@
     @Mock
     private DevicePostureController mDevicePostureController;
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private SessionTracker mSessionTracker;
 
     private DozeTriggers mTriggers;
@@ -127,9 +132,9 @@
 
         mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters,
                 asyncSensorManager, wakeLock, mDockManager, mProximitySensor,
-                mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(),
+                mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(),
                 mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController,
-                mDevicePostureController);
+                mDevicePostureController, mUserTracker);
         mTriggers.setDozeMachine(mMachine);
         waitForSensorManager();
     }
@@ -342,6 +347,16 @@
         verify(mProximityCheck).destroy();
     }
 
+    @Test
+    public void testIsExecutingTransition_dropPulse() {
+        when(mHost.isPulsePending()).thenReturn(false);
+        when(mMachine.isExecutingTransition()).thenReturn(true);
+
+        mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null);
+
+        verify(mDozeLog).tracePulseDropped(anyString(), eq(null));
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
new file mode 100644
index 0000000..99406ed
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -0,0 +1,125 @@
+package com.android.systemui.dreams
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.statusbar.BlurUtils
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
+
+    companion object {
+        private const val DREAM_IN_BLUR_ANIMATION_DURATION = 1L
+        private const val DREAM_IN_BLUR_ANIMATION_DELAY = 2L
+        private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L
+        private const val DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = 4L
+        private const val DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = 5L
+        private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6
+        private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L
+        private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L
+        private const val DREAM_OUT_TRANSLATION_Y_DELAY_TOP = 9L
+        private const val DREAM_OUT_ALPHA_DURATION = 10L
+        private const val DREAM_OUT_ALPHA_DELAY_BOTTOM = 11L
+        private const val DREAM_OUT_ALPHA_DELAY_TOP = 12L
+        private const val DREAM_OUT_BLUR_DURATION = 13L
+    }
+
+    @Mock private lateinit var mockAnimator: AnimatorSet
+    @Mock private lateinit var blurUtils: BlurUtils
+    @Mock private lateinit var hostViewController: ComplicationHostViewController
+    @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController
+    @Mock private lateinit var stateController: DreamOverlayStateController
+    private lateinit var controller: DreamOverlayAnimationsController
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        controller =
+            DreamOverlayAnimationsController(
+                blurUtils,
+                hostViewController,
+                statusBarViewController,
+                stateController,
+                DREAM_IN_BLUR_ANIMATION_DURATION,
+                DREAM_IN_BLUR_ANIMATION_DELAY,
+                DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
+                DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY,
+                DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY,
+                DREAM_OUT_TRANSLATION_Y_DISTANCE,
+                DREAM_OUT_TRANSLATION_Y_DURATION,
+                DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM,
+                DREAM_OUT_TRANSLATION_Y_DELAY_TOP,
+                DREAM_OUT_ALPHA_DURATION,
+                DREAM_OUT_ALPHA_DELAY_BOTTOM,
+                DREAM_OUT_ALPHA_DELAY_TOP,
+                DREAM_OUT_BLUR_DURATION
+            )
+    }
+
+    @Test
+    fun testExitAnimationOnEnd() {
+        val mockCallback: () -> Unit = mock()
+
+        controller.startExitAnimations(
+            view = mock(),
+            doneCallback = mockCallback,
+            animatorBuilder = { mockAnimator }
+        )
+
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator).addListener(captor.capture())
+        val listener = captor.value
+
+        verify(mockCallback, never()).invoke()
+        listener.onAnimationEnd(mockAnimator)
+        verify(mockCallback, times(1)).invoke()
+    }
+
+    @Test
+    fun testCancellation() {
+        controller.startExitAnimations(
+            view = mock(),
+            doneCallback = mock(),
+            animatorBuilder = { mockAnimator }
+        )
+
+        verify(mockAnimator, never()).cancel()
+        controller.cancelAnimations()
+        verify(mockAnimator, times(1)).cancel()
+    }
+
+    @Test
+    fun testExitAfterStartWillCancel() {
+        val mockStartAnimator: AnimatorSet = mock()
+        val mockExitAnimator: AnimatorSet = mock()
+
+        controller.startEntryAnimations(view = mock(), animatorBuilder = { mockStartAnimator })
+
+        verify(mockStartAnimator, never()).cancel()
+
+        controller.startExitAnimations(
+            view = mock(),
+            doneCallback = mock(),
+            animatorBuilder = { mockExitAnimator }
+        )
+
+        // Verify that we cancelled the start animator in favor of the exit
+        // animator.
+        verify(mockStartAnimator, times(1)).cancel()
+        verify(mockExitAnimator, never()).cancel()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index c5a7de4..73c226d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -36,10 +36,10 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
-import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
+import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 
 import org.junit.Before;
@@ -90,7 +90,13 @@
     ViewRootImpl mViewRoot;
 
     @Mock
-    BouncerCallbackInteractor mBouncerCallbackInteractor;
+    PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
+
+    @Mock
+    DreamOverlayAnimationsController mAnimationsController;
+
+    @Mock
+    DreamOverlayStateController mStateController;
 
     DreamOverlayContainerViewController mController;
 
@@ -100,7 +106,7 @@
 
         when(mDreamOverlayContainerView.getResources()).thenReturn(mResources);
         when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
-        when(mStatusBarKeyguardViewManager.getBouncer()).thenReturn(mBouncer);
+        when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn(mBouncer);
         when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
 
         mController = new DreamOverlayContainerViewController(
@@ -115,7 +121,9 @@
                 MAX_BURN_IN_OFFSET,
                 BURN_IN_PROTECTION_UPDATE_INTERVAL,
                 MILLIS_UNTIL_FULL_JITTER,
-                mBouncerCallbackInteractor);
+                mPrimaryBouncerCallbackInteractor,
+                mAnimationsController,
+                mStateController);
     }
 
     @Test
@@ -159,8 +167,8 @@
 
     @Test
     public void testBouncerAnimation_doesNotApply() {
-        final ArgumentCaptor<BouncerExpansionCallback> bouncerExpansionCaptor =
-                ArgumentCaptor.forClass(BouncerExpansionCallback.class);
+        final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
+                ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
         mController.onViewAttached();
         verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
 
@@ -170,8 +178,8 @@
 
     @Test
     public void testBouncerAnimation_updateBlur() {
-        final ArgumentCaptor<BouncerExpansionCallback> bouncerExpansionCaptor =
-                ArgumentCaptor.forClass(BouncerExpansionCallback.class);
+        final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
+                ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
         mController.onViewAttached();
         verify(mBouncer).addBouncerExpansionCallback(bouncerExpansionCaptor.capture());
 
@@ -188,4 +196,31 @@
         verify(mBlurUtils).blurRadiusOfRatio(1 - scaledFraction);
         verify(mBlurUtils).applyBlur(mViewRoot, (int) blurRadius, false);
     }
+
+    @Test
+    public void testStartDreamEntryAnimationsOnAttachedNonLowLight() {
+        when(mStateController.isLowLightActive()).thenReturn(false);
+
+        mController.onViewAttached();
+
+        verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView);
+        verify(mAnimationsController, never()).cancelAnimations();
+    }
+
+    @Test
+    public void testNeverStartDreamEntryAnimationsOnAttachedForLowLight() {
+        when(mStateController.isLowLightActive()).thenReturn(true);
+
+        mController.onViewAttached();
+
+        verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView);
+    }
+
+    @Test
+    public void testCancelDreamEntryAnimationsOnDetached() {
+        mController.onViewAttached();
+        mController.onViewDetached();
+
+        verify(mAnimationsController).cancelAnimations();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index f370be1..ffb8342 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -253,6 +254,7 @@
         verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
         verify(mStateController).setOverlayActive(false);
         verify(mStateController).setLowLightActive(false);
+        verify(mStateController).setEntryAnimationsFinished(false);
     }
 
     @Test
@@ -273,27 +275,31 @@
 
     @Test
     public void testDecorViewNotAddedToWindowAfterDestroy() throws Exception {
-        when(mDreamOverlayContainerView.getParent())
-                .thenReturn(mDreamOverlayContainerViewParent)
-                .thenReturn(null);
-
         final IBinder proxy = mService.onBind(new Intent());
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
 
+        // Destroy the service.
+        mService.onDestroy();
+        mMainExecutor.runAllReady();
+
         // Inform the overlay service of dream starting.
         overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
                 false /*shouldShowComplication*/);
-
-        // Destroy the service.
-        mService.onDestroy();
-
-        // Run executor tasks.
         mMainExecutor.runAllReady();
 
         verify(mWindowManager, never()).addView(any(), any());
     }
 
     @Test
+    public void testNeverRemoveDecorViewIfNotAdded() {
+        // Service destroyed before dream started.
+        mService.onDestroy();
+        mMainExecutor.runAllReady();
+
+        verify(mWindowManager, never()).removeView(any());
+    }
+
+    @Test
     public void testResetCurrentOverlayWhenConnectedToNewDream() throws RemoteException {
         final IBinder proxy = mService.onBind(new Intent());
         final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
@@ -332,4 +338,28 @@
         verify(mDreamOverlayComponent).getDreamOverlayContainerViewController();
         verify(mDreamOverlayComponent).getDreamOverlayTouchMonitor();
     }
+
+    @Test
+    public void testWakeUp() throws RemoteException {
+        final IBinder proxy = mService.onBind(new Intent());
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT,
+                true /*shouldShowComplication*/);
+        mMainExecutor.runAllReady();
+
+        final Runnable callback = mock(Runnable.class);
+        mService.onWakeUp(callback);
+        mMainExecutor.runAllReady();
+        verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor);
+    }
+
+    @Test
+    public void testWakeUpBeforeStartDoesNothing() {
+        final Runnable callback = mock(Runnable.class);
+        mService.onWakeUp(callback);
+        mMainExecutor.runAllReady();
+        verify(mDreamOverlayContainerViewController, never()).wakeUp(callback, mMainExecutor);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index d1d32a1..c21c7a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -234,4 +234,20 @@
         verify(mCallback, times(1)).onStateChanged();
         assertThat(stateController.isLowLightActive()).isTrue();
     }
+
+    @Test
+    public void testNotifyEntryAnimationsFinishedChanged() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor);
+
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        assertThat(stateController.areEntryAnimationsFinished()).isFalse();
+
+        stateController.setEntryAnimationsFinished(true);
+        mExecutor.runAllReady();
+
+        verify(mCallback, times(1)).onStateChanged();
+        assertThat(stateController.areEntryAnimationsFinished()).isTrue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index aa02178..85c2819 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -19,16 +19,20 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AlarmManager;
+import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.SensorPrivacyManager;
 import android.net.ConnectivityManager;
@@ -55,6 +59,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -69,7 +74,7 @@
             "{count, plural, =1 {# notification} other {# notifications}}";
 
     @Mock
-    DreamOverlayStatusBarView mView;
+    MockDreamOverlayStatusBarView mView;
     @Mock
     ConnectivityManager mConnectivityManager;
     @Mock
@@ -105,6 +110,9 @@
     @Mock
     DreamOverlayStateController mDreamOverlayStateController;
 
+    @Captor
+    private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
+
     private final Executor mMainExecutor = Runnable::run;
 
     DreamOverlayStatusBarViewController mController;
@@ -115,6 +123,8 @@
 
         when(mResources.getString(R.string.dream_overlay_status_bar_notification_indicator))
                 .thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING);
+        doCallRealMethod().when(mView).setVisibility(anyInt());
+        doCallRealMethod().when(mView).getVisibility();
 
         mController = new DreamOverlayStatusBarViewController(
                 mView,
@@ -454,12 +464,10 @@
     public void testStatusBarHiddenWhenSystemStatusBarShown() {
         mController.onViewAttached();
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING);
+        updateEntryAnimationsFinished();
+        updateStatusBarWindowState(true);
 
-        verify(mView).setVisibility(View.INVISIBLE);
+        assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE);
     }
 
     @Test
@@ -467,29 +475,43 @@
         mController.onViewAttached();
         reset(mView);
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN);
+        updateEntryAnimationsFinished();
+        updateStatusBarWindowState(false);
 
-        verify(mView).setVisibility(View.VISIBLE);
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
     public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() {
         mController.onViewAttached();
+        updateEntryAnimationsFinished();
         mController.onViewDetached();
         reset(mView);
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING);
+        updateStatusBarWindowState(true);
 
         verify(mView, never()).setVisibility(anyInt());
     }
 
     @Test
+    public void testNoChangeToVisibilityBeforeDreamStartedWhenStatusBarHidden() {
+        mController.onViewAttached();
+
+        // Trigger status bar window state change.
+        final StatusBarWindowStateListener listener = updateStatusBarWindowState(false);
+
+        // Verify no visibility change because dream not started.
+        verify(mView, never()).setVisibility(anyInt());
+
+        // Dream entry animations finished.
+        updateEntryAnimationsFinished();
+
+        // Trigger another status bar window state change, and verify visibility change.
+        listener.onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN);
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
     public void testExtraStatusBarItemSetWhenItemsChange() {
         mController.onViewAttached();
         when(mStatusBarItem.getView()).thenReturn(mStatusBarItemView);
@@ -507,16 +529,75 @@
     public void testLowLightHidesStatusBar() {
         when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true);
         mController.onViewAttached();
+        updateEntryAnimationsFinished();
 
-        verify(mView).setVisibility(View.INVISIBLE);
-        reset(mView);
-
-        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
         final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
                 ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
         verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
         callbackCapture.getValue().onStateChanged();
 
-        verify(mView).setVisibility(View.VISIBLE);
+        assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE);
+        reset(mView);
+
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        callbackCapture.getValue().onStateChanged();
+
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testNoChangeToVisibilityBeforeDreamStartedWhenLowLightStateChange() {
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        mController.onViewAttached();
+
+        // No change to visibility because dream not fully started.
+        verify(mView, never()).setVisibility(anyInt());
+
+        // Dream entry animations finished.
+        updateEntryAnimationsFinished();
+
+        // Trigger state change and verify visibility changed.
+        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onStateChanged();
+
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) {
+        when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show);
+        final ArgumentCaptor<StatusBarWindowStateListener>
+                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
+        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
+        final StatusBarWindowStateListener listener = callbackCapture.getValue();
+        listener.onStatusBarWindowStateChanged(show ? WINDOW_STATE_SHOWING : WINDOW_STATE_HIDDEN);
+        return listener;
+    }
+
+    private void updateEntryAnimationsFinished() {
+        when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true);
+
+        verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture());
+        final DreamOverlayStateController.Callback callback = mCallbackCaptor.getValue();
+        callback.onStateChanged();
+    }
+
+    private static class MockDreamOverlayStatusBarView extends DreamOverlayStatusBarView {
+        private int mVisibility = View.VISIBLE;
+
+        private MockDreamOverlayStatusBarView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void setVisibility(int visibility) {
+            mVisibility = visibility;
+        }
+
+        @Override
+        public int getVisibility() {
+            return mVisibility;
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
index 3b9e398..b477592 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
@@ -16,6 +16,7 @@
 package com.android.systemui.dreams.complication;
 
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -29,16 +30,18 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.DreamOverlayStateController;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 
 @SmallTest
@@ -77,9 +80,20 @@
     @Mock
     ComplicationLayoutParams mComplicationLayoutParams;
 
+    @Mock
+    DreamOverlayStateController mDreamOverlayStateController;
+
+    @Captor
+    private ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> mObserverCaptor;
+
+    @Captor
+    private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
+
     @Complication.Category
     static final int COMPLICATION_CATEGORY = Complication.CATEGORY_SYSTEM;
 
+    private ComplicationHostViewController mController;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -91,6 +105,15 @@
         when(mViewHolder.getCategory()).thenReturn(COMPLICATION_CATEGORY);
         when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams);
         when(mComplicationView.getParent()).thenReturn(mComplicationHostView);
+
+        mController = new ComplicationHostViewController(
+                mComplicationHostView,
+                mLayoutEngine,
+                mDreamOverlayStateController,
+                mLifecycleOwner,
+                mViewModel);
+
+        mController.init();
     }
 
     /**
@@ -98,25 +121,12 @@
      */
     @Test
     public void testViewModelObservation() {
-        final ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> observerArgumentCaptor =
-                ArgumentCaptor.forClass(Observer.class);
-        final ComplicationHostViewController controller = new ComplicationHostViewController(
-                mComplicationHostView,
-                mLayoutEngine,
-                mLifecycleOwner,
-                mViewModel);
-
-        controller.init();
-
-        verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner),
-                observerArgumentCaptor.capture());
-
         final Observer<Collection<ComplicationViewModel>> observer =
-                observerArgumentCaptor.getValue();
+                captureComplicationViewModelsObserver();
 
-        // Add complication and ensure it is added to the view.
+        // Add a complication and ensure it is added to the view.
         final HashSet<ComplicationViewModel> complications = new HashSet<>(
-                Arrays.asList(mComplicationViewModel));
+                Collections.singletonList(mComplicationViewModel));
         observer.onChanged(complications);
 
         verify(mLayoutEngine).addComplication(eq(mComplicationId), eq(mComplicationView),
@@ -127,4 +137,48 @@
 
         verify(mLayoutEngine).removeComplication(eq(mComplicationId));
     }
+
+    @Test
+    public void testNewComplicationsBeforeEntryAnimationsFinishSetToInvisible() {
+        final Observer<Collection<ComplicationViewModel>> observer =
+                captureComplicationViewModelsObserver();
+
+        // Add a complication before entry animations are finished.
+        final HashSet<ComplicationViewModel> complications = new HashSet<>(
+                Collections.singletonList(mComplicationViewModel));
+        observer.onChanged(complications);
+
+        // The complication view should be set to invisible.
+        verify(mComplicationView).setVisibility(View.INVISIBLE);
+    }
+
+    @Test
+    public void testNewComplicationsAfterEntryAnimationsFinishNotSetToInvisible() {
+        final Observer<Collection<ComplicationViewModel>> observer =
+                captureComplicationViewModelsObserver();
+
+        // Dream entry animations finished.
+        when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true);
+        final DreamOverlayStateController.Callback stateCallback = captureOverlayStateCallback();
+        stateCallback.onStateChanged();
+
+        // Add a complication after entry animations are finished.
+        final HashSet<ComplicationViewModel> complications = new HashSet<>(
+                Collections.singletonList(mComplicationViewModel));
+        observer.onChanged(complications);
+
+        // The complication view should not be set to invisible.
+        verify(mComplicationView, never()).setVisibility(View.INVISIBLE);
+    }
+
+    private Observer<Collection<ComplicationViewModel>> captureComplicationViewModelsObserver() {
+        verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner),
+                mObserverCaptor.capture());
+        return mObserverCaptor.getValue();
+    }
+
+    private DreamOverlayStateController.Callback captureOverlayStateCallback() {
+        verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture());
+        return mCallbackCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
index 849ac5e..7a2ba95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutEngineTest.java
@@ -347,21 +347,22 @@
 
         addComplication(engine, thirdViewInfo);
 
-        // The first added view should now be underneath the second view.
+        // The first added view should now be underneath the third view.
         verifyChange(firstViewInfo, false, lp -> {
             assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
             assertThat(lp.topMargin).isEqualTo(margin);
         });
 
-        // The second view should be in underneath the third view.
+        // The second view should be to the start of the third view.
         verifyChange(secondViewInfo, false, lp -> {
             assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
             assertThat(lp.getMarginEnd()).isEqualTo(margin);
         });
 
-        // The third view should be in at the top.
+        // The third view should be at the top end corner. No margin should be applied if not
+        // specified.
         verifyChange(thirdViewInfo, true, lp -> {
             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
@@ -425,14 +426,14 @@
 
         addComplication(engine, thirdViewInfo);
 
-        // The first added view should now be underneath the second view.
+        // The first added view should now be underneath the third view.
         verifyChange(firstViewInfo, false, lp -> {
             assertThat(lp.topToBottom == thirdViewInfo.view.getId()).isTrue();
             assertThat(lp.endToEnd == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
             assertThat(lp.topMargin).isEqualTo(complicationMargin);
         });
 
-        // The second view should be in underneath the third view.
+        // The second view should be to the start of the third view.
         verifyChange(secondViewInfo, false, lp -> {
             assertThat(lp.endToStart == thirdViewInfo.view.getId()).isTrue();
             assertThat(lp.topToTop == ConstraintLayout.LayoutParams.PARENT_ID).isTrue();
@@ -441,6 +442,69 @@
     }
 
     /**
+     * Ensures the root complication applies margin if specified.
+     */
+    @Test
+    public void testRootComplicationSpecifiedMargin() {
+        final int defaultMargin = 5;
+        final int complicationMargin = 10;
+        final ComplicationLayoutEngine engine =
+                new ComplicationLayoutEngine(mLayout, defaultMargin, mTouchSession, 0, 0);
+
+        final ViewInfo firstViewInfo = new ViewInfo(
+                new ComplicationLayoutParams(
+                        100,
+                        100,
+                        ComplicationLayoutParams.POSITION_TOP
+                                | ComplicationLayoutParams.POSITION_END,
+                        ComplicationLayoutParams.DIRECTION_DOWN,
+                        0),
+                Complication.CATEGORY_STANDARD,
+                mLayout);
+
+        addComplication(engine, firstViewInfo);
+
+        final ViewInfo secondViewInfo = new ViewInfo(
+                new ComplicationLayoutParams(
+                        100,
+                        100,
+                        ComplicationLayoutParams.POSITION_TOP
+                                | ComplicationLayoutParams.POSITION_END,
+                        ComplicationLayoutParams.DIRECTION_START,
+                        0),
+                Complication.CATEGORY_SYSTEM,
+                mLayout);
+
+        addComplication(engine, secondViewInfo);
+
+        firstViewInfo.clearInvocations();
+        secondViewInfo.clearInvocations();
+
+        final ViewInfo thirdViewInfo = new ViewInfo(
+                new ComplicationLayoutParams(
+                        100,
+                        100,
+                        ComplicationLayoutParams.POSITION_TOP
+                                | ComplicationLayoutParams.POSITION_END,
+                        ComplicationLayoutParams.DIRECTION_START,
+                        1,
+                        complicationMargin),
+                Complication.CATEGORY_SYSTEM,
+                mLayout);
+
+        addComplication(engine, thirdViewInfo);
+
+        // The third view is the root view and has specified margin, which should be applied based
+        // on its direction.
+        verifyChange(thirdViewInfo, true, lp -> {
+            assertThat(lp.getMarginStart()).isEqualTo(0);
+            assertThat(lp.getMarginEnd()).isEqualTo(complicationMargin);
+            assertThat(lp.topMargin).isEqualTo(0);
+            assertThat(lp.bottomMargin).isEqualTo(0);
+        });
+    }
+
+    /**
      * Ensures layout in a particular position updates.
      */
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
index cb7e47b..ce7561e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java
@@ -97,6 +97,31 @@
     }
 
     /**
+     * Ensures ComplicationLayoutParams correctly returns whether the complication specified margin.
+     */
+    @Test
+    public void testIsMarginSpecified() {
+        final ComplicationLayoutParams paramsNoMargin = new ComplicationLayoutParams(
+                100,
+                100,
+                ComplicationLayoutParams.POSITION_TOP
+                        | ComplicationLayoutParams.POSITION_START,
+                ComplicationLayoutParams.DIRECTION_DOWN,
+                0);
+        assertThat(paramsNoMargin.isMarginSpecified()).isFalse();
+
+        final ComplicationLayoutParams paramsWithMargin = new ComplicationLayoutParams(
+                100,
+                100,
+                ComplicationLayoutParams.POSITION_TOP
+                        | ComplicationLayoutParams.POSITION_START,
+                ComplicationLayoutParams.DIRECTION_DOWN,
+                0,
+                20 /*margin*/);
+        assertThat(paramsWithMargin.isMarginSpecified()).isTrue();
+    }
+
+    /**
      * Ensures unspecified margin uses default.
      */
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
index e099c92..ea16cb5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER;
@@ -63,6 +64,8 @@
                 .isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS);
         assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_SMARTSPACE))
                 .isEqualTo(COMPLICATION_TYPE_SMARTSPACE);
+        assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY))
+                .isEqualTo(COMPLICATION_TYPE_MEDIA_ENTRY);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index aa8c93e..30ad485 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -90,7 +90,10 @@
     private ActivityStarter mActivityStarter;
 
     @Mock
-    UiEventLogger mUiEventLogger;
+    private UiEventLogger mUiEventLogger;
+
+    @Captor
+    private ArgumentCaptor<DreamOverlayStateController.Callback> mStateCallbackCaptor;
 
     @Before
     public void setup() {
@@ -164,6 +167,29 @@
         verify(mDreamOverlayStateController).addComplication(mComplication);
     }
 
+    @Test
+    public void complicationAvailability_checkAvailabilityWhenDreamOverlayBecomesActive() {
+        final DreamHomeControlsComplication.Registrant registrant =
+                new DreamHomeControlsComplication.Registrant(mComplication,
+                        mDreamOverlayStateController, mControlsComponent);
+        registrant.start();
+
+        setServiceAvailable(true);
+        setHaveFavorites(false);
+
+        // Complication not available on start.
+        verify(mDreamOverlayStateController, never()).addComplication(mComplication);
+
+        // Favorite controls added, complication should be available now.
+        setHaveFavorites(true);
+
+        // Dream overlay becomes active.
+        setDreamOverlayActive(true);
+
+        // Verify complication is added.
+        verify(mDreamOverlayStateController).addComplication(mComplication);
+    }
+
     /**
      * Ensures clicking home controls chip logs UiEvent.
      */
@@ -196,10 +222,17 @@
 
     private void setServiceAvailable(boolean value) {
         final List<ControlsServiceInfo> serviceInfos = mock(List.class);
+        when(mControlsListingController.getCurrentServices()).thenReturn(serviceInfos);
         when(serviceInfos.isEmpty()).thenReturn(!value);
         triggerControlsListingCallback(serviceInfos);
     }
 
+    private void setDreamOverlayActive(boolean value) {
+        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(value);
+        verify(mDreamOverlayStateController).addCallback(mStateCallbackCaptor.capture());
+        mStateCallbackCaptor.getValue().onStateChanged();
+    }
+
     private void triggerControlsListingCallback(List<ControlsServiceInfo> serviceInfos) {
         verify(mControlsListingController).addCallback(mCallbackCaptor.capture());
         mCallbackCaptor.getValue().onServicesUpdated(serviceInfos);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index 522b5b5..0295030 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -16,8 +16,11 @@
 
 package com.android.systemui.dreams.complication;
 
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,8 +35,9 @@
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaCarouselController;
+import com.android.systemui.media.controls.ui.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -51,6 +55,9 @@
 @TestableLooper.RunWithLooper
 public class DreamMediaEntryComplicationTest extends SysuiTestCase {
     @Mock
+    private DreamMediaEntryComplicationComponent.Factory mComponentFactory;
+
+    @Mock
     private View mView;
 
     @Mock
@@ -89,6 +96,14 @@
         when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false);
     }
 
+    @Test
+    public void testGetRequiredTypeAvailability() {
+        final DreamMediaEntryComplication complication =
+                new DreamMediaEntryComplication(mComponentFactory);
+        assertThat(complication.getRequiredTypeAvailability()).isEqualTo(
+                COMPLICATION_TYPE_MEDIA_ENTRY);
+    }
+
     /**
      * Ensures clicking media entry chip adds/removes media complication.
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
index 14a5702..4e3aca7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.dreams.touch;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
@@ -33,6 +31,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.Complication;
 import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -52,6 +51,7 @@
 @RunWith(AndroidTestingRunner.class)
 public class HideComplicationTouchHandlerTest extends SysuiTestCase {
     private static final int RESTORE_TIMEOUT = 1000;
+    private static final int HIDE_DELAY = 500;
 
     @Mock
     Complication.VisibilityController mVisibilityController;
@@ -71,11 +71,18 @@
     @Mock
     DreamTouchHandler.TouchSession mSession;
 
-    FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+    @Mock
+    DreamOverlayStateController mStateController;
+
+    FakeSystemClock mClock;
+
+    FakeExecutor mFakeExecutor;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mClock = new FakeSystemClock();
+        mFakeExecutor = new FakeExecutor(mClock);
     }
 
     /**
@@ -86,10 +93,11 @@
         final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
                 mVisibilityController,
                 RESTORE_TIMEOUT,
+                HIDE_DELAY,
                 mTouchInsetManager,
                 mStatusBarKeyguardViewManager,
                 mFakeExecutor,
-                mHandler);
+                mStateController);
 
         // Report multiple active sessions.
         when(mSession.getActiveSessionCount()).thenReturn(2);
@@ -103,8 +111,10 @@
         // Verify session end.
         verify(mSession).pop();
 
+        mClock.advanceTime(HIDE_DELAY);
+
         // Verify no interaction with visibility controller.
-        verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean());
+        verify(mVisibilityController, never()).setVisibility(anyInt());
     }
 
     /**
@@ -115,10 +125,11 @@
         final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
                 mVisibilityController,
                 RESTORE_TIMEOUT,
+                HIDE_DELAY,
                 mTouchInsetManager,
                 mStatusBarKeyguardViewManager,
                 mFakeExecutor,
-                mHandler);
+                mStateController);
 
         // Report one session.
         when(mSession.getActiveSessionCount()).thenReturn(1);
@@ -132,8 +143,10 @@
         // Verify session end.
         verify(mSession).pop();
 
+        mClock.advanceTime(HIDE_DELAY);
+
         // Verify no interaction with visibility controller.
-        verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean());
+        verify(mVisibilityController, never()).setVisibility(anyInt());
     }
 
     /**
@@ -144,10 +157,11 @@
         final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
                 mVisibilityController,
                 RESTORE_TIMEOUT,
+                HIDE_DELAY,
                 mTouchInsetManager,
                 mStatusBarKeyguardViewManager,
                 mFakeExecutor,
-                mHandler);
+                mStateController);
 
         // Report one session
         when(mSession.getActiveSessionCount()).thenReturn(1);
@@ -177,8 +191,10 @@
         // Verify session ended.
         verify(mSession).pop();
 
+        mClock.advanceTime(HIDE_DELAY);
+
         // Verify no interaction with visibility controller.
-        verify(mVisibilityController, never()).setVisibility(anyInt(), anyBoolean());
+        verify(mVisibilityController, never()).setVisibility(anyInt());
     }
 
     /**
@@ -189,10 +205,11 @@
         final HideComplicationTouchHandler touchHandler = new HideComplicationTouchHandler(
                 mVisibilityController,
                 RESTORE_TIMEOUT,
+                HIDE_DELAY,
                 mTouchInsetManager,
                 mStatusBarKeyguardViewManager,
                 mFakeExecutor,
-                mHandler);
+                mStateController);
 
         // Report one session
         when(mSession.getActiveSessionCount()).thenReturn(1);
@@ -221,11 +238,11 @@
         inputEventListenerCaptor.getValue().onInputEvent(mMotionEvent);
         mFakeExecutor.runAllReady();
 
-        // Verify callback to restore visibility cancelled.
-        verify(mHandler).removeCallbacks(any());
-
+        // Verify visibility controller doesn't hide until after timeout
+        verify(mVisibilityController, never()).setVisibility(eq(View.INVISIBLE));
+        mClock.advanceTime(HIDE_DELAY);
         // Verify visibility controller told to hide complications.
-        verify(mVisibilityController).setVisibility(eq(View.INVISIBLE), anyBoolean());
+        verify(mVisibilityController).setVisibility(eq(View.INVISIBLE));
 
         Mockito.clearInvocations(mVisibilityController, mHandler);
 
@@ -235,11 +252,8 @@
         mFakeExecutor.runAllReady();
 
         // Verify visibility controller told to show complications.
-        ArgumentCaptor<Runnable> delayRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
-        verify(mHandler).postDelayed(delayRunnableCaptor.capture(),
-                eq(Long.valueOf(RESTORE_TIMEOUT)));
-        delayRunnableCaptor.getValue().run();
-        verify(mVisibilityController).setVisibility(eq(View.VISIBLE), anyBoolean());
+        mClock.advanceTime(RESTORE_TIMEOUT);
+        verify(mVisibilityController).setVisibility(eq(View.VISIBLE));
 
         // Verify session ended.
         verify(mSession).pop();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index fc67201..65ae90b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -17,11 +17,19 @@
 package com.android.systemui.dump
 
 import androidx.test.filters.SmallTest
+import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
+import com.android.systemui.ProtoDumpable
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.io.StringWriter
+import javax.inject.Provider
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -29,7 +37,6 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
 
 @SmallTest
 class DumpHandlerTest : SysuiTestCase() {
@@ -43,6 +50,8 @@
 
     @Mock
     private lateinit var pw: PrintWriter
+    @Mock
+    private lateinit var fd: FileDescriptor
 
     @Mock
     private lateinit var dumpable1: Dumpable
@@ -52,6 +61,11 @@
     private lateinit var dumpable3: Dumpable
 
     @Mock
+    private lateinit var protoDumpable1: ProtoDumpable
+    @Mock
+    private lateinit var protoDumpable2: ProtoDumpable
+
+    @Mock
     private lateinit var buffer1: LogBuffer
     @Mock
     private lateinit var buffer2: LogBuffer
@@ -66,7 +80,9 @@
             mContext,
             dumpManager,
             logBufferEulogizer,
-            mutableMapOf(),
+            mutableMapOf(
+                EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
+            ),
             exceptionHandlerManager
         )
     }
@@ -82,7 +98,7 @@
 
         // WHEN some of them are dumped explicitly
         val args = arrayOf("dumpable1", "dumpable3", "buffer2")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN only the requested ones have their dump() method called
         verify(dumpable1).dump(pw, args)
@@ -101,7 +117,7 @@
 
         // WHEN that module is dumped
         val args = arrayOf("dumpable1")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN its dump() method is called
         verify(dumpable1).dump(pw, args)
@@ -118,7 +134,7 @@
 
         // WHEN a critical dump is requested
         val args = arrayOf("--dump-priority", "CRITICAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all modules are dumped (but no buffers)
         verify(dumpable1).dump(pw, args)
@@ -139,7 +155,7 @@
 
         // WHEN a normal dump is requested
         val args = arrayOf("--dump-priority", "NORMAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all buffers are dumped (but no modules)
         verify(dumpable1, never()).dump(
@@ -154,4 +170,44 @@
         verify(buffer1).dump(pw, 0)
         verify(buffer2).dump(pw, 0)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testConfigDump() {
+        // GIVEN a StringPrintWriter
+        val stringWriter = StringWriter()
+        val spw = PrintWriter(stringWriter)
+
+        // When a config dump is requested
+        dumpHandler.dump(fd, spw, arrayOf("config"))
+
+        assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
+    }
+
+    @Test
+    fun testDumpAllProtoDumpables() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO)
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2).dumpProto(any(), eq(args))
+    }
+
+    @Test
+    fun testDumpSingleProtoDumpable() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO, "protoDumpable1")
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2, never()).dumpProto(any(), any())
+    }
+
+    private class EmptyCoreStartable : CoreStartable {
+        override fun start() {}
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index bd029a7..64547f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.dump
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogcatEchoTracker
 
 /**
  * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
index 318f2bc..170a70f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
-import java.lang.IllegalStateException
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,12 +28,12 @@
 @RunWith(AndroidTestingRunner::class)
 class FakeFeatureFlagsTest : SysuiTestCase() {
 
-    private val unreleasedFlag = UnreleasedFlag(-1000)
-    private val releasedFlag = ReleasedFlag(-1001)
-    private val stringFlag = StringFlag(-1002)
-    private val resourceBooleanFlag = ResourceBooleanFlag(-1003, resourceId = -1)
-    private val resourceStringFlag = ResourceStringFlag(-1004, resourceId = -1)
-    private val sysPropBooleanFlag = SysPropBooleanFlag(-1005, name = "test")
+    private val unreleasedFlag = UnreleasedFlag(-1000, "-1000", "test")
+    private val releasedFlag = ReleasedFlag(-1001, "-1001", "test")
+    private val stringFlag = StringFlag(-1002, "-1002", "test")
+    private val resourceBooleanFlag = ResourceBooleanFlag(-1003, "-1003", "test", resourceId = -1)
+    private val resourceStringFlag = ResourceStringFlag(-1004, "-1004", "test", resourceId = -1)
+    private val sysPropBooleanFlag = SysPropBooleanFlag(-1005, "test", "test")
 
     /**
      * FakeFeatureFlags does not honor any default values. All flags which are accessed must be
@@ -47,7 +46,7 @@
             assertThat(flags.isEnabled(Flags.TEAMFOOD)).isFalse()
             fail("Expected an exception when accessing an unspecified flag.")
         } catch (ex: IllegalStateException) {
-            assertThat(ex.message).contains("TEAMFOOD")
+            assertThat(ex.message).contains("id=1")
         }
         try {
             assertThat(flags.isEnabled(unreleasedFlag)).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
new file mode 100644
index 0000000..1e7b1f2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package com.android.systemui.flags
+
+import android.test.suitebuilder.annotation.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+/**
+ * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()!
+ */
+@SmallTest
+class FeatureFlagsDebugRestarterTest : SysuiTestCase() {
+    private lateinit var restarter: FeatureFlagsDebugRestarter
+
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var systemExitRestarter: SystemExitRestarter
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        restarter = FeatureFlagsDebugRestarter(wakefulnessLifecycle, systemExitRestarter)
+    }
+
+    @Test
+    fun testRestart_ImmediateWhenAsleep() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        restarter.restart()
+        verify(systemExitRestarter).restart()
+    }
+
+    @Test
+    fun testRestart_WaitsForSceenOff() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
+
+        restarter.restart()
+        verify(systemExitRestarter, never()).restart()
+
+        val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
+        verify(wakefulnessLifecycle).addObserver(captor.capture())
+
+        captor.value.onFinishedGoingToSleep()
+
+        verify(systemExitRestarter).restart()
+    }
+}
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..7592cc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import android.content.pm.PackageManager.NameNotFoundException
 import android.content.res.Resources
+import android.content.res.Resources.NotFoundException
 import android.test.suitebuilder.annotation.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -30,10 +31,6 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
-import java.io.PrintWriter
-import java.io.Serializable
-import java.io.StringWriter
-import java.util.function.Consumer
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
@@ -45,8 +42,12 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import java.io.PrintWriter
+import java.io.Serializable
+import java.io.StringWriter
+import java.util.function.Consumer
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * NOTE: This test is for the version of FeatureFlagManager in src-debug, which allows overriding
@@ -56,21 +57,32 @@
 class FeatureFlagsDebugTest : SysuiTestCase() {
     private lateinit var mFeatureFlagsDebug: FeatureFlagsDebug
 
-    @Mock private lateinit var flagManager: FlagManager
-    @Mock private lateinit var mockContext: Context
-    @Mock private lateinit var secureSettings: SecureSettings
-    @Mock private lateinit var systemProperties: SystemPropertiesHelper
-    @Mock private lateinit var resources: Resources
-    @Mock private lateinit var commandRegistry: CommandRegistry
-    @Mock private lateinit var restarter: Restarter
+    @Mock
+    private lateinit var flagManager: FlagManager
+    @Mock
+    private lateinit var mockContext: Context
+    @Mock
+    private lateinit var secureSettings: SecureSettings
+    @Mock
+    private lateinit var systemProperties: SystemPropertiesHelper
+    @Mock
+    private lateinit var resources: Resources
+    @Mock
+    private lateinit var commandRegistry: CommandRegistry
+    @Mock
+    private lateinit var restarter: Restarter
     private val flagMap = mutableMapOf<Int, Flag<*>>()
     private lateinit var broadcastReceiver: BroadcastReceiver
     private lateinit var clearCacheAction: Consumer<Int>
     private val serverFlagReader = ServerFlagReaderFake()
 
     private val deviceConfig = DeviceConfigProxyFake()
-    private val teamfoodableFlagA = UnreleasedFlag(500, true)
-    private val teamfoodableFlagB = ReleasedFlag(501, true)
+    private val teamfoodableFlagA = UnreleasedFlag(
+        500, name = "a", namespace = "test", teamfood = true
+    )
+    private val teamfoodableFlagB = ReleasedFlag(
+        501, name = "b", namespace = "test", teamfood = true
+    )
 
     @Before
     fun setup() {
@@ -83,15 +95,17 @@
             secureSettings,
             systemProperties,
             resources,
-            deviceConfig,
             serverFlagReader,
             flagMap,
             restarter
         )
+        mFeatureFlagsDebug.init()
         verify(flagManager).onSettingsChangedAction = any()
         broadcastReceiver = withArgCaptor {
-            verify(mockContext).registerReceiver(capture(), any(), nullable(), nullable(),
-                any())
+            verify(mockContext).registerReceiver(
+                capture(), any(), nullable(), nullable(),
+                any()
+            )
         }
         clearCacheAction = withArgCaptor {
             verify(flagManager).clearCacheAction = capture()
@@ -105,10 +119,42 @@
         whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
 
-        assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(2))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(3))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(ReleasedFlag(4))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(UnreleasedFlag(5))).isFalse()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ReleasedFlag(
+                    2,
+                    name = "2",
+                    namespace = "test"
+                )
+            )
+        ).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                UnreleasedFlag(
+                    3,
+                    name = "3",
+                    namespace = "test"
+                )
+            )
+        ).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ReleasedFlag(
+                    4,
+                    name = "3",
+                    namespace = "test"
+                )
+            )
+        ).isFalse()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                UnreleasedFlag(
+                    5,
+                    name = "4",
+                    namespace = "test"
+                )
+            )
+        ).isFalse()
     }
 
     @Test
@@ -136,9 +182,9 @@
     @Test
     fun teamFoodFlag_Overridden() {
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.id), any()))
-                .thenReturn(true)
+            .thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.id), any()))
-                .thenReturn(false)
+            .thenReturn(false)
         whenever(flagManager.readFlagValue<Boolean>(eq(1), any())).thenReturn(true)
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
@@ -159,17 +205,26 @@
         whenever(flagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
 
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(1, 1001))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, 1002))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, 1003))).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                ResourceBooleanFlag(
+                    1,
+                    "1",
+                    "test",
+                    1001
+                )
+            )
+        ).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, "2", "test", 1002))).isTrue()
+        assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, "3", "test", 1003))).isTrue()
 
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, 1004))
+            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, "4", "test", 1004))
         }
         // Test that resource is loaded (and validated) even when the setting is set.
         //  This prevents developers from not noticing when they reference an invalid resource.
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, 1005))
+            mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, "5", "test", 1005))
         }
     }
 
@@ -182,36 +237,30 @@
             return@thenAnswer it.getArgument(1)
         }
 
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(1, "a"))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(2, "b"))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(3, "c", true))).isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(4, "d", false))).isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(5, "e"))).isFalse()
-    }
-
-    @Test
-    fun readDeviceConfigBooleanFlag() {
-        val namespace = "test_namespace"
-        deviceConfig.setProperty(namespace, "a", "true", false)
-        deviceConfig.setProperty(namespace, "b", "false", false)
-        deviceConfig.setProperty(namespace, "c", null, false)
-
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(1, "a", namespace)))
-            .isTrue()
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(2, "b", namespace)))
-            .isFalse()
-        assertThat(mFeatureFlagsDebug.isEnabled(DeviceConfigBooleanFlag(3, "c", namespace)))
-            .isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(1, "a", "test"))).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(2, "b", "test"))).isTrue()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(3, "c", "test", true))).isTrue()
+        assertThat(
+            mFeatureFlagsDebug.isEnabled(
+                SysPropBooleanFlag(
+                    4,
+                    "d",
+                    "test",
+                    false
+                )
+            )
+        ).isFalse()
+        assertThat(mFeatureFlagsDebug.isEnabled(SysPropBooleanFlag(5, "e", "test"))).isFalse()
     }
 
     @Test
     fun readStringFlag() {
         whenever(flagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
         whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "biz"))).isEqualTo("biz")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "baz"))).isEqualTo("baz")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "buz"))).isEqualTo("foo")
-        assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "buz"))).isEqualTo("bar")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "1", "test", "biz"))).isEqualTo("biz")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "2", "test", "baz"))).isEqualTo("baz")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "3", "test", "buz"))).isEqualTo("foo")
+        assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "4", "test", "buz"))).isEqualTo("bar")
     }
 
     @Test
@@ -227,45 +276,109 @@
         whenever(flagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
         whenever(flagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
 
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(2, 1002))).isEqualTo("resource2")
-        assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(3, 1003))).isEqualTo("override3")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    1,
+                    "1",
+                    "test",
+                    1001
+                )
+            )
+        ).isEqualTo("")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    2,
+                    "2",
+                    "test",
+                    1002
+                )
+            )
+        ).isEqualTo("resource2")
+        assertThat(
+            mFeatureFlagsDebug.getString(
+                ResourceStringFlag(
+                    3,
+                    "3",
+                    "test",
+                    1003
+                )
+            )
+        ).isEqualTo("override3")
 
         Assert.assertThrows(NullPointerException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(4, 1004))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(4, "4", "test", 1004))
         }
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(5, 1005))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(5, "5", "test", 1005))
         }
         // Test that resource is loaded (and validated) even when the setting is set.
         //  This prevents developers from not noticing when they reference an invalid resource.
         Assert.assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsDebug.getString(ResourceStringFlag(6, 1005))
+            mFeatureFlagsDebug.getString(ResourceStringFlag(6, "6", "test", 1005))
+        }
+    }
+
+    @Test
+    fun readIntFlag() {
+        whenever(flagManager.readFlagValue<Int>(eq(3), any())).thenReturn(22)
+        whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(48)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(1, "1", "test", 12))).isEqualTo(12)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(2, "2", "test", 93))).isEqualTo(93)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(3, "3", "test", 8))).isEqualTo(22)
+        assertThat(mFeatureFlagsDebug.getInt(IntFlag(4, "4", "test", 234))).isEqualTo(48)
+    }
+
+    @Test
+    fun readResourceIntFlag() {
+        whenever(resources.getInteger(1001)).thenReturn(88)
+        whenever(resources.getInteger(1002)).thenReturn(61)
+        whenever(resources.getInteger(1003)).thenReturn(9342)
+        whenever(resources.getInteger(1004)).thenThrow(NotFoundException("unknown resource"))
+        whenever(resources.getInteger(1005)).thenThrow(NotFoundException("unknown resource"))
+        whenever(resources.getInteger(1006)).thenThrow(NotFoundException("unknown resource"))
+
+        whenever(flagManager.readFlagValue<Int>(eq(3), any())).thenReturn(20)
+        whenever(flagManager.readFlagValue<Int>(eq(4), any())).thenReturn(500)
+        whenever(flagManager.readFlagValue<Int>(eq(5), any())).thenReturn(9519)
+
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(1, "1", "test", 1001))).isEqualTo(88)
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(2, "2", "test", 1002))).isEqualTo(61)
+        assertThat(mFeatureFlagsDebug.getInt(ResourceIntFlag(3, "3", "test", 1003))).isEqualTo(20)
+
+        Assert.assertThrows(NotFoundException::class.java) {
+            mFeatureFlagsDebug.getInt(ResourceIntFlag(4, "4", "test", 1004))
+        }
+        // Test that resource is loaded (and validated) even when the setting is set.
+        //  This prevents developers from not noticing when they reference an invalid resource.
+        Assert.assertThrows(NotFoundException::class.java) {
+            mFeatureFlagsDebug.getInt(ResourceIntFlag(5, "5", "test", 1005))
         }
     }
 
     @Test
     fun broadcastReceiver_IgnoresInvalidData() {
-        addFlag(UnreleasedFlag(1))
-        addFlag(ResourceBooleanFlag(2, 1002))
-        addFlag(StringFlag(3, "flag3"))
-        addFlag(ResourceStringFlag(4, 1004))
+        addFlag(UnreleasedFlag(1, "1", "test"))
+        addFlag(ResourceBooleanFlag(2, "2", "test", 1002))
+        addFlag(StringFlag(3, "3", "test", "flag3"))
+        addFlag(ResourceStringFlag(4, "4", "test", 1004))
 
         broadcastReceiver.onReceive(mockContext, null)
         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)
     }
 
     @Test
     fun intentWithId_NoValueKeyClears() {
-        addFlag(UnreleasedFlag(1))
+        addFlag(UnreleasedFlag(1, name = "1", namespace = "test"))
 
         // trying to erase an id not in the map does nothing
         broadcastReceiver.onReceive(
@@ -284,10 +397,10 @@
 
     @Test
     fun setBooleanFlag() {
-        addFlag(UnreleasedFlag(1))
-        addFlag(UnreleasedFlag(2))
-        addFlag(ResourceBooleanFlag(3, 1003))
-        addFlag(ResourceBooleanFlag(4, 1004))
+        addFlag(UnreleasedFlag(1, "1", "test"))
+        addFlag(UnreleasedFlag(2, "2", "test"))
+        addFlag(ResourceBooleanFlag(3, "3", "test", 1003))
+        addFlag(ResourceBooleanFlag(4, "4", "test", 1004))
 
         setByBroadcast(1, false)
         verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
@@ -304,8 +417,8 @@
 
     @Test
     fun setStringFlag() {
-        addFlag(StringFlag(1, "flag1"))
-        addFlag(ResourceStringFlag(2, 1002))
+        addFlag(StringFlag(1, "flag1", "1", "test"))
+        addFlag(ResourceStringFlag(2, "2", "test", 1002))
 
         setByBroadcast(1, "override1")
         verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
@@ -316,7 +429,7 @@
 
     @Test
     fun setFlag_ClearsCache() {
-        val flag1 = addFlag(StringFlag(1, "flag1"))
+        val flag1 = addFlag(StringFlag(1, "1", "test", "flag1"))
         whenever(flagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
 
         // gets the flag & cache it
@@ -338,31 +451,31 @@
 
     @Test
     fun serverSide_Overrides_MakesFalse() {
-        val flag = ReleasedFlag(100)
+        val flag = ReleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, false)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, false)
 
         assertThat(mFeatureFlagsDebug.isEnabled(flag)).isFalse()
     }
 
     @Test
     fun serverSide_Overrides_MakesTrue() {
-        val flag = UnreleasedFlag(100)
+        val flag = UnreleasedFlag(100, name = "100", namespace = "test")
 
-        serverFlagReader.setFlagValue(flag.id, true)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
 
         assertThat(mFeatureFlagsDebug.isEnabled(flag)).isTrue()
     }
 
     @Test
     fun dumpFormat() {
-        val flag1 = ReleasedFlag(1)
-        val flag2 = ResourceBooleanFlag(2, 1002)
-        val flag3 = UnreleasedFlag(3)
-        val flag4 = StringFlag(4, "")
-        val flag5 = StringFlag(5, "flag5default")
-        val flag6 = ResourceStringFlag(6, 1006)
-        val flag7 = ResourceStringFlag(7, 1007)
+        val flag1 = ReleasedFlag(1, "1", "test")
+        val flag2 = ResourceBooleanFlag(2, "2", "test", 1002)
+        val flag3 = UnreleasedFlag(3, "3", "test")
+        val flag4 = StringFlag(4, "4", "test", "")
+        val flag5 = StringFlag(5, "5", "test", "flag5default")
+        val flag6 = ResourceStringFlag(6, "6", "test", 1006)
+        val flag7 = ResourceStringFlag(7, "7", "test", 1007)
 
         whenever(resources.getBoolean(1002)).thenReturn(true)
         whenever(resources.getString(1006)).thenReturn("resource1006")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
new file mode 100644
index 0000000..68ca48d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.
+ */
+package com.android.systemui.flags
+
+import android.test.suitebuilder.annotation.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+/**
+ * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()!
+ */
+@SmallTest
+class FeatureFlagsReleaseRestarterTest : SysuiTestCase() {
+    private lateinit var restarter: FeatureFlagsReleaseRestarter
+
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var batteryController: BatteryController
+    @Mock private lateinit var systemExitRestarter: SystemExitRestarter
+    private val executor = FakeExecutor(FakeSystemClock())
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        restarter =
+            FeatureFlagsReleaseRestarter(
+                wakefulnessLifecycle,
+                batteryController,
+                executor,
+                systemExitRestarter
+            )
+    }
+
+    @Test
+    fun testRestart_ScheduledWhenReady() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testRestart_RestartsWhenIdle() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+
+        restarter.restart()
+        verify(systemExitRestarter, never()).restart()
+        executor.advanceClockToLast()
+        executor.runAllReady()
+        verify(systemExitRestarter).restart()
+    }
+
+    @Test
+    fun testRestart_NotScheduledWhenAwake() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testRestart_NotScheduledWhenNotPluggedIn() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        whenever(batteryController.isPluggedIn).thenReturn(false)
+
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testRestart_NotDoubleSheduled() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+        restarter.restart()
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testWakefulnessLifecycle_CanRestart() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE)
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+
+        val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
+        verify(wakefulnessLifecycle).addObserver(captor.capture())
+
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+
+        captor.value.onFinishedGoingToSleep()
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testBatteryController_CanRestart() {
+        whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP)
+        whenever(batteryController.isPluggedIn).thenReturn(false)
+        assertThat(executor.numPending()).isEqualTo(0)
+        restarter.restart()
+
+        val captor =
+            ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java)
+        verify(batteryController).addCallback(captor.capture())
+
+        whenever(batteryController.isPluggedIn).thenReturn(true)
+
+        captor.value.onBatteryLevelChanged(0, true, true)
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+}
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..d5b5a4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -25,8 +25,8 @@
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow
@@ -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,14 +50,18 @@
             mResources,
             mSystemProperties,
             deviceConfig,
-            serverFlagReader)
+            serverFlagReader,
+            flagMap,
+            restarter)
     }
 
     @Test
     fun testBooleanResourceFlag() {
         val flagId = 213
         val flagResourceId = 3
-        val flag = ResourceBooleanFlag(flagId, flagResourceId)
+        val flagName = "213"
+        val flagNamespace = "test"
+        val flag = ResourceBooleanFlag(flagId, flagName, flagNamespace, flagResourceId)
         whenever(mResources.getBoolean(flagResourceId)).thenReturn(true)
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isTrue()
     }
@@ -68,57 +73,45 @@
         whenever(mResources.getString(1003)).thenReturn(null)
         whenever(mResources.getString(1004)).thenAnswer { throw NameNotFoundException() }
 
-        assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
-        assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(2, 1002))).isEqualTo("res2")
+        assertThat(mFeatureFlagsRelease.getString(
+            ResourceStringFlag(1, "1", "test", 1001))).isEqualTo("")
+        assertThat(mFeatureFlagsRelease.getString(
+            ResourceStringFlag(2, "2", "test", 1002))).isEqualTo("res2")
 
         assertThrows(NullPointerException::class.java) {
-            mFeatureFlagsRelease.getString(ResourceStringFlag(3, 1003))
+            mFeatureFlagsRelease.getString(ResourceStringFlag(3, "3", "test", 1003))
         }
         assertThrows(NameNotFoundException::class.java) {
-            mFeatureFlagsRelease.getString(ResourceStringFlag(4, 1004))
+            mFeatureFlagsRelease.getString(ResourceStringFlag(4, "4", "test", 1004))
         }
     }
 
     @Test
-    fun testReadDeviceConfigBooleanFlag() {
-        val namespace = "test_namespace"
-        deviceConfig.setProperty(namespace, "a", "true", false)
-        deviceConfig.setProperty(namespace, "b", "false", false)
-        deviceConfig.setProperty(namespace, "c", null, false)
-
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(1, "a", namespace)))
-            .isTrue()
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(2, "b", namespace)))
-            .isFalse()
-        assertThat(mFeatureFlagsRelease.isEnabled(DeviceConfigBooleanFlag(3, "c", namespace)))
-            .isFalse()
-    }
-
-    @Test
     fun testSysPropBooleanFlag() {
         val flagId = 213
         val flagName = "sys_prop_flag"
+        val flagNamespace = "test"
         val flagDefault = true
 
-        val flag = SysPropBooleanFlag(flagId, flagName, flagDefault)
+        val flag = SysPropBooleanFlag(flagId, flagName, flagNamespace, flagDefault)
         whenever(mSystemProperties.getBoolean(flagName, flagDefault)).thenReturn(flagDefault)
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isEqualTo(flagDefault)
     }
 
     @Test
     fun serverSide_OverridesReleased_MakesFalse() {
-        val flag = ReleasedFlag(100)
+        val flag = ReleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, false)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, false)
 
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isFalse()
     }
 
     @Test
     fun serverSide_OverridesUnreleased_Ignored() {
-        val flag = UnreleasedFlag(100)
+        val flag = UnreleasedFlag(100, "100", "test")
 
-        serverFlagReader.setFlagValue(flag.id, true)
+        serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
 
         assertThat(mFeatureFlagsRelease.isEnabled(flag)).isFalse()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index 4c61138..fea91c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
@@ -33,8 +33,10 @@
     @Mock private lateinit var featureFlags: FeatureFlagsDebug
     @Mock private lateinit var pw: PrintWriter
     private val flagMap = mutableMapOf<Int, Flag<*>>()
-    private val flagA = UnreleasedFlag(500)
-    private val flagB = ReleasedFlag(501)
+    private val flagA = UnreleasedFlag(500, "500", "test")
+    private val flagB = ReleasedFlag(501, "501", "test")
+    private val stringFlag = StringFlag(502, "502", "test", "abracadabra")
+    private val intFlag = IntFlag(503, "503", "test", 12)
 
     private lateinit var cmd: FlagCommand
 
@@ -44,34 +46,59 @@
 
         whenever(featureFlags.isEnabled(any(UnreleasedFlag::class.java))).thenReturn(false)
         whenever(featureFlags.isEnabled(any(ReleasedFlag::class.java))).thenReturn(true)
+        whenever(featureFlags.getString(any(StringFlag::class.java))).thenAnswer { invocation ->
+            (invocation.getArgument(0) as StringFlag).default
+        }
+        whenever(featureFlags.getInt(any(IntFlag::class.java))).thenAnswer { invocation ->
+            (invocation.getArgument(0) as IntFlag).default
+        }
+
         flagMap.put(flagA.id, flagA)
         flagMap.put(flagB.id, flagB)
+        flagMap.put(stringFlag.id, stringFlag)
+        flagMap.put(intFlag.id, intFlag)
 
         cmd = FlagCommand(featureFlags, flagMap)
     }
 
     @Test
-    fun noOpCommand() {
-        cmd.execute(pw, ArrayList())
-        Mockito.verify(pw, Mockito.atLeastOnce()).println()
-        Mockito.verify(featureFlags).isEnabled(flagA)
-        Mockito.verify(featureFlags).isEnabled(flagB)
-    }
-
-    @Test
-    fun readFlagCommand() {
+    fun readBooleanFlagCommand() {
         cmd.execute(pw, listOf(flagA.id.toString()))
         Mockito.verify(featureFlags).isEnabled(flagA)
     }
 
     @Test
-    fun setFlagCommand() {
+    fun readStringFlagCommand() {
+        cmd.execute(pw, listOf(stringFlag.id.toString()))
+        Mockito.verify(featureFlags).getString(stringFlag)
+    }
+
+    @Test
+    fun readIntFlag() {
+        cmd.execute(pw, listOf(intFlag.id.toString()))
+        Mockito.verify(featureFlags).getInt(intFlag)
+    }
+
+    @Test
+    fun setBooleanFlagCommand() {
         cmd.execute(pw, listOf(flagB.id.toString(), "on"))
         Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, true)
     }
 
     @Test
-    fun toggleFlagCommand() {
+    fun setStringFlagCommand() {
+        cmd.execute(pw, listOf(stringFlag.id.toString(), "set", "foobar"))
+        Mockito.verify(featureFlags).setStringFlagInternal(stringFlag, "foobar")
+    }
+
+    @Test
+    fun setIntFlag() {
+        cmd.execute(pw, listOf(intFlag.id.toString(), "put", "123"))
+        Mockito.verify(featureFlags).setIntFlagInternal(intFlag, 123)
+    }
+
+    @Test
+    fun toggleBooleanFlagCommand() {
         cmd.execute(pw, listOf(flagB.id.toString(), "toggle"))
         Mockito.verify(featureFlags).setBooleanFlagInternal(flagB, false)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
index 17324a0..fca7e96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
@@ -64,14 +64,14 @@
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // adding the first listener registers the observer
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // adding another listener does nothing
-        mFlagManager.addListener(ReleasedFlag(2), listener2)
+        mFlagManager.addListener(ReleasedFlag(2, "2", "test"), listener2)
         verifyNoMoreInteractions(mFlagSettingsHelper)
 
         // removing the original listener does nothing with second one still present
@@ -89,7 +89,7 @@
         val listener = mock<FlagListenable.Listener>()
         val clearCacheAction = mock<Consumer<Int>>()
         mFlagManager.clearCacheAction = clearCacheAction
-        mFlagManager.addListener(ReleasedFlag(1), listener)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
@@ -101,8 +101,8 @@
     fun testObserverInvokesListeners() {
         val listener1 = mock<FlagListenable.Listener>()
         val listener10 = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
-        mFlagManager.addListener(ReleasedFlag(10), listener10)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
         val observer = withArgCaptor<ContentObserver> {
             verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
         }
@@ -127,8 +127,8 @@
     fun testOnlySpecificFlagListenerIsInvoked() {
         val listener1 = mock<FlagListenable.Listener>()
         val listener10 = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener1)
-        mFlagManager.addListener(ReleasedFlag(10), listener10)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener1)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener10)
 
         mFlagManager.dispatchListenersAndMaybeRestart(1, null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
@@ -148,8 +148,8 @@
     @Test
     fun testSameListenerCanBeUsedForMultipleFlags() {
         val listener = mock<FlagListenable.Listener>()
-        mFlagManager.addListener(ReleasedFlag(1), listener)
-        mFlagManager.addListener(ReleasedFlag(10), listener)
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test"), listener)
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test"), listener)
 
         mFlagManager.dispatchListenersAndMaybeRestart(1, null)
         val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
@@ -177,7 +177,7 @@
     @Test
     fun testListenerCanSuppressRestart() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(1)) { event ->
+        mFlagManager.addListener(ReleasedFlag(1, "1", "test")) { event ->
             event.requestNoRestart()
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
@@ -188,7 +188,7 @@
     @Test
     fun testListenerOnlySuppressesRestartForOwnFlag() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(10)) { event ->
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
             event.requestNoRestart()
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
@@ -199,10 +199,10 @@
     @Test
     fun testRestartWhenNotAllListenersRequestSuppress() {
         val restartAction = mock<Consumer<Boolean>>()
-        mFlagManager.addListener(ReleasedFlag(10)) { event ->
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) { event ->
             event.requestNoRestart()
         }
-        mFlagManager.addListener(ReleasedFlag(10)) {
+        mFlagManager.addListener(ReleasedFlag(10, "10", "test")) {
             // do not request
         }
         mFlagManager.dispatchListenersAndMaybeRestart(1, restartAction)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java
deleted file mode 100644
index 250cc48..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagsTest.java
+++ /dev/null
@@ -1,119 +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.
- */
-
-package com.android.systemui.flags;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.util.Pair;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Test;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-public class FlagsTest extends SysuiTestCase {
-
-    @Test
-    public void testDuplicateFlagIdCheckWorks() {
-        List<Pair<String, Flag<?>>> flags = collectFlags(DuplicateFlagContainer.class);
-        Map<Integer, List<String>> duplicates = groupDuplicateFlags(flags);
-
-        assertWithMessage(generateAssertionMessage(duplicates))
-                .that(duplicates.size()).isEqualTo(2);
-    }
-
-    @Test
-    public void testNoDuplicateFlagIds() {
-        List<Pair<String, Flag<?>>> flags = collectFlags(Flags.class);
-        Map<Integer, List<String>> duplicates = groupDuplicateFlags(flags);
-
-        assertWithMessage(generateAssertionMessage(duplicates))
-                .that(duplicates.size()).isEqualTo(0);
-    }
-
-    private String generateAssertionMessage(Map<Integer, List<String>> duplicates) {
-        StringBuilder stringBuilder = new StringBuilder();
-        stringBuilder.append("Duplicate flag keys found: {");
-        for (int id : duplicates.keySet()) {
-            stringBuilder
-                    .append(" ")
-                    .append(id)
-                    .append(": [")
-                    .append(String.join(", ", duplicates.get(id)))
-                    .append("]");
-        }
-        stringBuilder.append(" }");
-
-        return stringBuilder.toString();
-    }
-
-    private List<Pair<String, Flag<?>>> collectFlags(Class<?> clz) {
-        List<Pair<String, Flag<?>>> flags = new ArrayList<>();
-
-        Field[] fields = clz.getFields();
-
-        for (Field field : fields) {
-            Class<?> t = field.getType();
-            if (Flag.class.isAssignableFrom(t)) {
-                try {
-                    flags.add(Pair.create(field.getName(), (Flag<?>) field.get(null)));
-                } catch (IllegalAccessException e) {
-                    // no-op
-                }
-            }
-        }
-
-        return flags;
-    }
-
-    private Map<Integer, List<String>> groupDuplicateFlags(List<Pair<String, Flag<?>>> flags) {
-        Map<Integer, List<String>> grouping = new HashMap<>();
-
-        for (Pair<String, Flag<?>> flag : flags) {
-            grouping.putIfAbsent(flag.second.getId(), new ArrayList<>());
-            grouping.get(flag.second.getId()).add(flag.first);
-        }
-
-        Map<Integer, List<String>> result = new HashMap<>();
-        for (Integer id : grouping.keySet()) {
-            if (grouping.get(id).size() > 1) {
-                result.put(id, grouping.get(id));
-            }
-        }
-
-        return result;
-    }
-
-    private static class DuplicateFlagContainer {
-        public static final BooleanFlag A_FLAG = new UnreleasedFlag(0);
-        public static final BooleanFlag B_FLAG = new UnreleasedFlag(0);
-        public static final StringFlag C_FLAG = new StringFlag(0);
-
-        public static final BooleanFlag D_FLAG = new UnreleasedFlag(1);
-
-        public static final DoubleFlag E_FLAG = new DoubleFlag(3);
-        public static final DoubleFlag F_FLAG = new DoubleFlag(3);
-    }
-}
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..1633912
--- /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, "1", "test")
+        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/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 8b1554c..d52616b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -63,6 +63,7 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.plugins.GlobalActions;
 import com.android.systemui.settings.UserContextProvider;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -103,6 +104,7 @@
     @Mock private SecureSettings mSecureSettings;
     @Mock private Resources mResources;
     @Mock private ConfigurationController mConfigurationController;
+    @Mock private UserTracker mUserTracker;
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private UserManager mUserManager;
     @Mock private TrustManager mTrustManager;
@@ -152,6 +154,7 @@
                 mVibratorHelper,
                 mResources,
                 mConfigurationController,
+                mUserTracker,
                 mKeyguardStateController,
                 mUserManager,
                 mTrustManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
new file mode 100644
index 0000000..8395f02
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -0,0 +1,429 @@
+/*
+ * 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
+
+import android.content.ContentValues
+import android.content.pm.PackageManager
+import android.content.pm.ProviderInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SystemUIAppComponentFactoryBase
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    private lateinit var underTest: KeyguardQuickAffordanceProvider
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = KeyguardQuickAffordanceProvider()
+        val scope = CoroutineScope(IMMEDIATE)
+        val selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock<UserFileManager>().apply {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = userTracker,
+            )
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                appContext = context,
+                scope = scope,
+                selectionManager = selectionManager,
+                configs =
+                    setOf(
+                        FakeKeyguardQuickAffordanceConfig(
+                            key = AFFORDANCE_1,
+                            pickerIconResourceId = 1,
+                        ),
+                        FakeKeyguardQuickAffordanceConfig(
+                            key = AFFORDANCE_2,
+                            pickerIconResourceId = 2,
+                        ),
+                    ),
+                legacySettingSyncer =
+                    KeyguardQuickAffordanceLegacySettingSyncer(
+                        scope = scope,
+                        backgroundDispatcher = IMMEDIATE,
+                        secureSettings = FakeSettings(),
+                        selectionsManager = selectionManager,
+                    ),
+            )
+        underTest.interactor =
+            KeyguardQuickAffordanceInteractor(
+                keyguardInteractor =
+                    KeyguardInteractor(
+                        repository = FakeKeyguardRepository(),
+                    ),
+                registry = mock(),
+                lockPatternUtils = lockPatternUtils,
+                keyguardStateController = keyguardStateController,
+                userTracker = userTracker,
+                activityStarter = activityStarter,
+                featureFlags =
+                    FakeFeatureFlags().apply {
+                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+                    },
+                repository = { quickAffordanceRepository },
+            )
+
+        underTest.attachInfoForTesting(
+            context,
+            ProviderInfo().apply { authority = Contract.AUTHORITY },
+        )
+        context.contentResolver.addProvider(Contract.AUTHORITY, underTest)
+        context.testablePermissions.setPermission(
+            Contract.PERMISSION,
+            PackageManager.PERMISSION_GRANTED,
+        )
+    }
+
+    @Test
+    fun `onAttachInfo - reportsContext`() {
+        val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock()
+        underTest.setContextAvailableCallback(callback)
+
+        underTest.attachInfo(context, null)
+
+        verify(callback).onContextAvailable(context)
+    }
+
+    @Test
+    fun getType() {
+        assertThat(underTest.getType(Contract.AffordanceTable.URI))
+            .isEqualTo(
+                "vnd.android.cursor.dir/vnd." +
+                    "${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}"
+            )
+        assertThat(underTest.getType(Contract.SlotTable.URI))
+            .isEqualTo(
+                "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}"
+            )
+        assertThat(underTest.getType(Contract.SelectionTable.URI))
+            .isEqualTo(
+                "vnd.android.cursor.dir/vnd." +
+                    "${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}"
+            )
+    }
+
+    @Test
+    fun `insert and query selection`() =
+        runBlocking(IMMEDIATE) {
+            val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+            val affordanceId = AFFORDANCE_2
+
+            insertSelection(
+                slotId = slotId,
+                affordanceId = affordanceId,
+            )
+
+            assertThat(querySelections())
+                .isEqualTo(
+                    listOf(
+                        Selection(
+                            slotId = slotId,
+                            affordanceId = affordanceId,
+                        )
+                    )
+                )
+        }
+
+    @Test
+    fun `query slots`() =
+        runBlocking(IMMEDIATE) {
+            assertThat(querySlots())
+                .isEqualTo(
+                    listOf(
+                        Slot(
+                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                            capacity = 1,
+                        ),
+                        Slot(
+                            id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                            capacity = 1,
+                        ),
+                    )
+                )
+        }
+
+    @Test
+    fun `query affordances`() =
+        runBlocking(IMMEDIATE) {
+            assertThat(queryAffordances())
+                .isEqualTo(
+                    listOf(
+                        Affordance(
+                            id = AFFORDANCE_1,
+                            name = AFFORDANCE_1,
+                            iconResourceId = 1,
+                        ),
+                        Affordance(
+                            id = AFFORDANCE_2,
+                            name = AFFORDANCE_2,
+                            iconResourceId = 2,
+                        ),
+                    )
+                )
+        }
+
+    @Test
+    fun `delete and query selection`() =
+        runBlocking(IMMEDIATE) {
+            insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceId = AFFORDANCE_1,
+            )
+            insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                affordanceId = AFFORDANCE_2,
+            )
+
+            context.contentResolver.delete(
+                Contract.SelectionTable.URI,
+                "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" +
+                    " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?",
+                arrayOf(
+                    KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                    AFFORDANCE_2,
+                ),
+            )
+
+            assertThat(querySelections())
+                .isEqualTo(
+                    listOf(
+                        Selection(
+                            slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                            affordanceId = AFFORDANCE_1,
+                        )
+                    )
+                )
+        }
+
+    @Test
+    fun `delete all selections in a slot`() =
+        runBlocking(IMMEDIATE) {
+            insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceId = AFFORDANCE_1,
+            )
+            insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                affordanceId = AFFORDANCE_2,
+            )
+
+            context.contentResolver.delete(
+                Contract.SelectionTable.URI,
+                Contract.SelectionTable.Columns.SLOT_ID,
+                arrayOf(
+                    KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                ),
+            )
+
+            assertThat(querySelections())
+                .isEqualTo(
+                    listOf(
+                        Selection(
+                            slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                            affordanceId = AFFORDANCE_1,
+                        )
+                    )
+                )
+        }
+
+    private fun insertSelection(
+        slotId: String,
+        affordanceId: String,
+    ) {
+        context.contentResolver.insert(
+            Contract.SelectionTable.URI,
+            ContentValues().apply {
+                put(Contract.SelectionTable.Columns.SLOT_ID, slotId)
+                put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId)
+            }
+        )
+    }
+
+    private fun querySelections(): List<Selection> {
+        return context.contentResolver
+            .query(
+                Contract.SelectionTable.URI,
+                null,
+                null,
+                null,
+                null,
+            )
+            ?.use { cursor ->
+                buildList {
+                    val slotIdColumnIndex =
+                        cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID)
+                    val affordanceIdColumnIndex =
+                        cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID)
+                    if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) {
+                        return@buildList
+                    }
+
+                    while (cursor.moveToNext()) {
+                        add(
+                            Selection(
+                                slotId = cursor.getString(slotIdColumnIndex),
+                                affordanceId = cursor.getString(affordanceIdColumnIndex),
+                            )
+                        )
+                    }
+                }
+            }
+            ?: emptyList()
+    }
+
+    private fun querySlots(): List<Slot> {
+        return context.contentResolver
+            .query(
+                Contract.SlotTable.URI,
+                null,
+                null,
+                null,
+                null,
+            )
+            ?.use { cursor ->
+                buildList {
+                    val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID)
+                    val capacityColumnIndex =
+                        cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY)
+                    if (idColumnIndex == -1 || capacityColumnIndex == -1) {
+                        return@buildList
+                    }
+
+                    while (cursor.moveToNext()) {
+                        add(
+                            Slot(
+                                id = cursor.getString(idColumnIndex),
+                                capacity = cursor.getInt(capacityColumnIndex),
+                            )
+                        )
+                    }
+                }
+            }
+            ?: emptyList()
+    }
+
+    private fun queryAffordances(): List<Affordance> {
+        return context.contentResolver
+            .query(
+                Contract.AffordanceTable.URI,
+                null,
+                null,
+                null,
+                null,
+            )
+            ?.use { cursor ->
+                buildList {
+                    val idColumnIndex = cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID)
+                    val nameColumnIndex =
+                        cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME)
+                    val iconColumnIndex =
+                        cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON)
+                    if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) {
+                        return@buildList
+                    }
+
+                    while (cursor.moveToNext()) {
+                        add(
+                            Affordance(
+                                id = cursor.getString(idColumnIndex),
+                                name = cursor.getString(nameColumnIndex),
+                                iconResourceId = cursor.getInt(iconColumnIndex),
+                            )
+                        )
+                    }
+                }
+            }
+            ?: emptyList()
+    }
+
+    data class Slot(
+        val id: String,
+        val capacity: Int,
+    )
+
+    data class Affordance(
+        val id: String,
+        val name: String,
+        val iconResourceId: Int,
+    )
+
+    data class Selection(
+        val slotId: String,
+        val affordanceId: String,
+    )
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val AFFORDANCE_1 = "affordance_1"
+        private const val AFFORDANCE_2 = "affordance_2"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index 23516c9..729a1cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -48,6 +48,7 @@
 import com.android.systemui.SystemUIInitializerImpl;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -93,6 +94,8 @@
     private NextAlarmController mNextAlarmController;
     @Mock
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    private UserTracker mUserTracker;
     private TestableKeyguardSliceProvider mProvider;
     private boolean mIsZenMode;
 
@@ -105,6 +108,7 @@
         mProvider.attachInfo(getContext(), null);
         reset(mContentResolver);
         SliceProvider.setSpecs(new HashSet<>(Arrays.asList(SliceSpecs.LIST)));
+        when(mUserTracker.getUserId()).thenReturn(100);
     }
 
     @After
@@ -267,6 +271,7 @@
             mKeyguardBypassController = KeyguardSliceProviderTest.this.mKeyguardBypassController;
             mMediaManager = KeyguardSliceProviderTest.this.mNotificationMediaManager;
             mKeyguardUpdateMonitor = KeyguardSliceProviderTest.this.mKeyguardUpdateMonitor;
+            mUserTracker = KeyguardSliceProviderTest.this.mUserTracker;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 4c986bf..d17e374 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -57,11 +57,15 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.NavigationModeController;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -84,6 +88,7 @@
 public class KeyguardViewMediatorTest extends SysuiTestCase {
     private KeyguardViewMediator mViewMediator;
 
+    private @Mock UserTracker mUserTracker;
     private @Mock DevicePolicyManager mDevicePolicyManager;
     private @Mock LockPatternUtils mLockPatternUtils;
     private @Mock KeyguardUpdateMonitor mUpdateMonitor;
@@ -104,14 +109,18 @@
     private @Mock ScreenOffAnimationController mScreenOffAnimationController;
     private @Mock InteractionJankMonitor mInteractionJankMonitor;
     private @Mock ScreenOnCoordinator mScreenOnCoordinator;
+    private @Mock ShadeController mShadeController;
     private @Mock Lazy<NotificationShadeWindowController> mNotificationShadeWindowControllerLazy;
     private @Mock DreamOverlayStateController mDreamOverlayStateController;
     private @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
+    private @Mock ScrimController mScrimController;
     private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake();
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
     private FalsingCollectorFake mFalsingCollector;
 
+    private @Mock CentralSurfaces mCentralSurfaces;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -258,9 +267,30 @@
         verify(mKeyguardStateController).notifyKeyguardGoingAway(false);
     }
 
+    @Test
+    public void testUpdateIsKeyguardAfterOccludeAnimationEnds() {
+        mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd(
+                false /* isExpandingFullyAbove */);
+
+        // Since the updateIsKeyguard call is delayed during the animation, ensure it's called once
+        // it ends.
+        verify(mCentralSurfaces).updateIsKeyguard();
+    }
+
+    @Test
+    public void testUpdateIsKeyguardAfterOccludeAnimationIsCancelled() {
+        mViewMediator.mOccludeAnimationController.onLaunchAnimationCancelled(
+                null /* newKeyguardOccludedState */);
+
+        // Since the updateIsKeyguard call is delayed during the animation, ensure it's called if
+        // it's cancelled.
+        verify(mCentralSurfaces).updateIsKeyguard();
+    }
+
     private void createAndStartViewMediator() {
         mViewMediator = new KeyguardViewMediator(
                 mContext,
+                mUserTracker,
                 mFalsingCollector,
                 mLockPatternUtils,
                 mBroadcastDispatcher,
@@ -284,8 +314,12 @@
                 mScreenOnCoordinator,
                 mInteractionJankMonitor,
                 mDreamOverlayStateController,
+                () -> mShadeController,
                 mNotificationShadeWindowControllerLazy,
-                () -> mActivityLaunchAnimator);
+                () -> mActivityLaunchAnimator,
+                () -> mScrimController);
         mViewMediator.start();
+
+        mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
deleted file mode 100644
index 27a5190..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ /dev/null
@@ -1,478 +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.
- */
-
-package com.android.systemui.keyguard;
-
-import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.keyguard.LockIconView.ICON_LOCK;
-import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.AnimatedStateListDrawable;
-import android.hardware.biometrics.BiometricSourceType;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.util.Pair;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.KeyguardViewController;
-import com.android.keyguard.LockIconView;
-import com.android.keyguard.LockIconViewController;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.doze.util.BurnInHelperKt;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class LockIconViewControllerTest extends SysuiTestCase {
-    private static final String UNLOCKED_LABEL = "unlocked";
-    private static final int PADDING = 10;
-
-    private MockitoSession mStaticMockSession;
-
-    private @Mock LockIconView mLockIconView;
-    private @Mock AnimatedStateListDrawable mIconDrawable;
-    private @Mock Context mContext;
-    private @Mock Resources mResources;
-    private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
-    private @Mock StatusBarStateController mStatusBarStateController;
-    private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private @Mock KeyguardViewController mKeyguardViewController;
-    private @Mock KeyguardStateController mKeyguardStateController;
-    private @Mock FalsingManager mFalsingManager;
-    private @Mock AuthController mAuthController;
-    private @Mock DumpManager mDumpManager;
-    private @Mock AccessibilityManager mAccessibilityManager;
-    private @Mock ConfigurationController mConfigurationController;
-    private @Mock VibratorHelper mVibrator;
-    private @Mock AuthRippleController mAuthRippleController;
-    private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
-
-    private LockIconViewController mLockIconViewController;
-
-    // Capture listeners so that they can be used to send events
-    @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
-            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-    private View.OnAttachStateChangeListener mAttachListener;
-
-    @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
-            ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
-    private KeyguardStateController.Callback mKeyguardStateCallback;
-
-    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
-            ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
-    private StatusBarStateController.StateListener mStatusBarStateListener;
-
-    @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
-    private AuthController.Callback mAuthControllerCallback;
-
-    @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
-            mKeyguardUpdateMonitorCallbackCaptor =
-            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
-    private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
-
-    @Captor private ArgumentCaptor<Point> mPointCaptor;
-
-    @Before
-    public void setUp() throws Exception {
-        mStaticMockSession = mockitoSession()
-                .mockStatic(BurnInHelperKt.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
-        MockitoAnnotations.initMocks(this);
-
-        setupLockIconViewMocks();
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
-        Rect windowBounds = new Rect(0, 0, 800, 1200);
-        when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
-        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
-        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
-        when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
-        when(mAuthController.getScaleFactor()).thenReturn(1f);
-
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
-        when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-
-        mLockIconViewController = new LockIconViewController(
-                mLockIconView,
-                mStatusBarStateController,
-                mKeyguardUpdateMonitor,
-                mKeyguardViewController,
-                mKeyguardStateController,
-                mFalsingManager,
-                mAuthController,
-                mDumpManager,
-                mAccessibilityManager,
-                mConfigurationController,
-                mDelayableExecutor,
-                mVibrator,
-                mAuthRippleController,
-                mResources
-        );
-    }
-
-    @After
-    public void tearDown() {
-        mStaticMockSession.finishMocking();
-    }
-
-    @Test
-    public void testUpdateFingerprintLocationOnInit() {
-        // GIVEN fp sensor location is available pre-attached
-        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-
-        // WHEN lock icon view controller is initialized and attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN lock icon view location is updated to the udfps location with UDFPS radius
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testUpdatePaddingBasedOnResolutionScale() {
-        // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
-        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-        when(mAuthController.getScaleFactor()).thenReturn(5f);
-
-        // WHEN lock icon view controller is initialized and attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN lock icon view location is updated with the scaled radius
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING * 5));
-    }
-
-    @Test
-    public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
-        // GIVEN fp sensor location is not available pre-init
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        resetLockIconView(); // reset any method call counts for when we verify method calls later
-
-        // GIVEN fp sensor location is available post-attached
-        captureAuthControllerCallback();
-        Pair<Float, Point> udfps = setupUdfps();
-
-        // WHEN all authenticators are registered
-        mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
-        mDelayableExecutor.runAllReady();
-
-        // THEN lock icon view location is updated with the same coordinates as auth controller vals
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
-        // GIVEN fp sensor location is not available pre-init
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        resetLockIconView(); // reset any method call counts for when we verify method calls later
-
-        // GIVEN fp sensor location is available post-attached
-        captureAuthControllerCallback();
-        Pair<Float, Point> udfps = setupUdfps();
-
-        // WHEN udfps location changes
-        mAuthControllerCallback.onUdfpsLocationChanged();
-        mDelayableExecutor.runAllReady();
-
-        // THEN lock icon view location is updated with the same coordinates as auth controller vals
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
-        // GIVEN Udpfs sensor location is available
-        setupUdfps();
-
-        mLockIconViewController.init();
-        captureAttachListener();
-
-        // WHEN the view is attached
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon view background should be enabled
-        verify(mLockIconView).setUseBackground(true);
-    }
-
-    @Test
-    public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
-        // GIVEN Udfps sensor location is not supported
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-
-        mLockIconViewController.init();
-        captureAttachListener();
-
-        // WHEN the view is attached
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon view background should be disabled
-        verify(mLockIconView).setUseBackground(false);
-    }
-
-    @Test
-    public void testUnlockIconShows_biometricUnlockedTrue() {
-        // GIVEN UDFPS sensor location is available
-        setupUdfps();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureKeyguardUpdateMonitorCallback();
-
-        // GIVEN user has unlocked with a biometric auth (ie: face auth)
-        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
-        reset(mLockIconView);
-
-        // WHEN face auth's biometric running state changes
-        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
-                BiometricSourceType.FACE);
-
-        // THEN the unlock icon is shown
-        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
-    }
-
-    @Test
-    public void testLockIconStartState() {
-        // GIVEN lock icon state
-        setupShowLockIcon();
-
-        // WHEN lock icon controller is initialized
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon should show
-        verify(mLockIconView).updateIcon(ICON_LOCK, false);
-    }
-
-    @Test
-    public void testLockIcon_updateToUnlock() {
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureKeyguardStateCallback();
-        reset(mLockIconView);
-
-        // WHEN the unlocked state changes to canDismissLockScreen=true
-        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
-        mKeyguardStateCallback.onUnlockedChanged();
-
-        // THEN the unlock should show
-        verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
-    }
-
-    @Test
-    public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
-        // GIVEN udfps not enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
-
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN the dozing state changes
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
-        // THEN the icon is cleared
-        verify(mLockIconView).clearIcon();
-    }
-
-    @Test
-    public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
-        // GIVEN udfps enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN the dozing state changes
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
-        // THEN the AOD lock icon should show
-        verify(mLockIconView).updateIcon(ICON_LOCK, true);
-    }
-
-    @Test
-    public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
-        // GIVEN udfps enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
-        // GIVEN burn-in offset = 5
-        int burnInOffset = 5;
-        when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
-
-        // GIVEN starting state for the lock icon (keyguard)
-        setupShowLockIcon();
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN dozing updates
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-        mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
-
-        // THEN the view's translation is updated to use the AoD burn-in offsets
-        verify(mLockIconView).setTranslationY(burnInOffset);
-        verify(mLockIconView).setTranslationX(burnInOffset);
-        reset(mLockIconView);
-
-        // WHEN the device is no longer dozing
-        mStatusBarStateListener.onDozingChanged(false /* isDozing */);
-        mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
-
-        // THEN the view is updated to NO translation (no burn-in offsets anymore)
-        verify(mLockIconView).setTranslationY(0);
-        verify(mLockIconView).setTranslationX(0);
-
-    }
-    private Pair<Float, Point> setupUdfps() {
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
-        final Point udfpsLocation = new Point(50, 75);
-        final float radius = 33f;
-        when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
-        when(mAuthController.getUdfpsRadius()).thenReturn(radius);
-
-        return new Pair(radius, udfpsLocation);
-    }
-
-    private void setupShowLockIcon() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
-        when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
-    }
-
-    private void captureAuthControllerCallback() {
-        verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
-        mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
-    }
-
-    private void captureAttachListener() {
-        verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
-        mAttachListener = mAttachCaptor.getValue();
-    }
-
-    private void captureKeyguardStateCallback() {
-        verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
-        mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
-    }
-
-    private void captureStatusBarStateListener() {
-        verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
-        mStatusBarStateListener = mStatusBarStateCaptor.getValue();
-    }
-
-    private void captureKeyguardUpdateMonitorCallback() {
-        verify(mKeyguardUpdateMonitor).registerCallback(
-                mKeyguardUpdateMonitorCallbackCaptor.capture());
-        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
-    }
-
-    private void setupLockIconViewMocks() {
-        when(mLockIconView.getResources()).thenReturn(mResources);
-        when(mLockIconView.getContext()).thenReturn(mContext);
-    }
-
-    private void resetLockIconView() {
-        reset(mLockIconView);
-        setupLockIconViewMocks();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
deleted file mode 100644
index 640e6dc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.keyguard;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.UserIdInt;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * runtest systemui -c com.android.systemui.keyguard.WorkLockActivityTest
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class WorkLockActivityTest extends SysuiTestCase {
-    private static final @UserIdInt int USER_ID = 270;
-    private static final String CALLING_PACKAGE_NAME = "com.android.test";
-
-    private @Mock UserManager mUserManager;
-    private @Mock PackageManager mPackageManager;
-    private @Mock Context mContext;
-    private @Mock BroadcastDispatcher mBroadcastDispatcher;
-    private @Mock Drawable mDrawable;
-    private @Mock Drawable mBadgedDrawable;
-
-    private WorkLockActivity mActivity;
-
-    private static class WorkLockActivityTestable extends WorkLockActivity {
-        WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher,
-                UserManager userManager, PackageManager packageManager) {
-            super(broadcastDispatcher, userManager, packageManager);
-            attachBaseContext(baseContext);
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-        mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher, mUserManager,
-                mPackageManager);
-    }
-
-    @Test
-    public void testGetBadgedIcon() throws Exception {
-        ApplicationInfo info = new ApplicationInfo();
-        when(mPackageManager.getApplicationInfoAsUser(eq(CALLING_PACKAGE_NAME), any(),
-                eq(USER_ID))).thenReturn(info);
-        when(mPackageManager.getApplicationIcon(eq(info))).thenReturn(mDrawable);
-        when(mUserManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID)))).thenReturn(
-                mBadgedDrawable);
-        mActivity.setIntent(new Intent()
-                .putExtra(Intent.EXTRA_USER_ID, USER_ID)
-                .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME));
-
-        assertEquals(mBadgedDrawable, mActivity.getBadgedIcon());
-    }
-
-    @Test
-    public void testUnregisteredFromDispatcher() {
-        mActivity.unregisterBroadcastReceiver();
-        verify(mBroadcastDispatcher).unregisterReceiver(any());
-        verify(mContext, never()).unregisterReceiver(any());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt
new file mode 100644
index 0000000..c7f1dec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard
+
+import android.annotation.UserIdInt
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ApplicationInfoFlags
+import android.graphics.drawable.Drawable
+import android.os.Looper
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+/** runtest systemui -c com.android.systemui.keyguard.WorkLockActivityTest */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkLockActivityTest : SysuiTestCase() {
+    private val context: Context = mock()
+    private val userManager: UserManager = mock()
+    private val packageManager: PackageManager = mock()
+    private val broadcastDispatcher: BroadcastDispatcher = mock()
+    private val drawable: Drawable = mock()
+    private val badgedDrawable: Drawable = mock()
+    private lateinit var activity: WorkLockActivity
+
+    private class WorkLockActivityTestable
+    constructor(
+        baseContext: Context,
+        broadcastDispatcher: BroadcastDispatcher,
+        userManager: UserManager,
+        packageManager: PackageManager,
+    ) : WorkLockActivity(broadcastDispatcher, userManager, packageManager) {
+        init {
+            attachBaseContext(baseContext)
+        }
+    }
+
+    @Before
+    fun setUp() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare()
+        }
+        activity =
+            WorkLockActivityTestable(
+                baseContext = context,
+                broadcastDispatcher = broadcastDispatcher,
+                userManager = userManager,
+                packageManager = packageManager
+            )
+    }
+
+    @Test
+    fun testGetBadgedIcon() {
+        val info = ApplicationInfo()
+        whenever(
+                packageManager.getApplicationInfoAsUser(
+                    eq(CALLING_PACKAGE_NAME),
+                    any<ApplicationInfoFlags>(),
+                    eq(USER_ID)
+                )
+            )
+            .thenReturn(info)
+        whenever(packageManager.getApplicationIcon(ArgumentMatchers.eq(info))).thenReturn(drawable)
+        whenever(userManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID))))
+            .thenReturn(badgedDrawable)
+        activity.intent =
+            Intent()
+                .putExtra(Intent.EXTRA_USER_ID, USER_ID)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME)
+        assertEquals(badgedDrawable, activity.badgedIcon)
+    }
+
+    @Test
+    fun testUnregisteredFromDispatcher() {
+        activity.unregisterBroadcastReceiver()
+        verify(broadcastDispatcher).unregisterReceiver(any())
+        verify(context, never()).unregisterReceiver(nullable())
+    }
+
+    @Test
+    fun onBackPressed_finishActivity() {
+        assertFalse(activity.isFinishing)
+
+        activity.onBackPressed()
+
+        assertFalse(activity.isFinishing)
+    }
+
+    companion object {
+        @UserIdInt private val USER_ID = 270
+        private const val CALLING_PACKAGE_NAME = "com.android.test"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..623becf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.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.keyguard.data.quickaffordance
+
+import android.app.StatusBarManager
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.camera.CameraGestureHelper
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class CameraQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var cameraGestureHelper: CameraGestureHelper
+    @Mock private lateinit var context: Context
+    private lateinit var underTest: CameraQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = CameraQuickAffordanceConfig(
+                context,
+                cameraGestureHelper,
+        )
+    }
+
+    @Test
+    fun `affordance triggered -- camera launch called`() {
+        //when
+        val result = underTest.onTriggered(null)
+
+        //then
+        verify(cameraGestureHelper)
+                .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
+        assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..0fb181d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,56 @@
+/*
+ *  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.animation.Expandable
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.yield
+
+/** Fake implementation of a quick affordance data source. */
+class FakeKeyguardQuickAffordanceConfig(
+    override val key: String,
+    override val pickerName: String = key,
+    override val pickerIconResourceId: Int = 0,
+) : KeyguardQuickAffordanceConfig {
+
+    var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled
+
+    private val _lockScreenState =
+        MutableStateFlow<KeyguardQuickAffordanceConfig.LockScreenState>(
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+        )
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        _lockScreenState
+
+    override fun onTriggered(
+        expandable: Expandable?,
+    ): OnTriggeredResult {
+        return onTriggeredResult
+    }
+
+    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
+        // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this.
+        yield()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
new file mode 100644
index 0000000..c94cec6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -0,0 +1,149 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(Parameterized::class)
+class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
+
+    companion object {
+        @Parameters(
+            name =
+                "feature enabled = {0}, has favorites = {1}, has service infos = {2}, can show" +
+                    " while locked = {3}, visibility is AVAILABLE {4} - expected visible = {5}"
+        )
+        @JvmStatic
+        fun data() =
+            (0 until 32)
+                .map { combination ->
+                    arrayOf(
+                        /* isFeatureEnabled= */ combination and 0b10000 != 0,
+                        /* hasFavorites= */ combination and 0b01000 != 0,
+                        /* hasServiceInfos= */ combination and 0b00100 != 0,
+                        /* canShowWhileLocked= */ combination and 0b00010 != 0,
+                        /* visibilityAvailable= */ combination and 0b00001 != 0,
+                        /* isVisible= */ combination == 0b11111,
+                    )
+                }
+                .toList()
+    }
+
+    @Mock private lateinit var component: ControlsComponent
+    @Mock private lateinit var controlsController: ControlsController
+    @Mock private lateinit var controlsListingController: ControlsListingController
+    @Captor
+    private lateinit var callbackCaptor:
+        ArgumentCaptor<ControlsListingController.ControlsListingCallback>
+
+    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
+
+    @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false
+    @JvmField @Parameter(1) var hasFavorites: Boolean = false
+    @JvmField @Parameter(2) var hasServiceInfos: Boolean = false
+    @JvmField @Parameter(3) var canShowWhileLocked: Boolean = false
+    @JvmField @Parameter(4) var isVisibilityAvailable: Boolean = false
+    @JvmField @Parameter(5) var isVisibleExpected: Boolean = false
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+        whenever(component.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+        whenever(component.canShowWhileLockedSetting)
+            .thenReturn(MutableStateFlow(canShowWhileLocked))
+        whenever(component.getVisibility())
+            .thenReturn(
+                if (isVisibilityAvailable) {
+                    ControlsComponent.Visibility.AVAILABLE
+                } else {
+                    ControlsComponent.Visibility.UNAVAILABLE
+                }
+            )
+
+        underTest =
+            HomeControlsKeyguardQuickAffordanceConfig(
+                context = context,
+                component = component,
+            )
+    }
+
+    @Test
+    fun state() = runBlockingTest {
+        whenever(component.isEnabled()).thenReturn(isFeatureEnabled)
+        whenever(controlsController.getFavorites())
+            .thenReturn(
+                if (hasFavorites) {
+                    listOf(mock())
+                } else {
+                    emptyList()
+                }
+            )
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
+
+        if (canShowWhileLocked) {
+            verify(controlsListingController).addCallback(callbackCaptor.capture())
+            callbackCaptor.value.onServicesUpdated(
+                if (hasServiceInfos) {
+                    listOf(mock())
+                } else {
+                    emptyList()
+                }
+            )
+        }
+
+        assertThat(values.last())
+            .isInstanceOf(
+                if (isVisibleExpected) {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java
+                } else {
+                    KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java
+                }
+            )
+        job.cancel()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..659c1e5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+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.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var component: ControlsComponent
+    @Mock private lateinit var expandable: Expandable
+
+    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
+
+        underTest =
+            HomeControlsKeyguardQuickAffordanceConfig(
+                context = context,
+                component = component,
+            )
+    }
+
+    @Test
+    fun `state - when cannot show while locked - returns Hidden`() = runBlockingTest {
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
+        whenever(component.isEnabled()).thenReturn(true)
+        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+        val controlsController = mock<ControlsController>()
+        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+        whenever(component.getControlsListingController()).thenReturn(Optional.empty())
+        whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
+        whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
+
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
+
+        assertThat(values.last())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java)
+        job.cancel()
+    }
+
+    @Test
+    fun `state - when listing controller is missing - returns Hidden`() = runBlockingTest {
+        whenever(component.isEnabled()).thenReturn(true)
+        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+        val controlsController = mock<ControlsController>()
+        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+        whenever(component.getControlsListingController()).thenReturn(Optional.empty())
+        whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
+        whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
+
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
+
+        assertThat(values.last())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java)
+        job.cancel()
+    }
+
+    @Test
+    fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is true`() = runBlockingTest {
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
+
+        val onClickedResult = underTest.onTriggered(expandable)
+
+        assertThat(onClickedResult).isInstanceOf(OnTriggeredResult.StartActivity::class.java)
+        assertThat((onClickedResult as OnTriggeredResult.StartActivity).canShowWhileLocked).isTrue()
+    }
+
+    @Test
+    fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is false`() = runBlockingTest {
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
+
+        val onClickedResult = underTest.onTriggered(expandable)
+
+        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/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
new file mode 100644
index 0000000..8ef921e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.Context
+import android.content.res.Resources
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
+
+    @Mock private lateinit var sharedPrefs: FakeSharedPreferences
+
+    private lateinit var underTest: KeyguardQuickAffordanceLegacySettingSyncer
+
+    private lateinit var testScope: TestScope
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager
+    private lateinit var settings: FakeSettings
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        val context: Context = mock()
+        sharedPrefs = FakeSharedPreferences()
+        whenever(context.getSharedPreferences(anyString(), any())).thenReturn(sharedPrefs)
+        val resources: Resources = mock()
+        whenever(resources.getStringArray(R.array.config_keyguardQuickAffordanceDefaults))
+            .thenReturn(emptyArray())
+        whenever(context.resources).thenReturn(resources)
+
+        testDispatcher = UnconfinedTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = FakeUserTracker(),
+            )
+        settings = FakeSettings()
+        settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
+        settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET, 0)
+        settings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0)
+
+        underTest =
+            KeyguardQuickAffordanceLegacySettingSyncer(
+                scope = testScope,
+                backgroundDispatcher = testDispatcher,
+                secureSettings = settings,
+                selectionsManager = selectionManager,
+            )
+    }
+
+    @Test
+    fun `Setting a setting selects the affordance`() =
+        testScope.runTest {
+            val job = underTest.startSyncing()
+
+            settings.putInt(
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                1,
+            )
+
+            assertThat(
+                    selectionManager
+                        .getSelections()
+                        .getOrDefault(
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                            emptyList()
+                        )
+                )
+                .contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `Clearing a setting selects the affordance`() =
+        testScope.runTest {
+            val job = underTest.startSyncing()
+
+            settings.putInt(
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                1,
+            )
+            settings.putInt(
+                Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+                0,
+            )
+
+            assertThat(
+                    selectionManager
+                        .getSelections()
+                        .getOrDefault(
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                            emptyList()
+                        )
+                )
+                .doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `Selecting an affordance sets its setting`() =
+        testScope.runTest {
+            val job = underTest.startSyncing()
+
+            selectionManager.setSelections(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+            )
+
+            advanceUntilIdle()
+            assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `Unselecting an affordance clears its setting`() =
+        testScope.runTest {
+            val job = underTest.startSyncing()
+
+            selectionManager.setSelections(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+            )
+            selectionManager.setSelections(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                emptyList()
+            )
+
+            assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0)
+
+            job.cancel()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
new file mode 100644
index 0000000..d8ee9f1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.SharedPreferences
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+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.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
+
+    @Mock private lateinit var userFileManager: UserFileManager
+
+    private lateinit var underTest: KeyguardQuickAffordanceSelectionManager
+
+    private lateinit var userTracker: FakeUserTracker
+    private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        sharedPrefs = mutableMapOf()
+        whenever(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())).thenAnswer {
+            val userId = it.arguments[2] as Int
+            sharedPrefs.getOrPut(userId) { FakeSharedPreferences() }
+        }
+        userTracker = FakeUserTracker()
+
+        underTest =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager = userFileManager,
+                userTracker = userTracker,
+            )
+    }
+
+    @Test
+    fun setSelections() = runTest {
+        overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+        val slotId1 = "slot1"
+        val slotId2 = "slot2"
+        val affordanceId1 = "affordance1"
+        val affordanceId2 = "affordance2"
+        val affordanceId3 = "affordance3"
+
+        underTest.setSelections(
+            slotId = slotId1,
+            affordanceIds = listOf(affordanceId1),
+        )
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId1),
+            ),
+        )
+
+        underTest.setSelections(
+            slotId = slotId2,
+            affordanceIds = listOf(affordanceId2),
+        )
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId1),
+                slotId2 to listOf(affordanceId2),
+            )
+        )
+
+        underTest.setSelections(
+            slotId = slotId1,
+            affordanceIds = listOf(affordanceId1, affordanceId3),
+        )
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId1, affordanceId3),
+                slotId2 to listOf(affordanceId2),
+            )
+        )
+
+        underTest.setSelections(
+            slotId = slotId1,
+            affordanceIds = listOf(affordanceId3),
+        )
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId3),
+                slotId2 to listOf(affordanceId2),
+            )
+        )
+
+        underTest.setSelections(
+            slotId = slotId2,
+            affordanceIds = listOf(),
+        )
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId3),
+                slotId2 to listOf(),
+            )
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun `remembers selections by user`() = runTest {
+        val slot1 = "slot_1"
+        val slot2 = "slot_2"
+        val affordance1 = "affordance_1"
+        val affordance2 = "affordance_2"
+        val affordance3 = "affordance_3"
+
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+
+        val userInfos =
+            listOf(
+                UserInfo(/* id= */ 0, "zero", /* flags= */ 0),
+                UserInfo(/* id= */ 1, "one", /* flags= */ 0),
+            )
+        userTracker.set(
+            userInfos = userInfos,
+            selectedUserIndex = 0,
+        )
+        underTest.setSelections(
+            slotId = slot1,
+            affordanceIds = listOf(affordance1),
+        )
+        underTest.setSelections(
+            slotId = slot2,
+            affordanceIds = listOf(affordance2),
+        )
+
+        // Switch to user 1
+        userTracker.set(
+            userInfos = userInfos,
+            selectedUserIndex = 1,
+        )
+        // We never set selections on user 1, so it should be empty.
+        assertSelections(
+            observed = affordanceIdsBySlotId.last(),
+            expected = emptyMap(),
+        )
+        // Now, let's set selections on user 1.
+        underTest.setSelections(
+            slotId = slot1,
+            affordanceIds = listOf(affordance2),
+        )
+        underTest.setSelections(
+            slotId = slot2,
+            affordanceIds = listOf(affordance3),
+        )
+        assertSelections(
+            observed = affordanceIdsBySlotId.last(),
+            expected =
+                mapOf(
+                    slot1 to listOf(affordance2),
+                    slot2 to listOf(affordance3),
+                ),
+        )
+
+        // Switch back to user 0.
+        userTracker.set(
+            userInfos = userInfos,
+            selectedUserIndex = 0,
+        )
+        // Assert that we still remember the old selections for user 0.
+        assertSelections(
+            observed = affordanceIdsBySlotId.last(),
+            expected =
+                mapOf(
+                    slot1 to listOf(affordance1),
+                    slot2 to listOf(affordance2),
+                ),
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun `selections respects defaults`() = runTest {
+        val slotId1 = "slot1"
+        val slotId2 = "slot2"
+        val affordanceId1 = "affordance1"
+        val affordanceId2 = "affordance2"
+        val affordanceId3 = "affordance3"
+        overrideResource(
+            R.array.config_keyguardQuickAffordanceDefaults,
+            arrayOf(
+                "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
+                "$slotId2:${listOf(affordanceId2).joinToString(",")}",
+            ),
+        )
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId1, affordanceId3),
+                slotId2 to listOf(affordanceId2),
+            ),
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun `selections ignores defaults after selecting an affordance`() = runTest {
+        val slotId1 = "slot1"
+        val slotId2 = "slot2"
+        val affordanceId1 = "affordance1"
+        val affordanceId2 = "affordance2"
+        val affordanceId3 = "affordance3"
+        overrideResource(
+            R.array.config_keyguardQuickAffordanceDefaults,
+            arrayOf(
+                "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
+                "$slotId2:${listOf(affordanceId2).joinToString(",")}",
+            ),
+        )
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+
+        underTest.setSelections(slotId1, listOf(affordanceId2))
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(affordanceId2),
+                slotId2 to listOf(affordanceId2),
+            ),
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun `selections ignores defaults after clearing a slot`() = runTest {
+        val slotId1 = "slot1"
+        val slotId2 = "slot2"
+        val affordanceId1 = "affordance1"
+        val affordanceId2 = "affordance2"
+        val affordanceId3 = "affordance3"
+        overrideResource(
+            R.array.config_keyguardQuickAffordanceDefaults,
+            arrayOf(
+                "$slotId1:${listOf(affordanceId1, affordanceId3).joinToString(",")}",
+                "$slotId2:${listOf(affordanceId2).joinToString(",")}",
+            ),
+        )
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+
+        underTest.setSelections(slotId1, listOf())
+        assertSelections(
+            affordanceIdsBySlotId.last(),
+            mapOf(
+                slotId1 to listOf(),
+                slotId2 to listOf(affordanceId2),
+            ),
+        )
+
+        job.cancel()
+    }
+
+    private fun assertSelections(
+        observed: Map<String, List<String>>?,
+        expected: Map<String, List<String>>,
+    ) {
+        assertThat(underTest.getSelections()).isEqualTo(expected)
+        assertThat(observed).isEqualTo(expected)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..2bd8e9a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,149 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+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
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var controller: QRCodeScannerController
+
+    private lateinit var underTest: QrCodeScannerKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(controller.intent).thenReturn(INTENT_1)
+
+        underTest = QrCodeScannerKeyguardQuickAffordanceConfig(mock(), controller)
+    }
+
+    @Test
+    fun `affordance - sets up registration and delivers initial model`() = runBlockingTest {
+        whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
+        verify(controller).addCallback(callbackCaptor.capture())
+        verify(controller)
+            .registerQRCodeScannerChangeObservers(
+                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+            )
+        assertVisibleState(latest)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun `affordance - scanner activity changed - delivers model with updated intent`() =
+        runBlockingTest {
+            whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
+            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.intent).thenReturn(INTENT_2)
+            callbackCaptor.value.onQRCodeScannerActivityChanged()
+
+            assertVisibleState(latest)
+
+            job.cancel()
+            verify(controller).removeCallback(callbackCaptor.value)
+        }
+
+    @Test
+    fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest {
+        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(true)
+        callbackCaptor.value.onQRCodeScannerPreferenceChanged()
+
+        assertVisibleState(latest)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun `affordance - scanner preference changed - delivers none`() = runBlockingTest {
+        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.LockScreenState.Hidden)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun onQuickAffordanceTriggered() {
+        assertThat(underTest.onTriggered(mock()))
+            .isEqualTo(
+                OnTriggeredResult.StartActivity(
+                    intent = INTENT_1,
+                    canShowWhileLocked = true,
+                )
+            )
+    }
+
+    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()
+    }
+
+    companion object {
+        private val INTENT_1 = Intent("intent1")
+        private val INTENT_2 = Intent("intent2")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..5178154
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityLaunchAnimator
+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.plugins.ActivityStarter
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.wallet.controller.QuickAccessWalletController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var walletController: QuickAccessWalletController
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    private lateinit var underTest: QuickAccessWalletKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            QuickAccessWalletKeyguardQuickAffordanceConfig(
+                mock(),
+                walletController,
+                activityStarter,
+            )
+    }
+
+    @Test
+    fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest {
+        setUpState()
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
+        assertThat(visibleModel.icon)
+            .isEqualTo(
+                Icon.Loaded(
+                    drawable = ICON,
+                    contentDescription =
+                        ContentDescription.Resource(
+                            res = R.string.accessibility_wallet_button,
+                        ),
+                )
+            )
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - wallet not enabled - model is none`() = runBlockingTest {
+        setUpState(isWalletEnabled = false)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - query not successful - model is none`() = runBlockingTest {
+        setUpState(isWalletQuerySuccessful = false)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - missing icon - model is none`() = runBlockingTest {
+        setUpState(hasWalletIcon = false)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - no selected card - model is none`() = runBlockingTest {
+        setUpState(hasWalletIcon = false)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun onQuickAffordanceTriggered() {
+        val animationController: ActivityLaunchAnimator.Controller = mock()
+        val expandable: Expandable = mock {
+            whenever(this.activityLaunchController()).thenReturn(animationController)
+        }
+
+        assertThat(underTest.onTriggered(expandable))
+            .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled)
+        verify(walletController)
+            .startQuickAccessUiIntent(
+                activityStarter,
+                animationController,
+                /* hasCard= */ true,
+            )
+    }
+
+    private fun setUpState(
+        isWalletEnabled: Boolean = true,
+        isWalletQuerySuccessful: Boolean = true,
+        hasWalletIcon: Boolean = true,
+        hasSelectedCard: Boolean = true,
+    ) {
+        whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled)
+
+        val walletClient: QuickAccessWalletClient = mock()
+        val icon: Drawable? =
+            if (hasWalletIcon) {
+                ICON
+            } else {
+                null
+            }
+        whenever(walletClient.tileIcon).thenReturn(icon)
+        whenever(walletController.walletClient).thenReturn(walletClient)
+
+        whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
+            with(
+                invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback
+            ) {
+                if (isWalletQuerySuccessful) {
+                    onWalletCardsRetrieved(
+                        if (hasSelectedCard) {
+                            GetWalletCardsResponse(listOf(mock()), 0)
+                        } else {
+                            GetWalletCardsResponse(emptyList(), 0)
+                        }
+                    )
+                } else {
+                    onWalletCardRetrievalError(mock())
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val ICON: Drawable = mock()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
new file mode 100644
index 0000000..d8a3605
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.UserFileManager
+import com.android.systemui.util.FakeSharedPreferences
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardQuickAffordanceRepository
+
+    private lateinit var config1: FakeKeyguardQuickAffordanceConfig
+    private lateinit var config2: FakeKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        config1 = FakeKeyguardQuickAffordanceConfig("built_in:1")
+        config2 = FakeKeyguardQuickAffordanceConfig("built_in:2")
+        val scope = CoroutineScope(IMMEDIATE)
+        val selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock<UserFileManager>().apply {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = FakeUserTracker(),
+            )
+
+        underTest =
+            KeyguardQuickAffordanceRepository(
+                appContext = context,
+                scope = scope,
+                selectionManager = selectionManager,
+                legacySettingSyncer =
+                    KeyguardQuickAffordanceLegacySettingSyncer(
+                        scope = scope,
+                        backgroundDispatcher = IMMEDIATE,
+                        secureSettings = FakeSettings(),
+                        selectionsManager = selectionManager,
+                    ),
+                configs = setOf(config1, config2),
+            )
+    }
+
+    @Test
+    fun setSelections() =
+        runBlocking(IMMEDIATE) {
+            var configsBySlotId: Map<String, List<KeyguardQuickAffordanceConfig>>? = null
+            val job = underTest.selections.onEach { configsBySlotId = it }.launchIn(this)
+            val slotId1 = "slot1"
+            val slotId2 = "slot2"
+
+            underTest.setSelections(slotId1, listOf(config1.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to listOf(config1),
+                ),
+            )
+
+            underTest.setSelections(slotId2, listOf(config2.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to listOf(config1),
+                    slotId2 to listOf(config2),
+                ),
+            )
+
+            underTest.setSelections(slotId1, emptyList())
+            underTest.setSelections(slotId2, listOf(config1.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to emptyList(),
+                    slotId2 to listOf(config1),
+                ),
+            )
+
+            job.cancel()
+        }
+
+    @Test
+    fun getAffordancePickerRepresentations() {
+        assertThat(underTest.getAffordancePickerRepresentations())
+            .isEqualTo(
+                listOf(
+                    KeyguardQuickAffordancePickerRepresentation(
+                        id = config1.key,
+                        name = config1.pickerName,
+                        iconResourceId = config1.pickerIconResourceId,
+                    ),
+                    KeyguardQuickAffordancePickerRepresentation(
+                        id = config2.key,
+                        name = config2.pickerName,
+                        iconResourceId = config2.pickerIconResourceId,
+                    ),
+                )
+            )
+    }
+
+    @Test
+    fun getSlotPickerRepresentations() {
+        val slot1 = "slot1"
+        val slot2 = "slot2"
+        val slot3 = "slot3"
+        context.orCreateTestableResources.addOverride(
+            R.array.config_keyguardQuickAffordanceSlots,
+            arrayOf(
+                "$slot1:2",
+                "$slot2:4",
+                "$slot3:5",
+            ),
+        )
+
+        assertThat(underTest.getSlotPickerRepresentations())
+            .isEqualTo(
+                listOf(
+                    KeyguardSlotPickerRepresentation(
+                        id = slot1,
+                        maxSelectedAffordances = 2,
+                    ),
+                    KeyguardSlotPickerRepresentation(
+                        id = slot2,
+                        maxSelectedAffordances = 4,
+                    ),
+                    KeyguardSlotPickerRepresentation(
+                        id = slot3,
+                        maxSelectedAffordances = 5,
+                    ),
+                )
+            )
+    }
+
+    private suspend fun assertSelections(
+        observed: Map<String, List<KeyguardQuickAffordanceConfig>>?,
+        expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
+    ) {
+        assertThat(observed).isEqualTo(expected)
+        assertThat(underTest.getSelections())
+            .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
+        expected.forEach { (slotId, configs) ->
+            assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
+        }
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 7a15680..6ba0634 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -17,10 +17,16 @@
 package com.android.systemui.keyguard.data.repository
 
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.doze.DozeHost
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.whenever
@@ -43,6 +49,9 @@
     @Mock private lateinit var statusBarStateController: StatusBarStateController
     @Mock private lateinit var dozeHost: DozeHost
     @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var biometricUnlockController: BiometricUnlockController
 
     private lateinit var underTest: KeyguardRepositoryImpl
 
@@ -53,8 +62,11 @@
         underTest =
             KeyguardRepositoryImpl(
                 statusBarStateController,
-                keyguardStateController,
                 dozeHost,
+                wakefulnessLifecycle,
+                biometricUnlockController,
+                keyguardStateController,
+                keyguardUpdateMonitor,
             )
     }
 
@@ -184,4 +196,143 @@
         job.cancel()
         verify(statusBarStateController).removeCallback(captor.value)
     }
+
+    @Test
+    fun wakefulness() = runBlockingTest {
+        val values = mutableListOf<WakefulnessModel>()
+        val job = underTest.wakefulnessState.onEach(values::add).launchIn(this)
+
+        val captor = argumentCaptor<WakefulnessLifecycle.Observer>()
+        verify(wakefulnessLifecycle).addObserver(captor.capture())
+
+        captor.value.onStartedWakingUp()
+        captor.value.onFinishedWakingUp()
+        captor.value.onStartedGoingToSleep()
+        captor.value.onFinishedGoingToSleep()
+
+        assertThat(values)
+            .isEqualTo(
+                listOf(
+                    // Initial value will be ASLEEP
+                    WakefulnessModel.ASLEEP,
+                    WakefulnessModel.STARTING_TO_WAKE,
+                    WakefulnessModel.AWAKE,
+                    WakefulnessModel.STARTING_TO_SLEEP,
+                    WakefulnessModel.ASLEEP,
+                )
+            )
+
+        job.cancel()
+        verify(wakefulnessLifecycle).removeObserver(captor.value)
+    }
+
+    @Test
+    fun isUdfpsSupported() = runBlockingTest {
+        whenever(keyguardUpdateMonitor.isUdfpsSupported).thenReturn(true)
+        assertThat(underTest.isUdfpsSupported()).isTrue()
+
+        whenever(keyguardUpdateMonitor.isUdfpsSupported).thenReturn(false)
+        assertThat(underTest.isUdfpsSupported()).isFalse()
+    }
+
+    @Test
+    fun isBouncerShowing() = runBlockingTest {
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isBouncerShowing.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardStateController.Callback>()
+        verify(keyguardStateController).addCallback(captor.capture())
+
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(true)
+        captor.value.onBouncerShowingChanged()
+        assertThat(latest).isTrue()
+
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
+        captor.value.onBouncerShowingChanged()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isKeyguardGoingAway() = runBlockingTest {
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isKeyguardGoingAway.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardStateController.Callback>()
+        verify(keyguardStateController).addCallback(captor.capture())
+
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(true)
+        captor.value.onKeyguardGoingAwayChanged()
+        assertThat(latest).isTrue()
+
+        whenever(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
+        captor.value.onKeyguardGoingAwayChanged()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isDreaming() = runBlockingTest {
+        whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isDreaming.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(captor.capture())
+
+        captor.value.onDreamingStateChanged(true)
+        assertThat(latest).isTrue()
+
+        captor.value.onDreamingStateChanged(false)
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun biometricUnlockState() = runBlockingTest {
+        val values = mutableListOf<BiometricUnlockModel>()
+        val job = underTest.biometricUnlockState.onEach(values::add).launchIn(this)
+
+        val captor = argumentCaptor<BiometricUnlockController.BiometricModeListener>()
+        verify(biometricUnlockController).addBiometricModeListener(captor.capture())
+
+        captor.value.onModeChanged(BiometricUnlockController.MODE_NONE)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_SHOW_BOUNCER)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_ONLY_WAKE)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_UNLOCK_COLLAPSING)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_DISMISS_BOUNCER)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM)
+
+        assertThat(values)
+            .isEqualTo(
+                listOf(
+                    // Initial value will be NONE, followed by onModeChanged() call
+                    BiometricUnlockModel.NONE,
+                    BiometricUnlockModel.NONE,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING,
+                    BiometricUnlockModel.SHOW_BOUNCER,
+                    BiometricUnlockModel.ONLY_WAKE,
+                    BiometricUnlockModel.UNLOCK_COLLAPSING,
+                    BiometricUnlockModel.DISMISS_BOUNCER,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM,
+                )
+            )
+
+        job.cancel()
+        verify(biometricUnlockController).removeBiometricModeListener(captor.value)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
new file mode 100644
index 0000000..2b03722
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -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.systemui.keyguard.data.repository
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider
+import android.animation.ValueAnimator
+import android.util.Log
+import android.util.Log.TerribleFailure
+import android.util.Log.TerribleFailureHandler
+import android.view.Choreographer.FrameCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardTransitionRepository
+    private lateinit var oldWtfHandler: TerribleFailureHandler
+    private lateinit var wtfHandler: WtfHandler
+
+    @Before
+    fun setUp() {
+        underTest = KeyguardTransitionRepositoryImpl()
+        wtfHandler = WtfHandler()
+        oldWtfHandler = Log.setWtfHandler(wtfHandler)
+    }
+
+    @After
+    fun tearDown() {
+        oldWtfHandler?.let { Log.setWtfHandler(it) }
+    }
+
+    @Test
+    fun `startTransition runs animator to completion`() =
+        runBlocking(IMMEDIATE) {
+            val (animator, provider) = setupAnimator(this)
+
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+
+            val startTime = System.currentTimeMillis()
+            while (animator.isRunning()) {
+                yield()
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
+
+            job.cancel()
+            provider.stop()
+        }
+
+    @Test
+    fun `starting second transition will cancel the first transition`() {
+        runBlocking(IMMEDIATE) {
+            val (animator, provider) = setupAnimator(this)
+
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+            // 3 yields(), alternating with the animator, results in a value 0.1, which can be
+            // canceled and tested against
+            yield()
+            yield()
+            yield()
+
+            // Now start 2nd transition, which will interrupt the first
+            val job2 = underTest.transition(LOCKSCREEN, AOD).onEach { steps.add(it) }.launchIn(this)
+            val (animator2, provider2) = setupAnimator(this)
+            underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, AOD, animator2))
+
+            val startTime = System.currentTimeMillis()
+            while (animator2.isRunning()) {
+                yield()
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
+            assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
+
+            val secondTransitionSteps = listWithStep(step = BigDecimal(.1), start = BigDecimal(.9))
+            assertSteps(steps.subList(4, steps.size), secondTransitionSteps, LOCKSCREEN, AOD)
+
+            job.cancel()
+            job2.cancel()
+            provider.stop()
+            provider2.stop()
+        }
+    }
+
+    @Test
+    fun `Null animator enables manual control with updateTransition`() =
+        runBlocking(IMMEDIATE) {
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            val uuid =
+                underTest.startTransition(
+                    TransitionInfo(
+                        ownerName = OWNER_NAME,
+                        from = AOD,
+                        to = LOCKSCREEN,
+                        animator = null,
+                    )
+                )
+
+            checkNotNull(uuid).let {
+                underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+                underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            }
+
+            assertThat(steps.size).isEqualTo(3)
+            assertThat(steps[0])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
+            assertThat(steps[1])
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING, OWNER_NAME)
+                )
+            assertThat(steps[2])
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME)
+                )
+            job.cancel()
+        }
+
+    @Test
+    fun `Attempt to  manually update transition with invalid UUID throws exception`() {
+        underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    @Test
+    fun `Attempt to manually update transition after FINISHED state throws exception`() {
+        val uuid =
+            underTest.startTransition(
+                TransitionInfo(
+                    ownerName = OWNER_NAME,
+                    from = AOD,
+                    to = LOCKSCREEN,
+                    animator = null,
+                )
+            )
+
+        checkNotNull(uuid).let {
+            underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+        }
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    private fun listWithStep(
+        step: BigDecimal,
+        start: BigDecimal = BigDecimal.ZERO,
+        stop: BigDecimal = BigDecimal.ONE,
+    ): List<BigDecimal> {
+        val steps = mutableListOf<BigDecimal>()
+
+        var i = start
+        while (i.compareTo(stop) <= 0) {
+            steps.add(i)
+            i = (i + step).setScale(2, RoundingMode.HALF_UP)
+        }
+
+        return steps
+    }
+
+    private fun assertSteps(
+        steps: List<TransitionStep>,
+        fractions: List<BigDecimal>,
+        from: KeyguardState,
+        to: KeyguardState,
+    ) {
+        assertThat(steps[0])
+            .isEqualTo(
+                TransitionStep(
+                    from,
+                    to,
+                    fractions[0].toFloat(),
+                    TransitionState.STARTED,
+                    OWNER_NAME
+                )
+            )
+        fractions.forEachIndexed { index, fraction ->
+            assertThat(steps[index + 1])
+                .isEqualTo(
+                    TransitionStep(
+                        from,
+                        to,
+                        fraction.toFloat(),
+                        TransitionState.RUNNING,
+                        OWNER_NAME
+                    )
+                )
+        }
+        val lastValue = fractions[fractions.size - 1].toFloat()
+        val status =
+            if (lastValue < 1f) {
+                TransitionState.CANCELED
+            } else {
+                TransitionState.FINISHED
+            }
+        assertThat(steps[steps.size - 1])
+            .isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME))
+
+        assertThat(wtfHandler.failed).isFalse()
+    }
+
+    private fun setupAnimator(
+        scope: CoroutineScope
+    ): Pair<ValueAnimator, TestFrameCallbackProvider> {
+        val animator =
+            ValueAnimator().apply {
+                setInterpolator(Interpolators.LINEAR)
+                setDuration(ANIMATION_DURATION)
+            }
+
+        val provider = TestFrameCallbackProvider(animator, scope)
+        provider.start()
+
+        return Pair(animator, provider)
+    }
+
+    /** Gives direct control over ValueAnimator. See [AnimationHandler] */
+    private class TestFrameCallbackProvider(
+        private val animator: ValueAnimator,
+        private val scope: CoroutineScope,
+    ) : AnimationFrameCallbackProvider {
+
+        private var frameCount = 1L
+        private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
+        private var job: Job? = null
+
+        fun start() {
+            animator.getAnimationHandler().setProvider(this)
+
+            job =
+                scope.launch {
+                    frames.collect {
+                        // Delay is required for AnimationHandler to properly register a callback
+                        yield()
+                        val (frameNumber, callback) = it
+                        callback?.doFrame(frameNumber)
+                    }
+                }
+        }
+
+        fun stop() {
+            job?.cancel()
+            animator.getAnimationHandler().setProvider(null)
+        }
+
+        override fun postFrameCallback(cb: FrameCallback) {
+            frames.value = Pair(frameCount++, cb)
+        }
+        override fun postCommitCallback(runnable: Runnable) {}
+        override fun getFrameTime() = frameCount
+        override fun getFrameDelay() = 1L
+        override fun setFrameDelay(delay: Long) {}
+    }
+
+    private class WtfHandler : TerribleFailureHandler {
+        var failed = false
+        override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) {
+            failed = true
+        }
+    }
+
+    companion object {
+        private const val MAX_TEST_DURATION = 100L
+        private const val ANIMATION_DURATION = 10L
+        private const val OWNER_NAME = "Test"
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt
deleted file mode 100644
index 3a61c57..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt
+++ /dev/null
@@ -1,86 +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.interactor
-
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.KeyguardBouncer
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class BouncerCallbackInteractorTest : SysuiTestCase() {
-    private val bouncerCallbackInteractor = BouncerCallbackInteractor()
-    @Mock private lateinit var bouncerExpansionCallback: KeyguardBouncer.BouncerExpansionCallback
-    @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        bouncerCallbackInteractor.addBouncerExpansionCallback(bouncerExpansionCallback)
-        bouncerCallbackInteractor.addKeyguardResetCallback(keyguardResetCallback)
-    }
-
-    @Test
-    fun testOnFullyShown() {
-        bouncerCallbackInteractor.dispatchFullyShown()
-        verify(bouncerExpansionCallback).onFullyShown()
-    }
-
-    @Test
-    fun testOnFullyHidden() {
-        bouncerCallbackInteractor.dispatchFullyHidden()
-        verify(bouncerExpansionCallback).onFullyHidden()
-    }
-
-    @Test
-    fun testOnExpansionChanged() {
-        bouncerCallbackInteractor.dispatchExpansionChanged(5f)
-        verify(bouncerExpansionCallback).onExpansionChanged(5f)
-    }
-
-    @Test
-    fun testOnVisibilityChanged() {
-        bouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
-        verify(bouncerExpansionCallback).onVisibilityChanged(false)
-    }
-
-    @Test
-    fun testOnStartingToHide() {
-        bouncerCallbackInteractor.dispatchStartingToHide()
-        verify(bouncerExpansionCallback).onStartingToHide()
-    }
-
-    @Test
-    fun testOnStartingToShow() {
-        bouncerCallbackInteractor.dispatchStartingToShow()
-        verify(bouncerExpansionCallback).onStartingToShow()
-    }
-
-    @Test
-    fun testOnKeyguardReset() {
-        bouncerCallbackInteractor.dispatchReset()
-        verify(keyguardResetCallback).onKeyguardReset()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt
deleted file mode 100644
index e6c8dd8..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt
+++ /dev/null
@@ -1,278 +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.interactor
-
-import android.os.Looper
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardSecurityModel
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.DejankUtils
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.BouncerView
-import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
-import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
-import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
-import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.utils.os.FakeHandler
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Answers
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class BouncerInteractorTest : SysuiTestCase() {
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
-    private lateinit var repository: KeyguardBouncerRepository
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
-    @Mock private lateinit var bouncerCallbackInteractor: BouncerCallbackInteractor
-    @Mock private lateinit var falsingCollector: FalsingCollector
-    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
-    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
-    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
-    private val mainHandler = FakeHandler(Looper.getMainLooper())
-    private lateinit var bouncerInteractor: BouncerInteractor
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        DejankUtils.setImmediate(true)
-        bouncerInteractor =
-            BouncerInteractor(
-                repository,
-                bouncerView,
-                mainHandler,
-                keyguardStateController,
-                keyguardSecurityModel,
-                bouncerCallbackInteractor,
-                falsingCollector,
-                dismissCallbackRegistry,
-                keyguardBypassController,
-                keyguardUpdateMonitor,
-            )
-        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        `when`(repository.show.value).thenReturn(null)
-    }
-
-    @Test
-    fun testShow_isScrimmed() {
-        bouncerInteractor.show(true)
-        verify(repository).setShowMessage(null)
-        verify(repository).setOnScreenTurnedOff(false)
-        verify(repository).setKeyguardAuthenticated(null)
-        verify(repository).setHide(false)
-        verify(repository).setStartingToHide(false)
-        verify(repository).setScrimmed(true)
-        verify(repository).setExpansion(EXPANSION_VISIBLE)
-        verify(repository).setShowingSoon(true)
-        verify(keyguardStateController).notifyBouncerShowing(true)
-        verify(bouncerCallbackInteractor).dispatchStartingToShow()
-        verify(repository).setVisible(true)
-        verify(repository).setShow(any(KeyguardBouncerModel::class.java))
-        verify(repository).setShowingSoon(false)
-    }
-
-    @Test
-    fun testShow_isNotScrimmed() {
-        verify(repository, never()).setExpansion(EXPANSION_VISIBLE)
-    }
-
-    @Test
-    fun testShow_keyguardIsDone() {
-        `when`(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
-        verify(keyguardStateController, never()).notifyBouncerShowing(true)
-        verify(bouncerCallbackInteractor, never()).dispatchStartingToShow()
-    }
-
-    @Test
-    fun testHide() {
-        bouncerInteractor.hide()
-        verify(falsingCollector).onBouncerHidden()
-        verify(keyguardStateController).notifyBouncerShowing(false)
-        verify(repository).setShowingSoon(false)
-        verify(repository).setOnDismissAction(null)
-        verify(repository).setVisible(false)
-        verify(repository).setHide(true)
-        verify(repository).setShow(null)
-    }
-
-    @Test
-    fun testExpansion() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
-        bouncerInteractor.setExpansion(0.6f)
-        verify(repository).setExpansion(0.6f)
-        verify(bouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
-    }
-
-    @Test
-    fun testExpansion_fullyShown() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
-        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        bouncerInteractor.setExpansion(EXPANSION_VISIBLE)
-        verify(falsingCollector).onBouncerShown()
-        verify(bouncerCallbackInteractor).dispatchFullyShown()
-    }
-
-    @Test
-    fun testExpansion_fullyHidden() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
-        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        bouncerInteractor.setExpansion(EXPANSION_HIDDEN)
-        verify(repository).setVisible(false)
-        verify(repository).setShow(null)
-        verify(falsingCollector).onBouncerHidden()
-        verify(bouncerCallbackInteractor).dispatchReset()
-        verify(bouncerCallbackInteractor).dispatchFullyHidden()
-    }
-
-    @Test
-    fun testExpansion_startingToHide() {
-        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        bouncerInteractor.setExpansion(0.1f)
-        verify(repository).setStartingToHide(true)
-        verify(bouncerCallbackInteractor).dispatchStartingToHide()
-    }
-
-    @Test
-    fun testShowMessage() {
-        bouncerInteractor.showMessage("abc", null)
-        verify(repository).setShowMessage(BouncerShowMessageModel("abc", null))
-    }
-
-    @Test
-    fun testDismissAction() {
-        val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
-        val cancelAction = mock(Runnable::class.java)
-        bouncerInteractor.setDismissAction(onDismissAction, cancelAction)
-        verify(repository)
-            .setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
-    }
-
-    @Test
-    fun testUpdateResources() {
-        bouncerInteractor.updateResources()
-        verify(repository).setResourceUpdateRequests(true)
-    }
-
-    @Test
-    fun testNotifyKeyguardAuthenticated() {
-        bouncerInteractor.notifyKeyguardAuthenticated(true)
-        verify(repository).setKeyguardAuthenticated(true)
-    }
-
-    @Test
-    fun testOnScreenTurnedOff() {
-        bouncerInteractor.onScreenTurnedOff()
-        verify(repository).setOnScreenTurnedOff(true)
-    }
-
-    @Test
-    fun testSetKeyguardPosition() {
-        bouncerInteractor.setKeyguardPosition(0f)
-        verify(repository).setKeyguardPosition(0f)
-    }
-
-    @Test
-    fun testNotifyKeyguardAuthenticatedHandled() {
-        bouncerInteractor.notifyKeyguardAuthenticatedHandled()
-        verify(repository).setKeyguardAuthenticated(null)
-    }
-
-    @Test
-    fun testNotifyUpdatedResources() {
-        bouncerInteractor.notifyUpdatedResources()
-        verify(repository).setResourceUpdateRequests(false)
-    }
-
-    @Test
-    fun testSetBackButtonEnabled() {
-        bouncerInteractor.setBackButtonEnabled(true)
-        verify(repository).setIsBackButtonEnabled(true)
-    }
-
-    @Test
-    fun testStartDisappearAnimation() {
-        val runnable = mock(Runnable::class.java)
-        bouncerInteractor.startDisappearAnimation(runnable)
-        verify(repository).setStartDisappearAnimation(any(Runnable::class.java))
-    }
-
-    @Test
-    fun testIsFullShowing() {
-        `when`(repository.isVisible.value).thenReturn(true)
-        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        assertThat(bouncerInteractor.isFullyShowing()).isTrue()
-        `when`(repository.isVisible.value).thenReturn(false)
-        assertThat(bouncerInteractor.isFullyShowing()).isFalse()
-    }
-
-    @Test
-    fun testIsScrimmed() {
-        `when`(repository.isScrimmed.value).thenReturn(true)
-        assertThat(bouncerInteractor.isScrimmed()).isTrue()
-        `when`(repository.isScrimmed.value).thenReturn(false)
-        assertThat(bouncerInteractor.isScrimmed()).isFalse()
-    }
-
-    @Test
-    fun testIsInTransit() {
-        `when`(repository.showingSoon.value).thenReturn(true)
-        assertThat(bouncerInteractor.isInTransit()).isTrue()
-        `when`(repository.showingSoon.value).thenReturn(false)
-        assertThat(bouncerInteractor.isInTransit()).isFalse()
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
-        assertThat(bouncerInteractor.isInTransit()).isTrue()
-    }
-
-    @Test
-    fun testIsAnimatingAway() {
-        `when`(repository.startingDisappearAnimation.value).thenReturn(Runnable {})
-        assertThat(bouncerInteractor.isAnimatingAway()).isTrue()
-        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        assertThat(bouncerInteractor.isAnimatingAway()).isFalse()
-    }
-
-    @Test
-    fun testWillDismissWithAction() {
-        `when`(repository.onDismissAction.value?.onDismissAction)
-            .thenReturn(mock(ActivityStarter.OnDismissAction::class.java))
-        assertThat(bouncerInteractor.willDismissWithAction()).isTrue()
-        `when`(repository.onDismissAction.value?.onDismissAction).thenReturn(null)
-        assertThat(bouncerInteractor.willDismissWithAction()).isFalse()
-    }
-}
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..1e1d3f1 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,17 +25,29 @@
 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.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+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.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 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.data.repository.KeyguardQuickAffordanceRepository
 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.FakeUserTracker
+import com.android.systemui.settings.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.Before
 import org.junit.Test
@@ -43,6 +55,8 @@
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameter
 import org.junit.runners.Parameterized.Parameters
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.ArgumentMatchers.same
 import org.mockito.Mock
@@ -188,11 +202,12 @@
                     /* startActivity= */ true,
                 ),
             )
+
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
     @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var userTracker: UserTracker
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
     @Mock private lateinit var expandable: Expandable
@@ -205,13 +220,53 @@
     @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false
     @JvmField @Parameter(4) var startActivity: Boolean = false
     private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
+    private lateinit var userTracker: UserTracker
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(expandable.activityLaunchController()).thenReturn(animationController)
 
-        homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
+        userTracker = FakeUserTracker()
+        homeControls =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+        val quickAccessWallet =
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
+        val qrCodeScanner =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
+        val scope = CoroutineScope(IMMEDIATE)
+        val selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock<UserFileManager>().apply {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = userTracker,
+            )
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                appContext = context,
+                scope = scope,
+                selectionManager = selectionManager,
+                legacySettingSyncer =
+                    KeyguardQuickAffordanceLegacySettingSyncer(
+                        scope = scope,
+                        backgroundDispatcher = IMMEDIATE,
+                        secureSettings = FakeSettings(),
+                        selectionsManager = selectionManager,
+                    ),
+                configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
+            )
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
@@ -224,8 +279,8 @@
                                 ),
                             KeyguardQuickAffordancePosition.BOTTOM_END to
                                 listOf(
-                                    object : FakeKeyguardQuickAffordanceConfig() {},
-                                    object : FakeKeyguardQuickAffordanceConfig() {},
+                                    quickAccessWallet,
+                                    qrCodeScanner,
                                 ),
                         ),
                     ),
@@ -233,34 +288,39 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
+                featureFlags =
+                    FakeFeatureFlags().apply {
+                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                    },
+                repository = { quickAffordanceRepository },
             )
     }
 
     @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,
         )
 
@@ -290,7 +350,6 @@
         needStrongAuthAfterBoot: Boolean = true,
         keyguardIsUnlocked: Boolean = false,
     ) {
-        whenever(userTracker.userHandle).thenReturn(mock())
         whenever(lockPatternUtils.getStrongAuthForUser(any()))
             .thenReturn(
                 if (needStrongAuthAfterBoot) {
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..c47e6f5 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,30 +22,47 @@
 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.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+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.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 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.UserFileManager
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@@ -61,6 +78,7 @@
     private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
     private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
     private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig
+    private lateinit var featureFlags: FakeFeatureFlags
 
     @Before
     fun setUp() {
@@ -69,9 +87,50 @@
         repository = FakeKeyguardRepository()
         repository.setKeyguardShowing(true)
 
-        homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
-        quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {}
-        qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {}
+        homeControls =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+        quickAccessWallet =
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
+        qrCodeScanner =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
+        val scope = CoroutineScope(IMMEDIATE)
+
+        val selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock<UserFileManager>().apply {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = userTracker,
+            )
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                appContext = context,
+                scope = scope,
+                selectionManager = selectionManager,
+                legacySettingSyncer =
+                    KeyguardQuickAffordanceLegacySettingSyncer(
+                        scope = scope,
+                        backgroundDispatcher = IMMEDIATE,
+                        secureSettings = FakeSettings(),
+                        selectionsManager = selectionManager,
+                    ),
+                configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
+            )
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+            }
 
         underTest =
             KeyguardQuickAffordanceInteractor(
@@ -94,16 +153,18 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
+                featureFlags = featureFlags,
+                repository = { quickAffordanceRepository },
             )
     }
 
     @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 +185,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 +215,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 +223,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 +243,7 @@
         runBlockingTest {
             repository.setKeyguardShowing(false)
             homeControls.setState(
-                KeyguardQuickAffordanceConfig.State.Visible(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     icon = ICON,
                 )
             )
@@ -197,6 +258,270 @@
             job.cancel()
         }
 
+    @Test
+    fun select() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            var startConfig: KeyguardQuickAffordanceModel? = null
+            val job1 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                    .onEach { startConfig = it }
+                    .launchIn(this)
+            var endConfig: KeyguardQuickAffordanceModel? = null
+            val job2 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                    .onEach { endConfig = it }
+                    .launchIn(this)
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${homeControls.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(homeControls.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            underTest.select(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                quickAccessWallet.key
+            )
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(quickAccessWallet.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key)
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
+                                "::${qrCodeScanner.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(quickAccessWallet.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(qrCodeScanner.key),
+                    )
+                )
+
+            job1.cancel()
+            job2.cancel()
+        }
+
+    @Test
+    fun `unselect - one`() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            var startConfig: KeyguardQuickAffordanceModel? = null
+            val job1 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                    .onEach { startConfig = it }
+                    .launchIn(this)
+            var endConfig: KeyguardQuickAffordanceModel? = null
+            val job2 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                    .onEach { endConfig = it }
+                    .launchIn(this)
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
+            yield()
+            yield()
+
+            underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(quickAccessWallet.key),
+                    )
+                )
+
+            underTest.unselect(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                quickAccessWallet.key
+            )
+            yield()
+            yield()
+
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            job1.cancel()
+            job2.cancel()
+        }
+
+    @Test
+    fun `unselect - all`() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
+            yield()
+            yield()
+
+            underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null)
+            yield()
+            yield()
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(quickAccessWallet.key),
+                    )
+                )
+
+            underTest.unselect(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                null,
+            )
+            yield()
+            yield()
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+        }
+
     companion object {
         private val ICON: Icon = mock {
             whenever(this.contentDescription)
@@ -207,5 +532,6 @@
                 )
         }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
new file mode 100644
index 0000000..6333b24
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardTransitionInteractor
+    private lateinit var repository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardTransitionRepository()
+        underTest = KeyguardTransitionInteractor(repository)
+    }
+
+    @Test
+    fun `transition collectors receives only appropriate events`() =
+        runBlocking(IMMEDIATE) {
+            var lockscreenToAodSteps = mutableListOf<TransitionStep>()
+            val job1 =
+                underTest.lockscreenToAodTransition
+                    .onEach { lockscreenToAodSteps.add(it) }
+                    .launchIn(this)
+
+            var aodToLockscreenSteps = mutableListOf<TransitionStep>()
+            val job2 =
+                underTest.aodToLockscreenTransition
+                    .onEach { aodToLockscreenSteps.add(it) }
+                    .launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+            steps.add(TransitionStep(AOD, GONE, 0f, STARTED))
+            steps.add(TransitionStep(AOD, GONE, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5))
+            assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8))
+
+            job1.cancel()
+            job2.cancel()
+        }
+
+    @Test
+    fun dozeAmountTransitionTest() =
+        runBlocking(IMMEDIATE) {
+            var dozeAmountSteps = mutableListOf<TransitionStep>()
+            val job =
+                underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.8f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(dozeAmountSteps.subList(0, 3))
+                .isEqualTo(
+                    listOf(
+                        steps[0].copy(value = 1f - steps[0].value),
+                        steps[1].copy(value = 1f - steps[1].value),
+                        steps[2].copy(value = 1f - steps[2].value),
+                    )
+                )
+            assertThat(dozeAmountSteps.subList(3, 7)).isEqualTo(steps.subList(3, 7))
+
+            job.cancel()
+        }
+
+    @Test
+    fun keyguardStateTests() =
+        runBlocking(IMMEDIATE) {
+            var finishedSteps = mutableListOf<KeyguardState>()
+            val job =
+                underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD))
+
+            job.cancel()
+        }
+
+    @Test
+    fun finishedKeyguardTransitionStepTests() =
+        runBlocking(IMMEDIATE) {
+            var finishedSteps = mutableListOf<TransitionStep>()
+            val job =
+                underTest.finishedKeyguardTransitionStep
+                    .onEach { finishedSteps.add(it) }
+                    .launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5]))
+
+            job.cancel()
+        }
+
+    @Test
+    fun startedKeyguardTransitionStepTests() =
+        runBlocking(IMMEDIATE) {
+            var startedSteps = mutableListOf<TransitionStep>()
+            val job =
+                underTest.startedKeyguardTransitionStep
+                    .onEach { startedSteps.add(it) }
+                    .launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6]))
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
new file mode 100644
index 0000000..db9c4e7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.interactor
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
+    private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
+    @Mock
+    private lateinit var mPrimaryBouncerExpansionCallback:
+        KeyguardBouncer.PrimaryBouncerExpansionCallback
+    @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(
+            mPrimaryBouncerExpansionCallback
+        )
+        mPrimaryBouncerCallbackInteractor.addKeyguardResetCallback(keyguardResetCallback)
+    }
+
+    @Test
+    fun testOnFullyShown() {
+        mPrimaryBouncerCallbackInteractor.dispatchFullyShown()
+        verify(mPrimaryBouncerExpansionCallback).onFullyShown()
+    }
+
+    @Test
+    fun testOnFullyHidden() {
+        mPrimaryBouncerCallbackInteractor.dispatchFullyHidden()
+        verify(mPrimaryBouncerExpansionCallback).onFullyHidden()
+    }
+
+    @Test
+    fun testOnExpansionChanged() {
+        mPrimaryBouncerCallbackInteractor.dispatchExpansionChanged(5f)
+        verify(mPrimaryBouncerExpansionCallback).onExpansionChanged(5f)
+    }
+
+    @Test
+    fun testOnVisibilityChanged() {
+        mPrimaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
+        verify(mPrimaryBouncerExpansionCallback).onVisibilityChanged(false)
+    }
+
+    @Test
+    fun testOnStartingToHide() {
+        mPrimaryBouncerCallbackInteractor.dispatchStartingToHide()
+        verify(mPrimaryBouncerExpansionCallback).onStartingToHide()
+    }
+
+    @Test
+    fun testOnStartingToShow() {
+        mPrimaryBouncerCallbackInteractor.dispatchStartingToShow()
+        verify(mPrimaryBouncerExpansionCallback).onStartingToShow()
+    }
+
+    @Test
+    fun testOnKeyguardReset() {
+        mPrimaryBouncerCallbackInteractor.dispatchReset()
+        verify(keyguardResetCallback).onKeyguardReset()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
new file mode 100644
index 0000000..3269f5a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -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 com.android.systemui.keyguard.domain.interactor
+
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.DejankUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class PrimaryBouncerInteractorTest : SysuiTestCase() {
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private lateinit var repository: KeyguardBouncerRepository
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
+    @Mock private lateinit var bouncerViewDelegate: BouncerViewDelegate
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+    @Mock private lateinit var mPrimaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        DejankUtils.setImmediate(true)
+        mPrimaryBouncerInteractor =
+            PrimaryBouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                mPrimaryBouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                keyguardBypassController,
+                keyguardUpdateMonitor,
+            )
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        `when`(repository.primaryBouncerShow.value).thenReturn(null)
+        `when`(bouncerView.delegate).thenReturn(bouncerViewDelegate)
+    }
+
+    @Test
+    fun testShow_isScrimmed() {
+        mPrimaryBouncerInteractor.show(true)
+        verify(repository).setOnScreenTurnedOff(false)
+        verify(repository).setKeyguardAuthenticated(null)
+        verify(repository).setPrimaryHide(false)
+        verify(repository).setPrimaryStartingToHide(false)
+        verify(repository).setPrimaryScrimmed(true)
+        verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
+        verify(repository).setPrimaryShowingSoon(true)
+        verify(keyguardStateController).notifyBouncerShowing(true)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToShow()
+        verify(repository).setPrimaryVisible(true)
+        verify(repository).setPrimaryShow(any(KeyguardBouncerModel::class.java))
+        verify(repository).setPrimaryShowingSoon(false)
+    }
+
+    @Test
+    fun testShow_isNotScrimmed() {
+        verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE)
+    }
+
+    @Test
+    fun testShow_keyguardIsDone() {
+        `when`(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
+        verify(keyguardStateController, never()).notifyBouncerShowing(true)
+        verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow()
+    }
+
+    @Test
+    fun testHide() {
+        mPrimaryBouncerInteractor.hide()
+        verify(falsingCollector).onBouncerHidden()
+        verify(keyguardStateController).notifyBouncerShowing(false)
+        verify(repository).setPrimaryShowingSoon(false)
+        verify(repository).setPrimaryVisible(false)
+        verify(repository).setPrimaryHide(true)
+        verify(repository).setPrimaryShow(null)
+    }
+
+    @Test
+    fun testExpansion() {
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        mPrimaryBouncerInteractor.setPanelExpansion(0.6f)
+        verify(repository).setPanelExpansion(0.6f)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
+    }
+
+    @Test
+    fun testExpansion_fullyShown() {
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE)
+        verify(falsingCollector).onBouncerShown()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyShown()
+    }
+
+    @Test
+    fun testExpansion_fullyHidden() {
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        mPrimaryBouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
+        verify(repository).setPrimaryVisible(false)
+        verify(repository).setPrimaryShow(null)
+        verify(repository).setPrimaryHide(true)
+        verify(falsingCollector).onBouncerHidden()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchReset()
+        verify(mPrimaryBouncerCallbackInteractor).dispatchFullyHidden()
+    }
+
+    @Test
+    fun testExpansion_startingToHide() {
+        `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        mPrimaryBouncerInteractor.setPanelExpansion(0.1f)
+        verify(repository).setPrimaryStartingToHide(true)
+        verify(mPrimaryBouncerCallbackInteractor).dispatchStartingToHide()
+    }
+
+    @Test
+    fun testShowMessage() {
+        mPrimaryBouncerInteractor.showMessage("abc", null)
+        verify(repository).setShowMessage(BouncerShowMessageModel("abc", null))
+    }
+
+    @Test
+    fun testDismissAction() {
+        val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
+        val cancelAction = mock(Runnable::class.java)
+        mPrimaryBouncerInteractor.setDismissAction(onDismissAction, cancelAction)
+        verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
+    }
+
+    @Test
+    fun testUpdateResources() {
+        mPrimaryBouncerInteractor.updateResources()
+        verify(repository).setResourceUpdateRequests(true)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticated() {
+        mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(true)
+        verify(repository).setKeyguardAuthenticated(true)
+    }
+
+    @Test
+    fun testOnScreenTurnedOff() {
+        mPrimaryBouncerInteractor.onScreenTurnedOff()
+        verify(repository).setOnScreenTurnedOff(true)
+    }
+
+    @Test
+    fun testSetKeyguardPosition() {
+        mPrimaryBouncerInteractor.setKeyguardPosition(0f)
+        verify(repository).setKeyguardPosition(0f)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticatedHandled() {
+        mPrimaryBouncerInteractor.notifyKeyguardAuthenticatedHandled()
+        verify(repository).setKeyguardAuthenticated(null)
+    }
+
+    @Test
+    fun testNotifyUpdatedResources() {
+        mPrimaryBouncerInteractor.notifyUpdatedResources()
+        verify(repository).setResourceUpdateRequests(false)
+    }
+
+    @Test
+    fun testSetBackButtonEnabled() {
+        mPrimaryBouncerInteractor.setBackButtonEnabled(true)
+        verify(repository).setIsBackButtonEnabled(true)
+    }
+
+    @Test
+    fun testStartDisappearAnimation() {
+        val runnable = mock(Runnable::class.java)
+        mPrimaryBouncerInteractor.startDisappearAnimation(runnable)
+        verify(repository).setPrimaryStartDisappearAnimation(any(Runnable::class.java))
+    }
+
+    @Test
+    fun testIsFullShowing() {
+        `when`(repository.primaryBouncerVisible.value).thenReturn(true)
+        `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isTrue()
+        `when`(repository.primaryBouncerVisible.value).thenReturn(false)
+        assertThat(mPrimaryBouncerInteractor.isFullyShowing()).isFalse()
+    }
+
+    @Test
+    fun testIsScrimmed() {
+        `when`(repository.primaryBouncerScrimmed.value).thenReturn(true)
+        assertThat(mPrimaryBouncerInteractor.isScrimmed()).isTrue()
+        `when`(repository.primaryBouncerScrimmed.value).thenReturn(false)
+        assertThat(mPrimaryBouncerInteractor.isScrimmed()).isFalse()
+    }
+
+    @Test
+    fun testIsInTransit() {
+        `when`(repository.primaryBouncerShowingSoon.value).thenReturn(true)
+        assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+        `when`(repository.primaryBouncerShowingSoon.value).thenReturn(false)
+        assertThat(mPrimaryBouncerInteractor.isInTransit()).isFalse()
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        assertThat(mPrimaryBouncerInteractor.isInTransit()).isTrue()
+    }
+
+    @Test
+    fun testIsAnimatingAway() {
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(Runnable {})
+        assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isTrue()
+        `when`(repository.primaryBouncerStartingDisappearAnimation.value).thenReturn(null)
+        assertThat(mPrimaryBouncerInteractor.isAnimatingAway()).isFalse()
+    }
+
+    @Test
+    fun testWillDismissWithAction() {
+        `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
+        assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isTrue()
+        `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
+        assertThat(mPrimaryBouncerInteractor.willDismissWithAction()).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
deleted file mode 100644
index e99c139..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,56 +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.animation.Expandable
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.yield
-
-/**
- * Fake implementation of a quick affordance data source.
- *
- * 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 {
-
-    var onClickedResult: OnClickedResult = OnClickedResult.Handled
-
-    private val _state =
-        MutableStateFlow<KeyguardQuickAffordanceConfig.State>(
-            KeyguardQuickAffordanceConfig.State.Hidden
-        )
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state
-
-    override fun onQuickAffordanceClicked(
-        expandable: Expandable?,
-    ): OnClickedResult {
-        return onClickedResult
-    }
-
-    suspend fun setState(state: KeyguardQuickAffordanceConfig.State) {
-        _state.value = state
-        // 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
-        // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this.
-        yield()
-    }
-}
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/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
deleted file mode 100644
index 9a91ea91..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ /dev/null
@@ -1,149 +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 androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.controls.controller.ControlsController
-import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.controls.management.ControlsListingController
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import java.util.Optional
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(Parameterized::class)
-class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
-
-    companion object {
-        @Parameters(
-            name =
-                "feature enabled = {0}, has favorites = {1}, has service infos = {2}, can show" +
-                    " while locked = {3}, visibility is AVAILABLE {4} - expected visible = {5}"
-        )
-        @JvmStatic
-        fun data() =
-            (0 until 32)
-                .map { combination ->
-                    arrayOf(
-                        /* isFeatureEnabled= */ combination and 0b10000 != 0,
-                        /* hasFavorites= */ combination and 0b01000 != 0,
-                        /* hasServiceInfos= */ combination and 0b00100 != 0,
-                        /* canShowWhileLocked= */ combination and 0b00010 != 0,
-                        /* visibilityAvailable= */ combination and 0b00001 != 0,
-                        /* isVisible= */ combination == 0b11111,
-                    )
-                }
-                .toList()
-    }
-
-    @Mock private lateinit var component: ControlsComponent
-    @Mock private lateinit var controlsController: ControlsController
-    @Mock private lateinit var controlsListingController: ControlsListingController
-    @Captor
-    private lateinit var callbackCaptor:
-        ArgumentCaptor<ControlsListingController.ControlsListingCallback>
-
-    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
-
-    @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false
-    @JvmField @Parameter(1) var hasFavorites: Boolean = false
-    @JvmField @Parameter(2) var hasServiceInfos: Boolean = false
-    @JvmField @Parameter(3) var canShowWhileLocked: Boolean = false
-    @JvmField @Parameter(4) var isVisibilityAvailable: Boolean = false
-    @JvmField @Parameter(5) var isVisibleExpected: Boolean = false
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
-        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
-        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
-        whenever(component.getControlsListingController())
-            .thenReturn(Optional.of(controlsListingController))
-        whenever(component.canShowWhileLockedSetting)
-            .thenReturn(MutableStateFlow(canShowWhileLocked))
-        whenever(component.getVisibility())
-            .thenReturn(
-                if (isVisibilityAvailable) {
-                    ControlsComponent.Visibility.AVAILABLE
-                } else {
-                    ControlsComponent.Visibility.UNAVAILABLE
-                }
-            )
-
-        underTest =
-            HomeControlsKeyguardQuickAffordanceConfig(
-                context = context,
-                component = component,
-            )
-    }
-
-    @Test
-    fun state() = runBlockingTest {
-        whenever(component.isEnabled()).thenReturn(isFeatureEnabled)
-        whenever(controlsController.getFavorites())
-            .thenReturn(
-                if (hasFavorites) {
-                    listOf(mock())
-                } else {
-                    emptyList()
-                }
-            )
-        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
-        val job = underTest.state.onEach(values::add).launchIn(this)
-
-        if (canShowWhileLocked) {
-            verify(controlsListingController).addCallback(callbackCaptor.capture())
-            callbackCaptor.value.onServicesUpdated(
-                if (hasServiceInfos) {
-                    listOf(mock())
-                } else {
-                    emptyList()
-                }
-            )
-        }
-
-        assertThat(values.last())
-            .isInstanceOf(
-                if (isVisibleExpected) {
-                    KeyguardQuickAffordanceConfig.State.Visible::class.java
-                } else {
-                    KeyguardQuickAffordanceConfig.State.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/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
deleted file mode 100644
index a809f05..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.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 androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-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.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import java.util.Optional
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
-
-    @Mock private lateinit var component: ControlsComponent
-    @Mock private lateinit var expandable: Expandable
-
-    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
-
-        underTest =
-            HomeControlsKeyguardQuickAffordanceConfig(
-                context = context,
-                component = component,
-            )
-    }
-
-    @Test
-    fun `state - when cannot show while locked - returns Hidden`() = runBlockingTest {
-        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
-        whenever(component.isEnabled()).thenReturn(true)
-        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
-        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
-        val controlsController = mock<ControlsController>()
-        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
-        whenever(component.getControlsListingController()).thenReturn(Optional.empty())
-        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)
-
-        assertThat(values.last())
-            .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
-        job.cancel()
-    }
-
-    @Test
-    fun `state - when listing controller is missing - returns Hidden`() = runBlockingTest {
-        whenever(component.isEnabled()).thenReturn(true)
-        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
-        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
-        val controlsController = mock<ControlsController>()
-        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
-        whenever(component.getControlsListingController()).thenReturn(Optional.empty())
-        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)
-
-        assertThat(values.last())
-            .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
-        job.cancel()
-    }
-
-    @Test
-    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest {
-        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
-
-        val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
-
-        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
-        assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue()
-    }
-
-    @Test
-    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest {
-        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
-
-        val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
-
-        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
-        assertThat((onClickedResult as OnClickedResult.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/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
deleted file mode 100644
index 329c4db..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ /dev/null
@@ -1,148 +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 androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
-
-    @Mock private lateinit var controller: QRCodeScannerController
-
-    private lateinit var underTest: QrCodeScannerKeyguardQuickAffordanceConfig
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        whenever(controller.intent).thenReturn(INTENT_1)
-
-        underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller)
-    }
-
-    @Test
-    fun `affordance - sets up registration and delivers initial model`() = runBlockingTest {
-        whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
-        verify(controller).addCallback(callbackCaptor.capture())
-        verify(controller)
-            .registerQRCodeScannerChangeObservers(
-                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
-                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
-            )
-        assertVisibleState(latest)
-
-        job.cancel()
-        verify(controller).removeCallback(callbackCaptor.value)
-    }
-
-    @Test
-    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)
-            val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
-            verify(controller).addCallback(callbackCaptor.capture())
-
-            whenever(controller.intent).thenReturn(INTENT_2)
-            callbackCaptor.value.onQRCodeScannerActivityChanged()
-
-            assertVisibleState(latest)
-
-            job.cancel()
-            verify(controller).removeCallback(callbackCaptor.value)
-        }
-
-    @Test
-    fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest {
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
-        verify(controller).addCallback(callbackCaptor.capture())
-
-        whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
-        callbackCaptor.value.onQRCodeScannerPreferenceChanged()
-
-        assertVisibleState(latest)
-
-        job.cancel()
-        verify(controller).removeCallback(callbackCaptor.value)
-    }
-
-    @Test
-    fun `affordance - scanner preference changed - delivers none`() = runBlockingTest {
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-        val job = underTest.state.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)
-
-        job.cancel()
-        verify(controller).removeCallback(callbackCaptor.value)
-    }
-
-    @Test
-    fun onQuickAffordanceClicked() {
-        assertThat(underTest.onQuickAffordanceClicked(mock()))
-            .isEqualTo(
-                OnClickedResult.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
-        assertThat(visibleState.icon).isNotNull()
-        assertThat(visibleState.icon.contentDescription).isNotNull()
-    }
-
-    companion object {
-        private val INTENT_1 = Intent("intent1")
-        private val INTENT_2 = Intent("intent2")
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
deleted file mode 100644
index 98dc4c4..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ /dev/null
@@ -1,193 +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.GetWalletCardsResponse
-import android.service.quickaccesswallet.QuickAccessWalletClient
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.ActivityLaunchAnimator
-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.plugins.ActivityStarter
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.wallet.controller.QuickAccessWalletController
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(JUnit4::class)
-class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
-
-    @Mock private lateinit var walletController: QuickAccessWalletController
-    @Mock private lateinit var activityStarter: ActivityStarter
-
-    private lateinit var underTest: QuickAccessWalletKeyguardQuickAffordanceConfig
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            QuickAccessWalletKeyguardQuickAffordanceConfig(
-                walletController,
-                activityStarter,
-            )
-    }
-
-    @Test
-    fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest {
-        setUpState()
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible
-        assertThat(visibleModel.icon)
-            .isEqualTo(
-                Icon.Loaded(
-                    drawable = ICON,
-                    contentDescription =
-                        ContentDescription.Resource(
-                            res = R.string.accessibility_wallet_button,
-                        ),
-                )
-            )
-        job.cancel()
-    }
-
-    @Test
-    fun `affordance - wallet not enabled - model is none`() = runBlockingTest {
-        setUpState(isWalletEnabled = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
-
-        job.cancel()
-    }
-
-    @Test
-    fun `affordance - query not successful - model is none`() = runBlockingTest {
-        setUpState(isWalletQuerySuccessful = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
-
-        job.cancel()
-    }
-
-    @Test
-    fun `affordance - missing icon - model is none`() = runBlockingTest {
-        setUpState(hasWalletIcon = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
-
-        job.cancel()
-    }
-
-    @Test
-    fun `affordance - no selected card - model is none`() = runBlockingTest {
-        setUpState(hasWalletIcon = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
-
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
-
-        job.cancel()
-    }
-
-    @Test
-    fun onQuickAffordanceClicked() {
-        val animationController: ActivityLaunchAnimator.Controller = mock()
-        val expandable: Expandable = mock {
-            whenever(this.activityLaunchController()).thenReturn(animationController)
-        }
-
-        assertThat(underTest.onQuickAffordanceClicked(expandable))
-            .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled)
-        verify(walletController)
-            .startQuickAccessUiIntent(
-                activityStarter,
-                animationController,
-                /* hasCard= */ true,
-            )
-    }
-
-    private fun setUpState(
-        isWalletEnabled: Boolean = true,
-        isWalletQuerySuccessful: Boolean = true,
-        hasWalletIcon: Boolean = true,
-        hasSelectedCard: Boolean = true,
-    ) {
-        whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled)
-
-        val walletClient: QuickAccessWalletClient = mock()
-        val icon: Drawable? =
-            if (hasWalletIcon) {
-                ICON
-            } else {
-                null
-            }
-        whenever(walletClient.tileIcon).thenReturn(icon)
-        whenever(walletController.walletClient).thenReturn(walletClient)
-
-        whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
-            with(
-                invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback
-            ) {
-                if (isWalletQuerySuccessful) {
-                    onWalletCardsRetrieved(
-                        if (hasSelectedCard) {
-                            GetWalletCardsResponse(listOf(mock()), 0)
-                        } else {
-                            GetWalletCardsResponse(emptyList(), 0)
-                        }
-                    )
-                } else {
-                    onWalletCardRetrievalError(mock())
-                }
-            }
-        }
-    }
-
-    companion object {
-        private val ICON: Drawable = mock()
-    }
-}
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..ecc63ec 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,24 +23,34 @@
 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.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+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.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 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.UserFileManager
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.max
 import kotlin.math.min
-import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runBlockingTest
@@ -50,6 +60,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.verifyZeroInteractions
@@ -81,9 +92,14 @@
         whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
             .thenReturn(RETURNED_BURN_IN_OFFSET)
 
-        homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
-        quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
-        qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+        homeControlsQuickAffordanceConfig =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+        quickAccessWalletAffordanceConfig =
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
+        qrCodeScannerAffordanceConfig =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
         registry =
             FakeKeyguardQuickAffordanceRegistry(
                 mapOf(
@@ -104,6 +120,42 @@
         whenever(userTracker.userHandle).thenReturn(mock())
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
+        val scope = CoroutineScope(IMMEDIATE)
+        val selectionManager =
+            KeyguardQuickAffordanceSelectionManager(
+                context = context,
+                userFileManager =
+                    mock<UserFileManager>().apply {
+                        whenever(
+                                getSharedPreferences(
+                                    anyString(),
+                                    anyInt(),
+                                    anyInt(),
+                                )
+                            )
+                            .thenReturn(FakeSharedPreferences())
+                    },
+                userTracker = userTracker,
+            )
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                appContext = context,
+                scope = scope,
+                selectionManager = selectionManager,
+                legacySettingSyncer =
+                    KeyguardQuickAffordanceLegacySettingSyncer(
+                        scope = scope,
+                        backgroundDispatcher = IMMEDIATE,
+                        secureSettings = FakeSettings(),
+                        selectionsManager = selectionManager,
+                    ),
+                configs =
+                    setOf(
+                        homeControlsQuickAffordanceConfig,
+                        quickAccessWalletAffordanceConfig,
+                        qrCodeScannerAffordanceConfig,
+                    ),
+            )
         underTest =
             KeyguardBottomAreaViewModel(
                 keyguardInteractor = keyguardInteractor,
@@ -115,6 +167,11 @@
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
                         activityStarter = activityStarter,
+                        featureFlags =
+                            FakeFeatureFlags().apply {
+                                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                            },
+                        repository = { quickAffordanceRepository },
                     ),
                 bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
                 burnInHelperWrapper = burnInHelperWrapper,
@@ -489,42 +546,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)
@@ -564,5 +621,6 @@
     companion object {
         private const val DEFAULT_BURN_IN_OFFSET = 5
         private const val RETURNED_BURN_IN_OFFSET = 3
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
deleted file mode 100644
index 56aff3c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.android.systemui.log
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import java.io.PrintWriter
-import java.io.StringWriter
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnitRunner
-
-@SmallTest
-@RunWith(MockitoJUnitRunner::class)
-class LogBufferTest : SysuiTestCase() {
-    private lateinit var buffer: LogBuffer
-
-    private lateinit var outputWriter: StringWriter
-
-    @Mock
-    private lateinit var logcatEchoTracker: LogcatEchoTracker
-
-    @Before
-    fun setup() {
-        outputWriter = StringWriter()
-        buffer = createBuffer()
-    }
-
-    private fun createBuffer(): LogBuffer {
-        return LogBuffer("TestBuffer", 1, logcatEchoTracker, false)
-    }
-
-    @Test
-    fun log_shouldSaveLogToBuffer() {
-        buffer.log("Test", LogLevel.INFO, "Some test message")
-
-        val dumpedString = dumpBuffer()
-
-        assertThat(dumpedString).contains("Some test message")
-    }
-
-    @Test
-    fun log_shouldRotateIfLogBufferIsFull() {
-        buffer.log("Test", LogLevel.INFO, "This should be rotated")
-        buffer.log("Test", LogLevel.INFO, "New test message")
-
-        val dumpedString = dumpBuffer()
-
-        assertThat(dumpedString).contains("New test message")
-    }
-
-    @Test
-    fun dump_writesExceptionAndStacktrace() {
-        buffer = createBuffer()
-        val exception = createTestException("Exception message", "TestClass")
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
-
-        val dumpedString = dumpBuffer()
-
-        assertThat(dumpedString).contains("Extra message")
-        assertThat(dumpedString).contains("java.lang.RuntimeException: Exception message")
-        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
-        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
-    }
-
-    @Test
-    fun dump_writesCauseAndStacktrace() {
-        buffer = createBuffer()
-        val exception = createTestException("Exception message",
-                "TestClass",
-                cause = createTestException("The real cause!", "TestClass"))
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
-
-        val dumpedString = dumpBuffer()
-
-        assertThat(dumpedString)
-                .contains("Caused by: java.lang.RuntimeException: The real cause!")
-        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
-        assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
-    }
-
-    @Test
-    fun dump_writesSuppressedExceptionAndStacktrace() {
-        buffer = createBuffer()
-        val exception = RuntimeException("Root exception message")
-        exception.addSuppressed(
-                createTestException(
-                        "First suppressed exception",
-                        "FirstClass",
-                        createTestException("Cause of suppressed exp", "ThirdClass")
-                ))
-        exception.addSuppressed(
-                createTestException("Second suppressed exception", "SecondClass"))
-        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
-
-        val dumpedStr = dumpBuffer()
-
-        // first suppressed exception
-        assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: First suppressed exception")
-        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
-        assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
-
-        assertThat(dumpedStr)
-                .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
-        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
-        assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
-
-        // second suppressed exception
-        assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: Second suppressed exception")
-        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
-        assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
-    }
-
-    private fun createTestException(
-            message: String,
-            errorClass: String,
-            cause: Throwable? = null,
-    ): Exception {
-        val exception = RuntimeException(message, cause)
-        exception.stackTrace = (1..5).map { lineNumber ->
-            StackTraceElement(errorClass,
-                    "TestMethod",
-                    "$errorClass.java",
-                    lineNumber)
-        }.toTypedArray()
-        return exception
-    }
-
-    private fun dumpBuffer(): String {
-        buffer.dump(PrintWriter(outputWriter), tailLength = 100)
-        return outputWriter.toString()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
deleted file mode 100644
index e4cab18..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
+++ /dev/null
@@ -1,144 +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.media
-
-import org.mockito.Mockito.`when` as whenever
-import android.graphics.drawable.Animatable2
-import android.graphics.drawable.Drawable
-import android.test.suitebuilder.annotation.SmallTest
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import com.android.systemui.SysuiTestCase
-import junit.framework.Assert.assertTrue
-import junit.framework.Assert.assertFalse
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.times
-import org.mockito.Mockito.never
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class AnimationBindHandlerTest : SysuiTestCase() {
-
-    private interface Callback : () -> Unit
-    private abstract class AnimatedDrawable : Drawable(), Animatable2
-    private lateinit var handler: AnimationBindHandler
-
-    @Mock private lateinit var animatable: AnimatedDrawable
-    @Mock private lateinit var animatable2: AnimatedDrawable
-    @Mock private lateinit var callback: Callback
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        handler = AnimationBindHandler()
-    }
-
-    @After
-    fun tearDown() {}
-
-    @Test
-    fun registerNoAnimations_executeCallbackImmediately() {
-        handler.tryExecute(callback)
-        verify(callback).invoke()
-    }
-
-    @Test
-    fun registerStoppedAnimations_executeCallbackImmediately() {
-        whenever(animatable.isRunning).thenReturn(false)
-        whenever(animatable2.isRunning).thenReturn(false)
-
-        handler.tryExecute(callback)
-        verify(callback).invoke()
-    }
-
-    @Test
-    fun registerRunningAnimations_executeCallbackDelayed() {
-        whenever(animatable.isRunning).thenReturn(true)
-        whenever(animatable2.isRunning).thenReturn(true)
-
-        handler.tryRegister(animatable)
-        handler.tryRegister(animatable2)
-        handler.tryExecute(callback)
-
-        verify(callback, never()).invoke()
-
-        whenever(animatable.isRunning).thenReturn(false)
-        handler.onAnimationEnd(animatable)
-        verify(callback, never()).invoke()
-
-        whenever(animatable2.isRunning).thenReturn(false)
-        handler.onAnimationEnd(animatable2)
-        verify(callback, times(1)).invoke()
-    }
-
-    @Test
-    fun repeatedEndCallback_executeSingleCallback() {
-        whenever(animatable.isRunning).thenReturn(true)
-
-        handler.tryRegister(animatable)
-        handler.tryExecute(callback)
-
-        verify(callback, never()).invoke()
-
-        whenever(animatable.isRunning).thenReturn(false)
-        handler.onAnimationEnd(animatable)
-        handler.onAnimationEnd(animatable)
-        handler.onAnimationEnd(animatable)
-        verify(callback, times(1)).invoke()
-    }
-
-    @Test
-    fun registerUnregister_executeImmediately() {
-        whenever(animatable.isRunning).thenReturn(true)
-
-        handler.tryRegister(animatable)
-        handler.unregisterAll()
-        handler.tryExecute(callback)
-
-        verify(callback).invoke()
-    }
-
-    @Test
-    fun updateRebindId_returnsAsExpected() {
-        // Previous or current call is null, returns true
-        assertTrue(handler.updateRebindId(null))
-        assertTrue(handler.updateRebindId(null))
-        assertTrue(handler.updateRebindId(null))
-        assertTrue(handler.updateRebindId(10))
-        assertTrue(handler.updateRebindId(null))
-        assertTrue(handler.updateRebindId(20))
-
-        // Different integer from prevoius, returns true
-        assertTrue(handler.updateRebindId(10))
-        assertTrue(handler.updateRebindId(20))
-
-        // Matches previous call, returns false
-        assertFalse(handler.updateRebindId(20))
-        assertFalse(handler.updateRebindId(20))
-        assertTrue(handler.updateRebindId(10))
-        assertFalse(handler.updateRebindId(10))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
deleted file mode 100644
index f56d42e..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
+++ /dev/null
@@ -1,154 +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.media
-
-import android.animation.ValueAnimator
-import android.graphics.Color
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.monet.ColorScheme
-import junit.framework.Assert.assertEquals
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-private const val DEFAULT_COLOR = Color.RED
-private const val TARGET_COLOR = Color.BLUE
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class ColorSchemeTransitionTest : SysuiTestCase() {
-
-    private interface ExtractCB : (ColorScheme) -> Int
-    private interface ApplyCB : (Int) -> Unit
-    private lateinit var colorTransition: AnimatingColorTransition
-    private lateinit var colorSchemeTransition: ColorSchemeTransition
-
-    @Mock private lateinit var mockAnimatingTransition: AnimatingColorTransition
-    @Mock private lateinit var valueAnimator: ValueAnimator
-    @Mock private lateinit var colorScheme: ColorScheme
-    @Mock private lateinit var extractColor: ExtractCB
-    @Mock private lateinit var applyColor: ApplyCB
-
-    private lateinit var animatingColorTransitionFactory: AnimatingColorTransitionFactory
-    @Mock private lateinit var mediaViewHolder: MediaViewHolder
-    @Mock private lateinit var gutsViewHolder: GutsViewHolder
-
-    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        whenever(mediaViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
-        animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition }
-        whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
-
-        colorSchemeTransition = ColorSchemeTransition(
-            context, mediaViewHolder, animatingColorTransitionFactory
-        )
-
-        colorTransition = object : AnimatingColorTransition(
-            DEFAULT_COLOR, extractColor, applyColor
-        ) {
-            override fun buildAnimator(): ValueAnimator {
-                return valueAnimator
-            }
-        }
-    }
-
-    @After
-    fun tearDown() {}
-
-    @Test
-    fun testColorTransition_nullColorScheme_keepsDefault() {
-        colorTransition.updateColorScheme(null)
-        verify(applyColor, times(1)).invoke(DEFAULT_COLOR)
-        verify(valueAnimator, never()).start()
-        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
-        assertEquals(DEFAULT_COLOR, colorTransition.targetColor)
-    }
-
-    @Test
-    fun testColorTransition_newColor_startsAnimation() {
-        colorTransition.updateColorScheme(colorScheme)
-        verify(applyColor, times(1)).invoke(DEFAULT_COLOR)
-        verify(valueAnimator, times(1)).start()
-        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
-        assertEquals(TARGET_COLOR, colorTransition.targetColor)
-    }
-
-    @Test
-    fun testColorTransition_sameColor_noAnimation() {
-        whenever(extractColor.invoke(colorScheme)).thenReturn(DEFAULT_COLOR)
-        colorTransition.updateColorScheme(colorScheme)
-        verify(valueAnimator, never()).start()
-        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
-        assertEquals(DEFAULT_COLOR, colorTransition.targetColor)
-    }
-
-    @Test
-    fun testColorTransition_colorAnimation_startValues() {
-        val expectedColor = DEFAULT_COLOR
-        whenever(valueAnimator.animatedFraction).thenReturn(0f)
-        colorTransition.updateColorScheme(colorScheme)
-        colorTransition.onAnimationUpdate(valueAnimator)
-
-        assertEquals(expectedColor, colorTransition.currentColor)
-        assertEquals(expectedColor, colorTransition.sourceColor)
-        verify(applyColor, times(2)).invoke(expectedColor) // applied once in constructor
-    }
-
-    @Test
-    fun testColorTransition_colorAnimation_endValues() {
-        val expectedColor = TARGET_COLOR
-        whenever(valueAnimator.animatedFraction).thenReturn(1f)
-        colorTransition.updateColorScheme(colorScheme)
-        colorTransition.onAnimationUpdate(valueAnimator)
-
-        assertEquals(expectedColor, colorTransition.currentColor)
-        assertEquals(expectedColor, colorTransition.targetColor)
-        verify(applyColor).invoke(expectedColor)
-    }
-
-    @Test
-    fun testColorTransition_colorAnimation_interpolatedMidpoint() {
-        val expectedColor = Color.rgb(186, 0, 186)
-        whenever(valueAnimator.animatedFraction).thenReturn(0.5f)
-        colorTransition.updateColorScheme(colorScheme)
-        colorTransition.onAnimationUpdate(valueAnimator)
-
-        assertEquals(expectedColor, colorTransition.currentColor)
-        verify(applyColor).invoke(expectedColor)
-    }
-
-    @Test
-    fun testColorSchemeTransition_update() {
-        colorSchemeTransition.updateColorScheme(colorScheme)
-        verify(mockAnimatingTransition, times(8)).updateColorScheme(colorScheme)
-        verify(gutsViewHolder).colorScheme = colorScheme
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
deleted file mode 100644
index c41fac7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.provider.Settings
-import android.test.suitebuilder.annotation.SmallTest
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.widget.FrameLayout
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.MediaContainerView
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.settings.FakeSettings
-import com.android.systemui.utils.os.FakeHandler
-import com.google.common.truth.Truth.assertThat
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class KeyguardMediaControllerTest : SysuiTestCase() {
-
-    @Mock
-    private lateinit var mediaHost: MediaHost
-    @Mock
-    private lateinit var bypassController: KeyguardBypassController
-    @Mock
-    private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock
-    private lateinit var configurationController: ConfigurationController
-
-    @JvmField @Rule
-    val mockito = MockitoJUnit.rule()
-
-    private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
-    private val hostView = UniqueObjectHostView(context)
-    private val settings = FakeSettings()
-    private lateinit var keyguardMediaController: KeyguardMediaController
-    private lateinit var testableLooper: TestableLooper
-    private lateinit var fakeHandler: FakeHandler
-
-    @Before
-    fun setup() {
-        // default state is positive, media should show up
-        whenever(mediaHost.visible).thenReturn(true)
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        whenever(mediaHost.hostView).thenReturn(hostView)
-        hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
-        testableLooper = TestableLooper.get(this)
-        fakeHandler = FakeHandler(testableLooper.looper)
-        keyguardMediaController = KeyguardMediaController(
-            mediaHost,
-            bypassController,
-            statusBarStateController,
-            context,
-            settings,
-            fakeHandler,
-            configurationController,
-        )
-        keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
-        keyguardMediaController.useSplitShade = false
-    }
-
-    @Test
-    fun testHiddenWhenHostIsHidden() {
-        whenever(mediaHost.visible).thenReturn(false)
-
-        keyguardMediaController.refreshMediaPosition()
-
-        assertThat(mediaContainerView.visibility).isEqualTo(GONE)
-    }
-
-    @Test
-    fun testVisibleOnKeyguardOrFullScreenUserSwitcher() {
-        testStateVisibility(StatusBarState.SHADE, GONE)
-        testStateVisibility(StatusBarState.SHADE_LOCKED, GONE)
-        testStateVisibility(StatusBarState.KEYGUARD, VISIBLE)
-    }
-
-    private fun testStateVisibility(state: Int, visibility: Int) {
-        whenever(statusBarStateController.state).thenReturn(state)
-        keyguardMediaController.refreshMediaPosition()
-        assertThat(mediaContainerView.visibility).isEqualTo(visibility)
-    }
-
-    @Test
-    fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() {
-        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0)
-
-        keyguardMediaController.refreshMediaPosition()
-
-        assertThat(mediaContainerView.visibility).isEqualTo(GONE)
-    }
-
-    @Test
-    fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() {
-        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
-
-        keyguardMediaController.refreshMediaPosition()
-
-        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
-    }
-
-    @Test
-    fun testActivatesSplitShadeContainerInSplitShadeMode() {
-        val splitShadeContainer = FrameLayout(context)
-        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
-        keyguardMediaController.useSplitShade = true
-
-        assertThat(splitShadeContainer.visibility).isEqualTo(VISIBLE)
-    }
-
-    @Test
-    fun testActivatesSinglePaneContainerInSinglePaneMode() {
-        val splitShadeContainer = FrameLayout(context)
-        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
-
-        assertThat(splitShadeContainer.visibility).isEqualTo(GONE)
-        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
-    }
-
-    @Test
-    fun testAttachedToSplitShade() {
-        val splitShadeContainer = FrameLayout(context)
-        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
-        keyguardMediaController.useSplitShade = true
-
-        assertTrue("HostView wasn't attached to the split pane container",
-            splitShadeContainer.childCount == 1)
-    }
-
-    @Test
-    fun testAttachedToSinglePane() {
-        val splitShadeContainer = FrameLayout(context)
-        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
-
-        assertTrue("HostView wasn't attached to the single pane container",
-            mediaContainerView.childCount == 1)
-    }
-
-    @Test
-    fun testMediaHost_expandedPlayer() {
-        verify(mediaHost).expansion = MediaHostState.EXPANDED
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
deleted file mode 100644
index 7e0be6d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ /dev/null
@@ -1,476 +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.
- */
-
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
-import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import javax.inject.Provider
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-private val DATA = MediaTestUtils.emptyMediaData
-
-private val SMARTSPACE_KEY = "smartspace"
-
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
-
-    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
-    @Mock lateinit var panel: MediaControlPanel
-    @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
-    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
-    @Mock lateinit var mediaHostState: MediaHostState
-    @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
-    @Mock lateinit var mediaDataManager: MediaDataManager
-    @Mock lateinit var configurationController: ConfigurationController
-    @Mock lateinit var falsingCollector: FalsingCollector
-    @Mock lateinit var falsingManager: FalsingManager
-    @Mock lateinit var dumpManager: DumpManager
-    @Mock lateinit var logger: MediaUiEventLogger
-    @Mock lateinit var debugLogger: MediaCarouselControllerLogger
-    @Mock lateinit var mediaViewController: MediaViewController
-    @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
-    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
-    @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
-
-    private val clock = FakeSystemClock()
-    private lateinit var mediaCarouselController: MediaCarouselController
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mediaCarouselController = MediaCarouselController(
-            context,
-            mediaControlPanelFactory,
-            visualStabilityProvider,
-            mediaHostStatesManager,
-            activityStarter,
-            clock,
-            executor,
-            mediaDataManager,
-            configurationController,
-            falsingCollector,
-            falsingManager,
-            dumpManager,
-            logger,
-            debugLogger
-        )
-        verify(mediaDataManager).addListener(capture(listener))
-        verify(visualStabilityProvider)
-            .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
-        whenever(mediaControlPanelFactory.get()).thenReturn(panel)
-        whenever(panel.mediaViewController).thenReturn(mediaViewController)
-        whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
-        MediaPlayerData.clear()
-    }
-
-    @Test
-    fun testPlayerOrdering() {
-        // Test values: key, data, last active time
-        val playingLocal = Triple("playing local",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
-            4500L)
-
-        val playingCast = Triple("playing cast",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
-            5000L)
-
-        val pausedLocal = Triple("paused local",
-            DATA.copy(active = true, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
-            1000L)
-
-        val pausedCast = Triple("paused cast",
-            DATA.copy(active = true, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
-            2000L)
-
-        val playingRcn = Triple("playing RCN",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
-            5000L)
-
-        val pausedRcn = Triple("paused RCN",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
-                5000L)
-
-        val active = Triple("active",
-            DATA.copy(active = true, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            250L)
-
-        val resume1 = Triple("resume 1",
-            DATA.copy(active = false, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            500L)
-
-        val resume2 = Triple("resume 2",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            1000L)
-
-        val activeMoreRecent = Triple("active more recent",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L),
-            1000L)
-
-        val activeLessRecent = Triple("active less recent",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L),
-            1000L)
-        // Expected ordering for media players:
-        // Actively playing local sessions
-        // Actively playing cast sessions
-        // Paused local and cast sessions, by last active
-        // RCNs
-        // Resume controls, by last active
-
-        val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
-                pausedRcn, active, resume2, resume1)
-
-        expected.forEach {
-            clock.setCurrentTimeMillis(it.third)
-            MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
-                panel, clock, isSsReactivated = false)
-        }
-
-        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
-            assertEquals(expected.get(index).first, key.data.notificationKey)
-        }
-
-        for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) {
-            assertEquals(expected.get(index).first, key.data.notificationKey)
-        }
-    }
-
-    @Test
-    fun testOrderWithSmartspace_prioritized() {
-        testPlayerOrdering()
-
-        // If smartspace is prioritized
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
-            true, clock)
-
-        // Then it should be shown immediately after any actively playing controls
-        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
-    }
-
-    @Test
-    fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
-        testPlayerOrdering()
-
-        // If smartspace is prioritized
-        listener.value.onSmartspaceMediaDataLoaded(
-                SMARTSPACE_KEY,
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-                true
-        )
-
-        // Then it should be shown immediately after any actively playing controls
-        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
-        assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
-    }
-
-    @Test
-    fun testOrderWithSmartspace_notPrioritized() {
-        testPlayerOrdering()
-
-        // If smartspace is not prioritized
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
-            false, clock)
-
-        // Then it should be shown at the end of the carousel's active entries
-        val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
-        assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
-    }
-
-    @Test
-    fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
-        testPlayerOrdering()
-        // playing paused player
-        listener.value.onMediaDataLoaded("paused local",
-                "paused local",
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-        listener.value.onMediaDataLoaded("playing local",
-                "playing local",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
-        )
-
-        assertEquals(
-                MediaPlayerData.getMediaPlayerIndex("paused local"),
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-        // paused player order should stays the same in visibleMediaPLayer map.
-        // paused player order should be first in mediaPlayer map.
-        assertEquals(
-                MediaPlayerData.visiblePlayerKeys().elementAt(3),
-                MediaPlayerData.playerKeys().elementAt(0)
-        )
-    }
-    @Test
-    fun testSwipeDismiss_logged() {
-        mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
-
-        verify(logger).logSwipeDismiss()
-    }
-
-    @Test
-    fun testSettingsButton_logged() {
-        mediaCarouselController.settingsButton.callOnClick()
-
-        verify(logger).logCarouselSettings()
-    }
-
-    @Test
-    fun testLocationChangeQs_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_QS,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
-    }
-
-    @Test
-    fun testLocationChangeQqs_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_QQS,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
-    }
-
-    @Test
-    fun testLocationChangeLockscreen_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_LOCKSCREEN,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
-    }
-
-    @Test
-    fun testLocationChangeDream_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
-    }
-
-    @Test
-    fun testRecommendationRemoved_logged() {
-        val packageName = "smartspace package"
-        val instanceId = InstanceId.fakeInstanceId(123)
-
-        val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-            packageName = packageName,
-            instanceId = instanceId
-        )
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
-        mediaCarouselController.removePlayer(SMARTSPACE_KEY)
-
-        verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
-    }
-
-    @Test
-    fun testMediaLoaded_ScrollToActivePlayer() {
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
-        )
-        listener.value.onMediaDataLoaded("paused local",
-                null,
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-        // adding a media recommendation card.
-        listener.value.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA,
-                false)
-        mediaCarouselController.shouldScrollToKey = true
-        // switching between media players.
-        listener.value.onMediaDataLoaded("playing local",
-        "playing local",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
-        )
-        listener.value.onMediaDataLoaded("paused local",
-                "paused local",
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-
-        assertEquals(
-                MediaPlayerData.getMediaPlayerIndex("paused local"),
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-    }
-
-    @Test
-    fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
-        listener.value.onSmartspaceMediaDataLoaded(
-                SMARTSPACE_KEY,
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
-                false
-        )
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
-        )
-
-        var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
-        assertEquals(
-                playerIndex,
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-        assertEquals(playerIndex, 0)
-
-        // Replaying the same media player one more time.
-        // And check that the card stays in its position.
-        mediaCarouselController.shouldScrollToKey = true
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false,
-                        packageName = "PACKAGE_NAME")
-        )
-        playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
-        assertEquals(playerIndex, 0)
-    }
-
-    @Test
-    fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
-        var result = false
-        mediaCarouselController.updateHostVisibility = { result = true }
-
-        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
-        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
-
-        assertEquals(true, result)
-    }
-
-    @Test
-    fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
-        var result = false
-        mediaCarouselController.updateHostVisibility = { result = true }
-
-        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
-        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
-        assertEquals(false, result)
-
-        visualStabilityCallback.value.onReorderingAllowed()
-        assertEquals(true, result)
-    }
-
-    @Test
-    fun testGetCurrentVisibleMediaContentIntent() {
-        val clickIntent1 = mock(PendingIntent::class.java)
-        val player1 = Triple("player1",
-                DATA.copy(clickIntent = clickIntent1),
-                1000L)
-        clock.setCurrentTimeMillis(player1.third)
-        MediaPlayerData.addMediaPlayer(player1.first,
-                player1.second.copy(notificationKey = player1.first),
-                panel, clock, isSsReactivated = false)
-
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
-
-        val clickIntent2 = mock(PendingIntent::class.java)
-        val player2 = Triple("player2",
-                DATA.copy(clickIntent = clickIntent2),
-                2000L)
-        clock.setCurrentTimeMillis(player2.third)
-        MediaPlayerData.addMediaPlayer(player2.first,
-                player2.second.copy(notificationKey = player2.first),
-                panel, clock, isSsReactivated = false)
-
-        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
-        // added to the front because it was active more recently.
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
-
-        val clickIntent3 = mock(PendingIntent::class.java)
-        val player3 = Triple("player3",
-                DATA.copy(clickIntent = clickIntent3),
-                500L)
-        clock.setCurrentTimeMillis(player3.third)
-        MediaPlayerData.addMediaPlayer(player3.first,
-                player3.second.copy(notificationKey = player3.first),
-                panel, clock, isSsReactivated = false)
-
-        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
-        // added to the end because it was active less recently.
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
-    }
-
-    @Test
-    fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
-        val delta = 0.0001F
-        val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
-        val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        whenever(mediaHostStatesManager.mediaHostStates)
-            .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
-        whenever(mediaHostState.visible).thenReturn(true)
-        mediaCarouselController.currentEndLocation = LOCATION_QS
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
-        mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
-
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
-        mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
deleted file mode 100644
index 7de5719..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ /dev/null
@@ -1,1966 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorSet
-import android.app.PendingIntent
-import android.app.smartspace.SmartspaceAction
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.drawable.Animatable2
-import android.graphics.drawable.AnimatedVectorDrawable
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.Icon
-import android.graphics.drawable.RippleDrawable
-import android.graphics.drawable.TransitionDrawable
-import android.media.MediaMetadata
-import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.os.Bundle
-import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import android.view.ViewGroup
-import android.view.animation.Interpolator
-import android.widget.FrameLayout
-import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.SeekBar
-import android.widget.TextView
-import androidx.constraintlayout.widget.Barrier
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.lifecycle.LiveData
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.ActivityIntentHelper
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.bluetooth.BroadcastDialogController
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.animation.TransitionLayout
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.KotlinArgumentCaptor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import dagger.Lazy
-import junit.framework.Assert.assertTrue
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.Mock
-import org.mockito.Mockito.anyString
-import org.mockito.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.junit.MockitoJUnit
-
-private const val KEY = "TEST_KEY"
-private const val PACKAGE = "PKG"
-private const val ARTIST = "ARTIST"
-private const val TITLE = "TITLE"
-private const val DEVICE_NAME = "DEVICE_NAME"
-private const val SESSION_KEY = "SESSION_KEY"
-private const val SESSION_ARTIST = "SESSION_ARTIST"
-private const val SESSION_TITLE = "SESSION_TITLE"
-private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
-private const val REC_APP_NAME = "REC APP NAME"
-private const val APP_NAME = "APP_NAME"
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class MediaControlPanelTest : SysuiTestCase() {
-
-    private lateinit var player: MediaControlPanel
-
-    private lateinit var bgExecutor: FakeExecutor
-    private lateinit var mainExecutor: FakeExecutor
-    @Mock private lateinit var activityStarter: ActivityStarter
-    @Mock private lateinit var broadcastSender: BroadcastSender
-
-    @Mock private lateinit var gutsViewHolder: GutsViewHolder
-    @Mock private lateinit var viewHolder: MediaViewHolder
-    @Mock private lateinit var view: TransitionLayout
-    @Mock private lateinit var seekBarViewModel: SeekBarViewModel
-    @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
-    @Mock private lateinit var mediaViewController: MediaViewController
-    @Mock private lateinit var mediaDataManager: MediaDataManager
-    @Mock private lateinit var expandedSet: ConstraintSet
-    @Mock private lateinit var collapsedSet: ConstraintSet
-    @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
-    @Mock private lateinit var mediaCarouselController: MediaCarouselController
-    @Mock private lateinit var falsingManager: FalsingManager
-    @Mock private lateinit var transitionParent: ViewGroup
-    @Mock private lateinit var broadcastDialogController: BroadcastDialogController
-    private lateinit var appIcon: ImageView
-    @Mock private lateinit var albumView: ImageView
-    private lateinit var titleText: TextView
-    private lateinit var artistText: TextView
-    private lateinit var seamless: ViewGroup
-    private lateinit var seamlessButton: View
-    @Mock private lateinit var seamlessBackground: RippleDrawable
-    private lateinit var seamlessIcon: ImageView
-    private lateinit var seamlessText: TextView
-    private lateinit var seekBar: SeekBar
-    private lateinit var action0: ImageButton
-    private lateinit var action1: ImageButton
-    private lateinit var action2: ImageButton
-    private lateinit var action3: ImageButton
-    private lateinit var action4: ImageButton
-    private lateinit var actionPlayPause: ImageButton
-    private lateinit var actionNext: ImageButton
-    private lateinit var actionPrev: ImageButton
-    private lateinit var scrubbingElapsedTimeView: TextView
-    private lateinit var scrubbingTotalTimeView: TextView
-    private lateinit var actionsTopBarrier: Barrier
-    @Mock private lateinit var gutsText: TextView
-    @Mock private lateinit var mockAnimator: AnimatorSet
-    private lateinit var settings: ImageButton
-    private lateinit var cancel: View
-    private lateinit var cancelText: TextView
-    private lateinit var dismiss: FrameLayout
-    private lateinit var dismissText: TextView
-
-    private lateinit var session: MediaSession
-    private lateinit var device: MediaDeviceData
-    private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null,
-            showBroadcastButton = false)
-    private lateinit var mediaData: MediaData
-    private val clock = FakeSystemClock()
-    @Mock private lateinit var logger: MediaUiEventLogger
-    @Mock private lateinit var instanceId: InstanceId
-    @Mock private lateinit var packageManager: PackageManager
-    @Mock private lateinit var applicationInfo: ApplicationInfo
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
-    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
-
-    @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
-    @Mock private lateinit var smartspaceAction: SmartspaceAction
-    private lateinit var smartspaceData: SmartspaceMediaData
-    @Mock private lateinit var coverContainer1: ViewGroup
-    @Mock private lateinit var coverContainer2: ViewGroup
-    @Mock private lateinit var coverContainer3: ViewGroup
-    private lateinit var coverItem1: ImageView
-    private lateinit var coverItem2: ImageView
-    private lateinit var coverItem3: ImageView
-    private lateinit var recTitle1: TextView
-    private lateinit var recTitle2: TextView
-    private lateinit var recTitle3: TextView
-    private lateinit var recSubtitle1: TextView
-    private lateinit var recSubtitle2: TextView
-    private lateinit var recSubtitle3: TextView
-    private var shouldShowBroadcastButton: Boolean = false
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        bgExecutor = FakeExecutor(FakeSystemClock())
-        mainExecutor = FakeExecutor(FakeSystemClock())
-        whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
-        whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
-
-        // Set up package manager mocks
-        val icon = context.getDrawable(R.drawable.ic_android)
-        whenever(packageManager.getApplicationIcon(anyString())).thenReturn(icon)
-        whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
-            .thenReturn(icon)
-        whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
-            .thenReturn(applicationInfo)
-        whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
-        context.setMockPackageManager(packageManager)
-
-        player = object : MediaControlPanel(
-            context,
-            bgExecutor,
-            mainExecutor,
-            activityStarter,
-            broadcastSender,
-            mediaViewController,
-            seekBarViewModel,
-            Lazy { mediaDataManager },
-            mediaOutputDialogFactory,
-            mediaCarouselController,
-            falsingManager,
-            clock,
-            logger,
-            keyguardStateController,
-            activityIntentHelper,
-            lockscreenUserManager,
-            broadcastDialogController) {
-                override fun loadAnimator(
-                    animId: Int,
-                    otionInterpolator: Interpolator,
-                    vararg targets: View
-                ): AnimatorSet {
-                    return mockAnimator
-                }
-            }
-
-        initGutsViewHolderMocks()
-        initMediaViewHolderMocks()
-
-        initDeviceMediaData(false, DEVICE_NAME)
-
-        // Set up recommendation view
-        initRecommendationViewHolderMocks()
-
-        // Set valid recommendation data
-        val extras = Bundle()
-        extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
-        val intent = Intent().apply {
-            putExtras(extras)
-            setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        }
-        whenever(smartspaceAction.intent).thenReturn(intent)
-        whenever(smartspaceAction.extras).thenReturn(extras)
-        smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-            packageName = PACKAGE,
-            instanceId = instanceId,
-            recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
-            cardAction = smartspaceAction
-        )
-    }
-
-    private fun initGutsViewHolderMocks() {
-        settings = ImageButton(context)
-        cancel = View(context)
-        cancelText = TextView(context)
-        dismiss = FrameLayout(context)
-        dismissText = TextView(context)
-        whenever(gutsViewHolder.gutsText).thenReturn(gutsText)
-        whenever(gutsViewHolder.settings).thenReturn(settings)
-        whenever(gutsViewHolder.cancel).thenReturn(cancel)
-        whenever(gutsViewHolder.cancelText).thenReturn(cancelText)
-        whenever(gutsViewHolder.dismiss).thenReturn(dismiss)
-        whenever(gutsViewHolder.dismissText).thenReturn(dismissText)
-    }
-
-    private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
-        device = MediaDeviceData(true, null, name, null,
-                showBroadcastButton = shouldShowBroadcastButton)
-
-        // Create media session
-        val metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
-        val playbackBuilder = PlaybackState.Builder().apply {
-            setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
-            setActions(PlaybackState.ACTION_PLAY)
-        }
-        session = MediaSession(context, SESSION_KEY).apply {
-            setMetadata(metadataBuilder.build())
-            setPlaybackState(playbackBuilder.build())
-        }
-        session.setActive(true)
-
-        mediaData = MediaTestUtils.emptyMediaData.copy(
-                artist = ARTIST,
-                song = TITLE,
-                packageName = PACKAGE,
-                token = session.sessionToken,
-                device = device,
-                instanceId = instanceId)
-    }
-
-    /**
-     * Initialize elements in media view holder
-     */
-    private fun initMediaViewHolderMocks() {
-        whenever(seekBarViewModel.progress).thenReturn(seekBarData)
-
-        // Set up mock views for the players
-        appIcon = ImageView(context)
-        titleText = TextView(context)
-        artistText = TextView(context)
-        seamless = FrameLayout(context)
-        seamless.foreground = seamlessBackground
-        seamlessButton = View(context)
-        seamlessIcon = ImageView(context)
-        seamlessText = TextView(context)
-        seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar }
-
-        action0 = ImageButton(context).also { it.setId(R.id.action0) }
-        action1 = ImageButton(context).also { it.setId(R.id.action1) }
-        action2 = ImageButton(context).also { it.setId(R.id.action2) }
-        action3 = ImageButton(context).also { it.setId(R.id.action3) }
-        action4 = ImageButton(context).also { it.setId(R.id.action4) }
-
-        actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) }
-        actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) }
-        actionNext = ImageButton(context).also { it.setId(R.id.actionNext) }
-        scrubbingElapsedTimeView =
-            TextView(context).also { it.setId(R.id.media_scrubbing_elapsed_time) }
-        scrubbingTotalTimeView =
-            TextView(context).also { it.setId(R.id.media_scrubbing_total_time) }
-
-        actionsTopBarrier =
-            Barrier(context).also {
-                it.id = R.id.media_action_barrier_top
-                it.referencedIds =
-                    intArrayOf(
-                        actionPrev.id,
-                        seekBar.id,
-                        actionNext.id,
-                        action0.id,
-                        action1.id,
-                        action2.id,
-                        action3.id,
-                        action4.id)
-            }
-
-        whenever(viewHolder.player).thenReturn(view)
-        whenever(viewHolder.appIcon).thenReturn(appIcon)
-        whenever(viewHolder.albumView).thenReturn(albumView)
-        whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
-        whenever(viewHolder.titleText).thenReturn(titleText)
-        whenever(viewHolder.artistText).thenReturn(artistText)
-        whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
-        whenever(viewHolder.seamless).thenReturn(seamless)
-        whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
-        whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
-        whenever(viewHolder.seamlessText).thenReturn(seamlessText)
-        whenever(viewHolder.seekBar).thenReturn(seekBar)
-        whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
-        whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
-
-        whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
-
-        // Transition View
-        whenever(view.parent).thenReturn(transitionParent)
-        whenever(view.rootView).thenReturn(transitionParent)
-
-        // Action buttons
-        whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause)
-        whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause)
-        whenever(viewHolder.actionNext).thenReturn(actionNext)
-        whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext)
-        whenever(viewHolder.actionPrev).thenReturn(actionPrev)
-        whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev)
-        whenever(viewHolder.action0).thenReturn(action0)
-        whenever(viewHolder.getAction(R.id.action0)).thenReturn(action0)
-        whenever(viewHolder.action1).thenReturn(action1)
-        whenever(viewHolder.getAction(R.id.action1)).thenReturn(action1)
-        whenever(viewHolder.action2).thenReturn(action2)
-        whenever(viewHolder.getAction(R.id.action2)).thenReturn(action2)
-        whenever(viewHolder.action3).thenReturn(action3)
-        whenever(viewHolder.getAction(R.id.action3)).thenReturn(action3)
-        whenever(viewHolder.action4).thenReturn(action4)
-        whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4)
-
-        whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
-    }
-
-    /**
-     * Initialize elements for the recommendation view holder
-     */
-    private fun initRecommendationViewHolderMocks() {
-        recTitle1 = TextView(context)
-        recTitle2 = TextView(context)
-        recTitle3 = TextView(context)
-        recSubtitle1 = TextView(context)
-        recSubtitle2 = TextView(context)
-        recSubtitle3 = TextView(context)
-
-        whenever(recommendationViewHolder.recommendations).thenReturn(view)
-        whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
-
-        // Add a recommendation item
-        coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) }
-        coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) }
-        coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) }
-
-        whenever(recommendationViewHolder.mediaCoverItems)
-            .thenReturn(listOf(coverItem1, coverItem2, coverItem3))
-        whenever(recommendationViewHolder.mediaCoverContainers)
-            .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
-        whenever(recommendationViewHolder.mediaTitles)
-            .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
-        whenever(recommendationViewHolder.mediaSubtitles).thenReturn(
-            listOf(recSubtitle1, recSubtitle2, recSubtitle3)
-        )
-
-        whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
-
-        val actionIcon = Icon.createWithResource(context, R.drawable.ic_android)
-        whenever(smartspaceAction.icon).thenReturn(actionIcon)
-
-        // Needed for card and item action click
-        val mockContext = mock(Context::class.java)
-        whenever(view.context).thenReturn(mockContext)
-        whenever(coverContainer1.context).thenReturn(mockContext)
-        whenever(coverContainer2.context).thenReturn(mockContext)
-        whenever(coverContainer3.context).thenReturn(mockContext)
-    }
-
-    @After
-    fun tearDown() {
-        session.release()
-        player.onDestroy()
-    }
-
-    @Test
-    fun bindWhenUnattached() {
-        val state = mediaData.copy(token = null)
-        player.bindPlayer(state, PACKAGE)
-        assertThat(player.isPlaying()).isFalse()
-    }
-
-    @Test
-    fun bindSemanticActions() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(actionPrev.isEnabled()).isFalse()
-        assertThat(actionPrev.drawable).isNull()
-        verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
-
-        assertThat(actionPlayPause.isEnabled()).isTrue()
-        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
-        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
-
-        assertThat(actionNext.isEnabled()).isTrue()
-        assertThat(actionNext.contentDescription).isEqualTo("next")
-        verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
-
-        // Called twice since these IDs are used as generic buttons
-        assertThat(action0.contentDescription).isEqualTo("custom 0")
-        assertThat(action0.isEnabled()).isFalse()
-        verify(collapsedSet, times(2)).setVisibility(R.id.action0, ConstraintSet.GONE)
-
-        assertThat(action1.contentDescription).isEqualTo("custom 1")
-        assertThat(action1.isEnabled()).isFalse()
-        verify(collapsedSet, times(2)).setVisibility(R.id.action1, ConstraintSet.GONE)
-
-        // Verify generic buttons are hidden
-        verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
-
-        verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
-
-        verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
-    }
-
-    @Test
-    fun bindSemanticActions_reservedPrev() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-
-        // Setup button state: no prev or next button and their slots reserved
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = null,
-            prevOrCustom = null,
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg),
-            false,
-            true
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(actionPrev.isEnabled()).isFalse()
-        assertThat(actionPrev.drawable).isNull()
-        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.INVISIBLE)
-
-        assertThat(actionNext.isEnabled()).isFalse()
-        assertThat(actionNext.drawable).isNull()
-        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
-    }
-
-    @Test
-    fun bindSemanticActions_reservedNext() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-
-        // Setup button state: no prev or next button and their slots reserved
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = null,
-            prevOrCustom = null,
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg),
-            true,
-            false
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(actionPrev.isEnabled()).isFalse()
-        assertThat(actionPrev.drawable).isNull()
-        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
-
-        assertThat(actionNext.isEnabled()).isFalse()
-        assertThat(actionNext.drawable).isNull()
-        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.INVISIBLE)
-    }
-
-    @Test
-    fun bindAlbumView_testHardwareAfterAttach() {
-        player.attachPlayer(viewHolder)
-
-        verify(albumView).setLayerType(View.LAYER_TYPE_HARDWARE, null)
-    }
-
-    @Test
-    fun bindAlbumView_artUsesResource() {
-        val albumArt = Icon.createWithResource(context, R.drawable.ic_android)
-        val state = mediaData.copy(artwork = albumArt)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-
-        verify(albumView).setImageDrawable(any(Drawable::class.java))
-    }
-
-    @Test
-    fun bindAlbumView_setAfterExecutors() {
-        val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
-        val canvas = Canvas(bmp)
-        canvas.drawColor(Color.RED)
-        val albumArt = Icon.createWithBitmap(bmp)
-        val state = mediaData.copy(artwork = albumArt)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-
-        verify(albumView).setImageDrawable(any(Drawable::class.java))
-    }
-
-    @Test
-    fun bindAlbumView_bitmapInLaterStates_setAfterExecutors() {
-        val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
-        val redCanvas = Canvas(redBmp)
-        redCanvas.drawColor(Color.RED)
-        val redArt = Icon.createWithBitmap(redBmp)
-
-        val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
-        val greenCanvas = Canvas(greenBmp)
-        greenCanvas.drawColor(Color.GREEN)
-        val greenArt = Icon.createWithBitmap(greenBmp)
-
-        val state0 = mediaData.copy(artwork = null)
-        val state1 = mediaData.copy(artwork = redArt)
-        val state2 = mediaData.copy(artwork = redArt)
-        val state3 = mediaData.copy(artwork = greenArt)
-        player.attachPlayer(viewHolder)
-
-        // First binding sets (empty) drawable
-        player.bindPlayer(state0, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-        verify(albumView).setImageDrawable(any(Drawable::class.java))
-
-        // Run Metadata update so that later states don't update
-        val captor = argumentCaptor<Animator.AnimatorListener>()
-        verify(mockAnimator, times(2)).addListener(captor.capture())
-        captor.value.onAnimationEnd(mockAnimator)
-        assertThat(titleText.getText()).isEqualTo(TITLE)
-        assertThat(artistText.getText()).isEqualTo(ARTIST)
-
-        // Second binding sets transition drawable
-        player.bindPlayer(state1, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-        val drawableCaptor = argumentCaptor<Drawable>()
-        verify(albumView, times(2)).setImageDrawable(drawableCaptor.capture())
-        assertTrue(drawableCaptor.allValues[1] is TransitionDrawable)
-
-        // Third binding doesn't run transition or update background
-        player.bindPlayer(state2, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-        verify(albumView, times(2)).setImageDrawable(any(Drawable::class.java))
-
-        // Fourth binding to new image runs transition due to color scheme change
-        player.bindPlayer(state3, PACKAGE)
-        bgExecutor.runAllReady()
-        mainExecutor.runAllReady()
-        verify(albumView, times(3)).setImageDrawable(any(Drawable::class.java))
-    }
-
-    @Test
-    fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() {
-        useRealConstraintSets()
-
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", null),
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        getEnabledChangeListener().onEnabledChanged(enabled = false)
-
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
-    }
-
-    @Test
-    fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() {
-        useRealConstraintSets()
-
-        val state = mediaData.copy(semanticActions = MediaButton())
-        player.attachPlayer(viewHolder)
-        getEnabledChangeListener().onEnabledChanged(enabled = false)
-
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
-    }
-
-    @Test
-    fun bind_seekBarEnabled_seekBarVisible() {
-        useRealConstraintSets()
-
-        val state = mediaData.copy(semanticActions = MediaButton())
-        player.attachPlayer(viewHolder)
-        getEnabledChangeListener().onEnabledChanged(enabled = true)
-
-        player.bindPlayer(state, PACKAGE)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
-    }
-
-    @Test
-    fun seekBarChangesToEnabledAfterBind_seekBarChangesToVisible() {
-        useRealConstraintSets()
-
-        val state = mediaData.copy(semanticActions = MediaButton())
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        getEnabledChangeListener().onEnabledChanged(enabled = true)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
-    }
-
-    @Test
-    fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() {
-        useRealConstraintSets()
-
-        val state = mediaData.copy(semanticActions = MediaButton())
-
-        player.attachPlayer(viewHolder)
-        getEnabledChangeListener().onEnabledChanged(enabled = true)
-        player.bindPlayer(state, PACKAGE)
-
-        getEnabledChangeListener().onEnabledChanged(enabled = false)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
-    }
-
-    @Test
-    fun seekBarChangesToDisabledAfterBind_hasActions_seekBarChangesToInvisible() {
-        useRealConstraintSets()
-
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        getEnabledChangeListener().onEnabledChanged(enabled = true)
-        player.bindPlayer(state, PACKAGE)
-
-        getEnabledChangeListener().onEnabledChanged(enabled = false)
-
-        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
-    }
-
-    @Test
-    fun bind_notScrubbing_scrubbingViewsGone() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
-    }
-
-    @Test
-    fun setIsScrubbing_noSemanticActions_viewsNotChanged() {
-        val state = mediaData.copy(semanticActions = null)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        reset(expandedSet)
-
-        val listener = getScrubbingChangeListener()
-
-        listener.onScrubbingChanged(true)
-        mainExecutor.runAllReady()
-
-        verify(expandedSet, never()).setVisibility(eq(R.id.actionPrev), anyInt())
-        verify(expandedSet, never()).setVisibility(eq(R.id.actionNext), anyInt())
-        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_elapsed_time), anyInt())
-        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_total_time), anyInt())
-    }
-
-    @Test
-    fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = null,
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        reset(expandedSet)
-
-        getScrubbingChangeListener().onScrubbingChanged(true)
-        mainExecutor.runAllReady()
-
-        verify(expandedSet).setVisibility(R.id.actionNext, View.VISIBLE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
-    }
-
-    @Test
-    fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = null
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        reset(expandedSet)
-
-        getScrubbingChangeListener().onScrubbingChanged(true)
-        mainExecutor.runAllReady()
-
-        verify(expandedSet).setVisibility(R.id.actionPrev, View.VISIBLE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
-    }
-
-    @Test
-    fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        reset(expandedSet)
-
-        getScrubbingChangeListener().onScrubbingChanged(true)
-        mainExecutor.runAllReady()
-
-        // Only in expanded, we should show the scrubbing times and hide prev+next
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.VISIBLE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.VISIBLE)
-        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
-    }
-
-    @Test
-    fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
-        val state = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        getScrubbingChangeListener().onScrubbingChanged(true)
-        mainExecutor.runAllReady()
-        reset(expandedSet)
-
-        getScrubbingChangeListener().onScrubbingChanged(false)
-        mainExecutor.runAllReady()
-
-        // Only in expanded, we should hide the scrubbing times and show prev+next
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.VISIBLE)
-        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
-    }
-
-    @Test
-    fun bindNotificationActions() {
-        val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-        val actions = listOf(
-            MediaAction(icon, Runnable {}, "previous", bg),
-            MediaAction(icon, Runnable {}, "play", bg),
-            MediaAction(icon, null, "next", bg),
-            MediaAction(icon, null, "custom 0", bg),
-            MediaAction(icon, Runnable {}, "custom 1", bg)
-        )
-        val state = mediaData.copy(
-            actions = actions,
-            actionsToShowInCompact = listOf(1, 2),
-            semanticActions = null
-        )
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-
-        // Verify semantic actions are hidden
-        verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
-
-        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
-
-        verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
-        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
-
-        // Generic actions all enabled
-        assertThat(action0.contentDescription).isEqualTo("previous")
-        assertThat(action0.isEnabled()).isTrue()
-        verify(collapsedSet).setVisibility(R.id.action0, ConstraintSet.GONE)
-
-        assertThat(action1.contentDescription).isEqualTo("play")
-        assertThat(action1.isEnabled()).isTrue()
-        verify(collapsedSet).setVisibility(R.id.action1, ConstraintSet.VISIBLE)
-
-        assertThat(action2.contentDescription).isEqualTo("next")
-        assertThat(action2.isEnabled()).isFalse()
-        verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE)
-
-        assertThat(action3.contentDescription).isEqualTo("custom 0")
-        assertThat(action3.isEnabled()).isFalse()
-        verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
-
-        assertThat(action4.contentDescription).isEqualTo("custom 1")
-        assertThat(action4.isEnabled()).isTrue()
-        verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
-    }
-
-    @Test
-    fun bindAnimatedSemanticActions() {
-        val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
-        val mockAvd1 = mock(AnimatedVectorDrawable::class.java)
-        val mockAvd2 = mock(AnimatedVectorDrawable::class.java)
-        whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
-        whenever(mockAvd1.mutate()).thenReturn(mockAvd1)
-        whenever(mockAvd2.mutate()).thenReturn(mockAvd2)
-
-        val icon = context.getDrawable(R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.ic_media_play_container)
-        val semanticActions0 = MediaButton(
-            playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
-        )
-        val semanticActions1 = MediaButton(
-            playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)
-        )
-        val semanticActions2 = MediaButton(
-            playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)
-        )
-        val state0 = mediaData.copy(semanticActions = semanticActions0)
-        val state1 = mediaData.copy(semanticActions = semanticActions1)
-        val state2 = mediaData.copy(semanticActions = semanticActions2)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state0, PACKAGE)
-
-        // Validate first binding
-        assertThat(actionPlayPause.isEnabled()).isTrue()
-        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
-        assertThat(actionPlayPause.getBackground()).isNull()
-        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
-        assertThat(actionPlayPause.hasOnClickListeners()).isTrue()
-
-        // Trigger animation & update mock
-        actionPlayPause.performClick()
-        verify(mockAvd0, times(1)).start()
-        whenever(mockAvd0.isRunning()).thenReturn(true)
-
-        // Validate states no longer bind
-        player.bindPlayer(state1, PACKAGE)
-        player.bindPlayer(state2, PACKAGE)
-        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
-
-        // Complete animation and run callbacks
-        whenever(mockAvd0.isRunning()).thenReturn(false)
-        val captor = ArgumentCaptor.forClass(Animatable2.AnimationCallback::class.java)
-        verify(mockAvd0, times(1)).registerAnimationCallback(captor.capture())
-        verify(mockAvd1, never())
-            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd2, never())
-            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        captor.getValue().onAnimationEnd(mockAvd0)
-
-        // Validate correct state was bound
-        assertThat(actionPlayPause.contentDescription).isEqualTo("loading")
-        assertThat(actionPlayPause.getBackground()).isNull()
-        verify(mockAvd0, times(1))
-            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd1, times(1))
-            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd2, times(1))
-            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd0, times(1))
-            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd1, times(1))
-            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-        verify(mockAvd2, never())
-            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
-    }
-
-    @Test
-    fun bindText() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, PACKAGE)
-
-        // Capture animation handler
-        val captor = argumentCaptor<Animator.AnimatorListener>()
-        verify(mockAnimator, times(2)).addListener(captor.capture())
-        val handler = captor.value
-
-        // Validate text views unchanged but animation started
-        assertThat(titleText.getText()).isEqualTo("")
-        assertThat(artistText.getText()).isEqualTo("")
-        verify(mockAnimator, times(1)).start()
-
-        // Binding only after animator runs
-        handler.onAnimationEnd(mockAnimator)
-        assertThat(titleText.getText()).isEqualTo(TITLE)
-        assertThat(artistText.getText()).isEqualTo(ARTIST)
-
-        // Rebinding should not trigger animation
-        player.bindPlayer(mediaData, PACKAGE)
-        verify(mockAnimator, times(2)).start()
-    }
-
-    @Test
-    fun bindTextInterrupted() {
-        val data0 = mediaData.copy(artist = "ARTIST_0")
-        val data1 = mediaData.copy(artist = "ARTIST_1")
-        val data2 = mediaData.copy(artist = "ARTIST_2")
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data0, PACKAGE)
-
-        // Capture animation handler
-        val captor = argumentCaptor<Animator.AnimatorListener>()
-        verify(mockAnimator, times(2)).addListener(captor.capture())
-        val handler = captor.value
-
-        handler.onAnimationEnd(mockAnimator)
-        assertThat(artistText.getText()).isEqualTo("ARTIST_0")
-
-        // Bind trigges new animation
-        player.bindPlayer(data1, PACKAGE)
-        verify(mockAnimator, times(3)).start()
-        whenever(mockAnimator.isRunning()).thenReturn(true)
-
-        // Rebind before animation end binds corrct data
-        player.bindPlayer(data2, PACKAGE)
-        handler.onAnimationEnd(mockAnimator)
-        assertThat(artistText.getText()).isEqualTo("ARTIST_2")
-    }
-
-    @Test
-    fun bindDevice() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, PACKAGE)
-        assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
-        assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME)
-        assertThat(seamless.isEnabled()).isTrue()
-    }
-
-    @Test
-    fun bindDisabledDevice() {
-        seamless.id = 1
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(device = disabledDevice)
-        player.bindPlayer(state, PACKAGE)
-        assertThat(seamless.isEnabled()).isFalse()
-        assertThat(seamlessText.getText()).isEqualTo(DISABLED_DEVICE_NAME)
-        assertThat(seamless.contentDescription).isEqualTo(DISABLED_DEVICE_NAME)
-    }
-
-    @Test
-    fun bindNullDevice() {
-        val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(device = null)
-        player.bindPlayer(state, PACKAGE)
-        assertThat(seamless.isEnabled()).isTrue()
-        assertThat(seamlessText.getText()).isEqualTo(fallbackString)
-        assertThat(seamless.contentDescription).isEqualTo(fallbackString)
-    }
-
-    @Test
-    fun bindDeviceWithNullName() {
-        val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(device = device.copy(name = null))
-        player.bindPlayer(state, PACKAGE)
-        assertThat(seamless.isEnabled()).isTrue()
-        assertThat(seamlessText.getText()).isEqualTo(fallbackString)
-        assertThat(seamless.contentDescription).isEqualTo(fallbackString)
-    }
-
-    @Test
-    fun bindDeviceResumptionPlayer() {
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(resumption = true)
-        player.bindPlayer(state, PACKAGE)
-        assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
-        assertThat(seamless.isEnabled()).isFalse()
-    }
-
-    @Test
-    fun bindBroadcastButton() {
-        initMediaViewHolderMocks()
-        initDeviceMediaData(true, APP_NAME)
-
-        val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
-        whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
-        val semanticActions0 = MediaButton(
-                playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
-        )
-        val state = mediaData.copy(resumption = true, semanticActions = semanticActions0,
-                isPlaying = false)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(state, PACKAGE)
-        assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
-        assertThat(seamless.isEnabled()).isTrue()
-
-        seamless.callOnClick()
-
-        verify(logger).logOpenBroadcastDialog(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    /* ***** Guts tests for the player ***** */
-
-    @Test
-    fun player_longClickWhenGutsClosed_gutsOpens() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, KEY)
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).setOnLongClickListener(captor.capture())
-
-        captor.value.onLongClick(viewHolder.player)
-        verify(mediaViewController).openGuts()
-        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun player_longClickWhenGutsOpen_gutsCloses() {
-        player.attachPlayer(viewHolder)
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).setOnLongClickListener(captor.capture())
-
-        captor.value.onLongClick(viewHolder.player)
-        verify(mediaViewController, never()).openGuts()
-        verify(mediaViewController).closeGuts(false)
-    }
-
-    @Test
-    fun player_cancelButtonClick_animation() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, KEY)
-
-        cancel.callOnClick()
-
-        verify(mediaViewController).closeGuts(false)
-    }
-
-    @Test
-    fun player_settingsButtonClick() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, KEY)
-
-        settings.callOnClick()
-        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
-
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(activityStarter).startActivity(captor.capture(), eq(true))
-
-        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
-    }
-
-    @Test
-    fun player_dismissButtonClick() {
-        val mediaKey = "key for dismissal"
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(notificationKey = KEY)
-        player.bindPlayer(state, mediaKey)
-
-        assertThat(dismiss.isEnabled).isEqualTo(true)
-        dismiss.callOnClick()
-        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
-        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
-    }
-
-    @Test
-    fun player_dismissButtonDisabled() {
-        val mediaKey = "key for dismissal"
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(isClearable = false, notificationKey = KEY)
-        player.bindPlayer(state, mediaKey)
-
-        assertThat(dismiss.isEnabled).isEqualTo(false)
-    }
-
-    @Test
-    fun player_dismissButtonClick_notInManager() {
-        val mediaKey = "key for dismissal"
-        whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong())).thenReturn(false)
-
-        player.attachPlayer(viewHolder)
-        val state = mediaData.copy(notificationKey = KEY)
-        player.bindPlayer(state, mediaKey)
-
-        assertThat(dismiss.isEnabled).isEqualTo(true)
-        dismiss.callOnClick()
-
-        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
-        verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false))
-    }
-
-    @Test
-    fun player_gutsOpen_contentDescriptionIsForGuts() {
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        player.attachPlayer(viewHolder)
-
-        val gutsTextString = "gutsText"
-        whenever(gutsText.text).thenReturn(gutsTextString)
-        player.bindPlayer(mediaData, KEY)
-
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).isEqualTo(gutsTextString)
-    }
-
-    @Test
-    fun player_gutsClosed_contentDescriptionIsForPlayer() {
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        player.attachPlayer(viewHolder)
-
-        val app = "appName"
-        player.bindPlayer(mediaData.copy(app = app), KEY)
-
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).contains(mediaData.song!!)
-        assertThat(description).contains(mediaData.artist!!)
-        assertThat(description).contains(app)
-    }
-
-    @Test
-    fun player_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
-        // Start out open
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        whenever(gutsText.text).thenReturn("gutsText")
-        player.attachPlayer(viewHolder)
-        val app = "appName"
-        player.bindPlayer(mediaData.copy(app = app), KEY)
-
-        // Update to closed by long pressing
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-        reset(viewHolder.player)
-
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        captor.value.onLongClick(viewHolder.player)
-
-        // Then content description is now the player content description
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).contains(mediaData.song!!)
-        assertThat(description).contains(mediaData.artist!!)
-        assertThat(description).contains(app)
-    }
-
-    @Test
-    fun player_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
-        // Start out closed
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        val gutsTextString = "gutsText"
-        whenever(gutsText.text).thenReturn(gutsTextString)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData.copy(app = "appName"), KEY)
-
-        // Update to open by long pressing
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-        reset(viewHolder.player)
-
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        captor.value.onLongClick(viewHolder.player)
-
-        // Then content description is now the guts content description
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).isEqualTo(gutsTextString)
-    }
-
-    /* ***** END guts tests for the player ***** */
-
-    /* ***** Guts tests for the recommendations ***** */
-
-    @Test
-    fun recommendations_longClickWhenGutsClosed_gutsOpens() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-
-        captor.value.onLongClick(viewHolder.player)
-        verify(mediaViewController).openGuts()
-        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun recommendations_longClickWhenGutsOpen_gutsCloses() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-
-        captor.value.onLongClick(viewHolder.player)
-        verify(mediaViewController, never()).openGuts()
-        verify(mediaViewController).closeGuts(false)
-    }
-
-    @Test
-    fun recommendations_cancelButtonClick_animation() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        cancel.callOnClick()
-
-        verify(mediaViewController).closeGuts(false)
-    }
-
-    @Test
-    fun recommendations_settingsButtonClick() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        settings.callOnClick()
-        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
-
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(activityStarter).startActivity(captor.capture(), eq(true))
-
-        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
-    }
-
-    @Test
-    fun recommendations_dismissButtonClick() {
-        val mediaKey = "key for dismissal"
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData.copy(targetId = mediaKey))
-
-        assertThat(dismiss.isEnabled).isEqualTo(true)
-        dismiss.callOnClick()
-        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
-        verify(mediaDataManager).dismissSmartspaceRecommendation(eq(mediaKey), anyLong())
-    }
-
-    @Test
-    fun recommendation_gutsOpen_contentDescriptionIsForGuts() {
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        player.attachRecommendation(recommendationViewHolder)
-
-        val gutsTextString = "gutsText"
-        whenever(gutsText.text).thenReturn(gutsTextString)
-        player.bindRecommendation(smartspaceData)
-
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).isEqualTo(gutsTextString)
-    }
-
-    @Test
-    fun recommendation_gutsClosed_contentDescriptionIsForPlayer() {
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        player.attachRecommendation(recommendationViewHolder)
-
-        player.bindRecommendation(smartspaceData)
-
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).contains(REC_APP_NAME)
-    }
-
-    @Test
-    fun recommendation_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
-        // Start out open
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        whenever(gutsText.text).thenReturn("gutsText")
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        // Update to closed by long pressing
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-        reset(viewHolder.player)
-
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        captor.value.onLongClick(viewHolder.player)
-
-        // Then content description is now the player content description
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).contains(REC_APP_NAME)
-    }
-
-    @Test
-    fun recommendation_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
-        // Start out closed
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-        val gutsTextString = "gutsText"
-        whenever(gutsText.text).thenReturn(gutsTextString)
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        // Update to open by long pressing
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(viewHolder.player).onLongClickListener = captor.capture()
-        reset(viewHolder.player)
-
-        whenever(mediaViewController.isGutsVisible).thenReturn(true)
-        captor.value.onLongClick(viewHolder.player)
-
-        // Then content description is now the guts content description
-        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
-        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
-        val description = descriptionCaptor.value.toString()
-
-        assertThat(description).isEqualTo(gutsTextString)
-    }
-
-    /* ***** END guts tests for the recommendations ***** */
-
-    @Test
-    fun actionPlayPauseClick_isLogged() {
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(null, Runnable {}, "play", null)
-        )
-        val data = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.actionPlayPause.callOnClick()
-        verify(logger).logTapAction(eq(R.id.actionPlayPause), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionPrevClick_isLogged() {
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(null, Runnable {}, "previous", null)
-        )
-        val data = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.actionPrev.callOnClick()
-        verify(logger).logTapAction(eq(R.id.actionPrev), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionNextClick_isLogged() {
-        val semanticActions = MediaButton(
-            nextOrCustom = MediaAction(null, Runnable {}, "next", null)
-        )
-        val data = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.actionNext.callOnClick()
-        verify(logger).logTapAction(eq(R.id.actionNext), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionCustom0Click_isLogged() {
-        val semanticActions = MediaButton(
-            custom0 = MediaAction(null, Runnable {}, "custom 0", null)
-        )
-        val data = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.action0.callOnClick()
-        verify(logger).logTapAction(eq(R.id.action0), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionCustom1Click_isLogged() {
-        val semanticActions = MediaButton(
-            custom1 = MediaAction(null, Runnable {}, "custom 1", null)
-        )
-        val data = mediaData.copy(semanticActions = semanticActions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.action1.callOnClick()
-        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionCustom2Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
-        val data = mediaData.copy(actions = actions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.action2.callOnClick()
-        verify(logger).logTapAction(eq(R.id.action2), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionCustom3Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
-        val data = mediaData.copy(actions = actions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.action1.callOnClick()
-        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun actionCustom4Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
-        val data = mediaData.copy(actions = actions)
-
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-
-        viewHolder.action1.callOnClick()
-        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun openOutputSwitcher_isLogged() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, KEY)
-
-        seamless.callOnClick()
-
-        verify(logger).logOpenOutputSwitcher(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun tapContentView_isLogged() {
-        val pendingIntent = mock(PendingIntent::class.java)
-        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
-        val data = mediaData.copy(clickIntent = pendingIntent)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-        verify(viewHolder.player).setOnClickListener(captor.capture())
-
-        captor.value.onClick(viewHolder.player)
-
-        verify(logger).logTapContentView(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun logSeek() {
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(mediaData, KEY)
-
-        val callback: () -> Unit = {}
-        val captor = KotlinArgumentCaptor(callback::class.java)
-        verify(seekBarViewModel).logSeek = captor.capture()
-        captor.value.invoke()
-
-        verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun tapContentView_showOverLockscreen_openActivity() {
-        // WHEN we are on lockscreen and this activity can show over lockscreen
-        whenever(keyguardStateController.isShowing).thenReturn(true)
-        whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(true)
-
-        val clickIntent = mock(Intent::class.java)
-        val pendingIntent = mock(PendingIntent::class.java)
-        whenever(pendingIntent.intent).thenReturn(clickIntent)
-        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
-        val data = mediaData.copy(clickIntent = pendingIntent)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-        verify(viewHolder.player).setOnClickListener(captor.capture())
-
-        // THEN it shows without dismissing keyguard first
-        captor.value.onClick(viewHolder.player)
-        verify(activityStarter).startActivity(eq(clickIntent), eq(true),
-                nullable(), eq(true))
-    }
-
-    @Test
-    fun tapContentView_noShowOverLockscreen_dismissKeyguard() {
-        // WHEN we are on lockscreen and the activity cannot show over lockscreen
-        whenever(keyguardStateController.isShowing).thenReturn(true)
-        whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(false)
-
-        val clickIntent = mock(Intent::class.java)
-        val pendingIntent = mock(PendingIntent::class.java)
-        whenever(pendingIntent.intent).thenReturn(clickIntent)
-        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
-        val data = mediaData.copy(clickIntent = pendingIntent)
-        player.attachPlayer(viewHolder)
-        player.bindPlayer(data, KEY)
-        verify(viewHolder.player).setOnClickListener(captor.capture())
-
-        // THEN keyguard has to be dismissed
-        captor.value.onClick(viewHolder.player)
-        verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
-    }
-
-    @Test
-    fun recommendation_gutsClosed_longPressOpens() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-        whenever(mediaViewController.isGutsVisible).thenReturn(false)
-
-        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
-        verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture())
-
-        captor.value.onLongClick(recommendationViewHolder.recommendations)
-        verify(mediaViewController).openGuts()
-        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun recommendation_settingsButtonClick_isLogged() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        settings.callOnClick()
-        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
-
-        val captor = ArgumentCaptor.forClass(Intent::class.java)
-        verify(activityStarter).startActivity(captor.capture(), eq(true))
-
-        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
-    }
-
-    @Test
-    fun recommendation_dismissButton_isLogged() {
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        dismiss.callOnClick()
-        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun recommendation_tapOnCard_isLogged() {
-        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture())
-        captor.value.onClick(recommendationViewHolder.recommendations)
-
-        verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId))
-    }
-
-    @Test
-    fun recommendation_tapOnItem_isLogged() {
-        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
-        player.attachRecommendation(recommendationViewHolder)
-        player.bindRecommendation(smartspaceData)
-
-        verify(coverContainer1).setOnClickListener(captor.capture())
-        captor.value.onClick(recommendationViewHolder.recommendations)
-
-        verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0))
-    }
-
-    @Test
-    fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
-        player.attachRecommendation(recommendationViewHolder)
-        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("subtitle1")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-            )
-        )
-
-        player.bindRecommendation(data)
-
-        assertThat(recTitle1.text).isEqualTo("")
-        verify(mediaViewController, never()).refreshState()
-    }
-
-    @Test
-    fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
-        player.attachRecommendation(recommendationViewHolder)
-        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("subtitle1")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "empty icon 1")
-                    .setSubtitle("subtitle2")
-                    .setIcon(null)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "empty icon 2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(null)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-            )
-        )
-
-        player.bindRecommendation(data)
-
-        assertThat(recTitle1.text).isEqualTo("")
-        verify(mediaViewController, never()).refreshState()
-    }
-
-    @Test
-    fun bindRecommendation_hasTitlesAndSubtitles() {
-        player.attachRecommendation(recommendationViewHolder)
-
-        val title1 = "Title1"
-        val title2 = "Title2"
-        val title3 = "Title3"
-        val subtitle1 = "Subtitle1"
-        val subtitle2 = "Subtitle2"
-        val subtitle3 = "Subtitle3"
-        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", title1)
-                    .setSubtitle(subtitle1)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", title2)
-                    .setSubtitle(subtitle2)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", title3)
-                    .setSubtitle(subtitle3)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-        player.bindRecommendation(data)
-
-        assertThat(recTitle1.text).isEqualTo(title1)
-        assertThat(recTitle2.text).isEqualTo(title2)
-        assertThat(recTitle3.text).isEqualTo(title3)
-        assertThat(recSubtitle1.text).isEqualTo(subtitle1)
-        assertThat(recSubtitle2.text).isEqualTo(subtitle2)
-        assertThat(recSubtitle3.text).isEqualTo(subtitle3)
-    }
-
-    @Test
-    fun bindRecommendation_noTitle_subtitleNotShown() {
-        player.attachRecommendation(recommendationViewHolder)
-
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-        player.bindRecommendation(data)
-
-        assertThat(recSubtitle1.text).isEqualTo("")
-    }
-
-    @Test
-    fun bindRecommendation_someHaveTitles_allTitleViewsShown() {
-        useRealConstraintSets()
-        player.attachRecommendation(recommendationViewHolder)
-
-        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-        player.bindRecommendation(data)
-
-        assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
-        assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
-        assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
-    }
-
-    @Test
-    fun bindRecommendation_someHaveSubtitles_allSubtitleViewsShown() {
-        useRealConstraintSets()
-        player.attachRecommendation(recommendationViewHolder)
-
-        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "title3")
-                    .setSubtitle("subtitle3")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-        player.bindRecommendation(data)
-
-        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
-        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
-        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
-    }
-
-    @Test
-    fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
-        useRealConstraintSets()
-        player.attachRecommendation(recommendationViewHolder)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "title3")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-
-        player.bindRecommendation(data)
-
-        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
-    }
-
-    @Test
-    fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
-        useRealConstraintSets()
-        player.attachRecommendation(recommendationViewHolder)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("subtitle1")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "")
-                    .setSubtitle("subtitle2")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "")
-                    .setSubtitle("subtitle3")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
-            )
-        )
-
-        player.bindRecommendation(data)
-
-        assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
-        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
-    }
-
-    private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
-        withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
-
-    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener =
-        withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) }
-
-    /**
-     *  Update our test to use real ConstraintSets instead of mocks.
-     *
-     *  Some item visibilities, such as the seekbar visibility, are dependent on other action's
-     *  visibilities. If we use mocks for the ConstraintSets, then action visibility changes are
-     *  just thrown away instead of being saved for reference later. This method sets us up to use
-     *  ConstraintSets so that we do save visibility changes.
-     *
-     *  TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
-     */
-    private fun useRealConstraintSets() {
-        expandedSet = ConstraintSet()
-        collapsedSet = ConstraintSet()
-        whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
-        whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
deleted file mode 100644
index 04b93d7..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-
-import android.graphics.Color;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.InstanceId;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.ArrayList;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class MediaDataCombineLatestTest extends SysuiTestCase {
-
-    @Rule public MockitoRule mockito = MockitoJUnit.rule();
-
-    private static final String KEY = "TEST_KEY";
-    private static final String OLD_KEY = "TEST_KEY_OLD";
-    private static final String APP = "APP";
-    private static final String PACKAGE = "PKG";
-    private static final String ARTIST = "ARTIST";
-    private static final String TITLE = "TITLE";
-    private static final String DEVICE_NAME = "DEVICE_NAME";
-    private static final int USER_ID = 0;
-
-    private MediaDataCombineLatest mManager;
-
-    @Mock private MediaDataManager.Listener mListener;
-
-    private MediaData mMediaData;
-    private MediaDeviceData mDeviceData;
-
-    @Before
-    public void setUp() {
-        mManager = new MediaDataCombineLatest();
-        mManager.addListener(mListener);
-
-        mMediaData = new MediaData(
-                USER_ID, true, APP, null, ARTIST, TITLE, null,
-                new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
-                MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
-                InstanceId.fakeInstanceId(-1), -1);
-        mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
-    }
-
-    @Test
-    public void eventNotEmittedWithoutDevice() {
-        // WHEN data source emits an event without device data
-        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // THEN an event isn't emitted
-        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean());
-    }
-
-    @Test
-    public void eventNotEmittedWithoutMedia() {
-        // WHEN device source emits an event without media data
-        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        // THEN an event isn't emitted
-        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean());
-    }
-
-    @Test
-    public void emitEventAfterDeviceFirst() {
-        // GIVEN that a device event has already been received
-        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        // WHEN media event is received
-        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // THEN the listener receives a combined event
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void emitEventAfterMediaFirst() {
-        // GIVEN that media event has already been received
-        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // WHEN device event is received
-        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        // THEN the listener receives a combined event
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void migrateKeyMediaFirst() {
-        // GIVEN that media and device info has already been received
-        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        reset(mListener);
-        // WHEN a key migration event is received
-        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // THEN the listener receives a combined event
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void migrateKeyDeviceFirst() {
-        // GIVEN that media and device info has already been received
-        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        reset(mListener);
-        // WHEN a key migration event is received
-        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
-        // THEN the listener receives a combined event
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void migrateKeyMediaAfter() {
-        // GIVEN that media and device info has already been received
-        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
-        reset(mListener);
-        // WHEN a second key migration event is received for media
-        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // THEN the key has already been migrated
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void migrateKeyDeviceAfter() {
-        // GIVEN that media and device info has already been received
-        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        reset(mListener);
-        // WHEN a second key migration event is received for the device
-        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
-        // THEN the key has already be migrated
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean(),
-                anyInt(), anyBoolean());
-        assertThat(captor.getValue().getDevice()).isNotNull();
-    }
-
-    @Test
-    public void mediaDataRemoved() {
-        // WHEN media data is removed without first receiving device or data
-        mManager.onMediaDataRemoved(KEY);
-        // THEN a removed event isn't emitted
-        verify(mListener, never()).onMediaDataRemoved(eq(KEY));
-    }
-
-    @Test
-    public void mediaDataRemovedAfterMediaEvent() {
-        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDataRemoved(KEY);
-        verify(mListener).onMediaDataRemoved(eq(KEY));
-    }
-
-    @Test
-    public void mediaDataRemovedAfterDeviceEvent() {
-        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        mManager.onMediaDataRemoved(KEY);
-        verify(mListener).onMediaDataRemoved(eq(KEY));
-    }
-
-    @Test
-    public void mediaDataKeyUpdated() {
-        // GIVEN that device and media events have already been received
-        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
-        // WHEN the key is changed
-        mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData, true /* immediately */,
-                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
-        // THEN the listener gets a load event with the correct keys
-        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(
-                eq("NEW_KEY"), any(), captor.capture(), anyBoolean(), anyInt(), anyBoolean());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
deleted file mode 100644
index 6468fe1..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ /dev/null
@@ -1,495 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.app.smartspace.SmartspaceAction
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.Executor
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-
-private const val KEY = "TEST_KEY"
-private const val KEY_ALT = "TEST_KEY_2"
-private const val USER_MAIN = 0
-private const val USER_GUEST = 10
-private const val PACKAGE = "PKG"
-private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
-private const val APP_UID = 99
-private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
-private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
-private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaDataFilterTest : SysuiTestCase() {
-
-    @Mock
-    private lateinit var listener: MediaDataManager.Listener
-    @Mock
-    private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock
-    private lateinit var broadcastSender: BroadcastSender
-    @Mock
-    private lateinit var mediaDataManager: MediaDataManager
-    @Mock
-    private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
-    @Mock
-    private lateinit var executor: Executor
-    @Mock
-    private lateinit var smartspaceData: SmartspaceMediaData
-    @Mock
-    private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
-    @Mock
-    private lateinit var logger: MediaUiEventLogger
-
-    private lateinit var mediaDataFilter: MediaDataFilter
-    private lateinit var dataMain: MediaData
-    private lateinit var dataGuest: MediaData
-    private val clock = FakeSystemClock()
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        MediaPlayerData.clear()
-        mediaDataFilter = MediaDataFilter(
-            context,
-            broadcastDispatcher,
-            broadcastSender,
-            lockscreenUserManager,
-            executor,
-            clock,
-            logger)
-        mediaDataFilter.mediaDataManager = mediaDataManager
-        mediaDataFilter.addListener(listener)
-
-        // Start all tests as main user
-        setUser(USER_MAIN)
-
-        // Set up test media data
-        dataMain = MediaTestUtils.emptyMediaData.copy(
-                userId = USER_MAIN,
-                packageName = PACKAGE,
-                instanceId = INSTANCE_ID,
-                appUid = APP_UID)
-        dataGuest = dataMain.copy(userId = USER_GUEST)
-
-        `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
-        `when`(smartspaceData.isActive).thenReturn(true)
-        `when`(smartspaceData.isValid()).thenReturn(true)
-        `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
-        `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
-        `when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn(
-                clock.currentTimeMillis() - 100)
-        `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
-    }
-
-    private fun setUser(id: Int) {
-        `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
-        `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
-        mediaDataFilter.handleUserSwitched(id)
-    }
-
-    @Test
-    fun testOnDataLoadedForCurrentUser_callsListener() {
-        // GIVEN a media for main user
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-
-        // THEN we should tell the listener
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun testOnDataLoadedForGuest_doesNotCallListener() {
-        // GIVEN a media for guest user
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
-
-        // THEN we should NOT tell the listener
-        verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean())
-    }
-
-    @Test
-    fun testOnRemovedForCurrent_callsListener() {
-        // GIVEN a media was removed for main user
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-        mediaDataFilter.onMediaDataRemoved(KEY)
-
-        // THEN we should tell the listener
-        verify(listener).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun testOnRemovedForGuest_doesNotCallListener() {
-        // GIVEN a media was removed for guest user
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
-        mediaDataFilter.onMediaDataRemoved(KEY)
-
-        // THEN we should NOT tell the listener
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun testOnUserSwitched_removesOldUserControls() {
-        // GIVEN that we have a media loaded for main user
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-
-        // and we switch to guest user
-        setUser(USER_GUEST)
-
-        // THEN we should remove the main user's media
-        verify(listener).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun testOnUserSwitched_addsNewUserControls() {
-        // GIVEN that we had some media for both users
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
-        reset(listener)
-
-        // and we switch to guest user
-        setUser(USER_GUEST)
-
-        // THEN we should add back the guest user media
-        verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true),
-                eq(0), eq(false))
-
-        // but not the main user's
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(),
-                anyInt(), anyBoolean())
-    }
-
-    @Test
-    fun hasAnyMedia_noMediaSet_returnsFalse() {
-        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
-    }
-
-    @Test
-    fun hasAnyMedia_mediaSet_returnsTrue() {
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-
-        assertThat(mediaDataFilter.hasAnyMedia()).isTrue()
-    }
-
-    @Test
-    fun hasAnyMedia_recommendationSet_returnsFalse() {
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
-    }
-
-    @Test
-    fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() {
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
-    }
-
-    @Test
-    fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() {
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
-    }
-
-    @Test
-    fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() {
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
-    }
-
-    @Test
-    fun hasActiveMedia_noMediaSet_returnsFalse() {
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMedia_inactiveMediaSet_returnsFalse() {
-        val data = dataMain.copy(active = false)
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMedia_activeMediaSet_returnsTrue() {
-        val data = dataMain.copy(active = true)
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
-        assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() {
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() {
-        val data = dataMain.copy(active = false)
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() {
-        val data = dataMain.copy(active = true)
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
-        `when`(smartspaceData.isActive).thenReturn(false)
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
-        `when`(smartspaceData.isValid()).thenReturn(false)
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-    }
-
-    @Test
-    fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
-        `when`(smartspaceData.isActive).thenReturn(true)
-        `when`(smartspaceData.isValid()).thenReturn(true)
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-    }
-
-    @Test
-    fun testHasAnyMediaOrRecommendation_onlyCurrentUser() {
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
-
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
-    }
-
-    @Test
-    fun testHasActiveMediaOrRecommendation_onlyCurrentUser() {
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        val data = dataGuest.copy(active = true)
-
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
-    }
-
-    @Test
-    fun testOnNotificationRemoved_doesntHaveMedia() {
-        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
-        mediaDataFilter.onMediaDataRemoved(KEY)
-        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
-    }
-
-    @Test
-    fun testOnSwipeToDismiss_setsTimedOut() {
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
-        mediaDataFilter.onSwipeToDismiss()
-
-        verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() {
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
-        verify(logger, never()).logRecommendationActivated(any(), any(), any())
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
-
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean())
-        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-        verify(logger, never()).logRecommendationAdded(any(), any())
-        verify(logger, never()).logRecommendationActivated(any(), any(), any())
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() {
-        val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
-        clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
-        verify(logger, never()).logRecommendationActivated(any(), any(), any())
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
-
-        val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
-        clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-        verify(logger, never()).logRecommendationAdded(any(), any())
-        verify(logger, never()).logRecommendationActivated(any(), any(), any())
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
-        `when`(smartspaceData.isActive).thenReturn(false)
-
-        // WHEN we have media that was recently played, but not currently active
-        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
-
-        // AND we get a smartspace signal
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        // THEN we should tell listeners to treat the media as not active instead
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(),
-                anyInt(), anyBoolean())
-        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-        verify(logger, never()).logRecommendationAdded(any(), any())
-        verify(logger, never()).logRecommendationActivated(any(), any(), any())
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
-        `when`(smartspaceData.isValid()).thenReturn(false)
-
-        // WHEN we have media that was recently played, but not currently active
-        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
-
-        // AND we get a smartspace signal
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        // THEN we should tell listeners to treat the media as active instead
-        val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-        // Smartspace update shouldn't be propagated for the empty rec list.
-        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
-        verify(logger, never()).logRecommendationAdded(any(), any())
-        verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() {
-        // WHEN we have media that was recently played, but not currently active
-        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
-
-        // AND we get a smartspace signal
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        // THEN we should tell listeners to treat the media as active instead
-        val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
-        // Smartspace update should also be propagated but not prioritized.
-        verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
-        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
-        verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() {
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
-        verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
-        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
-
-        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-
-        val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
-
-        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-
-        verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
-        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
deleted file mode 100644
index f9c7d2d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ /dev/null
@@ -1,1109 +0,0 @@
-package com.android.systemui.media
-
-import android.app.Notification
-import android.app.Notification.MediaStyle
-import android.app.PendingIntent
-import android.app.smartspace.SmartspaceAction
-import android.app.smartspace.SmartspaceTarget
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.drawable.Icon
-import android.media.MediaDescription
-import android.media.MediaMetadata
-import android.media.session.MediaController
-import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.os.Bundle
-import android.provider.Settings
-import android.service.notification.StatusBarNotification
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.media.utils.MediaConstants
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.InstanceIdSequenceFake
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.SbnBuilder
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-private const val KEY = "KEY"
-private const val KEY_2 = "KEY_2"
-private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
-private const val PACKAGE_NAME = "com.example.app"
-private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
-private const val APP_NAME = "SystemUI"
-private const val SESSION_ARTIST = "artist"
-private const val SESSION_TITLE = "title"
-private const val USER_ID = 0
-private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
-
-private fun <T> anyObject(): T {
-    return Mockito.anyObject<T>()
-}
-
-@SmallTest
-@RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class MediaDataManagerTest : SysuiTestCase() {
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-    @Mock lateinit var mediaControllerFactory: MediaControllerFactory
-    @Mock lateinit var controller: MediaController
-    @Mock lateinit var transportControls: MediaController.TransportControls
-    @Mock lateinit var playbackInfo: MediaController.PlaybackInfo
-    lateinit var session: MediaSession
-    lateinit var metadataBuilder: MediaMetadata.Builder
-    lateinit var backgroundExecutor: FakeExecutor
-    lateinit var foregroundExecutor: FakeExecutor
-    @Mock lateinit var dumpManager: DumpManager
-    @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
-    @Mock lateinit var mediaResumeListener: MediaResumeListener
-    @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
-    @Mock lateinit var mediaDeviceManager: MediaDeviceManager
-    @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
-    @Mock lateinit var mediaDataFilter: MediaDataFilter
-    @Mock lateinit var listener: MediaDataManager.Listener
-    @Mock lateinit var pendingIntent: PendingIntent
-    @Mock lateinit var activityStarter: ActivityStarter
-    lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
-    @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
-    @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
-    lateinit var validRecommendationList: List<SmartspaceAction>
-    @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
-    @Mock private lateinit var mediaFlags: MediaFlags
-    @Mock private lateinit var logger: MediaUiEventLogger
-    lateinit var mediaDataManager: MediaDataManager
-    lateinit var mediaNotification: StatusBarNotification
-    @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
-    private val clock = FakeSystemClock()
-    @Mock private lateinit var tunerService: TunerService
-    @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
-    @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
-
-    private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
-
-    private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
-
-    @Before
-    fun setup() {
-        foregroundExecutor = FakeExecutor(clock)
-        backgroundExecutor = FakeExecutor(clock)
-        smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
-        mediaDataManager = MediaDataManager(
-            context = context,
-            backgroundExecutor = backgroundExecutor,
-            foregroundExecutor = foregroundExecutor,
-            mediaControllerFactory = mediaControllerFactory,
-            broadcastDispatcher = broadcastDispatcher,
-            dumpManager = dumpManager,
-            mediaTimeoutListener = mediaTimeoutListener,
-            mediaResumeListener = mediaResumeListener,
-            mediaSessionBasedFilter = mediaSessionBasedFilter,
-            mediaDeviceManager = mediaDeviceManager,
-            mediaDataCombineLatest = mediaDataCombineLatest,
-            mediaDataFilter = mediaDataFilter,
-            activityStarter = activityStarter,
-            smartspaceMediaDataProvider = smartspaceMediaDataProvider,
-            useMediaResumption = true,
-            useQsMediaPlayer = true,
-            systemClock = clock,
-            tunerService = tunerService,
-            mediaFlags = mediaFlags,
-            logger = logger
-        )
-        verify(tunerService).addTunable(capture(tunableCaptor),
-                eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
-        session = MediaSession(context, "MediaDataManagerTestSession")
-        mediaNotification = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-            }
-            build()
-        }
-        metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
-        whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
-        whenever(controller.transportControls).thenReturn(transportControls)
-        whenever(controller.playbackInfo).thenReturn(playbackInfo)
-        whenever(playbackInfo.playbackType).thenReturn(
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
-
-        // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
-        // listeners in the internal processing pipeline. It receives events, but ince it is a
-        // mock, it doesn't pass those events along the chain to the external listeners. So, just
-        // treat mediaSessionBasedFilter as a listener for testing.
-        listener = mediaSessionBasedFilter
-
-        val recommendationExtras = Bundle().apply {
-            putString("package_name", PACKAGE_NAME)
-            putParcelable("dismiss_intent", DISMISS_INTENT)
-        }
-        val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
-        whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
-        whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
-        whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
-        whenever(mediaRecommendationItem.icon).thenReturn(icon)
-        validRecommendationList = listOf(
-            mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem
-        )
-        whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
-        whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
-        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
-        whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
-        whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
-    }
-
-    @After
-    fun tearDown() {
-        session.release()
-        mediaDataManager.destroy()
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting)
-    }
-
-    @Test
-    fun testSetTimedOut_active_deactivatesMedia() {
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        assertThat(data.active).isTrue()
-
-        mediaDataManager.setTimedOut(KEY, timedOut = true)
-        assertThat(data.active).isFalse()
-        verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testSetTimedOut_resume_dismissesMedia() {
-        // WHEN resume controls are present, and time out
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-                APP_NAME, pendingIntent, PACKAGE_NAME)
-
-        backgroundExecutor.runAllReady()
-        foregroundExecutor.runAllReady()
-        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
-            eq(true), eq(0), eq(false))
-
-        mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
-        verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId))
-
-        // THEN it is removed and listeners are informed
-        foregroundExecutor.advanceClockToLast()
-        foregroundExecutor.runAllReady()
-        verify(listener).onMediaDataRemoved(PACKAGE_NAME)
-    }
-
-    @Test
-    fun testLoadsMetadataOnBackground() {
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        assertThat(backgroundExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun testOnMetaDataLoaded_callsListener() {
-        addNotificationAndLoad()
-        verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_LOCAL))
-    }
-
-    @Test
-    fun testOnMetaDataLoaded_conservesActiveFlag() {
-        whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        mediaDataManager.addListener(listener)
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value!!.active).isTrue()
-    }
-
-    @Test
-    fun testOnNotificationAdded_isRcn_markedRemote() {
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
-            }
-            build()
-        }
-
-        mediaDataManager.onNotificationAdded(KEY, rcn)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo(
-                MediaData.PLAYBACK_CAST_REMOTE)
-        verify(logger).logActiveMediaAdded(anyInt(), eq(SYSTEM_PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
-    }
-
-    @Test
-    fun testOnNotificationAdded_hasSubstituteName_isUsed() {
-        val subName = "Substitute Name"
-        val notif = SbnBuilder().run {
-            modifyNotification(context).also {
-                it.extras = Bundle().apply {
-                    putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
-                }
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                })
-            }
-            build()
-        }
-
-        mediaDataManager.onNotificationAdded(KEY, notif)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
-
-        assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName)
-    }
-
-    @Test
-    fun testLoadMediaDataInBg_invalidTokenNoCrash() {
-        val bundle = Bundle()
-        // wrong data type
-        bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle())
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.addExtras(bundle)
-                it.setStyle(MediaStyle().apply {
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
-            }
-            build()
-        }
-
-        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
-        // no crash even though the data structure is incorrect
-    }
-
-    @Test
-    fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() {
-        val bundle = Bundle()
-        // wrong data type
-        bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle())
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.addExtras(bundle)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
-            }
-            build()
-        }
-
-        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
-        // no crash even though the data structure is incorrect
-    }
-
-    @Test
-    fun testOnNotificationRemoved_callsListener() {
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        mediaDataManager.onNotificationRemoved(KEY)
-        verify(listener).onMediaDataRemoved(eq(KEY))
-        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testOnNotificationRemoved_withResumption() {
-        // GIVEN that the manager has a notification with a resume action
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
-        // WHEN the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
-        // THEN the media data indicates that it is for resumption
-        verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.resumption).isTrue()
-        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
-        verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testOnNotificationRemoved_twoWithResumption() {
-        // GIVEN that the manager has two notifications with resume actions
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
-        verify(listener)
-            .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        val data = mediaDataCaptor.value
-        assertThat(data.resumption).isFalse()
-        val resumableData = data.copy(resumeAction = Runnable {})
-        mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
-        mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
-        reset(listener)
-        // WHEN the first is removed
-        mediaDataManager.onNotificationRemoved(KEY)
-        // THEN the data is for resumption and the key is migrated to the package name
-        verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener, never()).onMediaDataRemoved(eq(KEY))
-        // WHEN the second is removed
-        mediaDataManager.onNotificationRemoved(KEY_2)
-        // THEN the data is for resumption and the second key is removed
-        verify(listener)
-            .onMediaDataLoaded(
-                eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.resumption).isTrue()
-        verify(listener).onMediaDataRemoved(eq(KEY_2))
-    }
-
-    @Test
-    fun testOnNotificationRemoved_withResumption_butNotLocal() {
-        // GIVEN that the manager has a notification with a resume action, but is not local
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        whenever(playbackInfo.playbackType).thenReturn(
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        val dataRemoteWithResume = data.copy(resumeAction = Runnable {},
-                playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
-        verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
-
-        // WHEN the notification is removed
-        mediaDataManager.onNotificationRemoved(KEY)
-
-        // THEN the media data is removed
-        verify(listener).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun testAddResumptionControls() {
-        // WHEN resumption controls are added
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        val currentTime = clock.elapsedRealtime()
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-                APP_NAME, pendingIntent, PACKAGE_NAME)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        // THEN the media data indicates that it is for resumption
-        verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        val data = mediaDataCaptor.value
-        assertThat(data.resumption).isTrue()
-        assertThat(data.song).isEqualTo(SESSION_TITLE)
-        assertThat(data.app).isEqualTo(APP_NAME)
-        assertThat(data.actions).hasSize(1)
-        assertThat(data.semanticActions!!.playOrPause).isNotNull()
-        assertThat(data.lastActive).isAtLeast(currentTime)
-        verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testResumptionDisabled_dismissesResumeControls() {
-        // WHEN there are resume controls and resumption is switched off
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-            APP_NAME, pendingIntent, PACKAGE_NAME)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
-            eq(true), eq(0), eq(false))
-        val data = mediaDataCaptor.value
-        mediaDataManager.setMediaResumptionEnabled(false)
-
-        // THEN the resume controls are dismissed
-        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
-        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testDismissMedia_listenerCalled() {
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
-        assertThat(removed).isTrue()
-
-        foregroundExecutor.advanceClockToLast()
-        foregroundExecutor.runAllReady()
-
-        verify(listener).onMediaDataRemoved(eq(KEY))
-        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
-    }
-
-    @Test
-    fun testDismissMedia_keyDoesNotExist_returnsFalse() {
-        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
-        assertThat(removed).isFalse()
-    }
-
-    @Test
-    fun testBadArtwork_doesNotUse() {
-        // WHEN notification has a too-small artwork
-        val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                it.setLargeIcon(artwork)
-            }
-            build()
-        }
-        mediaDataManager.onNotificationAdded(KEY, notif)
-
-        // THEN it still loads
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener)
-            .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(logger).getNewInstanceId()
-        val instanceId = instanceIdSequence.lastInstanceId
-
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(SmartspaceMediaData(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                packageName = PACKAGE_NAME,
-                cardAction = mediaSmartspaceBaseAction,
-                recommendations = validRecommendationList,
-                dismissIntent = DISMISS_INTENT,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
-        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(logger).getNewInstanceId()
-        val instanceId = instanceIdSequence.lastInstanceId
-
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                dismissIntent = DISMISS_INTENT,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
-        val recommendationExtras = Bundle().apply {
-            putString("package_name", PACKAGE_NAME)
-            putParcelable("dismiss_intent", null)
-        }
-        whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
-        whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
-        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
-
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(logger).getNewInstanceId()
-        val instanceId = instanceIdSequence.lastInstanceId
-
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                dismissIntent = null,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf())
-        verify(logger, never()).getNewInstanceId()
-        verify(listener, never())
-                .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
-    }
-
-    @Ignore("b/233283726")
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(logger).getNewInstanceId()
-
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf())
-        foregroundExecutor.advanceClockToLast()
-        foregroundExecutor.runAllReady()
-
-        verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
-        verifyNoMoreInteractions(logger)
-    }
-
-    @Test
-    fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
-        // WHEN media recommendation setting is off
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
-        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
-
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-
-        // THEN smartspace signal is ignored
-        verify(listener, never())
-                .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
-    }
-
-    @Ignore("b/229838140")
-    @Test
-    fun testMediaRecommendationDisabled_removesSmartspaceData() {
-        // GIVEN a media recommendation card is present
-        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(listener).onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(),
-                anyBoolean())
-
-        // WHEN the media recommendation setting is turned off
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
-        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
-
-        // THEN listeners are notified
-        foregroundExecutor.advanceClockToLast()
-        foregroundExecutor.runAllReady()
-        verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
-    }
-
-    @Test
-    fun testOnMediaDataChanged_updatesLastActiveTime() {
-        val currentTime = clock.elapsedRealtime()
-        addNotificationAndLoad()
-        assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime)
-    }
-
-    @Test
-    fun testOnMediaDataTimedOut_doesNotUpdateLastActiveTime() {
-        // GIVEN that the manager has a notification
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-
-        // WHEN the notification times out
-        clock.advanceTime(100)
-        val currentTime = clock.elapsedRealtime()
-        mediaDataManager.setTimedOut(KEY, true, true)
-
-        // THEN the last active time is not changed
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
-    }
-
-    @Test
-    fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
-        // GIVEN that the manager has a notification with a resume action
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        val instanceId = data.instanceId
-        assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
-
-        // WHEN the notification is removed
-        clock.advanceTime(100)
-        val currentTime = clock.elapsedRealtime()
-        mediaDataManager.onNotificationRemoved(KEY)
-
-        // THEN the last active time is not changed
-        verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.resumption).isTrue()
-        assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
-
-        // Log as a conversion event, not as a new resume control
-        verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
-        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
-    }
-
-    @Test
-    fun testTooManyCompactActions_isTruncated() {
-        // GIVEN a notification where too many compact actions were specified
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setShowActionsInCompactView(0, 1, 2, 3, 4)
-                })
-            }
-            build()
-        }
-
-        // WHEN the notification is loaded
-        mediaDataManager.onNotificationAdded(KEY, notif)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN only the first MAX_COMPACT_ACTIONS are actually set
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo(
-                MediaDataManager.MAX_COMPACT_ACTIONS)
-    }
-
-    @Test
-    fun testTooManyNotificationActions_isTruncated() {
-        // GIVEN a notification where too many notification actions are added
-        val action = Notification.Action(R.drawable.ic_android, "action", null)
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                })
-                for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
-                    it.addAction(action)
-                }
-            }
-            build()
-        }
-
-        // WHEN the notification is loaded
-        mediaDataManager.onNotificationAdded(KEY, notif)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.actions.size).isEqualTo(
-            MediaDataManager.MAX_NOTIFICATION_ACTIONS)
-    }
-
-    @Test
-    fun testPlaybackActions_noState_usesNotification() {
-        val desc = "Notification Action"
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        whenever(controller.playbackState).thenReturn(null)
-
-        val notifWithAction = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                it.addAction(android.R.drawable.ic_media_play, desc, null)
-            }
-            build()
-        }
-        mediaDataManager.onNotificationAdded(KEY, notifWithAction)
-
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNull()
-        assertThat(mediaDataCaptor.value!!.actions).hasSize(1)
-        assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
-    }
-
-    @Test
-    fun testPlaybackActions_hasPrevNext() {
-        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY or
-                PlaybackState.ACTION_SKIP_TO_PREVIOUS or
-                PlaybackState.ACTION_SKIP_TO_NEXT
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
-        customDesc.forEach {
-            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
-        }
-        whenever(controller.playbackState).thenReturn(stateBuilder.build())
-
-        addNotificationAndLoad()
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
-        val actions = mediaDataCaptor.value!!.semanticActions!!
-
-        assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
-        actions.playOrPause!!.action!!.run()
-        verify(transportControls).play()
-
-        assertThat(actions.prevOrCustom).isNotNull()
-        assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_prev))
-        actions.prevOrCustom!!.action!!.run()
-        verify(transportControls).skipToPrevious()
-
-        assertThat(actions.nextOrCustom).isNotNull()
-        assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_next))
-        actions.nextOrCustom!!.action!!.run()
-        verify(transportControls).skipToNext()
-
-        assertThat(actions.custom0).isNotNull()
-        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
-
-        assertThat(actions.custom1).isNotNull()
-        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
-    }
-
-    @Test
-    fun testPlaybackActions_noPrevNext_usesCustom() {
-        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
-        customDesc.forEach {
-            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
-        }
-        whenever(controller.playbackState).thenReturn(stateBuilder.build())
-
-        addNotificationAndLoad()
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
-        val actions = mediaDataCaptor.value!!.semanticActions!!
-
-        assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
-
-        assertThat(actions.prevOrCustom).isNotNull()
-        assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0])
-
-        assertThat(actions.nextOrCustom).isNotNull()
-        assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1])
-
-        assertThat(actions.custom0).isNotNull()
-        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[2])
-
-        assertThat(actions.custom1).isNotNull()
-        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
-    }
-
-    @Test
-    fun testPlaybackActions_connecting() {
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
-                .setActions(stateActions)
-        whenever(controller.playbackState).thenReturn(stateBuilder.build())
-
-        addNotificationAndLoad()
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
-        val actions = mediaDataCaptor.value!!.semanticActions!!
-
-        assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_connecting))
-    }
-
-    @Test
-    fun testPlaybackActions_reservedSpace() {
-        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
-        customDesc.forEach {
-            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
-        }
-        val extras = Bundle().apply {
-            putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
-            putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
-        }
-        whenever(controller.playbackState).thenReturn(stateBuilder.build())
-        whenever(controller.extras).thenReturn(extras)
-
-        addNotificationAndLoad()
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
-        val actions = mediaDataCaptor.value!!.semanticActions!!
-
-        assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
-
-        assertThat(actions.prevOrCustom).isNull()
-        assertThat(actions.nextOrCustom).isNull()
-
-        assertThat(actions.custom0).isNotNull()
-        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
-
-        assertThat(actions.custom1).isNotNull()
-        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
-
-        assertThat(actions.reserveNext).isTrue()
-        assertThat(actions.reservePrev).isTrue()
-    }
-
-    @Test
-    fun testPlaybackActions_playPause_hasButton() {
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY_PAUSE
-        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
-        whenever(controller.playbackState).thenReturn(stateBuilder.build())
-
-        addNotificationAndLoad()
-
-        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
-        val actions = mediaDataCaptor.value!!.semanticActions!!
-
-        assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-            context.getString(R.string.controls_media_button_play))
-        actions.playOrPause!!.action!!.run()
-        verify(transportControls).play()
-    }
-
-    @Test
-    fun testPlaybackLocationChange_isLogged() {
-        // Media control added for local playback
-        addNotificationAndLoad()
-        val instanceId = mediaDataCaptor.value.instanceId
-
-        // Location is updated to local cast
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        whenever(playbackInfo.playbackType).thenReturn(
-            MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
-        addNotificationAndLoad()
-        verify(logger).logPlaybackLocationChange(anyInt(), eq(PACKAGE_NAME),
-            eq(instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
-
-        // update to remote cast
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME) // System package
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
-            }
-            build()
-        }
-
-        mediaDataManager.onNotificationAdded(KEY, rcn)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(logger).logPlaybackLocationChange(anyInt(), eq(SYSTEM_PACKAGE_NAME),
-            eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
-    }
-
-    @Test
-    fun testPlaybackStateChange_keyExists_callsListener() {
-        // Notification has been added
-        addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-
-        // Callback gets an updated state
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
-        callbackCaptor.value.invoke(KEY, state)
-
-        // Listener is notified of updated state
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.isPlaying).isTrue()
-    }
-
-    @Test
-    fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
-        val state = PlaybackState.Builder().build()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-
-        // No media added with this key
-
-        callbackCaptor.value.invoke(KEY, state)
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(),
-                anyBoolean())
-    }
-
-    @Test
-    fun testPlaybackStateChange_keyHasNullToken_doesNothing() {
-        // When we get an update that sets the data's token to null
-        whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        addNotificationAndLoad()
-        val data = mediaDataCaptor.value
-        assertThat(data.resumption).isFalse()
-        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(token = null))
-
-        // And then get a state update
-        val state = PlaybackState.Builder().build()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-
-        // Then no changes are made
-        callbackCaptor.value.invoke(KEY, state)
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(),
-            anyBoolean())
-    }
-
-    @Test
-    fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() {
-        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
-                .build()
-        whenever(controller.playbackState).thenReturn(state)
-
-        addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(KEY, state)
-
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
-        assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
-    }
-
-    @Test
-    fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() {
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
-                .setActions(PlaybackState.ACTION_PLAY_PAUSE)
-                .build()
-
-        // Add resumption controls in order to have semantic actions.
-        // To make sure that they are not null after changing state.
-        mediaDataManager.addResumptionControls(
-                USER_ID,
-                desc,
-                Runnable {},
-                session.sessionToken,
-                APP_NAME,
-                pendingIntent,
-                PACKAGE_NAME
-        )
-        backgroundExecutor.runAllReady()
-        foregroundExecutor.runAllReady()
-
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(PACKAGE_NAME, state)
-
-        verify(listener)
-                .onMediaDataLoaded(
-                        eq(PACKAGE_NAME),
-                        eq(PACKAGE_NAME),
-                        capture(mediaDataCaptor),
-                        eq(true),
-                        eq(0),
-                        eq(false)
-                )
-        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
-        assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
-    }
-
-    @Test
-    fun testPlaybackStateNull_Pause_keyExists_callsListener() {
-        whenever(controller.playbackState).thenReturn(null)
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
-                .setActions(PlaybackState.ACTION_PLAY_PAUSE)
-                .build()
-
-        addNotificationAndLoad()
-        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
-        callbackCaptor.value.invoke(KEY, state)
-
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
-        assertThat(mediaDataCaptor.value.semanticActions).isNull()
-    }
-
-    /**
-     * Helper function to add a media notification and capture the resulting MediaData
-     */
-    private fun addNotificationAndLoad() {
-        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
deleted file mode 100644
index 121c894..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ /dev/null
@@ -1,698 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.bluetooth.BluetoothLeBroadcast
-import android.bluetooth.BluetoothLeBroadcastMetadata
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
-import android.media.session.MediaController
-import android.media.session.MediaController.PlaybackInfo
-import android.media.session.MediaSession
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
-import com.android.settingslib.media.LocalMediaManager
-import com.android.settingslib.media.MediaDevice
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.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.verifyNoMoreInteractions
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-private const val KEY = "TEST_KEY"
-private const val KEY_OLD = "TEST_KEY_OLD"
-private const val PACKAGE = "PKG"
-private const val SESSION_KEY = "SESSION_KEY"
-private const val DEVICE_ID = "DEVICE_ID"
-private const val DEVICE_NAME = "DEVICE_NAME"
-private const val REMOTE_DEVICE_NAME = "REMOTE_DEVICE_NAME"
-private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME"
-private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-public class MediaDeviceManagerTest : SysuiTestCase() {
-
-    private lateinit var manager: MediaDeviceManager
-    @Mock private lateinit var controllerFactory: MediaControllerFactory
-    @Mock private lateinit var lmmFactory: LocalMediaManagerFactory
-    @Mock private lateinit var lmm: LocalMediaManager
-    @Mock private lateinit var mr2: MediaRouter2Manager
-    @Mock private lateinit var muteAwaitFactory: MediaMuteAwaitConnectionManagerFactory
-    @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager
-    private lateinit var fakeFgExecutor: FakeExecutor
-    private lateinit var fakeBgExecutor: FakeExecutor
-    @Mock private lateinit var dumpster: DumpManager
-    @Mock private lateinit var listener: MediaDeviceManager.Listener
-    @Mock private lateinit var device: MediaDevice
-    @Mock private lateinit var icon: Drawable
-    @Mock private lateinit var route: RoutingSessionInfo
-    @Mock private lateinit var controller: MediaController
-    @Mock private lateinit var playbackInfo: PlaybackInfo
-    @Mock private lateinit var configurationController: ConfigurationController
-    @Mock private lateinit var bluetoothLeBroadcast: BluetoothLeBroadcast
-    @Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager
-    @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
-    @Mock private lateinit var packageManager: PackageManager
-    @Mock private lateinit var applicationInfo: ApplicationInfo
-    private lateinit var localBluetoothManager: LocalBluetoothManager
-    private lateinit var session: MediaSession
-    private lateinit var mediaData: MediaData
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        fakeFgExecutor = FakeExecutor(FakeSystemClock())
-        fakeBgExecutor = FakeExecutor(FakeSystemClock())
-        localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
-        manager = MediaDeviceManager(
-                context,
-                controllerFactory,
-                lmmFactory,
-                mr2,
-                muteAwaitFactory,
-                configurationController,
-                localBluetoothManager,
-                fakeFgExecutor,
-                fakeBgExecutor,
-                dumpster
-        )
-        manager.addListener(listener)
-
-        // Configure mocks.
-        whenever(device.name).thenReturn(DEVICE_NAME)
-        whenever(device.iconWithoutBackground).thenReturn(icon)
-        whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm)
-        whenever(muteAwaitFactory.create(lmm)).thenReturn(muteAwaitManager)
-        whenever(lmm.getCurrentConnectedDevice()).thenReturn(device)
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route)
-
-        // Create a media sesssion and notification for testing.
-        session = MediaSession(context, SESSION_KEY)
-
-        mediaData = MediaTestUtils.emptyMediaData.copy(
-                packageName = PACKAGE,
-                token = session.sessionToken)
-        whenever(controllerFactory.create(session.sessionToken))
-                .thenReturn(controller)
-        setupLeAudioConfiguration(false)
-    }
-
-    @After
-    fun tearDown() {
-        session.release()
-    }
-
-    @Test
-    fun removeUnknown() {
-        manager.onMediaDataRemoved("unknown")
-    }
-
-    @Test
-    fun loadMediaData() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        verify(lmmFactory).create(PACKAGE)
-    }
-
-    @Test
-    fun loadAndRemoveMediaData() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        manager.onMediaDataRemoved(KEY)
-        fakeBgExecutor.runAllReady()
-        verify(lmm).unregisterCallback(any())
-        verify(muteAwaitManager).stopListening()
-    }
-
-    @Test
-    fun loadMediaDataWithNullToken() {
-        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-    }
-
-    @Test
-    fun loadWithNewKey() {
-        // GIVEN that media data has been loaded with an old key
-        manager.onMediaDataLoaded(KEY_OLD, null, mediaData)
-        reset(listener)
-        // WHEN data is loaded with a new key
-        manager.onMediaDataLoaded(KEY, KEY_OLD, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the listener for the old key should removed.
-        verify(lmm).unregisterCallback(any())
-        verify(muteAwaitManager).stopListening()
-        // AND a new device event emitted
-        val data = captureDeviceData(KEY, KEY_OLD)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-    }
-
-    @Test
-    fun newKeySameAsOldKey() {
-        // GIVEN that media data has been loaded
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        reset(listener)
-        // WHEN the new key is the same as the old key
-        manager.onMediaDataLoaded(KEY, KEY, mediaData)
-        // THEN no event should be emitted
-        verify(listener, never()).onMediaDeviceChanged(eq(KEY), eq(null), any())
-    }
-
-    @Test
-    fun unknownOldKey() {
-        val oldKey = "unknown"
-        manager.onMediaDataLoaded(KEY, oldKey, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        verify(listener).onMediaDeviceChanged(eq(KEY), eq(oldKey), any())
-    }
-
-    @Test
-    fun updateToSessionTokenWithNullRoute() {
-        // GIVEN that media data has been loaded with a null token
-        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-        // WHEN media data is loaded with a different token
-        // AND that token results in a null route
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device should be disabled
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-    }
-
-    @Test
-    fun deviceEventOnAddNotification() {
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the update is dispatched to the listener
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.icon).isEqualTo(icon)
-    }
-
-    @Test
-    fun removeListener() {
-        // WHEN a listener is removed
-        manager.removeListener(listener)
-        // THEN it doesn't receive device events
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        verify(listener, never()).onMediaDeviceChanged(eq(KEY), eq(null), any())
-    }
-
-    @Test
-    fun deviceListUpdate() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        val deviceCallback = captureCallback()
-        verify(muteAwaitManager).startListening()
-        // WHEN the device list changes
-        deviceCallback.onDeviceListUpdate(mutableListOf(device))
-        assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-        // THEN the update is dispatched to the listener
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.icon).isEqualTo(icon)
-    }
-
-    @Test
-    fun selectedDeviceStateChanged() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        val deviceCallback = captureCallback()
-        // WHEN the selected device changes state
-        deviceCallback.onSelectedDeviceStateChanged(device, 1)
-        assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-        // THEN the update is dispatched to the listener
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.icon).isEqualTo(icon)
-    }
-
-    @Test
-    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        // Run and reset the executors and listeners so we only focus on new events.
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-
-        // Ensure we'll get device info when using the address
-        val fullMediaDevice = mock(MediaDevice::class.java)
-        val address = "fakeAddress"
-        val nameFromDevice = "nameFromDevice"
-        val iconFromDevice = mock(Drawable::class.java)
-        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
-        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
-        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)
-
-        // WHEN the about-to-connect device changes to non-null
-        val deviceCallback = captureCallback()
-        val nameFromParam = "nameFromParam"
-        val iconFromParam = mock(Drawable::class.java)
-        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN the about-to-connect device based on the address is returned
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(nameFromDevice)
-        assertThat(data.name).isNotEqualTo(nameFromParam)
-        assertThat(data.icon).isEqualTo(iconFromDevice)
-        assertThat(data.icon).isNotEqualTo(iconFromParam)
-    }
-
-    @Test
-    fun onAboutToConnectDeviceAdded_cantFindDeviceInfoFromAddress() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        // Run and reset the executors and listeners so we only focus on new events.
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-
-        // Ensure we can't get device info based on the address
-        val address = "fakeAddress"
-        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(null)
-
-        // WHEN the about-to-connect device changes to non-null
-        val deviceCallback = captureCallback()
-        val name = "AboutToConnectDeviceName"
-        val mockIcon = mock(Drawable::class.java)
-        deviceCallback.onAboutToConnectDeviceAdded(address, name, mockIcon)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN the about-to-connect device based on the parameters is returned
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(name)
-        assertThat(data.icon).isEqualTo(mockIcon)
-    }
-
-    @Test
-    fun onAboutToConnectDeviceAddedThenRemoved_usesNormalDevice() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        val deviceCallback = captureCallback()
-        // First set a non-null about-to-connect device
-        deviceCallback.onAboutToConnectDeviceAdded(
-            "fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java)
-        )
-        // Run and reset the executors and listeners so we only focus on new events.
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-
-        // WHEN hasDevice switches to false
-        deviceCallback.onAboutToConnectDeviceRemoved()
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-        // THEN the normal device is returned
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.icon).isEqualTo(icon)
-    }
-
-    @Test
-    fun listenerReceivesKeyRemoved() {
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        // WHEN the notification is removed
-        manager.onMediaDataRemoved(KEY)
-        // THEN the listener receives key removed event
-        verify(listener).onKeyRemoved(eq(KEY))
-    }
-
-    @Test
-    fun deviceNameFromMR2RouteInfo() {
-        // GIVEN that MR2Manager returns a valid routing session
-        whenever(route.name).thenReturn(REMOTE_DEVICE_NAME)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN it uses the route name (instead of device name)
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
-    }
-
-    @Test
-    fun deviceDisabledWhenMR2ReturnsNullRouteInfo() {
-        // GIVEN that MR2Manager returns null for routing session
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is disabled and name is set to null
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isNull()
-    }
-
-    @Test
-    fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
-        // GIVEN a notif is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-        // AND MR2Manager returns null for routing session
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        // WHEN the selected device changes state
-        val deviceCallback = captureCallback()
-        deviceCallback.onSelectedDeviceStateChanged(device, 1)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is disabled and name is set to null
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isNull()
-    }
-
-    @Test
-    fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
-        // GIVEN a notif is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-        // GIVEN that MR2Manager returns null for routing session
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        // WHEN the selected device changes state
-        val deviceCallback = captureCallback()
-        deviceCallback.onDeviceListUpdate(mutableListOf(device))
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is disabled and name is set to null
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isNull()
-    }
-
-    @Test
-    fun mr2ReturnsRouteWithNullName_useLocalDeviceName() {
-        // GIVEN that MR2Manager returns a routing session that does not have a name
-        whenever(route.name).thenReturn(null)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is enabled and uses the current connected device name
-        val data = captureDeviceData(KEY)
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.enabled).isTrue()
-    }
-
-    @Test
-    fun audioInfoChanged() {
-        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
-        whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo)
-        // GIVEN a controller with local playback type
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(mr2)
-        // WHEN onAudioInfoChanged fires with remote playback type
-        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
-        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
-        verify(controller).registerCallback(captor.capture())
-        captor.value.onAudioInfoChanged(playbackInfo)
-        // THEN the route is checked
-        verify(mr2).getRoutingSessionForMediaController(eq(controller))
-    }
-
-    @Test
-    fun audioInfoHasntChanged() {
-        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
-        whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo)
-        // GIVEN a controller with remote playback type
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(mr2)
-        // WHEN onAudioInfoChanged fires with remote playback type
-        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
-        verify(controller).registerCallback(captor.capture())
-        captor.value.onAudioInfoChanged(playbackInfo)
-        // THEN the route is not checked
-        verify(mr2, never()).getRoutingSessionForMediaController(eq(controller))
-    }
-
-    @Test
-    fun deviceIdChanged_informListener() {
-        // GIVEN a notification is added, with a particular device connected
-        whenever(device.id).thenReturn(DEVICE_ID)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        // and later the manager gets a new device ID
-        val deviceCallback = captureCallback()
-        val updatedId = DEVICE_ID + "_new"
-        whenever(device.id).thenReturn(updatedId)
-        deviceCallback.onDeviceListUpdate(mutableListOf(device))
-
-        // THEN the listener gets the updated info
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
-        verify(listener, times(2)).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
-
-        val firstDevice = dataCaptor.allValues.get(0)
-        assertThat(firstDevice.id).isEqualTo(DEVICE_ID)
-
-        val secondDevice = dataCaptor.allValues.get(1)
-        assertThat(secondDevice.id).isEqualTo(updatedId)
-    }
-
-    @Test
-    fun deviceNameChanged_informListener() {
-        // GIVEN a notification is added, with a particular device connected
-        whenever(device.id).thenReturn(DEVICE_ID)
-        whenever(device.name).thenReturn(DEVICE_NAME)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        // and later the manager gets a new device name
-        val deviceCallback = captureCallback()
-        val updatedName = DEVICE_NAME + "_new"
-        whenever(device.name).thenReturn(updatedName)
-        deviceCallback.onDeviceListUpdate(mutableListOf(device))
-
-        // THEN the listener gets the updated info
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
-        verify(listener, times(2)).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
-
-        val firstDevice = dataCaptor.allValues.get(0)
-        assertThat(firstDevice.name).isEqualTo(DEVICE_NAME)
-
-        val secondDevice = dataCaptor.allValues.get(1)
-        assertThat(secondDevice.name).isEqualTo(updatedName)
-    }
-
-    @Test
-    fun deviceIconChanged_doesNotCallListener() {
-        // GIVEN a notification is added, with a particular device connected
-        whenever(device.id).thenReturn(DEVICE_ID)
-        whenever(device.name).thenReturn(DEVICE_NAME)
-        val firstIcon = mock(Drawable::class.java)
-        whenever(device.icon).thenReturn(firstIcon)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
-        verify(listener).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
-
-        // and later the manager gets a callback with only the icon changed
-        val deviceCallback = captureCallback()
-        val secondIcon = mock(Drawable::class.java)
-        whenever(device.icon).thenReturn(secondIcon)
-        deviceCallback.onDeviceListUpdate(mutableListOf(device))
-
-        // THEN the listener is not called again
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testRemotePlaybackDeviceOverride() {
-        whenever(route.name).thenReturn(DEVICE_NAME)
-        val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null,
-                showBroadcastButton = false)
-        val mediaDataWithDevice = mediaData.copy(device = deviceData)
-
-        // GIVEN media data that already has a device set
-        manager.onMediaDataLoaded(KEY, null, mediaDataWithDevice)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        // THEN we keep the device info, and don't register a listener
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
-        verify(lmm, never()).registerCallback(any())
-    }
-
-    @Test
-    fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting() {
-        val broadcastCallback = setupBroadcastCallback()
-        setupLeAudioConfiguration(true)
-        setupBroadcastPackage(BROADCAST_APP_NAME)
-        broadcastCallback.onBroadcastStarted(1, 1)
-
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val data = captureDeviceData(KEY)
-        assertThat(data.showBroadcastButton).isTrue()
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(context.getString(
-                R.string.broadcasting_description_is_broadcasting))
-    }
-
-    @Test
-    fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() {
-        val broadcastCallback = setupBroadcastCallback()
-        setupLeAudioConfiguration(true)
-        setupBroadcastPackage(NORMAL_APP_NAME)
-        broadcastCallback.onBroadcastStarted(1, 1)
-
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val data = captureDeviceData(KEY)
-        assertThat(data.showBroadcastButton).isTrue()
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(BROADCAST_APP_NAME)
-    }
-
-    @Test
-    fun onBroadcastStopped_bluetoothLeBroadcastIsDisabledAndBroadcastingButtonIsGone() {
-        val broadcastCallback = setupBroadcastCallback()
-        setupLeAudioConfiguration(false)
-        broadcastCallback.onBroadcastStopped(1, 1)
-
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-
-        val data = captureDeviceData(KEY)
-        assertThat(data.showBroadcastButton).isFalse()
-    }
-
-    fun captureCallback(): LocalMediaManager.DeviceCallback {
-        val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
-        verify(lmm).registerCallback(captor.capture())
-        return captor.getValue()
-    }
-
-    fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
-        val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback {
-            override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastStartFailed(reason: Int) {}
-            override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastStopFailed(reason: Int) {}
-            override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
-            override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastMetadataChanged(
-                broadcastId: Int,
-                metadata: BluetoothLeBroadcastMetadata
-            ) {}
-        }
-
-        bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
-        return callback
-    }
-
-    fun setupLeAudioConfiguration(isLeAudio: Boolean) {
-        whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
-        whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
-                .thenReturn(localBluetoothLeBroadcast)
-        whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio)
-        whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
-    }
-
-    fun setupBroadcastPackage(currentName: String) {
-        whenever(lmm.packageName).thenReturn(PACKAGE)
-        whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
-                .thenReturn(applicationInfo)
-        whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName)
-        context.setMockPackageManager(packageManager)
-    }
-
-    fun captureDeviceData(key: String, oldKey: String? = null): MediaDeviceData {
-        val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
-        verify(listener).onMediaDeviceChanged(eq(key), eq(oldKey), captor.capture())
-        return captor.getValue()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
deleted file mode 100644
index 954b438..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ /dev/null
@@ -1,413 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.graphics.Rect
-import android.provider.Settings
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.ViewGroup
-import android.widget.FrameLayout
-import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardViewController
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
-import com.android.systemui.dreams.DreamOverlayStateController
-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.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.settings.FakeSettings
-import com.android.systemui.utils.os.FakeHandler
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertNotNull
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaHierarchyManagerTest : SysuiTestCase() {
-
-    @Mock private lateinit var lockHost: MediaHost
-    @Mock private lateinit var qsHost: MediaHost
-    @Mock private lateinit var qqsHost: MediaHost
-    @Mock private lateinit var bypassController: KeyguardBypassController
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock private lateinit var mediaCarouselController: MediaCarouselController
-    @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
-    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
-    @Mock private lateinit var keyguardViewController: KeyguardViewController
-    @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
-    @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
-    @Captor
-    private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
-    @Captor
-    private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)>
-    @Captor
-    private lateinit var dreamOverlayCallback:
-            ArgumentCaptor<(DreamOverlayStateController.Callback)>
-    @JvmField
-    @Rule
-    val mockito = MockitoJUnit.rule()
-    private lateinit var mediaHierarchyManager: MediaHierarchyManager
-    private lateinit var mediaFrame: ViewGroup
-    private val configurationController = FakeConfigurationController()
-    private val notifPanelEvents = FakeNotifPanelEvents()
-    private val settings = FakeSettings()
-    private lateinit var testableLooper: TestableLooper
-    private lateinit var fakeHandler: FakeHandler
-
-    @Before
-    fun setup() {
-        context.getOrCreateTestableResources().addOverride(
-                R.bool.config_use_split_notification_shade, false)
-        mediaFrame = FrameLayout(context)
-        testableLooper = TestableLooper.get(this)
-        fakeHandler = FakeHandler(testableLooper.looper)
-        whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
-        mediaHierarchyManager = MediaHierarchyManager(
-                context,
-                statusBarStateController,
-                keyguardStateController,
-                bypassController,
-                mediaCarouselController,
-                keyguardViewController,
-                dreamOverlayStateController,
-                configurationController,
-                wakefulnessLifecycle,
-                notifPanelEvents,
-                settings,
-                fakeHandler,)
-        verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
-        verify(statusBarStateController).addCallback(statusBarCallback.capture())
-        verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
-        setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
-        setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
-        setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
-        whenever(mediaCarouselController.mediaCarouselScrollHandler)
-                .thenReturn(mediaCarouselScrollHandler)
-        val observer = wakefullnessObserver.value
-        assertNotNull("lifecycle observer wasn't registered", observer)
-        observer.onFinishedWakingUp()
-        // We'll use the viewmanager to verify a few calls below, let's reset this.
-        clearInvocations(mediaCarouselController)
-    }
-
-    private fun setupHost(host: MediaHost, location: Int, top: Int) {
-        whenever(host.location).thenReturn(location)
-        whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top))
-        whenever(host.hostView).thenReturn(uniqueObjectHostView)
-        whenever(host.visible).thenReturn(true)
-        mediaHierarchyManager.register(host)
-    }
-
-    @Test
-    fun testHostViewSetOnRegister() {
-        val host = mediaHierarchyManager.register(lockHost)
-        verify(lockHost).hostView = eq(host)
-    }
-
-    @Test
-    fun testBlockedWhenScreenTurningOff() {
-        // Let's set it onto QS:
-        mediaHierarchyManager.qsExpansion = 1.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
-        val observer = wakefullnessObserver.value
-        assertNotNull("lifecycle observer wasn't registered", observer)
-        observer.onStartedGoingToSleep()
-        clearInvocations(mediaCarouselController)
-        mediaHierarchyManager.qsExpansion = 0.0f
-        verify(mediaCarouselController, times(0))
-                .onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
-    }
-
-    @Test
-    fun testAllowedWhenNotTurningOff() {
-        // Let's set it onto QS:
-        mediaHierarchyManager.qsExpansion = 1.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
-        val observer = wakefullnessObserver.value
-        assertNotNull("lifecycle observer wasn't registered", observer)
-        clearInvocations(mediaCarouselController)
-        mediaHierarchyManager.qsExpansion = 0.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
-    }
-
-    @Test
-    fun testGoingToFullShade() {
-        goToLockscreen()
-
-        // Let's transition all the way to full shade
-        mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-            eq(MediaHierarchyManager.LOCATION_QQS),
-            any(MediaHostState::class.java),
-            eq(false),
-            anyLong(),
-            anyLong())
-        clearInvocations(mediaCarouselController)
-
-        // Let's go back to the lock screen
-        mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-            eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
-            any(MediaHostState::class.java),
-            eq(false),
-            anyLong(),
-            anyLong())
-
-        // Let's make sure alpha is set
-        mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
-        assertThat(mediaFrame.alpha).isNotEqualTo(1.0f)
-    }
-
-    @Test
-    fun testTransformationOnLockScreenIsFading() {
-        goToLockscreen()
-        expandQS()
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
-    }
-
-    @Test
-    fun calculateTransformationType_notOnLockscreen_returnsTransition() {
-        expandQS()
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
-    }
-
-    @Test
-    fun calculateTransformationType_onLockscreen_returnsTransition() {
-        goToLockscreen()
-        expandQS()
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
-    }
-
-    @Test
-    fun calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransition() {
-        enableSplitShade()
-        goToLockscreen()
-        expandQS()
-        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
-    }
-
-    @Test
-    fun calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFade() {
-        enableSplitShade()
-        goToLockscreen()
-        expandQS()
-        whenever(lockHost.visible).thenReturn(false)
-        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
-    }
-
-    @Test
-    fun calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFade() {
-        enableSplitShade()
-        goToLockscreen()
-        goToLockedShade()
-        expandQS()
-        mediaHierarchyManager.setTransitionToFullShadeAmount(0f)
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
-    }
-
-    @Test
-    fun testTransformationOnLockScreenToQQSisFading() {
-        goToLockscreen()
-        goToLockedShade()
-
-        val transformType = mediaHierarchyManager.calculateTransformationType()
-        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
-    }
-
-    @Test
-    fun testCloseGutsRelayToCarousel() {
-        mediaHierarchyManager.closeGuts()
-
-        verify(mediaCarouselController).closeGuts()
-    }
-
-    @Test
-    fun testCloseGutsWhenDoze() {
-        statusBarCallback.value.onDozingChanged(true)
-
-        verify(mediaCarouselController).closeGuts()
-    }
-
-    @Test
-    fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() {
-        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
-    }
-
-    @Test
-    fun getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslation() {
-        enterGuidedTransformation()
-
-        val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
-        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
-                .isEqualTo(expectedTranslation)
-    }
-
-    @Test
-    fun isCurrentlyInGuidedTransformation_hostsVisible_returnsTrue() {
-        goToLockscreen()
-        enterGuidedTransformation()
-        whenever(lockHost.visible).thenReturn(true)
-        whenever(qsHost.visible).thenReturn(true)
-        whenever(qqsHost.visible).thenReturn(true)
-
-        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
-    }
-
-    @Test
-    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
-        notifPanelEvents.changeExpandImmediate(expandImmediate = true)
-        goToLockscreen()
-        enterGuidedTransformation()
-        whenever(lockHost.visible).thenReturn(true)
-        whenever(qsHost.visible).thenReturn(true)
-        whenever(qqsHost.visible).thenReturn(true)
-
-        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
-    }
-
-    @Test
-    fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() {
-        goToLockscreen()
-        enterGuidedTransformation()
-        whenever(lockHost.visible).thenReturn(false)
-        whenever(qsHost.visible).thenReturn(true)
-        whenever(qqsHost.visible).thenReturn(true)
-
-        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
-    }
-
-    @Test
-    fun testDream() {
-        goToDream()
-        setMediaDreamComplicationEnabled(true)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-                eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY),
-                nullable(),
-                eq(false),
-                anyLong(),
-                anyLong())
-        clearInvocations(mediaCarouselController)
-
-        setMediaDreamComplicationEnabled(false)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-                eq(MediaHierarchyManager.LOCATION_QQS),
-                any(MediaHostState::class.java),
-                eq(false),
-                anyLong(),
-                anyLong())
-    }
-
-    private fun enableSplitShade() {
-        context.getOrCreateTestableResources().addOverride(
-            R.bool.config_use_split_notification_shade, true
-        )
-        configurationController.notifyConfigurationChanged()
-    }
-
-    private fun goToLockscreen() {
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
-        statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
-        whenever(dreamOverlayStateController.isOverlayActive).thenReturn(false)
-        dreamOverlayCallback.value.onStateChanged()
-        clearInvocations(mediaCarouselController)
-    }
-
-    private fun goToLockedShade() {
-        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
-        statusBarCallback.value.onStatePreChange(
-            StatusBarState.KEYGUARD,
-            StatusBarState.SHADE_LOCKED
-        )
-    }
-
-    private fun goToDream() {
-        whenever(dreamOverlayStateController.isOverlayActive).thenReturn(true)
-        dreamOverlayCallback.value.onStateChanged()
-    }
-
-    private fun setMediaDreamComplicationEnabled(enabled: Boolean) {
-        val complications = if (enabled) listOf(mock<MediaDreamComplication>()) else emptyList()
-        whenever(dreamOverlayStateController.complications).thenReturn(complications)
-        dreamOverlayCallback.value.onComplicationsChanged()
-    }
-
-    private fun expandQS() {
-        mediaHierarchyManager.qsExpansion = 1.0f
-    }
-
-    private fun enterGuidedTransformation() {
-        mediaHierarchyManager.qsExpansion = 1.0f
-        goToLockscreen()
-        mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
-    }
-
-    companion object {
-        private const val QQS_TOP = 123
-        private const val QS_TOP = 456
-        private const val LOCKSCREEN_TOP = 789
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
deleted file mode 100644
index 6e38d264..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-public class MediaPlayerDataTest : SysuiTestCase() {
-
-    @Mock
-    private lateinit var playerIsPlaying: MediaControlPanel
-    private var systemClock: FakeSystemClock = FakeSystemClock()
-
-    @JvmField
-    @Rule
-    val mockito = MockitoJUnit.rule()
-
-    companion object {
-        val LOCAL = MediaData.PLAYBACK_LOCAL
-        val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
-        val RESUMPTION = true
-        val PLAYING = true
-        val UNDETERMINED = null
-    }
-
-    @Before
-    fun setup() {
-        MediaPlayerData.clear()
-    }
-
-    @Test
-    fun addPlayingThenRemote() {
-        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsRemote = mock(MediaControlPanel::class.java)
-        val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(2)
-        assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
-    }
-
-    @Test
-    fun switchPlayersPlaying() {
-        val playerIsPlaying1 = mock(MediaControlPanel::class.java)
-        var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsPlaying2 = mock(MediaControlPanel::class.java)
-        var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-        MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
-        dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(2)
-        assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
-    }
-
-    @Test
-    fun fullOrderTest() {
-        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
-        val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
-
-        val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
-        val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
-        val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
-
-        val playerCanResume = mock(MediaControlPanel::class.java)
-        val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
-
-        val playerUndetermined = mock(MediaControlPanel::class.java)
-        val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer(
-                "3", dataIsStoppedAndLocal, playerIsStoppedAndLocal, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer(
-                "5", dataIsStoppedAndRemote, playerIsStoppedAndRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer(
-                "2", dataIsPlayingAndRemote, playerIsPlayingAndRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock,
-                isSsReactivated = false)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(6)
-        assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
-            playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined,
-            playerCanResume).inOrder()
-    }
-
-    @Test
-    fun testMoveMediaKeysAround() {
-        val keyA = "a"
-        val keyB = "b"
-
-        val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        assertThat(MediaPlayerData.players()).hasSize(0)
-
-        MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        assertThat(MediaPlayerData.players()).hasSize(1)
-        MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        assertThat(MediaPlayerData.players()).hasSize(2)
-
-        MediaPlayerData.moveIfExists(keyA, keyB)
-
-        assertThat(MediaPlayerData.players()).hasSize(1)
-
-        assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
-        assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
-    }
-
-    private fun createMediaData(
-        app: String,
-        isPlaying: Boolean?,
-        location: Int,
-        resumption: Boolean
-    ) = MediaTestUtils.emptyMediaData.copy(
-        app = app,
-        packageName = "package: $app",
-        playbackLocation = location,
-        resumption = resumption,
-        notificationKey = "key: $app",
-        isPlaying = isPlaying
-    )
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
deleted file mode 100644
index 83168cb..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
+++ /dev/null
@@ -1,489 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.content.pm.ResolveInfo
-import android.content.pm.ServiceInfo
-import android.media.MediaDescription
-import android.media.session.MediaSession
-import android.provider.Settings
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.tuner.TunerService
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import org.junit.After
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.isNotNull
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-private const val KEY = "TEST_KEY"
-private const val OLD_KEY = "RESUME_KEY"
-private const val PACKAGE_NAME = "PKG"
-private const val CLASS_NAME = "CLASS"
-private const val TITLE = "TITLE"
-private const val MEDIA_PREFERENCES = "media_control_prefs"
-private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
-
-private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
-private fun <T> any(): T = Mockito.any<T>()
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaResumeListenerTest : SysuiTestCase() {
-
-    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var mediaDataManager: MediaDataManager
-    @Mock private lateinit var device: MediaDeviceData
-    @Mock private lateinit var token: MediaSession.Token
-    @Mock private lateinit var tunerService: TunerService
-    @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory
-    @Mock private lateinit var resumeBrowser: ResumeMediaBrowser
-    @Mock private lateinit var sharedPrefs: SharedPreferences
-    @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
-    @Mock private lateinit var mockContext: Context
-    @Mock private lateinit var pendingIntent: PendingIntent
-    @Mock private lateinit var dumpManager: DumpManager
-
-    @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
-    @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
-    @Captor lateinit var componentCaptor: ArgumentCaptor<String>
-
-    private lateinit var executor: FakeExecutor
-    private lateinit var data: MediaData
-    private lateinit var resumeListener: MediaResumeListener
-    private val clock = FakeSystemClock()
-
-    private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
-        Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
-    private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver,
-        Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-
-        Settings.Global.putInt(context.contentResolver,
-            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
-
-        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
-                .thenReturn(resumeBrowser)
-
-        // resume components are stored in sharedpreferences
-        whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
-                .thenReturn(sharedPrefs)
-        whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
-        whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
-        whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
-        whenever(mockContext.packageManager).thenReturn(context.packageManager)
-        whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
-
-        executor = FakeExecutor(clock)
-        resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
-        resumeListener.setManager(mediaDataManager)
-        mediaDataManager.addListener(resumeListener)
-
-        data = MediaTestUtils.emptyMediaData.copy(
-                song = TITLE,
-                packageName = PACKAGE_NAME,
-                token = token)
-    }
-
-    @After
-    fun tearDown() {
-        Settings.Global.putInt(context.contentResolver,
-            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting)
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting)
-    }
-
-    @Test
-    fun testWhenNoResumption_doesNothing() {
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
-
-        // When listener is created, we do NOT register a user change listener
-        val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
-                resumeBrowserFactory, dumpManager, clock)
-        listener.setManager(mediaDataManager)
-        verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
-            any(), any(), any(), anyInt(), any())
-
-        // When data is loaded, we do NOT execute or update anything
-        listener.onMediaDataLoaded(KEY, OLD_KEY, data)
-        assertThat(executor.numPending()).isEqualTo(0)
-        verify(mediaDataManager, never()).setResumeAction(any(), any())
-    }
-
-    @Test
-    fun testOnLoad_checksForResume_noService() {
-        // When media data is loaded that has not been checked yet, and does not have a MBS
-        resumeListener.onMediaDataLoaded(KEY, null, data)
-
-        // Then we report back to the manager
-        verify(mediaDataManager).setResumeAction(KEY, null)
-    }
-
-    @Test
-    fun testOnLoad_checksForResume_badService() {
-        setUpMbsWithValidResolveInfo()
-
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.onError()
-        }
-
-        // When media data is loaded that has not been checked yet, and does not have a MBS
-        resumeListener.onMediaDataLoaded(KEY, null, data)
-        executor.runAllReady()
-
-        // Then we report back to the manager
-        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
-    }
-
-    @Test
-    fun testOnLoad_localCast_doesNotCheck() {
-        // When media data is loaded that has not been checked yet, and is a local cast
-        val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCast)
-
-        // Then we do not take action
-        verify(mediaDataManager, never()).setResumeAction(any(), any())
-    }
-
-    @Test
-    fun testOnload_remoteCast_doesNotCheck() {
-        // When media data is loaded that has not been checked yet, and is a remote cast
-        val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
-        resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
-
-        // Then we do not take action
-        verify(mediaDataManager, never()).setResumeAction(any(), any())
-    }
-
-    @Test
-    fun testOnLoad_checksForResume_hasService() {
-        setUpMbsWithValidResolveInfo()
-
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // When media data is loaded that has not been checked yet, and does have a MBS
-        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-
-        // Then we test whether the service is valid
-        executor.runAllReady()
-        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
-        verify(resumeBrowser).testConnection()
-
-        // And since it is, we send info to the manager
-        verify(mediaDataManager).setResumeAction(eq(KEY), isNotNull())
-
-        // But we do not tell it to add new controls
-        verify(mediaDataManager, never())
-                .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
-    }
-
-    @Test
-    fun testOnLoad_doesNotCheckAgain() {
-        // When a media data is loaded that has been checked already
-        var dataCopy = data.copy(hasCheckedForResume = true)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-
-        // Then we should not check it again
-        verify(resumeBrowser, never()).testConnection()
-        verify(mediaDataManager, never()).setResumeAction(KEY, null)
-    }
-
-    @Test
-    fun testOnUserUnlock_loadsTracks() {
-        // Set up mock service to successfully find valid media
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.token).thenReturn(token)
-        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
-        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // Make sure broadcast receiver is registered
-        resumeListener.setManager(mediaDataManager)
-        verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver),
-                any(), any(), any(), anyInt(), any())
-
-        // When we get an unlock event
-        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
-        resumeListener.userChangeReceiver.onReceive(context, intent)
-
-        // Then we should attempt to find recent media for each saved component
-        verify(resumeBrowser, times(3)).findRecentMedia()
-
-        // Then since the mock service found media, the manager should be informed
-        verify(mediaDataManager, times(3)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
-    }
-
-    @Test
-    fun testGetResumeAction_restarts() {
-        setUpMbsWithValidResolveInfo()
-
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // When media data is loaded that has not been checked yet, and does have a MBS
-        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-
-        // Then we test whether the service is valid and set the resume action
-        executor.runAllReady()
-        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
-        verify(resumeBrowser).testConnection()
-        verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
-
-        // When the resume action is run
-        actionCaptor.value.run()
-
-        // Then we call restart
-        verify(resumeBrowser).restart()
-    }
-
-    @Test
-    fun testOnUserUnlock_missingTime_saves() {
-        val currentTime = clock.currentTimeMillis()
-
-        // When resume components without a last played time are loaded
-        testOnUserUnlock_loadsTracks()
-
-        // Then we save an update with the current time
-        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
-        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
-                .dropLastWhile { it.isEmpty() }.forEach {
-            val result = it.split("/")
-            assertThat(result.size).isEqualTo(3)
-            assertThat(result[2].toLong()).isEqualTo(currentTime)
-        }
-        verify(sharedPrefsEditor, times(1)).apply()
-    }
-
-    @Test
-    fun testLoadComponents_recentlyPlayed_adds() {
-        // Set up browser to return successfully
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.token).thenReturn(token)
-        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
-        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // Set up shared preferences to have a component with a recent lastplayed time
-        val lastPlayed = clock.currentTimeMillis()
-        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
-        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
-        resumeListener.setManager(mediaDataManager)
-        mediaDataManager.addListener(resumeListener)
-
-        // When we load a component that was played recently
-        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
-        resumeListener.userChangeReceiver.onReceive(mockContext, intent)
-
-        // We add its resume controls
-        verify(resumeBrowser, times(1)).findRecentMedia()
-        verify(mediaDataManager, times(1)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
-    }
-
-    @Test
-    fun testLoadComponents_old_ignores() {
-        // Set up shared preferences to have a component with an old lastplayed time
-        val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
-        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
-        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
-        resumeListener.setManager(mediaDataManager)
-        mediaDataManager.addListener(resumeListener)
-
-        // When we load a component that is not recent
-        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
-        resumeListener.userChangeReceiver.onReceive(mockContext, intent)
-
-        // We do not try to add resume controls
-        verify(resumeBrowser, times(0)).findRecentMedia()
-        verify(mediaDataManager, times(0)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), any())
-    }
-
-    @Test
-    fun testOnLoad_hasService_updatesLastPlayed() {
-        // Set up browser to return successfully
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.token).thenReturn(token)
-        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
-        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // Set up shared preferences to have a component with a lastplayed time
-        val currentTime = clock.currentTimeMillis()
-        val lastPlayed = currentTime - 1000
-        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
-        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
-        resumeListener.setManager(mediaDataManager)
-        mediaDataManager.addListener(resumeListener)
-
-        // When media data is loaded that has not been checked yet, and does have a MBS
-        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-
-        // Then we store the new lastPlayed time
-        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
-        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
-                .dropLastWhile { it.isEmpty() }.forEach {
-                    val result = it.split("/")
-                    assertThat(result.size).isEqualTo(3)
-                    assertThat(result[2].toLong()).isEqualTo(currentTime)
-                }
-        verify(sharedPrefsEditor, times(1)).apply()
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_newKeyDifferent_oldMediaBrowserDisconnected() {
-        setUpMbsWithValidResolveInfo()
-
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
-
-        resumeListener.onMediaDataLoaded(key = "newKey", oldKey = KEY, data)
-
-        verify(resumeBrowser).disconnect()
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_updatingResumptionListError_mediaBrowserDisconnected() {
-        setUpMbsWithValidResolveInfo()
-
-        // Set up mocks to return with an error
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.onError()
-        }
-
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
-
-        // Ensure we disconnect the browser
-        verify(resumeBrowser).disconnect()
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_trackAdded_mediaBrowserDisconnected() {
-        setUpMbsWithValidResolveInfo()
-
-        // Set up mocks to return with a track added
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
-
-        // Ensure we disconnect the browser
-        verify(resumeBrowser).disconnect()
-    }
-
-    @Test
-    fun testResumeAction_oldMediaBrowserDisconnected() {
-        setUpMbsWithValidResolveInfo()
-
-        val description = MediaDescription.Builder().setTitle(TITLE).build()
-        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.addTrack(description, component, resumeBrowser)
-        }
-
-        // Load media data that will require us to get the resume action
-        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-        executor.runAllReady()
-        verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
-
-        // Set up our factory to return a new browser so we can verify we disconnected the old one
-        val newResumeBrowser = mock(ResumeMediaBrowser::class.java)
-        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
-            .thenReturn(newResumeBrowser)
-
-        // When the resume action is run
-        actionCaptor.value.run()
-
-        // Then we disconnect the old one
-        verify(resumeBrowser).disconnect()
-    }
-
-    /** Sets up mocks to successfully find a MBS that returns valid media. */
-    private fun setUpMbsWithValidResolveInfo() {
-        val pm = mock(PackageManager::class.java)
-        whenever(mockContext.packageManager).thenReturn(pm)
-        val resolveInfo = ResolveInfo()
-        val serviceInfo = ServiceInfo()
-        serviceInfo.packageName = PACKAGE_NAME
-        resolveInfo.serviceInfo = serviceInfo
-        resolveInfo.serviceInfo.name = CLASS_NAME
-        val resumeInfo = listOf(resolveInfo)
-        whenever(pm.queryIntentServices(any(), anyInt())).thenReturn(resumeInfo)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
deleted file mode 100644
index 5586453..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
+++ /dev/null
@@ -1,417 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.session.MediaController
-import android.media.session.MediaController.PlaybackInfo
-import android.media.session.MediaSession
-import android.media.session.MediaSessionManager
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-import org.mockito.Mockito.`when` as whenever
-
-private const val PACKAGE = "PKG"
-private const val KEY = "TEST_KEY"
-private const val NOTIF_KEY = "TEST_KEY"
-
-private val info = MediaTestUtils.emptyMediaData.copy(
-    packageName = PACKAGE,
-    notificationKey = NOTIF_KEY
-)
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-public class MediaSessionBasedFilterTest : SysuiTestCase() {
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    // Unit to be tested
-    private lateinit var filter: MediaSessionBasedFilter
-
-    private lateinit var sessionListener: MediaSessionManager.OnActiveSessionsChangedListener
-    @Mock private lateinit var mediaListener: MediaDataManager.Listener
-
-    // MediaSessionBasedFilter dependencies
-    @Mock private lateinit var mediaSessionManager: MediaSessionManager
-    private lateinit var fgExecutor: FakeExecutor
-    private lateinit var bgExecutor: FakeExecutor
-
-    @Mock private lateinit var controller1: MediaController
-    @Mock private lateinit var controller2: MediaController
-    @Mock private lateinit var controller3: MediaController
-    @Mock private lateinit var controller4: MediaController
-
-    private lateinit var token1: MediaSession.Token
-    private lateinit var token2: MediaSession.Token
-    private lateinit var token3: MediaSession.Token
-    private lateinit var token4: MediaSession.Token
-
-    @Mock private lateinit var remotePlaybackInfo: PlaybackInfo
-    @Mock private lateinit var localPlaybackInfo: PlaybackInfo
-
-    private lateinit var session1: MediaSession
-    private lateinit var session2: MediaSession
-    private lateinit var session3: MediaSession
-    private lateinit var session4: MediaSession
-
-    private lateinit var mediaData1: MediaData
-    private lateinit var mediaData2: MediaData
-    private lateinit var mediaData3: MediaData
-    private lateinit var mediaData4: MediaData
-
-    @Before
-    fun setUp() {
-        fgExecutor = FakeExecutor(FakeSystemClock())
-        bgExecutor = FakeExecutor(FakeSystemClock())
-        filter = MediaSessionBasedFilter(context, mediaSessionManager, fgExecutor, bgExecutor)
-
-        // Configure mocks.
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(emptyList())
-
-        session1 = MediaSession(context, "MediaSessionBasedFilter1")
-        session2 = MediaSession(context, "MediaSessionBasedFilter2")
-        session3 = MediaSession(context, "MediaSessionBasedFilter3")
-        session4 = MediaSession(context, "MediaSessionBasedFilter4")
-
-        token1 = session1.sessionToken
-        token2 = session2.sessionToken
-        token3 = session3.sessionToken
-        token4 = session4.sessionToken
-
-        whenever(controller1.getSessionToken()).thenReturn(token1)
-        whenever(controller2.getSessionToken()).thenReturn(token2)
-        whenever(controller3.getSessionToken()).thenReturn(token3)
-        whenever(controller4.getSessionToken()).thenReturn(token4)
-
-        whenever(controller1.getPackageName()).thenReturn(PACKAGE)
-        whenever(controller2.getPackageName()).thenReturn(PACKAGE)
-        whenever(controller3.getPackageName()).thenReturn(PACKAGE)
-        whenever(controller4.getPackageName()).thenReturn(PACKAGE)
-
-        mediaData1 = info.copy(token = token1)
-        mediaData2 = info.copy(token = token2)
-        mediaData3 = info.copy(token = token3)
-        mediaData4 = info.copy(token = token4)
-
-        whenever(remotePlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
-        whenever(localPlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
-
-        whenever(controller1.getPlaybackInfo()).thenReturn(localPlaybackInfo)
-        whenever(controller2.getPlaybackInfo()).thenReturn(localPlaybackInfo)
-        whenever(controller3.getPlaybackInfo()).thenReturn(localPlaybackInfo)
-        whenever(controller4.getPlaybackInfo()).thenReturn(localPlaybackInfo)
-
-        // Capture listener
-        bgExecutor.runAllReady()
-        val listenerCaptor = ArgumentCaptor.forClass(
-                MediaSessionManager.OnActiveSessionsChangedListener::class.java)
-        verify(mediaSessionManager).addOnActiveSessionsChangedListener(
-                listenerCaptor.capture(), any())
-        sessionListener = listenerCaptor.value
-
-        filter.addListener(mediaListener)
-    }
-
-    @After
-    fun tearDown() {
-        session1.release()
-        session2.release()
-        session3.release()
-        session4.release()
-    }
-
-    @Test
-    fun noMediaSession_loadedEventNotFiltered() {
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun noMediaSession_removedEventNotFiltered() {
-        filter.onMediaDataRemoved(KEY)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        verify(mediaListener).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun matchingMediaSession_loadedEventNotFiltered() {
-        // GIVEN an active session
-        val controllers = listOf(controller1)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the session
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun matchingMediaSession_removedEventNotFiltered() {
-        // GIVEN an active session
-        val controllers = listOf(controller1)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a removed event is received
-        filter.onMediaDataRemoved(KEY)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataRemoved(eq(KEY))
-    }
-
-    @Test
-    fun remoteSession_loadedEventNotFiltered() {
-        // GIVEN a remote session
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matche the session
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun remoteAndLocalSessions_localLoadedEventFiltered() {
-        // GIVEN remote and local sessions
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the remote session
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(KEY, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is filtered
-        verify(mediaListener, never()).onMediaDataLoaded(
-            eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean())
-    }
-
-    @Test
-    fun remoteAndLocalSessions_remoteSessionWithoutNotification() {
-        // GIVEN remote and local sessions
-        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered because there isn't a notification for the remote
-        // session.
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun remoteAndLocalHaveDifferentKeys_localLoadedEventFiltered() {
-        // GIVEN remote and local sessions
-        val key1 = "KEY_1"
-        val key2 = "KEY_2"
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the remote session
-        filter.onMediaDataLoaded(key1, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(key2, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is filtered
-        verify(mediaListener, never())
-            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
-                    anyInt(), anyBoolean())
-        // AND there should be a removed event for key2
-        verify(mediaListener).onMediaDataRemoved(eq(key2))
-    }
-
-    @Test
-    fun remoteAndLocalHaveDifferentKeys_remoteSessionWithoutNotification() {
-        // GIVEN remote and local sessions
-        val key1 = "KEY_1"
-        val key2 = "KEY_2"
-        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(key1, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-        // WHEN a loaded event is received that matches the remote session
-        filter.onMediaDataLoaded(key2, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun multipleRemoteSessions_loadedEventNotFiltered() {
-        // GIVEN two remote sessions
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the remote session
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(KEY, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun multipleOtherSessions_loadedEventNotFiltered() {
-        // GIVEN multiple active sessions from other packages
-        val controllers = listOf(controller1, controller2, controller3, controller4)
-        whenever(controller1.getPackageName()).thenReturn("PKG_1")
-        whenever(controller2.getPackageName()).thenReturn("PKG_2")
-        whenever(controller3.getPackageName()).thenReturn("PKG_3")
-        whenever(controller4.getPackageName()).thenReturn("PKG_4")
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received
-        filter.onMediaDataLoaded(KEY, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun doNotFilterDuringKeyMigration() {
-        val key1 = "KEY_1"
-        val key2 = "KEY_2"
-        // GIVEN a loaded event
-        filter.onMediaDataLoaded(key1, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        reset(mediaListener)
-        // GIVEN remote and local sessions
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // WHEN a loaded event is received that matches the local session but it is a key migration
-        filter.onMediaDataLoaded(key2, key1, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the key migration event is fired
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true),
-                eq(0), eq(false))
-    }
-
-    @Test
-    fun filterAfterKeyMigration() {
-        val key1 = "KEY_1"
-        val key2 = "KEY_2"
-        // GIVEN a loaded event
-        filter.onMediaDataLoaded(key1, null, mediaData1)
-        filter.onMediaDataLoaded(key1, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        reset(mediaListener)
-        // GIVEN remote and local sessions
-        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
-        val controllers = listOf(controller1, controller2)
-        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
-        sessionListener.onActiveSessionsChanged(controllers)
-        // GIVEN that the keys have been migrated
-        filter.onMediaDataLoaded(key2, key1, mediaData1)
-        filter.onMediaDataLoaded(key2, key1, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        reset(mediaListener)
-        // WHEN a loaded event is received that matches the local session
-        filter.onMediaDataLoaded(key2, null, mediaData2)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the key migration event is filtered
-        verify(mediaListener, never())
-            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
-                    anyInt(), anyBoolean())
-        // WHEN a loaded event is received that matches the remote session
-        filter.onMediaDataLoaded(key2, null, mediaData1)
-        bgExecutor.runAllReady()
-        fgExecutor.runAllReady()
-        // THEN the key migration event is fired
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
deleted file mode 100644
index 3d9ed5f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.android.systemui.media
-
-import com.android.internal.logging.InstanceId
-
-class MediaTestUtils {
-    companion object {
-        val emptyMediaData = MediaData(
-            userId = 0,
-            initialized = true,
-            app = null,
-            appIcon = null,
-            artist = null,
-            song = null,
-            artwork = null,
-            actions = emptyList(),
-            actionsToShowInCompact = emptyList(),
-            packageName = "",
-            token = null,
-            clickIntent = null,
-            device = null,
-            active = true,
-            resumeAction = null,
-            isPlaying = false,
-            instanceId = InstanceId.fakeInstanceId(-1),
-            appUid = -1)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
deleted file mode 100644
index 823d4ae..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ /dev/null
@@ -1,593 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.MediaMetadata
-import android.media.session.MediaController
-import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-
-private const val KEY = "KEY"
-private const val PACKAGE = "PKG"
-private const val SESSION_KEY = "SESSION_KEY"
-private const val SESSION_ARTIST = "SESSION_ARTIST"
-private const val SESSION_TITLE = "SESSION_TITLE"
-
-private fun <T> anyObject(): T {
-    return Mockito.anyObject<T>()
-}
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class MediaTimeoutListenerTest : SysuiTestCase() {
-
-    @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
-    @Mock private lateinit var mediaController: MediaController
-    @Mock private lateinit var logger: MediaTimeoutLogger
-    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-    private lateinit var executor: FakeExecutor
-    @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
-    @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
-    @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
-    @Captor private lateinit var dozingCallbackCaptor:
-        ArgumentCaptor<StatusBarStateController.StateListener>
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-    private lateinit var metadataBuilder: MediaMetadata.Builder
-    private lateinit var playbackBuilder: PlaybackState.Builder
-    private lateinit var session: MediaSession
-    private lateinit var mediaData: MediaData
-    private lateinit var resumeData: MediaData
-    private lateinit var mediaTimeoutListener: MediaTimeoutListener
-    private var clock = FakeSystemClock()
-
-    @Before
-    fun setup() {
-        `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
-        executor = FakeExecutor(clock)
-        mediaTimeoutListener = MediaTimeoutListener(
-            mediaControllerFactory,
-            executor,
-            logger,
-            statusBarStateController,
-            clock
-        )
-        mediaTimeoutListener.timeoutCallback = timeoutCallback
-        mediaTimeoutListener.stateCallback = stateCallback
-
-        // Create a media session and notification for testing.
-        metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
-        playbackBuilder = PlaybackState.Builder().apply {
-            setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
-            setActions(PlaybackState.ACTION_PLAY)
-        }
-        session = MediaSession(context, SESSION_KEY).apply {
-            setMetadata(metadataBuilder.build())
-            setPlaybackState(playbackBuilder.build())
-        }
-        session.setActive(true)
-
-        mediaData = MediaTestUtils.emptyMediaData.copy(
-            app = PACKAGE,
-            packageName = PACKAGE,
-            token = session.sessionToken
-        )
-
-        resumeData = mediaData.copy(token = null, active = false, resumption = true)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_registersPlaybackListener() {
-        val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
-
-        `when`(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        verify(logger).logPlaybackState(eq(KEY), eq(playingState))
-
-        // Ignores if same key
-        clearInvocations(mediaController)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
-        verify(mediaController, never()).registerCallback(anyObject())
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
-        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false))
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnMediaDataRemoved_unregistersPlaybackListener() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
-        verify(mediaController).unregisterCallback(anyObject())
-
-        // Ignores duplicate requests
-        clearInvocations(mediaController)
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
-        verify(mediaController, never()).unregisterCallback(anyObject())
-    }
-
-    @Test
-    fun testOnMediaDataRemoved_clearsTimeout() {
-        // GIVEN media that is paused
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        assertThat(executor.numPending()).isEqualTo(1)
-        // WHEN the media is removed
-        mediaTimeoutListener.onMediaDataRemoved(KEY)
-        // THEN the timeout runnable is cancelled
-        assertThat(executor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_migratesKeys() {
-        val newKey = "NEWKEY"
-        // From not playing
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        clearInvocations(mediaController)
-
-        // To playing
-        val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
-        `when`(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
-        verify(mediaController).unregisterCallback(anyObject())
-        verify(mediaController).registerCallback(anyObject())
-        verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true))
-
-        // Enqueues callback
-        assertThat(executor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
-        val newKey = "NEWKEY"
-        // From not playing
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        clearInvocations(mediaController)
-
-        // Migrate, still not playing
-        val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
-        `when`(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
-
-        // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
-        // is another scheduled
-        assertThat(executor.numPending()).isEqualTo(1)
-        verify(logger).logUpdateListener(eq(newKey), eq(false))
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() {
-        // Assuming we're registered
-        testOnMediaDataLoaded_registersPlaybackListener()
-
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
-        assertThat(executor.numPending()).isEqualTo(1)
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_cancelsTimeout_whenResumed() {
-        // Assuming we have a pending timeout
-        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
-
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 0f).build())
-        assertThat(executor.numPending()).isEqualTo(0)
-        verify(logger).logTimeoutCancelled(eq(KEY), any())
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_reusesTimeout_whenNotPlaying() {
-        // Assuming we have a pending timeout
-        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
-
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_STOPPED, 0L, 0f).build())
-        assertThat(executor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun testTimeoutCallback_invokedIfTimeout() {
-        // Assuming we're have a pending timeout
-        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
-
-        with(executor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        verify(timeoutCallback).invoke(eq(KEY), eq(true))
-    }
-
-    @Test
-    fun testIsTimedOut() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
-    }
-
-    @Test
-    fun testOnSessionDestroyed_active_clearsTimeout() {
-        // GIVEN media that is paused
-        val mediaPaused = mediaData.copy(isPlaying = false)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
-        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
-
-        // WHEN the session is destroyed
-        mediaCallbackCaptor.value.onSessionDestroyed()
-
-        // THEN the controller is unregistered and timeout run
-        verify(mediaController).unregisterCallback(anyObject())
-        assertThat(executor.numPending()).isEqualTo(0)
-        verify(logger).logSessionDestroyed(eq(KEY))
-    }
-
-    @Test
-    fun testSessionDestroyed_thenRestarts_resetsTimeout() {
-        // Assuming we have previously destroyed the session
-        testOnSessionDestroyed_active_clearsTimeout()
-
-        // WHEN we get an update with media playing
-        val playingState = mock(android.media.session.PlaybackState::class.java)
-        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
-        `when`(mediaController.playbackState).thenReturn(playingState)
-        val mediaPlaying = mediaData.copy(isPlaying = true)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
-
-        // THEN the timeout runnable will update the state
-        assertThat(executor.numPending()).isEqualTo(1)
-        with(executor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        verify(timeoutCallback).invoke(eq(KEY), eq(false))
-        verify(logger).logReuseListener(eq(KEY))
-    }
-
-    @Test
-    fun testOnSessionDestroyed_resume_continuesTimeout() {
-        // GIVEN resume media with session info
-        val resumeWithSession = resumeData.copy(token = session.sessionToken)
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
-        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
-
-        // WHEN the session is destroyed
-        mediaCallbackCaptor.value.onSessionDestroyed()
-
-        // THEN the controller is unregistered, but the timeout is still scheduled
-        verify(mediaController).unregisterCallback(anyObject())
-        assertThat(executor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
-        // WHEN a regular media is loaded
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-
-        // AND it turns into a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
-
-        // THEN we register a timeout
-        assertThat(executor.numPending()).isEqualTo(1)
-        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
-        // WHEN regular media is paused
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
-        `when`(mediaController.playbackState).thenReturn(pausedState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        assertThat(executor.numPending()).isEqualTo(1)
-
-        // AND it turns into a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
-
-        // THEN we update the timeout length
-        assertThat(executor.numPending()).isEqualTo(1)
-        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_resumption_registersTimeout() {
-        // WHEN a resume media is loaded
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
-
-        // THEN we register a timeout
-        assertThat(executor.numPending()).isEqualTo(1)
-        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
-        // WHEN we have a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
-
-        // AND that media is resumed
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
-        `when`(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
-
-        // THEN the timeout length is changed to a regular media control
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
-    }
-
-    @Test
-    fun testOnMediaDataRemoved_resume_timeoutCancelled() {
-        // WHEN we have a resume control
-        testOnMediaDataLoaded_resumption_registersTimeout()
-        // AND the media is removed
-        mediaTimeoutListener.onMediaDataRemoved(PACKAGE)
-
-        // THEN the timeout runnable is cancelled
-        assertThat(executor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
-        // Load media data once
-        val pausedState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When media data is loaded again, with different actions
-        val playingState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
-        loadMediaDataWithPlaybackState(playingState)
-
-        // Then the callback is not invoked
-        verify(stateCallback, never()).invoke(eq(KEY), any())
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
-        // Load media data once
-        val pausedState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When the playback state changes, and has different actions
-        val playingState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
-
-        // Then the callback is invoked
-        verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
-        val customOne = PlaybackState.CustomAction.Builder(
-                    "ACTION_1",
-                    "custom action 1",
-                    android.R.drawable.ic_media_ff)
-                .build()
-        val pausedState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .addCustomAction(customOne)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When the playback state actions change
-        val customTwo = PlaybackState.CustomAction.Builder(
-                "ACTION_2",
-                "custom action 2",
-                android.R.drawable.ic_media_rew)
-                .build()
-        val pausedStateTwoActions = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .addCustomAction(customOne)
-                .addCustomAction(customTwo)
-                .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
-
-        // Then the callback is invoked
-        verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_sameActions_noCallback() {
-        val stateWithActions = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
-        loadMediaDataWithPlaybackState(stateWithActions)
-
-        // When the playback state updates with the same actions
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
-
-        // Then the callback is not invoked again
-        verify(stateCallback, never()).invoke(eq(KEY), any())
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
-        val actionName = "custom action"
-        val actionIcon = android.R.drawable.ic_media_ff
-        val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
-                .build()
-        val stateOne = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .addCustomAction(customOne)
-                .build()
-        loadMediaDataWithPlaybackState(stateOne)
-
-        // When the playback state is updated, but has the same actions
-        val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
-                .build()
-        val stateTwo = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .addCustomAction(customTwo)
-                .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
-
-        // Then the callback is not invoked
-        verify(stateCallback, never()).invoke(eq(KEY), any())
-    }
-
-    @Test
-    fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
-        // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When media data is loaded again but playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
-        loadMediaDataWithPlaybackState(playingState)
-
-        // Then the callback is not invoked
-        verify(stateCallback, never()).invoke(eq(KEY), any())
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
-        // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When the playback state changes to playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
-
-        // Then the callback is invoked
-        verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
-    }
-
-    @Test
-    fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
-        // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
-        loadMediaDataWithPlaybackState(pausedState)
-
-        // When the playback state is updated, but still not playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_STOPPED, 0L, 0f)
-                .build()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
-
-        // Then the callback is not invoked
-        verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
-    }
-
-    @Test
-    fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
-        // When paused media is loaded
-        testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
-        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
-
-        // And we doze past the scheduled timeout
-        val time = clock.currentTimeMillis()
-        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
-        assertThat(executor.numPending()).isEqualTo(1)
-
-        // Then when no longer dozing, the timeout runs immediately
-        dozingCallbackCaptor.value.onDozingChanged(false)
-        verify(timeoutCallback).invoke(eq(KEY), eq(true))
-        verify(logger).logTimeout(eq(KEY))
-
-        // and cancel any later scheduled timeout
-        verify(logger).logTimeoutCancelled(eq(KEY), any())
-        assertThat(executor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() {
-        // When paused media is loaded
-        val time = clock.currentTimeMillis()
-        clock.setElapsedRealtime(time)
-        testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
-        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
-
-        // And we doze, but not past the scheduled timeout
-        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
-        assertThat(executor.numPending()).isEqualTo(1)
-
-        // Then when no longer dozing, the timeout remains scheduled
-        dozingCallbackCaptor.value.onDozingChanged(false)
-        verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
-        assertThat(executor.numPending()).isEqualTo(1)
-    }
-
-    private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
-        `when`(mediaController.playbackState).thenReturn(state)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
deleted file mode 100644
index 622a512..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
+++ /dev/null
@@ -1,188 +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.media
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
-import com.android.systemui.util.animation.MeasurementInput
-import com.android.systemui.util.animation.TransitionLayout
-import com.android.systemui.util.animation.TransitionViewState
-import com.android.systemui.util.animation.WidgetState
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.floatThat
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class MediaViewControllerTest : SysuiTestCase() {
-    private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
-    private val mediaHostStatesManager = MediaHostStatesManager()
-    private val configurationController =
-        com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
-    private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
-    private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
-    @Mock lateinit var logger: MediaViewLogger
-    @Mock private lateinit var mockViewState: TransitionViewState
-    @Mock private lateinit var mockCopiedState: TransitionViewState
-    @Mock private lateinit var detailWidgetState: WidgetState
-    @Mock private lateinit var controlWidgetState: WidgetState
-    @Mock private lateinit var mediaTitleWidgetState: WidgetState
-    @Mock private lateinit var mediaContainerWidgetState: WidgetState
-
-    val delta = 0.0001F
-
-    private lateinit var mediaViewController: MediaViewController
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mediaViewController =
-            MediaViewController(context, configurationController, mediaHostStatesManager, logger)
-    }
-
-    @Test
-    fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
-        mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
-        player.measureState = TransitionViewState().apply { this.height = 100 }
-        mediaHostStateHolder.expansion = 1f
-        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
-        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
-        mediaHostStateHolder.measurementInput =
-            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
-
-        // Test no squish
-        mediaHostStateHolder.squishFraction = 1f
-        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
-
-        // Test half squish
-        mediaHostStateHolder.squishFraction = 0.5f
-        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
-    }
-
-    @Test
-    fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
-        mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
-        recommendation.measureState = TransitionViewState().apply { this.height = 100 }
-        mediaHostStateHolder.expansion = 1f
-        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
-        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
-        mediaHostStateHolder.measurementInput =
-            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
-
-        // Test no squish
-        mediaHostStateHolder.squishFraction = 1f
-        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
-
-        // Test half squish
-        mediaHostStateHolder.squishFraction = 0.5f
-        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
-    }
-
-    @Test
-    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
-        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
-        whenever(mockCopiedState.widgetStates)
-            .thenReturn(
-                mutableMapOf(
-                    R.id.media_progress_bar to controlWidgetState,
-                    R.id.header_artist to detailWidgetState
-                )
-            )
-
-        val detailSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
-        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val detailSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        mediaViewController.squishViewState(mockViewState, detailSquishEnd)
-        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
-        val controlSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
-        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val controlSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        mediaViewController.squishViewState(mockViewState, controlSquishEnd)
-        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-    }
-
-    @Test
-    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
-        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
-        whenever(mockCopiedState.widgetStates)
-            .thenReturn(
-                mutableMapOf(
-                    R.id.media_title1 to mediaTitleWidgetState,
-                    R.id.media_cover1_container to mediaContainerWidgetState
-                )
-            )
-
-        val containerSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
-        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val containerSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, containerSquishEnd)
-        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-
-        val titleSquishMiddle =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
-        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
-
-        val titleSquishEnd =
-            TRANSFORM_BEZIER.getInterpolation(
-                (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
-            )
-        mediaViewController.squishViewState(mockViewState, titleSquishEnd)
-        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt
deleted file mode 100644
index ee32793..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.android.systemui.media
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.widget.FrameLayout
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaViewHolderTest : SysuiTestCase() {
-
-    @Test
-    fun create_succeeds() {
-        val inflater = LayoutInflater.from(context)
-        val parent = FrameLayout(context)
-
-        MediaViewHolder.create(inflater, parent)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
deleted file mode 100644
index 311aa96..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
+++ /dev/null
@@ -1,201 +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.media
-
-import org.mockito.Mockito.`when` as whenever
-import android.animation.Animator
-import android.test.suitebuilder.annotation.SmallTest
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import com.android.systemui.SysuiTestCase
-import junit.framework.Assert.fail
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.times
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MetadataAnimationHandlerTest : SysuiTestCase() {
-
-    private interface Callback : () -> Unit
-    private lateinit var handler: MetadataAnimationHandler
-
-    @Mock private lateinit var enterAnimator: Animator
-    @Mock private lateinit var exitAnimator: Animator
-    @Mock private lateinit var postExitCB: Callback
-    @Mock private lateinit var postEnterCB: Callback
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
-    }
-
-    @After
-    fun tearDown() {}
-
-    @Test
-    fun firstBind_startsAnimationSet() {
-        val cb = { fail("Unexpected callback") }
-        handler.setNext("data-1", cb, cb)
-
-        verify(exitAnimator).start()
-    }
-
-    @Test
-    fun executeAnimationEnd_runsCallacks() {
-        // We expect this first call to only start the exit animator
-        handler.setNext("data-1", postExitCB, postEnterCB)
-        verify(exitAnimator, times(1)).start()
-        verify(enterAnimator, never()).start()
-        verify(postExitCB, never()).invoke()
-        verify(postEnterCB, never()).invoke()
-
-        // After the exit animator completes,
-        // the exit cb should run, and enter animation should start
-        handler.onAnimationEnd(exitAnimator)
-        verify(exitAnimator, times(1)).start()
-        verify(enterAnimator, times(1)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postEnterCB, never()).invoke()
-
-        // After the exit animator completes,
-        // the enter cb should run without other state changes
-        handler.onAnimationEnd(enterAnimator)
-        verify(exitAnimator, times(1)).start()
-        verify(enterAnimator, times(1)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postEnterCB, times(1)).invoke()
-    }
-
-    @Test
-    fun rebindSameData_executesFirstCallback() {
-        val postExitCB2 = mock(Callback::class.java)
-
-        handler.setNext("data-1", postExitCB, postEnterCB)
-        handler.setNext("data-1", postExitCB2, postEnterCB)
-        handler.onAnimationEnd(exitAnimator)
-
-        verify(postExitCB, times(1)).invoke()
-        verify(postExitCB2, never()).invoke()
-        verify(postEnterCB, never()).invoke()
-    }
-
-    @Test
-    fun rebindDifferentData_executesSecondCallback() {
-        val postExitCB2 = mock(Callback::class.java)
-
-        handler.setNext("data-1", postExitCB, postEnterCB)
-        handler.setNext("data-2", postExitCB2, postEnterCB)
-        handler.onAnimationEnd(exitAnimator)
-
-        verify(postExitCB, never()).invoke()
-        verify(postExitCB2, times(1)).invoke()
-        verify(postEnterCB, never()).invoke()
-    }
-
-    @Test
-    fun rebindBeforeEnterComplete_animationRestarts() {
-        val postExitCB2 = mock(Callback::class.java)
-        val postEnterCB2 = mock(Callback::class.java)
-
-        // We expect this first call to only start the exit animator
-        handler.setNext("data-1", postExitCB, postEnterCB)
-        verify(exitAnimator, times(1)).start()
-        verify(enterAnimator, never()).start()
-        verify(postExitCB, never()).invoke()
-        verify(postExitCB2, never()).invoke()
-        verify(postEnterCB, never()).invoke()
-        verify(postEnterCB2, never()).invoke()
-
-        // After the exit animator completes,
-        // the exit cb should run, and enter animation should start
-        whenever(exitAnimator.isRunning()).thenReturn(true)
-        whenever(enterAnimator.isRunning()).thenReturn(false)
-        handler.onAnimationEnd(exitAnimator)
-        verify(exitAnimator, times(1)).start()
-        verify(enterAnimator, times(1)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postExitCB2, never()).invoke()
-        verify(postEnterCB, never()).invoke()
-        verify(postEnterCB2, never()).invoke()
-
-        // Setting new data before the enter animator completes should not trigger
-        // the exit animator an additional time (since it's already running)
-        whenever(exitAnimator.isRunning()).thenReturn(false)
-        whenever(enterAnimator.isRunning()).thenReturn(true)
-        handler.setNext("data-2", postExitCB2, postEnterCB2)
-        verify(exitAnimator, times(1)).start()
-
-        // Finishing the enterAnimator should cause the exitAnimator to fire again
-        // since the data change and additional time. No enterCB should be executed.
-        handler.onAnimationEnd(enterAnimator)
-        verify(exitAnimator, times(2)).start()
-        verify(enterAnimator, times(1)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postExitCB2, never()).invoke()
-        verify(postEnterCB, never()).invoke()
-        verify(postEnterCB2, never()).invoke()
-
-        // Continuing the sequence, this triggers the enter animator an additional time
-        handler.onAnimationEnd(exitAnimator)
-        verify(exitAnimator, times(2)).start()
-        verify(enterAnimator, times(2)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postExitCB2, times(1)).invoke()
-        verify(postEnterCB, never()).invoke()
-        verify(postEnterCB2, never()).invoke()
-
-        // And finally the enter animator completes,
-        // triggering the correct postEnterCallback to fire
-        handler.onAnimationEnd(enterAnimator)
-        verify(exitAnimator, times(2)).start()
-        verify(enterAnimator, times(2)).start()
-        verify(postExitCB, times(1)).invoke()
-        verify(postExitCB2, times(1)).invoke()
-        verify(postEnterCB, never()).invoke()
-        verify(postEnterCB2, times(1)).invoke()
-    }
-
-    @Test
-    fun exitAnimationEndMultipleCalls_singleCallbackExecution() {
-        handler.setNext("data-1", postExitCB, postEnterCB)
-        handler.onAnimationEnd(exitAnimator)
-        handler.onAnimationEnd(exitAnimator)
-        handler.onAnimationEnd(exitAnimator)
-
-        verify(postExitCB, times(1)).invoke()
-    }
-
-    @Test
-    fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() {
-        handler.onAnimationEnd(enterAnimator)
-
-        verify(exitAnimator, never()).start()
-        verify(enterAnimator, never()).start()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
deleted file mode 100644
index dafaa6b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.ComponentName
-import android.content.Context
-import android.media.MediaDescription
-import android.media.browse.MediaBrowser
-import android.media.session.MediaController
-import android.media.session.MediaSession
-import android.service.media.MediaBrowserService
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.mockito.mock
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
-
-private const val PACKAGE_NAME = "package"
-private const val CLASS_NAME = "class"
-private const val TITLE = "song title"
-private const val MEDIA_ID = "media ID"
-private const val ROOT = "media browser root"
-
-private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
-private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
-private fun <T> any(): T = Mockito.any<T>()
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-public class ResumeMediaBrowserTest : SysuiTestCase() {
-
-    private lateinit var resumeBrowser: TestableResumeMediaBrowser
-    private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-    private val description = MediaDescription.Builder()
-            .setTitle(TITLE)
-            .setMediaId(MEDIA_ID)
-            .build()
-
-    @Mock lateinit var callback: ResumeMediaBrowser.Callback
-    @Mock lateinit var listener: MediaResumeListener
-    @Mock lateinit var service: MediaBrowserService
-    @Mock lateinit var logger: ResumeMediaBrowserLogger
-    @Mock lateinit var browserFactory: MediaBrowserFactory
-    @Mock lateinit var browser: MediaBrowser
-    @Mock lateinit var token: MediaSession.Token
-    @Mock lateinit var mediaController: MediaController
-    @Mock lateinit var transportControls: MediaController.TransportControls
-
-    @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
-    @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
-    @Captor lateinit var mediaControllerCallback: ArgumentCaptor<MediaController.Callback>
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        whenever(browserFactory.create(any(), capture(connectionCallback), any()))
-                .thenReturn(browser)
-
-        whenever(mediaController.transportControls).thenReturn(transportControls)
-        whenever(mediaController.sessionToken).thenReturn(token)
-
-        resumeBrowser = TestableResumeMediaBrowser(
-            context,
-            callback,
-            component,
-            browserFactory,
-            logger,
-            mediaController
-        )
-    }
-
-    @Test
-    fun testConnection_connectionFails_callsOnError() {
-        // When testConnection cannot connect to the service
-        setupBrowserFailed()
-        resumeBrowser.testConnection()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testConnection_connects_onConnected() {
-        // When testConnection can connect to the service
-        setupBrowserConnection()
-        resumeBrowser.testConnection()
-
-        // Then it calls onConnected
-        verify(callback).onConnected()
-    }
-
-    @Test
-    fun testConnection_noValidMedia_error() {
-        // When testConnection can connect to the service, and does not find valid media
-        setupBrowserConnectionNoResults()
-        resumeBrowser.testConnection()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testConnection_hasValidMedia_addTrack() {
-        // When testConnection can connect to the service, and finds valid media
-        setupBrowserConnectionValidMedia()
-        resumeBrowser.testConnection()
-
-        // Then it calls addTrack
-        verify(callback).onConnected()
-        verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
-    }
-
-    @Test
-    fun testConnection_thenSessionDestroyed_disconnects() {
-        // When testConnection is called and we connect successfully
-        setupBrowserConnection()
-        resumeBrowser.testConnection()
-        verify(mediaController).registerCallback(mediaControllerCallback.capture())
-        reset(browser)
-
-        // And a sessionDestroyed event is triggered
-        mediaControllerCallback.value.onSessionDestroyed()
-
-        // Then we disconnect the browser and unregister the callback
-        verify(browser).disconnect()
-        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
-    }
-
-    @Test
-    fun testConnection_calledTwice_oldBrowserDisconnected() {
-        val oldBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
-
-        // When testConnection can connect to the service
-        setupBrowserConnection()
-        resumeBrowser.testConnection()
-
-        // And testConnection is called again
-        val newBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
-        resumeBrowser.testConnection()
-
-        // Then we disconnect the old browser
-        verify(oldBrowser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_connectionFails_error() {
-        // When findRecentMedia is called and we cannot connect
-        setupBrowserFailed()
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_noRoot_error() {
-        // When findRecentMedia is called and does not get a valid root
-        setupBrowserConnection()
-        whenever(browser.getRoot()).thenReturn(null)
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_connects_onConnected() {
-        // When findRecentMedia is called and we connect
-        setupBrowserConnection()
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls onConnected
-        verify(callback).onConnected()
-    }
-
-    @Test
-    fun testFindRecentMedia_thenSessionDestroyed_disconnects() {
-        // When findRecentMedia is called and we connect successfully
-        setupBrowserConnection()
-        resumeBrowser.findRecentMedia()
-        verify(mediaController).registerCallback(mediaControllerCallback.capture())
-        reset(browser)
-
-        // And a sessionDestroyed event is triggered
-        mediaControllerCallback.value.onSessionDestroyed()
-
-        // Then we disconnect the browser and unregister the callback
-        verify(browser).disconnect()
-        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
-    }
-
-    @Test
-    fun testFindRecentMedia_calledTwice_oldBrowserDisconnected() {
-        val oldBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
-
-        // When findRecentMedia is called and we connect
-        setupBrowserConnection()
-        resumeBrowser.findRecentMedia()
-
-        // And findRecentMedia is called again
-        val newBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
-        resumeBrowser.findRecentMedia()
-
-        // Then we disconnect the old browser
-        verify(oldBrowser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_noChildren_error() {
-        // When findRecentMedia is called and we connect, but do not get any results
-        setupBrowserConnectionNoResults()
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_notPlayable_error() {
-        // When findRecentMedia is called and we connect, but do not get a playable child
-        setupBrowserConnectionNotPlayable()
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testFindRecentMedia_hasValidMedia_addTrack() {
-        // When findRecentMedia is called and we can connect and get playable media
-        setupBrowserConnectionValidMedia()
-        resumeBrowser.findRecentMedia()
-
-        // Then it calls addTrack
-        verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
-    }
-
-    @Test
-    fun testRestart_connectionFails_error() {
-        // When restart is called and we cannot connect
-        setupBrowserFailed()
-        resumeBrowser.restart()
-
-        // Then it calls onError and disconnects
-        verify(callback).onError()
-        verify(browser).disconnect()
-    }
-
-    @Test
-    fun testRestart_connects() {
-        // When restart is called and we connect successfully
-        setupBrowserConnection()
-        resumeBrowser.restart()
-        verify(callback).onConnected()
-
-        // Then it creates a new controller and sends play command
-        verify(transportControls).prepare()
-        verify(transportControls).play()
-    }
-
-    @Test
-    fun testRestart_thenSessionDestroyed_disconnects() {
-        // When restart is called and we connect successfully
-        setupBrowserConnection()
-        resumeBrowser.restart()
-        verify(mediaController).registerCallback(mediaControllerCallback.capture())
-        reset(browser)
-
-        // And a sessionDestroyed event is triggered
-        mediaControllerCallback.value.onSessionDestroyed()
-
-        // Then we disconnect the browser and unregister the callback
-        verify(browser).disconnect()
-        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
-    }
-
-    @Test
-    fun testRestart_calledTwice_oldBrowserDisconnected() {
-        val oldBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
-
-        // When restart is called and we connect successfully
-        setupBrowserConnection()
-        resumeBrowser.restart()
-
-        // And restart is called again
-        val newBrowser = mock<MediaBrowser>()
-        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
-        resumeBrowser.restart()
-
-        // Then we disconnect the old browser
-        verify(oldBrowser).disconnect()
-    }
-
-    /**
-     * Helper function to mock a failed connection
-     */
-    private fun setupBrowserFailed() {
-        whenever(browser.connect()).thenAnswer {
-            connectionCallback.value.onConnectionFailed()
-        }
-    }
-
-    /**
-     * Helper function to mock a successful connection only
-     */
-    private fun setupBrowserConnection() {
-        whenever(browser.connect()).thenAnswer {
-            connectionCallback.value.onConnected()
-        }
-        whenever(browser.isConnected()).thenReturn(true)
-        whenever(browser.getRoot()).thenReturn(ROOT)
-        whenever(browser.sessionToken).thenReturn(token)
-    }
-
-    /**
-     * Helper function to mock a successful connection, but no media results
-     */
-    private fun setupBrowserConnectionNoResults() {
-        setupBrowserConnection()
-        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
-            subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
-        }
-    }
-
-    /**
-     * Helper function to mock a successful connection, but no playable results
-     */
-    private fun setupBrowserConnectionNotPlayable() {
-        setupBrowserConnection()
-
-        val child = MediaBrowser.MediaItem(description, 0)
-
-        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
-            subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
-        }
-    }
-
-    /**
-     * Helper function to mock a successful connection with playable media
-     */
-    private fun setupBrowserConnectionValidMedia() {
-        setupBrowserConnection()
-
-        val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
-
-        whenever(browser.serviceComponent).thenReturn(component)
-        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
-            subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
-        }
-    }
-
-    /**
-     * Override so media controller use is testable
-     */
-    private class TestableResumeMediaBrowser(
-        context: Context,
-        callback: Callback,
-        componentName: ComponentName,
-        browserFactory: MediaBrowserFactory,
-        logger: ResumeMediaBrowserLogger,
-        private val fakeController: MediaController
-    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger) {
-
-        override fun createMediaController(token: MediaSession.Token): MediaController {
-            return fakeController
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
deleted file mode 100644
index 9e9cda8..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.ObjectAnimator
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import android.widget.SeekBar
-import android.widget.TextView
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class SeekBarObserverTest : SysuiTestCase() {
-
-    private val disabledHeight = 1
-    private val enabledHeight = 2
-
-    private lateinit var observer: SeekBarObserver
-    @Mock private lateinit var mockSeekbarAnimator: ObjectAnimator
-    @Mock private lateinit var mockHolder: MediaViewHolder
-    @Mock private lateinit var mockSquigglyProgress: SquigglyProgress
-    private lateinit var seekBarView: SeekBar
-    private lateinit var scrubbingElapsedTimeView: TextView
-    private lateinit var scrubbingTotalTimeView: TextView
-
-    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        context.orCreateTestableResources
-            .addOverride(R.dimen.qs_media_enabled_seekbar_height, enabledHeight)
-        context.orCreateTestableResources
-            .addOverride(R.dimen.qs_media_disabled_seekbar_height, disabledHeight)
-
-        seekBarView = SeekBar(context)
-        seekBarView.progressDrawable = mockSquigglyProgress
-        scrubbingElapsedTimeView = TextView(context)
-        scrubbingTotalTimeView = TextView(context)
-        whenever(mockHolder.seekBar).thenReturn(seekBarView)
-        whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
-        whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
-
-        observer = object : SeekBarObserver(mockHolder) {
-            override fun buildResetAnimator(targetTime: Int): Animator {
-                return mockSeekbarAnimator
-            }
-        }
-    }
-
-    @Test
-    fun seekBarGone() {
-        // WHEN seek bar is disabled
-        val isEnabled = false
-        val data = SeekBarViewModel.Progress(isEnabled, false, false, false, null, 0)
-        observer.onChanged(data)
-        // THEN seek bar shows just a thin line with no text
-        assertThat(seekBarView.isEnabled()).isFalse()
-        assertThat(seekBarView.getThumb().getAlpha()).isEqualTo(0)
-        assertThat(seekBarView.contentDescription).isEqualTo("")
-        assertThat(seekBarView.maxHeight).isEqualTo(disabledHeight)
-    }
-
-    @Test
-    fun seekBarVisible() {
-        // WHEN seek bar is enabled
-        val isEnabled = true
-        val data = SeekBarViewModel.Progress(isEnabled, true, false, false, 3000, 12000)
-        observer.onChanged(data)
-        // THEN seek bar is visible and thick
-        assertThat(seekBarView.getVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(seekBarView.maxHeight).isEqualTo(enabledHeight)
-    }
-
-    @Test
-    fun seekBarProgress() {
-        // WHEN part of the track has been played
-        val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000)
-        observer.onChanged(data)
-        // THEN seek bar shows the progress
-        assertThat(seekBarView.progress).isEqualTo(3000)
-        assertThat(seekBarView.max).isEqualTo(120000)
-
-        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
-        assertThat(seekBarView.contentDescription).isEqualTo(desc)
-    }
-
-    @Test
-    fun seekBarDisabledWhenSeekNotAvailable() {
-        // WHEN seek is not available
-        val isSeekAvailable = false
-        val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
-        observer.onChanged(data)
-        // THEN seek bar is not enabled
-        assertThat(seekBarView.isEnabled()).isFalse()
-    }
-
-    @Test
-    fun seekBarEnabledWhenSeekNotAvailable() {
-        // WHEN seek is available
-        val isSeekAvailable = true
-        val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
-        observer.onChanged(data)
-        // THEN seek bar is not enabled
-        assertThat(seekBarView.isEnabled()).isTrue()
-    }
-
-    @Test
-    fun seekBarPlayingNotScrubbing() {
-        // WHEN playing
-        val isPlaying = true
-        val isScrubbing = false
-        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
-        observer.onChanged(data)
-        // THEN progress drawable is animating
-        verify(mockSquigglyProgress).animate = true
-    }
-
-    @Test
-    fun seekBarNotPlayingNotScrubbing() {
-        // WHEN not playing & not scrubbing
-        val isPlaying = false
-        val isScrubbing = false
-        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
-        observer.onChanged(data)
-        // THEN progress drawable is not animating
-        verify(mockSquigglyProgress).animate = false
-    }
-
-    @Test
-    fun seekBarPlayingScrubbing() {
-        // WHEN playing & scrubbing
-        val isPlaying = true
-        val isScrubbing = true
-        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
-        observer.onChanged(data)
-        // THEN progress drawable is not animating
-        verify(mockSquigglyProgress).animate = false
-    }
-
-    @Test
-    fun seekBarNotPlayingScrubbing() {
-        // WHEN playing & scrubbing
-        val isPlaying = false
-        val isScrubbing = true
-        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
-        observer.onChanged(data)
-        // THEN progress drawable is not animating
-        verify(mockSquigglyProgress).animate = false
-    }
-
-    @Test
-    fun seekBarProgress_enabledAndScrubbing_timeViewsHaveTime() {
-        val isEnabled = true
-        val isScrubbing = true
-        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
-
-        observer.onChanged(data)
-
-        assertThat(scrubbingElapsedTimeView.text).isEqualTo("00:03")
-        assertThat(scrubbingTotalTimeView.text).isEqualTo("02:00")
-    }
-
-    @Test
-    fun seekBarProgress_disabledAndScrubbing_timeViewsEmpty() {
-        val isEnabled = false
-        val isScrubbing = true
-        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
-
-        observer.onChanged(data)
-
-        assertThat(scrubbingElapsedTimeView.text).isEqualTo("")
-        assertThat(scrubbingTotalTimeView.text).isEqualTo("")
-    }
-
-    @Test
-    fun seekBarProgress_enabledAndNotScrubbing_timeViewsEmpty() {
-        val isEnabled = true
-        val isScrubbing = false
-        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
-
-        observer.onChanged(data)
-
-        assertThat(scrubbingElapsedTimeView.text).isEqualTo("")
-        assertThat(scrubbingTotalTimeView.text).isEqualTo("")
-    }
-
-    @Test
-    fun seekBarJumpAnimation() {
-        val data0 = SeekBarViewModel.Progress(true, true, true, false, 4000, 120000)
-        val data1 = SeekBarViewModel.Progress(true, true, true, false, 10, 120000)
-
-        // Set initial position of progress bar
-        observer.onChanged(data0)
-        assertThat(seekBarView.progress).isEqualTo(4000)
-        assertThat(seekBarView.max).isEqualTo(120000)
-
-        // Change to second data & confirm no change to position (due to animation delay)
-        observer.onChanged(data1)
-        assertThat(seekBarView.progress).isEqualTo(4000)
-        verify(mockSeekbarAnimator).start()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
deleted file mode 100644
index 5973340..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ /dev/null
@@ -1,748 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.MediaMetadata
-import android.media.session.MediaController
-import android.media.session.MediaSession
-import android.media.session.PlaybackState
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.widget.SeekBar
-import androidx.arch.core.executor.ArchTaskExecutor
-import androidx.arch.core.executor.TaskExecutor
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.Classifier
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.concurrency.FakeRepeatableExecutor
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class SeekBarViewModelTest : SysuiTestCase() {
-
-    private lateinit var viewModel: SeekBarViewModel
-    private lateinit var fakeExecutor: FakeExecutor
-    private val taskExecutor: TaskExecutor = object : TaskExecutor() {
-        override fun executeOnDiskIO(runnable: Runnable) {
-            runnable.run()
-        }
-        override fun postToMainThread(runnable: Runnable) {
-            runnable.run()
-        }
-        override fun isMainThread(): Boolean {
-            return true
-        }
-    }
-    @Mock private lateinit var mockController: MediaController
-    @Mock private lateinit var mockTransport: MediaController.TransportControls
-    @Mock private lateinit var falsingManager: FalsingManager
-    @Mock private lateinit var mockBar: SeekBar
-    private val token1 = MediaSession.Token(1, null)
-    private val token2 = MediaSession.Token(2, null)
-
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    @Before
-    fun setUp() {
-        fakeExecutor = FakeExecutor(FakeSystemClock())
-        viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
-        viewModel.logSeek = { }
-        whenever(mockController.sessionToken).thenReturn(token1)
-        whenever(mockBar.context).thenReturn(context)
-
-        // LiveData to run synchronously
-        ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
-    }
-
-    @After
-    fun tearDown() {
-        ArchTaskExecutor.getInstance().setDelegate(null)
-    }
-
-    @Test
-    fun updateRegistersCallback() {
-        viewModel.updateController(mockController)
-        verify(mockController).registerCallback(any())
-    }
-
-    @Test
-    fun updateSecondTimeDoesNotRepeatRegistration() {
-        viewModel.updateController(mockController)
-        viewModel.updateController(mockController)
-        verify(mockController, times(1)).registerCallback(any())
-    }
-
-    @Test
-    fun updateDifferentControllerUnregistersCallback() {
-        viewModel.updateController(mockController)
-        viewModel.updateController(mock(MediaController::class.java))
-        verify(mockController).unregisterCallback(any())
-    }
-
-    @Test
-    fun updateDifferentControllerRegistersCallback() {
-        viewModel.updateController(mockController)
-        val controller2 = mock(MediaController::class.java)
-        whenever(controller2.sessionToken).thenReturn(token2)
-        viewModel.updateController(controller2)
-        verify(controller2).registerCallback(any())
-    }
-
-    @Test
-    fun updateToNullUnregistersCallback() {
-        viewModel.updateController(mockController)
-        viewModel.updateController(null)
-        verify(mockController).unregisterCallback(any())
-    }
-
-    @Test
-    @Ignore
-    fun updateDurationWithPlayback() {
-        // GIVEN that the duration is contained within the metadata
-        val duration = 12000L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
-        whenever(mockController.getMetadata()).thenReturn(metadata)
-        // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN the duration is extracted
-        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
-        assertThat(viewModel.progress.value!!.enabled).isTrue()
-    }
-
-    @Test
-    @Ignore
-    fun updateDurationWithoutPlayback() {
-        // GIVEN that the duration is contained within the metadata
-        val duration = 12000L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
-        whenever(mockController.getMetadata()).thenReturn(metadata)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN the duration is extracted
-        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
-        assertThat(viewModel.progress.value!!.enabled).isFalse()
-    }
-
-    @Test
-    fun updateDurationNegative() {
-        // GIVEN that the duration is negative
-        val duration = -1L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
-        whenever(mockController.getMetadata()).thenReturn(metadata)
-        // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN the seek bar is disabled
-        assertThat(viewModel.progress.value!!.enabled).isFalse()
-    }
-
-    @Test
-    fun updateDurationZero() {
-        // GIVEN that the duration is zero
-        val duration = 0L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
-        whenever(mockController.getMetadata()).thenReturn(metadata)
-        // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN the seek bar is disabled
-        assertThat(viewModel.progress.value!!.enabled).isFalse()
-    }
-
-    @Test
-    @Ignore
-    fun updateDurationNoMetadata() {
-        // GIVEN that the metadata is null
-        whenever(mockController.getMetadata()).thenReturn(null)
-        // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN the seek bar is disabled
-        assertThat(viewModel.progress.value!!.enabled).isFalse()
-    }
-
-    @Test
-    fun updateElapsedTime() {
-        // GIVEN that the PlaybackState contains the current position
-        val position = 200L
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, position, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN elapsed time is captured
-        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(200.toInt())
-    }
-
-    @Test
-    @Ignore
-    fun updateSeekAvailable() {
-        // GIVEN that seek is included in actions
-        val state = PlaybackState.Builder().run {
-            setActions(PlaybackState.ACTION_SEEK_TO)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN seek is available
-        assertThat(viewModel.progress.value!!.seekAvailable).isTrue()
-    }
-
-    @Test
-    @Ignore
-    fun updateSeekNotAvailable() {
-        // GIVEN that seek is not included in actions
-        val state = PlaybackState.Builder().run {
-            setActions(PlaybackState.ACTION_PLAY)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN seek is not available
-        assertThat(viewModel.progress.value!!.seekAvailable).isFalse()
-    }
-
-    @Test
-    fun onSeek() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN user input is dispatched
-        val pos = 42L
-        viewModel.onSeek(pos)
-        fakeExecutor.runAllReady()
-        // THEN transport controls should be used
-        verify(mockTransport).seekTo(pos)
-    }
-
-    @Test
-    fun onSeekWithFalse() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN a false is received during the seek gesture
-        val pos = 42L
-        with(viewModel) {
-            onSeekStarting()
-            onSeekFalse()
-            onSeek(pos)
-        }
-        fakeExecutor.runAllReady()
-        // THEN the seek is rejected and the transport never receives seekTo
-        verify(mockTransport, never()).seekTo(pos)
-    }
-
-    @Test
-    fun onSeekProgress() {
-        val pos = 42L
-        with(viewModel) {
-            onSeekStarting()
-            onSeekProgress(pos)
-        }
-        fakeExecutor.runAllReady()
-        // THEN then elapsed time should be updated
-        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
-    }
-
-    @Test
-    @Ignore
-    fun onSeekProgressWithSeekStarting() {
-        val pos = 42L
-        with(viewModel) {
-            onSeekProgress(pos)
-        }
-        fakeExecutor.runAllReady()
-        // THEN then elapsed time should not be updated
-        assertThat(viewModel.progress.value!!.elapsedTime).isNull()
-    }
-
-    @Test
-    fun seekStarted_listenerNotified() {
-        var isScrubbing: Boolean? = null
-        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
-            override fun onScrubbingChanged(scrubbing: Boolean) {
-                isScrubbing = scrubbing
-            }
-        }
-        viewModel.setScrubbingChangeListener(listener)
-
-        viewModel.onSeekStarting()
-        fakeExecutor.runAllReady()
-
-        assertThat(isScrubbing).isTrue()
-    }
-
-    @Test
-    fun seekEnded_listenerNotified() {
-        var isScrubbing: Boolean? = null
-        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
-            override fun onScrubbingChanged(scrubbing: Boolean) {
-                isScrubbing = scrubbing
-            }
-        }
-        viewModel.setScrubbingChangeListener(listener)
-
-        // Start seeking
-        viewModel.onSeekStarting()
-        fakeExecutor.runAllReady()
-        // End seeking
-        viewModel.onSeek(15L)
-        fakeExecutor.runAllReady()
-
-        assertThat(isScrubbing).isFalse()
-    }
-
-    @Test
-    @Ignore
-    fun onProgressChangedFromUser() {
-        // WHEN user starts dragging the seek bar
-        val pos = 42
-        val bar = SeekBar(context)
-        with(viewModel.seekBarListener) {
-            onStartTrackingTouch(bar)
-            onProgressChanged(bar, pos, true)
-        }
-        fakeExecutor.runAllReady()
-        // THEN then elapsed time should be updated
-        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
-    }
-
-    @Test
-    fun onProgressChangedFromUserWithoutStartTrackingTouch_transportUpdated() {
-        whenever(mockController.transportControls).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        val pos = 42
-        val bar = SeekBar(context)
-
-        // WHEN we get an onProgressChanged event without an onStartTrackingTouch event
-        with(viewModel.seekBarListener) {
-            onProgressChanged(bar, pos, true)
-        }
-        fakeExecutor.runAllReady()
-
-        // THEN we immediately update the transport
-        verify(mockTransport).seekTo(pos.toLong())
-    }
-
-    @Test
-    fun onProgressChangedNotFromUser() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN user starts dragging the seek bar
-        val pos = 42
-        viewModel.seekBarListener.onProgressChanged(SeekBar(context), pos, false)
-        fakeExecutor.runAllReady()
-        // THEN transport controls should be used
-        verify(mockTransport, never()).seekTo(pos.toLong())
-    }
-
-    @Test
-    fun onStartTrackingTouch() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN user starts dragging the seek bar
-        val pos = 42
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
-        viewModel.seekBarListener.onStartTrackingTouch(bar)
-        fakeExecutor.runAllReady()
-        // THEN transport controls should be used
-        verify(mockTransport, never()).seekTo(pos.toLong())
-    }
-
-    @Test
-    fun onStopTrackingTouch() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN user ends drag
-        val pos = 42
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
-        viewModel.seekBarListener.onStopTrackingTouch(bar)
-        fakeExecutor.runAllReady()
-        // THEN transport controls should be used
-        verify(mockTransport).seekTo(pos.toLong())
-    }
-
-    @Test
-    fun onStopTrackingTouchAfterProgress() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        viewModel.updateController(mockController)
-        // WHEN user starts dragging the seek bar
-        val pos = 42
-        val progPos = 84
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
-        with(viewModel.seekBarListener) {
-            onStartTrackingTouch(bar)
-            onProgressChanged(bar, progPos, true)
-            onStopTrackingTouch(bar)
-        }
-        fakeExecutor.runAllReady()
-        // THEN then elapsed time should be updated
-        verify(mockTransport).seekTo(eq(pos.toLong()))
-    }
-
-    @Test
-    fun onFalseTapOrTouch() {
-        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
-        whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true)
-        whenever(falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
-        viewModel.updateController(mockController)
-        val pos = 169
-
-        viewModel.attachTouchHandlers(mockBar)
-        with(viewModel.seekBarListener) {
-            onStartTrackingTouch(mockBar)
-            onProgressChanged(mockBar, pos, true)
-            onStopTrackingTouch(mockBar)
-        }
-
-        // THEN transport controls should not be used
-        verify(mockTransport, never()).seekTo(pos.toLong())
-    }
-
-    @Test
-    fun queuePollTaskWhenPlaying() {
-        // GIVEN that the track is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN the controller is updated
-        viewModel.updateController(mockController)
-        // THEN a task is queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun noQueuePollTaskWhenStopped() {
-        // GIVEN that the playback state is stopped
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_STOPPED, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN updated
-        viewModel.updateController(mockController)
-        // THEN an update task is not queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun queuePollTaskWhenListening() {
-        // GIVEN listening
-        viewModel.listening = true
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN updated
-        viewModel.updateController(mockController)
-        // THEN an update task is queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun noQueuePollTaskWhenNotListening() {
-        // GIVEN not listening
-        viewModel.listening = false
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // WHEN updated
-        viewModel.updateController(mockController)
-        // THEN an update task is not queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
-        // GIVEN that the track is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        viewModel.updateController(mockController)
-        // WHEN the next task runs
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN another task is queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun noQueuePollTaskWhenSeeking() {
-        // GIVEN listening
-        viewModel.listening = true
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        viewModel.updateController(mockController)
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // WHEN seek starts
-        viewModel.onSeekStarting()
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN an update task is not queued because we don't want it fighting with the user when
-        // they are trying to move the thumb.
-        assertThat(fakeExecutor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun queuePollTaskWhenDoneSeekingWithFalse() {
-        // GIVEN listening
-        viewModel.listening = true
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        viewModel.updateController(mockController)
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // WHEN seek finishes after a false
-        with(viewModel) {
-            onSeekStarting()
-            onSeekFalse()
-            onSeek(42L)
-        }
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN an update task is queued because the gesture was ignored and progress was restored.
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun noQueuePollTaskWhenDoneSeeking() {
-        // GIVEN listening
-        viewModel.listening = true
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        viewModel.updateController(mockController)
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // WHEN seek finishes after a false
-        with(viewModel) {
-            onSeekStarting()
-            onSeek(42L)
-        }
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN no update task is queued because we are waiting for an updated playback state to be
-        // returned in response to the seek.
-        assertThat(fakeExecutor.numPending()).isEqualTo(0)
-    }
-
-    @Test
-    fun startListeningQueuesPollTask() {
-        // GIVEN not listening
-        viewModel.listening = false
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_STOPPED, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        viewModel.updateController(mockController)
-        // WHEN start listening
-        viewModel.listening = true
-        // THEN an update task is queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    fun playbackChangeQueuesPollTask() {
-        viewModel.updateController(mockController)
-        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
-        verify(mockController).registerCallback(captor.capture())
-        val callback = captor.value
-        // WHEN the callback receives an new state
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
-        callback.onPlaybackStateChanged(state)
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN an update task is queued
-        assertThat(fakeExecutor.numPending()).isEqualTo(1)
-    }
-
-    @Test
-    @Ignore
-    fun clearSeekBar() {
-        // GIVEN that the duration is contained within the metadata
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
-            build()
-        }
-        whenever(mockController.getMetadata()).thenReturn(metadata)
-        // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
-        whenever(mockController.getPlaybackState()).thenReturn(state)
-        // AND the controller has been updated
-        viewModel.updateController(mockController)
-        // WHEN the controller is cleared on the event when the session is destroyed
-        viewModel.clearController()
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN the seek bar is disabled
-        assertThat(viewModel.progress.value!!.enabled).isFalse()
-    }
-
-    @Test
-    fun clearSeekBarUnregistersCallback() {
-        viewModel.updateController(mockController)
-        viewModel.clearController()
-        fakeExecutor.runAllReady()
-        verify(mockController).unregisterCallback(any())
-    }
-
-    @Test
-    fun destroyUnregistersCallback() {
-        viewModel.updateController(mockController)
-        viewModel.onDestroy()
-        fakeExecutor.runAllReady()
-        verify(mockController).unregisterCallback(any())
-    }
-
-    @Test
-    fun nullPlaybackStateUnregistersCallback() {
-        viewModel.updateController(mockController)
-        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
-        verify(mockController).registerCallback(captor.capture())
-        val callback = captor.value
-        // WHEN the callback receives a null state
-        callback.onPlaybackStateChanged(null)
-        with(fakeExecutor) {
-            advanceClockToNext()
-            runAllReady()
-        }
-        // THEN we unregister callback (as a result of clearing the controller)
-        fakeExecutor.runAllReady()
-        verify(mockController).unregisterCallback(any())
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
deleted file mode 100644
index b5078bc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.android.systemui.media
-
-import android.app.smartspace.SmartspaceAction
-import android.graphics.drawable.Icon
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-@SmallTest
-class SmartspaceMediaDataTest : SysuiTestCase() {
-
-    private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play)
-
-    @Test
-    fun getValidRecommendations_onlyReturnsRecsWithIcons() {
-        val withIcon1 = SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-        val withIcon2 = SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-        val withoutIcon1 = SmartspaceAction.Builder("id", "title").setIcon(null).build()
-        val withoutIcon2 = SmartspaceAction.Builder("id", "title").setIcon(null).build()
-        val recommendations = listOf(withIcon1, withoutIcon1, withIcon2, withoutIcon2)
-
-        val data = DEFAULT_DATA.copy(recommendations = recommendations)
-
-        assertThat(data.getValidRecommendations()).isEqualTo(listOf(withIcon1, withIcon2))
-    }
-
-    @Test
-    fun isValid_emptyList_returnsFalse() {
-        val data = DEFAULT_DATA.copy(recommendations = listOf())
-
-        assertThat(data.isValid()).isFalse()
-    }
-
-    @Test
-    fun isValid_tooFewRecs_returnsFalse() {
-        val data = DEFAULT_DATA.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
-        )
-
-        assertThat(data.isValid()).isFalse()
-    }
-
-    @Test
-    fun isValid_tooFewRecsWithIcons_returnsFalse() {
-        val recommendations = mutableListOf<SmartspaceAction>()
-        // Add one fewer recommendation w/ icon than the number required
-        for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
-        }
-        for (i in 1 until 3) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(null).build()
-            )
-        }
-
-        val data = DEFAULT_DATA.copy(recommendations = recommendations)
-
-        assertThat(data.isValid()).isFalse()
-    }
-
-    @Test
-    fun isValid_enoughRecsWithIcons_returnsTrue() {
-        val recommendations = mutableListOf<SmartspaceAction>()
-        // Add the number of required recommendations
-        for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
-        }
-
-        val data = DEFAULT_DATA.copy(recommendations = recommendations)
-
-        assertThat(data.isValid()).isTrue()
-    }
-
-    @Test
-    fun isValid_manyRecsWithIcons_returnsTrue() {
-        val recommendations = mutableListOf<SmartspaceAction>()
-        // Add more than enough recommendations
-        for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
-        }
-
-        val data = DEFAULT_DATA.copy(recommendations = recommendations)
-
-        assertThat(data.isValid()).isTrue()
-    }
-}
-
-private val DEFAULT_DATA = SmartspaceMediaData(
-    targetId = "INVALID",
-    isActive = false,
-    packageName = "INVALID",
-    cardAction = null,
-    recommendations = emptyList(),
-    dismissIntent = null,
-    headphoneConnectionTimeMillis = 0,
-    instanceId = InstanceId.fakeInstanceId(-1)
-)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
deleted file mode 100644
index d087b0f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package com.android.systemui.media
-
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.LightingColorFilter
-import android.graphics.Paint
-import android.graphics.Rect
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.graphics.ColorUtils
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.mockito.any
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class SquigglyProgressTest : SysuiTestCase() {
-
-    private val colorFilter = LightingColorFilter(Color.RED, Color.BLUE)
-    private val strokeWidth = 5f
-    private val alpha = 128
-    private val tint = Color.GREEN
-
-    lateinit var squigglyProgress: SquigglyProgress
-    @Mock lateinit var canvas: Canvas
-    @Captor lateinit var paintCaptor: ArgumentCaptor<Paint>
-    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
-
-    @Before
-    fun setup() {
-        squigglyProgress = SquigglyProgress()
-        squigglyProgress.waveLength = 30f
-        squigglyProgress.lineAmplitude = 10f
-        squigglyProgress.phaseSpeed = 8f
-        squigglyProgress.strokeWidth = strokeWidth
-        squigglyProgress.bounds = Rect(0, 0, 300, 30)
-    }
-
-    @Test
-    fun testDrawPathAndLine() {
-        squigglyProgress.draw(canvas)
-
-        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
-    }
-
-    @Test
-    fun testOnLevelChanged() {
-        assertThat(squigglyProgress.setLevel(5)).isFalse()
-        squigglyProgress.animate = true
-        assertThat(squigglyProgress.setLevel(4)).isTrue()
-    }
-
-    @Test
-    fun testStrokeWidth() {
-        squigglyProgress.draw(canvas)
-
-        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
-        val (wavePaint, linePaint) = paintCaptor.getAllValues()
-
-        assertThat(wavePaint.strokeWidth).isEqualTo(strokeWidth)
-        assertThat(linePaint.strokeWidth).isEqualTo(strokeWidth)
-    }
-
-    @Test
-    fun testAlpha() {
-        squigglyProgress.alpha = alpha
-        squigglyProgress.draw(canvas)
-
-        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
-        val (wavePaint, linePaint) = paintCaptor.getAllValues()
-
-        assertThat(squigglyProgress.alpha).isEqualTo(alpha)
-        assertThat(wavePaint.alpha).isEqualTo(alpha)
-        assertThat(linePaint.alpha).isEqualTo((alpha / 255f * DISABLED_ALPHA).toInt())
-    }
-
-    @Test
-    fun testColorFilter() {
-        squigglyProgress.colorFilter = colorFilter
-        squigglyProgress.draw(canvas)
-
-        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
-        val (wavePaint, linePaint) = paintCaptor.getAllValues()
-
-        assertThat(wavePaint.colorFilter).isEqualTo(colorFilter)
-        assertThat(linePaint.colorFilter).isEqualTo(colorFilter)
-    }
-
-    @Test
-    fun testTint() {
-        squigglyProgress.setTint(tint)
-        squigglyProgress.draw(canvas)
-
-        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
-        val (wavePaint, linePaint) = paintCaptor.getAllValues()
-
-        assertThat(wavePaint.color).isEqualTo(tint)
-        assertThat(linePaint.color).isEqualTo(
-                ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt
new file mode 100644
index 0000000..3437365
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.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.media.controls
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.media.controls.models.player.MediaData
+
+class MediaTestUtils {
+    companion object {
+        val emptyMediaData =
+            MediaData(
+                userId = 0,
+                initialized = true,
+                app = null,
+                appIcon = null,
+                artist = null,
+                song = null,
+                artwork = null,
+                actions = emptyList(),
+                actionsToShowInCompact = emptyList(),
+                packageName = "",
+                token = null,
+                clickIntent = null,
+                device = null,
+                active = true,
+                resumeAction = null,
+                isPlaying = false,
+                instanceId = InstanceId.fakeInstanceId(-1),
+                appUid = -1
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt
new file mode 100644
index 0000000..c829d4c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.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.media.controls.models.player
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaViewHolderTest : SysuiTestCase() {
+
+    @Test
+    fun create_succeeds() {
+        val inflater = LayoutInflater.from(context)
+        val parent = FrameLayout(context)
+
+        MediaViewHolder.create(inflater, parent)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
new file mode 100644
index 0000000..97b18e2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.ui.SquigglyProgress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SeekBarObserverTest : SysuiTestCase() {
+
+    private val disabledHeight = 1
+    private val enabledHeight = 2
+
+    private lateinit var observer: SeekBarObserver
+    @Mock private lateinit var mockSeekbarAnimator: ObjectAnimator
+    @Mock private lateinit var mockHolder: MediaViewHolder
+    @Mock private lateinit var mockSquigglyProgress: SquigglyProgress
+    private lateinit var seekBarView: SeekBar
+    private lateinit var scrubbingElapsedTimeView: TextView
+    private lateinit var scrubbingTotalTimeView: TextView
+
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        context.orCreateTestableResources.addOverride(
+            R.dimen.qs_media_enabled_seekbar_height,
+            enabledHeight
+        )
+        context.orCreateTestableResources.addOverride(
+            R.dimen.qs_media_disabled_seekbar_height,
+            disabledHeight
+        )
+
+        seekBarView = SeekBar(context)
+        seekBarView.progressDrawable = mockSquigglyProgress
+        scrubbingElapsedTimeView = TextView(context)
+        scrubbingTotalTimeView = TextView(context)
+        whenever(mockHolder.seekBar).thenReturn(seekBarView)
+        whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
+        whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
+
+        observer =
+            object : SeekBarObserver(mockHolder) {
+                override fun buildResetAnimator(targetTime: Int): Animator {
+                    return mockSeekbarAnimator
+                }
+            }
+    }
+
+    @Test
+    fun seekBarGone() {
+        // WHEN seek bar is disabled
+        val isEnabled = false
+        val data = SeekBarViewModel.Progress(isEnabled, false, false, false, null, 0)
+        observer.onChanged(data)
+        // THEN seek bar shows just a thin line with no text
+        assertThat(seekBarView.isEnabled()).isFalse()
+        assertThat(seekBarView.getThumb().getAlpha()).isEqualTo(0)
+        assertThat(seekBarView.contentDescription).isEqualTo("")
+        assertThat(seekBarView.maxHeight).isEqualTo(disabledHeight)
+    }
+
+    @Test
+    fun seekBarVisible() {
+        // WHEN seek bar is enabled
+        val isEnabled = true
+        val data = SeekBarViewModel.Progress(isEnabled, true, false, false, 3000, 12000)
+        observer.onChanged(data)
+        // THEN seek bar is visible and thick
+        assertThat(seekBarView.getVisibility()).isEqualTo(View.VISIBLE)
+        assertThat(seekBarView.maxHeight).isEqualTo(enabledHeight)
+    }
+
+    @Test
+    fun seekBarProgress() {
+        // WHEN part of the track has been played
+        val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000)
+        observer.onChanged(data)
+        // THEN seek bar shows the progress
+        assertThat(seekBarView.progress).isEqualTo(3000)
+        assertThat(seekBarView.max).isEqualTo(120000)
+
+        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
+        assertThat(seekBarView.contentDescription).isEqualTo(desc)
+    }
+
+    @Test
+    fun seekBarDisabledWhenSeekNotAvailable() {
+        // WHEN seek is not available
+        val isSeekAvailable = false
+        val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
+        observer.onChanged(data)
+        // THEN seek bar is not enabled
+        assertThat(seekBarView.isEnabled()).isFalse()
+    }
+
+    @Test
+    fun seekBarEnabledWhenSeekNotAvailable() {
+        // WHEN seek is available
+        val isSeekAvailable = true
+        val data = SeekBarViewModel.Progress(true, isSeekAvailable, false, false, 3000, 120000)
+        observer.onChanged(data)
+        // THEN seek bar is not enabled
+        assertThat(seekBarView.isEnabled()).isTrue()
+    }
+
+    @Test
+    fun seekBarPlayingNotScrubbing() {
+        // WHEN playing
+        val isPlaying = true
+        val isScrubbing = false
+        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+        observer.onChanged(data)
+        // THEN progress drawable is animating
+        verify(mockSquigglyProgress).animate = true
+    }
+
+    @Test
+    fun seekBarNotPlayingNotScrubbing() {
+        // WHEN not playing & not scrubbing
+        val isPlaying = false
+        val isScrubbing = false
+        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+        observer.onChanged(data)
+        // THEN progress drawable is not animating
+        verify(mockSquigglyProgress).animate = false
+    }
+
+    @Test
+    fun seekBarPlayingScrubbing() {
+        // WHEN playing & scrubbing
+        val isPlaying = true
+        val isScrubbing = true
+        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+        observer.onChanged(data)
+        // THEN progress drawable is not animating
+        verify(mockSquigglyProgress).animate = false
+    }
+
+    @Test
+    fun seekBarNotPlayingScrubbing() {
+        // WHEN playing & scrubbing
+        val isPlaying = false
+        val isScrubbing = true
+        val data = SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000)
+        observer.onChanged(data)
+        // THEN progress drawable is not animating
+        verify(mockSquigglyProgress).animate = false
+    }
+
+    @Test
+    fun seekBarProgress_enabledAndScrubbing_timeViewsHaveTime() {
+        val isEnabled = true
+        val isScrubbing = true
+        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+
+        observer.onChanged(data)
+
+        assertThat(scrubbingElapsedTimeView.text).isEqualTo("00:03")
+        assertThat(scrubbingTotalTimeView.text).isEqualTo("02:00")
+    }
+
+    @Test
+    fun seekBarProgress_disabledAndScrubbing_timeViewsEmpty() {
+        val isEnabled = false
+        val isScrubbing = true
+        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+
+        observer.onChanged(data)
+
+        assertThat(scrubbingElapsedTimeView.text).isEqualTo("")
+        assertThat(scrubbingTotalTimeView.text).isEqualTo("")
+    }
+
+    @Test
+    fun seekBarProgress_enabledAndNotScrubbing_timeViewsEmpty() {
+        val isEnabled = true
+        val isScrubbing = false
+        val data = SeekBarViewModel.Progress(isEnabled, true, true, isScrubbing, 3000, 120000)
+
+        observer.onChanged(data)
+
+        assertThat(scrubbingElapsedTimeView.text).isEqualTo("")
+        assertThat(scrubbingTotalTimeView.text).isEqualTo("")
+    }
+
+    @Test
+    fun seekBarJumpAnimation() {
+        val data0 = SeekBarViewModel.Progress(true, true, true, false, 4000, 120000)
+        val data1 = SeekBarViewModel.Progress(true, true, true, false, 10, 120000)
+
+        // Set initial position of progress bar
+        observer.onChanged(data0)
+        assertThat(seekBarView.progress).isEqualTo(4000)
+        assertThat(seekBarView.max).isEqualTo(120000)
+
+        // Change to second data & confirm no change to position (due to animation delay)
+        observer.onChanged(data1)
+        assertThat(seekBarView.progress).isEqualTo(4000)
+        verify(mockSeekbarAnimator).start()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
new file mode 100644
index 0000000..56c91bc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
@@ -0,0 +1,765 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.SeekBar
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.Classifier
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.concurrency.FakeRepeatableExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class SeekBarViewModelTest : SysuiTestCase() {
+
+    private lateinit var viewModel: SeekBarViewModel
+    private lateinit var fakeExecutor: FakeExecutor
+    private val taskExecutor: TaskExecutor =
+        object : TaskExecutor() {
+            override fun executeOnDiskIO(runnable: Runnable) {
+                runnable.run()
+            }
+            override fun postToMainThread(runnable: Runnable) {
+                runnable.run()
+            }
+            override fun isMainThread(): Boolean {
+                return true
+            }
+        }
+    @Mock private lateinit var mockController: MediaController
+    @Mock private lateinit var mockTransport: MediaController.TransportControls
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var mockBar: SeekBar
+    private val token1 = MediaSession.Token(1, null)
+    private val token2 = MediaSession.Token(2, null)
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        fakeExecutor = FakeExecutor(FakeSystemClock())
+        viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
+        viewModel.logSeek = {}
+        whenever(mockController.sessionToken).thenReturn(token1)
+        whenever(mockBar.context).thenReturn(context)
+
+        // LiveData to run synchronously
+        ArchTaskExecutor.getInstance().setDelegate(taskExecutor)
+    }
+
+    @After
+    fun tearDown() {
+        ArchTaskExecutor.getInstance().setDelegate(null)
+    }
+
+    @Test
+    fun updateRegistersCallback() {
+        viewModel.updateController(mockController)
+        verify(mockController).registerCallback(any())
+    }
+
+    @Test
+    fun updateSecondTimeDoesNotRepeatRegistration() {
+        viewModel.updateController(mockController)
+        viewModel.updateController(mockController)
+        verify(mockController, times(1)).registerCallback(any())
+    }
+
+    @Test
+    fun updateDifferentControllerUnregistersCallback() {
+        viewModel.updateController(mockController)
+        viewModel.updateController(mock(MediaController::class.java))
+        verify(mockController).unregisterCallback(any())
+    }
+
+    @Test
+    fun updateDifferentControllerRegistersCallback() {
+        viewModel.updateController(mockController)
+        val controller2 = mock(MediaController::class.java)
+        whenever(controller2.sessionToken).thenReturn(token2)
+        viewModel.updateController(controller2)
+        verify(controller2).registerCallback(any())
+    }
+
+    @Test
+    fun updateToNullUnregistersCallback() {
+        viewModel.updateController(mockController)
+        viewModel.updateController(null)
+        verify(mockController).unregisterCallback(any())
+    }
+
+    @Test
+    @Ignore
+    fun updateDurationWithPlayback() {
+        // GIVEN that the duration is contained within the metadata
+        val duration = 12000L
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
+        whenever(mockController.getMetadata()).thenReturn(metadata)
+        // AND a valid playback state (ie. media session is not destroyed)
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN the duration is extracted
+        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
+        assertThat(viewModel.progress.value!!.enabled).isTrue()
+    }
+
+    @Test
+    @Ignore
+    fun updateDurationWithoutPlayback() {
+        // GIVEN that the duration is contained within the metadata
+        val duration = 12000L
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
+        whenever(mockController.getMetadata()).thenReturn(metadata)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN the duration is extracted
+        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
+        assertThat(viewModel.progress.value!!.enabled).isFalse()
+    }
+
+    @Test
+    fun updateDurationNegative() {
+        // GIVEN that the duration is negative
+        val duration = -1L
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
+        whenever(mockController.getMetadata()).thenReturn(metadata)
+        // AND a valid playback state (ie. media session is not destroyed)
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN the seek bar is disabled
+        assertThat(viewModel.progress.value!!.enabled).isFalse()
+    }
+
+    @Test
+    fun updateDurationZero() {
+        // GIVEN that the duration is zero
+        val duration = 0L
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
+        whenever(mockController.getMetadata()).thenReturn(metadata)
+        // AND a valid playback state (ie. media session is not destroyed)
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN the seek bar is disabled
+        assertThat(viewModel.progress.value!!.enabled).isFalse()
+    }
+
+    @Test
+    @Ignore
+    fun updateDurationNoMetadata() {
+        // GIVEN that the metadata is null
+        whenever(mockController.getMetadata()).thenReturn(null)
+        // AND a valid playback state (ie. media session is not destroyed)
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN the seek bar is disabled
+        assertThat(viewModel.progress.value!!.enabled).isFalse()
+    }
+
+    @Test
+    fun updateElapsedTime() {
+        // GIVEN that the PlaybackState contains the current position
+        val position = 200L
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, position, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN elapsed time is captured
+        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(200.toInt())
+    }
+
+    @Test
+    @Ignore
+    fun updateSeekAvailable() {
+        // GIVEN that seek is included in actions
+        val state =
+            PlaybackState.Builder().run {
+                setActions(PlaybackState.ACTION_SEEK_TO)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN seek is available
+        assertThat(viewModel.progress.value!!.seekAvailable).isTrue()
+    }
+
+    @Test
+    @Ignore
+    fun updateSeekNotAvailable() {
+        // GIVEN that seek is not included in actions
+        val state =
+            PlaybackState.Builder().run {
+                setActions(PlaybackState.ACTION_PLAY)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN seek is not available
+        assertThat(viewModel.progress.value!!.seekAvailable).isFalse()
+    }
+
+    @Test
+    fun onSeek() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN user input is dispatched
+        val pos = 42L
+        viewModel.onSeek(pos)
+        fakeExecutor.runAllReady()
+        // THEN transport controls should be used
+        verify(mockTransport).seekTo(pos)
+    }
+
+    @Test
+    fun onSeekWithFalse() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN a false is received during the seek gesture
+        val pos = 42L
+        with(viewModel) {
+            onSeekStarting()
+            onSeekFalse()
+            onSeek(pos)
+        }
+        fakeExecutor.runAllReady()
+        // THEN the seek is rejected and the transport never receives seekTo
+        verify(mockTransport, never()).seekTo(pos)
+    }
+
+    @Test
+    fun onSeekProgress() {
+        val pos = 42L
+        with(viewModel) {
+            onSeekStarting()
+            onSeekProgress(pos)
+        }
+        fakeExecutor.runAllReady()
+        // THEN then elapsed time should be updated
+        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
+    }
+
+    @Test
+    @Ignore
+    fun onSeekProgressWithSeekStarting() {
+        val pos = 42L
+        with(viewModel) { onSeekProgress(pos) }
+        fakeExecutor.runAllReady()
+        // THEN then elapsed time should not be updated
+        assertThat(viewModel.progress.value!!.elapsedTime).isNull()
+    }
+
+    @Test
+    fun seekStarted_listenerNotified() {
+        var isScrubbing: Boolean? = null
+        val listener =
+            object : SeekBarViewModel.ScrubbingChangeListener {
+                override fun onScrubbingChanged(scrubbing: Boolean) {
+                    isScrubbing = scrubbing
+                }
+            }
+        viewModel.setScrubbingChangeListener(listener)
+
+        viewModel.onSeekStarting()
+        fakeExecutor.runAllReady()
+
+        assertThat(isScrubbing).isTrue()
+    }
+
+    @Test
+    fun seekEnded_listenerNotified() {
+        var isScrubbing: Boolean? = null
+        val listener =
+            object : SeekBarViewModel.ScrubbingChangeListener {
+                override fun onScrubbingChanged(scrubbing: Boolean) {
+                    isScrubbing = scrubbing
+                }
+            }
+        viewModel.setScrubbingChangeListener(listener)
+
+        // Start seeking
+        viewModel.onSeekStarting()
+        fakeExecutor.runAllReady()
+        // End seeking
+        viewModel.onSeek(15L)
+        fakeExecutor.runAllReady()
+
+        assertThat(isScrubbing).isFalse()
+    }
+
+    @Test
+    @Ignore
+    fun onProgressChangedFromUser() {
+        // WHEN user starts dragging the seek bar
+        val pos = 42
+        val bar = SeekBar(context)
+        with(viewModel.seekBarListener) {
+            onStartTrackingTouch(bar)
+            onProgressChanged(bar, pos, true)
+        }
+        fakeExecutor.runAllReady()
+        // THEN then elapsed time should be updated
+        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(pos)
+    }
+
+    @Test
+    fun onProgressChangedFromUserWithoutStartTrackingTouch_transportUpdated() {
+        whenever(mockController.transportControls).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        val pos = 42
+        val bar = SeekBar(context)
+
+        // WHEN we get an onProgressChanged event without an onStartTrackingTouch event
+        with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) }
+        fakeExecutor.runAllReady()
+
+        // THEN we immediately update the transport
+        verify(mockTransport).seekTo(pos.toLong())
+    }
+
+    @Test
+    fun onProgressChangedNotFromUser() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN user starts dragging the seek bar
+        val pos = 42
+        viewModel.seekBarListener.onProgressChanged(SeekBar(context), pos, false)
+        fakeExecutor.runAllReady()
+        // THEN transport controls should be used
+        verify(mockTransport, never()).seekTo(pos.toLong())
+    }
+
+    @Test
+    fun onStartTrackingTouch() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN user starts dragging the seek bar
+        val pos = 42
+        val bar = SeekBar(context).apply { progress = pos }
+        viewModel.seekBarListener.onStartTrackingTouch(bar)
+        fakeExecutor.runAllReady()
+        // THEN transport controls should be used
+        verify(mockTransport, never()).seekTo(pos.toLong())
+    }
+
+    @Test
+    fun onStopTrackingTouch() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN user ends drag
+        val pos = 42
+        val bar = SeekBar(context).apply { progress = pos }
+        viewModel.seekBarListener.onStopTrackingTouch(bar)
+        fakeExecutor.runAllReady()
+        // THEN transport controls should be used
+        verify(mockTransport).seekTo(pos.toLong())
+    }
+
+    @Test
+    fun onStopTrackingTouchAfterProgress() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        viewModel.updateController(mockController)
+        // WHEN user starts dragging the seek bar
+        val pos = 42
+        val progPos = 84
+        val bar = SeekBar(context).apply { progress = pos }
+        with(viewModel.seekBarListener) {
+            onStartTrackingTouch(bar)
+            onProgressChanged(bar, progPos, true)
+            onStopTrackingTouch(bar)
+        }
+        fakeExecutor.runAllReady()
+        // THEN then elapsed time should be updated
+        verify(mockTransport).seekTo(eq(pos.toLong()))
+    }
+
+    @Test
+    fun onFalseTapOrTouch() {
+        whenever(mockController.getTransportControls()).thenReturn(mockTransport)
+        whenever(falsingManager.isFalseTouch(Classifier.MEDIA_SEEKBAR)).thenReturn(true)
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+        viewModel.updateController(mockController)
+        val pos = 169
+
+        viewModel.attachTouchHandlers(mockBar)
+        with(viewModel.seekBarListener) {
+            onStartTrackingTouch(mockBar)
+            onProgressChanged(mockBar, pos, true)
+            onStopTrackingTouch(mockBar)
+        }
+
+        // THEN transport controls should not be used
+        verify(mockTransport, never()).seekTo(pos.toLong())
+    }
+
+    @Test
+    fun queuePollTaskWhenPlaying() {
+        // GIVEN that the track is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN the controller is updated
+        viewModel.updateController(mockController)
+        // THEN a task is queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun noQueuePollTaskWhenStopped() {
+        // GIVEN that the playback state is stopped
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_STOPPED, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN updated
+        viewModel.updateController(mockController)
+        // THEN an update task is not queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun queuePollTaskWhenListening() {
+        // GIVEN listening
+        viewModel.listening = true
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN updated
+        viewModel.updateController(mockController)
+        // THEN an update task is queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun noQueuePollTaskWhenNotListening() {
+        // GIVEN not listening
+        viewModel.listening = false
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // WHEN updated
+        viewModel.updateController(mockController)
+        // THEN an update task is not queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
+        // GIVEN that the track is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        viewModel.updateController(mockController)
+        // WHEN the next task runs
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN another task is queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun noQueuePollTaskWhenSeeking() {
+        // GIVEN listening
+        viewModel.listening = true
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        viewModel.updateController(mockController)
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // WHEN seek starts
+        viewModel.onSeekStarting()
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN an update task is not queued because we don't want it fighting with the user when
+        // they are trying to move the thumb.
+        assertThat(fakeExecutor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun queuePollTaskWhenDoneSeekingWithFalse() {
+        // GIVEN listening
+        viewModel.listening = true
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        viewModel.updateController(mockController)
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // WHEN seek finishes after a false
+        with(viewModel) {
+            onSeekStarting()
+            onSeekFalse()
+            onSeek(42L)
+        }
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN an update task is queued because the gesture was ignored and progress was restored.
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun noQueuePollTaskWhenDoneSeeking() {
+        // GIVEN listening
+        viewModel.listening = true
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        viewModel.updateController(mockController)
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // WHEN seek finishes after a false
+        with(viewModel) {
+            onSeekStarting()
+            onSeek(42L)
+        }
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN no update task is queued because we are waiting for an updated playback state to be
+        // returned in response to the seek.
+        assertThat(fakeExecutor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun startListeningQueuesPollTask() {
+        // GIVEN not listening
+        viewModel.listening = false
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // AND the playback state is playing
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_STOPPED, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        viewModel.updateController(mockController)
+        // WHEN start listening
+        viewModel.listening = true
+        // THEN an update task is queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun playbackChangeQueuesPollTask() {
+        viewModel.updateController(mockController)
+        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
+        verify(mockController).registerCallback(captor.capture())
+        val callback = captor.value
+        // WHEN the callback receives an new state
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
+        callback.onPlaybackStateChanged(state)
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN an update task is queued
+        assertThat(fakeExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    @Ignore
+    fun clearSeekBar() {
+        // GIVEN that the duration is contained within the metadata
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
+                build()
+            }
+        whenever(mockController.getMetadata()).thenReturn(metadata)
+        // AND a valid playback state (ie. media session is not destroyed)
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
+        whenever(mockController.getPlaybackState()).thenReturn(state)
+        // AND the controller has been updated
+        viewModel.updateController(mockController)
+        // WHEN the controller is cleared on the event when the session is destroyed
+        viewModel.clearController()
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN the seek bar is disabled
+        assertThat(viewModel.progress.value!!.enabled).isFalse()
+    }
+
+    @Test
+    fun clearSeekBarUnregistersCallback() {
+        viewModel.updateController(mockController)
+        viewModel.clearController()
+        fakeExecutor.runAllReady()
+        verify(mockController).unregisterCallback(any())
+    }
+
+    @Test
+    fun destroyUnregistersCallback() {
+        viewModel.updateController(mockController)
+        viewModel.onDestroy()
+        fakeExecutor.runAllReady()
+        verify(mockController).unregisterCallback(any())
+    }
+
+    @Test
+    fun nullPlaybackStateUnregistersCallback() {
+        viewModel.updateController(mockController)
+        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
+        verify(mockController).registerCallback(captor.capture())
+        val callback = captor.value
+        // WHEN the callback receives a null state
+        callback.onPlaybackStateChanged(null)
+        with(fakeExecutor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        // THEN we unregister callback (as a result of clearing the controller)
+        fakeExecutor.runAllReady()
+        verify(mockController).unregisterCallback(any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
new file mode 100644
index 0000000..1d6e980
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.media.controls.models.recommendation
+
+import android.app.smartspace.SmartspaceAction
+import android.graphics.drawable.Icon
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class SmartspaceMediaDataTest : SysuiTestCase() {
+
+    private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play)
+
+    @Test
+    fun getValidRecommendations_onlyReturnsRecsWithIcons() {
+        val withIcon1 = SmartspaceAction.Builder("id", "title").setIcon(icon).build()
+        val withIcon2 = SmartspaceAction.Builder("id", "title").setIcon(icon).build()
+        val withoutIcon1 = SmartspaceAction.Builder("id", "title").setIcon(null).build()
+        val withoutIcon2 = SmartspaceAction.Builder("id", "title").setIcon(null).build()
+        val recommendations = listOf(withIcon1, withoutIcon1, withIcon2, withoutIcon2)
+
+        val data = DEFAULT_DATA.copy(recommendations = recommendations)
+
+        assertThat(data.getValidRecommendations()).isEqualTo(listOf(withIcon1, withIcon2))
+    }
+
+    @Test
+    fun isValid_emptyList_returnsFalse() {
+        val data = DEFAULT_DATA.copy(recommendations = listOf())
+
+        assertThat(data.isValid()).isFalse()
+    }
+
+    @Test
+    fun isValid_tooFewRecs_returnsFalse() {
+        val data =
+            DEFAULT_DATA.copy(
+                recommendations =
+                    listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
+            )
+
+        assertThat(data.isValid()).isFalse()
+    }
+
+    @Test
+    fun isValid_tooFewRecsWithIcons_returnsFalse() {
+        val recommendations = mutableListOf<SmartspaceAction>()
+        // Add one fewer recommendation w/ icon than the number required
+        for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) {
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
+        }
+        for (i in 1 until 3) {
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build())
+        }
+
+        val data = DEFAULT_DATA.copy(recommendations = recommendations)
+
+        assertThat(data.isValid()).isFalse()
+    }
+
+    @Test
+    fun isValid_enoughRecsWithIcons_returnsTrue() {
+        val recommendations = mutableListOf<SmartspaceAction>()
+        // Add the number of required recommendations
+        for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) {
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
+        }
+
+        val data = DEFAULT_DATA.copy(recommendations = recommendations)
+
+        assertThat(data.isValid()).isTrue()
+    }
+
+    @Test
+    fun isValid_manyRecsWithIcons_returnsTrue() {
+        val recommendations = mutableListOf<SmartspaceAction>()
+        // Add more than enough recommendations
+        for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) {
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
+        }
+
+        val data = DEFAULT_DATA.copy(recommendations = recommendations)
+
+        assertThat(data.isValid()).isTrue()
+    }
+}
+
+private val DEFAULT_DATA =
+    SmartspaceMediaData(
+        targetId = "INVALID",
+        isActive = false,
+        packageName = "INVALID",
+        cardAction = null,
+        recommendations = emptyList(),
+        dismissIntent = null,
+        headphoneConnectionTimeMillis = 0,
+        instanceId = InstanceId.fakeInstanceId(-1)
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
new file mode 100644
index 0000000..4d2d0f0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.InstanceId;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.player.MediaDeviceData;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class MediaDataCombineLatestTest extends SysuiTestCase {
+
+    @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
+    private static final String KEY = "TEST_KEY";
+    private static final String OLD_KEY = "TEST_KEY_OLD";
+    private static final String APP = "APP";
+    private static final String PACKAGE = "PKG";
+    private static final String ARTIST = "ARTIST";
+    private static final String TITLE = "TITLE";
+    private static final String DEVICE_NAME = "DEVICE_NAME";
+    private static final int USER_ID = 0;
+
+    private MediaDataCombineLatest mManager;
+
+    @Mock private MediaDataManager.Listener mListener;
+
+    private MediaData mMediaData;
+    private MediaDeviceData mDeviceData;
+
+    @Before
+    public void setUp() {
+        mManager = new MediaDataCombineLatest();
+        mManager.addListener(mListener);
+
+        mMediaData = new MediaData(
+                USER_ID, true, APP, null, ARTIST, TITLE, null,
+                new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
+                MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L,
+                InstanceId.fakeInstanceId(-1), -1);
+        mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
+    }
+
+    @Test
+    public void eventNotEmittedWithoutDevice() {
+        // WHEN data source emits an event without device data
+        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // THEN an event isn't emitted
+        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void eventNotEmittedWithoutMedia() {
+        // WHEN device source emits an event without media data
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        // THEN an event isn't emitted
+        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(),
+                anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void emitEventAfterDeviceFirst() {
+        // GIVEN that a device event has already been received
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        // WHEN media event is received
+        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // THEN the listener receives a combined event
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void emitEventAfterMediaFirst() {
+        // GIVEN that media event has already been received
+        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // WHEN device event is received
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        // THEN the listener receives a combined event
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void migrateKeyMediaFirst() {
+        // GIVEN that media and device info has already been received
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        reset(mListener);
+        // WHEN a key migration event is received
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // THEN the listener receives a combined event
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void migrateKeyDeviceFirst() {
+        // GIVEN that media and device info has already been received
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        reset(mListener);
+        // WHEN a key migration event is received
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        // THEN the listener receives a combined event
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void migrateKeyMediaAfter() {
+        // GIVEN that media and device info has already been received
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        reset(mListener);
+        // WHEN a second key migration event is received for media
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // THEN the key has already been migrated
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void migrateKeyDeviceAfter() {
+        // GIVEN that media and device info has already been received
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        reset(mListener);
+        // WHEN a second key migration event is received for the device
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        // THEN the key has already be migrated
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean(),
+                anyInt(), anyBoolean());
+        assertThat(captor.getValue().getDevice()).isNotNull();
+    }
+
+    @Test
+    public void mediaDataRemoved() {
+        // WHEN media data is removed without first receiving device or data
+        mManager.onMediaDataRemoved(KEY);
+        // THEN a removed event isn't emitted
+        verify(mListener, never()).onMediaDataRemoved(eq(KEY));
+    }
+
+    @Test
+    public void mediaDataRemovedAfterMediaEvent() {
+        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDataRemoved(KEY);
+        verify(mListener).onMediaDataRemoved(eq(KEY));
+    }
+
+    @Test
+    public void mediaDataRemovedAfterDeviceEvent() {
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDataRemoved(KEY);
+        verify(mListener).onMediaDataRemoved(eq(KEY));
+    }
+
+    @Test
+    public void mediaDataKeyUpdated() {
+        // GIVEN that device and media events have already been received
+        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        // WHEN the key is changed
+        mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData, true /* immediately */,
+                0 /* receivedSmartspaceCardLatency */, false /* isSsReactivated */);
+        // THEN the listener gets a load event with the correct keys
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(
+                eq("NEW_KEY"), any(), captor.capture(), anyBoolean(), anyInt(), anyBoolean());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
new file mode 100644
index 0000000..9d33e6f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.app.smartspace.SmartspaceAction
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.ui.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val KEY_ALT = "TEST_KEY_2"
+private const val USER_MAIN = 0
+private const val USER_GUEST = 10
+private const val PACKAGE = "PKG"
+private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!!
+private const val APP_UID = 99
+private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
+private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
+private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaDataFilterTest : SysuiTestCase() {
+
+    @Mock private lateinit var listener: MediaDataManager.Listener
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var broadcastSender: BroadcastSender
+    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+    @Mock private lateinit var executor: Executor
+    @Mock private lateinit var smartspaceData: SmartspaceMediaData
+    @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
+    @Mock private lateinit var logger: MediaUiEventLogger
+
+    private lateinit var mediaDataFilter: MediaDataFilter
+    private lateinit var dataMain: MediaData
+    private lateinit var dataGuest: MediaData
+    private val clock = FakeSystemClock()
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        MediaPlayerData.clear()
+        mediaDataFilter =
+            MediaDataFilter(
+                context,
+                userTracker,
+                broadcastSender,
+                lockscreenUserManager,
+                executor,
+                clock,
+                logger
+            )
+        mediaDataFilter.mediaDataManager = mediaDataManager
+        mediaDataFilter.addListener(listener)
+
+        // Start all tests as main user
+        setUser(USER_MAIN)
+
+        // Set up test media data
+        dataMain =
+            MediaTestUtils.emptyMediaData.copy(
+                userId = USER_MAIN,
+                packageName = PACKAGE,
+                instanceId = INSTANCE_ID,
+                appUid = APP_UID
+            )
+        dataGuest = dataMain.copy(userId = USER_GUEST)
+
+        `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
+        `when`(smartspaceData.isActive).thenReturn(true)
+        `when`(smartspaceData.isValid()).thenReturn(true)
+        `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
+        `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
+        `when`(smartspaceData.headphoneConnectionTimeMillis)
+            .thenReturn(clock.currentTimeMillis() - 100)
+        `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
+    }
+
+    private fun setUser(id: Int) {
+        `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
+        `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
+        mediaDataFilter.handleUserSwitched(id)
+    }
+
+    @Test
+    fun testOnDataLoadedForCurrentUser_callsListener() {
+        // GIVEN a media for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+        // THEN we should tell the listener
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun testOnDataLoadedForGuest_doesNotCallListener() {
+        // GIVEN a media for guest user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+
+        // THEN we should NOT tell the listener
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testOnRemovedForCurrent_callsListener() {
+        // GIVEN a media was removed for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataRemoved(KEY)
+
+        // THEN we should tell the listener
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnRemovedForGuest_doesNotCallListener() {
+        // GIVEN a media was removed for guest user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
+        mediaDataFilter.onMediaDataRemoved(KEY)
+
+        // THEN we should NOT tell the listener
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnUserSwitched_removesOldUserControls() {
+        // GIVEN that we have a media loaded for main user
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+
+        // and we switch to guest user
+        setUser(USER_GUEST)
+
+        // THEN we should remove the main user's media
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testOnUserSwitched_addsNewUserControls() {
+        // GIVEN that we had some media for both users
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
+        reset(listener)
+
+        // and we switch to guest user
+        setUser(USER_GUEST)
+
+        // THEN we should add back the guest user media
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+
+        // but not the main user's
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun hasAnyMedia_noMediaSet_returnsFalse() {
+        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
+    }
+
+    @Test
+    fun hasAnyMedia_mediaSet_returnsTrue() {
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+        assertThat(mediaDataFilter.hasAnyMedia()).isTrue()
+    }
+
+    @Test
+    fun hasAnyMedia_recommendationSet_returnsFalse() {
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
+    }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() {
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
+    }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() {
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() {
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun hasActiveMedia_noMediaSet_returnsFalse() {
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMedia_inactiveMediaSet_returnsFalse() {
+        val data = dataMain.copy(active = false)
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMedia_activeMediaSet_returnsTrue() {
+        val data = dataMain.copy(active = true)
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+        assertThat(mediaDataFilter.hasActiveMedia()).isTrue()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() {
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() {
+        val data = dataMain.copy(active = false)
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() {
+        val data = dataMain.copy(active = true)
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
+        `when`(smartspaceData.isActive).thenReturn(false)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
+        `when`(smartspaceData.isValid()).thenReturn(false)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+    }
+
+    @Test
+    fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
+        `when`(smartspaceData.isActive).thenReturn(true)
+        `when`(smartspaceData.isValid()).thenReturn(true)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+    }
+
+    @Test
+    fun testHasAnyMediaOrRecommendation_onlyCurrentUser() {
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
+
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest)
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
+    }
+
+    @Test
+    fun testHasActiveMediaOrRecommendation_onlyCurrentUser() {
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        val data = dataGuest.copy(active = true)
+
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data)
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
+    }
+
+    @Test
+    fun testOnNotificationRemoved_doesntHaveMedia() {
+        mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain)
+        mediaDataFilter.onMediaDataRemoved(KEY)
+        assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasAnyMedia()).isFalse()
+    }
+
+    @Test
+    fun testOnSwipeToDismiss_setsTimedOut() {
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onSwipeToDismiss()
+
+        verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true))
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() {
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+        verify(logger, never()).logRecommendationActivated(any(), any(), any())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
+        `when`(smartspaceData.isActive).thenReturn(false)
+
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+        verify(logger, never()).logRecommendationAdded(any(), any())
+        verify(logger, never()).logRecommendationActivated(any(), any(), any())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() {
+        val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+        clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+        verify(logger, never()).logRecommendationActivated(any(), any(), any())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
+        `when`(smartspaceData.isActive).thenReturn(false)
+
+        val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
+        clock.advanceTime(SMARTSPACE_MAX_AGE + 100)
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+        verify(logger, never()).logRecommendationAdded(any(), any())
+        verify(logger, never()).logRecommendationActivated(any(), any(), any())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
+        `when`(smartspaceData.isActive).thenReturn(false)
+
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN we should tell listeners to treat the media as not active instead
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
+        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+        verify(logger, never()).logRecommendationAdded(any(), any())
+        verify(logger, never()).logRecommendationActivated(any(), any(), any())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
+        `when`(smartspaceData.isValid()).thenReturn(false)
+
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN we should tell listeners to treat the media as active instead
+        val dataCurrentAndActive = dataCurrent.copy(active = true)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+        // Smartspace update shouldn't be propagated for the empty rec list.
+        verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+        verify(logger, never()).logRecommendationAdded(any(), any())
+        verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() {
+        // WHEN we have media that was recently played, but not currently active
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        // AND we get a smartspace signal
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        // THEN we should tell listeners to treat the media as active instead
+        val dataCurrentAndActive = dataCurrent.copy(active = true)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
+        // Smartspace update should also be propagated but not prioritized.
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+        verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
+        verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() {
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+        verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
+        val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+
+        mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
+
+        val dataCurrentAndActive = dataCurrent.copy(active = true)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
+
+        mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+
+        verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+        assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
new file mode 100644
index 0000000..52b694f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -0,0 +1,1367 @@
+/*
+ * 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.media.controls.pipeline
+
+import android.app.Notification
+import android.app.Notification.MediaStyle
+import android.app.PendingIntent
+import android.app.smartspace.SmartspaceAction
+import android.app.smartspace.SmartspaceConfig
+import android.app.smartspace.SmartspaceManager
+import android.app.smartspace.SmartspaceTarget
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.os.Bundle
+import android.provider.Settings
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.media.utils.MediaConstants
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.InstanceIdSequenceFake
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.SbnBuilder
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+private const val KEY = "KEY"
+private const val KEY_2 = "KEY_2"
+private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
+private const val PACKAGE_NAME = "com.example.app"
+private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
+private const val APP_NAME = "SystemUI"
+private const val SESSION_ARTIST = "artist"
+private const val SESSION_TITLE = "title"
+private const val USER_ID = 0
+private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
+
+private fun <T> anyObject(): T {
+    return Mockito.anyObject<T>()
+}
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaDataManagerTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+    @Mock lateinit var mediaControllerFactory: MediaControllerFactory
+    @Mock lateinit var controller: MediaController
+    @Mock lateinit var transportControls: MediaController.TransportControls
+    @Mock lateinit var playbackInfo: MediaController.PlaybackInfo
+    lateinit var session: MediaSession
+    lateinit var metadataBuilder: MediaMetadata.Builder
+    lateinit var backgroundExecutor: FakeExecutor
+    lateinit var foregroundExecutor: FakeExecutor
+    lateinit var uiExecutor: FakeExecutor
+    @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
+    @Mock lateinit var mediaResumeListener: MediaResumeListener
+    @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
+    @Mock lateinit var mediaDeviceManager: MediaDeviceManager
+    @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
+    @Mock lateinit var mediaDataFilter: MediaDataFilter
+    @Mock lateinit var listener: MediaDataManager.Listener
+    @Mock lateinit var pendingIntent: PendingIntent
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock lateinit var smartspaceManager: SmartspaceManager
+    lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
+    @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
+    @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
+    lateinit var validRecommendationList: List<SmartspaceAction>
+    @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction
+    @Mock private lateinit var mediaFlags: MediaFlags
+    @Mock private lateinit var logger: MediaUiEventLogger
+    lateinit var mediaDataManager: MediaDataManager
+    lateinit var mediaNotification: StatusBarNotification
+    @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
+    private val clock = FakeSystemClock()
+    @Mock private lateinit var tunerService: TunerService
+    @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable>
+    @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit>
+    @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig>
+
+    private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
+
+    private val originalSmartspaceSetting =
+        Settings.Secure.getInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
+
+    @Before
+    fun setup() {
+        foregroundExecutor = FakeExecutor(clock)
+        backgroundExecutor = FakeExecutor(clock)
+        uiExecutor = FakeExecutor(clock)
+        smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
+        mediaDataManager =
+            MediaDataManager(
+                context = context,
+                backgroundExecutor = backgroundExecutor,
+                uiExecutor = uiExecutor,
+                foregroundExecutor = foregroundExecutor,
+                mediaControllerFactory = mediaControllerFactory,
+                broadcastDispatcher = broadcastDispatcher,
+                dumpManager = dumpManager,
+                mediaTimeoutListener = mediaTimeoutListener,
+                mediaResumeListener = mediaResumeListener,
+                mediaSessionBasedFilter = mediaSessionBasedFilter,
+                mediaDeviceManager = mediaDeviceManager,
+                mediaDataCombineLatest = mediaDataCombineLatest,
+                mediaDataFilter = mediaDataFilter,
+                activityStarter = activityStarter,
+                smartspaceMediaDataProvider = smartspaceMediaDataProvider,
+                useMediaResumption = true,
+                useQsMediaPlayer = true,
+                systemClock = clock,
+                tunerService = tunerService,
+                mediaFlags = mediaFlags,
+                logger = logger,
+                smartspaceManager = smartspaceManager,
+            )
+        verify(tunerService)
+            .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+        session = MediaSession(context, "MediaDataManagerTestSession")
+        mediaNotification =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                }
+                build()
+            }
+        metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
+        verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
+        whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
+        whenever(controller.transportControls).thenReturn(transportControls)
+        whenever(controller.playbackInfo).thenReturn(playbackInfo)
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+
+        // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
+        // listeners in the internal processing pipeline. It receives events, but ince it is a
+        // mock, it doesn't pass those events along the chain to the external listeners. So, just
+        // treat mediaSessionBasedFilter as a listener for testing.
+        listener = mediaSessionBasedFilter
+
+        val recommendationExtras =
+            Bundle().apply {
+                putString("package_name", PACKAGE_NAME)
+                putParcelable("dismiss_intent", DISMISS_INTENT)
+            }
+        val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
+        whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
+        whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
+        whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
+        whenever(mediaRecommendationItem.icon).thenReturn(icon)
+        validRecommendationList =
+            listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
+        whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
+        whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
+        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
+        whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
+        whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
+    }
+
+    @After
+    fun tearDown() {
+        session.release()
+        mediaDataManager.destroy()
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            originalSmartspaceSetting
+        )
+    }
+
+    @Test
+    fun testSetTimedOut_active_deactivatesMedia() {
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.active).isTrue()
+
+        mediaDataManager.setTimedOut(KEY, timedOut = true)
+        assertThat(data.active).isFalse()
+        verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testSetTimedOut_resume_dismissesMedia() {
+        // WHEN resume controls are present, and time out
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+
+        backgroundExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+
+        mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
+        verify(logger)
+            .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
+
+        // THEN it is removed and listeners are informed
+        foregroundExecutor.advanceClockToLast()
+        foregroundExecutor.runAllReady()
+        verify(listener).onMediaDataRemoved(PACKAGE_NAME)
+    }
+
+    @Test
+    fun testLoadsMetadataOnBackground() {
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        assertThat(backgroundExecutor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testOnMetaDataLoaded_callsListener() {
+        addNotificationAndLoad()
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_LOCAL)
+            )
+    }
+
+    @Test
+    fun testOnMetaDataLoaded_conservesActiveFlag() {
+        whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        mediaDataManager.addListener(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.active).isTrue()
+    }
+
+    @Test
+    fun testOnNotificationAdded_isRcn_markedRemote() {
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
+            }
+
+        mediaDataManager.onNotificationAdded(KEY, rcn)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.playbackLocation)
+            .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(SYSTEM_PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_CAST_REMOTE)
+            )
+    }
+
+    @Test
+    fun testOnNotificationAdded_hasSubstituteName_isUsed() {
+        val subName = "Substitute Name"
+        val notif =
+            SbnBuilder().run {
+                modifyNotification(context).also {
+                    it.extras =
+                        Bundle().apply {
+                            putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
+                        }
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                }
+                build()
+            }
+
+        mediaDataManager.onNotificationAdded(KEY, notif)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+
+        assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName)
+    }
+
+    @Test
+    fun testLoadMediaDataInBg_invalidTokenNoCrash() {
+        val bundle = Bundle()
+        // wrong data type
+        bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle())
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.addExtras(bundle)
+                    it.setStyle(
+                        MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) }
+                    )
+                }
+                build()
+            }
+
+        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+        // no crash even though the data structure is incorrect
+    }
+
+    @Test
+    fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() {
+        val bundle = Bundle()
+        // wrong data type
+        bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle())
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.addExtras(bundle)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
+            }
+
+        mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
+        // no crash even though the data structure is incorrect
+    }
+
+    @Test
+    fun testOnNotificationRemoved_callsListener() {
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        mediaDataManager.onNotificationRemoved(KEY)
+        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testOnNotificationRemoved_withResumption() {
+        // GIVEN that the manager has a notification with a resume action
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        // WHEN the notification is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+        // THEN the media data indicates that it is for resumption
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
+        verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testOnNotificationRemoved_twoWithResumption() {
+        // GIVEN that the manager has two notifications with resume actions
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isFalse()
+        val resumableData = data.copy(resumeAction = Runnable {})
+        mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
+        mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
+        reset(listener)
+        // WHEN the first is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+        // THEN the data is for resumption and the key is migrated to the package name
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        // WHEN the second is removed
+        mediaDataManager.onNotificationRemoved(KEY_2)
+        // THEN the data is for resumption and the second key is removed
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(PACKAGE_NAME),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        verify(listener).onMediaDataRemoved(eq(KEY_2))
+    }
+
+    @Test
+    fun testOnNotificationRemoved_withResumption_butNotLocal() {
+        // GIVEN that the manager has a notification with a resume action, but is not local
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val dataRemoteWithResume =
+            data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
+        mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_CAST_LOCAL)
+            )
+
+        // WHEN the notification is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN the media data is removed
+        verify(listener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun testAddResumptionControls() {
+        // WHEN resumption controls are added
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        val currentTime = clock.elapsedRealtime()
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        // THEN the media data indicates that it is for resumption
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isTrue()
+        assertThat(data.song).isEqualTo(SESSION_TITLE)
+        assertThat(data.app).isEqualTo(APP_NAME)
+        assertThat(data.actions).hasSize(1)
+        assertThat(data.semanticActions!!.playOrPause).isNotNull()
+        assertThat(data.lastActive).isAtLeast(currentTime)
+        verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testResumptionDisabled_dismissesResumeControls() {
+        // WHEN there are resume controls and resumption is switched off
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        val data = mediaDataCaptor.value
+        mediaDataManager.setMediaResumptionEnabled(false)
+
+        // THEN the resume controls are dismissed
+        verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testDismissMedia_listenerCalled() {
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        assertThat(removed).isTrue()
+
+        foregroundExecutor.advanceClockToLast()
+        foregroundExecutor.runAllReady()
+
+        verify(listener).onMediaDataRemoved(eq(KEY))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
+    fun testDismissMedia_keyDoesNotExist_returnsFalse() {
+        val removed = mediaDataManager.dismissMediaData(KEY, 0L)
+        assertThat(removed).isFalse()
+    }
+
+    @Test
+    fun testBadArtwork_doesNotUse() {
+        // WHEN notification has a too-small artwork
+        val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.setLargeIcon(artwork)
+                }
+                build()
+            }
+        mediaDataManager.onNotificationAdded(KEY, notif)
+
+        // THEN it still loads
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() {
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        verify(logger).getNewInstanceId()
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() {
+        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        verify(logger).getNewInstanceId()
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
+        val recommendationExtras =
+            Bundle().apply {
+                putString("package_name", PACKAGE_NAME)
+                putParcelable("dismiss_intent", null)
+            }
+        whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
+        whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
+        whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        verify(logger).getNewInstanceId()
+        val instanceId = instanceIdSequence.lastInstanceId
+
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        dismissIntent = null,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() {
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf())
+        verify(logger, never()).getNewInstanceId()
+        verify(listener, never())
+            .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() {
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        verify(logger).getNewInstanceId()
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf())
+        uiExecutor.advanceClockToLast()
+        uiExecutor.runAllReady()
+
+        verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
+        // WHEN media recommendation setting is off
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            0
+        )
+        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+
+        // THEN smartspace signal is ignored
+        verify(listener, never())
+            .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
+    }
+
+    @Test
+    fun testMediaRecommendationDisabled_removesSmartspaceData() {
+        // GIVEN a media recommendation card is present
+        smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
+
+        // WHEN the media recommendation setting is turned off
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            0
+        )
+        tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
+
+        // THEN listeners are notified
+        uiExecutor.advanceClockToLast()
+        foregroundExecutor.advanceClockToLast()
+        uiExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+        verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true))
+    }
+
+    @Test
+    fun testOnMediaDataChanged_updatesLastActiveTime() {
+        val currentTime = clock.elapsedRealtime()
+        addNotificationAndLoad()
+        assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime)
+    }
+
+    @Test
+    fun testOnMediaDataTimedOut_doesNotUpdateLastActiveTime() {
+        // GIVEN that the manager has a notification
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+        // WHEN the notification times out
+        clock.advanceTime(100)
+        val currentTime = clock.elapsedRealtime()
+        mediaDataManager.setTimedOut(KEY, true, true)
+
+        // THEN the last active time is not changed
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
+    }
+
+    @Test
+    fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() {
+        // GIVEN that the manager has a notification with a resume action
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        val instanceId = data.instanceId
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+
+        // WHEN the notification is removed
+        clock.advanceTime(100)
+        val currentTime = clock.elapsedRealtime()
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // THEN the last active time is not changed
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
+
+        // Log as a conversion event, not as a new resume control
+        verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId))
+        verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
+    }
+
+    @Test
+    fun testTooManyCompactActions_isTruncated() {
+        // GIVEN a notification where too many compact actions were specified
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setShowActionsInCompactView(0, 1, 2, 3, 4)
+                        }
+                    )
+                }
+                build()
+            }
+
+        // WHEN the notification is loaded
+        mediaDataManager.onNotificationAdded(KEY, notif)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+        // THEN only the first MAX_COMPACT_ACTIONS are actually set
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
+            .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
+    }
+
+    @Test
+    fun testTooManyNotificationActions_isTruncated() {
+        // GIVEN a notification where too many notification actions are added
+        val action = Notification.Action(R.drawable.ic_android, "action", null)
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+                        it.addAction(action)
+                    }
+                }
+                build()
+            }
+
+        // WHEN the notification is loaded
+        mediaDataManager.onNotificationAdded(KEY, notif)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+
+        // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.actions.size)
+            .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+    }
+
+    @Test
+    fun testPlaybackActions_noState_usesNotification() {
+        val desc = "Notification Action"
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        whenever(controller.playbackState).thenReturn(null)
+
+        val notifWithAction =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.addAction(android.R.drawable.ic_media_play, desc, null)
+                }
+                build()
+            }
+        mediaDataManager.onNotificationAdded(KEY, notifWithAction)
+
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNull()
+        assertThat(mediaDataCaptor.value!!.actions).hasSize(1)
+        assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
+    }
+
+    @Test
+    fun testPlaybackActions_hasPrevNext() {
+        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions =
+            PlaybackState.ACTION_PLAY or
+                PlaybackState.ACTION_SKIP_TO_PREVIOUS or
+                PlaybackState.ACTION_SKIP_TO_NEXT
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+        customDesc.forEach {
+            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+        }
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
+        actions.playOrPause!!.action!!.run()
+        verify(transportControls).play()
+
+        assertThat(actions.prevOrCustom).isNotNull()
+        assertThat(actions.prevOrCustom!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_prev))
+        actions.prevOrCustom!!.action!!.run()
+        verify(transportControls).skipToPrevious()
+
+        assertThat(actions.nextOrCustom).isNotNull()
+        assertThat(actions.nextOrCustom!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_next))
+        actions.nextOrCustom!!.action!!.run()
+        verify(transportControls).skipToNext()
+
+        assertThat(actions.custom0).isNotNull()
+        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
+
+        assertThat(actions.custom1).isNotNull()
+        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
+    }
+
+    @Test
+    fun testPlaybackActions_noPrevNext_usesCustom() {
+        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions = PlaybackState.ACTION_PLAY
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+        customDesc.forEach {
+            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+        }
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
+
+        assertThat(actions.prevOrCustom).isNotNull()
+        assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0])
+
+        assertThat(actions.nextOrCustom).isNotNull()
+        assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1])
+
+        assertThat(actions.custom0).isNotNull()
+        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[2])
+
+        assertThat(actions.custom1).isNotNull()
+        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
+    }
+
+    @Test
+    fun testPlaybackActions_connecting() {
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions = PlaybackState.ACTION_PLAY
+        val stateBuilder =
+            PlaybackState.Builder()
+                .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
+                .setActions(stateActions)
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_connecting))
+    }
+
+    @Test
+    fun testPlaybackActions_reservedSpace() {
+        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions = PlaybackState.ACTION_PLAY
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+        customDesc.forEach {
+            stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
+        }
+        val extras =
+            Bundle().apply {
+                putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
+                putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
+            }
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+        whenever(controller.extras).thenReturn(extras)
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
+
+        assertThat(actions.prevOrCustom).isNull()
+        assertThat(actions.nextOrCustom).isNull()
+
+        assertThat(actions.custom0).isNotNull()
+        assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0])
+
+        assertThat(actions.custom1).isNotNull()
+        assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
+
+        assertThat(actions.reserveNext).isTrue()
+        assertThat(actions.reservePrev).isTrue()
+    }
+
+    @Test
+    fun testPlaybackActions_playPause_hasButton() {
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions = PlaybackState.ACTION_PLAY_PAUSE
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
+        actions.playOrPause!!.action!!.run()
+        verify(transportControls).play()
+    }
+
+    @Test
+    fun testPlaybackLocationChange_isLogged() {
+        // Media control added for local playback
+        addNotificationAndLoad()
+        val instanceId = mediaDataCaptor.value.instanceId
+
+        // Location is updated to local cast
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        addNotificationAndLoad()
+        verify(logger)
+            .logPlaybackLocationChange(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(instanceId),
+                eq(MediaData.PLAYBACK_CAST_LOCAL)
+            )
+
+        // update to remote cast
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME) // System package
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
+            }
+
+        mediaDataManager.onNotificationAdded(KEY, rcn)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(logger)
+            .logPlaybackLocationChange(
+                anyInt(),
+                eq(SYSTEM_PACKAGE_NAME),
+                eq(instanceId),
+                eq(MediaData.PLAYBACK_CAST_REMOTE)
+            )
+    }
+
+    @Test
+    fun testPlaybackStateChange_keyExists_callsListener() {
+        // Notification has been added
+        addNotificationAndLoad()
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+
+        // Callback gets an updated state
+        val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
+        callbackCaptor.value.invoke(KEY, state)
+
+        // Listener is notified of updated state
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.isPlaying).isTrue()
+    }
+
+    @Test
+    fun testPlaybackStateChange_keyDoesNotExist_doesNothing() {
+        val state = PlaybackState.Builder().build()
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+
+        // No media added with this key
+
+        callbackCaptor.value.invoke(KEY, state)
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testPlaybackStateChange_keyHasNullToken_doesNothing() {
+        // When we get an update that sets the data's token to null
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(token = null))
+
+        // And then get a state update
+        val state = PlaybackState.Builder().build()
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+
+        // Then no changes are made
+        callbackCaptor.value.invoke(KEY, state)
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+    }
+
+    @Test
+    fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() {
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build()
+        whenever(controller.playbackState).thenReturn(state)
+
+        addNotificationAndLoad()
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+        callbackCaptor.value.invoke(KEY, state)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
+        assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
+    }
+
+    @Test
+    fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() {
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        val state =
+            PlaybackState.Builder()
+                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
+                .setActions(PlaybackState.ACTION_PLAY_PAUSE)
+                .build()
+
+        // Add resumption controls in order to have semantic actions.
+        // To make sure that they are not null after changing state.
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
+        backgroundExecutor.runAllReady()
+        foregroundExecutor.runAllReady()
+
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+        callbackCaptor.value.invoke(PACKAGE_NAME, state)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(PACKAGE_NAME),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
+        assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
+    }
+
+    @Test
+    fun testPlaybackStateNull_Pause_keyExists_callsListener() {
+        whenever(controller.playbackState).thenReturn(null)
+        val state =
+            PlaybackState.Builder()
+                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
+                .setActions(PlaybackState.ACTION_PLAY_PAUSE)
+                .build()
+
+        addNotificationAndLoad()
+        verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
+        callbackCaptor.value.invoke(KEY, state)
+
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
+        assertThat(mediaDataCaptor.value.semanticActions).isNull()
+    }
+
+    /** Helper function to add a media notification and capture the resulting MediaData */
+    private fun addNotificationAndLoad() {
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
new file mode 100644
index 0000000..a45e9d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.bluetooth.BluetoothLeBroadcast
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.media.MediaRouter2Manager
+import android.media.RoutingSessionInfo
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.settingslib.media.LocalMediaManager
+import com.android.settingslib.media.MediaDevice
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
+import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.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.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+private const val KEY = "TEST_KEY"
+private const val KEY_OLD = "TEST_KEY_OLD"
+private const val PACKAGE = "PKG"
+private const val SESSION_KEY = "SESSION_KEY"
+private const val DEVICE_ID = "DEVICE_ID"
+private const val DEVICE_NAME = "DEVICE_NAME"
+private const val REMOTE_DEVICE_NAME = "REMOTE_DEVICE_NAME"
+private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME"
+private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class MediaDeviceManagerTest : SysuiTestCase() {
+
+    private lateinit var manager: MediaDeviceManager
+    @Mock private lateinit var controllerFactory: MediaControllerFactory
+    @Mock private lateinit var lmmFactory: LocalMediaManagerFactory
+    @Mock private lateinit var lmm: LocalMediaManager
+    @Mock private lateinit var mr2: MediaRouter2Manager
+    @Mock private lateinit var muteAwaitFactory: MediaMuteAwaitConnectionManagerFactory
+    @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager
+    private lateinit var fakeFgExecutor: FakeExecutor
+    private lateinit var fakeBgExecutor: FakeExecutor
+    @Mock private lateinit var dumpster: DumpManager
+    @Mock private lateinit var listener: MediaDeviceManager.Listener
+    @Mock private lateinit var device: MediaDevice
+    @Mock private lateinit var icon: Drawable
+    @Mock private lateinit var route: RoutingSessionInfo
+    @Mock private lateinit var controller: MediaController
+    @Mock private lateinit var playbackInfo: PlaybackInfo
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var bluetoothLeBroadcast: BluetoothLeBroadcast
+    @Mock private lateinit var localBluetoothProfileManager: LocalBluetoothProfileManager
+    @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
+    private lateinit var localBluetoothManager: LocalBluetoothManager
+    private lateinit var session: MediaSession
+    private lateinit var mediaData: MediaData
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        fakeFgExecutor = FakeExecutor(FakeSystemClock())
+        fakeBgExecutor = FakeExecutor(FakeSystemClock())
+        localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
+        manager =
+            MediaDeviceManager(
+                context,
+                controllerFactory,
+                lmmFactory,
+                mr2,
+                muteAwaitFactory,
+                configurationController,
+                localBluetoothManager,
+                fakeFgExecutor,
+                fakeBgExecutor,
+                dumpster
+            )
+        manager.addListener(listener)
+
+        // Configure mocks.
+        whenever(device.name).thenReturn(DEVICE_NAME)
+        whenever(device.iconWithoutBackground).thenReturn(icon)
+        whenever(lmmFactory.create(PACKAGE)).thenReturn(lmm)
+        whenever(muteAwaitFactory.create(lmm)).thenReturn(muteAwaitManager)
+        whenever(lmm.getCurrentConnectedDevice()).thenReturn(device)
+        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route)
+
+        // Create a media sesssion and notification for testing.
+        session = MediaSession(context, SESSION_KEY)
+
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
+        whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
+        setupLeAudioConfiguration(false)
+    }
+
+    @After
+    fun tearDown() {
+        session.release()
+    }
+
+    @Test
+    fun removeUnknown() {
+        manager.onMediaDataRemoved("unknown")
+    }
+
+    @Test
+    fun loadMediaData() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        verify(lmmFactory).create(PACKAGE)
+    }
+
+    @Test
+    fun loadAndRemoveMediaData() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        manager.onMediaDataRemoved(KEY)
+        fakeBgExecutor.runAllReady()
+        verify(lmm).unregisterCallback(any())
+        verify(muteAwaitManager).stopListening()
+    }
+
+    @Test
+    fun loadMediaDataWithNullToken() {
+        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+    }
+
+    @Test
+    fun loadWithNewKey() {
+        // GIVEN that media data has been loaded with an old key
+        manager.onMediaDataLoaded(KEY_OLD, null, mediaData)
+        reset(listener)
+        // WHEN data is loaded with a new key
+        manager.onMediaDataLoaded(KEY, KEY_OLD, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the listener for the old key should removed.
+        verify(lmm).unregisterCallback(any())
+        verify(muteAwaitManager).stopListening()
+        // AND a new device event emitted
+        val data = captureDeviceData(KEY, KEY_OLD)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+    }
+
+    @Test
+    fun newKeySameAsOldKey() {
+        // GIVEN that media data has been loaded
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        reset(listener)
+        // WHEN the new key is the same as the old key
+        manager.onMediaDataLoaded(KEY, KEY, mediaData)
+        // THEN no event should be emitted
+        verify(listener, never()).onMediaDeviceChanged(eq(KEY), eq(null), any())
+    }
+
+    @Test
+    fun unknownOldKey() {
+        val oldKey = "unknown"
+        manager.onMediaDataLoaded(KEY, oldKey, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        verify(listener).onMediaDeviceChanged(eq(KEY), eq(oldKey), any())
+    }
+
+    @Test
+    fun updateToSessionTokenWithNullRoute() {
+        // GIVEN that media data has been loaded with a null token
+        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+        // WHEN media data is loaded with a different token
+        // AND that token results in a null route
+        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device should be disabled
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isFalse()
+    }
+
+    @Test
+    fun deviceEventOnAddNotification() {
+        // WHEN a notification is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the update is dispatched to the listener
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
+    }
+
+    @Test
+    fun removeListener() {
+        // WHEN a listener is removed
+        manager.removeListener(listener)
+        // THEN it doesn't receive device events
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        verify(listener, never()).onMediaDeviceChanged(eq(KEY), eq(null), any())
+    }
+
+    @Test
+    fun deviceListUpdate() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        val deviceCallback = captureCallback()
+        verify(muteAwaitManager).startListening()
+        // WHEN the device list changes
+        deviceCallback.onDeviceListUpdate(mutableListOf(device))
+        assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+        // THEN the update is dispatched to the listener
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
+    }
+
+    @Test
+    fun selectedDeviceStateChanged() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        val deviceCallback = captureCallback()
+        // WHEN the selected device changes state
+        deviceCallback.onSelectedDeviceStateChanged(device, 1)
+        assertThat(fakeBgExecutor.runAllReady()).isEqualTo(1)
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+        // THEN the update is dispatched to the listener
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
+    }
+
+    @Test
+    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        // Run and reset the executors and listeners so we only focus on new events.
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+
+        // Ensure we'll get device info when using the address
+        val fullMediaDevice = mock(MediaDevice::class.java)
+        val address = "fakeAddress"
+        val nameFromDevice = "nameFromDevice"
+        val iconFromDevice = mock(Drawable::class.java)
+        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
+        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
+        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)
+
+        // WHEN the about-to-connect device changes to non-null
+        val deviceCallback = captureCallback()
+        val nameFromParam = "nameFromParam"
+        val iconFromParam = mock(Drawable::class.java)
+        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+
+        // THEN the about-to-connect device based on the address is returned
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(nameFromDevice)
+        assertThat(data.name).isNotEqualTo(nameFromParam)
+        assertThat(data.icon).isEqualTo(iconFromDevice)
+        assertThat(data.icon).isNotEqualTo(iconFromParam)
+    }
+
+    @Test
+    fun onAboutToConnectDeviceAdded_cantFindDeviceInfoFromAddress() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        // Run and reset the executors and listeners so we only focus on new events.
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+
+        // Ensure we can't get device info based on the address
+        val address = "fakeAddress"
+        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(null)
+
+        // WHEN the about-to-connect device changes to non-null
+        val deviceCallback = captureCallback()
+        val name = "AboutToConnectDeviceName"
+        val mockIcon = mock(Drawable::class.java)
+        deviceCallback.onAboutToConnectDeviceAdded(address, name, mockIcon)
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+
+        // THEN the about-to-connect device based on the parameters is returned
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(name)
+        assertThat(data.icon).isEqualTo(mockIcon)
+    }
+
+    @Test
+    fun onAboutToConnectDeviceAddedThenRemoved_usesNormalDevice() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        val deviceCallback = captureCallback()
+        // First set a non-null about-to-connect device
+        deviceCallback.onAboutToConnectDeviceAdded(
+            "fakeAddress",
+            "AboutToConnectDeviceName",
+            mock(Drawable::class.java)
+        )
+        // Run and reset the executors and listeners so we only focus on new events.
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+
+        // WHEN hasDevice switches to false
+        deviceCallback.onAboutToConnectDeviceRemoved()
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+        // THEN the normal device is returned
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.icon).isEqualTo(icon)
+    }
+
+    @Test
+    fun listenerReceivesKeyRemoved() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        // WHEN the notification is removed
+        manager.onMediaDataRemoved(KEY)
+        // THEN the listener receives key removed event
+        verify(listener).onKeyRemoved(eq(KEY))
+    }
+
+    @Test
+    fun deviceNameFromMR2RouteInfo() {
+        // GIVEN that MR2Manager returns a valid routing session
+        whenever(route.name).thenReturn(REMOTE_DEVICE_NAME)
+        // WHEN a notification is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN it uses the route name (instead of device name)
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
+    }
+
+    @Test
+    fun deviceDisabledWhenMR2ReturnsNullRouteInfo() {
+        // GIVEN that MR2Manager returns null for routing session
+        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
+        // WHEN a notification is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device is disabled and name is set to null
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
+    }
+
+    @Test
+    fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
+        // GIVEN a notif is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+        // AND MR2Manager returns null for routing session
+        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
+        // WHEN the selected device changes state
+        val deviceCallback = captureCallback()
+        deviceCallback.onSelectedDeviceStateChanged(device, 1)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device is disabled and name is set to null
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
+    }
+
+    @Test
+    fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
+        // GIVEN a notif is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+        // GIVEN that MR2Manager returns null for routing session
+        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
+        // WHEN the selected device changes state
+        val deviceCallback = captureCallback()
+        deviceCallback.onDeviceListUpdate(mutableListOf(device))
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device is disabled and name is set to null
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
+    }
+
+    @Test
+    fun mr2ReturnsRouteWithNullName_useLocalDeviceName() {
+        // GIVEN that MR2Manager returns a routing session that does not have a name
+        whenever(route.name).thenReturn(null)
+        // WHEN a notification is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device is enabled and uses the current connected device name
+        val data = captureDeviceData(KEY)
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.enabled).isTrue()
+    }
+
+    @Test
+    fun audioInfoChanged() {
+        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+        whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo)
+        // GIVEN a controller with local playback type
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(mr2)
+        // WHEN onAudioInfoChanged fires with remote playback type
+        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
+        verify(controller).registerCallback(captor.capture())
+        captor.value.onAudioInfoChanged(playbackInfo)
+        // THEN the route is checked
+        verify(mr2).getRoutingSessionForMediaController(eq(controller))
+    }
+
+    @Test
+    fun audioInfoHasntChanged() {
+        whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo)
+        // GIVEN a controller with remote playback type
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(mr2)
+        // WHEN onAudioInfoChanged fires with remote playback type
+        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
+        verify(controller).registerCallback(captor.capture())
+        captor.value.onAudioInfoChanged(playbackInfo)
+        // THEN the route is not checked
+        verify(mr2, never()).getRoutingSessionForMediaController(eq(controller))
+    }
+
+    @Test
+    fun deviceIdChanged_informListener() {
+        // GIVEN a notification is added, with a particular device connected
+        whenever(device.id).thenReturn(DEVICE_ID)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        // and later the manager gets a new device ID
+        val deviceCallback = captureCallback()
+        val updatedId = DEVICE_ID + "_new"
+        whenever(device.id).thenReturn(updatedId)
+        deviceCallback.onDeviceListUpdate(mutableListOf(device))
+
+        // THEN the listener gets the updated info
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
+        verify(listener, times(2)).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
+
+        val firstDevice = dataCaptor.allValues.get(0)
+        assertThat(firstDevice.id).isEqualTo(DEVICE_ID)
+
+        val secondDevice = dataCaptor.allValues.get(1)
+        assertThat(secondDevice.id).isEqualTo(updatedId)
+    }
+
+    @Test
+    fun deviceNameChanged_informListener() {
+        // GIVEN a notification is added, with a particular device connected
+        whenever(device.id).thenReturn(DEVICE_ID)
+        whenever(device.name).thenReturn(DEVICE_NAME)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        // and later the manager gets a new device name
+        val deviceCallback = captureCallback()
+        val updatedName = DEVICE_NAME + "_new"
+        whenever(device.name).thenReturn(updatedName)
+        deviceCallback.onDeviceListUpdate(mutableListOf(device))
+
+        // THEN the listener gets the updated info
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
+        verify(listener, times(2)).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
+
+        val firstDevice = dataCaptor.allValues.get(0)
+        assertThat(firstDevice.name).isEqualTo(DEVICE_NAME)
+
+        val secondDevice = dataCaptor.allValues.get(1)
+        assertThat(secondDevice.name).isEqualTo(updatedName)
+    }
+
+    @Test
+    fun deviceIconChanged_doesNotCallListener() {
+        // GIVEN a notification is added, with a particular device connected
+        whenever(device.id).thenReturn(DEVICE_ID)
+        whenever(device.name).thenReturn(DEVICE_NAME)
+        val firstIcon = mock(Drawable::class.java)
+        whenever(device.icon).thenReturn(firstIcon)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val dataCaptor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
+        verify(listener).onMediaDeviceChanged(eq(KEY), any(), dataCaptor.capture())
+
+        // and later the manager gets a callback with only the icon changed
+        val deviceCallback = captureCallback()
+        val secondIcon = mock(Drawable::class.java)
+        whenever(device.icon).thenReturn(secondIcon)
+        deviceCallback.onDeviceListUpdate(mutableListOf(device))
+
+        // THEN the listener is not called again
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun testRemotePlaybackDeviceOverride() {
+        whenever(route.name).thenReturn(DEVICE_NAME)
+        val deviceData =
+            MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, showBroadcastButton = false)
+        val mediaDataWithDevice = mediaData.copy(device = deviceData)
+
+        // GIVEN media data that already has a device set
+        manager.onMediaDataLoaded(KEY, null, mediaDataWithDevice)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        // THEN we keep the device info, and don't register a listener
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isFalse()
+        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
+        verify(lmm, never()).registerCallback(any())
+    }
+
+    @Test
+    fun onBroadcastStarted_currentMediaDeviceDataIsBroadcasting() {
+        val broadcastCallback = setupBroadcastCallback()
+        setupLeAudioConfiguration(true)
+        setupBroadcastPackage(BROADCAST_APP_NAME)
+        broadcastCallback.onBroadcastStarted(1, 1)
+
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val data = captureDeviceData(KEY)
+        assertThat(data.showBroadcastButton).isTrue()
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name)
+            .isEqualTo(context.getString(R.string.broadcasting_description_is_broadcasting))
+    }
+
+    @Test
+    fun onBroadcastStarted_currentMediaDeviceDataIsNotBroadcasting() {
+        val broadcastCallback = setupBroadcastCallback()
+        setupLeAudioConfiguration(true)
+        setupBroadcastPackage(NORMAL_APP_NAME)
+        broadcastCallback.onBroadcastStarted(1, 1)
+
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val data = captureDeviceData(KEY)
+        assertThat(data.showBroadcastButton).isTrue()
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(BROADCAST_APP_NAME)
+    }
+
+    @Test
+    fun onBroadcastStopped_bluetoothLeBroadcastIsDisabledAndBroadcastingButtonIsGone() {
+        val broadcastCallback = setupBroadcastCallback()
+        setupLeAudioConfiguration(false)
+        broadcastCallback.onBroadcastStopped(1, 1)
+
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+
+        val data = captureDeviceData(KEY)
+        assertThat(data.showBroadcastButton).isFalse()
+    }
+
+    fun captureCallback(): LocalMediaManager.DeviceCallback {
+        val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
+        verify(lmm).registerCallback(captor.capture())
+        return captor.getValue()
+    }
+
+    fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
+        val callback: BluetoothLeBroadcast.Callback =
+            object : BluetoothLeBroadcast.Callback {
+                override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastStartFailed(reason: Int) {}
+                override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastStopFailed(reason: Int) {}
+                override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
+                override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastMetadataChanged(
+                    broadcastId: Int,
+                    metadata: BluetoothLeBroadcastMetadata
+                ) {}
+            }
+
+        bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
+        return callback
+    }
+
+    fun setupLeAudioConfiguration(isLeAudio: Boolean) {
+        whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
+        whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
+            .thenReturn(localBluetoothLeBroadcast)
+        whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio)
+        whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
+    }
+
+    fun setupBroadcastPackage(currentName: String) {
+        whenever(lmm.packageName).thenReturn(PACKAGE)
+        whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
+            .thenReturn(applicationInfo)
+        whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName)
+        context.setMockPackageManager(packageManager)
+    }
+
+    fun captureDeviceData(key: String, oldKey: String? = null): MediaDeviceData {
+        val captor = ArgumentCaptor.forClass(MediaDeviceData::class.java)
+        verify(listener).onMediaDeviceChanged(eq(key), eq(oldKey), captor.capture())
+        return captor.getValue()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
new file mode 100644
index 0000000..3099609
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+private const val PACKAGE = "PKG"
+private const val KEY = "TEST_KEY"
+private const val NOTIF_KEY = "TEST_KEY"
+
+private val info =
+    MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY)
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class MediaSessionBasedFilterTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    // Unit to be tested
+    private lateinit var filter: MediaSessionBasedFilter
+
+    private lateinit var sessionListener: MediaSessionManager.OnActiveSessionsChangedListener
+    @Mock private lateinit var mediaListener: MediaDataManager.Listener
+
+    // MediaSessionBasedFilter dependencies
+    @Mock private lateinit var mediaSessionManager: MediaSessionManager
+    private lateinit var fgExecutor: FakeExecutor
+    private lateinit var bgExecutor: FakeExecutor
+
+    @Mock private lateinit var controller1: MediaController
+    @Mock private lateinit var controller2: MediaController
+    @Mock private lateinit var controller3: MediaController
+    @Mock private lateinit var controller4: MediaController
+
+    private lateinit var token1: MediaSession.Token
+    private lateinit var token2: MediaSession.Token
+    private lateinit var token3: MediaSession.Token
+    private lateinit var token4: MediaSession.Token
+
+    @Mock private lateinit var remotePlaybackInfo: PlaybackInfo
+    @Mock private lateinit var localPlaybackInfo: PlaybackInfo
+
+    private lateinit var session1: MediaSession
+    private lateinit var session2: MediaSession
+    private lateinit var session3: MediaSession
+    private lateinit var session4: MediaSession
+
+    private lateinit var mediaData1: MediaData
+    private lateinit var mediaData2: MediaData
+    private lateinit var mediaData3: MediaData
+    private lateinit var mediaData4: MediaData
+
+    @Before
+    fun setUp() {
+        fgExecutor = FakeExecutor(FakeSystemClock())
+        bgExecutor = FakeExecutor(FakeSystemClock())
+        filter = MediaSessionBasedFilter(context, mediaSessionManager, fgExecutor, bgExecutor)
+
+        // Configure mocks.
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(emptyList())
+
+        session1 = MediaSession(context, "MediaSessionBasedFilter1")
+        session2 = MediaSession(context, "MediaSessionBasedFilter2")
+        session3 = MediaSession(context, "MediaSessionBasedFilter3")
+        session4 = MediaSession(context, "MediaSessionBasedFilter4")
+
+        token1 = session1.sessionToken
+        token2 = session2.sessionToken
+        token3 = session3.sessionToken
+        token4 = session4.sessionToken
+
+        whenever(controller1.getSessionToken()).thenReturn(token1)
+        whenever(controller2.getSessionToken()).thenReturn(token2)
+        whenever(controller3.getSessionToken()).thenReturn(token3)
+        whenever(controller4.getSessionToken()).thenReturn(token4)
+
+        whenever(controller1.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller2.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller3.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller4.getPackageName()).thenReturn(PACKAGE)
+
+        mediaData1 = info.copy(token = token1)
+        mediaData2 = info.copy(token = token2)
+        mediaData3 = info.copy(token = token3)
+        mediaData4 = info.copy(token = token4)
+
+        whenever(remotePlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        whenever(localPlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+
+        whenever(controller1.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller2.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller3.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller4.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+
+        // Capture listener
+        bgExecutor.runAllReady()
+        val listenerCaptor =
+            ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java)
+        verify(mediaSessionManager)
+            .addOnActiveSessionsChangedListener(listenerCaptor.capture(), any())
+        sessionListener = listenerCaptor.value
+
+        filter.addListener(mediaListener)
+    }
+
+    @After
+    fun tearDown() {
+        session1.release()
+        session2.release()
+        session3.release()
+        session4.release()
+    }
+
+    @Test
+    fun noMediaSession_loadedEventNotFiltered() {
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun noMediaSession_removedEventNotFiltered() {
+        filter.onMediaDataRemoved(KEY)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun matchingMediaSession_loadedEventNotFiltered() {
+        // GIVEN an active session
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun matchingMediaSession_removedEventNotFiltered() {
+        // GIVEN an active session
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a removed event is received
+        filter.onMediaDataRemoved(KEY)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun remoteSession_loadedEventNotFiltered() {
+        // GIVEN a remote session
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matche the session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun remoteAndLocalSessions_localLoadedEventFiltered() {
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(KEY, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is filtered
+        verify(mediaListener, never())
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
+    }
+
+    @Test
+    fun remoteAndLocalSessions_remoteSessionWithoutNotification() {
+        // GIVEN remote and local sessions
+        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered because there isn't a notification for the remote
+        // session.
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun remoteAndLocalHaveDifferentKeys_localLoadedEventFiltered() {
+        // GIVEN remote and local sessions
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(key1, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(key2, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is filtered
+        verify(mediaListener, never())
+            .onMediaDataLoaded(
+                eq(key2),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
+        // AND there should be a removed event for key2
+        verify(mediaListener).onMediaDataRemoved(eq(key2))
+    }
+
+    @Test
+    fun remoteAndLocalHaveDifferentKeys_remoteSessionWithoutNotification() {
+        // GIVEN remote and local sessions
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(key1, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(key2, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun multipleRemoteSessions_loadedEventNotFiltered() {
+        // GIVEN two remote sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(KEY, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun multipleOtherSessions_loadedEventNotFiltered() {
+        // GIVEN multiple active sessions from other packages
+        val controllers = listOf(controller1, controller2, controller3, controller4)
+        whenever(controller1.getPackageName()).thenReturn("PKG_1")
+        whenever(controller2.getPackageName()).thenReturn("PKG_2")
+        whenever(controller3.getPackageName()).thenReturn("PKG_3")
+        whenever(controller4.getPackageName()).thenReturn("PKG_4")
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun doNotFilterDuringKeyMigration() {
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        // GIVEN a loaded event
+        filter.onMediaDataLoaded(key1, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the local session but it is a key migration
+        filter.onMediaDataLoaded(key2, key1, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is fired
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false))
+    }
+
+    @Test
+    fun filterAfterKeyMigration() {
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        // GIVEN a loaded event
+        filter.onMediaDataLoaded(key1, null, mediaData1)
+        filter.onMediaDataLoaded(key1, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // GIVEN that the keys have been migrated
+        filter.onMediaDataLoaded(key2, key1, mediaData1)
+        filter.onMediaDataLoaded(key2, key1, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(key2, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is filtered
+        verify(mediaListener, never())
+            .onMediaDataLoaded(
+                eq(key2),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(key2, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is fired
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
new file mode 100644
index 0000000..344dffa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+
+private const val KEY = "KEY"
+private const val PACKAGE = "PKG"
+private const val SESSION_KEY = "SESSION_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
+
+private fun <T> anyObject(): T {
+    return Mockito.anyObject<T>()
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MediaTimeoutListenerTest : SysuiTestCase() {
+
+    @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
+    @Mock private lateinit var mediaController: MediaController
+    @Mock private lateinit var logger: MediaTimeoutLogger
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    private lateinit var executor: FakeExecutor
+    @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
+    @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
+    @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
+    @Captor
+    private lateinit var dozingCallbackCaptor:
+        ArgumentCaptor<StatusBarStateController.StateListener>
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+    private lateinit var metadataBuilder: MediaMetadata.Builder
+    private lateinit var playbackBuilder: PlaybackState.Builder
+    private lateinit var session: MediaSession
+    private lateinit var mediaData: MediaData
+    private lateinit var resumeData: MediaData
+    private lateinit var mediaTimeoutListener: MediaTimeoutListener
+    private var clock = FakeSystemClock()
+
+    @Before
+    fun setup() {
+        `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
+        executor = FakeExecutor(clock)
+        mediaTimeoutListener =
+            MediaTimeoutListener(
+                mediaControllerFactory,
+                executor,
+                logger,
+                statusBarStateController,
+                clock
+            )
+        mediaTimeoutListener.timeoutCallback = timeoutCallback
+        mediaTimeoutListener.stateCallback = stateCallback
+
+        // Create a media session and notification for testing.
+        metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
+        playbackBuilder =
+            PlaybackState.Builder().apply {
+                setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+                setActions(PlaybackState.ACTION_PLAY)
+            }
+        session =
+            MediaSession(context, SESSION_KEY).apply {
+                setMetadata(metadataBuilder.build())
+                setPlaybackState(playbackBuilder.build())
+            }
+        session.setActive(true)
+
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(
+                app = PACKAGE,
+                packageName = PACKAGE,
+                token = session.sessionToken
+            )
+
+        resumeData = mediaData.copy(token = null, active = false, resumption = true)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_registersPlaybackListener() {
+        val playingState = mock(android.media.session.PlaybackState::class.java)
+        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+
+        `when`(mediaController.playbackState).thenReturn(playingState)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
+        verify(logger).logPlaybackState(eq(KEY), eq(playingState))
+
+        // Ignores if same key
+        clearInvocations(mediaController)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
+        verify(mediaController, never()).registerCallback(anyObject())
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
+        assertThat(executor.numPending()).isEqualTo(1)
+        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
+        verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false))
+        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnMediaDataRemoved_unregistersPlaybackListener() {
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        verify(mediaController).unregisterCallback(anyObject())
+
+        // Ignores duplicate requests
+        clearInvocations(mediaController)
+        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        verify(mediaController, never()).unregisterCallback(anyObject())
+    }
+
+    @Test
+    fun testOnMediaDataRemoved_clearsTimeout() {
+        // GIVEN media that is paused
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        assertThat(executor.numPending()).isEqualTo(1)
+        // WHEN the media is removed
+        mediaTimeoutListener.onMediaDataRemoved(KEY)
+        // THEN the timeout runnable is cancelled
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_migratesKeys() {
+        val newKey = "NEWKEY"
+        // From not playing
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        clearInvocations(mediaController)
+
+        // To playing
+        val playingState = mock(android.media.session.PlaybackState::class.java)
+        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+        `when`(mediaController.playbackState).thenReturn(playingState)
+        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+        verify(mediaController).unregisterCallback(anyObject())
+        verify(mediaController).registerCallback(anyObject())
+        verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true))
+
+        // Enqueues callback
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
+        val newKey = "NEWKEY"
+        // From not playing
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        clearInvocations(mediaController)
+
+        // Migrate, still not playing
+        val playingState = mock(android.media.session.PlaybackState::class.java)
+        `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
+        `when`(mediaController.playbackState).thenReturn(playingState)
+        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+
+        // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
+        // is another scheduled
+        assertThat(executor.numPending()).isEqualTo(1)
+        verify(logger).logUpdateListener(eq(newKey), eq(false))
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() {
+        // Assuming we're registered
+        testOnMediaDataLoaded_registersPlaybackListener()
+
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
+        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_cancelsTimeout_whenResumed() {
+        // Assuming we have a pending timeout
+        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
+
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
+        )
+        assertThat(executor.numPending()).isEqualTo(0)
+        verify(logger).logTimeoutCancelled(eq(KEY), any())
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_reusesTimeout_whenNotPlaying() {
+        // Assuming we have a pending timeout
+        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
+
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
+        )
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testTimeoutCallback_invokedIfTimeout() {
+        // Assuming we're have a pending timeout
+        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
+
+        with(executor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        verify(timeoutCallback).invoke(eq(KEY), eq(true))
+    }
+
+    @Test
+    fun testIsTimedOut() {
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
+    }
+
+    @Test
+    fun testOnSessionDestroyed_active_clearsTimeout() {
+        // GIVEN media that is paused
+        val mediaPaused = mediaData.copy(isPlaying = false)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
+        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // WHEN the session is destroyed
+        mediaCallbackCaptor.value.onSessionDestroyed()
+
+        // THEN the controller is unregistered and timeout run
+        verify(mediaController).unregisterCallback(anyObject())
+        assertThat(executor.numPending()).isEqualTo(0)
+        verify(logger).logSessionDestroyed(eq(KEY))
+    }
+
+    @Test
+    fun testSessionDestroyed_thenRestarts_resetsTimeout() {
+        // Assuming we have previously destroyed the session
+        testOnSessionDestroyed_active_clearsTimeout()
+
+        // WHEN we get an update with media playing
+        val playingState = mock(android.media.session.PlaybackState::class.java)
+        `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
+        `when`(mediaController.playbackState).thenReturn(playingState)
+        val mediaPlaying = mediaData.copy(isPlaying = true)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
+
+        // THEN the timeout runnable will update the state
+        assertThat(executor.numPending()).isEqualTo(1)
+        with(executor) {
+            advanceClockToNext()
+            runAllReady()
+        }
+        verify(timeoutCallback).invoke(eq(KEY), eq(false))
+        verify(logger).logReuseListener(eq(KEY))
+    }
+
+    @Test
+    fun testOnSessionDestroyed_resume_continuesTimeout() {
+        // GIVEN resume media with session info
+        val resumeWithSession = resumeData.copy(token = session.sessionToken)
+        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
+        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // WHEN the session is destroyed
+        mediaCallbackCaptor.value.onSessionDestroyed()
+
+        // THEN the controller is unregistered, but the timeout is still scheduled
+        verify(mediaController).unregisterCallback(anyObject())
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
+        // WHEN a regular media is loaded
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+
+        // AND it turns into a resume control
+        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+
+        // THEN we register a timeout
+        assertThat(executor.numPending()).isEqualTo(1)
+        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
+        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
+        // WHEN regular media is paused
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        `when`(mediaController.playbackState).thenReturn(pausedState)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // AND it turns into a resume control
+        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+
+        // THEN we update the timeout length
+        assertThat(executor.numPending()).isEqualTo(1)
+        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
+        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_resumption_registersTimeout() {
+        // WHEN a resume media is loaded
+        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+
+        // THEN we register a timeout
+        assertThat(executor.numPending()).isEqualTo(1)
+        verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
+        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
+        // WHEN we have a resume control
+        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+
+        // AND that media is resumed
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        `when`(mediaController.playbackState).thenReturn(playingState)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
+
+        // THEN the timeout length is changed to a regular media control
+        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+    }
+
+    @Test
+    fun testOnMediaDataRemoved_resume_timeoutCancelled() {
+        // WHEN we have a resume control
+        testOnMediaDataLoaded_resumption_registersTimeout()
+        // AND the media is removed
+        mediaTimeoutListener.onMediaDataRemoved(PACKAGE)
+
+        // THEN the timeout runnable is cancelled
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
+        // Load media data once
+        val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When media data is loaded again, with different actions
+        val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
+        loadMediaDataWithPlaybackState(playingState)
+
+        // Then the callback is not invoked
+        verify(stateCallback, never()).invoke(eq(KEY), any())
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
+        // Load media data once
+        val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When the playback state changes, and has different actions
+        val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+
+        // Then the callback is invoked
+        verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
+        val customOne =
+            PlaybackState.CustomAction.Builder(
+                    "ACTION_1",
+                    "custom action 1",
+                    android.R.drawable.ic_media_ff
+                )
+                .build()
+        val pausedState =
+            PlaybackState.Builder()
+                .setActions(PlaybackState.ACTION_PAUSE)
+                .addCustomAction(customOne)
+                .build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When the playback state actions change
+        val customTwo =
+            PlaybackState.CustomAction.Builder(
+                    "ACTION_2",
+                    "custom action 2",
+                    android.R.drawable.ic_media_rew
+                )
+                .build()
+        val pausedStateTwoActions =
+            PlaybackState.Builder()
+                .setActions(PlaybackState.ACTION_PAUSE)
+                .addCustomAction(customOne)
+                .addCustomAction(customTwo)
+                .build()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+
+        // Then the callback is invoked
+        verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_sameActions_noCallback() {
+        val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
+        loadMediaDataWithPlaybackState(stateWithActions)
+
+        // When the playback state updates with the same actions
+        mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
+
+        // Then the callback is not invoked again
+        verify(stateCallback, never()).invoke(eq(KEY), any())
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
+        val actionName = "custom action"
+        val actionIcon = android.R.drawable.ic_media_ff
+        val customOne =
+            PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
+        val stateOne =
+            PlaybackState.Builder()
+                .setActions(PlaybackState.ACTION_PAUSE)
+                .addCustomAction(customOne)
+                .build()
+        loadMediaDataWithPlaybackState(stateOne)
+
+        // When the playback state is updated, but has the same actions
+        val customTwo =
+            PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
+        val stateTwo =
+            PlaybackState.Builder()
+                .setActions(PlaybackState.ACTION_PAUSE)
+                .addCustomAction(customTwo)
+                .build()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
+
+        // Then the callback is not invoked
+        verify(stateCallback, never()).invoke(eq(KEY), any())
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
+        // Load media data in paused state
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When media data is loaded again but playing
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
+        loadMediaDataWithPlaybackState(playingState)
+
+        // Then the callback is not invoked
+        verify(stateCallback, never()).invoke(eq(KEY), any())
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
+        // Load media data in paused state
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When the playback state changes to playing
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+
+        // Then the callback is invoked
+        verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
+    }
+
+    @Test
+    fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
+        // Load media data in paused state
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        loadMediaDataWithPlaybackState(pausedState)
+
+        // When the playback state is updated, but still not playing
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+
+        // Then the callback is not invoked
+        verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
+    }
+
+    @Test
+    fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
+        // When paused media is loaded
+        testOnMediaDataLoaded_registersPlaybackListener()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
+        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+
+        // And we doze past the scheduled timeout
+        val time = clock.currentTimeMillis()
+        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // Then when no longer dozing, the timeout runs immediately
+        dozingCallbackCaptor.value.onDozingChanged(false)
+        verify(timeoutCallback).invoke(eq(KEY), eq(true))
+        verify(logger).logTimeout(eq(KEY))
+
+        // and cancel any later scheduled timeout
+        verify(logger).logTimeoutCancelled(eq(KEY), any())
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() {
+        // When paused media is loaded
+        val time = clock.currentTimeMillis()
+        clock.setElapsedRealtime(time)
+        testOnMediaDataLoaded_registersPlaybackListener()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
+        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+
+        // And we doze, but not past the scheduled timeout
+        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // Then when no longer dozing, the timeout remains scheduled
+        dozingCallbackCaptor.value.onDozingChanged(false)
+        verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
+    private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
+        `when`(mediaController.playbackState).thenReturn(state)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
new file mode 100644
index 0000000..84fdfd7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.media.MediaDescription
+import android.media.session.MediaSession
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.isNotNull
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val OLD_KEY = "RESUME_KEY"
+private const val PACKAGE_NAME = "PKG"
+private const val CLASS_NAME = "CLASS"
+private const val TITLE = "TITLE"
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaResumeListenerTest : SysuiTestCase() {
+
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var device: MediaDeviceData
+    @Mock private lateinit var token: MediaSession.Token
+    @Mock private lateinit var tunerService: TunerService
+    @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory
+    @Mock private lateinit var resumeBrowser: ResumeMediaBrowser
+    @Mock private lateinit var sharedPrefs: SharedPreferences
+    @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var pendingIntent: PendingIntent
+    @Mock private lateinit var dumpManager: DumpManager
+
+    @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
+    @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable>
+    @Captor lateinit var componentCaptor: ArgumentCaptor<String>
+
+    private lateinit var executor: FakeExecutor
+    private lateinit var data: MediaData
+    private lateinit var resumeListener: MediaResumeListener
+    private val clock = FakeSystemClock()
+
+    private var originalQsSetting =
+        Settings.Global.getInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            1
+        )
+    private var originalResumeSetting =
+        Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        Settings.Global.putInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            1
+        )
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
+
+        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+            .thenReturn(resumeBrowser)
+
+        // resume components are stored in sharedpreferences
+        whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
+            .thenReturn(sharedPrefs)
+        whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
+        whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
+        whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
+        whenever(mockContext.packageManager).thenReturn(context.packageManager)
+        whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
+
+        executor = FakeExecutor(clock)
+        resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
+        resumeListener.setManager(mediaDataManager)
+        mediaDataManager.addListener(resumeListener)
+
+        data =
+            MediaTestUtils.emptyMediaData.copy(
+                song = TITLE,
+                packageName = PACKAGE_NAME,
+                token = token
+            )
+    }
+
+    @After
+    fun tearDown() {
+        Settings.Global.putInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            originalQsSetting
+        )
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RESUME,
+            originalResumeSetting
+        )
+    }
+
+    @Test
+    fun testWhenNoResumption_doesNothing() {
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+        // When listener is created, we do NOT register a user change listener
+        val listener =
+            MediaResumeListener(
+                context,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
+        listener.setManager(mediaDataManager)
+        verify(broadcastDispatcher, never())
+            .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any())
+
+        // When data is loaded, we do NOT execute or update anything
+        listener.onMediaDataLoaded(KEY, OLD_KEY, data)
+        assertThat(executor.numPending()).isEqualTo(0)
+        verify(mediaDataManager, never()).setResumeAction(any(), any())
+    }
+
+    @Test
+    fun testOnLoad_checksForResume_noService() {
+        // When media data is loaded that has not been checked yet, and does not have a MBS
+        resumeListener.onMediaDataLoaded(KEY, null, data)
+
+        // Then we report back to the manager
+        verify(mediaDataManager).setResumeAction(KEY, null)
+    }
+
+    @Test
+    fun testOnLoad_checksForResume_badService() {
+        setUpMbsWithValidResolveInfo()
+
+        whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
+
+        // When media data is loaded that has not been checked yet, and does not have a MBS
+        resumeListener.onMediaDataLoaded(KEY, null, data)
+        executor.runAllReady()
+
+        // Then we report back to the manager
+        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
+    }
+
+    @Test
+    fun testOnLoad_localCast_doesNotCheck() {
+        // When media data is loaded that has not been checked yet, and is a local cast
+        val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCast)
+
+        // Then we do not take action
+        verify(mediaDataManager, never()).setResumeAction(any(), any())
+    }
+
+    @Test
+    fun testOnload_remoteCast_doesNotCheck() {
+        // When media data is loaded that has not been checked yet, and is a remote cast
+        val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
+        resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
+
+        // Then we do not take action
+        verify(mediaDataManager, never()).setResumeAction(any(), any())
+    }
+
+    @Test
+    fun testOnLoad_checksForResume_hasService() {
+        setUpMbsWithValidResolveInfo()
+
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.testConnection()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // When media data is loaded that has not been checked yet, and does have a MBS
+        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+        // Then we test whether the service is valid
+        executor.runAllReady()
+        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
+        verify(resumeBrowser).testConnection()
+
+        // And since it is, we send info to the manager
+        verify(mediaDataManager).setResumeAction(eq(KEY), isNotNull())
+
+        // But we do not tell it to add new controls
+        verify(mediaDataManager, never())
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testOnLoad_doesNotCheckAgain() {
+        // When a media data is loaded that has been checked already
+        var dataCopy = data.copy(hasCheckedForResume = true)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+        // Then we should not check it again
+        verify(resumeBrowser, never()).testConnection()
+        verify(mediaDataManager, never()).setResumeAction(KEY, null)
+    }
+
+    @Test
+    fun testOnUserUnlock_loadsTracks() {
+        // Set up mock service to successfully find valid media
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.token).thenReturn(token)
+        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // Make sure broadcast receiver is registered
+        resumeListener.setManager(mediaDataManager)
+        verify(broadcastDispatcher)
+            .registerReceiver(
+                eq(resumeListener.userChangeReceiver),
+                any(),
+                any(),
+                any(),
+                anyInt(),
+                any()
+            )
+
+        // When we get an unlock event
+        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
+        resumeListener.userChangeReceiver.onReceive(context, intent)
+
+        // Then we should attempt to find recent media for each saved component
+        verify(resumeBrowser, times(3)).findRecentMedia()
+
+        // Then since the mock service found media, the manager should be informed
+        verify(mediaDataManager, times(3))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+    }
+
+    @Test
+    fun testGetResumeAction_restarts() {
+        setUpMbsWithValidResolveInfo()
+
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.testConnection()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // When media data is loaded that has not been checked yet, and does have a MBS
+        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+        // Then we test whether the service is valid and set the resume action
+        executor.runAllReady()
+        verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
+        verify(resumeBrowser).testConnection()
+        verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
+
+        // When the resume action is run
+        actionCaptor.value.run()
+
+        // Then we call restart
+        verify(resumeBrowser).restart()
+    }
+
+    @Test
+    fun testOnUserUnlock_missingTime_saves() {
+        val currentTime = clock.currentTimeMillis()
+
+        // When resume components without a last played time are loaded
+        testOnUserUnlock_loadsTracks()
+
+        // Then we save an update with the current time
+        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
+        componentCaptor.value
+            .split(ResumeMediaBrowser.DELIMITER.toRegex())
+            .dropLastWhile { it.isEmpty() }
+            .forEach {
+                val result = it.split("/")
+                assertThat(result.size).isEqualTo(3)
+                assertThat(result[2].toLong()).isEqualTo(currentTime)
+            }
+        verify(sharedPrefsEditor, times(1)).apply()
+    }
+
+    @Test
+    fun testLoadComponents_recentlyPlayed_adds() {
+        // Set up browser to return successfully
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.token).thenReturn(token)
+        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // Set up shared preferences to have a component with a recent lastplayed time
+        val lastPlayed = clock.currentTimeMillis()
+        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
+        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
+        resumeListener.setManager(mediaDataManager)
+        mediaDataManager.addListener(resumeListener)
+
+        // When we load a component that was played recently
+        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
+        resumeListener.userChangeReceiver.onReceive(mockContext, intent)
+
+        // We add its resume controls
+        verify(resumeBrowser, times(1)).findRecentMedia()
+        verify(mediaDataManager, times(1))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+    }
+
+    @Test
+    fun testLoadComponents_old_ignores() {
+        // Set up shared preferences to have a component with an old lastplayed time
+        val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
+        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
+        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
+        resumeListener.setManager(mediaDataManager)
+        mediaDataManager.addListener(resumeListener)
+
+        // When we load a component that is not recent
+        val intent = Intent(Intent.ACTION_USER_UNLOCKED)
+        resumeListener.userChangeReceiver.onReceive(mockContext, intent)
+
+        // We do not try to add resume controls
+        verify(resumeBrowser, times(0)).findRecentMedia()
+        verify(mediaDataManager, times(0))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun testOnLoad_hasService_updatesLastPlayed() {
+        // Set up browser to return successfully
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.token).thenReturn(token)
+        whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+        whenever(resumeBrowser.findRecentMedia()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // Set up shared preferences to have a component with a lastplayed time
+        val currentTime = clock.currentTimeMillis()
+        val lastPlayed = currentTime - 1000
+        val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
+        whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
+        resumeListener.setManager(mediaDataManager)
+        mediaDataManager.addListener(resumeListener)
+
+        // When media data is loaded that has not been checked yet, and does have a MBS
+        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+        // Then we store the new lastPlayed time
+        verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
+        componentCaptor.value
+            .split(ResumeMediaBrowser.DELIMITER.toRegex())
+            .dropLastWhile { it.isEmpty() }
+            .forEach {
+                val result = it.split("/")
+                assertThat(result.size).isEqualTo(3)
+                assertThat(result[2].toLong()).isEqualTo(currentTime)
+            }
+        verify(sharedPrefsEditor, times(1)).apply()
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_newKeyDifferent_oldMediaBrowserDisconnected() {
+        setUpMbsWithValidResolveInfo()
+
+        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
+        executor.runAllReady()
+
+        resumeListener.onMediaDataLoaded(key = "newKey", oldKey = KEY, data)
+
+        verify(resumeBrowser).disconnect()
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_updatingResumptionListError_mediaBrowserDisconnected() {
+        setUpMbsWithValidResolveInfo()
+
+        // Set up mocks to return with an error
+        whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
+
+        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
+        executor.runAllReady()
+
+        // Ensure we disconnect the browser
+        verify(resumeBrowser).disconnect()
+    }
+
+    @Test
+    fun testOnMediaDataLoaded_trackAdded_mediaBrowserDisconnected() {
+        setUpMbsWithValidResolveInfo()
+
+        // Set up mocks to return with a track added
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.testConnection()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
+        executor.runAllReady()
+
+        // Ensure we disconnect the browser
+        verify(resumeBrowser).disconnect()
+    }
+
+    @Test
+    fun testResumeAction_oldMediaBrowserDisconnected() {
+        setUpMbsWithValidResolveInfo()
+
+        val description = MediaDescription.Builder().setTitle(TITLE).build()
+        val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+        whenever(resumeBrowser.testConnection()).thenAnswer {
+            callbackCaptor.value.addTrack(description, component, resumeBrowser)
+        }
+
+        // Load media data that will require us to get the resume action
+        val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+        executor.runAllReady()
+        verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
+
+        // Set up our factory to return a new browser so we can verify we disconnected the old one
+        val newResumeBrowser = mock(ResumeMediaBrowser::class.java)
+        whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+            .thenReturn(newResumeBrowser)
+
+        // When the resume action is run
+        actionCaptor.value.run()
+
+        // Then we disconnect the old one
+        verify(resumeBrowser).disconnect()
+    }
+
+    /** Sets up mocks to successfully find a MBS that returns valid media. */
+    private fun setUpMbsWithValidResolveInfo() {
+        val pm = mock(PackageManager::class.java)
+        whenever(mockContext.packageManager).thenReturn(pm)
+        val resolveInfo = ResolveInfo()
+        val serviceInfo = ServiceInfo()
+        serviceInfo.packageName = PACKAGE_NAME
+        resolveInfo.serviceInfo = serviceInfo
+        resolveInfo.serviceInfo.name = CLASS_NAME
+        val resumeInfo = listOf(resolveInfo)
+        whenever(pm.queryIntentServices(any(), anyInt())).thenReturn(resumeInfo)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
new file mode 100644
index 0000000..a04cfd4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume
+
+import android.content.ComponentName
+import android.content.Context
+import android.media.MediaDescription
+import android.media.browse.MediaBrowser
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.service.media.MediaBrowserService
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private const val PACKAGE_NAME = "package"
+private const val CLASS_NAME = "class"
+private const val TITLE = "song title"
+private const val MEDIA_ID = "media ID"
+private const val ROOT = "media browser root"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class ResumeMediaBrowserTest : SysuiTestCase() {
+
+    private lateinit var resumeBrowser: TestableResumeMediaBrowser
+    private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+    private val description =
+        MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build()
+
+    @Mock lateinit var callback: ResumeMediaBrowser.Callback
+    @Mock lateinit var listener: MediaResumeListener
+    @Mock lateinit var service: MediaBrowserService
+    @Mock lateinit var logger: ResumeMediaBrowserLogger
+    @Mock lateinit var browserFactory: MediaBrowserFactory
+    @Mock lateinit var browser: MediaBrowser
+    @Mock lateinit var token: MediaSession.Token
+    @Mock lateinit var mediaController: MediaController
+    @Mock lateinit var transportControls: MediaController.TransportControls
+
+    @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
+    @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
+    @Captor lateinit var mediaControllerCallback: ArgumentCaptor<MediaController.Callback>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(browserFactory.create(any(), capture(connectionCallback), any()))
+            .thenReturn(browser)
+
+        whenever(mediaController.transportControls).thenReturn(transportControls)
+        whenever(mediaController.sessionToken).thenReturn(token)
+
+        resumeBrowser =
+            TestableResumeMediaBrowser(
+                context,
+                callback,
+                component,
+                browserFactory,
+                logger,
+                mediaController
+            )
+    }
+
+    @Test
+    fun testConnection_connectionFails_callsOnError() {
+        // When testConnection cannot connect to the service
+        setupBrowserFailed()
+        resumeBrowser.testConnection()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testConnection_connects_onConnected() {
+        // When testConnection can connect to the service
+        setupBrowserConnection()
+        resumeBrowser.testConnection()
+
+        // Then it calls onConnected
+        verify(callback).onConnected()
+    }
+
+    @Test
+    fun testConnection_noValidMedia_error() {
+        // When testConnection can connect to the service, and does not find valid media
+        setupBrowserConnectionNoResults()
+        resumeBrowser.testConnection()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testConnection_hasValidMedia_addTrack() {
+        // When testConnection can connect to the service, and finds valid media
+        setupBrowserConnectionValidMedia()
+        resumeBrowser.testConnection()
+
+        // Then it calls addTrack
+        verify(callback).onConnected()
+        verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+    }
+
+    @Test
+    fun testConnection_thenSessionDestroyed_disconnects() {
+        // When testConnection is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.testConnection()
+        verify(mediaController).registerCallback(mediaControllerCallback.capture())
+        reset(browser)
+
+        // And a sessionDestroyed event is triggered
+        mediaControllerCallback.value.onSessionDestroyed()
+
+        // Then we disconnect the browser and unregister the callback
+        verify(browser).disconnect()
+        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
+    }
+
+    @Test
+    fun testConnection_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When testConnection can connect to the service
+        setupBrowserConnection()
+        resumeBrowser.testConnection()
+
+        // And testConnection is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.testConnection()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_connectionFails_error() {
+        // When findRecentMedia is called and we cannot connect
+        setupBrowserFailed()
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_noRoot_error() {
+        // When findRecentMedia is called and does not get a valid root
+        setupBrowserConnection()
+        whenever(browser.getRoot()).thenReturn(null)
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_connects_onConnected() {
+        // When findRecentMedia is called and we connect
+        setupBrowserConnection()
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls onConnected
+        verify(callback).onConnected()
+    }
+
+    @Test
+    fun testFindRecentMedia_thenSessionDestroyed_disconnects() {
+        // When findRecentMedia is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.findRecentMedia()
+        verify(mediaController).registerCallback(mediaControllerCallback.capture())
+        reset(browser)
+
+        // And a sessionDestroyed event is triggered
+        mediaControllerCallback.value.onSessionDestroyed()
+
+        // Then we disconnect the browser and unregister the callback
+        verify(browser).disconnect()
+        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
+    }
+
+    @Test
+    fun testFindRecentMedia_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When findRecentMedia is called and we connect
+        setupBrowserConnection()
+        resumeBrowser.findRecentMedia()
+
+        // And findRecentMedia is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.findRecentMedia()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_noChildren_error() {
+        // When findRecentMedia is called and we connect, but do not get any results
+        setupBrowserConnectionNoResults()
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_notPlayable_error() {
+        // When findRecentMedia is called and we connect, but do not get a playable child
+        setupBrowserConnectionNotPlayable()
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testFindRecentMedia_hasValidMedia_addTrack() {
+        // When findRecentMedia is called and we can connect and get playable media
+        setupBrowserConnectionValidMedia()
+        resumeBrowser.findRecentMedia()
+
+        // Then it calls addTrack
+        verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+    }
+
+    @Test
+    fun testRestart_connectionFails_error() {
+        // When restart is called and we cannot connect
+        setupBrowserFailed()
+        resumeBrowser.restart()
+
+        // Then it calls onError and disconnects
+        verify(callback).onError()
+        verify(browser).disconnect()
+    }
+
+    @Test
+    fun testRestart_connects() {
+        // When restart is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.restart()
+        verify(callback).onConnected()
+
+        // Then it creates a new controller and sends play command
+        verify(transportControls).prepare()
+        verify(transportControls).play()
+    }
+
+    @Test
+    fun testRestart_thenSessionDestroyed_disconnects() {
+        // When restart is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.restart()
+        verify(mediaController).registerCallback(mediaControllerCallback.capture())
+        reset(browser)
+
+        // And a sessionDestroyed event is triggered
+        mediaControllerCallback.value.onSessionDestroyed()
+
+        // Then we disconnect the browser and unregister the callback
+        verify(browser).disconnect()
+        verify(mediaController).unregisterCallback(mediaControllerCallback.value)
+    }
+
+    @Test
+    fun testRestart_calledTwice_oldBrowserDisconnected() {
+        val oldBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(oldBrowser)
+
+        // When restart is called and we connect successfully
+        setupBrowserConnection()
+        resumeBrowser.restart()
+
+        // And restart is called again
+        val newBrowser = mock<MediaBrowser>()
+        whenever(browserFactory.create(any(), any(), any())).thenReturn(newBrowser)
+        resumeBrowser.restart()
+
+        // Then we disconnect the old browser
+        verify(oldBrowser).disconnect()
+    }
+
+    /** Helper function to mock a failed connection */
+    private fun setupBrowserFailed() {
+        whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
+    }
+
+    /** Helper function to mock a successful connection only */
+    private fun setupBrowserConnection() {
+        whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
+        whenever(browser.isConnected()).thenReturn(true)
+        whenever(browser.getRoot()).thenReturn(ROOT)
+        whenever(browser.sessionToken).thenReturn(token)
+    }
+
+    /** Helper function to mock a successful connection, but no media results */
+    private fun setupBrowserConnectionNoResults() {
+        setupBrowserConnection()
+        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+            subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
+        }
+    }
+
+    /** Helper function to mock a successful connection, but no playable results */
+    private fun setupBrowserConnectionNotPlayable() {
+        setupBrowserConnection()
+
+        val child = MediaBrowser.MediaItem(description, 0)
+
+        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+            subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+        }
+    }
+
+    /** Helper function to mock a successful connection with playable media */
+    private fun setupBrowserConnectionValidMedia() {
+        setupBrowserConnection()
+
+        val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
+
+        whenever(browser.serviceComponent).thenReturn(component)
+        whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+            subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+        }
+    }
+
+    /** Override so media controller use is testable */
+    private class TestableResumeMediaBrowser(
+        context: Context,
+        callback: Callback,
+        componentName: ComponentName,
+        browserFactory: MediaBrowserFactory,
+        logger: ResumeMediaBrowserLogger,
+        private val fakeController: MediaController
+    ) : ResumeMediaBrowser(context, callback, componentName, browserFactory, logger) {
+
+        override fun createMediaController(token: MediaSession.Token): MediaController {
+            return fakeController
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
new file mode 100644
index 0000000..99f56b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.media.controls.ui
+
+import android.graphics.drawable.Animatable2
+import android.graphics.drawable.Drawable
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AnimationBindHandlerTest : SysuiTestCase() {
+
+    private interface Callback : () -> Unit
+    private abstract class AnimatedDrawable : Drawable(), Animatable2
+    private lateinit var handler: AnimationBindHandler
+
+    @Mock private lateinit var animatable: AnimatedDrawable
+    @Mock private lateinit var animatable2: AnimatedDrawable
+    @Mock private lateinit var callback: Callback
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        handler = AnimationBindHandler()
+    }
+
+    @After fun tearDown() {}
+
+    @Test
+    fun registerNoAnimations_executeCallbackImmediately() {
+        handler.tryExecute(callback)
+        verify(callback).invoke()
+    }
+
+    @Test
+    fun registerStoppedAnimations_executeCallbackImmediately() {
+        whenever(animatable.isRunning).thenReturn(false)
+        whenever(animatable2.isRunning).thenReturn(false)
+
+        handler.tryExecute(callback)
+        verify(callback).invoke()
+    }
+
+    @Test
+    fun registerRunningAnimations_executeCallbackDelayed() {
+        whenever(animatable.isRunning).thenReturn(true)
+        whenever(animatable2.isRunning).thenReturn(true)
+
+        handler.tryRegister(animatable)
+        handler.tryRegister(animatable2)
+        handler.tryExecute(callback)
+
+        verify(callback, never()).invoke()
+
+        whenever(animatable.isRunning).thenReturn(false)
+        handler.onAnimationEnd(animatable)
+        verify(callback, never()).invoke()
+
+        whenever(animatable2.isRunning).thenReturn(false)
+        handler.onAnimationEnd(animatable2)
+        verify(callback, times(1)).invoke()
+    }
+
+    @Test
+    fun repeatedEndCallback_executeSingleCallback() {
+        whenever(animatable.isRunning).thenReturn(true)
+
+        handler.tryRegister(animatable)
+        handler.tryExecute(callback)
+
+        verify(callback, never()).invoke()
+
+        whenever(animatable.isRunning).thenReturn(false)
+        handler.onAnimationEnd(animatable)
+        handler.onAnimationEnd(animatable)
+        handler.onAnimationEnd(animatable)
+        verify(callback, times(1)).invoke()
+    }
+
+    @Test
+    fun registerUnregister_executeImmediately() {
+        whenever(animatable.isRunning).thenReturn(true)
+
+        handler.tryRegister(animatable)
+        handler.unregisterAll()
+        handler.tryExecute(callback)
+
+        verify(callback).invoke()
+    }
+
+    @Test
+    fun updateRebindId_returnsAsExpected() {
+        // Previous or current call is null, returns true
+        assertTrue(handler.updateRebindId(null))
+        assertTrue(handler.updateRebindId(null))
+        assertTrue(handler.updateRebindId(null))
+        assertTrue(handler.updateRebindId(10))
+        assertTrue(handler.updateRebindId(null))
+        assertTrue(handler.updateRebindId(20))
+
+        // Different integer from prevoius, returns true
+        assertTrue(handler.updateRebindId(10))
+        assertTrue(handler.updateRebindId(20))
+
+        // Matches previous call, returns false
+        assertFalse(handler.updateRebindId(20))
+        assertFalse(handler.updateRebindId(20))
+        assertTrue(handler.updateRebindId(10))
+        assertFalse(handler.updateRebindId(10))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
new file mode 100644
index 0000000..a943746
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.media.controls.ui
+
+import android.animation.ValueAnimator
+import android.graphics.Color
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController
+import junit.framework.Assert.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+private const val DEFAULT_COLOR = Color.RED
+private const val TARGET_COLOR = Color.BLUE
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class ColorSchemeTransitionTest : SysuiTestCase() {
+
+    private interface ExtractCB : (ColorScheme) -> Int
+    private interface ApplyCB : (Int) -> Unit
+    private lateinit var colorTransition: AnimatingColorTransition
+    private lateinit var colorSchemeTransition: ColorSchemeTransition
+
+    @Mock private lateinit var mockAnimatingTransition: AnimatingColorTransition
+    @Mock private lateinit var valueAnimator: ValueAnimator
+    @Mock private lateinit var colorScheme: ColorScheme
+    @Mock private lateinit var extractColor: ExtractCB
+    @Mock private lateinit var applyColor: ApplyCB
+
+    private lateinit var animatingColorTransitionFactory: AnimatingColorTransitionFactory
+    @Mock private lateinit var mediaViewHolder: MediaViewHolder
+    @Mock private lateinit var gutsViewHolder: GutsViewHolder
+    @Mock private lateinit var multiRippleController: MultiRippleController
+    @Mock private lateinit var turbulenceNoiseController: TurbulenceNoiseController
+
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        whenever(mediaViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
+        animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition }
+        whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
+
+        colorSchemeTransition =
+            ColorSchemeTransition(
+                context,
+                mediaViewHolder,
+                multiRippleController,
+                turbulenceNoiseController,
+                animatingColorTransitionFactory
+            )
+
+        colorTransition =
+            object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) {
+                override fun buildAnimator(): ValueAnimator {
+                    return valueAnimator
+                }
+            }
+    }
+
+    @After fun tearDown() {}
+
+    @Test
+    fun testColorTransition_nullColorScheme_keepsDefault() {
+        colorTransition.updateColorScheme(null)
+        verify(applyColor, times(1)).invoke(DEFAULT_COLOR)
+        verify(valueAnimator, never()).start()
+        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
+        assertEquals(DEFAULT_COLOR, colorTransition.targetColor)
+    }
+
+    @Test
+    fun testColorTransition_newColor_startsAnimation() {
+        colorTransition.updateColorScheme(colorScheme)
+        verify(applyColor, times(1)).invoke(DEFAULT_COLOR)
+        verify(valueAnimator, times(1)).start()
+        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
+        assertEquals(TARGET_COLOR, colorTransition.targetColor)
+    }
+
+    @Test
+    fun testColorTransition_sameColor_noAnimation() {
+        whenever(extractColor.invoke(colorScheme)).thenReturn(DEFAULT_COLOR)
+        colorTransition.updateColorScheme(colorScheme)
+        verify(valueAnimator, never()).start()
+        assertEquals(DEFAULT_COLOR, colorTransition.sourceColor)
+        assertEquals(DEFAULT_COLOR, colorTransition.targetColor)
+    }
+
+    @Test
+    fun testColorTransition_colorAnimation_startValues() {
+        val expectedColor = DEFAULT_COLOR
+        whenever(valueAnimator.animatedFraction).thenReturn(0f)
+        colorTransition.updateColorScheme(colorScheme)
+        colorTransition.onAnimationUpdate(valueAnimator)
+
+        assertEquals(expectedColor, colorTransition.currentColor)
+        assertEquals(expectedColor, colorTransition.sourceColor)
+        verify(applyColor, times(2)).invoke(expectedColor) // applied once in constructor
+    }
+
+    @Test
+    fun testColorTransition_colorAnimation_endValues() {
+        val expectedColor = TARGET_COLOR
+        whenever(valueAnimator.animatedFraction).thenReturn(1f)
+        colorTransition.updateColorScheme(colorScheme)
+        colorTransition.onAnimationUpdate(valueAnimator)
+
+        assertEquals(expectedColor, colorTransition.currentColor)
+        assertEquals(expectedColor, colorTransition.targetColor)
+        verify(applyColor).invoke(expectedColor)
+    }
+
+    @Test
+    fun testColorTransition_colorAnimation_interpolatedMidpoint() {
+        val expectedColor = Color.rgb(186, 0, 186)
+        whenever(valueAnimator.animatedFraction).thenReturn(0.5f)
+        colorTransition.updateColorScheme(colorScheme)
+        colorTransition.onAnimationUpdate(valueAnimator)
+
+        assertEquals(expectedColor, colorTransition.currentColor)
+        verify(applyColor).invoke(expectedColor)
+    }
+
+    @Test
+    fun testColorSchemeTransition_update() {
+        colorSchemeTransition.updateColorScheme(colorScheme)
+        verify(mockAnimatingTransition, times(8)).updateColorScheme(colorScheme)
+        verify(gutsViewHolder).colorScheme = colorScheme
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
new file mode 100644
index 0000000..2026006
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.provider.Settings
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.FrameLayout
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.stack.MediaContainerView
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class KeyguardMediaControllerTest : SysuiTestCase() {
+
+    @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var configurationController: ConfigurationController
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
+    private val hostView = UniqueObjectHostView(context)
+    private val settings = FakeSettings()
+    private lateinit var keyguardMediaController: KeyguardMediaController
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
+
+    @Before
+    fun setup() {
+        // default state is positive, media should show up
+        whenever(mediaHost.visible).thenReturn(true)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(mediaHost.hostView).thenReturn(hostView)
+        hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
+        keyguardMediaController =
+            KeyguardMediaController(
+                mediaHost,
+                bypassController,
+                statusBarStateController,
+                context,
+                settings,
+                fakeHandler,
+                configurationController,
+            )
+        keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
+        keyguardMediaController.useSplitShade = false
+    }
+
+    @Test
+    fun testHiddenWhenHostIsHidden() {
+        whenever(mediaHost.visible).thenReturn(false)
+
+        keyguardMediaController.refreshMediaPosition()
+
+        assertThat(mediaContainerView.visibility).isEqualTo(GONE)
+    }
+
+    @Test
+    fun testVisibleOnKeyguardOrFullScreenUserSwitcher() {
+        testStateVisibility(StatusBarState.SHADE, GONE)
+        testStateVisibility(StatusBarState.SHADE_LOCKED, GONE)
+        testStateVisibility(StatusBarState.KEYGUARD, VISIBLE)
+    }
+
+    private fun testStateVisibility(state: Int, visibility: Int) {
+        whenever(statusBarStateController.state).thenReturn(state)
+        keyguardMediaController.refreshMediaPosition()
+        assertThat(mediaContainerView.visibility).isEqualTo(visibility)
+    }
+
+    @Test
+    fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0)
+
+        keyguardMediaController.refreshMediaPosition()
+
+        assertThat(mediaContainerView.visibility).isEqualTo(GONE)
+    }
+
+    @Test
+    fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
+
+        keyguardMediaController.refreshMediaPosition()
+
+        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
+    }
+
+    @Test
+    fun testActivatesSplitShadeContainerInSplitShadeMode() {
+        val splitShadeContainer = FrameLayout(context)
+        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
+        keyguardMediaController.useSplitShade = true
+
+        assertThat(splitShadeContainer.visibility).isEqualTo(VISIBLE)
+    }
+
+    @Test
+    fun testActivatesSinglePaneContainerInSinglePaneMode() {
+        val splitShadeContainer = FrameLayout(context)
+        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
+
+        assertThat(splitShadeContainer.visibility).isEqualTo(GONE)
+        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
+    }
+
+    @Test
+    fun testAttachedToSplitShade() {
+        val splitShadeContainer = FrameLayout(context)
+        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
+        keyguardMediaController.useSplitShade = true
+
+        assertTrue(
+            "HostView wasn't attached to the split pane container",
+            splitShadeContainer.childCount == 1
+        )
+    }
+
+    @Test
+    fun testAttachedToSinglePane() {
+        val splitShadeContainer = FrameLayout(context)
+        keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
+
+        assertTrue(
+            "HostView wasn't attached to the single pane container",
+            mediaContainerView.childCount == 1
+        )
+    }
+
+    @Test
+    fun testMediaHost_expandedPlayer() {
+        verify(mediaHost).expansion = MediaHostState.EXPANDED
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
new file mode 100644
index 0000000..6ca34e0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -0,0 +1,665 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.app.PendingIntent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import javax.inject.Provider
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private val DATA = MediaTestUtils.emptyMediaData
+
+private val SMARTSPACE_KEY = "smartspace"
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaCarouselControllerTest : SysuiTestCase() {
+
+    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
+    @Mock lateinit var panel: MediaControlPanel
+    @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
+    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock lateinit var mediaHostState: MediaHostState
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock @Main private lateinit var executor: DelayableExecutor
+    @Mock lateinit var mediaDataManager: MediaDataManager
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var falsingManager: FalsingManager
+    @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var logger: MediaUiEventLogger
+    @Mock lateinit var debugLogger: MediaCarouselControllerLogger
+    @Mock lateinit var mediaViewController: MediaViewController
+    @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
+    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+    @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
+
+    private val clock = FakeSystemClock()
+    private lateinit var mediaCarouselController: MediaCarouselController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mediaCarouselController =
+            MediaCarouselController(
+                context,
+                mediaControlPanelFactory,
+                visualStabilityProvider,
+                mediaHostStatesManager,
+                activityStarter,
+                clock,
+                executor,
+                mediaDataManager,
+                configurationController,
+                falsingCollector,
+                falsingManager,
+                dumpManager,
+                logger,
+                debugLogger
+            )
+        verify(mediaDataManager).addListener(capture(listener))
+        verify(visualStabilityProvider)
+            .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
+        whenever(mediaControlPanelFactory.get()).thenReturn(panel)
+        whenever(panel.mediaViewController).thenReturn(mediaViewController)
+        whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
+        MediaPlayerData.clear()
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testPlayerOrdering() {
+        // Test values: key, data, last active time
+        val playingLocal =
+            Triple(
+                "playing local",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = false
+                ),
+                4500L
+            )
+
+        val playingCast =
+            Triple(
+                "playing cast",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val pausedLocal =
+            Triple(
+                "paused local",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = false
+                ),
+                1000L
+            )
+
+        val pausedCast =
+            Triple(
+                "paused cast",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
+                    resumption = false
+                ),
+                2000L
+            )
+
+        val playingRcn =
+            Triple(
+                "playing RCN",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val pausedRcn =
+            Triple(
+                "paused RCN",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val active =
+            Triple(
+                "active",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                250L
+            )
+
+        val resume1 =
+            Triple(
+                "resume 1",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                500L
+            )
+
+        val resume2 =
+            Triple(
+                "resume 2",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                1000L
+            )
+
+        val activeMoreRecent =
+            Triple(
+                "active more recent",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true,
+                    lastActive = 2L
+                ),
+                1000L
+            )
+
+        val activeLessRecent =
+            Triple(
+                "active less recent",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true,
+                    lastActive = 1L
+                ),
+                1000L
+            )
+        // Expected ordering for media players:
+        // Actively playing local sessions
+        // Actively playing cast sessions
+        // Paused local and cast sessions, by last active
+        // RCNs
+        // Resume controls, by last active
+
+        val expected =
+            listOf(
+                playingLocal,
+                playingCast,
+                pausedCast,
+                pausedLocal,
+                playingRcn,
+                pausedRcn,
+                active,
+                resume2,
+                resume1
+            )
+
+        expected.forEach {
+            clock.setCurrentTimeMillis(it.third)
+            MediaPlayerData.addMediaPlayer(
+                it.first,
+                it.second.copy(notificationKey = it.first),
+                panel,
+                clock,
+                isSsReactivated = false
+            )
+        }
+
+        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+
+        for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testOrderWithSmartspace_prioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        MediaPlayerData.addMediaRecommendation(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            panel,
+            true,
+            clock
+        )
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
+            true
+        )
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+        assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testOrderWithSmartspace_notPrioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is not prioritized
+        MediaPlayerData.addMediaRecommendation(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            panel,
+            false,
+            clock
+        )
+
+        // Then it should be shown at the end of the carousel's active entries
+        val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
+        assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
+        testPlayerOrdering()
+        // playing paused player
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            "paused local",
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            "playing local",
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = true
+            )
+        )
+
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+        // paused player order should stays the same in visibleMediaPLayer map.
+        // paused player order should be first in mediaPlayer map.
+        assertEquals(
+            MediaPlayerData.visiblePlayerKeys().elementAt(3),
+            MediaPlayerData.playerKeys().elementAt(0)
+        )
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testSwipeDismiss_logged() {
+        mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
+
+        verify(logger).logSwipeDismiss()
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testSettingsButton_logged() {
+        mediaCarouselController.settingsButton.callOnClick()
+
+        verify(logger).logCarouselSettings()
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testLocationChangeQs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QS,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testLocationChangeQqs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QQS,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testLocationChangeLockscreen_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_LOCKSCREEN,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testLocationChangeDream_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testRecommendationRemoved_logged() {
+        val packageName = "smartspace package"
+        val instanceId = InstanceId.fakeInstanceId(123)
+
+        val smartspaceData =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
+        mediaCarouselController.removePlayer(SMARTSPACE_KEY)
+
+        verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testMediaLoaded_ScrollToActivePlayer() {
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        // adding a media recommendation card.
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            false
+        )
+        mediaCarouselController.shouldScrollToKey = true
+        // switching between media players.
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            "playing local",
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = true
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            "paused local",
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
+            false
+        )
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+
+        var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
+        assertEquals(
+            playerIndex,
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+        assertEquals(playerIndex, 0)
+
+        // Replaying the same media player one more time.
+        // And check that the card stays in its position.
+        mediaCarouselController.shouldScrollToKey = true
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false,
+                packageName = "PACKAGE_NAME"
+            )
+        )
+        playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
+        assertEquals(playerIndex, 0)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
+        var result = false
+        mediaCarouselController.updateHostVisibility = { result = true }
+
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
+        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
+
+        assertEquals(true, result)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
+        var result = false
+        mediaCarouselController.updateHostVisibility = { result = true }
+
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
+        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
+        assertEquals(false, result)
+
+        visualStabilityCallback.value.onReorderingAllowed()
+        assertEquals(true, result)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testGetCurrentVisibleMediaContentIntent() {
+        val clickIntent1 = mock(PendingIntent::class.java)
+        val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
+        clock.setCurrentTimeMillis(player1.third)
+        MediaPlayerData.addMediaPlayer(
+            player1.first,
+            player1.second.copy(notificationKey = player1.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
+
+        val clickIntent2 = mock(PendingIntent::class.java)
+        val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L)
+        clock.setCurrentTimeMillis(player2.third)
+        MediaPlayerData.addMediaPlayer(
+            player2.first,
+            player2.second.copy(notificationKey = player2.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the front because it was active more recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+
+        val clickIntent3 = mock(PendingIntent::class.java)
+        val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L)
+        clock.setCurrentTimeMillis(player3.third)
+        MediaPlayerData.addMediaPlayer(
+            player3.first,
+            player3.second.copy(notificationKey = player3.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the end because it was active less recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+    }
+
+    @Ignore("b/253229241")
+    @Test
+    fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
+        val delta = 0.0001F
+        val paginationSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        val paginationSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        whenever(mediaHostStatesManager.mediaHostStates)
+            .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
+        whenever(mediaHostState.visible).thenReturn(true)
+        mediaCarouselController.currentEndLocation = LOCATION_QS
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
new file mode 100644
index 0000000..761773b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -0,0 +1,2090 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.app.PendingIntent
+import android.app.smartspace.SmartspaceAction
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.drawable.Animatable2
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.Icon
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.TransitionDrawable
+import android.media.MediaMetadata
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
+import android.os.Bundle
+import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.Interpolator
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.constraintlayout.widget.Barrier
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.lifecycle.LiveData
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.BroadcastDialogController
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaAction
+import com.android.systemui.media.controls.models.player.MediaButton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.player.SeekBarViewModel
+import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.surfaceeffects.ripple.MultiRippleView
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.KotlinArgumentCaptor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import junit.framework.Assert.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.anyString
+import org.mockito.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.junit.MockitoJUnit
+
+private const val KEY = "TEST_KEY"
+private const val PACKAGE = "PKG"
+private const val ARTIST = "ARTIST"
+private const val TITLE = "TITLE"
+private const val DEVICE_NAME = "DEVICE_NAME"
+private const val SESSION_KEY = "SESSION_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
+private const val DISABLED_DEVICE_NAME = "DISABLED_DEVICE_NAME"
+private const val REC_APP_NAME = "REC APP NAME"
+private const val APP_NAME = "APP_NAME"
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class MediaControlPanelTest : SysuiTestCase() {
+
+    private lateinit var player: MediaControlPanel
+
+    private lateinit var bgExecutor: FakeExecutor
+    private lateinit var mainExecutor: FakeExecutor
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var broadcastSender: BroadcastSender
+
+    @Mock private lateinit var gutsViewHolder: GutsViewHolder
+    @Mock private lateinit var viewHolder: MediaViewHolder
+    @Mock private lateinit var view: TransitionLayout
+    @Mock private lateinit var seekBarViewModel: SeekBarViewModel
+    @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
+    @Mock private lateinit var mediaViewController: MediaViewController
+    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var expandedSet: ConstraintSet
+    @Mock private lateinit var collapsedSet: ConstraintSet
+    @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
+    @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var transitionParent: ViewGroup
+    @Mock private lateinit var broadcastDialogController: BroadcastDialogController
+    private lateinit var appIcon: ImageView
+    @Mock private lateinit var albumView: ImageView
+    private lateinit var titleText: TextView
+    private lateinit var artistText: TextView
+    private lateinit var seamless: ViewGroup
+    private lateinit var seamlessButton: View
+    @Mock private lateinit var seamlessBackground: RippleDrawable
+    private lateinit var seamlessIcon: ImageView
+    private lateinit var seamlessText: TextView
+    private lateinit var seekBar: SeekBar
+    private lateinit var action0: ImageButton
+    private lateinit var action1: ImageButton
+    private lateinit var action2: ImageButton
+    private lateinit var action3: ImageButton
+    private lateinit var action4: ImageButton
+    private lateinit var actionPlayPause: ImageButton
+    private lateinit var actionNext: ImageButton
+    private lateinit var actionPrev: ImageButton
+    private lateinit var scrubbingElapsedTimeView: TextView
+    private lateinit var scrubbingTotalTimeView: TextView
+    private lateinit var actionsTopBarrier: Barrier
+    @Mock private lateinit var gutsText: TextView
+    @Mock private lateinit var mockAnimator: AnimatorSet
+    private lateinit var settings: ImageButton
+    private lateinit var cancel: View
+    private lateinit var cancelText: TextView
+    private lateinit var dismiss: FrameLayout
+    private lateinit var dismissText: TextView
+    private lateinit var multiRippleView: MultiRippleView
+    private lateinit var turbulenceNoiseView: TurbulenceNoiseView
+
+    private lateinit var session: MediaSession
+    private lateinit var device: MediaDeviceData
+    private val disabledDevice =
+        MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false)
+    private lateinit var mediaData: MediaData
+    private val clock = FakeSystemClock()
+    @Mock private lateinit var logger: MediaUiEventLogger
+    @Mock private lateinit var instanceId: InstanceId
+    @Mock private lateinit var packageManager: PackageManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
+    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+
+    @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder
+    @Mock private lateinit var smartspaceAction: SmartspaceAction
+    private lateinit var smartspaceData: SmartspaceMediaData
+    @Mock private lateinit var coverContainer1: ViewGroup
+    @Mock private lateinit var coverContainer2: ViewGroup
+    @Mock private lateinit var coverContainer3: ViewGroup
+    private lateinit var coverItem1: ImageView
+    private lateinit var coverItem2: ImageView
+    private lateinit var coverItem3: ImageView
+    private lateinit var recTitle1: TextView
+    private lateinit var recTitle2: TextView
+    private lateinit var recTitle3: TextView
+    private lateinit var recSubtitle1: TextView
+    private lateinit var recSubtitle2: TextView
+    private lateinit var recSubtitle3: TextView
+    private var shouldShowBroadcastButton: Boolean = false
+    private val fakeFeatureFlag =
+        FakeFeatureFlags().apply {
+            this.set(Flags.UMO_SURFACE_RIPPLE, false)
+            this.set(Flags.MEDIA_FALSING_PENALTY, true)
+        }
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        bgExecutor = FakeExecutor(FakeSystemClock())
+        mainExecutor = FakeExecutor(FakeSystemClock())
+        whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
+        whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
+
+        // Set up package manager mocks
+        val icon = context.getDrawable(R.drawable.ic_android)
+        whenever(packageManager.getApplicationIcon(anyString())).thenReturn(icon)
+        whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
+            .thenReturn(icon)
+        whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
+            .thenReturn(applicationInfo)
+        whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
+        context.setMockPackageManager(packageManager)
+
+        player =
+            object :
+                MediaControlPanel(
+                    context,
+                    bgExecutor,
+                    mainExecutor,
+                    activityStarter,
+                    broadcastSender,
+                    mediaViewController,
+                    seekBarViewModel,
+                    Lazy { mediaDataManager },
+                    mediaOutputDialogFactory,
+                    mediaCarouselController,
+                    falsingManager,
+                    clock,
+                    logger,
+                    keyguardStateController,
+                    activityIntentHelper,
+                    lockscreenUserManager,
+                    broadcastDialogController,
+                    fakeFeatureFlag
+                ) {
+                override fun loadAnimator(
+                    animId: Int,
+                    otionInterpolator: Interpolator,
+                    vararg targets: View
+                ): AnimatorSet {
+                    return mockAnimator
+                }
+            }
+
+        initGutsViewHolderMocks()
+        initMediaViewHolderMocks()
+
+        initDeviceMediaData(false, DEVICE_NAME)
+
+        // Set up recommendation view
+        initRecommendationViewHolderMocks()
+
+        // Set valid recommendation data
+        val extras = Bundle()
+        extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
+        val intent =
+            Intent().apply {
+                putExtras(extras)
+                setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            }
+        whenever(smartspaceAction.intent).thenReturn(intent)
+        whenever(smartspaceAction.extras).thenReturn(extras)
+        smartspaceData =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                packageName = PACKAGE,
+                instanceId = instanceId,
+                recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
+                cardAction = smartspaceAction
+            )
+    }
+
+    private fun initGutsViewHolderMocks() {
+        settings = ImageButton(context)
+        cancel = View(context)
+        cancelText = TextView(context)
+        dismiss = FrameLayout(context)
+        dismissText = TextView(context)
+        whenever(gutsViewHolder.gutsText).thenReturn(gutsText)
+        whenever(gutsViewHolder.settings).thenReturn(settings)
+        whenever(gutsViewHolder.cancel).thenReturn(cancel)
+        whenever(gutsViewHolder.cancelText).thenReturn(cancelText)
+        whenever(gutsViewHolder.dismiss).thenReturn(dismiss)
+        whenever(gutsViewHolder.dismissText).thenReturn(dismissText)
+    }
+
+    private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
+        device =
+            MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton)
+
+        // Create media session
+        val metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
+        val playbackBuilder =
+            PlaybackState.Builder().apply {
+                setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+                setActions(PlaybackState.ACTION_PLAY)
+            }
+        session =
+            MediaSession(context, SESSION_KEY).apply {
+                setMetadata(metadataBuilder.build())
+                setPlaybackState(playbackBuilder.build())
+            }
+        session.setActive(true)
+
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(
+                artist = ARTIST,
+                song = TITLE,
+                packageName = PACKAGE,
+                token = session.sessionToken,
+                device = device,
+                instanceId = instanceId
+            )
+    }
+
+    /** Initialize elements in media view holder */
+    private fun initMediaViewHolderMocks() {
+        whenever(seekBarViewModel.progress).thenReturn(seekBarData)
+
+        // Set up mock views for the players
+        appIcon = ImageView(context)
+        titleText = TextView(context)
+        artistText = TextView(context)
+        seamless = FrameLayout(context)
+        seamless.foreground = seamlessBackground
+        seamlessButton = View(context)
+        seamlessIcon = ImageView(context)
+        seamlessText = TextView(context)
+        seekBar = SeekBar(context).also { it.id = R.id.media_progress_bar }
+
+        action0 = ImageButton(context).also { it.setId(R.id.action0) }
+        action1 = ImageButton(context).also { it.setId(R.id.action1) }
+        action2 = ImageButton(context).also { it.setId(R.id.action2) }
+        action3 = ImageButton(context).also { it.setId(R.id.action3) }
+        action4 = ImageButton(context).also { it.setId(R.id.action4) }
+
+        actionPlayPause = ImageButton(context).also { it.setId(R.id.actionPlayPause) }
+        actionPrev = ImageButton(context).also { it.setId(R.id.actionPrev) }
+        actionNext = ImageButton(context).also { it.setId(R.id.actionNext) }
+        scrubbingElapsedTimeView =
+            TextView(context).also { it.setId(R.id.media_scrubbing_elapsed_time) }
+        scrubbingTotalTimeView =
+            TextView(context).also { it.setId(R.id.media_scrubbing_total_time) }
+
+        actionsTopBarrier =
+            Barrier(context).also {
+                it.id = R.id.media_action_barrier_top
+                it.referencedIds =
+                    intArrayOf(
+                        actionPrev.id,
+                        seekBar.id,
+                        actionNext.id,
+                        action0.id,
+                        action1.id,
+                        action2.id,
+                        action3.id,
+                        action4.id
+                    )
+            }
+
+        multiRippleView = MultiRippleView(context, null)
+        turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+        whenever(viewHolder.player).thenReturn(view)
+        whenever(viewHolder.appIcon).thenReturn(appIcon)
+        whenever(viewHolder.albumView).thenReturn(albumView)
+        whenever(albumView.foreground).thenReturn(mock(Drawable::class.java))
+        whenever(viewHolder.titleText).thenReturn(titleText)
+        whenever(viewHolder.artistText).thenReturn(artistText)
+        whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java))
+        whenever(viewHolder.seamless).thenReturn(seamless)
+        whenever(viewHolder.seamlessButton).thenReturn(seamlessButton)
+        whenever(viewHolder.seamlessIcon).thenReturn(seamlessIcon)
+        whenever(viewHolder.seamlessText).thenReturn(seamlessText)
+        whenever(viewHolder.seekBar).thenReturn(seekBar)
+        whenever(viewHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
+        whenever(viewHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
+
+        whenever(viewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
+
+        // Transition View
+        whenever(view.parent).thenReturn(transitionParent)
+        whenever(view.rootView).thenReturn(transitionParent)
+
+        // Action buttons
+        whenever(viewHolder.actionPlayPause).thenReturn(actionPlayPause)
+        whenever(viewHolder.getAction(R.id.actionPlayPause)).thenReturn(actionPlayPause)
+        whenever(viewHolder.actionNext).thenReturn(actionNext)
+        whenever(viewHolder.getAction(R.id.actionNext)).thenReturn(actionNext)
+        whenever(viewHolder.actionPrev).thenReturn(actionPrev)
+        whenever(viewHolder.getAction(R.id.actionPrev)).thenReturn(actionPrev)
+        whenever(viewHolder.action0).thenReturn(action0)
+        whenever(viewHolder.getAction(R.id.action0)).thenReturn(action0)
+        whenever(viewHolder.action1).thenReturn(action1)
+        whenever(viewHolder.getAction(R.id.action1)).thenReturn(action1)
+        whenever(viewHolder.action2).thenReturn(action2)
+        whenever(viewHolder.getAction(R.id.action2)).thenReturn(action2)
+        whenever(viewHolder.action3).thenReturn(action3)
+        whenever(viewHolder.getAction(R.id.action3)).thenReturn(action3)
+        whenever(viewHolder.action4).thenReturn(action4)
+        whenever(viewHolder.getAction(R.id.action4)).thenReturn(action4)
+
+        whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
+
+        whenever(viewHolder.multiRippleView).thenReturn(multiRippleView)
+        whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView)
+    }
+
+    /** Initialize elements for the recommendation view holder */
+    private fun initRecommendationViewHolderMocks() {
+        recTitle1 = TextView(context)
+        recTitle2 = TextView(context)
+        recTitle3 = TextView(context)
+        recSubtitle1 = TextView(context)
+        recSubtitle2 = TextView(context)
+        recSubtitle3 = TextView(context)
+
+        whenever(recommendationViewHolder.recommendations).thenReturn(view)
+        whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
+
+        // Add a recommendation item
+        coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) }
+        coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) }
+        coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) }
+
+        whenever(recommendationViewHolder.mediaCoverItems)
+            .thenReturn(listOf(coverItem1, coverItem2, coverItem3))
+        whenever(recommendationViewHolder.mediaCoverContainers)
+            .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
+        whenever(recommendationViewHolder.mediaTitles)
+            .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
+        whenever(recommendationViewHolder.mediaSubtitles)
+            .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
+
+        whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
+
+        val actionIcon = Icon.createWithResource(context, R.drawable.ic_android)
+        whenever(smartspaceAction.icon).thenReturn(actionIcon)
+
+        // Needed for card and item action click
+        val mockContext = mock(Context::class.java)
+        whenever(view.context).thenReturn(mockContext)
+        whenever(coverContainer1.context).thenReturn(mockContext)
+        whenever(coverContainer2.context).thenReturn(mockContext)
+        whenever(coverContainer3.context).thenReturn(mockContext)
+    }
+
+    @After
+    fun tearDown() {
+        session.release()
+        player.onDestroy()
+    }
+
+    @Test
+    fun bindWhenUnattached() {
+        val state = mediaData.copy(token = null)
+        player.bindPlayer(state, PACKAGE)
+        assertThat(player.isPlaying()).isFalse()
+    }
+
+    @Test
+    fun bindSemanticActions() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg)
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(actionPrev.isEnabled()).isFalse()
+        assertThat(actionPrev.drawable).isNull()
+        verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+
+        assertThat(actionPlayPause.isEnabled()).isTrue()
+        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
+        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
+
+        assertThat(actionNext.isEnabled()).isTrue()
+        assertThat(actionNext.contentDescription).isEqualTo("next")
+        verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
+
+        // Called twice since these IDs are used as generic buttons
+        assertThat(action0.contentDescription).isEqualTo("custom 0")
+        assertThat(action0.isEnabled()).isFalse()
+        verify(collapsedSet, times(2)).setVisibility(R.id.action0, ConstraintSet.GONE)
+
+        assertThat(action1.contentDescription).isEqualTo("custom 1")
+        assertThat(action1.isEnabled()).isFalse()
+        verify(collapsedSet, times(2)).setVisibility(R.id.action1, ConstraintSet.GONE)
+
+        // Verify generic buttons are hidden
+        verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.action2, ConstraintSet.GONE)
+
+        verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
+
+        verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bindSemanticActions_reservedPrev() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
+
+        // Setup button state: no prev or next button and their slots reserved
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = null,
+                prevOrCustom = null,
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg),
+                false,
+                true
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(actionPrev.isEnabled()).isFalse()
+        assertThat(actionPrev.drawable).isNull()
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.INVISIBLE)
+
+        assertThat(actionNext.isEnabled()).isFalse()
+        assertThat(actionNext.drawable).isNull()
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bindSemanticActions_reservedNext() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
+
+        // Setup button state: no prev or next button and their slots reserved
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = null,
+                prevOrCustom = null,
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg),
+                true,
+                false
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(actionPrev.isEnabled()).isFalse()
+        assertThat(actionPrev.drawable).isNull()
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+
+        assertThat(actionNext.isEnabled()).isFalse()
+        assertThat(actionNext.drawable).isNull()
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.INVISIBLE)
+    }
+
+    @Test
+    fun bindAlbumView_testHardwareAfterAttach() {
+        player.attachPlayer(viewHolder)
+
+        verify(albumView).setLayerType(View.LAYER_TYPE_HARDWARE, null)
+    }
+
+    @Test
+    fun bindAlbumView_artUsesResource() {
+        val albumArt = Icon.createWithResource(context, R.drawable.ic_android)
+        val state = mediaData.copy(artwork = albumArt)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+
+        verify(albumView).setImageDrawable(any(Drawable::class.java))
+    }
+
+    @Test
+    fun bindAlbumView_setAfterExecutors() {
+        val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bmp)
+        canvas.drawColor(Color.RED)
+        val albumArt = Icon.createWithBitmap(bmp)
+        val state = mediaData.copy(artwork = albumArt)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+
+        verify(albumView).setImageDrawable(any(Drawable::class.java))
+    }
+
+    @Test
+    fun bindAlbumView_bitmapInLaterStates_setAfterExecutors() {
+        val redBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+        val redCanvas = Canvas(redBmp)
+        redCanvas.drawColor(Color.RED)
+        val redArt = Icon.createWithBitmap(redBmp)
+
+        val greenBmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+        val greenCanvas = Canvas(greenBmp)
+        greenCanvas.drawColor(Color.GREEN)
+        val greenArt = Icon.createWithBitmap(greenBmp)
+
+        val state0 = mediaData.copy(artwork = null)
+        val state1 = mediaData.copy(artwork = redArt)
+        val state2 = mediaData.copy(artwork = redArt)
+        val state3 = mediaData.copy(artwork = greenArt)
+        player.attachPlayer(viewHolder)
+
+        // First binding sets (empty) drawable
+        player.bindPlayer(state0, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+        verify(albumView).setImageDrawable(any(Drawable::class.java))
+
+        // Run Metadata update so that later states don't update
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator, times(2)).addListener(captor.capture())
+        captor.value.onAnimationEnd(mockAnimator)
+        assertThat(titleText.getText()).isEqualTo(TITLE)
+        assertThat(artistText.getText()).isEqualTo(ARTIST)
+
+        // Second binding sets transition drawable
+        player.bindPlayer(state1, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+        val drawableCaptor = argumentCaptor<Drawable>()
+        verify(albumView, times(2)).setImageDrawable(drawableCaptor.capture())
+        assertTrue(drawableCaptor.allValues[1] is TransitionDrawable)
+
+        // Third binding doesn't run transition or update background
+        player.bindPlayer(state2, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+        verify(albumView, times(2)).setImageDrawable(any(Drawable::class.java))
+
+        // Fourth binding to new image runs transition due to color scheme change
+        player.bindPlayer(state3, PACKAGE)
+        bgExecutor.runAllReady()
+        mainExecutor.runAllReady()
+        verify(albumView, times(3)).setImageDrawable(any(Drawable::class.java))
+    }
+
+    @Test
+    fun bind_seekBarDisabled_hasActions_seekBarVisibilityIsSetToInvisible() {
+        useRealConstraintSets()
+
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", null),
+                nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
+    }
+
+    @Test
+    fun bind_seekBarDisabled_noActions_seekBarVisibilityIsSetToGone() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bind_seekBarEnabled_seekBarVisible() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        player.bindPlayer(state, PACKAGE)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun seekBarChangesToEnabledAfterBind_seekBarChangesToVisible() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun seekBarChangesToDisabledAfterBind_noActions_seekBarChangesToGone() {
+        useRealConstraintSets()
+
+        val state = mediaData.copy(semanticActions = MediaButton())
+
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+        player.bindPlayer(state, PACKAGE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun seekBarChangesToDisabledAfterBind_hasActions_seekBarChangesToInvisible() {
+        useRealConstraintSets()
+
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null))
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        getEnabledChangeListener().onEnabledChanged(enabled = true)
+        player.bindPlayer(state, PACKAGE)
+
+        getEnabledChangeListener().onEnabledChanged(enabled = false)
+
+        assertThat(expandedSet.getVisibility(seekBar.id)).isEqualTo(ConstraintSet.INVISIBLE)
+    }
+
+    @Test
+    fun bind_notScrubbing_scrubbingViewsGone() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noSemanticActions_viewsNotChanged() {
+        val state = mediaData.copy(semanticActions = null)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        val listener = getScrubbingChangeListener()
+
+        listener.onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        verify(expandedSet, never()).setVisibility(eq(R.id.actionPrev), anyInt())
+        verify(expandedSet, never()).setVisibility(eq(R.id.actionNext), anyInt())
+        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_elapsed_time), anyInt())
+        verify(expandedSet, never()).setVisibility(eq(R.id.media_scrubbing_total_time), anyInt())
+    }
+
+    @Test
+    fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null))
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        verify(expandedSet).setVisibility(R.id.actionNext, View.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null)
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        verify(expandedSet).setVisibility(R.id.actionPrev, View.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, View.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, View.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should show the scrubbing times and hide prev+next
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
+        val state = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        getScrubbingChangeListener().onScrubbingChanged(true)
+        mainExecutor.runAllReady()
+        reset(expandedSet)
+
+        getScrubbingChangeListener().onScrubbingChanged(false)
+        mainExecutor.runAllReady()
+
+        // Only in expanded, we should hide the scrubbing times and show prev+next
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_elapsed_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.media_scrubbing_total_time, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.VISIBLE)
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun bindNotificationActions() {
+        val icon = context.getDrawable(android.R.drawable.ic_media_play)
+        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
+        val actions =
+            listOf(
+                MediaAction(icon, Runnable {}, "previous", bg),
+                MediaAction(icon, Runnable {}, "play", bg),
+                MediaAction(icon, null, "next", bg),
+                MediaAction(icon, null, "custom 0", bg),
+                MediaAction(icon, Runnable {}, "custom 1", bg)
+            )
+        val state =
+            mediaData.copy(
+                actions = actions,
+                actionsToShowInCompact = listOf(1, 2),
+                semanticActions = null
+            )
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+
+        // Verify semantic actions are hidden
+        verify(collapsedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionPrev, ConstraintSet.GONE)
+
+        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.GONE)
+
+        verify(collapsedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
+        verify(expandedSet).setVisibility(R.id.actionNext, ConstraintSet.GONE)
+
+        // Generic actions all enabled
+        assertThat(action0.contentDescription).isEqualTo("previous")
+        assertThat(action0.isEnabled()).isTrue()
+        verify(collapsedSet).setVisibility(R.id.action0, ConstraintSet.GONE)
+
+        assertThat(action1.contentDescription).isEqualTo("play")
+        assertThat(action1.isEnabled()).isTrue()
+        verify(collapsedSet).setVisibility(R.id.action1, ConstraintSet.VISIBLE)
+
+        assertThat(action2.contentDescription).isEqualTo("next")
+        assertThat(action2.isEnabled()).isFalse()
+        verify(collapsedSet).setVisibility(R.id.action2, ConstraintSet.VISIBLE)
+
+        assertThat(action3.contentDescription).isEqualTo("custom 0")
+        assertThat(action3.isEnabled()).isFalse()
+        verify(collapsedSet).setVisibility(R.id.action3, ConstraintSet.GONE)
+
+        assertThat(action4.contentDescription).isEqualTo("custom 1")
+        assertThat(action4.isEnabled()).isTrue()
+        verify(collapsedSet).setVisibility(R.id.action4, ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bindAnimatedSemanticActions() {
+        val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
+        val mockAvd1 = mock(AnimatedVectorDrawable::class.java)
+        val mockAvd2 = mock(AnimatedVectorDrawable::class.java)
+        whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
+        whenever(mockAvd1.mutate()).thenReturn(mockAvd1)
+        whenever(mockAvd2.mutate()).thenReturn(mockAvd2)
+
+        val icon = context.getDrawable(R.drawable.ic_media_play)
+        val bg = context.getDrawable(R.drawable.ic_media_play_container)
+        val semanticActions0 =
+            MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
+        val semanticActions1 =
+            MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
+        val semanticActions2 =
+            MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
+        val state0 = mediaData.copy(semanticActions = semanticActions0)
+        val state1 = mediaData.copy(semanticActions = semanticActions1)
+        val state2 = mediaData.copy(semanticActions = semanticActions2)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state0, PACKAGE)
+
+        // Validate first binding
+        assertThat(actionPlayPause.isEnabled()).isTrue()
+        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
+        assertThat(actionPlayPause.getBackground()).isNull()
+        verify(collapsedSet).setVisibility(R.id.actionPlayPause, ConstraintSet.VISIBLE)
+        assertThat(actionPlayPause.hasOnClickListeners()).isTrue()
+
+        // Trigger animation & update mock
+        actionPlayPause.performClick()
+        verify(mockAvd0, times(1)).start()
+        whenever(mockAvd0.isRunning()).thenReturn(true)
+
+        // Validate states no longer bind
+        player.bindPlayer(state1, PACKAGE)
+        player.bindPlayer(state2, PACKAGE)
+        assertThat(actionPlayPause.contentDescription).isEqualTo("play")
+
+        // Complete animation and run callbacks
+        whenever(mockAvd0.isRunning()).thenReturn(false)
+        val captor = ArgumentCaptor.forClass(Animatable2.AnimationCallback::class.java)
+        verify(mockAvd0, times(1)).registerAnimationCallback(captor.capture())
+        verify(mockAvd1, never())
+            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd2, never())
+            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        captor.getValue().onAnimationEnd(mockAvd0)
+
+        // Validate correct state was bound
+        assertThat(actionPlayPause.contentDescription).isEqualTo("loading")
+        assertThat(actionPlayPause.getBackground()).isNull()
+        verify(mockAvd0, times(1))
+            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd1, times(1))
+            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd2, times(1))
+            .registerAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd0, times(1))
+            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd1, times(1))
+            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+        verify(mockAvd2, never())
+            .unregisterAnimationCallback(any(Animatable2.AnimationCallback::class.java))
+    }
+
+    @Test
+    fun bindText() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, PACKAGE)
+
+        // Capture animation handler
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator, times(2)).addListener(captor.capture())
+        val handler = captor.value
+
+        // Validate text views unchanged but animation started
+        assertThat(titleText.getText()).isEqualTo("")
+        assertThat(artistText.getText()).isEqualTo("")
+        verify(mockAnimator, times(1)).start()
+
+        // Binding only after animator runs
+        handler.onAnimationEnd(mockAnimator)
+        assertThat(titleText.getText()).isEqualTo(TITLE)
+        assertThat(artistText.getText()).isEqualTo(ARTIST)
+
+        // Rebinding should not trigger animation
+        player.bindPlayer(mediaData, PACKAGE)
+        verify(mockAnimator, times(2)).start()
+    }
+
+    @Test
+    fun bindTextInterrupted() {
+        val data0 = mediaData.copy(artist = "ARTIST_0")
+        val data1 = mediaData.copy(artist = "ARTIST_1")
+        val data2 = mediaData.copy(artist = "ARTIST_2")
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data0, PACKAGE)
+
+        // Capture animation handler
+        val captor = argumentCaptor<Animator.AnimatorListener>()
+        verify(mockAnimator, times(2)).addListener(captor.capture())
+        val handler = captor.value
+
+        handler.onAnimationEnd(mockAnimator)
+        assertThat(artistText.getText()).isEqualTo("ARTIST_0")
+
+        // Bind trigges new animation
+        player.bindPlayer(data1, PACKAGE)
+        verify(mockAnimator, times(3)).start()
+        whenever(mockAnimator.isRunning()).thenReturn(true)
+
+        // Rebind before animation end binds corrct data
+        player.bindPlayer(data2, PACKAGE)
+        handler.onAnimationEnd(mockAnimator)
+        assertThat(artistText.getText()).isEqualTo("ARTIST_2")
+    }
+
+    @Test
+    fun bindDevice() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, PACKAGE)
+        assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
+        assertThat(seamless.contentDescription).isEqualTo(DEVICE_NAME)
+        assertThat(seamless.isEnabled()).isTrue()
+    }
+
+    @Test
+    fun bindDisabledDevice() {
+        seamless.id = 1
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(device = disabledDevice)
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamless.isEnabled()).isFalse()
+        assertThat(seamlessText.getText()).isEqualTo(DISABLED_DEVICE_NAME)
+        assertThat(seamless.contentDescription).isEqualTo(DISABLED_DEVICE_NAME)
+    }
+
+    @Test
+    fun bindNullDevice() {
+        val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(device = null)
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamless.isEnabled()).isTrue()
+        assertThat(seamlessText.getText()).isEqualTo(fallbackString)
+        assertThat(seamless.contentDescription).isEqualTo(fallbackString)
+    }
+
+    @Test
+    fun bindDeviceWithNullName() {
+        val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(device = device.copy(name = null))
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamless.isEnabled()).isTrue()
+        assertThat(seamlessText.getText()).isEqualTo(fallbackString)
+        assertThat(seamless.contentDescription).isEqualTo(fallbackString)
+    }
+
+    @Test
+    fun bindDeviceResumptionPlayer() {
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(resumption = true)
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
+        assertThat(seamless.isEnabled()).isFalse()
+    }
+
+    @Test
+    fun bindBroadcastButton() {
+        initMediaViewHolderMocks()
+        initDeviceMediaData(true, APP_NAME)
+
+        val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
+        whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
+        val semanticActions0 =
+            MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
+        val state =
+            mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
+        assertThat(seamless.isEnabled()).isTrue()
+
+        seamless.callOnClick()
+
+        verify(logger).logOpenBroadcastDialog(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    /* ***** Guts tests for the player ***** */
+
+    @Test
+    fun player_longClick_isFalse() {
+        whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
+        player.attachPlayer(viewHolder)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController, never()).openGuts()
+        verify(mediaViewController, never()).closeGuts()
+    }
+
+    @Test
+    fun player_longClickWhenGutsClosed_gutsOpens() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, KEY)
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).setOnLongClickListener(captor.capture())
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController).openGuts()
+        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun player_longClickWhenGutsOpen_gutsCloses() {
+        player.attachPlayer(viewHolder)
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).setOnLongClickListener(captor.capture())
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController, never()).openGuts()
+        verify(mediaViewController).closeGuts(false)
+    }
+
+    @Test
+    fun player_cancelButtonClick_animation() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, KEY)
+
+        cancel.callOnClick()
+
+        verify(mediaViewController).closeGuts(false)
+    }
+
+    @Test
+    fun player_settingsButtonClick() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, KEY)
+
+        settings.callOnClick()
+        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
+
+        val captor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(activityStarter).startActivity(captor.capture(), eq(true))
+
+        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
+    }
+
+    @Test
+    fun player_dismissButtonClick() {
+        val mediaKey = "key for dismissal"
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(notificationKey = KEY)
+        player.bindPlayer(state, mediaKey)
+
+        assertThat(dismiss.isEnabled).isEqualTo(true)
+        dismiss.callOnClick()
+        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
+        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
+    }
+
+    @Test
+    fun player_dismissButtonDisabled() {
+        val mediaKey = "key for dismissal"
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(isClearable = false, notificationKey = KEY)
+        player.bindPlayer(state, mediaKey)
+
+        assertThat(dismiss.isEnabled).isEqualTo(false)
+    }
+
+    @Test
+    fun player_dismissButtonClick_notInManager() {
+        val mediaKey = "key for dismissal"
+        whenever(mediaDataManager.dismissMediaData(eq(mediaKey), anyLong())).thenReturn(false)
+
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(notificationKey = KEY)
+        player.bindPlayer(state, mediaKey)
+
+        assertThat(dismiss.isEnabled).isEqualTo(true)
+        dismiss.callOnClick()
+
+        verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
+        verify(mediaCarouselController).removePlayer(eq(mediaKey), eq(false), eq(false))
+    }
+
+    @Test
+    fun player_gutsOpen_contentDescriptionIsForGuts() {
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        player.attachPlayer(viewHolder)
+
+        val gutsTextString = "gutsText"
+        whenever(gutsText.text).thenReturn(gutsTextString)
+        player.bindPlayer(mediaData, KEY)
+
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).isEqualTo(gutsTextString)
+    }
+
+    @Test
+    fun player_gutsClosed_contentDescriptionIsForPlayer() {
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        player.attachPlayer(viewHolder)
+
+        val app = "appName"
+        player.bindPlayer(mediaData.copy(app = app), KEY)
+
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).contains(mediaData.song!!)
+        assertThat(description).contains(mediaData.artist!!)
+        assertThat(description).contains(app)
+    }
+
+    @Test
+    fun player_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
+        // Start out open
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        whenever(gutsText.text).thenReturn("gutsText")
+        player.attachPlayer(viewHolder)
+        val app = "appName"
+        player.bindPlayer(mediaData.copy(app = app), KEY)
+
+        // Update to closed by long pressing
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+        reset(viewHolder.player)
+
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        captor.value.onLongClick(viewHolder.player)
+
+        // Then content description is now the player content description
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).contains(mediaData.song!!)
+        assertThat(description).contains(mediaData.artist!!)
+        assertThat(description).contains(app)
+    }
+
+    @Test
+    fun player_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
+        // Start out closed
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        val gutsTextString = "gutsText"
+        whenever(gutsText.text).thenReturn(gutsTextString)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData.copy(app = "appName"), KEY)
+
+        // Update to open by long pressing
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+        reset(viewHolder.player)
+
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        captor.value.onLongClick(viewHolder.player)
+
+        // Then content description is now the guts content description
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).isEqualTo(gutsTextString)
+    }
+
+    /* ***** END guts tests for the player ***** */
+
+    /* ***** Guts tests for the recommendations ***** */
+
+    @Test
+    fun recommendations_longClick_isFalse() {
+        whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true)
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController, never()).openGuts()
+        verify(mediaViewController, never()).closeGuts()
+    }
+
+    @Test
+    fun recommendations_longClickWhenGutsClosed_gutsOpens() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController).openGuts()
+        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun recommendations_longClickWhenGutsOpen_gutsCloses() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+
+        captor.value.onLongClick(viewHolder.player)
+        verify(mediaViewController, never()).openGuts()
+        verify(mediaViewController).closeGuts(false)
+    }
+
+    @Test
+    fun recommendations_cancelButtonClick_animation() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        cancel.callOnClick()
+
+        verify(mediaViewController).closeGuts(false)
+    }
+
+    @Test
+    fun recommendations_settingsButtonClick() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        settings.callOnClick()
+        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
+
+        val captor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(activityStarter).startActivity(captor.capture(), eq(true))
+
+        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
+    }
+
+    @Test
+    fun recommendations_dismissButtonClick() {
+        val mediaKey = "key for dismissal"
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData.copy(targetId = mediaKey))
+
+        assertThat(dismiss.isEnabled).isEqualTo(true)
+        dismiss.callOnClick()
+        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
+        verify(mediaDataManager).dismissSmartspaceRecommendation(eq(mediaKey), anyLong())
+    }
+
+    @Test
+    fun recommendation_gutsOpen_contentDescriptionIsForGuts() {
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        player.attachRecommendation(recommendationViewHolder)
+
+        val gutsTextString = "gutsText"
+        whenever(gutsText.text).thenReturn(gutsTextString)
+        player.bindRecommendation(smartspaceData)
+
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).isEqualTo(gutsTextString)
+    }
+
+    @Test
+    fun recommendation_gutsClosed_contentDescriptionIsForPlayer() {
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        player.attachRecommendation(recommendationViewHolder)
+
+        player.bindRecommendation(smartspaceData)
+
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).contains(REC_APP_NAME)
+    }
+
+    @Test
+    fun recommendation_gutsChangesFromOpenToClosed_contentDescriptionUpdated() {
+        // Start out open
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        whenever(gutsText.text).thenReturn("gutsText")
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        // Update to closed by long pressing
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+        reset(viewHolder.player)
+
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        captor.value.onLongClick(viewHolder.player)
+
+        // Then content description is now the player content description
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).contains(REC_APP_NAME)
+    }
+
+    @Test
+    fun recommendation_gutsChangesFromClosedToOpen_contentDescriptionUpdated() {
+        // Start out closed
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+        val gutsTextString = "gutsText"
+        whenever(gutsText.text).thenReturn(gutsTextString)
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        // Update to open by long pressing
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(viewHolder.player).onLongClickListener = captor.capture()
+        reset(viewHolder.player)
+
+        whenever(mediaViewController.isGutsVisible).thenReturn(true)
+        captor.value.onLongClick(viewHolder.player)
+
+        // Then content description is now the guts content description
+        val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java)
+        verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
+        val description = descriptionCaptor.value.toString()
+
+        assertThat(description).isEqualTo(gutsTextString)
+    }
+
+    /* ***** END guts tests for the recommendations ***** */
+
+    @Test
+    fun actionPlayPauseClick_isLogged() {
+        val semanticActions =
+            MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null))
+        val data = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.actionPlayPause.callOnClick()
+        verify(logger).logTapAction(eq(R.id.actionPlayPause), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionPrevClick_isLogged() {
+        val semanticActions =
+            MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null))
+        val data = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.actionPrev.callOnClick()
+        verify(logger).logTapAction(eq(R.id.actionPrev), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionNextClick_isLogged() {
+        val semanticActions =
+            MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null))
+        val data = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.actionNext.callOnClick()
+        verify(logger).logTapAction(eq(R.id.actionNext), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionCustom0Click_isLogged() {
+        val semanticActions =
+            MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null))
+        val data = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.action0.callOnClick()
+        verify(logger).logTapAction(eq(R.id.action0), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionCustom1Click_isLogged() {
+        val semanticActions =
+            MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null))
+        val data = mediaData.copy(semanticActions = semanticActions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.action1.callOnClick()
+        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionCustom2Click_isLogged() {
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
+        val data = mediaData.copy(actions = actions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.action2.callOnClick()
+        verify(logger).logTapAction(eq(R.id.action2), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionCustom3Click_isLogged() {
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
+        val data = mediaData.copy(actions = actions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.action1.callOnClick()
+        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun actionCustom4Click_isLogged() {
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
+        val data = mediaData.copy(actions = actions)
+
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.action1.callOnClick()
+        verify(logger).logTapAction(eq(R.id.action1), anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun openOutputSwitcher_isLogged() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, KEY)
+
+        seamless.callOnClick()
+
+        verify(logger).logOpenOutputSwitcher(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun tapContentView_isLogged() {
+        val pendingIntent = mock(PendingIntent::class.java)
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        val data = mediaData.copy(clickIntent = pendingIntent)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+        verify(viewHolder.player).setOnClickListener(captor.capture())
+
+        captor.value.onClick(viewHolder.player)
+
+        verify(logger).logTapContentView(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun logSeek() {
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(mediaData, KEY)
+
+        val callback: () -> Unit = {}
+        val captor = KotlinArgumentCaptor(callback::class.java)
+        verify(seekBarViewModel).logSeek = captor.capture()
+        captor.value.invoke()
+
+        verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun tapContentView_showOverLockscreen_openActivity() {
+        // WHEN we are on lockscreen and this activity can show over lockscreen
+        whenever(keyguardStateController.isShowing).thenReturn(true)
+        whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(true)
+
+        val clickIntent = mock(Intent::class.java)
+        val pendingIntent = mock(PendingIntent::class.java)
+        whenever(pendingIntent.intent).thenReturn(clickIntent)
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        val data = mediaData.copy(clickIntent = pendingIntent)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+        verify(viewHolder.player).setOnClickListener(captor.capture())
+
+        // THEN it shows without dismissing keyguard first
+        captor.value.onClick(viewHolder.player)
+        verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true))
+    }
+
+    @Test
+    fun tapContentView_noShowOverLockscreen_dismissKeyguard() {
+        // WHEN we are on lockscreen and the activity cannot show over lockscreen
+        whenever(keyguardStateController.isShowing).thenReturn(true)
+        whenever(activityIntentHelper.wouldShowOverLockscreen(any(), any())).thenReturn(false)
+
+        val clickIntent = mock(Intent::class.java)
+        val pendingIntent = mock(PendingIntent::class.java)
+        whenever(pendingIntent.intent).thenReturn(clickIntent)
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        val data = mediaData.copy(clickIntent = pendingIntent)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+        verify(viewHolder.player).setOnClickListener(captor.capture())
+
+        // THEN keyguard has to be dismissed
+        captor.value.onClick(viewHolder.player)
+        verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
+    }
+
+    @Test
+    fun recommendation_gutsClosed_longPressOpens() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+        whenever(mediaViewController.isGutsVisible).thenReturn(false)
+
+        val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+        verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture())
+
+        captor.value.onLongClick(recommendationViewHolder.recommendations)
+        verify(mediaViewController).openGuts()
+        verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun recommendation_settingsButtonClick_isLogged() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        settings.callOnClick()
+        verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId))
+
+        val captor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(activityStarter).startActivity(captor.capture(), eq(true))
+
+        assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
+    }
+
+    @Test
+    fun recommendation_dismissButton_isLogged() {
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        dismiss.callOnClick()
+        verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun recommendation_tapOnCard_isLogged() {
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture())
+        captor.value.onClick(recommendationViewHolder.recommendations)
+
+        verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
+    fun recommendation_tapOnItem_isLogged() {
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        player.attachRecommendation(recommendationViewHolder)
+        player.bindRecommendation(smartspaceData)
+
+        verify(coverContainer1).setOnClickListener(captor.capture())
+        captor.value.onClick(recommendationViewHolder.recommendations)
+
+        verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0))
+    }
+
+    @Test
+    fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
+        player.attachRecommendation(recommendationViewHolder)
+        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                    )
+            )
+
+        player.bindRecommendation(data)
+
+        assertThat(recTitle1.text).isEqualTo("")
+        verify(mediaViewController, never()).refreshState()
+    }
+
+    @Test
+    fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
+        player.attachRecommendation(recommendationViewHolder)
+        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "empty icon 1")
+                            .setSubtitle("subtitle2")
+                            .setIcon(null)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "empty icon 2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(null)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                    )
+            )
+
+        player.bindRecommendation(data)
+
+        assertThat(recTitle1.text).isEqualTo("")
+        verify(mediaViewController, never()).refreshState()
+    }
+
+    @Test
+    fun bindRecommendation_hasTitlesAndSubtitles() {
+        player.attachRecommendation(recommendationViewHolder)
+
+        val title1 = "Title1"
+        val title2 = "Title2"
+        val title3 = "Title3"
+        val subtitle1 = "Subtitle1"
+        val subtitle2 = "Subtitle2"
+        val subtitle3 = "Subtitle3"
+        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
+
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", title1)
+                            .setSubtitle(subtitle1)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", title2)
+                            .setSubtitle(subtitle2)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", title3)
+                            .setSubtitle(subtitle3)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+        player.bindRecommendation(data)
+
+        assertThat(recTitle1.text).isEqualTo(title1)
+        assertThat(recTitle2.text).isEqualTo(title2)
+        assertThat(recTitle3.text).isEqualTo(title3)
+        assertThat(recSubtitle1.text).isEqualTo(subtitle1)
+        assertThat(recSubtitle2.text).isEqualTo(subtitle2)
+        assertThat(recSubtitle3.text).isEqualTo(subtitle3)
+    }
+
+    @Test
+    fun bindRecommendation_noTitle_subtitleNotShown() {
+        player.attachRecommendation(recommendationViewHolder)
+
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+        player.bindRecommendation(data)
+
+        assertThat(recSubtitle1.text).isEqualTo("")
+    }
+
+    @Test
+    fun bindRecommendation_someHaveTitles_allTitleViewsShown() {
+        useRealConstraintSets()
+        player.attachRecommendation(recommendationViewHolder)
+
+        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+        player.bindRecommendation(data)
+
+        assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun bindRecommendation_someHaveSubtitles_allSubtitleViewsShown() {
+        useRealConstraintSets()
+        player.attachRecommendation(recommendationViewHolder)
+
+        val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("subtitle3")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+        player.bindRecommendation(data)
+
+        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.VISIBLE)
+        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.VISIBLE)
+    }
+
+    @Test
+    fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
+        useRealConstraintSets()
+        player.attachRecommendation(recommendationViewHolder)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+
+        player.bindRecommendation(data)
+
+        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
+        useRealConstraintSets()
+        player.attachRecommendation(recommendationViewHolder)
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("subtitle1")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "")
+                            .setSubtitle("subtitle2")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "")
+                            .setSubtitle("subtitle3")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
+            )
+
+        player.bindRecommendation(data)
+
+        assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE)
+        assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE)
+    }
+
+    @Test
+    fun onButtonClick_touchRippleFlagEnabled_playsTouchRipple() {
+        fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, true)
+        val semanticActions =
+            MediaButton(
+                playOrPause =
+                    MediaAction(
+                        icon = null,
+                        action = {},
+                        contentDescription = "play",
+                        background = null
+                    )
+            )
+        val data = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.actionPlayPause.callOnClick()
+
+        assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(1)
+    }
+
+    @Test
+    fun onButtonClick_touchRippleFlagDisabled_doesNotPlayTouchRipple() {
+        fakeFeatureFlag.set(Flags.UMO_SURFACE_RIPPLE, false)
+        val semanticActions =
+            MediaButton(
+                playOrPause =
+                    MediaAction(
+                        icon = null,
+                        action = {},
+                        contentDescription = "play",
+                        background = null
+                    )
+            )
+        val data = mediaData.copy(semanticActions = semanticActions)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+
+        viewHolder.actionPlayPause.callOnClick()
+
+        assertThat(viewHolder.multiRippleView.ripples.size).isEqualTo(0)
+    }
+
+    private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
+        withArgCaptor {
+            verify(seekBarViewModel).setScrubbingChangeListener(capture())
+        }
+
+    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor {
+        verify(seekBarViewModel).setEnabledChangeListener(capture())
+    }
+
+    /**
+     * Update our test to use real ConstraintSets instead of mocks.
+     *
+     * Some item visibilities, such as the seekbar visibility, are dependent on other action's
+     * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just
+     * thrown away instead of being saved for reference later. This method sets us up to use
+     * ConstraintSets so that we do save visibility changes.
+     *
+     * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
+     */
+    private fun useRealConstraintSets() {
+        expandedSet = ConstraintSet()
+        collapsedSet = ConstraintSet()
+        whenever(mediaViewController.expandedLayout).thenReturn(expandedSet)
+        whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
+    }
+}
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
new file mode 100644
index 0000000..920801f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.graphics.Rect
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
+import com.android.systemui.dreams.DreamOverlayStateController
+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.ShadeExpansionStateManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaHierarchyManagerTest : SysuiTestCase() {
+
+    @Mock private lateinit var lockHost: MediaHost
+    @Mock private lateinit var qsHost: MediaHost
+    @Mock private lateinit var qqsHost: MediaHost
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var keyguardViewController: KeyguardViewController
+    @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
+    @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
+    @Captor
+    private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
+    @Captor
+    private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)>
+    @Captor
+    private lateinit var dreamOverlayCallback:
+        ArgumentCaptor<(DreamOverlayStateController.Callback)>
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+    private lateinit var mediaHierarchyManager: MediaHierarchyManager
+    private lateinit var mediaFrame: ViewGroup
+    private val configurationController = FakeConfigurationController()
+    private val notifPanelEvents = ShadeExpansionStateManager()
+    private val settings = FakeSettings()
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
+
+    @Before
+    fun setup() {
+        context
+            .getOrCreateTestableResources()
+            .addOverride(R.bool.config_use_split_notification_shade, false)
+        mediaFrame = FrameLayout(context)
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
+        whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
+        mediaHierarchyManager =
+            MediaHierarchyManager(
+                context,
+                statusBarStateController,
+                keyguardStateController,
+                bypassController,
+                mediaCarouselController,
+                keyguardViewController,
+                dreamOverlayStateController,
+                configurationController,
+                wakefulnessLifecycle,
+                notifPanelEvents,
+                settings,
+                fakeHandler,
+            )
+        verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
+        verify(statusBarStateController).addCallback(statusBarCallback.capture())
+        verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
+        setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
+        setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
+        setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        whenever(mediaCarouselController.mediaCarouselScrollHandler)
+            .thenReturn(mediaCarouselScrollHandler)
+        val observer = wakefullnessObserver.value
+        assertNotNull("lifecycle observer wasn't registered", observer)
+        observer.onFinishedWakingUp()
+        // We'll use the viewmanager to verify a few calls below, let's reset this.
+        clearInvocations(mediaCarouselController)
+    }
+
+    private fun setupHost(host: MediaHost, location: Int, top: Int) {
+        whenever(host.location).thenReturn(location)
+        whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top))
+        whenever(host.hostView).thenReturn(uniqueObjectHostView)
+        whenever(host.visible).thenReturn(true)
+        mediaHierarchyManager.register(host)
+    }
+
+    @Test
+    fun testHostViewSetOnRegister() {
+        val host = mediaHierarchyManager.register(lockHost)
+        verify(lockHost).hostView = eq(host)
+    }
+
+    @Test
+    fun testBlockedWhenScreenTurningOff() {
+        // Let's set it onto QS:
+        mediaHierarchyManager.qsExpansion = 1.0f
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
+        val observer = wakefullnessObserver.value
+        assertNotNull("lifecycle observer wasn't registered", observer)
+        observer.onStartedGoingToSleep()
+        clearInvocations(mediaCarouselController)
+        mediaHierarchyManager.qsExpansion = 0.0f
+        verify(mediaCarouselController, times(0))
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
+    }
+
+    @Test
+    fun testAllowedWhenNotTurningOff() {
+        // Let's set it onto QS:
+        mediaHierarchyManager.qsExpansion = 1.0f
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
+        val observer = wakefullnessObserver.value
+        assertNotNull("lifecycle observer wasn't registered", observer)
+        clearInvocations(mediaCarouselController)
+        mediaHierarchyManager.qsExpansion = 0.0f
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
+    }
+
+    @Test
+    fun testGoingToFullShade() {
+        goToLockscreen()
+
+        // Let's transition all the way to full shade
+        mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_QQS),
+                any(MediaHostState::class.java),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
+        clearInvocations(mediaCarouselController)
+
+        // Let's go back to the lock screen
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
+                any(MediaHostState::class.java),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
+
+        // Let's make sure alpha is set
+        mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
+        assertThat(mediaFrame.alpha).isNotEqualTo(1.0f)
+    }
+
+    @Test
+    fun testTransformationOnLockScreenIsFading() {
+        goToLockscreen()
+        expandQS()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun calculateTransformationType_notOnLockscreen_returnsTransition() {
+        expandQS()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockscreen_returnsTransition() {
+        goToLockscreen()
+        expandQS()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransition() {
+        enableSplitShade()
+        goToLockscreen()
+        expandQS()
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFade() {
+        enableSplitShade()
+        goToLockscreen()
+        expandQS()
+        whenever(lockHost.visible).thenReturn(false)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFade() {
+        enableSplitShade()
+        goToLockscreen()
+        goToLockedShade()
+        expandQS()
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0f)
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun testTransformationOnLockScreenToQQSisFading() {
+        goToLockscreen()
+        goToLockedShade()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun testCloseGutsRelayToCarousel() {
+        mediaHierarchyManager.closeGuts()
+
+        verify(mediaCarouselController).closeGuts()
+    }
+
+    @Test
+    fun testCloseGutsWhenDoze() {
+        statusBarCallback.value.onDozingChanged(true)
+
+        verify(mediaCarouselController).closeGuts()
+    }
+
+    @Test
+    fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() {
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
+    }
+
+    @Test
+    fun getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslation() {
+        enterGuidedTransformation()
+
+        val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
+            .isEqualTo(expectedTranslation)
+    }
+
+    @Test
+    fun isCurrentlyInGuidedTransformation_hostsVisible_returnsTrue() {
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(true)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+    }
+
+    @Test
+    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
+        notifPanelEvents.notifyExpandImmediateChange(true)
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(true)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+    }
+
+    @Test
+    fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() {
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(false)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+    }
+
+    @Test
+    fun testDream() {
+        goToDream()
+        setMediaDreamComplicationEnabled(true)
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY),
+                nullable(),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
+        clearInvocations(mediaCarouselController)
+
+        setMediaDreamComplicationEnabled(false)
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_QQS),
+                any(MediaHostState::class.java),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
+    }
+
+    private fun enableSplitShade() {
+        context
+            .getOrCreateTestableResources()
+            .addOverride(R.bool.config_use_split_notification_shade, true)
+        configurationController.notifyConfigurationChanged()
+    }
+
+    private fun goToLockscreen() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
+        statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
+        whenever(dreamOverlayStateController.isOverlayActive).thenReturn(false)
+        dreamOverlayCallback.value.onStateChanged()
+        clearInvocations(mediaCarouselController)
+    }
+
+    private fun goToLockedShade() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+        statusBarCallback.value.onStatePreChange(
+            StatusBarState.KEYGUARD,
+            StatusBarState.SHADE_LOCKED
+        )
+    }
+
+    private fun goToDream() {
+        whenever(dreamOverlayStateController.isOverlayActive).thenReturn(true)
+        dreamOverlayCallback.value.onStateChanged()
+    }
+
+    private fun setMediaDreamComplicationEnabled(enabled: Boolean) {
+        val complications = if (enabled) listOf(mock<MediaDreamComplication>()) else emptyList()
+        whenever(dreamOverlayStateController.complications).thenReturn(complications)
+        dreamOverlayCallback.value.onComplicationsChanged()
+    }
+
+    private fun expandQS() {
+        mediaHierarchyManager.qsExpansion = 1.0f
+    }
+
+    private fun enterGuidedTransformation() {
+        mediaHierarchyManager.qsExpansion = 1.0f
+        goToLockscreen()
+        mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
+    }
+
+    companion object {
+        private const val QQS_TOP = 123
+        private const val QS_TOP = 456
+        private const val LOCKSCREEN_TOP = 789
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
new file mode 100644
index 0000000..32b822d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+public class MediaPlayerDataTest : SysuiTestCase() {
+
+    @Mock private lateinit var playerIsPlaying: MediaControlPanel
+    private var systemClock: FakeSystemClock = FakeSystemClock()
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    companion object {
+        val LOCAL = MediaData.PLAYBACK_LOCAL
+        val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
+        val RESUMPTION = true
+        val PLAYING = true
+        val UNDETERMINED = null
+    }
+
+    @Before
+    fun setup() {
+        MediaPlayerData.clear()
+    }
+
+    @Test
+    fun addPlayingThenRemote() {
+        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsRemote = mock(MediaControlPanel::class.java)
+        val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsRemote,
+            playerIsRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(2)
+        assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
+    }
+
+    @Test
+    fun switchPlayersPlaying() {
+        val playerIsPlaying1 = mock(MediaControlPanel::class.java)
+        var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsPlaying2 = mock(MediaControlPanel::class.java)
+        var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying1,
+            playerIsPlaying1,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlaying2,
+            playerIsPlaying2,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
+        dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying1,
+            playerIsPlaying1,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlaying2,
+            playerIsPlaying2,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(2)
+        assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
+    }
+
+    @Test
+    fun fullOrderTest() {
+        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
+        val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
+
+        val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
+        val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
+        val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
+
+        val playerCanResume = mock(MediaControlPanel::class.java)
+        val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
+
+        val playerUndetermined = mock(MediaControlPanel::class.java)
+        val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "3",
+            dataIsStoppedAndLocal,
+            playerIsStoppedAndLocal,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "5",
+            dataIsStoppedAndRemote,
+            playerIsStoppedAndRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "4",
+            dataCanResume,
+            playerCanResume,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlayingAndRemote,
+            playerIsPlayingAndRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "6",
+            dataUndetermined,
+            playerUndetermined,
+            systemClock,
+            isSsReactivated = false
+        )
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(6)
+        assertThat(players)
+            .containsExactly(
+                playerIsPlaying,
+                playerIsPlayingAndRemote,
+                playerIsStoppedAndRemote,
+                playerIsStoppedAndLocal,
+                playerUndetermined,
+                playerCanResume
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun testMoveMediaKeysAround() {
+        val keyA = "a"
+        val keyB = "b"
+
+        val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        assertThat(MediaPlayerData.players()).hasSize(0)
+
+        MediaPlayerData.addMediaPlayer(
+            keyA,
+            data,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        assertThat(MediaPlayerData.players()).hasSize(1)
+        MediaPlayerData.addMediaPlayer(
+            keyB,
+            data,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        assertThat(MediaPlayerData.players()).hasSize(2)
+
+        MediaPlayerData.moveIfExists(keyA, keyB)
+
+        assertThat(MediaPlayerData.players()).hasSize(1)
+
+        assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
+        assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
+    }
+
+    private fun createMediaData(
+        app: String,
+        isPlaying: Boolean?,
+        location: Int,
+        resumption: Boolean
+    ) =
+        MediaTestUtils.emptyMediaData.copy(
+            app = app,
+            packageName = "package: $app",
+            playbackLocation = location,
+            resumption = resumption,
+            notificationKey = "key: $app",
+            isPlaying = isPlaying
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
new file mode 100644
index 0000000..6b76155
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.media.controls.ui
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.WidgetState
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.floatThat
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaViewControllerTest : SysuiTestCase() {
+    private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
+    private val mediaHostStatesManager = MediaHostStatesManager()
+    private val configurationController =
+        com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context)
+    private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+    private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0)
+    @Mock lateinit var logger: MediaViewLogger
+    @Mock private lateinit var mockViewState: TransitionViewState
+    @Mock private lateinit var mockCopiedState: TransitionViewState
+    @Mock private lateinit var detailWidgetState: WidgetState
+    @Mock private lateinit var controlWidgetState: WidgetState
+    @Mock private lateinit var mediaTitleWidgetState: WidgetState
+    @Mock private lateinit var mediaContainerWidgetState: WidgetState
+
+    val delta = 0.0001F
+
+    private lateinit var mediaViewController: MediaViewController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mediaViewController =
+            MediaViewController(context, configurationController, mediaHostStatesManager, logger)
+    }
+
+    @Test
+    fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() {
+        mediaViewController.attach(player, MediaViewController.TYPE.PLAYER)
+        player.measureState = TransitionViewState().apply { this.height = 100 }
+        mediaHostStateHolder.expansion = 1f
+        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        mediaHostStateHolder.measurementInput =
+            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+        // Test no squish
+        mediaHostStateHolder.squishFraction = 1f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+        // Test half squish
+        mediaHostStateHolder.squishFraction = 0.5f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+    }
+
+    @Test
+    fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() {
+        mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION)
+        recommendation.measureState = TransitionViewState().apply { this.height = 100 }
+        mediaHostStateHolder.expansion = 1f
+        val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+        mediaHostStateHolder.measurementInput =
+            MeasurementInput(widthMeasureSpec, heightMeasureSpec)
+
+        // Test no squish
+        mediaHostStateHolder.squishFraction = 1f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100)
+
+        // Test half squish
+        mediaHostStateHolder.squishFraction = 0.5f
+        assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50)
+    }
+
+    @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_progress_bar to controlWidgetState,
+                    R.id.header_artist to detailWidgetState
+                )
+            )
+
+        val detailSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, detailSquishMiddle)
+        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val detailSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+        mediaViewController.squishViewState(mockViewState, detailSquishEnd)
+        verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+        val controlSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, controlSquishMiddle)
+        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val controlSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION)
+        mediaViewController.squishViewState(mockViewState, controlSquishEnd)
+        verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+    }
+
+    @Test
+    fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() {
+        whenever(mockViewState.copy()).thenReturn(mockCopiedState)
+        whenever(mockCopiedState.widgetStates)
+            .thenReturn(
+                mutableMapOf(
+                    R.id.media_title1 to mediaTitleWidgetState,
+                    R.id.media_cover1_container to mediaContainerWidgetState
+                )
+            )
+
+        val containerSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, containerSquishMiddle)
+        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val containerSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, containerSquishEnd)
+        verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+
+        val titleSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, titleSquishMiddle)
+        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta }
+
+        val titleSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        mediaViewController.squishViewState(mockViewState, titleSquishEnd)
+        verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
new file mode 100644
index 0000000..323b781
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MetadataAnimationHandlerTest : SysuiTestCase() {
+
+    private interface Callback : () -> Unit
+    private lateinit var handler: MetadataAnimationHandler
+
+    @Mock private lateinit var enterAnimator: Animator
+    @Mock private lateinit var exitAnimator: Animator
+    @Mock private lateinit var postExitCB: Callback
+    @Mock private lateinit var postEnterCB: Callback
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Before
+    fun setUp() {
+        handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
+    }
+
+    @After fun tearDown() {}
+
+    @Test
+    fun firstBind_startsAnimationSet() {
+        val cb = { fail("Unexpected callback") }
+        handler.setNext("data-1", cb, cb)
+
+        verify(exitAnimator).start()
+    }
+
+    @Test
+    fun executeAnimationEnd_runsCallacks() {
+        // We expect this first call to only start the exit animator
+        handler.setNext("data-1", postExitCB, postEnterCB)
+        verify(exitAnimator, times(1)).start()
+        verify(enterAnimator, never()).start()
+        verify(postExitCB, never()).invoke()
+        verify(postEnterCB, never()).invoke()
+
+        // After the exit animator completes,
+        // the exit cb should run, and enter animation should start
+        handler.onAnimationEnd(exitAnimator)
+        verify(exitAnimator, times(1)).start()
+        verify(enterAnimator, times(1)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postEnterCB, never()).invoke()
+
+        // After the exit animator completes,
+        // the enter cb should run without other state changes
+        handler.onAnimationEnd(enterAnimator)
+        verify(exitAnimator, times(1)).start()
+        verify(enterAnimator, times(1)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postEnterCB, times(1)).invoke()
+    }
+
+    @Test
+    fun rebindSameData_executesFirstCallback() {
+        val postExitCB2 = mock(Callback::class.java)
+
+        handler.setNext("data-1", postExitCB, postEnterCB)
+        handler.setNext("data-1", postExitCB2, postEnterCB)
+        handler.onAnimationEnd(exitAnimator)
+
+        verify(postExitCB, times(1)).invoke()
+        verify(postExitCB2, never()).invoke()
+        verify(postEnterCB, never()).invoke()
+    }
+
+    @Test
+    fun rebindDifferentData_executesSecondCallback() {
+        val postExitCB2 = mock(Callback::class.java)
+
+        handler.setNext("data-1", postExitCB, postEnterCB)
+        handler.setNext("data-2", postExitCB2, postEnterCB)
+        handler.onAnimationEnd(exitAnimator)
+
+        verify(postExitCB, never()).invoke()
+        verify(postExitCB2, times(1)).invoke()
+        verify(postEnterCB, never()).invoke()
+    }
+
+    @Test
+    fun rebindBeforeEnterComplete_animationRestarts() {
+        val postExitCB2 = mock(Callback::class.java)
+        val postEnterCB2 = mock(Callback::class.java)
+
+        // We expect this first call to only start the exit animator
+        handler.setNext("data-1", postExitCB, postEnterCB)
+        verify(exitAnimator, times(1)).start()
+        verify(enterAnimator, never()).start()
+        verify(postExitCB, never()).invoke()
+        verify(postExitCB2, never()).invoke()
+        verify(postEnterCB, never()).invoke()
+        verify(postEnterCB2, never()).invoke()
+
+        // After the exit animator completes,
+        // the exit cb should run, and enter animation should start
+        whenever(exitAnimator.isRunning()).thenReturn(true)
+        whenever(enterAnimator.isRunning()).thenReturn(false)
+        handler.onAnimationEnd(exitAnimator)
+        verify(exitAnimator, times(1)).start()
+        verify(enterAnimator, times(1)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postExitCB2, never()).invoke()
+        verify(postEnterCB, never()).invoke()
+        verify(postEnterCB2, never()).invoke()
+
+        // Setting new data before the enter animator completes should not trigger
+        // the exit animator an additional time (since it's already running)
+        whenever(exitAnimator.isRunning()).thenReturn(false)
+        whenever(enterAnimator.isRunning()).thenReturn(true)
+        handler.setNext("data-2", postExitCB2, postEnterCB2)
+        verify(exitAnimator, times(1)).start()
+
+        // Finishing the enterAnimator should cause the exitAnimator to fire again
+        // since the data change and additional time. No enterCB should be executed.
+        handler.onAnimationEnd(enterAnimator)
+        verify(exitAnimator, times(2)).start()
+        verify(enterAnimator, times(1)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postExitCB2, never()).invoke()
+        verify(postEnterCB, never()).invoke()
+        verify(postEnterCB2, never()).invoke()
+
+        // Continuing the sequence, this triggers the enter animator an additional time
+        handler.onAnimationEnd(exitAnimator)
+        verify(exitAnimator, times(2)).start()
+        verify(enterAnimator, times(2)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postExitCB2, times(1)).invoke()
+        verify(postEnterCB, never()).invoke()
+        verify(postEnterCB2, never()).invoke()
+
+        // And finally the enter animator completes,
+        // triggering the correct postEnterCallback to fire
+        handler.onAnimationEnd(enterAnimator)
+        verify(exitAnimator, times(2)).start()
+        verify(enterAnimator, times(2)).start()
+        verify(postExitCB, times(1)).invoke()
+        verify(postExitCB2, times(1)).invoke()
+        verify(postEnterCB, never()).invoke()
+        verify(postEnterCB2, times(1)).invoke()
+    }
+
+    @Test
+    fun exitAnimationEndMultipleCalls_singleCallbackExecution() {
+        handler.setNext("data-1", postExitCB, postEnterCB)
+        handler.onAnimationEnd(exitAnimator)
+        handler.onAnimationEnd(exitAnimator)
+        handler.onAnimationEnd(exitAnimator)
+
+        verify(postExitCB, times(1)).invoke()
+    }
+
+    @Test
+    fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() {
+        handler.onAnimationEnd(enterAnimator)
+
+        verify(exitAnimator, never()).start()
+        verify(enterAnimator, never()).start()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
new file mode 100644
index 0000000..d6cff81
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.media.controls.ui
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.LightingColorFilter
+import android.graphics.Paint
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.graphics.ColorUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SquigglyProgressTest : SysuiTestCase() {
+
+    private val colorFilter = LightingColorFilter(Color.RED, Color.BLUE)
+    private val strokeWidth = 5f
+    private val alpha = 128
+    private val tint = Color.GREEN
+
+    lateinit var squigglyProgress: SquigglyProgress
+    @Mock lateinit var canvas: Canvas
+    @Captor lateinit var paintCaptor: ArgumentCaptor<Paint>
+    @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
+
+    @Before
+    fun setup() {
+        squigglyProgress = SquigglyProgress()
+        squigglyProgress.waveLength = 30f
+        squigglyProgress.lineAmplitude = 10f
+        squigglyProgress.phaseSpeed = 8f
+        squigglyProgress.strokeWidth = strokeWidth
+        squigglyProgress.bounds = Rect(0, 0, 300, 30)
+    }
+
+    @Test
+    fun testDrawPathAndLine() {
+        squigglyProgress.draw(canvas)
+
+        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
+    }
+
+    @Test
+    fun testOnLevelChanged() {
+        assertThat(squigglyProgress.setLevel(5)).isFalse()
+        squigglyProgress.animate = true
+        assertThat(squigglyProgress.setLevel(4)).isTrue()
+    }
+
+    @Test
+    fun testStrokeWidth() {
+        squigglyProgress.draw(canvas)
+
+        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
+        val (wavePaint, linePaint) = paintCaptor.getAllValues()
+
+        assertThat(wavePaint.strokeWidth).isEqualTo(strokeWidth)
+        assertThat(linePaint.strokeWidth).isEqualTo(strokeWidth)
+    }
+
+    @Test
+    fun testAlpha() {
+        squigglyProgress.alpha = alpha
+        squigglyProgress.draw(canvas)
+
+        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
+        val (wavePaint, linePaint) = paintCaptor.getAllValues()
+
+        assertThat(squigglyProgress.alpha).isEqualTo(alpha)
+        assertThat(wavePaint.alpha).isEqualTo(alpha)
+        assertThat(linePaint.alpha).isEqualTo((alpha / 255f * DISABLED_ALPHA).toInt())
+    }
+
+    @Test
+    fun testColorFilter() {
+        squigglyProgress.colorFilter = colorFilter
+        squigglyProgress.draw(canvas)
+
+        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
+        val (wavePaint, linePaint) = paintCaptor.getAllValues()
+
+        assertThat(wavePaint.colorFilter).isEqualTo(colorFilter)
+        assertThat(linePaint.colorFilter).isEqualTo(colorFilter)
+    }
+
+    @Test
+    fun testTint() {
+        squigglyProgress.setTint(tint)
+        squigglyProgress.draw(canvas)
+
+        verify(canvas, times(2)).drawPath(any(), paintCaptor.capture())
+        val (wavePaint, linePaint) = paintCaptor.getAllValues()
+
+        assertThat(wavePaint.color).isEqualTo(tint)
+        assertThat(linePaint.color).isEqualTo(ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
new file mode 100644
index 0000000..771b986
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.media.MediaOutputConstants;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MediaOutputDialogReceiverTest extends SysuiTestCase {
+
+    private MediaOutputDialogReceiver mMediaOutputDialogReceiver;
+
+    private final MediaOutputDialogFactory mMockMediaOutputDialogFactory =
+            mock(MediaOutputDialogFactory.class);
+
+    private final MediaOutputBroadcastDialogFactory mMockMediaOutputBroadcastDialogFactory =
+            mock(MediaOutputBroadcastDialogFactory.class);
+
+    @Before
+    public void setup() {
+        mMediaOutputDialogReceiver = new MediaOutputDialogReceiver(mMockMediaOutputDialogFactory,
+                mMockMediaOutputBroadcastDialogFactory);
+    }
+
+    @Test
+    public void showOutputSwitcher_ExtraPackageName_DialogFactoryCalled() {
+        Intent intent = new Intent(Intent.ACTION_SHOW_OUTPUT_SWITCHER);
+        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, times(1))
+                .create(getContext().getPackageName(), false, null);
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void showOutputSwitcher_WrongExtraKey_DialogFactoryNotCalled() {
+        Intent intent = new Intent(Intent.ACTION_SHOW_OUTPUT_SWITCHER);
+        intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void showOutputSwitcher_NoExtra_DialogFactoryNotCalled() {
+        Intent intent = new Intent(Intent.ACTION_SHOW_OUTPUT_SWITCHER);
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void launchMediaOutputDialog_ExtraPackageName_DialogFactoryCalled() {
+        Intent intent = new Intent(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG);
+        intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, times(1))
+                .create(getContext().getPackageName(), false, null);
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void launchMediaOutputDialog_WrongExtraKey_DialogFactoryNotCalled() {
+        Intent intent = new Intent(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG);
+        intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void launchMediaOutputDialog_NoExtra_DialogFactoryNotCalled() {
+        Intent intent = new Intent(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG);
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void launchMediaOutputBroadcastDialog_ExtraPackageName_BroadcastDialogFactoryCalled() {
+        Intent intent = new Intent(
+                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG);
+        intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, times(1))
+                .create(getContext().getPackageName(), false, null);
+    }
+
+    @Test
+    public void launchMediaOutputBroadcastDialog_WrongExtraKey_DialogBroadcastFactoryNotCalled() {
+        Intent intent = new Intent(
+                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG);
+        intent.putExtra("Wrong Package Name Key", getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void launchMediaOutputBroadcastDialog_NoExtra_BroadcastDialogFactoryNotCalled() {
+        Intent intent = new Intent(
+                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG);
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void unKnownAction_ExtraPackageName_FactoriesNotCalled() {
+        Intent intent = new Intent("UnKnown Action");
+        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, getContext().getPackageName());
+        intent.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, getContext().getPackageName());
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+
+    @Test
+    public void unKnownActionAnd_NoExtra_FactoriesNotCalled() {
+        Intent intent = new Intent("UnKnown Action");
+        mMediaOutputDialogReceiver.onReceive(getContext(), intent);
+
+        verify(mMockMediaOutputDialogFactory, never()).create(any(), anyBoolean(), any());
+        verify(mMockMediaOutputBroadcastDialogFactory, never()).create(any(), anyBoolean(), any());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
index 29188da..ce885c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
@@ -25,7 +25,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.util.animation.UniqueObjectHostView;
 
 import org.junit.Before;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index af53016..ed928a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -33,8 +33,8 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.DreamMediaEntryComplication;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index 1078cda..e009e86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 7c83cb7..6a4c0f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -22,6 +22,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -62,6 +65,34 @@
     }
 
     @Test
+    fun getIconFromPackageName_nullPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_invalidPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_validPackageName_returnsAppInfo() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger)
+
+        assertThat(icon)
+            .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME)))
+    }
+
+    @Test
     fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
         val iconInfo =
             MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 8c3ae3d..885cc54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
+import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -85,6 +86,10 @@
     private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
     private lateinit var receiverUiEventLogger: MediaTttReceiverUiEventLogger
+    private lateinit var fakeClock: FakeSystemClock
+    private lateinit var fakeExecutor: FakeExecutor
+    private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
+    private lateinit var fakeWakeLock: WakeLockFake
 
     @Before
     fun setUp() {
@@ -99,15 +104,22 @@
         )).thenReturn(applicationInfo)
         context.setMockPackageManager(packageManager)
 
+        fakeClock = FakeSystemClock()
+        fakeExecutor = FakeExecutor(fakeClock)
+
         uiEventLoggerFake = UiEventLoggerFake()
         receiverUiEventLogger = MediaTttReceiverUiEventLogger(uiEventLoggerFake)
 
+        fakeWakeLock = WakeLockFake()
+        fakeWakeLockBuilder = WakeLockFake.Builder(context)
+        fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
+
         controllerReceiver = MediaTttChipControllerReceiver(
             commandQueue,
             context,
             logger,
             windowManager,
-            FakeExecutor(FakeSystemClock()),
+            fakeExecutor,
             accessibilityManager,
             configurationController,
             powerManager,
@@ -115,6 +127,7 @@
             mediaTttFlags,
             receiverUiEventLogger,
             viewUtil,
+            fakeWakeLockBuilder,
         )
         controllerReceiver.start()
 
@@ -141,6 +154,7 @@
             mediaTttFlags,
             receiverUiEventLogger,
             viewUtil,
+            fakeWakeLockBuilder,
         )
         controllerReceiver.start()
 
@@ -200,6 +214,39 @@
     }
 
     @Test
+    fun commandQueueCallback_closeThenFar_wakeLockAcquiredThenReleased() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+                StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
+                routeInfo,
+                null,
+                null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isTrue()
+
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+                StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
+                routeInfo,
+                null,
+                null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
+    }
+
+    @Test
+    fun commandQueueCallback_closeThenFar_wakeLockNeverAcquired() {
+        commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
+                StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER,
+                routeInfo,
+                null,
+                null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
+    }
+
+    @Test
     fun receivesNewStateFromCommandQueue_isLogged() {
         commandQueueCallback.updateMediaTapToTransferReceiverDisplay(
             StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER,
@@ -214,7 +261,12 @@
     @Test
     fun updateView_noOverrides_usesInfoFromAppIcon() {
         controllerReceiver.displayView(
-            ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride = null)
+            ChipReceiverInfo(
+                routeInfo,
+                appIconDrawableOverride = null,
+                appNameOverride = null,
+                id = "id",
+            )
         )
 
         val view = getChipView()
@@ -227,7 +279,12 @@
         val drawableOverride = context.getDrawable(R.drawable.ic_celebration)!!
 
         controllerReceiver.displayView(
-            ChipReceiverInfo(routeInfo, drawableOverride, appNameOverride = null)
+            ChipReceiverInfo(
+                routeInfo,
+                drawableOverride,
+                appNameOverride = null,
+                id = "id",
+            )
         )
 
         val view = getChipView()
@@ -239,7 +296,12 @@
         val appNameOverride = "Sweet New App"
 
         controllerReceiver.displayView(
-            ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appNameOverride)
+            ChipReceiverInfo(
+                routeInfo,
+                appIconDrawableOverride = null,
+                appNameOverride,
+                id = "id",
+            )
         )
 
         val view = getChipView()
@@ -293,7 +355,7 @@
             .addFeature("feature")
             .setClientPackageName(packageName)
             .build()
-        return ChipReceiverInfo(routeInfo, null, null)
+        return ChipReceiverInfo(routeInfo, null, null, id = "id")
     }
 
     private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
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 110bbb8..4437394 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
@@ -17,14 +17,19 @@
 package com.android.systemui.media.taptotransfer.sender
 
 import android.app.StatusBarManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
 import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
@@ -32,18 +37,22 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 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
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
+import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -60,20 +69,32 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class MediaTttSenderCoordinatorTest : SysuiTestCase() {
+
+    // Note: This tests are a bit like integration tests because they use a real instance of
+    //   [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on
+    //   the inputs from [MediaTttSenderCoordinator].
+
     private lateinit var underTest: MediaTttSenderCoordinator
 
     @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
     @Mock private lateinit var commandQueue: CommandQueue
     @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
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var viewUtil: ViewUtil
     @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var vibratorHelper: VibratorHelper
+    private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
+    private lateinit var fakeWakeLock: WakeLockFake
     private lateinit var chipbarCoordinator: ChipbarCoordinator
     private lateinit var commandQueueCallback: CommandQueue.Callbacks
+    private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -85,25 +106,42 @@
         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
+        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
+        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
+        whenever(
+                packageManager.getApplicationInfo(
+                    eq(PACKAGE_NAME),
+                    any<PackageManager.ApplicationInfoFlags>()
+                )
+            )
+            .thenReturn(applicationInfo)
+        context.setMockPackageManager(packageManager)
+
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
+        fakeWakeLock = WakeLockFake()
+        fakeWakeLockBuilder = WakeLockFake.Builder(context)
+        fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
+
         uiEventLoggerFake = UiEventLoggerFake()
         uiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
         chipbarCoordinator =
             FakeChipbarCoordinator(
                 context,
-                logger,
+                chipbarLogger,
                 windowManager,
                 fakeExecutor,
                 accessibilityManager,
                 configurationController,
                 powerManager,
-                uiEventLogger,
                 falsingManager,
                 falsingCollector,
                 viewUtil,
+                vibratorHelper,
+                fakeWakeLockBuilder,
             )
         chipbarCoordinator.start()
 
@@ -149,10 +187,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -163,10 +208,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -177,12 +229,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -193,12 +250,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -209,12 +271,66 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id)
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -225,12 +341,68 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(
+                MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
+            )
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -241,12 +413,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -257,12 +434,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -298,6 +480,36 @@
     }
 
     @Test
+    fun commandQueueCallback_almostCloseThenFarFromReceiver_wakeLockAcquiredThenReleased() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST,
+            routeInfo,
+            null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isTrue()
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
+    }
+
+    @Test
+    fun commandQueueCallback_FarFromReceiver_wakeLockNeverReleased() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
+    }
+
+    @Test
     fun commandQueueCallback_invalidStateParam_noChipShown() {
         commandQueueCallback.updateMediaTapToTransferSenderDisplay(100, routeInfo, null)
 
@@ -407,53 +619,113 @@
         verify(windowManager).removeView(any())
     }
 
-    private fun getChipView(): ViewGroup {
+    @Test
+    fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
 
+    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon)
+
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
+    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
+    private fun ChipStateSender.getExpectedStateText(): String? {
+        return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
+    }
 }
 
+private const val APP_NAME = "Fake app name"
 private const val OTHER_DEVICE_NAME = "My Tablet"
+private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
 
 private val routeInfo =
     MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
         .addFeature("feature")
-        .setClientPackageName("com.android.systemui")
+        .setClientPackageName(PACKAGE_NAME)
         .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/IconLoaderLibAppIconLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/IconLoaderLibAppIconLoaderTest.kt
new file mode 100644
index 0000000..9b346d0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/IconLoaderLibAppIconLoaderTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection.appselector.data
+
+import android.content.ComponentName
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import androidx.test.filters.SmallTest
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.FastBitmapDrawable
+import com.android.launcher3.icons.IconFactory
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.system.PackageManagerWrapper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class IconLoaderLibAppIconLoaderTest : SysuiTestCase() {
+
+    private val iconFactory: IconFactory = mock()
+    private val packageManagerWrapper: PackageManagerWrapper = mock()
+    private val packageManager: PackageManager = mock()
+    private val dispatcher = Dispatchers.Unconfined
+
+    private val appIconLoader =
+        IconLoaderLibAppIconLoader(
+            backgroundDispatcher = dispatcher,
+            context = context,
+            packageManagerWrapper = packageManagerWrapper,
+            packageManager = packageManager,
+            iconFactoryProvider = { iconFactory }
+        )
+
+    @Test
+    fun loadIcon_loadsIconUsingTheSameUserId() {
+        val icon = createIcon()
+        val component = ComponentName("com.test", "TestApplication")
+        givenIcon(component, userId = 123, icon = icon)
+
+        val loadedIcon = runBlocking { appIconLoader.loadIcon(userId = 123, component = component) }
+
+        assertThat(loadedIcon).isEqualTo(icon)
+    }
+
+    private fun givenIcon(component: ComponentName, userId: Int, icon: FastBitmapDrawable) {
+        val activityInfo = mock<ActivityInfo>()
+        whenever(packageManagerWrapper.getActivityInfo(component, userId)).thenReturn(activityInfo)
+        val rawIcon = mock<Drawable>()
+        whenever(activityInfo.loadIcon(packageManager)).thenReturn(rawIcon)
+
+        val bitmapInfo = mock<BitmapInfo>()
+        whenever(iconFactory.createBadgedIconBitmap(eq(rawIcon), any())).thenReturn(bitmapInfo)
+        whenever(bitmapInfo.newIcon(context)).thenReturn(icon)
+    }
+
+    private fun createIcon(): FastBitmapDrawable =
+        FastBitmapDrawable(Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888))
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
index 939af16..d35a212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
@@ -4,6 +4,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -11,11 +12,11 @@
 import com.android.wm.shell.util.GroupedRecentTaskInfo
 import com.google.common.truth.Truth.assertThat
 import java.util.*
+import java.util.function.Consumer
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.function.Consumer
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -23,8 +24,14 @@
 
     private val dispatcher = Dispatchers.Unconfined
     private val recentTasks: RecentTasks = mock()
+    private val userTracker: UserTracker = mock()
     private val recentTaskListProvider =
-        ShellRecentTaskListProvider(dispatcher, Runnable::run, Optional.of(recentTasks))
+        ShellRecentTaskListProvider(
+            dispatcher,
+            Runnable::run,
+            Optional.of(recentTasks),
+            userTracker
+        )
 
     @Test
     fun loadRecentTasks_oneTask_returnsTheSameTask() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 0badd861..1bc4719 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -147,6 +147,18 @@
     }
 
     @Test
+    public void testMonochromatic() {
+        int colorInt = 0xffB3588A; // H350 C50 T50
+        ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
+                Style.MONOCHROMATIC /* style */);
+        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        Assert.assertTrue(
+                Color.red(neutralMid) == Color.green(neutralMid)
+                && Color.green(neutralMid) == Color.blue(neutralMid)
+        );
+    }
+
+    @Test
     @SuppressWarnings("ResultOfMethodCallIgnored")
     public void testToString() {
         new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 6adce7a..c1fa9b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -61,6 +61,7 @@
 import android.view.DisplayInfo;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowManager;
@@ -201,6 +202,8 @@
     private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock
     private Resources mResources;
+    @Mock
+    private ViewRootImpl mViewRootImpl;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
 
@@ -227,6 +230,7 @@
         when(mUserContextProvider.createCurrentUserContext(any(Context.class)))
                 .thenReturn(mContext);
         when(mNavigationBarView.getResources()).thenReturn(mResources);
+        when(mNavigationBarView.getViewRootImpl()).thenReturn(mViewRootImpl);
         setupSysuiDependency();
         // This class inflates views that call Dependency.get, thus these injections are still
         // necessary.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
new file mode 100644
index 0000000..9758842
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.view.KeyEvent
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskControllerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskControllerTest : SysuiTestCase() {
+
+    private val notesIntent = Intent(NOTES_ACTION)
+
+    @Mock lateinit var context: Context
+    @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+    @Mock lateinit var bubbles: Bubbles
+    @Mock lateinit var optionalBubbles: Optional<Bubbles>
+    @Mock lateinit var keyguardManager: KeyguardManager
+    @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
+    @Mock lateinit var optionalUserManager: Optional<UserManager>
+    @Mock lateinit var userManager: UserManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+        whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
+        whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
+        whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
+        whenever(userManager.isUserUnlocked).thenReturn(true)
+    }
+
+    private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
+        return NoteTaskController(
+            context = context,
+            intentResolver = noteTaskIntentResolver,
+            optionalBubbles = optionalBubbles,
+            optionalKeyguardManager = optionalKeyguardManager,
+            optionalUserManager = optionalUserManager,
+            isEnabled = isEnabled,
+        )
+    }
+
+    @Test
+    fun handleSystemKey_keyguardIsLocked_shouldStartActivity() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_keyguardIsUnlocked_shouldStartBubbles() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(bubbles).showAppBubble(notesIntent)
+        verify(context, never()).startActivity(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_bubblesIsNull_shouldDoNothing() {
+        whenever(optionalBubbles.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() {
+        whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_userManagerIsNull_shouldDoNothing() {
+        whenever(optionalUserManager.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() {
+        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_flagDisabled_shouldDoNothing() {
+        createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_userIsLocked_shouldDoNothing() {
+        whenever(userManager.isUserUnlocked).thenReturn(false)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(bubbles, never()).showAppBubble(notesIntent)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
new file mode 100644
index 0000000..334089c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.notetask
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInitializerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInitializerTest : SysuiTestCase() {
+
+    @Mock lateinit var commandQueue: CommandQueue
+    @Mock lateinit var bubbles: Bubbles
+    @Mock lateinit var optionalBubbles: Optional<Bubbles>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(optionalBubbles.isPresent).thenReturn(true)
+        whenever(optionalBubbles.orElse(null)).thenReturn(bubbles)
+    }
+
+    private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+        return NoteTaskInitializer(
+            optionalBubbles = optionalBubbles,
+            lazyNoteTaskController = mock(),
+            commandQueue = commandQueue,
+            isEnabled = isEnabled,
+        )
+    }
+
+    @Test
+    fun initialize_shouldAddCallbacks() {
+        createNoteTaskInitializer().initialize()
+
+        verify(commandQueue).addCallback(any())
+    }
+
+    @Test
+    fun initialize_flagDisabled_shouldDoNothing() {
+        createNoteTaskInitializer(isEnabled = false).initialize()
+
+        verify(commandQueue, never()).addCallback(any())
+    }
+
+    @Test
+    fun initialize_bubblesNotPresent_shouldDoNothing() {
+        whenever(optionalBubbles.isPresent).thenReturn(false)
+
+        createNoteTaskInitializer().initialize()
+
+        verify(commandQueue, never()).addCallback(any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
new file mode 100644
index 0000000..dd2cc2f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -0,0 +1,222 @@
+/*
+ * 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.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskIntentResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskIntentResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskIntentResolverTest : SysuiTestCase() {
+
+    @Mock lateinit var packageManager: PackageManager
+
+    private lateinit var resolver: NoteTaskIntentResolver
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        resolver = NoteTaskIntentResolver(packageManager)
+    }
+
+    private fun createResolveInfo(
+        packageName: String = "PackageName",
+        activityInfo: ActivityInfo? = null,
+    ): ResolveInfo {
+        return ResolveInfo().apply {
+            serviceInfo =
+                ServiceInfo().apply {
+                    applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
+                }
+            this.activityInfo = activityInfo
+        }
+    }
+
+    private fun createActivityInfo(
+        name: String? = "ActivityName",
+        exported: Boolean = true,
+        enabled: Boolean = true,
+        showWhenLocked: Boolean = true,
+        turnScreenOn: Boolean = true,
+    ): ActivityInfo {
+        return ActivityInfo().apply {
+            this.name = name
+            this.exported = exported
+            this.enabled = enabled
+            if (showWhenLocked) {
+                flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED
+            }
+            if (turnScreenOn) {
+                flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
+            }
+        }
+    }
+
+    private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) {
+        whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
+            .thenReturn(block())
+    }
+
+    private fun givenResolveActivity(block: () -> ResolveInfo?) {
+        whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
+    }
+
+    @Test
+    fun resolveIntent_shouldReturnNotesIntent() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }
+
+        val actual = resolver.resolveIntent()
+
+        val expected =
+            Intent(NOTES_ACTION)
+                .setComponent(ComponentName("PackageName", "ActivityName"))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        // Compares the string representation of both intents, as they are different instances.
+        assertThat(actual.toString()).isEqualTo(expected.toString())
+    }
+
+    @Test
+    fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(enabled = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(exported = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = null) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_resolveActivityIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { null }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityNotFoundForAction_shouldReturnNull() {
+        givenQueryIntentActivities { emptyList() }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index 4c72406..3620233 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.privacy.PrivacyItemController
 import com.android.systemui.privacy.logging.PrivacyLogger
 import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -66,6 +67,8 @@
     private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock
     private lateinit var safetyCenterManager: SafetyCenterManager
+    @Mock
+    private lateinit var deviceProvisionedController: DeviceProvisionedController
 
     private val uiExecutor = FakeExecutor(FakeSystemClock())
     private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -80,6 +83,7 @@
         whenever(privacyChip.context).thenReturn(context)
         whenever(privacyChip.resources).thenReturn(context.resources)
         whenever(privacyChip.isAttachedToWindow).thenReturn(true)
+        whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
 
         cameraSlotName = context.getString(com.android.internal.R.string.status_bar_camera)
         microphoneSlotName = context.getString(com.android.internal.R.string.status_bar_microphone)
@@ -98,7 +102,8 @@
                 activityStarter,
                 appOpsController,
                 broadcastDispatcher,
-                safetyCenterManager
+                safetyCenterManager,
+                deviceProvisionedController
         )
 
         backgroundExecutor.runAllReady()
@@ -199,6 +204,18 @@
         )
     }
 
+    @Test
+    fun testNoDialogWhenDeviceNotProvisioned() {
+        whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(false)
+        controller.onParentVisible()
+
+        val captor = argumentCaptor<View.OnClickListener>()
+        verify(privacyChip).setOnClickListener(capture(captor))
+
+        captor.value.onClick(privacyChip)
+        verify(privacyDialogController, never()).showDialog(any(Context::class.java))
+    }
+
     private fun setPrivacyController(micCamera: Boolean, location: Boolean) {
         whenever(privacyItemController.micCameraAvailable).thenReturn(micCamera)
         whenever(privacyItemController.locationAvailable).thenReturn(location)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
index aacc695..68c10f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index d2c2d58..72e022e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -50,7 +50,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
@@ -439,6 +439,17 @@
         verify(mQSPanelController).setExpanded(false);
     }
 
+    @Test
+    public void startsListeningAfterStateChangeToExpanded_inSplitShade() {
+        QSFragment fragment = resumeAndGetFragment();
+        enableSplitShade();
+        fragment.setQsVisible(true);
+        clearInvocations(mQSPanelController);
+
+        fragment.setExpanded(true);
+        verify(mQSPanelController).setListening(true, true);
+    }
+
     @Override
     protected Fragment instantiate(Context context, String className, Bundle arguments) {
         MockitoAnnotations.initMocks(this);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index b847ad0..caf8321 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -44,7 +44,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.customize.QSCustomizerController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index e539705..9f28708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -7,8 +7,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.MediaHost
-import com.android.systemui.media.MediaHostState
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.customize.QSCustomizerController
@@ -64,7 +64,7 @@
         whenever(brightnessSliderFactory.create(any(), any())).thenReturn(brightnessSlider)
         whenever(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
         whenever(qsPanel.resources).thenReturn(mContext.orCreateTestableResources.resources)
-        whenever(statusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(false)
+        whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
         whenever(qsPanel.setListening(anyBoolean())).then {
             whenever(qsPanel.isListening).thenReturn(it.getArgument(0))
         }
@@ -116,9 +116,9 @@
 
     @Test
     fun testIsBouncerInTransit() {
-        whenever(statusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(true)
+        whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true)
         assertThat(controller.isBouncerInTransit()).isEqualTo(true)
-        whenever(statusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(false)
+        whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
         assertThat(controller.isBouncerInTransit()).isEqualTo(false)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
index 1c686c6..5e9c1aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -22,7 +22,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -52,6 +51,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import com.android.systemui.R;
@@ -97,6 +97,7 @@
     private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline;
 
     private ViewGroup mRootView;
+    private ViewGroup mSecurityFooterView;
     private TextView mFooterText;
     private TestableImageView mPrimaryFooterIcon;
     private QSSecurityFooter mFooter;
@@ -121,21 +122,26 @@
         Looper looper = mTestableLooper.getLooper();
         Handler mainHandler = new Handler(looper);
         when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
-        mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
+        mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext)
                 .replace("ImageView", TestableImageView.class)
                 .build().inflate(R.layout.quick_settings_security_footer, null, false);
         mFooterUtils = new QSSecurityFooterUtils(getContext(),
                 getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
                 mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
-        mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
-                mBroadcastDispatcher, mFooterUtils);
-        mFooterText = mRootView.findViewById(R.id.footer_text);
-        mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
+        mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController,
+                looper, mBroadcastDispatcher, mFooterUtils);
+        mFooterText = mSecurityFooterView.findViewById(R.id.footer_text);
+        mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon);
 
         when(mSecurityController.getDeviceOwnerComponentOnAnyUser())
                 .thenReturn(DEVICE_OWNER_COMPONENT);
         when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
                 .thenReturn(DEVICE_OWNER_TYPE_DEFAULT);
+
+        // mSecurityFooterView must have a ViewGroup parent so that
+        // DialogLaunchAnimator.Controller.fromView() does not return null.
+        mRootView = new FrameLayout(mContext);
+        mRootView.addView(mSecurityFooterView);
         ViewUtils.attachView(mRootView);
 
         mFooter.init();
@@ -153,7 +159,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -165,7 +171,7 @@
         TestableLooper.get(this).processAllMessages();
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_management),
                      mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -181,7 +187,7 @@
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management,
                                         MANAGING_ORGANIZATION),
                 mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -200,7 +206,7 @@
         assertEquals(mContext.getString(
                 R.string.quick_settings_financed_disclosure_named_management,
                 MANAGING_ORGANIZATION), mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -217,7 +223,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -227,8 +233,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -241,8 +247,9 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertTrue(mRootView.isClickable());
-        assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertTrue(mSecurityFooterView.isClickable());
+        assertEquals(View.VISIBLE,
+                mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -254,8 +261,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -734,11 +741,11 @@
     @Test
     public void testDialogUsesDialogLauncher() {
         when(mSecurityController.isDeviceManaged()).thenReturn(true);
-        mFooter.onClick(mRootView);
+        mFooter.onClick(mSecurityFooterView);
 
         mTestableLooper.processAllMessages();
 
-        verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any());
+        verify(mDialogLaunchAnimator).show(any(), any());
     }
 
     @Test
@@ -775,7 +782,7 @@
         ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class);
 
         mTestableLooper.processAllMessages();
-        verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any());
+        verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any());
 
         AlertDialog dialog = dialogCaptor.getValue();
         dialog.create();
@@ -817,8 +824,8 @@
         verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(),
                 any());
 
-        // Pretend view is not visible temporarily
-        mRootView.onVisibilityAggregated(false);
+        // Pretend view is not attached anymore.
+        mRootView.removeView(mSecurityFooterView);
         captor.getValue().onReceive(mContext,
                 new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
         mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 3c58b6fc..c452872 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -114,8 +115,6 @@
     @Mock
     private DumpManager mDumpManager;
     @Mock
-    private QSTile.State mMockState;
-    @Mock
     private CentralSurfaces mCentralSurfaces;
     @Mock
     private QSLogger mQSLogger;
@@ -195,7 +194,6 @@
     }
 
     private void setUpTileFactory() {
-        when(mMockState.toString()).thenReturn(MOCK_STATE_STRING);
         // Only create this kind of tiles
         when(mDefaultFactory.createTile(anyString())).thenAnswer(
                 invocation -> {
@@ -209,7 +207,11 @@
                     } else if ("na".equals(spec)) {
                         return new NotAvailableTile(mQSTileHost);
                     } else if (CUSTOM_TILE_SPEC.equals(spec)) {
-                        return mCustomTile;
+                        QSTile tile = mCustomTile;
+                        QSTile.State s = mock(QSTile.State.class);
+                        s.spec = spec;
+                        when(mCustomTile.getState()).thenReturn(s);
+                        return tile;
                     } else if ("internet".equals(spec)
                             || "wifi".equals(spec)
                             || "cell".equals(spec)) {
@@ -647,7 +649,7 @@
     @Test
     public void testSetTileRemoved_removedBySystem() {
         int user = mUserTracker.getUserId();
-        saveSetting("spec1" + CUSTOM_TILE_SPEC);
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
 
         // This will be done by TileServiceManager
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
@@ -658,6 +660,27 @@
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
+    @Test
+    public void testProtoDump_noTiles() {
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(0, proto.tiles.length);
+    }
+
+    @Test
+    public void testTilesInOrder() {
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
+
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(2, proto.tiles.length);
+        assertEquals("spec1", proto.tiles[0].getSpec());
+        assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName);
+        assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className);
+    }
+
     private SharedPreferences getSharedPreferenecesForUser(int user) {
         return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
     }
@@ -707,12 +730,9 @@
 
         @Override
         public State newTileState() {
-            return mMockState;
-        }
-
-        @Override
-        public State getState() {
-            return mMockState;
+            State s = mock(QSTile.State.class);
+            when(s.toString()).thenReturn(MOCK_STATE_STRING);
+            return s;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 6af8e49..f53e997 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -23,8 +23,8 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaHost
-import com.android.systemui.media.MediaHostState
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTileView
 import com.android.systemui.qs.customize.QSCustomizerController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
index 5abc0e1..35c8cc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java
@@ -27,6 +27,8 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
+import android.content.res.Resources;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -42,16 +44,22 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class TileLayoutTest extends SysuiTestCase {
-    private TileLayout mTileLayout;
+    private Resources mResources;
     private int mLayoutSizeForOneTile;
+    private TileLayout mTileLayout; // under test
 
     @Before
     public void setUp() throws Exception {
-        mTileLayout = new TileLayout(mContext);
+        Context context = Mockito.spy(mContext);
+        mResources = Mockito.spy(context.getResources());
+        Mockito.when(mContext.getResources()).thenReturn(mResources);
+
+        mTileLayout = new TileLayout(context);
         // Layout needs to leave space for the tile margins. Three times the margin size is
         // sufficient for any number of columns.
         mLayoutSizeForOneTile =
@@ -203,4 +211,21 @@
         verify(tileRecord1.tileView).setPosition(0);
         verify(tileRecord2.tileView).setPosition(1);
     }
+
+    @Test
+    public void resourcesChanged_updateResources_returnsTrue() {
+        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+        mTileLayout.updateResources(); // setup with 1
+        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(2);
+
+        assertEquals(true, mTileLayout.updateResources());
+    }
+
+    @Test
+    public void resourcesSame_updateResources_returnsFalse() {
+        Mockito.when(mResources.getInteger(R.integer.quick_settings_num_columns)).thenReturn(1);
+        mTileLayout.updateResources(); // setup with 1
+
+        assertEquals(false, mTileLayout.updateResources());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
new file mode 100644
index 0000000..629c663
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
@@ -0,0 +1,104 @@
+package com.android.systemui.qs
+
+import android.content.ComponentName
+import android.service.quicksettings.Tile
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TileStateToProtoTest : SysuiTestCase() {
+
+    companion object {
+        private const val TEST_LABEL = "label"
+        private const val TEST_SUBTITLE = "subtitle"
+        private const val TEST_SPEC = "spec"
+        private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
+    }
+
+    @Test
+    fun platformTile_INACTIVE() {
+        val state =
+            QSTile.State().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_INACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun componentTile_UNAVAILABLE() {
+        val state =
+            QSTile.State().apply {
+                spec = CustomTile.toSpec(TEST_COMPONENT)
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_UNAVAILABLE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isFalse()
+        assertThat(proto?.hasComponentName()).isTrue()
+        val componentName = proto?.componentName
+        assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName)
+        assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className)
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun booleanState_ACTIVE() {
+        val state =
+            QSTile.BooleanState().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+                value = true
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE)
+        assertThat(proto?.hasBooleanState()).isTrue()
+        assertThat(proto?.booleanState).isTrue()
+    }
+
+    @Test
+    fun noSpec_returnsNull() {
+        val state =
+            QSTile.State().apply {
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
index 99a17a6..9115ab3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
@@ -24,6 +24,7 @@
 import android.testing.TestableLooper;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
 
@@ -48,6 +49,7 @@
     public void setUp() throws Exception {
         mTestableLooper = TestableLooper.get(this);
         LayoutInflater inflater = LayoutInflater.from(mContext);
+        mContext.ensureTestableResources();
         mTestableLooper.runWithLooper(() ->
                 mQSCarrier = (QSCarrier) inflater.inflate(R.layout.qs_carrier, null));
 
@@ -119,4 +121,30 @@
         mQSCarrier.updateState(c, true);
         assertEquals(View.GONE, mQSCarrier.getRSSIView().getVisibility());
     }
+
+    @Test
+    public void testCarrierNameMaxWidth_smallScreen_fromResource() {
+        int maxEms = 10;
+        mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms);
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_large_screen_shade_header, false);
+        TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text);
+
+        mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration());
+
+        assertEquals(maxEms, carrierText.getMaxEms());
+    }
+
+    @Test
+    public void testCarrierNameMaxWidth_largeScreen_maxInt() {
+        int maxEms = 10;
+        mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms);
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_use_large_screen_shade_header, true);
+        TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text);
+
+        mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration());
+
+        assertEquals(Integer.MAX_VALUE, carrierText.getMaxEms());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
index 3c25807..645b1cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
@@ -16,31 +16,25 @@
 
 package com.android.systemui.qs.footer.domain.interactor
 
-import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.os.UserHandle
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.animation.Expandable
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.QSSecurityFooterUtils
 import com.android.systemui.qs.footer.FooterActionsTestUtils
-import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.truth.correspondence.FakeUiEvent
 import com.android.systemui.truth.correspondence.LogMaker
-import com.android.systemui.user.UserSwitcherActivity
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.eq
@@ -70,13 +64,13 @@
         val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
 
         val quickSettingsContext = mock<Context>()
-        underTest.showDeviceMonitoringDialog(quickSettingsContext)
+
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, null)
         verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
 
-        val view = mock<View>()
-        whenever(view.context).thenReturn(quickSettingsContext)
-        underTest.showDeviceMonitoringDialog(view)
-        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
+        val expandable = mock<Expandable>()
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
     @Test
@@ -85,8 +79,8 @@
         val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger)
 
         val globalActionsDialogLite = mock<GlobalActionsDialogLite>()
-        val view = mock<View>()
-        underTest.showPowerMenuDialog(globalActionsDialogLite, view)
+        val expandable = mock<Expandable>()
+        underTest.showPowerMenuDialog(globalActionsDialogLite, expandable)
 
         // Event is logged.
         val logs = uiEventLogger.logs
@@ -99,7 +93,7 @@
             .showOrHideDialog(
                 /* keyguardShowing= */ false,
                 /* isDeviceProvisioned= */ true,
-                view,
+                expandable,
             )
     }
 
@@ -156,57 +150,4 @@
         // We only unlock the device.
         verify(activityStarter).postQSRunnableDismissingKeyguard(any())
     }
-
-    @Test
-    fun showUserSwitcher_fullScreenDisabled() {
-        val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
-        val userSwitchDialogController = mock<UserSwitchDialogController>()
-        val underTest =
-            utils.footerActionsInteractor(
-                featureFlags = featureFlags,
-                userSwitchDialogController = userSwitchDialogController,
-            )
-
-        val view = mock<View>()
-        underTest.showUserSwitcher(view)
-
-        // Dialog is shown.
-        verify(userSwitchDialogController).showDialog(view)
-    }
-
-    @Test
-    fun showUserSwitcher_fullScreenEnabled() {
-        val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
-        val activityStarter = mock<ActivityStarter>()
-        val underTest =
-            utils.footerActionsInteractor(
-                featureFlags = featureFlags,
-                activityStarter = activityStarter,
-            )
-
-        // The clicked view. The context is necessary because it's used to build the intent, that
-        // we check below.
-        val view = mock<View>()
-        whenever(view.context).thenReturn(context)
-
-        underTest.showUserSwitcher(view)
-
-        // Dialog is shown.
-        val intentCaptor = argumentCaptor<Intent>()
-        verify(activityStarter)
-            .startActivity(
-                intentCaptor.capture(),
-                /* dismissShade= */ eq(true),
-                /* ActivityLaunchAnimator.Controller= */ nullable(),
-                /* showOverLockscreenWhenLocked= */ eq(true),
-                eq(UserHandle.SYSTEM),
-            )
-        assertThat(intentCaptor.value.component)
-            .isEqualTo(
-                ComponentName(
-                    context,
-                    UserSwitcherActivity::class.java,
-                )
-            )
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index 2c76be6..f55d262 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.service.quicksettings.Tile;
 import android.testing.AndroidTestingRunner;
@@ -39,6 +40,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
 
 @RunWith(AndroidTestingRunner.class)
 @UiThreadTest
@@ -136,6 +139,22 @@
         assertEquals(mIconView.getColor(s1), mIconView.getColor(s2));
     }
 
+    @Test
+    public void testIconStartedAndStoppedWhenAllowAnimationsFalse() {
+        ImageView iv = new ImageView(mContext);
+        AnimatedVectorDrawable d = mock(AnimatedVectorDrawable.class);
+        State s = new State();
+        s.icon = mock(Icon.class);
+        when(s.icon.getDrawable(any())).thenReturn(d);
+        when(s.icon.getInvisibleDrawable(any())).thenReturn(d);
+
+        mIconView.updateIcon(iv, s, false);
+
+        InOrder inOrder = Mockito.inOrder(d);
+        inOrder.verify(d).start();
+        inOrder.verify(d).stop();
+    }
+
     private static Drawable.ConstantState fakeConstantState(Drawable otherDrawable) {
         return new Drawable.ConstantState() {
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
index 73a0cbc..030c59f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.settings.GlobalSettings
 import com.google.common.truth.Truth.assertThat
 import dagger.Lazy
@@ -64,6 +65,8 @@
     private lateinit var mConnectivityManager: Lazy<ConnectivityManager>
     @Mock
     private lateinit var mGlobalSettings: GlobalSettings
+    @Mock
+    private lateinit var mUserTracker: UserTracker
     private lateinit var mTestableLooper: TestableLooper
     private lateinit var mTile: AirplaneModeTile
 
@@ -87,7 +90,8 @@
             mQsLogger,
             mBroadcastDispatcher,
             mConnectivityManager,
-            mGlobalSettings)
+            mGlobalSettings,
+            mUserTracker)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
index b652aee..cac90a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
@@ -119,6 +119,6 @@
         when(mController.isEnabledForQuickSettings()).thenReturn(true);
         QSTile.State state = new QSTile.State();
         mTile.handleUpdateState(state, null);
-        assertEquals(state.state, Tile.STATE_ACTIVE);
+        assertEquals(state.state, Tile.STATE_INACTIVE);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index da52a9b..08a90b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,9 +30,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,6 +42,7 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
@@ -66,10 +69,15 @@
 
         mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater)
         `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
-                .thenReturn(mInflatedUserDetailItemView)
+            .thenReturn(mInflatedUserDetailItemView)
         `when`(mParent.context).thenReturn(mContext)
-        adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger,
-                falsingManagerFake)
+        adapter =
+            UserDetailView.Adapter(
+                mContext,
+                mUserSwitcherController,
+                uiEventLogger,
+                falsingManagerFake
+            )
         mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user))
     }
 
@@ -139,6 +147,20 @@
         clickableTest(false, false, mUserDetailItemView, true)
     }
 
+    @Test
+    fun testManageUsersIsNotAvailable() {
+        assertNull(adapter.users.find { it.isManageUsers })
+    }
+
+    @Test
+    fun clickDismissDialog() {
+        val shower: UserSwitchDialogController.DialogShower =
+            mock(UserSwitchDialogController.DialogShower::class.java)
+        adapter.injectDialogShower(shower)
+        adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
+        verify(shower).dismiss()
+    }
+
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index d703705..48a53bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
@@ -5,6 +5,7 @@
 import static android.telephony.SignalStrength.SIGNAL_STRENGTH_GREAT;
 import static android.telephony.SignalStrength.SIGNAL_STRENGTH_POOR;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.systemui.qs.tiles.dialog.InternetDialogController.TOAST_PARAMS_HORIZONTAL_WEIGHT;
 import static com.android.systemui.qs.tiles.dialog.InternetDialogController.TOAST_PARAMS_VERTICAL_WEIGHT;
 import static com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX;
@@ -13,6 +14,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -38,6 +40,7 @@
 import android.os.Handler;
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
+import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.testing.AndroidTestingRunner;
@@ -57,6 +60,8 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.connectivity.AccessPointController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -70,12 +75,15 @@
 import com.android.wifitrackerlib.MergedCarrierEntry;
 import com.android.wifitrackerlib.WifiEntry;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -86,6 +94,9 @@
 public class InternetDialogControllerTest extends SysuiTestCase {
 
     private static final int SUB_ID = 1;
+    private static final int SUB_ID2 = 2;
+
+    private MockitoSession mStaticMockSession;
 
     //SystemUIToast
     private static final int GRAVITY_FLAGS = Gravity.FILL_HORIZONTAL | Gravity.FILL_VERTICAL;
@@ -159,6 +170,8 @@
     @Mock
     private SignalStrength mSignalStrength;
 
+    private FakeFeatureFlags mFlags = new FakeFeatureFlags();
+
     private TestableResources mTestableResources;
     private InternetDialogController mInternetDialogController;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -167,6 +180,10 @@
 
     @Before
     public void setUp() {
+        mStaticMockSession = mockitoSession()
+                .mockStatic(SubscriptionManager.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
         MockitoAnnotations.initMocks(this);
         mTestableResources = mContext.getOrCreateTestableResources();
         doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
@@ -183,6 +200,7 @@
         mAccessPoints.add(mWifiEntry1);
         when(mAccessPointController.getMergedCarrierEntry()).thenReturn(mMergedCarrierEntry);
         when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
+        when(SubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(SUB_ID);
         when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt()))
             .thenReturn(mSystemUIToast);
         when(mSystemUIToast.getView()).thenReturn(mToastView);
@@ -196,13 +214,19 @@
                 mConnectivityManager, mHandler, mExecutor, mBroadcastDispatcher,
                 mock(KeyguardUpdateMonitor.class), mGlobalSettings, mKeyguardStateController,
                 mWindowManager, mToastFactory, mWorkerHandler, mCarrierConfigTracker,
-                mLocationController, mDialogLaunchAnimator, mWifiStateWorker);
+                mLocationController, mDialogLaunchAnimator, mWifiStateWorker, mFlags);
         mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor,
                 mInternetDialogController.mOnSubscriptionsChangedListener);
         mInternetDialogController.onStart(mInternetDialogCallback, true);
         mInternetDialogController.onAccessPointsChanged(mAccessPoints);
         mInternetDialogController.mActivityStarter = mActivityStarter;
         mInternetDialogController.mWifiIconInjector = mWifiIconInjector;
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, false);
+    }
+
+    @After
+    public void tearDown() {
+        mStaticMockSession.finishMocking();
     }
 
     @Test
@@ -387,15 +411,45 @@
 
     @Test
     public void getSubtitleText_withNoService_returnNoNetworksAvailable() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        InternetDialogController spyController = spy(mInternetDialogController);
         fakeAirplaneModeEnabled(false);
         when(mWifiStateWorker.isWifiEnabled()).thenReturn(true);
-        mInternetDialogController.onAccessPointsChanged(null /* accessPoints */);
+        spyController.onAccessPointsChanged(null /* accessPoints */);
+
+        doReturn(SUB_ID2).when(spyController).getActiveAutoSwitchNonDdsSubId();
+        doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getState();
+        doReturn(mServiceState).when(mTelephonyManager).getServiceState();
+        doReturn(TelephonyManager.DATA_DISCONNECTED).when(mTelephonyManager).getDataState();
+
+        assertFalse(TextUtils.equals(spyController.getSubtitleText(false),
+                getResourcesString("all_network_unavailable")));
+
+        doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+                .when(spyController).getActiveAutoSwitchNonDdsSubId();
+        spyController.onAccessPointsChanged(null /* accessPoints */);
+        assertTrue(TextUtils.equals(spyController.getSubtitleText(false),
+                getResourcesString("all_network_unavailable")));
+    }
+
+    @Test
+    public void getSubtitleText_withNoService_returnNoNetworksAvailable_flagOff() {
+        InternetDialogController spyController = spy(mInternetDialogController);
+        fakeAirplaneModeEnabled(false);
+        when(mWifiStateWorker.isWifiEnabled()).thenReturn(true);
+        spyController.onAccessPointsChanged(null /* accessPoints */);
 
         doReturn(ServiceState.STATE_OUT_OF_SERVICE).when(mServiceState).getState();
         doReturn(mServiceState).when(mTelephonyManager).getServiceState();
         doReturn(TelephonyManager.DATA_DISCONNECTED).when(mTelephonyManager).getDataState();
 
-        assertTrue(TextUtils.equals(mInternetDialogController.getSubtitleText(false),
+        assertTrue(TextUtils.equals(spyController.getSubtitleText(false),
+                getResourcesString("all_network_unavailable")));
+
+        doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+                .when(spyController).getActiveAutoSwitchNonDdsSubId();
+        spyController.onAccessPointsChanged(null /* accessPoints */);
+        assertTrue(TextUtils.equals(spyController.getSubtitleText(false),
                 getResourcesString("all_network_unavailable")));
     }
 
@@ -713,6 +767,108 @@
     }
 
     @Test
+    public void getSignalStrengthIcon_differentSubId() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        InternetDialogController spyController = spy(mInternetDialogController);
+        Drawable icons = spyController.getSignalStrengthIcon(SUB_ID, mContext, 1, 1, 0, false);
+        Drawable icons2 = spyController.getSignalStrengthIcon(SUB_ID2, mContext, 1, 1, 0, false);
+
+        assertThat(icons).isNotEqualTo(icons2);
+    }
+
+    @Test
+    public void getActiveAutoSwitchNonDdsSubId() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        // active on non-DDS
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        int subId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+        assertThat(subId).isEqualTo(SUB_ID2);
+
+        // active on CBRS
+        doReturn(true).when(info).isOpportunistic();
+        subId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+        assertThat(subId).isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
+        // active on DDS
+        doReturn(false).when(info).isOpportunistic();
+        doReturn(SUB_ID).when(info).getSubscriptionId();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        subId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+        assertThat(subId).isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+    }
+
+    @Test
+    public void getActiveAutoSwitchNonDdsSubId_flagOff() {
+        // active on non-DDS
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        doReturn(SUB_ID2).when(info).getSubscriptionId();
+        when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+        int subId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+        assertThat(subId).isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+    }
+
+    @Test
+    public void getMobileNetworkSummary() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        InternetDialogController spyController = spy(mInternetDialogController);
+        doReturn(SUB_ID2).when(spyController).getActiveAutoSwitchNonDdsSubId();
+        doReturn(true).when(spyController).isMobileDataEnabled();
+        doReturn(true).when(spyController).activeNetworkIsCellular();
+        String dds = spyController.getMobileNetworkSummary(SUB_ID);
+        String nonDds = spyController.getMobileNetworkSummary(SUB_ID2);
+
+        assertThat(dds).contains(mContext.getString(R.string.mobile_data_poor_connection));
+        assertThat(dds).isNotEqualTo(nonDds);
+    }
+
+    @Test
+    public void getMobileNetworkSummary_flagOff() {
+        InternetDialogController spyController = spy(mInternetDialogController);
+        doReturn(true).when(spyController).isMobileDataEnabled();
+        doReturn(true).when(spyController).activeNetworkIsCellular();
+        String dds = spyController.getMobileNetworkSummary(SUB_ID);
+
+        assertThat(dds).contains(mContext.getString(R.string.mobile_data_connection_active));
+    }
+
+    @Test
+    public void launchMobileNetworkSettings_validSubId() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        InternetDialogController spyController = spy(mInternetDialogController);
+        doReturn(SUB_ID2).when(spyController).getActiveAutoSwitchNonDdsSubId();
+        spyController.launchMobileNetworkSettings(mDialogLaunchView);
+
+        verify(mActivityStarter).postStartActivityDismissingKeyguard(any(Intent.class), anyInt(),
+                any());
+    }
+
+    @Test
+    public void launchMobileNetworkSettings_invalidSubId() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        InternetDialogController spyController = spy(mInternetDialogController);
+        doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+                .when(spyController).getActiveAutoSwitchNonDdsSubId();
+        spyController.launchMobileNetworkSettings(mDialogLaunchView);
+
+        verify(mActivityStarter, never())
+                .postStartActivityDismissingKeyguard(any(Intent.class), anyInt());
+    }
+
+    @Test
+    public void setAutoDataSwitchMobileDataPolicy() {
+        mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+        mInternetDialogController.setAutoDataSwitchMobileDataPolicy(SUB_ID, true);
+
+        verify(mTelephonyManager).setMobileDataPolicyEnabled(eq(
+                TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH), eq(true));
+    }
+
+    @Test
     public void getSignalStrengthDrawableWithLevel_carrierNetworkIsNotActive_useMobileDataLevel() {
         // Fake mobile data level as SIGNAL_STRENGTH_POOR(1)
         when(mSignalStrength.getLevel()).thenReturn(SIGNAL_STRENGTH_POOR);
@@ -720,9 +876,9 @@
         when(mInternetDialogController.getCarrierNetworkLevel()).thenReturn(WIFI_LEVEL_MAX);
 
         InternetDialogController spyController = spy(mInternetDialogController);
-        spyController.getSignalStrengthDrawableWithLevel(false /* isCarrierNetworkActive */);
+        spyController.getSignalStrengthDrawableWithLevel(false /* isCarrierNetworkActive */, 0);
 
-        verify(spyController).getSignalStrengthIcon(any(), eq(SIGNAL_STRENGTH_POOR),
+        verify(spyController).getSignalStrengthIcon(eq(0), any(), eq(SIGNAL_STRENGTH_POOR),
                 eq(NUM_SIGNAL_STRENGTH_BINS), anyInt(), anyBoolean());
     }
 
@@ -734,9 +890,9 @@
         when(mInternetDialogController.getCarrierNetworkLevel()).thenReturn(WIFI_LEVEL_MAX);
 
         InternetDialogController spyController = spy(mInternetDialogController);
-        spyController.getSignalStrengthDrawableWithLevel(true /* isCarrierNetworkActive */);
+        spyController.getSignalStrengthDrawableWithLevel(true /* isCarrierNetworkActive */, 0);
 
-        verify(spyController).getSignalStrengthIcon(any(), eq(WIFI_LEVEL_MAX),
+        verify(spyController).getSignalStrengthIcon(eq(0), any(), eq(WIFI_LEVEL_MAX),
                 eq(WIFI_LEVEL_MAX + 1), anyInt(), anyBoolean());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
index f922475..3ae8428 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
@@ -8,12 +8,15 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AlertDialog;
+import android.content.DialogInterface;
 import android.os.Handler;
 import android.telephony.TelephonyManager;
 import android.testing.AndroidTestingRunner;
@@ -31,6 +34,7 @@
 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -38,6 +42,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -48,6 +53,7 @@
 
 import java.util.List;
 
+@Ignore("b/257089187")
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -72,6 +78,8 @@
     private InternetDialogController mInternetDialogController;
     @Mock
     private KeyguardStateController mKeyguard;
+    @Mock
+    private DialogLaunchAnimator mDialogLaunchAnimator;
 
     private FakeExecutor mBgExecutor = new FakeExecutor(new FakeSystemClock());
     private InternetDialog mInternetDialog;
@@ -100,8 +108,9 @@
         when(mInternetWifiEntry.hasInternetAccess()).thenReturn(true);
         when(mWifiEntries.size()).thenReturn(1);
 
-        when(mInternetDialogController.getMobileNetworkTitle()).thenReturn(MOBILE_NETWORK_TITLE);
-        when(mInternetDialogController.getMobileNetworkSummary())
+        when(mInternetDialogController.getMobileNetworkTitle(anyInt()))
+                .thenReturn(MOBILE_NETWORK_TITLE);
+        when(mInternetDialogController.getMobileNetworkSummary(anyInt()))
                 .thenReturn(MOBILE_NETWORK_SUMMARY);
         when(mInternetDialogController.isWifiEnabled()).thenReturn(true);
 
@@ -115,7 +124,8 @@
 
     private void createInternetDialog() {
         mInternetDialog = new InternetDialog(mContext, mock(InternetDialogFactory.class),
-                mInternetDialogController, true, true, true, mock(UiEventLogger.class), mHandler,
+                mInternetDialogController, true, true, true, mock(UiEventLogger.class),
+                mDialogLaunchAnimator, mHandler,
                 mBgExecutor, mKeyguard);
         mInternetDialog.mAdapter = mInternetAdapter;
         mInternetDialog.mConnectedWifiEntry = mInternetWifiEntry;
@@ -307,12 +317,18 @@
 
     @Test
     public void updateDialog_wifiOnAndHasInternetWifi_showConnectedWifi() {
+        mInternetDialog.dismissDialog();
+        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        createInternetDialog();
         // The preconditions WiFi ON and Internet WiFi are already in setUp()
         doReturn(false).when(mInternetDialogController).activeNetworkIsCellular();
 
-        mInternetDialog.updateDialog(false);
+        mInternetDialog.updateDialog(true);
 
         assertThat(mConnectedWifi.getVisibility()).isEqualTo(View.VISIBLE);
+        LinearLayout secondaryLayout = mDialogView.requireViewById(
+                R.id.secondary_mobile_network_layout);
+        assertThat(secondaryLayout.getVisibility()).isEqualTo(View.GONE);
     }
 
     @Test
@@ -460,6 +476,44 @@
     }
 
     @Test
+    public void updateDialog_showSecondaryDataSub() {
+        mInternetDialog.dismissDialog();
+        doReturn(1).when(mInternetDialogController).getActiveAutoSwitchNonDdsSubId();
+        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        doReturn(false).when(mInternetDialogController).isAirplaneModeEnabled();
+        createInternetDialog();
+
+        clearInvocations(mInternetDialogController);
+        mInternetDialog.updateDialog(true);
+
+        LinearLayout primaryLayout = mDialogView.requireViewById(
+                R.id.mobile_network_layout);
+        LinearLayout secondaryLayout = mDialogView.requireViewById(
+                R.id.secondary_mobile_network_layout);
+
+        verify(mInternetDialogController).getMobileNetworkSummary(1);
+        assertThat(primaryLayout.getBackground()).isNotEqualTo(secondaryLayout.getBackground());
+
+        // Tap the primary sub info
+        primaryLayout.performClick();
+        ArgumentCaptor<AlertDialog> dialogArgumentCaptor =
+                ArgumentCaptor.forClass(AlertDialog.class);
+        verify(mDialogLaunchAnimator).showFromDialog(dialogArgumentCaptor.capture(),
+                eq(mInternetDialog), eq(null), eq(false));
+        AlertDialog dialog = dialogArgumentCaptor.getValue();
+        dialog.show();
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+        TestableLooper.get(this).processAllMessages();
+        verify(mInternetDialogController).setAutoDataSwitchMobileDataPolicy(1, false);
+
+        // Tap the secondary sub info
+        secondaryLayout.performClick();
+        verify(mInternetDialogController).launchMobileNetworkSettings(any(View.class));
+
+        dialog.dismiss();
+    }
+
+    @Test
     public void updateDialog_wifiOn_hideWifiScanNotify() {
         // The preconditions WiFi ON and WiFi entries are already in setUp()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 9d908fd..0a34810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -20,12 +20,12 @@
 import android.content.Intent
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
-import android.view.View
 import android.widget.Button
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PseudoGridView
@@ -35,6 +35,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,7 +64,7 @@
     @Mock
     private lateinit var userDetailViewAdapter: UserDetailView.Adapter
     @Mock
-    private lateinit var launchView: View
+    private lateinit var launchExpandable: Expandable
     @Mock
     private lateinit var neutralButton: Button
     @Mock
@@ -79,7 +80,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(launchView.context).thenReturn(mContext)
         `when`(dialog.context).thenReturn(mContext)
 
         controller = UserSwitchDialogController(
@@ -94,32 +94,34 @@
 
     @Test
     fun showDialog_callsDialogShow() {
-        controller.showDialog(launchView)
-        verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean())
+        val launchController = mock<DialogLaunchAnimator.Controller>()
+        `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController)
+        controller.showDialog(context, launchExpandable)
+        verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean())
         verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
     }
 
     @Test
     fun dialog_showForAllUsers() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setShowForAllUsers(true)
     }
 
     @Test
     fun dialog_cancelOnTouchOutside() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setCanceledOnTouchOutside(true)
     }
 
     @Test
     fun adapterAndGridLinked() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>())
     }
 
     @Test
     fun doneButtonLogsCorrectly() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor))
 
@@ -132,7 +134,7 @@
     fun clickSettingsButton_noFalsing_opensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
@@ -153,7 +155,7 @@
     fun clickSettingsButton_Falsing_notOpensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
deleted file mode 100644
index 2d2f4cc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
+++ /dev/null
@@ -1,51 +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.ripple
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class RippleViewTest : SysuiTestCase() {
-    @Mock
-    private lateinit var rippleView: RippleView
-
-    @Before
-    fun setup() {
-        rippleView = RippleView(context, null)
-    }
-
-    @Test
-    fun testSetupShader_compilesCircle() {
-        rippleView.setupShader(RippleShader.RippleShape.CIRCLE)
-    }
-
-    @Test
-    fun testSetupShader_compilesRoundedBox() {
-        rippleView.setupShader(RippleShader.RippleShape.ROUNDED_BOX)
-    }
-
-    @Test
-    fun testSetupShader_compilesEllipse() {
-        rippleView.setupShader(RippleShader.RippleShape.ELLIPSE)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
new file mode 100644
index 0000000..0aa3621
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -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.systemui.screenrecord
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.Spinner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+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.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class ScreenRecordPermissionDialogTest : SysuiTestCase() {
+
+    @Mock private lateinit var starter: ActivityStarter
+    @Mock private lateinit var controller: RecordingController
+    @Mock private lateinit var userContextProvider: UserContextProvider
+    @Mock private lateinit var flags: FeatureFlags
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+    @Mock private lateinit var onStartRecordingClicked: Runnable
+
+    private lateinit var dialog: ScreenRecordPermissionDialog
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        dialog =
+            ScreenRecordPermissionDialog(
+                context,
+                controller,
+                starter,
+                dialogLaunchAnimator,
+                userContextProvider,
+                onStartRecordingClicked
+            )
+        dialog.onCreate(null)
+        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
+    }
+
+    @After
+    fun teardown() {
+        if (::dialog.isInitialized) {
+            dialog.dismiss()
+        }
+    }
+
+    @Test
+    fun testShowDialog_partialScreenSharingEnabled_optionsSpinnerIsVisible() {
+        dialog.show()
+
+        val visibility = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner).visibility
+        assertThat(visibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun testShowDialog_singleAppSelected_showTapsIsGone() {
+        dialog.show()
+        onSpinnerItemSelected(SINGLE_APP)
+
+        val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
+        assertThat(visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testShowDialog_entireScreenSelected_showTapsIsVisible() {
+        dialog.show()
+        onSpinnerItemSelected(ENTIRE_SCREEN)
+
+        val visibility = dialog.requireViewById<View>(R.id.show_taps).visibility
+        assertThat(visibility).isEqualTo(View.VISIBLE)
+    }
+
+    private fun onSpinnerItemSelected(position: Int) {
+        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
+        spinner.onItemSelectedListener.onItemSelected(spinner, mock(), position, /* id= */ 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
index 4c44dac..df3a62f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
@@ -23,6 +23,7 @@
 
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
+import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.graphics.Bitmap;
@@ -31,9 +32,11 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.net.Uri;
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.UserHandle;
 import android.provider.MediaStore;
 import android.testing.AndroidTestingRunner;
 
@@ -41,11 +44,18 @@
 import androidx.test.filters.MediumTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -60,13 +70,21 @@
 @RunWith(AndroidTestingRunner.class)
 @MediumTest // file I/O
 public class ImageExporterTest extends SysuiTestCase {
-
     /** Executes directly in the caller's thread */
     private static final Executor DIRECT_EXECUTOR = Runnable::run;
     private static final byte[] EXIF_FILE_TAG = "Exif\u0000\u0000".getBytes(US_ASCII);
 
     private static final ZonedDateTime CAPTURE_TIME =
-            ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 13, 15), ZoneId.of("EST"));
+            ZonedDateTime.of(LocalDateTime.of(2020, 12, 15, 13, 15), ZoneId.of("America/New_York"));
+
+    private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    @Mock
+    private ContentResolver mMockContentResolver;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
 
     @Test
     public void testImageFilename() {
@@ -92,7 +110,8 @@
     @Test
     public void testImageExport() throws ExecutionException, InterruptedException, IOException {
         ContentResolver contentResolver = mContext.getContentResolver();
-        ImageExporter exporter = new ImageExporter(contentResolver);
+        mFeatureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true);
+        ImageExporter exporter = new ImageExporter(contentResolver, mFeatureFlags);
 
         UUID requestId = UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814");
         Bitmap original = createCheckerBitmap(10, 10, 10);
@@ -168,6 +187,44 @@
                 values.getAsLong(MediaStore.MediaColumns.DATE_EXPIRES));
     }
 
+    @Test
+    public void testSetUser() {
+        mFeatureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true);
+        ImageExporter exporter = new ImageExporter(mMockContentResolver, mFeatureFlags);
+
+        UserHandle imageUserHande = UserHandle.of(10);
+
+        ArgumentCaptor<Uri> uriCaptor = ArgumentCaptor.forClass(Uri.class);
+        // Capture the URI and then return null to bail out of export.
+        Mockito.when(mMockContentResolver.insert(uriCaptor.capture(), Mockito.any())).thenReturn(
+                null);
+        exporter.export(DIRECT_EXECUTOR, UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"),
+                null, CAPTURE_TIME, imageUserHande);
+
+        Uri expected = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+        expected = ContentProvider.maybeAddUserId(expected, imageUserHande.getIdentifier());
+
+        assertEquals(expected, uriCaptor.getValue());
+    }
+
+    @Test
+    public void testSetUser_noWorkProfile() {
+        mFeatureFlags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false);
+        ImageExporter exporter = new ImageExporter(mMockContentResolver, mFeatureFlags);
+
+        UserHandle imageUserHandle = UserHandle.of(10);
+
+        ArgumentCaptor<Uri> uriCaptor = ArgumentCaptor.forClass(Uri.class);
+        // Capture the URI and then return null to bail out of export.
+        Mockito.when(mMockContentResolver.insert(uriCaptor.capture(), Mockito.any())).thenReturn(
+                null);
+        exporter.export(DIRECT_EXECUTOR, UUID.fromString("3c11da99-9284-4863-b1d5-6f3684976814"),
+                null, CAPTURE_TIME, imageUserHandle);
+
+        // The user handle should be ignored here since the flag is off.
+        assertEquals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, uriCaptor.getValue());
+    }
+
     @SuppressWarnings("SameParameterValue")
     private Bitmap createCheckerBitmap(int tileSize, int w, int h) {
         Bitmap bitmap = Bitmap.createBitmap(w * tileSize, h * tileSize, Bitmap.Config.ARGB_8888);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 8c9404e..85c8ba7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -184,7 +184,7 @@
                         ActionTransition::new, mSmartActionsProvider);
 
         Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
-                Uri.parse("Screenshot_123.png")).get().action;
+                Uri.parse("Screenshot_123.png"), true).get().action;
 
         Intent intent = shareAction.actionIntent.getIntent();
         assertNotNull(intent);
@@ -212,7 +212,7 @@
                         ActionTransition::new, mSmartActionsProvider);
 
         Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
-                Uri.parse("Screenshot_123.png")).get().action;
+                Uri.parse("Screenshot_123.png"), true).get().action;
 
         Intent intent = editAction.actionIntent.getIntent();
         assertNotNull(intent);
@@ -241,7 +241,7 @@
 
         Notification.Action deleteAction = task.createDeleteAction(mContext,
                 mContext.getResources(),
-                Uri.parse("Screenshot_123.png"));
+                Uri.parse("Screenshot_123.png"), true);
 
         Intent intent = deleteAction.actionIntent.getIntent();
         assertNotNull(intent);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index 3a4da86..fa1fedb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -62,7 +62,7 @@
 import org.mockito.Mockito.`when` as whenever
 
 private const val USER_ID = 1
-private const val TASK_ID = 1
+private const val TASK_ID = 11
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java
deleted file mode 100644
index 1b515c6..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserTrackerTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.settings;
-
-import android.content.Intent;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Testing functionality of the current user tracker
- */
-@SmallTest
-public class CurrentUserTrackerTest extends SysuiTestCase {
-
-    private CurrentUserTracker mTracker;
-    private CurrentUserTracker.UserReceiver mReceiver;
-    @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mReceiver = new CurrentUserTracker.UserReceiver(mBroadcastDispatcher);
-        mTracker = new CurrentUserTracker(mReceiver) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                stopTracking();
-            }
-        };
-    }
-
-    @Test
-    public void testBroadCastDoesntCrashOnConcurrentModification() {
-        mTracker.startTracking();
-        CurrentUserTracker secondTracker = new CurrentUserTracker(mReceiver) {
-            @Override
-            public void onUserSwitched(int newUserId) {
-                stopTracking();
-            }
-        };
-        secondTracker.startTracking();
-        triggerUserSwitch();
-    }
-    /**
-     * Simulates a user switch event.
-     */
-    private void triggerUserSwitch() {
-        Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
-        intent.putExtra(Intent.EXTRA_USER_HANDLE, 1);
-        mReceiver.onReceive(getContext(), intent);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index bd4b94e..52462c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -311,6 +311,37 @@
     }
 
     @Test
+    fun testCallbackCalledOnUserInfoChanged() {
+        tracker.initialize(0)
+        val callback = TestCallback()
+        tracker.addCallback(callback, executor)
+        val profileID = tracker.userId + 10
+
+        `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
+            val id = invocation.getArgument<Int>(0)
+            val info = UserInfo(id, "", UserInfo.FLAG_FULL)
+            val infoProfile = UserInfo(
+                id + 10,
+                "",
+                "",
+                UserInfo.FLAG_MANAGED_PROFILE,
+                UserManager.USER_TYPE_PROFILE_MANAGED
+            )
+            infoProfile.profileGroupId = id
+            listOf(info, infoProfile)
+        }
+
+        val intent = Intent(Intent.ACTION_USER_INFO_CHANGED)
+            .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID))
+
+        tracker.onReceive(context, intent)
+
+        assertThat(callback.calledOnUserChanged).isEqualTo(0)
+        assertThat(callback.calledOnProfilesChanged).isEqualTo(1)
+        assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID)
+    }
+
+    @Test
     fun testCallbackRemoved() {
         tracker.initialize(0)
         val newID = 5
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
new file mode 100644
index 0000000..9d1802a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.settings.brightness
+
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class BrightnessDialogTest : SysuiTestCase() {
+
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
+    @Mock private lateinit var mainExecutor: Executor
+    @Mock private lateinit var backgroundHandler: Handler
+    @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object : SingleActivityFactory<TestDialog>(TestDialog::class.java) {
+                override fun create(intent: Intent?): TestDialog {
+                    return TestDialog(
+                        userTracker,
+                        brightnessSliderControllerFactory,
+                        mainExecutor,
+                        backgroundHandler
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        `when`(brightnessSliderControllerFactory.create(any(), any()))
+            .thenReturn(brightnessSliderController)
+        `when`(brightnessSliderController.rootView).thenReturn(View(context))
+
+        activityRule.launchActivity(null)
+    }
+
+    @After
+    fun tearDown() {
+        activityRule.finishActivity()
+    }
+
+    @Test
+    fun testGestureExclusion() {
+        val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container)
+
+        val lp = frame.layoutParams as ViewGroup.MarginLayoutParams
+        val horizontalMargin =
+            activityRule.activity.resources.getDimensionPixelSize(
+                R.dimen.notification_side_paddings
+            )
+        assertThat(lp.leftMargin).isEqualTo(horizontalMargin)
+        assertThat(lp.rightMargin).isEqualTo(horizontalMargin)
+
+        assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1)
+        val exclusion = frame.systemGestureExclusionRects[0]
+        assertThat(exclusion)
+            .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height))
+    }
+
+    class TestDialog(
+        userTracker: UserTracker,
+        brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+        mainExecutor: Executor,
+        backgroundHandler: Handler
+    ) :
+        BrightnessDialog(
+            userTracker,
+            brightnessSliderControllerFactory,
+            mainExecutor,
+            backgroundHandler
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
index 0ce9056..bc17c19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -320,6 +321,64 @@
         assertThat(changes.largeScreenConstraintsChanges).isNull()
     }
 
+    @Test
+    fun testRelevantViewsAreNotMatchConstraints() {
+        val views = mapOf(
+                R.id.clock to "clock",
+                R.id.date to "date",
+                R.id.statusIcons to "icons",
+                R.id.privacy_container to "privacy",
+                R.id.carrier_group to "carriers",
+                R.id.batteryRemainingIcon to "battery",
+        )
+        views.forEach { (id, name) ->
+            assertWithMessage("$name has 0 height in qqs")
+                    .that(qqsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+            assertWithMessage("$name has 0 width in qqs")
+                    .that(qqsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+            assertWithMessage("$name has 0 height in qs")
+                    .that(qsConstraint.getConstraint(id).layout.mHeight).isNotEqualTo(0)
+            assertWithMessage("$name has 0 width in qs")
+                    .that(qsConstraint.getConstraint(id).layout.mWidth).isNotEqualTo(0)
+        }
+    }
+
+    @Test
+    fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() {
+        val views = mapOf(
+                R.id.clock to "clock",
+                R.id.date to "date",
+                R.id.statusIcons to "icons",
+                R.id.privacy_container to "privacy",
+                R.id.carrier_group to "carriers",
+                R.id.batteryRemainingIcon to "battery",
+        )
+        views.forEach { (id, name) ->
+            assertWithMessage("$name changes height")
+                    .that(qqsConstraint.getConstraint(id).layout.mHeight)
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mHeight)
+            assertWithMessage("$name changes width")
+                    .that(qqsConstraint.getConstraint(id).layout.mWidth)
+                    .isEqualTo(qsConstraint.getConstraint(id).layout.mWidth)
+        }
+    }
+
+    @Test
+    fun testEmptyCutoutDateIconsAreConstrainedWidth() {
+        CombinedShadeHeadersConstraintManagerImpl.emptyCutoutConstraints()()
+
+        assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue()
+        assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue()
+    }
+
+    @Test
+    fun testCenterCutoutDateIconsAreConstrainedWidth() {
+        CombinedShadeHeadersConstraintManagerImpl.centerCutoutConstraints(false, 10)()
+
+        assertThat(qqsConstraint.getConstraint(R.id.date).layout.constrainedWidth).isTrue()
+        assertThat(qqsConstraint.getConstraint(R.id.statusIcons).layout.constrainedWidth).isTrue()
+    }
+
     private operator fun ConstraintsChanges.invoke() {
         qqsConstraintsChanges?.invoke(qqsConstraint)
         qsConstraintsChanges?.invoke(qsConstraint)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index 0151822..14a3bc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -659,6 +659,51 @@
         verify(privacyIconsController, never()).onParentInvisible()
     }
 
+    @Test
+    fun clockPivotYInCenter() {
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+        var height = 100
+        val width = 50
+
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+
+        height = 150
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+    }
+
+    private fun View.executeLayoutChange(
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            listener: View.OnLayoutChangeListener
+    ) {
+        val oldLeft = this.left
+        val oldTop = this.top
+        val oldRight = this.right
+        val oldBottom = this.bottom
+        whenever(this.left).thenReturn(left)
+        whenever(this.top).thenReturn(top)
+        whenever(this.right).thenReturn(right)
+        whenever(this.bottom).thenReturn(bottom)
+        whenever(this.height).thenReturn(bottom - top)
+        whenever(this.width).thenReturn(right - left)
+        listener.onLayoutChange(
+                this,
+                oldLeft,
+                oldTop,
+                oldRight,
+                oldBottom,
+                left,
+                top,
+                right,
+                bottom
+        )
+    }
+
     private fun createWindowInsets(
         topCutout: Rect? = Rect()
     ): WindowInsets {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index c0dae03..69a4559 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -18,6 +18,7 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -33,11 +34,13 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -76,6 +79,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.keyguard.KeyguardClockSwitchController;
 import com.android.keyguard.KeyguardStatusView;
@@ -90,10 +94,8 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -102,14 +104,15 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.media.KeyguardMediaController;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -125,12 +128,12 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.events.PrivacyDotViewController;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.AmbientState;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
@@ -149,7 +152,6 @@
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -165,7 +167,6 @@
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
-import com.android.systemui.wallet.controller.QuickAccessWalletController;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import org.junit.After;
@@ -173,6 +174,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
@@ -194,9 +197,9 @@
     @Mock private KeyguardBottomAreaView mKeyguardBottomArea;
     @Mock private KeyguardBottomAreaViewController mKeyguardBottomAreaViewController;
     @Mock private KeyguardBottomAreaView mQsFrame;
-    @Mock private NotificationIconAreaController mNotificationAreaController;
     @Mock private HeadsUpManagerPhone mHeadsUpManager;
     @Mock private NotificationShelfController mNotificationShelfController;
+    @Mock private NotificationGutsManager mGutsManager;
     @Mock private KeyguardStatusBarView mKeyguardStatusBar;
     @Mock private KeyguardUserSwitcherView mUserSwitcherView;
     @Mock private ViewStub mUserSwitcherStubView;
@@ -222,7 +225,7 @@
     @Mock private Resources mResources;
     @Mock private Configuration mConfiguration;
     @Mock private KeyguardClockSwitch mKeyguardClockSwitch;
-    @Mock private MediaHierarchyManager mMediaHiearchyManager;
+    @Mock private MediaHierarchyManager mMediaHierarchyManager;
     @Mock private ConversationNotificationManager mConversationNotificationManager;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
@@ -249,19 +252,16 @@
     @Mock private UiEventLogger mUiEventLogger;
     @Mock private LockIconViewController mLockIconViewController;
     @Mock private KeyguardMediaController mKeyguardMediaController;
-    @Mock private PrivacyDotViewController mPrivacyDotViewController;
     @Mock private NavigationModeController mNavigationModeController;
+    @Mock private NavigationBarController mNavigationBarController;
     @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     @Mock private ContentResolver mContentResolver;
     @Mock private TapAgainViewController mTapAgainViewController;
     @Mock private KeyguardIndicationController mKeyguardIndicationController;
     @Mock private FragmentService mFragmentService;
     @Mock private FragmentHostManager mFragmentHostManager;
-    @Mock private QuickAccessWalletController mQuickAccessWalletController;
-    @Mock private QRCodeScannerController mQrCodeScannerController;
     @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock private RecordingController mRecordingController;
-    @Mock private ControlsComponent mControlsComponent;
     @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock private DumpManager mDumpManager;
     @Mock private InteractionJankMonitor mInteractionJankMonitor;
@@ -282,12 +282,16 @@
     @Mock private ViewTreeObserver mViewTreeObserver;
     @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    @Mock private MotionEvent mDownMotionEvent;
+    @Captor
+    private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
+            mEmptySpaceClickListenerCaptor;
 
     private NotificationPanelViewController.TouchHandler mTouchHandler;
     private ConfigurationController mConfigurationController;
     private SysuiStatusBarStateController mStatusBarStateController;
     private NotificationPanelViewController mNotificationPanelViewController;
-    private View.AccessibilityDelegate mAccessibiltyDelegate;
+    private View.AccessibilityDelegate mAccessibilityDelegate;
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
     private List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners;
     private Handler mMainHandler;
@@ -305,7 +309,7 @@
         MockitoAnnotations.initMocks(this);
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
-                mInteractionJankMonitor);
+                mInteractionJankMonitor, mShadeExpansionStateManager);
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -373,9 +377,10 @@
 
         NotificationWakeUpCoordinator coordinator =
                 new NotificationWakeUpCoordinator(
+                        mDumpManager,
                         mock(HeadsUpManagerPhone.class),
                         new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
-                                mInteractionJankMonitor),
+                                mInteractionJankMonitor, mShadeExpansionStateManager),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController);
@@ -388,6 +393,7 @@
                 mConfigurationController,
                 mStatusBarStateController,
                 mFalsingManager,
+                mShadeExpansionStateManager,
                 mLockscreenShadeTransitionController,
                 new FalsingCollectorFake(),
                 mDumpManager);
@@ -425,10 +431,10 @@
         when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
         when(mView.getParent()).thenReturn(mViewParent);
         when(mQs.getHeader()).thenReturn(mQsHeader);
+        when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
         mMainHandler = new Handler(Looper.getMainLooper());
-        NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter =
-                new NotificationPanelViewController.PanelEventsEmitter();
 
         mNotificationPanelViewController = new NotificationPanelViewController(
                 mView,
@@ -447,8 +453,9 @@
                 mShadeLog,
                 mConfigurationController,
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
-                mConversationNotificationManager, mMediaHiearchyManager,
+                mConversationNotificationManager, mMediaHierarchyManager,
                 mStatusBarKeyguardViewManager,
+                mGutsManager,
                 mNotificationsQSContainerController,
                 mNotificationStackScrollLayoutController,
                 mKeyguardStatusViewComponentFactory,
@@ -456,7 +463,6 @@
                 mKeyguardUserSwitcherComponentFactory,
                 mKeyguardStatusBarViewComponentFactory,
                 mLockscreenShadeTransitionController,
-                mNotificationAreaController,
                 mAuthController,
                 mScrimController,
                 mUserManager,
@@ -465,9 +471,9 @@
                 mAmbientState,
                 mLockIconViewController,
                 mKeyguardMediaController,
-                mPrivacyDotViewController,
                 mTapAgainViewController,
                 mNavigationModeController,
+                mNavigationBarController,
                 mFragmentService,
                 mContentResolver,
                 mRecordingController,
@@ -482,22 +488,20 @@
                 mSysUiState,
                 () -> mKeyguardBottomAreaViewController,
                 mKeyguardUnlockAnimationController,
+                mKeyguardIndicationController,
                 mNotificationListContainer,
-                panelEventsEmitter,
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
                 systemClock,
-                mock(CameraGestureHelper.class),
                 mKeyguardBottomAreaViewModel,
-                mKeyguardBottomAreaInteractor);
+                mKeyguardBottomAreaInteractor,
+                mDumpManager);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
                 () -> {},
                 mNotificationShelfController);
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
-        mNotificationPanelViewController.setKeyguardIndicationController(
-                mKeyguardIndicationController);
         ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
         verify(mView, atLeast(1)).addOnAttachStateChangeListener(
@@ -507,11 +511,13 @@
         ArgumentCaptor<View.AccessibilityDelegate> accessibilityDelegateArgumentCaptor =
                 ArgumentCaptor.forClass(View.AccessibilityDelegate.class);
         verify(mView).setAccessibilityDelegate(accessibilityDelegateArgumentCaptor.capture());
-        mAccessibiltyDelegate = accessibilityDelegateArgumentCaptor.getValue();
+        mAccessibilityDelegate = accessibilityDelegateArgumentCaptor.getValue();
         mNotificationPanelViewController.getStatusBarStateController()
-                .addCallback(mNotificationPanelViewController.mStatusBarStateListener);
+                .addCallback(mNotificationPanelViewController.getStatusBarStateListener());
         mNotificationPanelViewController
                 .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        verify(mNotificationStackScrollLayoutController)
+                .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
     }
 
     @After
@@ -750,6 +756,40 @@
     }
 
     @Test
+    public void testOnTouchEvent_expansionResumesAfterBriefTouch() {
+        mFalsingManager.setIsClassifierEnabled(true);
+        mFalsingManager.setIsFalseTouch(false);
+        // Start shade collapse with swipe up
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        assertThat(mNotificationPanelViewController.isClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.isFlinging()).isTrue();
+
+        // simulate touch that does not exceed touch slop
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        mNotificationPanelViewController.setTouchSlopExceeded(false);
+
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        // fling should still be called after a touch that does not exceed touch slop
+        assertThat(mNotificationPanelViewController.isClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.isFlinging()).isTrue();
+    }
+
+    @Test
     public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
         when(mCommandQueue.panelsEnabled()).thenReturn(false);
 
@@ -801,7 +841,7 @@
     @Test
     public void testA11y_initializeNode() {
         AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
-        mAccessibiltyDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo);
+        mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo);
 
         List<AccessibilityNodeInfo.AccessibilityAction> actionList = nodeInfo.getActionList();
         assertThat(actionList).containsAtLeastElementsIn(
@@ -813,22 +853,22 @@
 
     @Test
     public void testA11y_scrollForward() {
-        mAccessibiltyDelegate.performAccessibilityAction(
+        mAccessibilityDelegate.performAccessibilityAction(
                 mView,
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId(),
                 null);
 
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
     }
 
     @Test
     public void testA11y_scrollUp() {
-        mAccessibiltyDelegate.performAccessibilityAction(
+        mAccessibilityDelegate.performAccessibilityAction(
                 mView,
                 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP.getId(),
                 null);
 
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
     }
 
     @Test
@@ -1084,6 +1124,19 @@
     }
 
     @Test
+    public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() {
+        enableSplitShade(true);
+        mStatusBarStateController.setState(SHADE);
+        mNotificationPanelViewController.setQsExpanded(true);
+
+        mStatusBarStateController.setState(KEYGUARD);
+
+
+        assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false);
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false);
+    }
+
+    @Test
     public void testSwitchesToCorrectClockInSinglePaneShade() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -1239,7 +1292,7 @@
         mNotificationPanelViewController.expandWithQs();
 
         verify(mLockscreenShadeTransitionController).goToLockedShade(
-                /* expandedView= */null, /* needsQSAnimation= */false);
+                /* expandedView= */null, /* needsQSAnimation= */true);
     }
 
     @Test
@@ -1286,11 +1339,11 @@
     public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
         enableSplitShade(/* enabled= */ true);
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
-        assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
 
         mShadeExpansionStateManager.updateState(STATE_OPENING);
 
-        assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue();
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isTrue();
     }
 
     @Test
@@ -1302,18 +1355,18 @@
         // going to lockscreen would trigger STATE_OPENING
         mShadeExpansionStateManager.updateState(STATE_OPENING);
 
-        assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
     }
 
     @Test
     public void testQsImmediateResetsWhenPanelOpensOrCloses() {
-        mNotificationPanelViewController.mQsExpandImmediate = true;
+        mNotificationPanelViewController.setQsExpandImmediate(true);
         mShadeExpansionStateManager.updateState(STATE_OPEN);
-        assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
 
-        mNotificationPanelViewController.mQsExpandImmediate = true;
+        mNotificationPanelViewController.setQsExpandImmediate(true);
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
-        assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+        assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isFalse();
     }
 
     @Test
@@ -1356,7 +1409,7 @@
 
     @Test
     public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() {
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
         when(mQsFrame.getX()).thenReturn(0f);
         when(mQsFrame.getWidth()).thenReturn(1000);
         when(mQsHeader.getTop()).thenReturn(0);
@@ -1376,7 +1429,7 @@
     @Test
     public void interceptTouchEvent_withinQs_shadeExpanded_inSplitShade_doesNotStartQsTracking() {
         enableSplitShade(true);
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
         when(mQsFrame.getX()).thenReturn(0f);
         when(mQsFrame.getWidth()).thenReturn(1000);
         when(mQsHeader.getTop()).thenReturn(0);
@@ -1452,7 +1505,7 @@
 
     @Test
     public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() {
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
 
         setIsFullWidth(true);
 
@@ -1461,7 +1514,7 @@
 
     @Test
     public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() {
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
 
         setIsFullWidth(false);
 
@@ -1470,7 +1523,7 @@
 
     @Test
     public void onLayoutChange_qsNotSet_doesNotCrash() {
-        mNotificationPanelViewController.mQs = null;
+        mNotificationPanelViewController.setQs(null);
 
         triggerLayoutChange();
     }
@@ -1496,7 +1549,7 @@
     @Test
     public void setQsExpansion_lockscreenShadeTransitionInProgress_usesLockscreenSquishiness() {
         float squishinessFraction = 0.456f;
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
         when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
                 .thenReturn(squishinessFraction);
         when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
@@ -1509,7 +1562,7 @@
                 /* delay= */ 0
         );
 
-        mNotificationPanelViewController.setQsExpansion(/* height= */ 123);
+        mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123);
 
         // First for setTransitionToFullShadeAmount and then setQsExpansion
         verify(mQs, times(2)).setQsExpansion(
@@ -1524,13 +1577,13 @@
     public void setQsExpansion_lockscreenShadeTransitionNotInProgress_usesStandardSquishiness() {
         float lsSquishinessFraction = 0.456f;
         float nsslSquishinessFraction = 0.987f;
-        mNotificationPanelViewController.mQs = mQs;
+        mNotificationPanelViewController.setQs(mQs);
         when(mLockscreenShadeTransitionController.getQsSquishTransitionFraction())
                 .thenReturn(lsSquishinessFraction);
         when(mNotificationStackScrollLayoutController.getNotificationSquishinessFraction())
                 .thenReturn(nsslSquishinessFraction);
 
-        mNotificationPanelViewController.setQsExpansion(/* height= */ 123);
+        mNotificationPanelViewController.setQsExpansionHeight(/* height= */ 123);
 
         verify(mQs).setQsExpansion(
                 /* expansion= */ anyFloat(),
@@ -1540,6 +1593,112 @@
         );
     }
 
+    @Test
+    public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.getStatusBarStateListener();
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor).requestFaceAuth(
+                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.getStatusBarStateListener();
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, 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);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.getStatusBarStateListener();
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+        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);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.getStatusBarStateListener();
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(true, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyString());
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.getStatusBarStateListener();
+        statusBarStateListener.onStateChanged(SHADE_LOCKED);
+
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyString());
+
+    }
+
+    /**
+     * When shade is flinging to close and this fling is not intercepted,
+     * {@link AmbientState#setIsClosing(boolean)} should be called before
+     * {@link NotificationStackScrollLayoutController#onExpansionStopped()}
+     * to ensure scrollY can be correctly set to be 0
+     */
+    @Test
+    public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
+        // Given: Shade is expanded
+        mNotificationPanelViewController.notifyExpandingFinished();
+        mNotificationPanelViewController.setClosing(false);
+
+        // When: Shade flings to close not canceled
+        mNotificationPanelViewController.notifyExpandingStarted();
+        mNotificationPanelViewController.setClosing(true);
+        mNotificationPanelViewController.onFlingEnd(false);
+
+        // Then: AmbientState's mIsClosing should be set to false
+        // before mNotificationStackScrollLayoutController.onExpansionStopped() is called
+        // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition
+        // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed
+        InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController);
+        inOrder.verify(mAmbientState).setIsClosing(false);
+        inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped();
+    }
+
+    @Test
+    public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() {
+        mStatusBarStateController.setState(SHADE);
+        enableSplitShade(true);
+        int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
+        mNotificationPanelViewController.setExpandedHeight(transitionDistance);
+        assertThat(mNotificationPanelViewController.isFullyExpanded()).isTrue();
+    }
+
     private static MotionEvent createMotionEvent(int x, int y, int action) {
         return MotionEvent.obtain(
                 /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index 12ef036..bdafc7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
@@ -66,6 +66,8 @@
     @Mock
     private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController
     @Mock
+    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock
     private lateinit var featureFlags: FeatureFlags
     @Captor
     lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@@ -96,6 +98,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
@@ -380,6 +383,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index ad3d3d2..d7d17b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -88,6 +88,7 @@
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private AuthController mAuthController;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
@@ -103,7 +104,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController) {
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
@@ -238,9 +239,9 @@
 
     @Test
     public void setPanelExpanded_notFocusable_altFocusable_whenPanelIsOpen() {
-        mNotificationShadeWindowController.setPanelExpanded(true);
+        mNotificationShadeWindowController.onShadeExpansionFullyChanged(true);
         clearInvocations(mWindowManager);
-        mNotificationShadeWindowController.setPanelExpanded(true);
+        mNotificationShadeWindowController.onShadeExpansionFullyChanged(true);
         verifyNoMoreInteractions(mWindowManager);
         mNotificationShadeWindowController.setNotificationShadeFocusable(true);
 
@@ -312,7 +313,7 @@
         verifyNoMoreInteractions(mWindowManager);
 
         clearInvocations(mWindowManager);
-        mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+        mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> {
             mNotificationShadeWindowController.setForceDozeBrightness(false);
             verify(mWindowManager, never()).updateViewLayout(any(), any());
         });
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index db7e017..c3207c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
+import com.android.systemui.statusbar.NotificationInsetsController
 import com.android.systemui.statusbar.NotificationShadeDepthController
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -94,6 +95,8 @@
     private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
     @Mock
     private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock
+    private lateinit var notificationInsetsController: NotificationInsetsController
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerContainer: ViewGroup
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -124,6 +127,7 @@
             centralSurfaces,
             notificationShadeWindowController,
             keyguardUnlockAnimationController,
+            notificationInsetsController,
             ambientState,
             pulsingGestureListener,
             featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 26a0770..4bf00c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.NotificationInsetsController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -91,6 +92,7 @@
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel;
     @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
+    @Mock private NotificationInsetsController mNotificationInsetsController;
 
     @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler>
             mInteractionEventHandlerCaptor;
@@ -125,6 +127,7 @@
                 mCentralSurfaces,
                 mNotificationShadeWindowController,
                 mKeyguardUnlockAnimationController,
+                mNotificationInsetsController,
                 mAmbientState,
                 mPulsingGestureListener,
                 mFeatureFlags,
@@ -152,7 +155,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should intercept touch
@@ -165,7 +168,7 @@
 
         // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(false);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(false);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we shouldn't intercept touch
@@ -178,7 +181,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should handle the touch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 09add65..43c6942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -66,6 +66,8 @@
     private lateinit var dumpManager: DumpManager
     @Mock
     private lateinit var statusBarStateController: StatusBarStateController
+    @Mock
+    private lateinit var shadeLogger: ShadeLogger
 
     private lateinit var tunableCaptor: ArgumentCaptor<Tunable>
     private lateinit var underTest: PulsingGestureListener
@@ -81,6 +83,7 @@
                 centralSurfaces,
                 ambientDisplayConfiguration,
                 statusBarStateController,
+                shadeLogger,
                 tunerService,
                 dumpManager
         )
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/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index eb34561..cc45cf88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -55,7 +56,7 @@
         clockView.animateAppearOnLockscreen()
         clockView.measure(50, 50)
 
-        verify(mockTextAnimator).glyphFilter = null
+        verify(mockTextAnimator).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
     }
@@ -66,7 +67,7 @@
         clockView.measure(50, 50)
         clockView.animateAppearOnLockscreen()
 
-        verify(mockTextAnimator, times(2)).glyphFilter = null
+        verify(mockTextAnimator, times(2)).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null)
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ffb41e5..28bd26a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Handler
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -32,6 +33,7 @@
 import com.android.systemui.util.mockito.eq
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.fail
+import org.json.JSONException
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -104,13 +106,14 @@
             mockContext,
             mockPluginManager,
             mockHandler,
-            fakeDefaultProvider
+            isEnabled = true,
+            userHandle = UserHandle.USER_ALL,
+            defaultClockProvider = fakeDefaultProvider
         ) {
             override var currentClockId: ClockId
                 get() = settingValue
                 set(value) { settingValue = value }
         }
-        registry.isEnabled = true
 
         verify(mockPluginManager)
             .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
@@ -236,4 +239,52 @@
         pluginListener.onPluginDisconnected(plugin2)
         assertEquals(1, changeCallCount)
     }
+
+    @Test
+    fun jsonDeserialization_gotExpectedObject() {
+        val expected = ClockRegistry.ClockSetting("ID", 500)
+        val actual = ClockRegistry.ClockSetting.deserialize("""{
+            "clockId":"ID",
+            "_applied_timestamp":500
+        }""")
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun jsonDeserialization_noTimestamp_gotExpectedObject() {
+        val expected = ClockRegistry.ClockSetting("ID", null)
+        val actual = ClockRegistry.ClockSetting.deserialize("{\"clockId\":\"ID\"}")
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
+        val expected = ClockRegistry.ClockSetting("ID", null)
+        val actual = ClockRegistry.ClockSetting.deserialize("""{
+            "clockId":"ID",
+            "_applied_timestamp":null
+        }""")
+        assertEquals(expected, actual)
+    }
+
+    @Test(expected = JSONException::class)
+    fun jsonDeserialization_noId_threwException() {
+        val expected = ClockRegistry.ClockSetting("ID", 500)
+        val actual = ClockRegistry.ClockSetting.deserialize("{\"_applied_timestamp\":500}")
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun jsonSerialization_gotExpectedString() {
+        val expected = "{\"clockId\":\"ID\",\"_applied_timestamp\":500}"
+        val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", 500))
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun jsonSerialization_noTimestamp_gotExpectedString() {
+        val expected = "{\"clockId\":\"ID\"}"
+        val actual = ClockRegistry.ClockSetting.serialize( ClockRegistry.ClockSetting("ID", null))
+        assertEquals(expected, actual)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index 539a54b..a7588dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -43,6 +43,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.notNull
 import org.mockito.Mock
+import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -139,12 +140,19 @@
     }
 
     @Test
-    fun defaultClock_events_onFontSettingChanged() {
+    fun defaultSmallClock_events_onFontSettingChanged() {
         val clock = provider.createClock(DEFAULT_CLOCK_ID)
-        clock.events.onFontSettingChanged()
+        clock.smallClock.events.onFontSettingChanged(100f)
 
-        verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat())
-        verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), anyFloat())
+        verify(mockSmallClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(100f))
+    }
+
+    @Test
+    fun defaultLargeClock_events_onFontSettingChanged() {
+        val clock = provider.createClock(DEFAULT_CLOCK_ID)
+        clock.largeClock.events.onFontSettingChanged(200f)
+
+        verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(200f))
         verify(mockLargeClockView).setLayoutParams(any())
     }
 
@@ -171,4 +179,12 @@
         verify(mockSmallClockView, times(2)).refreshFormat()
         verify(mockLargeClockView, times(2)).refreshFormat()
     }
+
+    @Test
+    fun test_aodClock_always_whiteColor() {
+        val clock = provider.createClock(DEFAULT_CLOCK_ID)
+        clock.animations.doze(0.9f) // set AOD mode to active
+        clock.smallClock.events.onRegionDarknessChanged(true)
+        verify((clock.smallClock.view as AnimatableClockView), never()).animateAppearOnLockscreen()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
new file mode 100644
index 0000000..5a62cc1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplerTest.kt
@@ -0,0 +1,102 @@
+package com.android.systemui.shared.regionsampling
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.navigationbar.RegionSamplingHelper
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RegionSamplerTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    @Mock private lateinit var sampledView: View
+    @Mock private lateinit var mainExecutor: Executor
+    @Mock private lateinit var bgExecutor: Executor
+    @Mock private lateinit var regionSampler: RegionSamplingHelper
+    @Mock private lateinit var pw: PrintWriter
+    @Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback
+
+    private lateinit var mRegionSampler: RegionSampler
+    private var updateFun: UpdateColorCallback = {}
+
+    @Before
+    fun setUp() {
+        whenever(sampledView.isAttachedToWindow).thenReturn(true)
+        whenever(regionSampler.callback).thenReturn(this@RegionSamplerTest.callback)
+
+        mRegionSampler =
+            object : RegionSampler(sampledView, mainExecutor, bgExecutor, true, updateFun) {
+                override fun createRegionSamplingHelper(
+                    sampledView: View,
+                    callback: RegionSamplingHelper.SamplingCallback,
+                    mainExecutor: Executor?,
+                    bgExecutor: Executor?
+                ): RegionSamplingHelper {
+                    return this@RegionSamplerTest.regionSampler
+                }
+            }
+    }
+
+    @Test
+    fun testStartRegionSampler() {
+        mRegionSampler.startRegionSampler()
+
+        verify(regionSampler).start(Rect(0, 0, 0, 0))
+    }
+
+    @Test
+    fun testStopRegionSampler() {
+        mRegionSampler.stopRegionSampler()
+
+        verify(regionSampler).stop()
+    }
+
+    @Test
+    fun testDump() {
+        mRegionSampler.dump(pw)
+
+        verify(regionSampler).dump(pw)
+    }
+
+    @Test
+    fun testUpdateColorCallback() {
+        regionSampler.callback.onRegionDarknessChanged(false)
+        verify(regionSampler.callback).onRegionDarknessChanged(false)
+        clearInvocations(regionSampler.callback)
+        regionSampler.callback.onRegionDarknessChanged(true)
+        verify(regionSampler.callback).onRegionDarknessChanged(true)
+    }
+
+    @Test
+    fun testFlagFalse() {
+        mRegionSampler =
+            object : RegionSampler(sampledView, mainExecutor, bgExecutor, false, updateFun) {
+                override fun createRegionSamplingHelper(
+                    sampledView: View,
+                    callback: RegionSamplingHelper.SamplingCallback,
+                    mainExecutor: Executor?,
+                    bgExecutor: Executor?
+                ): RegionSamplingHelper {
+                    return this@RegionSamplerTest.regionSampler
+                }
+            }
+
+        Assert.assertEquals(mRegionSampler.regionSampler, null)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt
deleted file mode 100644
index 09d51f6..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/regionsampling/RegionSamplingInstanceTest.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package com.android.systemui.shared.regionsampling
-
-import android.graphics.Rect
-import android.testing.AndroidTestingRunner
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.shared.navigationbar.RegionSamplingHelper
-import java.io.PrintWriter
-import java.util.concurrent.Executor
-import org.junit.Assert
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.junit.MockitoJUnit
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class RegionSamplingInstanceTest : SysuiTestCase() {
-
-    @JvmField @Rule
-    val mockito = MockitoJUnit.rule()
-
-    @Mock private lateinit var sampledView: View
-    @Mock private lateinit var mainExecutor: Executor
-    @Mock private lateinit var bgExecutor: Executor
-    @Mock private lateinit var regionSampler: RegionSamplingHelper
-    @Mock private lateinit var updateFun: RegionSamplingInstance.UpdateColorCallback
-    @Mock private lateinit var pw: PrintWriter
-    @Mock private lateinit var callback: RegionSamplingHelper.SamplingCallback
-
-    private lateinit var regionSamplingInstance: RegionSamplingInstance
-
-    @Before
-    fun setUp() {
-        whenever(sampledView.isAttachedToWindow).thenReturn(true)
-        whenever(regionSampler.callback).thenReturn(this@RegionSamplingInstanceTest.callback)
-
-        regionSamplingInstance = object : RegionSamplingInstance(
-                sampledView,
-                mainExecutor,
-                bgExecutor,
-                true,
-                updateFun
-        ) {
-            override fun createRegionSamplingHelper(
-                    sampledView: View,
-                    callback: RegionSamplingHelper.SamplingCallback,
-                    mainExecutor: Executor?,
-                    bgExecutor: Executor?
-            ): RegionSamplingHelper {
-                return this@RegionSamplingInstanceTest.regionSampler
-            }
-        }
-    }
-
-    @Test
-    fun testStartRegionSampler() {
-        regionSamplingInstance.startRegionSampler()
-
-        verify(regionSampler).start(Rect(0, 0, 0, 0))
-    }
-
-    @Test
-    fun testStopRegionSampler() {
-        regionSamplingInstance.stopRegionSampler()
-
-        verify(regionSampler).stop()
-    }
-
-    @Test
-    fun testDump() {
-        regionSamplingInstance.dump(pw)
-
-        verify(regionSampler).dump(pw)
-    }
-
-    @Test
-    fun testUpdateColorCallback() {
-        regionSampler.callback.onRegionDarknessChanged(false)
-        verify(regionSampler.callback).onRegionDarknessChanged(false)
-        clearInvocations(regionSampler.callback)
-        regionSampler.callback.onRegionDarknessChanged(true)
-        verify(regionSampler.callback).onRegionDarknessChanged(true)
-    }
-
-    @Test
-    fun testFlagFalse() {
-        regionSamplingInstance = object : RegionSamplingInstance(
-                sampledView,
-                mainExecutor,
-                bgExecutor,
-                false,
-                updateFun
-        ) {
-            override fun createRegionSamplingHelper(
-                    sampledView: View,
-                    callback: RegionSamplingHelper.SamplingCallback,
-                    mainExecutor: Executor?,
-                    bgExecutor: Executor?
-            ): RegionSamplingHelper {
-                return this@RegionSamplingInstanceTest.regionSampler
-            }
-        }
-
-        Assert.assertEquals(regionSamplingInstance.regionSampler, null)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index cf5fa87..4478039 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -16,20 +16,21 @@
 
 package com.android.systemui.shared.system;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -40,6 +41,7 @@
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -68,17 +70,20 @@
         TransitionInfo combined = new TransitionInfoBuilder(TRANSIT_CLOSE)
                 .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER,
                         createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD))
+                // Embedded TaskFragment should be excluded when animated with Task.
+                .addChange(TRANSIT_CLOSE, FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, null /* taskInfo */)
                 .addChange(TRANSIT_CLOSE, 0 /* flags */,
                         createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD))
                 .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
-                .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
+                .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */)
+                .build();
         // Check apps extraction
-        RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
+        RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
                 mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(2, wrapped.length);
         int changeLayer = -1;
         int closeLayer = -1;
-        for (RemoteAnimationTargetCompat t : wrapped) {
+        for (RemoteAnimationTarget t : wrapped) {
             if (t.mode == MODE_CHANGING) {
                 changeLayer = t.prefixOrderIndex;
             } else if (t.mode == MODE_CLOSING) {
@@ -91,14 +96,14 @@
         assertTrue(closeLayer < changeLayer);
 
         // Check wallpaper extraction
-        RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, wallps.length);
         assertTrue(wallps[0].prefixOrderIndex < closeLayer);
         assertEquals(MODE_OPENING, wallps[0].mode);
 
         // Check non-apps extraction
-        RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, nonApps.length);
         assertTrue(nonApps[0].prefixOrderIndex < closeLayer);
@@ -115,9 +120,9 @@
         change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME));
         change.setEndAbsBounds(endBounds);
         change.setEndRelOffset(0, 0);
-        final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change,
-                0 /* order */, tinfo, mock(SurfaceControl.Transaction.class));
-        assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType);
+        RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget(
+                change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null);
+        assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType());
         assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds);
         assertEquals(endBounds, wrapped.screenSpaceBounds);
         assertTrue(wrapped.isTranslucent);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index 5b34a95..b761647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -17,58 +17,58 @@
 
 @SmallTest
 class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
-  private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
+    private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
 
-  @Mock private lateinit var mockHandler: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler: UncaughtExceptionHandler
 
-  @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
 
-  @Before
-  fun setUp() {
-    MockitoAnnotations.initMocks(this)
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager = UncaughtExceptionPreHandlerManager()
-  }
-
-  @Test
-  fun registerHandler_registersOnceOnly() {
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_setsUncaughtExceptionPreHandler() {
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager.registerHandler(mockHandler)
-    assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
-  }
-
-  @Test
-  fun registerHandler_preservesOriginalHandler() {
-    Thread.setUncaughtExceptionPreHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  @Ignore
-  fun registerHandler_toleratesHandlersThatThrow() {
-    `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler2, only()).uncaughtException(any(), any())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_doesNotSetUpTwice() {
-    UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
-    assertThrows(IllegalStateException::class.java) {
-      preHandlerManager.registerHandler(mockHandler)
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager = UncaughtExceptionPreHandlerManager()
     }
-  }
+
+    @Test
+    fun registerHandler_registersOnceOnly() {
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_setsUncaughtExceptionPreHandler() {
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager.registerHandler(mockHandler)
+        assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
+    }
+
+    @Test
+    fun registerHandler_preservesOriginalHandler() {
+        Thread.setUncaughtExceptionPreHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    @Ignore
+    fun registerHandler_toleratesHandlersThatThrow() {
+        `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler2, only()).uncaughtException(any(), any())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_doesNotSetUpTwice() {
+        UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
+        assertThrows(IllegalStateException::class.java) {
+            preHandlerManager.registerHandler(mockHandler)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index cf7f8dd..08933fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -33,9 +33,10 @@
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -135,27 +136,29 @@
     public void testOnSystemBarAttributesChanged() {
         doTestOnSystemBarAttributesChanged(DEFAULT_DISPLAY, 1,
                 new AppearanceRegion[]{new AppearanceRegion(2, new Rect())}, false,
-                BEHAVIOR_DEFAULT, new InsetsVisibilities(), "test", TEST_LETTERBOX_DETAILS);
+                BEHAVIOR_DEFAULT, WindowInsets.Type.defaultVisible(), "test",
+                TEST_LETTERBOX_DETAILS);
     }
 
     @Test
     public void testOnSystemBarAttributesChangedForSecondaryDisplay() {
         doTestOnSystemBarAttributesChanged(SECONDARY_DISPLAY, 1,
                 new AppearanceRegion[]{new AppearanceRegion(2, new Rect())}, false,
-                BEHAVIOR_DEFAULT, new InsetsVisibilities(), "test", TEST_LETTERBOX_DETAILS);
+                BEHAVIOR_DEFAULT, WindowInsets.Type.defaultVisible(), "test",
+                TEST_LETTERBOX_DETAILS);
     }
 
     private void doTestOnSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName,
+            @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails) {
         mCommandQueue.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
-                navbarColorManagedByIme, behavior, requestedVisibilities, packageName,
+                navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName,
                 letterboxDetails);
         waitForIdleSync();
         verify(mCallbacks).onSystemBarAttributesChanged(eq(displayId), eq(appearance),
                 eq(appearanceRegions), eq(navbarColorManagedByIme), eq(behavior),
-                eq(requestedVisibilities), eq(packageName), eq(letterboxDetails));
+                eq(requestedVisibleTypes), eq(packageName), eq(letterboxDetails));
     }
 
     @Test
@@ -492,11 +495,12 @@
     }
 
     @Test
-    public void testSetUdfpsHbmListener() {
-        final IUdfpsHbmListener listener = mock(IUdfpsHbmListener.class);
-        mCommandQueue.setUdfpsHbmListener(listener);
+    public void testSetUdfpsRefreshRateCallback() {
+        final IUdfpsRefreshRateRequestCallback callback =
+                mock(IUdfpsRefreshRateRequestCallback.class);
+        mCommandQueue.setUdfpsRefreshRateCallback(callback);
         waitForIdleSync();
-        verify(mCallbacks).setUdfpsHbmListener(eq(listener));
+        verify(mCallbacks).setUdfpsRefreshRateCallback(eq(callback));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index ec5d089..c8a392b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -20,6 +20,7 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
+import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT;
 
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
@@ -31,12 +32,12 @@
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_DISCLOSURE;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_LOGOUT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_OWNER_INFO;
-import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_RESTING;
+import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRANSIENT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST;
-import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_OFF;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
+import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_TURNING_ON;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -54,6 +55,7 @@
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.Instrumentation;
@@ -85,9 +87,12 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.TrustGrantFlags;
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.FaceHelpMessageDeferral;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
@@ -173,6 +178,8 @@
     private FaceHelpMessageDeferral mFaceHelpMessageDeferral;
     @Mock
     private ScreenLifecycle mScreenLifecycle;
+    @Mock
+    private AuthController mAuthController;
     @Captor
     private ArgumentCaptor<DockManager.AlignmentStateListener> mAlignmentListener;
     @Captor
@@ -263,9 +270,10 @@
                 mWakeLockBuilder,
                 mKeyguardStateController, mStatusBarStateController, mKeyguardUpdateMonitor,
                 mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats,
-                mUserManager, mExecutor, mExecutor,  mFalsingManager, mLockPatternUtils,
-                mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager,
-                mFaceHelpMessageDeferral);
+                mUserManager, mExecutor, mExecutor, mFalsingManager,
+                mAuthController, mLockPatternUtils, mScreenLifecycle,
+                mKeyguardBypassController, mAccessibilityManager,
+                mFaceHelpMessageDeferral, mock(KeyguardLogger.class));
         mController.init();
         mController.setIndicationArea(mIndicationArea);
         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
@@ -822,31 +830,6 @@
     }
 
     @Test
-    public void updateMonitor_listenerUpdatesIndication() {
-        createController();
-        String restingIndication = "Resting indication";
-        reset(mKeyguardUpdateMonitor);
-
-        mController.setVisible(true);
-        verifyIndicationMessage(INDICATION_TYPE_USER_LOCKED,
-                mContext.getString(com.android.internal.R.string.lockscreen_storage_locked));
-
-        reset(mRotateTextViewController);
-        when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
-        when(mKeyguardUpdateMonitor.isUserUnlocked(anyInt())).thenReturn(true);
-        mController.setRestingIndication(restingIndication);
-        verifyHideIndication(INDICATION_TYPE_USER_LOCKED);
-        verifyIndicationMessage(INDICATION_TYPE_RESTING, restingIndication);
-
-        reset(mRotateTextViewController);
-        reset(mKeyguardUpdateMonitor);
-        when(mKeyguardUpdateMonitor.isUserUnlocked(anyInt())).thenReturn(true);
-        when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false);
-        mKeyguardStateControllerCallback.onUnlockedChanged();
-        verifyIndicationMessage(INDICATION_TYPE_RESTING, restingIndication);
-    }
-
-    @Test
     public void onRefreshBatteryInfo_computesChargingTime() throws RemoteException {
         createController();
         BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_CHARGING,
@@ -1061,7 +1044,8 @@
 
         // GIVEN a trust granted message but trust isn't granted
         final String trustGrantedMsg = "testing trust granted message";
-        mController.getKeyguardCallback().showTrustGrantedMessage(trustGrantedMsg);
+        mController.getKeyguardCallback().onTrustGrantedForCurrentUser(
+                false, new TrustGrantFlags(0), trustGrantedMsg);
 
         verifyHideIndication(INDICATION_TYPE_TRUST);
 
@@ -1085,7 +1069,8 @@
 
         // WHEN the showTrustGranted method is called
         final String trustGrantedMsg = "testing trust granted message";
-        mController.getKeyguardCallback().showTrustGrantedMessage(trustGrantedMsg);
+        mController.getKeyguardCallback().onTrustGrantedForCurrentUser(
+                false, new TrustGrantFlags(0), trustGrantedMsg);
 
         // THEN verify the trust granted message shows
         verifyIndicationMessage(
@@ -1102,7 +1087,8 @@
         when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
 
         // WHEN the showTrustGranted method is called with a null message
-        mController.getKeyguardCallback().showTrustGrantedMessage(null);
+        mController.getKeyguardCallback().onTrustGrantedForCurrentUser(
+                false, new TrustGrantFlags(0), null);
 
         // THEN verify the default trust granted message shows
         verifyIndicationMessage(
@@ -1119,7 +1105,8 @@
         when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true);
 
         // WHEN the showTrustGranted method is called with an EMPTY string
-        mController.getKeyguardCallback().showTrustGrantedMessage("");
+        mController.getKeyguardCallback().onTrustGrantedForCurrentUser(
+                false, new TrustGrantFlags(0), "");
 
         // THEN verify NO trust message is shown
         verifyNoMessage(INDICATION_TYPE_TRUST);
@@ -1374,6 +1361,201 @@
     }
 
 
+    @Test
+    public void onBiometricError_faceLockedOutFirstTime_showsThePassedInMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, "first lockout");
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutFirstTimeAndFpAllowed_showsTheFpFollowupMessage() {
+        createController();
+        fingerprintUnlockIsPossible();
+        onFaceLockoutError("first lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_suggest_fingerprint));
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutFirstTimeAndFpNotAllowed_showsDefaultFollowup() {
+        createController();
+        fingerprintUnlockIsNotPossible();
+        onFaceLockoutError("first lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutSecondTimeInSession_showsUnavailableMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+
+        onFaceLockoutError("second lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                mContext.getString(R.string.keyguard_face_unlock_unavailable));
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutSecondTimeOnBouncer_showsUnavailableMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
+
+        onFaceLockoutError("second lockout");
+
+        verify(mStatusBarKeyguardViewManager)
+                .setKeyguardMessage(
+                        eq(mContext.getString(R.string.keyguard_face_unlock_unavailable)),
+                        any());
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutSecondTimeButUdfpsActive_showsNoMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+
+        when(mAuthController.isUdfpsFingerDown()).thenReturn(true);
+        onFaceLockoutError("second lockout");
+
+        verifyNoMoreInteractions(mRotateTextViewController);
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutAgainAndFpAllowed_showsTheFpFollowupMessage() {
+        createController();
+        fingerprintUnlockIsPossible();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+
+        onFaceLockoutError("second lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_suggest_fingerprint));
+    }
+
+    @Test
+    public void onBiometricError_faceLockedOutAgainAndFpNotAllowed_showsDefaultFollowup() {
+        createController();
+        fingerprintUnlockIsNotPossible();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+
+        onFaceLockoutError("second lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricError_whenFaceLockoutReset_onLockOutError_showsPassedInMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+        when(mKeyguardUpdateMonitor.isFaceLockedOut()).thenReturn(false);
+        mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE);
+
+        onFaceLockoutError("second lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, "second lockout");
+    }
+
+    @Test
+    public void onFpLockoutStateChanged_whenFpIsLockedOut_showsPersistentMessage() {
+        createController();
+        mController.setVisible(true);
+        when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(true);
+
+        mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT);
+
+        verifyIndicationShown(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onFpLockoutStateChanged_whenFpIsNotLockedOut_showsPersistentMessage() {
+        createController();
+        mController.setVisible(true);
+        clearInvocations(mRotateTextViewController);
+        when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(false);
+
+        mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT);
+
+        verifyHideIndication(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE);
+    }
+
+    @Test
+    public void onVisibilityChange_showsPersistentMessage_ifFpIsLockedOut() {
+        createController();
+        mController.setVisible(false);
+        when(mKeyguardUpdateMonitor.isFingerprintLockedOut()).thenReturn(true);
+        mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT);
+        clearInvocations(mRotateTextViewController);
+
+        mController.setVisible(true);
+
+        verifyIndicationShown(INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricError_whenFaceIsLocked_onMultipleLockOutErrors_showUnavailableMessage() {
+        createController();
+        onFaceLockoutError("first lockout");
+        clearInvocations(mRotateTextViewController);
+        when(mKeyguardUpdateMonitor.isFaceLockedOut()).thenReturn(true);
+        mKeyguardUpdateMonitorCallback.onLockedOutStateChanged(BiometricSourceType.FACE);
+
+        onFaceLockoutError("second lockout");
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                mContext.getString(R.string.keyguard_face_unlock_unavailable));
+    }
+
+    @Test
+    public void onBiometricError_screenIsTurningOn_faceLockedOutFpIsNotAvailable_showsMessage() {
+        createController();
+        screenIsTurningOn();
+        fingerprintUnlockIsNotPossible();
+
+        onFaceLockoutError("lockout error");
+        verifyNoMoreInteractions(mRotateTextViewController);
+
+        mScreenObserver.onScreenTurnedOn();
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                "lockout error");
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_unlock));
+    }
+
+    @Test
+    public void onBiometricError_screenIsTurningOn_faceLockedOutFpIsAvailable_showsMessage() {
+        createController();
+        screenIsTurningOn();
+        fingerprintUnlockIsPossible();
+
+        onFaceLockoutError("lockout error");
+        verifyNoMoreInteractions(mRotateTextViewController);
+
+        mScreenObserver.onScreenTurnedOn();
+
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                "lockout error");
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_suggest_fingerprint));
+    }
+
+    private void screenIsTurningOn() {
+        when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_TURNING_ON);
+    }
 
     private void sendUpdateDisclosureBroadcast() {
         mBroadcastReceiver.onReceive(mContext, new Intent());
@@ -1422,4 +1604,33 @@
                     anyObject(), anyBoolean());
         }
     }
+
+    private void verifyIndicationShown(int indicationType, String message) {
+        verify(mRotateTextViewController)
+                .updateIndication(eq(indicationType),
+                        mKeyguardIndicationCaptor.capture(),
+                        eq(true));
+        assertThat(mKeyguardIndicationCaptor.getValue().getMessage().toString())
+                .isEqualTo(message);
+    }
+
+    private void fingerprintUnlockIsNotPossible() {
+        setupFingerprintUnlockPossible(false);
+    }
+
+    private void fingerprintUnlockIsPossible() {
+        setupFingerprintUnlockPossible(true);
+    }
+
+    private void setupFingerprintUnlockPossible(boolean possible) {
+        when(mKeyguardUpdateMonitor
+                .getCachedIsUnlockWithFingerprintPossible(KeyguardUpdateMonitor.getCurrentUser()))
+                .thenReturn(possible);
+    }
+
+    private void onFaceLockoutError(String errMsg) {
+        mKeyguardUpdateMonitorCallback.onBiometricError(FACE_ERROR_LOCKOUT_PERMANENT,
+                errMsg,
+                BiometricSourceType.FACE);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 8cb530c..5fc0ffe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -4,7 +4,7 @@
 import android.util.DisplayMetrics
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 8643e86..3d11ced 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -10,7 +10,7 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.shade.NotificationPanelViewController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index bdafa48..15a687d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -53,6 +53,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -78,6 +79,8 @@
     private NotificationPresenter mPresenter;
     @Mock
     private UserManager mUserManager;
+    @Mock
+    private UserTracker mUserTracker;
 
     // Dependency mocks:
     @Mock
@@ -115,6 +118,7 @@
         MockitoAnnotations.initMocks(this);
 
         int currentUserId = ActivityManager.getCurrentUser();
+        when(mUserTracker.getUserId()).thenReturn(currentUserId);
         mSettings = new FakeSettings();
         mSettings.setUserId(ActivityManager.getCurrentUser());
         mCurrentUser = new UserInfo(currentUserId, "", 0);
@@ -344,6 +348,7 @@
                     mBroadcastDispatcher,
                     mDevicePolicyManager,
                     mUserManager,
+                    mUserTracker,
                     (() -> mVisibilityProvider),
                     (() -> mNotifCollection),
                     mClickNotifier,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
index 44cbe51..fbb8ebf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
@@ -56,6 +57,7 @@
     private val configurationController: ConfigurationController = mock()
     private val statusBarStateController: StatusBarStateController = mock()
     private val falsingManager: FalsingManager = mock()
+    private val shadeExpansionStateManager: ShadeExpansionStateManager = mock()
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock()
     private val falsingCollector: FalsingCollector = mock()
     private val dumpManager: DumpManager = mock()
@@ -65,7 +67,8 @@
     fun setUp() {
         whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
 
-        pulseExpansionHandler = PulseExpansionHandler(
+        pulseExpansionHandler =
+            PulseExpansionHandler(
                 mContext,
                 wakeUpCoordinator,
                 bypassController,
@@ -74,10 +77,11 @@
                 configurationController,
                 statusBarStateController,
                 falsingManager,
+                shadeExpansionStateManager,
                 lockscreenShadeTransitionController,
                 falsingCollector,
                 dumpManager
-        )
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 1d8e5de..5124eb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -48,6 +49,7 @@
 
     @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
     @Mock private lateinit var mockDarkAnimator: ObjectAnimator
+    @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
 
     private lateinit var controller: StatusBarStateControllerImpl
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -62,7 +64,7 @@
         controller = object : StatusBarStateControllerImpl(
             uiEventLogger,
             mock(DumpManager::class.java),
-            interactionJankMonitor
+            interactionJankMonitor, shadeExpansionStateManager
         ) {
             override fun createDarkAnimator(): ObjectAnimator { return mockDarkAnimator }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.kt
new file mode 100644
index 0000000..62b4e7b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileIconCarrierIdOverridesFake.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.systemui.statusbar.connectivity
+
+import android.content.res.Resources
+import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
+
+typealias CarrierId = Int
+
+typealias NetworkType = String
+
+typealias ResId = Int
+
+class MobileIconCarrierIdOverridesFake : MobileIconCarrierIdOverrides {
+    /** Backing for [carrierIdEntryExists] */
+    var overriddenIds = mutableSetOf<Int>()
+
+    /** Backing for [getOverrideFor]. Map should be Map< CarrierId < NetworkType, ResId>> */
+    var overridesByCarrierId = mutableMapOf<CarrierId, Map<NetworkType, ResId>>()
+
+    override fun getOverrideFor(
+        carrierId: CarrierId,
+        networkType: NetworkType,
+        resources: Resources
+    ): ResId {
+        if (!overriddenIds.contains(carrierId)) return 0
+
+        return overridesByCarrierId[carrierId]?.get(networkType) ?: 0
+    }
+
+    override fun carrierIdEntryExists(carrierId: Int): Boolean {
+        return overriddenIds.contains(carrierId)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java
deleted file mode 100644
index 7ddfde3..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.java
+++ /dev/null
@@ -1,137 +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.
- */
-
-package com.android.systemui.statusbar.connectivity;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-
-import com.android.settingslib.mobile.TelephonyIcons;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class MobileStateTest extends SysuiTestCase {
-
-    private final MobileState mState = new MobileState();
-
-    @Before
-    public void setUp() {
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_dataDisabled() {
-        mState.iconGroup = TelephonyIcons.DATA_DISABLED;
-        mState.userSetup = true;
-
-        assertTrue(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_notDefaultData() {
-        mState.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA;
-        mState.userSetup = true;
-
-        assertTrue(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testIsDataDisabledOrNotDefault_notDisabled() {
-        mState.iconGroup = TelephonyIcons.G;
-        mState.userSetup = true;
-
-        assertFalse(mState.isDataDisabledOrNotDefault());
-    }
-
-    @Test
-    public void testHasActivityIn_noData_noActivity() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = false;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_noData_activityIn() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = true;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_dataConnected_activityIn() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityIn = true;
-
-        assertTrue(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityIn_carrierNetworkChange() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = true;
-        mState.activityIn = true;
-
-        assertFalse(mState.hasActivityIn());
-    }
-
-    @Test
-    public void testHasActivityOut_noData_noActivity() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = false;
-
-        assertFalse(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_noData_activityOut() {
-        mState.dataConnected = false;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = true;
-
-        assertFalse(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_dataConnected_activityOut() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = false;
-        mState.activityOut = true;
-
-        assertTrue(mState.hasActivityOut());
-    }
-
-    @Test
-    public void testHasActivityOut_carrierNetworkChange() {
-        mState.dataConnected = true;
-        mState.carrierNetworkChangeMode = true;
-        mState.activityOut = true;
-
-        assertFalse(mState.hasActivityOut());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt
new file mode 100644
index 0000000..a226ded
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/MobileStateTest.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+package com.android.systemui.statusbar.connectivity
+
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MobileStateTest : SysuiTestCase() {
+
+    private val state = MobileState()
+    @Before fun setUp() {}
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_dataDisabled() {
+        state.iconGroup = TelephonyIcons.DATA_DISABLED
+        state.userSetup = true
+        assertTrue(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_notDefaultData() {
+        state.iconGroup = TelephonyIcons.NOT_DEFAULT_DATA
+        state.userSetup = true
+        assertTrue(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testIsDataDisabledOrNotDefault_notDisabled() {
+        state.iconGroup = TelephonyIcons.G
+        state.userSetup = true
+        assertFalse(state.isDataDisabledOrNotDefault)
+    }
+
+    @Test
+    fun testHasActivityIn_noData_noActivity() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityIn = false
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_noData_activityIn() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityIn = true
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_dataConnected_activityIn() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = false
+        state.activityIn = true
+        assertTrue(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityIn_carrierNetworkChange() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = true
+        state.activityIn = true
+        assertFalse(state.hasActivityIn())
+    }
+
+    @Test
+    fun testHasActivityOut_noData_noActivity() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityOut = false
+        assertFalse(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_noData_activityOut() {
+        state.dataConnected = false
+        state.carrierNetworkChangeMode = false
+        state.activityOut = true
+        assertFalse(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_dataConnected_activityOut() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = false
+        state.activityOut = true
+        assertTrue(state.hasActivityOut())
+    }
+
+    @Test
+    fun testHasActivityOut_carrierNetworkChange() {
+        state.dataConnected = true
+        state.carrierNetworkChangeMode = true
+        state.activityOut = true
+        assertFalse(state.hasActivityOut())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index f8a0d2f..faf4592 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -70,7 +70,9 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -115,6 +117,7 @@
     protected TelephonyManager mMockTm;
     protected TelephonyListenerManager mTelephonyListenerManager;
     protected BroadcastDispatcher mMockBd;
+    protected UserTracker mUserTracker;
     protected Config mConfig;
     protected CallbackHandler mCallbackHandler;
     protected SubscriptionDefaults mMockSubDefaults;
@@ -125,6 +128,8 @@
     protected CarrierConfigTracker mCarrierConfigTracker;
     protected FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     protected Handler mMainHandler;
+    // Use a real mobile mappings object since lots of tests rely on it
+    protected FakeMobileMappingsProxy mMobileMappingsProxy = new FakeMobileMappingsProxy();
     protected WifiStatusTrackerFactory mWifiStatusTrackerFactory;
     protected MobileSignalControllerFactory mMobileFactory;
 
@@ -169,6 +174,7 @@
         mMockSm = mock(SubscriptionManager.class);
         mMockCm = mock(ConnectivityManager.class);
         mMockBd = mock(BroadcastDispatcher.class);
+        mUserTracker = mock(UserTracker.class);
         mMockNsm = mock(NetworkScoreManager.class);
         mMockSubDefaults = mock(SubscriptionDefaults.class);
         mCarrierConfigTracker = mock(CarrierConfigTracker.class);
@@ -219,10 +225,13 @@
 
         mWifiStatusTrackerFactory = new WifiStatusTrackerFactory(
                 mContext, mMockWm, mMockNsm, mMockCm, mMainHandler);
+        // Most of these tests rely on the actual MobileMappings behavior
+        mMobileMappingsProxy.setUseRealImpl(true);
         mMobileFactory = new MobileSignalControllerFactory(
                 mContext,
                 mCallbackHandler,
-                mCarrierConfigTracker
+                mCarrierConfigTracker,
+                mMobileMappingsProxy
         );
 
         mNetworkController = new NetworkControllerImpl(mContext,
@@ -240,6 +249,7 @@
                 mMockSubDefaults,
                 mMockProvisionController,
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mCarrierConfigTracker,
                 mWifiStatusTrackerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ed8a3e1..ca75a40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -18,12 +18,21 @@
 
 import static android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN;
 import static android.telephony.NetworkRegistrationInfo.DOMAIN_PS;
+import static android.telephony.TelephonyManager.ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED;
+import static android.telephony.TelephonyManager.EXTRA_CARRIER_ID;
+import static android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID;
 
+import static com.android.settingslib.mobile.TelephonyIcons.NR_5G_PLUS;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.content.Intent;
 import android.net.NetworkCapabilities;
 import android.os.Handler;
 import android.os.Looper;
@@ -35,16 +44,19 @@
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
+import com.android.settingslib.SignalIcon.MobileIconGroup;
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.HashMap;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -142,6 +154,7 @@
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mock(CarrierConfigTracker.class),
                 mWifiStatusTrackerFactory,
@@ -329,6 +342,57 @@
         assertFalse(mNetworkController.isMobileDataNetworkInService());
     }
 
+    @Test
+    public void mobileSignalController_getsCarrierId() {
+        when(mMockTm.getSimCarrierId()).thenReturn(1);
+        setupDefaultSignal();
+
+        assertEquals(1, mMobileSignalController.getState().getCarrierId());
+    }
+
+    @Test
+    public void mobileSignalController_updatesCarrierId_onChange() {
+        when(mMockTm.getSimCarrierId()).thenReturn(1);
+        setupDefaultSignal();
+
+        // Updates are sent down through this broadcast, we can send the intent directly
+        Intent intent = new Intent(ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
+        intent.putExtra(EXTRA_SUBSCRIPTION_ID, mSubId);
+        intent.putExtra(EXTRA_CARRIER_ID, 2);
+
+        mMobileSignalController.handleBroadcast(intent);
+
+        assertEquals(2, mMobileSignalController.getState().getCarrierId());
+    }
+
+    @Test
+    public void networkTypeIcon_hasCarrierIdOverride() {
+        int fakeCarrier = 1;
+        int fakeIconOverride = 12345;
+        int testDataNetType = 100;
+        String testDataString = "100";
+        HashMap<String, MobileIconGroup> testMap = new HashMap<>();
+        testMap.put(testDataString, NR_5G_PLUS);
+
+        // Pretend that there is an override for this icon, and this carrier ID
+        NetworkTypeResIdCache mockCache = mock(NetworkTypeResIdCache.class);
+        when(mockCache.get(eq(NR_5G_PLUS), eq(fakeCarrier), any())).thenReturn(fakeIconOverride);
+
+        // Turn off the default mobile mapping, so we can override
+        mMobileMappingsProxy.setUseRealImpl(false);
+        mMobileMappingsProxy.setIconMap(testMap);
+        // Use the mocked cache
+        mMobileSignalController.mCurrentState.setNetworkTypeResIdCache(mockCache);
+        // Rebuild the network map
+        mMobileSignalController.setConfiguration(mConfig);
+        when(mMockTm.getSimCarrierId()).thenReturn(fakeCarrier);
+
+        setupDefaultSignal();
+        updateDataConnectionState(TelephonyManager.DATA_CONNECTED, testDataNetType);
+
+        verifyDataIndicators(fakeIconOverride);
+    }
+
     private void testDataActivity(int direction, boolean in, boolean out) {
         updateDataActivity(direction);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index a76676e..84c242c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -43,7 +43,7 @@
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
@@ -82,6 +82,7 @@
                 mMockSubDefaults,
                 mMockProvisionController,
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mCarrierConfigTracker,
                 mWifiStatusTrackerFactory,
@@ -118,6 +119,7 @@
                 mMockSubDefaults,
                 mMockProvisionController,
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mCarrierConfigTracker,
                 mWifiStatusTrackerFactory,
@@ -152,6 +154,7 @@
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mock(CarrierConfigTracker.class),
                 mWifiStatusTrackerFactory,
@@ -189,6 +192,7 @@
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mock(CarrierConfigTracker.class),
                 mWifiStatusTrackerFactory,
@@ -274,6 +278,7 @@
                 mMockSubDefaults,
                 mock(DeviceProvisionedController.class),
                 mMockBd,
+                mUserTracker,
                 mDemoModeController,
                 mock(CarrierConfigTracker.class),
                 mWifiStatusTrackerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt
new file mode 100644
index 0000000..9e73487
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkTypeResIdCacheTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.connectivity
+
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NetworkTypeResIdCacheTest : SysuiTestCase() {
+    private lateinit var cache: NetworkTypeResIdCache
+    private var overrides = MobileIconCarrierIdOverridesFake()
+
+    @Before
+    fun setUp() {
+        cache = NetworkTypeResIdCache(overrides)
+    }
+
+    @Test
+    fun carrier1_noOverride_usesDefault() {
+        assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconDefault1)
+    }
+
+    @Test
+    fun carrier1_overridden_usesOverride() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group1, CARRIER_1, context)).isEqualTo(iconOverride1)
+    }
+
+    @Test
+    fun carrier1_override_carrier2UsesDefault() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group1, CARRIER_2, context)).isEqualTo(iconDefault1)
+    }
+
+    @Test
+    fun carrier1_overrideType1_type2UsesDefault() {
+        overrides.overriddenIds.add(CARRIER_1)
+        overrides.overridesByCarrierId[CARRIER_1] = mapOf(NET_TYPE_1 to iconOverride1)
+
+        assertThat(cache.get(group2, CARRIER_1, context)).isEqualTo(iconDefault2)
+    }
+
+    companion object {
+        // Simplified icon overrides here
+        const val CARRIER_1 = 1
+        const val CARRIER_2 = 2
+
+        const val NET_TYPE_1 = "one"
+        const val iconDefault1 = 123
+        const val iconOverride1 = 321
+        val group1 = MobileIconGroup(NET_TYPE_1, /* dataContentDesc */ 0, iconDefault1)
+
+        const val NET_TYPE_2 = "two"
+        const val iconDefault2 = 234
+
+        val group2 = MobileIconGroup(NET_TYPE_2, /* dataContentDesc*/ 0, iconDefault2)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index 4b458f5..dda7fad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -31,8 +31,8 @@
     private long mCreationTime = 0;
     @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY;
     private NotifSection mNotifSection;
-    private NotificationEntry mSummary = null;
-    private List<NotificationEntry> mChildren = new ArrayList<>();
+    @Nullable private NotificationEntry mSummary = null;
+    private final List<NotificationEntry> mChildren = new ArrayList<>();
 
     /** Builds a new instance of GroupEntry */
     public GroupEntry build() {
@@ -41,7 +41,9 @@
         ge.getAttachState().setSection(mNotifSection);
 
         ge.setSummary(mSummary);
-        mSummary.setParent(ge);
+        if (mSummary != null) {
+            mSummary.setParent(ge);
+        }
 
         for (NotificationEntry child : mChildren) {
             ge.addChild(child);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 851517e..3b05321 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1498,45 +1498,8 @@
     }
 
     @Test
-    public void testMissingRankingWhenRemovalFeatureIsDisabled() {
+    public void testMissingRanking() {
         // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
-        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
-        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
-        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
-        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
-        clearInvocations(mCollectionListener);
-
-        // GIVEN the message for removing key1 gets does not reach NotifCollection
-        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
-        // WHEN the message for removing key2 arrives
-        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
-
-        // THEN only entry2 gets removed
-        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
-        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
-        verify(mLogger, never()).logRecoveredRankings(any(), anyInt());
-        clearInvocations(mCollectionListener, mLogger);
-
-        // WHEN a ranking update includes key1 again
-        mNoMan.setRanking(key1, ranking1);
-        mNoMan.issueRankingUpdate();
-
-        // VERIFY that we do nothing but log the 'recovery'
-        verify(mCollectionListener).onRankingUpdate(any());
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
-        verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0));
-    }
-
-    @Test
-    public void testMissingRankingWhenRemovalFeatureIsEnabled() {
-        // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
         String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
         String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
         NotificationEntry entry1 = mCollectionListener.getEntry(key1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 82e32b2..09f8a10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -34,10 +34,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -135,6 +137,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
 
         mListBuilder = new ShadeListBuilder(
                 mDumpManager,
@@ -1995,22 +1998,89 @@
     }
 
     @Test
-    public void testStableOrdering() {
+    public void testActiveOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
         mStabilityManager.setAllowEntryReordering(false);
-        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap
-        verify(mStabilityManager, times(4)).onEntryReorderSuppressed();
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        mStabilityManager.setAllowEntryReordering(false);
+        // No input or output
+        assertOrder("", "", "", true);
+        // Remove everything
+        assertOrder("ABCDEFG", "", "", true);
+        // Literally no changes
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
+
+        // No stable order
+        assertOrder("", "ABCDEFG", "ABCDEFG", true);
+
+        // F moved after A, and...
+        assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
+
+        // B moved after F, and...
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
+        assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
+
+        // Swap F and B, and...
+        assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
+        assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
+        assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
+
+        // Remove a bunch of entries at once
+        assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
+
+        // Remove a bunch of entries and scramble
+        assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
+
+        // Add a bunch of entries at once
+        assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
+        assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
+        assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
+        assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
+        assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
     }
 
     @Test
     public void testActiveOrdering() {
-        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
     }
 
     @Test
@@ -2062,6 +2132,52 @@
     }
 
     @Test
+    public void stableOrderingDisregardedWithSectionChange() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        // GIVEN the first sectioner's packages can be changed from run-to-run
+        List<String> mutableSectionerPackages = new ArrayList<>();
+        mutableSectionerPackages.add(PACKAGE_1);
+        mListBuilder.setSectioners(asList(
+                new PackageSectioner(mutableSectionerPackages, null),
+                new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(4);
+        addNotif(1, PACKAGE_1).setRank(5);
+        addNotif(2, PACKAGE_2).setRank(1);
+        addNotif(3, PACKAGE_2).setRank(2);
+        addNotif(4, PACKAGE_3).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3),
+                notif(4)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the first section now claims PACKAGE_3 notifications
+        mutableSectionerPackages.add(PACKAGE_3);
+        dispatchBuild();
+
+        // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
+        // is the correct position based on its rank, rather than #3 in the new section simply
+        // because it was #3 in its previous section.
+        verifyBuiltList(
+                notif(4),
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+    }
+
+    @Test
     public void testStableChildOrdering() {
         // WHEN the list is originally built with reordering disabled
         mStabilityManager.setAllowEntryReordering(false);
@@ -2112,6 +2228,85 @@
         );
     }
 
+    @Test
+    public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
+        // despite visual stability being active
+        verifyBuiltList(
+                notif(2),
+                notif(0),
+                notif(1)
+        );
+    }
+
+    @Test
+    public void groupRevertingToSummaryRetainsStablePosition() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary stays in the same location on rebuild
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2)
+        );
+    }
+
     private static void setNewRank(NotificationEntry entry, int rank) {
         entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
@@ -2255,26 +2450,35 @@
         return addGroupChildWithTag(index, packageId, groupId, null);
     }
 
-    private void assertOrder(String visible, String active, String expected) {
+    private void assertOrder(String visible, String active, String expected,
+            boolean isOrderedCorrectly) {
         StringBuilder differenceSb = new StringBuilder();
+        NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
         for (char c : active.toCharArray()) {
             if (visible.indexOf(c) < 0) differenceSb.append(c);
         }
         String difference = differenceSb.toString();
 
+        int globalIndex = 0;
         for (int i = 0; i < visible.length(); i++) {
-            addNotif(i, String.valueOf(visible.charAt(i)))
-                    .setRank(active.indexOf(visible.charAt(i)))
+            final char c = visible.charAt(i);
+            // Skip notifications which aren't active anymore
+            if (!active.contains(String.valueOf(c))) continue;
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(i);
-
         }
 
-        for (int i = 0; i < difference.length(); i++) {
-            addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
-                    .setRank(active.indexOf(difference.charAt(i)))
+        for (char c : difference.toCharArray()) {
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(-1);
         }
 
+        clearInvocations(mStabilityManager);
+
         dispatchBuild();
         StringBuilder resultSb = new StringBuilder();
         for (int i = 0; i < expected.length(); i++) {
@@ -2284,6 +2488,9 @@
         assertEquals("visible [" + visible + "] active [" + active + "]",
                 expected, resultSb.toString());
         mEntrySet.clear();
+
+        verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
+                .onEntryReorderSuppressed();
     }
 
     private int nextId(String packageName) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 340bc96..aa1114b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
+import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
@@ -86,6 +87,7 @@
     private val mRemoteInputManager: NotificationRemoteInputManager = mock()
     private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
     private val mHeaderController: NodeController = mock()
+    private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock()
 
     private lateinit var mEntry: NotificationEntry
     private lateinit var mGroupSummary: NotificationEntry
@@ -110,6 +112,7 @@
             mHeadsUpViewBinder,
             mNotificationInterruptStateProvider,
             mRemoteInputManager,
+            mLaunchFullScreenIntentProvider,
             mHeaderController,
             mExecutor)
         mCoordinator.attach(mNotifPipeline)
@@ -242,6 +245,20 @@
     }
 
     @Test
+    fun testOnEntryAdded_shouldFullScreen() {
+        setShouldFullScreen(mEntry)
+        mCollectionListener.onEntryAdded(mEntry)
+        verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+    }
+
+    @Test
+    fun testOnEntryAdded_shouldNotFullScreen() {
+        setShouldFullScreen(mEntry, should = false)
+        mCollectionListener.onEntryAdded(mEntry)
+        verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
+    }
+
+    @Test
     fun testPromotesAddedHUN() {
         // GIVEN the current entry should heads up
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true)
@@ -406,6 +423,10 @@
 
         verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
         verify(mHeadsUpManager).showNotification(mGroupSibling1)
+
+        // In addition make sure we have explicitly marked the summary as having interrupted due
+        // to the alert being transferred
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -424,6 +445,7 @@
 
         verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
         verify(mHeadsUpManager).showNotification(mGroupChild1)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -449,6 +471,7 @@
         verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
         verify(mHeadsUpManager).showNotification(mGroupSibling1)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -474,6 +497,7 @@
         verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
         verify(mHeadsUpManager).showNotification(mGroupChild1)
         verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -512,6 +536,7 @@
         verify(mHeadsUpManager).showNotification(mGroupPriority)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -548,6 +573,7 @@
         verify(mHeadsUpManager).showNotification(mGroupPriority)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -582,6 +608,7 @@
         verify(mHeadsUpManager).showNotification(mGroupPriority)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
         verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+        assertTrue(mGroupSummary.hasInterrupted())
     }
 
     @Test
@@ -672,9 +699,40 @@
     }
 
     @Test
+    fun testNoTransfer_groupSummaryNotAlerting() {
+        // When we have a group where the summary should not alert and exactly one child should
+        // alert, we should never mark the group summary as interrupted (because it doesn't).
+        setShouldHeadsUp(mGroupSummary, false)
+        setShouldHeadsUp(mGroupChild1, true)
+        setShouldHeadsUp(mGroupChild2, false)
+
+        mCollectionListener.onEntryAdded(mGroupSummary)
+        mCollectionListener.onEntryAdded(mGroupChild1)
+        mCollectionListener.onEntryAdded(mGroupChild2)
+        val groupEntry = GroupEntryBuilder()
+            .setSummary(mGroupSummary)
+            .setChildren(listOf(mGroupChild1, mGroupChild2))
+            .build()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupEntry))
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
+
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any())
+        finishBind(mGroupChild1)
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild2), any())
+
+        verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
+        verify(mHeadsUpManager).showNotification(mGroupChild1)
+        verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
+        assertFalse(mGroupSummary.hasInterrupted())
+    }
+
+    @Test
     fun testOnRankingApplied_newEntryShouldAlert() {
         // GIVEN that mEntry has never interrupted in the past, and now should
+        // and is new enough to do so
         assertFalse(mEntry.hasInterrupted())
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -690,8 +748,9 @@
 
     @Test
     fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
-        // GIVEN that mEntry has alerted in the past
+        // GIVEN that mEntry has alerted in the past, even if it's new
         mEntry.setInterruption()
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -725,12 +784,38 @@
         verify(mHeadsUpManager).showNotification(mEntry)
     }
 
+    @Test
+    fun testOnRankingApplied_entryUpdatedButTooOld() {
+        // GIVEN that mEntry is added in a state where it should not HUN
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it was actually added 10s ago
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000)
+
+        // WHEN it is updated to HUN and then a ranking update occurs
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is never bound or shown
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
     private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
         whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
                 .thenReturn(should)
     }
 
+    private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) {
+        whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+            .thenReturn(should)
+    }
+
     private fun finishBind(entry: NotificationEntry) {
         verify(mHeadsUpManager, never()).showNotification(entry)
         withArgCaptor<BindCallback> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 7e2e6f6..bdedd24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -13,57 +13,55 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
-import java.util.function.Consumer
-import org.junit.Before
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.verify
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
 import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class KeyguardCoordinatorTest : SysuiTestCase() {
-    private val notifPipeline: NotifPipeline = mock()
+
     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
+    private val keyguardRepository = FakeKeyguardRepository()
+    private val notifPipelineFlags: NotifPipelineFlags = mock()
+    private val notifPipeline: NotifPipeline = mock()
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
     private val statusBarStateController: StatusBarStateController = mock()
 
-    private lateinit var onStateChangeListener: Consumer<String>
-    private lateinit var keyguardFilter: NotifFilter
-
-    @Before
-    fun setup() {
-        val keyguardCoordinator = KeyguardCoordinator(
-            keyguardNotifVisibilityProvider,
-            sectionHeaderVisibilityProvider,
-            statusBarStateController
-        )
-        keyguardCoordinator.attach(notifPipeline)
-        onStateChangeListener = withArgCaptor {
-            verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
-        }
-        keyguardFilter = withArgCaptor {
-            verify(notifPipeline).addFinalizeFilter(capture())
-        }
-    }
-
     @Test
-    fun testSetSectionHeadersVisibleInShade() {
+    fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
         clearInvocations(sectionHeaderVisibilityProvider)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
         onStateChangeListener.accept("state change")
@@ -71,10 +69,176 @@
     }
 
     @Test
-    fun testSetSectionHeadersNotVisibleOnKeyguard() {
+    fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest {
         clearInvocations(sectionHeaderVisibilityProvider)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         onStateChangeListener.accept("state change")
         verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
     }
+
+    @Test
+    fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is not showing, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN: The keyguard goes away
+            keyguardRepository.setKeyguardShowing(false)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is shown regardless
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterAllowsNewNotif() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is showing, no notifications present
+        keyguardRepository.setKeyguardShowing(true)
+        runKeyguardCoordinatorTest {
+            // WHEN: A new notification is posted
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // THEN: The notification is recognized as "unseen" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterSeenGroupSummaryWithUnseenChild() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is not showing, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        runKeyguardCoordinatorTest {
+            // WHEN: A new notification is posted
+            val fakeSummary = NotificationEntryBuilder().build()
+            val fakeChild = NotificationEntryBuilder()
+                    .setGroup(context, "group")
+                    .setGroupSummary(context, false)
+                    .build()
+            GroupEntryBuilder()
+                    .setSummary(fakeSummary)
+                    .addChild(fakeChild)
+                    .build()
+
+            collectionListener.onEntryAdded(fakeSummary)
+            collectionListener.onEntryAdded(fakeChild)
+
+            // WHEN: Keyguard is now showing, both notifications are marked as seen
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // WHEN: The child notification is now unseen
+            collectionListener.onEntryUpdated(fakeChild)
+
+            // THEN: The summary is not filtered out, because the child is unseen
+            assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is showing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: Keyguard is no longer showing for 5 seconds
+            keyguardRepository.setKeyguardShowing(false)
+            testScheduler.runCurrent()
+            testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds)
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is now recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+        }
+    }
+
+    @Test
+    fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is showing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: Keyguard is no longer showing for <5 seconds
+            keyguardRepository.setKeyguardShowing(false)
+            testScheduler.runCurrent()
+            testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds)
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is not recognized as "seen" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    private fun runKeyguardCoordinatorTest(
+        testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
+    ) {
+        val testScope = TestScope(UnconfinedTestDispatcher())
+        val keyguardCoordinator =
+            KeyguardCoordinator(
+                keyguardNotifVisibilityProvider,
+                keyguardRepository,
+                notifPipelineFlags,
+                testScope.backgroundScope,
+                sectionHeaderVisibilityProvider,
+                statusBarStateController,
+            )
+        keyguardCoordinator.attach(notifPipeline)
+        KeyguardCoordinatorTestScope(keyguardCoordinator, testScope).run {
+            testScheduler.advanceUntilIdle()
+            testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { testBlock() }
+        }
+    }
+
+    private inner class KeyguardCoordinatorTestScope(
+        private val keyguardCoordinator: KeyguardCoordinator,
+        private val scope: TestScope,
+    ) : CoroutineScope by scope {
+        val testScheduler: TestCoroutineScheduler
+            get() = scope.testScheduler
+
+        val onStateChangeListener: Consumer<String> =
+            withArgCaptor {
+                verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+            }
+
+        val unseenFilter: NotifFilter
+            get() = keyguardCoordinator.unseenNotifFilter
+
+        // TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and
+        //  removed
+        val collectionListener: NotifCollectionListener by lazy {
+            withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) }
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
index e1e5051..590c902 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -35,7 +35,7 @@
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index dcf2455..b6b0b77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -261,23 +261,15 @@
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification is moved under a parent
-        NotificationEntry groupSummary = getNotificationEntryBuilder()
-                .setParent(ROOT_ENTRY)
-                .setGroupSummary(mContext, true)
-                .setGroup(mContext, TEST_GROUP_KEY)
-                .build();
-        GroupEntry parent = mock(GroupEntry.class);
-        when(parent.getSummary()).thenReturn(groupSummary);
-        NotificationEntryBuilder.setNewParent(mEntry, parent);
-        mCollectionListener.onEntryInit(groupSummary);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary));
+        NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
 
         // THEN we rebind it as not-minimized
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertFalse(mParamsCaptor.getValue().isLowPriority());
 
-        // THEN we filter it because the parent summary is not yet inflated.
-        assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0));
+        // THEN we do not filter it because it's not the first inflation.
+        assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
     }
 
     @Test
@@ -401,6 +393,36 @@
     }
 
     @Test
+    public void testNullGroupSummary() {
+        // GIVEN a newly-posted group with a summary and two children
+        final GroupEntry group = new GroupEntryBuilder()
+                .setCreationTime(400)
+                .setSummary(getNotificationEntryBuilder().setId(1).build())
+                .addChild(getNotificationEntryBuilder().setId(2).build())
+                .addChild(getNotificationEntryBuilder().setId(3).build())
+                .build();
+        fireAddEvents(List.of(group));
+        final NotificationEntry child0 = group.getChildren().get(0);
+        final NotificationEntry child1 = group.getChildren().get(1);
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
+
+        // WHEN the summary is pruned
+        new GroupEntryBuilder()
+                .setCreationTime(400)
+                .addChild(child0)
+                .addChild(child1)
+                .build();
+
+        // WHEN all of the children (but not the summary) finish inflating
+        mNotifInflater.invokeInflateCallbackForEntry(child0);
+        mNotifInflater.invokeInflateCallbackForEntry(child1);
+
+        // THEN the entire group is not filtered out
+        assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
+        assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
+    }
+
+    @Test
     public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() {
         // GIVEN a newly-posted group with a summary and two children
         final String groupKey = "test_reinflate_group";
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..e488f39 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
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertFalse;
 
 import static org.junit.Assert.assertTrue;
@@ -36,7 +38,9 @@
 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.VisibilityLocationProvider;
 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 +75,13 @@
     @Mock private StatusBarStateController mStatusBarStateController;
     @Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener;
     @Mock private HeadsUpManager mHeadsUpManager;
-    @Mock private NotifPanelEvents mNotifPanelEvents;
+    @Mock private ShadeStateEvents mShadeStateEvents;
+    @Mock private VisibilityLocationProvider mVisibilityLocationProvider;
     @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 +89,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,8 +102,9 @@
                 mFakeExecutor,
                 mDumpManager,
                 mHeadsUpManager,
-                mNotifPanelEvents,
+                mShadeStateEvents,
                 mStatusBarStateController,
+                mVisibilityLocationProvider,
                 mVisualStabilityProvider,
                 mWakefulnessLifecycle);
 
@@ -111,7 +117,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());
@@ -353,6 +360,38 @@
     }
 
     @Test
+    public void testMovingVisibleHeadsUpNotAllowed() {
+        // GIVEN stability enforcing conditions
+        setPanelExpanded(true);
+        setSleepy(false);
+
+        // WHEN a notification is alerting and visible
+        when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true);
+        when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class)))
+                .thenReturn(true);
+
+        // VERIFY the notification cannot be reordered
+        assertThat(mNotifStabilityManager.isEntryReorderingAllowed(mEntry)).isFalse();
+        assertThat(mNotifStabilityManager.isSectionChangeAllowed(mEntry)).isFalse();
+    }
+
+    @Test
+    public void testMovingInvisibleHeadsUpAllowed() {
+        // GIVEN stability enforcing conditions
+        setPanelExpanded(true);
+        setSleepy(false);
+
+        // WHEN a notification is alerting but not visible
+        when(mHeadsUpManager.isAlerting(mEntry.getKey())).thenReturn(true);
+        when(mVisibilityLocationProvider.isInVisibleLocation(any(NotificationEntry.class)))
+                .thenReturn(false);
+
+        // VERIFY the notification can be reordered
+        assertThat(mNotifStabilityManager.isEntryReorderingAllowed(mEntry)).isTrue();
+        assertThat(mNotifStabilityManager.isSectionChangeAllowed(mEntry)).isTrue();
+    }
+
+    @Test
     public void testNeverSuppressedChanges_noInvalidationCalled() {
         // GIVEN no notifications are currently being suppressed from grouping nor being sorted
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
new file mode 100644
index 0000000..1cdd023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SemiStableSortTest : SysuiTestCase() {
+
+    var shuffleInput: Boolean = false
+    var testStabilizeTo: Boolean = false
+    var sorter: SemiStableSort? = null
+
+    @Before
+    fun setUp() {
+        shuffleInput = false
+        sorter = null
+    }
+
+    private fun stringStabilizeTo(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        val result = mutableListOf<Char>()
+        return (sorter ?: SemiStableSort())
+            .stabilizeTo(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                result,
+            )
+            .let { ordered -> result.joinToString("") to ordered }
+    }
+
+    private fun stringSort(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        if (shuffleInput) {
+            actives.shuffle()
+        }
+        return (sorter ?: SemiStableSort())
+            .sort(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                compareBy { activeOrder.indexOf(it) },
+            )
+            .let { ordered -> actives.joinToString("") to ordered }
+    }
+
+    private fun testCase(
+        stableOrder: String,
+        activeOrder: String,
+        expected: String,
+        expectOrdered: Boolean,
+    ) {
+        val (mergeResult, ordered) =
+            if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
+            else stringSort(stableOrder, activeOrder)
+        val resultPass = expected == mergeResult
+        val orderedPass = ordered == expectOrdered
+        val pass = resultPass && orderedPass
+        val resultSuffix =
+            if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
+        val orderedSuffix =
+            if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
+        val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
+        Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
+        if (!pass) {
+            throw AssertionError("Test case failed: $readableResult")
+        }
+    }
+
+    private fun runAllTestCases() {
+        // No input or output
+        testCase("", "", "", true)
+        // Remove everything
+        testCase("ABCDEFG", "", "", true)
+        // Literally no changes
+        testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
+
+        // No stable order
+        testCase("", "ABCDEFG", "ABCDEFG", true)
+
+        // F moved after A, and...
+        testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
+
+        // B moved after F, and...
+        testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
+        testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
+
+        // Swap F and B, and...
+        testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
+        testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
+        testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
+
+        // Remove a bunch of entries at once
+        testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
+
+        // Remove a bunch of entries and scramble
+        testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
+
+        // Add a bunch of entries at once
+        testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
+        testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
+        testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
+        testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
+        testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
+    }
+
+    @Test
+    fun testSort() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithSingleInstance() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithShuffledInput() {
+        testStabilizeTo = false
+        shuffleInput = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeTo() {
+        testStabilizeTo = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeToWithSingleInstance() {
+        testStabilizeTo = true
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testIsSorted() {
+        val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
+        SemiStableSort.apply {
+            assertTrue(emptyList<Int>().isSorted(intCmp))
+            assertTrue(listOf(1).isSorted(intCmp))
+            assertTrue(listOf(1, 2).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
+            assertFalse(listOf(2, 1).isSorted(intCmp))
+            assertFalse(listOf(2, 1, 2).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 1).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
+            assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
new file mode 100644
index 0000000..2036954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ShadeListBuilderHelperTest : SysuiTestCase() {
+
+    @Test
+    fun testGetContiguousSubLists() {
+        assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A', 'A', 'A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBB".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('D'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java
deleted file mode 100644
index ab71264..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.render;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import android.content.Context;
-import android.testing.AndroidTestingRunner;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class ShadeViewDifferTest extends SysuiTestCase {
-    private ShadeViewDiffer mDiffer;
-
-    private FakeController mRootController = new FakeController(mContext, "RootController");
-    private FakeController mController1 = new FakeController(mContext, "Controller1");
-    private FakeController mController2 = new FakeController(mContext, "Controller2");
-    private FakeController mController3 = new FakeController(mContext, "Controller3");
-    private FakeController mController4 = new FakeController(mContext, "Controller4");
-    private FakeController mController5 = new FakeController(mContext, "Controller5");
-    private FakeController mController6 = new FakeController(mContext, "Controller6");
-    private FakeController mController7 = new FakeController(mContext, "Controller7");
-
-    @Mock
-    ShadeViewDifferLogger mLogger;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-
-        mDiffer = new ShadeViewDiffer(mRootController, mLogger);
-    }
-
-    @Test
-    public void testAddInitialViews() {
-        // WHEN a spec is applied to an empty root
-        // THEN the final tree matches the spec
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4)
-                ),
-                node(mController5)
-        );
-    }
-
-    @Test
-    public void testDetachViews() {
-        // GIVEN a preexisting tree of controllers
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4)
-                ),
-                node(mController5)
-        );
-
-        // WHEN the new spec removes nodes
-        // THEN the final tree matches the spec
-        applySpecAndCheck(
-                node(mController5)
-        );
-    }
-
-    @Test
-    public void testReparentChildren() {
-        // GIVEN a preexisting tree of controllers
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4)
-                ),
-                node(mController5)
-        );
-
-        // WHEN the parents of the controllers are all shuffled around
-        // THEN the final tree matches the spec
-        applySpecAndCheck(
-                node(mController1),
-                node(mController4),
-                node(mController3,
-                        node(mController2)
-                )
-        );
-    }
-
-    @Test
-    public void testReorderChildren() {
-        // GIVEN a preexisting tree of controllers
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2),
-                node(mController3),
-                node(mController4)
-        );
-
-        // WHEN the children change order
-        // THEN the final tree matches the spec
-        applySpecAndCheck(
-                node(mController3),
-                node(mController2),
-                node(mController4),
-                node(mController1)
-        );
-    }
-
-    @Test
-    public void testRemovedGroupsAreBrokenApart() {
-        // GIVEN a preexisting tree with a group
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4),
-                        node(mController5)
-                )
-        );
-
-        // WHEN the new spec removes the entire group
-        applySpecAndCheck(
-                node(mController1)
-        );
-
-        // THEN the group children are no longer attached to their parent
-        assertNull(mController3.getView().getParent());
-        assertNull(mController4.getView().getParent());
-        assertNull(mController5.getView().getParent());
-    }
-
-    @Test
-    public void testUnmanagedViews() {
-        // GIVEN a preexisting tree of controllers
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4)
-                ),
-                node(mController5)
-        );
-
-        // GIVEN some additional unmanaged views attached to the tree
-        View unmanagedView1 = new View(mContext);
-        View unmanagedView2 = new View(mContext);
-
-        mRootController.getView().addView(unmanagedView1, 1);
-        mController2.getView().addView(unmanagedView2, 0);
-
-        // WHEN a new spec is applied with additional nodes
-        // THEN the final tree matches the spec
-        applySpecAndCheck(
-                node(mController1),
-                node(mController2,
-                        node(mController3),
-                        node(mController4),
-                        node(mController6)
-                ),
-                node(mController5),
-                node(mController7)
-        );
-
-        // THEN the unmanaged views have been pushed to the end of their parents
-        assertEquals(unmanagedView1, mRootController.view.getChildAt(4));
-        assertEquals(unmanagedView2, mController2.view.getChildAt(3));
-    }
-
-    private void applySpecAndCheck(NodeSpec spec) {
-        mDiffer.applySpec(spec);
-        checkMatchesSpec(spec);
-    }
-
-    private void applySpecAndCheck(SpecBuilder... children) {
-        applySpecAndCheck(node(mRootController, children).build());
-    }
-
-    private void checkMatchesSpec(NodeSpec spec) {
-        final NodeController parent = spec.getController();
-        final List<NodeSpec> children = spec.getChildren();
-
-        for (int i = 0; i < children.size(); i++) {
-            NodeSpec childSpec = children.get(i);
-            View view = parent.getChildAt(i);
-
-            assertEquals(
-                    "Child " + i + " of parent " + parent.getNodeLabel() + " should be "
-                            + childSpec.getController().getNodeLabel() + " but is instead "
-                            + (view != null ? mDiffer.getViewLabel(view) : "null"),
-                    view,
-                    childSpec.getController().getView());
-
-            if (!childSpec.getChildren().isEmpty()) {
-                checkMatchesSpec(childSpec);
-            }
-        }
-    }
-
-    private static class FakeController implements NodeController {
-
-        public final FrameLayout view;
-        private final String mLabel;
-
-        FakeController(Context context, String label) {
-            view = new FrameLayout(context);
-            mLabel = label;
-        }
-
-        @NonNull
-        @Override
-        public String getNodeLabel() {
-            return mLabel;
-        }
-
-        @NonNull
-        @Override
-        public FrameLayout getView() {
-            return view;
-        }
-
-        @Override
-        public int getChildCount() {
-            return view.getChildCount();
-        }
-
-        @Override
-        public View getChildAt(int index) {
-            return view.getChildAt(index);
-        }
-
-        @Override
-        public void addChildAt(@NonNull NodeController child, int index) {
-            view.addView(child.getView(), index);
-        }
-
-        @Override
-        public void moveChildTo(@NonNull NodeController child, int index) {
-            view.removeView(child.getView());
-            view.addView(child.getView(), index);
-        }
-
-        @Override
-        public void removeChild(@NonNull NodeController child, boolean isTransfer) {
-            view.removeView(child.getView());
-        }
-
-        @Override
-        public void onViewAdded() {
-        }
-
-        @Override
-        public void onViewMoved() {
-        }
-
-        @Override
-        public void onViewRemoved() {
-        }
-    }
-
-    private static class SpecBuilder {
-        private final NodeController mController;
-        private final SpecBuilder[] mChildren;
-
-        SpecBuilder(NodeController controller, SpecBuilder... children) {
-            mController = controller;
-            mChildren = children;
-        }
-
-        public NodeSpec build() {
-            return build(null);
-        }
-
-        public NodeSpec build(@Nullable NodeSpec parent) {
-            final NodeSpecImpl spec = new NodeSpecImpl(parent, mController);
-            for (SpecBuilder childBuilder : mChildren) {
-                spec.getChildren().add(childBuilder.build(spec));
-            }
-            return spec;
-        }
-    }
-
-    private static SpecBuilder node(NodeController controller, SpecBuilder... children) {
-        return new SpecBuilder(controller, children);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt
new file mode 100644
index 0000000..15cf17d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.render
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ShadeViewDifferTest : SysuiTestCase() {
+    private lateinit var differ: ShadeViewDiffer
+    private val rootController = FakeController(mContext, "RootController")
+    private val controller1 = FakeController(mContext, "Controller1")
+    private val controller2 = FakeController(mContext, "Controller2")
+    private val controller3 = FakeController(mContext, "Controller3")
+    private val controller4 = FakeController(mContext, "Controller4")
+    private val controller5 = FakeController(mContext, "Controller5")
+    private val controller6 = FakeController(mContext, "Controller6")
+    private val controller7 = FakeController(mContext, "Controller7")
+    private val logger: ShadeViewDifferLogger = mock()
+
+    @Before
+    fun setUp() {
+        differ = ShadeViewDiffer(rootController, logger)
+    }
+
+    @Test
+    fun testAddInitialViews() {
+        // WHEN a spec is applied to an empty root
+        // THEN the final tree matches the spec
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4)),
+            node(controller5)
+        )
+    }
+
+    @Test
+    fun testDetachViews() {
+        // GIVEN a preexisting tree of controllers
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4)),
+            node(controller5)
+        )
+
+        // WHEN the new spec removes nodes
+        // THEN the final tree matches the spec
+        applySpecAndCheck(node(controller5))
+    }
+
+    @Test
+    fun testReparentChildren() {
+        // GIVEN a preexisting tree of controllers
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4)),
+            node(controller5)
+        )
+
+        // WHEN the parents of the controllers are all shuffled around
+        // THEN the final tree matches the spec
+        applySpecAndCheck(
+            node(controller1),
+            node(controller4),
+            node(controller3, node(controller2))
+        )
+    }
+
+    @Test
+    fun testReorderChildren() {
+        // GIVEN a preexisting tree of controllers
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2),
+            node(controller3),
+            node(controller4)
+        )
+
+        // WHEN the children change order
+        // THEN the final tree matches the spec
+        applySpecAndCheck(
+            node(controller3),
+            node(controller2),
+            node(controller4),
+            node(controller1)
+        )
+    }
+
+    @Test
+    fun testRemovedGroupsAreBrokenApart() {
+        // GIVEN a preexisting tree with a group
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4), node(controller5))
+        )
+
+        // WHEN the new spec removes the entire group
+        applySpecAndCheck(node(controller1))
+
+        // THEN the group children are no longer attached to their parent
+        Assert.assertNull(controller3.view.parent)
+        Assert.assertNull(controller4.view.parent)
+        Assert.assertNull(controller5.view.parent)
+    }
+
+    @Test
+    fun testUnmanagedViews() {
+        // GIVEN a preexisting tree of controllers
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4)),
+            node(controller5)
+        )
+
+        // GIVEN some additional unmanaged views attached to the tree
+        val unmanagedView1 = View(mContext)
+        val unmanagedView2 = View(mContext)
+        rootController.view.addView(unmanagedView1, 1)
+        controller2.view.addView(unmanagedView2, 0)
+
+        // WHEN a new spec is applied with additional nodes
+        // THEN the final tree matches the spec
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4), node(controller6)),
+            node(controller5),
+            node(controller7)
+        )
+
+        // THEN the unmanaged views have been pushed to the end of their parents
+        Assert.assertEquals(unmanagedView1, rootController.view.getChildAt(4))
+        Assert.assertEquals(unmanagedView2, controller2.view.getChildAt(3))
+    }
+
+    private fun applySpecAndCheck(spec: NodeSpec) {
+        differ.applySpec(spec)
+        checkMatchesSpec(spec)
+    }
+
+    private fun applySpecAndCheck(vararg children: SpecBuilder) {
+        applySpecAndCheck(node(rootController, *children).build())
+    }
+
+    private fun checkMatchesSpec(spec: NodeSpec) {
+        val parent = spec.controller
+        val children = spec.children
+        for (i in children.indices) {
+            val childSpec = children[i]
+            val view = parent.getChildAt(i)
+            Assert.assertEquals(
+                "Child $i of parent ${parent.nodeLabel} " +
+                    "should be ${childSpec.controller.nodeLabel} " +
+                    "but instead " +
+                    view?.let(differ::getViewLabel),
+                view,
+                childSpec.controller.view
+            )
+            if (childSpec.children.isNotEmpty()) {
+                checkMatchesSpec(childSpec)
+            }
+        }
+    }
+
+    private class FakeController(context: Context, label: String) : NodeController {
+        override val view: FrameLayout = FrameLayout(context)
+        override val nodeLabel: String = label
+        override fun getChildCount(): Int = view.childCount
+
+        override fun getChildAt(index: Int): View? {
+            return view.getChildAt(index)
+        }
+
+        override fun addChildAt(child: NodeController, index: Int) {
+            view.addView(child.view, index)
+        }
+
+        override fun moveChildTo(child: NodeController, index: Int) {
+            view.removeView(child.view)
+            view.addView(child.view, index)
+        }
+
+        override fun removeChild(child: NodeController, isTransfer: Boolean) {
+            view.removeView(child.view)
+        }
+
+        override fun onViewAdded() {}
+        override fun onViewMoved() {}
+        override fun onViewRemoved() {}
+    }
+
+    private class SpecBuilder(
+        private val mController: NodeController,
+        private val children: Array<out SpecBuilder>
+    ) {
+
+        @JvmOverloads
+        fun build(parent: NodeSpec? = null): NodeSpec {
+            val spec = NodeSpecImpl(parent, mController)
+            for (childBuilder in children) {
+                spec.children.add(childBuilder.build(spec))
+            }
+            return spec
+        }
+    }
+
+    companion object {
+        private fun node(controller: NodeController, vararg children: SpecBuilder): SpecBuilder {
+            return SpecBuilder(controller, children)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 46f630b..ea311da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -51,12 +51,14 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -97,6 +99,7 @@
     NotifPipelineFlags mFlags;
     @Mock
     KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    UiEventLoggerFake mUiEventLoggerFake;
     @Mock
     PendingIntent mPendingIntent;
 
@@ -107,6 +110,8 @@
         MockitoAnnotations.initMocks(this);
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
 
+        mUiEventLoggerFake = new UiEventLoggerFake();
+
         mNotifInterruptionStateProvider =
                 new NotificationInterruptStateProviderImpl(
                         mContext.getContentResolver(),
@@ -120,7 +125,8 @@
                         mLogger,
                         mMockHandler,
                         mFlags,
-                        mKeyguardNotificationVisibilityProvider);
+                        mKeyguardNotificationVisibilityProvider,
+                        mUiEventLoggerFake);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -442,6 +448,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     @Test
@@ -600,6 +613,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
index b2dc842..7117c23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
@@ -41,6 +41,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
@@ -88,6 +89,7 @@
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NotificationListener mListener;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private NotificationEntry mEntry;
     private TestableNotificationLogger mLogger;
@@ -118,6 +120,7 @@
                 mVisibilityProvider,
                 mNotifPipeline,
                 mock(StatusBarStateControllerImpl.class),
+                mShadeExpansionStateManager,
                 mBarService,
                 mExpansionStateLogger
         );
@@ -152,7 +155,7 @@
 
         when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
 
@@ -162,7 +165,7 @@
 
         // |mEntry| won't change visibility, so it shouldn't be reported again:
         Mockito.reset(mBarService);
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
 
@@ -174,7 +177,7 @@
             throws Exception {
         when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
-        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
+        mLogger.onChildLocationsChanged();
         TestableLooper.get(this).processAllMessages();
         mUiBgExecutor.runAllReady();
         Mockito.reset(mBarService);
@@ -189,13 +192,13 @@
     }
 
     private void setStateAsleep() {
-        mLogger.onPanelExpandedChanged(true);
+        mLogger.onShadeExpansionFullyChanged(true);
         mLogger.onDozingChanged(true);
         mLogger.onStateChanged(StatusBarState.KEYGUARD);
     }
 
     private void setStateAwake() {
-        mLogger.onPanelExpandedChanged(false);
+        mLogger.onShadeExpansionFullyChanged(false);
         mLogger.onDozingChanged(false);
         mLogger.onStateChanged(StatusBarState.SHADE);
     }
@@ -221,7 +224,7 @@
         when(mActiveNotifEntries.getValue()).thenReturn(Lists.newArrayList(mEntry));
         setStateAwake();
         // Now expand panel
-        mLogger.onPanelExpandedChanged(true);
+        mLogger.onShadeExpansionFullyChanged(true);
         assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
         assertFalse(mNotificationPanelLoggerFake.get(0).isLockscreen);
         assertEquals(1, mNotificationPanelLoggerFake.get(0).list.notifications.length);
@@ -263,6 +266,7 @@
                 NotificationVisibilityProvider visibilityProvider,
                 NotifPipeline notifPipeline,
                 StatusBarStateControllerImpl statusBarStateController,
+                ShadeExpansionStateManager shadeExpansionStateManager,
                 IStatusBarService barService,
                 ExpansionStateLogger expansionStateLogger) {
             super(
@@ -272,6 +276,7 @@
                     visibilityProvider,
                     notifPipeline,
                     statusBarStateController,
+                    shadeExpansionStateManager,
                     expansionStateLogger,
                     mNotificationPanelLoggerFake
             );
@@ -280,9 +285,5 @@
             // Make this on the current thread so we can wait for it during tests.
             mHandler = Handler.createAsync(Looper.myLooper());
         }
-
-        OnChildLocationsChangedListener getChildLocationsChangedListenerForTest() {
-            return mNotificationLocationsChangedListener;
-        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
new file mode 100644
index 0000000..f69839b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
@@ -0,0 +1,305 @@
+/*
+ *
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationMemoryMeterTest : SysuiTestCase() {
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification() {
+        val notification = createBasicNotification().build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+        val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = 0,
+            extras = 3316,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
+        val notification =
+            createBasicNotification()
+                .setCustomContentView(
+                    RemoteViews(context.packageName, android.R.layout.list_content)
+                )
+                .build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3384,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = true,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
+        val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
+        val notification =
+            createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = 444444,
+            largeIcon = 0,
+            extras = 3212,
+            bigPicture = 0,
+            extender = 0,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_bigPictureStyle() {
+        val bigPicture =
+            Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
+        val bigPictureIcon =
+            Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.BigPictureStyle()
+                        .bigPicture(bigPicture)
+                        .bigLargeIcon(bigPictureIcon)
+                )
+                .build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4092,
+            bigPicture = bigPicture.bitmap.allocationByteCount,
+            extender = 0,
+            style = "BigPictureStyle",
+            styleIcon = bigPictureIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_callingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val fakeIntent =
+            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+        val notification =
+            createBasicNotification()
+                .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
+                .build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 4084,
+            bigPicture = 0,
+            extender = 0,
+            style = "CallStyle",
+            styleIcon = personIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_messagingStyle() {
+        val personIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
+        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
+        val message = Notification.MessagingStyle.Message("Message!", 4323, person)
+        val historicPersonIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
+        val historicPerson =
+            Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
+        val historicMessage =
+            Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
+
+        val notification =
+            createBasicNotification()
+                .setStyle(
+                    Notification.MessagingStyle(person)
+                        .addMessage(message)
+                        .addHistoricMessage(historicMessage)
+                )
+                .build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 5024,
+            bigPicture = 0,
+            extender = 0,
+            style = "MessagingStyle",
+            styleIcon =
+                personIcon.bitmap.allocationByteCount +
+                    historicPersonIcon.bitmap.allocationByteCount,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_carExtender() {
+        val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
+        val extender = Notification.CarExtender().setLargeIcon(carIcon)
+        val notification = createBasicNotification().extend(extender).build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3612,
+            bigPicture = 0,
+            extender = 556656,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    @Test
+    fun currentNotificationMemoryUse_tvWearExtender() {
+        val tvExtender = Notification.TvExtender().setChannel("channel2")
+        val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
+        val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
+        val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
+        assertNotificationObjectSizes(
+            memoryUse = memoryUse,
+            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
+            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
+            extras = 3820,
+            bigPicture = 0,
+            extender = 388 + wearBackground.allocationByteCount,
+            style = null,
+            styleIcon = 0,
+            hasCustomView = false,
+        )
+    }
+
+    private fun createBasicNotification(): Notification.Builder {
+        val smallIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
+        val largeIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
+        return Notification.Builder(context)
+            .setSmallIcon(smallIcon)
+            .setLargeIcon(largeIcon)
+            .setContentTitle("This is a title")
+            .setContentText("This is content text.")
+    }
+
+    /** This will generate a nicer error message than comparing objects */
+    private fun assertNotificationObjectSizes(
+        memoryUse: NotificationMemoryUsage,
+        smallIcon: Int,
+        largeIcon: Int,
+        extras: Int,
+        bigPicture: Int,
+        extender: Int,
+        style: String?,
+        styleIcon: Int,
+        hasCustomView: Boolean,
+    ) {
+        assertThat(memoryUse.packageName).isEqualTo("test_pkg")
+        assertThat(memoryUse.notificationKey)
+            .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
+        assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
+        assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
+        assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
+        if (style == null) {
+            assertThat(memoryUse.objectUsage.style).isNull()
+        } else {
+            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
+        }
+        assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
+        assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
+    }
+
+    private fun getUseObject(
+        singleItemUseList: List<NotificationMemoryUsage>,
+    ): NotificationMemoryUsage {
+        assertThat(singleItemUseList).hasSize(1)
+        return singleItemUseList[0]
+    }
+
+    private fun createNotificationEntry(
+        notification: Notification,
+    ): NotificationEntry =
+        NotificationEntryBuilder().setTag("test").setNotification(notification).build()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
deleted file mode 100644
index 16e2441..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
+++ /dev/null
@@ -1,321 +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.statusbar.notification.logging
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Person
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.drawable.Icon
-import android.testing.AndroidTestingRunner
-import android.widget.RemoteViews
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.NotificationUtils
-import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class NotificationMemoryMonitorTest : SysuiTestCase() {
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_plainNotification() {
-        val notification = createBasicNotification().build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 3316,
-            bigPicture = 0,
-            extender = 0,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
-        val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
-        val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = 0,
-            extras = 3316,
-            bigPicture = 0,
-            extender = 0,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
-        val notification =
-            createBasicNotification()
-                .setCustomContentView(
-                    RemoteViews(context.packageName, android.R.layout.list_content)
-                )
-                .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 3384,
-            bigPicture = 0,
-            extender = 0,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = true,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
-        val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
-        val notification =
-            createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = 444444,
-            largeIcon = 0,
-            extras = 3212,
-            bigPicture = 0,
-            extender = 0,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_bigPictureStyle() {
-        val bigPicture =
-            Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
-        val bigPictureIcon =
-            Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
-        val notification =
-            createBasicNotification()
-                .setStyle(
-                    Notification.BigPictureStyle()
-                        .bigPicture(bigPicture)
-                        .bigLargeIcon(bigPictureIcon)
-                )
-                .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 4092,
-            bigPicture = bigPicture.bitmap.allocationByteCount,
-            extender = 0,
-            style = "BigPictureStyle",
-            styleIcon = bigPictureIcon.bitmap.allocationByteCount,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_callingStyle() {
-        val personIcon =
-            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
-        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
-        val fakeIntent =
-            PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
-        val notification =
-            createBasicNotification()
-                .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
-                .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 4084,
-            bigPicture = 0,
-            extender = 0,
-            style = "CallStyle",
-            styleIcon = personIcon.bitmap.allocationByteCount,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_messagingStyle() {
-        val personIcon =
-            Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
-        val person = Person.Builder().setIcon(personIcon).setName("Person").build()
-        val message = Notification.MessagingStyle.Message("Message!", 4323, person)
-        val historicPersonIcon =
-            Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
-        val historicPerson =
-            Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
-        val historicMessage =
-            Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
-
-        val notification =
-            createBasicNotification()
-                .setStyle(
-                    Notification.MessagingStyle(person)
-                        .addMessage(message)
-                        .addHistoricMessage(historicMessage)
-                )
-                .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 5024,
-            bigPicture = 0,
-            extender = 0,
-            style = "MessagingStyle",
-            styleIcon =
-                personIcon.bitmap.allocationByteCount +
-                    historicPersonIcon.bitmap.allocationByteCount,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_carExtender() {
-        val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
-        val extender = Notification.CarExtender().setLargeIcon(carIcon)
-        val notification = createBasicNotification().extend(extender).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 3612,
-            bigPicture = 0,
-            extender = 556656,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = false,
-        )
-    }
-
-    @Test
-    fun currentNotificationMemoryUse_tvWearExtender() {
-        val tvExtender = Notification.TvExtender().setChannel("channel2")
-        val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
-        val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
-        val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
-        assertNotificationObjectSizes(
-            memoryUse = memoryUse,
-            smallIcon = notification.smallIcon.bitmap.allocationByteCount,
-            largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
-            extras = 3820,
-            bigPicture = 0,
-            extender = 388 + wearBackground.allocationByteCount,
-            style = null,
-            styleIcon = 0,
-            hasCustomView = false,
-        )
-    }
-
-    private fun createBasicNotification(): Notification.Builder {
-        val smallIcon =
-            Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
-        val largeIcon =
-            Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
-        return Notification.Builder(context)
-            .setSmallIcon(smallIcon)
-            .setLargeIcon(largeIcon)
-            .setContentTitle("This is a title")
-            .setContentText("This is content text.")
-    }
-
-    /** This will generate a nicer error message than comparing objects */
-    private fun assertNotificationObjectSizes(
-        memoryUse: NotificationMemoryUsage,
-        smallIcon: Int,
-        largeIcon: Int,
-        extras: Int,
-        bigPicture: Int,
-        extender: Int,
-        style: String?,
-        styleIcon: Int,
-        hasCustomView: Boolean
-    ) {
-        assertThat(memoryUse.packageName).isEqualTo("test_pkg")
-        assertThat(memoryUse.notificationId)
-            .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
-        assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
-        assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
-        assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture)
-        if (style == null) {
-            assertThat(memoryUse.objectUsage.style).isNull()
-        } else {
-            assertThat(memoryUse.objectUsage.style).isEqualTo(style)
-        }
-        assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon)
-        assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
-    }
-
-    private fun getUseObject(
-        singleItemUseList: List<NotificationMemoryUsage>
-    ): NotificationMemoryUsage {
-        assertThat(singleItemUseList).hasSize(1)
-        return singleItemUseList[0]
-    }
-
-    private fun createNMMWithNotifications(
-        notifications: List<Notification>
-    ): NotificationMemoryMonitor {
-        val notifPipeline: NotifPipeline = mock()
-        val notificationEntries =
-            notifications.map { n ->
-                NotificationEntryBuilder().setTag("test").setNotification(n).build()
-            }
-        whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
-        return NotificationMemoryMonitor(notifPipeline, mock())
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
new file mode 100644
index 0000000..3a16fb3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
@@ -0,0 +1,148 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.tests.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationMemoryViewWalkerTest : SysuiTestCase() {
+
+    private lateinit var testHelper: NotificationTestHelper
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+    }
+
+    @Test
+    fun testViewWalker_nullRow_returnsEmptyView() {
+        val result = NotificationMemoryViewWalker.getViewUsage(null)
+        assertThat(result).isNotNull()
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun testViewWalker_plainNotification() {
+        val row = testHelper.createRow()
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun testViewWalker_bigPictureNotification() {
+        val bigPicture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
+        val largeIcon = Icon.createWithBitmap(Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888))
+        val row =
+            testHelper.createRow(
+                Notification.Builder(mContext)
+                    .setContentText("Test")
+                    .setContentTitle("title")
+                    .setSmallIcon(icon)
+                    .setLargeIcon(largeIcon)
+                    .setStyle(Notification.BigPictureStyle().bigPicture(bigPicture))
+                    .build()
+            )
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_EXPANDED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    largeIcon.bitmap.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount +
+                        icon.bitmap.allocationByteCount +
+                        largeIcon.bitmap.allocationByteCount
+                )
+            )
+
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_CONTRACTED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    largeIcon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount
+                )
+            )
+        // Due to deduplication, this should all be 0.
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun testViewWalker_customView() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
+        val bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
+
+        val views = RemoteViews(mContext.packageName, R.layout.custom_view_dark)
+        views.setImageViewBitmap(R.id.custom_view_dark_image, bitmap)
+        val row =
+            testHelper.createRow(
+                Notification.Builder(mContext)
+                    .setContentText("Test")
+                    .setContentTitle("title")
+                    .setSmallIcon(icon)
+                    .setCustomContentView(views)
+                    .setCustomBigContentView(views)
+                    .build()
+            )
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_CONTRACTED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    bitmap.allocationByteCount,
+                    bitmap.allocationByteCount + icon.bitmap.allocationByteCount
+                )
+            )
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_EXPANDED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    bitmap.allocationByteCount,
+                    bitmap.allocationByteCount + icon.bitmap.allocationByteCount
+                )
+            )
+        // Due to deduplication, this should all be 0.
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 137842e..12cc114 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -232,7 +232,6 @@
     @Test
     public void testUserLockedResetEvenWhenNoChildren() {
         mGroupRow.setUserLocked(true);
-        mGroupRow.removeAllChildren();
         mGroupRow.setUserLocked(false);
         assertFalse("The childrencontainer should not be userlocked but is, the state "
                 + "seems out of sync.", mGroupRow.getChildrenContainer().isUserLocked());
@@ -240,12 +239,11 @@
 
     @Test
     public void testReinflatedOnDensityChange() {
-        mGroupRow.setUserLocked(true);
-        mGroupRow.removeAllChildren();
-        mGroupRow.setUserLocked(false);
         NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
-        mGroupRow.setChildrenContainer(mockContainer);
-        mGroupRow.onDensityOrFontScaleChanged();
+        mNotifRow.setChildrenContainer(mockContainer);
+
+        mNotifRow.onDensityOrFontScaleChanged();
+
         verify(mockContainer).reInflateViews(any(), any());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 8375e7c..5394d88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -51,7 +51,7 @@
 import androidx.test.filters.Suppress;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
deleted file mode 100644
index 81b8e98..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.view.NotificationHeaderView;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.internal.widget.NotificationActionListLayout;
-import com.android.internal.widget.NotificationExpandButton;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.statusbar.notification.FeedbackIcon;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NotificationContentViewTest extends SysuiTestCase {
-
-    NotificationContentView mView;
-
-    @Before
-    @UiThreadTest
-    public void setup() {
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
-
-        mView = new NotificationContentView(mContext, null);
-        ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null);
-        ExpandableNotificationRow mockRow = spy(row);
-        doReturn(10).when(mockRow).getIntrinsicHeight();
-
-        mView.setContainingNotification(mockRow);
-        mView.setHeights(10, 20, 30);
-
-        mView.setContractedChild(createViewWithHeight(10));
-        mView.setExpandedChild(createViewWithHeight(20));
-        mView.setHeadsUpChild(createViewWithHeight(30));
-
-        mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
-    }
-
-    private View createViewWithHeight(int height) {
-        View view = new View(mContext, null);
-        view.setMinimumHeight(height);
-        return view;
-    }
-
-    @Test
-    @UiThreadTest
-    public void testSetFeedbackIcon() {
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockContracted);
-        when(mockContracted.getContext()).thenReturn(mContext);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockExpanded);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockHeadsUp);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted,
-                R.string.notification_feedback_indicator_alerted));
-
-        verify(mockContracted, times(1)).setVisibility(View.VISIBLE);
-        verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
-        verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testExpandButtonFocusIsCalled() {
-        View mockContractedEB = mock(NotificationExpandButton.class);
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockContractedEB);
-        when(mockContracted.getContext()).thenReturn(mContext);
-
-        View mockExpandedEB = mock(NotificationExpandButton.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockExpandedEB);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-
-        View mockHeadsUpEB = mock(NotificationExpandButton.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockHeadsUpEB);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        // Set up all 3 child forms
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        // This is required to call requestAccessibilityFocus()
-        mView.setFocusOnVisibilityChange();
-
-        // The following will initialize the view and switch from not visible to expanded.
-        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
-        mView.setHeadsUp(true);
-
-        verify(mockContractedEB, times(0)).requestAccessibilityFocus();
-        verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
-        verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(true);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(false);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
new file mode 100644
index 0000000..562b4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -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.systemui.statusbar.notification.row
+
+import android.content.res.Resources
+import android.os.UserHandle
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.view.NotificationHeaderView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.NotificationActionListLayout
+import com.android.internal.widget.NotificationExpandButton
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.statusbar.notification.FeedbackIcon
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationContentViewTest : SysuiTestCase() {
+    private lateinit var view: NotificationContentView
+
+    @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+
+    private val notificationContentMargin =
+        mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+
+    @Before
+    fun setup() {
+        initMocks(this)
+
+        mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
+
+        view = spy(NotificationContentView(mContext, /* attrs= */ null))
+        val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
+        row.entry = createMockNotificationEntry(false)
+        val spyRow = spy(row)
+        doReturn(10).whenever(spyRow).intrinsicHeight
+
+        with(view) {
+            initialize(mPeopleNotificationIdentifier, mock(), mock(), mock())
+            setContainingNotification(spyRow)
+            setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
+            contractedChild = createViewWithHeight(10)
+            expandedChild = createViewWithHeight(20)
+            headsUpChild = createViewWithHeight(30)
+            measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+            layout(0, 0, view.measuredWidth, view.measuredHeight)
+        }
+    }
+
+    private fun createViewWithHeight(height: Int) =
+        View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
+    @Test
+    fun testSetFeedbackIcon() {
+        // Given: contractedChild, enpandedChild, and headsUpChild being set
+        val mockContracted = createMockNotificationHeaderView()
+        val mockExpanded = createMockNotificationHeaderView()
+        val mockHeadsUp = createMockNotificationHeaderView()
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        // When: FeedBackIcon is set
+        view.setFeedbackIcon(
+            FeedbackIcon(
+                R.drawable.ic_feedback_alerted,
+                R.string.notification_feedback_indicator_alerted
+            )
+        )
+
+        // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
+        verify(mockContracted).visibility = View.VISIBLE
+        verify(mockExpanded).visibility = View.VISIBLE
+        verify(mockHeadsUp).visibility = View.VISIBLE
+    }
+
+    private fun createMockNotificationHeaderView() =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testExpandButtonFocusIsCalled() {
+        val mockContractedEB = mock<NotificationExpandButton>()
+        val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+
+        val mockExpandedEB = mock<NotificationExpandButton>()
+        val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+
+        val mockHeadsUpEB = mock<NotificationExpandButton>()
+        val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+
+        // Set up all 3 child forms
+        view.contractedChild = mockContracted
+        view.expandedChild = mockExpanded
+        view.headsUpChild = mockHeadsUp
+
+        // This is required to call requestAccessibilityFocus()
+        view.setFocusOnVisibilityChange()
+
+        // The following will initialize the view and switch from not visible to expanded.
+        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
+        view.setHeadsUp(true)
+        verify(mockContractedEB, never()).requestAccessibilityFocus()
+        verify(mockExpandedEB).requestAccessibilityFocus()
+        verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
+    }
+
+    private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.animate()).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(true)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+        verify(mockHeadsUpActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+    }
+
+    @Test
+    fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(false)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+        verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+    }
+
+    @Test
+    fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should not be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should not change,
+        // still be notificationContentMargin
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should be set to 0
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should not show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(true))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+            whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
+        }
+
+    private fun createMockNotificationEntry(showButton: Boolean) =
+        mock<NotificationEntry>().apply {
+            whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
+                .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
+            whenever(this.bubbleMetadata).thenReturn(mock())
+            val sbnMock: StatusBarNotification = mock()
+            val userMock: UserHandle = mock()
+            whenever(this.sbn).thenReturn(sbnMock)
+            whenever(sbnMock.user).thenReturn(userMock)
+            doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
+        }
+
+    private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
+        val outerLayout = LinearLayout(mContext)
+        val innerLayout = LinearLayout(mContext)
+        outerLayout.addView(innerLayout)
+        val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams
+        mlp.setMargins(0, 0, 0, bottomMargin)
+        return innerLayout
+    }
+
+    private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+
+            val resourcesMock: Resources = mock()
+            whenever(resourcesMock.configuration).thenReturn(mock())
+            whenever(this.resources).thenReturn(resourcesMock)
+        }
+
+    private fun getMarginBottom(layout: LinearLayout): Int =
+        (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index cc4cbbf..2b189b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -22,6 +22,7 @@
 
 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -52,14 +53,16 @@
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -149,7 +152,8 @@
                 mock(ConfigurationControllerImpl.class),
                 new Handler(mTestLooper.getLooper()),
                 mock(AccessibilityManagerWrapper.class),
-                mock(UiEventLogger.class)
+                mock(UiEventLogger.class),
+                mock(ShadeExpansionStateManager.class)
         );
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
@@ -192,6 +196,25 @@
     }
 
     /**
+     * Creates a generic row with rounded border.
+     *
+     * @return a generic row with the set roundness.
+     * @throws Exception
+     */
+    public ExpandableNotificationRow createRowWithRoundness(
+            float topRoundness,
+            float bottomRoundness,
+            SourceType sourceType
+    ) throws Exception {
+        ExpandableNotificationRow row = createRow();
+        row.requestTopRoundness(topRoundness, false, sourceType);
+        row.requestBottomRoundness(bottomRoundness, /*animate = */ false, sourceType);
+        assertEquals(topRoundness, row.getTopRoundness(), /* delta = */ 0f);
+        assertEquals(bottomRoundness, row.getBottomRoundness(), /* delta = */ 0f);
+        return row;
+    }
+
+    /**
      * Creates a generic row.
      *
      * @return a generic row with no special properties.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 11798a7..87f4c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -361,6 +361,22 @@
         assertThat(sut.isOnKeyguard).isFalse()
     }
     // endregion
+
+    // region mIsClosing
+    @Test
+    fun isClosing_whenShadeClosing_shouldReturnTrue() {
+        sut.setIsClosing(true)
+
+        assertThat(sut.isClosing).isTrue()
+    }
+
+    @Test
+    fun isClosing_whenShadeFinishClosing_shouldReturnFalse() {
+        sut.setIsClosing(false)
+
+        assertThat(sut.isClosing).isFalse()
+    }
+    // endregion
 }
 
 // region Arrange helper methods.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 7c41abba..438b528 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
@@ -25,6 +25,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
 
@@ -151,4 +152,37 @@
         Assert.assertNotNull("Children container must have a header after recreation",
                 mChildrenContainer.getCurrentHeaderView());
     }
+
+    @Test
+    public void addNotification_shouldResetOnScrollRoundness() throws Exception {
+        ExpandableNotificationRow row = mNotificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.OnScroll);
+
+        mChildrenContainer.addNotification(row, 0);
+
+        Assert.assertEquals(0f, row.getTopRoundness(), /* delta = */ 0f);
+        Assert.assertEquals(0f, row.getBottomRoundness(), /* delta = */ 0f);
+    }
+
+    @Test
+    public void addNotification_shouldNotResetOtherRoundness() throws Exception {
+        ExpandableNotificationRow row1 = mNotificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.DefaultValue);
+        ExpandableNotificationRow row2 = mNotificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.OnDismissAnimation);
+
+        mChildrenContainer.addNotification(row1, 0);
+        mChildrenContainer.addNotification(row2, 0);
+
+        Assert.assertEquals(1f, row1.getTopRoundness(), /* delta = */ 0f);
+        Assert.assertEquals(1f, row1.getBottomRoundness(), /* delta = */ 0f);
+        Assert.assertEquals(1f, row2.getTopRoundness(), /* delta = */ 0f);
+        Assert.assertEquals(1f, row2.getBottomRoundness(), /* delta = */ 0f);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index a95a49c..8c8b644 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -147,8 +147,8 @@
                 createSection(mFirst, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -170,13 +170,13 @@
         when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true);
         row.setHeadsUp(true);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getTopRoundness(), 0.0f);
 
         row.setHeadsUp(false);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -185,8 +185,8 @@
                 createSection(mFirst, mFirst),
                 createSection(null, mSecond)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -195,8 +195,8 @@
                 createSection(mFirst, mFirst),
                 createSection(mSecond, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -205,8 +205,8 @@
                 createSection(mFirst, null),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -215,8 +215,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -226,8 +226,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -238,8 +238,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -250,8 +250,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -262,8 +262,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -274,8 +274,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -286,8 +286,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -298,8 +298,8 @@
                 createSection(null, null)
         });
         mFirst.setHeadsUpAnimatingAway(true);
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
 
@@ -312,8 +312,8 @@
         });
         mFirst.setHeadsUpAnimatingAway(true);
         mFirst.setHeadsUpAnimatingAway(false);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index 9d848e8..ecc0224 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,7 +30,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 7741813..bda2336 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
@@ -8,8 +9,10 @@
 import com.android.systemui.animation.ShadeInterpolation
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.notification.SourceType
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
 import com.android.systemui.util.mockito.mock
 import junit.framework.Assert.assertEquals
@@ -37,6 +40,13 @@
     private val shelfState = shelf.viewState as NotificationShelf.ShelfState
     private val ambientState = mock(AmbientState::class.java)
     private val hostLayoutController: NotificationStackScrollLayoutController = mock()
+    private val notificationTestHelper by lazy {
+        allowTestableLooperAsMainThread()
+        NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this))
+    }
 
     @Before
     fun setUp() {
@@ -299,6 +309,39 @@
         )
     }
 
+    @Test
+    fun resetOnScrollRoundness_shouldSetOnScrollTo0() {
+        val row: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.OnScroll)
+
+        NotificationShelf.resetOnScrollRoundness(row)
+
+        assertEquals(0f, row.topRoundness)
+        assertEquals(0f, row.bottomRoundness)
+    }
+
+    @Test
+    fun resetOnScrollRoundness_shouldNotResetOtherRoundness() {
+        val row1: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.DefaultValue)
+        val row2: ExpandableNotificationRow = notificationTestHelper.createRowWithRoundness(
+                /* topRoundness = */ 1f,
+                /* bottomRoundness = */ 1f,
+                /* sourceType = */ SourceType.OnDismissAnimation)
+
+        NotificationShelf.resetOnScrollRoundness(row1)
+        NotificationShelf.resetOnScrollRoundness(row2)
+
+        assertEquals(1f, row1.topRoundness)
+        assertEquals(1f, row1.bottomRoundness)
+        assertEquals(1f, row2.topRoundness)
+        assertEquals(1f, row2.bottomRoundness)
+    }
+
     private fun setFractionToShade(fraction: Float) {
         whenever(ambientState.fractionToShade).thenReturn(fraction)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 1c9b0be..026c82e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -46,7 +46,7 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -61,6 +61,7 @@
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
@@ -122,6 +123,7 @@
     @Mock private UiEventLogger mUiEventLogger;
     @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
+    @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     @Mock private ShadeController mShadeController;
     @Mock private InteractionJankMonitor mJankMonitor;
     @Mock private StackStateLogger mStackLogger;
@@ -129,6 +131,7 @@
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     @Mock private ShadeTransitionController mShadeTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
+    @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -172,12 +175,14 @@
                 mShadeTransitionController,
                 mUiEventLogger,
                 mRemoteInputManager,
+                mVisibilityLocationProviderDelegator,
                 mShadeController,
                 mJankMonitor,
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
-                mFeatureFlags
+                mFeatureFlags,
+                mNotificationTargetsHelper
         );
 
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 4353036..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
@@ -163,7 +165,7 @@
         mStackScroller.setCentralSurfaces(mCentralSurfaces);
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
-        when(mStackScrollLayoutController.getNoticationRoundessManager())
+        when(mStackScrollLayoutController.getNotificationRoundnessManager())
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
 
@@ -728,6 +730,57 @@
         verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
     }
 
+    @Test
+    public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() {
+        // Given: shade is not closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(false);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should be set to 1
+        assertEquals(1, mAmbientState.getScrollY());
+
+        // Reset scrollY back to 0 to avoid interfering with other tests
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() {
+        // Given: shade is closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(true);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should not change, it should still be 0
+        assertEquals(0, mAmbientState.getScrollY());
+
+        // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+        mAmbientState.setIsClosing(false);
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
+        // Given: mAmbientState.mIsClosing is set to be true
+        // mIsExpanded is set to be false
+        mAmbientState.setIsClosing(true);
+        mStackScroller.setIsExpanded(false);
+
+        // When: onExpansionStopped is called
+        mStackScroller.onExpansionStopped();
+
+        // Then: mAmbientState.scrollY should be set to be 0
+        assertEquals(mAmbientState.getScrollY(), 0);
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
new file mode 100644
index 0000000..a2e9230
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -0,0 +1,107 @@
+package com.android.systemui.statusbar.notification.stack
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.mock
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for {@link NotificationTargetsHelper}. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationTargetsHelperTest : SysuiTestCase() {
+    lateinit var notificationTestHelper: NotificationTestHelper
+    private val sectionsManager: NotificationSectionsManager = mock()
+    private val stackScrollLayout: NotificationStackScrollLayout = mock()
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        notificationTestHelper =
+            NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+    }
+
+    private fun notificationTargetsHelper(
+        notificationGroupCorner: Boolean = true,
+    ) =
+        NotificationTargetsHelper(
+            FakeFeatureFlags().apply {
+                set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+            }
+        )
+
+    @Test
+    fun targetsForFirstNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[0]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.notificationHeaderWrapper, // group header
+                swiped = swiped,
+                after = children.attachedChildren[1],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForMiddleNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[1]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[0],
+                swiped = swiped,
+                after = children.attachedChildren[2],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForLastNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[2]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[1],
+                swiped = swiped,
+                after = null,
+            )
+        assertEquals(expected, actual)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 40aec82..4d9db8c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -14,6 +14,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
@@ -31,10 +32,10 @@
 
     private val hostView = FrameLayout(context)
     private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
-    private val notificationRow = mock(ExpandableNotificationRow::class.java)
-    private val dumpManager = mock(DumpManager::class.java)
-    private val mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager::class.java)
-    private val notificationShelf = mock(NotificationShelf::class.java)
+    private val notificationRow = mock<ExpandableNotificationRow>()
+    private val dumpManager = mock<DumpManager>()
+    private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
+    private val notificationShelf = mock<NotificationShelf>()
     private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
         layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
     }
@@ -46,7 +47,7 @@
             mStatusBarKeyguardViewManager
     )
 
-    private val testableResources = mContext.orCreateTestableResources
+    private val testableResources = mContext.getOrCreateTestableResources()
 
     private fun px(@DimenRes id: Int): Float =
             testableResources.resources.getDimensionPixelSize(id).toFloat()
@@ -98,7 +99,7 @@
         stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
 
         val marginBottom =
-            context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
+                context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
         val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY
         val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f
         assertThat(emptyShadeView.viewState?.yTranslation).isEqualTo(centeredY)
@@ -118,7 +119,7 @@
 
     @Test
     fun resetViewStates_expansionChanging_notificationBecomesTransparent() {
-        whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(false)
+        whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
                 expansionFraction = 0.25f,
                 expectedAlpha = 0.0f
@@ -127,7 +128,7 @@
 
     @Test
     fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() {
-        whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(true)
+        whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
                 expansionFraction = 0.85f,
                 expectedAlpha = 0.0f
@@ -136,7 +137,7 @@
 
     @Test
     fun resetViewStates_expansionChanging_notificationAlphaUpdated() {
-        whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(false)
+        whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
                 expansionFraction = 0.6f,
                 expectedAlpha = getContentAlpha(0.6f)
@@ -145,7 +146,7 @@
 
     @Test
     fun resetViewStates_expansionChangingWhileBouncerInTransit_notificationAlphaUpdated() {
-        whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(true)
+        whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
         resetViewStates_expansionChanging_notificationAlphaUpdated(
                 expansionFraction = 0.95f,
                 expectedAlpha = aboutToShowBouncerProgress(0.95f)
@@ -507,6 +508,192 @@
         assertEquals(1f, currentRoundness)
     }
 
+    @Test
+    fun shadeOpened_hunFullyOverlapsQqsPanel_hunShouldHaveFullShadow() {
+        // Given: shade is opened, yTranslation of HUN is 0,
+        // the height of HUN equals to the height of QQS Panel,
+        // and HUN fully overlaps with QQS Panel
+        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
+                px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView = createHunViewMock(
+                isShadeOpen = true,
+                fullyVisible = false,
+                headerVisibleAmount = 1f
+        )
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(childHunView)
+
+        // When: updateChildZValue() is called for the top HUN
+        stackScrollAlgorithm.updateChildZValue(
+                /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
+                /* StackScrollAlgorithmState= */ algorithmState,
+                /* ambientState= */ ambientState,
+                /* shouldElevateHun= */ true
+        )
+
+        // Then: full shadow would be applied
+        assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation)
+    }
+
+    @Test
+    fun shadeOpened_hunPartiallyOverlapsQQS_hunShouldHavePartialShadow() {
+        // Given: shade is opened, yTranslation of HUN is greater than 0,
+        // the height of HUN is equal to the height of QQS Panel,
+        // and HUN partially overlaps with QQS Panel
+        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
+                px(R.dimen.qqs_layout_padding_bottom)
+        val childHunView = createHunViewMock(
+                isShadeOpen = true,
+                fullyVisible = false,
+                headerVisibleAmount = 1f
+        )
+        // Use half of the HUN's height as overlap
+        childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(childHunView)
+
+        // When: updateChildZValue() is called for the top HUN
+        stackScrollAlgorithm.updateChildZValue(
+                /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
+                /* StackScrollAlgorithmState= */ algorithmState,
+                /* ambientState= */ ambientState,
+                /* shouldElevateHun= */ true
+        )
+
+        // Then: HUN should have shadow, but not as full size
+        assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
+        assertThat(childHunView.viewState.zTranslation)
+                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+    }
+
+    @Test
+    fun shadeOpened_hunDoesNotOverlapQQS_hunShouldHaveNoShadow() {
+        // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height,
+        // the height of HUN is equal to the height of QQS Panel,
+        // and HUN doesn't overlap with QQS Panel
+        ambientState.stackTranslation = px(R.dimen.qqs_layout_margin_top) +
+                px(R.dimen.qqs_layout_padding_bottom)
+        // Mock the height of shade
+        ambientState.setLayoutMinHeight(1000)
+        val childHunView = createHunViewMock(
+                isShadeOpen = true,
+                fullyVisible = true,
+                headerVisibleAmount = 1f
+        )
+        // HUN doesn't overlap with QQS Panel
+        childHunView.viewState.yTranslation = ambientState.topPadding +
+                ambientState.stackTranslation
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(childHunView)
+
+        // When: updateChildZValue() is called for the top HUN
+        stackScrollAlgorithm.updateChildZValue(
+                /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
+                /* StackScrollAlgorithmState= */ algorithmState,
+                /* ambientState= */ ambientState,
+                /* shouldElevateHun= */ true
+        )
+
+        // Then: HUN should not have shadow
+        assertEquals(0f, childHunView.viewState.zTranslation)
+    }
+
+    @Test
+    fun shadeClosed_hunShouldHaveFullShadow() {
+        // Given: shade is closed, ambientState.stackTranslation == -ambientState.topPadding,
+        // the height of HUN is equal to the height of QQS Panel,
+        ambientState.stackTranslation = -ambientState.topPadding
+        // Mock the height of shade
+        ambientState.setLayoutMinHeight(1000)
+        val childHunView = createHunViewMock(
+                isShadeOpen = false,
+                fullyVisible = false,
+                headerVisibleAmount = 0f
+        )
+        childHunView.viewState.yTranslation = 0f
+        // Shade is closed, thus childHunView's headerVisibleAmount is 0
+        childHunView.headerVisibleAmount = 0f
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(childHunView)
+
+        // When: updateChildZValue() is called for the top HUN
+        stackScrollAlgorithm.updateChildZValue(
+                /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
+                /* StackScrollAlgorithmState= */ algorithmState,
+                /* ambientState= */ ambientState,
+                /* shouldElevateHun= */ true
+        )
+
+        // Then: HUN should have full shadow
+        assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation)
+    }
+
+    @Test
+    fun draggingHunToOpenShade_hunShouldHavePartialShadow() {
+        // Given: shade is closed when HUN pops up,
+        // now drags down the HUN to open shade
+        ambientState.stackTranslation = -ambientState.topPadding
+        // Mock the height of shade
+        ambientState.setLayoutMinHeight(1000)
+        val childHunView = createHunViewMock(
+                isShadeOpen = false,
+                fullyVisible = false,
+                headerVisibleAmount = 0.5f
+        )
+        childHunView.viewState.yTranslation = 0f
+        // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
+        // use 0.5 as headerVisibleAmount here
+        childHunView.headerVisibleAmount = 0.5f
+        val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
+        algorithmState.visibleChildren.add(childHunView)
+
+        // When: updateChildZValue() is called for the top HUN
+        stackScrollAlgorithm.updateChildZValue(
+                /* i= */ 0,
+                /* childrenOnTop= */ 0.0f,
+                /* StackScrollAlgorithmState= */ algorithmState,
+                /* ambientState= */ ambientState,
+                /* shouldElevateHun= */ true
+        )
+
+        // Then: HUN should have shadow, but not as full size
+        assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
+        assertThat(childHunView.viewState.zTranslation)
+                .isLessThan(px(R.dimen.heads_up_pinned_elevation))
+    }
+
+    private fun createHunViewMock(
+            isShadeOpen: Boolean,
+            fullyVisible: Boolean,
+            headerVisibleAmount: Float
+    ) =
+            mock<ExpandableNotificationRow>().apply {
+                val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
+                whenever(this.viewState).thenReturn(childViewStateMock)
+
+                whenever(this.mustStayOnScreen()).thenReturn(true)
+                whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
+            }
+
+
+    private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
+            ExpandableViewState().apply {
+                // Mock the HUN's height with ambientState.topPadding +
+                // ambientState.stackTranslation
+                height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
+                if (isShadeOpen && fullyVisible) {
+                    yTranslation =
+                            ambientState.topPadding + ambientState.stackTranslation
+                } else {
+                    yTranslation = 0f
+                }
+                headsUpIsVisible = fullyVisible
+            }
+
     private fun resetViewStates_expansionChanging_notificationAlphaUpdated(
             expansionFraction: Float,
             expectedAlpha: Float
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 4dea6be..7246116 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -136,28 +136,28 @@
                 mAuthController, mStatusBarStateController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
-        mBiometricUnlockController.setBiometricModeListener(mBiometricModeListener);
+        mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener);
     }
 
     @Test
-    public void onBiometricAuthenticated_whenFingerprintAndBiometricsDisallowed_showBouncer() {
+    public void onBiometricAuthenticated_fingerprintAndBiometricsDisallowed_showPrimaryBouncer() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */))
                 .thenReturn(false);
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
-        verify(mStatusBarKeyguardViewManager).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
         verify(mStatusBarKeyguardViewManager, never()).notifyKeyguardAuthenticated(anyBoolean());
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
     }
 
     @Test
-    public void onBiometricAuthenticated_whenFingerprint_nonStrongBioDisallowed_showBouncer() {
+    public void onBiometricAuthenticated_fingerprint_nonStrongBioDisallowed_showPrimaryBouncer() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(false /* isStrongBiometric */))
                 .thenReturn(false);
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, false /* isStrongBiometric */);
-        verify(mStatusBarKeyguardViewManager).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
         assertThat(mBiometricUnlockController.getBiometricType())
@@ -207,7 +207,7 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FINGERPRINT, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
         verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_UNLOCK_COLLAPSING);
@@ -216,7 +216,7 @@
     @Test
     public void onBiometricAuthenticated_whenFingerprintOnBouncer_dismissBouncer() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
         // the value of isStrongBiometric doesn't matter here since we only care about the returned
         // value of isUnlockingWithBiometricAllowed()
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
@@ -274,7 +274,7 @@
     }
 
     @Test
-    public void onBiometricAuthenticated_whenFace_andBypass_encrypted_showBouncer() {
+    public void onBiometricAuthenticated_whenFace_andBypass_encrypted_showPrimaryBouncer() {
         reset(mUpdateMonitor);
         when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
@@ -285,7 +285,7 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FACE, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_SHOW_BOUNCER);
     }
@@ -314,7 +314,7 @@
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
                 BiometricSourceType.FACE, true /* isStrongBiometric */);
 
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
         assertThat(mBiometricUnlockController.getMode())
                 .isEqualTo(BiometricUnlockController.MODE_NONE);
     }
@@ -322,7 +322,7 @@
     @Test
     public void onBiometricAuthenticated_whenFaceOnBouncer_dismissBouncer() {
         when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
         // the value of isStrongBiometric doesn't matter here since we only care about the returned
         // value of isUnlockingWithBiometricAllowed()
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
@@ -342,7 +342,7 @@
         when(mKeyguardBypassController.getBypassEnabled()).thenReturn(true);
         when(mKeyguardBypassController.onBiometricAuthenticated(any(), anyBoolean()))
                 .thenReturn(true);
-        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(true);
         // the value of isStrongBiometric doesn't matter here since we only care about the returned
         // value of isUnlockingWithBiometricAllowed()
         mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT,
@@ -369,23 +369,23 @@
     }
 
     @Test
-    public void onUdfpsConsecutivelyFailedThreeTimes_showBouncer() {
+    public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() {
         // GIVEN UDFPS is supported
         when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true);
 
         // WHEN udfps fails once - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
 
         // WHEN udfps fails the second time - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
-        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
+        verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean());
 
         // WHEN udpfs fails the third time
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
 
         // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index e1f20d5..d5bfe1f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -29,7 +29,7 @@
 import android.os.PowerManager;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
 
 import androidx.test.filters.SmallTest;
 
@@ -41,6 +41,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -60,6 +61,8 @@
 
 import java.util.Optional;
 
+import dagger.Lazy;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase {
@@ -84,6 +87,7 @@
     @Mock private Vibrator mVibrator;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private SystemBarAttributesListener mSystemBarAttributesListener;
+    @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -115,7 +119,8 @@
                 Optional.of(mVibrator),
                 new DisableFlagsLogger(),
                 DEFAULT_DISPLAY,
-                mSystemBarAttributesListener);
+                mSystemBarAttributesListener,
+                mCameraLauncherLazy);
 
         when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
         when(mRemoteInputQuickSettingsDisabler.adjustDisableFlags(anyInt()))
@@ -177,7 +182,7 @@
         AppearanceRegion[] appearanceRegions = new AppearanceRegion[]{};
         boolean navbarColorManagedByIme = true;
         int behavior = 456;
-        InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        int requestedVisibleTypes = WindowInsets.Type.systemBars();
         String packageName = "test package name";
         LetterboxDetails[] letterboxDetails = new LetterboxDetails[]{};
 
@@ -187,7 +192,7 @@
                 appearanceRegions,
                 navbarColorManagedByIme,
                 behavior,
-                requestedVisibilities,
+                requestedVisibleTypes,
                 packageName,
                 letterboxDetails);
 
@@ -197,7 +202,7 @@
                 appearanceRegions,
                 navbarColorManagedByIme,
                 behavior,
-                requestedVisibilities,
+                requestedVisibleTypes,
                 packageName,
                 letterboxDetails
         );
@@ -209,7 +214,7 @@
         AppearanceRegion[] appearanceRegions = new AppearanceRegion[]{};
         boolean navbarColorManagedByIme = true;
         int behavior = 456;
-        InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
+        int requestedVisibleTypes = WindowInsets.Type.systemBars();
         String packageName = "test package name";
         LetterboxDetails[] letterboxDetails = new LetterboxDetails[]{};
 
@@ -219,7 +224,7 @@
                 appearanceRegions,
                 navbarColorManagedByIme,
                 behavior,
-                requestedVisibilities,
+                requestedVisibleTypes,
                 packageName,
                 letterboxDetails);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index ad497a2..41912f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -80,6 +80,7 @@
 
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
@@ -98,7 +99,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -110,6 +112,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
+import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.NotificationPanelView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
@@ -218,6 +221,7 @@
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
     @Mock private StatusBarStateControllerImpl mStatusBarStateController;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
     @Mock private StatusBarNotificationPresenter mNotificationPresenter;
@@ -233,6 +237,7 @@
     @Mock private NavigationBarController mNavigationBarController;
     @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController;
     @Mock private SysuiColorExtractor mColorExtractor;
+    private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock private ColorExtractor.GradientColors mGradientColors;
     @Mock private PulseExpansionHandler mPulseExpansionHandler;
     @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
@@ -271,7 +276,6 @@
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private LockscreenShadeTransitionController mLockscreenTransitionController;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private WallpaperManager mWallpaperManager;
     @Mock private IWallpaperManager mIWallpaperManager;
@@ -285,6 +289,8 @@
     @Mock private InteractionJankMonitor mJankMonitor;
     @Mock private DeviceStateManager mDeviceStateManager;
     @Mock private WiredChargingRippleController mWiredChargingRippleController;
+    @Mock private Lazy<CameraLauncher> mCameraLauncherLazy;
+    @Mock private CameraLauncher mCameraLauncher;
     /**
      * The process of registering/unregistering a predictive back callback requires a
      * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test.
@@ -296,9 +302,10 @@
 
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
-    private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
-    private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
-    private InitController mInitController = new InitController();
+    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private final InitController mInitController = new InitController();
     private final DumpManager mDumpManager = new DumpManager();
 
     @Before
@@ -322,7 +329,8 @@
                         mock(NotificationInterruptLogger.class),
                         new Handler(TestableLooper.get(this).getLooper()),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class));
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class));
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -335,6 +343,7 @@
                 mVisibilityProvider,
                 mock(NotifPipeline.class),
                 mStatusBarStateController,
+                mShadeExpansionStateManager,
                 mExpansionStateLogger,
                 new NotificationPanelLoggerFake()
         );
@@ -363,10 +372,10 @@
             return null;
         }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
 
-        WakefulnessLifecycle wakefulnessLifecycle =
+        mWakefulnessLifecycle =
                 new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager);
-        wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
-        wakefulnessLifecycle.dispatchFinishedWakingUp();
+        mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
+        mWakefulnessLifecycle.dispatchFinishedWakingUp();
 
         when(mGradientColors.supportsDarkText()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
@@ -374,6 +383,7 @@
 
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
+        when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher);
 
         when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent);
         when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn(
@@ -425,7 +435,7 @@
                 mBatteryController,
                 mColorExtractor,
                 new ScreenLifecycle(mDumpManager),
-                wakefulnessLifecycle,
+                mWakefulnessLifecycle,
                 mStatusBarStateController,
                 Optional.of(mBubbles),
                 mDeviceProvisionedController,
@@ -475,7 +485,9 @@
                 mActivityLaunchAnimator,
                 mJankMonitor,
                 mDeviceStateManager,
-                mWiredChargingRippleController, mDreamManager) {
+                mWiredChargingRippleController,
+                mDreamManager,
+                mCameraLauncherLazy) {
             @Override
             protected ViewRootImpl getViewRootImpl() {
                 return mViewRootImpl;
@@ -496,7 +508,7 @@
                 mKeyguardVieMediatorCallback);
 
         // TODO: we should be able to call mCentralSurfaces.start() and have all the below values
-        // initialized automatically.
+        // initialized automatically and make NPVC private.
         mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView;
         mCentralSurfaces.mNotificationPanelViewController = mNotificationPanelViewController;
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
@@ -504,6 +516,8 @@
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
         mCentralSurfaces.mBarService = mBarService;
         mCentralSurfaces.mStackScroller = mStackScroller;
+        mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock(
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "sysui:GestureWakeLock");
         mCentralSurfaces.startKeyguard();
         mInitController.executePostInitTasks();
         notificationLogger.setUpWithContainer(mNotificationListContainer);
@@ -885,7 +899,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(true);
+        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(true);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any());
     }
@@ -921,7 +935,7 @@
         mCentralSurfaces.showKeyguardImpl();
 
         // Starting a pulse should change the scrim controller to the pulsing state
-        when(mNotificationPanelViewController.isLaunchingAffordanceWithPreview()).thenReturn(false);
+        when(mCameraLauncher.isLaunchingAffordance()).thenReturn(false);
         mCentralSurfaces.updateScrimController();
         verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
     }
@@ -1017,6 +1031,60 @@
     }
 
     @Test
+    public void collapseShade_callsAnimateCollapsePanels_whenExpanded() {
+        // GIVEN the shade is expanded
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() {
+        // GIVEN the shade is collapsed
+        mCentralSurfaces.onShadeExpansionFullyChanged(false);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is NOT called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.onShadeExpansionFullyChanged(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
     public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() {
         when(mKeyguardStateController.isShowing()).thenReturn(false);
         setFoldedStates(FOLD_STATE_FOLDED);
@@ -1068,6 +1136,55 @@
         assertThat(onDismissActionCaptor.getValue().onDismiss()).isFalse();
     }
 
+    @Test
+    public void testKeyguardHideDelayedIfOcclusionAnimationRunning() {
+        // Show the keyguard and verify we've done so.
+        setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD);
+
+        // Request to hide the keyguard, but while the occlude animation is playing. We should delay
+        // this hide call, since we're playing the occlude animation over the keyguard and thus want
+        // it to remain visible.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController, never()).setState(StatusBarState.SHADE);
+
+        // Once the animation ends, verify that the keyguard is actually hidden.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.SHADE);
+    }
+
+    @Test
+    public void testKeyguardHideNotDelayedIfOcclusionAnimationNotRunning() {
+        // Show the keyguard and verify we've done so.
+        setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD);
+
+        // Hide the keyguard while the occlusion animation is not running. Verify that we
+        // immediately hide the keyguard.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.SHADE);
+    }
+
+    /**
+     * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
+     * to reconfigure the keyguard to reflect the requested showing/occluded states.
+     */
+    private void setKeyguardShowingAndOccluded(boolean showing, boolean occluded) {
+        when(mStatusBarStateController.isKeyguardRequested()).thenReturn(showing);
+        when(mKeyguardStateController.isOccluded()).thenReturn(occluded);
+
+        // If we want to show the keyguard, make sure that we think we're awake and not unlocking.
+        if (showing) {
+            when(mBiometricUnlockController.isWakeAndUnlock()).thenReturn(false);
+            mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
+        }
+
+        mCentralSurfaces.updateIsKeyguard(false /* forceStateChange */);
+    }
+
     private void setDeviceState(int state) {
         ArgumentCaptor<DeviceStateManager.DeviceStateCallback> callbackCaptor =
                 ArgumentCaptor.forClass(DeviceStateManager.DeviceStateCallback.class);
@@ -1102,7 +1219,8 @@
                 NotificationInterruptLogger logger,
                 Handler mainHandler,
                 NotifPipelineFlags flags,
-                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+                UiEventLogger uiEventLogger) {
             super(
                     contentResolver,
                     powerManager,
@@ -1115,7 +1233,8 @@
                     logger,
                     mainHandler,
                     flags,
-                    keyguardNotificationVisibilityProvider
+                    keyguardNotificationVisibilityProvider,
+                    uiEventLogger
             );
             mUseHeadsUp = true;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
index fee3ccb..038af8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt
@@ -14,23 +14,37 @@
 
 package com.android.systemui.statusbar.phone
 
-import androidx.test.filters.SmallTest
+import android.content.res.Configuration
+import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR
+import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.content.res.Configuration.UI_MODE_TYPE_CAR
+import android.os.LocaleList
 import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import java.util.Locale
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class ConfigurationControllerImplTest : SysuiTestCase() {
 
-    private val mConfigurationController =
-            com.android.systemui.statusbar.phone.ConfigurationControllerImpl(mContext)
+    private lateinit var mConfigurationController: ConfigurationControllerImpl
+
+    @Before
+    fun setUp() {
+        mConfigurationController = ConfigurationControllerImpl(mContext)
+    }
 
     @Test
     fun testThemeChange() {
@@ -57,4 +71,303 @@
         verify(listener).onThemeChanged()
         verify(listener2, never()).onThemeChanged()
     }
+
+    @Test
+    fun configChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.densityDpi = 12
+        config.smallestScreenWidthDp = 240
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the config is updated
+        config.densityDpi = 20
+        config.smallestScreenWidthDp = 300
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.changedConfig?.densityDpi).isEqualTo(20)
+        assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300)
+    }
+
+    @Test
+    fun densityChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.densityDpi = 12
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the density is updated
+        config.densityDpi = 20
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.densityOrFontScaleChanged).isTrue()
+    }
+
+    @Test
+    fun fontChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.fontScale = 1.5f
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the font is updated
+        config.fontScale = 1.4f
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.densityOrFontScaleChanged).isTrue()
+    }
+
+    @Test
+    fun isCarAndUiModeChanged_densityListenerNotified() {
+        val config = mContext.resources.configuration
+        config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES
+        // Re-create the controller since we calculate car mode on creation
+        mConfigurationController = ConfigurationControllerImpl(mContext)
+
+        val listener = createAndAddListener()
+
+        // WHEN the ui mode is updated
+        config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.densityOrFontScaleChanged).isTrue()
+    }
+
+    @Test
+    fun isNotCarAndUiModeChanged_densityListenerNotNotified() {
+        val config = mContext.resources.configuration
+        config.uiMode = UI_MODE_NIGHT_YES
+        // Re-create the controller since we calculate car mode on creation
+        mConfigurationController = ConfigurationControllerImpl(mContext)
+
+        val listener = createAndAddListener()
+
+        // WHEN the ui mode is updated
+        config.uiMode = UI_MODE_NIGHT_NO
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is not notified because it's not car mode
+        assertThat(listener.densityOrFontScaleChanged).isFalse()
+    }
+
+    @Test
+    fun smallestScreenWidthChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.smallestScreenWidthDp = 240
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the width is updated
+        config.smallestScreenWidthDp = 300
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.smallestScreenWidthChanged).isTrue()
+    }
+
+    @Test
+    fun maxBoundsChange_newConfigObject_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.windowConfiguration.setMaxBounds(0, 0, 200, 200)
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN a new configuration object with new bounds is sent
+        val newConfig = Configuration()
+        newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100)
+        mConfigurationController.onConfigurationChanged(newConfig)
+
+        // THEN the listener is notified
+        assertThat(listener.maxBoundsChanged).isTrue()
+    }
+
+    // Regression test for b/245799099
+    @Test
+    fun maxBoundsChange_sameObject_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.windowConfiguration.setMaxBounds(0, 0, 200, 200)
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the existing config is updated with new bounds
+        config.windowConfiguration.setMaxBounds(0, 0, 100, 100)
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.maxBoundsChanged).isTrue()
+    }
+
+
+    @Test
+    fun localeListChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.locales = LocaleList(Locale.CANADA, Locale.GERMANY)
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the locales are updated
+        config.locales = LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE)
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.localeListChanged).isTrue()
+    }
+
+    @Test
+    fun uiModeChanged_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.uiMode = UI_MODE_NIGHT_YES
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the ui mode is updated
+        config.uiMode = UI_MODE_NIGHT_NO
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.uiModeChanged).isTrue()
+    }
+
+    @Test
+    fun layoutDirectionUpdated_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the layout is updated
+        config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.layoutDirectionChanged).isTrue()
+    }
+
+    @Test
+    fun assetPathsUpdated_listenerNotified() {
+        val config = mContext.resources.configuration
+        config.assetsSeq = 45
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN the assets sequence is updated
+        config.assetsSeq = 46
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified
+        assertThat(listener.themeChanged).isTrue()
+    }
+
+    @Test
+    fun multipleUpdates_listenerNotifiedOfAll() {
+        val config = mContext.resources.configuration
+        config.densityDpi = 14
+        config.windowConfiguration.setMaxBounds(0, 0, 2, 2)
+        config.uiMode = UI_MODE_NIGHT_YES
+        mConfigurationController.onConfigurationChanged(config)
+
+        val listener = createAndAddListener()
+
+        // WHEN multiple fields are updated
+        config.densityDpi = 20
+        config.windowConfiguration.setMaxBounds(0, 0, 3, 3)
+        config.uiMode = UI_MODE_NIGHT_NO
+        mConfigurationController.onConfigurationChanged(config)
+
+        // THEN the listener is notified of all of them
+        assertThat(listener.densityOrFontScaleChanged).isTrue()
+        assertThat(listener.maxBoundsChanged).isTrue()
+        assertThat(listener.uiModeChanged).isTrue()
+    }
+
+    @Test
+    fun equivalentConfigObject_listenerNotNotified() {
+        val config = mContext.resources.configuration
+        val listener = createAndAddListener()
+
+        // WHEN we update with the new object that has all the same fields
+        mConfigurationController.onConfigurationChanged(Configuration(config))
+
+        listener.assertNoMethodsCalled()
+    }
+
+    private fun createAndAddListener(): TestListener {
+        val listener = TestListener()
+        mConfigurationController.addCallback(listener)
+        // Adding a listener can trigger some callbacks, so we want to reset the values right
+        // after the listener is added
+        listener.reset()
+        return listener
+    }
+
+    private class TestListener : ConfigurationListener {
+        var changedConfig: Configuration? = null
+        var densityOrFontScaleChanged = false
+        var smallestScreenWidthChanged = false
+        var maxBoundsChanged = false
+        var uiModeChanged = false
+        var themeChanged = false
+        var localeListChanged = false
+        var layoutDirectionChanged = false
+
+        override fun onConfigChanged(newConfig: Configuration?) {
+            changedConfig = newConfig
+        }
+        override fun onDensityOrFontScaleChanged() {
+            densityOrFontScaleChanged = true
+        }
+        override fun onSmallestScreenWidthChanged() {
+            smallestScreenWidthChanged = true
+        }
+        override fun onMaxBoundsChanged() {
+            maxBoundsChanged = true
+        }
+        override fun onUiModeChanged() {
+            uiModeChanged = true
+        }
+        override fun onThemeChanged() {
+            themeChanged = true
+        }
+        override fun onLocaleListChanged() {
+            localeListChanged = true
+        }
+        override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
+            layoutDirectionChanged = true
+        }
+
+        fun assertNoMethodsCalled() {
+            assertThat(densityOrFontScaleChanged).isFalse()
+            assertThat(smallestScreenWidthChanged).isFalse()
+            assertThat(maxBoundsChanged).isFalse()
+            assertThat(uiModeChanged).isFalse()
+            assertThat(themeChanged).isFalse()
+            assertThat(localeListChanged).isFalse()
+            assertThat(layoutDirectionChanged).isFalse()
+        }
+
+        fun reset() {
+            changedConfig = null
+            densityOrFontScaleChanged = false
+            smallestScreenWidthChanged = false
+            maxBoundsChanged = false
+            uiModeChanged = false
+            themeChanged = false
+            localeListChanged = false
+            layoutDirectionChanged = false
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index e252401..780e0c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -31,6 +31,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.AlertingNotificationManagerTest;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -67,6 +68,7 @@
     @Mock private KeyguardBypassController mBypassController;
     @Mock private ConfigurationControllerImpl mConfigurationController;
     @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private UiEventLogger mUiEventLogger;
     private boolean mLivesPastNormalTime;
 
@@ -81,7 +83,8 @@
                 ConfigurationController configurationController,
                 Handler handler,
                 AccessibilityManagerWrapper accessibilityManagerWrapper,
-                UiEventLogger uiEventLogger
+                UiEventLogger uiEventLogger,
+                ShadeExpansionStateManager shadeExpansionStateManager
         ) {
             super(
                     context,
@@ -93,7 +96,8 @@
                     configurationController,
                     handler,
                     accessibilityManagerWrapper,
-                    uiEventLogger
+                    uiEventLogger,
+                    shadeExpansionStateManager
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
@@ -125,7 +129,8 @@
                 mConfigurationController,
                 mTestHandler,
                 mAccessibilityManagerWrapper,
-                mUiEventLogger
+                mUiEventLogger,
+                mShadeExpansionStateManager
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
index ab209d1..d3b5418 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
@@ -58,7 +58,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
-import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
+import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Assert;
@@ -86,7 +86,7 @@
     @Mock
     private KeyguardHostViewController mKeyguardHostViewController;
     @Mock
-    private BouncerExpansionCallback mExpansionCallback;
+    private KeyguardBouncer.PrimaryBouncerExpansionCallback mExpansionCallback;
     @Mock
     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock
@@ -476,7 +476,8 @@
         mBouncer.ensureView();
         mBouncer.setExpansion(0.5f);
 
-        final BouncerExpansionCallback callback = mock(BouncerExpansionCallback.class);
+        final PrimaryBouncerExpansionCallback callback =
+                mock(PrimaryBouncerExpansionCallback.class);
         mBouncer.addBouncerExpansionCallback(callback);
 
         mBouncer.setExpansion(EXPANSION_HIDDEN);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 6ec5cf8..eb0b9b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -56,14 +56,12 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController;
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherFeatureController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -112,16 +110,12 @@
     private StatusBarContentInsetsProvider mStatusBarContentInsetsProvider;
     @Mock
     private UserManager mUserManager;
+    @Mock
+    private StatusBarUserChipViewModel mStatusBarUserChipViewModel;
     @Captor
     private ArgumentCaptor<ConfigurationListener> mConfigurationListenerCaptor;
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardCallbackCaptor;
-    @Mock
-    private StatusBarUserSwitcherFeatureController mStatusBarUserSwitcherFeatureController;
-    @Mock
-    private StatusBarUserSwitcherController mStatusBarUserSwitcherController;
-    @Mock
-    private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
     @Mock private SecureSettings mSecureSettings;
     @Mock private CommandQueue mCommandQueue;
     @Mock private KeyguardLogger mLogger;
@@ -169,9 +163,7 @@
                 mStatusBarStateController,
                 mStatusBarContentInsetsProvider,
                 mUserManager,
-                mStatusBarUserSwitcherFeatureController,
-                mStatusBarUserSwitcherController,
-                mStatusBarUserInfoTracker,
+                mStatusBarUserChipViewModel,
                 mSecureSettings,
                 mCommandQueue,
                 mFakeExecutor,
@@ -479,8 +471,7 @@
     @Test
     public void testNewUserSwitcherDisablesAvatar_newUiOn() {
         // GIVEN the status bar user switcher chip is enabled
-        when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled())
-                .thenReturn(true);
+        when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(true);
 
         // WHEN the controller is created
         mController = createController();
@@ -492,8 +483,7 @@
     @Test
     public void testNewUserSwitcherDisablesAvatar_newUiOff() {
         // GIVEN the status bar user switcher chip is disabled
-        when(mStatusBarUserSwitcherFeatureController.isStatusBarUserSwitcherFeatureEnabled())
-                .thenReturn(false);
+        when(mStatusBarUserChipViewModel.getChipEnabled()).thenReturn(false);
 
         // WHEN the controller is created
         mController = createController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
index fca9771..287ebba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
@@ -30,6 +30,7 @@
 import android.view.Display;
 import android.view.View;
 import android.view.ViewPropertyAnimator;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 
 import androidx.lifecycle.Observer;
@@ -107,7 +108,7 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                null /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 null /* packageName */,
                 null /* letterboxDetails */);
         assertTrue(mLightsOutNotifController.areLightsOut());
@@ -121,7 +122,7 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                null /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 null /* packageName */,
                 null /* letterboxDetails */);
         assertFalse(mLightsOutNotifController.areLightsOut());
@@ -153,7 +154,7 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                null /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 null /* packageName */,
                 null /* letterboxDetails */);
 
@@ -174,7 +175,7 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                null /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 null /* packageName */,
                 null /* letterboxDetails */);
 
@@ -195,7 +196,7 @@
                 null /* appearanceRegions */,
                 false /* navbarColorManagedByIme */,
                 BEHAVIOR_DEFAULT,
-                null /* requestedVisibilities */,
+                WindowInsets.Type.defaultVisible(),
                 null /* packageName */,
                 null /* letterboxDetails */);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
index 086e5df..b80b825 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
@@ -92,7 +92,7 @@
 
         iconContainer.calculateIconXTranslations()
         assertEquals(10f, iconState.xTranslation)
-        assertFalse(iconContainer.hasOverflow())
+        assertFalse(iconContainer.areIconsOverflowing())
     }
 
     @Test
@@ -121,7 +121,7 @@
         assertEquals(30f, iconContainer.getIconState(iconThree).xTranslation)
         assertEquals(40f, iconContainer.getIconState(iconFour).xTranslation)
 
-        assertFalse(iconContainer.hasOverflow())
+        assertFalse(iconContainer.areIconsOverflowing())
     }
 
     @Test
@@ -150,7 +150,7 @@
         assertEquals(10f, iconContainer.getIconState(iconOne).xTranslation)
         assertEquals(20f, iconContainer.getIconState(iconTwo).xTranslation)
         assertEquals(30f, iconContainer.getIconState(iconThree).xTranslation)
-        assertTrue(iconContainer.hasOverflow())
+        assertTrue(iconContainer.areIconsOverflowing())
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index a61fba5..320a083 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -27,11 +27,11 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.shade.NotificationPanelViewController
-import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.unfold.SysUIUnfoldComponent
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
+import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -64,7 +64,7 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
     @Mock
-    private lateinit var userSwitcherController: StatusBarUserSwitcherController
+    private lateinit var userChipViewModel: StatusBarUserChipViewModel
     @Mock
     private lateinit var viewUtil: ViewUtil
 
@@ -79,14 +79,13 @@
         `when`(notificationPanelViewController.view).thenReturn(panelView)
         `when`(sysuiUnfoldComponent.getStatusBarMoveFromCenterAnimationController())
             .thenReturn(moveFromCenterAnimation)
-        // create the view on main thread as it requires main looper
+        // create the view and controller on main thread as it requires main looper
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             val parent = FrameLayout(mContext) // add parent to keep layout params
             view = LayoutInflater.from(mContext)
                 .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView
+            controller = createAndInitController(view)
         }
-
-        controller = createAndInitController(view)
     }
 
     @Test
@@ -106,7 +105,10 @@
         val view = createViewMock()
         val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java)
         unfoldConfig.isEnabled = true
-        controller = createAndInitController(view)
+        // create the controller on main thread as it requires main looper
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            controller = createAndInitController(view)
+        }
 
         verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture())
         argumentCaptor.value.onPreDraw()
@@ -126,7 +128,7 @@
         return PhoneStatusBarViewController.Factory(
             Optional.of(sysuiUnfoldComponent),
             Optional.of(progressProvider),
-            userSwitcherController,
+            userChipViewModel,
             viewUtil,
             configurationController
         ).create(view, touchEventHandler).also {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 4d1a52c..de71e2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1143,7 +1143,7 @@
     }
 
     @Test
-    public void testAuthScrim_notifScrimOpaque_whenShadeFullyExpanded() {
+    public void testAuthScrim_setClipQSScrimTrue_notifScrimOpaque_whenShadeFullyExpanded() {
         // GIVEN device has an activity showing ('UNLOCKED' state can occur on the lock screen
         // with the camera app occluding the keyguard)
         mScrimController.transitionTo(ScrimState.UNLOCKED);
@@ -1169,6 +1169,34 @@
         ));
     }
 
+
+    @Test
+    public void testAuthScrim_setClipQSScrimFalse_notifScrimOpaque_whenShadeFullyExpanded() {
+        // GIVEN device has an activity showing ('UNLOCKED' state can occur on the lock screen
+        // with the camera app occluding the keyguard)
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        mScrimController.setClipsQsScrim(false);
+        mScrimController.setRawPanelExpansionFraction(1);
+        // notifications scrim alpha change require calling setQsPosition
+        mScrimController.setQsPosition(0, 300);
+        finishAnimationsImmediately();
+
+        // WHEN the user triggers the auth bouncer
+        mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE);
+        finishAnimationsImmediately();
+
+        assertEquals("Behind scrim should be opaque",
+                mScrimBehind.getViewAlpha(), 1, 0.0);
+        assertEquals("Notifications scrim should be opaque",
+                mNotificationsScrim.getViewAlpha(), 1, 0.0);
+
+        assertScrimTinted(Map.of(
+                mScrimInFront, true,
+                mScrimBehind, true,
+                mNotificationsScrim, false
+        ));
+    }
+
     @Test
     public void testAuthScrimKeyguard() {
         // GIVEN device is on the keyguard
@@ -1273,7 +1301,7 @@
 
     @Test
     public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         // clipping doesn't change tested logic but allows to assert scrims more in line with
         // their expected large screen behaviour
         mScrimController.setClipsQsScrim(false);
@@ -1289,7 +1317,7 @@
 
     @Test
     public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
 
         mScrimController.transitionTo(SHADE_LOCKED);
 
@@ -1305,7 +1333,7 @@
 
     @Test
     public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(false);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
         mScrimController.transitionTo(SHADE_LOCKED);
 
@@ -1320,7 +1348,7 @@
 
     @Test
     public void notificationAlpha_unnocclusionAnimating_bouncerActive_usesKeyguardNotifAlpha() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         mScrimController.setClipsQsScrim(true);
 
         mScrimController.transitionTo(ScrimState.KEYGUARD);
@@ -1342,7 +1370,7 @@
 
     @Test
     public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(false);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
 
         mScrimController.transitionTo(ScrimState.KEYGUARD);
         mScrimController.setUnocclusionAnimationRunning(true);
@@ -1363,7 +1391,7 @@
 
     @Test
     public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true);
         mScrimController.setClipsQsScrim(true);
 
         mScrimController.transitionTo(ScrimState.KEYGUARD);
@@ -1383,7 +1411,7 @@
 
     @Test
     public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() {
-        when(mStatusBarKeyguardViewManager.isBouncerInTransit()).thenReturn(false);
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
         mScrimController.setClipsQsScrim(true);
 
         mScrimController.transitionTo(ScrimState.KEYGUARD);
@@ -1402,6 +1430,17 @@
     }
 
     @Test
+    public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() {
+        when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false);
+        mScrimController.setClipsQsScrim(false);
+
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        finishAnimationsImmediately();
+        assertThat(mScrimBehind.getTint())
+                .isEqualTo(ScrimState.KEYGUARD.getBehindTint());
+    }
+
+    @Test
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.transitionTo(SHADE_LOCKED);
         mScrimController.setRawPanelExpansionFraction(1.0f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index e86676b..1759fb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -19,9 +19,9 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
-import android.test.suitebuilder.annotation.SmallTest
 import android.view.Display
 import android.view.DisplayCutout
+import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -463,16 +463,10 @@
         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
             mock(DumpManager::class.java))
 
-        givenDisplay(
-            screenBounds = Rect(0, 0, 1080, 2160),
-            displayUniqueId = "1"
-        )
+        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
         val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
-        givenDisplay(
-            screenBounds = Rect(0, 0, 800, 600),
-            displayUniqueId = "2"
-        )
-        configurationController.onConfigurationChanged(configuration)
+
+        configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600)
 
         // WHEN: get insets on the second display
         val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
@@ -487,23 +481,15 @@
         // get insets and switch back
         val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
             mock(DumpManager::class.java))
-        givenDisplay(
-            screenBounds = Rect(0, 0, 1080, 2160),
-            displayUniqueId = "1"
-        )
+
+        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
         val firstDisplayInsetsFirstCall = provider
             .getStatusBarContentAreaForRotation(ROTATION_NONE)
-        givenDisplay(
-            screenBounds = Rect(0, 0, 800, 600),
-            displayUniqueId = "2"
-        )
-        configurationController.onConfigurationChanged(configuration)
+
+        configuration.windowConfiguration.maxBounds = Rect(0, 0, 800, 600)
         provider.getStatusBarContentAreaForRotation(ROTATION_NONE)
-        givenDisplay(
-            screenBounds = Rect(0, 0, 1080, 2160),
-            displayUniqueId = "1"
-        )
-        configurationController.onConfigurationChanged(configuration)
+
+        configuration.windowConfiguration.maxBounds = Rect(0, 0, 1080, 2160)
 
         // WHEN: get insets on the first display again
         val firstDisplayInsetsSecondCall = provider
@@ -513,9 +499,70 @@
         assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall)
     }
 
-    private fun givenDisplay(screenBounds: Rect, displayUniqueId: String) {
-        `when`(display.uniqueId).thenReturn(displayUniqueId)
-        configuration.windowConfiguration.maxBounds = screenBounds
+    // Regression test for b/245799099
+    @Test
+    fun onMaxBoundsChanged_listenerNotified() {
+        // Start out with an existing configuration with bounds
+        configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
+        configurationController.onConfigurationChanged(configuration)
+        val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
+                mock(DumpManager::class.java))
+        val listener = object : StatusBarContentInsetsChangedListener {
+            var triggered = false
+
+            override fun onStatusBarContentInsetsChanged() {
+                triggered = true
+            }
+        }
+        provider.addCallback(listener)
+
+        // WHEN the config is updated with new bounds
+        configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789)
+        configurationController.onConfigurationChanged(configuration)
+
+        // THEN the listener is notified
+        assertThat(listener.triggered).isTrue()
+    }
+
+    @Test
+    fun onDensityOrFontScaleChanged_listenerNotified() {
+        configuration.densityDpi = 12
+        val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
+                mock(DumpManager::class.java))
+        val listener = object : StatusBarContentInsetsChangedListener {
+            var triggered = false
+
+            override fun onStatusBarContentInsetsChanged() {
+                triggered = true
+            }
+        }
+        provider.addCallback(listener)
+
+        // WHEN the config is updated
+        configuration.densityDpi = 20
+        configurationController.onConfigurationChanged(configuration)
+
+        // THEN the listener is notified
+        assertThat(listener.triggered).isTrue()
+    }
+
+    @Test
+    fun onThemeChanged_listenerNotified() {
+        val provider = StatusBarContentInsetsProvider(contextMock, configurationController,
+                mock(DumpManager::class.java))
+        val listener = object : StatusBarContentInsetsChangedListener {
+            var triggered = false
+
+            override fun onStatusBarContentInsetsChanged() {
+                triggered = true
+            }
+        }
+        provider.addCallback(listener)
+
+        configurationController.notifyThemeChanged()
+
+        // THEN the listener is notified
+        assertThat(listener.triggered).isTrue()
     }
 
     private fun assertRects(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 9c56c26..6fb6893 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -45,7 +45,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
 import org.junit.Before;
@@ -80,7 +80,7 @@
                 layout,
                 StatusBarLocation.HOME,
                 mock(StatusBarPipelineFlags.class),
-                mock(WifiViewModel.class),
+                mock(WifiUiAdapter.class),
                 mock(MobileUiAdapter.class),
                 mMobileContextProvider,
                 mock(DarkIconDispatcher.class));
@@ -124,14 +124,14 @@
                 LinearLayout group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider contextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(group,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     contextProvider,
                     darkIconDispatcher);
@@ -172,7 +172,7 @@
             super(group,
                     StatusBarLocation.HOME,
                     mock(StatusBarPipelineFlags.class),
-                    mock(WifiViewModel.class),
+                    mock(WifiUiAdapter.class),
                     mock(MobileUiAdapter.class),
                     contextProvider);
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 8da8d04..9f70565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -56,8 +56,8 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.data.BouncerView;
 import com.android.systemui.keyguard.data.BouncerViewDelegate;
-import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
-import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
@@ -105,8 +105,8 @@
     @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory;
     @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory;
     @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController;
-    @Mock private KeyguardBouncer mBouncer;
-    @Mock private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor;
+    @Mock private KeyguardBouncer mPrimaryBouncer;
+    @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer;
     @Mock private KeyguardMessageArea mKeyguardMessageArea;
     @Mock private ShadeController mShadeController;
     @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
@@ -114,14 +114,13 @@
     @Mock private LatencyTracker mLatencyTracker;
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
-    @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor;
-    @Mock private BouncerInteractor mBouncerInteractor;
+    @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
+    @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     @Mock private BouncerView mBouncerView;
-//    @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference;
     @Mock private BouncerViewDelegate mBouncerViewDelegate;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
+    private KeyguardBouncer.PrimaryBouncerExpansionCallback mBouncerExpansionCallback;
     private FakeKeyguardStateController mKeyguardStateController =
             spy(new FakeKeyguardStateController());
 
@@ -135,8 +134,9 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mKeyguardBouncerFactory.create(
-                        any(ViewGroup.class), any(KeyguardBouncer.BouncerExpansionCallback.class)))
-                .thenReturn(mBouncer);
+                any(ViewGroup.class),
+                any(KeyguardBouncer.PrimaryBouncerExpansionCallback.class)))
+                .thenReturn(mPrimaryBouncer);
         when(mCentralSurfaces.getBouncerContainer()).thenReturn(mContainer);
         when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
         when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
@@ -164,8 +164,8 @@
                         mLatencyTracker,
                         mKeyguardSecurityModel,
                         mFeatureFlags,
-                        mBouncerCallbackInteractor,
-                        mBouncerInteractor,
+                        mPrimaryBouncerCallbackInteractor,
+                        mPrimaryBouncerInteractor,
                         mBouncerView) {
                     @Override
                     public ViewRootImpl getViewRootImpl() {
@@ -182,8 +182,8 @@
                 mNotificationContainer,
                 mBypassController);
         mStatusBarKeyguardViewManager.show(null);
-        ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback> callbackArgumentCaptor =
-                ArgumentCaptor.forClass(KeyguardBouncer.BouncerExpansionCallback.class);
+        ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> callbackArgumentCaptor =
+                ArgumentCaptor.forClass(KeyguardBouncer.PrimaryBouncerExpansionCallback.class);
         verify(mKeyguardBouncerFactory).create(any(ViewGroup.class),
                 callbackArgumentCaptor.capture());
         mBouncerExpansionCallback = callbackArgumentCaptor.getValue();
@@ -195,86 +195,86 @@
         Runnable cancelAction = () -> {};
         mStatusBarKeyguardViewManager.dismissWithAction(
                 action, cancelAction, false /* afterKeyguardGone */);
-        verify(mBouncer).showWithDismissAction(eq(action), eq(cancelAction));
+        verify(mPrimaryBouncer).showWithDismissAction(eq(action), eq(cancelAction));
     }
 
     @Test
     public void showBouncer_onlyWhenShowing() {
         mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
-        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
-        verify(mBouncer, never()).show(anyBoolean(), anyBoolean());
-        verify(mBouncer, never()).show(anyBoolean());
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+        verify(mPrimaryBouncer, never()).show(anyBoolean());
     }
 
     @Test
     public void showBouncer_notWhenBouncerAlreadyShowing() {
         mStatusBarKeyguardViewManager.hide(0 /* startTime */, 0 /* fadeoutDuration */);
-        when(mBouncer.isSecure()).thenReturn(true);
-        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
-        verify(mBouncer, never()).show(anyBoolean(), anyBoolean());
-        verify(mBouncer, never()).show(anyBoolean());
+        when(mPrimaryBouncer.isSecure()).thenReturn(true);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
+        verify(mPrimaryBouncer, never()).show(anyBoolean());
     }
 
     @Test
     public void showBouncer_showsTheBouncer() {
-        mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
-        verify(mBouncer).show(anyBoolean(), eq(true));
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+        verify(mPrimaryBouncer).show(anyBoolean(), eq(true));
     }
 
     @Test
-    public void onPanelExpansionChanged_neverHidesScrimmedBouncer() {
-        when(mBouncer.isShowing()).thenReturn(true);
-        when(mBouncer.isScrimmed()).thenReturn(true);
+    public void onPanelExpansionChanged_neverHidesFullscreenBouncer() {
+        when(mPrimaryBouncer.isShowing()).thenReturn(true);
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+                KeyguardSecurityModel.SecurityMode.SimPuk);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
+        verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
+
+        reset(mPrimaryBouncer);
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+                KeyguardSecurityModel.SecurityMode.SimPin);
+        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
+        verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_VISIBLE));
     }
 
     @Test
     public void onPanelExpansionChanged_neverShowsDuringHintAnimation() {
         when(mNotificationPanelView.isUnlockHintRunning()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+        verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
     }
 
     @Test
     public void onPanelExpansionChanged_propagatesToBouncer() {
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer).setExpansion(eq(0.5f));
+        verify(mPrimaryBouncer).setExpansion(eq(0.5f));
     }
 
     @Test
     public void onPanelExpansionChanged_hideBouncer_afterKeyguardHidden() {
         mStatusBarKeyguardViewManager.hide(0, 0);
-        when(mBouncer.inTransit()).thenReturn(true);
+        when(mPrimaryBouncer.inTransit()).thenReturn(true);
 
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
+        verify(mPrimaryBouncer).setExpansion(eq(KeyguardBouncer.EXPANSION_HIDDEN));
     }
 
     @Test
     public void onPanelExpansionChanged_showsBouncerWhenSwiping() {
         mKeyguardStateController.setCanDismissLockScreen(false);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer).show(eq(false), eq(false));
+        verify(mPrimaryBouncer).show(eq(false), eq(false));
 
         // But not when it's already visible
-        reset(mBouncer);
-        when(mBouncer.isShowing()).thenReturn(true);
+        reset(mPrimaryBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer, never()).show(eq(false), eq(false));
+        verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
 
         // Or animating away
-        reset(mBouncer);
-        when(mBouncer.isAnimatingAway()).thenReturn(true);
+        reset(mPrimaryBouncer);
+        when(mPrimaryBouncer.isAnimatingAway()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer, never()).show(eq(false), eq(false));
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenOccluded() {
-        mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animate */);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(EXPANSION_EVENT);
-        verify(mBouncer, never()).setExpansion(eq(0.5f));
+        verify(mPrimaryBouncer, never()).show(eq(false), eq(false));
     }
 
     @Test
@@ -286,7 +286,7 @@
                         /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
-        verify(mBouncer, never()).setExpansion(anyFloat());
+        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
     }
 
     @Test
@@ -303,18 +303,7 @@
                         /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
-        verify(mBouncer, never()).setExpansion(anyFloat());
-    }
-
-    @Test
-    public void onPanelExpansionChanged_neverTranslatesBouncerWhenLaunchingApp() {
-        when(mCentralSurfaces.isInLaunchTransition()).thenReturn(true);
-        mStatusBarKeyguardViewManager.onPanelExpansionChanged(
-                expansionEvent(
-                        /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
-                        /* expanded= */ true,
-                        /* tracking= */ false));
-        verify(mBouncer, never()).setExpansion(anyFloat());
+        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
     }
 
     @Test
@@ -325,7 +314,7 @@
                         /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
                         /* expanded= */ true,
                         /* tracking= */ false));
-        verify(mBouncer, never()).setExpansion(anyFloat());
+        verify(mPrimaryBouncer, never()).setExpansion(anyFloat());
     }
 
     @Test
@@ -333,7 +322,7 @@
         mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
         verify(mCentralSurfaces).animateKeyguardUnoccluding();
 
-        when(mBouncer.isShowing()).thenReturn(true);
+        when(mPrimaryBouncer.isShowing()).thenReturn(true);
         clearInvocations(mCentralSurfaces);
         mStatusBarKeyguardViewManager.setOccluded(false /* occluded */, true /* animated */);
         verify(mCentralSurfaces, never()).animateKeyguardUnoccluding();
@@ -362,7 +351,6 @@
 
     @Test
     public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() {
-        when(mCentralSurfaces.isInLaunchTransition()).thenReturn(true);
         mStatusBarKeyguardViewManager.show(null);
 
         mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
@@ -385,7 +373,7 @@
         mStatusBarKeyguardViewManager.dismissWithAction(
                 action, cancelAction, true /* afterKeyguardGone */);
 
-        when(mBouncer.isShowing()).thenReturn(false);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
         mStatusBarKeyguardViewManager.hideBouncer(true);
         mStatusBarKeyguardViewManager.hide(0, 30);
         verify(action, never()).onDismiss();
@@ -399,7 +387,7 @@
         mStatusBarKeyguardViewManager.dismissWithAction(
                 action, cancelAction, true /* afterKeyguardGone */);
 
-        when(mBouncer.isShowing()).thenReturn(false);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
         mStatusBarKeyguardViewManager.hideBouncer(true);
 
         verify(action, never()).onDismiss();
@@ -420,9 +408,9 @@
 
     @Test
     public void testShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        when(mBouncer.isShowing()).thenReturn(false);
-        when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
+        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
+        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
         assertTrue(
                 "Is showing not accurate when alternative auth showing",
                 mStatusBarKeyguardViewManager.isBouncerShowing());
@@ -430,93 +418,93 @@
 
     @Test
     public void testWillBeShowing_whenAlternateAuthShowing() {
-        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        when(mBouncer.isShowing()).thenReturn(false);
-        when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
+        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
+        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
         assertTrue(
                 "Is or will be showing not accurate when alternative auth showing",
-                mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing());
+                mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
     }
 
     @Test
-    public void testHideAltAuth_onShowBouncer() {
+    public void testHideAlternateBouncer_onShowBouncer() {
         // GIVEN alt auth is showing
-        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        when(mBouncer.isShowing()).thenReturn(false);
-        when(mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()).thenReturn(true);
-        reset(mAlternateAuthInterceptor);
+        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
+        when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true);
+        reset(mAlternateBouncer);
 
         // WHEN showBouncer is called
-        mStatusBarKeyguardViewManager.showBouncer(true);
+        mStatusBarKeyguardViewManager.showPrimaryBouncer(true);
 
         // THEN alt bouncer should be hidden
-        verify(mAlternateAuthInterceptor).hideAlternateAuthBouncer();
+        verify(mAlternateBouncer).hideAlternateBouncer();
     }
 
     @Test
     public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() {
-        when(mBouncer.isShowing()).thenReturn(false);
-        when(mBouncer.inTransit()).thenReturn(true);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
+        when(mPrimaryBouncer.inTransit()).thenReturn(true);
 
         assertTrue(
                 "Is or will be showing should be true when bouncer is in transit",
-                mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing());
+                mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing());
     }
 
     @Test
     public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
         // GIVEN alt auth exists, unlocking with biometric isn't allowed
-        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        when(mBouncer.isShowing()).thenReturn(false);
+        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
                 .thenReturn(false);
 
         // WHEN showGenericBouncer is called
         final boolean scrimmed = true;
-        mStatusBarKeyguardViewManager.showGenericBouncer(scrimmed);
+        mStatusBarKeyguardViewManager.showBouncer(scrimmed);
 
         // THEN regular bouncer is shown
-        verify(mBouncer).show(anyBoolean(), eq(scrimmed));
-        verify(mAlternateAuthInterceptor, never()).showAlternateAuthBouncer();
+        verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed));
+        verify(mAlternateBouncer, never()).showAlternateBouncer();
     }
 
     @Test
-    public void testShowAltAuth_unlockingWithBiometricAllowed() {
+    public void testShowAlternateBouncer_unlockingWithBiometricAllowed() {
         // GIVEN alt auth exists, unlocking with biometric is allowed
-        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        when(mBouncer.isShowing()).thenReturn(false);
+        mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer);
+        when(mPrimaryBouncer.isShowing()).thenReturn(false);
         when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
 
         // WHEN showGenericBouncer is called
-        mStatusBarKeyguardViewManager.showGenericBouncer(true);
+        mStatusBarKeyguardViewManager.showBouncer(true);
 
         // THEN alt auth bouncer is shown
-        verify(mAlternateAuthInterceptor).showAlternateAuthBouncer();
-        verify(mBouncer, never()).show(anyBoolean(), anyBoolean());
+        verify(mAlternateBouncer).showAlternateBouncer();
+        verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean());
     }
 
     @Test
     public void testUpdateResources_delegatesToBouncer() {
         mStatusBarKeyguardViewManager.updateResources();
 
-        verify(mBouncer).updateResources();
+        verify(mPrimaryBouncer).updateResources();
     }
 
     @Test
     public void updateKeyguardPosition_delegatesToBouncer() {
         mStatusBarKeyguardViewManager.updateKeyguardPosition(1.0f);
 
-        verify(mBouncer).updateKeyguardPosition(1.0f);
+        verify(mPrimaryBouncer).updateKeyguardPosition(1.0f);
     }
 
     @Test
     public void testIsBouncerInTransit() {
-        when(mBouncer.inTransit()).thenReturn(true);
-        Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isTrue();
-        when(mBouncer.inTransit()).thenReturn(false);
-        Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isFalse();
-        mBouncer = null;
-        Truth.assertThat(mStatusBarKeyguardViewManager.isBouncerInTransit()).isFalse();
+        when(mPrimaryBouncer.inTransit()).thenReturn(true);
+        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isTrue();
+        when(mPrimaryBouncer.inTransit()).thenReturn(false);
+        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
+        mPrimaryBouncer = null;
+        Truth.assertThat(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).isFalse();
     }
 
     private static ShadeExpansionChangeEvent expansionEvent(
@@ -547,7 +535,7 @@
                 eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
                 mOnBackInvokedCallback.capture());
 
-        when(mBouncer.isShowing()).thenReturn(true);
+        when(mPrimaryBouncer.isShowing()).thenReturn(true);
         when(mCentralSurfaces.shouldKeyguardHideImmediately()).thenReturn(true);
         /* invoke the back callback directly */
         mOnBackInvokedCallback.getValue().onBackInvoked();
@@ -580,6 +568,6 @@
     public void flag_off_DoesNotCallBouncerInteractor() {
         when(mFeatureFlags.isEnabled(MODERN_BOUNCER)).thenReturn(false);
         mStatusBarKeyguardViewManager.hideBouncer(false);
-        verify(mBouncerInteractor, never()).hide();
+        verify(mPrimaryBouncerInteractor, never()).hide();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index c409857..ce54d78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -67,8 +67,8 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -123,8 +123,6 @@
     @Mock
     private ShadeControllerImpl mShadeController;
     @Mock
-    private NotifPipeline mNotifPipeline;
-    @Mock
     private NotificationVisibilityProvider mVisibilityProvider;
     @Mock
     private ActivityIntentHelper mActivityIntentHelper;
@@ -197,7 +195,6 @@
                         getContext(),
                         mHandler,
                         mUiBgExecutor,
-                        mNotifPipeline,
                         mVisibilityProvider,
                         headsUpManager,
                         mActivityStarter,
@@ -222,7 +219,8 @@
                         mock(NotificationPresenter.class),
                         mock(NotificationPanelViewController.class),
                         mActivityLaunchAnimator,
-                        notificationAnimationProvider
+                        notificationAnimationProvider,
+                        mock(LaunchFullScreenIntentProvider.class)
                 );
 
         // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
@@ -384,11 +382,9 @@
         NotificationEntry entry = mock(NotificationEntry.class);
         when(entry.getImportance()).thenReturn(NotificationManager.IMPORTANCE_HIGH);
         when(entry.getSbn()).thenReturn(sbn);
-        when(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(eq(entry)))
-                .thenReturn(true);
 
         // WHEN
-        mNotificationActivityStarter.handleFullScreenIntent(entry);
+        mNotificationActivityStarter.launchFullScreenIntent(entry);
 
         // THEN display should try wake up for the full screen intent
         verify(mCentralSurfaces).wakeUpForFullScreenIntent();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index c3a7e65..613238f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -99,6 +99,6 @@
         mRemoteInputCallback.onLockedRemoteInput(
                 mock(ExpandableNotificationRow.class), mock(View.class));
 
-        verify(mStatusBarKeyguardViewManager).showGenericBouncer(true);
+        verify(mStatusBarKeyguardViewManager).showBouncer(true);
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
index fa7b259..4d79a53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
@@ -3,19 +3,14 @@
 import android.graphics.Rect
 import android.testing.AndroidTestingRunner
 import android.view.Display
-import android.view.InsetsVisibilities
+import android.view.WindowInsets
 import android.view.WindowInsetsController
-import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
-import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
-import android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS
-import android.view.WindowInsetsController.Appearance
+import android.view.WindowInsetsController.*
 import androidx.test.filters.SmallTest
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import org.junit.Before
 import org.junit.Test
@@ -29,8 +24,8 @@
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -40,7 +35,6 @@
     @Mock private lateinit var lightBarController: LightBarController
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
     @Mock private lateinit var letterboxAppearanceCalculator: LetterboxAppearanceCalculator
-    @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var centralSurfaces: CentralSurfaces
 
     private lateinit var sysBarAttrsListener: SystemBarAttributesListener
@@ -57,7 +51,6 @@
         sysBarAttrsListener =
             SystemBarAttributesListener(
                 centralSurfaces,
-                featureFlags,
                 letterboxAppearanceCalculator,
                 statusBarStateController,
                 lightBarController,
@@ -74,18 +67,14 @@
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToCentralSurfaces() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToCentralSurfaces() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
 
         verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance)
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() {
         changeSysBarAttrs(TEST_APPEARANCE, arrayOf<LetterboxDetails>())
 
         verify(centralSurfaces).setAppearance(TEST_APPEARANCE)
@@ -96,18 +85,16 @@
         changeSysBarAttrs(TEST_APPEARANCE)
 
         verify(statusBarStateController)
-            .setSystemBarAttributes(eq(TEST_APPEARANCE), anyInt(), any(), any())
+            .setSystemBarAttributes(eq(TEST_APPEARANCE), anyInt(), anyInt(), any())
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToStatusBarStateCtrl() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToStatusBarStateCtrl() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
 
         verify(statusBarStateController)
             .setSystemBarAttributes(
-                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), any(), any())
+                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), anyInt(), any())
     }
 
     @Test
@@ -120,9 +107,7 @@
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToLightBarController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToLightBarController() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
 
         verify(lightBarController)
@@ -135,7 +120,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToStatusBarStateController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -143,12 +127,11 @@
 
         verify(statusBarStateController)
             .setSystemBarAttributes(
-                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), any(), any())
+                eq(TEST_LETTERBOX_APPEARANCE.appearance), anyInt(), anyInt(), any())
     }
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToLightBarController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -164,7 +147,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToCentralSurfaces() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -175,7 +157,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_previousCallEmptyLetterbox_doesNothing() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf())
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -184,17 +165,6 @@
         verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
     }
 
-    @Test
-    fun onStatusBarBoundsChanged_flagFalse_doesNothing() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(false)
-        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
-        reset(centralSurfaces, lightBarController, statusBarStateController)
-
-        sysBarAttrsListener.onStatusBarBoundsChanged()
-
-        verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
-    }
-
     private fun changeSysBarAttrs(@Appearance appearance: Int) {
         changeSysBarAttrs(appearance, arrayOf<LetterboxDetails>())
     }
@@ -224,7 +194,7 @@
             appearanceRegions,
             /* navbarColorManagedByIme= */ false,
             WindowInsetsController.BEHAVIOR_DEFAULT,
-            InsetsVisibilities(),
+            WindowInsets.Type.defaultVisible(),
             "package name",
             letterboxDetails)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 65e2964..3a0a94d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 3a006ad..36e76f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -49,9 +49,9 @@
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
deleted file mode 100644
index bf43238..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
+++ /dev/null
@@ -1,103 +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.statusbar.phone.userswitcher
-
-import android.content.Intent
-import android.os.UserHandle
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.user.UserSwitchDialogController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-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.Mockito.`when`
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-@SmallTest
-class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() {
-    @Mock
-    private lateinit var tracker: StatusBarUserInfoTracker
-
-    @Mock
-    private lateinit var featureController: StatusBarUserSwitcherFeatureController
-
-    @Mock
-    private lateinit var userSwitcherDialogController: UserSwitchDialogController
-
-    @Mock
-    private lateinit var featureFlags: FeatureFlags
-
-    @Mock
-    private lateinit var activityStarter: ActivityStarter
-
-    @Mock
-    private lateinit var falsingManager: FalsingManager
-
-    private lateinit var statusBarUserSwitcherContainer: StatusBarUserSwitcherContainer
-    private lateinit var controller: StatusBarUserSwitcherControllerImpl
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        statusBarUserSwitcherContainer = StatusBarUserSwitcherContainer(mContext, null)
-        statusBarUserSwitcherContainer
-        controller = StatusBarUserSwitcherControllerImpl(
-                statusBarUserSwitcherContainer,
-                tracker,
-                featureController,
-                userSwitcherDialogController,
-                featureFlags,
-                activityStarter,
-                falsingManager
-        )
-        controller.init()
-        controller.onViewAttached()
-    }
-
-    @Test
-    fun testFalsingManager() {
-        statusBarUserSwitcherContainer.callOnClick()
-        verify(falsingManager).isFalseTap(FalsingManager.LOW_PENALTY)
-    }
-
-    @Test
-    fun testStartActivity() {
-        `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false)
-        statusBarUserSwitcherContainer.callOnClick()
-        verify(userSwitcherDialogController).showDialog(any(View::class.java))
-        `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true)
-        statusBarUserSwitcherContainer.callOnClick()
-        verify(activityStarter).startActivity(any(Intent::class.java),
-                eq(true) /* dismissShade */,
-                eq(null) /* animationController */,
-                eq(true) /* showOverLockscreenWhenLocked */,
-                eq(UserHandle.SYSTEM))
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt
new file mode 100644
index 0000000..b7a6c01
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.airplane.data.repository
+
+import android.os.Handler
+import android.os.Looper
+import android.os.UserHandle
+import android.provider.Settings.Global
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class AirplaneModeRepositoryImplTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeRepositoryImpl
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    private lateinit var bgHandler: Handler
+    private lateinit var scope: CoroutineScope
+    private lateinit var settings: FakeSettings
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        bgHandler = Handler(Looper.getMainLooper())
+        scope = CoroutineScope(IMMEDIATE)
+        settings = FakeSettings()
+        settings.userId = UserHandle.USER_ALL
+
+        underTest =
+            AirplaneModeRepositoryImpl(
+                bgHandler,
+                settings,
+                logger,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun isAirplaneMode_initiallyGetsSettingsValue() =
+        runBlocking(IMMEDIATE) {
+            settings.putInt(Global.AIRPLANE_MODE_ON, 1)
+
+            underTest =
+                AirplaneModeRepositoryImpl(
+                    bgHandler,
+                    settings,
+                    logger,
+                    scope,
+                )
+
+            val job = underTest.isAirplaneMode.launchIn(this)
+
+            assertThat(underTest.isAirplaneMode.value).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneMode_settingUpdated_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isAirplaneMode.launchIn(this)
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 0)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isFalse()
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 1)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isTrue()
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 0)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isFalse()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt
new file mode 100644
index 0000000..63bbdfc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.airplane.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeAirplaneModeRepository : AirplaneModeRepository {
+    private val _isAirplaneMode = MutableStateFlow(false)
+    override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode
+
+    fun setIsAirplaneMode(isAirplaneMode: Boolean) {
+        _isAirplaneMode.value = isAirplaneMode
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt
new file mode 100644
index 0000000..33a80e1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.airplane.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+class AirplaneModeInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeInteractor
+
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var connectivityRepository: FakeConnectivityRepository
+
+    @Before
+    fun setUp() {
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        connectivityRepository = FakeConnectivityRepository()
+        underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository)
+    }
+
+    @Test
+    fun isAirplaneMode_matchesRepo() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            yield()
+            assertThat(latest).isTrue()
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            yield()
+            assertThat(latest).isFalse()
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            yield()
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isForceHidden_repoHasWifiHidden_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt
new file mode 100644
index 0000000..76016a1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.airplane.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+class AirplaneModeViewModelTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeViewModel
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var connectivityRepository: FakeConnectivityRepository
+    private lateinit var interactor: AirplaneModeInteractor
+    private lateinit var scope: CoroutineScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        connectivityRepository = FakeConnectivityRepository()
+        interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository)
+        scope = CoroutineScope(IMMEDIATE)
+
+        underTest =
+            AirplaneModeViewModel(
+                interactor,
+                logger,
+                scope,
+            )
+    }
+
+    @Test
+    fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneModeIconVisible_forceHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
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
new file mode 100644
index 0000000..288f54c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.repository
+
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileConnectionRepository : MobileConnectionRepository {
+    private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
+    override val subscriptionModelFlow = _subscriptionsModelFlow
+
+    private val _dataEnabled = MutableStateFlow(true)
+    override val dataEnabled = _dataEnabled
+
+    private val _isDefaultDataSubscription = MutableStateFlow(true)
+    override val isDefaultDataSubscription = _isDefaultDataSubscription
+
+    fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
+        _subscriptionsModelFlow.value = model
+    }
+
+    fun setDataEnabled(enabled: Boolean) {
+        _dataEnabled.value = enabled
+    }
+
+    fun setIsDefaultDataSubscription(isDefault: Boolean) {
+        _isDefaultDataSubscription.value = isDefault
+    }
+}
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
new file mode 100644
index 0000000..533d5d9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileConnectionsRepository : MobileConnectionsRepository {
+    private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
+
+    private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+    override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
+
+    private val _defaultDataSubRatConfig = MutableStateFlow(Config())
+    override val defaultDataSubRatConfig = _defaultDataSubRatConfig
+
+    private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+    override val defaultDataSubId = _defaultDataSubId
+
+    private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
+    override val defaultMobileNetworkConnectivity = _mobileConnectivity
+
+    private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
+    }
+
+    private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
+    override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
+
+    fun setSubscriptions(subs: List<SubscriptionInfo>) {
+        _subscriptionsFlow.value = subs
+    }
+
+    fun setDefaultDataSubRatConfig(config: Config) {
+        _defaultDataSubRatConfig.value = config
+    }
+
+    fun setDefaultDataSubId(id: Int) {
+        _defaultDataSubId.value = id
+    }
+
+    fun setMobileConnectivity(model: MobileConnectivityModel) {
+        _mobileConnectivity.value = model
+    }
+
+    suspend fun triggerGlobalMobileDataSettingChangedEvent() {
+        _globalMobileDataSettingChangedEvent.emit(Unit)
+    }
+
+    fun setActiveMobileDataSubscriptionId(subId: Int) {
+        _activeMobileDataSubscriptionId.value = subId
+    }
+
+    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/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
deleted file mode 100644
index 0d15268..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
+++ /dev/null
@@ -1,51 +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.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
-    private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
-    override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
-
-    private val _activeMobileDataSubscriptionId =
-        MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
-    override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
-
-    private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
-    override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
-        return subIdFlows[subId]
-            ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
-    }
-
-    fun setSubscriptions(subs: List<SubscriptionInfo>) {
-        _subscriptionsFlow.value = subs
-    }
-
-    fun setActiveMobileDataSubscriptionId(subId: Int) {
-        _activeMobileDataSubscriptionId.value = subId
-    }
-
-    fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
-        val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
-        subscription.value = model
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
index 6c495c5..141b50c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -16,13 +16,12 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Defaults to `true` */
 class FakeUserSetupRepository : UserSetupRepository {
     private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
-    override val isUserSetupFlow: Flow<Boolean> = _isUserSetup
+    override val isUserSetupFlow = _isUserSetup
 
     fun setUserSetup(setup: Boolean) {
         _isUserSetup.value = setup
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
new file mode 100644
index 0000000..5ce51bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -0,0 +1,414 @@
+/*
+ * 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.repository
+
+import android.os.UserHandle
+import android.provider.Settings
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+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
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionRepositoryImpl
+
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    private val scope = CoroutineScope(IMMEDIATE)
+    private val globalSettings = FakeSettings()
+    private val connectionsRepo = FakeMobileConnectionsRepository()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        globalSettings.userId = UserHandle.USER_ALL
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+        underTest =
+            MobileConnectionRepositoryImpl(
+                context,
+                SUB_1_ID,
+                telephonyManager,
+                globalSettings,
+                connectionsRepo.defaultDataSubId,
+                connectionsRepo.globalMobileDataSettingChangedEvent,
+                IMMEDIATE,
+                logger,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testFlowForSubId_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly_toggles() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<ServiceStateListener>()
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+            callback.onServiceStateChanged(serviceState)
+            serviceState.isEmergencyOnly = false
+            callback.onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_signalStrengths_levelsUpdate() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+            val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+            callback.onSignalStrengthsChanged(strength)
+
+            assertThat(latest?.isGsm).isEqualTo(true)
+            assertThat(latest?.primaryLevel).isEqualTo(1)
+            assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    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(DATA_CONNECTED, 200 /* unused */)
+
+            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()
+        }
+
+    @Test
+    fun testFlowForSubId_dataActivity() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
+            callback.onDataActivity(3)
+
+            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_carrierNetworkChange() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
+            callback.onCarrierNetworkChange(true)
+
+            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val type = NETWORK_TYPE_UNKNOWN
+            val expected = DefaultNetworkType(type)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = NETWORK_TYPE_LTE
+            val expected = DefaultNetworkType(type)
+            val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingOverride() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+            val expected = OverrideNetworkType(type)
+            val ti =
+                mock<TelephonyDisplayInfo>().also {
+                    whenever(it.overrideNetworkType).thenReturn(type)
+                }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataEnabled_initial_false() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+
+            assertThat(underTest.dataEnabled.value).isFalse()
+        }
+
+    @Test
+    fun dataEnabled_isEnabled_true() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+            val job = underTest.dataEnabled.launchIn(this)
+
+            assertThat(underTest.dataEnabled.value).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataEnabled_isDisabled() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+            val job = underTest.dataEnabled.launchIn(this)
+
+            assertThat(underTest.dataEnabled.value).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isDefaultDataSubscription_isDefault() =
+        runBlocking(IMMEDIATE) {
+            connectionsRepo.setDefaultDataSubId(SUB_1_ID)
+
+            var latest: Boolean? = null
+            val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isDefaultDataSubscription_isNotDefault() =
+        runBlocking(IMMEDIATE) {
+            // Our subId is SUB_1_ID
+            connectionsRepo.setDefaultDataSubId(123)
+
+            var latest: Boolean? = null
+            val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID"
+
+            var latest: Boolean? = null
+            val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+            // We don't read the setting directly, we query telephony when changes happen
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+            globalSettings.putInt(subIdSettingName, 0)
+            assertThat(latest).isFalse()
+
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+            globalSettings.putInt(subIdSettingName, 1)
+            assertThat(latest).isTrue()
+
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+            globalSettings.putInt(subIdSettingName, 0)
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    /** Convenience constructor for SignalStrength */
+    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+        val signalStrength = mock<SignalStrength>()
+        whenever(signalStrength.isGsm).thenReturn(isGsm)
+        whenever(signalStrength.level).thenReturn(gsmLevel)
+        val cdmaStrength =
+            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+            .thenReturn(listOf(cdmaStrength))
+
+        return signalStrength
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..a953a3d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -0,0 +1,382 @@
+/*
+ * 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.repository
+
+import android.content.Intent
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.internal.telephony.PhoneConstants
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionsRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionsRepositoryImpl
+
+    @Mock private lateinit var connectivityManager: ConnectivityManager
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    private val scope = CoroutineScope(IMMEDIATE)
+    private val globalSettings = FakeSettings()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            MobileConnectionsRepositoryImpl(
+                connectivityManager,
+                subscriptionManager,
+                telephonyManager,
+                logger,
+                fakeBroadcastDispatcher,
+                globalSettings,
+                context,
+                IMMEDIATE,
+                scope,
+                mock(),
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testSubscriptions_initiallyEmpty() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+        }
+
+    @Test
+    fun testSubscriptions_listUpdates() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_removingSub_updatesList() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // WHEN 2 networks show up
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN one network is removed
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // THEN the subscriptions list represents the newest change
+            assertThat(latest).isEqualTo(listOf(SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value)
+                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_updates() =
+        runBlocking(IMMEDIATE) {
+            var active: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(active).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_validSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_1_ID)
+
+            assertThat(repo1).isSameInstanceAs(repo2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionCache_clearsInvalidSubscriptions() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // Get repos to trigger caching
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+
+            assertThat(underTest.getSubIdRepoCache())
+                .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
+
+            // SUB_2 disappears
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_invalidSubId_throws() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            assertThrows(IllegalArgumentException::class.java) {
+                underTest.getRepoForSubId(SUB_1_ID)
+            }
+
+            job.cancel()
+        }
+
+    @Test
+    fun testDefaultDataSubId_updatesOnBroadcast() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+                )
+            }
+
+            assertThat(latest).isEqualTo(SUB_2_ID)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(
+                    context,
+                    Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+                        .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+                )
+            }
+
+            assertThat(latest).isEqualTo(SUB_1_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun mobileConnectivity_default() {
+        assertThat(underTest.defaultMobileNetworkConnectivity.value)
+            .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
+    }
+
+    @Test
+    fun mobileConnectivity_isConnected_isValidated() =
+        runBlocking(IMMEDIATE) {
+            val caps = createCapabilities(connected = true, validated = true)
+
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+            assertThat(latest)
+                .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true))
+
+            job.cancel()
+        }
+
+    @Test
+    fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() =
+        runBlocking(IMMEDIATE) {
+            var produced = false
+            val job =
+                underTest.globalMobileDataSettingChangedEvent
+                    .onEach { produced = true }
+                    .launchIn(this)
+
+            assertThat(produced).isFalse()
+
+            globalSettings.putInt(Settings.Global.MOBILE_DATA, 0)
+
+            assertThat(produced).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun mobileConnectivity_isConnected_isNotValidated() =
+        runBlocking(IMMEDIATE) {
+            val caps = createCapabilities(connected = true, validated = false)
+
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+            assertThat(latest)
+                .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false))
+
+            job.cancel()
+        }
+
+    @Test
+    fun mobileConnectivity_isNotConnected_isNotValidated() =
+        runBlocking(IMMEDIATE) {
+            val caps = createCapabilities(connected = false, validated = false)
+
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+            assertThat(latest)
+                .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
+
+            job.cancel()
+        }
+
+    /** In practice, I don't think this state can ever happen (!connected, validated) */
+    @Test
+    fun mobileConnectivity_isNotConnected_isValidated() =
+        runBlocking(IMMEDIATE) {
+            val caps = createCapabilities(connected = false, validated = true)
+
+            var latest: MobileConnectivityModel? = null
+            val job =
+                underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+            getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+            assertThat(latest).isEqualTo(MobileConnectivityModel(false, true))
+
+            job.cancel()
+        }
+
+    private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
+        mock<NetworkCapabilities>().also {
+            whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
+            whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated)
+        }
+
+    private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+        verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+
+        private const val NET_ID = 123
+        private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
deleted file mode 100644
index 316b795..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
+++ /dev/null
@@ -1,360 +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.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-class MobileSubscriptionRepositoryTest : SysuiTestCase() {
-    private lateinit var underTest: MobileSubscriptionRepositoryImpl
-
-    @Mock private lateinit var subscriptionManager: SubscriptionManager
-    @Mock private lateinit var telephonyManager: TelephonyManager
-    private val scope = CoroutineScope(IMMEDIATE)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            MobileSubscriptionRepositoryImpl(
-                subscriptionManager,
-                telephonyManager,
-                IMMEDIATE,
-                scope,
-            )
-    }
-
-    @After
-    fun tearDown() {
-        scope.cancel()
-    }
-
-    @Test
-    fun testSubscriptions_initiallyEmpty() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
-        }
-
-    @Test
-    fun testSubscriptions_listUpdates() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testSubscriptions_removingSub_updatesList() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            // WHEN 2 networks show up
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // WHEN one network is removed
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // THEN the subscriptions list represents the newest change
-            assertThat(latest).isEqualTo(listOf(SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.activeMobileDataSubscriptionId.value)
-                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_updates() =
-        runBlocking(IMMEDIATE) {
-            var active: Int? = null
-
-            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
-
-            getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
-            assertThat(active).isEqualTo(SUB_2_ID)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_default() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isEqualTo(MobileSubscriptionModel())
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-
-            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly_toggles() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<ServiceStateListener>()
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-            callback.onServiceStateChanged(serviceState)
-            serviceState.isEmergencyOnly = false
-            callback.onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_signalStrengths_levelsUpdate() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
-            val strength = signalStrength(1, 2, true)
-            callback.onSignalStrengthsChanged(strength)
-
-            assertThat(latest?.isGsm).isEqualTo(true)
-            assertThat(latest?.primaryLevel).isEqualTo(1)
-            assertThat(latest?.cdmaLevel).isEqualTo(2)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataConnectionState() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
-            callback.onDataConnectionStateChanged(100, 200 /* unused */)
-
-            assertThat(latest?.dataConnectionState).isEqualTo(100)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataActivity() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataActivityListener>()
-            callback.onDataActivity(3)
-
-            assertThat(latest?.dataActivityDirection).isEqualTo(3)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_carrierNetworkChange() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
-            callback.onCarrierNetworkChange(true)
-
-            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_displayInfo() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DisplayInfoListener>()
-            val ti = mock<TelephonyDisplayInfo>()
-            callback.onDisplayInfoChanged(ti)
-
-            assertThat(latest?.displayInfo).isEqualTo(ti)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_isCached() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            val state1 = underTest.getFlowForSubId(SUB_1_ID)
-            val state2 = underTest.getFlowForSubId(SUB_1_ID)
-
-            assertThat(state1).isEqualTo(state2)
-        }
-
-    @Test
-    fun testFlowForSubId_isRemovedAfterFinish() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-
-            // Start collecting on some flow
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            // There should be once cached flow now
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
-
-            // When the job is canceled, the cache should be cleared
-            job.cancel()
-
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
-        }
-
-    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
-        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
-        verify(subscriptionManager)
-            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
-        return callbackCaptor.value!!
-    }
-
-    private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
-        getTelephonyCallbackForType()
-
-    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
-        val callbackCaptor = argumentCaptor<TelephonyCallback>()
-        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
-        return callbackCaptor.allValues
-    }
-
-    private inline fun <reified T> getTelephonyCallbackForType(): T {
-        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
-        assertThat(cbs.size).isEqualTo(1)
-        return cbs[0]
-    }
-
-    /** Convenience constructor for SignalStrength */
-    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
-        val signalStrength = mock<SignalStrength>()
-        whenever(signalStrength.isGsm).thenReturn(isGsm)
-        whenever(signalStrength.level).thenReturn(gsmLevel)
-        val cdmaStrength =
-            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
-        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
-            .thenReturn(listOf(cdmaStrength))
-
-        return signalStrength
-    }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private const val SUB_1_ID = 1
-        private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
-    }
-}
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 8ec68f3..3ae7d3c 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,21 +22,29 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeMobileIconInteractor : MobileIconInteractor {
-    private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
-    override val iconGroup = _iconGroup
+    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 _isFailedConnection = MutableStateFlow(false)
+    override val isDefaultConnectionFailed = _isFailedConnection
+
+    override val isDataConnected = MutableStateFlow(true)
+
+    private val _isDataEnabled = MutableStateFlow(true)
+    override val isDataEnabled = _isDataEnabled
+
+    private val _isDefaultDataEnabled = MutableStateFlow(true)
+    override val isDefaultDataEnabled = _isDefaultDataEnabled
+
+    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)
-    override val cutOut = _cutOut
-
     fun setIconGroup(group: SignalIcon.MobileIconGroup) {
         _iconGroup.value = group
     }
@@ -45,6 +53,18 @@
         _isEmergencyOnly.value = emergency
     }
 
+    fun setIsDataEnabled(enabled: Boolean) {
+        _isDataEnabled.value = enabled
+    }
+
+    fun setIsDefaultDataEnabled(disabled: Boolean) {
+        _isDefaultDataEnabled.value = disabled
+    }
+
+    fun setIsFailedConnection(failed: Boolean) {
+        _isFailedConnection.value = failed
+    }
+
     fun setLevel(level: Int) {
         _level.value = level
     }
@@ -52,8 +72,4 @@
     fun setNumberOfLevels(num: Int) {
         _numberOfLevels.value = num
     }
-
-    fun setCutOut(cutOut: Boolean) {
-        _cutOut.value = cutOut
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
new file mode 100644
index 0000000..061c3b54
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
+    val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
+    val LTE_KEY = mobileMappings.toIconKey(LTE)
+    val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
+    val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE)
+
+    /**
+     * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+     * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+     * the exhaustive set of icons
+     */
+    val TEST_MAPPING: Map<String, MobileIconGroup> =
+        mapOf(
+            THREE_G_KEY to TelephonyIcons.THREE_G,
+            LTE_KEY to TelephonyIcons.LTE,
+            FOUR_G_KEY to TelephonyIcons.FOUR_G,
+            FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
+        )
+
+    override val isDefaultConnectionFailed = MutableStateFlow(false)
+
+    private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val filteredSubscriptions = _filteredSubscriptions
+
+    private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
+    override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
+
+    private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
+    override val defaultMobileIconMapping = _defaultMobileIconMapping
+
+    private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
+    override val defaultMobileIconGroup = _defaultMobileIconGroup
+
+    private val _isUserSetup = MutableStateFlow(true)
+    override val isUserSetup = _isUserSetup
+
+    /** Always returns a new fake interactor */
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
+        return FakeMobileIconInteractor()
+    }
+
+    companion object {
+        val DEFAULT_ICON = TelephonyIcons.G
+
+        // Use [MobileMappings] to define some simple definitions
+        const val THREE_G = NETWORK_TYPE_GSM
+        const val LTE = NETWORK_TYPE_LTE
+        const val FOUR_G = NETWORK_TYPE_UMTS
+        const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2f07d9c..7fc1c0f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -18,37 +18,60 @@
 
 import android.telephony.CellSignalStrength
 import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 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.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 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
 
 @SmallTest
 class MobileIconInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconInteractor
-    private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
-    private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
+    private val connectionRepository = FakeMobileConnectionRepository()
+
+    private val scope = CoroutineScope(IMMEDIATE)
 
     @Before
     fun setUp() {
-        underTest = MobileIconInteractorImpl(sub1Flow)
+        underTest =
+            MobileIconInteractorImpl(
+                scope,
+                mobileIconsInteractor.activeDataConnectionHasDataEnabled,
+                mobileIconsInteractor.defaultMobileIconMapping,
+                mobileIconsInteractor.defaultMobileIconGroup,
+                mobileIconsInteractor.isDefaultConnectionFailed,
+                mobileMappingsProxy,
+                connectionRepository,
+            )
     }
 
     @Test
     fun gsm_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = true),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -62,13 +85,12 @@
     @Test
     fun gsm_usesGsmLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = true,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -82,9 +104,8 @@
     @Test
     fun cdma_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = false),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -97,13 +118,12 @@
     @Test
     fun cdma_usesCdmaLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = false,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -114,6 +134,135 @@
             job.cancel()
         }
 
+    @Test
+    fun iconGroup_three_g() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_updates_on_change() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(FOUR_G),
+                ),
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_5g_override_type() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.NR_5G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_default_if_no_lookup() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON)
+
+            job.cancel()
+        }
+
+    @Test
+    fun test_isDefaultDataEnabled_matchesParent() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this)
+
+            mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
+            assertThat(latest).isTrue()
+
+            mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun test_isDefaultConnectionFailed_matchedParent() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isDefaultConnectionFailed.launchIn(this)
+
+            mobileIconsInteractor.isDefaultConnectionFailed.value = false
+            assertThat(underTest.isDefaultConnectionFailed.value).isFalse()
+
+            mobileIconsInteractor.isDefaultConnectionFailed.value = true
+            assertThat(underTest.isDefaultConnectionFailed.value).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataState_connected() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected)
+            )
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataState_notConnected() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected)
+            )
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -123,9 +272,5 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
     }
 }
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 89ad9cb..b56dcd7 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
@@ -17,18 +17,24 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+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
 import com.android.systemui.util.CarrierConfigTracker
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
@@ -39,18 +45,33 @@
 class MobileIconsInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconsInteractor
     private val userSetupRepository = FakeUserSetupRepository()
-    private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+    private val connectionsRepository = FakeMobileConnectionsRepository()
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val scope = CoroutineScope(IMMEDIATE)
 
     @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
 
     @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 =
-            MobileIconsInteractor(
-                subscriptionsRepository,
+            MobileIconsInteractorImpl(
+                connectionsRepository,
                 carrierConfigTracker,
+                mobileMappingsProxy,
                 userSetupRepository,
+                scope
             )
     }
 
@@ -70,7 +91,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)
@@ -83,8 +104,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)
 
@@ -100,8 +121,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)
 
@@ -117,8 +138,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)
 
@@ -135,8 +156,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)
 
@@ -150,16 +171,104 @@
             job.cancel()
         }
 
+    @Test
+    fun activeDataConnection_turnedOn() =
+        runBlocking(IMMEDIATE) {
+            CONNECTION_1.setDataEnabled(true)
+            var latest: Boolean? = null
+            val job =
+                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun activeDataConnection_turnedOff() =
+        runBlocking(IMMEDIATE) {
+            CONNECTION_1.setDataEnabled(true)
+            var latest: Boolean? = null
+            val job =
+                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+            CONNECTION_1.setDataEnabled(false)
+            yield()
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun activeDataConnection_invalidSubId() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job =
+                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
+            yield()
+
+            // An invalid active subId should tell us that data is off
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun failedConnection_connected_validated_notFailed() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
+            yield()
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun failedConnection_notConnected_notValidated_notFailed() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false))
+            yield()
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun failedConnection_connected_notValidated_failed() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false))
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
         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 =
@@ -167,6 +276,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 =
@@ -174,5 +284,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..d4c2c3f 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
@@ -43,10 +46,12 @@
         MockitoAnnotations.initMocks(this)
         interactor.apply {
             setLevel(1)
-            setCutOut(false)
-            setIconGroup(TelephonyIcons.THREE_G)
+            setIsDefaultDataEnabled(true)
+            setIsFailedConnection(false)
+            setIconGroup(THREE_G)
             setIsEmergencyOnly(false)
             setNumberOfLevels(4)
+            isDataConnected.value = true
         }
         underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
     }
@@ -56,12 +61,127 @@
         runBlocking(IMMEDIATE) {
             var latest: Int? = null
             val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+            val expected = defaultSignal()
 
-            assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false))
+            assertThat(latest).isEqualTo(expected)
 
             job.cancel()
         }
 
+    @Test
+    fun iconId_cutout_whenDefaultDataDisabled() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIsDefaultDataEnabled(false)
+
+            var latest: Int? = null
+            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+            val expected = defaultSignal(level = 1, connected = false)
+
+            assertThat(latest).isEqualTo(expected)
+
+            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_nullWhenFailedConnection() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIconGroup(THREE_G)
+            interactor.setIsDataEnabled(true)
+            interactor.setIsFailedConnection(true)
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkType_nullWhenDataDisconnects() =
+        runBlocking(IMMEDIATE) {
+            val initial =
+                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)
+
+            interactor.setIconGroup(THREE_G)
+            assertThat(latest).isEqualTo(initial)
+
+            interactor.isDataConnected.value = false
+            yield()
+
+            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()
+        }
+
+    /** Convenience constructor for these tests */
+    private fun defaultSignal(
+        level: Int = 1,
+        connected: Boolean = true,
+    ): Int {
+        return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
+    }
+
     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/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
new file mode 100644
index 0000000..a052008
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.util
+
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.settingslib.mobile.TelephonyIcons
+
+class FakeMobileMappingsProxy : MobileMappingsProxy {
+    // The old [NetworkControllerDataTest] infra requires us to be able to use the real
+    // impl sometimes
+    var useRealImpl = false
+
+    private var realImpl = MobileMappingsProxyImpl()
+    private var iconMap = mapOf<String, MobileIconGroup>()
+    private var defaultIcons = TelephonyIcons.THREE_G
+
+    fun setIconMap(map: Map<String, MobileIconGroup>) {
+        iconMap = map
+    }
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> {
+        if (useRealImpl) {
+            return realImpl.mapIconSets(config)
+        }
+        return iconMap
+    }
+    fun getIconMap() = iconMap
+
+    fun setDefaultIcons(group: MobileIconGroup) {
+        defaultIcons = group
+    }
+    override fun getDefaultIcons(config: Config): MobileIconGroup {
+        if (useRealImpl) {
+            return realImpl.getDefaultIcons(config)
+        }
+        return defaultIcons
+    }
+
+    /** This is only used in the old pipeline, use the real impl always */
+    override fun getIconKey(displayInfo: TelephonyDisplayInfo): String {
+        return realImpl.getIconKey(displayInfo)
+    }
+
+    fun getDefaultIcons(): MobileIconGroup = defaultIcons
+
+    override fun toIconKey(networkType: Int): String {
+        if (useRealImpl) {
+            return realImpl.toIconKeyOverride(networkType)
+        }
+        return networkType.toString()
+    }
+
+    override fun toIconKeyOverride(networkType: Int): String {
+        if (useRealImpl) {
+            return realImpl.toIconKeyOverride(networkType)
+        }
+        return toIconKey(networkType) + "_override"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 0e75c74..b32058f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index f751afc..2f18ce3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -27,6 +27,9 @@
     private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
 
+    private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
+
     private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
         MutableStateFlow(WifiNetworkModel.Inactive)
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
@@ -38,6 +41,10 @@
         _isWifiEnabled.value = enabled
     }
 
+    fun setIsWifiDefault(default: Boolean) {
+        _isWifiDefault.value = default
+    }
+
     fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
         _wifiNetwork.value = wifiNetworkModel
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 0ba0bd6..a64a4bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -222,6 +222,83 @@
     }
 
     @Test
+    fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(
+            NETWORK,
+            createWifiNetworkCapabilities(wifiInfo)
+        )
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(mock())
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        // First, add a network
+        getDefaultNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        // WHEN the network is lost
+        getDefaultNetworkCallback().onLost(NETWORK)
+
+        // THEN we update to false
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
@@ -745,6 +822,12 @@
         return callbackCaptor.value!!
     }
 
+    private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+        verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
     private fun createWifiNetworkCapabilities(
         wifiInfo: WifiInfo,
         isValidated: Boolean = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index 39b886a..71b8bab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -178,6 +178,29 @@
     }
 
     @Test
+    fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .isDefault
+            .onEach { latest = it }
+            .launchIn(this)
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        wifiRepository.setIsWifiDefault(false)
+        yield()
+        assertThat(latest).isFalse()
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) {
         val wifiNetwork = WifiNetworkModel.Active(
             networkId = 45,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 4efb135..37457b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -28,8 +28,10 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
@@ -37,6 +39,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -63,11 +66,13 @@
     private lateinit var connectivityConstants: ConnectivityConstants
     @Mock
     private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
-    private lateinit var viewModel: WifiViewModel
+    private lateinit var viewModel: LocationBasedWifiViewModel
     private lateinit var scope: CoroutineScope
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
 
     @JvmField @Rule
     val instantTaskExecutor = InstantTaskExecutorRule()
@@ -77,12 +82,22 @@
         MockitoAnnotations.initMocks(this)
         testableLooper = TestableLooper.get(this)
 
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(Dispatchers.Unconfined)
+        airplaneModeViewModel = AirplaneModeViewModel(
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                connectivityRepository,
+            ),
+            logger,
+            scope,
+        )
         viewModel = WifiViewModel(
+            airplaneModeViewModel,
             connectivityConstants,
             context,
             logger,
@@ -90,23 +105,19 @@
             scope,
             statusBarPipelineFlags,
             wifiConstants,
-        )
+        ).home
     }
 
     @Test
     fun constructAndBind_hasCorrectSlot() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, "slotName", viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, "slotName", viewModel)
 
         assertThat(view.slot).isEqualTo("slotName")
     }
 
     @Test
     fun getVisibleState_icon_returnsIcon() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_ICON, /* animate= */ false)
 
@@ -115,9 +126,7 @@
 
     @Test
     fun getVisibleState_dot_returnsDot() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_DOT, /* animate= */ false)
 
@@ -126,9 +135,7 @@
 
     @Test
     fun getVisibleState_hidden_returnsHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
 
@@ -140,9 +147,7 @@
 
     @Test
     fun setVisibleState_icon_iconShownDotHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_ICON, /* animate= */ false)
 
@@ -157,9 +162,7 @@
 
     @Test
     fun setVisibleState_dot_iconHiddenDotShown() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_DOT, /* animate= */ false)
 
@@ -174,9 +177,7 @@
 
     @Test
     fun setVisibleState_hidden_iconAndDotHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
 
@@ -196,9 +197,7 @@
             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
         )
 
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
@@ -215,9 +214,7 @@
             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
         )
 
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index a3ad028..a1afcd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -22,11 +22,14 @@
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
@@ -64,19 +67,31 @@
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
     private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(IMMEDIATE)
+        airplaneModeViewModel =
+            AirplaneModeViewModel(
+                AirplaneModeInteractor(
+                    airplaneModeRepository,
+                    connectivityRepository,
+                ),
+                logger,
+                scope,
+            )
     }
 
     @After
@@ -88,6 +103,7 @@
     fun wifiIcon() =
         runBlocking(IMMEDIATE) {
             wifiRepository.setIsWifiEnabled(testCase.enabled)
+            wifiRepository.setIsWifiDefault(testCase.isDefault)
             connectivityRepository.setForceHiddenIcons(
                 if (testCase.forceHidden) {
                     setOf(ConnectivitySlot.WIFI)
@@ -101,6 +117,7 @@
                 .thenReturn(testCase.hasDataCapabilities)
             underTest =
                 WifiViewModel(
+                    airplaneModeViewModel,
                     connectivityConstants,
                     context,
                     logger,
@@ -125,19 +142,12 @@
                 } else {
                     testCase.expected.contentDescription.invoke(context)
                 }
-            assertThat(iconFlow.value?.contentDescription?.getAsString())
+            assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context))
                 .isEqualTo(expectedContentDescription)
 
             job.cancel()
         }
 
-    private fun ContentDescription.getAsString(): String? {
-        return when (this) {
-            is ContentDescription.Loaded -> this.description
-            is ContentDescription.Resource -> context.getString(this.res)
-        }
-    }
-
     internal data class Expected(
         /** The resource that should be used for the icon. */
         @DrawableRes val iconResource: Int,
@@ -159,6 +169,7 @@
         val forceHidden: Boolean = false,
         val alwaysShowIconWhenEnabled: Boolean = false,
         val hasDataCapabilities: Boolean = true,
+        val isDefault: Boolean = false,
         val network: WifiNetworkModel,
 
         /** The expected output. Null if we expect the output to be null. */
@@ -169,6 +180,7 @@
                 "forceHidden=$forceHidden, " +
                 "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
                 "hasDataCaps=$hasDataCapabilities, " +
+                "isDefault=$isDefault, " +
                 "network=$network) then " +
                 "EXPECTED($expected)"
         }
@@ -303,6 +315,46 @@
                         ),
                 ),
 
+                // isDefault = true => all Inactive and Active networks shown
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Inactive,
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_NETWORK,
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_NO_CONNECTION)}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No network icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_INTERNET_ICONS[3],
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No internet level 3 icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_FULL_ICONS[1],
+                            contentDescription = { context ->
+                                context.getString(WIFI_CONNECTION_STRENGTH[1])
+                            },
+                            description = "Full internet level 1 icon",
+                        ),
+                ),
+
                 // network = CarrierMerged => not shown
                 TestCase(
                     network = WifiNetworkModel.CarrierMerged,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 3169eef..7d2c560 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -20,8 +20,12 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
@@ -55,19 +59,31 @@
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
     private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(IMMEDIATE)
+        airplaneModeViewModel = AirplaneModeViewModel(
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                connectivityRepository,
+            ),
+            logger,
+            scope,
+        )
+
         createAndSetViewModel()
     }
 
@@ -76,6 +92,8 @@
         scope.cancel()
     }
 
+    // See [WifiViewModelIconParameterizedTest] for additional view model tests.
+
     // Note on testing: [WifiViewModel] exposes 3 different instances of
     // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact
     // same data for icon, activity, etc. flows. So, most of these tests will test just one of the
@@ -460,11 +478,64 @@
         job.cancel()
     }
 
+    @Test
+    fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(false)
+        yield()
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(true)
+        connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+        yield()
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(true)
+        yield()
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
     private fun createAndSetViewModel() {
         // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow
         // creations rely on certain config values that we mock out in individual tests. This method
         // allows tests to create the view model only after those configs are correctly set up.
         underTest = WifiViewModel(
+            airplaneModeViewModel,
             connectivityConstants,
             context,
             logger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt
index f304647..0a3da0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt
@@ -237,7 +237,7 @@
     fun refresh() {
         underTest.refresh()
 
-        verify(controller).refreshUsers(UserHandle.USER_NULL)
+        verify(controller).refreshUsers()
     }
 
     private fun createUserRecord(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 43d0fe9..1eee08c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -221,4 +221,33 @@
 
         Assert.assertFalse(mBatteryController.isChargingSourceDock());
     }
+
+    @Test
+    public void batteryStateChanged_healthNotOverheated_outputsFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isOverheated());
+    }
+
+    @Test
+    public void batteryStateChanged_healthOverheated_outputsTrue() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertTrue(mBatteryController.isOverheated());
+    }
+
+    @Test
+    public void batteryStateChanged_noHealthGiven_outputsFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isOverheated());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
index d0391ac..833cabb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.bluetooth.BluetoothLogger;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -56,6 +57,7 @@
 @SmallTest
 public class BluetoothControllerImplTest extends SysuiTestCase {
 
+    private UserTracker mUserTracker;
     private LocalBluetoothManager mMockBluetoothManager;
     private CachedBluetoothDeviceManager mMockDeviceManager;
     private LocalBluetoothAdapter mMockAdapter;
@@ -70,6 +72,7 @@
         mTestableLooper = TestableLooper.get(this);
         mMockBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager.class);
         mDevices = new ArrayList<>();
+        mUserTracker = mock(UserTracker.class);
         mMockDeviceManager = mock(CachedBluetoothDeviceManager.class);
         when(mMockDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
         when(mMockBluetoothManager.getCachedDeviceManager()).thenReturn(mMockDeviceManager);
@@ -81,6 +84,7 @@
         mMockDumpManager = mock(DumpManager.class);
 
         mBluetoothControllerImpl = new BluetoothControllerImpl(mContext,
+                mUserTracker,
                 mMockDumpManager,
                 mock(BluetoothLogger.class),
                 mTestableLooper.getLooper(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
index 4f1fb02..dc08aba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -25,6 +26,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.net.TetheringManager;
@@ -36,8 +38,10 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,6 +62,8 @@
 public class HotspotControllerImplTest extends SysuiTestCase {
 
     @Mock
+    private UserTracker mUserTracker;
+    @Mock
     private DumpManager mDumpManager;
     @Mock
     private TetheringManager mTetheringManager;
@@ -96,9 +102,13 @@
         }).when(mWifiManager).registerSoftApCallback(any(Executor.class),
                 any(WifiManager.SoftApCallback.class));
 
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_show_wifi_tethering, true);
+
         Handler handler = new Handler(mLooper.getLooper());
 
-        mController = new HotspotControllerImpl(mContext, handler, handler, mDumpManager);
+        mController = new HotspotControllerImpl(mContext, mUserTracker, handler, handler,
+                mDumpManager);
         verify(mTetheringManager)
                 .registerTetheringEventCallback(any(), mTetheringCallbackCaptor.capture());
     }
@@ -176,4 +186,18 @@
 
         verify(mCallback1).onHotspotAvailabilityChanged(false);
     }
+
+    @Test
+    public void testHotspotSupported_resource_false() {
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_show_wifi_tethering, false);
+
+        Handler handler = new Handler(mLooper.getLooper());
+
+        HotspotController controller =
+                new HotspotControllerImpl(mContext, mUserTracker, handler, handler, mDumpManager);
+
+        verifyNoMoreInteractions(mTetheringManager);
+        assertFalse(controller.isHotspotSupported());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 6ace404..915e999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -23,8 +23,12 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.app.PendingIntent;
@@ -43,10 +47,14 @@
 import android.testing.TestableLooper;
 import android.view.ContentInfo;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
@@ -67,6 +75,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -229,6 +238,67 @@
     }
 
     @Test
+    public void testPredictiveBack_registerAndUnregister() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+
+        /* verify that predictive back callback registered when RemoteInputView becomes visible */
+        view.onVisibilityAggregated(true);
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        /* verify that same callback unregistered when RemoteInputView becomes invisible */
+        view.onVisibilityAggregated(false);
+        verify(backInvokedDispatcher).unregisterOnBackInvokedCallback(
+                eq(onBackInvokedCallbackCaptor.getValue()));
+    }
+
+    @Test
+    public void testUiPredictiveBack_openAndDispatchCallback() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+        view.onVisibilityAggregated(true);
+        view.setEditTextReferenceToSelf();
+
+        /* capture the callback during registration */
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        view.focus();
+
+        /* invoke the captured callback */
+        onBackInvokedCallbackCaptor.getValue().onBackInvoked();
+
+        /* verify that the RemoteInputView goes away */
+        assertEquals(view.getVisibility(), View.GONE);
+    }
+
+    @Test
     public void testUiEventLogging_openAndSend() throws Exception {
         NotificationTestHelper helper = new NotificationTestHelper(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
index d44cdb2..15235b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SecurityControllerTest.java
@@ -50,6 +50,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -72,10 +73,12 @@
     private final DevicePolicyManager mDevicePolicyManager = mock(DevicePolicyManager.class);
     private final IKeyChainService.Stub mKeyChainService = mock(IKeyChainService.Stub.class);
     private final UserManager mUserManager = mock(UserManager.class);
+    private final UserTracker mUserTracker = mock(UserTracker.class);
     private final BroadcastDispatcher mBroadcastDispatcher = mock(BroadcastDispatcher.class);
     private final Handler mHandler = mock(Handler.class);
     private SecurityControllerImpl mSecurityController;
     private ConnectivityManager mConnectivityManager = mock(ConnectivityManager.class);
+    private FakeExecutor mMainExecutor;
     private FakeExecutor mBgExecutor;
     private BroadcastReceiver mBroadcastReceiver;
 
@@ -102,11 +105,14 @@
         ArgumentCaptor<BroadcastReceiver> brCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
 
+        mMainExecutor = new FakeExecutor(new FakeSystemClock());
         mBgExecutor = new FakeExecutor(new FakeSystemClock());
         mSecurityController = new SecurityControllerImpl(
                 mContext,
+                mUserTracker,
                 mHandler,
                 mBroadcastDispatcher,
+                mMainExecutor,
                 mBgExecutor,
                 Mockito.mock(DumpManager.class));
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
deleted file mode 100644
index 169f4fb..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
+++ /dev/null
@@ -1,727 +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
- */
-
-package com.android.systemui.statusbar.policy
-
-import android.app.IActivityManager
-import android.app.NotificationManager
-import android.app.admin.DevicePolicyManager
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.DialogInterface
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.graphics.Bitmap
-import android.hardware.face.FaceManager
-import android.hardware.fingerprint.FingerprintManager
-import android.os.Handler
-import android.os.UserHandle
-import android.os.UserManager
-import android.provider.Settings
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.ThreadedRenderer
-import androidx.test.filters.SmallTest
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.util.LatencyTracker
-import com.android.internal.util.UserIcons
-import com.android.systemui.GuestResetOrExitSessionReceiver
-import com.android.systemui.GuestResumeSessionReceiver
-import com.android.systemui.GuestSessionNotification
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.DialogCuj
-import com.android.systemui.animation.DialogLaunchAnimator
-import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.QSUserSwitcherEvent
-import com.android.systemui.qs.user.UserSwitchDialogController
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.shade.NotificationShadeWindowView
-import com.android.systemui.telephony.TelephonyListenerManager
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.Mockito.doNothing
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@SmallTest
-class UserSwitcherControllerOldImplTest : SysuiTestCase() {
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var activityManager: IActivityManager
-    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
-    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
-    @Mock private lateinit var handler: Handler
-    @Mock private lateinit var userTracker: UserTracker
-    @Mock private lateinit var userManager: UserManager
-    @Mock private lateinit var activityStarter: ActivityStarter
-    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock private lateinit var broadcastSender: BroadcastSender
-    @Mock private lateinit var telephonyListenerManager: TelephonyListenerManager
-    @Mock private lateinit var secureSettings: SecureSettings
-    @Mock private lateinit var falsingManager: FalsingManager
-    @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock private lateinit var latencyTracker: LatencyTracker
-    @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
-    @Mock private lateinit var notificationShadeWindowView: NotificationShadeWindowView
-    @Mock private lateinit var threadedRenderer: ThreadedRenderer
-    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
-    @Mock private lateinit var globalSettings: GlobalSettings
-    @Mock private lateinit var guestSessionNotification: GuestSessionNotification
-    @Mock private lateinit var guestResetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
-    private lateinit var resetSessionDialogFactory:
-                            GuestResumeSessionReceiver.ResetSessionDialog.Factory
-    private lateinit var guestResumeSessionReceiver: GuestResumeSessionReceiver
-    private lateinit var testableLooper: TestableLooper
-    private lateinit var bgExecutor: FakeExecutor
-    private lateinit var longRunningExecutor: FakeExecutor
-    private lateinit var uiExecutor: FakeExecutor
-    private lateinit var uiEventLogger: UiEventLoggerFake
-    private lateinit var userSwitcherController: UserSwitcherControllerOldImpl
-    private lateinit var picture: Bitmap
-    private val ownerId = UserHandle.USER_SYSTEM
-    private val ownerInfo = UserInfo(ownerId, "Owner", null,
-            UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL or UserInfo.FLAG_INITIALIZED or
-                    UserInfo.FLAG_PRIMARY or UserInfo.FLAG_SYSTEM or UserInfo.FLAG_ADMIN,
-            UserManager.USER_TYPE_FULL_SYSTEM)
-    private val guestId = 1234
-    private val guestInfo = UserInfo(guestId, "Guest", null,
-            UserInfo.FLAG_FULL or UserInfo.FLAG_GUEST, UserManager.USER_TYPE_FULL_GUEST)
-    private val secondaryUser =
-            UserInfo(10, "Secondary", null, 0, UserManager.USER_TYPE_FULL_SECONDARY)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        testableLooper = TestableLooper.get(this)
-        bgExecutor = FakeExecutor(FakeSystemClock())
-        longRunningExecutor = FakeExecutor(FakeSystemClock())
-        uiExecutor = FakeExecutor(FakeSystemClock())
-        uiEventLogger = UiEventLoggerFake()
-
-        mContext.orCreateTestableResources.addOverride(
-                com.android.internal.R.bool.config_guestUserAutoCreated, false)
-
-        mContext.addMockSystemService(Context.FACE_SERVICE, mock(FaceManager::class.java))
-        mContext.addMockSystemService(Context.NOTIFICATION_SERVICE,
-                mock(NotificationManager::class.java))
-        mContext.addMockSystemService(Context.FINGERPRINT_SERVICE,
-                mock(FingerprintManager::class.java))
-
-        resetSessionDialogFactory = object : GuestResumeSessionReceiver.ResetSessionDialog.Factory {
-                override fun create(userId: Int): GuestResumeSessionReceiver.ResetSessionDialog {
-                    return GuestResumeSessionReceiver.ResetSessionDialog(
-                                mContext,
-                                mock(UserSwitcherController::class.java),
-                                uiEventLogger,
-                                userId
-                            )
-                }
-            }
-
-        guestResumeSessionReceiver = GuestResumeSessionReceiver(userTracker,
-                                        secureSettings,
-                                        broadcastDispatcher,
-                                        guestSessionNotification,
-                                        resetSessionDialogFactory)
-
-        `when`(userManager.canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY)))
-                .thenReturn(true)
-        `when`(notificationShadeWindowView.context).thenReturn(context)
-
-        // Since userSwitcherController involves InteractionJankMonitor.
-        // Let's fulfill the dependencies.
-        val mockedContext = mock(Context::class.java)
-        doReturn(mockedContext).`when`(notificationShadeWindowView).context
-        doReturn(true).`when`(notificationShadeWindowView).isAttachedToWindow
-        doNothing().`when`(threadedRenderer).addObserver(any())
-        doNothing().`when`(threadedRenderer).removeObserver(any())
-        doReturn(threadedRenderer).`when`(notificationShadeWindowView).threadedRenderer
-
-        picture = UserIcons.convertToBitmap(context.getDrawable(R.drawable.ic_avatar_user))
-
-        // Create defaults for the current user
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(1)
-
-        setupController()
-    }
-
-    private fun setupController() {
-        userSwitcherController =
-            UserSwitcherControllerOldImpl(
-                mContext,
-                activityManager,
-                userManager,
-                userTracker,
-                keyguardStateController,
-                deviceProvisionedController,
-                devicePolicyManager,
-                handler,
-                activityStarter,
-                broadcastDispatcher,
-                broadcastSender,
-                uiEventLogger,
-                falsingManager,
-                telephonyListenerManager,
-                secureSettings,
-                globalSettings,
-                bgExecutor,
-                longRunningExecutor,
-                uiExecutor,
-                interactionJankMonitor,
-                latencyTracker,
-                dumpManager,
-                dialogLaunchAnimator,
-                guestResumeSessionReceiver,
-                guestResetOrExitSessionReceiver
-            )
-        userSwitcherController.init(notificationShadeWindowView)
-    }
-
-    @Test
-    fun testSwitchUser_parentDialogDismissed() {
-        val otherUserRecord = UserRecord(
-            secondaryUser,
-            picture,
-            false /* guest */,
-            false /* current */,
-            false /* isAddUser */,
-            false /* isRestricted */,
-            true /* isSwitchToEnabled */,
-            false /* isAddSupervisedUser */
-        )
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        userSwitcherController.onUserListItemClicked(otherUserRecord, dialogShower)
-        testableLooper.processAllMessages()
-
-        verify(dialogShower).dismiss()
-    }
-
-    @Test
-    fun testAddGuest_okButtonPressed() {
-        val emptyGuestUserRecord =
-            UserRecord(
-                null,
-                null,
-                true /* guest */,
-                false /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        `when`(userManager.createGuest(any())).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, null)
-        bgExecutor.runAllReady()
-        uiExecutor.runAllReady()
-        testableLooper.processAllMessages()
-        verify(interactionJankMonitor).begin(any())
-        verify(latencyTracker).onActionStart(LatencyTracker.ACTION_USER_SWITCH)
-        verify(activityManager).switchUser(guestInfo.id)
-        assertEquals(1, uiEventLogger.numLogs())
-        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_ADD.id, uiEventLogger.eventId(0))
-    }
-
-    @Test
-    fun testAddGuest_parentDialogDismissed() {
-        val emptyGuestUserRecord =
-            UserRecord(
-                null,
-                null,
-                true /* guest */,
-                false /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        `when`(userManager.createGuest(any())).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(emptyGuestUserRecord, dialogShower)
-        bgExecutor.runAllReady()
-        uiExecutor.runAllReady()
-        testableLooper.processAllMessages()
-        verify(dialogShower).dismiss()
-    }
-
-    @Test
-    fun testRemoveGuest_removeButtonPressed_isLogged() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                true /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestInfo.id)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
-        assertNotNull(userSwitcherController.mExitGuestDialog)
-        userSwitcherController.mExitGuestDialog
-                .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
-        testableLooper.processAllMessages()
-        assertEquals(1, uiEventLogger.numLogs())
-        assertTrue(
-            QSUserSwitcherEvent.QS_USER_GUEST_REMOVE.id == uiEventLogger.eventId(0) ||
-            QSUserSwitcherEvent.QS_USER_SWITCH.id == uiEventLogger.eventId(0)
-        )
-    }
-
-    @Test
-    fun testRemoveGuest_removeButtonPressed_dialogDismissed() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                true /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestInfo.id)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
-        assertNotNull(userSwitcherController.mExitGuestDialog)
-        userSwitcherController.mExitGuestDialog
-                .getButton(DialogInterface.BUTTON_POSITIVE).performClick()
-        testableLooper.processAllMessages()
-        assertFalse(userSwitcherController.mExitGuestDialog.isShowing)
-    }
-
-    @Test
-    fun testRemoveGuest_dialogShowerUsed() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                true /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestInfo.id)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, dialogShower)
-        assertNotNull(userSwitcherController.mExitGuestDialog)
-        testableLooper.processAllMessages()
-        verify(dialogShower)
-            .showDialog(
-                userSwitcherController.mExitGuestDialog,
-                DialogCuj(InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, "exit_guest_mode"))
-    }
-
-    @Test
-    fun testRemoveGuest_cancelButtonPressed_isNotLogged() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                true /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestId)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
-        assertNotNull(userSwitcherController.mExitGuestDialog)
-        userSwitcherController.mExitGuestDialog
-                .getButton(DialogInterface.BUTTON_NEUTRAL).performClick()
-        testableLooper.processAllMessages()
-        assertEquals(0, uiEventLogger.numLogs())
-    }
-
-    @Test
-    fun testWipeGuest_startOverButtonPressed_isLogged() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                false /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestId)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        // Simulate that guest user has already logged in
-        `when`(secureSettings.getIntForUser(
-                eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
-                .thenReturn(1)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
-
-        // Simulate a user switch event
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
-
-        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
-        userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)
-
-        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
-        userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
-                .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_WIPE).performClick()
-        testableLooper.processAllMessages()
-        assertEquals(1, uiEventLogger.numLogs())
-        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_WIPE.id, uiEventLogger.eventId(0))
-    }
-
-    @Test
-    fun testWipeGuest_continueButtonPressed_isLogged() {
-        val currentGuestUserRecord =
-            UserRecord(
-                guestInfo,
-                picture,
-                true /* guest */,
-                false /* current */,
-                false /* isAddUser */,
-                false /* isRestricted */,
-                true /* isSwitchToEnabled */,
-                false /* isAddSupervisedUser */
-            )
-        `when`(userTracker.userId).thenReturn(guestId)
-        `when`(userTracker.userInfo).thenReturn(guestInfo)
-
-        // Simulate that guest user has already logged in
-        `when`(secureSettings.getIntForUser(
-                eq(GuestResumeSessionReceiver.SETTING_GUEST_HAS_LOGGED_IN), anyInt(), anyInt()))
-                .thenReturn(1)
-
-        userSwitcherController.onUserListItemClicked(currentGuestUserRecord, null)
-
-        // Simulate a user switch event
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
-
-        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver)
-        userSwitcherController.mGuestResumeSessionReceiver.onReceive(context, intent)
-
-        assertNotNull(userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog)
-        userSwitcherController.mGuestResumeSessionReceiver.mNewSessionDialog
-                .getButton(GuestResumeSessionReceiver.ResetSessionDialog.BUTTON_DONTWIPE)
-                .performClick()
-        testableLooper.processAllMessages()
-        assertEquals(1, uiEventLogger.numLogs())
-        assertEquals(QSUserSwitcherEvent.QS_USER_GUEST_CONTINUE.id, uiEventLogger.eventId(0))
-    }
-
-    @Test
-    fun test_getCurrentUserName_shouldReturnNameOfTheCurrentUser() {
-        fun addUser(id: Int, name: String, isCurrent: Boolean) {
-            userSwitcherController.users.add(
-                UserRecord(
-                    UserInfo(id, name, 0),
-                    null, false, isCurrent, false,
-                    false, false, false
-                )
-            )
-        }
-        val bgUserName = "background_user"
-        val fgUserName = "foreground_user"
-
-        addUser(1, bgUserName, false)
-        addUser(2, fgUserName, true)
-
-        assertEquals(fgUserName, userSwitcherController.currentUserName)
-    }
-
-    @Test
-    fun isSystemUser_currentUserIsSystemUser_shouldReturnTrue() {
-        `when`(userTracker.userId).thenReturn(UserHandle.USER_SYSTEM)
-        assertEquals(true, userSwitcherController.isSystemUser)
-    }
-
-    @Test
-    fun isSystemUser_currentUserIsNotSystemUser_shouldReturnFalse() {
-        `when`(userTracker.userId).thenReturn(1)
-        assertEquals(false, userSwitcherController.isSystemUser)
-    }
-
-    @Test
-    fun testCanCreateSupervisedUserWithConfiguredPackage() {
-        // GIVEN the supervised user creation package is configured
-        `when`(context.getString(
-            com.android.internal.R.string.config_supervisedUserCreationPackage))
-            .thenReturn("some_pkg")
-
-        // AND the current user is allowed to create new users
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        // WHEN the controller is started with the above config
-        setupController()
-        testableLooper.processAllMessages()
-
-        // THEN a supervised user can be constructed
-        assertTrue(userSwitcherController.canCreateSupervisedUser())
-    }
-
-    @Test
-    fun testCannotCreateSupervisedUserWithConfiguredPackage() {
-        // GIVEN the supervised user creation package is NOT configured
-        `when`(context.getString(
-            com.android.internal.R.string.config_supervisedUserCreationPackage))
-            .thenReturn(null)
-
-        // AND the current user is allowed to create new users
-        `when`(userTracker.userId).thenReturn(ownerId)
-        `when`(userTracker.userInfo).thenReturn(ownerInfo)
-
-        // WHEN the controller is started with the above config
-        setupController()
-        testableLooper.processAllMessages()
-
-        // THEN a supervised user can NOT be constructed
-        assertFalse(userSwitcherController.canCreateSupervisedUser())
-    }
-
-    @Test
-    fun testCannotCreateUserWhenUserSwitcherDisabled() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-        setupController()
-        assertFalse(userSwitcherController.canCreateUser())
-    }
-
-    @Test
-    fun testCannotCreateGuestUserWhenUserSwitcherDisabled() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-        setupController()
-        assertFalse(userSwitcherController.canCreateGuest(false))
-    }
-
-    @Test
-    fun testCannotCreateSupervisedUserWhenUserSwitcherDisabled() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-        setupController()
-        assertFalse(userSwitcherController.canCreateSupervisedUser())
-    }
-
-    @Test
-    fun testCanManageUser_userSwitcherEnabled_addUserWhenLocked() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(1)
-
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(1)
-        setupController()
-        assertTrue(userSwitcherController.canManageUsers())
-    }
-
-    @Test
-    fun testCanManageUser_userSwitcherDisabled_addUserWhenLocked() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.ADD_USERS_WHEN_LOCKED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(1)
-        setupController()
-        assertFalse(userSwitcherController.canManageUsers())
-    }
-
-    @Test
-    fun testCanManageUser_userSwitcherEnabled_isAdmin() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(1)
-
-        setupController()
-        assertTrue(userSwitcherController.canManageUsers())
-    }
-
-    @Test
-    fun testCanManageUser_userSwitcherDisabled_isAdmin() {
-        `when`(
-            globalSettings.getIntForUser(
-                eq(Settings.Global.USER_SWITCHER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_SYSTEM)
-            )
-        ).thenReturn(0)
-
-        setupController()
-        assertFalse(userSwitcherController.canManageUsers())
-    }
-
-    @Test
-    fun addUserSwitchCallback() {
-        val broadcastReceiverCaptor = argumentCaptor<BroadcastReceiver>()
-        verify(broadcastDispatcher).registerReceiver(
-                capture(broadcastReceiverCaptor),
-                any(),
-                nullable(), nullable(), anyInt(), nullable())
-
-        val cb = mock(UserSwitcherController.UserSwitchCallback::class.java)
-        userSwitcherController.addUserSwitchCallback(cb)
-
-        val intent = Intent(Intent.ACTION_USER_SWITCHED).putExtra(Intent.EXTRA_USER_HANDLE, guestId)
-        broadcastReceiverCaptor.value.onReceive(context, intent)
-        verify(cb).onUserSwitched()
-    }
-
-    @Test
-    fun onUserItemClicked_guest_runsOnBgThread() {
-        val dialogShower = mock(UserSwitchDialogController.DialogShower::class.java)
-        val guestUserRecord = UserRecord(
-            null,
-            picture,
-            true /* guest */,
-            false /* current */,
-            false /* isAddUser */,
-            false /* isRestricted */,
-            true /* isSwitchToEnabled */,
-            false /* isAddSupervisedUser */
-        )
-
-        userSwitcherController.onUserListItemClicked(guestUserRecord, dialogShower)
-        assertTrue(bgExecutor.numPending() > 0)
-        verify(userManager, never()).createGuest(context)
-        bgExecutor.runAllReady()
-        verify(userManager).createGuest(context)
-    }
-
-    @Test
-    fun onUserItemClicked_manageUsers() {
-        val manageUserRecord = LegacyUserDataHelper.createRecord(
-            mContext,
-            ownerId,
-            UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-            isRestricted = false,
-            isSwitchToEnabled = true
-        )
-
-        userSwitcherController.onUserListItemClicked(manageUserRecord, null)
-        val intentCaptor = kotlinArgumentCaptor<Intent>()
-        verify(activityStarter).startActivity(intentCaptor.capture(),
-            eq(true)
-        )
-        Truth.assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
index 3fe1a9f..c06dbdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ZenModeControllerImplTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.ZenModeController.Callback;
 import com.android.systemui.util.settings.FakeSettings;
 
@@ -58,6 +59,8 @@
     BroadcastDispatcher mBroadcastDispatcher;
     @Mock
     DumpManager mDumpManager;
+    @Mock
+    UserTracker mUserTracker;
 
     private ZenModeControllerImpl mController;
 
@@ -72,7 +75,8 @@
                 Handler.createAsync(Looper.myLooper()),
                 mBroadcastDispatcher,
                 mDumpManager,
-                new FakeSettings());
+                new FakeSettings(),
+                mUserTracker);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt
new file mode 100644
index 0000000..0d19ab1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import android.graphics.Color
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MultiRippleControllerTest : SysuiTestCase() {
+    private lateinit var multiRippleController: MultiRippleController
+    private lateinit var multiRippleView: MultiRippleView
+    private lateinit var rippleAnimationConfig: RippleAnimationConfig
+    private val fakeSystemClock = FakeSystemClock()
+
+    // FakeExecutor is needed to run animator.
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Before
+    fun setup() {
+        rippleAnimationConfig = RippleAnimationConfig(duration = 1000L)
+        multiRippleView = MultiRippleView(context, null)
+        multiRippleController = MultiRippleController(multiRippleView)
+    }
+
+    @Test
+    fun updateColor_updatesColor() {
+        val initialColor = Color.WHITE
+        val expectedColor = Color.RED
+
+        fakeExecutor.execute {
+            val rippleAnimation =
+                RippleAnimation(rippleAnimationConfig.apply { this.color = initialColor })
+
+            with(multiRippleController) {
+                play(rippleAnimation)
+                updateColor(expectedColor)
+            }
+
+            assertThat(rippleAnimationConfig.color).isEqualTo(expectedColor)
+        }
+    }
+
+    @Test
+    fun play_playsRipple() {
+        fakeExecutor.execute {
+            val rippleAnimation = RippleAnimation(rippleAnimationConfig)
+
+            multiRippleController.play(rippleAnimation)
+
+            assertThat(multiRippleView.ripples.size).isEqualTo(1)
+            assertThat(multiRippleView.ripples[0]).isEqualTo(rippleAnimation)
+        }
+    }
+
+    @Test
+    fun play_doesNotExceedMaxRipple() {
+        fakeExecutor.execute {
+            for (i in 0..MAX_RIPPLE_NUMBER + 10) {
+                multiRippleController.play(RippleAnimation(rippleAnimationConfig))
+            }
+
+            assertThat(multiRippleView.ripples.size).isEqualTo(MAX_RIPPLE_NUMBER)
+        }
+    }
+
+    @Test
+    fun play_onEnd_removesAnimation() {
+        fakeExecutor.execute {
+            val rippleAnimation = RippleAnimation(rippleAnimationConfig)
+            multiRippleController.play(rippleAnimation)
+
+            assertThat(multiRippleView.ripples.size).isEqualTo(1)
+            assertThat(multiRippleView.ripples[0]).isEqualTo(rippleAnimation)
+
+            fakeSystemClock.advanceTime(rippleAnimationConfig.duration)
+
+            assertThat(multiRippleView.ripples.size).isEqualTo(0)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt
new file mode 100644
index 0000000..2024d53
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.surfaceeffects.ripple
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MultiRippleViewTest : SysuiTestCase() {
+    private val fakeSystemClock = FakeSystemClock()
+    // FakeExecutor is needed to run animator.
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun onRippleFinishes_triggersRippleFinished() {
+        val multiRippleView = MultiRippleView(context, null)
+        val multiRippleController = MultiRippleController(multiRippleView)
+        val rippleAnimationConfig = RippleAnimationConfig(duration = 1000L)
+
+        var isTriggered = false
+        val listener =
+            object : MultiRippleView.Companion.RipplesFinishedListener {
+                override fun onRipplesFinish() {
+                    isTriggered = true
+                }
+            }
+        multiRippleView.addRipplesFinishedListener(listener)
+
+        fakeExecutor.execute {
+            val rippleAnimation = RippleAnimation(rippleAnimationConfig)
+            multiRippleController.play(rippleAnimation)
+
+            fakeSystemClock.advanceTime(rippleAnimationConfig.duration)
+
+            assertThat(isTriggered).isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
new file mode 100644
index 0000000..756397a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.surfaceeffects.ripple
+
+import android.graphics.Color
+import android.testing.AndroidTestingRunner
+import androidx.core.graphics.ColorUtils
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RippleAnimationTest : SysuiTestCase() {
+
+    private val fakeSystemClock = FakeSystemClock()
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun init_shaderHasCorrectConfig() {
+        val config =
+            RippleAnimationConfig(
+                duration = 3000L,
+                pixelDensity = 2f,
+                color = Color.RED,
+                opacity = 30,
+                shouldFillRipple = true,
+                sparkleStrength = 0.3f
+            )
+        val rippleAnimation = RippleAnimation(config)
+
+        with(rippleAnimation.rippleShader) {
+            assertThat(rippleFill).isEqualTo(config.shouldFillRipple)
+            assertThat(pixelDensity).isEqualTo(config.pixelDensity)
+            assertThat(color).isEqualTo(ColorUtils.setAlphaComponent(config.color, config.opacity))
+            assertThat(sparkleStrength).isEqualTo(config.sparkleStrength)
+        }
+    }
+
+    @Test
+    fun updateColor_updatesColorCorrectly() {
+        val initialColor = Color.WHITE
+        val expectedColor = Color.RED
+        val config = RippleAnimationConfig(color = initialColor)
+        val rippleAnimation = RippleAnimation(config)
+
+        fakeExecutor.execute {
+            with(rippleAnimation) {
+                play()
+                updateColor(expectedColor)
+            }
+
+            assertThat(config.color).isEqualTo(expectedColor)
+        }
+    }
+
+    @Test
+    fun play_updatesIsPlaying() {
+        val config = RippleAnimationConfig(duration = 1000L)
+        val rippleAnimation = RippleAnimation(config)
+
+        fakeExecutor.execute {
+            rippleAnimation.play()
+
+            assertThat(rippleAnimation.isPlaying()).isTrue()
+
+            // move time to finish the animation
+            fakeSystemClock.advanceTime(config.duration)
+
+            assertThat(rippleAnimation.isPlaying()).isFalse()
+        }
+    }
+
+    @Test
+    fun play_onEnd_triggersOnAnimationEnd() {
+        val config = RippleAnimationConfig(duration = 1000L)
+        val rippleAnimation = RippleAnimation(config)
+        var animationEnd = false
+
+        fakeExecutor.execute {
+            rippleAnimation.play(onAnimationEnd = { animationEnd = true })
+
+            fakeSystemClock.advanceTime(config.duration)
+
+            assertThat(animationEnd).isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt
new file mode 100644
index 0000000..1e5ab7e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.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.surfaceeffects.ripple
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RippleViewTest : SysuiTestCase() {
+    private lateinit var rippleView: RippleView
+
+    @Before
+    fun setup() {
+        rippleView = RippleView(context, null)
+    }
+
+    @Test
+    fun testSetupShader_compilesCircle() {
+        rippleView.setupShader(RippleShader.RippleShape.CIRCLE)
+    }
+
+    @Test
+    fun testSetupShader_compilesRoundedBox() {
+        rippleView.setupShader(RippleShader.RippleShape.ROUNDED_BOX)
+    }
+
+    @Test
+    fun testSetupShader_compilesEllipse() {
+        rippleView.setupShader(RippleShader.RippleShape.ELLIPSE)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
new file mode 100644
index 0000000..d25c8c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.surfaceeffects.turbulencenoise
+
+import android.graphics.Color
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TurbulenceNoiseControllerTest : SysuiTestCase() {
+    private val fakeSystemClock = FakeSystemClock()
+    // FakeExecutor is needed to run animator.
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun play_playsTurbulenceNoise() {
+        val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+        val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+        val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
+
+        fakeExecutor.execute {
+            turbulenceNoiseController.play(config)
+
+            assertThat(turbulenceNoiseView.isPlaying).isTrue()
+
+            fakeSystemClock.advanceTime(config.duration.toLong())
+
+            assertThat(turbulenceNoiseView.isPlaying).isFalse()
+        }
+    }
+
+    @Test
+    fun updateColor_updatesCorrectColor() {
+        val config = TurbulenceNoiseAnimationConfig(duration = 1000f, color = Color.WHITE)
+        val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+        val expectedColor = Color.RED
+
+        val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
+
+        fakeExecutor.execute {
+            turbulenceNoiseController.play(config)
+
+            turbulenceNoiseView.updateColor(expectedColor)
+
+            fakeSystemClock.advanceTime(config.duration.toLong())
+
+            assertThat(config.color).isEqualTo(expectedColor)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt
new file mode 100644
index 0000000..633aac0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.surfaceeffects.turbulencenoise
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TurbulenceNoiseViewTest : SysuiTestCase() {
+
+    private val fakeSystemClock = FakeSystemClock()
+    // FakeExecutor is needed to run animator.
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Test
+    fun play_viewHasCorrectVisibility() {
+        val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+        val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+        assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+
+        fakeExecutor.execute {
+            turbulenceNoiseView.play(config)
+
+            assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
+
+            fakeSystemClock.advanceTime(config.duration.toLong())
+
+            assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+        }
+    }
+
+    @Test
+    fun play_playsAnimation() {
+        val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+        val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+        fakeExecutor.execute {
+            turbulenceNoiseView.play(config)
+
+            assertThat(turbulenceNoiseView.isPlaying).isTrue()
+        }
+    }
+
+    @Test
+    fun play_onEnd_triggersOnAnimationEnd() {
+        var animationEnd = false
+        val config =
+            TurbulenceNoiseAnimationConfig(
+                duration = 1000f,
+                onAnimationEnd = { animationEnd = true }
+            )
+        val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+        fakeExecutor.execute {
+            turbulenceNoiseView.play(config)
+
+            assertThat(turbulenceNoiseView.isPlaying).isTrue()
+
+            fakeSystemClock.advanceTime(config.duration.toLong())
+
+            assertThat(animationEnd).isTrue()
+        }
+    }
+}
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..09f0d4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -35,12 +35,15 @@
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.wakelock.WakeLock
+import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 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
@@ -52,6 +55,9 @@
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
 
+    private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
+    private lateinit var fakeWakeLock: WakeLockFake
+
     @Mock
     private lateinit var logger: TemporaryViewLogger
     @Mock
@@ -73,6 +79,10 @@
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
+        fakeWakeLock = WakeLockFake()
+        fakeWakeLockBuilder = WakeLockFake.Builder(context)
+        fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
+
         underTest = TestController(
                 context,
                 logger,
@@ -81,32 +91,71 @@
                 accessibilityManager,
                 configurationController,
                 powerManager,
+                fakeWakeLockBuilder,
         )
+        underTest.start()
     }
 
     @Test
-    fun displayView_viewAdded() {
+    fun displayView_viewAddedWithCorrectTitle() {
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "Fake Window Title",
+            )
+        )
+
+        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("id", "Fake Window Title")
+    }
+
+    @Test
+    fun displayView_wakeLockAcquired() {
         underTest.displayView(getState())
 
-        verify(windowManager).addView(any(), any())
+        assertThat(fakeWakeLock.isHeld).isTrue()
     }
 
     @Test
-    fun displayView_screenOff_screenWakes() {
-        whenever(powerManager.isScreenOn).thenReturn(false)
-
-        underTest.displayView(getState())
-
-        verify(powerManager).wakeUp(any(), any(), any())
-    }
-
-    @Test
-    fun displayView_screenAlreadyOn_screenNotWoken() {
+    fun displayView_screenAlreadyOn_wakeLockAcquired() {
         whenever(powerManager.isScreenOn).thenReturn(true)
 
         underTest.displayView(getState())
 
-        verify(powerManager, never()).wakeUp(any(), any(), any())
+        assertThat(fakeWakeLock.isHeld).isTrue()
+    }
+
+    @Test
+    fun displayView_wakeLockCanBeReleasedAfterTimeOut() {
+        underTest.displayView(getState())
+        assertThat(fakeWakeLock.isHeld).isTrue()
+
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
+    }
+
+    @Test
+    fun displayView_removeView_wakeLockCanBeReleased() {
+        underTest.displayView(getState())
+        assertThat(fakeWakeLock.isHeld).isTrue()
+
+        underTest.removeView("id", "test reason")
+
+        assertThat(fakeWakeLock.isHeld).isFalse()
     }
 
     @Test
@@ -119,6 +168,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)
@@ -188,21 +263,143 @@
     }
 
     @Test
+    fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() {
+        underTest.displayView(ViewInfo("First name", id = "id1"))
+
+        verify(windowManager).addView(any(), any())
+
+        reset(windowManager)
+        underTest.displayView(ViewInfo("Second name", id = "id2"))
+        underTest.removeView("id2", "test reason")
+
+        verify(windowManager).removeView(any())
+
+        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+        assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+        assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+
+        reset(windowManager)
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        verify(windowManager).removeView(any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun multipleViewsWithDifferentIds_oldViewRemoved_recentViewIsDisplayed() {
+        underTest.displayView(ViewInfo("First name", id = "id1"))
+
+        verify(windowManager).addView(any(), any())
+
+        reset(windowManager)
+        underTest.displayView(ViewInfo("Second name", id = "id2"))
+        underTest.removeView("id1", "test reason")
+
+        verify(windowManager, never()).removeView(any())
+        assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
+        assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        verify(windowManager).removeView(any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun multipleViewsWithDifferentIds_threeDifferentViews_recentActiveViewIsDisplayed() {
+        underTest.displayView(ViewInfo("First name", id = "id1"))
+        underTest.displayView(ViewInfo("Second name", id = "id2"))
+        underTest.displayView(ViewInfo("Third name", id = "id3"))
+
+        verify(windowManager).addView(any(), any())
+
+        reset(windowManager)
+        underTest.removeView("id3", "test reason")
+
+        verify(windowManager).removeView(any())
+
+        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+        assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2")
+        assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name")
+
+        reset(windowManager)
+        underTest.removeView("id2", "test reason")
+
+        verify(windowManager).removeView(any())
+
+        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+        assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+        assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
+
+        reset(windowManager)
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        verify(windowManager).removeView(any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun multipleViewsWithDifferentIds_oneViewStateChanged_stackHasRecentState() {
+        underTest.displayView(ViewInfo("First name", id = "id1"))
+        underTest.displayView(ViewInfo("New name", id = "id1"))
+
+        verify(windowManager).addView(any(), any())
+
+        reset(windowManager)
+        underTest.displayView(ViewInfo("Second name", id = "id2"))
+        underTest.removeView("id2", "test reason")
+
+        verify(windowManager).removeView(any())
+
+        fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
+
+        assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
+        assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name")
+        assertThat(underTest.activeViews[0].second.name).isEqualTo("New name")
+
+        reset(windowManager)
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        verify(windowManager).removeView(any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
+    fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() {
+        underTest.displayView(ViewInfo("First name", id = "id1"))
+        fakeClock.advanceTime(TIMEOUT_MS / 3)
+        underTest.displayView(ViewInfo("Second name", id = "id2"))
+        fakeClock.advanceTime(TIMEOUT_MS / 3)
+        underTest.displayView(ViewInfo("Third name", id = "id3"))
+
+        reset(windowManager)
+        fakeClock.advanceTime(TIMEOUT_MS + 1)
+
+        verify(windowManager).removeView(any())
+        verify(windowManager, never()).addView(any(), any())
+        assertThat(underTest.activeViews.size).isEqualTo(0)
+    }
+
+    @Test
     fun removeView_viewRemovedAndRemovalLogged() {
         // First, add the view
         underTest.displayView(getState())
 
         // Then, remove it
         val reason = "test reason"
-        underTest.removeView(reason)
+        val deviceId = "id"
+        underTest.removeView(deviceId, reason)
 
         verify(windowManager).removeView(any())
-        verify(logger).logChipRemoval(reason)
+        verify(logger).logViewRemoval(deviceId, reason)
     }
 
     @Test
     fun removeView_noAdd_viewNotRemoved() {
-        underTest.removeView("reason")
+        underTest.removeView("id", "reason")
 
         verify(windowManager, never()).removeView(any())
     }
@@ -223,6 +420,7 @@
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
+        wakeLockBuilder: WakeLock.Builder,
     ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>(
         context,
         logger,
@@ -232,15 +430,12 @@
         configurationController,
         powerManager,
         R.layout.chipbar,
-        "Window Title",
-        "WAKE_REASON",
+        wakeLockBuilder,
     ) {
         var mostRecentViewInfo: ViewInfo? = null
 
         override val windowLayoutParams = commonWindowLayoutParams
 
-        override fun start() {}
-
         override fun updateView(newInfo: ViewInfo, currentView: ViewGroup) {
             mostRecentViewInfo = newInfo
         }
@@ -248,11 +443,17 @@
         override fun getTouchableRegion(view: View, outRect: Rect) {
             outRect.setEmpty()
         }
+
+        override fun start() {}
     }
 
-    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,
+        override val id: String = "id",
+    ) : 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 c9f2b4d..116b8fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -43,20 +43,22 @@
     }
 
     @Test
-    fun logChipAddition_bufferHasLog() {
-        logger.logChipAddition()
+    fun logViewAddition_bufferHasLog() {
+        logger.logViewAddition("test id", "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)
+        val deviceId = "test id"
+        logger.logViewRemoval(deviceId, reason)
 
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
@@ -64,6 +66,7 @@
 
         assertThat(actualString).contains(TAG)
         assertThat(actualString).contains(reason)
+        assertThat(actualString).contains(deviceId)
     }
 }
 
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 6225d0c..47c84ab 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
@@ -16,11 +16,8 @@
 
 package com.android.systemui.temporarydisplay.chipbar
 
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -31,21 +28,22 @@
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.sender.ChipStateSender
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents
+import com.android.systemui.common.shared.model.ContentDescription
+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.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.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -53,7 +51,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -64,437 +61,345 @@
 class ChipbarCoordinatorTest : SysuiTestCase() {
     private lateinit var underTest: FakeChipbarCoordinator
 
-    @Mock
-    private lateinit var packageManager: PackageManager
-    @Mock
-    private lateinit var applicationInfo: ApplicationInfo
-    @Mock
-    private lateinit var logger: MediaTttLogger
-    @Mock
-    private lateinit var accessibilityManager: AccessibilityManager
-    @Mock
-    private lateinit var configurationController: ConfigurationController
-    @Mock
-    private lateinit var powerManager: PowerManager
-    @Mock
-    private lateinit var windowManager: WindowManager
-    @Mock
-    private lateinit var falsingManager: FalsingManager
-    @Mock
-    private lateinit var falsingCollector: FalsingCollector
-    @Mock
-    private lateinit var viewUtil: ViewUtil
-    private lateinit var fakeAppIconDrawable: Drawable
+    @Mock private lateinit var logger: ChipbarLogger
+    @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var powerManager: PowerManager
+    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var viewUtil: ViewUtil
+    @Mock private lateinit var vibratorHelper: VibratorHelper
+    private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
+    private lateinit var fakeWakeLock: WakeLockFake
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
-    private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
-        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
-        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
-        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
-        whenever(packageManager.getApplicationInfo(
-            eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>()
-        )).thenReturn(applicationInfo)
-        context.setMockPackageManager(packageManager)
+        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
+        fakeWakeLock = WakeLockFake()
+        fakeWakeLockBuilder = WakeLockFake.Builder(context)
+        fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
+
         uiEventLoggerFake = UiEventLoggerFake()
-        senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
-        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
-
-        underTest = FakeChipbarCoordinator(
-            context,
-            logger,
-            windowManager,
-            fakeExecutor,
-            accessibilityManager,
-            configurationController,
-            powerManager,
-            senderUiEventLogger,
-            falsingManager,
-            falsingCollector,
-            viewUtil,
-        )
+        underTest =
+            FakeChipbarCoordinator(
+                context,
+                logger,
+                windowManager,
+                fakeExecutor,
+                accessibilityManager,
+                configurationController,
+                powerManager,
+                falsingManager,
+                falsingCollector,
+                viewUtil,
+                vibratorHelper,
+                fakeWakeLockBuilder,
+            )
         underTest.start()
     }
 
     @Test
-    fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToStartCast()
-        underTest.displayView(state)
+    fun displayView_loadedIcon_correctlyRendered() {
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
 
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToEndCast()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToReceiverTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToThisDeviceTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToReceiverSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToReceiverSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isFalse()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToThisDeviceSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToReceiverFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToThisDeviceFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() {
-        underTest.displayView(almostCloseToStartCast())
-        underTest.displayView(transferToReceiverTriggered())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverSucceeded())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
-        underTest.displayView(transferToReceiverTriggered())
         underTest.displayView(
-            transferToReceiverSucceeded(
-                object : IUndoMediaTransferCallback.Stub() {
-                    override fun onUndoTriggered() {}
-                }
+            createChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("text"),
+                endItem = null,
             )
         )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.drawable).isEqualTo(drawable)
+        assertThat(iconView.contentDescription).isEqualTo("loadedCD")
     }
 
     @Test
-    fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() {
-        underTest.displayView(transferToReceiverSucceeded())
-        underTest.displayView(almostCloseToStartCast())
+    fun displayView_resourceIcon_correctlyRendered() {
+        val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout)
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.contentDescription)
+            .isEqualTo(contentDescription.loadContentDescription(context))
     }
 
     @Test
-    fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverFailed())
+    fun displayView_loadedText_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("display view text here"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
     }
 
-    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
+    @Test
+    fun displayView_resourceText_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Resource(R.string.screenrecord_start_error),
+                endItem = null,
+            )
+        )
+
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(context.getString(R.string.screenrecord_start_error))
+    }
+
+    @Test
+    fun displayView_endItemNull_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemLoading_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemError_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Error,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemButton_correctlyRendered() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        onClickListener = {},
+                    ),
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().text).isEqualTo("button text")
+        assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_falseTap_listenerNotRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isFalse()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_notFalseTap_listenerRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isTrue()
+    }
+
+    @Test
+    fun displayView_vibrationEffect_doubleClickEffect() {
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+                vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
+            )
+        )
+
+        verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
+    }
+
+    @Test
+    fun updateView_viewUpdated() {
+        // First, display a view
+        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,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD")
+        assertThat(chipbarView.getChipText()).isEqualTo("title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+
+        // WHEN the view is updated
+        val newDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        underTest.updateView(
+            createChipbarInfo(
+                Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")),
+                Text.Loaded("new title text"),
+                endItem = ChipbarEndItem.Error,
+            ),
+            chipbarView
+        )
+
+        // THEN we display the new view
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD")
+        assertThat(chipbarView.getChipText()).isEqualTo("new title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        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,
+            id = DEVICE_ID,
+        )
+    }
+
+    private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
 
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    private fun ViewGroup.getLoadingIconVisibility(): Int =
-        this.requireViewById<View>(R.id.loading).visibility
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo)
+    private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button)
 
-    private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    private fun getChipView(): ViewGroup {
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
-
-    // TODO(b/245610654): For now, the below methods are duplicated between this test and
-    //   [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator],
-    //   these will no longer be duplicated.
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
 }
 
-private const val APP_NAME = "Fake app name"
-private const val OTHER_DEVICE_NAME = "My Tablet"
-private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
-
-private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
-    .addFeature("feature")
-    .setClientPackageName(PACKAGE_NAME)
-    .build()
+private const val WINDOW_TITLE = "Test Chipbar Window Title"
+private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON"
+private const val DEVICE_ID = "id"
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 10704ac..beedf9f 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,27 +22,27 @@
 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.media.taptotransfer.sender.MediaTttSenderUiEventLogger
 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.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
+import com.android.systemui.util.wakelock.WakeLock
 
 /** A fake implementation of [ChipbarCoordinator] for testing. */
 class FakeChipbarCoordinator(
     context: Context,
-    @MediaTttReceiverLogger logger: MediaTttLogger,
+    logger: ChipbarLogger,
     windowManager: WindowManager,
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
     configurationController: ConfigurationController,
     powerManager: PowerManager,
-    uiEventLogger: MediaTttSenderUiEventLogger,
     falsingManager: FalsingManager,
     falsingCollector: FalsingCollector,
     viewUtil: ViewUtil,
+    vibratorHelper: VibratorHelper,
+    wakeLockBuilder: WakeLock.Builder,
 ) :
     ChipbarCoordinator(
         context,
@@ -52,10 +52,11 @@
         accessibilityManager,
         configurationController,
         powerManager,
-        uiEventLogger,
         falsingManager,
         falsingCollector,
         viewUtil,
+        vibratorHelper,
+        wakeLockBuilder,
     ) {
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index e18dd3a..7d5f06c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -140,6 +140,40 @@
     }
 
     @Test
+    fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() {
+        setFoldState(folded = true)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        sendHingeAngleEvent(10)
+        sendHingeAngleEvent(20)
+        sendHingeAngleEvent(10)
+        screenOnStatusProvider.notifyScreenTurnedOn()
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+    }
+
+    @Test
+    fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() {
+        setFoldState(folded = true)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        sendHingeAngleEvent(10)
+        sendHingeAngleEvent(20)
+        screenOnStatusProvider.notifyScreenTurnedOn()
+        sendHingeAngleEvent(30)
+        sendHingeAngleEvent(40)
+        sendHingeAngleEvent(10)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
     fun testOnFolded_stopsHingeAngleProvider() {
         setFoldState(folded = true)
 
@@ -237,7 +271,7 @@
     }
 
     @Test
-    fun startClosingEvent_afterTimeout_abortEmitted() {
+    fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() {
         sendHingeAngleEvent(90)
         sendHingeAngleEvent(80)
 
@@ -269,7 +303,7 @@
     }
 
     @Test
-    fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() {
+    fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() {
         sendHingeAngleEvent(180)
         sendHingeAngleEvent(90)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
new file mode 100644
index 0000000..51afbcb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt
@@ -0,0 +1,55 @@
+package com.android.systemui.user
+
+import android.app.Dialog
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class CreateUserActivityTest : SysuiTestCase() {
+    open class CreateUserActivityTestable :
+        CreateUserActivity(
+            /* userCreator = */ mock(),
+            /* editUserInfoController = */ mock {
+                val dialog: Dialog = mock()
+                whenever(
+                        createDialog(
+                            /* activity = */ nullable(),
+                            /* activityStarter = */ nullable(),
+                            /* oldUserIcon = */ nullable(),
+                            /* defaultUserName = */ nullable(),
+                            /* title = */ nullable(),
+                            /* successCallback = */ nullable(),
+                            /* cancelCallback = */ nullable()
+                        )
+                    )
+                    .thenReturn(dialog)
+            },
+            /* activityManager = */ mock(),
+            /* activityStarter = */ mock(),
+        )
+
+    @get:Rule val activityRule = ActivityScenarioRule(CreateUserActivityTestable::class.java)
+
+    @Test
+    fun onBackPressed_finishActivity() {
+        activityRule.scenario.onActivity { activity ->
+            assertThat(activity.isFinishing).isFalse()
+
+            activity.onBackPressed()
+
+            assertThat(activity.isFinishing).isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
deleted file mode 100644
index d951f36..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
+++ /dev/null
@@ -1,225 +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.user.data.repository
-
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.os.UserManager
-import android.provider.Settings
-import androidx.test.filters.SmallTest
-import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
-
-    @Before
-    fun setUp() {
-        super.setUp(isRefactored = true)
-    }
-
-    @Test
-    fun userSwitcherSettings() = runSelfCancelingTest {
-        setUpGlobalSettings(
-            isSimpleUserSwitcher = true,
-            isAddUsersFromLockscreen = true,
-            isUserSwitcherEnabled = true,
-        )
-        underTest = create(this)
-
-        var value: UserSwitcherSettingsModel? = null
-        underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
-
-        assertUserSwitcherSettings(
-            model = value,
-            expectedSimpleUserSwitcher = true,
-            expectedAddUsersFromLockscreen = true,
-            expectedUserSwitcherEnabled = true,
-        )
-
-        setUpGlobalSettings(
-            isSimpleUserSwitcher = false,
-            isAddUsersFromLockscreen = true,
-            isUserSwitcherEnabled = true,
-        )
-        assertUserSwitcherSettings(
-            model = value,
-            expectedSimpleUserSwitcher = false,
-            expectedAddUsersFromLockscreen = true,
-            expectedUserSwitcherEnabled = true,
-        )
-    }
-
-    @Test
-    fun refreshUsers() = runSelfCancelingTest {
-        underTest = create(this)
-        val initialExpectedValue =
-            setUpUsers(
-                count = 3,
-                selectedIndex = 0,
-            )
-        var userInfos: List<UserInfo>? = null
-        var selectedUserInfo: UserInfo? = null
-        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
-        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
-
-        underTest.refreshUsers()
-        assertThat(userInfos).isEqualTo(initialExpectedValue)
-        assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
-        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
-
-        val secondExpectedValue =
-            setUpUsers(
-                count = 4,
-                selectedIndex = 1,
-            )
-        underTest.refreshUsers()
-        assertThat(userInfos).isEqualTo(secondExpectedValue)
-        assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
-        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
-
-        val selectedNonGuestUserId = selectedUserInfo?.id
-        val thirdExpectedValue =
-            setUpUsers(
-                count = 2,
-                hasGuest = true,
-                selectedIndex = 1,
-            )
-        underTest.refreshUsers()
-        assertThat(userInfos).isEqualTo(thirdExpectedValue)
-        assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
-        assertThat(selectedUserInfo?.isGuest).isTrue()
-        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
-    }
-
-    @Test
-    fun `refreshUsers - sorts by creation time`() = runSelfCancelingTest {
-        underTest = create(this)
-        val unsortedUsers =
-            setUpUsers(
-                count = 3,
-                selectedIndex = 0,
-            )
-        unsortedUsers[0].creationTime = 900
-        unsortedUsers[1].creationTime = 700
-        unsortedUsers[2].creationTime = 999
-        val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2])
-        var userInfos: List<UserInfo>? = null
-        var selectedUserInfo: UserInfo? = null
-        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
-        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
-
-        underTest.refreshUsers()
-        assertThat(userInfos).isEqualTo(expectedUsers)
-    }
-
-    private fun setUpUsers(
-        count: Int,
-        hasGuest: Boolean = false,
-        selectedIndex: Int = 0,
-    ): List<UserInfo> {
-        val userInfos =
-            (0 until count).map { index ->
-                createUserInfo(
-                    index,
-                    isGuest = hasGuest && index == count - 1,
-                )
-            }
-        whenever(manager.aliveUsers).thenReturn(userInfos)
-        tracker.set(userInfos, selectedIndex)
-        return userInfos
-    }
-
-    private fun createUserInfo(
-        id: Int,
-        isGuest: Boolean,
-    ): UserInfo {
-        val flags = 0
-        return UserInfo(
-            id,
-            "user_$id",
-            /* iconPath= */ "",
-            flags,
-            if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
-        )
-    }
-
-    private fun setUpGlobalSettings(
-        isSimpleUserSwitcher: Boolean = false,
-        isAddUsersFromLockscreen: Boolean = false,
-        isUserSwitcherEnabled: Boolean = true,
-    ) {
-        context.orCreateTestableResources.addOverride(
-            com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
-            true,
-        )
-        globalSettings.putIntForUser(
-            UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
-            if (isSimpleUserSwitcher) 1 else 0,
-            UserHandle.USER_SYSTEM,
-        )
-        globalSettings.putIntForUser(
-            Settings.Global.ADD_USERS_WHEN_LOCKED,
-            if (isAddUsersFromLockscreen) 1 else 0,
-            UserHandle.USER_SYSTEM,
-        )
-        globalSettings.putIntForUser(
-            Settings.Global.USER_SWITCHER_ENABLED,
-            if (isUserSwitcherEnabled) 1 else 0,
-            UserHandle.USER_SYSTEM,
-        )
-    }
-
-    private fun assertUserSwitcherSettings(
-        model: UserSwitcherSettingsModel?,
-        expectedSimpleUserSwitcher: Boolean,
-        expectedAddUsersFromLockscreen: Boolean,
-        expectedUserSwitcherEnabled: Boolean,
-    ) {
-        checkNotNull(model)
-        assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
-        assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
-        assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
-    }
-
-    /**
-     * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
-     * is then automatically canceled and cleaned-up.
-     */
-    private fun runSelfCancelingTest(
-        block: suspend CoroutineScope.() -> Unit,
-    ) =
-        runBlocking(Dispatchers.Main.immediate) {
-            val scope = CoroutineScope(coroutineContext + Job())
-            block(scope)
-            scope.cancel()
-        }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index dcea83a..2e527be1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,54 +17,263 @@
 
 package com.android.systemui.user.data.repository
 
+import android.content.pm.UserInfo
+import android.os.UserHandle
 import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.settings.FakeUserTracker
-import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
-abstract class UserRepositoryImplTest : SysuiTestCase() {
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplTest : SysuiTestCase() {
 
-    @Mock protected lateinit var manager: UserManager
-    @Mock protected lateinit var controller: UserSwitcherController
+    @Mock private lateinit var manager: UserManager
 
-    protected lateinit var underTest: UserRepositoryImpl
+    private lateinit var underTest: UserRepositoryImpl
 
-    protected lateinit var globalSettings: FakeSettings
-    protected lateinit var tracker: FakeUserTracker
-    protected lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var globalSettings: FakeSettings
+    private lateinit var tracker: FakeUserTracker
 
-    protected fun setUp(isRefactored: Boolean) {
+    @Before
+    fun setUp() {
         MockitoAnnotations.initMocks(this)
 
         globalSettings = FakeSettings()
         tracker = FakeUserTracker()
-        featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
     }
 
-    protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+    @Test
+    fun userSwitcherSettings() = runSelfCancelingTest {
+        setUpGlobalSettings(
+            isSimpleUserSwitcher = true,
+            isAddUsersFromLockscreen = true,
+            isUserSwitcherEnabled = true,
+        )
+        underTest = create(this)
+
+        var value: UserSwitcherSettingsModel? = null
+        underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
+
+        assertUserSwitcherSettings(
+            model = value,
+            expectedSimpleUserSwitcher = true,
+            expectedAddUsersFromLockscreen = true,
+            expectedUserSwitcherEnabled = true,
+        )
+
+        setUpGlobalSettings(
+            isSimpleUserSwitcher = false,
+            isAddUsersFromLockscreen = true,
+            isUserSwitcherEnabled = true,
+        )
+        assertUserSwitcherSettings(
+            model = value,
+            expectedSimpleUserSwitcher = false,
+            expectedAddUsersFromLockscreen = true,
+            expectedUserSwitcherEnabled = true,
+        )
+    }
+
+    @Test
+    fun refreshUsers() = runSelfCancelingTest {
+        underTest = create(this)
+        val initialExpectedValue =
+            setUpUsers(
+                count = 3,
+                selectedIndex = 0,
+            )
+        var userInfos: List<UserInfo>? = null
+        var selectedUserInfo: UserInfo? = null
+        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(initialExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+        val secondExpectedValue =
+            setUpUsers(
+                count = 4,
+                selectedIndex = 1,
+            )
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(secondExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+        val selectedNonGuestUserId = selectedUserInfo?.id
+        val thirdExpectedValue =
+            setUpUsers(
+                count = 2,
+                isLastGuestUser = true,
+                selectedIndex = 1,
+            )
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(thirdExpectedValue)
+        assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
+        assertThat(selectedUserInfo?.isGuest).isTrue()
+        assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+    }
+
+    @Test
+    fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest {
+        underTest = create(this)
+        val unsortedUsers =
+            setUpUsers(
+                count = 3,
+                selectedIndex = 0,
+                isLastGuestUser = true,
+            )
+        unsortedUsers[0].creationTime = 999
+        unsortedUsers[1].creationTime = 900
+        unsortedUsers[2].creationTime = 950
+        val expectedUsers =
+            listOf(
+                unsortedUsers[1],
+                unsortedUsers[0],
+                unsortedUsers[2], // last because this is the guest
+            )
+        var userInfos: List<UserInfo>? = null
+        underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+
+        underTest.refreshUsers()
+        assertThat(userInfos).isEqualTo(expectedUsers)
+    }
+
+    private fun setUpUsers(
+        count: Int,
+        isLastGuestUser: Boolean = false,
+        selectedIndex: Int = 0,
+    ): List<UserInfo> {
+        val userInfos =
+            (0 until count).map { index ->
+                createUserInfo(
+                    index,
+                    isGuest = isLastGuestUser && index == count - 1,
+                )
+            }
+        whenever(manager.aliveUsers).thenReturn(userInfos)
+        tracker.set(userInfos, selectedIndex)
+        return userInfos
+    }
+    @Test
+    fun `userTrackerCallback - updates selectedUserInfo`() = runSelfCancelingTest {
+        underTest = create(this)
+        var selectedUserInfo: UserInfo? = null
+        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+        setUpUsers(
+            count = 2,
+            selectedIndex = 0,
+        )
+        tracker.onProfileChanged()
+        assertThat(selectedUserInfo?.id).isEqualTo(0)
+        setUpUsers(
+            count = 2,
+            selectedIndex = 1,
+        )
+        tracker.onProfileChanged()
+        assertThat(selectedUserInfo?.id).isEqualTo(1)
+    }
+
+    private fun createUserInfo(
+        id: Int,
+        isGuest: Boolean,
+    ): UserInfo {
+        val flags = 0
+        return UserInfo(
+            id,
+            "user_$id",
+            /* iconPath= */ "",
+            flags,
+            if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
+        )
+    }
+
+    private fun setUpGlobalSettings(
+        isSimpleUserSwitcher: Boolean = false,
+        isAddUsersFromLockscreen: Boolean = false,
+        isUserSwitcherEnabled: Boolean = true,
+    ) {
+        context.orCreateTestableResources.addOverride(
+            com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
+            true,
+        )
+        globalSettings.putIntForUser(
+            UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
+            if (isSimpleUserSwitcher) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+        globalSettings.putIntForUser(
+            Settings.Global.ADD_USERS_WHEN_LOCKED,
+            if (isAddUsersFromLockscreen) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+        globalSettings.putIntForUser(
+            Settings.Global.USER_SWITCHER_ENABLED,
+            if (isUserSwitcherEnabled) 1 else 0,
+            UserHandle.USER_SYSTEM,
+        )
+    }
+
+    private fun assertUserSwitcherSettings(
+        model: UserSwitcherSettingsModel?,
+        expectedSimpleUserSwitcher: Boolean,
+        expectedAddUsersFromLockscreen: Boolean,
+        expectedUserSwitcherEnabled: Boolean,
+    ) {
+        checkNotNull(model)
+        assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
+        assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
+        assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
+    }
+
+    /**
+     * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
+     * is then automatically canceled and cleaned-up.
+     */
+    private fun runSelfCancelingTest(
+        block: suspend CoroutineScope.() -> Unit,
+    ) =
+        runBlocking(Dispatchers.Main.immediate) {
+            val scope = CoroutineScope(coroutineContext + Job())
+            block(scope)
+            scope.cancel()
+        }
+
+    private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
         return UserRepositoryImpl(
             appContext = context,
             manager = manager,
-            controller = controller,
             applicationScope = scope,
             mainDispatcher = IMMEDIATE,
             backgroundDispatcher = IMMEDIATE,
             globalSettings = globalSettings,
             tracker = tracker,
-            featureFlags = featureFlags,
         )
     }
 
     companion object {
-        @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
+        @JvmStatic private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
deleted file mode 100644
index d4b41c1..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
+++ /dev/null
@@ -1,205 +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.user.data.repository
-
-import android.content.pm.UserInfo
-import androidx.test.filters.SmallTest
-import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-    }
-
-    @Captor
-    private lateinit var userSwitchCallbackCaptor:
-        ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
-
-    @Before
-    fun setUp() {
-        super.setUp(isRefactored = false)
-
-        whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
-        whenever(controller.isGuestUserAutoCreated).thenReturn(false)
-        whenever(controller.isGuestUserResetting).thenReturn(false)
-
-        underTest = create()
-    }
-
-    @Test
-    fun `users - registers for updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.users.onEach {}.launchIn(this)
-
-            verify(controller).addUserSwitchCallback(any())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `users - unregisters from updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.users.onEach {}.launchIn(this)
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
-            job.cancel()
-
-            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
-        }
-
-    @Test
-    fun `users - does not include actions`() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createActionRecord(UserActionModel.ADD_USER),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
-                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
-                    )
-                )
-            var models: List<UserModel>? = null
-            val job = underTest.users.onEach { models = it }.launchIn(this)
-
-            assertThat(models).hasSize(3)
-            assertThat(models?.get(0)?.id).isEqualTo(0)
-            assertThat(models?.get(0)?.isSelected).isTrue()
-            assertThat(models?.get(1)?.id).isEqualTo(1)
-            assertThat(models?.get(1)?.isSelected).isFalse()
-            assertThat(models?.get(2)?.id).isEqualTo(2)
-            assertThat(models?.get(2)?.isSelected).isFalse()
-            job.cancel()
-        }
-
-    @Test
-    fun selectedUser() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                    )
-                )
-            var id: Int? = null
-            val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
-
-            assertThat(id).isEqualTo(0)
-
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0),
-                        createUserRecord(1),
-                        createUserRecord(2, isSelected = true),
-                    )
-                )
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-            userSwitchCallbackCaptor.value.onUserSwitched()
-            assertThat(id).isEqualTo(2)
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - unregisters from updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.actions.onEach {}.launchIn(this)
-            verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
-            job.cancel()
-
-            verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
-        }
-
-    @Test
-    fun `actions - registers for updates`() =
-        runBlocking(IMMEDIATE) {
-            val job = underTest.actions.onEach {}.launchIn(this)
-
-            verify(controller).addUserSwitchCallback(any())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - does not include users`() =
-        runBlocking(IMMEDIATE) {
-            whenever(controller.users)
-                .thenReturn(
-                    arrayListOf(
-                        createUserRecord(0, isSelected = true),
-                        createActionRecord(UserActionModel.ADD_USER),
-                        createUserRecord(1),
-                        createUserRecord(2),
-                        createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
-                        createActionRecord(UserActionModel.ENTER_GUEST_MODE),
-                    )
-                )
-            var models: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { models = it }.launchIn(this)
-
-            assertThat(models).hasSize(3)
-            assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
-            assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
-            assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
-            job.cancel()
-        }
-
-    private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
-        return UserRecord(
-            info = UserInfo(id, "name$id", 0),
-            isCurrent = isSelected,
-        )
-    }
-
-    private fun createActionRecord(action: UserActionModel): UserRecord {
-        return UserRecord(
-            isAddUser = action == UserActionModel.ADD_USER,
-            isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
-            isGuest = action == UserActionModel.ENTER_GUEST_MODE,
-        )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
index 120bf79..fb781e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -23,6 +23,8 @@
 import android.os.UserManager
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.user.data.repository.FakeUserRepository
@@ -55,6 +57,8 @@
     @Mock private lateinit var dismissDialog: () -> Unit
     @Mock private lateinit var selectUser: (Int) -> Unit
     @Mock private lateinit var switchUser: (Int) -> Unit
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
     private lateinit var underTest: GuestUserInteractor
 
@@ -87,10 +91,18 @@
                         repository = repository,
                     ),
                 uiEventLogger = uiEventLogger,
+                resumeSessionReceiver = resumeSessionReceiver,
+                resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
     }
 
     @Test
+    fun `registers broadcast receivers`() {
+        verify(resumeSessionReceiver).register()
+        verify(resetOrExitSessionReceiver).register()
+    }
+
+    @Test
     fun `onDeviceBootCompleted - allowed to add - create guest`() =
         runBlocking(IMMEDIATE) {
             setAllowedToAdd()
@@ -219,6 +231,7 @@
             repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
             repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
             val targetUserId = NON_GUEST_USER_INFO.id
+            val ephemeralGuestUserHandle = UserHandle.of(EPHEMERAL_GUEST_USER_INFO.id)
 
             underTest.exit(
                 guestUserId = GUEST_USER_INFO.id,
@@ -230,7 +243,7 @@
             )
 
             verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
-            verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
+            verify(manager).removeUserWhenPossible(ephemeralGuestUserHandle, false)
             verify(switchUser).invoke(targetUserId)
         }
 
@@ -240,6 +253,7 @@
             whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
             repository.setSelectedUserInfo(GUEST_USER_INFO)
             val targetUserId = NON_GUEST_USER_INFO.id
+            val guestUserHandle = UserHandle.of(GUEST_USER_INFO.id)
 
             underTest.exit(
                 guestUserId = GUEST_USER_INFO.id,
@@ -251,7 +265,7 @@
             )
 
             verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
-            verify(manager).removeUser(GUEST_USER_INFO.id)
+            verify(manager).removeUserWhenPossible(guestUserHandle, false)
             verify(switchUser).invoke(targetUserId)
         }
 
@@ -296,6 +310,7 @@
             repository.setSelectedUserInfo(GUEST_USER_INFO)
 
             val targetUserId = NON_GUEST_USER_INFO.id
+            val guestUserHandle = UserHandle.of(GUEST_USER_INFO.id)
             underTest.remove(
                 guestUserId = GUEST_USER_INFO.id,
                 targetUserId = targetUserId,
@@ -305,7 +320,7 @@
             )
 
             verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
-            verify(manager).removeUser(GUEST_USER_INFO.id)
+            verify(manager).removeUserWhenPossible(guestUserHandle, false)
             verify(switchUser).invoke(targetUserId)
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
deleted file mode 100644
index e80d516..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
+++ /dev/null
@@ -1,737 +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.user.domain.interactor
-
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.graphics.Bitmap
-import android.graphics.drawable.Drawable
-import android.os.UserHandle
-import android.os.UserManager
-import android.provider.Settings
-import androidx.test.filters.SmallTest
-import com.android.internal.R.drawable.ic_account_circle
-import com.android.systemui.R
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.domain.model.ShowDialogRequestModel
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.advanceUntilIdle
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-@SmallTest
-@RunWith(JUnit4::class)
-class UserInteractorRefactoredTest : UserInteractorTest() {
-
-    override fun isRefactored(): Boolean {
-        return true
-    }
-
-    @Before
-    override fun setUp() {
-        super.setUp()
-
-        overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
-        overrideResource(R.dimen.max_avatar_size, 10)
-        overrideResource(
-            com.android.internal.R.string.config_supervisedUserCreationPackage,
-            SUPERVISED_USER_CREATION_APP_PACKAGE,
-        )
-        whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
-        whenever(manager.canAddMoreUsers(any())).thenReturn(true)
-    }
-
-    @Test
-    fun `onRecordSelected - user`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
-
-            verify(dialogShower).dismiss()
-            verify(activityManager).switchUser(userInfos[1].id)
-            Unit
-        }
-
-    @Test
-    fun `onRecordSelected - switch to guest user`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = true)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            underTest.onRecordSelected(UserRecord(info = userInfos.last()))
-
-            verify(activityManager).switchUser(userInfos.last().id)
-            Unit
-        }
-
-    @Test
-    fun `onRecordSelected - enter guest mode`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
-            whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
-
-            underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
-
-            verify(dialogShower).dismiss()
-            verify(manager).createGuest(any())
-            Unit
-        }
-
-    @Test
-    fun `onRecordSelected - action`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = true)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
-
-            verify(dialogShower, never()).dismiss()
-            verify(activityStarter).startActivity(any(), anyBoolean())
-        }
-
-    @Test
-    fun `users - switcher enabled`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = true)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 3, includeGuest = true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun `users - switches to second user`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            userRepository.setSelectedUserInfo(userInfos[1])
-
-            assertUsers(models = value, count = 2, selectedIndex = 1)
-            job.cancel()
-        }
-
-    @Test
-    fun `users - switcher not enabled`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
-
-            var value: List<UserModel>? = null
-            val job = underTest.users.onEach { value = it }.launchIn(this)
-            assertUsers(models = value, count = 1)
-
-            job.cancel()
-        }
-
-    @Test
-    fun selectedUser() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-
-            var value: UserModel? = null
-            val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
-            assertUser(value, id = 0, isSelected = true)
-
-            userRepository.setSelectedUserInfo(userInfos[1])
-            assertUser(value, id = 1, isSelected = true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - device unlocked`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
-
-            assertThat(value)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - device unlocked user not primary - empty list`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[1])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
-
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - device unlocked user is guest - empty list`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = true)
-            assertThat(userInfos[1].isGuest).isTrue()
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[1])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
-
-            assertThat(value).isEqualTo(emptyList<UserActionModel>())
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - device locked add from lockscreen set - full list`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(
-                UserSwitcherSettingsModel(
-                    isUserSwitcherEnabled = true,
-                    isAddUsersFromLockscreen = true,
-                )
-            )
-            keyguardRepository.setKeyguardShowing(false)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
-
-            assertThat(value)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - device locked - only guest action and manage user is shown`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            keyguardRepository.setKeyguardShowing(true)
-            var value: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { value = it }.launchIn(this)
-
-            assertThat(value)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
-                    )
-                )
-
-            job.cancel()
-        }
-
-    @Test
-    fun `executeAction - add user - dialog shown`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            keyguardRepository.setKeyguardShowing(false)
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
-
-            underTest.executeAction(UserActionModel.ADD_USER)
-            assertThat(dialogRequest)
-                .isEqualTo(
-                    ShowDialogRequestModel.ShowAddUserDialog(
-                        userHandle = userInfos[0].userHandle,
-                        isKeyguardShowing = false,
-                        showEphemeralMessage = false,
-                    )
-                )
-
-            underTest.onDialogShown()
-            assertThat(dialogRequest).isNull()
-
-            job.cancel()
-        }
-
-    @Test
-    fun `executeAction - add supervised user - starts activity`() =
-        runBlocking(IMMEDIATE) {
-            underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
-            val intentCaptor = kotlinArgumentCaptor<Intent>()
-            verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
-            assertThat(intentCaptor.value.action)
-                .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
-            assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
-        }
-
-    @Test
-    fun `executeAction - navigate to manage users`() =
-        runBlocking(IMMEDIATE) {
-            underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
-            val intentCaptor = kotlinArgumentCaptor<Intent>()
-            verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
-            assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
-        }
-
-    @Test
-    fun `executeAction - guest mode`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
-            whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
-            val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
-            val showDialogsJob =
-                underTest.dialogShowRequests
-                    .onEach {
-                        dialogRequests.add(it)
-                        if (it != null) {
-                            underTest.onDialogShown()
-                        }
-                    }
-                    .launchIn(this)
-            val dismissDialogsJob =
-                underTest.dialogDismissRequests
-                    .onEach {
-                        if (it != null) {
-                            underTest.onDialogDismissed()
-                        }
-                    }
-                    .launchIn(this)
-
-            underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
-            assertThat(dialogRequests)
-                .contains(
-                    ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
-                )
-            verify(activityManager).switchUser(guestUserInfo.id)
-
-            showDialogsJob.cancel()
-            dismissDialogsJob.cancel()
-        }
-
-    @Test
-    fun `selectUser - already selected guest re-selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = true)
-            val guestUserInfo = userInfos[1]
-            assertThat(guestUserInfo.isGuest).isTrue()
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(guestUserInfo)
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
-
-            underTest.selectUser(
-                newlySelectedUserId = guestUserInfo.id,
-                dialogShower = dialogShower,
-            )
-
-            assertThat(dialogRequest)
-                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
-            verify(dialogShower, never()).dismiss()
-            job.cancel()
-        }
-
-    @Test
-    fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = true)
-            val guestUserInfo = userInfos[1]
-            assertThat(guestUserInfo.isGuest).isTrue()
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(guestUserInfo)
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
-
-            underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
-
-            assertThat(dialogRequest)
-                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
-            verify(dialogShower, never()).dismiss()
-            job.cancel()
-        }
-
-    @Test
-    fun `selectUser - not currently guest - switches users`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            var dialogRequest: ShowDialogRequestModel? = null
-            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
-
-            underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
-
-            assertThat(dialogRequest).isNull()
-            verify(activityManager).switchUser(userInfos[1].id)
-            verify(dialogShower).dismiss()
-            job.cancel()
-        }
-
-    @Test
-    fun `Telephony call state changes - refreshes users`() =
-        runBlocking(IMMEDIATE) {
-            val refreshUsersCallCount = userRepository.refreshUsersCallCount
-
-            telephonyRepository.setCallState(1)
-
-            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
-        }
-
-    @Test
-    fun `User switched broadcast`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            val callback1: UserInteractor.UserCallback = mock()
-            val callback2: UserInteractor.UserCallback = mock()
-            underTest.addCallback(callback1)
-            underTest.addCallback(callback2)
-            val refreshUsersCallCount = userRepository.refreshUsersCallCount
-
-            userRepository.setSelectedUserInfo(userInfos[1])
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_SWITCHED)
-                        .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
-                )
-            }
-
-            verify(callback1).onUserStateChanged()
-            verify(callback2).onUserStateChanged()
-            assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
-            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
-        }
-
-    @Test
-    fun `User info changed broadcast`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            val refreshUsersCallCount = userRepository.refreshUsersCallCount
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_INFO_CHANGED),
-                )
-            }
-
-            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
-        }
-
-    @Test
-    fun `System user unlocked broadcast - refresh users`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            val refreshUsersCallCount = userRepository.refreshUsersCallCount
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_UNLOCKED)
-                        .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
-                )
-            }
-
-            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
-        }
-
-    @Test
-    fun `Non-system user unlocked broadcast - do not refresh users`() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 2, includeGuest = false)
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            val refreshUsersCallCount = userRepository.refreshUsersCallCount
-
-            fakeBroadcastDispatcher.registeredReceivers.forEach {
-                it.onReceive(
-                    context,
-                    Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
-                )
-            }
-
-            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
-        }
-
-    @Test
-    fun userRecords() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = false)
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            keyguardRepository.setKeyguardShowing(false)
-
-            testCoroutineScope.advanceUntilIdle()
-
-            assertRecords(
-                records = underTest.userRecords.value,
-                userIds = listOf(0, 1, 2),
-                selectedUserIndex = 0,
-                includeGuest = false,
-                expectedActions =
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    ),
-            )
-        }
-
-    @Test
-    fun selectedUserRecord() =
-        runBlocking(IMMEDIATE) {
-            val userInfos = createUserInfos(count = 3, includeGuest = true)
-            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
-            userRepository.setUserInfos(userInfos)
-            userRepository.setSelectedUserInfo(userInfos[0])
-            keyguardRepository.setKeyguardShowing(false)
-
-            assertRecordForUser(
-                record = underTest.selectedUserRecord.value,
-                id = 0,
-                hasPicture = true,
-                isCurrent = true,
-                isSwitchToEnabled = true,
-            )
-        }
-
-    private fun assertUsers(
-        models: List<UserModel>?,
-        count: Int,
-        selectedIndex: Int = 0,
-        includeGuest: Boolean = false,
-    ) {
-        checkNotNull(models)
-        assertThat(models.size).isEqualTo(count)
-        models.forEachIndexed { index, model ->
-            assertUser(
-                model = model,
-                id = index,
-                isSelected = index == selectedIndex,
-                isGuest = includeGuest && index == count - 1
-            )
-        }
-    }
-
-    private fun assertUser(
-        model: UserModel?,
-        id: Int,
-        isSelected: Boolean = false,
-        isGuest: Boolean = false,
-    ) {
-        checkNotNull(model)
-        assertThat(model.id).isEqualTo(id)
-        assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
-        assertThat(model.isSelected).isEqualTo(isSelected)
-        assertThat(model.isSelectable).isTrue()
-        assertThat(model.isGuest).isEqualTo(isGuest)
-    }
-
-    private fun assertRecords(
-        records: List<UserRecord>,
-        userIds: List<Int>,
-        selectedUserIndex: Int = 0,
-        includeGuest: Boolean = false,
-        expectedActions: List<UserActionModel> = emptyList(),
-    ) {
-        assertThat(records.size >= userIds.size).isTrue()
-        userIds.indices.forEach { userIndex ->
-            val record = records[userIndex]
-            assertThat(record.info).isNotNull()
-            val isGuest = includeGuest && userIndex == userIds.size - 1
-            assertRecordForUser(
-                record = record,
-                id = userIds[userIndex],
-                hasPicture = !isGuest,
-                isCurrent = userIndex == selectedUserIndex,
-                isGuest = isGuest,
-                isSwitchToEnabled = true,
-            )
-        }
-
-        assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
-        (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
-            val record = records[actionIndex]
-            assertThat(record.info).isNull()
-            assertRecordForAction(
-                record = record,
-                type = expectedActions[actionIndex - userIds.size],
-            )
-        }
-    }
-
-    private fun assertRecordForUser(
-        record: UserRecord?,
-        id: Int? = null,
-        hasPicture: Boolean = false,
-        isCurrent: Boolean = false,
-        isGuest: Boolean = false,
-        isSwitchToEnabled: Boolean = false,
-    ) {
-        checkNotNull(record)
-        assertThat(record.info?.id).isEqualTo(id)
-        assertThat(record.picture != null).isEqualTo(hasPicture)
-        assertThat(record.isCurrent).isEqualTo(isCurrent)
-        assertThat(record.isGuest).isEqualTo(isGuest)
-        assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
-    }
-
-    private fun assertRecordForAction(
-        record: UserRecord,
-        type: UserActionModel,
-    ) {
-        assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
-        assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
-        assertThat(record.isAddSupervisedUser)
-            .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
-    }
-
-    private fun createUserInfos(
-        count: Int,
-        includeGuest: Boolean,
-    ): List<UserInfo> {
-        return (0 until count).map { index ->
-            val isGuest = includeGuest && index == count - 1
-            createUserInfo(
-                id = index,
-                name =
-                    if (isGuest) {
-                        "guest"
-                    } else {
-                        "user_$index"
-                    },
-                isPrimary = !isGuest && index == 0,
-                isGuest = isGuest,
-            )
-        }
-    }
-
-    private fun createUserInfo(
-        id: Int,
-        name: String,
-        isPrimary: Boolean = false,
-        isGuest: Boolean = false,
-    ): UserInfo {
-        return UserInfo(
-            id,
-            name,
-            /* iconPath= */ "",
-            /* flags= */ if (isPrimary) {
-                UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
-            } else {
-                0
-            },
-            if (isGuest) {
-                UserManager.USER_TYPE_FULL_GUEST
-            } else {
-                UserManager.USER_TYPE_FULL_SYSTEM
-            },
-        )
-    }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
-        private val GUEST_ICON: Drawable = mock()
-        private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index 1680c36c..4b49420 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -19,9 +19,23 @@
 
 import android.app.ActivityManager
 import android.app.admin.DevicePolicyManager
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
 import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.internal.R.drawable.ic_account_circle
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -29,38 +43,76 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.UserSwitcherActivity
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-abstract class UserInteractorTest : SysuiTestCase() {
+@SmallTest
+@RunWith(JUnit4::class)
+class UserInteractorTest : SysuiTestCase() {
 
-    @Mock protected lateinit var controller: UserSwitcherController
-    @Mock protected lateinit var activityStarter: ActivityStarter
-    @Mock protected lateinit var manager: UserManager
-    @Mock protected lateinit var activityManager: ActivityManager
-    @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
-    @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
-    @Mock protected lateinit var uiEventLogger: UiEventLogger
-    @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var activityManager: ActivityManager
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var dialogShower: UserSwitchDialogController.DialogShower
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
-    protected lateinit var underTest: UserInteractor
+    private lateinit var underTest: UserInteractor
 
-    protected lateinit var testCoroutineScope: TestCoroutineScope
-    protected lateinit var userRepository: FakeUserRepository
-    protected lateinit var keyguardRepository: FakeKeyguardRepository
-    protected lateinit var telephonyRepository: FakeTelephonyRepository
+    private lateinit var testCoroutineScope: TestCoroutineScope
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var telephonyRepository: FakeTelephonyRepository
+    private lateinit var featureFlags: FakeFeatureFlags
 
-    abstract fun isRefactored(): Boolean
-
-    open fun setUp() {
+    @Before
+    fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
+        whenever(manager.canAddMoreUsers(any())).thenReturn(true)
 
+        overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
+        overrideResource(R.dimen.max_avatar_size, 10)
+        overrideResource(
+            com.android.internal.R.string.config_supervisedUserCreationPackage,
+            SUPERVISED_USER_CREATION_APP_PACKAGE,
+        )
+
+        featureFlags = FakeFeatureFlags()
         userRepository = FakeUserRepository()
         keyguardRepository = FakeKeyguardRepository()
         telephonyRepository = FakeTelephonyRepository()
@@ -75,16 +127,11 @@
             UserInteractor(
                 applicationContext = context,
                 repository = userRepository,
-                controller = controller,
                 activityStarter = activityStarter,
                 keyguardInteractor =
                     KeyguardInteractor(
                         repository = keyguardRepository,
                     ),
-                featureFlags =
-                    FakeFeatureFlags().apply {
-                        set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
-                    },
                 manager = manager,
                 applicationScope = testCoroutineScope,
                 telephonyInteractor =
@@ -107,11 +154,761 @@
                         devicePolicyManager = devicePolicyManager,
                         refreshUsersScheduler = refreshUsersScheduler,
                         uiEventLogger = uiEventLogger,
-                    )
+                        resumeSessionReceiver = resumeSessionReceiver,
+                        resetOrExitSessionReceiver = resetOrExitSessionReceiver,
+                    ),
+                featureFlags = featureFlags,
             )
     }
 
+    @Test
+    fun `onRecordSelected - user`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
+
+            verify(dialogShower).dismiss()
+            verify(activityManager).switchUser(userInfos[1].id)
+            Unit
+        }
+
+    @Test
+    fun `onRecordSelected - switch to guest user`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+
+            verify(activityManager).switchUser(userInfos.last().id)
+            Unit
+        }
+
+    @Test
+    fun `onRecordSelected - enter guest mode`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+            whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+
+            underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
+
+            verify(dialogShower).dismiss()
+            verify(manager).createGuest(any())
+            Unit
+        }
+
+    @Test
+    fun `onRecordSelected - action`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
+
+            verify(dialogShower, never()).dismiss()
+            verify(activityStarter).startActivity(any(), anyBoolean())
+        }
+
+    @Test
+    fun `users - switcher enabled`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            assertUsers(models = value, count = 3, includeGuest = true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `users - switches to second user`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            userRepository.setSelectedUserInfo(userInfos[1])
+
+            assertUsers(models = value, count = 2, selectedIndex = 1)
+            job.cancel()
+        }
+
+    @Test
+    fun `users - switcher not enabled`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+
+            var value: List<UserModel>? = null
+            val job = underTest.users.onEach { value = it }.launchIn(this)
+            assertUsers(models = value, count = 1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun selectedUser() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var value: UserModel? = null
+            val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
+            assertUser(value, id = 0, isSelected = true)
+
+            userRepository.setSelectedUserInfo(userInfos[1])
+            assertUser(value, id = 1, isSelected = true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked user not primary - empty list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device unlocked user is guest - empty list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            assertThat(userInfos[1].isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device locked add from lockscreen set - full list`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(
+                UserSwitcherSettingsModel(
+                    isUserSwitcherEnabled = true,
+                    isAddUsersFromLockscreen = true,
+                )
+            )
+            keyguardRepository.setKeyguardShowing(false)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `actions - device locked - only guest action and manage user is shown`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(true)
+            var value: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+            assertThat(value)
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `executeAction - add user - dialog shown`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogShower: UserSwitchDialogController.DialogShower = mock()
+
+            underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
+            assertThat(dialogRequest)
+                .isEqualTo(
+                    ShowDialogRequestModel.ShowAddUserDialog(
+                        userHandle = userInfos[0].userHandle,
+                        isKeyguardShowing = false,
+                        showEphemeralMessage = false,
+                        dialogShower = dialogShower,
+                    )
+                )
+
+            underTest.onDialogShown()
+            assertThat(dialogRequest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `executeAction - add supervised user - starts activity`() =
+        runBlocking(IMMEDIATE) {
+            underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+            val intentCaptor = kotlinArgumentCaptor<Intent>()
+            verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
+            assertThat(intentCaptor.value.action)
+                .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
+            assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
+        }
+
+    @Test
+    fun `executeAction - navigate to manage users`() =
+        runBlocking(IMMEDIATE) {
+            underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+            val intentCaptor = kotlinArgumentCaptor<Intent>()
+            verify(activityStarter).startActivity(intentCaptor.capture(), eq(true))
+            assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+        }
+
+    @Test
+    fun `executeAction - guest mode`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+            whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+            val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
+            val showDialogsJob =
+                underTest.dialogShowRequests
+                    .onEach {
+                        dialogRequests.add(it)
+                        if (it != null) {
+                            underTest.onDialogShown()
+                        }
+                    }
+                    .launchIn(this)
+            val dismissDialogsJob =
+                underTest.dialogDismissRequests
+                    .onEach {
+                        if (it != null) {
+                            underTest.onDialogDismissed()
+                        }
+                    }
+                    .launchIn(this)
+
+            underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+            assertThat(dialogRequests)
+                .contains(
+                    ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
+                )
+            verify(activityManager).switchUser(guestUserInfo.id)
+
+            showDialogsJob.cancel()
+            dismissDialogsJob.cancel()
+        }
+
+    @Test
+    fun `selectUser - already selected guest re-selected - exit guest dialog`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            val guestUserInfo = userInfos[1]
+            assertThat(guestUserInfo.isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(guestUserInfo)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(
+                newlySelectedUserId = guestUserInfo.id,
+                dialogShower = dialogShower,
+            )
+
+            assertThat(dialogRequest)
+                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+            verify(dialogShower, never()).dismiss()
+            job.cancel()
+        }
+
+    @Test
+    fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = true)
+            val guestUserInfo = userInfos[1]
+            assertThat(guestUserInfo.isGuest).isTrue()
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(guestUserInfo)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(newlySelectedUserId = userInfos[0].id, dialogShower = dialogShower)
+
+            assertThat(dialogRequest)
+                .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+            verify(dialogShower, never()).dismiss()
+            job.cancel()
+        }
+
+    @Test
+    fun `selectUser - not currently guest - switches users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            var dialogRequest: ShowDialogRequestModel? = null
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            underTest.selectUser(newlySelectedUserId = userInfos[1].id, dialogShower = dialogShower)
+
+            assertThat(dialogRequest).isNull()
+            verify(activityManager).switchUser(userInfos[1].id)
+            verify(dialogShower).dismiss()
+            job.cancel()
+        }
+
+    @Test
+    fun `Telephony call state changes - refreshes users`() =
+        runBlocking(IMMEDIATE) {
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            telephonyRepository.setCallState(1)
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `User switched broadcast`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            val callback1: UserInteractor.UserCallback = mock()
+            val callback2: UserInteractor.UserCallback = mock()
+            underTest.addCallback(callback1)
+            underTest.addCallback(callback2)
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            userRepository.setSelectedUserInfo(userInfos[1])
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_SWITCHED)
+                        .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+                )
+            }
+
+            verify(callback1).onUserStateChanged()
+            verify(callback2).onUserStateChanged()
+            assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `User info changed broadcast`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_INFO_CHANGED),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `System user unlocked broadcast - refresh users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_UNLOCKED)
+                        .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+        }
+
+    @Test
+    fun `Non-system user unlocked broadcast - do not refresh users`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach {
+                it.onReceive(
+                    context,
+                    Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+                )
+            }
+
+            assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
+        }
+
+    @Test
+    fun userRecords() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = false)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+
+            testCoroutineScope.advanceUntilIdle()
+
+            assertRecords(
+                records = underTest.userRecords.value,
+                userIds = listOf(0, 1, 2),
+                selectedUserIndex = 0,
+                includeGuest = false,
+                expectedActions =
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    ),
+            )
+        }
+
+    @Test
+    fun selectedUserRecord() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            keyguardRepository.setKeyguardShowing(false)
+
+            assertRecordForUser(
+                record = underTest.selectedUserRecord.value,
+                id = 0,
+                hasPicture = true,
+                isCurrent = true,
+                isSwitchToEnabled = true,
+            )
+        }
+
+    @Test
+    fun `users - secondary user - no guest user`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var res: List<UserModel>? = null
+            val job = underTest.users.onEach { res = it }.launchIn(this)
+            assertThat(res?.size == 2).isTrue()
+            assertThat(res?.find { it.isGuest }).isNull()
+            job.cancel()
+        }
+
+    @Test
+    fun `users - secondary user - no guest action`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var res: List<UserActionModel>? = null
+            val job = underTest.actions.onEach { res = it }.launchIn(this)
+            assertThat(res?.find { it == UserActionModel.ENTER_GUEST_MODE }).isNull()
+            job.cancel()
+        }
+
+    @Test
+    fun `users - secondary user - no guest user record`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 3, includeGuest = true)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            var res: List<UserRecord>? = null
+            val job = underTest.userRecords.onEach { res = it }.launchIn(this)
+            assertThat(res?.find { it.isGuest }).isNull()
+            job.cancel()
+        }
+
+    @Test
+    fun `show user switcher - full screen disabled - shows dialog switcher`() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+
+            var dialogRequest: ShowDialogRequestModel? = null
+            val expandable = mock<Expandable>()
+            underTest.showUserSwitcher(context, expandable)
+
+            val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+            // Dialog is shown.
+            assertThat(dialogRequest).isEqualTo(ShowDialogRequestModel.ShowUserSwitcherDialog)
+
+            underTest.onDialogShown()
+            assertThat(dialogRequest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `show user switcher - full screen enabled - launches activity`() {
+        featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+
+        val expandable = mock<Expandable>()
+        underTest.showUserSwitcher(context, expandable)
+
+        // Dialog is shown.
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(activityStarter)
+            .startActivity(
+                intentCaptor.capture(),
+                /* dismissShade= */ eq(true),
+                /* ActivityLaunchAnimator.Controller= */ nullable(),
+                /* showOverLockscreenWhenLocked= */ eq(true),
+                eq(UserHandle.SYSTEM),
+            )
+        assertThat(intentCaptor.value.component)
+            .isEqualTo(
+                ComponentName(
+                    context,
+                    UserSwitcherActivity::class.java,
+                )
+            )
+    }
+
+    private fun assertUsers(
+        models: List<UserModel>?,
+        count: Int,
+        selectedIndex: Int = 0,
+        includeGuest: Boolean = false,
+    ) {
+        checkNotNull(models)
+        assertThat(models.size).isEqualTo(count)
+        models.forEachIndexed { index, model ->
+            assertUser(
+                model = model,
+                id = index,
+                isSelected = index == selectedIndex,
+                isGuest = includeGuest && index == count - 1
+            )
+        }
+    }
+
+    private fun assertUser(
+        model: UserModel?,
+        id: Int,
+        isSelected: Boolean = false,
+        isGuest: Boolean = false,
+    ) {
+        checkNotNull(model)
+        assertThat(model.id).isEqualTo(id)
+        assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
+        assertThat(model.isSelected).isEqualTo(isSelected)
+        assertThat(model.isSelectable).isTrue()
+        assertThat(model.isGuest).isEqualTo(isGuest)
+    }
+
+    private fun assertRecords(
+        records: List<UserRecord>,
+        userIds: List<Int>,
+        selectedUserIndex: Int = 0,
+        includeGuest: Boolean = false,
+        expectedActions: List<UserActionModel> = emptyList(),
+    ) {
+        assertThat(records.size >= userIds.size).isTrue()
+        userIds.indices.forEach { userIndex ->
+            val record = records[userIndex]
+            assertThat(record.info).isNotNull()
+            val isGuest = includeGuest && userIndex == userIds.size - 1
+            assertRecordForUser(
+                record = record,
+                id = userIds[userIndex],
+                hasPicture = !isGuest,
+                isCurrent = userIndex == selectedUserIndex,
+                isGuest = isGuest,
+                isSwitchToEnabled = true,
+            )
+        }
+
+        assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
+        (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
+            val record = records[actionIndex]
+            assertThat(record.info).isNull()
+            assertRecordForAction(
+                record = record,
+                type = expectedActions[actionIndex - userIds.size],
+            )
+        }
+    }
+
+    private fun assertRecordForUser(
+        record: UserRecord?,
+        id: Int? = null,
+        hasPicture: Boolean = false,
+        isCurrent: Boolean = false,
+        isGuest: Boolean = false,
+        isSwitchToEnabled: Boolean = false,
+    ) {
+        checkNotNull(record)
+        assertThat(record.info?.id).isEqualTo(id)
+        assertThat(record.picture != null).isEqualTo(hasPicture)
+        assertThat(record.isCurrent).isEqualTo(isCurrent)
+        assertThat(record.isGuest).isEqualTo(isGuest)
+        assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
+    }
+
+    private fun assertRecordForAction(
+        record: UserRecord,
+        type: UserActionModel,
+    ) {
+        assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
+        assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
+        assertThat(record.isAddSupervisedUser)
+            .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+    }
+
+    private fun createUserInfos(
+        count: Int,
+        includeGuest: Boolean,
+    ): List<UserInfo> {
+        return (0 until count).map { index ->
+            val isGuest = includeGuest && index == count - 1
+            createUserInfo(
+                id = index,
+                name =
+                    if (isGuest) {
+                        "guest"
+                    } else {
+                        "user_$index"
+                    },
+                isPrimary = !isGuest && index == 0,
+                isGuest = isGuest,
+            )
+        }
+    }
+
+    private fun createUserInfo(
+        id: Int,
+        name: String,
+        isPrimary: Boolean = false,
+        isGuest: Boolean = false,
+    ): UserInfo {
+        return UserInfo(
+            id,
+            name,
+            /* iconPath= */ "",
+            /* flags= */ if (isPrimary) {
+                UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
+            } else {
+                0
+            },
+            if (isGuest) {
+                UserManager.USER_TYPE_FULL_GUEST
+            } else {
+                UserManager.USER_TYPE_FULL_SYSTEM
+            },
+        )
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
+        private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        private val GUEST_ICON: Drawable = mock()
+        private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
deleted file mode 100644
index c3a9705..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
+++ /dev/null
@@ -1,188 +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.user.domain.interactor
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
-
-@SmallTest
-@RunWith(JUnit4::class)
-open class UserInteractorUnrefactoredTest : UserInteractorTest() {
-
-    override fun isRefactored(): Boolean {
-        return false
-    }
-
-    @Before
-    override fun setUp() {
-        super.setUp()
-    }
-
-    @Test
-    fun `actions - not actionable when locked and locked - no actions`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(UserActionModel.values().toList())
-            userRepository.setActionableWhenLocked(false)
-            keyguardRepository.setKeyguardShowing(true)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions).isEmpty()
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - not actionable when locked and not locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
-            userRepository.setActionableWhenLocked(false)
-            keyguardRepository.setKeyguardShowing(false)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - actionable when locked and not locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
-            userRepository.setActionableWhenLocked(true)
-            keyguardRepository.setKeyguardShowing(false)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun `actions - actionable when locked and locked`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
-            userRepository.setActionableWhenLocked(true)
-            keyguardRepository.setKeyguardShowing(true)
-
-            var actions: List<UserActionModel>? = null
-            val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
-            assertThat(actions)
-                .isEqualTo(
-                    listOf(
-                        UserActionModel.ENTER_GUEST_MODE,
-                        UserActionModel.ADD_USER,
-                        UserActionModel.ADD_SUPERVISED_USER,
-                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
-                    )
-                )
-            job.cancel()
-        }
-
-    @Test
-    fun selectUser() {
-        val userId = 3
-
-        underTest.selectUser(userId)
-
-        verify(controller).onUserSelected(eq(userId), nullable())
-    }
-
-    @Test
-    fun `executeAction - guest`() {
-        underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
-        verify(controller).createAndSwitchToGuestUser(nullable())
-    }
-
-    @Test
-    fun `executeAction - add user`() {
-        underTest.executeAction(UserActionModel.ADD_USER)
-
-        verify(controller).showAddUserDialog(nullable())
-    }
-
-    @Test
-    fun `executeAction - add supervised user`() {
-        underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
-        verify(controller).startSupervisedUserActivity()
-    }
-
-    @Test
-    fun `executeAction - manage users`() {
-        underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
-        verify(activityStarter).startActivity(any(), anyBoolean())
-    }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
new file mode 100644
index 0000000..db348b80
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -0,0 +1,306 @@
+/*
+ * 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.user.ui.viewmodel
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class StatusBarUserChipViewModelTest : SysuiTestCase() {
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var activityManager: ActivityManager
+    @Mock private lateinit var manager: UserManager
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
+
+    private lateinit var underTest: StatusBarUserChipViewModel
+
+    private val userRepository = FakeUserRepository()
+    private val keyguardRepository = FakeKeyguardRepository()
+    private val featureFlags = FakeFeatureFlags()
+    private lateinit var guestUserInteractor: GuestUserInteractor
+    private lateinit var refreshUsersScheduler: RefreshUsersScheduler
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        doAnswer { invocation ->
+                val userId = invocation.arguments[0] as Int
+                when (userId) {
+                    USER_ID_0 -> return@doAnswer USER_IMAGE_0
+                    USER_ID_1 -> return@doAnswer USER_IMAGE_1
+                    USER_ID_2 -> return@doAnswer USER_IMAGE_2
+                    else -> return@doAnswer mock<Bitmap>()
+                }
+            }
+            .`when`(manager)
+            .getUserIcon(anyInt())
+
+        userRepository.isStatusBarUserChipEnabled = true
+
+        refreshUsersScheduler =
+            RefreshUsersScheduler(
+                applicationScope = testScope.backgroundScope,
+                mainDispatcher = testDispatcher,
+                repository = userRepository,
+            )
+        guestUserInteractor =
+            GuestUserInteractor(
+                applicationContext = context,
+                applicationScope = testScope.backgroundScope,
+                mainDispatcher = testDispatcher,
+                backgroundDispatcher = testDispatcher,
+                manager = manager,
+                repository = userRepository,
+                deviceProvisionedController = deviceProvisionedController,
+                devicePolicyManager = devicePolicyManager,
+                refreshUsersScheduler = refreshUsersScheduler,
+                uiEventLogger = uiEventLogger,
+                resumeSessionReceiver = resumeSessionReceiver,
+                resetOrExitSessionReceiver = resetOrExitSessionReceiver,
+            )
+
+        underTest = viewModel()
+    }
+
+    @Test
+    fun `config is false - chip is disabled`() {
+        // the enabled bit is set at SystemUI startup, so recreate the view model here
+        userRepository.isStatusBarUserChipEnabled = false
+        underTest = viewModel()
+
+        assertThat(underTest.chipEnabled).isFalse()
+    }
+
+    @Test
+    fun `config is true - chip is enabled`() {
+        // the enabled bit is set at SystemUI startup, so recreate the view model here
+        userRepository.isStatusBarUserChipEnabled = true
+        underTest = viewModel()
+
+        assertThat(underTest.chipEnabled).isTrue()
+    }
+
+    @Test
+    fun `should show chip criteria - single user`() =
+        testScope.runTest {
+            userRepository.setUserInfos(listOf(USER_0))
+            userRepository.setSelectedUserInfo(USER_0)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            val values = mutableListOf<Boolean>()
+
+            val job = launch { underTest.isChipVisible.toList(values) }
+            advanceUntilIdle()
+
+            assertThat(values).containsExactly(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `should show chip criteria - multiple users`() =
+        testScope.runTest {
+            setMultipleUsers()
+
+            var latest: Boolean? = null
+            val job = underTest.isChipVisible.onEach { latest = it }.launchIn(this)
+            yield()
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `user chip name - shows selected user info`() =
+        testScope.runTest {
+            setMultipleUsers()
+
+            var latest: Text? = null
+            val job = underTest.userName.onEach { latest = it }.launchIn(this)
+
+            userRepository.setSelectedUserInfo(USER_0)
+            assertThat(latest).isEqualTo(USER_NAME_0)
+
+            userRepository.setSelectedUserInfo(USER_1)
+            assertThat(latest).isEqualTo(USER_NAME_1)
+
+            userRepository.setSelectedUserInfo(USER_2)
+            assertThat(latest).isEqualTo(USER_NAME_2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `user chip avatar - shows selected user info`() =
+        testScope.runTest {
+            setMultipleUsers()
+
+            // A little hacky. System server passes us bitmaps and we wrap them in the interactor.
+            // Unwrap them to make sure we're always tracking the current user's bitmap
+            var latest: Bitmap? = null
+            val job =
+                underTest.userAvatar
+                    .onEach {
+                        if (it !is BitmapDrawable) {
+                            latest = null
+                        }
+
+                        latest = (it as BitmapDrawable).bitmap
+                    }
+                    .launchIn(this)
+
+            userRepository.setSelectedUserInfo(USER_0)
+            assertThat(latest).isEqualTo(USER_IMAGE_0)
+
+            userRepository.setSelectedUserInfo(USER_1)
+            assertThat(latest).isEqualTo(USER_IMAGE_1)
+
+            userRepository.setSelectedUserInfo(USER_2)
+            assertThat(latest).isEqualTo(USER_IMAGE_2)
+
+            job.cancel()
+        }
+
+    private fun viewModel(): StatusBarUserChipViewModel {
+        return StatusBarUserChipViewModel(
+            context = context,
+            interactor =
+                UserInteractor(
+                    applicationContext = context,
+                    repository = userRepository,
+                    activityStarter = activityStarter,
+                    keyguardInteractor =
+                        KeyguardInteractor(
+                            repository = keyguardRepository,
+                        ),
+                    featureFlags = featureFlags,
+                    manager = manager,
+                    applicationScope = testScope.backgroundScope,
+                    telephonyInteractor =
+                        TelephonyInteractor(
+                            repository = FakeTelephonyRepository(),
+                        ),
+                    broadcastDispatcher = fakeBroadcastDispatcher,
+                    backgroundDispatcher = testDispatcher,
+                    activityManager = activityManager,
+                    refreshUsersScheduler = refreshUsersScheduler,
+                    guestUserInteractor = guestUserInteractor,
+                )
+        )
+    }
+
+    private suspend fun setMultipleUsers() {
+        userRepository.setUserInfos(listOf(USER_0, USER_1, USER_2))
+        userRepository.setSelectedUserInfo(USER_0)
+        userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+    }
+
+    companion object {
+        private const val USER_ID_0 = 0
+        private val USER_IMAGE_0 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        private val USER_NAME_0 = Text.Loaded("zero")
+
+        private const val USER_ID_1 = 1
+        private val USER_IMAGE_1 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        private val USER_NAME_1 = Text.Loaded("one")
+
+        private const val USER_ID_2 = 2
+        private val USER_IMAGE_2 = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+        private val USER_NAME_2 = Text.Loaded("two")
+
+        private val USER_0 =
+            UserInfo(
+                USER_ID_0,
+                USER_NAME_0.text!!,
+                /* iconPath */ "",
+                /* flags */ 0,
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+            )
+
+        private val USER_1 =
+            UserInfo(
+                USER_ID_1,
+                USER_NAME_1.text!!,
+                /* iconPath */ "",
+                /* flags */ 0,
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+            )
+
+        private val USER_2 =
+            UserInfo(
+                USER_ID_2,
+                USER_NAME_2.text!!,
+                /* iconPath */ "",
+                /* flags */ 0,
+                /* userType */ UserManager.USER_TYPE_FULL_SYSTEM
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 0344e3f..eac7fc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -19,38 +19,46 @@
 
 import android.app.ActivityManager
 import android.app.admin.DevicePolicyManager
-import android.graphics.drawable.Drawable
+import android.content.pm.UserInfo
 import android.os.UserManager
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
-import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.GuestUserInteractor
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.yield
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestResult
+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
@@ -58,17 +66,19 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class UserSwitcherViewModelTest : SysuiTestCase() {
 
-    @Mock private lateinit var controller: UserSwitcherController
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var activityManager: ActivityManager
     @Mock private lateinit var manager: UserManager
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
     private lateinit var underTest: UserSwitcherViewModel
 
@@ -76,34 +86,55 @@
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var powerRepository: FakePowerRepository
 
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var testScope: TestScope
+    private lateinit var injectedScope: CoroutineScope
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+        whenever(manager.getUserSwitchability(any()))
+            .thenReturn(UserManager.SWITCHABILITY_STATUS_OK)
+        overrideResource(
+            com.android.internal.R.string.config_supervisedUserCreationPackage,
+            SUPERVISED_USER_CREATION_PACKAGE,
+        )
 
+        testDispatcher = UnconfinedTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        injectedScope = CoroutineScope(testScope.coroutineContext + SupervisorJob())
         userRepository = FakeUserRepository()
+        runBlocking {
+            userRepository.setSettings(
+                UserSwitcherSettingsModel(
+                    isUserSwitcherEnabled = true,
+                )
+            )
+        }
+
         keyguardRepository = FakeKeyguardRepository()
         powerRepository = FakePowerRepository()
-        val featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
-        val scope = TestCoroutineScope()
         val refreshUsersScheduler =
             RefreshUsersScheduler(
-                applicationScope = scope,
-                mainDispatcher = IMMEDIATE,
+                applicationScope = injectedScope,
+                mainDispatcher = testDispatcher,
                 repository = userRepository,
             )
         val guestUserInteractor =
             GuestUserInteractor(
                 applicationContext = context,
-                applicationScope = scope,
-                mainDispatcher = IMMEDIATE,
-                backgroundDispatcher = IMMEDIATE,
+                applicationScope = injectedScope,
+                mainDispatcher = testDispatcher,
+                backgroundDispatcher = testDispatcher,
                 manager = manager,
                 repository = userRepository,
                 deviceProvisionedController = deviceProvisionedController,
                 devicePolicyManager = devicePolicyManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 uiEventLogger = uiEventLogger,
+                resumeSessionReceiver = resumeSessionReceiver,
+                resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
 
         underTest =
@@ -112,21 +143,20 @@
                         UserInteractor(
                             applicationContext = context,
                             repository = userRepository,
-                            controller = controller,
                             activityStarter = activityStarter,
                             keyguardInteractor =
                                 KeyguardInteractor(
                                     repository = keyguardRepository,
                                 ),
-                            featureFlags = featureFlags,
+                            featureFlags = FakeFeatureFlags(),
                             manager = manager,
-                            applicationScope = scope,
+                            applicationScope = injectedScope,
                             telephonyInteractor =
                                 TelephonyInteractor(
                                     repository = FakeTelephonyRepository(),
                                 ),
                             broadcastDispatcher = fakeBroadcastDispatcher,
-                            backgroundDispatcher = IMMEDIATE,
+                            backgroundDispatcher = testDispatcher,
                             activityManager = activityManager,
                             refreshUsersScheduler = refreshUsersScheduler,
                             guestUserInteractor = guestUserInteractor,
@@ -135,202 +165,216 @@
                         PowerInteractor(
                             repository = powerRepository,
                         ),
-                    featureFlags = featureFlags,
                     guestUserInteractor = guestUserInteractor,
                 )
                 .create(UserSwitcherViewModel::class.java)
     }
 
     @Test
-    fun users() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setUsers(
+    fun users() = selfCancelingTest {
+        val userInfos =
+            listOf(
+                UserInfo(
+                    /* id= */ 0,
+                    /* name= */ "zero",
+                    /* iconPath= */ "",
+                    /* flags= */ UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN,
+                    UserManager.USER_TYPE_FULL_SYSTEM,
+                ),
+                UserInfo(
+                    /* id= */ 1,
+                    /* name= */ "one",
+                    /* iconPath= */ "",
+                    /* flags= */ 0,
+                    UserManager.USER_TYPE_FULL_SYSTEM,
+                ),
+                UserInfo(
+                    /* id= */ 2,
+                    /* name= */ "two",
+                    /* iconPath= */ "",
+                    /* flags= */ 0,
+                    UserManager.USER_TYPE_FULL_SYSTEM,
+                ),
+            )
+        userRepository.setUserInfos(userInfos)
+        userRepository.setSelectedUserInfo(userInfos[0])
+
+        val userViewModels = mutableListOf<List<UserViewModel>>()
+        val job = launch(testDispatcher) { underTest.users.toList(userViewModels) }
+
+        assertThat(userViewModels.last()).hasSize(3)
+        assertUserViewModel(
+            viewModel = userViewModels.last()[0],
+            viewKey = 0,
+            name = "zero",
+            isSelectionMarkerVisible = true,
+        )
+        assertUserViewModel(
+            viewModel = userViewModels.last()[1],
+            viewKey = 1,
+            name = "one",
+            isSelectionMarkerVisible = false,
+        )
+        assertUserViewModel(
+            viewModel = userViewModels.last()[2],
+            viewKey = 2,
+            name = "two",
+            isSelectionMarkerVisible = false,
+        )
+        job.cancel()
+    }
+
+    @Test
+    fun `maximumUserColumns - few users`() = selfCancelingTest {
+        setUsers(count = 2)
+        val values = mutableListOf<Int>()
+        val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+        assertThat(values.last()).isEqualTo(4)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `maximumUserColumns - many users`() = selfCancelingTest {
+        setUsers(count = 5)
+        val values = mutableListOf<Int>()
+        val job = launch(testDispatcher) { underTest.maximumUserColumns.toList(values) }
+
+        assertThat(values.last()).isEqualTo(3)
+        job.cancel()
+    }
+
+    @Test
+    fun `isOpenMenuButtonVisible - has actions - true`() = selfCancelingTest {
+        setUsers(2)
+
+        val isVisible = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+        assertThat(isVisible.last()).isTrue()
+        job.cancel()
+    }
+
+    @Test
+    fun `isOpenMenuButtonVisible - no actions - false`() = selfCancelingTest {
+        val userInfos = setUsers(2)
+        userRepository.setSelectedUserInfo(userInfos[1])
+        keyguardRepository.setKeyguardShowing(true)
+        whenever(manager.canAddMoreUsers(any())).thenReturn(false)
+
+        val isVisible = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isOpenMenuButtonVisible.toList(isVisible) }
+
+        assertThat(isVisible.last()).isFalse()
+        job.cancel()
+    }
+
+    @Test
+    fun menu() = selfCancelingTest {
+        val isMenuVisible = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isMenuVisible.toList(isMenuVisible) }
+        assertThat(isMenuVisible.last()).isFalse()
+
+        underTest.onOpenMenuButtonClicked()
+        assertThat(isMenuVisible.last()).isTrue()
+
+        underTest.onMenuClosed()
+        assertThat(isMenuVisible.last()).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun `menu actions`() = selfCancelingTest {
+        setUsers(2)
+        val actions = mutableListOf<List<UserActionViewModel>>()
+        val job = launch(testDispatcher) { underTest.menu.toList(actions) }
+
+        assertThat(actions.last().map { it.viewKey })
+            .isEqualTo(
                 listOf(
-                    UserModel(
-                        id = 0,
-                        name = Text.Loaded("zero"),
-                        image = USER_IMAGE,
-                        isSelected = true,
-                        isSelectable = true,
-                        isGuest = false,
-                    ),
-                    UserModel(
-                        id = 1,
-                        name = Text.Loaded("one"),
-                        image = USER_IMAGE,
-                        isSelected = false,
-                        isSelectable = true,
-                        isGuest = false,
-                    ),
-                    UserModel(
-                        id = 2,
-                        name = Text.Loaded("two"),
-                        image = USER_IMAGE,
-                        isSelected = false,
-                        isSelectable = false,
-                        isGuest = false,
-                    ),
+                    UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+                    UserActionModel.ADD_USER.ordinal.toLong(),
+                    UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+                    UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
                 )
             )
 
-            var userViewModels: List<UserViewModel>? = null
-            val job = underTest.users.onEach { userViewModels = it }.launchIn(this)
-
-            assertThat(userViewModels).hasSize(3)
-            assertUserViewModel(
-                viewModel = userViewModels?.get(0),
-                viewKey = 0,
-                name = "zero",
-                isSelectionMarkerVisible = true,
-                alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
-                isClickable = true,
-            )
-            assertUserViewModel(
-                viewModel = userViewModels?.get(1),
-                viewKey = 1,
-                name = "one",
-                isSelectionMarkerVisible = false,
-                alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA,
-                isClickable = true,
-            )
-            assertUserViewModel(
-                viewModel = userViewModels?.get(2),
-                viewKey = 2,
-                name = "two",
-                isSelectionMarkerVisible = false,
-                alpha = LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA,
-                isClickable = false,
-            )
-            job.cancel()
-        }
+        job.cancel()
+    }
 
     @Test
-    fun `maximumUserColumns - few users`() =
-        runBlocking(IMMEDIATE) {
-            setUsers(count = 2)
-            var value: Int? = null
-            val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
+    fun `isFinishRequested - finishes when user is switched`() = selfCancelingTest {
+        val userInfos = setUsers(count = 2)
+        val isFinishRequested = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+        assertThat(isFinishRequested.last()).isFalse()
 
-            assertThat(value).isEqualTo(4)
-            job.cancel()
-        }
+        userRepository.setSelectedUserInfo(userInfos[1])
+
+        assertThat(isFinishRequested.last()).isTrue()
+
+        job.cancel()
+    }
 
     @Test
-    fun `maximumUserColumns - many users`() =
-        runBlocking(IMMEDIATE) {
-            setUsers(count = 5)
-            var value: Int? = null
-            val job = underTest.maximumUserColumns.onEach { value = it }.launchIn(this)
+    fun `isFinishRequested - finishes when the screen turns off`() = selfCancelingTest {
+        setUsers(count = 2)
+        powerRepository.setInteractive(true)
+        val isFinishRequested = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+        assertThat(isFinishRequested.last()).isFalse()
 
-            assertThat(value).isEqualTo(3)
-            job.cancel()
-        }
+        powerRepository.setInteractive(false)
+
+        assertThat(isFinishRequested.last()).isTrue()
+
+        job.cancel()
+    }
 
     @Test
-    fun `isOpenMenuButtonVisible - has actions - true`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(UserActionModel.values().toList())
+    fun `isFinishRequested - finishes when cancel button is clicked`() = selfCancelingTest {
+        setUsers(count = 2)
+        powerRepository.setInteractive(true)
+        val isFinishRequested = mutableListOf<Boolean>()
+        val job = launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) }
+        assertThat(isFinishRequested.last()).isFalse()
 
-            var isVisible: Boolean? = null
-            val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
+        underTest.onCancelButtonClicked()
 
-            assertThat(isVisible).isTrue()
-            job.cancel()
-        }
+        assertThat(isFinishRequested.last()).isTrue()
 
-    @Test
-    fun `isOpenMenuButtonVisible - no actions - false`() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(emptyList())
+        underTest.onFinished()
 
-            var isVisible: Boolean? = null
-            val job = underTest.isOpenMenuButtonVisible.onEach { isVisible = it }.launchIn(this)
+        assertThat(isFinishRequested.last()).isFalse()
 
-            assertThat(isVisible).isFalse()
-            job.cancel()
-        }
+        job.cancel()
+    }
 
-    @Test
-    fun menu() =
-        runBlocking(IMMEDIATE) {
-            userRepository.setActions(UserActionModel.values().toList())
-            var isMenuVisible: Boolean? = null
-            val job = underTest.isMenuVisible.onEach { isMenuVisible = it }.launchIn(this)
-            assertThat(isMenuVisible).isFalse()
-
-            underTest.onOpenMenuButtonClicked()
-            assertThat(isMenuVisible).isTrue()
-
-            underTest.onMenuClosed()
-            assertThat(isMenuVisible).isFalse()
-
-            job.cancel()
-        }
-
-    @Test
-    fun `isFinishRequested - finishes when user is switched`() =
-        runBlocking(IMMEDIATE) {
-            setUsers(count = 2)
-            var isFinishRequested: Boolean? = null
-            val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
-            assertThat(isFinishRequested).isFalse()
-
-            userRepository.setSelectedUser(1)
-            yield()
-            assertThat(isFinishRequested).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
-    fun `isFinishRequested - finishes when the screen turns off`() =
-        runBlocking(IMMEDIATE) {
-            setUsers(count = 2)
-            powerRepository.setInteractive(true)
-            var isFinishRequested: Boolean? = null
-            val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
-            assertThat(isFinishRequested).isFalse()
-
-            powerRepository.setInteractive(false)
-            yield()
-            assertThat(isFinishRequested).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
-    fun `isFinishRequested - finishes when cancel button is clicked`() =
-        runBlocking(IMMEDIATE) {
-            setUsers(count = 2)
-            powerRepository.setInteractive(true)
-            var isFinishRequested: Boolean? = null
-            val job = underTest.isFinishRequested.onEach { isFinishRequested = it }.launchIn(this)
-            assertThat(isFinishRequested).isFalse()
-
-            underTest.onCancelButtonClicked()
-            yield()
-            assertThat(isFinishRequested).isTrue()
-
-            underTest.onFinished()
-            yield()
-            assertThat(isFinishRequested).isFalse()
-
-            job.cancel()
-        }
-
-    private suspend fun setUsers(count: Int) {
-        userRepository.setUsers(
+    private suspend fun setUsers(count: Int): List<UserInfo> {
+        val userInfos =
             (0 until count).map { index ->
-                UserModel(
-                    id = index,
-                    name = Text.Loaded("$index"),
-                    image = USER_IMAGE,
-                    isSelected = index == 0,
-                    isSelectable = true,
-                    isGuest = false,
+                UserInfo(
+                    /* id= */ index,
+                    /* name= */ "$index",
+                    /* iconPath= */ "",
+                    /* flags= */ if (index == 0) {
+                        // This is the primary user.
+                        UserInfo.FLAG_PRIMARY or UserInfo.FLAG_ADMIN
+                    } else {
+                        // This isn't the primary user.
+                        0
+                    },
+                    UserManager.USER_TYPE_FULL_SYSTEM,
                 )
             }
-        )
+        userRepository.setUserInfos(userInfos)
+
+        if (userInfos.isNotEmpty()) {
+            userRepository.setSelectedUserInfo(userInfos[0])
+        }
+        return userInfos
     }
 
     private fun assertUserViewModel(
@@ -338,19 +382,25 @@
         viewKey: Int,
         name: String,
         isSelectionMarkerVisible: Boolean,
-        alpha: Float,
-        isClickable: Boolean,
     ) {
         checkNotNull(viewModel)
         assertThat(viewModel.viewKey).isEqualTo(viewKey)
         assertThat(viewModel.name).isEqualTo(Text.Loaded(name))
         assertThat(viewModel.isSelectionMarkerVisible).isEqualTo(isSelectionMarkerVisible)
-        assertThat(viewModel.alpha).isEqualTo(alpha)
-        assertThat(viewModel.onClicked != null).isEqualTo(isClickable)
+        assertThat(viewModel.alpha)
+            .isEqualTo(LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA)
+        assertThat(viewModel.onClicked).isNotNull()
     }
 
+    private fun selfCancelingTest(
+        block: suspend TestScope.() -> Unit,
+    ): TestResult =
+        testScope.runTest {
+            block()
+            injectedScope.coroutineContext[Job.Key]?.cancelAndJoin()
+        }
+
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private val USER_IMAGE = mock<Drawable>()
+        private const val SUPERVISED_USER_CREATION_PACKAGE = "com.some.package"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
deleted file mode 100644
index 5e09b81..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.collection
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class RingBufferTest : SysuiTestCase() {
-
-    private val buffer = RingBuffer(5) { TestElement() }
-
-    private val history = mutableListOf<TestElement>()
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun testBarelyFillBuffer() {
-        fillBuffer(5)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-        assertEquals(3, buffer[3].id)
-        assertEquals(4, buffer[4].id)
-    }
-
-    @Test
-    fun testPartiallyFillBuffer() {
-        fillBuffer(3)
-
-        assertEquals(3, buffer.size)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
-    }
-
-    @Test
-    fun testSpinBuffer() {
-        fillBuffer(277)
-
-        assertEquals(272, buffer[0].id)
-        assertEquals(273, buffer[1].id)
-        assertEquals(274, buffer[2].id)
-        assertEquals(275, buffer[3].id)
-        assertEquals(276, buffer[4].id)
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
-
-        assertEquals(5, buffer.size)
-    }
-
-    @Test
-    fun testElementsAreRecycled() {
-        fillBuffer(23)
-
-        assertSame(history[4], buffer[1])
-        assertSame(history[9], buffer[1])
-        assertSame(history[14], buffer[1])
-        assertSame(history[19], buffer[1])
-    }
-
-    @Test
-    fun testIterator() {
-        fillBuffer(13)
-
-        val iterator = buffer.iterator()
-
-        for (i in 0 until 5) {
-            assertEquals(history[8 + i], iterator.next())
-        }
-        assertFalse(iterator.hasNext())
-        assertThrows(NoSuchElementException::class.java) { iterator.next() }
-    }
-
-    @Test
-    fun testForEach() {
-        fillBuffer(13)
-        var i = 8
-
-        buffer.forEach {
-            assertEquals(history[i], it)
-            i++
-        }
-        assertEquals(13, i)
-    }
-
-    private fun fillBuffer(count: Int) {
-        for (i in 0 until count) {
-            val elem = buffer.advance()
-            elem.id = history.size
-            history.add(elem)
-        }
-    }
-}
-
-private class TestElement(var id: Int = 0) {
-    override fun toString(): String {
-        return "{TestElement $id}"
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java
index fe01f84..6e109ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java
@@ -42,7 +42,9 @@
 
     @Before
     public void setUp() {
-        mInner = WakeLock.createPartialInner(mContext, WakeLockTest.class.getName());
+        mInner = WakeLock.createWakeLockInner(mContext,
+                WakeLockTest.class.getName(),
+                PowerManager.PARTIAL_WAKE_LOCK);
         mWakeLock = WakeLock.wrap(mInner, 20000);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
index c254358..379bb28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/ImageWallpaperTest.java
@@ -26,8 +26,8 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -44,6 +44,7 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Display;
@@ -135,9 +136,10 @@
         when(mWallpaperBitmap.getHeight()).thenReturn(mBitmapHeight);
 
         // set up wallpaper manager
-        when(mWallpaperManager.peekBitmapDimensions()).thenReturn(
-                new Rect(0, 0, mBitmapWidth, mBitmapHeight));
-        when(mWallpaperManager.getBitmap(false)).thenReturn(mWallpaperBitmap);
+        when(mWallpaperManager.peekBitmapDimensions())
+                .thenReturn(new Rect(0, 0, mBitmapWidth, mBitmapHeight));
+        when(mWallpaperManager.getBitmapAsUser(eq(UserHandle.USER_CURRENT), anyBoolean()))
+                .thenReturn(mWallpaperBitmap);
         when(mMockContext.getSystemService(WallpaperManager.class)).thenReturn(mWallpaperManager);
 
         // set up surface
@@ -286,9 +288,6 @@
         testMinSurfaceHelper(8, 8);
         testMinSurfaceHelper(100, 2000);
         testMinSurfaceHelper(200, 1);
-        testMinSurfaceHelper(0, 1);
-        testMinSurfaceHelper(1, 0);
-        testMinSurfaceHelper(0, 0);
     }
 
     private void testMinSurfaceHelper(int bitmapWidth, int bitmapHeight) {
@@ -307,28 +306,6 @@
     }
 
     @Test
-    public void testZeroBitmap() {
-        // test that a frame is never drawn with a 0 bitmap
-        testZeroBitmapHelper(0, 1);
-        testZeroBitmapHelper(1, 0);
-        testZeroBitmapHelper(0, 0);
-    }
-
-    private void testZeroBitmapHelper(int bitmapWidth, int bitmapHeight) {
-
-        clearInvocations(mSurfaceHolder);
-        setBitmapDimensions(bitmapWidth, bitmapHeight);
-
-        ImageWallpaper imageWallpaper = createImageWallpaperCanvas();
-        ImageWallpaper.CanvasEngine engine =
-                (ImageWallpaper.CanvasEngine) imageWallpaper.onCreateEngine();
-        ImageWallpaper.CanvasEngine spyEngine = spy(engine);
-        spyEngine.onCreate(mSurfaceHolder);
-        spyEngine.onSurfaceRedrawNeeded(mSurfaceHolder);
-        verify(spyEngine, never()).drawFrameOnCanvas(any());
-    }
-
-    @Test
     public void testLoadDrawAndUnloadBitmap() {
         setBitmapDimensions(LOW_BMP_WIDTH, LOW_BMP_HEIGHT);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
deleted file mode 100644
index 76bff1d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
+++ /dev/null
@@ -1,350 +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.wallpapers.canvas;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.app.WallpaperColors;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class WallpaperColorExtractorTest extends SysuiTestCase {
-    private static final int LOW_BMP_WIDTH = 128;
-    private static final int LOW_BMP_HEIGHT = 128;
-    private static final int HIGH_BMP_WIDTH = 3000;
-    private static final int HIGH_BMP_HEIGHT = 4000;
-    private static final int VERY_LOW_BMP_WIDTH = 1;
-    private static final int VERY_LOW_BMP_HEIGHT = 1;
-    private static final int DISPLAY_WIDTH = 1920;
-    private static final int DISPLAY_HEIGHT = 1080;
-
-    private static final int PAGES_LOW = 4;
-    private static final int PAGES_HIGH = 7;
-
-    private static final int MIN_AREAS = 4;
-    private static final int MAX_AREAS = 10;
-
-    private int mMiniBitmapWidth;
-    private int mMiniBitmapHeight;
-
-    @Mock
-    private Executor mBackgroundExecutor;
-
-    private int mColorsProcessed;
-    private int mMiniBitmapUpdatedCount;
-    private int mActivatedCount;
-    private int mDeactivatedCount;
-
-    @Before
-    public void setUp() throws Exception {
-        allowTestableLooperAsMainThread();
-        MockitoAnnotations.initMocks(this);
-        doAnswer(invocation ->  {
-            ((Runnable) invocation.getArgument(0)).run();
-            return null;
-        }).when(mBackgroundExecutor).execute(any(Runnable.class));
-    }
-
-    private void resetCounters() {
-        mColorsProcessed = 0;
-        mMiniBitmapUpdatedCount = 0;
-        mActivatedCount = 0;
-        mDeactivatedCount = 0;
-    }
-
-    private Bitmap getMockBitmap(int width, int height) {
-        Bitmap bitmap = mock(Bitmap.class);
-        when(bitmap.getWidth()).thenReturn(width);
-        when(bitmap.getHeight()).thenReturn(height);
-        return bitmap;
-    }
-
-    private WallpaperColorExtractor getSpyWallpaperColorExtractor() {
-
-        WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor(
-                mBackgroundExecutor,
-                new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
-                    @Override
-                    public void onColorsProcessed(List<RectF> regions,
-                            List<WallpaperColors> colors) {
-                        assertThat(regions.size()).isEqualTo(colors.size());
-                        mColorsProcessed += regions.size();
-                    }
-
-                    @Override
-                    public void onMiniBitmapUpdated() {
-                        mMiniBitmapUpdatedCount++;
-                    }
-
-                    @Override
-                    public void onActivated() {
-                        mActivatedCount++;
-                    }
-
-                    @Override
-                    public void onDeactivated() {
-                        mDeactivatedCount++;
-                    }
-                });
-        WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor);
-
-        doAnswer(invocation -> {
-            mMiniBitmapWidth = invocation.getArgument(1);
-            mMiniBitmapHeight = invocation.getArgument(2);
-            return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
-        }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
-
-
-        doAnswer(invocation -> getMockBitmap(
-                        invocation.getArgument(1),
-                        invocation.getArgument(2)))
-                .when(spyWallpaperColorExtractor)
-                .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
-
-        doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
-                .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class));
-
-        return spyWallpaperColorExtractor;
-    }
-
-    private RectF randomArea() {
-        float width = (float) Math.random();
-        float startX = (float) (Math.random() * (1 - width));
-        float height = (float) Math.random();
-        float startY = (float) (Math.random() * (1 - height));
-        return new RectF(startX, startY, startX + width, startY + height);
-    }
-
-    private List<RectF> listOfRandomAreas(int min, int max) {
-        int nAreas = randomBetween(min, max);
-        List<RectF> result = new ArrayList<>();
-        for (int i = 0; i < nAreas; i++) {
-            result.add(randomArea());
-        }
-        return result;
-    }
-
-    private int randomBetween(int minIncluded, int maxIncluded) {
-        return (int) (Math.random() * ((maxIncluded - minIncluded) + 1)) + minIncluded;
-    }
-
-    /**
-     * Test that for bitmaps of random dimensions, the mini bitmap is always created
-     * with either a width <= SMALL_SIDE or a height <= SMALL_SIDE
-     */
-    @Test
-    public void testMiniBitmapCreation() {
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-        int nSimulations = 10;
-        for (int i = 0; i < nSimulations; i++) {
-            resetCounters();
-            int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
-            int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
-            Bitmap bitmap = getMockBitmap(width, height);
-            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
-
-            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-            assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
-                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
-        }
-    }
-
-    /**
-     * Test that for bitmaps with both width and height <= SMALL_SIDE,
-     * the mini bitmap is always created with both width and height <= SMALL_SIDE
-     */
-    @Test
-    public void testSmallMiniBitmapCreation() {
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-        int nSimulations = 10;
-        for (int i = 0; i < nSimulations; i++) {
-            resetCounters();
-            int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
-            int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
-            Bitmap bitmap = getMockBitmap(width, height);
-            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
-
-            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-            assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
-                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
-        }
-    }
-
-    /**
-     * Test that for a new color extractor with information
-     * (number of pages, display dimensions, wallpaper bitmap) given in random order,
-     * the colors are processed and all the callbacks are properly executed.
-     */
-    @Test
-    public void testNewColorExtraction() {
-        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-
-        int nSimulations = 10;
-        for (int i = 0; i < nSimulations; i++) {
-            resetCounters();
-            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-            List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
-            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
-            List<Runnable> tasks = Arrays.asList(
-                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
-                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
-                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
-                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
-                    () -> spyWallpaperColorExtractor.addLocalColorsAreas(
-                            regions));
-            Collections.shuffle(tasks);
-            tasks.forEach(Runnable::run);
-
-            assertThat(mActivatedCount).isEqualTo(1);
-            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-            assertThat(mColorsProcessed).isEqualTo(regions.size());
-
-            spyWallpaperColorExtractor.removeLocalColorAreas(regions);
-            assertThat(mDeactivatedCount).isEqualTo(1);
-        }
-    }
-
-    /**
-     * Test that the method removeLocalColorAreas behaves properly and does not call
-     * the onDeactivated callback unless all color areas are removed.
-     */
-    @Test
-    public void testRemoveColors() {
-        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-        int nSimulations = 10;
-        for (int i = 0; i < nSimulations; i++) {
-            resetCounters();
-            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-            List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
-            List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
-            List<RectF> regions = new ArrayList<>();
-            regions.addAll(regions1);
-            regions.addAll(regions2);
-            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
-            List<Runnable> tasks = Arrays.asList(
-                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
-                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
-                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
-                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
-                    () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1));
-
-            spyWallpaperColorExtractor.addLocalColorsAreas(regions);
-            assertThat(mActivatedCount).isEqualTo(1);
-            Collections.shuffle(tasks);
-            tasks.forEach(Runnable::run);
-
-            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-            assertThat(mDeactivatedCount).isEqualTo(0);
-            spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
-            assertThat(mDeactivatedCount).isEqualTo(1);
-        }
-    }
-
-    /**
-     * Test that if we change some information (wallpaper bitmap, number of pages),
-     * the colors are correctly recomputed.
-     * Test that if we remove some color areas in the middle of the process,
-     * only the remaining areas are recomputed.
-     */
-    @Test
-    public void testRecomputeColorExtraction() {
-        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-        List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
-        List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
-        List<RectF> regions = new ArrayList<>();
-        regions.addAll(regions1);
-        regions.addAll(regions2);
-        spyWallpaperColorExtractor.addLocalColorsAreas(regions);
-        assertThat(mActivatedCount).isEqualTo(1);
-        int nPages = PAGES_LOW;
-        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
-        spyWallpaperColorExtractor.onPageChanged(nPages);
-        spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
-
-        int nSimulations = 20;
-        for (int i = 0; i < nSimulations; i++) {
-            resetCounters();
-
-            // verify that if we remove some regions, they are not recomputed after other changes
-            if (i == nSimulations / 2) {
-                regions.removeAll(regions2);
-                spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
-            }
-
-            if (Math.random() >= 0.5) {
-                int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
-                if (nPagesNew == nPages) continue;
-                nPages = nPagesNew;
-                spyWallpaperColorExtractor.onPageChanged(nPagesNew);
-            } else {
-                Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-                spyWallpaperColorExtractor.onBitmapChanged(newBitmap);
-                assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-            }
-            assertThat(mColorsProcessed).isEqualTo(regions.size());
-        }
-        spyWallpaperColorExtractor.removeLocalColorAreas(regions);
-        assertThat(mDeactivatedCount).isEqualTo(1);
-    }
-
-    @Test
-    public void testCleanUp() {
-        resetCounters();
-        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-        doNothing().when(bitmap).recycle();
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-        spyWallpaperColorExtractor.onPageChanged(PAGES_LOW);
-        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
-        assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-        spyWallpaperColorExtractor.cleanUp();
-        spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
-        assertThat(mColorsProcessed).isEqualTo(0);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
new file mode 100644
index 0000000..7e8ffeb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.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.systemui.wallpapers.canvas;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class WallpaperLocalColorExtractorTest extends SysuiTestCase {
+    private static final int LOW_BMP_WIDTH = 128;
+    private static final int LOW_BMP_HEIGHT = 128;
+    private static final int HIGH_BMP_WIDTH = 3000;
+    private static final int HIGH_BMP_HEIGHT = 4000;
+    private static final int VERY_LOW_BMP_WIDTH = 1;
+    private static final int VERY_LOW_BMP_HEIGHT = 1;
+    private static final int DISPLAY_WIDTH = 1920;
+    private static final int DISPLAY_HEIGHT = 1080;
+
+    private static final int PAGES_LOW = 4;
+    private static final int PAGES_HIGH = 7;
+
+    private static final int MIN_AREAS = 4;
+    private static final int MAX_AREAS = 10;
+
+    private int mMiniBitmapWidth;
+    private int mMiniBitmapHeight;
+
+    @Mock
+    private Executor mBackgroundExecutor;
+
+    private int mColorsProcessed;
+    private int mMiniBitmapUpdatedCount;
+    private int mActivatedCount;
+    private int mDeactivatedCount;
+
+    @Before
+    public void setUp() throws Exception {
+        allowTestableLooperAsMainThread();
+        MockitoAnnotations.initMocks(this);
+        doAnswer(invocation ->  {
+            ((Runnable) invocation.getArgument(0)).run();
+            return null;
+        }).when(mBackgroundExecutor).execute(any(Runnable.class));
+    }
+
+    private void resetCounters() {
+        mColorsProcessed = 0;
+        mMiniBitmapUpdatedCount = 0;
+        mActivatedCount = 0;
+        mDeactivatedCount = 0;
+    }
+
+    private Bitmap getMockBitmap(int width, int height) {
+        Bitmap bitmap = mock(Bitmap.class);
+        when(bitmap.getWidth()).thenReturn(width);
+        when(bitmap.getHeight()).thenReturn(height);
+        return bitmap;
+    }
+
+    private WallpaperLocalColorExtractor getSpyWallpaperLocalColorExtractor() {
+
+        WallpaperLocalColorExtractor colorExtractor = new WallpaperLocalColorExtractor(
+                mBackgroundExecutor,
+                new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
+                    @Override
+                    public void onColorsProcessed(List<RectF> regions,
+                            List<WallpaperColors> colors) {
+                        assertThat(regions.size()).isEqualTo(colors.size());
+                        mColorsProcessed += regions.size();
+                    }
+
+                    @Override
+                    public void onMiniBitmapUpdated() {
+                        mMiniBitmapUpdatedCount++;
+                    }
+
+                    @Override
+                    public void onActivated() {
+                        mActivatedCount++;
+                    }
+
+                    @Override
+                    public void onDeactivated() {
+                        mDeactivatedCount++;
+                    }
+                });
+        WallpaperLocalColorExtractor spyColorExtractor = spy(colorExtractor);
+
+        doAnswer(invocation -> {
+            mMiniBitmapWidth = invocation.getArgument(1);
+            mMiniBitmapHeight = invocation.getArgument(2);
+            return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
+        }).when(spyColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+
+
+        doAnswer(invocation -> getMockBitmap(
+                        invocation.getArgument(1),
+                        invocation.getArgument(2)))
+                .when(spyColorExtractor)
+                .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+
+        doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
+                .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class));
+
+        return spyColorExtractor;
+    }
+
+    private RectF randomArea() {
+        float width = (float) Math.random();
+        float startX = (float) (Math.random() * (1 - width));
+        float height = (float) Math.random();
+        float startY = (float) (Math.random() * (1 - height));
+        return new RectF(startX, startY, startX + width, startY + height);
+    }
+
+    private List<RectF> listOfRandomAreas(int min, int max) {
+        int nAreas = randomBetween(min, max);
+        List<RectF> result = new ArrayList<>();
+        for (int i = 0; i < nAreas; i++) {
+            result.add(randomArea());
+        }
+        return result;
+    }
+
+    private int randomBetween(int minIncluded, int maxIncluded) {
+        return (int) (Math.random() * ((maxIncluded - minIncluded) + 1)) + minIncluded;
+    }
+
+    /**
+     * Test that for bitmaps of random dimensions, the mini bitmap is always created
+     * with either a width <= SMALL_SIDE or a height <= SMALL_SIDE
+     */
+    @Test
+    public void testMiniBitmapCreation() {
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
+            int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
+            Bitmap bitmap = getMockBitmap(width, height);
+            spyColorExtractor.onBitmapChanged(bitmap);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
+                    .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
+        }
+    }
+
+    /**
+     * Test that for bitmaps with both width and height <= SMALL_SIDE,
+     * the mini bitmap is always created with both width and height <= SMALL_SIDE
+     */
+    @Test
+    public void testSmallMiniBitmapCreation() {
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
+            int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
+            Bitmap bitmap = getMockBitmap(width, height);
+            spyColorExtractor.onBitmapChanged(bitmap);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
+                    .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
+        }
+    }
+
+    /**
+     * Test that for a new color extractor with information
+     * (number of pages, display dimensions, wallpaper bitmap) given in random order,
+     * the colors are processed and all the callbacks are properly executed.
+     */
+    @Test
+    public void testNewColorExtraction() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+            List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
+            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
+            List<Runnable> tasks = Arrays.asList(
+                    () -> spyColorExtractor.onPageChanged(nPages),
+                    () -> spyColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyColorExtractor.setDisplayDimensions(
+                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    () -> spyColorExtractor.addLocalColorsAreas(
+                            regions));
+            Collections.shuffle(tasks);
+            tasks.forEach(Runnable::run);
+
+            assertThat(mActivatedCount).isEqualTo(1);
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(mColorsProcessed).isEqualTo(regions.size());
+
+            spyColorExtractor.removeLocalColorAreas(regions);
+            assertThat(mDeactivatedCount).isEqualTo(1);
+        }
+    }
+
+    /**
+     * Test that the method removeLocalColorAreas behaves properly and does not call
+     * the onDeactivated callback unless all color areas are removed.
+     */
+    @Test
+    public void testRemoveColors() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        int nSimulations = 10;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+            WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+            List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+            List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+            List<RectF> regions = new ArrayList<>();
+            regions.addAll(regions1);
+            regions.addAll(regions2);
+            int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
+            List<Runnable> tasks = Arrays.asList(
+                    () -> spyColorExtractor.onPageChanged(nPages),
+                    () -> spyColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyColorExtractor.setDisplayDimensions(
+                            DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    () -> spyColorExtractor.removeLocalColorAreas(regions1));
+
+            spyColorExtractor.addLocalColorsAreas(regions);
+            assertThat(mActivatedCount).isEqualTo(1);
+            Collections.shuffle(tasks);
+            tasks.forEach(Runnable::run);
+
+            assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            assertThat(mDeactivatedCount).isEqualTo(0);
+            spyColorExtractor.removeLocalColorAreas(regions2);
+            assertThat(mDeactivatedCount).isEqualTo(1);
+        }
+    }
+
+    /**
+     * Test that if we change some information (wallpaper bitmap, number of pages),
+     * the colors are correctly recomputed.
+     * Test that if we remove some color areas in the middle of the process,
+     * only the remaining areas are recomputed.
+     */
+    @Test
+    public void testRecomputeColorExtraction() {
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+        List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+        List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
+        List<RectF> regions = new ArrayList<>();
+        regions.addAll(regions1);
+        regions.addAll(regions2);
+        spyColorExtractor.addLocalColorsAreas(regions);
+        assertThat(mActivatedCount).isEqualTo(1);
+        int nPages = PAGES_LOW;
+        spyColorExtractor.onBitmapChanged(bitmap);
+        spyColorExtractor.onPageChanged(nPages);
+        spyColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
+
+        int nSimulations = 20;
+        for (int i = 0; i < nSimulations; i++) {
+            resetCounters();
+
+            // verify that if we remove some regions, they are not recomputed after other changes
+            if (i == nSimulations / 2) {
+                regions.removeAll(regions2);
+                spyColorExtractor.removeLocalColorAreas(regions2);
+            }
+
+            if (Math.random() >= 0.5) {
+                int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
+                if (nPagesNew == nPages) continue;
+                nPages = nPagesNew;
+                spyColorExtractor.onPageChanged(nPagesNew);
+            } else {
+                Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+                spyColorExtractor.onBitmapChanged(newBitmap);
+                assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+            }
+            assertThat(mColorsProcessed).isEqualTo(regions.size());
+        }
+        spyColorExtractor.removeLocalColorAreas(regions);
+        assertThat(mDeactivatedCount).isEqualTo(1);
+    }
+
+    @Test
+    public void testCleanUp() {
+        resetCounters();
+        Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
+        doNothing().when(bitmap).recycle();
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+        spyColorExtractor.onPageChanged(PAGES_LOW);
+        spyColorExtractor.onBitmapChanged(bitmap);
+        assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
+        spyColorExtractor.cleanUp();
+        spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
+        assertThat(mColorsProcessed).isEqualTo(0);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 09da52e..db3b9b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -77,20 +77,24 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
@@ -154,6 +158,7 @@
 import java.util.List;
 import java.util.Optional;
 
+@FlakyTest
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -190,6 +195,8 @@
     private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock
     private AuthController mAuthController;
+    @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private SysUiState mSysUiState;
     private boolean mSysUiStateBubblesExpanded;
@@ -290,7 +297,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController);
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager);
         mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
@@ -343,7 +350,8 @@
                         mock(NotificationInterruptLogger.class),
                         mock(Handler.class),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class)
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class)
                 );
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
@@ -389,6 +397,7 @@
                 mCommonNotifCollection,
                 mNotifPipeline,
                 mSysUiState,
+                mock(FeatureFlags.class),
                 syncExecutor);
         mBubblesManager.addNotifCallback(mNotifCallback);
 
@@ -1322,7 +1331,7 @@
         spyOn(mContext);
         mBubbleController.updateBubble(mBubbleEntry);
         verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
-                mFilterArgumentCaptor.capture());
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
         assertThat(mFilterArgumentCaptor.getValue().getAction(0)).isEqualTo(
                 Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         assertThat(mFilterArgumentCaptor.getValue().getAction(1)).isEqualTo(
@@ -1342,7 +1351,7 @@
         mBubbleController.updateBubble(mBubbleEntry);
         mBubbleData.setExpanded(true);
         verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
-                mFilterArgumentCaptor.capture());
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
         Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
 
@@ -1356,7 +1365,7 @@
         mBubbleData.setExpanded(true);
 
         verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
-                mFilterArgumentCaptor.capture());
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
         Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         i.putExtra("reason", "gestureNav");
         mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
@@ -1370,7 +1379,7 @@
         mBubbleData.setExpanded(true);
 
         verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
-                mFilterArgumentCaptor.capture());
+                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
 
         Intent i = new Intent(Intent.ACTION_SCREEN_OFF);
         mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 9635faf..e5316bc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -22,6 +22,7 @@
 import android.os.PowerManager;
 import android.service.dreams.IDreamManager;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
@@ -46,7 +47,8 @@
             NotificationInterruptLogger logger,
             Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         super(contentResolver,
                 powerManager,
                 dreamManager,
@@ -58,7 +60,8 @@
                 logger,
                 mainHandler,
                 flags,
-                keyguardNotificationVisibilityProvider);
+                keyguardNotificationVisibilityProvider,
+                uiEventLogger);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index cebe946..7ae47b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -28,13 +28,15 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.floating.FloatingTasks;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -49,6 +51,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 /**
  * Tests for {@link WMShell}.
@@ -75,16 +78,31 @@
     @Mock ProtoTracer mProtoTracer;
     @Mock UserTracker mUserTracker;
     @Mock ShellExecutor mSysUiMainExecutor;
-    @Mock FloatingTasks mFloatingTasks;
+    @Mock NoteTaskInitializer mNoteTaskInitializer;
+    @Mock DesktopMode mDesktopMode;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
-                Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
-                mCommandQueue, mConfigurationController, mKeyguardStateController,
-                mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
-                mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
+        mWMShell = new WMShell(
+                mContext,
+                mShellInterface,
+                Optional.of(mPip),
+                Optional.of(mSplitScreen),
+                Optional.of(mOneHanded),
+                Optional.of(mDesktopMode),
+                mCommandQueue,
+                mConfigurationController,
+                mKeyguardStateController,
+                mKeyguardUpdateMonitor,
+                mScreenLifecycle,
+                mSysUiState,
+                mProtoTracer,
+                mWakefulnessLifecycle,
+                mUserTracker,
+                mNoteTaskInitializer,
+                mSysUiMainExecutor
+        );
     }
 
     @Test
@@ -103,4 +121,12 @@
         verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
         verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class));
     }
+
+    @Test
+    public void initDesktopMode_registersListener() {
+        mWMShell.initDesktopMode(mDesktopMode);
+        verify(mDesktopMode).addListener(
+                any(DesktopModeTaskRepository.VisibleTasksListener.class),
+                any(Executor.class));
+    }
 }
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/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java
index 34c83bd..d47e88f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java
@@ -39,6 +39,7 @@
     private boolean mShouldEnforceBouncer;
     private boolean mIsReportingEnabled;
     private boolean mIsFalseRobustTap;
+    private boolean mIsFalseLongTap;
     private boolean mDestroyed;
     private boolean mIsProximityNear;
 
@@ -87,6 +88,10 @@
         mIsProximityNear = proxNear;
     }
 
+    public void setFalseLongTap(boolean falseLongTap) {
+        mIsFalseLongTap = falseLongTap;
+    }
+
     @Override
     public boolean isSimpleTap() {
         checkDestroyed();
@@ -100,6 +105,12 @@
     }
 
     @Override
+    public boolean isFalseLongTap(int penalty) {
+        checkDestroyed();
+        return mIsFalseLongTap;
+    }
+
+    @Override
     public boolean isFalseDoubleTap() {
         checkDestroyed();
         return mIsFalseDoubleTap;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 5d52be2..6c82cef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -21,14 +21,14 @@
 class FakeFeatureFlags : FeatureFlags {
     private val booleanFlags = mutableMapOf<Int, Boolean>()
     private val stringFlags = mutableMapOf<Int, String>()
+    private val intFlags = mutableMapOf<Int, Int>()
     private val knownFlagNames = mutableMapOf<Int, String>()
     private val flagListeners = mutableMapOf<Int, MutableSet<FlagListenable.Listener>>()
     private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
 
     init {
-        Flags.getFlagFields().forEach { field ->
-            val flag: Flag<*> = field.get(null) as Flag<*>
-            knownFlagNames[flag.id] = field.name
+        FlagsFactory.knownFlags.forEach { entry: Map.Entry<String, Flag<*>> ->
+            knownFlagNames[entry.value.id] = entry.key
         }
     }
 
@@ -87,14 +87,16 @@
 
     override fun isEnabled(flag: ResourceBooleanFlag): Boolean = requireBooleanValue(flag.id)
 
-    override fun isEnabled(flag: DeviceConfigBooleanFlag): Boolean = requireBooleanValue(flag.id)
-
     override fun isEnabled(flag: SysPropBooleanFlag): Boolean = requireBooleanValue(flag.id)
 
     override fun getString(flag: StringFlag): String = requireStringValue(flag.id)
 
     override fun getString(flag: ResourceStringFlag): String = requireStringValue(flag.id)
 
+    override fun getInt(flag: IntFlag): Int = requireIntValue(flag.id)
+
+    override fun getInt(flag: ResourceIntFlag): Int = requireIntValue(flag.id)
+
     override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
         flagListeners.getOrPut(flag.id) { mutableSetOf() }.add(listener)
         listenerFlagIds.getOrPut(listener) { mutableSetOf() }.add(flag.id)
@@ -118,11 +120,16 @@
 
     private fun requireBooleanValue(flagId: Int): Boolean {
         return booleanFlags[flagId]
-            ?: error("Flag ${flagName(flagId)} was accessed but not specified.")
+            ?: error("Flag ${flagName(flagId)} was accessed as boolean but not specified.")
     }
 
     private fun requireStringValue(flagId: Int): String {
         return stringFlags[flagId]
-            ?: error("Flag ${flagName(flagId)} was accessed but not specified.")
+            ?: error("Flag ${flagName(flagId)} was accessed as string but not specified.")
+    }
+
+    private fun requireIntValue(flagId: Int): Int {
+        return intFlags[flagId]
+            ?: error("Flag ${flagName(flagId)} was accessed as int but not specified.")
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 725b1f4..a798f40 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,9 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -41,9 +44,29 @@
     private val _isDozing = MutableStateFlow(false)
     override val isDozing: Flow<Boolean> = _isDozing
 
+    private val _isDreaming = MutableStateFlow(false)
+    override val isDreaming: Flow<Boolean> = _isDreaming
+
     private val _dozeAmount = MutableStateFlow(0f)
     override val dozeAmount: Flow<Float> = _dozeAmount
 
+    private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
+    override val statusBarState: Flow<StatusBarState> = _statusBarState
+
+    private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP)
+    override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState
+
+    private val _isUdfpsSupported = MutableStateFlow(false)
+
+    private val _isBouncerShowing = MutableStateFlow(false)
+    override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing
+
+    private val _isKeyguardGoingAway = MutableStateFlow(false)
+    override val isKeyguardGoingAway: Flow<Boolean> = _isKeyguardGoingAway
+
+    private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE)
+    override val biometricUnlockState: Flow<BiometricUnlockModel> = _biometricUnlockState
+
     override fun isKeyguardShowing(): Boolean {
         return _isKeyguardShowing.value
     }
@@ -71,4 +94,8 @@
     fun setDozeAmount(dozeAmount: Float) {
         _dozeAmount.value = dozeAmount
     }
+
+    override fun isUdfpsSupported(): Boolean {
+        return _isUdfpsSupported.value
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
new file mode 100644
index 0000000..6c44244
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.repository
+
+import android.annotation.FloatRange
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import java.util.UUID
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+
+/** Fake implementation of [KeyguardTransitionRepository] */
+class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
+
+    private val _transitions = MutableSharedFlow<TransitionStep>()
+    override val transitions: SharedFlow<TransitionStep> = _transitions
+
+    suspend fun sendTransitionStep(step: TransitionStep) {
+        _transitions.emit(step)
+    }
+
+    override fun startTransition(info: TransitionInfo): UUID? {
+        return null
+    }
+
+    override fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) = Unit
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 5272585..c33ce5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener
 import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,7 +54,7 @@
 
     override fun init() {}
 
-    override fun showDialog(viewLaunchedFrom: View?) {}
+    override fun showDialog(expandable: Expandable?) {}
 
     override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
         numRunningPackagesListeners.add(listener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
index 2a9aedd..63448e2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
@@ -28,8 +28,6 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -43,7 +41,6 @@
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
-import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.security.data.repository.SecurityRepository
 import com.android.systemui.security.data.repository.SecurityRepositoryImpl
 import com.android.systemui.settings.FakeUserTracker
@@ -54,10 +51,10 @@
 import com.android.systemui.statusbar.policy.SecurityController
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.time.FakeSystemClock
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestCoroutineDispatcher
 
@@ -68,7 +65,6 @@
 class FooterActionsTestUtils(
     private val context: Context,
     private val testableLooper: TestableLooper,
-    private val fakeClock: FakeSystemClock = FakeSystemClock(),
 ) {
     /** Enable or disable the user switcher in the settings. */
     fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) {
@@ -99,13 +95,12 @@
     /** Create a [FooterActionsInteractor] to be used in tests. */
     fun footerActionsInteractor(
         activityStarter: ActivityStarter = mock(),
-        featureFlags: FeatureFlags = FakeFeatureFlags(),
         metricsLogger: MetricsLogger = FakeMetricsLogger(),
         uiEventLogger: UiEventLogger = UiEventLoggerFake(),
         deviceProvisionedController: DeviceProvisionedController = mock(),
         qsSecurityFooterUtils: QSSecurityFooterUtils = mock(),
         fgsManagerController: FgsManagerController = mock(),
-        userSwitchDialogController: UserSwitchDialogController = mock(),
+        userInteractor: UserInteractor = mock(),
         securityRepository: SecurityRepository = securityRepository(),
         foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(),
         userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(),
@@ -114,13 +109,12 @@
     ): FooterActionsInteractor {
         return FooterActionsInteractorImpl(
             activityStarter,
-            featureFlags,
             metricsLogger,
             uiEventLogger,
             deviceProvisionedController,
             qsSecurityFooterUtils,
             fgsManagerController,
-            userSwitchDialogController,
+            userInteractor,
             securityRepository,
             foregroundServicesRepository,
             userSwitcherRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index 9726bf8..a7eadba 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -68,4 +68,8 @@
 
         callbacks.forEach { it.onUserChanged(_userId, userContext) }
     }
+
+    fun onProfileChanged() {
+        callbacks.forEach { it.onProfilesChanged(_userProfiles) }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 4df8aa4..ea5a302 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -20,26 +20,15 @@
 import android.content.pm.UserInfo
 import android.os.UserHandle
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.yield
 
 class FakeUserRepository : UserRepository {
 
-    private val _users = MutableStateFlow<List<UserModel>>(emptyList())
-    override val users: Flow<List<UserModel>> = _users.asStateFlow()
-    override val selectedUser: Flow<UserModel> =
-        users.map { models -> models.first { model -> model.isSelected } }
-
-    private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
-    override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
-
     private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
     override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
         _userSwitcherSettings.asStateFlow()
@@ -52,9 +41,6 @@
 
     override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
 
-    private val _isActionableWhenLocked = MutableStateFlow(false)
-    override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
-
     private var _isGuestUserAutoCreated: Boolean = false
     override val isGuestUserAutoCreated: Boolean
         get() = _isGuestUserAutoCreated
@@ -63,6 +49,8 @@
 
     override val isGuestUserCreationScheduled = AtomicBoolean()
 
+    override var isStatusBarUserChipEnabled: Boolean = false
+
     override var secondaryUserId: Int = UserHandle.USER_NULL
 
     override var isRefreshUsersPaused: Boolean = false
@@ -100,35 +88,6 @@
         yield()
     }
 
-    fun setUsers(models: List<UserModel>) {
-        _users.value = models
-    }
-
-    suspend fun setSelectedUser(userId: Int) {
-        check(_users.value.find { it.id == userId } != null) {
-            "Cannot select a user with ID $userId - no user with that ID found!"
-        }
-
-        setUsers(
-            _users.value.map { model ->
-                when {
-                    model.isSelected && model.id != userId -> model.copy(isSelected = false)
-                    !model.isSelected && model.id == userId -> model.copy(isSelected = true)
-                    else -> model
-                }
-            }
-        )
-        yield()
-    }
-
-    fun setActions(models: List<UserActionModel>) {
-        _actions.value = models
-    }
-
-    fun setActionableWhenLocked(value: Boolean) {
-        _isActionableWhenLocked.value = value
-    }
-
     fun setGuestUserAutoCreated(value: Boolean) {
         _isGuestUserAutoCreated = value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
index 33ece00..21e16a1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.util;
 
-import android.content.Context;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
@@ -83,7 +82,7 @@
     }
 
     @Override
-    public void enforceReadPermission(Context context, String namespace) {
+    public void enforceReadPermission(String namespace) {
         // no-op
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 8d171be..69575a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -26,7 +26,9 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatcher
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
 
 /**
  * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
@@ -89,7 +91,8 @@
  *
  * @see Mockito.when
  */
-fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
 
 /**
  * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 23c7a61..2d6d29a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -62,7 +62,11 @@
     }
 
     @Override
-    public void setSignalIcon(String slot, WifiIconState state) {
+    public void setWifiIcon(String slot, WifiIconState state) {
+    }
+
+    @Override
+    public void setNewWifiIcon() {
     }
 
     @Override
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index 043aff6..b568186 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.unfold.progress
 
+import android.os.Trace
 import android.util.Log
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -117,6 +118,7 @@
 
         if (DEBUG) {
             Log.d(TAG, "onFoldUpdate = $update")
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update)
         }
     }
 
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 07473b3..808128d 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.updates
 
 import android.os.Handler
+import android.os.Trace
 import android.util.Log
 import androidx.annotation.FloatRange
 import androidx.annotation.VisibleForTesting
@@ -108,6 +109,7 @@
     private fun onHingeAngle(angle: Float) {
         if (DEBUG) {
             Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle")
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt())
         }
 
         val isClosing = angle < lastHingeAngle
@@ -115,8 +117,16 @@
         val closingThresholdMet = closingThreshold == null || angle < closingThreshold
         val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
         val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+        val screenAvailableEventSent = isUnfoldHandled
 
-        if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) {
+        if (isClosing // hinge angle should be decreasing since last update
+                && closingThresholdMet // hinge angle is below certain threshold
+                && !closingEventDispatched  // we haven't sent closing event already
+                && !isFullyOpened // do not send closing event if we are in fully opened hinge
+                                  // angle range as closing threshold could overlap this range
+                && screenAvailableEventSent // do not send closing event if we are still in
+                                            // the process of turning on the inner display
+        ) {
             notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
         }
 
diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp
index 05135b2..e4f80e2 100644
--- a/packages/VpnDialogs/Android.bp
+++ b/packages/VpnDialogs/Android.bp
@@ -23,10 +23,15 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
+android_library {
+    name: "VpnDialogsLib",
+    srcs: ["src/**/*.java"],
+}
+
 android_app {
     name: "VpnDialogs",
     certificate: "platform",
     privileged: true,
-    srcs: ["src/**/*.java"],
+    static_libs: ["VpnDialogsLib"],
     platform_apis: true,
 }
diff --git a/packages/VpnDialogs/res/values-de/strings.xml b/packages/VpnDialogs/res/values-de/strings.xml
index f38e395..1de7805 100644
--- a/packages/VpnDialogs/res/values-de/strings.xml
+++ b/packages/VpnDialogs/res/values-de/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="prompt" msgid="3183836924226407828">"Verbindungsanfrage"</string>
-    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> möchte eine VPN-Verbindung herstellen, über die der Netzwerkverkehr überwacht werden kann. Lass die Verbindung nur zu, wenn die App vertrauenswürdig ist. Wenn VPN aktiv ist, wird oben im Display &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; angezeigt."</string>
+    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> möchte eine VPN-Verbindung herstellen, über die der Netzwerkverkehr überwacht werden kann. Lass die Verbindung nur zu, wenn die App vertrauenswürdig ist. &lt;br /&gt; &lt;br /&gt; Wenn das VPN aktiv ist, wird oben im Display &lt;img src=vpn_icon /&gt; angezeigt."</string>
     <string name="warning" product="tv" msgid="5188957997628124947">"<xliff:g id="APP">%s</xliff:g> möchte eine VPN-Verbindung herstellen, über die der Netzwerkverkehr überwacht werden kann. Lass die Verbindung nur zu, wenn die App vertrauenswürdig ist. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; wird auf dem Display angezeigt, wenn VPN aktiv ist."</string>
     <string name="legacy_title" msgid="192936250066580964">"VPN ist verbunden"</string>
     <string name="session" msgid="6470628549473641030">"Sitzung:"</string>
diff --git a/packages/VpnDialogs/res/values-es-rUS/strings.xml b/packages/VpnDialogs/res/values-es-rUS/strings.xml
index 108a24e..232b53a 100644
--- a/packages/VpnDialogs/res/values-es-rUS/strings.xml
+++ b/packages/VpnDialogs/res/values-es-rUS/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="prompt" msgid="3183836924226407828">"Solicitud de conexión"</string>
-    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN capaz de controlar el tráfico de la red. Acéptala solo si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparece en la parte superior de la pantalla cuando se activa la VPN."</string>
+    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN capaz de supervisar el tráfico de la red. Acéptala solo si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparece en la parte superior de la pantalla cuando se activa la VPN."</string>
     <string name="warning" product="tv" msgid="5188957997628124947">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN que le permita supervisar el tráfico de red. Solo acéptala si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparecerá en tu pantalla cuando se active la VPN."</string>
     <string name="legacy_title" msgid="192936250066580964">"La VPN está conectada."</string>
     <string name="session" msgid="6470628549473641030">"Sesión:"</string>
diff --git a/packages/VpnDialogs/res/values-es/strings.xml b/packages/VpnDialogs/res/values-es/strings.xml
index 9bf86f5..4e21fd09 100644
--- a/packages/VpnDialogs/res/values-es/strings.xml
+++ b/packages/VpnDialogs/res/values-es/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="prompt" msgid="3183836924226407828">"Solicitud de conexión"</string>
-    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN para controlar el tráfico de red. Solo debes aceptarla si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparece en la parte superior de la pantalla cuando se active la conexión VPN."</string>
+    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN para controlar el tráfico de red. Solo debes aceptarla si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparece en la parte superior de la pantalla cuando la conexión VPN está activa."</string>
     <string name="warning" product="tv" msgid="5188957997628124947">"<xliff:g id="APP">%s</xliff:g> quiere configurar una conexión VPN que le permita monitorizar el tráfico de red. Acéptalo solo si confías en la fuente. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; aparecerá en la pantalla cuando la VPN esté activa."</string>
     <string name="legacy_title" msgid="192936250066580964">"La VPN está conectada"</string>
     <string name="session" msgid="6470628549473641030">"Sesión:"</string>
diff --git a/packages/VpnDialogs/res/values-it/strings.xml b/packages/VpnDialogs/res/values-it/strings.xml
index c443c51..118fb6a 100644
--- a/packages/VpnDialogs/res/values-it/strings.xml
+++ b/packages/VpnDialogs/res/values-it/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="prompt" msgid="3183836924226407828">"Richiesta di connessione"</string>
-    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> vuole impostare una connessione VPN che le consenta di monitorare il traffico di rete. Accetta soltanto se ritieni la fonte attendibile. Quando la connessione VPN è attiva, nella parte superiore dello schermo viene visualizzata l\'icona &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt;."</string>
+    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> vuole impostare una connessione VPN per monitorare il traffico di rete. Accetta soltanto se ritieni la fonte attendibile. Quando la connessione VPN è attiva, in alto sullo schermo appare l\'icona &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt;."</string>
     <string name="warning" product="tv" msgid="5188957997628124947">"<xliff:g id="APP">%s</xliff:g> vuole configurare una connessione VPN che le consenta di monitorare il traffico di rete. Accetta soltanto se ritieni la fonte attendibile. Quando la connessione VPN è attiva, sullo schermo viene visualizzata l\'icona &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt;."</string>
     <string name="legacy_title" msgid="192936250066580964">"VPN connessa"</string>
     <string name="session" msgid="6470628549473641030">"Sessione:"</string>
diff --git a/packages/VpnDialogs/res/values-nl/strings.xml b/packages/VpnDialogs/res/values-nl/strings.xml
index 33f8a89..76f56af 100644
--- a/packages/VpnDialogs/res/values-nl/strings.xml
+++ b/packages/VpnDialogs/res/values-nl/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="prompt" msgid="3183836924226407828">"Verbindingsverzoek"</string>
-    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> wil een VPN-verbinding opzetten om netwerkverkeer te controleren. Accepteer het verzoek alleen als je de bron vertrouwt. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; wordt boven aan je scherm weergegeven wanneer VPN actief is."</string>
+    <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> wil een VPN-verbinding instellen waarmee de app het netwerkverkeer kan bijhouden. Accepteer dit alleen als je de bron vertrouwt. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; verschijnt op je scherm als het VPN actief is."</string>
     <string name="warning" product="tv" msgid="5188957997628124947">"<xliff:g id="APP">%s</xliff:g> wil een VPN-verbinding instellen waarmee de app het netwerkverkeer kan bijhouden. Accepteer dit alleen als je de bron vertrouwt. &lt;br /&gt; &lt;br /&gt; &lt;img src=vpn_icon /&gt; verschijnt op je scherm als het VPN actief is."</string>
     <string name="legacy_title" msgid="192936250066580964">"Verbinding met VPN"</string>
     <string name="session" msgid="6470628549473641030">"Sessie:"</string>
diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml
index f971a09..28e7272 100644
--- a/packages/VpnDialogs/res/values/strings.xml
+++ b/packages/VpnDialogs/res/values/strings.xml
@@ -100,4 +100,33 @@
          without any consequences. [CHAR LIMIT=20] -->
     <string name="dismiss">Dismiss</string>
 
+    <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+         into displaying what they want. The system will attempt to sanitize the label, and if the
+         label is deemed dangerous, then this string is used instead. The first argument is the
+         first 30 characters of the label, and the second argument is the package name of the app.
+         Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+         "My VPN app wants to set up a VPN connection...". If the label is very long, then, this
+         will be used to show "VerylongVPNlabel… (com.my.vpn.app) wants to set up a VPN
+         connection...". For this case, the code will refer to sanitized_vpn_label_with_ellipsis.
+    -->
+    <string name="sanitized_vpn_label_with_ellipsis">
+        <xliff:g id="sanitized_vpn_label_with_ellipsis" example="My VPN app">%1$s</xliff:g>… (
+        <xliff:g id="sanitized_vpn_label_with_ellipsis" example="com.my.vpn.app">%2$s</xliff:g>)
+    </string>
+
+    <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
+         into displaying what they want. The system will attempt to sanitize the label, and if the
+         label is deemed dangerous, then this string is used instead. The first argument is the
+         label, and the second argument is the package name of the app.
+         Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
+         "My VPN app wants to set up a VPN connection...". If the VPN label contains HTML tag but
+         the length is not very long, the dialog will show "VpnLabelWith&lt;br&gt;HtmlTag
+         (com.my.vpn.app) wants to set up a VPN connection...". For this case, the code will refer
+         to sanitized_vpn_label.
+    -->
+    <string name="sanitized_vpn_label">
+        <xliff:g id="sanitized_vpn_label" example="My VPN app">%1$s</xliff:g> (
+        <xliff:g id="sanitized_vpn_label" example="com.my.vpn.app">%2$s</xliff:g>)
+    </string>
+
 </resources>
diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
index fb23678..a98d6d8 100644
--- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
+++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java
@@ -33,6 +33,7 @@
 import android.widget.Button;
 import android.widget.TextView;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AlertActivity;
 import com.android.internal.net.VpnConfig;
 
@@ -40,12 +41,19 @@
         implements DialogInterface.OnClickListener, ImageGetter {
     private static final String TAG = "VpnConfirm";
 
+    // Usually the label represents the app name, 150 code points might be enough to display the app
+    // name, and 150 code points won't cover the warning message from VpnDialog.
+    @VisibleForTesting
+    static final int MAX_VPN_LABEL_LENGTH = 150;
+
     @VpnManager.VpnType private final int mVpnType;
 
     private String mPackage;
 
     private VpnManager mVm;
 
+    private View mView;
+
     public ConfirmDialog() {
         this(VpnManager.TYPE_VPN_SERVICE);
     }
@@ -54,6 +62,43 @@
         mVpnType = vpnType;
     }
 
+    /**
+     * This function will use the string resource to combine the VPN label and the package name.
+     *
+     * If the VPN label violates the length restriction, the first 30 code points of VPN label and
+     * the package name will be returned. Or return the VPN label and the package name directly if
+     * the VPN label doesn't violate the length restriction.
+     *
+     * The result will be something like,
+     * - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app)
+     *   if the VPN label violates the length restriction.
+     * or
+     * - VpnLabelWith&lt;br&gt;HtmlTag (com.vpn.app)
+     *   if the VPN label doesn't violate the length restriction.
+     *
+     */
+    private String getSimplifiedLabel(String vpnLabel, String packageName) {
+        if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) {
+            return getString(R.string.sanitized_vpn_label_with_ellipsis,
+                    vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)),
+                            packageName);
+        }
+
+        return getString(R.string.sanitized_vpn_label, vpnLabel, packageName);
+    }
+
+    @VisibleForTesting
+    protected String getSanitizedVpnLabel(String vpnLabel, String packageName) {
+        final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel);
+        final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0,
+                sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH;
+        if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) {
+            return getSimplifiedLabel(sanitizedVpnLabel, packageName);
+        }
+
+        return sanitizedVpnLabel;
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -75,15 +120,16 @@
             finish();
             return;
         }
-        View view = View.inflate(this, R.layout.confirm, null);
-        ((TextView) view.findViewById(R.id.warning)).setText(
-                Html.fromHtml(getString(R.string.warning, getVpnLabel()),
-                        this, null /* tagHandler */));
+        mView = View.inflate(this, R.layout.confirm, null);
+        ((TextView) mView.findViewById(R.id.warning)).setText(
+                Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel(
+                        getVpnLabel().toString(), mPackage)),
+                        this /* imageGetter */, null /* tagHandler */));
         mAlertParams.mTitle = getText(R.string.prompt);
         mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
         mAlertParams.mPositiveButtonListener = this;
         mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
-        mAlertParams.mView = view;
+        mAlertParams.mView = mView;
         setupAlert();
 
         getWindow().setCloseOnTouchOutside(false);
@@ -92,6 +138,11 @@
         button.setFilterTouchesWhenObscured(true);
     }
 
+    @VisibleForTesting
+    public CharSequence getWarningText() {
+        return ((TextView) mView.findViewById(R.id.warning)).getText();
+    }
+
     private CharSequence getVpnLabel() {
         try {
             return VpnConfig.getVpnLabel(this, mPackage);
diff --git a/packages/VpnDialogs/tests/Android.bp b/packages/VpnDialogs/tests/Android.bp
new file mode 100644
index 0000000..68639bd
--- /dev/null
+++ b/packages/VpnDialogs/tests/Android.bp
@@ -0,0 +1,36 @@
+// Copyright 2022, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "VpnDialogsTests",
+    // Use platform certificate because the test will invoke a hidden API.
+    // (e.g. VpnManager#prepareVpn()).
+    certificate: "platform",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "mockito-target-minus-junit4",
+        "VpnDialogsLib",
+    ],
+    srcs: ["src/**/*.java"],
+}
diff --git a/packages/VpnDialogs/tests/AndroidManifest.xml b/packages/VpnDialogs/tests/AndroidManifest.xml
new file mode 100644
index 0000000..f26c1fe
--- /dev/null
+++ b/packages/VpnDialogs/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.vpndialogs.tests">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="com.android.vpndialogs.VpnDialogTest$InstrumentedConfirmDialog"/>
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.vpndialogs.tests"
+                     android:label="Vpn dialog tests">
+    </instrumentation>
+</manifest>
diff --git a/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
new file mode 100644
index 0000000..7cfa466
--- /dev/null
+++ b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.vpndialogs;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.VpnManager;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class VpnDialogTest {
+    private ActivityScenario<ConfirmDialog> mActivityScenario;
+
+    @SuppressWarnings("StaticMockMember")
+    @Mock
+    private static PackageManager sPm;
+
+    @SuppressWarnings("StaticMockMember")
+    @Mock
+    private static VpnManager sVm;
+
+    @Mock
+    private ApplicationInfo mAi;
+
+    private static final String VPN_APP_NAME = "VpnApp";
+    private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest";
+    private static final String VPN_LABEL_CONTAINS_HTML_TAG =
+            "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a>";
+    private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION =
+            "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a></b>"
+            + " Wants to connect the network. <br></br><br></br><br></br><br></br><br></br>"
+            + " <br></br><br></br><br></br><br></br><br></br><br></br><br></br><br></br> Deny it?";
+    private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label"
+            + " which violates the length restriction. The length restriction here are 150 code"
+            + " points. So the VPN label should be sanitized, and shows the package name to the"
+            + " user.";
+
+    public static class InstrumentedConfirmDialog extends ConfirmDialog {
+        @Override
+        public PackageManager getPackageManager() {
+            return sPm;
+        }
+
+        @Override
+        public @Nullable Object getSystemService(@ServiceName @NonNull String name) {
+            switch (name) {
+                case Context.VPN_MANAGEMENT_SERVICE:
+                    return sVm;
+                default:
+                    return super.getSystemService(name);
+            }
+        }
+
+        @Override
+        public String getCallingPackage() {
+            return VPN_APP_PACKAGE_NAME;
+        }
+    }
+
+    private void launchActivity() {
+        final Context context = getInstrumentation().getContext();
+        mActivityScenario = ActivityScenario.launch(
+                new Intent(context, InstrumentedConfirmDialog.class));
+    }
+
+    @Test
+    public void testGetSanitizedVpnLabel_withNormalCase() throws Exception {
+        // Test the normal case that the VPN label showed in the VpnDialog is the app name.
+        doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm);
+        launchActivity();
+        mActivityScenario.onActivity(activity -> {
+            assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME));
+        });
+    }
+
+    private void verifySanitizedVpnLabel(String originalLabel) {
+        doReturn(originalLabel).when(mAi).loadLabel(sPm);
+        launchActivity();
+        mActivityScenario.onActivity(activity -> {
+            // The VPN label was sanitized because violating length restriction or having a html
+            // tag, so the warning message will contain the package name.
+            assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage()));
+            // Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH
+            // and it shouldn't contain html tag.
+            final String sanitizedVpnLabel =
+                    activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME);
+            assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length())
+                    < ConfirmDialog.MAX_VPN_LABEL_LENGTH);
+            assertFalse(sanitizedVpnLabel.contains("<b>"));
+        });
+    }
+
+    @Test
+    public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception {
+        // Test the case that the VPN label was sanitized because there is a html tag.
+        verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG);
+    }
+
+    @Test
+    public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception {
+        // Test the case that the VPN label was sanitized because there is a html tag.
+        verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION);
+    }
+
+    @Test
+    public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception {
+        // Test the case that the VPN label was sanitized because hitting the length restriction.
+        verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt());
+        doReturn(null).when(sPm).queryIntentServices(any(), anyInt());
+        doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt());
+    }
+}
diff --git a/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml b/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml
index 9db960f..e7ec332 100644
--- a/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml
+++ b/packages/overlays/DisplayCutoutEmulationHoleOverlay/res/values-en-rCA/strings.xml
@@ -17,5 +17,5 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="display_cutout_emulation_overlay" msgid="7305489596221077240">"Punch hole cutout"</string>
+    <string name="display_cutout_emulation_overlay" msgid="7305489596221077240">"Punch Hole cutout"</string>
 </resources>
diff --git a/packages/overlays/NavigationBarMode3ButtonOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarMode3ButtonOverlay/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..8e466e0
--- /dev/null
+++ b/packages/overlays/NavigationBarMode3ButtonOverlay/res/values-sw600dp/config.xml
@@ -0,0 +1,26 @@
+<?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>
+    <!-- Controls whether seamless rotation should be allowed even though the navbar can move
+         (which normally prevents seamless rotation). Allow seamless rotation because the bar
+         is relatively small in large screen and its appearance is similar to gestural mode
+         even if it jumps to another side for display orientation change. -->
+    <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool>
+</resources>
+
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 3d24588..382aa87 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.graphics.Camera;
 import android.graphics.GraphicBuffer;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
@@ -124,26 +123,31 @@
 
     private static final String CAMERA_EXTENSION_VERSION_NAME =
             "androidx.camera.extensions.impl.ExtensionVersionImpl";
-    private static final String LATEST_VERSION = "1.3.0";
+    private static final String LATEST_VERSION = "1.4.0";
     // No support for the init sequence
     private static final String NON_INIT_VERSION_PREFIX = "1.0";
     // Support advanced API and latency queries
     private static final String ADVANCED_VERSION_PREFIX = "1.2";
     // Support for the capture request & result APIs
     private static final String RESULTS_VERSION_PREFIX = "1.3";
-    private static final String[] ADVANCED_VERSION_PREFIXES = {ADVANCED_VERSION_PREFIX,
-            RESULTS_VERSION_PREFIX};
-    private static final String[] SUPPORTED_VERSION_PREFIXES = {RESULTS_VERSION_PREFIX,
-            ADVANCED_VERSION_PREFIX, "1.1", NON_INIT_VERSION_PREFIX};
+    // Support for various latency improvements
+    private static final String LATENCY_VERSION_PREFIX = "1.4";
+    private static final String[] ADVANCED_VERSION_PREFIXES = {LATENCY_VERSION_PREFIX,
+            ADVANCED_VERSION_PREFIX, RESULTS_VERSION_PREFIX };
+    private static final String[] SUPPORTED_VERSION_PREFIXES = {LATENCY_VERSION_PREFIX,
+            RESULTS_VERSION_PREFIX, ADVANCED_VERSION_PREFIX, "1.1", NON_INIT_VERSION_PREFIX};
     private static final boolean EXTENSIONS_PRESENT = checkForExtensions();
     private static final String EXTENSIONS_VERSION = EXTENSIONS_PRESENT ?
             (new ExtensionVersionImpl()).checkApiVersion(LATEST_VERSION) : null;
     private static final boolean LATENCY_API_SUPPORTED = checkForLatencyAPI();
+    private static final boolean LATENCY_IMPROVEMENTS_SUPPORTED = EXTENSIONS_PRESENT &&
+            (EXTENSIONS_VERSION.startsWith(LATENCY_VERSION_PREFIX));
     private static final boolean ADVANCED_API_SUPPORTED = checkForAdvancedAPI();
     private static final boolean INIT_API_SUPPORTED = EXTENSIONS_PRESENT &&
             (!EXTENSIONS_VERSION.startsWith(NON_INIT_VERSION_PREFIX));
     private static final boolean RESULT_API_SUPPORTED = EXTENSIONS_PRESENT &&
-            (EXTENSIONS_VERSION.startsWith(RESULTS_VERSION_PREFIX));
+            (EXTENSIONS_VERSION.startsWith(RESULTS_VERSION_PREFIX) ||
+            EXTENSIONS_VERSION.startsWith(LATENCY_VERSION_PREFIX));
 
     private HashMap<String, CameraCharacteristics> mCharacteristicsHashMap = new HashMap<>();
     private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>();
@@ -825,6 +829,15 @@
 
             return null;
         }
+
+        @Override
+        public boolean isCaptureProcessProgressAvailable() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                return mAdvancedExtender.isCaptureProcessProgressAvailable();
+            }
+
+            return false;
+        }
     }
 
     private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback {
@@ -918,6 +931,15 @@
                 Log.e(TAG, "Failed to notify capture complete due to remote exception!");
             }
         }
+
+        @Override
+        public void onCaptureProcessProgressed(int progress) {
+            try {
+                mCaptureCallback.onCaptureProcessProgressed(progress);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote client doesn't respond to capture progress callbacks!");
+            }
+        }
     }
 
     private class RequestCallbackStub extends IRequestCallback.Stub {
@@ -1169,6 +1191,10 @@
                 ret.outputConfigs.add(entry);
             }
             ret.sessionTemplateId = sessionConfig.getSessionTemplateId();
+            ret.sessionType = -1;
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                ret.sessionType = sessionConfig.getSessionType();
+            }
             ret.sessionParameter = initializeParcelableMetadata(
                     sessionConfig.getSessionParameters(), cameraId);
             mCameraId = cameraId;
@@ -1312,6 +1338,15 @@
         }
 
         @Override
+        public int getSessionType() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                return mPreviewExtender.onSessionType();
+            }
+
+            return -1;
+        }
+
+        @Override
         public int getProcessorType() {
             ProcessorType processorType = mPreviewExtender.getProcessorType();
             if (processorType == ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
@@ -1397,6 +1432,15 @@
         }
 
         @Override
+        public boolean isCaptureProcessProgressAvailable() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                return mImageExtender.isCaptureProcessProgressAvailable();
+            }
+
+            return false;
+        }
+
+        @Override
         public CaptureStageImpl onEnableSession() {
             return initializeParcelable(mImageExtender.onEnableSession(), mCameraId);
         }
@@ -1407,6 +1451,15 @@
         }
 
         @Override
+        public int getSessionType() {
+            if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+                return mImageExtender.onSessionType();
+            }
+
+            return -1;
+        }
+
+        @Override
         public void init(String cameraId, CameraMetadataNative chars) {
             CameraCharacteristics c = new CameraCharacteristics(chars);
             mCameraManager.registerDeviceStateListener(c);
@@ -1544,6 +1597,15 @@
         }
 
         @Override
+        public void onCaptureProcessProgressed(int progress) {
+            try {
+                mProcessResult.onCaptureProcessProgressed(progress);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote client doesn't respond to capture progress callbacks!");
+            }
+        }
+
+        @Override
         public void onCaptureCompleted(long shutterTimestamp,
                 List<Pair<CaptureResult.Key, Object>> result) {
             if (result == null) {
diff --git a/proto/src/camera.proto b/proto/src/camera.proto
index 38d74e4..205e806 100644
--- a/proto/src/camera.proto
+++ b/proto/src/camera.proto
@@ -67,4 +67,6 @@
     optional int64 dynamic_range_profile = 14;
     // The stream use case
     optional int64 stream_use_case = 15;
+    // The color space of the stream
+    optional int32 color_space = 16;
 }
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index a94bfe2..12e7226 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -61,7 +61,7 @@
 
     // Notify the user that they should select an input method
     // Package: android
-    NOTE_SELECT_INPUT_METHOD = 8;
+    NOTE_SELECT_INPUT_METHOD = 8 [deprecated = true];
 
     // Notify the user about limited functionality before decryption
     // Package: android
diff --git a/proto/src/task_snapshot.proto b/proto/src/task_snapshot.proto
deleted file mode 100644
index 1cbc17e..0000000
--- a/proto/src/task_snapshot.proto
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- syntax = "proto3";
-
- package com.android.server.wm;
-
- option java_package = "com.android.server.wm";
- option java_outer_classname = "WindowManagerProtos";
-
- message TaskSnapshotProto {
-     int32 orientation = 1;
-     int32 inset_left = 2;
-     int32 inset_top = 3;
-     int32 inset_right = 4;
-     int32 inset_bottom = 5;
-     bool is_real_snapshot = 6;
-     int32 windowing_mode = 7;
-     int32 system_ui_visibility = 8 [deprecated=true];
-     bool is_translucent = 9;
-     string top_activity_component = 10;
-     // deprecated because original width and height are stored now instead of the scale.
-     float legacy_scale = 11 [deprecated=true];
-     int64 id = 12;
-     int32 rotation = 13;
-     // The task width when the snapshot was taken
-     int32 task_width = 14;
-     // The task height when the snapshot was taken
-     int32 task_height = 15;
-     int32 appearance = 16;
-     int32 letterbox_inset_left = 17;
-     int32 letterbox_inset_top = 18;
-     int32 letterbox_inset_right = 19;
-     int32 letterbox_inset_bottom = 20;
- }
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
new file mode 100644
index 0000000..f26404c6
--- /dev/null
+++ b/proto/src/windowmanager.proto
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package com.android.server.wm;
+
+option java_package = "com.android.server.wm";
+option java_outer_classname = "WindowManagerProtos";
+
+message TaskSnapshotProto {
+  int32 orientation = 1;
+  int32 inset_left = 2;
+  int32 inset_top = 3;
+  int32 inset_right = 4;
+  int32 inset_bottom = 5;
+  bool is_real_snapshot = 6;
+  int32 windowing_mode = 7;
+  int32 system_ui_visibility = 8 [deprecated=true];
+  bool is_translucent = 9;
+  string top_activity_component = 10;
+  // deprecated because original width and height are stored now instead of the scale.
+  float legacy_scale = 11 [deprecated=true];
+  int64 id = 12;
+  int32 rotation = 13;
+  // The task width when the snapshot was taken
+  int32 task_width = 14;
+  // The task height when the snapshot was taken
+  int32 task_height = 15;
+  int32 appearance = 16;
+  int32 letterbox_inset_left = 17;
+  int32 letterbox_inset_top = 18;
+  int32 letterbox_inset_right = 19;
+  int32 letterbox_inset_bottom = 20;
+}
+
+// Persistent letterboxing configurations
+message LetterboxProto {
+
+  // Possible values for the letterbox horizontal reachability
+  enum LetterboxHorizontalReachability {
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT = 0;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT = 2;
+  }
+
+  // Possible values for the letterbox vertical reachability
+  enum LetterboxVerticalReachability {
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP = 0;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2;
+  }
+
+  // Represents the current horizontal position for the letterboxed activity
+  LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
+  // Represents the current vertical position for the letterboxed activity
+  LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+}
\ No newline at end of file
diff --git a/services/Android.bp b/services/Android.bp
index 76a1484..f6570e9 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -60,9 +60,17 @@
                 ignore_warnings: false,
                 proguard_flags_files: ["proguard.flags"],
             },
-            // Note: Optimizations are disabled by default if unspecified in
-            // the java_library rule.
-            conditions_default: {},
+            conditions_default: {
+                optimize: {
+                    enabled: true,
+                    optimize: false,
+                    shrink: true,
+                    ignore_warnings: false,
+                    // Note that this proguard config is very conservative, only shrinking the
+                    // permission subpackage to prune unused jarjar'ed Kotlin dependencies.
+                    proguard_flags_files: ["proguard_permission.flags"],
+                },
+            },
         },
     },
 }
@@ -97,6 +105,7 @@
         ":services.midi-sources",
         ":services.musicsearch-sources",
         ":services.net-sources",
+        ":services.permission-sources",
         ":services.print-sources",
         ":services.profcollect-sources",
         ":services.restrictions-sources",
@@ -131,6 +140,7 @@
         app_image: true,
         profile: "art-profile",
     },
+    exclude_kotlinc_generated_files: true,
 
     srcs: [":services-main-sources"],
 
@@ -152,6 +162,7 @@
         "services.musicsearch",
         "services.net",
         "services.people",
+        "services.permission",
         "services.print",
         "services.profcollect",
         "services.restrictions",
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 1df382f..c77b597 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -16,6 +16,7 @@
 
 package com.android.server.accessibility;
 
+import static android.accessibilityservice.AccessibilityService.ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS;
 import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE;
 import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER;
 import static android.accessibilityservice.AccessibilityService.KEY_ACCESSIBILITY_SCREENSHOT_STATUS;
@@ -83,6 +84,7 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
 import android.view.inputmethod.EditorInfo;
+import android.window.ScreenCapture;
 import android.window.ScreenCapture.ScreenshotHardwareBuffer;
 
 import com.android.internal.annotations.GuardedBy;
@@ -176,6 +178,7 @@
 
     private boolean mSendMotionEvents;
 
+    private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0);
     boolean mRequestFilterKeyEvents;
 
     boolean mRetrieveInteractiveWindows;
@@ -210,6 +213,11 @@
 
     /** The timestamp of requesting to take screenshot in milliseconds */
     private long mRequestTakeScreenshotTimestampMs;
+    /**
+     * The timestamps of requesting to take a window screenshot in milliseconds,
+     * mapping from accessibility window id -> timestamp.
+     */
+    private SparseArray<Long> mRequestTakeScreenshotOfWindowTimestampMs = new SparseArray<>();
 
     public interface SystemSupport {
         /**
@@ -1251,6 +1259,51 @@
     }
 
     @Override
+    public void takeScreenshotOfWindow(int accessibilityWindowId, int interactionId,
+            ScreenCapture.ScreenCaptureListener listener,
+            IAccessibilityInteractionConnectionCallback callback) throws RemoteException {
+        final long currentTimestamp = SystemClock.uptimeMillis();
+        if ((currentTimestamp
+                - mRequestTakeScreenshotOfWindowTimestampMs.get(accessibilityWindowId, 0L))
+                <= ACCESSIBILITY_TAKE_SCREENSHOT_REQUEST_INTERVAL_TIMES_MS) {
+            callback.sendTakeScreenshotOfWindowError(
+                    AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERVAL_TIME_SHORT, interactionId);
+            return;
+        }
+        mRequestTakeScreenshotOfWindowTimestampMs.put(accessibilityWindowId, currentTimestamp);
+
+        synchronized (mLock) {
+            if (!hasRightsToCurrentUserLocked()) {
+                callback.sendTakeScreenshotOfWindowError(
+                        AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR, interactionId);
+                return;
+            }
+            if (!mSecurityPolicy.canTakeScreenshotLocked(this)) {
+                callback.sendTakeScreenshotOfWindowError(
+                        AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS,
+                        interactionId);
+                return;
+            }
+        }
+        if (!mSecurityPolicy.checkAccessibilityAccess(this)) {
+            callback.sendTakeScreenshotOfWindowError(
+                    AccessibilityService.ERROR_TAKE_SCREENSHOT_NO_ACCESSIBILITY_ACCESS,
+                    interactionId);
+            return;
+        }
+
+        RemoteAccessibilityConnection connection = mA11yWindowManager.getConnectionLocked(
+                mSystemSupport.getCurrentUserIdLocked(),
+                resolveAccessibilityWindowIdLocked(accessibilityWindowId));
+        if (connection == null) {
+            callback.sendTakeScreenshotOfWindowError(
+                    AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_WINDOW, interactionId);
+            return;
+        }
+        connection.getRemote().takeScreenshotOfWindow(interactionId, listener, callback);
+    }
+
+    @Override
     public void takeScreenshot(int displayId, RemoteCallback callback) {
         if (svcConnTracingEnabled()) {
             logTraceSvcConn("takeScreenshot", "displayId=" + displayId + ";callback=" + callback);
@@ -2369,9 +2422,17 @@
     }
 
     public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+        mServiceDetectsGestures.put(displayId, mode);
         mSystemSupport.setServiceDetectsGesturesEnabled(displayId, mode);
     }
 
+    public boolean isServiceDetectsGesturesEnabled(int displayId) {
+        if (mServiceDetectsGestures.contains(displayId)) {
+            return mServiceDetectsGestures.get(displayId);
+        }
+        return false;
+    }
+
     public void requestTouchExploration(int displayId) {
         mSystemSupport.requestTouchExploration(displayId);
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 75724bf..d80117d 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -176,6 +176,8 @@
 
     private int mEnabledFeatures;
 
+    // Display-specific features
+    private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>();
     private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0);
 
     private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0);
@@ -458,7 +460,9 @@
 
         final Context displayContext = mContext.createDisplayContext(display);
         final int displayId = display.getDisplayId();
-
+        if (!mServiceDetectsGestures.contains(displayId)) {
+            mServiceDetectsGestures.put(displayId, false);
+        }
         if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
             if (mAutoclickController == null) {
                 mAutoclickController = new AutoclickController(
@@ -481,6 +485,7 @@
             if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) {
                 explorer.setSendMotionEventsEnabled(true);
             }
+            explorer.setServiceDetectsGestures(mServiceDetectsGestures.get(displayId));
             addFirstEventHandler(displayId, explorer);
             mTouchExplorer.put(displayId, explorer);
         }
@@ -897,6 +902,11 @@
         if (mTouchExplorer.contains(displayId)) {
             mTouchExplorer.get(displayId).setServiceDetectsGestures(mode);
         }
+        mServiceDetectsGestures.put(displayId, mode);
+    }
+
+    public void resetServiceDetectsGestures() {
+        mServiceDetectsGestures.clear();
     }
 
     public void requestTouchExploration(int displayId) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index bfa1b20..e3ae03c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1695,31 +1695,34 @@
     }
 
     private boolean scheduleNotifyMotionEvent(MotionEvent event) {
+        boolean result = false;
+        int displayId = event.getDisplayId();
         synchronized (mLock) {
             AccessibilityUserState state = getCurrentUserStateLocked();
             for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
                 AccessibilityServiceConnection service = state.mBoundServices.get(i);
-                if (service.mRequestTouchExplorationMode) {
+                if (service.isServiceDetectsGesturesEnabled(displayId)) {
                     service.notifyMotionEvent(event);
-                    return true;
+                    result = true;
                 }
             }
         }
-        return false;
+        return result;
     }
 
     private boolean scheduleNotifyTouchState(int displayId, int touchState) {
+        boolean result = false;
         synchronized (mLock) {
             AccessibilityUserState state = getCurrentUserStateLocked();
             for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
                 AccessibilityServiceConnection service = state.mBoundServices.get(i);
-                if (service.mRequestTouchExplorationMode) {
+                if (service.isServiceDetectsGesturesEnabled(displayId)) {
                     service.notifyTouchState(displayId, touchState);
-                    return true;
+                    result = true;
                 }
             }
         }
-        return false;
+        return result;
     }
 
     private void notifyClearAccessibilityCacheLocked() {
@@ -2292,8 +2295,9 @@
                 if (!mHasInputFilter) {
                     mHasInputFilter = true;
                     if (mInputFilter == null) {
-                        mInputFilter = new AccessibilityInputFilter(mContext,
-                                AccessibilityManagerService.this);
+                        mInputFilter =
+                                new AccessibilityInputFilter(
+                                        mContext, AccessibilityManagerService.this);
                     }
                     inputFilter = mInputFilter;
                     setInputFilter = true;
@@ -2303,6 +2307,17 @@
                 if (mHasInputFilter) {
                     mHasInputFilter = false;
                     mInputFilter.setUserAndEnabledFeatures(userState.mUserId, 0);
+                    mInputFilter.resetServiceDetectsGestures();
+                    if (userState.isTouchExplorationEnabledLocked()) {
+                        //  Service gesture detection is turned on and off on a per-display
+                        // basis.
+                        final ArrayList<Display> displays = getValidDisplayList();
+                        for (Display display : displays) {
+                            int displayId = display.getDisplayId();
+                            boolean mode = userState.isServiceDetectsGesturesEnabled(displayId);
+                            mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode);
+                        }
+                    }
                     inputFilter = null;
                     setInputFilter = true;
                 }
@@ -2618,6 +2633,18 @@
                 Binder.restoreCallingIdentity(identity);
             }
         }
+        // Service gesture detection is turned on and off on a per-display
+        // basis.
+        userState.resetServiceDetectsGestures();
+        final ArrayList<Display> displays = getValidDisplayList();
+        for (AccessibilityServiceConnection service: userState.mBoundServices) {
+            for (Display display : displays) {
+                int displayId = display.getDisplayId();
+                if (service.isServiceDetectsGesturesEnabled(displayId)) {
+                    userState.setServiceDetectsGesturesEnabled(displayId, true);
+                }
+            }
+        }
         userState.setServiceHandlesDoubleTapLocked(serviceHandlesDoubleTapEnabled);
         userState.setMultiFingerGesturesLocked(requestMultiFingerGestures);
         userState.setTwoFingerPassthroughLocked(requestTwoFingerPassthrough);
@@ -3624,6 +3651,10 @@
             throw new IllegalArgumentException("The display " + displayId + " does not exist or is"
                     + " not tracked by accessibility.");
         }
+        if (mProxyManager.isProxyed(displayId)) {
+            throw new IllegalArgumentException("The display " + displayId + " is already being"
+                    + "proxy-ed");
+        }
 
         mProxyManager.registerProxy(client, displayId);
         return true;
@@ -4144,7 +4175,7 @@
                     if (readEnabledAccessibilityServicesLocked(userState)) {
                         mSecurityPolicy.onEnabledServicesChangedLocked(userState.mUserId,
                                 userState.mEnabledServices);
-                        userState.updateCrashedServicesIfNeededLocked();
+                        userState.removeDisabledServicesFromTemporaryStatesLocked();
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mTouchExplorationGrantedAccessibilityServicesUri.equals(uri)) {
@@ -4342,6 +4373,7 @@
 
     private void setServiceDetectsGesturesInternal(int displayId, boolean mode) {
         synchronized (mLock) {
+            getCurrentUserStateLocked().setServiceDetectsGesturesEnabled(displayId, mode);
             if (mHasInputFilter && mInputFilter != null) {
                 mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode);
             }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 55dc196..0db169f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -44,6 +44,7 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IAccessibilityManagerClient;
@@ -118,6 +119,7 @@
     private boolean mRequestMultiFingerGestures;
     private boolean mRequestTwoFingerPassthrough;
     private boolean mSendMotionEventsEnabled;
+    private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0);
     private int mUserInteractiveUiTimeout;
     private int mUserNonInteractiveUiTimeout;
     private int mNonInteractiveUiTimeout = 0;
@@ -381,18 +383,22 @@
     }
 
     /**
-     * Remove service from crashed service list if users disable it.
+     * Remove the service from the crashed and binding service lists if the user disabled it.
      */
-    void updateCrashedServicesIfNeededLocked() {
+    void removeDisabledServicesFromTemporaryStatesLocked() {
         for (int i = 0, count = mInstalledServices.size(); i < count; i++) {
             final AccessibilityServiceInfo installedService = mInstalledServices.get(i);
             final ComponentName componentName = ComponentName.unflattenFromString(
                     installedService.getId());
 
-            if (mCrashedServices.contains(componentName)
-                    && !mEnabledServices.contains(componentName)) {
-                // Remove it from mCrashedServices since users toggle the switch bar to retry.
+            if (!mEnabledServices.contains(componentName)) {
+                // Remove from mCrashedServices, since users may toggle the on/off switch to retry.
                 mCrashedServices.remove(componentName);
+                // Remove from mBindingServices, since services can get stuck in the binding state
+                // if binding starts but never finishes. If the service later attempts to finish
+                // binding but it is not in the enabled list then it will exit before initializing;
+                // see AccessibilityServiceConnection#initializeService().
+                mBindingServices.remove(componentName);
             }
         }
     }
@@ -987,4 +993,19 @@
         mFocusStrokeWidth = strokeWidth;
         mFocusColor = color;
     }
+
+    public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+        mServiceDetectsGestures.put(displayId, mode);
+    }
+
+    public void resetServiceDetectsGestures() {
+        mServiceDetectsGestures.clear();
+    }
+
+    public boolean isServiceDetectsGesturesEnabled(int displayId) {
+        if (mServiceDetectsGestures.contains(displayId)) {
+            return mServiceDetectsGestures.get(displayId);
+        }
+        return false;
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
index 6958b66..c08b6ab 100644
--- a/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
+++ b/services/accessibility/java/com/android/server/accessibility/ActionReplacingCallback.java
@@ -167,6 +167,12 @@
         mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
     }
 
+    @Override
+    public void sendTakeScreenshotOfWindowError(int errorCode, int interactionId)
+            throws RemoteException {
+        mServiceCallback.sendTakeScreenshotOfWindowError(errorCode, interactionId);
+    }
+
     private void replaceInfoActionsAndCallService() {
         final AccessibilityNodeInfo nodeToReturn;
         boolean doCallback = false;
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 934b665..247f320 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -32,6 +32,7 @@
 import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityDisplayProxy;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.Nullable;
@@ -44,7 +45,7 @@
 import java.util.Set;
 
 /**
- * Represents the system connection to an {@link android.view.accessibility.AccessibilityProxy}.
+ * Represents the system connection to an {@link AccessibilityDisplayProxy}.
  *
  * <p>Most methods are no-ops since this connection does not need to capture input or listen to
  * hardware-related changes.
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index fb0b8f3..a2ce610 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -16,6 +16,8 @@
 package com.android.server.accessibility;
 import android.accessibilityservice.IAccessibilityServiceClient;
 
+import java.util.HashSet;
+
 /**
  * Manages proxy connections.
  *
@@ -26,6 +28,7 @@
  */
 public class ProxyManager {
     private final Object mLock;
+    private final HashSet<Integer> mDisplayIds = new HashSet<>();
 
     ProxyManager(Object lock) {
         mLock = lock;
@@ -35,12 +38,21 @@
      * TODO: Create the proxy service connection.
      */
     public void registerProxy(IAccessibilityServiceClient client, int displayId) {
+        mDisplayIds.add(displayId);
     }
 
     /**
      * TODO: Unregister the proxy service connection based on display id.
      */
     public boolean unregisterProxy(int displayId) {
+        mDisplayIds.remove(displayId);
         return true;
     }
+
+    /**
+     * Checks if a display id is being proxy-ed.
+     */
+    public boolean isProxyed(int displayId) {
+        return mDisplayIds.contains(displayId);
+    }
 }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index a185b58..3cfae60 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -104,8 +104,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
@@ -124,6 +122,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.IRemoteViewsFactory;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.WidgetBackupProvider;
@@ -872,6 +872,33 @@
     }
 
     @Override
+    public void setAppWidgetHidden(String callingPackage, int hostId) {
+        final int userId = UserHandle.getCallingUserId();
+
+        if (DEBUG) {
+            Slog.i(TAG, "setAppWidgetHidden() " + userId);
+        }
+
+        mSecurityPolicy.enforceCallFromPackage(callingPackage);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false);
+
+            HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage);
+            Host host = lookupHostLocked(id);
+
+            if (host != null) {
+                try {
+                    mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
+                } catch (NullPointerException e) {
+                    Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e);
+                    throw e;
+                }
+            }
+        }
+    }
+
+    @Override
     public void deleteAppWidgetId(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
 
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
index 92435d0..7d8bb51 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
@@ -23,8 +23,9 @@
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -113,7 +114,7 @@
         info.minWidth = parser.getAttributeInt(null, ATTR_MIN_WIDTH, 0);
         info.minHeight = parser.getAttributeInt(null, ATTR_MIN_HEIGHT, 0);
         info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_WIDTH, 0);
-        info.minResizeWidth = parser.getAttributeInt(null, ATTR_MIN_RESIZE_HEIGHT, 0);
+        info.minResizeHeight = parser.getAttributeInt(null, ATTR_MIN_RESIZE_HEIGHT, 0);
         info.maxResizeWidth = parser.getAttributeInt(null, ATTR_MAX_RESIZE_WIDTH, 0);
         info.maxResizeHeight = parser.getAttributeInt(null, ATTR_MAX_RESIZE_HEIGHT, 0);
         info.targetCellWidth = parser.getAttributeInt(null, ATTR_TARGET_CELL_WIDTH, 0);
diff --git a/services/art-profile b/services/art-profile
index 3e05078..b6398c0 100644
--- a/services/art-profile
+++ b/services/art-profile
@@ -41551,7 +41551,7 @@
 HPLcom/android/server/statusbar/StatusBarManagerService$1;->setNavigationBarLumaSamplingEnabled(IZ)V
 HSPLcom/android/server/statusbar/StatusBarManagerService$1;->setNotificationDelegate(Lcom/android/server/notification/NotificationDelegate;)V
 HPLcom/android/server/statusbar/StatusBarManagerService$1;->setTopAppHidesStatusBar(Z)V+]Lcom/android/internal/statusbar/IStatusBar;Lcom/android/internal/statusbar/IStatusBar$Stub$Proxy;
-PLcom/android/server/statusbar/StatusBarManagerService$1;->setUdfpsHbmListener(Landroid/hardware/fingerprint/IUdfpsHbmListener;)V
+PLcom/android/server/statusbar/StatusBarManagerService$1;->setUdfpsRefreshRateCallback(Landroid/hardware/fingerprint/IUdfpsRefreshRate;)V
 HPLcom/android/server/statusbar/StatusBarManagerService$1;->setWindowState(III)V
 PLcom/android/server/statusbar/StatusBarManagerService$1;->showChargingAnimation(I)V
 PLcom/android/server/statusbar/StatusBarManagerService$1;->showRecentApps(Z)V
@@ -41677,6 +41677,11 @@
 PLcom/android/server/statusbar/StatusBarManagerService;->setIcon(Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V
 HSPLcom/android/server/statusbar/StatusBarManagerService;->setIconVisibility(Ljava/lang/String;Z)V
 HPLcom/android/server/statusbar/StatusBarManagerService;->setImeWindowStatus(ILandroid/os/IBinder;IIZ)V
+PLcom/android/server/statusbar/StatusBarManagerService;->setUdfpsRefreshRateCallback(Landroid/hardware/fingerprint/IUdfpsRefreshRate;)V
+PLcom/android/server/statusbar/StatusBarManagerService;->setUdfpsRefreshRateCallback(Landroid/hardware/fingerprint/IUdfpsRefreshRate;)V
+HSPLcom/android/server/statusbar/StatusBarManagerService;->setIconVisibility(Ljava/lang/String;Z)V+]Landroid/util/ArrayMap;Landroid/util/ArrayMap;]Lcom/android/server/statusbar/StatusBarManagerService;Lcom/android/server/statusbar/StatusBarManagerService;
+HPLcom/android/server/statusbar/StatusBarManagerService;->setImeWindowStatus(ILandroid/os/IBinder;IIZ)V+]Landroid/os/Handler;Landroid/os/Handler;]Lcom/android/server/statusbar/StatusBarManagerService;Lcom/android/server/statusbar/StatusBarManagerService;
+PLcom/android/server/statusbar/StatusBarManagerService;->setNavBarMode(I)V
 PLcom/android/server/statusbar/StatusBarManagerService;->setUdfpsHbmListener(Landroid/hardware/fingerprint/IUdfpsHbmListener;)V
 PLcom/android/server/statusbar/StatusBarManagerService;->showAuthenticationDialog(Landroid/hardware/biometrics/PromptInfo;Landroid/hardware/biometrics/IBiometricSysuiReceiver;[IZZIJLjava/lang/String;JI)V
 PLcom/android/server/statusbar/StatusBarManagerService;->suppressAmbientDisplay(Z)V
diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
index 8525e36..592045c 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java
@@ -245,6 +245,7 @@
         });
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     private void maybeRequestShowInlineSuggestions(int sessionId,
             @Nullable InlineSuggestionsRequest request,
             @Nullable List<Dataset> inlineSuggestionsData, @Nullable Bundle clientState,
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 47ce592..64b7688 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -24,6 +24,7 @@
 import static android.service.autofill.FillEventHistory.Event.UI_TYPE_UNKNOWN;
 import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
 import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE;
+import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE;
 import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG;
 import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED;
 import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
@@ -416,6 +417,14 @@
     @GuardedBy("mLock")
     private boolean mPreviouslyFillDialogPotentiallyStarted;
 
+    /**
+     * Keeps the fill dialog trigger ids of the last response. This invalidates
+     * the trigger ids of the previous response.
+     */
+    @Nullable
+    @GuardedBy("mLock")
+    private AutofillId[] mLastFillDialogTriggerIds;
+
     void onSwitchInputMethodLocked() {
         // One caveat is that for the case where the focus is on a field for which regular autofill
         // returns null, and augmented autofill is triggered,  and then the user switches the input
@@ -1222,6 +1231,8 @@
                 return;
             }
 
+            mLastFillDialogTriggerIds = response.getFillDialogTriggerIds();
+
             final int flags = response.getFlags();
             if ((flags & FillResponse.FLAG_DELAY_FILL) != 0) {
                 Slog.v(TAG, "Service requested to wait for delayed fill response.");
@@ -1310,6 +1321,7 @@
 
         // fallback to the default platform password manager
         mSessionFlags.mClientSuggestionsEnabled = false;
+        mLastFillDialogTriggerIds = null;
 
         final InlineSuggestionsRequest inlineRequest =
                 (mLastInlineSuggestionsRequest != null
@@ -1348,6 +1360,7 @@
                         + (timedOut ? "timeout" : "failure"));
             }
             mService.resetLastResponse();
+            mLastFillDialogTriggerIds = null;
             final LogMaker requestLog = mRequestLogs.get(requestId);
             if (requestLog == null) {
                 Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId);
@@ -3049,6 +3062,11 @@
             }
         }
 
+        if ((flags & FLAG_RESET_FILL_DIALOG_STATE) != 0) {
+            if (sDebug) Log.d(TAG, "force to reset fill dialog state");
+            mSessionFlags.mFillDialogDisabled = false;
+        }
+
         switch(action) {
             case ACTION_START_SESSION:
                 // View is triggering autofill.
@@ -3488,10 +3506,8 @@
     }
 
     private boolean isFillDialogUiEnabled() {
-        // TODO read from Settings or somewhere
-        final boolean isSettingsEnabledFillDialog = true;
         synchronized (mLock) {
-            return isSettingsEnabledFillDialog && !mSessionFlags.mFillDialogDisabled;
+            return !mSessionFlags.mFillDialogDisabled;
         }
     }
 
@@ -3517,14 +3533,25 @@
             AutofillId filledId, String filterText, int flags) {
         if (!isFillDialogUiEnabled()) {
             // Unsupported fill dialog UI
+            if (sDebug) Log.w(TAG, "requestShowFillDialog: fill dialog is disabled");
             return false;
         }
 
         if ((flags & FillRequest.FLAG_IME_SHOWING) != 0) {
             // IME is showing, fallback to normal suggestions UI
+            if (sDebug) Log.w(TAG, "requestShowFillDialog: IME is showing");
             return false;
         }
 
+        synchronized (mLock) {
+            if (mLastFillDialogTriggerIds == null
+                    || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
+                // Last fill dialog triggered ids are changed.
+                if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+                return false;
+            }
+        }
+
         final Drawable serviceIcon = getServiceIcon();
 
         getUiForShowing().showFillDialog(filledId, response, filterText,
@@ -4394,6 +4421,13 @@
         if (mSessionFlags.mAugmentedAutofillOnly) {
             pw.print(prefix); pw.println("For Augmented Autofill Only");
         }
+        if (mSessionFlags.mFillDialogDisabled) {
+            pw.print(prefix); pw.println("Fill Dialog disabled");
+        }
+        if (mLastFillDialogTriggerIds != null) {
+            pw.print(prefix); pw.println("Last Fill Dialog trigger ids: ");
+            pw.println(mSelectedDatasetIds);
+        }
         if (mAugmentedAutofillDestroyer != null) {
             pw.print(prefix); pw.println("has mAugmentedAutofillDestroyer");
         }
diff --git a/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java b/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java
new file mode 100644
index 0000000..042bcbd
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/BackupAndRestoreFeatureFlags.java
@@ -0,0 +1,55 @@
+/*
+ * 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.backup;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.provider.DeviceConfig;
+
+/**
+ * Retrieves values of feature flags.
+ *
+ * <p>These flags are intended to be configured server-side and their values should be set in {@link
+ * DeviceConfig} by a service that periodically syncs with the server.
+ *
+ * <p>This class must ensure that the namespace, flag name, and default value passed into {@link
+ * DeviceConfig} matches what's declared on the server. The namespace is shared for all backup and
+ * restore flags.
+ */
+public class BackupAndRestoreFeatureFlags {
+    private static final String NAMESPACE = "backup_and_restore";
+
+    private BackupAndRestoreFeatureFlags() {}
+
+    /** Retrieves the value of the flag "backup_transport_future_timeout_millis". */
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
+    public static long getBackupTransportFutureTimeoutMillis() {
+        return DeviceConfig.getLong(
+                NAMESPACE,
+                /* name= */ "backup_transport_future_timeout_millis",
+                /* defaultValue= */ 600000); // 10 minutes
+    }
+
+    /** Retrieves the value of the flag "backup_transport_callback_timeout_millis". */
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
+    public static long getBackupTransportCallbackTimeoutMillis() {
+        return DeviceConfig.getLong(
+                NAMESPACE,
+                /* name= */ "backup_transport_callback_timeout_millis",
+                /* defaultValue= */ 300000); // 5 minutes
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/OperationStorage.java b/services/backup/java/com/android/server/backup/OperationStorage.java
index 466f647..8f73436 100644
--- a/services/backup/java/com/android/server/backup/OperationStorage.java
+++ b/services/backup/java/com/android/server/backup/OperationStorage.java
@@ -153,4 +153,4 @@
      * @return a set of operation tokens for operations in that state.
      */
     Set<Integer> operationTokensForOpState(@OpState int state);
-};
+}
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index eac98f2..f0492a8 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -376,6 +376,16 @@
                 return;
             }
 
+            // In some cases there may not be a monitor passed in when creating this task. So, if we
+            // don't have one already we ask the transport for a monitor.
+            if (mMonitor == null) {
+                try {
+                    mMonitor = transport.getBackupManagerMonitor();
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Failed to retrieve monitor from transport");
+                }
+            }
+
             // Set up to send data to the transport
             final int N = mPackages.size();
             final byte[] buffer = new byte[8192];
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 03796ea..95cc289 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -21,6 +21,7 @@
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.app.backup.BackupManager.OperationType;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreSet;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -203,6 +204,14 @@
                     }
                 }
 
+                // Ask the transport for a monitor that will be used to relay log events back to it.
+                IBackupManagerMonitor monitor = null;
+                try {
+                    monitor = transport.getBackupManagerMonitor();
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Failed to retrieve monitor from transport");
+                }
+
                 // At this point, we have started a new journal file, and the old
                 // file identity is being passed to the backup processing task.
                 // When it completes successfully, that old journal file will be
@@ -225,7 +234,7 @@
                                 queue,
                                 oldJournal,
                                 /* observer */ null,
-                                /* monitor */ null,
+                                monitor,
                                 listener,
                                 Collections.emptyList(),
                                 /* userInitiated */ false,
diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
index 6908c60..a94167e 100644
--- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
+++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java
@@ -353,4 +353,4 @@
             op.callback.handleCancel(cancelAll);
         }
     }
-};
+}
diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
index 237a3fa..21005bb 100644
--- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -18,16 +18,19 @@
 
 import android.annotation.Nullable;
 import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.util.Slog;
 
 import com.android.internal.backup.IBackupTransport;
 import com.android.internal.infra.AndroidFuture;
+import com.android.server.backup.BackupAndRestoreFeatureFlags;
 
 import java.util.ArrayDeque;
 import java.util.HashSet;
@@ -363,6 +366,15 @@
     }
 
     /**
+     * See {@link IBackupTransport#getBackupManagerMonitor()}
+     */
+    public IBackupManagerMonitor getBackupManagerMonitor() throws RemoteException {
+        AndroidFuture<IBackupManagerMonitor> resultFuture = mTransportFutures.newFuture();
+        mTransportBinder.getBackupManagerMonitor(resultFuture);
+        return IBackupManagerMonitor.Stub.asInterface((IBinder) getFutureResult(resultFuture));
+    }
+
+    /**
      * Allows the {@link TransportConnection} to notify this client
      * if the underlying transport has become unusable.  If that happens
      * we want to cancel all active futures or callbacks.
@@ -374,7 +386,8 @@
 
     private <T> T getFutureResult(AndroidFuture<T> future) {
         try {
-            return future.get(600, TimeUnit.SECONDS);
+            return future.get(BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis(),
+                    TimeUnit.MILLISECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException
                  | CancellationException e) {
             Slog.w(TAG, "Failed to get result from transport:", e);
diff --git a/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
index fb98825..deaa86c 100644
--- a/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
+++ b/services/backup/java/com/android/server/backup/transport/TransportStatusCallback.java
@@ -23,13 +23,13 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.backup.ITransportStatusCallback;
+import com.android.server.backup.BackupAndRestoreFeatureFlags;
 
 public class TransportStatusCallback extends ITransportStatusCallback.Stub {
     private static final String TAG = "TransportStatusCallback";
-    private static final int TIMEOUT_MILLIS = 300 * 1000; // 5 minutes.
     private static final int OPERATION_STATUS_DEFAULT = 0;
 
-    private final int mOperationTimeout;
+    private final long mOperationTimeout;
 
     @GuardedBy("this")
     private int mOperationStatus = OPERATION_STATUS_DEFAULT;
@@ -37,7 +37,7 @@
     private boolean mHasCompletedOperation = false;
 
     public TransportStatusCallback() {
-        mOperationTimeout = TIMEOUT_MILLIS;
+        mOperationTimeout = BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis();
     }
 
     @VisibleForTesting
diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING
index 38d9372..37c47ba 100644
--- a/services/companion/TEST_MAPPING
+++ b/services/companion/TEST_MAPPING
@@ -8,14 +8,6 @@
     },
     {
       "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases"
-    },
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
     }
   ]
 }
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java
index c4f5766..a57f5a2 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java
@@ -45,11 +45,11 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
index dbff628..2c5d582 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
@@ -35,12 +35,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
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..ce7854d 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;
@@ -85,6 +86,15 @@
     }
 
     /**
+     * For communicating when activities are blocked from entering PIP on the display by this
+     * policy controller.
+     */
+    public interface PipBlockedCallback {
+        /** Called when an activity is blocked from entering PIP. */
+        void onEnteringPipBlocked(int uid);
+    }
+
+    /**
      * If required, allow the secure activity to display on remote device since
      * {@link android.os.Build.VERSION_CODES#TIRAMISU}.
      */
@@ -111,8 +121,11 @@
     @GuardedBy("mGenericWindowPolicyControllerLock")
     final ArraySet<Integer> mRunningUids = new ArraySet<>();
     @Nullable private final ActivityListener mActivityListener;
+    @Nullable private final PipBlockedCallback mPipBlockedCallback;
     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;
@@ -152,6 +165,7 @@
             @NonNull Set<ComponentName> blockedActivities,
             @ActivityPolicy int defaultActivityPolicy,
             @NonNull ActivityListener activityListener,
+            @NonNull PipBlockedCallback pipBlockedCallback,
             @NonNull ActivityBlockedCallback activityBlockedCallback,
             @NonNull SecureWindowCallback secureWindowCallback,
             @AssociationRequest.DeviceProfile String deviceProfile) {
@@ -166,6 +180,7 @@
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
         mDeviceProfile = deviceProfile;
+        mPipBlockedCallback = pipBlockedCallback;
         mSecureWindowCallback = secureWindowCallback;
     }
 
@@ -178,12 +193,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 +302,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
@@ -306,6 +329,17 @@
         }
     }
 
+    @Override
+    public boolean isEnteringPipAllowed(int uid) {
+        if (super.isEnteringPipAllowed(uid)) {
+            return true;
+        }
+        mHandler.post(() -> {
+            mPipBlockedCallback.onEnteringPipBlocked(uid);
+        });
+        return false;
+    }
+
     /**
      * Returns true if an app with the given UID has an activity running on the virtual display for
      * this controller.
@@ -354,4 +388,11 @@
         }
         return true;
     }
+
+    @VisibleForTesting
+    int getRunningAppsChangedListenersSizeForTesting() {
+        synchronized (mGenericWindowPolicyControllerLock) {
+            return mRunningAppsChangedListeners.size();
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index ec30369b..02053cc 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -31,6 +31,7 @@
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IInputConstants;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
@@ -75,7 +76,7 @@
     @interface PhysType {
     }
 
-    private final Object mLock;
+    final Object mLock;
 
     /* Token -> file descriptor associations. */
     @VisibleForTesting
@@ -220,6 +221,19 @@
         }
     }
 
+    /**
+     * @return the device id for a given token (identifiying a device)
+     */
+    int getInputDeviceId(IBinder token) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(token);
+            if (inputDeviceDescriptor == null) {
+                throw new IllegalArgumentException("Could not get device id for given token");
+            }
+            return inputDeviceDescriptor.getInputDeviceId();
+        }
+    }
+
     void setShowPointerIcon(boolean visible, int displayId) {
         mInputManagerInternal.setPointerIconVisible(visible, displayId);
     }
@@ -393,10 +407,22 @@
                         + inputDeviceDescriptor.getCreationOrderNumber());
                 fout.println("          type: " + inputDeviceDescriptor.getType());
                 fout.println("          phys: " + inputDeviceDescriptor.getPhys());
+                fout.println(
+                        "          inputDeviceId: " + inputDeviceDescriptor.getInputDeviceId());
             }
         }
     }
 
+    @VisibleForTesting
+    void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId,
+            String phys, int inputDeviceId) {
+        synchronized (mLock) {
+            mInputDeviceDescriptors.put(deviceToken,
+                    new InputDeviceDescriptor(fd, () -> {}, type, displayId, phys,
+                            inputDeviceId));
+        }
+    }
+
     private static native int nativeOpenUinputDpad(String deviceName, int vendorId,
             int productId, String phys);
     private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId,
@@ -493,16 +519,20 @@
         private final @Type int mType;
         private final int mDisplayId;
         private final String mPhys;
+        // The input device id that was associated to the device by the InputReader on device
+        // creation.
+        private final int mInputDeviceId;
         // Monotonically increasing number; devices with lower numbers were created earlier.
         private final long mCreationOrderNumber;
 
         InputDeviceDescriptor(int fd, IBinder.DeathRecipient deathRecipient, @Type int type,
-                int displayId, String phys) {
+                int displayId, String phys, int inputDeviceId) {
             mFd = fd;
             mDeathRecipient = deathRecipient;
             mType = type;
             mDisplayId = displayId;
             mPhys = phys;
+            mInputDeviceId = inputDeviceId;
             mCreationOrderNumber = sNextCreationOrderNumber.getAndIncrement();
         }
 
@@ -533,6 +563,10 @@
         public String getPhys() {
             return mPhys;
         }
+
+        public int getInputDeviceId() {
+            return mInputDeviceId;
+        }
     }
 
     private final class BinderDeathRecipient implements IBinder.DeathRecipient {
@@ -558,6 +592,8 @@
         private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
         private final InputManager.InputDeviceListener mListener;
 
+        private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
+
         WaitForDevice(String deviceName, int vendorId, int productId) {
             mListener = new InputManager.InputDeviceListener() {
                 @Override
@@ -572,6 +608,7 @@
                     if (id.getVendorId() != vendorId || id.getProductId() != productId) {
                         return;
                     }
+                    mInputDeviceId = deviceId;
                     mDeviceAddedLatch.countDown();
                 }
 
@@ -588,8 +625,13 @@
             InputManager.getInstance().registerInputDeviceListener(mListener, mHandler);
         }
 
-        /** Note: This must not be called from {@link #mHandler}'s thread. */
-        void waitForDeviceCreation() throws DeviceCreationException {
+        /**
+         * Note: This must not be called from {@link #mHandler}'s thread.
+         * @throws DeviceCreationException if the device was not created successfully within the
+         * timeout.
+         * @return The id of the created input device.
+         */
+        int waitForDeviceCreation() throws DeviceCreationException {
             try {
                 if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
                     throw new DeviceCreationException(
@@ -599,6 +641,12 @@
                 throw new DeviceCreationException(
                         "Interrupted while waiting for virtual device to be created.", e);
             }
+            if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) {
+                throw new IllegalStateException(
+                        "Virtual input device was created with an invalid "
+                                + "id=" + mInputDeviceId);
+            }
+            return mInputDeviceId;
         }
 
         @Override
@@ -643,6 +691,8 @@
         final int fd;
         final BinderDeathRecipient binderDeathRecipient;
 
+        final int inputDeviceId;
+
         setUniqueIdAssociation(displayId, phys);
         try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
             fd = deviceOpener.get();
@@ -652,7 +702,7 @@
             }
             // The fd is valid from here, so ensure that all failures close the fd after this point.
             try {
-                waiter.waitForDeviceCreation();
+                inputDeviceId = waiter.waitForDeviceCreation();
 
                 binderDeathRecipient = new BinderDeathRecipient(deviceToken);
                 try {
@@ -672,7 +722,8 @@
 
         synchronized (mLock) {
             mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys));
+                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys,
+                            inputDeviceId));
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 2835b69..fbde9e0 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -227,6 +227,12 @@
         return mParams.getName();
     }
 
+    /** Returns the policy specified for this policy type */
+    public @VirtualDeviceParams.DevicePolicy int getDevicePolicy(
+            @VirtualDeviceParams.PolicyType int policyType) {
+        return mParams.getDevicePolicy(policyType);
+    }
+
     /** Returns the unique device ID of this device. */
     @Override // Binder call
     public int getDeviceId() {
@@ -498,6 +504,17 @@
     }
 
     @Override // Binder call
+    public int getInputDeviceId(IBinder token) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.getInputDeviceId(token);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+
+    @Override // Binder call
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
         final long binderToken = Binder.clearCallingIdentity();
         try {
@@ -613,6 +630,7 @@
                             mParams.getBlockedActivities(),
                             mParams.getDefaultActivityPolicy(),
                             createListenerAdapter(),
+                            this::onEnteringPipBlocked,
                             this::onActivityBlocked,
                             this::onSecureWindowShown,
                             mAssociationInfo.getDeviceProfile());
@@ -768,6 +786,11 @@
         return mVirtualDisplayIds.contains(displayId);
     }
 
+    void onEnteringPipBlocked(int uid) {
+        showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_pip_blocked,
+                Toast.LENGTH_LONG, mContext.getMainLooper());
+    }
+
     interface OnDeviceCloseListener {
         void onClose(int associationId);
     }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index c400a74..a8797a0 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -233,6 +233,13 @@
         mLocalService.onAppsOnVirtualDeviceChanged();
     }
 
+    @VisibleForTesting
+    void addVirtualDevice(VirtualDeviceImpl virtualDevice) {
+        synchronized (mVirtualDeviceManagerLock) {
+            mVirtualDevices.put(virtualDevice.getAssociationId(), virtualDevice);
+        }
+    }
+
     class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements
             VirtualDeviceImpl.PendingTrampolineCallback {
 
@@ -358,6 +365,12 @@
             return virtualDevices;
         }
 
+        @Override // BinderCall
+        @VirtualDeviceParams.DevicePolicy
+        public int getDevicePolicy(int deviceId, @VirtualDeviceParams.PolicyType int policyType) {
+            return mLocalService.getDevicePolicy(deviceId, policyType);
+        }
+
         @Nullable
         private AssociationInfo getAssociationInfo(String packageName, int associationId) {
             final int callingUserId = getCallingUserHandle().getIdentifier();
@@ -439,6 +452,20 @@
         }
 
         @Override
+        @VirtualDeviceParams.DevicePolicy
+        public int getDevicePolicy(int deviceId, @VirtualDeviceParams.PolicyType int policyType) {
+            synchronized (mVirtualDeviceManagerLock) {
+                for (int i = 0; i < mVirtualDevices.size(); i++) {
+                    final VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
+                    if (device.getDeviceId() == deviceId) {
+                        return device.getDevicePolicy(policyType);
+                    }
+                }
+            }
+            return VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+        }
+
+        @Override
         public void onVirtualDisplayCreated(int displayId) {
             final VirtualDisplayListener[] listeners;
             synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 0f101b0..08ee6d7 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -658,7 +658,6 @@
                 int sessionId, int flags, @NonNull IResultReceiver result) {
             Objects.requireNonNull(activityToken);
             Objects.requireNonNull(shareableActivityToken);
-            Objects.requireNonNull(sessionId);
             final int userId = UserHandle.getCallingUserId();
 
             final ActivityPresentationInfo activityPresentationInfo = getAmInternal()
@@ -677,7 +676,6 @@
 
         @Override
         public void finishSession(int sessionId) {
-            Objects.requireNonNull(sessionId);
             final int userId = UserHandle.getCallingUserId();
 
             synchronized (mLock) {
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index cee78e0..34787a3 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -728,7 +728,7 @@
             if (oldList != null && adding != null) {
                 adding.removeAll(oldList);
             }
-
+            addingCount = CollectionUtils.size(adding);
             EventLog.writeEvent(EventLogTags.CC_UPDATE_OPTIONS, mUserId, addingCount);
             for (int i = 0; i < addingCount; i++) {
                 String packageName = adding.valueAt(i);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3aed167..84f2b63 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -156,6 +156,8 @@
         "android.hardware.health-translate-java",
         "android.hardware.light-V1-java",
         "android.hardware.tv.cec-V1.1-java",
+        "android.hardware.tv.cec-V1-java",
+        "android.hardware.tv.hdmi-V1-java",
         "android.hardware.weaver-V1.0-java",
         "android.hardware.biometrics.face-V1.0-java",
         "android.hardware.biometrics.fingerprint-V2.3-java",
@@ -166,7 +168,7 @@
         "android.hardware.rebootescrow-V1-java",
         "android.hardware.soundtrigger-V2.3-java",
         "android.hardware.power.stats-V1-java",
-        "android.hardware.power-V3-java",
+        "android.hardware.power-V4-java",
         "android.hidl.manager-V1.2-java",
         "capture_state_listener-aidl-java",
         "icu4j_calendar_astronomer",
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 41044bf..b70cbe3 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -27,7 +27,6 @@
 import java.util.Collection;
 import java.util.List;
 
-
 /**
  * Battery stats local system service interface. This is used to pass internal data out of
  * BatteryStatsImpl, as well as make unchecked calls into BatteryStatsImpl.
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index fa52ac9..544dd4e 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -26,10 +26,21 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.IBackgroundInstallControlService;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.Signature;
+import android.content.pm.SigningDetails;
+import android.content.pm.SigningInfo;
+import android.content.pm.parsing.result.ParseInput;
+import android.content.pm.parsing.result.ParseResult;
+import android.content.pm.parsing.result.ParseTypeImpl;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -37,19 +48,29 @@
 import android.os.ShellCallback;
 import android.os.ShellCommand;
 import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.PackageUtils;
 import android.util.Slog;
+import android.util.apk.ApkSignatureVerifier;
+import android.util.apk.ApkSigningBlockUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IBinaryTransparencyService;
 import com.android.internal.util.FrameworkStatsLog;
 
+import libcore.util.HexEncoding;
+
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
@@ -70,10 +91,36 @@
     @VisibleForTesting
     static final String BINARY_HASH_ERROR = "SHA256HashError";
 
+    static final int MEASURE_APEX_AND_MODULES = 1;
+    static final int MEASURE_PRELOADS = 2;
+    static final int MEASURE_NEW_MBAS = 3;
+
+    static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000;
+
+    @VisibleForTesting
+    static final String BUNDLE_PACKAGE_INFO = "package-info";
+    @VisibleForTesting
+    static final String BUNDLE_CONTENT_DIGEST_ALGORITHM = "content-digest-algo";
+    @VisibleForTesting
+    static final String BUNDLE_CONTENT_DIGEST = "content-digest";
+
+    // used for indicating any type of error during MBA measurement
+    static final int MBA_STATUS_ERROR = 0;
+    // used for indicating factory condition preloads
+    static final int MBA_STATUS_PRELOADED = 1;
+    // used for indicating preloaded apps that are updated
+    static final int MBA_STATUS_UPDATED_PRELOAD = 2;
+    // used for indicating newly installed MBAs
+    static final int MBA_STATUS_NEW_INSTALL = 3;
+    // used for indicating newly installed MBAs that are updated (but unused currently)
+    static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4;
+
+    private static final boolean DEBUG = true;     // set this to false upon submission
+
     private final Context mContext;
     private String mVbmetaDigest;
-    private HashMap<String, String> mBinaryHashes;
-    private HashMap<String, Long> mBinaryLastUpdateTimes;
+    // the system time (in ms) the last measurement was taken
+    private long mMeasurementsLastRecordedMs;
 
     final class BinaryTransparencyServiceImpl extends IBinaryTransparencyService.Stub {
 
@@ -83,25 +130,298 @@
         }
 
         @Override
-        public Map getApexInfo() {
-            HashMap results = new HashMap();
-            if (!updateBinaryMeasurements()) {
-                Slog.e(TAG, "Error refreshing APEX measurements.");
-                return results;
-            }
-            PackageManager pm = mContext.getPackageManager();
-            if (pm == null) {
-                Slog.e(TAG, "Error obtaining an instance of PackageManager.");
-                return results;
-            }
+        public List getApexInfo() {
+            List<Bundle> results = new ArrayList<>();
 
-            for (PackageInfo packageInfo : getInstalledApexs()) {
-                results.put(packageInfo, mBinaryHashes.get(packageInfo.packageName));
+            for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
+                Bundle apexMeasurement = measurePackage(packageInfo);
+                results.add(apexMeasurement);
             }
 
             return results;
         }
 
+        /**
+         * A helper function to compute the SHA256 digest of APK package signer.
+         * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}.
+         * @return an array of {@code String} representing hex encoded string of the
+         *         SHA256 digest of APK signer(s). The number of signers will be reflected by the
+         *         size of the array.
+         *         However, {@code null} is returned if there is any error.
+         */
+        private String[] computePackageSignerSha256Digests(@Nullable SigningInfo signingInfo) {
+            if (signingInfo == null) {
+                Slog.e(TAG, "signingInfo is null");
+                return null;
+            }
+
+            Signature[] packageSigners = signingInfo.getApkContentsSigners();
+            List<String> resultList = new ArrayList<>();
+            for (Signature packageSigner : packageSigners) {
+                byte[] digest = PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray());
+                String digestHexString = HexEncoding.encodeToString(digest, false);
+                resultList.add(digestHexString);
+            }
+            return resultList.toArray(new String[1]);
+        }
+
+        /**
+         * Perform basic measurement (i.e. content digest) on a given package.
+         * @param packageInfo The package to be measured.
+         * @return a {@link android.os.Bundle} that packs the measurement result with the following
+         *         keys: {@link #BUNDLE_PACKAGE_INFO},
+         *               {@link #BUNDLE_CONTENT_DIGEST_ALGORITHM}
+         *               {@link #BUNDLE_CONTENT_DIGEST}
+         */
+        private @NonNull Bundle measurePackage(PackageInfo packageInfo) {
+            Bundle result = new Bundle();
+
+            // compute content digest
+            if (DEBUG) {
+                Slog.d(TAG, "Computing content digest for " + packageInfo.packageName + " at "
+                        + packageInfo.applicationInfo.sourceDir);
+            }
+            Map<Integer, byte[]> contentDigests = computeApkContentDigest(
+                    packageInfo.applicationInfo.sourceDir);
+            result.putParcelable(BUNDLE_PACKAGE_INFO, packageInfo);
+            if (contentDigests == null) {
+                Slog.d(TAG, "Failed to compute content digest for "
+                        + packageInfo.applicationInfo.sourceDir);
+                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
+                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
+                return result;
+            }
+
+            // in this iteration, we'll be supporting only 2 types of digests:
+            // CHUNKED_SHA256 and CHUNKED_SHA512.
+            // And only one of them will be available per package.
+            if (contentDigests.containsKey(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) {
+                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256;
+                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
+                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+            } else if (contentDigests.containsKey(
+                    ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) {
+                Integer algorithmId = ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512;
+                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, algorithmId);
+                result.putByteArray(BUNDLE_CONTENT_DIGEST, contentDigests.get(algorithmId));
+            } else {
+                // TODO(b/259423111): considering putting the raw values for the algorithm & digest
+                //  into the bundle to track potential other digest algorithms that may be in use
+                result.putInt(BUNDLE_CONTENT_DIGEST_ALGORITHM, 0);
+                result.putByteArray(BUNDLE_CONTENT_DIGEST, null);
+            }
+
+            return result;
+        }
+
+
+        /**
+         * Measures and records digests for *all* covered binaries/packages.
+         *
+         * This method will be called in a Job scheduled to take measurements periodically.
+         *
+         * Packages that are covered so far are:
+         * - all APEXs (introduced in Android T)
+         * - all mainline modules (introduced in Android T)
+         * - all preloaded apps and their update(s) (new in Android U)
+         * - dynamically installed mobile bundled apps (MBAs) (new in Android U)
+         *
+         * @return a {@code List<Bundle>}. Each Bundle item contains values as
+         *          defined by the return value of {@link #measurePackage(PackageInfo)}.
+         */
+        public List getMeasurementsForAllPackages() {
+            List<Bundle> results = new ArrayList<>();
+            PackageManager pm = mContext.getPackageManager();
+            Set<String> packagesMeasured = new HashSet<>();
+
+            // check if we should record the resulting measurements
+            long currentTimeMs = System.currentTimeMillis();
+            boolean record = false;
+            if ((currentTimeMs - mMeasurementsLastRecordedMs) >= RECORD_MEASUREMENTS_COOLDOWN_MS) {
+                Slog.d(TAG, "Measurement was last taken at " + mMeasurementsLastRecordedMs
+                        + " and is now updated to: " + currentTimeMs);
+                mMeasurementsLastRecordedMs = currentTimeMs;
+                record = true;
+            }
+
+            // measure all APEXs first
+            if (DEBUG) {
+                Slog.d(TAG, "Measuring APEXs...");
+            }
+            for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
+                packagesMeasured.add(packageInfo.packageName);
+
+                Bundle apexMeasurement = measurePackage(packageInfo);
+                results.add(apexMeasurement);
+
+                if (record) {
+                    // compute digests of signing info
+                    String[] signerDigestHexStrings = computePackageSignerSha256Digests(
+                            packageInfo.signingInfo);
+
+                    // log to Westworld
+                    FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED,
+                                            packageInfo.packageName,
+                                            packageInfo.getLongVersionCode(),
+                                            HexEncoding.encodeToString(apexMeasurement.getByteArray(
+                                                    BUNDLE_CONTENT_DIGEST), false),
+                                            apexMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM),
+                                            signerDigestHexStrings);
+                }
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Measured " + packagesMeasured.size()
+                        + " packages after considering APEXs.");
+            }
+
+            // proceed with all preloaded apps
+            for (PackageInfo packageInfo : pm.getInstalledPackages(
+                    PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY
+                            | PackageManager.GET_SIGNING_CERTIFICATES))) {
+                if (packagesMeasured.contains(packageInfo.packageName)) {
+                    continue;
+                }
+                packagesMeasured.add(packageInfo.packageName);
+
+                int mba_status = MBA_STATUS_PRELOADED;
+                if (packageInfo.signingInfo == null) {
+                    Slog.d(TAG, "Preload " + packageInfo.packageName  + " at "
+                            + packageInfo.applicationInfo.sourceDir + " has likely been updated.");
+                    mba_status = MBA_STATUS_UPDATED_PRELOAD;
+
+                    PackageInfo origPackageInfo = packageInfo;
+                    try {
+                        packageInfo = pm.getPackageInfo(packageInfo.packageName,
+                                PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL
+                                        | PackageManager.GET_SIGNING_CERTIFICATES));
+                    } catch (PackageManager.NameNotFoundException e) {
+                        Slog.e(TAG, "Failed to obtain an updated PackageInfo of "
+                                + origPackageInfo.packageName, e);
+                        packageInfo = origPackageInfo;
+                        mba_status = MBA_STATUS_ERROR;
+                    }
+                }
+
+
+                Bundle packageMeasurement = measurePackage(packageInfo);
+                results.add(packageMeasurement);
+
+                if (record) {
+                    // compute digests of signing info
+                    String[] signerDigestHexStrings = computePackageSignerSha256Digests(
+                            packageInfo.signingInfo);
+
+                    // now we should have all the bits for the atom
+                    /*  TODO: Uncomment and test after merging new atom definition.
+                    FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED,
+                            packageInfo.packageName,
+                            packageInfo.getLongVersionCode(),
+                            HexEncoding.encodeToString(packageMeasurement.getByteArray(
+                                    BUNDLE_CONTENT_DIGEST), false),
+                            packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM),
+                            signerDigestHexStrings, // signer_cert_digest
+                            mba_status,                 // mba_status
+                            null,                   // initiator
+                            null,                   // initiator_signer_digest
+                            null,                   // installer
+                            null                    // originator
+                    );
+                     */
+                }
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Measured " + packagesMeasured.size()
+                        + " packages after considering preloads");
+            }
+
+            // lastly measure all newly installed MBAs
+            for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
+                if (packagesMeasured.contains(packageInfo.packageName)) {
+                    continue;
+                }
+                packagesMeasured.add(packageInfo.packageName);
+
+                Bundle packageMeasurement = measurePackage(packageInfo);
+                results.add(packageMeasurement);
+
+                if (record) {
+                    // compute digests of signing info
+                    String[] signerDigestHexStrings = computePackageSignerSha256Digests(
+                            packageInfo.signingInfo);
+
+                    // then extract package's InstallSourceInfo
+                    if (DEBUG) {
+                        Slog.d(TAG, "Extracting InstallSourceInfo for " + packageInfo.packageName);
+                    }
+                    InstallSourceInfo installSourceInfo = getInstallSourceInfo(
+                            packageInfo.packageName);
+                    String initiator = null;
+                    SigningInfo initiatorSignerInfo = null;
+                    String[] initiatorSignerInfoDigest = null;
+                    String installer = null;
+                    String originator = null;
+
+                    if (installSourceInfo != null) {
+                        initiator = installSourceInfo.getInitiatingPackageName();
+                        initiatorSignerInfo = installSourceInfo.getInitiatingPackageSigningInfo();
+                        if (initiatorSignerInfo != null) {
+                            initiatorSignerInfoDigest = computePackageSignerSha256Digests(
+                                    initiatorSignerInfo);
+                        }
+                        installer = installSourceInfo.getInstallingPackageName();
+                        originator = installSourceInfo.getOriginatingPackageName();
+                    }
+
+                    // we should now have all the info needed for the atom
+                    /*  TODO: Uncomment and test after merging new atom definition.
+                    FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED,
+                            packageInfo.packageName,
+                            packageInfo.getLongVersionCode(),
+                            HexEncoding.encodeToString(packageMeasurement.getByteArray(
+                                    BUNDLE_CONTENT_DIGEST), false),
+                            packageMeasurement.getInt(BUNDLE_CONTENT_DIGEST_ALGORITHM),
+                            signerDigestHexStrings,
+                            MBA_STATUS_NEW_INSTALL,   // mba_status
+                            initiator,
+                            initiatorSignerInfoDigest,
+                            installer,
+                            originator
+                    );
+                     */
+                }
+            }
+            if (DEBUG) {
+                long timeSpentMeasuring = System.currentTimeMillis() - currentTimeMs;
+                Slog.d(TAG, "Measured " + packagesMeasured.size()
+                        + " packages altogether in " + timeSpentMeasuring + "ms");
+            }
+
+            return results;
+        }
+
+        /**
+         * A wrapper around
+         * {@link ApkSignatureVerifier#verifySignaturesInternal(ParseInput, String, int, boolean)}.
+         * @param pathToApk The APK's installation path
+         * @return a {@code Map<Integer, byte[]>} with algorithm type as the key and content
+         *         digest as the value.
+         *         a {@code null} is returned upon encountering any error.
+         */
+        private Map<Integer, byte[]> computeApkContentDigest(String pathToApk) {
+            final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
+            ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> parseResult =
+                    ApkSignatureVerifier.verifySignaturesInternal(input,
+                            pathToApk,
+                            SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2, false);
+            if (parseResult.isError()) {
+                Slog.e(TAG, "Failed to compute content digest for "
+                        + pathToApk + " due to: "
+                        + parseResult.getErrorMessage());
+                return null;
+            }
+            return parseResult.getResult().contentDigests;
+        }
+
         @Override
         public void onShellCommand(@Nullable FileDescriptor in,
                                    @Nullable FileDescriptor out,
@@ -151,6 +471,145 @@
                     return 0;
                 }
 
+                private void printPackageMeasurements(PackageInfo packageInfo,
+                                                      final PrintWriter pw) {
+                    Map<Integer, byte[]> contentDigests = computeApkContentDigest(
+                            packageInfo.applicationInfo.sourceDir);
+                    if (contentDigests == null) {
+                        pw.println("ERROR: Failed to compute package content digest for "
+                                + packageInfo.applicationInfo.sourceDir);
+                        return;
+                    }
+
+                    for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) {
+                        Integer algorithmId = entry.getKey();
+                        byte[] contentDigest = entry.getValue();
+
+                        // TODO(b/259348134): consider refactoring the following to a helper method
+                        switch (algorithmId) {
+                            case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256:
+                                pw.print("CHUNKED_SHA256:");
+                                break;
+                            case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512:
+                                pw.print("CHUNKED_SHA512:");
+                                break;
+                            case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+                                pw.print("VERITY_CHUNKED_SHA256:");
+                                break;
+                            case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256:
+                                pw.print("SHA256:");
+                                break;
+                            default:
+                                pw.print("UNKNOWN_ALGO_ID(" + algorithmId + "):");
+                        }
+                        pw.print(HexEncoding.encodeToString(contentDigest, false));
+                    }
+                }
+
+                private void printPackageInstallationInfo(PackageInfo packageInfo,
+                                                          final PrintWriter pw) {
+                    pw.println("--- Package Installation Info ---");
+                    pw.println("Current install location: "
+                            + packageInfo.applicationInfo.sourceDir);
+                    if (packageInfo.applicationInfo.sourceDir.startsWith("/data/apex/")) {
+                        String origPackageFilepath = getOriginalApexPreinstalledLocation(
+                                packageInfo.packageName, packageInfo.applicationInfo.sourceDir);
+                        pw.println("|--> Pre-installed package install location: "
+                                + origPackageFilepath);
+
+                        // TODO(b/259347186): revive this with the proper cmd options.
+                        /*
+                        String digest = PackageUtils.computeSha256DigestForLargeFile(
+                        origPackageFilepath, PackageUtils.createLargeFileBuffer());
+                         */
+
+                        Map<Integer, byte[]> contentDigests = computeApkContentDigest(
+                                origPackageFilepath);
+                        if (contentDigests == null) {
+                            pw.println("ERROR: Failed to compute package content digest for "
+                                    + origPackageFilepath);
+                        } else {
+                            // TODO(b/259348134): consider refactoring this to a helper method
+                            for (Map.Entry<Integer, byte[]> entry : contentDigests.entrySet()) {
+                                Integer algorithmId = entry.getKey();
+                                byte[] contentDigest = entry.getValue();
+                                pw.print("|--> Pre-installed package content digest algorithm: ");
+                                switch (algorithmId) {
+                                    case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256:
+                                        pw.print("CHUNKED_SHA256");
+                                        break;
+                                    case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512:
+                                        pw.print("CHUNKED_SHA512");
+                                        break;
+                                    case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+                                        pw.print("VERITY_CHUNKED_SHA256");
+                                        break;
+                                    case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256:
+                                        pw.print("SHA256");
+                                        break;
+                                    default:
+                                        pw.print("UNKNOWN");
+                                }
+                                pw.print("\n");
+                                pw.print("|--> Pre-installed package content digest: ");
+                                pw.print(HexEncoding.encodeToString(contentDigest, false));
+                                pw.print("\n");
+                            }
+                        }
+                    }
+                    pw.println("First install time (ms): " + packageInfo.firstInstallTime);
+                    pw.println("Last update time (ms): " + packageInfo.lastUpdateTime);
+                    boolean isPreloaded = (packageInfo.firstInstallTime
+                            == packageInfo.lastUpdateTime);
+                    pw.println("Is preloaded: " + isPreloaded);
+
+                    InstallSourceInfo installSourceInfo = getInstallSourceInfo(
+                            packageInfo.packageName);
+                    if (installSourceInfo == null) {
+                        pw.println("ERROR: Unable to obtain installSourceInfo of "
+                                + packageInfo.packageName);
+                    } else {
+                        pw.println("Installation initiated by: "
+                                + installSourceInfo.getInitiatingPackageName());
+                        pw.println("Installation done by: "
+                                + installSourceInfo.getInstallingPackageName());
+                        pw.println("Installation originating from: "
+                                + installSourceInfo.getOriginatingPackageName());
+                    }
+
+                    if (packageInfo.isApex) {
+                        pw.println("Is an active APEX: " + packageInfo.isActiveApex);
+                    }
+                }
+
+                private void printPackageSignerDetails(SigningInfo signerInfo,
+                                                       final PrintWriter pw) {
+                    if (signerInfo == null) {
+                        pw.println("ERROR: Package's signingInfo is null.");
+                        return;
+                    }
+                    pw.println("--- Package Signer Info ---");
+                    pw.println("Has multiple signers: " + signerInfo.hasMultipleSigners());
+                    Signature[] packageSigners = signerInfo.getApkContentsSigners();
+                    for (Signature packageSigner : packageSigners) {
+                        byte[] packageSignerDigestBytes =
+                                PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray());
+                        String packageSignerDigestHextring =
+                                HexEncoding.encodeToString(packageSignerDigestBytes, false);
+                        pw.println("Signer cert's SHA256-digest: " + packageSignerDigestHextring);
+                        try {
+                            PublicKey publicKey = packageSigner.getPublicKey();
+                            pw.println("Signing key algorithm: " + publicKey.getAlgorithm());
+                        } catch (CertificateException e) {
+                            Slog.e(TAG,
+                                    "Failed to obtain public key of signer for cert with hash: "
+                                    + packageSignerDigestHextring);
+                            e.printStackTrace();
+                        }
+                    }
+
+                }
+
                 private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) {
                     pw.println("--- Module Details ---");
                     pw.println("Module name: " + moduleInfo.getName());
@@ -158,21 +617,90 @@
                             + (moduleInfo.isHidden() ? "hidden" : "visible"));
                 }
 
+                private void printAppDetails(PackageInfo packageInfo,
+                                             boolean printLibraries,
+                                             final PrintWriter pw) {
+                    pw.println("--- App Details ---");
+                    pw.println("Name: " + packageInfo.applicationInfo.name);
+                    pw.println("Label: " + mContext.getPackageManager().getApplicationLabel(
+                            packageInfo.applicationInfo));
+                    pw.println("Description: " + packageInfo.applicationInfo.loadDescription(
+                            mContext.getPackageManager()));
+                    pw.println("Has code: " + packageInfo.applicationInfo.hasCode());
+                    pw.println("Is enabled: " + packageInfo.applicationInfo.enabled);
+                    pw.println("Is suspended: " + ((packageInfo.applicationInfo.flags
+                                                    & ApplicationInfo.FLAG_SUSPENDED) != 0));
+
+                    pw.println("Compile SDK version: " + packageInfo.compileSdkVersion);
+                    pw.println("Target SDK version: "
+                            + packageInfo.applicationInfo.targetSdkVersion);
+
+                    pw.println("Is privileged: "
+                            + packageInfo.applicationInfo.isPrivilegedApp());
+                    pw.println("Is a stub: " + packageInfo.isStub);
+                    pw.println("Is a core app: " + packageInfo.coreApp);
+                    pw.println("SEInfo: " + packageInfo.applicationInfo.seInfo);
+                    pw.println("Component factory: "
+                            + packageInfo.applicationInfo.appComponentFactory);
+                    pw.println("Process name: " + packageInfo.applicationInfo.processName);
+                    pw.println("Task affinity : " + packageInfo.applicationInfo.taskAffinity);
+                    pw.println("UID: " + packageInfo.applicationInfo.uid);
+                    pw.println("Shared UID: " + packageInfo.sharedUserId);
+
+                    if (printLibraries) {
+                        pw.println("== App's Shared Libraries ==");
+                        List<SharedLibraryInfo> sharedLibraryInfos =
+                                packageInfo.applicationInfo.getSharedLibraryInfos();
+                        if (sharedLibraryInfos == null || sharedLibraryInfos.isEmpty()) {
+                            pw.println("<none>");
+                        }
+
+                        for (int i = 0; i < sharedLibraryInfos.size(); i++) {
+                            SharedLibraryInfo sharedLibraryInfo = sharedLibraryInfos.get(i);
+                            pw.println("  ++ Library #" + (i + 1) + " ++");
+                            pw.println("  Lib name: " + sharedLibraryInfo.getName());
+                            long libVersion = sharedLibraryInfo.getLongVersion();
+                            pw.print("  Lib version: ");
+                            if (libVersion == SharedLibraryInfo.VERSION_UNDEFINED) {
+                                pw.print("undefined");
+                            } else {
+                                pw.print(libVersion);
+                            }
+                            pw.print("\n");
+
+                            pw.println("  Lib package name (if available): "
+                                    + sharedLibraryInfo.getPackageName());
+                            pw.println("  Lib path: " + sharedLibraryInfo.getPath());
+                            pw.print("  Lib type: ");
+                            switch (sharedLibraryInfo.getType()) {
+                                case SharedLibraryInfo.TYPE_BUILTIN:
+                                    pw.print("built-in");
+                                    break;
+                                case SharedLibraryInfo.TYPE_DYNAMIC:
+                                    pw.print("dynamic");
+                                    break;
+                                case SharedLibraryInfo.TYPE_STATIC:
+                                    pw.print("static");
+                                    break;
+                                case SharedLibraryInfo.TYPE_SDK_PACKAGE:
+                                    pw.print("SDK");
+                                    break;
+                                case SharedLibraryInfo.VERSION_UNDEFINED:
+                                default:
+                                    pw.print("undefined");
+                                    break;
+                            }
+                            pw.print("\n");
+                            pw.println("  Is a native lib: " + sharedLibraryInfo.isNative());
+                        }
+                    }
+
+                }
+
                 private int printAllApexs() {
                     final PrintWriter pw = getOutPrintWriter();
                     boolean verbose = false;
                     String opt;
-
-                    // refresh cache to make sure info is most up-to-date
-                    if (!updateBinaryMeasurements()) {
-                        pw.println("ERROR: Failed to refresh info for APEXs.");
-                        return -1;
-                    }
-                    if (mBinaryHashes == null || (mBinaryHashes.size() == 0)) {
-                        pw.println("ERROR: Unable to obtain apex_info at this time.");
-                        return -1;
-                    }
-
                     while ((opt = getNextOption()) != null) {
                         switch (opt) {
                             case "-v":
@@ -190,28 +718,37 @@
                         return -1;
                     }
 
-                    pw.println("APEX Info:");
-                    for (PackageInfo packageInfo : getInstalledApexs()) {
+                    if (!verbose) {
+                        pw.println("APEX Info [Format: package_name,package_version,"
+                                // TODO(b/259347186): revive via special cmd line option
+                                //+ "package_sha256_digest,"
+                                + "content_digest_algorithm:content_digest]:");
+                    }
+                    for (PackageInfo packageInfo : getCurrentInstalledApexs()) {
+                        if (verbose) {
+                            pw.println("APEX Info [Format: package_name,package_version,"
+                                    // TODO(b/259347186): revive via special cmd line option
+                                    //+ "package_sha256_digest,"
+                                    + "content_digest_algorithm:content_digest]:");
+                        }
                         String packageName = packageInfo.packageName;
-                        pw.println(packageName + ";"
-                                + packageInfo.getLongVersionCode() + ":"
-                                + mBinaryHashes.get(packageName).toLowerCase());
+                        pw.print(packageName + ","
+                                + packageInfo.getLongVersionCode() + ",");
+                        printPackageMeasurements(packageInfo, pw);
+                        pw.print("\n");
 
                         if (verbose) {
-                            pw.println("Install location: "
-                                    + packageInfo.applicationInfo.sourceDir);
-                            pw.println("Last Update Time (ms): " + packageInfo.lastUpdateTime);
-
                             ModuleInfo moduleInfo;
                             try {
                                 moduleInfo = pm.getModuleInfo(packageInfo.packageName, 0);
+                                pw.println("Is a module: true");
+                                printModuleDetails(moduleInfo, pw);
                             } catch (PackageManager.NameNotFoundException e) {
-                                pw.println("Is A Module: False");
-                                pw.println("");
-                                continue;
+                                pw.println("Is a module: false");
                             }
-                            pw.println("Is A Module: True");
-                            printModuleDetails(moduleInfo, pw);
+
+                            printPackageInstallationInfo(packageInfo, pw);
+                            printPackageSignerDetails(packageInfo.signingInfo, pw);
                             pw.println("");
                         }
                     }
@@ -222,17 +759,6 @@
                     final PrintWriter pw = getOutPrintWriter();
                     boolean verbose = false;
                     String opt;
-
-                    // refresh cache to make sure info is most up-to-date
-                    if (!updateBinaryMeasurements()) {
-                        pw.println("ERROR: Failed to refresh info for Modules.");
-                        return -1;
-                    }
-                    if (mBinaryHashes == null || (mBinaryHashes.size() == 0)) {
-                        pw.println("ERROR: Unable to obtain module_info at this time.");
-                        return -1;
-                    }
-
                     while ((opt = getNextOption()) != null) {
                         switch (opt) {
                             case "-v":
@@ -250,25 +776,39 @@
                         return -1;
                     }
 
-                    pw.println("Module Info:");
+                    if (!verbose) {
+                        pw.println("Module Info [Format: package_name,package_version,"
+                                // TODO(b/259347186): revive via special cmd line option
+                                //+ "package_sha256_digest,"
+                                + "content_digest_algorithm:content_digest]:");
+                    }
                     for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) {
                         String packageName = module.getPackageName();
+                        if (verbose) {
+                            pw.println("Module Info [Format: package_name,package_version,"
+                                    // TODO(b/259347186): revive via special cmd line option
+                                    //+ "package_sha256_digest,"
+                                    + "content_digest_algorithm:content_digest]:");
+                        }
                         try {
                             PackageInfo packageInfo = pm.getPackageInfo(packageName,
-                                    PackageManager.MATCH_APEX);
-                            pw.println(packageInfo.packageName + ";"
-                                    + packageInfo.getLongVersionCode() + ":"
-                                    + mBinaryHashes.get(packageName).toLowerCase());
+                                    PackageManager.MATCH_APEX
+                                            | PackageManager.GET_SIGNING_CERTIFICATES);
+                            //pw.print("package:");
+                            pw.print(packageInfo.packageName + ",");
+                            pw.print(packageInfo.getLongVersionCode() + ",");
+                            printPackageMeasurements(packageInfo, pw);
+                            pw.print("\n");
 
                             if (verbose) {
-                                pw.println("Install location: "
-                                        + packageInfo.applicationInfo.sourceDir);
                                 printModuleDetails(module, pw);
+                                printPackageInstallationInfo(packageInfo, pw);
+                                printPackageSignerDetails(packageInfo.signingInfo, pw);
                                 pw.println("");
                             }
                         } catch (PackageManager.NameNotFoundException e) {
                             pw.println(packageName
-                                    + ";ERROR:Unable to find PackageInfo for this module.");
+                                    + ",ERROR:Unable to find PackageInfo for this module.");
                             if (verbose) {
                                 printModuleDetails(module, pw);
                                 pw.println("");
@@ -279,6 +819,72 @@
                     return 0;
                 }
 
+                private int printAllMbas() {
+                    final PrintWriter pw = getOutPrintWriter();
+                    boolean verbose = false;
+                    boolean printLibraries = false;
+                    String opt;
+                    while ((opt = getNextOption()) != null) {
+                        switch (opt) {
+                            case "-v":
+                                verbose = true;
+                                break;
+                            case "-l":
+                                printLibraries = true;
+                                break;
+                            default:
+                                pw.println("ERROR: Unknown option: " + opt);
+                                return 1;
+                        }
+                    }
+
+                    if (!verbose) {
+                        pw.println("MBA Info [Format: package_name,package_version,"
+                                // TODO(b/259347186): revive via special cmd line option
+                                //+ "package_sha256_digest,"
+                                + "content_digest_algorithm:content_digest]:");
+                    }
+                    for (PackageInfo packageInfo : getNewlyInstalledMbas()) {
+                        if (verbose) {
+                            pw.println("MBA Info [Format: package_name,package_version,"
+                                    // TODO(b/259347186): revive via special cmd line option
+                                    //+ "package_sha256_digest,"
+                                    + "content_digest_algorithm:content_digest]:");
+                        }
+                        pw.print(packageInfo.packageName + ",");
+                        pw.print(packageInfo.getLongVersionCode() + ",");
+                        printPackageMeasurements(packageInfo, pw);
+                        pw.print("\n");
+
+                        if (verbose) {
+                            printAppDetails(packageInfo, printLibraries, pw);
+                            printPackageInstallationInfo(packageInfo, pw);
+                            printPackageSignerDetails(packageInfo.signingInfo, pw);
+                            pw.println("");
+                        }
+                    }
+                    return 0;
+                }
+
+                // TODO(b/259347186): add option handling full file-based SHA256 digest
+                private int printAllPreloads() {
+                    final PrintWriter pw = getOutPrintWriter();
+
+                    PackageManager pm = mContext.getPackageManager();
+                    if (pm == null) {
+                        Slog.e(TAG, "Failed to obtain PackageManager.");
+                        return -1;
+                    }
+                    List<PackageInfo> factoryApps = pm.getInstalledPackages(
+                            PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY));
+
+                    pw.println("Preload Info [Format: package_name]");
+                    for (PackageInfo packageInfo : factoryApps) {
+                        pw.println(packageInfo.packageName);
+                    }
+                    return 0;
+                }
+
                 @Override
                 public int onCommand(String cmd) {
                     if (cmd == null) {
@@ -301,6 +907,10 @@
                                     return printAllApexs();
                                 case "module_info":
                                     return printAllModules();
+                                case "mba_info":
+                                    return printAllMbas();
+                                case "preload_info":
+                                    return printAllPreloads();
                                 default:
                                     pw.println(String.format("ERROR: Unknown info type '%s'",
                                             infoType));
@@ -324,11 +934,18 @@
                     pw.println("");
                     pw.println("    get apex_info [-v]");
                     pw.println("        Print information about installed APEXs on device.");
-                    pw.println("            -v: lists more verbose information about each APEX");
+                    pw.println("            -v: lists more verbose information about each APEX.");
                     pw.println("");
                     pw.println("    get module_info [-v]");
                     pw.println("        Print information about installed modules on device.");
-                    pw.println("            -v: lists more verbose information about each module");
+                    pw.println("            -v: lists more verbose information about each module.");
+                    pw.println("");
+                    pw.println("    get mba_info [-v] [-l]");
+                    pw.println("        Print information about installed mobile bundle apps "
+                               + "(MBAs on device).");
+                    pw.println("            -v: lists more verbose information about each app.");
+                    pw.println("            -l: lists shared library info. This will only be "
+                               + "listed with -v");
                     pw.println("");
                 }
 
@@ -346,8 +963,7 @@
         mContext = context;
         mServiceImpl = new BinaryTransparencyServiceImpl();
         mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED;
-        mBinaryHashes = new HashMap<>();
-        mBinaryLastUpdateTimes = new HashMap<>();
+        mMeasurementsLastRecordedMs = 0;
     }
 
     /**
@@ -378,44 +994,43 @@
             Slog.i(TAG, "Boot completed. Getting VBMeta Digest.");
             getVBMetaDigestInformation();
 
-            // due to potentially long computation that holds up boot time, computations for
-            // SHA256 digests of APEX and Module packages are scheduled here,
-            // but only executed when device is idle.
-            Slog.i(TAG, "Scheduling APEX and Module measurements to be updated.");
+            // to avoid the risk of holding up boot time, computations to measure APEX, Module, and
+            // MBA digests are scheduled here, but only executed when the device is idle and plugged
+            // in.
+            Slog.i(TAG, "Scheduling measurements to be taken.");
             UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext,
                     BinaryTransparencyService.this);
         }
     }
 
     /**
-     * JobService to update binary measurements and update internal cache.
+     * JobService to measure all covered binaries and record result to Westworld.
      */
     public static class UpdateMeasurementsJobService extends JobService {
-        private static final int COMPUTE_APEX_MODULE_SHA256_JOB_ID =
-                BinaryTransparencyService.UpdateMeasurementsJobService.class.hashCode();
+        private static final int DO_BINARY_MEASUREMENTS_JOB_ID =
+                UpdateMeasurementsJobService.class.hashCode();
 
         @Override
         public boolean onStartJob(JobParameters params) {
             Slog.d(TAG, "Job to update binary measurements started.");
-            if (params.getJobId() != COMPUTE_APEX_MODULE_SHA256_JOB_ID) {
+            if (params.getJobId() != DO_BINARY_MEASUREMENTS_JOB_ID) {
                 return false;
             }
 
-            // we'll still update the measurements via threads to be mindful of low-end devices
+            // we'll perform binary measurements via threads to be mindful of low-end devices
             // where this operation might take longer than expected, and so that we don't block
             // system_server's main thread.
             Executors.defaultThreadFactory().newThread(() -> {
-                // since we can't call updateBinaryMeasurements() directly, calling
-                // getApexInfo() achieves the same effect, and we simply discard the return
-                // value
-
+                // we discard the return value of getMeasurementsForAllPackages() as the
+                // results of the measurements will be recorded, and that is what we're aiming
+                // for with this job.
                 IBinder b = ServiceManager.getService(Context.BINARY_TRANSPARENCY_SERVICE);
                 IBinaryTransparencyService iBtsService =
                         IBinaryTransparencyService.Stub.asInterface(b);
                 try {
-                    iBtsService.getApexInfo();
+                    iBtsService.getMeasurementsForAllPackages();
                 } catch (RemoteException e) {
-                    Slog.e(TAG, "Updating binary measurements was interrupted.", e);
+                    Slog.e(TAG, "Taking binary measurements was interrupted.", e);
                     return;
                 }
                 jobFinished(params, false);
@@ -431,25 +1046,26 @@
 
         @SuppressLint("DefaultLocale")
         static void scheduleBinaryMeasurements(Context context, BinaryTransparencyService service) {
-            Slog.i(TAG, "Scheduling APEX & Module SHA256 digest computation job");
+            Slog.i(TAG, "Scheduling binary content-digest computation job");
             final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
             if (jobScheduler == null) {
                 Slog.e(TAG, "Failed to obtain an instance of JobScheduler.");
                 return;
             }
 
-            final JobInfo jobInfo = new JobInfo.Builder(COMPUTE_APEX_MODULE_SHA256_JOB_ID,
+            final JobInfo jobInfo = new JobInfo.Builder(DO_BINARY_MEASUREMENTS_JOB_ID,
                     new ComponentName(context, UpdateMeasurementsJobService.class))
                     .setRequiresDeviceIdle(true)
                     .setRequiresCharging(true)
+                    .setPeriodic(RECORD_MEASUREMENTS_COOLDOWN_MS)
                     .build();
             if (jobScheduler.schedule(jobInfo) != JobScheduler.RESULT_SUCCESS) {
-                Slog.e(TAG, "Failed to schedule job to update binary measurements.");
+                Slog.e(TAG, "Failed to schedule job to measure binaries.");
                 return;
             }
-            Slog.d(TAG, String.format(
-                    "Job %d to update binary measurements scheduled successfully.",
-                    COMPUTE_APEX_MODULE_SHA256_JOB_ID));
+            Slog.d(TAG, TextUtils.formatSimple(
+                    "Job %d to measure binaries was scheduled successfully.",
+                    DO_BINARY_MEASUREMENTS_JOB_ID));
         }
     }
 
@@ -460,7 +1076,7 @@
     }
 
     @NonNull
-    private List<PackageInfo> getInstalledApexs() {
+    private List<PackageInfo> getCurrentInstalledApexs() {
         List<PackageInfo> results = new ArrayList<>();
         PackageManager pm = mContext.getPackageManager();
         if (pm == null) {
@@ -468,7 +1084,8 @@
             return results;
         }
         List<PackageInfo> allPackages = pm.getInstalledPackages(
-                PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX));
+                PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX
+                        | PackageManager.GET_SIGNING_CERTIFICATES));
         if (allPackages == null) {
             Slog.e(TAG, "Error obtaining installed packages (including APEX)");
             return results;
@@ -478,133 +1095,67 @@
         return results;
     }
 
-
-    /**
-     * Updates the internal data structure with the most current APEX measurements.
-     * @return true if update is successful; false otherwise.
-     */
-    private boolean updateBinaryMeasurements() {
-        if (mBinaryHashes.size() == 0) {
-            Slog.d(TAG, "No apex in cache yet.");
-            doFreshBinaryMeasurements();
-            return true;
-        }
-
+    @Nullable
+    private InstallSourceInfo getInstallSourceInfo(String packageName) {
         PackageManager pm = mContext.getPackageManager();
         if (pm == null) {
-            Slog.e(TAG, "Failed to obtain a valid PackageManager instance.");
-            return false;
+            Slog.e(TAG, "Error obtaining an instance of PackageManager.");
+            return null;
         }
-
-        // We're assuming updates to existing modules and APEXs can happen, but not brand new
-        // ones appearing out of the blue. Thus, we're going to only go through our cache to check
-        // for changes, rather than freshly invoking `getInstalledPackages()` and
-        // `getInstalledModules()`
-        byte[] largeFileBuffer = PackageUtils.createLargeFileBuffer();
-        for (Map.Entry<String, Long> entry : mBinaryLastUpdateTimes.entrySet()) {
-            String packageName = entry.getKey();
-            try {
-                PackageInfo packageInfo = pm.getPackageInfo(packageName,
-                        PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX));
-                long cachedUpdateTime = entry.getValue();
-
-                if (packageInfo.lastUpdateTime > cachedUpdateTime) {
-                    Slog.d(TAG, packageName + " has been updated!");
-                    entry.setValue(packageInfo.lastUpdateTime);
-
-                    // compute the digest for the updated package
-                    String sha256digest = PackageUtils.computeSha256DigestForLargeFile(
-                            packageInfo.applicationInfo.sourceDir, largeFileBuffer);
-                    if (sha256digest == null) {
-                        Slog.e(TAG, "Failed to compute SHA256sum for file at "
-                                + packageInfo.applicationInfo.sourceDir);
-                        mBinaryHashes.put(packageName, BINARY_HASH_ERROR);
-                    } else {
-                        mBinaryHashes.put(packageName, sha256digest);
-                    }
-
-                    if (packageInfo.isApex) {
-                        FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED,
-                                packageInfo.packageName,
-                                packageInfo.getLongVersionCode(),
-                                mBinaryHashes.get(packageInfo.packageName));
-                    }
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                Slog.e(TAG, "Could not find package with name " + packageName);
-                continue;
-            }
-        }
-
-        return true;
-    }
-
-    private void doFreshBinaryMeasurements() {
-        PackageManager pm = mContext.getPackageManager();
-        Slog.d(TAG, "Obtained package manager");
-
-        // In general, we care about all APEXs, *and* all Modules, which may include some APKs.
-
-        // First, we deal with all installed APEXs.
-        byte[] largeFileBuffer = PackageUtils.createLargeFileBuffer();
-        for (PackageInfo packageInfo : getInstalledApexs()) {
-            ApplicationInfo appInfo = packageInfo.applicationInfo;
-
-            // compute SHA256 for these APEXs
-            String sha256digest = PackageUtils.computeSha256DigestForLargeFile(appInfo.sourceDir,
-                    largeFileBuffer);
-            if (sha256digest == null) {
-                Slog.e(TAG, String.format("Failed to compute SHA256 digest for %s",
-                        packageInfo.packageName));
-                mBinaryHashes.put(packageInfo.packageName, BINARY_HASH_ERROR);
-            } else {
-                mBinaryHashes.put(packageInfo.packageName, sha256digest);
-            }
-            FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, packageInfo.packageName,
-                    packageInfo.getLongVersionCode(), mBinaryHashes.get(packageInfo.packageName));
-            Slog.d(TAG, String.format("Last update time for %s: %d", packageInfo.packageName,
-                    packageInfo.lastUpdateTime));
-            mBinaryLastUpdateTimes.put(packageInfo.packageName, packageInfo.lastUpdateTime);
-        }
-
-        // Next, get all installed modules from PackageManager - skip over those APEXs we've
-        // processed above
-        for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) {
-            String packageName = module.getPackageName();
-            if (packageName == null) {
-                Slog.e(TAG, "ERROR: Encountered null package name for module "
-                        + module.getApexModuleName());
-                continue;
-            }
-            if (mBinaryHashes.containsKey(module.getPackageName())) {
-                continue;
-            }
-
-            // get PackageInfo for this module
-            try {
-                PackageInfo packageInfo = pm.getPackageInfo(packageName,
-                        PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX));
-                ApplicationInfo appInfo = packageInfo.applicationInfo;
-
-                // compute SHA256 digest for these modules
-                String sha256digest = PackageUtils.computeSha256DigestForLargeFile(
-                        appInfo.sourceDir, largeFileBuffer);
-                if (sha256digest == null) {
-                    Slog.e(TAG, String.format("Failed to compute SHA256 digest for %s",
-                            packageName));
-                    mBinaryHashes.put(packageName, BINARY_HASH_ERROR);
-                } else {
-                    mBinaryHashes.put(packageName, sha256digest);
-                }
-                Slog.d(TAG, String.format("Last update time for %s: %d", packageName,
-                        packageInfo.lastUpdateTime));
-                mBinaryLastUpdateTimes.put(packageName, packageInfo.lastUpdateTime);
-            } catch (PackageManager.NameNotFoundException e) {
-                Slog.e(TAG, "ERROR: Could not obtain PackageInfo for package name: "
-                        + packageName);
-                continue;
-            }
+        try {
+            return pm.getInstallSourceInfo(packageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+            return null;
         }
     }
 
+    // TODO(b/259349011): Need to be more robust against package name mismatch in the filename
+    private String getOriginalApexPreinstalledLocation(String packageName,
+                                                   String currentInstalledLocation) {
+        if (currentInstalledLocation.contains("/decompressed/")) {
+            String resultPath = "system/apex" + packageName + ".capex";
+            File f = new File(resultPath);
+            if (f.exists()) {
+                return resultPath;
+            }
+            return "/system/apex/" + packageName + ".next.capex";
+        }
+        return "/system/apex" + packageName + "apex";
+    }
+
+    /**
+     * Wrapper method to call into IBICS to get a list of all newly installed MBAs.
+     *
+     * We expect IBICS to maintain an accurate list of installed MBAs, and we merely make use of
+     * the results within this service. This means we do not further check whether the
+     * apps in the returned slice is still installed or not, esp. considering that preloaded apps
+     * could be updated, or post-setup installed apps *might* be deleted in real time.
+     *
+     * Note that we do *not* cache the results from IBICS because of the more dynamic nature of
+     * MBAs v.s. other binaries that we measure.
+     *
+     * @return a list of preloaded apps + dynamically installed apps that fit the definition of MBA.
+     */
+    @NonNull
+    private List<PackageInfo> getNewlyInstalledMbas() {
+        List<PackageInfo> result = new ArrayList<>();
+        IBackgroundInstallControlService iBics = IBackgroundInstallControlService.Stub.asInterface(
+                ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE));
+        if (iBics == null) {
+            Slog.e(TAG,
+                    "Failed to obtain an IBinder instance of IBackgroundInstallControlService");
+            return result;
+        }
+        ParceledListSlice<PackageInfo> slice;
+        try {
+            slice = iBics.getBackgroundInstalledPackages(
+                    PackageManager.MATCH_ALL | PackageManager.GET_SIGNING_CERTIFICATES,
+                    UserHandle.USER_SYSTEM);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to get a list of MBAs.", e);
+            return result;
+        }
+        return slice.getList();
+    }
 }
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index c713a41..551ffff 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -38,8 +38,6 @@
 import android.util.AtomicFile;
 import android.util.EventLog;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -47,6 +45,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.am.DropboxRateLimiter;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/ConsumerIrService.java b/services/core/java/com/android/server/ConsumerIrService.java
index a9bdf06..ee6d808 100644
--- a/services/core/java/com/android/server/ConsumerIrService.java
+++ b/services/core/java/com/android/server/ConsumerIrService.java
@@ -92,6 +92,8 @@
     @Override
     @EnforcePermission(TRANSMIT_IR)
     public void transmit(String packageName, int carrierFrequency, int[] pattern) {
+        super.transmit_enforcePermission();
+
         long totalXmitTime = 0;
 
         for (int slice : pattern) {
@@ -128,6 +130,8 @@
     @Override
     @EnforcePermission(TRANSMIT_IR)
     public int[] getCarrierFrequencies() {
+        super.getCarrierFrequencies_enforcePermission();
+
         throwIfNoIrEmitter();
 
         synchronized(mHalLock) {
diff --git a/services/core/java/com/android/server/ContextHubSystemService.java b/services/core/java/com/android/server/ContextHubSystemService.java
index 96ff900..e6e83e0 100644
--- a/services/core/java/com/android/server/ContextHubSystemService.java
+++ b/services/core/java/com/android/server/ContextHubSystemService.java
@@ -23,6 +23,7 @@
 
 import com.android.internal.util.ConcurrentUtils;
 import com.android.server.location.contexthub.ContextHubService;
+import com.android.server.location.contexthub.IContextHubWrapper;
 
 import java.util.concurrent.Future;
 
@@ -35,7 +36,8 @@
     public ContextHubSystemService(Context context) {
         super(context);
         mInit = SystemServerInitThreadPool.submit(() -> {
-            mContextHubService = new ContextHubService(context);
+            mContextHubService = new ContextHubService(context,
+                    IContextHubWrapper.getContextHubWrapper());
         }, "Init ContextHubSystemService");
     }
 
diff --git a/services/core/java/com/android/server/DynamicSystemService.java b/services/core/java/com/android/server/DynamicSystemService.java
index ce0e69c..27215b2 100644
--- a/services/core/java/com/android/server/DynamicSystemService.java
+++ b/services/core/java/com/android/server/DynamicSystemService.java
@@ -77,6 +77,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean startInstallation(String dsuSlot) throws RemoteException {
+        super.startInstallation_enforcePermission();
+
         IGsiService service = getGsiService();
         mGsiService = service;
         // priority from high to low: sysprop -> sdcard -> /data
@@ -124,6 +126,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public int createPartition(String name, long size, boolean readOnly) throws RemoteException {
+        super.createPartition_enforcePermission();
+
         IGsiService service = getGsiService();
         int status = service.createPartition(name, size, readOnly);
         if (status != IGsiService.INSTALL_OK) {
@@ -135,6 +139,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean closePartition() throws RemoteException {
+        super.closePartition_enforcePermission();
+
         IGsiService service = getGsiService();
         if (service.closePartition() != 0) {
             Slog.i(TAG, "Partition installation completes with error");
@@ -146,6 +152,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean finishInstallation() throws RemoteException {
+        super.finishInstallation_enforcePermission();
+
         IGsiService service = getGsiService();
         if (service.closeInstall() != 0) {
             Slog.i(TAG, "Failed to finish installation");
@@ -157,12 +165,16 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public GsiProgress getInstallationProgress() throws RemoteException {
+        super.getInstallationProgress_enforcePermission();
+
         return getGsiService().getInstallProgress();
     }
 
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean abort() throws RemoteException {
+        super.abort_enforcePermission();
+
         return getGsiService().cancelGsiInstall();
     }
 
@@ -183,12 +195,16 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean isEnabled() throws RemoteException {
+        super.isEnabled_enforcePermission();
+
         return getGsiService().isGsiEnabled();
     }
 
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean remove() throws RemoteException {
+        super.remove_enforcePermission();
+
         try {
             GsiServiceCallback callback = new GsiServiceCallback();
             synchronized (callback) {
@@ -205,6 +221,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean setEnable(boolean enable, boolean oneShot) throws RemoteException {
+        super.setEnable_enforcePermission();
+
         IGsiService gsiService = getGsiService();
         if (enable) {
             try {
@@ -229,6 +247,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean setAshmem(ParcelFileDescriptor ashmem, long size) {
+        super.setAshmem_enforcePermission();
+
         try {
             return getGsiService().setGsiAshmem(ashmem, size);
         } catch (RemoteException e) {
@@ -239,6 +259,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean submitFromAshmem(long size) {
+        super.submitFromAshmem_enforcePermission();
+
         try {
             return getGsiService().commitGsiChunkFromAshmem(size);
         } catch (RemoteException e) {
@@ -249,6 +271,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public boolean getAvbPublicKey(AvbPublicKey dst) {
+        super.getAvbPublicKey_enforcePermission();
+
         try {
             return getGsiService().getAvbPublicKey(dst) == 0;
         } catch (RemoteException e) {
@@ -259,6 +283,8 @@
     @Override
     @EnforcePermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM)
     public long suggestScratchSize() throws RemoteException {
+        super.suggestScratchSize_enforcePermission();
+
         return getGsiService().suggestScratchSize();
     }
 }
diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java
index e529010..7d2e276 100644
--- a/services/core/java/com/android/server/GestureLauncherService.java
+++ b/services/core/java/com/android/server/GestureLauncherService.java
@@ -466,7 +466,8 @@
     public static boolean isEmergencyGestureSettingEnabled(Context context, int userId) {
         return isEmergencyGestureEnabled(context.getResources())
                 && Settings.Secure.getIntForUser(context.getContentResolver(),
-                Settings.Secure.EMERGENCY_GESTURE_ENABLED, 1, userId) != 0;
+                Settings.Secure.EMERGENCY_GESTURE_ENABLED,
+                isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0;
     }
 
     /**
@@ -513,6 +514,11 @@
         return resources.getBoolean(com.android.internal.R.bool.config_emergencyGestureEnabled);
     }
 
+    private static boolean isDefaultEmergencyGestureEnabled(Resources resources) {
+        return resources.getBoolean(
+                com.android.internal.R.bool.config_defaultEmergencyGestureEnabled);
+    }
+
     /**
      * Whether GestureLauncherService should be enabled according to system properties.
      */
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index d29e25c..5d54b6c 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -867,6 +867,8 @@
     public void shutdown() {
         // TODO: remove from aidl if nobody calls externally
 
+        super.shutdown_enforcePermission();
+
         Slog.i(TAG, "Shutting down");
     }
 
@@ -1207,6 +1209,8 @@
     @Override
     public boolean setDataSaverModeEnabled(boolean enable) {
 
+        super.setDataSaverModeEnabled_enforcePermission();
+
         if (DBG) Log.d(TAG, "setDataSaverMode: " + enable);
         synchronized (mQuotaLock) {
             if (mDataSaverMode == enable) {
@@ -1744,6 +1748,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.OBSERVE_NETWORK_POLICY)
     @Override
     public boolean isNetworkRestricted(int uid) {
+        super.isNetworkRestricted_enforcePermission();
+
         return isNetworkRestrictedInternal(uid);
     }
 
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 960922e..92889c0 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -40,8 +40,6 @@
 import android.util.LongArrayQueue;
 import android.util.MathUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -49,6 +47,8 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index c1c9fbb..b56d1fc 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -231,7 +231,12 @@
             String namespaceToReset = namespaceIt.next();
             Properties properties = new Properties.Builder(namespaceToReset).build();
             try {
-                DeviceConfig.setProperties(properties);
+                if (!DeviceConfig.setProperties(properties)) {
+                    logCriticalInfo(Log.ERROR, "Failed to clear properties under "
+                            + namespaceToReset
+                            + ". Running `device_config get_sync_disabled_for_tests` will confirm"
+                            + " if config-bulk-update is enabled.");
+                }
             } catch (DeviceConfig.BadConfigException exception) {
                 logCriticalInfo(Log.WARN, "namespace " + namespaceToReset
                         + " is already banned, skip reset.");
diff --git a/services/core/java/com/android/server/SerialService.java b/services/core/java/com/android/server/SerialService.java
index e915fa1..ff903a0 100644
--- a/services/core/java/com/android/server/SerialService.java
+++ b/services/core/java/com/android/server/SerialService.java
@@ -37,6 +37,8 @@
 
     @EnforcePermission(android.Manifest.permission.SERIAL_PORT)
     public String[] getSerialPorts() {
+        super.getSerialPorts_enforcePermission();
+
         ArrayList<String> ports = new ArrayList<String>();
         for (int i = 0; i < mSerialPorts.length; i++) {
             String path = mSerialPorts[i];
@@ -51,6 +53,8 @@
 
     @EnforcePermission(android.Manifest.permission.SERIAL_PORT)
     public ParcelFileDescriptor openSerialPort(String path) {
+        super.openSerialPort_enforcePermission();
+
         for (int i = 0; i < mSerialPorts.length; i++) {
             if (mSerialPorts[i].equals(path)) {
                 return native_open(path);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 0cf7915..6b6351f 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -131,8 +131,6 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -147,6 +145,8 @@
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Installer;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.storage.AppFuseBridge;
@@ -802,12 +802,20 @@
                     break;
                 }
                 case H_CLOUD_MEDIA_PROVIDER_CHANGED: {
-                    final Object listener = msg.obj;
-                    if (listener instanceof StorageManagerInternal.CloudProviderChangeListener) {
-                        notifyCloudMediaProviderChangedAsync(
-                                (StorageManagerInternal.CloudProviderChangeListener) listener);
+                    // We send this message in two cases:
+                    // 1. After the cloud provider has been set/updated for a user.
+                    //    In this case Message's #arg1 is set to UserId, and #obj is set to the
+                    //    authority of the new cloud provider.
+                    // 2. After a new CloudProviderChangeListener is registered.
+                    //    In this case Message's #obj is set to the CloudProviderChangeListener.
+                    if (msg.obj instanceof StorageManagerInternal.CloudProviderChangeListener) {
+                        final StorageManagerInternal.CloudProviderChangeListener listener =
+                                (StorageManagerInternal.CloudProviderChangeListener) msg.obj;
+                        notifyCloudMediaProviderChangedAsync(listener);
                     } else {
-                        onCloudMediaProviderChangedAsync(msg.arg1);
+                        final int userId = msg.arg1;
+                        final String authority = (String) msg.obj;
+                        onCloudMediaProviderChangedAsync(userId, authority);
                     }
                     break;
                 }
@@ -1251,6 +1259,8 @@
     // Binder entry point for kicking off an immediate fstrim
     @Override
     public void runMaintenance() {
+        super.runMaintenance_enforcePermission();
+
         runIdleMaintenance(null);
     }
 
@@ -1684,17 +1694,15 @@
             @NonNull StorageManagerInternal.CloudProviderChangeListener listener) {
         synchronized (mCloudMediaProviders) {
             for (int i = mCloudMediaProviders.size() - 1; i >= 0; --i) {
-                listener.onCloudProviderChanged(
-                        mCloudMediaProviders.keyAt(i), mCloudMediaProviders.valueAt(i));
+                final int userId = mCloudMediaProviders.keyAt(i);
+                final String authority = mCloudMediaProviders.valueAt(i);
+                listener.onCloudProviderChanged(userId, authority);
             }
         }
     }
 
-    private void onCloudMediaProviderChangedAsync(int userId) {
-        final String authority;
-        synchronized (mCloudMediaProviders) {
-            authority = mCloudMediaProviders.get(userId);
-        }
+    private void onCloudMediaProviderChangedAsync(
+            @UserIdInt int userId, @Nullable String authority) {
         for (StorageManagerInternal.CloudProviderChangeListener listener :
                 mStorageManagerInternal.mCloudProviderChangeListeners) {
             listener.onCloudProviderChanged(userId, authority);
@@ -2167,6 +2175,8 @@
     @Override
     public void shutdown(final IStorageShutdownObserver observer) {
 
+        super.shutdown_enforcePermission();
+
         Slog.i(TAG, "Shutting down");
         mHandler.obtainMessage(H_SHUTDOWN, observer).sendToTarget();
     }
@@ -2175,6 +2185,8 @@
     @Override
     public void mount(String volId) {
 
+        super.mount_enforcePermission();
+
         final VolumeInfo vol = findVolumeByIdOrThrow(volId);
         if (isMountDisallowed(vol)) {
             throw new SecurityException("Mounting " + volId + " restricted by policy");
@@ -2243,6 +2255,8 @@
     @Override
     public void unmount(String volId) {
 
+        super.unmount_enforcePermission();
+
         final VolumeInfo vol = findVolumeByIdOrThrow(volId);
         unmount(vol);
     }
@@ -2267,6 +2281,8 @@
     @Override
     public void format(String volId) {
 
+        super.format_enforcePermission();
+
         final VolumeInfo vol = findVolumeByIdOrThrow(volId);
         final String fsUuid = vol.fsUuid;
         try {
@@ -2286,6 +2302,8 @@
     @Override
     public void benchmark(String volId, IVoldTaskListener listener) {
 
+        super.benchmark_enforcePermission();
+
         try {
             mVold.benchmark(volId, new IVoldTaskListener.Stub() {
                 @Override
@@ -2325,6 +2343,8 @@
     @Override
     public void partitionPublic(String diskId) {
 
+        super.partitionPublic_enforcePermission();
+
         final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);
         try {
             mVold.partition(diskId, IVold.PARTITION_TYPE_PUBLIC, -1);
@@ -2337,6 +2357,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
     @Override
     public void partitionPrivate(String diskId) {
+        super.partitionPrivate_enforcePermission();
+
         enforceAdminUser();
 
         final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);
@@ -2351,6 +2373,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
     @Override
     public void partitionMixed(String diskId, int ratio) {
+        super.partitionMixed_enforcePermission();
+
         enforceAdminUser();
 
         final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);
@@ -2366,6 +2390,8 @@
     @Override
     public void setVolumeNickname(String fsUuid, String nickname) {
 
+        super.setVolumeNickname_enforcePermission();
+
         Objects.requireNonNull(fsUuid);
         synchronized (mLock) {
             final VolumeRecord rec = mRecords.get(fsUuid);
@@ -2379,6 +2405,8 @@
     @Override
     public void setVolumeUserFlags(String fsUuid, int flags, int mask) {
 
+        super.setVolumeUserFlags_enforcePermission();
+
         Objects.requireNonNull(fsUuid);
         synchronized (mLock) {
             final VolumeRecord rec = mRecords.get(fsUuid);
@@ -2392,6 +2420,8 @@
     @Override
     public void forgetVolume(String fsUuid) {
 
+        super.forgetVolume_enforcePermission();
+
         Objects.requireNonNull(fsUuid);
 
         synchronized (mLock) {
@@ -2416,6 +2446,8 @@
     @Override
     public void forgetAllVolumes() {
 
+        super.forgetAllVolumes_enforcePermission();
+
         synchronized (mLock) {
             for (int i = 0; i < mRecords.size(); i++) {
                 final String fsUuid = mRecords.keyAt(i);
@@ -2448,6 +2480,8 @@
     @Override
     public void fstrim(int flags, IVoldTaskListener listener) {
 
+        super.fstrim_enforcePermission();
+
         try {
             // Block based checkpoint process runs fstrim. So, if checkpoint is in progress
             // (first boot after OTA), We skip idle maintenance and make sure the last
@@ -2742,6 +2776,8 @@
     @Override
     public void setDebugFlags(int flags, int mask) {
 
+        super.setDebugFlags_enforcePermission();
+
         if ((mask & (StorageManager.DEBUG_ADOPTABLE_FORCE_ON
                 | StorageManager.DEBUG_ADOPTABLE_FORCE_OFF)) != 0) {
             final String value;
@@ -2812,6 +2848,8 @@
     @Override
     public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {
 
+        super.setPrimaryStorageUuid_enforcePermission();
+
         final VolumeInfo from;
         final VolumeInfo to;
 
@@ -3020,6 +3058,8 @@
      */
     @Override
     public boolean needsCheckpoint() throws RemoteException {
+        super.needsCheckpoint_enforcePermission();
+
         return mVold.needsCheckpoint();
     }
 
@@ -3040,6 +3080,8 @@
     @Override
     public void createUserKey(int userId, int serialNumber, boolean ephemeral) {
 
+        super.createUserKey_enforcePermission();
+
         try {
             mVold.createUserKey(userId, serialNumber, ephemeral);
             // New keys are always unlocked.
@@ -3055,6 +3097,8 @@
     @Override
     public void destroyUserKey(int userId) {
 
+        super.destroyUserKey_enforcePermission();
+
         try {
             mVold.destroyUserKey(userId);
             // Destroying a key also locks it.
@@ -3070,6 +3114,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
     public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException {
+        super.setUserKeyProtection_enforcePermission();
+
         mVold.setUserKeyProtection(userId, HexDump.toHexString(secret));
     }
 
@@ -3078,6 +3124,8 @@
     @Override
     public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret)
         throws RemoteException {
+        super.unlockUserKey_enforcePermission();
+
         if (StorageManager.isFileEncrypted()) {
             mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret));
         }
@@ -3090,6 +3138,8 @@
     @Override
     public void lockUserKey(int userId) {
         //  Do not lock user 0 data for headless system user
+        super.lockUserKey_enforcePermission();
+
         if (userId == UserHandle.USER_SYSTEM
                 && UserManager.isHeadlessSystemUserMode()) {
             throw new IllegalArgumentException("Headless system user data cannot be locked..");
@@ -3153,6 +3203,8 @@
     @Override
     public void prepareUserStorage(String volumeUuid, int userId, int serialNumber, int flags) {
 
+        super.prepareUserStorage_enforcePermission();
+
         try {
             prepareUserStorageInternal(volumeUuid, userId, serialNumber, flags);
         } catch (Exception e) {
@@ -3196,6 +3248,8 @@
     @Override
     public void destroyUserStorage(String volumeUuid, int userId, int flags) {
 
+        super.destroyUserStorage_enforcePermission();
+
         try {
             mVold.destroyUserStorage(volumeUuid, userId, flags);
         } catch (Exception e) {
@@ -3584,6 +3638,13 @@
         final boolean includeSharedProfile =
                 (flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0;
 
+        // When the caller is the app actually hosting external storage, we
+        // should never attempt to augment the actual storage volume state,
+        // otherwise we risk confusing it with race conditions as users go
+        // through various unlocked states
+        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
+                mMediaStoreAuthorityAppId);
+
         // Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile
         if (includeSharedProfile) {
             try {
@@ -3596,8 +3657,13 @@
                 // Checking first entry in packagesFromUid is enough as using "sharedUserId"
                 // mechanism is rare and discouraged. Also, Apps that share same UID share the same
                 // permissions.
-                if (!mStorageManagerInternal.hasExternalStorageAccess(callingUid,
-                        packagesFromUid[0])) {
+                // Allowing Media Provider is an exception, Media Provider process should be allowed
+                // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access.
+                // Note that ordinarily Media provider process has the above permission, but if they
+                // are revoked, Storage Volume(s) should still be returned.
+                if (!callerIsMediaStore
+                        && !mStorageManagerInternal.hasExternalStorageAccess(callingUid,
+                                packagesFromUid[0])) {
                     throw new SecurityException("Only File Manager Apps permitted");
                 }
             } catch (RemoteException re) {
@@ -3610,13 +3676,6 @@
         // point
         final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);
 
-        // When the caller is the app actually hosting external storage, we
-        // should never attempt to augment the actual storage volume state,
-        // otherwise we risk confusing it with race conditions as users go
-        // through various unlocked states
-        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
-                mMediaStoreAuthorityAppId);
-
         final boolean userIsDemo;
         final boolean userKeyUnlocked;
         final boolean storagePermission;
@@ -4242,6 +4301,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_MEDIA_STORAGE)
     @Override
     public int getExternalStorageMountMode(int uid, String packageName) {
+        super.getExternalStorageMountMode_enforcePermission();
+
         return mStorageManagerInternal.getExternalStorageMountMode(uid, packageName);
     }
 
@@ -4776,7 +4837,7 @@
         public void registerCloudProviderChangeListener(
                 @NonNull StorageManagerInternal.CloudProviderChangeListener listener) {
             mCloudProviderChangeListeners.add(listener);
-            mHandler.obtainMessage(H_CLOUD_MEDIA_PROVIDER_CHANGED, listener);
+            mHandler.obtainMessage(H_CLOUD_MEDIA_PROVIDER_CHANGED, listener).sendToTarget();
         }
     }
 }
diff --git a/services/core/java/com/android/server/SystemServerInitThreadPool.java b/services/core/java/com/android/server/SystemServerInitThreadPool.java
index 9323d95..7f24c52 100644
--- a/services/core/java/com/android/server/SystemServerInitThreadPool.java
+++ b/services/core/java/com/android/server/SystemServerInitThreadPool.java
@@ -32,6 +32,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -192,10 +193,12 @@
     private static void dumpStackTraces() {
         final ArrayList<Integer> pids = new ArrayList<>();
         pids.add(Process.myPid());
-        ActivityManagerService.dumpStackTraces(pids, /* processCpuTracker= */null,
-                /* lastPids= */null, Watchdog.getInterestingNativePids(),
+        ActivityManagerService.dumpStackTraces(pids,
+                /* processCpuTracker= */null, /* lastPids= */null,
+                CompletableFuture.completedFuture(Watchdog.getInterestingNativePids()),
                 /* logExceptionCreatingFile= */null, /* subject= */null,
-                /* criticalEventSection= */null, /* latencyTracker= */null);
+                /* criticalEventSection= */null, Runnable::run,
+                /* latencyTracker= */null);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java
index 1a8cf0b..a05b84b 100644
--- a/services/core/java/com/android/server/SystemServiceManager.java
+++ b/services/core/java/com/android/server/SystemServiceManager.java
@@ -75,13 +75,13 @@
     // Constants used on onUser(...)
     // NOTE: do not change their values, as they're used on Trace calls and changes might break
     // performance tests that rely on them.
-    private static final String USER_STARTING = "Start"; // Logged as onStartUser
-    private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUnlockingUser
-    private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUnlockedUser
-    private static final String USER_SWITCHING = "Switch"; // Logged as onSwitchUser
-    private static final String USER_STOPPING = "Stop"; // Logged as onStopUser
-    private static final String USER_STOPPED = "Cleanup"; // Logged as onCleanupUser
-    private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onCompletedEventUser
+    private static final String USER_STARTING = "Start"; // Logged as onUserStarting()
+    private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUserUnlocking()
+    private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUserUnlocked()
+    private static final String USER_SWITCHING = "Switch"; // Logged as onUserSwitching()
+    private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping()
+    private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped()
+    private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent()
 
     // The default number of threads to use if lifecycle thread pool is enabled.
     private static final int DEFAULT_MAX_USER_POOL_THREADS = 3;
@@ -351,13 +351,24 @@
      * Starts the given user.
      */
     public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) {
-        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
-
         final TargetUser targetUser = newTargetUser(userId);
         synchronized (mTargetUsers) {
+            // On Automotive / Headless System User Mode, the system user will be started twice:
+            // - Once by some external or local service that switches the system user to
+            //   the background.
+            // - Once by the ActivityManagerService, when the system is marked ready.
+            // These two events are not synchronized and the order of execution is
+            // non-deterministic. To avoid starting the system user twice, verify whether
+            // the system user has already been started by checking the mTargetUsers.
+            // TODO(b/242195409): this workaround shouldn't be necessary once we move
+            // the headless-user start logic to UserManager-land.
+            if (userId == UserHandle.USER_SYSTEM && mTargetUsers.contains(userId)) {
+                Slog.e(TAG, "Skipping starting system user twice");
+                return;
+            }
             mTargetUsers.put(userId, targetUser);
         }
-
+        EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
         onUser(t, USER_STARTING, /* prevUser= */ null, targetUser);
     }
 
@@ -456,13 +467,12 @@
         TargetUser targetUser = getTargetUser(userId);
         Preconditions.checkState(targetUser != null, "No TargetUser for " + userId);
 
-        onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null,
-                targetUser);
+        onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, targetUser);
     }
 
     private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat,
             @Nullable TargetUser prevUser, @NonNull TargetUser curUser) {
-        onUser(t, onWhat, prevUser, curUser, /* completedEventType=*/ null);
+        onUser(t, onWhat, prevUser, curUser, /* completedEventType= */ null);
     }
 
     private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat,
diff --git a/services/core/java/com/android/server/SystemUpdateManagerService.java b/services/core/java/com/android/server/SystemUpdateManagerService.java
index fcba9b5..811a780 100644
--- a/services/core/java/com/android/server/SystemUpdateManagerService.java
+++ b/services/core/java/com/android/server/SystemUpdateManagerService.java
@@ -38,12 +38,12 @@
 import android.provider.Settings;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index f7833b0..2652ebe 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2581,33 +2581,39 @@
         if (!checkNotifyPermission("notifyBarringInfo()")) {
             return;
         }
-        if (barringInfo == null) {
-            log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
-            mBarringInfo.set(phoneId, new BarringInfo());
+        if (!validatePhoneId(phoneId)) {
+            loge("Received invalid phoneId for BarringInfo = " + phoneId);
             return;
         }
 
         synchronized (mRecords) {
-            if (validatePhoneId(phoneId)) {
-                mBarringInfo.set(phoneId, barringInfo);
-                // Barring info is non-null
-                BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
-                if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
-                for (Record r : mRecords) {
-                    if (r.matchTelephonyCallbackEvent(
-                            TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
-                            && idMatch(r, subId, phoneId)) {
-                        try {
-                            if (DBG_LOC) {
-                                log("notifyBarringInfo: mBarringInfo="
-                                        + barringInfo + " r=" + r);
-                            }
-                            r.callback.onBarringInfoChanged(
-                                    checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
-                                        ? barringInfo : biNoLocation);
-                        } catch (RemoteException ex) {
-                            mRemoveList.add(r.binder);
+            if (barringInfo == null) {
+                loge("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
+                mBarringInfo.set(phoneId, new BarringInfo());
+                return;
+            }
+            if (barringInfo.equals(mBarringInfo.get(phoneId))) {
+                if (VDBG) log("Ignoring duplicate barring info.");
+                return;
+            }
+            mBarringInfo.set(phoneId, barringInfo);
+            // Barring info is non-null
+            BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
+            if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        if (DBG_LOC) {
+                            log("notifyBarringInfo: mBarringInfo="
+                                    + barringInfo + " r=" + r);
                         }
+                        r.callback.onBarringInfoChanged(
+                                checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
+                                    ? barringInfo : biNoLocation);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 202f4775..5d46de3 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -152,6 +152,8 @@
 
     // flag set by resource, whether to start dream immediately upon docking even if unlocked.
     private boolean mStartDreamImmediatelyOnDock = true;
+    // flag set by resource, whether to disable dreams when ambient mode suppression is enabled.
+    private boolean mDreamsDisabledByAmbientModeSuppression = false;
     // flag set by resource, whether to enable Car dock launch when starting car mode.
     private boolean mEnableCarDockLaunch = true;
     // flag set by resource, whether to lock UI mode to the default one or not.
@@ -364,6 +366,11 @@
         mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock;
     }
 
+    @VisibleForTesting
+    void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) {
+        mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression;
+    }
+
     @Override
     public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
         mCurrentUser = to.getUserIdentifier();
@@ -424,6 +431,8 @@
         final Resources res = context.getResources();
         mStartDreamImmediatelyOnDock = res.getBoolean(
                 com.android.internal.R.bool.config_startDreamImmediatelyOnDock);
+        mDreamsDisabledByAmbientModeSuppression = res.getBoolean(
+                com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
         mNightMode = res.getInteger(
                 com.android.internal.R.integer.config_defaultNightMode);
         mDefaultUiModeType = res.getInteger(
@@ -1827,10 +1836,14 @@
         // Send the new configuration.
         applyConfigurationExternallyLocked();
 
+        final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression
+                && mLocalPowerManager.isAmbientDisplaySuppressed();
+
         // If we did not start a dock app, then start dreaming if appropriate.
-        if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
-                || mWindowManager.isKeyguardShowingAndNotOccluded()
-                || !mPowerManager.isInteractive())) {
+        if (category != null && !dockAppStarted && !dreamsSuppressed && (
+                mStartDreamImmediatelyOnDock
+                        || mWindowManager.isKeyguardShowingAndNotOccluded()
+                        || !mPowerManager.isInteractive())) {
             mInjector.startDreamWhenDockedIfAppropriate(getContext());
         }
     }
diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java
index 3f1d1fe..ae50b23 100644
--- a/services/core/java/com/android/server/VpnManagerService.java
+++ b/services/core/java/com/android/server/VpnManagerService.java
@@ -186,6 +186,10 @@
         synchronized (mVpns) {
             for (int i = 0; i < mVpns.size(); i++) {
                 pw.println(mVpns.keyAt(i) + ": " + mVpns.valueAt(i).getPackage());
+                pw.increaseIndent();
+                mVpns.valueAt(i).dump(pw);
+                pw.decreaseIndent();
+                pw.println();
             }
             pw.decreaseIndent();
         }
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index b00dec0..6eeb906 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -49,7 +49,7 @@
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.ProcessCpuTracker;
@@ -75,6 +75,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -894,8 +895,9 @@
         ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(false);
         StringWriter tracesFileException = new StringWriter();
         final File stack = ActivityManagerService.dumpStackTraces(
-                pids, processCpuTracker, new SparseArray<>(), getInterestingNativePids(),
-                tracesFileException, subject, criticalEvents, /* latencyTracker= */null);
+                pids, processCpuTracker, new SparseBooleanArray(),
+                CompletableFuture.completedFuture(getInterestingNativePids()), tracesFileException,
+                subject, criticalEvents, Runnable::run, /* latencyTracker= */null);
         // Give some extra time to make sure the stack traces get written.
         // The system's been hanging for a whlie, another second or two won't hurt much.
         SystemClock.sleep(5000);
diff --git a/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java b/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
index 725bccf..12f2e10 100644
--- a/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
+++ b/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
@@ -27,8 +27,9 @@
 import android.content.res.TypedArray;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
index b379b5d..3603dcd 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
@@ -29,13 +29,13 @@
 import android.util.PackageUtils;
 import android.util.Pair;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 672ee0e..c7c2655 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -528,7 +528,7 @@
     private Map<Account, Integer> getAccountsAndVisibilityForPackage(String packageName,
             List<String> accountTypes, Integer callingUid, UserAccounts accounts) {
         if (!packageExistsForUser(packageName, accounts.userId)) {
-            Log.d(TAG, "Package not found " + packageName);
+            Log.w(TAG, "getAccountsAndVisibilityForPackage#Package not found " + packageName);
             return new LinkedHashMap<>();
         }
 
@@ -677,7 +677,7 @@
                 restoreCallingIdentity(identityToken);
             }
         } catch (NameNotFoundException e) {
-            Log.d(TAG, "Package not found " + e.getMessage());
+            Log.w(TAG, "resolveAccountVisibility#Package not found " + e.getMessage());
             return AccountManager.VISIBILITY_NOT_VISIBLE;
         }
 
@@ -756,7 +756,7 @@
             }
             return true;
         } catch (NameNotFoundException e) {
-            Log.d(TAG, "Package not found " + e.getMessage());
+            Log.w(TAG, "isPreOApplication#Package not found " + e.getMessage());
             return true;
         }
     }
@@ -840,6 +840,7 @@
                 }
 
                 if (notify) {
+                    Log.i(TAG, "Notifying visibility changed for package=" + packageName);
                     for (Entry<String, Integer> packageToVisibility : packagesToVisibility
                             .entrySet()) {
                         int oldVisibility = packageToVisibility.getValue();
@@ -850,9 +851,14 @@
                         }
                     }
                     for (String packageNameToNotify : accountRemovedReceivers) {
-                        sendAccountRemovedBroadcast(account, packageNameToNotify, accounts.userId);
+                        sendAccountRemovedBroadcast(
+                                account,
+                                packageNameToNotify,
+                                accounts.userId,
+                                /*useCase=*/"setAccountVisibility");
                     }
-                    sendAccountsChangedBroadcast(accounts.userId);
+                    sendAccountsChangedBroadcast(
+                            accounts.userId, account.type, /*useCase=*/"setAccountVisibility");
                 }
                 return true;
             }
@@ -973,6 +979,8 @@
      * @param accounts UserAccount that currently hosts the account
      */
     private void notifyPackage(String packageName, UserAccounts accounts) {
+        Log.i(TAG, "notifying package=" + packageName + " for userId=" + accounts.userId
+                +", sending broadcast of " + AccountManager.ACTION_VISIBLE_ACCOUNTS_CHANGED);
         Intent intent = new Intent(AccountManager.ACTION_VISIBLE_ACCOUNTS_CHANGED);
         intent.setPackage(packageName);
         intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -1059,13 +1067,21 @@
                 || AccountManager.PACKAGE_NAME_KEY_LEGACY_NOT_VISIBLE.equals(packageName));
     }
 
-    private void sendAccountsChangedBroadcast(int userId) {
-        Log.i(TAG, "the accounts changed, sending broadcast of "
-                + ACCOUNTS_CHANGED_INTENT.getAction());
+    private void sendAccountsChangedBroadcast(
+            int userId, String accountType, @NonNull String useCase) {
+        Objects.requireNonNull(useCase, "useCase can't be null");
+        Log.i(TAG, "the accountType= " + (accountType == null ? "" : accountType)
+                + " changed with useCase=" + useCase + " for userId=" + userId
+                + ", sending broadcast of " + ACCOUNTS_CHANGED_INTENT.getAction());
         mContext.sendBroadcastAsUser(ACCOUNTS_CHANGED_INTENT, new UserHandle(userId));
     }
 
-    private void sendAccountRemovedBroadcast(Account account, String packageName, int userId) {
+    private void sendAccountRemovedBroadcast(
+            Account account, String packageName, int userId, @NonNull String useCase) {
+        Objects.requireNonNull(useCase, "useCase can't be null");
+        Log.i(TAG, "the account with type=" + account.type + " removed while useCase="
+                + useCase + " for userId=" + userId + ", sending broadcast of "
+                + AccountManager.ACTION_ACCOUNT_REMOVED);
         Intent intent = new Intent(AccountManager.ACTION_ACCOUNT_REMOVED);
         intent.setFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         intent.setPackage(packageName);
@@ -1212,6 +1228,8 @@
                                 accountsDb.endTransaction();
                             }
                             accountDeleted = true;
+                            Log.i(TAG, "validateAccountsInternal#Deleted UserId="
+                                    + accounts.userId + ", AccountId=" + accountId);
 
                             logRecord(AccountsDb.DEBUG_ACTION_AUTHENTICATOR_REMOVE,
                                     AccountsDb.TABLE_ACCOUNTS, accountId, accounts);
@@ -1228,7 +1246,11 @@
                                 }
                             }
                             for (String packageName : accountRemovedReceivers) {
-                                sendAccountRemovedBroadcast(account, packageName, accounts.userId);
+                                sendAccountRemovedBroadcast(
+                                        account,
+                                        packageName,
+                                        accounts.userId,
+                                        /*useCase=*/"validateAccounts");
                             }
                         } else {
                             ArrayList<String> accountNames = accountNamesByType.get(account.type);
@@ -1253,7 +1275,10 @@
                     AccountManager.invalidateLocalAccountsDataCaches();
                 } finally {
                     if (accountDeleted) {
-                        sendAccountsChangedBroadcast(accounts.userId);
+                        sendAccountsChangedBroadcast(
+                                accounts.userId,
+                                /*accountType=*/"ambiguous",
+                                /*useCase=*/"validateAccounts");
                     }
                 }
             }
@@ -1845,7 +1870,7 @@
                     }
                     if (accounts.accountsDb.findAllDeAccounts().size() > 100) {
                         Log.w(TAG, "insertAccountIntoDatabase: " + account.toSafeString()
-                                + ", skipping since more than 50 accounts on device exist");
+                                + ", skipping since more than 100 accounts on device exist");
                         return false;
                     }
                     long accountId = accounts.accountsDb.insertCeAccount(account, password);
@@ -1899,7 +1924,9 @@
 
         sendNotificationAccountUpdated(account, accounts);
         // Only send LOGIN_ACCOUNTS_CHANGED when the database changed.
-        sendAccountsChangedBroadcast(accounts.userId);
+        Log.i(TAG, "callingUid=" + callingUid + ", userId=" + accounts.userId
+                + " added account");
+        sendAccountsChangedBroadcast(accounts.userId, account.type, /*useCase=*/"addAccount");
 
         logAddAccountExplicitlyMetrics(opPackageName, account.type, packageToVisibility);
         return true;
@@ -2089,6 +2116,8 @@
         final long identityToken = clearCallingIdentity();
         try {
             UserAccounts accounts = getUserAccounts(userId);
+            Log.i(TAG, "callingUid=" + callingUid + ", userId=" + accounts.userId
+                    + " performing rename account");
             Account resultingAccount = renameAccountInternal(accounts, accountToRename, newName);
             Bundle result = new Bundle();
             result.putString(AccountManager.KEY_ACCOUNT_NAME, resultingAccount.name);
@@ -2199,9 +2228,14 @@
                 }
 
                 sendNotificationAccountUpdated(resultAccount, accounts);
-                sendAccountsChangedBroadcast(accounts.userId);
+                sendAccountsChangedBroadcast(
+                        accounts.userId, accountToRename.type, /*useCase=*/"renameAccount");
                 for (String packageName : accountRemovedReceivers) {
-                    sendAccountRemovedBroadcast(accountToRename, packageName, accounts.userId);
+                    sendAccountRemovedBroadcast(
+                            accountToRename,
+                            packageName,
+                            accounts.userId,
+                            /*useCase=*/"renameAccount");
                 }
 
                 AccountManager.invalidateLocalAccountsDataCaches();
@@ -2433,9 +2467,13 @@
                     }
 
                     // Only broadcast LOGIN_ACCOUNTS_CHANGED if a change occurred.
-                    sendAccountsChangedBroadcast(accounts.userId);
+                    Log.i(TAG, "callingUid=" + callingUid + ", userId=" + accounts.userId
+                            + " removed account");
+                    sendAccountsChangedBroadcast(
+                            accounts.userId, account.type, /*useCase=*/"removeAccount");
                     for (String packageName : accountRemovedReceivers) {
-                        sendAccountRemovedBroadcast(account, packageName, accounts.userId);
+                        sendAccountRemovedBroadcast(
+                                account, packageName, accounts.userId, /*useCase=*/"removeAccount");
                     }
                     String action = userUnlocked ? AccountsDb.DEBUG_ACTION_ACCOUNT_REMOVE
                             : AccountsDb.DEBUG_ACTION_ACCOUNT_REMOVE_DE;
@@ -2709,7 +2747,9 @@
                     if (isChanged) {
                         // Send LOGIN_ACCOUNTS_CHANGED only if the something changed.
                         sendNotificationAccountUpdated(account, accounts);
-                        sendAccountsChangedBroadcast(accounts.userId);
+                        Log.i(TAG, "callingUid=" + callingUid + " changed password");
+                        sendAccountsChangedBroadcast(
+                                accounts.userId, account.type, /*useCase=*/"setPassword");
                     }
                 }
             }
@@ -3480,10 +3520,10 @@
 
                 @Override
                 protected String toDebugString(long now) {
-                    String requiredFeaturesStr = TextUtils.join(",", requiredFeatures);
                     return super.toDebugString(now) + ", startAddAccountSession" + ", accountType "
                             + accountType + ", requiredFeatures "
-                            + (requiredFeatures != null ? requiredFeaturesStr : null);
+                            + (requiredFeatures != null
+                                ? TextUtils.join(",", requiredFeatures) : "null");
                 }
             }.bind();
         } finally {
@@ -4063,7 +4103,7 @@
             int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
             return hasAccountAccess(account, packageName, uid);
         } catch (NameNotFoundException e) {
-            Log.d(TAG, "Package not found " + e.getMessage());
+            Log.w(TAG, "hasAccountAccess#Package not found " + e.getMessage());
             return false;
         }
     }
@@ -4195,7 +4235,7 @@
         }
         final long token = Binder.clearCallingIdentity();
         try {
-            AccountAndUser[] allAccounts = getAllAccounts();
+            AccountAndUser[] allAccounts = getAllAccountsForSystemProcess();
             for (int i = allAccounts.length - 1; i >= 0; i--) {
                 if (allAccounts[i].account.equals(account)) {
                     return true;
@@ -4345,10 +4385,11 @@
     /**
      * Returns accounts for all running users, ignores visibility values.
      *
+     * Should only be called by System process.
      * @hide
      */
     @NonNull
-    public AccountAndUser[] getRunningAccounts() {
+    public AccountAndUser[] getRunningAccountsForSystem() {
         final int[] runningUserIds;
         try {
             runningUserIds = ActivityManager.getService().getRunningUserIds();
@@ -4356,26 +4397,34 @@
             // Running in system_server; should never happen
             throw new RuntimeException(e);
         }
-        return getAccounts(runningUserIds);
+        return getAccountsForSystem(runningUserIds);
     }
 
     /**
      * Returns accounts for all users, ignores visibility values.
      *
+     * Should only be called by system process
+     *
      * @hide
      */
     @NonNull
-    public AccountAndUser[] getAllAccounts() {
+    public AccountAndUser[] getAllAccountsForSystemProcess() {
         final List<UserInfo> users = getUserManager().getAliveUsers();
         final int[] userIds = new int[users.size()];
         for (int i = 0; i < userIds.length; i++) {
             userIds[i] = users.get(i).id;
         }
-        return getAccounts(userIds);
+        return getAccountsForSystem(userIds);
     }
 
+    /**
+     * Returns all accounts for the given user, ignores all visibility checks.
+     * This should only be called by system process.
+     *
+     * @hide
+     */
     @NonNull
-    private AccountAndUser[] getAccounts(int[] userIds) {
+    private AccountAndUser[] getAccountsForSystem(int[] userIds) {
         final ArrayList<AccountAndUser> runningAccounts = Lists.newArrayList();
         for (int userId : userIds) {
             UserAccounts userAccounts = getUserAccounts(userId);
@@ -4384,7 +4433,7 @@
                     userAccounts,
                     null /* type */,
                     Binder.getCallingUid(),
-                    null /* packageName */,
+                    "android"/* packageName */,
                     false /* include managed not visible*/);
             for (Account account : accounts) {
                 runningAccounts.add(new AccountAndUser(account, userId));
@@ -4795,6 +4844,7 @@
 
     private abstract class Session extends IAccountAuthenticatorResponse.Stub
             implements IBinder.DeathRecipient, ServiceConnection {
+        private final Object mSessionLock = new Object();
         IAccountManagerResponse mResponse;
         final String mAccountType;
         final boolean mExpectActivityLaunch;
@@ -4985,9 +5035,11 @@
         }
 
         private void unbind() {
-            if (mAuthenticator != null) {
-                mAuthenticator = null;
-                mContext.unbindService(this);
+            synchronized (mSessionLock) {
+                if (mAuthenticator != null) {
+                    mAuthenticator = null;
+                    mContext.unbindService(this);
+                }
             }
         }
 
@@ -4997,12 +5049,14 @@
 
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
-            mAuthenticator = IAccountAuthenticator.Stub.asInterface(service);
-            try {
-                run();
-            } catch (RemoteException e) {
-                onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
-                        "remote exception");
+            synchronized (mSessionLock) {
+                mAuthenticator = IAccountAuthenticator.Stub.asInterface(service);
+                try {
+                    run();
+                } catch (RemoteException e) {
+                    onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION,
+                            "remote exception");
+                }
             }
         }
 
@@ -5355,7 +5409,7 @@
             }
         } else {
             Account[] accounts = getAccountsFromCache(userAccounts, null /* type */,
-                    Process.SYSTEM_UID, null /* packageName */, false);
+                    Process.SYSTEM_UID, "android" /* packageName */, false);
             fout.println("Accounts: " + accounts.length);
             for (Account account : accounts) {
                 fout.println("  " + account.toString());
@@ -5550,7 +5604,7 @@
                         return true;
                     }
                 } catch (PackageManager.NameNotFoundException e) {
-                    Log.d(TAG, "Package not found " + e.getMessage());
+                    Log.w(TAG, "isPrivileged#Package not found " + e.getMessage());
                 }
             }
         } finally {
@@ -6074,7 +6128,7 @@
                     }
                 }
             } catch (NameNotFoundException e) {
-                Log.d(TAG, "Package not found " + e.getMessage());
+                Log.w(TAG, "filterSharedAccounts#Package not found " + e.getMessage());
             }
             Map<Account, Integer> filtered = new LinkedHashMap<>();
             for (Map.Entry<Account, Integer> entry : unfiltered.entrySet()) {
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 04bd869..62a97dc 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -65,8 +65,6 @@
 import android.util.AtomicFile;
 import android.util.Base64;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -75,6 +73,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c677edc..5c18635 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -23,18 +23,29 @@
 import static android.app.ActivityManager.PROCESS_STATE_HEAVY_WEIGHT;
 import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DEPRECATED;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_DISABLED;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_OK;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE;
+import static android.app.ForegroundServiceTypePolicy.FGS_TYPE_POLICY_CHECK_UNKNOWN;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
+import static android.os.PowerExemptionManager.REASON_ACTIVE_DEVICE_ADMIN;
 import static android.os.PowerExemptionManager.REASON_ACTIVITY_STARTER;
 import static android.os.PowerExemptionManager.REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD;
 import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE;
 import static android.os.PowerExemptionManager.REASON_BACKGROUND_ACTIVITY_PERMISSION;
 import static android.os.PowerExemptionManager.REASON_BACKGROUND_FGS_PERMISSION;
+import static android.os.PowerExemptionManager.REASON_CARRIER_PRIVILEGED_APP;
 import static android.os.PowerExemptionManager.REASON_COMPANION_DEVICE_MANAGER;
 import static android.os.PowerExemptionManager.REASON_CURRENT_INPUT_METHOD;
 import static android.os.PowerExemptionManager.REASON_DENIED;
 import static android.os.PowerExemptionManager.REASON_DEVICE_DEMO_MODE;
 import static android.os.PowerExemptionManager.REASON_DEVICE_OWNER;
+import static android.os.PowerExemptionManager.REASON_DISALLOW_APPS_CONTROL;
+import static android.os.PowerExemptionManager.REASON_DPO_PROTECTED_APP;
 import static android.os.PowerExemptionManager.REASON_FGS_BINDING;
 import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_ACTIVITY_PERMISSION;
 import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_FGS_PERMISSION;
@@ -45,10 +56,12 @@
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI;
 import static android.os.PowerExemptionManager.REASON_PROC_STATE_TOP;
 import static android.os.PowerExemptionManager.REASON_PROFILE_OWNER;
+import static android.os.PowerExemptionManager.REASON_ROLE_EMERGENCY;
 import static android.os.PowerExemptionManager.REASON_SERVICE_LAUNCH;
 import static android.os.PowerExemptionManager.REASON_START_ACTIVITY_FLAG;
 import static android.os.PowerExemptionManager.REASON_SYSTEM_ALERT_WINDOW_PERMISSION;
 import static android.os.PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED;
+import static android.os.PowerExemptionManager.REASON_SYSTEM_MODULE;
 import static android.os.PowerExemptionManager.REASON_SYSTEM_UID;
 import static android.os.PowerExemptionManager.REASON_TEMP_ALLOWED_WHILE_IN_USE;
 import static android.os.PowerExemptionManager.REASON_UID_VISIBLE;
@@ -63,6 +76,9 @@
 import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
 
 import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH;
+import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED;
+import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER;
+import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
 import static com.android.internal.util.FrameworkStatsLog.SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT;
@@ -94,6 +110,11 @@
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.ForegroundServiceStartNotAllowedException;
+import android.app.ForegroundServiceTypeNotAllowedException;
+import android.app.ForegroundServiceTypePolicy;
+import android.app.ForegroundServiceTypePolicy.ForegroundServicePolicyCheckCode;
+import android.app.ForegroundServiceTypePolicy.ForegroundServiceTypePermission;
+import android.app.ForegroundServiceTypePolicy.ForegroundServiceTypePolicyInfo;
 import android.app.IApplicationThread;
 import android.app.IForegroundServiceObserver;
 import android.app.IServiceConnection;
@@ -116,12 +137,14 @@
 import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.pm.ServiceInfo.ForegroundServiceType;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
@@ -287,6 +310,12 @@
     final ArrayList<ServiceRecord> mPendingFgsNotifications = new ArrayList<>();
 
     /**
+     * Map of ForegroundServiceDelegation to the delegation ServiceRecord. The delegation
+     * ServiceRecord has flag isFgsDelegate set to true.
+     */
+    final ArrayMap<ForegroundServiceDelegation, ServiceRecord> mFgsDelegations = new ArrayMap<>();
+
+    /**
      * Whether there is a rate limit that suppresses immediate re-deferral of new FGS
      * notifications from each app.  On by default, disabled only by shell command for
      * test-suite purposes.  To disable the behavior more generally, use the usual
@@ -539,7 +568,7 @@
                     try {
                         final ServiceRecord.StartItem si = r.pendingStarts.get(0);
                         startServiceInnerLocked(this, si.intent, r, false, true, si.callingId,
-                                r.startRequested);
+                                si.mCallingProcessName, r.startRequested);
                     } catch (TransactionTooLargeException e) {
                         // Ignore, nobody upstack cares.
                     }
@@ -576,6 +605,7 @@
         getAppStateTracker().addBackgroundRestrictedAppListener(new BackgroundRestrictedListener());
         mAppWidgetManagerInternal = LocalServices.getService(AppWidgetManagerInternal.class);
         setAllowListWhileInUsePermissionInFgs();
+        initSystemExemptedFgsTypePermission();
     }
 
     private AppStateTracker getAppStateTracker() {
@@ -724,7 +754,7 @@
 
         ServiceRecord r = res.record;
         setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, r, userId,
-                allowBackgroundActivityStarts);
+                allowBackgroundActivityStarts, false /* isBindService */);
 
         if (!mAm.mUserController.exists(r.userId)) {
             Slog.w(TAG, "Trying to start service with non-existent user! " + r.userId);
@@ -757,8 +787,8 @@
                 Slog.w(TAG, msg);
                 showFgsBgRestrictedNotificationLocked(r);
                 logFGSStateChangeLocked(r,
-                        FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
-                        0, FGS_STOP_REASON_UNKNOWN);
+                        FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
+                        0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN);
                 if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID, callingUid)) {
                     throw new ForegroundServiceStartNotAllowedException(msg);
                 }
@@ -861,8 +891,10 @@
         // alias component name to the client, not the "target" component name, which is
         // what realResult contains.
         final ComponentName realResult =
-                startServiceInnerLocked(r, service, callingUid, callingPid, fgRequired, callerFg,
-                allowBackgroundActivityStarts, backgroundActivityStartsToken);
+                startServiceInnerLocked(r, service, callingUid, callingPid,
+                        getCallingProcessNameLocked(callingUid, callingPid, callingPackage),
+                        fgRequired, callerFg, allowBackgroundActivityStarts,
+                        backgroundActivityStartsToken);
         if (res.aliasComponent != null
                 && !realResult.getPackageName().startsWith("!")
                 && !realResult.getPackageName().startsWith("?")) {
@@ -872,10 +904,18 @@
         }
     }
 
+    private String getCallingProcessNameLocked(int callingUid, int callingPid,
+            String callingPackage) {
+        synchronized (mAm.mPidsSelfLocked) {
+            final ProcessRecord callingApp = mAm.mPidsSelfLocked.get(callingPid);
+            return callingApp != null ? callingApp.processName : callingPackage;
+        }
+    }
+
     private ComponentName startServiceInnerLocked(ServiceRecord r, Intent service,
-            int callingUid, int callingPid, boolean fgRequired, boolean callerFg,
-            boolean allowBackgroundActivityStarts, @Nullable IBinder backgroundActivityStartsToken)
-            throws TransactionTooLargeException {
+            int callingUid, int callingPid, String callingProcessName, boolean fgRequired,
+            boolean callerFg, boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken) throws TransactionTooLargeException {
         NeededUriGrants neededGrants = mAm.mUgmInternal.checkGrantUriPermissionFromIntent(
                 service, callingUid, r.packageName, r.userId);
         if (unscheduleServiceRestartLocked(r, callingUid, false)) {
@@ -887,7 +927,7 @@
         r.delayedStop = false;
         r.fgRequired = fgRequired;
         r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
-                service, neededGrants, callingUid));
+                service, neededGrants, callingUid, callingProcessName));
 
         if (fgRequired) {
             // We are now effectively running a foreground service.
@@ -972,7 +1012,7 @@
             r.allowBgActivityStartsOnServiceStart(backgroundActivityStartsToken);
         }
         ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting,
-                callingUid, wasStartRequested);
+                callingUid, callingProcessName, wasStartRequested);
         return cmp;
     }
 
@@ -1090,6 +1130,8 @@
             curPendingBringups = new ArrayList<>();
             mPendingBringups.put(s, curPendingBringups);
         }
+        final String callingProcessName = getCallingProcessNameLocked(
+                callingUid, callingPid, callingPackage);
         curPendingBringups.add(new Runnable() {
             @Override
             public void run() {
@@ -1122,8 +1164,8 @@
                     } else { // Starting a service
                         try {
                             startServiceInnerLocked(s, serviceIntent, callingUid, callingPid,
-                                    fgRequired, callerFg, allowBackgroundActivityStarts,
-                                    backgroundActivityStartsToken);
+                                    callingProcessName, fgRequired, callerFg,
+                                    allowBackgroundActivityStarts, backgroundActivityStartsToken);
                         } catch (TransactionTooLargeException e) {
                             /* ignore - local call */
                         }
@@ -1168,8 +1210,8 @@
     }
 
     ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r,
-            boolean callerFg, boolean addToStarting, int callingUid, boolean wasStartRequested)
-            throws TransactionTooLargeException {
+            boolean callerFg, boolean addToStarting, int callingUid, String callingProcessName,
+            boolean wasStartRequested) throws TransactionTooLargeException {
         synchronized (mAm.mProcessStats.mLock) {
             final ServiceState stracker = r.getTracker();
             if (stracker != null) {
@@ -1197,13 +1239,14 @@
         }
 
         FrameworkStatsLog.write(SERVICE_REQUEST_EVENT_REPORTED, uid, callingUid,
-                ActivityManagerService.getShortAction(service.getAction()),
+                service.getAction(),
                 SERVICE_REQUEST_EVENT_REPORTED__REQUEST_TYPE__START, false,
                 r.app == null || r.app.getThread() == null
                 ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD
                 : (wasStartRequested || !r.getConnections().isEmpty()
                 ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT
-                : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM));
+                : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM),
+                getShortProcessNameForStats(callingUid, callingProcessName));
 
         if (r.startRequested && addToStarting) {
             boolean first = smap.mStartingBackground.size() == 0;
@@ -1226,6 +1269,22 @@
         return r.name;
     }
 
+    private @Nullable String getShortProcessNameForStats(int uid, String processName) {
+        final String[] packages = mAm.mContext.getPackageManager().getPackagesForUid(uid);
+        if (packages != null && packages.length == 1) {
+            // Not the shared UID case, let's see if the package name equals to the process name.
+            if (TextUtils.equals(packages[0], processName)) {
+                // same name, just return null here.
+                return null;
+            } else if (processName != null && processName.startsWith(packages[0])) {
+                // return the suffix of the process name
+                return processName.substring(packages[0].length());
+            }
+        }
+        // return the full process name.
+        return processName;
+    }
+
     private void stopServiceLocked(ServiceRecord service, boolean enqueueOomAdj) {
         try {
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "stopServiceLocked()");
@@ -1911,6 +1970,7 @@
                     ignoreForeground = true;
                 }
 
+                int fgsTypeCheckCode = FGS_TYPE_POLICY_CHECK_UNKNOWN;
                 if (!ignoreForeground) {
                     if (r.mStartForegroundCount == 0) {
                         /*
@@ -1931,7 +1991,9 @@
                             if (delayMs > mAm.mConstants.mFgsStartForegroundTimeoutMs) {
                                 resetFgsRestrictionLocked(r);
                                 setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
-                                        r.appInfo.uid, r.intent.getIntent(), r, r.userId, false);
+                                        r.appInfo.uid, r.intent.getIntent(), r, r.userId,
+                                        false /* allowBackgroundActivityStarts */,
+                                        false /* isBindService */);
                                 final String temp = "startForegroundDelayMs:" + delayMs;
                                 if (r.mInfoAllowStartForeground != null) {
                                     r.mInfoAllowStartForeground += "; " + temp;
@@ -1945,7 +2007,9 @@
                         // The second or later time startForeground() is called after service is
                         // started. Check for app state again.
                         setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
-                                r.appInfo.uid, r.intent.getIntent(), r, r.userId, false);
+                                r.appInfo.uid, r.intent.getIntent(), r, r.userId,
+                                false /* allowBackgroundActivityStarts */,
+                                false /* isBindService */);
                     }
                     // If the foreground service is not started from TOP process, do not allow it to
                     // have while-in-use location/camera/microphone access.
@@ -1965,13 +2029,49 @@
                         updateServiceForegroundLocked(psr, true);
                         ignoreForeground = true;
                         logFGSStateChangeLocked(r,
-                                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
-                                0, FGS_STOP_REASON_UNKNOWN);
+                                FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
+                                0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN);
                         if (CompatChanges.isChangeEnabled(FGS_START_EXCEPTION_CHANGE_ID,
                                 r.appInfo.uid)) {
                             throw new ForegroundServiceStartNotAllowedException(msg);
                         }
                     }
+
+                    if (!ignoreForeground) {
+                        Pair<Integer, RuntimeException> fgsTypeResult = null;
+                        if (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+                            fgsTypeResult = validateForegroundServiceType(r,
+                                    foregroundServiceType,
+                                    ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
+                        } else {
+                            int fgsTypes = foregroundServiceType;
+                            // If the service has declared some unknown types which might be coming
+                            // from future releases, and if it also comes with the "specialUse",
+                            // then it'll be deemed as the "specialUse" and we ignore this
+                            // unknown type. Otherwise, it'll be treated as an invalid type.
+                            int defaultFgsTypes = (foregroundServiceType
+                                    & ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) != 0
+                                    ? ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
+                                    : ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+                            for (int serviceType = Integer.highestOneBit(fgsTypes);
+                                    serviceType != 0;
+                                    serviceType = Integer.highestOneBit(fgsTypes)) {
+                                fgsTypeResult = validateForegroundServiceType(r,
+                                        serviceType, defaultFgsTypes);
+                                fgsTypes &= ~serviceType;
+                                if (fgsTypeResult.first != FGS_TYPE_POLICY_CHECK_OK) {
+                                    break;
+                                }
+                            }
+                        }
+                        fgsTypeCheckCode = fgsTypeResult.first;
+                        if (fgsTypeResult.second != null) {
+                            logFGSStateChangeLocked(r,
+                                    FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED,
+                                    0, FGS_STOP_REASON_UNKNOWN, fgsTypeResult.first);
+                            throw fgsTypeResult.second;
+                        }
+                    }
                 }
 
                 // Apps under strict background restrictions simply don't get to have foreground
@@ -2040,8 +2140,8 @@
                         registerAppOpCallbackLocked(r);
                         mAm.updateForegroundServiceUsageStats(r.name, r.userId, true);
                         logFGSStateChangeLocked(r,
-                                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
-                                0, FGS_STOP_REASON_UNKNOWN);
+                                FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
+                                0, FGS_STOP_REASON_UNKNOWN, fgsTypeCheckCode);
                         updateNumForegroundServicesLocked();
                     }
                     // Even if the service is already a FGS, we need to update the notification,
@@ -2122,10 +2222,11 @@
                         AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null);
                 unregisterAppOpCallbackLocked(r);
                 logFGSStateChangeLocked(r,
-                        FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
+                        FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                         r.mFgsExitTime > r.mFgsEnterTime
                                 ? (int) (r.mFgsExitTime - r.mFgsEnterTime) : 0,
-                        FGS_STOP_REASON_STOP_FOREGROUND);
+                        FGS_STOP_REASON_STOP_FOREGROUND,
+                        FGS_TYPE_POLICY_CHECK_UNKNOWN);
                 r.mFgsNotificationWasDeferred = false;
                 signalForegroundServiceObserversLocked(r);
                 resetFgsRestrictionLocked(r);
@@ -2161,6 +2262,118 @@
         return now < eligible;
     }
 
+    /**
+     * Validate if the given service can start a foreground service with given type.
+     *
+     * @return A pair, where the first parameter is the result code and second is the exception
+     *         object if it fails to start a foreground service with given type.
+     */
+    @NonNull
+    private Pair<Integer, RuntimeException> validateForegroundServiceType(ServiceRecord r,
+            @ForegroundServiceType int type,
+            @ForegroundServiceType int defaultToType) {
+        final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy();
+        final ForegroundServiceTypePolicyInfo policyInfo =
+                policy.getForegroundServiceTypePolicyInfo(type, defaultToType);
+        final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy(
+                mAm.mContext, r.packageName, r.app.uid, r.app.getPid(),
+                r.mAllowWhileInUsePermissionInFgs, policyInfo);
+        RuntimeException exception = null;
+        switch (code) {
+            case FGS_TYPE_POLICY_CHECK_DEPRECATED: {
+                final String msg = "Starting FGS with type "
+                        + ServiceInfo.foregroundServiceTypeToLabel(type)
+                        + " code=" + code
+                        + " callerApp=" + r.app
+                        + " targetSDK=" + r.app.info.targetSdkVersion;
+                Slog.wtfQuiet(TAG, msg);
+                Slog.w(TAG, msg);
+            } break;
+            case FGS_TYPE_POLICY_CHECK_DISABLED: {
+                exception = new ForegroundServiceTypeNotAllowedException(
+                        "Starting FGS with type "
+                        + ServiceInfo.foregroundServiceTypeToLabel(type)
+                        + " callerApp=" + r.app
+                        + " targetSDK=" + r.app.info.targetSdkVersion
+                        + " has been prohibited");
+            } break;
+            case FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_PERMISSIVE: {
+                final String msg = "Starting FGS with type "
+                        + ServiceInfo.foregroundServiceTypeToLabel(type)
+                        + " code=" + code
+                        + " callerApp=" + r.app
+                        + " targetSDK=" + r.app.info.targetSdkVersion
+                        + " requiredPermissions=" + policyInfo.toPermissionString();
+                Slog.wtfQuiet(TAG, msg);
+                Slog.w(TAG, msg);
+            } break;
+            case FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED: {
+                exception = new SecurityException("Starting FGS with type "
+                        + ServiceInfo.foregroundServiceTypeToLabel(type)
+                        + " callerApp=" + r.app
+                        + " targetSDK=" + r.app.info.targetSdkVersion
+                        + " requires permissions: "
+                        + policyInfo.toPermissionString());
+            } break;
+            case FGS_TYPE_POLICY_CHECK_OK:
+            default:
+                break;
+        }
+        return Pair.create(code, exception);
+    }
+
+    private class SystemExemptedFgsTypePermission extends ForegroundServiceTypePermission {
+        SystemExemptedFgsTypePermission() {
+            super("System exempted");
+        }
+
+        @Override
+        public int checkPermission(@NonNull Context context, int callerUid, int callerPid,
+                @NonNull String packageName, boolean allowWhileInUse) {
+            final AppRestrictionController appRestrictionController = mAm.mAppRestrictionController;
+            @ReasonCode int reason = appRestrictionController
+                    .getPotentialSystemExemptionReason(callerUid);
+            if (reason == REASON_DENIED) {
+                reason = appRestrictionController
+                        .getPotentialSystemExemptionReason(callerUid, packageName);
+                if (reason == REASON_DENIED) {
+                    reason = appRestrictionController
+                            .getPotentialUserAllowedExemptionReason(callerUid, packageName);
+                }
+            }
+            switch (reason) {
+                case REASON_SYSTEM_UID:
+                case REASON_SYSTEM_ALLOW_LISTED:
+                case REASON_DEVICE_DEMO_MODE:
+                case REASON_DISALLOW_APPS_CONTROL:
+                case REASON_DEVICE_OWNER:
+                case REASON_PROFILE_OWNER:
+                case REASON_PROC_STATE_PERSISTENT:
+                case REASON_PROC_STATE_PERSISTENT_UI:
+                case REASON_SYSTEM_MODULE:
+                case REASON_CARRIER_PRIVILEGED_APP:
+                case REASON_DPO_PROTECTED_APP:
+                case REASON_ACTIVE_DEVICE_ADMIN:
+                case REASON_ROLE_EMERGENCY:
+                case REASON_ALLOWLISTED_PACKAGE:
+                    return PERMISSION_GRANTED;
+                default:
+                    return PERMISSION_DENIED;
+            }
+        }
+    }
+
+    private void initSystemExemptedFgsTypePermission() {
+        final ForegroundServiceTypePolicy policy = ForegroundServiceTypePolicy.getDefaultPolicy();
+        final ForegroundServiceTypePolicyInfo policyInfo =
+                policy.getForegroundServiceTypePolicyInfo(
+                       ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED,
+                       ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
+        if (policyInfo != null) {
+            policyInfo.setCustomPermission(new SystemExemptedFgsTypePermission());
+        }
+    }
+
     ServiceNotificationPolicy applyForegroundServiceNotificationLocked(Notification notification,
             final String tag, final int id, final String pkg, final int userId) {
         // By nature of the FGS API, all FGS notifications have a null tag
@@ -2866,7 +3079,7 @@
         ServiceLookupResult res = retrieveServiceLocked(service, instanceName,
                 isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
                 resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
-                isBindExternal, allowInstant);
+                isBindExternal, allowInstant, null /* fgsDelegateOptions */);
         if (res == null) {
             return 0;
         }
@@ -2985,7 +3198,7 @@
                 }
             }
             setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, s, userId,
-                    false);
+                    false /* allowBackgroundActivityStarts */, true /* isBindService */);
 
             if (s.app != null) {
                 ProcessServiceRecord servicePsr = s.app.mServices;
@@ -3015,7 +3228,8 @@
                     ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD
                     : (wasStartRequested || hadConnections
                     ? SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_HOT
-                    : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM));
+                    : SERVICE_REQUEST_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM),
+                    getShortProcessNameForStats(callingUid, callerApp.processName));
 
             if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bind " + s + " with " + b
                     + ": received=" + b.intent.received
@@ -3324,7 +3538,7 @@
             boolean allowInstant) {
         return retrieveServiceLocked(service, instanceName, false, 0, null, resolvedType,
                 callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg,
-                isBindExternal, allowInstant);
+                isBindExternal, allowInstant, null /* fgsDelegateOptions */);
     }
 
     private ServiceLookupResult retrieveServiceLocked(Intent service,
@@ -3332,7 +3546,7 @@
             String sdkSandboxClientAppPackage, String resolvedType,
             String callingPackage, int callingPid, int callingUid, int userId,
             boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
-            boolean allowInstant) {
+            boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions) {
         if (isSdkSandboxService && instanceName == null) {
             throw new IllegalArgumentException("No instanceName provided for sdk sandbox process");
         }
@@ -3395,6 +3609,53 @@
                 }
             }
         }
+
+        if (r == null && fgsDelegateOptions != null) {
+            // Create a ServiceRecord for FGS delegate.
+            final ServiceInfo sInfo = new ServiceInfo();
+            ApplicationInfo aInfo = null;
+            try {
+                aInfo = AppGlobals.getPackageManager().getApplicationInfo(
+                        fgsDelegateOptions.mClientPackageName,
+                        ActivityManagerService.STOCK_PM_FLAGS,
+                        userId);
+            } catch (RemoteException ex) {
+            // pm is in same process, this will never happen.
+            }
+            if (aInfo == null) {
+                throw new SecurityException("startForegroundServiceDelegate failed, "
+                        + "could not resolve client package " + callingPackage);
+            }
+            if (aInfo.uid != fgsDelegateOptions.mClientUid) {
+                throw new SecurityException("startForegroundServiceDelegate failed, "
+                        + "uid:" + aInfo.uid
+                        + " does not match clientUid:" + fgsDelegateOptions.mClientUid);
+            }
+            sInfo.applicationInfo = aInfo;
+            sInfo.packageName = aInfo.packageName;
+            sInfo.mForegroundServiceType = fgsDelegateOptions.mForegroundServiceTypes;
+            sInfo.processName = aInfo.processName;
+            final ComponentName cn = service.getComponent();
+            sInfo.name = cn.getClassName();
+            if (createIfNeeded) {
+                final Intent.FilterComparison filter =
+                        new Intent.FilterComparison(service.cloneFilter());
+                final ServiceRestarter res = new ServiceRestarter();
+                r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */,
+                        sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo,
+                        callingFromFg, res, null /* sdkSandboxProcessName */,
+                        INVALID_UID /* sdkSandboxClientAppUid */,
+                        null /* sdkSandboxClientAppPackage */);
+                res.setService(r);
+                smap.mServicesByInstanceName.put(cn, r);
+                smap.mServicesByIntent.put(filter, r);
+                if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Retrieve created new service: " + r);
+                r.mRecentCallingPackage = callingPackage;
+                r.mRecentCallingUid = callingUid;
+            }
+            return new ServiceLookupResult(r, resolution.getAlias());
+        }
+
         if (r == null) {
             try {
                 int flags = ActivityManagerService.STOCK_PM_FLAGS
@@ -4484,7 +4745,7 @@
         // be called.
         if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {
             r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
-                    null, null, 0));
+                    null, null, 0, null));
         }
 
         sendServiceArgsLocked(r, execInFg, true);
@@ -4773,10 +5034,11 @@
             unregisterAppOpCallbackLocked(r);
             r.mFgsExitTime = SystemClock.uptimeMillis();
             logFGSStateChangeLocked(r,
-                    FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
+                    FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT,
                     r.mFgsExitTime > r.mFgsEnterTime
                             ? (int) (r.mFgsExitTime - r.mFgsEnterTime) : 0,
-                    FGS_STOP_REASON_STOP_SERVICE);
+                    FGS_STOP_REASON_STOP_SERVICE,
+                    FGS_TYPE_POLICY_CHECK_UNKNOWN);
             mAm.updateForegroundServiceUsageStats(r.name, r.userId, false);
         }
 
@@ -4805,16 +5067,31 @@
                 // Bump the process to the top of LRU list
                 mAm.updateLruProcessLocked(r.app, false, null);
                 updateServiceForegroundLocked(r.app.mServices, false);
-                try {
-                    oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy",
-                            oomAdjusted ? 0 : OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE);
-                    mDestroyingServices.add(r);
-                    r.destroying = true;
-                    r.app.getThread().scheduleStopService(r);
-                } catch (Exception e) {
-                    Slog.w(TAG, "Exception when destroying service "
-                            + r.shortInstanceName, e);
-                    serviceProcessGoneLocked(r, enqueueOomAdj);
+                if (r.mIsFgsDelegate) {
+                    if (r.mFgsDelegation.mConnection != null) {
+                        mAm.mHandler.post(() -> {
+                            r.mFgsDelegation.mConnection.onServiceDisconnected(
+                                    r.mFgsDelegation.mOptions.getComponentName());
+                        });
+                    }
+                    for (int i = mFgsDelegations.size() - 1; i >= 0; i--) {
+                        if (mFgsDelegations.valueAt(i) == r) {
+                            mFgsDelegations.removeAt(i);
+                            break;
+                        }
+                    }
+                } else {
+                    try {
+                        oomAdjusted |= bumpServiceExecutingLocked(r, false, "destroy",
+                                oomAdjusted ? 0 : OomAdjuster.OOM_ADJ_REASON_UNBIND_SERVICE);
+                        mDestroyingServices.add(r);
+                        r.destroying = true;
+                        r.app.getThread().scheduleStopService(r);
+                    } catch (Exception e) {
+                        Slog.w(TAG, "Exception when destroying service "
+                                + r.shortInstanceName, e);
+                        serviceProcessGoneLocked(r, enqueueOomAdj);
+                    }
                 }
             } else {
                 if (DEBUG_SERVICE) Slog.v(
@@ -5442,7 +5719,7 @@
                     stopServiceLocked(sr, true);
                 } else {
                     sr.pendingStarts.add(new ServiceRecord.StartItem(sr, true,
-                            sr.getLastStartId(), baseIntent, null, 0));
+                            sr.getLastStartId(), baseIntent, null, 0, null));
                     if (sr.app != null && sr.app.getThread() != null) {
                         // We always run in the foreground, since this is called as
                         // part of the "remove task" UI operation.
@@ -6500,7 +6777,7 @@
      */
     private void setFgsRestrictionLocked(String callingPackage,
             int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId,
-            boolean allowBackgroundActivityStarts) {
+            boolean allowBackgroundActivityStarts, boolean isBindService) {
         r.mLastSetFgsRestrictionTime = SystemClock.elapsedRealtime();
         // Check DeviceConfig flag.
         if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
@@ -6510,14 +6787,15 @@
         if (!r.mAllowWhileInUsePermissionInFgs
                 || (r.mAllowStartForeground == REASON_DENIED)) {
             final @ReasonCode int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
-                    callingPackage, callingPid, callingUid, r, allowBackgroundActivityStarts);
+                    callingPackage, callingPid, callingUid, r, allowBackgroundActivityStarts,
+                    isBindService);
             if (!r.mAllowWhileInUsePermissionInFgs) {
                 r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED);
             }
             if (r.mAllowStartForeground == REASON_DENIED) {
                 r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked(
                         allowWhileInUse, callingPackage, callingPid, callingUid, intent, r,
-                        userId);
+                        userId, isBindService);
             }
         }
     }
@@ -6537,9 +6815,10 @@
         }
         final @ReasonCode int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
                 callingPackage, callingPid, callingUid, null /* serviceRecord */,
-                false /* allowBackgroundActivityStarts */);
+                false /* allowBackgroundActivityStarts */, false);
         @ReasonCode int allowStartFgs = shouldAllowFgsStartForegroundNoBindingCheckLocked(
-                allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */);
+                allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */,
+                false /* isBindService */);
 
         if (allowStartFgs == REASON_DENIED) {
             if (canBindingClientStartFgsLocked(callingUid) != null) {
@@ -6559,7 +6838,7 @@
      */
     private @ReasonCode int shouldAllowFgsWhileInUsePermissionLocked(String callingPackage,
             int callingPid, int callingUid, @Nullable ServiceRecord targetService,
-            boolean allowBackgroundActivityStarts) {
+            boolean allowBackgroundActivityStarts, boolean isBindService) {
         int ret = REASON_DENIED;
 
         final int uidState = mAm.getUidStateLocked(callingUid);
@@ -6713,12 +6992,12 @@
                                         shouldAllowFgsWhileInUsePermissionLocked(
                                                 clientPackageName,
                                                 clientPid, clientUid, null /* serviceRecord */,
-                                                false /* allowBackgroundActivityStarts */);
+                                                false /* allowBackgroundActivityStarts */, false);
                                 final @ReasonCode int allowStartFgs =
                                         shouldAllowFgsStartForegroundNoBindingCheckLocked(
                                                 allowWhileInUse2,
                                                 clientPid, clientUid, clientPackageName,
-                                                null /* targetService */);
+                                                null /* targetService */, false);
                                 if (allowStartFgs != REASON_DENIED) {
                                     return new Pair<>(allowStartFgs, clientPackageName);
                                 } else {
@@ -6750,11 +7029,11 @@
      */
     private @ReasonCode int shouldAllowFgsStartForegroundWithBindingCheckLocked(
             @ReasonCode int allowWhileInUse, String callingPackage, int callingPid,
-            int callingUid, Intent intent, ServiceRecord r, int userId) {
+            int callingUid, Intent intent, ServiceRecord r, int userId, boolean isBindService) {
         ActivityManagerService.FgsTempAllowListItem tempAllowListReason =
                 r.mInfoTempFgsAllowListReason = mAm.isAllowlistedForFgsStartLOSP(callingUid);
         int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid,
-                callingUid, callingPackage, r);
+                callingUid, callingPackage, r, isBindService);
 
         String bindFromPackage = null;
         if (ret == REASON_DENIED) {
@@ -6789,6 +7068,7 @@
                         + "; callerTargetSdkVersion:" + callerTargetSdkVersion
                         + "; startForegroundCount:" + r.mStartForegroundCount
                         + "; bindFromPackage:" + bindFromPackage
+                        + ": isBindService:" + isBindService
                         + "]";
         if (!debugInfo.equals(r.mInfoAllowStartForeground)) {
             r.mLoggedInfoAllowStartForeground = false;
@@ -6799,7 +7079,7 @@
 
     private @ReasonCode int shouldAllowFgsStartForegroundNoBindingCheckLocked(
             @ReasonCode int allowWhileInUse, int callingPid, int callingUid, String callingPackage,
-            @Nullable ServiceRecord targetService) {
+            @Nullable ServiceRecord targetService, boolean isBindService) {
         int ret = allowWhileInUse;
 
         if (ret == REASON_DENIED) {
@@ -6981,10 +7261,12 @@
     }
 
     private void logFgsBackgroundStart(ServiceRecord r) {
+        /*
         // Only log if FGS is started from background.
         if (!isFgsBgStart(r.mAllowStartForeground)) {
             return;
         }
+        */
         if (!r.mLoggedInfoAllowStartForeground) {
             final String msg = "Background started FGS: "
                     + ((r.mAllowStartForeground != REASON_DENIED) ? "Allowed " : "Disallowed ")
@@ -6996,10 +7278,10 @@
                 }
                 Slog.i(TAG, msg);
             } else {
-                if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName,
-                        mAm.mConstants.mFgsStartDeniedLogSampleRate)) {
+                //if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName,
+                //        mAm.mConstants.mFgsStartDeniedLogSampleRate)) {
                     Slog.wtfQuiet(TAG, msg);
-                }
+                //}
                 Slog.w(TAG, msg);
             }
             r.mLoggedInfoAllowStartForeground = true;
@@ -7011,17 +7293,20 @@
      * @param r ServiceRecord
      * @param state one of ENTER/EXIT/DENIED event.
      * @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state.
+     * @param fgsStopReason why was this FGS stopped.
+     * @param fgsTypeCheckCode The FGS type policy check result.
      */
     private void logFGSStateChangeLocked(ServiceRecord r, int state, int durationMs,
-            @FgsStopReason int fgsStopReason) {
+            @FgsStopReason int fgsStopReason,
+            @ForegroundServicePolicyCheckCode int fgsTypeCheckCode) {
         if (!ActivityManagerUtils.shouldSamplePackageForAtom(
                 r.packageName, mAm.mConstants.mFgsAtomSampleRate)) {
             return;
         }
         boolean allowWhileInUsePermissionInFgs;
         @PowerExemptionManager.ReasonCode int fgsStartReasonCode;
-        if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER
-                || state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) {
+        if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER
+                || state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) {
             allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering;
             fgsStartReasonCode = r.mAllowStartForegroundAtEntering;
         } else {
@@ -7047,14 +7332,20 @@
                 r.mStartForegroundCount,
                 ActivityManagerUtils.hashComponentNameForAtom(r.shortInstanceName),
                 r.mFgsHasNotificationPermission,
-                r.foregroundServiceType);
+                r.foregroundServiceType,
+                fgsTypeCheckCode,
+                r.mIsFgsDelegate,
+                r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mClientUid : INVALID_UID,
+                r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService
+                        : ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT
+        );
 
         int event = 0;
-        if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) {
+        if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) {
             event = EventLogTags.AM_FOREGROUND_SERVICE_START;
-        } else if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) {
+        } else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__EXIT) {
             event = EventLogTags.AM_FOREGROUND_SERVICE_STOP;
-        } else if (state == FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED) {
+        } else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED) {
             event = EventLogTags.AM_FOREGROUND_SERVICE_DENIED;
         } else {
             // Unknown event.
@@ -7082,7 +7373,7 @@
             String callingPackage) {
         return shouldAllowFgsWhileInUsePermissionLocked(callingPackage, callingPid, callingUid,
                 /* targetService */ null,
-                /* allowBackgroundActivityStarts */ false)
+                /* allowBackgroundActivityStarts */ false, false)
                 != REASON_DENIED;
     }
 
@@ -7111,4 +7402,163 @@
                 return "UNKNOWN";
         }
     }
+
+    /**
+     * Start a foreground service delegate. The delegate is not an actual service component, it is
+     * merely a delegate that promotes the client process into foreground service process state.
+     *
+     * @param options an ForegroundServiceDelegationOptions object.
+     * @param connection callback if the delegate is started successfully.
+     * @return true if delegate is started, false otherwise.
+     * @throw SecurityException if PackageManaager can not resolve
+     *        {@link ForegroundServiceDelegationOptions#mClientPackageName} or the resolved
+     *        package's UID is not same as {@link ForegroundServiceDelegationOptions#mClientUid}
+     */
+    boolean startForegroundServiceDelegateLocked(
+            @NonNull ForegroundServiceDelegationOptions options,
+            @Nullable ServiceConnection connection) {
+        Slog.v(TAG, "startForegroundServiceDelegateLocked " + options.getDescription());
+        final ComponentName cn = options.getComponentName();
+        for (int i = mFgsDelegations.size() - 1; i >= 0; i--) {
+            ForegroundServiceDelegation delegation = mFgsDelegations.keyAt(i);
+            if (delegation.mOptions.isSameDelegate(options)) {
+                Slog.e(TAG, "startForegroundServiceDelegate " + options.getDescription()
+                        + " already exists, multiple connections are not allowed");
+                return false;
+            }
+        }
+        final int callingPid = options.mClientPid;
+        final int callingUid = options.mClientUid;
+        final int userId = UserHandle.getUserId(callingUid);
+        final String callingPackage = options.mClientPackageName;
+
+        if (!canStartForegroundServiceLocked(callingPid, callingUid, callingPackage)) {
+            Slog.d(TAG, "startForegroundServiceDelegateLocked aborted,"
+                    + " app is in the background");
+            return false;
+        }
+
+        IApplicationThread caller = options.mClientAppThread;
+        ProcessRecord callerApp;
+        if (caller != null) {
+            callerApp = mAm.getRecordForAppLOSP(caller);
+        } else {
+            synchronized (mAm.mPidsSelfLocked) {
+                callerApp = mAm.mPidsSelfLocked.get(callingPid);
+                caller = callerApp.getThread();
+            }
+        }
+        if (callerApp == null) {
+            throw new SecurityException(
+                    "Unable to find app for caller " + caller
+                            + " (pid=" + callingPid
+                            + ") when startForegroundServiceDelegateLocked " + cn);
+        }
+
+        Intent intent = new Intent();
+        intent.setComponent(cn);
+        ServiceLookupResult res = retrieveServiceLocked(intent, null /*instanceName */,
+                false /* isSdkSandboxService */, INVALID_UID /* sdkSandboxClientAppUid */,
+                null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage,
+                callingPid, callingUid, userId, true /* createIfNeeded */,
+                false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ ,
+                options);
+        if (res == null || res.record == null) {
+            Slog.d(TAG,
+                    "startForegroundServiceDelegateLocked retrieveServiceLocked returns null");
+            return false;
+        }
+
+        final ServiceRecord r = res.record;
+        r.setProcess(callerApp, caller, callingPid, null);
+        r.mIsFgsDelegate = true;
+        final ForegroundServiceDelegation delegation =
+                new ForegroundServiceDelegation(options, connection);
+        r.mFgsDelegation = delegation;
+        mFgsDelegations.put(delegation, r);
+        r.isForeground = true;
+        r.mFgsEnterTime = SystemClock.uptimeMillis();
+        r.foregroundServiceType = options.mForegroundServiceTypes;
+        setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId,
+                false, false);
+        final ProcessServiceRecord psr = callerApp.mServices;
+        final boolean newService = psr.startService(r);
+        // updateOomAdj.
+        updateServiceForegroundLocked(psr, /* oomAdj= */ true);
+
+        synchronized (mAm.mProcessStats.mLock) {
+            final ServiceState stracker = r.getTracker();
+            if (stracker != null) {
+                stracker.setForeground(true,
+                        mAm.mProcessStats.getMemFactorLocked(),
+                        SystemClock.uptimeMillis());
+            }
+        }
+
+        mAm.mBatteryStatsService.noteServiceStartRunning(callingUid, callingPackage,
+                cn.getClassName());
+        mAm.mAppOpsService.startOperation(AppOpsManager.getToken(mAm.mAppOpsService),
+                AppOpsManager.OP_START_FOREGROUND, r.appInfo.uid, r.packageName, null,
+                true, false, null, false,
+                AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
+        registerAppOpCallbackLocked(r);
+        logFGSStateChangeLocked(r,
+                FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER,
+                0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN);
+        // Notify the caller.
+        if (connection != null) {
+            mAm.mHandler.post(() -> {
+                connection.onServiceConnected(cn, delegation.mBinder);
+            });
+        }
+        signalForegroundServiceObserversLocked(r);
+        return true;
+    }
+
+    /**
+     * Stop the foreground service delegate. This removes the process out of foreground service
+     * process state.
+     *
+     * @param options an ForegroundServiceDelegationOptions object.
+     */
+    void stopForegroundServiceDelegateLocked(@NonNull ForegroundServiceDelegationOptions options) {
+        ServiceRecord r = null;
+        for (int i = mFgsDelegations.size() - 1; i >= 0; i--) {
+            if (mFgsDelegations.keyAt(i).mOptions.isSameDelegate(options)) {
+                Slog.d(TAG, "stopForegroundServiceDelegateLocked " + options.getDescription());
+                r = mFgsDelegations.valueAt(i);
+                break;
+            }
+        }
+        if (r != null) {
+            bringDownServiceLocked(r, false);
+        } else {
+            Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist "
+                    + options.getDescription());
+        }
+    }
+
+    /**
+     * Stop the foreground service delegate by its ServiceConnection.
+     * This removes the process out of foreground service process state.
+     *
+     * @param connection an ServiceConnection object.
+     */
+    void stopForegroundServiceDelegateLocked(@NonNull ServiceConnection connection) {
+        ServiceRecord r = null;
+        for (int i = mFgsDelegations.size() - 1; i >= 0; i--) {
+            final ForegroundServiceDelegation d = mFgsDelegations.keyAt(i);
+            if (d.mConnection == connection) {
+                Slog.d(TAG, "stopForegroundServiceDelegateLocked "
+                        + d.mOptions.getDescription());
+                r = mFgsDelegations.valueAt(i);
+                break;
+            }
+        }
+        if (r != null) {
+            bringDownServiceLocked(r, false);
+        } else {
+            Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist");
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 16fe121..8b5b795 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -249,6 +249,18 @@
     private static final String KEY_MAX_PHANTOM_PROCESSES = "max_phantom_processes";
 
     /**
+     * Enables proactive killing of cached apps
+     */
+    private static final String KEY_PROACTIVE_KILLS_ENABLED = "proactive_kills_enabled";
+
+    /**
+      * Trim LRU cached app when swap falls below this minimum percentage.
+      *
+      * Depends on KEY_PROACTIVE_KILLS_ENABLED
+      */
+    private static final String KEY_LOW_SWAP_THRESHOLD_PERCENT = "low_swap_threshold_percent";
+
+    /**
      * Default value for mFlagBackgroundActivityStartsEnabled if not explicitly set in
      * Settings.Global. This allows it to be set experimentally unless it has been
      * enabled/disabled in developer options. Defaults to false.
@@ -306,6 +318,12 @@
             "deferred_fgs_notification_interval";
 
     /**
+     * Same as {@link #KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL} but for "short FGS".
+     */
+    private static final String KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT =
+            "deferred_fgs_notification_interval_for_short";
+
+    /**
      * Time in milliseconds; once an FGS notification for a given uid has been
      * deferred, no subsequent FGS notification from that uid will be deferred
      * until this amount of time has passed.  Default is two minutes
@@ -315,6 +333,12 @@
             "deferred_fgs_notification_exclusion_time";
 
     /**
+     * Same as {@link #KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME} but for "short FGS".
+     */
+    private static final String KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT =
+            "deferred_fgs_notification_exclusion_time_for_short";
+
+    /**
      * Default value for mPushMessagingOverQuotaBehavior if not explicitly set in
      * Settings.Global.
      */
@@ -571,11 +595,22 @@
     // the foreground state.
     volatile long mFgsNotificationDeferralInterval = 10_000;
 
+    /**
+     * Same as {@link #mFgsNotificationDeferralInterval} but used for "short FGS".
+     */
+    volatile long mFgsNotificationDeferralIntervalForShort = mFgsNotificationDeferralInterval;
+
     // Rate limit: minimum time after an app's FGS notification is deferred
     // before another FGS notification from that app can be deferred.
     volatile long mFgsNotificationDeferralExclusionTime = 2 * 60 * 1000L;
 
     /**
+     * Same as {@link #mFgsNotificationDeferralExclusionTime} but used for "short FGS".
+     */
+    volatile long mFgsNotificationDeferralExclusionTimeForShort =
+            mFgsNotificationDeferralExclusionTime;
+
+    /**
      * When server pushing message is over the quote, select one of the temp allow list type as
      * defined in {@link PowerExemptionManager.TempAllowListType}
      */
@@ -874,6 +909,10 @@
      */
     private static final long DEFAULT_MIN_ASSOC_LOG_DURATION = 5 * 60 * 1000; // 5 mins
 
+    private static final boolean DEFAULT_PROACTIVE_KILLS_ENABLED = false;
+
+    private static final float DEFAULT_LOW_SWAP_THRESHOLD_PERCENT = 0.10f;
+
     private static final String KEY_MIN_ASSOC_LOG_DURATION = "min_assoc_log_duration";
 
     public static long MIN_ASSOC_LOG_DURATION = DEFAULT_MIN_ASSOC_LOG_DURATION;
@@ -904,6 +943,34 @@
     public static boolean BINDER_HEAVY_HITTER_AUTO_SAMPLER_ENABLED;
     public static int BINDER_HEAVY_HITTER_AUTO_SAMPLER_BATCHSIZE;
     public static float BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD;
+    public static boolean PROACTIVE_KILLS_ENABLED = DEFAULT_PROACTIVE_KILLS_ENABLED;
+    public static float LOW_SWAP_THRESHOLD_PERCENT = DEFAULT_LOW_SWAP_THRESHOLD_PERCENT;
+
+    /** Timeout for a "short service" FGS, in milliseconds. */
+    private static final String KEY_SHORT_FGS_TIMEOUT_DURATION =
+            "short_fgs_timeout_duration";
+
+    /** @see #KEY_SHORT_FGS_TIMEOUT_DURATION */
+    static final long DEFAULT_SHORT_FGS_TIMEOUT_DURATION = 60_000;
+
+    /** @see #KEY_SHORT_FGS_TIMEOUT_DURATION */
+    public static volatile long mShortFgsTimeoutDuration = DEFAULT_SHORT_FGS_TIMEOUT_DURATION;
+
+    /**
+     * If a "short service" doesn't finish within this after the timeout (
+     * {@link #KEY_SHORT_FGS_TIMEOUT_DURATION}), then we'll declare an ANR.
+     * i.e. if the timeout is 60 seconds, and this ANR extra duration is 5 seconds, then
+     * the app will be ANR'ed in 65 seconds after a short service starts and it's not stopped.
+     */
+    private static final String KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION =
+            "short_fgs_anr_extra_wait_duration";
+
+    /** @see #KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION */
+    static final long DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION = 5_000;
+
+    /** @see #KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION */
+    public static volatile long mShortFgsAnrExtraWaitDuration =
+            DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION;
 
     private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
             new OnPropertiesChangedListener() {
@@ -944,6 +1011,12 @@
                             case KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME:
                                 updateFgsNotificationDeferralExclusionTime();
                                 break;
+                            case KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT:
+                                updateFgsNotificationDeferralIntervalForShort();
+                                break;
+                            case KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT:
+                                updateFgsNotificationDeferralExclusionTimeForShort();
+                                break;
                             case KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR:
                                 updatePushMessagingOverQuotaBehavior();
                                 break;
@@ -1040,6 +1113,18 @@
                             case KEY_MAX_SERVICE_CONNECTIONS_PER_PROCESS:
                                 updateMaxServiceConnectionsPerProcess();
                                 break;
+                            case KEY_SHORT_FGS_TIMEOUT_DURATION:
+                                updateShortFgsTimeoutDuration();
+                                break;
+                            case KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION:
+                                updateShortFgsAnrExtraWaitDuration();
+                                break;
+                            case KEY_PROACTIVE_KILLS_ENABLED:
+                                updateProactiveKillsEnabled();
+                                break;
+                            case KEY_LOW_SWAP_THRESHOLD_PERCENT:
+                                updateLowSwapThresholdPercent();
+                                break;
                             default:
                                 break;
                         }
@@ -1350,6 +1435,13 @@
                 /*default value*/ 10_000L);
     }
 
+    private void updateFgsNotificationDeferralIntervalForShort() {
+        mFgsNotificationDeferralIntervalForShort = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT,
+                /*default value*/ 10_000L);
+    }
+
     private void updateFgsNotificationDeferralExclusionTime() {
         mFgsNotificationDeferralExclusionTime = DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -1357,6 +1449,13 @@
                 /*default value*/ 2 * 60 * 1000L);
     }
 
+    private void updateFgsNotificationDeferralExclusionTimeForShort() {
+        mFgsNotificationDeferralExclusionTimeForShort = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT,
+                /*default value*/ 2 * 60 * 1000L);
+    }
+
     private void updatePushMessagingOverQuotaBehavior() {
         mPushMessagingOverQuotaBehavior = DeviceConfig.getInt(
                 DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -1660,6 +1759,20 @@
         CUR_TRIM_CACHED_PROCESSES = (MAX_CACHED_PROCESSES-rawMaxEmptyProcesses)/3;
     }
 
+    private void updateProactiveKillsEnabled() {
+        PROACTIVE_KILLS_ENABLED = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_PROACTIVE_KILLS_ENABLED,
+                DEFAULT_PROACTIVE_KILLS_ENABLED);
+    }
+
+    private void updateLowSwapThresholdPercent() {
+        LOW_SWAP_THRESHOLD_PERCENT = DeviceConfig.getFloat(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_LOW_SWAP_THRESHOLD_PERCENT,
+                DEFAULT_LOW_SWAP_THRESHOLD_PERCENT);
+    }
+
     private void updateMinAssocLogDuration() {
         MIN_ASSOC_LOG_DURATION = DeviceConfig.getLong(
                 DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_MIN_ASSOC_LOG_DURATION,
@@ -1708,6 +1821,20 @@
                 DEFAULT_MAX_SERVICE_CONNECTIONS_PER_PROCESS);
     }
 
+    private void updateShortFgsTimeoutDuration() {
+        mShortFgsTimeoutDuration = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_SHORT_FGS_TIMEOUT_DURATION,
+                DEFAULT_SHORT_FGS_TIMEOUT_DURATION);
+    }
+
+    private void updateShortFgsAnrExtraWaitDuration() {
+        mShortFgsAnrExtraWaitDuration = DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION,
+                DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     void dump(PrintWriter pw) {
         pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) "
@@ -1860,6 +1987,30 @@
         pw.print("="); pw.println(mNetworkAccessTimeoutMs);
         pw.print("  "); pw.print(KEY_MAX_SERVICE_CONNECTIONS_PER_PROCESS);
         pw.print("="); pw.println(mMaxServiceConnectionsPerProcess);
+        pw.print("  "); pw.print(KEY_PROACTIVE_KILLS_ENABLED);
+        pw.print("="); pw.println(PROACTIVE_KILLS_ENABLED);
+        pw.print("  "); pw.print(KEY_LOW_SWAP_THRESHOLD_PERCENT);
+        pw.print("="); pw.println(LOW_SWAP_THRESHOLD_PERCENT);
+
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATIONS_ENABLED);
+        pw.print("="); pw.println(mFlagFgsNotificationDeferralEnabled);
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATIONS_API_GATED);
+        pw.print("="); pw.println(mFlagFgsNotificationDeferralApiGated);
+
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL);
+        pw.print("="); pw.println(mFgsNotificationDeferralInterval);
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_INTERVAL_FOR_SHORT);
+        pw.print("="); pw.println(mFgsNotificationDeferralIntervalForShort);
+
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME);
+        pw.print("="); pw.println(mFgsNotificationDeferralExclusionTime);
+        pw.print("  "); pw.print(KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME_FOR_SHORT);
+        pw.print("="); pw.println(mFgsNotificationDeferralExclusionTimeForShort);
+
+        pw.print("  "); pw.print(KEY_SHORT_FGS_TIMEOUT_DURATION);
+        pw.print("="); pw.println(mShortFgsTimeoutDuration);
+        pw.print("  "); pw.print(KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
+        pw.print("="); pw.println(mShortFgsAnrExtraWaitDuration);
 
         pw.println();
         if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java
index 1d2c36b..9f2cc7f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerLocal.java
+++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.content.Context;
@@ -92,4 +93,28 @@
             int clientAppUid, @NonNull String clientAppPackage, @NonNull String processName,
             @Context.BindServiceFlags int flags)
             throws RemoteException;
+
+    /**
+     * Start a foreground service delegate.
+     * @param options foreground service delegate options.
+     * @param connection a service connection served as callback to caller.
+     * @return true if delegate is started successfully, false otherwise.
+     * @hide
+     */
+    boolean startForegroundServiceDelegate(@NonNull ForegroundServiceDelegationOptions options,
+            @Nullable ServiceConnection connection);
+
+    /**
+     * Stop a foreground service delegate.
+     * @param options the foreground service delegate options.
+     * @hide
+     */
+    void stopForegroundServiceDelegate(@NonNull ForegroundServiceDelegationOptions options);
+
+    /**
+     * Stop a foreground service delegate by service connection.
+     * @param connection service connection used to start delegate previously.
+     * @hide
+     */
+    void stopForegroundServiceDelegate(@NonNull ServiceConnection connection);
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1a4da7d..39f6ef2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -181,6 +181,7 @@
 import android.app.ApplicationExitInfo;
 import android.app.ApplicationThreadConstants;
 import android.app.BroadcastOptions;
+import android.app.ComponentOptions;
 import android.app.ContentProviderHolder;
 import android.app.IActivityController;
 import android.app.IActivityManager;
@@ -332,6 +333,7 @@
 import android.util.PrintWriterPrinter;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
@@ -377,12 +379,10 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.MemInfoReader;
 import com.android.internal.util.Preconditions;
-import com.android.internal.util.function.DecFunction;
 import com.android.internal.util.function.HeptFunction;
 import com.android.internal.util.function.HexFunction;
 import com.android.internal.util.function.QuadFunction;
 import com.android.internal.util.function.QuintFunction;
-import com.android.internal.util.function.TriFunction;
 import com.android.internal.util.function.UndecFunction;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.BootReceiver;
@@ -467,10 +467,15 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiFunction;
+import java.util.function.Supplier;
 
 public class ActivityManagerService extends IActivityManager.Stub
         implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback, ActivityManagerGlobalLock {
@@ -3426,14 +3431,15 @@
      * @param lastPids of dalvik VM processes to dump stack traces for last
      * @param nativePids optional list of native pids to dump stack crawls
      * @param logExceptionCreatingFile optional writer to which we log errors creating the file
+     * @param auxiliaryTaskExecutor executor to execute auxiliary tasks on
      * @param latencyTracker the latency tracker instance of the current ANR.
      */
     public static File dumpStackTraces(ArrayList<Integer> firstPids,
-            ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids,
-            ArrayList<Integer> nativePids, StringWriter logExceptionCreatingFile,
-            AnrLatencyTracker latencyTracker) {
-        return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePids,
-                logExceptionCreatingFile, null, null, null, latencyTracker);
+            ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids,
+            Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile,
+            @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) {
+        return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture,
+                logExceptionCreatingFile, null, null, null, auxiliaryTaskExecutor, latencyTracker);
     }
 
     /**
@@ -3444,64 +3450,44 @@
      * @param logExceptionCreatingFile optional writer to which we log errors creating the file
      * @param subject optional line related to the error
      * @param criticalEventSection optional lines containing recent critical events.
+     * @param auxiliaryTaskExecutor executor to execute auxiliary tasks on
      * @param latencyTracker the latency tracker instance of the current ANR.
      */
     public static File dumpStackTraces(ArrayList<Integer> firstPids,
-            ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids,
-            ArrayList<Integer> nativePids, StringWriter logExceptionCreatingFile,
-            String subject, String criticalEventSection, AnrLatencyTracker latencyTracker) {
-        return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePids,
-                logExceptionCreatingFile, null, subject, criticalEventSection, latencyTracker);
+            ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids,
+            Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile,
+            String subject, String criticalEventSection, @NonNull Executor auxiliaryTaskExecutor,
+            AnrLatencyTracker latencyTracker) {
+        return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture,
+                logExceptionCreatingFile, null, subject, criticalEventSection,
+                auxiliaryTaskExecutor, latencyTracker);
     }
 
     /**
-     * @param firstPidOffsets Optional, when it's set, it receives the start/end offset
+     * @param firstPidEndOffset Optional, when it's set, it receives the start/end offset
      *                        of the very first pid to be dumped.
      */
     /* package */ static File dumpStackTraces(ArrayList<Integer> firstPids,
-            ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids,
-            ArrayList<Integer> nativePids, StringWriter logExceptionCreatingFile,
-            long[] firstPidOffsets, String subject, String criticalEventSection,
-            AnrLatencyTracker latencyTracker) {
+            ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids,
+            Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile,
+            AtomicLong firstPidEndOffset, String subject, String criticalEventSection,
+            @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) {
         try {
+
             if (latencyTracker != null) {
                 latencyTracker.dumpStackTracesStarted();
             }
-            ArrayList<Integer> extraPids = null;
 
-            Slog.i(TAG, "dumpStackTraces pids=" + lastPids + " nativepids=" + nativePids);
+            Slog.i(TAG, "dumpStackTraces pids=" + lastPids);
 
             // Measure CPU usage as soon as we're called in order to get a realistic sampling
             // of the top users at the time of the request.
-            if (processCpuTracker != null) {
-                if (latencyTracker != null) {
-                    latencyTracker.processCpuTrackerMethodsCalled();
-                }
-                processCpuTracker.init();
-                try {
-                    Thread.sleep(200);
-                } catch (InterruptedException ignored) {
-                }
-
-                processCpuTracker.update();
-
-                // We'll take the stack crawls of just the top apps using CPU.
-                final int workingStatsNumber = processCpuTracker.countWorkingStats();
-                extraPids = new ArrayList<>();
-                for (int i = 0; i < workingStatsNumber && extraPids.size() < 5; i++) {
-                    ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i);
-                    if (lastPids.indexOfKey(stats.pid) >= 0) {
-                        if (DEBUG_ANR) Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid);
-
-                        extraPids.add(stats.pid);
-                    } else {
-                        Slog.i(TAG, "Skipping next CPU consuming process, not a java proc: "
-                                + stats.pid);
-                    }
-                }
-                if (latencyTracker != null) {
-                    latencyTracker.processCpuTrackerMethodsReturned();
-                }
+            Supplier<ArrayList<Integer>> extraPidsSupplier = processCpuTracker != null
+                    ? () -> getExtraPids(processCpuTracker, lastPids, latencyTracker) : null;
+            Future<ArrayList<Integer>> extraPidsFuture = null;
+            if (extraPidsSupplier != null) {
+                extraPidsFuture =
+                        CompletableFuture.supplyAsync(extraPidsSupplier, auxiliaryTaskExecutor);
             }
 
             final File tracesDir = new File(ANR_TRACE_DIR);
@@ -3534,15 +3520,11 @@
                         + (criticalEventSection != null ? criticalEventSection : ""));
             }
 
-            Pair<Long, Long> offsets = dumpStackTraces(
-                    tracesFile.getAbsolutePath(), firstPids, nativePids, extraPids, latencyTracker);
-            if (firstPidOffsets != null) {
-                if (offsets == null) {
-                    firstPidOffsets[0] = firstPidOffsets[1] = -1;
-                } else {
-                    firstPidOffsets[0] = offsets.first; // Start offset to the ANR trace file
-                    firstPidOffsets[1] = offsets.second; // End offset to the ANR trace file
-                }
+            long firstPidEndPos = dumpStackTraces(
+                    tracesFile.getAbsolutePath(), firstPids, nativePidsFuture,
+                    extraPidsFuture, latencyTracker);
+            if (firstPidEndOffset != null) {
+                firstPidEndOffset.set(firstPidEndPos);
             }
 
             return tracesFile;
@@ -3558,6 +3540,42 @@
     private static SimpleDateFormat sAnrFileDateFormat;
     static final String ANR_FILE_PREFIX = "anr_";
 
+    private static ArrayList<Integer> getExtraPids(ProcessCpuTracker processCpuTracker,
+            SparseBooleanArray lastPids, AnrLatencyTracker latencyTracker) {
+        if (latencyTracker != null) {
+            latencyTracker.processCpuTrackerMethodsCalled();
+        }
+        ArrayList<Integer> extraPids = new ArrayList<>();
+        processCpuTracker.init();
+        try {
+            Thread.sleep(200);
+        } catch (InterruptedException ignored) {
+        }
+
+        processCpuTracker.update();
+
+        // We'll take the stack crawls of just the top apps using CPU.
+        final int workingStatsNumber = processCpuTracker.countWorkingStats();
+        for (int i = 0; i < workingStatsNumber && extraPids.size() < 5; i++) {
+            ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i);
+            if (lastPids.indexOfKey(stats.pid) >= 0) {
+                if (DEBUG_ANR) {
+                    Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid);
+                }
+
+                extraPids.add(stats.pid);
+            } else {
+                Slog.i(TAG,
+                        "Skipping next CPU consuming process, not a java proc: "
+                        + stats.pid);
+            }
+        }
+        if (latencyTracker != null) {
+            latencyTracker.processCpuTrackerMethodsReturned();
+        }
+        return extraPids;
+    }
+
     private static synchronized File createAnrDumpFile(File tracesDir) throws IOException {
         if (sAnrFileDateFormat == null) {
             sAnrFileDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
@@ -3661,11 +3679,11 @@
 
 
     /**
-     * @return The start/end offset of the trace of the very first PID
+     * @return The end offset of the trace of the very first PID
      */
-    public static Pair<Long, Long> dumpStackTraces(String tracesFile,
-            ArrayList<Integer> firstPids, ArrayList<Integer> nativePids,
-            ArrayList<Integer> extraPids, AnrLatencyTracker latencyTracker) {
+    public static long dumpStackTraces(String tracesFile,
+            ArrayList<Integer> firstPids, Future<ArrayList<Integer>> nativePidsFuture,
+            Future<ArrayList<Integer>> extraPidsFuture, AnrLatencyTracker latencyTracker) {
 
         Slog.i(TAG, "Dumping to " + tracesFile);
 
@@ -3679,7 +3697,6 @@
         // As applications are usually interested with the ANR stack traces, but we can't share with
         // them the stack traces other than their own stacks. So after the very first PID is
         // dumped, remember the current file size.
-        long firstPidStart = -1;
         long firstPidEnd = -1;
 
         // First collect all of the stacks of the most important pids.
@@ -3687,16 +3704,12 @@
             if (latencyTracker != null) {
                 latencyTracker.dumpingFirstPidsStarted();
             }
+
             int num = firstPids.size();
             for (int i = 0; i < num; i++) {
                 final int pid = firstPids.get(i);
                 // We don't copy ANR traces from the system_server intentionally.
                 final boolean firstPid = i == 0 && MY_PID != pid;
-                File tf = null;
-                if (firstPid) {
-                    tf = new File(tracesFile);
-                    firstPidStart = tf.exists() ? tf.length() : 0;
-                }
                 if (latencyTracker != null) {
                     latencyTracker.dumpingPidStarted(pid);
                 }
@@ -3712,11 +3725,11 @@
                 if (remainingTime <= 0) {
                     Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid
                             + "); deadline exceeded.");
-                    return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
+                    return firstPidEnd;
                 }
 
                 if (firstPid) {
-                    firstPidEnd = tf.length();
+                    firstPidEnd = new File(tracesFile).length();
                     // Full latency dump
                     if (latencyTracker != null) {
                         appendtoANRFile(tracesFile,
@@ -3733,6 +3746,10 @@
         }
 
         // Next collect the stacks of the native pids
+        ArrayList<Integer> nativePids = collectPids(nativePidsFuture, "native pids");
+
+        Slog.i(TAG, "dumpStackTraces nativepids=" + nativePids);
+
         if (nativePids != null) {
             if (latencyTracker != null) {
                 latencyTracker.dumpingNativePidsStarted();
@@ -3755,7 +3772,7 @@
                 if (remainingTime <= 0) {
                     Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid +
                         "); deadline exceeded.");
-                    return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
+                    return firstPidEnd;
                 }
 
                 if (DEBUG_ANR) {
@@ -3768,6 +3785,19 @@
         }
 
         // Lastly, dump stacks for all extra PIDs from the CPU tracker.
+        ArrayList<Integer> extraPids = collectPids(extraPidsFuture, "extra pids");
+
+        if (extraPidsFuture != null) {
+            try {
+                extraPids = extraPidsFuture.get();
+            } catch (ExecutionException e) {
+                Slog.w(TAG, "Failed to collect extra pids", e.getCause());
+            } catch (InterruptedException e) {
+                Slog.w(TAG, "Interrupted while collecting extra pids", e);
+            }
+        }
+        Slog.i(TAG, "dumpStackTraces extraPids=" + extraPids);
+
         if (extraPids != null) {
             if (latencyTracker != null) {
                 latencyTracker.dumpingExtraPidsStarted();
@@ -3785,7 +3815,7 @@
                 if (remainingTime <= 0) {
                     Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid +
                             "); deadline exceeded.");
-                    return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
+                    return firstPidEnd;
                 }
 
                 if (DEBUG_ANR) {
@@ -3800,7 +3830,25 @@
         appendtoANRFile(tracesFile, "----- dumping ended at " + SystemClock.uptimeMillis() + "\n");
         Slog.i(TAG, "Done dumping");
 
-        return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
+        return firstPidEnd;
+    }
+
+    private static ArrayList<Integer> collectPids(Future<ArrayList<Integer>> pidsFuture,
+            String logName) {
+
+        ArrayList<Integer> pids = null;
+
+        if (pidsFuture == null) {
+            return pids;
+        }
+        try {
+            pids = pidsFuture.get();
+        } catch (ExecutionException e) {
+            Slog.w(TAG, "Failed to collect " + logName, e.getCause());
+        } catch (InterruptedException e) {
+            Slog.w(TAG, "Interrupted while collecting " + logName , e);
+        }
+        return pids;
     }
 
     @Override
@@ -3891,23 +3939,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, 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, 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) {
@@ -3966,8 +4020,12 @@
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
+        final boolean hasKillAllPermission = checkCallingPermission(
+                android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) == PERMISSION_GRANTED;
+        final int callingUid = Binder.getCallingUid();
+        final int callingAppId = UserHandle.getAppId(callingUid);
 
-        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid,
                 userId, true, ALLOW_FULL_ONLY, "killBackgroundProcesses", null);
         final int[] userIds = mUserController.expandUserId(userId);
 
@@ -3982,7 +4040,7 @@
                                     targetUserId));
                 } catch (RemoteException e) {
                 }
-                if (appId == -1) {
+                if (appId == -1 || (!hasKillAllPermission && appId != callingAppId)) {
                     Slog.w(TAG, "Invalid packageName: " + packageName);
                     return;
                 }
@@ -4001,11 +4059,11 @@
 
     @Override
     public void killAllBackgroundProcesses() {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcesses() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -4041,11 +4099,11 @@
      *                     processes, or {@code -1} to ignore the process state
      */
     void killAllBackgroundProcessesExcept(int minTargetSdk, int maxProcState) {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcessesExcept() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -4444,7 +4502,8 @@
         intent.putExtra(Intent.EXTRA_UID, uid);
         intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
         broadcastIntentLocked(null /* callerApp */, null /* callerPackage */,
-                null /* callerFeatureId */, intent, null /* resolvedType */, null /* resultTo */,
+                null /* callerFeatureId */, intent, null /* resolvedType */,
+                null /* resultToApp */, null /* resultTo */,
                 0 /* resultCode */, null /* resultData */, null /* resultExtras */,
                 null /* requiredPermissions */, null /* excludedPermissions */,
                 null /* excludedPackages */, OP_NONE, null /* bOptions */, false /* ordered */,
@@ -5515,12 +5574,12 @@
     }
 
     @Override
-    public int sendIntentSender(IIntentSender target, IBinder allowlistToken, int code,
-            Intent intent, String resolvedType,
+    public int sendIntentSender(IApplicationThread caller, IIntentSender target,
+            IBinder allowlistToken, int code, Intent intent, String resolvedType,
             IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
         if (target instanceof PendingIntentRecord) {
-            return ((PendingIntentRecord)target).sendWithResult(code, intent, resolvedType,
-                    allowlistToken, finishedReceiver, requiredPermission, options);
+            return ((PendingIntentRecord) target).sendWithResult(caller, code, intent,
+                    resolvedType, allowlistToken, finishedReceiver, requiredPermission, options);
         } else {
             if (intent == null) {
                 // Weird case: someone has given us their own custom IIntentSender, and now
@@ -8340,15 +8399,12 @@
         mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_FOREGROUND_START,
                 Integer.toString(currentUserId), currentUserId);
 
-        // On Automotive, at this point the system user has already been started and unlocked,
-        // and some of the tasks we do here have already been done. So skip those in that case.
-        // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the
-        // headless-user start logic to UserManager-land
-        final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM;
-
-        if (bootingSystemUser) {
-            mSystemServiceManager.onUserStarting(t, currentUserId);
-        }
+        // On Automotive / Headless System User Mode, at this point the system user has already been
+        // started and unlocked, and some of the tasks we do here have already been done. So skip
+        // those in that case. The duplicate system user start is guarded in SystemServiceManager.
+        // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user
+        // start logic to UserManager-land.
+        mUserController.onSystemUserStarting();
 
         synchronized (this) {
             // Only start up encryption-aware persistent apps; once user is
@@ -8378,7 +8434,15 @@
                 t.traceEnd();
             }
 
-            if (bootingSystemUser) {
+            // Some systems - like automotive - will explicitly unlock system user then switch
+            // to a secondary user. Hence, we don't want to send duplicate broadcasts for
+            // the system user here.
+            // TODO(b/242195409): this workaround shouldn't be necessary once we move
+            // the headless-user start logic to UserManager-land.
+            final boolean isBootingSystemUser = (currentUserId == UserHandle.USER_SYSTEM)
+                    && !UserManager.isHeadlessSystemUserMode();
+
+            if (isBootingSystemUser) {
                 t.traceBegin("startHomeOnAllDisplays");
                 mAtmInternal.startHomeOnAllDisplays(currentUserId, "systemReady");
                 t.traceEnd();
@@ -8389,7 +8453,7 @@
             t.traceEnd();
 
 
-            if (bootingSystemUser) {
+            if (isBootingSystemUser) {
                 t.traceBegin("sendUserStartBroadcast");
                 final int callingUid = Binder.getCallingUid();
                 final int callingPid = Binder.getCallingPid();
@@ -8430,7 +8494,7 @@
             mAtmInternal.resumeTopActivities(false /* scheduleIdle */);
             t.traceEnd();
 
-            if (bootingSystemUser) {
+            if (isBootingSystemUser) {
                 t.traceBegin("sendUserSwitchBroadcasts");
                 mUserController.sendUserSwitchBroadcasts(-1, currentUserId);
                 t.traceEnd();
@@ -11051,11 +11115,13 @@
         final long pss;
         final long swapPss;
         final long mRss;
-        final int id;
+        final int id; // pid
+        final int userId;
         final boolean hasActivities;
         ArrayList<MemItem> subitems;
 
         MemItem(String label, String shortLabel, long pss, long swapPss, long rss, int id,
+                @UserIdInt int userId,
                 boolean hasActivities) {
             this.isProc = true;
             this.label = label;
@@ -11064,6 +11130,7 @@
             this.swapPss = swapPss;
             this.mRss = rss;
             this.id = id;
+            this.userId = userId;
             this.hasActivities = hasActivities;
         }
 
@@ -11075,6 +11142,7 @@
             this.swapPss = swapPss;
             this.mRss = rss;
             this.id = id;
+            this.userId = UserHandle.USER_SYSTEM;
             this.hasActivities = false;
         }
     }
@@ -11109,8 +11177,9 @@
                     pw.printf("%s%s: %-60s (%s in swap)\n", prefix, stringifyKBSize(mi.pss),
                             mi.label, stringifyKBSize(mi.swapPss));
                 } else {
-                    pw.printf("%s%s: %s\n", prefix, stringifyKBSize(dumpPss ? mi.pss : mi.mRss),
-                            mi.label);
+                    pw.printf("%s%s: %s%s\n", prefix, stringifyKBSize(dumpPss ? mi.pss : mi.mRss),
+                            mi.label,
+                            mi.userId != UserHandle.USER_SYSTEM ? " (user " + mi.userId + ")" : "");
                 }
             } else if (mi.isProc) {
                 pw.print("proc,"); pw.print(tag); pw.print(","); pw.print(mi.shortLabel);
@@ -11602,7 +11671,7 @@
                     ss[INDEX_TOTAL_MEMTRACK_GL] += memtrackGl;
                     MemItem pssItem = new MemItem(r.processName + " (pid " + pid +
                             (hasActivities ? " / activities)" : ")"), r.processName, myTotalPss,
-                            myTotalSwapPss, myTotalRss, pid, hasActivities);
+                            myTotalSwapPss, myTotalRss, pid, r.userId, hasActivities);
                     procMems.add(pssItem);
                     procMemsMap.put(pid, pssItem);
 
@@ -11699,7 +11768,7 @@
 
                     MemItem pssItem = new MemItem(st.name + " (pid " + st.pid + ")",
                             st.name, myTotalPss, info.getSummaryTotalSwapPss(), myTotalRss,
-                            st.pid, false);
+                            st.pid, UserHandle.getUserId(st.uid), false);
                     procMems.add(pssItem);
 
                     ss[INDEX_NATIVE_PSS] += info.nativePss;
@@ -12245,7 +12314,7 @@
                 ss[INDEX_TOTAL_RSS] += myTotalRss;
                 MemItem pssItem = new MemItem(r.processName + " (pid " + pid +
                         (hasActivities ? " / activities)" : ")"), r.processName, myTotalPss,
-                        myTotalSwapPss, myTotalRss, pid, hasActivities);
+                        myTotalSwapPss, myTotalRss, pid, r.userId, hasActivities);
                 procMems.add(pssItem);
                 procMemsMap.put(pid, pssItem);
 
@@ -12333,7 +12402,7 @@
 
                     MemItem pssItem = new MemItem(st.name + " (pid " + st.pid + ")",
                             st.name, myTotalPss, info.getSummaryTotalSwapPss(), myTotalRss,
-                            st.pid, false);
+                            st.pid, UserHandle.getUserId(st.uid), false);
                     procMems.add(pssItem);
 
                     ss[INDEX_NATIVE_PSS] += info.nativePss;
@@ -13371,27 +13440,19 @@
         int callingPid;
         boolean instantApp;
         synchronized(this) {
-            if (caller != null) {
-                callerApp = getRecordForAppLOSP(caller);
-                if (callerApp == null) {
-                    throw new SecurityException(
-                            "Unable to find app for caller " + caller
-                            + " (pid=" + Binder.getCallingPid()
-                            + ") when registering receiver " + receiver);
-                }
-                if (callerApp.info.uid != SYSTEM_UID
-                        && !callerApp.getPkgList().containsKey(callerPackage)
-                        && !"android".equals(callerPackage)) {
-                    throw new SecurityException("Given caller package " + callerPackage
-                            + " is not running in process " + callerApp);
-                }
-                callingUid = callerApp.info.uid;
-                callingPid = callerApp.getPid();
-            } else {
-                callerPackage = null;
-                callingUid = Binder.getCallingUid();
-                callingPid = Binder.getCallingPid();
+            callerApp = getRecordForAppLOSP(caller);
+            if (callerApp == null) {
+                Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
+                return null;
             }
+            if (callerApp.info.uid != SYSTEM_UID
+                    && !callerApp.getPkgList().containsKey(callerPackage)
+                    && !"android".equals(callerPackage)) {
+                throw new SecurityException("Given caller package " + callerPackage
+                        + " is not running in process " + callerApp);
+            }
+            callingUid = callerApp.info.uid;
+            callingPid = callerApp.getPid();
 
             instantApp = isInstantApp(callerApp, callerPackage, callingUid);
             userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
@@ -13449,9 +13510,19 @@
             // Don't enforce the flag check if we're EITHER registering for only protected
             // broadcasts, or the receiver is null (a sticky broadcast). Sticky broadcasts should
             // not be used generally, so we will be marking them as exported by default
-            final boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled(
+            boolean requireExplicitFlagForDynamicReceivers = CompatChanges.isChangeEnabled(
                     DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid)
                     && mConstants.mEnforceReceiverExportedFlagRequirement;
+            // STOPSHIP(b/259139792): Allow apps that are currently targeting U and in process of
+            // updating their receivers to be exempt from this requirement until their receivers
+            // are flagged.
+            if (requireExplicitFlagForDynamicReceivers) {
+                if ("com.google.android.apps.messaging".equals(callerPackage)) {
+                    // Note, a versionCode check for this package is not performed because it could
+                    // cause breakage with a subsequent update outside the system image.
+                    requireExplicitFlagForDynamicReceivers = false;
+                }
+            }
             if (!onlyProtectedBroadcasts) {
                 if (receiver == null && !explicitExportStateDefined) {
                     // sticky broadcast, no flag specified (flag isn't required)
@@ -13586,8 +13657,8 @@
                     BroadcastQueue queue = broadcastQueueForIntent(intent);
                     BroadcastRecord r = new BroadcastRecord(queue, intent, null,
                             null, null, -1, -1, false, null, null, null, null, OP_NONE, null,
-                            receivers, null, 0, null, null, false, true, true, -1, false, null,
-                            false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
+                            receivers, null, null, 0, null, null, false, true, true, -1, false,
+                            null, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
                             null /* filterExtrasForReceiver */);
                     queue.enqueueBroadcastLocked(r);
                 }
@@ -13833,6 +13904,25 @@
         }
     }
 
+    // Apply permission policy around the use of specific broadcast options
+    void enforceBroadcastOptionPermissionsInternal(@Nullable Bundle options, int callingUid) {
+        if (options != null && callingUid != Process.SYSTEM_UID) {
+            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+                if (DEBUG_BROADCAST_LIGHT) {
+                    Slog.w(TAG, "Non-system caller " + callingUid
+                            + " may not flag broadcast as alarm");
+                }
+                throw new SecurityException(
+                        "Non-system callers may not flag broadcasts as alarm");
+            }
+            if (options.containsKey(ComponentOptions.KEY_INTERACTIVE)) {
+                enforceCallingPermission(
+                        android.Manifest.permission.COMPONENT_OPTION_INTERACTIVE,
+                        "setInteractive");
+            }
+        }
+    }
+
     @GuardedBy("this")
     final int broadcastIntentLocked(ProcessRecord callerApp,
             String callerPackage, String callerFeatureId, Intent intent, String resolvedType,
@@ -13842,9 +13932,9 @@
             boolean sticky, int callingPid,
             int callingUid, int realCallingUid, int realCallingPid, int userId) {
         return broadcastIntentLocked(callerApp, callerPackage, callerFeatureId, intent,
-                resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions,
-                excludedPermissions, excludedPackages, appOp, bOptions, ordered, sticky, callingPid,
-                callingUid, realCallingUid, realCallingPid, userId,
+                resolvedType, null, resultTo, resultCode, resultData, resultExtras,
+                requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
+                ordered, sticky, callingPid, callingUid, realCallingUid, realCallingPid, userId,
                 false /* allowBackgroundActivityStarts */,
                 null /* tokenNeededForBackgroundActivityStarts */,
                 null /* broadcastAllowList */, null /* filterExtrasForReceiver */);
@@ -13853,7 +13943,7 @@
     @GuardedBy("this")
     final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage,
             @Nullable String callerFeatureId, Intent intent, String resolvedType,
-            IIntentReceiver resultTo, int resultCode, String resultData,
+            ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
             Bundle resultExtras, String[] requiredPermissions,
             String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
             boolean ordered, boolean sticky, int callingPid, int callingUid,
@@ -13862,6 +13952,44 @@
             @Nullable IBinder backgroundActivityStartsToken,
             @Nullable int[] broadcastAllowList,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
+        final int cookie = BroadcastQueue.traceBegin("broadcastIntentLockedTraced");
+        final int res = broadcastIntentLockedTraced(callerApp, callerPackage, callerFeatureId,
+                intent, resolvedType, resultToApp, resultTo, resultCode, resultData, resultExtras,
+                requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
+                ordered, sticky, callingPid, callingUid, realCallingUid, realCallingPid, userId,
+                allowBackgroundActivityStarts, backgroundActivityStartsToken, broadcastAllowList,
+                filterExtrasForReceiver);
+        BroadcastQueue.traceEnd(cookie);
+        return res;
+    }
+
+    @GuardedBy("this")
+    final int broadcastIntentLockedTraced(ProcessRecord callerApp, String callerPackage,
+            @Nullable String callerFeatureId, Intent intent, String resolvedType,
+            ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
+            Bundle resultExtras, String[] requiredPermissions,
+            String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
+            boolean ordered, boolean sticky, int callingPid, int callingUid,
+            int realCallingUid, int realCallingPid, int userId,
+            boolean allowBackgroundActivityStarts,
+            @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
+                Slog.wtf(TAG, "Sending broadcast " + intent.getAction()
+                        + " with resultTo requires resultToApp", new Throwable());
+            } else {
+                // If not a BinderProxy above, then resultTo is an in-process
+                // receiver, so splice in system_server process
+                resultToApp = getProcessRecordLocked("system", SYSTEM_UID);
+            }
+        }
+
         intent = new Intent(intent);
 
         final boolean callerInstantApp = isInstantApp(callerApp, callerPackage, callingUid);
@@ -13887,7 +14015,7 @@
         if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
                 (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent
                 + " ordered=" + ordered + " userid=" + userId);
-        if ((resultTo != null) && !ordered) {
+        if ((resultTo != null) && !ordered && !mEnableModernQueue) {
             Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!");
         }
 
@@ -14398,6 +14526,7 @@
         }
 
         // Figure out who all will receive this broadcast.
+        final int cookie = BroadcastQueue.traceBegin("queryReceivers");
         List receivers = null;
         List<BroadcastFilter> registeredReceivers = null;
         // Need to resolve the intent to interested receivers...
@@ -14428,6 +14557,7 @@
                         resolvedType, false /*defaultOnly*/, userId);
             }
         }
+        BroadcastQueue.traceEnd(cookie);
 
         final boolean replacePending =
                 (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
@@ -14449,10 +14579,12 @@
         filterNonExportedComponents(intent, callingUid, registeredReceivers,
                 mPlatformCompat, callerPackage);
         int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
-        if (!ordered && NR > 0) {
+        if (!ordered && NR > 0 && !mEnableModernQueue) {
             // If we are not serializing this broadcast, then send the
             // registered receivers separately so they don't wait for the
-            // components to be launched.
+            // components to be launched. We don't do this split for the modern
+            // queue because delivery to registered receivers isn't blocked
+            // behind manifest receivers.
             if (isCallerSystem) {
                 checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
                         isProtectedBroadcast, registeredReceivers);
@@ -14461,8 +14593,8 @@
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
-                    registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,
-                    sticky, false, userId, allowBackgroundActivityStarts,
+                    registeredReceivers, resultToApp, resultTo, resultCode, resultData,
+                    resultExtras, ordered, sticky, false, userId, allowBackgroundActivityStarts,
                     backgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r);
             queue.enqueueBroadcastLocked(r);
@@ -14555,7 +14687,7 @@
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
-                    receivers, resultTo, resultCode, resultData, resultExtras,
+                    receivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
                     ordered, sticky, false, userId, allowBackgroundActivityStarts,
                     backgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
 
@@ -14610,6 +14742,15 @@
         mCurBroadcastStats.addBackgroundCheckViolation(action, targetPackage);
     }
 
+    final void notifyBroadcastFinishedLocked(@NonNull BroadcastRecord original) {
+        final ApplicationInfo info = original.callerApp != null ? original.callerApp.info : null;
+        final String callerPackage = info != null ? info.packageName : original.callerPackage;
+        if (callerPackage != null) {
+            mHandler.obtainMessage(ActivityManagerService.DISPATCH_SENDING_BROADCAST_EVENT,
+                    original.callingUid, 0, callerPackage).sendToTarget();
+        }
+    }
+
     final Intent verifyBroadcastLocked(Intent intent) {
         // Refuse possible leaked file descriptors
         if (intent != null && intent.hasFileDescriptors() == true) {
@@ -14680,26 +14821,20 @@
             final int callingPid = Binder.getCallingPid();
             final int callingUid = Binder.getCallingUid();
 
-            // Non-system callers can't declare that a broadcast is alarm-related.
-            // The PendingIntent invocation case is handled in PendingIntentRecord.
-            if (bOptions != null && callingUid != SYSTEM_UID) {
-                if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
-                    if (DEBUG_BROADCAST) {
-                        Slog.w(TAG, "Non-system caller " + callingUid
-                                + " may not flag broadcast as alarm-related");
-                    }
-                    throw new SecurityException(
-                            "Non-system callers may not flag broadcasts as alarm-related");
-                }
-            }
+            // We're delivering the result to the caller
+            final ProcessRecord resultToApp = callerApp;
+
+            // Permission regimes around sender-supplied broadcast options.
+            enforceBroadcastOptionPermissionsInternal(bOptions, callingUid);
 
             final long origId = Binder.clearCallingIdentity();
             try {
                 return broadcastIntentLocked(callerApp,
                         callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
-                        intent, resolvedType, resultTo, resultCode, resultData, resultExtras,
-                        requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
-                        serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId);
+                        intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
+                        resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
+                        appOp, bOptions, serialized, sticky, callingPid, callingUid, callingUid,
+                        callingPid, userId, false, null, null, null);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
@@ -14709,11 +14844,10 @@
     // Not the binder call surface
     int broadcastIntentInPackage(String packageName, @Nullable String featureId, int uid,
             int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
-            IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
-            String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-            int userId, boolean allowBackgroundActivityStarts,
-            @Nullable IBinder backgroundActivityStartsToken,
-            @Nullable int[] broadcastAllowList) {
+            ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode,
+            String resultData, Bundle resultExtras, String requiredPermission, Bundle bOptions,
+            boolean serialized, boolean sticky, int userId, boolean allowBackgroundActivityStarts,
+            @Nullable IBinder backgroundActivityStartsToken, @Nullable int[] broadcastAllowList) {
         synchronized(this) {
             intent = verifyBroadcastLocked(intent);
 
@@ -14722,9 +14856,9 @@
                     : new String[] {requiredPermission};
             try {
                 return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
-                        resultTo, resultCode, resultData, resultExtras, requiredPermissions, null,
-                        null, OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid,
-                        realCallingPid, userId, allowBackgroundActivityStarts,
+                        resultToApp, resultTo, resultCode, resultData, resultExtras,
+                        requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
+                        uid, realCallingUid, realCallingPid, userId, allowBackgroundActivityStarts,
                         backgroundActivityStartsToken, broadcastAllowList,
                         null /* filterExtrasForReceiver */);
             } finally {
@@ -16747,7 +16881,6 @@
             mAtmInternal.onUserStopped(userId);
             // Clean up various services by removing the user
             mBatteryStatsService.onUserRemoved(userId);
-            mUserController.onUserRemoved(userId);
         }
 
         @Override
@@ -16881,6 +17014,16 @@
             return mSystemReady;
         }
 
+        @Override
+        public boolean isModernQueueEnabled() {
+            return mEnableModernQueue;
+        }
+
+        @Override
+        public void enforceBroadcastOptionsPermissions(Bundle options, int callingUid) {
+            enforceBroadcastOptionPermissionsInternal(options, callingUid);
+        }
+
         /**
          * Returns package name by pid.
          */
@@ -17253,16 +17396,18 @@
         @Override
         public int broadcastIntentInPackage(String packageName, @Nullable String featureId, int uid,
                 int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
-                IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
-                String requiredPermission, Bundle bOptions, boolean serialized, boolean sticky,
-                int userId, boolean allowBackgroundActivityStarts,
+                IApplicationThread resultToThread, IIntentReceiver resultTo, int resultCode,
+                String resultData, Bundle resultExtras, String requiredPermission, Bundle bOptions,
+                boolean serialized, boolean sticky, int userId,
+                boolean allowBackgroundActivityStarts,
                 @Nullable IBinder backgroundActivityStartsToken,
                 @Nullable int[] broadcastAllowList) {
             synchronized (ActivityManagerService.this) {
+                final ProcessRecord resultToApp = getRecordForAppLOSP(resultToThread);
                 return ActivityManagerService.this.broadcastIntentInPackage(packageName, featureId,
-                        uid, realCallingUid, realCallingPid, intent, resolvedType, resultTo,
-                        resultCode, resultData, resultExtras, requiredPermission, bOptions,
-                        serialized, sticky, userId, allowBackgroundActivityStarts,
+                        uid, realCallingUid, realCallingPid, intent, resolvedType, resultToApp,
+                        resultTo, resultCode, resultData, resultExtras, requiredPermission,
+                        bOptions, serialized, sticky, userId, allowBackgroundActivityStarts,
                         backgroundActivityStartsToken, broadcastAllowList);
             }
         }
@@ -17283,8 +17428,9 @@
                 try {
                     return ActivityManagerService.this.broadcastIntentLocked(null /*callerApp*/,
                             null /*callerPackage*/, null /*callingFeatureId*/, intent,
-                            null /*resolvedType*/, resultTo, 0 /*resultCode*/, null /*resultData*/,
-                            null /*resultExtras*/, requiredPermissions,
+                            null /* resolvedType */, null /* resultToApp */, resultTo,
+                            0 /* resultCode */, null /* resultData */,
+                            null /* resultExtras */, requiredPermissions,
                             null /*excludedPermissions*/, null /*excludedPackages*/,
                             AppOpsManager.OP_NONE, bOptions /*options*/, serialized,
                             false /*sticky*/, callingPid, callingUid, callingUid, callingPid,
@@ -17299,6 +17445,21 @@
         }
 
         @Override
+        public int broadcastIntentWithCallback(Intent intent,
+                IIntentReceiver resultTo,
+                String[] requiredPermissions,
+                int userId, int[] appIdAllowList,
+                @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
+                @Nullable Bundle bOptions) {
+            // Sending broadcasts with a finish callback without the need for the broadcasts
+            // delivery to be serialized is only supported by modern queue. So, when modern
+            // queue is disabled, we continue to send broadcasts in a serialized fashion.
+            final boolean serialized = !isModernQueueEnabled();
+            return broadcastIntent(intent, resultTo, requiredPermissions, serialized, userId,
+                    appIdAllowList, filterExtrasForReceiver, bOptions);
+        }
+
+        @Override
         public ComponentName startServiceInPackage(int uid, Intent service, String resolvedType,
                 boolean fgRequired, String callingPackage, @Nullable String callingFeatureId,
                 int userId, boolean allowBackgroundActivityStarts,
@@ -17897,7 +18058,7 @@
         public int sendIntentSender(IIntentSender target, IBinder allowlistToken, int code,
                 Intent intent, String resolvedType,
                 IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
-            return ActivityManagerService.this.sendIntentSender(target, allowlistToken, code,
+            return ActivityManagerService.this.sendIntentSender(null, target, allowlistToken, code,
                     intent, resolvedType, finishedReceiver, requiredPermission, options);
         }
 
@@ -17965,6 +18126,30 @@
             mUidObserverController.register(observer, which, cutpoint, callingPackage,
                     Binder.getCallingUid());
         }
+
+        @Override
+        public boolean startForegroundServiceDelegate(
+                @NonNull ForegroundServiceDelegationOptions options,
+                @Nullable ServiceConnection connection) {
+            synchronized (ActivityManagerService.this) {
+                return mServices.startForegroundServiceDelegateLocked(options, connection);
+            }
+        }
+
+        @Override
+        public void stopForegroundServiceDelegate(
+                @NonNull ForegroundServiceDelegationOptions options) {
+            synchronized (ActivityManagerService.this) {
+                mServices.stopForegroundServiceDelegateLocked(options);
+            }
+        }
+
+        @Override
+        public void stopForegroundServiceDelegate(@NonNull ServiceConnection connection) {
+            synchronized (ActivityManagerService.this) {
+                mServices.stopForegroundServiceDelegateLocked(connection);
+            }
+        }
     }
 
     long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
@@ -18086,6 +18271,7 @@
 
     public void waitForBroadcastIdle(@Nullable PrintWriter pw) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastIdle()");
+        BroadcastLoopers.waitForIdle(pw);
         for (BroadcastQueue queue : mBroadcastQueues) {
             queue.waitForIdle(pw);
         }
@@ -18097,6 +18283,7 @@
 
     public void waitForBroadcastBarrier(@Nullable PrintWriter pw) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
+        BroadcastLoopers.waitForIdle(pw);
         for (BroadcastQueue queue : mBroadcastQueues) {
             queue.waitForBarrier(pw);
         }
@@ -18152,6 +18339,59 @@
     }
 
     /**
+     * Start/stop foreground service delegate on a app's process.
+     * This interface is intended for the shell command to use.
+     */
+    void setForegroundServiceDelegate(String packageName, int uid, boolean isStart,
+            @ForegroundServiceDelegationOptions.DelegationService int delegateService,
+            String clientInstanceName) {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != SYSTEM_UID && callingUid != ROOT_UID && callingUid != SHELL_UID) {
+            throw new SecurityException(
+                    "No permission to start/stop foreground service delegate");
+        }
+        final long callingId = Binder.clearCallingIdentity();
+        try {
+            boolean foundPid = false;
+            synchronized (this) {
+                ArrayList<ForegroundServiceDelegationOptions> delegates = new ArrayList<>();
+                synchronized (mPidsSelfLocked) {
+                    for (int i = 0; i < mPidsSelfLocked.size(); i++) {
+                        final ProcessRecord p = mPidsSelfLocked.valueAt(i);
+                        final IApplicationThread thread = p.getThread();
+                        if (p.uid == uid && thread != null) {
+                            foundPid = true;
+                            int pid = mPidsSelfLocked.keyAt(i);
+                            ForegroundServiceDelegationOptions options =
+                                    new ForegroundServiceDelegationOptions(pid, uid, packageName,
+                                            null /* clientAppThread */,
+                                            false /* isSticky */,
+                                            clientInstanceName, 0 /* foregroundServiceType */,
+                                            delegateService);
+                            delegates.add(options);
+                        }
+                    }
+                }
+                for (int i = delegates.size() - 1; i >= 0; i--) {
+                    final ForegroundServiceDelegationOptions options = delegates.get(i);
+                    if (isStart) {
+                        ((ActivityManagerLocal) mInternal).startForegroundServiceDelegate(options,
+                                null /* connection */);
+                    } else {
+                        ((ActivityManagerLocal) mInternal).stopForegroundServiceDelegate(options);
+                    }
+                }
+            }
+            if (!foundPid) {
+                Slog.e(TAG, "setForegroundServiceDelegate can not find process for packageName:"
+                        + packageName + " uid:" + uid);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(callingId);
+        }
+    }
+
+    /**
      * Force the settings cache to be loaded
      */
     void refreshSettingsCache() {
@@ -18755,19 +18995,20 @@
         }
 
         @Override
-        public SyncNotedAppOp startProxyOperation(int code,
+        public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
                 boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
                 boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
                 @AttributionFlags int proxiedAttributionFlags, int attributionChainId,
-                @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean,
-                        Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) {
+                @NonNull UndecFunction<IBinder, Integer, AttributionSource,
+                        Boolean, Boolean, String, Boolean, Boolean, Integer, Integer, Integer,
+                        SyncNotedAppOp> superImpl) {
             if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) {
                 final int shellUid = UserHandle.getUid(UserHandle.getUserId(
                         attributionSource.getUid()), Process.SHELL_UID);
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    return superImpl.apply(code, new AttributionSource(shellUid,
+                    return superImpl.apply(clientId, code, new AttributionSource(shellUid,
                             "com.android.shell", attributionSource.getAttributionTag(),
                             attributionSource.getToken(), attributionSource.getNext()),
                             startIfModeDefault, shouldCollectAsyncNotedOp, message,
@@ -18777,21 +19018,22 @@
                     Binder.restoreCallingIdentity(identity);
                 }
             }
-            return superImpl.apply(code, attributionSource, startIfModeDefault,
+            return superImpl.apply(clientId, code, attributionSource, startIfModeDefault,
                     shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation,
                     proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
         }
 
         @Override
-        public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource,
-                boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource,
-                        Boolean, Void> superImpl) {
+        public void finishProxyOperation(@NonNull IBinder clientId, int code,
+                @NonNull AttributionSource attributionSource, boolean skipProxyOperation,
+                @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean,
+                        Void> superImpl) {
             if (attributionSource.getUid() == mTargetUid && isTargetOp(code)) {
                 final int shellUid = UserHandle.getUid(UserHandle.getUserId(
                         attributionSource.getUid()), Process.SHELL_UID);
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    superImpl.apply(code, new AttributionSource(shellUid,
+                    superImpl.apply(clientId, code, new AttributionSource(shellUid,
                             "com.android.shell", attributionSource.getAttributionTag(),
                             attributionSource.getToken(), attributionSource.getNext()),
                             skipProxyOperation);
@@ -18799,7 +19041,7 @@
                     Binder.restoreCallingIdentity(identity);
                 }
             }
-            superImpl.apply(code, attributionSource, skipProxyOperation);
+            superImpl.apply(clientId, code, attributionSource, skipProxyOperation);
         }
 
         private boolean isTargetOp(int code) {
@@ -18901,6 +19143,10 @@
         return mOomAdjuster.mCachedAppOptimizer.useFreezer();
     }
 
+    public boolean isAppFreezerExemptInstPkg() {
+        return mOomAdjuster.mCachedAppOptimizer.freezerExemptInstPkg();
+    }
+
     /**
      * Resets the state of the {@link com.android.server.am.AppErrors} instance.
      * This is intended for testing within the CTS only and is protected by
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index e4f947d..10f5a36 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -369,6 +369,8 @@
                     return runResetDropboxRateLimiter();
                 case "list-secondary-displays-for-starting-users":
                     return runListSecondaryDisplaysForStartingUsers(pw);
+                case "set-foreground-service-delegate":
+                    return runSetForegroundServiceDelegate(pw);
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -3592,6 +3594,45 @@
         return 0;
     }
 
+    int runSetForegroundServiceDelegate(PrintWriter pw) throws RemoteException {
+        int userId = UserHandle.USER_CURRENT;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            if (opt.equals("--user")) {
+                userId = UserHandle.parseUserArg(getNextArgRequired());
+            } else {
+                getErrPrintWriter().println("Error: Unknown option: " + opt);
+                return -1;
+            }
+        }
+        final String packageName = getNextArgRequired();
+        final String action = getNextArgRequired();
+        boolean isStart = true;
+        if ("start".equals(action)) {
+            isStart = true;
+        } else if ("stop".equals(action)) {
+            isStart = false;
+        } else {
+            pw.println("Error: action is either start or stop");
+            return -1;
+        }
+
+        int uid = INVALID_UID;
+        try {
+            final PackageManager pm = mInternal.mContext.getPackageManager();
+            uid = pm.getPackageUidAsUser(packageName,
+                    PackageManager.PackageInfoFlags.of(MATCH_ANY_USER), userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            pw.println("Error: userId:" + userId + " package:" + packageName + " is not found");
+            return -1;
+        }
+        mInternal.setForegroundServiceDelegate(packageName, uid, isStart,
+                ForegroundServiceDelegationOptions.DELEGATION_SERVICE_SPECIAL_USE,
+                "FgsDelegate");
+        return 0;
+    }
+
     int runResetDropboxRateLimiter() throws RemoteException {
         mInternal.resetDropboxRateLimiter();
         return 0;
@@ -3968,6 +4009,8 @@
             pw.println("  list-secondary-displays-for-starting-users");
             pw.println("         Lists the id of displays that can be used to start users on "
                     + "background.");
+            pw.println("  set-foreground-service-delegate [--user <USER_ID>] <PACKAGE> start|stop");
+            pw.println("         Start/stop an app's foreground service delegate.");
             pw.println();
             Intent.printIntentArgsHelp(pw, "");
         }
diff --git a/services/core/java/com/android/server/am/AnrHelper.java b/services/core/java/com/android/server/am/AnrHelper.java
index 6de4118..71c80ea 100644
--- a/services/core/java/com/android/server/am/AnrHelper.java
+++ b/services/core/java/com/android/server/am/AnrHelper.java
@@ -25,10 +25,15 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.TimeoutRecord;
 import com.android.server.wm.WindowProcessController;
 
 import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -51,6 +56,14 @@
      */
     private static final long CONSECUTIVE_ANR_TIME_MS = TimeUnit.MINUTES.toMillis(2);
 
+    /**
+     * The keep alive time for the threads in the helper threadpool executor
+    */
+    private static final int AUX_THREAD_KEEP_ALIVE_SECOND = 10;
+
+    private static final ThreadFactory sDefaultThreadFactory =  r ->
+            new Thread(r, "AnrAuxiliaryTaskExecutor");
+
     @GuardedBy("mAnrRecords")
     private final ArrayList<AnrRecord> mAnrRecords = new ArrayList<>();
     private final AtomicBoolean mRunning = new AtomicBoolean(false);
@@ -66,8 +79,18 @@
     @GuardedBy("mAnrRecords")
     private int mProcessingPid = -1;
 
+    private final ExecutorService mAuxiliaryTaskExecutor;
+
     AnrHelper(final ActivityManagerService service) {
+        this(service, new ThreadPoolExecutor(/* corePoolSize= */ 0, /* maximumPoolSize= */ 1,
+                /* keepAliveTime= */ AUX_THREAD_KEEP_ALIVE_SECOND, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(), sDefaultThreadFactory));
+    }
+
+    @VisibleForTesting
+    AnrHelper(ActivityManagerService service, ExecutorService auxExecutor) {
         mService = service;
+        mAuxiliaryTaskExecutor = auxExecutor;
     }
 
     void appNotResponding(ProcessRecord anrProcess, TimeoutRecord timeoutRecord) {
@@ -108,7 +131,8 @@
                 }
                 timeoutRecord.mLatencyTracker.anrRecordPlacingOnQueueWithSize(mAnrRecords.size());
                 mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName, aInfo,
-                        parentShortComponentName, parentProcess, aboveSystem, timeoutRecord));
+                        parentShortComponentName, parentProcess, aboveSystem,
+                        mAuxiliaryTaskExecutor, timeoutRecord));
             }
             startAnrConsumerIfNeeded();
         } finally {
@@ -204,11 +228,12 @@
         final ApplicationInfo mAppInfo;
         final WindowProcessController mParentProcess;
         final boolean mAboveSystem;
+        final ExecutorService mAuxiliaryTaskExecutor;
         final long mTimestamp = SystemClock.uptimeMillis();
         AnrRecord(ProcessRecord anrProcess, String activityShortComponentName,
                 ApplicationInfo aInfo, String parentShortComponentName,
                 WindowProcessController parentProcess, boolean aboveSystem,
-                TimeoutRecord timeoutRecord) {
+                ExecutorService auxiliaryTaskExecutor, TimeoutRecord timeoutRecord) {
             mApp = anrProcess;
             mPid = anrProcess.mPid;
             mActivityShortComponentName = activityShortComponentName;
@@ -217,6 +242,7 @@
             mAppInfo = aInfo;
             mParentProcess = parentProcess;
             mAboveSystem = aboveSystem;
+            mAuxiliaryTaskExecutor = auxiliaryTaskExecutor;
         }
 
         void appNotResponding(boolean onlyDumpSelf) {
@@ -224,7 +250,7 @@
                 mTimeoutRecord.mLatencyTracker.anrProcessingStarted();
                 mApp.mErrorState.appNotResponding(mActivityShortComponentName, mAppInfo,
                         mParentShortComponentName, mParentProcess, mAboveSystem,
-                        mTimeoutRecord, onlyDumpSelf);
+                        mTimeoutRecord, mAuxiliaryTaskExecutor, onlyDumpSelf);
             } finally {
                 mTimeoutRecord.mLatencyTracker.anrProcessingEnded();
             }
diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java
index 50515cd..1f98aba 100644
--- a/services/core/java/com/android/server/am/AppFGSTracker.java
+++ b/services/core/java/com/android/server/am/AppFGSTracker.java
@@ -16,10 +16,10 @@
 
 package com.android.server.am;
 
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPES_MAX_INDEX;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
-import static android.content.pm.ServiceInfo.NUM_OF_FOREGROUND_SERVICE_TYPES;
 import static android.content.pm.ServiceInfo.foregroundServiceTypeToLabel;
 import static android.os.PowerExemptionManager.REASON_DENIED;
 
@@ -645,7 +645,7 @@
 
         PackageDurations(int uid, String packageName,
                 MaxTrackingDurationConfig maxTrackingDurationConfig, AppFGSTracker tracker) {
-            super(uid, packageName, NUM_OF_FOREGROUND_SERVICE_TYPES + 1, TAG,
+            super(uid, packageName, FOREGROUND_SERVICE_TYPES_MAX_INDEX + 1, TAG,
                     maxTrackingDurationConfig);
             mEvents[DEFAULT_INDEX] = new LinkedList<>();
             mTracker = tracker;
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index e0690bf..6abf6d8 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -146,8 +146,6 @@
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -157,6 +155,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.function.TriConsumer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.AppStateTracker;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
@@ -2784,6 +2784,37 @@
      */
     @ReasonCode
     int getBackgroundRestrictionExemptionReason(int uid) {
+        @ReasonCode int reason = getPotentialSystemExemptionReason(uid);
+        if (reason != REASON_DENIED) {
+            return reason;
+        }
+        final String[] packages = mInjector.getPackageManager().getPackagesForUid(uid);
+        if (packages != null) {
+            // Check each packages to see if any of them is in the "fixed" exemption cases.
+            for (String pkg : packages) {
+                reason = getPotentialSystemExemptionReason(uid, pkg);
+                if (reason != REASON_DENIED) {
+                    return reason;
+                }
+            }
+            // Loop the packages again, and check the user-configurable exemptions.
+            for (String pkg : packages) {
+                reason = getPotentialUserAllowedExemptionReason(uid, pkg);
+                if (reason != REASON_DENIED) {
+                    return reason;
+                }
+            }
+        }
+        return REASON_DENIED;
+    }
+
+    /**
+     * @param uid The uid to check.
+     * @return The potential exemption reason of the given uid. The caller must decide
+     * whether or not it should be exempted.
+     */
+    @ReasonCode
+    int getPotentialSystemExemptionReason(int uid) {
         if (UserHandle.isCore(uid)) {
             return REASON_SYSTEM_UID;
         }
@@ -2811,37 +2842,51 @@
         } else if (uidProcState <= PROCESS_STATE_PERSISTENT_UI) {
             return REASON_PROC_STATE_PERSISTENT_UI;
         }
-        final String[] packages = mInjector.getPackageManager().getPackagesForUid(uid);
-        if (packages != null) {
-            final AppOpsManager appOpsManager = mInjector.getAppOpsManager();
-            final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
-            final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
-            // Check each packages to see if any of them is in the "fixed" exemption cases.
-            for (String pkg : packages) {
-                if (isSystemModule(pkg)) {
-                    return REASON_SYSTEM_MODULE;
-                } else if (isCarrierApp(pkg)) {
-                    return REASON_CARRIER_PRIVILEGED_APP;
-                } else if (isExemptedFromSysConfig(pkg)) {
-                    return REASON_SYSTEM_ALLOW_LISTED;
-                } else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) {
-                    return REASON_SYSTEM_ALLOW_LISTED;
-                } else if (pm.isPackageStateProtected(pkg, userId)) {
-                    return REASON_DPO_PROTECTED_APP;
-                } else if (appStandbyInternal.isActiveDeviceAdmin(pkg, userId)) {
-                    return REASON_ACTIVE_DEVICE_ADMIN;
-                }
-            }
-            // Loop the packages again, and check the user-configurable exemptions.
-            for (String pkg : packages) {
-                if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN,
-                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
-                    return REASON_OP_ACTIVATE_VPN;
-                } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN,
-                        uid, pkg) == AppOpsManager.MODE_ALLOWED) {
-                    return REASON_OP_ACTIVATE_PLATFORM_VPN;
-                }
-            }
+        return REASON_DENIED;
+    }
+
+    /**
+     * @param uid The uid to check.
+     * @param pkgName The package name to check.
+     * @return The potential system-fixed exemption reason of the given uid/package. The caller
+     * must decide whether or not it should be exempted.
+     */
+    @ReasonCode
+    int getPotentialSystemExemptionReason(int uid, String pkg) {
+        final PackageManagerInternal pm = mInjector.getPackageManagerInternal();
+        final AppStandbyInternal appStandbyInternal = mInjector.getAppStandbyInternal();
+        final int userId = UserHandle.getUserId(uid);
+        if (isSystemModule(pkg)) {
+            return REASON_SYSTEM_MODULE;
+        } else if (isCarrierApp(pkg)) {
+            return REASON_CARRIER_PRIVILEGED_APP;
+        } else if (isExemptedFromSysConfig(pkg)) {
+            return REASON_SYSTEM_ALLOW_LISTED;
+        } else if (mConstantsObserver.mBgRestrictionExemptedPackages.contains(pkg)) {
+            return REASON_SYSTEM_ALLOW_LISTED;
+        } else if (pm.isPackageStateProtected(pkg, userId)) {
+            return REASON_DPO_PROTECTED_APP;
+        } else if (appStandbyInternal.isActiveDeviceAdmin(pkg, userId)) {
+            return REASON_ACTIVE_DEVICE_ADMIN;
+        }
+        return REASON_DENIED;
+    }
+
+    /**
+     * @param uid The uid to check.
+     * @param pkgName The package name to check.
+     * @return The potential user-allowed exemption reason of the given uid/package. The caller
+     * must decide whether or not it should be exempted.
+     */
+    @ReasonCode
+    int getPotentialUserAllowedExemptionReason(int uid, String pkg) {
+        final AppOpsManager appOpsManager = mInjector.getAppOpsManager();
+        if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN,
+                uid, pkg) == AppOpsManager.MODE_ALLOWED) {
+            return REASON_OP_ACTIVATE_VPN;
+        } else if (appOpsManager.checkOpNoThrow(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN,
+                uid, pkg) == AppOpsManager.MODE_ALLOWED) {
+            return REASON_OP_ACTIVATE_PLATFORM_VPN;
         }
         if (isRoleHeldByUid(RoleManager.ROLE_DIALER, uid)) {
             return REASON_ROLE_DIALER;
@@ -2852,6 +2897,7 @@
         if (isOnDeviceIdleAllowlist(uid)) {
             return REASON_ALLOWLISTED_PACKAGE;
         }
+        final ActivityManagerInternal am = mInjector.getActivityManagerInternal();
         if (am.isAssociatedCompanionApp(UserHandle.getUserId(uid), uid)) {
             return REASON_COMPANION_DEVICE_MANAGER;
         }
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 75d1f68..d1bcf87 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -731,6 +731,8 @@
     @Override
     @EnforcePermission(BATTERY_STATS)
     public List<BatteryUsageStats> getBatteryUsageStats(List<BatteryUsageStatsQuery> queries) {
+        super.getBatteryUsageStats_enforcePermission();
+
         awaitCompletion();
 
         if (mBatteryUsageStatsProvider.shouldUpdateStats(queries,
@@ -846,6 +848,8 @@
     @Override
     @EnforcePermission(BATTERY_STATS)
     public long computeBatteryScreenOffRealtimeMs() {
+        super.computeBatteryScreenOffRealtimeMs_enforcePermission();
+
         synchronized (mStats) {
             final long curTimeUs = SystemClock.elapsedRealtimeNanos() / 1000;
             long timeUs = mStats.computeBatteryScreenOffRealtime(curTimeUs,
@@ -857,6 +861,8 @@
     @Override
     @EnforcePermission(BATTERY_STATS)
     public long getScreenOffDischargeMah() {
+        super.getScreenOffDischargeMah_enforcePermission();
+
         synchronized (mStats) {
             long dischargeUah = mStats.getUahDischargeScreenOff(BatteryStats.STATS_SINCE_CHARGED);
             return dischargeUah / 1000;
@@ -866,6 +872,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteEvent(final int code, final String name, final int uid) {
+        super.noteEvent_enforcePermission();
+
         if (name == null) {
             // TODO(b/194733136): Replace with an IllegalArgumentException throw.
             Slog.wtfStack(TAG, "noteEvent called with null name. code = " + code);
@@ -886,6 +894,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteSyncStart(final String name, final int uid) {
+        super.noteSyncStart_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -902,6 +912,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteSyncFinish(final String name, final int uid) {
+        super.noteSyncFinish_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -919,6 +931,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteJobStart(final String name, final int uid) {
+        super.noteJobStart_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -934,6 +948,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteJobFinish(final String name, final int uid, final int stopReason) {
+        super.noteJobFinish_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1007,6 +1023,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartWakelock(final int uid, final int pid, final String name,
             final String historyName, final int type, final boolean unimportantForLogging) {
+        super.noteStartWakelock_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1023,6 +1041,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopWakelock(final int uid, final int pid, final String name,
             final String historyName, final int type) {
+        super.noteStopWakelock_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1039,6 +1059,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartWakelockFromSource(final WorkSource ws, final int pid, final String name,
             final String historyName, final int type, final boolean unimportantForLogging) {
+        super.noteStartWakelockFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1058,6 +1080,8 @@
             final String historyName, final int type, final WorkSource newWs, final int newPid,
             final String newName, final String newHistoryName, final int newType,
             final boolean newUnimportantForLogging) {
+        super.noteChangeWakelockFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         final WorkSource localNewWs = newWs != null ? new WorkSource(newWs) : null;
         synchronized (mLock) {
@@ -1077,6 +1101,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopWakelockFromSource(final WorkSource ws, final int pid, final String name,
             final String historyName, final int type) {
+        super.noteStopWakelockFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1094,6 +1120,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteLongPartialWakelockStart(final String name, final String historyName,
             final int uid) {
+        super.noteLongPartialWakelockStart_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1110,6 +1138,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteLongPartialWakelockStartFromSource(final String name, final String historyName,
             final WorkSource workSource) {
+        super.noteLongPartialWakelockStartFromSource_enforcePermission();
+
         final WorkSource localWs = workSource != null ? new WorkSource(workSource) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1127,6 +1157,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteLongPartialWakelockFinish(final String name, final String historyName,
             final int uid) {
+        super.noteLongPartialWakelockFinish_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1143,6 +1175,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteLongPartialWakelockFinishFromSource(final String name, final String historyName,
             final WorkSource workSource) {
+        super.noteLongPartialWakelockFinishFromSource_enforcePermission();
+
         final WorkSource localWs = workSource != null ? new WorkSource(workSource) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1159,6 +1193,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartSensor(final int uid, final int sensor) {
+        super.noteStartSensor_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1175,6 +1211,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopSensor(final int uid, final int sensor) {
+        super.noteStopSensor_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1191,6 +1229,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteVibratorOn(final int uid, final long durationMillis) {
+        super.noteVibratorOn_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1205,6 +1245,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteVibratorOff(final int uid) {
+        super.noteVibratorOff_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1219,6 +1261,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteGpsChanged(final WorkSource oldWs, final WorkSource newWs) {
+        super.noteGpsChanged_enforcePermission();
+
         final WorkSource localOldWs = oldWs != null ? new WorkSource(oldWs) : null;
         final WorkSource localNewWs = newWs != null ? new WorkSource(newWs) : null;
         synchronized (mLock) {
@@ -1235,6 +1279,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteGpsSignalQuality(final int signalLevel) {
+        super.noteGpsSignalQuality_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1249,6 +1295,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteScreenState(final int state) {
+        super.noteScreenState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1267,6 +1315,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteScreenBrightness(final int brightness) {
+        super.noteScreenBrightness_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1282,6 +1332,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteUserActivity(final int uid, final int event) {
+        super.noteUserActivity_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1296,6 +1348,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWakeUp(final String reason, final int reasonUid) {
+        super.noteWakeUp_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1310,6 +1364,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteInteractive(final boolean interactive) {
+        super.noteInteractive_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             mHandler.post(() -> {
@@ -1323,6 +1379,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteConnectivityChanged(final int type, final String extra) {
+        super.noteConnectivityChanged_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1338,6 +1396,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteMobileRadioPowerState(final int powerState, final long timestampNs,
             final int uid) {
+        super.noteMobileRadioPowerState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1364,6 +1424,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void notePhoneOn() {
+        super.notePhoneOn_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1378,6 +1440,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void notePhoneOff() {
+        super.notePhoneOff_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1392,6 +1456,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void notePhoneSignalStrength(final SignalStrength signalStrength) {
+        super.notePhoneSignalStrength_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1407,6 +1473,8 @@
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void notePhoneDataConnectionState(final int dataType, final boolean hasData,
             final int serviceType, final int nrFrequency) {
+        super.notePhoneDataConnectionState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1422,6 +1490,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void notePhoneState(final int state) {
+        super.notePhoneState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1437,6 +1507,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiOn() {
+        super.noteWifiOn_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1453,6 +1525,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiOff() {
+        super.noteWifiOff_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1469,6 +1543,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartAudio(final int uid) {
+        super.noteStartAudio_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1485,6 +1561,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopAudio(final int uid) {
+        super.noteStopAudio_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1501,6 +1579,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartVideo(final int uid) {
+        super.noteStartVideo_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1517,6 +1597,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopVideo(final int uid) {
+        super.noteStopVideo_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1533,6 +1615,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteResetAudio() {
+        super.noteResetAudio_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1549,6 +1633,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteResetVideo() {
+        super.noteResetVideo_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1565,6 +1651,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFlashlightOn(final int uid) {
+        super.noteFlashlightOn_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1581,6 +1669,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFlashlightOff(final int uid) {
+        super.noteFlashlightOff_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1597,6 +1687,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStartCamera(final int uid) {
+        super.noteStartCamera_enforcePermission();
+
         if (DBG) Slog.d(TAG, "begin noteStartCamera");
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1615,6 +1707,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteStopCamera(final int uid) {
+        super.noteStopCamera_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1631,6 +1725,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteResetCamera() {
+        super.noteResetCamera_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1647,6 +1743,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteResetFlashlight() {
+        super.noteResetFlashlight_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1663,6 +1761,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiRadioPowerState(final int powerState, final long tsNanos, final int uid) {
+        super.noteWifiRadioPowerState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1694,6 +1794,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiRunning(final WorkSource ws) {
+        super.noteWifiRunning_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1712,6 +1814,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiRunningChanged(final WorkSource oldWs, final WorkSource newWs) {
+        super.noteWifiRunningChanged_enforcePermission();
+
         final WorkSource localOldWs = oldWs != null ? new WorkSource(oldWs) : null;
         final WorkSource localNewWs = newWs != null ? new WorkSource(newWs) : null;
         synchronized (mLock) {
@@ -1733,6 +1837,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiStopped(final WorkSource ws) {
+        super.noteWifiStopped_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : ws;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1750,6 +1856,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiState(final int wifiState, final String accessPoint) {
+        super.noteWifiState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             mHandler.post(() -> {
@@ -1763,6 +1871,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiSupplicantStateChanged(final int supplState, final boolean failedAuth) {
+        super.noteWifiSupplicantStateChanged_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1778,6 +1888,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiRssiChanged(final int newRssi) {
+        super.noteWifiRssiChanged_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1792,6 +1904,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFullWifiLockAcquired(final int uid) {
+        super.noteFullWifiLockAcquired_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1806,6 +1920,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFullWifiLockReleased(final int uid) {
+        super.noteFullWifiLockReleased_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1820,6 +1936,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiScanStarted(final int uid) {
+        super.noteWifiScanStarted_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1834,6 +1952,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiScanStopped(final int uid) {
+        super.noteWifiScanStopped_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1848,6 +1968,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiMulticastEnabled(final int uid) {
+        super.noteWifiMulticastEnabled_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1862,6 +1984,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiMulticastDisabled(final int uid) {
+        super.noteWifiMulticastDisabled_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -1876,6 +2000,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFullWifiLockAcquiredFromSource(final WorkSource ws) {
+        super.noteFullWifiLockAcquiredFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1892,6 +2018,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteFullWifiLockReleasedFromSource(final WorkSource ws) {
+        super.noteFullWifiLockReleasedFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1908,6 +2036,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiScanStartedFromSource(final WorkSource ws) {
+        super.noteWifiScanStartedFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1923,6 +2053,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiScanStoppedFromSource(final WorkSource ws) {
+        super.noteWifiScanStoppedFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1938,6 +2070,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiBatchedScanStartedFromSource(final WorkSource ws, final int csph) {
+        super.noteWifiBatchedScanStartedFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1954,6 +2088,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiBatchedScanStoppedFromSource(final WorkSource ws) {
+        super.noteWifiBatchedScanStoppedFromSource_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -1970,6 +2106,8 @@
     @Override
     @EnforcePermission(anyOf = {NETWORK_STACK, PERMISSION_MAINLINE_NETWORK_STACK})
     public void noteNetworkInterfaceForTransports(final String iface, int[] transportTypes) {
+        super.noteNetworkInterfaceForTransports_enforcePermission();
+
         synchronized (mLock) {
             mHandler.post(() -> {
                 mStats.noteNetworkInterfaceForTransports(iface, transportTypes);
@@ -1983,6 +2121,8 @@
         // During device boot, qtaguid isn't enabled until after the inital
         // loading of battery stats. Now that they're enabled, take our initial
         // snapshot for future delta calculation.
+        super.noteNetworkStatsEnabled_enforcePermission();
+
         synchronized (mLock) {
             // Still schedule it on the handler to make sure we have existing pending works done
             mHandler.post(() -> {
@@ -1996,6 +2136,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteDeviceIdleMode(final int mode, final String activeReason, final int activeUid) {
+        super.noteDeviceIdleMode_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -2039,6 +2181,8 @@
     @Override
     @EnforcePermission(BLUETOOTH_CONNECT)
     public void noteBluetoothOn(int uid, int reason, String packageName) {
+        super.noteBluetoothOn_enforcePermission();
+
         FrameworkStatsLog.write_non_chained(FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED,
                 Binder.getCallingUid(), null,
                 FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED,
@@ -2051,6 +2195,8 @@
     @Override
     @EnforcePermission(BLUETOOTH_CONNECT)
     public void noteBluetoothOff(int uid, int reason, String packageName) {
+        super.noteBluetoothOff_enforcePermission();
+
         FrameworkStatsLog.write_non_chained(FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED,
                 Binder.getCallingUid(), null,
                 FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED,
@@ -2060,6 +2206,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteBleScanStarted(final WorkSource ws, final boolean isUnoptimized) {
+        super.noteBleScanStarted_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -2076,6 +2224,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteBleScanStopped(final WorkSource ws, final boolean isUnoptimized) {
+        super.noteBleScanStopped_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -2092,6 +2242,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteBleScanReset() {
+        super.noteBleScanReset_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -2106,6 +2258,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteBleScanResults(final WorkSource ws, final int numNewResults) {
+        super.noteBleScanResults_enforcePermission();
+
         final WorkSource localWs = ws != null ? new WorkSource(ws) : null;
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
@@ -2122,6 +2276,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteWifiControllerActivity(final WifiActivityEnergyInfo info) {
+        super.noteWifiControllerActivity_enforcePermission();
+
         if (info == null || !info.isValid()) {
             Slog.e(TAG, "invalid wifi data given: " + info);
             return;
@@ -2142,6 +2298,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteBluetoothControllerActivity(final BluetoothActivityEnergyInfo info) {
+        super.noteBluetoothControllerActivity_enforcePermission();
+
         if (info == null || !info.isValid()) {
             Slog.e(TAG, "invalid bluetooth data given: " + info);
             return;
@@ -2162,6 +2320,8 @@
     @Override
     @EnforcePermission(UPDATE_DEVICE_STATS)
     public void noteModemControllerActivity(final ModemActivityInfo info) {
+        super.noteModemControllerActivity_enforcePermission();
+
         if (info == null) {
             Slog.e(TAG, "invalid modem data given: " + info);
             return;
@@ -2188,6 +2348,8 @@
     public void setBatteryState(final int status, final int health, final int plugType,
             final int level, final int temp, final int volt, final int chargeUAh,
             final int chargeFullUAh, final long chargeTimeToFullSeconds) {
+        super.setBatteryState_enforcePermission();
+
         synchronized (mLock) {
             final long elapsedRealtime = SystemClock.elapsedRealtime();
             final long uptime = SystemClock.uptimeMillis();
@@ -2230,12 +2392,16 @@
     @Override
     @EnforcePermission(BATTERY_STATS)
     public long getAwakeTimeBattery() {
+        super.getAwakeTimeBattery_enforcePermission();
+
         return mStats.getAwakeTimeBattery();
     }
 
     @Override
     @EnforcePermission(BATTERY_STATS)
     public long getAwakeTimePlugged() {
+        super.getAwakeTimePlugged_enforcePermission();
+
         return mStats.getAwakeTimePlugged();
     }
 
@@ -2738,6 +2904,8 @@
     @EnforcePermission(anyOf = {UPDATE_DEVICE_STATS, BATTERY_STATS})
     public CellularBatteryStats getCellularBatteryStats() {
         // Wait for the completion of pending works if there is any
+        super.getCellularBatteryStats_enforcePermission();
+
         awaitCompletion();
         synchronized (mStats) {
             return mStats.getCellularBatteryStats();
@@ -2752,6 +2920,8 @@
     @EnforcePermission(anyOf = {UPDATE_DEVICE_STATS, BATTERY_STATS})
     public WifiBatteryStats getWifiBatteryStats() {
         // Wait for the completion of pending works if there is any
+        super.getWifiBatteryStats_enforcePermission();
+
         awaitCompletion();
         synchronized (mStats) {
             return mStats.getWifiBatteryStats();
@@ -2766,6 +2936,8 @@
     @EnforcePermission(BATTERY_STATS)
     public GpsBatteryStats getGpsBatteryStats() {
         // Wait for the completion of pending works if there is any
+        super.getGpsBatteryStats_enforcePermission();
+
         awaitCompletion();
         synchronized (mStats) {
             return mStats.getGpsBatteryStats();
@@ -2780,6 +2952,8 @@
     @EnforcePermission(BATTERY_STATS)
     public WakeLockStats getWakeLockStats() {
         // Wait for the completion of pending works if there is any
+        super.getWakeLockStats_enforcePermission();
+
         awaitCompletion();
         synchronized (mStats) {
             return mStats.getWakeLockStats();
@@ -2794,6 +2968,8 @@
     @EnforcePermission(BATTERY_STATS)
     public BluetoothBatteryStats getBluetoothBatteryStats() {
         // Wait for the completion of pending works if there is any
+        super.getBluetoothBatteryStats_enforcePermission();
+
         awaitCompletion();
         synchronized (mStats) {
             return mStats.getBluetoothBatteryStats();
@@ -2901,6 +3077,8 @@
      */
     @EnforcePermission(POWER_SAVER)
     public boolean setChargingStateUpdateDelayMillis(int delayMillis) {
+        super.setChargingStateUpdateDelayMillis_enforcePermission();
+
         final long ident = Binder.clearCallingIdentity();
 
         try {
@@ -3051,6 +3229,8 @@
     @Override
     @EnforcePermission(DEVICE_POWER)
     public void setChargerAcOnline(boolean online, boolean forceUpdate) {
+        super.setChargerAcOnline_enforcePermission();
+
         mBatteryManagerInternal.setChargerAcOnline(online, forceUpdate);
     }
 
@@ -3060,6 +3240,8 @@
     @Override
     @EnforcePermission(DEVICE_POWER)
     public void setBatteryLevel(int level, boolean forceUpdate) {
+        super.setBatteryLevel_enforcePermission();
+
         mBatteryManagerInternal.setBatteryLevel(level, forceUpdate);
     }
 
@@ -3069,6 +3251,8 @@
     @Override
     @EnforcePermission(DEVICE_POWER)
     public void unplugBattery(boolean forceUpdate) {
+        super.unplugBattery_enforcePermission();
+
         mBatteryManagerInternal.unplugBattery(forceUpdate);
     }
 
@@ -3078,6 +3262,8 @@
     @Override
     @EnforcePermission(DEVICE_POWER)
     public void resetBattery(boolean forceUpdate) {
+        super.resetBattery_enforcePermission();
+
         mBatteryManagerInternal.resetBattery(forceUpdate);
     }
 
@@ -3087,6 +3273,8 @@
     @Override
     @EnforcePermission(DEVICE_POWER)
     public void suspendBatteryInput() {
+        super.suspendBatteryInput_enforcePermission();
+
         mBatteryManagerInternal.suspendBatteryInput();
     }
 }
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index a4a1c2f..dfac82c 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
@@ -141,7 +141,8 @@
      */
     public int MAX_RUNNING_PROCESS_QUEUES = DEFAULT_MAX_RUNNING_PROCESS_QUEUES;
     private static final String KEY_MAX_RUNNING_PROCESS_QUEUES = "bcast_max_running_process_queues";
-    private static final int DEFAULT_MAX_RUNNING_PROCESS_QUEUES = 4;
+    private static final int DEFAULT_MAX_RUNNING_PROCESS_QUEUES =
+            ActivityManager.isLowRamDeviceStatic() ? 2 : 4;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of active broadcasts
@@ -150,7 +151,8 @@
      */
     public int MAX_RUNNING_ACTIVE_BROADCASTS = DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS;
     private static final String KEY_MAX_RUNNING_ACTIVE_BROADCASTS = "bcast_max_running_active_broadcasts";
-    private static final int DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS = 16;
+    private static final int DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS =
+            ActivityManager.isLowRamDeviceStatic() ? 8 : 16;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of pending
@@ -159,7 +161,8 @@
      */
     public int MAX_PENDING_BROADCASTS = DEFAULT_MAX_PENDING_BROADCASTS;
     private static final String KEY_MAX_PENDING_BROADCASTS = "bcast_max_pending_broadcasts";
-    private static final int DEFAULT_MAX_PENDING_BROADCASTS = 256;
+    private static final int DEFAULT_MAX_PENDING_BROADCASTS =
+            ActivityManager.isLowRamDeviceStatic() ? 128 : 256;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to normal
@@ -167,7 +170,7 @@
      */
     public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
     private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis";
-    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_NORMAL_MILLIS = +500;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts
@@ -175,7 +178,16 @@
      */
     public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
     private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis";
-    private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_CACHED_MILLIS = +120_000;
+
+    /**
+     * For {@link BroadcastQueueModernImpl}: Delay to apply to urgent
+     * broadcasts, typically a negative value to indicate they should be
+     * executed before most other pending broadcasts.
+     */
+    public long DELAY_URGENT_MILLIS = DEFAULT_DELAY_URGENT_MILLIS;
+    private static final String KEY_DELAY_URGENT_MILLIS = "bcast_delay_urgent_millis";
+    private static final long DEFAULT_DELAY_URGENT_MILLIS = -120_000;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of complete
@@ -313,6 +325,8 @@
                     DEFAULT_DELAY_NORMAL_MILLIS);
             DELAY_CACHED_MILLIS = getDeviceConfigLong(KEY_DELAY_CACHED_MILLIS,
                     DEFAULT_DELAY_CACHED_MILLIS);
+            DELAY_URGENT_MILLIS = getDeviceConfigLong(KEY_DELAY_URGENT_MILLIS,
+                    DEFAULT_DELAY_URGENT_MILLIS);
             MAX_HISTORY_COMPLETE_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_COMPLETE_SIZE,
                     DEFAULT_MAX_HISTORY_COMPLETE_SIZE);
             MAX_HISTORY_SUMMARY_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_SUMMARY_SIZE,
@@ -354,6 +368,8 @@
                     TimeUtils.formatDuration(DELAY_NORMAL_MILLIS)).println();
             pw.print(KEY_DELAY_CACHED_MILLIS,
                     TimeUtils.formatDuration(DELAY_CACHED_MILLIS)).println();
+            pw.print(KEY_DELAY_URGENT_MILLIS,
+                    TimeUtils.formatDuration(DELAY_URGENT_MILLIS)).println();
             pw.print(KEY_MAX_HISTORY_COMPLETE_SIZE, MAX_HISTORY_COMPLETE_SIZE).println();
             pw.print(KEY_MAX_HISTORY_SUMMARY_SIZE, MAX_HISTORY_SUMMARY_SIZE).println();
             pw.decreaseIndent();
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..b828720
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastLoopers.java
@@ -0,0 +1,112 @@
+/*
+ * 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 com.android.internal.annotations.GuardedBy;
+
+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";
+
+    @GuardedBy("sLoopers")
+    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 97635b5..7d9b477 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -85,9 +85,16 @@
     @Nullable ProcessRecord app;
 
     /**
-     * Track name to use for {@link Trace} events.
+     * Track name to use for {@link Trace} events, defined as part of upgrading
+     * into a running slot.
      */
-    @Nullable String traceTrackName;
+    @Nullable String runningTraceTrackName;
+
+    /**
+     * Flag indicating if this process should be OOM adjusted, defined as part
+     * of upgrading into a running slot.
+     */
+    boolean runningOomAdjusted;
 
     /**
      * Snapshotted value of {@link ProcessRecord#getCpuDelayTime()}, typically
@@ -103,6 +110,20 @@
     private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>();
 
     /**
+     * Ordered collection of "urgent" broadcasts that are waiting to be
+     * dispatched to this process, in the same representation as
+     * {@link #mPending}.
+     */
+    private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(4);
+
+    /**
+     * Ordered collection of "offload" broadcasts that are waiting to be
+     * dispatched to this process, in the same representation as
+     * {@link #mPending}.
+     */
+    private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);
+
+    /**
      * Broadcast actively being dispatched to this process.
      */
     private @Nullable BroadcastRecord mActive;
@@ -126,18 +147,24 @@
     private boolean mActiveViaColdStart;
 
     /**
-     * Count of {@link #mPending} broadcasts of these various flavors.
+     * Count of pending broadcasts of these various flavors.
      */
     private int mCountForeground;
     private int mCountOrdered;
     private int mCountAlarm;
     private int mCountPrioritized;
+    private int mCountInteractive;
+    private int mCountResultTo;
+    private int mCountInstrumented;
+    private int mCountManifest;
 
     private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
     private @Reason int mRunnableAtReason = REASON_EMPTY;
     private boolean mRunnableAtInvalidated;
 
     private boolean mProcessCached;
+    private boolean mProcessInstrumented;
+    private boolean mProcessPersistent;
 
     private String mCachedToString;
     private String mCachedToShortString;
@@ -149,6 +176,16 @@
         this.uid = uid;
     }
 
+    private @NonNull ArrayDeque<SomeArgs> getQueueForBroadcast(@NonNull BroadcastRecord record) {
+        if (record.isUrgent()) {
+            return mPendingUrgent;
+        } else if (record.isOffload()) {
+            return mPendingOffload;
+        } else {
+            return mPending;
+        }
+    }
+
     /**
      * Enqueue the given broadcast to be dispatched to this process at some
      * future point in time. The target receiver is indicated by the given index
@@ -162,39 +199,60 @@
      * given count of other receivers have reached a terminal state; typically
      * used for ordered broadcasts and priority traunches.
      */
-    public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
-            int blockedUntilTerminalCount) {
-        // If caller wants to replace, walk backwards looking for any matches
+    public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) {
         if (record.isReplacePending()) {
-            final Iterator<SomeArgs> it = mPending.descendingIterator();
-            final Object receiver = record.receivers.get(recordIndex);
-            while (it.hasNext()) {
-                final SomeArgs args = it.next();
-                final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
-                final Object testReceiver = testRecord.receivers.get(args.argi1);
-                if ((record.callingUid == testRecord.callingUid)
-                        && (record.userId == testRecord.userId)
-                        && record.intent.filterEquals(testRecord.intent)
-                        && isReceiverEquals(receiver, testReceiver)) {
-                    // Exact match found; perform in-place swap
-                    args.arg1 = record;
-                    args.argi1 = recordIndex;
-                    args.argi2 = blockedUntilTerminalCount;
-                    onBroadcastDequeued(testRecord);
-                    onBroadcastEnqueued(record);
-                    return;
-                }
+            boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex)
+                    || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex)
+                    || replaceBroadcastInQueue(mPendingOffload, record, recordIndex);
+            if (didReplace) {
+                return;
             }
         }
 
         // Caller isn't interested in replacing, or we didn't find any pending
         // item to replace above, so enqueue as a new broadcast
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = record;
-        args.argi1 = recordIndex;
-        args.argi2 = blockedUntilTerminalCount;
-        mPending.addLast(args);
-        onBroadcastEnqueued(record);
+        SomeArgs newBroadcastArgs = SomeArgs.obtain();
+        newBroadcastArgs.arg1 = record;
+        newBroadcastArgs.argi1 = recordIndex;
+
+        // Cross-broadcast prioritization policy:  some broadcasts might warrant being
+        // issued ahead of others that are already pending, for example if this new
+        // broadcast is in a different delivery class or is tied to a direct user interaction
+        // with implicit responsiveness expectations.
+        getQueueForBroadcast(record).addLast(newBroadcastArgs);
+        onBroadcastEnqueued(record, recordIndex);
+    }
+
+    /**
+     * Searches from newest to oldest, and at the first matching pending broadcast
+     * it finds, replaces it in-place and returns -- does not attempt to handle
+     * "duplicate" broadcasts in the queue.
+     * <p>
+     * @return {@code true} if it found and replaced an existing record in the queue;
+     * {@code false} otherwise.
+     */
+    private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastRecord record, int recordIndex) {
+        final Iterator<SomeArgs> it = queue.descendingIterator();
+        final Object receiver = record.receivers.get(recordIndex);
+        while (it.hasNext()) {
+            final SomeArgs args = it.next();
+            final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
+            final int testRecordIndex = args.argi1;
+            final Object testReceiver = testRecord.receivers.get(testRecordIndex);
+            if ((record.callingUid == testRecord.callingUid)
+                    && (record.userId == testRecord.userId)
+                    && record.intent.filterEquals(testRecord.intent)
+                    && isReceiverEquals(receiver, testReceiver)) {
+                // Exact match found; perform in-place swap
+                args.arg1 = record;
+                args.argi1 = recordIndex;
+                onBroadcastDequeued(testRecord, testRecordIndex);
+                onBroadcastEnqueued(record, recordIndex);
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
@@ -226,17 +284,30 @@
     public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
             @NonNull BroadcastConsumer consumer, boolean andRemove) {
         boolean didSomething = false;
-        final Iterator<SomeArgs> it = mPending.iterator();
+        didSomething |= forEachMatchingBroadcastInQueue(mPending,
+                predicate, consumer, andRemove);
+        didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
+                predicate, consumer, andRemove);
+        didSomething |= forEachMatchingBroadcastInQueue(mPendingOffload,
+                predicate, consumer, andRemove);
+        return didSomething;
+    }
+
+    private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer,
+            boolean andRemove) {
+        boolean didSomething = false;
+        final Iterator<SomeArgs> it = queue.iterator();
         while (it.hasNext()) {
             final SomeArgs args = it.next();
             final BroadcastRecord record = (BroadcastRecord) args.arg1;
-            final int index = args.argi1;
-            if (predicate.test(record, index)) {
-                consumer.accept(record, index);
+            final int recordIndex = args.argi1;
+            if (predicate.test(record, recordIndex)) {
+                consumer.accept(record, recordIndex);
                 if (andRemove) {
                     args.recycle();
                     it.remove();
-                    onBroadcastDequeued(record);
+                    onBroadcastDequeued(record, recordIndex);
                 }
                 didSomething = true;
             }
@@ -247,6 +318,20 @@
     }
 
     /**
+     * Update the actively running "warm" process for this process.
+     */
+    public void setProcess(@Nullable ProcessRecord app) {
+        this.app = app;
+        if (app != null) {
+            setProcessInstrumented(app.getActiveInstrumentation() != null);
+            setProcessPersistent(app.isPersistent());
+        } else {
+            setProcessInstrumented(false);
+            setProcessPersistent(false);
+        }
+    }
+
+    /**
      * Update if this process is in the "cached" state, typically signaling that
      * broadcast dispatch should be paused or delayed.
      */
@@ -258,20 +343,42 @@
     }
 
     /**
+     * Update if this process is in the "instrumented" state, typically
+     * signaling that broadcast dispatch should bypass all pauses or delays, to
+     * avoid holding up test suites.
+     */
+    public void setProcessInstrumented(boolean instrumented) {
+        if (mProcessInstrumented != instrumented) {
+            mProcessInstrumented = instrumented;
+            invalidateRunnableAt();
+        }
+    }
+
+    /**
+     * Update if this process is in the "persistent" state, which signals broadcast dispatch should
+     * bypass all pauses or delays to prevent the system from becoming out of sync with itself.
+     */
+    public void setProcessPersistent(boolean persistent) {
+        if (mProcessPersistent != persistent) {
+            mProcessPersistent = persistent;
+            invalidateRunnableAt();
+        }
+    }
+
+    /**
      * Return if we know of an actively running "warm" process for this queue.
      */
     public boolean isProcessWarm() {
-        return (app != null) && (app.getThread() != null) && !app.isKilled();
+        return (app != null) && (app.getOnewayThread() != null) && !app.isKilled();
     }
 
     public int getPreferredSchedulingGroupLocked() {
-        if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) {
-            // We have an important broadcast somewhere down the queue, so
+        if (mCountForeground > 0) {
+            // We have a foreground broadcast somewhere down the queue, so
             // boost priority until we drain them all
             return ProcessList.SCHED_GROUP_DEFAULT;
-        } else if ((mActive != null)
-                && (mActive.isForeground() || mActive.ordered || mActive.alarm)) {
-            // We have an important broadcast right now, so boost priority
+        } else if ((mActive != null) && mActive.isForeground()) {
+            // We have a foreground broadcast right now, so boost priority
             return ProcessList.SCHED_GROUP_DEFAULT;
         } else if (!isIdle()) {
             return ProcessList.SCHED_GROUP_BACKGROUND;
@@ -301,13 +408,13 @@
      */
     public void makeActiveNextPending() {
         // TODO: what if the next broadcast isn't runnable yet?
-        final SomeArgs next = mPending.removeFirst();
+        final SomeArgs next = removeNextBroadcast();
         mActive = (BroadcastRecord) next.arg1;
         mActiveIndex = next.argi1;
         mActiveCountSinceIdle++;
         mActiveViaColdStart = false;
         next.recycle();
-        onBroadcastDequeued(mActive);
+        onBroadcastDequeued(mActive, mActiveIndex);
     }
 
     /**
@@ -324,7 +431,7 @@
     /**
      * Update summary statistics when the given record has been enqueued.
      */
-    private void onBroadcastEnqueued(@NonNull BroadcastRecord record) {
+    private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) {
         if (record.isForeground()) {
             mCountForeground++;
         }
@@ -337,13 +444,25 @@
         if (record.prioritized) {
             mCountPrioritized++;
         }
+        if (record.interactive) {
+            mCountInteractive++;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo++;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented++;
+        }
+        if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
+            mCountManifest++;
+        }
         invalidateRunnableAt();
     }
 
     /**
      * Update summary statistics when the given record has been dequeued.
      */
-    private void onBroadcastDequeued(@NonNull BroadcastRecord record) {
+    private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) {
         if (record.isForeground()) {
             mCountForeground--;
         }
@@ -356,34 +475,46 @@
         if (record.prioritized) {
             mCountPrioritized--;
         }
+        if (record.interactive) {
+            mCountInteractive--;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo--;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented--;
+        }
+        if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
+            mCountManifest--;
+        }
         invalidateRunnableAt();
     }
 
     public void traceProcessStartingBegin() {
         Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                traceTrackName, toShortString() + " starting", hashCode());
+                runningTraceTrackName, toShortString() + " starting", hashCode());
     }
 
     public void traceProcessRunningBegin() {
         Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                traceTrackName, toShortString() + " running", hashCode());
+                runningTraceTrackName, toShortString() + " running", hashCode());
     }
 
     public void traceProcessEnd() {
         Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                traceTrackName, hashCode());
+                runningTraceTrackName, hashCode());
     }
 
     public void traceActiveBegin() {
         final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
         Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                traceTrackName, mActive.toShortString() + " scheduled", cookie);
+                runningTraceTrackName, mActive.toShortString() + " scheduled", cookie);
     }
 
     public void traceActiveEnd() {
         final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
         Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                traceTrackName, cookie);
+                runningTraceTrackName, cookie);
     }
 
     /**
@@ -403,7 +534,7 @@
     }
 
     public boolean isEmpty() {
-        return mPending.isEmpty();
+        return mPending.isEmpty() && mPendingUrgent.isEmpty() && mPendingOffload.isEmpty();
     }
 
     public boolean isActive() {
@@ -411,6 +542,48 @@
     }
 
     /**
+     * Will thrown an exception if there are no pending broadcasts; relies on
+     * {@link #isEmpty()} being false.
+     */
+    SomeArgs removeNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return queue.removeFirst();
+    }
+
+    @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
+        if (!mPendingUrgent.isEmpty()) {
+            return mPendingUrgent;
+        } else if (!mPending.isEmpty()) {
+            return mPending;
+        } else if (!mPendingOffload.isEmpty()) {
+            return mPendingOffload;
+        }
+        return null;
+    }
+
+    /**
+     * Returns null if there are no pending broadcasts
+     */
+    @Nullable SomeArgs peekNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? queue.peekFirst() : null;
+    }
+
+    @VisibleForTesting
+    @Nullable BroadcastRecord peekNextBroadcastRecord() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
+    }
+
+    /**
+     * Quickly determine if this queue has broadcasts waiting to be delivered to
+     * manifest receivers, which indicates we should request an OOM adjust.
+     */
+    public boolean isPendingManifest() {
+        return mCountManifest > 0;
+    }
+
+    /**
      * Quickly determine if this queue has broadcasts that are still waiting to
      * be delivered at some point in the future.
      */
@@ -423,15 +596,21 @@
      * barrier timestamp that are still waiting to be delivered.
      */
     public boolean isBeyondBarrierLocked(@UptimeMillisLong long barrierTime) {
-        if (mActive != null) {
-            return mActive.enqueueTime > barrierTime;
-        }
         final SomeArgs next = mPending.peekFirst();
-        if (next != null) {
-            return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
-        }
-        // Nothing running or runnable means we're past the barrier
-        return true;
+        final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
+        final SomeArgs nextOffload = mPendingOffload.peekFirst();
+
+        // Empty records are always past any barrier
+        final boolean activeBeyond = (mActive == null)
+                || mActive.enqueueTime > barrierTime;
+        final boolean nextBeyond = (next == null)
+                || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
+        final boolean nextUrgentBeyond = (nextUrgent == null)
+                || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
+        final boolean nextOffloadBeyond = (nextOffload == null)
+                || ((BroadcastRecord) nextOffload.arg1).enqueueTime > barrierTime;
+
+        return activeBeyond && nextBeyond && nextUrgentBeyond && nextOffloadBeyond;
     }
 
     public boolean isRunnable() {
@@ -467,25 +646,37 @@
     }
 
     static final int REASON_EMPTY = 0;
-    static final int REASON_CONTAINS_FOREGROUND = 1;
-    static final int REASON_CONTAINS_ORDERED = 2;
-    static final int REASON_CONTAINS_ALARM = 3;
-    static final int REASON_CONTAINS_PRIORITIZED = 4;
-    static final int REASON_CACHED = 5;
-    static final int REASON_NORMAL = 6;
-    static final int REASON_MAX_PENDING = 7;
-    static final int REASON_BLOCKED = 8;
+    static final int REASON_CACHED = 1;
+    static final int REASON_NORMAL = 2;
+    static final int REASON_MAX_PENDING = 3;
+    static final int REASON_BLOCKED = 4;
+    static final int REASON_INSTRUMENTED = 5;
+    static final int REASON_PERSISTENT = 6;
+    static final int REASON_CONTAINS_FOREGROUND = 10;
+    static final int REASON_CONTAINS_ORDERED = 11;
+    static final int REASON_CONTAINS_ALARM = 12;
+    static final int REASON_CONTAINS_PRIORITIZED = 13;
+    static final int REASON_CONTAINS_INTERACTIVE = 14;
+    static final int REASON_CONTAINS_RESULT_TO = 15;
+    static final int REASON_CONTAINS_INSTRUMENTED = 16;
+    static final int REASON_CONTAINS_MANIFEST = 17;
 
     @IntDef(flag = false, prefix = { "REASON_" }, value = {
             REASON_EMPTY,
-            REASON_CONTAINS_FOREGROUND,
-            REASON_CONTAINS_ORDERED,
-            REASON_CONTAINS_ALARM,
-            REASON_CONTAINS_PRIORITIZED,
             REASON_CACHED,
             REASON_NORMAL,
             REASON_MAX_PENDING,
             REASON_BLOCKED,
+            REASON_INSTRUMENTED,
+            REASON_PERSISTENT,
+            REASON_CONTAINS_FOREGROUND,
+            REASON_CONTAINS_ORDERED,
+            REASON_CONTAINS_ALARM,
+            REASON_CONTAINS_PRIORITIZED,
+            REASON_CONTAINS_INTERACTIVE,
+            REASON_CONTAINS_RESULT_TO,
+            REASON_CONTAINS_INSTRUMENTED,
+            REASON_CONTAINS_MANIFEST,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Reason {}
@@ -493,14 +684,20 @@
     static @NonNull String reasonToString(@Reason int reason) {
         switch (reason) {
             case REASON_EMPTY: return "EMPTY";
-            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
-            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
-            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
-            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
             case REASON_CACHED: return "CACHED";
             case REASON_NORMAL: return "NORMAL";
             case REASON_MAX_PENDING: return "MAX_PENDING";
             case REASON_BLOCKED: return "BLOCKED";
+            case REASON_INSTRUMENTED: return "INSTRUMENTED";
+            case REASON_PERSISTENT: return "PERSISTENT";
+            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
+            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
+            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
+            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
+            case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE";
+            case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO";
+            case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED";
+            case REASON_CONTAINS_MANIFEST: return "CONTAINS_MANIFEST";
             default: return Integer.toString(reason);
         }
     }
@@ -509,11 +706,11 @@
      * Update {@link #getRunnableAt()} if it's currently invalidated.
      */
     private void updateRunnableAt() {
-        final SomeArgs next = mPending.peekFirst();
+        final SomeArgs next = peekNextBroadcast();
         if (next != null) {
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
             final int index = next.argi1;
-            final int blockedUntilTerminalCount = next.argi2;
+            final int blockedUntilTerminalCount = r.blockedUntilTerminalCount[index];
             final long runnableAt = r.enqueueTime;
 
             // We might be blocked waiting for other receivers to finish,
@@ -525,17 +722,18 @@
                 return;
             }
 
-            // If we have too many broadcasts pending, bypass any delays that
-            // might have been applied above to aid draining
-            if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) {
-                mRunnableAt = runnableAt;
-                mRunnableAtReason = REASON_MAX_PENDING;
-                return;
-            }
-
             if (mCountForeground > 0) {
-                mRunnableAt = runnableAt;
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                 mRunnableAtReason = REASON_CONTAINS_FOREGROUND;
+            } else if (mCountInteractive > 0) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_CONTAINS_INTERACTIVE;
+            } else if (mCountInstrumented > 0) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED;
+            } else if (mProcessInstrumented) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_INSTRUMENTED;
             } else if (mCountOrdered > 0) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_CONTAINS_ORDERED;
@@ -545,6 +743,15 @@
             } else if (mCountPrioritized > 0) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_CONTAINS_PRIORITIZED;
+            } else if (mCountResultTo > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_RESULT_TO;
+            } else if (mCountManifest > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_MANIFEST;
+            } else if (mProcessPersistent) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_PERSISTENT;
             } else if (mProcessCached) {
                 mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
                 mRunnableAtReason = REASON_CACHED;
@@ -552,6 +759,14 @@
                 mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS;
                 mRunnableAtReason = REASON_NORMAL;
             }
+
+            // If we have too many broadcasts pending, bypass any delays that
+            // might have been applied above to aid draining
+            if (mPending.size() + mPendingUrgent.size()
+                    + mPendingOffload.size() >= constants.MAX_PENDING_BROADCASTS) {
+                mRunnableAt = Math.min(mRunnableAt, runnableAt);
+                mRunnableAtReason = REASON_MAX_PENDING;
+            }
         } else {
             mRunnableAt = Long.MAX_VALUE;
             mRunnableAtReason = REASON_EMPTY;
@@ -564,8 +779,8 @@
      */
     public void checkHealthLocked() {
         if (mRunnableAtReason == REASON_BLOCKED) {
-            final SomeArgs next = mPending.peekFirst();
-            Objects.requireNonNull(next, "peekFirst");
+            final SomeArgs next = peekNextBroadcast();
+            Objects.requireNonNull(next, "peekNextBroadcast");
 
             // If blocked more than 10 minutes, we're likely wedged
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
@@ -653,7 +868,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()) {
@@ -664,35 +879,30 @@
         }
         pw.print(" because ");
         pw.print(reasonToString(mRunnableAtReason));
-        if (mRunnableAtReason == REASON_BLOCKED) {
-            final SomeArgs next = mPending.peekFirst();
-            if (next != null) {
-                final BroadcastRecord r = (BroadcastRecord) next.arg1;
-                final int blockedUntilTerminalCount = next.argi2;
-                pw.print(" waiting for ");
-                pw.print(blockedUntilTerminalCount);
-                pw.print(" at ");
-                pw.print(r.terminalCount);
-                pw.print(" of ");
-                pw.print(r.receivers.size());
-            }
-        }
         pw.println();
         pw.increaseIndent();
         if (mActive != null) {
-            dumpRecord(now, pw, mActive, mActiveIndex);
+            dumpRecord("ACTIVE", now, pw, mActive, mActiveIndex);
+        }
+        for (SomeArgs args : mPendingUrgent) {
+            final BroadcastRecord r = (BroadcastRecord) args.arg1;
+            dumpRecord("URGENT", now, pw, r, args.argi1);
         }
         for (SomeArgs args : mPending) {
             final BroadcastRecord r = (BroadcastRecord) args.arg1;
-            dumpRecord(now, pw, r, args.argi1);
+            dumpRecord(null, now, pw, r, args.argi1);
+        }
+        for (SomeArgs args : mPendingOffload) {
+            final BroadcastRecord r = (BroadcastRecord) args.arg1;
+            dumpRecord("OFFLOAD", now, pw, r, args.argi1);
         }
         pw.decreaseIndent();
         pw.println();
     }
 
     @NeverCompile
-    private void dumpRecord(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw,
-            @NonNull BroadcastRecord record, int recordIndex) {
+    private void dumpRecord(@Nullable String flavor, @UptimeMillisLong long now,
+            @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record, int recordIndex) {
         TimeUtils.formatDuration(record.enqueueTime, now, pw);
         pw.print(' ');
         pw.println(record.toShortString());
@@ -703,6 +913,10 @@
             pw.print(" at ");
             TimeUtils.formatDuration(record.scheduledTime[recordIndex], now, pw);
         }
+        if (flavor != null) {
+            pw.print(' ');
+            pw.print(flavor);
+        }
         final Object receiver = record.receivers.get(recordIndex);
         if (receiver instanceof BroadcastFilter) {
             final BroadcastFilter filter = (BroadcastFilter) receiver;
@@ -714,5 +928,14 @@
             pw.print(info.activityInfo.name);
         }
         pw.println();
+        final int blockedUntilTerminalCount = record.blockedUntilTerminalCount[recordIndex];
+        if (blockedUntilTerminalCount != -1) {
+            pw.print("    blocked until ");
+            pw.print(blockedUntilTerminalCount);
+            pw.print(", currently at ");
+            pw.print(record.terminalCount);
+            pw.print(" of ");
+            pw.println(record.receivers.size());
+        }
     }
 }
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 1e172fc..153ad1e 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.os.DropBoxManager;
 import android.os.Handler;
+import android.os.Trace;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
@@ -76,6 +77,30 @@
         }
     }
 
+    static void checkState(boolean expression, @NonNull String msg) {
+        if (!expression) {
+            throw new IllegalStateException(msg);
+        }
+    }
+
+    static void checkStateWtf(boolean expression, @NonNull String msg) {
+        if (!expression) {
+            Slog.wtf(TAG, new IllegalStateException(msg));
+        }
+    }
+
+    static int traceBegin(@NonNull String methodName) {
+        final int cookie = methodName.hashCode();
+        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                TAG, methodName, cookie);
+        return cookie;
+    }
+
+    static void traceEnd(int cookie) {
+        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                TAG, cookie);
+    }
+
     @Override
     public String toString() {
         return mQueueName;
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.md b/services/core/java/com/android/server/am/BroadcastQueue.md
new file mode 100644
index 0000000..8131793
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastQueue.md
@@ -0,0 +1,98 @@
+# Broadcast Queue Design
+
+Broadcast intents are one of the major building blocks of the Android platform,
+generally intended for asynchronous notification of events. There are three
+flavors of intents that can be broadcast:
+
+* **Normal** broadcast intents are dispatched to relevant receivers.
+* **Ordered** broadcast intents are dispatched in a specific order to
+receivers, where each receiver has the opportunity to influence the final
+"result" of a broadcast, including aborting delivery to any remaining receivers.
+* **Sticky** broadcast intents are dispatched to relevant receivers, and are
+then retained internally for immediate dispatch to any future receivers. (This
+capability has been deprecated and its use is discouraged due to its system
+health impact.)
+
+And there are there two ways to receive these intents:
+
+* Registered receivers (via `Context.registerReceiver()` methods) are
+dynamically requested by a running app to receive intents. These requests are
+only maintained while the process is running, and are discarded at process
+death.
+* Manifest receivers (via the `<receiver>` tag in `AndroidManifest.xml`) are
+statically requested by an app to receive intents. These requests are delivered
+regardless of process running state, and have the ability to cold-start a
+process that isn't currently running.
+
+## Per-process queues
+
+The design of `BroadcastQueueModernImpl` is centered around maintaining a
+separate `BroadcastProcessQueue` instance for each potential process on the
+device. At this level, a process refers to the `android:process` attributes
+defined in `AndroidManifest.xml` files, which means it can be defined and
+populated regardless of the process state. (For example, a given
+`android:process` can have multiple `ProcessRecord`/PIDs defined as it's
+launched, killed, and relaunched over long periods of time.)
+
+Each per-process queue has the concept of a _runnable at_ timestamp when it's
+next eligible for execution, and that value can be influenced by a wide range
+of policies, such as:
+
+* Which broadcasts are pending dispatch to a given process. For example, an
+"urgent" broadcast typically results in an earlier _runnable at_ time, or a
+"delayed" broadcast typically results in a later _runnable at_ time.
+* Current state of the process or UID. For example, a "cached" process
+typically results in a later _runnable at_ time, or an "instrumented" process
+typically results in an earlier _runnable at_ time.
+* Blocked waiting for an earlier receiver to complete. For example, an
+"ordered" or "prioritized" broadcast typically results in a _not currently
+runnable_ value.
+
+Each per-process queue represents a single remote `ApplicationThread`, and we
+only dispatch a single broadcast at a time to each process to ensure developers
+see consistent ordering of broadcast events. The flexible _runnable at_
+policies above mean that no inter-process ordering guarantees are provided,
+except for those explicitly provided by "ordered" or "prioritized" broadcasts.
+
+## Parallel dispatch
+
+Given a collection of per-process queues with valid _runnable at_ timestamps,
+BroadcastQueueModernImpl is then willing to promote those _runnable_ queues
+into a _running_ state. We choose the next per-process queue to promote based
+on the sorted ordering of the _runnable at_ timestamps, selecting the
+longest-waiting process first, which aims to reduce overall broadcast dispatch
+latency.
+
+To preserve system health, at most
+`BroadcastConstants.MAX_RUNNING_PROCESS_QUEUES` processes are allowed to be in
+the _running_ state at any given time, and at most one process is allowed to be
+_cold started_ at any given time. (For background, _cold starting_ a process
+by forking and specializing the zygote is a relatively heavy operation, so
+limiting ourselves to a single pending _cold start_ reduces system-wide
+resource contention.)
+
+After each broadcast is dispatched to a given process, we consider dispatching
+any additional pending broadcasts to that process, aimed at batching dispatch
+to better amortize the cost of OOM adjustments.
+
+## Starvation considerations
+
+Careful attention is given to several types of potential resource starvation,
+along with the mechanisms of mitigation:
+
+* A per-process queue that has a delayed _runnable at_ policy applied can risk
+growing very large. This is mitigated by
+`BroadcastConstants.MAX_PENDING_BROADCASTS` bypassing any delays when the queue
+grows too large.
+* A per-process queue that has a large number of pending broadcasts can risk
+monopolizing one of the limited _runnable_ slots. This is mitigated by
+`BroadcastConstants.MAX_RUNNING_ACTIVE_BROADCASTS` being used to temporarily
+"retire" a running process to give other processes a chance to run.
+* An "urgent" broadcast dispatched to a process with a large backlog of
+"non-urgent" broadcasts can risk large dispatch latencies. This is mitigated
+by maintaining a separate `mPendingUrgent` queue of urgent events, which we
+prefer to dispatch before the normal `mPending` queue.
+* A process with a scheduled broadcast desires to execute, but heavy CPU
+contention can risk the process not receiving enough resources before an ANR
+timeout is triggered. This is mitigated by extending the "soft" ANR timeout by
+up to double the original timeout length.
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index d7a075b..fb7e0be 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -51,7 +51,6 @@
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
@@ -262,7 +261,7 @@
                 if (oldRecord.resultTo != null) {
                     try {
                         oldRecord.mIsReceiverAppRunning = true;
-                        performReceiveLocked(oldRecord.callerApp, oldRecord.resultTo,
+                        performReceiveLocked(oldRecord.resultToApp, oldRecord.resultTo,
                                 oldRecord.intent,
                                 Activity.RESULT_CANCELED, null, null,
                                 false, false, oldRecord.userId, oldRecord.callingUid, r.callingUid,
@@ -1120,7 +1119,7 @@
                                 r.dispatchTime = now;
                             }
                             r.mIsReceiverAppRunning = true;
-                            performReceiveLocked(r.callerApp, r.resultTo,
+                            performReceiveLocked(r.resultToApp, r.resultTo,
                                     new Intent(r.intent), r.resultCode,
                                     r.resultData, r.resultExtras, false, false, r.userId,
                                     r.callingUid, r.callingUid,
@@ -1488,7 +1487,8 @@
             // LocalServices.getService() here.
             final UserManagerInternal umInternal = LocalServices.getService(
                     UserManagerInternal.class);
-            final UserInfo userInfo = umInternal.getUserInfo(r.userId);
+            final UserInfo userInfo =
+                    (umInternal != null) ? umInternal.getUserInfo(r.userId) : null;
             if (userInfo != null) {
                 userType = UserManager.getUserTypeForStatsd(userInfo.userType);
             }
@@ -1638,9 +1638,8 @@
             }
             Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);
             logBroadcastReceiverDiscardLocked(r);
-            String anrMessage =
-                    "Broadcast of " + r.intent.toString() + ", waited " + timeoutDurationMs + "ms";
-            TimeoutRecord timeoutRecord = TimeoutRecord.forBroadcastReceiver(anrMessage);
+            TimeoutRecord timeoutRecord = TimeoutRecord.forBroadcastReceiver(r.intent,
+                    timeoutDurationMs);
             if (curReceiver != null && curReceiver instanceof BroadcastFilter) {
                 BroadcastFilter bf = (BroadcastFilter) curReceiver;
                 if (bf.receiverList.pid != 0
@@ -1689,13 +1688,7 @@
                 System.identityHashCode(original));
         }
 
-        final ApplicationInfo info = original.callerApp != null ? original.callerApp.info : null;
-        final String callerPackage = info != null ? info.packageName : original.callerPackage;
-        if (callerPackage != null) {
-            mService.mHandler.obtainMessage(ActivityManagerService.DISPATCH_SENDING_BROADCAST_EVENT,
-                    original.callingUid, 0, callerPackage).sendToTarget();
-        }
-
+        mService.notifyBroadcastFinishedLocked(original);
         mHistory.addBroadcastToHistoryLocked(original);
     }
 
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 89a0283..a7d8433 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -25,14 +25,12 @@
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME;
-import static com.android.internal.util.Preconditions.checkState;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.reasonToString;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
 import static com.android.server.am.BroadcastRecord.deliveryStateToString;
 import static com.android.server.am.BroadcastRecord.getReceiverPackageName;
-import static com.android.server.am.BroadcastRecord.getReceiverPriority;
 import static com.android.server.am.BroadcastRecord.getReceiverProcessName;
 import static com.android.server.am.BroadcastRecord.getReceiverUid;
 import static com.android.server.am.BroadcastRecord.isDeliveryStateTerminal;
@@ -44,6 +42,7 @@
 import android.annotation.UptimeMillisLong;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.BroadcastOptions;
 import android.app.IApplicationThread;
 import android.app.RemoteServiceException.CannotDeliverBroadcastException;
 import android.app.UidObserver;
@@ -57,12 +56,12 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
+import android.os.BundleMerger;
 import android.os.Handler;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.Trace;
 import android.os.UserHandle;
 import android.text.format.DateUtils;
 import android.util.IndentingPrintWriter;
@@ -142,14 +141,6 @@
         mRunning = new BroadcastProcessQueue[mConstants.MAX_RUNNING_PROCESS_QUEUES];
     }
 
-    // TODO: add support for replacing pending broadcasts
-    // TODO: add support for merging pending broadcasts
-
-    // TODO: consider reordering foreground broadcasts within queue
-
-    // TODO: pause queues when background services are running
-    // TODO: pause queues when processes are frozen
-
     /**
      * Map from UID to per-process broadcast queues. If a UID hosts more than
      * one process, each additional process is stored as a linked list using
@@ -209,17 +200,33 @@
     private final BroadcastConstants mFgConstants;
     private final BroadcastConstants mBgConstants;
 
+    /**
+     * Timestamp when last {@link #testAllProcessQueues} failure was observed;
+     * used for throttling log messages.
+     */
+    private @UptimeMillisLong long mLastTestFailureTime;
+
     private static final int MSG_UPDATE_RUNNING_LIST = 1;
     private static final int MSG_DELIVERY_TIMEOUT_SOFT = 2;
     private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
     private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4;
     private static final int MSG_CHECK_HEALTH = 5;
+    private static final int MSG_FINISH_RECEIVER = 6;
 
     private void enqueueUpdateRunningList() {
         mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
         mLocalHandler.sendEmptyMessage(MSG_UPDATE_RUNNING_LIST);
     }
 
+    private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
+            @DeliveryState int deliveryState, @NonNull String reason) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.arg1 = queue;
+        args.argi1 = deliveryState;
+        args.arg2 = reason;
+        mLocalHandler.sendMessage(Message.obtain(mLocalHandler, MSG_FINISH_RECEIVER, args));
+    }
+
     private final Handler mLocalHandler;
 
     private final Handler.Callback mLocalCallback = (msg) -> {
@@ -232,7 +239,7 @@
             }
             case MSG_DELIVERY_TIMEOUT_SOFT: {
                 synchronized (mService) {
-                    deliveryTimeoutSoftLocked((BroadcastProcessQueue) msg.obj);
+                    deliveryTimeoutSoftLocked((BroadcastProcessQueue) msg.obj, msg.arg1);
                 }
                 return true;
             }
@@ -258,6 +265,17 @@
                 }
                 return true;
             }
+            case MSG_FINISH_RECEIVER: {
+                synchronized (mService) {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
+                    final int deliveryState = args.argi1;
+                    final String reason = (String) args.arg2;
+                    args.recycle();
+                    finishReceiverLocked(queue, deliveryState, reason);
+                }
+                return true;
+            }
         }
         return false;
     };
@@ -341,7 +359,7 @@
         int avail = mRunning.length - getRunningSize();
         if (avail == 0) return;
 
-        final int cookie = traceBegin(TAG, "updateRunningList");
+        final int cookie = traceBegin("updateRunningList");
         final long now = SystemClock.uptimeMillis();
 
         // If someone is waiting for a state, everything is runnable now
@@ -357,6 +375,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) {
@@ -394,12 +419,14 @@
             mRunnableHead = removeFromRunnableList(mRunnableHead, queue);
 
             // Emit all trace events for this process into a consistent track
-            queue.traceTrackName = TAG + ".mRunning[" + queueIndex + "]";
+            queue.runningTraceTrackName = TAG + ".mRunning[" + queueIndex + "]";
+            queue.runningOomAdjusted = queue.isPendingManifest();
 
-            // If we're already warm, boost OOM adjust now; if cold we'll boost
-            // it after the app has been started
+            // If already warm, we can make OOM adjust request immediately;
+            // otherwise we need to wait until process becomes warm
             if (processWarm) {
                 notifyStartedRunning(queue);
+                updateOomAdj |= queue.runningOomAdjusted;
             }
 
             // If we're already warm, schedule next pending broadcast now;
@@ -413,10 +440,6 @@
                 scheduleReceiverColdLocked(queue);
             }
 
-            // We've moved at least one process into running state above, so we
-            // need to kick off an OOM adjustment pass
-            updateOomAdj = true;
-
             // Move to considering next runnable queue
             queue = nextQueue;
         }
@@ -436,18 +459,29 @@
             });
         }
 
-        traceEnd(TAG, cookie);
+        traceEnd(cookie);
     }
 
     @Override
     public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app) {
+        // Process records can be recycled, so always start by looking up the
+        // relevant per-process queue
+        final BroadcastProcessQueue queue = getProcessQueue(app);
+        if (queue != null) {
+            queue.setProcess(app);
+        }
+
         boolean didSomething = false;
-        if ((mRunningColdStart != null) && (mRunningColdStart.app == app)) {
+        if ((mRunningColdStart != null) && (mRunningColdStart == queue)) {
             // We've been waiting for this app to cold start, and it's ready
             // now; dispatch its next broadcast and clear the slot
-            final BroadcastProcessQueue queue = mRunningColdStart;
             mRunningColdStart = null;
 
+            // Now that we're running warm, we can finally request that OOM
+            // adjust we've been waiting for
+            notifyStartedRunning(queue);
+            mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER);
+
             queue.traceProcessEnd();
             queue.traceProcessRunningBegin();
             scheduleReceiverWarmLocked(queue);
@@ -471,22 +505,29 @@
 
     @Override
     public void onApplicationCleanupLocked(@NonNull ProcessRecord app) {
-        if ((mRunningColdStart != null) && (mRunningColdStart.app == app)) {
+        // Process records can be recycled, so always start by looking up the
+        // relevant per-process queue
+        final BroadcastProcessQueue queue = getProcessQueue(app);
+        if (queue != null) {
+            queue.setProcess(null);
+        }
+
+        if ((mRunningColdStart != null) && (mRunningColdStart == queue)) {
             // We've been waiting for this app to cold start, and it had
             // trouble; clear the slot and fail delivery below
             mRunningColdStart = null;
 
+            queue.traceProcessEnd();
+
             // We might be willing to kick off another cold start
             enqueueUpdateRunningList();
         }
 
-        final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
-            queue.app = null;
-
             // If queue was running a broadcast, fail it
             if (queue.isActive()) {
-                finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+                finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
+                        "onApplicationCleanupLocked");
             }
 
             // Skip any pending registered receivers, since the old process
@@ -512,6 +553,9 @@
 
     @Override
     public void enqueueBroadcastLocked(@NonNull BroadcastRecord r) {
+        if (DEBUG_BROADCAST) logv("Enqueuing " + r + " for " + r.receivers.size() + " receivers");
+
+        final int cookie = traceBegin("enqueueBroadcast");
         r.applySingletonPolicy(mService);
 
         final IntentFilter removeMatchingFilter = (r.options != null)
@@ -526,6 +570,8 @@
             }, mBroadcastConsumerSkipAndCanceled, true);
         }
 
+        applyDeliveryGroupPolicy(r);
+
         if (r.isReplacePending()) {
             // Leave the skipped broadcasts intact in queue, so that we can
             // replace them at their current position during enqueue below
@@ -541,36 +587,11 @@
         r.enqueueRealTime = SystemClock.elapsedRealtime();
         r.enqueueClockTime = System.currentTimeMillis();
 
-        int lastPriority = 0;
-        int lastPriorityIndex = 0;
-
         for (int i = 0; i < r.receivers.size(); i++) {
             final Object receiver = r.receivers.get(i);
             final BroadcastProcessQueue queue = getOrCreateProcessQueue(
                     getReceiverProcessName(receiver), getReceiverUid(receiver));
-
-            final int blockedUntilTerminalCount;
-            if (r.ordered) {
-                // When sending an ordered broadcast, we need to block this
-                // receiver until all previous receivers have terminated
-                blockedUntilTerminalCount = i;
-            } else if (r.prioritized) {
-                // When sending a prioritized broadcast, we only need to wait
-                // for the previous traunch of receivers to be terminated
-                final int thisPriority = getReceiverPriority(receiver);
-                if ((i == 0) || (thisPriority != lastPriority)) {
-                    lastPriority = thisPriority;
-                    lastPriorityIndex = i;
-                    blockedUntilTerminalCount = i;
-                } else {
-                    blockedUntilTerminalCount = lastPriorityIndex;
-                }
-            } else {
-                // Otherwise we don't need to block at all
-                blockedUntilTerminalCount = 0;
-            }
-
-            queue.enqueueOrReplaceBroadcast(r, i, blockedUntilTerminalCount);
+            queue.enqueueOrReplaceBroadcast(r, i);
             updateRunnableList(queue);
             enqueueUpdateRunningList();
         }
@@ -578,7 +599,45 @@
         // If nothing to dispatch, send any pending result immediately
         if (r.receivers.isEmpty()) {
             scheduleResultTo(r);
+            notifyFinishBroadcast(r);
         }
+
+        traceEnd(cookie);
+    }
+
+    private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
+        final int policy = (r.options != null)
+                ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
+        final BroadcastConsumer broadcastConsumer;
+        switch (policy) {
+            case BroadcastOptions.DELIVERY_GROUP_POLICY_ALL:
+                // Older broadcasts need to be left as is in this case, so nothing more to do.
+                return;
+            case BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT:
+                broadcastConsumer = mBroadcastConsumerSkipAndCanceled;
+                break;
+            case BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED:
+                final BundleMerger extrasMerger = r.options.getDeliveryGroupExtrasMerger();
+                if (extrasMerger == null) {
+                    // Extras merger is required to be able to merge the extras. So, if it's not
+                    // supplied, then ignore the delivery group policy.
+                    return;
+                }
+                broadcastConsumer = (record, recordIndex) -> {
+                    r.intent.mergeExtras(record.intent, extrasMerger);
+                    mBroadcastConsumerSkipAndCanceled.accept(record, recordIndex);
+                };
+                break;
+            default:
+                logw("Unknown delivery group policy: " + policy);
+                return;
+        }
+        forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
+            // We only allow caller to remove broadcasts they enqueued
+            return (r.callingUid == testRecord.callingUid)
+                    && (r.userId == testRecord.userId)
+                    && r.matchesDeliveryGroup(testRecord);
+        }, broadcastConsumer, true);
     }
 
     /**
@@ -599,7 +658,8 @@
         // Ignore registered receivers from a previous PID
         if (receiver instanceof BroadcastFilter) {
             mRunningColdStart = null;
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
+                    "BroadcastFilter for cold app");
             return;
         }
 
@@ -619,11 +679,10 @@
         if (DEBUG_BROADCAST) logv("Scheduling " + r + " to cold " + queue);
         queue.app = mService.startProcessLocked(queue.processName, info, true, intentFlags,
                 hostingRecord, zygotePolicyFlags, allowWhileBooting, false);
-        if (queue.app != null) {
-            notifyStartedRunning(queue);
-        } else {
+        if (queue.app == null) {
             mRunningColdStart = null;
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE,
+                    "startProcessLocked failed");
             return;
         }
     }
@@ -654,38 +713,43 @@
         // If someone already finished this broadcast, finish immediately
         final int oldDeliveryState = getDeliveryState(r, index);
         if (isDeliveryStateTerminal(oldDeliveryState)) {
-            finishReceiverLocked(queue, oldDeliveryState);
+            enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state");
             return;
         }
 
         // Consider additional cases where we'd want to finish immediately
         if (app.isInFullBackup()) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
             return;
         }
         if (mSkipPolicy.shouldSkip(r, receiver)) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
             return;
         }
         final Intent receiverIntent = r.getReceiverIntent(receiver);
         if (receiverIntent == null) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "getReceiverIntent");
             return;
         }
 
         // Ignore registered receivers from a previous PID
         if ((receiver instanceof BroadcastFilter)
                 && ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
+                    "BroadcastFilter for mismatched PID");
             return;
         }
 
-        if (mService.mProcessesReady && !r.timeoutExempt) {
+        // Skip ANR tracking early during boot, when requested, or when we
+        // immediately assume delivery success
+        final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered;
+        if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
             queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
 
-            final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT;
-            mLocalHandler.sendMessageDelayed(
-                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_SOFT, queue), timeout);
+            final int softTimeoutMillis = (int) (r.isForeground() ? mFgConstants.TIMEOUT
+                    : mBgConstants.TIMEOUT);
+            mLocalHandler.sendMessageDelayed(Message.obtain(mLocalHandler,
+                    MSG_DELIVERY_TIMEOUT_SOFT, softTimeoutMillis, 0, queue), softTimeoutMillis);
         }
 
         if (r.allowBackgroundActivityStarts) {
@@ -708,9 +772,10 @@
         }
 
         if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app);
-        setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED);
+        setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED,
+                "scheduleReceiverWarmLocked");
 
-        final IApplicationThread thread = app.getThread();
+        final IApplicationThread thread = app.getOnewayThread();
         if (thread != null) {
             try {
                 if (receiver instanceof BroadcastFilter) {
@@ -722,8 +787,9 @@
 
                     // TODO: consider making registered receivers of unordered
                     // broadcasts report results to detect ANRs
-                    if (!r.ordered) {
-                        finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
+                    if (assumeDelivered) {
+                        enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_DELIVERED,
+                                "assuming delivered");
                     }
                 } else {
                     notifyScheduleReceiver(app, r, (ResolveInfo) receiver);
@@ -737,10 +803,11 @@
                 logw(msg);
                 app.scheduleCrashLocked(msg, CannotDeliverBroadcastException.TYPE_ID, null);
                 app.setKilled(true);
-                finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+                enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, "remote app");
             }
         } else {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE,
+                    "missing IApplicationThread");
         }
     }
 
@@ -749,9 +816,9 @@
      * ordered broadcast; assumes the sender is still a warm process.
      */
     private void scheduleResultTo(@NonNull BroadcastRecord r) {
-        if ((r.callerApp == null) || (r.resultTo == null)) return;
-        final ProcessRecord app = r.callerApp;
-        final IApplicationThread thread = app.getThread();
+        if (r.resultTo == null) return;
+        final ProcessRecord app = r.resultToApp;
+        final IApplicationThread thread = (app != null) ? app.getOnewayThread() : null;
         if (thread != null) {
             mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(
                     app, OOM_ADJ_REASON_FINISH_RECEIVER);
@@ -769,22 +836,25 @@
         r.resultTo = null;
     }
 
-    private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue) {
+    private void deliveryTimeoutSoftLocked(@NonNull BroadcastProcessQueue queue,
+            int softTimeoutMillis) {
         if (queue.app != null) {
             // Instead of immediately triggering an ANR, extend the timeout by
             // the amount of time the process was runnable-but-waiting; we're
             // only willing to do this once before triggering an hard ANR
             final long cpuDelayTime = queue.app.getCpuDelayTime() - queue.lastCpuDelayTime;
-            final long timeout = MathUtils.constrain(cpuDelayTime, 0, mConstants.TIMEOUT);
+            final long hardTimeoutMillis = MathUtils.constrain(cpuDelayTime, 0, softTimeoutMillis);
             mLocalHandler.sendMessageDelayed(
-                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue), timeout);
+                    Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT_HARD, queue),
+                    hardTimeoutMillis);
         } else {
             deliveryTimeoutHardLocked(queue);
         }
     }
 
     private void deliveryTimeoutHardLocked(@NonNull BroadcastProcessQueue queue) {
-        finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT);
+        finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT,
+                "deliveryTimeoutHardLocked");
     }
 
     @Override
@@ -798,57 +868,64 @@
         }
 
         final BroadcastRecord r = queue.getActive();
-        r.resultCode = resultCode;
-        r.resultData = resultData;
-        r.resultExtras = resultExtras;
-        if (!r.isNoAbort()) {
-            r.resultAbort = resultAbort;
-        }
+        if (r.ordered) {
+            r.resultCode = resultCode;
+            r.resultData = resultData;
+            r.resultExtras = resultExtras;
+            if (!r.isNoAbort()) {
+                r.resultAbort = resultAbort;
+            }
 
-        // When the caller aborted an ordered broadcast, we mark all remaining
-        // receivers as skipped
-        if (r.ordered && r.resultAbort) {
-            for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
-                setDeliveryState(null, null, r, i, r.receivers.get(i),
-                        BroadcastRecord.DELIVERY_SKIPPED);
+            // When the caller aborted an ordered broadcast, we mark all
+            // remaining receivers as skipped
+            if (r.resultAbort) {
+                for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
+                    setDeliveryState(null, null, r, i, r.receivers.get(i),
+                            BroadcastRecord.DELIVERY_SKIPPED, "resultAbort");
+                }
             }
         }
 
-        return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
+        return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
     }
 
     private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
-            @DeliveryState int deliveryState) {
-        checkState(queue.isActive(), "isActive");
+            @DeliveryState int deliveryState, @NonNull String reason) {
+        if (!queue.isActive()) {
+            logw("Ignoring finish; no active broadcast for " + queue);
+            return false;
+        }
 
+        final int cookie = traceBegin("finishReceiver");
         final ProcessRecord app = queue.app;
         final BroadcastRecord r = queue.getActive();
         final int index = queue.getActiveIndex();
         final Object receiver = r.receivers.get(index);
 
-        setDeliveryState(queue, app, r, index, receiver, deliveryState);
+        setDeliveryState(queue, app, r, index, receiver, deliveryState, reason);
 
         if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) {
             r.anrCount++;
             if (app != null && !app.isDebugging()) {
-                mService.appNotResponding(queue.app, TimeoutRecord
-                        .forBroadcastReceiver("Broadcast of " + r.toShortString()));
+                mService.appNotResponding(queue.app, TimeoutRecord.forBroadcastReceiver(r.intent));
             }
         } else {
             mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_SOFT, queue);
             mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_HARD, queue);
         }
 
-        // Even if we have more broadcasts, if we've made reasonable progress
-        // and someone else is waiting, retire ourselves to avoid starvation
-        final boolean shouldRetire = (mRunnableHead != null)
-                && (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
+        // If we've made reasonable progress, periodically retire ourselves to
+        // avoid starvation of other processes and stack overflow when a
+        // broadcast is immediately finished without waiting
+        final boolean shouldRetire =
+                (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
 
+        final boolean res;
         if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
             // We're on a roll; move onto the next broadcast for this process
             queue.makeActiveNextPending();
             scheduleReceiverWarmLocked(queue);
-            return true;
+            res = true;
         } else {
             // We've drained running broadcasts; maybe move back to runnable
             queue.makeActiveIdle();
@@ -862,8 +939,10 @@
             // Tell other OS components that app is not actively running, giving
             // a chance to update OOM adjustment
             notifyStoppedRunning(queue);
-            return false;
+            res = false;
         }
+        traceEnd(cookie);
+        return res;
     }
 
     /**
@@ -872,7 +951,8 @@
      */
     private void setDeliveryState(@Nullable BroadcastProcessQueue queue,
             @Nullable ProcessRecord app, @NonNull BroadcastRecord r, int index,
-            @NonNull Object receiver, @DeliveryState int newDeliveryState) {
+            @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) {
+        final int cookie = traceBegin("setDeliveryState");
         final int oldDeliveryState = getDeliveryState(r, index);
 
         // Only apply state when we haven't already reached a terminal state;
@@ -900,14 +980,15 @@
                 logw("Delivery state of " + r + " to " + receiver
                         + " via " + app + " changed from "
                         + deliveryStateToString(oldDeliveryState) + " to "
-                        + deliveryStateToString(newDeliveryState));
+                        + deliveryStateToString(newDeliveryState) + " because " + reason);
             }
 
             r.terminalCount++;
             notifyFinishReceiver(queue, r, index, receiver);
 
             // When entire ordered broadcast finished, deliver final result
-            if (r.ordered && (r.terminalCount == r.receivers.size())) {
+            final boolean recordFinished = (r.terminalCount == r.receivers.size());
+            if (recordFinished) {
                 scheduleResultTo(r);
             }
 
@@ -929,6 +1010,8 @@
                 enqueueUpdateRunningList();
             }
         }
+
+        traceEnd(cookie);
     }
 
     private @DeliveryState int getDeliveryState(@NonNull BroadcastRecord r, int index) {
@@ -989,7 +1072,8 @@
      * of it matching a predicate.
      */
     private final BroadcastConsumer mBroadcastConsumerSkip = (r, i) -> {
-        setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+        setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED,
+                "mBroadcastConsumerSkip");
     };
 
     /**
@@ -997,7 +1081,8 @@
      * cancelled, usually as a result of it matching a predicate.
      */
     private final BroadcastConsumer mBroadcastConsumerSkipAndCanceled = (r, i) -> {
-        setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+        setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED,
+                "mBroadcastConsumerSkipAndCanceled");
         r.resultCode = Activity.RESULT_CANCELED;
         r.resultData = null;
         r.resultExtras = null;
@@ -1013,7 +1098,11 @@
             BroadcastProcessQueue leaf = mProcessQueues.valueAt(i);
             while (leaf != null) {
                 if (!test.test(leaf)) {
-                    logv("Test " + label + " failed due to " + leaf.toShortString(), pw);
+                    final long now = SystemClock.uptimeMillis();
+                    if (now > mLastTestFailureTime + DateUtils.SECOND_IN_MILLIS) {
+                        mLastTestFailureTime = now;
+                        logv("Test " + label + " failed due to " + leaf.toShortString(), pw);
+                    }
                     return false;
                 }
                 leaf = leaf.processNameNext;
@@ -1159,6 +1248,12 @@
                 }
             }
 
+            // Verify that pending cold start hasn't been orphaned
+            if (mRunningColdStart != null) {
+                checkState(getRunningIndexOf(mRunningColdStart) >= 0,
+                        "isOrphaned " + mRunningColdStart);
+            }
+
             // Verify health of all known process queues
             for (int i = 0; i < mProcessQueues.size(); i++) {
                 BroadcastProcessQueue leaf = mProcessQueues.valueAt(i);
@@ -1179,21 +1274,9 @@
         }
     }
 
-    private int traceBegin(String trackName, String methodName) {
-        final int cookie = methodName.hashCode();
-        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                trackName, methodName, cookie);
-        return cookie;
-    }
-
-    private void traceEnd(String trackName, int cookie) {
-        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
-                trackName, cookie);
-    }
-
     private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) {
         if (!queue.isProcessWarm()) {
-            queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid);
+            queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid));
         }
     }
 
@@ -1205,8 +1288,6 @@
         if (queue.app != null) {
             queue.app.mReceivers.incrementCurReceivers();
 
-            queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
-
             // Don't bump its LRU position if it's in the background restricted.
             if (mService.mInternal.getRestrictionLevel(
                     queue.uid) < ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
@@ -1216,7 +1297,10 @@
             mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(queue.app,
                     OOM_ADJ_REASON_START_RECEIVER);
 
-            mService.enqueueOomAdjTargetLocked(queue.app);
+            if (queue.runningOomAdjusted) {
+                queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
+                mService.enqueueOomAdjTargetLocked(queue.app);
+            }
         }
     }
 
@@ -1226,10 +1310,11 @@
      */
     private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) {
         if (queue.app != null) {
-            // Update during our next pass; no need for an immediate update
-            mService.enqueueOomAdjTargetLocked(queue.app);
-
             queue.app.mReceivers.decrementCurReceivers();
+
+            if (queue.runningOomAdjusted) {
+                mService.enqueueOomAdjTargetLocked(queue.app);
+            }
         }
     }
 
@@ -1320,29 +1405,34 @@
 
         final boolean recordFinished = (r.terminalCount == r.receivers.size());
         if (recordFinished) {
-            mHistory.addBroadcastToHistoryLocked(r);
+            notifyFinishBroadcast(r);
+        }
+    }
 
-            r.finishTime = SystemClock.uptimeMillis();
-            r.nextReceiver = r.receivers.size();
-            BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+    private void notifyFinishBroadcast(@NonNull BroadcastRecord r) {
+        mService.notifyBroadcastFinishedLocked(r);
+        mHistory.addBroadcastToHistoryLocked(r);
 
-            if (r.intent.getComponent() == null && r.intent.getPackage() == null
-                    && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
-                int manifestCount = 0;
-                int manifestSkipCount = 0;
-                for (int i = 0; i < r.receivers.size(); i++) {
-                    if (r.receivers.get(i) instanceof ResolveInfo) {
-                        manifestCount++;
-                        if (r.delivery[i] == BroadcastRecord.DELIVERY_SKIPPED) {
-                            manifestSkipCount++;
-                        }
+        r.finishTime = SystemClock.uptimeMillis();
+        r.nextReceiver = r.receivers.size();
+        BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+
+        if (r.intent.getComponent() == null && r.intent.getPackage() == null
+                && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+            int manifestCount = 0;
+            int manifestSkipCount = 0;
+            for (int i = 0; i < r.receivers.size(); i++) {
+                if (r.receivers.get(i) instanceof ResolveInfo) {
+                    manifestCount++;
+                    if (r.delivery[i] == BroadcastRecord.DELIVERY_SKIPPED) {
+                        manifestSkipCount++;
                     }
                 }
-
-                final long dispatchTime = SystemClock.uptimeMillis() - r.enqueueTime;
-                mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
-                        manifestCount, manifestSkipCount, dispatchTime);
             }
+
+            final long dispatchTime = SystemClock.uptimeMillis() - r.enqueueTime;
+            mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
+                    manifestCount, manifestSkipCount, dispatchTime);
         }
     }
 
@@ -1365,7 +1455,7 @@
         }
 
         BroadcastProcessQueue created = new BroadcastProcessQueue(mConstants, processName, uid);
-        created.app = mService.getProcessRecordLocked(processName, uid);
+        created.setProcess(mService.getProcessRecordLocked(processName, uid));
 
         if (leaf == null) {
             mProcessQueues.put(uid, created);
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index bcc76e9..84d7442 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -78,11 +78,13 @@
     final int callingPid;   // the pid of who sent this
     final int callingUid;   // the uid of who sent this
     final boolean callerInstantApp; // caller is an Instant App?
+    final boolean callerInstrumented; // caller is being instrumented?
     final boolean ordered;  // serialize the send to receivers?
     final boolean sticky;   // originated from existing sticky data?
     final boolean alarm;    // originated from an alarm triggering?
     final boolean pushMessage; // originated from a push message?
     final boolean pushMessageOverQuota; // originated from a push message which was over quota?
+    final boolean interactive; // originated from user interaction?
     final boolean initialSticky; // initial broadcast from register to sticky?
     final boolean prioritized; // contains more than one priority tranche
     final int userId;       // user id this broadcast was for
@@ -94,6 +96,8 @@
     final @Nullable BroadcastOptions options; // BroadcastOptions supplied by caller
     final @NonNull List<Object> receivers;   // contains BroadcastFilter and ResolveInfo
     final @DeliveryState int[] delivery;   // delivery state of each receiver
+    final int[] blockedUntilTerminalCount; // blocked until count of each receiver
+    @Nullable ProcessRecord resultToApp; // who receives final result if non-null
     @Nullable IIntentReceiver resultTo; // who receives final result if non-null
     boolean deferred;
     int splitCount;         // refcount for result callback, when split
@@ -345,7 +349,8 @@
             boolean _callerInstantApp, String _resolvedType,
             String[] _requiredPermissions, String[] _excludedPermissions,
             String[] _excludedPackages, int _appOp,
-            BroadcastOptions _options, List _receivers, IIntentReceiver _resultTo, int _resultCode,
+            BroadcastOptions _options, List _receivers,
+            ProcessRecord _resultToApp, IIntentReceiver _resultTo, int _resultCode,
             String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky,
             boolean _initialSticky, int _userId, boolean allowBackgroundActivityStarts,
             @Nullable IBinder backgroundActivityStartsToken, boolean timeoutExempt,
@@ -362,6 +367,7 @@
         callingPid = _callingPid;
         callingUid = _callingUid;
         callerInstantApp = _callerInstantApp;
+        callerInstrumented = isCallerInstrumented(_callerApp, _callingUid);
         resolvedType = _resolvedType;
         requiredPermissions = _requiredPermissions;
         excludedPermissions = _excludedPermissions;
@@ -370,8 +376,10 @@
         options = _options;
         receivers = (_receivers != null) ? _receivers : EMPTY_RECEIVERS;
         delivery = new int[_receivers != null ? _receivers.size() : 0];
+        blockedUntilTerminalCount = calculateBlockedUntilTerminalCount(receivers, _serialized);
         scheduledTime = new long[delivery.length];
         terminalTime = new long[delivery.length];
+        resultToApp = _resultToApp;
         resultTo = _resultTo;
         resultCode = _resultCode;
         resultData = _resultData;
@@ -379,7 +387,7 @@
         ordered = _serialized;
         sticky = _sticky;
         initialSticky = _initialSticky;
-        prioritized = isPrioritized(receivers);
+        prioritized = isPrioritized(blockedUntilTerminalCount, _serialized);
         userId = _userId;
         nextReceiver = 0;
         state = IDLE;
@@ -389,6 +397,7 @@
         alarm = options != null && options.isAlarmBroadcast();
         pushMessage = options != null && options.isPushMessagingBroadcast();
         pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
+        interactive = options != null && options.isInteractive();
         this.filterExtrasForReceiver = filterExtrasForReceiver;
     }
 
@@ -406,6 +415,7 @@
         callingPid = from.callingPid;
         callingUid = from.callingUid;
         callerInstantApp = from.callerInstantApp;
+        callerInstrumented = from.callerInstrumented;
         ordered = from.ordered;
         sticky = from.sticky;
         initialSticky = from.initialSticky;
@@ -419,8 +429,10 @@
         options = from.options;
         receivers = from.receivers;
         delivery = from.delivery;
+        blockedUntilTerminalCount = from.blockedUntilTerminalCount;
         scheduledTime = from.scheduledTime;
         terminalTime = from.terminalTime;
+        resultToApp = from.resultToApp;
         resultTo = from.resultTo;
         enqueueTime = from.enqueueTime;
         enqueueRealTime = from.enqueueRealTime;
@@ -446,6 +458,7 @@
         alarm = from.alarm;
         pushMessage = from.pushMessage;
         pushMessageOverQuota = from.pushMessageOverQuota;
+        interactive = from.interactive;
         filterExtrasForReceiver = from.filterExtrasForReceiver;
     }
 
@@ -480,8 +493,8 @@
         BroadcastRecord split = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                 requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
-                splitReceivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky,
-                initialSticky, userId, allowBackgroundActivityStarts,
+                splitReceivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
+                ordered, sticky, initialSticky, userId, allowBackgroundActivityStarts,
                 mBackgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
         split.enqueueTime = this.enqueueTime;
         split.enqueueRealTime = this.enqueueRealTime;
@@ -559,7 +572,7 @@
             final BroadcastRecord br = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
-                    uid2receiverList.valueAt(i), null /* _resultTo */,
+                    uid2receiverList.valueAt(i), null /* _resultToApp */, null /* _resultTo */,
                     resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId,
                     allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt,
                     filterExtrasForReceiver);
@@ -607,6 +620,22 @@
         return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
     }
 
+    boolean isOffload() {
+        return (intent.getFlags() & Intent.FLAG_RECEIVER_OFFLOAD) != 0;
+    }
+
+    /**
+     * Core policy determination about this broadcast's delivery prioritization
+     */
+    boolean isUrgent() {
+        // TODO: flags for controlling policy
+        // TODO: migrate alarm-prioritization flag to BroadcastConstants
+        return (isForeground()
+                || interactive
+                || alarm)
+                && receivers.size() == 1;
+    }
+
     @NonNull String getHostingRecordTriggerType() {
         if (alarm) {
             return HostingRecord.TRIGGER_TYPE_ALARM;
@@ -652,23 +681,72 @@
         return (newIntent != null) ? newIntent : intent;
     }
 
-    /**
-     * Return if given receivers list has more than one traunch of priorities.
-     */
-    @VisibleForTesting
-    static boolean isPrioritized(@NonNull List<Object> receivers) {
-        int firstPriority = 0;
-        for (int i = 0; i < receivers.size(); i++) {
-            final int thisPriority = getReceiverPriority(receivers.get(i));
-            if (i == 0) {
-                firstPriority = thisPriority;
-            } else if (thisPriority != firstPriority) {
+    static boolean isCallerInstrumented(@Nullable ProcessRecord callerApp, int callingUid) {
+        switch (UserHandle.getAppId(callingUid)) {
+            case android.os.Process.ROOT_UID:
+            case android.os.Process.SHELL_UID:
+                // Broadcasts sent via "shell" are typically invoked by test
+                // suites, so we treat them as if the caller was instrumented
                 return true;
-            }
         }
-        return false;
+        return (callerApp != null) ? (callerApp.getActiveInstrumentation() != null) : false;
     }
 
+    /**
+     * Determine if the result of {@link #calculateBlockedUntilTerminalCount}
+     * has prioritized tranches of receivers.
+     */
+    @VisibleForTesting
+    static boolean isPrioritized(@NonNull int[] blockedUntilTerminalCount,
+            boolean ordered) {
+        return !ordered && (blockedUntilTerminalCount.length > 0)
+                && (blockedUntilTerminalCount[0] != -1);
+    }
+
+    /**
+     * Calculate the {@link #terminalCount} that each receiver should be
+     * considered blocked until.
+     * <p>
+     * For example, in an ordered broadcast, receiver {@code N} is blocked until
+     * receiver {@code N-1} reaches a terminal state. Similarly, in a
+     * prioritized broadcast, receiver {@code N} is blocked until all receivers
+     * of a higher priority reach a terminal state.
+     * <p>
+     * When there are no terminal count constraints, the blocked value for each
+     * receiver is {@code -1}.
+     */
+    @VisibleForTesting
+    static @NonNull int[] calculateBlockedUntilTerminalCount(
+            @NonNull List<Object> receivers, boolean ordered) {
+        final int N = receivers.size();
+        final int[] blockedUntilTerminalCount = new int[N];
+        int lastPriority = 0;
+        int lastPriorityIndex = 0;
+        for (int i = 0; i < N; i++) {
+            if (ordered) {
+                // When sending an ordered broadcast, we need to block this
+                // receiver until all previous receivers have terminated
+                blockedUntilTerminalCount[i] = i;
+            } else {
+                // When sending a prioritized broadcast, we only need to wait
+                // for the previous tranche of receivers to be terminated
+                final int thisPriority = getReceiverPriority(receivers.get(i));
+                if ((i == 0) || (thisPriority != lastPriority)) {
+                    lastPriority = thisPriority;
+                    lastPriorityIndex = i;
+                    blockedUntilTerminalCount[i] = i;
+                } else {
+                    blockedUntilTerminalCount[i] = lastPriorityIndex;
+                }
+            }
+        }
+        // If the entire list is in the same priority tranche, mark as -1 to
+        // indicate that none of them need to wait
+        if (N > 0 && blockedUntilTerminalCount[N - 1] == 0) {
+            Arrays.fill(blockedUntilTerminalCount, -1);
+        }
+        return blockedUntilTerminalCount;
+    }
 
     static int getReceiverUid(@NonNull Object receiver) {
         if (receiver instanceof BroadcastFilter) {
@@ -792,6 +870,16 @@
         }
     }
 
+    public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) {
+        final String key = (options != null) ? options.getDeliveryGroupKey() : null;
+        final String otherKey = (other.options != null)
+                ? other.options.getDeliveryGroupKey() : null;
+        if (key == null && otherKey == null) {
+            return intent.filterEquals(other.intent);
+        }
+        return Objects.equals(key, otherKey);
+    }
+
     @Override
     public String toString() {
         if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index 60fddf0..481ab17 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -21,6 +21,7 @@
 import static com.android.server.am.BroadcastQueue.TAG;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -42,6 +43,8 @@
 
 import com.android.internal.util.ArrayUtils;
 
+import java.util.Objects;
+
 /**
  * Policy logic that decides if delivery of a particular {@link BroadcastRecord}
  * should be skipped for a given {@link ResolveInfo} or {@link BroadcastFilter}.
@@ -51,8 +54,8 @@
 public class BroadcastSkipPolicy {
     private final ActivityManagerService mService;
 
-    public BroadcastSkipPolicy(ActivityManagerService service) {
-        mService = service;
+    public BroadcastSkipPolicy(@NonNull ActivityManagerService service) {
+        mService = Objects.requireNonNull(service);
     }
 
     /**
@@ -60,18 +63,39 @@
      * the given {@link BroadcastFilter} or {@link ResolveInfo}.
      */
     public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull Object target) {
-        if (target instanceof BroadcastFilter) {
-            return shouldSkip(r, (BroadcastFilter) target);
+        final String msg = shouldSkipMessage(r, target);
+        if (msg != null) {
+            Slog.w(TAG, msg);
+            return true;
         } else {
-            return shouldSkip(r, (ResolveInfo) target);
+            return false;
+        }
+    }
+
+    /**
+     * Determine if the given {@link BroadcastRecord} is eligible to be sent to
+     * the given {@link BroadcastFilter} or {@link ResolveInfo}.
+     *
+     * @return message indicating why the argument should be skipped, otherwise
+     *         {@code null} if it can proceed.
+     */
+    public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+        if (target instanceof BroadcastFilter) {
+            return shouldSkipMessage(r, (BroadcastFilter) target);
+        } else {
+            return shouldSkipMessage(r, (ResolveInfo) target);
         }
     }
 
     /**
      * Determine if the given {@link BroadcastRecord} is eligible to be sent to
      * the given {@link ResolveInfo}.
+     *
+     * @return message indicating why the argument should be skipped, otherwise
+     *         {@code null} if it can proceed.
      */
-    public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull ResolveInfo info) {
+    private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
+            @NonNull ResolveInfo info) {
         final BroadcastOptions brOptions = r.options;
         final ComponentName component = new ComponentName(
                 info.activityInfo.applicationInfo.packageName,
@@ -82,58 +106,52 @@
                         < brOptions.getMinManifestReceiverApiLevel() ||
                 info.activityInfo.applicationInfo.targetSdkVersion
                         > brOptions.getMaxManifestReceiverApiLevel())) {
-            Slog.w(TAG, "Target SDK mismatch: receiver " + info.activityInfo
+            return "Target SDK mismatch: receiver " + info.activityInfo
                     + " targets " + info.activityInfo.applicationInfo.targetSdkVersion
                     + " but delivery restricted to ["
                     + brOptions.getMinManifestReceiverApiLevel() + ", "
                     + brOptions.getMaxManifestReceiverApiLevel()
-                    + "] broadcasting " + broadcastDescription(r, component));
-            return true;
+                    + "] broadcasting " + broadcastDescription(r, component);
         }
         if (brOptions != null &&
                 !brOptions.testRequireCompatChange(info.activityInfo.applicationInfo.uid)) {
-            Slog.w(TAG, "Compat change filtered: broadcasting " + broadcastDescription(r, component)
+            return "Compat change filtered: broadcasting " + broadcastDescription(r, component)
                     + " to uid " + info.activityInfo.applicationInfo.uid + " due to compat change "
-                    + r.options.getRequireCompatChangeId());
-            return true;
+                    + r.options.getRequireCompatChangeId();
         }
         if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid,
                 component.getPackageName(), info.activityInfo.applicationInfo.uid)) {
-            Slog.w(TAG, "Association not allowed: broadcasting "
-                    + broadcastDescription(r, component));
-            return true;
+            return "Association not allowed: broadcasting "
+                    + broadcastDescription(r, component);
         }
         if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid,
                 r.callingPid, r.resolvedType, info.activityInfo.applicationInfo.uid)) {
-            Slog.w(TAG, "Firewall blocked: broadcasting "
-                    + broadcastDescription(r, component));
-            return true;
+            return "Firewall blocked: broadcasting "
+                    + broadcastDescription(r, component);
         }
         int perm = checkComponentPermission(info.activityInfo.permission,
                 r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid,
                 info.activityInfo.exported);
         if (perm != PackageManager.PERMISSION_GRANTED) {
             if (!info.activityInfo.exported) {
-                Slog.w(TAG, "Permission Denial: broadcasting "
+                return "Permission Denial: broadcasting "
                         + broadcastDescription(r, component)
-                        + " is not exported from uid " + info.activityInfo.applicationInfo.uid);
+                        + " is not exported from uid " + info.activityInfo.applicationInfo.uid;
             } else {
-                Slog.w(TAG, "Permission Denial: broadcasting "
+                return "Permission Denial: broadcasting "
                         + broadcastDescription(r, component)
-                        + " requires " + info.activityInfo.permission);
+                        + " requires " + info.activityInfo.permission;
             }
-            return true;
         } else if (info.activityInfo.permission != null) {
             final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
             if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
                     r.callingUid, r.callerPackage, r.callerFeatureId,
                     "Broadcast delivered to " + info.activityInfo.name)
                     != AppOpsManager.MODE_ALLOWED) {
-                Slog.w(TAG, "Appop Denial: broadcasting "
+                return "Appop Denial: broadcasting "
                         + broadcastDescription(r, component)
                         + " requires appop " + AppOpsManager.permissionToOp(
-                                info.activityInfo.permission));
-                return true;
+                                info.activityInfo.permission);
             }
         }
 
@@ -142,38 +160,34 @@
                     android.Manifest.permission.INTERACT_ACROSS_USERS,
                     info.activityInfo.applicationInfo.uid)
                             != PackageManager.PERMISSION_GRANTED) {
-                Slog.w(TAG, "Permission Denial: Receiver " + component.flattenToShortString()
+                return "Permission Denial: Receiver " + component.flattenToShortString()
                         + " requests FLAG_SINGLE_USER, but app does not hold "
-                        + android.Manifest.permission.INTERACT_ACROSS_USERS);
-                return true;
+                        + android.Manifest.permission.INTERACT_ACROSS_USERS;
             }
         }
         if (info.activityInfo.applicationInfo.isInstantApp()
                 && r.callingUid != info.activityInfo.applicationInfo.uid) {
-            Slog.w(TAG, "Instant App Denial: receiving "
+            return "Instant App Denial: receiving "
                     + r.intent
                     + " to " + component.flattenToShortString()
                     + " due to sender " + r.callerPackage
                     + " (uid " + r.callingUid + ")"
-                    + " Instant Apps do not support manifest receivers");
-            return true;
+                    + " Instant Apps do not support manifest receivers";
         }
         if (r.callerInstantApp
                 && (info.activityInfo.flags & ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0
                 && r.callingUid != info.activityInfo.applicationInfo.uid) {
-            Slog.w(TAG, "Instant App Denial: receiving "
+            return "Instant App Denial: receiving "
                     + r.intent
                     + " to " + component.flattenToShortString()
                     + " requires receiver have visibleToInstantApps set"
                     + " due to sender " + r.callerPackage
-                    + " (uid " + r.callingUid + ")");
-            return true;
+                    + " (uid " + r.callingUid + ")";
         }
         if (r.curApp != null && r.curApp.mErrorState.isCrashing()) {
             // If the target process is crashing, just skip it.
-            Slog.w(TAG, "Skipping deliver ordered [" + r.queue.toString() + "] " + r
-                    + " to " + r.curApp + ": process crashing");
-            return true;
+            return "Skipping deliver ordered [" + r.queue.toString() + "] " + r
+                    + " to " + r.curApp + ": process crashing";
         }
 
         boolean isAvailable = false;
@@ -183,15 +197,13 @@
                     UserHandle.getUserId(info.activityInfo.applicationInfo.uid));
         } catch (Exception e) {
             // all such failures mean we skip this receiver
-            Slog.w(TAG, "Exception getting recipient info for "
-                    + info.activityInfo.packageName, e);
+            return "Exception getting recipient info for "
+                    + info.activityInfo.packageName;
         }
         if (!isAvailable) {
-            Slog.w(TAG,
-                    "Skipping delivery to " + info.activityInfo.packageName + " / "
+            return "Skipping delivery to " + info.activityInfo.packageName + " / "
                     + info.activityInfo.applicationInfo.uid
-                    + " : package no longer available");
-            return true;
+                    + " : package no longer available";
         }
 
         // If permissions need a review before any of the app components can run, we drop
@@ -201,10 +213,8 @@
         if (!requestStartTargetPermissionsReviewIfNeededLocked(r,
                 info.activityInfo.packageName, UserHandle.getUserId(
                         info.activityInfo.applicationInfo.uid))) {
-            Slog.w(TAG,
-                    "Skipping delivery: permission review required for "
-                            + broadcastDescription(r, component));
-            return true;
+            return "Skipping delivery: permission review required for "
+                            + broadcastDescription(r, component);
         }
 
         final int allowed = mService.getAppStartModeLOSP(
@@ -216,10 +226,9 @@
             // to it and the app is in a state that should not receive it
             // (depending on how getAppStartModeLOSP has determined that).
             if (allowed == ActivityManager.APP_START_MODE_DISABLED) {
-                Slog.w(TAG, "Background execution disabled: receiving "
+                return "Background execution disabled: receiving "
                         + r.intent + " to "
-                        + component.flattenToShortString());
-                return true;
+                        + component.flattenToShortString();
             } else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0)
                     || (r.intent.getComponent() == null
                         && r.intent.getPackage() == null
@@ -228,10 +237,9 @@
                         && !isSignaturePerm(r.requiredPermissions))) {
                 mService.addBackgroundCheckViolationLocked(r.intent.getAction(),
                         component.getPackageName());
-                Slog.w(TAG, "Background execution not allowed: receiving "
+                return "Background execution not allowed: receiving "
                         + r.intent + " to "
-                        + component.flattenToShortString());
-                return true;
+                        + component.flattenToShortString();
             }
         }
 
@@ -239,10 +247,8 @@
                 && !mService.mUserController
                 .isUserRunning(UserHandle.getUserId(info.activityInfo.applicationInfo.uid),
                         0 /* flags */)) {
-            Slog.w(TAG,
-                    "Skipping delivery to " + info.activityInfo.packageName + " / "
-                            + info.activityInfo.applicationInfo.uid + " : user is not running");
-            return true;
+            return "Skipping delivery to " + info.activityInfo.packageName + " / "
+                            + info.activityInfo.applicationInfo.uid + " : user is not running";
         }
 
         if (r.excludedPermissions != null && r.excludedPermissions.length > 0) {
@@ -268,13 +274,15 @@
                                 info.activityInfo.applicationInfo.uid,
                                 info.activityInfo.packageName)
                             == AppOpsManager.MODE_ALLOWED)) {
-                        return true;
+                        return "Skipping delivery to " + info.activityInfo.packageName
+                                + " due to excluded permission " + excludedPermission;
                     }
                 } else {
                     // When there is no app op associated with the permission,
                     // skip when permission is granted.
                     if (perm == PackageManager.PERMISSION_GRANTED) {
-                        return true;
+                        return "Skipping delivery to " + info.activityInfo.packageName
+                                + " due to excluded permission " + excludedPermission;
                     }
                 }
             }
@@ -283,13 +291,12 @@
         // Check that the receiver does *not* belong to any of the excluded packages
         if (r.excludedPackages != null && r.excludedPackages.length > 0) {
             if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) {
-                Slog.w(TAG, "Skipping delivery of excluded package "
+                return "Skipping delivery of excluded package "
                         + r.intent + " to "
                         + component.flattenToShortString()
                         + " excludes package " + component.getPackageName()
                         + " due to sender " + r.callerPackage
-                        + " (uid " + r.callingUid + ")");
-                return true;
+                        + " (uid " + r.callingUid + ")";
             }
         }
 
@@ -307,95 +314,94 @@
                     perm = PackageManager.PERMISSION_DENIED;
                 }
                 if (perm != PackageManager.PERMISSION_GRANTED) {
-                    Slog.w(TAG, "Permission Denial: receiving "
+                    return "Permission Denial: receiving "
                             + r.intent + " to "
                             + component.flattenToShortString()
                             + " requires " + requiredPermission
                             + " due to sender " + r.callerPackage
-                            + " (uid " + r.callingUid + ")");
-                    return true;
+                            + " (uid " + r.callingUid + ")";
                 }
                 int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
                 if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
                     if (!noteOpForManifestReceiver(appOp, r, info, component)) {
-                        return true;
+                        return "Skipping delivery to " + info.activityInfo.packageName
+                                + " due to required appop " + appOp;
                     }
                 }
             }
         }
         if (r.appOp != AppOpsManager.OP_NONE) {
             if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
-                return true;
+                return "Skipping delivery to " + info.activityInfo.packageName
+                        + " due to required appop " + r.appOp;
             }
         }
 
-        return false;
+        return null;
     }
 
     /**
      * Determine if the given {@link BroadcastRecord} is eligible to be sent to
      * the given {@link BroadcastFilter}.
+     *
+     * @return message indicating why the argument should be skipped, otherwise
+     *         {@code null} if it can proceed.
      */
-    public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull BroadcastFilter filter) {
+    private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
+            @NonNull BroadcastFilter filter) {
         if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
-            Slog.w(TAG, "Compat change filtered: broadcasting " + r.intent.toString()
+            return "Compat change filtered: broadcasting " + r.intent.toString()
                     + " to uid " + filter.owningUid + " due to compat change "
-                    + r.options.getRequireCompatChangeId());
-            return true;
+                    + r.options.getRequireCompatChangeId();
         }
         if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid,
                 filter.packageName, filter.owningUid)) {
-            Slog.w(TAG, "Association not allowed: broadcasting "
+            return "Association not allowed: broadcasting "
                     + r.intent.toString()
                     + " from " + r.callerPackage + " (pid=" + r.callingPid
                     + ", uid=" + r.callingUid + ") to " + filter.packageName + " through "
-                    + filter);
-            return true;
+                    + filter;
         }
         if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid,
                 r.callingPid, r.resolvedType, filter.receiverList.uid)) {
-            Slog.w(TAG, "Firewall blocked: broadcasting "
+            return "Firewall blocked: broadcasting "
                     + r.intent.toString()
                     + " from " + r.callerPackage + " (pid=" + r.callingPid
                     + ", uid=" + r.callingUid + ") to " + filter.packageName + " through "
-                    + filter);
-            return true;
+                    + filter;
         }
         // Check that the sender has permission to send to this receiver
         if (filter.requiredPermission != null) {
             int perm = checkComponentPermission(filter.requiredPermission,
                     r.callingPid, r.callingUid, -1, true);
             if (perm != PackageManager.PERMISSION_GRANTED) {
-                Slog.w(TAG, "Permission Denial: broadcasting "
+                return "Permission Denial: broadcasting "
                         + r.intent.toString()
                         + " from " + r.callerPackage + " (pid="
                         + r.callingPid + ", uid=" + r.callingUid + ")"
                         + " requires " + filter.requiredPermission
-                        + " due to registered receiver " + filter);
-                return true;
+                        + " due to registered receiver " + filter;
             } else {
                 final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
                 if (opCode != AppOpsManager.OP_NONE
                         && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
                         r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
                         != AppOpsManager.MODE_ALLOWED) {
-                    Slog.w(TAG, "Appop Denial: broadcasting "
+                    return "Appop Denial: broadcasting "
                             + r.intent.toString()
                             + " from " + r.callerPackage + " (pid="
                             + r.callingPid + ", uid=" + r.callingUid + ")"
                             + " requires appop " + AppOpsManager.permissionToOp(
                                     filter.requiredPermission)
-                            + " due to registered receiver " + filter);
-                    return true;
+                            + " due to registered receiver " + filter;
                 }
             }
         }
 
         if ((filter.receiverList.app == null || filter.receiverList.app.isKilled()
                 || filter.receiverList.app.mErrorState.isCrashing())) {
-            Slog.w(TAG, "Skipping deliver [" + r.queue.toString() + "] " + r
-                    + " to " + filter.receiverList + ": process gone or crashing");
-            return true;
+            return "Skipping deliver [" + r.queue.toString() + "] " + r
+                    + " to " + filter.receiverList + ": process gone or crashing";
         }
 
         // Ensure that broadcasts are only sent to other Instant Apps if they are marked as
@@ -405,28 +411,26 @@
 
         if (!visibleToInstantApps && filter.instantApp
                 && filter.receiverList.uid != r.callingUid) {
-            Slog.w(TAG, "Instant App Denial: receiving "
+            return "Instant App Denial: receiving "
                     + r.intent.toString()
                     + " to " + filter.receiverList.app
                     + " (pid=" + filter.receiverList.pid
                     + ", uid=" + filter.receiverList.uid + ")"
                     + " due to sender " + r.callerPackage
                     + " (uid " + r.callingUid + ")"
-                    + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS");
-            return true;
+                    + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS";
         }
 
         if (!filter.visibleToInstantApp && r.callerInstantApp
                 && filter.receiverList.uid != r.callingUid) {
-            Slog.w(TAG, "Instant App Denial: receiving "
+            return "Instant App Denial: receiving "
                     + r.intent.toString()
                     + " to " + filter.receiverList.app
                     + " (pid=" + filter.receiverList.pid
                     + ", uid=" + filter.receiverList.uid + ")"
                     + " requires receiver be visible to instant apps"
                     + " due to sender " + r.callerPackage
-                    + " (uid " + r.callingUid + ")");
-            return true;
+                    + " (uid " + r.callingUid + ")";
         }
 
         // Check that the receiver has the required permission(s) to receive this broadcast.
@@ -436,15 +440,14 @@
                 int perm = checkComponentPermission(requiredPermission,
                         filter.receiverList.pid, filter.receiverList.uid, -1, true);
                 if (perm != PackageManager.PERMISSION_GRANTED) {
-                    Slog.w(TAG, "Permission Denial: receiving "
+                    return "Permission Denial: receiving "
                             + r.intent.toString()
                             + " to " + filter.receiverList.app
                             + " (pid=" + filter.receiverList.pid
                             + ", uid=" + filter.receiverList.uid + ")"
                             + " requires " + requiredPermission
                             + " due to sender " + r.callerPackage
-                            + " (uid " + r.callingUid + ")");
-                    return true;
+                            + " (uid " + r.callingUid + ")";
                 }
                 int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
                 if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp
@@ -452,7 +455,7 @@
                         filter.receiverList.uid, filter.packageName, filter.featureId,
                         "Broadcast delivered to registered receiver " + filter.receiverId)
                         != AppOpsManager.MODE_ALLOWED) {
-                    Slog.w(TAG, "Appop Denial: receiving "
+                    return "Appop Denial: receiving "
                             + r.intent.toString()
                             + " to " + filter.receiverList.app
                             + " (pid=" + filter.receiverList.pid
@@ -460,8 +463,7 @@
                             + " requires appop " + AppOpsManager.permissionToOp(
                             requiredPermission)
                             + " due to sender " + r.callerPackage
-                            + " (uid " + r.callingUid + ")");
-                    return true;
+                            + " (uid " + r.callingUid + ")";
                 }
             }
         }
@@ -469,14 +471,13 @@
             int perm = checkComponentPermission(null,
                     filter.receiverList.pid, filter.receiverList.uid, -1, true);
             if (perm != PackageManager.PERMISSION_GRANTED) {
-                Slog.w(TAG, "Permission Denial: security check failed when receiving "
+                return "Permission Denial: security check failed when receiving "
                         + r.intent.toString()
                         + " to " + filter.receiverList.app
                         + " (pid=" + filter.receiverList.pid
                         + ", uid=" + filter.receiverList.uid + ")"
                         + " due to sender " + r.callerPackage
-                        + " (uid " + r.callingUid + ")");
-                return true;
+                        + " (uid " + r.callingUid + ")";
             }
         }
         // Check that the receiver does *not* have any excluded permissions
@@ -496,7 +497,7 @@
                                     filter.receiverList.uid,
                                     filter.packageName)
                                     == AppOpsManager.MODE_ALLOWED)) {
-                        Slog.w(TAG, "Appop Denial: receiving "
+                        return "Appop Denial: receiving "
                                 + r.intent.toString()
                                 + " to " + filter.receiverList.app
                                 + " (pid=" + filter.receiverList.pid
@@ -504,22 +505,20 @@
                                 + " excludes appop " + AppOpsManager.permissionToOp(
                                 excludedPermission)
                                 + " due to sender " + r.callerPackage
-                                + " (uid " + r.callingUid + ")");
-                        return true;
+                                + " (uid " + r.callingUid + ")";
                     }
                 } else {
                     // When there is no app op associated with the permission,
                     // skip when permission is granted.
                     if (perm == PackageManager.PERMISSION_GRANTED) {
-                        Slog.w(TAG, "Permission Denial: receiving "
+                        return "Permission Denial: receiving "
                                 + r.intent.toString()
                                 + " to " + filter.receiverList.app
                                 + " (pid=" + filter.receiverList.pid
                                 + ", uid=" + filter.receiverList.uid + ")"
                                 + " excludes " + excludedPermission
                                 + " due to sender " + r.callerPackage
-                                + " (uid " + r.callingUid + ")");
-                        return true;
+                                + " (uid " + r.callingUid + ")";
                     }
                 }
             }
@@ -528,15 +527,14 @@
         // Check that the receiver does *not* belong to any of the excluded packages
         if (r.excludedPackages != null && r.excludedPackages.length > 0) {
             if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) {
-                Slog.w(TAG, "Skipping delivery of excluded package "
+                return "Skipping delivery of excluded package "
                         + r.intent.toString()
                         + " to " + filter.receiverList.app
                         + " (pid=" + filter.receiverList.pid
                         + ", uid=" + filter.receiverList.uid + ")"
                         + " excludes package " + filter.packageName
                         + " due to sender " + r.callerPackage
-                        + " (uid " + r.callingUid + ")");
-                return true;
+                        + " (uid " + r.callingUid + ")";
             }
         }
 
@@ -546,15 +544,14 @@
                 filter.receiverList.uid, filter.packageName, filter.featureId,
                 "Broadcast delivered to registered receiver " + filter.receiverId)
                 != AppOpsManager.MODE_ALLOWED) {
-            Slog.w(TAG, "Appop Denial: receiving "
+            return "Appop Denial: receiving "
                     + r.intent.toString()
                     + " to " + filter.receiverList.app
                     + " (pid=" + filter.receiverList.pid
                     + ", uid=" + filter.receiverList.uid + ")"
                     + " requires appop " + AppOpsManager.opToName(r.appOp)
                     + " due to sender " + r.callerPackage
-                    + " (uid " + r.callingUid + ")");
-            return true;
+                    + " (uid " + r.callingUid + ")";
         }
 
         // Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -562,15 +559,14 @@
         if (!filter.exported && checkComponentPermission(null, r.callingPid,
                 r.callingUid, filter.receiverList.uid, filter.exported)
                 != PackageManager.PERMISSION_GRANTED) {
-            Slog.w(TAG, "Exported Denial: sending "
+            return "Exported Denial: sending "
                     + r.intent.toString()
                     + ", action: " + r.intent.getAction()
                     + " from " + r.callerPackage
                     + " (uid=" + r.callingUid + ")"
                     + " due to receiver " + filter.receiverList.app
                     + " (uid " + filter.receiverList.uid + ")"
-                    + " not specifying RECEIVER_EXPORTED");
-            return true;
+                    + " not specifying RECEIVER_EXPORTED";
         }
 
         // If permissions need a review before any of the app components can run, we drop
@@ -579,10 +575,10 @@
         // broadcast.
         if (!requestStartTargetPermissionsReviewIfNeededLocked(r, filter.packageName,
                 filter.owningUserId)) {
-            return true;
+            return "Skipping delivery to " + filter.packageName + " due to permissions review";
         }
 
-        return false;
+        return null;
     }
 
     private static String broadcastDescription(BroadcastRecord r, ComponentName component) {
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index cbf0aae..b98639e 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -37,6 +37,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.EventLog;
+import android.util.IntArray;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -89,6 +90,8 @@
             "compact_proc_state_throttle";
     @VisibleForTesting static final String KEY_FREEZER_DEBOUNCE_TIMEOUT =
             "freeze_debounce_timeout";
+    @VisibleForTesting static final String KEY_FREEZER_EXEMPT_INST_PKG =
+            "freeze_exempt_inst_pkg";
 
     // RSS Indices
     private static final int RSS_TOTAL_INDEX = 0;
@@ -137,6 +140,7 @@
     @VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
             String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
     @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L;
+    @VisibleForTesting static final Boolean DEFAULT_FREEZER_EXEMPT_INST_PKG = true;
 
     @VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
                 Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -277,6 +281,8 @@
                         for (String name : properties.getKeyset()) {
                             if (KEY_FREEZER_DEBOUNCE_TIMEOUT.equals(name)) {
                                 updateFreezerDebounceTimeout();
+                            } else if (KEY_FREEZER_EXEMPT_INST_PKG.equals(name)) {
+                                updateFreezerExemptInstPkg();
                             }
                         }
                     }
@@ -357,6 +363,7 @@
     private boolean mFreezerOverride = false;
 
     @VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
+    @VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
 
     // Maps process ID to last compaction statistics for processes that we've fully compacted. Used
     // when evaluating throttles that we only consider for "full" compaction, so we don't store
@@ -566,6 +573,15 @@
         }
     }
 
+    /**
+     * Returns whether freezer exempts INSTALL_PACKAGES.
+     */
+    public boolean freezerExemptInstPkg() {
+        synchronized (mPhenotypeFlagLock) {
+            return mUseFreezer && mFreezerExemptInstPkg;
+        }
+    }
+
     @GuardedBy("mProcLock")
     void dump(PrintWriter pw) {
         pw.println("CachedAppOptimizer settings");
@@ -647,6 +663,7 @@
             pw.println("  " + KEY_USE_FREEZER + "=" + mUseFreezer);
             pw.println("  " + KEY_FREEZER_STATSD_SAMPLE_RATE + "=" + mFreezerStatsdSampleRate);
             pw.println("  " + KEY_FREEZER_DEBOUNCE_TIMEOUT + "=" + mFreezerDebounceTimeout);
+            pw.println("  " + KEY_FREEZER_EXEMPT_INST_PKG + "=" + mFreezerExemptInstPkg);
             synchronized (mProcLock) {
                 int size = mFrozenProcesses.size();
                 pw.println("  Apps frozen: " + size);
@@ -829,7 +846,7 @@
     /**
      * Retrieves the free swap percentage.
      */
-    static private native double getFreeSwapPercent();
+    static native double getFreeSwapPercent();
 
     /**
      * Retrieves the total used physical ZRAM
@@ -1007,6 +1024,7 @@
                     KEY_USE_FREEZER, DEFAULT_USE_FREEZER)) {
             mUseFreezer = isFreezerSupported();
             updateFreezerDebounceTimeout();
+            updateFreezerExemptInstPkg();
         } else {
             mUseFreezer = false;
         }
@@ -1194,6 +1212,15 @@
         if (mFreezerDebounceTimeout < 0) {
             mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
         }
+        Slog.d(TAG_AM, "Freezer timeout set to " + mFreezerDebounceTimeout);
+    }
+
+    @GuardedBy("mPhenotypeFlagLock")
+    private void updateFreezerExemptInstPkg() {
+        mFreezerExemptInstPkg = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+                KEY_FREEZER_EXEMPT_INST_PKG, DEFAULT_FREEZER_EXEMPT_INST_PKG);
+        Slog.d(TAG_AM, "Freezer exemption set to " + mFreezerExemptInstPkg);
     }
 
     private boolean parseProcStateThrottle(String procStateThrottleString) {
@@ -2084,15 +2111,28 @@
 
         @GuardedBy({"mAm"})
         @Override
-        public void onBlockingFileLock(int pid) {
+        public void onBlockingFileLock(IntArray pids) {
             if (DEBUG_FREEZER) {
-                Slog.d(TAG_AM, "Process (pid=" + pid + ") holds blocking file lock");
+                Slog.d(TAG_AM, "Blocking file lock found: " + pids);
             }
             synchronized (mProcLock) {
+                int pid = pids.get(0);
                 ProcessRecord app = mFrozenProcesses.get(pid);
+                ProcessRecord pr;
                 if (app != null) {
-                    Slog.i(TAG_AM, app.processName + " (" + pid + ") holds blocking file lock");
-                    unfreezeAppLSP(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+                    for (int i = 1; i < pids.size(); i++) {
+                        int blocked = pids.get(i);
+                        synchronized (mAm.mPidsSelfLocked) {
+                            pr = mAm.mPidsSelfLocked.get(blocked);
+                        }
+                        if (pr != null && pr.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ) {
+                            Slog.d(TAG_AM, app.processName + " (" + pid + ") blocks "
+                                    + pr.processName + " (" + blocked + ")");
+                            // Found at least one blocked non-cached process
+                            unfreezeAppLSP(app, OomAdjuster.OOM_ADJ_REASON_NONE);
+                            break;
+                        }
+                    }
                 }
             }
         }
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index a97173d..16055b9 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -81,6 +81,7 @@
 import com.android.server.LocalServices;
 import com.android.server.RescueParty;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.UserManagerService;
 import com.android.server.pm.pkg.AndroidPackage;
 
 import java.io.FileDescriptor;
@@ -163,7 +164,7 @@
     private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
             String name, IBinder token, int callingUid, String callingPackage, String callingTag,
             boolean stable, int userId) {
-        ContentProviderRecord cpr;
+        ContentProviderRecord cpr = null;
         ContentProviderConnection conn = null;
         ProviderInfo cpi = null;
         boolean providerRunning = false;
@@ -185,8 +186,21 @@
 
             checkTime(startTime, "getContentProviderImpl: getProviderByName");
 
-            // First check if this content provider has been published...
-            cpr = mProviderMap.getProviderByName(name, userId);
+            UserManagerService userManagerService = UserManagerService.getInstance();
+
+            /*
+             For clone user profile and allowed authority, skipping finding provider and redirecting
+             it to owner profile. Ideally clone profile should not have MediaProvider instance
+             installed and mProviderMap would not have entry for clone user. This is just fallback
+             check to ensure even if MediaProvider is installed in Clone Profile, it should not be
+             used and redirect to owner user's MediaProvider.
+             */
+            //todo(b/236121588) MediaProvider should not be installed in clone profile.
+            if (!isAuthorityRedirectedForCloneProfile(name)
+                    || !userManagerService.isMediaSharedWithParent(userId)) {
+                // First check if this content provider has been published...
+                cpr = mProviderMap.getProviderByName(name, userId);
+            }
             // If that didn't work, check if it exists for user 0 and then
             // verify that it's a singleton provider before using it.
             if (cpr == null && userId != UserHandle.USER_SYSTEM) {
@@ -201,11 +215,9 @@
                         userId = UserHandle.USER_SYSTEM;
                         checkCrossUser = false;
                     } else if (isAuthorityRedirectedForCloneProfile(name)) {
-                        UserManagerInternal umInternal = LocalServices.getService(
-                                UserManagerInternal.class);
-                        UserInfo userInfo = umInternal.getUserInfo(userId);
-
-                        if (userInfo != null && userInfo.isCloneProfile()) {
+                        if (userManagerService.isMediaSharedWithParent(userId)) {
+                            UserManagerInternal umInternal = LocalServices.getService(
+                                    UserManagerInternal.class);
                             userId = umInternal.getProfileParentId(userId);
                             checkCrossUser = false;
                         }
diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags
index d080036..ea3c8dc 100644
--- a/services/core/java/com/android/server/am/EventLogTags.logtags
+++ b/services/core/java/com/android/server/am/EventLogTags.logtags
@@ -101,12 +101,13 @@
 30073 uc_finish_user_stopping (userId|1|5)
 30074 uc_finish_user_stopped (userId|1|5)
 30075 uc_switch_user (userId|1|5)
-30076 uc_start_user_internal (userId|1|5)
+30076 uc_start_user_internal (userId|1|5),(foreground|1),(displayId|1|5)
 30077 uc_unlock_user (userId|1|5)
 30078 uc_finish_user_boot (userId|1|5)
 30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5)
 30080 uc_continue_user_switch (oldUserId|1|5),(newUserId|1|5)
 30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3)
+
 # Tags below are used by SystemServiceManager - although it's technically part of am, these are
 # also user switch events and useful to be analyzed together with events above.
 30082 ssm_user_starting (userId|1|5)
@@ -117,10 +118,10 @@
 30087 ssm_user_stopped (userId|1|5)
 30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5)
 
+# Similarly, tags below are used by UserManagerService
+30091 um_user_visibility_changed (userId|1|5),(visible|1)
+
 # Foreground service start/stop events.
 30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
 30101 am_foreground_service_denied (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
 30102 am_foreground_service_stop (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
-
-
-
diff --git a/services/core/java/com/android/server/am/ForegroundServiceDelegation.java b/services/core/java/com/android/server/am/ForegroundServiceDelegation.java
new file mode 100644
index 0000000..a051d17
--- /dev/null
+++ b/services/core/java/com/android/server/am/ForegroundServiceDelegation.java
@@ -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.server.am;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.IBinder;
+
+/**
+ * A foreground service delegate which has client options and connection callback.
+ */
+public class ForegroundServiceDelegation {
+    public final IBinder mBinder = new Binder();
+    @NonNull
+    public final ForegroundServiceDelegationOptions mOptions;
+    @Nullable
+    public final ServiceConnection mConnection;
+
+    public ForegroundServiceDelegation(@NonNull ForegroundServiceDelegationOptions options,
+            @Nullable ServiceConnection connection) {
+        mOptions = options;
+        mConnection = connection;
+    }
+}
diff --git a/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java b/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java
new file mode 100644
index 0000000..5eb5a55
--- /dev/null
+++ b/services/core/java/com/android/server/am/ForegroundServiceDelegationOptions.java
@@ -0,0 +1,262 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.IApplicationThread;
+import android.content.ComponentName;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A service module such as MediaSessionService, VOIP, Camera, Microphone, Location can ask
+ * ActivityManagerService to start a foreground service delegate on behalf of the actual app,
+ * by which the client app's process state can be promoted to FOREGROUND_SERVICE process state which
+ * is higher than the app's actual process state if the app is in the background. This can help to
+ * keep the app in the memory and extra run-time.
+ * The app does not need to define an actual service component nor add it into manifest file.
+ */
+public class ForegroundServiceDelegationOptions {
+
+    public static final int DELEGATION_SERVICE_DEFAULT = 0;
+    public static final int DELEGATION_SERVICE_DATA_SYNC = 1;
+    public static final int DELEGATION_SERVICE_MEDIA_PLAYBACK = 2;
+    public static final int DELEGATION_SERVICE_PHONE_CALL = 3;
+    public static final int DELEGATION_SERVICE_LOCATION = 4;
+    public static final int DELEGATION_SERVICE_CONNECTED_DEVICE = 5;
+    public static final int DELEGATION_SERVICE_MEDIA_PROJECTION = 6;
+    public static final int DELEGATION_SERVICE_CAMERA = 7;
+    public static final int DELEGATION_SERVICE_MICROPHONE = 8;
+    public static final int DELEGATION_SERVICE_HEALTH = 9;
+    public static final int DELEGATION_SERVICE_REMOTE_MESSAGING = 10;
+    public static final int DELEGATION_SERVICE_SYSTEM_EXEMPTED = 11;
+    public static final int DELEGATION_SERVICE_SPECIAL_USE = 12;
+
+    @IntDef(flag = false, prefix = { "DELEGATION_SERVICE_" }, value = {
+            DELEGATION_SERVICE_DEFAULT,
+            DELEGATION_SERVICE_DATA_SYNC,
+            DELEGATION_SERVICE_MEDIA_PLAYBACK,
+            DELEGATION_SERVICE_PHONE_CALL,
+            DELEGATION_SERVICE_LOCATION,
+            DELEGATION_SERVICE_CONNECTED_DEVICE,
+            DELEGATION_SERVICE_MEDIA_PROJECTION,
+            DELEGATION_SERVICE_CAMERA,
+            DELEGATION_SERVICE_MICROPHONE,
+            DELEGATION_SERVICE_HEALTH,
+            DELEGATION_SERVICE_REMOTE_MESSAGING,
+            DELEGATION_SERVICE_SYSTEM_EXEMPTED,
+            DELEGATION_SERVICE_SPECIAL_USE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DelegationService {}
+
+    // The actual app's PID
+    public final int mClientPid;
+    // The actual app's UID
+    public final int mClientUid;
+    // The actual app's package name
+    @NonNull
+    public final String mClientPackageName;
+    // The actual app's app thread
+    @Nullable
+    public final IApplicationThread mClientAppThread;
+    public final boolean mSticky; // Is it a sticky service
+
+    // The delegation service's instance name which is to identify the delegate.
+    @NonNull
+    public String mClientInstanceName;
+    // The foreground service types it consists of.
+    public final int mForegroundServiceTypes;
+    /**
+     * The service's name such as MediaSessionService, VOIP, Camera, Microphone, Location. This is
+     * the internal module's name which actually starts the FGS delegate on behalf of the client
+     * app.
+     */
+    public final @DelegationService int mDelegationService;
+
+    public ForegroundServiceDelegationOptions(int clientPid,
+            int clientUid,
+            @NonNull String clientPackageName,
+            @NonNull IApplicationThread clientAppThread,
+            boolean isSticky,
+            @NonNull String clientInstanceName,
+            int foregroundServiceTypes,
+            @DelegationService int delegationService) {
+        mClientPid = clientPid;
+        mClientUid = clientUid;
+        mClientPackageName = clientPackageName;
+        mClientAppThread = clientAppThread;
+        mSticky = isSticky;
+        mClientInstanceName = clientInstanceName;
+        mForegroundServiceTypes = foregroundServiceTypes;
+        mDelegationService = delegationService;
+    }
+
+    /**
+     * A service delegates a foreground service state to a clientUID using a instanceName.
+     * This delegation is uniquely identified by
+     * mDelegationService/mClientUid/mClientPid/mClientInstanceName
+     */
+    public boolean isSameDelegate(ForegroundServiceDelegationOptions that) {
+        return this.mDelegationService == that.mDelegationService
+                && this.mClientUid == that.mClientUid
+                && this.mClientPid == that.mClientPid
+                && this.mClientInstanceName.equals(that.mClientInstanceName);
+    }
+
+    /**
+     * Construct a component name for this delegate.
+     */
+    public ComponentName getComponentName() {
+        return new ComponentName(mClientPackageName, serviceCodeToString(mDelegationService)
+                + ":" + mClientInstanceName);
+    }
+
+    /**
+     * Get string description of this delegate options.
+     */
+    public String getDescription() {
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("ForegroundServiceDelegate{")
+                .append("package:")
+                .append(mClientPackageName)
+                .append(",")
+                .append("service:")
+                .append(serviceCodeToString(mDelegationService))
+                .append(",")
+                .append("uid:")
+                .append(mClientUid)
+                .append(",")
+                .append("pid:")
+                .append(mClientPid)
+                .append(",")
+                .append("instance:")
+                .append(mClientInstanceName)
+                .append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Map the integer service code to string name.
+     * @param serviceCode
+     * @return
+     */
+    public static String serviceCodeToString(@DelegationService int serviceCode) {
+        switch (serviceCode) {
+            case DELEGATION_SERVICE_DEFAULT:
+                return "DEFAULT";
+            case DELEGATION_SERVICE_DATA_SYNC:
+                return "DATA_SYNC";
+            case DELEGATION_SERVICE_MEDIA_PLAYBACK:
+                return "MEDIA_PLAYBACK";
+            case DELEGATION_SERVICE_PHONE_CALL:
+                return "PHONE_CALL";
+            case DELEGATION_SERVICE_LOCATION:
+                return "LOCATION";
+            case DELEGATION_SERVICE_CONNECTED_DEVICE:
+                return "CONNECTED_DEVICE";
+            case DELEGATION_SERVICE_MEDIA_PROJECTION:
+                return "MEDIA_PROJECTION";
+            case DELEGATION_SERVICE_CAMERA:
+                return "CAMERA";
+            case DELEGATION_SERVICE_MICROPHONE:
+                return "MICROPHONE";
+            case DELEGATION_SERVICE_HEALTH:
+                return "HEALTH";
+            case DELEGATION_SERVICE_REMOTE_MESSAGING:
+                return "REMOTE_MESSAGING";
+            case DELEGATION_SERVICE_SYSTEM_EXEMPTED:
+                return "SYSTEM_EXEMPTED";
+            case DELEGATION_SERVICE_SPECIAL_USE:
+                return "SPECIAL_USE";
+            default:
+                return "(unknown:" + serviceCode + ")";
+        }
+    }
+
+    public static class Builder {
+        int mClientPid; // The actual app PID
+        int mClientUid; // The actual app UID
+        String mClientPackageName; // The actual app's package name
+        int mClientNotificationId; // The actual app's notification
+        IApplicationThread mClientAppThread; // The actual app's app thread
+        boolean mSticky; // Is it a sticky service
+        String mClientInstanceName; // The delegation service instance name
+        int mForegroundServiceTypes; // The foreground service types it consists of
+        @DelegationService int mDelegationService; // The internal service's name, i.e. VOIP
+
+        public Builder setClientPid(int clientPid) {
+            mClientPid = clientPid;
+            return this;
+        }
+
+        public Builder setClientUid(int clientUid) {
+            mClientUid = clientUid;
+            return this;
+        }
+
+        public Builder setClientPackageName(@NonNull String clientPackageName) {
+            mClientPackageName = clientPackageName;
+            return this;
+        }
+
+        public Builder setClientNotificationId(int clientNotificationId) {
+            mClientNotificationId = clientNotificationId;
+            return this;
+        }
+
+        public Builder setClientAppThread(@NonNull IApplicationThread clientAppThread) {
+            mClientAppThread = clientAppThread;
+            return this;
+        }
+
+        public Builder setClientInstanceName(@NonNull String clientInstanceName) {
+            mClientInstanceName = clientInstanceName;
+            return this;
+        }
+
+        public Builder setSticky(boolean isSticky) {
+            mSticky = isSticky;
+            return this;
+        }
+
+        public Builder setForegroundServiceTypes(int foregroundServiceTypes) {
+            mForegroundServiceTypes = foregroundServiceTypes;
+            return this;
+        }
+
+        public Builder setDelegationService(@DelegationService int delegationService) {
+            mDelegationService = delegationService;
+            return this;
+        }
+
+        public ForegroundServiceDelegationOptions build() {
+            return new ForegroundServiceDelegationOptions(mClientPid,
+                mClientUid,
+                mClientPackageName,
+                mClientAppThread,
+                mSticky,
+                mClientInstanceName,
+                mForegroundServiceTypes,
+                mDelegationService
+            );
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 68e5a5d..eb2b7d4 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1118,6 +1118,12 @@
 
     private long mNextNoKillDebugMessageTime;
 
+    private double mLastFreeSwapPercent = 1.00;
+
+    private static double getFreeSwapPercent() {
+        return CachedAppOptimizer.getFreeSwapPercent();
+    }
+
     @GuardedBy({"mService", "mProcLock"})
     private boolean updateAndTrimProcessLSP(final long now, final long nowElapsed,
             final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason) {
@@ -1142,6 +1148,11 @@
         int numEmpty = 0;
         int numTrimming = 0;
 
+        boolean proactiveKillsEnabled = mConstants.PROACTIVE_KILLS_ENABLED;
+        double lowSwapThresholdPercent = mConstants.LOW_SWAP_THRESHOLD_PERCENT;
+        double freeSwapPercent =  proactiveKillsEnabled ? getFreeSwapPercent() : 1.00;
+        ProcessRecord lruCachedApp = null;
+
         for (int i = numLru - 1; i >= 0; i--) {
             ProcessRecord app = lruList.get(i);
             final ProcessStateRecord state = app.mState;
@@ -1179,6 +1190,8 @@
                                     ApplicationExitInfo.REASON_OTHER,
                                     ApplicationExitInfo.SUBREASON_TOO_MANY_CACHED,
                                     true);
+                        } else if (proactiveKillsEnabled) {
+                            lruCachedApp = app;
                         }
                         break;
                     case PROCESS_STATE_CACHED_EMPTY:
@@ -1198,6 +1211,8 @@
                                         ApplicationExitInfo.REASON_OTHER,
                                         ApplicationExitInfo.SUBREASON_TOO_MANY_EMPTY,
                                         true);
+                            } else if (proactiveKillsEnabled) {
+                                lruCachedApp = app;
                             }
                         }
                         break;
@@ -1229,6 +1244,20 @@
             }
         }
 
+        if (proactiveKillsEnabled                               // Proactive kills enabled?
+                && doKillExcessiveProcesses                     // Should kill excessive processes?
+                && freeSwapPercent < lowSwapThresholdPercent    // Swap below threshold?
+                && lruCachedApp != null                         // If no cached app, let LMKD decide
+                // If swap is non-decreasing, give reclaim a chance to catch up
+                && freeSwapPercent < mLastFreeSwapPercent) {
+            lruCachedApp.killLocked("swap low and too many cached",
+                    ApplicationExitInfo.REASON_OTHER,
+                    ApplicationExitInfo.SUBREASON_TOO_MANY_CACHED,
+                    true);
+        }
+
+        mLastFreeSwapPercent = freeSwapPercent;
+
         return mService.mAppProfiler.updateLowMemStateLSP(numCached, numEmpty, numTrimming);
     }
 
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index bda60ff..fed0b11 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -18,7 +18,6 @@
 
 import static android.app.ActivityManager.START_SUCCESS;
 
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
@@ -26,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.BroadcastOptions;
+import android.app.IApplicationThread;
 import android.app.PendingIntent;
 import android.content.IIntentReceiver;
 import android.content.IIntentSender;
@@ -35,7 +35,6 @@
 import android.os.IBinder;
 import android.os.PowerWhitelistManager;
 import android.os.PowerWhitelistManager.ReasonCode;
-import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.TransactionTooLargeException;
@@ -302,13 +301,21 @@
 
     public void send(int code, Intent intent, String resolvedType, IBinder allowlistToken,
             IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
-        sendInner(code, intent, resolvedType, allowlistToken, finishedReceiver,
+        sendInner(null, code, intent, resolvedType, allowlistToken, finishedReceiver,
                 requiredPermission, null, null, 0, 0, 0, options);
     }
 
-    public int sendWithResult(int code, Intent intent, String resolvedType, IBinder allowlistToken,
-            IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
-        return sendInner(code, intent, resolvedType, allowlistToken, finishedReceiver,
+    public void send(IApplicationThread caller, int code, Intent intent, String resolvedType,
+            IBinder allowlistToken, IIntentReceiver finishedReceiver, String requiredPermission,
+            Bundle options) {
+        sendInner(caller, code, intent, resolvedType, allowlistToken, finishedReceiver,
+                requiredPermission, null, null, 0, 0, 0, options);
+    }
+
+    public int sendWithResult(IApplicationThread caller, int code, Intent intent,
+            String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver,
+            String requiredPermission, Bundle options) {
+        return sendInner(caller, code, intent, resolvedType, allowlistToken, finishedReceiver,
                 requiredPermission, null, null, 0, 0, 0, options);
     }
 
@@ -339,9 +346,19 @@
                 ActivityOptions.PENDING_INTENT_BAL_ALLOWED_DEFAULT);
     }
 
+    @Deprecated
     public int sendInner(int code, Intent intent, String resolvedType, IBinder allowlistToken,
             IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo,
             String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) {
+        return sendInner(null, code, intent, resolvedType, allowlistToken, finishedReceiver,
+                requiredPermission, resultTo, resultWho, requestCode, flagsMask, flagsValues,
+                options);
+    }
+
+    public int sendInner(IApplicationThread caller, int code, Intent intent,
+            String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver,
+            String requiredPermission, IBinder resultTo, String resultWho, int requestCode,
+            int flagsMask, int flagsValues, Bundle options) {
         if (intent != null) intent.setDefusable(true);
         if (options != null) options.setDefusable(true);
 
@@ -379,11 +396,16 @@
                 resolvedType = key.requestResolvedType;
             }
 
-            // Apply any launch flags from the ActivityOptions. This is to ensure that the caller
-            // can specify a consistent launch mode even if the PendingIntent is immutable
+            // Apply any launch flags from the ActivityOptions. This is used only by SystemUI
+            // to ensure that we can launch the pending intent with a consistent launch mode even
+            // if the provided PendingIntent is immutable (ie. to force an activity to launch into
+            // a new task, or to launch multiple instances if supported by the app)
             final ActivityOptions opts = ActivityOptions.fromBundle(options);
             if (opts != null) {
-                finalIntent.addFlags(opts.getPendingIntentLaunchFlags());
+                // TODO(b/254490217): Move this check into SafeActivityOptions
+                if (controller.mAtmInternal.isCallerRecents(Binder.getCallingUid())) {
+                    finalIntent.addFlags(opts.getPendingIntentLaunchFlags());
+                }
             }
 
             // Extract options before clearing calling identity
@@ -422,16 +444,8 @@
         // Only system senders can declare a broadcast to be alarm-originated.  We check
         // this here rather than in the general case handling below to fail before the other
         // invocation side effects such as allowlisting.
-        if (options != null && callingUid != Process.SYSTEM_UID
-                && key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
-            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
-                if (DEBUG_BROADCAST_LIGHT) {
-                    Slog.w(TAG, "Non-system caller " + callingUid
-                            + " may not flag broadcast as alarm-related");
-                }
-                throw new SecurityException(
-                        "Non-system callers may not flag broadcasts as alarm-related");
-            }
+        if (key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
+            controller.mAmInternal.enforceBroadcastOptionsPermissions(options, callingUid);
         }
 
         final long origId = Binder.clearCallingIdentity();
@@ -468,7 +482,13 @@
                 }
             }
 
+            final IApplicationThread finishedReceiverThread = caller;
             boolean sendFinish = finishedReceiver != null;
+            if ((finishedReceiver != null) && (finishedReceiverThread == null)) {
+                Slog.w(TAG, "Sending of " + intent + " from " + Binder.getCallingUid()
+                        + " requested resultTo without an IApplicationThread!", new Throwable());
+            }
+
             int userId = key.userId;
             if (userId == UserHandle.USER_CURRENT) {
                 userId = controller.mUserController.getCurrentOrTargetUserId();
@@ -525,9 +545,9 @@
                         // that the broadcast be delivered synchronously
                         int sent = controller.mAmInternal.broadcastIntentInPackage(key.packageName,
                                 key.featureId, uid, callingUid, callingPid, finalIntent,
-                                resolvedType, finishedReceiver, code, null, null,
-                                requiredPermission, options, (finishedReceiver != null), false,
-                                userId, allowedByToken || allowTrampoline, bgStartsToken,
+                                resolvedType, finishedReceiverThread, finishedReceiver, code, null,
+                                null, requiredPermission, options, (finishedReceiver != null),
+                                false, userId, allowedByToken || allowTrampoline, bgStartsToken,
                                 null /* broadcastAllowList */);
                         if (sent == ActivityManager.BROADCAST_SUCCESS) {
                             sendFinish = false;
diff --git a/services/core/java/com/android/server/am/PreBootBroadcaster.java b/services/core/java/com/android/server/am/PreBootBroadcaster.java
index 9b7c3ac..77fcef6 100644
--- a/services/core/java/com/android/server/am/PreBootBroadcaster.java
+++ b/services/core/java/com/android/server/am/PreBootBroadcaster.java
@@ -57,7 +57,6 @@
     private static final String TAG = "PreBootBroadcaster";
 
     private final ActivityManagerService mService;
-    private final ProcessRecord mSystemApp;
     private final int mUserId;
     private final ProgressReporter mProgress;
     private final boolean mQuiet;
@@ -70,9 +69,6 @@
     public PreBootBroadcaster(ActivityManagerService service, int userId,
             ProgressReporter progress, boolean quiet) {
         mService = service;
-        synchronized (mService) {
-            mSystemApp = mService.getProcessRecordLocked("system", android.os.Process.SYSTEM_UID);
-        }
         mUserId = userId;
         mProgress = progress;
         mQuiet = quiet;
@@ -127,7 +123,7 @@
                 TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                 REASON_PRE_BOOT_COMPLETED, "");
         synchronized (mService) {
-            mService.broadcastIntentLocked(mSystemApp, "android", null, mIntent, null, this, 0,
+            mService.broadcastIntentLocked(null, null, null, mIntent, null, this, 0,
                     null, null, null, null, null, AppOpsManager.OP_NONE, bOptions.toBundle(), true,
                     false, ActivityManagerService.MY_PID,
                     Process.SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), mUserId);
diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
index 71d39964..68d906b 100644
--- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java
@@ -44,7 +44,7 @@
 import android.provider.Settings;
 import android.util.EventLog;
 import android.util.Slog;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.CompositeRWLock;
 import com.android.internal.annotations.GuardedBy;
@@ -62,6 +62,12 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicLong;
+
+
 /**
  * The error state of the process, such as if it's crashing/ANR etc.
  */
@@ -257,12 +263,13 @@
     void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
             String parentShortComponentName, WindowProcessController parentProcess,
             boolean aboveSystem, TimeoutRecord timeoutRecord,
-            boolean onlyDumpSelf) {
+            ExecutorService auxiliaryTaskExecutor, boolean onlyDumpSelf) {
         String annotation = timeoutRecord.mReason;
         AnrLatencyTracker latencyTracker = timeoutRecord.mLatencyTracker;
+        Future<?> updateCpuStatsNowFirstCall = null;
 
         ArrayList<Integer> firstPids = new ArrayList<>(5);
-        SparseArray<Boolean> lastPids = new SparseArray<>(20);
+        SparseBooleanArray lastPids = new SparseBooleanArray(20);
 
         mApp.getWindowProcessController().appEarlyNotResponding(annotation, () -> {
             latencyTracker.waitingOnAMSLockStarted();
@@ -275,10 +282,15 @@
         });
 
         long anrTime = SystemClock.uptimeMillis();
+
         if (isMonitorCpuUsage()) {
-            latencyTracker.updateCpuStatsNowCalled();
-            mService.updateCpuStatsNow();
-            latencyTracker.updateCpuStatsNowReturned();
+            updateCpuStatsNowFirstCall = auxiliaryTaskExecutor.submit(
+                    () -> {
+                    latencyTracker.updateCpuStatsNowCalled();
+                    mService.updateCpuStatsNow();
+                    latencyTracker.updateCpuStatsNowReturned();
+                });
+
         }
 
         final boolean isSilentAnr;
@@ -367,7 +379,7 @@
                                 firstPids.add(myPid);
                                 if (DEBUG_ANR) Slog.i(TAG, "Adding likely IME: " + r);
                             } else {
-                                lastPids.put(myPid, Boolean.TRUE);
+                                lastPids.put(myPid, true);
                                 if (DEBUG_ANR) Slog.i(TAG, "Adding ANR proc: " + r);
                             }
                         }
@@ -430,41 +442,59 @@
         report.append(currentPsiState);
         ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);
 
-        latencyTracker.nativePidCollectionStarted();
-        // don't dump native PIDs for background ANRs unless it is the process of interest
-        String[] nativeProcs = null;
-        if (isSilentAnr || onlyDumpSelf) {
-            for (int i = 0; i < NATIVE_STACKS_OF_INTEREST.length; i++) {
-                if (NATIVE_STACKS_OF_INTEREST[i].equals(mApp.processName)) {
-                    nativeProcs = new String[] { mApp.processName };
-                    break;
-                }
-            }
-        } else {
-            nativeProcs = NATIVE_STACKS_OF_INTEREST;
-        }
+        // We push the native pids collection task to the helper thread through
+        // the Anr auxiliary task executor, and wait on it later after dumping the first pids
+        Future<ArrayList<Integer>> nativePidsFuture =
+                auxiliaryTaskExecutor.submit(
+                    () -> {
+                        latencyTracker.nativePidCollectionStarted();
+                        // don't dump native PIDs for background ANRs unless
+                        // it is the process of interest
+                        String[] nativeProcs = null;
+                        if (isSilentAnr || onlyDumpSelf) {
+                            for (int i = 0; i < NATIVE_STACKS_OF_INTEREST.length; i++) {
+                                if (NATIVE_STACKS_OF_INTEREST[i].equals(mApp.processName)) {
+                                    nativeProcs = new String[] { mApp.processName };
+                                    break;
+                                }
+                            }
+                        } else {
+                            nativeProcs = NATIVE_STACKS_OF_INTEREST;
+                        }
 
-        int[] pids = nativeProcs == null ? null : Process.getPidsForCommands(nativeProcs);
-        ArrayList<Integer> nativePids = null;
+                        int[] pids = nativeProcs == null
+                                ? null : Process.getPidsForCommands(nativeProcs);
+                        ArrayList<Integer> nativePids = null;
 
-        if (pids != null) {
-            nativePids = new ArrayList<>(pids.length);
-            for (int i : pids) {
-                nativePids.add(i);
-            }
-        }
-        latencyTracker.nativePidCollectionEnded();
+                        if (pids != null) {
+                            nativePids = new ArrayList<>(pids.length);
+                            for (int i : pids) {
+                                nativePids.add(i);
+                            }
+                        }
+                        latencyTracker.nativePidCollectionEnded();
+                        return nativePids;
+                    });
+
         // For background ANRs, don't pass the ProcessCpuTracker to
         // avoid spending 1/2 second collecting stats to rank lastPids.
         StringWriter tracesFileException = new StringWriter();
         // To hold the start and end offset to the ANR trace file respectively.
-        final long[] offsets = new long[2];
+        final AtomicLong firstPidEndOffset = new AtomicLong(-1);
         File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,
                 isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids,
-                nativePids, tracesFileException, offsets, annotation, criticalEventLog,
-                latencyTracker);
+                nativePidsFuture, tracesFileException, firstPidEndOffset, annotation,
+                criticalEventLog, auxiliaryTaskExecutor, latencyTracker);
 
         if (isMonitorCpuUsage()) {
+            // Wait for the first call to finish
+            try {
+                updateCpuStatsNowFirstCall.get();
+            } catch (ExecutionException e) {
+                Slog.w(TAG, "Failed to update the CPU stats", e.getCause());
+            } catch (InterruptedException e) {
+                Slog.w(TAG, "Interrupted while updating the CPU stats", e);
+            }
             mService.updateCpuStatsNow();
             mService.mAppProfiler.printCurrentCpuState(report, anrTime);
             info.append(processCpuTracker.printCurrentLoad());
@@ -478,10 +508,14 @@
         if (tracesFile == null) {
             // There is no trace file, so dump (only) the alleged culprit's threads to the log
             Process.sendSignal(pid, Process.SIGNAL_QUIT);
-        } else if (offsets[1] > 0) {
+        } else if (firstPidEndOffset.get() > 0) {
             // We've dumped into the trace file successfully
+            // We pass the start and end offsets of the first section of
+            // the ANR file (the headers and first process dump)
+            final long startOffset = 0L;
+            final long endOffset = firstPidEndOffset.get();
             mService.mProcessList.mAppExitInfoTracker.scheduleLogAnrTrace(
-                    pid, mApp.uid, mApp.getPackageList(), tracesFile, offsets[0], offsets[1]);
+                    pid, mApp.uid, mApp.getPackageList(), tracesFile, startOffset, endOffset);
         }
 
         // Check if package is still being loaded
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 42bfc4c..ecea96e 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -1693,7 +1693,8 @@
                             app.info.packageName);
                     externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,
                             app.info.packageName);
-                    if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
+                    if (mService.isAppFreezerExemptInstPkg()
+                            && pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
                             app.info.packageName, userId)
                             == PackageManager.PERMISSION_GRANTED) {
                         Slog.i(TAG, app.info.packageName + " is exempt from freezer");
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 3b04dbb..0a8c640 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -54,6 +54,7 @@
 import com.android.internal.app.procstats.ProcessState;
 import com.android.internal.app.procstats.ProcessStats;
 import com.android.internal.os.Zygote;
+import com.android.server.FgThread;
 import com.android.server.wm.WindowProcessController;
 import com.android.server.wm.WindowProcessListener;
 
@@ -143,6 +144,13 @@
     private IApplicationThread mThread;
 
     /**
+     * Instance of {@link #mThread} that will always meet the {@code oneway}
+     * contract, possibly by using {@link SameProcessApplicationThread}.
+     */
+    @CompositeRWLock({"mService", "mProcLock"})
+    private IApplicationThread mOnewayThread;
+
+    /**
      * Always keep this application running?
      */
     private volatile boolean mPersistent;
@@ -603,16 +611,27 @@
         return mThread;
     }
 
+    @GuardedBy(anyOf = {"mService", "mProcLock"})
+    IApplicationThread getOnewayThread() {
+        return mOnewayThread;
+    }
+
     @GuardedBy({"mService", "mProcLock"})
     public void makeActive(IApplicationThread thread, ProcessStatsService tracker) {
         mProfile.onProcessActive(thread, tracker);
         mThread = thread;
+        if (mPid == Process.myPid()) {
+            mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler());
+        } else {
+            mOnewayThread = thread;
+        }
         mWindowProcessController.setThread(thread);
     }
 
     @GuardedBy({"mService", "mProcLock"})
     public void makeInactive(ProcessStatsService tracker) {
         mThread = null;
+        mOnewayThread = null;
         mWindowProcessController.setThread(null);
         mProfile.onProcessInactive(tracker);
     }
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index d2ef479..2ad2077 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -30,6 +30,7 @@
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.util.Slog;
 import android.util.TimeUtils;
 
@@ -43,6 +44,9 @@
  * The state info of the process, including proc state, oom adj score, et al.
  */
 final class ProcessStateRecord {
+    // Enable this to trace all OomAdjuster state transitions
+    private static final boolean TRACE_OOM_ADJ = false;
+
     private final ProcessRecord mApp;
     private final ActivityManagerService mService;
     private final ActivityManagerGlobalLock mProcLock;
@@ -916,6 +920,12 @@
 
     @GuardedBy("mService")
     void setAdjType(String adjType) {
+        if (TRACE_OOM_ADJ) {
+            Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                    "oom:" + mApp.processName + "/u" + mApp.uid, 0);
+            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                    "oom:" + mApp.processName + "/u" + mApp.uid, adjType, 0);
+        }
         mAdjType = adjType;
     }
 
@@ -1153,6 +1163,10 @@
 
     @GuardedBy({"mService", "mProcLock"})
     void onCleanupApplicationRecordLSP() {
+        if (TRACE_OOM_ADJ) {
+            Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                    "oom:" + mApp.processName + "/u" + mApp.uid, 0);
+        }
         setHasForegroundActivities(false);
         mHasShownUi = false;
         mForcingToImportant = null;
diff --git a/services/core/java/com/android/server/am/ProcessStatsService.java b/services/core/java/com/android/server/am/ProcessStatsService.java
index 33e4070..438a2d43 100644
--- a/services/core/java/com/android/server/am/ProcessStatsService.java
+++ b/services/core/java/com/android/server/am/ProcessStatsService.java
@@ -567,6 +567,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
     @Override
     public byte[] getCurrentStats(List<ParcelFileDescriptor> historic) {
+        super.getCurrentStats_enforcePermission();
+
         Parcel current = Parcel.obtain();
         synchronized (mLock) {
             long now = SystemClock.uptimeMillis();
@@ -623,6 +625,8 @@
     public long getCommittedStatsMerged(long highWaterMarkMs, int section, boolean doAggregate,
             List<ParcelFileDescriptor> committedStats, ProcessStats mergedStats) {
 
+        super.getCommittedStatsMerged_enforcePermission();
+
         long newHighWaterMark = highWaterMarkMs;
         mFileLock.lock();
         try {
@@ -709,6 +713,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
     @Override
     public ParcelFileDescriptor getStatsOverTime(long minTime) {
+        super.getStatsOverTime_enforcePermission();
+
         Parcel current = Parcel.obtain();
         long curTime;
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
new file mode 100644
index 0000000..a3c0111
--- /dev/null
+++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
@@ -0,0 +1,73 @@
+/*
+ * 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.app.IApplicationThread;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.CompatibilityInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+
+import java.util.Objects;
+
+/**
+ * Wrapper around an {@link IApplicationThread} that delegates selected calls
+ * through a {@link Handler} so they meet the {@code oneway} contract of
+ * returning immediately after dispatch.
+ */
+public class SameProcessApplicationThread extends IApplicationThread.Default {
+    private final IApplicationThread mWrapped;
+    private final Handler mHandler;
+
+    public SameProcessApplicationThread(@NonNull IApplicationThread wrapped,
+            @NonNull Handler handler) {
+        mWrapped = Objects.requireNonNull(wrapped);
+        mHandler = Objects.requireNonNull(handler);
+    }
+
+    @Override
+    public void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo,
+            int resultCode, String data, Bundle extras, boolean sync, int sendingUser,
+            int processState) {
+        mHandler.post(() -> {
+            try {
+                mWrapped.scheduleReceiver(intent, info, compatInfo, resultCode, data, extras, sync,
+                        sendingUser, processState);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    @Override
+    public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode,
+            String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser,
+            int processState) {
+        mHandler.post(() -> {
+            try {
+                mWrapped.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras,
+                        ordered, sticky, sendingUser, processState);
+            } catch (RemoteException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 4b82ad8..c27ed7a 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -206,6 +206,10 @@
     // Last time mAllowWhileInUsePermissionInFgs or mAllowStartForeground is set.
     long mLastSetFgsRestrictionTime;
 
+    // This is a service record of a FGS delegate (not a service record of a real service)
+    boolean mIsFgsDelegate;
+    @Nullable ForegroundServiceDelegation mFgsDelegation;
+
     String stringName;      // caching of toString
 
     private int lastStartId;    // identifier of most recent start request.
@@ -233,6 +237,7 @@
         final boolean taskRemoved;
         final int id;
         final int callingId;
+        final String mCallingProcessName;
         final Intent intent;
         final NeededUriGrants neededGrants;
         long deliveredTime;
@@ -242,14 +247,16 @@
 
         String stringName;      // caching of toString
 
-        StartItem(ServiceRecord _sr, boolean _taskRemoved, int _id, Intent _intent,
-                NeededUriGrants _neededGrants, int _callingId) {
+        StartItem(ServiceRecord _sr, boolean _taskRemoved, int _id,
+                Intent _intent, NeededUriGrants _neededGrants, int _callingId,
+                String callingProcessName) {
             sr = _sr;
             taskRemoved = _taskRemoved;
             id = _id;
             intent = _intent;
             neededGrants = _neededGrants;
             callingId = _callingId;
+            mCallingProcessName = callingProcessName;
         }
 
         UriPermissionOwner getUriPermissionsLocked() {
@@ -502,6 +509,9 @@
                     pw.print(" foregroundId="); pw.print(foregroundId);
                     pw.print(" foregroundNoti="); pw.println(foregroundNoti);
         }
+        if (mIsFgsDelegate) {
+            pw.print(prefix); pw.print("isFgsDelegate="); pw.println(mIsFgsDelegate);
+        }
         pw.print(prefix); pw.print("createTime=");
                 TimeUtils.formatDuration(createRealTime, nowReal, pw);
                 pw.print(" startingBgTimeout=");
@@ -634,7 +644,9 @@
                     serviceInfo.applicationInfo.uid,
                     serviceInfo.applicationInfo.longVersionCode,
                     serviceInfo.processName, serviceInfo.name);
-            tracker.applyNewOwner(this);
+            if (tracker != null) {
+                tracker.applyNewOwner(this);
+            }
         }
         return tracker;
     }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 216a48e..4d86140 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import static android.Manifest.permission.CREATE_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
@@ -101,6 +100,7 @@
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
@@ -120,6 +120,7 @@
 import com.android.server.SystemServiceManager;
 import com.android.server.am.UserState.KeyEvictedCallback;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
 import com.android.server.pm.UserManagerService;
 import com.android.server.utils.Slogf;
 import com.android.server.utils.TimingsTraceAndSlog;
@@ -133,6 +134,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -175,6 +177,7 @@
     static final int START_USER_SWITCH_FG_MSG = 120;
     static final int COMPLETE_USER_SWITCH_MSG = 130;
     static final int USER_COMPLETED_EVENT_MSG = 140;
+    static final int USER_VISIBILITY_CHANGED_MSG = 150;
 
     // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if
     // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not
@@ -196,6 +199,14 @@
     private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000;
 
     /**
+     * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
+     * called after dismissing the keyguard.
+     * Otherwise, we should move on to unfreeze the screen {@link #unfreezeScreen}
+     * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
+     */
+    private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
+
+    /**
      * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be
      * scheduled (although it may fire sooner instead).
      * When it fires, {@link #reportOnUserCompletedEvent} will be processed.
@@ -317,8 +328,12 @@
     @GuardedBy("mLock")
     private int[] mStartedUserArray = new int[] { 0 };
 
-    // If there are multiple profiles for the current user, their ids are here
-    // Currently only the primary user can have managed profiles
+    /**
+     * Contains the current user and its profiles (if any).
+     *
+     * <p><b>NOTE: </b>it lists all profiles, regardless of their running state (i.e., they're in
+     * this list even if not running).
+     */
     @GuardedBy("mLock")
     private int[] mCurrentProfileIds = new int[] {};
 
@@ -422,6 +437,32 @@
     /** @see #getLastUserUnlockingUptime */
     private volatile long mLastUserUnlockingUptime = 0;
 
+    // TODO(b/244333150) remove this array and let UserVisibilityMediator call the listeners
+    // directly, as that class should be responsible for all user visibility logic (for example,
+    // when the foreground user is switched out, its profiles also become invisible)
+    /**
+     * List of visible users (as defined by {@link UserManager#isUserVisible()}).
+     *
+     * <p>It's only used to call {@link UserManagerInternal} when the visibility is changed upon
+     * the user starting or stopping.
+     *
+     * <p>Note: only the key is used, not the value.
+     */
+    @GuardedBy("mLock")
+    private final SparseBooleanArray mVisibleUsers = new SparseBooleanArray();
+
+    private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() {
+        @Override
+        public void onUserCreated(UserInfo user, Object token) {
+            onUserAdded(user);
+        }
+
+        @Override
+        public void onUserRemoved(UserInfo user) {
+            UserController.this.onUserRemoved(user.id);
+        }
+    };
+
     UserController(ActivityManagerService service) {
         this(new Injector(service));
     }
@@ -1049,13 +1090,26 @@
             // TODO(b/239982558): for now we're just updating the user's visibility, but most likely
             // we'll need to remove this call and handle that as part of the user state workflow
             // instead.
-            userManagerInternal.unassignUserFromDisplay(userId);
+            userManagerInternal.unassignUserFromDisplayOnStop(userId);
+
+            final boolean visibilityChanged;
+            boolean visibleBefore;
+            synchronized (mLock) {
+                visibleBefore = mVisibleUsers.get(userId);
+                if (visibleBefore) {
+                    deleteVisibleUserLocked(userId);
+                    visibilityChanged = true;
+                } else {
+                    visibilityChanged = false;
+                }
+            }
 
             updateStartedUserArrayLU();
 
             final boolean allowDelayedLockingCopied = allowDelayedLocking;
             Runnable finishUserStoppingAsync = () ->
-                    mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied));
+                    mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied,
+                            visibilityChanged));
 
             if (mInjector.getUserManager().isPreCreated(userId)) {
                 finishUserStoppingAsync.run();
@@ -1092,8 +1146,22 @@
         }
     }
 
+    private void addVisibleUserLocked(@UserIdInt int userId) {
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "adding %d to mVisibleUsers", userId);
+        }
+        mVisibleUsers.put(userId, true);
+    }
+
+    private void deleteVisibleUserLocked(@UserIdInt int userId) {
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "deleting %d from mVisibleUsers", userId);
+        }
+        mVisibleUsers.delete(userId);
+    }
+
     private void finishUserStopping(final int userId, final UserState uss,
-            final boolean allowDelayedLocking) {
+            final boolean allowDelayedLocking, final boolean visibilityChanged) {
         EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId);
         synchronized (mLock) {
             if (uss.state != UserState.STATE_STOPPING) {
@@ -1111,6 +1179,9 @@
                 BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH,
                 Integer.toString(userId), userId);
         mInjector.getSystemServiceManager().onUserStopping(userId);
+        if (visibilityChanged) {
+            mInjector.onUserVisibilityChanged(userId, /* visible= */ false);
+        }
 
         Runnable finishUserStoppedAsync = () ->
                 mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking));
@@ -1482,7 +1553,7 @@
     // defined
     boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId) {
         checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
-                MANAGE_USERS, CREATE_USERS);
+                MANAGE_USERS, INTERACT_ACROSS_USERS);
 
         // DEFAULT_DISPLAY is used for the current foreground user only
         Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
@@ -1514,16 +1585,17 @@
     private boolean startUserInternal(@UserIdInt int userId, int displayId, boolean foreground,
             @Nullable IProgressListener unlockListener, @NonNull TimingsTraceAndSlog t) {
         if (DEBUG_MU) {
-            Slogf.i(TAG, "Starting user %d on display %d %s", userId, displayId,
+            Slogf.i(TAG, "Starting user %d on display %d%s", userId, displayId,
                     foreground ? " in foreground" : "");
         }
 
-        if (displayId != Display.DEFAULT_DISPLAY) {
+        boolean onSecondaryDisplay = displayId != Display.DEFAULT_DISPLAY;
+        if (onSecondaryDisplay) {
             Preconditions.checkArgument(!foreground, "Cannot start user %d in foreground AND "
                     + "on secondary display (%d)", userId, displayId);
         }
-        // TODO(b/239982558): log display id (or use a new event)
-        EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId);
+        EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId, foreground ? 1 : 0,
+                displayId);
 
         final int callingUid = Binder.getCallingUid();
         final int callingPid = Binder.getCallingPid();
@@ -1572,12 +1644,35 @@
                 return false;
             }
 
-            if (foreground && userInfo.preCreated) {
-                Slogf.w(TAG, "Cannot start pre-created user #" + userId + " as foreground");
+            if ((foreground || onSecondaryDisplay) && userInfo.preCreated) {
+                Slogf.w(TAG, "Cannot start pre-created user #" + userId + " in foreground or on "
+                        + "secondary display");
                 return false;
             }
 
-            mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId);
+            t.traceBegin("assignUserToDisplayOnStart");
+            int result = mInjector.getUserManagerInternal().assignUserToDisplayOnStart(userId,
+                    userInfo.profileGroupId, foreground, displayId);
+            t.traceEnd();
+
+            boolean visible;
+            switch (result) {
+                case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE:
+                    visible = true;
+                    break;
+                case UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE:
+                    visible = false;
+                    break;
+                default:
+                    Slogf.wtf(TAG, "Wrong result from assignUserToDisplayOnStart(): %d", result);
+                    // Fall through
+                case UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE:
+                    Slogf.e(TAG, "%s user(%d) / display (%d) assignment failed: %s",
+                            (foreground ? "fg" : "bg"), userId, displayId,
+                            UserManagerInternal.userAssignmentResultToString(result));
+                    return false;
+            }
+
 
             // TODO(b/239982558): might need something similar for bg users on secondary display
             if (foreground && isUserSwitchUiEnabled()) {
@@ -1629,13 +1724,24 @@
                 // Make sure the old user is no longer considering the display to be on.
                 mInjector.reportGlobalUsageEvent(UsageEvents.Event.SCREEN_NON_INTERACTIVE);
                 boolean userSwitchUiEnabled;
+                // TODO(b/244333150): temporary state until the callback logic is moved to
+                // UserVisibilityManager
+                int previousCurrentUserId; boolean notifyPreviousCurrentUserId;
                 synchronized (mLock) {
+                    previousCurrentUserId = mCurrentUserId;
+                    notifyPreviousCurrentUserId = mVisibleUsers.get(previousCurrentUserId);
+                    if (notifyPreviousCurrentUserId) {
+                        deleteVisibleUserLocked(previousCurrentUserId);
+                    }
                     mCurrentUserId = userId;
                     mTargetUserId = UserHandle.USER_NULL; // reset, mCurrentUserId has caught up
                     userSwitchUiEnabled = mUserSwitchUiEnabled;
                 }
                 mInjector.updateUserConfiguration();
-                updateCurrentProfileIds();
+                // TODO(b/244644281): updateProfileRelatedCaches() is called on both if and else
+                // parts, ideally it should be moved outside, but for now it's not as there are many
+                // calls to external components here afterwards
+                updateProfileRelatedCaches();
                 mInjector.getWindowManager().setCurrentUser(userId);
                 mInjector.reportCurWakefulnessUsageEvent();
                 // Once the internal notion of the active user has switched, we lock the device
@@ -1647,9 +1753,14 @@
                         mInjector.getWindowManager().lockNow(null);
                     }
                 }
+                if (notifyPreviousCurrentUserId) {
+                    mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG,
+                            previousCurrentUserId, 0));
+                }
+
             } else {
                 final Integer currentUserIdInt = mCurrentUserId;
-                updateCurrentProfileIds();
+                updateProfileRelatedCaches();
                 synchronized (mLock) {
                     mUserLru.remove(currentUserIdInt);
                     mUserLru.add(currentUserIdInt);
@@ -1657,6 +1768,12 @@
             }
             t.traceEnd();
 
+            if (visible) {
+                synchronized (mLock) {
+                    addVisibleUserLocked(userId);
+                }
+            }
+
             // Make sure user is in the started state.  If it is currently
             // stopping, we need to knock that off.
             if (uss.state == UserState.STATE_STOPPING) {
@@ -1693,10 +1810,20 @@
                 // Booting up a new user, need to tell system services about it.
                 // Note that this is on the same handler as scheduling of broadcasts,
                 // which is important because it needs to go first.
-                mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, 0));
+                mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId,
+                        visible ? 1 : 0));
                 t.traceEnd();
             }
 
+            if (visible) {
+                // User was already running and became visible (for example, when switching to a
+                // user that was started in the background before), so it's necessary to explicitly
+                // notify the services (while when the user starts from BOOTING, USER_START_MSG
+                // takes care of that.
+                mHandler.sendMessage(
+                        mHandler.obtainMessage(USER_VISIBILITY_CHANGED_MSG, userId, 1));
+            }
+
             t.traceBegin("sendMessages");
             if (foreground) {
                 mHandler.sendMessage(mHandler.obtainMessage(USER_CURRENT_MSG, userId, oldUserId));
@@ -1997,6 +2124,8 @@
     }
 
     private void timeoutUserSwitch(UserState uss, int oldUserId, int newUserId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("timeoutUserSwitch-" + oldUserId + "-to-" + newUserId);
         synchronized (mLock) {
             Slogf.e(TAG, "User switch timeout: from " + oldUserId + " to " + newUserId);
             mTimeoutUserSwitchCallbacks = mCurWaitingUserSwitchCallbacks;
@@ -2006,6 +2135,7 @@
             mHandler.sendMessageDelayed(mHandler.obtainMessage(USER_SWITCH_CALLBACKS_TIMEOUT_MSG,
                     oldUserId, newUserId), USER_SWITCH_CALLBACKS_TIMEOUT_MS);
         }
+        t.traceEnd();
     }
 
     private void timeoutUserSwitchCallbacks(int oldUserId, int newUserId) {
@@ -2063,6 +2193,8 @@
                                             + " ms after dispatchUserSwitch.");
                                 }
 
+                                TimingsTraceAndSlog t2 = new TimingsTraceAndSlog(TAG);
+                                t2.traceBegin("onUserSwitchingReply-" + name);
                                 curWaitingUserSwitchCallbacks.remove(name);
                                 // Continue switching if all callbacks have been notified and
                                 // user switching session is still valid
@@ -2071,11 +2203,15 @@
                                         == mCurWaitingUserSwitchCallbacks)) {
                                     sendContinueUserSwitchLU(uss, oldUserId, newUserId);
                                 }
+                                t2.traceEnd();
                             }
                         }
                     };
+                    t.traceBegin("onUserSwitching-" + name);
                     mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback);
+                    t.traceEnd();
                 } catch (RemoteException e) {
+                    // Ignore
                 }
             }
         } else {
@@ -2089,10 +2225,13 @@
 
     @GuardedBy("mLock")
     private void sendContinueUserSwitchLU(UserState uss, int oldUserId, int newUserId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("sendContinueUserSwitchLU-" + oldUserId + "-to-" + newUserId);
         mCurWaitingUserSwitchCallbacks = null;
         mHandler.removeMessages(USER_SWITCH_TIMEOUT_MSG);
         mHandler.sendMessage(mHandler.obtainMessage(CONTINUE_USER_SWITCH_MSG,
                 oldUserId, newUserId, uss));
+        t.traceEnd();
     }
 
     @VisibleForTesting
@@ -2107,31 +2246,35 @@
         mHandler.sendMessage(mHandler.obtainMessage(COMPLETE_USER_SWITCH_MSG, newUserId, 0));
 
         uss.switching = false;
-        mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
-        mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0));
         stopGuestOrEphemeralUserIfBackground(oldUserId);
         stopUserOnSwitchIfEnforced(oldUserId);
+        if (oldUserId == UserHandle.USER_SYSTEM) {
+            // System user is never stopped, but its visibility is changed (as it is brought to the
+            // background)
+            updateSystemUserVisibility(t, /* visible= */ false);
+        }
 
         t.traceEnd(); // end continueUserSwitch
     }
 
     @VisibleForTesting
     void completeUserSwitch(int newUserId) {
-        if (isUserSwitchUiEnabled()) {
-            // If there is no challenge set, dismiss the keyguard right away
-            if (!mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
-                // Wait until the keyguard is dismissed to unfreeze
-                mInjector.dismissKeyguard(
-                        new Runnable() {
-                            public void run() {
-                                unfreezeScreen();
-                            }
-                        },
-                        "User Switch");
-                return;
-            } else {
+        final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
+        final Runnable runnable = () -> {
+            if (isUserSwitchUiEnabled) {
                 unfreezeScreen();
             }
+            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0));
+        };
+
+        // If there is no challenge set, dismiss the keyguard right away
+        if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
+            // Wait until the keyguard is dismissed to unfreeze
+            mInjector.dismissKeyguard(runnable, "User Switch");
+        } else {
+            runnable.run();
         }
     }
 
@@ -2414,9 +2557,7 @@
     void setAllowUserUnlocking(boolean allowed) {
         mAllowUserUnlocking = allowed;
         if (DEBUG_MU) {
-            // TODO(b/245335748): use Slogf.d instead
-            // Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed);
-            android.util.Slog.d(TAG, "setAllowUserUnlocking():" + allowed, new Exception());
+            Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed);
         }
     }
 
@@ -2458,18 +2599,50 @@
     }
 
     void onSystemReady() {
-        updateCurrentProfileIds();
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "onSystemReady()");
+
+        }
+        mInjector.getUserManagerInternal().addUserLifecycleListener(mUserLifecycleListener);
+        updateProfileRelatedCaches();
         mInjector.reportCurWakefulnessUsageEvent();
     }
 
+    // TODO(b/242195409): remove this method if initial system user boot logic is refactored?
+    void onSystemUserStarting() {
+        if (!UserManager.isHeadlessSystemUserMode()) {
+            // Don't need to call on HSUM because it will be called when the system user is
+            // restarted on background
+            mInjector.onUserStarting(UserHandle.USER_SYSTEM);
+            mInjector.onUserVisibilityChanged(UserHandle.USER_SYSTEM, /* visible= */ true);
+        }
+    }
+
+    private void updateSystemUserVisibility(TimingsTraceAndSlog t, boolean visible) {
+        t.traceBegin("update-system-userVisibility-" + visible);
+        if (DEBUG_MU) {
+            Slogf.d(TAG, "updateSystemUserVisibility(): visible=%b", visible);
+        }
+        int userId = UserHandle.USER_SYSTEM;
+        synchronized (mLock) {
+            if (visible) {
+                addVisibleUserLocked(userId);
+            } else {
+                deleteVisibleUserLocked(userId);
+            }
+        }
+        mInjector.onUserVisibilityChanged(userId, visible);
+        t.traceEnd();
+    }
+
     /**
-     * Refreshes the list of users related to the current user when either a
-     * user switch happens or when a new related user is started in the
-     * background.
+     * Refreshes the internal caches related to user profiles.
+     *
+     * <p>It's called every time a user is started.
      */
-    private void updateCurrentProfileIds() {
+    private void updateProfileRelatedCaches() {
         final List<UserInfo> profiles = mInjector.getUserManager().getProfiles(getCurrentUserId(),
-                false /* enabledOnly */);
+                /* enabledOnly= */ false);
         int[] currentProfileIds = new int[profiles.size()]; // profiles will not be null
         for (int i = 0; i < currentProfileIds.length; i++) {
             currentProfileIds[i] = profiles.get(i).id;
@@ -2736,6 +2909,18 @@
         }
     }
 
+    private void onUserAdded(UserInfo user) {
+        if (!user.isProfile()) {
+            return;
+        }
+        synchronized (mLock) {
+            if (user.profileGroupId == mCurrentUserId) {
+                mCurrentProfileIds = ArrayUtils.appendInt(mCurrentProfileIds, user.id);
+            }
+            mUserProfileGroupIds.put(user.id, user.profileGroupId);
+        }
+    }
+
     void onUserRemoved(@UserIdInt int userId) {
         synchronized (mLock) {
             int size = mUserProfileGroupIds.size();
@@ -2847,6 +3032,13 @@
                     proto.end(uToken);
                 }
             }
+            for (int i = 0; i < mVisibleUsers.size(); i++) {
+                proto.write(UserControllerProto.VISIBLE_USERS_ARRAY, mVisibleUsers.keyAt(i));
+            }
+            proto.write(UserControllerProto.CURRENT_USER, mCurrentUserId);
+            for (int i = 0; i < mCurrentProfileIds.length; i++) {
+                proto.write(UserControllerProto.CURRENT_PROFILES, mCurrentProfileIds[i]);
+            }
             proto.end(token);
         }
     }
@@ -2884,6 +3076,7 @@
                     pw.println(mUserProfileGroupIds.valueAt(i));
                 }
             }
+            pw.println("  mCurrentProfileIds:" + Arrays.toString(mCurrentProfileIds));
             pw.println("  mCurrentUserId:" + mCurrentUserId);
             pw.println("  mTargetUserId:" + mTargetUserId);
             pw.println("  mLastActiveUsers:" + mLastActiveUsers);
@@ -2900,7 +3093,8 @@
             if (mSwitchingToSystemUserMessage != null) {
                 pw.println("  mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage);
             }
-            pw.println("  mLastUserUnlockingUptime:" + mLastUserUnlockingUptime);
+            pw.println("  mLastUserUnlockingUptime: " + mLastUserUnlockingUptime);
+            pw.println("  mVisibleUsers: " + mVisibleUsers);
         }
     }
 
@@ -2937,8 +3131,7 @@
                 logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER,
                         USER_LIFECYCLE_EVENT_STATE_BEGIN);
 
-                mInjector.getSystemServiceManager().onUserStarting(
-                        TimingsTraceAndSlog.newAsyncLog(), msg.arg1);
+                mInjector.onUserStarting(/* userId= */ msg.arg1);
                 scheduleOnUserCompletedEvent(msg.arg1,
                         UserCompletedEventType.EVENT_TYPE_USER_STARTING,
                         USER_COMPLETED_EVENT_DELAY_MS);
@@ -3019,6 +3212,10 @@
             case COMPLETE_USER_SWITCH_MSG:
                 completeUserSwitch(msg.arg1);
                 break;
+            case USER_VISIBILITY_CHANGED_MSG:
+                mInjector.onUserVisibilityChanged(/* userId= */ msg.arg1,
+                        /* visible= */ msg.arg2 == 1);
+                break;
         }
         return false;
     }
@@ -3328,6 +3525,14 @@
             }
             EventLog.writeEvent(EventLogTags.UC_SEND_USER_BROADCAST, logUserId, intent.getAction());
 
+            // When the modern broadcast stack is enabled, deliver all our
+            // broadcasts as unordered, since the modern stack has better
+            // support for sequencing cold-starts, and it supports delivering
+            // resultTo for non-ordered broadcasts
+            if (mService.mEnableModernQueue) {
+                ordered = false;
+            }
+
             // TODO b/64165549 Verify that mLock is not held before calling AMS methods
             synchronized (mService) {
                 return mService.broadcastIntentLocked(null, null, null, intent, resolvedType,
@@ -3511,20 +3716,28 @@
         }
 
         protected void dismissKeyguard(Runnable runnable, String reason) {
+            final AtomicBoolean isFirst = new AtomicBoolean(true);
+            final Runnable runOnce = () -> {
+                if (isFirst.getAndSet(false)) {
+                    runnable.run();
+                }
+            };
+
+            mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS);
             getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() {
                 @Override
                 public void onDismissError() throws RemoteException {
-                    mHandler.post(runnable);
+                    mHandler.post(runOnce);
                 }
 
                 @Override
                 public void onDismissSucceeded() throws RemoteException {
-                    mHandler.post(runnable);
+                    mHandler.post(runOnce);
                 }
 
                 @Override
                 public void onDismissCancelled() throws RemoteException {
-                    mHandler.post(runnable);
+                    mHandler.post(runOnce);
                 }
             }, reason);
         }
@@ -3532,5 +3745,13 @@
         boolean isUsersOnSecondaryDisplaysEnabled() {
             return UserManager.isUsersOnSecondaryDisplaysEnabled();
         }
+
+        void onUserStarting(@UserIdInt int userId) {
+            getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId);
+        }
+
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            getUserManagerInternal().onUserVisibilityChanged(userId, visible);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index e11d95a..64f2aa3 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -490,6 +490,8 @@
         private final Object mModeConfigLock = new Object();
         @GuardedBy("mModeConfigLock")
         private final ArrayMap<Integer, GameModeConfiguration> mModeConfigs = new ArrayMap<>();
+        // if adding new properties or make any of the below overridable, the method
+        // copyAndApplyOverride should be updated accordingly
         private boolean mPerfModeOptedIn = false;
         private boolean mBatteryModeOptedIn = false;
         private boolean mAllowDownscale = true;
@@ -800,6 +802,42 @@
             }
         }
 
+        GamePackageConfiguration copyAndApplyOverride(GamePackageConfiguration overrideConfig) {
+            GamePackageConfiguration copy = new GamePackageConfiguration(mPackageName);
+            // if a game mode is overridden, we treat it with the highest priority and reset any
+            // opt-in game modes so that interventions are always executed.
+            copy.mPerfModeOptedIn = mPerfModeOptedIn && !(overrideConfig != null
+                    && overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE)
+                    != null);
+            copy.mBatteryModeOptedIn = mBatteryModeOptedIn && !(overrideConfig != null
+                    && overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY)
+                    != null);
+
+            // if any game mode is overridden, we will consider all interventions forced-active,
+            // this can be done more granular by checking if a specific intervention is
+            // overridden under each game mode override, but only if necessary.
+            copy.mAllowDownscale = mAllowDownscale || overrideConfig != null;
+            copy.mAllowAngle = mAllowAngle || overrideConfig != null;
+            copy.mAllowFpsOverride = mAllowFpsOverride || overrideConfig != null;
+            if (overrideConfig != null) {
+                synchronized (copy.mModeConfigLock) {
+                    synchronized (mModeConfigLock) {
+                        for (Map.Entry<Integer, GameModeConfiguration> entry :
+                                mModeConfigs.entrySet()) {
+                            copy.mModeConfigs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                    synchronized (overrideConfig.mModeConfigLock) {
+                        for (Map.Entry<Integer, GameModeConfiguration> entry :
+                                overrideConfig.mModeConfigs.entrySet()) {
+                            copy.mModeConfigs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                }
+            }
+            return copy;
+        }
+
         public String toString() {
             synchronized (mModeConfigLock) {
                 return "[Name:" + mPackageName + " Modes: " + mModeConfigs.toString() + "]";
@@ -845,6 +883,7 @@
 
         @Override
         public void onUserStarting(@NonNull TargetUser user) {
+            Slog.d(TAG, "Starting user " + user.getUserIdentifier());
             mService.onUserStarting(user,
                     Environment.getDataSystemDeDirectory(user.getUserIdentifier()));
         }
@@ -1009,6 +1048,8 @@
                     "com.android.server.app.GameManagerService");
 
             if (!mSettings.containsKey(userId)) {
+                Slog.d(TAG, "Failed to set game mode for package " + packageName
+                        + " as user " + userId + " is not started");
                 return;
             }
             GameManagerSettings userSettings = mSettings.get(userId);
@@ -1273,16 +1314,9 @@
 
     void onUserSwitching(TargetUser from, TargetUser to) {
         final int toUserId = to.getUserIdentifier();
-        if (from != null) {
-            synchronized (mLock) {
-                final int fromUserId = from.getUserIdentifier();
-                if (mSettings.containsKey(fromUserId)) {
-                    sendUserMessage(fromUserId, REMOVE_SETTINGS, "ON_USER_SWITCHING",
-                            0 /*delayMillis*/);
-                }
-            }
-        }
-
+        // we want to re-populate the setting when switching user as the device config may have
+        // changed, which will only update for the previous user, see
+        // DeviceConfigListener#onPropertiesChanged.
         sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_SWITCHING",
                 0 /*delayMillis*/);
 
@@ -1298,7 +1332,7 @@
         try {
             final float fps = 0.0f;
             final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
-            nativeSetOverrideFrameRate(uid, fps);
+            setOverrideFrameRate(uid, fps);
         } catch (PackageManager.NameNotFoundException e) {
             return;
         }
@@ -1330,7 +1364,7 @@
         try {
             final float fps = modeConfig.getFps();
             final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
-            nativeSetOverrideFrameRate(uid, fps);
+            setOverrideFrameRate(uid, fps);
         } catch (PackageManager.NameNotFoundException e) {
             return;
         }
@@ -1339,20 +1373,21 @@
 
     private void updateInterventions(String packageName,
             @GameMode int gameMode, @UserIdInt int userId) {
-        if (gameMode == GameManager.GAME_MODE_STANDARD
-                || gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
-            resetFps(packageName, userId);
-            return;
-        }
         final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
-        if (packageConfig == null) {
-            Slog.v(TAG, "Package configuration not found for " + packageName);
-            return;
+        if (gameMode == GameManager.GAME_MODE_STANDARD
+                || gameMode == GameManager.GAME_MODE_UNSUPPORTED || packageConfig == null
+                || packageConfig.willGamePerformOptimizations(gameMode)) {
+            resetFps(packageName, userId);
+            // resolution scaling does not need to be reset as it's now read dynamically on game
+            // restart, see #getResolutionScalingFactor and CompatModePackages#getCompatScale.
+            // TODO: reset Angle intervention here once implemented
+            if (packageConfig == null) {
+                Slog.v(TAG, "Package configuration not found for " + packageName);
+                return;
+            }
+        } else {
+            updateFps(packageConfig, packageName, gameMode, userId);
         }
-        if (packageConfig.willGamePerformOptimizations(gameMode)) {
-            return;
-        }
-        updateFps(packageConfig, packageName, gameMode, userId);
         updateUseAngle(packageName, gameMode);
     }
 
@@ -1375,7 +1410,7 @@
             // look for the existing GamePackageConfiguration override
             configOverride = settings.getConfigOverride(packageName);
             if (configOverride == null) {
-                configOverride = new GamePackageConfiguration(mPackageManager, packageName, userId);
+                configOverride = new GamePackageConfiguration(packageName);
                 settings.setConfigOverride(packageName, configOverride);
             }
         }
@@ -1430,18 +1465,12 @@
                     return;
                 }
                 // if the game mode to reset is the only mode other than standard mode or there
-                // is device config, the config override is removed.
+                // is device config, the entire package config override is removed.
                 if (Integer.bitCount(modesBitfield) <= 2 || deviceConfig == null) {
                     settings.removeConfigOverride(packageName);
                 } else {
-                    final GamePackageConfiguration.GameModeConfiguration defaultModeConfig =
-                            deviceConfig.getGameModeConfiguration(gameModeToReset);
-                    // otherwise we reset the mode by copying the original config.
-                    if (defaultModeConfig == null) {
-                        configOverride.removeModeConfig(gameModeToReset);
-                    } else {
-                        configOverride.addModeConfig(defaultModeConfig);
-                    }
+                    // otherwise we reset the mode by removing the game mode config override
+                    configOverride.removeModeConfig(gameModeToReset);
                 }
             } else {
                 settings.removeConfigOverride(packageName);
@@ -1661,18 +1690,21 @@
      * @hide
      */
     public GamePackageConfiguration getConfig(String packageName, int userId) {
-        GamePackageConfiguration packageConfig = null;
+        GamePackageConfiguration overrideConfig = null;
+        GamePackageConfiguration config;
+        synchronized (mDeviceConfigLock) {
+            config = mConfigs.get(packageName);
+        }
+
         synchronized (mLock) {
             if (mSettings.containsKey(userId)) {
-                packageConfig = mSettings.get(userId).getConfigOverride(packageName);
+                overrideConfig = mSettings.get(userId).getConfigOverride(packageName);
             }
         }
-        if (packageConfig == null) {
-            synchronized (mDeviceConfigLock) {
-                packageConfig = mConfigs.get(packageName);
-            }
+        if (overrideConfig == null || config == null) {
+            return overrideConfig == null ? config : overrideConfig;
         }
-        return packageConfig;
+        return config.copyAndApplyOverride(overrideConfig);
     }
 
     private void registerPackageReceiver() {
@@ -1774,6 +1806,11 @@
         return handlerThread;
     }
 
+    @VisibleForTesting
+    void setOverrideFrameRate(int uid, float frameRate) {
+        nativeSetOverrideFrameRate(uid, frameRate);
+    }
+
     /**
      * load dynamic library for frame rate overriding JNI calls
      */
diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java
index 1162498..1e68837 100644
--- a/services/core/java/com/android/server/app/GameManagerSettings.java
+++ b/services/core/java/com/android/server/app/GameManagerSettings.java
@@ -21,12 +21,12 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.app.GameManagerService.GamePackageConfiguration;
 import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;
 
diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
index 4aaf1ab..908cb3f 100644
--- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
+++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java
@@ -185,6 +185,8 @@
                 @Override
                 @EnforcePermission(MANAGE_GAME_ACTIVITY)
                 public void createGameSession(int taskId) {
+                    super.createGameSession_enforcePermission();
+
                     mBackgroundExecutor.execute(() -> {
                         GameServiceProviderInstanceImpl.this.createGameSession(taskId);
                     });
@@ -197,6 +199,8 @@
                 @EnforcePermission(MANAGE_GAME_ACTIVITY)
                 public void takeScreenshot(int taskId,
                         @NonNull AndroidFuture gameScreenshotResultFuture) {
+                    super.takeScreenshot_enforcePermission();
+
                     mBackgroundExecutor.execute(() -> {
                         GameServiceProviderInstanceImpl.this.takeScreenshot(taskId,
                                 gameScreenshotResultFuture);
@@ -206,6 +210,8 @@
                 @Override
                 @EnforcePermission(MANAGE_GAME_ACTIVITY)
                 public void restartGame(int taskId) {
+                    super.restartGame_enforcePermission();
+
                     mBackgroundExecutor.execute(() -> {
                         GameServiceProviderInstanceImpl.this.restartGame(taskId);
                     });
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index bc650ad..7e00c32 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -131,8 +131,6 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -152,6 +150,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
 import com.android.server.SystemServerInitThreadPool;
@@ -2920,18 +2920,18 @@
     }
 
     @Override
-    public SyncNotedAppOp startProxyOperation(int code,
+    public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
             boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
             boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
             @AttributionFlags int proxiedAttributionFlags, int attributionChainId) {
-        return mCheckOpsDelegateDispatcher.startProxyOperation(code, attributionSource,
+        return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, attributionSource,
                 startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
                 skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags,
                 attributionChainId);
     }
 
-    private SyncNotedAppOp startProxyOperationImpl(int code,
+    private SyncNotedAppOp startProxyOperationImpl(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource,
             boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message,
             boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags
@@ -2940,11 +2940,9 @@
         final int proxyUid = attributionSource.getUid();
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
-        final IBinder proxyToken = attributionSource.getToken();
         final int proxiedUid = attributionSource.getNextUid();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
-        final IBinder proxiedToken = attributionSource.getNextToken();
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
@@ -2986,7 +2984,7 @@
 
         if (!skipProxyOperation) {
             // Test if the proxied operation will succeed before starting the proxy operation
-            final SyncNotedAppOp testProxiedOp = startOperationUnchecked(proxiedToken, code,
+            final SyncNotedAppOp testProxiedOp = startOperationUnchecked(clientId, code,
                     proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid,
                     resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault,
                     shouldCollectAsyncNotedOp, message, shouldCollectMessage,
@@ -2998,7 +2996,7 @@
             final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY
                     : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
 
-            final SyncNotedAppOp proxyAppOp = startOperationUnchecked(proxyToken, code, proxyUid,
+            final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
                     resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null,
                     proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message,
                     shouldCollectMessage, proxyAttributionFlags, attributionChainId,
@@ -3008,7 +3006,7 @@
             }
         }
 
-        return startOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName,
+        return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
                 proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag,
                 proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message,
                 shouldCollectMessage, proxiedAttributionFlags, attributionChainId,
@@ -3151,22 +3149,20 @@
     }
 
     @Override
-    public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource,
-            boolean skipProxyOperation) {
-        mCheckOpsDelegateDispatcher.finishProxyOperation(code, attributionSource,
+    public void finishProxyOperation(@NonNull IBinder clientId, int code,
+            @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
+        mCheckOpsDelegateDispatcher.finishProxyOperation(clientId, code, attributionSource,
                 skipProxyOperation);
     }
 
-    private Void finishProxyOperationImpl(int code, @NonNull AttributionSource attributionSource,
-            boolean skipProxyOperation) {
+    private Void finishProxyOperationImpl(IBinder clientId, int code,
+            @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
         final int proxyUid = attributionSource.getUid();
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
-        final IBinder proxyToken = attributionSource.getToken();
         final int proxiedUid = attributionSource.getNextUid();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
-        final IBinder proxiedToken = attributionSource.getNextToken();
 
         skipProxyOperation = skipProxyOperation
                 && isCallerAndAttributionTrusted(attributionSource);
@@ -3185,7 +3181,7 @@
         }
 
         if (!skipProxyOperation) {
-            finishOperationUnchecked(proxyToken, code, proxyUid, resolvedProxyPackageName,
+            finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
                     proxyAttributionTag);
         }
 
@@ -3195,7 +3191,7 @@
             return null;
         }
 
-        finishOperationUnchecked(proxiedToken, code, proxiedUid, resolvedProxiedPackageName,
+        finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
                 proxiedAttributionTag);
 
         return null;
@@ -6262,7 +6258,6 @@
         Objects.requireNonNull(stackTrace);
         Preconditions.checkArgument(op >= 0);
         Preconditions.checkArgument(op < AppOpsManager._NUM_OP);
-        Objects.requireNonNull(version);
 
         NoteOpTrace noteOpTrace = new NoteOpTrace(stackTrace, op, packageName, version);
 
@@ -6436,42 +6431,42 @@
                     attributionFlags, attributionChainId, AppOpsService.this::startOperationImpl);
         }
 
-        public SyncNotedAppOp startProxyOperation(int code,
+        public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
                 boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
                 boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
                 @AttributionFlags int proxiedAttributionFlags, int attributionChainId) {
             if (mPolicy != null) {
                 if (mCheckOpsDelegate != null) {
-                    return mPolicy.startProxyOperation(code, attributionSource,
+                    return mPolicy.startProxyOperation(clientId, code, attributionSource,
                             startIfModeDefault, shouldCollectAsyncNotedOp, message,
                             shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
                             proxiedAttributionFlags, attributionChainId,
                             this::startDelegateProxyOperationImpl);
                 } else {
-                    return mPolicy.startProxyOperation(code, attributionSource,
+                    return mPolicy.startProxyOperation(clientId, code, attributionSource,
                             startIfModeDefault, shouldCollectAsyncNotedOp, message,
                             shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
                             proxiedAttributionFlags, attributionChainId,
                             AppOpsService.this::startProxyOperationImpl);
                 }
             } else if (mCheckOpsDelegate != null) {
-                return startDelegateProxyOperationImpl(code, attributionSource,
+                return startDelegateProxyOperationImpl(clientId, code, attributionSource,
                         startIfModeDefault, shouldCollectAsyncNotedOp, message,
                         shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
                         proxiedAttributionFlags, attributionChainId);
             }
-            return startProxyOperationImpl(code, attributionSource, startIfModeDefault,
+            return startProxyOperationImpl(clientId, code, attributionSource, startIfModeDefault,
                     shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation,
                     proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
         }
 
-        private SyncNotedAppOp startDelegateProxyOperationImpl(int code,
+        private SyncNotedAppOp startDelegateProxyOperationImpl(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
                 boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
                 boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
                 @AttributionFlags int proxiedAttributionFlsgs, int attributionChainId) {
-            return mCheckOpsDelegate.startProxyOperation(code, attributionSource,
+            return mCheckOpsDelegate.startProxyOperation(clientId, code, attributionSource,
                     startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
                     skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlsgs,
                     attributionChainId, AppOpsService.this::startProxyOperationImpl);
@@ -6500,27 +6495,28 @@
                     AppOpsService.this::finishOperationImpl);
         }
 
-        public void finishProxyOperation(int code,
+        public void finishProxyOperation(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
             if (mPolicy != null) {
                 if (mCheckOpsDelegate != null) {
-                    mPolicy.finishProxyOperation(code, attributionSource,
+                    mPolicy.finishProxyOperation(clientId, code, attributionSource,
                             skipProxyOperation, this::finishDelegateProxyOperationImpl);
                 } else {
-                    mPolicy.finishProxyOperation(code, attributionSource,
+                    mPolicy.finishProxyOperation(clientId, code, attributionSource,
                             skipProxyOperation, AppOpsService.this::finishProxyOperationImpl);
                 }
             } else if (mCheckOpsDelegate != null) {
-                finishDelegateProxyOperationImpl(code, attributionSource, skipProxyOperation);
+                finishDelegateProxyOperationImpl(clientId, code, attributionSource,
+                        skipProxyOperation);
             } else {
-                finishProxyOperationImpl(code, attributionSource, skipProxyOperation);
+                finishProxyOperationImpl(clientId, code, attributionSource, skipProxyOperation);
             }
         }
 
-        private Void finishDelegateProxyOperationImpl(int code,
+        private Void finishDelegateProxyOperationImpl(@NonNull IBinder clientId, int code,
                 @NonNull AttributionSource attributionSource, boolean skipProxyOperation) {
-            mCheckOpsDelegate.finishProxyOperation(code, attributionSource, skipProxyOperation,
-                    AppOpsService.this::finishProxyOperationImpl);
+            mCheckOpsDelegate.finishProxyOperation(clientId, code, attributionSource,
+                    skipProxyOperation, AppOpsService.this::finishProxyOperationImpl);
             return null;
         }
     }
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index 3c281d1..5114bd5 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -26,6 +26,7 @@
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_IGNORED;
 import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
 import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
@@ -171,6 +172,7 @@
                     return MODE_ALLOWED;
                 }
             case OP_RECORD_AUDIO:
+            case OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO:
                 if ((capability & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) == 0) {
                     return MODE_IGNORED;
                 } else {
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index dd0c4b86..10243e2 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -53,13 +53,13 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index 2c68aaf..bd9d057 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -52,8 +52,6 @@
 import android.util.LongSparseArray;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -62,6 +60,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index c59ee83..2fe06094 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -286,22 +286,9 @@
         if (AudioService.DEBUG_COMM_RTE) {
             Log.v(TAG, "setSpeakerphoneOn, on: " + on + " pid: " + pid);
         }
-
-        synchronized (mSetModeLock) {
-            synchronized (mDeviceStateLock) {
-                AudioDeviceAttributes device = null;
-                if (on) {
-                    device = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, "");
-                } else {
-                    CommunicationRouteClient client = getCommunicationRouteClientForPid(pid);
-                    if (client == null || !client.requestsSpeakerphone()) {
-                        return;
-                    }
-                }
-                postSetCommunicationRouteForClient(new CommunicationClientInfo(
-                        cb, pid, device, BtHelper.SCO_MODE_UNDEFINED, eventSource));
-            }
-        }
+        postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
+                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
+                on, BtHelper.SCO_MODE_UNDEFINED, eventSource, false));
     }
 
     /**
@@ -311,6 +298,9 @@
      * @param device Device selected or null to unselect.
      * @param eventSource for logging purposes
      */
+
+    private static final long SET_COMMUNICATION_DEVICE_TIMEOUT_MS = 3000;
+
     /*package*/ boolean setCommunicationDevice(
             IBinder cb, int pid, AudioDeviceInfo device, String eventSource) {
 
@@ -318,21 +308,53 @@
             Log.v(TAG, "setCommunicationDevice, device: " + device + ", pid: " + pid);
         }
 
-        synchronized (mSetModeLock) {
-            synchronized (mDeviceStateLock) {
-                AudioDeviceAttributes deviceAttr = null;
-                if (device != null) {
-                    deviceAttr = new AudioDeviceAttributes(device);
-                } else {
-                    CommunicationRouteClient client = getCommunicationRouteClientForPid(pid);
-                    if (client == null) {
-                        return false;
+        AudioDeviceAttributes deviceAttr =
+                (device != null) ? new AudioDeviceAttributes(device) : null;
+        CommunicationDeviceInfo deviceInfo = new CommunicationDeviceInfo(cb, pid, deviceAttr,
+                device != null, BtHelper.SCO_MODE_UNDEFINED, eventSource, true);
+        postSetCommunicationDeviceForClient(deviceInfo);
+        boolean status;
+        synchronized (deviceInfo) {
+            final long start = System.currentTimeMillis();
+            long elapsed = 0;
+            while (deviceInfo.mWaitForStatus) {
+                try {
+                    deviceInfo.wait(SET_COMMUNICATION_DEVICE_TIMEOUT_MS - elapsed);
+                } catch (InterruptedException e) {
+                    elapsed = System.currentTimeMillis() - start;
+                    if (elapsed >= SET_COMMUNICATION_DEVICE_TIMEOUT_MS) {
+                        deviceInfo.mStatus = false;
+                        deviceInfo.mWaitForStatus = false;
                     }
                 }
-                postSetCommunicationRouteForClient(new CommunicationClientInfo(
-                        cb, pid, deviceAttr, BtHelper.SCO_MODE_UNDEFINED, eventSource));
+            }
+            status = deviceInfo.mStatus;
+        }
+        return status;
+    }
+
+    /**
+     * Sets or resets the communication device for matching client. If no client matches and the
+     * request is to reset for a given device (deviceInfo.mOn == false), the method is a noop.
+     * @param deviceInfo information on the device and requester {@link #CommunicationDeviceInfo}
+     * @return true if the communication device is set or reset
+     */
+    @GuardedBy("mDeviceStateLock")
+    /*package*/ boolean onSetCommunicationDeviceForClient(CommunicationDeviceInfo deviceInfo) {
+        if (AudioService.DEBUG_COMM_RTE) {
+            Log.v(TAG, "onSetCommunicationDeviceForClient: " + deviceInfo);
+        }
+        if (!deviceInfo.mOn) {
+            CommunicationRouteClient client = getCommunicationRouteClientForPid(deviceInfo.mPid);
+            if (client == null || (deviceInfo.mDevice != null
+                    && !deviceInfo.mDevice.equals(client.getDevice()))) {
+                return false;
             }
         }
+
+        AudioDeviceAttributes device = deviceInfo.mOn ? deviceInfo.mDevice : null;
+        setCommunicationRouteForClient(deviceInfo.mCb, deviceInfo.mPid, device,
+                deviceInfo.mScoAudioMode, deviceInfo.mEventSource);
         return true;
     }
 
@@ -344,7 +366,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));
@@ -390,7 +412,7 @@
             mBtHelper.stopBluetoothSco(eventSource);
         }
 
-        sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE, SENDMSG_QUEUE, eventSource);
+        updateCommunicationRoute(eventSource);
     }
 
     /**
@@ -424,7 +446,7 @@
         CommunicationRouteClient crc = topCommunicationRouteClient();
         AudioDeviceAttributes device = crc != null ? crc.getDevice() : null;
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "requestedCommunicationDevice, device: "
+            Log.v(TAG, "requestedCommunicationDevice: "
                     + device + " mAudioModeOwner: " + mAudioModeOwner.toString());
         }
         return device;
@@ -822,37 +844,22 @@
                 @NonNull String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "startBluetoothScoForClient_Sync, pid: " + pid);
+            Log.v(TAG, "startBluetoothScoForClient, pid: " + pid);
         }
-
-        synchronized (mSetModeLock) {
-            synchronized (mDeviceStateLock) {
-                AudioDeviceAttributes device =
-                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, "");
-
-                postSetCommunicationRouteForClient(new CommunicationClientInfo(
-                        cb, pid, device, scoAudioMode, eventSource));
-            }
-        }
+        postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
+                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
+                true, scoAudioMode, eventSource, false));
     }
 
     /*package*/ void stopBluetoothScoForClient(
                         IBinder cb, int pid, @NonNull String eventSource) {
 
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "stopBluetoothScoForClient_Sync, pid: " + pid);
+            Log.v(TAG, "stopBluetoothScoForClient, pid: " + pid);
         }
-
-        synchronized (mSetModeLock) {
-            synchronized (mDeviceStateLock) {
-                CommunicationRouteClient client = getCommunicationRouteClientForPid(pid);
-                if (client == null || !client.requestsBluetoothSco()) {
-                    return;
-                }
-                postSetCommunicationRouteForClient(new CommunicationClientInfo(
-                        cb, pid, null, BtHelper.SCO_MODE_UNDEFINED, eventSource));
-            }
-        }
+        postSetCommunicationDeviceForClient(new CommunicationDeviceInfo(
+                cb, pid, new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, ""),
+                false, BtHelper.SCO_MODE_UNDEFINED, eventSource, false));
     }
 
     /*package*/ int setPreferredDevicesForStrategySync(int strategy,
@@ -990,7 +997,8 @@
     }
 
     //---------------------------------------------------------------------
-    // Message handling on behalf of helper classes
+    // Message handling on behalf of helper classes.
+    // Each of these methods posts a message to mBrokerHandler message queue.
     /*package*/ void postBroadcastScoConnectionState(int state) {
         sendIMsgNoDelay(MSG_I_BROADCAST_BT_CONNECTION_STATE, SENDMSG_QUEUE, state);
     }
@@ -1046,28 +1054,34 @@
         sendLMsgNoDelay(MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, eventSource);
     }
 
-    /*package*/ void postSetCommunicationRouteForClient(CommunicationClientInfo info) {
-        sendLMsgNoDelay(MSG_L_SET_COMMUNICATION_ROUTE_FOR_CLIENT, SENDMSG_QUEUE, info);
+    /*package*/ void postSetCommunicationDeviceForClient(CommunicationDeviceInfo info) {
+        sendLMsgNoDelay(MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT, SENDMSG_QUEUE, info);
     }
 
     /*package*/ void postScoAudioStateChanged(int state) {
         sendIMsgNoDelay(MSG_I_SCO_AUDIO_STATE_CHANGED, SENDMSG_QUEUE, state);
     }
 
-    /*package*/ static final class CommunicationClientInfo {
-        final @NonNull IBinder mCb;
-        final int mPid;
-        final @NonNull AudioDeviceAttributes mDevice;
-        final int mScoAudioMode;
-        final @NonNull String mEventSource;
+    /*package*/ static final class CommunicationDeviceInfo {
+        final @NonNull IBinder mCb; // Identifies the requesting client for death handler
+        final int mPid; // Requester process ID
+        final @Nullable AudioDeviceAttributes mDevice; // Device being set or reset.
+        final boolean mOn; // true if setting, false if resetting
+        final int mScoAudioMode; // only used for SCO: requested audio mode
+        final @NonNull String mEventSource; // caller identifier for logging
+        boolean mWaitForStatus; // true if the caller waits for a completion status (API dependent)
+        boolean mStatus = false; // completion status only used if mWaitForStatus is true
 
-        CommunicationClientInfo(@NonNull IBinder cb, int pid, @NonNull AudioDeviceAttributes device,
-                int scoAudioMode, @NonNull String eventSource) {
+        CommunicationDeviceInfo(@NonNull IBinder cb, int pid,
+                @Nullable AudioDeviceAttributes device, boolean on, int scoAudioMode,
+                @NonNull String eventSource, boolean waitForStatus) {
             mCb = cb;
             mPid = pid;
             mDevice = device;
+            mOn = on;
             mScoAudioMode = scoAudioMode;
             mEventSource = eventSource;
+            mWaitForStatus = waitForStatus;
         }
 
         // redefine equality op so we can match messages intended for this client
@@ -1079,21 +1093,24 @@
             if (this == o) {
                 return true;
             }
-            if (!(o instanceof CommunicationClientInfo)) {
+            if (!(o instanceof CommunicationDeviceInfo)) {
                 return false;
             }
 
-            return mCb.equals(((CommunicationClientInfo) o).mCb)
-                    && mPid == ((CommunicationClientInfo) o).mPid;
+            return mCb.equals(((CommunicationDeviceInfo) o).mCb)
+                    && mPid == ((CommunicationDeviceInfo) o).mPid;
         }
 
         @Override
         public String toString() {
-            return "CommunicationClientInfo mCb=" + mCb.toString()
-                    +"mPid=" + mPid
-                    +"mDevice=" + mDevice.toString()
-                    +"mScoAudioMode=" + mScoAudioMode
-                    +"mEventSource=" + mEventSource;
+            return "CommunicationDeviceInfo mCb=" + mCb.toString()
+                    + " mPid=" + mPid
+                    + " mDevice=[" + (mDevice != null ? mDevice.toString() : "null") + "]"
+                    + " mOn=" + mOn
+                    + " mScoAudioMode=" + mScoAudioMode
+                    + " mEventSource=" + mEventSource
+                    + " mWaitForStatus=" + mWaitForStatus
+                    + " mStatus=" + mStatus;
         }
     }
 
@@ -1212,7 +1229,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 +1247,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();
     }
@@ -1297,7 +1314,7 @@
                             updateActiveCommunicationDevice();
                             mDeviceInventory.onRestoreDevices();
                             mBtHelper.onAudioServerDiedRestoreA2dp();
-                            onUpdateCommunicationRoute("MSG_RESTORE_DEVICES");
+                            updateCommunicationRoute("MSG_RESTORE_DEVICES");
                         }
                     }
                     break;
@@ -1392,12 +1409,19 @@
                     }
                     break;
 
-                case MSG_L_SET_COMMUNICATION_ROUTE_FOR_CLIENT:
+                case MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT:
+                    CommunicationDeviceInfo deviceInfo = (CommunicationDeviceInfo) msg.obj;
+                    boolean status;
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            CommunicationClientInfo info = (CommunicationClientInfo) msg.obj;
-                            setCommunicationRouteForClient(info.mCb, info.mPid, info.mDevice,
-                                    info.mScoAudioMode, info.mEventSource);
+                            status = onSetCommunicationDeviceForClient(deviceInfo);
+                        }
+                    }
+                    synchronized (deviceInfo) {
+                        if (deviceInfo.mWaitForStatus) {
+                            deviceInfo.mStatus = status;
+                            deviceInfo.mWaitForStatus = false;
+                            deviceInfo.notify();
                         }
                     }
                     break;
@@ -1410,14 +1434,6 @@
                     }
                     break;
 
-                case MSG_L_UPDATE_COMMUNICATION_ROUTE:
-                    synchronized (mSetModeLock) {
-                        synchronized (mDeviceStateLock) {
-                            onUpdateCommunicationRoute((String) msg.obj);
-                        }
-                    }
-                    break;
-
                 case MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
@@ -1468,7 +1484,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
@@ -1568,8 +1584,7 @@
     private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 37;
     private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
 
-    private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE = 39;
-    private static final int MSG_L_SET_COMMUNICATION_ROUTE_FOR_CLIENT = 42;
+    private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42;
     private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
     private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44;
 
@@ -1793,18 +1808,6 @@
         AudioDeviceAttributes getDevice() {
             return mDevice;
         }
-
-        boolean requestsBluetoothSco() {
-            return mDevice != null
-                    && mDevice.getType()
-                        == AudioDeviceInfo.TYPE_BLUETOOTH_SCO;
-        }
-
-        boolean requestsSpeakerphone() {
-            return mDevice != null
-                    && mDevice.getType()
-                        == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
-        }
     }
 
     // @GuardedBy("mSetModeLock")
@@ -1852,14 +1855,14 @@
      */
     // @GuardedBy("mSetModeLock")
     @GuardedBy("mDeviceStateLock")
-    private void onUpdateCommunicationRoute(String eventSource) {
+    private void updateCommunicationRoute(String eventSource) {
         AudioDeviceAttributes preferredCommunicationDevice = preferredCommunicationDevice();
         if (AudioService.DEBUG_COMM_RTE) {
-            Log.v(TAG, "onUpdateCommunicationRoute, preferredCommunicationDevice: "
+            Log.v(TAG, "updateCommunicationRoute, preferredCommunicationDevice: "
                     + preferredCommunicationDevice + " eventSource: " + eventSource);
         }
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
-                "onUpdateCommunicationRoute, preferredCommunicationDevice: "
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                "updateCommunicationRoute, preferredCommunicationDevice: "
                 + preferredCommunicationDevice + " eventSource: " + eventSource)));
 
         if (preferredCommunicationDevice == null
@@ -1895,7 +1898,7 @@
     // @GuardedBy("mSetModeLock")
     @GuardedBy("mDeviceStateLock")
     private void onUpdateCommunicationRouteClient(String eventSource) {
-        onUpdateCommunicationRoute(eventSource);
+        updateCommunicationRoute(eventSource);
         CommunicationRouteClient crc = topCommunicationRouteClient();
         if (AudioService.DEBUG_COMM_RTE) {
             Log.v(TAG, "onUpdateCommunicationRouteClient, crc: "
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 cbfd17f0..4fda233 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -41,6 +41,7 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.ActivityThread;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -153,6 +154,7 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.System;
 import android.service.notification.ZenModeConfig;
@@ -174,6 +176,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
@@ -233,6 +236,7 @@
             AudioSystemAdapter.OnVolRangeInitRequestListener {
 
     private static final String TAG = "AS.AudioService";
+    private static final boolean CONFIG_DEFAULT_VAL = false;
 
     private final AudioSystemAdapter mAudioSystem;
     private final SystemServerAdapter mSystemServer;
@@ -987,10 +991,11 @@
      * @param looper Looper to use for the service's message handler. If this is null, an
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      */
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     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;
@@ -1026,8 +1031,12 @@
         mUseVolumeGroupAliases = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
 
-        mNotifAliasRing = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_alias_ring_notif_stream_types);
+        mNotifAliasRing = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
+
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+                ActivityThread.currentApplication().getMainExecutor(),
+                this::onDeviceConfigChange);
 
         // Initialize volume
         // Priority 1 - Android Property
@@ -1246,6 +1255,22 @@
     }
 
     /**
+     * Separating notification volume from ring is NOT of aliasing the corresponding streams
+     * @param properties
+     */
+    private void onDeviceConfigChange(DeviceConfig.Properties properties) {
+        Set<String> changeSet = properties.getKeyset();
+        if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
+            boolean newNotifAliasRing = !properties.getBoolean(
+                    SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL);
+            if (mNotifAliasRing != newNotifAliasRing) {
+                mNotifAliasRing = newNotifAliasRing;
+                updateStreamVolumeAlias(true, TAG);
+            }
+        }
+    }
+
+    /**
      * Called by handling of MSG_INIT_STREAMS_VOLUMES
      */
     private void onInitStreamsAndVolumes() {
@@ -1539,14 +1564,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 +1801,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 +1815,7 @@
         }
 
         // success
-        sLifecycleLogger.log(new EventLogger.StringEvent(
+        sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                 caller + ": initStreamVolume succeeded").printLog(ALOGI, TAG));
     }
 
@@ -1813,7 +1838,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,
@@ -1859,6 +1884,8 @@
      * @see AudioManager#setSupportedSystemUsages(int[])
      */
     public void setSupportedSystemUsages(@NonNull @AttributeSystemUsage int[] systemUsages) {
+        super.setSupportedSystemUsages_enforcePermission();
+
         verifySystemUsages(systemUsages);
 
         synchronized (mSupportedSystemUsagesLock) {
@@ -1872,6 +1899,8 @@
      * @see AudioManager#getSupportedSystemUsages()
      */
     public @NonNull @AttributeSystemUsage int[] getSupportedSystemUsages() {
+        super.getSupportedSystemUsages_enforcePermission();
+
         synchronized (mSupportedSystemUsagesLock) {
             return Arrays.copyOf(mSupportedSystemUsages, mSupportedSystemUsages.length);
         }
@@ -1893,6 +1922,8 @@
     @NonNull
     public List<AudioProductStrategy> getAudioProductStrategies() {
         // verify permissions
+        super.getAudioProductStrategies_enforcePermission();
+
         return AudioProductStrategy.getAudioProductStrategies();
     }
 
@@ -1904,6 +1935,8 @@
     @NonNull
     public List<AudioVolumeGroup> getAudioVolumeGroups() {
         // verify permissions
+        super.getAudioVolumeGroups_enforcePermission();
+
         return AudioVolumeGroup.getAudioVolumeGroups();
     }
 
@@ -2764,7 +2797,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);
@@ -2782,9 +2815,11 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#removePreferredDeviceForStrategy(AudioProductStrategy) */
     public int removePreferredDevicesForStrategy(int strategy) {
+        super.removePreferredDevicesForStrategy_enforcePermission();
+
         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) {
@@ -2799,6 +2834,8 @@
      * @see AudioManager#getPreferredDevicesForStrategy(AudioProductStrategy)
      */
     public List<AudioDeviceAttributes> getPreferredDevicesForStrategy(int strategy) {
+        super.getPreferredDevicesForStrategy_enforcePermission();
+
         List<AudioDeviceAttributes> devices = new ArrayList<>();
         final long identity = Binder.clearCallingIdentity();
         final int status = AudioSystem.getDevicesForRoleAndStrategy(
@@ -2850,7 +2887,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);
@@ -2869,9 +2906,11 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#clearPreferredDevicesForCapturePreset(int) */
     public int clearPreferredDevicesForCapturePreset(int capturePreset) {
+        super.clearPreferredDevicesForCapturePreset_enforcePermission();
+
         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) {
@@ -2885,6 +2924,8 @@
      * @see AudioManager#getPreferredDevicesForCapturePreset(int)
      */
     public List<AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int capturePreset) {
+        super.getPreferredDevicesForCapturePreset_enforcePermission();
+
         List<AudioDeviceAttributes> devices = new ArrayList<>();
         final long identity = Binder.clearCallingIdentity();
         final int status = AudioSystem.getDevicesForRoleAndCapturePreset(
@@ -3043,9 +3084,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 +3186,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,
@@ -3617,6 +3659,8 @@
     /** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
     public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
             String callingPackage, String attributionTag) {
+        super.setVolumeIndexForAttributes_enforcePermission();
+
         Objects.requireNonNull(attr, "attr must not be null");
         final int volumeGroup = getVolumeGroupIdForAttributes(attr);
         if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
@@ -3625,7 +3669,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);
@@ -3660,6 +3704,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#getVolumeIndexForAttributes(attr) */
     public int getVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        super.getVolumeIndexForAttributes_enforcePermission();
+
         Objects.requireNonNull(attr, "attr must not be null");
         final int volumeGroup = getVolumeGroupIdForAttributes(attr);
         if (sVolumeGroupStates.indexOfKey(volumeGroup) < 0) {
@@ -3672,6 +3718,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#getMaxVolumeIndexForAttributes(attr) */
     public int getMaxVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        super.getMaxVolumeIndexForAttributes_enforcePermission();
+
         Objects.requireNonNull(attr, "attr must not be null");
         return AudioSystem.getMaxVolumeIndexForAttributes(attr);
     }
@@ -3679,6 +3727,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#getMinVolumeIndexForAttributes(attr) */
     public int getMinVolumeIndexForAttributes(@NonNull AudioAttributes attr) {
+        super.getMinVolumeIndexForAttributes_enforcePermission();
+
         Objects.requireNonNull(attr, "attr must not be null");
         return AudioSystem.getMinVolumeIndexForAttributes(attr);
     }
@@ -3776,7 +3826,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());
@@ -3785,6 +3835,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_ULTRASOUND)
     /** @see AudioManager#isUltrasoundSupported() */
     public boolean isUltrasoundSupported() {
+        super.isUltrasoundSupported_enforcePermission();
+
         return AudioSystem.isUltrasoundSupported();
     }
 
@@ -3977,7 +4029,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,13 +4070,13 @@
         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);
         }
     }
 
-    private void setLeAudioVolumeOnModeUpdate(int mode, int streamType, int device) {
+    private void setLeAudioVolumeOnModeUpdate(int mode, int device) {
         switch (mode) {
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
@@ -4038,10 +4090,16 @@
                 return;
         }
 
-        // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
-        // (See AudioDeviceBroker#createBtDeviceInfo())
-        int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
-        int maxIndex = mStreamStates[streamType].getMaxIndex();
+        // Forcefully set LE audio volume as a workaround, since in some cases
+        // (like the outgoing call) the value of 'device' is not DEVICE_OUT_BLE_*
+        // even when BLE is connected.
+        if (!AudioSystem.isLeAudioDeviceType(device)) {
+            device = AudioSystem.DEVICE_OUT_BLE_HEADSET;
+        }
+
+        final int streamType = getBluetoothContextualVolumeStream(mode);
+        final int index = mStreamStates[streamType].getIndex(device);
+        final int maxIndex = mStreamStates[streamType].getMaxIndex();
 
         if (DEBUG_VOL) {
             Log.d(TAG, "setLeAudioVolumeOnModeUpdate postSetLeAudioVolumeIndex index="
@@ -4596,6 +4654,8 @@
     /** @see AudioManager#setMasterMute(boolean, int) */
     public void setMasterMute(boolean mute, int flags, String callingPackage, int userId,
             String attributionTag) {
+        super.setMasterMute_enforcePermission();
+
         setMasterMuteInternal(mute, flags, callingPackage,
                 Binder.getCallingUid(), userId, Binder.getCallingPid(), attributionTag);
     }
@@ -4640,6 +4700,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.QUERY_AUDIO_STATE)
     /** Get last audible volume before stream was muted. */
     public int getLastAudibleStreamVolume(int streamType) {
+        super.getLastAudibleStreamVolume_enforcePermission();
+
         ensureValidStreamType(streamType);
         int device = getDeviceForStream(streamType);
         return (mStreamStates[streamType].getIndex(device) + 5) / 10;
@@ -5413,7 +5475,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);
@@ -5427,9 +5489,7 @@
                 // change of mode may require volume to be re-applied on some devices
                 updateAbsVolumeMultiModeDevices(previousMode, mode);
 
-                // Forcefully set LE audio volume as a workaround, since the value of 'device'
-                // is not DEVICE_OUT_BLE_* even when BLE is connected.
-                setLeAudioVolumeOnModeUpdate(mode, streamType, device);
+                setLeAudioVolumeOnModeUpdate(mode, device);
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
@@ -5495,6 +5555,8 @@
     /** @see AudioManager#isPstnCallAudioInterceptable() */
     public boolean isPstnCallAudioInterceptable() {
 
+        super.isPstnCallAudioInterceptable_enforcePermission();
+
         boolean uplinkDeviceFound = false;
         boolean downlinkDeviceFound = false;
         AudioDeviceInfo[] devices = AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_ALL);
@@ -5558,7 +5620,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()));
@@ -6886,9 +6948,11 @@
             @AudioManager.DeviceVolumeBehavior int deviceVolumeBehavior, @Nullable String pkgName) {
         // verify permissions
         // verify arguments
+        super.setDeviceVolumeBehavior_enforcePermission();
+
         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)
@@ -6944,7 +7008,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
@@ -7041,6 +7105,8 @@
      */
     public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes,
             @ConnectionState int state, String caller) {
+        super.setWiredDeviceConnectionState_enforcePermission();
+
         if (state != CONNECTION_STATE_CONNECTED
                 && state != CONNECTION_STATE_DISCONNECTED) {
             throw new IllegalArgumentException("Invalid state " + state);
@@ -7590,7 +7656,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,
@@ -8009,7 +8075,7 @@
                 }
             }
             if (changed) {
-                sVolumeLogger.log(new VolumeEvent(
+                sVolumeLogger.enqueue(new VolumeEvent(
                         VolumeEvent.VOL_MUTE_STREAM_INT, mStreamType, state));
             }
             return changed;
@@ -8179,10 +8245,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);
@@ -8360,7 +8426,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);
                 }
@@ -8629,7 +8695,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);
@@ -8763,13 +8829,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 ||
@@ -9165,24 +9239,32 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#isAvailableForDevice(AudioDeviceAttributes) */
     public boolean isSpatializerAvailableForDevice(@NonNull AudioDeviceAttributes device)  {
+        super.isSpatializerAvailableForDevice_enforcePermission();
+
         return mSpatializerHelper.isAvailableForDevice(Objects.requireNonNull(device));
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#hasHeadTracker(AudioDeviceAttributes) */
     public boolean hasHeadTracker(@NonNull AudioDeviceAttributes device) {
+        super.hasHeadTracker_enforcePermission();
+
         return mSpatializerHelper.hasHeadTracker(Objects.requireNonNull(device));
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setHeadTrackerEnabled(boolean, AudioDeviceAttributes) */
     public void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes device) {
+        super.setHeadTrackerEnabled_enforcePermission();
+
         mSpatializerHelper.setHeadTrackerEnabled(enabled, Objects.requireNonNull(device));
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#isHeadTrackerEnabled(AudioDeviceAttributes) */
     public boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes device) {
+        super.isHeadTrackerEnabled_enforcePermission();
+
         return mSpatializerHelper.isHeadTrackerEnabled(Objects.requireNonNull(device));
     }
 
@@ -9194,6 +9276,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setSpatializerEnabled(boolean) */
     public void setSpatializerEnabled(boolean enabled) {
+        super.setSpatializerEnabled_enforcePermission();
+
         mSpatializerHelper.setFeatureEnabled(enabled);
     }
 
@@ -9223,6 +9307,8 @@
     /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
     public void registerSpatializerHeadTrackingCallback(
             @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        super.registerSpatializerHeadTrackingCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.registerHeadTrackingModeCallback(cb);
     }
@@ -9231,6 +9317,8 @@
     /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */
     public void unregisterSpatializerHeadTrackingCallback(
             @NonNull ISpatializerHeadTrackingModeCallback cb) {
+        super.unregisterSpatializerHeadTrackingCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.unregisterHeadTrackingModeCallback(cb);
     }
@@ -9246,6 +9334,8 @@
     /** @see Spatializer#setOnHeadToSoundstagePoseUpdatedListener */
     public void registerHeadToSoundstagePoseCallback(
             @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        super.registerHeadToSoundstagePoseCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.registerHeadToSoundstagePoseCallback(cb);
     }
@@ -9254,6 +9344,8 @@
     /** @see Spatializer#clearOnHeadToSoundstagePoseUpdatedListener */
     public void unregisterHeadToSoundstagePoseCallback(
             @NonNull ISpatializerHeadToSoundStagePoseCallback cb) {
+        super.unregisterHeadToSoundstagePoseCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.unregisterHeadToSoundstagePoseCallback(cb);
     }
@@ -9261,12 +9353,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getSpatializerCompatibleAudioDevices() */
     public @NonNull List<AudioDeviceAttributes> getSpatializerCompatibleAudioDevices() {
+        super.getSpatializerCompatibleAudioDevices_enforcePermission();
+
         return mSpatializerHelper.getCompatibleAudioDevices();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#addSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */
     public void addSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        super.addSpatializerCompatibleAudioDevice_enforcePermission();
+
         Objects.requireNonNull(ada);
         mSpatializerHelper.addCompatibleAudioDevice(ada);
     }
@@ -9274,6 +9370,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#removeSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */
     public void removeSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
+        super.removeSpatializerCompatibleAudioDevice_enforcePermission();
+
         Objects.requireNonNull(ada);
         mSpatializerHelper.removeCompatibleAudioDevice(ada);
     }
@@ -9281,24 +9379,32 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getSupportedHeadTrackingModes() */
     public int[] getSupportedHeadTrackingModes() {
+        super.getSupportedHeadTrackingModes_enforcePermission();
+
         return mSpatializerHelper.getSupportedHeadTrackingModes();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getHeadTrackingMode() */
     public int getActualHeadTrackingMode() {
+        super.getActualHeadTrackingMode_enforcePermission();
+
         return mSpatializerHelper.getActualHeadTrackingMode();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getDesiredHeadTrackingMode() */
     public int getDesiredHeadTrackingMode() {
+        super.getDesiredHeadTrackingMode_enforcePermission();
+
         return mSpatializerHelper.getDesiredHeadTrackingMode();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setGlobalTransform */
     public void setSpatializerGlobalTransform(@NonNull float[] transform) {
+        super.setSpatializerGlobalTransform_enforcePermission();
+
         Objects.requireNonNull(transform);
         mSpatializerHelper.setGlobalTransform(transform);
     }
@@ -9306,12 +9412,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#recenterHeadTracker() */
     public void recenterHeadTracker() {
+        super.recenterHeadTracker_enforcePermission();
+
         mSpatializerHelper.recenterHeadTracker();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setDesiredHeadTrackingMode */
     public void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
+        super.setDesiredHeadTrackingMode_enforcePermission();
+
         switch(mode) {
             case Spatializer.HEAD_TRACKING_MODE_DISABLED:
             case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
@@ -9326,6 +9436,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setEffectParameter */
     public void setSpatializerParameter(int key, @NonNull byte[] value) {
+        super.setSpatializerParameter_enforcePermission();
+
         Objects.requireNonNull(value);
         mSpatializerHelper.setEffectParameter(key, value);
     }
@@ -9333,6 +9445,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getEffectParameter */
     public void getSpatializerParameter(int key, @NonNull byte[] value) {
+        super.getSpatializerParameter_enforcePermission();
+
         Objects.requireNonNull(value);
         mSpatializerHelper.getEffectParameter(key, value);
     }
@@ -9340,12 +9454,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#getOutput */
     public int getSpatializerOutput() {
+        super.getSpatializerOutput_enforcePermission();
+
         return mSpatializerHelper.getOutput();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#setOnSpatializerOutputChangedListener */
     public void registerSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        super.registerSpatializerOutputCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.registerSpatializerOutputCallback(cb);
     }
@@ -9353,6 +9471,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
     /** @see Spatializer#clearOnSpatializerOutputChangedListener */
     public void unregisterSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        super.unregisterSpatializerOutputCallback_enforcePermission();
+
         Objects.requireNonNull(cb);
         mSpatializerHelper.unregisterSpatializerOutputCallback(cb);
     }
@@ -9470,6 +9590,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioManager#getMutingExpectedDevice */
     public @Nullable AudioDeviceAttributes getMutingExpectedDevice() {
+        super.getMutingExpectedDevice_enforcePermission();
+
         synchronized (mMuteAwaitConnectionLock) {
             return mMutingExpectedDevice;
         }
@@ -9511,6 +9633,8 @@
     /** @see AudioManager#registerMuteAwaitConnectionCallback */
     public void registerMuteAwaitConnectionDispatcher(@NonNull IMuteAwaitConnectionCallback cb,
             boolean register) {
+        super.registerMuteAwaitConnectionDispatcher_enforcePermission();
+
         if (register) {
             mMuteAwaitConnectionDispatchers.register(cb);
         } else {
@@ -10664,7 +10788,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));
@@ -10862,7 +10986,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());
@@ -11036,6 +11160,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     /** @see AudioPolicy#getFocusStack() */
     public List<AudioFocusInfo> getFocusStack() {
+        super.getFocusStack_enforcePermission();
+
         return mMediaFocusControl.getFocusStack();
     }
 
@@ -11390,7 +11516,7 @@
         }
 
         public void binderDied() {
-            mDynPolicyLogger.log((new EventLogger.StringEvent("AudioPolicy "
+            mDynPolicyLogger.enqueue((new EventLogger.StringEvent("AudioPolicy "
                     + mPolicyCallback.asBinder() + " died").printLog(TAG)));
             release();
         }
@@ -11813,6 +11939,8 @@
     // Multi Audio Focus
     //======================
     public void setMultiAudioFocusEnabled(boolean enabled) {
+        super.setMultiAudioFocusEnabled_enforcePermission();
+
         if (mMediaFocusControl != null) {
             boolean mafEnabled = mMediaFocusControl.getMultiAudioFocusEnabled();
             if (mafEnabled != enabled) {
@@ -11915,6 +12043,8 @@
     /** @see AudioManager#addAssistantServicesUids(int []) */
     @Override
     public void addAssistantServicesUids(int [] assistantUids) {
+        super.addAssistantServicesUids_enforcePermission();
+
         Objects.requireNonNull(assistantUids);
 
         synchronized (mSettingsLock) {
@@ -11926,6 +12056,8 @@
     /** @see AudioManager#removeAssistantServicesUids(int []) */
     @Override
     public void removeAssistantServicesUids(int [] assistantUids) {
+        super.removeAssistantServicesUids_enforcePermission();
+
         Objects.requireNonNull(assistantUids);
         synchronized (mSettingsLock) {
             removeAssistantServiceUidsLocked(assistantUids);
@@ -11936,6 +12068,8 @@
     /** @see AudioManager#getAssistantServicesUids() */
     @Override
     public int[] getAssistantServicesUids() {
+        super.getAssistantServicesUids_enforcePermission();
+
         int [] assistantUids;
         synchronized (mSettingsLock) {
             assistantUids = mAssistantUids.stream().mapToInt(Integer::intValue).toArray();
@@ -11947,6 +12081,8 @@
     /** @see AudioManager#setActiveAssistantServiceUids(int []) */
     @Override
     public void setActiveAssistantServiceUids(int [] activeAssistantUids) {
+        super.setActiveAssistantServiceUids_enforcePermission();
+
         Objects.requireNonNull(activeAssistantUids);
         synchronized (mSettingsLock) {
             mActiveAssistantServiceUids = activeAssistantUids;
@@ -11958,6 +12094,8 @@
     /** @see AudioManager#getActiveAssistantServiceUids() */
     @Override
     public int[] getActiveAssistantServiceUids() {
+        super.getActiveAssistantServiceUids_enforcePermission();
+
         int [] activeAssistantUids;
         synchronized (mSettingsLock) {
             activeAssistantUids = mActiveAssistantServiceUids.clone();
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..0bc4b20 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -17,12 +17,12 @@
 package com.android.server.audio;
 
 import static android.media.AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_MUTE;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_CLIENT_VOLUME;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_MASTER;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_PLAYBACK_RESTRICTED;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_STREAM_MUTED;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_STREAM_VOLUME;
-import static android.media.AudioPlaybackConfiguration.PLAYER_MUTE_VOLUME_SHAPER;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_APP_OPS;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_CLIENT_VOLUME;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_MASTER;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_STREAM_MUTED;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_STREAM_VOLUME;
+import static android.media.AudioPlaybackConfiguration.MUTED_BY_VOLUME_SHAPER;
 import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
 import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_MUTED;
 
@@ -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,
@@ -1155,22 +1155,22 @@
                     if (mEventValue <= 0) {
                         builder.append("none ");
                     } else {
-                        if ((mEventValue & PLAYER_MUTE_MASTER) != 0) {
+                        if ((mEventValue & MUTED_BY_MASTER) != 0) {
                             builder.append("masterMute ");
                         }
-                        if ((mEventValue & PLAYER_MUTE_STREAM_VOLUME) != 0) {
+                        if ((mEventValue & MUTED_BY_STREAM_VOLUME) != 0) {
                             builder.append("streamVolume ");
                         }
-                        if ((mEventValue & PLAYER_MUTE_STREAM_MUTED) != 0) {
+                        if ((mEventValue & MUTED_BY_STREAM_MUTED) != 0) {
                             builder.append("streamMute ");
                         }
-                        if ((mEventValue & PLAYER_MUTE_PLAYBACK_RESTRICTED) != 0) {
-                            builder.append("playbackRestricted ");
+                        if ((mEventValue & MUTED_BY_APP_OPS) != 0) {
+                            builder.append("appOps ");
                         }
-                        if ((mEventValue & PLAYER_MUTE_CLIENT_VOLUME) != 0) {
+                        if ((mEventValue & MUTED_BY_CLIENT_VOLUME) != 0) {
                             builder.append("clientVolume ");
                         }
-                        if ((mEventValue & PLAYER_MUTE_VOLUME_SHAPER) != 0) {
+                        if ((mEventValue & MUTED_BY_VOLUME_SHAPER) != 0) {
                             builder.append("volumeShaper ");
                         }
                     }
@@ -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/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index d2016c47..acfc2a7 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -178,6 +178,8 @@
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) throws RemoteException {
 
+            super.createTestSession_enforcePermission();
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 return mInjector.getBiometricService()
@@ -192,6 +194,8 @@
         public List<SensorPropertiesInternal> getSensorProperties(String opPackageName)
                 throws RemoteException {
 
+            super.getSensorProperties_enforcePermission();
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 // Get the result from BiometricService, since it is the source of truth for all
@@ -206,6 +210,8 @@
         @Override
         public String getUiPackage() {
 
+            super.getUiPackage_enforcePermission();
+
             return getContext().getResources()
                     .getString(R.string.config_biometric_prompt_ui_package);
         }
@@ -404,6 +410,17 @@
         }
 
         @Override
+        public void resetLockout(int userId, byte[] hardwareAuthToken) throws RemoteException {
+            checkInternalPermission();
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mBiometricService.resetLockout(userId, hardwareAuthToken);
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+
+        @Override
         public CharSequence getButtonLabel(
                 int userId,
                 String opPackageName,
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index c29755a..d971953 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -72,6 +72,7 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.DumpUtils;
 import com.android.server.SystemService;
+import com.android.server.biometrics.log.BiometricContext;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -100,6 +101,7 @@
     private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks;
     private final Random mRandom = new Random();
     @NonNull private final Supplier<Long> mRequestCounter;
+    @NonNull private final BiometricContext mBiometricContext;
 
     @VisibleForTesting
     IStatusBarService mStatusBarService;
@@ -495,6 +497,8 @@
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) throws RemoteException {
 
+            super.createTestSession_enforcePermission();
+
             for (BiometricSensor sensor : mSensors) {
                 if (sensor.id == sensorId) {
                     return sensor.impl.createTestSession(callback, opPackageName);
@@ -510,6 +514,8 @@
         public List<SensorPropertiesInternal> getSensorProperties(String opPackageName)
                 throws RemoteException {
 
+            super.getSensorProperties_enforcePermission();
+
             final List<SensorPropertiesInternal> sensors = new ArrayList<>();
             for (BiometricSensor sensor : mSensors) {
                 // Explicitly re-create as the super class, since AIDL doesn't play nicely with
@@ -526,6 +532,8 @@
         @Override // Binder call
         public void onReadyForAuthentication(long requestId, int cookie) {
 
+            super.onReadyForAuthentication_enforcePermission();
+
             mHandler.post(() -> handleOnReadyForAuthentication(requestId, cookie));
         }
 
@@ -534,6 +542,8 @@
         public long authenticate(IBinder token, long operationId, int userId,
                 IBiometricServiceReceiver receiver, String opPackageName, PromptInfo promptInfo) {
 
+            super.authenticate_enforcePermission();
+
             if (token == null || receiver == null || opPackageName == null || promptInfo == null) {
                 Slog.e(TAG, "Unable to authenticate, one or more null arguments");
                 return -1;
@@ -564,6 +574,8 @@
         @Override // Binder call
         public void cancelAuthentication(IBinder token, String opPackageName, long requestId) {
 
+            super.cancelAuthentication_enforcePermission();
+
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = token;
             args.arg2 = opPackageName;
@@ -577,6 +589,8 @@
         public int canAuthenticate(String opPackageName, int userId, int callingUserId,
                 @Authenticators.Types int authenticators) {
 
+            super.canAuthenticate_enforcePermission();
+
             Slog.d(TAG, "canAuthenticate: User=" + userId
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
@@ -599,6 +613,8 @@
         @Override
         public boolean hasEnrolledBiometrics(int userId, String opPackageName) {
 
+            super.hasEnrolledBiometrics_enforcePermission();
+
             try {
                 for (BiometricSensor sensor : mSensors) {
                     if (sensor.impl.hasEnrolledTemplates(userId, opPackageName)) {
@@ -618,6 +634,8 @@
                 @Authenticators.Types int strength,
                 @NonNull IBiometricAuthenticator authenticator) {
 
+            super.registerAuthenticator_enforcePermission();
+
             Slog.d(TAG, "Registering ID: " + id
                     + " Modality: " + modality
                     + " Strength: " + strength);
@@ -664,6 +682,8 @@
         public void registerEnabledOnKeyguardCallback(
                 IBiometricEnabledOnKeyguardCallback callback, int callingUserId) {
 
+            super.registerEnabledOnKeyguardCallback_enforcePermission();
+
             mEnabledOnKeyguardCallbacks.add(new EnabledOnKeyguardCallback(callback));
             try {
                 callback.onChanged(mSettingObserver.getEnabledOnKeyguard(callingUserId),
@@ -678,6 +698,8 @@
         public void invalidateAuthenticatorIds(int userId, int fromSensorId,
                 IInvalidationCallback callback) {
 
+            super.invalidateAuthenticatorIds_enforcePermission();
+
             InvalidationTracker.start(getContext(), mSensors, userId, fromSensorId, callback);
         }
 
@@ -685,6 +707,8 @@
         @Override // Binder call
         public long[] getAuthenticatorIds(int callingUserId) {
 
+            super.getAuthenticatorIds_enforcePermission();
+
             final List<Long> authenticatorIds = new ArrayList<>();
             for (BiometricSensor sensor : mSensors) {
                 try {
@@ -717,6 +741,8 @@
                 int userId, byte[] hardwareAuthToken) {
 
             // Check originating strength
+            super.resetLockoutTimeBound_enforcePermission();
+
             if (!Utils.isAtLeastStrength(getSensorForId(fromSensorId).getCurrentStrength(),
                     Authenticators.BIOMETRIC_STRONG)) {
                 Slog.w(TAG, "Sensor: " + fromSensorId + " is does not meet the required strength to"
@@ -752,8 +778,22 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
+        public void resetLockout(
+                int userId, byte[] hardwareAuthToken) {
+            super.resetLockout_enforcePermission();
+
+            Slog.d(TAG, "resetLockout(userId=" + userId
+                    + ", hat=" + (hardwareAuthToken == null ? "null " : "present") + ")");
+            mBiometricContext.getAuthSessionCoordinator()
+                    .resetLockoutFor(userId, Authenticators.BIOMETRIC_STRONG, -1);
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override // Binder call
         public int getCurrentStrength(int sensorId) {
 
+            super.getCurrentStrength_enforcePermission();
+
             for (BiometricSensor sensor : mSensors) {
                 if (sensor.id == sensorId) {
                     return sensor.getCurrentStrength();
@@ -772,6 +812,8 @@
                 @Authenticators.Types int authenticators) {
 
 
+            super.getCurrentModality_enforcePermission();
+
             Slog.d(TAG, "getCurrentModality: User=" + userId
                     + ", Caller=" + callingUserId
                     + ", Authenticators=" + authenticators);
@@ -794,6 +836,8 @@
         @Override // Binder call
         public int getSupportedModalities(@Authenticators.Types int authenticators) {
 
+            super.getSupportedModalities_enforcePermission();
+
             Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators);
 
             if (!Utils.isValidAuthenticatorConfig(authenticators)) {
@@ -954,6 +998,10 @@
             final AtomicLong generator = new AtomicLong(0);
             return () -> generator.incrementAndGet();
         }
+
+        public BiometricContext getBiometricContext(Context context) {
+            return BiometricContext.getInstance(context);
+        }
     }
 
     /**
@@ -980,6 +1028,7 @@
         mSettingObserver = mInjector.getSettingObserver(context, mHandler,
                 mEnabledOnKeyguardCallbacks);
         mRequestCounter = mInjector.getRequestGenerator();
+        mBiometricContext = injector.getBiometricContext(context);
 
         try {
             injector.getActivityManagerService().registerUserSwitchObserver(
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index aec98f0..3813fd1 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -48,8 +48,6 @@
  * the PreAuthInfo should not change any sensor state.
  */
 class PreAuthInfo {
-    private static final String TAG = "BiometricService/PreAuthInfo";
-
     static final int AUTHENTICATOR_OK = 1;
     static final int BIOMETRIC_NO_HARDWARE = 2;
     static final int BIOMETRIC_DISABLED_BY_DEVICE_POLICY = 3;
@@ -62,24 +60,7 @@
     static final int BIOMETRIC_LOCKOUT_TIMED = 10;
     static final int BIOMETRIC_LOCKOUT_PERMANENT = 11;
     static final int BIOMETRIC_SENSOR_PRIVACY_ENABLED = 12;
-    @IntDef({AUTHENTICATOR_OK,
-            BIOMETRIC_NO_HARDWARE,
-            BIOMETRIC_DISABLED_BY_DEVICE_POLICY,
-            BIOMETRIC_INSUFFICIENT_STRENGTH,
-            BIOMETRIC_INSUFFICIENT_STRENGTH_AFTER_DOWNGRADE,
-            BIOMETRIC_HARDWARE_NOT_DETECTED,
-            BIOMETRIC_NOT_ENROLLED,
-            BIOMETRIC_NOT_ENABLED_FOR_APPS,
-            CREDENTIAL_NOT_ENROLLED,
-            BIOMETRIC_LOCKOUT_TIMED,
-            BIOMETRIC_LOCKOUT_PERMANENT,
-            BIOMETRIC_SENSOR_PRIVACY_ENABLED})
-    @Retention(RetentionPolicy.SOURCE)
-    @interface AuthenticatorStatus {}
-
-    private final boolean mBiometricRequested;
-    private final int mBiometricStrengthRequested;
-
+    private static final String TAG = "BiometricService/PreAuthInfo";
     final boolean credentialRequested;
     // Sensors that can be used for this request (e.g. strong enough, enrolled, enabled).
     final List<BiometricSensor> eligibleSensors;
@@ -90,6 +71,25 @@
     final boolean ignoreEnrollmentState;
     final int userId;
     final Context context;
+    private final boolean mBiometricRequested;
+    private final int mBiometricStrengthRequested;
+    private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
+            boolean credentialRequested, List<BiometricSensor> eligibleSensors,
+            List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
+            boolean confirmationRequested, boolean ignoreEnrollmentState, int userId,
+            Context context) {
+        mBiometricRequested = biometricRequested;
+        mBiometricStrengthRequested = biometricStrengthRequested;
+        this.credentialRequested = credentialRequested;
+
+        this.eligibleSensors = eligibleSensors;
+        this.ineligibleSensors = ineligibleSensors;
+        this.credentialAvailable = credentialAvailable;
+        this.confirmationRequested = confirmationRequested;
+        this.ignoreEnrollmentState = ignoreEnrollmentState;
+        this.userId = userId;
+        this.context = context;
+    }
 
     static PreAuthInfo create(ITrustManager trustManager,
             DevicePolicyManager devicePolicyManager,
@@ -158,7 +158,8 @@
      *
      * @return @AuthenticatorStatus
      */
-    private static @AuthenticatorStatus int getStatusForBiometricAuthenticator(
+    private static @AuthenticatorStatus
+    int getStatusForBiometricAuthenticator(
             DevicePolicyManager devicePolicyManager,
             BiometricService.SettingObserver settingObserver,
             BiometricSensor sensor, int userId, String opPackageName,
@@ -200,7 +201,6 @@
                 }
             }
 
-
             final @LockoutTracker.LockoutMode int lockoutMode =
                     sensor.impl.getLockoutModeForUser(userId);
             if (lockoutMode == LockoutTracker.LOCKOUT_TIMED) {
@@ -248,8 +248,8 @@
 
     /**
      * @param modality one of {@link BiometricAuthenticator#TYPE_FINGERPRINT},
-     * {@link BiometricAuthenticator#TYPE_IRIS} or {@link BiometricAuthenticator#TYPE_FACE}
-     * @return
+     *                 {@link BiometricAuthenticator#TYPE_IRIS} or
+     *                 {@link BiometricAuthenticator#TYPE_FACE}
      */
     private static int mapModalityToDevicePolicyType(int modality) {
         switch (modality) {
@@ -265,24 +265,6 @@
         }
     }
 
-    private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
-            boolean credentialRequested, List<BiometricSensor> eligibleSensors,
-            List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
-            boolean confirmationRequested, boolean ignoreEnrollmentState, int userId,
-            Context context) {
-        mBiometricRequested = biometricRequested;
-        mBiometricStrengthRequested = biometricStrengthRequested;
-        this.credentialRequested = credentialRequested;
-
-        this.eligibleSensors = eligibleSensors;
-        this.ineligibleSensors = ineligibleSensors;
-        this.credentialAvailable = credentialAvailable;
-        this.confirmationRequested = confirmationRequested;
-        this.ignoreEnrollmentState = ignoreEnrollmentState;
-        this.userId = userId;
-        this.context = context;
-    }
-
     private Pair<BiometricSensor, Integer> calculateErrorByPriority() {
         // If the caller requested STRONG, and the device contains both STRONG and non-STRONG
         // sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's
@@ -303,6 +285,7 @@
      * surface, combined with the actual sensor/credential and user/system settings, calculate the
      * internal {@link AuthenticatorStatus} that should be returned to the client. Note that this
      * will need to be converted into the public API constant.
+     *
      * @return Pair<Modality, Error> with error being the internal {@link AuthenticatorStatus} code
      */
     private Pair<Integer, Integer> getInternalStatus() {
@@ -391,7 +374,8 @@
     /**
      * @return public BiometricManager result for the current request.
      */
-    @BiometricManager.BiometricError int getCanAuthenticateResult() {
+    @BiometricManager.BiometricError
+    int getCanAuthenticateResult() {
         // TODO: Convert this directly
         return Utils.biometricConstantsToBiometricManager(
                 Utils.authenticatorStatusToBiometricConstant(
@@ -401,6 +385,7 @@
     /**
      * For the given request, generate the appropriate reason why authentication cannot be started.
      * Note that for some errors, modality is intentionally cleared.
+     *
      * @return Pair<Modality, Error> with modality being filtered if necessary, and error
      * being one of the public {@link android.hardware.biometrics.BiometricConstants} codes.
      */
@@ -443,7 +428,8 @@
      * @return bitmask representing the modalities that are running or could be running for the
      * current session.
      */
-    @BiometricAuthenticator.Modality int getEligibleModalities() {
+    @BiometricAuthenticator.Modality
+    int getEligibleModalities() {
         @BiometricAuthenticator.Modality int modalities = 0;
         for (BiometricSensor sensor : eligibleSensors) {
             modalities |= sensor.modality;
@@ -474,7 +460,7 @@
                         + ", StrengthRequested: " + mBiometricStrengthRequested
                         + ", CredentialRequested: " + credentialRequested);
         string.append(", Eligible:{");
-        for (BiometricSensor sensor: eligibleSensors) {
+        for (BiometricSensor sensor : eligibleSensors) {
             string.append(sensor.id).append(" ");
         }
         string.append("}");
@@ -489,4 +475,20 @@
         string.append(", ");
         return string.toString();
     }
+
+    @IntDef({AUTHENTICATOR_OK,
+            BIOMETRIC_NO_HARDWARE,
+            BIOMETRIC_DISABLED_BY_DEVICE_POLICY,
+            BIOMETRIC_INSUFFICIENT_STRENGTH,
+            BIOMETRIC_INSUFFICIENT_STRENGTH_AFTER_DOWNGRADE,
+            BIOMETRIC_HARDWARE_NOT_DETECTED,
+            BIOMETRIC_NOT_ENROLLED,
+            BIOMETRIC_NOT_ENABLED_FOR_APPS,
+            CREDENTIAL_NOT_ENROLLED,
+            BIOMETRIC_LOCKOUT_TIMED,
+            BIOMETRIC_LOCKOUT_PERMANENT,
+            BIOMETRIC_SENSOR_PRIVACY_ENABLED})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface AuthenticatorStatus {
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/TEST_MAPPING b/services/core/java/com/android/server/biometrics/TEST_MAPPING
index 36acc3c..8b80674 100644
--- a/services/core/java/com/android/server/biometrics/TEST_MAPPING
+++ b/services/core/java/com/android/server/biometrics/TEST_MAPPING
@@ -2,6 +2,9 @@
     "presubmit": [
         {
             "name": "CtsBiometricsTestCases"
+        },
+        {
+            "name": "CtsBiometricsHostTestCases"
         }
     ]
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index 1a5f31c..d584c99 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -52,16 +52,13 @@
     private boolean mDestroyed = false;
     private boolean mDestroyRequested = false;
     private boolean mDisableRequested = false;
-    private volatile NextConsumer mNextConsumer = null;
+    private NextConsumer mNextConsumer = null;
     private volatile float mLastAmbientLux = -1;
 
     private final SensorEventListener mLightSensorListener = new SensorEventListener() {
         @Override
         public void onSensorChanged(SensorEvent event) {
-            mLastAmbientLux = event.values[0];
-            if (mNextConsumer != null) {
-                completeNextConsumer(mLastAmbientLux);
-            }
+            onNext(event.values[0]);
         }
 
         @Override
@@ -123,7 +120,7 @@
 
         // if a final consumer is set it will call destroy/disable on the next value if requested
         if (!mDestroyed && mNextConsumer == null) {
-            disableLightSensorLoggingLocked();
+            disableLightSensorLoggingLocked(false /* destroying */);
         }
     }
 
@@ -133,11 +130,29 @@
 
         // if a final consumer is set it will call destroy/disable on the next value if requested
         if (!mDestroyed && mNextConsumer == null) {
-            disable();
+            disableLightSensorLoggingLocked(true /* destroying */);
             mDestroyed = true;
         }
     }
 
+    private synchronized void onNext(float value) {
+        mLastAmbientLux = value;
+
+        final NextConsumer consumer = mNextConsumer;
+        mNextConsumer = null;
+        if (consumer != null) {
+            Slog.v(TAG, "Finishing next consumer");
+
+            if (mDestroyRequested) {
+                destroy();
+            } else if (mDisableRequested) {
+                disable();
+            }
+
+            consumer.consume(value);
+        }
+    }
+
     /** The most recent lux reading. */
     public float getMostRecentLux() {
         return mLastAmbientLux;
@@ -160,35 +175,17 @@
             @Nullable Handler handler) {
         final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
         final float current = mLastAmbientLux;
-        if (current > 0) {
+        if (current > -1f) {
             nextConsumer.consume(current);
-        } else if (mDestroyed) {
-            nextConsumer.consume(-1f);
         } else if (mNextConsumer != null) {
             mNextConsumer.add(nextConsumer);
         } else {
+            mDestroyed = false;
             mNextConsumer = nextConsumer;
             enableLightSensorLoggingLocked();
         }
     }
 
-    private synchronized void completeNextConsumer(float value) {
-        Slog.v(TAG, "Finishing next consumer");
-
-        final NextConsumer consumer = mNextConsumer;
-        mNextConsumer = null;
-
-        if (mDestroyRequested) {
-            destroy();
-        } else if (mDisableRequested) {
-            disable();
-        }
-
-        if (consumer != null) {
-            consumer.consume(value);
-        }
-    }
-
     private void enableLightSensorLoggingLocked() {
         if (!mEnabled) {
             mEnabled = true;
@@ -201,12 +198,14 @@
         resetTimerLocked(true /* start */);
     }
 
-    private void disableLightSensorLoggingLocked() {
+    private void disableLightSensorLoggingLocked(boolean destroying) {
         resetTimerLocked(false /* start */);
 
         if (mEnabled) {
             mEnabled = false;
-            mLastAmbientLux = -1;
+            if (!destroying) {
+                mLastAmbientLux = -1;
+            }
             mSensorManager.unregisterListener(mLightSensorListener);
             Slog.v(TAG, "Disable ALS: " + mLightSensorListener.hashCode());
         }
@@ -219,9 +218,13 @@
         }
     }
 
-    private void onTimeout() {
+    private synchronized void onTimeout() {
         Slog.e(TAG, "Max time exceeded for ALS logger - disabling: "
                 + mLightSensorListener.hashCode());
+
+        // if consumers are waiting but there was no sensor change, complete them with the latest
+        // value before disabling
+        onNext(mLastAmbientLux);
         disable();
     }
 
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
index 23b2714..d456736 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java
@@ -44,7 +44,7 @@
 /**
  * A default provider for {@link BiometricContext}.
  */
-final class BiometricContextProvider implements BiometricContext {
+public final class BiometricContextProvider implements BiometricContext {
 
     private static final String TAG = "BiometricContextProvider";
 
@@ -83,7 +83,8 @@
     private boolean mIsAwake = false;
 
     @VisibleForTesting
-    BiometricContextProvider(@NonNull AmbientDisplayConfiguration ambientDisplayConfiguration,
+    public BiometricContextProvider(
+            @NonNull AmbientDisplayConfiguration ambientDisplayConfiguration,
             @NonNull IStatusBarService service, @Nullable Handler handler,
             AuthSessionCoordinator authSessionCoordinator) {
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
index 1d90954..055c63d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java
@@ -38,8 +38,7 @@
  * Abstract {@link HalClientMonitor} subclass that operations eligible/interested in acquisition
  * messages should extend.
  */
-public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implements Interruptable,
-        ErrorConsumer {
+public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implements ErrorConsumer {
 
     private static final String TAG = "Biometrics/AcquisitionClient";
 
@@ -217,4 +216,9 @@
                     HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
         }
     }
+
+    @Override
+    public boolean isInterruptable() {
+        return true;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
index bdae5f3..a48a9d1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java
@@ -75,7 +75,10 @@
      * Adds auth success for a given strength to the current operation list.
      */
     void authenticatedFor(@Authenticators.Types int strength) {
-        updateState(strength, (old) -> AUTHENTICATOR_UNLOCKED | old);
+        // Only strong unlocks matter.
+        if (strength == Authenticators.BIOMETRIC_STRONG) {
+            updateState(strength, (old) -> AUTHENTICATOR_UNLOCKED | old);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
index dec1b55..1aee5d4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -54,7 +54,7 @@
     private AuthResultCoordinator mAuthResultCoordinator;
 
     public AuthSessionCoordinator() {
-        this(SystemClock.currentNetworkTimeClock());
+        this(SystemClock.elapsedRealtimeClock());
     }
 
     @VisibleForTesting
@@ -75,6 +75,7 @@
         mUserId = userId;
         mIsAuthenticating = true;
         mAuthOperations.clear();
+        mTimedLockouts.clear();
         mAuthResultCoordinator = new AuthResultCoordinator();
         mRingBuffer.addApiCall("internal : onAuthSessionStarted(" + userId + ")");
     }
@@ -88,7 +89,6 @@
      */
     void endAuthSession() {
         if (mIsAuthenticating) {
-            mAuthOperations.clear();
             final long currentTime = mClock.millis();
             for (Pair<Integer, Long> timedLockouts : mTimedLockouts) {
                 mMultiBiometricLockoutState.increaseLockoutTime(mUserId, timedLockouts.first,
@@ -109,16 +109,24 @@
                 }
 
             }
+
             mRingBuffer.addApiCall("internal : onAuthSessionEnded(" + mUserId + ")");
-            mIsAuthenticating = false;
+            clearSession();
         }
     }
 
+    private void clearSession() {
+        mIsAuthenticating = false;
+        mTimedLockouts.clear();
+        mAuthOperations.clear();
+    }
+
     /**
-     * @return true if a user can authenticate with a given strength.
+     * Returns the current lockout state for a given user/strength.
      */
-    public boolean getCanAuthFor(int userId, @Authenticators.Types int strength) {
-        return mMultiBiometricLockoutState.canUserAuthenticate(userId, strength);
+    @LockoutTracker.LockoutMode
+    public int getLockoutStateFor(int userId, @Authenticators.Types int strength) {
+        return mMultiBiometricLockoutState.getLockoutState(userId, strength);
     }
 
     @Override
@@ -145,19 +153,8 @@
     }
 
     @Override
-    public void authenticatedFor(int userId, @Authenticators.Types int biometricStrength,
-            int sensorId, long requestId) {
-        final String authStr =
-                "authenticatedFor(userId=" + userId + ", strength=" + biometricStrength
-                        + " , sensorId=" + sensorId + ", requestId= " + requestId + ")";
-        mRingBuffer.addApiCall(authStr);
-        mAuthResultCoordinator.authenticatedFor(biometricStrength);
-        attemptToFinish(userId, sensorId, authStr);
-    }
-
-    @Override
-    public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength,
-            int sensorId, long requestId) {
+    public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
+            long requestId) {
         final String lockedOutStr =
                 "lockOutFor(userId=" + userId + ", biometricStrength=" + biometricStrength
                         + ", sensorId=" + sensorId + ", requestId=" + requestId + ")";
@@ -179,12 +176,16 @@
     }
 
     @Override
-    public void authEndedFor(int userId, @Authenticators.Types int biometricStrength,
-            int sensorId, long requestId) {
+    public void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
+            long requestId, boolean wasSuccessful) {
         final String authEndedStr =
                 "authEndedFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
-                        + ", sensorId=" + sensorId + ", requestId=" + requestId + ")";
+                        + ", sensorId=" + sensorId + ", requestId=" + requestId + ", wasSuccessful="
+                        + wasSuccessful + ")";
         mRingBuffer.addApiCall(authEndedStr);
+        if (wasSuccessful) {
+            mAuthResultCoordinator.authenticatedFor(biometricStrength);
+        }
         attemptToFinish(userId, sensorId, authEndedStr);
     }
 
@@ -195,6 +196,12 @@
                 "resetLockoutFor(userId=" + userId + " ,biometricStrength=" + biometricStrength
                         + ", requestId=" + requestId + ")";
         mRingBuffer.addApiCall(resetLockStr);
+        if (biometricStrength == Authenticators.BIOMETRIC_STRONG) {
+            clearSession();
+        } else {
+            // Lockouts cannot be reset by non-strong biometrics
+            return;
+        }
         mMultiBiometricLockoutState.setAuthenticatorTo(userId, biometricStrength,
                 true /*canAuthenticate */);
         mMultiBiometricLockoutState.clearLockoutTime(userId, biometricStrength);
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
index d97f793..6bddf14 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java
@@ -28,16 +28,10 @@
     void authStartedFor(int userId, int sensorId, long requestId);
 
     /**
-     * Indicates a successful authentication occurred for a sensor of a given strength.
-     */
-    void authenticatedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
-            long requestId);
-
-    /**
      * Indicates authentication ended for a sensor of a given strength.
      */
     void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId,
-            long requestId);
+            long requestId, boolean wasSuccessful);
 
     /**
      * Indicates a lockout occurred for a sensor of a given strength.
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 8a24ff6..57d28f9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -81,9 +81,12 @@
     @State
     protected int mState = STATE_NEW;
     private long mStartTimeMs;
-
     private boolean mAuthAttempted;
     private boolean mAuthSuccess = false;
+    private final int mSensorStrength;
+    // This is used to determine if we should use the old lockout counter (HIDL) or the new lockout
+    // counter implementation (AIDL)
+    private final boolean mShouldUseLockoutTracker;
 
     public AuthenticationClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
             @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener,
@@ -92,7 +95,7 @@
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             boolean isStrongBiometric, @Nullable TaskStackListener taskStackListener,
             @NonNull LockoutTracker lockoutTracker, boolean allowBackgroundAuthentication,
-            boolean shouldVibrate, boolean isKeyguardBypassEnabled) {
+            boolean shouldVibrate, boolean isKeyguardBypassEnabled, int sensorStrength) {
         super(context, lazyDaemon, token, listener, targetUserId, owner, cookie, sensorId,
                 shouldVibrate, biometricLogger, biometricContext);
         mIsStrongBiometric = isStrongBiometric;
@@ -105,22 +108,13 @@
         mIsRestricted = restricted;
         mAllowBackgroundAuthentication = allowBackgroundAuthentication;
         mIsKeyguardBypassEnabled = isKeyguardBypassEnabled;
+        mShouldUseLockoutTracker = lockoutTracker != null;
+        mSensorStrength = sensorStrength;
     }
 
     @LockoutTracker.LockoutMode
     public int handleFailedAttempt(int userId) {
-        @LockoutTracker.LockoutMode final int lockoutMode =
-                mLockoutTracker.getLockoutModeForUser(userId);
-        final PerformanceTracker performanceTracker =
-                PerformanceTracker.getInstanceForSensorId(getSensorId());
-
-        if (lockoutMode == LockoutTracker.LOCKOUT_PERMANENT) {
-            performanceTracker.incrementPermanentLockoutForUser(userId);
-        } else if (lockoutMode == LockoutTracker.LOCKOUT_TIMED) {
-            performanceTracker.incrementTimedLockoutForUser(userId);
-        }
-
-        return lockoutMode;
+        return LockoutTracker.LOCKOUT_NONE;
     }
 
     protected long getStartTimeMs() {
@@ -273,10 +267,12 @@
                 cancel();
             } else {
                 // Allow system-defined limit of number of attempts before giving up
-                @LockoutTracker.LockoutMode final int lockoutMode =
-                        handleFailedAttempt(getTargetUserId());
-                if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
-                    markAlreadyDone();
+                if (mShouldUseLockoutTracker) {
+                    @LockoutTracker.LockoutMode final int lockoutMode =
+                            handleFailedAttempt(getTargetUserId());
+                    if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
+                        markAlreadyDone();
+                    }
                 }
 
                 try {
@@ -309,13 +305,6 @@
     @Override
     public void onAcquired(int acquiredInfo, int vendorCode) {
         super.onAcquired(acquiredInfo, vendorCode);
-
-        @LockoutTracker.LockoutMode final int lockoutMode =
-                mLockoutTracker.getLockoutModeForUser(getTargetUserId());
-        if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
-            PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
-            pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation());
-        }
     }
 
     @Override
@@ -331,8 +320,14 @@
     public void start(@NonNull ClientMonitorCallback callback) {
         super.start(callback);
 
-        @LockoutTracker.LockoutMode final int lockoutMode =
-                mLockoutTracker.getLockoutModeForUser(getTargetUserId());
+        final @LockoutTracker.LockoutMode int lockoutMode;
+        if (mShouldUseLockoutTracker) {
+            lockoutMode = mLockoutTracker.getLockoutModeForUser(getTargetUserId());
+        } else {
+            lockoutMode = getBiometricContext().getAuthSessionCoordinator()
+                    .getLockoutStateFor(getTargetUserId(), mSensorStrength);
+        }
+
         if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {
             Slog.v(TAG, "In lockout mode(" + lockoutMode + ") ; disallowing authentication");
             int errorCode = lockoutMode == LockoutTracker.LOCKOUT_TIMED
@@ -406,6 +401,14 @@
         return mAuthSuccess;
     }
 
+    protected int getSensorStrength() {
+        return mSensorStrength;
+    }
+
+    protected LockoutTracker getLockoutTracker() {
+        return mLockoutTracker;
+    }
+
     protected int getShowOverlayReason() {
         if (isKeyguard()) {
             return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 1370fd8..0216e49 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -192,9 +193,9 @@
         }
 
         // If the current client dies we should cancel the current operation.
-        if (this instanceof Interruptable) {
+        if (this.isInterruptable()) {
             Slog.e(TAG, "Binder died, cancelling client");
-            ((Interruptable) this).cancel();
+            this.cancel();
         }
         mToken = null;
         if (clearListener) {
@@ -293,4 +294,38 @@
                 + ", requestId=" + getRequestId()
                 + ", userId=" + getTargetUserId() + "}";
     }
+
+    /**
+     * Cancels this ClientMonitor
+     */
+    public void cancel() {
+        cancelWithoutStarting(mCallback);
+    }
+
+    /**
+     * Cancels this ClientMonitor without starting
+     * @param callback
+     */
+    public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) {
+        Slog.d(TAG, "cancelWithoutStarting: " + this);
+
+        final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+        try {
+            ClientMonitorCallbackConverter listener = getListener();
+            if (listener != null) {
+                listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to invoke sendError", e);
+        }
+        callback.onClientFinished(this, true /* success */);
+    }
+
+    /**
+     * Checks if other client monitor can interrupt current client monitor
+     * @return if current client can be interrupted
+     */
+    public boolean isInterruptable() {
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 9317c4e..fb978b2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -543,4 +543,37 @@
         mPendingOperations.clear();
         mCurrentOperation = null;
     }
+
+    /**
+     * Marks all pending operations as canceling and cancels the current
+     * operation.
+     */
+    private void clearScheduler() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
+            Slog.d(getTag(), "[Watchdog cancelling pending] "
+                    + pendingOperation.getClientMonitor());
+            pendingOperation.markCanceling();
+        }
+        Slog.d(getTag(), "[Watchdog cancelling current] "
+                + mCurrentOperation.getClientMonitor());
+        mCurrentOperation.cancel(mHandler, getInternalCallback());
+    }
+
+    /**
+     * Start the timeout for the watchdog.
+     */
+    public void startWatchdog() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        final BiometricSchedulerOperation mOperation = mCurrentOperation;
+        mHandler.postDelayed(() -> {
+            if (mOperation == mCurrentOperation) {
+                clearScheduler();
+            }
+        }, 10000);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index ef2931f..4825f1d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -46,7 +46,7 @@
 
     /**
      * The operation is added to the list of pending operations, but a subsequent operation
-     * has been added. This state only applies to {@link Interruptable} operations. When this
+     * has been added. This state only applies to interruptable operations. When this
      * operation reaches the head of the queue, it will send ERROR_CANCELED and finish.
      */
     protected static final int STATE_WAITING_IN_QUEUE_CANCELING = 1;
@@ -267,7 +267,7 @@
 
     /** Flags this operation as canceled, if possible, but does not cancel it until started. */
     public boolean markCanceling() {
-        if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
+        if (mState == STATE_WAITING_IN_QUEUE) {
             mState = STATE_WAITING_IN_QUEUE_CANCELING;
             return true;
         }
@@ -287,10 +287,6 @@
         }
 
         final int currentState = mState;
-        if (!isInterruptable()) {
-            Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this);
-            return;
-        }
         if (currentState == STATE_STARTED_CANCELING) {
             Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this);
             return;
@@ -301,10 +297,10 @@
                 || currentState == STATE_WAITING_IN_QUEUE_CANCELING
                 || currentState == STATE_WAITING_FOR_COOKIE) {
             Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback));
+            mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback));
         } else {
             Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancel();
+            mClientMonitor.cancel();
         }
 
         // forcibly finish this client if the HAL does not acknowledge within the timeout
@@ -351,9 +347,9 @@
         return mClientMonitor == clientMonitor;
     }
 
-    /** If this operation is {@link Interruptable}. */
+    /** If this operation is interruptable. */
     public boolean isInterruptable() {
-        return mClientMonitor instanceof Interruptable;
+        return mClientMonitor.isInterruptable();
     }
 
     private boolean isHalOperation() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
index 49cddaa..7fb27b6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
@@ -23,11 +23,11 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/Interruptable.java b/services/core/java/com/android/server/biometrics/sensors/Interruptable.java
deleted file mode 100644
index 4f645ef..0000000
--- a/services/core/java/com/android/server/biometrics/sensors/Interruptable.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.biometrics.sensors;
-
-import android.annotation.NonNull;
-
-/**
- * Interface that {@link BaseClientMonitor} subclasses eligible for cancellation should implement.
- */
-public interface Interruptable {
-    /**
-     * Requests to end the ClientMonitor's lifecycle.
-     */
-    void cancel();
-
-    /**
-     * Notifies the client that it needs to finish before
-     * {@link BaseClientMonitor#start(ClientMonitorCallback)} was invoked. This usually happens
-     * if the client is still waiting in the pending queue and got notified that a subsequent
-     * operation is preempting it.
-     *
-     * This method must invoke
-     * {@link ClientMonitorCallback#onClientFinished(BaseClientMonitor, boolean)} on the
-     * given callback (with success).
-     *
-     * @param callback invoked when the operation is completed.
-     */
-    void cancelWithoutStarting(@NonNull ClientMonitorCallback callback);
-}
diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
index d9bd04d..c24a989 100644
--- a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
@@ -22,7 +22,6 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
 
 import android.hardware.biometrics.BiometricManager;
-import android.os.SystemClock;
 import android.util.Slog;
 
 import java.time.Clock;
@@ -43,10 +42,6 @@
     private final Map<Integer, Map<Integer, AuthenticatorState>> mCanUserAuthenticate;
     private final Clock mClock;
 
-    MultiBiometricLockoutState() {
-        this(SystemClock.currentNetworkTimeClock());
-    }
-
     MultiBiometricLockoutState(Clock clock) {
         mCanUserAuthenticate = new HashMap<>();
         mClock = clock;
@@ -80,6 +75,9 @@
                 // fall through
             case Authenticators.BIOMETRIC_CONVENIENCE:
                 authMap.get(BIOMETRIC_CONVENIENCE).mPermanentlyLockedOut = !canAuth;
+                return;
+            default:
+                Slog.e(TAG, "increaseLockoutTime called for invalid strength : "  + strength);
         }
     }
 
@@ -94,6 +92,9 @@
                 // fall through
             case Authenticators.BIOMETRIC_CONVENIENCE:
                 authMap.get(BIOMETRIC_CONVENIENCE).increaseLockoutTo(duration);
+                return;
+            default:
+                Slog.e(TAG, "increaseLockoutTime called for invalid strength : "  + strength);
         }
     }
 
@@ -108,22 +109,34 @@
                 // fall through
             case Authenticators.BIOMETRIC_CONVENIENCE:
                 authMap.get(BIOMETRIC_CONVENIENCE).setTimedLockout(0);
+                return;
+            default:
+                Slog.e(TAG, "clearLockoutTime called for invalid strength : "  + strength);
         }
     }
 
     /**
-     * Indicates if a user can perform an authentication operation with a given
-     * {@link Authenticators.Types}
+     * Retrieves the lockout state for a user of a specified strength.
      *
      * @param userId   The user.
      * @param strength The strength of biometric that is requested to authenticate.
-     * @return If a user can authenticate with a given biometric of this strength.
      */
-    boolean canUserAuthenticate(int userId, @Authenticators.Types int strength) {
-        final boolean canAuthenticate = getAuthMapForUser(userId).get(strength).canAuthenticate();
-        Slog.d(TAG, "canUserAuthenticate(userId=" + userId + ", strength=" + strength + ") ="
-                + canAuthenticate);
-        return canAuthenticate;
+    @LockoutTracker.LockoutMode
+    int getLockoutState(int userId, @Authenticators.Types int strength) {
+        final Map<Integer, AuthenticatorState> authMap = getAuthMapForUser(userId);
+        if (!authMap.containsKey(strength)) {
+            Slog.e(TAG, "Error, getLockoutState for unknown strength: " + strength
+                    + " returning LOCKOUT_NONE");
+            return LockoutTracker.LOCKOUT_NONE;
+        }
+        final AuthenticatorState state = authMap.get(strength);
+        if (state.mPermanentlyLockedOut) {
+            return LockoutTracker.LOCKOUT_PERMANENT;
+        } else if (state.isTimedLockout()) {
+            return LockoutTracker.LOCKOUT_TIMED;
+        } else {
+            return LockoutTracker.LOCKOUT_NONE;
+        }
     }
 
     @Override
@@ -157,7 +170,15 @@
         }
 
         boolean canAuthenticate() {
-            return !mPermanentlyLockedOut && mClock.millis() - mTimedLockout >= 0;
+            return !mPermanentlyLockedOut && !isTimedLockout();
+        }
+
+        boolean isTimedLockout() {
+            return mClock.millis() - mTimedLockout < 0;
+        }
+
+        void setTimedLockout(long duration) {
+            mTimedLockout = duration;
         }
 
         /**
@@ -167,10 +188,6 @@
             mTimedLockout = Math.max(mTimedLockout, duration);
         }
 
-        void setTimedLockout(long duration) {
-            mTimedLockout = duration;
-        }
-
         String toString(long currentTime) {
             final String duration =
                     mTimedLockout - currentTime > 0 ? (mTimedLockout - currentTime) + "ms" : "none";
diff --git a/services/core/java/com/android/server/biometrics/sensors/PerformanceTracker.java b/services/core/java/com/android/server/biometrics/sensors/PerformanceTracker.java
index 42b22b0..eed2bdd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/PerformanceTracker.java
+++ b/services/core/java/com/android/server/biometrics/sensors/PerformanceTracker.java
@@ -85,7 +85,7 @@
         }
     }
 
-    void incrementAcquireForUser(int userId, boolean isCrypto) {
+    public void incrementAcquireForUser(int userId, boolean isCrypto) {
         createUserEntryIfNecessary(userId);
 
         if (isCrypto) {
@@ -95,13 +95,13 @@
         }
     }
 
-    void incrementTimedLockoutForUser(int userId) {
+    public void incrementTimedLockoutForUser(int userId) {
         createUserEntryIfNecessary(userId);
 
         mAllUsersInfo.get(userId).mTimedLockout++;
     }
 
-    void incrementPermanentLockoutForUser(int userId) {
+    public void incrementPermanentLockoutForUser(int userId) {
         createUserEntryIfNecessary(userId);
 
         mAllUsersInfo.get(userId).mPermanentLockout++;
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/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 271bce9..2f147c4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -99,6 +99,8 @@
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) {
+            super.createTestSession_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
 
             if (provider == null) {
@@ -112,6 +114,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) {
+            super.dumpSensorServiceStateProto_enforcePermission();
+
             final ProtoOutputStream proto = new ProtoOutputStream();
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider != null) {
@@ -125,6 +129,8 @@
         @Override // Binder call
         public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(
                 String opPackageName) {
+            super.getSensorPropertiesInternal_enforcePermission();
+
             return mRegistry.getAllProperties();
         }
 
@@ -132,6 +138,8 @@
         @Override // Binder call
         public FaceSensorPropertiesInternal getSensorProperties(int sensorId,
                 @NonNull String opPackageName) {
+            super.getSensorProperties_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId
@@ -146,6 +154,8 @@
         @Override // Binder call
         public void generateChallenge(IBinder token, int sensorId, int userId,
                 IFaceServiceReceiver receiver, String opPackageName) {
+            super.generateChallenge_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId);
@@ -159,6 +169,8 @@
         @Override // Binder call
         public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName,
                 long challenge) {
+            super.revokeChallenge_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId);
@@ -173,6 +185,8 @@
         public long enroll(int userId, final IBinder token, final byte[] hardwareAuthToken,
                 final IFaceServiceReceiver receiver, final String opPackageName,
                 final int[] disabledFeatures, Surface previewSurface, boolean debugConsent) {
+            super.enroll_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for enroll");
@@ -183,18 +197,36 @@
                     receiver, opPackageName, disabledFeatures, previewSurface, debugConsent);
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            super.scheduleWatchdog_enforcePermission();
+
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
+
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken,
                 final IFaceServiceReceiver receiver, final String opPackageName,
                 final int[] disabledFeatures) {
             // TODO(b/145027036): Implement this.
+            super.enrollRemotely_enforcePermission();
+
             return -1;
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public void cancelEnrollment(final IBinder token, long requestId) {
+            super.cancelEnrollment_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelEnrollment");
@@ -212,6 +244,8 @@
             // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or
             //  lockdown, something wrong happened. See similar path in FingerprintService.
 
+            super.authenticate_enforcePermission();
+
             final boolean restricted = false; // Face APIs are private
             final int statsClient = Utils.isKeyguard(getContext(), opPackageName)
                     ? BiometricsProtoEnums.CLIENT_KEYGUARD
@@ -237,6 +271,8 @@
         @Override // Binder call
         public long detectFace(final IBinder token, final int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName) {
+            super.detectFace_enforcePermission();
+
             if (!Utils.isKeyguard(getContext(), opPackageName)) {
                 Slog.w(TAG, "detectFace called from non-sysui package: " + opPackageName);
                 return -1;
@@ -266,6 +302,8 @@
                 IBinder token, long operationId, int userId,
                 IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId,
                 int cookie, boolean allowBackgroundAuthentication) {
+            super.prepareForAuthentication_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for prepareForAuthentication");
@@ -283,6 +321,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public void startPreparedClient(int sensorId, int cookie) {
+            super.startPreparedClient_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for startPreparedClient");
@@ -296,6 +336,8 @@
         @Override // Binder call
         public void cancelAuthentication(final IBinder token, final String opPackageName,
                 final long requestId) {
+            super.cancelAuthentication_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthentication");
@@ -309,6 +351,8 @@
         @Override // Binder call
         public void cancelFaceDetect(final IBinder token, final String opPackageName,
                 final long requestId) {
+            super.cancelFaceDetect_enforcePermission();
+
             if (!Utils.isKeyguard(getContext(), opPackageName)) {
                 Slog.w(TAG, "cancelFaceDetect called from non-sysui package: "
                         + opPackageName);
@@ -328,6 +372,8 @@
         @Override // Binder call
         public void cancelAuthenticationFromService(int sensorId, final IBinder token,
                 final String opPackageName, final long requestId) {
+            super.cancelAuthenticationFromService_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthenticationFromService");
@@ -341,6 +387,8 @@
         @Override // Binder call
         public void remove(final IBinder token, final int faceId, final int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName) {
+            super.remove_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for remove");
@@ -355,6 +403,8 @@
         @Override // Binder call
         public void removeAll(final IBinder token, final int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName) {
+            super.removeAll_enforcePermission();
+
             final FaceServiceReceiver internalReceiver = new FaceServiceReceiver() {
                 int sensorsFinishedRemoving = 0;
                 final int numSensors = getSensorPropertiesInternal(
@@ -387,6 +437,8 @@
         @Override // Binder call
         public void addLockoutResetCallback(final IBiometricServiceLockoutResetCallback callback,
                 final String opPackageName) {
+            super.addLockoutResetCallback_enforcePermission();
+
             mLockoutResetDispatcher.addCallback(callback, opPackageName);
         }
 
@@ -446,6 +498,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean isHardwareDetected(int sensorId, String opPackageName) {
+            super.isHardwareDetected_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
@@ -462,6 +516,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public List<Face> getEnrolledFaces(int sensorId, int userId, String opPackageName) {
+            super.getEnrolledFaces_enforcePermission();
+
             if (userId != UserHandle.getCallingUserId()) {
                 Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS);
             }
@@ -478,6 +534,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) {
+            super.hasEnrolledFaces_enforcePermission();
+
             if (userId != UserHandle.getCallingUserId()) {
                 Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS);
             }
@@ -494,6 +552,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) {
+            super.getLockoutModeForUser_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getLockoutModeForUser");
@@ -507,6 +567,8 @@
         @Override
         public void invalidateAuthenticatorId(int sensorId, int userId,
                 IInvalidationCallback callback) {
+            super.invalidateAuthenticatorId_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for invalidateAuthenticatorId");
@@ -519,6 +581,8 @@
         @Override // Binder call
         public long getAuthenticatorId(int sensorId, int userId) {
 
+            super.getAuthenticatorId_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getAuthenticatorId");
@@ -532,6 +596,8 @@
         @Override // Binder call
         public void resetLockout(IBinder token, int sensorId, int userId, byte[] hardwareAuthToken,
                 String opPackageName) {
+            super.resetLockout_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName);
@@ -546,6 +612,8 @@
         public void setFeature(final IBinder token, int userId, int feature, boolean enabled,
                 final byte[] hardwareAuthToken, IFaceServiceReceiver receiver,
                 final String opPackageName) {
+            super.setFeature_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for setFeature");
@@ -560,6 +628,8 @@
         @Override
         public void getFeature(final IBinder token, int userId, int feature,
                 IFaceServiceReceiver receiver, final String opPackageName) {
+            super.getFeature_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getFeature");
@@ -588,8 +658,9 @@
                 }
                 try {
                     final SensorProps[] props = face.getSensorProps();
-                    final FaceProvider provider = new FaceProvider(getContext(), props, instance,
-                            mLockoutResetDispatcher, BiometricContext.getInstance(getContext()));
+                    final FaceProvider provider = new FaceProvider(getContext(),
+                            mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
+                            BiometricContext.getInstance(getContext()));
                     providers.add(provider);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -600,14 +671,16 @@
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
-        @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
+            super.registerAuthenticators_enforcePermission();
+
             mRegistry.registerAll(() -> {
                 final List<ServiceProvider> providers = new ArrayList<>();
                 for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
                     providers.add(
-                            Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
+                            Face10.newInstance(getContext(), mBiometricStateCallback,
+                                    hidlSensor, mLockoutResetDispatcher));
                 }
                 providers.addAll(getAidlProviders());
                 return providers;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
index a9981d0..5a82b3a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
@@ -19,10 +19,10 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.face.Face;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.biometrics.sensors.BiometricUserState;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
index 4efaedb..85f95ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
@@ -128,4 +128,10 @@
             @NonNull String opPackageName);
 
     void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 73c272f..d11f099 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -16,14 +16,13 @@
 
 package com.android.server.biometrics.sensors.face.aidl;
 
-import static android.Manifest.permission.TEST_BIOMETRIC;
-
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
 import android.hardware.biometrics.face.AuthenticationFrame;
 import android.hardware.biometrics.face.BaseFrame;
+import android.hardware.biometrics.face.EnrollmentFrame;
 import android.hardware.face.Face;
 import android.hardware.face.FaceAuthenticationFrame;
 import android.hardware.face.FaceEnrollFrame;
@@ -33,9 +32,9 @@
 import android.util.Slog;
 
 import com.android.server.biometrics.HardwareAuthTokenUtils;
-import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
+import com.android.server.biometrics.sensors.EnrollClient;
 import com.android.server.biometrics.sensors.face.FaceUtils;
 
 import java.util.HashSet;
@@ -142,6 +141,8 @@
     @Override
     public void setTestHalEnabled(boolean enabled) {
 
+        super.setTestHalEnabled_enforcePermission();
+
         mProvider.setTestHalEnabled(enabled);
         mSensor.setTestHalEnabled(enabled);
     }
@@ -150,6 +151,8 @@
     @Override
     public void startEnroll(int userId) {
 
+        super.startEnroll_enforcePermission();
+
         mProvider.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver,
                 mContext.getOpPackageName(), new int[0] /* disabledFeatures */,
                 null /* previewSurface */, false /* debugConsent */);
@@ -159,6 +162,8 @@
     @Override
     public void finishEnroll(int userId) {
 
+        super.finishEnroll_enforcePermission();
+
         int nextRandomId = mRandom.nextInt();
         while (mEnrollmentIds.contains(nextRandomId)) {
             nextRandomId = mRandom.nextInt();
@@ -174,6 +179,8 @@
     public void acceptAuthentication(int userId)  {
 
         // Fake authentication with any of the existing faces
+        super.acceptAuthentication_enforcePermission();
+
         List<Face> faces = FaceUtils.getInstance(mSensorId)
                 .getBiometricsForUser(mContext, userId);
         if (faces.isEmpty()) {
@@ -189,30 +196,38 @@
     @Override
     public void rejectAuthentication(int userId)  {
 
+        super.rejectAuthentication_enforcePermission();
+
         mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFailed();
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
-    // TODO(b/178414967): replace with notifyAuthenticationFrame and notifyEnrollmentFrame.
     @Override
     public void notifyAcquired(int userId, int acquireInfo) {
+        super.notifyAcquired_enforcePermission();
 
         BaseFrame data = new BaseFrame();
         data.acquiredInfo = (byte) acquireInfo;
 
-        AuthenticationFrame authenticationFrame = new AuthenticationFrame();
-        authenticationFrame.data = data;
-
-        // TODO(b/178414967): Currently onAuthenticationFrame and onEnrollmentFrame are the same.
-        // This will need to call the correct callback once the onAcquired callback is removed.
-        mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFrame(
-                authenticationFrame);
+        if (mSensor.getScheduler().getCurrentClient() instanceof EnrollClient) {
+            final EnrollmentFrame frame = new EnrollmentFrame();
+            frame.data = data;
+            mSensor.getSessionForUser(userId).getHalSessionCallback()
+                    .onEnrollmentFrame(frame);
+        } else {
+            final AuthenticationFrame frame = new AuthenticationFrame();
+            frame.data = data;
+            mSensor.getSessionForUser(userId).getHalSessionCallback()
+                    .onAuthenticationFrame(frame);
+        }
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
     @Override
     public void notifyError(int userId, int errorCode)  {
 
+        super.notifyError_enforcePermission();
+
         mSensor.getSessionForUser(userId).getHalSessionCallback().onError((byte) errorCode,
                 0 /* vendorCode */);
     }
@@ -221,6 +236,8 @@
     @Override
     public void cleanupInternalState(int userId)  {
 
+        super.cleanupInternalState_enforcePermission();
+
         Slog.d(TAG, "cleanupInternalState: " + userId);
         mProvider.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() {
             @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index c27d71f..b1cb257 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -47,7 +47,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
-import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.face.UsageStats;
 
 import java.util.ArrayList;
@@ -63,8 +63,6 @@
     @NonNull
     private final UsageStats mUsageStats;
     @NonNull
-    private final LockoutCache mLockoutCache;
-    @NonNull
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @Nullable
     private final NotificationManager mNotificationManager;
@@ -72,7 +70,6 @@
     private final int[] mBiometricPromptIgnoreListVendor;
     private final int[] mKeyguardIgnoreList;
     private final int[] mKeyguardIgnoreListVendor;
-    private final int mBiometricStrength;
     @Nullable
     private ICancellationSignal mCancellationSignal;
     @Nullable
@@ -88,12 +85,12 @@
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             boolean isStrongBiometric, @NonNull UsageStats usageStats,
             @NonNull LockoutCache lockoutCache, boolean allowBackgroundAuthentication,
-            boolean isKeyguardBypassEnabled, @Authenticators.Types int biometricStrength) {
+            boolean isKeyguardBypassEnabled, @Authenticators.Types int sensorStrength) {
         this(context, lazyDaemon, token, requestId, listener, targetUserId, operationId,
                 restricted, owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
-                isStrongBiometric, usageStats, lockoutCache, allowBackgroundAuthentication,
-                isKeyguardBypassEnabled, context.getSystemService(SensorPrivacyManager.class),
-                biometricStrength);
+                isStrongBiometric, usageStats, lockoutCache /* lockoutCache */,
+                allowBackgroundAuthentication, isKeyguardBypassEnabled,
+                context.getSystemService(SensorPrivacyManager.class), sensorStrength);
     }
 
     @VisibleForTesting
@@ -109,13 +106,11 @@
             @Authenticators.Types int biometricStrength) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
-                isStrongBiometric, null /* taskStackListener */, lockoutCache,
-                allowBackgroundAuthentication,
-                false /* shouldVibrate */,
-                isKeyguardBypassEnabled);
+                isStrongBiometric, null /* taskStackListener */, null /* lockoutCache */,
+                allowBackgroundAuthentication, false /* shouldVibrate */,
+                isKeyguardBypassEnabled, biometricStrength);
         setRequestId(requestId);
         mUsageStats = usageStats;
-        mLockoutCache = lockoutCache;
         mNotificationManager = context.getSystemService(NotificationManager.class);
         mSensorPrivacyManager = sensorPrivacyManager;
         mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator();
@@ -129,14 +124,12 @@
                 R.array.config_face_acquire_keyguard_ignorelist);
         mKeyguardIgnoreListVendor = resources.getIntArray(
                 R.array.config_face_acquire_vendor_keyguard_ignorelist);
-        mBiometricStrength = biometricStrength;
     }
 
     @Override
     public void start(@NonNull ClientMonitorCallback callback) {
         super.start(callback);
         mState = STATE_STARTED;
-        mAuthSessionCoordinator.authStartedFor(getTargetUserId(), getSensorId(), getRequestId());
     }
 
     @NonNull
@@ -221,9 +214,6 @@
                 0 /* error */,
                 0 /* vendorError */,
                 getTargetUserId()));
-        mAuthSessionCoordinator
-                .authenticatedFor(getTargetUserId(), mBiometricStrength, getSensorId(),
-                        getRequestId());
     }
 
     @Override
@@ -239,8 +229,6 @@
         if (error == BiometricConstants.BIOMETRIC_ERROR_RE_ENROLL) {
             BiometricNotificationUtils.showReEnrollmentNotification(getContext());
         }
-        mAuthSessionCoordinator.authEndedFor(getTargetUserId(), mBiometricStrength, getSensorId(),
-                getRequestId());
         super.onError(error, vendorCode);
     }
 
@@ -263,6 +251,8 @@
         mLastAcquire = acquireInfo;
         final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode);
         onAcquiredInternal(acquireInfo, vendorCode, shouldSend);
+        PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
+        pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation());
     }
 
     /**
@@ -290,35 +280,39 @@
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_TIMED);
+        mAuthSessionCoordinator.lockOutTimed(getTargetUserId(), getSensorStrength(), getSensorId(),
+                durationMillis, getRequestId());
         // Lockout metrics are logged as an error code.
         final int error = BiometricFaceConstants.FACE_ERROR_LOCKOUT;
         getLogger().logOnError(getContext(), getOperationContext(),
                 error, 0 /* vendorCode */, getTargetUserId());
 
+        PerformanceTracker.getInstanceForSensorId(getSensorId())
+                .incrementTimedLockoutForUser(getTargetUserId());
+
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
-        mAuthSessionCoordinator.lockOutTimed(getTargetUserId(), mBiometricStrength, getSensorId(),
-                durationMillis, getRequestId());
     }
 
     @Override
     public void onLockoutPermanent() {
-        mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_PERMANENT);
+        mAuthSessionCoordinator.lockedOutFor(getTargetUserId(), getSensorStrength(), getSensorId(),
+                getRequestId());
         // Lockout metrics are logged as an error code.
         final int error = BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
         getLogger().logOnError(getContext(), getOperationContext(),
                 error, 0 /* vendorCode */, getTargetUserId());
 
+        PerformanceTracker.getInstanceForSensorId(getSensorId())
+                .incrementPermanentLockoutForUser(getTargetUserId());
+
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Remote exception", e);
         }
-        mAuthSessionCoordinator.lockedOutFor(getTargetUserId(), mBiometricStrength, getSensorId(),
-                getRequestId());
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index b60f9d8..89852a1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -50,10 +50,14 @@
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.PerformanceTracker;
@@ -79,19 +83,34 @@
 
     private boolean mTestHalEnabled;
 
-    @NonNull private final Context mContext;
-    @NonNull private final String mHalInstanceName;
-    @NonNull @VisibleForTesting
+    @NonNull
+    @VisibleForTesting
     final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports
-    @NonNull private final Handler mHandler;
-    @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher;
-    @NonNull private final UsageStats mUsageStats;
-    @NonNull private final ActivityTaskManager mActivityTaskManager;
-    @NonNull private final BiometricTaskStackListener mTaskStackListener;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final BiometricStateCallback mBiometricStateCallback;
+    @NonNull
+    private final String mHalInstanceName;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    @NonNull
+    private final UsageStats mUsageStats;
+    @NonNull
+    private final ActivityTaskManager mActivityTaskManager;
+    @NonNull
+    private final BiometricTaskStackListener mTaskStackListener;
     // for requests that do not use biometric prompt
-    @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
-    @NonNull private final BiometricContext mBiometricContext;
-    @Nullable private IFace mDaemon;
+    @NonNull
+    private final AtomicLong mRequestCounter = new AtomicLong(0);
+    @NonNull
+    private final BiometricContext mBiometricContext;
+    @NonNull
+    private final AuthSessionCoordinator mAuthSessionCoordinator;
+    @Nullable
+    private IFace mDaemon;
 
     private final class BiometricTaskStackListener extends TaskStackListener {
         @Override
@@ -121,11 +140,14 @@
         }
     }
 
-    public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props,
+    public FaceProvider(@NonNull Context context,
+            @NonNull BiometricStateCallback biometricStateCallback,
+            @NonNull SensorProps[] props,
             @NonNull String halInstanceName,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull BiometricContext biometricContext) {
         mContext = context;
+        mBiometricStateCallback = biometricStateCallback;
         mHalInstanceName = halInstanceName;
         mSensors = new SparseArray<>();
         mHandler = new Handler(Looper.getMainLooper());
@@ -134,6 +156,7 @@
         mActivityTaskManager = ActivityTaskManager.getInstance();
         mTaskStackListener = new BiometricTaskStackListener();
         mBiometricContext = biometricContext;
+        mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
 
         for (SensorProps prop : props) {
             final int sensorId = prop.commonProps.sensorId;
@@ -305,7 +328,8 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mSensors.get(sensorId).getLockoutCache().getLockoutModeForUser(userId);
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(sensorId));
     }
 
     @Override
@@ -362,16 +386,18 @@
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, maxTemplatesPerUser, debugConsent);
-            scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
-                @Override
-                public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
-                        boolean success) {
-                    if (success) {
-                        scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
-                        scheduleInvalidationRequest(sensorId, userId);
-                    }
-                }
-            });
+            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                            mBiometricStateCallback, new ClientMonitorCallback() {
+                        @Override
+                        public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                                boolean success) {
+                            ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
+                            if (success) {
+                                scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+                                scheduleInvalidationRequest(sensorId, userId);
+                            }
+                        }
+                    }));
         });
         return id;
     }
@@ -395,7 +421,7 @@
                     token, id, callback, userId, opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric);
-            scheduleForSensor(sensorId, client);
+            scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
 
         return id;
@@ -414,7 +440,6 @@
             boolean allowBackgroundAuthentication, boolean isKeyguardBypassEnabled) {
         mHandler.post(() -> {
             final boolean isStrongBiometric = Utils.isStrongBiometric(sensorId);
-            final int biometricStrength = Utils.getCurrentStrength(sensorId);
             final FaceAuthenticationClient client = new FaceAuthenticationClient(
                     mContext, mSensors.get(sensorId).getLazySession(), token, requestId, callback,
                     userId, operationId, restricted, opPackageName, cookie,
@@ -422,8 +447,24 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mUsageStats, mSensors.get(sensorId).getLockoutCache(),
-                    allowBackgroundAuthentication, isKeyguardBypassEnabled, biometricStrength);
-            scheduleForSensor(sensorId, client);
+                    allowBackgroundAuthentication, isKeyguardBypassEnabled,
+                    Utils.getCurrentStrength(sensorId)
+                    );
+            scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
+                @Override
+                public void onClientStarted(
+                         BaseClientMonitor clientMonitor) {
+                    mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+                }
+
+                @Override
+                public void onClientFinished(
+                        BaseClientMonitor clientMonitor,
+                        boolean success) {
+                    mAuthSessionCoordinator.authEndedFor(userId, Utils.getCurrentStrength(sensorId),
+                            sensorId, requestId, success);
+                }
+            });
         });
     }
 
@@ -478,7 +519,7 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
                     mSensors.get(sensorId).getAuthenticatorIds());
-            scheduleForSensor(sensorId, client);
+            scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
 
@@ -567,7 +608,8 @@
             if (favorHalEnrollments) {
                 client.setFavorHalEnrollments();
             }
-            scheduleForSensor(sensorId, client, callback);
+            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback,
+                    mBiometricStateCallback));
         });
     }
 
@@ -661,4 +703,14 @@
     void setTestHalEnabled(boolean enabled) {
         mTestHalEnabled = enabled;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for face");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
index 32bed48..759c52a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
@@ -89,9 +89,9 @@
     void onLockoutCleared() {
         resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache,
                 mLockoutResetDispatcher);
+        mCallback.onClientFinished(this, true /* success */);
         getBiometricContext().getAuthSessionCoordinator()
                 .resetLockoutFor(getTargetUserId(), mBiometricStrength, getRequestId());
-        mCallback.onClientFinished(this, true /* success */);
     }
 
     public boolean interruptsPrecedingClients() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index 800d4b8..0d30ddd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -59,7 +59,6 @@
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
-import com.android.server.biometrics.sensors.Interruptable;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -638,7 +637,7 @@
 
     public void onBinderDied() {
         final BaseClientMonitor client = mScheduler.getCurrentClient();
-        if (client instanceof Interruptable) {
+        if (client != null && client.isInterruptable()) {
             Slog.e(mTag, "Sending ERROR_HW_UNAVAILABLE for client: " + client);
             final ErrorConsumer errorConsumer = (ErrorConsumer) client;
             errorConsumer.onError(FaceManager.FACE_ERROR_HW_UNAVAILABLE,
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
index 14af216..151ffaa 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.server.biometrics.sensors.face.hidl;
 
-import static android.Manifest.permission.TEST_BIOMETRIC;
-
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.biometrics.ITestSession;
@@ -30,7 +28,6 @@
 import android.os.RemoteException;
 import android.util.Slog;
 
-import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.face.FaceUtils;
@@ -53,6 +50,7 @@
     @NonNull private final Set<Integer> mEnrollmentIds;
     @NonNull private final Random mRandom;
 
+
     private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() {
         @Override
         public void onEnrollResult(Face face, int remaining) {
@@ -116,7 +114,8 @@
     };
 
     BiometricTestSessionImpl(@NonNull Context context, int sensorId,
-            @NonNull ITestSessionCallback callback, @NonNull Face10 face10,
+            @NonNull ITestSessionCallback callback,
+            @NonNull Face10 face10,
             @NonNull Face10.HalResultController halResultController) {
         mContext = context;
         mSensorId = sensorId;
@@ -131,6 +130,8 @@
     @Override
     public void setTestHalEnabled(boolean enabled) {
 
+        super.setTestHalEnabled_enforcePermission();
+
         mFace10.setTestHalEnabled(enabled);
     }
 
@@ -138,6 +139,8 @@
     @Override
     public void startEnroll(int userId) {
 
+        super.startEnroll_enforcePermission();
+
         mFace10.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver,
                 mContext.getOpPackageName(), new int[0] /* disabledFeatures */,
                 null /* previewSurface */, false /* debugConsent */);
@@ -147,6 +150,8 @@
     @Override
     public void finishEnroll(int userId) {
 
+        super.finishEnroll_enforcePermission();
+
         int nextRandomId = mRandom.nextInt();
         while (mEnrollmentIds.contains(nextRandomId)) {
             nextRandomId = mRandom.nextInt();
@@ -162,6 +167,8 @@
     public void acceptAuthentication(int userId) {
 
         // Fake authentication with any of the existing fingers
+        super.acceptAuthentication_enforcePermission();
+
         List<Face> faces = FaceUtils.getLegacyInstance(mSensorId)
                 .getBiometricsForUser(mContext, userId);
         if (faces.isEmpty()) {
@@ -177,6 +184,8 @@
     @Override
     public void rejectAuthentication(int userId) {
 
+        super.rejectAuthentication_enforcePermission();
+
         mHalResultController.onAuthenticated(0 /* deviceId */, 0 /* faceId */, userId, null);
     }
 
@@ -184,6 +193,8 @@
     @Override
     public void notifyAcquired(int userId, int acquireInfo) {
 
+        super.notifyAcquired_enforcePermission();
+
         mHalResultController.onAcquired(0 /* deviceId */, userId, acquireInfo, 0 /* vendorCode */);
     }
 
@@ -191,6 +202,8 @@
     @Override
     public void notifyError(int userId, int errorCode) {
 
+        super.notifyError_enforcePermission();
+
         mHalResultController.onError(0 /* deviceId */, userId, errorCode, 0 /* vendorCode */);
     }
 
@@ -198,6 +211,8 @@
     @Override
     public void cleanupInternalState(int userId) {
 
+        super.cleanupInternalState_enforcePermission();
+
         mFace10.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() {
             @Override
             public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index c0a119f..1adc5e3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -62,8 +62,10 @@
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricNotificationUtils;
 import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.EnumerateConsumer;
 import com.android.server.biometrics.sensors.ErrorConsumer;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -110,6 +112,7 @@
     private boolean mTestHalEnabled;
 
     @NonNull private final FaceSensorPropertiesInternal mSensorProperties;
+    @NonNull private final BiometricStateCallback mBiometricStateCallback;
     @NonNull private final Context mContext;
     @NonNull private final BiometricScheduler mScheduler;
     @NonNull private final Handler mHandler;
@@ -336,6 +339,7 @@
 
     @VisibleForTesting
     Face10(@NonNull Context context,
+            @NonNull BiometricStateCallback biometricStateCallback,
             @NonNull FaceSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher,
             @NonNull Handler handler,
@@ -343,6 +347,7 @@
             @NonNull BiometricContext biometricContext) {
         mSensorProperties = sensorProps;
         mContext = context;
+        mBiometricStateCallback = biometricStateCallback;
         mSensorId = sensorProps.sensorId;
         mScheduler = scheduler;
         mHandler = handler;
@@ -366,11 +371,12 @@
     }
 
     public static Face10 newInstance(@NonNull Context context,
+            @NonNull BiometricStateCallback biometricStateCallback,
             @NonNull FaceSensorPropertiesInternal sensorProps,
             @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
         final Handler handler = new Handler(Looper.getMainLooper());
-        return new Face10(context, sensorProps, lockoutResetDispatcher, handler,
-                new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+        return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
+                handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
                         null /* gestureAvailabilityTracker */),
                 BiometricContext.getInstance(context));
     }
@@ -615,8 +621,19 @@
 
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
+                public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
+                    mBiometricStateCallback.onClientStarted(clientMonitor);
+                }
+
+                @Override
+                public void onBiometricAction(int action) {
+                    mBiometricStateCallback.onBiometricAction(action);
+                }
+
+                @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
+                    mBiometricStateCallback.onClientFinished(clientMonitor, success);
                     if (success) {
                         // Update authenticatorIds
                         scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId());
@@ -660,7 +677,8 @@
                     opPackageName, cookie, false /* requireConfirmation */, mSensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric, mLockoutTracker,
-                    mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled);
+                    mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled,
+                    Utils.getCurrentStrength(mSensorId));
             mScheduler.scheduleClientMonitor(client);
         });
     }
@@ -696,7 +714,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client);
+            mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
     }
 
@@ -714,7 +732,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_REMOVE,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client);
+            mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
     }
 
@@ -806,14 +824,15 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext, enrolledList,
                     FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
-            mScheduler.scheduleClientMonitor(client, callback);
+            mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback,
+                    mBiometricStateCallback));
         });
     }
 
     @Override
     public void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback) {
-        scheduleInternalCleanup(userId, callback);
+        scheduleInternalCleanup(userId, mBiometricStateCallback);
     }
 
     @Override
@@ -1011,7 +1030,7 @@
     @Override
     public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName) {
-        return new BiometricTestSessionImpl(mContext, mSensorId, callback, this,
-                mHalResultController);
+        return new BiometricTestSessionImpl(mContext, mSensorId, callback,
+                this, mHalResultController);
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
index 91eec7d..d4a7f08 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
@@ -23,6 +23,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricManager.Authenticators;
 import android.hardware.biometrics.face.V1_0.IBiometricsFace;
 import android.hardware.face.FaceManager;
 import android.os.IBinder;
@@ -39,6 +40,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.face.UsageStats;
 
 import java.util.ArrayList;
@@ -70,12 +72,12 @@
             @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
             boolean isStrongBiometric, @NonNull LockoutTracker lockoutTracker,
             @NonNull UsageStats usageStats, boolean allowBackgroundAuthentication,
-            boolean isKeyguardBypassEnabled) {
+            boolean isKeyguardBypassEnabled, @Authenticators.Types int sensorStrength) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
                 isStrongBiometric, null /* taskStackListener */,
                 lockoutTracker, allowBackgroundAuthentication, false /* shouldVibrate */,
-                isKeyguardBypassEnabled);
+                isKeyguardBypassEnabled, sensorStrength);
         setRequestId(requestId);
         mUsageStats = usageStats;
         mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
@@ -154,6 +156,21 @@
     }
 
     @Override
+    public @LockoutTracker.LockoutMode int handleFailedAttempt(int userId) {
+        @LockoutTracker.LockoutMode final int lockoutMode =
+                getLockoutTracker().getLockoutModeForUser(userId);
+        final PerformanceTracker performanceTracker =
+                PerformanceTracker.getInstanceForSensorId(getSensorId());
+        if (lockoutMode == LockoutTracker.LOCKOUT_PERMANENT) {
+            performanceTracker.incrementPermanentLockoutForUser(userId);
+        } else if (lockoutMode == LockoutTracker.LOCKOUT_TIMED) {
+            performanceTracker.incrementTimedLockoutForUser(userId);
+        }
+
+        return lockoutMode;
+    }
+
+    @Override
     public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
             boolean authenticated, ArrayList<Byte> token) {
         super.onAuthenticated(identifier, authenticated, token);
@@ -204,6 +221,12 @@
         if (acquireInfo == FaceManager.FACE_ACQUIRED_RECALIBRATE) {
             BiometricNotificationUtils.showReEnrollmentNotification(getContext());
         }
+        @LockoutTracker.LockoutMode final int lockoutMode =
+                getLockoutTracker().getLockoutModeForUser(getTargetUserId());
+        if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
+            PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
+            pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation());
+        }
 
         final boolean shouldSend = shouldSend(acquireInfo, vendorCode);
         onAcquiredInternal(acquireInfo, vendorCode, shouldSend);
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 7e2742e..dca9ef2 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;
@@ -132,6 +133,8 @@
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) {
+            super.createTestSession_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
 
             if (provider == null) {
@@ -145,6 +148,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) {
+            super.dumpSensorServiceStateProto_enforcePermission();
+
             final ProtoOutputStream proto = new ProtoOutputStream();
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider != null) {
@@ -168,6 +173,8 @@
         @Override
         public FingerprintSensorPropertiesInternal getSensorProperties(int sensorId,
                 @NonNull String opPackageName) {
+            super.getSensorProperties_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId
@@ -181,6 +188,8 @@
         @Override // Binder call
         public void generateChallenge(IBinder token, int sensorId, int userId,
                 IFingerprintServiceReceiver receiver, String opPackageName) {
+            super.generateChallenge_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId);
@@ -194,6 +203,8 @@
         @Override // Binder call
         public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName,
                 long challenge) {
+            super.revokeChallenge_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId);
@@ -209,6 +220,8 @@
         public long enroll(final IBinder token, @NonNull final byte[] hardwareAuthToken,
                 final int userId, final IFingerprintServiceReceiver receiver,
                 final String opPackageName, @FingerprintManager.EnrollReason int enrollReason) {
+            super.enroll_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for enroll");
@@ -222,6 +235,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override // Binder call
         public void cancelEnrollment(final IBinder token, long requestId) {
+            super.cancelEnrollment_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelEnrollment");
@@ -398,6 +413,8 @@
         @Override
         public long detectFingerprint(final IBinder token, final int userId,
                 final IFingerprintServiceReceiver receiver, final String opPackageName) {
+            super.detectFingerprint_enforcePermission();
+
             if (!Utils.isKeyguard(getContext(), opPackageName)) {
                 Slog.w(TAG, "detectFingerprint called from non-sysui package: " + opPackageName);
                 return -1;
@@ -426,6 +443,8 @@
         public void prepareForAuthentication(int sensorId, IBinder token, long operationId,
                 int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName,
                 long requestId, int cookie, boolean allowBackgroundAuthentication) {
+            super.prepareForAuthentication_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for prepareForAuthentication");
@@ -442,6 +461,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public void startPreparedClient(int sensorId, int cookie) {
+            super.startPreparedClient_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for startPreparedClient");
@@ -485,6 +506,8 @@
         @Override // Binder call
         public void cancelFingerprintDetect(final IBinder token, final String opPackageName,
                 final long requestId) {
+            super.cancelFingerprintDetect_enforcePermission();
+
             if (!Utils.isKeyguard(getContext(), opPackageName)) {
                 Slog.w(TAG, "cancelFingerprintDetect called from non-sysui package: "
                         + opPackageName);
@@ -506,6 +529,8 @@
         @Override // Binder call
         public void cancelAuthenticationFromService(final int sensorId, final IBinder token,
                 final String opPackageName, final long requestId) {
+            super.cancelAuthenticationFromService_enforcePermission();
+
             Slog.d(TAG, "cancelAuthenticationFromService, sensorId: " + sensorId);
 
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
@@ -521,6 +546,8 @@
         @Override // Binder call
         public void remove(final IBinder token, final int fingerId, final int userId,
                 final IFingerprintServiceReceiver receiver, final String opPackageName) {
+            super.remove_enforcePermission();
+
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for remove");
@@ -535,6 +562,8 @@
         public void removeAll(final IBinder token, final int userId,
                 final IFingerprintServiceReceiver receiver, final String opPackageName) {
 
+            super.removeAll_enforcePermission();
+
             final FingerprintServiceReceiver internalReceiver = new FingerprintServiceReceiver() {
                 int sensorsFinishedRemoving = 0;
                 final int numSensors = getSensorPropertiesInternal(
@@ -567,6 +596,8 @@
         @Override // Binder call
         public void addLockoutResetCallback(final IBiometricServiceLockoutResetCallback callback,
                 final String opPackageName) {
+            super.addLockoutResetCallback_enforcePermission();
+
             mLockoutResetDispatcher.addCallback(callback, opPackageName);
         }
 
@@ -650,6 +681,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean isHardwareDetected(int sensorId, String opPackageName) {
+            super.isHardwareDetected_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for isHardwareDetected, caller: " + opPackageName);
@@ -662,6 +695,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override // Binder call
         public void rename(final int fingerId, final int userId, final String name) {
+            super.rename_enforcePermission();
+
             if (!Utils.isCurrentUserOrProfile(getContext(), userId)) {
                 return;
             }
@@ -717,6 +752,8 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         public boolean hasEnrolledFingerprints(int sensorId, int userId, String opPackageName) {
+            super.hasEnrolledFingerprints_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for hasEnrolledFingerprints, caller: " + opPackageName);
@@ -729,6 +766,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) {
+            super.getLockoutModeForUser_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getLockoutModeForUser");
@@ -741,6 +780,8 @@
         @Override
         public void invalidateAuthenticatorId(int sensorId, int userId,
                 IInvalidationCallback callback) {
+            super.invalidateAuthenticatorId_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for invalidateAuthenticatorId");
@@ -752,6 +793,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public long getAuthenticatorId(int sensorId, int userId) {
+            super.getAuthenticatorId_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getAuthenticatorId");
@@ -764,6 +807,8 @@
         @Override // Binder call
         public void resetLockout(IBinder token, int sensorId, int userId,
                 @Nullable byte[] hardwareAuthToken, String opPackageName) {
+            super.resetLockout_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName);
@@ -776,18 +821,24 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override
         public boolean isClientActive() {
+            super.isClientActive_enforcePermission();
+
             return mGestureAvailabilityDispatcher.isAnySensorActive();
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override
         public void addClientActiveCallback(IFingerprintClientActiveCallback callback) {
+            super.addClientActiveCallback_enforcePermission();
+
             mGestureAvailabilityDispatcher.registerCallback(callback);
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override
         public void removeClientActiveCallback(IFingerprintClientActiveCallback callback) {
+            super.removeClientActiveCallback_enforcePermission();
+
             mGestureAvailabilityDispatcher.removeCallback(callback);
         }
 
@@ -795,6 +846,8 @@
         @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
+            super.registerAuthenticators_enforcePermission();
+
             mRegistry.registerAll(() -> {
                 final List<ServiceProvider> providers = new ArrayList<>();
                 providers.addAll(getHidlProviders(hidlSensors));
@@ -813,12 +866,16 @@
         @Override
         public void addAuthenticatorsRegisteredCallback(
                 IFingerprintAuthenticatorsRegisteredCallback callback) {
+            super.addAuthenticatorsRegisteredCallback_enforcePermission();
+
             mRegistry.addAllRegisteredCallback(callback);
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
+            super.registerBiometricStateListener_enforcePermission();
+
             mBiometricStateCallback.registerBiometricStateListener(listener);
         }
 
@@ -826,6 +883,8 @@
         @Override
         public void onPointerDown(long requestId, int sensorId, int x, int y,
                 float minor, float major) {
+            super.onPointerDown_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onFingerDown, sensorId: " + sensorId);
@@ -837,6 +896,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onPointerUp(long requestId, int sensorId) {
+            super.onPointerUp_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onFingerUp, sensorId: " + sensorId);
@@ -848,6 +909,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onUiReady(long requestId, int sensorId) {
+            super.onUiReady_enforcePermission();
+
             final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onUiReady, sensorId: " + sensorId);
@@ -859,6 +922,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller) {
+            super.setUdfpsOverlayController_enforcePermission();
+
             for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.setUdfpsOverlayController(controller);
             }
@@ -867,6 +932,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void setSidefpsController(@NonNull ISidefpsController controller) {
+            super.setSidefpsController_enforcePermission();
+
             for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.setSidefpsController(controller);
             }
@@ -874,11 +941,37 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
+        public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+            super.setUdfpsOverlay_enforcePermission();
+
+            for (ServiceProvider provider : mRegistry.getProviders()) {
+                provider.setUdfpsOverlay(controller);
+            }
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
         public void onPowerPressed() {
+            super.onPowerPressed_enforcePermission();
+
             for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.onPowerPressed();
             }
         }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            super.scheduleWatchdog_enforcePermission();
+
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
     };
 
     public FingerprintService(Context context) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
index ae173f7..b1a9ef1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
@@ -19,10 +19,10 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.fingerprint.Fingerprint;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.biometrics.sensors.BiometricUserState;
 
 import org.xmlpull.v1.XmlPullParser;
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 9075e7e..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();
 
     /**
@@ -140,4 +147,10 @@
     @NonNull
     ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
index 4181b99..135eccf 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
@@ -135,6 +135,8 @@
     @Override
     public void setTestHalEnabled(boolean enabled) {
 
+        super.setTestHalEnabled_enforcePermission();
+
         mProvider.setTestHalEnabled(enabled);
         mSensor.setTestHalEnabled(enabled);
     }
@@ -143,6 +145,8 @@
     @Override
     public void startEnroll(int userId) {
 
+        super.startEnroll_enforcePermission();
+
         mProvider.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver,
                 mContext.getOpPackageName(), FingerprintManager.ENROLL_ENROLL);
     }
@@ -151,6 +155,8 @@
     @Override
     public void finishEnroll(int userId) {
 
+        super.finishEnroll_enforcePermission();
+
         int nextRandomId = mRandom.nextInt();
         while (mEnrollmentIds.contains(nextRandomId)) {
             nextRandomId = mRandom.nextInt();
@@ -166,6 +172,8 @@
     public void acceptAuthentication(int userId)  {
 
         // Fake authentication with any of the existing fingers
+        super.acceptAuthentication_enforcePermission();
+
         List<Fingerprint> fingerprints = FingerprintUtils.getInstance(mSensorId)
                 .getBiometricsForUser(mContext, userId);
         if (fingerprints.isEmpty()) {
@@ -181,6 +189,8 @@
     @Override
     public void rejectAuthentication(int userId)  {
 
+        super.rejectAuthentication_enforcePermission();
+
         mSensor.getSessionForUser(userId).getHalSessionCallback().onAuthenticationFailed();
     }
 
@@ -188,6 +198,8 @@
     @Override
     public void notifyAcquired(int userId, int acquireInfo)  {
 
+        super.notifyAcquired_enforcePermission();
+
         mSensor.getSessionForUser(userId).getHalSessionCallback()
                 .onAcquired((byte) acquireInfo, 0 /* vendorCode */);
     }
@@ -196,6 +208,8 @@
     @Override
     public void notifyError(int userId, int errorCode)  {
 
+        super.notifyError_enforcePermission();
+
         mSensor.getSessionForUser(userId).getHalSessionCallback().onError((byte) errorCode,
                 0 /* vendorCode */);
     }
@@ -204,6 +218,8 @@
     @Override
     public void cleanupInternalState(int userId)  {
 
+        super.cleanupInternalState_enforcePermission();
+
         Slog.d(TAG, "cleanupInternalState: " + userId);
         mProvider.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() {
             @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index f599acac..893c9b1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 
 import android.annotation.NonNull;
@@ -32,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;
@@ -54,11 +56,12 @@
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutConsumer;
-import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.function.Supplier;
 
@@ -69,12 +72,9 @@
 class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
         implements Udfps, LockoutConsumer, PowerPressHandler {
     private static final String TAG = "FingerprintAuthenticationClient";
-    private static final int MESSAGE_IGNORE_AUTH = 1;
     private static final int MESSAGE_AUTH_SUCCESS = 2;
     private static final int MESSAGE_FINGER_UP = 3;
     @NonNull
-    private final LockoutCache mLockoutCache;
-    @NonNull
     private final SensorOverlays mSensorOverlays;
     @NonNull
     private final FingerprintSensorPropertiesInternal mSensorProps;
@@ -83,7 +83,6 @@
     private final Handler mHandler;
     private final int mSkipWaitForPowerAcquireMessage;
     private final int mSkipWaitForPowerVendorAcquireMessage;
-    private final int mBiometricStrength;
     private final long mFingerUpIgnoresPower = 500;
     private final AuthSessionCoordinator mAuthSessionCoordinator;
     @Nullable
@@ -92,7 +91,10 @@
     private long mWaitForAuthKeyguard;
     private long mWaitForAuthBp;
     private long mIgnoreAuthFor;
+    private long mSideFpsLastAcquireStartTime;
     private Runnable mAuthSuccessRunnable;
+    private final Clock mClock;
+    private boolean mDidFinishSfps;
 
     FingerprintAuthenticationClient(
             @NonNull Context context,
@@ -114,10 +116,12 @@
             @NonNull LockoutCache lockoutCache,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
-            @Authenticators.Types int biometricStrength) {
+            @Authenticators.Types int biometricStrength,
+            @NonNull Clock clock) {
         super(
                 context,
                 lazyDaemon,
@@ -134,13 +138,14 @@
                 biometricContext,
                 isStrongBiometric,
                 taskStackListener,
-                lockoutCache,
+                null /* lockoutCache */,
                 allowBackgroundAuthentication,
                 false /* shouldVibrate */,
-                false /* isKeyguardBypassEnabled */);
+                false /* isKeyguardBypassEnabled */,
+                biometricStrength);
         setRequestId(requestId);
-        mLockoutCache = lockoutCache;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
         mHandler = handler;
@@ -159,8 +164,9 @@
         mSkipWaitForPowerVendorAcquireMessage =
                 context.getResources().getInteger(
                         R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage);
-        mBiometricStrength = biometricStrength;
         mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator();
+        mSideFpsLastAcquireStartTime = -1;
+        mClock = clock;
 
         if (mSensorProps.isAnySidefpsType()) {
             if (Build.isDebuggable()) {
@@ -187,8 +193,6 @@
         } else {
             mState = STATE_STARTED;
         }
-        mAuthSessionCoordinator.authStartedFor(getTargetUserId(), getSensorId(),
-                getRequestId());
     }
 
     @NonNull
@@ -200,10 +204,9 @@
 
     @Override
     protected void handleLifecycleAfterAuth(boolean authenticated) {
-        if (authenticated) {
+        if (authenticated && !mDidFinishSfps) {
             mCallback.onClientFinished(this, true /* success */);
-            mAuthSessionCoordinator.authenticatedFor(
-                    getTargetUserId(), mBiometricStrength, getSensorId(), getRequestId());
+            mDidFinishSfps = true;
         }
     }
 
@@ -239,15 +242,15 @@
                 () -> {
                     long delay = 0;
                     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);
-                            return;
-                        }
                         delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
-                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: "
-                                + delay + "ms");
+
+                        if (mSideFpsLastAcquireStartTime != -1) {
+                            delay = Math.max(0,
+                                    delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
+                        }
+
+                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
+                                + "waiting for power until: " + delay + "ms");
                     }
 
                     if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
@@ -271,13 +274,15 @@
         mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
         super.onAcquired(acquiredInfo, vendorCode);
         if (mSensorProps.isAnySidefpsType()) {
+            if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+                mSideFpsLastAcquireStartTime = mClock.millis();
+            }
             final boolean shouldLookForVendor =
                     mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
             final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
             final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
             final boolean ignorePowerPress =
-                    (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor
-                            && acquireMessageMatch && vendorMessageMatch);
+                    acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
 
             if (ignorePowerPress) {
                 Slog.d(TAG, "(sideFPS) onFingerUp");
@@ -293,6 +298,8 @@
                 });
             }
         }
+        PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
+        pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation());
 
     }
 
@@ -305,8 +312,6 @@
         }
 
         mSensorOverlays.hide(getSensorId());
-        mAuthSessionCoordinator.authEndedFor(getTargetUserId(), mBiometricStrength, getSensorId(),
-                getRequestId());
     }
 
     @Override
@@ -444,7 +449,8 @@
 
     @Override
     public void onLockoutTimed(long durationMillis) {
-        mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_TIMED);
+        mAuthSessionCoordinator.lockOutTimed(getTargetUserId(), getSensorStrength(), getSensorId(),
+                durationMillis, getRequestId());
         // Lockout metrics are logged as an error code.
         final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
         getLogger()
@@ -455,6 +461,9 @@
                         0 /* vendorCode */,
                         getTargetUserId());
 
+        PerformanceTracker.getInstanceForSensorId(getSensorId())
+                .incrementTimedLockoutForUser(getTargetUserId());
+
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
         } catch (RemoteException e) {
@@ -463,13 +472,12 @@
 
         mSensorOverlays.hide(getSensorId());
         mCallback.onClientFinished(this, false /* success */);
-        mAuthSessionCoordinator.lockOutTimed(getTargetUserId(), mBiometricStrength, getSensorId(),
-                durationMillis, getRequestId());
     }
 
     @Override
     public void onLockoutPermanent() {
-        mLockoutCache.setLockoutModeForUser(getTargetUserId(), LockoutTracker.LOCKOUT_PERMANENT);
+        mAuthSessionCoordinator.lockedOutFor(getTargetUserId(), getSensorStrength(), getSensorId(),
+                getRequestId());
         // Lockout metrics are logged as an error code.
         final int error = BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
         getLogger()
@@ -480,6 +488,9 @@
                         0 /* vendorCode */,
                         getTargetUserId());
 
+        PerformanceTracker.getInstanceForSensorId(getSensorId())
+                .incrementPermanentLockoutForUser(getTargetUserId());
+
         try {
             getListener().onError(getSensorId(), getCookie(), error, 0 /* vendorCode */);
         } catch (RemoteException e) {
@@ -488,8 +499,6 @@
 
         mSensorOverlays.hide(getSensorId());
         mCallback.onClientFinished(this, false /* success */);
-        mAuthSessionCoordinator.lockedOutFor(getTargetUserId(), mBiometricStrength, getSensorId(),
-                getRequestId());
     }
 
     @Override
@@ -497,18 +506,17 @@
         if (mSensorProps.isAnySidefpsType()) {
             Slog.i(TAG, "(sideFPS): onPowerPressed");
             mHandler.post(() -> {
-                if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) {
-                    Slog.i(TAG, "(sideFPS): Ignoring auth in queue");
-                    mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
-                    // Do not call onError() as that will send an additional callback to coex.
-                    onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
-                    mAuthSessionCoordinator.authEndedFor(getTargetUserId(),
-                            mBiometricStrength, getSensorId(), getRequestId());
+                if (mDidFinishSfps) {
+                    return;
                 }
-                mHandler.removeMessages(MESSAGE_IGNORE_AUTH);
-                mHandler.postDelayed(() -> {
-                }, MESSAGE_IGNORE_AUTH, mIgnoreAuthFor);
-
+                Slog.i(TAG, "(sideFPS): finishing auth");
+                // Ignore auths after a power has been detected
+                mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
+                // Do not call onError() as that will send an additional callback to coex.
+                mDidFinishSfps = true;
+                onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
+                stopHalOperation();
+                mSensorOverlays.hide(getSensorId());
             });
         }
     }
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 774aff1..b42b1c6 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;
@@ -47,6 +48,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -56,8 +58,10 @@
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -91,21 +95,31 @@
 
     private boolean mTestHalEnabled;
 
-    @NonNull private final Context mContext;
-    @NonNull private final BiometricStateCallback mBiometricStateCallback;
-    @NonNull private final String mHalInstanceName;
-    @NonNull @VisibleForTesting
+    @NonNull
+    @VisibleForTesting
     final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports
-    @NonNull private final Handler mHandler;
-    @NonNull private final LockoutResetDispatcher mLockoutResetDispatcher;
-    @NonNull private final ActivityTaskManager mActivityTaskManager;
-    @NonNull private final BiometricTaskStackListener mTaskStackListener;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final BiometricStateCallback mBiometricStateCallback;
+    @NonNull
+    private final String mHalInstanceName;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final LockoutResetDispatcher mLockoutResetDispatcher;
+    @NonNull
+    private final ActivityTaskManager mActivityTaskManager;
+    @NonNull
+    private final BiometricTaskStackListener mTaskStackListener;
     // for requests that do not use biometric prompt
     @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
     @NonNull private final BiometricContext mBiometricContext;
     @Nullable private IFingerprint mDaemon;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
+    @Nullable private IUdfpsOverlay mUdfpsOverlay;
+    private AuthSessionCoordinator mAuthSessionCoordinator;
 
     private final class BiometricTaskStackListener extends TaskStackListener {
         @Override
@@ -150,6 +164,7 @@
         mActivityTaskManager = ActivityTaskManager.getInstance();
         mTaskStackListener = new BiometricTaskStackListener();
         mBiometricContext = biometricContext;
+        mAuthSessionCoordinator = mBiometricContext.getAuthSessionCoordinator();
 
         final List<SensorLocationInternal> workaroundLocations = getWorkaroundSensorProps(context);
 
@@ -175,11 +190,11 @@
                             true /* resetLockoutRequiresHardwareAuthToken */,
                             !workaroundLocations.isEmpty() ? workaroundLocations :
                                     Arrays.stream(prop.sensorLocations).map(location ->
-                                            new SensorLocationInternal(
-                                                    location.display,
-                                                    location.sensorLocationX,
-                                                    location.sensorLocationY,
-                                                    location.sensorRadius))
+                                                    new SensorLocationInternal(
+                                                            location.display,
+                                                            location.sensorLocationX,
+                                                            location.sensorLocationY,
+                                                            location.sensorRadius))
                                             .collect(Collectors.toList()));
             final Sensor sensor = new Sensor(getTag() + "/" + sensorId, this, mContext, mHandler,
                     internalProp, lockoutResetDispatcher, gestureAvailabilityDispatcher,
@@ -343,7 +358,7 @@
                             mSensors.get(sensorId).getLazySession(), token,
                             new ClientMonitorCallbackConverter(receiver), userId, opPackageName,
                             sensorId, createLogger(BiometricsProtoEnums.ACTION_UNKNOWN,
-                                BiometricsProtoEnums.CLIENT_UNKNOWN),
+                            BiometricsProtoEnums.CLIENT_UNKNOWN),
                             mBiometricContext);
             scheduleForSensor(sensorId, client);
         });
@@ -381,29 +396,20 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
                     mSensors.get(sensorId).getSensorProperties(),
-                    mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason);
-            scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
-
-                @Override
-                public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
-                    mBiometricStateCallback.onClientStarted(clientMonitor);
-                }
-
-                @Override
-                public void onBiometricAction(int action) {
-                    mBiometricStateCallback.onBiometricAction(action);
-                }
-
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    maxTemplatesPerUser, enrollReason);
+            scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+                    mBiometricStateCallback, new ClientMonitorCallback() {
                 @Override
                 public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
                         boolean success) {
-                    mBiometricStateCallback.onClientFinished(clientMonitor, success);
+                    ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
                     if (success) {
                         scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
                         scheduleInvalidationRequest(sensorId, userId);
                     }
                 }
-            });
+            }));
         });
         return id;
     }
@@ -426,7 +432,7 @@
                     opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext,
-                    mUdfpsOverlayController, isStrongBiometric);
+                    mUdfpsOverlayController, mUdfpsOverlay, isStrongBiometric);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
 
@@ -447,10 +453,33 @@
                     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));
-            scheduleForSensor(sensorId, client, mBiometricStateCallback);
+                    Utils.getCurrentStrength(sensorId),
+                    SystemClock.elapsedRealtimeClock());
+            scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
+
+                @Override
+                public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
+                    mBiometricStateCallback.onClientStarted(clientMonitor);
+                    mAuthSessionCoordinator.authStartedFor(userId, sensorId, requestId);
+                }
+
+                @Override
+                public void onBiometricAction(int action) {
+                    mBiometricStateCallback.onBiometricAction(action);
+                }
+
+                @Override
+                public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+                        boolean success) {
+                    mBiometricStateCallback.onClientFinished(clientMonitor, success);
+                    mAuthSessionCoordinator.authEndedFor(userId, Utils.getCurrentStrength(sensorId),
+                            sensorId, requestId, success);
+                }
+            });
+
         });
     }
 
@@ -588,7 +617,8 @@
 
     @Override
     public int getLockoutModeForUser(int sensorId, int userId) {
-        return mSensors.get(sensorId).getLockoutCache().getLockoutModeForUser(userId);
+        return mBiometricContext.getAuthSessionCoordinator().getLockoutStateFor(userId,
+                Utils.getCurrentStrength(sensorId));
     }
 
     @Override
@@ -656,6 +686,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)) {
@@ -777,4 +812,14 @@
         }
         return null;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for fingerprint");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
index 22f504c..0b2421b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
@@ -93,9 +93,9 @@
     void onLockoutCleared() {
         resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache,
                 mLockoutResetDispatcher);
+        mCallback.onClientFinished(this, true /* success */);
         getBiometricContext().getAuthSessionCoordinator()
                 .resetLockoutFor(getTargetUserId(), mBiometricStrength, getRequestId());
-        mCallback.onClientFinished(this, true /* success */);
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java
index 682c005..86a9f79 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java
@@ -136,6 +136,8 @@
     @Override
     public void setTestHalEnabled(boolean enabled) {
 
+        super.setTestHalEnabled_enforcePermission();
+
         mFingerprint21.setTestHalEnabled(enabled);
     }
 
@@ -143,6 +145,8 @@
     @Override
     public void startEnroll(int userId) {
 
+        super.startEnroll_enforcePermission();
+
         mFingerprint21.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver,
                 mContext.getOpPackageName(), FingerprintManager.ENROLL_ENROLL);
     }
@@ -151,6 +155,8 @@
     @Override
     public void finishEnroll(int userId) {
 
+        super.finishEnroll_enforcePermission();
+
         int nextRandomId = mRandom.nextInt();
         while (mEnrollmentIds.contains(nextRandomId)) {
             nextRandomId = mRandom.nextInt();
@@ -166,6 +172,8 @@
     public void acceptAuthentication(int userId)  {
 
         // Fake authentication with any of the existing fingers
+        super.acceptAuthentication_enforcePermission();
+
         List<Fingerprint> fingerprints = FingerprintUtils.getLegacyInstance(mSensorId)
                 .getBiometricsForUser(mContext, userId);
         if (fingerprints.isEmpty()) {
@@ -181,6 +189,8 @@
     @Override
     public void rejectAuthentication(int userId)  {
 
+        super.rejectAuthentication_enforcePermission();
+
         mHalResultController.onAuthenticated(0 /* deviceId */, 0 /* fingerId */, userId, null);
     }
 
@@ -188,6 +198,8 @@
     @Override
     public void notifyAcquired(int userId, int acquireInfo)  {
 
+        super.notifyAcquired_enforcePermission();
+
         mHalResultController.onAcquired(0 /* deviceId */, acquireInfo, 0 /* vendorCode */);
     }
 
@@ -195,6 +207,8 @@
     @Override
     public void notifyError(int userId, int errorCode)  {
 
+        super.notifyError_enforcePermission();
+
         mHalResultController.onError(0 /* deviceId */, errorCode, 0 /* vendorCode */);
     }
 
@@ -202,6 +216,8 @@
     @Override
     public void cleanupInternalState(int userId)  {
 
+        super.cleanupInternalState_enforcePermission();
+
         mFingerprint21.scheduleInternalCleanup(mSensorId, userId, new ClientMonitorCallback() {
             @Override
             public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
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..1f30363 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,8 +666,9 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mLockoutTracker,
-                    mUdfpsOverlayController, mSidefpsController,
-                    allowBackgroundAuthentication, mSensorProperties);
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    allowBackgroundAuthentication, mSensorProperties,
+                    Utils.getCurrentStrength(mSensorId));
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
     }
@@ -853,6 +856,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..089317e 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
@@ -23,9 +23,11 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricManager.Authenticators;
 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;
@@ -41,6 +43,7 @@
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
 import com.android.server.biometrics.sensors.LockoutTracker;
+import com.android.server.biometrics.sensors.PerformanceTracker;
 import com.android.server.biometrics.sensors.SensorOverlays;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 import com.android.server.biometrics.sensors.fingerprint.UdfpsHelper;
@@ -76,15 +79,19 @@
             @NonNull LockoutFrameworkImpl lockoutTracker,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
-            @NonNull FingerprintSensorPropertiesInternal sensorProps) {
+            @NonNull FingerprintSensorPropertiesInternal sensorProps,
+            @Authenticators.Types int sensorStrength) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
                 isStrongBiometric, taskStackListener, lockoutTracker, allowBackgroundAuthentication,
-                false /* shouldVibrate */, false /* isKeyguardBypassEnabled */);
+                false /* shouldVibrate */, false /* isKeyguardBypassEnabled */,
+                sensorStrength);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
     }
@@ -163,6 +170,18 @@
     }
 
     @Override
+    public void onAcquired(int acquiredInfo, int vendorCode) {
+        super.onAcquired(acquiredInfo, vendorCode);
+
+        @LockoutTracker.LockoutMode final int lockoutMode =
+                getLockoutTracker().getLockoutModeForUser(getTargetUserId());
+        if (lockoutMode == LockoutTracker.LOCKOUT_NONE) {
+            PerformanceTracker pt = PerformanceTracker.getInstanceForSensorId(getSensorId());
+            pt.incrementAcquireForUser(getTargetUserId(), isCryptoOperation());
+        }
+    }
+
+    @Override
     public boolean wasUserDetected() {
         // TODO: Update if it needs to be used for fingerprint, i.e. success/reject, error_timeout
         return false;
@@ -171,7 +190,17 @@
     @Override
     public @LockoutTracker.LockoutMode int handleFailedAttempt(int userId) {
         mLockoutFrameworkImpl.addFailedAttemptForUser(userId);
-        return super.handleFailedAttempt(userId);
+        @LockoutTracker.LockoutMode final int lockoutMode =
+                getLockoutTracker().getLockoutModeForUser(userId);
+        final PerformanceTracker performanceTracker =
+                PerformanceTracker.getInstanceForSensorId(getSensorId());
+        if (lockoutMode == LockoutTracker.LOCKOUT_PERMANENT) {
+            performanceTracker.incrementPermanentLockoutForUser(userId);
+        } else if (lockoutMode == LockoutTracker.LOCKOUT_TIMED) {
+            performanceTracker.incrementTimedLockoutForUser(userId);
+        }
+
+        return lockoutMode;
     }
 
     @Override
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/biometrics/sensors/iris/IrisService.java b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java
index ff1e762..35ea36c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/iris/IrisService.java
@@ -63,6 +63,8 @@
             // to wait, and some of the operations below might take a significant amount of time to
             // complete (calls to the HALs). To avoid blocking the rest of system server we put
             // this on a background thread.
+            super.registerAuthenticators_enforcePermission();
+
             final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
                     true /* allowIo */);
             thread.start();
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 0770062..6a01042 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -29,6 +29,8 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
@@ -47,7 +49,7 @@
     private static final List<String> SERVICE_NAMES = Arrays.asList(
             IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
 
-    private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioServiceImpl mHalAidl;
     private final BroadcastRadioService mService;
 
     /**
@@ -65,10 +67,15 @@
     }
 
     IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
+        this(service, new BroadcastRadioServiceImpl(serviceList));
         Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
-        mService = Objects.requireNonNull(service);
-        mHalAidl =
-                new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
+    }
+
+    @VisibleForTesting
+    IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHalAidl = Objects.requireNonNull(halAidl,
+                "Broadcast radio service implementation for AIDL HAL cannot be null");
     }
 
     @Override
@@ -96,8 +103,8 @@
         if (isDebugEnabled()) {
             Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         return mHalAidl.addAnnouncementListener(enabledTypes, listener);
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 28b6d02..a8e4034 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
 
 import java.io.FileDescriptor;
@@ -53,7 +54,7 @@
     private final List<RadioManager.ModuleProperties> mV1Modules;
 
     IRadioServiceHidlImpl(BroadcastRadioService service) {
-        mService = Objects.requireNonNull(service);
+        mService = Objects.requireNonNull(service, "broadcast radio service cannot be null");
         mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
@@ -61,6 +62,18 @@
                 max.isPresent() ? max.getAsInt() + 1 : 0, mLock);
     }
 
+    @VisibleForTesting
+    IRadioServiceHidlImpl(BroadcastRadioService service,
+            com.android.server.broadcastradio.hal1.BroadcastRadioService hal1,
+            com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHal1 = Objects.requireNonNull(hal1,
+                "Broadcast radio service implementation for HIDL 1 HAL cannot be null");
+        mV1Modules = mHal1.loadModules();
+        mHal2 = Objects.requireNonNull(hal2,
+                "Broadcast radio service implementation for HIDL 2 HAL cannot be null");
+    }
+
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
@@ -95,8 +108,8 @@
         if (isDebugEnabled()) {
             Slog.d(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index 71ba296..4fcfea2 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -169,7 +169,7 @@
         synchronized (mLock) {
             List<RadioManager.ModuleProperties> moduleList = new ArrayList<>(mModules.size());
             for (int i = 0; i < mModules.size(); i++) {
-                moduleList.add(mModules.valueAt(i).mProperties);
+                moduleList.add(mModules.valueAt(i).getProperties());
             }
             return moduleList;
         }
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index c6dc431..d4c7242 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -54,11 +54,11 @@
     private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
 
     private final IBroadcastRadio mService;
-    public final RadioManager.ModuleProperties mProperties;
 
     private final Object mLock;
     private final Handler mHandler;
     private final RadioLogger mLogger;
+    private final RadioManager.ModuleProperties mProperties;
 
     /**
      * Tracks antenna state reported by HAL (if any).
@@ -217,6 +217,10 @@
         return mService;
     }
 
+    public RadioManager.ModuleProperties getProperties() {
+        return mProperties;
+    }
+
     void setInternalHalCallback() throws RemoteException {
         synchronized (mLock) {
             mService.setTunerCallback(mHalTunerCallback);
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 5605737..4c37609 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -33,6 +33,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.Collection;
 import java.util.HashMap;
@@ -132,10 +133,22 @@
         }
     }
 
+    @VisibleForTesting
+    BroadcastRadioService(int nextModuleId, Object lock, IServiceManager manager) {
+        mNextModuleId = nextModuleId;
+        mLock = lock;
+        Objects.requireNonNull(manager, "Service manager cannot be null");
+        try {
+            manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener);
+        } catch (RemoteException ex) {
+            Slog.e(TAG, "Failed to register for service notifications: ", ex);
+        }
+    }
+
     public @NonNull Collection<RadioManager.ModuleProperties> listModules() {
         Slog.v(TAG, "List HIDL 2.0 modules");
         synchronized (mLock) {
-            return mModules.values().stream().map(module -> module.mProperties)
+            return mModules.values().stream().map(module -> module.getProperties())
                     .collect(Collectors.toList());
         }
     }
@@ -154,7 +167,7 @@
 
     public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
         boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
-        Slog.v(TAG, "Open HIDL 2.0 session");
+        Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
         Objects.requireNonNull(callback);
 
         if (!withAudio) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index 726cdc3..3daf1db 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -52,8 +52,13 @@
 import java.util.stream.Collectors;
 
 class Convert {
+
     private static final String TAG = "BcRadio2Srv.convert";
 
+    private Convert() {
+        throw new UnsupportedOperationException("Convert class is noninstantiable");
+    }
+
     static void throwOnError(String action, int result) {
         switch (result) {
             case Result.OK:
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java b/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java
index a9d8054..a6cf72c 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Mutable.java
@@ -34,13 +34,4 @@
     public Mutable() {
         value = null;
     }
-
-    /**
-     * Initialize value with specific value.
-     *
-     * @param value initial value.
-     */
-    public Mutable(E value) {
-        this.value = value;
-    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index 0a23e38..5913e068 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -58,7 +58,7 @@
     private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
 
     @NonNull private final IBroadcastRadio mService;
-    @NonNull public final RadioManager.ModuleProperties mProperties;
+    @NonNull private final RadioManager.ModuleProperties mProperties;
 
     private final Object mLock;
     @NonNull private final Handler mHandler;
@@ -177,6 +177,10 @@
         return mService;
     }
 
+    public RadioManager.ModuleProperties getProperties() {
+        return mProperties;
+    }
+
     public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb)
             throws RemoteException {
         mEventLogger.logRadioEvent("Open TunerSession");
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Utils.java b/services/core/java/com/android/server/broadcastradio/hal2/Utils.java
index 384c9ba..188c25d 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Utils.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Utils.java
@@ -25,9 +25,14 @@
     AM_LW,
     AM_MW,
     AM_SW,
-};
+}
 
 class Utils {
+
+    private Utils() {
+        throw new UnsupportedOperationException("Utils class is noninstantiable");
+    }
+
     private static final String TAG = "BcRadio2Srv.utils";
 
     static FrequencyBand getBand(int freq) {
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 11eb782..b882c47 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -841,6 +841,7 @@
                     streamProtos[i].histogramCounts = streamStats.getHistogramCounts();
                     streamProtos[i].dynamicRangeProfile = streamStats.getDynamicRangeProfile();
                     streamProtos[i].streamUseCase = streamStats.getStreamUseCase();
+                    streamProtos[i].colorSpace = streamStats.getColorSpace();
 
                     if (CameraServiceProxy.DEBUG) {
                         String histogramTypeName =
@@ -863,7 +864,8 @@
                                 + ", histogramCounts "
                                 + Arrays.toString(streamProtos[i].histogramCounts)
                                 + ", dynamicRangeProfile " + streamProtos[i].dynamicRangeProfile
-                                + ", streamUseCase " + streamProtos[i].streamUseCase);
+                                + ", streamUseCase " + streamProtos[i].streamUseCase
+                                + ", colorSpace " + streamProtos[i].colorSpace);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 81b56a3..d2e572f 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.VirtualDeviceParams;
 
 import java.util.Set;
 
@@ -109,4 +110,14 @@
      * Returns true if the {@code displayId} is owned by any virtual device
      */
     public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId);
+
+    /**
+     * Returns the device policy for the given virtual device and policy type.
+     *
+     * <p>In case the virtual device identifier is not valid, or there's no explicitly specified
+     * policy for that device and policy type, then
+     * {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT} is returned.
+     */
+    public abstract @VirtualDeviceParams.DevicePolicy int getDevicePolicy(
+            int deviceId, @VirtualDeviceParams.PolicyType int policyType);
 }
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 387e00f..2c83c6f 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -95,6 +95,8 @@
     @Override
     @EnforcePermission(LOG_COMPAT_CHANGE)
     public void reportChange(long changeId, ApplicationInfo appInfo) {
+        super.reportChange_enforcePermission();
+
         reportChangeInternal(changeId, appInfo.uid, ChangeReporter.STATE_LOGGED);
     }
 
@@ -102,6 +104,8 @@
     @EnforcePermission(LOG_COMPAT_CHANGE)
     public void reportChangeByPackageName(long changeId, String packageName,
             @UserIdInt int userId) {
+        super.reportChangeByPackageName_enforcePermission();
+
         ApplicationInfo appInfo = getApplicationInfo(packageName, userId);
         if (appInfo != null) {
             reportChangeInternal(changeId, appInfo.uid, ChangeReporter.STATE_LOGGED);
@@ -111,6 +115,8 @@
     @Override
     @EnforcePermission(LOG_COMPAT_CHANGE)
     public void reportChangeByUid(long changeId, int uid) {
+        super.reportChangeByUid_enforcePermission();
+
         reportChangeInternal(changeId, uid, ChangeReporter.STATE_LOGGED);
     }
 
@@ -121,6 +127,8 @@
     @Override
     @EnforcePermission(allOf = {LOG_COMPAT_CHANGE, READ_COMPAT_CHANGE_CONFIG})
     public boolean isChangeEnabled(long changeId, ApplicationInfo appInfo) {
+        super.isChangeEnabled_enforcePermission();
+
         return isChangeEnabledInternal(changeId, appInfo);
     }
 
@@ -128,6 +136,8 @@
     @EnforcePermission(allOf = {LOG_COMPAT_CHANGE, READ_COMPAT_CHANGE_CONFIG})
     public boolean isChangeEnabledByPackageName(long changeId, String packageName,
             @UserIdInt int userId) {
+        super.isChangeEnabledByPackageName_enforcePermission();
+
         ApplicationInfo appInfo = getApplicationInfo(packageName, userId);
         if (appInfo == null) {
             return mCompatConfig.willChangeBeEnabled(changeId, packageName);
@@ -138,6 +148,8 @@
     @Override
     @EnforcePermission(allOf = {LOG_COMPAT_CHANGE, READ_COMPAT_CHANGE_CONFIG})
     public boolean isChangeEnabledByUid(long changeId, int uid) {
+        super.isChangeEnabledByUid_enforcePermission();
+
         String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
         if (packages == null || packages.length == 0) {
             return mCompatConfig.defaultChangeIdValue(changeId);
@@ -199,6 +211,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public void setOverrides(CompatibilityChangeConfig overrides, String packageName) {
+        super.setOverrides_enforcePermission();
+
         Map<Long, PackageOverride> overridesMap = new HashMap<>();
         for (long change : overrides.enabledChanges()) {
             overridesMap.put(change, new PackageOverride.Builder().setEnabled(true).build());
@@ -215,6 +229,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public void setOverridesForTest(CompatibilityChangeConfig overrides, String packageName) {
+        super.setOverridesForTest_enforcePermission();
+
         Map<Long, PackageOverride> overridesMap = new HashMap<>();
         for (long change : overrides.enabledChanges()) {
             overridesMap.put(change, new PackageOverride.Builder().setEnabled(true).build());
@@ -231,6 +247,8 @@
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD)
     public void putAllOverridesOnReleaseBuilds(
             CompatibilityOverridesByPackageConfig overridesByPackage) {
+        super.putAllOverridesOnReleaseBuilds_enforcePermission();
+
         for (CompatibilityOverrideConfig overrides :
                 overridesByPackage.packageNameToOverrides.values()) {
             checkAllCompatOverridesAreOverridable(overrides.overrides.keySet());
@@ -242,6 +260,8 @@
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD)
     public void putOverridesOnReleaseBuilds(CompatibilityOverrideConfig overrides,
             String packageName) {
+        super.putOverridesOnReleaseBuilds_enforcePermission();
+
         checkAllCompatOverridesAreOverridable(overrides.overrides.keySet());
         mCompatConfig.addPackageOverrides(overrides, packageName, /* skipUnknownChangeIds= */ true);
     }
@@ -249,6 +269,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public int enableTargetSdkChanges(String packageName, int targetSdkVersion) {
+        super.enableTargetSdkChanges_enforcePermission();
+
         int numChanges =
                 mCompatConfig.enableTargetSdkChangesForPackage(packageName, targetSdkVersion);
         killPackage(packageName);
@@ -258,6 +280,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public int disableTargetSdkChanges(String packageName, int targetSdkVersion) {
+        super.disableTargetSdkChanges_enforcePermission();
+
         int numChanges =
                 mCompatConfig.disableTargetSdkChangesForPackage(packageName, targetSdkVersion);
         killPackage(packageName);
@@ -267,6 +291,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public void clearOverrides(String packageName) {
+        super.clearOverrides_enforcePermission();
+
         mCompatConfig.removePackageOverrides(packageName);
         killPackage(packageName);
     }
@@ -274,12 +300,16 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public void clearOverridesForTest(String packageName) {
+        super.clearOverridesForTest_enforcePermission();
+
         mCompatConfig.removePackageOverrides(packageName);
     }
 
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public boolean clearOverride(long changeId, String packageName) {
+        super.clearOverride_enforcePermission();
+
         boolean existed = mCompatConfig.removeOverride(changeId, packageName);
         killPackage(packageName);
         return existed;
@@ -288,6 +318,8 @@
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public boolean clearOverrideForTest(long changeId, String packageName) {
+        super.clearOverrideForTest_enforcePermission();
+
         return mCompatConfig.removeOverride(changeId, packageName);
     }
 
@@ -295,6 +327,8 @@
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD)
     public void removeAllOverridesOnReleaseBuilds(
             CompatibilityOverridesToRemoveByPackageConfig overridesToRemoveByPackage) {
+        super.removeAllOverridesOnReleaseBuilds_enforcePermission();
+
         for (CompatibilityOverridesToRemoveConfig overridesToRemove :
                 overridesToRemoveByPackage.packageNameToOverridesToRemove.values()) {
             checkAllCompatOverridesAreOverridable(overridesToRemove.changeIds);
@@ -307,6 +341,8 @@
     public void removeOverridesOnReleaseBuilds(
             CompatibilityOverridesToRemoveConfig overridesToRemove,
             String packageName) {
+        super.removeOverridesOnReleaseBuilds_enforcePermission();
+
         checkAllCompatOverridesAreOverridable(overridesToRemove.changeIds);
         mCompatConfig.removePackageOverrides(overridesToRemove, packageName);
     }
@@ -314,12 +350,16 @@
     @Override
     @EnforcePermission(allOf = {LOG_COMPAT_CHANGE, READ_COMPAT_CHANGE_CONFIG})
     public CompatibilityChangeConfig getAppConfig(ApplicationInfo appInfo) {
+        super.getAppConfig_enforcePermission();
+
         return mCompatConfig.getAppConfig(appInfo);
     }
 
     @Override
     @EnforcePermission(READ_COMPAT_CHANGE_CONFIG)
     public CompatibilityChangeInfo[] listAllChanges() {
+        super.listAllChanges_enforcePermission();
+
         return mCompatConfig.dumpChanges();
     }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 6795b6b..bc9bc03 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -28,6 +28,7 @@
 import static android.os.PowerWhitelistManager.REASON_VPN;
 import static android.os.UserHandle.PER_USER_RANGE;
 
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
 import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER;
 
 import static java.util.Objects.requireNonNull;
@@ -79,10 +80,12 @@
 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;
 import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.ChildSaProposal;
 import android.net.ipsec.ike.ChildSessionCallback;
 import android.net.ipsec.ike.ChildSessionConfiguration;
 import android.net.ipsec.ike.ChildSessionParams;
@@ -92,6 +95,7 @@
 import android.net.ipsec.ike.IkeSessionConnectionInfo;
 import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
+import android.net.ipsec.ike.exceptions.IkeIOException;
 import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
 import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
@@ -123,6 +127,8 @@
 import android.system.keystore2.KeyPermission;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
 import android.util.Log;
 import android.util.Range;
 
@@ -139,6 +145,7 @@
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.net.BaseNetworkObserver;
+import com.android.server.vcn.util.MtuUtils;
 import com.android.server.vcn.util.PersistableBundleUtils;
 
 import libcore.io.IoUtils;
@@ -151,6 +158,8 @@
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
@@ -164,6 +173,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -226,6 +236,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,12 +291,17 @@
     private final UserManager mUserManager;
 
     private final VpnProfileStore mVpnProfileStore;
+    protected boolean mDataStallSuspected = false;
 
     @VisibleForTesting
     VpnProfileStore getVpnProfileStore() {
         return mVpnProfileStore;
     }
 
+    private static final int MAX_EVENTS_LOGS = 20;
+    private final LocalLog mUnderlyNetworkChanges = new LocalLog(MAX_EVENTS_LOGS);
+    private final LocalLog mVpnManagerEvents = new LocalLog(MAX_EVENTS_LOGS);
+
     /**
      * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This
      * only applies to {@link VpnService} connections.
@@ -522,10 +547,46 @@
                 @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];
+            }
+        }
+
+        /** Gets the MTU of an interface using Java NetworkInterface primitives */
+        public int getJavaNetworkInterfaceMtu(@Nullable String iface, int defaultValue)
+                throws SocketException {
+            if (iface == null) return defaultValue;
+
+            final NetworkInterface networkInterface = NetworkInterface.getByName(iface);
+            return networkInterface == null ? defaultValue : networkInterface.getMTU();
+        }
+
+        /** Calculates the VPN Network's max MTU based on underlying network and configuration */
+        public int calculateVpnMtu(
+                @NonNull List<ChildSaProposal> childProposals,
+                int maxMtu,
+                int underlyingMtu,
+                boolean isIpv4) {
+            return MtuUtils.getMtu(childProposals, maxMtu, underlyingMtu, isIpv4);
+        }
+    }
+
+    @VisibleForTesting
+    interface ValidationStatusCallback {
+        void onValidationStatus(int status);
     }
 
     public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd,
@@ -584,7 +645,8 @@
                 .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                 .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
-                .setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE, null))
+                .setTransportInfo(new VpnTransportInfo(
+                        VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
                 .build();
 
         loadAlwaysOnPackage();
@@ -648,7 +710,8 @@
     private void resetNetworkCapabilities() {
         mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities)
                 .setUids(null)
-                .setTransportInfo(new VpnTransportInfo(VpnManager.TYPE_VPN_NONE, null))
+                .setTransportInfo(new VpnTransportInfo(
+                        VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
                 .build();
     }
 
@@ -786,6 +849,9 @@
             int errorCode, @NonNull final String packageName, @Nullable final String sessionKey,
             @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork,
             @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) {
+        mVpnManagerEvents.log("Event class=" + getVpnManagerEventClassName(errorClass)
+                + ", err=" + getVpnManagerEventErrorName(errorCode) + " for " + packageName
+                + " on session " + sessionKey);
         final Intent intent = buildVpnManagerEventIntent(category, errorClass, errorCode,
                 packageName, sessionKey, profileState, underlyingNetwork, nc, lp);
         return sendEventToVpnManagerApp(intent, packageName);
@@ -1367,6 +1433,11 @@
     }
 
     private LinkProperties makeLinkProperties() {
+        // The design of disabling IPv6 is only enabled for IKEv2 VPN because it needs additional
+        // logic to handle IPv6 only VPN, and the IPv6 only VPN may be restarted when its MTU
+        // is lower than 1280. The logic is controlled by IKEv2VpnRunner, so the design is only
+        // enabled for IKEv2 VPN.
+        final boolean disableIPV6 = (isIkev2VpnRunner() && mConfig.mtu < IPV6_MIN_MTU);
         boolean allowIPv4 = mConfig.allowIPv4;
         boolean allowIPv6 = mConfig.allowIPv6;
 
@@ -1376,6 +1447,7 @@
 
         if (mConfig.addresses != null) {
             for (LinkAddress address : mConfig.addresses) {
+                if (disableIPV6 && address.isIpv6()) continue;
                 lp.addLinkAddress(address);
                 allowIPv4 |= address.getAddress() instanceof Inet4Address;
                 allowIPv6 |= address.getAddress() instanceof Inet6Address;
@@ -1384,8 +1456,9 @@
 
         if (mConfig.routes != null) {
             for (RouteInfo route : mConfig.routes) {
+                final InetAddress address = route.getDestination().getAddress();
+                if (disableIPV6 && address instanceof Inet6Address) continue;
                 lp.addRoute(route);
-                InetAddress address = route.getDestination().getAddress();
 
                 if (route.getType() == RouteInfo.RTN_UNICAST) {
                     allowIPv4 |= address instanceof Inet4Address;
@@ -1396,7 +1469,8 @@
 
         if (mConfig.dnsServers != null) {
             for (String dnsServer : mConfig.dnsServers) {
-                InetAddress address = InetAddresses.parseNumericAddress(dnsServer);
+                final InetAddress address = InetAddresses.parseNumericAddress(dnsServer);
+                if (disableIPV6 && address instanceof Inet6Address) continue;
                 lp.addDnsServer(address);
                 allowIPv4 |= address instanceof Inet4Address;
                 allowIPv6 |= address instanceof Inet6Address;
@@ -1410,7 +1484,7 @@
                     NetworkStackConstants.IPV4_ADDR_ANY, 0), null /*gateway*/,
                     null /*iface*/, RTN_UNREACHABLE));
         }
-        if (!allowIPv6) {
+        if (!allowIPv6 || disableIPV6) {
             lp.addRoute(new RouteInfo(new IpPrefix(
                     NetworkStackConstants.IPV6_ADDR_ANY, 0), null /*gateway*/,
                     null /*iface*/, RTN_UNREACHABLE));
@@ -1460,6 +1534,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
@@ -1490,7 +1569,8 @@
         capsBuilder.setUids(createUserAndRestrictedProfilesRanges(mUserId,
                 mConfig.allowedApplications, mConfig.disallowedApplications));
 
-        capsBuilder.setTransportInfo(new VpnTransportInfo(getActiveVpnType(), mConfig.session));
+        capsBuilder.setTransportInfo(
+                new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass));
 
         // Only apps targeting Q and above can explicitly declare themselves as metered.
         // These VPNs are assumed metered unless they state otherwise.
@@ -1504,10 +1584,11 @@
                 ? Arrays.asList(mConfig.underlyingNetworks) : null);
 
         mNetworkCapabilities = capsBuilder.build();
+        logUnderlyNetworkChanges(mNetworkCapabilities.getUnderlyingNetworks());
         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();
@@ -1531,6 +1612,11 @@
         }
     }
 
+    private void logUnderlyNetworkChanges(List<Network> networks) {
+        mUnderlyNetworkChanges.log("Switch to "
+                + ((networks != null) ? TextUtils.join(", ", networks) : "null"));
+    }
+
     private void agentDisconnect(NetworkAgent networkAgent) {
         if (networkAgent != null) {
             networkAgent.unregister();
@@ -1541,6 +1627,18 @@
         updateState(DetailedState.DISCONNECTED, "agentDisconnect");
     }
 
+    @GuardedBy("this")
+    private void startNewNetworkAgent(NetworkAgent oldNetworkAgent, String reason) {
+        // Initialize the state for a new agent, while keeping the old one connected
+        // in case this new connection fails.
+        mNetworkAgent = null;
+        updateState(DetailedState.CONNECTING, reason);
+        // Bringing up a new NetworkAgent to prevent the data leakage before tearing down the old
+        // NetworkAgent.
+        agentConnect();
+        agentDisconnect(oldNetworkAgent);
+    }
+
     /**
      * Establish a VPN network and return the file descriptor of the VPN interface. This methods
      * returns {@code null} if the application is revoked or not prepared.
@@ -1630,16 +1728,7 @@
                     setUnderlyingNetworks(config.underlyingNetworks);
                 }
             } else {
-                // Initialize the state for a new agent, while keeping the old one connected
-                // in case this new connection fails.
-                mNetworkAgent = null;
-                updateState(DetailedState.CONNECTING, "establish");
-                // Set up forwarding and DNS rules.
-                agentConnect();
-                // Remove the old tun's user forwarding rules
-                // The new tun's user rules have already been added above so they will take over
-                // as rules are deleted. This prevents data leakage as the rules are moved over.
-                agentDisconnect(oldNetworkAgent);
+                startNewNetworkAgent(oldNetworkAgent, "establish");
             }
 
             if (oldConnection != null) {
@@ -2676,6 +2765,17 @@
         void onSessionLost(int token, @Nullable Exception exception);
     }
 
+    private static boolean isIPv6Only(List<LinkAddress> linkAddresses) {
+        boolean hasIPV6 = false;
+        boolean hasIPV4 = false;
+        for (final LinkAddress address : linkAddresses) {
+            hasIPV6 |= address.isIpv6();
+            hasIPV4 |= address.isIpv4();
+        }
+
+        return hasIPV6 && !hasIPV4;
+    }
+
     /**
      * Internal class managing IKEv2/IPsec VPN connectivity
      *
@@ -2723,7 +2823,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 +2850,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
@@ -2838,7 +2946,6 @@
                     ikeConfiguration.isIkeExtensionEnabled(
                             IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE);
             onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo());
-            mRetryCount = 0;
         }
 
         /**
@@ -2881,15 +2988,27 @@
 
             try {
                 final String interfaceName = mTunnelIface.getInterfaceName();
-                final int maxMtu = mProfile.getMaxMtu();
                 final List<LinkAddress> internalAddresses = childConfig.getInternalAddresses();
                 final List<String> dnsAddrStrings = new ArrayList<>();
+                int vpnMtu;
+                vpnMtu = calculateVpnMtu();
+
+                // If the VPN is IPv6 only and its MTU is lower than 1280, mark the network as lost
+                // and send the VpnManager event to the VPN app.
+                if (isIPv6Only(internalAddresses) && vpnMtu < IPV6_MIN_MTU) {
+                    onSessionLost(
+                            token,
+                            new IkeIOException(
+                                    new IOException("No valid addresses for MTU < 1280")));
+                    return;
+                }
 
                 final Collection<RouteInfo> newRoutes = VpnIkev2Utils.getRoutesFromTrafficSelectors(
                         childConfig.getOutboundTrafficSelectors());
                 for (final LinkAddress address : internalAddresses) {
                     mTunnelIface.addAddress(address.getAddress(), address.getPrefixLength());
                 }
+
                 for (InetAddress addr : childConfig.getInternalDnsServers()) {
                     dnsAddrStrings.add(addr.getHostAddress());
                 }
@@ -2907,7 +3026,7 @@
                     if (mVpnRunner != this) return;
 
                     mInterface = interfaceName;
-                    mConfig.mtu = maxMtu;
+                    mConfig.mtu = vpnMtu;
                     mConfig.interfaze = mInterface;
 
                     mConfig.addresses.clear();
@@ -2931,7 +3050,7 @@
                         if (isSettingsVpnLocked()) {
                             prepareStatusIntent();
                         }
-                        agentConnect();
+                        agentConnect(this::onValidationStatus);
                         return; // Link properties are already sent.
                     } else {
                         // Underlying networks also set in agentConnect()
@@ -2946,6 +3065,7 @@
                 }
 
                 doSendLinkProperties(networkAgent, lp);
+                mRetryCount = 0;
             } catch (Exception e) {
                 Log.d(TAG, "Error in ChildOpened for token " + token, e);
                 onSessionLost(token, e);
@@ -3010,12 +3130,54 @@
                     // Ignore stale runner.
                     if (mVpnRunner != this) return;
 
+                    final LinkProperties oldLp = makeLinkProperties();
+
+                    final boolean underlyingNetworkHasChanged =
+                            !Arrays.equals(mConfig.underlyingNetworks, new Network[]{network});
                     mConfig.underlyingNetworks = new Network[] {network};
-                    mNetworkCapabilities =
-                            new NetworkCapabilities.Builder(mNetworkCapabilities)
-                                    .setUnderlyingNetworks(Collections.singletonList(network))
-                                    .build();
-                    doSetUnderlyingNetworks(mNetworkAgent, Collections.singletonList(network));
+                    mConfig.mtu = calculateVpnMtu();
+
+                    final LinkProperties newLp = makeLinkProperties();
+
+                    // If MTU is < 1280, IPv6 addresses will be removed. If there are no addresses
+                    // left (e.g. IPv6-only VPN network), mark VPN as having lost the session.
+                    if (newLp.getLinkAddresses().isEmpty()) {
+                        onSessionLost(
+                                token,
+                                new IkeIOException(
+                                        new IOException("No valid addresses for MTU < 1280")));
+                        return;
+                    }
+
+                    final Set<LinkAddress> removedAddrs = new HashSet<>(oldLp.getLinkAddresses());
+                    removedAddrs.removeAll(newLp.getLinkAddresses());
+
+                    // If addresses were removed despite no IKE config change, IPv6 addresses must
+                    // have been removed due to MTU size. Restart the VPN to ensure all IPv6
+                    // unconnected sockets on the new VPN network are closed and retried on the new
+                    // VPN network.
+                    if (!removedAddrs.isEmpty()) {
+                        startNewNetworkAgent(
+                                mNetworkAgent, "MTU too low for IPv6; restarting network agent");
+
+                        for (LinkAddress removed : removedAddrs) {
+                            mTunnelIface.removeAddress(
+                                    removed.getAddress(), removed.getPrefixLength());
+                        }
+                    } else {
+                        // Put below 3 updates into else block is because agentConnect() will do
+                        // those things, so there is no need to do the redundant work.
+                        if (!newLp.equals(oldLp)) doSendLinkProperties(mNetworkAgent, newLp);
+                        if (underlyingNetworkHasChanged) {
+                            mNetworkCapabilities =
+                                    new NetworkCapabilities.Builder(mNetworkCapabilities)
+                                            .setUnderlyingNetworks(
+                                                    Collections.singletonList(network))
+                                            .build();
+                            doSetUnderlyingNetworks(mNetworkAgent,
+                                    Collections.singletonList(network));
+                        }
+                    }
                 }
 
                 mTunnelIface.setUnderlyingNetwork(network);
@@ -3065,6 +3227,60 @@
             startOrMigrateIkeSession(network);
         }
 
+        @NonNull
+        private IkeSessionParams getIkeSessionParams(@NonNull Network underlyingNetwork) {
+            final IkeTunnelConnectionParams ikeTunConnParams =
+                    mProfile.getIkeTunnelConnectionParams();
+            if (ikeTunConnParams != null) {
+                final IkeSessionParams.Builder builder =
+                        new IkeSessionParams.Builder(ikeTunConnParams.getIkeSessionParams())
+                                .setNetwork(underlyingNetwork);
+                return builder.build();
+            } else {
+                return VpnIkev2Utils.buildIkeSessionParams(mContext, mProfile, underlyingNetwork);
+            }
+        }
+
+        @NonNull
+        private ChildSessionParams getChildSessionParams() {
+            final IkeTunnelConnectionParams ikeTunConnParams =
+                    mProfile.getIkeTunnelConnectionParams();
+            if (ikeTunConnParams != null) {
+                return ikeTunConnParams.getTunnelModeChildSessionParams();
+            } else {
+                return VpnIkev2Utils.buildChildSessionParams(mProfile.getAllowedAlgorithms());
+            }
+        }
+
+        private int calculateVpnMtu() {
+            final Network underlyingNetwork = mIkeConnectionInfo.getNetwork();
+            final LinkProperties lp = mConnectivityManager.getLinkProperties(underlyingNetwork);
+            if (underlyingNetwork == null || lp == null) {
+                // Return the max MTU defined in VpnProfile as the fallback option when there is no
+                // underlying network or LinkProperties is null.
+                return mProfile.getMaxMtu();
+            }
+
+            int underlyingMtu = lp.getMtu();
+
+            // Try to get MTU from kernel if MTU is not set in LinkProperties.
+            if (underlyingMtu == 0) {
+                try {
+                    underlyingMtu = mDeps.getJavaNetworkInterfaceMtu(lp.getInterfaceName(),
+                            mProfile.getMaxMtu());
+                } catch (SocketException e) {
+                    Log.d(TAG, "Got a SocketException when getting MTU from kernel: " + e);
+                    return mProfile.getMaxMtu();
+                }
+            }
+
+            return mDeps.calculateVpnMtu(
+                    getChildSessionParams().getSaProposals(),
+                    mProfile.getMaxMtu(),
+                    underlyingMtu,
+                    mIkeConnectionInfo.getLocalAddress() instanceof Inet4Address);
+        }
+
         /**
          * Start a new IKE session.
          *
@@ -3115,24 +3331,6 @@
                 // (non-default) network, and start the new one.
                 resetIkeState();
 
-                // Get Ike options from IkeTunnelConnectionParams if it's available in the
-                // profile.
-                final IkeTunnelConnectionParams ikeTunConnParams =
-                        mProfile.getIkeTunnelConnectionParams();
-                final IkeSessionParams ikeSessionParams;
-                final ChildSessionParams childSessionParams;
-                if (ikeTunConnParams != null) {
-                    final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(
-                            ikeTunConnParams.getIkeSessionParams()).setNetwork(underlyingNetwork);
-                    ikeSessionParams = builder.build();
-                    childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams();
-                } else {
-                    ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams(
-                            mContext, mProfile, underlyingNetwork);
-                    childSessionParams = VpnIkev2Utils.buildChildSessionParams(
-                            mProfile.getAllowedAlgorithms());
-                }
-
                 // TODO: Remove the need for adding two unused addresses with
                 // IPsec tunnels.
                 final InetAddress address = InetAddress.getLocalHost();
@@ -3150,8 +3348,8 @@
                 mSession =
                         mIkev2SessionCreator.createIkeSession(
                                 mContext,
-                                ikeSessionParams,
-                                childSessionParams,
+                                getIkeSessionParams(underlyingNetwork),
+                                getChildSessionParams(),
                                 mExecutor,
                                 new VpnIkev2Utils.IkeSessionCallbackImpl(
                                         TAG, IkeV2VpnRunner.this, token),
@@ -3165,6 +3363,10 @@
         }
 
         private void scheduleRetryNewIkeSession() {
+            if (mScheduledHandleRetryIkeSessionFuture != null) {
+                Log.d(TAG, "There is a pending retrying task, skip the new retrying task");
+                return;
+            }
             final long retryDelay = mDeps.getNextRetryDelaySeconds(mRetryCount++);
             Log.d(TAG, "Retry new IKE session after " + retryDelay + " seconds.");
             // If the default network is lost during the retry delay, the mActiveNetwork will be
@@ -3200,18 +3402,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
          *
@@ -4154,6 +4390,7 @@
         // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
         //  ConnectivityServiceTest.
         if (SdkLevel.isAtLeastT()) {
+            mVpnManagerEvents.log(packageName + " stopped");
             sendEventToVpnManagerApp(intent, packageName);
         }
     }
@@ -4321,8 +4558,10 @@
     /** Proxy to allow different testing setups */
     // TODO: b/240492694 Remove VpnNetworkAgentWrapper and this method when
     // NetworkAgent#setUnderlyingNetworks can be un-finalized.
-    private static void doSetUnderlyingNetworks(
+    private void doSetUnderlyingNetworks(
             @NonNull NetworkAgent agent, @NonNull List<Network> networks) {
+        logUnderlyNetworkChanges(networks);
+
         if (agent instanceof VpnNetworkAgentWrapper) {
             ((VpnNetworkAgentWrapper) agent).doSetUnderlyingNetworks(networks);
         } else {
@@ -4339,6 +4578,7 @@
     // un-finalized.
     @VisibleForTesting
     public static class VpnNetworkAgentWrapper extends NetworkAgent {
+        private final ValidationStatusCallback mCallback;
         /** Create an VpnNetworkAgentWrapper */
         public VpnNetworkAgentWrapper(
                 @NonNull Context context,
@@ -4348,8 +4588,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 +4613,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);
+            }
+        }
     }
 
     /**
@@ -4431,4 +4680,57 @@
     static Range<Integer> createUidRangeForUser(int userId) {
         return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
     }
+
+    private String getVpnManagerEventClassName(int code) {
+        switch (code) {
+            case VpnManager.ERROR_CLASS_NOT_RECOVERABLE:
+                return "ERROR_CLASS_NOT_RECOVERABLE";
+            case VpnManager.ERROR_CLASS_RECOVERABLE:
+                return "ERROR_CLASS_RECOVERABLE";
+            default:
+                return "UNKNOWN_CLASS";
+        }
+    }
+
+    private String getVpnManagerEventErrorName(int code) {
+        switch (code) {
+            case VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST:
+                return "ERROR_CODE_NETWORK_UNKNOWN_HOST";
+            case VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT:
+                return "ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT";
+            case VpnManager.ERROR_CODE_NETWORK_IO:
+                return "ERROR_CODE_NETWORK_IO";
+            case VpnManager.ERROR_CODE_NETWORK_LOST:
+                return "ERROR_CODE_NETWORK_LOST";
+            default:
+                return "UNKNOWN_ERROR";
+        }
+    }
+
+    /** Dumps VPN state. */
+    public void dump(IndentingPrintWriter pw) {
+        synchronized (Vpn.this) {
+            pw.println("Active package name: " + mPackage);
+            pw.println("Active vpn type: " + getActiveVpnType());
+            pw.println("NetworkCapabilities: " + mNetworkCapabilities);
+            if (isIkev2VpnRunner()) {
+                final IkeV2VpnRunner runner = ((IkeV2VpnRunner) mVpnRunner);
+                pw.println("Token: " + runner.mSessionKey);
+                pw.println("MOBIKE " + (runner.mMobikeEnabled ? "enabled" : "disabled"));
+                if (mDataStallSuspected) pw.println("Data stall suspected");
+                if (runner.mScheduledHandleDataStallFuture != null) {
+                    pw.println("Reset session scheduled");
+                }
+            }
+            pw.println("mUnderlyNetworkChanges (most recent first):");
+            pw.increaseIndent();
+            mUnderlyNetworkChanges.reverseDump(pw);
+            pw.decreaseIndent();
+
+            pw.println("mVpnManagerEvent (most recent first):");
+            pw.increaseIndent();
+            mVpnManagerEvents.reverseDump(pw);
+            pw.decreaseIndent();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/content/SyncAdapterStateFetcher.java b/services/core/java/com/android/server/content/SyncAdapterStateFetcher.java
index ffaf364..108cddc 100644
--- a/services/core/java/com/android/server/content/SyncAdapterStateFetcher.java
+++ b/services/core/java/com/android/server/content/SyncAdapterStateFetcher.java
@@ -17,8 +17,8 @@
 
 import android.app.ActivityManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.pm.UserPackage;
 import android.os.SystemClock;
-import android.util.Pair;
 
 import com.android.server.LocalServices;
 
@@ -26,8 +26,7 @@
 
 class SyncAdapterStateFetcher {
 
-    private final HashMap<Pair<Integer, String>, Integer> mBucketCache =
-            new HashMap<>();
+    private final HashMap<UserPackage, Integer> mBucketCache = new HashMap<>();
 
     public SyncAdapterStateFetcher() {
     }
@@ -36,7 +35,7 @@
      * Return sync adapter state with a cache.
      */
     public int getStandbyBucket(int userId, String packageName) {
-        final Pair<Integer, String> key = Pair.create(userId, packageName);
+        final UserPackage key = UserPackage.of(userId, packageName);
         final Integer cached = mBucketCache.get(key);
         if (cached != null) {
             return cached;
diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java
index 73afa60..eb81e70 100644
--- a/services/core/java/com/android/server/content/SyncManager.java
+++ b/services/core/java/com/android/server/content/SyncManager.java
@@ -2215,7 +2215,8 @@
         pw.print("Storage low: "); pw.println(storageLowIntent != null);
         pw.print("Clock valid: "); pw.println(mSyncStorageEngine.isClockValid());
 
-        final AccountAndUser[] accounts = AccountManagerService.getSingleton().getAllAccounts();
+        final AccountAndUser[] accounts =
+                AccountManagerService.getSingleton().getAllAccountsForSystemProcess();
 
         pw.print("Accounts: ");
         if (accounts != INITIAL_ACCOUNTS_ARRAY) {
@@ -3274,7 +3275,8 @@
         private void updateRunningAccountsH(EndPoint syncTargets) {
             synchronized (mAccountsLock) {
                 AccountAndUser[] oldAccounts = mRunningAccounts;
-                mRunningAccounts = AccountManagerService.getSingleton().getRunningAccounts();
+                mRunningAccounts =
+                        AccountManagerService.getSingleton().getRunningAccountsForSystem();
                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
                     Slog.v(TAG, "Accounts list: ");
                     for (AccountAndUser acc : mRunningAccounts) {
@@ -3316,7 +3318,8 @@
             }
 
             // Cancel all jobs from non-existent accounts.
-            AccountAndUser[] allAccounts = AccountManagerService.getSingleton().getAllAccounts();
+            AccountAndUser[] allAccounts =
+                    AccountManagerService.getSingleton().getAllAccountsForSystemProcess();
             List<SyncOperation> ops = getAllPendingSyncs();
             for (int i = 0, opsSize = ops.size(); i < opsSize; i++) {
                 SyncOperation op = ops.get(i);
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index 5c679b8..9c1cf38 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -51,8 +51,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
@@ -60,6 +58,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IntPair;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/cpu/OWNERS b/services/core/java/com/android/server/cpu/OWNERS
new file mode 100644
index 0000000..2f42363
--- /dev/null
+++ b/services/core/java/com/android/server/cpu/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 608533
+
+include platform/packages/services/Car:/OWNERS
+lakshmana@google.com
diff --git a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
index 7c9a484..3581981 100644
--- a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
+++ b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
@@ -22,12 +22,12 @@
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index 1686cb2..df4c471 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -55,8 +55,6 @@
 import android.provider.Settings;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -64,6 +62,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.RingBuffer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import libcore.io.IoUtils;
@@ -1133,7 +1133,7 @@
 
         public void registerReceiver(Context context,
                 BroadcastReceiver receiver, IntentFilter filter) {
-            context.registerReceiver(receiver, filter);
+            context.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED_UNAUDITED);
         }
 
         public void unregisterReceiver(Context context,
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index a3b1a42..523a2dc 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -699,7 +699,6 @@
      */
     public float getNitsFromBacklight(float backlight) {
         if (mBacklightToNitsSpline == null) {
-            Slog.wtf(TAG, "requesting nits when no mapping exists.");
             return NITS_INVALID;
         }
         backlight = Math.max(backlight, mBacklightMinimum);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 587db41..78b697d 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -40,6 +40,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.compat.CompatChanges;
@@ -101,6 +102,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
@@ -117,6 +119,7 @@
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
 import android.window.DisplayWindowPolicyController;
 import android.window.ScreenCapture;
 
@@ -124,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;
@@ -148,6 +152,7 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
 
+
 /**
  * Manages attached displays.
  * <p>
@@ -202,8 +207,6 @@
     private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable";
 
     private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top";
-    private static final String PROP_USE_NEW_DISPLAY_POWER_CONTROLLER =
-            "persist.sys.use_new_display_power_controller";
     private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000;
     // This value needs to be in sync with the threshold
     // in RefreshRateConfigs::getFrameRateDivisor.
@@ -1356,11 +1359,19 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
-                final int displayId = createVirtualDisplayLocked(callback, projection, callingUid,
-                        packageName, surface, flags, virtualDisplayConfig);
+                final int displayId =
+                        createVirtualDisplayLocked(
+                                callback,
+                                projection,
+                                callingUid,
+                                packageName,
+                                virtualDevice,
+                                surface,
+                                flags,
+                                virtualDisplayConfig);
                 if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) {
-                    mDisplayWindowPolicyControllers.put(displayId,
-                            Pair.create(virtualDevice, dwpc));
+                    mDisplayWindowPolicyControllers.put(
+                            displayId, Pair.create(virtualDevice, dwpc));
                 }
                 return displayId;
             }
@@ -1369,12 +1380,20 @@
         }
     }
 
-    private int createVirtualDisplayLocked(IVirtualDisplayCallback callback,
-            IMediaProjection projection, int callingUid, String packageName, Surface surface,
-            int flags, VirtualDisplayConfig virtualDisplayConfig) {
+    private int createVirtualDisplayLocked(
+            IVirtualDisplayCallback callback,
+            IMediaProjection projection,
+            int callingUid,
+            String packageName,
+            IVirtualDevice virtualDevice,
+            Surface surface,
+            int flags,
+            VirtualDisplayConfig virtualDisplayConfig) {
         if (mVirtualDisplayAdapter == null) {
-            Slog.w(TAG, "Rejecting request to create private virtual display "
-                    + "because the virtual display adapter is not available.");
+            Slog.w(
+                    TAG,
+                    "Rejecting request to create private virtual display "
+                            + "because the virtual display adapter is not available.");
             return -1;
         }
 
@@ -1385,6 +1404,19 @@
             return -1;
         }
 
+        // If the display is to be added to a device display group, we need to make the
+        // LogicalDisplayMapper aware of the link between the new display and its associated virtual
+        // device before triggering DISPLAY_DEVICE_EVENT_ADDED.
+        if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) {
+            try {
+                final int virtualDeviceId = virtualDevice.getDeviceId();
+                mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(
+                        device, virtualDeviceId);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+
         // DisplayDevice events are handled manually for Virtual Displays.
         // TODO: multi-display Fix this so that generic add/remove events are not handled in a
         // different code path for virtual displays.  Currently this happens so that we can
@@ -1393,8 +1425,7 @@
         // called on the DisplayThread (which we don't want to wait for?).
         // One option would be to actually wait here on the binder thread
         // to be notified when the virtual display is created (or failed).
-        mDisplayDeviceRepo.onDisplayDeviceEvent(device,
-                DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        mDisplayDeviceRepo.onDisplayDeviceEvent(device, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
 
         final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
         if (display != null) {
@@ -1685,7 +1716,20 @@
         final Point userPreferredResolution =
                 mPersistentDataStore.getUserPreferredResolution(device);
         final float refreshRate = mPersistentDataStore.getUserPreferredRefreshRate(device);
-        if (userPreferredResolution == null && Float.isNaN(refreshRate)) {
+        // If value in persistentDataStore is null, preserving the mode from systemPreferredMode.
+        // This is required because in some devices, user-preferred mode was not stored in
+        // persistentDataStore, but was stored in a config which is returned through
+        // systemPreferredMode.
+        if ((userPreferredResolution == null && Float.isNaN(refreshRate))
+                || (userPreferredResolution.equals(0, 0) && refreshRate == 0.0f)) {
+            Display.Mode systemPreferredMode = device.getSystemPreferredDisplayModeLocked();
+            if (systemPreferredMode == null) {
+                return;
+            }
+            storeModeInPersistentDataStoreLocked(
+                    display.getDisplayIdLocked(), systemPreferredMode.getPhysicalWidth(),
+                    systemPreferredMode.getPhysicalHeight(), systemPreferredMode.getRefreshRate());
+            device.setUserPreferredDisplayModeLocked(systemPreferredMode);
             return;
         }
         Display.Mode.Builder modeBuilder = new Display.Mode.Builder();
@@ -1871,6 +1915,15 @@
                 if (displayDevice == null) {
                     return;
                 }
+                if (mLogicalDisplayMapper.getDisplayLocked(displayDevice) != null
+                        && mLogicalDisplayMapper.getDisplayLocked(displayDevice)
+                        .getDisplayInfoLocked().type == Display.TYPE_INTERNAL && c != null) {
+                    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 {
@@ -2575,12 +2628,14 @@
         mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked);
     }
 
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     private void addDisplayPowerControllerLocked(LogicalDisplay display) {
         if (mPowerHandler == null) {
             // initPowerManagement has not yet been called.
             return;
         }
-        if (mBrightnessTracker == null) {
+
+        if (mBrightnessTracker == null && display.getDisplayIdLocked() == Display.DEFAULT_DISPLAY) {
             mBrightnessTracker = new BrightnessTracker(mContext, null);
         }
 
@@ -2588,7 +2643,8 @@
                 display, mSyncRoot);
         final DisplayPowerControllerInterface displayPowerController;
 
-        if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) {
+        if (DeviceConfig.getBoolean("display_manager",
+                "use_newly_structured_display_power_controller", false)) {
             displayPowerController = new DisplayPowerController2(
                     mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
                     mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 912b1b2..a5e5c24 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -33,8 +33,7 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IThermalEventListener;
@@ -49,6 +48,7 @@
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.provider.Settings;
+import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -58,6 +58,8 @@
 import android.util.SparseIntArray;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -135,6 +137,9 @@
 
     private boolean mAlwaysRespectAppRequest;
 
+    // TODO(b/241447632): remove the flag once SF changes are ready
+    private final boolean mRenderFrameRateIsPhysicalRefreshRate;
+
     /**
      * The allowed refresh rate switching type. This is used by SurfaceFlinger.
      */
@@ -170,6 +175,7 @@
         mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
                 mDeviceConfigDisplaySettings);
         mAlwaysRespectAppRequest = false;
+        mRenderFrameRateIsPhysicalRefreshRate = injector.renderFrameRateIsPhysicalRefreshRate();
     }
 
     /**
@@ -230,28 +236,48 @@
                 }
             }
         }
+
+        if (mRenderFrameRateIsPhysicalRefreshRate) {
+            for (int i = 0; i < votes.size(); i++) {
+
+                Vote vote = votes.valueAt(i);
+                vote.refreshRateRanges.physical.min = Math.max(vote.refreshRateRanges.physical.min,
+                        vote.refreshRateRanges.render.min);
+                vote.refreshRateRanges.physical.max = Math.min(vote.refreshRateRanges.physical.max,
+                        vote.refreshRateRanges.render.max);
+                vote.refreshRateRanges.render.min = Math.max(vote.refreshRateRanges.physical.min,
+                        vote.refreshRateRanges.render.min);
+                vote.refreshRateRanges.render.max = Math.min(vote.refreshRateRanges.physical.max,
+                        vote.refreshRateRanges.render.max);
+            }
+        }
+
         return votes;
     }
 
     private static final class VoteSummary {
-        public float minRefreshRate;
-        public float maxRefreshRate;
+        public float minPhysicalRefreshRate;
+        public float maxPhysicalRefreshRate;
+        public float minRenderFrameRate;
+        public float maxRenderFrameRate;
         public int width;
         public int height;
         public boolean disableRefreshRateSwitching;
-        public float baseModeRefreshRate;
+        public float appRequestBaseModeRefreshRate;
 
         VoteSummary() {
             reset();
         }
 
         public void reset() {
-            minRefreshRate = 0f;
-            maxRefreshRate = Float.POSITIVE_INFINITY;
+            minPhysicalRefreshRate = 0f;
+            maxPhysicalRefreshRate = Float.POSITIVE_INFINITY;
+            minRenderFrameRate = 0f;
+            maxRenderFrameRate = Float.POSITIVE_INFINITY;
             width = Vote.INVALID_SIZE;
             height = Vote.INVALID_SIZE;
             disableRefreshRateSwitching = false;
-            baseModeRefreshRate = 0f;
+            appRequestBaseModeRefreshRate = 0f;
         }
     }
 
@@ -270,9 +296,25 @@
             if (vote == null) {
                 continue;
             }
-            // For refresh rates, just use the tightest bounds of all the votes
-            summary.minRefreshRate = Math.max(summary.minRefreshRate, vote.refreshRateRange.min);
-            summary.maxRefreshRate = Math.min(summary.maxRefreshRate, vote.refreshRateRange.max);
+
+
+            // For physical refresh rates, just use the tightest bounds of all the votes.
+            // The refresh rate cannot be lower than the minimal render frame rate.
+            final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min,
+                    vote.refreshRateRanges.render.min);
+            summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+                    minPhysicalRefreshRate);
+            summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
+                    vote.refreshRateRanges.physical.max);
+
+            // Same goes to render frame rate, but frame rate cannot exceed the max physical
+            // refresh rate
+            final float maxRenderFrameRate = Math.min(vote.refreshRateRanges.render.max,
+                    vote.refreshRateRanges.physical.max);
+            summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate,
+                    vote.refreshRateRanges.render.min);
+            summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, maxRenderFrameRate);
+
             // For display size, disable refresh rate switching and base mode refresh rate use only
             // the first vote we come across (i.e. the highest priority vote that includes the
             // attribute).
@@ -284,12 +326,57 @@
             if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
                 summary.disableRefreshRateSwitching = true;
             }
-            if (summary.baseModeRefreshRate == 0f && vote.baseModeRefreshRate > 0f) {
-                summary.baseModeRefreshRate = vote.baseModeRefreshRate;
+            if (summary.appRequestBaseModeRefreshRate == 0f
+                    && vote.appRequestBaseModeRefreshRate > 0f) {
+                summary.appRequestBaseModeRefreshRate = vote.appRequestBaseModeRefreshRate;
+            }
+
+            if (mLoggingEnabled) {
+                Slog.w(TAG, "Vote summary for priority "
+                        + Vote.priorityToString(priority)
+                        + ": width=" + summary.width
+                        + ", height=" + summary.height
+                        + ", minPhysicalRefreshRate=" + summary.minPhysicalRefreshRate
+                        + ", maxPhysicalRefreshRate=" + summary.maxPhysicalRefreshRate
+                        + ", minRenderFrameRate=" + summary.minRenderFrameRate
+                        + ", maxRenderFrameRate=" + summary.maxRenderFrameRate
+                        + ", disableRefreshRateSwitching="
+                        + summary.disableRefreshRateSwitching
+                        + ", appRequestBaseModeRefreshRate="
+                        + summary.appRequestBaseModeRefreshRate);
             }
         }
     }
 
+    private boolean equalsWithinFloatTolerance(float a, float b) {
+        return a >= b - FLOAT_TOLERANCE && a <= b + FLOAT_TOLERANCE;
+    }
+
+    private Display.Mode selectBaseMode(VoteSummary summary,
+            ArrayList<Display.Mode> availableModes, Display.Mode defaultMode) {
+        // The base mode should be as close as possible to the app requested mode. Since all the
+        // available modes already have the same size, we just need to look for a matching refresh
+        // rate. If the summary doesn't include an app requested refresh rate, we'll use the default
+        // mode refresh rate. This is important because SurfaceFlinger can do only seamless switches
+        // by default. Some devices (e.g. TV) don't support seamless switching so the mode we select
+        // here won't be changed.
+        float preferredRefreshRate =
+                summary.appRequestBaseModeRefreshRate > 0
+                        ? summary.appRequestBaseModeRefreshRate : defaultMode.getRefreshRate();
+        for (Display.Mode availableMode : availableModes) {
+            if (equalsWithinFloatTolerance(preferredRefreshRate, availableMode.getRefreshRate())) {
+                return availableMode;
+            }
+        }
+
+        // If we couldn't find a mode id based on the refresh rate, it means that the available
+        // modes were filtered by the app requested size, which is different that the default mode
+        // size, and the requested app refresh rate was dropped from the summary due to a higher
+        // priority vote. Since we don't have any other hint about the refresh rate,
+        // we just pick the first.
+        return !availableModes.isEmpty() ? availableModes.get(0) : null;
+    }
+
     /**
      * Calculates the refresh rate ranges and display modes that the system is allowed to freely
      * switch between based on global and display-specific constraints.
@@ -346,11 +433,16 @@
                                 + " and constraints: "
                                 + "width=" + primarySummary.width
                                 + ", height=" + primarySummary.height
-                                + ", minRefreshRate=" + primarySummary.minRefreshRate
-                                + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                                + ", minPhysicalRefreshRate="
+                                + primarySummary.minPhysicalRefreshRate
+                                + ", maxPhysicalRefreshRate="
+                                + primarySummary.maxPhysicalRefreshRate
+                                + ", minRenderFrameRate=" + primarySummary.minRenderFrameRate
+                                + ", maxRenderFrameRate=" + primarySummary.maxRenderFrameRate
                                 + ", disableRefreshRateSwitching="
                                 + primarySummary.disableRefreshRateSwitching
-                                + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
+                                + ", appRequestBaseModeRefreshRate="
+                                + primarySummary.appRequestBaseModeRefreshRate);
                     }
                     break;
                 }
@@ -361,11 +453,14 @@
                             + " and with the following constraints: "
                             + "width=" + primarySummary.width
                             + ", height=" + primarySummary.height
-                            + ", minRefreshRate=" + primarySummary.minRefreshRate
-                            + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                            + ", minPhysicalRefreshRate=" + primarySummary.minPhysicalRefreshRate
+                            + ", maxPhysicalRefreshRate=" + primarySummary.maxPhysicalRefreshRate
+                            + ", minRenderFrameRate=" + primarySummary.minRenderFrameRate
+                            + ", maxRenderFrameRate=" + primarySummary.maxRenderFrameRate
                             + ", disableRefreshRateSwitching="
                             + primarySummary.disableRefreshRateSwitching
-                            + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
+                            + ", appRequestBaseModeRefreshRate="
+                            + primarySummary.appRequestBaseModeRefreshRate);
                 }
 
                 // If we haven't found anything with the current set of votes, drop the
@@ -373,73 +468,84 @@
                 lowestConsideredPriority++;
             }
 
+            if (mLoggingEnabled) {
+                Slog.i(TAG,
+                        "Primary physical range: ["
+                                + primarySummary.minPhysicalRefreshRate
+                                + " "
+                                + primarySummary.maxPhysicalRefreshRate
+                                + "] render frame rate range: ["
+                                + primarySummary.minRenderFrameRate
+                                + " "
+                                + primarySummary.maxRenderFrameRate
+                                + "]");
+            }
+
             VoteSummary appRequestSummary = new VoteSummary();
             summarizeVotes(
                     votes,
                     Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF,
                     Vote.MAX_PRIORITY,
                     appRequestSummary);
-            appRequestSummary.minRefreshRate =
-                    Math.min(appRequestSummary.minRefreshRate, primarySummary.minRefreshRate);
-            appRequestSummary.maxRefreshRate =
-                    Math.max(appRequestSummary.maxRefreshRate, primarySummary.maxRefreshRate);
+            appRequestSummary.minPhysicalRefreshRate =
+                    Math.min(appRequestSummary.minPhysicalRefreshRate,
+                            primarySummary.minPhysicalRefreshRate);
+            appRequestSummary.maxPhysicalRefreshRate =
+                    Math.max(appRequestSummary.maxPhysicalRefreshRate,
+                            primarySummary.maxPhysicalRefreshRate);
+            appRequestSummary.minRenderFrameRate =
+                    Math.min(appRequestSummary.minRenderFrameRate,
+                            primarySummary.minRenderFrameRate);
+            appRequestSummary.maxRenderFrameRate =
+                    Math.max(appRequestSummary.maxRenderFrameRate,
+                            primarySummary.maxRenderFrameRate);
             if (mLoggingEnabled) {
                 Slog.i(TAG,
-                        String.format("App request range: [%.0f %.0f]",
-                                appRequestSummary.minRefreshRate,
-                                appRequestSummary.maxRefreshRate));
+                        "App request range: ["
+                                + appRequestSummary.minPhysicalRefreshRate
+                                + " "
+                                + appRequestSummary.maxPhysicalRefreshRate
+                                + "] Frame rate range: ["
+                                + appRequestSummary.minRenderFrameRate
+                                + " "
+                                + appRequestSummary.maxRenderFrameRate
+                                + "]");
             }
 
-            // Select the base mode id based on the base mode refresh rate, if available, since this
-            // will be the mode id the app voted for.
-            Display.Mode baseMode = null;
-            for (Display.Mode availableMode : availableModes) {
-                if (primarySummary.baseModeRefreshRate
-                        >= availableMode.getRefreshRate() - FLOAT_TOLERANCE
-                        && primarySummary.baseModeRefreshRate
-                        <= availableMode.getRefreshRate() + FLOAT_TOLERANCE) {
-                    baseMode = availableMode;
-                }
-            }
-
-            // Select the default mode if available. This is important because SurfaceFlinger
-            // can do only seamless switches by default. Some devices (e.g. TV) don't support
-            // seamless switching so the mode we select here won't be changed.
-            if (baseMode == null) {
-                for (Display.Mode availableMode : availableModes) {
-                    if (availableMode.getModeId() == defaultMode.getModeId()) {
-                        baseMode = defaultMode;
-                        break;
-                    }
-                }
-            }
-
-            // If the application requests a display mode by setting
-            // LayoutParams.preferredDisplayModeId, it will be the only available mode and it'll
-            // be stored as baseModeId.
-            if (baseMode == null && !availableModes.isEmpty()) {
-                baseMode = availableModes.get(0);
-            }
-
+            Display.Mode baseMode = selectBaseMode(primarySummary, availableModes, defaultMode);
             if (baseMode == null) {
                 Slog.w(TAG, "Can't find a set of allowed modes which satisfies the votes. Falling"
                         + " back to the default mode. Display = " + displayId + ", votes = " + votes
                         + ", supported modes = " + Arrays.toString(modes));
 
                 float fps = defaultMode.getRefreshRate();
+                final RefreshRateRange range = new RefreshRateRange(fps, fps);
+                final RefreshRateRanges ranges = new RefreshRateRanges(range, range);
                 return new DesiredDisplayModeSpecs(defaultMode.getModeId(),
                         /*allowGroupSwitching */ false,
-                        new RefreshRateRange(fps, fps),
-                        new RefreshRateRange(fps, fps));
+                        ranges, ranges);
+            }
+
+            boolean modeSwitchingDisabled =
+                    mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE
+                            || mModeSwitchingType
+                                == DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY;
+
+            if (modeSwitchingDisabled || primarySummary.disableRefreshRateSwitching) {
+                float fps = baseMode.getRefreshRate();
+                primarySummary.minPhysicalRefreshRate = primarySummary.maxPhysicalRefreshRate = fps;
+                if (modeSwitchingDisabled) {
+                    appRequestSummary.minPhysicalRefreshRate =
+                            appRequestSummary.maxPhysicalRefreshRate = fps;
+                }
             }
 
             if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE
-                    || primarySummary.disableRefreshRateSwitching) {
-                float fps = baseMode.getRefreshRate();
-                primarySummary.minRefreshRate = primarySummary.maxRefreshRate = fps;
-                if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE) {
-                    appRequestSummary.minRefreshRate = appRequestSummary.maxRefreshRate = fps;
-                }
+                    || mRenderFrameRateIsPhysicalRefreshRate) {
+                primarySummary.minRenderFrameRate = primarySummary.minPhysicalRefreshRate;
+                primarySummary.maxRenderFrameRate = primarySummary.maxPhysicalRefreshRate;
+                appRequestSummary.minRenderFrameRate = appRequestSummary.minPhysicalRefreshRate;
+                appRequestSummary.maxRenderFrameRate = appRequestSummary.maxPhysicalRefreshRate;
             }
 
             boolean allowGroupSwitching =
@@ -447,17 +553,36 @@
 
             return new DesiredDisplayModeSpecs(baseMode.getModeId(),
                     allowGroupSwitching,
-                    new RefreshRateRange(
-                            primarySummary.minRefreshRate, primarySummary.maxRefreshRate),
-                    new RefreshRateRange(
-                            appRequestSummary.minRefreshRate, appRequestSummary.maxRefreshRate));
+                    new RefreshRateRanges(
+                            new RefreshRateRange(
+                                    primarySummary.minPhysicalRefreshRate,
+                                    primarySummary.maxPhysicalRefreshRate),
+                            new RefreshRateRange(
+                                primarySummary.minRenderFrameRate,
+                                primarySummary.maxRenderFrameRate)),
+                    new RefreshRateRanges(
+                            new RefreshRateRange(
+                                    appRequestSummary.minPhysicalRefreshRate,
+                                    appRequestSummary.maxPhysicalRefreshRate),
+                            new RefreshRateRange(
+                                    appRequestSummary.minRenderFrameRate,
+                                    appRequestSummary.maxRenderFrameRate)));
         }
     }
 
     private ArrayList<Display.Mode> filterModes(Display.Mode[] supportedModes,
             VoteSummary summary) {
+        if (summary.minRenderFrameRate > summary.maxRenderFrameRate + FLOAT_TOLERANCE) {
+            if (mLoggingEnabled) {
+                Slog.w(TAG, "Vote summary resulted in empty set (invalid frame rate range)"
+                        + ": minRenderFrameRate=" + summary.minRenderFrameRate
+                        + ", maxRenderFrameRate=" + summary.maxRenderFrameRate);
+            }
+            return new ArrayList<>();
+        }
+
         ArrayList<Display.Mode> availableModes = new ArrayList<>();
-        boolean missingBaseModeRefreshRate = summary.baseModeRefreshRate > 0f;
+        boolean missingBaseModeRefreshRate = summary.appRequestBaseModeRefreshRate > 0f;
         for (Display.Mode mode : supportedModes) {
             if (mode.getPhysicalWidth() != summary.width
                     || mode.getPhysicalHeight() != summary.height) {
@@ -470,24 +595,47 @@
                 }
                 continue;
             }
-            final float refreshRate = mode.getRefreshRate();
+            final float physicalRefreshRate = mode.getRefreshRate();
             // Some refresh rates are calculated based on frame timings, so they aren't *exactly*
             // equal to expected refresh rate. Given that, we apply a bit of tolerance to this
             // comparison.
-            if (refreshRate < (summary.minRefreshRate - FLOAT_TOLERANCE)
-                    || refreshRate > (summary.maxRefreshRate + FLOAT_TOLERANCE)) {
+            if (physicalRefreshRate < (summary.minPhysicalRefreshRate - FLOAT_TOLERANCE)
+                    || physicalRefreshRate > (summary.maxPhysicalRefreshRate + FLOAT_TOLERANCE)) {
                 if (mLoggingEnabled) {
                     Slog.w(TAG, "Discarding mode " + mode.getModeId()
                             + ", outside refresh rate bounds"
-                            + ": minRefreshRate=" + summary.minRefreshRate
-                            + ", maxRefreshRate=" + summary.maxRefreshRate
-                            + ", modeRefreshRate=" + refreshRate);
+                            + ": minPhysicalRefreshRate=" + summary.minPhysicalRefreshRate
+                            + ", maxPhysicalRefreshRate=" + summary.maxPhysicalRefreshRate
+                            + ", modeRefreshRate=" + physicalRefreshRate);
                 }
                 continue;
             }
+
+            // Check whether the render frame rate range is achievable by the mode's physical
+            // refresh rate, meaning that if a divisor of the physical refresh rate is in range
+            // of the render frame rate.
+            // For example for the render frame rate [50, 70]:
+            //   - 120Hz is in range as we can render at 60hz by skipping every other frame,
+            //     which is within the render rate range
+            //   - 90hz is not in range as none of the even divisors (i.e. 90, 45, 30)
+            //     fall within the acceptable render range.
+            final int divisor = (int) Math.ceil(physicalRefreshRate / summary.maxRenderFrameRate);
+            float adjustedPhysicalRefreshRate = physicalRefreshRate / divisor;
+            if (adjustedPhysicalRefreshRate < (summary.minRenderFrameRate - FLOAT_TOLERANCE)) {
+                if (mLoggingEnabled) {
+                    Slog.w(TAG, "Discarding mode " + mode.getModeId()
+                            + " with adjusted refresh rate: " + adjustedPhysicalRefreshRate
+                            + ", outside frame rate bounds"
+                            + ": minRenderFrameRate=" + summary.minRenderFrameRate
+                            + ", maxRenderFrameRate=" + summary.maxRenderFrameRate
+                            + ", modePhysicalRefreshRate=" + physicalRefreshRate);
+                }
+                continue;
+            }
+
             availableModes.add(mode);
-            if (mode.getRefreshRate() >= summary.baseModeRefreshRate - FLOAT_TOLERANCE
-                    && mode.getRefreshRate() <= summary.baseModeRefreshRate + FLOAT_TOLERANCE) {
+            if (equalsWithinFloatTolerance(mode.getRefreshRate(),
+                    summary.appRequestBaseModeRefreshRate)) {
                 missingBaseModeRefreshRate = false;
             }
         }
@@ -700,6 +848,8 @@
                 return "SWITCHING_TYPE_WITHIN_GROUPS";
             case DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS:
                 return "SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS";
+            case DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY:
+                return "SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY";
             default:
                 return "Unknown SwitchingType " + type;
         }
@@ -854,30 +1004,30 @@
         public boolean allowGroupSwitching;
 
         /**
-         * The primary refresh rate range.
+         * The primary refresh rate ranges.
          */
-        public final RefreshRateRange primaryRefreshRateRange;
+        public final RefreshRateRanges primary;
         /**
-         * The app request refresh rate range. Lower priority considerations won't be included in
+         * The app request refresh rate ranges. Lower priority considerations won't be included in
          * this range, allowing SurfaceFlinger to consider additional refresh rates for apps that
          * call setFrameRate(). This range will be greater than or equal to the primary refresh rate
          * range, never smaller.
          */
-        public final RefreshRateRange appRequestRefreshRateRange;
+        public final RefreshRateRanges appRequest;
 
         public DesiredDisplayModeSpecs() {
-            primaryRefreshRateRange = new RefreshRateRange();
-            appRequestRefreshRateRange = new RefreshRateRange();
+            primary = new RefreshRateRanges();
+            appRequest = new RefreshRateRanges();
         }
 
         public DesiredDisplayModeSpecs(int baseModeId,
                 boolean allowGroupSwitching,
-                @NonNull RefreshRateRange primaryRefreshRateRange,
-                @NonNull RefreshRateRange appRequestRefreshRateRange) {
+                @NonNull RefreshRateRanges primary,
+                @NonNull RefreshRateRanges appRequest) {
             this.baseModeId = baseModeId;
             this.allowGroupSwitching = allowGroupSwitching;
-            this.primaryRefreshRateRange = primaryRefreshRateRange;
-            this.appRequestRefreshRateRange = appRequestRefreshRateRange;
+            this.primary = primary;
+            this.appRequest = appRequest;
         }
 
         /**
@@ -886,12 +1036,12 @@
         @Override
         public String toString() {
             return String.format("baseModeId=%d allowGroupSwitching=%b"
-                            + " primaryRefreshRateRange=[%.0f %.0f]"
-                            + " appRequestRefreshRateRange=[%.0f %.0f]",
-                    baseModeId, allowGroupSwitching, primaryRefreshRateRange.min,
-                    primaryRefreshRateRange.max, appRequestRefreshRateRange.min,
-                    appRequestRefreshRateRange.max);
+                            + " primary=%s"
+                            + " appRequest=%s",
+                    baseModeId, allowGroupSwitching, primary.toString(),
+                    appRequest.toString());
         }
+
         /**
          * Checks whether the two objects have the same values.
          */
@@ -913,11 +1063,11 @@
             if (allowGroupSwitching != desiredDisplayModeSpecs.allowGroupSwitching) {
                 return false;
             }
-            if (!primaryRefreshRateRange.equals(desiredDisplayModeSpecs.primaryRefreshRateRange)) {
+            if (!primary.equals(desiredDisplayModeSpecs.primary)) {
                 return false;
             }
-            if (!appRequestRefreshRateRange.equals(
-                        desiredDisplayModeSpecs.appRequestRefreshRateRange)) {
+            if (!appRequest.equals(
+                    desiredDisplayModeSpecs.appRequest)) {
                 return false;
             }
             return true;
@@ -925,8 +1075,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(baseModeId, allowGroupSwitching, primaryRefreshRateRange,
-                    appRequestRefreshRateRange);
+            return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest);
         }
 
         /**
@@ -935,18 +1084,24 @@
         public void copyFrom(DesiredDisplayModeSpecs other) {
             baseModeId = other.baseModeId;
             allowGroupSwitching = other.allowGroupSwitching;
-            primaryRefreshRateRange.min = other.primaryRefreshRateRange.min;
-            primaryRefreshRateRange.max = other.primaryRefreshRateRange.max;
-            appRequestRefreshRateRange.min = other.appRequestRefreshRateRange.min;
-            appRequestRefreshRateRange.max = other.appRequestRefreshRateRange.max;
+            primary.physical.min = other.primary.physical.min;
+            primary.physical.max = other.primary.physical.max;
+            primary.render.min = other.primary.render.min;
+            primary.render.max = other.primary.render.max;
+
+            appRequest.physical.min = other.appRequest.physical.min;
+            appRequest.physical.max = other.appRequest.physical.max;
+            appRequest.render.min = other.appRequest.render.min;
+            appRequest.render.max = other.appRequest.render.max;
         }
     }
 
     @VisibleForTesting
     static final class Vote {
-        // DEFAULT_FRAME_RATE votes for [0, DEFAULT]. As the lowest priority vote, it's overridden
-        // by all other considerations. It acts to set a default frame rate for a device.
-        public static final int PRIORITY_DEFAULT_REFRESH_RATE = 0;
+        // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
+        // priority vote, it's overridden by all other considerations. It acts to set a default
+        // frame rate for a device.
+        public static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
 
         // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
         // null. It is used to set a preferred refresh rate value in case the higher priority votes
@@ -956,16 +1111,16 @@
         // High-brightness-mode may need a specific range of refresh-rates to function properly.
         public static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
 
-        // SETTING_MIN_REFRESH_RATE is used to propose a lower bound of display refresh rate.
+        // SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
         // It votes [MIN_REFRESH_RATE, Float.POSITIVE_INFINITY]
-        public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 3;
+        public static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
 
-        // APP_REQUEST_REFRESH_RATE_RANGE is used to for internal apps to limit the refresh
-        // rate in certain cases, mostly to preserve power.
+        // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
+        // frame rate in certain cases, mostly to preserve power.
         // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
         // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
         // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
-        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE = 4;
+        public static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4;
 
         // We split the app request into different priorities in case we can satisfy one desire
         // without the other.
@@ -973,21 +1128,33 @@
         // Application can specify preferred refresh rate with below attrs.
         // @see android.view.WindowManager.LayoutParams#preferredRefreshRate
         // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
-        // These translates into votes for the base mode refresh rate and resolution to be
-        // used by SurfaceFlinger as the policy of choosing the display mode. The system also
-        // forces some apps like denylisted app to run at a lower refresh rate.
+        //
+        // When the app specifies a LayoutParams#preferredDisplayModeId, in addition to the
+        // refresh rate, it also chooses a preferred size (resolution) as part of the selected
+        // mode id. The app preference is then translated to APP_REQUEST_BASE_MODE_REFRESH_RATE and
+        // optionally to APP_REQUEST_SIZE as well, if a mode id was selected.
+        // The system also forces some apps like denylisted app to run at a lower refresh rate.
         // @see android.R.array#config_highRefreshRateBlacklist
+        //
+        // When summarizing the votes and filtering the allowed display modes, these votes determine
+        // which mode id should be the base mode id to be sent to SurfaceFlinger:
+        // - APP_REQUEST_BASE_MODE_REFRESH_RATE is used to validate the vote summary. If a summary
+        //   includes a base mode refresh rate, but it is not in the refresh rate range, then the
+        //   summary is considered invalid so we could drop a lower priority vote and try again.
+        // - APP_REQUEST_SIZE is used to filter out display modes of a different size.
+        //
         // The preferred refresh rate is set on the main surface of the app outside of
         // DisplayModeDirector.
         // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
         public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5;
         public static final int PRIORITY_APP_REQUEST_SIZE = 6;
 
-        // SETTING_PEAK_REFRESH_RATE has a high priority and will restrict the bounds of the rest
-        // of low priority voters. It votes [0, max(PEAK, MIN)]
-        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 7;
+        // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
+        // rest of low priority voters. It votes [0, max(PEAK, MIN)]
+        public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
 
-        // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
+        // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
+        // Settings.Global.LOW_POWER_MODE is on.
         public static final int PRIORITY_LOW_POWER_MODE = 8;
 
         // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
@@ -1010,13 +1177,13 @@
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
 
-        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_REFRESH_RATE;
+        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
         public static final int MAX_PRIORITY = PRIORITY_UDFPS;
 
         // The cutoff for the app request refresh rate range. Votes with priorities lower than this
         // value will not be considered when constructing the app request refresh rate range.
         public static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
-                PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE;
+                PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
 
         /**
          * A value signifying an invalid width or height in a vote.
@@ -1032,9 +1199,9 @@
          */
         public final int height;
         /**
-         * Information about the min and max refresh rate DM would like to set the display to.
+         * Information about the refresh rate frame rate ranges DM would like to set the display to.
          */
-        public final RefreshRateRange refreshRateRange;
+        public final RefreshRateRanges refreshRateRanges;
 
         /**
          * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
@@ -1043,52 +1210,66 @@
         public final boolean disableRefreshRateSwitching;
 
         /**
-         * The base mode refresh rate to be used for this display. This would be used when deciding
-         * the base mode id.
+         * The preferred refresh rate selected by the app. It is used to validate that the summary
+         * refresh rate ranges include this value, and are not restricted by a lower priority vote.
          */
-        public final float baseModeRefreshRate;
+        public final float appRequestBaseModeRefreshRate;
 
-        public static Vote forRefreshRates(float minRefreshRate, float maxRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate,
+        public static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0,
+                    Float.POSITIVE_INFINITY,
                     minRefreshRate == maxRefreshRate, 0f);
         }
 
+        public static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate,
+                    maxFrameRate,
+                    false, 0f);
+        }
+
         public static Vote forSize(int width, int height) {
-            return new Vote(width, height, 0f, Float.POSITIVE_INFINITY, false,
+            return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY,
+                    false,
                     0f);
         }
 
         public static Vote forDisableRefreshRateSwitching() {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, true,
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                    Float.POSITIVE_INFINITY, true,
                     0f);
         }
 
         public static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, false,
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                    Float.POSITIVE_INFINITY, false,
                     baseModeRefreshRate);
         }
 
         private Vote(int width, int height,
-                float minRefreshRate, float maxRefreshRate,
+                float minPhysicalRefreshRate,
+                float maxPhysicalRefreshRate,
+                float minRenderFrameRate,
+                float maxRenderFrameRate,
                 boolean disableRefreshRateSwitching,
                 float baseModeRefreshRate) {
             this.width = width;
             this.height = height;
-            this.refreshRateRange =
-                    new RefreshRateRange(minRefreshRate, maxRefreshRate);
+            this.refreshRateRanges = new RefreshRateRanges(
+                    new RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
+                    new RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
             this.disableRefreshRateSwitching = disableRefreshRateSwitching;
-            this.baseModeRefreshRate = baseModeRefreshRate;
+            this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
         }
 
         public static String priorityToString(int priority) {
             switch (priority) {
                 case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
                     return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
-                case PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE:
-                    return "PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE";
+                case PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE:
+                    return "PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE";
                 case PRIORITY_APP_REQUEST_SIZE:
                     return "PRIORITY_APP_REQUEST_SIZE";
-                case PRIORITY_DEFAULT_REFRESH_RATE:
+                case PRIORITY_DEFAULT_RENDER_FRAME_RATE:
                     return "PRIORITY_DEFAULT_REFRESH_RATE";
                 case PRIORITY_FLICKER_REFRESH_RATE:
                     return "PRIORITY_FLICKER_REFRESH_RATE";
@@ -1104,10 +1285,10 @@
                     return "PRIORITY_SKIN_TEMPERATURE";
                 case PRIORITY_UDFPS:
                     return "PRIORITY_UDFPS";
-                case PRIORITY_USER_SETTING_MIN_REFRESH_RATE:
-                    return "PRIORITY_USER_SETTING_MIN_REFRESH_RATE";
-                case PRIORITY_USER_SETTING_PEAK_REFRESH_RATE:
-                    return "PRIORITY_USER_SETTING_PEAK_REFRESH_RATE";
+                case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE:
+                    return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
+                case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
+                    return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
                 default:
                     return Integer.toString(priority);
             }
@@ -1116,11 +1297,10 @@
         @Override
         public String toString() {
             return "Vote{"
-                + "width=" + width + ", height=" + height
-                + ", minRefreshRate=" + refreshRateRange.min
-                + ", maxRefreshRate=" + refreshRateRange.max
-                + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
-                + ", baseModeRefreshRate=" + baseModeRefreshRate + "}";
+                    + "width=" + width + ", height=" + height
+                    + ", refreshRateRanges=" + refreshRateRanges
+                    + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
+                    + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate + "}";
         }
     }
 
@@ -1237,7 +1417,7 @@
                     Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0;
             final Vote vote;
             if (inLowPowerMode) {
-                vote = Vote.forRefreshRates(0f, 60f);
+                vote = Vote.forRenderFrameRates(0f, 60f);
             } else {
                 vote = null;
             }
@@ -1262,13 +1442,14 @@
             // than necessary, and we should improve it. See b/156304339 for more info.
             Vote peakVote = peakRefreshRate == 0f
                     ? null
-                    : Vote.forRefreshRates(0f, Math.max(minRefreshRate, peakRefreshRate));
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, peakVote);
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                    Vote.forRefreshRates(minRefreshRate, Float.POSITIVE_INFINITY));
+                    : Vote.forRenderFrameRates(0f, Math.max(minRefreshRate, peakRefreshRate));
+            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE, peakVote);
+            updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                    Vote.forRenderFrameRates(minRefreshRate, Float.POSITIVE_INFINITY));
             Vote defaultVote =
-                    defaultRefreshRate == 0f ? null : Vote.forRefreshRates(0f, defaultRefreshRate);
-            updateVoteLocked(Vote.PRIORITY_DEFAULT_REFRESH_RATE, defaultVote);
+                    defaultRefreshRate == 0f
+                            ? null : Vote.forRenderFrameRates(0f, defaultRefreshRate);
+            updateVoteLocked(Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE, defaultVote);
 
             float maxRefreshRate;
             if (peakRefreshRate == 0f && defaultRefreshRate == 0f) {
@@ -1374,13 +1555,15 @@
 
             if (refreshRateRange != null) {
                 mAppPreferredRefreshRateRangeByDisplay.put(displayId, refreshRateRange);
-                vote = Vote.forRefreshRates(refreshRateRange.min, refreshRateRange.max);
+                vote = Vote.forRenderFrameRates(refreshRateRange.min, refreshRateRange.max);
             } else {
                 mAppPreferredRefreshRateRangeByDisplay.remove(displayId);
                 vote = null;
             }
             synchronized (mLock) {
-                updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, vote);
+                updateVoteLocked(displayId,
+                        Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                        vote);
             }
         }
 
@@ -1956,6 +2139,7 @@
 
             return false;
         }
+
         private void onBrightnessChangedLocked() {
             Vote refreshRateVote = null;
             Vote refreshRateSwitchingVote = null;
@@ -1969,7 +2153,7 @@
             boolean insideLowZone = hasValidLowZone() && isInsideLowZone(mBrightness, mAmbientLux);
             if (insideLowZone) {
                 refreshRateVote =
-                        Vote.forRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
+                        Vote.forPhysicalRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
                 refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
@@ -1977,7 +2161,8 @@
                     && isInsideHighZone(mBrightness, mAmbientLux);
             if (insideHighZone) {
                 refreshRateVote =
-                        Vote.forRefreshRates(mRefreshRateInHighZone, mRefreshRateInHighZone);
+                        Vote.forPhysicalRefreshRates(mRefreshRateInHighZone,
+                                mRefreshRateInHighZone);
                 refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
@@ -2182,39 +2367,39 @@
         }
     }
 
-    private class UdfpsObserver extends IUdfpsHbmListener.Stub {
-        private final SparseBooleanArray mLocalHbmEnabled = new SparseBooleanArray();
+    private class UdfpsObserver extends IUdfpsRefreshRateRequestCallback.Stub {
+        private final SparseBooleanArray mUdfpsRefreshRateEnabled = new SparseBooleanArray();
 
         public void observe() {
             StatusBarManagerInternal statusBar =
                     LocalServices.getService(StatusBarManagerInternal.class);
             if (statusBar != null) {
-                statusBar.setUdfpsHbmListener(this);
+                statusBar.setUdfpsRefreshRateCallback(this);
             }
         }
 
         @Override
-        public void onHbmEnabled(int displayId) {
+        public void onRequestEnabled(int displayId) {
             synchronized (mLock) {
-                updateHbmStateLocked(displayId, true /*enabled*/);
+                updateRefreshRateStateLocked(displayId, true /*enabled*/);
             }
         }
 
         @Override
-        public void onHbmDisabled(int displayId) {
+        public void onRequestDisabled(int displayId) {
             synchronized (mLock) {
-                updateHbmStateLocked(displayId, false /*enabled*/);
+                updateRefreshRateStateLocked(displayId, false /*enabled*/);
             }
         }
 
-        private void updateHbmStateLocked(int displayId, boolean enabled) {
-            mLocalHbmEnabled.put(displayId, enabled);
+        private void updateRefreshRateStateLocked(int displayId, boolean enabled) {
+            mUdfpsRefreshRateEnabled.put(displayId, enabled);
             updateVoteLocked(displayId);
         }
 
         private void updateVoteLocked(int displayId) {
             final Vote vote;
-            if (mLocalHbmEnabled.get(displayId)) {
+            if (mUdfpsRefreshRateEnabled.get(displayId)) {
                 Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
                 float maxRefreshRate = 0f;
                 for (Display.Mode mode : modes) {
@@ -2222,7 +2407,7 @@
                         maxRefreshRate = mode.getRefreshRate();
                     }
                 }
-                vote = Vote.forRefreshRates(maxRefreshRate, maxRefreshRate);
+                vote = Vote.forPhysicalRefreshRates(maxRefreshRate, maxRefreshRate);
             } else {
                 vote = null;
             }
@@ -2232,10 +2417,10 @@
 
         void dumpLocked(PrintWriter pw) {
             pw.println("  UdfpsObserver");
-            pw.println("    mLocalHbmEnabled: ");
-            for (int i = 0; i < mLocalHbmEnabled.size(); i++) {
-                final int displayId = mLocalHbmEnabled.keyAt(i);
-                final String enabled = mLocalHbmEnabled.valueAt(i) ? "enabled" : "disabled";
+            pw.println("    mUdfpsRefreshRateEnabled: ");
+            for (int i = 0; i < mUdfpsRefreshRateEnabled.size(); i++) {
+                final int displayId = mUdfpsRefreshRateEnabled.keyAt(i);
+                final String enabled = mUdfpsRefreshRateEnabled.valueAt(i) ? "enabled" : "disabled";
                 pw.println("      Display " + displayId + ": " + enabled);
             }
         }
@@ -2303,7 +2488,7 @@
                             mDisplayManagerInternal.getRefreshRateForDisplayAndSensor(
                                     displayId, mProximitySensorName, mProximitySensorType);
                     if (rate != null) {
-                        vote = Vote.forRefreshRates(rate.min, rate.max);
+                        vote = Vote.forPhysicalRefreshRates(rate.min, rate.max);
                     }
                 }
                 mBallotBox.vote(displayId, Vote.PRIORITY_PROXIMITY, vote);
@@ -2472,7 +2657,7 @@
                 if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT) {
                     // Device resource properties take priority over DisplayDeviceConfig
                     if (mRefreshRateInHbmSunlight > 0) {
-                        vote = Vote.forRefreshRates(mRefreshRateInHbmSunlight,
+                        vote = Vote.forPhysicalRefreshRates(mRefreshRateInHbmSunlight,
                                 mRefreshRateInHbmSunlight);
                     } else {
                         final List<RefreshRateLimitation> limits =
@@ -2480,7 +2665,7 @@
                         for (int i = 0; limits != null && i < limits.size(); i++) {
                             final RefreshRateLimitation limitation = limits.get(i);
                             if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) {
-                                vote = Vote.forRefreshRates(limitation.range.min,
+                                vote = Vote.forPhysicalRefreshRates(limitation.range.min,
                                         limitation.range.max);
                                 break;
                             }
@@ -2490,7 +2675,7 @@
                         mRefreshRateInHbmHdr > 0) {
                     // HBM for HDR vote isn't supported through DisplayDeviceConfig yet, so look for
                     // a vote from Device properties
-                    vote = Vote.forRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
+                    vote = Vote.forPhysicalRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
                 } else {
                     Slog.w(TAG, "Unexpected HBM mode " + hbmMode + " for display ID " + displayId);
                 }
@@ -2528,7 +2713,7 @@
             }
             final Vote vote;
             if (mStatus >= Temperature.THROTTLING_CRITICAL) {
-                vote = Vote.forRefreshRates(0f, 60f);
+                vote = Vote.forRenderFrameRates(0f, 60f);
             } else {
                 vote = null;
             }
@@ -2741,6 +2926,8 @@
         boolean isDozeState(Display d);
 
         IThermalService getThermalService();
+
+        boolean renderFrameRateIsPhysicalRefreshRate();
     }
 
     @VisibleForTesting
@@ -2794,6 +2981,12 @@
                     ServiceManager.getService(Context.THERMAL_SERVICE));
         }
 
+        @Override
+        public boolean renderFrameRateIsPhysicalRefreshRate() {
+            return DisplayProperties
+                    .debug_render_frame_rate_is_physical_refresh_rate().orElse(true);
+        }
+
         private DisplayManager getDisplayManager() {
             if (mDisplayManager == null) {
                 mDisplayManager = mContext.getSystemService(DisplayManager.class);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 422e98f..d6f0fd0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -927,7 +927,7 @@
 
         // Initialize all of the brightness tracking state
         final float brightness = convertToNits(mPowerState.getScreenBrightness());
-        if (brightness >= PowerManager.BRIGHTNESS_MIN) {
+        if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
         }
         mBrightnessSettingListener = brightnessValue -> {
@@ -1059,7 +1059,9 @@
             }
 
             loadAmbientLightSensor();
-            if (mBrightnessTracker != null) {
+            // BrightnessTracker should only use one light sensor, we want to use the light sensor
+            // from the default display and not e.g. temporary displays when switching layouts.
+            if (mBrightnessTracker != null && mDisplayId == Display.DEFAULT_DISPLAY) {
                 mBrightnessTracker.setLightSensor(mLightSensor);
             }
 
@@ -1701,6 +1703,7 @@
         mTempBrightnessEvent.setRbcStrength(mCdsi != null
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
         mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
+        mTempBrightnessEvent.setWasShortTermModelActive(hadUserBrightnessPoint);
         // Temporary is what we use during slider interactions. We avoid logging those so that
         // we don't spam logcat when the slider is being used.
         boolean tempToTempTransition =
@@ -1711,12 +1714,6 @@
                 || brightnessAdjustmentFlags != 0) {
             float lastBrightness = mLastBrightnessEvent.getBrightness();
             mTempBrightnessEvent.setInitialBrightness(lastBrightness);
-            mTempBrightnessEvent.setFastAmbientLux(
-                    mAutomaticBrightnessController == null
-                        ? -1f : mAutomaticBrightnessController.getFastAmbientLux());
-            mTempBrightnessEvent.setSlowAmbientLux(
-                    mAutomaticBrightnessController == null
-                        ? -1f : mAutomaticBrightnessController.getSlowAmbientLux());
             mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness);
             mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
             BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
@@ -2485,7 +2482,7 @@
             boolean hadUserDataPoint) {
         final float brightnessInNits = convertToNits(brightness);
         if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f
-                && mAutomaticBrightnessController != null) {
+                && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
             // values into a physical brightness unit since the value provided by the API is in
             // nits and not using the arbitrary backlight units.
@@ -2838,18 +2835,22 @@
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
                 ? -1f : convertToNits(event.getThermalMax());
 
-        FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                convertToNits(event.getInitialBrightness()),
-                convertToNits(event.getBrightness()),
-                event.getSlowAmbientLux(),
-                event.getPhysicalDisplayId(),
-                event.isShortTermModelActive(),
-                appliedLowPowerMode,
-                appliedRbcStrength,
-                appliedHbmMaxNits,
-                appliedThermalCapNits,
-                event.isAutomaticBrightnessEnabled(),
-                FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        if (mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
+                && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
+                    .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL) {
+            FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
+                    convertToNits(event.getInitialBrightness()),
+                    convertToNits(event.getBrightness()),
+                    event.getLux(),
+                    event.getPhysicalDisplayId(),
+                    event.wasShortTermModelActive(),
+                    appliedLowPowerMode,
+                    appliedRbcStrength,
+                    appliedHbmMaxNits,
+                    appliedThermalCapNits,
+                    event.isAutomaticBrightnessEnabled(),
+                    FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        }
     }
 
     private final class DisplayControllerHandler extends Handler {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 3c1bf0b..300b589 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -26,8 +26,6 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.AmbientBrightnessDayStats;
 import android.hardware.display.BrightnessChangeEvent;
@@ -54,7 +52,6 @@
 import android.util.MutableFloat;
 import android.util.MutableInt;
 import android.util.Slog;
-import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.R;
@@ -109,7 +106,7 @@
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
     private static final boolean DEBUG = false;
-    private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
+
 
     // If true, uses the color fade on animation.
     // We might want to turn this off if we cannot get a guarantee that the screen
@@ -123,31 +120,19 @@
     private static final int COLOR_FADE_OFF_ANIMATION_DURATION_MILLIS = 400;
 
     private static final int MSG_UPDATE_POWER_STATE = 1;
-    private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2;
-    private static final int MSG_SCREEN_ON_UNBLOCKED = 3;
-    private static final int MSG_SCREEN_OFF_UNBLOCKED = 4;
-    private static final int MSG_CONFIGURE_BRIGHTNESS = 5;
-    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 6;
-    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 7;
-    private static final int MSG_IGNORE_PROXIMITY = 8;
-    private static final int MSG_STOP = 9;
-    private static final int MSG_UPDATE_BRIGHTNESS = 10;
-    private static final int MSG_UPDATE_RBC = 11;
-    private static final int MSG_BRIGHTNESS_RAMP_DONE = 12;
-    private static final int MSG_STATSD_HBM_BRIGHTNESS = 13;
-
-    private static final int PROXIMITY_UNKNOWN = -1;
-    private static final int PROXIMITY_NEGATIVE = 0;
-    private static final int PROXIMITY_POSITIVE = 1;
-
-    // Proximity sensor debounce delay in milliseconds for positive or negative transitions.
-    private static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0;
-    private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250;
+    private static final int MSG_SCREEN_ON_UNBLOCKED = 2;
+    private static final int MSG_SCREEN_OFF_UNBLOCKED = 3;
+    private static final int MSG_CONFIGURE_BRIGHTNESS = 4;
+    private static final int MSG_SET_TEMPORARY_BRIGHTNESS = 5;
+    private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 6;
+    private static final int MSG_STOP = 7;
+    private static final int MSG_UPDATE_BRIGHTNESS = 8;
+    private static final int MSG_UPDATE_RBC = 9;
+    private static final int MSG_BRIGHTNESS_RAMP_DONE = 10;
+    private static final int MSG_STATSD_HBM_BRIGHTNESS = 11;
 
     private static final int BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS = 500;
 
-    // Trigger proximity if distance is less than 5 cm.
-    private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f;
 
     // State machine constants for tracking initial brightness ramp skipping when enabled.
     private static final int RAMP_STATE_SKIP_NONE = 0;
@@ -200,9 +185,6 @@
     // Tracker for brightness settings changes.
     private final SettingsObserver mSettingsObserver;
 
-    // The proximity sensor, or null if not available or needed.
-    private Sensor mProximitySensor;
-
     // The doze screen brightness.
     private final float mScreenBrightnessDozeConfig;
 
@@ -266,10 +248,6 @@
     @GuardedBy("mLock")
     private DisplayPowerRequest mPendingRequestLocked;
 
-    // True if a request has been made to wait for the proximity sensor to go negative.
-    @GuardedBy("mLock")
-    private boolean mPendingWaitForNegativeProximityLocked;
-
     // True if the pending power request or wait for negative proximity flag
     // has been changed since the last update occurred.
     @GuardedBy("mLock")
@@ -296,37 +274,7 @@
     // Must only be accessed on the handler thread.
     private DisplayPowerState mPowerState;
 
-    // True if the device should wait for negative proximity sensor before
-    // waking up the screen.  This is set to false as soon as a negative
-    // proximity sensor measurement is observed or when the device is forced to
-    // go to sleep by the user.  While true, the screen remains off.
-    private boolean mWaitingForNegativeProximity;
 
-    // True if the device should not take into account the proximity sensor
-    // until either the proximity sensor state changes, or there is no longer a
-    // request to listen to proximity sensor.
-    private boolean mIgnoreProximityUntilChanged;
-
-    // The actual proximity sensor threshold value.
-    private float mProximityThreshold;
-
-    // Set to true if the proximity sensor listener has been registered
-    // with the sensor manager.
-    private boolean mProximitySensorEnabled;
-
-    // The debounced proximity sensor state.
-    private int mProximity = PROXIMITY_UNKNOWN;
-
-    // The raw non-debounced proximity sensor state.
-    private int mPendingProximity = PROXIMITY_UNKNOWN;
-
-    // -1 if fully debounced. Else, represents the time in ms when the debounce suspend blocker will
-    // be removed. Applies for both positive and negative proximity flips.
-    private long mPendingProximityDebounceTime = -1;
-
-    // True if the screen was turned off because of the proximity sensor.
-    // When the screen turns on again, we report user activity to the power manager.
-    private boolean mScreenOffBecauseOfProximity;
 
     // The currently active screen on unblocker.  This field is non-null whenever
     // we are waiting for a callback to release it and unblock the screen.
@@ -407,6 +355,9 @@
     // a medium of communication between this class and the PowerManagerService.
     private final WakelockController mWakelockController;
 
+    // Tracks and manages the proximity state of the associated display.
+    private final DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+
     // A record of state for skipping brightness ramps.
     private int mSkipRampState = RAMP_STATE_SKIP_NONE;
 
@@ -491,13 +442,20 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
+        mSensorManager = sensorManager;
+        mHandler = new DisplayControllerHandler(handler.getLooper());
+        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
+                .getDisplayDeviceConfig();
         mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
+        mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
+                () -> updatePowerState(), mDisplayId, mSensorManager);
         mTag = "DisplayPowerController2[" + mDisplayId + "]";
 
         mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
         mDisplayStatsId = mUniqueDisplayId.hashCode();
-        mHandler = new DisplayControllerHandler(handler.getLooper());
+
         mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
         mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
 
@@ -508,7 +466,6 @@
         }
 
         mSettingsObserver = new SettingsObserver(mHandler);
-        mSensorManager = sensorManager;
         mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
         mBlanker = blanker;
         mContext = context;
@@ -545,9 +502,6 @@
         mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean(
                 R.bool.config_allowAutoBrightnessWhileDozing);
 
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
-
         loadBrightnessRampRates();
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
                 R.bool.config_skipScreenOnBrightnessRamp);
@@ -611,8 +565,6 @@
         mBrightnessBucketsInDozeConfig = resources.getBoolean(
                 R.bool.config_displayBrightnessBucketsInDoze);
 
-        loadProximitySensor();
-
         mCurrentScreenBrightnessSetting = getScreenBrightnessSetting();
         mScreenBrightnessForVr = getScreenBrightnessForVrSetting();
         mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
@@ -653,7 +605,7 @@
      */
     @Override
     public boolean isProximitySensorAvailable() {
-        return mProximitySensor != null;
+        return mDisplayPowerProximityStateController.isProximitySensorAvailable();
     }
 
     /**
@@ -727,13 +679,8 @@
                 return true;
             }
 
-            boolean changed = false;
-
-            if (waitForNegativeProximity
-                    && !mPendingWaitForNegativeProximityLocked) {
-                mPendingWaitForNegativeProximityLocked = true;
-                changed = true;
-            }
+            boolean changed = mDisplayPowerProximityStateController
+                    .setPendingWaitForNegativeProximityLocked(waitForNegativeProximity);
 
             if (mPendingRequestLocked == null) {
                 mPendingRequestLocked = new DisplayPowerRequest(request);
@@ -790,6 +737,7 @@
                 mDisplayStatsId = mUniqueDisplayId.hashCode();
                 mDisplayDeviceConfig = config;
                 loadFromDisplayDeviceConfig(token, info);
+                mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(config);
                 updatePowerState();
             }
         });
@@ -838,7 +786,6 @@
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
         loadBrightnessRampRates();
-        loadProximitySensor();
         loadNitsRange(mContext.getResources());
         setUpAutoBrightness(mContext.getResources(), mHandler);
         reloadReduceBrightColours();
@@ -903,7 +850,7 @@
 
         // Initialize all of the brightness tracking state
         final float brightness = convertToNits(mPowerState.getScreenBrightness());
-        if (brightness >= PowerManager.BRIGHTNESS_MIN) {
+        if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
         }
         mBrightnessSettingListener = brightnessValue -> {
@@ -1035,7 +982,9 @@
             }
 
             loadAmbientLightSensor();
-            if (mBrightnessTracker != null) {
+            // BrightnessTracker should only use one light sensor, we want to use the light sensor
+            // from the default display and not e.g. temporary displays when switching layouts.
+            if (mBrightnessTracker != null && mDisplayId == Display.DEFAULT_DISPLAY) {
                 mBrightnessTracker.setLightSensor(mLightSensor);
             }
 
@@ -1132,7 +1081,7 @@
 
     /** Clean up all resources that are accessed via the {@link #mHandler} thread. */
     private void cleanupHandlerThreadAfterStop() {
-        setProximitySensorEnabled(false);
+        mDisplayPowerProximityStateController.cleanup();
         mHbmController.stop();
         mBrightnessThrottler.stop();
         mHandler.removeCallbacksAndMessages(null);
@@ -1177,7 +1126,7 @@
 
             if (mPowerRequest == null) {
                 mPowerRequest = new DisplayPowerRequest(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mustInitialize = true;
                 // Assume we're on and bright until told otherwise, since that's the state we turn
@@ -1186,7 +1135,7 @@
             } else if (mPendingRequestChangedLocked) {
                 previousPolicy = mPowerRequest.policy;
                 mPowerRequest.copyFrom(mPendingRequestLocked);
-                updatePendingProximityRequestsLocked();
+                mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
                 mPendingRequestChangedLocked = false;
                 mDisplayReadyLocked = false;
             } else {
@@ -1229,55 +1178,11 @@
         }
         assert (state != Display.STATE_UNKNOWN);
 
-        boolean skipRampBecauseOfProximityChangeToNegative = false;
-        // Apply the proximity sensor.
-        if (mProximitySensor != null) {
-            if (mPowerRequest.useProximitySensor && state != Display.STATE_OFF) {
-                // At this point the policy says that the screen should be on, but we've been
-                // asked to listen to the prox sensor to adjust the display state, so lets make
-                // sure the sensor is on.
-                setProximitySensorEnabled(true);
-                if (!mScreenOffBecauseOfProximity
-                        && mProximity == PROXIMITY_POSITIVE
-                        && !mIgnoreProximityUntilChanged) {
-                    // Prox sensor already reporting "near" so we should turn off the screen.
-                    // Also checked that we aren't currently set to ignore the proximity sensor
-                    // temporarily.
-                    mScreenOffBecauseOfProximity = true;
-                    sendOnProximityPositiveWithWakelock();
-                }
-            } else if (mWaitingForNegativeProximity
-                    && mScreenOffBecauseOfProximity
-                    && mProximity == PROXIMITY_POSITIVE
-                    && state != Display.STATE_OFF) {
-                // The policy says that we should have the screen on, but it's off due to the prox
-                // and we've been asked to wait until the screen is far from the user to turn it
-                // back on. Let keep the prox sensor on so we can tell when it's far again.
-                setProximitySensorEnabled(true);
-            } else {
-                // We haven't been asked to use the prox sensor and we're not waiting on the screen
-                // to turn back on...so lets shut down the prox sensor.
-                setProximitySensorEnabled(false);
-                mWaitingForNegativeProximity = false;
-            }
-
-            if (mScreenOffBecauseOfProximity
-                    && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) {
-                // The screen *was* off due to prox being near, but now it's "far" so lets turn
-                // the screen back on.  Also turn it back on if we've been asked to ignore the
-                // prox sensor temporarily.
-                mScreenOffBecauseOfProximity = false;
-                skipRampBecauseOfProximityChangeToNegative = true;
-                sendOnProximityNegativeWithWakelock();
-            }
-        } else {
-            mWaitingForNegativeProximity = false;
-            mIgnoreProximityUntilChanged = false;
-        }
+        mDisplayPowerProximityStateController.updateProximityState(mPowerRequest, state);
 
         if (!mLogicalDisplay.isEnabled()
                 || mLogicalDisplay.getPhase() == LogicalDisplay.DISPLAY_PHASE_LAYOUT_TRANSITION
-                || mScreenOffBecauseOfProximity) {
+                || mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
             state = Display.STATE_OFF;
         }
 
@@ -1548,7 +1453,8 @@
             final boolean wasOrWillBeInVr =
                     (state == Display.STATE_VR || oldState == Display.STATE_VR);
             final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
-                    != RAMP_STATE_SKIP_NONE) || skipRampBecauseOfProximityChangeToNegative;
+                    != RAMP_STATE_SKIP_NONE) || mDisplayPowerProximityStateController
+                    .shouldSkipRampBecauseOfProximityChangeToNegative();
             // While dozing, sometimes the brightness is split into buckets. Rather than animating
             // through the buckets, which is unlikely to be smooth in the first place, just jump
             // right to the suggested brightness.
@@ -1662,6 +1568,7 @@
         mTempBrightnessEvent.setRbcStrength(mCdsi != null
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
         mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
+        mTempBrightnessEvent.setWasShortTermModelActive(hadUserBrightnessPoint);
         // Temporary is what we use during slider interactions. We avoid logging those so that
         // we don't spam logcat when the slider is being used.
         boolean tempToTempTransition =
@@ -1672,12 +1579,6 @@
                 || brightnessAdjustmentFlags != 0) {
             float lastBrightness = mLastBrightnessEvent.getBrightness();
             mTempBrightnessEvent.setInitialBrightness(lastBrightness);
-            mTempBrightnessEvent.setFastAmbientLux(
-                    mAutomaticBrightnessController == null
-                        ? -1f : mAutomaticBrightnessController.getFastAmbientLux());
-            mTempBrightnessEvent.setSlowAmbientLux(
-                    mAutomaticBrightnessController == null
-                        ? -1f : mAutomaticBrightnessController.getSlowAmbientLux());
             mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness);
             mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
             BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
@@ -1768,7 +1669,7 @@
      */
     @Override
     public void ignoreProximitySensorUntilChanged() {
-        mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY);
+        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChanged();
     }
 
     @Override
@@ -1934,7 +1835,7 @@
                 || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
             // If we are trying to turn screen off, give policy a chance to do something before we
             // actually turn the screen off.
-            if (isOff && !mScreenOffBecauseOfProximity) {
+            if (isOff && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
                 if (mReportedScreenStateToPolicy == REPORTED_TO_POLICY_SCREEN_ON
                         || mReportedScreenStateToPolicy == REPORTED_TO_POLICY_UNREPORTED) {
                     setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF);
@@ -1964,7 +1865,7 @@
         // it is only removed once the window manager tells us that the activity has
         // finished drawing underneath.
         if (isOff && mReportedScreenStateToPolicy != REPORTED_TO_POLICY_SCREEN_OFF
-                && !mScreenOffBecauseOfProximity) {
+                && !mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) {
             setReportedScreenState(REPORTED_TO_POLICY_SCREEN_OFF);
             unblockScreenOn();
             mWindowManagerPolicy.screenTurnedOff(mDisplayId);
@@ -2006,22 +1907,6 @@
                 fallbackType);
     }
 
-    private void loadProximitySensor() {
-        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
-            return;
-        }
-        final DisplayDeviceConfig.SensorData proxSensor =
-                mDisplayDeviceConfig.getProximitySensor();
-        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
-                ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK;
-        mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
-                fallbackType);
-        if (mProximitySensor != null) {
-            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
-                    TYPICAL_PROXIMITY_THRESHOLD);
-        }
-    }
-
     private float clampScreenBrightnessForVr(float value) {
         return MathUtils.constrain(
                 value, mScreenBrightnessForVrRangeMinimum,
@@ -2223,98 +2108,6 @@
 
     private final Runnable mCleanListener = this::sendUpdatePowerState;
 
-    private void setProximitySensorEnabled(boolean enable) {
-        if (enable) {
-            if (!mProximitySensorEnabled) {
-                // Register the listener.
-                // Proximity sensor state already cleared initially.
-                mProximitySensorEnabled = true;
-                mIgnoreProximityUntilChanged = false;
-                mSensorManager.registerListener(mProximitySensorListener, mProximitySensor,
-                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
-            }
-        } else {
-            if (mProximitySensorEnabled) {
-                // Unregister the listener.
-                // Clear the proximity sensor state for next time.
-                mProximitySensorEnabled = false;
-                mProximity = PROXIMITY_UNKNOWN;
-                mIgnoreProximityUntilChanged = false;
-                mPendingProximity = PROXIMITY_UNKNOWN;
-                mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mSensorManager.unregisterListener(mProximitySensorListener);
-                // release wake lock(must be last)
-                boolean proxDebounceSuspendBlockerReleased =
-                        mWakelockController.releaseWakelock(
-                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-                if (proxDebounceSuspendBlockerReleased) {
-                    mPendingProximityDebounceTime = -1;
-                }
-            }
-        }
-    }
-
-    private void handleProximitySensorEvent(long time, boolean positive) {
-        if (mProximitySensorEnabled) {
-            if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) {
-                return; // no change
-            }
-            if (mPendingProximity == PROXIMITY_POSITIVE && positive) {
-                return; // no change
-            }
-
-            // Only accept a proximity sensor reading if it remains
-            // stable for the entire debounce delay.  We hold a wake lock while
-            // debouncing the sensor.
-            mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-            if (positive) {
-                mPendingProximity = PROXIMITY_POSITIVE;
-                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY;
-                mWakelockController.acquireWakelock(
-                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
-            } else {
-                mPendingProximity = PROXIMITY_NEGATIVE;
-                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY;
-                mWakelockController.acquireWakelock(
-                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
-            }
-
-            // Debounce the new sensor reading.
-            debounceProximitySensor();
-        }
-    }
-
-    private void debounceProximitySensor() {
-        if (mProximitySensorEnabled
-                && mPendingProximity != PROXIMITY_UNKNOWN
-                && mPendingProximityDebounceTime >= 0) {
-            final long now = mClock.uptimeMillis();
-            if (mPendingProximityDebounceTime <= now) {
-                if (mProximity != mPendingProximity) {
-                    // if the status of the sensor changed, stop ignoring.
-                    mIgnoreProximityUntilChanged = false;
-                    Slog.i(mTag, "No longer ignoring proximity [" + mPendingProximity + "]");
-                }
-                // Sensor reading accepted.  Apply the change then release the wake lock.
-                mProximity = mPendingProximity;
-                updatePowerState();
-                // (must be last)
-                boolean proxDebounceSuspendBlockerReleased =
-                        mWakelockController.releaseWakelock(
-                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
-                if (proxDebounceSuspendBlockerReleased) {
-                    mPendingProximityDebounceTime = -1;
-                }
-
-            } else {
-                // Need to wait a little longer.
-                // Debounce again later.  We continue holding a wake lock while waiting.
-                Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
-            }
-        }
-    }
-
     private void sendOnStateChangedWithWakelock() {
         boolean wakeLockAcquired = mWakelockController.acquireWakelock(
                 WakelockController.WAKE_LOCK_STATE_CHANGED);
@@ -2439,7 +2232,7 @@
             boolean hadUserDataPoint) {
         final float brightnessInNits = convertToNits(brightness);
         if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f
-                && mAutomaticBrightnessController != null) {
+                && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
             // values into a physical brightness unit since the value provided by the API is in
             // nits and not using the arbitrary backlight units.
@@ -2459,39 +2252,6 @@
         return mAutomaticBrightnessController.convertToNits(brightness);
     }
 
-    @GuardedBy("mLock")
-    private void updatePendingProximityRequestsLocked() {
-        mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
-        mPendingWaitForNegativeProximityLocked = false;
-
-        if (mIgnoreProximityUntilChanged) {
-            // Also, lets stop waiting for negative proximity if we're ignoring it.
-            mWaitingForNegativeProximity = false;
-        }
-    }
-
-    private void ignoreProximitySensorUntilChangedInternal() {
-        if (!mIgnoreProximityUntilChanged
-                && mProximity == PROXIMITY_POSITIVE) {
-            // Only ignore if it is still reporting positive (near)
-            mIgnoreProximityUntilChanged = true;
-            Slog.i(mTag, "Ignoring proximity");
-            updatePowerState();
-        }
-    }
-
-    private void sendOnProximityPositiveWithWakelock() {
-        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE);
-        mHandler.post(mWakelockController.getOnProximityPositiveRunnable());
-    }
-
-
-    private void sendOnProximityNegativeWithWakelock() {
-        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE);
-        mHandler.post(mWakelockController.getOnProximityNegativeRunnable());
-    }
-
-
     @Override
     public void dump(final PrintWriter pw) {
         synchronized (mLock) {
@@ -2505,8 +2265,6 @@
             pw.println("  mDisplayReadyLocked=" + mDisplayReadyLocked);
             pw.println("  mPendingRequestLocked=" + mPendingRequestLocked);
             pw.println("  mPendingRequestChangedLocked=" + mPendingRequestChangedLocked);
-            pw.println("  mPendingWaitForNegativeProximityLocked="
-                    + mPendingWaitForNegativeProximityLocked);
             pw.println("  mPendingUpdatePowerStateLocked=" + mPendingUpdatePowerStateLocked);
         }
 
@@ -2541,7 +2299,6 @@
         }
         pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
         pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
-
         mHandler.runWithScissors(() -> dumpLocal(pw), 1000);
     }
 
@@ -2549,15 +2306,6 @@
         pw.println();
         pw.println("Display Power Controller Thread State:");
         pw.println("  mPowerRequest=" + mPowerRequest);
-        pw.println("  mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
-        pw.println("  mProximitySensor=" + mProximitySensor);
-        pw.println("  mProximitySensorEnabled=" + mProximitySensorEnabled);
-        pw.println("  mProximityThreshold=" + mProximityThreshold);
-        pw.println("  mProximity=" + proximityToString(mProximity));
-        pw.println("  mPendingProximity=" + proximityToString(mPendingProximity));
-        pw.println("  mPendingProximityDebounceTime="
-                + TimeUtils.formatUptime(mPendingProximityDebounceTime));
-        pw.println("  mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
         pw.println("  mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness);
         pw.println("  mPendingScreenBrightnessSetting="
                 + mPendingScreenBrightnessSetting);
@@ -2629,21 +2377,13 @@
         if (mWakelockController != null) {
             mWakelockController.dumpLocal(pw);
         }
-    }
 
-    private static String proximityToString(int state) {
-        switch (state) {
-            case PROXIMITY_UNKNOWN:
-                return "Unknown";
-            case PROXIMITY_NEGATIVE:
-                return "Negative";
-            case PROXIMITY_POSITIVE:
-                return "Positive";
-            default:
-                return Integer.toString(state);
+        if (mDisplayPowerProximityStateController != null) {
+            mDisplayPowerProximityStateController.dumpLocal(pw);
         }
     }
 
+
     private static String reportedToPolicyToString(int state) {
         switch (state) {
             case REPORTED_TO_POLICY_SCREEN_OFF:
@@ -2766,19 +2506,22 @@
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
                 ? -1f : convertToNits(event.getThermalMax());
-
-        FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                convertToNits(event.getInitialBrightness()),
-                convertToNits(event.getBrightness()),
-                event.getSlowAmbientLux(),
-                event.getPhysicalDisplayId(),
-                event.isShortTermModelActive(),
-                appliedLowPowerMode,
-                appliedRbcStrength,
-                appliedHbmMaxNits,
-                appliedThermalCapNits,
-                event.isAutomaticBrightnessEnabled(),
-                FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        if (mLogicalDisplay.getPrimaryDisplayDeviceLocked() != null
+                && mLogicalDisplay.getPrimaryDisplayDeviceLocked()
+                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL) {
+            FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
+                    convertToNits(event.getInitialBrightness()),
+                    convertToNits(event.getBrightness()),
+                    event.getLux(),
+                    event.getPhysicalDisplayId(),
+                    event.wasShortTermModelActive(),
+                    appliedLowPowerMode,
+                    appliedRbcStrength,
+                    appliedHbmMaxNits,
+                    appliedThermalCapNits,
+                    event.isAutomaticBrightnessEnabled(),
+                    FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
+        }
     }
 
     private final class DisplayControllerHandler extends Handler {
@@ -2793,10 +2536,6 @@
                     updatePowerState();
                     break;
 
-                case MSG_PROXIMITY_SENSOR_DEBOUNCED:
-                    debounceProximitySensor();
-                    break;
-
                 case MSG_SCREEN_ON_UNBLOCKED:
                     if (mPendingScreenOnUnblocker == msg.obj) {
                         unblockScreenOn();
@@ -2825,10 +2564,6 @@
                     updatePowerState();
                     break;
 
-                case MSG_IGNORE_PROXIMITY:
-                    ignoreProximitySensorUntilChangedInternal();
-                    break;
-
                 case MSG_STOP:
                     cleanupHandlerThreadAfterStop();
                     break;
@@ -2858,23 +2593,6 @@
         }
     }
 
-    private final SensorEventListener mProximitySensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mProximitySensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float distance = event.values[0];
-                boolean positive = distance >= 0.0f && distance < mProximityThreshold;
-                handleProximitySensorEvent(time, positive);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
 
     private final class SettingsObserver extends ContentObserver {
         SettingsObserver(Handler handler) {
@@ -2964,6 +2682,15 @@
                 DisplayPowerCallbacks displayPowerCallbacks) {
             return new WakelockController(displayId, displayPowerCallbacks);
         }
+
+        DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                Looper looper, Runnable nudgeUpdatePowerState,
+                int displayId, SensorManager sensorManager) {
+            return new DisplayPowerProximityStateController(wakelockController, displayDeviceConfig,
+                    looper, nudgeUpdatePowerState,
+                    displayId, sensorManager, /* injector= */ null);
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
new file mode 100644
index 0000000..a3433d9
--- /dev/null
+++ b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java
@@ -0,0 +1,550 @@
+/*
+ * 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.display;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.utils.SensorUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Maintains the proximity state of the display.
+ * Internally listens for proximity updates and schedules a power state update when the proximity
+ * state changes.
+ */
+public final class DisplayPowerProximityStateController {
+    @VisibleForTesting
+    static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 1;
+    @VisibleForTesting
+    static final int PROXIMITY_UNKNOWN = -1;
+    @VisibleForTesting
+    static final int PROXIMITY_POSITIVE = 1;
+    @VisibleForTesting
+    static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0;
+
+    private static final int MSG_IGNORE_PROXIMITY = 2;
+
+    private static final int PROXIMITY_NEGATIVE = 0;
+
+    private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
+    // Proximity sensor debounce delay in milliseconds for positive transitions.
+
+    // Proximity sensor debounce delay in milliseconds for negative transitions.
+    private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250;
+    // Trigger proximity if distance is less than 5 cm.
+    private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f;
+
+    private final String mTag;
+    // A lock to handle the deadlock and race conditions.
+    private final Object mLock = new Object();
+    // The manager which lets us access the device's ProximitySensor
+    private final SensorManager mSensorManager;
+    // An entity which manages the wakelocks.
+    private final WakelockController mWakelockController;
+    // A handler to process all the events on this thread in a synchronous manner
+    private final DisplayPowerProximityStateHandler mHandler;
+    // A runnable to execute the utility to update the power state.
+    private final Runnable mNudgeUpdatePowerState;
+    private Clock mClock;
+    // A listener which listen's to the events emitted by the proximity sensor.
+    private final SensorEventListener mProximitySensorListener = new SensorEventListener() {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (mProximitySensorEnabled) {
+                final long time = mClock.uptimeMillis();
+                final float distance = event.values[0];
+                boolean positive = distance >= 0.0f && distance < mProximityThreshold;
+                handleProximitySensorEvent(time, positive);
+            }
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // Not used.
+        }
+    };
+
+    // The proximity sensor, or null if not available or needed.
+    private Sensor mProximitySensor;
+
+    // The configurations for the associated display
+    private DisplayDeviceConfig mDisplayDeviceConfig;
+
+    // True if a request has been made to wait for the proximity sensor to go negative.
+    @GuardedBy("mLock")
+    private boolean mPendingWaitForNegativeProximityLocked;
+
+    // True if the device should wait for negative proximity sensor before
+    // waking up the screen.  This is set to false as soon as a negative
+    // proximity sensor measurement is observed or when the device is forced to
+    // go to sleep by the user.  While true, the screen remains off.
+    private boolean mWaitingForNegativeProximity;
+
+    // True if the device should not take into account the proximity sensor
+    // until either the proximity sensor state changes, or there is no longer a
+    // request to listen to proximity sensor.
+    private boolean mIgnoreProximityUntilChanged;
+
+    // Set to true if the proximity sensor listener has been registered
+    // with the sensor manager.
+    private boolean mProximitySensorEnabled;
+
+    // The raw non-debounced proximity sensor state.
+    private int mPendingProximity = PROXIMITY_UNKNOWN;
+
+    // -1 if fully debounced. Else, represents the time in ms when the debounce suspend blocker will
+    // be removed. Applies for both positive and negative proximity flips.
+    private long mPendingProximityDebounceTime = -1;
+
+    // True if the screen was turned off because of the proximity sensor.
+    // When the screen turns on again, we report user activity to the power manager.
+    private boolean mScreenOffBecauseOfProximity;
+
+    // The debounced proximity sensor state.
+    private int mProximity = PROXIMITY_UNKNOWN;
+
+    // The actual proximity sensor threshold value.
+    private float mProximityThreshold;
+
+    // A flag representing if the ramp is to be skipped when the proximity changes from positive
+    // to negative
+    private boolean mSkipRampBecauseOfProximityChangeToNegative = false;
+
+    // The DisplayId of the associated Logical Display.
+    private int mDisplayId;
+
+    /**
+     * Create a new instance of DisplayPowerProximityStateController.
+     *
+     * @param wakeLockController    WakelockController used to acquire/release wakelocks
+     * @param displayDeviceConfig   DisplayDeviceConfig instance from which the configs(Proximity
+     *                              Sensor) are to be loaded
+     * @param looper                A looper onto which the handler is to be associated.
+     * @param nudgeUpdatePowerState A runnable to execute the utility to update the power state
+     * @param displayId             The DisplayId of the associated Logical Display.
+     * @param sensorManager         The manager which lets us access the display's ProximitySensor
+     */
+    public DisplayPowerProximityStateController(
+            WakelockController wakeLockController, DisplayDeviceConfig displayDeviceConfig,
+            Looper looper,
+            Runnable nudgeUpdatePowerState, int displayId, SensorManager sensorManager,
+            Injector injector) {
+        if (injector == null) {
+            injector = new Injector();
+        }
+        mClock = injector.createClock();
+        mWakelockController = wakeLockController;
+        mHandler = new DisplayPowerProximityStateHandler(looper);
+        mNudgeUpdatePowerState = nudgeUpdatePowerState;
+        mDisplayDeviceConfig = displayDeviceConfig;
+        mDisplayId = displayId;
+        mTag = "DisplayPowerProximityStateController[" + mDisplayId + "]";
+        mSensorManager = sensorManager;
+        loadProximitySensor();
+    }
+
+    /**
+     * Manages the pending state of the proximity.
+     */
+    public void updatePendingProximityRequestsLocked() {
+        synchronized (mLock) {
+            mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
+            mPendingWaitForNegativeProximityLocked = false;
+
+            if (mIgnoreProximityUntilChanged) {
+                // Also, lets stop waiting for negative proximity if we're ignoring it.
+                mWaitingForNegativeProximity = false;
+            }
+        }
+    }
+
+    /**
+     * Clean up all resources that are accessed via the {@link #mHandler} thread.
+     */
+    public void cleanup() {
+        setProximitySensorEnabled(false);
+    }
+
+    /**
+     * Returns true if the proximity sensor screen-off function is available.
+     */
+    public boolean isProximitySensorAvailable() {
+        return mProximitySensor != null;
+    }
+
+    /**
+     * Sets the flag to indicate that the system is waiting for the negative proximity event
+     */
+    public boolean setPendingWaitForNegativeProximityLocked(
+            boolean requestWaitForNegativeProximity) {
+        synchronized (mLock) {
+            if (requestWaitForNegativeProximity
+                    && !mPendingWaitForNegativeProximityLocked) {
+                mPendingWaitForNegativeProximityLocked = true;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Updates the proximity state of the display, based on the newly received DisplayPowerRequest
+     * and the target display state
+     */
+    public void updateProximityState(
+            DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
+            int displayState) {
+        mSkipRampBecauseOfProximityChangeToNegative = false;
+        if (mProximitySensor != null) {
+            if (displayPowerRequest.useProximitySensor && displayState != Display.STATE_OFF) {
+                // At this point the policy says that the screen should be on, but we've been
+                // asked to listen to the prox sensor to adjust the display state, so lets make
+                // sure the sensor is on.
+                setProximitySensorEnabled(true);
+                if (!mScreenOffBecauseOfProximity
+                        && mProximity == PROXIMITY_POSITIVE
+                        && !mIgnoreProximityUntilChanged) {
+                    // Prox sensor already reporting "near" so we should turn off the screen.
+                    // Also checked that we aren't currently set to ignore the proximity sensor
+                    // temporarily.
+                    mScreenOffBecauseOfProximity = true;
+                    sendOnProximityPositiveWithWakelock();
+                }
+            } else if (mWaitingForNegativeProximity
+                    && mScreenOffBecauseOfProximity
+                    && mProximity == PROXIMITY_POSITIVE
+                    && displayState != Display.STATE_OFF) {
+                // The policy says that we should have the screen on, but it's off due to the prox
+                // and we've been asked to wait until the screen is far from the user to turn it
+                // back on. Let keep the prox sensor on so we can tell when it's far again.
+                setProximitySensorEnabled(true);
+            } else {
+                // We haven't been asked to use the prox sensor and we're not waiting on the screen
+                // to turn back on...so let's shut down the prox sensor.
+                setProximitySensorEnabled(false);
+                mWaitingForNegativeProximity = false;
+            }
+            if (mScreenOffBecauseOfProximity
+                    && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) {
+                // The screen *was* off due to prox being near, but now it's "far" so lets turn
+                // the screen back on.  Also turn it back on if we've been asked to ignore the
+                // prox sensor temporarily.
+                mScreenOffBecauseOfProximity = false;
+                mSkipRampBecauseOfProximityChangeToNegative = true;
+                sendOnProximityNegativeWithWakelock();
+            }
+        } else {
+            mWaitingForNegativeProximity = false;
+            mIgnoreProximityUntilChanged = false;
+        }
+    }
+
+    /**
+     * A utility to check if the brightness change ramp is to be skipped because the proximity was
+     * changed from positive to negative.
+     */
+    public boolean shouldSkipRampBecauseOfProximityChangeToNegative() {
+        return mSkipRampBecauseOfProximityChangeToNegative;
+    }
+
+    /**
+     * Represents of the screen is currently turned off because of the proximity state.
+     */
+    public boolean isScreenOffBecauseOfProximity() {
+        return mScreenOffBecauseOfProximity;
+    }
+
+    /**
+     * Ignores the proximity sensor until the sensor state changes, but only if the sensor is
+     * currently enabled and forcing the screen to be dark.
+     */
+    public void ignoreProximitySensorUntilChanged() {
+        mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY);
+    }
+
+    /**
+     * This adjusts the state of this class when a change in the DisplayDevice is detected.
+     */
+    public void notifyDisplayDeviceChanged(DisplayDeviceConfig displayDeviceConfig) {
+        this.mDisplayDeviceConfig = displayDeviceConfig;
+        loadProximitySensor();
+    }
+
+    /**
+     * Used to dump the state.
+     *
+     * @param pw The PrintWriter used to dump the state.
+     */
+    public void dumpLocal(PrintWriter pw) {
+        pw.println();
+        pw.println("DisplayPowerProximityStateController:");
+        synchronized (mLock) {
+            pw.println("  mPendingWaitForNegativeProximityLocked="
+                    + mPendingWaitForNegativeProximityLocked);
+        }
+        pw.println("  mDisplayId=" + mDisplayId);
+        pw.println("  mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
+        pw.println("  mIgnoreProximityUntilChanged=" + mIgnoreProximityUntilChanged);
+        pw.println("  mProximitySensor=" + mProximitySensor);
+        pw.println("  mProximitySensorEnabled=" + mProximitySensorEnabled);
+        pw.println("  mProximityThreshold=" + mProximityThreshold);
+        pw.println("  mProximity=" + proximityToString(mProximity));
+        pw.println("  mPendingProximity=" + proximityToString(mPendingProximity));
+        pw.println("  mPendingProximityDebounceTime="
+                + TimeUtils.formatUptime(mPendingProximityDebounceTime));
+        pw.println("  mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
+        pw.println("  mSkipRampBecauseOfProximityChangeToNegative="
+                + mSkipRampBecauseOfProximityChangeToNegative);
+    }
+
+    void ignoreProximitySensorUntilChangedInternal() {
+        if (!mIgnoreProximityUntilChanged
+                && mProximity == PROXIMITY_POSITIVE) {
+            // Only ignore if it is still reporting positive (near)
+            mIgnoreProximityUntilChanged = true;
+            Slog.i(mTag, "Ignoring proximity");
+            mNudgeUpdatePowerState.run();
+        }
+    }
+
+    private void sendOnProximityPositiveWithWakelock() {
+        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE);
+        mHandler.post(mWakelockController.getOnProximityPositiveRunnable());
+    }
+
+    private void sendOnProximityNegativeWithWakelock() {
+        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE);
+        mHandler.post(mWakelockController.getOnProximityNegativeRunnable());
+    }
+
+    private void loadProximitySensor() {
+        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
+            return;
+        }
+        final DisplayDeviceConfig.SensorData proxSensor =
+                mDisplayDeviceConfig.getProximitySensor();
+        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
+                ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK;
+        mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
+                fallbackType);
+        if (mProximitySensor != null) {
+            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
+                    TYPICAL_PROXIMITY_THRESHOLD);
+        }
+    }
+
+    private void setProximitySensorEnabled(boolean enable) {
+        if (enable) {
+            if (!mProximitySensorEnabled) {
+                // Register the listener.
+                // Proximity sensor state already cleared initially.
+                mProximitySensorEnabled = true;
+                mIgnoreProximityUntilChanged = false;
+                mSensorManager.registerListener(mProximitySensorListener, mProximitySensor,
+                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
+            }
+        } else {
+            if (mProximitySensorEnabled) {
+                // Unregister the listener.
+                // Clear the proximity sensor state for next time.
+                mProximitySensorEnabled = false;
+                mProximity = PROXIMITY_UNKNOWN;
+                mIgnoreProximityUntilChanged = false;
+                mPendingProximity = PROXIMITY_UNKNOWN;
+                mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
+                mSensorManager.unregisterListener(mProximitySensorListener);
+                // release wake lock(must be last)
+                boolean proxDebounceSuspendBlockerReleased =
+                        mWakelockController.releaseWakelock(
+                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
+                if (proxDebounceSuspendBlockerReleased) {
+                    mPendingProximityDebounceTime = -1;
+                }
+            }
+        }
+    }
+
+    private void handleProximitySensorEvent(long time, boolean positive) {
+        if (mProximitySensorEnabled) {
+            if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) {
+                return; // no change
+            }
+            if (mPendingProximity == PROXIMITY_POSITIVE && positive) {
+                return; // no change
+            }
+
+            // Only accept a proximity sensor reading if it remains
+            // stable for the entire debounce delay.  We hold a wake lock while
+            // debouncing the sensor.
+            mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
+            if (positive) {
+                mPendingProximity = PROXIMITY_POSITIVE;
+                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY;
+                mWakelockController.acquireWakelock(
+                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
+            } else {
+                mPendingProximity = PROXIMITY_NEGATIVE;
+                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY;
+                mWakelockController.acquireWakelock(
+                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
+            }
+
+            // Debounce the new sensor reading.
+            debounceProximitySensor();
+        }
+    }
+
+    private void debounceProximitySensor() {
+        if (mProximitySensorEnabled
+                && mPendingProximity != PROXIMITY_UNKNOWN
+                && mPendingProximityDebounceTime >= 0) {
+            final long now = mClock.uptimeMillis();
+            if (mPendingProximityDebounceTime <= now) {
+                if (mProximity != mPendingProximity) {
+                    // if the status of the sensor changed, stop ignoring.
+                    mIgnoreProximityUntilChanged = false;
+                    Slog.i(mTag, "No longer ignoring proximity [" + mPendingProximity + "]");
+                }
+                // Sensor reading accepted.  Apply the change then release the wake lock.
+                mProximity = mPendingProximity;
+                mNudgeUpdatePowerState.run();
+                // (must be last)
+                boolean proxDebounceSuspendBlockerReleased =
+                        mWakelockController.releaseWakelock(
+                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
+                if (proxDebounceSuspendBlockerReleased) {
+                    mPendingProximityDebounceTime = -1;
+                }
+
+            } else {
+                // Need to wait a little longer.
+                // Debounce again later.  We continue holding a wake lock while waiting.
+                Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
+                mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
+            }
+        }
+    }
+
+    private class DisplayPowerProximityStateHandler extends Handler {
+        DisplayPowerProximityStateHandler(Looper looper) {
+            super(looper, null, true /*async*/);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_PROXIMITY_SENSOR_DEBOUNCED:
+                    debounceProximitySensor();
+                    break;
+
+                case MSG_IGNORE_PROXIMITY:
+                    ignoreProximitySensorUntilChangedInternal();
+                    break;
+            }
+        }
+    }
+
+    private String proximityToString(int state) {
+        switch (state) {
+            case PROXIMITY_UNKNOWN:
+                return "Unknown";
+            case PROXIMITY_NEGATIVE:
+                return "Negative";
+            case PROXIMITY_POSITIVE:
+                return "Positive";
+            default:
+                return Integer.toString(state);
+        }
+    }
+
+    @VisibleForTesting
+    boolean getPendingWaitForNegativeProximityLocked() {
+        synchronized (mLock) {
+            return mPendingWaitForNegativeProximityLocked;
+        }
+    }
+
+    @VisibleForTesting
+    boolean getWaitingForNegativeProximity() {
+        return mWaitingForNegativeProximity;
+    }
+
+    @VisibleForTesting
+    boolean shouldIgnoreProximityUntilChanged() {
+        return mIgnoreProximityUntilChanged;
+    }
+
+    boolean isProximitySensorEnabled() {
+        return mProximitySensorEnabled;
+    }
+
+    @VisibleForTesting
+    Handler getHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    int getPendingProximity() {
+        return mPendingProximity;
+    }
+
+    @VisibleForTesting
+    int getProximity() {
+        return mProximity;
+    }
+
+
+    @VisibleForTesting
+    long getPendingProximityDebounceTime() {
+        return mPendingProximityDebounceTime;
+    }
+
+    @VisibleForTesting
+    SensorEventListener getProximitySensorListener() {
+        return mProximitySensorListener;
+    }
+
+    /** Functional interface for providing time. */
+    @VisibleForTesting
+    interface Clock {
+        /**
+         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
+         */
+        long uptimeMillis();
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        Clock createClock() {
+            return () -> SystemClock.uptimeMillis();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 002209e..2c2075d 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -387,14 +387,8 @@
                 // list of available modes will take care of updating display mode specs.
                 if (activeBaseMode == INVALID_MODE_ID
                         || mDisplayModeSpecs.baseModeId != activeBaseMode
-                        || mDisplayModeSpecs.primaryRefreshRateRange.min
-                                != modeSpecs.primaryRefreshRateMin
-                        || mDisplayModeSpecs.primaryRefreshRateRange.max
-                                != modeSpecs.primaryRefreshRateMax
-                        || mDisplayModeSpecs.appRequestRefreshRateRange.min
-                                != modeSpecs.appRequestRefreshRateMin
-                        || mDisplayModeSpecs.appRequestRefreshRateRange.max
-                                != modeSpecs.appRequestRefreshRateMax) {
+                        || !mDisplayModeSpecs.primary.equals(modeSpecs.primaryRanges)
+                        || !mDisplayModeSpecs.appRequest.equals(modeSpecs.appRequestRanges)) {
                     mDisplayModeSpecsInvalid = true;
                     sendTraversalRequestLocked();
                 }
@@ -997,10 +991,8 @@
                         getDisplayTokenLocked(),
                         new SurfaceControl.DesiredDisplayModeSpecs(baseSfModeId,
                                 mDisplayModeSpecs.allowGroupSwitching,
-                                mDisplayModeSpecs.primaryRefreshRateRange.min,
-                                mDisplayModeSpecs.primaryRefreshRateRange.max,
-                                mDisplayModeSpecs.appRequestRefreshRateRange.min,
-                                mDisplayModeSpecs.appRequestRefreshRateRange.max)));
+                                mDisplayModeSpecs.primary,
+                                mDisplayModeSpecs.appRequest)));
             }
         }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 70c9e23..cb97e28 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -28,6 +28,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -123,6 +124,12 @@
     /** Map of all display groups indexed by display group id. */
     private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>();
 
+    /**
+     * Map of display groups which are linked to virtual devices (all displays in the group are
+     * linked to that device). Keyed by virtual device unique id.
+     */
+    private final SparseIntArray mDeviceDisplayGroupIds = new SparseIntArray();
+
     private final DisplayDeviceRepository mDisplayDeviceRepo;
     private final DeviceStateToLayoutMap mDeviceStateToLayoutMap;
     private final Listener mListener;
@@ -157,6 +164,12 @@
      */
     private final SparseIntArray mDisplayGroupsToUpdate = new SparseIntArray();
 
+    /**
+     * ArrayMap of display device unique ID to virtual device ID. Used in {@link
+     * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays.
+     */
+    private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>();
+
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
@@ -362,6 +375,19 @@
         mDeviceStateToLayoutMap.dumpLocked(ipw);
     }
 
+    /**
+     * Creates an association between a displayDevice and a virtual device. Any displays associated
+     * with this virtual device will be grouped together in a single {@link DisplayGroup} unless
+     * created with {@link Display.FLAG_OWN_DISPLAY_GROUP}.
+     *
+     * @param displayDevice the displayDevice to be linked
+     * @param virtualDeviceUniqueId the unique ID of the virtual device.
+     */
+    void associateDisplayDeviceWithVirtualDevice(
+            DisplayDevice displayDevice, int virtualDeviceUniqueId) {
+        mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId);
+    }
+
     void setDeviceStateLocked(int state, boolean isOverrideActive) {
         Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
                 + ", interactive=" + mInteractive);
@@ -556,6 +582,9 @@
         }
         DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked();
 
+        // Remove any virtual device mapping which exists for the display.
+        mVirtualDeviceDisplayMapping.remove(device.getUniqueId());
+
         if (layoutDisplay.getAddress().equals(deviceInfo.address)) {
             layout.removeDisplayLocked(DEFAULT_DISPLAY);
 
@@ -749,24 +778,44 @@
                 // We wait until we sent the EVENT_REMOVED event before actually removing the
                 // group.
                 mDisplayGroups.delete(id);
+                // Remove possible reference to the removed group.
+                int deviceIndex = mDeviceDisplayGroupIds.indexOfValue(id);
+                if (deviceIndex >= 0) {
+                    mDeviceDisplayGroupIds.removeAt(deviceIndex);
+                }
             }
         }
     }
 
     private void assignDisplayGroupLocked(LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
+        final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId();
+        final Integer linkedDeviceUniqueId =
+                mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId);
 
         // Get current display group data
         int groupId = getDisplayGroupIdFromDisplayIdLocked(displayId);
+        Integer deviceDisplayGroupId = null;
+        if (linkedDeviceUniqueId != null
+                && mDeviceDisplayGroupIds.indexOfKey(linkedDeviceUniqueId) > 0) {
+            deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+        }
         final DisplayGroup oldGroup = getDisplayGroupLocked(groupId);
 
         // Get the new display group if a change is needed
         final DisplayInfo info = display.getDisplayInfoLocked();
         final boolean needsOwnDisplayGroup = (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0;
         final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP;
+        final boolean needsDeviceDisplayGroup =
+                !needsOwnDisplayGroup && linkedDeviceUniqueId != null;
+        final boolean hasDeviceDisplayGroup =
+                deviceDisplayGroupId != null && groupId == deviceDisplayGroupId;
         if (groupId == Display.INVALID_DISPLAY_GROUP
-                || hasOwnDisplayGroup != needsOwnDisplayGroup) {
-            groupId = assignDisplayGroupIdLocked(needsOwnDisplayGroup);
+                || hasOwnDisplayGroup != needsOwnDisplayGroup
+                || hasDeviceDisplayGroup != needsDeviceDisplayGroup) {
+            groupId =
+                    assignDisplayGroupIdLocked(
+                            needsOwnDisplayGroup, needsDeviceDisplayGroup, linkedDeviceUniqueId);
         }
 
         // Create a new group if needed
@@ -931,7 +980,17 @@
         display.setPhase(phase);
     }
 
-    private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) {
+    private int assignDisplayGroupIdLocked(
+            boolean isOwnDisplayGroup, boolean isDeviceDisplayGroup, Integer linkedDeviceUniqueId) {
+        if (isDeviceDisplayGroup && linkedDeviceUniqueId != null) {
+            int deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+            // A value of 0 indicates that no device display group was found.
+            if (deviceDisplayGroupId == 0) {
+                deviceDisplayGroupId = mNextNonDefaultGroupId++;
+                mDeviceDisplayGroupIds.put(linkedDeviceUniqueId, deviceDisplayGroupId);
+            }
+            return deviceDisplayGroupId;
+        }
         return isOwnDisplayGroup ? mNextNonDefaultGroupId++ : Display.DEFAULT_DISPLAY_GROUP;
     }
 
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index a11f172..f30a84f 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -26,14 +26,14 @@
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index e3fa622..f19852b 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -39,8 +39,6 @@
     private String mPhysicalDisplayId;
     private long mTime;
     private float mLux;
-    private float mFastAmbientLux;
-    private float mSlowAmbientLux;
     private float mPreThresholdLux;
     private float mInitialBrightness;
     private float mBrightness;
@@ -51,6 +49,7 @@
     private int mRbcStrength;
     private float mThermalMax;
     private float mPowerFactor;
+    private boolean mWasShortTermModelActive;
     private int mFlags;
     private int mAdjustmentFlags;
     private boolean mAutomaticBrightnessEnabled;
@@ -76,8 +75,6 @@
         mTime = that.getTime();
         // Lux values
         mLux = that.getLux();
-        mFastAmbientLux = that.getFastAmbientLux();
-        mSlowAmbientLux = that.getSlowAmbientLux();
         mPreThresholdLux = that.getPreThresholdLux();
         // Brightness values
         mInitialBrightness = that.getInitialBrightness();
@@ -90,6 +87,7 @@
         mRbcStrength = that.getRbcStrength();
         mThermalMax = that.getThermalMax();
         mPowerFactor = that.getPowerFactor();
+        mWasShortTermModelActive = that.wasShortTermModelActive();
         mFlags = that.getFlags();
         mAdjustmentFlags = that.getAdjustmentFlags();
         // Auto-brightness setting
@@ -105,8 +103,6 @@
         mPhysicalDisplayId = "";
         // Lux values
         mLux = 0;
-        mFastAmbientLux = 0;
-        mSlowAmbientLux = 0;
         mPreThresholdLux = 0;
         // Brightness values
         mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
@@ -119,6 +115,7 @@
         mRbcStrength = 0;
         mThermalMax = PowerManager.BRIGHTNESS_MAX;
         mPowerFactor = 1f;
+        mWasShortTermModelActive = false;
         mFlags = 0;
         mAdjustmentFlags = 0;
         // Auto-brightness setting
@@ -140,10 +137,6 @@
                 && mDisplayId == that.mDisplayId
                 && mPhysicalDisplayId.equals(that.mPhysicalDisplayId)
                 && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux)
-                && Float.floatToRawIntBits(mFastAmbientLux)
-                == Float.floatToRawIntBits(that.mFastAmbientLux)
-                && Float.floatToRawIntBits(mSlowAmbientLux)
-                == Float.floatToRawIntBits(that.mSlowAmbientLux)
                 && Float.floatToRawIntBits(mPreThresholdLux)
                 == Float.floatToRawIntBits(that.mPreThresholdLux)
                 && Float.floatToRawIntBits(mInitialBrightness)
@@ -161,6 +154,7 @@
                 == Float.floatToRawIntBits(that.mThermalMax)
                 && Float.floatToRawIntBits(mPowerFactor)
                 == Float.floatToRawIntBits(that.mPowerFactor)
+                && mWasShortTermModelActive == that.mWasShortTermModelActive
                 && mFlags == that.mFlags
                 && mAdjustmentFlags == that.mAdjustmentFlags
                 && mAutomaticBrightnessEnabled == that.mAutomaticBrightnessEnabled;
@@ -182,14 +176,13 @@
                 + ", rcmdBrt=" + mRecommendedBrightness
                 + ", preBrt=" + mPreThresholdBrightness
                 + ", lux=" + mLux
-                + ", fastLux=" + mFastAmbientLux
-                + ", slowLux=" + mSlowAmbientLux
                 + ", preLux=" + mPreThresholdLux
                 + ", hbmMax=" + mHbmMax
                 + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode)
                 + ", rbcStrength=" + mRbcStrength
                 + ", thrmMax=" + mThermalMax
                 + ", powerFactor=" + mPowerFactor
+                + ", wasShortTermModelActive=" + mWasShortTermModelActive
                 + ", flags=" + flagsToString()
                 + ", reason=" + mReason.toString(mAdjustmentFlags)
                 + ", autoBrightness=" + mAutomaticBrightnessEnabled;
@@ -240,22 +233,6 @@
         this.mLux = lux;
     }
 
-    public float getFastAmbientLux() {
-        return mFastAmbientLux;
-    }
-
-    public void setFastAmbientLux(float mFastAmbientLux) {
-        this.mFastAmbientLux = mFastAmbientLux;
-    }
-
-    public float getSlowAmbientLux() {
-        return mSlowAmbientLux;
-    }
-
-    public void setSlowAmbientLux(float mSlowAmbientLux) {
-        this.mSlowAmbientLux = mSlowAmbientLux;
-    }
-
     public float getPreThresholdLux() {
         return mPreThresholdLux;
     }
@@ -344,6 +321,20 @@
         return (mFlags & FLAG_LOW_POWER_MODE) != 0;
     }
 
+    /**
+     * Set whether the short term model was active before the brightness event.
+     */
+    public boolean setWasShortTermModelActive(boolean wasShortTermModelActive) {
+        return this.mWasShortTermModelActive = wasShortTermModelActive;
+    }
+
+    /**
+     * Returns whether the short term model was active before the brightness event.
+     */
+    public boolean wasShortTermModelActive() {
+        return this.mWasShortTermModelActive;
+    }
+
     public int getFlags() {
         return mFlags;
     }
@@ -352,10 +343,6 @@
         this.mFlags = flags;
     }
 
-    public boolean isShortTermModelActive() {
-        return (mFlags & FLAG_USER_SET) != 0;
-    }
-
     public int getAdjustmentFlags() {
         return mAdjustmentFlags;
     }
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 21a8518..5824887 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -1682,6 +1682,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
         @Override
         public boolean isSaturationActivated() {
+            super.isSaturationActivated_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 return !mGlobalSaturationTintController.isActivatedStateNotSet()
@@ -1694,6 +1696,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
         @Override
         public boolean setAppSaturationLevel(String packageName, int level) {
+            super.setAppSaturationLevel_enforcePermission();
+
             final String callingPackageName = LocalServices.getService(PackageManagerInternal.class)
                     .getNameForUid(Binder.getCallingUid());
             final long token = Binder.clearCallingIdentity();
@@ -1706,6 +1710,8 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS)
         public int getTransformCapabilities() {
+            super.getTransformCapabilities_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 return getTransformCapabilitiesInternal();
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index b8af1bf..c3313e0 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -16,6 +16,9 @@
 
 package com.android.server.dreams;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+
+import android.app.ActivityTaskManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -34,13 +37,13 @@
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamService;
 import android.util.Slog;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 /**
@@ -60,9 +63,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final Listener mListener;
-    private final IWindowManager mIWindowManager;
-    private long mDreamStartTime;
-    private String mSavedStopReason;
+    private final ActivityTaskManager mActivityTaskManager;
 
     private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
             .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -73,27 +74,21 @@
 
     private DreamRecord mCurrentDream;
 
-    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) {
-                Slog.w(TAG, "Bound dream did not connect in the time allotted");
-                stopDream(true /*immediate*/, "slow to connect");
-            }
-        }
-    };
+    // Whether a dreaming started intent has been broadcast.
+    private boolean mSentStartBroadcast = false;
 
-    private final Runnable mStopStubbornDreamRunnable = () -> {
-        Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
-        stopDream(true /*immediate*/, "slow to finish");
-        mSavedStopReason = null;
-    };
+    // When a new dream is started and there is an existing dream, the existing dream is allowed to
+    // live a little longer until the new dream is started, for a smoother transition. This dream is
+    // stopped as soon as the new dream is started, and this list is cleared. Usually there should
+    // only be one previous dream while waiting for a new dream to start, but we store a list to
+    // proof the edge case of multiple previous dreams.
+    private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>();
 
     public DreamController(Context context, Handler handler, Listener listener) {
         mContext = context;
         mHandler = handler;
         mListener = listener;
-        mIWindowManager = WindowManagerGlobal.getWindowManagerService();
+        mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         mCloseNotificationShadeIntent.putExtra("reason", "dream");
     }
@@ -109,18 +104,17 @@
             pw.println("    mUserId=" + mCurrentDream.mUserId);
             pw.println("    mBound=" + mCurrentDream.mBound);
             pw.println("    mService=" + mCurrentDream.mService);
-            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
             pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
         } else {
             pw.println("  mCurrentDream: null");
         }
+
+        pw.println("  mSentStartBroadcast=" + mSentStartBroadcast);
     }
 
     public void startDream(Binder token, ComponentName name,
             boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock,
             ComponentName overlayComponentName, String reason) {
-        stopDream(true /*immediate*/, "starting new dream");
-
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream");
         try {
             // Close the notification shade. No need to send to all, but better to be explicit.
@@ -130,9 +124,12 @@
                     + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
                     + ", userId=" + userId + ", reason='" + reason + "'");
 
+            if (mCurrentDream != null) {
+                mPreviousDreams.add(mCurrentDream);
+            }
             mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);
 
-            mDreamStartTime = SystemClock.elapsedRealtime();
+            mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
             MetricsLogger.visible(mContext,
                     mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
 
@@ -155,31 +152,49 @@
             }
 
             mCurrentDream.mBound = true;
-            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
+            mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
+                    DREAM_CONNECTION_TIMEOUT);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops dreaming.
+     *
+     * The current dream, if any, and any unstopped previous dreams are stopped. The device stops
+     * dreaming.
+     */
     public void stopDream(boolean immediate, String reason) {
-        if (mCurrentDream == null) {
+        stopPreviousDreams();
+        stopDreamInstance(immediate, reason, mCurrentDream);
+    }
+
+    /**
+     * Stops the given dream instance.
+     *
+     * The device may still be dreaming afterwards if there are other dreams running.
+     */
+    private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) {
+        if (dream == null) {
             return;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
         try {
             if (!immediate) {
-                if (mCurrentDream.mWakingGently) {
+                if (dream.mWakingGently) {
                     return; // already waking gently
                 }
 
-                if (mCurrentDream.mService != null) {
+                if (dream.mService != null) {
                     // Give the dream a moment to wake up and finish itself gently.
-                    mCurrentDream.mWakingGently = true;
+                    dream.mWakingGently = true;
                     try {
-                        mSavedStopReason = reason;
-                        mCurrentDream.mService.wakeUp();
-                        mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT);
+                        dream.mStopReason = reason;
+                        dream.mService.wakeUp();
+                        mHandler.postDelayed(dream.mStopStubbornDreamRunnable,
+                                DREAM_FINISH_TIMEOUT);
                         return;
                     } catch (RemoteException ex) {
                         // oh well, we tried, finish immediately instead
@@ -187,54 +202,77 @@
                 }
             }
 
-            final DreamRecord oldDream = mCurrentDream;
-            mCurrentDream = null;
-            Slog.i(TAG, "Stopping dream: name=" + oldDream.mName
-                    + ", isPreviewMode=" + oldDream.mIsPreviewMode
-                    + ", canDoze=" + oldDream.mCanDoze
-                    + ", userId=" + oldDream.mUserId
+            Slog.i(TAG, "Stopping dream: name=" + dream.mName
+                    + ", isPreviewMode=" + dream.mIsPreviewMode
+                    + ", canDoze=" + dream.mCanDoze
+                    + ", userId=" + dream.mUserId
                     + ", reason='" + reason + "'"
-                    + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')"));
+                    + (dream.mStopReason == null ? "" : "(from '"
+                    + dream.mStopReason + "')"));
             MetricsLogger.hidden(mContext,
-                    oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
+                    dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
             MetricsLogger.histogram(mContext,
-                    oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" ,
-                    (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L)));
+                    dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes",
+                    (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L
+                            * 60L)));
 
-            mHandler.removeCallbacks(mStopUnconnectedDreamRunnable);
-            mHandler.removeCallbacks(mStopStubbornDreamRunnable);
-            mSavedStopReason = null;
+            mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable);
+            mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable);
 
-            if (oldDream.mSentStartBroadcast) {
-                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
-            }
-
-            if (oldDream.mService != null) {
+            if (dream.mService != null) {
                 try {
-                    oldDream.mService.detach();
+                    dream.mService.detach();
                 } catch (RemoteException ex) {
                     // we don't care; this thing is on the way out
                 }
 
                 try {
-                    oldDream.mService.asBinder().unlinkToDeath(oldDream, 0);
+                    dream.mService.asBinder().unlinkToDeath(dream, 0);
                 } catch (NoSuchElementException ex) {
                     // don't care
                 }
-                oldDream.mService = null;
+                dream.mService = null;
             }
 
-            if (oldDream.mBound) {
-                mContext.unbindService(oldDream);
+            if (dream.mBound) {
+                mContext.unbindService(dream);
             }
-            oldDream.releaseWakeLockIfNeeded();
+            dream.releaseWakeLockIfNeeded();
 
-            mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
+            // Current dream stopped, device no longer dreaming.
+            if (dream == mCurrentDream) {
+                mCurrentDream = null;
+
+                if (mSentStartBroadcast) {
+                    mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
+                    mSentStartBroadcast = false;
+                }
+
+                mActivityTaskManager.removeRootTasksWithActivityTypes(
+                        new int[] {ACTIVITY_TYPE_DREAM});
+
+                mListener.onDreamStopped(dream.mToken);
+            }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops all previous dreams, if any.
+     */
+    private void stopPreviousDreams() {
+        if (mPreviousDreams.isEmpty()) {
+            return;
+        }
+
+        // Using an iterator because mPreviousDreams is modified while the iteration is in process.
+        for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) {
+            stopDreamInstance(true /*immediate*/, "stop previous dream", it.next());
+            it.remove();
+        }
+    }
+
     private void attach(IDreamService service) {
         try {
             service.asBinder().linkToDeath(mCurrentDream, 0);
@@ -248,9 +286,9 @@
 
         mCurrentDream.mService = service;
 
-        if (!mCurrentDream.mIsPreviewMode) {
+        if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
             mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
-            mCurrentDream.mSentStartBroadcast = true;
+            mSentStartBroadcast = true;
         }
     }
 
@@ -272,10 +310,35 @@
         public boolean mBound;
         public boolean mConnected;
         public IDreamService mService;
-        public boolean mSentStartBroadcast;
-
+        private String mStopReason;
+        private long mDreamStartTime;
         public boolean mWakingGently;
 
+        private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
+        private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
+
+        private final Runnable mStopUnconnectedDreamRunnable = () -> {
+            if (mBound && !mConnected) {
+                Slog.w(TAG, "Bound dream did not connect in the time allotted");
+                stopDream(true /*immediate*/, "slow to connect" /*reason*/);
+            }
+        };
+
+        private final Runnable mStopStubbornDreamRunnable = () -> {
+            Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
+            stopDream(true /*immediate*/, "slow to finish" /*reason*/);
+            mStopReason = null;
+        };
+
+        private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
+            // May be called on any thread.
+            @Override
+            public void sendResult(Bundle data) {
+                mHandler.post(mStopPreviousDreamsIfNeeded);
+                mHandler.post(mReleaseWakeLockIfNeeded);
+            }
+        };
+
         DreamRecord(Binder token, ComponentName name, boolean isPreviewMode,
                 boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
             mToken = token;
@@ -286,7 +349,9 @@
             mWakeLock = wakeLock;
             // Hold the lock while we're waiting for the service to connect and start dreaming.
             // Released after the service has started dreaming, we stop dreaming, or it timed out.
-            mWakeLock.acquire();
+            if (mWakeLock != null) {
+                mWakeLock.acquire();
+            }
             mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000);
         }
 
@@ -326,6 +391,12 @@
             });
         }
 
+        void stopPreviousDreamsIfNeeded() {
+            if (mCurrentDream == DreamRecord.this) {
+                stopPreviousDreams();
+            }
+        }
+
         void releaseWakeLockIfNeeded() {
             if (mWakeLock != null) {
                 mWakeLock.release();
@@ -333,15 +404,5 @@
                 mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
             }
         }
-
-        final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
-
-        final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
-            // May be called on any thread.
-            @Override
-            public void sendResult(Bundle data) throws RemoteException {
-                mHandler.post(mReleaseWakeLockIfNeeded);
-            }
-        };
     }
 }
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 951a8a2..4a0ba22 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 mDreamsOnlyEnabledForDockUser;
+    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;
@@ -157,13 +211,21 @@
         mDozeConfig = new AmbientDisplayConfiguration(mContext);
         mUiEventLogger = new UiEventLoggerImpl();
         mDreamUiEventLogger = new DreamUiEventLoggerImpl(
-                mContext.getResources().getString(R.string.config_loggable_dream_prefix));
+                mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes));
         AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext);
         mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent());
-        mDreamsOnlyEnabledForSystemUser =
-                mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser);
+        mDreamsOnlyEnabledForDockUser =
+                mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForDockUser);
         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();
         }
     }
 
@@ -206,7 +292,14 @@
             pw.println();
             pw.println("mCurrentDream=" + mCurrentDream);
             pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
-            pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+            pw.println("mDreamsOnlyEnabledForDockUser=" + mDreamsOnlyEnabledForDockUser);
+            pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting);
+            pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
+            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 +307,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 +350,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();
     }
@@ -352,10 +490,6 @@
         }
     }
 
-    private ComponentName getActiveDreamComponentInternal(boolean doze) {
-        return chooseDreamForUser(doze, ActivityManager.getCurrentUser());
-    }
-
     /**
      * If doze is true, returns the doze component for the user.
      * Otherwise, returns the system dream component, if present.
@@ -467,7 +601,8 @@
     }
 
     private boolean dreamsEnabledForUser(int userId) {
-        return !mDreamsOnlyEnabledForSystemUser || (userId == UserHandle.USER_SYSTEM);
+        // TODO(b/257333623): Support non-system Dock Users in HSUM.
+        return !mDreamsOnlyEnabledForDockUser || (userId == UserHandle.USER_SYSTEM);
     }
 
     private ServiceInfo getServiceInfo(ComponentName name) {
@@ -493,8 +628,6 @@
             return;
         }
 
-        stopDreamLocked(true /*immediate*/, "starting new dream");
-
         Slog.i(TAG, "Entering dreamland.");
 
         mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
@@ -510,7 +643,7 @@
                 .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, DREAM_WAKE_LOCK_TAG);
         final Binder dreamToken = mCurrentDream.token;
         mHandler.post(wakeLock.wrap(() -> {
-            mAtmInternal.notifyDreamStateChanged(true);
+            mAtmInternal.notifyActiveDreamChanged(name);
             mController.startDream(dreamToken, name, isPreviewMode, canDoze, userId, wakeLock,
                     mDreamOverlayServiceName, reason);
         }));
@@ -535,7 +668,7 @@
 
     @GuardedBy("mLock")
     private void cleanupDreamLocked() {
-        mHandler.post(() -> mAtmInternal.notifyDreamStateChanged(false /*dreaming*/));
+        mHandler.post(() -> mAtmInternal.notifyActiveDreamChanged(null));
 
         if (mCurrentDream == null) {
             return;
@@ -871,8 +1004,8 @@
         }
 
         @Override
-        public ComponentName getActiveDreamComponent(boolean doze) {
-            return getActiveDreamComponentInternal(doze);
+        public boolean canStartDreaming(boolean isScreenOn) {
+            return canStartDreamingInternal(isScreenOn);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
index 26ca74a..96ebcbb 100644
--- a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
+++ b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
@@ -26,10 +26,10 @@
  * @hide
  */
 public class DreamUiEventLoggerImpl implements DreamUiEventLogger {
-    final String mLoggableDreamPrefix;
+    private final String[] mLoggableDreamPrefixes;
 
-    DreamUiEventLoggerImpl(String loggableDreamPrefix) {
-        mLoggableDreamPrefix = loggableDreamPrefix;
+    DreamUiEventLoggerImpl(String[] loggableDreamPrefixes) {
+        mLoggableDreamPrefixes = loggableDreamPrefixes;
     }
 
     @Override
@@ -38,13 +38,20 @@
         if (eventID <= 0) {
             return;
         }
-        final boolean isFirstPartyDream =
-                mLoggableDreamPrefix.isEmpty() ? false : dreamComponentName.startsWith(
-                        mLoggableDreamPrefix);
         FrameworkStatsLog.write(FrameworkStatsLog.DREAM_UI_EVENT_REPORTED,
                 /* uid = 1 */ 0,
                 /* event_id = 2 */ eventID,
                 /* instance_id = 3 */ 0,
-                /* dream_component_name = 4 */ isFirstPartyDream ? dreamComponentName : "other");
+                /* dream_component_name = 4 */
+                isFirstPartyDream(dreamComponentName) ? dreamComponentName : "other");
+    }
+
+    private boolean isFirstPartyDream(String dreamComponentName) {
+        for (int i = 0; i < mLoggableDreamPrefixes.length; ++i) {
+            if (dreamComponentName.startsWith(mLoggableDreamPrefixes[i])) {
+                return true;
+            }
+        }
+        return false;
     }
 }
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 326d720..28dc318 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -26,6 +26,7 @@
 import android.graphics.fonts.FontManager;
 import android.graphics.fonts.FontUpdateRequest;
 import android.graphics.fonts.SystemFonts;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.ResultReceiver;
 import android.os.SharedMemory;
@@ -35,8 +36,10 @@
 import android.util.AndroidException;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
+import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.graphics.fonts.IFontManager;
 import com.android.internal.security.VerityUtils;
@@ -47,7 +50,9 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.PrintWriter;
 import java.nio.ByteBuffer;
 import java.nio.DirectByteBuffer;
@@ -68,6 +73,8 @@
     @RequiresPermission(Manifest.permission.UPDATE_FONTS)
     @Override
     public FontConfig getFontConfig() {
+        super.getFontConfig_enforcePermission();
+
         return getSystemFontConfig();
     }
 
@@ -153,9 +160,30 @@
     }
 
     private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
+
+        private final String[] mDerCertPaths;
+
+        FsverityUtilImpl(String[] derCertPaths) {
+            mDerCertPaths = derCertPaths;
+        }
+
         @Override
-        public boolean hasFsverity(String filePath) {
-            return VerityUtils.hasFsverity(filePath);
+        public boolean isFromTrustedProvider(String fontPath, byte[] pkcs7Signature) {
+            final byte[] digest = VerityUtils.getFsverityDigest(fontPath);
+            if (digest == null) {
+                Log.w(TAG, "Failed to get fs-verity digest for " + fontPath);
+                return false;
+            }
+            for (String certPath : mDerCertPaths) {
+                try (InputStream is = new FileInputStream(certPath)) {
+                    if (VerityUtils.verifyPkcs7DetachedSignature(pkcs7Signature, digest, is)) {
+                        return true;
+                    }
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to read certificate file: " + certPath);
+                }
+            }
+            return false;
         }
 
         @Override
@@ -173,11 +201,15 @@
     @NonNull
     private final Context mContext;
 
+    private final boolean mIsSafeMode;
+
     private final Object mUpdatableFontDirLock = new Object();
 
+    private String mDebugCertFilePath = null;
+
     @GuardedBy("mUpdatableFontDirLock")
     @Nullable
-    private final UpdatableFontDir mUpdatableFontDir;
+    private UpdatableFontDir mUpdatableFontDir;
 
     // mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock.
     // mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock.
@@ -193,22 +225,43 @@
             UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
         }
         mContext = context;
-        mUpdatableFontDir = createUpdatableFontDir(safeMode);
+        mIsSafeMode = safeMode;
         initialize();
     }
 
     @Nullable
-    private static UpdatableFontDir createUpdatableFontDir(boolean safeMode) {
+    private UpdatableFontDir createUpdatableFontDir() {
         // Never read updatable font files in safe mode.
-        if (safeMode) return null;
+        if (mIsSafeMode) return null;
         // If apk verity is supported, fs-verity should be available.
         if (!VerityUtils.isFsVeritySupported()) return null;
+
+        String[] certs = mContext.getResources().getStringArray(
+                R.array.config_fontManagerServiceCerts);
+
+        if (mDebugCertFilePath != null && (Build.IS_USERDEBUG || Build.IS_ENG)) {
+            String[] tmp = new String[certs.length + 1];
+            System.arraycopy(certs, 0, tmp, 0, certs.length);
+            tmp[certs.length] = mDebugCertFilePath;
+            certs = tmp;
+        }
+
         return new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser(),
-                new FsverityUtilImpl(), new File(CONFIG_XML_FILE));
+                new FsverityUtilImpl(certs), new File(CONFIG_XML_FILE));
+    }
+
+    /**
+     * Add debug certificate to the cert list. This must be called only on userdebug/eng
+     * build.
+     * @param debugCertPath a debug certificate file path
+     */
+    public void addDebugCertificate(@Nullable String debugCertPath) {
+        mDebugCertFilePath = debugCertPath;
     }
 
     private void initialize() {
         synchronized (mUpdatableFontDirLock) {
+            mUpdatableFontDir = createUpdatableFontDir();
             if (mUpdatableFontDir == null) {
                 setSerializedFontMap(serializeSystemServerFontMap());
                 return;
@@ -231,12 +284,12 @@
 
     /* package */ void update(int baseVersion, List<FontUpdateRequest> requests)
             throws SystemFontException {
-        if (mUpdatableFontDir == null) {
-            throw new SystemFontException(
-                    FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
-                    "The font updater is disabled.");
-        }
         synchronized (mUpdatableFontDirLock) {
+            if (mUpdatableFontDir == null) {
+                throw new SystemFontException(
+                        FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
+                        "The font updater is disabled.");
+            }
             // baseVersion == -1 only happens from shell command. This is filtered and treated as
             // error from SystemApi call.
             if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) {
@@ -271,10 +324,10 @@
     }
 
     /* package */ Map<String, File> getFontFileMap() {
-        if (mUpdatableFontDir == null) {
-            return Collections.emptyMap();
-        }
         synchronized (mUpdatableFontDirLock) {
+            if (mUpdatableFontDir == null) {
+                return Collections.emptyMap();
+            }
             return mUpdatableFontDir.getPostScriptMap();
         }
     }
@@ -300,10 +353,10 @@
      * Returns an active system font configuration.
      */
     public @NonNull FontConfig getSystemFontConfig() {
-        if (mUpdatableFontDir == null) {
-            return SystemFonts.getSystemPreinstalledFontConfig();
-        }
         synchronized (mUpdatableFontDirLock) {
+            if (mUpdatableFontDir == null) {
+                return SystemFonts.getSystemPreinstalledFontConfig();
+            }
             return mUpdatableFontDir.getSystemFontConfig();
         }
     }
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
index 3fecef7..4cd0d6e 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
@@ -28,16 +28,17 @@
 import android.graphics.fonts.FontVariationAxis;
 import android.graphics.fonts.SystemFonts;
 import android.os.Binder;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.ShellCommand;
 import android.text.FontConfig;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.DumpUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -103,6 +104,10 @@
         w.println("update-family [family definition XML path]");
         w.println("    Update font families with the new definitions.");
         w.println();
+        w.println("install-debug-cert [cert file path]");
+        w.println("    Install debug certificate file. This command can be used only on userdebug");
+        w.println("    or eng device with root user.");
+        w.println();
         w.println("clear");
         w.println("    Remove all installed font files and reset to the initial state.");
         w.println();
@@ -322,6 +327,33 @@
         return 0;
     }
 
+    private int installCert(ShellCommand shell) throws SystemFontException {
+        if (!(Build.IS_USERDEBUG || Build.IS_ENG)) {
+            throw new SecurityException("Only userdebug/eng device can add debug certificate");
+        }
+        if (Binder.getCallingUid() != Process.ROOT_UID) {
+            throw new SecurityException("Only root can add debug certificate");
+        }
+
+        String certPath = shell.getNextArg();
+        if (certPath == null) {
+            throw new SystemFontException(
+                    FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
+                    "Cert file path argument is required.");
+        }
+        File file = new File(certPath);
+        if (!file.isFile()) {
+            throw new SystemFontException(
+                    FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
+                    "Cert file (" + file + ") is not found");
+        }
+
+        mService.addDebugCertificate(certPath);
+        mService.restart();
+        shell.getOutPrintWriter().println("Success");
+        return 0;
+    }
+
     private int update(ShellCommand shell) throws SystemFontException {
         String fontPath = shell.getNextArg();
         if (fontPath == null) {
@@ -494,6 +526,8 @@
                     return restart(shell);
                 case "status":
                     return status(shell);
+                case "install-debug-cert":
+                    return installCert(shell);
                 default:
                     return shell.handleDefaultCommands(cmd);
             }
diff --git a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
index 15abbd5..fd00980 100644
--- a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
+++ b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
@@ -21,10 +21,11 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index 743b4d9..457d5b7 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -40,6 +40,8 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -59,6 +61,8 @@
     private static final String TAG = "UpdatableFontDir";
     private static final String RANDOM_DIR_PREFIX = "~~";
 
+    private static final String FONT_SIGNATURE_FILE = "font.fsv_sig";
+
     /** Interface to mock font file access in tests. */
     interface FontFileParser {
         String getPostScriptName(File file) throws IOException;
@@ -72,7 +76,7 @@
 
     /** Interface to mock fs-verity in tests. */
     interface FsverityUtil {
-        boolean hasFsverity(String path);
+        boolean isFromTrustedProvider(String path, byte[] pkcs7Signature);
 
         void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;
 
@@ -188,12 +192,35 @@
                     FileUtils.deleteContentsAndDir(dir);
                     continue;
                 }
+
+                File signatureFile = new File(dir, FONT_SIGNATURE_FILE);
+                if (!signatureFile.exists()) {
+                    Slog.i(TAG, "The signature file is missing.");
+                    FileUtils.deleteContentsAndDir(dir);
+                    continue;
+                }
+                byte[] signature;
+                try {
+                    signature = Files.readAllBytes(Paths.get(signatureFile.getAbsolutePath()));
+                } catch (IOException e) {
+                    Slog.e(TAG, "Failed to read signature file.");
+                    return;
+                }
+
                 File[] files = dir.listFiles();
-                if (files == null || files.length != 1) {
+                if (files == null || files.length != 2) {
                     Slog.e(TAG, "Unexpected files in dir: " + dir);
                     return;
                 }
-                FontFileInfo fontFileInfo = validateFontFile(files[0]);
+
+                File fontFile;
+                if (files[0].equals(signatureFile)) {
+                    fontFile = files[1];
+                } else {
+                    fontFile = files[0];
+                }
+
+                FontFileInfo fontFileInfo = validateFontFile(fontFile, signature);
                 if (fontConfig == null) {
                     fontConfig = getSystemFontConfig();
                 }
@@ -359,9 +386,25 @@
             } catch (ErrnoException e) {
                 throw new SystemFontException(
                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
-                        "Failed to change mode to 711", e);
+                        "Failed to change font file mode to 644", e);
             }
-            FontFileInfo fontFileInfo = validateFontFile(newFontFile);
+            File signatureFile = new File(newDir, FONT_SIGNATURE_FILE);
+            try (FileOutputStream out = new FileOutputStream(signatureFile)) {
+                out.write(pkcs7Signature);
+            } catch (IOException e) {
+                // TODO: Do we need new error code for signature write failure?
+                throw new SystemFontException(
+                        FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
+                        "Failed to write font signature file to storage.", e);
+            }
+            try {
+                Os.chmod(signatureFile.getAbsolutePath(), 0600);
+            } catch (ErrnoException e) {
+                throw new SystemFontException(
+                        FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
+                        "Failed to change the signature file mode to 600", e);
+            }
+            FontFileInfo fontFileInfo = validateFontFile(newFontFile, pkcs7Signature);
 
             // Try to create Typeface and treat as failure something goes wrong.
             try {
@@ -478,8 +521,9 @@
      * is higher than the currently used font.
      */
     @NonNull
-    private FontFileInfo validateFontFile(File file) throws SystemFontException {
-        if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) {
+    private FontFileInfo validateFontFile(File file, byte[] pkcs7Signature)
+            throws SystemFontException {
+        if (!mFsverityUtil.isFromTrustedProvider(file.getAbsolutePath(), pkcs7Signature)) {
             throw new SystemFontException(
                     FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
                     "Font validation failed. Fs-verity is not enabled: " + file);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index 5aa3fa4..5c1b33c 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -19,20 +19,25 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.hardware.hdmi.HdmiPortInfo;
-import android.hardware.tv.cec.V1_0.CecMessage;
+import android.hardware.tv.cec.CecMessage;
+import android.hardware.tv.cec.IHdmiCec;
+import android.hardware.tv.cec.IHdmiCecCallback;
 import android.hardware.tv.cec.V1_0.HotplugEvent;
-import android.hardware.tv.cec.V1_0.IHdmiCec;
 import android.hardware.tv.cec.V1_0.IHdmiCec.getPhysicalAddressCallback;
-import android.hardware.tv.cec.V1_0.IHdmiCecCallback;
+import android.hardware.tv.cec.V1_0.OptionKey;
 import android.hardware.tv.cec.V1_0.Result;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.hardware.tv.hdmi.IHdmi;
+import android.hardware.tv.hdmi.IHdmiCallback;
 import android.icu.util.IllformedLocaleException;
 import android.icu.util.ULocale;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.IHwBinder;
 import android.os.Looper;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.stats.hdmi.HdmiStatsEnums;
 import android.util.Slog;
 
@@ -170,8 +175,14 @@
      *         returns {@code null}.
      */
     static HdmiCecController create(HdmiControlService service, HdmiCecAtomWriter atomWriter) {
-        HdmiCecController controller = createWithNativeWrapper(service, new NativeWrapperImpl11(),
-                atomWriter);
+        HdmiCecController controller =
+                createWithNativeWrapper(service, new NativeWrapperImplAidl(), atomWriter);
+        if (controller != null) {
+            return controller;
+        }
+        HdmiLogger.warning("Unable to use CEC and HDMI AIDL HALs");
+
+        controller = createWithNativeWrapper(service, new NativeWrapperImpl11(), atomWriter);
         if (controller != null) {
             return controller;
         }
@@ -362,16 +373,43 @@
     }
 
     /**
-     * Set an option to CEC HAL.
+     * Configures the TV panel device wakeup behaviour in standby mode when it receives an OTP
+     * (One Touch Play) from a source device.
      *
-     * @param flag key of option
-     * @param enabled whether to enable/disable the given option.
+     * @param value If true, the TV device will wake up when OTP is received and if false, the TV
+     *     device will not wake up for an OTP.
      */
     @ServiceThreadOnly
-    void setOption(int flag, boolean enabled) {
+    void enableWakeupByOtp(boolean enabled) {
         assertRunOnServiceThread();
-        HdmiLogger.debug("setOption: [flag:%d, enabled:%b]", flag, enabled);
-        mNativeWrapperImpl.nativeSetOption(flag, enabled);
+        HdmiLogger.debug("enableWakeupByOtp: %b", enabled);
+        mNativeWrapperImpl.enableWakeupByOtp(enabled);
+    }
+
+    /**
+     * Switch to enable or disable CEC on the device.
+     *
+     * @param value If true, the device will have all CEC functionalities and if false, the device
+     *     will not perform any CEC functions.
+     */
+    @ServiceThreadOnly
+    void enableCec(boolean enabled) {
+        assertRunOnServiceThread();
+        HdmiLogger.debug("enableCec: %b", enabled);
+        mNativeWrapperImpl.enableCec(enabled);
+    }
+
+    /**
+     * Configures the module that processes CEC messages - the Android framework or the HAL.
+     *
+     * @param value If true, the Android framework will actively process CEC messages and if false,
+     *     only the HAL will process the CEC messages.
+     */
+    @ServiceThreadOnly
+    void enableSystemCecControl(boolean enabled) {
+        assertRunOnServiceThread();
+        HdmiLogger.debug("enableSystemCecControl: %b", enabled);
+        mNativeWrapperImpl.enableSystemCecControl(enabled);
     }
 
     /**
@@ -829,12 +867,233 @@
         int nativeGetVersion();
         int nativeGetVendorId();
         HdmiPortInfo[] nativeGetPortInfos();
-        void nativeSetOption(int flag, boolean enabled);
+
+        void enableWakeupByOtp(boolean enabled);
+
+        void enableCec(boolean enabled);
+
+        void enableSystemCecControl(boolean enabled);
+
         void nativeSetLanguage(String language);
         void nativeEnableAudioReturnChannel(int port, boolean flag);
         boolean nativeIsConnected(int port);
     }
 
+    private static final class NativeWrapperImplAidl
+            implements NativeWrapper, IBinder.DeathRecipient {
+        private IHdmiCec mHdmiCec;
+        private IHdmi mHdmi;
+        @Nullable private HdmiCecCallback mCallback;
+
+        private final Object mLock = new Object();
+
+        @Override
+        public String nativeInit() {
+            return connectToHal() ? mHdmiCec.toString() + " " + mHdmi.toString() : null;
+        }
+
+        boolean connectToHal() {
+            mHdmiCec =
+                    IHdmiCec.Stub.asInterface(
+                            ServiceManager.getService(IHdmiCec.DESCRIPTOR + "/default"));
+            if (mHdmiCec == null) {
+                HdmiLogger.error("Could not initialize HDMI CEC AIDL HAL");
+                return false;
+            }
+            try {
+                mHdmiCec.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Couldn't link to death : ", e);
+            }
+
+            mHdmi =
+                    IHdmi.Stub.asInterface(
+                            ServiceManager.getService(IHdmi.DESCRIPTOR + "/default"));
+            if (mHdmi == null) {
+                HdmiLogger.error("Could not initialize HDMI AIDL HAL");
+                return false;
+            }
+            try {
+                mHdmi.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Couldn't link to death : ", e);
+            }
+            return true;
+        }
+
+        @Override
+        public void binderDied() {
+            // One of the services died, try to reconnect to both.
+            mHdmiCec.asBinder().unlinkToDeath(this, 0);
+            mHdmi.asBinder().unlinkToDeath(this, 0);
+            HdmiLogger.error("HDMI or CEC service died, reconnecting");
+            connectToHal();
+            // Reconnect the callback
+            if (mCallback != null) {
+                setCallback(mCallback);
+            }
+        }
+
+        @Override
+        public void setCallback(HdmiCecCallback callback) {
+            mCallback = callback;
+            try {
+                // Create an AIDL callback that can callback onCecMessage
+                mHdmiCec.setCallback(new HdmiCecCallbackAidl(callback));
+            } catch (RemoteException e) {
+                HdmiLogger.error("Couldn't initialise tv.cec callback : ", e);
+            }
+            try {
+                // Create an AIDL callback that can callback onHotplugEvent
+                mHdmi.setCallback(new HdmiCallbackAidl(callback));
+            } catch (RemoteException e) {
+                HdmiLogger.error("Couldn't initialise tv.hdmi callback : ", e);
+            }
+        }
+
+        @Override
+        public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) {
+            CecMessage message = new CecMessage();
+            message.initiator = (byte) (srcAddress & 0xF);
+            message.destination = (byte) (dstAddress & 0xF);
+            message.body = body;
+            try {
+                return mHdmiCec.sendMessage(message);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to send CEC message : ", e);
+                return SendMessageResult.FAIL;
+            }
+        }
+
+        @Override
+        public int nativeAddLogicalAddress(int logicalAddress) {
+            try {
+                return mHdmiCec.addLogicalAddress((byte) logicalAddress);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to add a logical address : ", e);
+                return Result.FAILURE_INVALID_ARGS;
+            }
+        }
+
+        @Override
+        public void nativeClearLogicalAddress() {
+            try {
+                mHdmiCec.clearLogicalAddress();
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to clear logical address : ", e);
+            }
+        }
+
+        @Override
+        public int nativeGetPhysicalAddress() {
+            try {
+                return mHdmiCec.getPhysicalAddress();
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to get physical address : ", e);
+                return INVALID_PHYSICAL_ADDRESS;
+            }
+        }
+
+        @Override
+        public int nativeGetVersion() {
+            try {
+                return mHdmiCec.getCecVersion();
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to get cec version : ", e);
+                return Result.FAILURE_UNKNOWN;
+            }
+        }
+
+        @Override
+        public int nativeGetVendorId() {
+            try {
+                return mHdmiCec.getVendorId();
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to get vendor id : ", e);
+                return Result.FAILURE_UNKNOWN;
+            }
+        }
+
+        @Override
+        public void enableWakeupByOtp(boolean enabled) {
+            try {
+                mHdmiCec.enableWakeupByOtp(enabled);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed call to enableWakeupByOtp : ", e);
+            }
+        }
+
+        @Override
+        public void enableCec(boolean enabled) {
+            try {
+                mHdmiCec.enableCec(enabled);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed call to enableCec : ", e);
+            }
+        }
+
+        @Override
+        public void enableSystemCecControl(boolean enabled) {
+            try {
+                mHdmiCec.enableSystemCecControl(enabled);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed call to enableSystemCecControl : ", e);
+            }
+        }
+
+        @Override
+        public void nativeSetLanguage(String language) {
+            try {
+                mHdmiCec.setLanguage(language);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to set language : ", e);
+            }
+        }
+
+        @Override
+        public void nativeEnableAudioReturnChannel(int port, boolean flag) {
+            try {
+                mHdmiCec.enableAudioReturnChannel(port, flag);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to enable/disable ARC : ", e);
+            }
+        }
+
+        @Override
+        public HdmiPortInfo[] nativeGetPortInfos() {
+            try {
+                android.hardware.tv.hdmi.HdmiPortInfo[] hdmiPortInfos = mHdmi.getPortInfo();
+                HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[hdmiPortInfos.length];
+                int i = 0;
+                for (android.hardware.tv.hdmi.HdmiPortInfo portInfo : hdmiPortInfos) {
+                    hdmiPortInfo[i] =
+                            new HdmiPortInfo(
+                                    portInfo.portId,
+                                    portInfo.type,
+                                    portInfo.physicalAddress,
+                                    portInfo.cecSupported,
+                                    false,
+                                    portInfo.arcSupported);
+                    i++;
+                }
+                return hdmiPortInfo;
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to get port information : ", e);
+                return null;
+            }
+        }
+
+        @Override
+        public boolean nativeIsConnected(int port) {
+            try {
+                return mHdmi.isConnected(port);
+            } catch (RemoteException e) {
+                HdmiLogger.error("Failed to get connection info : ", e);
+                return false;
+            }
+        }
+    }
+
     private static final class NativeWrapperImpl11 implements NativeWrapper,
             IHwBinder.DeathRecipient, getPhysicalAddressCallback {
         private android.hardware.tv.cec.V1_1.IHdmiCec mHdmiCec;
@@ -985,8 +1244,7 @@
             }
         }
 
-        @Override
-        public void nativeSetOption(int flag, boolean enabled) {
+        private void nativeSetOption(int flag, boolean enabled) {
             try {
                 mHdmiCec.setOption(flag, enabled);
             } catch (RemoteException e) {
@@ -995,6 +1253,21 @@
         }
 
         @Override
+        public void enableWakeupByOtp(boolean enabled) {
+            nativeSetOption(OptionKey.WAKEUP, enabled);
+        }
+
+        @Override
+        public void enableCec(boolean enabled) {
+            nativeSetOption(OptionKey.ENABLE_CEC, enabled);
+        }
+
+        @Override
+        public void enableSystemCecControl(boolean enabled) {
+            nativeSetOption(OptionKey.SYSTEM_CEC_CONTROL, enabled);
+        }
+
+        @Override
         public void nativeSetLanguage(String language) {
             try {
                 mHdmiCec.setLanguage(language);
@@ -1038,7 +1311,7 @@
 
         boolean connectToHal() {
             try {
-                mHdmiCec = IHdmiCec.getService(true);
+                mHdmiCec = android.hardware.tv.cec.V1_0.IHdmiCec.getService(true);
                 try {
                     mHdmiCec.linkToDeath(this, HDMI_CEC_HAL_DEATH_COOKIE);
                 } catch (RemoteException e) {
@@ -1063,7 +1336,8 @@
 
         @Override
         public int nativeSendCecCommand(int srcAddress, int dstAddress, byte[] body) {
-            CecMessage message = new CecMessage();
+            android.hardware.tv.cec.V1_0.CecMessage message =
+                    new android.hardware.tv.cec.V1_0.CecMessage();
             message.initiator = srcAddress;
             message.destination = dstAddress;
             message.body = new ArrayList<>(body.length);
@@ -1151,8 +1425,7 @@
             }
         }
 
-        @Override
-        public void nativeSetOption(int flag, boolean enabled) {
+        private void nativeSetOption(int flag, boolean enabled) {
             try {
                 mHdmiCec.setOption(flag, enabled);
             } catch (RemoteException e) {
@@ -1161,6 +1434,21 @@
         }
 
         @Override
+        public void enableWakeupByOtp(boolean enabled) {
+            nativeSetOption(OptionKey.WAKEUP, enabled);
+        }
+
+        @Override
+        public void enableCec(boolean enabled) {
+            nativeSetOption(OptionKey.ENABLE_CEC, enabled);
+        }
+
+        @Override
+        public void enableSystemCecControl(boolean enabled) {
+            nativeSetOption(OptionKey.SYSTEM_CEC_CONTROL, enabled);
+        }
+
+        @Override
         public void nativeSetLanguage(String language) {
             try {
                 mHdmiCec.setLanguage(language);
@@ -1221,7 +1509,8 @@
         }
     }
 
-    private static final class HdmiCecCallback10 extends IHdmiCecCallback.Stub {
+    private static final class HdmiCecCallback10
+            extends android.hardware.tv.cec.V1_0.IHdmiCecCallback.Stub {
         private final HdmiCecCallback mHdmiCecCallback;
 
         HdmiCecCallback10(HdmiCecCallback hdmiCecCallback) {
@@ -1229,7 +1518,8 @@
         }
 
         @Override
-        public void onCecMessage(CecMessage message) throws RemoteException {
+        public void onCecMessage(android.hardware.tv.cec.V1_0.CecMessage message)
+                throws RemoteException {
             byte[] body = new byte[message.body.size()];
             for (int i = 0; i < message.body.size(); i++) {
                 body[i] = message.body.get(i);
@@ -1262,7 +1552,8 @@
         }
 
         @Override
-        public void onCecMessage(CecMessage message) throws RemoteException {
+        public void onCecMessage(android.hardware.tv.cec.V1_0.CecMessage message)
+                throws RemoteException {
             byte[] body = new byte[message.body.size()];
             for (int i = 0; i < message.body.size(); i++) {
                 body[i] = message.body.get(i);
@@ -1276,6 +1567,52 @@
         }
     }
 
+    private static final class HdmiCecCallbackAidl extends IHdmiCecCallback.Stub {
+        private final HdmiCecCallback mHdmiCecCallback;
+
+        HdmiCecCallbackAidl(HdmiCecCallback hdmiCecCallback) {
+            mHdmiCecCallback = hdmiCecCallback;
+        }
+
+        @Override
+        public void onCecMessage(CecMessage message) throws RemoteException {
+            mHdmiCecCallback.onCecMessage(message.initiator, message.destination, message.body);
+        }
+
+        @Override
+        public synchronized String getInterfaceHash() throws android.os.RemoteException {
+            return IHdmiCecCallback.Stub.HASH;
+        }
+
+        @Override
+        public int getInterfaceVersion() throws android.os.RemoteException {
+            return IHdmiCecCallback.Stub.VERSION;
+        }
+    }
+
+    private static final class HdmiCallbackAidl extends IHdmiCallback.Stub {
+        private final HdmiCecCallback mHdmiCecCallback;
+
+        HdmiCallbackAidl(HdmiCecCallback hdmiCecCallback) {
+            mHdmiCecCallback = hdmiCecCallback;
+        }
+
+        @Override
+        public void onHotplugEvent(boolean connected, int portId) throws RemoteException {
+            mHdmiCecCallback.onHotplugEvent(portId, connected);
+        }
+
+        @Override
+        public synchronized String getInterfaceHash() throws android.os.RemoteException {
+            return IHdmiCallback.Stub.HASH;
+        }
+
+        @Override
+        public int getInterfaceVersion() throws android.os.RemoteException {
+            return IHdmiCallback.Stub.VERSION;
+        }
+    }
+
     public abstract static class Dumpable {
         protected final long mTime;
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 9bce471f..8a22ab9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -837,7 +837,7 @@
     void enableAudioReturnChannel(boolean enabled) {
         assertRunOnServiceThread();
         HdmiDeviceInfo avr = getAvrDeviceInfo();
-        if (avr != null) {
+        if (avr != null && avr.getPortId() != Constants.INVALID_PORT_ID) {
             mService.enableAudioReturnChannel(avr.getPortId(), enabled);
         }
     }
@@ -1336,19 +1336,31 @@
     }
 
     @ServiceThreadOnly
+    private void forceDisableArcOnAllPins() {
+        List<HdmiPortInfo> ports = mService.getPortInfo();
+        for (HdmiPortInfo port : ports) {
+            if (isArcFeatureEnabled(port.getId())) {
+                mService.enableAudioReturnChannel(port.getId(), false);
+            }
+        }
+    }
+
+    @ServiceThreadOnly
     private void disableArcIfExist() {
         assertRunOnServiceThread();
         HdmiDeviceInfo avr = getAvrDeviceInfo();
         if (avr == null) {
             return;
         }
-        disableArc();
 
         // Seq #44.
         removeAllRunningArcAction();
         if (!hasAction(RequestArcTerminationAction.class) && isArcEstablished()) {
             addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
         }
+
+        // Disable ARC Pin earlier, prevent the case where AVR doesn't send <Terminate ARC> in time
+        forceDisableArcOnAllPins();
     }
 
     @ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 3ee3503..1ae1b5b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -56,7 +56,6 @@
 import android.hardware.hdmi.IHdmiRecordListener;
 import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
 import android.hardware.hdmi.IHdmiVendorCommandListener;
-import android.hardware.tv.cec.V1_0.OptionKey;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
@@ -656,7 +655,7 @@
         if (mHdmiControlEnabled == HdmiControlManager.HDMI_CEC_CONTROL_ENABLED) {
             initializeCec(INITIATED_BY_BOOT_UP);
         } else {
-            mCecController.setOption(OptionKey.ENABLE_CEC, false);
+            mCecController.enableCec(false);
         }
         mMhlDevices = Collections.emptyList();
 
@@ -730,10 +729,11 @@
                     @Override
                     public void onChange(String setting) {
                         if (isTvDeviceEnabled()) {
-                            setCecOption(OptionKey.WAKEUP, tv().getAutoWakeup());
+                            mCecController.enableWakeupByOtp(tv().getAutoWakeup());
                         }
                     }
-                }, mServiceThreadExecutor);
+                },
+                mServiceThreadExecutor);
     }
 
     /** Returns true if the device screen is off */
@@ -854,7 +854,7 @@
         mWakeUpMessageReceived = false;
 
         if (isTvDeviceEnabled()) {
-            mCecController.setOption(OptionKey.WAKEUP, tv().getAutoWakeup());
+            mCecController.enableWakeupByOtp(tv().getAutoWakeup());
         }
         int reason = -1;
         switch (initiatedBy) {
@@ -988,7 +988,7 @@
         mCecVersion = Math.max(HdmiControlManager.HDMI_CEC_VERSION_1_4_B,
                 Math.min(settingsCecVersion, supportedCecVersion));
 
-        mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, true);
+        mCecController.enableSystemCecControl(true);
         mCecController.setLanguage(mMenuLanguage);
         initializeLocalDevices(initiatedBy);
     }
@@ -3424,7 +3424,7 @@
                 device.onStandby(mStandbyMessageReceived, standbyAction);
             }
             if (!isAudioSystemDevice()) {
-                mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, false);
+                mCecController.enableSystemCecControl(false);
                 mMhlController.setOption(OPTION_MHL_SERVICE_CONTROL, DISABLED);
             }
         }
@@ -3573,12 +3573,6 @@
     }
 
     @ServiceThreadOnly
-    void setCecOption(int key, boolean value) {
-        assertRunOnServiceThread();
-        mCecController.setOption(key, value);
-    }
-
-    @ServiceThreadOnly
     void setControlEnabled(@HdmiControlManager.HdmiCecControl int enabled) {
         assertRunOnServiceThread();
 
@@ -3612,8 +3606,8 @@
 
     @ServiceThreadOnly
     private void enableHdmiControlService() {
-        mCecController.setOption(OptionKey.ENABLE_CEC, true);
-        mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, true);
+        mCecController.enableCec(true);
+        mCecController.enableSystemCecControl(true);
         mMhlController.setOption(OPTION_MHL_ENABLE, ENABLED);
 
         initializeCec(INITIATED_BY_ENABLE_CEC);
@@ -3621,21 +3615,23 @@
 
     @ServiceThreadOnly
     private void disableHdmiControlService() {
-        disableDevices(new PendingActionClearedCallback() {
-            @Override
-            public void onCleared(HdmiCecLocalDevice device) {
-                assertRunOnServiceThread();
-                mCecController.flush(new Runnable() {
+        disableDevices(
+                new PendingActionClearedCallback() {
                     @Override
-                    public void run() {
-                        mCecController.setOption(OptionKey.ENABLE_CEC, false);
-                        mCecController.setOption(OptionKey.SYSTEM_CEC_CONTROL, false);
-                        mMhlController.setOption(OPTION_MHL_ENABLE, DISABLED);
-                        clearLocalDevices();
+                    public void onCleared(HdmiCecLocalDevice device) {
+                        assertRunOnServiceThread();
+                        mCecController.flush(
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mCecController.enableCec(false);
+                                        mCecController.enableSystemCecControl(false);
+                                        mMhlController.setOption(OPTION_MHL_ENABLE, DISABLED);
+                                        clearLocalDevices();
+                                    }
+                                });
                     }
                 });
-            }
-        });
     }
 
     @ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiUtils.java b/services/core/java/com/android/server/hdmi/HdmiUtils.java
index ba19cf0..5646e1b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiUtils.java
+++ b/services/core/java/com/android/server/hdmi/HdmiUtils.java
@@ -20,16 +20,18 @@
 import static com.android.server.hdmi.Constants.ADDR_BACKUP_2;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 
+import static java.util.Map.entry;
+
 import android.annotation.Nullable;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.hdmi.Constants.AbortReason;
 import com.android.server.hdmi.Constants.AudioCodec;
 import com.android.server.hdmi.Constants.FeatureOpcode;
@@ -45,7 +47,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -57,38 +58,34 @@
 
     private static final String TAG = "HdmiUtils";
 
-    private static final Map<Integer, List<Integer>> ADDRESS_TO_TYPE =
-            new HashMap<Integer, List<Integer>>() {
-                {
-                    put(Constants.ADDR_TV, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TV));
-                    put(Constants.ADDR_RECORDER_1,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER));
-                    put(Constants.ADDR_RECORDER_2,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER));
-                    put(Constants.ADDR_TUNER_1, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER));
-                    put(Constants.ADDR_PLAYBACK_1,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK));
-                    put(Constants.ADDR_AUDIO_SYSTEM,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM));
-                    put(Constants.ADDR_TUNER_2, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER));
-                    put(Constants.ADDR_TUNER_3, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER));
-                    put(Constants.ADDR_PLAYBACK_2,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK));
-                    put(Constants.ADDR_RECORDER_3,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER));
-                    put(Constants.ADDR_TUNER_4, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER));
-                    put(Constants.ADDR_PLAYBACK_3,
-                            Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK));
-                    put(Constants.ADDR_BACKUP_1, Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK,
-                            HdmiDeviceInfo.DEVICE_RECORDER, HdmiDeviceInfo.DEVICE_TUNER,
-                            HdmiDeviceInfo.DEVICE_VIDEO_PROCESSOR));
-                    put(Constants.ADDR_BACKUP_2, Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK,
-                            HdmiDeviceInfo.DEVICE_RECORDER, HdmiDeviceInfo.DEVICE_TUNER,
-                            HdmiDeviceInfo.DEVICE_VIDEO_PROCESSOR));
-                    put(Constants.ADDR_SPECIFIC_USE, Lists.newArrayList(ADDR_TV));
-                    put(Constants.ADDR_UNREGISTERED, Collections.emptyList());
-                }
-            };
+    private static final Map<Integer, List<Integer>> ADDRESS_TO_TYPE = Map.ofEntries(
+            entry(Constants.ADDR_TV, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TV)),
+            entry(Constants.ADDR_RECORDER_1,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER)),
+            entry(Constants.ADDR_RECORDER_2,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER)),
+            entry(Constants.ADDR_TUNER_1, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER)),
+            entry(Constants.ADDR_PLAYBACK_1,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK)),
+            entry(Constants.ADDR_AUDIO_SYSTEM,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)),
+            entry(Constants.ADDR_TUNER_2, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER)),
+            entry(Constants.ADDR_TUNER_3, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER)),
+            entry(Constants.ADDR_PLAYBACK_2,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK)),
+            entry(Constants.ADDR_RECORDER_3,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_RECORDER)),
+            entry(Constants.ADDR_TUNER_4, Lists.newArrayList(HdmiDeviceInfo.DEVICE_TUNER)),
+            entry(Constants.ADDR_PLAYBACK_3,
+                    Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK)),
+            entry(Constants.ADDR_BACKUP_1, Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK,
+                    HdmiDeviceInfo.DEVICE_RECORDER, HdmiDeviceInfo.DEVICE_TUNER,
+                    HdmiDeviceInfo.DEVICE_VIDEO_PROCESSOR)),
+            entry(Constants.ADDR_BACKUP_2, Lists.newArrayList(HdmiDeviceInfo.DEVICE_PLAYBACK,
+                    HdmiDeviceInfo.DEVICE_RECORDER, HdmiDeviceInfo.DEVICE_TUNER,
+                    HdmiDeviceInfo.DEVICE_VIDEO_PROCESSOR)),
+            entry(Constants.ADDR_SPECIFIC_USE, Lists.newArrayList(ADDR_TV)),
+            entry(Constants.ADDR_UNREGISTERED, Collections.emptyList()));
 
     private static final String[] DEFAULT_NAMES = {
         "TV",
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 324eefc..c83fa2d 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -32,6 +32,7 @@
 import android.os.UEventObserver;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
 import android.view.InputDevice;
@@ -44,6 +45,8 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
@@ -63,6 +66,8 @@
 
     @VisibleForTesting
     static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds
+    @VisibleForTesting
+    static final long USI_BATTERY_VALIDITY_DURATION_MILLIS = 60 * 60_000; // 1 hour
 
     private final Object mLock = new Object();
     private final Context mContext;
@@ -98,8 +103,12 @@
     }
 
     public void systemRunning() {
-        Objects.requireNonNull(mContext.getSystemService(InputManager.class))
-                .registerInputDeviceListener(mInputDeviceListener, mHandler);
+        final InputManager inputManager =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
+        for (int deviceId : inputManager.getInputDeviceIds()) {
+            mInputDeviceListener.onInputDeviceAdded(deviceId);
+        }
     }
 
     /**
@@ -165,19 +174,20 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void notifyAllListenersForDeviceLocked(State state) {
-        if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
-        mListenerRecords.forEach((pid, listenerRecord) -> {
-            if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
-                notifyBatteryListener(listenerRecord, state);
-            }
-        });
+    private void notifyAllListenersForDevice(State state) {
+        synchronized (mLock) {
+            if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
+            mListenerRecords.forEach((pid, listenerRecord) -> {
+                if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
+                    notifyBatteryListener(listenerRecord, state);
+                }
+            });
+        }
     }
 
     @GuardedBy("mLock")
     private void updatePollingLocked(boolean delayStart) {
-        if (mDeviceMonitors.isEmpty() || !mIsInteractive) {
+        if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) {
             // Stop polling.
             mIsPolling = false;
             mHandler.removeCallbacks(this::handlePollEvent);
@@ -192,6 +202,13 @@
         mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
     }
 
+    private String getInputDeviceName(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null ? device.getName() : "<none>";
+    }
+
     private boolean hasBattery(int deviceId) {
         final InputDevice device =
                 Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -199,6 +216,13 @@
         return device != null && device.hasBattery();
     }
 
+    private boolean isUsiDevice(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null && device.supportsUsi();
+    }
+
     @GuardedBy("mLock")
     private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) {
         return Objects.requireNonNull(mDeviceMonitors.get(deviceId),
@@ -252,8 +276,10 @@
         if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
             // There are no more listeners monitoring this device.
             final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId);
-            monitor.stopMonitoring();
-            mDeviceMonitors.remove(deviceId);
+            if (!monitor.isPersistent()) {
+                monitor.onMonitorDestroy();
+                mDeviceMonitors.remove(deviceId);
+            }
         }
 
         if (listenerRecord.mMonitoredDevices.isEmpty()) {
@@ -298,9 +324,7 @@
             if (monitor == null) {
                 return;
             }
-            if (monitor.updateBatteryState(eventTime)) {
-                notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-            }
+            monitor.onUEvent(eventTime);
         }
     }
 
@@ -310,18 +334,22 @@
                 return;
             }
             final long eventTime = SystemClock.uptimeMillis();
-            mDeviceMonitors.forEach((deviceId, monitor) -> {
-                // Re-acquire lock in the lambda to silence error-prone build warnings.
-                synchronized (mLock) {
-                    if (monitor.updateBatteryState(eventTime)) {
-                        notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                    }
-                }
-            });
+            mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime));
             mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
         }
     }
 
+    private void handleMonitorTimeout(int deviceId) {
+        synchronized (mLock) {
+            final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
+            if (monitor == null) {
+                return;
+            }
+            final long updateTime = SystemClock.uptimeMillis();
+            monitor.onTimeout(updateTime);
+        }
+    }
+
     /** Gets the current battery state of an input device. */
     public IInputDeviceBatteryState getBatteryState(int deviceId) {
         synchronized (mLock) {
@@ -329,15 +357,11 @@
             final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
             if (monitor == null) {
                 // The input device's battery is not being monitored by any listener.
-                return queryBatteryStateFromNative(deviceId, updateTime);
+                return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId));
             }
             // Force the battery state to update, and notify listeners if necessary.
-            final boolean stateChanged = monitor.updateBatteryState(updateTime);
-            final State state = monitor.getBatteryStateForReporting();
-            if (stateChanged) {
-                notifyAllListenersForDeviceLocked(state);
-            }
-            return state;
+            monitor.onPoll(updateTime);
+            return monitor.getBatteryStateForReporting();
         }
     }
 
@@ -348,24 +372,39 @@
         }
     }
 
-    public void dump(PrintWriter pw, String prefix) {
+    public void notifyStylusGestureStarted(int deviceId, long eventTime) {
         synchronized (mLock) {
-            final String indent = prefix + "  ";
-            final String indent2 = indent + "  ";
+            final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
+            if (monitor == null) {
+                return;
+            }
 
-            pw.println(prefix + TAG + ":");
-            pw.println(indent + "State: Polling = " + mIsPolling
+            monitor.onStylusGestureStarted(eventTime);
+        }
+    }
+
+    public void dump(PrintWriter pw) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+        synchronized (mLock) {
+            ipw.println(TAG + ":");
+            ipw.increaseIndent();
+            ipw.println("State: Polling = " + mIsPolling
                     + ", Interactive = " + mIsInteractive);
 
-            pw.println(indent + "Listeners: " + mListenerRecords.size() + " battery listeners");
+            ipw.println("Listeners: " + mListenerRecords.size() + " battery listeners");
+            ipw.increaseIndent();
             for (int i = 0; i < mListenerRecords.size(); i++) {
-                pw.println(indent2 + i + ": " + mListenerRecords.valueAt(i));
+                ipw.println(i + ": " + mListenerRecords.valueAt(i));
             }
+            ipw.decreaseIndent();
 
-            pw.println(indent + "Device Monitors: " + mDeviceMonitors.size() + " monitors");
+            ipw.println("Device Monitors: " + mDeviceMonitors.size() + " monitors");
+            ipw.increaseIndent();
             for (int i = 0; i < mDeviceMonitors.size(); i++) {
-                pw.println(indent2 + i + ": " + mDeviceMonitors.valueAt(i));
+                ipw.println(i + ": " + mDeviceMonitors.valueAt(i));
             }
+            ipw.decreaseIndent();
+            ipw.decreaseIndent();
         }
     }
 
@@ -379,7 +418,14 @@
     private final InputManager.InputDeviceListener mInputDeviceListener =
             new InputManager.InputDeviceListener() {
         @Override
-        public void onInputDeviceAdded(int deviceId) {}
+        public void onInputDeviceAdded(int deviceId) {
+            synchronized (mLock) {
+                if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) {
+                    // Start monitoring USI device immediately.
+                    mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId));
+                }
+            }
+        }
 
         @Override
         public void onInputDeviceRemoved(int deviceId) {}
@@ -392,9 +438,7 @@
                     return;
                 }
                 final long eventTime = SystemClock.uptimeMillis();
-                if (monitor.updateBatteryState(eventTime)) {
-                    notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                }
+                monitor.onConfiguration(eventTime);
             }
         }
     };
@@ -422,8 +466,7 @@
     }
 
     // Queries the battery state of an input device from native code.
-    private State queryBatteryStateFromNative(int deviceId, long updateTime) {
-        final boolean isPresent = hasBattery(deviceId);
+    private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) {
         return new State(
                 deviceId,
                 updateTime,
@@ -434,8 +477,9 @@
 
     // Holds the state of an InputDevice for which battery changes are currently being monitored.
     private class DeviceMonitor {
-        @NonNull
-        private State mState;
+        protected final State mState;
+        // Represents whether the input device has a sysfs battery node.
+        protected boolean mHasBattery = false;
 
         @Nullable
         private UEventBatteryListener mUEventBatteryListener;
@@ -445,26 +489,32 @@
 
             // Load the initial battery state and start monitoring.
             final long eventTime = SystemClock.uptimeMillis();
-            updateBatteryState(eventTime);
+            configureDeviceMonitor(eventTime);
         }
 
-        // Returns true if the battery state changed since the last time it was updated.
-        public boolean updateBatteryState(long updateTime) {
-            mState.updateTime = updateTime;
-
-            final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime);
-            if (mState.equals(updatedState)) {
-                return false;
+        protected void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
+            final State oldState = getBatteryStateForReporting();
+            changes.accept(eventTime);
+            final State newState = getBatteryStateForReporting();
+            if (!oldState.equals(newState)) {
+                notifyAllListenersForDevice(newState);
             }
-            if (mState.isPresent != updatedState.isPresent) {
-                if (updatedState.isPresent) {
+        }
+
+        public void onConfiguration(long eventTime) {
+            processChangesAndNotify(eventTime, this::configureDeviceMonitor);
+        }
+
+        private void configureDeviceMonitor(long eventTime) {
+            if (mHasBattery != hasBattery(mState.deviceId)) {
+                mHasBattery = !mHasBattery;
+                if (mHasBattery) {
                     startMonitoring();
                 } else {
                     stopMonitoring();
                 }
+                updateBatteryStateFromNative(eventTime);
             }
-            mState = updatedState;
-            return true;
         }
 
         private void startMonitoring() {
@@ -483,19 +533,48 @@
                     mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath));
         }
 
-        private String formatDevPath(String path) {
+        private String formatDevPath(@NonNull String path) {
             // Remove the "/sys" prefix if it has one.
             return path.startsWith("/sys") ? path.substring(4) : path;
         }
 
-        // This must be called when the device is no longer being monitored.
-        public void stopMonitoring() {
+        private void stopMonitoring() {
             if (mUEventBatteryListener != null) {
                 mUEventManager.removeListener(mUEventBatteryListener);
                 mUEventBatteryListener = null;
             }
         }
 
+        // This must be called when the device is no longer being monitored.
+        public void onMonitorDestroy() {
+            stopMonitoring();
+        }
+
+        protected void updateBatteryStateFromNative(long eventTime) {
+            mState.updateIfChanged(
+                    queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
+        }
+
+        public void onPoll(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public void onUEvent(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public boolean requiresPolling() {
+            return true;
+        }
+
+        public boolean isPersistent() {
+            return false;
+        }
+
+        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);
@@ -503,8 +582,114 @@
 
         @Override
         public String toString() {
-            return "state=" + mState
-                    + ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+            return "DeviceId=" + mState.deviceId
+                    + ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+                    + ", NativeBattery=" + mState
+                    + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+        }
+    }
+
+    // Battery monitoring logic that is specific to stylus devices that support the
+    // Universal Stylus Initiative (USI) protocol.
+    private class UsiDeviceMonitor extends DeviceMonitor {
+
+        // For USI devices, we only treat the battery state as valid for a fixed amount of time
+        // after receiving a battery update. Once the timeout has passed, we signal to all listeners
+        // that there is no longer a battery present for the device. The battery state is valid
+        // as long as this callback is non-null.
+        @Nullable
+        private Runnable mValidityTimeoutCallback;
+
+        UsiDeviceMonitor(int deviceId) {
+            super(deviceId);
+        }
+
+        @Override
+        public void onPoll(long eventTime) {
+            // Disregard polling for USI devices.
+        }
+
+        @Override
+        public void onUEvent(long eventTime) {
+            processChangesAndNotify(eventTime, (time) -> {
+                updateBatteryStateFromNative(time);
+                markUsiBatteryValid();
+            });
+        }
+
+        @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());
+        }
+
+        @Override
+        public void onConfiguration(long eventTime) {
+            super.onConfiguration(eventTime);
+
+            if (!mHasBattery) {
+                throw new IllegalStateException(
+                        "UsiDeviceMonitor: USI devices are always expected to "
+                                + "report a valid battery, but no battery was detected!");
+            }
+        }
+
+        private void markUsiBatteryValid() {
+            if (mValidityTimeoutCallback != null) {
+                mHandler.removeCallbacks(mValidityTimeoutCallback);
+            } else {
+                final int deviceId = mState.deviceId;
+                mValidityTimeoutCallback =
+                        () -> BatteryController.this.handleMonitorTimeout(deviceId);
+            }
+            mHandler.postDelayed(mValidityTimeoutCallback, USI_BATTERY_VALIDITY_DURATION_MILLIS);
+        }
+
+        private void markUsiBatteryInvalid() {
+            if (mValidityTimeoutCallback == null) {
+                return;
+            }
+            mHandler.removeCallbacks(mValidityTimeoutCallback);
+            mValidityTimeoutCallback = null;
+        }
+
+        @Override
+        public State getBatteryStateForReporting() {
+            return mValidityTimeoutCallback != null
+                    ? new State(mState) : new State(mState.deviceId);
+        }
+
+        @Override
+        public boolean requiresPolling() {
+            // Do not poll the battery state for USI devices.
+            return false;
+        }
+
+        @Override
+        public boolean isPersistent() {
+            // Do not remove the battery monitor for USI devices.
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString()
+                    + ", UsiStateIsValid=" + (mValidityTimeoutCallback != null);
         }
     }
 
@@ -548,18 +733,33 @@
     private static class State extends IInputDeviceBatteryState {
 
         State(int deviceId) {
-            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
-                    Float.NaN /*capacity*/);
+            reset(deviceId);
         }
 
         State(IInputDeviceBatteryState s) {
-            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+            copyFrom(s);
         }
 
         State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) {
             initialize(deviceId, updateTime, isPresent, status, capacity);
         }
 
+        // Updates this from other if there is a difference between them, ignoring the updateTime.
+        public void updateIfChanged(IInputDeviceBatteryState other) {
+            if (!equalsIgnoringUpdateTime(other)) {
+                copyFrom(other);
+            }
+        }
+
+        public void reset(int deviceId) {
+            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
+                    Float.NaN /*capacity*/);
+        }
+
+        private void copyFrom(IInputDeviceBatteryState s) {
+            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+        }
+
         private void initialize(int deviceId, long updateTime, boolean isPresent, int status,
                 float capacity) {
             this.deviceId = deviceId;
@@ -569,11 +769,34 @@
             this.capacity = capacity;
         }
 
+        private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) {
+            long updateTime = this.updateTime;
+            this.updateTime = other.updateTime;
+            boolean eq = this.equals(other);
+            this.updateTime = updateTime;
+            return eq;
+        }
+
         @Override
         public String toString() {
-            return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime
-                    + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity
-                    + " }";
+            if (!isPresent) {
+                return "State{<not present>}";
+            }
+            return "State{time=" + updateTime
+                    + ", isPresent=" + isPresent
+                    + ", status=" + status
+                    + ", capacity=" + capacity
+                    + "}";
         }
     }
+
+    // Check if any value in an ArrayMap matches the predicate in an optimized way.
+    private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
+        for (int i = 0; i < arrayMap.size(); i++) {
+            if (test.test(arrayMap.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/input/ConfigurationProcessor.java b/services/core/java/com/android/server/input/ConfigurationProcessor.java
index 0563806..b6953a3 100644
--- a/services/core/java/com/android/server/input/ConfigurationProcessor.java
+++ b/services/core/java/com/android/server/input/ConfigurationProcessor.java
@@ -18,11 +18,11 @@
 
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import java.io.InputStream;
 import java.util.ArrayList;
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 7eb5a10..298098a 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -17,10 +17,15 @@
 package com.android.server.input;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.graphics.PointF;
 import android.hardware.display.DisplayViewport;
 import android.os.IBinder;
 import android.view.InputChannel;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 
 import java.util.List;
 
@@ -142,6 +147,20 @@
     public abstract void pilferPointers(IBinder token);
 
     /**
+     * Called when the current input method and/or {@link InputMethodSubtype} is updated.
+     *
+     * @param userId User ID to be notified about.
+     * @param subtypeHandle A {@link InputMethodSubtypeHandle} corresponds to {@code subtype}.
+     * @param subtype A {@link InputMethodSubtype} object, or {@code null} when the current
+     *                {@link InputMethodSubtype} is not suitable for the physical keyboard layout
+     *                mapping.
+     * @see InputMethodSubtype#isSuitableForPhysicalKeyboardLayoutMapping()
+     */
+    public abstract void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+            @Nullable InputMethodSubtypeHandle subtypeHandle,
+            @Nullable InputMethodSubtype subtype);
+
+    /**
      * Increments keyboard backlight level if the device has an associated keyboard backlight
      * {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT}
      */
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 69b0e65..8497dfb 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -19,8 +19,11 @@
 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.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -89,6 +92,7 @@
 import android.provider.Settings.SettingNotFoundException;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -108,11 +112,13 @@
 import android.view.SurfaceControl;
 import android.view.VerifiedInputEvent;
 import android.view.ViewConfiguration;
+import android.view.inputmethod.InputMethodSubtype;
 import android.widget.Toast;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.SomeArgs;
@@ -300,9 +306,9 @@
     private final AdditionalDisplayInputProperties mCurrentDisplayProperties =
             new AdditionalDisplayInputProperties();
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private int mIconType = PointerIcon.TYPE_NOT_SPECIFIED;
+    private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private PointerIcon mIcon;
+    private PointerIcon mPointerIcon;
 
     // Holds all the registered gesture monitors that are implemented as spy windows. The spy
     // windows are mapped by their InputChannel tokens.
@@ -1808,8 +1814,8 @@
      */
     public boolean transferTouchFocus(@NonNull IBinder fromChannelToken,
             @NonNull IBinder toChannelToken) {
-        Objects.nonNull(fromChannelToken);
-        Objects.nonNull(toChannelToken);
+        Objects.requireNonNull(fromChannelToken);
+        Objects.requireNonNull(toChannelToken);
         return mNative.transferTouchFocus(fromChannelToken, toChannelToken,
                 false /* isDragDrop */);
     }
@@ -2323,12 +2329,12 @@
             throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers");
         }
         synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mIcon = null;
-            mIconType = iconType;
+            mPointerIcon = null;
+            mPointerIconType = iconType;
 
             if (!mCurrentDisplayProperties.pointerIconVisible) return;
 
-            mNative.setPointerIconType(mIconType);
+            mNative.setPointerIconType(mPointerIconType);
         }
     }
 
@@ -2337,12 +2343,12 @@
     public void setCustomPointerIcon(PointerIcon icon) {
         Objects.requireNonNull(icon);
         synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mIconType = PointerIcon.TYPE_CUSTOM;
-            mIcon = icon;
+            mPointerIconType = PointerIcon.TYPE_CUSTOM;
+            mPointerIcon = icon;
 
             if (!mCurrentDisplayProperties.pointerIconVisible) return;
 
-            mNative.setCustomPointerIcon(mIcon);
+            mNative.setCustomPointerIcon(mPointerIcon);
         }
     }
 
@@ -2671,77 +2677,98 @@
         mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid());
     }
 
+    @EnforcePermission(Manifest.permission.BLUETOOTH)
+    @Override
+    public String getInputDeviceBluetoothAddress(int deviceId) {
+        super.getInputDeviceBluetoothAddress_enforcePermission();
+
+        return mNative.getBluetoothAddress(deviceId);
+    }
+
+    @EnforcePermission(Manifest.permission.MONITOR_INPUT)
+    @Override
+    public void pilferPointers(IBinder inputChannelToken) {
+        super.pilferPointers_enforcePermission();
+
+        Objects.requireNonNull(inputChannelToken);
+        mNative.pilferPointers(inputChannelToken);
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
 
-        pw.println("INPUT MANAGER (dumpsys input)\n");
+        ipw.println("INPUT MANAGER (dumpsys input)\n");
         String dumpStr = mNative.dump();
         if (dumpStr != null) {
             pw.println(dumpStr);
         }
 
-        pw.println("Input Manager Service (Java) State:");
-        dumpAssociations(pw, "  " /*prefix*/);
-        dumpSpyWindowGestureMonitors(pw, "  " /*prefix*/);
-        dumpDisplayInputPropertiesValues(pw, "  " /*prefix*/);
-        mBatteryController.dump(pw, "  " /*prefix*/);
-        mKeyboardBacklightController.dump(pw, "  " /*prefix*/);
+        ipw.println("Input Manager Service (Java) State:");
+        ipw.increaseIndent();
+        dumpAssociations(ipw);
+        dumpSpyWindowGestureMonitors(ipw);
+        dumpDisplayInputPropertiesValues(ipw);
+        mBatteryController.dump(ipw);
+        mKeyboardBacklightController.dump(ipw);
     }
 
-    private void dumpAssociations(PrintWriter pw, String prefix) {
+    private void dumpAssociations(IndentingPrintWriter pw) {
         if (!mStaticAssociations.isEmpty()) {
-            pw.println(prefix + "Static Associations:");
+            pw.println("Static Associations:");
             mStaticAssociations.forEach((k, v) -> {
-                pw.print(prefix + "  port: " + k);
+                pw.print("  port: " + k);
                 pw.println("  display: " + v);
             });
         }
 
         synchronized (mAssociationsLock) {
             if (!mRuntimeAssociations.isEmpty()) {
-                pw.println(prefix + "Runtime Associations:");
+                pw.println("Runtime Associations:");
                 mRuntimeAssociations.forEach((k, v) -> {
-                    pw.print(prefix + "  port: " + k);
+                    pw.print("  port: " + k);
                     pw.println("  display: " + v);
                 });
             }
             if (!mUniqueIdAssociations.isEmpty()) {
-                pw.println(prefix + "Unique Id Associations:");
+                pw.println("Unique Id Associations:");
                 mUniqueIdAssociations.forEach((k, v) -> {
-                    pw.print(prefix + "  port: " + k);
+                    pw.print("  port: " + k);
                     pw.println("  uniqueId: " + v);
                 });
             }
         }
     }
 
-    private void dumpSpyWindowGestureMonitors(PrintWriter pw, String prefix) {
+    private void dumpSpyWindowGestureMonitors(IndentingPrintWriter pw) {
         synchronized (mInputMonitors) {
             if (mInputMonitors.isEmpty()) return;
-            pw.println(prefix + "Gesture Monitors (implemented as spy windows):");
+            pw.println("Gesture Monitors (implemented as spy windows):");
             int i = 0;
             for (final GestureMonitorSpyWindow monitor : mInputMonitors.values()) {
-                pw.append(prefix + "  " + i++ + ": ").println(monitor.dump());
+                pw.append("  " + i++ + ": ").println(monitor.dump());
             }
         }
     }
 
-    private void dumpDisplayInputPropertiesValues(PrintWriter pw, String prefix) {
+    private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) {
         synchronized (mAdditionalDisplayInputPropertiesLock) {
             if (mAdditionalDisplayInputProperties.size() != 0) {
-                pw.println(prefix + "mAdditionalDisplayInputProperties:");
+                pw.println("mAdditionalDisplayInputProperties:");
+                pw.increaseIndent();
                 for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) {
-                    pw.println(prefix + "  displayId: "
+                    pw.println("displayId: "
                             + mAdditionalDisplayInputProperties.keyAt(i));
                     final AdditionalDisplayInputProperties properties =
                             mAdditionalDisplayInputProperties.valueAt(i);
-                    pw.println(prefix + "  pointerAcceleration: " + properties.pointerAcceleration);
-                    pw.println(prefix + "  pointerIconVisible: " + properties.pointerIconVisible);
+                    pw.println("pointerAcceleration: " + properties.pointerAcceleration);
+                    pw.println("pointerIconVisible: " + properties.pointerIconVisible);
                 }
+                pw.decreaseIndent();
             }
             if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
-                pw.println(prefix + "mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
+                pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
             }
         }
     }
@@ -3052,6 +3079,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.
@@ -3766,6 +3799,16 @@
         }
 
         @Override
+        public void onInputMethodSubtypeChangedForKeyboardLayoutMapping(@UserIdInt int userId,
+                @Nullable InputMethodSubtypeHandle subtypeHandle,
+                @Nullable InputMethodSubtype subtype) {
+            if (DEBUG) {
+                Slog.i(TAG, "InputMethodSubtype changed: userId=" + userId
+                        + " subtypeHandle=" + subtypeHandle);
+            }
+        }
+
+        @Override
         public void incrementKeyboardBacklight(int deviceId) {
             mKeyboardBacklightController.incrementKeyboardBacklight(deviceId);
         }
@@ -3825,11 +3868,11 @@
         if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
             mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
             if (properties.pointerIconVisible) {
-                if (mIconType == PointerIcon.TYPE_CUSTOM) {
-                    Objects.requireNonNull(mIcon);
-                    mNative.setCustomPointerIcon(mIcon);
+                if (mPointerIconType == PointerIcon.TYPE_CUSTOM) {
+                    Objects.requireNonNull(mPointerIcon);
+                    mNative.setCustomPointerIcon(mPointerIcon);
                 } else {
-                    mNative.setPointerIconType(mIconType);
+                    mNative.setPointerIconType(mPointerIconType);
                 }
             } else {
                 mNative.setPointerIconType(PointerIcon.TYPE_NULL);
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index e33f28c..b207e27 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -24,6 +24,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -216,12 +217,14 @@
         return null;
     }
 
-    void dump(PrintWriter pw, String prefix) {
-        pw.println(prefix + TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+    void dump(PrintWriter pw) {
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+        ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
+        ipw.increaseIndent();
         for (int i = 0; i < mKeyboardBacklights.size(); i++) {
             Light light = mKeyboardBacklights.get(i);
-            pw.println(prefix + "  " + i + ": { id: " + light.getId() + ", name: " + light.getName()
-                    + " }");
+            ipw.println(i + ": { id: " + light.getId() + ", name: " + light.getName() + " }");
         }
+        ipw.decreaseIndent();
     }
 }
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/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 5513cd6..1bb10c7 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -21,8 +21,6 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Surface;
 
@@ -34,6 +32,9 @@
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
index 816d08a..4b040fa 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
@@ -25,12 +25,13 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -57,6 +58,7 @@
     private static final String NODE_IMI = "imi";
     private static final String ATTR_ID = "id";
     private static final String ATTR_LABEL = "label";
+    private static final String ATTR_NAME_OVERRIDE = "nameOverride";
     private static final String ATTR_ICON = "icon";
     private static final String ATTR_IME_SUBTYPE_ID = "subtypeId";
     private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
@@ -160,6 +162,7 @@
                     }
                     out.attributeInt(null, ATTR_ICON, subtype.getIconResId());
                     out.attributeInt(null, ATTR_LABEL, subtype.getNameResId());
+                    out.attribute(null, ATTR_NAME_OVERRIDE, subtype.getNameOverride().toString());
                     out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
                     out.attribute(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG,
                             subtype.getLanguageTag());
@@ -242,6 +245,8 @@
                     }
                     final int icon = parser.getAttributeInt(null, ATTR_ICON);
                     final int label = parser.getAttributeInt(null, ATTR_LABEL);
+                    final String untranslatableName = parser.getAttributeValue(null,
+                            ATTR_NAME_OVERRIDE);
                     final String imeSubtypeLocale =
                             parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
                     final String languageTag =
@@ -257,6 +262,7 @@
                     final InputMethodSubtype.InputMethodSubtypeBuilder
                             builder = new InputMethodSubtype.InputMethodSubtypeBuilder()
                             .setSubtypeNameResId(label)
+                            .setSubtypeNameOverride(untranslatableName)
                             .setSubtypeIconResId(icon)
                             .setSubtypeLocale(imeSubtypeLocale)
                             .setLanguageTag(languageTag)
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index a4830be..1c7294f 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -18,9 +18,12 @@
 
 import static android.view.InputDevice.SOURCE_STYLUS;
 
+import android.Manifest;
 import android.annotation.AnyThread;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UiThread;
+import android.hardware.input.InputManager;
 import android.os.IBinder;
 import android.os.Looper;
 import android.util.Slog;
@@ -141,6 +144,7 @@
      * input events and disposing the input event receiver.
      * @return the handwriting session to send to the IME, or null if the request was invalid.
      */
+    @RequiresPermission(Manifest.permission.MONITOR_INPUT)
     @UiThread
     @Nullable
     HandwritingSession startHandwritingSession(
@@ -169,7 +173,7 @@
         }
         if (DEBUG) Slog.d(TAG, "Starting handwriting session in display: " + mCurrentDisplayId);
 
-        mInputManagerInternal.pilferPointers(mHandwritingSurface.getInputChannel().getToken());
+        InputManager.getInstance().pilferPointers(mHandwritingSurface.getInputChannel().getToken());
 
         // Stop processing more events.
         mHandwritingEventReceiver.dispose();
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index 1a0f6f7..015e576 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -28,6 +28,7 @@
 import android.view.InputChannel;
 import android.view.MotionEvent;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputMethodSubtype;
 import android.window.ImeOnBackInvokedDispatcher;
@@ -198,9 +199,10 @@
 
     // TODO(b/192412909): Convert this back to void method
     @AnyThread
-    boolean showSoftInput(IBinder showInputToken, int flags, ResultReceiver resultReceiver) {
+    boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+            ResultReceiver resultReceiver) {
         try {
-            mTarget.showSoftInput(showInputToken, flags, resultReceiver);
+            mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
         } catch (RemoteException e) {
             logRemoteException(e);
             return false;
@@ -210,9 +212,10 @@
 
     // TODO(b/192412909): Convert this back to void method
     @AnyThread
-    boolean hideSoftInput(IBinder hideInputToken, int flags, ResultReceiver resultReceiver) {
+    boolean hideSoftInput(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken, int flags,
+            ResultReceiver resultReceiver) {
         try {
-            mTarget.hideSoftInput(hideInputToken, flags, resultReceiver);
+            mTarget.hideSoftInput(hideInputToken, statsToken, flags, resultReceiver);
         } catch (RemoteException e) {
             logRemoteException(e);
             return false;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 76331fd..8b083bd 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -58,7 +58,6 @@
 import android.accessibilityservice.AccessibilityService;
 import android.annotation.AnyThread;
 import android.annotation.BinderThread;
-import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.DurationMillisLong;
 import android.annotation.EnforcePermission;
@@ -69,9 +68,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -94,7 +90,6 @@
 import android.media.AudioManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.IBinder;
@@ -134,6 +129,7 @@
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethod;
@@ -149,12 +145,14 @@
 import android.window.ImeOnBackInvokedDispatcher;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.inputmethod.DirectBootAwareness;
 import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
 import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
 import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethod;
 import com.android.internal.inputmethod.IInputMethodClient;
 import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
 import com.android.internal.inputmethod.IInputMethodSession;
@@ -166,12 +164,11 @@
 import com.android.internal.inputmethod.InputBindResult;
 import com.android.internal.inputmethod.InputMethodDebug;
 import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.inputmethod.StartInputFlags;
 import com.android.internal.inputmethod.StartInputReason;
 import com.android.internal.inputmethod.UnbindReason;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.TransferPipe;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ConcurrentUtils;
@@ -255,13 +252,6 @@
     private static final String HANDLER_THREAD_NAME = "android.imms";
 
     /**
-     * A protected broadcast intent action for internal use for {@link PendingIntent} in
-     * the notification.
-     */
-    private static final String ACTION_SHOW_INPUT_METHOD_PICKER =
-            "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER";
-
-    /**
      * When set, {@link #startInputUncheckedLocked} will return
      * {@link InputBindResult#NO_EDITOR} instead of starting an IME connection
      * unless {@link StartInputFlags#IS_TEXT_EDITOR} is set. This behavior overrides
@@ -334,13 +324,8 @@
     @GuardedBy("ImfLock.class")
     private int mDisplayIdToShowIme = INVALID_DISPLAY;
 
-    // Ongoing notification
-    private NotificationManager mNotificationManager;
     @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
-    private final Notification.Builder mImeSwitcherNotification;
-    private final PendingIntent mImeSwitchPendingIntent;
     private boolean mShowOngoingImeSwitcherForPhones;
-    private boolean mNotificationShown;
     @GuardedBy("ImfLock.class")
     private final HandwritingModeController mHwController;
     @GuardedBy("ImfLock.class")
@@ -659,6 +644,10 @@
      */
     private boolean mInputShown;
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mCurStatsToken;
+
     /**
      * {@code true} if the current input method is in fullscreen mode.
      */
@@ -778,7 +767,7 @@
      * <dd>
      *   If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
      * </dd>
-     * dt>{@link InputMethodService#IME_INVISIBLE}</dt>
+     * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
      * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
      *    currently invisible.
      * </dd>
@@ -802,8 +791,7 @@
 
     /**
      * Internal state snapshot when
-     * {@link com.android.internal.view.IInputMethod#startInput(IBinder, IRemoteInputConnection, EditorInfo,
-     * boolean)} is about to be called.
+     * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
      *
      * <p>Calling that IPC endpoint basically means that
      * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
@@ -1088,7 +1076,7 @@
 
         /**
          * Add a new entry and discard the oldest entry as needed.
-         * @param info {@lin StartInputInfo} to be added.
+         * @param info {@link StartInputInfo} to be added.
          */
         void addEntry(@NonNull StartInputInfo info) {
             final int index = mNextIndex;
@@ -1206,18 +1194,18 @@
                 } else if (accessibilityRequestingNoImeUri.equals(uri)) {
                     final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
                             mContext.getContentResolver(),
-                            Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
+                            Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId);
                     mAccessibilityRequestingNoSoftKeyboard =
                             (accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK)
                                     == AccessibilityService.SHOW_MODE_HIDDEN;
                     if (mAccessibilityRequestingNoSoftKeyboard) {
                         final boolean showRequested = mShowRequested;
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
                         mShowRequested = showRequested;
                     } else if (mShowRequested) {
-                        showCurrentInputLocked(mCurFocusedWindow,
-                                InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(mCurFocusedWindow,
                                 SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
                     }
                 } else {
@@ -1253,17 +1241,6 @@
                 return;
             } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
                 onActionLocaleChanged();
-            } else if (ACTION_SHOW_INPUT_METHOD_PICKER.equals(action)) {
-                // ACTION_SHOW_INPUT_METHOD_PICKER action is a protected-broadcast and it is
-                // guaranteed to be send only from the system, so that there is no need for extra
-                // security check such as
-                // {@link #canShowInputMethodPickerLocked(IInputMethodClient)}.
-                mHandler.obtainMessage(
-                        MSG_SHOW_IM_SUBTYPE_PICKER,
-                        // TODO(b/120076400): Design and implement IME switcher for heterogeneous
-                        // navbar configuration.
-                        InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES,
-                        DEFAULT_DISPLAY).sendToTarget();
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
             }
@@ -1622,8 +1599,13 @@
         private final InputMethodManagerService mService;
 
         public Lifecycle(Context context) {
+            this(context, new InputMethodManagerService(context));
+        }
+
+        public Lifecycle(
+                Context context, @NonNull InputMethodManagerService inputMethodManagerService) {
             super(context);
-            mService = new InputMethodManagerService(context);
+            mService = inputMethodManagerService;
         }
 
         @Override
@@ -1689,8 +1671,8 @@
         }
         // Hide soft input before user switch task since switch task may block main handler a while
         // and delayed the hideCurrentInputLocked().
-        hideCurrentInputLocked(
-                mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_SWITCH_USER);
+        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                null /* resultReceiver */, SoftInputShowHideReason.HIDE_SWITCH_USER);
         final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
                 clientToBeReset);
         mUserSwitchHandlerTask = task;
@@ -1698,12 +1680,25 @@
     }
 
     public InputMethodManagerService(Context context) {
+        this(context, null, null);
+    }
+
+    @VisibleForTesting
+    InputMethodManagerService(
+            Context context,
+            @Nullable ServiceThread serviceThreadForTesting,
+            @Nullable InputMethodBindingController bindingControllerForTesting) {
         mContext = context;
         mRes = context.getResources();
         // TODO(b/196206770): Disallow I/O on this thread. Currently it's needed for loading
         // additional subtypes in switchUserOnHandlerLocked().
-        final ServiceThread thread = new ServiceThread(
-                HANDLER_THREAD_NAME, Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
+        final ServiceThread thread =
+                serviceThreadForTesting != null
+                        ? serviceThreadForTesting
+                        : new ServiceThread(
+                                HANDLER_THREAD_NAME,
+                                Process.THREAD_PRIORITY_FOREGROUND,
+                                true /* allowIo */);
         thread.start();
         mHandler = Handler.createAsync(thread.getLooper(), this);
         // Note: SettingsObserver doesn't register observers in its constructor.
@@ -1720,27 +1715,8 @@
 
         mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
 
-        Bundle extras = new Bundle();
-        extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
-        @ColorInt final int accentColor = mContext.getColor(
-                com.android.internal.R.color.system_notification_accent_color);
-        mImeSwitcherNotification =
-                new Notification.Builder(mContext, SystemNotificationChannels.VIRTUAL_KEYBOARD)
-                        .setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default)
-                        .setWhen(0)
-                        .setOngoing(true)
-                        .addExtras(extras)
-                        .setCategory(Notification.CATEGORY_SYSTEM)
-                        .setColor(accentColor);
-
-        Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
-                .setPackage(mContext.getPackageName());
-        mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
-                PendingIntent.FLAG_IMMUTABLE);
-
         mShowOngoingImeSwitcherForPhones = false;
 
-        mNotificationShown = false;
         final int userId = mActivityManagerInternal.getCurrentUserId();
 
         mLastSwitchUserId = userId;
@@ -1750,10 +1726,13 @@
 
         updateCurrentProfileIds();
         AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
-        mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
-                mSettings, context);
+        mSwitchingController =
+                InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);
         mMenuController = new InputMethodMenuController(this);
-        mBindingController = new InputMethodBindingController(this);
+        mBindingController =
+                bindingControllerForTesting != null
+                        ? bindingControllerForTesting
+                        : new InputMethodBindingController(this);
         mAutofillController = new AutofillSuggestionsController(this);
         mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
                 com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
@@ -1939,7 +1918,6 @@
                 final int currentUserId = mSettings.getCurrentUserId();
                 mSettings.switchCurrentUser(currentUserId,
                         !mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId));
-                mNotificationManager = mContext.getSystemService(NotificationManager.class);
                 mStatusBarManagerInternal =
                         LocalServices.getService(StatusBarManagerInternal.class);
                 hideStatusBarIconLocked();
@@ -1977,7 +1955,6 @@
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_ADDED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_REMOVED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_LOCALE_CHANGED);
-                broadcastFilterForSystemUser.addAction(ACTION_SHOW_INPUT_METHOD_PICKER);
                 mContext.registerReceiver(new ImmsBroadcastReceiverForSystemUser(),
                         broadcastFilterForSystemUser);
 
@@ -2250,7 +2227,7 @@
             }
             final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
             try {
-                client.asBinder().linkToDeath(deathRecipient, 0);
+                client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
@@ -2275,7 +2252,7 @@
         synchronized (ImfLock.class) {
             ClientState cs = mClients.remove(client.asBinder());
             if (cs != null) {
-                client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0);
+                client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
                 clearClientSessionLocked(cs);
                 clearClientSessionForAccessibilityLocked(cs);
 
@@ -2288,8 +2265,8 @@
                 }
 
                 if (mCurClient == cs) {
-                    hideCurrentInputLocked(
-                            mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
                     if (mBoundToMethod) {
                         mBoundToMethod = false;
                         IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -2336,6 +2313,8 @@
             mCurClient.mSessionRequestedForAccessibility = false;
             mCurClient = null;
             mCurVirtualDisplayToScreenMatrix = null;
+            ImeTracker.get().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            mCurStatsToken = null;
 
             mMenuController.hideInputMethodMenuLocked();
         }
@@ -2408,8 +2387,11 @@
                 navButtonFlags, mCurImeDispatcher);
         if (mShowRequested) {
             if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
-            showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null,
-                    SoftInputShowHideReason.ATTACH_NEW_INPUT);
+            // Re-use current statsToken, if it exists.
+            final ImeTracker.Token statsToken = mCurStatsToken;
+            mCurStatsToken = null;
+            showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(),
+                    null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
         }
 
         String curId = getCurIdLocked();
@@ -2528,7 +2510,8 @@
 
         if (mDisplayIdToShowIme == INVALID_DISPLAY) {
             mImeHiddenByDisplayPolicy = true;
-            hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+            hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                    null /* resultReceiver */,
                     SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
             return InputBindResult.NO_IME;
         }
@@ -3159,41 +3142,6 @@
                 mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
                         getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher);
             }
-            final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
-            if (imi != null && needsToShowImeSwitcher) {
-                // Used to load label
-                final CharSequence title = mRes.getText(
-                        com.android.internal.R.string.select_input_method);
-                final int currentUserId = mSettings.getCurrentUserId();
-                final Context userAwareContext = mContext.getUserId() == currentUserId
-                        ? mContext
-                        : mContext.createContextAsUser(UserHandle.of(currentUserId), 0 /* flags */);
-                final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName(
-                        userAwareContext, imi, mCurrentSubtype);
-                mImeSwitcherNotification.setContentTitle(title)
-                        .setContentText(summary)
-                        .setContentIntent(mImeSwitchPendingIntent);
-                // TODO(b/120076400): Figure out what is the best behavior
-                if ((mNotificationManager != null)
-                        && !mWindowManagerInternal.hasNavigationBar(DEFAULT_DISPLAY)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- show notification: label =  " + summary);
-                    }
-                    mNotificationManager.notifyAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD,
-                            mImeSwitcherNotification.build(), UserHandle.ALL);
-                    mNotificationShown = true;
-                }
-            } else {
-                if (mNotificationShown && mNotificationManager != null) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- hide notification");
-                    }
-                    mNotificationManager.cancelAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD, UserHandle.ALL);
-                    mNotificationShown = false;
-                }
-            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -3265,16 +3213,30 @@
     }
 
     @GuardedBy("ImfLock.class")
+    private void notifyInputMethodSubtypeChangedLocked(@UserIdInt int userId,
+            @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+        final InputMethodSubtype normalizedSubtype =
+                subtype != null && subtype.isSuitableForPhysicalKeyboardLayoutMapping()
+                        ? subtype : null;
+        final InputMethodSubtypeHandle newSubtypeHandle = normalizedSubtype != null
+                ? InputMethodSubtypeHandle.of(imi, normalizedSubtype) : null;
+        mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping(
+                userId, newSubtypeHandle, normalizedSubtype);
+    }
+
+    @GuardedBy("ImfLock.class")
     void setInputMethodLocked(String id, int subtypeId) {
         InputMethodInfo info = mMethodMap.get(id);
         if (info == null) {
-            throw new IllegalArgumentException("Unknown id: " + id);
+            throw getExceptionForUnknownImeId(id);
         }
 
         // See if we need to notify a subtype change within the same IME.
         if (id.equals(getSelectedMethodIdLocked())) {
+            final int userId = mSettings.getCurrentUserId();
             final int subtypeCount = info.getSubtypeCount();
             if (subtypeCount <= 0) {
+                notifyInputMethodSubtypeChangedLocked(userId, info, null);
                 return;
             }
             final InputMethodSubtype oldSubtype = mCurrentSubtype;
@@ -3289,6 +3251,7 @@
             if (newSubtype == null || oldSubtype == null) {
                 Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype
                         + ", new subtype = " + newSubtype);
+                notifyInputMethodSubtypeChangedLocked(userId, info, null);
                 return;
             }
             if (newSubtype != oldSubtype) {
@@ -3326,22 +3289,23 @@
     }
 
     @Override
-    public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
-            int lastClickTooType, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+    public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
+            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput");
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "showSoftInput")) {
+            if (!canInteractWithImeLocked(uid, client, "showSoftInput", statsToken)) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
                 return false;
             }
             final long ident = Binder.clearCallingIdentity();
             try {
                 if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
-                return showCurrentInputLocked(
-                        windowToken, flags, lastClickTooType, resultReceiver, reason);
+                return showCurrentInputLocked(windowToken, statsToken, flags, lastClickTooType,
+                        resultReceiver, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3358,7 +3322,8 @@
                     "InputMethodManagerService#startStylusHandwriting");
             int uid = Binder.getCallingUid();
             synchronized (ImfLock.class) {
-                if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting")) {
+                if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting",
+                        null /* statsToken */)) {
                     return;
                 }
                 if (!hasSupportedStylusLocked()) {
@@ -3413,19 +3378,33 @@
     }
 
     @GuardedBy("ImfLock.class")
-    boolean showCurrentInputLocked(IBinder windowToken, int flags,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
-        return showCurrentInputLocked(
-                windowToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
+    boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        return showCurrentInputLocked(windowToken, statsToken, flags,
+                MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean showCurrentInputLocked(IBinder windowToken, int flags, int lastClickToolType,
+    private boolean showCurrentInputLocked(IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        // Create statsToken is none exists.
+        if (statsToken == null) {
+            String packageName = null;
+            if (mCurEditorInfo != null) {
+                packageName = mCurEditorInfo.packageName;
+            }
+            statsToken = new ImeTracker.Token(packageName);
+            ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_SERVER_START_INPUT,
+                    reason);
+        }
+
         mShowRequested = true;
         if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) {
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
 
         if ((flags & InputMethodManager.SHOW_FORCED) != 0) {
             mShowExplicitlyRequested = true;
@@ -3435,8 +3414,10 @@
         }
 
         if (!mSystemReady) {
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
 
         mBindingController.setCurrentMethodVisible();
         final IInputMethodInvoker curMethod = getCurMethodLocked();
@@ -3444,6 +3425,9 @@
             // create a placeholder token for IMS so that IMS cannot inject windows into client app.
             Binder showInputToken = new Binder();
             mShowRequestWindowMap.put(showInputToken, windowToken);
+            ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
+            mCurStatsToken = null;
             final int showFlags = getImeShowFlagsLocked();
             if (DEBUG) {
                 Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -3455,23 +3439,34 @@
                 curMethod.updateEditorToolType(lastClickToolType);
             }
             // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
-            if (curMethod.showSoftInput(showInputToken, showFlags, resultReceiver)) {
+            if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
                 onShowHideSoftInputRequested(true /* show */, windowToken, reason);
             }
             mInputShown = true;
             return true;
+        } else {
+            ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+            mCurStatsToken = statsToken;
         }
         return false;
     }
 
     @Override
-    public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, int flags,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+    public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+            @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput");
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "hideSoftInput")) {
+            if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
+                if (mInputShown) {
+                    ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+                } else {
+                    ImeTracker.get().onCancelled(statsToken,
+                            ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+                }
                 return false;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -3479,7 +3474,7 @@
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
                 if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
                 return InputMethodManagerService.this.hideCurrentInputLocked(windowToken,
-                        flags, resultReceiver, reason);
+                        statsToken, flags, resultReceiver, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -3488,17 +3483,32 @@
     }
 
     @GuardedBy("ImfLock.class")
-    boolean hideCurrentInputLocked(IBinder windowToken, int flags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+    boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+        // Create statsToken is none exists.
+        if (statsToken == null) {
+            String packageName = null;
+            if (mCurEditorInfo != null) {
+                packageName = mCurEditorInfo.packageName;
+            }
+            statsToken = new ImeTracker.Token(packageName);
+            ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+        }
+
         if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
                 && (mShowExplicitlyRequested || mShowForced)) {
             if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
+
         if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
             if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+            ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
             return false;
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
 
         // There is a chance that IMM#hideSoftInput() is called in a transient state where
         // IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting
@@ -3509,8 +3519,8 @@
         // IMMS#InputShown indicates that the software keyboard is shown.
         // TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.
         IInputMethodInvoker curMethod = getCurMethodLocked();
-        final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown
-                || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+        final boolean shouldHideSoftInput = (curMethod != null)
+                && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
         boolean res;
         if (shouldHideSoftInput) {
             final Binder hideInputToken = new Binder();
@@ -3519,17 +3529,20 @@
             // delivered to the IME process as an IPC.  Hence the inconsistency between
             // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
             // the final state.
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
             if (DEBUG) {
                 Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
                         + ", " + resultReceiver + ") for reason: "
                         + InputMethodDebug.softInputDisplayReasonToString(reason));
             }
             // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
-            if (curMethod.hideSoftInput(hideInputToken, 0 /* flags */, resultReceiver)) {
+            if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */,
+                    resultReceiver)) {
                 onShowHideSoftInputRequested(false /* show */, windowToken, reason);
             }
             res = true;
         } else {
+            ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
             res = false;
         }
         mBindingController.setCurrentMethodNotVisible();
@@ -3537,6 +3550,9 @@
         mShowRequested = false;
         mShowExplicitlyRequested = false;
         mShowForced = false;
+        // Cancel existing statsToken for show IME as we got a hide request.
+        ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+        mCurStatsToken = null;
         return res;
     }
 
@@ -3694,8 +3710,8 @@
             Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
                     + " a background user, use EditorInfo.targetInputMethodUser with"
                     + " INTERACT_ACROSS_USERS_FULL permission.");
-            hideCurrentInputLocked(
-                    mCurFocusedWindow, 0, null, SoftInputShowHideReason.HIDE_INVALID_USER);
+            hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                    null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
             return InputBindResult.INVALID_USER;
         }
 
@@ -3751,7 +3767,7 @@
         boolean didStart = false;
 
         InputBindResult res = null;
-        // We shows the IME when the system allows the IME focused target window to restore the
+        // We show the IME when the system allows the IME focused target window to restore the
         // IME visibility (e.g. switching to the app task when last time the IME is visible).
         // Note that we don't restore IME visibility for some cases (e.g. when the soft input
         // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
@@ -3759,10 +3775,11 @@
         // UI for input.
         if (isTextEditor && editorInfo != null
                 && shouldRestoreImeVisibility(windowToken, softInputMode)) {
+            if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
             res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
                     editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
                     imeDispatcher);
-            showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+            showCurrentInputImplicitLocked(windowToken,
                     SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
             return res;
         }
@@ -3775,8 +3792,8 @@
                         // be behind any soft input window, so hide the
                         // soft input window if it is shown.
                         if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
-                        hideCurrentInputLocked(
-                                mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
 
                         // If focused display changed, we should unbind current method
@@ -3805,24 +3822,29 @@
                                 imeDispatcher);
                         didStart = true;
                     }
-                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                    showCurrentInputImplicitLocked(windowToken,
                             SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
                 }
                 break;
             case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+                if (DEBUG) {
+                    Slog.v(TAG, "Window asks to keep the input in whatever state it was last in");
+                }
                 // Do nothing.
                 break;
             case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
                 if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
                     if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
                 }
                 break;
             case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
                 if (!sameWindowFocused) {
                     if (DEBUG) Slog.v(TAG, "Window asks to hide input");
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
                 }
                 break;
@@ -3838,7 +3860,7 @@
                                     imeDispatcher);
                             didStart = true;
                         }
-                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(windowToken,
                                 SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
                     } else {
                         Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
@@ -3859,7 +3881,7 @@
                                     imeDispatcher);
                             didStart = true;
                         }
-                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
+                        showCurrentInputImplicitLocked(windowToken,
                                 SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
                     }
                 } else {
@@ -3880,7 +3902,9 @@
                     // To maintain compatibility, we are now hiding the IME when we don't have
                     // an editor upon refocusing a window.
                     if (startInputByWinGainedFocus) {
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
                     }
                 }
@@ -3893,7 +3917,9 @@
                     // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
                     // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
                     // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */,
                             SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
                 }
                 res = startInputUncheckedLocked(cs, inputContext,
@@ -3908,8 +3934,15 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean canInteractWithImeLocked(
-            int uid, IInputMethodClient client, String methodName) {
+    private void showCurrentInputImplicitLocked(@NonNull IBinder windowToken,
+            @SoftInputShowHideReason int reason) {
+        showCurrentInputLocked(windowToken, null /* statsToken */, InputMethodManager.SHOW_IMPLICIT,
+                null /* resultReceiver */, reason);
+    }
+
+    @GuardedBy("ImfLock.class")
+    private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
+            @Nullable ImeTracker.Token statsToken) {
         if (mCurClient == null || client == null
                 || mCurClient.mClient.asBinder() != client.asBinder()) {
             // We need to check if this is the current client with
@@ -3917,13 +3950,16 @@
             // be made before input is started in it.
             final ClientState cs = mClients.get(client.asBinder());
             if (cs == null) {
+                ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
                 throw new IllegalArgumentException("unknown client " + client.asBinder());
             }
+            ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
             if (!isImeClientFocused(mCurFocusedWindow, cs)) {
                 Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
                 return false;
             }
         }
+        ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
         return true;
     }
 
@@ -3980,10 +4016,11 @@
 
     @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
     @Override
-    public void showInputMethodPickerFromSystem(IInputMethodClient client, int auxiliarySubtypeMode,
-            int displayId) {
+    public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
         // Always call subtype picker, because subtype picker is a superset of input method
         // picker.
+        super.showInputMethodPickerFromSystem_enforcePermission();
+
         mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
                 .sendToTarget();
     }
@@ -3993,17 +4030,32 @@
      */
     @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
     public boolean isInputMethodPickerShownForTest() {
+        super.isInputMethodPickerShownForTest_enforcePermission();
+
         synchronized (ImfLock.class) {
             return mMenuController.isisInputMethodPickerShownForTestLocked();
         }
     }
 
+    @NonNull
+    private static IllegalArgumentException getExceptionForUnknownImeId(
+            @Nullable String imeId) {
+        return new IllegalArgumentException("Unknown id: " + imeId);
+    }
+
     @BinderThread
     private void setInputMethod(@NonNull IBinder token, String id) {
+        final int callingUid = Binder.getCallingUid();
+        final int userId = UserHandle.getUserId(callingUid);
         synchronized (ImfLock.class) {
             if (!calledWithValidTokenLocked(token)) {
                 return;
             }
+            final InputMethodInfo imi = mMethodMap.get(id);
+            if (imi == null || !canCallerAccessInputMethod(
+                    imi.getPackageName(), callingUid, userId, mSettings)) {
+                throw getExceptionForUnknownImeId(id);
+            }
             setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID);
         }
     }
@@ -4011,14 +4063,20 @@
     @BinderThread
     private void setInputMethodAndSubtype(@NonNull IBinder token, String id,
             InputMethodSubtype subtype) {
+        final int callingUid = Binder.getCallingUid();
+        final int userId = UserHandle.getUserId(callingUid);
         synchronized (ImfLock.class) {
             if (!calledWithValidTokenLocked(token)) {
                 return;
             }
+            final InputMethodInfo imi = mMethodMap.get(id);
+            if (imi == null || !canCallerAccessInputMethod(
+                    imi.getPackageName(), callingUid, userId, mSettings)) {
+                throw getExceptionForUnknownImeId(id);
+            }
             if (subtype != null) {
                 setInputMethodWithSubtypeIdLocked(token, id,
-                        SubtypeUtils.getSubtypeIdFromHashCode(mMethodMap.get(id),
-                                subtype.hashCode()));
+                        SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()));
             } else {
                 setInputMethod(token, id);
             }
@@ -4254,7 +4312,7 @@
             final int curTokenDisplayId;
             synchronized (ImfLock.class) {
                 if (!canInteractWithImeLocked(callingUid, client,
-                        "getInputMethodWindowVisibleHeight")) {
+                        "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
                     if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) {
                         EventLog.writeEvent(0x534e4554, "204906124", callingUid, "");
                         mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true);
@@ -4272,6 +4330,8 @@
     @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
     @Override
     public void removeImeSurface() {
+        super.removeImeSurface_enforcePermission();
+
         mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
     }
 
@@ -4468,12 +4528,15 @@
      * a stylus deviceId is not already registered on device.
      */
     @BinderThread
-    @EnforcePermission(Manifest.permission.INJECT_EVENTS)
+    @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
     @Override
     public void addVirtualStylusIdForTestSession(IInputMethodClient client) {
+        super.addVirtualStylusIdForTestSession_enforcePermission();
+
         int uid = Binder.getCallingUid();
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession")) {
+            if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession",
+                    null /* statsToken */)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -4496,9 +4559,12 @@
     @Override
     public void setStylusWindowIdleTimeoutForTest(
             IInputMethodClient client, @DurationMillisLong long timeout) {
+        super.setStylusWindowIdleTimeoutForTest_enforcePermission();
+
         int uid = Binder.getCallingUid();
         synchronized (ImfLock.class) {
-            if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest")) {
+            if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest",
+                    null /* statsToken */)) {
                 return;
             }
             final long ident = Binder.clearCallingIdentity();
@@ -4593,6 +4659,8 @@
     @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING)
     @Override
     public void startImeTrace() {
+        super.startImeTrace_enforcePermission();
+
         ImeTracing.getInstance().startTrace(null /* printwriter */);
         ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
@@ -4609,6 +4677,8 @@
     @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING)
     @Override
     public void stopImeTrace() {
+        super.stopImeTrace_enforcePermission();
+
         ImeTracing.getInstance().stopTrace(null /* printwriter */);
         ArrayMap<IBinder, ClientState> clients;
         synchronized (ImfLock.class) {
@@ -4679,7 +4749,8 @@
     }
 
     @BinderThread
-    private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible) {
+    private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
+            @Nullable ImeTracker.Token statsToken) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
         synchronized (ImfLock.class) {
             if (!calledWithValidTokenLocked(token)) {
@@ -4687,19 +4758,22 @@
             }
             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.
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+
                     mWindowManagerInternal.hideIme(
                             mHideRequestWindowMap.get(windowToken),
-                            mCurClient.mSelfReportedDisplayId);
+                            mCurClient.mSelfReportedDisplayId, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken,
+                            ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
                 }
             } else {
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
                 // Send to window manager to show IME after IME layout finishes.
-                mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken));
+                mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken),
+                        statsToken);
             }
         }
         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -4766,7 +4840,8 @@
             }
             final long ident = Binder.clearCallingIdentity();
             try {
-                hideCurrentInputLocked(mLastImeTargetWindow, flags, null, reason);
+                hideCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+                        null /* resultReceiver */, reason);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -4783,7 +4858,8 @@
             }
             final long ident = Binder.clearCallingIdentity();
             try {
-                showCurrentInputLocked(mLastImeTargetWindow, flags, null,
+                showCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+                        null /* resultReceiver */,
                         SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME);
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -4874,7 +4950,8 @@
             case MSG_HIDE_CURRENT_INPUT_METHOD:
                 synchronized (ImfLock.class) {
                     final @SoftInputShowHideReason int reason = (int) msg.obj;
-                    hideCurrentInputLocked(mCurFocusedWindow, 0, null, reason);
+                    hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+                            null /* resultReceiver */, reason);
 
                 }
                 return true;
@@ -5326,6 +5403,7 @@
                 mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
             }
         }
+        notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype);
 
         if (!setSubtypeOnly) {
             // Set InputMethod here
@@ -6361,7 +6439,8 @@
                     final String nextIme;
                     final List<InputMethodInfo> nextEnabledImes;
                     if (userId == mSettings.getCurrentUserId()) {
-                        hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                        hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+                                0 /* flags */, null /* resultReceiver */,
                                 SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
                         mBindingController.unbindCurrentMethod();
                         // Reset the current IME
@@ -6626,8 +6705,9 @@
 
         @BinderThread
         @Override
-        public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible) {
-            mImms.applyImeVisibility(mToken, windowToken, setVisible);
+        public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
+                @Nullable ImeTracker.Token statsToken) {
+            mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
         }
 
         @BinderThread
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c7ff8ca..ebf9237d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -179,16 +179,6 @@
         }
     }
 
-    static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
-            InputMethodSubtype subtype) {
-        final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
-        return subtype != null
-                ? TextUtils.concat(subtype.getDisplayName(context,
-                        imi.getPackageName(), imi.getServiceInfo().applicationInfo),
-                                (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
-                : imiLabel;
-    }
-
     /**
      * Returns true if a package name belongs to a UID.
      *
diff --git a/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
index ab91290..e831e40 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
@@ -17,9 +17,9 @@
 package com.android.server.integrity.parser;
 
 import android.annotation.Nullable;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.integrity.model.RuleMetadata;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
index 7aed352..022b4b8 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
@@ -19,9 +19,9 @@
 import static com.android.server.integrity.parser.RuleMetadataParser.RULE_PROVIDER_TAG;
 import static com.android.server.integrity.parser.RuleMetadataParser.VERSION_TAG;
 
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.integrity.model.RuleMetadata;
 
 import org.xmlpull.v1.XmlSerializer;
diff --git a/services/core/java/com/android/server/locales/AppUpdateTracker.java b/services/core/java/com/android/server/locales/AppUpdateTracker.java
new file mode 100644
index 0000000..3474f1e
--- /dev/null
+++ b/services/core/java/com/android/server/locales/AppUpdateTracker.java
@@ -0,0 +1,180 @@
+/*
+ * 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.locales;
+
+import android.app.LocaleConfig;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.FeatureFlagUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Track when a app is being updated.
+ */
+public class AppUpdateTracker {
+    private static final String TAG = "AppUpdateTracker";
+
+    private final Context mContext;
+    private final LocaleManagerService mLocaleManagerService;
+    private final LocaleManagerBackupHelper mBackupHelper;
+
+    AppUpdateTracker(Context context, LocaleManagerService localeManagerService,
+            LocaleManagerBackupHelper backupHelper) {
+        mContext = context;
+        mLocaleManagerService = localeManagerService;
+        mBackupHelper = backupHelper;
+    }
+
+    /**
+     * <p><b>Note:</b> This is invoked by service's common monitor
+     * {@link LocaleManagerServicePackageMonitor#onPackageUpdateFinished} when a package is upgraded
+     * on device.
+     */
+    public void onPackageUpdateFinished(String packageName, int uid) {
+        Log.d(TAG, "onPackageUpdateFinished " + packageName);
+        int userId = UserHandle.getUserId(uid);
+        cleanApplicationLocalesIfNeeded(packageName, userId);
+    }
+
+    /**
+     * When the user has set per-app locales for a specific application from a delegate selector,
+     * and then the LocaleConfig of that application is removed in the upgraded version, the per-app
+     * locales needs to be reset to system default locales to avoid the user being unable to change
+     * system locales setting.
+     */
+    private void cleanApplicationLocalesIfNeeded(String packageName, int userId) {
+        Set<String> packageNames = new ArraySet<>();
+        SharedPreferences delegateAppLocalePackages = mBackupHelper.getPersistedInfo();
+        if (delegateAppLocalePackages != null) {
+            packageNames = delegateAppLocalePackages.getStringSet(Integer.toString(userId),
+                    new ArraySet<>());
+        }
+
+        try {
+            LocaleList appLocales = mLocaleManagerService.getApplicationLocales(packageName,
+                    userId);
+            if (appLocales.isEmpty() || isLocalesExistedInLocaleConfig(appLocales, packageName,
+                    userId) || !packageNames.contains(packageName)) {
+                return;
+            }
+        } catch (RemoteException | IllegalArgumentException e) {
+            Slog.e(TAG, "Exception when getting locales for " + packageName, e);
+            return;
+        }
+
+        Slog.d(TAG, "Clear app locales for " + packageName);
+        try {
+            mLocaleManagerService.setApplicationLocales(packageName, userId,
+                    LocaleList.forLanguageTags(""), false);
+        } catch (RemoteException | IllegalArgumentException e) {
+            Slog.e(TAG, "Could not clear locales for " + packageName, e);
+        }
+    }
+
+    /**
+     * Check whether the LocaleConfig is existed and the per-app locales is presented in the
+     * LocaleConfig file after the application is upgraded.
+     */
+    private boolean isLocalesExistedInLocaleConfig(LocaleList appLocales, String packageName,
+            int userId) {
+        LocaleList packageLocalesList = getPackageLocales(packageName, userId);
+        HashSet<Locale> packageLocales = new HashSet<>();
+
+        if (isSettingsAppLocalesOptIn()) {
+            if (packageLocalesList == null || packageLocalesList.isEmpty()) {
+                // The app locale feature is not enabled by the app
+                Slog.d(TAG, "opt-in: the app locale feature is not enabled");
+                return false;
+            }
+        } else {
+            if (packageLocalesList != null && packageLocalesList.isEmpty()) {
+                // The app locale feature is not enabled by the app
+                Slog.d(TAG, "opt-out: the app locale feature is not enabled");
+                return false;
+            }
+        }
+
+        if (packageLocalesList != null && !packageLocalesList.isEmpty()) {
+            // The app has added the supported locales into the LocaleConfig
+            for (int i = 0; i < packageLocalesList.size(); i++) {
+                packageLocales.add(packageLocalesList.get(i));
+            }
+            if (!matchesLocale(packageLocales, appLocales)) {
+                // The set app locales do not match with the list of app supported locales
+                Slog.d(TAG, "App locales: " + appLocales.toLanguageTags()
+                        + " are not existed in the supported locale list");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Get locales from LocaleConfig.
+     */
+    @VisibleForTesting
+    public LocaleList getPackageLocales(String packageName, int userId) {
+        try {
+            LocaleConfig localeConfig = new LocaleConfig(
+                    mContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId)));
+            if (localeConfig.getStatus() == LocaleConfig.STATUS_SUCCESS) {
+                return localeConfig.getSupportedLocales();
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Can not found the package name : " + packageName + " / " + e);
+        }
+        return null;
+    }
+
+    /**
+     * Check whether the feature to show per-app locales list in Settings is enabled.
+     */
+    @VisibleForTesting
+    public boolean isSettingsAppLocalesOptIn() {
+        return FeatureFlagUtils.isEnabled(mContext,
+                FeatureFlagUtils.SETTINGS_APP_LOCALE_OPT_IN_ENABLED);
+    }
+
+    private boolean matchesLocale(HashSet<Locale> supported, LocaleList appLocales) {
+        if (supported.size() <= 0 || appLocales.size() <= 0) {
+            return true;
+        }
+
+        for (int i = 0; i < appLocales.size(); i++) {
+            final Locale appLocale = appLocales.get(i);
+            if (supported.stream().anyMatch(
+                    locale -> LocaleList.matchesLanguageAndScript(locale, appLocale))) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 37a4869..898c6f1 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -27,35 +27,40 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.os.Environment;
 import android.os.HandlerThread;
 import android.os.LocaleList;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
-import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.Duration;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Set;
 
 /**
  * Helper class for managing backup and restore of app-specific locales.
@@ -68,9 +73,14 @@
     private static final String PACKAGE_XML_TAG = "package";
     private static final String ATTR_PACKAGE_NAME = "name";
     private static final String ATTR_LOCALES = "locales";
-    private static final String ATTR_CREATION_TIME_MILLIS = "creationTimeMillis";
+    private static final String ATTR_DELEGATE_SELECTOR = "delegate_selector";
 
     private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
+    /**
+     * The name of the xml file used to persist the target package name that sets per-app locales
+     * from the delegate selector.
+     */
+    private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml";
     // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
     // retention period OR next reboot, whichever happens earlier.
     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
@@ -85,23 +95,28 @@
     // SparseArray because it is more memory-efficient than a HashMap.
     private final SparseArray<StagedData> mStagedData;
 
+    // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to
+    // the application setting the app-locale itself.
+    private final SharedPreferences mDelegateAppLocalePackages;
     private final BroadcastReceiver mUserMonitor;
 
     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
         this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
-                new SparseArray<>(), broadcastHandlerThread);
+                new SparseArray<>(), broadcastHandlerThread, null);
     }
 
     @VisibleForTesting LocaleManagerBackupHelper(Context context,
             LocaleManagerService localeManagerService,
             PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
-            HandlerThread broadcastHandlerThread) {
+            HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
         mContext = context;
         mLocaleManagerService = localeManagerService;
         mPackageManager = packageManager;
         mClock = clock;
         mStagedData = stagedData;
+        mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
+                : createPersistedInfo();
 
         mUserMonitor = new UserMonitor();
         IntentFilter filter = new IntentFilter();
@@ -127,20 +142,29 @@
             cleanStagedDataForOldEntriesLocked();
         }
 
-        HashMap<String, String> pkgStates = new HashMap<>();
+        HashMap<String, LocalesInfo> pkgStates = new HashMap<>();
         for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
                 PackageManager.ApplicationInfoFlags.of(0), userId)) {
             try {
                 LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
                         appInfo.packageName,
                         userId);
-                // Backup locales only for apps which do have app-specific overrides.
+                // Backup locales and package names for per-app locales set from a delegate
+                // selector only for apps which do have app-specific overrides.
                 if (!appLocales.isEmpty()) {
                     if (DEBUG) {
                         Slog.d(TAG, "Add package=" + appInfo.packageName + " locales="
                                 + appLocales.toLanguageTags() + " to backup payload");
                     }
-                    pkgStates.put(appInfo.packageName, appLocales.toLanguageTags());
+                    boolean localeSetFromDelegate = false;
+                    if (mDelegateAppLocalePackages != null) {
+                        localeSetFromDelegate = mDelegateAppLocalePackages.getStringSet(
+                                Integer.toString(userId), Collections.<String>emptySet()).contains(
+                                appInfo.packageName);
+                    }
+                    LocalesInfo localesInfo = new LocalesInfo(appLocales.toLanguageTags(),
+                            localeSetFromDelegate);
+                    pkgStates.put(appInfo.packageName, localesInfo);
                 }
             } catch (RemoteException | IllegalArgumentException e) {
                 Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName,
@@ -200,7 +224,7 @@
 
         final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);
 
-        HashMap<String, String> pkgStates;
+        HashMap<String, LocalesInfo> pkgStates;
         try {
             // Parse the input blob into a list of BackupPackageState.
             final TypedXmlPullParser parser = Xml.newFastPullParser();
@@ -222,16 +246,17 @@
             StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());
 
             for (String pkgName : pkgStates.keySet()) {
-                String languageTags = pkgStates.get(pkgName);
+                LocalesInfo localesInfo = pkgStates.get(pkgName);
                 // Check if the application is already installed for the concerned user.
                 if (isPackageInstalledForUser(pkgName, userId)) {
                     // Don't apply the restore if the locales have already been set for the app.
-                    checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
+                    checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
                 } else {
                     // Stage the data if the app isn't installed.
-                    stagedData.mPackageStates.put(pkgName, languageTags);
+                    stagedData.mPackageStates.put(pkgName, localesInfo);
                     if (DEBUG) {
-                        Slog.d(TAG, "Add locales=" + languageTags
+                        Slog.d(TAG, "Add locales=" + localesInfo.mLocales
+                                + " fromDelegate=" + localesInfo.mSetFromDelegate
                                 + " package=" + pkgName + " for lazy restore.");
                     }
                 }
@@ -276,9 +301,11 @@
      * {@link LocaleManagerServicePackageMonitor#onPackageDataCleared} when a package's data
      * is cleared.
      */
-    void onPackageDataCleared() {
+    void onPackageDataCleared(String packageName, int uid) {
         try {
             notifyBackupManager();
+            int userId = UserHandle.getUserId(uid);
+            removePackageFromPersistedInfo(packageName, userId);
         } catch (Exception e) {
             Slog.e(TAG, "Exception in onPackageDataCleared.", e);
         }
@@ -289,9 +316,11 @@
      * {@link LocaleManagerServicePackageMonitor#onPackageRemoved} when a package is removed
      * from device.
      */
-    void onPackageRemoved() {
+    void onPackageRemoved(String packageName, int uid) {
         try {
             notifyBackupManager();
+            int userId = UserHandle.getUserId(uid);
+            removePackageFromPersistedInfo(packageName, userId);
         } catch (Exception e) {
             Slog.e(TAG, "Exception in onPackageRemoved.", e);
         }
@@ -317,7 +346,12 @@
      * case, we want to keep the user settings and discard the restore.
      */
     private void checkExistingLocalesAndApplyRestore(@NonNull String pkgName,
-            @NonNull String languageTags, int userId) {
+            LocalesInfo localesInfo, int userId) {
+        if (localesInfo == null) {
+            Slog.w(TAG, "No locales info for " + pkgName);
+            return;
+        }
+
         try {
             LocaleList currLocales = mLocaleManagerService.getApplicationLocales(
                     pkgName,
@@ -325,16 +359,17 @@
             if (!currLocales.isEmpty()) {
                 return;
             }
-        } catch (RemoteException e) {
+        } catch (RemoteException | IllegalArgumentException e) {
             Slog.e(TAG, "Could not check for current locales before restoring", e);
         }
 
         // Restore the locale immediately
         try {
             mLocaleManagerService.setApplicationLocales(pkgName, userId,
-                    LocaleList.forLanguageTags(languageTags));
+                    LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate);
             if (DEBUG) {
-                Slog.d(TAG, "Restored locales=" + languageTags + " for package=" + pkgName);
+                Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate="
+                        + localesInfo.mSetFromDelegate + " for package=" + pkgName);
             }
         } catch (RemoteException | IllegalArgumentException e) {
             Slog.e(TAG, "Could not restore locales for " + pkgName, e);
@@ -348,18 +383,21 @@
     /**
      * Parses the backup data from the serialized xml input stream.
      */
-    private @NonNull HashMap<String, String> readFromXml(XmlPullParser parser)
+    private @NonNull HashMap<String, LocalesInfo> readFromXml(TypedXmlPullParser parser)
             throws IOException, XmlPullParserException {
-        HashMap<String, String> packageStates = new HashMap<>();
+        HashMap<String, LocalesInfo> packageStates = new HashMap<>();
         int depth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, depth)) {
             if (parser.getName().equals(PACKAGE_XML_TAG)) {
                 String packageName = parser.getAttributeValue(/* namespace= */ null,
                         ATTR_PACKAGE_NAME);
                 String languageTags = parser.getAttributeValue(/* namespace= */ null, ATTR_LOCALES);
+                boolean delegateSelector = parser.getAttributeBoolean(/* namespace= */ null,
+                        ATTR_DELEGATE_SELECTOR);
 
                 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(languageTags)) {
-                    packageStates.put(packageName, languageTags);
+                    LocalesInfo localesInfo = new LocalesInfo(languageTags, delegateSelector);
+                    packageStates.put(packageName, localesInfo);
                 }
             }
         }
@@ -369,8 +407,8 @@
     /**
      * Converts the list of app backup data into a serialized xml stream.
      */
-    private static void writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)
-            throws IOException {
+    private static void writeToXml(OutputStream stream,
+            @NonNull HashMap<String, LocalesInfo> pkgStates) throws IOException {
         if (pkgStates.isEmpty()) {
             // No need to write anything at all if pkgStates is empty.
             return;
@@ -384,7 +422,9 @@
         for (String pkg : pkgStates.keySet()) {
             out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
             out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
-            out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg));
+            out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg).mLocales);
+            out.attributeBoolean(/* namespace= */ null, ATTR_DELEGATE_SELECTOR,
+                    pkgStates.get(pkg).mSetFromDelegate);
             out.endTag(/*namespace= */ null, PACKAGE_XML_TAG);
         }
 
@@ -394,14 +434,24 @@
 
     static class StagedData {
         final long mCreationTimeMillis;
-        final HashMap<String, String> mPackageStates;
+        final HashMap<String, LocalesInfo> mPackageStates;
 
-        StagedData(long creationTimeMillis, HashMap<String, String> pkgStates) {
+        StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates) {
             mCreationTimeMillis = creationTimeMillis;
             mPackageStates = pkgStates;
         }
     }
 
+    static class LocalesInfo {
+        final String mLocales;
+        final boolean mSetFromDelegate;
+
+        LocalesInfo(String locales, boolean setFromDelegate) {
+            mLocales = locales;
+            mSetFromDelegate = setFromDelegate;
+        }
+    }
+
     /**
      * Broadcast listener to capture user removed event.
      *
@@ -416,6 +466,7 @@
                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
                     synchronized (mStagedDataLock) {
                         deleteStagedDataLocked(userId);
+                        removeProfileFromPersistedInfo(userId);
                     }
                 }
             } catch (Exception e) {
@@ -443,11 +494,11 @@
 
         StagedData stagedData = mStagedData.get(userId);
         for (String pkgName : stagedData.mPackageStates.keySet()) {
-            String languageTags = stagedData.mPackageStates.get(pkgName);
+            LocalesInfo localesInfo = stagedData.mPackageStates.get(pkgName);
 
             if (pkgName.equals(packageName)) {
 
-                checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
+                checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
 
                 // Remove the restored entry from the staged data list.
                 stagedData.mPackageStates.remove(pkgName);
@@ -463,4 +514,98 @@
             }
         }
     }
+
+    SharedPreferences createPersistedInfo() {
+        final File prefsFile = new File(
+                Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
+                LOCALES_FROM_DELEGATE_PREFS);
+        return mContext.createDeviceProtectedStorageContext().getSharedPreferences(prefsFile,
+                Context.MODE_PRIVATE);
+    }
+
+    public SharedPreferences getPersistedInfo() {
+        return mDelegateAppLocalePackages;
+    }
+
+    private void removePackageFromPersistedInfo(String packageName, @UserIdInt int userId) {
+        if (mDelegateAppLocalePackages == null) {
+            Slog.w(TAG, "Failed to persist data into the shared preference!");
+            return;
+        }
+
+        String key = Integer.toString(userId);
+        Set<String> packageNames = new ArraySet<>(
+                mDelegateAppLocalePackages.getStringSet(key, new ArraySet<>()));
+        if (packageNames.contains(packageName)) {
+            if (DEBUG) {
+                Slog.d(TAG, "remove " + packageName + " from persisted info");
+            }
+            packageNames.remove(packageName);
+            SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
+            editor.putStringSet(key, packageNames);
+
+            // commit and log the result.
+            if (!editor.commit()) {
+                Slog.e(TAG, "Failed to commit data!");
+            }
+        }
+    }
+
+    private void removeProfileFromPersistedInfo(@UserIdInt int userId) {
+        String key = Integer.toString(userId);
+
+        if (mDelegateAppLocalePackages == null || !mDelegateAppLocalePackages.contains(key)) {
+            Slog.w(TAG, "The profile is not existed in the persisted info");
+            return;
+        }
+
+        if (!mDelegateAppLocalePackages.edit().remove(key).commit()) {
+            Slog.e(TAG, "Failed to commit data!");
+        }
+    }
+
+    /**
+     * Persists the package name of per-app locales set from a delegate selector.
+     *
+     * <p>This information is used when the user has set per-app locales for a specific application
+     * from the delegate selector, and then the LocaleConfig of that application is removed in the
+     * upgraded version, the per-app locales needs to be reset to system default locales to avoid
+     * the user being unable to change system locales setting.
+     */
+    void persistLocalesModificationInfo(@UserIdInt int userId, String packageName,
+            boolean fromDelegate, boolean emptyLocales) {
+        if (mDelegateAppLocalePackages == null) {
+            Slog.w(TAG, "Failed to persist data into the shared preference!");
+            return;
+        }
+
+        SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
+        String user = Integer.toString(userId);
+        Set<String> packageNames = new ArraySet<>(
+                mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
+        if (fromDelegate && !emptyLocales) {
+            if (!packageNames.contains(packageName)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "persist package: " + packageName);
+                }
+                packageNames.add(packageName);
+                editor.putStringSet(user, packageNames);
+            }
+        } else {
+            // Remove the package name if per-app locales was not set from the delegate selector
+            // or they were set to empty.
+            if (packageNames.contains(packageName)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "remove package: " + packageName);
+                }
+                packageNames.remove(packageName);
+                editor.putStringSet(user, packageNames);
+            }
+        }
+
+        // commit and log the result.
+        if (!editor.commit()) {
+            Slog.e(TAG, "failed to commit locale setter info");
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index fc7be7f..39b9f1f 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -25,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ILocaleManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -38,6 +39,8 @@
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
 import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -91,9 +94,11 @@
 
         mBackupHelper = new LocaleManagerBackupHelper(this,
                 mPackageManager, broadcastHandlerThread);
+        AppUpdateTracker appUpdateTracker =
+                new AppUpdateTracker(mContext, this, mBackupHelper);
 
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-                systemAppUpdateTracker);
+                systemAppUpdateTracker, appUpdateTracker);
         mPackageMonitor.register(context, broadcastHandlerThread.getLooper(),
                 UserHandle.ALL,
                 true);
@@ -144,8 +149,9 @@
     private final class LocaleManagerBinderService extends ILocaleManager.Stub {
         @Override
         public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
-                @NonNull LocaleList locales) throws RemoteException {
-            LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales);
+                @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException {
+            LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales,
+                    fromDelegate);
         }
 
         @Override
@@ -175,7 +181,8 @@
      * Sets the current UI locales for a specified app.
      */
     public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
-            @NonNull LocaleList locales) throws RemoteException, IllegalArgumentException {
+            @NonNull LocaleList locales, boolean fromDelegate)
+            throws RemoteException, IllegalArgumentException {
         AppLocaleChangedAtomRecord atomRecordForMetrics = new
                 AppLocaleChangedAtomRecord(Binder.getCallingUid());
         try {
@@ -200,6 +207,8 @@
                 enforceChangeConfigurationPermission(atomRecordForMetrics);
             }
 
+            mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate,
+                    locales.isEmpty());
             final long token = Binder.clearCallingIdentity();
             try {
                 setApplicationLocalesUnchecked(appPackageName, userId, locales,
@@ -357,17 +366,23 @@
                 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
                 "getApplicationLocales", /* callerPackage= */ null);
 
-        // This function handles three types of query operations:
+        // This function handles four types of query operations:
         // 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.) 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. If neither conditions matched,
-        // check if the caller has the necessary permission and fetch locales.
+        // 2.) The installer of the given app querying locales of a package installed by said
+        // installer.
+        // 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, 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)) {
+                && !isCallerInstaller(appPackageName, userId)
+                && !(isCallerFromCurrentInputMethod(userId)
+                    && mActivityManagerInternal.isAppForeground(
+                            getPackageUid(appPackageName, userId)))) {
             enforceReadAppSpecificLocalesPermission();
         }
         final long token = Binder.clearCallingIdentity();
@@ -412,6 +427,26 @@
         return false;
     }
 
+    /**
+     * Checks if the calling app is the current input method.
+     */
+    private boolean isCallerFromCurrentInputMethod(int userId) {
+        String currentInputMethod = Settings.Secure.getStringForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.DEFAULT_INPUT_METHOD,
+                userId);
+        if (!TextUtils.isEmpty(currentInputMethod)) {
+            String inputMethodPkgName = ComponentName
+                    .unflattenFromString(currentInputMethod)
+                    .getPackageName();
+            int inputMethodUid = getPackageUid(inputMethodPkgName, userId);
+            return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(),
+                    inputMethodUid);
+        }
+
+        return false;
+    }
+
     private void enforceReadAppSpecificLocalesPermission() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index 32080ef..1a38f0c 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -34,11 +34,13 @@
 final class LocaleManagerServicePackageMonitor extends PackageMonitor {
     private LocaleManagerBackupHelper mBackupHelper;
     private SystemAppUpdateTracker mSystemAppUpdateTracker;
+    private AppUpdateTracker mAppUpdateTracker;
 
     LocaleManagerServicePackageMonitor(LocaleManagerBackupHelper localeManagerBackupHelper,
-            SystemAppUpdateTracker systemAppUpdateTracker) {
+            SystemAppUpdateTracker systemAppUpdateTracker, AppUpdateTracker appUpdateTracker) {
         mBackupHelper = localeManagerBackupHelper;
         mSystemAppUpdateTracker = systemAppUpdateTracker;
+        mAppUpdateTracker = appUpdateTracker;
     }
 
     @Override
@@ -48,16 +50,17 @@
 
     @Override
     public void onPackageDataCleared(String packageName, int uid) {
-        mBackupHelper.onPackageDataCleared();
+        mBackupHelper.onPackageDataCleared(packageName, uid);
     }
 
     @Override
     public void onPackageRemoved(String packageName, int uid) {
-        mBackupHelper.onPackageRemoved();
+        mBackupHelper.onPackageRemoved(packageName, uid);
     }
 
     @Override
     public void onPackageUpdateFinished(String packageName, int uid) {
+        mAppUpdateTracker.onPackageUpdateFinished(packageName, uid);
         mSystemAppUpdateTracker.onPackageUpdateFinished(packageName, uid);
     }
 }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
index 803b5a3..c5069e5 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
@@ -56,7 +56,8 @@
         pw.println("Locale manager (locale) shell commands:");
         pw.println("  help");
         pw.println("      Print this help text.");
-        pw.println("  set-app-locales <PACKAGE_NAME> [--user <USER_ID>] [--locales <LOCALE_INFO>]");
+        pw.println("  set-app-locales <PACKAGE_NAME> [--user <USER_ID>] [--locales <LOCALE_INFO>]"
+                + "[--delegate <FROM_DELEGATE>]");
         pw.println("      Set the locales for the specified app.");
         pw.println("      --user <USER_ID>: apply for the given user, "
                 + "the current user is used when unspecified.");
@@ -64,6 +65,8 @@
                 + "as a single String separated by commas");
         pw.println("                 Empty locale list is used when unspecified.");
         pw.println("                 eg. en,en-US,hi ");
+        pw.println("      --delegate <FROM_DELEGATE>: The locales are set from a delegate, "
+                + "the value could be true or false. false is the default when unspecified.");
         pw.println("  get-app-locales <PACKAGE_NAME> [--user <USER_ID>]");
         pw.println("      Get the locales for the specified app.");
         pw.println("      --user <USER_ID>: get for the given user, "
@@ -77,6 +80,7 @@
         if (packageName != null) {
             int userId = ActivityManager.getCurrentUser();
             LocaleList locales = LocaleList.getEmptyLocaleList();
+            boolean fromDelegate = false;
             do {
                 String option = getNextOption();
                 if (option == null) {
@@ -91,6 +95,10 @@
                         locales = parseLocales();
                         break;
                     }
+                    case "--delegate": {
+                        fromDelegate = parseFromDelegate();
+                        break;
+                    }
                     default: {
                         throw new IllegalArgumentException("Unknown option: " + option);
                     }
@@ -98,7 +106,7 @@
             } while (true);
 
             try {
-                mBinderService.setApplicationLocales(packageName, userId, locales);
+                mBinderService.setApplicationLocales(packageName, userId, locales, fromDelegate);
             } catch (RemoteException e) {
                 getOutPrintWriter().println("Remote Exception: " + e);
             } catch (IllegalArgumentException e) {
@@ -148,12 +156,26 @@
     }
 
     private LocaleList parseLocales() {
-        if (getRemainingArgsCount() <= 0) {
+        String locales = getNextArg();
+        if (locales == null) {
             return LocaleList.getEmptyLocaleList();
+        } else {
+            if (locales.startsWith("-")) {
+                throw new IllegalArgumentException("Unknown locales: " + locales);
+            }
+            return LocaleList.forLanguageTags(locales);
         }
-        String[] args = peekRemainingArgs();
-        String inputLocales = args[0];
-        LocaleList locales = LocaleList.forLanguageTags(inputLocales);
-        return locales;
+    }
+
+    private boolean parseFromDelegate() {
+        String result = getNextArg();
+        if (result == null) {
+            return false;
+        } else {
+            if (result.startsWith("-")) {
+                throw new IllegalArgumentException("Unknown source: " + result);
+            }
+            return Boolean.parseBoolean(result);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
index d13b1f4..215c653 100644
--- a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
+++ b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
@@ -28,12 +28,12 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 9bd48f2..2669d21 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -140,7 +140,9 @@
 import com.android.server.location.provider.proxy.ProxyLocationProvider;
 import com.android.server.location.settings.LocationSettings;
 import com.android.server.location.settings.LocationUserSettings;
+import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -308,6 +310,10 @@
         permissionManagerInternal.setLocationExtraPackagesProvider(
                 userId -> mContext.getResources().getStringArray(
                         com.android.internal.R.array.config_locationExtraPackageNames));
+
+        // TODO(b/241604546): properly handle this callback
+        LocalServices.getService(UserManagerInternal.class).addUserVisibilityListener(
+                (u, v) -> Slogf.i(TAG, "onUserVisibilityChanged(): %d -> %b", u, v));
     }
 
     @Nullable
@@ -950,6 +956,8 @@
     @Override
     public void injectLocation(Location location) {
 
+        super.injectLocation_enforcePermission();
+
         Preconditions.checkArgument(location.isComplete());
 
         int userId = UserHandle.getCallingUserId();
@@ -1160,6 +1168,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public void setExtraLocationControllerPackage(String packageName) {
+        super.setExtraLocationControllerPackage_enforcePermission();
+
         synchronized (mLock) {
             mExtraLocationControllerPackage = packageName;
         }
@@ -1175,6 +1185,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.LOCATION_HARDWARE)
     @Override
     public void setExtraLocationControllerPackageEnabled(boolean enabled) {
+        super.setExtraLocationControllerPackageEnabled_enforcePermission();
+
         synchronized (mLock) {
             mExtraLocationControllerPackageEnabled = enabled;
         }
@@ -1234,6 +1246,8 @@
     @RequiresPermission(android.Manifest.permission.CONTROL_AUTOMOTIVE_GNSS)
     public void setAutomotiveGnssSuspended(boolean suspended) {
 
+        super.setAutomotiveGnssSuspended_enforcePermission();
+
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
             throw new IllegalStateException(
                     "setAutomotiveGnssSuspended only allowed on automotive devices");
@@ -1247,6 +1261,8 @@
     @RequiresPermission(android.Manifest.permission.CONTROL_AUTOMOTIVE_GNSS)
     public boolean isAutomotiveGnssSuspended() {
 
+        super.isAutomotiveGnssSuspended_enforcePermission();
+
         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
             throw new IllegalStateException(
                     "isAutomotiveGnssSuspended only allowed on automotive devices");
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 7ce1017..90245b5e 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -122,9 +122,9 @@
 
     private final Context mContext;
 
-    private final Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
-    private final List<String> mSupportedContextHubPerms;
-    private final List<ContextHubInfo> mContextHubInfoList;
+    private Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
+    private List<String> mSupportedContextHubPerms;
+    private List<ContextHubInfo> mContextHubInfoList;
     private final RemoteCallbackList<IContextHubCallback> mCallbacksList =
             new RemoteCallbackList<>();
 
@@ -132,13 +132,13 @@
     private final IContextHubWrapper mContextHubWrapper;
 
     // The manager for transaction queue
-    private final ContextHubTransactionManager mTransactionManager;
+    private ContextHubTransactionManager mTransactionManager;
 
     // The manager for sending messages to/from clients
-    private final ContextHubClientManager mClientManager;
+    private ContextHubClientManager mClientManager;
 
     // The default client for old API clients
-    private final Map<Integer, IContextHubClient> mDefaultClientMap;
+    private Map<Integer, IContextHubClient> mDefaultClientMap;
 
     // The manager for the internal nanoapp state cache
     private final NanoAppStateManager mNanoAppStateManager = new NanoAppStateManager();
@@ -167,7 +167,7 @@
     // Lock object for sendWifiSettingUpdate()
     private final Object mSendWifiSettingUpdateLock = new Object();
 
-    private final SensorPrivacyManagerInternal mSensorPrivacyManagerInternal;
+    private SensorPrivacyManagerInternal mSensorPrivacyManagerInternal;
 
     private final Map<Integer, AtomicLong> mLastRestartTimestampMap = new HashMap<>();
 
@@ -207,158 +207,35 @@
             handleClientMessageCallback(mContextHubId, hostEndpointId, message, nanoappPermissions,
                     messagePermissions);
         }
+
+        @Override
+        public void handleServiceRestart() {
+            Log.i(TAG, "Starting Context Hub Service restart");
+            initExistingCallbacks();
+            resetSettings();
+            Log.i(TAG, "Finished Context Hub Service restart");
+        }
     }
 
-    public ContextHubService(Context context) {
-        long startTimeNs = SystemClock.elapsedRealtimeNanos();
+    public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
+        Log.i(TAG, "Starting Context Hub Service init");
         mContext = context;
-
-        mContextHubWrapper = getContextHubWrapper();
-        if (mContextHubWrapper == null) {
-            mTransactionManager = null;
-            mClientManager = null;
-            mSensorPrivacyManagerInternal = null;
-            mDefaultClientMap = Collections.emptyMap();
-            mContextHubIdToInfoMap = Collections.emptyMap();
-            mSupportedContextHubPerms = Collections.emptyList();
-            mContextHubInfoList = Collections.emptyList();
+        long startTimeNs = SystemClock.elapsedRealtimeNanos();
+        mContextHubWrapper = contextHubWrapper;
+        if (!initContextHubServiceState(startTimeNs)) {
+            Log.e(TAG, "Failed to initialize the Context Hub Service");
             return;
         }
+        initDefaultClientMap();
 
-        Pair<List<ContextHubInfo>, List<String>> hubInfo;
-        try {
-            hubInfo = mContextHubWrapper.getHubs();
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while getting Context Hub info", e);
-            hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
-        }
-        long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
-        int numContextHubs = hubInfo.first.size();
-        ContextHubStatsLog.write(ContextHubStatsLog.CONTEXT_HUB_BOOTED, bootTimeNs, numContextHubs);
-
-        mContextHubIdToInfoMap = Collections.unmodifiableMap(
-                ContextHubServiceUtil.createContextHubInfoMap(hubInfo.first));
-        mSupportedContextHubPerms = hubInfo.second;
-        mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
-        mClientManager = new ContextHubClientManager(mContext, mContextHubWrapper);
-        mTransactionManager = new ContextHubTransactionManager(
-                mContextHubWrapper, mClientManager, mNanoAppStateManager);
-        mSensorPrivacyManagerInternal =
-                LocalServices.getService(SensorPrivacyManagerInternal.class);
-
-        HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
-        for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
-            mLastRestartTimestampMap.put(contextHubId,
-                    new AtomicLong(SystemClock.elapsedRealtimeNanos()));
-
-            ContextHubInfo contextHubInfo = mContextHubIdToInfoMap.get(contextHubId);
-            IContextHubClient client = mClientManager.registerClient(
-                    contextHubInfo, createDefaultClientCallback(contextHubId),
-                    null /* attributionTag */, mTransactionManager, mContext.getPackageName());
-            defaultClientMap.put(contextHubId, client);
-
-            try {
-                mContextHubWrapper.registerCallback(
-                        contextHubId, new ContextHubServiceCallback(contextHubId));
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
-                        + contextHubId + ")", e);
-            }
-
-            // Do a query to initialize the service cache list of nanoapps
-            // TODO(b/69270990): Remove this when old API is deprecated
-            queryNanoAppsInternal(contextHubId);
-        }
-        mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
-
-        if (mContextHubWrapper.supportsLocationSettingNotifications()) {
-            sendLocationSettingUpdate();
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendLocationSettingUpdate();
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsWifiSettingNotifications()) {
-            sendWifiSettingUpdate(true /* forceUpdate */);
-
-            BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())
-                            || WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED.equals(
-                            intent.getAction())) {
-                        sendWifiSettingUpdate(false /* forceUpdate */);
-                    }
-                }
-            };
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
-            filter.addAction(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED);
-            mContext.registerReceiver(wifiReceiver, filter);
-
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendWifiSettingUpdate(false /* forceUpdate */);
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsAirplaneModeSettingNotifications()) {
-            sendAirplaneModeSettingUpdate();
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendAirplaneModeSettingUpdate();
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsMicrophoneSettingNotifications()) {
-            sendMicrophoneDisableSettingUpdateForCurrentUser();
-
-            mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
-                    SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
-                        if (userId == getCurrentUserId()) {
-                            Log.d(TAG, "User: " + userId + "mic privacy: " + enabled);
-                            sendMicrophoneDisableSettingUpdate(enabled);
-                        }
-                    });
-
-        }
-
-        if (mContextHubWrapper.supportsBtSettingNotifications()) {
-            sendBtSettingUpdate(true /* forceUpdate */);
-
-            BroadcastReceiver btReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())
-                            || BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(
-                            intent.getAction())) {
-                        sendBtSettingUpdate(false /* forceUpdate */);
-                    }
-                }
-            };
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-            filter.addAction(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
-            mContext.registerReceiver(btReceiver, filter);
-        }
+        initLocationSettingNotifications();
+        initWifiSettingNotifications();
+        initAirplaneModeSettingNotifications();
+        initMicrophoneSettingNotifications();
+        initBtSettingNotifications();
 
         scheduleDailyMetricSnapshot();
+        Log.i(TAG, "Finished Context Hub Service init");
     }
 
     /**
@@ -437,21 +314,231 @@
     }
 
     /**
-     * @return the IContextHubWrapper interface
+     * Initializes the private state of the ContextHubService
+     *
+     * @param startTimeNs               the start time when init was called
+     *
+     * @return      if mContextHubWrapper is not null and a full state init was done
      */
-    private IContextHubWrapper getContextHubWrapper() {
-        IContextHubWrapper wrapper = IContextHubWrapper.maybeConnectToAidl();
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_2();
-        }
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_1();
-        }
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_0();
+    private boolean initContextHubServiceState(long startTimeNs) {
+        if (mContextHubWrapper == null) {
+            mTransactionManager = null;
+            mClientManager = null;
+            mSensorPrivacyManagerInternal = null;
+            mDefaultClientMap = Collections.emptyMap();
+            mContextHubIdToInfoMap = Collections.emptyMap();
+            mSupportedContextHubPerms = Collections.emptyList();
+            mContextHubInfoList = Collections.emptyList();
+            return false;
         }
 
-        return wrapper;
+        Pair<List<ContextHubInfo>, List<String>> hubInfo;
+        try {
+            hubInfo = mContextHubWrapper.getHubs();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException while getting Context Hub info", e);
+            hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
+        }
+
+        long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
+        int numContextHubs = hubInfo.first.size();
+        ContextHubStatsLog.write(ContextHubStatsLog.CONTEXT_HUB_BOOTED, bootTimeNs,
+                numContextHubs);
+
+        mContextHubIdToInfoMap = Collections.unmodifiableMap(
+                ContextHubServiceUtil.createContextHubInfoMap(hubInfo.first));
+        mSupportedContextHubPerms = hubInfo.second;
+        mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
+        mClientManager = new ContextHubClientManager(mContext, mContextHubWrapper);
+        mTransactionManager = new ContextHubTransactionManager(
+                mContextHubWrapper, mClientManager, mNanoAppStateManager);
+        mSensorPrivacyManagerInternal =
+                LocalServices.getService(SensorPrivacyManagerInternal.class);
+        return true;
+    }
+
+    /**
+     * Creates the default client map that maps context hub IDs to the associated
+     * ClientManager. The client map is unmodifiable
+     */
+    private void initDefaultClientMap() {
+        HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
+        for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
+            mLastRestartTimestampMap.put(contextHubId,
+                    new AtomicLong(SystemClock.elapsedRealtimeNanos()));
+
+            ContextHubInfo contextHubInfo = mContextHubIdToInfoMap.get(contextHubId);
+            IContextHubClient client = mClientManager.registerClient(
+                    contextHubInfo, createDefaultClientCallback(contextHubId),
+                    /* attributionTag= */ null, mTransactionManager, mContext.getPackageName());
+            defaultClientMap.put(contextHubId, client);
+
+            try {
+                mContextHubWrapper.registerCallback(contextHubId,
+                        new ContextHubServiceCallback(contextHubId));
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
+                        + contextHubId + ")", e);
+            }
+
+            // Do a query to initialize the service cache list of nanoapps
+            // TODO(b/194289715): Remove this when old API is deprecated
+            queryNanoAppsInternal(contextHubId);
+        }
+        mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
+    }
+
+    /**
+     * Initializes existing callbacks with the mContextHubWrapper for every context hub
+     */
+    private void initExistingCallbacks() {
+        for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
+            try {
+                mContextHubWrapper.registerExistingCallback(contextHubId);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while registering existing service callback for hub "
+                        + "(ID = " + contextHubId + ")", e);
+            }
+        }
+    }
+
+    /**
+     * Handles the initialization of location settings notifications
+     */
+    private void initLocationSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsLocationSettingNotifications()) {
+            return;
+        }
+
+        sendLocationSettingUpdate();
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendLocationSettingUpdate();
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of wifi settings notifications
+     */
+    private void initWifiSettingNotifications() {
+        if (mContextHubWrapper == null || !mContextHubWrapper.supportsWifiSettingNotifications()) {
+            return;
+        }
+
+        sendWifiSettingUpdate(/* forceUpdate= */ true);
+
+        BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())
+                        || WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED.equals(
+                        intent.getAction())) {
+                    sendWifiSettingUpdate(/* forceUpdate= */ false);
+                }
+            }
+        };
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED);
+        mContext.registerReceiver(wifiReceiver, filter);
+
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendWifiSettingUpdate(/* forceUpdate= */ false);
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of airplane mode settings notifications
+     */
+    private void initAirplaneModeSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsAirplaneModeSettingNotifications()) {
+            return;
+        }
+
+        sendAirplaneModeSettingUpdate();
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendAirplaneModeSettingUpdate();
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of microphone settings notifications
+     */
+    private void initMicrophoneSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsMicrophoneSettingNotifications()) {
+            return;
+        }
+
+        sendMicrophoneDisableSettingUpdateForCurrentUser();
+        if (mSensorPrivacyManagerInternal == null) {
+            Log.e(TAG, "Unable to add a sensor privacy listener for all users");
+            return;
+        }
+
+        mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
+                SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
+                    if (userId == getCurrentUserId()) {
+                        Log.d(TAG, "User: " + userId + "mic privacy: " + enabled);
+                        sendMicrophoneDisableSettingUpdate(enabled);
+                    }
+                });
+    }
+
+    /**
+     * Handles the initialization of bluetooth settings notifications
+     */
+    private void initBtSettingNotifications() {
+        if (mContextHubWrapper == null || !mContextHubWrapper.supportsBtSettingNotifications()) {
+            return;
+        }
+
+        sendBtSettingUpdate(/* forceUpdate= */ true);
+
+        BroadcastReceiver btReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())
+                        || BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(
+                        intent.getAction())) {
+                    sendBtSettingUpdate(/* forceUpdate= */ false);
+                }
+            }
+        };
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        filter.addAction(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
+        mContext.registerReceiver(btReceiver, filter);
+    }
+
+    /**
+     * Resets the settings. Called when a context hub restarts or the AIDL HAL dies
+     */
+    private void resetSettings() {
+        sendLocationSettingUpdate();
+        sendWifiSettingUpdate(/* forceUpdate= */ true);
+        sendAirplaneModeSettingUpdate();
+        sendMicrophoneDisableSettingUpdateForCurrentUser();
+        sendBtSettingUpdate(/* forceUpdate= */ true);
     }
 
     @Override
@@ -463,6 +550,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public int registerCallback(IContextHubCallback callback) throws RemoteException {
+        super.registerCallback_enforcePermission();
+
         mCallbacksList.register(callback);
 
         Log.d(TAG, "Added callback, total callbacks " +
@@ -473,12 +562,16 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public int[] getContextHubHandles() throws RemoteException {
+        super.getContextHubHandles_enforcePermission();
+
         return ContextHubServiceUtil.createPrimitiveIntArray(mContextHubIdToInfoMap.keySet());
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public ContextHubInfo getContextHubInfo(int contextHubHandle) throws RemoteException {
+        super.getContextHubInfo_enforcePermission();
+
         if (!mContextHubIdToInfoMap.containsKey(contextHubHandle)) {
             Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in getContextHubInfo");
             return null;
@@ -495,6 +588,8 @@
      */
     @Override
     public List<ContextHubInfo> getContextHubs() throws RemoteException {
+        super.getContextHubs_enforcePermission();
+
         return mContextHubInfoList;
     }
 
@@ -561,6 +656,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) throws RemoteException {
+        super.loadNanoApp_enforcePermission();
+
         if (mContextHubWrapper == null) {
             return -1;
         }
@@ -588,6 +685,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
     @Override
     public int unloadNanoApp(int nanoAppHandle) throws RemoteException {
+        super.unloadNanoApp_enforcePermission();
+
         if (mContextHubWrapper == null) {
             return -1;
         }
@@ -614,6 +713,8 @@
     @Override
     public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) throws RemoteException {
 
+        super.getNanoAppInstanceInfo_enforcePermission();
+
         return mNanoAppStateManager.getNanoAppInstanceInfo(nanoAppHandle);
     }
 
@@ -622,6 +723,8 @@
     public int[] findNanoAppOnHub(
             int contextHubHandle, NanoAppFilter filter) throws RemoteException {
 
+        super.findNanoAppOnHub_enforcePermission();
+
         ArrayList<Integer> foundInstances = new ArrayList<>();
         if (filter != null) {
             mNanoAppStateManager.foreachNanoAppInstanceInfo((info) -> {
@@ -666,6 +769,8 @@
     @Override
     public int sendMessage(int contextHubHandle, int nanoAppHandle, ContextHubMessage msg)
             throws RemoteException {
+        super.sendMessage_enforcePermission();
+
         if (mContextHubWrapper == null) {
             return -1;
         }
@@ -729,7 +834,7 @@
 
     /**
      * A helper function to handle a load response from the Context Hub for the old API.
-     * TODO(b/69270990): Remove this once the old APIs are obsolete.
+     * TODO(b/194289715): Remove this once the old APIs are obsolete.
      */
     private void handleLoadResponseOldApi(
             int contextHubId, int result, NanoAppBinary nanoAppBinary) {
@@ -750,7 +855,7 @@
     /**
      * A helper function to handle an unload response from the Context Hub for the old API.
      * <p>
-     * TODO(b/69270990): Remove this once the old APIs are obsolete.
+     * TODO(b/194289715): Remove this once the old APIs are obsolete.
      */
     private void handleUnloadResponseOldApi(int contextHubId, int result) {
         byte[] data = new byte[1];
@@ -787,11 +892,7 @@
 
             ContextHubEventLogger.getInstance().logContextHubRestart(contextHubId);
 
-            sendLocationSettingUpdate();
-            sendWifiSettingUpdate(true /* forceUpdate */);
-            sendAirplaneModeSettingUpdate();
-            sendMicrophoneDisableSettingUpdateForCurrentUser();
-            sendBtSettingUpdate(true /* forceUpdate */);
+            resetSettings();
 
             mTransactionManager.onHubReset();
             queryNanoAppsInternal(contextHubId);
@@ -862,6 +963,8 @@
     public IContextHubClient createClient(
             int contextHubId, IContextHubClientCallback clientCallback,
             @Nullable String attributionTag, String packageName) throws RemoteException {
+        super.createClient_enforcePermission();
+
         if (!isValidContextHubId(contextHubId)) {
             throw new IllegalArgumentException("Invalid context hub ID " + contextHubId);
         }
@@ -890,6 +993,8 @@
     public IContextHubClient createPendingIntentClient(
             int contextHubId, PendingIntent pendingIntent, long nanoAppId,
             @Nullable String attributionTag) throws RemoteException {
+        super.createPendingIntentClient_enforcePermission();
+
         if (!isValidContextHubId(contextHubId)) {
             throw new IllegalArgumentException("Invalid context hub ID " + contextHubId);
         }
@@ -912,6 +1017,8 @@
     public void loadNanoAppOnHub(
             int contextHubId, IContextHubTransactionCallback transactionCallback,
             NanoAppBinary nanoAppBinary) throws RemoteException {
+        super.loadNanoAppOnHub_enforcePermission();
+
         if (!checkHalProxyAndContextHubId(
                 contextHubId, transactionCallback, ContextHubTransaction.TYPE_LOAD_NANOAPP)) {
             return;
@@ -941,6 +1048,8 @@
     public void unloadNanoAppFromHub(
             int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
             throws RemoteException {
+        super.unloadNanoAppFromHub_enforcePermission();
+
         if (!checkHalProxyAndContextHubId(
                 contextHubId, transactionCallback, ContextHubTransaction.TYPE_UNLOAD_NANOAPP)) {
             return;
@@ -964,6 +1073,8 @@
     public void enableNanoApp(
             int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
             throws RemoteException {
+        super.enableNanoApp_enforcePermission();
+
         if (!checkHalProxyAndContextHubId(
                 contextHubId, transactionCallback, ContextHubTransaction.TYPE_ENABLE_NANOAPP)) {
             return;
@@ -987,6 +1098,8 @@
     public void disableNanoApp(
             int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
             throws RemoteException {
+        super.disableNanoApp_enforcePermission();
+
         if (!checkHalProxyAndContextHubId(
                 contextHubId, transactionCallback, ContextHubTransaction.TYPE_DISABLE_NANOAPP)) {
             return;
@@ -1008,6 +1121,8 @@
     @Override
     public void queryNanoApps(int contextHubId, IContextHubTransactionCallback transactionCallback)
             throws RemoteException {
+        super.queryNanoApps_enforcePermission();
+
         if (!checkHalProxyAndContextHubId(
                 contextHubId, transactionCallback, ContextHubTransaction.TYPE_QUERY_NANOAPPS)) {
             return;
@@ -1066,8 +1181,8 @@
         mClientManager.forEachClientOfHub(contextHubId, client -> {
             if (client.getPackageName().equals(packageName)) {
                 client.updateNanoAppAuthState(
-                        nanoAppId, Collections.emptyList() /* nanoappPermissions */,
-                        false /* gracePeriodExpired */, true /* forceDenied */);
+                        nanoAppId, /* nanoappPermissions= */ Collections.emptyList(),
+                        /* gracePeriodExpired= */ false, /* forceDenied= */ true);
             }
         });
     }
@@ -1151,7 +1266,7 @@
         }
         if (!isValidContextHubId(contextHubId)) {
             Log.e(TAG, "Cannot start "
-                    + ContextHubTransaction.typeToString(transactionType, false /* upperCase */)
+                    + ContextHubTransaction.typeToString(transactionType, /* upperCase= */ false)
                     + " transaction for invalid hub ID " + contextHubId);
             try {
                 callback.onTransactionComplete(ContextHubTransaction.RESULT_FAILED_BAD_PARAMS);
@@ -1260,7 +1375,8 @@
      * Hub.
      */
     private void sendMicrophoneDisableSettingUpdateForCurrentUser() {
-        boolean isEnabled = mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
+        boolean isEnabled = mSensorPrivacyManagerInternal == null ? false :
+                mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
                 getCurrentUserId(), SensorPrivacyManager.Sensors.MICROPHONE);
         sendMicrophoneDisableSettingUpdate(isEnabled);
     }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index 4f6d0d4..e46b8c0c 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -523,9 +523,9 @@
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder(100);
-        TransactionRecord[] arr;
+        ContextHubServiceTransaction[] arr;
         synchronized (this) {
-            arr = mTransactionQueue.toArray(new TransactionRecord[0]);
+            arr = mTransactionQueue.toArray(new ContextHubServiceTransaction[0]);
         }
         for (int i = 0; i < arr.length; i++) {
             sb.append(i + ": " + arr[i] + "\n");
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index acc0746..48152b4 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -32,6 +32,7 @@
 import android.hardware.location.NanoAppState;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -92,6 +93,29 @@
          */
         void handleNanoappMessage(short hostEndpointId, NanoAppMessage message,
                 List<String> nanoappPermissions, List<String> messagePermissions);
+
+        /**
+         * Handles a restart of the service
+         */
+        void handleServiceRestart();
+    }
+
+    /**
+     * @return the IContextHubWrapper interface
+     */
+    public static IContextHubWrapper getContextHubWrapper() {
+        IContextHubWrapper wrapper = maybeConnectToAidl();
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_2();
+        }
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_1();
+        }
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_0();
+        }
+
+        return wrapper;
     }
 
     /**
@@ -152,12 +176,9 @@
     }
 
     /**
-     * Attempts to connect to the Contexthub HAL AIDL service, if it exists.
-     *
-     * @return A valid IContextHubWrapper if the connection was successful, null otherwise.
+     * Attempts to connect to the AIDL HAL and returns the proxy IContextHub.
      */
-    @Nullable
-    public static IContextHubWrapper maybeConnectToAidl() {
+    public static android.hardware.contexthub.IContextHub maybeConnectToAidlGetProxy() {
         android.hardware.contexthub.IContextHub proxy = null;
         final String aidlServiceName =
                 android.hardware.contexthub.IContextHub.class.getCanonicalName() + "/default";
@@ -170,8 +191,18 @@
         } else {
             Log.d(TAG, "Context Hub AIDL service is not declared");
         }
+        return proxy;
+    }
 
-        return (proxy == null) ? null : new ContextHubWrapperAidl(proxy);
+    /**
+     * Attempts to connect to the Contexthub HAL AIDL service, if it exists.
+     *
+     * @return A valid IContextHubWrapper if the connection was successful, null otherwise.
+     */
+    @Nullable
+    public static IContextHubWrapper maybeConnectToAidl() {
+        android.hardware.contexthub.IContextHub proxy = maybeConnectToAidlGetProxy();
+        return proxy == null ? null : new ContextHubWrapperAidl(proxy);
     }
 
     /**
@@ -336,12 +367,22 @@
     public abstract void registerCallback(int contextHubId, @NonNull ICallback callback)
             throws RemoteException;
 
-    private static class ContextHubWrapperAidl extends IContextHubWrapper {
+    /**
+     * Registers an existing callback with the Context Hub.
+     *
+     * @param contextHubId The ID of the Context Hub to register the callback with.
+     */
+    public abstract void registerExistingCallback(int contextHubId) throws RemoteException;
+
+    private static class ContextHubWrapperAidl extends IContextHubWrapper
+            implements IBinder.DeathRecipient {
         private android.hardware.contexthub.IContextHub mHub;
 
         private final Map<Integer, ContextHubAidlCallback> mAidlCallbackMap =
                     new HashMap<>();
 
+        private Runnable mHandleServiceRestartCallback = null;
+
         // Use this thread in case where the execution requires to be on a service thread.
         // For instance, AppOpsManager.noteOp requires the UPDATE_APP_OPS_STATS permission.
         private HandlerThread mHandlerThread =
@@ -401,17 +442,51 @@
         }
 
         ContextHubWrapperAidl(android.hardware.contexthub.IContextHub hub) {
-            mHub = hub;
+            setHub(hub);
             mHandlerThread.start();
             mHandler = new Handler(mHandlerThread.getLooper());
+            linkWrapperToHubDeath();
+        }
+
+        private synchronized android.hardware.contexthub.IContextHub getHub() {
+            return mHub;
+        }
+
+        private synchronized void setHub(android.hardware.contexthub.IContextHub hub) {
+            mHub = hub;
+        }
+
+        @Override
+        public void binderDied() {
+            Log.i(TAG, "Context Hub AIDL HAL died");
+
+            setHub(maybeConnectToAidlGetProxy());
+            if (getHub() == null) {
+                // TODO(b/256860015): Make this reconnection more robust
+                Log.e(TAG, "Could not reconnect to Context Hub AIDL HAL");
+                return;
+            }
+            linkWrapperToHubDeath();
+
+            if (mHandleServiceRestartCallback != null) {
+                mHandleServiceRestartCallback.run();
+            } else {
+                Log.e(TAG, "mHandleServiceRestartCallback is not set");
+            }
         }
 
         public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return new Pair<List<ContextHubInfo>, List<String>>(new ArrayList<ContextHubInfo>(),
+                        new ArrayList<String>());
+            }
+
             Set<String> supportedPermissions = new HashSet<>();
             ArrayList<ContextHubInfo> hubInfoList = new ArrayList<>();
-            for (android.hardware.contexthub.ContextHubInfo hub : mHub.getContextHubs()) {
-                hubInfoList.add(new ContextHubInfo(hub));
-                for (String permission : hub.supportedPermissions) {
+            for (android.hardware.contexthub.ContextHubInfo hubInfo : hub.getContextHubs()) {
+                hubInfoList.add(new ContextHubInfo(hubInfo));
+                for (String permission : hubInfo.supportedPermissions) {
                     supportedPermissions.add(permission);
                 }
             }
@@ -471,8 +546,13 @@
 
         @Override
         public void onHostEndpointConnected(HostEndpointInfo info) {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
             try {
-                mHub.onHostEndpointConnected(info);
+                hub.onHostEndpointConnected(info);
             } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(TAG, "Exception in onHostEndpointConnected" + e.getMessage());
             }
@@ -480,8 +560,13 @@
 
         @Override
         public void onHostEndpointDisconnected(short hostEndpointId) {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
             try {
-                mHub.onHostEndpointDisconnected((char) hostEndpointId);
+                hub.onHostEndpointDisconnected((char) hostEndpointId);
             } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(TAG, "Exception in onHostEndpointDisconnected" + e.getMessage());
             }
@@ -491,8 +576,13 @@
         public int sendMessageToContextHub(
                 short hostEndpointId, int contextHubId, NanoAppMessage message)
                 throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             try {
-                mHub.sendMessageToHub(contextHubId,
+                hub.sendMessageToHub(contextHubId,
                         ContextHubServiceUtil.createAidlContextHubMessage(hostEndpointId, message));
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException e) {
@@ -505,10 +595,15 @@
         @ContextHubTransaction.Result
         public int loadNanoapp(int contextHubId, NanoAppBinary binary,
                 int transactionId) throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             android.hardware.contexthub.NanoappBinary aidlNanoAppBinary =
                     ContextHubServiceUtil.createAidlNanoAppBinary(binary);
             try {
-                mHub.loadNanoapp(contextHubId, aidlNanoAppBinary, transactionId);
+                hub.loadNanoapp(contextHubId, aidlNanoAppBinary, transactionId);
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -520,8 +615,13 @@
         @ContextHubTransaction.Result
         public int unloadNanoapp(int contextHubId, long nanoappId, int transactionId)
                 throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             try {
-                mHub.unloadNanoapp(contextHubId, nanoappId, transactionId);
+                hub.unloadNanoapp(contextHubId, nanoappId, transactionId);
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -533,8 +633,13 @@
         @ContextHubTransaction.Result
         public int enableNanoapp(int contextHubId, long nanoappId, int transactionId)
                 throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             try {
-                mHub.enableNanoapp(contextHubId, nanoappId, transactionId);
+                hub.enableNanoapp(contextHubId, nanoappId, transactionId);
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -546,8 +651,13 @@
         @ContextHubTransaction.Result
         public int disableNanoapp(int contextHubId, long nanoappId, int transactionId)
                 throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             try {
-                mHub.disableNanoapp(contextHubId, nanoappId, transactionId);
+                hub.disableNanoapp(contextHubId, nanoappId, transactionId);
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -558,8 +668,13 @@
 
         @ContextHubTransaction.Result
         public int queryNanoapps(int contextHubId) throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+            }
+
             try {
-                mHub.queryNanoapps(contextHubId);
+                hub.queryNanoapps(contextHubId);
                 return ContextHubTransaction.RESULT_SUCCESS;
             } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
                 return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -568,22 +683,65 @@
             }
         }
 
-        public void registerCallback(int contextHubId, ICallback callback) throws RemoteException {
-            mAidlCallbackMap.put(contextHubId, new ContextHubAidlCallback(contextHubId, callback));
+        public void registerExistingCallback(int contextHubId) {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
+            ContextHubAidlCallback callback = mAidlCallbackMap.get(contextHubId);
+            if (callback == null) {
+                Log.e(TAG, "Could not find existing callback to register for context hub ID = "
+                        + contextHubId);
+                return;
+            }
+
             try {
-                mHub.registerCallback(contextHubId, mAidlCallbackMap.get(contextHubId));
+                hub.registerCallback(contextHubId, callback);
             } catch (RemoteException | ServiceSpecificException | IllegalArgumentException e) {
                 Log.e(TAG, "Exception while registering callback: " + e.getMessage());
             }
         }
 
+        public void registerCallback(int contextHubId, ICallback callback) {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
+            mHandleServiceRestartCallback = callback::handleServiceRestart;
+            mAidlCallbackMap.put(contextHubId, new ContextHubAidlCallback(contextHubId, callback));
+            registerExistingCallback(contextHubId);
+        }
+
         private void onSettingChanged(byte setting, boolean enabled) {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
             try {
-                mHub.onSettingChanged(setting, enabled);
+                hub.onSettingChanged(setting, enabled);
             } catch (RemoteException | ServiceSpecificException e) {
                 Log.e(TAG, "Exception while sending setting update: " + e.getMessage());
             }
         }
+
+        /**
+         * Links the mHub death handler to this
+         */
+        private void linkWrapperToHubDeath() {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return;
+            }
+
+            try {
+                hub.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException exception) {
+                Log.e(TAG, "Context Hub AIDL service death receipt could not be linked");
+            }
+        }
     }
 
     /**
@@ -711,6 +869,17 @@
             mHub.registerCallback(contextHubId, mHidlCallbackMap.get(contextHubId));
         }
 
+        public void registerExistingCallback(int contextHubId) throws RemoteException {
+            ContextHubWrapperHidlCallback callback = mHidlCallbackMap.get(contextHubId);
+            if (callback == null) {
+                Log.e(TAG, "Could not find existing callback for context hub with ID = "
+                        + contextHubId);
+                return;
+            }
+
+            mHub.registerCallback(contextHubId, callback);
+        }
+
         public boolean supportsBtSettingNotifications() {
             return false;
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
index 1435016..77cd673 100644
--- a/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
+++ b/services/core/java/com/android/server/location/gnss/GnssConfiguration.java
@@ -76,6 +76,8 @@
             "ENABLE_PSDS_PERIODIC_DOWNLOAD";
     private static final String CONFIG_ENABLE_ACTIVE_SIM_EMERGENCY_SUPL =
             "ENABLE_ACTIVE_SIM_EMERGENCY_SUPL";
+    private static final String CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION =
+            "ENABLE_NI_SUPL_MESSAGE_INJECTION";
     static final String CONFIG_LONGTERM_PSDS_SERVER_1 = "LONGTERM_PSDS_SERVER_1";
     static final String CONFIG_LONGTERM_PSDS_SERVER_2 = "LONGTERM_PSDS_SERVER_2";
     static final String CONFIG_LONGTERM_PSDS_SERVER_3 = "LONGTERM_PSDS_SERVER_3";
@@ -218,6 +220,14 @@
     }
 
     /**
+     * Returns true if NI SUPL message injection is enabled; Returns false otherwise.
+     * Default false if not set.
+     */
+    boolean isNiSuplMessageInjectionEnabled() {
+        return getBooleanConfig(CONFIG_ENABLE_NI_SUPL_MESSAGE_INJECTION, false);
+    }
+
+    /**
      * Returns true if a long-term PSDS server is configured.
      */
     boolean isLongTermPsdsServerConfigured() {
@@ -286,26 +296,24 @@
                 Log.e(TAG, "Unable to set " + CONFIG_ES_EXTENSION_SEC + ": " + mEsExtensionSec);
             }
 
-            Map<String, SetCarrierProperty> map = new HashMap<String, SetCarrierProperty>() {
-                {
-                    put(CONFIG_SUPL_VER, GnssConfiguration::native_set_supl_version);
-                    put(CONFIG_SUPL_MODE, GnssConfiguration::native_set_supl_mode);
+            Map<String, SetCarrierProperty> map = new HashMap<String, SetCarrierProperty>();
 
-                    if (isConfigSuplEsSupported(gnssConfigurationIfaceVersion)) {
-                        put(CONFIG_SUPL_ES, GnssConfiguration::native_set_supl_es);
-                    }
+            map.put(CONFIG_SUPL_VER, GnssConfiguration::native_set_supl_version);
+            map.put(CONFIG_SUPL_MODE, GnssConfiguration::native_set_supl_mode);
 
-                    put(CONFIG_LPP_PROFILE, GnssConfiguration::native_set_lpp_profile);
-                    put(CONFIG_A_GLONASS_POS_PROTOCOL_SELECT,
-                            GnssConfiguration::native_set_gnss_pos_protocol_select);
-                    put(CONFIG_USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL,
-                            GnssConfiguration::native_set_emergency_supl_pdn);
+            if (isConfigSuplEsSupported(gnssConfigurationIfaceVersion)) {
+                map.put(CONFIG_SUPL_ES, GnssConfiguration::native_set_supl_es);
+            }
 
-                    if (isConfigGpsLockSupported(gnssConfigurationIfaceVersion)) {
-                        put(CONFIG_GPS_LOCK, GnssConfiguration::native_set_gps_lock);
-                    }
-                }
-            };
+            map.put(CONFIG_LPP_PROFILE, GnssConfiguration::native_set_lpp_profile);
+            map.put(CONFIG_A_GLONASS_POS_PROTOCOL_SELECT,
+                    GnssConfiguration::native_set_gnss_pos_protocol_select);
+            map.put(CONFIG_USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL,
+                    GnssConfiguration::native_set_emergency_supl_pdn);
+
+            if (isConfigGpsLockSupported(gnssConfigurationIfaceVersion)) {
+                map.put(CONFIG_GPS_LOCK, GnssConfiguration::native_set_gps_lock);
+            }
 
             for (Entry<String, SetCarrierProperty> entry : map.entrySet()) {
                 String propertyName = entry.getKey();
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index e653f04..6f6b1c9 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -84,6 +84,7 @@
 import android.os.WorkSource;
 import android.os.WorkSource.WorkChain;
 import android.provider.Settings;
+import android.provider.Telephony.Sms.Intents;
 import android.telephony.CarrierConfigManager;
 import android.telephony.CellIdentity;
 import android.telephony.CellIdentityGsm;
@@ -95,6 +96,7 @@
 import android.telephony.CellInfoLte;
 import android.telephony.CellInfoNr;
 import android.telephony.CellInfoWcdma;
+import android.telephony.SmsMessage;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -107,6 +109,7 @@
 import com.android.internal.location.GpsNetInitiatedHandler;
 import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.HexDump;
 import com.android.server.FgThread;
 import com.android.server.location.gnss.GnssSatelliteBlocklistHelper.GnssSatelliteBlocklistCallback;
 import com.android.server.location.gnss.NtpTimeHelper.InjectNtpTimeCallback;
@@ -523,23 +526,31 @@
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
         intentFilter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
-        mContext.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                String action = intent.getAction();
-                if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
-                if (action == null) {
-                    return;
-                }
+        mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
 
-                switch (action) {
-                    case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
-                    case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
-                        subscriptionOrCarrierConfigChanged();
-                        break;
-                }
+        if (mNetworkConnectivityHandler.isNativeAgpsRilSupported()
+                && mGnssConfiguration.isNiSuplMessageInjectionEnabled()) {
+            // Listen to WAP PUSH NI SUPL message.
+            // See User Plane Location Protocol Candidate Version 3.0,
+            // OMA-TS-ULP-V3_0-20110920-C, Section 8.3 OMA Push.
+            intentFilter = new IntentFilter();
+            intentFilter.addAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+            try {
+                intentFilter.addDataType("application/vnd.omaloc-supl-init");
+            } catch (IntentFilter.MalformedMimeTypeException e) {
+                Log.w(TAG, "Malformed SUPL init mime type");
             }
-        }, intentFilter, null, mHandler);
+            mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+
+            // Listen to MT SMS NI SUPL message.
+            // See User Plane Location Protocol Candidate Version 3.0,
+            // OMA-TS-ULP-V3_0-20110920-C, Section 8.4 MT SMS.
+            intentFilter = new IntentFilter();
+            intentFilter.addAction(Intents.DATA_SMS_RECEIVED_ACTION);
+            intentFilter.addDataScheme("sms");
+            intentFilter.addDataAuthority("localhost", "7275");
+            mContext.registerReceiver(mIntentReceiver, intentFilter, null, mHandler);
+        }
 
         mNetworkConnectivityHandler.registerNetworkCallbacks();
 
@@ -560,6 +571,80 @@
         updateEnabled();
     }
 
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
+            if (action == null) {
+                return;
+            }
+
+            switch (action) {
+                case CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED:
+                case TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED:
+                    subscriptionOrCarrierConfigChanged();
+                    break;
+                case Intents.WAP_PUSH_RECEIVED_ACTION:
+                case Intents.DATA_SMS_RECEIVED_ACTION:
+                    injectSuplInit(intent);
+                    break;
+            }
+        }
+    };
+
+    private void injectSuplInit(Intent intent) {
+        if (!isNfwLocationAccessAllowed()) {
+            Log.w(TAG, "Reject SUPL INIT as no NFW location access");
+            return;
+        }
+
+        int slotIndex = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX,
+                SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+        if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+            Log.e(TAG, "Invalid slot index");
+            return;
+        }
+
+        byte[] suplInit = null;
+        String action = intent.getAction();
+        if (action.equals(Intents.DATA_SMS_RECEIVED_ACTION)) {
+            SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
+            if (messages == null) {
+                Log.e(TAG, "Message does not exist in the intent");
+                return;
+            }
+            for (SmsMessage message : messages) {
+                suplInit = message.getUserData();
+                injectSuplInit(suplInit, slotIndex);
+            }
+        } else if (action.equals(Intents.WAP_PUSH_RECEIVED_ACTION)) {
+            suplInit = intent.getByteArrayExtra("data");
+            injectSuplInit(suplInit, slotIndex);
+        }
+    }
+
+    private void injectSuplInit(byte[] suplInit, int slotIndex) {
+        if (suplInit != null) {
+            if (DEBUG) {
+                Log.d(TAG, "suplInit = "
+                        + HexDump.toHexString(suplInit) + " slotIndex = " + slotIndex);
+            }
+            mGnssNative.injectNiSuplMessageData(suplInit, suplInit.length , slotIndex);
+        }
+    }
+
+    private boolean isNfwLocationAccessAllowed() {
+        if (mGnssNative.isInEmergencySession()) {
+            return true;
+        }
+        if (mGnssVisibilityControl != null
+                && mGnssVisibilityControl.hasLocationPermissionEnabledProxyApps()) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Implements {@link InjectNtpTimeCallback#injectTime}
      */
@@ -1447,7 +1532,9 @@
      * @return the cell ID or -1 if invalid
      */
     private static long getCidFromCellIdentity(CellIdentity id) {
-        if (id == null) return -1;
+        if (id == null) {
+            return -1;
+        }
         long cid = -1;
         switch(id.getType()) {
             case CellInfo.TYPE_GSM: cid = ((CellIdentityGsm) id).getCid(); break;
@@ -1522,7 +1609,8 @@
 
                 for (CellInfo ci : cil) {
                     int status = ci.getCellConnectionStatus();
-                    if (status == CellInfo.CONNECTION_PRIMARY_SERVING
+                    if (ci.isRegistered()
+                            || status == CellInfo.CONNECTION_PRIMARY_SERVING
                             || status == CellInfo.CONNECTION_SECONDARY_SERVING) {
                         CellIdentity c = ci.getCellIdentity();
                         int t = getCellType(ci);
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index 07e9fe6..6c4c829 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -115,6 +115,16 @@
         if (request.getIntervalMillis() == GnssMeasurementRequest.PASSIVE_INTERVAL) {
             return true;
         }
+        // The HAL doc does not specify if consecutive start() calls will be allowed.
+        // Some vendors may ignore the 2nd start() call if stop() is not called.
+        // Thus, here we always call stop() before calling start() to avoid being ignored.
+        if (mGnssNative.stopMeasurementCollection()) {
+            if (D) {
+                Log.d(TAG, "stopping gnss measurements");
+            }
+        } else {
+            Log.e(TAG, "error stopping gnss measurements");
+        }
         if (mGnssNative.startMeasurementCollection(request.isFullTracking(),
                 request.isCorrelationVectorOutputsEnabled(),
                 request.getIntervalMillis())) {
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 02bdfd5..a7fffe2 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -762,6 +762,10 @@
         return APN_INVALID;
     }
 
+    protected boolean isNativeAgpsRilSupported() {
+        return native_is_agps_ril_supported();
+    }
+
     // AGPS support
     private native void native_agps_data_conn_open(long networkHandle, String apn, int apnIpType);
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
index 631dbbf..4e5e5f8 100644
--- a/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
+++ b/services/core/java/com/android/server/location/gnss/GnssVisibilityControl.java
@@ -437,6 +437,10 @@
         return locationPermissionEnabledProxyApps;
     }
 
+    public boolean hasLocationPermissionEnabledProxyApps() {
+        return getLocationPermissionEnabledProxyApps().length > 0;
+    }
+
     private void handleNfwNotification(NfwNotification nfwNotification) {
         if (DEBUG) Log.d(TAG, "Non-framework location access notification: " + nfwNotification);
 
diff --git a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
index 1fa56bc..edb2e5b 100644
--- a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
+++ b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
@@ -25,6 +25,7 @@
 import android.location.GnssMeasurementCorrections;
 import android.location.GnssMeasurementsEvent;
 import android.location.GnssNavigationMessage;
+import android.location.GnssSignalType;
 import android.location.GnssStatus;
 import android.location.Location;
 import android.os.Binder;
@@ -988,6 +989,14 @@
         mGnssHal.injectPsdsData(data, length, psdsType);
     }
 
+    /**
+     * Injects NI SUPL message data into the GNSS HAL.
+     */
+    public void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+        Preconditions.checkState(mRegistered);
+        mGnssHal.injectNiSuplMessageData(data, length, slotIndex);
+    }
+
     @NativeEntryPoint
     void reportGnssServiceDied() {
         // Not necessary to clear (and restore) binder identity since it runs on another thread.
@@ -1144,6 +1153,13 @@
         onCapabilitiesChanged(oldCapabilities, mCapabilities);
     }
 
+    @NativeEntryPoint
+    void setSignalTypeCapabilities(List<GnssSignalType> signalTypes) {
+        GnssCapabilities oldCapabilities = mCapabilities;
+        mCapabilities = oldCapabilities.withSignalTypes(signalTypes);
+        onCapabilitiesChanged(oldCapabilities, mCapabilities);
+    }
+
     private void onCapabilitiesChanged(GnssCapabilities oldCapabilities,
             GnssCapabilities newCapabilities) {
         Binder.withCleanCallingIdentity(() -> {
@@ -1270,7 +1286,7 @@
     }
 
     @NativeEntryPoint
-    boolean isInEmergencySession() {
+    public boolean isInEmergencySession() {
         return Binder.withCleanCallingIdentity(
                 () -> mEmergencyHelper.isInEmergency(
                         TimeUnit.SECONDS.toMillis(mConfiguration.getEsExtensionSec())));
@@ -1499,6 +1515,10 @@
         protected void injectPsdsData(byte[] data, int length, int psdsType) {
             native_inject_psds_data(data, length, psdsType);
         }
+
+        protected void injectNiSuplMessageData(byte[] data, int length, int slotIndex) {
+            native_inject_ni_supl_message_data(data, length, slotIndex);
+        }
     }
 
     // basic APIs
@@ -1642,6 +1662,9 @@
     private static native void native_agps_set_ref_location_cellid(int type, int mcc, int mnc,
             int lac, long cid, int tac, int pcid, int arfcn);
 
+    private static native void native_inject_ni_supl_message_data(byte[] data, int length,
+            int slotIndex);
+
     // PSDS APIs
 
     private static native boolean native_supports_psds();
diff --git a/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java b/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java
index f144cf8..46f486d 100644
--- a/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java
+++ b/services/core/java/com/android/server/locksettings/BiometricDeferredQueue.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricManager;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintManager;
@@ -48,11 +49,13 @@
     @NonNull private final Handler mHandler;
     @Nullable private FingerprintManager mFingerprintManager;
     @Nullable private FaceManager mFaceManager;
+    @Nullable private BiometricManager mBiometricManager;
 
     // Entries added by LockSettingsService once a user's synthetic password is known. At this point
     // things are still keyed by userId.
     @NonNull private final ArrayList<UserAuthInfo> mPendingResetLockoutsForFingerprint;
     @NonNull private final ArrayList<UserAuthInfo> mPendingResetLockoutsForFace;
+    @NonNull private final ArrayList<UserAuthInfo> mPendingResetLockouts;
 
     /**
      * Authentication info for a successful user unlock via Synthetic Password. This can be used to
@@ -125,7 +128,6 @@
     }
 
     @Nullable private FaceResetLockoutTask mFaceResetLockoutTask;
-
     private final FaceResetLockoutTask.FinishCallback mFaceFinishCallback = () -> {
         mFaceResetLockoutTask = null;
     };
@@ -135,12 +137,14 @@
         mHandler = handler;
         mPendingResetLockoutsForFingerprint = new ArrayList<>();
         mPendingResetLockoutsForFace = new ArrayList<>();
+        mPendingResetLockouts = new ArrayList<>();
     }
 
     public void systemReady(@Nullable FingerprintManager fingerprintManager,
-            @Nullable FaceManager faceManager) {
+            @Nullable FaceManager faceManager, @Nullable BiometricManager biometricManager) {
         mFingerprintManager = fingerprintManager;
         mFaceManager = faceManager;
+        mBiometricManager = biometricManager;
     }
 
     /**
@@ -151,7 +155,7 @@
      * Note that this should only ever be invoked for successful authentications, otherwise it will
      * consume a Gatekeeper authentication attempt and potentially wipe the user/device.
      *
-     * @param userId The user that the operation will apply for.
+     * @param userId             The user that the operation will apply for.
      * @param gatekeeperPassword The Gatekeeper Password
      */
     void addPendingLockoutResetForUser(int userId, @NonNull byte[] gatekeeperPassword) {
@@ -167,6 +171,12 @@
                 mPendingResetLockoutsForFingerprint.add(new UserAuthInfo(userId,
                         gatekeeperPassword));
             }
+
+            if (mBiometricManager != null) {
+                Slog.d(TAG, "Fingerprint addPendingLockoutResetForUser: " + userId);
+                mPendingResetLockouts.add(new UserAuthInfo(userId,
+                        gatekeeperPassword));
+            }
         });
     }
 
@@ -184,6 +194,14 @@
                         new ArrayList<>(mPendingResetLockoutsForFingerprint));
                 mPendingResetLockoutsForFingerprint.clear();
             }
+
+            if (!mPendingResetLockouts.isEmpty()) {
+                Slog.d(TAG, "Processing pending resetLockouts(Generic)");
+                processPendingLockoutsGeneric(
+                        new ArrayList<>(mPendingResetLockouts));
+                mPendingResetLockouts.clear();
+            }
+
         });
     }
 
@@ -257,6 +275,17 @@
         }
     }
 
+    private void processPendingLockoutsGeneric(List<UserAuthInfo> pendingResetLockouts) {
+        for (UserAuthInfo user : pendingResetLockouts) {
+            Slog.d(TAG, "Resetting biometric lockout for user: " + user.userId);
+            final byte[] hat = requestHatFromGatekeeperPassword(mSpManager, user,
+                    0 /* challenge */);
+            if (hat != null) {
+                mBiometricManager.resetLockout(user.userId, hat);
+            }
+        }
+    }
+
     @Nullable
     private static byte[] requestHatFromGatekeeperPassword(
             @NonNull SyntheticPasswordManager spManager,
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c899cf2..25e71e8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -70,6 +70,7 @@
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
 import android.hardware.authsecret.V1_0.IAuthSecret;
+import android.hardware.biometrics.BiometricManager;
 import android.hardware.face.Face;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.Fingerprint;
@@ -247,14 +248,14 @@
 
     // Locking order is mUserCreationAndRemovalLock -> mSpManager.
     private final Object mUserCreationAndRemovalLock = new Object();
-    // These two arrays are only used at boot time.  To save memory, they are set to null when
-    // PHASE_BOOT_COMPLETED is reached.
+    // These two arrays are only used at boot time.  To save memory, they are set to null near the
+    // end of the boot, when onThirdPartyAppsStarted() is called.
     @GuardedBy("mUserCreationAndRemovalLock")
     private SparseIntArray mEarlyCreatedUsers = new SparseIntArray();
     @GuardedBy("mUserCreationAndRemovalLock")
     private SparseIntArray mEarlyRemovedUsers = new SparseIntArray();
     @GuardedBy("mUserCreationAndRemovalLock")
-    private boolean mBootComplete;
+    private boolean mThirdPartyAppsStarted;
 
     // Current password metrics for all secured users on the device. Updated when user unlocks the
     // device or changes password. Removed when user is stopped.
@@ -296,16 +297,9 @@
         @Override
         public void onBootPhase(int phase) {
             super.onBootPhase(phase);
-            switch (phase) {
-                case PHASE_ACTIVITY_MANAGER_READY:
-                    mLockSettingsService.migrateOldDataAfterSystemReady();
-                    mLockSettingsService.loadEscrowData();
-                    break;
-                case PHASE_BOOT_COMPLETED:
-                    mLockSettingsService.bootCompleted();
-                    break;
-                default:
-                    break;
+            if (phase == PHASE_ACTIVITY_MANAGER_READY) {
+                mLockSettingsService.migrateOldDataAfterSystemReady();
+                mLockSettingsService.loadEscrowData();
             }
         }
 
@@ -552,6 +546,10 @@
             }
         }
 
+        public BiometricManager getBiometricManager() {
+            return (BiometricManager) mContext.getSystemService(Context.BIOMETRIC_SERVICE);
+        }
+
         public int settingsGlobalGetInt(ContentResolver contentResolver, String keyName,
                 int defaultValue) {
             return Settings.Global.getInt(contentResolver, keyName, defaultValue);
@@ -744,8 +742,8 @@
      * <p>
      * This is primarily needed for users that were removed by Android 13 or earlier, which didn't
      * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent.  It is
-     * also needed because {@link #removeUser()} delays requests to remove LSS state until the
-     * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost.
+     * also needed because {@link #removeUser()} delays requests to remove LSS state until Weaver is
+     * guaranteed to be available, so they can be lost.
      * <p>
      * Stale state is detected by checking whether the user serial number changed.  This works
      * because user serial numbers are never reused.
@@ -837,7 +835,7 @@
         // TODO: maybe skip this for split system user mode.
         mStorage.prefetchUser(UserHandle.USER_SYSTEM);
         mBiometricDeferredQueue.systemReady(mInjector.getFingerprintManager(),
-                mInjector.getFaceManager());
+                mInjector.getFaceManager(), mInjector.getBiometricManager());
     }
 
     private void loadEscrowData() {
@@ -926,7 +924,9 @@
         return success;
     }
 
-    private void bootCompleted() {
+    // This is called when Weaver is guaranteed to be available (if the device supports Weaver).
+    // It does any synthetic password related work that was delayed from earlier in the boot.
+    private void onThirdPartyAppsStarted() {
         synchronized (mUserCreationAndRemovalLock) {
             // Handle delayed calls to LSS.removeUser() and LSS.createNewUser().
             for (int i = 0; i < mEarlyRemovedUsers.size(); i++) {
@@ -971,7 +971,7 @@
                 setString("migrated_all_users_to_sp_and_bound_ce", "true", 0);
             }
 
-            mBootComplete = true;
+            mThirdPartyAppsStarted = true;
         }
     }
 
@@ -2081,9 +2081,11 @@
     public VerifyCredentialResponse checkCredential(LockscreenCredential credential, int userId,
             ICheckCredentialProgressCallback progressCallback) {
         checkPasswordReadPermission();
+        final long identity = Binder.clearCallingIdentity();
         try {
             return doVerifyCredential(credential, userId, progressCallback, 0 /* flags */);
         } finally {
+            Binder.restoreCallingIdentity(identity);
             scheduleGc();
         }
     }
@@ -2299,14 +2301,14 @@
 
     private void createNewUser(@UserIdInt int userId, int userSerialNumber) {
         synchronized (mUserCreationAndRemovalLock) {
-            // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but
-            // rather automatically delay it to later.  We do this because protecting the synthetic
+            // During early boot, don't actually create the synthetic password yet, but rather
+            // automatically delay it to later.  We do this because protecting the synthetic
             // password requires the Weaver HAL if the device supports it, and some devices don't
             // make Weaver available until fairly late in the boot process.  This logic ensures a
             // consistent flow across all devices, regardless of their Weaver implementation.
-            if (!mBootComplete) {
-                Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete",
-                        userId);
+            if (!mThirdPartyAppsStarted) {
+                Slogf.i(TAG, "Delaying locksettings state creation for user %d until third-party " +
+                        "apps are started", userId);
                 mEarlyCreatedUsers.put(userId, userSerialNumber);
                 mEarlyRemovedUsers.delete(userId);
                 return;
@@ -2320,14 +2322,14 @@
 
     private void removeUser(@UserIdInt int userId) {
         synchronized (mUserCreationAndRemovalLock) {
-            // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather
-            // automatically delay it to later.  We do this because deleting synthetic password
-            // protectors requires the Weaver HAL if the device supports it, and some devices don't
-            // make Weaver available until fairly late in the boot process.  This logic ensures a
-            // consistent flow across all devices, regardless of their Weaver implementation.
-            if (!mBootComplete) {
-                Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete",
-                        userId);
+            // During early boot, don't actually remove the LSS state yet, but rather automatically
+            // delay it to later.  We do this because deleting synthetic password protectors
+            // requires the Weaver HAL if the device supports it, and some devices don't make Weaver
+            // available until fairly late in the boot process.  This logic ensures a consistent
+            // flow across all devices, regardless of their Weaver implementation.
+            if (!mThirdPartyAppsStarted) {
+                Slogf.i(TAG, "Delaying locksettings state removal for user %d until third-party " +
+                        "apps are started", userId);
                 if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) {
                     mEarlyCreatedUsers.delete(userId);
                 } else {
@@ -2629,9 +2631,8 @@
      * protects the user's CE key with a key derived from the SP.
      * <p>
      * This is called just once in the lifetime of the user: at user creation time (possibly delayed
-     * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device
-     * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't
-     * necessarily have an SP.
+     * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13
+     * or earlier where users with no LSKF didn't necessarily have an SP.
      */
     @GuardedBy("mSpManager")
     @VisibleForTesting
@@ -3154,7 +3155,7 @@
 
         pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size());
         synchronized (mUserCreationAndRemovalLock) {
-            pw.println("BootComplete: " + mBootComplete);
+            pw.println("ThirdPartyAppsStarted: " + mThirdPartyAppsStarted);
         }
     }
 
@@ -3312,6 +3313,11 @@
     private final class LocalService extends LockSettingsInternal {
 
         @Override
+        public void onThirdPartyAppsStarted() {
+            LockSettingsService.this.onThirdPartyAppsStarted();
+        }
+
+        @Override
         public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
             LockSettingsService.this.unlockUserKeyIfUnsecured(userId);
         }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index db036b0..807ba3c 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -36,6 +36,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.AtomicFile;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -52,6 +53,7 @@
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
@@ -307,30 +309,31 @@
     }
 
     private void writeFile(File path, byte[] data) {
+        writeFile(path, data, /* syncParentDir= */ true);
+    }
+
+    private void writeFile(File path, byte[] data, boolean syncParentDir) {
         synchronized (mFileWriteLock) {
-            RandomAccessFile raf = null;
+            // Use AtomicFile to guarantee atomicity of the file write, including when an existing
+            // file is replaced with a new one.  This method is usually used to create new files,
+            // but there are some edge cases in which it is used to replace an existing file.
+            AtomicFile file = new AtomicFile(path);
+            FileOutputStream out = null;
             try {
-                // Write the data to the file, requiring each write to be synchronized to the
-                // underlying storage device immediately to avoid data loss in case of power loss.
-                raf = new RandomAccessFile(path, "rws");
-                // Truncate the file if the data is empty.
-                if (data == null || data.length == 0) {
-                    raf.setLength(0);
-                } else {
-                    raf.write(data, 0, data.length);
-                }
-                raf.close();
-                fsyncDirectory(path.getParentFile());
+                out = file.startWrite();
+                out.write(data);
+                file.finishWrite(out);
+                out = null;
             } catch (IOException e) {
-                Slog.e(TAG, "Error writing to file " + e);
+                Slog.e(TAG, "Error writing file " + path, e);
             } finally {
-                if (raf != null) {
-                    try {
-                        raf.close();
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error closing file " + e);
-                    }
-                }
+                file.failWrite(out);
+            }
+            // For performance reasons, AtomicFile only syncs the file itself, not also the parent
+            // directory.  The latter must be done explicitly when requested here, as some callers
+            // need a guarantee that the file really exists on-disk when this returns.
+            if (syncParentDir) {
+                fsyncDirectory(path.getParentFile());
             }
             mCache.putFile(path, data);
         }
@@ -338,18 +341,20 @@
 
     private void deleteFile(File path) {
         synchronized (mFileWriteLock) {
+            // Zeroize the file to try to make its contents unrecoverable.  This is *not* guaranteed
+            // to be effective, and in fact it usually isn't, but it doesn't hurt.  We also don't
+            // bother zeroizing |path|.new, which may exist from an interrupted AtomicFile write.
             if (path.exists()) {
-                // Zeroize the file to try to make its contents unrecoverable.  This is *not*
-                // guaranteed to be effective, and in fact it usually isn't, but it doesn't hurt.
                 try (RandomAccessFile raf = new RandomAccessFile(path, "rws")) {
                     final int fileSize = (int) raf.length();
                     raf.write(new byte[fileSize]);
                 } catch (Exception e) {
                     Slog.w(TAG, "Failed to zeroize " + path, e);
                 }
-                path.delete();
-                mCache.putFile(path, null);
             }
+            // To ensure that |path|.new is deleted if it exists, use AtomicFile.delete() here.
+            new AtomicFile(path).delete();
+            mCache.putFile(path, null);
         }
     }
 
@@ -379,10 +384,20 @@
         }
     }
 
+    /**
+     * Writes the synthetic password state file for the given user ID, protector ID, and state name.
+     * If the file already exists, then it is atomically replaced.
+     * <p>
+     * This doesn't sync the parent directory, and a result the new state file may be lost if the
+     * system crashes.  The caller must call {@link syncSyntheticPasswordState()} afterwards to sync
+     * the parent directory if needed, preferably after batching up other state file creations for
+     * the same user.  We do it this way because directory syncs are expensive on some filesystems.
+     */
     public void writeSyntheticPasswordState(int userId, long protectorId, String name,
             byte[] data) {
         ensureSyntheticPasswordDirectoryForUser(userId);
-        writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data);
+        writeFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name), data,
+                /* syncParentDir= */ false);
     }
 
     public byte[] readSyntheticPasswordState(int userId, long protectorId, String name) {
@@ -393,6 +408,13 @@
         deleteFile(getSyntheticPasswordStateFileForUser(userId, protectorId, name));
     }
 
+    /**
+     * Ensures that all synthetic password state files for the user have really been saved to disk.
+     */
+    public void syncSyntheticPasswordState(int userId) {
+        fsyncDirectory(getSyntheticPasswordDirectoryForUser(userId));
+    }
+
     public Map<Integer, List<Long>> listSyntheticPasswordProtectorsForAllUsers(String stateName) {
         Map<Integer, List<Long>> result = new ArrayMap<>();
         final UserManager um = UserManager.get(mContext);
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 3fd488e..73a16fd 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -619,12 +619,16 @@
 
     /**
      * Creates a new synthetic password (SP) for the given user.
-     *
+     * <p>
      * Any existing SID for the user is cleared.
-     *
+     * <p>
      * Also saves the escrow information necessary to re-generate the synthetic password under
      * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if
      * password escrow should be disabled completely on the given user.
+     * <p>
+     * {@link syncState()} is not called yet; the caller should create a protector afterwards, which
+     * handles this.  This makes it so that all the user's initial SP state files, including the
+     * initial LSKF-based protector, are efficiently created with only a single {@link syncState()}.
      */
     SyntheticPassword newSyntheticPassword(int userId) {
         clearSidForUser(userId);
@@ -668,6 +672,7 @@
 
     private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) {
         saveState(SP_HANDLE_NAME, spHandle, NULL_PROTECTOR_ID, userId);
+        syncState(userId);
     }
 
     private boolean loadEscrowData(SyntheticPassword sp, int userId) {
@@ -677,6 +682,11 @@
         return e0 != null && p1 != null;
     }
 
+    /**
+     * Saves the escrow data for the synthetic password.  The caller is responsible for calling
+     * {@link syncState()} afterwards, once the user's other initial synthetic password state files
+     * have been created.
+     */
     private void saveEscrowData(SyntheticPassword sp, int userId) {
         saveState(SP_E0_NAME, sp.mEncryptedEscrowSplit0, NULL_PROTECTOR_ID, userId);
         saveState(SP_P1_NAME, sp.mEscrowSplit1, NULL_PROTECTOR_ID, userId);
@@ -708,6 +718,10 @@
         return buffer.getInt();
     }
 
+    /**
+     * Creates a file that stores the Weaver slot the protector is using.  The caller is responsible
+     * for calling {@link syncState()} afterwards, once all the protector's files have been created.
+     */
     private void saveWeaverSlot(int slot, long protectorId, int userId) {
         ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES);
         buffer.put(WEAVER_VERSION);
@@ -837,6 +851,7 @@
         }
         createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret,
                 sid, userId);
+        syncState(userId); // ensure the new files are really saved to disk
         return protectorId;
     }
 
@@ -996,6 +1011,7 @@
         saveSecdiscardable(tokenHandle, tokenData.secdiscardableOnDisk, userId);
         createSyntheticPasswordBlob(tokenHandle, getTokenBasedProtectorType(tokenData.mType), sp,
                 tokenData.aggregatedSecret, 0L, userId);
+        syncState(userId); // ensure the new files are really saved to disk
         tokenMap.get(userId).remove(tokenHandle);
         if (tokenData.mCallback != null) {
             tokenData.mCallback.onEscrowTokenActivated(tokenHandle, userId);
@@ -1003,6 +1019,11 @@
         return true;
     }
 
+    /**
+     * Creates a synthetic password blob, i.e. the file that stores the encrypted synthetic password
+     * (or encrypted escrow secret) for a protector.  The caller is responsible for calling
+     * {@link syncState()} afterwards, once all the protector's files have been created.
+     */
     private void createSyntheticPasswordBlob(long protectorId, byte protectorType,
             SyntheticPassword sp, byte[] protectorSecret, long sid, int userId) {
         final byte[] spSecret;
@@ -1118,6 +1139,7 @@
                             // (getting rid of CREDENTIAL_TYPE_PASSWORD_OR_PIN)
                             pwd.credentialType = credential.getType();
                             saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
+                            syncState(userId);
                             synchronizeFrpPassword(pwd, 0, userId);
                         } else {
                             Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
@@ -1156,6 +1178,7 @@
         if (result.syntheticPassword != null && !credential.isNone() &&
                 !hasPasswordMetrics(protectorId, userId)) {
             savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId);
+            syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe.
         }
         return result;
     }
@@ -1275,6 +1298,7 @@
                     + blob.mProtectorType);
             createSyntheticPasswordBlob(protectorId, blob.mProtectorType, result, protectorSecret,
                     sid, userId);
+            syncState(userId); // Not strictly needed as the upgrade can be re-done, but be safe.
         }
         return result;
     }
@@ -1396,12 +1420,21 @@
         return ArrayUtils.concat(data, secdiscardable);
     }
 
+    /**
+     * Generates and writes the secdiscardable file for the given protector.  The caller is
+     * responsible for calling {@link syncState()} afterwards, once all the protector's files have
+     * been created.
+     */
     private byte[] createSecdiscardable(long protectorId, int userId) {
         byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
         saveSecdiscardable(protectorId, data, userId);
         return data;
     }
 
+    /**
+     * Writes the secdiscardable file for the given protector.  The caller is responsible for
+     * calling {@link syncState()} afterwards, once all the protector's files have been created.
+     */
     private void saveSecdiscardable(long protectorId, byte[] secdiscardable, int userId) {
         saveState(SECDISCARDABLE_NAME, secdiscardable, protectorId, userId);
     }
@@ -1445,6 +1478,11 @@
         return VersionedPasswordMetrics.deserialize(decrypted).getMetrics();
     }
 
+    /**
+     * Creates the password metrics file: the file associated with the LSKF-based protector that
+     * contains the encrypted metrics about the LSKF.  The caller is responsible for calling
+     * {@link syncState()} afterwards if needed.
+     */
     private void savePasswordMetrics(LockscreenCredential credential, SyntheticPassword sp,
             long protectorId, int userId) {
         final byte[] encrypted = SyntheticPasswordCrypto.encrypt(sp.deriveMetricsKey(),
@@ -1466,10 +1504,21 @@
         return mStorage.readSyntheticPasswordState(userId, protectorId, stateName);
     }
 
+    /**
+     * Persists the given synthetic password state for the given user ID and protector ID.
+     * <p>
+     * For performance reasons, this doesn't sync the user's synthetic password state directory.  As
+     * a result, it doesn't guarantee that the file will really be present after a crash.  If that
+     * is needed, call {@link syncState()} afterwards, preferably after batching up related updates.
+     */
     private void saveState(String stateName, byte[] data, long protectorId, int userId) {
         mStorage.writeSyntheticPasswordState(userId, protectorId, stateName, data);
     }
 
+    private void syncState(int userId) {
+        mStorage.syncSyntheticPasswordState(userId);
+    }
+
     private void destroyState(String stateName, long protectorId, int userId) {
         mStorage.deleteSyntheticPasswordState(userId, protectorId, stateName);
     }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
index 0c209c5..2596cee 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
@@ -45,9 +45,10 @@
 import android.security.keystore.recovery.KeyDerivationParams;
 import android.security.keystore.recovery.WrappedApplicationKey;
 import android.util.Base64;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
index eb34e98..9e3da23 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
@@ -46,9 +46,10 @@
 import android.security.keystore.recovery.KeyDerivationParams;
 import android.security.keystore.recovery.WrappedApplicationKey;
 import android.util.Base64;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.security.cert.CertPath;
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index fdc5bab..497ed03 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -41,7 +42,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ILogAccessDialogCallback;
-import com.android.internal.app.LogAccessDialogActivity;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -62,6 +62,10 @@
 public final class LogcatManagerService extends SystemService {
     private static final String TAG = "LogcatManagerService";
     private static final boolean DEBUG = false;
+    private static final String TARGET_PACKAGE_NAME = "com.android.systemui";
+    private static final String TARGET_ACTIVITY_NAME =
+            "com.android.systemui.logcat.LogAccessDialogActivity";
+    public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
 
     /** How long to wait for the user to approve/decline before declining automatically */
     @VisibleForTesting
@@ -442,6 +446,7 @@
                 mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS);
         final Intent mIntent = createIntent(client);
         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mIntent.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME));
         mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
     }
 
@@ -536,13 +541,13 @@
      * Create the Intent for LogAccessDialogActivity.
      */
     public Intent createIntent(LogAccessClient client) {
-        final Intent intent = new Intent(mContext, LogAccessDialogActivity.class);
+        final Intent intent = new Intent();
 
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName);
         intent.putExtra(Intent.EXTRA_UID, client.mUid);
-        intent.putExtra(LogAccessDialogActivity.EXTRA_CALLBACK, mDialogCallback.asBinder());
+        intent.putExtra(EXTRA_CALLBACK, mDialogCallback.asBinder());
 
         return intent;
     }
diff --git a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
index dcdb881..72ce38b 100644
--- a/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
+++ b/services/core/java/com/android/server/media/MediaButtonReceiverHolder.java
@@ -275,6 +275,10 @@
                 String.valueOf(mComponentType));
     }
 
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
     @ComponentType
     private static int getComponentType(PendingIntent pendingIntent) {
         if (pendingIntent.isBroadcast()) {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 5f06ca9..f5ec880 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -131,8 +131,6 @@
                             UserHandler::updateDiscoveryPreferenceOnHandler, userHandler));
                 }
             }
-
-            mEventLogger.log(new EventLogger.StringEvent("mScreenOnOffReceiver", null));
         }
     };
 
@@ -151,10 +149,7 @@
         mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter);
     }
 
-    ////////////////////////////////////////////////////////////////
-    ////  Calls from MediaRouter2
-    ////   - Should not have @NonNull/@Nullable on any arguments
-    ////////////////////////////////////////////////////////////////
+    // Start of methods that implement MediaRouter2 operations.
 
     @NonNull
     public void enforceMediaContentControlPermission() {
@@ -202,6 +197,383 @@
         }
     }
 
+    public void registerRouter2(@NonNull IMediaRouter2 router, @NonNull String packageName) {
+        Objects.requireNonNull(router, "router must not be null");
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName must not be empty");
+        }
+
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+        final boolean hasConfigureWifiDisplayPermission = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+                == PackageManager.PERMISSION_GRANTED;
+        final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+                == PackageManager.PERMISSION_GRANTED;
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                registerRouter2Locked(router, uid, pid, packageName, userId,
+                        hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void unregisterRouter2(@NonNull IMediaRouter2 router) {
+        Objects.requireNonNull(router, "router must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                unregisterRouter2Locked(router, false);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull RouteDiscoveryPreference preference) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(preference, "preference must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
+                if (routerRecord == null) {
+                    Slog.w(TAG, "Ignoring updating discoveryRequest of null routerRecord.");
+                    return;
+                }
+                setDiscoveryRequestWithRouter2Locked(routerRecord, preference);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void setRouteVolumeWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                setRouteVolumeWithRouter2Locked(router, route, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void requestCreateSessionWithRouter2(@NonNull IMediaRouter2 router, int requestId,
+            long managerRequestId, @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route, Bundle sessionHints) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(oldSession, "oldSession must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestCreateSessionWithRouter2Locked(requestId, managerRequestId,
+                        router, oldSession, route, sessionHints);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void selectRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                selectRouteWithRouter2Locked(router, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void deselectRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                deselectRouteWithRouter2Locked(router, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void transferToRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                transferToRouteWithRouter2Locked(router, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void setSessionVolumeWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, int volume) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                setSessionVolumeWithRouter2Locked(router, uniqueSessionId, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void releaseSessionWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId) {
+        Objects.requireNonNull(router, "router must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                releaseSessionWithRouter2Locked(router, uniqueSessionId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // End of methods that implement MediaRouter2 operations.
+
+    // Start of methods that implement MediaRouter2Manager operations.
+
+    @NonNull
+    public List<RoutingSessionInfo> getRemoteSessions(@NonNull IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                return getRemoteSessionsLocked(manager);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void registerManager(@NonNull IMediaRouter2Manager manager,
+            @NonNull String packageName) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(packageName)) {
+            throw new IllegalArgumentException("packageName must not be empty");
+        }
+
+        final int uid = Binder.getCallingUid();
+        final int pid = Binder.getCallingPid();
+        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                registerManagerLocked(manager, uid, pid, packageName, userId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void unregisterManager(@NonNull IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                unregisterManagerLocked(manager, false);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void startScan(@NonNull IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                startScanLocked(manager);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void stopScan(@NonNull IMediaRouter2Manager manager) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                stopScanLocked(manager);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void setRouteVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull MediaRoute2Info route, int volume) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                setRouteVolumeWithManagerLocked(requestId, manager, route, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void requestCreateSessionWithManager(@NonNull IMediaRouter2Manager manager,
+            int requestId, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        Objects.requireNonNull(oldSession, "oldSession must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void selectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                selectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void deselectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                deselectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void transferToRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                transferToRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void setSessionVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, int volume) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                setSessionVolumeWithManagerLocked(requestId, manager, uniqueSessionId, volume);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public void releaseSessionWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId) {
+        Objects.requireNonNull(manager, "manager must not be null");
+        if (TextUtils.isEmpty(uniqueSessionId)) {
+            throw new IllegalArgumentException("uniqueSessionId must not be empty");
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                releaseSessionWithManagerLocked(requestId, manager, uniqueSessionId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // End of methods that implement MediaRouter2Manager operations.
+
+    // Start of methods that implements operations for both MediaRouter2 and MediaRouter2Manager.
+
     @NonNull
     public RoutingSessionInfo getSystemSessionInfo(
             @Nullable String packageName, boolean setDeviceRouteSelected) {
@@ -242,376 +614,7 @@
         }
     }
 
-    public void registerRouter2(IMediaRouter2 router, String packageName) {
-        Objects.requireNonNull(router, "router must not be null");
-        if (TextUtils.isEmpty(packageName)) {
-            throw new IllegalArgumentException("packageName must not be empty");
-        }
-
-        final int uid = Binder.getCallingUid();
-        final int pid = Binder.getCallingPid();
-        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
-        final boolean hasConfigureWifiDisplayPermission = mContext.checkCallingOrSelfPermission(
-                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
-                == PackageManager.PERMISSION_GRANTED;
-        final boolean hasModifyAudioRoutingPermission = mContext.checkCallingOrSelfPermission(
-                android.Manifest.permission.MODIFY_AUDIO_ROUTING)
-                == PackageManager.PERMISSION_GRANTED;
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                registerRouter2Locked(router, uid, pid, packageName, userId,
-                        hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void unregisterRouter2(IMediaRouter2 router) {
-        Objects.requireNonNull(router, "router must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                unregisterRouter2Locked(router, false);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
-            RouteDiscoveryPreference preference) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(preference, "preference must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
-                if (routerRecord == null) {
-                    Slog.w(TAG, "Ignoring updating discoveryRequest of null routerRecord.");
-                    return;
-                }
-                setDiscoveryRequestWithRouter2Locked(routerRecord, preference);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void setRouteVolumeWithRouter2(IMediaRouter2 router,
-            MediaRoute2Info route, int volume) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                setRouteVolumeWithRouter2Locked(router, route, volume);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId,
-            long managerRequestId, RoutingSessionInfo oldSession,
-            MediaRoute2Info route, Bundle sessionHints) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(oldSession, "oldSession must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                requestCreateSessionWithRouter2Locked(requestId, managerRequestId,
-                        router, oldSession, route, sessionHints);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void selectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                selectRouteWithRouter2Locked(router, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void deselectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                deselectRouteWithRouter2Locked(router, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void transferToRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                transferToRouteWithRouter2Locked(router, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void setSessionVolumeWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            int volume) {
-        Objects.requireNonNull(router, "router must not be null");
-        Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                setSessionVolumeWithRouter2Locked(router, uniqueSessionId, volume);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void releaseSessionWithRouter2(IMediaRouter2 router, String uniqueSessionId) {
-        Objects.requireNonNull(router, "router must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                releaseSessionWithRouter2Locked(router, uniqueSessionId);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    ////////////////////////////////////////////////////////////////
-    ////  Calls from MediaRouter2Manager
-    ////   - Should not have @NonNull/@Nullable on any arguments
-    ////////////////////////////////////////////////////////////////
-
-    @NonNull
-    public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                return getRemoteSessionsLocked(manager);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void registerManager(IMediaRouter2Manager manager, String packageName) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(packageName)) {
-            throw new IllegalArgumentException("packageName must not be empty");
-        }
-
-        final int uid = Binder.getCallingUid();
-        final int pid = Binder.getCallingPid();
-        final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                registerManagerLocked(manager, uid, pid, packageName, userId);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void unregisterManager(IMediaRouter2Manager manager) {
-        Objects.requireNonNull(manager, "manager must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                unregisterManagerLocked(manager, false);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void startScan(IMediaRouter2Manager manager) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                startScanLocked(manager);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void stopScan(IMediaRouter2Manager manager) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                stopScanLocked(manager);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId,
-            MediaRoute2Info route, int volume) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                setRouteVolumeWithManagerLocked(requestId, manager, route, volume);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId,
-            RoutingSessionInfo oldSession, MediaRoute2Info route) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        Objects.requireNonNull(oldSession, "oldSession must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void selectRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                selectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                deselectRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-        Objects.requireNonNull(route, "route must not be null");
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                transferToRouteWithManagerLocked(requestId, manager, uniqueSessionId, route);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void setSessionVolumeWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, int volume) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                setSessionVolumeWithManagerLocked(requestId, manager, uniqueSessionId, volume);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    public void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId) {
-        Objects.requireNonNull(manager, "manager must not be null");
-        if (TextUtils.isEmpty(uniqueSessionId)) {
-            throw new IllegalArgumentException("uniqueSessionId must not be empty");
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mLock) {
-                releaseSessionWithManagerLocked(requestId, manager, uniqueSessionId);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
+    // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager.
 
     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
         pw.println(prefix + "MediaRouter2ServiceImpl");
@@ -637,7 +640,7 @@
     /* package */ void updateRunningUserAndProfiles(int newActiveUserId) {
         synchronized (mLock) {
             if (mCurrentActiveUserId != newActiveUserId) {
-                mEventLogger.log(
+                mEventLogger.enqueue(
                         EventLogger.StringEvent.from("switchUser",
                                 "userId: %d", newActiveUserId));
 
@@ -681,10 +684,7 @@
         return mUserManagerInternal.getProfileParentId(userId) == mCurrentActiveUserId;
     }
 
-    ////////////////////////////////////////////////////////////////
-    ////  ***Locked methods related to MediaRouter2
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////////
+    // Start of locked methods that are used by MediaRouter2.
 
     @GuardedBy("mLock")
     private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid,
@@ -713,7 +713,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));
     }
@@ -726,7 +726,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from(
                         "unregisterRouter2",
                         "package: %s, router id: %d",
@@ -752,7 +752,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "setDiscoveryRequestWithRouter2",
                 "router id: %d, discovery request: %s",
                 routerRecord.mRouterId, discoveryRequest.toString()));
@@ -774,7 +774,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));
@@ -859,7 +859,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "selectRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -879,7 +879,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "deselectRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -899,7 +899,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "transferToRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -929,7 +929,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "setSessionVolumeWithRouter2",
                 "router id: %d, session: %s, volume: %d",
                 routerRecord.mRouterId,  uniqueSessionId, volume));
@@ -949,7 +949,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "releaseSessionWithRouter2",
                 "router id: %d, session: %s",
                 routerRecord.mRouterId,  uniqueSessionId));
@@ -960,10 +960,9 @@
                         DUMMY_REQUEST_ID, routerRecord, uniqueSessionId));
     }
 
-    ////////////////////////////////////////////////////////////
-    ////  ***Locked methods related to MediaRouter2Manager
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////
+    // End of locked methods that are used by MediaRouter2.
+
+    // Start of locked methods that are used by MediaRouter2Manager.
 
     private List<RoutingSessionInfo> getRemoteSessionsLocked(
             @NonNull IMediaRouter2Manager manager) {
@@ -996,7 +995,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("registerManager",
                         "uid: %d, pid: %d, package: %s, userId: %d",
                         uid, pid, packageName, userId));
@@ -1038,7 +1037,7 @@
         }
         UserRecord userRecord = managerRecord.mUserRecord;
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from(
                         "unregisterManager",
                         "package: %s, userId: %d, managerId: %d",
@@ -1058,7 +1057,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("startScan",
                         "manager: %d", managerRecord.mManagerId));
 
@@ -1072,7 +1071,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("stopScan",
                         "manager: %d", managerRecord.mManagerId));
 
@@ -1089,7 +1088,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("setRouteVolumeWithManager",
                         "managerId: %d, routeId: %s, volume: %d",
                         managerRecord.mManagerId, route.getId(), volume));
@@ -1102,14 +1101,14 @@
     }
 
     private void requestCreateSessionWithManagerLocked(int requestId,
-            @NonNull IMediaRouter2Manager manager,
-            @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+            @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route) {
         ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
         if (managerRecord == null) {
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("requestCreateSessionWithManager",
                         "managerId: %d, routeId: %s",
                         managerRecord.mManagerId, route.getId()));
@@ -1159,7 +1158,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("selectRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1185,7 +1184,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("deselectRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1211,7 +1210,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("transferToRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1237,7 +1236,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("setSessionVolumeWithManager",
                         "managerId: %d, session: %s, volume: %d",
                         managerRecord.mManagerId, uniqueSessionId, volume));
@@ -1250,8 +1249,7 @@
     }
 
     private void releaseSessionWithManagerLocked(int requestId,
-            @NonNull IMediaRouter2Manager manager,
-            @NonNull String uniqueSessionId) {
+            @NonNull IMediaRouter2Manager manager, @NonNull String uniqueSessionId) {
         final IBinder binder = manager.asBinder();
         ManagerRecord managerRecord = mAllManagerRecords.get(binder);
 
@@ -1259,7 +1257,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("releaseSessionWithManager",
                         "managerId: %d, session: %s",
                         managerRecord.mManagerId, uniqueSessionId));
@@ -1274,10 +1272,9 @@
                         uniqueRequestId, routerRecord, uniqueSessionId));
     }
 
-    ////////////////////////////////////////////////////////////
-    ////  ***Locked methods used by both router2 and manager
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////
+    // End of locked methods that are used by MediaRouter2Manager.
+
+    // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
 
     @GuardedBy("mLock")
     private UserRecord getOrCreateUserRecordLocked(int userId) {
@@ -1313,6 +1310,8 @@
         }
     }
 
+    // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
+
     static long toUniqueRequestId(int requesterId, int originalRequestId) {
         return ((long) requesterId << 32) | originalRequestId;
     }
@@ -1681,116 +1680,75 @@
         }
 
         private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) {
-            MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo();
-
+            MediaRoute2ProviderInfo newInfo = provider.getProviderInfo();
             int providerInfoIndex =
                     indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos);
-
-            MediaRoute2ProviderInfo prevInfo =
+            MediaRoute2ProviderInfo oldInfo =
                     providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex);
-
-            // Ignore if no changes
-            if (Objects.equals(prevInfo, currentInfo)) {
+            if (oldInfo == newInfo) {
+                // Nothing to do.
                 return;
             }
 
-            boolean hasAddedOrModifiedRoutes = false;
-            boolean hasRemovedRoutes = false;
-
-            boolean isSystemProvider = provider.mIsSystemRouteProvider;
-
-            if (prevInfo == null) {
-                // Provider is being added.
-                mLastProviderInfos.add(currentInfo);
-                addToRoutesMap(currentInfo.getRoutes(), isSystemProvider);
-                // Check if new provider exposes routes.
-                hasAddedOrModifiedRoutes = !currentInfo.getRoutes().isEmpty();
-            } else if (currentInfo == null) {
-                // Provider is being removed.
-                hasRemovedRoutes = true;
-                mLastProviderInfos.remove(prevInfo);
-                removeFromRoutesMap(prevInfo.getRoutes(), isSystemProvider);
-            } else {
-                // Provider is being updated.
-                mLastProviderInfos.set(providerInfoIndex, currentInfo);
-                final Collection<MediaRoute2Info> currentRoutes = currentInfo.getRoutes();
-
-                // Checking for individual routes.
-                for (MediaRoute2Info route : currentRoutes) {
-                    if (!route.isValid()) {
-                        Slog.w(
-                                TAG,
-                                "onProviderStateChangedOnHandler: Ignoring invalid route : "
-                                        + route);
-                        continue;
-                    }
-
-                    MediaRoute2Info prevRoute = prevInfo.getRoute(route.getOriginalId());
-                    if (prevRoute == null || !Objects.equals(prevRoute, route)) {
-                        hasAddedOrModifiedRoutes = true;
-                        mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route);
-                        if (!isSystemProvider) {
-                            mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route);
-                        }
-                    }
+            Collection<MediaRoute2Info> newRoutes;
+            Set<String> newRouteIds;
+            if (newInfo != null) {
+                // Adding or updating a provider.
+                newRoutes = newInfo.getRoutes();
+                newRouteIds =
+                        newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
+                if (providerInfoIndex >= 0) {
+                    mLastProviderInfos.set(providerInfoIndex, newInfo);
+                } else {
+                    mLastProviderInfos.add(newInfo);
                 }
+            } else /* newInfo == null */ {
+                // Removing a provider.
+                mLastProviderInfos.remove(oldInfo);
+                newRouteIds = Collections.emptySet();
+                newRoutes = Collections.emptySet();
+            }
 
-                // Checking for individual removals
-                for (MediaRoute2Info prevRoute : prevInfo.getRoutes()) {
-                    if (currentInfo.getRoute(prevRoute.getOriginalId()) == null) {
-                        hasRemovedRoutes = true;
-                        mLastNotifiedRoutesToPrivilegedRouters.remove(prevRoute.getId());
-                        if (!isSystemProvider) {
-                            mLastNotifiedRoutesToNonPrivilegedRouters.remove(prevRoute.getId());
-                        }
-                    }
+            // Add new routes to the maps.
+            boolean hasAddedOrModifiedRoutes = false;
+            for (MediaRoute2Info newRouteInfo : newRoutes) {
+                if (!newRouteInfo.isValid()) {
+                    Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : "
+                            + newRouteInfo);
+                    continue;
+                }
+                if (!provider.mIsSystemRouteProvider) {
+                    mLastNotifiedRoutesToNonPrivilegedRouters.put(
+                            newRouteInfo.getId(), newRouteInfo);
+                }
+                MediaRoute2Info oldRouteInfo =
+                        mLastNotifiedRoutesToPrivilegedRouters.put(
+                                newRouteInfo.getId(), newRouteInfo);
+                hasAddedOrModifiedRoutes |=
+                        oldRouteInfo == null || !oldRouteInfo.equals(newRouteInfo);
+            }
+
+            // Remove stale routes from the maps.
+            Collection<MediaRoute2Info> oldRoutes =
+                    oldInfo == null ? Collections.emptyList() : oldInfo.getRoutes();
+            boolean hasRemovedRoutes = false;
+            for (MediaRoute2Info oldRoute : oldRoutes) {
+                String oldRouteId = oldRoute.getId();
+                if (!newRouteIds.contains(oldRouteId)) {
+                    hasRemovedRoutes = true;
+                    mLastNotifiedRoutesToPrivilegedRouters.remove(oldRouteId);
+                    mLastNotifiedRoutesToNonPrivilegedRouters.remove(oldRouteId);
                 }
             }
 
             dispatchUpdates(
                     hasAddedOrModifiedRoutes,
                     hasRemovedRoutes,
-                    isSystemProvider,
+                    provider.mIsSystemRouteProvider,
                     mSystemProvider.getDefaultRoute());
         }
 
         /**
-         * Adds provided routes to {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also adds them
-         * to {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were provided by a
-         * non-system route provider. Overwrites any route with matching id that already exists.
-         *
-         * @param routes list of routes to be added.
-         * @param isSystemRoutes indicates whether routes come from a system route provider.
-         */
-        private void addToRoutesMap(
-                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) {
-            for (MediaRoute2Info route : routes) {
-                if (!isSystemRoutes) {
-                    mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route);
-                }
-                mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route);
-            }
-        }
-
-        /**
-         * Removes provided routes from {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also
-         * removes them from {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were
-         * provided by a non-system route provider.
-         *
-         * @param routes list of routes to be removed.
-         * @param isSystemRoutes whether routes come from a system route provider.
-         */
-        private void removeFromRoutesMap(
-                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) {
-            for (MediaRoute2Info route : routes) {
-                if (!isSystemRoutes) {
-                    mLastNotifiedRoutesToNonPrivilegedRouters.remove(route.getId());
-                }
-                mLastNotifiedRoutesToPrivilegedRouters.remove(route.getId());
-            }
-        }
-
-        /**
          * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters}
          * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link
          * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index d770f71..56f3296 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -239,8 +239,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.SparseSetArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -255,6 +253,8 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.StatLogger;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkIdentityUtils;
 import com.android.net.module.util.NetworkStatsUtils;
 import com.android.net.module.util.PermissionUtils;
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
index 4506b7d..7da78f3 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
@@ -20,14 +20,14 @@
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3238f1f..3329f54 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -36,10 +36,10 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
 import java.io.IOException;
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 4d55d4e..004caf3 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -60,14 +60,14 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.TriPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 import com.android.server.utils.TimingsTraceAndSlog;
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 77fea09..d6b9bd5 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -61,6 +61,7 @@
 import static android.content.pm.PackageManager.FEATURE_TELECOM;
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.content.pm.PackageManager.MATCH_ALL;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -257,8 +258,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 import android.view.accessibility.AccessibilityEvent;
@@ -290,6 +289,8 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.TriPredicate;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.EventLogTags;
 import com.android.server.IoThread;
@@ -3421,6 +3422,8 @@
         @Override
         public void setToastRateLimitingEnabled(boolean enable) {
 
+            super.setToastRateLimitingEnabled_enforcePermission();
+
             synchronized (mToastQueue) {
                 int uid = Binder.getCallingUid();
                 int userId = UserHandle.getUserId(uid);
@@ -3794,13 +3797,13 @@
         }
 
         private void createNotificationChannelsImpl(String pkg, int uid,
-                ParceledListSlice channelsList) {
-            createNotificationChannelsImpl(pkg, uid, channelsList,
+                ParceledListSlice channelsList, boolean fromTargetApp) {
+            createNotificationChannelsImpl(pkg, uid, channelsList, fromTargetApp,
                     ActivityTaskManager.INVALID_TASK_ID);
         }
 
         private void createNotificationChannelsImpl(String pkg, int uid,
-                ParceledListSlice channelsList, int startingTaskId) {
+                ParceledListSlice channelsList, boolean fromTargetApp, int startingTaskId) {
             List<NotificationChannel> channels = channelsList.getList();
             final int channelsSize = channels.size();
             ParceledListSlice<NotificationChannel> oldChannels =
@@ -3812,7 +3815,7 @@
                 final NotificationChannel channel = channels.get(i);
                 Objects.requireNonNull(channel, "channel in list is null");
                 needsPolicyFileChange = mPreferencesHelper.createNotificationChannel(pkg, uid,
-                        channel, true /* fromTargetApp */,
+                        channel, fromTargetApp,
                         mConditionProviders.isPackageOrComponentAllowed(
                                 pkg, UserHandle.getUserId(uid)));
                 if (needsPolicyFileChange) {
@@ -3848,6 +3851,7 @@
         @Override
         public void createNotificationChannels(String pkg, ParceledListSlice channelsList) {
             checkCallerIsSystemOrSameApp(pkg);
+            boolean fromTargetApp = !isCallerSystemOrPhone();  // if not system, it's from the app
             int taskId = ActivityTaskManager.INVALID_TASK_ID;
             try {
                 int uid = mPackageManager.getPackageUid(pkg, 0,
@@ -3856,14 +3860,15 @@
             } catch (RemoteException e) {
                 // Do nothing
             }
-            createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, taskId);
+            createNotificationChannelsImpl(pkg, Binder.getCallingUid(), channelsList, fromTargetApp,
+                    taskId);
         }
 
         @Override
         public void createNotificationChannelsForPackage(String pkg, int uid,
                 ParceledListSlice channelsList) {
             enforceSystemOrSystemUI("only system can call this");
-            createNotificationChannelsImpl(pkg, uid, channelsList);
+            createNotificationChannelsImpl(pkg, uid, channelsList, false /* fromTargetApp */);
         }
 
         @Override
@@ -3878,7 +3883,8 @@
                     CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId));
             conversationChannel.setConversationId(parentId, conversationId);
             createNotificationChannelsImpl(
-                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)));
+                    pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel)),
+                    false /* fromTargetApp */);
             mRankingHandler.requestSort();
             handleSavePolicyFile();
         }
@@ -4964,10 +4970,10 @@
             }
             enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule");
 
-            // If the caller is system, take the package name from the rule's owner rather than
-            // from the caller's package.
+            // If the calling app is the system (from any user), take the package name from the
+            // rule's owner rather than from the caller's package.
             String rulePkg = pkg;
-            if (isCallingUidSystem()) {
+            if (isCallingAppIdSystem()) {
                 if (automaticZenRule.getOwner() != null) {
                     rulePkg = automaticZenRule.getOwner().getPackageName();
                 }
@@ -9770,6 +9776,12 @@
         return uid == Process.SYSTEM_UID;
     }
 
+    protected boolean isCallingAppIdSystem() {
+        final int uid = Binder.getCallingUid();
+        final int appid = UserHandle.getAppId(uid);
+        return appid == Process.SYSTEM_UID;
+    }
+
     protected boolean isUidSystemOrPhone(int uid) {
         final int appid = UserHandle.getAppId(uid);
         return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID
@@ -10700,10 +10712,18 @@
         private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>();
         ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter>
                 mRequestedNotificationListeners = new ArrayMap<>();
+        private final boolean mIsHeadlessSystemUserMode;
 
         public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
                 IPackageManager pm) {
+            this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode());
+        }
+
+        @VisibleForTesting
+        public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
+                IPackageManager pm, boolean isHeadlessSystemUserMode) {
             super(context, lock, userProfiles, pm);
+            this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode;
         }
 
         @Override
@@ -10728,10 +10748,16 @@
                     if (TextUtils.isEmpty(listeners[i])) {
                         continue;
                     }
+                    int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
+                    // In the headless system user mode, packages might not be installed for the
+                    // system user. Match packages for any user since apps can be installed only for
+                    // non-system users and would be considering uninstalled for the system user.
+                    if (mIsHeadlessSystemUserMode) {
+                        packageQueryFlags += MATCH_ANY_USER;
+                    }
                     ArraySet<ComponentName> approvedListeners =
-                            this.queryPackageForServices(listeners[i],
-                                    MATCH_DIRECT_BOOT_AWARE
-                                            | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM);
+                            this.queryPackageForServices(listeners[i], packageQueryFlags,
+                                    USER_SYSTEM);
                     for (int k = 0; k < approvedListeners.size(); k++) {
                         ComponentName cn = approvedListeners.valueAt(k);
                         addDefaultComponentOrPackage(cn.flattenToString());
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index d8aa469..444fef6 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -63,8 +63,6 @@
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
@@ -73,6 +71,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.PermissionHelper.PackagePermission;
 
 import org.json.JSONArray;
@@ -852,7 +852,9 @@
         Objects.requireNonNull(pkg);
         Objects.requireNonNull(group);
         Objects.requireNonNull(group.getId());
-        Objects.requireNonNull(!TextUtils.isEmpty(group.getName()));
+        if (TextUtils.isEmpty(group.getName())) {
+            throw new IllegalArgumentException("group.getName() can't be empty");
+        }
         boolean needsDndChange = false;
         synchronized (mPackagePreferences) {
             PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
@@ -916,7 +918,7 @@
                 throw new IllegalArgumentException("Reserved id");
             }
             NotificationChannel existing = r.channels.get(channel.getId());
-            if (existing != null && fromTargetApp) {
+            if (existing != null) {
                 // Actually modifying an existing channel - keep most of the existing settings
                 if (existing.isDeleted()) {
                     // The existing channel was deleted - undelete it.
@@ -1002,9 +1004,7 @@
                 }
                 if (fromTargetApp) {
                     channel.setLockscreenVisibility(r.visibility);
-                    channel.setAllowBubbles(existing != null
-                            ? existing.getAllowBubbles()
-                            : NotificationChannel.DEFAULT_ALLOW_BUBBLE);
+                    channel.setAllowBubbles(NotificationChannel.DEFAULT_ALLOW_BUBBLE);
                 }
                 clearLockedFieldsLocked(channel);
 
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 61936df..4bbd40d 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -30,12 +30,12 @@
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.PackageManagerService;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 4c23ab8..4b2c88c 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -72,8 +72,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
@@ -82,6 +80,8 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import libcore.io.IoUtils;
diff --git a/services/core/java/com/android/server/oemlock/OemLockService.java b/services/core/java/com/android/server/oemlock/OemLockService.java
index 6735d55..bac8916 100644
--- a/services/core/java/com/android/server/oemlock/OemLockService.java
+++ b/services/core/java/com/android/server/oemlock/OemLockService.java
@@ -120,6 +120,8 @@
         @Nullable
         @EnforcePermission(MANAGE_CARRIER_OEM_UNLOCK_STATE)
         public String getLockName() {
+            super.getLockName_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 return mOemLock.getLockName();
@@ -131,6 +133,8 @@
         @Override
         @EnforcePermission(MANAGE_CARRIER_OEM_UNLOCK_STATE)
         public void setOemUnlockAllowedByCarrier(boolean allowed, @Nullable byte[] signature) {
+            super.setOemUnlockAllowedByCarrier_enforcePermission();
+
             enforceUserIsAdmin();
 
             final long token = Binder.clearCallingIdentity();
@@ -144,6 +148,8 @@
         @Override
         @EnforcePermission(MANAGE_CARRIER_OEM_UNLOCK_STATE)
         public boolean isOemUnlockAllowedByCarrier() {
+            super.isOemUnlockAllowedByCarrier_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
               return mOemLock.isOemUnlockAllowedByCarrier();
@@ -157,6 +163,8 @@
         @Override
         @EnforcePermission(MANAGE_USER_OEM_UNLOCK_STATE)
         public void setOemUnlockAllowedByUser(boolean allowedByUser) {
+            super.setOemUnlockAllowedByUser_enforcePermission();
+
             if (ActivityManager.isUserAMonkey()) {
                 // Prevent a monkey from changing this
                 return;
@@ -183,6 +191,8 @@
         @Override
         @EnforcePermission(MANAGE_USER_OEM_UNLOCK_STATE)
         public boolean isOemUnlockAllowedByUser() {
+            super.isOemUnlockAllowedByUser_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 return mOemLock.isOemUnlockAllowedByDevice();
@@ -199,6 +209,8 @@
         @Override
         @EnforcePermission(anyOf = {READ_OEM_UNLOCK_STATE, OEM_UNLOCK_STATE})
         public boolean isOemUnlockAllowed() {
+            super.isOemUnlockAllowed_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 boolean allowed = mOemLock.isOemUnlockAllowedByCarrier()
@@ -213,6 +225,8 @@
         @Override
         @EnforcePermission(anyOf = {READ_OEM_UNLOCK_STATE, OEM_UNLOCK_STATE})
         public boolean isDeviceOemUnlocked() {
+            super.isDeviceOemUnlocked_enforcePermission();
+
             String locked = SystemProperties.get(FLASH_LOCK_PROP);
             switch (locked) {
                 case FLASH_LOCK_UNLOCKED:
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index baa471c..3421eb7 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -55,10 +55,12 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.content.res.ApkAssets;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.FabricatedOverlayInternal;
@@ -880,14 +882,14 @@
                     }
                     Slog.d(TAG, "commit failed: " + e.getMessage(), e);
                     throw new SecurityException("commit failed"
-                            + (DEBUG ? ": " + e.getMessage() : ""));
+                            + (DEBUG || Build.IS_DEBUGGABLE ? ": " + e.getMessage() : ""));
                 }
             } finally {
                 traceEnd(TRACE_TAG_RRO);
             }
         }
 
-        private Set<PackageAndUser> executeRequest(
+        private Set<UserPackage> executeRequest(
                 @NonNull final OverlayManagerTransaction.Request request)
                 throws OperationFailedException {
             Objects.requireNonNull(request, "Transaction contains a null request");
@@ -932,7 +934,7 @@
             try {
                 switch (request.type) {
                     case TYPE_SET_ENABLED:
-                        Set<PackageAndUser> result = null;
+                        Set<UserPackage> result = null;
                         result = CollectionUtils.addAll(result,
                                 mImpl.setEnabled(request.overlay, true, realUserId));
                         result = CollectionUtils.addAll(result,
@@ -973,7 +975,7 @@
 
             synchronized (mLock) {
                 // execute the requests (as calling user)
-                Set<PackageAndUser> affectedPackagesToUpdate = null;
+                Set<UserPackage> affectedPackagesToUpdate = null;
                 for (final OverlayManagerTransaction.Request request : transaction) {
                     affectedPackagesToUpdate = CollectionUtils.addAll(affectedPackagesToUpdate,
                             executeRequest(request));
@@ -1370,13 +1372,13 @@
         }
     }
 
-    private void updateTargetPackagesLocked(@Nullable PackageAndUser updatedTarget) {
+    private void updateTargetPackagesLocked(@Nullable UserPackage updatedTarget) {
         if (updatedTarget != null) {
             updateTargetPackagesLocked(Set.of(updatedTarget));
         }
     }
 
-    private void updateTargetPackagesLocked(@Nullable Set<PackageAndUser> updatedTargets) {
+    private void updateTargetPackagesLocked(@Nullable Set<UserPackage> updatedTargets) {
         if (CollectionUtils.isEmpty(updatedTargets)) {
             return;
         }
@@ -1405,7 +1407,7 @@
 
     @Nullable
     private static SparseArray<ArraySet<String>> groupTargetsByUserId(
-            @Nullable final Set<PackageAndUser> targetsAndUsers) {
+            @Nullable final Set<UserPackage> targetsAndUsers) {
         final SparseArray<ArraySet<String>> userTargets = new SparseArray<>();
         CollectionUtils.forEach(targetsAndUsers, target -> {
             ArraySet<String> targets = userTargets.get(target.userId);
@@ -1472,7 +1474,7 @@
 
     @NonNull
     private SparseArray<List<String>> updatePackageManagerLocked(
-            @Nullable Set<PackageAndUser> targets) {
+            @Nullable Set<UserPackage> targets) {
         if (CollectionUtils.isEmpty(targets)) {
             return new SparseArray<>();
         }
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 8e672c3..6ffe60d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -35,6 +35,7 @@
 import android.content.om.CriticalOverlayInfo;
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.os.FabricatedOverlayInfo;
@@ -154,18 +155,26 @@
      * set of targets that had, but no longer have, active overlays.
      */
     @NonNull
-    ArraySet<PackageAndUser> updateOverlaysForUser(final int newUserId) {
+    ArraySet<UserPackage> updateOverlaysForUser(final int newUserId) {
         if (DEBUG) {
             Slog.d(TAG, "updateOverlaysForUser newUserId=" + newUserId);
         }
 
         // Remove the settings of all overlays that are no longer installed for this user.
-        final ArraySet<PackageAndUser> updatedTargets = new ArraySet<>();
+        final ArraySet<UserPackage> updatedTargets = new ArraySet<>();
         final ArrayMap<String, AndroidPackage> userPackages = mPackageManager.initializeForUser(
                 newUserId);
         CollectionUtils.addAll(updatedTargets, removeOverlaysForUser(
                 (info) -> !userPackages.containsKey(info.packageName), newUserId));
 
+        final ArraySet<String> overlaidByOthers = new ArraySet<>();
+        for (AndroidPackage androidPackage : userPackages.values()) {
+            final String overlayTarget = androidPackage.getOverlayTarget();
+            if (!TextUtils.isEmpty(overlayTarget)) {
+                overlaidByOthers.add(overlayTarget);
+            }
+        }
+
         // Update the state of all installed packages containing overlays, and initialize new
         // overlays that are not currently in the settings.
         for (int i = 0, n = userPackages.size(); i < n; i++) {
@@ -175,8 +184,10 @@
                         updatePackageOverlays(pkg, newUserId, 0 /* flags */));
 
                 // When a new user is switched to for the first time, package manager must be
-                // informed of the overlay paths for all packages installed in the user.
-                updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+                // informed of the overlay paths for all overlaid packages installed in the user.
+                if (overlaidByOthers.contains(pkg.getPackageName())) {
+                    updatedTargets.add(UserPackage.of(newUserId, pkg.getPackageName()));
+                }
             } catch (OperationFailedException e) {
                 Slog.e(TAG, "failed to initialize overlays of '" + pkg.getPackageName()
                         + "' for user " + newUserId + "", e);
@@ -227,7 +238,7 @@
                     mSettings.setEnabled(overlay, newUserId, true);
                     if (updateState(oi, newUserId, 0)) {
                         CollectionUtils.add(updatedTargets,
-                                new PackageAndUser(oi.targetPackageName, oi.userId));
+                                UserPackage.of(oi.userId, oi.targetPackageName));
                     }
                 }
             } catch (OverlayManagerSettings.BadKeyException e) {
@@ -248,40 +259,40 @@
     }
 
     @NonNull
-    Set<PackageAndUser> onPackageAdded(@NonNull final String pkgName,
+    Set<UserPackage> onPackageAdded(@NonNull final String pkgName,
             final int userId) throws OperationFailedException {
-        final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+        final Set<UserPackage> updatedTargets = new ArraySet<>();
         // Always update the overlays of newly added packages.
-        updatedTargets.add(new PackageAndUser(pkgName, userId));
+        updatedTargets.add(UserPackage.of(userId, pkgName));
         updatedTargets.addAll(reconcileSettingsForPackage(pkgName, userId, 0 /* flags */));
         return updatedTargets;
     }
 
     @NonNull
-    Set<PackageAndUser> onPackageChanged(@NonNull final String pkgName,
+    Set<UserPackage> onPackageChanged(@NonNull final String pkgName,
             final int userId) throws OperationFailedException {
         return reconcileSettingsForPackage(pkgName, userId, 0 /* flags */);
     }
 
     @NonNull
-    Set<PackageAndUser> onPackageReplacing(@NonNull final String pkgName, final int userId)
+    Set<UserPackage> onPackageReplacing(@NonNull final String pkgName, final int userId)
             throws OperationFailedException {
         return reconcileSettingsForPackage(pkgName, userId, FLAG_OVERLAY_IS_BEING_REPLACED);
     }
 
     @NonNull
-    Set<PackageAndUser> onPackageReplaced(@NonNull final String pkgName, final int userId)
+    Set<UserPackage> onPackageReplaced(@NonNull final String pkgName, final int userId)
             throws OperationFailedException {
         return reconcileSettingsForPackage(pkgName, userId, 0 /* flags */);
     }
 
     @NonNull
-    Set<PackageAndUser> onPackageRemoved(@NonNull final String pkgName, final int userId) {
+    Set<UserPackage> onPackageRemoved(@NonNull final String pkgName, final int userId) {
         if (DEBUG) {
             Slog.d(TAG, "onPackageRemoved pkgName=" + pkgName + " userId=" + userId);
         }
         // Update the state of all overlays that target this package.
-        final Set<PackageAndUser> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
+        final Set<UserPackage> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
 
         // Remove all the overlays this package declares.
         return CollectionUtils.addAll(targets,
@@ -289,15 +300,15 @@
     }
 
     @NonNull
-    private Set<PackageAndUser> removeOverlaysForUser(
+    private Set<UserPackage> removeOverlaysForUser(
             @NonNull final Predicate<OverlayInfo> condition, final int userId) {
         final List<OverlayInfo> overlays = mSettings.removeIf(
                 io -> userId == io.userId && condition.test(io));
-        Set<PackageAndUser> targets = Collections.emptySet();
+        Set<UserPackage> targets = Collections.emptySet();
         for (int i = 0, n = overlays.size(); i < n; i++) {
             final OverlayInfo info = overlays.get(i);
             targets = CollectionUtils.add(targets,
-                    new PackageAndUser(info.targetPackageName, userId));
+                    UserPackage.of(userId, info.targetPackageName));
 
             // Remove the idmap if the overlay is no longer installed for any user.
             removeIdmapIfPossible(info);
@@ -306,7 +317,7 @@
     }
 
     @NonNull
-    private Set<PackageAndUser> updateOverlaysForTarget(@NonNull final String targetPackage,
+    private Set<UserPackage> updateOverlaysForTarget(@NonNull final String targetPackage,
             final int userId, final int flags) {
         boolean modified = false;
         final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackage, userId);
@@ -322,18 +333,18 @@
         if (!modified) {
             return Collections.emptySet();
         }
-        return Set.of(new PackageAndUser(targetPackage, userId));
+        return Set.of(UserPackage.of(userId, targetPackage));
     }
 
     @NonNull
-    private Set<PackageAndUser> updatePackageOverlays(@NonNull AndroidPackage pkg,
+    private Set<UserPackage> updatePackageOverlays(@NonNull AndroidPackage pkg,
             final int userId, final int flags) throws OperationFailedException {
         if (pkg.getOverlayTarget() == null) {
             // This package does not have overlays declared in its manifest.
             return Collections.emptySet();
         }
 
-        Set<PackageAndUser> updatedTargets = Collections.emptySet();
+        Set<UserPackage> updatedTargets = Collections.emptySet();
         final OverlayIdentifier overlay = new OverlayIdentifier(pkg.getPackageName());
         final int priority = getPackageConfiguredPriority(pkg);
         try {
@@ -343,7 +354,7 @@
                     // If the targetPackageName has changed, the package that *used* to
                     // be the target must also update its assets.
                     updatedTargets = CollectionUtils.add(updatedTargets,
-                            new PackageAndUser(currentInfo.targetPackageName, userId));
+                            UserPackage.of(userId, currentInfo.targetPackageName));
                 }
 
                 currentInfo = mSettings.init(overlay, userId, pkg.getOverlayTarget(),
@@ -357,13 +368,13 @@
                 // reinitialized. Reorder the overlay and update its target package.
                 mSettings.setPriority(overlay, userId, priority);
                 updatedTargets = CollectionUtils.add(updatedTargets,
-                        new PackageAndUser(currentInfo.targetPackageName, userId));
+                        UserPackage.of(userId, currentInfo.targetPackageName));
             }
 
             // Update the enabled state of the overlay.
             if (updateState(currentInfo, userId, flags)) {
                 updatedTargets = CollectionUtils.add(updatedTargets,
-                        new PackageAndUser(currentInfo.targetPackageName, userId));
+                        UserPackage.of(userId, currentInfo.targetPackageName));
             }
         } catch (OverlayManagerSettings.BadKeyException e) {
             throw new OperationFailedException("failed to update settings", e);
@@ -372,14 +383,14 @@
     }
 
     @NonNull
-    private Set<PackageAndUser> reconcileSettingsForPackage(@NonNull final String pkgName,
+    private Set<UserPackage> reconcileSettingsForPackage(@NonNull final String pkgName,
             final int userId, final int flags) throws OperationFailedException {
         if (DEBUG) {
             Slog.d(TAG, "reconcileSettingsForPackage pkgName=" + pkgName + " userId=" + userId);
         }
 
         // Update the state of overlays that target this package.
-        Set<PackageAndUser> updatedTargets = Collections.emptySet();
+        Set<UserPackage> updatedTargets = Collections.emptySet();
         updatedTargets = CollectionUtils.addAll(updatedTargets,
                 updateOverlaysForTarget(pkgName, userId, flags));
 
@@ -413,7 +424,7 @@
     }
 
     @NonNull
-    Set<PackageAndUser> setEnabled(@NonNull final OverlayIdentifier overlay,
+    Set<UserPackage> setEnabled(@NonNull final OverlayIdentifier overlay,
             final boolean enable, final int userId) throws OperationFailedException {
         if (DEBUG) {
             Slog.d(TAG, String.format("setEnabled overlay=%s enable=%s userId=%d",
@@ -432,7 +443,7 @@
             modified |= updateState(oi, userId, 0);
 
             if (modified) {
-                return Set.of(new PackageAndUser(oi.targetPackageName, userId));
+                return Set.of(UserPackage.of(userId, oi.targetPackageName));
             }
             return Set.of();
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -440,7 +451,7 @@
         }
     }
 
-    Optional<PackageAndUser> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
+    Optional<UserPackage> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
             boolean withinCategory, final int userId) throws OperationFailedException {
         if (DEBUG) {
             Slog.d(TAG, String.format("setEnabledExclusive overlay=%s"
@@ -483,7 +494,7 @@
             modified |= updateState(enabledInfo, userId, 0);
 
             if (modified) {
-                return Optional.of(new PackageAndUser(enabledInfo.targetPackageName, userId));
+                return Optional.of(UserPackage.of(userId, enabledInfo.targetPackageName));
             }
             return Optional.empty();
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -492,7 +503,7 @@
     }
 
     @NonNull
-    Set<PackageAndUser> registerFabricatedOverlay(
+    Set<UserPackage> registerFabricatedOverlay(
             @NonNull final FabricatedOverlayInternal overlay)
             throws OperationFailedException {
         if (FrameworkParsingPackageUtils.validateName(overlay.overlayName,
@@ -506,7 +517,7 @@
             throw new OperationFailedException("failed to create fabricated overlay");
         }
 
-        final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+        final Set<UserPackage> updatedTargets = new ArraySet<>();
         for (int userId : mSettings.getUsers()) {
             updatedTargets.addAll(registerFabricatedOverlay(info, userId));
         }
@@ -514,13 +525,13 @@
     }
 
     @NonNull
-    private Set<PackageAndUser> registerFabricatedOverlay(
+    private Set<UserPackage> registerFabricatedOverlay(
             @NonNull final FabricatedOverlayInfo info, int userId)
             throws OperationFailedException {
         final OverlayIdentifier overlayIdentifier = new OverlayIdentifier(
                 info.packageName, info.overlayName);
 
-        final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+        final Set<UserPackage> updatedTargets = new ArraySet<>();
         OverlayInfo oi = mSettings.getNullableOverlayInfo(overlayIdentifier, userId);
         if (oi != null) {
             if (!oi.isFabricated) {
@@ -533,7 +544,7 @@
                 if (oi != null) {
                     // If the fabricated overlay changes its target package, update the previous
                     // target package so it no longer is overlaid.
-                    updatedTargets.add(new PackageAndUser(oi.targetPackageName, userId));
+                    updatedTargets.add(UserPackage.of(userId, oi.targetPackageName));
                 }
                 oi = mSettings.init(overlayIdentifier, userId, info.targetPackageName,
                         info.targetOverlayable, info.path, true, false,
@@ -544,7 +555,7 @@
                 mSettings.setBaseCodePath(overlayIdentifier, userId, info.path);
             }
             if (updateState(oi, userId, 0)) {
-                updatedTargets.add(new PackageAndUser(oi.targetPackageName, userId));
+                updatedTargets.add(UserPackage.of(userId, oi.targetPackageName));
             }
         } catch (OverlayManagerSettings.BadKeyException e) {
             throw new OperationFailedException("failed to update settings", e);
@@ -554,8 +565,8 @@
     }
 
     @NonNull
-    Set<PackageAndUser> unregisterFabricatedOverlay(@NonNull final OverlayIdentifier overlay) {
-        final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+    Set<UserPackage> unregisterFabricatedOverlay(@NonNull final OverlayIdentifier overlay) {
+        final Set<UserPackage> updatedTargets = new ArraySet<>();
         for (int userId : mSettings.getUsers()) {
             updatedTargets.addAll(unregisterFabricatedOverlay(overlay, userId));
         }
@@ -563,7 +574,7 @@
     }
 
     @NonNull
-    private Set<PackageAndUser> unregisterFabricatedOverlay(
+    private Set<UserPackage> unregisterFabricatedOverlay(
             @NonNull final OverlayIdentifier overlay, int userId) {
         final OverlayInfo oi = mSettings.getNullableOverlayInfo(overlay, userId);
         if (oi != null) {
@@ -571,7 +582,7 @@
             if (oi.isEnabled()) {
                 // Removing a fabricated overlay only changes the overlay path of a package if it is
                 // currently enabled.
-                return Set.of(new PackageAndUser(oi.targetPackageName, userId));
+                return Set.of(UserPackage.of(userId, oi.targetPackageName));
             }
         }
         return Set.of();
@@ -617,7 +628,7 @@
         return mOverlayConfig.isEnabled(overlay.getPackageName());
     }
 
-    Optional<PackageAndUser> setPriority(@NonNull final OverlayIdentifier overlay,
+    Optional<UserPackage> setPriority(@NonNull final OverlayIdentifier overlay,
             @NonNull final OverlayIdentifier newParentOverlay, final int userId)
             throws OperationFailedException {
         try {
@@ -634,7 +645,7 @@
             }
 
             if (mSettings.setPriority(overlay, newParentOverlay, userId)) {
-                return Optional.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+                return Optional.of(UserPackage.of(userId, overlayInfo.targetPackageName));
             }
             return Optional.empty();
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -642,7 +653,7 @@
         }
     }
 
-    Set<PackageAndUser> setHighestPriority(@NonNull final OverlayIdentifier overlay,
+    Set<UserPackage> setHighestPriority(@NonNull final OverlayIdentifier overlay,
             final int userId) throws OperationFailedException {
         try{
             if (DEBUG) {
@@ -657,7 +668,7 @@
             }
 
             if (mSettings.setHighestPriority(overlay, userId)) {
-                return Set.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+                return Set.of(UserPackage.of(userId, overlayInfo.targetPackageName));
             }
             return Set.of();
         } catch (OverlayManagerSettings.BadKeyException e) {
@@ -665,7 +676,7 @@
         }
     }
 
-    Optional<PackageAndUser> setLowestPriority(@NonNull final OverlayIdentifier overlay,
+    Optional<UserPackage> setLowestPriority(@NonNull final OverlayIdentifier overlay,
             final int userId) throws OperationFailedException {
         try{
             if (DEBUG) {
@@ -680,7 +691,7 @@
             }
 
             if (mSettings.setLowestPriority(overlay, userId)) {
-                return Optional.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+                return Optional.of(UserPackage.of(userId, overlayInfo.targetPackageName));
             }
             return Optional.empty();
         } catch (OverlayManagerSettings.BadKeyException e) {
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index 9e39226..eae614a 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -28,14 +28,14 @@
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
index bb918d5..978e436 100644
--- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
+++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
@@ -29,15 +29,17 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.os.Binder;
+import android.os.ParcelFileDescriptor;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ShellCommand;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -63,7 +65,8 @@
     private final IOverlayManager mInterface;
     private static final Map<String, Integer> TYPE_MAP = Map.of(
             "color", TypedValue.TYPE_FIRST_COLOR_INT,
-            "string", TypedValue.TYPE_STRING);
+            "string", TypedValue.TYPE_STRING,
+            "drawable", -1);
 
     OverlayManagerShellCommand(@NonNull final Context ctx, @NonNull final IOverlayManager iom) {
         mContext = ctx;
@@ -257,7 +260,7 @@
         String name = "";
         String filename = null;
         String opt;
-        String configuration = null;
+        String config = null;
         while ((opt = getNextOption()) != null) {
             switch (opt) {
                 case "--user":
@@ -276,7 +279,7 @@
                     filename = getNextArgRequired();
                     break;
                 case "--config":
-                    configuration = getNextArgRequired();
+                    config = getNextArgRequired();
                     break;
                 default:
                     err.println("Error: Unknown option: " + opt);
@@ -311,7 +314,9 @@
             final String resourceName = getNextArgRequired();
             final String typeStr = getNextArgRequired();
             final String strData = String.join(" ", peekRemainingArgs());
-            addOverlayValue(overlayBuilder, resourceName, typeStr, strData, configuration);
+            if (addOverlayValue(overlayBuilder, resourceName, typeStr, strData, config) != 0) {
+                return 1;
+            }
         }
 
         mInterface.commit(new OverlayManagerTransaction.Builder()
@@ -368,8 +373,10 @@
                             return 1;
                         }
                         String config = parser.getAttributeValue(null, "config");
-                        addOverlayValue(overlayBuilder, targetPackage + ':' + target,
-                                overlayType, value, config);
+                        if (addOverlayValue(overlayBuilder, targetPackage + ':' + target,
+                                  overlayType, value, config) != 0) {
+                            return 1;
+                        }
                     }
                 }
             }
@@ -383,7 +390,7 @@
         return 0;
     }
 
-    private void addOverlayValue(FabricatedOverlay.Builder overlayBuilder,
+    private int addOverlayValue(FabricatedOverlay.Builder overlayBuilder,
             String resourceName, String typeString, String valueString, String configuration) {
         final int type;
         typeString = typeString.toLowerCase(Locale.getDefault());
@@ -398,6 +405,9 @@
         }
         if (type == TypedValue.TYPE_STRING) {
             overlayBuilder.setResourceValue(resourceName, type, valueString, configuration);
+        } else if (type < 0) {
+            ParcelFileDescriptor pfd =  openFileForSystem(valueString, "r");
+            overlayBuilder.setResourceValue(resourceName, pfd, configuration);
         } else {
             final int intData;
             if (valueString.startsWith("0x")) {
@@ -407,6 +417,7 @@
             }
             overlayBuilder.setResourceValue(resourceName, type, intData, configuration);
         }
+        return 0;
     }
 
     private int runEnableExclusive() throws RemoteException {
diff --git a/services/core/java/com/android/server/pm/ApexPackageInfo.java b/services/core/java/com/android/server/pm/ApexPackageInfo.java
deleted file mode 100644
index 672ae2e..0000000
--- a/services/core/java/com/android/server/pm/ApexPackageInfo.java
+++ /dev/null
@@ -1,419 +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 com.android.server.pm.ApexManager.MATCH_ACTIVE_PACKAGE;
-import static com.android.server.pm.ApexManager.MATCH_FACTORY_PACKAGE;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.apex.ApexInfo;
-import android.content.pm.PackageManager;
-import android.util.ArrayMap;
-import android.util.Pair;
-import android.util.PrintWriterPrinter;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.Preconditions;
-import com.android.server.pm.parsing.PackageParser2;
-import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
-import com.android.server.pm.parsing.pkg.ParsedPackage;
-import com.android.server.pm.pkg.AndroidPackage;
-import com.android.server.pm.pkg.PackageStateInternal;
-import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
-
-import java.io.File;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-
-/**
- * A temporary holder to store PackageInfo for scanned apex packages. We will unify the scan/install
- * flows of APK and APEX and PMS will be the only source of truth for all package information
- * including both APK and APEX. This class will no longer be needed when the migration is done.
- */
-class ApexPackageInfo {
-    public static final boolean ENABLE_FEATURE_SCAN_APEX = true;
-
-    private static final String TAG = "ApexManager";
-    private static final String VNDK_APEX_MODULE_NAME_PREFIX = "com.android.vndk.";
-
-    private final Object mLock = new Object();
-
-    @GuardedBy("mLock")
-    private List<Pair<ApexInfo, AndroidPackage>> mAllPackagesCache;
-
-    @Nullable
-    private final PackageManagerService mPackageManager;
-
-    ApexPackageInfo() {
-        mPackageManager = null;
-    }
-
-    ApexPackageInfo(@NonNull PackageManagerService pms) {
-        mPackageManager = pms;
-    }
-
-    /**
-     * Called by package manager service to scan apex package files when device boots up.
-     *
-     * @param allPackages All apex packages to scan.
-     * @param packageParser The package parser to support apex package parsing and caching parsed
-     *                      results.
-     * @param executorService An executor to support parallel package parsing.
-     */
-    List<ApexManager.ScanResult> scanApexPackages(ApexInfo[] allPackages,
-            @NonNull PackageParser2 packageParser, @NonNull ExecutorService executorService) {
-        synchronized (mLock) {
-            return scanApexPackagesInternalLocked(allPackages, packageParser, executorService);
-        }
-    }
-
-    void notifyScanResult(List<ApexManager.ScanResult> scanResults) {
-        synchronized (mLock) {
-            notifyScanResultLocked(scanResults);
-        }
-    }
-
-    /**
-     * Retrieves information about an APEX package.
-     *
-     * @param packageName the package name to look for. Note that this is the package name reported
-     *                    in the APK container manifest (i.e. AndroidManifest.xml), which might
-     *                    differ from the one reported in the APEX manifest (i.e.
-     *                    apex_manifest.json).
-     * @param flags the type of package to return. This may match to active packages
-     *              and factory (pre-installed) packages.
-     * @return a PackageInfo object with the information about the package, or null if the package
-     *         is not found.
-     */
-    @Nullable
-    Pair<ApexInfo, AndroidPackage> getPackageInfo(String packageName,
-            @ApexManager.PackageInfoFlags int flags) {
-        synchronized (mLock) {
-            Preconditions.checkState(mAllPackagesCache != null,
-                    "APEX packages have not been scanned");
-            boolean matchActive = (flags & MATCH_ACTIVE_PACKAGE) != 0;
-            boolean matchFactory = (flags & MATCH_FACTORY_PACKAGE) != 0;
-            for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                final Pair<ApexInfo, AndroidPackage> pair = mAllPackagesCache.get(i);
-                var apexInfo = pair.first;
-                var pkg = pair.second;
-                if (!pkg.getPackageName().equals(packageName)) {
-                    continue;
-                }
-                if ((matchActive && apexInfo.isActive)
-                        || (matchFactory && apexInfo.isFactory)) {
-                    return pair;
-                }
-            }
-            return null;
-        }
-    }
-
-    /**
-     * Retrieves information about all active APEX packages.
-     *
-     * @return list containing information about different active packages.
-     */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getActivePackages() {
-        synchronized (mLock) {
-            Preconditions.checkState(mAllPackagesCache != null,
-                    "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> activePackages = new ArrayList<>();
-            for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.first.isActive) {
-                    activePackages.add(pair);
-                }
-            }
-            return activePackages;
-        }
-    }
-
-    /**
-     * Retrieves information about all pre-installed APEX packages.
-     *
-     * @return list containing information about different pre-installed packages.
-     */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getFactoryPackages() {
-        synchronized (mLock) {
-            Preconditions.checkState(mAllPackagesCache != null,
-                    "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> factoryPackages = new ArrayList<>();
-            for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.first.isFactory) {
-                    factoryPackages.add(pair);
-                }
-            }
-            return factoryPackages;
-        }
-    }
-
-    /**
-     * Retrieves information about all inactive APEX packages.
-     *
-     * @return list containing information about different inactive packages.
-     */
-    @NonNull
-    List<Pair<ApexInfo, AndroidPackage>> getInactivePackages() {
-        synchronized (mLock) {
-            Preconditions.checkState(mAllPackagesCache != null,
-                    "APEX packages have not been scanned");
-            final List<Pair<ApexInfo, AndroidPackage>> inactivePackages = new ArrayList<>();
-            for (int i = 0; i < mAllPackagesCache.size(); i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (!pair.first.isActive) {
-                    inactivePackages.add(pair);
-                }
-            }
-            return inactivePackages;
-        }
-    }
-
-    /**
-     * Checks if {@code packageName} is an apex package.
-     *
-     * @param packageName package to check.
-     * @return {@code true} if {@code packageName} is an apex package.
-     */
-    boolean isApexPackage(String packageName) {
-        synchronized (mLock) {
-            Preconditions.checkState(mAllPackagesCache != null,
-                    "APEX packages have not been scanned");
-            for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                final var pair = mAllPackagesCache.get(i);
-                if (pair.second.getPackageName().equals(packageName)) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Called to update cached PackageInfo when installing rebootless APEX.
-     */
-    void notifyPackageInstalled(ApexInfo apexInfo, PackageParser2 packageParser)
-            throws PackageManagerException {
-        final int flags = PackageManager.GET_META_DATA
-                | PackageManager.GET_SIGNING_CERTIFICATES
-                | PackageManager.GET_SIGNATURES;
-        final ParsedPackage parsedPackage = packageParser.parsePackage(
-                new File(apexInfo.modulePath), flags, /* useCaches= */ false);
-        notifyPackageInstalled(apexInfo, parsedPackage.hideAsFinal());
-    }
-
-    void notifyPackageInstalled(ApexInfo apexInfo, AndroidPackage pkg) {
-        final String packageName = pkg.getPackageName();
-        synchronized (mLock) {
-            for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) {
-                var pair = mAllPackagesCache.get(i);
-                var oldApexInfo = pair.first;
-                var oldApexPkg = pair.second;
-                if (oldApexInfo.isActive && oldApexPkg.getPackageName().equals(packageName)) {
-                    if (oldApexInfo.isFactory) {
-                        oldApexInfo.isActive = false;
-                        mAllPackagesCache.add(Pair.create(apexInfo, pkg));
-                    } else {
-                        mAllPackagesCache.set(i, Pair.create(apexInfo, pkg));
-                    }
-                    break;
-                }
-            }
-        }
-    }
-
-    /**
-     * Dumps various state information to the provided {@link PrintWriter} object.
-     *
-     * @param pw the {@link PrintWriter} object to send information to.
-     * @param packageName a {@link String} containing a package name, or {@code null}. If set, only
-     *                    information about that specific package will be dumped.
-     */
-    void dump(PrintWriter pw, @Nullable String packageName) {
-        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
-        synchronized (mLock) {
-            if (mAllPackagesCache == null) {
-                ipw.println("APEX packages have not been scanned");
-                return;
-            }
-        }
-        ipw.println("Active APEX packages:");
-        dumpPackages(getActivePackages(), packageName, ipw);
-        ipw.println("Inactive APEX packages:");
-        dumpPackages(getInactivePackages(), packageName, ipw);
-        ipw.println("Factory APEX packages:");
-        dumpPackages(getFactoryPackages(), packageName, ipw);
-    }
-
-    @GuardedBy("mLock")
-    private void notifyScanResultLocked(List<ApexManager.ScanResult> scanResults) {
-        mAllPackagesCache = new ArrayList<>();
-        final int flags = PackageManager.GET_META_DATA
-                | PackageManager.GET_SIGNING_CERTIFICATES
-                | PackageManager.GET_SIGNATURES;
-
-        HashSet<String> activePackagesSet = new HashSet<>();
-        HashSet<String> factoryPackagesSet = new HashSet<>();
-        for (ApexManager.ScanResult result : scanResults) {
-            ApexInfo ai = result.apexInfo;
-            String packageName = result.pkg.getPackageName();
-            if (!packageName.equals(result.packageName)) {
-                throw new IllegalStateException("Unmatched package name: "
-                        + result.packageName + " != " + packageName
-                        + ", path=" + ai.modulePath);
-            }
-            mAllPackagesCache.add(Pair.create(ai, result.pkg));
-            if (ai.isActive) {
-                if (!activePackagesSet.add(packageName)) {
-                    throw new IllegalStateException(
-                            "Two active packages have the same name: " + packageName);
-                }
-            }
-            if (ai.isFactory) {
-                // Don't throw when the duplicating APEX is VNDK APEX
-                if (!factoryPackagesSet.add(packageName)
-                        && !ai.moduleName.startsWith(VNDK_APEX_MODULE_NAME_PREFIX)) {
-                    throw new IllegalStateException(
-                            "Two factory packages have the same name: " + packageName);
-                }
-            }
-        }
-    }
-
-    @GuardedBy("mLock")
-    private List<ApexManager.ScanResult> scanApexPackagesInternalLocked(final ApexInfo[] allPkgs,
-            PackageParser2 packageParser, ExecutorService executorService) {
-        if (allPkgs == null || allPkgs.length == 0) {
-            notifyScanResultLocked(Collections.EMPTY_LIST);
-            return Collections.EMPTY_LIST;
-        }
-
-        ArrayMap<File, ApexInfo> parsingApexInfo = new ArrayMap<>();
-        ParallelPackageParser parallelPackageParser =
-                new ParallelPackageParser(packageParser, executorService);
-        for (ApexInfo ai : allPkgs) {
-            File apexFile = new File(ai.modulePath);
-            parallelPackageParser.submit(apexFile,
-                    ParsingPackageUtils.PARSE_COLLECT_CERTIFICATES);
-            parsingApexInfo.put(apexFile, ai);
-        }
-
-        List<ApexManager.ScanResult> results = new ArrayList<>(parsingApexInfo.size());
-        // Process results one by one
-        for (int i = 0; i < parsingApexInfo.size(); i++) {
-            ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
-            Throwable throwable = parseResult.throwable;
-            ApexInfo ai = parsingApexInfo.get(parseResult.scanFile);
-
-            if (throwable == null) {
-                // TODO: When ENABLE_FEATURE_SCAN_APEX is finalized, remove this and the entire
-                //  calling path code
-                ScanPackageUtils.applyPolicy(parseResult.parsedPackage,
-                        PackageManagerService.SCAN_AS_SYSTEM,
-                        mPackageManager == null ? null : mPackageManager.getPlatformPackage(),
-                        false);
-                // Calling hideAsFinal to assign derived fields for the app info flags.
-                AndroidPackage finalPkg = parseResult.parsedPackage.hideAsFinal();
-                results.add(new ApexManager.ScanResult(ai, finalPkg, finalPkg.getPackageName()));
-            } else if (throwable instanceof PackageManagerException) {
-                throw new IllegalStateException("Unable to parse: " + ai.modulePath, throwable);
-            } else {
-                throw new IllegalStateException("Unexpected exception occurred while parsing "
-                        + ai.modulePath, throwable);
-            }
-        }
-
-        notifyScanResultLocked(results);
-        return results;
-    }
-
-    /**
-     * @see #dumpPackages(List, String, IndentingPrintWriter)
-     */
-    static void dumpPackageStates(List<PackageStateInternal> packageStates, boolean isActive,
-            @Nullable String packageName, IndentingPrintWriter ipw) {
-        ipw.println();
-        ipw.increaseIndent();
-        for (int i = 0, size = packageStates.size(); i < size; i++) {
-            final var packageState = packageStates.get(i);
-            var pkg = packageState.getPkg();
-            if (packageName != null && !packageName.equals(pkg.getPackageName())) {
-                continue;
-            }
-            ipw.println(pkg.getPackageName());
-            ipw.increaseIndent();
-            ipw.println("Version: " + pkg.getLongVersionCode());
-            ipw.println("Path: " + pkg.getBaseApkPath());
-            ipw.println("IsActive: " + isActive);
-            ipw.println("IsFactory: " + !packageState.isUpdatedSystemApp());
-            ipw.println("ApplicationInfo: ");
-            ipw.increaseIndent();
-            // TODO: Dump the package manually
-            AndroidPackageUtils.generateAppInfoWithoutState(pkg)
-                    .dump(new PrintWriterPrinter(ipw), "");
-            ipw.decreaseIndent();
-            ipw.decreaseIndent();
-        }
-        ipw.decreaseIndent();
-        ipw.println();
-    }
-
-    /**
-     * Dump information about the packages contained in a particular cache
-     * @param packagesCache the cache to print information about.
-     * @param packageName a {@link String} containing a package name, or {@code null}. If set,
-     *                    only information about that specific package will be dumped.
-     * @param ipw the {@link IndentingPrintWriter} object to send information to.
-     */
-    static void dumpPackages(List<Pair<ApexInfo, AndroidPackage>> packagesCache,
-            @Nullable String packageName, IndentingPrintWriter ipw) {
-        ipw.println();
-        ipw.increaseIndent();
-        for (int i = 0, size = packagesCache.size(); i < size; i++) {
-            final var pair = packagesCache.get(i);
-            var apexInfo = pair.first;
-            var pkg = pair.second;
-            if (packageName != null && !packageName.equals(pkg.getPackageName())) {
-                continue;
-            }
-            ipw.println(pkg.getPackageName());
-            ipw.increaseIndent();
-            ipw.println("Version: " + pkg.getLongVersionCode());
-            ipw.println("Path: " + pkg.getBaseApkPath());
-            ipw.println("IsActive: " + apexInfo.isActive);
-            ipw.println("IsFactory: " + apexInfo.isFactory);
-            ipw.println("ApplicationInfo: ");
-            ipw.increaseIndent();
-            // TODO: Dump the package manually
-            AndroidPackageUtils.generateAppInfoWithoutState(pkg)
-                    .dump(new PrintWriterPrinter(ipw), "");
-            ipw.decreaseIndent();
-            ipw.decreaseIndent();
-        }
-        ipw.decreaseIndent();
-        ipw.println();
-    }
-}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index b8e1e9a..f3cfa95 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -61,7 +61,7 @@
 /**
  * Prepares app data for users
  */
-final class AppDataHelper {
+public class AppDataHelper {
     private static final boolean DEBUG_APP_DATA = false;
 
     private final PackageManagerService mPm;
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index 3b676c65..b4792c6 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -44,7 +44,6 @@
 import com.android.server.pm.snapshot.PackageDataSnapshot;
 import com.android.server.utils.SnapshotCache;
 import com.android.server.utils.Watched;
-import com.android.server.utils.WatchedArrayList;
 import com.android.server.utils.WatchedArrayMap;
 import com.android.server.utils.WatchedArraySet;
 import com.android.server.utils.WatchedSparseBooleanMatrix;
@@ -179,9 +178,9 @@
 
     @NonNull
     @Watched
-    protected WatchedArrayList<String> mProtectedBroadcasts;
+    protected WatchedArraySet<String> mProtectedBroadcasts;
     @NonNull
-    protected SnapshotCache<WatchedArrayList<String>> mProtectedBroadcastsSnapshot;
+    protected SnapshotCache<WatchedArraySet<String>> mProtectedBroadcastsSnapshot;
 
     /**
      * This structure maps uid -> uid and indicates whether access from the first should be
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index 4c21195..5b837f1 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -22,6 +22,15 @@
 import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED;
+import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED;
 import static com.android.server.pm.AppsFilterUtils.canQueryAsInstaller;
 import static com.android.server.pm.AppsFilterUtils.canQueryViaComponents;
 import static com.android.server.pm.AppsFilterUtils.canQueryViaPackage;
@@ -36,6 +45,7 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.UserInfo;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -49,6 +59,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.FgThread;
 import com.android.server.compat.CompatChange;
 import com.android.server.om.OverlayReferenceMapper;
@@ -62,7 +73,6 @@
 import com.android.server.utils.SnapshotCache;
 import com.android.server.utils.Watchable;
 import com.android.server.utils.WatchableImpl;
-import com.android.server.utils.WatchedArrayList;
 import com.android.server.utils.WatchedArraySet;
 import com.android.server.utils.WatchedSparseBooleanMatrix;
 import com.android.server.utils.WatchedSparseSetArray;
@@ -71,11 +81,8 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * Implementation of the methods that update the internal structures of AppsFilter. Because of the
@@ -103,7 +110,7 @@
      */
     @GuardedBy("mQueryableViaUsesPermissionLock")
     @NonNull
-    private HashMap<String, Set<Integer>> mPermissionToUids;
+    private final ArrayMap<String, ArraySet<Integer>> mPermissionToUids;
 
     /**
      * A cache that maps parsed {@link android.R.styleable#AndroidManifestUsesPermission
@@ -113,7 +120,7 @@
      */
     @GuardedBy("mQueryableViaUsesPermissionLock")
     @NonNull
-    private HashMap<String, Set<Integer>> mUsesPermissionToUids;
+    private final ArrayMap<String, ArraySet<Integer>> mUsesPermissionToUids;
 
     /**
      * Ensures an observer is in the list, exactly once. The observer cannot be null.  The
@@ -212,11 +219,11 @@
         mForceQueryable = new WatchedArraySet<>();
         mForceQueryableSnapshot = new SnapshotCache.Auto<>(
                 mForceQueryable, mForceQueryable, "AppsFilter.mForceQueryable");
-        mProtectedBroadcasts = new WatchedArrayList<>();
+        mProtectedBroadcasts = new WatchedArraySet<>();
         mProtectedBroadcastsSnapshot = new SnapshotCache.Auto<>(
                 mProtectedBroadcasts, mProtectedBroadcasts, "AppsFilter.mProtectedBroadcasts");
-        mPermissionToUids = new HashMap<>();
-        mUsesPermissionToUids = new HashMap<>();
+        mPermissionToUids = new ArrayMap<>();
+        mUsesPermissionToUids = new ArrayMap<>();
 
         mSnapshot = new SnapshotCache<AppsFilterSnapshot>(this, this) {
             @Override
@@ -351,8 +358,15 @@
             if (pkg == null) {
                 return;
             }
+            final long currentTimeUs = SystemClock.currentTimeMicro();
             updateEnabledState(pkg);
             mAppsFilter.updateShouldFilterCacheForPackage(snapshot, packageName);
+            mAppsFilter.logCacheUpdated(
+                    PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED,
+                    SystemClock.currentTimeMicro() - currentTimeUs,
+                    snapshot.getUserInfos().length,
+                    snapshot.getPackageStates().size(),
+                    pkg.getUid());
         }
 
         private void updateEnabledState(@NonNull AndroidPackage pkg) {
@@ -465,7 +479,8 @@
         mOverlayReferenceMapper.rebuildIfDeferred();
         mFeatureConfig.onSystemReady();
 
-        updateEntireShouldFilterCacheAsync(pmInternal);
+        updateEntireShouldFilterCacheAsync(pmInternal,
+                PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT);
     }
 
     /**
@@ -473,16 +488,23 @@
      *
      * @param newPkgSetting the new setting being added
      * @param isReplace     if the package is being replaced and may need extra cleanup.
+     * @param retainImplicitGrantOnReplace {@code true} to retain implicit grant access if
+     *                                     the package is being replaced.
      */
     public void addPackage(Computer snapshot, PackageStateInternal newPkgSetting,
-            boolean isReplace) {
+            boolean isReplace, boolean retainImplicitGrantOnReplace) {
+        final long currentTimeUs = SystemClock.currentTimeMicro();
+        final int logType = isReplace
+                ? PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED
+                : PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED;
         if (DEBUG_TRACING) {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "filter.addPackage");
         }
         try {
             if (isReplace) {
                 // let's first remove any prior rules for this package
-                removePackage(snapshot, newPkgSetting, true /*isReplace*/);
+                removePackageInternal(snapshot, newPkgSetting,
+                        true /*isReplace*/, retainImplicitGrantOnReplace);
             }
             final ArrayMap<String, ? extends PackageStateInternal> settings =
                     snapshot.getPackageStates();
@@ -508,6 +530,8 @@
                         }
                     }
                 }
+                logCacheUpdated(logType, SystemClock.currentTimeMicro() - currentTimeUs,
+                        users.length, settings.size(), newPkgSetting.getAppId());
             } else {
                 invalidateCache("addPackage: " + newPkgSetting.getPackageName());
             }
@@ -545,13 +569,17 @@
             return null;
         }
 
-        final boolean protectedBroadcastsChanged;
-        synchronized (mProtectedBroadcastsLock) {
-            protectedBroadcastsChanged =
-                    mProtectedBroadcasts.addAll(newPkg.getProtectedBroadcasts());
-        }
-        if (protectedBroadcastsChanged) {
-            mQueriesViaComponentRequireRecompute.set(true);
+        final List<String> newBroadcasts = newPkg.getProtectedBroadcasts();
+        if (newBroadcasts.size() != 0) {
+            final boolean protectedBroadcastsChanged;
+            synchronized (mProtectedBroadcastsLock) {
+                final int oldSize = mProtectedBroadcasts.size();
+                mProtectedBroadcasts.addAll(newBroadcasts);
+                protectedBroadcastsChanged = mProtectedBroadcasts.size() != oldSize;
+            }
+            if (protectedBroadcastsChanged) {
+                mQueriesViaComponentRequireRecompute.set(true);
+            }
         }
 
         final boolean newIsForceQueryable;
@@ -578,7 +606,10 @@
                     // Lookup in the mPermissionToUids cache if installed packages have
                     // defined this permission.
                     if (mPermissionToUids.containsKey(usesPermissionName)) {
-                        for (int targetAppId : mPermissionToUids.get(usesPermissionName)) {
+                        final ArraySet<Integer> permissionDefiners =
+                                mPermissionToUids.get(usesPermissionName);
+                        for (int j = 0; j < permissionDefiners.size(); j++) {
+                            final int targetAppId = permissionDefiners.valueAt(j);
                             if (targetAppId != newPkgSetting.getAppId()) {
                                 mQueryableViaUsesPermission.add(newPkgSetting.getAppId(),
                                         targetAppId);
@@ -588,7 +619,7 @@
                     // Record in mUsesPermissionToUids that a permission was requested
                     // by a new package
                     if (!mUsesPermissionToUids.containsKey(usesPermissionName)) {
-                        mUsesPermissionToUids.put(usesPermissionName, new HashSet<>());
+                        mUsesPermissionToUids.put(usesPermissionName, new ArraySet<>());
                     }
                     mUsesPermissionToUids.get(usesPermissionName).add(newPkgSetting.getAppId());
                 }
@@ -602,7 +633,10 @@
                     // Lookup in the mUsesPermissionToUids cache if installed packages have
                     // requested this permission.
                     if (mUsesPermissionToUids.containsKey(permissionName)) {
-                        for (int queryingAppId : mUsesPermissionToUids.get(permissionName)) {
+                        final ArraySet<Integer> permissionUsers = mUsesPermissionToUids.get(
+                                permissionName);
+                        for (int j = 0; j < permissionUsers.size(); j++) {
+                            final int queryingAppId = permissionUsers.valueAt(j);
                             if (queryingAppId != newPkgSetting.getAppId()) {
                                 mQueryableViaUsesPermission.add(queryingAppId,
                                         newPkgSetting.getAppId());
@@ -611,7 +645,7 @@
                     }
                     // Record in mPermissionToUids that a permission was defined by a new package
                     if (!mPermissionToUids.containsKey(permissionName)) {
-                        mPermissionToUids.put(permissionName, new HashSet<>());
+                        mPermissionToUids.put(permissionName, new ArraySet<>());
                     }
                     mPermissionToUids.get(permissionName).add(newPkgSetting.getAppId());
                 }
@@ -757,18 +791,19 @@
         }
     }
 
-    private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal) {
-        updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS);
+    private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal, int reason) {
+        updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS, reason);
     }
 
     private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal,
-            long delayMs) {
+            long delayMs, int reason) {
         mBackgroundHandler.postDelayed(() -> {
             if (!mCacheValid.compareAndSet(CACHE_INVALID, CACHE_VALID)) {
                 // Cache is already valid.
                 return;
             }
 
+            final long currentTimeUs = SystemClock.currentTimeMicro();
             final ArrayMap<String, AndroidPackage> packagesCache = new ArrayMap<>();
             final UserInfo[][] usersRef = new UserInfo[1][];
             final Computer snapshot = (Computer) pmInternal.snapshot();
@@ -787,11 +822,13 @@
 
             updateEntireShouldFilterCacheInner(snapshot, settings, usersRef[0], USER_ALL);
             onChanged();
+            logCacheRebuilt(reason, SystemClock.currentTimeMicro() - currentTimeUs,
+                    users.length, settings.size());
 
             if (!mCacheValid.compareAndSet(CACHE_VALID, CACHE_VALID)) {
                 Slog.i(TAG, "Cache invalidated while building, retrying.");
                 updateEntireShouldFilterCacheAsync(pmInternal,
-                        Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS));
+                        Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS), reason);
                 return;
             }
 
@@ -803,15 +840,27 @@
         if (!mCacheReady) {
             return;
         }
+        final long currentTimeUs = SystemClock.currentTimeMicro();
         updateEntireShouldFilterCache(snapshot, newUserId);
+        logCacheRebuilt(
+                PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED,
+                SystemClock.currentTimeMicro() - currentTimeUs,
+                snapshot.getUserInfos().length,
+                snapshot.getPackageStates().size());
     }
 
-    public void onUserDeleted(@UserIdInt int userId) {
+    public void onUserDeleted(Computer snapshot, @UserIdInt int userId) {
         if (!mCacheReady) {
             return;
         }
+        final long currentTimeUs = SystemClock.currentTimeMicro();
         removeShouldFilterCacheForUser(userId);
         onChanged();
+        logCacheRebuilt(
+                PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED,
+                SystemClock.currentTimeMicro() - currentTimeUs,
+                snapshot.getUserInfos().length,
+                snapshot.getPackageStates().size());
     }
 
     private void updateShouldFilterCacheForPackage(Computer snapshot,
@@ -976,13 +1025,31 @@
     }
 
     /**
-     * Equivalent to calling {@link #addPackage(Computer, PackageStateInternal, boolean)}
-     * with {@code isReplace} equal to {@code false}.
+     * Equivalent to calling {@link #addPackage(Computer, PackageStateInternal, boolean, boolean)}
+     * with {@code isReplace} and {@code retainImplicitGrantOnReplace} equal to {@code false}.
      *
-     * @see AppsFilterImpl#addPackage(Computer, PackageStateInternal, boolean)
+     * @see AppsFilterImpl#addPackage(Computer, PackageStateInternal, boolean, boolean)
      */
     public void addPackage(Computer snapshot, PackageStateInternal newPkgSetting) {
-        addPackage(snapshot, newPkgSetting, false /* isReplace */);
+        addPackage(snapshot, newPkgSetting, false /* isReplace */,
+                false /* retainImplicitGrantOnReplace */);
+    }
+
+    /**
+     * Removes a package for consideration when filtering visibility between apps.
+     *
+     * @param setting the setting of the package being removed.
+     */
+    public void removePackage(Computer snapshot, PackageStateInternal setting) {
+        final long currentTimeUs = SystemClock.currentTimeMicro();
+        removePackageInternal(snapshot, setting,
+                false /* isReplace */, false /* retainImplicitGrantOnReplace */);
+        logCacheUpdated(
+                PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED,
+                SystemClock.currentTimeMicro() - currentTimeUs,
+                snapshot.getUserInfos().length,
+                snapshot.getPackageStates().size(),
+                setting.getAppId());
     }
 
     /**
@@ -990,33 +1057,37 @@
      *
      * @param setting   the setting of the package being removed.
      * @param isReplace if the package is being replaced.
+     * @param retainImplicitGrantOnReplace {@code true} to retain implicit grant access if
+     *                                     the package is being replaced.
      */
-    public void removePackage(Computer snapshot, PackageStateInternal setting,
-            boolean isReplace) {
+    private void removePackageInternal(Computer snapshot, PackageStateInternal setting,
+            boolean isReplace, boolean retainImplicitGrantOnReplace) {
         final ArraySet<String> additionalChangedPackages;
         final ArrayMap<String, ? extends PackageStateInternal> settings =
                 snapshot.getPackageStates();
         final UserInfo[] users = snapshot.getUserInfos();
         final Collection<SharedUserSetting> sharedUserSettings = snapshot.getAllSharedUsers();
         final int userCount = users.length;
-        synchronized (mImplicitlyQueryableLock) {
-            for (int u = 0; u < userCount; u++) {
-                final int userId = users[u].id;
-                final int removingUid = UserHandle.getUid(userId, setting.getAppId());
-                mImplicitlyQueryable.remove(removingUid);
-                for (int i = mImplicitlyQueryable.size() - 1; i >= 0; i--) {
-                    mImplicitlyQueryable.remove(mImplicitlyQueryable.keyAt(i),
-                            removingUid);
-                }
+        if (!isReplace || !retainImplicitGrantOnReplace) {
+            synchronized (mImplicitlyQueryableLock) {
+                for (int u = 0; u < userCount; u++) {
+                    final int userId = users[u].id;
+                    final int removingUid = UserHandle.getUid(userId, setting.getAppId());
+                    mImplicitlyQueryable.remove(removingUid);
+                    for (int i = mImplicitlyQueryable.size() - 1; i >= 0; i--) {
+                        mImplicitlyQueryable.remove(mImplicitlyQueryable.keyAt(i),
+                                removingUid);
+                    }
 
-                if (isReplace) {
-                    continue;
-                }
+                    if (isReplace) {
+                        continue;
+                    }
 
-                mRetainedImplicitlyQueryable.remove(removingUid);
-                for (int i = mRetainedImplicitlyQueryable.size() - 1; i >= 0; i--) {
-                    mRetainedImplicitlyQueryable.remove(
-                            mRetainedImplicitlyQueryable.keyAt(i), removingUid);
+                    mRetainedImplicitlyQueryable.remove(removingUid);
+                    for (int i = mRetainedImplicitlyQueryable.size() - 1; i >= 0; i--) {
+                        mRetainedImplicitlyQueryable.remove(
+                                mRetainedImplicitlyQueryable.keyAt(i), removingUid);
+                    }
                 }
             }
         }
@@ -1084,7 +1155,12 @@
                 final ArrayList<String> protectedBroadcasts = new ArrayList<>(
                         mProtectedBroadcasts.untrackedStorage());
                 collectProtectedBroadcasts(settings, removingPackageName);
-                protectedBroadcastsChanged = !mProtectedBroadcasts.containsAll(protectedBroadcasts);
+                for (int i = 0; i < protectedBroadcasts.size(); ++i) {
+                    if (!mProtectedBroadcasts.contains(protectedBroadcasts.get(i))) {
+                        protectedBroadcastsChanged = true;
+                        break;
+                    }
+                }
             }
         }
 
@@ -1174,4 +1250,18 @@
             }
         }
     }
+
+    private void logCacheRebuilt(int eventId, long latency, int userCount, int packageCount) {
+        FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED,
+                eventId, latency, userCount, packageCount, mShouldFilterCache.size());
+    }
+
+    private void logCacheUpdated(int eventId, long latency, int userCount, int packageCount,
+            int appId) {
+        if (!mCacheReady) {
+            return;
+        }
+        FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED,
+                eventId, appId, latency, userCount, packageCount, mShouldFilterCache.size());
+    }
 }
diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java
index 7daa0b9..483fa8a 100644
--- a/services/core/java/com/android/server/pm/AppsFilterUtils.java
+++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java
@@ -29,7 +29,7 @@
 import com.android.server.pm.pkg.component.ParsedIntentInfo;
 import com.android.server.pm.pkg.component.ParsedMainComponent;
 import com.android.server.pm.pkg.component.ParsedProvider;
-import com.android.server.utils.WatchedArrayList;
+import com.android.server.utils.WatchedArraySet;
 
 import java.util.List;
 import java.util.Set;
@@ -45,7 +45,7 @@
 
     /** Returns true if the querying package may query for the potential target package */
     public static boolean canQueryViaComponents(AndroidPackage querying,
-            AndroidPackage potentialTarget, WatchedArrayList<String> protectedBroadcasts) {
+            AndroidPackage potentialTarget, WatchedArraySet<String> protectedBroadcasts) {
         if (!querying.getQueriesIntents().isEmpty()) {
             for (Intent intent : querying.getQueriesIntents()) {
                 if (matchesPackage(intent, potentialTarget, protectedBroadcasts)) {
@@ -117,7 +117,7 @@
     }
 
     private static boolean matchesPackage(Intent intent, AndroidPackage potentialTarget,
-            WatchedArrayList<String> protectedBroadcasts) {
+            WatchedArraySet<String> protectedBroadcasts) {
         if (matchesAnyComponents(
                 intent, potentialTarget.getServices(), null /*protectedBroadcasts*/)) {
             return true;
@@ -138,7 +138,7 @@
 
     private static boolean matchesAnyComponents(Intent intent,
             List<? extends ParsedMainComponent> components,
-            WatchedArrayList<String> protectedBroadcasts) {
+            WatchedArraySet<String> protectedBroadcasts) {
         for (int i = ArrayUtils.size(components) - 1; i >= 0; i--) {
             ParsedMainComponent component = components.get(i);
             if (!component.isExported()) {
@@ -152,7 +152,7 @@
     }
 
     private static boolean matchesAnyFilter(Intent intent, ParsedComponent component,
-            WatchedArrayList<String> protectedBroadcasts) {
+            WatchedArraySet<String> protectedBroadcasts) {
         List<ParsedIntentInfo> intents = component.getIntents();
         for (int i = ArrayUtils.size(intents) - 1; i >= 0; i--) {
             IntentFilter intentFilter = intents.get(i).getIntentFilter();
@@ -164,7 +164,7 @@
     }
 
     private static boolean matchesIntentFilter(Intent intent, IntentFilter intentFilter,
-            @Nullable WatchedArrayList<String> protectedBroadcasts) {
+            @Nullable WatchedArraySet<String> protectedBroadcasts) {
         return intentFilter.match(intent.getAction(), intent.getType(), intent.getScheme(),
                 intent.getData(), intent.getCategories(), "AppsFilter", true,
                 protectedBroadcasts != null ? protectedBroadcasts.untrackedStorage() : null) > 0;
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index e7412c5..d72aacc 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -152,8 +152,6 @@
 
     @GuardedBy("mLock") @Status private int mLastExecutionStatus = STATUS_OK;
 
-    @GuardedBy("mLock") private long mLastExecutionStartTimeMs;
-    @GuardedBy("mLock") private long mLastExecutionDurationIncludingSleepMs;
     @GuardedBy("mLock") private long mLastExecutionStartUptimeMs;
     @GuardedBy("mLock") private long mLastExecutionDurationMs;
 
@@ -234,10 +232,6 @@
             writer.println(mDisableJobSchedulerJobs);
             writer.print("mLastExecutionStatus:");
             writer.println(mLastExecutionStatus);
-            writer.print("mLastExecutionStartTimeMs:");
-            writer.println(mLastExecutionStartTimeMs);
-            writer.print("mLastExecutionDurationIncludingSleepMs:");
-            writer.println(mLastExecutionDurationIncludingSleepMs);
             writer.print("mLastExecutionStartUptimeMs:");
             writer.println(mLastExecutionStartUptimeMs);
             writer.print("mLastExecutionDurationMs:");
@@ -410,7 +404,7 @@
                                 job.jobFinished(params, !completed);
                             } else {
                                 // Periodic job
-                                job.jobFinished(params, true);
+                                job.jobFinished(params, false /* reschedule */);
                             }
                             markDexOptCompleted();
                         }
@@ -564,8 +558,6 @@
     private boolean runIdleOptimization(
             PackageManagerService pm, List<String> pkgs, boolean isPostBootUpdate) {
         synchronized (mLock) {
-            mLastExecutionStartTimeMs = SystemClock.elapsedRealtime();
-            mLastExecutionDurationIncludingSleepMs = -1;
             mLastExecutionStartUptimeMs = SystemClock.uptimeMillis();
             mLastExecutionDurationMs = -1;
         }
@@ -574,8 +566,6 @@
         logStatus(status);
         synchronized (mLock) {
             mLastExecutionStatus = status;
-            mLastExecutionDurationIncludingSleepMs =
-                    SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs;
             mLastExecutionDurationMs = SystemClock.uptimeMillis() - mLastExecutionStartUptimeMs;
         }
 
@@ -979,10 +969,9 @@
         synchronized (mLock) {
             status = mLastExecutionStatus;
             durationMs = mLastExecutionDurationMs;
-            durationIncludingSleepMs = mLastExecutionDurationIncludingSleepMs;
         }
 
-        mStatsLogger.write(status, params.getStopReason(), durationMs, durationIncludingSleepMs);
+        mStatsLogger.write(status, params.getStopReason(), durationMs);
     }
 
     /** Injector pattern for testing purpose */
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/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 4e9c472..d6233c7 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -148,9 +148,18 @@
                         + intent.toShortString(false, true, false, false)
                         + " " + intent.getExtras(), here);
             }
+            final boolean ordered;
+            if (mAmInternal.isModernQueueEnabled()) {
+                // When the modern broadcast stack is enabled, deliver all our
+                // broadcasts as unordered, since the modern stack has better
+                // support for sequencing cold-starts, and it supports
+                // delivering resultTo for non-ordered broadcasts
+                ordered = false;
+            } else {
+                ordered = (finishedReceiver != null);
+            }
             mAmInternal.broadcastIntent(
-                    intent, finishedReceiver, requiredPermissions,
-                    finishedReceiver != null, userId,
+                    intent, finishedReceiver, requiredPermissions, ordered, userId,
                     broadcastAllowList == null ? null : broadcastAllowList.get(userId),
                     filterExtrasForReceiver, bOptions);
         }
diff --git a/services/core/java/com/android/server/pm/CloneProfileResolver.java b/services/core/java/com/android/server/pm/CloneProfileResolver.java
new file mode 100644
index 0000000..d3113e1
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CloneProfileResolver.java
@@ -0,0 +1,99 @@
+/*
+ * 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.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.resolution.ComponentResolverApi;
+import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Cross Profile intent resolution strategy used for and to clone profile.
+ */
+public class CloneProfileResolver extends CrossProfileResolver {
+
+    public CloneProfileResolver(ComponentResolverApi componentResolver,
+            UserManagerService userManagerService) {
+        super(componentResolver, userManagerService);
+    }
+
+    /**
+     * This is resolution strategy for Clone Profile.
+     * In case of clone profile, the profile is supposed to be transparent to end user. To end user
+     * clone and owner profile should be part of same user space. Hence, the resolution strategy
+     * would resolve intent in both profile and return combined result without any filtering of the
+     * results.
+     *
+     * @param computer ComputerEngine instance that would be needed by ComponentResolverApi
+     * @param intent request
+     * @param resolvedType the MIME data type of intent request
+     * @param userId source/initiating user
+     * @param targetUserId target user id
+     * @param flags of intent request
+     * @param pkgName the application package name this Intent is limited to
+     * @param matchingFilters {@link CrossProfileIntentFilter}s configured for source user,
+     *                                                        targeting the targetUserId
+     * @param hasNonNegativePriorityResult if source have any non-negative(active and valid)
+     *                                     resolveInfo in their profile.
+     * @param pkgSettingFunction function to find PackageStateInternal for given package
+     * @return list of {@link CrossProfileDomainInfo}
+     */
+    @Override
+    public List<CrossProfileDomainInfo> resolveIntent(Computer computer, Intent intent,
+            String resolvedType, int userId, int targetUserId, long flags,
+            String pkgName, List<CrossProfileIntentFilter> matchingFilters,
+            boolean hasNonNegativePriorityResult,
+            Function<String, PackageStateInternal> pkgSettingFunction) {
+        List<ResolveInfo> resolveInfos = mComponentResolver.queryActivities(computer,
+                intent, resolvedType, flags, targetUserId);
+        List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+        if (resolveInfos != null) {
+
+            for (int index = 0; index < resolveInfos.size(); index++) {
+                crossProfileDomainInfos.add(new CrossProfileDomainInfo(resolveInfos.get(index),
+                        DomainVerificationManagerInternal.APPROVAL_LEVEL_NONE,
+                        targetUserId));
+            }
+        }
+        return filterIfNotSystemUser(crossProfileDomainInfos, userId);
+    }
+
+    /**
+     * As clone and owner profile are going to be part of the same userspace, we need no filtering
+     * out of any clone profile's result
+     * @param intent request
+     * @param crossProfileDomainInfos resolved in target user
+     * @param flags for intent resolution
+     * @param sourceUserId source user
+     * @param targetUserId target user
+     * @param highestApprovalLevel highest level of domain approval
+     * @return list of CrossProfileDomainInfo
+     */
+    @Override
+    public List<CrossProfileDomainInfo> filterResolveInfoWithDomainPreferredActivity(Intent intent,
+            List<CrossProfileDomainInfo> crossProfileDomainInfos, long flags, int sourceUserId,
+            int targetUserId, int highestApprovalLevel) {
+        // no filtering for clone profile
+        return crossProfileDomainInfos;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/CommitRequest.java b/services/core/java/com/android/server/pm/CommitRequest.java
deleted file mode 100644
index d1a6002..0000000
--- a/services/core/java/com/android/server/pm/CommitRequest.java
+++ /dev/null
@@ -1,35 +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.
- */
-
-package com.android.server.pm;
-
-import android.annotation.NonNull;
-
-import java.util.Map;
-
-/**
- * Package state to commit to memory and disk after reconciliation has completed.
- */
-final class CommitRequest {
-    final Map<String, ReconciledPackage> mReconciledPackages;
-    @NonNull final int[] mAllUsers;
-
-    CommitRequest(Map<String, ReconciledPackage> reconciledPackages,
-            @NonNull int[] allUsers) {
-        mReconciledPackages = reconciledPackages;
-        mAllUsers = allUsers;
-    }
-}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index a4e295b..5b8ee2b 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -125,6 +125,14 @@
     ActivityInfo getActivityInfo(ComponentName component, long flags, int userId);
 
     /**
+     * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+     * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+     * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+     * chained cross profiles
+     */
+    ActivityInfo getActivityInfoCrossProfile(ComponentName component, long flags, int userId);
+
+    /**
      * Important: The provided filterCallingUid is used exclusively to filter out activities
      * that can be seen based on user state. It's typically the original caller uid prior
      * to clearing. Because it can only be provided by trusted code, its value can be
@@ -203,6 +211,12 @@
     boolean filterSharedLibPackage(@Nullable PackageStateInternal ps, int uid, int userId,
             long flags);
     boolean isCallerSameApp(String packageName, int uid);
+    /**
+     * Returns true if the package name and the uid represent the same app.
+     *
+     * @param resolveIsolatedUid if true, resolves an isolated uid into the real uid.
+     */
+    boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component,
             @PackageManager.ComponentType int type);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 5d479d5..a8534b0 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -63,7 +63,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.apex.ApexInfo;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -112,9 +111,9 @@
 import android.util.LongSparseLongArray;
 import android.util.MathUtils;
 import android.util.Pair;
+import android.util.PrintWriterPrinter;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -123,6 +122,7 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.parsing.PackageInfoUtils;
@@ -396,7 +396,6 @@
     private final UserManagerService mUserManager;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final ApexManager mApexManager;
-    private final ApexPackageInfo mApexPackageInfo;
     private final PackageManagerServiceInjector mInjector;
     private final ComponentResolverApi mComponentResolver;
     private final InstantAppResolverConnection mInstantAppResolverConnection;
@@ -452,7 +451,6 @@
         mContext = args.service.mContext;
         mInjector = args.service.mInjector;
         mApexManager = args.service.mApexManager;
-        mApexPackageInfo = args.service.mApexPackageInfo;
         mInstantAppResolverConnection = args.service.mInstantAppResolverConnection;
         mDefaultAppProvider = args.service.getDefaultAppProvider();
         mDomainVerificationManager = args.service.mDomainVerificationManager;
@@ -462,7 +460,7 @@
         mBackgroundDexOptService = args.service.mBackgroundDexOptService;
         mExternalSourcesPolicy = args.service.mExternalSourcesPolicy;
         mCrossProfileIntentResolverEngine = new CrossProfileIntentResolverEngine(
-                mUserManager, mDomainVerificationManager, mDefaultAppProvider);
+                mUserManager, mDomainVerificationManager, mDefaultAppProvider, mContext);
 
         // Used to reference PMS attributes that are primitives and which are not
         // updated under control of the PMS lock.
@@ -837,6 +835,24 @@
     }
 
     /**
+     * Similar to {@link Computer#getActivityInfo(android.content.ComponentName, long, int)} but
+     * only visible as internal service. This method bypass INTERACT_ACROSS_USERS or
+     * INTERACT_ACROSS_USERS_FULL permission checks and only to be used for intent resolution across
+     * chained cross profiles
+     * @param component application's component
+     * @param flags resolve info flags
+     * @param userId user id where activity resides
+     * @return ActivityInfo corresponding to requested component.
+     */
+    public final ActivityInfo getActivityInfoCrossProfile(ComponentName component,
+            @PackageManager.ResolveInfoFlagsBits long flags, int userId) {
+        if (!mUserManager.exists(userId)) return null;
+        flags = updateFlagsForComponent(flags, userId);
+
+        return getActivityInfoInternalBody(component, flags, Binder.getCallingUid(), userId);
+    }
+
+    /**
      * Important: The provided filterCallingUid is used exclusively to filter out activities
      * that can be seen based on user state. It's typically the original caller uid prior
      * to clearing. Because it can only be provided by trusted code, its value can be
@@ -968,10 +984,8 @@
         if (p != null) {
             PackageStateInternal ps = mSettings.getPackage(packageName);
             if (ps == null) return null;
-            if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                if (!matchApex && p.isApex()) {
-                    return null;
-                }
+            if (!matchApex && p.isApex()) {
+                return null;
             }
             if (filterSharedLibPackage(ps, filterCallingUid, userId, flags)) {
                 return null;
@@ -987,24 +1001,6 @@
             }
             return ai;
         }
-        if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-            if (matchApex) {
-                // For APKs, PackageInfo.applicationInfo is not exactly the same as ApplicationInfo
-                // returned from getApplicationInfo, but for APEX packages difference shouldn't be
-                // very big.
-                // TODO(b/155328545): generate proper application info for APEXes as well.
-                int apexFlags = ApexManager.MATCH_ACTIVE_PACKAGE;
-                if ((flags & PackageManager.MATCH_SYSTEM_ONLY) != 0) {
-                    apexFlags = ApexManager.MATCH_FACTORY_PACKAGE;
-                }
-                final var pair = mApexPackageInfo.getPackageInfo(packageName, apexFlags);
-                if (pair == null) {
-                    return null;
-                }
-                return PackageInfoUtils.generateApplicationInfo(pair.second, flags,
-                        PackageUserStateInternal.DEFAULT, userId, null);
-            }
-        }
         if ("android".equals(packageName) || "system".equals(packageName)) {
             return androidApplication();
         }
@@ -1553,22 +1549,10 @@
         final boolean matchApex = (flags & MATCH_APEX) != 0;
         if (matchFactoryOnly) {
             // Instant app filtering for APEX modules is ignored
-            if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                if (matchApex) {
-                    final var pair = mApexPackageInfo.getPackageInfo(packageName,
-                            ApexManager.MATCH_FACTORY_PACKAGE);
-                    if (pair == null) {
-                        return null;
-                    }
-                    return PackageInfoUtils.generate(pair.second, pair.first, flags, null, userId);
-                }
-            }
             final PackageStateInternal ps = mSettings.getDisabledSystemPkg(packageName);
             if (ps != null) {
-                if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                    if (!matchApex && ps.getPkg() != null && ps.getPkg().isApex()) {
-                        return null;
-                    }
+                if (!matchApex && ps.getPkg() != null && ps.getPkg().isApex()) {
+                    return null;
                 }
                 if (filterSharedLibPackage(ps, filterCallingUid, userId, flags)) {
                     return null;
@@ -1589,10 +1573,8 @@
         }
         if (p != null) {
             final PackageStateInternal ps = getPackageStateInternal(p.getPackageName());
-            if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                if (!matchApex && p.isApex()) {
-                    return null;
-                }
+            if (!matchApex && p.isApex()) {
+                return null;
             }
             if (filterSharedLibPackage(ps, filterCallingUid, userId, flags)) {
                 return null;
@@ -1614,16 +1596,6 @@
             }
             return generatePackageInfo(ps, flags, userId);
         }
-        if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-            if (matchApex) {
-                final var pair = mApexPackageInfo.getPackageInfo(packageName,
-                        ApexManager.MATCH_ACTIVE_PACKAGE);
-                if (pair == null) {
-                    return null;
-                }
-                return PackageInfoUtils.generate(pair.second, pair.first, flags, null, userId);
-            }
-        }
         return null;
     }
 
@@ -1692,10 +1664,8 @@
                         ps = psDisabled;
                     }
                 }
-                if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                    if (!listApex && ps.getPkg() != null && ps.getPkg().isApex()) {
-                        continue;
-                    }
+                if (!listApex && ps.getPkg() != null && ps.getPkg().isApex()) {
+                    continue;
                 }
                 if (filterSharedLibPackage(ps, callingUid, userId, flags)) {
                     continue;
@@ -1722,10 +1692,8 @@
                         ps = psDisabled;
                     }
                 }
-                if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                    if (!listApex && p.isApex()) {
-                        continue;
-                    }
+                if (!listApex && p.isApex()) {
+                    continue;
                 }
                 if (filterSharedLibPackage(ps, callingUid, userId, flags)) {
                     continue;
@@ -1739,22 +1707,6 @@
                 }
             }
         }
-        if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-            if (listApex) {
-                List<Pair<ApexInfo, AndroidPackage>> pairs;
-                if (listFactory) {
-                    pairs = mApexPackageInfo.getFactoryPackages();
-                } else {
-                    pairs = mApexPackageInfo.getActivePackages();
-                }
-
-                for (int index = 0; index < pairs.size(); index++) {
-                    var pair = pairs.get(index);
-                    list.add(PackageInfoUtils.generate(pair.second, pair.first, flags, null,
-                            userId));
-                }
-            }
-        }
         return new ParceledListSlice<>(list);
     }
 
@@ -1777,7 +1729,7 @@
         ComponentName forwardingActivityComponentName = new ComponentName(
                 androidApplication().packageName, className);
         ActivityInfo forwardingActivityInfo =
-                getActivityInfo(forwardingActivityComponentName, 0,
+                getActivityInfoCrossProfile(forwardingActivityComponentName, 0,
                         sourceUserId);
         if (!targetIsProfile) {
             forwardingActivityInfo.showUserIcon = targetUserId;
@@ -2209,11 +2161,19 @@
     }
 
     public final boolean isCallerSameApp(String packageName, int uid) {
+        return isCallerSameApp(packageName, uid, false /* resolveIsolatedUid */);
+    }
+
+    @Override
+    public final boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid) {
         if (Process.isSdkSandboxUid(uid)) {
             return (packageName != null
                     && packageName.equals(mService.getSdkSandboxPackageName()));
         }
         AndroidPackage pkg = mPackages.get(packageName);
+        if (resolveIsolatedUid && Process.isIsolated(uid)) {
+            uid = getIsolatedOwner(uid);
+        }
         return pkg != null
                 && UserHandle.getAppId(uid) == pkg.getUid();
     }
@@ -3147,24 +3107,56 @@
     }
 
     private void dumpApex(PrintWriter pw, String packageName) {
-        if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-            final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
-            List<PackageStateInternal> activePackages = new ArrayList<>();
-            List<PackageStateInternal> inactivePackages = new ArrayList<>();
-            List<PackageStateInternal> factoryActivePackages = new ArrayList<>();
-            List<PackageStateInternal> factoryInactivePackages = new ArrayList<>();
-            generateApexPackageInfo(activePackages, inactivePackages, factoryActivePackages,
-                    factoryInactivePackages);
-            ipw.println("Active APEX packages:");
-            ApexPackageInfo.dumpPackageStates(activePackages, true, packageName, ipw);
-            ipw.println("Inactive APEX packages:");
-            ApexPackageInfo.dumpPackageStates(inactivePackages, false, packageName, ipw);
-            ipw.println("Factory APEX packages:");
-            ApexPackageInfo.dumpPackageStates(factoryActivePackages, true, packageName, ipw);
-            ApexPackageInfo.dumpPackageStates(factoryInactivePackages, false, packageName, ipw);
-        } else {
-            mApexPackageInfo.dump(pw, packageName);
+        final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ", 120);
+        List<PackageStateInternal> activePackages = new ArrayList<>();
+        List<PackageStateInternal> inactivePackages = new ArrayList<>();
+        List<PackageStateInternal> factoryActivePackages = new ArrayList<>();
+        List<PackageStateInternal> factoryInactivePackages = new ArrayList<>();
+        generateApexPackageInfo(activePackages, inactivePackages, factoryActivePackages,
+                factoryInactivePackages);
+        ipw.println("Active APEX packages:");
+        dumpApexPackageStates(activePackages, true, packageName, ipw);
+        ipw.println("Inactive APEX packages:");
+        dumpApexPackageStates(inactivePackages, false, packageName, ipw);
+        ipw.println("Factory APEX packages:");
+        dumpApexPackageStates(factoryActivePackages, true, packageName, ipw);
+        dumpApexPackageStates(factoryInactivePackages, false, packageName, ipw);
+    }
+
+
+    /**
+     * Dump information about the packages contained in a particular cache
+     * @param packageStates the states to print information about.
+     * @param packageName a {@link String} containing a package name, or {@code null}. If set,
+     *                    only information about that specific package will be dumped.
+     * @param ipw the {@link IndentingPrintWriter} object to send information to.
+     */
+    private static void dumpApexPackageStates(List<PackageStateInternal> packageStates,
+            boolean isActive, @Nullable String packageName, IndentingPrintWriter ipw) {
+        ipw.println();
+        ipw.increaseIndent();
+        for (int i = 0, size = packageStates.size(); i < size; i++) {
+            final var packageState = packageStates.get(i);
+            var pkg = packageState.getPkg();
+            if (packageName != null && !packageName.equals(pkg.getPackageName())) {
+                continue;
+            }
+            ipw.println(pkg.getPackageName());
+            ipw.increaseIndent();
+            ipw.println("Version: " + pkg.getLongVersionCode());
+            ipw.println("Path: " + pkg.getBaseApkPath());
+            ipw.println("IsActive: " + isActive);
+            ipw.println("IsFactory: " + !packageState.isUpdatedSystemApp());
+            ipw.println("ApplicationInfo: ");
+            ipw.increaseIndent();
+            // TODO: Dump the package manually
+            AndroidPackageUtils.generateAppInfoWithoutState(pkg)
+                    .dump(new PrintWriterPrinter(ipw), "");
+            ipw.decreaseIndent();
+            ipw.decreaseIndent();
         }
+        ipw.decreaseIndent();
+        ipw.println();
     }
 
     // The body of findPreferredActivity.
@@ -3543,12 +3535,8 @@
 
     @Override
     public boolean isApexPackage(String packageName) {
-        if (!ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-            return mApexPackageInfo.isApexPackage(packageName);
-        } else {
-            final AndroidPackage pkg = mPackages.get(packageName);
-            return pkg != null && pkg.isApex();
-        }
+        final AndroidPackage pkg = mPackages.get(packageName);
+        return pkg != null && pkg.isApex();
     }
 
     @Override
@@ -4555,10 +4543,8 @@
                     effectiveFlags |= PackageManager.MATCH_ANY_USER;
                 }
                 if (ps.getPkg() != null) {
-                    if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                        if (!listApex && ps.getPkg().isApex()) {
-                            continue;
-                        }
+                    if (!listApex && ps.getPkg().isApex()) {
+                        continue;
                     }
                     if (filterSharedLibPackage(ps, callingUid, userId, flags)) {
                         continue;
@@ -4588,10 +4574,8 @@
                 if (pkg == null) {
                     continue;
                 }
-                if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                    if (!listApex && pkg.isApex()) {
-                        continue;
-                    }
+                if (!listApex && pkg.isApex()) {
+                    continue;
                 }
                 if (filterSharedLibPackage(packageState, Binder.getCallingUid(), userId, flags)) {
                     continue;
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
index 718756f..04bd135 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
@@ -21,10 +21,10 @@
 import android.content.IntentFilter;
 import android.os.UserHandle;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -46,6 +46,18 @@
     private static final String ATTR_FILTER = "filter";
     private static final String ATTR_ACCESS_CONTROL = "accessControl";
 
+    //flag to decide if intent needs to be resolved cross profile if pkgName is already defined
+    public static final int FLAG_IS_PACKAGE_FOR_FILTER = 0x00000008;
+
+    /*
+    This flag, denotes if further cross profile resolution is allowed, e.g. if profile#0 is linked
+    to profile#1 and profile#2 . When intent resolution from profile#1 is started we resolve it in
+    profile#1 and profile#0. The profile#0 is also linked to profile#2, we will only resolve in
+    profile#2 if CrossProfileIntentFilter between profile#1 and profile#0 have set flag
+    FLAG_ALLOW_CHAINED_RESOLUTION.
+     */
+    public static final int FLAG_ALLOW_CHAINED_RESOLUTION = 0x00000010;
+
     private static final String TAG = "CrossProfileIntentFilter";
 
     /**
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolver.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolver.java
index 9ea16d3..2581878 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolver.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolver.java
@@ -16,6 +16,8 @@
 
 package com.android.server.pm;
 
+import static com.android.server.pm.CrossProfileIntentFilter.FLAG_IS_PACKAGE_FOR_FILTER;
+
 import android.annotation.NonNull;
 import android.content.IntentFilter;
 
@@ -37,7 +39,7 @@
 
     @Override
     protected boolean isPackageForFilter(String packageName, CrossProfileIntentFilter filter) {
-        return false;
+        return (FLAG_IS_PACKAGE_FOR_FILTER & filter.mFlags) != 0;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
index 7752fdf..4362956 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentResolverEngine.java
@@ -25,23 +25,30 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.os.Process;
 import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.server.LocalServices;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
 import com.android.server.pm.verify.domain.DomainVerificationUtils;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Queue;
+import java.util.Set;
 import java.util.function.Function;
 
 /**
@@ -54,13 +61,15 @@
     private final UserManagerService mUserManager;
     private final DomainVerificationManagerInternal mDomainVerificationManager;
     private final DefaultAppProvider mDefaultAppProvider;
+    private final Context mContext;
 
     public CrossProfileIntentResolverEngine(UserManagerService userManager,
             DomainVerificationManagerInternal domainVerificationManager,
-            DefaultAppProvider defaultAppProvider) {
+            DefaultAppProvider defaultAppProvider, Context context) {
         mUserManager = userManager;
         mDomainVerificationManager = domainVerificationManager;
         mDefaultAppProvider = defaultAppProvider;
+        mContext = context;
     }
 
     /**
@@ -111,73 +120,111 @@
             Intent intent, String resolvedType, int userId, long flags, String pkgName,
             boolean hasNonNegativePriorityResult,
             Function<String, PackageStateInternal> pkgSettingFunction) {
-
+        Queue<Integer> pendingUsers = new ArrayDeque<>();
+        Set<Integer> visitedUserIds = new HashSet<>();
+        SparseBooleanArray hasNonNegativePriorityResultFromParent = new SparseBooleanArray();
+        visitedUserIds.add(userId);
+        pendingUsers.add(userId);
+        hasNonNegativePriorityResultFromParent.put(userId, hasNonNegativePriorityResult);
+        UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
         List<CrossProfileDomainInfo> crossProfileDomainInfos = new ArrayList<>();
+        while (!pendingUsers.isEmpty()) {
+            int currentUserId = pendingUsers.poll();
+            List<CrossProfileIntentFilter> matchingFilters =
+                    computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+                            currentUserId);
 
-        List<CrossProfileIntentFilter> matchingFilters =
-                computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);
-
-        if (matchingFilters == null || matchingFilters.isEmpty()) {
-            /** if intent is web intent, checking if parent profile should handle the intent even
-            if there is no matching filter. The configuration is based on user profile
-            restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
-            if (intent.hasWebURI()) {
-                UserInfo parent = computer.getProfileParent(userId);
-                if (parent != null) {
-                    CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
-                            .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags, userId,
-                                    parent.id);
-                    if (generalizedCrossProfileDomainInfo != null) {
-                        crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+            if (matchingFilters == null || matchingFilters.isEmpty()) {
+                /** if intent is web intent, checking if parent profile should handle the intent
+                 * even if there is no matching filter. The configuration is based on user profile
+                 * restriction android.os.UserManager#ALLOW_PARENT_PROFILE_APP_LINKING **/
+                if (currentUserId == userId && intent.hasWebURI()) {
+                    UserInfo parent = computer.getProfileParent(currentUserId);
+                    if (parent != null) {
+                        CrossProfileDomainInfo generalizedCrossProfileDomainInfo = computer
+                                .getCrossProfileDomainPreferredLpr(intent, resolvedType, flags,
+                                        currentUserId, parent.id);
+                        if (generalizedCrossProfileDomainInfo != null) {
+                            crossProfileDomainInfos.add(generalizedCrossProfileDomainInfo);
+                        }
                     }
                 }
+                continue;
             }
-            return crossProfileDomainInfos;
-        }
 
-        UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
-        UserInfo sourceUserInfo = umInternal.getUserInfo(userId);
+            UserInfo sourceUserInfo = umInternal.getUserInfo(currentUserId);
 
-       // Grouping the CrossProfileIntentFilters based on targerId
-        SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
-                new SparseArray<>();
+            // Grouping the CrossProfileIntentFilters based on targerId
+            SparseArray<List<CrossProfileIntentFilter>> crossProfileIntentFiltersByUser =
+                    new SparseArray<>();
 
-        for (int index = 0; index < matchingFilters.size(); index++) {
-            CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
+            for (int index = 0; index < matchingFilters.size(); index++) {
+                CrossProfileIntentFilter crossProfileIntentFilter = matchingFilters.get(index);
 
-            if (!crossProfileIntentFiltersByUser
-                    .contains(crossProfileIntentFilter.mTargetUserId)) {
-                crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
-                        new ArrayList<>());
+                if (!crossProfileIntentFiltersByUser
+                        .contains(crossProfileIntentFilter.mTargetUserId)) {
+                    crossProfileIntentFiltersByUser.put(crossProfileIntentFilter.mTargetUserId,
+                            new ArrayList<>());
+                }
+                crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
+                        .add(crossProfileIntentFilter);
             }
-            crossProfileIntentFiltersByUser.get(crossProfileIntentFilter.mTargetUserId)
-                    .add(crossProfileIntentFilter);
-        }
 
-        /*
-         For each target user, we would call their corresponding strategy
-         {@link CrossProfileResolver} to resolve intent in corresponding user
-         */
-        for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
+            /*
+             For each target user, we would call their corresponding strategy
+             {@link CrossProfileResolver} to resolve intent in corresponding user
+             */
+            for (int index = 0; index < crossProfileIntentFiltersByUser.size(); index++) {
 
-            UserInfo targetUserInfo = umInternal.getUserInfo(crossProfileIntentFiltersByUser
-                    .keyAt(index));
+                int targetUserId = crossProfileIntentFiltersByUser.keyAt(index);
 
-            // Choosing strategy based on source and target user
-            CrossProfileResolver crossProfileResolver =
-                    chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
+                //if user is already visited then skip resolution for particular user.
+                if (visitedUserIds.contains(targetUserId)) {
+                    continue;
+                }
+
+                UserInfo targetUserInfo = umInternal.getUserInfo(targetUserId);
+
+                // Choosing strategy based on source and target user
+                CrossProfileResolver crossProfileResolver =
+                        chooseCrossProfileResolver(computer, sourceUserInfo, targetUserInfo);
 
             /*
             If {@link CrossProfileResolver} is available for source,target pair we will call it to
             get {@link CrossProfileDomainInfo}s from that user.
              */
-            if (crossProfileResolver != null) {
-                List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
-                        .resolveIntent(computer, intent, resolvedType, userId,
-                                crossProfileIntentFiltersByUser.keyAt(index), flags, pkgName,
-                                crossProfileIntentFiltersByUser.valueAt(index),
-                                hasNonNegativePriorityResult, pkgSettingFunction);
-                crossProfileDomainInfos.addAll(crossProfileInfos);
+                if (crossProfileResolver != null) {
+                    List<CrossProfileDomainInfo> crossProfileInfos = crossProfileResolver
+                            .resolveIntent(computer, intent, resolvedType, currentUserId,
+                                    targetUserId, flags, pkgName,
+                                    crossProfileIntentFiltersByUser.valueAt(index),
+                                    hasNonNegativePriorityResultFromParent.get(currentUserId),
+                                    pkgSettingFunction);
+                    crossProfileDomainInfos.addAll(crossProfileInfos);
+
+                    hasNonNegativePriorityResultFromParent.put(targetUserId,
+                            hasNonNegativePriority(crossProfileInfos));
+
+                    /*
+                    Adding target user to queue if flag
+                    {@link CrossProfileIntentFilter#FLAG_ALLOW_CHAINED_RESOLUTION} is set for any
+                    {@link CrossProfileIntentFilter}
+                     */
+                    boolean allowChainedResolution = false;
+                    for (int filterIndex = 0; filterIndex < crossProfileIntentFiltersByUser
+                            .valueAt(index).size(); filterIndex++) {
+                        if ((CrossProfileIntentFilter
+                                .FLAG_ALLOW_CHAINED_RESOLUTION & crossProfileIntentFiltersByUser
+                                .valueAt(index).get(filterIndex).mFlags) != 0) {
+                            allowChainedResolution = true;
+                            break;
+                        }
+                    }
+                    if (allowChainedResolution) {
+                        pendingUsers.add(targetUserId);
+                    }
+                    visitedUserIds.add(targetUserId);
+                }
             }
         }
 
@@ -196,6 +243,21 @@
     @SuppressWarnings("unused")
     private CrossProfileResolver chooseCrossProfileResolver(@NonNull Computer computer,
             UserInfo sourceUserInfo, UserInfo targetUserInfo) {
+        //todo change isCloneProfile to user properties b/241532322
+        /**
+         * If source or target user is clone profile, using {@link CloneProfileResolver}
+         * We would allow CloneProfileResolver only if flag
+         * SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE is enabled
+         */
+        if (sourceUserInfo.isCloneProfile() || targetUserInfo.isCloneProfile()) {
+            if (FeatureFlagUtils.isEnabled(mContext,
+                    FeatureFlagUtils.SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE)) {
+                return new CloneProfileResolver(computer.getComponentResolver(),
+                        mUserManager);
+            } else {
+                return null;
+            }
+        }
         return new DefaultCrossProfileResolver(computer.getComponentResolver(),
                 mUserManager, mDomainVerificationManager);
     }
@@ -218,7 +280,7 @@
 
     /**
      * Returns true if we source user can reach target user for given intent. The source can
-     * directly or indirectly reach to target. This will perform depth first search to check if
+     * directly or indirectly reach to target. This will perform breadth first search to check if
      * source can reach target.
      * @param computer {@link Computer} instance used for resolution by {@link ComponentResolverApi}
      * @param intent request
@@ -232,13 +294,38 @@
             @UserIdInt int targetUserId) {
         if (sourceUserId == targetUserId) return true;
 
-        List<CrossProfileIntentFilter> matches =
-                computer.getMatchingCrossProfileIntentFilters(intent, resolvedType, sourceUserId);
-        if (matches != null) {
-            for (int index = 0; index < matches.size(); index++) {
-                CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
-                if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
-                    return true;
+        Queue<Integer> pendingUsers = new ArrayDeque<>();
+        Set<Integer> visitedUserIds = new HashSet<>();
+        visitedUserIds.add(sourceUserId);
+        pendingUsers.add(sourceUserId);
+
+        while (!pendingUsers.isEmpty()) {
+            int currentUserId = pendingUsers.poll();
+
+            List<CrossProfileIntentFilter> matches =
+                    computer.getMatchingCrossProfileIntentFilters(intent, resolvedType,
+                            currentUserId);
+            if (matches != null) {
+                for (int index = 0; index < matches.size(); index++) {
+                    CrossProfileIntentFilter crossProfileIntentFilter = matches.get(index);
+                    if (crossProfileIntentFilter.mTargetUserId == targetUserId) {
+                        return true;
+                    }
+                    if (visitedUserIds.contains(crossProfileIntentFilter.mTargetUserId)) {
+                        continue;
+                    }
+
+                    /*
+                     If source cannot directly reach to target, we will add
+                     CrossProfileIntentFilter.mTargetUserId user to queue to check if target user
+                     can be reached via CrossProfileIntentFilter.mTargetUserId i.e. it can be
+                     indirectly reached through chained/linked profiles.
+                     */
+                    if ((CrossProfileIntentFilter.FLAG_ALLOW_CHAINED_RESOLUTION
+                            & crossProfileIntentFilter.mFlags) != 0) {
+                        pendingUsers.add(crossProfileIntentFilter.mTargetUserId);
+                        visitedUserIds.add(crossProfileIntentFilter.mTargetUserId);
+                    }
                 }
             }
         }
@@ -313,8 +400,6 @@
         }
 
         if (pkgName == null && intent.hasWebURI()) {
-            // If instant apps are not allowed and there is result only from current or cross
-            // profile return it
             if (!addInstant && ((candidates.size() <= 1 && crossProfileCandidates.isEmpty())
                     || (candidates.isEmpty() && !crossProfileCandidates.isEmpty()))) {
                 candidates.addAll(resolveInfoFromCrossProfileDomainInfo(crossProfileCandidates));
@@ -588,4 +673,14 @@
 
         return resolveInfoList;
     }
+
+    /**
+     * @param crossProfileDomainInfos list of cross profile domain info in descending priority order
+     * @return if the list contains a resolve info with non-negative priority
+     */
+    private boolean hasNonNegativePriority(List<CrossProfileDomainInfo> crossProfileDomainInfos) {
+        return crossProfileDomainInfos.size() > 0
+                && crossProfileDomainInfos.get(0).mResolveInfo != null
+                && crossProfileDomainInfos.get(0).mResolveInfo.priority >= 0;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
index cac9323..ceaaefd 100644
--- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
@@ -319,4 +319,135 @@
                 HOME,
                 MOBILE_NETWORK_SETTINGS);
     }
+
+    /**
+     * Clone profile's DefaultCrossProfileIntentFilter
+     */
+
+    /*
+     Allowing media capture from clone to parent profile as clone profile would not have camera
+     */
+    private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_MEDIA_CAPTURE =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                                            // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(MediaStore.ACTION_IMAGE_CAPTURE)
+                    .addAction(MediaStore.ACTION_IMAGE_CAPTURE_SECURE)
+                    .addAction(MediaStore.ACTION_VIDEO_CAPTURE)
+                    .addAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+                    .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+                    .addAction(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE)
+                    .addAction(MediaStore.INTENT_ACTION_VIDEO_CAMERA)
+                    .addCategory(Intent.CATEGORY_DEFAULT)
+                    .build();
+
+    /*
+     Allowing send action from clone to parent profile to share content from clone apps to parent
+     apps
+     */
+    private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_SEND_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                    // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_SEND)
+                    .addAction(Intent.ACTION_SEND_MULTIPLE)
+                    .addAction(Intent.ACTION_SENDTO)
+                    .addDataType("*/*")
+                    .build();
+
+    /*
+     Allowing send action from parent to clone profile to share content from parent apps to clone
+     apps
+     */
+    private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_SEND_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                                            // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_SEND)
+                    .addAction(Intent.ACTION_SEND_MULTIPLE)
+                    .addAction(Intent.ACTION_SENDTO)
+                    .addDataType("*/*")
+                    .build();
+
+    /*
+     Allowing view action from clone to parent profile to open any app-links or web links
+     */
+    private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_VIEW_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                    // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_VIEW)
+                    .addDataScheme("https")
+                    .addDataScheme("http")
+                    .build();
+
+    /*
+     Allowing view action from parent to clone profile to open any app-links or web links
+     */
+    private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_VIEW_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                                            // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_VIEW)
+                    .addDataScheme("https")
+                    .addDataScheme("http")
+                    .build();
+
+    /*
+     Allowing pick,insert and edit action from clone to parent profile to open picker or contacts
+     insert/edit.
+     */
+    private static final DefaultCrossProfileIntentFilter CLONE_TO_PARENT_PICK_INSERT_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                                            // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_PICK)
+                    .addAction(Intent.ACTION_GET_CONTENT)
+                    .addAction(Intent.ACTION_EDIT)
+                    .addAction(Intent.ACTION_INSERT)
+                    .addAction(Intent.ACTION_INSERT_OR_EDIT)
+                    .addDataType("*/*")
+                    .build();
+
+    /*
+     Allowing pick,insert and edit action from parent to clone profile to open picker
+     */
+    private static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_PICK_INSERT_ACTION =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */ 0x00000018, // 0x00000018 means FLAG_IS_PACKAGE_FOR_FILTER
+                                            // and FLAG_ALLOW_CHAINED_RESOLUTION set
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(Intent.ACTION_PICK)
+                    .addAction(Intent.ACTION_GET_CONTENT)
+                    .addAction(Intent.ACTION_EDIT)
+                    .addAction(Intent.ACTION_INSERT)
+                    .addAction(Intent.ACTION_INSERT_OR_EDIT)
+                    .addDataType("*/*")
+                    .build();
+
+    public static List<DefaultCrossProfileIntentFilter> getDefaultCloneProfileFilters() {
+        return Arrays.asList(
+                PARENT_TO_CLONE_SEND_ACTION,
+                PARENT_TO_CLONE_VIEW_ACTION,
+                PARENT_TO_CLONE_PICK_INSERT_ACTION,
+                CLONE_TO_PARENT_MEDIA_CAPTURE,
+                CLONE_TO_PARENT_SEND_ACTION,
+                CLONE_TO_PARENT_VIEW_ACTION,
+                CLONE_TO_PARENT_PICK_INSERT_ACTION
+
+        );
+    }
 }
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 88a3f8e..095a7f6 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -261,6 +261,7 @@
             final boolean killApp = (deleteFlags & PackageManager.DELETE_DONT_KILL_APP) == 0;
             info.sendPackageRemovedBroadcasts(killApp, removedBySystem);
             info.sendSystemPackageUpdatedBroadcasts();
+            PackageMetrics.onUninstallSucceeded(info, deleteFlags, mUserManagerInternal);
         }
 
         // Force a gc to clear up things.
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 3f04264..c4f6836 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -18,6 +18,7 @@
 
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
+import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
 import static com.android.server.pm.ApexManager.ActiveApexInfo;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
@@ -34,6 +35,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
@@ -56,9 +58,16 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.pm.PackageDexOptimizer.DexOptResult;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 
 import dalvik.system.DexFile;
@@ -72,11 +81,15 @@
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
-final class DexOptHelper {
+/**
+ * Helper class for dex optimization operations in PackageManagerService.
+ */
+public final class DexOptHelper {
     private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
 
     private final PackageManagerService mPm;
@@ -405,11 +418,12 @@
      * {@link PackageDexOptimizer#DEX_OPT_CANCELLED}
      * {@link PackageDexOptimizer#DEX_OPT_FAILED}
      */
-    @PackageDexOptimizer.DexOptResult
+    @DexOptResult
     /* package */ int performDexOptWithStatus(DexoptOptions options) {
         return performDexOptTraced(options);
     }
 
+    @DexOptResult
     private int performDexOptTraced(DexoptOptions options) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
         try {
@@ -421,7 +435,13 @@
 
     // Run dexopt on a given package. Returns true if dexopt did not fail, i.e.
     // if the package can now be considered up to date for the given filter.
+    @DexOptResult
     private int performDexOptInternal(DexoptOptions options) {
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        if (artSrvRes.isPresent()) {
+            return artSrvRes.get();
+        }
+
         AndroidPackage p;
         PackageSetting pkgSetting;
         synchronized (mPm.mLock) {
@@ -446,8 +466,74 @@
         }
     }
 
-    private int performDexOptInternalWithDependenciesLI(AndroidPackage p,
-            @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
+    /**
+     * Performs dexopt on the given package using ART Service.
+     *
+     * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is
+     *     necessary to fall back to the legacy code paths.
+     */
+    private Optional<Integer> performDexOptWithArtService(DexoptOptions options) {
+        ArtManagerLocal artManager = getArtManagerLocal();
+        if (artManager == null) {
+            return Optional.empty();
+        }
+
+        try (PackageManagerLocal.FilteredSnapshot snapshot =
+                        getPackageManagerLocal().withFilteredSnapshot()) {
+            PackageState ops = snapshot.getPackageState(options.getPackageName());
+            if (ops == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            AndroidPackage oap = ops.getAndroidPackage();
+            if (oap == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            if (oap.isApex()) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED);
+            }
+
+            // TODO(b/245301593): Delete the conditional when ART Service supports
+            // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally.
+            /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty()
+                    ? 0
+                    : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
+
+            OptimizeParams params = options.convertToOptimizeParams(extraFlags);
+            if (params == null) {
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here.
+            OptimizeResult result;
+            try {
+                result = artManager.optimizePackage(snapshot, options.getPackageName(), params);
+            } catch (UnsupportedOperationException e) {
+                reportArtManagerFallback(options.getPackageName(), e.toString());
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when
+            // it is implemented.
+            for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) {
+                PackageState ps = snapshot.getPackageState(pkgRes.getPackageName());
+                AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null;
+                if (ap != null) {
+                    CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap);
+                    for (OptimizeResult.DexContainerFileOptimizeResult dexRes :
+                            pkgRes.getDexContainerFileOptimizeResults()) {
+                        stats.setCompileTime(
+                                dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis());
+                    }
+                }
+            }
+
+            return Optional.of(convertToDexOptResult(result));
+        }
+    }
+
+    @DexOptResult
+    private int performDexOptInternalWithDependenciesLI(
+            AndroidPackage p, @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
         // System server gets a special path.
         if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) {
             return mPm.getDexManager().dexoptSystemServer(options);
@@ -514,10 +600,20 @@
 
         // Whoever is calling forceDexOpt wants a compiled package.
         // Don't use profiles since that may cause compilation to be skipped.
-        final int res = performDexOptInternalWithDependenciesLI(pkg, packageState,
-                new DexoptOptions(packageName, REASON_CMDLINE,
-                        getDefaultCompilerFilter(), null /* splitName */,
-                        DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE));
+        DexoptOptions options = new DexoptOptions(packageName, REASON_CMDLINE,
+                getDefaultCompilerFilter(), null /* splitName */,
+                DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE);
+
+        // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with
+        // the package checks above, but at worst the effect is only a bit less friendly error
+        // below.
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        int res;
+        if (artSrvRes.isPresent()) {
+            res = artSrvRes.get();
+        } else {
+            res = performDexOptInternalWithDependenciesLI(pkg, packageState, options);
+        }
 
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) {
@@ -800,4 +896,59 @@
         }
         return false;
     }
+
+    private @NonNull PackageManagerLocal getPackageManagerLocal() {
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Called whenever we need to fall back from ART Service to the legacy dexopt code.
+     */
+    public static void reportArtManagerFallback(String packageName, String reason) {
+        // STOPSHIP(b/251903639): Minimize these calls to avoid platform getting shipped with code
+        // paths that will always bypass ART Service.
+        Slog.i(TAG, "Falling back to old PackageManager dexopt for " + packageName + ": " + reason);
+    }
+
+    /**
+     * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization.
+     */
+    private @Nullable ArtManagerLocal getArtManagerLocal() {
+        if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) {
+            return null;
+        }
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Converts an ART Service {@link OptimizeResult} to {@link DexOptResult}.
+     *
+     * For interfacing {@link ArtManagerLocal} with legacy dex optimization code in PackageManager.
+     */
+    @DexOptResult
+    private static int convertToDexOptResult(OptimizeResult result) {
+        /*@OptimizeStatus*/ int status = result.getFinalStatus();
+        switch (status) {
+            case OptimizeResult.OPTIMIZE_SKIPPED:
+                return PackageDexOptimizer.DEX_OPT_SKIPPED;
+            case OptimizeResult.OPTIMIZE_FAILED:
+                return PackageDexOptimizer.DEX_OPT_FAILED;
+            case OptimizeResult.OPTIMIZE_PERFORMED:
+                return PackageDexOptimizer.DEX_OPT_PERFORMED;
+            case OptimizeResult.OPTIMIZE_CANCELLED:
+                return PackageDexOptimizer.DEX_OPT_CANCELLED;
+            default:
+                throw new IllegalArgumentException("OptimizeResult for "
+                        + result.getPackageOptimizeResults().get(0).getPackageName()
+                        + " has unsupported status " + status);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index f6472a7..12b5ab8 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -72,7 +72,6 @@
     private final int mSystemScanFlags;
     private final InstallPackageHelper mInstallPackageHelper;
     private final ApexManager mApexManager;
-    private final ApexPackageInfo mApexPackageInfo;
     private final ExecutorService mExecutorService;
     /* Tracks how long system scan took */
     private long mSystemScanTime;
@@ -96,13 +95,11 @@
     private final List<String> mStubSystemApps = new ArrayList<>();
 
     // TODO(b/198166813): remove PMS dependency
-    InitAppsHelper(PackageManagerService pm,
-            ApexManager apexManager, ApexPackageInfo apexPackageInfo,
+    InitAppsHelper(PackageManagerService pm, ApexManager apexManager,
             InstallPackageHelper installPackageHelper,
             List<ScanPartition> systemPartitions) {
         mPm = pm;
         mApexManager = apexManager;
-        mApexPackageInfo = apexPackageInfo;
         mInstallPackageHelper = installPackageHelper;
         mSystemPartitions = systemPartitions;
         mDirsToScanAsSystem = getSystemScanPartitions();
@@ -165,16 +162,8 @@
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanApexPackages");
 
         try {
-            final List<ApexManager.ScanResult> apexScanResults;
-            if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                apexScanResults = mInstallPackageHelper.scanApexPackages(
-                        mApexManager.getAllApexInfos(), mSystemParseFlags, mSystemScanFlags,
-                        packageParser, mExecutorService);
-            } else {
-                apexScanResults = mApexPackageInfo.scanApexPackages(
-                        mApexManager.getAllApexInfos(), packageParser, mExecutorService);
-            }
-            return apexScanResults;
+            return mInstallPackageHelper.scanApexPackages(mApexManager.getAllApexInfos(),
+                    mSystemParseFlags, mSystemScanFlags, packageParser, mExecutorService);
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
@@ -352,8 +341,7 @@
     }
 
     @GuardedBy({"mPm.mInstallLock", "mPm.mLock"})
-    private void scanDirTracedLI(File scanDir,
-            int parseFlags, int scanFlags,
+    private void scanDirTracedLI(File scanDir, int parseFlags, int scanFlags,
             PackageParser2 packageParser, ExecutorService executorService) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
         try {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 30ecc1c..70bd24c 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -150,7 +150,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.content.F2fsUtils;
-import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.security.VerityUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
@@ -245,50 +244,42 @@
     @GuardedBy("mPm.mLock")
     public AndroidPackage commitReconciledScanResultLocked(
             @NonNull ReconciledPackage reconciledPkg, int[] allUsers) {
-        final ScanResult result = reconciledPkg.mScanResult;
-        final ScanRequest request = result.mRequest;
+        final InstallRequest request = reconciledPkg.mInstallRequest;
         // TODO(b/135203078): Move this even further away
-        ParsedPackage parsedPackage = request.mParsedPackage;
-        if ("android".equals(parsedPackage.getPackageName())) {
+        ParsedPackage parsedPackage = request.getParsedPackage();
+        if (parsedPackage != null && "android".equals(parsedPackage.getPackageName())) {
             // TODO(b/135203078): Move this to initial parse
             parsedPackage.setVersionCode(mPm.getSdkVersion())
                     .setVersionCodeMajor(0);
         }
 
-        final AndroidPackage oldPkg = request.mOldPkg;
-        final @ParsingPackageUtils.ParseFlags int parseFlags = request.mParseFlags;
-        final @PackageManagerService.ScanFlags int scanFlags = request.mScanFlags;
-        final PackageSetting oldPkgSetting = request.mOldPkgSetting;
-        final PackageSetting originalPkgSetting = request.mOriginalPkgSetting;
-        final UserHandle user = request.mUser;
-        final String realPkgName = request.mRealPkgName;
-        final List<String> changedAbiCodePath = result.mChangedAbiCodePath;
+        final @PackageManagerService.ScanFlags int scanFlags = request.getScanFlags();
+        final PackageSetting oldPkgSetting = request.getScanRequestOldPackageSetting();
+        final PackageSetting originalPkgSetting = request.getScanRequestOriginalPackageSetting();
+        final String realPkgName = request.getRealPackageName();
+        final List<String> changedAbiCodePath = request.getChangedAbiCodePath();
         final PackageSetting pkgSetting;
-        if (request.mPkgSetting != null) {
+        if (request.getScanRequestPackageSetting() != null) {
             SharedUserSetting requestSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr(
-                    request.mPkgSetting);
+                    request.getScanRequestPackageSetting());
             SharedUserSetting resultSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr(
-                    result.mPkgSetting);
+                    request.getScanRequestPackageSetting());
             if (requestSharedUserSetting != null
                     && requestSharedUserSetting != resultSharedUserSetting) {
                 // shared user changed, remove from old shared user
-                requestSharedUserSetting.removePackage(request.mPkgSetting);
+                requestSharedUserSetting.removePackage(request.getScanRequestPackageSetting());
                 // Prune unused SharedUserSetting
                 if (mPm.mSettings.checkAndPruneSharedUserLPw(requestSharedUserSetting, false)) {
                     // Set the app ID in removed info for UID_REMOVED broadcasts
-                    if (reconciledPkg.mInstallRequest != null
-                            && reconciledPkg.mInstallRequest.getRemovedInfo() != null) {
-                        reconciledPkg.mInstallRequest.getRemovedInfo().mRemovedAppId =
-                                requestSharedUserSetting.mAppId;
-                    }
+                    request.setRemovedAppId(requestSharedUserSetting.mAppId);
                 }
             }
         }
-        if (result.mExistingSettingCopied) {
-            pkgSetting = request.mPkgSetting;
-            pkgSetting.updateFrom(result.mPkgSetting);
+        if (request.isExistingSettingCopied()) {
+            pkgSetting = request.getScanRequestPackageSetting();
+            pkgSetting.updateFrom(request.getScannedPackageSetting());
         } else {
-            pkgSetting = result.mPkgSetting;
+            pkgSetting = request.getScannedPackageSetting();
             if (originalPkgSetting != null) {
                 mPm.mSettings.addRenamedPackageLPw(
                         AndroidPackageUtils.getRealPackageOrNull(parsedPackage),
@@ -308,26 +299,23 @@
                 mPm.mSettings.convertSharedUserSettingsLPw(sharedUserSetting);
             }
         }
-        if (reconciledPkg.mInstallRequest != null
-                && reconciledPkg.mInstallRequest.isForceQueryableOverride()) {
+        if (request.isForceQueryableOverride()) {
             pkgSetting.setForceQueryableOverride(true);
         }
 
         // If this is part of a standard install, set the initiating package name, else rely on
         // previous device state.
-        if (reconciledPkg.mInstallRequest != null) {
-            InstallSource installSource = reconciledPkg.mInstallRequest.getInstallSource();
-            if (installSource != null) {
-                if (installSource.initiatingPackageName != null) {
-                    final PackageSetting ips = mPm.mSettings.getPackageLPr(
-                            installSource.initiatingPackageName);
-                    if (ips != null) {
-                        installSource = installSource.setInitiatingPackageSignatures(
-                                ips.getSignatures());
-                    }
+        InstallSource installSource = request.getInstallSource();
+        if (installSource != null) {
+            if (installSource.initiatingPackageName != null) {
+                final PackageSetting ips = mPm.mSettings.getPackageLPr(
+                        installSource.initiatingPackageName);
+                if (ips != null) {
+                    installSource = installSource.setInitiatingPackageSignatures(
+                            ips.getSignatures());
                 }
-                pkgSetting.setInstallSource(installSource);
             }
+            pkgSetting.setInstallSource(installSource);
         }
 
         if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
@@ -382,10 +370,9 @@
             }
         }
 
-        final int userId = user == null ? 0 : user.getIdentifier();
+        final int userId = request.getUserId();
         // Modify state for the given package setting
-        commitPackageSettings(pkg, oldPkg, pkgSetting, oldPkgSetting, scanFlags,
-                (parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0 /*chatty*/, reconciledPkg);
+        commitPackageSettings(pkg, pkgSetting, oldPkgSetting, reconciledPkg);
         if (pkgSetting.getInstantApp(userId)) {
             mPm.mInstantAppRegistry.addInstantApp(userId, pkgSetting.getAppId());
         }
@@ -401,11 +388,14 @@
      * Adds a scanned package to the system. When this method is finished, the package will
      * be available for query, resolution, etc...
      */
-    private void commitPackageSettings(@NonNull AndroidPackage pkg, @Nullable AndroidPackage oldPkg,
+    private void commitPackageSettings(@NonNull AndroidPackage pkg,
             @NonNull PackageSetting pkgSetting, @Nullable PackageSetting oldPkgSetting,
-            final @PackageManagerService.ScanFlags int scanFlags, boolean chatty,
             ReconciledPackage reconciledPkg) {
         final String pkgName = pkg.getPackageName();
+        final InstallRequest request = reconciledPkg.mInstallRequest;
+        final AndroidPackage oldPkg = request.getScanRequestOldPackage();
+        final int scanFlags = request.getScanFlags();
+        final boolean chatty = (request.getParseFlags() & ParsingPackageUtils.PARSE_CHATTY) != 0;
         if (mPm.mCustomResolverComponentName != null
                 && mPm.mCustomResolverComponentName.getPackageName().equals(pkg.getPackageName())) {
             mPm.setUpCustomResolverActivity(pkg, pkgSetting);
@@ -421,9 +411,7 @@
                         reconciledPkg.mAllowedSharedLibraryInfos,
                         reconciledPkg.getCombinedAvailablePackages(), scanFlags);
 
-        if (reconciledPkg.mInstallRequest != null) {
-            reconciledPkg.mInstallRequest.setLibraryConsumers(clientLibPkgs);
-        }
+        request.setLibraryConsumers(clientLibPkgs);
 
         if ((scanFlags & SCAN_BOOTING) != 0) {
             // No apps can run during boot scan, so they don't need to be frozen
@@ -438,8 +426,7 @@
             mPm.snapshotComputer().checkPackageFrozen(pkgName);
         }
 
-        final boolean isReplace =
-                reconciledPkg.mPrepareResult != null && reconciledPkg.mPrepareResult.mReplace;
+        final boolean isReplace = request.isInstallReplace();
         // Also need to kill any apps that are dependent on the library, except the case of
         // installation of new version static shared library.
         if (clientLibPkgs != null) {
@@ -475,7 +462,8 @@
 
             final Computer snapshot = mPm.snapshotComputer();
             mPm.mComponentResolver.addAllComponents(pkg, chatty, mPm.mSetupWizardPackage, snapshot);
-            mPm.mAppsFilter.addPackage(snapshot, pkgSetting, isReplace);
+            mPm.mAppsFilter.addPackage(snapshot, pkgSetting, isReplace,
+                    (scanFlags & SCAN_DONT_KILL_APP) != 0 /* retainImplicitGrantOnReplace */);
             mPm.addAllPackageProperties(pkg);
 
             if (oldPkgSetting == null || oldPkgSetting.getPkg() == null) {
@@ -705,6 +693,9 @@
      * Returns whether the restore successfully completed.
      */
     private boolean performBackupManagerRestore(int userId, int token, InstallRequest request) {
+        if (request.getPkg() == null) {
+            return false;
+        }
         IBackupManager iBackupManager = mInjector.getIBackupManager();
         if (iBackupManager != null) {
             // For backwards compatibility as USER_ALL previously routed directly to USER_SYSTEM
@@ -743,6 +734,9 @@
      * Returns whether the restore successfully completed.
      */
     private boolean performRollbackManagerRestore(int userId, int token, InstallRequest request) {
+        if (request.getPkg() == null) {
+            return false;
+        }
         final String packageName = request.getPkg().getPackageName();
         final int[] allUsers = mPm.mUserManager.getUserIds();
         final int[] installedUsers;
@@ -810,22 +804,17 @@
      */
     @GuardedBy("mPm.mInstallLock")
     private void installPackagesLI(List<InstallRequest> requests) {
-        final Map<String, ScanResult> preparedScans = new ArrayMap<>(requests.size());
-        final Map<String, PrepareResult> prepareResults = new ArrayMap<>(requests.size());
-        final Map<String, InstallRequest> installRequests = new ArrayMap<>(requests.size());
+        final Set<String> scannedPackages = new ArraySet<>(requests.size());
         final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
         final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
         boolean success = false;
         try {
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
             for (InstallRequest request : requests) {
-                // TODO(b/109941548): remove this once we've pulled everything from it and into
-                //                    scan, reconcile or commit.
-                final PrepareResult prepareResult;
                 try {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
-                    prepareResult =
-                            preparePackageLI(request);
+                    request.onPrepareStarted();
+                    preparePackageLI(request);
                 } catch (PrepareFailure prepareFailure) {
                     request.setError(prepareFailure.error,
                             prepareFailure.getMessage());
@@ -833,30 +822,36 @@
                     request.setOriginPermission(prepareFailure.mConflictingPermission);
                     return;
                 } finally {
+                    request.onPrepareFinished();
                     Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                 }
-                request.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
-                request.setInstallerPackageName(request.getSourceInstallerPackageName());
 
-                final String packageName = prepareResult.mPackageToScan.getPackageName();
-                prepareResults.put(packageName, prepareResult);
-                installRequests.put(packageName, request);
+                final ParsedPackage packageToScan = request.getParsedPackage();
+                if (packageToScan == null) {
+                    request.setError(INSTALL_FAILED_SESSION_INVALID,
+                            "Failed to obtain package to scan");
+                    return;
+                }
+                request.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
+                final String packageName = packageToScan.getPackageName();
                 try {
-                    final ScanResult result = scanPackageTracedLI(
-                            prepareResult.mPackageToScan, prepareResult.mParseFlags,
-                            prepareResult.mScanFlags, System.currentTimeMillis(),
-                            request.getUser(), request.getAbiOverride());
-                    if (null != preparedScans.put(result.mPkgSetting.getPkg().getPackageName(),
-                            result)) {
+                    request.onScanStarted();
+                    final ScanResult scanResult = scanPackageTracedLI(request.getParsedPackage(),
+                            request.getParseFlags(), request.getScanFlags(),
+                            System.currentTimeMillis(), request.getUser(),
+                            request.getAbiOverride());
+                    request.setScanResult(scanResult);
+                    request.onScanFinished();
+                    if (!scannedPackages.add(packageName)) {
                         request.setError(
                                 PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE,
                                 "Duplicate package "
-                                        + result.mPkgSetting.getPkg().getPackageName()
+                                        + packageName
                                         + " in multi-package install request.");
                         return;
                     }
                     if (!checkNoAppStorageIsConsistent(
-                            result.mRequest.mOldPkg, result.mPkgSetting.getPkg())) {
+                            request.getScanRequestOldPackage(), packageToScan)) {
                         // TODO: INSTALL_FAILED_UPDATE_INCOMPATIBLE is about incomptabible
                         //  signatures. Is there a better error code?
                         request.setError(
@@ -865,31 +860,28 @@
                                         + PackageManager.PROPERTY_NO_APP_DATA_STORAGE);
                         return;
                     }
-                    final boolean isApex = (result.mRequest.mScanFlags & SCAN_AS_APEX) != 0;
+                    final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
                     if (!isApex) {
-                        createdAppId.put(packageName, optimisticallyRegisterAppId(result));
+                        createdAppId.put(packageName, optimisticallyRegisterAppId(request));
                     } else {
-                        result.mPkgSetting.setAppId(Process.INVALID_UID);
+                        request.getScannedPackageSetting().setAppId(Process.INVALID_UID);
                     }
-                    versionInfos.put(result.mPkgSetting.getPkg().getPackageName(),
-                            mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
+                    versionInfos.put(packageName,
+                            mPm.getSettingsVersionForPackage(packageToScan));
                 } catch (PackageManagerException e) {
                     request.setError("Scanning Failed.", e);
                     return;
                 }
             }
 
-            CommitRequest commitRequest;
+            List<ReconciledPackage> reconciledPackages;
             synchronized (mPm.mLock) {
-                ReconcileRequest reconcileRequest = new ReconcileRequest(preparedScans,
-                        installRequests, prepareResults,
-                        Collections.unmodifiableMap(mPm.mPackages), versionInfos);
-                Map<String, ReconciledPackage> reconciledPackages;
                 try {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
                     reconciledPackages = ReconcilePackageUtils.reconcilePackages(
-                            reconcileRequest, mSharedLibraries,
-                            mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
+                            requests, Collections.unmodifiableMap(mPm.mPackages),
+                            versionInfos, mSharedLibraries, mPm.mSettings.getKeySetManagerService(),
+                            mPm.mSettings);
                 } catch (ReconcileFailure e) {
                     for (InstallRequest request : requests) {
                         request.setError("Reconciliation failed...", e);
@@ -900,15 +892,13 @@
                 }
                 try {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
-                    commitRequest = new CommitRequest(reconciledPackages,
-                            mPm.mUserManager.getUserIds());
-                    commitPackagesLocked(commitRequest);
+                    commitPackagesLocked(reconciledPackages, mPm.mUserManager.getUserIds());
                     success = true;
                 } finally {
                     Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                 }
             }
-            executePostCommitStepsLIF(commitRequest);
+            executePostCommitStepsLIF(reconciledPackages);
         } finally {
             if (success) {
                 for (InstallRequest request : requests) {
@@ -932,10 +922,10 @@
                             request.getDataLoaderType(), request.getUser(), mContext);
                 }
             } else {
-                for (ScanResult result : preparedScans.values()) {
-                    if (createdAppId.getOrDefault(result.mRequest.mParsedPackage.getPackageName(),
-                            false)) {
-                        cleanUpAppIdCreation(result);
+                for (InstallRequest installRequest : requests) {
+                    if (installRequest.getParsedPackage() != null && createdAppId.getOrDefault(
+                            installRequest.getParsedPackage().getPackageName(), false)) {
+                        cleanUpAppIdCreation(installRequest);
                     }
                 }
                 // TODO(b/194319951): create a more descriptive reason than unknown
@@ -968,8 +958,7 @@
     }
 
     @GuardedBy("mPm.mInstallLock")
-    private PrepareResult preparePackageLI(InstallRequest request)
-            throws PrepareFailure {
+    private void preparePackageLI(InstallRequest request) throws PrepareFailure {
         final int installFlags = request.getInstallFlags();
         final boolean onExternal = request.getVolumeUuid() != null;
         final boolean instantApp = ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0);
@@ -980,7 +969,7 @@
         final boolean isRollback =
                 request.getInstallReason() == PackageManager.INSTALL_REASON_ROLLBACK;
         @PackageManagerService.ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE;
-        if (request.isMoveInstall()) {
+        if (request.isInstallMove()) {
             // moving a complete application; perform an initial scan on the new install location
             scanFlags |= SCAN_INITIAL;
         }
@@ -1363,7 +1352,7 @@
             }
         }
 
-        if (request.isMoveInstall()) {
+        if (request.isInstallMove()) {
             // We did an in-place move, so dex is ready to roll
             scanFlags |= SCAN_NO_DEX;
             scanFlags |= SCAN_MOVE;
@@ -1413,7 +1402,7 @@
             doRenameLI(request, parsedPackage);
 
             try {
-                setUpFsVerityIfPossible(parsedPackage);
+                setUpFsVerity(parsedPackage);
             } catch (Installer.InstallerException | IOException | DigestException
                     | NoSuchAlgorithmException e) {
                 throw new PrepareFailure(INSTALL_FAILED_INTERNAL_ERROR,
@@ -1550,8 +1539,7 @@
 
                     // don't allow an upgrade from full to ephemeral
                     if (isInstantApp) {
-                        if (request.getUser() == null
-                                || request.getUserId() == UserHandle.USER_ALL) {
+                        if (request.getUserId() == UserHandle.USER_ALL) {
                             for (int currentUser : allUsers) {
                                 if (!ps.getInstantApp(currentUser)) {
                                     // can't downgrade from full to instant
@@ -1624,7 +1612,6 @@
                     targetParseFlags = systemParseFlags;
                     targetScanFlags = systemScanFlags;
                 } else { // non system replace
-                    replace = true;
                     if (DEBUG_INSTALL) {
                         Slog.d(TAG,
                                 "replaceNonSystemPackageLI: new=" + parsedPackage + ", old="
@@ -1634,7 +1621,6 @@
             } else { // new package install
                 ps = null;
                 disabledPs = null;
-                replace = false;
                 oldPackage = null;
                 // Remember this for later, in case we need to rollback this install
                 String pkgName1 = parsedPackage.getPackageName();
@@ -1665,7 +1651,7 @@
             // we're passing the freezer back to be closed in a later phase of install
             shouldCloseFreezerBeforeReturn = false;
 
-            return new PrepareResult(replace, targetScanFlags, targetParseFlags,
+            request.setPrepareResult(replace, targetScanFlags, targetParseFlags,
                     oldPackage, parsedPackage, replace /* clearCodeCache */, sysPkg,
                     ps, disabledPs);
         } finally {
@@ -1685,7 +1671,7 @@
             ParsedPackage parsedPackage) throws PrepareFailure {
         final int status = request.getReturnCode();
         final String statusMsg = request.getReturnMsg();
-        if (request.isMoveInstall()) {
+        if (request.isInstallMove()) {
             if (status != PackageManager.INSTALL_SUCCEEDED) {
                 mRemovePackageHelper.cleanUpForMoveInstall(request.getMoveToUuid(),
                         request.getMovePackageName(), request.getMoveFromCodePath());
@@ -1813,13 +1799,10 @@
     }
 
     /**
-     * Set up fs-verity for the given package if possible.  This requires a feature flag of system
-     * property to be enabled only if the kernel supports fs-verity.
-     *
-     * <p>When the feature flag is set to legacy mode, only APK is supported (with some experimental
-     * kernel patches). In normal mode, all file format can be supported.
+     * Set up fs-verity for the given package. For older devices that do not support fs-verity,
+     * this is a no-op.
      */
-    private void setUpFsVerityIfPossible(AndroidPackage pkg) throws Installer.InstallerException,
+    private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException,
             PrepareFailure, IOException, DigestException, NoSuchAlgorithmException {
         if (!PackageManagerServiceUtils.isApkVerityEnabled()) {
             return;
@@ -1854,17 +1837,22 @@
         }
 
         for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) {
-            final String filePath = entry.getKey();
-            final String signaturePath = entry.getValue();
-
-            // fs-verity is optional for now.  Only set up if signature is provided.
-            if (new File(signaturePath).exists() && !VerityUtils.hasFsverity(filePath)) {
-                try {
-                    VerityUtils.setUpFsverity(filePath, signaturePath);
-                } catch (IOException e) {
-                    throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
-                            "Failed to enable fs-verity: " + e);
+            try {
+                final String filePath = entry.getKey();
+                if (VerityUtils.hasFsverity(filePath)) {
+                    continue;
                 }
+
+                // Set up fs-verity with optional signature.
+                final String signaturePath = entry.getValue();
+                String optionalSignaturePath = null;
+                if (new File(signaturePath).exists()) {
+                    optionalSignaturePath = signaturePath;
+                }
+                VerityUtils.setUpFsverity(filePath, optionalSignaturePath);
+            } catch (IOException e) {
+                throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
+                        "Failed to enable fs-verity: " + e);
             }
         }
     }
@@ -1894,19 +1882,19 @@
     }
 
     @GuardedBy("mPm.mLock")
-    private void commitPackagesLocked(final CommitRequest request) {
+    private void commitPackagesLocked(List<ReconciledPackage> reconciledPackages,
+            @NonNull int[] allUsers) {
         // TODO: remove any expected failures from this method; this should only be able to fail due
         //       to unavoidable errors (I/O, etc.)
-        for (ReconciledPackage reconciledPkg : request.mReconciledPackages.values()) {
-            final ScanResult scanResult = reconciledPkg.mScanResult;
-            final ScanRequest scanRequest = scanResult.mRequest;
-            final ParsedPackage parsedPackage = scanRequest.mParsedPackage;
-            final String packageName = parsedPackage.getPackageName();
+        for (ReconciledPackage reconciledPkg : reconciledPackages) {
             final InstallRequest installRequest = reconciledPkg.mInstallRequest;
+            final ParsedPackage parsedPackage = installRequest.getParsedPackage();
+            final String packageName = parsedPackage.getPackageName();
             final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
             final DeletePackageHelper deletePackageHelper = new DeletePackageHelper(mPm);
 
-            if (reconciledPkg.mPrepareResult.mReplace) {
+            installRequest.onCommitStarted();
+            if (installRequest.isInstallReplace()) {
                 AndroidPackage oldPackage = mPm.mPackages.get(packageName);
 
                 // Set the update and install times
@@ -1914,15 +1902,16 @@
                         .getPackageStateInternal(oldPackage.getPackageName());
                 // TODO(b/225756739): For rebootless APEX, consider using lastUpdateMillis provided
                 //  by apexd to be more accurate.
-                reconciledPkg.mPkgSetting
-                        .setFirstInstallTimeFromReplaced(deletedPkgSetting, request.mAllUsers)
-                        .setLastUpdateTime(System.currentTimeMillis());
+                installRequest.setScannedPackageSettingFirstInstallTimeFromReplaced(
+                        deletedPkgSetting, allUsers);
+                installRequest.setScannedPackageSettingLastUpdateTime(
+                        System.currentTimeMillis());
 
                 installRequest.getRemovedInfo().mBroadcastAllowList =
                         mPm.mAppsFilter.getVisibilityAllowList(mPm.snapshotComputer(),
-                                reconciledPkg.mPkgSetting, request.mAllUsers,
-                                mPm.mSettings.getPackagesLocked());
-                if (reconciledPkg.mPrepareResult.mSystem) {
+                                installRequest.getScannedPackageSetting(),
+                                allUsers, mPm.mSettings.getPackagesLocked());
+                if (installRequest.isInstallSystem()) {
                     // Remove existing system package
                     removePackageHelper.removePackage(oldPackage, true);
                     if (!disableSystemPackageLPw(oldPackage)) {
@@ -1942,7 +1931,7 @@
                         // Settings will be written during the call to updateSettingsLI().
                         deletePackageHelper.executeDeletePackage(
                                 reconciledPkg.mDeletePackageAction, packageName,
-                                true, request.mAllUsers, false);
+                                true, allUsers, false);
                     } catch (SystemDeleteException e) {
                         if (mPm.mIsEngBuild) {
                             throw new RuntimeException("Unexpected failure", e);
@@ -1952,7 +1941,7 @@
                     // Successfully deleted the old package; proceed with replace.
                     // Update the in-memory copy of the previous code paths.
                     PackageSetting ps1 = mPm.mSettings.getPackageLPr(
-                            reconciledPkg.mPrepareResult.mExistingPackage.getPackageName());
+                            installRequest.getExistingPackageName());
                     if ((installRequest.getInstallFlags() & PackageManager.DONT_KILL_APP)
                             == 0) {
                         Set<String> oldCodePaths = ps1.getOldCodePaths();
@@ -1977,9 +1966,8 @@
                 }
             }
 
-            AndroidPackage pkg = commitReconciledScanResultLocked(
-                    reconciledPkg, request.mAllUsers);
-            updateSettingsLI(pkg, reconciledPkg, request.mAllUsers, installRequest);
+            AndroidPackage pkg = commitReconciledScanResultLocked(reconciledPkg, allUsers);
+            updateSettingsLI(pkg, allUsers, installRequest);
 
             final PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
             if (ps != null) {
@@ -1991,6 +1979,7 @@
                 mPm.updateSequenceNumberLP(ps, installRequest.getNewUsers());
                 mPm.updateInstantAppInstallerLocked(packageName);
             }
+            installRequest.onCommitFinished();
         }
         ApplicationPackageManager.invalidateGetPackagesForUidCache();
     }
@@ -2000,12 +1989,12 @@
         return mPm.mSettings.disableSystemPackageLPw(oldPkg.getPackageName(), true);
     }
 
-    private void updateSettingsLI(AndroidPackage newPackage, ReconciledPackage reconciledPkg,
+    private void updateSettingsLI(AndroidPackage newPackage,
             int[] allUsers, InstallRequest installRequest) {
-        updateSettingsInternalLI(newPackage, reconciledPkg, allUsers, installRequest);
+        updateSettingsInternalLI(newPackage, allUsers, installRequest);
     }
 
-    private void updateSettingsInternalLI(AndroidPackage pkg, ReconciledPackage reconciledPkg,
+    private void updateSettingsInternalLI(AndroidPackage pkg,
             int[] allUsers, InstallRequest installRequest) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "updateSettings");
 
@@ -2175,8 +2164,7 @@
                 }
                 final int autoRevokePermissionsMode = installRequest.getAutoRevokePermissionsMode();
                 permissionParamsBuilder.setAutoRevokePermissionsMode(autoRevokePermissionsMode);
-                final ScanResult scanResult = reconciledPkg.mScanResult;
-                mPm.mPermissionManager.onPackageInstalled(pkg, scanResult.mPreviousAppId,
+                mPm.mPermissionManager.onPackageInstalled(pkg, installRequest.getPreviousAppId(),
                         permissionParamsBuilder.build(), userId);
                 // Apply restricted settings on potentially dangerous packages.
                 if (installRequest.getPackageSource() == PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
@@ -2216,14 +2204,13 @@
      * locks on {@link com.android.server.pm.PackageManagerService.mLock}.
      */
     @GuardedBy("mPm.mInstallLock")
-    private void executePostCommitStepsLIF(CommitRequest commitRequest) {
+    private void executePostCommitStepsLIF(List<ReconciledPackage> reconciledPackages) {
         final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
-        for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
-            final boolean instantApp = ((reconciledPkg.mScanResult.mRequest.mScanFlags
-                    & SCAN_AS_INSTANT_APP) != 0);
-            final boolean isApex = ((reconciledPkg.mScanResult.mRequest.mScanFlags
-                    & SCAN_AS_APEX) != 0);
-            final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
+        for (ReconciledPackage reconciledPkg : reconciledPackages) {
+            final InstallRequest installRequest = reconciledPkg.mInstallRequest;
+            final boolean instantApp = ((installRequest.getScanFlags() & SCAN_AS_INSTANT_APP) != 0);
+            final boolean isApex = ((installRequest.getScanFlags() & SCAN_AS_APEX) != 0);
+            final AndroidPackage pkg = installRequest.getScannedPackageSetting().getPkg();
             final String packageName = pkg.getPackageName();
             final String codePath = pkg.getPath();
             final boolean onIncremental = mIncrementalManager != null
@@ -2238,12 +2225,12 @@
             }
             // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088)
             mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0);
-            if (reconciledPkg.mPrepareResult.mClearCodeCache) {
+            if (installRequest.isClearCodeCache()) {
                 mAppDataHelper.clearAppDataLIF(pkg, UserHandle.USER_ALL,
                         FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
                                 | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
             }
-            if (reconciledPkg.mPrepareResult.mReplace) {
+            if (installRequest.isInstallReplace()) {
                 mDexManager.notifyPackageUpdated(pkg.getPackageName(),
                         pkg.getBaseApkPath(), pkg.getSplitCodePaths());
             }
@@ -2252,14 +2239,13 @@
             // This needs to be done before invoking dexopt so that any install-time profile
             // can be used for optimizations.
             mArtManagerService.prepareAppProfiles(
-                    pkg,
-                    mPm.resolveUserIds(reconciledPkg.mInstallRequest.getUserId()),
+                    pkg, mPm.resolveUserIds(installRequest.getUserId()),
                     /* updateReferenceProfileContent= */ true);
 
             // Compute the compilation reason from the installation scenario.
             final int compilationReason =
                     mDexManager.getCompilationReasonForInstallScenario(
-                            reconciledPkg.mInstallRequest.getInstallScenario());
+                            installRequest.getInstallScenario());
 
             // Construct the DexoptOptions early to see if we should skip running dexopt.
             //
@@ -2268,10 +2254,8 @@
             //
             // Also, don't fail application installs if the dexopt step fails.
             final boolean isBackupOrRestore =
-                    reconciledPkg.mInstallRequest.getInstallReason()
-                            == INSTALL_REASON_DEVICE_RESTORE
-                            || reconciledPkg.mInstallRequest.getInstallReason()
-                            == INSTALL_REASON_DEVICE_SETUP;
+                    installRequest.getInstallReason() == INSTALL_REASON_DEVICE_RESTORE
+                            || installRequest.getInstallReason() == INSTALL_REASON_DEVICE_SETUP;
 
             final int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
                     | DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE
@@ -2323,22 +2307,14 @@
                 }
 
                 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
-                ScanResult result = reconciledPkg.mScanResult;
 
                 // This mirrors logic from commitReconciledScanResultLocked, where the library files
                 // needed for dexopt are assigned.
-                // TODO: Fix this to have 1 mutable PackageSetting for scan/install. If the previous
-                //  setting needs to be passed to have a comparison, hide it behind an immutable
-                //  interface. There's no good reason to have 3 different ways to access the real
-                //  PackageSetting object, only one of which is actually correct.
-                PackageSetting realPkgSetting = result.mExistingSettingCopied
-                        ? result.mRequest.mPkgSetting : result.mPkgSetting;
-                if (realPkgSetting == null) {
-                    realPkgSetting = reconciledPkg.mPkgSetting;
-                }
+                PackageSetting realPkgSetting = installRequest.getRealPackageSetting();
 
                 // Unfortunately, the updated system app flag is only tracked on this PackageSetting
-                boolean isUpdatedSystemApp = reconciledPkg.mPkgSetting.getPkgState()
+                boolean isUpdatedSystemApp =
+                        installRequest.getScannedPackageSetting().getPkgState()
                         .isUpdatedSystemApp();
 
                 realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
@@ -2361,45 +2337,6 @@
                 incrementalStorages);
     }
 
-    public int installLocationPolicy(PackageInfoLite pkgLite, int installFlags) {
-        String packageName = pkgLite.packageName;
-        int installLocation = pkgLite.installLocation;
-        // reader
-        synchronized (mPm.mLock) {
-            // Currently installed package which the new package is attempting to replace or
-            // null if no such package is installed.
-            AndroidPackage installedPkg = mPm.mPackages.get(packageName);
-
-            if (installedPkg != null) {
-                if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
-                    // Check for updated system application.
-                    if (installedPkg.isSystem()) {
-                        return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
-                    } else {
-                        // If current upgrade specifies particular preference
-                        if (installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
-                            // Application explicitly specified internal.
-                            return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
-                        } else if (
-                                installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
-                            // App explicitly prefers external. Let policy decide
-                        } else {
-                            // Prefer previous location
-                            if (installedPkg.isExternalStorage()) {
-                                return InstallLocationUtils.RECOMMEND_INSTALL_EXTERNAL;
-                            }
-                            return InstallLocationUtils.RECOMMEND_INSTALL_INTERNAL;
-                        }
-                    }
-                } else {
-                    // Invalid install. Return error code
-                    return InstallLocationUtils.RECOMMEND_FAILED_ALREADY_EXISTS;
-                }
-            }
-        }
-        return pkgLite.recommendedInstallLocation;
-    }
-
     Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite,
             long requiredInstalledVersionCode, int installFlags) {
         if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
@@ -2682,11 +2619,29 @@
                 }
             }
 
+            Bundle extras = new Bundle();
+            extras.putInt(Intent.EXTRA_UID, request.getUid());
+            if (update) {
+                extras.putBoolean(Intent.EXTRA_REPLACING, true);
+            }
+            extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType);
+
+            // If a package is a static shared library, then only the installer of the package
+            // should get the broadcast.
+            if (installerPackageName != null
+                    && request.getPkg().getStaticSharedLibraryName() != null) {
+                mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
+                        extras, 0 /*flags*/,
+                        installerPackageName, null /*finishedReceiver*/,
+                        request.getNewUsers(), null /* instantUserIds*/,
+                        null /* broadcastAllowList */, null);
+            }
+
             // Send installed broadcasts if the package is not a static shared lib.
             if (request.getPkg().getStaticSharedLibraryName() == null) {
                 mPm.mProcessLoggingHandler.invalidateBaseApkHash(request.getPkg().getBaseApkPath());
 
-                // Send added for users that see the package for the first time
+                // Send PACKAGE_ADDED broadcast for users that see the package for the first time
                 // sendPackageAddedForNewUsers also deals with system apps
                 int appId = UserHandle.getAppId(request.getUid());
                 boolean isSystem = request.getPkg().isSystem();
@@ -2694,13 +2649,9 @@
                         isSystem || virtualPreload, virtualPreload /*startReceiver*/, appId,
                         firstUserIds, firstInstantUserIds, dataLoaderType);
 
-                // Send added for users that don't see the package for the first time
-                Bundle extras = new Bundle();
-                extras.putInt(Intent.EXTRA_UID, request.getUid());
-                if (update) {
-                    extras.putBoolean(Intent.EXTRA_REPLACING, true);
-                }
-                extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType);
+                // Send PACKAGE_ADDED broadcast for users that don't see
+                // the package for the first time
+
                 // Send to all running apps.
                 final SparseArray<int[]> newBroadcastAllowList;
                 synchronized (mPm.mLock) {
@@ -2713,8 +2664,8 @@
                         extras, 0 /*flags*/,
                         null /*targetPackage*/, null /*finishedReceiver*/,
                         updateUserIds, instantUserIds, newBroadcastAllowList, null);
+                // Send to the installer, even if it's not running.
                 if (installerPackageName != null) {
-                    // Send to the installer, even if it's not running.
                     mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
                             extras, 0 /*flags*/,
                             installerPackageName, null /*finishedReceiver*/,
@@ -3059,7 +3010,7 @@
         final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
         removePackageHelper.removePackage(stubPkg, true /*chatty*/);
         try {
-            return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags, null);
+            return scanSystemPackageTracedLI(scanFile, parseFlags, scanFlags);
         } catch (PackageManagerException e) {
             Slog.w(TAG, "Failed to install compressed system package:" + stubPkg.getPackageName(),
                     e);
@@ -3191,7 +3142,7 @@
                         | ParsingPackageUtils.PARSE_IS_SYSTEM_DIR;
         @PackageManagerService.ScanFlags int scanFlags = mPm.getSystemPackageScanFlags(codePath);
         final AndroidPackage pkg = scanSystemPackageTracedLI(
-                codePath, parseFlags, scanFlags, null);
+                codePath, parseFlags, scanFlags);
 
         synchronized (mPm.mLock) {
             PackageSetting pkgSetting = mPm.mSettings.getPackageLPr(pkg.getPackageName());
@@ -3371,7 +3322,7 @@
                 try {
                     final File codePath = new File(pkg.getPath());
                     synchronized (mPm.mInstallLock) {
-                        scanSystemPackageTracedLI(codePath, 0, scanFlags, null);
+                        scanSystemPackageTracedLI(codePath, 0, scanFlags);
                     }
                 } catch (PackageManagerException e) {
                     Slog.e(TAG, "Failed to parse updated, ex-system package: "
@@ -3521,7 +3472,7 @@
                 }
                 try {
                     addForInitLI(parseResult.parsedPackage, parseFlags, scanFlags,
-                            null);
+                            new UserHandle(UserHandle.USER_SYSTEM));
                 } catch (PackageManagerException e) {
                     errorCode = e.error;
                     errorMsg = "Failed to scan " + parseResult.scanFile + ": " + e.getMessage();
@@ -3584,7 +3535,7 @@
             try {
                 synchronized (mPm.mInstallLock) {
                     final AndroidPackage newPkg = scanSystemPackageTracedLI(
-                            scanFile, reparseFlags, rescanFlags, null);
+                            scanFile, reparseFlags, rescanFlags);
                     // We rescanned a stub, add it to the list of stubbed system packages
                     if (newPkg.isStub()) {
                         stubSystemApps.add(packageName);
@@ -3603,10 +3554,10 @@
      */
     @GuardedBy("mPm.mInstallLock")
     public AndroidPackage scanSystemPackageTracedLI(File scanFile, final int parseFlags,
-            int scanFlags, UserHandle user) throws PackageManagerException {
+            int scanFlags) throws PackageManagerException {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanPackage [" + scanFile.toString() + "]");
         try {
-            return scanSystemPackageLI(scanFile, parseFlags, scanFlags, user);
+            return scanSystemPackageLI(scanFile, parseFlags, scanFlags);
         } finally {
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         }
@@ -3617,8 +3568,8 @@
      *  Returns {@code null} in case of errors and the error code is stored in mLastScanError
      */
     @GuardedBy("mPm.mInstallLock")
-    private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags,
-            UserHandle user) throws PackageManagerException {
+    private AndroidPackage scanSystemPackageLI(File scanFile, int parseFlags, int scanFlags)
+            throws PackageManagerException {
         if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);
 
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
@@ -3634,7 +3585,8 @@
             PackageManagerService.renameStaticSharedLibraryPackage(parsedPackage);
         }
 
-        return addForInitLI(parsedPackage, parseFlags, scanFlags, user);
+        return addForInitLI(parsedPackage, parseFlags, scanFlags,
+                new UserHandle(UserHandle.USER_SYSTEM));
     }
 
     /**
@@ -3660,33 +3612,32 @@
                 parsedPackage, parseFlags, scanFlags, user);
         final ScanResult scanResult = scanResultPair.first;
         boolean shouldHideSystemApp = scanResultPair.second;
-        if (scanResult.mSuccess) {
-            synchronized (mPm.mLock) {
-                boolean appIdCreated = false;
-                try {
-                    final String pkgName = scanResult.mPkgSetting.getPackageName();
-                    final ReconcileRequest reconcileRequest = new ReconcileRequest(
-                            Collections.singletonMap(pkgName, scanResult),
-                            mPm.mPackages,
-                            Collections.singletonMap(pkgName,
-                                    mPm.getSettingsVersionForPackage(parsedPackage)));
-                    final Map<String, ReconciledPackage> reconcileResult =
-                            ReconcilePackageUtils.reconcilePackages(reconcileRequest,
-                                    mSharedLibraries, mPm.mSettings.getKeySetManagerService(),
-                                    mPm.mSettings);
-                    if ((scanFlags & SCAN_AS_APEX) == 0) {
-                        appIdCreated = optimisticallyRegisterAppId(scanResult);
-                    } else {
-                        scanResult.mPkgSetting.setAppId(Process.INVALID_UID);
-                    }
-                    commitReconciledScanResultLocked(reconcileResult.get(pkgName),
-                            mPm.mUserManager.getUserIds());
-                } catch (PackageManagerException e) {
-                    if (appIdCreated) {
-                        cleanUpAppIdCreation(scanResult);
-                    }
-                    throw e;
+        final InstallRequest installRequest = new InstallRequest(
+                parsedPackage, parseFlags, scanFlags, user, scanResult);
+
+        synchronized (mPm.mLock) {
+            boolean appIdCreated = false;
+            try {
+                final String pkgName = scanResult.mPkgSetting.getPackageName();
+                final List<ReconciledPackage> reconcileResult =
+                        ReconcilePackageUtils.reconcilePackages(
+                                Collections.singletonList(installRequest),
+                                mPm.mPackages, Collections.singletonMap(pkgName,
+                                        mPm.getSettingsVersionForPackage(parsedPackage)),
+                                mSharedLibraries, mPm.mSettings.getKeySetManagerService(),
+                                mPm.mSettings);
+                if ((scanFlags & SCAN_AS_APEX) == 0) {
+                    appIdCreated = optimisticallyRegisterAppId(installRequest);
+                } else {
+                    installRequest.setScannedPackageSettingAppId(Process.INVALID_UID);
                 }
+                commitReconciledScanResultLocked(reconcileResult.get(0),
+                        mPm.mUserManager.getUserIds());
+            } catch (PackageManagerException e) {
+                if (appIdCreated) {
+                    cleanUpAppIdCreation(installRequest);
+                }
+                throw e;
             }
         }
 
@@ -3711,13 +3662,14 @@
      * @return {@code true} if a new app ID was registered and will need to be cleaned up on
      *         failure.
      */
-    private boolean optimisticallyRegisterAppId(@NonNull ScanResult result)
+    private boolean optimisticallyRegisterAppId(@NonNull InstallRequest installRequest)
             throws PackageManagerException {
-        if (!result.mExistingSettingCopied || result.needsNewAppId()) {
+        if (!installRequest.isExistingSettingCopied() || installRequest.needsNewAppId()) {
             synchronized (mPm.mLock) {
                 // THROWS: when we can't allocate a user id. add call to check if there's
                 // enough space to ensure we won't throw; otherwise, don't modify state
-                return mPm.mSettings.registerAppIdLPw(result.mPkgSetting, result.needsNewAppId());
+                return mPm.mSettings.registerAppIdLPw(installRequest.getScannedPackageSetting(),
+                        installRequest.needsNewAppId());
             }
         }
         return false;
@@ -3725,15 +3677,16 @@
 
     /**
      * Reverts any app ID creation that were made by
-     * {@link #optimisticallyRegisterAppId(ScanResult)}. Note: this is only necessary if the
+     * {@link #optimisticallyRegisterAppId(InstallRequest)}. Note: this is only necessary if the
      * referenced method returned true.
      */
-    private void cleanUpAppIdCreation(@NonNull ScanResult result) {
+    private void cleanUpAppIdCreation(@NonNull InstallRequest installRequest) {
         // iff we've acquired an app ID for a new package setting, remove it so that it can be
         // acquired by another request.
-        if (result.mPkgSetting.getAppId() > 0) {
+        if (installRequest.getScannedPackageSetting() != null
+                && installRequest.getScannedPackageSetting().getAppId() > 0) {
             synchronized (mPm.mLock) {
-                mPm.mSettings.removeAppIdLPw(result.mPkgSetting.getAppId());
+                mPm.mSettings.removeAppIdLPw(installRequest.getScannedPackageSetting().getAppId());
             }
         }
     }
@@ -3857,7 +3810,6 @@
         }
     }
 
-    @GuardedBy("mPm.mInstallLock")
     private Pair<ScanResult, Boolean> scanSystemPackageLI(ParsedPackage parsedPackage,
             @ParsingPackageUtils.ParseFlags int parseFlags,
             @PackageManagerService.ScanFlags int scanFlags,
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 36bbf41..4e5a6f9 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -16,9 +16,13 @@
 
 package com.android.server.pm;
 
+import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
 import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN;
 import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT;
+import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
+import static android.os.Process.INVALID_UID;
 
+import static com.android.server.pm.PackageManagerService.SCAN_AS_INSTANT_APP;
 import static com.android.server.pm.PackageManagerService.TAG;
 
 import android.annotation.NonNull;
@@ -29,13 +33,19 @@
 import android.content.pm.IPackageInstallObserver2;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
+import android.content.pm.SharedLibraryInfo;
 import android.content.pm.SigningDetails;
 import android.net.Uri;
+import android.os.Build;
+import android.os.Process;
 import android.os.UserHandle;
 import android.util.ExceptionUtils;
 import android.util.Slog;
 
+import com.android.server.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -45,13 +55,69 @@
     private final int mUserId;
     @Nullable
     private final InstallArgs mInstallArgs;
-    @NonNull
-    private final PackageInstalledInfo mInstalledInfo;
     @Nullable
     private Runnable mPostInstallRunnable;
     @Nullable
     private PackageRemovedInfo mRemovedInfo;
 
+    @PackageManagerService.ScanFlags
+    private int mScanFlags;
+    @ParsingPackageUtils.ParseFlags
+    private int mParseFlags;
+    private boolean mReplace;
+
+    @Nullable /* The original Package if it is being replaced, otherwise {@code null} */
+    private AndroidPackage mExistingPackage;
+    /** parsed package to be scanned */
+    @Nullable
+    private ParsedPackage mParsedPackage;
+    private boolean mClearCodeCache;
+    private boolean mSystem;
+    @Nullable
+    private PackageSetting mOriginalPs;
+    @Nullable
+    private PackageSetting mDisabledPs;
+
+    /** Package Installed Info */
+    @Nullable
+    private String mName;
+    private int mUid = -1;
+    // The set of users that originally had this package installed.
+    @Nullable
+    private int[] mOrigUsers;
+    // The set of users that now have this package installed.
+    @Nullable
+    private int[] mNewUsers;
+    @Nullable
+    private AndroidPackage mPkg;
+    private int mReturnCode;
+    @Nullable
+    private String mReturnMsg;
+    // The set of packages consuming this shared library or null if no consumers exist.
+    @Nullable
+    private ArrayList<AndroidPackage> mLibraryConsumers;
+    @Nullable
+    private PackageFreezer mFreezer;
+    /** The package this package replaces */
+    @Nullable
+    private String mOrigPackage;
+    @Nullable
+    private String mOrigPermission;
+    // The ApexInfo returned by ApexManager#installPackage, used by rebootless APEX install
+    @Nullable
+    private ApexInfo mApexInfo;
+
+    @Nullable
+    private ScanResult mScanResult;
+
+    private boolean mIsInstallInherit;
+    private boolean mIsInstallForUsers;
+
+    @Nullable
+    private final PackageMetrics mPackageMetrics;
+    private final int mSessionId;
+    private final int mRequireUserAction;
+
     // New install
     InstallRequest(InstallingSession params) {
         mUserId = params.getUser().getIdentifier();
@@ -63,7 +129,10 @@
                 params.mTraceMethod, params.mTraceCookie, params.mSigningDetails,
                 params.mInstallReason, params.mInstallScenario, params.mForceQueryableOverride,
                 params.mDataLoaderType, params.mPackageSource);
-        mInstalledInfo = new PackageInstalledInfo();
+        mPackageMetrics = new PackageMetrics(this);
+        mIsInstallInherit = params.mIsInherit;
+        mSessionId = params.mSessionId;
+        mRequireUserAction = params.mRequireUserAction;
     }
 
     // Install existing package as user
@@ -71,56 +140,63 @@
             Runnable runnable) {
         mUserId = userId;
         mInstallArgs = null;
-        mInstalledInfo = new PackageInstalledInfo();
-        mInstalledInfo.mReturnCode = returnCode;
-        mInstalledInfo.mPkg = pkg;
-        mInstalledInfo.mNewUsers = newUsers;
+        mReturnCode = returnCode;
+        mPkg = pkg;
+        mNewUsers = newUsers;
         mPostInstallRunnable = runnable;
+        mPackageMetrics = new PackageMetrics(this);
+        mIsInstallForUsers = true;
+        mSessionId = -1;
+        mRequireUserAction = USER_ACTION_UNSPECIFIED;
     }
 
-    private static class PackageInstalledInfo {
-        String mName;
-        int mUid = -1;
-        // The set of users that originally had this package installed.
-        int[] mOrigUsers;
-        // The set of users that now have this package installed.
-        int[] mNewUsers;
-        AndroidPackage mPkg;
-        int mReturnCode;
-        String mReturnMsg;
-        String mInstallerPackageName;
-        // The set of packages consuming this shared library or null if no consumers exist.
-        ArrayList<AndroidPackage> mLibraryConsumers;
-        PackageFreezer mFreezer;
-        // In some error cases we want to convey more info back to the observer
-        String mOrigPackage;
-        String mOrigPermission;
-        // The ApexInfo returned by ApexManager#installPackage, used by rebootless APEX install
-        ApexInfo mApexInfo;
+    // addForInit
+    InstallRequest(ParsedPackage parsedPackage, int parseFlags, int scanFlags,
+            @Nullable UserHandle user, ScanResult scanResult) {
+        if (user != null) {
+            mUserId = user.getIdentifier();
+        } else {
+            // APEX
+            mUserId = INVALID_UID;
+        }
+        mInstallArgs = null;
+        mParsedPackage = parsedPackage;
+        mParseFlags = parseFlags;
+        mScanFlags = scanFlags;
+        mScanResult = scanResult;
+        mPackageMetrics = null; // No logging from this code path
+        mSessionId = -1;
+        mRequireUserAction = USER_ACTION_UNSPECIFIED;
     }
 
+    @Nullable
     public String getName() {
-        return mInstalledInfo.mName;
+        return mName;
     }
 
+    @Nullable
     public String getReturnMsg() {
-        return mInstalledInfo.mReturnMsg;
+        return mReturnMsg;
     }
 
+    @Nullable
     public OriginInfo getOriginInfo() {
         return mInstallArgs == null ? null : mInstallArgs.mOriginInfo;
     }
 
+    @Nullable
     public PackageRemovedInfo getRemovedInfo() {
         return mRemovedInfo;
     }
 
+    @Nullable
     public String getOrigPackage() {
-        return mInstalledInfo.mOrigPackage;
+        return mOrigPackage;
     }
 
+    @Nullable
     public String getOrigPermission() {
-        return mInstalledInfo.mOrigPermission;
+        return mOrigPermission;
     }
 
     @Nullable
@@ -140,7 +216,7 @@
     }
 
     public int getReturnCode() {
-        return mInstalledInfo.mReturnCode;
+        return mReturnCode;
     }
 
     @Nullable
@@ -148,7 +224,7 @@
         return mInstallArgs == null ? null : mInstallArgs.mObserver;
     }
 
-    public boolean isMoveInstall() {
+    public boolean isInstallMove() {
         return mInstallArgs != null && mInstallArgs.mMoveInfo != null;
     }
 
@@ -160,13 +236,13 @@
 
     @Nullable
     public String getMovePackageName() {
-        return  (mInstallArgs != null && mInstallArgs.mMoveInfo != null)
+        return (mInstallArgs != null && mInstallArgs.mMoveInfo != null)
                 ? mInstallArgs.mMoveInfo.mPackageName : null;
     }
 
     @Nullable
     public String getMoveFromCodePath() {
-        return  (mInstallArgs != null && mInstallArgs.mMoveInfo != null)
+        return (mInstallArgs != null && mInstallArgs.mMoveInfo != null)
                 ? mInstallArgs.mMoveInfo.mFromCodePath : null;
     }
 
@@ -203,8 +279,9 @@
         return mInstallArgs == null ? null : mInstallArgs.mVolumeUuid;
     }
 
+    @Nullable
     public AndroidPackage getPkg() {
-        return mInstalledInfo.mPkg;
+        return mPkg;
     }
 
     @Nullable
@@ -225,7 +302,7 @@
         return mRemovedInfo != null ? mRemovedInfo.mRemovedPackage : null;
     }
 
-    public boolean isInstallForExistingUser() {
+    public boolean isInstallExistingForUser() {
         return mInstallArgs == null;
     }
 
@@ -256,13 +333,15 @@
 
     @Nullable
     public Uri getOriginUri() {
-        return mInstallArgs == null ?  null : Uri.fromFile(mInstallArgs.mOriginInfo.mResolvedFile);
+        return mInstallArgs == null ? null : Uri.fromFile(mInstallArgs.mOriginInfo.mResolvedFile);
     }
 
+    @Nullable
     public ApexInfo getApexInfo() {
-        return mInstalledInfo.mApexInfo;
+        return mApexInfo;
     }
 
+    @Nullable
     public String getSourceInstallerPackageName() {
         return mInstallArgs.mInstallSource.installerPackageName;
     }
@@ -272,25 +351,33 @@
                 && mInstallArgs.mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK;
     }
 
+    @Nullable
     public int[] getNewUsers() {
-        return mInstalledInfo.mNewUsers;
+        return mNewUsers;
     }
 
+    @Nullable
     public int[] getOriginUsers() {
-        return mInstalledInfo.mOrigUsers;
+        return mOrigUsers;
     }
 
     public int getUid() {
-        return mInstalledInfo.mUid;
+        return mUid;
     }
 
     @Nullable
     public String[] getInstallGrantPermissions() {
-        return mInstallArgs == null ?  null : mInstallArgs.mInstallGrantPermissions;
+        return mInstallArgs == null ? null : mInstallArgs.mInstallGrantPermissions;
     }
 
+    @Nullable
     public ArrayList<AndroidPackage> getLibraryConsumers() {
-        return mInstalledInfo.mLibraryConsumers;
+        return mLibraryConsumers;
+    }
+
+    @Nullable
+    public AndroidPackage getExistingPackage() {
+        return mExistingPackage;
     }
 
     @Nullable
@@ -312,13 +399,196 @@
         return mInstallArgs == null ? INSTALL_SCENARIO_DEFAULT : mInstallArgs.mInstallScenario;
     }
 
+    @Nullable
+    public ParsedPackage getParsedPackage() {
+        return mParsedPackage;
+    }
+
+    @ParsingPackageUtils.ParseFlags
+    public int getParseFlags() {
+        return mParseFlags;
+    }
+
+    @PackageManagerService.ScanFlags
+    public int getScanFlags() {
+        return mScanFlags;
+    }
+
+    @Nullable
+    public String getExistingPackageName() {
+        if (mExistingPackage != null) {
+            return mExistingPackage.getPackageName();
+        }
+        return null;
+    }
+
+    @Nullable
+    public AndroidPackage getScanRequestOldPackage() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mOldPkg;
+    }
+
+    public boolean isClearCodeCache() {
+        return mClearCodeCache;
+    }
+
+    public boolean isInstallReplace() {
+        return mReplace;
+    }
+
+    public boolean isInstallSystem() {
+        return mSystem;
+    }
+
+    public boolean isInstallInherit() {
+        return mIsInstallInherit;
+    }
+
+    public boolean isInstallForUsers() {
+        return mIsInstallForUsers;
+    }
+
+    public boolean isInstallFromAdb() {
+        return mInstallArgs != null
+                && (mInstallArgs.mInstallFlags & PackageManager.INSTALL_FROM_ADB) != 0;
+    }
+
+    @Nullable
+    public PackageSetting getOriginalPackageSetting() {
+        return mOriginalPs;
+    }
+
+    @Nullable
+    public PackageSetting getDisabledPackageSetting() {
+        return mDisabledPs;
+    }
+
+    @Nullable
+    public PackageSetting getScanRequestOldPackageSetting() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mOldPkgSetting;
+    }
+
+    @Nullable
+    public PackageSetting getScanRequestOriginalPackageSetting() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mOriginalPkgSetting;
+    }
+
+    @Nullable
+    public PackageSetting getScanRequestPackageSetting() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mPkgSetting;
+    }
+
+    @Nullable
+    public String getRealPackageName() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mRealPkgName;
+    }
+
+    @Nullable
+    public List<String> getChangedAbiCodePath() {
+        assertScanResultExists();
+        return mScanResult.mChangedAbiCodePath;
+    }
+
     public boolean isForceQueryableOverride() {
         return mInstallArgs != null && mInstallArgs.mForceQueryableOverride;
     }
 
+    @Nullable
+    public SharedLibraryInfo getSdkSharedLibraryInfo() {
+        assertScanResultExists();
+        return mScanResult.mSdkSharedLibraryInfo;
+    }
+
+    @Nullable
+    public SharedLibraryInfo getStaticSharedLibraryInfo() {
+        assertScanResultExists();
+        return mScanResult.mStaticSharedLibraryInfo;
+    }
+
+    @Nullable
+    public List<SharedLibraryInfo> getDynamicSharedLibraryInfos() {
+        assertScanResultExists();
+        return mScanResult.mDynamicSharedLibraryInfos;
+    }
+
+    @Nullable
+    public PackageSetting getScannedPackageSetting() {
+        assertScanResultExists();
+        return mScanResult.mPkgSetting;
+    }
+
+    @Nullable
+    public PackageSetting getRealPackageSetting() {
+        // TODO: Fix this to have 1 mutable PackageSetting for scan/install. If the previous
+        //  setting needs to be passed to have a comparison, hide it behind an immutable
+        //  interface. There's no good reason to have 3 different ways to access the real
+        //  PackageSetting object, only one of which is actually correct.
+        PackageSetting realPkgSetting = isExistingSettingCopied()
+                ? getScanRequestPackageSetting() : getScannedPackageSetting();
+        if (realPkgSetting == null) {
+            realPkgSetting = getScannedPackageSetting();
+        }
+        return realPkgSetting;
+    }
+
+    public boolean isExistingSettingCopied() {
+        assertScanResultExists();
+        return mScanResult.mExistingSettingCopied;
+    }
+
+    /**
+     * Whether the original PackageSetting needs to be updated with
+     * a new app ID. Useful when leaving a sharedUserId.
+     */
+    public boolean needsNewAppId() {
+        assertScanResultExists();
+        return mScanResult.mPreviousAppId != Process.INVALID_UID;
+    }
+
+    public int getPreviousAppId() {
+        assertScanResultExists();
+        return mScanResult.mPreviousAppId;
+    }
+
+    public boolean isPlatformPackage() {
+        assertScanResultExists();
+        return mScanResult.mRequest.mIsPlatformPackage;
+    }
+
+    public boolean isInstantInstall() {
+        return (mScanFlags & SCAN_AS_INSTANT_APP) != 0;
+    }
+
+    public void assertScanResultExists() {
+        if (mScanResult == null) {
+            // Should not happen. This indicates a bug in the installation code flow
+            if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                throw new IllegalStateException("ScanResult cannot be null.");
+            } else {
+                Slog.e(TAG, "ScanResult is null and it should not happen");
+            }
+        }
+    }
+
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    public int getRequireUserAction() {
+        return mRequireUserAction;
+    }
+
+    public void setScanFlags(int scanFlags) {
+        mScanFlags = scanFlags;
+    }
+
     public void closeFreezer() {
-        if (mInstalledInfo.mFreezer != null) {
-            mInstalledInfo.mFreezer.close();
+        if (mFreezer != null) {
+            mFreezer.close();
         }
     }
 
@@ -341,57 +611,53 @@
     }
 
     public void setError(String msg, PackageManagerException e) {
-        mInstalledInfo.mReturnCode = e.error;
+        mReturnCode = e.error;
         setReturnMessage(ExceptionUtils.getCompleteMessage(msg, e));
         Slog.w(TAG, msg, e);
     }
 
     public void setReturnCode(int returnCode) {
-        mInstalledInfo.mReturnCode = returnCode;
+        mReturnCode = returnCode;
     }
 
     public void setReturnMessage(String returnMsg) {
-        mInstalledInfo.mReturnMsg = returnMsg;
+        mReturnMsg = returnMsg;
     }
 
     public void setApexInfo(ApexInfo apexInfo) {
-        mInstalledInfo.mApexInfo = apexInfo;
+        mApexInfo = apexInfo;
     }
 
     public void setPkg(AndroidPackage pkg) {
-        mInstalledInfo.mPkg = pkg;
+        mPkg = pkg;
     }
 
     public void setUid(int uid) {
-        mInstalledInfo.mUid = uid;
+        mUid = uid;
     }
 
     public void setNewUsers(int[] newUsers) {
-        mInstalledInfo.mNewUsers = newUsers;
+        mNewUsers = newUsers;
     }
 
     public void setOriginPackage(String originPackage) {
-        mInstalledInfo.mOrigPackage = originPackage;
+        mOrigPackage = originPackage;
     }
 
     public void setOriginPermission(String originPermission) {
-        mInstalledInfo.mOrigPermission = originPermission;
-    }
-
-    public void setInstallerPackageName(String installerPackageName) {
-        mInstalledInfo.mInstallerPackageName = installerPackageName;
+        mOrigPermission = originPermission;
     }
 
     public void setName(String packageName) {
-        mInstalledInfo.mName = packageName;
+        mName = packageName;
     }
 
     public void setOriginUsers(int[] userIds) {
-        mInstalledInfo.mOrigUsers = userIds;
+        mOrigUsers = userIds;
     }
 
     public void setFreezer(PackageFreezer freezer) {
-        mInstalledInfo.mFreezer = freezer;
+        mFreezer = freezer;
     }
 
     public void setRemovedInfo(PackageRemovedInfo removedInfo) {
@@ -399,6 +665,103 @@
     }
 
     public void setLibraryConsumers(ArrayList<AndroidPackage> libraryConsumers) {
-        mInstalledInfo.mLibraryConsumers = libraryConsumers;
+        mLibraryConsumers = libraryConsumers;
+    }
+
+    public void setPrepareResult(boolean replace, int scanFlags,
+            int parseFlags, AndroidPackage existingPackage,
+            ParsedPackage packageToScan, boolean clearCodeCache, boolean system,
+            PackageSetting originalPs, PackageSetting disabledPs) {
+        mReplace = replace;
+        mScanFlags = scanFlags;
+        mParseFlags = parseFlags;
+        mExistingPackage = existingPackage;
+        mParsedPackage = packageToScan;
+        mClearCodeCache = clearCodeCache;
+        mSystem = system;
+        mOriginalPs = originalPs;
+        mDisabledPs = disabledPs;
+    }
+
+    public void setScanResult(@NonNull ScanResult scanResult) {
+        mScanResult = scanResult;
+    }
+
+    public void setScannedPackageSettingAppId(int appId) {
+        assertScanResultExists();
+        mScanResult.mPkgSetting.setAppId(appId);
+    }
+
+    public void setScannedPackageSettingFirstInstallTimeFromReplaced(
+            @Nullable PackageStateInternal replacedPkgSetting, int[] userId) {
+        assertScanResultExists();
+        mScanResult.mPkgSetting.setFirstInstallTimeFromReplaced(replacedPkgSetting, userId);
+    }
+
+    public void setScannedPackageSettingLastUpdateTime(long lastUpdateTim) {
+        assertScanResultExists();
+        mScanResult.mPkgSetting.setLastUpdateTime(lastUpdateTim);
+    }
+
+    public void setRemovedAppId(int appId) {
+        if (mRemovedInfo != null) {
+            mRemovedInfo.mRemovedAppId = appId;
+        }
+    }
+
+    public void onPrepareStarted() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepStarted(PackageMetrics.STEP_PREPARE);
+        }
+    }
+
+    public void onPrepareFinished() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepFinished(PackageMetrics.STEP_PREPARE);
+        }
+    }
+
+    public void onScanStarted() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepStarted(PackageMetrics.STEP_SCAN);
+        }
+    }
+
+    public void onScanFinished() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepFinished(PackageMetrics.STEP_SCAN);
+        }
+    }
+
+    public void onReconcileStarted() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepStarted(PackageMetrics.STEP_RECONCILE);
+        }
+    }
+
+    public void onReconcileFinished() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepFinished(PackageMetrics.STEP_RECONCILE);
+        }
+    }
+
+    public void onCommitStarted() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepStarted(PackageMetrics.STEP_COMMIT);
+        }
+    }
+
+    public void onCommitFinished() {
+        if (mPackageMetrics != null) {
+            mPackageMetrics.onStepFinished(PackageMetrics.STEP_COMMIT);
+        }
+    }
+
+    public void onInstallCompleted(Computer snapshot) {
+        if (getReturnCode() == INSTALL_SUCCEEDED) {
+            if (mPackageMetrics != null) {
+                mPackageMetrics.onInstallSucceed(snapshot);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index 8d5a5e1..d8494db 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -17,6 +17,8 @@
 package com.android.server.pm;
 
 import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.content.pm.PackageInstaller.SessionParams.MODE_INHERIT_EXISTING;
+import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
 import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
 import static android.content.pm.PackageManager.INSTALL_STAGED;
 import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
@@ -42,7 +44,7 @@
 import android.os.Environment;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Pair;
 import android.util.Slog;
 
@@ -60,7 +62,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 
 class InstallingSession {
     final OriginInfo mOriginInfo;
@@ -93,7 +95,11 @@
     final PackageManagerService mPm;
     final InstallPackageHelper mInstallPackageHelper;
     final RemovePackageHelper mRemovePackageHelper;
+    final boolean mIsInherit;
+    final int mSessionId;
+    final int mRequireUserAction;
 
+    // For move install
     InstallingSession(OriginInfo originInfo, MoveInfo moveInfo, IPackageInstallObserver2 observer,
             int installFlags, InstallSource installSource, String volumeUuid,
             UserHandle user, String packageAbiOverride, int packageSource,
@@ -121,9 +127,12 @@
         mRequiredInstalledVersionCode = PackageManager.VERSION_CODE_HIGHEST;
         mPackageSource = packageSource;
         mPackageLite = packageLite;
+        mIsInherit = false;
+        mSessionId = -1;
+        mRequireUserAction = USER_ACTION_UNSPECIFIED;
     }
 
-    InstallingSession(File stagedDir, IPackageInstallObserver2 observer,
+    InstallingSession(int sessionId, File stagedDir, IPackageInstallObserver2 observer,
             PackageInstaller.SessionParams sessionParams, InstallSource installSource,
             UserHandle user, SigningDetails signingDetails, int installerUid,
             PackageLite packageLite, PackageManagerService pm) {
@@ -151,6 +160,9 @@
         mRequiredInstalledVersionCode = sessionParams.requiredInstalledVersionCode;
         mPackageSource = sessionParams.packageSource;
         mPackageLite = packageLite;
+        mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING;
+        mSessionId = sessionId;
+        mRequireUserAction = sessionParams.requireUserAction;
     }
 
     @Override
@@ -506,6 +518,7 @@
             mInstallPackageHelper.installPackagesTraced(installRequests);
 
             for (InstallRequest request : installRequests) {
+                request.onInstallCompleted(mPm.snapshotComputer());
                 doPostInstall(request);
             }
         }
@@ -531,7 +544,7 @@
     }
 
     private void cleanUpForFailedInstall(InstallRequest request) {
-        if (request.isMoveInstall()) {
+        if (request.isInstallMove()) {
             mRemovePackageHelper.cleanUpForMoveInstall(request.getMoveToUuid(),
                     request.getMovePackageName(), request.getMoveFromCodePath());
         } else {
@@ -572,19 +585,15 @@
             }
             try (PackageParser2 packageParser = mPm.mInjector.getScanningPackageParser()) {
                 ApexInfo apexInfo = mPm.mApexManager.installPackage(apexes[0]);
-                if (ApexPackageInfo.ENABLE_FEATURE_SCAN_APEX) {
-                    // APEX has been handled successfully by apexd. Let's continue the install flow
-                    // so it will be scanned and registered with the system.
-                    // TODO(b/225756739): Improve atomicity of rebootless APEX install.
-                    // The newly installed APEX will not be reverted even if
-                    // processApkInstallRequests() fails. Need a way to keep info stored in apexd
-                    // and PMS in sync in the face of install failures.
-                    request.setApexInfo(apexInfo);
-                    mPm.mHandler.post(() -> processApkInstallRequests(true, requests));
-                    return;
-                } else {
-                    mPm.mApexPackageInfo.notifyPackageInstalled(apexInfo, packageParser);
-                }
+                // APEX has been handled successfully by apexd. Let's continue the install flow
+                // so it will be scanned and registered with the system.
+                // TODO(b/225756739): Improve atomicity of rebootless APEX install.
+                // The newly installed APEX will not be reverted even if
+                // processApkInstallRequests() fails. Need a way to keep info stored in apexd
+                // and PMS in sync in the face of install failures.
+                request.setApexInfo(apexInfo);
+                mPm.mHandler.post(() -> processApkInstallRequests(true, requests));
+                return;
             }
         } catch (PackageManagerException e) {
             request.setError("APEX installation failed", e);
@@ -599,7 +608,7 @@
      */
     private class MultiPackageInstallingSession {
         private final List<InstallingSession> mChildInstallingSessions;
-        private final Map<InstallRequest, Integer> mCurrentState;
+        private final Set<InstallRequest> mCurrentInstallRequests;
         @NonNull
         final PackageManagerService mPm;
         final UserHandle mUser;
@@ -618,7 +627,7 @@
                 final InstallingSession childInstallingSession = childInstallingSessions.get(i);
                 childInstallingSession.mParentInstallingSession = this;
             }
-            this.mCurrentState = new ArrayMap<>(mChildInstallingSessions.size());
+            mCurrentInstallRequests = new ArraySet<>(mChildInstallingSessions.size());
         }
 
         public void start() {
@@ -636,23 +645,24 @@
         }
 
         public void tryProcessInstallRequest(InstallRequest request) {
-            mCurrentState.put(request, request.getReturnCode());
-            if (mCurrentState.size() != mChildInstallingSessions.size()) {
+            mCurrentInstallRequests.add(request);
+            if (mCurrentInstallRequests.size() != mChildInstallingSessions.size()) {
                 return;
             }
             int completeStatus = PackageManager.INSTALL_SUCCEEDED;
-            for (Integer status : mCurrentState.values()) {
-                if (status == PackageManager.INSTALL_UNKNOWN) {
+            for (InstallRequest installRequest : mCurrentInstallRequests) {
+                if (installRequest.getReturnCode() == PackageManager.INSTALL_UNKNOWN) {
                     return;
-                } else if (status != PackageManager.INSTALL_SUCCEEDED) {
-                    completeStatus = status;
+                } else if (installRequest.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
+                    completeStatus = installRequest.getReturnCode();
                     break;
                 }
             }
-            final List<InstallRequest> installRequests = new ArrayList<>(mCurrentState.size());
-            for (Map.Entry<InstallRequest, Integer> entry : mCurrentState.entrySet()) {
-                entry.getKey().setReturnCode(completeStatus);
-                installRequests.add(entry.getKey());
+            final List<InstallRequest> installRequests = new ArrayList<>(
+                    mCurrentInstallRequests.size());
+            for (InstallRequest installRequest : mCurrentInstallRequests) {
+                installRequest.setReturnCode(completeStatus);
+                installRequests.add(installRequest);
             }
             int finalCompleteStatus = completeStatus;
             mPm.mHandler.post(() -> processInstallRequests(
diff --git a/services/core/java/com/android/server/pm/InstantAppRegistry.java b/services/core/java/com/android/server/pm/InstantAppRegistry.java
index bedc12a..032d030 100644
--- a/services/core/java/com/android/server/pm/InstantAppRegistry.java
+++ b/services/core/java/com/android/server/pm/InstantAppRegistry.java
@@ -46,8 +46,6 @@
 import android.util.PackageUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -55,6 +53,8 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
diff --git a/services/core/java/com/android/server/pm/KeySetManagerService.java b/services/core/java/com/android/server/pm/KeySetManagerService.java
index 7774b6a..f1453c8 100644
--- a/services/core/java/com/android/server/pm/KeySetManagerService.java
+++ b/services/core/java/com/android/server/pm/KeySetManagerService.java
@@ -27,9 +27,9 @@
 import android.util.Base64;
 import android.util.LongSparseArray;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.SharedUserApi;
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/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index d25bca7..2a2410fd 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -652,12 +652,6 @@
     @DexOptResult
     private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path,
             PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
-        if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) {
-            // We are asked to optimize only the dex files used by other apps and this is not
-            // on of them: skip it.
-            return DEX_OPT_SKIPPED;
-        }
-
         String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(),
                 dexUseInfo.isUsedByOtherApps());
         // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags.
diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java
index 4e8b0a1..66ef93d 100644
--- a/services/core/java/com/android/server/pm/PackageHandler.java
+++ b/services/core/java/com/android/server/pm/PackageHandler.java
@@ -95,7 +95,7 @@
 
                 request.closeFreezer();
                 request.runPostInstallRunnable();
-                if (!request.isInstallForExistingUser()) {
+                if (!request.isInstallExistingForUser()) {
                     mInstallPackageHelper.handlePackagePostInstall(request, didRestore);
                 } else if (DEBUG_INSTALL) {
                     // No post-install when we run restore from installExistingPackageForUser
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 218d9d1..cc1d879 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -78,8 +78,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -89,6 +87,8 @@
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.ImageUtils;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 022bf3c..a2b462a 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -29,6 +29,7 @@
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
 import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT;
+import static android.content.pm.PackageManager.INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE;
 import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
 import static android.content.pm.PackageManager.INSTALL_STAGED;
 import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
@@ -139,8 +140,6 @@
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.apk.ApkSignatureVerifier;
 
 import com.android.internal.R;
@@ -156,6 +155,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.DexManager;
@@ -379,6 +380,14 @@
     @GuardedBy("mLock")
     private boolean mStageDirInUse = false;
 
+    /**
+     * True if the installation is already in progress. This is used to prevent the caller
+     * from {@link #commit(IntentSender, boolean) committing} the session again while the
+     * installation is still in progress.
+     */
+    @GuardedBy("mLock")
+    private boolean mInstallationInProgress = false;
+
     /** Permissions have been accepted by the user (see {@link #setPermissionsResult}) */
     @GuardedBy("mLock")
     private boolean mPermissionsManuallyAccepted = false;
@@ -1691,6 +1700,14 @@
             }
         }
 
+        synchronized (mLock) {
+            if (mInstallationInProgress) {
+                throw new IllegalStateException("Installation is already in progress. Don't "
+                        + "commit session=" + sessionId + " again.");
+            }
+            mInstallationInProgress = true;
+        }
+
         dispatchSessionSealed();
     }
 
@@ -2549,8 +2566,8 @@
         }
 
         synchronized (mLock) {
-            return new InstallingSession(stageDir, localObserver, params, mInstallSource, user,
-                    mSigningDetails, mInstallerUid, mPackageLite, mPm);
+            return new InstallingSession(sessionId, stageDir, localObserver, params, mInstallSource,
+                    user, mSigningDetails, mInstallerUid, mPackageLite, mPm);
         }
     }
 
@@ -2762,10 +2779,11 @@
                     "Missing existing base package");
         }
 
-        // Default to require only if existing base apk has fs-verity.
+        // Default to require only if existing base apk has fs-verity signature.
         mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled()
                 && params.mode == SessionParams.MODE_INHERIT_EXISTING
-                && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath());
+                && (new File(VerityUtils.getFsveritySignatureFilePath(
+                        pkgInfo.applicationInfo.getBaseCodePath()))).exists();
 
         final List<File> removedFiles = getRemovedFilesLocked();
         final List<String> removeSplitList = new ArrayList<>();
@@ -3325,7 +3343,7 @@
                     "Failure to obtain package info.");
         }
         final List<String> filePaths = packageLite.getAllApkPaths();
-        final String appLabel = mPreapprovalDetails.getLabel();
+        final CharSequence appLabel = mPreapprovalDetails.getLabel();
         final ULocale appLocale = mPreapprovalDetails.getLocale();
         final ApplicationInfo appInfo = packageInfo.applicationInfo;
         boolean appLabelMatched = false;
@@ -3693,7 +3711,9 @@
     private boolean dispatchPendingAbandonCallback() {
         final Runnable callback;
         synchronized (mLock) {
-            Preconditions.checkState(mStageDirInUse);
+            if (!mStageDirInUse) {
+                return false;
+            }
             mStageDirInUse = false;
             callback = mPendingAbandonCallback;
             mPendingAbandonCallback = null;
@@ -4243,6 +4263,13 @@
     public void requestUserPreapproval(@NonNull PreapprovalDetails details,
             @NonNull IntentSender statusReceiver) {
         validatePreapprovalRequest(details, statusReceiver);
+
+        if (!mPm.isPreapprovalRequestAvailable()) {
+            sendUpdateToRemoteStatusReceiver(INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE,
+                    "Request user pre-approval is currently not available.", null /* extras */);
+            return;
+        }
+
         dispatchPreapprovalRequest();
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 8fed153..8089af3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -107,6 +107,7 @@
 import android.content.pm.SuspendDialogInfo;
 import android.content.pm.TestUtilityService;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.pm.VerifierDeviceIdentity;
 import android.content.pm.VersionedPackage;
 import android.content.pm.overlay.OverlayPaths;
@@ -161,8 +162,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -181,6 +180,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.FunctionalUtils;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.permission.persistence.RuntimePermissionsPersistence;
 import com.android.server.EventLogTags;
 import com.android.server.FgThread;
@@ -495,6 +496,15 @@
     private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list";
 
     /**
+     * Whether of not requesting the approval before committing sessions is available.
+     *
+     * Flag type: {@code boolean}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_IS_PRE_APPROVAL_REQUEST_AVAILABLE =
+            "is_preapproval_available";
+
+    /**
      * The default response for package verification timeout.
      *
      * This can be either PackageManager.VERIFICATION_ALLOW or
@@ -681,7 +691,6 @@
     private final ModuleInfoProvider mModuleInfoProvider;
 
     final ApexManager mApexManager;
-    final ApexPackageInfo mApexPackageInfo;
 
     final PackageManagerServiceInjector mInjector;
 
@@ -1138,7 +1147,7 @@
         var done = SystemClock.currentTimeMicro();
 
         if (mSnapshotStatistics != null) {
-            mSnapshotStatistics.rebuild(now, done, hits);
+            mSnapshotStatistics.rebuild(now, done, hits, newSnapshot.getPackageStates().size());
         }
         return newSnapshot;
     }
@@ -1632,7 +1641,6 @@
         mSharedLibraries = injector.getSharedLibrariesImpl();
 
         mApexManager = testParams.apexManager;
-        mApexPackageInfo = new ApexPackageInfo(this);
         mArtManagerService = testParams.artManagerService;
         mAvailableFeatures = testParams.availableFeatures;
         mBackgroundDexOptService = testParams.backgroundDexOptService;
@@ -1832,7 +1840,6 @@
         mProtectedPackages = new ProtectedPackages(mContext);
 
         mApexManager = injector.getApexManager();
-        mApexPackageInfo = new ApexPackageInfo(this);
         mAppsFilter = mInjector.getAppsFilter();
 
         mInstantAppRegistry = new InstantAppRegistry(mContext, mPermissionManager,
@@ -1970,8 +1977,8 @@
                         + ver.fingerprint + " to " + PackagePartitions.FINGERPRINT);
             }
 
-            mInitAppsHelper = new InitAppsHelper(this, mApexManager, mApexPackageInfo,
-                mInstallPackageHelper, mInjector.getSystemPartitions());
+            mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
+                    mInjector.getSystemPartitions());
 
             // when upgrading from pre-M, promote system app permissions from install to runtime
             mPromoteSystemApps =
@@ -2980,6 +2987,7 @@
     @Override
     public void notifyPackageRemoved(String packageName, int uid) {
         mPackageObserverHelper.notifyRemoved(packageName, uid);
+        UserPackage.removeFromCache(UserHandle.getUserId(uid), packageName);
     }
 
     void sendPackageAddedForUser(@NonNull Computer snapshot, String packageName,
@@ -4211,7 +4219,7 @@
             mSettings.removeUserLPw(userId);
             mPendingBroadcasts.remove(userId);
             mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId);
-            mAppsFilter.onUserDeleted(userId);
+            mAppsFilter.onUserDeleted(snapshotComputer(), userId);
         }
         mInstantAppRegistry.onUserRemoved(userId);
     }
@@ -5055,8 +5063,11 @@
                     "getUnsuspendablePackagesForUser");
             final int callingUid = Binder.getCallingUid();
             if (UserHandle.getUserId(callingUid) != userId) {
-                throw new SecurityException("Calling uid " + callingUid
-                        + " cannot query getUnsuspendablePackagesForUser for user " + userId);
+                mContext.enforceCallingOrSelfPermission(
+                        Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+                        "Calling uid " + callingUid
+                                + " cannot query getUnsuspendablePackagesForUser for user "
+                                + userId);
             }
             return mSuspendPackageHelper.getUnsuspendablePackagesForUser(snapshotComputer(),
                     packageNames, userId, callingUid);
@@ -5242,25 +5253,30 @@
                 Map<String, String> classLoaderContextMap,
                 String loaderIsa) {
             int callingUid = Binder.getCallingUid();
-            if (PackageManagerService.PLATFORM_PACKAGE_NAME.equals(loadingPackageName)
-                    && callingUid != Process.SYSTEM_UID) {
+
+            // TODO(b/254043366): System server should not report its own dex load because there's
+            // nothing ART can do with it.
+
+            Computer snapshot = snapshot();
+
+            // System server should be able to report dex load on behalf of other apps. E.g., it
+            // could potentially resend the notifications in order to migrate the existing dex load
+            // info to ART Service.
+            if (!PackageManagerServiceUtils.isSystemOrRoot()
+                    && !snapshot.isCallerSameApp(
+                            loadingPackageName, callingUid, true /* resolveIsolatedUid */)) {
                 Slog.w(PackageManagerService.TAG,
-                        "Non System Server process reporting dex loads as system server. uid="
-                                + callingUid);
-                // Do not record dex loads from processes pretending to be system server.
-                // Only the system server should be assigned the package "android", so reject calls
-                // that don't satisfy the constraint.
-                //
-                // notifyDexLoad is a PM API callable from the app process. So in theory, apps could
-                // craft calls to this API and pretend to be system server. Doing so poses no
-                // particular danger for dex load reporting or later dexopt, however it is a
-                // sensible check to do in order to verify the expectations.
+                        TextUtils.formatSimple(
+                                "Invalid dex load report. loadingPackageName=%s, uid=%d",
+                                loadingPackageName, callingUid));
                 return;
             }
 
+            // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`.
+
             int userId = UserHandle.getCallingUserId();
-            ApplicationInfo ai = snapshot().getApplicationInfo(loadingPackageName, /*flags*/ 0,
-                    userId);
+            ApplicationInfo ai =
+                    snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
             if (ai == null) {
                 Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package="
                         + loadingPackageName + ", user=" + userId);
@@ -6889,6 +6905,16 @@
         }
     }
 
+    static boolean isPreapprovalRequestAvailable() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getBoolean(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                    PROPERTY_IS_PRE_APPROVAL_REQUEST_AVAILABLE, true /* defaultValue */);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Returns the array containing per-uid timeout configuration.
      * This is derived from DeviceConfig flags.
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 76858d9..9f21f11 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -316,6 +316,8 @@
                     return runCreateUser();
                 case "remove-user":
                     return runRemoveUser();
+                case "rename-user":
+                    return runRenameUser();
                 case "set-user-restriction":
                     return runSetUserRestriction();
                 case "supports-multiple-users":
@@ -3024,6 +3026,28 @@
         }
     }
 
+    private int runRenameUser() throws RemoteException {
+        String arg = getNextArg();
+        if (arg == null) {
+            getErrPrintWriter().println("Error: no user id specified.");
+            return 1;
+        }
+        int userId = resolveUserId(UserHandle.parseUserArg(arg));
+
+        String name = getNextArg();
+        if (name == null) {
+            Slog.i(TAG, "Resetting name of user " + userId);
+        } else {
+            Slog.i(TAG, "Renaming user " + userId + " to '" + name + "'");
+        }
+
+        IUserManager um = IUserManager.Stub.asInterface(
+                ServiceManager.getService(Context.USER_SERVICE));
+        um.setUserName(userId, name);
+
+        return 0;
+    }
+
     public int runSetUserRestriction() throws RemoteException {
         int userId = UserHandle.USER_SYSTEM;
         String opt = getNextOption();
@@ -3683,7 +3707,7 @@
                 fd = ParcelFileDescriptor.dup(getInFileDescriptor());
             }
             if (sizeBytes <= 0) {
-                getErrPrintWriter().println("Error: must specify a APK size");
+                getErrPrintWriter().println("Error: must specify an APK size");
                 return 1;
             }
 
@@ -3937,6 +3961,11 @@
         return res;
     }
 
+    // Resolves the userId; supports UserHandle.USER_CURRENT, but not other special values
+    private @UserIdInt int resolveUserId(@UserIdInt int userId) {
+        return userId == UserHandle.USER_CURRENT ? ActivityManager.getCurrentUser() : userId;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -4208,6 +4237,9 @@
         pw.println("        switch or reboot)");
         pw.println("      --wait: Wait until user is removed. Ignored if set-ephemeral-if-in-use");
         pw.println("");
+        pw.println("  rename-user USER_ID [USER_NAME]");
+        pw.println("    Rename USER_ID with USER_NAME (or null when [USER_NAME] is not set)");
+        pw.println("");
         pw.println("  set-user-restriction [--user USER_ID] RESTRICTION VALUE");
         pw.println("");
         pw.println("  get-max-users");
diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java
new file mode 100644
index 0000000..0574f73
--- /dev/null
+++ b/services/core/java/com/android/server/pm/PackageMetrics.java
@@ -0,0 +1,201 @@
+/*
+ * 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.Process.INVALID_UID;
+
+import android.annotation.IntDef;
+import android.content.pm.PackageManager;
+import android.content.pm.parsing.ApkLiteParseUtils;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.LocalServices;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Stream;
+
+/**
+ * Metrics class for reporting stats to logging infrastructures like Westworld
+ */
+final class PackageMetrics {
+    public static final int STEP_PREPARE = 1;
+    public static final int STEP_SCAN = 2;
+    public static final int STEP_RECONCILE = 3;
+    public static final int STEP_COMMIT = 4;
+
+    @IntDef(prefix = {"STEP_"}, value = {
+            STEP_PREPARE,
+            STEP_SCAN,
+            STEP_RECONCILE,
+            STEP_COMMIT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StepInt {
+    }
+
+    private final long mInstallStartTimestampMillis;
+    private final SparseArray<InstallStep> mInstallSteps = new SparseArray<>();
+    private final InstallRequest mInstallRequest;
+
+    PackageMetrics(InstallRequest installRequest) {
+        // New instance is used for tracking installation metrics only.
+        // Other metrics should use static methods of this class.
+        mInstallStartTimestampMillis = System.currentTimeMillis();
+        mInstallRequest = installRequest;
+    }
+
+    public void onInstallSucceed(Computer snapshot) {
+        // TODO(b/239722919): report to SecurityLog if on work profile or managed device
+        reportInstallationStats(snapshot, true /* success */);
+    }
+
+    private void reportInstallationStats(Computer snapshot, boolean success) {
+        UserManagerInternal userManagerInternal =
+                LocalServices.getService(UserManagerInternal.class);
+        // TODO(b/249294752): do not log if adb
+        final long installDurationMillis =
+                System.currentTimeMillis() - mInstallStartTimestampMillis;
+        // write to stats
+        final Pair<int[], long[]> stepDurations = getInstallStepDurations();
+        final int[] newUsers = mInstallRequest.getNewUsers();
+        final int[] originalUsers = mInstallRequest.getOriginUsers();
+        final String packageName = mInstallRequest.getName();
+        final String installerPackageName = mInstallRequest.getInstallerPackageName();
+        final int installerUid = installerPackageName == null ? INVALID_UID
+                : snapshot.getPackageUid(installerPackageName, 0, 0);
+        final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName);
+        final long versionCode = success ? 0 : ps.getVersionCode();
+        final long apksSize = getApksSize(ps.getPath());
+
+        FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLATION_SESSION_REPORTED,
+                mInstallRequest.getSessionId() /* session_id */,
+                success ? null : packageName /* not report package_name on success */,
+                mInstallRequest.getUid() /* uid */,
+                newUsers /* user_ids */,
+                userManagerInternal.getUserTypesForStatsd(newUsers) /* user_types */,
+                originalUsers /* original_user_ids */,
+                userManagerInternal.getUserTypesForStatsd(originalUsers) /* original_user_types */,
+                mInstallRequest.getReturnCode() /* public_return_code */,
+                0 /* internal_error_code */,
+                apksSize /* apks_size_bytes */,
+                versionCode /* version_code */,
+                stepDurations.first /* install_steps */,
+                stepDurations.second /* step_duration_millis */,
+                installDurationMillis /* total_duration_millis */,
+                mInstallRequest.getInstallFlags() /* install_flags */,
+                installerUid /* installer_package_uid */,
+                -1 /* original_installer_package_uid */,
+                mInstallRequest.getDataLoaderType() /* data_loader_type */,
+                mInstallRequest.getRequireUserAction() /* user_action_required_type */,
+                mInstallRequest.isInstantInstall() /* is_instant */,
+                mInstallRequest.isInstallReplace() /* is_replace */,
+                mInstallRequest.isInstallSystem() /* is_system */,
+                mInstallRequest.isInstallInherit() /* is_inherit */,
+                mInstallRequest.isInstallForUsers() /* is_installing_existing_as_user */,
+                mInstallRequest.isInstallMove() /* is_move_install */,
+                false /* is_staged */
+        );
+    }
+
+    private long getApksSize(File apkDir) {
+        // TODO(b/249294752): also count apk sizes for failed installs
+        final AtomicLong apksSize = new AtomicLong();
+        try (Stream<Path> walkStream = Files.walk(apkDir.toPath())) {
+            walkStream.filter(p -> p.toFile().isFile()
+                    && ApkLiteParseUtils.isApkFile(p.toFile())).forEach(
+                            f -> apksSize.addAndGet(f.toFile().length()));
+        } catch (IOException e) {
+            // ignore
+        }
+        return apksSize.get();
+    }
+
+    public void onStepStarted(@StepInt int step) {
+        mInstallSteps.put(step, new InstallStep());
+    }
+
+    public void onStepFinished(@StepInt int step) {
+        final InstallStep installStep = mInstallSteps.get(step);
+        if (installStep != null) {
+            // Only valid if the start timestamp is set; otherwise no-op
+            installStep.finish();
+        }
+    }
+
+    // List of steps (e.g., 1, 2, 3) and corresponding list of durations (e.g., 200ms, 100ms, 150ms)
+    private Pair<int[], long[]> getInstallStepDurations() {
+        ArrayList<Integer> steps = new ArrayList<>();
+        ArrayList<Long> durations = new ArrayList<>();
+        for (int i = 0; i < mInstallSteps.size(); i++) {
+            final long duration = mInstallSteps.valueAt(i).getDurationMillis();
+            if (duration >= 0) {
+                steps.add(mInstallSteps.keyAt(i));
+                durations.add(mInstallSteps.valueAt(i).getDurationMillis());
+            }
+        }
+        int[] stepsArray = new int[steps.size()];
+        long[] durationsArray = new long[durations.size()];
+        for (int i = 0; i < stepsArray.length; i++) {
+            stepsArray[i] = steps.get(i);
+            durationsArray[i] = durations.get(i);
+        }
+        return new Pair<>(stepsArray, durationsArray);
+    }
+
+    private static class InstallStep {
+        private final long mStartTimestampMillis;
+        private long mDurationMillis = -1;
+
+        InstallStep() {
+            mStartTimestampMillis = System.currentTimeMillis();
+        }
+
+        void finish() {
+            mDurationMillis = System.currentTimeMillis() - mStartTimestampMillis;
+        }
+
+        long getDurationMillis() {
+            return mDurationMillis;
+        }
+    }
+
+    public static void onUninstallSucceeded(PackageRemovedInfo info, int deleteFlags,
+            UserManagerInternal userManagerInternal) {
+        if (info.mIsUpdate) {
+            // Not logging uninstalls caused by app updates
+            return;
+        }
+        final int[] removedUsers = info.mRemovedUsers;
+        final int[] removedUserTypes = userManagerInternal.getUserTypesForStatsd(removedUsers);
+        final int[] originalUsers = info.mOrigUsers;
+        final int[] originalUserTypes = userManagerInternal.getUserTypesForStatsd(originalUsers);
+        FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_UNINSTALLATION_REPORTED,
+                info.mUid, removedUsers, removedUserTypes, originalUsers, originalUserTypes,
+                deleteFlags, PackageManager.DELETE_SUCCEEDED, info.mIsRemovedPackageSystemUpdate,
+                !info.mRemovedForAllUsers);
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageRemovedInfo.java b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
index 3c863d0..4cac115 100644
--- a/services/core/java/com/android/server/pm/PackageRemovedInfo.java
+++ b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
@@ -111,12 +111,6 @@
     }
 
     private void sendPackageRemovedBroadcastInternal(boolean killApp, boolean removedBySystem) {
-        // Don't send static shared library removal broadcasts as these
-        // libs are visible only the apps that depend on them an one
-        // cannot remove the library if it has a dependency.
-        if (mIsStaticSharedLib) {
-            return;
-        }
         Bundle extras = new Bundle();
         final int removedUid = mRemovedAppId >= 0  ? mRemovedAppId : mUid;
         extras.putInt(Intent.EXTRA_UID, removedUid);
@@ -128,15 +122,22 @@
             extras.putBoolean(Intent.EXTRA_REPLACING, true);
         }
         extras.putBoolean(Intent.EXTRA_REMOVED_FOR_ALL_USERS, mRemovedForAllUsers);
+
+        // Send PACKAGE_REMOVED broadcast to the respective installer.
+        if (mRemovedPackage != null && mInstallerPackageName != null) {
+            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
+                    mRemovedPackage, extras, 0 /*flags*/,
+                    mInstallerPackageName, null, mBroadcastUsers, mInstantUserIds, null, null);
+        }
+        if (mIsStaticSharedLib) {
+            // When uninstalling static shared libraries, only the package's installer needs to be
+            // sent a PACKAGE_REMOVED broadcast. There are no other intended recipients.
+            return;
+        }
         if (mRemovedPackage != null) {
             mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
                     mRemovedPackage, extras, 0, null /*targetPackage*/, null,
                     mBroadcastUsers, mInstantUserIds, mBroadcastAllowList, null);
-            if (mInstallerPackageName != null) {
-                mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
-                        mRemovedPackage, extras, 0 /*flags*/,
-                        mInstallerPackageName, null, mBroadcastUsers, mInstantUserIds, null, null);
-            }
             mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED_INTERNAL,
                     mRemovedPackage, extras, 0 /*flags*/, PLATFORM_PACKAGE_NAME,
                     null /*finishedReceiver*/, mBroadcastUsers, mInstantUserIds,
diff --git a/services/core/java/com/android/server/pm/PackageSignatures.java b/services/core/java/com/android/server/pm/PackageSignatures.java
index 76364fe..90f57a7 100644
--- a/services/core/java/com/android/server/pm/PackageSignatures.java
+++ b/services/core/java/com/android/server/pm/PackageSignatures.java
@@ -21,10 +21,10 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningDetails.SignatureSchemeVersion;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
index 3b306a8..b310c62a 100644
--- a/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
+++ b/services/core/java/com/android/server/pm/PerPackageReadTimeouts.java
@@ -16,7 +16,7 @@
 
 package com.android.server.pm;
 
-import android.annotation.NonNull;;
+import android.annotation.NonNull;
 import android.text.TextUtils;
 
 import com.android.internal.util.HexDump;
diff --git a/services/core/java/com/android/server/pm/PersistentPreferredActivity.java b/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
index ad3950c..d0ee0c8 100644
--- a/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
+++ b/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
@@ -19,10 +19,10 @@
 import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/pm/PreferredActivity.java b/services/core/java/com/android/server/pm/PreferredActivity.java
index 5bc915f..1a49bf9 100644
--- a/services/core/java/com/android/server/pm/PreferredActivity.java
+++ b/services/core/java/com/android/server/pm/PreferredActivity.java
@@ -19,10 +19,10 @@
 import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
index 0ca5febd..e7727f0 100644
--- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java
+++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
@@ -42,11 +42,11 @@
 import android.util.PrintStreamPrinter;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.net.NetworkPolicyManagerInternal;
 import com.android.server.pm.pkg.PackageStateInternal;
 
diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java
index 2a1ca2c..5507e7c 100644
--- a/services/core/java/com/android/server/pm/PreferredComponent.java
+++ b/services/core/java/com/android/server/pm/PreferredComponent.java
@@ -23,10 +23,10 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.pkg.PackageUserState;
 
diff --git a/services/core/java/com/android/server/pm/PrepareResult.java b/services/core/java/com/android/server/pm/PrepareResult.java
deleted file mode 100644
index e074f44a..0000000
--- a/services/core/java/com/android/server/pm/PrepareResult.java
+++ /dev/null
@@ -1,54 +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.
- */
-
-package com.android.server.pm;
-
-import android.annotation.Nullable;
-
-import com.android.server.pm.parsing.pkg.ParsedPackage;
-import com.android.server.pm.pkg.AndroidPackage;
-
-/**
- * The set of data needed to successfully install the prepared package. This includes data that
- * will be used to scan and reconcile the package.
- */
-final class PrepareResult {
-    public final boolean mReplace;
-    public final int mScanFlags;
-    public final int mParseFlags;
-    @Nullable /* The original Package if it is being replaced, otherwise {@code null} */
-    public final AndroidPackage mExistingPackage;
-    public final ParsedPackage mPackageToScan;
-    public final boolean mClearCodeCache;
-    public final boolean mSystem;
-    public final PackageSetting mOriginalPs;
-    public final PackageSetting mDisabledPs;
-
-    PrepareResult(boolean replace, int scanFlags,
-            int parseFlags, AndroidPackage existingPackage,
-            ParsedPackage packageToScan, boolean clearCodeCache, boolean system,
-            PackageSetting originalPs, PackageSetting disabledPs) {
-        mReplace = replace;
-        mScanFlags = scanFlags;
-        mParseFlags = parseFlags;
-        mExistingPackage = existingPackage;
-        mPackageToScan = packageToScan;
-        mClearCodeCache = clearCodeCache;
-        mSystem = system;
-        mOriginalPs = originalPs;
-        mDisabledPs = disabledPs;
-    }
-}
diff --git a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
index 165b450..99bcbc9 100644
--- a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
+++ b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
@@ -35,37 +35,49 @@
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.server.utils.WatchedLongSparseArray;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+/**
+ * Package scan results and related request details used to reconcile the potential addition of
+ * one or more packages to the system.
+ *
+ * Reconcile will take a set of package details that need to be committed to the system and make
+ * sure that they are valid in the context of the system and the other installing apps. Any
+ * invalid state or app will result in a failed reconciliation and thus whatever operation (such
+ * as install) led to the request.
+ */
 final class ReconcilePackageUtils {
-    public static Map<String, ReconciledPackage> reconcilePackages(
-            final ReconcileRequest request, SharedLibrariesImpl sharedLibraries,
+    public static List<ReconciledPackage> reconcilePackages(
+            List<InstallRequest> installRequests,
+            Map<String, AndroidPackage> allPackages,
+            Map<String, Settings.VersionInfo> versionInfos,
+            SharedLibrariesImpl sharedLibraries,
             KeySetManagerService ksms, Settings settings)
             throws ReconcileFailure {
-        final Map<String, ScanResult> scannedPackages = request.mScannedPackages;
-
-        final Map<String, ReconciledPackage> result = new ArrayMap<>(scannedPackages.size());
+        final List<ReconciledPackage> result = new ArrayList<>(installRequests.size());
 
         // make a copy of the existing set of packages so we can combine them with incoming packages
         final ArrayMap<String, AndroidPackage> combinedPackages =
-                new ArrayMap<>(request.mAllPackages.size() + scannedPackages.size());
+                new ArrayMap<>(allPackages.size() + installRequests.size());
 
-        combinedPackages.putAll(request.mAllPackages);
+        combinedPackages.putAll(allPackages);
 
         final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> incomingSharedLibraries =
                 new ArrayMap<>();
 
-        for (String installPackageName : scannedPackages.keySet()) {
-            final ScanResult scanResult = scannedPackages.get(installPackageName);
+        for (InstallRequest installRequest :  installRequests) {
+            installRequest.onReconcileStarted();
+            final String installPackageName = installRequest.getParsedPackage().getPackageName();
 
             // add / replace existing with incoming packages
-            combinedPackages.put(scanResult.mPkgSetting.getPackageName(),
-                    scanResult.mRequest.mParsedPackage);
+            combinedPackages.put(installRequest.getScannedPackageSetting().getPackageName(),
+                    installRequest.getParsedPackage());
 
             // in the first pass, we'll build up the set of incoming shared libraries
             final List<SharedLibraryInfo> allowedSharedLibInfos =
-                    sharedLibraries.getAllowedSharedLibInfos(scanResult);
+                    sharedLibraries.getAllowedSharedLibInfos(installRequest);
             if (allowedSharedLibInfos != null) {
                 for (SharedLibraryInfo info : allowedSharedLibInfos) {
                     if (!SharedLibraryUtils.addSharedLibraryToPackageVersionMap(
@@ -76,24 +88,17 @@
                 }
             }
 
-            // the following may be null if we're just reconciling on boot (and not during install)
-            final InstallRequest installRequest = request.mInstallRequests.get(installPackageName);
-            final PrepareResult prepareResult = request.mPreparedPackages.get(installPackageName);
-            final boolean isInstall = installRequest != null;
-            if (isInstall && prepareResult == null) {
-                throw new ReconcileFailure("Reconcile arguments are not balanced for "
-                        + installPackageName + "!");
-            }
 
             final DeletePackageAction deletePackageAction;
             // we only want to try to delete for non system apps
-            if (isInstall && prepareResult.mReplace && !prepareResult.mSystem) {
-                final boolean killApp = (scanResult.mRequest.mScanFlags & SCAN_DONT_KILL_APP) == 0;
+            if (installRequest.isInstallReplace() && !installRequest.isInstallSystem()) {
+                final boolean killApp = (installRequest.getScanFlags() & SCAN_DONT_KILL_APP) == 0;
                 final int deleteFlags = PackageManager.DELETE_KEEP_DATA
                         | (killApp ? 0 : PackageManager.DELETE_DONT_KILL_APP);
                 deletePackageAction = DeletePackageHelper.mayDeletePackageLocked(
                         installRequest.getRemovedInfo(),
-                        prepareResult.mOriginalPs, prepareResult.mDisabledPs,
+                        installRequest.getOriginalPackageSetting(),
+                        installRequest.getDisabledPackageSetting(),
                         deleteFlags, null /* all users */);
                 if (deletePackageAction == null) {
                     throw new ReconcileFailure(
@@ -104,21 +109,24 @@
                 deletePackageAction = null;
             }
 
-            final int scanFlags = scanResult.mRequest.mScanFlags;
-            final int parseFlags = scanResult.mRequest.mParseFlags;
-            final ParsedPackage parsedPackage = scanResult.mRequest.mParsedPackage;
-
-            final PackageSetting disabledPkgSetting = scanResult.mRequest.mDisabledPkgSetting;
+            final int scanFlags = installRequest.getScanFlags();
+            final int parseFlags = installRequest.getParseFlags();
+            final ParsedPackage parsedPackage = installRequest.getParsedPackage();
+            final PackageSetting disabledPkgSetting = installRequest.getDisabledPackageSetting();
             final PackageSetting lastStaticSharedLibSetting =
-                    scanResult.mStaticSharedLibraryInfo == null ? null
-                            : sharedLibraries.getStaticSharedLibLatestVersionSetting(scanResult);
+                    installRequest.getStaticSharedLibraryInfo() == null ? null
+                            : sharedLibraries.getStaticSharedLibLatestVersionSetting(
+                                    installRequest);
             final PackageSetting signatureCheckPs =
-                    (prepareResult != null && lastStaticSharedLibSetting != null)
+                    lastStaticSharedLibSetting != null
                             ? lastStaticSharedLibSetting
-                            : scanResult.mPkgSetting;
+                            : installRequest.getScannedPackageSetting();
             boolean removeAppKeySetData = false;
             boolean sharedUserSignaturesChanged = false;
             SigningDetails signingDetails = null;
+            if (parsedPackage != null) {
+                signingDetails = parsedPackage.getSigningDetails();
+            }
             SharedUserSetting sharedUserSetting = settings.getSharedUserSettingLPr(
                     signatureCheckPs);
             if (ksms.shouldCheckUpgradeKeySetLocked(
@@ -138,28 +146,21 @@
                         PackageManagerService.reportSettingsProblem(Log.WARN, msg);
                     }
                 }
-                signingDetails = parsedPackage.getSigningDetails();
             } else {
-
                 try {
-                    final Settings.VersionInfo versionInfo =
-                            request.mVersionInfos.get(installPackageName);
+                    final Settings.VersionInfo versionInfo = versionInfos.get(installPackageName);
                     final boolean compareCompat = isCompatSignatureUpdateNeeded(versionInfo);
                     final boolean compareRecover = isRecoverSignatureUpdateNeeded(versionInfo);
-                    final boolean isRollback = installRequest != null
-                            && installRequest.isRollback();
+                    final boolean isRollback = installRequest.isRollback();
                     final boolean compatMatch =
                             PackageManagerServiceUtils.verifySignatures(signatureCheckPs,
                                     sharedUserSetting, disabledPkgSetting,
-                                    parsedPackage.getSigningDetails(), compareCompat,
+                                    signingDetails, compareCompat,
                                     compareRecover, isRollback);
                     // The new KeySets will be re-added later in the scanning process.
                     if (compatMatch) {
                         removeAppKeySetData = true;
                     }
-                    // We just determined the app is signed correctly, so bring
-                    // over the latest parsed certs.
-                    signingDetails = parsedPackage.getSigningDetails();
 
                     // if this is is a sharedUser, check to see if the new package is signed by a
                     // newer
@@ -256,14 +257,11 @@
                 }
             }
 
-            result.put(installPackageName,
-                    new ReconciledPackage(request, installRequest, scanResult.mPkgSetting,
-                            request.mPreparedPackages.get(installPackageName), scanResult,
+            final ReconciledPackage reconciledPackage =
+                    new ReconciledPackage(installRequests, allPackages, installRequest,
                             deletePackageAction, allowedSharedLibInfos, signingDetails,
-                            sharedUserSignaturesChanged, removeAppKeySetData));
-        }
+                            sharedUserSignaturesChanged, removeAppKeySetData);
 
-        for (String installPackageName : scannedPackages.keySet()) {
             // Check all shared libraries and map to their actual file path.
             // We only do this here for apps not on a system dir, because those
             // are the only ones that can fail an install due to this.  We
@@ -271,20 +269,21 @@
             // library paths after the scan is done. Also during the initial
             // scan don't update any libs as we do this wholesale after all
             // apps are scanned to avoid dependency based scanning.
-            final ScanResult scanResult = scannedPackages.get(installPackageName);
-            if ((scanResult.mRequest.mScanFlags & SCAN_BOOTING) != 0
-                    || (scanResult.mRequest.mParseFlags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR)
-                    != 0) {
-                continue;
+            if ((installRequest.getScanFlags() & SCAN_BOOTING) == 0
+                    && (installRequest.getParseFlags() & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR)
+                    == 0) {
+                try {
+                    reconciledPackage.mCollectedSharedLibraryInfos =
+                            sharedLibraries.collectSharedLibraryInfos(
+                                    installRequest.getParsedPackage(), combinedPackages,
+                                    incomingSharedLibraries);
+                } catch (PackageManagerException e) {
+                    throw new ReconcileFailure(e.error, e.getMessage());
+                }
             }
-            try {
-                result.get(installPackageName).mCollectedSharedLibraryInfos =
-                        sharedLibraries.collectSharedLibraryInfos(
-                                scanResult.mRequest.mParsedPackage, combinedPackages,
-                                incomingSharedLibraries);
-            } catch (PackageManagerException e) {
-                throw new ReconcileFailure(e.error, e.getMessage());
-            }
+
+            installRequest.onReconcileFinished();
+            result.add(reconciledPackage);
         }
 
         return result;
diff --git a/services/core/java/com/android/server/pm/ReconcileRequest.java b/services/core/java/com/android/server/pm/ReconcileRequest.java
deleted file mode 100644
index 3568c15..0000000
--- a/services/core/java/com/android/server/pm/ReconcileRequest.java
+++ /dev/null
@@ -1,59 +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.
- */
-
-package com.android.server.pm;
-
-import com.android.server.pm.pkg.AndroidPackage;
-
-import java.util.Collections;
-import java.util.Map;
-
-/**
- * Package scan results and related request details used to reconcile the potential addition of
- * one or more packages to the system.
- *
- * Reconcile will take a set of package details that need to be committed to the system and make
- * sure that they are valid in the context of the system and the other installing apps. Any
- * invalid state or app will result in a failed reconciliation and thus whatever operation (such
- * as install) led to the request.
- */
-final class ReconcileRequest {
-    public final Map<String, ScanResult> mScannedPackages;
-
-    public final Map<String, AndroidPackage> mAllPackages;
-    public final Map<String, InstallRequest> mInstallRequests;
-    public final Map<String, PrepareResult> mPreparedPackages;
-    public final Map<String, Settings.VersionInfo> mVersionInfos;
-
-    ReconcileRequest(Map<String, ScanResult> scannedPackages,
-            Map<String, InstallRequest> installRequests,
-            Map<String, PrepareResult> preparedPackages,
-            Map<String, AndroidPackage> allPackages,
-            Map<String, Settings.VersionInfo> versionInfos) {
-        mScannedPackages = scannedPackages;
-        mInstallRequests = installRequests;
-        mPreparedPackages = preparedPackages;
-        mAllPackages = allPackages;
-        mVersionInfos = versionInfos;
-    }
-
-    ReconcileRequest(Map<String, ScanResult> scannedPackages,
-            Map<String, AndroidPackage> allPackages,
-            Map<String, Settings.VersionInfo> versionInfos) {
-        this(scannedPackages, Collections.emptyMap(),
-                Collections.emptyMap(), allPackages, versionInfos);
-    }
-}
diff --git a/services/core/java/com/android/server/pm/ReconciledPackage.java b/services/core/java/com/android/server/pm/ReconciledPackage.java
index d4da6c7..701baee 100644
--- a/services/core/java/com/android/server/pm/ReconciledPackage.java
+++ b/services/core/java/com/android/server/pm/ReconciledPackage.java
@@ -17,7 +17,6 @@
 package com.android.server.pm;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.pm.SharedLibraryInfo;
 import android.content.pm.SigningDetails;
 import android.util.ArrayMap;
@@ -33,12 +32,9 @@
  * TODO: move most of the data contained here into a PackageSetting for commit.
  */
 final class ReconciledPackage {
-    public final ReconcileRequest mRequest;
-    public final PackageSetting mPkgSetting;
-    public final ScanResult mScanResult;
-    // TODO: Remove install-specific details from the reconcile result
-    @Nullable public final PrepareResult mPrepareResult;
-    @Nullable public final InstallRequest mInstallRequest;
+    private final List<InstallRequest> mInstallRequests;
+    private final Map<String, AndroidPackage> mAllPackages;
+    @NonNull public final InstallRequest mInstallRequest;
     public final DeletePackageAction mDeletePackageAction;
     public final List<SharedLibraryInfo> mAllowedSharedLibraryInfos;
     public final SigningDetails mSigningDetails;
@@ -46,21 +42,17 @@
     public ArrayList<SharedLibraryInfo> mCollectedSharedLibraryInfos;
     public final boolean mRemoveAppKeySetData;
 
-    ReconciledPackage(ReconcileRequest request,
+    ReconciledPackage(List<InstallRequest> installRequests,
+            Map<String, AndroidPackage> allPackages,
             InstallRequest installRequest,
-            PackageSetting pkgSetting,
-            PrepareResult prepareResult,
-            ScanResult scanResult,
             DeletePackageAction deletePackageAction,
             List<SharedLibraryInfo> allowedSharedLibraryInfos,
             SigningDetails signingDetails,
             boolean sharedUserSignaturesChanged,
             boolean removeAppKeySetData) {
-        mRequest = request;
+        mInstallRequests = installRequests;
+        mAllPackages = allPackages;
         mInstallRequest = installRequest;
-        mPkgSetting = pkgSetting;
-        mPrepareResult = prepareResult;
-        mScanResult = scanResult;
         mDeletePackageAction = deletePackageAction;
         mAllowedSharedLibraryInfos = allowedSharedLibraryInfos;
         mSigningDetails = signingDetails;
@@ -75,13 +67,13 @@
      */
     @NonNull Map<String, AndroidPackage> getCombinedAvailablePackages() {
         final ArrayMap<String, AndroidPackage> combined =
-                new ArrayMap<>(mRequest.mAllPackages.size() + mRequest.mScannedPackages.size());
+                new ArrayMap<>(mAllPackages.size() + mInstallRequests.size());
 
-        combined.putAll(mRequest.mAllPackages);
+        combined.putAll(mAllPackages);
 
-        for (ScanResult scanResult : mRequest.mScannedPackages.values()) {
-            combined.put(scanResult.mPkgSetting.getPackageName(),
-                    scanResult.mRequest.mParsedPackage);
+        for (InstallRequest installRequest : mInstallRequests) {
+            combined.put(installRequest.getScannedPackageSetting().getPackageName(),
+                    installRequest.getParsedPackage());
         }
 
         return combined;
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index bbc4fde..7e93673 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -308,7 +308,7 @@
                 mPm.mSettings.getKeySetManagerService().removeAppKeySetDataLPw(packageName);
                 final Computer snapshot = mPm.snapshotComputer();
                 mPm.mAppsFilter.removePackage(snapshot,
-                        snapshot.getPackageStateInternal(packageName), false /* isReplace */);
+                        snapshot.getPackageStateInternal(packageName));
                 removedAppId = mPm.mSettings.removePackageLPw(packageName);
                 if (outInfo != null) {
                     outInfo.mRemovedAppId = removedAppId;
diff --git a/services/core/java/com/android/server/pm/RestrictionsSet.java b/services/core/java/com/android/server/pm/RestrictionsSet.java
index e5a70c3..e7ad5b9 100644
--- a/services/core/java/com/android/server/pm/RestrictionsSet.java
+++ b/services/core/java/com/android/server/pm/RestrictionsSet.java
@@ -22,10 +22,10 @@
 import android.os.Bundle;
 import android.os.UserManager;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index bd3c7dd..a905df9 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -462,7 +462,7 @@
             }
         }
 
-        return new ScanResult(request, true, pkgSetting, changedAbiCodePath,
+        return new ScanResult(request, pkgSetting, changedAbiCodePath,
                 !createNewPackage /* existingSettingCopied */,
                 Process.INVALID_UID /* previousAppId */ , sdkLibraryInfo,
                 staticSharedLibraryInfo, dynamicSharedLibraryInfos);
diff --git a/services/core/java/com/android/server/pm/ScanResult.java b/services/core/java/com/android/server/pm/ScanResult.java
index e2860ca..750e893 100644
--- a/services/core/java/com/android/server/pm/ScanResult.java
+++ b/services/core/java/com/android/server/pm/ScanResult.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.pm.SharedLibraryInfo;
 import android.os.Process;
@@ -28,9 +29,7 @@
 @VisibleForTesting
 final class ScanResult {
     /** The request that initiated the scan that produced this result. */
-    public final ScanRequest mRequest;
-    /** Whether or not the package scan was successful */
-    public final boolean mSuccess;
+    @NonNull public final ScanRequest mRequest;
     /**
      * Whether or not the original PackageSetting needs to be updated with this result on
      * commit.
@@ -58,7 +57,7 @@
     public final List<SharedLibraryInfo> mDynamicSharedLibraryInfos;
 
     ScanResult(
-            ScanRequest request, boolean success,
+            @NonNull ScanRequest request,
             @Nullable PackageSetting pkgSetting,
             @Nullable List<String> changedAbiCodePath, boolean existingSettingCopied,
             int previousAppId,
@@ -66,7 +65,6 @@
             SharedLibraryInfo staticSharedLibraryInfo,
             List<SharedLibraryInfo> dynamicSharedLibraryInfos) {
         mRequest = request;
-        mSuccess = success;
         mPkgSetting = pkgSetting;
         mChangedAbiCodePath = changedAbiCodePath;
         mExistingSettingCopied = existingSettingCopied;
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index f2a7651..45c0d6e 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -83,8 +83,6 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -96,6 +94,8 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.JournaledFile;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.permission.persistence.RuntimePermissionsPersistence;
 import com.android.permission.persistence.RuntimePermissionsState;
 import com.android.server.LocalServices;
@@ -455,19 +455,24 @@
     // The user's preferred activities associated with particular intent
     // filters.
     @Watched
-    private final WatchedSparseArray<PreferredIntentResolver>
-            mPreferredActivities = new WatchedSparseArray<>();
+    private final WatchedSparseArray<PreferredIntentResolver> mPreferredActivities;
+    private final SnapshotCache<WatchedSparseArray<PreferredIntentResolver>>
+            mPreferredActivitiesSnapshot;
 
     // The persistent preferred activities of the user's profile/device owner
     // associated with particular intent filters.
     @Watched
     private final WatchedSparseArray<PersistentPreferredIntentResolver>
-            mPersistentPreferredActivities = new WatchedSparseArray<>();
+            mPersistentPreferredActivities;
+    private final SnapshotCache<WatchedSparseArray<PersistentPreferredIntentResolver>>
+            mPersistentPreferredActivitiesSnapshot;
+
 
     // For every user, it is used to find to which other users the intent can be forwarded.
     @Watched
-    private final WatchedSparseArray<CrossProfileIntentResolver>
-            mCrossProfileIntentResolvers = new WatchedSparseArray<>();
+    private final WatchedSparseArray<CrossProfileIntentResolver> mCrossProfileIntentResolvers;
+    private final SnapshotCache<WatchedSparseArray<CrossProfileIntentResolver>>
+            mCrossProfileIntentResolversSnapshot;
 
     @Watched
     final WatchedArrayMap<String, SharedUserSetting> mSharedUsers = new WatchedArrayMap<>();
@@ -476,11 +481,12 @@
 
     // For reading/writing settings file.
     @Watched
-    private final WatchedArrayList<Signature> mPastSignatures =
-            new WatchedArrayList<Signature>();
+    private final WatchedArrayList<Signature> mPastSignatures;
+    private final SnapshotCache<WatchedArrayList<Signature>> mPastSignaturesSnapshot;
+
     @Watched
-    private final WatchedArrayMap<Long, Integer> mKeySetRefs =
-            new WatchedArrayMap<Long, Integer>();
+    private final WatchedArrayMap<Long, Integer> mKeySetRefs;
+    private final SnapshotCache<WatchedArrayMap<Long, Integer>> mKeySetRefsSnapshot;
 
     // Packages that have been renamed since they were first installed.
     // Keys are the new names of the packages, values are the original
@@ -511,7 +517,8 @@
      * scanning to make it less confusing.
      */
     @Watched
-    private final WatchedArrayList<PackageSetting> mPendingPackages = new WatchedArrayList<>();
+    private final WatchedArrayList<PackageSetting> mPendingPackages;
+    private final SnapshotCache<WatchedArrayList<PackageSetting>> mPendingPackagesSnapshot;
 
     private final File mSystemDir;
 
@@ -583,6 +590,26 @@
         mInstallerPackagesSnapshot =
                 new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages,
                                          "Settings.mInstallerPackages");
+        mPreferredActivities = new WatchedSparseArray<>();
+        mPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(mPreferredActivities,
+                mPreferredActivities, "Settings.mPreferredActivities");
+        mPersistentPreferredActivities = new WatchedSparseArray<>();
+        mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(
+                mPersistentPreferredActivities, mPersistentPreferredActivities,
+                "Settings.mPersistentPreferredActivities");
+        mCrossProfileIntentResolvers = new WatchedSparseArray<>();
+        mCrossProfileIntentResolversSnapshot = new SnapshotCache.Auto<>(
+                mCrossProfileIntentResolvers, mCrossProfileIntentResolvers,
+                "Settings.mCrossProfileIntentResolvers");
+        mPastSignatures = new WatchedArrayList<>();
+        mPastSignaturesSnapshot = new SnapshotCache.Auto<>(mPastSignatures, mPastSignatures,
+                "Settings.mPastSignatures");
+        mKeySetRefs = new WatchedArrayMap<>();
+        mKeySetRefsSnapshot = new SnapshotCache.Auto<>(mKeySetRefs, mKeySetRefs,
+                "Settings.mKeySetRefs");
+        mPendingPackages = new WatchedArrayList<>();
+        mPendingPackagesSnapshot = new SnapshotCache.Auto<>(mPendingPackages, mPendingPackages,
+                "Settings.mPendingPackages");
         mKeySetManagerService = new KeySetManagerService(mPackages);
 
         // Test-only handler working on background thread.
@@ -623,6 +650,26 @@
         mInstallerPackagesSnapshot =
                 new SnapshotCache.Auto<>(mInstallerPackages, mInstallerPackages,
                                          "Settings.mInstallerPackages");
+        mPreferredActivities = new WatchedSparseArray<>();
+        mPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(mPreferredActivities,
+                mPreferredActivities, "Settings.mPreferredActivities");
+        mPersistentPreferredActivities = new WatchedSparseArray<>();
+        mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Auto<>(
+                mPersistentPreferredActivities, mPersistentPreferredActivities,
+                "Settings.mPersistentPreferredActivities");
+        mCrossProfileIntentResolvers = new WatchedSparseArray<>();
+        mCrossProfileIntentResolversSnapshot = new SnapshotCache.Auto<>(
+                mCrossProfileIntentResolvers, mCrossProfileIntentResolvers,
+                "Settings.mCrossProfileIntentResolvers");
+        mPastSignatures = new WatchedArrayList<>();
+        mPastSignaturesSnapshot = new SnapshotCache.Auto<>(mPastSignatures, mPastSignatures,
+                "Settings.mPastSignatures");
+        mKeySetRefs = new WatchedArrayMap<>();
+        mKeySetRefsSnapshot = new SnapshotCache.Auto<>(mKeySetRefs, mKeySetRefs,
+                "Settings.mKeySetRefs");
+        mPendingPackages = new WatchedArrayList<>();
+        mPendingPackagesSnapshot = new SnapshotCache.Auto<>(mPendingPackages, mPendingPackages,
+                "Settings.mPendingPackages");
         mKeySetManagerService = new KeySetManagerService(mPackages);
 
         mHandler = handler;
@@ -699,24 +746,27 @@
         mBlockUninstallPackages.snapshot(r.mBlockUninstallPackages);
         mVersion.putAll(r.mVersion);
         mVerifierDeviceIdentity = r.mVerifierDeviceIdentity;
-        WatchedSparseArray.snapshot(
-                mPreferredActivities, r.mPreferredActivities);
-        WatchedSparseArray.snapshot(
-                mPersistentPreferredActivities, r.mPersistentPreferredActivities);
-        WatchedSparseArray.snapshot(
-                mCrossProfileIntentResolvers, r.mCrossProfileIntentResolvers);
+        mPreferredActivities = r.mPreferredActivitiesSnapshot.snapshot();
+        mPreferredActivitiesSnapshot = new SnapshotCache.Sealed<>();
+        mPersistentPreferredActivities = r.mPersistentPreferredActivitiesSnapshot.snapshot();
+        mPersistentPreferredActivitiesSnapshot = new SnapshotCache.Sealed<>();
+        mCrossProfileIntentResolvers = r.mCrossProfileIntentResolversSnapshot.snapshot();
+        mCrossProfileIntentResolversSnapshot = new SnapshotCache.Sealed<>();
+
         mSharedUsers.snapshot(r.mSharedUsers);
         mAppIds = r.mAppIds.snapshot();
-        WatchedArrayList.snapshot(
-                mPastSignatures, r.mPastSignatures);
-        WatchedArrayMap.snapshot(
-                mKeySetRefs, r.mKeySetRefs);
+
+        mPastSignatures = r.mPastSignaturesSnapshot.snapshot();
+        mPastSignaturesSnapshot = new SnapshotCache.Sealed<>();
+        mKeySetRefs = r.mKeySetRefsSnapshot.snapshot();
+        mKeySetRefsSnapshot = new SnapshotCache.Sealed<>();
+
         mRenamedPackages.snapshot(r.mRenamedPackages);
         mNextAppLinkGeneration.snapshot(r.mNextAppLinkGeneration);
         mDefaultBrowserApp.snapshot(r.mDefaultBrowserApp);
         // mReadMessages
-        WatchedArrayList.snapshot(
-                mPendingPackages, r.mPendingPackages);
+        mPendingPackages = r.mPendingPackagesSnapshot.snapshot();
+        mPendingPackagesSnapshot = new SnapshotCache.Sealed<>();
         mSystemDir = null;
         // mKeySetManagerService;
         mPermissions = r.mPermissions;
@@ -4185,15 +4235,16 @@
                         // such as APEX
                         continue;
                     }
-                    // Need to create a data directory for all apps installed for this user.
-                    // Accumulate all required args and call the installer after mPackages lock
-                    // has been released
+                    // We need to create the DE data directory for all apps installed for this user.
+                    // (CE storage is not ready yet; the CE data directories will be created later,
+                    // when the user is "unlocked".)  Accumulate all required args, and call the
+                    // installer after the mPackages lock has been released.
                     final String seInfo = AndroidPackageUtils.getSeInfo(ps.getPkg(), ps);
                     final boolean usesSdk = !ps.getPkg().getUsesSdkLibraries().isEmpty();
                     final CreateAppDataArgs args = Installer.buildCreateAppDataArgs(
                             ps.getVolumeUuid(), ps.getPackageName(), userHandle,
-                            StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE,
-                            ps.getAppId(), seInfo, ps.getPkg().getTargetSdkVersion(), usesSdk);
+                            StorageManager.FLAG_STORAGE_DE, ps.getAppId(), seInfo,
+                            ps.getPkg().getTargetSdkVersion(), usesSdk);
                     batch.createAppData(args);
                 } else {
                     // Make sure the app is excluded from storage mapping for this user
diff --git a/services/core/java/com/android/server/pm/SettingsXml.java b/services/core/java/com/android/server/pm/SettingsXml.java
index c53fef7..5fb6731 100644
--- a/services/core/java/com/android/server/pm/SettingsXml.java
+++ b/services/core/java/com/android/server/pm/SettingsXml.java
@@ -19,10 +19,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/pm/ShareTargetInfo.java b/services/core/java/com/android/server/pm/ShareTargetInfo.java
index 660874e..bfb5f39 100644
--- a/services/core/java/com/android/server/pm/ShareTargetInfo.java
+++ b/services/core/java/com/android/server/pm/ShareTargetInfo.java
@@ -17,8 +17,9 @@
 
 import android.annotation.NonNull;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
index 094e748..aa23d8d 100644
--- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
+++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
@@ -422,15 +422,18 @@
      * Given a package scanned result of a static shared library, returns its package setting of
      * the latest version
      *
-     * @param scanResult The scanned result of a static shared library package.
+     * @param installRequest The install result of a static shared library package.
      * @return The package setting that represents the latest version of shared library info.
      */
     @Nullable
-    PackageSetting getStaticSharedLibLatestVersionSetting(@NonNull ScanResult scanResult) {
+    PackageSetting getStaticSharedLibLatestVersionSetting(@NonNull InstallRequest installRequest) {
+        if (installRequest.getParsedPackage() == null) {
+            return null;
+        }
         PackageSetting sharedLibPackage = null;
         synchronized (mPm.mLock) {
             final SharedLibraryInfo latestSharedLibraVersionLPr =
-                    getLatestStaticSharedLibraVersionLPr(scanResult.mRequest.mParsedPackage);
+                    getLatestStaticSharedLibraVersionLPr(installRequest.getParsedPackage());
             if (latestSharedLibraVersionLPr != null) {
                 sharedLibPackage = mPm.mSettings.getPackageLPr(
                         latestSharedLibraVersionLPr.getPackageName());
@@ -823,34 +826,35 @@
      * Compare the newly scanned package with current system state to see which of its declared
      * shared libraries should be allowed to be added to the system.
      */
-    List<SharedLibraryInfo> getAllowedSharedLibInfos(ScanResult scanResult) {
+    List<SharedLibraryInfo> getAllowedSharedLibInfos(InstallRequest installRequest) {
         // Let's used the parsed package as scanResult.pkgSetting may be null
-        final ParsedPackage parsedPackage = scanResult.mRequest.mParsedPackage;
-        if (scanResult.mSdkSharedLibraryInfo == null && scanResult.mStaticSharedLibraryInfo == null
-                && scanResult.mDynamicSharedLibraryInfos == null) {
+        final ParsedPackage parsedPackage = installRequest.getParsedPackage();
+        if (installRequest.getSdkSharedLibraryInfo() == null
+                && installRequest.getStaticSharedLibraryInfo() == null
+                && installRequest.getDynamicSharedLibraryInfos() == null) {
             return null;
         }
 
         // Any app can add new SDKs and static shared libraries.
-        if (scanResult.mSdkSharedLibraryInfo != null) {
-            return Collections.singletonList(scanResult.mSdkSharedLibraryInfo);
+        if (installRequest.getSdkSharedLibraryInfo() != null) {
+            return Collections.singletonList(installRequest.getSdkSharedLibraryInfo());
         }
-        if (scanResult.mStaticSharedLibraryInfo != null) {
-            return Collections.singletonList(scanResult.mStaticSharedLibraryInfo);
+        if (installRequest.getStaticSharedLibraryInfo() != null) {
+            return Collections.singletonList(installRequest.getStaticSharedLibraryInfo());
         }
-        final boolean hasDynamicLibraries = parsedPackage.isSystem()
-                && scanResult.mDynamicSharedLibraryInfos != null;
+        final boolean hasDynamicLibraries = parsedPackage != null && parsedPackage.isSystem()
+                && installRequest.getDynamicSharedLibraryInfos() != null;
         if (!hasDynamicLibraries) {
             return null;
         }
-        final boolean isUpdatedSystemApp = scanResult.mPkgSetting.getPkgState()
-                .isUpdatedSystemApp();
+        final boolean isUpdatedSystemApp = installRequest.getScannedPackageSetting() != null
+                && installRequest.getScannedPackageSetting().getPkgState().isUpdatedSystemApp();
         // We may not yet have disabled the updated package yet, so be sure to grab the
         // current setting if that's the case.
         final PackageSetting updatedSystemPs = isUpdatedSystemApp
-                ? scanResult.mRequest.mDisabledPkgSetting == null
-                ? scanResult.mRequest.mOldPkgSetting
-                : scanResult.mRequest.mDisabledPkgSetting
+                ? installRequest.getDisabledPackageSetting() == null
+                ? installRequest.getScanRequestOldPackageSetting()
+                : installRequest.getDisabledPackageSetting()
                 : null;
         if (isUpdatedSystemApp && (updatedSystemPs.getPkg() == null
                 || updatedSystemPs.getPkg().getLibraryNames() == null)) {
@@ -859,8 +863,8 @@
             return null;
         }
         final ArrayList<SharedLibraryInfo> infos =
-                new ArrayList<>(scanResult.mDynamicSharedLibraryInfos.size());
-        for (SharedLibraryInfo info : scanResult.mDynamicSharedLibraryInfos) {
+                new ArrayList<>(installRequest.getDynamicSharedLibraryInfos().size());
+        for (SharedLibraryInfo info : installRequest.getDynamicSharedLibraryInfos()) {
             final String name = info.getName();
             if (isUpdatedSystemApp) {
                 // New library entries can only be added through the
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index c6a7dd7..00f7dc4 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -20,17 +20,17 @@
 import android.annotation.UserIdInt;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
+import android.content.pm.UserPackage;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.DumpFilter;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
 
 import libcore.io.IoUtils;
 
@@ -70,7 +70,7 @@
     /**
      * Package name -> IDs.
      */
-    final private ArrayMap<PackageWithUser, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
+    private final ArrayMap<UserPackage, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
 
     private ShortcutLauncher(@NonNull ShortcutUser shortcutUser,
             @UserIdInt int ownerUserId, @NonNull String packageName,
@@ -101,12 +101,12 @@
      * Called when the new package can't receive the backup, due to signature or version mismatch.
      */
     private void onRestoreBlocked() {
-        final ArrayList<PackageWithUser> pinnedPackages =
+        final ArrayList<UserPackage> pinnedPackages =
                 new ArrayList<>(mPinnedShortcuts.keySet());
         mPinnedShortcuts.clear();
         for (int i = pinnedPackages.size() - 1; i >= 0; i--) {
-            final PackageWithUser pu = pinnedPackages.get(i);
-            final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(pu.packageName);
+            final UserPackage up = pinnedPackages.get(i);
+            final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(up.packageName);
             if (p != null) {
                 p.refreshPinnedFlags();
             }
@@ -135,13 +135,13 @@
             return; // No need to instantiate.
         }
 
-        final PackageWithUser pu = PackageWithUser.of(packageUserId, packageName);
+        final UserPackage up = UserPackage.of(packageUserId, packageName);
 
         final int idSize = ids.size();
         if (idSize == 0) {
-            mPinnedShortcuts.remove(pu);
+            mPinnedShortcuts.remove(up);
         } else {
-            final ArraySet<String> prevSet = mPinnedShortcuts.get(pu);
+            final ArraySet<String> prevSet = mPinnedShortcuts.get(up);
 
             // Actually pin shortcuts.
             // This logic here is to make sure a launcher cannot pin a shortcut that is not dynamic
@@ -165,7 +165,7 @@
                     newSet.add(id);
                 }
             }
-            mPinnedShortcuts.put(pu, newSet);
+            mPinnedShortcuts.put(up, newSet);
         }
         packageShortcuts.refreshPinnedFlags();
     }
@@ -176,7 +176,7 @@
     @Nullable
     public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName,
             @UserIdInt int packageUserId) {
-        return mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName));
+        return mPinnedShortcuts.get(UserPackage.of(packageUserId, packageName));
     }
 
     /**
@@ -207,7 +207,7 @@
     }
 
     boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) {
-        return mPinnedShortcuts.remove(PackageWithUser.of(packageUserId, packageName)) != null;
+        return mPinnedShortcuts.remove(UserPackage.of(packageUserId, packageName)) != null;
     }
 
     public void ensurePackageInfo() {
@@ -241,15 +241,15 @@
         getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
 
         for (int i = 0; i < size; i++) {
-            final PackageWithUser pu = mPinnedShortcuts.keyAt(i);
+            final UserPackage up = mPinnedShortcuts.keyAt(i);
 
-            if (forBackup && (pu.userId != getOwnerUserId())) {
+            if (forBackup && (up.userId != getOwnerUserId())) {
                 continue; // Target package on a different user, skip. (i.e. work profile)
             }
 
             out.startTag(null, TAG_PACKAGE);
-            ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, pu.packageName);
-            ShortcutService.writeAttr(out, ATTR_PACKAGE_USER_ID, pu.userId);
+            ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, up.packageName);
+            ShortcutService.writeAttr(out, ATTR_PACKAGE_USER_ID, up.userId);
 
             final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
             final int idSize = ids.size();
@@ -345,7 +345,7 @@
                                 ATTR_PACKAGE_USER_ID, ownerUserId);
                         ids = new ArraySet<>();
                         ret.mPinnedShortcuts.put(
-                                PackageWithUser.of(packageUserId, packageName), ids);
+                                UserPackage.of(packageUserId, packageName), ids);
                         continue;
                     }
                 }
@@ -386,14 +386,14 @@
         for (int i = 0; i < size; i++) {
             pw.println();
 
-            final PackageWithUser pu = mPinnedShortcuts.keyAt(i);
+            final UserPackage up = mPinnedShortcuts.keyAt(i);
 
             pw.print(prefix);
             pw.print("  ");
             pw.print("Package: ");
-            pw.print(pu.packageName);
+            pw.print(up.packageName);
             pw.print("  User: ");
-            pw.println(pu.userId);
+            pw.println(up.userId);
 
             final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
             final int idSize = ids.size();
@@ -418,7 +418,7 @@
 
     @VisibleForTesting
     ArraySet<String> getAllPinnedShortcutsForTest(String packageName, int packageUserId) {
-        return new ArraySet<>(mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName)));
+        return new ArraySet<>(mPinnedShortcuts.get(UserPackage.of(packageUserId, packageName)));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 890c891..0362ddd 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -53,8 +53,6 @@
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -65,6 +63,8 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.DumpFilter;
 import com.android.server.pm.ShortcutService.ShortcutOperation;
 import com.android.server.pm.ShortcutService.Stats;
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
index fce6610..79b725d 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
@@ -23,10 +23,10 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningInfo;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.backup.BackupUtils;
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 7800183..e20330d 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -22,11 +22,11 @@
 import android.graphics.Bitmap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 0b20683..83720f1 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -57,6 +57,7 @@
 import android.content.pm.ShortcutManager;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.Bitmap;
@@ -100,8 +101,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.IWindowManager;
 
@@ -116,9 +115,10 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.StatLogger;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
 import com.android.server.uri.UriGrantsManagerInternal;
 
 import libcore.io.IoUtils;
@@ -3774,7 +3774,7 @@
 
         final long start = getStatStartTime();
         try {
-            final ArrayList<PackageWithUser> gonePackages = new ArrayList<>();
+            final ArrayList<UserPackage> gonePackages = new ArrayList<>();
 
             synchronized (mLock) {
                 final ShortcutUser user = getUserShortcutsLocked(ownerUserId);
@@ -3789,13 +3789,14 @@
                             Slog.d(TAG, "Uninstalled: " + spi.getPackageName()
                                     + " user " + spi.getPackageUserId());
                         }
-                        gonePackages.add(PackageWithUser.of(spi));
+                        gonePackages.add(
+                                UserPackage.of(spi.getPackageUserId(), spi.getPackageName()));
                     }
                 });
                 if (gonePackages.size() > 0) {
                     for (int i = gonePackages.size() - 1; i >= 0; i--) {
-                        final PackageWithUser pu = gonePackages.get(i);
-                        cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId,
+                        final UserPackage up = gonePackages.get(i);
+                        cleanUpPackageLocked(up.packageName, ownerUserId, up.userId,
                                 /* appStillExists = */ false);
                     }
                 }
@@ -5274,7 +5275,7 @@
             final ShortcutUser user = mUsers.get(userId);
             if (user == null) return null;
 
-            return user.getAllLaunchersForTest().get(PackageWithUser.of(userId, packageName));
+            return user.getAllLaunchersForTest().get(UserPackage.of(userId, packageName));
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index b9fd2fd..94eb6bb 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -21,6 +21,7 @@
 import android.app.appsearch.AppSearchManager;
 import android.app.appsearch.AppSearchSession;
 import android.content.pm.ShortcutManager;
+import android.content.pm.UserPackage;
 import android.metrics.LogMaker;
 import android.os.Binder;
 import android.os.FileUtils;
@@ -30,14 +31,14 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 import com.android.server.pm.ShortcutService.DumpFilter;
 import com.android.server.pm.ShortcutService.InvalidFileFormatException;
@@ -52,7 +53,6 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
@@ -82,44 +82,6 @@
     private static final String KEY_LAUNCHERS = "launchers";
     private static final String KEY_PACKAGES = "packages";
 
-    static final class PackageWithUser {
-        final int userId;
-        final String packageName;
-
-        private PackageWithUser(int userId, String packageName) {
-            this.userId = userId;
-            this.packageName = Objects.requireNonNull(packageName);
-        }
-
-        public static PackageWithUser of(int userId, String packageName) {
-            return new PackageWithUser(userId, packageName);
-        }
-
-        public static PackageWithUser of(ShortcutPackageItem spi) {
-            return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName());
-        }
-
-        @Override
-        public int hashCode() {
-            return packageName.hashCode() ^ userId;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (!(obj instanceof PackageWithUser)) {
-                return false;
-            }
-            final PackageWithUser that = (PackageWithUser) obj;
-
-            return userId == that.userId && packageName.equals(that.packageName);
-        }
-
-        @Override
-        public String toString() {
-            return String.format("[Package: %d, %s]", userId, packageName);
-        }
-    }
-
     final ShortcutService mService;
     final AppSearchManager mAppSearchManager;
     final Executor mExecutor;
@@ -129,7 +91,7 @@
 
     private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
 
-    private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
+    private final ArrayMap<UserPackage, ShortcutLauncher> mLaunchers = new ArrayMap<>();
 
     /** In-memory-cached default launcher. */
     private String mCachedLauncher;
@@ -204,20 +166,20 @@
     // We don't expose this directly to non-test code because only ShortcutUser should add to/
     // remove from it.
     @VisibleForTesting
-    ArrayMap<PackageWithUser, ShortcutLauncher> getAllLaunchersForTest() {
+    ArrayMap<UserPackage, ShortcutLauncher> getAllLaunchersForTest() {
         return mLaunchers;
     }
 
     private void addLauncher(ShortcutLauncher launcher) {
         launcher.replaceUser(this);
-        mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(),
+        mLaunchers.put(UserPackage.of(launcher.getPackageUserId(),
                 launcher.getPackageName()), launcher);
     }
 
     @Nullable
     public ShortcutLauncher removeLauncher(
             @UserIdInt int packageUserId, @NonNull String packageName) {
-        return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
+        return mLaunchers.remove(UserPackage.of(packageUserId, packageName));
     }
 
     @Nullable
@@ -242,7 +204,7 @@
     @NonNull
     public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
             @UserIdInt int launcherUserId) {
-        final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
+        final UserPackage key = UserPackage.of(launcherUserId, packageName);
         ShortcutLauncher ret = mLaunchers.get(key);
         if (ret == null) {
             ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
diff --git a/services/core/java/com/android/server/pm/SnapshotStatistics.java b/services/core/java/com/android/server/pm/SnapshotStatistics.java
index 2cfc894..e04a1e5 100644
--- a/services/core/java/com/android/server/pm/SnapshotStatistics.java
+++ b/services/core/java/com/android/server/pm/SnapshotStatistics.java
@@ -24,11 +24,13 @@
 import android.text.TextUtils;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.EventLogTags;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.concurrent.TimeUnit;
 
 /**
  * This class records statistics about PackageManagerService snapshots.  It maintains two sets of
@@ -59,9 +61,9 @@
     public static final int SNAPSHOT_TICK_INTERVAL_MS = 60 * 1000;
 
     /**
-     * The number of ticks for long statistics.  This is one week.
+     * The interval of the snapshot statistics logging.
      */
-    public static final int SNAPSHOT_LONG_TICKS = 7 * 24 * 60;
+    private static final long SNAPSHOT_LOG_INTERVAL_US = TimeUnit.DAYS.toMicros(1);
 
     /**
      * The number snapshot event logs that can be generated in a single logging interval.
@@ -93,6 +95,28 @@
     public static final int SNAPSHOT_SHORT_LIFETIME = 5;
 
     /**
+     *  Buckets to represent a range of the rebuild latency for the histogram of
+     *  snapshot rebuild latency.
+     */
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS = 1;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS = 2;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS = 5;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS = 10;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS = 20;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS = 50;
+    private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS = 100;
+
+    /**
+     *  Buckets to represent a range of the reuse count for the histogram of
+     *  snapshot reuse counts.
+     */
+    private static final int REUSE_COUNT_BUCKET_LESS_THAN_1 = 1;
+    private static final int REUSE_COUNT_BUCKET_LESS_THAN_10 = 10;
+    private static final int REUSE_COUNT_BUCKET_LESS_THAN_100 = 100;
+    private static final int REUSE_COUNT_BUCKET_LESS_THAN_1000 = 1000;
+    private static final int REUSE_COUNT_BUCKET_LESS_THAN_10000 = 10000;
+
+    /**
      * The lock to control access to this object.
      */
     private final Object mLock = new Object();
@@ -113,11 +137,6 @@
     private int mEventsReported = 0;
 
     /**
-     * The tick counter.  At the default tick interval, this wraps every 4000 years or so.
-     */
-    private int mTicks = 0;
-
-    /**
      * The handler used for the periodic ticks.
      */
     private Handler mHandler = null;
@@ -139,8 +158,6 @@
 
         // The number of bins
         private int mCount;
-        // The mapping of low integers to bins
-        private int[] mBinMap;
         // The maximum mapped value.  Values at or above this are mapped to the
         // top bin.
         private int mMaxBin;
@@ -158,16 +175,6 @@
             mCount = mUserKey.length + 1;
             // The maximum value is one more than the last one in the map.
             mMaxBin = mUserKey[mUserKey.length - 1] + 1;
-            mBinMap = new int[mMaxBin + 1];
-
-            int j = 0;
-            for (int i = 0; i < mUserKey.length; i++) {
-                while (j <= mUserKey[i]) {
-                    mBinMap[j] = i;
-                    j++;
-                }
-            }
-            mBinMap[mMaxBin] = mUserKey.length;
         }
 
         /**
@@ -175,9 +182,14 @@
          */
         public int getBin(int x) {
             if (x >= 0 && x < mMaxBin) {
-                return mBinMap[x];
+                for (int i = 0; i < mUserKey.length; i++) {
+                    if (x <= mUserKey[i]) {
+                        return i;
+                    }
+                }
+                return 0; // should not happen
             } else if (x >= mMaxBin) {
-                return mBinMap[mMaxBin];
+                return mUserKey.length;
             } else {
                 // x is negative.  The bin will not be used.
                 return 0;
@@ -263,6 +275,11 @@
         public int mMaxBuildTimeUs = 0;
 
         /**
+         * The maximum used count since the last log.
+         */
+        public int mMaxUsedCount = 0;
+
+        /**
          * Record the rebuild.  The parameters are the length of time it took to build the
          * latest snapshot, and the number of times the _previous_ snapshot was used.  A
          * negative value for used signals an invalid value, which is the case the first
@@ -279,7 +296,6 @@
             }
 
             mTotalTimeUs += duration;
-            boolean reportIt = false;
 
             if (big) {
                 mBigBuilds++;
@@ -290,6 +306,9 @@
             if (mMaxBuildTimeUs < duration) {
                 mMaxBuildTimeUs = duration;
             }
+            if (mMaxUsedCount < used) {
+                mMaxUsedCount = used;
+            }
         }
 
         private Stats(long now) {
@@ -313,6 +332,7 @@
             mShortLived = orig.mShortLived;
             mTotalTimeUs = orig.mTotalTimeUs;
             mMaxBuildTimeUs = orig.mMaxBuildTimeUs;
+            mMaxUsedCount = orig.mMaxUsedCount;
         }
 
         /**
@@ -443,18 +463,19 @@
         }
 
         /**
-         * Report the object via an event.  Presumably the record indicates an anomalous
-         * incident.
+         * Report the snapshot statistics to FrameworkStatsLog.
          */
-        private void report() {
-            EventLogTags.writePmSnapshotStats(
-                    mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived,
-                    mMaxBuildTimeUs / US_IN_MS, mTotalTimeUs / US_IN_MS);
+        private void logSnapshotStatistics(int packageCount) {
+            final long avgLatencyUs = (mTotalBuilds == 0 ? 0 : mTotalTimeUs / mTotalBuilds);
+            final int avgUsedCount = (mTotalBuilds == 0 ? 0 : mTotalUsed / mTotalBuilds);
+            FrameworkStatsLog.write(
+                    FrameworkStatsLog.PACKAGE_MANAGER_SNAPSHOT_REPORTED, mTimes, mUsed,
+                    mMaxBuildTimeUs, mMaxUsedCount, avgLatencyUs, avgUsedCount, packageCount);
         }
     }
 
     /**
-     * Long statistics.  These roll over approximately every week.
+     * Long statistics.  These roll over approximately one day.
      */
     private Stats[] mLong;
 
@@ -464,10 +485,14 @@
     private Stats[] mShort;
 
     /**
-     * The time of the last build.  This can be used to compute the length of time a
-     * snapshot existed before being replaced.
+     * The time of last logging to the FrameworkStatsLog.
      */
-    private long mLastBuildTime = 0;
+    private long mLastLogTimeUs;
+
+    /**
+     * The number of packages on the device.
+     */
+    private int mPackageCount;
 
     /**
      * Create a snapshot object.  Initialize the bin levels.  The last bin catches
@@ -475,8 +500,20 @@
      */
     public SnapshotStatistics() {
         // Create the bin thresholds.  The time bins are in units of us.
-        mTimeBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 });
-        mUseBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 });
+        mTimeBins = new BinMap(new int[] {
+                REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS,
+                REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS });
+        mUseBins = new BinMap(new int[] {
+                REUSE_COUNT_BUCKET_LESS_THAN_1,
+                REUSE_COUNT_BUCKET_LESS_THAN_10,
+                REUSE_COUNT_BUCKET_LESS_THAN_100,
+                REUSE_COUNT_BUCKET_LESS_THAN_1000,
+                REUSE_COUNT_BUCKET_LESS_THAN_10000 });
 
         // Create the raw statistics
         final long now = SystemClock.currentTimeMicro();
@@ -484,6 +521,7 @@
         mLong[0] = new Stats(now);
         mShort = new Stats[10];
         mShort[0] = new Stats(now);
+        mLastLogTimeUs = now;
 
         // Create the message handler for ticks and start the ticker.
         mHandler = new Handler(Looper.getMainLooper()) {
@@ -516,13 +554,14 @@
      * @param now The time at which the snapshot rebuild began, in ns.
      * @param done The time at which the snapshot rebuild completed, in ns.
      * @param hits The number of times the previous snapshot was used.
+     * @param packageCount The number of packages on the device.
      */
-    public final void rebuild(long now, long done, int hits) {
+    public final void rebuild(long now, long done, int hits, int packageCount) {
         // The duration has a span of about 2000s
         final int duration = (int) (done - now);
         boolean reportEvent = false;
         synchronized (mLock) {
-            mLastBuildTime = now;
+            mPackageCount = packageCount;
 
             final int timeBin = mTimeBins.getBin(duration / 1000);
             final int useBin = mUseBins.getBin(hits);
@@ -570,10 +609,12 @@
     private void tick() {
         synchronized (mLock) {
             long now = SystemClock.currentTimeMicro();
-            mTicks++;
-            if (mTicks % SNAPSHOT_LONG_TICKS == 0) {
+            if (now - mLastLogTimeUs > SNAPSHOT_LOG_INTERVAL_US) {
                 shift(mLong, now);
+                mLastLogTimeUs = now;
+                mLong[mLong.length - 1].logSnapshotStatistics(mPackageCount);
             }
+
             shift(mShort, now);
             mEventsReported = 0;
         }
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index 477e260..fbfc84a 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -158,7 +158,7 @@
                 final AndroidPackage pkg;
                 try {
                     pkg = installPackageHelper.scanSystemPackageTracedLI(
-                            ps.getPath(), parseFlags, SCAN_INITIAL, null);
+                            ps.getPath(), parseFlags, SCAN_INITIAL);
                     loaded.add(pkg);
 
                 } catch (PackageManagerException e) {
@@ -196,8 +196,11 @@
                     appDataHelper.reconcileAppsDataLI(volumeUuid, user.id, flags,
                             true /* migrateAppData */);
                 }
-            } catch (IllegalStateException e) {
-                // Device was probably ejected, and we'll process that event momentarily
+            } catch (RuntimeException e) {
+                // The volume was probably already unmounted.  We'll probably process the unmount
+                // event momentarily.  TODO(b/256909937): ignoring errors from prepareUserStorage()
+                // is very dangerous.  Instead, we should fix the race condition that allows this
+                // code to run on an unmounted volume in the first place.
                 Slog.w(TAG, "Failed to prepare storage: " + e);
             }
         }
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 56ec8e4..1420cbf 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -25,6 +25,7 @@
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.UserManager;
+import android.util.DebugUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -46,6 +47,18 @@
     public @interface OwnerType {
     }
 
+    public static final int USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE = 1;
+    public static final int USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE = 2;
+    public static final int USER_ASSIGNMENT_RESULT_FAILURE = -1;
+
+    private static final String PREFIX_USER_ASSIGNMENT_RESULT = "USER_ASSIGNMENT_RESULT";
+    @IntDef(flag = false, prefix = {PREFIX_USER_ASSIGNMENT_RESULT}, value = {
+            USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE,
+            USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE,
+            USER_ASSIGNMENT_RESULT_FAILURE
+    })
+    public @interface UserAssignmentResult {}
+
     public interface UserRestrictionsListener {
         /**
          * Called when a user restriction changes.
@@ -77,6 +90,23 @@
     }
 
     /**
+     * Listener for {@link UserManager#isUserVisible() user visibility} changes.
+     */
+    public interface UserVisibilityListener {
+
+        /**
+         * Called when the {@link UserManager#isUserVisible() user visibility} changed.
+         *
+         * <p><b>Note:</b> this method is called independently of
+         * {@link com.android.server.SystemService} callbacks; for example, the call with
+         * {@code visible} {@code true} might be called before the
+         * {@link com.android.server.SystemService#onUserStarting(com.android.server.SystemService.TargetUser)}
+         * call.
+         */
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible);
+    }
+
+    /**
      * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to set
      * restrictions enforced by the user.
      *
@@ -326,29 +356,28 @@
     public abstract @Nullable UserProperties getUserProperties(@UserIdInt int userId);
 
     /**
-     * Assigns a user to a display.
-     *
-     * <p>On most devices this call will be a no-op, but it will be used on devices that support
-     * multiple users on multiple displays (like automotives with passenger displays).
-     *
-     * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to
-     * check it. In fact, one of the intended clients for this method is
-     * {@code DisplayManagerService}, which will call it when a virtual display is created (another
-     * client is {@code UserController}, which will call it when a user is started).
-     *
-     */
-    public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId);
-
-    /**
-     * Unassigns a user from its current display.
-     *
-     * <p>On most devices this call will be a no-op, but it will be used on devices that support
-     * multiple users on multiple displays (like automotives with passenger displays).
+     * Assigns a user to a display when it's starting, returning whether the assignment succeeded
+     * and the user is {@link UserManager#isUserVisible() visible}.
      *
      * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
-     * is stopped) and {@code DisplayManagerService} (when a virtual display is destroyed).
+     * is started). If other clients (like {@code CarService} need to explicitly change the user /
+     * display assignment, we'll need to provide other APIs.
+     *
+     * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to
+     * pass a valid display id.
      */
-    public abstract void unassignUserFromDisplay(@UserIdInt int userId);
+    public abstract @UserAssignmentResult int assignUserToDisplayOnStart(@UserIdInt int userId,
+            @UserIdInt int profileGroupId,
+            boolean foreground, int displayId);
+
+    /**
+     * Unassigns a user from its current display when it's stopping.
+     *
+     * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user
+     * is stopped). If other clients (like {@code CarService} need to explicitly change the user /
+     * display assignment, we'll need to provide other APIs.
+     */
+    public abstract void unassignUserFromDisplayOnStop(@UserIdInt int userId);
 
     /**
      * Returns {@code true} if the user is visible (as defined by
@@ -390,4 +419,26 @@
      * would make such call).
      */
     public abstract @UserIdInt int getUserAssignedToDisplay(int displayId);
+
+    /**
+     * Gets the user-friendly representation of the {@code result} of a
+     * {@link #assignUserToDisplayOnStart(int, int, boolean, int)} call.
+     */
+    public static String userAssignmentResultToString(@UserAssignmentResult int result) {
+        return DebugUtils.constantToString(UserManagerInternal.class, PREFIX_USER_ASSIGNMENT_RESULT,
+                result);
+    }
+
+    /** Adds a {@link UserVisibilityListener}. */
+    public abstract void addUserVisibilityListener(UserVisibilityListener listener);
+
+    /** Removes a {@link UserVisibilityListener}. */
+    public abstract void removeUserVisibilityListener(UserVisibilityListener listener);
+
+    /** TODO(b/244333150): temporary method until UserVisibilityMediator handles that logic */
+    public abstract void onUserVisibilityChanged(@UserIdInt int userId, boolean visible);
+
+    /** Return the integer types of the given user IDs. Only used for reporting metrics to statsd.
+     */
+    public abstract int[] getUserTypesForStatsd(@UserIdInt int[] userIds);
 }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 60f2478..9f84ab0 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -18,7 +18,9 @@
 
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY;
 import static android.os.UserManager.DISALLOW_USER_SWITCH;
+import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY;
 
 import android.Manifest;
 import android.accounts.Account;
@@ -52,6 +54,7 @@
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserPackage;
 import android.content.pm.UserProperties;
 import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.content.res.Configuration;
@@ -94,6 +97,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.EventLog;
 import android.util.IndentingPrintWriter;
 import android.util.IntArray;
 import android.util.Slog;
@@ -103,10 +107,7 @@
 import android.util.StatsEvent;
 import android.util.TimeUtils;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
-import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -119,13 +120,17 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
 import com.android.server.SystemService;
+import com.android.server.am.EventLogTags;
 import com.android.server.am.UserState;
 import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
 import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
+import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
 import com.android.server.storage.DeviceStorageMonitorInternal;
 import com.android.server.utils.Slogf;
 import com.android.server.utils.TimingsTraceAndSlog;
@@ -260,7 +265,7 @@
     @VisibleForTesting
     static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100;
 
-    private static final int USER_VERSION = 10;
+    private static final int USER_VERSION = 11;
 
     private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
 
@@ -314,7 +319,7 @@
     @VisibleForTesting
     static class UserData {
         // Basic user information and properties
-        UserInfo info;
+        @NonNull UserInfo info;
         // Account name used when there is a strong association between a user and an account
         String account;
         // Account information for seeding into a newly created user. This could also be
@@ -503,6 +508,10 @@
     @GuardedBy("mUserLifecycleListeners")
     private final ArrayList<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>();
 
+    // TODO(b/244333150): temporary array, should belong to UserVisibilityMediator
+    @GuardedBy("mUserVisibilityListeners")
+    private final ArrayList<UserVisibilityListener> mUserVisibilityListeners = new ArrayList<>();
+
     private final LockPatternUtils mLockPatternUtils;
 
     private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK =
@@ -625,14 +634,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 = new UserVisibilityMediator();
 
     private static UserManagerService sInstance;
 
@@ -708,8 +710,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 +721,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 +757,6 @@
         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;
-        }
     }
 
     void systemReady() {
@@ -1540,10 +1533,9 @@
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserProperties");
         final UserProperties origProperties = getUserPropertiesInternal(userId);
         if (origProperties != null) {
-            int callingUid = Binder.getCallingUid();
-            boolean exposeAllFields = callingUid == Process.SYSTEM_UID;
-            boolean hasManage = hasPermissionGranted(Manifest.permission.MANAGE_USERS, callingUid);
-            boolean hasQuery = hasPermissionGranted(Manifest.permission.QUERY_USERS, callingUid);
+            boolean exposeAllFields = Binder.getCallingUid() == Process.SYSTEM_UID;
+            boolean hasManage = hasManageUsersPermission();
+            boolean hasQuery = hasQueryUsersPermission();
             return new UserProperties(origProperties, exposeAllFields, hasManage, hasQuery);
         }
         // A non-existent or partial user will reach here.
@@ -1653,7 +1645,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();
@@ -1730,11 +1723,6 @@
         return userId == getCurrentUserId();
     }
 
-    @VisibleForTesting
-    boolean isUsersOnSecondaryDisplaysEnabled() {
-        return mUsersOnSecondaryDisplaysEnabled;
-    }
-
     @Override
     public boolean isUserVisible(@UserIdInt int userId) {
         int callingUserId = UserHandle.getCallingUserId();
@@ -1745,69 +1733,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);
     }
 
     /**
@@ -1851,58 +1777,13 @@
         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
-    public List<UserHandle> getVisibleUsers() {
+    public int[] getVisibleUsers() {
         if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) {
             throw new SecurityException("Caller needs MANAGE_USERS or INTERACT_ACROSS_USERS "
                     + "permission to get list of visible users");
@@ -1910,18 +1791,19 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             // TODO(b/2399825580): refactor into UserDisplayAssigner
+            IntArray visibleUsers;
             synchronized (mUsersLock) {
                 int usersSize = mUsers.size();
-                ArrayList<UserHandle> visibleUsers = new ArrayList<>(usersSize);
+                visibleUsers = new IntArray();
                 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)) {
-                        visibleUsers.add(UserHandle.of(ui.id));
+                            && mUserVisibilityMediator.isUserVisible(ui.id)) {
+                        visibleUsers.add(ui.id);
                     }
                 }
-                return visibleUsers;
             }
+            return visibleUsers.toArray();
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -2279,26 +2161,31 @@
     @Override
     public void setUserName(@UserIdInt int userId, String name) {
         checkManageUsersPermission("rename users");
-        boolean changed = false;
         synchronized (mPackagesLock) {
             UserData userData = getUserDataNoChecks(userId);
             if (userData == null || userData.info.partial) {
-                Slog.w(LOG_TAG, "setUserName: unknown user #" + userId);
+                Slogf.w(LOG_TAG, "setUserName: unknown user #%d", userId);
                 return;
             }
-            if (name != null && !name.equals(userData.info.name)) {
-                userData.info.name = name;
-                writeUserLP(userData);
-                changed = true;
+            if (Objects.equals(name, userData.info.name)) {
+                Slogf.i(LOG_TAG, "setUserName: ignoring for user #%d as it didn't change (%s)",
+                        userId, getRedacted(name));
+                return;
             }
+            if (name == null) {
+                Slogf.i(LOG_TAG, "setUserName: resetting name of user #%d", userId);
+            } else {
+                Slogf.i(LOG_TAG, "setUserName: setting name of user #%d to %s", userId,
+                        getRedacted(name));
+            }
+            userData.info.name = name;
+            writeUserLP(userData);
         }
-        if (changed) {
-            final long ident = Binder.clearCallingIdentity();
-            try {
-                sendUserInfoChangedBroadcast(userId);
-            } finally {
-                Binder.restoreCallingIdentity(ident);
-            }
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            sendUserInfoChangedBroadcast(userId);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -2633,6 +2520,9 @@
     /** @return a specific user restriction that's in effect currently. */
     @Override
     public boolean hasUserRestriction(String restrictionKey, @UserIdInt int userId) {
+        if (!userExists(userId)) {
+            return false;
+        }
         checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasUserRestriction");
         return mLocalService.hasUserRestriction(restrictionKey, userId);
     }
@@ -2925,7 +2815,8 @@
         synchronized (mUsersLock) {
             count = getAliveUsersExcludingGuestsCountLU();
         }
-        return count >= UserManager.getMaxSupportedUsers();
+        return count >= UserManager.getMaxSupportedUsers()
+                && !isCreationOverrideEnabled();
     }
 
     /**
@@ -2935,15 +2826,16 @@
      * <p>For checking whether more profiles can be added to a particular parent use
      * {@link #canAddMoreProfilesToUser}.
      */
-    private boolean canAddMoreUsersOfType(UserTypeDetails userTypeDetails) {
-        if (!userTypeDetails.isEnabled()) {
+    private boolean canAddMoreUsersOfType(@NonNull UserTypeDetails userTypeDetails) {
+        if (!isUserTypeEnabled(userTypeDetails)) {
             return false;
         }
         final int max = userTypeDetails.getMaxAllowed();
         if (max == UserTypeDetails.UNLIMITED_NUMBER_OF_USERS) {
             return true; // Indicates that there is no max.
         }
-        return getNumberOfUsersOfType(userTypeDetails.getName()) < max;
+        return getNumberOfUsersOfType(userTypeDetails.getName()) < max
+                || isCreationOverrideEnabled();
     }
 
     /**
@@ -2954,7 +2846,7 @@
     public int getRemainingCreatableUserCount(String userType) {
         checkQueryOrCreateUsersPermission("get the remaining number of users that can be added.");
         final UserTypeDetails type = mUserTypes.get(userType);
-        if (type == null || !type.isEnabled()) {
+        if (type == null || !isUserTypeEnabled(type)) {
             return 0;
         }
         synchronized (mUsersLock) {
@@ -3028,7 +2920,21 @@
     public boolean isUserTypeEnabled(String userType) {
         checkCreateUsersPermission("check if user type is enabled.");
         final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
-        return userTypeDetails != null && userTypeDetails.isEnabled();
+        return userTypeDetails != null && isUserTypeEnabled(userTypeDetails);
+    }
+
+    /** Returns whether the creation of users of the given user type is enabled on this device. */
+    private boolean isUserTypeEnabled(@NonNull UserTypeDetails userTypeDetails) {
+        return userTypeDetails.isEnabled() || isCreationOverrideEnabled();
+    }
+
+    /**
+     * Returns whether to almost-always allow creating users even beyond their limit or if disabled.
+     * For Debug builds only.
+     */
+    private boolean isCreationOverrideEnabled() {
+        return Build.isDebuggable()
+                && SystemProperties.getBoolean(DEV_CREATE_OVERRIDE_PROPERTY, false);
     }
 
     @Override
@@ -3041,7 +2947,8 @@
     @Override
     public boolean canAddMoreProfilesToUser(String userType, @UserIdInt int userId,
             boolean allowedToRemoveOne) {
-        return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne);
+        return 0 < getRemainingCreatableProfileCount(userType, userId, allowedToRemoveOne)
+                || isCreationOverrideEnabled();
     }
 
     @Override
@@ -3059,7 +2966,7 @@
         checkQueryOrCreateUsersPermission(
                 "get the remaining number of profiles that can be added to the given user.");
         final UserTypeDetails type = mUserTypes.get(userType);
-        if (type == null || !type.isEnabled()) {
+        if (type == null || !isUserTypeEnabled(type)) {
             return 0;
         }
         // Managed profiles have their own specific rules.
@@ -3387,11 +3294,39 @@
         }
     }
 
+    /** Checks whether the device is currently in headless system user mode (for any reason). */
+    @Override
+    public boolean isHeadlessSystemUserMode() {
+        synchronized (mUsersLock) {
+            final UserData systemUserData = mUsers.get(UserHandle.USER_SYSTEM);
+            return !systemUserData.info.isFull();
+        }
+    }
+
     /**
-     * Checks whether the device is really headless system user mode, ignoring system user mode
-     * emulation.
+     * Checks whether the default state of the device is headless system user mode, i.e. what the
+     * mode would be if we did a fresh factory reset.
+     * If the mode is  being emulated (via SYSTEM_USER_MODE_EMULATION_PROPERTY) then that will be
+     * returned instead.
+     * Note that, even in the absence of emulation, a device might deviate from the current default
+     * due to an OTA changing the default (which won't change the already-decided mode).
      */
-    private boolean isReallyHeadlessSystemUserMode() {
+    private boolean isDefaultHeadlessSystemUserMode() {
+        if (!Build.isDebuggable()) {
+            return RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER;
+        }
+
+        final String emulatedValue = SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY);
+        if (!TextUtils.isEmpty(emulatedValue)) {
+            if (UserManager.SYSTEM_USER_MODE_EMULATION_HEADLESS.equals(emulatedValue)) return true;
+            if (UserManager.SYSTEM_USER_MODE_EMULATION_FULL.equals(emulatedValue)) return false;
+            if (!UserManager.SYSTEM_USER_MODE_EMULATION_DEFAULT.equals(emulatedValue)) {
+                Slogf.e(LOG_TAG, "isDefaultHeadlessSystemUserMode(): ignoring invalid valued of "
+                                + "property %s: %s",
+                        SYSTEM_USER_MODE_EMULATION_PROPERTY, emulatedValue);
+            }
+        }
+
         return RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER;
     }
 
@@ -3403,30 +3338,11 @@
         if (!Build.isDebuggable()) {
             return;
         }
-
-        final String emulatedValue = SystemProperties
-                .get(UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY);
-        if (TextUtils.isEmpty(emulatedValue)) {
+        if (TextUtils.isEmpty(SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY))) {
             return;
         }
 
-        final boolean newHeadlessSystemUserMode;
-        switch (emulatedValue) {
-            case UserManager.SYSTEM_USER_MODE_EMULATION_FULL:
-                newHeadlessSystemUserMode = false;
-                break;
-            case UserManager.SYSTEM_USER_MODE_EMULATION_HEADLESS:
-                newHeadlessSystemUserMode = true;
-                break;
-            case UserManager.SYSTEM_USER_MODE_EMULATION_DEFAULT:
-                newHeadlessSystemUserMode = isReallyHeadlessSystemUserMode();
-                break;
-            default:
-                Slogf.wtf(LOG_TAG, "emulateSystemUserModeIfNeeded(): ignoring invalid valued of "
-                        + "property %s: %s", UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY,
-                        emulatedValue);
-                return;
-        }
+        final boolean newHeadlessSystemUserMode = isDefaultHeadlessSystemUserMode();
 
         // Update system user type
         synchronized (mPackagesLock) {
@@ -3439,6 +3355,7 @@
                 final int oldFlags = systemUserData.info.flags;
                 final int newFlags;
                 final String newUserType;
+                // TODO(b/256624031): Also handle FLAG_MAIN
                 if (newHeadlessSystemUserMode) {
                     newUserType = UserManager.USER_TYPE_SYSTEM_HEADLESS;
                     newFlags = oldFlags & ~UserInfo.FLAG_FULL;
@@ -3462,7 +3379,7 @@
             }
         }
 
-        // Update emulated mode, which will used to triger an update on user packages
+        // Update emulated mode, which will used to trigger an update on user packages
         mUpdatingSystemUserMode = true;
     }
 
@@ -3652,7 +3569,11 @@
             synchronized (mUsersLock) {
                 UserData userData = mUsers.get(UserHandle.USER_SYSTEM);
                 userData.info.flags |= UserInfo.FLAG_SYSTEM;
-                if (!UserManager.isHeadlessSystemUserMode()) {
+                // We assume that isDefaultHeadlessSystemUserMode() does not change during the OTA
+                // from userVersion < 8 since it is documented that pre-R devices do not support its
+                // modification. Therefore, its current value should be the same as the pre-update
+                // version.
+                if (!isDefaultHeadlessSystemUserMode()) {
                     userData.info.flags |= UserInfo.FLAG_FULL;
                 }
                 userIdsToWrite.add(userData.info.id);
@@ -3727,6 +3648,22 @@
             userVersion = 10;
         }
 
+        if (userVersion < 11) {
+            // Add FLAG_MAIN
+            if (isHeadlessSystemUserMode()) {
+                final UserInfo earliestCreatedUser = getEarliestCreatedFullUser();
+                earliestCreatedUser.flags |= UserInfo.FLAG_MAIN;
+                userIdsToWrite.add(earliestCreatedUser.id);
+            } else {
+                synchronized (mUsersLock) {
+                    final UserData userData = mUsers.get(UserHandle.USER_SYSTEM);
+                    userData.info.flags |= UserInfo.FLAG_MAIN;
+                    userIdsToWrite.add(userData.info.id);
+                }
+            }
+            userVersion = 11;
+        }
+
         // Reminder: If you add another upgrade, make sure to increment USER_VERSION too.
 
         // Done with userVersion changes, moving on to deal with userTypeVersion upgrades
@@ -3856,12 +3793,27 @@
         userInfo.profileBadge = getFreeProfileBadgeLU(userInfo.profileGroupId, userInfo.userType);
     }
 
+    private UserInfo getEarliestCreatedFullUser() {
+        final List<UserInfo> users = getUsersInternal(true, true, true);
+        UserInfo earliestUser = users.get(0);
+        long earliestCreationTime = earliestUser.creationTime;
+        for (int i = 0; i < users.size(); i++) {
+            final UserInfo info = users.get(i);
+            if (info.isFull() && info.isAdmin() && info.creationTime > 0
+                    && info.creationTime < earliestCreationTime) {
+                earliestCreationTime = info.creationTime;
+                earliestUser = info;
+            }
+        }
+        return earliestUser;
+    }
+
     @GuardedBy({"mPackagesLock"})
     private void fallbackToSingleUserLP() {
         int flags = UserInfo.FLAG_SYSTEM | UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN
                 | UserInfo.FLAG_PRIMARY;
         // Create the system user
-        String systemUserType = UserManager.isHeadlessSystemUserMode()
+        String systemUserType = isDefaultHeadlessSystemUserMode()
                 ? UserManager.USER_TYPE_SYSTEM_HEADLESS
                 : UserManager.USER_TYPE_FULL_SYSTEM;
         flags |= mUserTypes.get(systemUserType).getDefaultUserInfoFlags();
@@ -4505,7 +4457,7 @@
                     + ") indicated SYSTEM user, which cannot be created.");
             return null;
         }
-        if (!userTypeDetails.isEnabled()) {
+        if (!isUserTypeEnabled(userTypeDetails)) {
             throwCheckedUserOperationException(
                     "Cannot add a user of disabled type " + userType + ".",
                     UserManager.USER_OPERATION_ERROR_MAX_USERS);
@@ -4577,27 +4529,12 @@
                                     + " for user " + parentId,
                             UserManager.USER_OPERATION_ERROR_MAX_USERS);
                 }
-                // In legacy mode, restricted profile's parent can only be the owner user
-                if (isRestricted && !UserManager.isSplitSystemUser()
-                        && (parentId != UserHandle.USER_SYSTEM)) {
+                if (isRestricted && (parentId != UserHandle.USER_SYSTEM)
+                        && !isCreationOverrideEnabled()) {
                     throwCheckedUserOperationException(
-                            "Cannot add restricted profile - parent user must be owner",
+                            "Cannot add restricted profile - parent user must be system",
                             UserManager.USER_OPERATION_ERROR_UNKNOWN);
                 }
-                if (isRestricted && UserManager.isSplitSystemUser()) {
-                    if (parent == null) {
-                        throwCheckedUserOperationException(
-                                "Cannot add restricted profile - parent user must be specified",
-                                UserManager.USER_OPERATION_ERROR_UNKNOWN);
-                    }
-                    if (!parent.info.canHaveProfile()) {
-                        throwCheckedUserOperationException(
-                                "Cannot add restricted profile - profiles cannot be created for "
-                                        + "the specified parent user id "
-                                        + parentId,
-                                UserManager.USER_OPERATION_ERROR_UNKNOWN);
-                    }
-                }
 
                 userId = getNextAvailableId();
                 Slog.i(LOG_TAG, "Creating user " + userId + " of type " + userType);
@@ -4659,9 +4596,12 @@
             storage.createUserKey(userId, userInfo.serialNumber, userInfo.isEphemeral());
             t.traceEnd();
 
+            // Only prepare DE storage here.  CE storage will be prepared later, when the user is
+            // unlocked.  We do this to ensure that CE storage isn't prepared before the CE key is
+            // saved to disk.  This also matches what is done for user 0.
             t.traceBegin("prepareUserData");
             mUserDataPreparer.prepareUserData(userId, userInfo.serialNumber,
-                    StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+                    StorageManager.FLAG_STORAGE_DE);
             t.traceEnd();
 
             t.traceBegin("LSS.createNewUser");
@@ -5350,7 +5290,8 @@
                 Slog.w(LOG_TAG, "Unable to notify AppOpsService of removing user.", e);
             }
 
-            if (userData.info.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID) {
+            if (userData.info.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
+                    && userData.info.isProfile()) {
                 sendProfileRemovedBroadcast(userData.info.profileGroupId, userData.info.id,
                         userData.info.userType);
             }
@@ -5516,6 +5457,13 @@
 
     private void removeUserState(final @UserIdInt int userId) {
         Slog.i(LOG_TAG, "Removing user state of user " + userId);
+
+        // Cleanup lock settings.  This must happen before destroyUserKey(), since the user's DE
+        // storage must still be accessible for the lock settings state to be properly cleaned up.
+        mLockPatternUtils.removeUser(userId);
+
+        // Evict and destroy the user's CE and DE encryption keys.  At this point, the user's CE and
+        // DE storage is made inaccessible, except to delete its contents.
         try {
             mContext.getSystemService(StorageManager.class).destroyUserKey(userId);
         } catch (IllegalStateException e) {
@@ -5523,9 +5471,6 @@
             Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
         }
 
-        // Cleanup lock settings
-        mLockPatternUtils.removeUser(userId);
-
         // Cleanup package manager settings
         mPm.cleanUpUser(this, userId);
 
@@ -5961,6 +5906,7 @@
                 Slog.d(LOG_TAG, "updateUserIds(): userIds= " + Arrays.toString(mUserIds)
                         + " includingPreCreated=" + Arrays.toString(mUserIdsIncludingPreCreated));
             }
+            UserPackage.setValidUserIds(mUserIds);
         }
     }
 
@@ -6102,6 +6048,11 @@
         return RESTRICTIONS_FILE_PREFIX + packageName + XML_SUFFIX;
     }
 
+    @Nullable
+    private static String getRedacted(@Nullable String string) {
+        return string == null ? null : string.length() + "_chars";
+    }
+
     @Override
     public void setSeedAccountData(@UserIdInt int userId, String accountName, String accountType,
             PersistableBundle accountOptions, boolean persist) {
@@ -6232,9 +6183,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, args);
+                    return;
+            }
         }
 
         final int currentUserId = getCurrentUserId();
@@ -6297,18 +6254,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, args);
+        pw.println();
 
         // Dump some capabilities
         pw.println();
@@ -6319,9 +6267,12 @@
                 com.android.internal.R.bool.config_guestUserEphemeral));
         pw.println("  Force ephemeral users: " + mForceEphemeralUsers);
         pw.println("  Is split-system user: " + UserManager.isSplitSystemUser());
-        final boolean isHeadlessSystemUserMode = UserManager.isHeadlessSystemUserMode();
+        final boolean isHeadlessSystemUserMode = isHeadlessSystemUserMode();
         pw.println("  Is headless-system mode: " + isHeadlessSystemUserMode);
-        if (isHeadlessSystemUserMode != isReallyHeadlessSystemUserMode()) {
+        if (isHeadlessSystemUserMode != RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER) {
+            pw.println("  (differs from the current default build value)");
+        }
+        if (!TextUtils.isEmpty(SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY))) {
             pw.println("  (emulated by 'cmd user set-system-user-mode-emulation')");
             if (mUpdatingSystemUserMode) {
                 pw.println("  (and being updated after boot)");
@@ -6341,6 +6292,9 @@
         synchronized (mUserLifecycleListeners) {
             pw.println("  user lifecycle events: " + mUserLifecycleListeners.size());
         }
+        synchronized (mUserVisibilityListeners) {
+            pw.println("  user visibility events: " + mUserVisibilityListeners.size());
+        }
 
         // Dump UserTypes
         pw.println();
@@ -6880,136 +6834,87 @@
         }
 
         @Override
-        public void assignUserToDisplay(int userId, int displayId) {
-            if (DBG_MUMD) {
-                Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
-            }
+        public int assignUserToDisplayOnStart(@UserIdInt int userId, @UserIdInt int profileGroupId,
+                boolean foreground, int displayId) {
+            return mUserVisibilityMediator.startUser(userId, profileGroupId, foreground,
+                    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;
-                }
-            }
+        @Override
+        public void unassignUserFromDisplayOnStop(@UserIdInt int userId) {
+            mUserVisibilityMediator.stopUser(userId);
+        }
 
-            if (!mUsersOnSecondaryDisplaysEnabled) {
-                throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", "
-                        + displayId + ") called on device that doesn't support multiple "
-                        + "users on multiple displays");
-            }
+        @Override
+        public boolean isUserVisible(@UserIdInt int userId) {
+            return mUserVisibilityMediator.isUserVisible(userId);
+        }
 
-            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);
+        @Override
+        public boolean isUserVisible(@UserIdInt int userId, int displayId) {
+            return mUserVisibilityMediator.isUserVisible(userId, displayId);
+        }
 
-            int currentUserId = getCurrentUserId();
-            Preconditions.checkArgument(userId != currentUserId,
-                    "Cannot assign current user (%d) to other displays", currentUserId);
+        @Override
+        public int getDisplayAssignedToUser(@UserIdInt int userId) {
+            return mUserVisibilityMediator.getDisplayAssignedToUser(userId);
+        }
 
-            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;
-                }
+        @Override
+        public @UserIdInt int getUserAssignedToDisplay(int displayId) {
+            return mUserVisibilityMediator.getUserAssignedToDisplay(displayId);
+        }
 
-                // 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);
+        @Override
+        public void addUserVisibilityListener(UserVisibilityListener listener) {
+            synchronized (mUserVisibilityListeners) {
+                mUserVisibilityListeners.add(listener);
             }
         }
 
         @Override
-        public void unassignUserFromDisplay(@UserIdInt int userId) {
-            if (DBG_MUMD) {
-                Slogf.d(LOG_TAG, "unassignUserFromDisplay(%d)", userId);
+        public void removeUserVisibilityListener(UserVisibilityListener listener) {
+            synchronized (mUserVisibilityListeners) {
+                mUserVisibilityListeners.remove(listener);
             }
-            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");
+        }
+
+        @Override
+        public void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            EventLog.writeEvent(EventLogTags.UM_USER_VISIBILITY_CHANGED, userId, visible ? 1 : 0);
+            mHandler.post(() -> {
+                UserVisibilityListener[] listeners;
+                synchronized (mUserVisibilityListeners) {
+                    listeners = new UserVisibilityListener[mUserVisibilityListeners.size()];
+                    mUserVisibilityListeners.toArray(listeners);
                 }
-                return;
-            }
-
-            synchronized (mUsersOnSecondaryDisplays) {
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId,
-                            mUsersOnSecondaryDisplays);
+                for (UserVisibilityListener listener : listeners) {
+                    listener.onUserVisibilityChanged(userId, visible);
                 }
-                mUsersOnSecondaryDisplays.delete(userId);
+            });
+        }
+
+        @Override
+        public int[] getUserTypesForStatsd(@UserIdInt int[] userIds) {
+            if (userIds == null) {
+                return null;
             }
-        }
-
-        @Override
-        public boolean isUserVisible(int userId) {
-            return isUserVisibleUnchecked(userId);
-        }
-
-        @Override
-        public boolean isUserVisible(int userId, int displayId) {
-            return isUserVisibleOnDisplay(userId, displayId);
-        }
-
-        @Override
-        public int getDisplayAssignedToUser(int userId) {
-            return UserManagerService.this.getDisplayAssignedToUser(userId);
-        }
-
-        @Override
-        public int getUserAssignedToDisplay(int displayId) {
-            return UserManagerService.this.getUserAssignedToDisplay(displayId);
+            final int[] userTypes = new int[userIds.length];
+            for (int i = 0; i < userTypes.length; i++) {
+                final UserInfo userInfo = getUserInfo(userIds[i]);
+                if (userInfo == null) {
+                    // Not possible because the input user ids should all be valid
+                    userTypes[i] = UserManager.getUserTypeForStatsd("");
+                } else {
+                    userTypes[i] = UserManager.getUserTypeForStatsd(userInfo.userType);
+                }
+            }
+            return userTypes;
         }
     } // class LocalService
 
+
+
     /**
      * Check if user has restrictions
      * @param restriction restrictions to check
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 016c1cb..27d74d5 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -40,11 +40,11 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 import com.android.server.LocalServices;
 
@@ -147,7 +147,9 @@
             UserManager.DISALLOW_WIFI_TETHERING,
             UserManager.DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI,
             UserManager.DISALLOW_WIFI_DIRECT,
-            UserManager.DISALLOW_ADD_WIFI_CONFIG
+            UserManager.DISALLOW_ADD_WIFI_CONFIG,
+            UserManager.DISALLOW_CELLULAR_2G,
+            UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
     });
 
     public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet(
@@ -195,7 +197,9 @@
             UserManager.DISALLOW_CHANGE_WIFI_STATE,
             UserManager.DISALLOW_WIFI_TETHERING,
             UserManager.DISALLOW_WIFI_DIRECT,
-            UserManager.DISALLOW_ADD_WIFI_CONFIG
+            UserManager.DISALLOW_ADD_WIFI_CONFIG,
+            UserManager.DISALLOW_CELLULAR_2G,
+            UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
     );
 
     /**
@@ -234,7 +238,9 @@
                     UserManager.DISALLOW_CHANGE_WIFI_STATE,
                     UserManager.DISALLOW_WIFI_TETHERING,
                     UserManager.DISALLOW_WIFI_DIRECT,
-                    UserManager.DISALLOW_ADD_WIFI_CONFIG
+                    UserManager.DISALLOW_ADD_WIFI_CONFIG,
+                    UserManager.DISALLOW_CELLULAR_2G,
+                    UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO
     );
 
     /**
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index b98d20e..8fb5773 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -126,9 +126,13 @@
                 .setCrossProfileIntentFilterAccessControl(
                         CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM)
                 .setIsCredentialSharableWithParent(true)
+                .setDefaultCrossProfileIntentFilters(getDefaultCloneCrossProfileIntentFilter())
                 .setDefaultUserProperties(new UserProperties.Builder()
                         .setStartWithParent(true)
-                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT));
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
+                        .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
+                        .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
+                        .setUseParentsContacts(true));
     }
 
     /**
@@ -163,7 +167,8 @@
                 .setIsCredentialSharableWithParent(true)
                 .setDefaultUserProperties(new UserProperties.Builder()
                         .setStartWithParent(true)
-                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE));
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
+                        .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE));
     }
 
     /**
@@ -257,7 +262,8 @@
     private static UserTypeDetails.Builder getDefaultTypeFullSystem() {
         return new UserTypeDetails.Builder()
                 .setName(USER_TYPE_FULL_SYSTEM)
-                .setBaseType(FLAG_SYSTEM | FLAG_FULL);
+                .setBaseType(FLAG_SYSTEM | FLAG_FULL)
+                .setDefaultUserInfoPropertyFlags(UserInfo.FLAG_MAIN);
     }
 
     /**
@@ -307,6 +313,10 @@
         return DefaultCrossProfileIntentFiltersUtils.getDefaultManagedProfileFilters();
     }
 
+    private static List<DefaultCrossProfileIntentFilter> getDefaultCloneCrossProfileIntentFilter() {
+        return DefaultCrossProfileIntentFiltersUtils.getDefaultCloneProfileFilters();
+    }
+
     /**
      * Reads the given xml parser to obtain device user-type customization, and updates the given
      * map of {@link UserTypeDetails.Builder}s accordingly.
@@ -388,6 +398,7 @@
                 }
 
                 setIntAttribute(parser, "enabled", builder::setEnabled);
+                setIntAttribute(parser, "max-allowed", builder::setMaxAllowed);
 
                 // Process child elements.
                 final int depth = parser.getDepth();
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..cbf7dfe
--- /dev/null
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -0,0 +1,517 @@
+/*
+ * 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.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
+import static android.os.UserHandle.USER_NULL;
+import static android.os.UserHandle.USER_SYSTEM;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Dumpable;
+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.pm.UserManagerInternal.UserAssignmentResult;
+import com.android.server.utils.Slogf;
+
+import java.io.PrintWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 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 implements Dumpable {
+
+    private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
+
+    private static final String TAG = UserVisibilityMediator.class.getSimpleName();
+
+    // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices
+    @VisibleForTesting
+    static final int INITIAL_CURRENT_USER_ID = USER_SYSTEM;
+
+    private final Object mLock = new Object();
+
+    private final boolean mUsersOnSecondaryDisplaysEnabled;
+
+    @UserIdInt
+    @GuardedBy("mLock")
+    private int mCurrentUserId = INITIAL_CURRENT_USER_ID;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();
+
+    /**
+     * Mapping from each started user to its profile group.
+     */
+    @GuardedBy("mLock")
+    private final SparseIntArray mStartedProfileGroupIds = new SparseIntArray();
+
+    UserVisibilityMediator() {
+        this(UserManager.isUsersOnSecondaryDisplaysEnabled());
+    }
+
+    @VisibleForTesting
+    UserVisibilityMediator(boolean usersOnSecondaryDisplaysEnabled) {
+        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
+    }
+
+    /**
+     * See {@link UserManagerInternal#assignUserToDisplayOnStart(int, int, boolean, int)}.
+     */
+    public @UserAssignmentResult int startUser(@UserIdInt int userId, @UserIdInt int profileGroupId,
+            boolean foreground, int displayId) {
+        // TODO(b/244644281): this method need to perform 4 actions:
+        //
+        // 1. Check if the user can be started given the provided arguments
+        // 2. If it can, decide whether it's visible or not (which is the return value)
+        // 3. Update the current user / profiles state
+        // 4. Update the users on secondary display state (if applicable)
+        //
+        // Ideally, they should be done "atomically" (i.e, only changing state while holding the
+        // mLock), but the initial implementation is just calling the existing methods, as the
+        // focus is to change the UserController startUser() workflow (so it relies on this class
+        // for the logic above).
+        //
+        // The next CL will refactor it (and the unit tests) to achieve that atomicity.
+        int result = startOnly(userId, profileGroupId, foreground, displayId);
+        if (result != USER_ASSIGNMENT_RESULT_FAILURE) {
+            assignUserToDisplay(userId, profileGroupId, displayId);
+        }
+        return result;
+    }
+
+    /**
+     * @deprecated - see comment inside {@link #startUser(int, int, boolean, int)}
+     */
+    @Deprecated
+    @VisibleForTesting
+    @UserAssignmentResult int startOnly(@UserIdInt int userId,
+            @UserIdInt int profileGroupId, boolean foreground, int displayId) {
+        int actualProfileGroupId = profileGroupId == NO_PROFILE_GROUP_ID
+                ? userId
+                : profileGroupId;
+        if (DBG) {
+            Slogf.d(TAG, "startUser(%d, %d, %b, %d): actualProfileGroupId=%d",
+                    userId, profileGroupId, foreground, displayId, actualProfileGroupId);
+        }
+        if (foreground && displayId != DEFAULT_DISPLAY) {
+            Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start foreground user on "
+                    + "secondary display", userId, actualProfileGroupId, foreground, displayId);
+            return USER_ASSIGNMENT_RESULT_FAILURE;
+        }
+
+        int visibility;
+        synchronized (mLock) {
+            if (isProfile(userId, actualProfileGroupId)) {
+                if (displayId != DEFAULT_DISPLAY) {
+                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user on "
+                            + "secondary display", userId, actualProfileGroupId, foreground,
+                            displayId);
+                    return USER_ASSIGNMENT_RESULT_FAILURE;
+                }
+                if (foreground) {
+                    Slogf.w(TAG, "startUser(%d, %d, %b, %d) failed: cannot start profile user in "
+                            + "foreground", userId, actualProfileGroupId, foreground, displayId);
+                    return USER_ASSIGNMENT_RESULT_FAILURE;
+                } else {
+                    boolean isParentRunning = mStartedProfileGroupIds
+                            .get(actualProfileGroupId) == actualProfileGroupId;
+                    if (DBG) {
+                        Slogf.d(TAG, "profile parent running: %b", isParentRunning);
+                    }
+                    visibility = isParentRunning
+                            ? USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE
+                            : USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+                }
+            } else if (foreground) {
+                mCurrentUserId = userId;
+                visibility = USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+            } else {
+                visibility = USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+            }
+            if (DBG) {
+                Slogf.d(TAG, "adding user / profile mapping (%d -> %d) and returning %s",
+                        userId, actualProfileGroupId, userAssignmentResultToString(visibility));
+            }
+            mStartedProfileGroupIds.put(userId, actualProfileGroupId);
+        }
+        return visibility;
+    }
+
+    /**
+     * @deprecated - see comment inside {@link #startUser(int, int, boolean, int)}
+     */
+    @Deprecated
+    @VisibleForTesting
+    void assignUserToDisplay(int userId, int profileGroupId, int displayId) {
+        if (DBG) {
+            Slogf.d(TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplaysEnabled=%b",
+                    userId, displayId, mUsersOnSecondaryDisplaysEnabled);
+        }
+
+        if (displayId == DEFAULT_DISPLAY
+                && (!mUsersOnSecondaryDisplaysEnabled || !isProfile(userId, profileGroupId))) {
+            // 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.
+            // But on MUMD devices, it profiles are only supported in the default display, so 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).
+            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(userId, profileGroupId)) {
+            // 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 = getStartedProfileGroupId(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;
+        }
+
+        synchronized (mLock) {
+            // 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#unassignUserFromDisplayOnStop(int)}.
+     */
+    public void stopUser(int userId) {
+        if (DBG) {
+            Slogf.d(TAG, "stopUser(%d)", userId);
+        }
+        synchronized (mLock) {
+            if (DBG) {
+                Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId,
+                        mStartedProfileGroupIds);
+            }
+            mStartedProfileGroupIds.delete(userId);
+
+            if (!mUsersOnSecondaryDisplaysEnabled) {
+                // Don't need to do update mUsersOnSecondaryDisplays because methods (such as
+                // isUserVisible()) already know that the current user (and their profiles) is
+                // assigned to the default display.
+                return;
+            }
+            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 (!isStartedProfile(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("UserVisibilityMediator");
+        ipw.increaseIndent();
+
+        synchronized (mLock) {
+            ipw.print("Current user id: ");
+            ipw.println(mCurrentUserId);
+
+            dumpIntArray(ipw, mStartedProfileGroupIds, "started user / profile group", "u", "pg");
+
+            ipw.print("Supports background users on secondary displays: ");
+            ipw.println(mUsersOnSecondaryDisplaysEnabled);
+
+            if (mUsersOnSecondaryDisplaysEnabled) {
+                dumpIntArray(ipw, mUsersOnSecondaryDisplays, "background user / secondary display",
+                        "u", "d");
+            }
+        }
+
+        ipw.decreaseIndent();
+    }
+
+    private static void dumpIntArray(IndentingPrintWriter ipw, SparseIntArray array,
+            String arrayDescription, String keyName, String valueName) {
+        ipw.print("Number of ");
+        ipw.print(arrayDescription);
+        ipw.print(" mappings: ");
+        ipw.println(array.size());
+        if (array.size() <= 0) {
+            return;
+        }
+        ipw.increaseIndent();
+        for (int i = 0; i < array.size(); i++) {
+            ipw.print(keyName); ipw.print(':');
+            ipw.print(array.keyAt(i));
+            ipw.print(" -> ");
+            ipw.print(valueName); ipw.print(':');
+            ipw.println(array.valueAt(i));
+        }
+        ipw.decreaseIndent();
+    }
+
+    @Override
+    public void dump(PrintWriter pw, String[] args) {
+        if (pw instanceof IndentingPrintWriter) {
+            dump((IndentingPrintWriter) pw);
+            return;
+        }
+        dump(new IndentingPrintWriter(pw));
+    }
+
+    @VisibleForTesting
+    Map<Integer, Integer> getUsersOnSecondaryDisplays() {
+        Map<Integer, Integer> map;
+        synchronized (mLock) {
+            int size = mUsersOnSecondaryDisplays.size();
+            map = new LinkedHashMap<>(size);
+            for (int i = 0; i < size; i++) {
+                map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
+            }
+        }
+        Slogf.v(TAG, "getUsersOnSecondaryDisplays(): returning %s", map);
+        return map;
+    }
+
+    // TODO(b/244644281): methods below are needed because some APIs use the current users (full and
+    // profiles) state to decide whether a user is visible or not. If we decide to always store that
+    // info into intermediate maps, we should remove them.
+
+    @VisibleForTesting
+    @UserIdInt int getCurrentUserId() {
+        synchronized (mLock) {
+            return mCurrentUserId;
+        }
+    }
+
+    @VisibleForTesting
+    boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            // Special case as NO_PROFILE_GROUP_ID == USER_NULL
+            if (userId == USER_NULL || mCurrentUserId == USER_NULL) {
+                return false;
+            }
+            if (mCurrentUserId == userId) {
+                return true;
+            }
+            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID) == mCurrentUserId;
+        }
+    }
+
+    private static boolean isProfile(@UserIdInt int userId, @UserIdInt int profileGroupId) {
+        return profileGroupId != NO_PROFILE_GROUP_ID && profileGroupId != userId;
+    }
+
+    @VisibleForTesting
+    boolean isStartedUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mStartedProfileGroupIds.get(userId,
+                    INITIAL_CURRENT_USER_ID) != INITIAL_CURRENT_USER_ID;
+        }
+    }
+
+    @VisibleForTesting
+    boolean isStartedProfile(@UserIdInt int userId) {
+        int profileGroupId;
+        synchronized (mLock) {
+            profileGroupId = mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+        }
+        return isProfile(userId, profileGroupId);
+    }
+
+    @VisibleForTesting
+    @UserIdInt int getStartedProfileGroupId(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mStartedProfileGroupIds.get(userId, NO_PROFILE_GROUP_ID);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 47a3705..415ddd3 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -707,7 +707,7 @@
 
     private List<ComponentName> matchVerifiers(PackageInfoLite pkgInfo,
             List<ResolveInfo> receivers, final PackageVerificationState verificationState) {
-        if (pkgInfo.verifiers.length == 0) {
+        if (pkgInfo.verifiers == null || pkgInfo.verifiers.length == 0) {
             return null;
         }
 
diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
index 37f7ac2..0bdd980 100644
--- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java
+++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
@@ -52,6 +52,7 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.Installer;
 import com.android.server.pm.Installer.InstallerException;
+import com.android.server.pm.PackageManagerService;
 import com.android.server.pm.PackageManagerServiceCompilerMapping;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -724,6 +725,13 @@
         @Override
         public PackageOptimizationInfo getPackageOptimizationInfo(
                 ApplicationInfo info, String abi, String activityName) {
+            if (info.packageName.equals(PackageManagerService.PLATFORM_PACKAGE_NAME)) {
+                // PackageManagerService.PLATFORM_PACKAGE_NAME in this context means that the
+                // activity is defined in bootclasspath. Currently, we don't have an API to get the
+                // correct optimization info.
+                return PackageOptimizationInfo.createWithNoInfo();
+            }
+
             String compilationReason;
             String compilationFilter;
             try {
diff --git a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
index 905bcf9..1407530 100644
--- a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
+++ b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
@@ -320,12 +320,15 @@
     public static class BackgroundDexoptJobStatsLogger {
         /** Writes background dexopt job stats to statsd. */
         public void write(@BackgroundDexOptService.Status int status,
-                @JobParameters.StopReason int cancellationReason, long durationMs,
-                long durationIncludingSleepMs) {
-            ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
+                          @JobParameters.StopReason int cancellationReason,
+                          long durationMs) {
+            ArtStatsLog.write(
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
                     STATUS_MAP.getOrDefault(status,
                             ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN),
-                    cancellationReason, durationMs, durationIncludingSleepMs);
+                    cancellationReason,
+                    durationMs,
+                    0);  // deprecated, used to be durationIncludingSleepMs
         }
     }
 }
diff --git a/services/core/java/com/android/server/pm/dex/DexoptOptions.java b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
index ea23316..f5557c4 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptOptions.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
@@ -18,6 +18,16 @@
 
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 
+import android.annotation.Nullable;
+
+import com.android.server.art.ReasonMapping;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.pm.DexOptHelper;
+import com.android.server.pm.PackageManagerService;
+
+import dalvik.system.DexFile;
+
 /**
  * Options used for dexopt invocations.
  */
@@ -40,10 +50,6 @@
     // will only consider the primary apk.
     public static final int DEXOPT_ONLY_SECONDARY_DEX = 1 << 3;
 
-    // When set, dexopt will optimize only dex files that are used by other apps.
-    // Currently, this flag is ignored for primary apks.
-    public static final int DEXOPT_ONLY_SHARED_DEX = 1 << 4;
-
     // When set, dexopt will attempt to scale down the optimizations previously applied in order
     // save disk space.
     public static final int DEXOPT_DOWNGRADE = 1 << 5;
@@ -105,7 +111,6 @@
                 DEXOPT_FORCE |
                 DEXOPT_BOOT_COMPLETE |
                 DEXOPT_ONLY_SECONDARY_DEX |
-                DEXOPT_ONLY_SHARED_DEX |
                 DEXOPT_DOWNGRADE |
                 DEXOPT_AS_SHARED_LIBRARY |
                 DEXOPT_IDLE_BACKGROUND_JOB |
@@ -146,10 +151,6 @@
         return (mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0;
     }
 
-    public boolean isDexoptOnlySharedDex() {
-        return (mFlags & DEXOPT_ONLY_SHARED_DEX) != 0;
-    }
-
     public boolean isDowngrade() {
         return (mFlags & DEXOPT_DOWNGRADE) != 0;
     }
@@ -198,4 +199,133 @@
                 mSplitName,
                 mFlags);
     }
+
+    /**
+     * Returns an {@link OptimizeParams} instance corresponding to this object, for use with
+     * {@link com.android.server.art.ArtManagerLocal}.
+     *
+     * @param extraFlags extra {@link ArtFlags#OptimizeFlags} to set in the returned
+     *     {@code OptimizeParams} beyond those converted from this object
+     * @return null if the settings cannot be accurately represented, and hence the old
+     *     PackageManager/installd code paths need to be used.
+     */
+    public @Nullable OptimizeParams convertToOptimizeParams(/*@OptimizeFlags*/ int extraFlags) {
+        if (mSplitName != null) {
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "Request to optimize only split " + mSplitName);
+            return null;
+        }
+
+        /*@OptimizeFlags*/ int flags = extraFlags;
+        if ((mFlags & DEXOPT_CHECK_FOR_PROFILES_UPDATES) == 0
+                && DexFile.isProfileGuidedCompilerFilter(mCompilerFilter)) {
+            // ART Service doesn't support bypassing this, so not setting this flag is not
+            // supported.
+            DexOptHelper.reportArtManagerFallback(mPackageName,
+                    "DEXOPT_CHECK_FOR_PROFILES_UPDATES not set with profile compiler filter");
+            return null;
+        }
+        if ((mFlags & DEXOPT_FORCE) != 0) {
+            flags |= ArtFlags.FLAG_FORCE;
+        }
+        if ((mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0) {
+            flags |= ArtFlags.FLAG_FOR_SECONDARY_DEX;
+        } else {
+            flags |= ArtFlags.FLAG_FOR_PRIMARY_DEX;
+        }
+        if ((mFlags & DEXOPT_DOWNGRADE) != 0) {
+            flags |= ArtFlags.FLAG_SHOULD_DOWNGRADE;
+        }
+        if ((mFlags & DEXOPT_INSTALL_WITH_DEX_METADATA_FILE) == 0) {
+            // ART Service cannot be instructed to ignore a DM file if present, so not setting this
+            // flag is not supported.
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "DEXOPT_INSTALL_WITH_DEX_METADATA_FILE not set");
+            return null;
+        }
+
+        /*@PriorityClassApi*/ int priority;
+        // Replicates logic in RunDex2Oat::PrepareCompilerRuntimeAndPerfConfigFlags in installd.
+        if ((mFlags & DEXOPT_BOOT_COMPLETE) != 0) {
+            if ((mFlags & DEXOPT_FOR_RESTORE) != 0) {
+                priority = ArtFlags.PRIORITY_INTERACTIVE_FAST;
+            } else {
+                // TODO(b/251903639): Repurpose DEXOPT_IDLE_BACKGROUND_JOB to choose new
+                // dalvik.vm.background-dex2oat-* properties.
+                priority = ArtFlags.PRIORITY_INTERACTIVE;
+            }
+        } else {
+            priority = ArtFlags.PRIORITY_BOOT;
+        }
+
+        // The following flags in mFlags are ignored:
+        //
+        // -  DEXOPT_AS_SHARED_LIBRARY: It's implicit with ART Service since it always looks at
+        //    <uses-library> rather than actual dependencies.
+        //
+        //    We don't require it to be set either. It's safe when switching between old and new
+        //    code paths since the only effect is that some packages may be unnecessarily compiled
+        //    without user profiles.
+        //
+        // -  DEXOPT_IDLE_BACKGROUND_JOB: Its only effect is to allow the debug variant dex2oatd to
+        //    be used, but ART Service never uses that (cf. Artd::GetDex2Oat in artd.cc).
+
+        String reason;
+        switch (mCompilationReason) {
+            case PackageManagerService.REASON_FIRST_BOOT:
+                reason = ReasonMapping.REASON_FIRST_BOOT;
+                break;
+            case PackageManagerService.REASON_BOOT_AFTER_OTA:
+                reason = ReasonMapping.REASON_BOOT_AFTER_OTA;
+                break;
+            case PackageManagerService.REASON_POST_BOOT:
+                // This reason will go away with the legacy dexopt code.
+                DexOptHelper.reportArtManagerFallback(
+                        mPackageName, "Unsupported compilation reason REASON_POST_BOOT");
+                return null;
+            case PackageManagerService.REASON_INSTALL:
+                reason = ReasonMapping.REASON_INSTALL;
+                break;
+            case PackageManagerService.REASON_INSTALL_FAST:
+                reason = ReasonMapping.REASON_INSTALL_FAST;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK:
+                reason = ReasonMapping.REASON_INSTALL_BULK;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_BACKGROUND_DEXOPT:
+                reason = ReasonMapping.REASON_BG_DEXOPT;
+                break;
+            case PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE:
+                reason = ReasonMapping.REASON_INACTIVE;
+                break;
+            case PackageManagerService.REASON_CMDLINE:
+                reason = ReasonMapping.REASON_CMDLINE;
+                break;
+            case PackageManagerService.REASON_SHARED:
+            case PackageManagerService.REASON_AB_OTA:
+                // REASON_SHARED shouldn't go into this code path - it's only used at lower levels
+                // in PackageDexOptimizer.
+                // TODO(b/251921228): OTA isn't supported, so REASON_AB_OTA shouldn't come this way
+                // either.
+                throw new UnsupportedOperationException(
+                        "ART Service unsupported compilation reason " + mCompilationReason);
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid compilation reason " + mCompilationReason);
+        }
+
+        return new OptimizeParams.Builder(reason, flags)
+                .setCompilerFilter(mCompilerFilter)
+                .setPriorityClass(priority)
+                .build();
+    }
 }
diff --git a/services/core/java/com/android/server/pm/dex/DexoptUtils.java b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
index 5ba209d..9bca155 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptUtils.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptUtils.java
@@ -295,6 +295,7 @@
      * NOTE: Keep this in sync with the dexopt expectations! Right now that is either "PCL[path]"
      * for a PathClassLoader or "DLC[path]" for a DelegateLastClassLoader.
      */
+    @SuppressWarnings("ReturnValueIgnored")
     /*package*/ static String encodeClassLoader(String classpath, String classLoaderName) {
         classpath.getClass();  // Throw NPE if classpath is null
         String classLoaderDexoptEncoding = classLoaderName;
diff --git a/services/core/java/com/android/server/pm/dex/OdsignStatsLogger.java b/services/core/java/com/android/server/pm/dex/OdsignStatsLogger.java
index fa08add..227a3a1 100644
--- a/services/core/java/com/android/server/pm/dex/OdsignStatsLogger.java
+++ b/services/core/java/com/android/server/pm/dex/OdsignStatsLogger.java
@@ -39,6 +39,7 @@
     // These need to be kept in sync with system/security/ondevice-signing/StatsReporter.{h, cpp}.
     private static final String METRICS_FILE = "/data/misc/odsign/metrics/odsign-metrics.txt";
     private static final String COMPOS_METRIC_NAME = "comp_os_artifacts_check_record";
+    private static final String ODSIGN_METRIC_NAME = "odsign_record";
 
     /**
      * Arrange for stats to be uploaded in the background.
@@ -64,18 +65,45 @@
             for (String line : lines.split("\n")) {
                 String[] metrics = line.split(" ");
 
-                if (metrics.length != 4 || !metrics[0].equals(COMPOS_METRIC_NAME)) {
-                    Slog.w(TAG, "Malformed metrics file");
-                    break;
+                if (line.isEmpty() || metrics.length < 1) {
+                    Slog.w(TAG, "Empty metrics line");
+                    continue;
                 }
 
-                boolean currentArtifactsOk = metrics[1].equals("1");
-                boolean compOsPendingArtifactsExists = metrics[2].equals("1");
-                boolean useCompOsGeneratedArtifacts = metrics[3].equals("1");
+                switch (metrics[0]) {
+                    case COMPOS_METRIC_NAME: {
+                        if (metrics.length != 4) {
+                            Slog.w(TAG, "Malformed CompOS metrics line '" + line + "'");
+                            continue;
+                        }
 
-                ArtStatsLog.write(ArtStatsLog.EARLY_BOOT_COMP_OS_ARTIFACTS_CHECK_REPORTED,
-                        currentArtifactsOk, compOsPendingArtifactsExists,
-                        useCompOsGeneratedArtifacts);
+                        boolean currentArtifactsOk = metrics[1].equals("1");
+                        boolean compOsPendingArtifactsExists = metrics[2].equals("1");
+                        boolean useCompOsGeneratedArtifacts = metrics[3].equals("1");
+
+                        ArtStatsLog.write(ArtStatsLog.EARLY_BOOT_COMP_OS_ARTIFACTS_CHECK_REPORTED,
+                                currentArtifactsOk, compOsPendingArtifactsExists,
+                                useCompOsGeneratedArtifacts);
+                        break;
+                    }
+                    case ODSIGN_METRIC_NAME: {
+                        if (metrics.length != 2) {
+                            Slog.w(TAG, "Malformed odsign metrics line '" + line + "'");
+                            continue;
+                        }
+
+                        try {
+                            int status = Integer.parseInt(metrics[1]);
+                            ArtStatsLog.write(ArtStatsLog.ODSIGN_REPORTED, status);
+                        } catch (NumberFormatException e) {
+                            Slog.w(TAG, "Malformed odsign metrics line '" + line + "'");
+                        }
+
+                        break;
+                    }
+                    default:
+                        Slog.w(TAG, "Malformed metrics line '" + line + "'");
+                }
             }
         } catch (FileNotFoundException e) {
             // This is normal and probably means no new metrics have been generated.
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 6b31555..a7d4cea 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.apex.ApexInfo;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.Attribution;
@@ -79,11 +78,8 @@
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.server.pm.pkg.parsing.ParsingUtils;
 
-import libcore.util.EmptyArray;
-
 import java.io.File;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -109,21 +105,9 @@
     public static PackageInfo generate(AndroidPackage pkg, int[] gids,
             @PackageManager.PackageInfoFlagsBits long flags, long firstInstallTime,
             long lastUpdateTime, Set<String> grantedPermissions, PackageUserStateInternal state,
-            @UserIdInt int userId, @Nullable PackageStateInternal pkgSetting) {
+            @UserIdInt int userId, @NonNull PackageStateInternal pkgSetting) {
         return generateWithComponents(pkg, gids, flags, firstInstallTime, lastUpdateTime,
-                grantedPermissions, state, userId, null, pkgSetting);
-    }
-
-    /**
-     * @param pkgSetting See {@link PackageInfoUtils} for description of pkgSetting usage.
-     * @deprecated Once ENABLE_FEATURE_SCAN_APEX is removed, this should also be removed.
-     */
-    @Deprecated
-    @Nullable
-    public static PackageInfo generate(AndroidPackage pkg, ApexInfo apexInfo, long flags,
-            @Nullable PackageStateInternal pkgSetting, @UserIdInt int userId) {
-        return generateWithComponents(pkg, EmptyArray.INT, flags, 0, 0, Collections.emptySet(),
-                PackageUserStateInternal.DEFAULT, userId, apexInfo, pkgSetting);
+                grantedPermissions, state, userId, pkgSetting);
     }
 
     /**
@@ -132,8 +116,7 @@
     private static PackageInfo generateWithComponents(AndroidPackage pkg, int[] gids,
             @PackageManager.PackageInfoFlagsBits long flags, long firstInstallTime,
             long lastUpdateTime, Set<String> grantedPermissions, PackageUserStateInternal state,
-            @UserIdInt int userId, @Nullable ApexInfo apexInfo,
-            @Nullable PackageStateInternal pkgSetting) {
+            @UserIdInt int userId, @NonNull PackageStateInternal pkgSetting) {
         ApplicationInfo applicationInfo = generateApplicationInfo(pkg, flags, state, userId,
                 pkgSetting);
         if (applicationInfo == null) {
@@ -247,22 +230,6 @@
                     &= ~ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE;
         }
 
-        if (apexInfo != null) {
-            File apexFile = new File(apexInfo.modulePath);
-
-            info.applicationInfo.sourceDir = apexFile.getPath();
-            info.applicationInfo.publicSourceDir = apexFile.getPath();
-            info.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
-            info.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
-            if (apexInfo.isFactory) {
-                info.applicationInfo.flags &= ~ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
-            } else {
-                info.applicationInfo.flags |= ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
-            }
-            info.isApex = true;
-            info.isActiveApex = apexInfo.isActive;
-        }
-
         final SigningDetails signingDetails = pkg.getSigningDetails();
         // deprecated method of getting signing certificates
         if ((flags & PackageManager.GET_SIGNATURES) != 0) {
@@ -294,7 +261,7 @@
         info.coreApp = pkg.isCoreApp();
         info.isApex = pkg.isApex();
 
-        if (pkgSetting != null && !pkgSetting.hasSharedUser()) {
+        if (!pkgSetting.hasSharedUser()) {
             // It is possible that this shared UID app has left
             info.sharedUserId = null;
             info.sharedUserLabel = 0;
@@ -452,7 +419,7 @@
     public static ApplicationInfo generateApplicationInfo(AndroidPackage pkg,
             @PackageManager.ApplicationInfoFlagsBits long flags,
             @NonNull PackageUserStateInternal state, @UserIdInt int userId,
-            @Nullable PackageStateInternal pkgSetting) {
+            @NonNull PackageStateInternal pkgSetting) {
         if (pkg == null) {
             return null;
         }
@@ -463,35 +430,31 @@
         }
 
         // Make shallow copy so we can store the metadata/libraries safely
-        ApplicationInfo info = AndroidPackageUtils.toAppInfoWithoutState(pkg);
+        ApplicationInfo info = AndroidPackageUtils.generateAppInfoWithoutState(pkg);
 
         updateApplicationInfo(info, flags, state);
 
         initForUser(info, pkg, userId);
 
-        if (pkgSetting != null) {
-            // TODO(b/135203078): Remove PackageParser1/toAppInfoWithoutState and clean all this up
-            PackageStateUnserialized pkgState = pkgSetting.getTransientState();
-            info.hiddenUntilInstalled = pkgState.isHiddenUntilInstalled();
-            List<String> usesLibraryFiles = pkgState.getUsesLibraryFiles();
-            var usesLibraries = pkgState.getUsesLibraryInfos();
-            var usesLibraryInfos = new ArrayList<SharedLibraryInfo>();
-            for (int index = 0; index < usesLibraries.size(); index++) {
-                usesLibraryInfos.add(usesLibraries.get(index).getInfo());
-            }
-            info.sharedLibraryFiles = usesLibraryFiles.isEmpty()
-                    ? null : usesLibraryFiles.toArray(new String[0]);
-            info.sharedLibraryInfos = usesLibraryInfos.isEmpty() ? null : usesLibraryInfos;
-            if (info.category == ApplicationInfo.CATEGORY_UNDEFINED) {
-                info.category = pkgSetting.getCategoryOverride();
-            }
+        // TODO(b/135203078): Remove PackageParser1/toAppInfoWithoutState and clean all this up
+        PackageStateUnserialized pkgState = pkgSetting.getTransientState();
+        info.hiddenUntilInstalled = pkgState.isHiddenUntilInstalled();
+        List<String> usesLibraryFiles = pkgState.getUsesLibraryFiles();
+        var usesLibraries = pkgState.getUsesLibraryInfos();
+        var usesLibraryInfos = new ArrayList<SharedLibraryInfo>();
+        for (int index = 0; index < usesLibraries.size(); index++) {
+            usesLibraryInfos.add(usesLibraries.get(index).getInfo());
+        }
+        info.sharedLibraryFiles = usesLibraryFiles.isEmpty()
+                ? null : usesLibraryFiles.toArray(new String[0]);
+        info.sharedLibraryInfos = usesLibraryInfos.isEmpty() ? null : usesLibraryInfos;
+        if (info.category == ApplicationInfo.CATEGORY_UNDEFINED) {
+            info.category = pkgSetting.getCategoryOverride();
         }
 
         info.seInfo = AndroidPackageUtils.getSeInfo(pkg, pkgSetting);
-        info.primaryCpuAbi = pkgSetting == null ? AndroidPackageUtils.getRawPrimaryCpuAbi(pkg)
-                : pkgSetting.getPrimaryCpuAbi();
-        info.secondaryCpuAbi = pkgSetting == null ? AndroidPackageUtils.getRawSecondaryCpuAbi(pkg)
-                : pkgSetting.getSecondaryCpuAbi();
+        info.primaryCpuAbi = pkgSetting.getPrimaryCpuAbi();
+        info.secondaryCpuAbi = pkgSetting.getSecondaryCpuAbi();
 
         info.flags |= appInfoFlags(info.flags, pkgSetting);
         info.privateFlags |= appInfoPrivateFlags(info.privateFlags, pkgSetting);
@@ -508,7 +471,7 @@
     public static ActivityInfo generateActivityInfo(AndroidPackage pkg, ParsedActivity a,
             @PackageManager.ComponentInfoFlagsBits long flags,
             @NonNull PackageUserStateInternal state, @UserIdInt int userId,
-            @Nullable PackageStateInternal pkgSetting) {
+            @NonNull PackageStateInternal pkgSetting) {
         return generateActivityInfo(pkg, a, flags, state, null, userId, pkgSetting);
     }
 
@@ -520,7 +483,7 @@
     public static ActivityInfo generateActivityInfo(AndroidPackage pkg, ParsedActivity a,
             @PackageManager.ComponentInfoFlagsBits long flags,
             @NonNull PackageUserStateInternal state, @Nullable ApplicationInfo applicationInfo,
-            @UserIdInt int userId, @Nullable PackageStateInternal pkgSetting) {
+            @UserIdInt int userId, @NonNull PackageStateInternal pkgSetting) {
         if (a == null) return null;
         if (!checkUseInstalledOrHidden(pkg, pkgSetting, state, flags)) {
             return null;
@@ -570,6 +533,7 @@
             ai.metaData = null;
         }
         ai.applicationInfo = applicationInfo;
+        ai.targetDisplayCategory = a.getTargetDisplayCategory();
         ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
         assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId);
         return ai;
@@ -596,7 +560,7 @@
     @Nullable
     public static ServiceInfo generateServiceInfo(AndroidPackage pkg, ParsedService s,
             @PackageManager.ComponentInfoFlagsBits long flags, PackageUserStateInternal state,
-            @UserIdInt int userId, @Nullable PackageStateInternal pkgSetting) {
+            @UserIdInt int userId, @NonNull PackageStateInternal pkgSetting) {
         return generateServiceInfo(pkg, s, flags, state, null, userId, pkgSetting);
     }
 
@@ -608,7 +572,7 @@
     public static ServiceInfo generateServiceInfo(AndroidPackage pkg, ParsedService s,
             @PackageManager.ComponentInfoFlagsBits long flags, PackageUserStateInternal state,
             @Nullable ApplicationInfo applicationInfo, int userId,
-            @Nullable PackageStateInternal pkgSetting) {
+            @NonNull PackageStateInternal pkgSetting) {
         if (s == null) return null;
         if (!checkUseInstalledOrHidden(pkg, pkgSetting, state, flags)) {
             return null;
@@ -646,7 +610,7 @@
     public static ProviderInfo generateProviderInfo(AndroidPackage pkg, ParsedProvider p,
             @PackageManager.ComponentInfoFlagsBits long flags, PackageUserStateInternal state,
             @NonNull ApplicationInfo applicationInfo, int userId,
-            @Nullable PackageStateInternal pkgSetting) {
+            @NonNull PackageStateInternal pkgSetting) {
         if (p == null) return null;
         if (!checkUseInstalledOrHidden(pkg, pkgSetting, state, flags)) {
             return null;
@@ -695,7 +659,7 @@
     @Nullable
     public static InstrumentationInfo generateInstrumentationInfo(ParsedInstrumentation i,
             AndroidPackage pkg, @PackageManager.ComponentInfoFlagsBits long flags,
-            PackageUserStateInternal state, int userId, @Nullable PackageStateInternal pkgSetting) {
+            PackageUserStateInternal state, int userId, @NonNull PackageStateInternal pkgSetting) {
         if (i == null) return null;
         if (!checkUseInstalledOrHidden(pkg, pkgSetting, state, flags)) {
             return null;
@@ -718,10 +682,8 @@
 
         initForUser(info, pkg, userId);
 
-        info.primaryCpuAbi = pkgSetting == null ? AndroidPackageUtils.getRawPrimaryCpuAbi(pkg)
-                : pkgSetting.getPrimaryCpuAbi();
-        info.secondaryCpuAbi = pkgSetting == null ? AndroidPackageUtils.getRawSecondaryCpuAbi(pkg)
-                : pkgSetting.getSecondaryCpuAbi();
+        info.primaryCpuAbi = pkgSetting.getPrimaryCpuAbi();
+        info.secondaryCpuAbi = pkgSetting.getSecondaryCpuAbi();
         info.nativeLibraryDir = pkg.getNativeLibraryDir();
         info.secondaryNativeLibraryDir = pkg.getSecondaryNativeLibraryDir();
 
@@ -819,12 +781,11 @@
      * all uninstalled and hidden packages as well.
      */
     public static boolean checkUseInstalledOrHidden(AndroidPackage pkg,
-            PackageStateInternal pkgSetting, PackageUserStateInternal state,
+            @NonNull PackageStateInternal pkgSetting, PackageUserStateInternal state,
             @PackageManager.PackageInfoFlagsBits long flags) {
         // Returns false if the package is hidden system app until installed.
         if ((flags & PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS) == 0
                 && !state.isInstalled()
-                && pkgSetting != null
                 && pkgSetting.getTransientState().isHiddenUntilInstalled()) {
             return false;
         }
@@ -877,7 +838,7 @@
 
     private static void assignFieldsComponentInfoParsedMainComponent(
             @NonNull ComponentInfo info, @NonNull ParsedMainComponent component,
-            @Nullable PackageStateInternal pkgSetting, int userId) {
+            @NonNull PackageStateInternal pkgSetting, @UserIdInt int userId) {
         assignFieldsComponentInfoParsedMainComponent(info, component);
         Pair<CharSequence, Integer> labelAndIcon =
                 ParsedComponentStateUtils.getNonLocalizedLabelAndIcon(component, pkgSetting,
@@ -888,7 +849,7 @@
 
     private static void assignFieldsPackageItemInfoParsedComponent(
             @NonNull PackageItemInfo info, @NonNull ParsedComponent component,
-            @Nullable PackageStateInternal pkgSetting, int userId) {
+            @NonNull PackageStateInternal pkgSetting, @UserIdInt int userId) {
         assignFieldsPackageItemInfoParsedComponent(info, component);
         Pair<CharSequence, Integer> labelAndIcon =
                 ParsedComponentStateUtils.getNonLocalizedLabelAndIcon(component, pkgSetting,
@@ -1140,7 +1101,7 @@
         @Nullable
         public ApplicationInfo generate(AndroidPackage pkg,
                 @PackageManager.ApplicationInfoFlagsBits long flags, PackageUserStateInternal state,
-                int userId, @Nullable PackageStateInternal pkgSetting) {
+                int userId, @NonNull PackageStateInternal pkgSetting) {
             ApplicationInfo appInfo = mCache.get(pkg.getPackageName());
             if (appInfo != null) {
                 return appInfo;
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index a6f1b29..5b0cc51 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -321,8 +321,4 @@
         info.versionCode = ((ParsingPackageHidden) pkg).getVersionCode();
         info.versionCodeMajor = ((ParsingPackageHidden) pkg).getVersionCodeMajor();
     }
-
-    public static ApplicationInfo toAppInfoWithoutState(AndroidPackage pkg) {
-        return ((ParsingPackageHidden) pkg).toAppInfoWithoutState();
-    }
 }
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 37abeac..83e17a5 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -62,12 +62,12 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.pm.KnownPackages;
@@ -86,6 +86,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -165,6 +166,11 @@
         COARSE_BACKGROUND_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
     }
 
+    private static final Set<String> FINE_LOCATION_PERMISSIONS = new ArraySet<>();
+    static {
+        FINE_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_FINE_LOCATION);
+    }
+
     private static final Set<String> ACTIVITY_RECOGNITION_PERMISSIONS = new ArraySet<>();
     static {
         ACTIVITY_RECOGNITION_PERMISSIONS.add(Manifest.permission.ACTIVITY_RECOGNITION);
@@ -616,6 +622,10 @@
         grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId,
                 NOTIFICATION_PERMISSIONS);
 
+        // Dock Manager
+        grantPermissionsToSystemPackage(pm, getDefaultDockManagerPackage(), userId,
+                NOTIFICATION_PERMISSIONS);
+
         // Camera
         grantPermissionsToSystemPackage(pm,
                 getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId),
@@ -783,6 +793,8 @@
                         CONTACTS_PERMISSIONS, CALENDAR_PERMISSIONS, MICROPHONE_PERMISSIONS,
                         PHONE_PERMISSIONS, SMS_PERMISSIONS, COARSE_BACKGROUND_LOCATION_PERMISSIONS,
                         NEARBY_DEVICES_PERMISSIONS, NOTIFICATION_PERMISSIONS);
+                revokeRuntimePermissions(pm, voiceInteractPackageName, FINE_LOCATION_PERMISSIONS,
+                        false, userId);
             }
         }
 
@@ -933,6 +945,10 @@
         return mContext.getString(R.string.config_defaultCaptivePortalLoginPackageName);
     }
 
+    private String getDefaultDockManagerPackage() {
+        return mContext.getString(R.string.config_defaultDockManagerPackageName);
+    }
+
     @SafeVarargs
     private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
             ArrayList<String> packages, int userId, Set<String>... permissions) {
@@ -1924,7 +1940,7 @@
                             mPkgRequestingPerm, newRestrictionExcemptFlags, -1, mUser);
                 }
 
-                if (newGranted != null && newGranted != mOriginalGranted) {
+                if (newGranted != null && !Objects.equals(newGranted, mOriginalGranted)) {
                     if (newGranted) {
                         NO_PM_CACHE.grantPermission(mPermission, mPkgRequestingPerm, mUser);
                     } else {
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermission.java b/services/core/java/com/android/server/pm/permission/LegacyPermission.java
index 5f8f342..d8b4faa 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermission.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermission.java
@@ -21,9 +21,9 @@
 import android.annotation.Nullable;
 import android.content.pm.PermissionInfo;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.DumpState;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
index f63600a..fc6d202 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
@@ -21,11 +21,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.DumpState;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java
index 661161f..2a65a01 100644
--- a/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java
+++ b/services/core/java/com/android/server/pm/permission/OneTimePermissionUserManager.java
@@ -16,17 +16,18 @@
 
 package com.android.server.pm.permission;
 
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED;
-
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.permission.PermissionControllerManager;
 import android.provider.DeviceConfig;
 import android.util.Log;
@@ -48,7 +49,7 @@
             "one_time_permissions_killed_delay_millis";
 
     private final @NonNull Context mContext;
-    private final @NonNull ActivityManager mActivityManager;
+    private final @NonNull IActivityManager mIActivityManager;
     private final @NonNull AlarmManager mAlarmManager;
     private final @NonNull PermissionControllerManager mPermissionControllerManager;
 
@@ -78,50 +79,15 @@
 
     OneTimePermissionUserManager(@NonNull Context context) {
         mContext = context;
-        mActivityManager = context.getSystemService(ActivityManager.class);
+        mIActivityManager = ActivityManager.getService();
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mPermissionControllerManager = new PermissionControllerManager(
                 mContext, PermissionThread.getHandler());
         mHandler = context.getMainThreadHandler();
     }
 
-    /**
-     * Starts a one-time permission session for a given package. A one-time permission session is
-     * ended if app becomes inactive. Inactivity is defined as the package's uid importance level
-     * staying > importanceToResetTimer for timeoutMillis milliseconds. If the package's uid
-     * importance level goes <= importanceToResetTimer then the timer is reset and doesn't start
-     * until going > importanceToResetTimer.
-     * <p>
-     * When this timeoutMillis is reached if the importance level is <= importanceToKeepSessionAlive
-     * then the session is extended until either the importance goes above
-     * importanceToKeepSessionAlive which will end the session or <= importanceToResetTimer which
-     * will continue the session and reset the timer.
-     * </p>
-     * <p>
-     * Importance levels are defined in {@link android.app.ActivityManager.RunningAppProcessInfo}.
-     * </p>
-     * <p>
-     * Once the session ends PermissionControllerService#onNotifyOneTimePermissionSessionTimeout
-     * is invoked.
-     * </p>
-     * <p>
-     * Note that if there is currently an active session for a package a new one isn't created and
-     * the existing one isn't changed.
-     * </p>
-     * @param packageName The package to start a one-time permission session for
-     * @param timeoutMillis Number of milliseconds for an app to be in an inactive state
-     * @param revokeAfterKilledDelayMillis Number of milliseconds to wait after the process dies
-     *                                     before ending the session. Set to -1 to use default value
-     *                                     for the device.
-     * @param importanceToResetTimer The least important level to uid must be to reset the timer
-     * @param importanceToKeepSessionAlive The least important level the uid must be to keep the
-     *                                     session alive
-     *
-     * @hide
-     */
     void startPackageOneTimeSession(@NonNull String packageName, long timeoutMillis,
-            long revokeAfterKilledDelayMillis, int importanceToResetTimer,
-            int importanceToKeepSessionAlive) {
+            long revokeAfterKilledDelayMillis) {
         int uid;
         try {
             uid = mContext.getPackageManager().getPackageUid(packageName, 0);
@@ -133,13 +99,11 @@
         synchronized (mLock) {
             PackageInactivityListener listener = mListeners.get(uid);
             if (listener != null) {
-                listener.updateSessionParameters(timeoutMillis, revokeAfterKilledDelayMillis,
-                        importanceToResetTimer, importanceToKeepSessionAlive);
+                listener.updateSessionParameters(timeoutMillis, revokeAfterKilledDelayMillis);
                 return;
             }
             listener = new PackageInactivityListener(uid, packageName, timeoutMillis,
-                    revokeAfterKilledDelayMillis, importanceToResetTimer,
-                    importanceToKeepSessionAlive);
+                    revokeAfterKilledDelayMillis);
             mListeners.put(uid, listener);
         }
     }
@@ -184,34 +148,58 @@
 
         private static final long TIMER_INACTIVE = -1;
 
+        private static final int STATE_GONE = 0;
+        private static final int STATE_TIMER = 1;
+        private static final int STATE_ACTIVE = 2;
+
         private final int mUid;
         private final @NonNull String mPackageName;
         private long mTimeout;
         private long mRevokeAfterKilledDelay;
-        private int mImportanceToResetTimer;
-        private int mImportanceToKeepSessionAlive;
 
         private boolean mIsAlarmSet;
         private boolean mIsFinished;
 
         private long mTimerStart = TIMER_INACTIVE;
 
-        private final ActivityManager.OnUidImportanceListener mStartTimerListener;
-        private final ActivityManager.OnUidImportanceListener mSessionKillableListener;
-        private final ActivityManager.OnUidImportanceListener mGoneListener;
-
         private final Object mInnerLock = new Object();
         private final Object mToken = new Object();
+        private final IUidObserver.Stub mObserver = new IUidObserver.Stub() {
+            @Override
+            public void onUidGone(int uid, boolean disabled) {
+                if (uid == mUid) {
+                    PackageInactivityListener.this.updateUidState(STATE_GONE);
+                }
+            }
+
+            @Override
+            public void onUidStateChanged(int uid, int procState, long procStateSeq,
+                    int capability) {
+                if (uid == mUid) {
+                    if (procState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+                            && procState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
+                        PackageInactivityListener.this.updateUidState(STATE_TIMER);
+                    } else {
+                        PackageInactivityListener.this.updateUidState(STATE_ACTIVE);
+                    }
+                }
+            }
+
+            public void onUidActive(int uid) {
+            }
+            public void onUidIdle(int uid, boolean disabled) {
+            }
+            public void onUidProcAdjChanged(int uid) {
+            }
+            public void onUidCachedChanged(int uid, boolean cached) {
+            }
+        };
 
         private PackageInactivityListener(int uid, @NonNull String packageName, long timeout,
-                long revokeAfterkilledDelay, int importanceToResetTimer,
-                int importanceToKeepSessionAlive) {
-
+                long revokeAfterkilledDelay) {
             Log.i(LOG_TAG,
                     "Start tracking " + packageName + ". uid=" + uid + " timeout=" + timeout
-                            + " killedDelay=" + revokeAfterkilledDelay
-                            + " importanceToResetTimer=" + importanceToResetTimer
-                            + " importanceToKeepSessionAlive=" + importanceToKeepSessionAlive);
+                            + " killedDelay=" + revokeAfterkilledDelay);
 
             mUid = uid;
             mPackageName = packageName;
@@ -221,27 +209,24 @@
                             DeviceConfig.NAMESPACE_PERMISSIONS, PROPERTY_KILLED_DELAY_CONFIG_KEY,
                             DEFAULT_KILLED_DELAY_MILLIS)
                     : revokeAfterkilledDelay;
-            mImportanceToResetTimer = importanceToResetTimer;
-            mImportanceToKeepSessionAlive = importanceToKeepSessionAlive;
 
-            mStartTimerListener =
-                    (changingUid, importance) -> onImportanceChanged(changingUid, importance);
-            mSessionKillableListener =
-                    (changingUid, importance) -> onImportanceChanged(changingUid, importance);
-            mGoneListener =
-                    (changingUid, importance) -> onImportanceChanged(changingUid, importance);
+            try {
+                mIActivityManager.registerUidObserver(mObserver,
+                        ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_PROCSTATE,
+                        ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE,
+                        null);
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Couldn't check uid proc state", e);
+                // Can't register uid observer, just revoke immediately
+                synchronized (mInnerLock) {
+                    onPackageInactiveLocked();
+                }
+            }
 
-            mActivityManager.addOnUidImportanceListener(mStartTimerListener,
-                    importanceToResetTimer);
-            mActivityManager.addOnUidImportanceListener(mSessionKillableListener,
-                    importanceToKeepSessionAlive);
-            mActivityManager.addOnUidImportanceListener(mGoneListener, IMPORTANCE_CACHED);
-
-            onImportanceChanged(mUid, mActivityManager.getPackageImportance(packageName));
+            updateUidState();
         }
 
-        public void updateSessionParameters(long timeoutMillis, long revokeAfterKilledDelayMillis,
-                int importanceToResetTimer, int importanceToKeepSessionAlive) {
+        public void updateSessionParameters(long timeoutMillis, long revokeAfterKilledDelayMillis) {
             synchronized (mInnerLock) {
                 mTimeout = Math.min(mTimeout, timeoutMillis);
                 mRevokeAfterKilledDelay = Math.min(mRevokeAfterKilledDelay,
@@ -250,63 +235,79 @@
                                 DeviceConfig.NAMESPACE_PERMISSIONS,
                                 PROPERTY_KILLED_DELAY_CONFIG_KEY, DEFAULT_KILLED_DELAY_MILLIS)
                                 : revokeAfterKilledDelayMillis);
-                mImportanceToResetTimer = Math.min(importanceToResetTimer, mImportanceToResetTimer);
-                mImportanceToKeepSessionAlive = Math.min(importanceToKeepSessionAlive,
-                        mImportanceToKeepSessionAlive);
                 Log.v(LOG_TAG,
                         "Updated params for " + mPackageName + ". timeout=" + mTimeout
-                                + " killedDelay=" + mRevokeAfterKilledDelay
-                                + " importanceToResetTimer=" + mImportanceToResetTimer
-                                + " importanceToKeepSessionAlive=" + mImportanceToKeepSessionAlive);
-                onImportanceChanged(mUid, mActivityManager.getPackageImportance(mPackageName));
+                                + " killedDelay=" + mRevokeAfterKilledDelay);
+                updateUidState();
             }
         }
 
-        private void onImportanceChanged(int uid, int importance) {
-            if (uid != mUid) {
-                return;
+        private int getCurrentState() {
+            try {
+                return getStateFromProcState(mIActivityManager.getUidProcessState(mUid, null));
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Couldn't check uid proc state", e);
             }
+            return STATE_GONE;
+        }
 
-            Log.v(LOG_TAG, "Importance changed for " + mPackageName + " (" + mUid + ")."
-                    + " importance=" + importance);
+        private int getStateFromProcState(int procState) {
+            if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) {
+                return STATE_GONE;
+            } else {
+                if (procState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+                    return STATE_TIMER;
+                } else {
+                    return STATE_ACTIVE;
+                }
+            }
+        }
+
+        private void updateUidState() {
+            updateUidState(getCurrentState());
+        }
+
+        private void updateUidState(int state) {
+            Log.v(LOG_TAG, "Updating state for " + mPackageName + " (" + mUid + ")."
+                    + " state=" + state);
             synchronized (mInnerLock) {
                 // Remove any pending inactivity callback
                 mHandler.removeCallbacksAndMessages(mToken);
 
-                if (importance > IMPORTANCE_CACHED) {
+                if (state == STATE_GONE) {
                     if (mRevokeAfterKilledDelay == 0) {
                         onPackageInactiveLocked();
                         return;
                     }
                     // Delay revocation in case app is restarting
                     mHandler.postDelayed(() -> {
-                        int imp = mActivityManager.getUidImportance(mUid);
-                        if (imp > IMPORTANCE_CACHED) {
-                            onPackageInactiveLocked();
-                        } else {
-                            if (DEBUG) {
-                                Log.d(LOG_TAG, "No longer gone after delayed revocation. "
-                                        + "Rechecking for " + mPackageName + " (" + mUid + ").");
+                        int currentState;
+                        synchronized (mInnerLock) {
+                            currentState = getCurrentState();
+                            if (currentState == STATE_GONE) {
+                                onPackageInactiveLocked();
+                                return;
                             }
-                            onImportanceChanged(mUid, imp);
                         }
+                        if (DEBUG) {
+                            Log.d(LOG_TAG, "No longer gone after delayed revocation. "
+                                    + "Rechecking for " + mPackageName + " (" + mUid
+                                    + ").");
+                        }
+                        updateUidState(currentState);
                     }, mToken, mRevokeAfterKilledDelay);
                     return;
-                }
-                if (importance > mImportanceToResetTimer) {
+                } else if (state == STATE_TIMER) {
                     if (mTimerStart == TIMER_INACTIVE) {
                         if (DEBUG) {
                             Log.d(LOG_TAG, "Start the timer for "
                                     + mPackageName + " (" + mUid + ").");
                         }
                         mTimerStart = System.currentTimeMillis();
+                        setAlarmLocked();
                     }
-                } else {
+                } else if (state == STATE_ACTIVE) {
                     mTimerStart = TIMER_INACTIVE;
-                }
-                if (importance > mImportanceToKeepSessionAlive) {
-                    setAlarmLocked();
-                } else {
                     cancelAlarmLocked();
                 }
             }
@@ -320,19 +321,9 @@
                 mIsFinished = true;
                 cancelAlarmLocked();
                 try {
-                    mActivityManager.removeOnUidImportanceListener(mStartTimerListener);
-                } catch (IllegalArgumentException e) {
-                    Log.e(LOG_TAG, "Could not remove start timer listener", e);
-                }
-                try {
-                    mActivityManager.removeOnUidImportanceListener(mSessionKillableListener);
-                } catch (IllegalArgumentException e) {
-                    Log.e(LOG_TAG, "Could not remove session killable listener", e);
-                }
-                try {
-                    mActivityManager.removeOnUidImportanceListener(mGoneListener);
-                } catch (IllegalArgumentException e) {
-                    Log.e(LOG_TAG, "Could not remove gone listener", e);
+                    mIActivityManager.unregisterUidObserver(mObserver);
+                } catch (RemoteException e) {
+                    Log.e(LOG_TAG, "Unable to unregister uid observer.", e);
                 }
             }
         }
@@ -396,9 +387,11 @@
                         mPermissionControllerManager.notifyOneTimePermissionSessionTimeout(
                                 mPackageName);
                     });
-            mActivityManager.removeOnUidImportanceListener(mStartTimerListener);
-            mActivityManager.removeOnUidImportanceListener(mSessionKillableListener);
-            mActivityManager.removeOnUidImportanceListener(mGoneListener);
+            try {
+                mIActivityManager.unregisterUidObserver(mObserver);
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "Unable to unregister uid observer.", e);
+            }
             synchronized (mLock) {
                 mListeners.remove(mUid);
             }
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 3b9f0ba..9ec63fc 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -50,6 +50,7 @@
 import android.content.pm.PermissionGroupInfo;
 import android.content.pm.PermissionInfo;
 import android.content.pm.permission.SplitPermissionInfoParcelable;
+import android.healthconnect.HealthConnectManager;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Process;
@@ -385,8 +386,7 @@
 
     @Override
     public void startOneTimePermissionSession(String packageName, @UserIdInt int userId,
-            long timeoutMillis, long revokeAfterKilledDelayMillis, int importanceToResetTimer,
-            int importanceToKeepSessionAlive) {
+            long timeoutMillis, long revokeAfterKilledDelayMillis) {
         mContext.enforceCallingOrSelfPermission(
                 Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS,
                 "Must hold " + Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS
@@ -396,8 +396,7 @@
         final long token = Binder.clearCallingIdentity();
         try {
             getOneTimePermissionUserManager(userId).startPackageOneTimeSession(packageName,
-                    timeoutMillis, revokeAfterKilledDelayMillis, importanceToResetTimer,
-                    importanceToKeepSessionAlive);
+                    timeoutMillis, revokeAfterKilledDelayMillis);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -406,6 +405,8 @@
     @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS)
     @Override
     public void stopOneTimePermissionSession(String packageName, @UserIdInt int userId) {
+        super.stopOneTimePermissionSession_enforcePermission();
+
         Objects.requireNonNull(packageName);
 
         final long token = Binder.clearCallingIdentity();
@@ -1100,7 +1101,7 @@
                     if (resolvedPackageName == null) {
                         return;
                     }
-                    appOpsManager.finishOp(accessorSource.getToken(), op,
+                    appOpsManager.finishOp(attributionSourceState.token, op,
                             accessorSource.getUid(), resolvedPackageName,
                             accessorSource.getAttributionTag());
                 } else {
@@ -1109,8 +1110,9 @@
                     if (resolvedAttributionSource.getPackageName() == null) {
                         return;
                     }
-                    appOpsManager.finishProxyOp(AppOpsManager.opToPublicName(op),
-                            resolvedAttributionSource, skipCurrentFinish);
+                    appOpsManager.finishProxyOp(attributionSourceState.token,
+                            AppOpsManager.opToPublicName(op), resolvedAttributionSource,
+                            skipCurrentFinish);
                 }
                 RegisteredAttribution registered =
                         sRunningAttributionSources.remove(current.getToken());
@@ -1156,7 +1158,8 @@
             if (permissionInfo == null) {
                 try {
                     permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0);
-                    if (PLATFORM_PACKAGE_NAME.equals(permissionInfo.packageName)) {
+                    if (PLATFORM_PACKAGE_NAME.equals(permissionInfo.packageName)
+                            || HealthConnectManager.isHealthPermission(context, permission)) {
                         // Double addition due to concurrency is fine - the backing
                         // store is concurrent.
                         sPlatformPermissions.put(permission, permissionInfo);
@@ -1225,10 +1228,11 @@
                         && next.getNext() == null);
                 final boolean selfAccess = singleReceiverFromDatasource || next == null;
 
-                final int opMode = performOpTransaction(context, op, current, message,
-                        forDataDelivery, /*startDataDelivery*/ false, skipCurrentChecks,
-                        selfAccess, singleReceiverFromDatasource, AppOpsManager.OP_NONE,
-                        AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE,
+                final int opMode = performOpTransaction(context, attributionSource.getToken(), op,
+                        current, message, forDataDelivery, /*startDataDelivery*/ false,
+                        skipCurrentChecks, selfAccess, singleReceiverFromDatasource,
+                        AppOpsManager.OP_NONE, AppOpsManager.ATTRIBUTION_FLAGS_NONE,
+                        AppOpsManager.ATTRIBUTION_FLAGS_NONE,
                         AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
 
                 switch (opMode) {
@@ -1331,10 +1335,10 @@
                         attributionSource, next, fromDatasource, startDataDelivery, selfAccess,
                         isLinkTrusted) : ATTRIBUTION_FLAGS_NONE;
 
-                final int opMode = performOpTransaction(context, op, current, message,
-                        forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess,
-                        singleReceiverFromDatasource, attributedOp, proxyAttributionFlags,
-                        proxiedAttributionFlags, attributionChainId);
+                final int opMode = performOpTransaction(context, attributionSource.getToken(), op,
+                        current, message, forDataDelivery, startDataDelivery, skipCurrentChecks,
+                        selfAccess, singleReceiverFromDatasource, attributedOp,
+                        proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
 
                 switch (opMode) {
                     case AppOpsManager.MODE_ERRORED: {
@@ -1479,8 +1483,8 @@
                         attributionSource, next, /*fromDatasource*/ false, startDataDelivery,
                         selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE;
 
-                final int opMode = performOpTransaction(context, op, current, message,
-                        forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess,
+                final int opMode = performOpTransaction(context, current.getToken(), op, current,
+                        message, forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess,
                         /*fromDatasource*/ false, AppOpsManager.OP_NONE, proxyAttributionFlags,
                         proxiedAttributionFlags, attributionChainId);
 
@@ -1502,7 +1506,8 @@
         }
 
         @SuppressWarnings("ConstantConditions")
-        private static int performOpTransaction(@NonNull Context context, int op,
+        private static int performOpTransaction(@NonNull Context context,
+                @NonNull IBinder chainStartToken, int op,
                 @NonNull AttributionSource attributionSource, @Nullable String message,
                 boolean forDataDelivery, boolean startDataDelivery, boolean skipProxyOperation,
                 boolean selfAccess, boolean singleReceiverFromDatasource, int attributedOp,
@@ -1564,7 +1569,7 @@
                 if (selfAccess) {
                     try {
                         startedOpResult = appOpsManager.startOpNoThrow(
-                                resolvedAttributionSource.getToken(), startedOp,
+                                chainStartToken, startedOp,
                                 resolvedAttributionSource.getUid(),
                                 resolvedAttributionSource.getPackageName(),
                                 /*startIfModeDefault*/ false,
@@ -1575,14 +1580,14 @@
                                 + " platform defined runtime permission "
                                 + AppOpsManager.opToPermission(op) + " while not having "
                                 + Manifest.permission.UPDATE_APP_OPS_STATS);
-                        startedOpResult = appOpsManager.startProxyOpNoThrow(attributedOp,
-                                attributionSource, message, skipProxyOperation,
+                        startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken,
+                                attributedOp, attributionSource, message, skipProxyOperation,
                                 proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
                     }
                 } else {
                     try {
-                        startedOpResult = appOpsManager.startProxyOpNoThrow(startedOp,
-                                resolvedAttributionSource, message, skipProxyOperation,
+                        startedOpResult = appOpsManager.startProxyOpNoThrow(chainStartToken,
+                                startedOp, resolvedAttributionSource, message, skipProxyOperation,
                                 proxyAttributionFlags, proxiedAttributionFlags, attributionChainId);
                     } catch (SecurityException e) {
                         //TODO 195339480: remove
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 c81a3ee..ab223ef 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -649,8 +649,8 @@
             Permission bp = mRegistry.getPermission(info.name);
             added = bp == null;
             int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel);
+            enforcePermissionCapLocked(info, tree);
             if (added) {
-                enforcePermissionCapLocked(info, tree);
                 bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC);
             } else if (!bp.isDynamic()) {
                 throw new SecurityException("Not allowed to modify non-dynamic permission "
@@ -2156,6 +2156,46 @@
     }
 
     /**
+     * If the package was below api 23, got the SYSTEM_ALERT_WINDOW permission automatically, and
+     * then updated past api 23, and the app does not satisfy any of the other SAW permission flags,
+     * the permission should be revoked.
+     *
+     * @param newPackage The new package that was installed
+     * @param oldPackage The old package that was updated
+     */
+    private void revokeSystemAlertWindowIfUpgradedPast23(
+            @NonNull AndroidPackage newPackage,
+            @NonNull AndroidPackage oldPackage) {
+        if (oldPackage.getTargetSdkVersion() >= Build.VERSION_CODES.M
+                || newPackage.getTargetSdkVersion() < Build.VERSION_CODES.M
+                || !newPackage.getRequestedPermissions()
+                .contains(Manifest.permission.SYSTEM_ALERT_WINDOW)) {
+            return;
+        }
+
+        Permission saw;
+        synchronized (mLock) {
+            saw = mRegistry.getPermission(Manifest.permission.SYSTEM_ALERT_WINDOW);
+        }
+        final PackageStateInternal ps =
+                mPackageManagerInt.getPackageStateInternal(newPackage.getPackageName());
+        if (shouldGrantPermissionByProtectionFlags(newPackage, ps, saw, new ArraySet<>())
+                || shouldGrantPermissionBySignature(newPackage, saw)) {
+            return;
+        }
+        for (int userId : getAllUserIds()) {
+            try {
+                revokePermissionFromPackageForUser(newPackage.getPackageName(),
+                        Manifest.permission.SYSTEM_ALERT_WINDOW, false, userId,
+                        mDefaultPermissionCallback);
+            } catch (IllegalStateException | SecurityException e) {
+                Log.e(TAG, "unable to revoke SYSTEM_ALERT_WINDOW for "
+                        + newPackage.getPackageName() + " user " + userId, e);
+            }
+        }
+    }
+
+    /**
      * We might auto-grant permissions if any permission of the group is already granted. Hence if
      * the group of a granted permission changes we need to revoke it to avoid having permissions of
      * the new group auto-granted.
@@ -4205,7 +4245,6 @@
         }
         boolean changed = false;
 
-        Set<Permission> needsUpdate = null;
         synchronized (mLock) {
             final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator();
             while (it.hasNext()) {
@@ -4224,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;
@@ -4691,6 +4710,7 @@
                 if (hasOldPkg) {
                     revokeRuntimePermissionsIfGroupChangedInternal(pkg, oldPkg);
                     revokeStoragePermissionsIfScopeExpandedInternal(pkg, oldPkg);
+                    revokeSystemAlertWindowIfUpgradedPast23(pkg, oldPkg);
                 }
                 if (hasPermissionDefinitionChanges) {
                     revokeRuntimePermissionsIfPermissionDefinitionChangedInternal(
diff --git a/services/core/java/com/android/server/pm/pkg/SuspendParams.java b/services/core/java/com/android/server/pm/pkg/SuspendParams.java
index 0926ba2..dc48a33 100644
--- a/services/core/java/com/android/server/pm/pkg/SuspendParams.java
+++ b/services/core/java/com/android/server/pm/pkg/SuspendParams.java
@@ -21,8 +21,9 @@
 import android.os.BaseBundle;
 import android.os.PersistableBundle;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
index 0320818..e019215 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
@@ -96,4 +96,10 @@
     ActivityInfo.WindowLayout getWindowLayout();
 
     boolean isSupportsSizeChanges();
+
+    /**
+     * Gets the category of the target display this activity is supposed to run on.
+     */
+    @Nullable
+    String getTargetDisplayCategory();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
index aebe133..278e547 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
@@ -96,6 +96,9 @@
     @Nullable
     private ActivityInfo.WindowLayout windowLayout;
 
+    @Nullable
+    private String mTargetDisplayCategory;
+
     public ParsedActivityImpl(ParsedActivityImpl other) {
         super(other);
         this.theme = other.theme;
@@ -122,6 +125,7 @@
         this.colorMode = other.colorMode;
         this.windowLayout = other.windowLayout;
         this.mKnownActivityEmbeddingCerts = other.mKnownActivityEmbeddingCerts;
+        this.mTargetDisplayCategory = other.mTargetDisplayCategory;
     }
 
     /**
@@ -189,6 +193,7 @@
         alias.requestedVrComponent = target.getRequestedVrComponent();
         alias.setDirectBootAware(target.isDirectBootAware());
         alias.setProcessName(target.getProcessName());
+        alias.setTargetDisplayCategory(target.getTargetDisplayCategory());
         return alias;
 
         // Not all attributes from the target ParsedActivity are copied to the alias.
@@ -316,6 +321,7 @@
             dest.writeBoolean(false);
         }
         sForStringSet.parcel(this.mKnownActivityEmbeddingCerts, dest, flags);
+        dest.writeString8(this.mTargetDisplayCategory);
     }
 
     public ParsedActivityImpl() {
@@ -350,6 +356,7 @@
             windowLayout = new ActivityInfo.WindowLayout(in);
         }
         this.mKnownActivityEmbeddingCerts = sForStringSet.unparcel(in);
+        this.mTargetDisplayCategory = in.readString8();
     }
 
     @NonNull
@@ -406,7 +413,8 @@
             @Nullable String requestedVrComponent,
             int rotationAnimation,
             int colorMode,
-            @Nullable ActivityInfo.WindowLayout windowLayout) {
+            @Nullable ActivityInfo.WindowLayout windowLayout,
+            @Nullable String targetDisplayCategory) {
         this.theme = theme;
         this.uiOptions = uiOptions;
         this.targetActivity = targetActivity;
@@ -431,6 +439,7 @@
         this.rotationAnimation = rotationAnimation;
         this.colorMode = colorMode;
         this.windowLayout = windowLayout;
+        this.mTargetDisplayCategory = targetDisplayCategory;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -551,6 +560,11 @@
     }
 
     @DataClass.Generated.Member
+    public @Nullable String getTargetDisplayCategory() {
+        return mTargetDisplayCategory;
+    }
+
+    @DataClass.Generated.Member
     public @NonNull ParsedActivityImpl setTheme( int value) {
         theme = value;
         return this;
@@ -676,11 +690,17 @@
         return this;
     }
 
+    @DataClass.Generated.Member
+    public @NonNull ParsedActivityImpl setTargetDisplayCategory(@NonNull String value) {
+        mTargetDisplayCategory = value;
+        return this;
+    }
+
     @DataClass.Generated(
-            time = 1644372875433L,
+            time = 1664805688714L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java",
-            inputSignatures = "private  int theme\nprivate  int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate  int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate  int launchMode\nprivate  int documentLaunchMode\nprivate  int maxRecents\nprivate  int configChanges\nprivate  int softInputMode\nprivate  int persistableMode\nprivate  int lockTaskLaunchMode\nprivate  int screenOrientation\nprivate  int resizeMode\nprivate  float maxAspectRatio\nprivate  float minAspectRatio\nprivate  boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate  int rotationAnimation\nprivate  int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.server.pm.pkg.component.ParsedActivityImpl> CREATOR\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.server.pm.pkg.component.ParsedActivity)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic  void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic  java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.server.pm.pkg.component.ParsedMainComponentImpl implements [com.android.server.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
+            inputSignatures = "private  int theme\nprivate  int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate  int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate  int launchMode\nprivate  int documentLaunchMode\nprivate  int maxRecents\nprivate  int configChanges\nprivate  int softInputMode\nprivate  int persistableMode\nprivate  int lockTaskLaunchMode\nprivate  int screenOrientation\nprivate  int resizeMode\nprivate  float maxAspectRatio\nprivate  float minAspectRatio\nprivate  boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate  int rotationAnimation\nprivate  int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mTargetDisplayCategory\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.server.pm.pkg.component.ParsedActivityImpl> CREATOR\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.server.pm.pkg.component.ParsedActivity)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic  void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic  java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.server.pm.pkg.component.ParsedMainComponentImpl implements [com.android.server.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
index bbbf598..305062b 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
+import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.content.pm.parsing.result.ParseInput;
 import android.content.pm.parsing.result.ParseInput.DeferredError;
 import android.content.pm.parsing.result.ParseResult;
@@ -219,6 +220,18 @@
                 pkg.setVisibleToInstantApps(true);
             }
 
+            String targetDisplayCategory = sa.getNonConfigurationString(
+                    R.styleable.AndroidManifestActivity_targetDisplayCategory, 0);
+
+            if (targetDisplayCategory != null
+                    && FrameworkParsingPackageUtils.validateName(targetDisplayCategory,
+                    false /* requireSeparator */, false /* requireFilename */) != null) {
+                return input.error("targetDisplayCategory attribute can only consists of "
+                        + "alphanumeric characters, '_', and '.'");
+            }
+
+            activity.setTargetDisplayCategory(targetDisplayCategory);
+
             return parseActivityOrAlias(activity, pkg, tag, parser, res, sa, receiver,
                     false /*isAlias*/, visibleToEphemeral, input,
                     R.styleable.AndroidManifestActivity_parentActivityName,
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
index 4bad102..9fb8297 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
@@ -23,10 +23,10 @@
 import android.content.pm.PackageManager;
 import android.util.ArrayMap;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.SettingsXml;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
index 1714086..53ee189 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
@@ -33,9 +33,9 @@
 import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Computer;
 import com.android.server.pm.PackageSetting;
 import com.android.server.pm.Settings;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
index e803457..ac6d795 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
@@ -27,9 +27,9 @@
 import android.util.ArraySet;
 import android.util.PackageUtils;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.SettingsXml;
 import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState;
 import com.android.server.pm.verify.domain.models.DomainVerificationPkgState;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 400af36..f80ead6 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -46,11 +46,11 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.CollectionUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.compat.PlatformCompat;
@@ -1209,6 +1209,7 @@
     public void printOwnersForPackage(@NonNull IndentingPrintWriter writer,
             @Nullable String packageName, @Nullable @UserIdInt Integer userId)
             throws NameNotFoundException {
+        mEnforcer.assertApprovedQuerent(mConnection.getCallingUid(), mProxy);
         final Computer snapshot = mConnection.snapshot();
         synchronized (mLock) {
             if (packageName == null) {
@@ -1257,6 +1258,7 @@
     @Override
     public void printOwnersForDomains(@NonNull IndentingPrintWriter writer,
             @NonNull List<String> domains, @Nullable @UserIdInt Integer userId) {
+        mEnforcer.assertApprovedQuerent(mConnection.getCallingUid(), mProxy);
         final Computer snapshot = mConnection.snapshot();
         synchronized (mLock) {
             int size = domains.size();
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
index cde72cd..d256830 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
@@ -23,11 +23,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Computer;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageStateInternal;
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index a6d148c..383249f 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -45,13 +45,11 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.function.DecFunction;
 import com.android.internal.util.function.HeptFunction;
 import com.android.internal.util.function.HexFunction;
 import com.android.internal.util.function.QuadFunction;
 import com.android.internal.util.function.QuintConsumer;
 import com.android.internal.util.function.QuintFunction;
-import com.android.internal.util.function.TriFunction;
 import com.android.internal.util.function.UndecFunction;
 import com.android.server.LocalServices;
 
@@ -257,14 +255,14 @@
     }
 
     @Override
-    public SyncNotedAppOp startProxyOperation(int code,
+    public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code,
             @NonNull AttributionSource attributionSource, boolean startIfModeDefault,
             boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage,
             boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags,
             @AttributionFlags int proxiedAttributionFlags, int attributionChainId,
-            @NonNull DecFunction<Integer, AttributionSource, Boolean, Boolean, String, Boolean,
-                    Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) {
-        return superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(),
+            @NonNull UndecFunction<IBinder, Integer, AttributionSource, Boolean, Boolean, String,
+                    Boolean, Boolean, Integer, Integer, Integer, SyncNotedAppOp> superImpl) {
+        return superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(),
                 attributionSource.getPackageName(), attributionSource.getAttributionTag()),
                 attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message,
                 shouldCollectMessage, skipProxyOperation, proxyAttributionFlags,
@@ -280,10 +278,10 @@
     }
 
     @Override
-    public void finishProxyOperation(int code, @NonNull AttributionSource attributionSource,
-            boolean skipProxyOperation, @NonNull TriFunction<Integer, AttributionSource,
-            Boolean, Void> superImpl) {
-        superImpl.apply(resolveDatasourceOp(code, attributionSource.getUid(),
+    public void finishProxyOperation(@NonNull IBinder clientId, int code,
+            @NonNull AttributionSource attributionSource, boolean skipProxyOperation,
+            @NonNull QuadFunction<IBinder, Integer, AttributionSource, Boolean, Void> superImpl) {
+        superImpl.apply(clientId, resolveDatasourceOp(code, attributionSource.getUid(),
                 attributionSource.getPackageName(), attributionSource.getAttributionTag()),
                 attributionSource, skipProxyOperation);
     }
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index ffb652e..e61effa 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -58,6 +58,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.PackageManagerInternal.PackageListObserver;
 import android.content.pm.PermissionInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.os.Build;
 import android.os.Bundle;
@@ -78,7 +79,6 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.LongSparseLongArray;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 
@@ -146,7 +146,7 @@
      * scheduled for a package/user.
      */
     @GuardedBy("mLock")
-    private final ArraySet<Pair<String, Integer>> mIsPackageSyncsScheduled = new ArraySet<>();
+    private final ArraySet<UserPackage> mIsPackageSyncsScheduled = new ArraySet<>();
 
     /**
      * Whether an async {@link #resetAppOpPermissionsIfNotRequestedForUid} is currently
@@ -223,9 +223,11 @@
                 this::synchronizePackagePermissionsAndAppOpsAsyncForUser);
 
         mAppOpsCallback = new IAppOpsCallback.Stub() {
-            public void opChanged(int op, int uid, String packageName) {
-                synchronizePackagePermissionsAndAppOpsAsyncForUser(packageName,
-                        UserHandle.getUserId(uid));
+            public void opChanged(int op, int uid, @Nullable String packageName) {
+                if (packageName != null) {
+                    synchronizePackagePermissionsAndAppOpsAsyncForUser(packageName,
+                            UserHandle.getUserId(uid));
+                }
                 resetAppOpPermissionsIfNotRequestedForUidAsync(uid);
             }
         };
@@ -372,7 +374,7 @@
             @UserIdInt int changedUserId) {
         if (isStarted(changedUserId)) {
             synchronized (mLock) {
-                if (mIsPackageSyncsScheduled.add(new Pair<>(packageName, changedUserId))) {
+                if (mIsPackageSyncsScheduled.add(UserPackage.of(changedUserId, packageName))) {
                     // TODO(b/165030092): migrate this to PermissionThread.getHandler().
                     // synchronizePackagePermissionsAndAppOpsForUser is a heavy operation.
                     // Dispatched on a PermissionThread, it interferes with user switch.
@@ -640,7 +642,7 @@
     private void synchronizePackagePermissionsAndAppOpsForUser(@NonNull String packageName,
             @UserIdInt int userId) {
         synchronized (mLock) {
-            mIsPackageSyncsScheduled.remove(new Pair<>(packageName, userId));
+            mIsPackageSyncsScheduled.remove(UserPackage.of(userId, packageName));
         }
 
         if (DEBUG) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index ae99806..3aa333a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -114,6 +114,7 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Rect;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.hdmi.HdmiAudioSystemClient;
@@ -255,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;
@@ -391,6 +393,7 @@
     IStatusBarService mStatusBarService;
     StatusBarManagerInternal mStatusBarManagerInternal;
     AudioManagerInternal mAudioManagerInternal;
+    SensorPrivacyManager mSensorPrivacyManager;
     DisplayManager mDisplayManager;
     DisplayManagerInternal mDisplayManagerInternal;
     boolean mPreloadedRecentApps;
@@ -967,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");
@@ -1016,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
@@ -1591,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;
         }
@@ -1912,6 +1951,7 @@
         mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mPackageManager = mContext.getPackageManager();
@@ -2526,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();
@@ -2999,8 +3048,6 @@
                 if ((metaState & KeyEvent.META_META_MASK) == 0) {
                     return key_not_consumed;
                 }
-                // Share the same behavior with KEYCODE_LANGUAGE_SWITCH.
-            case KeyEvent.KEYCODE_LANGUAGE_SWITCH:
                 if (down && repeatCount == 0) {
                     int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
                     mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);
@@ -3081,6 +3128,18 @@
         return key_not_consumed;
     }
 
+    private void toggleMicrophoneMuteFromKey() {
+        if (mSensorPrivacyManager.supportsSensorToggle(
+                SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                SensorPrivacyManager.Sensors.MICROPHONE)) {
+            boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
+                    SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                    SensorPrivacyManager.Sensors.MICROPHONE);
+            mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE,
+                    !isEnabled);
+        }
+    }
+
     /**
      * TV only: recognizes a remote control gesture for capturing a bug report.
      */
@@ -3530,7 +3589,12 @@
                     @Override
                     public void onKeyguardExitResult(boolean success) {
                         if (success) {
-                            startDockOrHome(displayId, true /*fromHomeKey*/, awakenFromDreams);
+                            final long origId = Binder.clearCallingIdentity();
+                            try {
+                                startDockOrHome(displayId, true /*fromHomeKey*/, awakenFromDreams);
+                            } finally {
+                                Binder.restoreCallingIdentity(origId);
+                            }
                         }
                     }
                 });
@@ -4013,11 +4077,16 @@
                 break;
             }
 
+            case KeyEvent.KEYCODE_MUTE:
+                result &= ~ACTION_PASS_TO_USER;
+                if (down && event.getRepeatCount() == 0) {
+                    toggleMicrophoneMuteFromKey();
+                }
+                break;
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
@@ -4113,6 +4182,9 @@
             case KeyEvent.KEYCODE_DEMO_APP_2:
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4: {
+                // TODO(b/254604589): Dispatch KeyEvent to System UI.
+                sendSystemKeyToStatusBarAsync(keyCode);
+
                 // Just drop if keys are not intercepted for direct key.
                 result &= ~ACTION_PASS_TO_USER;
                 break;
@@ -4195,7 +4267,9 @@
         if (mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
             // Wake device up early to prevent display doing redundant turning off/on stuff.
-            wakeUpFromPowerKey(event.getDownTime());
+            wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
+                    PowerManager.WAKE_REASON_CAMERA_LAUNCH,
+                    "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
         }
         return true;
     }
@@ -4728,11 +4802,6 @@
             }
             mDefaultDisplayRotation.updateOrientationListener();
             reportScreenStateToVrManager(false);
-            if (mCameraGestureTriggeredDuringGoingToSleep) {
-                wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
-                        PowerManager.WAKE_REASON_CAMERA_LAUNCH,
-                        "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
index 8582f54..2d76c50 100644
--- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java
+++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
@@ -127,7 +127,7 @@
      */
     public void notifyPowerPressed() {
         Log.i(TAG, "notifyPowerPressed");
-        if (mFingerprintManager == null) {
+        if (mFingerprintManager == null && mSideFpsEventHandlerReady.get()) {
             mFingerprintManager = mContext.getSystemService(FingerprintManager.class);
         }
         if (mFingerprintManager == null) {
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 69fb22c..1fe82f4 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -22,8 +22,8 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.trust.TrustManager;
-import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerInternal;
@@ -796,18 +796,19 @@
         }
 
         if (mActivityManagerInternal.isSystemReady()) {
-            mContext.sendOrderedBroadcastAsUser(mScreenOnIntent, UserHandle.ALL, null,
-                    AppOpsManager.OP_NONE, mScreenOnOptions, mWakeUpBroadcastDone, mHandler,
-                    0, null, null);
+            final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
+            mActivityManagerInternal.broadcastIntent(mScreenOnIntent, mWakeUpBroadcastDone,
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
             sendNextBroadcast();
         }
     }
 
-    private final BroadcastReceiver mWakeUpBroadcastDone = new BroadcastReceiver() {
+    private final IIntentReceiver mWakeUpBroadcastDone = new IIntentReceiver.Stub() {
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+                boolean ordered, boolean sticky, int sendingUser) {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 1,
                     SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
             sendNextBroadcast();
@@ -820,18 +821,19 @@
         }
 
         if (mActivityManagerInternal.isSystemReady()) {
-            mContext.sendOrderedBroadcastAsUser(mScreenOffIntent, UserHandle.ALL, null,
-                    AppOpsManager.OP_NONE, mScreenOffOptions, mGoToSleepBroadcastDone, mHandler,
-                    0, null, null);
+            final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
+            mActivityManagerInternal.broadcastIntent(mScreenOffIntent, mGoToSleepBroadcastDone,
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOffOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
             sendNextBroadcast();
         }
     }
 
-    private final BroadcastReceiver mGoToSleepBroadcastDone = new BroadcastReceiver() {
+    private final IIntentReceiver mGoToSleepBroadcastDone = new IIntentReceiver.Stub() {
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+                boolean ordered, boolean sticky, int sendingUser) {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 0,
                     SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
             sendNextBroadcast();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 5abc875..cc84c85 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -42,8 +42,6 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.SynchronousUserSwitchObserver;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -64,7 +62,6 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatterySaverPolicyConfig;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
@@ -127,7 +124,6 @@
 import com.android.server.UserspaceRebootLogger;
 import com.android.server.Watchdog;
 import com.android.server.am.BatteryStatsService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LogicalLight;
 import com.android.server.policy.WindowManagerPolicy;
@@ -284,17 +280,6 @@
      */
     private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L;
 
-    /**
-     * Apps targeting Android U and above need to define
-     * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for
-     * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect.
-     * Note that most applications should use {@link android.R.attr#turnScreenOn} or
-     * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the
-     * previous foreground app from being resumed first when the screen turns on.
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L;
     /** Reason ID for holding display suspend blocker. */
     private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display";
 
@@ -318,7 +303,6 @@
     private final SystemPropertiesWrapper mSystemProperties;
     private final Clock mClock;
     private final Injector mInjector;
-    private final PlatformCompat mPlatformCompat;
 
     private AppOpsManager mAppOpsManager;
     private LightsManager mLightsManager;
@@ -1012,11 +996,6 @@
                 public void set(String key, String val) {
                     SystemProperties.set(key, val);
                 }
-
-                @Override
-                public boolean getBoolean(String key, boolean def) {
-                    return SystemProperties.getBoolean(key, def);
-                }
             };
         }
 
@@ -1053,10 +1032,6 @@
         AppOpsManager createAppOpsManager(Context context) {
             return context.getSystemService(AppOpsManager.class);
         }
-
-        PlatformCompat createPlatformCompat(Context context) {
-            return context.getSystemService(PlatformCompat.class);
-        }
     }
 
     final Constants mConstants;
@@ -1078,7 +1053,7 @@
         super(context);
 
         mContext = context;
-        mBinderService = new BinderService();
+        mBinderService = new BinderService(mContext);
         mLocalService = new LocalService();
         mNativeWrapper = injector.createNativeWrapper();
         mSystemProperties = injector.createSystemPropertiesWrapper();
@@ -1114,8 +1089,6 @@
 
         mAppOpsManager = injector.createAppOpsManager(mContext);
 
-        mPlatformCompat = injector.createPlatformCompat(mContext);
-
         mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener();
 
         // Save brightness values:
@@ -1626,28 +1599,14 @@
         }
         if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName)
                 == AppOpsManager.MODE_ALLOWED) {
-            if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION,
-                    opPackageName, UserHandle.getUserId(opUid))) {
-                if (mContext.checkCallingOrSelfPermission(
-                        android.Manifest.permission.TURN_SCREEN_ON)
-                        == PackageManager.PERMISSION_GRANTED) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "Allowing device wake-up from app " + opPackageName);
-                    }
-                    return true;
-                }
-            } else {
-                // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only
-                // check for appOp for apps targeting lower SDK versions
-                if (DEBUG_SPEW) {
-                    Slog.d(TAG, "Allowing device wake-up from app with "
-                            + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName);
-                }
+            if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON)
+                    == PackageManager.PERMISSION_GRANTED) {
+                Slog.i(TAG, "Allowing device wake-up from app " + opPackageName);
                 return true;
             }
         }
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
-            Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON");
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
+            Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on");
             return true;
         }
         Slog.w(TAG, "Not allowing device wake-up for " + opPackageName);
@@ -5526,12 +5485,17 @@
 
     @VisibleForTesting
     final class BinderService extends IPowerManager.Stub {
+        private final PowerManagerShellCommand mShellCommand;
+
+        BinderService(Context context) {
+            mShellCommand = new PowerManagerShellCommand(context, this);
+        }
+
         @Override
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
-            (new PowerManagerShellCommand(this)).exec(
-                    this, in, out, err, args, callback, resultReceiver);
+            mShellCommand.exec(this, in, out, err, args, callback, resultReceiver);
         }
 
         @Override // Binder call
@@ -6768,6 +6732,11 @@
         public void nap(long eventTime, boolean allowWake) {
             napInternal(eventTime, Process.SYSTEM_UID, allowWake);
         }
+
+        @Override
+        public boolean isAmbientDisplaySuppressed() {
+            return mAmbientDisplaySuppressionController.isSuppressed();
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/power/PowerManagerShellCommand.java b/services/core/java/com/android/server/power/PowerManagerShellCommand.java
index a9b33ed..9439b76 100644
--- a/services/core/java/com/android/server/power/PowerManagerShellCommand.java
+++ b/services/core/java/com/android/server/power/PowerManagerShellCommand.java
@@ -16,10 +16,15 @@
 
 package com.android.server.power;
 
+import android.content.Context;
 import android.content.Intent;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
 import android.os.ShellCommand;
+import android.util.SparseArray;
+import android.view.Display;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -27,9 +32,13 @@
 class PowerManagerShellCommand extends ShellCommand {
     private static final int LOW_POWER_MODE_ON = 1;
 
-    final PowerManagerService.BinderService mService;
+    private final Context mContext;
+    private final PowerManagerService.BinderService mService;
 
-    PowerManagerShellCommand(PowerManagerService.BinderService service) {
+    private SparseArray<WakeLock> mProxWakelocks = new SparseArray<>();
+
+    PowerManagerShellCommand(Context context, PowerManagerService.BinderService service) {
+        mContext = context;
         mService = service;
     }
 
@@ -52,6 +61,8 @@
                     return runSuppressAmbientDisplay();
                 case "list-ambient-display-suppression-tokens":
                     return runListAmbientDisplaySuppressionTokens();
+                case "set-prox":
+                    return runSetProx();
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -117,6 +128,56 @@
 
         return 0;
     }
+
+    /** TODO: Consider updating this code to support all wakelock types. */
+    private int runSetProx() throws RemoteException {
+        PrintWriter pw = getOutPrintWriter();
+        final boolean acquire;
+        switch (getNextArgRequired().toLowerCase()) {
+            case "list":
+                pw.println("Wakelocks:");
+                pw.println(mProxWakelocks);
+                return 0;
+            case "acquire":
+                acquire = true;
+                break;
+            case "release":
+                acquire = false;
+                break;
+            default:
+                pw.println("Error: Allowed options are 'list' 'enable' and 'disable'.");
+                return -1;
+        }
+
+        int displayId = Display.INVALID_DISPLAY;
+        String displayOption = getNextArg();
+        if ("-d".equals(displayOption)) {
+            String idStr = getNextArg();
+            displayId = Integer.parseInt(idStr);
+            if (displayId < 0) {
+                pw.println("Error: Specified displayId (" + idStr + ") must a non-negative int.");
+                return -1;
+            }
+        }
+
+        int wakelockIndex = displayId + 1; // SparseArray doesn't support negative indexes
+        WakeLock wakelock = mProxWakelocks.get(wakelockIndex);
+        if (wakelock == null) {
+            PowerManager pm = mContext.getSystemService(PowerManager.class);
+            wakelock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
+                        "PowerManagerShellCommand[" + displayId + "]", displayId);
+            mProxWakelocks.put(wakelockIndex, wakelock);
+        }
+
+        if (acquire) {
+            wakelock.acquire();
+        } else {
+            wakelock.release();
+        }
+        pw.println(wakelock);
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -138,6 +199,11 @@
         pw.println("    ambient display");
         pw.println("  list-ambient-display-suppression-tokens");
         pw.println("    prints the tokens used to suppress ambient display");
+        pw.println("  set-prox [list|acquire|release] (-d <display_id>)");
+        pw.println("    Acquires the proximity sensor wakelock. Wakelock is associated with");
+        pw.println("    a specific display if specified. 'list' lists wakelocks previously");
+        pw.println("    created by set-prox including their held status.");
+
         pw.println();
         Intent.printIntentArgsHelp(pw , "");
     }
diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
index c68f9c6..1acf798 100644
--- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
+++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
@@ -48,19 +48,4 @@
      * SELinux. libc will log the underlying reason.
      */
     void set(@NonNull String key, @Nullable String val);
-
-    /**
-     * Get the value for the given {@code key}, returned as a boolean.
-     * Values 'n', 'no', '0', 'false' or 'off' are considered false.
-     * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
-     * (case sensitive).
-     * If the key does not exist, or has any other value, then the default
-     * result is returned.
-     *
-     * @param key the key to lookup
-     * @param def a default value to return
-     * @return the key parsed as a boolean, or def if the key isn't found or is
-     *         not able to be parsed as a boolean.
-     */
-    boolean getBoolean(@NonNull String key, boolean def);
 }
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index dfa1281..0d13831 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -24,6 +24,7 @@
 import android.os.IBinder;
 import android.os.IHintManager;
 import android.os.IHintSession;
+import android.os.PerformanceHintManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.ArrayMap;
@@ -147,6 +148,8 @@
         private static native void nativeReportActualWorkDuration(
                 long halPtr, long[] actualDurationNanos, long[] timeStampNanos);
 
+        private static native void nativeSendHint(long halPtr, int hint);
+
         private static native long nativeGetHintSessionPreferredRate();
 
         /** Wrapper for HintManager.nativeInit */
@@ -186,6 +189,11 @@
                     timeStampNanos);
         }
 
+        /** Wrapper for HintManager.sendHint */
+        public void halSendHint(long halPtr, int hint) {
+            nativeSendHint(halPtr, hint);
+        }
+
         /** Wrapper for HintManager.nativeGetHintSessionPreferredRate */
         public long halGetHintSessionPreferredRate() {
             return nativeGetHintSessionPreferredRate();
@@ -475,6 +483,18 @@
             }
         }
 
+        @Override
+        public void sendHint(@PerformanceHintManager.Session.Hint int hint) {
+            synchronized (mLock) {
+                if (mHalSessionPtr == 0 || !updateHintAllowed()) {
+                    return;
+                }
+                Preconditions.checkArgument(hint >= 0, "the hint ID the hint value should be"
+                        + " greater than zero.");
+                mNativeWrapper.halSendHint(mHalSessionPtr, hint);
+            }
+        }
+
         private void onProcStateChanged() {
             updateHintAllowed();
         }
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 1e5b498..0c5e451 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -99,8 +99,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -130,6 +128,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 
@@ -11507,6 +11507,9 @@
 
         mHistory.reset();
 
+        // Store the empty state to disk to ensure consistency
+        writeSyncLocked();
+
         // Flush external data, gathering snapshots, but don't process it since it is pre-reset data
         mIgnoreNextExternalStats = true;
         mExternalSync.scheduleSync("reset", ExternalStatsSync.UPDATE_ON_RESET);
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
index 50cb33c..0d7a140 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
@@ -25,11 +25,11 @@
 import android.util.Log;
 import android.util.LongArray;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java
index 5f76fbc..79e35c2 100644
--- a/services/core/java/com/android/server/power/stats/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/CpuWakeupStats.java
@@ -384,7 +384,7 @@
     private static final class Wakeup {
         private static final String PARSER_TAG = "CpuWakeupStats.Wakeup";
         private static final String ABORT_REASON_PREFIX = "Abort";
-        private static final Pattern sIrqPattern = Pattern.compile("(\\d+)\\s+(\\S+)");
+        private static final Pattern sIrqPattern = Pattern.compile("^(\\d+)\\s+(\\S+)");
 
         String mRawReason;
         long mElapsedMillis;
@@ -409,7 +409,7 @@
             IrqDevice[] parsedDevices = new IrqDevice[components.length];
 
             for (String component : components) {
-                final Matcher matcher = sIrqPattern.matcher(component);
+                final Matcher matcher = sIrqPattern.matcher(component.trim());
                 if (matcher.find()) {
                     final int line;
                     final String device;
diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md
index 0c5cc15..08800da 100644
--- a/services/core/java/com/android/server/rollback/README.md
+++ b/services/core/java/com/android/server/rollback/README.md
@@ -1,4 +1,4 @@
-#Rollback Manager
+# Rollback Manager
 
 ## Introduction
 
@@ -7,9 +7,9 @@
 APEX update to the previous version installed on the device, and reverting any
 APK or APEX data to the state it was in at the time of install.
 
-##Rollback Basics
+## Rollback Basics
 
-###How Rollbacks Work
+### How Rollbacks Work
 
 A new install parameter ENABLE_ROLLBACK can be specified to enable rollback when
 updating an application. For example:
@@ -42,27 +42,27 @@
 
 See below for more details of shell commands for rollback.
 
-###Rollback Triggers
+### Rollback Triggers
 
-####Manually Triggered Rollback
+#### Manually Triggered Rollback
 
 As mentioned above, it is possible to trigger rollback on device using a shell
 command. This is for testing purposes only. We do not expect this mechanism to
 be used in production in practice.
 
-####Watchdog Triggered Rollback
+#### Watchdog Triggered Rollback
 
 Watchdog triggered rollback is intended to address severe issues with the
 device. The platform provides several different watchdogs that can trigger
 rollback.
 
-#####Package Watchdog
+##### Package Watchdog
 
 There is a package watchdog service running on device that will trigger rollback
 of an update if there are 5 ANRs or process crashes within a 1 minute window for
 a package in the update.
 
-#####Native Watchdog
+##### Native Watchdog
 
 If a native service crashes repeatedly after an update is installed, rollback
 will be triggered. This particularly applies to updates that include APEXes
@@ -70,25 +70,25 @@
 native services have been affected by an update, *any* crashing native service
 will cause the rollback to be triggered.
 
-#####Explicit Health Check
+##### Explicit Health Check
 
 There is an explicit check to verify the network stack is functional after an
 update. If there is no network connectivity within a certain time period after
 an update, rollback is triggered.
 
-####Server Triggered Rollback
+#### Server Triggered Rollback
 The RollbackManager API may be used by the installer to roll back an update
 based on a request from the server.
 
-##Rollback Details
+## Rollback Details
 
-###RollbackManager API
+### RollbackManager API
 
 The RollbackManager API is an @SystemAPI guarded by the MANAGE_ROLLBACKS and
 TEST_MANAGE_ROLLBACKS permissions. See RollbackManager.java for details about
 the RollbackManager API.
 
-###Rollback of APEX modules
+### Rollback of APEX modules
 
 Rollback is supported for APEX modules in addition to APK modules. In Q, there
 was no concept of data associated with an APEX, so only the APEX itself is
@@ -100,7 +100,7 @@
 directories). For example, FooV2.apex must not change the file format of some
 state stored on the device in such a way that FooV1.apex cannot read the file.
 
-###Rollback of MultiPackage Installs
+### Rollback of MultiPackage Installs
 
 Rollback can be enabled for multi-package installs. This requires that all
 packages in the install session, including the parent session, have the
@@ -119,7 +119,7 @@
 install session, rollback will not be enabled for any package in the
 multi-package install session.
 
-###Rollback of Staged Installs
+### Rollback of Staged Installs
 
 Rollback can be enabled for staged installs, which require reboot to take
 effect. If reboot was required when the package was updated, then reboot is
@@ -127,21 +127,21 @@
 package was updated, then no reboot is required when the package is rolled back.
 
 
-###Rollbacks on Multi User Devices
+### Rollbacks on Multi User Devices
 
 Rollbacks should work properly on devices with multiple users. There is special
 handling of user data backup to ensure app user data is properly backed up and
 restored for all users, even for credential encrypted users that have not been
 unlocked at various points during the flow.
 
-###Rollback whitelist
+### Rollback whitelist
 
 Outside of testing, rollback may only be enabled for packages listed in the
 sysconfig rollback whitelist - see
 `SystemConfig#getRollbackWhitelistedPackages`. Attempts to enable rollback for
 non-whitelisted packages will fail.
 
-###Failure to Enable Rollback
+### Failure to Enable Rollback
 
 There are a number of reasons why we may be unable to enable rollback for a
 package, including:
@@ -158,13 +158,13 @@
 rollback enabled. Failing to enable rollback does not cause the installation to
 fail.
 
-###Failure to Commit Rollback
+### Failure to Commit Rollback
 
 For the most part, a rollback will remain available after failure to commit it.
 This allows the caller to retry the rollback if they have reason to believe it
 will not fail again the next time the commit of the rollback is attempted.
 
-###Installing Previously Rolled Back Packages
+### Installing Previously Rolled Back Packages
 There is no logic in the platform itself to prevent installing a version of a
 package that was previously rolled back.
 
@@ -175,7 +175,7 @@
 installer to prevent reinstall of a previously rolled back package version if so
 desired.
 
-###Rollback Expiration
+### Rollback Expiration
 
 An available rollback is expired if the rollback lifetime has been exceeded or
 if there is a new update to package associated with the rollback. When an
@@ -183,9 +183,9 @@
 the rollback are deleted. Once a rollback is expired, it can no longer be
 executed.
 
-##Shell Commands for Rollback
+## Shell Commands for Rollback
 
-###Installing an App with Rollback Enabled
+### Installing an App with Rollback Enabled
 
 The `adb install` command accepts the `--enable-rollback` flag to install an app
 with rollback enabled. For example:
@@ -194,7 +194,7 @@
 $ adb install --enable-rollback FooV2.apk
 ```
 
-###Triggering Rollback Manually
+### Triggering Rollback Manually
 
 If rollback is available for an application, the pm command can be used to
 trigger rollback manually on device:
@@ -206,7 +206,7 @@
 For rollback of staged installs, you have to manually reboot the device for the
 rollback to take effect after running the 'pm rollback-app' command.
 
-###Listing the Status of Rollbacks on Device
+### Listing the Status of Rollbacks on Device
 
 You can get a list with details about available and recently committed rollbacks
 using dumpsys. For example:
@@ -246,9 +246,9 @@
 The list of rollbacks is also included in bug reports. Search for "DUMP OF
 SERVICE rollback".
 
-##Configuration Properties
+## Configuration Properties
 
-###Rollback Lifetime
+### Rollback Lifetime
 
 Rollback lifetime refers to the maximum duration of time after the rollback is
 first enabled that it will be available. The default is for rollbacks to be
@@ -263,7 +263,7 @@
 
 The update will not take effect until after system server has been restarted.
 
-###Enable Rollback Timeout
+### Enable Rollback Timeout
 
 The enable rollback timeout is how long RollbackManager is allowed to take to
 enable rollback when performing an update. This includes the time needed to make
@@ -279,7 +279,7 @@
 
 The update will take effect for the next install with rollback enabled.
 
-##Limitations
+## Limitations
 
 * You cannot enable rollback for the first version of an application installed
 on the device. Only updates to a package previously installed on the device can
diff --git a/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
index f797f09..58b2443 100644
--- a/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
+++ b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
@@ -21,13 +21,13 @@
 import android.os.Handler;
 import android.util.AtomicFile;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/sensorprivacy/PersistedState.java b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
index e79efdb8..85ec101 100644
--- a/services/core/java/com/android/server/sensorprivacy/PersistedState.java
+++ b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
@@ -28,14 +28,14 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.QuadConsumer;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index c79bc89..ab35dc8 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -26,6 +26,7 @@
 import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
 import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
 import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -34,6 +35,7 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS;
 import static android.hardware.SensorPrivacyManager.EXTRA_SENSOR;
+import static android.hardware.SensorPrivacyManager.EXTRA_TOGGLE_TYPE;
 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE;
 import static android.hardware.SensorPrivacyManager.Sources.DIALOG;
@@ -81,6 +83,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
+import android.database.ContentObserver;
 import android.graphics.drawable.Icon;
 import android.hardware.ISensorPrivacyListener;
 import android.hardware.ISensorPrivacyManager;
@@ -199,6 +202,7 @@
         if (phase == PHASE_SYSTEM_SERVICES_READY) {
             mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
             mCallStateHelper = new CallStateHelper();
+            mSensorPrivacyServiceImpl.registerSettingsObserver();
         } else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
             mCameraPrivacyLightController = new CameraPrivacyLightController(mContext);
         }
@@ -271,7 +275,7 @@
             mSensorPrivacyStateController = SensorPrivacyStateController.getInstance();
 
             int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE,
-                    OP_CAMERA, OP_PHONE_CALL_CAMERA};
+                    OP_CAMERA, OP_PHONE_CALL_CAMERA, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO};
             mAppOpsManager.startWatchingNoted(micAndCameraOps, this);
             mAppOpsManager.startWatchingStarted(micAndCameraOps, this);
 
@@ -340,7 +344,8 @@
 
             int sensor;
             if (result == MODE_IGNORED) {
-                if (code == OP_RECORD_AUDIO || code == OP_PHONE_CALL_MICROPHONE) {
+                if (code == OP_RECORD_AUDIO || code == OP_PHONE_CALL_MICROPHONE
+                        || code == OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO) {
                     sensor = MICROPHONE;
                 } else if (code == OP_CAMERA || code == OP_PHONE_CALL_CAMERA) {
                     sensor = CAMERA;
@@ -660,6 +665,29 @@
                             .build());
         }
 
+        private void showSensorStateChangedActivity(@SensorPrivacyManager.Sensors.Sensor int sensor,
+                @SensorPrivacyManager.ToggleType int toggleType) {
+            String activityName = mContext.getResources().getString(
+                    R.string.config_sensorStateChangedActivity);
+            if (TextUtils.isEmpty(activityName)) {
+                return;
+            }
+
+            Intent dialogIntent = new Intent();
+            dialogIntent.setComponent(
+                    ComponentName.unflattenFromString(activityName));
+
+            ActivityOptions options = ActivityOptions.makeBasic();
+            options.setTaskOverlay(true, true);
+
+            dialogIntent.addFlags(
+                    FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | FLAG_ACTIVITY_NO_USER_ACTION);
+
+            dialogIntent.putExtra(EXTRA_SENSOR, sensor);
+            dialogIntent.putExtra(EXTRA_TOGGLE_TYPE, toggleType);
+            mContext.startActivityAsUser(dialogIntent, options.toBundle(), UserHandle.SYSTEM);
+        }
+
         private boolean isTelevision(Context context) {
             int uiMode = context.getResources().getConfiguration().uiMode;
             return (uiMode & Configuration.UI_MODE_TYPE_MASK)
@@ -1072,6 +1100,14 @@
                     // restrict it when the microphone is disabled
                     mAppOpsManagerInternal.setGlobalRestriction(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
                             enabled, mAppOpsRestrictionToken);
+
+                    // Set restriction for OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO
+                    boolean allowed = (Settings.Global.getInt(mContext.getContentResolver(),
+                            Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED, 1)
+                            == 1);
+                    mAppOpsManagerInternal.setGlobalRestriction(
+                            OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, enabled && !allowed,
+                            mAppOpsRestrictionToken);
                     break;
                 case CAMERA:
                     mAppOpsManagerInternal.setGlobalRestriction(OP_CAMERA, enabled,
@@ -1112,6 +1148,19 @@
             }
         }
 
+        private void registerSettingsObserver() {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(
+                            Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED),
+                    false, new ContentObserver(mHandler) {
+                        @Override
+                        public void onChange(boolean selfChange) {
+                            setGlobalRestriction(MICROPHONE,
+                                    isCombinedToggleSensorPrivacyEnabled(MICROPHONE));
+                        }
+                    });
+        }
+
         /**
          * A owner of a suppressor token died. Clean up.
          *
@@ -1353,6 +1402,8 @@
                     mToggleSensorListeners.finishBroadcast();
                 }
             }
+
+            mSensorPrivacyServiceImpl.showSensorStateChangedActivity(sensor, toggleType);
         }
 
         public void removeSuppressPackageReminderToken(Pair<Integer, UserHandle> key,
diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
index d9f504e..ac97038 100644
--- a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
+++ b/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java
@@ -206,16 +206,21 @@
                 Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo);
             }
 
+            mRebinder = null;
+
             Intent bindIntent = new Intent(mBoundServiceInfo.getAction()).setComponent(
                     mBoundServiceInfo.getComponentName());
-            if (!mContext.bindServiceAsUser(bindIntent, this,
-                    BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
-                    mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) {
-                Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
-                mRebinder = this::bind;
-                mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
-            } else {
-                mRebinder = null;
+            try {
+                if (!mContext.bindServiceAsUser(bindIntent, this,
+                        BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
+                        mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) {
+                    Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
+                    mRebinder = this::bind;
+                    mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
+                }
+            } catch (SecurityException e) {
+                // if anything goes wrong it shouldn't crash the system server
+                Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " bind failed", e);
             }
         }
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 4111446..43ffa81 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -18,11 +18,11 @@
 
 import android.annotation.Nullable;
 import android.app.ITransientNotificationCallback;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -158,7 +158,7 @@
     /** @see com.android.internal.statusbar.IStatusBar#onSystemBarAttributesChanged */
     void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
             AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-            @Behavior int behavior, InsetsVisibilities requestedVisibilities, String packageName,
+            @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName,
             LetterboxDetails[] letterboxDetails);
 
     /** @see com.android.internal.statusbar.IStatusBar#showTransient */
@@ -192,9 +192,10 @@
     void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable);
 
     /**
-     * Sets the system-wide listener for UDFPS HBM status changes.
+     * Sets the system-wide callback for UDFPS refresh rate changes.
      *
-     * @see com.android.internal.statusbar.IStatusBar#setUdfpsHbmListener(IUdfpsHbmListener)
+     * @see com.android.internal.statusbar.IStatusBar#setUdfpsRefreshRateCallback
+     * (IUdfpsRefreshRateRequestCallback)
      */
-    void setUdfpsHbmListener(IUdfpsHbmListener listener);
+    void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7ccf85f..50eab256 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -16,12 +16,15 @@
 
 package com.android.server.statusbar;
 
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
 import static android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS;
 import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE;
 import static android.app.StatusBarManager.NAV_BAR_MODE_DEFAULT;
 import static android.app.StatusBarManager.NAV_BAR_MODE_KIDS;
 import static android.app.StatusBarManager.NavBarMode;
 import static android.app.StatusBarManager.SessionFlags;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
 
@@ -53,7 +56,7 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
 import android.net.Uri;
@@ -80,7 +83,8 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
@@ -172,7 +176,7 @@
 
     private final SparseArray<UiState> mDisplayUiState = new SparseArray<>();
     @GuardedBy("mLock")
-    private IUdfpsHbmListener mUdfpsHbmListener;
+    private IUdfpsRefreshRateRequestCallback mUdfpsRefreshRateRequestCallback;
     @GuardedBy("mLock")
     private IBiometricContextListener mBiometricContextListener;
 
@@ -268,7 +272,6 @@
         mContext = context;
 
         LocalServices.addService(StatusBarManagerInternal.class, mInternalService);
-        LocalServices.addService(GlobalActionsProvider.class, mGlobalActionsProvider);
 
         // We always have a default display.
         final UiState state = new UiState();
@@ -285,6 +288,17 @@
         mSessionMonitor = new SessionMonitor(mContext);
     }
 
+    /**
+     * Publish the {@link GlobalActionsProvider}.
+     */
+    // TODO(b/259420401): investigate if we can extract GlobalActionsProvider to its own system
+    // service.
+    public void publishGlobalActionsProvider() {
+        if (LocalServices.getService(GlobalActionsProvider.class) == null) {
+            LocalServices.addService(GlobalActionsProvider.class, mGlobalActionsProvider);
+        }
+    }
+
     private IOverlayManager getOverlayManager() {
         // No need to synchronize; worst-case scenario it will be fetched twice.
         if (mOverlayManager == null) {
@@ -614,15 +628,15 @@
         @Override
         public void onSystemBarAttributesChanged(int displayId, @Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                @Behavior int behavior, @InsetsType int requestedVisibleTypes,
                 String packageName, LetterboxDetails[] letterboxDetails) {
             getUiState(displayId).setBarAttributes(appearance, appearanceRegions,
-                    navbarColorManagedByIme, behavior, requestedVisibilities, packageName,
+                    navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName,
                     letterboxDetails);
             if (mBar != null) {
                 try {
                     mBar.onSystemBarAttributesChanged(displayId, appearance, appearanceRegions,
-                            navbarColorManagedByIme, behavior, requestedVisibilities, packageName,
+                            navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName,
                             letterboxDetails);
                 } catch (RemoteException ex) { }
             }
@@ -691,13 +705,13 @@
         }
 
         @Override
-        public void setUdfpsHbmListener(IUdfpsHbmListener listener) {
+        public void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback) {
             synchronized (mLock) {
-                mUdfpsHbmListener = listener;
+                mUdfpsRefreshRateRequestCallback = callback;
             }
             if (mBar != null) {
                 try {
-                    mBar.setUdfpsHbmListener(listener);
+                    mBar.setUdfpsRefreshRateCallback(callback);
                 } catch (RemoteException ex) { }
             }
         }
@@ -941,11 +955,11 @@
     }
 
     @Override
-    public void setUdfpsHbmListener(IUdfpsHbmListener listener) {
+    public void setUdfpsRefreshRateCallback(IUdfpsRefreshRateRequestCallback callback) {
         enforceStatusBarService();
         if (mBar != null) {
             try {
-                mBar.setUdfpsHbmListener(listener);
+                mBar.setUdfpsRefreshRateCallback(callback);
             } catch (RemoteException ex) {
             }
         }
@@ -1208,7 +1222,7 @@
         private final ArraySet<Integer> mTransientBarTypes = new ArraySet<>();
         private boolean mNavbarColorManagedByIme = false;
         private @Behavior int mBehavior;
-        private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         private String mPackageName = "none";
         private int mDisabled1 = 0;
         private int mDisabled2 = 0;
@@ -1220,14 +1234,14 @@
 
         private void setBarAttributes(@Appearance int appearance,
                 AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme,
-                @Behavior int behavior, InsetsVisibilities requestedVisibilities,
+                @Behavior int behavior, @InsetsType int requestedVisibleTypes,
                 String packageName,
                 LetterboxDetails[] letterboxDetails) {
             mAppearance = appearance;
             mAppearanceRegions = appearanceRegions;
             mNavbarColorManagedByIme = navbarColorManagedByIme;
             mBehavior = behavior;
-            mRequestedVisibilities = requestedVisibilities;
+            mRequestedVisibleTypes = requestedVisibleTypes;
             mPackageName = packageName;
             mLetterboxDetails = letterboxDetails;
         }
@@ -1304,18 +1318,23 @@
                 "StatusBarManagerService");
     }
 
+    private boolean doesCallerHoldInteractAcrossUserPermission() {
+        return mContext.checkCallingPermission(INTERACT_ACROSS_USERS_FULL) == PERMISSION_GRANTED
+                || mContext.checkCallingPermission(INTERACT_ACROSS_USERS) == PERMISSION_GRANTED;
+    }
+
     /**
      *  For targetSdk S+ we require STATUS_BAR. For targetSdk < S, we only require EXPAND_STATUS_BAR
      *  but also require that it falls into one of the allowed use-cases to lock down abuse vector.
      */
     private boolean checkCanCollapseStatusBar(String method) {
         int uid = Binder.getCallingUid();
-        int pid = Binder.getCallingUid();
+        int pid = Binder.getCallingPid();
         if (CompatChanges.isChangeEnabled(LOCK_DOWN_COLLAPSE_STATUS_BAR, uid)) {
             enforceStatusBar();
         } else {
             if (mContext.checkPermission(Manifest.permission.STATUS_BAR, pid, uid)
-                    != PackageManager.PERMISSION_GRANTED) {
+                    != PERMISSION_GRANTED) {
                 enforceExpandStatusBar();
                 if (!mActivityTaskManager.canCloseSystemDialogs(pid, uid)) {
                     Slog.e(TAG, "Permission Denial: Method " + method + "() requires permission "
@@ -1355,7 +1374,7 @@
                     state.mAppearance, state.mAppearanceRegions, state.mImeWindowVis,
                     state.mImeBackDisposition, state.mShowImeSwitcher,
                     gatherDisableActionsLocked(mCurrentUserId, 2), state.mImeToken,
-                    state.mNavbarColorManagedByIme, state.mBehavior, state.mRequestedVisibilities,
+                    state.mNavbarColorManagedByIme, state.mBehavior, state.mRequestedVisibleTypes,
                     state.mPackageName, transientBarTypes, state.mLetterboxDetails);
         }
     }
@@ -1366,11 +1385,11 @@
             mGlobalActionListener.onGlobalActionsAvailableChanged(mBar != null);
         });
         // If StatusBarService dies, system_server doesn't get killed with it, so we need to make
-        // sure the UDFPS listener is refreshed as well. Deferring to the handler just so to avoid
+        // sure the UDFPS callback is refreshed as well. Deferring to the handler just so to avoid
         // making registerStatusBar re-entrant.
         mHandler.post(() -> {
             synchronized (mLock) {
-                setUdfpsHbmListener(mUdfpsHbmListener);
+                setUdfpsRefreshRateCallback(mUdfpsRefreshRateRequestCallback);
                 setBiometicContextListener(mBiometricContextListener);
             }
         });
@@ -2021,6 +2040,11 @@
         }
 
         final int userId = mCurrentUserId;
+        final int callingUserId = UserHandle.getUserId(Binder.getCallingUid());
+        if (mCurrentUserId != callingUserId && !doesCallerHoldInteractAcrossUserPermission()) {
+            throw new SecurityException("Calling user id: " + callingUserId
+                    + ", cannot call on behalf of current user id: " + mCurrentUserId + ".");
+        }
         final long userIdentity = Binder.clearCallingIdentity();
         try {
             Settings.Secure.putIntForUser(mContext.getContentResolver(),
@@ -2272,6 +2296,25 @@
 
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        boolean proto = false;
+        for (int i = 0; i < args.length; i++) {
+            if ("--proto".equals(args[i])) {
+                proto = true;
+            }
+        }
+        if (proto) {
+            if (mBar == null)  return;
+            try (TransferPipe tp = new TransferPipe()) {
+                // Sending the command to the remote, which needs to execute async to avoid blocking
+                // See Binder#dumpAsync() for inspiration
+                mBar.dumpProto(args, tp.getWriteFd());
+                // Times out after 5s
+                tp.go(fd);
+            } catch (Throwable t) {
+                Slog.e(TAG, "Error sending command to IStatusBar", t);
+            }
+            return;
+        }
 
         synchronized (mLock) {
             for (int i = 0; i < mDisplayUiState.size(); i++) {
diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
index fc77ef1..dad3a78 100644
--- a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
+++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
@@ -47,11 +47,11 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseLongArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Installer;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
index 4972412..5801920 100644
--- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
+++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
@@ -28,7 +28,7 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemClockTime;
 import com.android.server.SystemClockTime.TimeConfidence;
-import com.android.server.timezonedetector.ConfigurationChangeListener;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.io.PrintWriter;
 import java.util.Objects;
@@ -60,10 +60,10 @@
 
     @Override
     public void setConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
-        ConfigurationChangeListener configurationChangeListener =
+            @NonNull StateChangeListener listener) {
+        StateChangeListener stateChangeListener =
                 () -> mHandler.post(listener::onChange);
-        mServiceConfigAccessor.addConfigurationInternalChangeListener(configurationChangeListener);
+        mServiceConfigAccessor.addConfigurationInternalChangeListener(stateChangeListener);
     }
 
     @Override
@@ -128,4 +128,5 @@
     @Override
     public void dumpDebugLog(@NonNull PrintWriter printWriter) {
         SystemClockTime.dump(printWriter);
-    }}
+    }
+}
diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java
index 773b517..e9827ce 100644
--- a/services/core/java/com/android/server/timedetector/ServerFlags.java
+++ b/services/core/java/com/android/server/timedetector/ServerFlags.java
@@ -25,8 +25,8 @@
 import android.util.ArrayMap;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.server.timezonedetector.ConfigurationChangeListener;
 import com.android.server.timezonedetector.ServiceConfigAccessor;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -185,8 +185,7 @@
      * ensure O(1) lookup performance when working out whether a listener should trigger.
      */
     @GuardedBy("mListeners")
-    private final ArrayMap<ConfigurationChangeListener, HashSet<String>> mListeners =
-            new ArrayMap<>();
+    private final ArrayMap<StateChangeListener, HashSet<String>> mListeners = new ArrayMap<>();
 
     private static final Object SLOCK = new Object();
 
@@ -213,7 +212,7 @@
 
     private void handlePropertiesChanged(@NonNull DeviceConfig.Properties properties) {
         synchronized (mListeners) {
-            for (Map.Entry<ConfigurationChangeListener, HashSet<String>> listenerEntry
+            for (Map.Entry<StateChangeListener, HashSet<String>> listenerEntry
                     : mListeners.entrySet()) {
                 // It's unclear which set of the following two Sets is going to be larger in the
                 // average case: monitoredKeys will be a subset of the set of possible keys, but
@@ -249,7 +248,7 @@
      * <p>Note: Only for use by long-lived objects like other singletons. There is deliberately no
      * associated remove method.
      */
-    public void addListener(@NonNull ConfigurationChangeListener listener,
+    public void addListener(@NonNull StateChangeListener listener,
             @NonNull Set<String> keys) {
         Objects.requireNonNull(listener);
         Objects.requireNonNull(keys);
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
index a39f64c..ff180eb 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessor.java
@@ -19,7 +19,7 @@
 import android.annotation.UserIdInt;
 import android.app.time.TimeConfiguration;
 
-import com.android.server.timezonedetector.ConfigurationChangeListener;
+import com.android.server.timezonedetector.StateChangeListener;
 
 /**
  * An interface that provides access to service configuration for time detection. This hides
@@ -33,18 +33,18 @@
      * Adds a listener that will be invoked when {@link ConfigurationInternal} may have changed.
      * The listener is invoked on the main thread.
      */
-    void addConfigurationInternalChangeListener(@NonNull ConfigurationChangeListener listener);
+    void addConfigurationInternalChangeListener(@NonNull StateChangeListener listener);
 
     /**
      * Removes a listener previously added via {@link
-     * #addConfigurationInternalChangeListener(ConfigurationChangeListener)}.
+     * #addConfigurationInternalChangeListener(StateChangeListener)}.
      */
-    void removeConfigurationInternalChangeListener(@NonNull ConfigurationChangeListener listener);
+    void removeConfigurationInternalChangeListener(@NonNull StateChangeListener listener);
 
     /**
      * Returns a snapshot of the {@link ConfigurationInternal} for the current user. This is only a
      * snapshot so callers must use {@link
-     * #addConfigurationInternalChangeListener(ConfigurationChangeListener)} to be notified when it
+     * #addConfigurationInternalChangeListener(StateChangeListener)} to be notified when it
      * changes.
      */
     @NonNull
diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
index 71acf35..4ef713c 100644
--- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java
@@ -49,7 +49,7 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.LocalServices;
 import com.android.server.timedetector.TimeDetectorStrategy.Origin;
-import com.android.server.timezonedetector.ConfigurationChangeListener;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -104,8 +104,8 @@
     @NonNull private final ServerFlagsOriginPrioritiesSupplier mServerFlagsOriginPrioritiesSupplier;
 
     @GuardedBy("this")
-    @NonNull private final List<ConfigurationChangeListener> mConfigurationInternalListeners =
-            new ArrayList<>();
+    @NonNull
+    private final List<StateChangeListener> mConfigurationInternalListeners = new ArrayList<>();
 
     /**
      * If a newly calculated system clock time and the current system clock time differs by this or
@@ -167,20 +167,20 @@
     }
 
     private synchronized void handleConfigurationInternalChangeOnMainThread() {
-        for (ConfigurationChangeListener changeListener : mConfigurationInternalListeners) {
+        for (StateChangeListener changeListener : mConfigurationInternalListeners) {
             changeListener.onChange();
         }
     }
 
     @Override
     public synchronized void addConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
+            @NonNull StateChangeListener listener) {
         mConfigurationInternalListeners.add(Objects.requireNonNull(listener));
     }
 
     @Override
     public synchronized void removeConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
+            @NonNull StateChangeListener listener) {
         mConfigurationInternalListeners.remove(Objects.requireNonNull(listener));
     }
 
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index 3cee19c..13ec753 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -44,8 +44,8 @@
 import com.android.server.SystemClockTime;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timezonedetector.ArrayMapWithHistory;
-import com.android.server.timezonedetector.ConfigurationChangeListener;
 import com.android.server.timezonedetector.ReferenceWithHistory;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.io.PrintWriter;
 import java.time.Duration;
@@ -136,11 +136,11 @@
     public interface Environment {
 
         /**
-         * Sets a {@link ConfigurationChangeListener} that will be invoked when there are any
-         * changes that could affect the content of {@link ConfigurationInternal}.
+         * Sets a {@link StateChangeListener} that will be invoked when there are any changes that
+         * could affect the content of {@link ConfigurationInternal}.
          * This is invoked during system server setup.
          */
-        void setConfigurationInternalChangeListener(@NonNull ConfigurationChangeListener listener);
+        void setConfigurationInternalChangeListener(@NonNull StateChangeListener listener);
 
         /** Returns the {@link ConfigurationInternal} for the current user. */
         @NonNull ConfigurationInternal getCurrentUserConfigurationInternal();
diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java b/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java
deleted file mode 100644
index aa8ad37..0000000
--- a/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 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.server.timezonedetector;
-
-/**
- * A listener used to receive notification that configuration has / may have changed (depending on
- * the usecase).
- */
-@FunctionalInterface
-public interface ConfigurationChangeListener {
-    /** Called when the configuration may have changed. */
-    void onChange();
-}
diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
index 8e2a5f4..0409a84 100644
--- a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
+++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java
@@ -26,7 +26,6 @@
 import android.annotation.UserIdInt;
 import android.app.time.Capabilities.CapabilityState;
 import android.app.time.TimeZoneCapabilities;
-import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
 import android.os.UserHandle;
 
@@ -75,7 +74,7 @@
         mEnhancedMetricsCollectionEnabled = builder.mEnhancedMetricsCollectionEnabled;
         mAutoDetectionEnabledSetting = builder.mAutoDetectionEnabledSetting;
 
-        mUserId = builder.mUserId;
+        mUserId = Objects.requireNonNull(builder.mUserId, "userId must be set");
         mUserConfigAllowed = builder.mUserConfigAllowed;
         mLocationEnabledSetting = builder.mLocationEnabledSetting;
         mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting;
@@ -151,8 +150,7 @@
      * Returns true if the user is allowed to modify time zone configuration, e.g. can be false due
      * to device policy (enterprise).
      *
-     * <p>See also {@link #createCapabilitiesAndConfig(boolean)} for situations where this value
-     * are ignored.
+     * <p>See also {@link #asCapabilities(boolean)} for situations where this value is ignored.
      */
     public boolean isUserConfigAllowed() {
         return mUserConfigAllowed;
@@ -196,20 +194,8 @@
                 || getGeoDetectionRunInBackgroundEnabled());
     }
 
-    /**
-     * Creates a {@link TimeZoneCapabilitiesAndConfig} object using the configuration values.
-     *
-     * @param bypassUserPolicyChecks {@code true} for device policy manager use cases where device
-     *   policy restrictions that should apply to actual users can be ignored
-     */
-    public TimeZoneCapabilitiesAndConfig createCapabilitiesAndConfig(
-            boolean bypassUserPolicyChecks) {
-        return new TimeZoneCapabilitiesAndConfig(
-                asCapabilities(bypassUserPolicyChecks), asConfiguration());
-    }
-
     @NonNull
-    private TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) {
+    public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) {
         UserHandle userHandle = UserHandle.of(mUserId);
         TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(userHandle);
 
@@ -262,7 +248,7 @@
     }
 
     /** Returns a {@link TimeZoneConfiguration} from the configuration values. */
-    private TimeZoneConfiguration asConfiguration() {
+    public TimeZoneConfiguration asConfiguration() {
         return new TimeZoneConfiguration.Builder()
                 .setAutoDetectionEnabled(getAutoDetectionEnabledSetting())
                 .setGeoDetectionEnabled(getGeoDetectionEnabledSetting())
@@ -335,8 +321,7 @@
      */
     public static class Builder {
 
-        private final @UserIdInt int mUserId;
-
+        private @UserIdInt Integer mUserId;
         private boolean mUserConfigAllowed;
         private boolean mTelephonyDetectionSupported;
         private boolean mGeoDetectionSupported;
@@ -348,11 +333,9 @@
         private boolean mGeoDetectionEnabledSetting;
 
         /**
-         * Creates a new Builder with only the userId set.
+         * Creates a new Builder.
          */
-        public Builder(@UserIdInt int userId) {
-            mUserId = userId;
-        }
+        public Builder() {}
 
         /**
          * Creates a new Builder by copying values from an existing instance.
@@ -371,6 +354,14 @@
         }
 
         /**
+         * Sets the user ID the configuration is for.
+         */
+        public Builder setUserId(@UserIdInt int userId) {
+            mUserId = userId;
+            return this;
+        }
+
+        /**
          * Sets whether the user is allowed to configure time zone settings on this device.
          */
         public Builder setUserConfigAllowed(boolean configAllowed) {
diff --git a/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java
index 4749f73..5cb48c2 100644
--- a/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java
@@ -18,8 +18,6 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
-import android.content.Context;
-import android.os.Handler;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 
@@ -29,7 +27,6 @@
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
 
 import java.io.PrintWriter;
-import java.util.Objects;
 
 /**
  * The real implementation of {@link TimeZoneDetectorStrategyImpl.Environment}.
@@ -38,29 +35,7 @@
 
     private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
 
-    @NonNull private final Context mContext;
-    @NonNull private final Handler mHandler;
-    @NonNull private final ServiceConfigAccessor mServiceConfigAccessor;
-
-    EnvironmentImpl(@NonNull Context context, @NonNull Handler handler,
-            @NonNull ServiceConfigAccessor serviceConfigAccessor) {
-        mContext = Objects.requireNonNull(context);
-        mHandler = Objects.requireNonNull(handler);
-        mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
-    }
-
-    @Override
-    public void setConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
-        ConfigurationChangeListener configurationChangeListener =
-                () -> mHandler.post(listener::onChange);
-        mServiceConfigAccessor.addConfigurationInternalChangeListener(configurationChangeListener);
-    }
-
-    @Override
-    @NonNull
-    public ConfigurationInternal getCurrentUserConfigurationInternal() {
-        return mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+    EnvironmentImpl() {
     }
 
     @Override
diff --git a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java
index 8218fa5..80d9599 100644
--- a/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java
+++ b/services/core/java/com/android/server/timezonedetector/GeolocationTimeZoneSuggestion.java
@@ -19,20 +19,15 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.ShellCommand;
-import android.os.SystemClock;
 
-import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
-import java.util.StringTokenizer;
 
 /**
- * A time zone suggestion from the location_time_zone_manager service to the time_zone_detector
- * service.
+ * A time zone suggestion from the location_time_zone_manager service (AKA the location-based time
+ * zone detection algorithm).
  *
  * <p>Geolocation-based suggestions have the following properties:
  *
@@ -63,24 +58,16 @@
  *     location_time_zone_manager may become uncertain if components further downstream cannot
  *     determine the device's location with sufficient accuracy, or if the location is known but no
  *     time zone can be determined because no time zone mapping information is available.</li>
- *     <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is
- *     used to record why the suggestion exists and how it was obtained. This information exists
- *     only to aid in debugging and therefore is used by {@link #toString()}, but it is not for use
- *     in detection logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}.
  *     </li>
  * </ul>
- *
- * @hide
  */
 public final class GeolocationTimeZoneSuggestion {
 
     @ElapsedRealtimeLong private final long mEffectiveFromElapsedMillis;
     @Nullable private final List<String> mZoneIds;
-    @Nullable private ArrayList<String> mDebugInfo;
 
     private GeolocationTimeZoneSuggestion(
-            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
-            @Nullable List<String> zoneIds) {
+            @ElapsedRealtimeLong long effectiveFromElapsedMillis, @Nullable List<String> zoneIds) {
         mEffectiveFromElapsedMillis = effectiveFromElapsedMillis;
         if (zoneIds == null) {
             // Unopinionated
@@ -104,8 +91,7 @@
      */
     @NonNull
     public static GeolocationTimeZoneSuggestion createCertainSuggestion(
-            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
-            @NonNull List<String> zoneIds) {
+            @ElapsedRealtimeLong long effectiveFromElapsedMillis, @NonNull List<String> zoneIds) {
         return new GeolocationTimeZoneSuggestion(effectiveFromElapsedMillis, zoneIds);
     }
 
@@ -126,25 +112,6 @@
         return mZoneIds;
     }
 
-    /** Returns debug information. See {@link GeolocationTimeZoneSuggestion} for details. */
-    @NonNull
-    public List<String> getDebugInfo() {
-        return mDebugInfo == null
-                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
-    }
-
-    /**
-     * Associates information with the instance that can be useful for debugging / logging. The
-     * information is present in {@link #toString()} but is not considered for
-     * {@link #equals(Object)} and {@link #hashCode()}.
-     */
-    public void addDebugInfo(String... debugInfos) {
-        if (mDebugInfo == null) {
-            mDebugInfo = new ArrayList<>();
-        }
-        mDebugInfo.addAll(Arrays.asList(debugInfos));
-    }
-
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -169,59 +136,6 @@
         return "GeolocationTimeZoneSuggestion{"
                 + "mEffectiveFromElapsedMillis=" + mEffectiveFromElapsedMillis
                 + ", mZoneIds=" + mZoneIds
-                + ", mDebugInfo=" + mDebugInfo
                 + '}';
     }
-
-    /** @hide */
-    public static GeolocationTimeZoneSuggestion parseCommandLineArg(@NonNull ShellCommand cmd) {
-        String zoneIdsString = null;
-        String opt;
-        while ((opt = cmd.getNextArg()) != null) {
-            switch (opt) {
-                case "--zone_ids": {
-                    zoneIdsString  = cmd.getNextArgRequired();
-                    break;
-                }
-                default: {
-                    throw new IllegalArgumentException("Unknown option: " + opt);
-                }
-            }
-        }
-
-        if (zoneIdsString == null) {
-            throw new IllegalArgumentException("Missing --zone_ids");
-        }
-
-        long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
-        List<String> zoneIds = parseZoneIdsArg(zoneIdsString);
-        GeolocationTimeZoneSuggestion suggestion =
-                new GeolocationTimeZoneSuggestion(elapsedRealtimeMillis, zoneIds);
-        suggestion.addDebugInfo("Command line injection");
-        return suggestion;
-    }
-
-    private static List<String> parseZoneIdsArg(String zoneIdsString) {
-        if ("UNCERTAIN".equals(zoneIdsString)) {
-            return null;
-        } else if ("EMPTY".equals(zoneIdsString)) {
-            return Collections.emptyList();
-        } else {
-            ArrayList<String> zoneIds = new ArrayList<>();
-            StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ",");
-            while (tokenizer.hasMoreTokens()) {
-                zoneIds.add(tokenizer.nextToken());
-            }
-            return zoneIds;
-        }
-    }
-
-    /** @hide */
-    public static void printCommandLineOpts(@NonNull PrintWriter pw) {
-        pw.println("Geolocation suggestion options:");
-        pw.println("  --zone_ids {UNCERTAIN|EMPTY|<Olson ID>+}");
-        pw.println();
-        pw.println("See " + GeolocationTimeZoneSuggestion.class.getName()
-                + " for more information");
-    }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java
new file mode 100644
index 0000000..1ffd9a1
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/LocationAlgorithmEvent.java
@@ -0,0 +1,194 @@
+/*
+ * 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.timezonedetector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringTokenizer;
+
+/**
+ * An event from the location_time_zone_manager service (AKA the location-based time zone detection
+ * algorithm). An event can represent a new time zone recommendation, an algorithm status change, or
+ * both.
+ *
+ * <p>Events have the following properties:
+ *
+ * <ul>
+ *     <li>{@code algorithmStatus}: The current status of the location-based time zone detection
+ *     algorithm.</li>
+ *     <li>{@code suggestion}: The latest time zone suggestion, if there is one.</li>
+ *     <li>{@code debugInfo} contains debugging metadata associated with the suggestion. This is
+ *     used to record why the event exists and how information contained within it was obtained.
+ *     This information exists only to aid in debugging and therefore is used by
+ *     {@link #toString()}, but it is not for use in detection logic and is not considered in
+ *     {@link #hashCode()} or {@link #equals(Object)}.
+ *     </li>
+ * </ul>
+ */
+public final class LocationAlgorithmEvent {
+
+    @NonNull private final LocationTimeZoneAlgorithmStatus mAlgorithmStatus;
+    @Nullable private final GeolocationTimeZoneSuggestion mSuggestion;
+    @Nullable private ArrayList<String> mDebugInfo;
+
+    /** Creates a new instance. */
+    public LocationAlgorithmEvent(
+            @NonNull LocationTimeZoneAlgorithmStatus algorithmStatus,
+            @Nullable GeolocationTimeZoneSuggestion suggestion) {
+        mAlgorithmStatus = Objects.requireNonNull(algorithmStatus);
+        mSuggestion = suggestion;
+    }
+
+    /**
+     * Returns the status of the location time zone detector algorithm.
+     */
+    @NonNull
+    public LocationTimeZoneAlgorithmStatus getAlgorithmStatus() {
+        return mAlgorithmStatus;
+    }
+
+    /**
+     * Returns the latest location algorithm suggestion. See {@link LocationAlgorithmEvent} for
+     * details.
+     */
+    @Nullable
+    public GeolocationTimeZoneSuggestion getSuggestion() {
+        return mSuggestion;
+    }
+
+    /** Returns debug information. See {@link LocationAlgorithmEvent} for details. */
+    @NonNull
+    public List<String> getDebugInfo() {
+        return mDebugInfo == null
+                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
+    }
+
+    /**
+     * Associates information with the instance that can be useful for debugging / logging. The
+     * information is present in {@link #toString()} but is not considered for
+     * {@link #equals(Object)} and {@link #hashCode()}.
+     */
+    public void addDebugInfo(String... debugInfos) {
+        if (mDebugInfo == null) {
+            mDebugInfo = new ArrayList<>();
+        }
+        mDebugInfo.addAll(Arrays.asList(debugInfos));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        LocationAlgorithmEvent that = (LocationAlgorithmEvent) o;
+        return mAlgorithmStatus.equals(that.mAlgorithmStatus)
+                && Objects.equals(mSuggestion, that.mSuggestion);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAlgorithmStatus, mSuggestion);
+    }
+
+    @Override
+    public String toString() {
+        return "LocationAlgorithmEvent{"
+                + "mAlgorithmStatus=" + mAlgorithmStatus
+                + ", mSuggestion=" + mSuggestion
+                + ", mDebugInfo=" + mDebugInfo
+                + '}';
+    }
+
+    static LocationAlgorithmEvent parseCommandLineArg(@NonNull ShellCommand cmd) {
+        String suggestionString = null;
+        LocationTimeZoneAlgorithmStatus algorithmStatus = null;
+        String opt;
+        while ((opt = cmd.getNextArg()) != null) {
+            switch (opt) {
+                case "--status": {
+                    algorithmStatus = LocationTimeZoneAlgorithmStatus.parseCommandlineArg(
+                            cmd.getNextArgRequired());
+                    break;
+                }
+                case "--suggestion": {
+                    suggestionString  = cmd.getNextArgRequired();
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("Unknown option: " + opt);
+                }
+            }
+        }
+
+        if (algorithmStatus == null) {
+            throw new IllegalArgumentException("Missing --status");
+        }
+
+        GeolocationTimeZoneSuggestion suggestion = null;
+        if (suggestionString != null) {
+            List<String> zoneIds = parseZoneIds(suggestionString);
+            long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
+            if (zoneIds == null) {
+                suggestion = GeolocationTimeZoneSuggestion.createUncertainSuggestion(
+                        elapsedRealtimeMillis);
+            } else {
+                suggestion = GeolocationTimeZoneSuggestion.createCertainSuggestion(
+                        elapsedRealtimeMillis, zoneIds);
+            }
+        }
+
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion);
+        event.addDebugInfo("Command line injection");
+        return event;
+    }
+
+    private static List<String> parseZoneIds(String zoneIdsString) {
+        if ("UNCERTAIN".equals(zoneIdsString)) {
+            return null;
+        } else if ("EMPTY".equals(zoneIdsString)) {
+            return Collections.emptyList();
+        } else {
+            ArrayList<String> zoneIds = new ArrayList<>();
+            StringTokenizer tokenizer = new StringTokenizer(zoneIdsString, ",");
+            while (tokenizer.hasMoreTokens()) {
+                zoneIds.add(tokenizer.nextToken());
+            }
+            return zoneIds;
+        }
+    }
+
+    static void printCommandLineOpts(@NonNull PrintWriter pw) {
+        pw.println("Location algorithm event options:");
+        pw.println("  --status {LocationTimeZoneAlgorithmStatus toString() format}");
+        pw.println("  [--suggestion {UNCERTAIN|EMPTY|<Olson ID>+}]");
+        pw.println();
+        pw.println("See " + LocationAlgorithmEvent.class.getName() + " for more information");
+    }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java
index 6c36989..aad5359 100644
--- a/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java
+++ b/services/core/java/com/android/server/timezonedetector/MetricsTimeZoneDetectorState.java
@@ -89,7 +89,7 @@
             @NonNull String deviceTimeZoneId,
             @Nullable ManualTimeZoneSuggestion latestManualSuggestion,
             @Nullable TelephonyTimeZoneSuggestion latestTelephonySuggestion,
-            @Nullable GeolocationTimeZoneSuggestion latestGeolocationSuggestion) {
+            @Nullable LocationAlgorithmEvent latestLocationAlgorithmEvent) {
 
         boolean includeZoneIds = configurationInternal.isEnhancedMetricsCollectionEnabled();
         String metricDeviceTimeZoneId = includeZoneIds ? deviceTimeZoneId : null;
@@ -101,9 +101,13 @@
         MetricsTimeZoneSuggestion latestCanonicalTelephonySuggestion =
                 createMetricsTimeZoneSuggestion(
                         tzIdOrdinalGenerator, latestTelephonySuggestion, includeZoneIds);
-        MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion =
-                createMetricsTimeZoneSuggestion(
-                        tzIdOrdinalGenerator, latestGeolocationSuggestion, includeZoneIds);
+
+        MetricsTimeZoneSuggestion latestCanonicalGeolocationSuggestion = null;
+        if (latestLocationAlgorithmEvent != null) {
+            GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion();
+            latestCanonicalGeolocationSuggestion = createMetricsTimeZoneSuggestion(
+                    tzIdOrdinalGenerator, suggestion, includeZoneIds);
+        }
 
         return new MetricsTimeZoneDetectorState(
                 configurationInternal, deviceTimeZoneIdOrdinal, metricDeviceTimeZoneId,
diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
index 8da5d6a..4ac2ba5 100644
--- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
+++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
@@ -59,20 +59,18 @@
      * Adds a listener that will be invoked when {@link ConfigurationInternal} may have changed.
      * The listener is invoked on the main thread.
      */
-    void addConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener);
+    void addConfigurationInternalChangeListener(@NonNull StateChangeListener listener);
 
     /**
      * Removes a listener previously added via {@link
-     * #addConfigurationInternalChangeListener(ConfigurationChangeListener)}.
+     * #addConfigurationInternalChangeListener(StateChangeListener)}.
      */
-    void removeConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener);
+    void removeConfigurationInternalChangeListener(@NonNull StateChangeListener listener);
 
     /**
      * Returns a snapshot of the {@link ConfigurationInternal} for the current user. This is only a
      * snapshot so callers must use {@link
-     * #addConfigurationInternalChangeListener(ConfigurationChangeListener)} to be notified when it
+     * #addConfigurationInternalChangeListener(StateChangeListener)} to be notified when it
      * changes.
      */
     @NonNull
@@ -104,8 +102,7 @@
      *
      * <p>Note: Currently only for use by long-lived objects; there is no associated remove method.
      */
-    void addLocationTimeZoneManagerConfigListener(
-            @NonNull ConfigurationChangeListener listener);
+    void addLocationTimeZoneManagerConfigListener(@NonNull StateChangeListener listener);
 
     /**
      * Returns {@code true} if the telephony-based time zone detection feature is supported on the
diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
index e2f4246..dfb9619 100644
--- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java
@@ -22,7 +22,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.time.TimeZoneCapabilities;
-import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -104,8 +103,8 @@
     @NonNull private final LocationManager mLocationManager;
 
     @GuardedBy("this")
-    @NonNull private final List<ConfigurationChangeListener> mConfigurationInternalListeners =
-            new ArrayList<>();
+    @NonNull
+    private final List<StateChangeListener> mConfigurationInternalListeners = new ArrayList<>();
 
     /**
      * The mode to use for the primary location time zone provider in a test. Setting this
@@ -207,20 +206,20 @@
     }
 
     private synchronized void handleConfigurationInternalChangeOnMainThread() {
-        for (ConfigurationChangeListener changeListener : mConfigurationInternalListeners) {
+        for (StateChangeListener changeListener : mConfigurationInternalListeners) {
             changeListener.onChange();
         }
     }
 
     @Override
     public synchronized void addConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
+            @NonNull StateChangeListener listener) {
         mConfigurationInternalListeners.add(Objects.requireNonNull(listener));
     }
 
     @Override
     public synchronized void removeConfigurationInternalChangeListener(
-            @NonNull ConfigurationChangeListener listener) {
+            @NonNull StateChangeListener listener) {
         mConfigurationInternalListeners.remove(Objects.requireNonNull(listener));
     }
 
@@ -237,10 +236,10 @@
             @NonNull TimeZoneConfiguration requestedConfiguration, boolean bypassUserPolicyChecks) {
         Objects.requireNonNull(requestedConfiguration);
 
-        TimeZoneCapabilitiesAndConfig capabilitiesAndConfig = getConfigurationInternal(userId)
-                .createCapabilitiesAndConfig(bypassUserPolicyChecks);
-        TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
-        TimeZoneConfiguration oldConfiguration = capabilitiesAndConfig.getConfiguration();
+        ConfigurationInternal configurationInternal = getConfigurationInternal(userId);
+        TimeZoneCapabilities capabilities =
+                configurationInternal.asCapabilities(bypassUserPolicyChecks);
+        TimeZoneConfiguration oldConfiguration = configurationInternal.asConfiguration();
 
         final TimeZoneConfiguration newConfiguration =
                 capabilities.tryApplyConfigChanges(oldConfiguration, requestedConfiguration);
@@ -292,7 +291,8 @@
     @Override
     @NonNull
     public synchronized ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) {
-        return new ConfigurationInternal.Builder(userId)
+        return new ConfigurationInternal.Builder()
+                .setUserId(userId)
                 .setTelephonyDetectionFeatureSupported(
                         isTelephonyTimeZoneDetectionFeatureSupported())
                 .setGeoDetectionFeatureSupported(isGeoTimeZoneDetectionFeatureSupported())
@@ -354,7 +354,7 @@
 
     @Override
     public void addLocationTimeZoneManagerConfigListener(
-            @NonNull ConfigurationChangeListener listener) {
+            @NonNull StateChangeListener listener) {
         mServerFlags.addListener(listener, LOCATION_TIME_ZONE_MANAGER_SERVER_FLAGS_KEYS_TO_WATCH);
     }
 
diff --git a/services/core/java/com/android/server/timezonedetector/StateChangeListener.java b/services/core/java/com/android/server/timezonedetector/StateChangeListener.java
new file mode 100644
index 0000000..2b5639c
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/StateChangeListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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.server.timezonedetector;
+
+/**
+ * A listener used to receive notification that state has / may have changed (depending on
+ * the usecase).
+ */
+@FunctionalInterface
+public interface StateChangeListener {
+    /** Called when something (may have) changed. */
+    void onChange();
+}
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
index 80cf1d6..74a518b 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java
@@ -59,11 +59,11 @@
     boolean setManualTimeZoneForDpm(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion);
 
     /**
-     * Suggests the current time zone, determined using geolocation, to the detector. The
-     * detector may ignore the signal based on system settings, whether better information is
-     * available, and so on. This method may be implemented asynchronously.
+     * Handles the supplied {@link LocationAlgorithmEvent}. The detector may ignore the event based
+     * on system settings, whether better information is available, and so on. This method may be
+     * implemented asynchronously.
      */
-    void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion);
+    void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent);
 
     /** Generates a state snapshot for metrics. */
     @NonNull
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
index ce64eac..07d0473 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java
@@ -35,17 +35,14 @@
     @NonNull private final Context mContext;
     @NonNull private final Handler mHandler;
     @NonNull private final CurrentUserIdentityInjector mCurrentUserIdentityInjector;
-    @NonNull private final ServiceConfigAccessor mServiceConfigAccessor;
     @NonNull private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
 
     public TimeZoneDetectorInternalImpl(@NonNull Context context, @NonNull Handler handler,
             @NonNull CurrentUserIdentityInjector currentUserIdentityInjector,
-            @NonNull ServiceConfigAccessor serviceConfigAccessor,
             @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
         mContext = Objects.requireNonNull(context);
         mHandler = Objects.requireNonNull(handler);
         mCurrentUserIdentityInjector = Objects.requireNonNull(currentUserIdentityInjector);
-        mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
         mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy);
     }
 
@@ -53,10 +50,9 @@
     @NonNull
     public TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfigForDpm() {
         int currentUserId = mCurrentUserIdentityInjector.getCurrentUserId();
-        ConfigurationInternal configurationInternal =
-                mServiceConfigAccessor.getConfigurationInternal(currentUserId);
         final boolean bypassUserPolicyChecks = true;
-        return configurationInternal.createCapabilitiesAndConfig(bypassUserPolicyChecks);
+        return mTimeZoneDetectorStrategy.getCapabilitiesAndConfig(
+                currentUserId, bypassUserPolicyChecks);
     }
 
     @Override
@@ -65,7 +61,7 @@
 
         int currentUserId = mCurrentUserIdentityInjector.getCurrentUserId();
         final boolean bypassUserPolicyChecks = true;
-        return mServiceConfigAccessor.updateConfiguration(
+        return mTimeZoneDetectorStrategy.updateConfiguration(
                 currentUserId, configuration, bypassUserPolicyChecks);
     }
 
@@ -80,13 +76,14 @@
     }
 
     @Override
-    public void suggestGeolocationTimeZone(
-            @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {
-        Objects.requireNonNull(timeZoneSuggestion);
+    public void handleLocationAlgorithmEvent(
+            @NonNull LocationAlgorithmEvent locationAlgorithmEvent) {
+        Objects.requireNonNull(locationAlgorithmEvent);
 
         // This call can take place on the mHandler thread because there is no return value.
         mHandler.post(
-                () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
+                () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent(
+                        locationAlgorithmEvent));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index 13f1694..f8c1c92 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -83,7 +83,7 @@
             ServiceConfigAccessor serviceConfigAccessor =
                     ServiceConfigAccessorImpl.getInstance(context);
             TimeZoneDetectorStrategy timeZoneDetectorStrategy =
-                    TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor);
+                    TimeZoneDetectorStrategyImpl.create(handler, serviceConfigAccessor);
             DeviceActivityMonitor deviceActivityMonitor =
                     DeviceActivityMonitorImpl.create(context, handler);
 
@@ -99,16 +99,14 @@
             CurrentUserIdentityInjector currentUserIdentityInjector =
                     CurrentUserIdentityInjector.REAL;
             TimeZoneDetectorInternal internal = new TimeZoneDetectorInternalImpl(
-                    context, handler, currentUserIdentityInjector, serviceConfigAccessor,
-                    timeZoneDetectorStrategy);
+                    context, handler, currentUserIdentityInjector, timeZoneDetectorStrategy);
             publishLocalService(TimeZoneDetectorInternal.class, internal);
 
             // Publish the binder service so it can be accessed from other (appropriately
             // permissioned) processes.
             CallerIdentityInjector callerIdentityInjector = CallerIdentityInjector.REAL;
             TimeZoneDetectorService service = new TimeZoneDetectorService(
-                    context, handler, callerIdentityInjector, serviceConfigAccessor,
-                    timeZoneDetectorStrategy);
+                    context, handler, callerIdentityInjector, timeZoneDetectorStrategy);
 
             // Dump the device activity monitor when the service is dumped.
             service.addDumpable(deviceActivityMonitor);
@@ -127,9 +125,6 @@
     private final CallerIdentityInjector mCallerIdentityInjector;
 
     @NonNull
-    private final ServiceConfigAccessor mServiceConfigAccessor;
-
-    @NonNull
     private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
 
     /**
@@ -150,18 +145,16 @@
     @VisibleForTesting
     public TimeZoneDetectorService(@NonNull Context context, @NonNull Handler handler,
             @NonNull CallerIdentityInjector callerIdentityInjector,
-            @NonNull ServiceConfigAccessor serviceConfigAccessor,
             @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) {
         mContext = Objects.requireNonNull(context);
         mHandler = Objects.requireNonNull(handler);
         mCallerIdentityInjector = Objects.requireNonNull(callerIdentityInjector);
-        mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
         mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy);
 
         // Wire up a change listener so that ITimeZoneDetectorListeners can be notified when
-        // the configuration changes for any reason.
-        mServiceConfigAccessor.addConfigurationInternalChangeListener(
-                () -> mHandler.post(this::handleConfigurationInternalChangedOnHandlerThread));
+        // the detector state changes for any reason.
+        mTimeZoneDetectorStrategy.addChangeListener(
+                () -> mHandler.post(this::handleChangeOnHandlerThread));
     }
 
     @Override
@@ -174,12 +167,15 @@
     TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(@UserIdInt int userId) {
         enforceManageTimeZoneDetectorPermission();
 
+        // Resolve constants like USER_CURRENT to the true user ID as needed.
+        int resolvedUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+                Binder.getCallingUid(), userId, false, false, "getCapabilitiesAndConfig", null);
+
         final long token = mCallerIdentityInjector.clearCallingIdentity();
         try {
-            ConfigurationInternal configurationInternal =
-                    mServiceConfigAccessor.getConfigurationInternal(userId);
             final boolean bypassUserPolicyChecks = false;
-            return configurationInternal.createCapabilitiesAndConfig(bypassUserPolicyChecks);
+            return mTimeZoneDetectorStrategy.getCapabilitiesAndConfig(
+                    resolvedUserId, bypassUserPolicyChecks);
         } finally {
             mCallerIdentityInjector.restoreCallingIdentity(token);
         }
@@ -204,7 +200,7 @@
         final long token = mCallerIdentityInjector.clearCallingIdentity();
         try {
             final boolean bypassUserPolicyChecks = false;
-            return mServiceConfigAccessor.updateConfiguration(
+            return mTimeZoneDetectorStrategy.updateConfiguration(
                     resolvedUserId, configuration, bypassUserPolicyChecks);
         } finally {
             mCallerIdentityInjector.restoreCallingIdentity(token);
@@ -285,8 +281,9 @@
         }
     }
 
-    void handleConfigurationInternalChangedOnHandlerThread() {
-        // Configuration has changed, but each user may have a different view of the configuration.
+    void handleChangeOnHandlerThread() {
+        // Detector state has changed. Each user may have a different view of the configuration so
+        // no information is passed; each client must query what they're interested in.
         // It's possible that this will cause unnecessary notifications but that shouldn't be a
         // problem.
         synchronized (mListeners) {
@@ -303,12 +300,13 @@
     }
 
     /** Provided for command-line access. This is not exposed as a binder API. */
-    void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {
+    void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent locationAlgorithmEvent) {
         enforceSuggestGeolocationTimeZonePermission();
-        Objects.requireNonNull(timeZoneSuggestion);
+        Objects.requireNonNull(locationAlgorithmEvent);
 
         mHandler.post(
-                () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion));
+                () -> mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent(
+                        locationAlgorithmEvent));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
index 1b9f8e6..69274db 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorShellCommand.java
@@ -19,6 +19,7 @@
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_DUMP_METRICS;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_GET_TIME_ZONE_STATE;
+import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_ENABLED;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_IS_GEO_DETECTION_SUPPORTED;
@@ -27,7 +28,6 @@
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_AUTO_DETECTION_ENABLED;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_GEO_DETECTION_ENABLED;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SET_TIME_ZONE_STATE;
-import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE;
 import static android.app.timezonedetector.TimeZoneDetector.SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE;
 import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME;
@@ -79,8 +79,8 @@
                 return runIsGeoDetectionEnabled();
             case SHELL_COMMAND_SET_GEO_DETECTION_ENABLED:
                 return runSetGeoDetectionEnabled();
-            case SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE:
-                return runSuggestGeolocationTimeZone();
+            case SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT:
+                return runHandleLocationEvent();
             case SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE:
                 return runSuggestManualTimeZone();
             case SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE:
@@ -153,34 +153,34 @@
         return mInterface.updateConfiguration(userId, configuration) ? 0 : 1;
     }
 
-    private int runSuggestGeolocationTimeZone() {
-        return runSuggestTimeZone(
-                () -> GeolocationTimeZoneSuggestion.parseCommandLineArg(this),
-                mInterface::suggestGeolocationTimeZone);
+    private int runHandleLocationEvent() {
+        return runSingleArgMethod(
+                () -> LocationAlgorithmEvent.parseCommandLineArg(this),
+                mInterface::handleLocationAlgorithmEvent);
     }
 
     private int runSuggestManualTimeZone() {
-        return runSuggestTimeZone(
+        return runSingleArgMethod(
                 () -> ManualTimeZoneSuggestion.parseCommandLineArg(this),
                 mInterface::suggestManualTimeZone);
     }
 
     private int runSuggestTelephonyTimeZone() {
-        return runSuggestTimeZone(
+        return runSingleArgMethod(
                 () -> TelephonyTimeZoneSuggestion.parseCommandLineArg(this),
                 mInterface::suggestTelephonyTimeZone);
     }
 
-    private <T> int runSuggestTimeZone(Supplier<T> suggestionParser, Consumer<T> invoker) {
+    private <T> int runSingleArgMethod(Supplier<T> argParser, Consumer<T> invoker) {
         final PrintWriter pw = getOutPrintWriter();
         try {
-            T suggestion = suggestionParser.get();
-            if (suggestion == null) {
-                pw.println("Error: suggestion not specified");
+            T arg = argParser.get();
+            if (arg == null) {
+                pw.println("Error: arg not specified");
                 return 1;
             }
-            invoker.accept(suggestion);
-            pw.println("Suggestion " + suggestion + " injected.");
+            invoker.accept(arg);
+            pw.println("Arg " + arg + " injected.");
             return 0;
         } catch (RuntimeException e) {
             pw.println(e);
@@ -263,18 +263,18 @@
         pw.printf("    Sets the geolocation time zone detection enabled setting.\n");
         pw.printf("  %s\n", SHELL_COMMAND_ENABLE_TELEPHONY_FALLBACK);
         pw.printf("    Signals that telephony time zone detection fall back can be used if"
-                + " geolocation detection is supported and enabled. This is a temporary state until"
-                + " geolocation detection becomes \"certain\". To have an effect this requires that"
-                + " the telephony fallback feature is supported on the device, see below for"
-                + " for device_config flags.\n");
-        pw.println();
-        pw.printf("  %s <geolocation suggestion opts>\n",
-                SHELL_COMMAND_SUGGEST_GEO_LOCATION_TIME_ZONE);
-        pw.printf("    Suggests a time zone as if via the \"location\" origin.\n");
+                + " geolocation detection is supported and enabled.\n)");
+        pw.printf("    This is a temporary state until geolocation detection becomes \"certain\"."
+                + "\n");
+        pw.printf("    To have an effect this requires that the telephony fallback feature is"
+                + " supported on the device, see below for device_config flags.\n");
+        pw.printf("  %s <location event opts>\n", SHELL_COMMAND_HANDLE_LOCATION_ALGORITHM_EVENT);
+        pw.printf("    Simulates an event from the location time zone detection algorithm.\n");
         pw.printf("  %s <manual suggestion opts>\n", SHELL_COMMAND_SUGGEST_MANUAL_TIME_ZONE);
-        pw.printf("    Suggests a time zone as if via the \"manual\" origin.\n");
+        pw.printf("    Suggests a time zone as if supplied by a user manually.\n");
         pw.printf("  %s <telephony suggestion opts>\n", SHELL_COMMAND_SUGGEST_TELEPHONY_TIME_ZONE);
-        pw.printf("    Suggests a time zone as if via the \"telephony\" origin.\n");
+        pw.printf("    Simulates a time zone suggestion from the telephony time zone detection"
+                + " algorithm.\n");
         pw.printf("  %s\n", SHELL_COMMAND_GET_TIME_ZONE_STATE);
         pw.printf("    Returns the current time zone setting state.\n");
         pw.printf("  %s <time zone state options>\n", SHELL_COMMAND_SET_TIME_ZONE_STATE);
@@ -284,7 +284,7 @@
         pw.printf("  %s\n", SHELL_COMMAND_DUMP_METRICS);
         pw.printf("    Dumps the service metrics to stdout for inspection.\n");
         pw.println();
-        GeolocationTimeZoneSuggestion.printCommandLineOpts(pw);
+        LocationAlgorithmEvent.printCommandLineOpts(pw);
         pw.println();
         ManualTimeZoneSuggestion.printCommandLineOpts(pw);
         pw.println();
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
index 69284e3..5768a6b 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
@@ -17,6 +17,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
+import android.app.time.TimeZoneCapabilitiesAndConfig;
+import android.app.time.TimeZoneConfiguration;
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
@@ -94,6 +96,48 @@
  */
 public interface TimeZoneDetectorStrategy extends Dumpable {
 
+    /**
+     * Adds a listener that will be triggered when something changes that could affect the result
+     * of the {@link #getCapabilitiesAndConfig} call for the <em>current user only</em>. This
+     * includes the current user changing. This is exposed so that (indirect) users like SettingsUI
+     * can monitor for changes to data derived from {@link TimeZoneCapabilitiesAndConfig} and update
+     * the UI accordingly.
+     */
+    void addChangeListener(StateChangeListener listener);
+
+    /**
+     * Returns a {@link TimeZoneCapabilitiesAndConfig} object for the specified user.
+     *
+     * <p>The strategy is dependent on device state like current user, settings and device config.
+     * These updates are usually handled asynchronously, so callers should expect some delay between
+     * a change being made directly to services like settings and the strategy becoming aware of
+     * them. Changes made via {@link #updateConfiguration} will be visible immediately.
+     *
+     * @param userId the user ID to retrieve the information for
+     * @param bypassUserPolicyChecks {@code true} for device policy manager use cases where device
+     *   policy restrictions that should apply to actual users can be ignored
+     */
+    TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(
+            @UserIdInt int userId, boolean bypassUserPolicyChecks);
+
+    /**
+     * Updates the configuration properties that control a device's time zone behavior.
+     *
+     * <p>This method returns {@code true} if the configuration was changed, {@code false}
+     * otherwise.
+     *
+     * <p>See {@link #getCapabilitiesAndConfig} for guarantees about visibility of updates to
+     * subsequent calls.
+     *
+     * @param userId the current user ID, supplied to make sure that the asynchronous process
+     *   that happens when users switch is completed when the call is made
+     * @param configuration the configuration changes
+     * @param bypassUserPolicyChecks {@code true} for device policy manager use cases where device
+     *   policy restrictions that should apply to actual users can be ignored
+     */
+    boolean updateConfiguration(@UserIdInt int userId, TimeZoneConfiguration configuration,
+            boolean bypassUserPolicyChecks);
+
     /** Returns a snapshot of the system time zone state. See {@link TimeZoneState} for details. */
     @NonNull
     TimeZoneState getTimeZoneState();
@@ -113,10 +157,9 @@
     boolean confirmTimeZone(@NonNull String timeZoneId);
 
     /**
-     * Suggests zero, one or more time zones for the device, or withdraws a previous suggestion if
-     * {@link GeolocationTimeZoneSuggestion#getZoneIds()} is {@code null}.
+     * Handles an event from the location-based time zone detection algorithm.
      */
-    void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion suggestion);
+    void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent event);
 
     /**
      * Suggests a time zone for the device using manually-entered (i.e. user sourced) information.
@@ -139,7 +182,7 @@
 
     /**
      * Tells the strategy that it can fall back to telephony detection while geolocation detection
-     * remains uncertain. {@link #suggestGeolocationTimeZone(GeolocationTimeZoneSuggestion)} can
+     * remains uncertain. {@link #handleLocationAlgorithmEvent(LocationAlgorithmEvent)} can
      * disable it again. See {@link TimeZoneDetectorStrategy} for details.
      */
     void enableTelephonyTimeZoneFallback();
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
index 18c8885..eecf0f7 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java
@@ -29,12 +29,16 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.time.DetectorStatusTypes;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.TelephonyTimeZoneAlgorithmStatus;
 import android.app.time.TimeZoneCapabilities;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
+import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
-import android.content.Context;
 import android.os.Handler;
 import android.os.TimestampedValue;
 import android.util.IndentingPrintWriter;
@@ -46,6 +50,7 @@
 
 import java.io.PrintWriter;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -68,16 +73,6 @@
     public interface Environment {
 
         /**
-         * Sets a {@link ConfigurationChangeListener} that will be invoked when there are any
-         * changes that could affect the content of {@link ConfigurationInternal}.
-         * This is invoked during system server setup.
-         */
-        void setConfigurationInternalChangeListener(@NonNull ConfigurationChangeListener listener);
-
-        /** Returns the {@link ConfigurationInternal} for the current user. */
-        @NonNull ConfigurationInternal getCurrentUserConfigurationInternal();
-
-        /**
          * Returns the device's currently configured time zone. May return an empty string.
          */
         @NonNull String getDeviceTimeZone();
@@ -192,11 +187,10 @@
             new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
 
     /**
-     * The latest geolocation suggestion received. If the user disabled geolocation time zone
-     * detection then the latest suggestion is cleared.
+     * The latest location algorithm event received.
      */
     @GuardedBy("this")
-    private final ReferenceWithHistory<GeolocationTimeZoneSuggestion> mLatestGeoLocationSuggestion =
+    private final ReferenceWithHistory<LocationAlgorithmEvent> mLatestLocationAlgorithmEvent =
             new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
 
     /**
@@ -206,6 +200,30 @@
     private final ReferenceWithHistory<ManualTimeZoneSuggestion> mLatestManualSuggestion =
             new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
 
+    @NonNull
+    private final ServiceConfigAccessor mServiceConfigAccessor;
+
+    /** The handler used for asynchronous operations triggered by this. */
+    @NonNull
+    private final Handler mStateChangeHandler;
+
+    @GuardedBy("this")
+    @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>();
+
+    /**
+     * A snapshot of the current detector status. A local copy is cached because it is relatively
+     * heavyweight to obtain and is used more often than it is expected to change.
+     */
+    @GuardedBy("this")
+    @NonNull
+    private TimeZoneDetectorStatus mDetectorStatus;
+
+    /**
+     * A snapshot of the current user's {@link ConfigurationInternal}. A local copy is cached
+     * because it is relatively heavyweight to obtain and is used more often than it is expected to
+     * change. Because many operations are asynchronous, this value may be out of date but should
+     * be "eventually consistent".
+     */
     @GuardedBy("this")
     @NonNull
     private ConfigurationInternal mCurrentConfigurationInternal;
@@ -229,29 +247,120 @@
      * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}.
      */
     public static TimeZoneDetectorStrategyImpl create(
-            @NonNull Context context, @NonNull Handler handler,
-            @NonNull ServiceConfigAccessor serviceConfigAccessor) {
+            @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) {
 
-        Environment environment = new EnvironmentImpl(context, handler, serviceConfigAccessor);
-        return new TimeZoneDetectorStrategyImpl(environment);
+        Environment environment = new EnvironmentImpl();
+        return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, handler, environment);
     }
 
     @VisibleForTesting
-    public TimeZoneDetectorStrategyImpl(@NonNull Environment environment) {
+    public TimeZoneDetectorStrategyImpl(
+            @NonNull ServiceConfigAccessor serviceConfigAccessor,
+            @NonNull Handler handler, @NonNull Environment environment) {
         mEnvironment = Objects.requireNonNull(environment);
+        mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor);
+        mStateChangeHandler = Objects.requireNonNull(handler);
 
         // Start with telephony fallback enabled.
         mTelephonyTimeZoneFallbackEnabled =
                 new TimestampedValue<>(mEnvironment.elapsedRealtimeMillis(), true);
 
         synchronized (this) {
-            mEnvironment.setConfigurationInternalChangeListener(
-                    this::handleConfigurationInternalChanged);
-            mCurrentConfigurationInternal = mEnvironment.getCurrentUserConfigurationInternal();
+            // Listen for config and user changes and get an initial snapshot of configuration.
+            StateChangeListener stateChangeListener = this::handleConfigurationInternalMaybeChanged;
+            mServiceConfigAccessor.addConfigurationInternalChangeListener(stateChangeListener);
+
+            // Initialize mCurrentConfigurationInternal and mDetectorStatus with their starting
+            // values.
+            updateCurrentConfigurationInternalIfRequired("TimeZoneDetectorStrategyImpl:");
         }
     }
 
     @Override
+    public synchronized TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(
+            @UserIdInt int userId, boolean bypassUserPolicyChecks) {
+        ConfigurationInternal configurationInternal;
+        if (mCurrentConfigurationInternal.getUserId() == userId) {
+            // Use the cached snapshot we have.
+            configurationInternal = mCurrentConfigurationInternal;
+        } else {
+            // This is not a common case: It would be unusual to want the configuration for a user
+            // other than the "current" user, but it is supported because it is trivial to do so.
+            // Unlike the current user config, there's no cached copy to worry about so read it
+            // directly from mServiceConfigAccessor.
+            configurationInternal = mServiceConfigAccessor.getConfigurationInternal(userId);
+        }
+        return new TimeZoneCapabilitiesAndConfig(
+                mDetectorStatus,
+                configurationInternal.asCapabilities(bypassUserPolicyChecks),
+                configurationInternal.asConfiguration());
+    }
+
+    @Override
+    public synchronized boolean updateConfiguration(
+            @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration,
+            boolean bypassUserPolicyChecks) {
+
+        // Write-through
+        boolean updateSuccessful = mServiceConfigAccessor.updateConfiguration(
+                userId, configuration, bypassUserPolicyChecks);
+
+        // The update above will trigger config update listeners asynchronously if they are needed,
+        // but that could mean an immediate call to getCapabilitiesAndConfig() for the current user
+        // wouldn't see the update. So, handle the cache update and notifications here. When the
+        // async update listener triggers it will find everything already up to date and do nothing.
+        if (updateSuccessful) {
+            String logMsg = "updateConfiguration:"
+                    + " userId=" + userId
+                    + ", configuration=" + configuration
+                    + ", bypassUserPolicyChecks=" + bypassUserPolicyChecks;
+            updateCurrentConfigurationInternalIfRequired(logMsg);
+        }
+        return updateSuccessful;
+    }
+
+    @GuardedBy("this")
+    private void updateCurrentConfigurationInternalIfRequired(@NonNull String logMsg) {
+        ConfigurationInternal newCurrentConfigurationInternal =
+                mServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        // mCurrentConfigurationInternal is null the first time this method is called.
+        ConfigurationInternal oldCurrentConfigurationInternal = mCurrentConfigurationInternal;
+
+        // If the configuration actually changed, update the cached copy synchronously and do
+        // other necessary house-keeping / (async) listener notifications.
+        if (!newCurrentConfigurationInternal.equals(oldCurrentConfigurationInternal)) {
+            mCurrentConfigurationInternal = newCurrentConfigurationInternal;
+
+            logMsg += " [oldConfiguration=" + oldCurrentConfigurationInternal
+                    + ", newConfiguration=" + newCurrentConfigurationInternal
+                    + "]";
+            logTimeZoneDebugInfo(logMsg);
+
+            // ConfigurationInternal changes can affect the detector's status.
+            updateDetectorStatus();
+
+            // The configuration and maybe the status changed so notify listeners.
+            notifyStateChangeListenersAsynchronously();
+
+            // The configuration change may have changed available suggestions or the way
+            // suggestions are used, so re-run detection.
+            doAutoTimeZoneDetection(mCurrentConfigurationInternal, logMsg);
+        }
+    }
+
+    @GuardedBy("this")
+    private void notifyStateChangeListenersAsynchronously() {
+        for (StateChangeListener listener : mStateChangeListeners) {
+            mStateChangeHandler.post(listener::onChange);
+        }
+    }
+
+    @Override
+    public synchronized void addChangeListener(StateChangeListener listener) {
+        mStateChangeListeners.add(listener);
+    }
+
+    @Override
     public synchronized boolean confirmTimeZone(@NonNull String timeZoneId) {
         Objects.requireNonNull(timeZoneId);
 
@@ -285,33 +394,39 @@
     }
 
     @Override
-    public synchronized void suggestGeolocationTimeZone(
-            @NonNull GeolocationTimeZoneSuggestion suggestion) {
-
+    public synchronized void handleLocationAlgorithmEvent(@NonNull LocationAlgorithmEvent event) {
         ConfigurationInternal currentUserConfig = mCurrentConfigurationInternal;
         if (DBG) {
-            Slog.d(LOG_TAG, "Geolocation suggestion received."
+            Slog.d(LOG_TAG, "Location algorithm event received."
                     + " currentUserConfig=" + currentUserConfig
-                    + " newSuggestion=" + suggestion);
+                    + " event=" + event);
         }
-        Objects.requireNonNull(suggestion);
+        Objects.requireNonNull(event);
 
-        // Geolocation suggestions may be stored but not used during time zone detection if the
+        // Location algorithm events may be stored but not used during time zone detection if the
         // configuration doesn't have geo time zone detection enabled. The caller is expected to
-        // withdraw a previous suggestion (i.e. submit an "uncertain" suggestion, when geo time zone
-        // detection is disabled.
+        // withdraw a previous suggestion, i.e. submit an event containing an "uncertain"
+        // suggestion, when geo time zone detection is disabled.
 
-        // The suggestion's "effective from" time is ignored: we currently assume suggestions
-        // are made in a sensible order and the most recent is always the best one to use.
-        mLatestGeoLocationSuggestion.set(suggestion);
+        // We currently assume events are made in a sensible order and the most recent is always the
+        // best one to use.
+        mLatestLocationAlgorithmEvent.set(event);
+
+        // The latest location algorithm event can affect the cached detector status, so update it
+        // and notify state change listeners as needed.
+        boolean statusChanged = updateDetectorStatus();
+        if (statusChanged) {
+            notifyStateChangeListenersAsynchronously();
+        }
 
         // Update the mTelephonyTimeZoneFallbackEnabled state if needed: a certain suggestion
         // will usually disable telephony fallback mode if it is currently enabled.
+        // TODO(b/236624675)Some provider status codes can be used to enable telephony fallback.
         disableTelephonyFallbackIfNeeded();
 
-        // Now perform auto time zone detection. The new suggestion may be used to modify the
-        // time zone setting.
-        String reason = "New geolocation time zone suggested. suggestion=" + suggestion;
+        // Now perform auto time zone detection. The new event may be used to modify the time zone
+        // setting.
+        String reason = "New location algorithm event received. event=" + event;
         doAutoTimeZoneDetection(currentUserConfig, reason);
     }
 
@@ -334,9 +449,8 @@
         String timeZoneId = suggestion.getZoneId();
         String cause = "Manual time suggestion received: suggestion=" + suggestion;
 
-        TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                currentUserConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-        TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+        TimeZoneCapabilities capabilities =
+                currentUserConfig.asCapabilities(bypassUserPolicyChecks);
         if (capabilities.getSetManualTimeZoneCapability() != CAPABILITY_POSSESSED) {
             Slog.i(LOG_TAG, "User does not have the capability needed to set the time zone manually"
                     + ": capabilities=" + capabilities
@@ -395,9 +509,9 @@
                     + mTelephonyTimeZoneFallbackEnabled;
             logTimeZoneDebugInfo(logMsg);
 
-            // mTelephonyTimeZoneFallbackEnabled and mLatestGeoLocationSuggestion interact.
-            // If there is currently a certain geolocation suggestion, then the telephony fallback
-            // value needs to be considered after changing it.
+            // mTelephonyTimeZoneFallbackEnabled and mLatestLocationAlgorithmEvent interact.
+            // If the latest event contains a "certain" geolocation suggestion, then the telephony
+            // fallback value needs to be considered after changing it.
             // With the way that the mTelephonyTimeZoneFallbackEnabled time is currently chosen
             // above, and the fact that geolocation suggestions should never have a time in the
             // future, the following call will be a no-op, and telephony fallback will remain
@@ -437,7 +551,7 @@
                 mEnvironment.getDeviceTimeZone(),
                 getLatestManualSuggestion(),
                 telephonySuggestion,
-                getLatestGeolocationSuggestion());
+                getLatestLocationAlgorithmEvent());
     }
 
     @Override
@@ -536,13 +650,15 @@
      */
     @GuardedBy("this")
     private boolean doGeolocationTimeZoneDetection(@NonNull String detectionReason) {
-        GeolocationTimeZoneSuggestion latestGeolocationSuggestion =
-                mLatestGeoLocationSuggestion.get();
-        if (latestGeolocationSuggestion == null) {
+        // Terminate early if there's nothing to do.
+        LocationAlgorithmEvent latestLocationAlgorithmEvent = mLatestLocationAlgorithmEvent.get();
+        if (latestLocationAlgorithmEvent == null
+                || latestLocationAlgorithmEvent.getSuggestion() == null) {
             return false;
         }
 
-        List<String> zoneIds = latestGeolocationSuggestion.getZoneIds();
+        GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion();
+        List<String> zoneIds = suggestion.getZoneIds();
         if (zoneIds == null) {
             // This means the originator of the suggestion is uncertain about the time zone. The
             // existing time zone setting must be left as it is but detection can go on looking for
@@ -575,13 +691,18 @@
     }
 
     /**
-     * Sets the mTelephonyTimeZoneFallbackEnabled state to {@code false} if the latest geo
-     * suggestion is a "certain" suggestion that comes after the time when telephony fallback was
-     * enabled.
+     * Sets the mTelephonyTimeZoneFallbackEnabled state to {@code false} if the latest location
+     * algorithm event contains a "certain" suggestion that comes after the time when telephony
+     * fallback was enabled.
      */
     @GuardedBy("this")
     private void disableTelephonyFallbackIfNeeded() {
-        GeolocationTimeZoneSuggestion suggestion = mLatestGeoLocationSuggestion.get();
+        LocationAlgorithmEvent latestLocationAlgorithmEvent = mLatestLocationAlgorithmEvent.get();
+        if (latestLocationAlgorithmEvent == null) {
+            return;
+        }
+
+        GeolocationTimeZoneSuggestion suggestion = latestLocationAlgorithmEvent.getSuggestion();
         boolean isLatestSuggestionCertain = suggestion != null && suggestion.getZoneIds() != null;
         if (isLatestSuggestionCertain && mTelephonyTimeZoneFallbackEnabled.getValue()) {
             // This transition ONLY changes mTelephonyTimeZoneFallbackEnabled from
@@ -735,18 +856,31 @@
         return findBestTelephonySuggestion();
     }
 
-    private synchronized void handleConfigurationInternalChanged() {
-        ConfigurationInternal currentUserConfig =
-                mEnvironment.getCurrentUserConfigurationInternal();
-        String logMsg = "handleConfigurationInternalChanged:"
-                + " oldConfiguration=" + mCurrentConfigurationInternal
-                + ", newConfiguration=" + currentUserConfig;
-        logTimeZoneDebugInfo(logMsg);
-        mCurrentConfigurationInternal = currentUserConfig;
+    /**
+     * Handles a configuration change notification.
+     */
+    private synchronized void handleConfigurationInternalMaybeChanged() {
+        String logMsg = "handleConfigurationInternalMaybeChanged:";
+        updateCurrentConfigurationInternalIfRequired(logMsg);
+    }
 
-        // The configuration change may have changed available suggestions or the way suggestions
-        // are used, so re-run detection.
-        doAutoTimeZoneDetection(currentUserConfig, logMsg);
+    /**
+     * Called whenever the information that contributes to {@link #mDetectorStatus} could have
+     * changed. Updates the cached status snapshot if required.
+     *
+     * @return true if the status had changed and has been updated
+     */
+    @GuardedBy("this")
+    private boolean updateDetectorStatus() {
+        TimeZoneDetectorStatus newDetectorStatus = createTimeZoneDetectorStatus(
+                mCurrentConfigurationInternal, mLatestLocationAlgorithmEvent.get());
+        // mDetectorStatus is null the first time this method is called.
+        TimeZoneDetectorStatus oldDetectorStatus = mDetectorStatus;
+        boolean statusChanged = !newDetectorStatus.equals(oldDetectorStatus);
+        if (statusChanged) {
+            mDetectorStatus = newDetectorStatus;
+        }
+        return statusChanged;
     }
 
     /**
@@ -758,10 +892,10 @@
 
         ipw.increaseIndent(); // level 1
         ipw.println("mCurrentConfigurationInternal=" + mCurrentConfigurationInternal);
+        ipw.println("mDetectorStatus=" + mDetectorStatus);
         final boolean bypassUserPolicyChecks = false;
         ipw.println("[Capabilities="
-                + mCurrentConfigurationInternal.createCapabilitiesAndConfig(bypassUserPolicyChecks)
-                + "]");
+                + mCurrentConfigurationInternal.asCapabilities(bypassUserPolicyChecks) + "]");
         ipw.println("mEnvironment.getDeviceTimeZone()=" + mEnvironment.getDeviceTimeZone());
         ipw.println("mEnvironment.getDeviceTimeZoneConfidence()="
                 + mEnvironment.getDeviceTimeZoneConfidence());
@@ -782,9 +916,9 @@
         mLatestManualSuggestion.dump(ipw);
         ipw.decreaseIndent(); // level 2
 
-        ipw.println("Geolocation suggestion history:");
+        ipw.println("Location algorithm event history:");
         ipw.increaseIndent(); // level 2
-        mLatestGeoLocationSuggestion.dump(ipw);
+        mLatestLocationAlgorithmEvent.dump(ipw);
         ipw.decreaseIndent(); // level 2
 
         ipw.println("Telephony suggestion history:");
@@ -798,6 +932,7 @@
      * A method used to inspect strategy state during tests. Not intended for general use.
      */
     @VisibleForTesting
+    @Nullable
     public synchronized ManualTimeZoneSuggestion getLatestManualSuggestion() {
         return mLatestManualSuggestion.get();
     }
@@ -806,6 +941,7 @@
      * A method used to inspect strategy state during tests. Not intended for general use.
      */
     @VisibleForTesting
+    @Nullable
     public synchronized QualifiedTelephonyTimeZoneSuggestion getLatestTelephonySuggestion(
             int slotIndex) {
         return mTelephonySuggestionsBySlotIndex.get(slotIndex);
@@ -815,8 +951,9 @@
      * A method used to inspect strategy state during tests. Not intended for general use.
      */
     @VisibleForTesting
-    public synchronized GeolocationTimeZoneSuggestion getLatestGeolocationSuggestion() {
-        return mLatestGeoLocationSuggestion.get();
+    @Nullable
+    public synchronized LocationAlgorithmEvent getLatestLocationAlgorithmEvent() {
+        return mLatestLocationAlgorithmEvent.get();
     }
 
     @VisibleForTesting
@@ -824,6 +961,16 @@
         return mTelephonyTimeZoneFallbackEnabled.getValue();
     }
 
+    @VisibleForTesting
+    public synchronized ConfigurationInternal getCachedCapabilitiesAndConfigForTests() {
+        return mCurrentConfigurationInternal;
+    }
+
+    @VisibleForTesting
+    public synchronized TimeZoneDetectorStatus getCachedDetectorStatusForTests() {
+        return mDetectorStatus;
+    }
+
     /**
      * A {@link TelephonyTimeZoneSuggestion} with additional qualifying metadata.
      */
@@ -877,4 +1024,42 @@
     private static String formatDebugString(TimestampedValue<?> value) {
         return value.getValue() + " @ " + Duration.ofMillis(value.getReferenceTimeMillis());
     }
+
+    @NonNull
+    private static TimeZoneDetectorStatus createTimeZoneDetectorStatus(
+            @NonNull ConfigurationInternal currentConfigurationInternal,
+            @Nullable LocationAlgorithmEvent latestLocationAlgorithmEvent) {
+
+        int detectorStatus;
+        if (!currentConfigurationInternal.isAutoDetectionSupported()) {
+            detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_NOT_SUPPORTED;
+        } else if (currentConfigurationInternal.getAutoDetectionEnabledBehavior()) {
+            detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+        } else {
+            detectorStatus = DetectorStatusTypes.DETECTOR_STATUS_NOT_RUNNING;
+        }
+
+        TelephonyTimeZoneAlgorithmStatus telephonyAlgorithmStatus =
+                createTelephonyAlgorithmStatus(currentConfigurationInternal);
+
+        LocationTimeZoneAlgorithmStatus locationAlgorithmStatus =
+                latestLocationAlgorithmEvent == null ? LocationTimeZoneAlgorithmStatus.UNKNOWN
+                        : latestLocationAlgorithmEvent.getAlgorithmStatus();
+
+        return new TimeZoneDetectorStatus(
+                detectorStatus, telephonyAlgorithmStatus, locationAlgorithmStatus);
+    }
+
+    @NonNull
+    private static TelephonyTimeZoneAlgorithmStatus createTelephonyAlgorithmStatus(
+            @NonNull ConfigurationInternal currentConfigurationInternal) {
+        int algorithmStatus;
+        if (!currentConfigurationInternal.isTelephonyDetectionSupported()) {
+            algorithmStatus = DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED;
+        } else {
+            // The telephony detector is passive, so we treat it as "running".
+            algorithmStatus = DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+        }
+        return new TelephonyTimeZoneAlgorithmStatus(algorithmStatus);
+    }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
index a1de294..71aa10d 100644
--- a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
@@ -53,7 +53,7 @@
     }
 
     @Override
-    void onInitialize() {
+    boolean onInitialize() {
         mProxy.initialize(new LocationTimeZoneProviderProxy.Listener() {
             @Override
             public void onReportTimeZoneProviderEvent(
@@ -71,6 +71,7 @@
                 handleTemporaryFailure("onProviderUnbound()");
             }
         });
+        return true;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java
new file mode 100644
index 0000000..5d6184e
--- /dev/null
+++ b/services/core/java/com/android/server/timezonedetector/location/DisabledLocationTimeZoneProvider.java
@@ -0,0 +1,86 @@
+/*
+ * 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.timezonedetector.location;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.IndentingPrintWriter;
+
+import java.time.Duration;
+
+/**
+ * A {@link LocationTimeZoneProvider} that provides minimal responses needed to operate correctly
+ * when there is no "real" provider configured / enabled. This is used when the platform supports
+ * more providers than are needed for an Android deployment.
+ *
+ * <p>That is, the {@link LocationTimeZoneProviderController} supports a primary and a secondary
+ * {@link LocationTimeZoneProvider}, but if only a primary is configured, the secondary provider
+ * config will marked as "disabled" and the {@link LocationTimeZoneProvider} implementation will use
+ * {@link DisabledLocationTimeZoneProvider}. The {@link DisabledLocationTimeZoneProvider} fails
+ * initialization and immediately moves to a "permanent failure" state, which ensures the {@link
+ * LocationTimeZoneProviderController} correctly categorizes it and won't attempt to use it.
+ */
+class DisabledLocationTimeZoneProvider extends LocationTimeZoneProvider {
+
+    DisabledLocationTimeZoneProvider(
+            @NonNull ProviderMetricsLogger providerMetricsLogger,
+            @NonNull ThreadingDomain threadingDomain,
+            @NonNull String providerName,
+            boolean recordStateChanges) {
+        super(providerMetricsLogger, threadingDomain, providerName, x -> x, recordStateChanges);
+    }
+
+    @Override
+    boolean onInitialize() {
+        // Fail initialization, preventing further use.
+        return false;
+    }
+
+    @Override
+    void onDestroy() {
+    }
+
+    @Override
+    void onStartUpdates(@NonNull Duration initializationTimeout,
+            @NonNull Duration eventFilteringAgeThreshold) {
+        throw new UnsupportedOperationException("Provider is disabled");
+    }
+
+    @Override
+    void onStopUpdates() {
+        throw new UnsupportedOperationException("Provider is disabled");
+    }
+
+    @Override
+    public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
+        synchronized (mSharedLock) {
+            ipw.println("{DisabledLocationTimeZoneProvider}");
+            ipw.println("mProviderName=" + mProviderName);
+            ipw.println("mCurrentState=" + mCurrentState);
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (mSharedLock) {
+            return "DisabledLocationTimeZoneProvider{"
+                    + "mProviderName=" + mProviderName
+                    + ", mCurrentState=" + mCurrentState
+                    + '}';
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
index 36ab111d..8d98544 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
@@ -447,11 +447,18 @@
 
         @NonNull
         LocationTimeZoneProvider createProvider() {
-            LocationTimeZoneProviderProxy proxy = createProxy();
             ProviderMetricsLogger providerMetricsLogger = new RealProviderMetricsLogger(mIndex);
-            return new BinderLocationTimeZoneProvider(
-                    providerMetricsLogger, mThreadingDomain, mName, proxy,
-                    mServiceConfigAccessor.getRecordStateChangesForTests());
+
+            String mode = getMode();
+            if (Objects.equals(mode, PROVIDER_MODE_DISABLED)) {
+                return new DisabledLocationTimeZoneProvider(providerMetricsLogger, mThreadingDomain,
+                        mName, mServiceConfigAccessor.getRecordStateChangesForTests());
+            } else {
+                LocationTimeZoneProviderProxy proxy = createBinderProxy();
+                return new BinderLocationTimeZoneProvider(
+                        providerMetricsLogger, mThreadingDomain, mName, proxy,
+                        mServiceConfigAccessor.getRecordStateChangesForTests());
+            }
         }
 
         @Override
@@ -460,17 +467,6 @@
             ipw.printf("getPackageName()=%s\n", getPackageName());
         }
 
-        @NonNull
-        private LocationTimeZoneProviderProxy createProxy() {
-            String mode = getMode();
-            if (Objects.equals(mode, PROVIDER_MODE_DISABLED)) {
-                return new NullLocationTimeZoneProviderProxy(mContext, mThreadingDomain);
-            } else {
-                // mode == PROVIDER_MODE_OVERRIDE_ENABLED (or unknown).
-                return createRealProxy();
-            }
-        }
-
         /** Returns the mode of the provider (enabled/disabled). */
         @NonNull
         private String getMode() {
@@ -482,7 +478,7 @@
         }
 
         @NonNull
-        private RealLocationTimeZoneProviderProxy createRealProxy() {
+        private RealLocationTimeZoneProviderProxy createBinderProxy() {
             String providerServiceAction = mServiceAction;
             boolean isTestProvider = isTestProvider();
             String providerPackageName = getPackageName();
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java
index 1f752f4..e90a1fe 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerServiceState.java
@@ -19,7 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
-import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.LocationAlgorithmEvent;
 import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState;
 import com.android.server.timezonedetector.location.LocationTimeZoneProviderController.State;
 
@@ -32,14 +32,14 @@
 final class LocationTimeZoneManagerServiceState {
 
     private final @State String mControllerState;
-    @Nullable private final GeolocationTimeZoneSuggestion mLastSuggestion;
+    @Nullable private final LocationAlgorithmEvent mLastEvent;
     @NonNull private final List<@State String> mControllerStates;
     @NonNull private final List<ProviderState> mPrimaryProviderStates;
     @NonNull private final List<ProviderState> mSecondaryProviderStates;
 
     LocationTimeZoneManagerServiceState(@NonNull Builder builder) {
         mControllerState = builder.mControllerState;
-        mLastSuggestion = builder.mLastSuggestion;
+        mLastEvent = builder.mLastEvent;
         mControllerStates = Objects.requireNonNull(builder.mControllerStates);
         mPrimaryProviderStates = Objects.requireNonNull(builder.mPrimaryProviderStates);
         mSecondaryProviderStates = Objects.requireNonNull(builder.mSecondaryProviderStates);
@@ -50,8 +50,8 @@
     }
 
     @Nullable
-    public GeolocationTimeZoneSuggestion getLastSuggestion() {
-        return mLastSuggestion;
+    public LocationAlgorithmEvent getLastEvent() {
+        return mLastEvent;
     }
 
     @NonNull
@@ -73,7 +73,7 @@
     public String toString() {
         return "LocationTimeZoneManagerServiceState{"
                 + "mControllerState=" + mControllerState
-                + ", mLastSuggestion=" + mLastSuggestion
+                + ", mLastEvent=" + mLastEvent
                 + ", mControllerStates=" + mControllerStates
                 + ", mPrimaryProviderStates=" + mPrimaryProviderStates
                 + ", mSecondaryProviderStates=" + mSecondaryProviderStates
@@ -83,7 +83,7 @@
     static final class Builder {
 
         private @State String mControllerState;
-        private GeolocationTimeZoneSuggestion mLastSuggestion;
+        private LocationAlgorithmEvent mLastEvent;
         private List<@State String> mControllerStates;
         private List<ProviderState> mPrimaryProviderStates;
         private List<ProviderState> mSecondaryProviderStates;
@@ -95,8 +95,8 @@
         }
 
         @NonNull
-        Builder setLastSuggestion(@NonNull GeolocationTimeZoneSuggestion lastSuggestion) {
-            mLastSuggestion = Objects.requireNonNull(lastSuggestion);
+        Builder setLastEvent(@NonNull LocationAlgorithmEvent lastEvent) {
+            mLastEvent = Objects.requireNonNull(lastEvent);
             return this;
         }
 
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
index 60bbea7..cefd0b5 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
@@ -15,6 +15,10 @@
  */
 package com.android.server.timezonedetector.location;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_UNKNOWN;
 import static android.app.time.LocationTimeZoneManager.DUMP_STATE_OPTION_PROTO;
 import static android.app.time.LocationTimeZoneManager.NULL_PACKAGE_NAME_TOKEN;
 import static android.app.time.LocationTimeZoneManager.SERVICE_NAME;
@@ -51,9 +55,14 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
 import android.app.time.GeolocationTimeZoneSuggestionProto;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.LocationTimeZoneAlgorithmStatusProto;
 import android.app.time.LocationTimeZoneManagerProto;
 import android.app.time.LocationTimeZoneManagerServiceStateProto;
+import android.app.time.LocationTimeZoneProviderEventProto;
+import android.app.time.TimeZoneDetectorProto;
 import android.app.time.TimeZoneProviderStateProto;
 import android.app.timezonedetector.TimeZoneDetector;
 import android.os.ShellCommand;
@@ -62,6 +71,7 @@
 
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.LocationAlgorithmEvent;
 import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum;
 import com.android.server.timezonedetector.location.LocationTimeZoneProviderController.State;
 
@@ -239,19 +249,39 @@
             outputStream = new DualDumpOutputStream(
                     new IndentingPrintWriter(getOutPrintWriter(), "  "));
         }
-        if (state.getLastSuggestion() != null) {
-            GeolocationTimeZoneSuggestion lastSuggestion = state.getLastSuggestion();
-            long lastSuggestionToken = outputStream.start(
-                    "last_suggestion", LocationTimeZoneManagerServiceStateProto.LAST_SUGGESTION);
-            for (String zoneId : lastSuggestion.getZoneIds()) {
-                outputStream.write(
-                        "zone_ids" , GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId);
+
+        if (state.getLastEvent() != null) {
+            LocationAlgorithmEvent lastEvent = state.getLastEvent();
+            long lastEventToken = outputStream.start(
+                    "last_event", LocationTimeZoneManagerServiceStateProto.LAST_EVENT);
+
+            // lastEvent.algorithmStatus
+            LocationTimeZoneAlgorithmStatus algorithmStatus = lastEvent.getAlgorithmStatus();
+            long algorithmStatusToken = outputStream.start(
+                    "algorithm_status", LocationTimeZoneProviderEventProto.ALGORITHM_STATUS);
+            outputStream.write("status", LocationTimeZoneAlgorithmStatusProto.STATUS,
+                    convertDetectionAlgorithmStatusToEnumToProtoEnum(algorithmStatus.getStatus()));
+            outputStream.end(algorithmStatusToken);
+
+            // lastEvent.suggestion
+            if (lastEvent.getSuggestion() != null) {
+                long suggestionToken = outputStream.start(
+                        "suggestion", LocationTimeZoneProviderEventProto.SUGGESTION);
+                GeolocationTimeZoneSuggestion lastSuggestion = lastEvent.getSuggestion();
+                for (String zoneId : lastSuggestion.getZoneIds()) {
+                    outputStream.write(
+                            "zone_ids", GeolocationTimeZoneSuggestionProto.ZONE_IDS, zoneId);
+                }
+                outputStream.end(suggestionToken);
             }
-            for (String debugInfo : lastSuggestion.getDebugInfo()) {
+
+            // lastEvent.debugInfo
+            for (String debugInfo : lastEvent.getDebugInfo()) {
                 outputStream.write(
-                        "debug_info", GeolocationTimeZoneSuggestionProto.DEBUG_INFO, debugInfo);
+                        "debug_info", LocationTimeZoneProviderEventProto.DEBUG_INFO, debugInfo);
             }
-            outputStream.end(lastSuggestionToken);
+
+            outputStream.end(lastEventToken);
         }
 
         writeControllerStates(outputStream, state.getControllerStates());
@@ -330,6 +360,22 @@
         }
     }
 
+    private static int convertDetectionAlgorithmStatusToEnumToProtoEnum(
+            @DetectionAlgorithmStatus int statusEnum) {
+        switch (statusEnum) {
+            case DETECTION_ALGORITHM_STATUS_UNKNOWN:
+                return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_UNKNOWN;
+            case DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED:
+                return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_NOT_SUPPORTED;
+            case DETECTION_ALGORITHM_STATUS_NOT_RUNNING:
+                return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+            case DETECTION_ALGORITHM_STATUS_RUNNING:
+                return TimeZoneDetectorProto.DETECTION_ALGORITHM_STATUS_RUNNING;
+            default:
+                throw new IllegalArgumentException("Unknown statusEnum=" + statusEnum);
+        }
+    }
+
     private void reportError(@NonNull Throwable e) {
         PrintWriter errPrintWriter = getErrPrintWriter();
         errPrintWriter.println("Error: ");
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
index 90540b0..ba7c328 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
@@ -16,6 +16,10 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY;
 import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
 import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
 import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
@@ -33,9 +37,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -294,6 +300,40 @@
                     || stateEnum == PROVIDER_STATE_DESTROYED;
         }
 
+        /**
+         * Maps the internal state enum value to one of the status values exposed to the layers
+         * above.
+         */
+        public @ProviderStatus int getProviderStatus() {
+            switch (stateEnum) {
+                case PROVIDER_STATE_STARTED_INITIALIZING:
+                    return PROVIDER_STATUS_NOT_READY;
+                case PROVIDER_STATE_STARTED_CERTAIN:
+                    return PROVIDER_STATUS_IS_CERTAIN;
+                case PROVIDER_STATE_STARTED_UNCERTAIN:
+                    return PROVIDER_STATUS_IS_UNCERTAIN;
+                case PROVIDER_STATE_PERM_FAILED:
+                    // Perm failed means the providers wasn't configured, configured properly,
+                    // or has removed itself for other reasons, e.g. turned-down server.
+                    return PROVIDER_STATUS_NOT_PRESENT;
+                case PROVIDER_STATE_STOPPED:
+                case PROVIDER_STATE_DESTROYED:
+                    // This is a "safe" default that best describes a provider that isn't in one of
+                    // the more obviously mapped states.
+                    return PROVIDER_STATUS_NOT_READY;
+                case PROVIDER_STATE_UNKNOWN:
+                default:
+                    throw new IllegalStateException(
+                            "Unknown state enum:" + prettyPrintStateEnum(stateEnum));
+            }
+        }
+
+        /** Returns the status reported by the provider, if available. */
+        @Nullable
+        TimeZoneProviderStatus getReportedStatus() {
+            return event == null ? null : event.getTimeZoneProviderStatus();
+        }
+
         @Override
         public String toString() {
             // this.provider is omitted deliberately to avoid recursion, since the provider holds
@@ -408,13 +448,21 @@
             currentState = currentState.newState(PROVIDER_STATE_STOPPED, null, null, "initialize");
             setCurrentState(currentState, false);
 
+            boolean initializationSuccess;
+            String initializationFailureReason;
             // Guard against uncaught exceptions due to initialization problems.
             try {
-                onInitialize();
+                initializationSuccess = onInitialize();
+                initializationFailureReason = "onInitialize() returned false";
             } catch (RuntimeException e) {
-                warnLog("Unable to initialize the provider", e);
+                warnLog("Unable to initialize the provider due to exception", e);
+                initializationSuccess = false;
+                initializationFailureReason = "onInitialize() threw exception:" + e.getMessage();
+            }
+
+            if (!initializationSuccess) {
                 currentState = currentState.newState(PROVIDER_STATE_PERM_FAILED, null, null,
-                        "Failed to initialize: " + e.getMessage());
+                        "Failed to initialize: " + initializationFailureReason);
                 setCurrentState(currentState, true);
             }
         }
@@ -422,9 +470,12 @@
 
     /**
      * Implemented by subclasses to do work during {@link #initialize}.
+     *
+     * @return returns {@code true} on success, {@code false} if the provider should be considered
+     *   "permanently failed" / disabled
      */
     @GuardedBy("mSharedLock")
-    abstract void onInitialize();
+    abstract boolean onInitialize();
 
     /**
      * Destroys the provider. Called after the provider is stopped. This instance will not be called
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
index a9b9884..ed7ea00 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
@@ -35,6 +35,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringDef;
+import android.app.time.DetectorStatusTypes;
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.LocationTimeZoneAlgorithmStatus.ProviderStatus;
 import android.service.timezone.TimeZoneProviderEvent;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
@@ -44,6 +48,7 @@
 import com.android.server.timezonedetector.ConfigurationInternal;
 import com.android.server.timezonedetector.Dumpable;
 import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.LocationAlgorithmEvent;
 import com.android.server.timezonedetector.ReferenceWithHistory;
 import com.android.server.timezonedetector.location.ThreadingDomain.SingleRunnableQueue;
 
@@ -83,8 +88,7 @@
  * <p>All incoming calls except for {@link
  * LocationTimeZoneProviderController#dump(android.util.IndentingPrintWriter, String[])} must be
  * made on the {@link android.os.Handler} thread of the {@link ThreadingDomain} passed to {@link
- * #LocationTimeZoneProviderController(ThreadingDomain, LocationTimeZoneProvider,
- * LocationTimeZoneProvider)}.
+ * #LocationTimeZoneProviderController}.
  *
  * <p>Provider / controller integration notes:
  *
@@ -172,10 +176,10 @@
     @GuardedBy("mSharedLock")
     private final ReferenceWithHistory<@State String> mState = new ReferenceWithHistory<>(10);
 
-    /** Contains the last suggestion actually made, if there is one. */
+    /** Contains the last event reported, if there is one. */
     @GuardedBy("mSharedLock")
     @Nullable
-    private GeolocationTimeZoneSuggestion mLastSuggestion;
+    private LocationAlgorithmEvent mLastEvent;
 
     LocationTimeZoneProviderController(@NonNull ThreadingDomain threadingDomain,
             @NonNull MetricsLogger metricsLogger,
@@ -213,7 +217,7 @@
             setState(STATE_PROVIDERS_INITIALIZING);
             mPrimaryProvider.initialize(providerListener);
             mSecondaryProvider.initialize(providerListener);
-            setState(STATE_STOPPED);
+            setStateAndReportStatusOnlyEvent(STATE_STOPPED, "initialize()");
 
             alterProvidersStartedStateIfRequired(
                     null /* oldConfiguration */, mCurrentUserConfiguration);
@@ -273,13 +277,51 @@
             // Enter destroyed state.
             mPrimaryProvider.destroy();
             mSecondaryProvider.destroy();
-            setState(STATE_DESTROYED);
+            setStateAndReportStatusOnlyEvent(STATE_DESTROYED, "destroy()");
         }
     }
 
     /**
-     * Updates {@link #mState} if needed, and performs all the record-keeping / callbacks associated
-     * with state changes.
+     * Sets the state and reports an event containing the algorithm status and a {@code null}
+     * suggestion.
+     */
+    @GuardedBy("mSharedLock")
+    private void setStateAndReportStatusOnlyEvent(@State String state, @NonNull String reason) {
+        setState(state);
+
+        final GeolocationTimeZoneSuggestion suggestion = null;
+        LocationAlgorithmEvent event =
+                new LocationAlgorithmEvent(generateCurrentAlgorithmStatus(), suggestion);
+        event.addDebugInfo(reason);
+        reportEvent(event);
+    }
+
+    /**
+     * Reports an event containing the algorithm status and the supplied suggestion.
+     */
+    @GuardedBy("mSharedLock")
+    private void reportSuggestionEvent(
+            @NonNull GeolocationTimeZoneSuggestion suggestion, @NonNull String reason) {
+        LocationTimeZoneAlgorithmStatus algorithmStatus = generateCurrentAlgorithmStatus();
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                algorithmStatus, suggestion);
+        event.addDebugInfo(reason);
+        reportEvent(event);
+    }
+
+    /**
+     * Sends an event immediately. This method updates {@link #mLastEvent}.
+     */
+    @GuardedBy("mSharedLock")
+    private void reportEvent(@NonNull LocationAlgorithmEvent event) {
+        debugLog("makeSuggestion: suggestion=" + event);
+        mCallback.sendEvent(event);
+        mLastEvent = event;
+    }
+
+    /**
+     * Updates the state if needed. This includes setting {@link #mState} and performing all the
+     * record-keeping / callbacks associated with state changes.
      */
     @GuardedBy("mSharedLock")
     private void setState(@State String state) {
@@ -300,17 +342,7 @@
         // By definition, if both providers are stopped, the controller is uncertain.
         cancelUncertaintyTimeout();
 
-        // If a previous "certain" suggestion has been made, then a new "uncertain"
-        // suggestion must now be made to indicate the controller {does not / no longer has}
-        // an opinion and will not be sending further updates (until at least the providers are
-        // re-started).
-        if (Objects.equals(mState.get(), STATE_CERTAIN)) {
-            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
-                    mEnvironment.elapsedRealtimeMillis(),
-                    "Withdraw previous suggestion, providers are stopping: " + reason);
-            makeSuggestion(suggestion, STATE_UNCERTAIN);
-        }
-        setState(STATE_STOPPED);
+        setStateAndReportStatusOnlyEvent(STATE_STOPPED, "Providers stopped: " + reason);
     }
 
     @GuardedBy("mSharedLock")
@@ -381,7 +413,7 @@
         //    timeout started when the primary entered {started uncertain} should be cancelled.
 
         if (newIsGeoDetectionExecutionEnabled) {
-            setState(STATE_INITIALIZING);
+            setStateAndReportStatusOnlyEvent(STATE_INITIALIZING, "initializing()");
 
             // Try to start the primary provider.
             tryStartProvider(mPrimaryProvider, newConfiguration);
@@ -397,13 +429,11 @@
                 ProviderState newSecondaryState = mSecondaryProvider.getCurrentState();
                 if (!newSecondaryState.isStarted()) {
                     // If both providers are {perm failed} then the controller immediately
-                    // reports uncertain.
-                    GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
-                            mEnvironment.elapsedRealtimeMillis(),
-                            "Providers are failed:"
-                                    + " primary=" + mPrimaryProvider.getCurrentState()
-                                    + " secondary=" + mPrimaryProvider.getCurrentState());
-                    makeSuggestion(suggestion, STATE_FAILED);
+                    // reports the failure.
+                    String reason = "Providers are failed:"
+                            + " primary=" + mPrimaryProvider.getCurrentState()
+                            + " secondary=" + mPrimaryProvider.getCurrentState();
+                    setStateAndReportStatusOnlyEvent(STATE_FAILED, reason);
                 }
             }
         } else {
@@ -537,12 +567,10 @@
 
             // If both providers are now terminated, then a suggestion must be sent informing the
             // time zone detector that there are no further updates coming in the future.
-            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
-                    mEnvironment.elapsedRealtimeMillis(),
-                    "Both providers are terminated:"
-                            + " primary=" + primaryCurrentState.provider
-                            + ", secondary=" + secondaryCurrentState.provider);
-            makeSuggestion(suggestion, STATE_FAILED);
+            String reason = "Both providers are terminated:"
+                    + " primary=" + primaryCurrentState.provider
+                    + ", secondary=" + secondaryCurrentState.provider;
+            setStateAndReportStatusOnlyEvent(STATE_FAILED, reason);
         }
     }
 
@@ -615,6 +643,9 @@
 
         TimeZoneProviderSuggestion providerSuggestion = providerEvent.getSuggestion();
 
+        // Set the current state so it is correct when the suggestion event is created.
+        setState(STATE_CERTAIN);
+
         // For the suggestion's effectiveFromElapsedMillis, use the time embedded in the provider's
         // suggestion (which indicates the time when the provider detected the location used to
         // establish the time zone).
@@ -623,15 +654,13 @@
         // this would hinder the ability for the time_zone_detector to judge which suggestions are
         // based on newer information when comparing suggestions between different sources.
         long effectiveFromElapsedMillis = providerSuggestion.getElapsedRealtimeMillis();
-        GeolocationTimeZoneSuggestion geoSuggestion =
+        GeolocationTimeZoneSuggestion suggestion =
                 GeolocationTimeZoneSuggestion.createCertainSuggestion(
                         effectiveFromElapsedMillis, providerSuggestion.getTimeZoneIds());
-
-        String debugInfo = "Event received provider=" + provider
+        String debugInfo = "Provider event received: provider=" + provider
                 + ", providerEvent=" + providerEvent
                 + ", suggestionCreationTime=" + mEnvironment.elapsedRealtimeMillis();
-        geoSuggestion.addDebugInfo(debugInfo);
-        makeSuggestion(geoSuggestion, STATE_CERTAIN);
+        reportSuggestionEvent(suggestion, debugInfo);
     }
 
     @Override
@@ -647,7 +676,7 @@
                     + mEnvironment.getProviderInitializationTimeoutFuzz());
             ipw.println("uncertaintyDelay=" + mEnvironment.getUncertaintyDelay());
             ipw.println("mState=" + mState.get());
-            ipw.println("mLastSuggestion=" + mLastSuggestion);
+            ipw.println("mLastEvent=" + mLastEvent);
 
             ipw.println("State history:");
             ipw.increaseIndent(); // level 2
@@ -668,19 +697,6 @@
         }
     }
 
-    /**
-     * Sends an immediate suggestion and enters a new state if needed. This method updates
-     * mLastSuggestion and changes mStateEnum / reports the new state for metrics.
-     */
-    @GuardedBy("mSharedLock")
-    private void makeSuggestion(@NonNull GeolocationTimeZoneSuggestion suggestion,
-            @State String newState) {
-        debugLog("makeSuggestion: suggestion=" + suggestion);
-        mCallback.suggest(suggestion);
-        mLastSuggestion = suggestion;
-        setState(newState);
-    }
-
     /** Clears the uncertainty timeout. */
     @GuardedBy("mSharedLock")
     private void cancelUncertaintyTimeout() {
@@ -688,18 +704,16 @@
     }
 
     /**
-     * Called when a provider has become "uncertain" about the time zone.
+     * Called when a provider has reported it is "uncertain" about the time zone.
      *
      * <p>A provider is expected to report its uncertainty as soon as it becomes uncertain, as
      * this enables the most flexibility for the controller to start other providers when there are
-     * multiple ones available. The controller is therefore responsible for deciding when to make a
-     * "uncertain" suggestion to the downstream time zone detector.
+     * multiple ones available. The controller is therefore responsible for deciding when to pass
+     * the "uncertain" suggestion to the downstream time zone detector.
      *
      * <p>This method schedules an "uncertainty" timeout (if one isn't already scheduled) to be
      * triggered later if nothing else preempts it. It can be preempted if the provider becomes
-     * certain (or does anything else that calls {@link
-     * #makeSuggestion(GeolocationTimeZoneSuggestion, String)}) within {@link
-     * Environment#getUncertaintyDelay()}. Preemption causes the scheduled
+     * certain within {@link Environment#getUncertaintyDelay()}. Preemption causes the scheduled
      * "uncertainty" timeout to be cancelled. If the provider repeatedly sends uncertainty events
      * within the uncertainty delay period, those events are effectively ignored (i.e. the timeout
      * is not reset each time).
@@ -741,6 +755,8 @@
         synchronized (mSharedLock) {
             long afterUncertaintyTimeoutElapsedMillis = mEnvironment.elapsedRealtimeMillis();
 
+            setState(STATE_UNCERTAIN);
+
             // For the effectiveFromElapsedMillis suggestion property, use the
             // uncertaintyStartedElapsedMillis. This is the time when the provider first reported
             // uncertainty, i.e. before the uncertainty timeout.
@@ -749,30 +765,65 @@
             // the location_time_zone_manager finally confirms that the time zone was uncertain,
             // but the suggestion property allows the information to be back-dated, which should
             // help when comparing suggestions from different sources.
-            GeolocationTimeZoneSuggestion suggestion = createUncertainSuggestion(
-                    uncertaintyStartedElapsedMillis,
-                    "Uncertainty timeout triggered for " + provider.getName() + ":"
-                            + " primary=" + mPrimaryProvider
-                            + ", secondary=" + mSecondaryProvider
-                            + ", uncertaintyStarted="
-                            + Duration.ofMillis(uncertaintyStartedElapsedMillis)
-                            + ", afterUncertaintyTimeout="
-                            + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis)
-                            + ", uncertaintyDelay=" + uncertaintyDelay
-            );
-            makeSuggestion(suggestion, STATE_UNCERTAIN);
+            GeolocationTimeZoneSuggestion suggestion =
+                    GeolocationTimeZoneSuggestion.createUncertainSuggestion(
+                            uncertaintyStartedElapsedMillis);
+            String debugInfo = "Uncertainty timeout triggered for " + provider.getName() + ":"
+                    + " primary=" + mPrimaryProvider
+                    + ", secondary=" + mSecondaryProvider
+                    + ", uncertaintyStarted="
+                    + Duration.ofMillis(uncertaintyStartedElapsedMillis)
+                    + ", afterUncertaintyTimeout="
+                    + Duration.ofMillis(afterUncertaintyTimeoutElapsedMillis)
+                    + ", uncertaintyDelay=" + uncertaintyDelay;
+            reportSuggestionEvent(suggestion, debugInfo);
         }
     }
 
+    @GuardedBy("mSharedLock")
     @NonNull
-    private static GeolocationTimeZoneSuggestion createUncertainSuggestion(
-            @ElapsedRealtimeLong long effectiveFromElapsedMillis,
-            @NonNull String reason) {
-        GeolocationTimeZoneSuggestion suggestion =
-                GeolocationTimeZoneSuggestion.createUncertainSuggestion(
-                        effectiveFromElapsedMillis);
-        suggestion.addDebugInfo(reason);
-        return suggestion;
+    private LocationTimeZoneAlgorithmStatus generateCurrentAlgorithmStatus() {
+        @State String controllerState = mState.get();
+        ProviderState primaryProviderState = mPrimaryProvider.getCurrentState();
+        ProviderState secondaryProviderState = mSecondaryProvider.getCurrentState();
+        return createAlgorithmStatus(controllerState, primaryProviderState, secondaryProviderState);
+    }
+
+    @NonNull
+    private static LocationTimeZoneAlgorithmStatus createAlgorithmStatus(
+            @NonNull @State String controllerState,
+            @NonNull ProviderState primaryProviderState,
+            @NonNull ProviderState secondaryProviderState) {
+
+        @DetectionAlgorithmStatus int algorithmStatus =
+                mapControllerStateToDetectionAlgorithmStatus(controllerState);
+        @ProviderStatus int primaryProviderStatus = primaryProviderState.getProviderStatus();
+        @ProviderStatus int secondaryProviderStatus = secondaryProviderState.getProviderStatus();
+
+        // Neither provider is running. The algorithm is not running.
+        return new LocationTimeZoneAlgorithmStatus(algorithmStatus,
+                primaryProviderStatus, primaryProviderState.getReportedStatus(),
+                secondaryProviderStatus, secondaryProviderState.getReportedStatus());
+    }
+
+    /**
+     * Maps the internal state enum value to one of the status values exposed to the layers above.
+     */
+    private static @DetectionAlgorithmStatus int mapControllerStateToDetectionAlgorithmStatus(
+            @NonNull @State String controllerState) {
+        switch (controllerState) {
+            case STATE_INITIALIZING:
+            case STATE_PROVIDERS_INITIALIZING:
+            case STATE_CERTAIN:
+            case STATE_UNCERTAIN:
+                return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+            case STATE_STOPPED:
+            case STATE_DESTROYED:
+            case STATE_FAILED:
+            case STATE_UNKNOWN:
+            default:
+                return DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+        }
     }
 
     /**
@@ -798,8 +849,8 @@
         synchronized (mSharedLock) {
             LocationTimeZoneManagerServiceState.Builder builder =
                     new LocationTimeZoneManagerServiceState.Builder();
-            if (mLastSuggestion != null) {
-                builder.setLastSuggestion(mLastSuggestion);
+            if (mLastEvent != null) {
+                builder.setLastEvent(mLastEvent);
             }
             builder.setControllerState(mState.get())
                     .setStateChanges(mRecordedStates)
@@ -867,17 +918,15 @@
     abstract static class Callback {
 
         @NonNull protected final ThreadingDomain mThreadingDomain;
-        @NonNull protected final Object mSharedLock;
 
         Callback(@NonNull ThreadingDomain threadingDomain) {
             mThreadingDomain = Objects.requireNonNull(threadingDomain);
-            mSharedLock = threadingDomain.getLockObject();
         }
 
         /**
          * Suggests the latest time zone state for the device.
          */
-        abstract void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion);
+        abstract void sendEvent(@NonNull LocationAlgorithmEvent event);
     }
 
     /**
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java
index 0c751aa..7eb7e01 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerCallbackImpl.java
@@ -19,7 +19,7 @@
 import android.annotation.NonNull;
 
 import com.android.server.LocalServices;
-import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.LocationAlgorithmEvent;
 import com.android.server.timezonedetector.TimeZoneDetectorInternal;
 
 /**
@@ -34,11 +34,11 @@
     }
 
     @Override
-    void suggest(@NonNull GeolocationTimeZoneSuggestion suggestion) {
+    void sendEvent(@NonNull LocationAlgorithmEvent event) {
         mThreadingDomain.assertCurrentThread();
 
         TimeZoneDetectorInternal timeZoneDetector =
                 LocalServices.getService(TimeZoneDetectorInternal.class);
-        timeZoneDetector.suggestGeolocationTimeZone(suggestion);
+        timeZoneDetector.handleLocationAlgorithmEvent(event);
     }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerEnvironmentImpl.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerEnvironmentImpl.java
index e7d16c8..5eeafc1 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerEnvironmentImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerEnvironmentImpl.java
@@ -20,9 +20,9 @@
 import android.annotation.NonNull;
 import android.os.SystemClock;
 
-import com.android.server.timezonedetector.ConfigurationChangeListener;
 import com.android.server.timezonedetector.ConfigurationInternal;
 import com.android.server.timezonedetector.ServiceConfigAccessor;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.time.Duration;
 import java.util.Objects;
@@ -35,7 +35,7 @@
         extends LocationTimeZoneProviderController.Environment {
 
     @NonNull private final ServiceConfigAccessor mServiceConfigAccessor;
-    @NonNull private final ConfigurationChangeListener mConfigurationInternalChangeListener;
+    @NonNull private final StateChangeListener mConfigurationInternalChangeListener;
 
     LocationTimeZoneProviderControllerEnvironmentImpl(@NonNull ThreadingDomain threadingDomain,
             @NonNull ServiceConfigAccessor serviceConfigAccessor,
diff --git a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java
deleted file mode 100644
index 9cb1813..0000000
--- a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.timezonedetector.location;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.os.SystemClock;
-import android.service.timezone.TimeZoneProviderEvent;
-import android.util.IndentingPrintWriter;
-
-/**
- * A {@link LocationTimeZoneProviderProxy} that provides minimal responses needed for the {@link
- * BinderLocationTimeZoneProvider} to operate correctly when there is no "real" provider
- * configured / enabled. This can be used during development / testing, or in a production build
- * when the platform supports more providers than are needed for an Android deployment.
- *
- * <p>For example, if the {@link LocationTimeZoneProviderController} supports a primary
- * and a secondary {@link LocationTimeZoneProvider}, but only a primary is configured, the secondary
- * config will be left null and the {@link LocationTimeZoneProviderProxy} implementation will be
- * defaulted to a {@link NullLocationTimeZoneProviderProxy}. The {@link
- * NullLocationTimeZoneProviderProxy} sends a "permanent failure" event immediately after being
- * started for the first time, which ensures the {@link LocationTimeZoneProviderController} won't
- * expect any further {@link TimeZoneProviderEvent}s to come from it, and won't attempt to use it
- * again.
- */
-class NullLocationTimeZoneProviderProxy extends LocationTimeZoneProviderProxy {
-
-    /** Creates the instance. */
-    NullLocationTimeZoneProviderProxy(
-            @NonNull Context context, @NonNull ThreadingDomain threadingDomain) {
-        super(context, threadingDomain);
-    }
-
-    @Override
-    void onInitialize() {
-        // No-op
-    }
-
-    @Override
-    void onDestroy() {
-        // No-op
-    }
-
-    @Override
-    void setRequest(@NonNull TimeZoneProviderRequest request) {
-        if (request.sendUpdates()) {
-            TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent(
-                    SystemClock.elapsedRealtime(), "Provider is disabled");
-            handleTimeZoneProviderEvent(event);
-        }
-    }
-
-    @Override
-    public void dump(@NonNull IndentingPrintWriter ipw, @Nullable String[] args) {
-        synchronized (mSharedLock) {
-            ipw.println("{NullLocationTimeZoneProviderProxy}");
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
index ff0529f..aa2b74e 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
@@ -16,10 +16,13 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.infoLog;
 
 import android.annotation.NonNull;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 
 import com.android.i18n.timezone.ZoneInfoDb;
 
@@ -53,7 +56,12 @@
         // enables immediate failover to a secondary provider, one that might provide valid IDs for
         // the same location, which should provide better behavior than just ignoring the event.
         if (hasInvalidZones(event)) {
-            return TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
+            TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder(
+                    event.getTimeZoneProviderStatus())
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+                    .build();
+            return TimeZoneProviderEvent.createUncertainEvent(
+                    event.getCreationElapsedMillis(), providerStatus);
         }
 
         return event;
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index cd0096b..c192057 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -1752,6 +1752,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.TRUST_LISTENER)
         @Override
         public boolean isTrustUsuallyManaged(int userId) {
+            super.isTrustUsuallyManaged_enforcePermission();
+
             return isTrustUsuallyManagedInternal(userId);
         }
 
diff --git a/services/core/java/com/android/server/tv/PersistentDataStore.java b/services/core/java/com/android/server/tv/PersistentDataStore.java
index 72556a7..f8a9988 100644
--- a/services/core/java/com/android/server/tv/PersistentDataStore.java
+++ b/services/core/java/com/android/server/tv/PersistentDataStore.java
@@ -26,11 +26,11 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index f52f0b7..2c8fd96 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1066,7 +1066,28 @@
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
+        }
 
+        @Override
+        public void notifyRecordingStopped(IBinder sessionToken, String recordingId, int userId) {
+            final int callingUid = Binder.getCallingUid();
+            final int callingPid = Binder.getCallingPid();
+            final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+                    "notifyRecordingStopped");
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    try {
+                        SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+                                resolvedUserId);
+                        getSessionLocked(sessionState).notifyRecordingStopped(recordingId);
+                    } catch (RemoteException | SessionNotFoundException e) {
+                        Slogf.e(TAG, "error in notifyRecordingStopped", e);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
         }
 
         @Override
@@ -2253,6 +2274,23 @@
         }
 
         @Override
+        public void onRequestStopRecording(String recordingId) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slogf.d(TAG, "onRequestStopRecording");
+                }
+                if (mSessionState.mSession == null || mSessionState.mClient == null) {
+                    return;
+                }
+                try {
+                    mSessionState.mClient.onRequestStopRecording(recordingId, mSessionState.mSeq);
+                } catch (RemoteException e) {
+                    Slogf.e(TAG, "error in onRequestStopRecording", e);
+                }
+            }
+        }
+
+        @Override
         public void onRequestSigning(String id, String algorithm, String alias, byte[] data) {
             synchronized (mLock) {
                 if (DEBUG) {
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index edd1ef3..ad1ff72 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -946,8 +946,12 @@
         int inUseLowestPriorityFrHandle = TunerResourceManager.INVALID_RESOURCE_HANDLE;
         // Priority max value is 1000
         int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
+        // If the desired frontend id was specified, we only need to check the frontend.
+        boolean hasDesiredFrontend = request.desiredId != TunerFrontendRequest.DEFAULT_DESIRED_ID;
         for (FrontendResource fr : getFrontendResources().values()) {
-            if (fr.getType() == request.frontendType) {
+            int frontendId = getResourceIdFromHandle(fr.getHandle());
+            if (fr.getType() == request.frontendType
+                    && (!hasDesiredFrontend || frontendId == request.desiredId)) {
                 if (!fr.isInUse()) {
                     // Unused resource cannot be acquired if the max is already reached, but
                     // TRM still has to look for the reclaim candidate
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
index 7f49eea..39df450 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
@@ -20,10 +20,10 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index 6aa06e8..01fdc88 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -71,14 +71,14 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
diff --git a/services/core/java/com/android/server/utils/EventLogger.java b/services/core/java/com/android/server/utils/EventLogger.java
index 004312f..770cb72 100644
--- a/services/core/java/com/android/server/utils/EventLogger.java
+++ b/services/core/java/com/android/server/utils/EventLogger.java
@@ -26,7 +26,9 @@
 import java.lang.annotation.RetentionPolicy;
 import java.text.SimpleDateFormat;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -43,7 +45,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 +62,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 +71,19 @@
     }
 
     /**
-     * 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 into the given {@link DumpSink}. */
+    public synchronized void dump(DumpSink dumpSink) {
+        dumpSink.sink(mTag, new ArrayList<>(mEvents));
     }
 
     /** Dumps events using {@link PrintWriter}. */
@@ -95,14 +92,23 @@
     }
 
     /** Dumps events using {@link PrintWriter} with a certain indent. */
-    public synchronized void dump(PrintWriter pw, String prefix) {
-        pw.println(prefix + "Events log: " + mTag);
-        String indent = prefix + "  ";
+    public synchronized void dump(PrintWriter pw, String indent) {
+        pw.println(indent + "Events log: " + mTag);
+
+        String childrenIndention = indent + "  ";
         for (Event evt : mEvents) {
-            pw.println(indent + evt.toString());
+            pw.println(childrenIndention + evt.toString());
         }
     }
 
+    /** Receives events from {@link EventLogger} upon a {@link #dump(DumpSink)} call. **/
+    public interface DumpSink {
+
+        /** Processes given events into some pipeline with a given tag. **/
+        void sink(String tag, List<Event> events);
+
+    }
+
     public abstract static class Event {
 
         /** Timestamps formatter. */
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 6012993..d944a3b 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -655,7 +655,8 @@
     }
 
     private void registerSettingsChangeReceiver(IntentFilter intentFilter) {
-        mContext.registerReceiver(mSettingChangeReceiver, intentFilter);
+        mContext.registerReceiver(mSettingChangeReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8ac4fd4..141be70 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -65,6 +65,9 @@
     public final DeviceVibrationEffectAdapter deviceEffectAdapter;
     public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
 
+    // Not guarded by lock because they're not modified by this conductor, it's used here only to
+    // check immutable attributes. The status and other mutable states are changed by the service or
+    // by the vibrator steps.
     private final Vibration mVibration;
     private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
 
@@ -412,6 +415,16 @@
         }
     }
 
+    /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
+    public boolean wasNotifiedToCancel() {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(false);
+        }
+        synchronized (mLock) {
+            return mSignalCancel != null;
+        }
+    }
+
     @GuardedBy("mLock")
     private boolean hasPendingNotifySignalLocked() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 8514e27..8613b50 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -864,8 +864,8 @@
         }
 
         Vibration currentVibration = mCurrentVibration.getVibration();
-        if (currentVibration.hasEnded()) {
-            // Current vibration is finishing up, it should not block incoming vibrations.
+        if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) {
+            // Current vibration has ended or is cancelling, should not block incoming vibrations.
             return null;
         }
 
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5f420bf..c875f4a 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -99,8 +99,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -111,6 +109,8 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.JournaledFile;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.EventLogTags;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
@@ -320,9 +320,7 @@
                                 IRemoteCallback.Stub callback = new IRemoteCallback.Stub() {
                                     @Override
                                     public void sendResult(Bundle data) throws RemoteException {
-                                        if (DEBUG) {
-                                            Slog.d(TAG, "publish system wallpaper changed!");
-                                        }
+                                        Slog.d(TAG, "publish system wallpaper changed!");
                                         notifyWallpaperChanged(wallpaper);
                                     }
                                 };
@@ -1159,6 +1157,8 @@
                     Slog.w(TAG, "WallpaperService is not connected yet");
                     return;
                 }
+                TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+                t.traceBegin("WPMS.connectLocked-" + wallpaper.wallpaperComponent);
                 if (DEBUG) Slog.v(TAG, "Adding window token: " + mToken);
                 mWindowManagerInternal.addWindowToken(mToken, TYPE_WALLPAPER, mDisplayId,
                         null /* options */);
@@ -1175,6 +1175,7 @@
                                 false /* fromUser */, wallpaper, null /* reply */);
                     }
                 }
+                t.traceEnd();
             }
 
             void disconnectLocked() {
@@ -1324,6 +1325,8 @@
 
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
+            TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+            t.traceBegin("WPMS.onServiceConnected-" + name);
             synchronized (mLock) {
                 if (mWallpaper.connection == this) {
                     mService = IWallpaperService.Stub.asInterface(service);
@@ -1340,6 +1343,7 @@
                     mContext.getMainThreadHandler().removeCallbacks(mDisconnectRunnable);
                 }
             }
+            t.traceEnd();
         }
 
         @Override
@@ -1547,12 +1551,17 @@
         public void engineShown(IWallpaperEngine engine) {
             synchronized (mLock) {
                 if (mReply != null) {
+                    TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+                    t.traceBegin("WPMS.mReply.sendResult");
                     final long ident = Binder.clearCallingIdentity();
                     try {
                         mReply.sendResult(null);
                     } catch (RemoteException e) {
+                        Slog.d(TAG, "failed to send callback!", e);
+                    } finally {
                         Binder.restoreCallingIdentity(ident);
                     }
+                    t.traceEnd();
                     mReply = null;
                 }
             }
@@ -1888,12 +1897,9 @@
         }
     }
 
-    private static final HashMap<Integer, String> sWallpaperType = new HashMap<Integer, String>() {
-        {
-            put(FLAG_SYSTEM, RECORD_FILE);
-            put(FLAG_LOCK, RECORD_LOCK_FILE);
-        }
-    };
+    private static final Map<Integer, String> sWallpaperType = Map.of(
+            FLAG_SYSTEM, RECORD_FILE,
+            FLAG_LOCK, RECORD_LOCK_FILE);
 
     private void errorCheck(int userID) {
         sWallpaperType.forEach((type, filename) -> {
@@ -3058,6 +3064,8 @@
             return true;
         }
 
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.bindWallpaperComponentLocked-" + componentName);
         try {
             if (componentName == null) {
                 componentName = mDefaultWallpaperComponent;
@@ -3190,6 +3198,8 @@
             }
             Slog.w(TAG, msg);
             return false;
+        } finally {
+            t.traceEnd();
         }
         return true;
     }
@@ -3234,7 +3244,10 @@
     }
 
     private void attachServiceLocked(WallpaperConnection conn, WallpaperData wallpaper) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.attachServiceLocked");
         conn.forEachDisplayConnector(connector-> connector.connectLocked(conn, wallpaper));
+        t.traceEnd();
     }
 
     private void notifyCallbacksLocked(WallpaperData wallpaper) {
@@ -3360,6 +3373,8 @@
     }
 
     void saveSettingsLocked(int userId) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
+        t.traceBegin("WPMS.saveSettingsLocked-" + userId);
         JournaledFile journal = makeJournaledFile(userId);
         FileOutputStream fstream = null;
         try {
@@ -3388,6 +3403,7 @@
             IoUtils.closeQuietly(fstream);
             journal.rollback();
         }
+        t.traceEnd();
     }
 
 
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 44b83096..4fef2a8 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -2225,8 +2225,7 @@
                 ProtoOutputStream proto = new ProtoOutputStream();
                 proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
                 long timeOffsetNs =
-                        TimeUnit.NANOSECONDS.convert(System.currentTimeMillis(),
-                                                     TimeUnit.NANOSECONDS)
+                        TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
                         - SystemClock.elapsedRealtimeNanos();
                 proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
                 mBuffer.writeTraceToFile(mTraceFile, proto);
diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
index d0c381e..21b241a 100644
--- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
+++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java
@@ -56,8 +56,11 @@
     private static final int SURFACE_FLINGER_CALLBACK_WINDOWS_STABLE_TIMES_MS = 35;
     // To avoid the surface flinger callbacks always comes within in 2 frames, then no windows
     // are reported to the A11y framework, and the animation duration time is 500ms, so setting
-    // this value as the max timeout value to force computing changed windows.
-    private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 500;
+    // this value as the max timeout value to force computing changed windows. However, since
+    // UiAutomator waits 500ms to determine that things are idle. Since we aren't actually idle,
+    // we need to reduce the timeout here a little so that we can deliver an updated state before
+    // UiAutomator reports idle based-on stale information.
+    private static final int WINDOWS_CHANGED_NOTIFICATION_MAX_DURATION_TIMES_MS = 450;
 
     private static final float[] sTempFloats = new float[9];
 
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 59f37c2..7157293 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -86,6 +86,7 @@
 import com.android.internal.app.AssistUtils;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
 import com.android.server.Watchdog;
 import com.android.server.pm.KnownPackages;
@@ -454,6 +455,39 @@
                         finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY;
                 if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY
                         || (finishWithRootActivity && r == rootR)) {
+                    ActivityRecord topActivity =
+                            r.getTask().getTopNonFinishingActivity();
+                    boolean passesAsmChecks = topActivity != null
+                            && topActivity.getUid() == r.getUid();
+                    if (!passesAsmChecks) {
+                        Slog.i(TAG, "Finishing task from background. r: " + r);
+                        FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
+                                /* caller_uid */
+                                r.getUid(),
+                                /* caller_activity_class_name */
+                                r.info.name,
+                                /* target_task_top_activity_uid */
+                                topActivity == null ? -1 : topActivity.getUid(),
+                                /* target_task_top_activity_class_name */
+                                topActivity == null ? null : topActivity.info.name,
+                                /* target_task_is_different */
+                                false,
+                                /* target_activity_uid */
+                                -1,
+                                /* target_activity_class_name */
+                                null,
+                                /* target_intent_action */
+                                null,
+                                /* target_intent_flags */
+                                0,
+                                /* action */
+                                FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK,
+                                /* version */
+                                1,
+                                /* multi_window */
+                                false
+                        );
+                    }
                     // If requested, remove the task that is associated to this activity only if it
                     // was the root activity in the task. The result code and data is ignored
                     // because we don't support returning them across task boundaries. Also, to
@@ -1177,7 +1211,8 @@
             synchronized (mGlobalLock) {
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
                 if (r != null) {
-                    r.reportFullyDrawnLocked(restoredFromBundle);
+                    mTaskSupervisor.getActivityMetricsLogger().notifyFullyDrawn(r,
+                            restoredFromBundle);
                 }
             }
         } finally {
@@ -1351,8 +1386,38 @@
         }
     }
 
+    /**
+     * Return {@code true} when the given Activity is a relative Task root. That is, the rest of
+     * the Activities in the Task should be finished when it finishes. Otherwise, return {@code
+     * false}.
+     */
+    private boolean isRelativeTaskRootActivity(ActivityRecord r, ActivityRecord taskRoot) {
+        // Not a relative root if the given Activity is not the root Activity of its TaskFragment.
+        final TaskFragment taskFragment = r.getTaskFragment();
+        if (r != taskFragment.getActivity(ar -> !ar.finishing || ar == r,
+                false /* traverseTopToBottom */)) {
+            return false;
+        }
+
+        // The given Activity is the relative Task root if its TaskFragment is a companion
+        // TaskFragment to the taskRoot (i.e. the taskRoot TF will be finished together).
+        return taskRoot.getTaskFragment().getCompanionTaskFragment() == taskFragment;
+    }
+
+    private boolean isTopActivityInTaskFragment(ActivityRecord activity) {
+        return activity.getTaskFragment().topRunningActivity() == activity;
+    }
+
+    private void requestCallbackFinish(IRequestFinishCallback callback) {
+        try {
+            callback.requestFinish();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to invoke request finish callback", e);
+        }
+    }
+
     @Override
-    public void onBackPressedOnTaskRoot(IBinder token, IRequestFinishCallback callback) {
+    public void onBackPressed(IBinder token, IRequestFinishCallback callback) {
         final long origId = Binder.clearCallingIdentity();
         try {
             final Intent baseActivityIntent;
@@ -1362,20 +1427,29 @@
                 final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
                 if (r == null) return;
 
-                if (mService.mWindowOrganizerController.mTaskOrganizerController
+                final Task task = r.getTask();
+                final ActivityRecord root = task.getRootActivity(false /*ignoreRelinquishIdentity*/,
+                        true /*setToBottomIfNone*/);
+                final boolean isTaskRoot = r == root;
+                if (isTaskRoot) {
+                    if (mService.mWindowOrganizerController.mTaskOrganizerController
                         .handleInterceptBackPressedOnTaskRoot(r.getRootTask())) {
-                    // This task is handled by a task organizer that has requested the back pressed
-                    // callback.
+                        // This task is handled by a task organizer that has requested the back
+                        // pressed callback.
+                        return;
+                    }
+                } else if (!isRelativeTaskRootActivity(r, root)) {
+                    // Finish the Activity if the activity is not the task root or relative root.
+                    requestCallbackFinish(callback);
                     return;
                 }
 
-                final Task task = r.getTask();
-                isLastRunningActivity = task.topRunningActivity() == r;
+                isLastRunningActivity = isTopActivityInTaskFragment(isTaskRoot ? root : r);
 
-                final boolean isBaseActivity = r.mActivityComponent.equals(task.realActivity);
-                baseActivityIntent = isBaseActivity ? r.intent : null;
+                final boolean isBaseActivity = root.mActivityComponent.equals(task.realActivity);
+                baseActivityIntent = isBaseActivity ? root.intent : null;
 
-                launchedFromHome = r.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME);
+                launchedFromHome = root.isLaunchSourceType(ActivityRecord.LAUNCH_SOURCE_TYPE_HOME);
             }
 
             // If the activity is one of the main entry points for the application, then we should
@@ -1390,16 +1464,12 @@
             if (baseActivityIntent != null && isLastRunningActivity
                     && ((launchedFromHome && ActivityRecord.isMainIntent(baseActivityIntent))
                         || isLauncherActivity(baseActivityIntent.getComponent()))) {
-                moveActivityTaskToBack(token, false /* nonRoot */);
+                moveActivityTaskToBack(token, true /* nonRoot */);
                 return;
             }
 
             // The default option for handling the back button is to finish the Activity.
-            try {
-                callback.requestFinish();
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to invoke request finish callback", e);
-            }
+            requestCallbackFinish(callback);
         } finally {
             Binder.restoreCallingIdentity(origId);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index f0de1d3..e1ab291 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -454,6 +454,7 @@
         final int windowsFullyDrawnDelayMs;
         final int activityRecordIdHashCode;
         final boolean relaunched;
+        final long timestampNs;
 
         private TransitionInfoSnapshot(TransitionInfo info) {
             this(info, info.mLastLaunchedActivity, INVALID_DELAY);
@@ -483,6 +484,7 @@
             activityRecordIdHashCode = System.identityHashCode(launchedActivity);
             this.windowsFullyDrawnDelayMs = windowsFullyDrawnDelayMs;
             relaunched = info.mRelaunched;
+            timestampNs = info.mLaunchingState.mStartRealtimeNs;
         }
 
         @WaitResult.LaunchState int getLaunchState() {
@@ -498,6 +500,10 @@
             }
         }
 
+        boolean isIntresetedToEventLog() {
+            return type == TYPE_TRANSITION_WARM_LAUNCH || type == TYPE_TRANSITION_COLD_LAUNCH;
+        }
+
         PackageOptimizationInfo getPackageOptimizationInfo(ArtManagerInternal artManagerInternal) {
             return artManagerInternal == null || launchedActivityAppRecordRequiredAbi == null
                     ? PackageOptimizationInfo.createWithNoInfo()
@@ -1022,16 +1028,17 @@
         // This will avoid any races with other operations that modify the ActivityRecord.
         final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
         if (info.isInterestingToLoggerAndObserver()) {
-            final long timestampNs = info.mLaunchingState.mStartRealtimeNs;
             final long uptimeNs = info.mLaunchingState.mStartUptimeNs;
             final int transitionDelay = info.mCurrentTransitionDelayMs;
             final int processState = info.mProcessState;
             final int processOomAdj = info.mProcessOomAdj;
             mLoggerHandler.post(() -> logAppTransition(
-                    timestampNs, uptimeNs, transitionDelay, infoSnapshot, isHibernating,
+                    uptimeNs, transitionDelay, infoSnapshot, isHibernating,
                     processState, processOomAdj));
         }
-        mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
+        if (infoSnapshot.isIntresetedToEventLog()) {
+            mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
+        }
         if (info.mPendingFullyDrawn != null) {
             info.mPendingFullyDrawn.run();
         }
@@ -1040,7 +1047,7 @@
     }
 
     // This gets called on another thread without holding the activity manager lock.
-    private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeNs,
+    private void logAppTransition(long transitionDeviceUptimeNs,
             int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating,
             int processState, int processOomAdj) {
         final LogMaker builder = new LogMaker(APP_TRANSITION);
@@ -1108,7 +1115,7 @@
                 isIncremental,
                 isLoading,
                 info.launchedActivityName.hashCode(),
-                TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs),
+                TimeUnit.NANOSECONDS.toMillis(info.timestampNs),
                 processState,
                 processOomAdj);
 
@@ -1132,10 +1139,6 @@
     }
 
     private void logAppDisplayed(TransitionInfoSnapshot info) {
-        if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) {
-            return;
-        }
-
         EventLog.writeEvent(WM_ACTIVITY_LAUNCH_TIME,
                 info.userId, info.activityRecordIdHashCode, info.launchedActivityShortComponentName,
                 info.windowsDrawnDelayMs);
@@ -1181,8 +1184,7 @@
     }
 
     /** @see android.app.Activity#reportFullyDrawn */
-    TransitionInfoSnapshot logAppTransitionReportedDrawn(ActivityRecord r,
-            boolean restoredFromBundle) {
+    TransitionInfoSnapshot notifyFullyDrawn(ActivityRecord r, boolean restoredFromBundle) {
         final TransitionInfo info = mLastTransitionInfo.get(r);
         if (info == null) {
             return null;
@@ -1191,7 +1193,7 @@
             // There are still undrawn activities, postpone reporting fully drawn until all of its
             // windows are drawn. So that is closer to an usable state.
             info.mPendingFullyDrawn = () -> {
-                logAppTransitionReportedDrawn(r, restoredFromBundle);
+                notifyFullyDrawn(r, restoredFromBundle);
                 info.mPendingFullyDrawn = null;
             };
             return null;
@@ -1204,7 +1206,9 @@
                         currentTimestampNs - info.mLaunchingState.mStartUptimeNs);
         final TransitionInfoSnapshot infoSnapshot =
                 new TransitionInfoSnapshot(info, r, (int) startupTimeMs);
-        mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot));
+        if (infoSnapshot.isIntresetedToEventLog()) {
+            mLoggerHandler.post(() -> logAppFullyDrawn(infoSnapshot));
+        }
         mLastTransitionInfo.remove(r);
 
         if (!info.isInterestingToLoggerAndObserver()) {
@@ -1216,46 +1220,8 @@
         // fullfils (handling reportFullyDrawn() callbacks).
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                 "ActivityManager:ReportingFullyDrawn " + info.mLastLaunchedActivity.packageName);
-
-        final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN);
-        builder.setPackageName(r.packageName);
-        builder.addTaggedData(FIELD_CLASS_NAME, r.info.name);
-        builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS, startupTimeMs);
-        builder.setType(restoredFromBundle
-                ? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE
-                : TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE);
-        builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING,
-                info.mProcessRunning ? 1 : 0);
-        mMetricsLogger.write(builder);
-        final PackageOptimizationInfo packageOptimizationInfo =
-                infoSnapshot.getPackageOptimizationInfo(getArtManagerInternal());
-        // Incremental info
-        boolean isIncremental = false, isLoading = false;
-        final String codePath = info.mLastLaunchedActivity.info.applicationInfo.getCodePath();
-        if (codePath != null && IncrementalManager.isIncrementalPath(codePath)) {
-            isIncremental = true;
-            isLoading = isIncrementalLoading(info.mLastLaunchedActivity.packageName,
-                            info.mLastLaunchedActivity.mUserId);
-        }
-        FrameworkStatsLog.write(
-                FrameworkStatsLog.APP_START_FULLY_DRAWN,
-                info.mLastLaunchedActivity.info.applicationInfo.uid,
-                info.mLastLaunchedActivity.packageName,
-                restoredFromBundle
-                        ? FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITH_BUNDLE
-                        : FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITHOUT_BUNDLE,
-                info.mLastLaunchedActivity.info.name,
-                info.mProcessRunning,
-                startupTimeMs,
-                packageOptimizationInfo.getCompilationReason(),
-                packageOptimizationInfo.getCompilationFilter(),
-                info.mSourceType,
-                info.mSourceEventDelayMs,
-                isIncremental,
-                isLoading,
-                info.mLastLaunchedActivity.info.name.hashCode(),
-                TimeUnit.NANOSECONDS.toMillis(info.mLaunchingState.mStartRealtimeNs));
-
+        mLoggerHandler.post(() -> logAppFullyDrawnMetrics(infoSnapshot, restoredFromBundle,
+                info.mProcessRunning));
         // Ends the trace started at the beginning of this function. This is located here to allow
         // the trace slice to have a noticable duration.
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
@@ -1266,11 +1232,48 @@
         return infoSnapshot;
     }
 
-    private void logAppFullyDrawn(TransitionInfoSnapshot info) {
-        if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) {
-            return;
+    private void logAppFullyDrawnMetrics(TransitionInfoSnapshot info, boolean restoredFromBundle,
+            boolean processRunning) {
+        final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN);
+        builder.setPackageName(info.packageName);
+        builder.addTaggedData(FIELD_CLASS_NAME, info.launchedActivityName);
+        builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS,
+                (long) info.windowsFullyDrawnDelayMs);
+        builder.setType(restoredFromBundle
+                ? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE
+                : TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE);
+        builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING, processRunning ? 1 : 0);
+        mMetricsLogger.write(builder);
+        final PackageOptimizationInfo packageOptimizationInfo =
+                info.getPackageOptimizationInfo(getArtManagerInternal());
+        // Incremental info
+        boolean isIncremental = false, isLoading = false;
+        final String codePath = info.applicationInfo.getCodePath();
+        if (codePath != null && IncrementalManager.isIncrementalPath(codePath)) {
+            isIncremental = true;
+            isLoading = isIncrementalLoading(info.packageName, info.userId);
         }
+        FrameworkStatsLog.write(
+                FrameworkStatsLog.APP_START_FULLY_DRAWN,
+                info.applicationInfo.uid,
+                info.packageName,
+                restoredFromBundle
+                        ? FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITH_BUNDLE
+                        : FrameworkStatsLog.APP_START_FULLY_DRAWN__TYPE__WITHOUT_BUNDLE,
+                info.launchedActivityName,
+                processRunning,
+                info.windowsFullyDrawnDelayMs,
+                packageOptimizationInfo.getCompilationReason(),
+                packageOptimizationInfo.getCompilationFilter(),
+                info.sourceType,
+                info.sourceEventDelayMs,
+                isIncremental,
+                isLoading,
+                info.launchedActivityName.hashCode(),
+                TimeUnit.NANOSECONDS.toMillis(info.timestampNs));
+    }
 
+    private void logAppFullyDrawn(TransitionInfoSnapshot info) {
         StringBuilder sb = mStringBuilder;
         sb.setLength(0);
         sb.append("Fully drawn ");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2232aa1..23ed188 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -312,8 +312,6 @@
 import android.util.MergedConfiguration;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 import android.view.AppTransitionAnimationSpec;
 import android.view.DisplayInfo;
@@ -349,6 +347,8 @@
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.am.AppTimeTracker;
 import com.android.server.am.PendingIntentRecord;
@@ -496,7 +496,7 @@
     /** The most recently given options. */
     private ActivityOptions mPendingOptions;
     /** Non-null if {@link #mPendingOptions} specifies the remote animation. */
-    private RemoteAnimationAdapter mPendingRemoteAnimation;
+    RemoteAnimationAdapter mPendingRemoteAnimation;
     private RemoteTransition mPendingRemoteTransition;
     ActivityOptions returningOptions; // options that are coming back via convertToTranslucent
     AppTimeTracker appTimeTracker; // set if we are tracking the time in this app/task/activity
@@ -812,7 +812,6 @@
     StartingData mStartingData;
     WindowState mStartingWindow;
     StartingSurfaceController.StartingSurface mStartingSurface;
-    boolean startingDisplayed;
     boolean startingMoved;
 
     /** The last set {@link DropInputMode} for this activity surface. */
@@ -821,13 +820,6 @@
     /** Whether the input to this activity will be dropped during the current playing animation. */
     private boolean mIsInputDroppedForAnimation;
 
-    /**
-     * If it is non-null, it requires all activities who have the same starting data to be drawn
-     * to remove the starting window.
-     * TODO(b/189385912): Remove starting window related fields after migrating them to task.
-     */
-    private StartingData mSharedStartingData;
-
     boolean mHandleExitSplashScreen;
     @TransferSplashScreenState
     int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
@@ -1201,14 +1193,11 @@
             pw.print(" firstWindowDrawn="); pw.print(firstWindowDrawn);
             pw.print(" mIsExiting="); pw.println(mIsExiting);
         }
-        if (mSharedStartingData != null) {
-            pw.println(prefix + "mSharedStartingData=" + mSharedStartingData);
-        }
-        if (mStartingWindow != null || mStartingSurface != null
-                || startingDisplayed || startingMoved || mVisibleSetFromTransferredStartingWindow) {
+        if (mStartingWindow != null || mStartingData != null || mStartingSurface != null
+                || startingMoved || mVisibleSetFromTransferredStartingWindow) {
             pw.print(prefix); pw.print("startingWindow="); pw.print(mStartingWindow);
             pw.print(" startingSurface="); pw.print(mStartingSurface);
-            pw.print(" startingDisplayed="); pw.print(startingDisplayed);
+            pw.print(" startingDisplayed="); pw.print(isStartingWindowDisplayed());
             pw.print(" startingMoved="); pw.print(startingMoved);
             pw.println(" mVisibleSetFromTransferredStartingWindow="
                     + mVisibleSetFromTransferredStartingWindow);
@@ -1758,6 +1747,7 @@
         }
 
         prevDc.mClosingApps.remove(this);
+        prevDc.getDisplayPolicy().removeRelaunchingApp(this);
 
         if (prevDc.mFocusedApp == this) {
             prevDc.setFocusedApp(null);
@@ -2687,13 +2677,23 @@
         }
     }
 
+    boolean isStartingWindowDisplayed() {
+        final StartingData data = mStartingData != null ? mStartingData : task != null
+                ? task.mSharedStartingData : null;
+        return data != null && data.mIsDisplayed;
+    }
+
     /** Called when the starting window is added to this activity. */
     void attachStartingWindow(@NonNull WindowState startingWindow) {
         startingWindow.mStartingData = mStartingData;
         mStartingWindow = startingWindow;
-        // The snapshot type may have called associateStartingDataWithTask().
-        if (mStartingData != null && mStartingData.mAssociatedTask != null) {
-            attachStartingSurfaceToAssociatedTask();
+        if (mStartingData != null) {
+            if (mStartingData.mAssociatedTask != null) {
+                // The snapshot type may have called associateStartingDataWithTask().
+                attachStartingSurfaceToAssociatedTask();
+            } else if (isEmbedded()) {
+                associateStartingWindowWithTaskIfNeeded();
+            }
         }
     }
 
@@ -2708,11 +2708,7 @@
     /** Called when the starting window is not added yet but its data is known to fill the task. */
     private void associateStartingDataWithTask() {
         mStartingData.mAssociatedTask = task;
-        task.forAllActivities(r -> {
-            if (r.mVisibleRequested && !r.firstWindowDrawn) {
-                r.mSharedStartingData = mStartingData;
-            }
-        });
+        task.mSharedStartingData = mStartingData;
     }
 
     /** Associates and attaches an added starting window to the current task. */
@@ -2743,10 +2739,8 @@
 
     void removeStartingWindowAnimation(boolean prepareAnimation) {
         mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
-        if (mSharedStartingData != null) {
-            mSharedStartingData.mAssociatedTask.forAllActivities(r -> {
-                r.mSharedStartingData = null;
-            });
+        if (task != null) {
+            task.mSharedStartingData = null;
         }
         if (mStartingWindow == null) {
             if (mStartingData != null) {
@@ -2774,7 +2768,6 @@
             mStartingData = null;
             mStartingSurface = null;
             mStartingWindow = null;
-            startingDisplayed = false;
             if (surface == null) {
                 ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "startingWindow was set but "
                         + "startingSurface==null, couldn't remove");
@@ -3138,8 +3131,8 @@
         }
 
         // Check to see if PiP is supported for the display this container is on.
-        if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isWindowingModeSupported(
-                WINDOWING_MODE_PINNED)) {
+        if (mDisplayContent != null && !mDisplayContent.mDwpcHelper.isEnteringPipAllowed(
+                getUid())) {
             Slog.w(TAG, "Display " + mDisplayContent.getDisplayId()
                     + " doesn't support enter picture-in-picture mode. caller = " + caller);
             return false;
@@ -3967,6 +3960,9 @@
     void startRelaunching() {
         if (mPendingRelaunchCount == 0) {
             mRelaunchStartTime = SystemClock.elapsedRealtime();
+            if (mVisibleRequested) {
+                mDisplayContent.getDisplayPolicy().addRelaunchingApp(this);
+            }
         }
         clearAllDrawn();
 
@@ -3980,7 +3976,7 @@
             mPendingRelaunchCount--;
             if (mPendingRelaunchCount == 0 && !isClientVisible()) {
                 // Don't count if the client won't report drawn.
-                mRelaunchStartTime = 0;
+                finishOrAbortReplacingWindow();
             }
         } else {
             // Update keyguard flags upon finishing relaunch.
@@ -4001,7 +3997,12 @@
             return;
         }
         mPendingRelaunchCount = 0;
+        finishOrAbortReplacingWindow();
+    }
+
+    void finishOrAbortReplacingWindow() {
         mRelaunchStartTime = 0;
+        mDisplayContent.getDisplayPolicy().removeRelaunchingApp(this);
     }
 
     /**
@@ -4247,7 +4248,7 @@
      * @return {@code true} if starting window is in app's hierarchy.
      */
     boolean hasStartingWindow() {
-        if (startingDisplayed || mStartingData != null) {
+        if (mStartingData != null) {
             return true;
         }
         for (int i = mChildren.size() - 1; i >= 0; i--) {
@@ -4345,10 +4346,7 @@
 
                 // Transfer the starting window over to the new token.
                 mStartingData = fromActivity.mStartingData;
-                mSharedStartingData = fromActivity.mSharedStartingData;
                 mStartingSurface = fromActivity.mStartingSurface;
-                startingDisplayed = fromActivity.startingDisplayed;
-                fromActivity.startingDisplayed = false;
                 mStartingWindow = tStartingWindow;
                 reportedVisible = fromActivity.reportedVisible;
                 fromActivity.mStartingData = null;
@@ -4414,7 +4412,6 @@
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW,
                     "Moving pending starting from %s to %s", fromActivity, this);
             mStartingData = fromActivity.mStartingData;
-            mSharedStartingData = fromActivity.mSharedStartingData;
             fromActivity.mStartingData = null;
             fromActivity.startingMoved = true;
             scheduleAddStartingWindow();
@@ -5114,6 +5111,9 @@
             mTaskSupervisor.onProcessActivityStateChanged(app, false /* forceBatch */);
         }
         logAppCompatState();
+        if (!visible) {
+            finishOrAbortReplacingWindow();
+        }
     }
 
     /**
@@ -6497,15 +6497,6 @@
         }
     }
 
-    void reportFullyDrawnLocked(boolean restoredFromBundle) {
-        final TransitionInfoSnapshot info = mTaskSupervisor
-            .getActivityMetricsLogger().logAppTransitionReportedDrawn(this, restoredFromBundle);
-        if (info != null) {
-            mTaskSupervisor.reportActivityLaunched(false /* timeout */, this,
-                    info.windowsFullyDrawnDelayMs, info.getLaunchState());
-        }
-    }
-
     void onFirstWindowDrawn(WindowState win) {
         firstWindowDrawn = true;
         // stop tracking
@@ -6526,14 +6517,11 @@
         // Remove starting window directly if is in a pure task. Otherwise if it is associated with
         // a task (e.g. nested task fragment), then remove only if all visible windows in the task
         // are drawn.
-        final Task associatedTask =
-                mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null;
+        final Task associatedTask = task.mSharedStartingData != null ? task : null;
         if (associatedTask == null) {
             removeStartingWindow();
-        } else if (associatedTask.getActivity(r -> r.mVisibleRequested && !r.firstWindowDrawn
-                // Don't block starting window removal if an Activity can't be a starting window
-                // target.
-                && r.mSharedStartingData != null) == null) {
+        } else if (associatedTask.getActivity(
+                r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
             final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
             if (r != null) {
@@ -6748,7 +6736,6 @@
         if (mLastTransactionSequence != mWmService.mTransactionSequence) {
             mLastTransactionSequence = mWmService.mTransactionSequence;
             mNumDrawnWindows = 0;
-            startingDisplayed = false;
 
             // There is the main base application window, even if it is exiting, wait for it
             mNumInterestingWindows = findMainWindow(false /* includeStartingApp */) != null ? 1 : 0;
@@ -6792,9 +6779,9 @@
                         isInterestingAndDrawn = true;
                     }
                 }
-            } else if (w.isDrawn()) {
+            } else if (mStartingData != null && w.isDrawn()) {
                 // The starting window for this container is drawn.
-                startingDisplayed = true;
+                mStartingData.mIsDisplayed = true;
             }
         }
 
@@ -6880,6 +6867,10 @@
         if (r == null || r.getParent() == null) {
             return INVALID_TASK_ID;
         }
+        return getTaskForActivityLocked(r, onlyRoot);
+    }
+
+    static int getTaskForActivityLocked(ActivityRecord r, boolean onlyRoot) {
         final Task task = r.task;
         if (onlyRoot && r.compareTo(task.getRootActivity(
                 false /*ignoreRelinquishIdentity*/, true /*setToBottomIfNone*/)) > 0) {
@@ -6912,11 +6903,8 @@
      *         {@link android.view.Display#INVALID_DISPLAY} if not attached.
      */
     int getDisplayId() {
-        final Task rootTask = getRootTask();
-        if (rootTask == null) {
-            return INVALID_DISPLAY;
-        }
-        return rootTask.getDisplayId();
+        return task != null && task.mDisplayContent != null
+                 ? task.mDisplayContent.mDisplayId : INVALID_DISPLAY;
     }
 
     final boolean isDestroyable() {
@@ -7552,7 +7540,8 @@
 
         ProtoLog.v(WM_DEBUG_ANIM, "Animation done in %s"
                 + ": reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b",
-                this, reportedVisible, okToDisplay(), okToAnimate(), startingDisplayed);
+                this, reportedVisible, okToDisplay(), okToAnimate(),
+                isStartingWindowDisplayed());
 
         // clean up thumbnail window
         if (mThumbnail != null) {
@@ -7916,8 +7905,8 @@
     }
 
     @Override
-    float getSizeCompatScale() {
-        return hasSizeCompatBounds() ? mSizeCompatScale : super.getSizeCompatScale();
+    float getCompatScale() {
+        return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
     }
 
     @Override
@@ -8454,7 +8443,7 @@
         getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                 mCompatDisplayInsets);
         // Use current screen layout as source because the size of app is independent to parent.
-        resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
+        resolvedConfig.screenLayout = computeScreenLayout(
                 getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
                 resolvedConfig.screenHeightDp);
 
@@ -9219,10 +9208,10 @@
                         + " preserveWindow=" + preserveWindow);
         if (andResume) {
             EventLogTags.writeWmRelaunchResumeActivity(mUserId, System.identityHashCode(this),
-                    task.mTaskId, shortComponentName);
+                    task.mTaskId, shortComponentName, Integer.toHexString(configChangeFlags));
         } else {
             EventLogTags.writeWmRelaunchActivity(mUserId, System.identityHashCode(this),
-                    task.mTaskId, shortComponentName);
+                    task.mTaskId, shortComponentName, Integer.toHexString(configChangeFlags));
         }
 
         startFreezingScreenLocked(0);
@@ -9650,7 +9639,7 @@
         if (mStartingWindow != null) {
             mStartingWindow.writeIdentifierToProto(proto, STARTING_WINDOW);
         }
-        proto.write(STARTING_DISPLAYED, startingDisplayed);
+        proto.write(STARTING_DISPLAYED, isStartingWindowDisplayed());
         proto.write(STARTING_MOVED, startingMoved);
         proto.write(VISIBLE_SET_FROM_TRANSFERRED_STARTING_WINDOW,
                 mVisibleSetFromTransferredStartingWindow);
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/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 1d70146..3ec24d5 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1663,7 +1663,8 @@
         }
         final Task startedTask = mStartActivity.getTask();
         if (newTask) {
-            EventLogTags.writeWmCreateTask(mStartActivity.mUserId, startedTask.mTaskId);
+            EventLogTags.writeWmCreateTask(mStartActivity.mUserId, startedTask.mTaskId,
+                    startedTask.getRootTaskId(), startedTask.getDisplayId());
         }
         mStartActivity.logStartActivity(EventLogTags.WM_CREATE_ACTIVITY, startedTask);
 
@@ -1856,6 +1857,11 @@
                         + " from background: " + mSourceRecord
                         + ". New task: " + newTask);
                 boolean newOrEmptyTask = newTask || (targetTopActivity == null);
+                int action = newTask
+                        ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_NEW_TASK
+                        : (mSourceRecord.getTask().equals(targetTask)
+                                ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK
+                                :  FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK);
                 FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
                         /* caller_uid */
                         callerUid,
@@ -1874,7 +1880,14 @@
                         /* target_intent_action */
                         r.intent.getAction(),
                         /* target_intent_flags */
-                        r.intent.getFlags()
+                        r.intent.getFlags(),
+                        /* action */
+                        action,
+                        /* version */
+                        1,
+                        /* multi_window */
+                        targetTask != null && !targetTask.equals(mSourceRecord.getTask())
+                                && targetTask.isVisible()
                 );
             }
         }
@@ -1976,12 +1989,6 @@
                 ? targetTask.getTopNonFinishingActivity()
                 : targetTaskTop;
 
-        // At this point we are certain we want the task moved to the front. If we need to dismiss
-        // any other always-on-top root tasks, now is the time to do it.
-        if (targetTaskTop.canTurnScreenOn() && mService.isDreaming()) {
-            targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag");
-        }
-
         if (mMovedToFront) {
             // We moved the task to front, use starting window to hide initial drawn delay.
             targetTaskTop.showStartingWindow(true /* taskSwitch */);
@@ -1993,6 +2000,12 @@
         // And for paranoia, make sure we have correctly resumed the top activity.
         resumeTargetRootTaskIfNeeded();
 
+        // This is moving an existing task to front. But since dream activity has a higher z-order
+        // to cover normal activities, it needs the awakening event to be dismissed.
+        if (mService.isDreaming() && targetTaskTop.canTurnScreenOn()) {
+            targetTaskTop.mTaskSupervisor.wakeUp("recycleTask#turnScreenOnFlag");
+        }
+
         mLastStartActivityRecord = targetTaskTop;
         return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;
     }
@@ -2123,7 +2136,7 @@
                             mStartActivity.mUserId);
             if (act != null) {
                 final Task task = act.getTask();
-                boolean actuallyMoved = task.moveActivityToFrontLocked(act);
+                boolean actuallyMoved = task.moveActivityToFront(act);
                 if (actuallyMoved) {
                     // Only record if the activity actually moved.
                     mMovedToTopActivity = act;
@@ -2643,10 +2656,14 @@
             }
         }
 
-        // Update the target's launch cookie to those specified in the options if set
+        // Update the target's launch cookie and pending remote animation to those specified in the
+        // options if set.
         if (mStartActivity.mLaunchCookie != null) {
             intentActivity.mLaunchCookie = mStartActivity.mLaunchCookie;
         }
+        if (mStartActivity.mPendingRemoteAnimation != null) {
+            intentActivity.mPendingRemoteAnimation = mStartActivity.mPendingRemoteAnimation;
+        }
 
         // Need to update mTargetRootTask because if task was moved out of it, the original root
         // task may be destroyed.
@@ -2728,7 +2745,12 @@
                 newParent = candidateTf;
             }
         }
-        newParent.mTransitionController.collect(newParent);
+        if (newParent.asTask() == null) {
+            // only collect task-fragments.
+            // TODO(b/258095975): we probably shouldn't ever collect the parent here since it isn't
+            //                    changing. The logic that changes it should collect it.
+            newParent.mTransitionController.collect(newParent);
+        }
         if (mStartActivity.getTaskFragment() == null
                 || mStartActivity.getTaskFragment() == newParent) {
             newParent.addChild(mStartActivity, POSITION_TOP);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 4d970f0..ec48643 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -287,8 +287,10 @@
 
     /**
      * Called when the device changes its dreaming state.
+     *
+     * @param activeDreamComponent The currently active dream. If null, the device is not dreaming.
      */
-    public abstract void notifyDreamStateChanged(boolean dreaming);
+    public abstract void notifyActiveDreamChanged(@Nullable ComponentName activeDreamComponent);
 
     /**
      * Set a uid that is allowed to bypass stopped app switches, launching an app
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 416d546..7dd8770 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -209,7 +209,6 @@
 import android.os.WorkSource;
 import android.provider.Settings;
 import android.service.dreams.DreamActivity;
-import android.service.dreams.DreamManagerInternal;
 import android.service.voice.IVoiceInteractionSession;
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.sysprop.DisplayProperties;
@@ -460,7 +459,6 @@
     KeyguardController mKeyguardController;
     private final ClientLifecycleManager mLifecycleManager;
 
-    @Nullable
     final BackNavigationController mBackNavigationController;
 
     private TaskChangeNotificationController mTaskChangeNotificationController;
@@ -670,11 +668,12 @@
     private volatile boolean mSleeping;
 
     /**
-     * The mDreaming state is set by the {@link DreamManagerService} when it receives a request to
-     * start/stop the dream. It is set to true shortly  before the {@link DreamService} is started.
-     * It is set to false after the {@link DreamService} is stopped.
+     * The mActiveDreamComponent state is set by the {@link DreamManagerService} when it receives a
+     * request to start/stop the dream. It is set to the active dream shortly before the
+     * {@link DreamService} is started. It is set to null after the {@link DreamService} is stopped.
      */
-    private volatile boolean mDreaming;
+    @Nullable
+    private volatile ComponentName mActiveDreamComponent;
 
     /**
      * The process state used for processes that are running the top activities.
@@ -847,8 +846,7 @@
         mTaskOrganizerController = mWindowOrganizerController.mTaskOrganizerController;
         mTaskFragmentOrganizerController =
                 mWindowOrganizerController.mTaskFragmentOrganizerController;
-        mBackNavigationController = BackNavigationController.isEnabled()
-                ? new BackNavigationController() : null;
+        mBackNavigationController = new BackNavigationController();
     }
 
     public void onSystemReady() {
@@ -1031,9 +1029,7 @@
             mLockTaskController.setWindowManager(wm);
             mTaskSupervisor.setWindowManager(wm);
             mRootWindowContainer.setWindowManager(wm);
-            if (mBackNavigationController != null) {
-                mBackNavigationController.setWindowManager(wm);
-            }
+            mBackNavigationController.setWindowManager(wm);
         }
     }
 
@@ -1320,7 +1316,7 @@
                 mAppSwitchesState = APP_SWITCH_ALLOW;
             }
         }
-        return pir.sendInner(0, fillInIntent, resolvedType, allowlistToken, null, null,
+        return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null,
                 resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions);
     }
 
@@ -1443,31 +1439,21 @@
     }
 
     boolean isDreaming() {
-        return mDreaming;
+        return mActiveDreamComponent != null;
     }
 
     boolean canLaunchDreamActivity(String packageName) {
-        if (!mDreaming || packageName == null) {
+        if (mActiveDreamComponent == null || packageName == null) {
             ProtoLog.e(WM_DEBUG_DREAM, "Cannot launch dream activity due to invalid state. "
-                    + "dreaming: %b packageName: %s", mDreaming, packageName);
+                    + "dream component: %s packageName: %s", mActiveDreamComponent, packageName);
             return false;
         }
-        final DreamManagerInternal dreamManager =
-                LocalServices.getService(DreamManagerInternal.class);
-        // Verify that the package is the current active dream or doze component. The
-        // getActiveDreamComponent() call path does not acquire the DreamManager lock and thus
-        // is safe to use.
-        final ComponentName activeDream = dreamManager.getActiveDreamComponent(false /* doze */);
-        if (activeDream != null && packageName.equals(activeDream.getPackageName())) {
-            return true;
-        }
-        final ComponentName activeDoze = dreamManager.getActiveDreamComponent(true /* doze */);
-        if (activeDoze != null && packageName.equals(activeDoze.getPackageName())) {
+        if (packageName.equals(mActiveDreamComponent.getPackageName())) {
             return true;
         }
         ProtoLog.e(WM_DEBUG_DREAM,
-                "Dream packageName does not match active dream. Package %s does not match %s or %s",
-                packageName, String.valueOf(activeDream), String.valueOf(activeDoze));
+                "Dream packageName does not match active dream. Package %s does not match %s",
+                packageName, String.valueOf(mActiveDreamComponent));
         return false;
     }
 
@@ -1852,9 +1838,6 @@
             IWindowFocusObserver observer, BackAnimationAdapter adapter) {
         mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
                 "startBackNavigation()");
-        if (mBackNavigationController == null) {
-            return null;
-        }
 
         return mBackNavigationController.startBackNavigation(observer, adapter);
     }
@@ -5687,9 +5670,9 @@
         }
 
         @Override
-        public void notifyDreamStateChanged(boolean dreaming) {
+        public void notifyActiveDreamChanged(@Nullable ComponentName dreamComponent) {
             synchronized (mGlobalLock) {
-                mDreaming = dreaming;
+                mActiveDreamComponent = dreamComponent;
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 214a2c1..2f70eda 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -27,6 +27,7 @@
 import static android.app.ActivityManager.START_FLAG_NATIVE_DEBUGGING;
 import static android.app.ActivityManager.START_FLAG_TRACK_ALLOCATION;
 import static android.app.ActivityManager.START_TASK_TO_FRONT;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
 import static android.app.WaitResult.INVALID_DELAY;
@@ -2577,7 +2578,14 @@
                         // Apply options to prevent pendingOptions be taken when scheduling
                         // activity lifecycle transaction to make sure the override pending app
                         // transition will be applied immediately.
+                        if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
+                            targetActivity.mPendingRemoteAnimation =
+                                    activityOptions.getRemoteAnimationAdapter();
+                        }
                         targetActivity.applyOptionsAnimation();
+                        if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
+                            targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
+                        }
                     } finally {
                         mActivityMetricsLogger.notifyActivityLaunched(launchingState,
                                 START_TASK_TO_FRONT, false /* newActivityCreated */,
diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java
index d42a74f..7c0d658 100644
--- a/services/core/java/com/android/server/wm/AnrController.java
+++ b/services/core/java/com/android/server/wm/AnrController.java
@@ -40,6 +40,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.OptionalInt;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -332,9 +333,10 @@
             String criticalEvents =
                     CriticalEventLog.getInstance().logLinesForSystemServerTraceFile();
             final File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,
-                    null /* processCpuTracker */, null /* lastPids */, nativePids,
+                    null /* processCpuTracker */, null /* lastPids */,
+                    CompletableFuture.completedFuture(nativePids),
                     null /* logExceptionCreatingFile */, "Pre-dump", criticalEvents,
-                    null/* AnrLatencyTracker */);
+                    Runnable::run, null/* AnrLatencyTracker */);
             if (tracesFile != null) {
                 tracesFile.renameTo(
                         new File(tracesFile.getParent(), tracesFile.getName() + "_pre"));
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index d2c098b..a487797 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -1448,6 +1448,12 @@
                 || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH;
     }
 
+    static boolean isTaskFragmentTransitOld(@TransitionOldType int transit) {
+        return transit == TRANSIT_OLD_TASK_FRAGMENT_OPEN
+                || transit == TRANSIT_OLD_TASK_FRAGMENT_CLOSE
+                || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+    }
+
     static boolean isChangeTransitOld(@TransitionOldType int transit) {
         return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE
                 || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index c2e87e6..bd22b32 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -76,7 +76,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.annotation.IntDef;
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.os.Trace;
@@ -167,16 +166,6 @@
                 ? null : wallpaperTarget;
     }
 
-    @NonNull
-    private static ArraySet<ActivityRecord> getAppsForAnimation(
-            @NonNull ArraySet<ActivityRecord> apps, boolean excludeLauncherFromAnimation) {
-        final ArraySet<ActivityRecord> appsForAnimation = new ArraySet<>(apps);
-        if (excludeLauncherFromAnimation) {
-            appsForAnimation.removeIf(ConfigurationContainer::isActivityTypeHome);
-        }
-        return appsForAnimation;
-    }
-
     /**
      * Handle application transition for given display.
      */
@@ -226,45 +215,42 @@
         mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(
                 mDisplayContent.mOpeningApps);
 
-        // Remove launcher from app transition animation while recents is running. Recents animation
-        // is managed outside of app transition framework, so we just need to commit visibility.
-        final boolean excludeLauncherFromAnimation =
-                mDisplayContent.mOpeningApps.stream().anyMatch(
-                        (app) -> app.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS))
-                || mDisplayContent.mClosingApps.stream().anyMatch(
-                        (app) -> app.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS));
-        final ArraySet<ActivityRecord> openingAppsForAnimation = getAppsForAnimation(
-                mDisplayContent.mOpeningApps, excludeLauncherFromAnimation);
-        final ArraySet<ActivityRecord> closingAppsForAnimation = getAppsForAnimation(
-                mDisplayContent.mClosingApps, excludeLauncherFromAnimation);
+        ArraySet<ActivityRecord> tmpOpenApps = mDisplayContent.mOpeningApps;
+        ArraySet<ActivityRecord> tmpCloseApps = mDisplayContent.mClosingApps;
+        if (mDisplayContent.mAtmService.mBackNavigationController.isWaitBackTransition()) {
+            tmpOpenApps = new ArraySet<>(mDisplayContent.mOpeningApps);
+            tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps);
+            if (mDisplayContent.mAtmService.mBackNavigationController
+                    .removeIfContainsBackAnimationTargets(tmpOpenApps, tmpCloseApps)) {
+                mDisplayContent.mAtmService.mBackNavigationController.clearBackAnimations(null);
+            }
+        }
 
         @TransitionOldType final int transit = getTransitCompatType(
-                mDisplayContent.mAppTransition, openingAppsForAnimation, closingAppsForAnimation,
-                mDisplayContent.mChangingContainers,
+                mDisplayContent.mAppTransition, tmpOpenApps,
+                tmpCloseApps, mDisplayContent.mChangingContainers,
                 mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                 mDisplayContent.mSkipAppTransitionAnimation);
         mDisplayContent.mSkipAppTransitionAnimation = false;
 
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                 "handleAppTransitionReady: displayId=%d appTransition={%s}"
-                + " excludeLauncherFromAnimation=%b openingApps=[%s] closingApps=[%s] transit=%s",
-                mDisplayContent.mDisplayId, appTransition.toString(), excludeLauncherFromAnimation,
-                mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
-                AppTransition.appTransitionOldToString(transit));
+                + " openingApps=[%s] closingApps=[%s] transit=%s",
+                mDisplayContent.mDisplayId, appTransition.toString(), tmpOpenApps,
+                tmpCloseApps, AppTransition.appTransitionOldToString(transit));
 
         // Find the layout params of the top-most application window in the tokens, which is
         // what will control the animation theme. If all closing windows are obscured, then there is
         // no need to do an animation. This is the case, for example, when this transition is being
         // done behind a dream window.
-        final ArraySet<Integer> activityTypes = collectActivityTypes(openingAppsForAnimation,
-                closingAppsForAnimation, mDisplayContent.mChangingContainers);
+        final ArraySet<Integer> activityTypes = collectActivityTypes(tmpOpenApps,
+                tmpCloseApps, mDisplayContent.mChangingContainers);
         final ActivityRecord animLpActivity = findAnimLayoutParamsToken(transit, activityTypes,
-                openingAppsForAnimation, closingAppsForAnimation,
-                mDisplayContent.mChangingContainers);
+                tmpOpenApps, tmpCloseApps, mDisplayContent.mChangingContainers);
         final ActivityRecord topOpeningApp =
-                getTopApp(openingAppsForAnimation, false /* ignoreHidden */);
+                getTopApp(tmpOpenApps, false /* ignoreHidden */);
         final ActivityRecord topClosingApp =
-                getTopApp(closingAppsForAnimation, false /* ignoreHidden */);
+                getTopApp(tmpCloseApps, false /* ignoreHidden */);
         final ActivityRecord topChangingApp =
                 getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
         final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);
@@ -276,14 +262,13 @@
             overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
         }
 
-        final boolean voiceInteraction = containsVoiceInteraction(closingAppsForAnimation)
-                || containsVoiceInteraction(openingAppsForAnimation);
+        final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mClosingApps)
+                || containsVoiceInteraction(mDisplayContent.mOpeningApps);
 
         final int layoutRedo;
         mService.mSurfaceAnimationRunner.deferStartingAnimations();
         try {
-            applyAnimations(openingAppsForAnimation, closingAppsForAnimation, transit, animLp,
-                    voiceInteraction);
+            applyAnimations(tmpOpenApps, tmpCloseApps, transit, animLp, voiceInteraction);
             handleClosingApps();
             handleOpeningApps();
             handleChangingApps(transit);
@@ -294,8 +279,8 @@
             final int flags = appTransition.getTransitFlags();
             layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
             appTransition.postAnimationCallback();
-            appTransition.clear();
         } finally {
+            appTransition.clear();
             mService.mSurfaceAnimationRunner.continueStartingAnimations();
         }
 
@@ -1200,14 +1185,19 @@
             if (activity == null) {
                 continue;
             }
+            if (activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
+                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
+                        "Delaying app transition for recents animation to finish");
+                return false;
+            }
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                     "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
                             + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
-                    activity, activity.allDrawn, activity.startingDisplayed,
+                    activity, activity.allDrawn, activity.isStartingWindowDisplayed(),
                     activity.startingMoved, activity.isRelaunching(),
                     activity.mStartingWindow);
             final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
-            if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
+            if (!allDrawn && !activity.isStartingWindowDisplayed() && !activity.startingMoved) {
                 return false;
             }
             if (allDrawn) {
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 5a24099..d22c38e 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -31,10 +31,11 @@
 import android.util.AtomicFile;
 import android.util.DisplayMetrics;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e977447..f5da4c8 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -19,12 +19,14 @@
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.ComponentName;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
@@ -33,6 +35,7 @@
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.view.IWindowFocusObserver;
 import android.view.RemoteAnimationTarget;
@@ -41,9 +44,7 @@
 import android.window.BackNavigationInfo;
 import android.window.IBackAnimationFinishedCallback;
 import android.window.OnBackInvokedCallbackInfo;
-import android.window.ScreenCapture;
 import android.window.TaskSnapshot;
-import android.window.WindowContainerToken;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -62,15 +63,15 @@
     private boolean mShowWallpaper;
     private Runnable mPendingAnimation;
 
-    // TODO (b/241808055) Find a appropriate time to remove during refactor
-    // Execute back animation with legacy transition system. Temporary flag for easier debugging.
-    static final boolean ENABLE_SHELL_TRANSITIONS = WindowManagerService.sEnableShellTransitions;
+    private final AnimationTargets mAnimationTargets = new AnimationTargets();
+    private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>();
+    private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>();
+
     /**
-     * Returns true if the back predictability feature is enabled
+     * true if the back predictability feature is enabled
      */
-    static boolean isEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.predictive_back", 1) != 0;
-    }
+    static final boolean sPredictBackEnable =
+            SystemProperties.getBoolean("persist.wm.debug.predictive_back", true);
 
     static boolean isScreenshotEnabled() {
         return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0;
@@ -88,6 +89,9 @@
     @Nullable
     BackNavigationInfo startBackNavigation(
             IWindowFocusObserver observer, BackAnimationAdapter adapter) {
+        if (!sPredictBackEnable) {
+            return null;
+        }
         final WindowManagerService wmService = mWindowManagerService;
         mFocusObserver = observer;
 
@@ -240,16 +244,22 @@
             } else if (currentActivity.isRootOfTask()) {
                 // TODO(208789724): Create single source of truth for this, maybe in
                 //  RootWindowContainer
-                // TODO: Also check Task.shouldUpRecreateTaskLocked() for prevActivity logic
                 prevTask = currentTask.mRootWindowContainer.getTaskBelow(currentTask);
                 removedWindowContainer = currentTask;
-                prevActivity = prevTask.getTopNonFinishingActivity();
-                if (prevTask.isActivityTypeHome()) {
-                    backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
+                // If it reaches the top activity, we will check the below task from parent.
+                // If it's null or multi-window, fallback the type to TYPE_CALLBACK.
+                // or set the type to proper value when it's return to home or another task.
+                if (prevTask == null || prevTask.inMultiWindowMode()) {
+                    backType = BackNavigationInfo.TYPE_CALLBACK;
                 } else {
-                    backType = BackNavigationInfo.TYPE_CROSS_TASK;
+                    prevActivity = prevTask.getTopNonFinishingActivity();
+                    if (prevTask.isActivityTypeHome()) {
+                        backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
+                        mShowWallpaper = true;
+                    } else {
+                        backType = BackNavigationInfo.TYPE_CROSS_TASK;
+                    }
                 }
-                mShowWallpaper = true;
             }
             infoBuilder.setType(backType);
 
@@ -260,8 +270,11 @@
                     removedWindowContainer,
                     BackNavigationInfo.typeToString(backType));
 
-            // For now, we only animate when going home.
-            boolean prepareAnimation = backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
+            // For now, we only animate when going home, cross task or cross-activity.
+            boolean prepareAnimation =
+                    (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
+                            || backType == BackNavigationInfo.TYPE_CROSS_TASK
+                            || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)
                     && adapter != null;
 
             // Only prepare animation if no leash has been created (no animation is running).
@@ -274,7 +287,6 @@
             }
 
             if (prepareAnimation) {
-                infoBuilder.setDepartingWCT(toWindowContainerToken(currentTask));
                 prepareAnimationIfNeeded(currentTask, prevTask, prevActivity,
                         removedWindowContainer, backType, adapter);
             }
@@ -293,11 +305,233 @@
         return infoBuilder.build();
     }
 
-    private static WindowContainerToken toWindowContainerToken(WindowContainer<?> windowContainer) {
-        if (windowContainer == null || windowContainer.mRemoteToken == null) {
-            return null;
+    boolean isWaitBackTransition() {
+        return mAnimationTargets.mComposed && mAnimationTargets.mWaitTransition;
+    }
+
+    // For legacy transition.
+    /**
+     *  Once we find the transition targets match back animation targets, remove the target from
+     *  list, so that transition won't count them in since the close animation was finished.
+     *
+     *  @return {@code true} if the participants of this transition was animated by back gesture
+     *  animations, and shouldn't join next transition.
+     */
+    boolean removeIfContainsBackAnimationTargets(ArraySet<ActivityRecord> openApps,
+            ArraySet<ActivityRecord> closeApps) {
+        if (!isWaitBackTransition()) {
+            return false;
         }
-        return windowContainer.mRemoteToken.toWindowContainerToken();
+        mTmpCloseApps.addAll(closeApps);
+        boolean result = false;
+        // Note: TmpOpenApps is empty. Unlike shell transition, the open apps will be removed from
+        // mOpeningApps if there is no visibility change.
+        if (mAnimationTargets.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps)) {
+            // remove close target from close list, open target from open list;
+            // but the open target can be in close list.
+            for (int i = openApps.size() - 1; i >= 0; --i) {
+                final ActivityRecord ar = openApps.valueAt(i);
+                if (mAnimationTargets.isTarget(ar, true /* open */)) {
+                    openApps.removeAt(i);
+                }
+            }
+            for (int i = closeApps.size() - 1; i >= 0; --i) {
+                final ActivityRecord ar = closeApps.valueAt(i);
+                if (mAnimationTargets.isTarget(ar, false /* open */)) {
+                    closeApps.removeAt(i);
+                }
+            }
+            result = true;
+        }
+        mTmpCloseApps.clear();
+        return result;
+    }
+
+    // For shell transition
+    /**
+     *  Check whether the transition targets was animated by back gesture animation.
+     *  Because the opening target could request to do other stuff at onResume, so it could become
+     *  close target for a transition. So the condition here is
+     *  The closing target should only exist in close list, but the opening target can be either in
+     *  open or close list.
+     *  @return {@code true} if the participants of this transition was animated by back gesture
+     *  animations, and shouldn't join next transition.
+     */
+    boolean containsBackAnimationTargets(Transition transition) {
+        if (!mAnimationTargets.mComposed
+                || (transition.mType != TRANSIT_CLOSE && transition.mType != TRANSIT_TO_BACK)) {
+            return false;
+        }
+        final ArraySet<WindowContainer> targets = transition.mParticipants;
+        for (int i = targets.size() - 1; i >= 0; --i) {
+            final WindowContainer wc = targets.valueAt(i);
+            if (wc.asActivityRecord() == null && wc.asTask() == null) {
+                continue;
+            }
+            // WC can be visible due to setLaunchBehind
+            if (wc.isVisibleRequested()) {
+                mTmpOpenApps.add(wc);
+            } else {
+                mTmpCloseApps.add(wc);
+            }
+        }
+        final boolean result = mAnimationTargets.containsBackAnimationTargets(
+                mTmpOpenApps, mTmpCloseApps);
+        mTmpOpenApps.clear();
+        mTmpCloseApps.clear();
+        return result;
+    }
+
+    boolean isMonitorTransitionTarget(WindowContainer wc) {
+        if (!mAnimationTargets.mComposed || !mAnimationTargets.mWaitTransition) {
+            return false;
+        }
+        return mAnimationTargets.isTarget(wc, wc.isVisibleRequested() /* open */);
+    }
+
+    /**
+     * Cleanup animation, this can either happen when transition ready or finish.
+     * @param cleanupTransaction The transaction which the caller want to apply the internal
+     *                           cleanup together.
+     */
+    void clearBackAnimations(SurfaceControl.Transaction cleanupTransaction) {
+        mAnimationTargets.clearBackAnimateTarget(cleanupTransaction);
+    }
+
+    /**
+     * TODO: Animation composer
+     * prepareAnimationIfNeeded will become too complicated in order to support
+     * ActivityRecord/WindowState, using a factory class to create the RemoteAnimationTargets for
+     * different scenario.
+     */
+    private static class AnimationTargets {
+        ActivityRecord mCloseTarget; // Must be activity
+        WindowContainer mOpenTarget; // Can be activity or task if activity was removed
+        private boolean mComposed;
+        private boolean mWaitTransition;
+        private int mSwitchType = UNKNOWN;
+        private SurfaceControl.Transaction mFinishedTransaction;
+
+        private static final int UNKNOWN = 0;
+        private static final int TASK_SWITCH = 1;
+        private static final int ACTIVITY_SWITCH = 2;
+
+        void reset(@NonNull WindowContainer close, @NonNull WindowContainer open) {
+            clearBackAnimateTarget(null);
+            if (close.asActivityRecord() != null && open.asActivityRecord() != null
+                    && (close.asActivityRecord().getTask() == open.asActivityRecord().getTask())) {
+                mSwitchType = ACTIVITY_SWITCH;
+                mCloseTarget = close.asActivityRecord();
+            } else if (close.asTask() != null && open.asTask() != null
+                    && close.asTask() != open.asTask()) {
+                mSwitchType = TASK_SWITCH;
+                mCloseTarget = close.asTask().getTopNonFinishingActivity();
+            } else {
+                mSwitchType = UNKNOWN;
+                return;
+            }
+
+            mOpenTarget = open;
+            mComposed = false;
+            mWaitTransition = false;
+        }
+
+        void composeNewAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) {
+            reset(close, open);
+            if (mSwitchType == UNKNOWN || mComposed || mCloseTarget == mOpenTarget
+                    || mCloseTarget == null || mOpenTarget == null) {
+                return;
+            }
+            mComposed = true;
+            mWaitTransition = false;
+        }
+
+        boolean containTarget(ArrayList<WindowContainer> wcs, boolean open) {
+            for (int i = wcs.size() - 1; i >= 0; --i) {
+                if (isTarget(wcs.get(i), open)) {
+                    return true;
+                }
+            }
+            return wcs.isEmpty();
+        }
+
+        boolean isTarget(WindowContainer wc, boolean open) {
+            if (open) {
+                return wc == mOpenTarget || mOpenTarget.hasChild(wc);
+            }
+            if (mSwitchType == TASK_SWITCH) {
+                return  wc == mCloseTarget
+                        || (wc.asTask() != null && wc.hasChild(mCloseTarget));
+            } else if (mSwitchType == ACTIVITY_SWITCH) {
+                return wc == mCloseTarget;
+            }
+            return false;
+        }
+
+        boolean setFinishTransaction(SurfaceControl.Transaction finishTransaction) {
+            if (!mComposed) {
+                return false;
+            }
+            mFinishedTransaction = finishTransaction;
+            return true;
+        }
+
+        void finishPresentAnimations(SurfaceControl.Transaction t) {
+            if (!mComposed) {
+                return;
+            }
+            final SurfaceControl.Transaction pt = t != null ? t
+                    : mOpenTarget.getPendingTransaction();
+            if (mFinishedTransaction != null) {
+                pt.merge(mFinishedTransaction);
+                mFinishedTransaction = null;
+            }
+        }
+
+        void clearBackAnimateTarget(SurfaceControl.Transaction cleanupTransaction) {
+            finishPresentAnimations(cleanupTransaction);
+            mCloseTarget = null;
+            mOpenTarget = null;
+            mComposed = false;
+            mWaitTransition = false;
+            mSwitchType = UNKNOWN;
+            if (mFinishedTransaction != null) {
+                Slog.w(TAG, "Clear back animation, found un-processed finished transaction");
+                if (cleanupTransaction != null) {
+                    cleanupTransaction.merge(mFinishedTransaction);
+                } else {
+                    mFinishedTransaction.apply();
+                }
+                mFinishedTransaction = null;
+            }
+        }
+
+        // The close target must in close list
+        // The open target can either in close or open list
+        boolean containsBackAnimationTargets(ArrayList<WindowContainer> openApps,
+                ArrayList<WindowContainer> closeApps) {
+            return containTarget(closeApps, false /* open */)
+                    && (containTarget(openApps, true /* open */)
+                    || containTarget(openApps, false /* open */));
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder(128);
+            sb.append("AnimationTargets{");
+            sb.append(" mOpenTarget= ");
+            sb.append(mOpenTarget);
+            sb.append(" mCloseTarget= ");
+            sb.append(mCloseTarget);
+            sb.append(" mSwitchType= ");
+            sb.append(mSwitchType);
+            sb.append(" mComposed= ");
+            sb.append(mComposed);
+            sb.append(" mWaitTransition= ");
+            sb.append(mWaitTransition);
+            sb.append('}');
+            return sb.toString();
+        }
     }
 
     private void prepareAnimationIfNeeded(Task currentTask,
@@ -325,6 +559,7 @@
         RemoteAnimationTarget behindAppTarget = null;
         if (needsScreenshot(backType)) {
             HardwareBuffer screenshotBuffer = null;
+            Task backTargetTask = prevTask;
             switch(backType) {
                 case BackNavigationInfo.TYPE_CROSS_TASK:
                     int prevTaskId = prevTask != null ? prevTask.mTaskId : 0;
@@ -332,14 +567,10 @@
                     screenshotBuffer = getTaskSnapshot(prevTaskId, prevUserId);
                     break;
                 case BackNavigationInfo.TYPE_CROSS_ACTIVITY:
-                    //TODO(207481538) Remove once the infrastructure to support per-activity
-                    // screenshot is implemented. For now we simply have the mBackScreenshots hash
-                    // map that dumbly saves the screenshots.
-                    if (prevActivity != null
-                            && prevActivity.mActivityComponent != null) {
-                        screenshotBuffer =
-                                getActivitySnapshot(currentTask, prevActivity.mActivityComponent);
+                    if (prevActivity != null && prevActivity.mActivityComponent != null) {
+                        screenshotBuffer = getActivitySnapshot(currentTask, prevActivity);
                     }
+                    backTargetTask = currentTask;
                     break;
             }
 
@@ -358,17 +589,14 @@
                 // leash needs to be added before to be in the synchronized block.
                 startedTransaction.setLayer(topAppTarget.leash, 1);
 
-                behindAppTarget = createRemoteAnimationTargetLocked(
-                        prevTask, screenshotSurface, MODE_OPENING);
+                behindAppTarget =
+                        createRemoteAnimationTargetLocked(
+                                backTargetTask, screenshotSurface, MODE_OPENING);
 
                 // reset leash after animation finished.
                 leashes.add(screenshotSurface);
             }
         } else if (prevTask != null) {
-            if (!ENABLE_SHELL_TRANSITIONS) {
-                // Special handling for preventing next transition.
-                currentTask.mBackGestureStarted = true;
-            }
             prevActivity = prevTask.getTopNonFinishingActivity();
             if (prevActivity != null) {
                 // Make previous task show from behind by marking its top activity as visible
@@ -409,35 +637,36 @@
                         for (SurfaceControl sc: leashes) {
                             finishedTransaction.remove(sc);
                         }
-
                         synchronized (mWindowManagerService.mGlobalLock) {
-                            if (ENABLE_SHELL_TRANSITIONS) {
-                                if (!triggerBack) {
-                                    if (!needsScreenshot(backType)) {
-                                        restoreLaunchBehind(finalPrevActivity);
-                                    }
+                            if (triggerBack) {
+                                final SurfaceControl surfaceControl =
+                                        removedWindowContainer.getSurfaceControl();
+                                if (surfaceControl != null && surfaceControl.isValid()) {
+                                    // The animation is finish and start waiting for transition,
+                                    // hide the task surface before it re-parented to avoid flicker.
+                                    finishedTransaction.hide(surfaceControl);
                                 }
+                            } else if (!needsScreenshot(backType)) {
+                                restoreLaunchBehind(finalPrevActivity);
+                            }
+                            if (!mAnimationTargets.setFinishTransaction(finishedTransaction)) {
+                                finishedTransaction.apply();
+                            }
+                            if (!triggerBack) {
+                                mAnimationTargets.clearBackAnimateTarget(null);
                             } else {
-                                if (triggerBack) {
-                                    final SurfaceControl surfaceControl =
-                                            removedWindowContainer.getSurfaceControl();
-                                    if (surfaceControl != null && surfaceControl.isValid()) {
-                                        // When going back to home, hide the task surface before it
-                                        // re-parented to avoid flicker.
-                                        finishedTransaction.hide(surfaceControl);
-                                    }
-                                } else {
-                                    currentTask.mBackGestureStarted = false;
-                                    if (!needsScreenshot(backType)) {
-                                        restoreLaunchBehind(finalPrevActivity);
-                                    }
-                                }
+                                mAnimationTargets.mWaitTransition = true;
                             }
                         }
-                        finishedTransaction.apply();
+                        // TODO Add timeout monitor if transition didn't happen
                     }
                 };
-
+        if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
+            mAnimationTargets.composeNewAnimations(removedWindowContainer, prevActivity);
+        } else if (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
+                || backType == BackNavigationInfo.TYPE_CROSS_TASK) {
+            mAnimationTargets.composeNewAnimations(removedWindowContainer, prevTask);
+        }
         scheduleAnimationLocked(backType, targets, adapter, callback);
     }
 
@@ -532,14 +761,8 @@
         mShowWallpaper = false;
     }
 
-    private HardwareBuffer getActivitySnapshot(@NonNull Task task,
-            ComponentName activityComponent) {
-        // Check if we have a screenshot of the previous activity, indexed by its
-        // component name.
-        ScreenCapture.ScreenshotHardwareBuffer backBuffer = task.mBackScreenshots
-                .get(activityComponent.flattenToString());
-        return backBuffer != null ? backBuffer.getHardwareBuffer() : null;
-
+    private HardwareBuffer getActivitySnapshot(@NonNull Task task, ActivityRecord r) {
+        return task.getSnapshotForActivityRecord(r);
     }
 
     private HardwareBuffer getTaskSnapshot(int taskId, int userId) {
@@ -588,6 +811,7 @@
 
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
                 "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
+        activity.mTaskSupervisor.mStoppingActivities.remove(activity);
         activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
                 0 /* configChanges */, false /* preserveWindows */, true);
     }
@@ -608,9 +832,8 @@
     }
 
     boolean isWallpaperVisible(WindowState w) {
-        if (mBackAnimationInProgress && w.isFocused()) {
-            return mShowWallpaper;
-        }
-        return false;
+        return mAnimationTargets.mComposed && mShowWallpaper
+                && w.mAttrs.type == TYPE_BASE_APPLICATION && w.mActivityRecord != null
+                && mAnimationTargets.isTarget(w.mActivityRecord, true /* open */);
     }
 }
diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java
index 6f19450..a035948 100644
--- a/services/core/java/com/android/server/wm/CompatModePackages.java
+++ b/services/core/java/com/android/server/wm/CompatModePackages.java
@@ -44,11 +44,11 @@
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index e7ab63e..13a1cb6 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -216,14 +216,10 @@
             return;
         }
 
-        if (container != null) {
-            // The dim method is called from WindowState.prepareSurfaces(), which is always called
-            // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
-            // relative to the highest Z layer with a dim.
-            t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
-        } else {
-            t.setLayer(d.mDimLayer, Integer.MAX_VALUE);
-        }
+        // The dim method is called from WindowState.prepareSurfaces(), which is always called
+        // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
+        // relative to the highest Z layer with a dim.
+        t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
         t.setAlpha(d.mDimLayer, alpha);
         t.setBackgroundBlurRadius(d.mDimLayer, blurRadius);
 
@@ -231,32 +227,6 @@
     }
 
     /**
-     * Finish a dim started by dimAbove in the case there was no call to dimAbove.
-     *
-     * @param t A Transaction in which to finish the dim.
-     */
-    void stopDim(SurfaceControl.Transaction t) {
-        if (mDimState != null) {
-            t.hide(mDimState.mDimLayer);
-            mDimState.isVisible = false;
-            mDimState.mDontReset = false;
-        }
-    }
-
-    /**
-     * Place a Dim above the entire host container. The caller is responsible for calling stopDim to
-     * remove this effect. If the Dim can be assosciated with a particular child of the host
-     * consider using the other variant of dimAbove which ties the Dim lifetime to the child
-     * lifetime more explicitly.
-     *
-     * @param t     A transaction in which to apply the Dim.
-     * @param alpha The alpha at which to Dim.
-     */
-    void dimAbove(SurfaceControl.Transaction t, float alpha) {
-        dim(t, null, 1, alpha, 0);
-    }
-
-    /**
      * Place a dim above the given container, which should be a child of the host container.
      * for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
      * and the child should call dimAbove again to request the Dim to continue.
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index b84b2d8..bedeabe 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -369,6 +369,55 @@
     }
 
     @Override
+    ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
+            ActivityRecord boundary) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return null;
+        }
+        return super.getActivity(callback, traverseTopToBottom, boundary);
+    }
+
+    @Override
+    Task getTask(Predicate<Task> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return null;
+        }
+        return super.getTask(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllActivities(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllRootTasks(callback, traverseTopToBottom);
+    }
+
+    @Override
+    boolean forAllTasks(Predicate<Task> callback) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllTasks(callback);
+    }
+
+    @Override
+    boolean forAllLeafTasks(Predicate<Task> callback) {
+        if (mType == Type.ABOVE_TASKS || mType == Type.BELOW_TASKS) {
+            return false;
+        }
+        return super.forAllLeafTasks(callback);
+    }
+
+    @Override
     void forAllDisplayAreas(Consumer<DisplayArea> callback) {
         super.forAllDisplayAreas(callback);
         callback.accept(this);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 3c847ce..12efe0d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -47,7 +47,6 @@
 import static android.view.Display.isSuspendedState;
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_270;
@@ -55,6 +54,7 @@
 import static android.view.View.GONE;
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
@@ -216,7 +216,6 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
 import android.view.MagnificationSpec;
 import android.view.PrivacyIndicatorBounds;
 import android.view.RemoteAnimationDefinition;
@@ -227,9 +226,11 @@
 import android.view.SurfaceControl.Transaction;
 import android.view.SurfaceSession;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
 import android.view.WindowManagerPolicyConstants.PointerEventListener;
+import android.view.inputmethod.ImeTracker;
 import android.window.DisplayWindowPolicyController;
 import android.window.IDisplayAreaOrganizer;
 import android.window.ScreenCapture;
@@ -454,7 +455,7 @@
 
     /**
      * Compat metrics computed based on {@link #mDisplayMetrics}.
-     * @see #updateDisplayAndOrientation(int, Configuration)
+     * @see #updateDisplayAndOrientation(Configuration)
      */
     private final DisplayMetrics mCompatDisplayMetrics = new DisplayMetrics();
 
@@ -788,11 +789,11 @@
         // higher window hierarchy, we don't give it focus if the next IME layering target
         // doesn't request IME visible.
         if (w.mIsImWindow && w.isChildWindow() && (mImeLayeringTarget == null
-                || !mImeLayeringTarget.getRequestedVisibility(ITYPE_IME))) {
+                || !mImeLayeringTarget.isRequestedVisible(ime()))) {
             return false;
         }
         if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null
-                && !mImeLayeringTarget.getRequestedVisibility(ITYPE_IME)
+                && !mImeLayeringTarget.isRequestedVisible(ime())
                 && !mImeLayeringTarget.isVisibleRequested()) {
             return false;
         }
@@ -2059,7 +2060,7 @@
         // is opened for logging metrics.
         if (mWmService.mAccessibilityController.hasCallbacks()) {
             final boolean isImeShow = mImeControlTarget != null
-                    && mImeControlTarget.getRequestedVisibility(ITYPE_IME);
+                    && mImeControlTarget.isRequestedVisible(ime());
             mWmService.mAccessibilityController.updateImeVisibilityIfNeeded(mDisplayId, isImeShow);
         }
     }
@@ -2188,8 +2189,7 @@
             mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED;
         }
 
-        computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
 
         mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
                 mDisplayInfo);
@@ -2289,8 +2289,7 @@
         displayInfo.appHeight = appBounds.height();
         final DisplayCutout displayCutout = calculateDisplayCutoutForRotation(rotation);
         displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
-        computeSizeRangesAndScreenLayout(displayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
         return displayInfo;
     }
 
@@ -2309,6 +2308,9 @@
         outConfig.screenHeightDp = (int) (info.mConfigFrame.height() / density + 0.5f);
         outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
         outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+        outConfig.screenLayout = computeScreenLayout(
+                Configuration.resetScreenLayout(outConfig.screenLayout),
+                outConfig.screenWidthDp, outConfig.screenHeightDp);
 
         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
         outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, dw, dh);
@@ -2450,7 +2452,7 @@
         return curSize;
     }
 
-    private void computeSizeRangesAndScreenLayout(DisplayInfo displayInfo, boolean rotated,
+    private void computeSizeRanges(DisplayInfo displayInfo, boolean rotated,
             int dw, int dh, float density, Configuration outConfig) {
 
         // We need to determine the smallest width that will occur under normal
@@ -2477,31 +2479,8 @@
         if (outConfig == null) {
             return;
         }
-        int sl = Configuration.resetScreenLayout(outConfig.screenLayout);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw);
         outConfig.smallestScreenWidthDp =
                 (int) (displayInfo.smallestNominalAppWidth / density + 0.5f);
-        outConfig.screenLayout = sl;
-    }
-
-    private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh) {
-        // Get the app screen size at this rotation.
-        final Rect size = mDisplayPolicy.getDecorInsetsInfo(rotation, dw, dh).mNonDecorFrame;
-
-        // Compute the screen layout size class for this rotation.
-        int longSize = size.width();
-        int shortSize = size.height();
-        if (longSize < shortSize) {
-            int tmp = longSize;
-            longSize = shortSize;
-            shortSize = tmp;
-        }
-        longSize = (int) (longSize / density + 0.5f);
-        shortSize = (int) (shortSize / density + 0.5f);
-        return Configuration.reduceScreenLayout(curLayout, longSize, shortSize);
     }
 
     private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh) {
@@ -5053,7 +5032,7 @@
      *   layer has been assigned since), to facilitate assigning the layer from the IME target, or
      *   fall back if there is no target.
      * - the container doesn't always participate in window traversal, according to
-     *   {@link #skipImeWindowsDuringTraversal()}
+     *   {@link #skipImeWindowsDuringTraversal(DisplayContent)}
      */
     private static class ImeContainer extends DisplayArea.Tokens {
         boolean mNeedsLayer = false;
@@ -5684,7 +5663,7 @@
         final int type = win.mAttrs.type;
         final int privateFlags = win.mAttrs.privateFlags;
         final boolean stickyHideNav =
-                !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR)
+                !win.isRequestedVisible(navigationBars())
                         && win.mAttrs.insetsFlags.behavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
         return (!stickyHideNav || ignoreRequest) && type != TYPE_INPUT_METHOD
                 && type != TYPE_NOTIFICATION_SHADE && win.getActivityType() != ACTIVITY_TYPE_HOME
@@ -6694,7 +6673,7 @@
 
     class RemoteInsetsControlTarget implements InsetsControlTarget {
         private final IDisplayWindowInsetsController mRemoteInsetsController;
-        private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+        private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         private final boolean mCanShowTransient;
 
         RemoteInsetsControlTarget(IDisplayWindowInsetsController controller) {
@@ -6707,12 +6686,12 @@
          * Notifies the remote insets controller that the top focused window has changed.
          *
          * @param component The application component that is open in the top focussed window.
-         * @param requestedVisibilities The insets visibilities requested by the focussed window.
+         * @param requestedVisibleTypes The insets types requested visible by the focused window.
          */
         void topFocusedWindowChanged(ComponentName component,
-                InsetsVisibilities requestedVisibilities) {
+                @InsetsType int requestedVisibleTypes) {
             try {
-                mRemoteInsetsController.topFocusedWindowChanged(component, requestedVisibilities);
+                mRemoteInsetsController.topFocusedWindowChanged(component, requestedVisibleTypes);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to deliver package in top focused window change", e);
             }
@@ -6734,25 +6713,35 @@
                 mRemoteInsetsController.insetsControlChanged(stateController.getRawInsetsState(),
                         stateController.getControlsForDispatch(this));
             } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to deliver inset state change", e);
+                Slog.w(TAG, "Failed to deliver inset control state change", e);
             }
         }
 
         @Override
-        public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) {
+        public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             try {
-                mRemoteInsetsController.showInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
+                mRemoteInsetsController.showInsets(types, fromIme, statsToken);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to deliver showInsets", e);
+                ImeTracker.get().onFailed(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS);
             }
         }
 
         @Override
-        public void hideInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) {
+        public void hideInsets(@InsetsType int types, boolean fromIme,
+                @Nullable ImeTracker.Token statsToken) {
             try {
-                mRemoteInsetsController.hideInsets(types, fromIme);
+                ImeTracker.get().onProgress(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
+                mRemoteInsetsController.hideInsets(types, fromIme, statsToken);
             } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to deliver showInsets", e);
+                Slog.w(TAG, "Failed to deliver hideInsets", e);
+                ImeTracker.get().onFailed(statsToken,
+                        ImeTracker.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS);
             }
         }
 
@@ -6762,15 +6751,25 @@
         }
 
         @Override
-        public boolean getRequestedVisibility(@InternalInsetsType int type) {
-            if (type == ITYPE_IME) {
+        public boolean isRequestedVisible(@InsetsType int types) {
+            if (types == ime()) {
                 return getInsetsStateController().getImeSourceProvider().isImeShowing();
             }
-            return mRequestedVisibilities.getVisibility(type);
+            return (mRequestedVisibleTypes & types) != 0;
         }
 
-        void setRequestedVisibilities(InsetsVisibilities requestedVisibilities) {
-            mRequestedVisibilities.set(requestedVisibilities);
+        @Override
+        public @InsetsType int getRequestedVisibleTypes() {
+            return mRequestedVisibleTypes;
+        }
+
+        /**
+         * @see #getRequestedVisibleTypes()
+         */
+        void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
+            if (mRequestedVisibleTypes != requestedVisibleTypes) {
+                mRequestedVisibleTypes = requestedVisibleTypes;
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 442777a..1fef3c2 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -19,14 +19,11 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.view.Display.TYPE_INTERNAL;
-import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES;
-import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
+import static android.view.InsetsFrameProvider.SOURCE_FRAME;
 import static android.view.InsetsState.ITYPE_CAPTION_BAR;
 import static android.view.InsetsState.ITYPE_CLIMATE_BAR;
 import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_LEFT_GESTURES;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_RIGHT_GESTURES;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -47,7 +44,6 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
@@ -79,11 +75,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SCREEN_ON;
 import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
-import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER;
-import static com.android.server.policy.WindowManagerPolicy.TRANSIT_EXIT;
-import static com.android.server.policy.WindowManagerPolicy.TRANSIT_HIDE;
 import static com.android.server.policy.WindowManagerPolicy.TRANSIT_PREVIEW_DONE;
-import static com.android.server.policy.WindowManagerPolicy.TRANSIT_SHOW;
 import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -123,7 +115,6 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewDebug;
@@ -209,14 +200,11 @@
     private StatusBarManagerInternal mStatusBarManagerInternal;
 
     @Px
-    private int mBottomGestureAdditionalInset;
-    @Px
     private int mLeftGestureInset;
     @Px
     private int mRightGestureInset;
 
     private boolean mCanSystemBarsBeShownByUser;
-    private boolean mNavButtonForcedVisible;
 
     StatusBarManagerInternal getStatusBarManagerInternal() {
         synchronized (mServiceAcquireLock) {
@@ -240,7 +228,6 @@
     private volatile boolean mHasNavigationBar;
     // Can the navigation bar ever move to the side?
     private volatile boolean mNavigationBarCanMove;
-    private volatile boolean mNavigationBarLetsThroughTaps;
     private volatile boolean mNavigationBarAlwaysShowOnSideGesture;
 
     // Written by vr manager thread, only read in this class.
@@ -283,6 +270,12 @@
 
     private final ArraySet<WindowState> mInsetsSourceWindowsExceptIme = new ArraySet<>();
 
+    /** Apps which are controlling the appearance of system bars */
+    private final ArraySet<ActivityRecord> mSystemBarColorApps = new ArraySet<>();
+
+    /** Apps which are relaunching and were controlling the appearance of system bars */
+    private final ArraySet<ActivityRecord> mRelaunchingSystemBarColorApps = new ArraySet<>();
+
     private boolean mIsFreeformWindowOverlappingWithNavBar;
 
     private boolean mLastImmersiveMode;
@@ -324,7 +317,7 @@
     private int mLastDisableFlags;
     private int mLastAppearance;
     private int mLastBehavior;
-    private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+    private int mLastRequestedVisibleTypes = Type.defaultVisible();
     private AppearanceRegion[] mLastStatusBarAppearanceRegions;
     private LetterboxDetails[] mLastLetterboxDetails;
 
@@ -360,8 +353,6 @@
 
     private PointerLocationView mPointerLocationView;
 
-    private int mDisplayCutoutTouchableRegionSize;
-
     private RefreshRatePolicy mRefreshRatePolicy;
 
     /**
@@ -1150,71 +1141,9 @@
                 break;
             case TYPE_NAVIGATION_BAR:
                 mNavigationBar = win;
-                final TriConsumer<DisplayFrames, WindowContainer, Rect> navFrameProvider =
-                        (displayFrames, windowContainer, inOutFrame) -> {
-                            if (!mNavButtonForcedVisible) {
-                                final LayoutParams lp =
-                                        win.mAttrs.forRotation(displayFrames.mRotation);
-                                if (lp.providedInsets != null) {
-                                    for (InsetsFrameProvider provider : lp.providedInsets) {
-                                        if (provider.type != ITYPE_NAVIGATION_BAR) {
-                                            continue;
-                                        }
-                                        InsetsFrameProvider.calculateInsetsFrame(
-                                                displayFrames.mUnrestricted,
-                                                win.getBounds(), displayFrames.mDisplayCutoutSafe,
-                                                inOutFrame, provider.source,
-                                                provider.insetsSize, lp.privateFlags,
-                                                provider.minimalInsetsSizeInDisplayCutoutSafe);
-                                    }
-                                }
-                                inOutFrame.inset(win.mGivenContentInsets);
-                            }
-                        };
-                final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>> imeOverride =
-                        new SparseArray<>();
-                // For IME, we don't modify the frame.
-                imeOverride.put(TYPE_INPUT_METHOD, null);
-                mDisplayContent.setInsetProvider(ITYPE_NAVIGATION_BAR, win,
-                        navFrameProvider, imeOverride);
-
-                mDisplayContent.setInsetProvider(ITYPE_BOTTOM_MANDATORY_GESTURES, win,
-                        (displayFrames, windowContainer, inOutFrame) -> {
-                            inOutFrame.top -= mBottomGestureAdditionalInset;
-                        });
-                mDisplayContent.setInsetProvider(ITYPE_LEFT_GESTURES, win,
-                        (displayFrames, windowContainer, inOutFrame) -> {
-                            final int leftSafeInset =
-                                    Math.max(displayFrames.mDisplayCutoutSafe.left, 0);
-                            inOutFrame.left = 0;
-                            inOutFrame.top = 0;
-                            inOutFrame.bottom = displayFrames.mHeight;
-                            inOutFrame.right = leftSafeInset + mLeftGestureInset;
-                        });
-                mDisplayContent.setInsetProvider(ITYPE_RIGHT_GESTURES, win,
-                        (displayFrames, windowContainer, inOutFrame) -> {
-                            final int rightSafeInset =
-                                    Math.min(displayFrames.mDisplayCutoutSafe.right,
-                                            displayFrames.mUnrestricted.right);
-                            inOutFrame.left = rightSafeInset - mRightGestureInset;
-                            inOutFrame.top = 0;
-                            inOutFrame.bottom = displayFrames.mHeight;
-                            inOutFrame.right = displayFrames.mWidth;
-                        });
-                mDisplayContent.setInsetProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT, win,
-                        (displayFrames, windowContainer, inOutFrame) -> {
-                            if ((win.getAttrs().flags & FLAG_NOT_TOUCHABLE) != 0
-                                    || mNavigationBarLetsThroughTaps) {
-                                inOutFrame.setEmpty();
-                            }
-                        });
-                mInsetsSourceWindowsExceptIme.add(win);
-                if (DEBUG_LAYOUT) Slog.i(TAG, "NAVIGATION BAR: " + mNavigationBar);
                 break;
         }
-        // TODO(b/239145252): Temporarily skip the navigation bar as it is still with the hard-coded
-        // logic.
-        if (attrs.providedInsets != null && attrs.type != TYPE_NAVIGATION_BAR) {
+        if (attrs.providedInsets != null) {
             for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
                 final InsetsFrameProvider provider = attrs.providedInsets[i];
                 switch (provider.type) {
@@ -1242,24 +1171,8 @@
                 // The index of the provider and corresponding insets types cannot change at
                 // runtime as ensured in WMS. Make use of the index in the provider directly
                 // to access the latest provided size at runtime.
-                final int index = i;
                 final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider =
-                        provider.insetsSize != null
-                                ? (displayFrames, windowContainer, inOutFrame) -> {
-                                    inOutFrame.inset(win.mGivenContentInsets);
-                                    final LayoutParams lp =
-                                            win.mAttrs.forRotation(displayFrames.mRotation);
-                                    final InsetsFrameProvider ifp =
-                                            win.mAttrs.forRotation(displayFrames.mRotation)
-                                                    .providedInsets[index];
-                                    InsetsFrameProvider.calculateInsetsFrame(
-                                            displayFrames.mUnrestricted,
-                                            windowContainer.getBounds(),
-                                            displayFrames.mDisplayCutoutSafe,
-                                            inOutFrame, ifp.source,
-                                            ifp.insetsSize, lp.privateFlags,
-                                            ifp.minimalInsetsSizeInDisplayCutoutSafe);
-                                } : null;
+                        getFrameProvider(win, provider, i);
                 final InsetsFrameProvider.InsetsSizeOverride[] overrides =
                         provider.insetsSizeOverrides;
                 final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>>
@@ -1267,27 +1180,10 @@
                 if (overrides != null) {
                     overrideProviders = new SparseArray<>();
                     for (int j = overrides.length - 1; j >= 0; j--) {
-                        final int overrideIndex = j;
                         final TriConsumer<DisplayFrames, WindowContainer, Rect>
                                 overrideFrameProvider =
-                                        (displayFrames, windowContainer, inOutFrame) -> {
-                                            final LayoutParams lp =
-                                                    win.mAttrs.forRotation(
-                                                            displayFrames.mRotation);
-                                            final InsetsFrameProvider ifp =
-                                                    win.mAttrs.providedInsets[index];
-                                            InsetsFrameProvider.calculateInsetsFrame(
-                                                    displayFrames.mUnrestricted,
-                                                    windowContainer.getBounds(),
-                                                    displayFrames.mDisplayCutoutSafe,
-                                                    inOutFrame, ifp.source,
-                                                    ifp.insetsSizeOverrides[
-                                                            overrideIndex].insetsSize,
-                                                    lp.privateFlags,
-                                                    null);
-                                        };
-                        overrideProviders.put(overrides[j].windowType,
-                                overrideFrameProvider);
+                                getOverrideFrameProvider(win, i, j);
+                        overrideProviders.put(overrides[j].windowType, overrideFrameProvider);
                     }
                 } else {
                     overrideProviders = null;
@@ -1299,6 +1195,36 @@
         }
     }
 
+    @Nullable
+    private TriConsumer<DisplayFrames, WindowContainer, Rect> getFrameProvider(WindowState win,
+            InsetsFrameProvider provider, int index) {
+        if (provider.insetsSize == null && provider.source == SOURCE_FRAME) {
+            return null;
+        }
+        return (displayFrames, windowContainer, inOutFrame) -> {
+            inOutFrame.inset(win.mGivenContentInsets);
+            final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation);
+            final InsetsFrameProvider ifp = lp.providedInsets[index];
+            InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted,
+                    windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame,
+                    ifp.source, ifp.insetsSize, lp.privateFlags,
+                    ifp.minimalInsetsSizeInDisplayCutoutSafe);
+        };
+    }
+
+    @NonNull
+    private TriConsumer<DisplayFrames, WindowContainer, Rect> getOverrideFrameProvider(
+            WindowState win, int index, int overrideIndex) {
+        return (displayFrames, windowContainer, inOutFrame) -> {
+            final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation);
+            final InsetsFrameProvider ifp = lp.providedInsets[index];
+            InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted,
+                    windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame,
+                    ifp.source, ifp.insetsSizeOverrides[overrideIndex].insetsSize, lp.privateFlags,
+                    null);
+        };
+    }
+
     @WindowManagerPolicy.AltBarPosition
     private int getAltBarPosition(WindowManager.LayoutParams params) {
         switch (params.gravity) {
@@ -1386,16 +1312,6 @@
         mInsetsSourceWindowsExceptIme.remove(win);
     }
 
-    private int getStatusBarHeight(DisplayFrames displayFrames) {
-        int statusBarHeight;
-        if (mStatusBar != null) {
-            statusBarHeight = mStatusBar.mAttrs.forRotation(displayFrames.mRotation).height;
-        } else {
-            statusBarHeight = 0;
-        }
-        return Math.max(statusBarHeight, displayFrames.mDisplayCutoutSafe.top);
-    }
-
     WindowState getStatusBar() {
         return mStatusBar != null ? mStatusBar : mStatusBarAlt;
     }
@@ -1424,90 +1340,6 @@
      */
     int selectAnimation(WindowState win, int transit) {
         ProtoLog.i(WM_DEBUG_ANIM, "selectAnimation in %s: transit=%d", win, transit);
-        if (win == mStatusBar) {
-            if (transit == TRANSIT_EXIT
-                    || transit == TRANSIT_HIDE) {
-                return R.anim.dock_top_exit;
-            } else if (transit == TRANSIT_ENTER
-                    || transit == TRANSIT_SHOW) {
-                return R.anim.dock_top_enter;
-            }
-        } else if (win == mNavigationBar) {
-            if (win.getAttrs().windowAnimations != 0) {
-                return ANIMATION_STYLEABLE;
-            }
-            // This can be on either the bottom or the right or the left.
-            if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
-                if (transit == TRANSIT_EXIT
-                        || transit == TRANSIT_HIDE) {
-                    if (mService.mPolicy.isKeyguardShowingAndNotOccluded()) {
-                        return R.anim.dock_bottom_exit_keyguard;
-                    } else {
-                        return R.anim.dock_bottom_exit;
-                    }
-                } else if (transit == TRANSIT_ENTER
-                        || transit == TRANSIT_SHOW) {
-                    return R.anim.dock_bottom_enter;
-                }
-            } else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
-                if (transit == TRANSIT_EXIT
-                        || transit == TRANSIT_HIDE) {
-                    return R.anim.dock_right_exit;
-                } else if (transit == TRANSIT_ENTER
-                        || transit == TRANSIT_SHOW) {
-                    return R.anim.dock_right_enter;
-                }
-            } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
-                if (transit == TRANSIT_EXIT
-                        || transit == TRANSIT_HIDE) {
-                    return R.anim.dock_left_exit;
-                } else if (transit == TRANSIT_ENTER
-                        || transit == TRANSIT_SHOW) {
-                    return R.anim.dock_left_enter;
-                }
-            }
-        } else if (win == mStatusBarAlt || win == mNavigationBarAlt || win == mClimateBarAlt
-                || win == mExtraNavBarAlt) {
-            if (win.getAttrs().windowAnimations != 0) {
-                return ANIMATION_STYLEABLE;
-            }
-
-            int pos = (win == mStatusBarAlt) ? mStatusBarAltPosition : mNavigationBarAltPosition;
-
-            boolean isExitOrHide = transit == TRANSIT_EXIT || transit == TRANSIT_HIDE;
-            boolean isEnterOrShow = transit == TRANSIT_ENTER || transit == TRANSIT_SHOW;
-
-            switch (pos) {
-                case ALT_BAR_LEFT:
-                    if (isExitOrHide) {
-                        return R.anim.dock_left_exit;
-                    } else if (isEnterOrShow) {
-                        return R.anim.dock_left_enter;
-                    }
-                    break;
-                case ALT_BAR_RIGHT:
-                    if (isExitOrHide) {
-                        return R.anim.dock_right_exit;
-                    } else if (isEnterOrShow) {
-                        return R.anim.dock_right_enter;
-                    }
-                    break;
-                case ALT_BAR_BOTTOM:
-                    if (isExitOrHide) {
-                        return R.anim.dock_bottom_exit;
-                    } else if (isEnterOrShow) {
-                        return R.anim.dock_bottom_enter;
-                    }
-                    break;
-                case ALT_BAR_TOP:
-                    if (isExitOrHide) {
-                        return R.anim.dock_top_exit;
-                    } else if (isEnterOrShow) {
-                        return R.anim.dock_top_enter;
-                    }
-                    break;
-            }
-        }
 
         if (transit == TRANSIT_PREVIEW_DONE) {
             if (win.hasAppShownWindows()) {
@@ -1551,7 +1383,7 @@
             mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation),
                     displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe,
                     displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH,
-                    UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale,
+                    UNSPECIFIED_LENGTH, win.getRequestedVisibleTypes(), win.mGlobalScale,
                     sTmpClientFrames);
             final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
             final InsetsState state = displayFrames.mInsetsState;
@@ -1598,7 +1430,7 @@
 
         mWindowLayout.computeFrames(attrs, win.getInsetsState(), displayFrames.mDisplayCutoutSafe,
                 win.getBounds(), win.getWindowingMode(), requestedWidth, requestedHeight,
-                win.getRequestedVisibilities(), win.mGlobalScale, sTmpClientFrames);
+                win.getRequestedVisibleTypes(), win.mGlobalScale, sTmpClientFrames);
 
         win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight);
     }
@@ -1623,6 +1455,7 @@
         mStatusBarBackgroundWindows.clear();
         mStatusBarColorCheckedBounds.setEmpty();
         mStatusBarBackgroundCheckedBounds.setEmpty();
+        mSystemBarColorApps.clear();
 
         mAllowLockscreenWhenOn = false;
         mShowingDream = false;
@@ -1699,6 +1532,7 @@
                             win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
                             new Rect(win.getFrame())));
                     mStatusBarColorCheckedBounds.union(sTmpRect);
+                    addSystemBarColorApp(win);
                 }
             }
 
@@ -1711,6 +1545,7 @@
             if (isOverlappingWithNavBar) {
                 if (mNavBarColorWindowCandidate == null) {
                     mNavBarColorWindowCandidate = win;
+                    addSystemBarColorApp(win);
                 }
                 if (mNavBarBackgroundWindow == null) {
                     mNavBarBackgroundWindow = win;
@@ -1729,9 +1564,11 @@
             }
         } else if (win.isDimming()) {
             if (mStatusBar != null) {
-                addStatusBarAppearanceRegionsForDimmingWindow(
+                if (addStatusBarAppearanceRegionsForDimmingWindow(
                         win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
-                        mStatusBar.getFrame(), win.getBounds(), win.getFrame());
+                        mStatusBar.getFrame(), win.getBounds(), win.getFrame())) {
+                    addSystemBarColorApp(win);
+                }
             }
             if (isOverlappingWithNavBar && mNavBarColorWindowCandidate == null) {
                 mNavBarColorWindowCandidate = win;
@@ -1739,18 +1576,21 @@
         }
     }
 
-    private void addStatusBarAppearanceRegionsForDimmingWindow(int appearance, Rect statusBarFrame,
-            Rect winBounds, Rect winFrame) {
+    /**
+     * Returns true if mStatusBarAppearanceRegionList is changed.
+     */
+    private boolean addStatusBarAppearanceRegionsForDimmingWindow(
+            int appearance, Rect statusBarFrame, Rect winBounds, Rect winFrame) {
         if (!sTmpRect.setIntersect(winBounds, statusBarFrame)) {
-            return;
+            return false;
         }
         if (mStatusBarColorCheckedBounds.contains(sTmpRect)) {
-            return;
+            return false;
         }
         if (appearance == 0 || !sTmpRect2.setIntersect(winFrame, statusBarFrame)) {
             mStatusBarAppearanceRegionList.add(new AppearanceRegion(0, new Rect(winBounds)));
             mStatusBarColorCheckedBounds.union(sTmpRect);
-            return;
+            return true;
         }
         // A dimming window can divide status bar into different appearance regions (up to 3).
         // +---------+-------------+---------+
@@ -1779,6 +1619,14 @@
             // We don't have vertical status bar yet, so we don't handle the other orientation.
         }
         mStatusBarColorCheckedBounds.union(sTmpRect);
+        return true;
+    }
+
+    private void addSystemBarColorApp(WindowState win) {
+        final ActivityRecord app = win.mActivityRecord;
+        if (app != null) {
+            mSystemBarColorApps.add(app);
+        }
     }
 
     /**
@@ -1812,7 +1660,16 @@
      */
     private void applyKeyguardPolicy(WindowState win, WindowState imeTarget) {
         if (win.canBeHiddenByKeyguard()) {
-            if (shouldBeHiddenByKeyguard(win, imeTarget)) {
+            final boolean shouldBeHiddenByKeyguard = shouldBeHiddenByKeyguard(win, imeTarget);
+            if (win.mIsImWindow) {
+                // Notify IME insets provider to freeze the IME insets. In case when turning off
+                // the screen, the IME insets source window will be hidden because of keyguard
+                // policy change and affects the system to freeze the last insets state. (And
+                // unfreeze when the IME is going to show)
+                mDisplayContent.getInsetsStateController().getImeSourceProvider().setFrozen(
+                        shouldBeHiddenByKeyguard);
+            }
+            if (shouldBeHiddenByKeyguard) {
                 win.hide(false /* doAnimation */, true /* requestAnim */);
             } else {
                 win.show(false /* doAnimation */, true /* requestAnim */);
@@ -1861,7 +1718,7 @@
         if (mTopFullscreenOpaqueWindowState == null || mForceShowSystemBars) {
             return false;
         }
-        return !mTopFullscreenOpaqueWindowState.getRequestedVisibility(ITYPE_STATUS_BAR);
+        return !mTopFullscreenOpaqueWindowState.isRequestedVisible(Type.statusBars());
     }
 
     /**
@@ -1892,27 +1749,12 @@
         final Resources res = getCurrentUserResources();
         final int portraitRotation = displayRotation.getPortraitRotation();
 
-        if (hasStatusBar()) {
-            mDisplayCutoutTouchableRegionSize = res.getDimensionPixelSize(
-                    R.dimen.display_cutout_touchable_region_size);
-        } else {
-            mDisplayCutoutTouchableRegionSize = 0;
-        }
-
         mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode);
         mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res);
         mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res);
-        mNavButtonForcedVisible =
-                mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
-        mNavigationBarLetsThroughTaps = res.getBoolean(R.bool.config_navBarTapThrough);
         mNavigationBarAlwaysShowOnSideGesture =
                 res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture);
 
-        // This should calculate how much above the frame we accept gestures.
-        mBottomGestureAdditionalInset =
-                res.getDimensionPixelSize(R.dimen.navigation_bar_gesture_height)
-                        - getNavigationBarFrameHeight(portraitRotation);
-
         updateConfigurationAndScreenSizeDependentBehaviors();
 
         final boolean shouldAttach =
@@ -2221,24 +2063,16 @@
             return;
         }
 
-        final @InsetsType int restorePositionTypes =
-                (controlTarget.getRequestedVisibility(ITYPE_NAVIGATION_BAR)
-                        ? Type.navigationBars() : 0)
-                | (controlTarget.getRequestedVisibility(ITYPE_STATUS_BAR)
-                        ? Type.statusBars() : 0)
-                | (mExtraNavBarAlt != null && controlTarget.getRequestedVisibility(
-                                ITYPE_EXTRA_NAVIGATION_BAR)
-                        ? Type.navigationBars() : 0)
-                | (mClimateBarAlt != null && controlTarget.getRequestedVisibility(
-                                ITYPE_CLIMATE_BAR)
-                        ? Type.statusBars() : 0);
+        final @InsetsType int restorePositionTypes = (Type.statusBars() | Type.navigationBars())
+                & controlTarget.getRequestedVisibleTypes();
 
         if (swipeTarget == mNavigationBar
                 && (restorePositionTypes & Type.navigationBars()) != 0) {
             // Don't show status bar when swiping on already visible navigation bar.
             // But restore the position of navigation bar if it has been moved by the control
             // target.
-            controlTarget.showInsets(Type.navigationBars(), false);
+            controlTarget.showInsets(Type.navigationBars(), false /* fromIme */,
+                    null /* statsToken */);
             return;
         }
 
@@ -2246,10 +2080,12 @@
             // Show transient bars if they are hidden; restore position if they are visible.
             mDisplayContent.getInsetsPolicy().showTransient(SHOW_TYPES_FOR_SWIPE,
                     isGestureOnSystemBar);
-            controlTarget.showInsets(restorePositionTypes, false);
+            controlTarget.showInsets(restorePositionTypes, false /* fromIme */,
+                    null /* statsToken */);
         } else {
             // Restore visibilities and positions of system bars.
-            controlTarget.showInsets(Type.statusBars() | Type.navigationBars(), false);
+            controlTarget.showInsets(Type.statusBars() | Type.navigationBars(),
+                    false /* fromIme */, null /* statsToken */);
             // To further allow the pull-down-from-the-top gesture to pull down the notification
             // shade as a consistent motion, we reroute the touch events here from the currently
             // touched window to the status bar after making it visible.
@@ -2275,6 +2111,25 @@
         return mDisplayContent.getInsetsPolicy();
     }
 
+    /**
+     * Called when an app has started replacing its main window.
+     */
+    void addRelaunchingApp(ActivityRecord app) {
+        if (mSystemBarColorApps.contains(app)) {
+            mRelaunchingSystemBarColorApps.add(app);
+        }
+    }
+
+    /**
+     * Called when an app has finished replacing its main window or aborted.
+     */
+    void removeRelaunchingApp(ActivityRecord app) {
+        final boolean removed = mRelaunchingSystemBarColorApps.remove(app);
+        if (removed & mRelaunchingSystemBarColorApps.isEmpty()) {
+            updateSystemBarAttributes();
+        }
+    }
+
     void resetSystemBarAttributes() {
         mLastDisableFlags = 0;
         updateSystemBarAttributes();
@@ -2317,6 +2172,11 @@
         final int displayId = getDisplayId();
         final int disableFlags = win.getDisableFlags();
         final int opaqueAppearance = updateSystemBarsLw(win, disableFlags);
+        if (!mRelaunchingSystemBarColorApps.isEmpty()) {
+            // The appearance of system bars might change while relaunching apps. We don't report
+            // the intermediate state to system UI. Otherwise, it might trigger redundant effects.
+            return;
+        }
         final WindowState navColorWin = chooseNavigationColorWindowLw(mNavBarColorWindowCandidate,
                 mDisplayContent.mInputMethodWindow, mNavigationBarPosition);
         final boolean isNavbarColorManagedByIme =
@@ -2325,8 +2185,8 @@
                 navColorWin) | opaqueAppearance;
         final int behavior = win.mAttrs.insetsFlags.behavior;
         final String focusedApp = win.mAttrs.packageName;
-        final boolean isFullscreen = !win.getRequestedVisibility(ITYPE_STATUS_BAR)
-                || !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
+        final boolean isFullscreen = !win.isRequestedVisible(Type.statusBars())
+                || !win.isRequestedVisible(Type.navigationBars());
         final AppearanceRegion[] statusBarAppearanceRegions =
                 new AppearanceRegion[mStatusBarAppearanceRegionList.size()];
         mStatusBarAppearanceRegionList.toArray(statusBarAppearanceRegions);
@@ -2336,11 +2196,12 @@
             callStatusBarSafely(statusBar -> statusBar.setDisableFlags(displayId, disableFlags,
                     cause));
         }
+        final @InsetsType int requestedVisibleTypes = win.getRequestedVisibleTypes();
         final LetterboxDetails[] letterboxDetails = new LetterboxDetails[mLetterboxDetails.size()];
         mLetterboxDetails.toArray(letterboxDetails);
         if (mLastAppearance == appearance
                 && mLastBehavior == behavior
-                && mRequestedVisibilities.equals(win.getRequestedVisibilities())
+                && mLastRequestedVisibleTypes == requestedVisibleTypes
                 && Objects.equals(mFocusedApp, focusedApp)
                 && mLastFocusIsFullscreen == isFullscreen
                 && Arrays.equals(mLastStatusBarAppearanceRegions, statusBarAppearanceRegions)
@@ -2352,18 +2213,16 @@
             mService.mInputManager.setSystemUiLightsOut(
                     isFullscreen || (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0);
         }
-        final InsetsVisibilities requestedVisibilities =
-                new InsetsVisibilities(win.getRequestedVisibilities());
         mLastAppearance = appearance;
         mLastBehavior = behavior;
-        mRequestedVisibilities = requestedVisibilities;
+        mLastRequestedVisibleTypes = requestedVisibleTypes;
         mFocusedApp = focusedApp;
         mLastFocusIsFullscreen = isFullscreen;
         mLastStatusBarAppearanceRegions = statusBarAppearanceRegions;
         mLastLetterboxDetails = letterboxDetails;
         callStatusBarSafely(statusBar -> statusBar.onSystemBarAttributesChanged(displayId,
                 appearance, statusBarAppearanceRegions, isNavbarColorManagedByIme, behavior,
-                requestedVisibilities, focusedApp, letterboxDetails));
+                requestedVisibleTypes, focusedApp, letterboxDetails));
     }
 
     private void callStatusBarSafely(Consumer<StatusBarManagerInternal> consumer) {
@@ -2456,7 +2315,7 @@
         appearance = configureNavBarOpacity(appearance, multiWindowTaskVisible,
                 freeformRootTaskVisible);
 
-        final boolean requestHideNavBar = !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR);
+        final boolean requestHideNavBar = !win.isRequestedVisible(Type.navigationBars());
         final long now = SystemClock.uptimeMillis();
         final boolean pendingPanic = mPendingPanicGestureUptime != 0
                 && now - mPendingPanicGestureUptime <= PANIC_GESTURE_EXPIRATION;
@@ -2780,6 +2639,14 @@
             pw.print(prefix); pw.print("mTopFullscreenOpaqueWindowState=");
             pw.println(mTopFullscreenOpaqueWindowState);
         }
+        if (!mSystemBarColorApps.isEmpty()) {
+            pw.print(prefix); pw.print("mSystemBarColorApps=");
+            pw.println(mSystemBarColorApps);
+        }
+        if (!mRelaunchingSystemBarColorApps.isEmpty()) {
+            pw.print(prefix); pw.print("mRelaunchingSystemBarColorApps=");
+            pw.println(mRelaunchingSystemBarColorApps);
+        }
         if (mNavBarColorWindowCandidate != null) {
             pw.print(prefix); pw.print("mNavBarColorWindowCandidate=");
             pw.println(mNavBarColorWindowCandidate);
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index a8d13c5..eaa08fd 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1578,7 +1578,9 @@
                         false /* forceRelayout */);
             } else {
                 // Revert the rotation to our saved value if we transition from HALF_FOLDED.
-                mRotation = mHalfFoldSavedRotation;
+                if (mHalfFoldSavedRotation != -1) {
+                    mRotation = mHalfFoldSavedRotation;
+                }
                 // Tell the device to update its orientation (mFoldState is still HALF_FOLDED here
                 // so we will override USER_ROTATION_LOCKED and allow a rotation).
                 mService.updateRotation(false /* alwaysSendConfiguration */,
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 5d49042..6f821b5 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -162,6 +162,17 @@
         return mDisplayWindowPolicyController.canShowTasksInRecents();
     }
 
+    /**
+     * @see DisplayWindowPolicyController#isEnteringPipAllowed(int)
+     */
+    public final boolean isEnteringPipAllowed(int uid) {
+        if (mDisplayWindowPolicyController == null) {
+            return true;
+        }
+        return mDisplayWindowPolicyController.isEnteringPipAllowed(uid);
+    }
+
+
     void dump(String prefix, PrintWriter pw) {
         if (mDisplayWindowPolicyController != null) {
             pw.println();
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index e0644b6..b735b30 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -442,11 +442,12 @@
                     mRemoveContentMode = other.mRemoveContentMode;
                     changed = true;
                 }
-                if (other.mShouldShowWithInsecureKeyguard != mShouldShowWithInsecureKeyguard) {
+                if (!Objects.equals(
+                        other.mShouldShowWithInsecureKeyguard, mShouldShowWithInsecureKeyguard)) {
                     mShouldShowWithInsecureKeyguard = other.mShouldShowWithInsecureKeyguard;
                     changed = true;
                 }
-                if (other.mShouldShowSystemDecors != mShouldShowSystemDecors) {
+                if (!Objects.equals(other.mShouldShowSystemDecors, mShouldShowSystemDecors)) {
                     mShouldShowSystemDecors = other.mShouldShowSystemDecors;
                     changed = true;
                 }
@@ -458,15 +459,15 @@
                     mFixedToUserRotation = other.mFixedToUserRotation;
                     changed = true;
                 }
-                if (other.mIgnoreOrientationRequest != mIgnoreOrientationRequest) {
+                if (!Objects.equals(other.mIgnoreOrientationRequest, mIgnoreOrientationRequest)) {
                     mIgnoreOrientationRequest = other.mIgnoreOrientationRequest;
                     changed = true;
                 }
-                if (other.mIgnoreDisplayCutout != mIgnoreDisplayCutout) {
+                if (!Objects.equals(other.mIgnoreDisplayCutout, mIgnoreDisplayCutout)) {
                     mIgnoreDisplayCutout = other.mIgnoreDisplayCutout;
                     changed = true;
                 }
-                if (other.mDontMoveToTop != mDontMoveToTop) {
+                if (!Objects.equals(other.mDontMoveToTop, mDontMoveToTop)) {
                     mDontMoveToTop = other.mDontMoveToTop;
                     changed = true;
                 }
@@ -522,14 +523,13 @@
                     mRemoveContentMode = delta.mRemoveContentMode;
                     changed = true;
                 }
-                if (delta.mShouldShowWithInsecureKeyguard != null
-                        && delta.mShouldShowWithInsecureKeyguard
-                        != mShouldShowWithInsecureKeyguard) {
+                if (delta.mShouldShowWithInsecureKeyguard != null && !Objects.equals(
+                        delta.mShouldShowWithInsecureKeyguard, mShouldShowWithInsecureKeyguard)) {
                     mShouldShowWithInsecureKeyguard = delta.mShouldShowWithInsecureKeyguard;
                     changed = true;
                 }
-                if (delta.mShouldShowSystemDecors != null
-                        && delta.mShouldShowSystemDecors != mShouldShowSystemDecors) {
+                if (delta.mShouldShowSystemDecors != null && !Objects.equals(
+                        delta.mShouldShowSystemDecors, mShouldShowSystemDecors)) {
                     mShouldShowSystemDecors = delta.mShouldShowSystemDecors;
                     changed = true;
                 }
@@ -543,18 +543,18 @@
                     mFixedToUserRotation = delta.mFixedToUserRotation;
                     changed = true;
                 }
-                if (delta.mIgnoreOrientationRequest != null
-                        && delta.mIgnoreOrientationRequest != mIgnoreOrientationRequest) {
+                if (delta.mIgnoreOrientationRequest != null && !Objects.equals(
+                        delta.mIgnoreOrientationRequest, mIgnoreOrientationRequest)) {
                     mIgnoreOrientationRequest = delta.mIgnoreOrientationRequest;
                     changed = true;
                 }
-                if (delta.mIgnoreDisplayCutout != null
-                        && delta.mIgnoreDisplayCutout != mIgnoreDisplayCutout) {
+                if (delta.mIgnoreDisplayCutout != null && !Objects.equals(
+                        delta.mIgnoreDisplayCutout, mIgnoreDisplayCutout)) {
                     mIgnoreDisplayCutout = delta.mIgnoreDisplayCutout;
                     changed = true;
                 }
-                if (delta.mDontMoveToTop != null
-                        && delta.mDontMoveToTop != mDontMoveToTop) {
+                if (delta.mDontMoveToTop != null && !Objects.equals(
+                        delta.mDontMoveToTop, mDontMoveToTop)) {
                     mDontMoveToTop = delta.mDontMoveToTop;
                     changed = true;
                 }
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 4a70fa3..1abb0a1 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -30,14 +30,14 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags
index 1e5a219..d94bf4b 100644
--- a/services/core/java/com/android/server/wm/EventLogTags.logtags
+++ b/services/core/java/com/android/server/wm/EventLogTags.logtags
@@ -8,11 +8,11 @@
 # An activity is being finished:
 30001 wm_finish_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Reason|3)
 # A task is being brought to the front of the screen:
-30002 wm_task_to_front (User|1|5),(Task|1|5)
+30002 wm_task_to_front (User|1|5),(Task|1|5),(Display Id|1|5)
 # An existing activity is being given a new intent:
 30003 wm_new_intent (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Action|3),(MIME Type|3),(URI|3),(Flags|1|5)
 # A new task is being created:
-30004 wm_create_task (User|1|5),(Task ID|1|5)
+30004 wm_create_task (User|1|5),(Task ID|1|5),(Root Task ID|1|5),(Display Id|1|5)
 # A new activity is being created in an existing task:
 30005 wm_create_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Action|3),(MIME Type|3),(URI|3),(Flags|1|5)
 # An activity has been resumed into the foreground but was not already running:
@@ -32,9 +32,9 @@
 # An activity is being destroyed:
 30018 wm_destroy_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(Reason|3)
 # An activity has been relaunched, resumed, and is now in the foreground:
-30019 wm_relaunch_resume_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3)
+30019 wm_relaunch_resume_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(config mask|3)
 # An activity has been relaunched:
-30020 wm_relaunch_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3)
+30020 wm_relaunch_activity (User|1|5),(Token|1|5),(Task ID|1|5),(Component Name|3),(config mask|3)
 
 # Activity set to resumed
 30043 wm_set_resumed_activity (User|1|5),(Component Name|3),(Reason|3)
@@ -45,9 +45,6 @@
 # Attempting to stop an activity
 30048 wm_stop_activity (User|1|5),(Token|1|5),(Component Name|3)
 
-# The task is being removed from its parent task
-30061 wm_remove_task (Task ID|1|5), (Root Task ID|1|5)
-
 # An activity been add into stopping list
 30066 wm_add_to_stopping (User|1|5),(Token|1|5),(Component Name|3),(Reason|3)
 
@@ -57,11 +54,11 @@
 # Out of memory for surfaces.
 31000 wm_no_surface_memory (Window|3),(PID|1|5),(Operation|3)
 # Task created.
-31001 wm_task_created (TaskId|1|5),(RootTaskId|1|5)
+31001 wm_task_created (TaskId|1|5)
 # Task moved to top (1) or bottom (0).
-31002 wm_task_moved (TaskId|1|5),(ToTop|1),(Index|1)
+31002 wm_task_moved (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(ToTop|1),(Index|1)
 # Task removed with source explanation.
-31003 wm_task_removed (TaskId|1|5),(Reason|3)
+31003 wm_task_removed (TaskId|1|5),(Root Task ID|1|5),(Display Id|1|5),(Reason|3)
 # bootanim finished:
 31007 wm_boot_animation_done (time|2|3)
 
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 14a1cd0..7fd093f 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -33,8 +33,11 @@
 import android.os.Trace;
 import android.util.proto.ProtoOutputStream;
 import android.view.InsetsSource;
+import android.view.InsetsSourceConsumer;
 import android.view.InsetsSourceControl;
+import android.view.InsetsState;
 import android.view.WindowInsets;
+import android.view.inputmethod.ImeTracker;
 import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -48,12 +51,21 @@
  */
 final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider {
 
+    /** The token tracking the current IME request or {@code null} otherwise. */
+    @Nullable
+    private ImeTracker.Token mImeRequesterStatsToken;
     private InsetsControlTarget mImeRequester;
     private Runnable mShowImeRunner;
     private boolean mIsImeLayoutDrawn;
     private boolean mImeShowing;
     private final InsetsSource mLastSource = new InsetsSource(ITYPE_IME);
 
+    /** @see #setFrozen(boolean) */
+    private boolean mFrozen;
+
+    /** @see #setServerVisible(boolean) */
+    private boolean mServerVisible;
+
     ImeInsetsSourceProvider(InsetsSource source,
             InsetsStateController stateController, DisplayContent displayContent) {
         super(source, stateController, displayContent);
@@ -80,6 +92,32 @@
     }
 
     @Override
+    void setServerVisible(boolean serverVisible) {
+        mServerVisible = serverVisible;
+        if (!mFrozen) {
+            super.setServerVisible(serverVisible);
+        }
+    }
+
+    /**
+     * Freeze IME insets source state when required.
+     *
+     * When setting {@param frozen} as {@code true}, the IME insets provider will freeze the
+     * current IME insets state and pending the IME insets state update until setting
+     * {@param frozen} as {@code false}.
+     */
+    void setFrozen(boolean frozen) {
+        if (mFrozen == frozen) {
+            return;
+        }
+        mFrozen = frozen;
+        if (!frozen) {
+            // Unfreeze and process the pending IME insets states.
+            super.setServerVisible(mServerVisible);
+        }
+    }
+
+    @Override
     void updateSourceFrame(Rect frame) {
         super.updateSourceFrame(frame);
         onSourceChanged();
@@ -104,7 +142,7 @@
     @Override
     protected boolean updateClientVisibility(InsetsControlTarget caller) {
         boolean changed = super.updateClientVisibility(caller);
-        if (changed && caller.getRequestedVisibility(mSource.getType())) {
+        if (changed && caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType()))) {
             reportImeDrawnForOrganizer(caller);
         }
         return changed;
@@ -129,14 +167,20 @@
     }
 
     /**
-     * Called from {@link WindowManagerInternal#showImePostLayout} when {@link InputMethodService}
-     * requests to show IME on {@param imeTarget}.
+     * Called from {@link WindowManagerInternal#showImePostLayout}
+     * when {@link android.inputmethodservice.InputMethodService} requests to show IME
+     * on {@param imeTarget}.
      *
-     * @param imeTarget imeTarget on which IME request is coming from.
+     * @param imeTarget imeTarget on which IME show request is coming from.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    void scheduleShowImePostLayout(InsetsControlTarget imeTarget) {
+    void scheduleShowImePostLayout(InsetsControlTarget imeTarget,
+            @Nullable ImeTracker.Token statsToken) {
         boolean targetChanged = isTargetChangedWithinActivity(imeTarget);
         mImeRequester = imeTarget;
+        // There was still a stats token, so that request presumably failed.
+        ImeTracker.get().onFailed(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+        mImeRequesterStatsToken = statsToken;
         if (targetChanged) {
             // target changed, check if new target can show IME.
             ProtoLog.d(WM_DEBUG_IME, "IME target changed within ActivityRecord");
@@ -150,15 +194,20 @@
         ProtoLog.d(WM_DEBUG_IME, "Schedule IME show for %s", mImeRequester.getWindow() == null
                 ? mImeRequester : mImeRequester.getWindow().getName());
         mShowImeRunner = () -> {
+            ImeTracker.get().onProgress(mImeRequesterStatsToken,
+                    ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
             ProtoLog.d(WM_DEBUG_IME, "Run showImeRunner");
             // Target should still be the same.
             if (isReadyToShowIme()) {
+                ImeTracker.get().onProgress(mImeRequesterStatsToken,
+                        ImeTracker.PHASE_WM_SHOW_IME_READY);
                 final InsetsControlTarget target = mDisplayContent.getImeTarget(IME_TARGET_CONTROL);
 
                 ProtoLog.i(WM_DEBUG_IME, "call showInsets(ime) on %s",
                         target.getWindow() != null ? target.getWindow().getName() : "");
                 setImeShowing(true);
-                target.showInsets(WindowInsets.Type.ime(), true /* fromIme */);
+                target.showInsets(WindowInsets.Type.ime(), true /* fromIme */,
+                        mImeRequesterStatsToken);
                 Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
                 if (target != mImeRequester && mImeRequester != null) {
                     ProtoLog.w(WM_DEBUG_IME,
@@ -166,7 +215,12 @@
                             (mImeRequester.getWindow() != null
                                     ? mImeRequester.getWindow().getName() : ""));
                 }
+            } else {
+                ImeTracker.get().onFailed(mImeRequesterStatsToken,
+                        ImeTracker.PHASE_WM_SHOW_IME_READY);
             }
+            // Clear token here so we don't report an error in abortShowImePostLayout().
+            mImeRequesterStatsToken = null;
             abortShowImePostLayout();
         };
         mDisplayContent.mWmService.requestTraversal();
@@ -201,6 +255,8 @@
         mImeRequester = null;
         mIsImeLayoutDrawn = false;
         mShowImeRunner = null;
+        ImeTracker.get().onCancelled(mImeRequesterStatsToken, ImeTracker.PHASE_WM_SHOW_IME_RUNNER);
+        mImeRequesterStatsToken = null;
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index 4c18d0b..56edde0 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -57,7 +57,6 @@
 import android.view.WindowInsets.Type;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
-import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.Button;
@@ -109,18 +108,13 @@
         mContext = display.getDisplayId() == DEFAULT_DISPLAY
                 ? uiContext : uiContext.createDisplayContext(display);
         mHandler = new H(looper);
-        mShowDelayMs = getNavBarExitDuration() * 3;
+        mShowDelayMs = context.getResources().getInteger(R.integer.dock_enter_exit_duration) * 3L;
         mPanicThresholdMs = context.getResources()
                 .getInteger(R.integer.config_immersive_mode_confirmation_panic);
         mVrModeEnabled = vrModeEnabled;
         mCanSystemBarsBeShownByUser = canSystemBarsBeShownByUser;
     }
 
-    private long getNavBarExitDuration() {
-        Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit);
-        return exit != null ? exit.getDuration() : 0;
-    }
-
     static boolean loadSetting(int currentUserId, Context context) {
         final boolean wasConfirmed = sConfirmed;
         sConfirmed = false;
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index 287dd74..8ecbc17 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -16,10 +16,11 @@
 
 package com.android.server.wm;
 
+import android.annotation.Nullable;
 import android.inputmethodservice.InputMethodService;
-import android.view.InsetsState;
-import android.view.InsetsState.InternalInsetsType;
+import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
+import android.view.inputmethod.ImeTracker;
 
 /**
  * Generalization of an object that can control insets state.
@@ -40,10 +41,17 @@
     }
 
     /**
-     * @return The requested visibility of this target.
+     * @return {@code true} if any of the {@link InsetsType} is requested visible by this target.
      */
-    default boolean getRequestedVisibility(@InternalInsetsType int type) {
-        return InsetsState.getDefaultVisibility(type);
+    default boolean isRequestedVisible(@InsetsType int types) {
+        return (WindowInsets.Type.defaultVisible() & types) != 0;
+    }
+
+    /**
+     * @return {@link InsetsType}s which are requested visible by this target.
+     */
+    default @InsetsType int getRequestedVisibleTypes() {
+        return WindowInsets.Type.defaultVisible();
     }
 
     /**
@@ -51,8 +59,10 @@
      *
      * @param types to specify which types of insets source window should be shown.
      * @param fromIme {@code true} if IME show request originated from {@link InputMethodService}.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    default void showInsets(@InsetsType int types, boolean fromIme) {
+    default void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     /**
@@ -60,8 +70,10 @@
      *
      * @param types to specify which types of insets source window should be hidden.
      * @param fromIme {@code true} if IME hide request originated from {@link InputMethodService}.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    default void hideInsets(@InsetsType int types, boolean fromIme) {
+    default void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 2de8faf..f66fa0f 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -177,8 +177,8 @@
                         : navControlTarget == notificationShade
                                 ? getNavControlTarget(topApp, true /* fake */)
                                 : null);
-        mStatusBar.updateVisibility(statusControlTarget, ITYPE_STATUS_BAR);
-        mNavBar.updateVisibility(navControlTarget, ITYPE_NAVIGATION_BAR);
+        mStatusBar.updateVisibility(statusControlTarget, Type.statusBars());
+        mNavBar.updateVisibility(navControlTarget, Type.navigationBars());
     }
 
     boolean isHidden(@InternalInsetsType int type) {
@@ -455,7 +455,7 @@
 
             if (originalImeSource != null) {
                 final boolean imeVisibility =
-                        w.mActivityRecord.mLastImeShown || w.getRequestedVisibility(ITYPE_IME);
+                        w.mActivityRecord.mLastImeShown || w.isRequestedVisible(Type.ime());
                 final InsetsState state = copyState ? new InsetsState(originalState)
                         : originalState;
                 final InsetsSource imeSource = new InsetsSource(originalImeSource);
@@ -501,11 +501,11 @@
     private void checkAbortTransient(InsetsControlTarget caller) {
         if (mShowingTransientTypes.size() != 0) {
             final IntArray abortTypes = new IntArray();
-            final boolean imeRequestedVisible = caller.getRequestedVisibility(ITYPE_IME);
+            final boolean imeRequestedVisible = caller.isRequestedVisible(Type.ime());
             for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
                 final @InternalInsetsType int type = mShowingTransientTypes.get(i);
                 if ((mStateController.isFakeTarget(type, caller)
-                                && caller.getRequestedVisibility(type))
+                                && caller.isRequestedVisible(InsetsState.toPublicType(type)))
                         || (type == ITYPE_NAVIGATION_BAR && imeRequestedVisible)) {
                     mShowingTransientTypes.remove(i);
                     abortTypes.add(type);
@@ -552,7 +552,7 @@
             ComponentName component = focusedWin.mActivityRecord != null
                     ? focusedWin.mActivityRecord.mActivityComponent : null;
             mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged(
-                    component, focusedWin.getRequestedVisibilities());
+                    component, focusedWin.getRequestedVisibleTypes());
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
         if (mPolicy.areSystemBarsForcedShownLw()) {
@@ -612,7 +612,7 @@
             ComponentName component = focusedWin.mActivityRecord != null
                     ? focusedWin.mActivityRecord.mActivityComponent : null;
             mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged(
-                    component, focusedWin.getRequestedVisibilities());
+                    component, focusedWin.getRequestedVisibleTypes());
             return mDisplayContent.mRemoteInsetsControlTarget;
         }
         if (mPolicy.areSystemBarsForcedShownLw()) {
@@ -734,8 +734,8 @@
         }
 
         private void updateVisibility(@Nullable InsetsControlTarget controlTarget,
-                @InternalInsetsType int type) {
-            setVisible(controlTarget == null || controlTarget.getRequestedVisibility(type));
+                @Type.InsetsType int type) {
+            setVisible(controlTarget == null || controlTarget.isRequestedVisible(type));
         }
 
         private void setVisible(boolean visible) {
@@ -799,7 +799,7 @@
                         show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show
                                 ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN
                                 : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN,
-                        null /* translator */);
+                        null /* translator */, null /* statsToken */);
                 SurfaceAnimationThread.getHandler().post(
                         () -> mListener.onReady(mAnimationControl, typesReady));
             }
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index bf4b65d..5b205f0 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -48,6 +48,7 @@
 import android.view.InsetsState;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -173,6 +174,7 @@
         mWindowContainer = windowContainer;
         // TODO: remove the frame provider for non-WindowState container.
         mFrameProvider = frameProvider;
+        mOverrideFrames.clear();
         mOverrideFrameProviders = overrideFrameProviders;
         if (windowContainer == null) {
             setServerVisible(false);
@@ -234,6 +236,8 @@
         updateSourceFrameForServerVisibility();
 
         if (mOverrideFrameProviders != null) {
+            // Not necessary to clear the mOverrideFrames here. It will be cleared every time the
+            // override frame provider updates.
             for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) {
                 final int windowType = mOverrideFrameProviders.keyAt(i);
                 final Rect overrideFrame;
@@ -455,8 +459,9 @@
         }
         final Point surfacePosition = getWindowFrameSurfacePosition();
         mAdapter = new ControlAdapter(surfacePosition);
-        if (getSource().getType() == ITYPE_IME) {
-            setClientVisible(target.getRequestedVisibility(mSource.getType()));
+        final int type = getSource().getType();
+        if (type == ITYPE_IME) {
+            setClientVisible(target.isRequestedVisible(WindowInsets.Type.ime()));
         }
         final Transaction t = mDisplayContent.getSyncTransaction();
         mWindowContainer.startAnimation(t, mAdapter, !mClientVisible /* hidden */,
@@ -469,8 +474,8 @@
         final SurfaceControl leash = mAdapter.mCapturedLeash;
         mControlTarget = target;
         updateVisibility();
-        mControl = new InsetsSourceControl(mSource.getType(), leash, mClientVisible,
-                surfacePosition, mInsetsHint);
+        mControl = new InsetsSourceControl(type, leash, mClientVisible, surfacePosition,
+                mInsetsHint);
 
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
@@ -488,7 +493,8 @@
     }
 
     boolean updateClientVisibility(InsetsControlTarget caller) {
-        final boolean requestedVisible = caller.getRequestedVisibility(mSource.getType());
+        final boolean requestedVisible =
+                caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType()));
         if (caller != mControlTarget || requestedVisible == mClientVisible) {
             return false;
         }
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index be3ceb8..bf511adf0 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -27,12 +27,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.PackageList;
 import com.android.server.wm.LaunchParamsController.LaunchParams;
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index a469c6b..c19353c 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -17,14 +17,17 @@
 package com.android.server.wm;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.function.Function;
 
 /** Reads letterbox configs from resources and controls their overrides at runtime. */
 final class LetterboxConfiguration {
@@ -156,34 +159,25 @@
     // portrait device orientation.
     private boolean mIsVerticalReachabilityEnabled;
 
-
-    // Horizontal position of a center of the letterboxed app window which is global to prevent
-    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
-    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
-    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
-    // ActivityRecord#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxHorizontalReachabilityPosition
-    private volatile int mLetterboxPositionForHorizontalReachability;
-
-    // Vertical position of a center of the letterboxed app window which is global to prevent
-    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
-    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
-    // LetterboxUiController#getVerticalPositionMultiplier which is called from
-    // ActivityRecord#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxVerticalReachabilityPosition
-    private volatile int mLetterboxPositionForVerticalReachability;
-
     // Whether education is allowed for letterboxed fullscreen apps.
     private boolean mIsEducationEnabled;
 
     // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
     private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
 
+    // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
+    @NonNull
+    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
     LetterboxConfiguration(Context systemUiContext) {
+        this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+    }
+
+    @VisibleForTesting
+    LetterboxConfiguration(Context systemUiContext,
+            LetterboxConfigurationPersister letterboxConfigurationPersister) {
         mContext = systemUiContext;
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 R.dimen.config_fixedOrientationLetterboxAspectRatio);
@@ -206,14 +200,14 @@
                 readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
         mDefaultPositionForVerticalReachability =
                 readLetterboxVerticalReachabilityPositionFromConfig(mContext);
-        mLetterboxPositionForHorizontalReachability = mDefaultPositionForHorizontalReachability;
-        mLetterboxPositionForVerticalReachability = mDefaultPositionForVerticalReachability;
         mIsEducationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEducationEnabled);
         setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
                 R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
         mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+        mLetterboxConfigurationPersister = letterboxConfigurationPersister;
+        mLetterboxConfigurationPersister.start();
     }
 
     /**
@@ -653,7 +647,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getHorizontalMultiplierForReachability() {
-        switch (mLetterboxPositionForHorizontalReachability) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        switch (letterboxPositionForHorizontalReachability) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
                 return 0.0f;
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
@@ -662,10 +658,11 @@
                 return 1.0f;
             default:
                 throw new AssertionError(
-                    "Unexpected letterbox position type: "
-                            + mLetterboxPositionForHorizontalReachability);
+                        "Unexpected letterbox position type: "
+                                + letterboxPositionForHorizontalReachability);
         }
     }
+
     /*
      * Gets vertical position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side.
@@ -673,7 +670,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getVerticalMultiplierForReachability() {
-        switch (mLetterboxPositionForVerticalReachability) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        switch (letterboxPositionForVerticalReachability) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
                 return 0.0f;
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
@@ -683,7 +682,7 @@
             default:
                 throw new AssertionError(
                         "Unexpected letterbox position type: "
-                                + mLetterboxPositionForVerticalReachability);
+                                + letterboxPositionForVerticalReachability);
         }
     }
 
@@ -693,7 +692,7 @@
      */
     @LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability() {
-        return mLetterboxPositionForHorizontalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
     }
 
     /*
@@ -702,7 +701,7 @@
      */
     @LetterboxVerticalReachabilityPosition
     int getLetterboxPositionForVerticalReachability() {
-        return mLetterboxPositionForVerticalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
     }
 
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -742,9 +741,8 @@
      * right side.
      */
     void movePositionForHorizontalReachabilityToNextRightStop() {
-        mLetterboxPositionForHorizontalReachability = Math.min(
-                mLetterboxPositionForHorizontalReachability + 1,
-                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT);
+        updatePositionForHorizontalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
     }
 
     /**
@@ -752,8 +750,7 @@
      * side.
      */
     void movePositionForHorizontalReachabilityToNextLeftStop() {
-        mLetterboxPositionForHorizontalReachability =
-                Math.max(mLetterboxPositionForHorizontalReachability - 1, 0);
+        updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -761,9 +758,8 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextBottomStop() {
-        mLetterboxPositionForVerticalReachability = Math.min(
-                mLetterboxPositionForVerticalReachability + 1,
-                LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM);
+        updatePositionForVerticalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
     }
 
     /**
@@ -771,8 +767,7 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextTopStop() {
-        mLetterboxPositionForVerticalReachability =
-                Math.max(mLetterboxPositionForVerticalReachability - 1, 0);
+        updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -822,4 +817,26 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
     }
 
+    /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
+    private void updatePositionForHorizontalReachability(
+            Function<Integer, Integer> newHorizonalPositionFun) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int nextHorizontalPosition = newHorizonalPositionFun.apply(
+                letterboxPositionForHorizontalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                nextHorizontalPosition);
+    }
+
+    /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
+    private void updatePositionForVerticalReachability(
+            Function<Integer, Integer> newVerticalPositionFun) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int nextVerticalPosition = newVerticalPositionFun.apply(
+                letterboxPositionForVerticalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                nextVerticalPosition);
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
new file mode 100644
index 0000000..70639b1
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Environment;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
+import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition;
+import com.android.server.wm.nano.WindowManagerProtos;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Persists the values of letterboxPositionForHorizontalReachability and
+ * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}.
+ */
+class LetterboxConfigurationPersister {
+
+    private static final String TAG =
+            TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
+
+    @VisibleForTesting
+    static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
+
+    private final Context mContext;
+    private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
+    private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+
+    // Horizontal position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsPosition.
+    @LetterboxHorizontalReachabilityPosition
+    private volatile int mLetterboxPositionForHorizontalReachability;
+
+    // Vertical position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getVerticalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsPosition.
+    @LetterboxVerticalReachabilityPosition
+    private volatile int mLetterboxPositionForVerticalReachability;
+
+    @NonNull
+    private final AtomicFile mConfigurationFile;
+
+    @Nullable
+    private final Consumer<String> mCompletionCallback;
+
+    @NonNull
+    private final PersisterQueue mPersisterQueue;
+
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier) {
+        this(systemUiContext, defaultHorizontalReachabilitySupplier,
+                defaultVerticalReachabilitySupplier,
+                Environment.getDataSystemDirectory(), new PersisterQueue(),
+                /* completionCallback */ null);
+    }
+
+    @VisibleForTesting
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+            PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
+        mContext = systemUiContext.createDeviceProtectedStorageContext();
+        mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
+        mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+        mCompletionCallback = completionCallback;
+        final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
+        mConfigurationFile = new AtomicFile(prefFiles);
+        mPersisterQueue = persisterQueue;
+        readCurrentConfiguration();
+    }
+
+    /**
+     * Startes the persistence queue
+     */
+    void start() {
+        mPersisterQueue.startPersisting();
+    }
+
+    /*
+     * Gets the horizontal position of the letterboxed app window when horizontal reachability is
+     * enabled.
+     */
+    @LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        return mLetterboxPositionForHorizontalReachability;
+    }
+
+    /*
+     * Gets the vertical position of the letterboxed app window when vertical reachability is
+     * enabled.
+     */
+    @LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        return mLetterboxPositionForVerticalReachability;
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForHorizontalReachability(
+            int letterboxPositionForHorizontalReachability) {
+        if (mLetterboxPositionForHorizontalReachability
+                != letterboxPositionForHorizontalReachability) {
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxPositionForHorizontalReachability;
+            updateConfiguration();
+        }
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForVerticalReachability(
+            int letterboxPositionForVerticalReachability) {
+        if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
+            mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
+            updateConfiguration();
+        }
+    }
+
+    @VisibleForTesting
+    void useDefaultValue() {
+        mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
+        mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+    }
+
+    private void readCurrentConfiguration() {
+        FileInputStream fis = null;
+        try {
+            fis = mConfigurationFile.openRead();
+            byte[] protoData = readInputStream(fis);
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    WindowManagerProtos.LetterboxProto.parseFrom(protoData);
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxData.letterboxPositionForHorizontalReachability;
+            mLetterboxPositionForVerticalReachability =
+                    letterboxData.letterboxPositionForVerticalReachability;
+        } catch (IOException ioe) {
+            Slog.e(TAG,
+                    "Error reading from LetterboxConfigurationPersister. "
+                            + "Using default values!", ioe);
+            useDefaultValue();
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    useDefaultValue();
+                    Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e);
+                }
+            }
+        }
+    }
+
+    private void updateConfiguration() {
+        mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
+                mLetterboxPositionForHorizontalReachability,
+                mLetterboxPositionForVerticalReachability,
+                mCompletionCallback), /* flush */ true);
+    }
+
+    private static byte[] readInputStream(InputStream in) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[1024];
+            int size = in.read(buffer);
+            while (size > 0) {
+                outputStream.write(buffer, 0, size);
+                size = in.read(buffer);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            outputStream.close();
+        }
+    }
+
+    private static class UpdateValuesCommand implements
+            PersisterQueue.WriteQueueItem<UpdateValuesCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToUpdate;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+
+        private final int mHorizontalReachability;
+        private final int mVerticalReachability;
+
+        UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
+                int horizontalReachability, int verticalReachability,
+                @Nullable Consumer<String> onComplete) {
+            mFileToUpdate = fileToUpdate;
+            mHorizontalReachability = horizontalReachability;
+            mVerticalReachability = verticalReachability;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    new WindowManagerProtos.LetterboxProto();
+            letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
+            letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+            final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
+
+            FileOutputStream fos = null;
+            try {
+                fos = mFileToUpdate.startWrite();
+                fos.write(bytes);
+                mFileToUpdate.finishWrite(fos);
+            } catch (IOException ioe) {
+                mFileToUpdate.failWrite(fos);
+                Slog.e(TAG,
+                        "Error writing to LetterboxConfigurationPersister. "
+                                + "Using default values!", ioe);
+            } finally {
+                if (mOnComplete != null) {
+                    mOnComplete.accept("UpdateValuesCommand");
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index ea82417..2dbccae 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -501,12 +501,16 @@
 
             if (hasVisibleTaskbar(mainWindow)) {
                 cropBounds = new Rect(mActivityRecord.getBounds());
+
+                // Rounded corners should be displayed above the taskbar.
+                // It is important to call adjustBoundsForTaskbarUnchecked before offsetTo
+                // because taskbar bounds are in screen coordinates
+                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
+
                 // Activity bounds are in screen coordinates while (0,0) for activity's surface
                 // control is at the top left corner of an app window so offsetting bounds
                 // accordingly.
                 cropBounds.offsetTo(0, 0);
-                // Rounded corners should be displayed above the taskbar.
-                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
             }
 
             transaction
@@ -576,9 +580,8 @@
         // Rounded corners should be displayed above the taskbar.
         bounds.bottom =
                 Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
-        if (mActivityRecord.inSizeCompatMode()
-                && mActivityRecord.getSizeCompatScale() < 1.0f) {
-            bounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
+        if (mActivityRecord.inSizeCompatMode() && mActivityRecord.getCompatScale() < 1.0f) {
+            bounds.scale(1.0f / mActivityRecord.getCompatScale());
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java
index f11c2a7..dcb7fe3 100644
--- a/services/core/java/com/android/server/wm/LockTaskController.java
+++ b/services/core/java/com/android/server/wm/LockTaskController.java
@@ -604,7 +604,10 @@
                 getDevicePolicyManager().notifyLockTaskModeChanged(false, null, userId);
             }
             if (oldLockTaskModeState == LOCK_TASK_MODE_PINNED) {
-                getStatusBarService().showPinningEnterExitToast(false /* entering */);
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEnterExitToast(false /* entering */);
+                }
             }
             mWindowManager.onLockTaskStateChanged(mLockTaskModeState);
         } catch (RemoteException ex) {
@@ -619,7 +622,10 @@
     void showLockTaskToast() {
         if (mLockTaskModeState == LOCK_TASK_MODE_PINNED) {
             try {
-                getStatusBarService().showPinningEscapeToast();
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEscapeToast();
+                }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to send pinning escape toast", e);
             }
@@ -727,7 +733,10 @@
         // When lock task starts, we disable the status bars.
         try {
             if (lockTaskModeState == LOCK_TASK_MODE_PINNED) {
-                getStatusBarService().showPinningEnterExitToast(true /* entering */);
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEnterExitToast(true /* entering */);
+                }
             }
             mWindowManager.onLockTaskStateChanged(lockTaskModeState);
             mLockTaskModeState = lockTaskModeState;
diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java
index 16f4377..18a7d2e 100644
--- a/services/core/java/com/android/server/wm/PackageConfigPersister.java
+++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java
@@ -23,12 +23,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index f3713eb..de42c55 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -16,15 +16,21 @@
 
 package com.android.server.wm;
 
+import static android.hardware.display.DisplayManager.SWITCHING_TYPE_NONE;
+import static android.hardware.display.DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY;
+
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
+import android.hardware.display.DisplayManager;
 import android.view.Display;
 import android.view.Display.Mode;
 import android.view.DisplayInfo;
+import android.view.Surface;
+import android.view.SurfaceControl.RefreshRateRange;
 
 import java.util.HashMap;
+import java.util.Objects;
 
 /**
  * Policy to select a lower refresh rate for the display if applicable.
@@ -154,39 +160,109 @@
         return LAYER_PRIORITY_UNSET;
     }
 
-    float getPreferredRefreshRate(WindowState w) {
+    public static class FrameRateVote {
+        float mRefreshRate;
+        @Surface.FrameRateCompatibility int mCompatibility;
+
+        FrameRateVote(float refreshRate, @Surface.FrameRateCompatibility int compatibility) {
+            update(refreshRate, compatibility);
+        }
+
+        FrameRateVote() {
+            reset();
+        }
+
+        boolean update(float refreshRate, @Surface.FrameRateCompatibility int compatibility) {
+            if (!refreshRateEquals(refreshRate) || mCompatibility != compatibility) {
+                mRefreshRate = refreshRate;
+                mCompatibility = compatibility;
+                return true;
+            }
+            return false;
+        }
+
+        boolean reset() {
+            return update(0, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof FrameRateVote)) {
+                return false;
+            }
+
+            FrameRateVote other = (FrameRateVote) o;
+            return refreshRateEquals(other.mRefreshRate)
+                    && mCompatibility == other.mCompatibility;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mRefreshRate, mCompatibility);
+        }
+
+        @Override
+        public String toString() {
+            return "mRefreshRate=" + mRefreshRate + ", mCompatibility=" + mCompatibility;
+        }
+
+        private boolean refreshRateEquals(float refreshRate) {
+            return mRefreshRate <= refreshRate + RefreshRateRange.FLOAT_TOLERANCE
+                    && mRefreshRate >= refreshRate - RefreshRateRange.FLOAT_TOLERANCE;
+        }
+    }
+
+    boolean updateFrameRateVote(WindowState w) {
+        @DisplayManager.SwitchingType int refreshRateSwitchingType =
+                mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType();
+
+        // If refresh rate switching is disabled there is no point to set the frame rate on the
+        // surface as the refresh rate will be limited by display manager to a single value
+        // and SurfaceFlinger wouldn't be able to change it anyways.
+        if (refreshRateSwitchingType == SWITCHING_TYPE_NONE) {
+            return w.mFrameRateVote.reset();
+        }
+
         // If app is animating, it's not able to control refresh rate because we want the animation
         // to run in default refresh rate.
         if (w.isAnimating(TRANSITION | PARENTS)) {
-            return 0;
+            return w.mFrameRateVote.reset();
         }
 
         // If the app set a preferredDisplayModeId, the preferred refresh rate is the refresh rate
         // of that mode id.
-        final int preferredModeId = w.mAttrs.preferredDisplayModeId;
-        if (preferredModeId > 0) {
-            DisplayInfo info = w.getDisplayInfo();
-            if (info != null) {
-                for (Display.Mode mode : info.supportedModes) {
-                    if (preferredModeId == mode.getModeId()) {
-                        return mode.getRefreshRate();
+        if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
+            final int preferredModeId = w.mAttrs.preferredDisplayModeId;
+            if (preferredModeId > 0) {
+                DisplayInfo info = w.getDisplayInfo();
+                if (info != null) {
+                    for (Display.Mode mode : info.supportedModes) {
+                        if (preferredModeId == mode.getModeId()) {
+                            return w.mFrameRateVote.update(mode.getRefreshRate(),
+                                    Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+
+                        }
                     }
                 }
             }
         }
 
         if (w.mAttrs.preferredRefreshRate > 0) {
-            return w.mAttrs.preferredRefreshRate;
+            return w.mFrameRateVote.update(w.mAttrs.preferredRefreshRate,
+                    Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
         }
 
         // If the app didn't set a preferred mode id or refresh rate, but it is part of the deny
         // list, we return the low refresh rate as the preferred one.
-        final String packageName = w.getOwningPackage();
-        if (mHighRefreshRateDenylist.isDenylisted(packageName)) {
-            return mLowRefreshRateMode.getRefreshRate();
+        if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
+            final String packageName = w.getOwningPackage();
+            if (mHighRefreshRateDenylist.isDenylisted(packageName)) {
+                return w.mFrameRateVote.update(mLowRefreshRateMode.getRefreshRate(),
+                        Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+            }
         }
 
-        return 0;
+        return w.mFrameRateVote.reset();
     }
 
     float getPreferredMinRefreshRate(WindowState w) {
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index 8db5289..d34e610 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -323,11 +323,11 @@
                 mService.closeSurfaceTransaction("RemoteAnimationController#finished");
                 mIsFinishing = false;
             }
+            // Reset input for all activities when the remote animation is finished.
+            final Consumer<ActivityRecord> updateActivities =
+                    activity -> activity.setDropInputForAnimation(false);
+            mDisplayContent.forAllActivities(updateActivities);
         }
-        // Reset input for all activities when the remote animation is finished.
-        final Consumer<ActivityRecord> updateActivities =
-                activity -> activity.setDropInputForAnimation(false);
-        mDisplayContent.forAllActivities(updateActivities);
         setRunningRemoteAnimation(false);
         ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation");
     }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index d8b5d78..0ed4835 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -840,11 +840,8 @@
         if (recentsAnimationController != null) {
             recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController);
         }
-        final BackNavigationController backNavigationController =
-                mWmService.mAtmService.mBackNavigationController;
-        if (backNavigationController != null) {
-            backNavigationController.checkAnimationReady(defaultDisplay.mWallpaperController);
-        }
+        mWmService.mAtmService.mBackNavigationController
+                .checkAnimationReady(defaultDisplay.mWallpaperController);
 
         for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java
index 9c85bc0..1cc1a57 100644
--- a/services/core/java/com/android/server/wm/RunningTasks.java
+++ b/services/core/java/com/android/server/wm/RunningTasks.java
@@ -20,13 +20,12 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.ArraySet;
 
-import java.util.Comparator;
-import java.util.Iterator;
+import java.util.ArrayList;
 import java.util.List;
-import java.util.TreeSet;
 import java.util.function.Consumer;
 
 /**
@@ -39,15 +38,13 @@
     static final int FLAG_CROSS_USERS = 1 << 2;
     static final int FLAG_KEEP_INTENT_EXTRA = 1 << 3;
 
-    // Comparator to sort by last active time (descending)
-    private static final Comparator<Task> LAST_ACTIVE_TIME_COMPARATOR =
-            (o1, o2) -> {
-                return o1.lastActiveTime == o2.lastActiveTime
-                        ? Integer.signum(o2.mTaskId - o1.mTaskId) :
-                        Long.signum(o2.lastActiveTime - o1.lastActiveTime);
-            };
-
-    private final TreeSet<Task> mTmpSortedSet = new TreeSet<>(LAST_ACTIVE_TIME_COMPARATOR);
+    // Tasks are sorted in order {focusedVisibleTasks, visibleTasks, invisibleTasks}.
+    private final ArrayList<Task> mTmpSortedTasks = new ArrayList<>();
+    // mTmpVisibleTasks, mTmpInvisibleTasks and mTmpFocusedTasks are sorted from top
+    // to bottom.
+    private final ArrayList<Task> mTmpVisibleTasks = new ArrayList<>();
+    private final ArrayList<Task> mTmpInvisibleTasks = new ArrayList<>();
+    private final ArrayList<Task> mTmpFocusedTasks = new ArrayList<>();
 
     private int mCallingUid;
     private int mUserId;
@@ -65,8 +62,6 @@
             return;
         }
 
-        // Gather all of the tasks across all of the tasks, and add them to the sorted set
-        mTmpSortedSet.clear();
         mCallingUid = callingUid;
         mUserId = UserHandle.getUserId(callingUid);
         mCrossUser = (flags & FLAG_CROSS_USERS) == FLAG_CROSS_USERS;
@@ -77,19 +72,64 @@
         mRecentTasks = recentTasks;
         mKeepIntentExtra = (flags & FLAG_KEEP_INTENT_EXTRA) == FLAG_KEEP_INTENT_EXTRA;
 
-        root.forAllLeafTasks(this, false /* traverseTopToBottom */);
+        if (root instanceof RootWindowContainer) {
+            ((RootWindowContainer) root).forAllDisplays(dc -> {
+                final Task focusedTask = dc.mFocusedApp != null ? dc.mFocusedApp.getTask() : null;
+                if (focusedTask != null) {
+                    mTmpFocusedTasks.add(focusedTask);
+                }
+                processTaskInWindowContainer(dc);
+            });
+        } else {
+            final DisplayContent dc = root.getDisplayContent();
+            final Task focusedTask = dc != null
+                    ? (dc.mFocusedApp != null ? dc.mFocusedApp.getTask() : null)
+                    : null;
+            // May not be include focusedTask if root is DisplayArea.
+            final boolean rootContainsFocusedTask = focusedTask != null
+                    && focusedTask.isDescendantOf(root);
+            if (rootContainsFocusedTask) {
+                mTmpFocusedTasks.add(focusedTask);
+            }
+            processTaskInWindowContainer(root);
+        }
+
+        final int visibleTaskCount = mTmpVisibleTasks.size();
+        for (int i = 0; i < mTmpFocusedTasks.size(); i++) {
+            final Task focusedTask = mTmpFocusedTasks.get(i);
+            final boolean containsFocusedTask = mTmpVisibleTasks.remove(focusedTask);
+            if (containsFocusedTask) {
+                // Put the visible focused task at the first position.
+                mTmpSortedTasks.add(focusedTask);
+            }
+        }
+        if (!mTmpVisibleTasks.isEmpty()) {
+            mTmpSortedTasks.addAll(mTmpVisibleTasks);
+        }
+        if (!mTmpInvisibleTasks.isEmpty()) {
+            mTmpSortedTasks.addAll(mTmpInvisibleTasks);
+        }
 
         // Take the first {@param maxNum} tasks and create running task infos for them
-        final Iterator<Task> iter = mTmpSortedSet.iterator();
-        while (iter.hasNext()) {
-            if (maxNum == 0) {
-                break;
-            }
-
-            final Task task = iter.next();
-            list.add(createRunningTaskInfo(task));
-            maxNum--;
+        final int size = Math.min(maxNum, mTmpSortedTasks.size());
+        final long now = SystemClock.elapsedRealtime();
+        for (int i = 0; i < size; i++) {
+            final Task task = mTmpSortedTasks.get(i);
+            // Override the last active to current time for the visible tasks because the visible
+            // tasks can be considered to be currently active, the values are descending as
+            // the item order.
+            final long visibleActiveTime = i < visibleTaskCount ? now + size - i : -1;
+            list.add(createRunningTaskInfo(task, visibleActiveTime));
         }
+
+        mTmpFocusedTasks.clear();
+        mTmpVisibleTasks.clear();
+        mTmpInvisibleTasks.clear();
+        mTmpSortedTasks.clear();
+    }
+
+    private void processTaskInWindowContainer(WindowContainer wc) {
+        wc.forAllLeafTasks(this, true /* traverseTopToBottom */);
     }
 
     @Override
@@ -117,25 +157,20 @@
             // home & recent tasks
             return;
         }
-
         if (task.isVisible()) {
-            // For the visible task, update the last active time so that it can be used to determine
-            // the order of the tasks (it may not be set for newly created tasks)
-            task.touchActiveTime();
-            if (!task.isFocused()) {
-                // TreeSet doesn't allow the same value and make sure this task is lower than the
-                // focused one.
-                task.lastActiveTime -= mTmpSortedSet.size();
-            }
+            mTmpVisibleTasks.add(task);
+        } else {
+            mTmpInvisibleTasks.add(task);
         }
-
-        mTmpSortedSet.add(task);
     }
 
     /** Constructs a {@link RunningTaskInfo} from a given {@param task}. */
-    private RunningTaskInfo createRunningTaskInfo(Task task) {
+    private RunningTaskInfo createRunningTaskInfo(Task task, long visibleActiveTime) {
         final RunningTaskInfo rti = new RunningTaskInfo();
         task.fillTaskInfo(rti, !mKeepIntentExtra);
+        if (visibleActiveTime > 0) {
+            rti.lastActiveTime = visibleActiveTime;
+        }
         // Fill in some deprecated values
         rti.id = rti.taskId;
 
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5505539..449e77f 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -55,6 +55,7 @@
 import android.window.ScreenCapture;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.display.DisplayControl;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -246,7 +247,7 @@
             HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "ScreenRotationAnimation#getMedianBorderLuma");
-            mStartLuma = RotationAnimationUtils.getMedianBorderLuma(hardwareBuffer,
+            mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
                     screenshotBuffer.getColorSpace());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
 
@@ -489,8 +490,8 @@
             return false;
         }
         if (!mStarted) {
-            mEndLuma = RotationAnimationUtils.getLumaOfSurfaceControl(mDisplayContent.getDisplay(),
-                    mDisplayContent.getWindowingLayer());
+            mEndLuma = TransitionAnimation.getBorderLuma(mDisplayContent.getWindowingLayer(),
+                    finalWidth, finalHeight);
             startAnimation(t, maxAnimationDuration, animationScale, finalWidth, finalHeight,
                     exitAnim, enterAnim);
         }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 9660fe2..b482181 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -69,10 +69,11 @@
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
@@ -117,7 +118,6 @@
     private float mLastReportedAnimatorScale;
     private String mPackageName;
     private String mRelayoutTag;
-    private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities();
     private final InsetsSourceControl[] mDummyControls =  new InsetsSourceControl[0];
     final boolean mSetsUnrestrictedKeepClearAreas;
 
@@ -196,23 +196,23 @@
 
     @Override
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
+            int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
-                UserHandle.getUserId(mUid), requestedVisibilities, outInputChannel, outInsetsState,
+                UserHandle.getUserId(mUid), requestedVisibleTypes, outInputChannel, outInsetsState,
                 outActiveControls, outAttachedFrame, outSizeCompatScale);
     }
 
     @Override
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
+            int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
-                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls,
+                requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,
                 outAttachedFrame, outSizeCompatScale);
     }
 
@@ -221,8 +221,9 @@
             int viewVisibility, int displayId, InsetsState outInsetsState, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
-                UserHandle.getUserId(mUid), mDummyRequestedVisibilities, null /* outInputChannel */,
-                outInsetsState, mDummyControls, outAttachedFrame, outSizeCompatScale);
+                UserHandle.getUserId(mUid), WindowInsets.Type.defaultVisible(),
+                null /* outInputChannel */, outInsetsState, mDummyControls, outAttachedFrame,
+                outSizeCompatScale);
     }
 
     @Override
@@ -683,12 +684,12 @@
     }
 
     @Override
-    public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities) {
+    public void updateRequestedVisibleTypes(IWindow window, @InsetsType int requestedVisibleTypes) {
         synchronized (mService.mGlobalLock) {
             final WindowState windowState = mService.windowForClientLocked(this, window,
                     false /* throwOnError */);
             if (windowState != null) {
-                windowState.setRequestedVisibilities(visibilities);
+                windowState.setRequestedVisibleTypes(requestedVisibleTypes);
                 windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState);
             }
         }
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index fbee343..300a894 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -38,6 +38,9 @@
      */
     Task mAssociatedTask;
 
+    /** Whether the starting window is drawn. */
+    boolean mIsDisplayed;
+
     protected StartingData(WindowManagerService service, int typeParams) {
         mService = service;
         mTypeParams = typeParams;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 885968f..b290bec 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -22,7 +22,6 @@
 import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -68,7 +67,6 @@
 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
@@ -172,8 +170,6 @@
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.InsetsState;
@@ -198,6 +194,8 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.Watchdog;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.AppTimeTracker;
@@ -359,6 +357,13 @@
 
     int mLockTaskUid = -1;  // The uid of the application that called startLockTask().
 
+    /**
+     * If non-null, the starting window should cover the associated task. It is assigned when the
+     * parent activity of starting window is put in a partial area of the task. This field will be
+     * cleared when all visible activities in this task are drawn.
+     */
+    StartingData mSharedStartingData;
+
     /** The process that had previously hosted the root activity of this task.
      * Used to know that we should try harder to keep this process around, in case the
      * user wants to return to it. */
@@ -601,12 +606,6 @@
 
     boolean mLastSurfaceShowing = true;
 
-    /**
-     * Tracks if a back gesture is in progress.
-     * Skips any system transition animations if this is set to {@code true}.
-     */
-    boolean mBackGestureStarted = false;
-
     private Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent,
             Intent _affinityIntent, String _affinity, String _rootAffinity,
             ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset,
@@ -674,7 +673,7 @@
         mLaunchCookie = _launchCookie;
         mDeferTaskAppear = _deferTaskAppear;
         mRemoveWithTaskOrganizer = _removeWithTaskOrganizer;
-        EventLogTags.writeWmTaskCreated(mTaskId, isRootTask() ? INVALID_TASK_ID : getRootTaskId());
+        EventLogTags.writeWmTaskCreated(mTaskId);
     }
 
     static Task fromWindowContainerToken(WindowContainerToken token) {
@@ -1291,7 +1290,8 @@
     }
 
     void updateTaskMovement(boolean toTop, int position) {
-        EventLogTags.writeWmTaskMoved(mTaskId, toTop ? 1 : 0, position);
+        EventLogTags.writeWmTaskMoved(mTaskId, getRootTaskId(), getDisplayId(), toTop ? 1 : 0,
+                position);
         final TaskDisplayArea taskDisplayArea = getDisplayArea();
         if (taskDisplayArea != null && isLeafTask()) {
             taskDisplayArea.onLeafTaskMoved(this, toTop);
@@ -1401,13 +1401,26 @@
      * Reorder the history task so that the passed activity is brought to the front.
      * @return whether it was actually moved (vs already being top).
      */
-    final boolean moveActivityToFrontLocked(ActivityRecord newTop) {
+    final boolean moveActivityToFront(ActivityRecord newTop) {
         ProtoLog.i(WM_DEBUG_ADD_REMOVE, "Removing and adding activity %s to root task at top "
                 + "callers=%s", newTop, Debug.getCallers(4));
-        int origDist = getDistanceFromTop(newTop);
-        positionChildAtTop(newTop);
+        final TaskFragment taskFragment = newTop.getTaskFragment();
+        boolean moved;
+        if (taskFragment != this) {
+            if (taskFragment.isEmbedded() && taskFragment.getNonFinishingActivityCount() == 1) {
+                taskFragment.mClearedForReorderActivityToFront = true;
+            }
+            newTop.reparent(this, POSITION_TOP);
+            moved = true;
+            if (taskFragment.isEmbedded()) {
+                mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController
+                        .onActivityReparentedToTask(newTop);
+            }
+        } else {
+            moved = moveChildToFront(newTop);
+        }
         updateEffectiveIntent();
-        return getDistanceFromTop(newTop) != origDist;
+        return moved;
     }
 
     @Override
@@ -1575,6 +1588,11 @@
                 removeChild(r, reason);
             });
         } else {
+            // Finish or destroy apps from the bottom to ensure that all the other activity have
+            // been finished and the top task in another task gets resumed when a top activity is
+            // removed. Otherwise, shell transitions wouldn't run because there would be no event
+            // that sets the transition ready.
+            final boolean traverseTopToBottom = !mTransitionController.isShellTransitionsEnabled();
             forAllActivities((r) -> {
                 if (r.finishing || (excludingTaskOverlay && r.isTaskOverlay())) {
                     return;
@@ -1588,7 +1606,7 @@
                 } else {
                     r.destroyIfPossible(reason);
                 }
-            });
+            }, traverseTopToBottom);
         }
     }
 
@@ -2554,7 +2572,7 @@
         }
         mRemoving = true;
 
-        EventLogTags.writeWmTaskRemoved(mTaskId, reason);
+        EventLogTags.writeWmTaskRemoved(mTaskId, getRootTaskId(), getDisplayId(), reason);
         clearPinnedTaskIfNeed();
         // If applicable let the TaskOrganizer know the Task is vanishing.
         setTaskOrganizer(null);
@@ -2567,7 +2585,8 @@
     void reparent(Task rootTask, int position, boolean moveParents, String reason) {
         if (DEBUG_ROOT_TASK) Slog.i(TAG, "reParentTask: removing taskId=" + mTaskId
                 + " from rootTask=" + getRootTask());
-        EventLogTags.writeWmTaskRemoved(mTaskId, "reParentTask:" + reason);
+        EventLogTags.writeWmTaskRemoved(mTaskId, getRootTaskId(), getDisplayId(),
+                "reParentTask:" + reason);
 
         reparent(rootTask, position);
 
@@ -3074,20 +3093,6 @@
         });
     }
 
-    void positionChildAtTop(ActivityRecord child) {
-        positionChildAt(child, POSITION_TOP);
-    }
-
-    void positionChildAt(ActivityRecord child, int position) {
-        if (child == null) {
-            Slog.w(TAG_WM,
-                    "Attempted to position of non-existing app");
-            return;
-        }
-
-        positionChildAt(position, child, false /* includeParents */);
-    }
-
     void setTaskDescription(TaskDescription taskDescription) {
         mTaskDescription = taskDescription;
     }
@@ -3325,14 +3330,6 @@
                     }
                 });
             }
-        } else if (mBackGestureStarted) {
-            // Cancel playing transitions if a back navigation animation is in progress.
-            // This bit is set by {@link BackNavigationController} when a back gesture is started.
-            // It is used as a one-off transition overwrite that is cleared when the back gesture
-            // is committed and triggers a transition, or when the gesture is cancelled.
-            mBackGestureStarted = false;
-            mDisplayContent.mSkipAppTransitionAnimation = true;
-            ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Skipping app transition animation. task=%s", this);
         } else {
             super.applyAnimationUnchecked(lp, enter, transit, isVoiceInteraction, sources);
         }
@@ -3514,7 +3511,7 @@
             final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION);
             if (topMainWin != null) {
                 info.mainWindowLayoutParams = topMainWin.getAttrs();
-                info.requestedVisibilities.set(topMainWin.getRequestedVisibilities());
+                info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes();
             }
         }
         // If the developer has persist a different configuration, we need to override it to the
@@ -3674,6 +3671,9 @@
         if (mRootProcess != null) {
             pw.print(prefix); pw.print("mRootProcess="); pw.println(mRootProcess);
         }
+        if (mSharedStartingData != null) {
+            pw.println(prefix + "mSharedStartingData=" + mSharedStartingData);
+        }
         pw.print(prefix); pw.print("taskId=" + mTaskId);
         pw.println(" rootTaskId=" + getRootTaskId());
         pw.print(prefix); pw.println("hasChildPipActivity=" + (mChildPipActivity != null));
@@ -4295,13 +4295,14 @@
     }
 
     /**
-     * @return true if the task is currently focused.
+     * @return {@code true} if the task is currently focused or one of its children is focused.
      */
     boolean isFocused() {
         if (mDisplayContent == null || mDisplayContent.mFocusedApp == null) {
             return false;
         }
-        return mDisplayContent.mFocusedApp.getTask() == this;
+        final Task focusedTask = mDisplayContent.mFocusedApp.getTask();
+        return focusedTask == this || (focusedTask != null && focusedTask.getParent() == this);
     }
 
     /**
@@ -4321,6 +4322,8 @@
      */
     void onAppFocusChanged(boolean hasFocus) {
         dispatchTaskInfoChangedIfNeeded(false /* force */);
+        final Task parentTask = getParent().asTask();
+        if (parentTask != null) parentTask.dispatchTaskInfoChangedIfNeeded(false /* force */);
     }
 
     void onPictureInPictureParamsChanged() {
@@ -5787,12 +5790,10 @@
             return false;
         }
 
-        // Existing Tasks can be reused if a new root task will be created anyway, or for the
-        // Dream - because there can only ever be one DreamActivity.
+        // Existing Tasks can be reused if a new root task will be created anyway.
         final int windowingMode = getWindowingMode();
         final int activityType = getActivityType();
-        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType)
-                || activityType == ACTIVITY_TYPE_DREAM;
+        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType);
     }
 
     void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index b6c14bb..6ff91af 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -455,7 +455,7 @@
         }
 
         mLastLeafTaskToFrontId = t.mTaskId;
-        EventLogTags.writeWmTaskToFront(t.mUserId, t.mTaskId);
+        EventLogTags.writeWmTaskToFront(t.mUserId, t.mTaskId, getDisplayId());
         // Notifying only when a leaf task moved to front. Or the listeners would be notified
         // couple times from the leaf task all the way up to the root task.
         mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(t.getTaskInfo());
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index efb6302..911a8da 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -79,6 +79,7 @@
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -223,6 +224,14 @@
     private TaskFragment mAdjacentTaskFragment;
 
     /**
+     * Unlike the {@link mAdjacentTaskFragment}, the companion TaskFragment is not always visually
+     * adjacent to this one, but this TaskFragment will be removed by the organizer if the
+     * companion TaskFragment is removed.
+     */
+    @Nullable
+    private TaskFragment mCompanionTaskFragment;
+
+    /**
      * Prevents duplicate calls to onTaskAppeared.
      */
     boolean mTaskFragmentAppearedSent;
@@ -240,6 +249,12 @@
     boolean mClearedTaskFragmentForPip;
 
     /**
+     * The last running activity of the TaskFragment was removed and added to the top-most of the
+     * Task because it was launched with FLAG_ACTIVITY_REORDER_TO_FRONT.
+     */
+    boolean mClearedForReorderActivityToFront;
+
+    /**
      * When we are in the process of pausing an activity, before starting the
      * next one, this variable holds the activity that is currently being paused.
      *
@@ -290,6 +305,12 @@
     private final IBinder mFragmentToken;
 
     /**
+     * Whether to delay the call to {@link #updateOrganizedTaskFragmentSurface()} when there is a
+     * configuration change.
+     */
+    private boolean mDelayOrganizedTaskFragmentSurfaceUpdate;
+
+    /**
      * Whether to delay the last activity of TaskFragment being immediately removed while finishing.
      * This should only be set on a embedded TaskFragment, where the organizer can have the
      * opportunity to perform animations and finishing the adjacent TaskFragment.
@@ -389,6 +410,14 @@
         }
     }
 
+    void setCompanionTaskFragment(@Nullable TaskFragment companionTaskFragment) {
+        mCompanionTaskFragment = companionTaskFragment;
+    }
+
+    TaskFragment getCompanionTaskFragment() {
+        return mCompanionTaskFragment;
+    }
+
     void resetAdjacentTaskFragment() {
         // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment.
         if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) {
@@ -1845,6 +1874,7 @@
         ActivityRecord r = topRunningActivity();
         mClearedTaskForReuse = false;
         mClearedTaskFragmentForPip = false;
+        mClearedForReorderActivityToFront = false;
 
         final ActivityRecord addingActivity = child.asActivityRecord();
         final boolean isAddingActivity = addingActivity != null;
@@ -1859,7 +1889,6 @@
         super.addChild(child, index);
 
         if (isAddingActivity && task != null) {
-
             // TODO(b/207481538): temporary per-activity screenshoting
             if (r != null && BackNavigationController.isScreenshotEnabled()) {
                 ProtoLog.v(WM_DEBUG_BACK_PREVIEW, "Screenshotting Activity %s",
@@ -1891,10 +1920,10 @@
     RemoteAnimationTarget createRemoteAnimationTarget(
             RemoteAnimationController.RemoteAnimationRecord record) {
         final ActivityRecord activity = record.getMode() == RemoteAnimationTarget.MODE_OPENING
-                // There may be a trampoline activity without window on top of the existing task
-                // which is moving to front. Exclude the finishing activity so the window of next
-                // activity can be chosen to create the animation target.
-                ? getTopNonFinishingActivity()
+                // There may be a launching (e.g. trampoline or embedded) activity without a window
+                // on top of the existing task which is moving to front. Exclude finishing activity
+                // so the window of next activity can be chosen to create the animation target.
+                ? getActivity(r -> !r.finishing && r.hasChild())
                 : getTopMostActivity();
         return activity != null ? activity.createRemoteAnimationTarget(record) : null;
     }
@@ -2189,7 +2218,7 @@
                 compatScreenHeightDp = inOutConfig.screenHeightDp;
             }
             // Reducing the screen layout starting from its parent config.
-            inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout,
+            inOutConfig.screenLayout = computeScreenLayout(parentConfig.screenLayout,
                     compatScreenWidthDp, compatScreenHeightDp);
         }
     }
@@ -2252,16 +2281,6 @@
         }
     }
 
-    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
-    static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp,
-            int screenHeightDp) {
-        sourceScreenLayout = sourceScreenLayout
-                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
-        final int longSize = Math.max(screenWidthDp, screenHeightDp);
-        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
-        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
-    }
-
     @Override
     public int getActivityType() {
         final int applicationType = super.getActivityType();
@@ -2274,35 +2293,41 @@
 
     @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
-        // Task will animate differently.
-        if (mTaskFragmentOrganizer != null) {
-            mTmpPrevBounds.set(getBounds());
-        }
-
         super.onConfigurationChanged(newParentConfig);
 
-        final boolean shouldStartChangeTransition = shouldStartChangeTransition(mTmpPrevBounds);
-        if (shouldStartChangeTransition) {
-            initializeChangeTransition(mTmpPrevBounds);
-        }
         if (mTaskFragmentOrganizer != null) {
-            if (mTransitionController.isShellTransitionsEnabled()
-                    && !mTransitionController.isCollecting(this)) {
-                // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so
-                // update the surface here if it is not collected by Shell transition.
-                updateOrganizedTaskFragmentSurface();
-            } else if (!mTransitionController.isShellTransitionsEnabled()
-                    && !shouldStartChangeTransition) {
-                // Update the surface here instead of in the organizer so that we can make sure
-                // it can be synced with the surface freezer for legacy app transition.
-                updateOrganizedTaskFragmentSurface();
-            }
+            updateOrganizedTaskFragmentSurface();
         }
 
         sendTaskFragmentInfoChanged();
     }
 
+    void deferOrganizedTaskFragmentSurfaceUpdate() {
+        mDelayOrganizedTaskFragmentSurfaceUpdate = true;
+    }
+
+    void continueOrganizedTaskFragmentSurfaceUpdate() {
+        mDelayOrganizedTaskFragmentSurfaceUpdate = false;
+        updateOrganizedTaskFragmentSurface();
+    }
+
     private void updateOrganizedTaskFragmentSurface() {
+        if (mDelayOrganizedTaskFragmentSurfaceUpdate) {
+            return;
+        }
+        if (mTransitionController.isShellTransitionsEnabled()
+                && !mTransitionController.isCollecting(this)) {
+            // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so
+            // update the surface here if it is not collected by Shell transition.
+            updateOrganizedTaskFragmentSurfaceUnchecked();
+        } else if (!mTransitionController.isShellTransitionsEnabled() && !isAnimating()) {
+            // Update the surface here instead of in the organizer so that we can make sure
+            // it can be synced with the surface freezer for legacy app transition.
+            updateOrganizedTaskFragmentSurfaceUnchecked();
+        }
+    }
+
+    private void updateOrganizedTaskFragmentSurfaceUnchecked() {
         final SurfaceControl.Transaction t = getSyncTransaction();
         updateSurfacePosition(t);
         updateOrganizedTaskFragmentSurfaceSize(t, false /* forceUpdate */);
@@ -2337,6 +2362,11 @@
         if (mTaskFragmentOrganizer != null
                 && (mLastSurfaceSize.x != 0 || mLastSurfaceSize.y != 0)) {
             t.setWindowCrop(mSurfaceControl, 0, 0);
+            final SurfaceControl.Transaction syncTransaction = getSyncTransaction();
+            if (t != syncTransaction) {
+                // Avoid restoring to old window crop if the sync transaction is applied later.
+                syncTransaction.setWindowCrop(mSurfaceControl, 0, 0);
+            }
             mLastSurfaceSize.set(0, 0);
         }
     }
@@ -2351,7 +2381,7 @@
     }
 
     /** Whether we should prepare a transition for this {@link TaskFragment} bounds change. */
-    private boolean shouldStartChangeTransition(Rect startBounds) {
+    boolean shouldStartChangeTransition(Rect startBounds) {
         if (mTaskFragmentOrganizer == null || !canStartChangeTransition()) {
             return false;
         }
@@ -2371,7 +2401,7 @@
     void setSurfaceControl(SurfaceControl sc) {
         super.setSurfaceControl(sc);
         if (mTaskFragmentOrganizer != null) {
-            updateOrganizedTaskFragmentSurface();
+            updateOrganizedTaskFragmentSurfaceUnchecked();
             // If the TaskFragmentOrganizer was set before we created the SurfaceControl, we need to
             // emit the callbacks now.
             sendTaskFragmentAppeared();
@@ -2433,6 +2463,7 @@
                 positionInParent,
                 mClearedTaskForReuse,
                 mClearedTaskFragmentForPip,
+                mClearedForReorderActivityToFront,
                 calculateMinDimension());
     }
 
@@ -2538,6 +2569,19 @@
         return !mCreatedByOrganizer || mIsRemovalRequested;
     }
 
+    @Nullable
+    HardwareBuffer getSnapshotForActivityRecord(@Nullable ActivityRecord r) {
+        if (!BackNavigationController.isScreenshotEnabled()) {
+            return null;
+        }
+        if (r != null && r.mActivityComponent != null) {
+            ScreenCapture.ScreenshotHardwareBuffer backBuffer =
+                    mBackScreenshots.get(r.mActivityComponent.flattenToString());
+            return backBuffer != null ? backBuffer.getHardwareBuffer() : null;
+        }
+        return null;
+    }
+
     @Override
     void removeChild(WindowContainer child) {
         removeChild(child, true /* removeSelfIfPossible */);
@@ -2706,6 +2750,16 @@
         return callback.test(this) ? this : null;
     }
 
+    /**
+     * Moves the passed child to front
+     * @return whether it was actually moved (vs already being top).
+     */
+    boolean moveChildToFront(WindowContainer newTop) {
+        int origDist = getDistanceFromTop(newTop);
+        positionChildAt(POSITION_TOP, newTop, false /* includeParents */);
+        return getDistanceFromTop(newTop) != origDist;
+    }
+
     String toFullString() {
         final StringBuilder sb = new StringBuilder(128);
         sb.append(this);
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 867833a..509b1e6 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -184,19 +184,30 @@
         }
 
         void dispose() {
-            while (!mOrganizedTaskFragments.isEmpty()) {
-                final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
-                // Cleanup before remove to prevent it from sending any additional event, such as
-                // #onTaskFragmentVanished, to the removed organizer.
+            for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
+                // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before
+                // removing the windows to prevent it from adding any additional TaskFragment
+                // pending event.
+                final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
                 taskFragment.onTaskFragmentOrganizerRemoved();
-                taskFragment.removeImmediately();
-                mOrganizedTaskFragments.remove(taskFragment);
             }
+
+            // Defer to avoid unnecessary layout when there are multiple TaskFragments removal.
+            mAtmService.deferWindowLayout();
+            try {
+                while (!mOrganizedTaskFragments.isEmpty()) {
+                    final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0);
+                    taskFragment.removeImmediately();
+                }
+            } finally {
+                mAtmService.continueWindowLayout();
+            }
+
             for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) {
                 // Cleanup any running transaction to unblock the current transition.
                 onTransactionFinished(mDeferredTransitions.keyAt(i));
             }
-            mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
+            mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */);
         }
 
         @NonNull
@@ -426,7 +437,6 @@
 
     @Override
     public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        validateAndGetState(organizer);
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -607,6 +617,13 @@
             int opType, @NonNull Throwable exception) {
         validateAndGetState(organizer);
         Slog.w(TAG, "onTaskFragmentError ", exception);
+        final PendingTaskFragmentEvent vanishedEvent = taskFragment != null
+                ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED)
+                : null;
+        if (vanishedEvent != null) {
+            // No need to notify if the TaskFragment has been removed.
+            return;
+        }
         addPendingEvent(new PendingTaskFragmentEvent.Builder(
                 PendingTaskFragmentEvent.EVENT_ERROR, organizer)
                 .setErrorCallbackToken(errorCallbackToken)
@@ -690,11 +707,17 @@
     }
 
     private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get(
+                organizer.asBinder());
+        if (state == null) {
+            Slog.w(TAG, "The organizer has already been removed.");
+            return;
+        }
+        // Remove any pending event of this organizer first because state.dispose() may trigger
+        // event dispatch as result of surface placement.
+        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         // remove all of the children of the organized TaskFragment
         state.dispose();
-        // Remove any pending event of this organizer.
-        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         mTaskFragmentOrganizerState.remove(organizer.asBinder());
     }
 
@@ -878,23 +901,6 @@
         return null;
     }
 
-    private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
-        if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR
-                // Always send parent info changed to update task visibility
-                || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
-            return true;
-        }
-
-        final TaskFragmentOrganizerState state =
-                mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder());
-        final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment);
-        final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo();
-        // Send an info changed callback if this event is for the last activities to finish in a
-        // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
-        return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
-                && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
-    }
-
     void dispatchPendingEvents() {
         if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
                 || mPendingTaskFragmentEvents.isEmpty()) {
@@ -908,37 +914,19 @@
         }
     }
 
-    void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
+    private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
             @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
         if (pendingEvents.isEmpty()) {
             return;
         }
-
-        final ArrayList<Task> visibleTasks = new ArrayList<>();
-        final ArrayList<Task> invisibleTasks = new ArrayList<>();
-        final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>();
-        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
-            final PendingTaskFragmentEvent event = pendingEvents.get(i);
-            final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
-            // TODO(b/251132298): move visibility check to the client side.
-            if (task != null && (task.lastActiveTime <= event.mDeferTime
-                    || !(isTaskVisible(task, visibleTasks, invisibleTasks)
-                    || shouldSendEventWhenTaskInvisible(event)))) {
-                // Defer sending events to the TaskFragment until the host task is active again.
-                event.mDeferTime = task.lastActiveTime;
-                continue;
-            }
-            candidateEvents.add(event);
-        }
-        final int numEvents = candidateEvents.size();
-        if (numEvents == 0) {
+        if (shouldDeferPendingEvents(state, pendingEvents)) {
             return;
         }
-
         mTmpTaskSet.clear();
+        final int numEvents = pendingEvents.size();
         final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
         for (int i = 0; i < numEvents; i++) {
-            final PendingTaskFragmentEvent event = candidateEvents.get(i);
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
             if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED
                     || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
                 final Task task = event.mTaskFragment.getTask();
@@ -954,7 +942,47 @@
         }
         mTmpTaskSet.clear();
         state.dispatchTransaction(transaction);
-        pendingEvents.removeAll(candidateEvents);
+        pendingEvents.clear();
+    }
+
+    /**
+     * Whether or not to defer sending the events to the organizer to avoid waking the app process
+     * when it is in background. We want to either send all events or none to avoid inconsistency.
+     */
+    private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state,
+            @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
+        final ArrayList<Task> visibleTasks = new ArrayList<>();
+        final ArrayList<Task> invisibleTasks = new ArrayList<>();
+        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
+            if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) {
+                // Send events for any other types.
+                return false;
+            }
+
+            // Check if we should send the event given the Task visibility and events.
+            final Task task;
+            if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
+                task = event.mTask;
+            } else {
+                task = event.mTaskFragment.getTask();
+            }
+            if (task.lastActiveTime > event.mDeferTime
+                    && isTaskVisible(task, visibleTasks, invisibleTasks)) {
+                // Send events when the app has at least one visible Task.
+                return false;
+            } else if (shouldSendEventWhenTaskInvisible(task, state, event)) {
+                // Sent events even if the Task is invisible.
+                return false;
+            }
+
+            // Defer sending events to the organizer until the host task is active (visible) again.
+            event.mDeferTime = task.lastActiveTime;
+        }
+        // Defer for invisible Task.
+        return true;
     }
 
     private static boolean isTaskVisible(@NonNull Task task,
@@ -975,6 +1003,28 @@
         }
     }
 
+    private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
+            @NonNull TaskFragmentOrganizerState state,
+            @NonNull PendingTaskFragmentEvent event) {
+        final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos
+                .get(task.mTaskId);
+        if (lastParentInfo == null || lastParentInfo.isVisible()) {
+            // When the Task was visible, or when there was no Task info changed sent (in which case
+            // the organizer will consider it as visible by default), always send the event to
+            // update the Task visibility.
+            return true;
+        }
+        if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
+            // Send info changed if the TaskFragment is becoming empty/non-empty so the
+            // organizer can choose whether or not to remove the TaskFragment.
+            final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos
+                    .get(event.mTaskFragment);
+            final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0;
+            return lastInfo == null || lastInfo.isEmpty() != isEmpty;
+        }
+        return false;
+    }
+
     void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) {
         final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
                 PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java
index 09fd900..29c192c 100644
--- a/services/core/java/com/android/server/wm/TaskPersister.java
+++ b/services/core/java/com/android/server/wm/TaskPersister.java
@@ -30,12 +30,12 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 9306749..29c98b9 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -587,7 +587,7 @@
         final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
         final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags,
                 attrs.privateFlags, attrs.insetsFlags.appearance, task.getTaskDescription(),
-                mHighResTaskSnapshotScale, insetsState);
+                mHighResTaskSnapshotScale, mainWindow.getRequestedVisibleTypes());
         final int taskWidth = taskBounds.width();
         final int taskHeight = taskBounds.height();
         final int width = (int) (taskWidth * mHighResTaskSnapshotScale);
@@ -750,12 +750,12 @@
         private final int mWindowFlags;
         private final int mWindowPrivateFlags;
         private final float mScale;
-        private final InsetsState mInsetsState;
+        private final @Type.InsetsType int mRequestedVisibleTypes;
         private final Rect mSystemBarInsets = new Rect();
 
         SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance,
                 ActivityManager.TaskDescription taskDescription, float scale,
-                InsetsState insetsState) {
+                @Type.InsetsType int requestedVisibleTypes) {
             mWindowFlags = windowFlags;
             mWindowPrivateFlags = windowPrivateFlags;
             mScale = scale;
@@ -774,7 +774,7 @@
                             && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim));
             mStatusBarPaint.setColor(mStatusBarColor);
             mNavigationBarPaint.setColor(mNavigationBarColor);
-            mInsetsState = insetsState;
+            mRequestedVisibleTypes = requestedVisibleTypes;
         }
 
         void setInsets(Rect systemBarInsets) {
@@ -785,7 +785,7 @@
             final boolean forceBarBackground =
                     (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
             if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
-                    mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) {
+                    mRequestedVisibleTypes, mStatusBarColor, mWindowFlags, forceBarBackground)) {
                 return (int) (mSystemBarInsets.top * mScale);
             } else {
                 return 0;
@@ -796,7 +796,7 @@
             final boolean forceBarBackground =
                     (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0;
             return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible(
-                    mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground);
+                    mRequestedVisibleTypes, mNavigationBarColor, mWindowFlags, forceBarBackground);
         }
 
         void drawDecors(Canvas c) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 4459d45..ef68590 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -83,11 +83,11 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.inputmethod.InputMethodManagerInternal;
-import com.android.server.wm.utils.RotationAnimationUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -469,6 +469,48 @@
     }
 
     /**
+     * Records that a particular container has been reparented. This only effects windows that have
+     * already been collected in the transition. This should be called before reparenting because
+     * the old parent may be removed during reparenting, for example:
+     * {@link Task#shouldRemoveSelfOnLastChildRemoval}
+     */
+    void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) {
+        if (!mChanges.containsKey(wc)) {
+            // #collectReparentChange() will be called when the window is reparented. Skip if it is
+            // a window that has not been collected, which means we don't care about this window for
+            // the current transition.
+            return;
+        }
+        final ChangeInfo change = mChanges.get(wc);
+        // Use the current common ancestor if there are multiple reparent, and the original parent
+        // has been detached. Otherwise, use the original parent before the transition.
+        final WindowContainer prevParent =
+                change.mStartParent == null || change.mStartParent.isAttached()
+                        ? change.mStartParent
+                        : change.mCommonAncestor;
+        if (prevParent == null || !prevParent.isAttached()) {
+            Slog.w(TAG, "Trying to collect reparenting of a window after the previous parent has"
+                    + " been detached: " + wc);
+            return;
+        }
+        if (prevParent == newParent) {
+            Slog.w(TAG, "Trying to collect reparenting of a window that has not been reparented: "
+                    + wc);
+            return;
+        }
+        if (!newParent.isAttached()) {
+            Slog.w(TAG, "Trying to collect reparenting of a window that is not attached after"
+                    + " reparenting: " + wc);
+            return;
+        }
+        WindowContainer ancestor = newParent;
+        while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
+            ancestor = ancestor.getParent();
+        }
+        change.mCommonAncestor = ancestor;
+    }
+
+    /**
      * @return {@code true} if `wc` is a participant or is a descendant of one.
      */
     boolean isInTransition(WindowContainer wc) {
@@ -830,8 +872,8 @@
     void abort() {
         // This calls back into itself via controller.abort, so just early return here.
         if (mState == STATE_ABORT) return;
-        if (mState != STATE_COLLECTING) {
-            throw new IllegalStateException("Too late to abort.");
+        if (mState != STATE_COLLECTING && mState != STATE_STARTED) {
+            throw new IllegalStateException("Too late to abort. state=" + mState);
         }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Aborting Transition: %d", mSyncId);
         mState = STATE_ABORT;
@@ -886,10 +928,16 @@
             mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED;
         }
 
+        // Check whether the participants were animated from back navigation.
+        final boolean markBackAnimated = mController.mAtm.mBackNavigationController
+                .containsBackAnimationTargets(this);
         // Resolve the animating targets from the participants
         mTargets = calculateTargets(mParticipants, mChanges);
         final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, mChanges,
                 transaction);
+        if (markBackAnimated) {
+            mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction);
+        }
         if (mOverrideOptions != null) {
             info.setAnimationOptions(mOverrideOptions);
             if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
@@ -1524,20 +1572,7 @@
             return out;
         }
 
-        // Find the top-most shared ancestor of app targets.
-        WindowContainer<?> ancestor = topApp.getParent();
-        // Go up ancestor parent chain until all targets are descendants.
-        ancestorLoop:
-        while (ancestor != null) {
-            for (int i = sortedTargets.size() - 1; i >= 0; --i) {
-                final WindowContainer wc = sortedTargets.get(i);
-                if (!isWallpaper(wc) && !wc.isDescendantOf(ancestor)) {
-                    ancestor = ancestor.getParent();
-                    continue ancestorLoop;
-                }
-            }
-            break;
-        }
+        WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp);
 
         // make leash based on highest (z-order) direct child of ancestor with a participant.
         WindowContainer leashReference = sortedTargets.get(0);
@@ -1568,7 +1603,11 @@
             change.setMode(info.getTransitMode(target));
             change.setStartAbsBounds(info.mAbsoluteBounds);
             change.setFlags(info.getChangeFlags(target));
+
             final Task task = target.asTask();
+            final TaskFragment taskFragment = target.asTaskFragment();
+            final ActivityRecord activityRecord = target.asActivityRecord();
+
             if (task != null) {
                 final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo();
                 task.fillTaskInfo(tinfo);
@@ -1602,12 +1641,7 @@
             change.setEndRelOffset(bounds.left - parentBounds.left,
                     bounds.top - parentBounds.top);
             int endRotation = target.getWindowConfiguration().getRotation();
-            final ActivityRecord activityRecord = target.asActivityRecord();
             if (activityRecord != null) {
-                final Task arTask = activityRecord.getTask();
-                final int backgroundColor = ColorUtils.setAlphaComponent(
-                        arTask.getTaskDescription().getBackgroundColor(), 255);
-                change.setBackgroundColor(backgroundColor);
                 // TODO(b/227427984): Shell needs to aware letterbox.
                 // Always use parent bounds of activity because letterbox area (e.g. fixed aspect
                 // ratio or size compat mode) should be included in the animation.
@@ -1620,6 +1654,18 @@
             } else {
                 change.setEndAbsBounds(bounds);
             }
+
+            if (activityRecord != null || (taskFragment != null && taskFragment.isEmbedded())) {
+                // Set background color to Task theme color for activity and embedded TaskFragment
+                // in case we want to show background during the animation.
+                final Task parentTask = activityRecord != null
+                        ? activityRecord.getTask()
+                        : taskFragment.getTask();
+                final int backgroundColor = ColorUtils.setAlphaComponent(
+                        parentTask.getTaskDescription().getBackgroundColor(), 255);
+                change.setBackgroundColor(backgroundColor);
+            }
+
             change.setRotation(info.mRotation, endRotation);
             if (info.mSnapshot != null) {
                 change.setSnapshot(info.mSnapshot, info.mSnapshotLuma);
@@ -1643,6 +1689,46 @@
         return out;
     }
 
+    /**
+     * Finds the top-most common ancestor of app targets.
+     *
+     * Makes sure that the previous parent is also a descendant to make sure the animation won't
+     * be covered by other windows below the previous parent. For example, when reparenting an
+     * activity from PiP Task to split screen Task.
+     */
+    @NonNull
+    private static WindowContainer<?> findCommonAncestor(
+            @NonNull ArrayList<WindowContainer> targets,
+            @NonNull ArrayMap<WindowContainer, ChangeInfo> changes,
+            @NonNull WindowContainer<?> topApp) {
+        WindowContainer<?> ancestor = topApp.getParent();
+        // Go up ancestor parent chain until all targets are descendants. Ancestor should never be
+        // null because all targets are attached.
+        for (int i = targets.size() - 1; i >= 0; i--) {
+            final WindowContainer wc = targets.get(i);
+            if (isWallpaper(wc)) {
+                // Skip the non-app window.
+                continue;
+            }
+            while (!wc.isDescendantOf(ancestor)) {
+                ancestor = ancestor.getParent();
+            }
+
+            // Make sure the previous parent is also a descendant to make sure the animation won't
+            // be covered by other windows below the previous parent. For example, when reparenting
+            // an activity from PiP Task to split screen Task.
+            final ChangeInfo change = changes.get(wc);
+            final WindowContainer prevParent = change.mCommonAncestor;
+            if (prevParent == null || !prevParent.isAttached()) {
+                continue;
+            }
+            while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
+                ancestor = ancestor.getParent();
+            }
+        }
+        return ancestor;
+    }
+
     private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type,
             ArrayList<WindowContainer> sortedTargets) {
         // Find the layout params of the top-most application window that is part of the
@@ -1761,10 +1847,19 @@
         @Retention(RetentionPolicy.SOURCE)
         @interface Flag {}
 
-        // Usually "post" change state.
+        /**
+         * "Parent" that is also included in the transition. When populating the parent changes, we
+         * may skip the intermediate parents, so this may not be the actual parent in the hierarchy.
+         */
         WindowContainer mEndParent;
-        // Parent before change state.
+        /** Actual parent window before change state. */
         WindowContainer mStartParent;
+        /**
+         * When the window is reparented during the transition, this is the common ancestor window
+         * of the {@link #mStartParent} and the current parent. This is needed because the
+         * {@link #mStartParent} may have been detached when the transition starts.
+         */
+        WindowContainer mCommonAncestor;
 
         // State tracking
         boolean mExistenceChanged = false;
@@ -1846,9 +1941,20 @@
             final Task task = wc.asTask();
             if (task != null) {
                 final ActivityRecord topActivity = task.getTopNonFinishingActivity();
-                if (topActivity != null && topActivity.mStartingData != null
-                        && topActivity.mStartingData.hasImeSurface()) {
-                    flags |= FLAG_WILL_IME_SHOWN;
+                if (topActivity != null) {
+                    if (topActivity.mStartingData != null
+                            && topActivity.mStartingData.hasImeSurface()) {
+                        flags |= FLAG_WILL_IME_SHOWN;
+                    }
+                    if (topActivity.mAtmService.mBackNavigationController
+                            .isMonitorTransitionTarget(topActivity)) {
+                        flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
+                    }
+                } else {
+                    if (task.mAtmService.mBackNavigationController
+                            .isMonitorTransitionTarget(task)) {
+                        flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
+                    }
                 }
                 if (task.voiceSession != null) {
                     flags |= FLAG_IS_VOICE_INTERACTION;
@@ -1862,6 +1968,10 @@
                     flags |= FLAG_IS_VOICE_INTERACTION;
                 }
                 flags |= record.mTransitionChangeFlags;
+                if (record.mAtmService.mBackNavigationController
+                        .isMonitorTransitionTarget(record)) {
+                    flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
+                }
             }
             final TaskFragment taskFragment = wc.asTaskFragment();
             if (taskFragment != null && task == null) {
@@ -2190,7 +2300,7 @@
             changeInfo.mSnapshot = snapshotSurface;
             if (isDisplayRotation) {
                 // This isn't cheap, so only do it for display rotations.
-                changeInfo.mSnapshotLuma = RotationAnimationUtils.getMedianBorderLuma(
+                changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
                         screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace());
             }
             SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index ac85c9a..25df511 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -423,7 +423,7 @@
         Transition newTransition = null;
         if (isCollecting()) {
             if (displayChange != null) {
-                throw new IllegalArgumentException("Provided displayChange for a non-new request");
+                Slog.e(TAG, "Provided displayChange for a non-new request", new Throwable());
             }
             // Make the collecting transition wait until this request is ready.
             mCollectingTransition.setReady(readyGroupRef, false);
@@ -533,6 +533,17 @@
         mCollectingTransition.collectVisibleChange(wc);
     }
 
+    /**
+     * Records that a particular container has been reparented. This only effects windows that have
+     * already been collected in the transition. This should be called before reparenting because
+     * the old parent may be removed during reparenting, for example:
+     * {@link Task#shouldRemoveSelfOnLastChildRemoval}
+     */
+    void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) {
+        if (!isCollecting()) return;
+        mCollectingTransition.collectReparentChange(wc, newParent);
+    }
+
     /** @see Transition#mStatusBarTransitionDelay */
     void setStatusBarTransitionDelay(long delay) {
         if (mCollectingTransition == null) return;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 81d6795..6522d93 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -239,8 +239,7 @@
 
     private boolean isBackNavigationTarget(WindowState w) {
         // The window is in animating by back navigation and set to show wallpaper.
-        final BackNavigationController controller = mService.mAtmService.mBackNavigationController;
-        return controller != null && controller.isWallpaperVisible(w);
+        return mService.mAtmService.mBackNavigationController.isWallpaperVisible(w);
     }
 
     /**
@@ -831,9 +830,7 @@
 
             // If there was a pending back navigation animation that would show wallpaper, start
             // the animation due to it was skipped in previous surface placement.
-            if (mService.mAtmService.mBackNavigationController != null) {
-                mService.mAtmService.mBackNavigationController.startAnimation();
-            }
+            mService.mAtmService.mBackNavigationController.startAnimation();
             return true;
         }
         return false;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9763df6..5087a0b 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -41,6 +41,7 @@
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.AppTransition.MAX_APP_TRANSITION_DURATION;
 import static com.android.server.wm.AppTransition.isActivityTransitOld;
+import static com.android.server.wm.AppTransition.isTaskFragmentTransitOld;
 import static com.android.server.wm.AppTransition.isTaskTransitOld;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.IdentifierProto.HASH_CODE;
@@ -541,6 +542,10 @@
             throw new IllegalArgumentException("WC=" + this + " already child of " + mParent);
         }
 
+        // Collect before removing child from old parent, because the old parent may be removed if
+        // this is the last child in it.
+        mTransitionController.collectReparentChange(this, newParent);
+
         // The display object before reparenting as that might lead to old parent getting removed
         // from the display if it no longer has any child.
         final DisplayContent prevDc = oldParent.getDisplayContent();
@@ -1607,6 +1612,16 @@
         return false;
     }
 
+    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
+    static int computeScreenLayout(int sourceScreenLayout, int screenWidthDp,
+            int screenHeightDp) {
+        sourceScreenLayout = sourceScreenLayout
+                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
+        final int longSize = Math.max(screenWidthDp, screenHeightDp);
+        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
+        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
+    }
+
     // TODO: Users would have their own window containers under the display container?
     void switchUser(int userId) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
@@ -2989,10 +3004,17 @@
             // {@link Activity#overridePendingTransition(int, int, int)}.
             @ColorInt int backdropColor = 0;
             if (controller.isFromActivityEmbedding()) {
-                final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter);
-                final Animation a = animAttr != 0
-                        ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null;
-                showBackdrop = a != null && a.getShowBackdrop();
+                if (isChanging) {
+                    // When there are more than one changing containers, it may leave part of the
+                    // screen empty. Show background color to cover that.
+                    showBackdrop = getDisplayContent().mChangingContainers.size() > 1;
+                } else {
+                    // Check whether or not to show backdrop for open/close transition.
+                    final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter);
+                    final Animation a = animAttr != 0
+                            ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null;
+                    showBackdrop = a != null && a.getShowBackdrop();
+                }
                 backdropColor = appTransition.getNextAppTransitionBackgroundColor();
             }
             final Rect localBounds = new Rect(mTmpRect);
@@ -3095,9 +3117,16 @@
                 }
             }
 
+            // Check if the animation requests to show background color for Activity and embedded
+            // TaskFragment.
             final ActivityRecord activityRecord = asActivityRecord();
-            if (activityRecord != null && isActivityTransitOld(transit)
-                    && adapter.getShowBackground()) {
+            final TaskFragment taskFragment = asTaskFragment();
+            if (adapter.getShowBackground()
+                    // Check if it is Activity transition.
+                    && ((activityRecord != null && isActivityTransitOld(transit))
+                    // Check if it is embedded TaskFragment transition.
+                    || (taskFragment != null && taskFragment.isEmbedded()
+                    && isTaskFragmentTransitOld(transit)))) {
                 final @ColorInt int backgroundColorForTransition;
                 if (adapter.getBackgroundColor() != 0) {
                     // If available use the background color provided through getBackgroundColor
@@ -3107,9 +3136,11 @@
                     // Otherwise default to the window's background color if provided through
                     // the theme as the background color for the animation - the top most window
                     // with a valid background color and showBackground set takes precedence.
-                    final Task arTask = activityRecord.getTask();
+                    final Task parentTask = activityRecord != null
+                            ? activityRecord.getTask()
+                            : taskFragment.getTask();
                     backgroundColorForTransition = ColorUtils.setAlphaComponent(
-                            arTask.getTaskDescription().getBackgroundColor(), 255);
+                            parentTask.getTaskDescription().getBackgroundColor(), 255);
                 }
                 animationRunnerBuilder.setTaskBackgroundColor(backgroundColorForTransition);
             }
@@ -3246,9 +3277,10 @@
 
     void resetSurfacePositionForAnimationLeash(Transaction t) {
         t.setPosition(mSurfaceControl, 0, 0);
-        if (mSyncState != SYNC_STATE_NONE && t != mSyncTransaction) {
+        final SurfaceControl.Transaction syncTransaction = getSyncTransaction();
+        if (t != syncTransaction) {
             // Avoid restoring to old position if the sync transaction is applied later.
-            mSyncTransaction.setPosition(mSurfaceControl, 0, 0);
+            syncTransaction.setPosition(mSurfaceControl, 0, 0);
         }
         mLastSurfacePosition.set(0, 0);
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 32feb6c..bab3a05 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -43,6 +43,7 @@
 import android.view.SurfaceControlViewHost;
 import android.view.WindowInfo;
 import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.server.input.InputManagerService;
@@ -613,15 +614,6 @@
             @NonNull IBinder imeTargetWindowToken);
 
     /**
-     * Returns the presence of a software navigation bar on the specified display.
-     *
-     * @param displayId the id of display to check if there is a software navigation bar.
-     * @return {@code true} if there is a software navigation. {@code false} otherwise, including
-     *         the case when the specified display does not exist.
-     */
-    public abstract boolean hasNavigationBar(int displayId);
-
-    /**
       * Returns true when the hardware keyboard is available.
       */
     public abstract boolean isHardKeyboardAvailable();
@@ -738,16 +730,20 @@
      * Show IME on imeTargetWindow once IME has finished layout.
      *
      * @param imeTargetWindowToken token of the (IME target) window on which IME should be shown.
+     * @param statsToken the token tracking the current IME show request or {@code null} otherwise.
      */
-    public abstract void showImePostLayout(IBinder imeTargetWindowToken);
+    public abstract void showImePostLayout(IBinder imeTargetWindowToken,
+            @Nullable ImeTracker.Token statsToken);
 
     /**
      * Hide IME using imeTargetWindow when requested.
      *
      * @param imeTargetWindowToken token of the (IME target) window on which IME should be hidden.
      * @param displayId the id of the display the IME is on.
+     * @param statsToken the token tracking the current IME hide request or {@code null} otherwise.
      */
-    public abstract void hideIme(IBinder imeTargetWindowToken, int displayId);
+    public abstract void hideIme(IBinder imeTargetWindowToken, int displayId,
+            @Nullable ImeTracker.Token statsToken);
 
     /**
      * Tell window manager about a package that should be running with a restricted range of
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c17af30..df343db 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -266,9 +266,9 @@
 import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputWindowHandle;
+import android.view.InsetsFrameProvider;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
 import android.view.MagnificationSpec;
 import android.view.MotionEvent;
@@ -283,6 +283,7 @@
 import android.view.View;
 import android.view.WindowContentFrameStats;
 import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
 import android.view.WindowManager.LayoutParams;
@@ -291,6 +292,7 @@
 import android.view.WindowManagerPolicyConstants.PointerEventListener;
 import android.view.displayhash.DisplayHash;
 import android.view.displayhash.VerifiedDisplayHash;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 import android.window.ITaskFpsCallback;
 import android.window.ScreenCapture;
@@ -1409,7 +1411,7 @@
     }
 
     public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
-            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
+            int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
@@ -1635,7 +1637,7 @@
             attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);
             attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid,
                     callingPid);
-            win.setRequestedVisibilities(requestedVisibilities);
+            win.setRequestedVisibleTypes(requestedVisibleTypes);
 
             res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);
             if (res != ADD_OKAY) {
@@ -1841,7 +1843,7 @@
                 // Make this invalid which indicates a null attached frame.
                 outAttachedFrame.set(0, 0, -1, -1);
             }
-            outSizeCompatScale[0] = win.getSizeCompatScale();
+            outSizeCompatScale[0] = win.getCompatScaleForClient();
         }
 
         Binder.restoreCallingIdentity(origId);
@@ -1927,22 +1929,14 @@
                     && attachedWindow.mActivityRecord.mTargetSdk >= Build.VERSION_CODES.O;
         } else {
             // Otherwise, look at the package
-            try {
-                ApplicationInfo appInfo = mContext.getPackageManager()
-                        .getApplicationInfoAsUser(packageName, 0,
-                                UserHandle.getUserId(callingUid));
-                if (appInfo.uid != callingUid) {
-                    throw new SecurityException("Package " + packageName + " not in UID "
-                            + callingUid);
-                }
-                if (appInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
-                    return true;
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                /* ignore */
+            final ApplicationInfo appInfo = mPmInternal.getApplicationInfo(
+                    packageName, 0 /* flags */, SYSTEM_UID, UserHandle.getUserId(callingUid));
+            if (appInfo == null || appInfo.uid != callingUid) {
+                throw new SecurityException("Package " + packageName + " not in UID "
+                        + callingUid);
             }
+            return appInfo.targetSdkVersion >= Build.VERSION_CODES.O;
         }
-        return false;
     }
 
     /**
@@ -2277,6 +2271,27 @@
                                         "Insets types can not be changed after the window is "
                                                 + "added.");
                             }
+                            final InsetsFrameProvider.InsetsSizeOverride[] overrides =
+                                    win.mAttrs.providedInsets[i].insetsSizeOverrides;
+                            final InsetsFrameProvider.InsetsSizeOverride[] newOverrides =
+                                    attrs.providedInsets[i].insetsSizeOverrides;
+                            if (!(overrides == null && newOverrides == null)) {
+                                if (overrides == null || newOverrides == null
+                                        || (overrides.length != newOverrides.length)) {
+                                    throw new IllegalArgumentException(
+                                            "Insets override types can not be changed after the "
+                                                    + "window is added.");
+                                } else {
+                                    final int overrideTypes = overrides.length;
+                                    for (int j = 0; j < overrideTypes; j++) {
+                                        if (overrides[j].windowType != newOverrides[j].windowType) {
+                                            throw new IllegalArgumentException(
+                                                    "Insets override types can not be changed after"
+                                                            + " the window is added.");
+                                        }
+                                    }
+                                }
+                            }
                         }
                     }
                 }
@@ -4428,7 +4443,8 @@
     }
 
     @Override
-    public void updateDisplayWindowRequestedVisibilities(int displayId, InsetsVisibilities vis) {
+    public void updateDisplayWindowRequestedVisibleTypes(
+            int displayId, @InsetsType int requestedVisibleTypes) {
         if (mContext.checkCallingOrSelfPermission(MANAGE_APP_TOKENS)
                 != PackageManager.PERMISSION_GRANTED) {
             throw new SecurityException("Must hold permission " + MANAGE_APP_TOKENS);
@@ -4440,7 +4456,7 @@
                 if (dc == null || dc.mRemoteInsetsControlTarget == null) {
                     return;
                 }
-                dc.mRemoteInsetsControlTarget.setRequestedVisibilities(vis);
+                dc.mRemoteInsetsControlTarget.setRequestedVisibleTypes(requestedVisibleTypes);
                 dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget);
             }
         } finally {
@@ -5272,7 +5288,6 @@
         public static final int WINDOW_FREEZE_TIMEOUT = 11;
 
         public static final int PERSIST_ANIMATION_SCALE = 14;
-        public static final int FORCE_GC = 15;
         public static final int ENABLE_SCREEN = 16;
         public static final int APP_FREEZE_TIMEOUT = 17;
         public static final int REPORT_WINDOWS_CHANGE = 19;
@@ -5363,26 +5378,6 @@
                     break;
                 }
 
-                case FORCE_GC: {
-                    synchronized (mGlobalLock) {
-                        // Since we're holding both mWindowMap and mAnimator we don't need to
-                        // hold mAnimator.mLayoutToAnim.
-                        if (mAnimator.isAnimationScheduled()) {
-                            // If we are animating, don't do the gc now but
-                            // delay a bit so we don't interrupt the animation.
-                            sendEmptyMessageDelayed(H.FORCE_GC, 2000);
-                            return;
-                        }
-                        // If we are currently rotating the display, it will
-                        // schedule a new message when done.
-                        if (mDisplayFrozen) {
-                            return;
-                        }
-                    }
-                    Runtime.getRuntime().gc();
-                    break;
-                }
-
                 case ENABLE_SCREEN: {
                     performEnableScreen();
                     break;
@@ -6241,14 +6236,6 @@
         // now to catch that.
         configChanged = displayContent != null && displayContent.updateOrientation();
 
-        // A little kludge: a lot could have happened while the
-        // display was frozen, so now that we are coming back we
-        // do a gc so that any remote references the system
-        // processes holds on others can be released if they are
-        // no longer needed.
-        mH.removeMessages(H.FORCE_GC);
-        mH.sendEmptyMessageDelayed(H.FORCE_GC, 2000);
-
         mScreenFrozenLock.release();
 
         if (updateRotation && displayContent != null) {
@@ -7917,11 +7904,6 @@
         }
 
         @Override
-        public boolean hasNavigationBar(int displayId) {
-            return WindowManagerService.this.hasNavigationBar(displayId);
-        }
-
-        @Override
         public boolean isHardKeyboardAvailable() {
             synchronized (mGlobalLock) {
                 return mHardKeyboardAvailable;
@@ -8033,7 +8015,8 @@
         }
 
         @Override
-        public void showImePostLayout(IBinder imeTargetWindowToken) {
+        public void showImePostLayout(IBinder imeTargetWindowToken,
+                @Nullable ImeTracker.Token statsToken) {
             synchronized (mGlobalLock) {
                 InputTarget imeTarget = getInputTargetFromWindowTokenLocked(imeTargetWindowToken);
                 if (imeTarget == null) {
@@ -8042,17 +8025,18 @@
                 Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.showImePostLayout", 0);
                 final InsetsControlTarget controlTarget = imeTarget.getImeControlTarget();
                 imeTarget = controlTarget.getWindow();
-                // If InsetsControlTarget doesn't have a window, its using remoteControlTarget which
-                // is controlled by default display
+                // If InsetsControlTarget doesn't have a window, it's using remoteControlTarget
+                // which is controlled by default display
                 final DisplayContent dc = imeTarget != null
                         ? imeTarget.getDisplayContent() : getDefaultDisplayContentLocked();
                 dc.getInsetsStateController().getImeSourceProvider()
-                        .scheduleShowImePostLayout(controlTarget);
+                        .scheduleShowImePostLayout(controlTarget, statsToken);
             }
         }
 
         @Override
-        public void hideIme(IBinder imeTargetWindowToken, int displayId) {
+        public void hideIme(IBinder imeTargetWindowToken, int displayId,
+                @Nullable ImeTracker.Token statsToken) {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.hideIme");
             synchronized (mGlobalLock) {
                 WindowState imeTarget = mWindowMap.get(imeTargetWindowToken);
@@ -8068,10 +8052,15 @@
                     dc.getInsetsStateController().getImeSourceProvider().abortShowImePostLayout();
                 }
                 if (dc != null && dc.getImeTarget(IME_TARGET_CONTROL) != null) {
+                    ImeTracker.get().onProgress(statsToken,
+                            ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
                     ProtoLog.d(WM_DEBUG_IME, "hideIme Control target: %s ",
                             dc.getImeTarget(IME_TARGET_CONTROL));
-                    dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(
-                            WindowInsets.Type.ime(), true /* fromIme */);
+                    dc.getImeTarget(IME_TARGET_CONTROL).hideInsets(WindowInsets.Type.ime(),
+                            true /* fromIme */, statsToken);
+                } else {
+                    ImeTracker.get().onFailed(statsToken,
+                            ImeTracker.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET);
                 }
                 if (dc != null) {
                     dc.getInsetsStateController().getImeSourceProvider().setImeShowing(false);
@@ -8703,11 +8692,12 @@
         h.ownerPid = callingPid;
 
         if (region == null) {
-            h.replaceTouchableRegionWithCrop = true;
+            h.replaceTouchableRegionWithCrop(null);
         } else {
             h.touchableRegion.set(region);
+            h.replaceTouchableRegionWithCrop = false;
+            h.setTouchableRegionCrop(surface);
         }
-        h.setTouchableRegionCrop(null /* use the input surface's bounds */);
 
         final SurfaceControl.Transaction t = mTransactionFactory.get();
         t.setInputWindowInfo(surface, h);
@@ -8913,7 +8903,7 @@
                 outInsetsState.set(state, true /* copySources */);
                 if (WindowState.hasCompatScale(attrs, token, overrideScale)) {
                     final float compatScale = token != null && token.hasSizeCompatBounds()
-                            ? token.getSizeCompatScale() * overrideScale
+                            ? token.getCompatScale() * overrideScale
                             : overrideScale;
                     outInsetsState.scale(1f / compatScale);
                 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java
index 6f2930c..1b70d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java
+++ b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java
@@ -21,7 +21,7 @@
 import static android.os.Process.myTid;
 import static android.os.Process.setThreadPriority;
 
-import static com.android.server.LockGuard.INDEX_WINDOW;;
+import static com.android.server.LockGuard.INDEX_WINDOW;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.AnimationThread;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3590e9c2..a15fc12 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -39,6 +39,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
@@ -48,6 +49,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED;
 import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK;
 import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG;
 import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED;
@@ -144,6 +146,8 @@
     @VisibleForTesting
     final ArrayMap<IBinder, TaskFragment> mLaunchTaskFragments = new ArrayMap<>();
 
+    private final Rect mTmpBounds = new Rect();
+
     WindowOrganizerController(ActivityTaskManagerService atm) {
         mService = atm;
         mGlobalLock = atm.mGlobalLock;
@@ -702,7 +706,7 @@
     }
 
     private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) {
-        int effects = 0;
+        int effects = applyChanges(tr, c, null /* errorCallbackToken */);
         final SurfaceControl.Transaction t = c.getBoundsChangeTransaction();
 
         if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) {
@@ -717,6 +721,10 @@
             effects = TRANSACT_EFFECTS_LIFECYCLE;
         }
 
+        if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING) != 0) {
+            tr.setDragResizing(c.getDragResizing(), DRAG_RESIZE_MODE_FREEFORM);
+        }
+
         final int childWindowingMode = c.getActivityWindowingMode();
         if (childWindowingMode > -1) {
             tr.forAllActivities(a -> { a.setWindowingMode(childWindowingMode); });
@@ -759,6 +767,7 @@
     private int applyDisplayAreaChanges(DisplayArea displayArea,
             WindowContainerTransaction.Change c) {
         final int[] effects = new int[1];
+        effects[0] = applyChanges(displayArea, c, null /* errorCallbackToken */);
 
         if ((c.getChangeMask()
                 & WindowContainerTransaction.Change.CHANGE_IGNORE_ORIENTATION_REQUEST) != 0) {
@@ -779,6 +788,27 @@
         return effects[0];
     }
 
+    private int applyTaskFragmentChanges(@NonNull TaskFragment taskFragment,
+            @NonNull WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) {
+        if (taskFragment.isEmbeddedTaskFragmentInPip()) {
+            // No override from organizer for embedded TaskFragment in a PIP Task.
+            return 0;
+        }
+
+        // When the TaskFragment is resized, we may want to create a change transition for it, for
+        // which we want to defer the surface update until we determine whether or not to start
+        // change transition.
+        mTmpBounds.set(taskFragment.getBounds());
+        taskFragment.deferOrganizedTaskFragmentSurfaceUpdate();
+        final int effects = applyChanges(taskFragment, c, errorCallbackToken);
+        if (taskFragment.shouldStartChangeTransition(mTmpBounds)) {
+            taskFragment.initializeChangeTransition(mTmpBounds);
+        }
+        taskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
+        mTmpBounds.set(0, 0, 0, 0);
+        return effects;
+    }
+
     private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects,
             int syncId, @Nullable Transition transition, boolean isInLockTaskMode,
             @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken,
@@ -967,6 +997,14 @@
                 tf1.setAdjacentTaskFragment(tf2);
                 effects |= TRANSACT_EFFECTS_LIFECYCLE;
 
+                // Clear the focused app if the focused app is no longer visible after reset the
+                // adjacent TaskFragments.
+                if (tf2 == null && tf1.getDisplayContent().mFocusedApp != null
+                        && tf1.hasChild(tf1.getDisplayContent().mFocusedApp)
+                        && !tf1.shouldBeVisible(null /* starting */)) {
+                    tf1.getDisplayContent().setFocusedApp(null);
+                }
+
                 final Bundle bundle = hop.getLaunchOptions();
                 final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams =
                         bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams(
@@ -1080,6 +1118,22 @@
                 effects |= sanitizeAndApplyHierarchyOp(wc, hop);
                 break;
             }
+            case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: {
+                final IBinder fragmentToken = hop.getContainer();
+                final IBinder companionToken = hop.getCompanionContainer();
+                final TaskFragment fragment = mLaunchTaskFragments.get(fragmentToken);
+                final TaskFragment companion = companionToken != null ? mLaunchTaskFragments.get(
+                        companionToken) : null;
+                if (fragment == null || !fragment.isAttached()) {
+                    final Throwable exception = new IllegalArgumentException(
+                            "Not allowed to set companion on invalid fragment tokens");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, fragment, type,
+                            exception);
+                    break;
+                }
+                fragment.setCompanionTaskFragment(companion);
+                break;
+            }
             default: {
                 // The other operations may change task order so they are skipped while in lock
                 // task mode. The above operations are still allowed because they don't move
@@ -1444,20 +1498,15 @@
     private int applyWindowContainerChange(WindowContainer wc,
             WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) {
         sanitizeWindowContainer(wc);
-        if (wc.asTaskFragment() != null && wc.asTaskFragment().isEmbeddedTaskFragmentInPip()) {
-            // No override from organizer for embedded TaskFragment in a PIP Task.
-            return 0;
+        if (wc.asDisplayArea() != null) {
+            return applyDisplayAreaChanges(wc.asDisplayArea(), c);
+        } else if (wc.asTask() != null) {
+            return applyTaskChanges(wc.asTask(), c);
+        } else if (wc.asTaskFragment() != null) {
+            return applyTaskFragmentChanges(wc.asTaskFragment(), c, errorCallbackToken);
+        } else {
+            return applyChanges(wc, c, errorCallbackToken);
         }
-
-        int effects = applyChanges(wc, c, errorCallbackToken);
-
-        if (wc instanceof DisplayArea) {
-            effects |= applyDisplayAreaChanges(wc.asDisplayArea(), c);
-        } else if (wc instanceof Task) {
-            effects |= applyTaskChanges(wc.asTask(), c);
-        }
-
-        return effects;
     }
 
     @Override
@@ -1547,6 +1596,12 @@
         return mTransitionController.mTransitionMetricsReporter;
     }
 
+    @Override
+    public IBinder getApplyToken() {
+        enforceTaskPermission("getApplyToken()");
+        return SurfaceControl.Transaction.getDefaultApplyToken();
+    }
+
     /** Whether the configuration changes are important to report back to an organizer. */
     static boolean configurationsAreEqualForOrganizer(
             Configuration newConfig, @Nullable Configuration oldConfig) {
@@ -1622,6 +1677,12 @@
                 case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT:
                     enforceTaskFragmentOrganized(func, hop.getNewParent(), organizer);
                     break;
+                case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT:
+                    enforceTaskFragmentOrganized(func, hop.getContainer(), organizer);
+                    if (hop.getCompanionContainer() != null) {
+                        enforceTaskFragmentOrganized(func, hop.getCompanionContainer(), organizer);
+                    }
+                    break;
                 case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS:
                     enforceTaskFragmentOrganized(func, hop.getContainer(), organizer);
                     if (hop.getAdjacentRoot() != null) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 36389ea..19409b1 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -24,13 +24,11 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.graphics.GraphicsProtos.dumpPointProto;
-import static android.hardware.display.DisplayManager.SWITCHING_TYPE_NONE;
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.os.PowerManager.DRAW_WAKE_LOCK;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_INVALID;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.SurfaceControl.Transaction;
 import static android.view.SurfaceControl.getGlobalTransaction;
 import static android.view.ViewRootImpl.LOCAL_LAYOUT;
@@ -41,6 +39,7 @@
 import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER;
 import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM;
 import static android.view.WindowCallbacks.RESIZE_MODE_INVALID;
+import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
@@ -233,7 +232,6 @@
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.Surface.Rotation;
 import android.view.SurfaceControl;
@@ -248,6 +246,7 @@
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 import android.window.OnBackInvokedCallbackInfo;
 
@@ -259,6 +258,7 @@
 import com.android.internal.util.ToBooleanFunction;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+import com.android.server.wm.RefreshRatePolicy.FrameRateVote;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 
 import dalvik.annotation.optimization.NeverCompile;
@@ -474,7 +474,7 @@
     // Current transformation being applied.
     float mGlobalScale = 1f;
     float mInvGlobalScale = 1f;
-    float mSizeCompatScale = 1f;
+    float mCompatScale = 1f;
     final float mOverrideScale;
     float mHScale = 1f, mVScale = 1f;
     float mLastHScale = 1f, mLastVScale = 1f;
@@ -767,7 +767,7 @@
      */
     private boolean mIsDimming = false;
 
-    private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
+    private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
     /**
      * Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
@@ -790,7 +790,7 @@
      * preferredDisplayModeId or is part of the high refresh rate deny list.
      * The variable is cached, so we do not send too many updates to SF.
      */
-    float mAppPreferredFrameRate = 0f;
+    FrameRateVote mFrameRateVote = new FrameRateVote();
 
     static final int BLAST_TIMEOUT_DURATION = 5000; /* milliseconds */
 
@@ -833,31 +833,33 @@
      */
     private int mSurfaceTranslationY;
 
+    @Override
+    public boolean isRequestedVisible(@InsetsType int types) {
+        return (mRequestedVisibleTypes & types) != 0;
+    }
+
     /**
-     * Returns the visibility of the given {@link InternalInsetsType type} requested by the client.
+     * Returns requested visible types of insets.
      *
-     * @param type the given {@link InternalInsetsType type}.
-     * @return {@code true} if the type is requested visible.
+     * @return an integer as the requested visible insets types.
      */
     @Override
-    public boolean getRequestedVisibility(@InternalInsetsType int type) {
-        return mRequestedVisibilities.getVisibility(type);
+    public @InsetsType int getRequestedVisibleTypes() {
+        return mRequestedVisibleTypes;
     }
 
     /**
-     * Returns all the requested visibilities.
-     *
-     * @return an {@link InsetsVisibilities} as the requested visibilities.
+     * @see #getRequestedVisibleTypes()
      */
-    InsetsVisibilities getRequestedVisibilities() {
-        return mRequestedVisibilities;
+    void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
+        if (mRequestedVisibleTypes != requestedVisibleTypes) {
+            mRequestedVisibleTypes = requestedVisibleTypes;
+        }
     }
 
-    /**
-     * @see #getRequestedVisibility(int)
-     */
-    void setRequestedVisibilities(InsetsVisibilities visibilities) {
-        mRequestedVisibilities.set(visibilities);
+    @VisibleForTesting
+    void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) {
+        setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
     }
 
     /**
@@ -973,7 +975,7 @@
     boolean isImplicitlyExcludingAllSystemGestures() {
         final boolean stickyHideNav =
                 mAttrs.insetsFlags.behavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
-                        && !getRequestedVisibility(ITYPE_NAVIGATION_BAR);
+                        && !isRequestedVisible(navigationBars());
         return stickyHideNav && mWmService.mConstants.mSystemGestureExcludedByPreQStickyImmersive
                 && mActivityRecord != null && mActivityRecord.mTargetSdk < Build.VERSION_CODES.Q;
     }
@@ -1253,19 +1255,21 @@
 
     void updateGlobalScale() {
         if (hasCompatScale()) {
-            mSizeCompatScale = (mOverrideScale == 1f || mToken.hasSizeCompatBounds())
-                    ? mToken.getSizeCompatScale()
+            mCompatScale = (mOverrideScale == 1f || mToken.hasSizeCompatBounds())
+                    ? mToken.getCompatScale()
                     : 1f;
-            mGlobalScale = mSizeCompatScale * mOverrideScale;
+            mGlobalScale = mCompatScale * mOverrideScale;
             mInvGlobalScale = 1f / mGlobalScale;
             return;
         }
 
-        mGlobalScale = mInvGlobalScale = mSizeCompatScale = 1f;
+        mGlobalScale = mInvGlobalScale = mCompatScale = 1f;
     }
 
-    float getSizeCompatScale() {
-        return mSizeCompatScale;
+    float getCompatScaleForClient() {
+        // If this window in the size compat mode. The scaling is fully controlled at the server
+        // side. The client doesn't need to take it into account.
+        return mToken.hasSizeCompatBounds() ? 1f : mCompatScale;
     }
 
     /**
@@ -1534,10 +1538,11 @@
             mWmService.makeWindowFreezingScreenIfNeededLocked(this);
 
             // If the orientation is changing, or we're starting or ending a drag resizing action,
-            // then we need to hold off on unfreezing the display until this window has been
-            // redrawn; to do that, we need to go through the process of getting informed by the
-            // application when it has finished drawing.
-            if (getOrientationChanging() || dragResizingChanged) {
+            // or we're resizing an embedded Activity, then we need to hold off on unfreezing the
+            // display until this window has been redrawn; to do that, we need to go through the
+            // process of getting informed by the application when it has finished drawing.
+            if (getOrientationChanging() || dragResizingChanged
+                    || isEmbeddedActivityResizeChanged()) {
                 if (dragResizingChanged) {
                     ProtoLog.v(WM_DEBUG_RESIZE,
                             "Resize start waiting for draw, "
@@ -1718,7 +1723,7 @@
     InsetsState getInsetsStateWithVisibilityOverride() {
         final InsetsState state = new InsetsState(getInsetsState());
         for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
-            final boolean requestedVisible = getRequestedVisibility(type);
+            final boolean requestedVisible = isRequestedVisible(InsetsState.toPublicType(type));
             InsetsSource source = state.peekSource(type);
             if (source != null && source.isVisible() != requestedVisible) {
                 source = new InsetsSource(source);
@@ -1841,8 +1846,8 @@
      * @return {@code true} if one or more windows have been displayed, else false.
      */
     boolean hasAppShownWindows() {
-        return mActivityRecord != null
-                && (mActivityRecord.firstWindowDrawn || mActivityRecord.startingDisplayed);
+        return mActivityRecord != null && (mActivityRecord.firstWindowDrawn
+                || mActivityRecord.isStartingWindowDisplayed());
     }
 
     @Override
@@ -3862,7 +3867,8 @@
                 outFrames.attachedFrame.scale(mInvGlobalScale);
             }
         }
-        outFrames.sizeCompatScale = mSizeCompatScale;
+
+        outFrames.compatScale = getCompatScaleForClient();
 
         // Note: in the cases where the window is tied to an activity, we should not send a
         // configuration update when the window has requested to be hidden. Doing so can lead to
@@ -4012,7 +4018,7 @@
             mClient.insetsControlChanged(getCompatInsetsState(),
                     stateController.getControlsForDispatch(this));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to deliver inset state change to w=" + this, e);
+            Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
         }
     }
 
@@ -4022,20 +4028,30 @@
     }
 
     @Override
-    public void showInsets(@InsetsType int types, boolean fromIme) {
+    public void showInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         try {
-            mClient.showInsets(types, fromIme);
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
+            mClient.showInsets(types, fromIme, statsToken);
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to deliver showInsets", e);
+            ImeTracker.get().onFailed(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS);
         }
     }
 
     @Override
-    public void hideInsets(@InsetsType int types, boolean fromIme) {
+    public void hideInsets(@InsetsType int types, boolean fromIme,
+            @Nullable ImeTracker.Token statsToken) {
         try {
-            mClient.hideInsets(types, fromIme);
+            ImeTracker.get().onProgress(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
+            mClient.hideInsets(types, fromIme, statsToken);
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to deliver showInsets", e);
+            Slog.w(TAG, "Failed to deliver hideInsets", e);
+            ImeTracker.get().onFailed(statsToken,
+                    ImeTracker.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS);
         }
     }
 
@@ -4143,6 +4159,20 @@
         return mActivityRecord == null || mActivityRecord.isFullyTransparentBarAllowed(frame);
     }
 
+    /**
+     * Whether this window belongs to a resizing embedded activity.
+     */
+    private boolean isEmbeddedActivityResizeChanged() {
+        if (mActivityRecord == null || !isVisibleRequested()) {
+            // No need to update if the window is in the background.
+            return false;
+        }
+
+        final TaskFragment embeddedTaskFragment = mActivityRecord.getOrganizedTaskFragment();
+        return embeddedTaskFragment != null
+                && mDisplayContent.mChangingContainers.contains(embeddedTaskFragment);
+    }
+
     boolean isDragResizeChanged() {
         return mDragResizing != computeDragResizing();
     }
@@ -4436,9 +4466,10 @@
         pw.println(prefix + "keepClearAreas: restricted=" + mKeepClearAreas
                           + ", unrestricted=" + mUnrestrictedKeepClearAreas);
         if (dumpAll) {
-            final String visibilityString = mRequestedVisibilities.toString();
-            if (!visibilityString.isEmpty()) {
-                pw.println(prefix + "Requested visibilities: " + visibilityString);
+            if (mRequestedVisibleTypes != WindowInsets.Type.defaultVisible()) {
+                pw.println(prefix + "Requested non-default-visibility types: "
+                        + WindowInsets.Type.toString(
+                                mRequestedVisibleTypes ^ WindowInsets.Type.defaultVisible()));
             }
         }
 
@@ -5474,18 +5505,12 @@
                     mFrameRateSelectionPriority);
         }
 
-        // If refresh rate switching is disabled there is no point to set the frame rate on the
-        // surface as the refresh rate will be limited by display manager to a single value
-        // and SurfaceFlinger wouldn't be able to change it anyways.
-        if (mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType()
-                != SWITCHING_TYPE_NONE) {
-            final float refreshRate = refreshRatePolicy.getPreferredRefreshRate(this);
-            if (mAppPreferredFrameRate != refreshRate) {
-                mAppPreferredFrameRate = refreshRate;
-                getPendingTransaction().setFrameRate(
-                        mSurfaceControl, mAppPreferredFrameRate,
-                        Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS);
-            }
+        boolean voteChanged = refreshRatePolicy.updateFrameRateVote(this);
+        if (voteChanged) {
+            getPendingTransaction().setFrameRate(
+                    mSurfaceControl, mFrameRateVote.mRefreshRate,
+                    mFrameRateVote.mCompatibility, Surface.CHANGE_FRAME_RATE_ALWAYS);
+
         }
     }
 
@@ -5711,6 +5736,15 @@
         return super.getAnimationLeashParent();
     }
 
+    @Override
+    public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+        super.onAnimationLeashCreated(t, leash);
+        if (isStartingWindowAssociatedToTask()) {
+            // Make sure the animation leash is still on top of the task.
+            t.setLayer(leash, Integer.MAX_VALUE);
+        }
+    }
+
     // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA
     // then we can drop all negative layering on the windowing side and simply inherit
     // the default implementation here.
@@ -6000,7 +6034,7 @@
             final long duration =
                     SystemClock.elapsedRealtime() - mActivityRecord.mRelaunchStartTime;
             Slog.i(TAG, "finishDrawing of relaunch: " + this + " " + duration + "ms");
-            mActivityRecord.mRelaunchStartTime = 0;
+            mActivityRecord.finishOrAbortReplacingWindow();
         }
         if (mActivityRecord != null && mAttrs.type == TYPE_APPLICATION_STARTING) {
             mWmService.mAtmService.mTaskSupervisor.getActivityMetricsLogger()
@@ -6092,10 +6126,10 @@
         if (mRedrawForSyncReported) {
             return false;
         }
-        // TODO(b/233286785): Remove mIsWallpaper once WallpaperService handles syncId of relayout.
-        if (mInRelayout && !mIsWallpaper) {
-            // The last sync seq id will return to the client, so there is no need to request the
-            // client to redraw.
+        if (mInRelayout && (mPrepareSyncSeqId > 0 || (mViewVisibility == View.VISIBLE
+                && mWinAnimator.mDrawState == DRAW_PENDING))) {
+            // The client will report draw if it gets the sync seq id from relayout or it is
+            // drawing for being visible, then no need to request redraw.
             return false;
         }
         return useBLASTSync();
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 5c0557f..a0ba8fd 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -374,13 +374,6 @@
     }
 
     void destroySurfaceLocked(SurfaceControl.Transaction t) {
-        final ActivityRecord activity = mWin.mActivityRecord;
-        if (activity != null) {
-            if (mWin == activity.mStartingWindow) {
-                activity.startingDisplayed = false;
-            }
-        }
-
         if (mSurfaceController == null) {
             return;
         }
@@ -602,11 +595,17 @@
             return true;
         }
 
-        final boolean isImeWindow = mWin.mAttrs.type == TYPE_INPUT_METHOD;
-        if (isEntrance && isImeWindow) {
+        if (mWin.mAttrs.type == TYPE_INPUT_METHOD) {
             mWin.getDisplayContent().adjustForImeIfNeeded();
-            mWin.setDisplayLayoutNeeded();
-            mService.mWindowPlacerLocked.requestTraversal();
+            if (isEntrance) {
+                mWin.setDisplayLayoutNeeded();
+                mService.mWindowPlacerLocked.requestTraversal();
+            }
+        }
+
+        if (mWin.mControllableInsetProvider != null) {
+            // All our animations should be driven by the insets control target.
+            return false;
         }
 
         // Only apply an animation if the display isn't frozen.  If it is
@@ -654,14 +653,10 @@
                 Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
                 mAnimationIsEntrance = isEntrance;
             }
-        } else if (!isImeWindow) {
+        } else {
             mWin.cancelAnimation();
         }
 
-        if (!isEntrance && isImeWindow) {
-            mWin.getDisplayContent().adjustForImeIfNeeded();
-        }
-
         return mWin.isAnimating(0 /* flags */, ANIMATION_TYPE_WINDOW_ANIMATION);
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 8055590..f2527b6 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -258,7 +258,7 @@
      * @return The scale for applications running in compatibility mode. Multiply the size in the
      *         application by this scale will be the size in the screen.
      */
-    float getSizeCompatScale() {
+    float getCompatScale() {
         return mDisplayContent.mCompatibleScreenScale;
     }
 
@@ -448,14 +448,8 @@
         if (mFixedRotationTransformState != null) {
             mFixedRotationTransformState.disassociate(this);
         }
-        // TODO(b/233855302): Remove TaskFragment override if the DisplayContent uses the same
-        //  bounds for screenLayout calculation.
-        final Configuration overrideConfig = new Configuration(config);
-        overrideConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
-                overrideConfig.screenLayout, overrideConfig.screenWidthDp,
-                overrideConfig.screenHeightDp);
         mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
-                overrideConfig, mDisplayContent.getRotation());
+                new Configuration(config), mDisplayContent.getRotation());
         mFixedRotationTransformState.mAssociatedTokens.add(this);
         mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames);
         onFixedRotationStatePrepared();
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index efcca5d..416d042 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -355,7 +355,7 @@
             ProtoOutputStream proto = new ProtoOutputStream();
             proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
             long timeOffsetNs =
-                    TimeUnit.NANOSECONDS.convert(System.currentTimeMillis(), TimeUnit.NANOSECONDS)
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
                     - SystemClock.elapsedRealtimeNanos();
             proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
             mBuffer.writeTraceToFile(mTraceFile, proto);
diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
index b93b8d8..c11a6d0 100644
--- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
+++ b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
@@ -16,24 +16,11 @@
 
 package com.android.server.wm.utils;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
 import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 
-import android.graphics.Color;
-import android.graphics.ColorSpace;
 import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
-import android.view.Display;
 import android.view.Surface;
-import android.view.SurfaceControl;
-import android.window.ScreenCapture;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
 
 
 /** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/
@@ -46,89 +33,6 @@
         return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    public static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    public static float getLumaOfSurfaceControl(Display display, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Point size = new Point();
-        display.getSize(size);
-        Rect crop = new Rect(0, 0, size.x, size.y);
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return RotationAnimationUtils.getMedianBorderLuma(buffer.getHardwareBuffer(),
-                buffer.getColorSpace());
-    }
-
     public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) {
         switch (rotation) {
             case Surface.ROTATION_0:
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 5fa8dcc..57b977c 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -152,7 +152,7 @@
         "android.hardware.broadcastradio@1.0",
         "android.hardware.broadcastradio@1.1",
         "android.hardware.contexthub@1.0",
-        "android.hardware.gnss-V2-cpp",
+        "android.hardware.gnss-V3-cpp",
         "android.hardware.gnss@1.0",
         "android.hardware.gnss@1.1",
         "android.hardware.gnss@2.0",
@@ -170,7 +170,7 @@
         "android.hardware.power@1.1",
         "android.hardware.power@1.2",
         "android.hardware.power@1.3",
-        "android.hardware.power-V3-cpp",
+        "android.hardware.power-V4-cpp",
         "android.hardware.power.stats@1.0",
         "android.hardware.power.stats-V1-ndk",
         "android.hardware.thermal@1.0",
diff --git a/services/core/jni/com_android_server_am_BatteryStatsService.cpp b/services/core/jni/com_android_server_am_BatteryStatsService.cpp
index 16eaa77..3678ced 100644
--- a/services/core/jni/com_android_server_am_BatteryStatsService.cpp
+++ b/services/core/jni/com_android_server_am_BatteryStatsService.cpp
@@ -98,7 +98,7 @@
 public:
     binder::Status notifyWakeup(bool success,
                                 const std::vector<std::string>& wakeupReasons) override {
-        ALOGI("In wakeup_callback: %s", success ? "resumed from suspend" : "suspend aborted");
+        ALOGV("In wakeup_callback: %s", success ? "resumed from suspend" : "suspend aborted");
         bool reasonsCaptured = false;
         {
             std::unique_lock<std::mutex> reasonsLock(mReasonsMutex, std::defer_lock);
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index 000cb83..d975760 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -34,6 +34,7 @@
 #include "jni.h"
 
 using android::hardware::power::IPowerHintSession;
+using android::hardware::power::SessionHint;
 using android::hardware::power::WorkDuration;
 
 using android::base::StringPrintf;
@@ -81,6 +82,11 @@
     appSession->reportActualWorkDuration(actualDurations);
 }
 
+static void sendHint(int64_t session_ptr, SessionHint hint) {
+    sp<IPowerHintSession> appSession = reinterpret_cast<IPowerHintSession*>(session_ptr);
+    appSession->sendHint(hint);
+}
+
 static int64_t getHintSessionPreferredRate() {
     int64_t rate = -1;
     auto result = gPowerHalController.getHintSessionPreferredRate();
@@ -139,6 +145,10 @@
     reportActualWorkDuration(session_ptr, actualList);
 }
 
+static void nativeSendHint(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint hint) {
+    sendHint(session_ptr, static_cast<SessionHint>(hint));
+}
+
 static jlong nativeGetHintSessionPreferredRate(JNIEnv* /* env */, jclass /* clazz */) {
     return static_cast<jlong>(getHintSessionPreferredRate());
 }
@@ -153,6 +163,7 @@
         {"nativeCloseHintSession", "(J)V", (void*)nativeCloseHintSession},
         {"nativeUpdateTargetWorkDuration", "(JJ)V", (void*)nativeUpdateTargetWorkDuration},
         {"nativeReportActualWorkDuration", "(J[J[J)V", (void*)nativeReportActualWorkDuration},
+        {"nativeSendHint", "(JI)V", (void*)nativeSendHint},
         {"nativeGetHintSessionPreferredRate", "()J", (void*)nativeGetHintSessionPreferredRate},
 };
 
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 3f380e7..5d0551b 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;
@@ -259,10 +260,9 @@
 
 // --- NativeInputManager ---
 
-class NativeInputManager : public virtual RefBase,
-    public virtual InputReaderPolicyInterface,
-    public virtual InputDispatcherPolicyInterface,
-    public virtual PointerControllerPolicyInterface {
+class NativeInputManager : public virtual InputReaderPolicyInterface,
+                           public virtual InputDispatcherPolicyInterface,
+                           public virtual PointerControllerPolicyInterface {
 protected:
     virtual ~NativeInputManager();
 
@@ -299,6 +299,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 +313,7 @@
                                                            int32_t surfaceRotation) override;
 
     TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr);
+    void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
 
     /* --- InputDispatcherPolicyInterface implementation --- */
 
@@ -370,37 +372,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 +421,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);
@@ -457,6 +449,10 @@
         dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n",
                              mLocked.pointerCaptureRequest.enable ? "Enabled" : "Disabled",
                              mLocked.pointerCaptureRequest.seq);
+        auto pointerController = mLocked.pointerController.lock();
+        if (pointerController != nullptr) {
+            pointerController->dump(dump);
+        }
     }
     dump += "\n";
 
@@ -1177,6 +1173,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;
@@ -1188,8 +1191,9 @@
                 static_cast<const KeyEvent*>(inputEvent));
         break;
     case AINPUT_EVENT_TYPE_MOTION:
-        inputEventObj = android_view_MotionEvent_obtainAsCopy(env,
-                static_cast<const MotionEvent*>(inputEvent));
+        inputEventObj =
+                android_view_MotionEvent_obtainAsCopy(env,
+                                                      static_cast<const MotionEvent&>(*inputEvent));
         break;
     default:
         return true; // dispatch the event normally
@@ -1487,6 +1491,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 =
@@ -1512,8 +1520,14 @@
         return 0;
     }
 
-    NativeInputManager* im = new NativeInputManager(serviceObj, messageQueue->getLooper());
-    im->incStrong(0);
+    static std::once_flag nativeInitialize;
+    NativeInputManager* im = nullptr;
+    std::call_once(nativeInitialize, [&]() {
+        // Create the NativeInputManager, which should not be destroyed or deallocated for the
+        // lifetime of the process.
+        im = new NativeInputManager(serviceObj, messageQueue->getLooper());
+    });
+    LOG_ALWAYS_FATAL_IF(im == nullptr, "NativeInputManager was already initialized.");
     return reinterpret_cast<jlong>(im);
 }
 
@@ -2326,6 +2340,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 +2428,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 +2490,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/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 9fa23c2..e1de05c 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -482,6 +482,17 @@
     agnssRilIface->setSetId(type, setid_string);
 }
 
+static void android_location_gnss_hal_GnssNative_inject_ni_supl_message_data(JNIEnv* env, jclass,
+                                                                             jbyteArray data,
+                                                                             jint length,
+                                                                             jint slotIndex) {
+    if (agnssRilIface == nullptr) {
+        ALOGE("%s: IAGnssRil interface not available.", __func__);
+        return;
+    }
+    agnssRilIface->injectNiSuplMessageData(data, length, slotIndex);
+}
+
 static jint android_location_gnss_hal_GnssNative_read_nmea(JNIEnv* env, jclass,
                                                            jbyteArray nmeaArray, jint buffer_size) {
     return gnssHal->readNmea(nmeaArray, buffer_size);
@@ -974,6 +985,8 @@
                  android_location_gnss_hal_GnssNative_agps_set_reference_location_cellid)},
         {"native_set_agps_server", "(ILjava/lang/String;I)V",
          reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_set_agps_server)},
+        {"native_inject_ni_supl_message_data", "([BII)V",
+         reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_inject_ni_supl_message_data)},
         {"native_send_ni_response", "(II)V",
          reinterpret_cast<void*>(android_location_gnss_hal_GnssNative_send_ni_response)},
         {"native_get_internal_state", "()Ljava/lang/String;",
diff --git a/services/core/jni/gnss/AGnssRil.cpp b/services/core/jni/gnss/AGnssRil.cpp
index 424ffd4..c7a1af7 100644
--- a/services/core/jni/gnss/AGnssRil.cpp
+++ b/services/core/jni/gnss/AGnssRil.cpp
@@ -55,13 +55,13 @@
         case IAGnssRil::AGnssRefLocationType::UMTS_CELLID:
         case IAGnssRil::AGnssRefLocationType::LTE_CELLID:
         case IAGnssRil::AGnssRefLocationType::NR_CELLID:
-            location.cellID.mcc = mcc;
-            location.cellID.mnc = mnc;
-            location.cellID.lac = lac;
-            location.cellID.cid = cid;
-            location.cellID.tac = tac;
-            location.cellID.pcid = pcid;
-            location.cellID.arfcn = arfcn;
+            location.cellID.mcc = static_cast<int>(mcc);
+            location.cellID.mnc = static_cast<int>(mnc);
+            location.cellID.lac = static_cast<int>(lac);
+            location.cellID.cid = static_cast<long>(cid);
+            location.cellID.tac = static_cast<int>(tac);
+            location.cellID.pcid = static_cast<int>(pcid);
+            location.cellID.arfcn = static_cast<int>(arfcn);
             break;
         default:
             ALOGE("Unknown cellid (%s:%d).", __FUNCTION__, __LINE__);
@@ -84,8 +84,19 @@
     networkAttributes.capabilities = static_cast<int32_t>(capabilities),
     networkAttributes.apn = jniApn.c_str();
 
-    auto result = mIAGnssRil->updateNetworkState(networkAttributes);
-    return checkAidlStatus(result, "IAGnssRilAidl updateNetworkState() failed.");
+    auto status = mIAGnssRil->updateNetworkState(networkAttributes);
+    return checkAidlStatus(status, "IAGnssRilAidl updateNetworkState() failed.");
+}
+
+jboolean AGnssRil::injectNiSuplMessageData(const jbyteArray& msgData, jint length, jint slotIndex) {
+    JNIEnv* env = getJniEnv();
+    jbyte* bytes = reinterpret_cast<jbyte*>(env->GetPrimitiveArrayCritical(msgData, 0));
+    auto status = mIAGnssRil->injectNiSuplMessageData(std::vector<uint8_t>((const uint8_t*)bytes,
+                                                                           (const uint8_t*)bytes +
+                                                                                   length),
+                                                      static_cast<int>(slotIndex));
+    env->ReleasePrimitiveArrayCritical(msgData, bytes, JNI_ABORT);
+    return checkAidlStatus(status, "IAGnssRil injectNiSuplMessageData() failed.");
 }
 
 // Implementation of AGnssRil_V1_0
@@ -106,20 +117,24 @@
     return checkHidlReturn(result, "IAGnssRil_V1_0 setSetId() failed.");
 }
 
-jboolean AGnssRil_V1_0::setRefLocation(jint type, jint mcc, jint mnc, jint lac, jlong cid, jint,
-                                       jint, jint) {
+jboolean AGnssRil_V1_0::setRefLocation(jint type, jint mcc, jint mnc, jint lac, jlong cid, jint tac,
+                                       jint pcid, jint) {
     IAGnssRil_V1_0::AGnssRefLocation location;
-    switch (static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type)) {
+    location.type = static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type);
+
+    switch (location.type) {
         case IAGnssRil_V1_0::AGnssRefLocationType::GSM_CELLID:
         case IAGnssRil_V1_0::AGnssRefLocationType::UMTS_CELLID:
-            location.type = static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type);
-            location.cellID.mcc = mcc;
-            location.cellID.mnc = mnc;
-            location.cellID.lac = lac;
-            location.cellID.cid = cid;
+        case IAGnssRil_V1_0::AGnssRefLocationType::LTE_CELLID:
+            location.cellID.mcc = static_cast<uint16_t>(mcc);
+            location.cellID.mnc = static_cast<uint16_t>(mnc);
+            location.cellID.lac = static_cast<uint16_t>(lac);
+            location.cellID.cid = static_cast<uint32_t>(cid);
+            location.cellID.tac = static_cast<uint16_t>(tac);
+            location.cellID.pcid = static_cast<uint16_t>(pcid);
             break;
         default:
-            ALOGE("Neither a GSM nor a UMTS cellid (%s:%d).", __FUNCTION__, __LINE__);
+            ALOGE("Unknown cellid (%s:%d).", __FUNCTION__, __LINE__);
             return JNI_FALSE;
             break;
     }
@@ -147,6 +162,11 @@
     return checkHidlReturn(result, "IAGnssRil_V1_0 updateNetworkState() failed.");
 }
 
+jboolean AGnssRil_V1_0::injectNiSuplMessageData(const jbyteArray&, jint, jint) {
+    ALOGI("IAGnssRil_V1_0 interface does not support injectNiSuplMessageData.");
+    return JNI_FALSE;
+}
+
 // Implementation of AGnssRil_V2_0
 
 AGnssRil_V2_0::AGnssRil_V2_0(const sp<IAGnssRil_V2_0>& iAGnssRil)
diff --git a/services/core/jni/gnss/AGnssRil.h b/services/core/jni/gnss/AGnssRil.h
index ce14a77d..b7e0282 100644
--- a/services/core/jni/gnss/AGnssRil.h
+++ b/services/core/jni/gnss/AGnssRil.h
@@ -43,6 +43,8 @@
     virtual jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming,
                                         jboolean available, const jstring& apn, jlong networkHandle,
                                         jshort capabilities) = 0;
+    virtual jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+                                             jint slotIndex) = 0;
 };
 
 class AGnssRil : public AGnssRilInterface {
@@ -55,6 +57,8 @@
     jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
                                 const jstring& apn, jlong networkHandle,
                                 jshort capabilities) override;
+    jboolean injectNiSuplMessageData(const jbyteArray& msgData, jint length,
+                                     jint slotIndex) override;
 
 private:
     const sp<android::hardware::gnss::IAGnssRil> mIAGnssRil;
@@ -70,6 +74,7 @@
     jboolean updateNetworkState(jboolean connected, jint type, jboolean roaming, jboolean available,
                                 const jstring& apn, jlong networkHandle,
                                 jshort capabilities) override;
+    jboolean injectNiSuplMessageData(const jbyteArray&, jint, jint) override;
 
 private:
     const sp<android::hardware::gnss::V1_0::IAGnssRil> mAGnssRil_V1_0;
diff --git a/services/core/jni/gnss/Android.bp b/services/core/jni/gnss/Android.bp
index 0531ae2..f3ba484f62 100644
--- a/services/core/jni/gnss/Android.bp
+++ b/services/core/jni/gnss/Android.bp
@@ -61,7 +61,7 @@
         "libnativehelper",
         "libhardware_legacy",
         "libutils",
-        "android.hardware.gnss-V2-cpp",
+        "android.hardware.gnss-V3-cpp",
         "android.hardware.gnss@1.0",
         "android.hardware.gnss@1.1",
         "android.hardware.gnss@2.0",
diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp
index b931e91..3c1ac1e 100644
--- a/services/core/jni/gnss/GnssCallback.cpp
+++ b/services/core/jni/gnss/GnssCallback.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define LOG_TAG "GnssCallbckJni"
+#define LOG_TAG "GnssCallbackJni"
 
 #include "GnssCallback.h"
 
@@ -31,6 +31,7 @@
 using hardware::Void;
 
 using GnssLocationAidl = android::hardware::gnss::GnssLocation;
+using GnssSignalType = android::hardware::gnss::GnssSignalType;
 using GnssLocation_V1_0 = android::hardware::gnss::V1_0::GnssLocation;
 using GnssLocation_V2_0 = android::hardware::gnss::V2_0::GnssLocation;
 using IGnssCallbackAidl = android::hardware::gnss::IGnssCallback;
@@ -42,11 +43,18 @@
 
 namespace {
 
+jclass class_arrayList;
+jclass class_gnssSignalType;
+
+jmethodID method_arrayListAdd;
+jmethodID method_arrayListCtor;
+jmethodID method_gnssSignalTypeCreate;
 jmethodID method_reportLocation;
 jmethodID method_reportStatus;
 jmethodID method_reportSvStatus;
 jmethodID method_reportNmea;
 jmethodID method_setTopHalCapabilities;
+jmethodID method_setSignalTypeCapabilities;
 jmethodID method_setGnssYearOfHardware;
 jmethodID method_setGnssHardwareModelName;
 jmethodID method_requestLocation;
@@ -88,6 +96,8 @@
     method_reportNmea = env->GetMethodID(clazz, "reportNmea", "(J)V");
 
     method_setTopHalCapabilities = env->GetMethodID(clazz, "setTopHalCapabilities", "(I)V");
+    method_setSignalTypeCapabilities =
+            env->GetMethodID(clazz, "setSignalTypeCapabilities", "(Ljava/util/List;)V");
     method_setGnssYearOfHardware = env->GetMethodID(clazz, "setGnssYearOfHardware", "(I)V");
     method_setGnssHardwareModelName =
             env->GetMethodID(clazz, "setGnssHardwareModelName", "(Ljava/lang/String;)V");
@@ -95,16 +105,58 @@
     method_requestLocation = env->GetMethodID(clazz, "requestLocation", "(ZZ)V");
     method_requestUtcTime = env->GetMethodID(clazz, "requestUtcTime", "()V");
     method_reportGnssServiceDied = env->GetMethodID(clazz, "reportGnssServiceDied", "()V");
+
+    jclass arrayListClass = env->FindClass("java/util/ArrayList");
+    class_arrayList = (jclass)env->NewGlobalRef(arrayListClass);
+    method_arrayListCtor = env->GetMethodID(class_arrayList, "<init>", "()V");
+    method_arrayListAdd = env->GetMethodID(class_arrayList, "add", "(Ljava/lang/Object;)Z");
+
+    jclass gnssSignalTypeClass = env->FindClass("android/location/GnssSignalType");
+    class_gnssSignalType = (jclass)env->NewGlobalRef(gnssSignalTypeClass);
+    method_gnssSignalTypeCreate =
+            env->GetStaticMethodID(class_gnssSignalType, "create",
+                                   "(IDLjava/lang/String;)Landroid/location/GnssSignalType;");
 }
 
 Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) {
-    ALOGD("GnssCallbackAidl::%s: %du\n", __func__, capabilities);
+    ALOGD("%s: %du\n", __func__, capabilities);
     JNIEnv* env = getJniEnv();
     env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities);
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return Status::ok();
 }
 
+namespace {
+
+jobject translateSingleSignalType(JNIEnv* env, const GnssSignalType& signalType) {
+    jstring jstringCodeType = env->NewStringUTF(signalType.codeType.c_str());
+    jobject signalTypeObject =
+            env->CallStaticObjectMethod(class_gnssSignalType, method_gnssSignalTypeCreate,
+                                        signalType.constellation, signalType.carrierFrequencyHz,
+                                        jstringCodeType);
+    env->DeleteLocalRef(jstringCodeType);
+    return signalTypeObject;
+}
+
+} // anonymous namespace
+
+Status GnssCallbackAidl::gnssSetSignalTypeCapabilitiesCb(
+        const std::vector<GnssSignalType>& signalTypes) {
+    ALOGD("%s: %d signal types", __func__, (int)signalTypes.size());
+    JNIEnv* env = getJniEnv();
+    jobject arrayList = env->NewObject(class_arrayList, method_arrayListCtor);
+    for (auto& signalType : signalTypes) {
+        jobject signalTypeObject = translateSingleSignalType(env, signalType);
+        env->CallBooleanMethod(arrayList, method_arrayListAdd, signalTypeObject);
+        // Delete Local Refs
+        env->DeleteLocalRef(signalTypeObject);
+    }
+    env->CallVoidMethod(mCallbacksObj, method_setSignalTypeCapabilities, arrayList);
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+    env->DeleteLocalRef(arrayList);
+    return Status::ok();
+}
+
 Status GnssCallbackAidl::gnssStatusCb(const GnssStatusValue status) {
     JNIEnv* env = getJniEnv();
     env->CallVoidMethod(mCallbacksObj, method_reportStatus, status);
diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h
index a7f96fb..33acec8 100644
--- a/services/core/jni/gnss/GnssCallback.h
+++ b/services/core/jni/gnss/GnssCallback.h
@@ -61,6 +61,8 @@
 class GnssCallbackAidl : public hardware::gnss::BnGnssCallback {
 public:
     binder::Status gnssSetCapabilitiesCb(const int capabilities) override;
+    binder::Status gnssSetSignalTypeCapabilitiesCb(
+            const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override;
     binder::Status gnssStatusCb(const GnssStatusValue status) override;
     binder::Status gnssSvStatusCb(const std::vector<GnssSvInfo>& svInfoList) override;
     binder::Status gnssLocationCb(const hardware::gnss::GnssLocation& location) override;
@@ -180,4 +182,4 @@
 
 } // namespace android::gnss
 
-#endif // _ANDROID_SERVER_GNSS_GNSSCALLBACK_H
\ No newline at end of file
+#endif // _ANDROID_SERVER_GNSS_GNSSCALLBACK_H
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
new file mode 100644
index 0000000..06d8e62
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -0,0 +1,104 @@
+/*
+ * 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.credentials.CreateCredentialRequest;
+import android.credentials.CreateCredentialResponse;
+import android.credentials.CredentialManager;
+import android.credentials.ICreateCredentialCallback;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.os.RemoteException;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Central session for a single {@link CredentialManager#executeCreateCredential} request.
+ * This class listens to the responses from providers, and the UX app, and updates the
+ * provider(s) state maintained in {@link ProviderCreateSession}.
+ */
+public final class CreateRequestSession extends RequestSession<CreateCredentialRequest,
+        ICreateCredentialCallback>
+        implements ProviderSession.ProviderInternalCallback<CreateCredentialResponse> {
+    private static final String TAG = "CreateRequestSession";
+
+    CreateRequestSession(@NonNull Context context, int userId,
+            CreateCredentialRequest request,
+            ICreateCredentialCallback callback,
+            String callingPackage) {
+        super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingPackage);
+    }
+
+    /**
+     * Creates a new provider session, and adds it to list of providers that are contributing to
+     * this request session.
+     *
+     * @return the provider session that was started
+     */
+    @Override
+    @Nullable
+    public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+            RemoteCredentialService remoteCredentialService) {
+        ProviderCreateSession providerCreateSession = ProviderCreateSession
+                .createNewSession(mContext, mUserId, providerInfo,
+                this, remoteCredentialService);
+        if (providerCreateSession != null) {
+            Log.i(TAG, "In startProviderSession - provider session created and being added");
+            mProviders.put(providerCreateSession.getComponentName().flattenToString(),
+                    providerCreateSession);
+        }
+        return providerCreateSession;
+    }
+
+    @Override
+    protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+        mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newCreateRequestInfo(
+                        mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage),
+                providerDataList));
+    }
+
+    private void respondToClientAndFinish(CreateCredentialResponse response) {
+        Log.i(TAG, "respondToClientAndFinish");
+        try {
+            mClientCallback.onResponse(response);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        finishSession();
+    }
+
+    @Override
+    public void onProviderStatusChanged(ProviderSession.Status status,
+            ComponentName componentName) {
+        super.onProviderStatusChanged(status, componentName);
+    }
+
+    @Override
+    public void onFinalResponseReceived(ComponentName componentName,
+            CreateCredentialResponse response) {
+        Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
+        if (response != null) {
+            respondToClientAndFinish(response);
+        }
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 91f5c69..374da1c 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -21,20 +21,32 @@
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.credentials.CreateCredentialRequest;
+import android.credentials.GetCredentialOption;
 import android.credentials.GetCredentialRequest;
+import android.credentials.IClearCredentialSessionCallback;
 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.service.credentials.GetCredentialsRequest;
+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;
+import java.util.stream.Collectors;
+
 /**
  * Entry point service for credential management.
  *
@@ -49,54 +61,155 @@
 
     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 (String serviceName : serviceNames) {
+            Log.i(TAG, "in newServiceListLocked, service: " + serviceName);
+            if (TextUtils.isEmpty(serviceName)) {
+                continue;
+            }
+            try {
+                serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId,
+                        serviceName));
+            } catch (PackageManager.NameNotFoundException | 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);
+                for (CredentialManagerServiceImpl s : services) {
+                    c.accept(s);
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    private List<ProviderSession> initiateProviderSessions(RequestSession session,
+            List<String> requestOptions) {
+        List<ProviderSession> providerSessions = new ArrayList<>();
+        // Invoke all services of a user to initiate a provider session
+        runForUser((service) -> {
+            if (service.isServiceCapable(requestOptions)) {
+                ProviderSession providerSession = service
+                        .initiateProviderSessionForRequest(session);
+                if (providerSession != null) {
+                    providerSessions.add(providerSession);
+                }
+            }
+        });
+        return providerSessions;
+    }
+
     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,
+                    request,
+                    callingPackage);
+
+            // Initiate all provider sessions
+            List<ProviderSession> providerSessions =
+                    initiateProviderSessions(session, request.getGetCredentialOptions()
+                            .stream().map(GetCredentialOption::getType)
+                            .collect(Collectors.toList()));
+            // TODO : Return error when no providers available
+
+            // Iterate over all provider sessions and invoke the request
+            providerSessions.forEach(providerGetSession -> {
+                providerGetSession.getRemoteCredentialService().onGetCredentials(
+                        (GetCredentialsRequest) providerGetSession.getProviderRequest(),
+                        /*callback=*/providerGetSession);
+            });
             return cancelTransport;
         }
 
         @Override
         public ICancellationSignal executeCreateCredential(
                 CreateCredentialRequest request,
-                ICreateCredentialCallback callback) {
+                ICreateCredentialCallback callback,
+                String callingPackage) {
+            Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage);
+            // TODO : Implement cancellation
+            ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+
+            // New request session, scoped for this request only.
+            final CreateRequestSession session = new CreateRequestSession(getContext(),
+                    UserHandle.getCallingUserId(),
+                    request,
+                    callback,
+                    callingPackage);
+
+            // Initiate all provider sessions
+            List<ProviderSession> providerSessions =
+                    initiateProviderSessions(session, List.of(request.getType()));
+            // TODO : Return error when no providers available
+
+            // Iterate over all provider sessions and invoke the request
+            providerSessions.forEach(providerCreateSession -> {
+                providerCreateSession.getRemoteCredentialService().onCreateCredential(
+                        (android.service.credentials.CreateCredentialRequest)
+                                providerCreateSession.getProviderRequest(),
+                        /*callback=*/providerCreateSession);
+            });
+            return cancelTransport;
+        }
+
+        @Override
+        public ICancellationSignal clearCredentialSession(
+                IClearCredentialSessionCallback callback, String callingPackage) {
             // TODO: implement.
-            Log.i(TAG, "executeCreateCredential");
+            Log.i(TAG, "clearCredentialSession");
             ICancellationSignal cancelTransport = CancellationSignal.createTransport();
             return cancelTransport;
         }
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index f45f626..0c32304 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -17,28 +17,82 @@
 package com.android.server.credentials;
 
 import android.annotation.NonNull;
-import android.util.Log;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Slog;
 
 import com.android.server.infra.AbstractPerUserSystemService;
 
+import java.util.List;
+
+
 /**
- * Per-user implementation of {@link CredentialManagerService}
+ * Per-user, per remote service implementation of {@link CredentialManagerService}
  */
-public class CredentialManagerServiceImpl extends
+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 {
+        // 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
+     * Starts a provider session and associates it with the given request session. */
+    @Nullable
+    public ProviderSession initiateProviderSessionForRequest(
+            RequestSession requestSession) {
+        Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl");
+        if (mInfo == null) {
+            Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, "
+                    + "but mInfo is null. This shouldn't happen");
+            return null;
+        }
+        final RemoteCredentialService remoteService = new RemoteCredentialService(
+                getContext(), mInfo.getServiceInfo().getComponentName(), mUserId);
+        ProviderSession providerSession =
+                requestSession.initiateProviderSession(mInfo, remoteService);
+        return providerSession;
+    }
+
+    /** Return true if at least one capability found. */
+    boolean isServiceCapable(List<String> requestedOptions) {
+        if (mInfo == null) {
+            Slog.i(TAG, "in isServiceCapable, mInfo is null");
+            return false;
+        }
+        for (String capability : requestedOptions) {
+            if (mInfo.hasCapability(capability)) {
+                Slog.i(TAG, "Provider can handle: " + capability);
+                return true;
+            } else {
+                Slog.i(TAG, "Provider cannot handle: " + capability);
+            }
+        }
+        return false;
     }
 }
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..e889594
--- /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.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;
+    // TODO : Use for starting the activity for this user
+    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 == UserSelectionDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION) {
+            UserSelectionDialogResult selection = UserSelectionDialogResult
+                    .fromResultData(resultData);
+            if (selection != null) {
+                mCallbacks.onUiSelection(selection);
+            } else {
+                Slog.i(TAG, "No selection found in UI result");
+            }
+        } else if (resultCode == UserSelectionDialogResult.RESULT_CODE_DIALOG_CANCELED) {
+            mCallbacks.onUiCancellation();
+        }
+    }
+
+    /**
+     * 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 onUiCancellation();
+    }
+    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, new ArrayList<>(),
+                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..8a698ca
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.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 com.android.server.credentials;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
+import android.credentials.IGetCredentialCallback;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.os.RemoteException;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * 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<GetCredentialRequest,
+        IGetCredentialCallback>
+        implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
+    private static final String TAG = "GetRequestSession";
+
+    public GetRequestSession(Context context, int userId,
+            IGetCredentialCallback callback, GetCredentialRequest request,
+            String callingPackage) {
+        super(context, userId, request, callback, RequestInfo.TYPE_GET, callingPackage);
+    }
+
+    /**
+     * Creates a new provider session, and adds it list of providers that are contributing to
+     * this session.
+     * @return the provider session created within this request session, for the given provider
+     * info.
+     */
+    @Override
+    @Nullable
+    public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+            RemoteCredentialService remoteCredentialService) {
+        ProviderGetSession providerGetSession = ProviderGetSession
+                .createNewSession(mContext, mUserId, providerInfo,
+                        this, remoteCredentialService);
+        if (providerGetSession != null) {
+            Log.i(TAG, "In startProviderSession - provider session created and being added");
+            mProviders.put(providerGetSession.getComponentName().flattenToString(),
+                    providerGetSession);
+        }
+        return providerGetSession;
+    }
+
+    @Override
+    protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+        mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo(
+                mRequestId, null, mIsFirstUiTurn, ""),
+                providerDataList));
+    }
+
+    @Override // from provider session
+    public void onProviderStatusChanged(ProviderSession.Status status,
+            ComponentName componentName) {
+        super.onProviderStatusChanged(status, componentName);
+    }
+
+
+    @Override
+    public void onFinalResponseReceived(ComponentName componentName,
+            GetCredentialResponse response) {
+        Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
+        if (response != null) {
+            respondToClientAndFinish(response);
+        }
+    }
+
+    private void respondToClientAndFinish(GetCredentialResponse response) {
+        Log.i(TAG, "respondToClientAndFinish");
+        try {
+            mClientCallback.onResponse(response);
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        finishSession();
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
new file mode 100644
index 0000000..4cdc457
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/PendingIntentResultHandler.java
@@ -0,0 +1,69 @@
+/*
+ * 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.app.Activity;
+import android.content.Intent;
+import android.credentials.CreateCredentialResponse;
+import android.credentials.Credential;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.service.credentials.CredentialProviderService;
+import android.service.credentials.CredentialsDisplayContent;
+
+/**
+ * Helper class for setting up pending intent, and extracting objects from it.
+ *
+ * @hide
+ */
+public class PendingIntentResultHandler {
+    /** Returns true if the result is successful and may contain result extras. */
+    public static boolean isSuccessfulResponse(
+            ProviderPendingIntentResponse pendingIntentResponse) {
+        //TODO: Differentiate based on extra_error in the resultData
+        return pendingIntentResponse.getResultCode() == Activity.RESULT_OK;
+    }
+
+    /** Extracts the {@link CredentialsDisplayContent} object added to the result data. */
+    public static CredentialsDisplayContent extractCredentialsDisplayContent(Intent resultData) {
+        if (resultData == null) {
+            return null;
+        }
+        return resultData.getParcelableExtra(
+                CredentialProviderService.EXTRA_GET_CREDENTIALS_DISPLAY_CONTENT,
+                CredentialsDisplayContent.class);
+    }
+
+    /** Extracts the {@link CreateCredentialResponse} object added to the result data. */
+    public static CreateCredentialResponse extractCreateCredentialResponse(Intent resultData) {
+        if (resultData == null) {
+            return null;
+        }
+        return resultData.getParcelableExtra(
+                CredentialProviderService.EXTRA_CREATE_CREDENTIAL_RESPONSE,
+                CreateCredentialResponse.class);
+    }
+
+    /** Extracts the {@link Credential} object added to the result data. */
+    public static Credential extractCredential(Intent resultData) {
+        if (resultData == null) {
+            return null;
+        }
+        return resultData.getParcelableExtra(
+                CredentialProviderService.EXTRA_GET_CREDENTIAL,
+                Credential.class);
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
new file mode 100644
index 0000000..bf37bd2
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -0,0 +1,242 @@
+/*
+ * 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.annotation.UserIdInt;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.credentials.ui.CreateCredentialProviderData;
+import android.credentials.ui.Entry;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.os.Bundle;
+import android.service.credentials.CreateCredentialRequest;
+import android.service.credentials.CreateCredentialResponse;
+import android.service.credentials.CredentialProviderInfo;
+import android.service.credentials.CredentialProviderService;
+import android.service.credentials.SaveEntry;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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 ProviderCreateSession extends ProviderSession<
+        CreateCredentialRequest, CreateCredentialResponse> {
+    private static final String TAG = "ProviderCreateSession";
+
+    // Key to be used as an entry key for a save entry
+    private static final String SAVE_ENTRY_KEY = "save_entry_key";
+
+    @NonNull
+    private final Map<String, SaveEntry> mUiSaveEntries = new HashMap<>();
+    /** The complete request to be used in the second round. */
+    private final CreateCredentialRequest mCompleteRequest;
+
+    /** Creates a new provider session to be used by the request session. */
+    @Nullable public static ProviderCreateSession createNewSession(
+            Context context,
+            @UserIdInt int userId,
+            CredentialProviderInfo providerInfo,
+            CreateRequestSession createRequestSession,
+            RemoteCredentialService remoteCredentialService) {
+        CreateCredentialRequest providerRequest =
+                createProviderRequest(providerInfo.getCapabilities(),
+                        createRequestSession.mClientRequest,
+                        createRequestSession.mClientCallingPackage);
+        if (providerRequest != null) {
+            return new ProviderCreateSession(context, providerInfo, createRequestSession, userId,
+                    remoteCredentialService, providerRequest);
+        }
+        Log.i(TAG, "Unable to create provider session");
+        return null;
+    }
+
+    @Nullable
+    private static CreateCredentialRequest createProviderRequest(List<String> providerCapabilities,
+            android.credentials.CreateCredentialRequest clientRequest,
+            String clientCallingPackage) {
+        String capability = clientRequest.getType();
+        if (providerCapabilities.contains(capability)) {
+            return new CreateCredentialRequest(clientCallingPackage, capability,
+                    clientRequest.getData());
+        }
+        Log.i(TAG, "Unable to create provider request - capabilities do not match");
+        return null;
+    }
+
+    private static CreateCredentialRequest getFirstRoundRequest(CreateCredentialRequest request) {
+        // TODO: Replace with first round bundle from request when ready
+        return new CreateCredentialRequest(
+                request.getCallingPackage(),
+                request.getType(),
+                new Bundle());
+    }
+
+    private ProviderCreateSession(
+            @NonNull Context context,
+            @NonNull CredentialProviderInfo info,
+            @NonNull ProviderInternalCallback callbacks,
+            @UserIdInt int userId,
+            @NonNull RemoteCredentialService remoteCredentialService,
+            @NonNull CreateCredentialRequest request) {
+        super(context, info, getFirstRoundRequest(request), callbacks, userId,
+                remoteCredentialService);
+        // TODO : Replace with proper splitting of request
+        mCompleteRequest = request;
+        setStatus(Status.PENDING);
+    }
+
+    /** Returns the save entry maintained in state by this provider session. */
+    public SaveEntry getUiSaveEntry(String entryId) {
+        return mUiSaveEntries.get(entryId);
+    }
+
+    @Override
+    public void onProviderResponseSuccess(
+            @Nullable CreateCredentialResponse response) {
+        Log.i(TAG, "in onProviderResponseSuccess");
+        onUpdateResponse(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");
+        }
+    }
+
+    private void onUpdateResponse(CreateCredentialResponse response) {
+        Log.i(TAG, "updateResponse with save entries");
+        mProviderResponse = response;
+        updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED);
+    }
+
+    @Override
+    @Nullable protected CreateCredentialProviderData prepareUiData()
+            throws IllegalArgumentException {
+        Log.i(TAG, "In prepareUiData");
+        if (!ProviderSession.isUiInvokingStatus(getStatus())) {
+            Log.i(TAG, "In prepareUiData not in uiInvokingStatus");
+            return null;
+        }
+        final CreateCredentialResponse response = getProviderResponse();
+        if (response == null) {
+            Log.i(TAG, "In prepareUiData response null");
+            throw new IllegalStateException("Response must be in completion mode");
+        }
+        if (response.getSaveEntries() != null) {
+            Log.i(TAG, "In prepareUiData save entries not null");
+            return prepareUiProviderData(
+                    prepareUiSaveEntries(response.getSaveEntries()),
+                    null,
+                    /*isDefaultProvider=*/false);
+        }
+        return null;
+    }
+
+    @Override
+    public void onUiEntrySelected(String entryType, String entryKey,
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
+        switch (entryType) {
+            case SAVE_ENTRY_KEY:
+                if (mUiSaveEntries.containsKey(entryKey)) {
+                    onSaveEntrySelected(providerPendingIntentResponse);
+                } else {
+                    //TODO: Handle properly
+                    Log.i(TAG, "Unexpected save entry key");
+                }
+                break;
+            case REMOTE_ENTRY_KEY:
+                if (mUiRemoteEntry.first.equals(entryKey)) {
+                    onRemoteEntrySelected(providerPendingIntentResponse);
+                } else {
+                    //TODO: Handle properly
+                    Log.i(TAG, "Unexpected remote entry key");
+                }
+                break;
+            default:
+                Log.i(TAG, "Unsupported entry type selected");
+        }
+    }
+
+    private List<Entry> prepareUiSaveEntries(@NonNull List<SaveEntry> saveEntries) {
+        Log.i(TAG, "in populateUiSaveEntries");
+        List<Entry> uiSaveEntries = new ArrayList<>();
+
+        // Populate the save entries
+        for (SaveEntry saveEntry : saveEntries) {
+            String entryId = generateEntryId();
+            mUiSaveEntries.put(entryId, saveEntry);
+            Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
+            uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice(),
+                    saveEntry.getPendingIntent(), setUpFillInIntent(saveEntry.getPendingIntent())));
+        }
+        return uiSaveEntries;
+    }
+
+    private Intent setUpFillInIntent(PendingIntent pendingIntent) {
+        Intent intent = pendingIntent.getIntent();
+        intent.putExtra(CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS,
+                mCompleteRequest.getData());
+        return intent;
+    }
+
+    private CreateCredentialProviderData prepareUiProviderData(List<Entry> saveEntries,
+            Entry remoteEntry, boolean isDefaultProvider) {
+        return new CreateCredentialProviderData.Builder(
+                mComponentName.flattenToString())
+                .setSaveEntries(saveEntries)
+                .setIsDefaultProvider(isDefaultProvider)
+                .build();
+    }
+
+    private void onSaveEntrySelected(ProviderPendingIntentResponse pendingIntentResponse) {
+        if (pendingIntentResponse == null) {
+            return;
+            //TODO: Handle failure if pending intent is null
+        }
+        if (PendingIntentResultHandler.isSuccessfulResponse(pendingIntentResponse)) {
+            android.credentials.CreateCredentialResponse credentialResponse =
+                    PendingIntentResultHandler.extractCreateCredentialResponse(
+                            pendingIntentResponse.getResultData());
+            if (credentialResponse != null) {
+                mCallbacks.onFinalResponseReceived(mComponentName, credentialResponse);
+                return;
+            }
+        }
+        //TODO: Handle failure case is pending intent response does not have a credential
+    }
+}
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..d63cdeb
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -0,0 +1,351 @@
+/*
+ * 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.annotation.UserIdInt;
+import android.content.Context;
+import android.credentials.Credential;
+import android.credentials.GetCredentialOption;
+import android.credentials.GetCredentialResponse;
+import android.credentials.ui.Entry;
+import android.credentials.ui.GetCredentialProviderData;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.service.credentials.Action;
+import android.service.credentials.CredentialEntry;
+import android.service.credentials.CredentialProviderInfo;
+import android.service.credentials.CredentialsDisplayContent;
+import android.service.credentials.GetCredentialsRequest;
+import android.service.credentials.GetCredentialsResponse;
+import android.util.Log;
+import android.util.Pair;
+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.
+ *
+ * @hide
+ */
+public final class ProviderGetSession extends ProviderSession<GetCredentialsRequest,
+        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";
+
+    // Key to be used as the entry key for an action entry
+    private static final String ACTION_ENTRY_KEY = "action_key";
+    // Key to be used as the entry key for the authentication entry
+    private static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
+
+    @NonNull
+    private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
+    @NonNull
+    private final Map<String, Action> mUiActionsEntries = new HashMap<>();
+    @Nullable
+    private Pair<String, Action> mUiAuthenticationAction = null;
+
+    /** Creates a new provider session to be used by the request session. */
+    @Nullable public static ProviderGetSession createNewSession(
+            Context context,
+            @UserIdInt int userId,
+            CredentialProviderInfo providerInfo,
+            GetRequestSession getRequestSession,
+            RemoteCredentialService remoteCredentialService) {
+        GetCredentialsRequest providerRequest =
+                createProviderRequest(providerInfo.getCapabilities(),
+                        getRequestSession.mClientRequest,
+                        getRequestSession.mClientCallingPackage);
+        if (providerRequest != null) {
+            return new ProviderGetSession(context, providerInfo, getRequestSession, userId,
+                    remoteCredentialService, providerRequest);
+        }
+        Log.i(TAG, "Unable to create provider session");
+        return null;
+    }
+
+    @Nullable
+    private static GetCredentialsRequest createProviderRequest(List<String> providerCapabilities,
+            android.credentials.GetCredentialRequest clientRequest,
+            String clientCallingPackage) {
+        List<GetCredentialOption> filteredOptions = new ArrayList<>();
+        for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) {
+            if (providerCapabilities.contains(option.getType())) {
+                Log.i(TAG, "In createProviderRequest - capability found : "
+                        + option.getType());
+                filteredOptions.add(option);
+            } else {
+                Log.i(TAG, "In createProviderRequest - capability not "
+                        + "found : " + option.getType());
+            }
+        }
+        if (!filteredOptions.isEmpty()) {
+            return new GetCredentialsRequest.Builder(clientCallingPackage).setGetCredentialOptions(
+                    filteredOptions).build();
+        }
+        Log.i(TAG, "In createProviderRequest - returning null");
+        return null;
+    }
+
+    public ProviderGetSession(Context context,
+            CredentialProviderInfo info,
+            ProviderInternalCallback callbacks,
+            int userId, RemoteCredentialService remoteCredentialService,
+            GetCredentialsRequest request) {
+        super(context, info, request, callbacks, userId, remoteCredentialService);
+        setStatus(Status.PENDING);
+    }
+
+    /** Returns the credential entry maintained in state by this provider session. */
+    @Nullable
+    public CredentialEntry getCredentialEntry(@NonNull String entryId) {
+        return mUiCredentialEntries.get(entryId);
+    }
+
+    /** Called when the provider response has been updated by an external source. */
+    @Override // Callback from the remote provider
+    public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) {
+        Log.i(TAG, "in onProviderResponseSuccess");
+        onUpdateResponse(response);
+    }
+
+    /** Called when the provider response resulted in a failure. */
+    @Override // Callback from the remote provider
+    public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
+        updateStatusAndInvokeCallback(toStatus(errorCode));
+    }
+
+    /** Called when provider service dies. */
+    @Override // Callback from the remote provider
+    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 // Selection call from the request provider
+    protected void onUiEntrySelected(String entryType, String entryKey,
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
+        switch (entryType) {
+            case CREDENTIAL_ENTRY_KEY:
+                CredentialEntry credentialEntry = mUiCredentialEntries.get(entryKey);
+                if (credentialEntry == null) {
+                    Log.i(TAG, "Credential entry not found");
+                    //TODO: Handle properly
+                    return;
+                }
+                onCredentialEntrySelected(credentialEntry, providerPendingIntentResponse);
+                break;
+            case ACTION_ENTRY_KEY:
+                Action actionEntry = mUiActionsEntries.get(entryKey);
+                if (actionEntry == null) {
+                    Log.i(TAG, "Action entry not found");
+                    //TODO: Handle properly
+                    return;
+                }
+                onActionEntrySelected(providerPendingIntentResponse);
+                break;
+            case AUTHENTICATION_ACTION_ENTRY_KEY:
+                if (mUiAuthenticationAction.first.equals(entryKey)) {
+                    onAuthenticationEntrySelected(providerPendingIntentResponse);
+                } else {
+                    //TODO: Handle properly
+                    Log.i(TAG, "Authentication entry not found");
+                }
+                break;
+            case REMOTE_ENTRY_KEY:
+                if (mUiRemoteEntry.first.equals(entryKey)) {
+                    onRemoteEntrySelected(providerPendingIntentResponse);
+                } else {
+                    //TODO: Handle properly
+                    Log.i(TAG, "Remote entry not found");
+                }
+                break;
+            default:
+                Log.i(TAG, "Unsupported entry type selected");
+        }
+    }
+
+    @Override // Call from request session to data to be shown on the UI
+    @Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
+        Log.i(TAG, "In prepareUiData");
+        if (!ProviderSession.isUiInvokingStatus(getStatus())) {
+            Log.i(TAG, "In prepareUiData - provider does not want to show UI: "
+                    + mComponentName.flattenToString());
+            return null;
+        }
+        if (mProviderResponse == null) {
+            Log.i(TAG, "In prepareUiData response null");
+            throw new IllegalStateException("Response must be in completion mode");
+        }
+        if (mProviderResponse.getAuthenticationAction() != null) {
+            Log.i(TAG, "In prepareUiData - top level authentication mode");
+            return prepareUiProviderData(null, null,
+                    prepareUiAuthenticationAction(mProviderResponse.getAuthenticationAction()),
+                    /*remoteEntry=*/null);
+        }
+        if (mProviderResponse.getCredentialsDisplayContent() != null) {
+            Log.i(TAG, "In prepareUiData displayContent not null");
+            return prepareUiProviderData(prepareUiActionEntries(
+                            mProviderResponse.getCredentialsDisplayContent().getActions()),
+                    prepareUiCredentialEntries(mProviderResponse.getCredentialsDisplayContent()
+                            .getCredentialEntries()),
+                    /*authenticationAction=*/null,
+                    prepareUiRemoteEntry(mProviderResponse
+                            .getCredentialsDisplayContent().getRemoteCredentialEntry()));
+        }
+        return null;
+    }
+
+    private Entry prepareUiRemoteEntry(Action remoteCredentialEntry) {
+        if (remoteCredentialEntry == null) {
+            return null;
+        }
+        String entryId = generateEntryId();
+        Entry remoteEntry = new Entry(REMOTE_ENTRY_KEY, entryId, remoteCredentialEntry.getSlice());
+        mUiRemoteEntry = new Pair<>(entryId, remoteCredentialEntry);
+        return remoteEntry;
+    }
+
+    private Entry prepareUiAuthenticationAction(@NonNull Action authenticationAction) {
+        String entryId = generateEntryId();
+        Entry authEntry = new Entry(
+                AUTHENTICATION_ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice(),
+                authenticationAction.getPendingIntent(), /*fillInIntent=*/null);
+        mUiAuthenticationAction = new Pair<>(entryId, authenticationAction);
+        return authEntry;
+    }
+
+    private List<Entry> prepareUiCredentialEntries(@NonNull
+            List<CredentialEntry> credentialEntries) {
+        Log.i(TAG, "in prepareUiProviderDataWithCredentials");
+        List<Entry> credentialUiEntries = new ArrayList<>();
+
+        // Populate the credential entries
+        for (CredentialEntry credentialEntry : credentialEntries) {
+            String entryId = generateEntryId();
+            mUiCredentialEntries.put(entryId, credentialEntry);
+            Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
+            if (credentialEntry.getPendingIntent() != null) {
+                credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
+                        credentialEntry.getSlice(), credentialEntry.getPendingIntent(),
+                        /*fillInIntent=*/null));
+            } else if (credentialEntry.getCredential() != null) {
+                credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
+                        credentialEntry.getSlice()));
+            } else {
+                Log.i(TAG, "No credential or pending intent. Should not happen.");
+            }
+        }
+        return credentialUiEntries;
+    }
+
+    private List<Entry> prepareUiActionEntries(@Nullable List<Action> actions) {
+        List<Entry> actionEntries = new ArrayList<>();
+        for (Action action : actions) {
+            String entryId = UUID.randomUUID().toString();
+            mUiActionsEntries.put(entryId, action);
+            // TODO : Remove conversion of string to int after change in Entry class
+            actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice(),
+                    action.getPendingIntent(), /*fillInIntent=*/null));
+        }
+        return actionEntries;
+    }
+
+    private GetCredentialProviderData prepareUiProviderData(List<Entry> actionEntries,
+            List<Entry> credentialEntries, Entry authenticationActionEntry,
+            Entry remoteEntry) {
+        return new GetCredentialProviderData.Builder(
+                mComponentName.flattenToString()).setActionChips(actionEntries)
+                .setCredentialEntries(credentialEntries)
+                .setAuthenticationEntry(authenticationActionEntry)
+                .setRemoteEntry(remoteEntry)
+                .build();
+    }
+
+    private void onCredentialEntrySelected(CredentialEntry credentialEntry,
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
+        if (credentialEntry.getCredential() != null) {
+            mCallbacks.onFinalResponseReceived(mComponentName, new GetCredentialResponse(
+                    credentialEntry.getCredential()));
+            return;
+        } else if (providerPendingIntentResponse != null) {
+            if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) {
+                Credential credential = PendingIntentResultHandler.extractCredential(
+                        providerPendingIntentResponse.getResultData());
+                if (credential != null) {
+                    mCallbacks.onFinalResponseReceived(mComponentName,
+                            new GetCredentialResponse(credential));
+                    return;
+                }
+            }
+            // TODO: Handle other pending intent statuses
+        }
+        Log.i(TAG, "CredentialEntry does not have a credential or a pending intent result");
+        // TODO: Propagate failure to client
+    }
+
+    private void onAuthenticationEntrySelected(
+            @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
+        if (providerPendingIntentResponse != null) {
+            if (PendingIntentResultHandler.isSuccessfulResponse(providerPendingIntentResponse)) {
+                CredentialsDisplayContent content = PendingIntentResultHandler
+                        .extractCredentialsDisplayContent(providerPendingIntentResponse
+                                .getResultData());
+                if (content != null) {
+                    onUpdateResponse(GetCredentialsResponse.createWithDisplayContent(content));
+                    return;
+                }
+            }
+            //TODO: Other provider intent statuses
+        }
+        Log.i(TAG, "Display content not present in pending intent result");
+        // TODO: Propagate error to client
+    }
+
+    private void onActionEntrySelected(ProviderPendingIntentResponse
+            providerPendingIntentResponse) {
+        //TODO: Implement if any result expected after an action
+    }
+
+
+    /** Updates the response being maintained in state by this provider session. */
+    private void onUpdateResponse(GetCredentialsResponse response) {
+        mProviderResponse = response;
+        if (response.getAuthenticationAction() != null) {
+            Log.i(TAG , "updateResponse with authentication entry");
+            updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION);
+        } else if (response.getCredentialsDisplayContent() != null) {
+            Log.i(TAG , "updateResponse with credentialEntries");
+            // TODO validate response
+            updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
+        }
+    }
+}
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..4a07f0a
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -0,0 +1,193 @@
+/*
+ * 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.credentials.Credential;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.service.credentials.Action;
+import android.service.credentials.CredentialProviderException;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Pair;
+
+import java.util.UUID;
+
+/**
+ * Provider session storing the state of provider response and ui entries.
+ * @param <T> The request to be sent to the provider
+ * @param <R> The response to be expected from the provider
+ */
+public abstract class ProviderSession<T, R>
+        implements RemoteCredentialService.ProviderCallbacks<R> {
+    // Key to be used as an entry key for a remote entry
+    protected static final String REMOTE_ENTRY_KEY = "remote_entry_key";
+
+    @NonNull protected final Context mContext;
+    @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;
+    @Nullable protected Credential mFinalCredentialResponse;
+    @NonNull protected final T mProviderRequest;
+    @Nullable protected R mProviderResponse;
+    @Nullable protected Pair<String, Action> mUiRemoteEntry;
+
+    /**
+     * Returns true if the given status reflects that the provider state is ready to be shown
+     * on the credMan UI.
+     */
+    public static boolean isUiInvokingStatus(Status status) {
+        return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED
+                || status == Status.REQUIRES_AUTHENTICATION;
+    }
+
+    /**
+     * Returns true if the given status reflects that the provider is waiting for a remote
+     * response.
+     */
+    public static boolean isStatusWaitingForRemoteResponse(Status status) {
+        return status == Status.PENDING;
+    }
+
+    /**
+     * 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 reflects that the provider is done getting the response,
+     * and is ready to return the final credential back to the user.
+     */
+    public static boolean isCompletionStatus(Status status) {
+        return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT
+                || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION;
+    }
+
+    /**
+     * 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.
+     * @param <V> the type of the final response expected
+     */
+    public interface ProviderInternalCallback<V> {
+        /** Called when status changes. */
+        void onProviderStatusChanged(Status status, ComponentName componentName);
+
+        /** Called when the final credential to be returned to the client has been received. */
+        void onFinalResponseReceived(ComponentName componentName, V response);
+    }
+
+    protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
+            @NonNull T providerRequest,
+            @NonNull ProviderInternalCallback callbacks,
+            @NonNull int userId,
+            @NonNull RemoteCredentialService remoteCredentialService) {
+        mContext = context;
+        mProviderInfo = info;
+        mProviderRequest = providerRequest;
+        mCallbacks = callbacks;
+        mUserId = userId;
+        mComponentName = info.getServiceInfo().getComponentName();
+        mRemoteCredentialService = remoteCredentialService;
+    }
+
+    /** Provider status at various states of the request session. */
+    // TODO: Review status values, and adjust where needed
+    enum Status {
+        NOT_STARTED,
+        PENDING,
+        REQUIRES_AUTHENTICATION,
+        CREDENTIALS_RECEIVED,
+        SERVICE_DEAD,
+        CREDENTIAL_RECEIVED_FROM_INTENT,
+        PENDING_INTENT_INVOKED,
+        CREDENTIAL_RECEIVED_FROM_SELECTION,
+        SAVE_ENTRIES_RECEIVED, CANCELED
+    }
+
+    /** Converts exception to a provider session status. */
+    @NonNull
+    public static Status toStatus(
+            @CredentialProviderException.CredentialProviderError int errorCode) {
+        // TODO : Add more mappings as more flows are supported
+        return Status.CANCELED;
+    }
+
+    protected String generateEntryId() {
+        return UUID.randomUUID().toString();
+    }
+
+    public Credential getFinalCredentialResponse() {
+        return  mFinalCredentialResponse;
+    }
+
+    protected void setStatus(@NonNull Status status) {
+        mStatus = status;
+    }
+
+    @NonNull
+    protected Status getStatus() {
+        return mStatus;
+    }
+
+    @NonNull
+    protected ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    @NonNull
+    protected RemoteCredentialService getRemoteCredentialService() {
+        return mRemoteCredentialService;
+    }
+
+    /** Updates the status .*/
+    protected void updateStatusAndInvokeCallback(@NonNull Status status) {
+        setStatus(status);
+        mCallbacks.onProviderStatusChanged(status, mComponentName);
+    }
+
+    protected void onRemoteEntrySelected(
+            ProviderPendingIntentResponse providerPendingIntentResponse) {
+        //TODO: Implement
+    }
+
+    /** Get the request to be sent to the provider. */
+    protected T getProviderRequest() {
+        return mProviderRequest;
+    }
+
+    /** Update the response state stored with the provider session. */
+    @Nullable protected R getProviderResponse() {
+        return mProviderResponse;
+    }
+
+    /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
+     * shown on the UI. */
+    @Nullable protected abstract ProviderData prepareUiData();
+
+    /** Should be overridden to handle the selected entry from the UI. */
+    protected abstract void onUiEntrySelected(String entryType, String entryId,
+            ProviderPendingIntentResponse providerPendingIntentResponse);
+}
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..c2464b5
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -0,0 +1,241 @@
+/*
+ * 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.CreateCredentialRequest;
+import android.service.credentials.CreateCredentialResponse;
+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.ICreateCredentialCallback;
+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), /*bindingFlags=*/0,
+                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 ProviderGetSession} 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(() ->
+                handleExecutionResponse(result, error, cancellationSink, callback)));
+    }
+
+    /** Main entry point to be called for executing a createCredential 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 ProviderCreateSession} class that maintains provider state
+     */
+    public void onCreateCredential(@NonNull CreateCredentialRequest request,
+            ProviderCallbacks<CreateCredentialResponse> callback) {
+        Log.i(TAG, "In onCreateCredential in RemoteCredentialService");
+        AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
+        AtomicReference<CompletableFuture<CreateCredentialResponse>> futureRef =
+                new AtomicReference<>();
+
+        CompletableFuture<CreateCredentialResponse> connectThenExecute = postAsync(service -> {
+            CompletableFuture<CreateCredentialResponse> createCredentialFuture =
+                    new CompletableFuture<>();
+            ICancellationSignal cancellationSignal = service.onCreateCredential(
+                    request, new ICreateCredentialCallback.Stub() {
+                        @Override
+                        public void onSuccess(CreateCredentialResponse response) {
+                            Log.i(TAG, "In onSuccess onCreateCredential "
+                                    + "in RemoteCredentialService");
+                            createCredentialFuture.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);
+                            createCredentialFuture.completeExceptionally(
+                                    new CredentialProviderException(errorCode, errorMsg));
+                        }});
+            CompletableFuture<CreateCredentialResponse> future = futureRef.get();
+            if (future != null && future.isCancelled()) {
+                dispatchCancellationSignal(cancellationSignal);
+            } else {
+                cancellationSink.set(cancellationSignal);
+            }
+            return createCredentialFuture;
+        }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
+
+        futureRef.set(connectThenExecute);
+        connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
+                handleExecutionResponse(result, error, cancellationSink, callback)));
+    }
+
+    private <T> void handleExecutionResponse(T result,
+            Throwable error,
+            AtomicReference<ICancellationSignal> cancellationSink,
+            ProviderCallbacks<T> callback) {
+        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..71fc67c
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -0,0 +1,168 @@
+/*
+ * 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.ProviderData;
+import android.credentials.ui.UserSelectionDialogResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 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<T, U> implements CredentialManagerUi.CredentialManagerUiCallback{
+    private static final String TAG = "RequestSession";
+
+    // TODO: Revise access levels of attributes
+    @NonNull protected final T mClientRequest;
+    @NonNull protected final U mClientCallback;
+    @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;
+    @NonNull protected final String mClientCallingPackage;
+
+    protected final Map<String, ProviderSession> mProviders = new HashMap<>();
+
+    protected RequestSession(@NonNull Context context,
+            @UserIdInt int userId, @NonNull T clientRequest, U clientCallback,
+            @NonNull String requestType,
+            String clientCallingPackage) {
+        mContext = context;
+        mUserId = userId;
+        mClientRequest = clientRequest;
+        mClientCallback = clientCallback;
+        mRequestType = requestType;
+        mClientCallingPackage = clientCallingPackage;
+        mHandler = new Handler(Looper.getMainLooper(), null, true);
+        mRequestId = new Binder();
+        mCredentialManagerUi = new CredentialManagerUi(mContext,
+                mUserId, this);
+    }
+
+    public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+            RemoteCredentialService remoteCredentialService);
+
+    protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList);
+
+    // UI callbacks
+
+    @Override // from CredentialManagerUiCallbacks
+    public void onUiSelection(UserSelectionDialogResult selection) {
+        String providerId = selection.getProviderId();
+        Log.i(TAG, "onUiSelection, providerId: " + providerId);
+        ProviderSession providerSession = mProviders.get(providerId);
+        if (providerSession == null) {
+            Log.i(TAG, "providerSession not found in onUiSelection");
+            return;
+        }
+        Log.i(TAG, "Provider session found");
+        providerSession.onUiEntrySelected(selection.getEntryKey(),
+                selection.getEntrySubkey(), selection.getPendingIntentProviderResponse());
+    }
+
+    @Override // from CredentialManagerUiCallbacks
+    public void onUiCancellation() {
+        // User canceled the activity
+        finishSession();
+    }
+
+    protected void onProviderStatusChanged(ProviderSession.Status status,
+            ComponentName componentName) {
+        Log.i(TAG, "in onStatusChanged with status: " + status);
+        if (ProviderSession.isTerminatingStatus(status)) {
+            Log.i(TAG, "in onStatusChanged terminating status");
+            onProviderTerminated(componentName);
+            //TODO: Check if this was the provider we were waiting for and can invoke the UI now
+        } else if (ProviderSession.isCompletionStatus(status)) {
+            Log.i(TAG, "in onStatusChanged isCompletionStatus status");
+            onProviderResponseComplete(componentName);
+        } else if (ProviderSession.isUiInvokingStatus(status)) {
+            Log.i(TAG, "in onStatusChanged isUiInvokingStatus status");
+            onProviderResponseRequiresUi();
+        }
+    }
+
+    protected void onProviderTerminated(ComponentName componentName) {
+        //TODO: Implement
+    }
+
+    protected void onProviderResponseComplete(ComponentName componentName) {
+        //TODO: Implement
+    }
+
+    protected void onProviderResponseRequiresUi() {
+        Log.i(TAG, "in onProviderResponseComplete");
+        // TODO: Determine whether UI has already been invoked, and deal accordingly
+        if (!isAnyProviderPending()) {
+            Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
+            getProviderDataAndInitiateUi();
+        } else {
+            Log.i(TAG, "Can't invoke UI - waiting on some providers");
+        }
+    }
+
+    protected void finishSession() {
+        clearProviderSessions();
+    }
+
+    protected void clearProviderSessions() {
+        //TODO: Implement
+        mProviders.clear();
+    }
+
+    private boolean isAnyProviderPending() {
+        for (ProviderSession session : mProviders.values()) {
+            if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void getProviderDataAndInitiateUi() {
+        ArrayList<ProviderData> providerDataList = new ArrayList<>();
+        for (ProviderSession session : mProviders.values()) {
+            Log.i(TAG, "preparing data for : " + session.getComponentName());
+            ProviderData providerData = session.prepareUiData();
+            if (providerData != null) {
+                Log.i(TAG, "Provider data is not null");
+                providerDataList.add(providerData);
+            }
+        }
+        if (!providerDataList.isEmpty()) {
+            Log.i(TAG, "provider list not empty about to initiate ui");
+            launchUiWithProviderData(providerDataList);
+        }
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index 222a96d..8047a53 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -47,11 +47,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.UserRestrictionsUtils;
 import com.android.server.utils.Slogf;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
index cc32c4d..953a9ee 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
@@ -27,10 +27,11 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
index 0305c35..8e430b3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
@@ -29,12 +29,12 @@
 import android.util.ArraySet;
 import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.JournaledFile;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a561307..c58e8d5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -23,6 +23,7 @@
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY;
 import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED;
 import static android.app.admin.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE;
 import static android.app.admin.DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE;
@@ -46,6 +47,7 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER;
+import static android.app.admin.DevicePolicyManager.EXEMPT_FROM_APP_STANDBY;
 import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE;
 import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_IDS;
 import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE;
@@ -259,6 +261,7 @@
 import android.content.pm.Signature;
 import android.content.pm.StringParceledListSlice;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.Cursor;
@@ -329,11 +332,10 @@
 import android.util.AtomicFile;
 import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -363,6 +365,8 @@
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.PasswordValidationError;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.ProxyUtils;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.LocalServices;
@@ -645,6 +649,15 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
     private static final long USE_SET_LOCATION_ENABLED = 117835097L;
 
+    /**
+     * Forces wipeDataNoLock to attempt removing the user or throw an error as
+     * opposed to trying to factory reset the device first and only then falling back to user
+     * removal.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long EXPLICIT_WIPE_BEHAVIOUR = 242193913L;
+
     // Only add to the end of the list. Do not change or rearrange these values, that will break
     // historical data. Do not use negative numbers or zero, logger only handles positive
     // integers.
@@ -661,6 +674,17 @@
     private @interface CopyAccountStatus {}
 
     /**
+     * Mapping of {@link android.app.admin.DevicePolicyManager.ApplicationExemptionConstants} to
+     * corresponding app-ops.
+     */
+    private static final Map<Integer, String> APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS =
+            new ArrayMap<>();
+    static {
+        APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.put(
+                EXEMPT_FROM_APP_STANDBY, OPSTR_SYSTEM_EXEMPT_FROM_APP_STANDBY);
+    }
+
+    /**
      * Admin apps targeting Android S+ may not use
      * {@link android.app.admin.DevicePolicyManager#setPasswordQuality} to set password quality
      * on the {@code DevicePolicyManager} instance obtained by calling
@@ -710,8 +734,7 @@
      * Contains (package-user) pairs to remove. An entry (p, u) implies that removal of package p
      * is requested for user u.
      */
-    private final Set<Pair<String, Integer>> mPackagesToRemove =
-            new ArraySet<Pair<String, Integer>>();
+    private final Set<UserPackage> mPackagesToRemove = new ArraySet<>();
 
     final LocalService mLocalService;
 
@@ -6699,8 +6722,8 @@
     }
 
     @Override
-    public void wipeDataWithReason(int flags, String wipeReasonForUser,
-            boolean calledOnParentInstance) {
+    public void wipeDataWithReason(int flags, @NonNull String wipeReasonForUser,
+            boolean calledOnParentInstance, boolean factoryReset) {
         if (!mHasFeature && !hasCallingOrSelfPermission(permission.MASTER_CLEAR)) {
             return;
         }
@@ -6782,7 +6805,8 @@
                 "DevicePolicyManager.wipeDataWithReason() from %s, organization-owned? %s",
                 adminName, calledByProfileOwnerOnOrgOwnedDevice);
 
-        wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId);
+        wipeDataNoLock(adminComp, flags, internalReason, wipeReasonForUser, userId,
+                calledOnParentInstance, factoryReset);
     }
 
     private String getGenericWipeReason(
@@ -6844,8 +6868,13 @@
         Slogf.i(LOG_TAG, "Cleaning up device-wide policies done.");
     }
 
+    /**
+     * @param factoryReset null: legacy behaviour, false: attempt to remove user, true: attempt to
+     *                     factory reset
+     */
     private void wipeDataNoLock(ComponentName admin, int flags, String internalReason,
-                                String wipeReasonForUser, int userId) {
+            @NonNull String wipeReasonForUser, int userId, boolean calledOnParentInstance,
+            @Nullable Boolean factoryReset) {
         wtfIfInLock();
 
         mInjector.binderWithCleanCallingIdentity(() -> {
@@ -6863,7 +6892,37 @@
                         + " restriction is set for user " + userId);
             }
 
-            if (userId == UserHandle.USER_SYSTEM) {
+            boolean isSystemUser = userId == UserHandle.USER_SYSTEM;
+            boolean wipeDevice;
+            if (factoryReset == null || !CompatChanges.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR)) {
+                // Legacy mode
+                wipeDevice = isSystemUser;
+            } else {
+                // Explicit behaviour
+                if (factoryReset) {
+                    // TODO(b/254031494) Replace with new factory reset permission checks
+                    boolean hasPermission = isDeviceOwnerUserId(userId)
+                            || (isOrganizationOwnedDeviceWithManagedProfile()
+                            && calledOnParentInstance);
+                    Preconditions.checkState(hasPermission,
+                            "Admin %s does not have permission to factory reset the device.",
+                            userId);
+                    wipeDevice = true;
+                } else {
+                    Preconditions.checkCallAuthorization(!isSystemUser,
+                            "User %s is a system user and cannot be removed", userId);
+                    boolean isLastNonHeadlessUser = getUserInfo(userId).isFull()
+                            && mUserManager.getAliveUsers().stream()
+                            .filter((it) -> it.getUserHandle().getIdentifier() != userId)
+                            .noneMatch(UserInfo::isFull);
+                    Preconditions.checkState(!isLastNonHeadlessUser,
+                            "Removing user %s would leave the device without any active users. "
+                                    + "Consider factory resetting the device instead.",
+                            userId);
+                    wipeDevice = false;
+                }
+            }
+            if (wipeDevice) {
                 forceWipeDeviceNoLock(
                         (flags & WIPE_EXTERNAL_STORAGE) != 0,
                         internalReason,
@@ -7131,7 +7190,7 @@
     }
 
     @Override
-    public void reportFailedPasswordAttempt(int userHandle) {
+    public void reportFailedPasswordAttempt(int userHandle, boolean parent) {
         Preconditions.checkArgumentNonnegative(userHandle, "Invalid userId");
 
         final CallerIdentity caller = getCallerIdentity();
@@ -7153,7 +7212,7 @@
                 saveSettingsLocked(userHandle);
                 if (mHasFeature) {
                     strictestAdmin = getAdminWithMinimumFailedPasswordsForWipeLocked(
-                            userHandle, /* parent */ false);
+                            userHandle, /* parent= */ false);
                     int max = strictestAdmin != null
                             ? strictestAdmin.maximumFailedPasswordsForWipe : 0;
                     if (max > 0 && policy.mFailedPasswordAttempts >= max) {
@@ -7183,10 +7242,13 @@
             // IMPORTANT: Call without holding the lock to prevent deadlock.
             try {
                 wipeDataNoLock(strictestAdmin.info.getComponent(),
-                        /*flags=*/ 0,
-                        /*reason=*/ "reportFailedPasswordAttempt()",
+                        /* flags= */ 0,
+                        /* reason= */ "reportFailedPasswordAttempt()",
                         getFailedPasswordAttemptWipeMessage(),
-                        userId);
+                        userId,
+                        /* calledOnParentInstance= */ parent,
+                        // factoryReset=null to enable U- behaviour
+                        /* factoryReset= */ null);
             } catch (SecurityException e) {
                 Slogf.w(LOG_TAG, "Failed to wipe user " + userId
                         + " after max failed password attempts reached.", e);
@@ -7195,7 +7257,7 @@
 
         if (mInjector.securityLogIsLoggingEnabled()) {
             SecurityLog.writeEvent(SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT,
-                    /*result*/ 0, /*method strength*/ 1);
+                    /* result= */ 0, /* method strength= */ 1);
         }
     }
 
@@ -13982,7 +14044,6 @@
     @Override
     public boolean isProvisioningAllowed(String action, String packageName) {
         Objects.requireNonNull(packageName);
-
         final CallerIdentity caller = getCallerIdentity();
         final long ident = mInjector.binderClearCallingIdentity();
         try {
@@ -13994,21 +14055,21 @@
             mInjector.binderRestoreCallingIdentity(ident);
         }
 
-        return checkProvisioningPreconditionSkipPermission(action, packageName) == STATUS_OK;
+        return checkProvisioningPreconditionSkipPermission(action, packageName, caller.getUserId())
+                == STATUS_OK;
     }
 
     @Override
     public int checkProvisioningPrecondition(String action, String packageName) {
         Objects.requireNonNull(packageName, "packageName is null");
-
+        final CallerIdentity caller = getCallerIdentity();
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS));
 
-        return checkProvisioningPreconditionSkipPermission(action, packageName);
+        return checkProvisioningPreconditionSkipPermission(action, packageName, caller.getUserId());
     }
-
     private int checkProvisioningPreconditionSkipPermission(String action,
-            String packageName) {
+            String packageName, int userId) {
         if (!mHasFeature) {
             logMissingFeatureAction("Cannot check provisioning for action " + action);
             return STATUS_DEVICE_ADMIN_NOT_SUPPORTED;
@@ -14016,7 +14077,8 @@
         if (!isProvisioningAllowed()) {
             return STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
         }
-        final int code = checkProvisioningPreConditionSkipPermissionNoLog(action, packageName);
+        final int code = checkProvisioningPreConditionSkipPermissionNoLog(
+                action, packageName, userId);
         if (code != STATUS_OK) {
             Slogf.d(LOG_TAG, "checkProvisioningPreCondition(" + action + ", " + packageName
                     + ") failed: "
@@ -14043,15 +14105,14 @@
     }
 
     private int checkProvisioningPreConditionSkipPermissionNoLog(String action,
-            String packageName) {
-        final int callingUserId = mInjector.userHandleGetCallingUserId();
+            String packageName, int userId) {
         if (action != null) {
             switch (action) {
                 case DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE:
-                    return checkManagedProfileProvisioningPreCondition(packageName, callingUserId);
+                    return checkManagedProfileProvisioningPreCondition(packageName, userId);
                 case DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE:
                 case DevicePolicyManager.ACTION_PROVISION_FINANCED_DEVICE:
-                    return checkDeviceOwnerProvisioningPreCondition(callingUserId);
+                    return checkDeviceOwnerProvisioningPreCondition(userId);
             }
         }
         throw new IllegalArgumentException("Unknown provisioning action " + action);
@@ -15044,7 +15105,7 @@
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(permission.MANAGE_DEVICE_ADMINS));
 
-        Pair<String, Integer> packageUserPair = new Pair<>(packageName, caller.getUserId());
+        UserPackage packageUserPair = UserPackage.of(caller.getUserId(), packageName);
         synchronized (getLockObject()) {
             return mPackagesToRemove.contains(packageUserPair);
         }
@@ -15072,7 +15133,7 @@
             throw new IllegalArgumentException("Cannot uninstall a package with a device owner");
         }
 
-        final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
+        final UserPackage packageUserPair = UserPackage.of(userId, packageName);
         synchronized (getLockObject()) {
             mPackagesToRemove.add(packageUserPair);
         }
@@ -15132,7 +15193,7 @@
     }
 
     private void startUninstallIntent(final String packageName, final int userId) {
-        final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
+        final UserPackage packageUserPair = UserPackage.of(userId, packageName);
         synchronized (getLockObject()) {
             if (!mPackagesToRemove.contains(packageUserPair)) {
                 // Do nothing if uninstall was not requested or was already started.
@@ -16969,6 +17030,88 @@
         });
     }
 
+    @Override
+    public void setApplicationExemptions(String packageName, int[] exemptions) {
+        if (!mHasFeature) {
+            return;
+        }
+        Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty.");
+        Objects.requireNonNull(exemptions, "Application exemptions must not be null.");
+        Preconditions.checkArgument(areApplicationExemptionsValid(exemptions),
+                "Invalid application exemption constant found in application exemptions set.");
+        Preconditions.checkCallAuthorization(
+                hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS));
+
+        final CallerIdentity caller = getCallerIdentity();
+        final ApplicationInfo packageInfo;
+        packageInfo = getPackageInfoWithNullCheck(packageName, caller);
+
+        for (Map.Entry<Integer, String> entry :
+                APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) {
+            int currentMode = mInjector.getAppOpsManager().unsafeCheckOpNoThrow(
+                    entry.getValue(), packageInfo.uid, packageInfo.packageName);
+            int newMode = ArrayUtils.contains(exemptions, entry.getKey())
+                    ? MODE_ALLOWED : MODE_DEFAULT;
+            mInjector.binderWithCleanCallingIdentity(() -> {
+                if (currentMode != newMode) {
+                    mInjector.getAppOpsManager()
+                            .setMode(entry.getValue(),
+                                    packageInfo.uid,
+                                    packageName,
+                                    newMode);
+                }
+            });
+        }
+    }
+
+    @Override
+    public int[] getApplicationExemptions(String packageName) {
+        if (!mHasFeature) {
+            return new int[0];
+        }
+        Preconditions.checkStringNotEmpty(packageName, "Package name cannot be empty.");
+        Preconditions.checkCallAuthorization(
+                hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS));
+
+        final CallerIdentity caller = getCallerIdentity();
+        final ApplicationInfo packageInfo;
+        packageInfo = getPackageInfoWithNullCheck(packageName, caller);
+
+        IntArray appliedExemptions = new IntArray(0);
+        for (Map.Entry<Integer, String> entry :
+                APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.entrySet()) {
+            if (mInjector.getAppOpsManager().unsafeCheckOpNoThrow(
+                    entry.getValue(), packageInfo.uid, packageInfo.packageName) == MODE_ALLOWED) {
+                appliedExemptions.add(entry.getKey());
+            }
+        }
+        return appliedExemptions.toArray();
+    }
+
+    private ApplicationInfo getPackageInfoWithNullCheck(String packageName, CallerIdentity caller) {
+        final ApplicationInfo packageInfo =
+                mInjector.getPackageManagerInternal().getApplicationInfo(
+                        packageName,
+                        /* flags= */ 0,
+                        caller.getUid(),
+                        caller.getUserId());
+        if (packageInfo == null) {
+            throw new ServiceSpecificException(
+                    DevicePolicyManager.ERROR_PACKAGE_NAME_NOT_FOUND,
+                    "Package name not found.");
+        }
+        return packageInfo;
+    }
+
+    private boolean areApplicationExemptionsValid(int[] exemptions) {
+        for (int exemption : exemptions) {
+            if (!APPLICATION_EXEMPTION_CONSTANTS_TO_APP_OPS.containsKey(exemption)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private boolean isCallingFromPackage(String packageName, int callingUid) {
         return mInjector.binderWithCleanCallingIdentity(() -> {
             try {
@@ -17622,7 +17765,7 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             final int result = checkProvisioningPreconditionSkipPermission(
-                    ACTION_PROVISION_MANAGED_PROFILE, admin.getPackageName());
+                    ACTION_PROVISION_MANAGED_PROFILE, admin.getPackageName(), caller.getUserId());
             if (result != STATUS_OK) {
                 throw new ServiceSpecificException(
                         ERROR_PRE_CONDITION_FAILED,
@@ -18032,7 +18175,8 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             int result = checkProvisioningPreconditionSkipPermission(
-                    ACTION_PROVISION_MANAGED_DEVICE, deviceAdmin.getPackageName());
+                    ACTION_PROVISION_MANAGED_DEVICE, deviceAdmin.getPackageName(),
+                    caller.getUserId());
             if (result != STATUS_OK) {
                 throw new ServiceSpecificException(
                         ERROR_PRE_CONDITION_FAILED,
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index 2ab5464..3040af2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -27,11 +27,11 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
index 289ed36..035f762 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
@@ -24,12 +24,12 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java
index 46e59a9..c3329795 100644
--- a/services/java/com/android/server/BootUserInitializer.java
+++ b/services/java/com/android/server/BootUserInitializer.java
@@ -83,9 +83,10 @@
             Slogf.d(TAG, "Creating initial user");
             t.traceBegin("create-initial-user");
             try {
+                int flags = UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN;
                 // TODO(b/204091126): proper name for user
                 UserInfo newUser = um.createUserEvenWhenDisallowed("Real User",
-                        UserManager.USER_TYPE_FULL_SECONDARY, UserInfo.FLAG_ADMIN,
+                        UserManager.USER_TYPE_FULL_SECONDARY, flags,
                         /* disallowedPackages= */ null, /* token= */ null);
                 Slogf.i(TAG, "Created initial user: %s", newUser.toFullString());
                 initialUserId = newUser.id;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9e449ae..433c170 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -103,6 +103,7 @@
 import com.android.internal.util.EmergencyAffordanceManager;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.widget.ILockSettings;
+import com.android.internal.widget.LockSettingsInternal;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.ambientcontext.AmbientContextManagerService;
 import com.android.server.appbinding.AppBindingService;
@@ -154,6 +155,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;
@@ -317,8 +319,6 @@
             "com.android.clockwork.displayoffload.DisplayOffloadService";
     private static final String WEAR_DISPLAY_SERVICE_CLASS =
             "com.android.clockwork.display.WearDisplayService";
-    private static final String WEAR_LEFTY_SERVICE_CLASS =
-            "com.google.android.clockwork.lefty.WearLeftyService";
     private static final String WEAR_TIME_SERVICE_CLASS =
             "com.android.clockwork.time.WearTimeService";
     private static final String WEAR_GLOBAL_ACTIONS_SERVICE_CLASS =
@@ -1403,7 +1403,6 @@
                 false);
         boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
                 false);
-        boolean enableLeftyService = SystemProperties.getBoolean("config.enable_lefty", false);
 
         boolean isEmulator = SystemProperties.get("ro.boot.qemu").equals("1");
 
@@ -1667,7 +1666,18 @@
         // Bring up services needed for UI.
         if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
             t.traceBegin("StartInputMethodManagerLifecycle");
-            mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
+            String immsClassName = context.getResources().getString(
+                    R.string.config_deviceSpecificInputMethodManagerService);
+            if (immsClassName.isEmpty()) {
+                mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
+            } else {
+                try {
+                    Slog.i(TAG, "Starting custom IMMS: " + immsClassName);
+                    mSystemServiceManager.startService(immsClassName);
+                } catch (Throwable e) {
+                    reportWtf("starting " + immsClassName, e);
+                }
+            }
             t.traceEnd();
 
             t.traceBegin("StartAccessibilityManagerService");
@@ -1788,16 +1798,18 @@
             dpms = mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class);
             t.traceEnd();
 
-            if (!isWatch) {
-                t.traceBegin("StartStatusBarManagerService");
-                try {
-                    statusBar = new StatusBarManagerService(context);
-                    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);
-                } catch (Throwable e) {
-                    reportWtf("starting StatusBarManagerService", e);
+            t.traceBegin("StartStatusBarManagerService");
+            try {
+                statusBar = new StatusBarManagerService(context);
+                if (!isWatch) {
+                    statusBar.publishGlobalActionsProvider();
                 }
-                t.traceEnd();
+                ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false,
+                        DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);
+            } catch (Throwable e) {
+                reportWtf("starting StatusBarManagerService", e);
             }
+            t.traceEnd();
 
             if (deviceHasConfigString(context,
                     R.string.config_defaultMusicRecognitionService)) {
@@ -2468,6 +2480,10 @@
             t.traceBegin("StartMediaMetricsManager");
             mSystemServiceManager.startService(MediaMetricsManagerService.class);
             t.traceEnd();
+
+            t.traceBegin("StartBackgroundInstallControlService");
+            mSystemServiceManager.startService(BackgroundInstallControlService.class);
+            t.traceEnd();
         }
 
         t.traceBegin("StartMediaProjectionManager");
@@ -2496,12 +2512,6 @@
             mSystemServiceManager.startService(WEAR_TIME_SERVICE_CLASS);
             t.traceEnd();
 
-            if (enableLeftyService) {
-                t.traceBegin("StartWearLeftyService");
-                mSystemServiceManager.startService(WEAR_LEFTY_SERVICE_CLASS);
-                t.traceEnd();
-            }
-
             t.traceBegin("StartWearGlobalActionsService");
             mSystemServiceManager.startService(WEAR_GLOBAL_ACTIONS_SERVICE_CLASS);
             t.traceEnd();
@@ -3011,6 +3021,14 @@
             t.traceEnd();
         }, t);
 
+        t.traceBegin("LockSettingsThirdPartyAppsStarted");
+        LockSettingsInternal lockSettingsInternal =
+            LocalServices.getService(LockSettingsInternal.class);
+        if (lockSettingsInternal != null) {
+            lockSettingsInternal.onThirdPartyAppsStarted();
+        }
+        t.traceEnd();
+
         t.traceBegin("StartSystemUI");
         try {
             startSystemUi(context, windowManagerF);
diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java
index eab3b77..292320e 100644
--- a/services/people/java/com/android/server/people/PeopleService.java
+++ b/services/people/java/com/android/server/people/PeopleService.java
@@ -53,6 +53,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 /**
@@ -372,7 +373,8 @@
         @Override
         public boolean equals(Object o) {
             ListenerKey key = (ListenerKey) o;
-            return key.getPackageName().equals(mPackageName) && key.getUserId() == mUserId
+            return key.getPackageName().equals(mPackageName)
+                    && Objects.equals(key.getUserId(), mUserId)
                     && key.getShortcutId().equals(mShortcutId);
         }
 
diff --git a/services/permission/Android.bp b/services/permission/Android.bp
new file mode 100644
index 0000000..b03f17b
--- /dev/null
+++ b/services/permission/Android.bp
@@ -0,0 +1,40 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "services.permission-sources",
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
+    path: "java",
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+    name: "services.permission",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.permission-sources"],
+    libs: [
+        "services.core",
+        // Soong fails to automatically add this dependency because all the
+        // *.kt sources are inside a filegroup.
+        "kotlin-annotations",
+    ],
+    static_libs: [
+        "kotlin-stdlib",
+    ],
+    jarjar_rules: "jarjar-rules.txt",
+    kotlincflags: [
+        "-Xjvm-default=all",
+        "-Xno-call-assertions",
+        "-Xno-param-assertions",
+        "-Xno-receiver-assertions",
+    ],
+}
diff --git a/services/permission/OWNERS b/services/permission/OWNERS
new file mode 100644
index 0000000..6c6c9fc
--- /dev/null
+++ b/services/permission/OWNERS
@@ -0,0 +1,4 @@
+ashfall@google.com
+joecastro@google.com
+ntmyren@google.com
+zhanghai@google.com
diff --git a/services/permission/jarjar-rules.txt b/services/permission/jarjar-rules.txt
new file mode 100644
index 0000000..34af3af
--- /dev/null
+++ b/services/permission/jarjar-rules.txt
@@ -0,0 +1 @@
+rule kotlin.** com.android.server.permission.jarjar.@0
diff --git a/services/permission/java/com/android/server/permission/ModernPermissionManagerServiceImpl.kt b/services/permission/java/com/android/server/permission/ModernPermissionManagerServiceImpl.kt
new file mode 100644
index 0000000..21ec159
--- /dev/null
+++ b/services/permission/java/com/android/server/permission/ModernPermissionManagerServiceImpl.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.permission
+
+import com.android.internal.annotations.Keep
+import com.android.server.pm.permission.PermissionManagerServiceInterface
+
+/**
+ * Modern implementation of [PermissionManagerServiceInterface].
+ */
+@Keep
+class ModernPermissionManagerServiceImpl
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/proguard_permission.flags b/services/proguard_permission.flags
new file mode 100644
index 0000000..15edc61
--- /dev/null
+++ b/services/proguard_permission.flags
@@ -0,0 +1,9 @@
+# Only shrink services.permission classes.
+# Note that while more aggressive services shrinking is enabled by default (see proguard.flags), for
+# cases where that's not yet possible, we still need to shrink the permission package to prune out
+# unused Kotlin stdlib dependencies.
+-keep class !com.android.server.permission.** { *; }
+
+# CoverageService guards optional jacoco class references with a runtime guard, so we can safely
+# suppress build-time warnings.
+-dontwarn org.jacoco.agent.rt.*
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
new file mode 100644
index 0000000..939fb6a
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "FrameworksInputMethodSystemServerTests",
+    defaults: [
+        "modules-utils-testable-device-config-defaults",
+    ],
+
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "androidx.test.espresso.core",
+        "androidx.test.espresso.contrib",
+        "androidx.test.ext.truth",
+        "frameworks-base-testutils",
+        "mockito-target-extended-minus-junit4",
+        "platform-test-annotations",
+        "services.core",
+        "servicestests-core-utils",
+        "servicestests-utils-mockito-extended",
+        "truth-prebuilt",
+    ],
+
+    libs: [
+        "android.test.mock",
+        "android.test.base",
+        "android.test.runner",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
new file mode 100644
index 0000000..12e7cfc
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.frameworks.inputmethodtests">
+
+    <uses-sdk android:targetSdkVersion="31" />
+
+    <!-- Permissions required for granting and logging -->
+    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
+    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
+    <uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
+
+    <!-- Permissions for reading system info -->
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+    <application android:testOnly="true"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.inputmethodtests"
+        android:label="Frameworks InputMethod System Service Tests" />
+
+</manifest>
diff --git a/services/tests/InputMethodSystemServerTests/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
new file mode 100644
index 0000000..92be780
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?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.
+-->
+<configuration description="Runs Frameworks InputMethod System Services Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="FrameworksInputMethodSystemServerTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="FrameworksInputMethodSystemServerTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.frameworks.inputmethodtests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+
+    <!-- Collect the files in the dump directory for debugging -->
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/sdcard/FrameworksInputMethodSystemServerTests/" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+</configuration>
diff --git a/services/tests/InputMethodSystemServerTests/OWNERS b/services/tests/InputMethodSystemServerTests/OWNERS
new file mode 100644
index 0000000..1f2c036
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/inputmethod/OWNERS
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
new file mode 100644
index 0000000..640bde3
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -0,0 +1,259 @@
+/*
+ * 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.inputmethod;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.view.inputmethod.EditorInfo;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.inputmethod.IInputMethod;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.view.IInputMethodManager;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.SystemServerInitThreadPool;
+import com.android.server.SystemService;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/** Base class for testing {@link InputMethodManagerService}. */
+public class InputMethodManagerServiceTestBase {
+    protected static final String TEST_SELECTED_IME_ID = "test.ime";
+    protected static final String TEST_EDITOR_PKG_NAME = "test.editor";
+    protected static final String TEST_FOCUSED_WINDOW_NAME = "test.editor/activity";
+    protected static final WindowManagerInternal.ImeTargetInfo TEST_IME_TARGET_INFO =
+            new WindowManagerInternal.ImeTargetInfo(
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME,
+                    TEST_FOCUSED_WINDOW_NAME);
+    protected static final InputBindResult SUCCESS_WAITING_IME_BINDING_RESULT =
+            new InputBindResult(
+                    InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+                    null,
+                    null,
+                    null,
+                    "0",
+                    0,
+                    null,
+                    false);
+
+    @Mock protected WindowManagerInternal mMockWindowManagerInternal;
+    @Mock protected ActivityManagerInternal mMockActivityManagerInternal;
+    @Mock protected PackageManagerInternal mMockPackageManagerInternal;
+    @Mock protected InputManagerInternal mMockInputManagerInternal;
+    @Mock protected DisplayManagerInternal mMockDisplayManagerInternal;
+    @Mock protected UserManagerInternal mMockUserManagerInternal;
+    @Mock protected InputMethodBindingController mMockInputMethodBindingController;
+    @Mock protected IInputMethodClient mMockInputMethodClient;
+    @Mock protected IBinder mWindowToken;
+    @Mock protected IRemoteInputConnection mMockRemoteInputConnection;
+    @Mock protected IRemoteAccessibilityInputConnection mMockRemoteAccessibilityInputConnection;
+    @Mock protected ImeOnBackInvokedDispatcher mMockImeOnBackInvokedDispatcher;
+    @Mock protected IInputMethodManager.Stub mMockIInputMethodManager;
+    @Mock protected IPlatformCompat.Stub mMockIPlatformCompat;
+    @Mock protected IInputMethod mMockInputMethod;
+    @Mock protected IBinder mMockInputMethodBinder;
+    @Mock protected IInputManager mMockIInputManager;
+
+    protected Context mContext;
+    protected MockitoSession mMockingSession;
+    protected int mTargetSdkVersion;
+    protected int mCallingUserId;
+    protected EditorInfo mEditorInfo;
+    protected IInputMethodInvoker mMockInputMethodInvoker;
+    protected InputMethodManagerService mInputMethodManagerService;
+    protected ServiceThread mServiceThread;
+
+    @Before
+    public void setUp() throws RemoteException {
+        mMockingSession =
+                mockitoSession()
+                        .initMocks(this)
+                        .strictness(Strictness.LENIENT)
+                        .mockStatic(LocalServices.class)
+                        .mockStatic(ServiceManager.class)
+                        .mockStatic(SystemServerInitThreadPool.class)
+                        .startMocking();
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        spyOn(mContext);
+
+        mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+        mCallingUserId = UserHandle.getCallingUserId();
+        mEditorInfo = new EditorInfo();
+        mEditorInfo.packageName = TEST_EDITOR_PKG_NAME;
+
+        // Injecting and mocking local services.
+        doReturn(mMockWindowManagerInternal)
+                .when(() -> LocalServices.getService(WindowManagerInternal.class));
+        doReturn(mMockActivityManagerInternal)
+                .when(() -> LocalServices.getService(ActivityManagerInternal.class));
+        doReturn(mMockPackageManagerInternal)
+                .when(() -> LocalServices.getService(PackageManagerInternal.class));
+        doReturn(mMockInputManagerInternal)
+                .when(() -> LocalServices.getService(InputManagerInternal.class));
+        doReturn(mMockDisplayManagerInternal)
+                .when(() -> LocalServices.getService(DisplayManagerInternal.class));
+        doReturn(mMockUserManagerInternal)
+                .when(() -> LocalServices.getService(UserManagerInternal.class));
+        doReturn(mMockIInputMethodManager)
+                .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
+        doReturn(mMockIPlatformCompat)
+                .when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+
+        // Stubbing out context related methods to avoid the system holding strong references to
+        // InputMethodManagerService.
+        doNothing().when(mContext).enforceCallingPermission(anyString(), anyString());
+        doNothing().when(mContext).sendBroadcastAsUser(any(), any());
+        doReturn(null).when(mContext).registerReceiver(any(), any());
+        doReturn(null)
+                .when(mContext)
+                .registerReceiverAsUser(any(), any(), any(), anyString(), any(), anyInt());
+
+        // Injecting and mocked InputMethodBindingController and InputMethod.
+        mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
+        InputManager.resetInstance(mMockIInputManager);
+        synchronized (ImfLock.class) {
+            when(mMockInputMethodBindingController.getCurMethod())
+                    .thenReturn(mMockInputMethodInvoker);
+            when(mMockInputMethodBindingController.bindCurrentMethod())
+                    .thenReturn(SUCCESS_WAITING_IME_BINDING_RESULT);
+            doNothing().when(mMockInputMethodBindingController).unbindCurrentMethod();
+            when(mMockInputMethodBindingController.getSelectedMethodId())
+                    .thenReturn(TEST_SELECTED_IME_ID);
+        }
+
+        // Shuffling around all other initialization to make the test runnable.
+        when(mMockIInputManager.getInputDeviceIds()).thenReturn(new int[0]);
+        when(mMockIInputMethodManager.isImeTraceEnabled()).thenReturn(false);
+        when(mMockIPlatformCompat.isChangeEnabledByUid(anyLong(), anyInt())).thenReturn(true);
+        when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(true);
+        when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean()))
+                .thenReturn(new int[] {0});
+        when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
+        when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
+                .thenReturn(Binder.getCallingUid());
+        when(mMockWindowManagerInternal.onToggleImeRequested(anyBoolean(), any(), any(), anyInt()))
+                .thenReturn(TEST_IME_TARGET_INFO);
+        when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder);
+
+        // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(),
+        // which is ok to be mocked out for now.
+        doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString()));
+
+        mServiceThread =
+                new ServiceThread(
+                        "TestServiceThread",
+                        Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
+                        false);
+        mInputMethodManagerService =
+                new InputMethodManagerService(
+                        mContext, mServiceThread, mMockInputMethodBindingController);
+
+        // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
+        // InputMethodManagerService, which is closer to the real situation.
+        InputMethodManagerService.Lifecycle lifecycle =
+                new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService);
+
+        // Public local InputMethodManagerService.
+        lifecycle.onStart();
+        try {
+            // After this boot phase, services can broadcast Intents.
+            lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
+        } catch (SecurityException e) {
+            // Security exception to permission denial is expected in test, mocking out to ensure
+            // InputMethodManagerService as system ready state.
+            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
+                throw e;
+            }
+        }
+
+        // Call InputMethodManagerService#addClient() as a preparation to start interacting with it.
+        mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0);
+    }
+
+    @After
+    public void tearDown() {
+        if (mServiceThread != null) {
+            mServiceThread.quitSafely();
+        }
+
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)
+            throws RemoteException {
+        synchronized (ImfLock.class) {
+            verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0))
+                    .setCurrentMethodVisible();
+        }
+        verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
+                .showSoftInput(any(), any(), anyInt(), any());
+    }
+
+    protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
+            throws RemoteException {
+        synchronized (ImfLock.class) {
+            verify(mMockInputMethodBindingController, times(setNotVisible ? 1 : 0))
+                    .setCurrentMethodNotVisible();
+        }
+        verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
+                .hideSoftInput(any(), any(), anyInt(), any());
+    }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
new file mode 100644
index 0000000..ffa2729
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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.inputmethod;
+
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test the behavior of {@link InputMethodManagerService#startInputOrWindowGainedFocus(int,
+ * IInputMethodClient, IBinder, int, int, int, EditorInfo, IRemoteInputConnection,
+ * IRemoteAccessibilityInputConnection, int, int, ImeOnBackInvokedDispatcher)}.
+ */
+@RunWith(Parameterized.class)
+public class InputMethodManagerServiceWindowGainedFocusTest
+        extends InputMethodManagerServiceTestBase {
+    private static final String TAG = "IMMSWindowGainedFocusTest";
+
+    private static final int[] SOFT_INPUT_STATE_FLAGS =
+            new int[] {
+                SOFT_INPUT_STATE_UNSPECIFIED,
+                SOFT_INPUT_STATE_UNCHANGED,
+                SOFT_INPUT_STATE_HIDDEN,
+                SOFT_INPUT_STATE_ALWAYS_HIDDEN,
+                SOFT_INPUT_STATE_VISIBLE,
+                SOFT_INPUT_STATE_ALWAYS_VISIBLE
+            };
+    private static final int[] SOFT_INPUT_ADJUST_FLAGS =
+            new int[] {
+                SOFT_INPUT_ADJUST_UNSPECIFIED,
+                SOFT_INPUT_ADJUST_RESIZE,
+                SOFT_INPUT_ADJUST_PAN,
+                SOFT_INPUT_ADJUST_NOTHING
+            };
+    private static final int DEFAULT_SOFT_INPUT_FLAG =
+            StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
+
+    @Parameterized.Parameters(name = "softInputState={0}, softInputAdjustment={1}")
+    public static List<Object[]> softInputModeConfigs() {
+        ArrayList<Object[]> params = new ArrayList<>();
+        for (int softInputState : SOFT_INPUT_STATE_FLAGS) {
+            for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
+                params.add(new Object[] {softInputState, softInputAdjust});
+            }
+        }
+        return params;
+    }
+
+    private final int mSoftInputState;
+    private final int mSoftInputAdjustment;
+
+    public InputMethodManagerServiceWindowGainedFocusTest(
+            int softInputState, int softInputAdjustment) {
+        mSoftInputState = softInputState;
+        mSoftInputAdjustment = softInputAdjustment;
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_forwardNavigation() throws RemoteException {
+        mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
+
+        switch (mSoftInputState) {
+            case SOFT_INPUT_STATE_UNSPECIFIED:
+                boolean showSoftInput = mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE;
+                verifyShowSoftInput(
+                        showSoftInput /* setVisible */, showSoftInput /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_VISIBLE:
+            case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+                verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_HIDDEN:
+            case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            default:
+                throw new IllegalStateException(
+                        "Unhandled soft input mode: "
+                                + InputMethodDebug.softInputModeToString(mSoftInputState));
+        }
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_notForwardNavigation() throws RemoteException {
+        mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, false /* forwardNavigation */))
+                .isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
+
+        switch (mSoftInputState) {
+            case SOFT_INPUT_STATE_UNSPECIFIED:
+                boolean hideSoftInput = mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE;
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_VISIBLE:
+            case SOFT_INPUT_STATE_HIDDEN:
+            case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+                verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
+                verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+                verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+                // Soft input was hidden by default, so it doesn't need to call
+                // {@code IMS#hideSoftInput()}.
+                verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
+                break;
+            default:
+                throw new IllegalStateException(
+                        "Unhandled soft input mode: "
+                                + InputMethodDebug.softInputModeToString(mSoftInputState));
+        }
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
+        when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
+
+        assertThat(
+                        startInputOrWindowGainedFocus(
+                                DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                .isEqualTo(InputBindResult.INVALID_USER);
+        verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+        verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+    }
+
+    @Test
+    public void startInputOrWindowGainedFocus_invalidFocusStatus() throws RemoteException {
+        int[] invalidImeClientFocus =
+                new int[] {
+                    WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW,
+                    WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH,
+                    WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID
+                };
+        InputBindResult[] inputBingResult =
+                new InputBindResult[] {
+                    InputBindResult.NOT_IME_TARGET_WINDOW,
+                    InputBindResult.DISPLAY_ID_MISMATCH,
+                    InputBindResult.INVALID_DISPLAY_ID
+                };
+
+        for (int i = 0; i < invalidImeClientFocus.length; i++) {
+            when(mMockWindowManagerInternal.hasInputMethodClientFocus(
+                            any(), anyInt(), anyInt(), anyInt()))
+                    .thenReturn(invalidImeClientFocus[i]);
+
+            assertThat(
+                            startInputOrWindowGainedFocus(
+                                    DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
+                    .isEqualTo(inputBingResult[i]);
+            verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
+            verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
+        }
+    }
+
+    private InputBindResult startInputOrWindowGainedFocus(
+            int startInputFlag, boolean forwardNavigation) {
+        int softInputMode = mSoftInputState | mSoftInputAdjustment;
+        if (forwardNavigation) {
+            softInputMode |= SOFT_INPUT_IS_FORWARD_NAVIGATION;
+        }
+
+        Log.i(
+                TAG,
+                "startInputOrWindowGainedFocus() softInputStateFlag="
+                        + InputMethodDebug.softInputModeToString(mSoftInputState)
+                        + ", softInputAdjustFlag="
+                        + InputMethodDebug.softInputModeToString(mSoftInputAdjustment));
+
+        return mInputMethodManagerService.startInputOrWindowGainedFocus(
+                StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
+                mMockInputMethodClient /* client */,
+                mWindowToken /* windowToken */,
+                startInputFlag /* startInputFlags */,
+                softInputMode /* softInputMode */,
+                0 /* windowFlags */,
+                mEditorInfo /* editorInfo */,
+                mMockRemoteInputConnection /* inputConnection */,
+                mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
+                mTargetSdkVersion /* unverifiedTargetSdkVersion */,
+                mCallingUserId /* userId */,
+                mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
+    }
+
+    private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) {
+        when(mMockWindowManagerInternal.hasInputMethodClientFocus(
+                        any(), anyInt(), anyInt(), anyInt()))
+                .thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS);
+        when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any()))
+                .thenReturn(restoreImeVisibility);
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
index 5180786..4ceae96 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
@@ -53,7 +53,8 @@
         ParsedActivity::getTaskAffinity,
         ParsedActivity::getTheme,
         ParsedActivity::getUiOptions,
-        ParsedActivity::isSupportsSizeChanges
+        ParsedActivity::isSupportsSizeChanges,
+        ParsedActivity::getTargetDisplayCategory
     )
 
     override fun mainComponentSubclassExtraParams() = listOf(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
index e4d124e..55645d7 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationEnforcerTest.kt
@@ -219,6 +219,20 @@
                     printState(mock(Computer::class.java), mock(IndentingPrintWriter::class.java),
                         null, null)
                 },
+                service(Type.QUERENT, "printOwnersForPackage") {
+                    printOwnersForPackage(
+                        mock(IndentingPrintWriter::class.java),
+                        it.targetPackageName,
+                        it.userId
+                    )
+                },
+                service(Type.QUERENT, "printOwnersForDomains") {
+                    printOwnersForDomains(
+                        mock(IndentingPrintWriter::class.java),
+                        listOf("example.com"),
+                        it.userId
+                    )
+                },
                 service(Type.VERIFIER, "setStatus") {
                     setDomainVerificationStatus(
                         it.targetDomainSetId,
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
index ad652df..65b99c5 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
@@ -20,9 +20,9 @@
 import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.SparseArray
-import android.util.TypedXmlPullParser
-import android.util.TypedXmlSerializer
 import android.util.Xml
+import com.android.modules.utils.TypedXmlPullParser
+import com.android.modules.utils.TypedXmlSerializer
 import com.android.server.pm.verify.domain.DomainVerificationPersistence
 import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState
 import com.android.server.pm.verify.domain.models.DomainVerificationPkgState
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 16317fe..681bfcf 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -56,6 +56,7 @@
         "service-jobscheduler",
         "service-permission.impl",
         "service-sdksandbox.impl",
+        "services.backup",
         "services.companion",
         "services.core",
         "services.devicepolicy",
@@ -78,6 +79,12 @@
         "servicestests-core-utils",
     ],
 
+    java_resources: [
+        ":apex.test",
+        ":test.rebootless_apex_v1",
+        ":test.rebootless_apex_v2",
+    ],
+
     jni_libs: [
         "libpsi",
     ],
diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS
index 2bb1649..4dda51f 100644
--- a/services/tests/mockingservicestests/OWNERS
+++ b/services/tests/mockingservicestests/OWNERS
@@ -1,5 +1,8 @@
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+# Game Platform
 per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS
 per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS
 per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS
 per-file Game* = file:/GAME_MANAGER_OWNERS
+per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS
diff --git a/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml
new file mode 100644
index 0000000..77fe786
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<game-mode-config
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:supportsPerformanceGameMode="true"
+    android:supportsBatteryGameMode="true"
+    android:allowGameAngleDriver="false"
+    android:allowGameDownscaling="false"
+    android:allowGameFpsOverride="false"
+/>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in.xml
similarity index 100%
rename from services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml
rename to services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in.xml
diff --git a/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml
new file mode 100644
index 0000000..96d2878
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<game-mode-config
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:supportsPerformanceGameMode="true"
+    android:supportsBatteryGameMode="true"
+    android:allowGameAngleDriver="true"
+    android:allowGameDownscaling="true"
+    android:allowGameFpsOverride="true"
+/>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml
similarity index 100%
rename from services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml
rename to services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml
diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
index cb14864..2583f44 100644
--- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
@@ -325,7 +325,7 @@
         doNothing().when(mAlarmManager).set(anyInt(), anyLong(), anyString(), any(), any());
         doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(), any());
         doNothing().when(mAlarmManager)
-                .setWindow(anyInt(), anyLong(), anyLong(), anyString(), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), anyString(), any(), any(Handler.class));
         doReturn(mock(Sensor.class)).when(mSensorManager)
                 .getDefaultSensor(eq(Sensor.TYPE_SIGNIFICANT_MOTION), eq(true));
         doReturn(true).when(mSensorManager).registerListener(any(), any(), anyInt());
@@ -1111,12 +1111,12 @@
         alarmManagerInOrder.verify(mAlarmManager).setWindow(
                 eq(AlarmManager.ELAPSED_REALTIME),
                 eq(idleAfterInactiveExpiryTime),
-                anyLong(), anyString(), any(), any());
+                anyLong(), anyString(), any(), any(Handler.class));
         // Maintenance alarm
         alarmManagerInOrder.verify(mAlarmManager).setWindow(
                 eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
                 eq(idleAfterInactiveExpiryTime + idlingTimeMs),
-                anyLong(), anyString(), any(), any());
+                anyLong(), anyString(), any(), any(Handler.class));
 
         final AlarmManager.OnAlarmListener progressionListener =
                 alarmListenerCaptor.getAllValues().get(0);
@@ -1130,7 +1130,7 @@
         alarmManagerInOrder.verify(mAlarmManager).setWindow(
                 eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
                 eq(mInjector.nowElapsed + idlingTimeMs),
-                anyLong(), anyString(), any(), any());
+                anyLong(), anyString(), any(), any(Handler.class));
 
         for (int i = 0; i < 2; ++i) {
             // IDLE->MAINTENANCE alarm
@@ -1144,12 +1144,12 @@
             alarmManagerInOrder.verify(mAlarmManager).setWindow(
                     eq(AlarmManager.ELAPSED_REALTIME),
                     eq(maintenanceExpiryTime),
-                    anyLong(), anyString(), any(), any());
+                    anyLong(), anyString(), any(), any(Handler.class));
             // Set IDLE->MAINTENANCE
             alarmManagerInOrder.verify(mAlarmManager).setWindow(
                     eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
                     eq(maintenanceExpiryTime + idlingTimeMs),
-                    anyLong(), anyString(), any(), any());
+                    anyLong(), anyString(), any(), any(Handler.class));
 
             // MAINTENANCE->IDLE alarm
             mInjector.nowElapsed = mDeviceIdleController.getNextLightAlarmTimeForTesting();
@@ -1159,7 +1159,7 @@
             alarmManagerInOrder.verify(mAlarmManager).setWindow(
                     eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
                     eq(mInjector.nowElapsed + idlingTimeMs),
-                    anyLong(), anyString(), any(), any());
+                    anyLong(), anyString(), any(), any(Handler.class));
         }
     }
 
@@ -2019,7 +2019,8 @@
         final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListener = ArgumentCaptor
                 .forClass(AlarmManager.OnAlarmListener.class);
         doNothing().when(mAlarmManager).setWindow(
-                anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), any());
+                anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(),
+                any(Handler.class));
         doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
                 eq("DeviceIdleController.motion_registration"),
                 alarmListener.capture(), any());
@@ -2063,7 +2064,8 @@
         final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListener = ArgumentCaptor
                 .forClass(AlarmManager.OnAlarmListener.class);
         doNothing().when(mAlarmManager).setWindow(
-                anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(), any());
+                anyInt(), anyLong(), anyLong(), eq("DeviceIdleController.motion"), any(),
+                any(Handler.class));
         doNothing().when(mAlarmManager).setWindow(anyInt(), anyLong(), anyLong(),
                 eq("DeviceIdleController.motion_registration"),
                 alarmListener.capture(), any());
@@ -2130,7 +2132,7 @@
                         eq(SensorManager.SENSOR_DELAY_NORMAL));
         inOrder.verify(mAlarmManager).setWindow(
                 anyInt(), eq(mInjector.nowElapsed + mConstants.MOTION_INACTIVE_TIMEOUT), anyLong(),
-                eq("DeviceIdleController.motion"), any(), any());
+                eq("DeviceIdleController.motion"), any(), any(Handler.class));
         final SensorEventListener listener = listenerCaptor.getValue();
 
         // Trigger motion
@@ -2140,7 +2142,7 @@
         final ArgumentCaptor<Long> registrationTimeCaptor = ArgumentCaptor.forClass(Long.class);
         inOrder.verify(mAlarmManager).setWindow(
                 anyInt(), registrationTimeCaptor.capture(), anyLong(),
-                eq("DeviceIdleController.motion_registration"), any(), any());
+                eq("DeviceIdleController.motion_registration"), any(), any(Handler.class));
 
         // Make sure the listener is re-registered.
         mInjector.nowElapsed = registrationTimeCaptor.getValue();
@@ -2150,7 +2152,7 @@
                         eq(SensorManager.SENSOR_DELAY_NORMAL));
         final ArgumentCaptor<Long> timeoutCaptor = ArgumentCaptor.forClass(Long.class);
         inOrder.verify(mAlarmManager).setWindow(anyInt(), timeoutCaptor.capture(), anyLong(),
-                eq("DeviceIdleController.motion"), any(), any());
+                eq("DeviceIdleController.motion"), any(), any(Handler.class));
 
         // No motion before timeout
         stationaryListener.motionExpected = false;
diff --git a/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java
new file mode 100644
index 0000000..33275bd
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/DumpableDumperRule.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import android.util.Dumpable;
+import android.util.Log;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@code JUnit} rule that logs (using tag {@value #TAG} the contents of
+ * {@link Dumpable dumpables} in case of failure.
+ */
+public final class DumpableDumperRule implements TestRule {
+
+    private static final String TAG = DumpableDumperRule.class.getSimpleName();
+
+    private static final String[] NO_ARGS = {};
+
+    private final List<Dumpable> mDumpables = new ArrayList<>();
+
+    /**
+     * Adds a {@link Dumpable} to be logged if the test case fails.
+     */
+    public void addDumpable(Dumpable dumpable) {
+        mDumpables.add(dumpable);
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    dumpOnFailure(description.getMethodName());
+                    throw t;
+                }
+            }
+        };
+    }
+
+    private void dumpOnFailure(String testName) throws IOException {
+        if (mDumpables.isEmpty()) {
+            return;
+        }
+        Log.w(TAG, "Dumping " + mDumpables.size() + " dumpables on failure of " + testName);
+        mDumpables.forEach(d -> logDumpable(d));
+    }
+
+    private void logDumpable(Dumpable dumpable) {
+        try {
+            try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
+                dumpable.dump(pw, NO_ARGS);
+                String[] dump = sw.toString().split(System.lineSeparator());
+                Log.w(TAG, "Dumping " + dumpable.getDumpableName() + " (" + dump.length
+                        + " lines):");
+                for (String line : dump) {
+                    Log.w(TAG, line);
+                }
+
+            } catch (RuntimeException e) {
+                Log.e(TAG, "RuntimeException dumping " + dumpable.getDumpableName(), e);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "IOException dumping " + dumpable.getDumpableName(), e);
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
index 9aa28ce..c0b5070 100644
--- a/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/ExtendedMockitoTestCase.java
@@ -23,6 +23,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
@@ -38,6 +39,9 @@
 
     private MockitoSession mSession;
 
+    @Rule
+    public final DumpableDumperRule mDumpableDumperRule = new DumpableDumperRule();
+
     @Before
     public void startSession() {
         if (DEBUG) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 86915da..2f6b07b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -16,6 +16,13 @@
 
 package com.android.server.am;
 
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_ALARM;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_FOREGROUND;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_INTERACTIVE;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_MANIFEST;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_ORDERED;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED;
+import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_RESULT_TO;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
 import static com.android.server.am.BroadcastQueueTest.CLASS_GREEN;
@@ -25,23 +32,35 @@
 import static com.android.server.am.BroadcastQueueTest.PACKAGE_YELLOW;
 import static com.android.server.am.BroadcastQueueTest.getUidForPackage;
 import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver;
+import static com.android.server.am.BroadcastQueueTest.withPriority;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
+import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ResolveInfo;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.BundleMerger;
 import android.os.HandlerThread;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.IndentingPrintWriter;
 
 import androidx.test.filters.SmallTest;
 
@@ -53,12 +72,17 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
 import java.util.List;
 
 @SmallTest
 @RunWith(MockitoJUnitRunner.class)
 public class BroadcastQueueModernImplTest {
     private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
+    private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1;
 
     @Mock ActivityManagerService mAms;
     @Mock ProcessRecord mProcess;
@@ -83,6 +107,10 @@
         mHandlerThread.start();
 
         mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+        mConstants.DELAY_URGENT_MILLIS = -120_000;
+        mConstants.DELAY_NORMAL_MILLIS = 10_000;
+        mConstants.DELAY_CACHED_MILLIS = 120_000;
+
         mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
                 mConstants, mConstants);
 
@@ -122,6 +150,18 @@
         }
     }
 
+    private static Intent makeMockIntent() {
+        return mock(Intent.class);
+    }
+
+    private static ResolveInfo makeMockManifestReceiver() {
+        return mock(ResolveInfo.class);
+    }
+
+    private static BroadcastFilter makeMockRegisteredReceiver() {
+        return mock(BroadcastFilter.class);
+    }
+
     private BroadcastRecord makeBroadcastRecord(Intent intent) {
         return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(),
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
@@ -132,6 +172,10 @@
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true);
     }
 
+    private BroadcastRecord makeBroadcastRecord(Intent intent, List receivers) {
+        return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(), receivers, false);
+    }
+
     private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options) {
         return makeBroadcastRecord(intent, options,
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
@@ -139,8 +183,13 @@
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options,
             List receivers, boolean ordered) {
+        return makeBroadcastRecord(intent, options, receivers, null, ordered);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options,
+            List receivers, IIntentReceiver resultTo, boolean ordered) {
         return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, 42, false, null,
-                null, null, null, AppOpsManager.OP_NONE, options, receivers, null,
+                null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
                 Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
                 false, null, false, null);
     }
@@ -269,14 +318,15 @@
                 PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0);
+        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
+                List.of(makeMockRegisteredReceiver()));
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
 
         queue.setProcessCached(false);
         final long notCachedRunnableAt = queue.getRunnableAt();
         queue.setProcessCached(true);
         final long cachedRunnableAt = queue.getRunnableAt();
-        assertTrue(cachedRunnableAt > notCachedRunnableAt);
+        assertThat(cachedRunnableAt).isGreaterThan(notCachedRunnableAt);
         assertEquals(ProcessList.SCHED_GROUP_BACKGROUND, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -289,20 +339,30 @@
         final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
                 PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
 
+        // enqueue a bg-priority broadcast then a fg-priority one
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
+        queue.enqueueOrReplaceBroadcast(timezoneRecord, 0);
+
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
 
+        // verify that:
+        // (a) the queue is immediately runnable by existence of a fg-priority broadcast
+        // (b) the next one up is the fg-priority broadcast despite its later enqueue time
         queue.setProcessCached(false);
         assertTrue(queue.isRunnable());
-        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueClockTime);
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
 
         queue.setProcessCached(true);
         assertTrue(queue.isRunnable());
-        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueClockTime);
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
     }
 
     /**
@@ -316,9 +376,9 @@
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null,
-                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
-                        makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, 1);
+                List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+                        withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 1);
 
         assertFalse(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason());
@@ -339,21 +399,145 @@
                 PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0);
+        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
+                List.of(makeMockRegisteredReceiver()));
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
 
         mConstants.MAX_PENDING_BROADCASTS = 128;
         queue.invalidateRunnableAt();
-        assertTrue(queue.getRunnableAt() > airplaneRecord.enqueueTime);
+        assertThat(queue.getRunnableAt()).isGreaterThan(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
 
         mConstants.MAX_PENDING_BROADCASTS = 1;
         queue.invalidateRunnableAt();
-        assertTrue(queue.getRunnableAt() == airplaneRecord.enqueueTime);
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_MAX_PENDING, queue.getRunnableAtReason());
     }
 
     /**
+     * Verify that a cached process that would normally be delayed becomes
+     * immediately runnable when the given broadcast is enqueued.
+     */
+    private void doRunnableAt_Cached(BroadcastRecord testRecord, int testRunnableAtReason) {
+        final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+        queue.setProcessCached(true);
+
+        final BroadcastRecord lazyRecord = makeBroadcastRecord(
+                new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED),
+                List.of(makeMockRegisteredReceiver()));
+
+        queue.enqueueOrReplaceBroadcast(lazyRecord, 0);
+        assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime);
+        assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason);
+
+        queue.enqueueOrReplaceBroadcast(testRecord, 0);
+        assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime);
+        assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Manifest() {
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
+                List.of(makeMockManifestReceiver()), null, false), REASON_CONTAINS_MANIFEST);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Ordered() {
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
+                List.of(makeMockRegisteredReceiver()), null, true), REASON_CONTAINS_ORDERED);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_ResultTo() {
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
+                List.of(makeMockRegisteredReceiver()), resultTo, false), REASON_CONTAINS_RESULT_TO);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Foreground() {
+        final Intent foregroundIntent = new Intent();
+        foregroundIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        doRunnableAt_Cached(makeBroadcastRecord(foregroundIntent, null,
+                List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_FOREGROUND);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Interactive() {
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setInteractive(true);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_INTERACTIVE);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Alarm() {
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setAlarmBroadcast(true);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_ALARM);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Prioritized() {
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
+                receivers, null, false), REASON_CONTAINS_PRIORITIZED);
+    }
+
+    /**
+     * Confirm that we always prefer running pending items marked as "urgent",
+     * then "normal", then "offload", dispatching by the relative ordering
+     * within each of those clustering groups.
+     */
+    @Test
+    public void testMakeActiveNextPending() {
+        BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+        queue.enqueueOrReplaceBroadcast(
+                makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction());
+
+        // To maximize test coverage, dump current state; we're not worried
+        // about the actual output, just that we don't crash
+        queue.getActive().setDeliveryState(0, BroadcastRecord.DELIVERY_SCHEDULED);
+        queue.dumpLocked(SystemClock.uptimeMillis(),
+                new IndentingPrintWriter(new PrintWriter(new ByteArrayOutputStream())));
+
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_LOCALE_CHANGED, queue.getActive().intent.getAction());
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_TIMEZONE_CHANGED, queue.getActive().intent.getAction());
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_TIME_TICK, queue.getActive().intent.getAction());
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_AIRPLANE_MODE_CHANGED, queue.getActive().intent.getAction());
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_ALARM_CHANGED, queue.getActive().intent.getAction());
+        assertTrue(queue.isEmpty());
+    }
+
+    /**
      * Verify that sending a broadcast that removes any matching pending
      * broadcasts is applied as expected.
      */
@@ -386,4 +570,178 @@
         assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction());
         assertTrue(queue.isEmpty());
     }
+
+    /**
+     * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected.
+     */
+    @Test
+    public void testDeliveryGroupPolicy_mostRecent() {
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic();
+        optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+        final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_MUSIC);
+        final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic();
+        optionsMusicVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsMusicVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_MUSIC));
+
+        final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_ALARM);
+        final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic();
+        optionsAlarmVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsAlarmVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_ALARM));
+
+        // Halt all processing so that we get a consistent view
+        mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        // Verify that the older musicVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, alarmVolumeChanged, musicVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        // Verify that the older alarmVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, musicVolumeChanged, alarmVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        // Verify that the older timeTick has been removed.
+        verifyPendingRecords(queue,
+                List.of(musicVolumeChanged, alarmVolumeChanged, timeTick));
+    }
+
+    /**
+     * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MERGED works as expected.
+     */
+    @Test
+    public void testDeliveryGroupPolicy_merged() {
+        final BundleMerger extrasMerger = new BundleMerger();
+        extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+                BundleMerger.STRATEGY_ARRAY_APPEND);
+
+        final Intent packageChangedForUid = createPackageChangedIntent(TEST_UID,
+                List.of("com.testuid.component1"));
+        final BroadcastOptions optionsPackageChangedForUid = BroadcastOptions.makeBasic();
+        optionsPackageChangedForUid.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+        optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID));
+        optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+        final Intent secondPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+                List.of("com.testuid.component2", "com.testuid.component3"));
+
+        final Intent packageChangedForUid2 = createPackageChangedIntent(TEST_UID2,
+                List.of("com.testuid2.component1"));
+        final BroadcastOptions optionsPackageChangedForUid2 = BroadcastOptions.makeBasic();
+        optionsPackageChangedForUid.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+        optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID2));
+        optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+        // Halt all processing so that we get a consistent view
+        mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid,
+                optionsPackageChangedForUid));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid2,
+                optionsPackageChangedForUid2));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(secondPackageChangedForUid,
+                optionsPackageChangedForUid));
+
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final Intent expectedPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+                List.of("com.testuid.component2", "com.testuid.component3",
+                        "com.testuid.component1"));
+        // Verify that packageChangedForUid and secondPackageChangedForUid broadcasts
+        // have been merged.
+        verifyPendingRecords(queue, List.of(packageChangedForUid2, expectedPackageChangedForUid));
+    }
+
+    private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
+        final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        packageChangedIntent.putExtra(Intent.EXTRA_UID, uid);
+        packageChangedIntent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+                componentNameList.toArray());
+        return packageChangedIntent;
+    }
+
+    private void verifyPendingRecords(BroadcastProcessQueue queue,
+            List<Intent> intents) {
+        for (int i = 0; i < intents.size(); i++) {
+            queue.makeActiveNextPending();
+            final Intent actualIntent = queue.getActive().intent;
+            final Intent expectedIntent = intents.get(i);
+            final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent
+                    + ", actual_extras=" + actualIntent.getExtras()
+                    + ", expected_extras=" + expectedIntent.getExtras();
+            assertTrue(errMsg, actualIntent.filterEquals(expectedIntent));
+            assertBundleEquals(expectedIntent.getExtras(), actualIntent.getExtras());
+        }
+        assertTrue(queue.isEmpty());
+    }
+
+    private void assertBundleEquals(Bundle expected, Bundle actual) {
+        final String errMsg = "expected=" + expected + ", actual=" + actual;
+        if (expected == actual) {
+            return;
+        } else if (expected == null || actual == null) {
+            fail(errMsg);
+        }
+        if (!expected.keySet().equals(actual.keySet())) {
+            fail(errMsg);
+        }
+        for (String key : expected.keySet()) {
+            final Object expectedValue = expected.get(key);
+            final Object actualValue = actual.get(key);
+            if (expectedValue == actualValue) {
+                continue;
+            } else if (expectedValue == null || actualValue == null) {
+                fail(errMsg);
+            }
+            assertEquals(errMsg, expectedValue.getClass(), actualValue.getClass());
+            if (expectedValue.getClass().isArray()) {
+                assertEquals(errMsg, Array.getLength(expectedValue), Array.getLength(actualValue));
+                for (int i = 0; i < Array.getLength(expectedValue); ++i) {
+                    assertEquals(errMsg, Array.get(expectedValue, i), Array.get(actualValue, i));
+                }
+            } else if (expectedValue instanceof ArrayList) {
+                final ArrayList<?> expectedList = (ArrayList<?>) expectedValue;
+                final ArrayList<?> actualList = (ArrayList<?>) actualValue;
+                assertEquals(errMsg, expectedList.size(), actualList.size());
+                for (int i = 0; i < expectedList.size(); ++i) {
+                    assertEquals(errMsg, expectedList.get(i), actualList.get(i));
+                }
+            } else {
+                assertEquals(errMsg, expectedValue, actualValue);
+            }
+        }
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 076fce9..fd605f7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -113,6 +113,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.UnaryOperator;
 
 /**
@@ -154,10 +155,11 @@
     private BroadcastQueue mQueue;
 
     /**
-     * When enabled {@link ActivityManagerService#startProcessLocked} will fail
-     * by returning {@code null}; otherwise it will spawn a new mock process.
+     * Desired behavior of the next
+     * {@link ActivityManagerService#startProcessLocked} call.
      */
-    private boolean mFailStartProcess;
+    private AtomicReference<ProcessStartBehavior> mNextProcessStartBehavior = new AtomicReference<>(
+            ProcessStartBehavior.SUCCESS);
 
     /**
      * Map from PID to registered registered runtime receivers.
@@ -216,16 +218,46 @@
         doAnswer((invocation) -> {
             Log.v(TAG, "Intercepting startProcessLocked() for "
                     + Arrays.toString(invocation.getArguments()));
-            if (mFailStartProcess) {
+            final ProcessStartBehavior behavior = mNextProcessStartBehavior
+                    .getAndSet(ProcessStartBehavior.SUCCESS);
+            if (behavior == ProcessStartBehavior.FAIL_NULL) {
                 return null;
             }
             final String processName = invocation.getArgument(0);
             final ApplicationInfo ai = invocation.getArgument(1);
             final ProcessRecord res = makeActiveProcessRecord(ai, processName,
                     ProcessBehavior.NORMAL, UnaryOperator.identity());
+            final ProcessRecord deliverRes;
+            switch (behavior) {
+                case SUCCESS_PREDECESSOR:
+                case FAIL_TIMEOUT_PREDECESSOR:
+                    // Create a different process that will be linked to the
+                    // returned process via a predecessor/successor relationship
+                    mActiveProcesses.remove(res);
+                    deliverRes = makeActiveProcessRecord(ai, processName,
+                          ProcessBehavior.NORMAL, UnaryOperator.identity());
+                    deliverRes.mPredecessor = res;
+                    res.mSuccessor = deliverRes;
+                    break;
+                default:
+                    deliverRes = res;
+                    break;
+            }
             mHandlerThread.getThreadHandler().post(() -> {
                 synchronized (mAms) {
-                    mQueue.onApplicationAttachedLocked(res);
+                    switch (behavior) {
+                        case SUCCESS:
+                        case SUCCESS_PREDECESSOR:
+                            mQueue.onApplicationAttachedLocked(deliverRes);
+                            break;
+                        case FAIL_TIMEOUT:
+                        case FAIL_TIMEOUT_PREDECESSOR:
+                            mActiveProcesses.remove(deliverRes);
+                            mQueue.onApplicationTimeoutLocked(deliverRes);
+                            break;
+                        default:
+                            throw new UnsupportedOperationException();
+                    }
                 }
             });
             return res;
@@ -248,13 +280,13 @@
         constants.TIMEOUT = 100;
         constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
-            public boolean shouldSkip(BroadcastRecord r, ResolveInfo info) {
+            public boolean shouldSkip(BroadcastRecord r, Object o) {
                 // Ignored
                 return false;
             }
-            public boolean shouldSkip(BroadcastRecord r, BroadcastFilter filter) {
+            public String shouldSkipMessage(BroadcastRecord r, Object o) {
                 // Ignored
-                return false;
+                return null;
             }
         };
         final BroadcastHistory emptyHistory = new BroadcastHistory(constants) {
@@ -281,9 +313,10 @@
 
         // Verify that all processes have finished handling broadcasts
         for (ProcessRecord app : mActiveProcesses) {
-            assertTrue(app.toShortString(), app.mReceivers.numberOfCurReceivers() == 0);
-            assertTrue(app.toShortString(), mQueue.getPreferredSchedulingGroupLocked(app)
-                    == ProcessList.SCHED_GROUP_UNDEFINED);
+            assertEquals(app.toShortString(), 0,
+                    app.mReceivers.numberOfCurReceivers());
+            assertEquals(app.toShortString(), ProcessList.SCHED_GROUP_UNDEFINED,
+                    mQueue.getPreferredSchedulingGroupLocked(app));
         }
     }
 
@@ -325,6 +358,19 @@
         }
     }
 
+    private enum ProcessStartBehavior {
+        /** Process starts successfully */
+        SUCCESS,
+        /** Process starts successfully via predecessor */
+        SUCCESS_PREDECESSOR,
+        /** Process fails by reporting timeout */
+        FAIL_TIMEOUT,
+        /** Process fails by reporting timeout via predecessor */
+        FAIL_TIMEOUT_PREDECESSOR,
+        /** Process fails by immediately returning null */
+        FAIL_NULL,
+    }
+
     private enum ProcessBehavior {
         /** Process broadcasts normally */
         NORMAL,
@@ -457,6 +503,11 @@
         return ai;
     }
 
+    static ResolveInfo withPriority(ResolveInfo info, int priority) {
+        info.priority = priority;
+        return info;
+    }
+
     static ResolveInfo makeManifestReceiver(String packageName, String name) {
         return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
     }
@@ -503,12 +554,6 @@
                 receivers, false, null, null, userId);
     }
 
-    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
-            List<Object> receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) {
-        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
-                receivers, true, orderedResultTo, orderedExtras, UserHandle.USER_SYSTEM);
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers) {
         return makeBroadcastRecord(intent, callerApp, options,
@@ -516,12 +561,24 @@
     }
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, false, resultTo, null, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo, Bundle resultExtras) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, true, resultTo, resultExtras, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers, boolean ordered,
-            IIntentReceiver orderedResultTo, Bundle orderedExtras, int userId) {
+            IIntentReceiver resultTo, Bundle resultExtras, int userId) {
         return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null,
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
-                AppOpsManager.OP_NONE, options, receivers, orderedResultTo, Activity.RESULT_OK,
-                null, orderedExtras, ordered, false, false, userId, false, null,
+                AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo,
+                Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, false, null,
                 false, null);
     }
 
@@ -836,7 +893,7 @@
         }) {
             // Confirm expected OOM adjustments; we were invoked once to upgrade
             // and once to downgrade
-            assertEquals(ActivityManager.PROCESS_STATE_RECEIVER,
+            assertEquals(String.valueOf(receiverApp), ActivityManager.PROCESS_STATE_RECEIVER,
                     receiverApp.mState.getReportedProcState());
             verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp));
 
@@ -845,8 +902,8 @@
                 // cold-started apps to be thawed, but the modern stack does
             } else {
                 // Confirm that app was thawed
-                verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(receiverApp),
-                        eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER));
+                verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()).unfreezeTemporarily(
+                        eq(receiverApp), eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER));
 
                 // Confirm that we added package to process
                 verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName),
@@ -956,18 +1013,16 @@
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
 
         // Send broadcast while process starts are failing
-        mFailStartProcess = true;
+        mNextProcessStartBehavior.set(ProcessStartBehavior.FAIL_NULL);
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
-                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
-                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW))));
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN))));
 
         // Confirm that queue goes idle, with no processes
         waitForIdle();
         assertEquals(1, mActiveProcesses.size());
 
         // Send more broadcasts with working process starts
-        mFailStartProcess = false;
         final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
         enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
@@ -981,7 +1036,6 @@
         final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
                 getUidForPackage(PACKAGE_YELLOW));
         verifyScheduleReceiver(never(), receiverGreenApp, airplane);
-        verifyScheduleReceiver(never(), receiverYellowApp, airplane);
         verifyScheduleReceiver(times(1), receiverGreenApp, timezone);
         verifyScheduleReceiver(times(1), receiverYellowApp, timezone);
     }
@@ -1071,6 +1125,52 @@
                 new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
     }
 
+    @Test
+    public void testCold_Success() throws Exception {
+        doCold(ProcessStartBehavior.SUCCESS);
+    }
+
+    @Test
+    public void testCold_Success_Predecessor() throws Exception {
+        doCold(ProcessStartBehavior.SUCCESS_PREDECESSOR);
+    }
+
+    @Test
+    public void testCold_Fail_Null() throws Exception {
+        doCold(ProcessStartBehavior.FAIL_NULL);
+    }
+
+    @Test
+    public void testCold_Fail_Timeout() throws Exception {
+        doCold(ProcessStartBehavior.FAIL_TIMEOUT);
+    }
+
+    @Test
+    public void testCold_Fail_Timeout_Predecessor() throws Exception {
+        doCold(ProcessStartBehavior.FAIL_TIMEOUT_PREDECESSOR);
+    }
+
+    private void doCold(ProcessStartBehavior behavior) throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+        mNextProcessStartBehavior.set(behavior);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN))));
+        waitForIdle();
+
+        // Regardless of success/failure of above, we should always be able to
+        // recover and begin sending future broadcasts
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN))));
+        waitForIdle();
+
+        final ProcessRecord receiverApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        verifyScheduleReceiver(receiverApp, timezone);
+    }
+
     /**
      * Verify that we skip broadcasts to an app being backed up.
      */
@@ -1258,6 +1358,26 @@
     }
 
     /**
+     * Verify that we deliver results for unordered broadcasts.
+     */
+    @Test
+    public void testUnordered_ResultTo() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final IApplicationThread callerThread = callerApp.getThread();
+
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo));
+
+        waitForIdle();
+        verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)),
+                eq(Activity.RESULT_OK), any(), any(), eq(false),
+                anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
+    }
+
+    /**
      * Verify that we're not surprised by a process attempting to finishing a
      * broadcast when none is in progress.
      */
@@ -1277,8 +1397,8 @@
         final BroadcastRecord r = new BroadcastRecord(mQueue, intent, callerApp,
                 callerApp.info.packageName, null, callerApp.getPid(), callerApp.info.uid, false,
                 null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(),
-                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, Activity.RESULT_OK,
-                null, null, false, false, false, UserHandle.USER_SYSTEM, true,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, null,
+                Activity.RESULT_OK, null, null, false, false, false, UserHandle.USER_SYSTEM, true,
                 backgroundActivityStartsToken, false, null);
         enqueueBroadcast(r);
 
@@ -1484,4 +1604,57 @@
         assertTrue(mQueue.isBeyondBarrierLocked(afterFirst));
         assertTrue(mQueue.isBeyondBarrierLocked(afterSecond));
     }
+
+    /**
+     * Verify that we OOM adjust for manifest receivers.
+     */
+    @Test
+    public void testOomAdjust_Manifest() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_GREEN, CLASS_RED))));
+
+        waitForIdle();
+        verify(mAms, atLeastOnce()).enqueueOomAdjTargetLocked(any());
+    }
+
+    /**
+     * Verify that we never OOM adjust for registered receivers.
+     */
+    @Test
+    public void testOomAdjust_Registered() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeRegisteredReceiver(receiverApp),
+                        makeRegisteredReceiver(receiverApp),
+                        makeRegisteredReceiver(receiverApp))));
+
+        waitForIdle();
+        verify(mAms, never()).enqueueOomAdjTargetLocked(any());
+    }
+
+    /**
+     * Verify that expected events are triggered when a broadcast is finished.
+     */
+    @Test
+    public void testNotifyFinished() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+        final Intent intent = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final BroadcastRecord record = makeBroadcastRecord(intent, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)));
+        enqueueBroadcast(record);
+
+        waitForIdle();
+        verify(mAms).notifyBroadcastFinishedLocked(eq(record));
+        verify(mAms).addBroadcastStatLocked(eq(Intent.ACTION_TIMEZONE_CHANGED), eq(PACKAGE_RED),
+                eq(1), eq(0), anyLong());
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
index 161dfa0..05ed0e2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -24,9 +24,10 @@
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_ALL;
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_BACKGROUND_RESTRICTED_ONLY;
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_NONE;
-import static com.android.server.am.BroadcastRecord.isPrioritized;
+import static com.android.server.am.BroadcastRecord.calculateBlockedUntilTerminalCount;
 import static com.android.server.am.BroadcastRecord.isReceiverEquals;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -99,6 +100,16 @@
         assertFalse(isPrioritized(List.of(createResolveInfo(PACKAGE1, getAppId(1), 0))));
         assertFalse(isPrioritized(List.of(createResolveInfo(PACKAGE1, getAppId(1), -10))));
         assertFalse(isPrioritized(List.of(createResolveInfo(PACKAGE1, getAppId(1), 10))));
+
+        assertArrayEquals(new int[] {-1},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 0)), false));
+        assertArrayEquals(new int[] {-1},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), -10)), false));
+        assertArrayEquals(new int[] {-1},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10)), false));
     }
 
     @Test
@@ -111,6 +122,17 @@
                 createResolveInfo(PACKAGE1, getAppId(1), 10),
                 createResolveInfo(PACKAGE2, getAppId(2), 10),
                 createResolveInfo(PACKAGE3, getAppId(3), 10))));
+
+        assertArrayEquals(new int[] {-1,-1,-1},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 0),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false));
+        assertArrayEquals(new int[] {-1,-1,-1},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false));
     }
 
     @Test
@@ -123,6 +145,19 @@
                 createResolveInfo(PACKAGE1, getAppId(1), 0),
                 createResolveInfo(PACKAGE2, getAppId(2), 0),
                 createResolveInfo(PACKAGE3, getAppId(3), 10))));
+
+        assertArrayEquals(new int[] {0,1,2},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), -10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false));
+        assertArrayEquals(new int[] {0,0,2,3,3},
+                calculateBlockedUntilTerminalCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 0),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 20)), false));
     }
 
     @Test
@@ -525,6 +560,7 @@
                 0 /* appOp */,
                 null /* options */,
                 new ArrayList<>(receivers), // Make a copy to not affect the original list.
+                null /* resultToApp */,
                 null /* resultTo */,
                 0 /* resultCode */,
                 null /* resultData */,
@@ -542,4 +578,9 @@
     private static int getAppId(int i) {
         return Process.FIRST_APPLICATION_UID + i;
     }
+
+    private static boolean isPrioritized(List<Object> receivers) {
+        return BroadcastRecord.isPrioritized(
+                calculateBlockedUntilTerminalCount(receivers, false), false);
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index 9022db8..24e5175 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -73,7 +73,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
@@ -93,6 +95,7 @@
     private static final String PACKAGE_NAME_INVALID = "com.android.app";
     private static final int USER_ID_1 = 1001;
     private static final int USER_ID_2 = 1002;
+    private static final int DEFAULT_PACKAGE_UID = 12345;
 
     private MockitoSession mMockingSession;
     private String mPackageName;
@@ -207,6 +210,8 @@
                 .thenReturn(packages);
         when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
                 .thenReturn(applicationInfo);
+        when(mMockPackageManager.getPackageUidAsUser(mPackageName, USER_ID_1)).thenReturn(
+                DEFAULT_PACKAGE_UID);
         LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
     }
 
@@ -382,38 +387,41 @@
                 .thenReturn(applicationInfo);
     }
 
-    private void mockInterventionsEnabledFromXml() throws Exception {
-        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
-                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
-        Bundle metaDataBundle = new Bundle();
-        final int resId = 123;
-        metaDataBundle.putInt(
-                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
-        applicationInfo.metaData = metaDataBundle;
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-        seedGameManagerServiceMetaDataFromFile(mPackageName, resId,
-                "res/xml/gama_manager_service_metadata_config_enabled.xml");
+    private void mockInterventionsEnabledNoOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml");
     }
 
-    private void mockInterventionsDisabledFromXml() throws Exception {
-        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
-                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
-        Bundle metaDataBundle = new Bundle();
-        final int resId = 123;
-        metaDataBundle.putInt(
-                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
-        applicationInfo.metaData = metaDataBundle;
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-        seedGameManagerServiceMetaDataFromFile(mPackageName, resId,
-                "res/xml/gama_manager_service_metadata_config_disabled.xml");
+    private void mockInterventionsEnabledAllOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in"
+                        + ".xml");
+    }
+
+    private void mockInterventionsDisabledNoOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in"
+                        + ".xml");
+    }
+
+    private void mockInterventionsDisabledAllOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in"
+                        + ".xml");
     }
 
 
     private void seedGameManagerServiceMetaDataFromFile(String packageName, int resId,
             String fileName)
             throws Exception {
+        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
+                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
+        Bundle metaDataBundle = new Bundle();
+        metaDataBundle.putInt(
+                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
+        applicationInfo.metaData = metaDataBundle;
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(applicationInfo);
         AssetManager assetManager =
                 InstrumentationRegistry.getInstrumentation().getContext().getAssets();
         XmlResourceParser xmlResourceParser =
@@ -641,6 +649,12 @@
         assertEquals(fps, config.getGameModeConfiguration(gameMode).getFps());
     }
 
+    private boolean checkOptedIn(GameManagerService gameManagerService, int gameMode) {
+        GameManagerService.GamePackageConfiguration config =
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
+        return config.willGamePerformOptimizations(gameMode);
+    }
+
     /**
      * Phenotype device config exists, but is only propagating the default value.
      */
@@ -756,7 +770,7 @@
      * Override device configs for both battery and performance modes exists and are valid.
      */
     @Test
-    public void testSetDeviceOverrideConfigAll() {
+    public void testSetDeviceConfigOverrideAll() {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
 
@@ -776,6 +790,75 @@
         checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 60);
     }
 
+    @Test
+    public void testSetBatteryModeConfigOverride_thenUpdateAllDeviceConfig() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90:mode=3,downscaleFactor=0.1,fps=30";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        startUser(gameManagerService, USER_ID_1);
+
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 1.0f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.1f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 30);
+
+        gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, 3, "40",
+                "0.2");
+
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+
+        String configStringAfter =
+                "mode=2,downscaleFactor=0.9,fps=60:mode=3,downscaleFactor=0.3,fps=50";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringAfter);
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+
+        // performance mode was not overridden thus it should be updated
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0.9f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 60);
+
+        // battery mode was overridden thus it should be the same as the override
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+    }
+
+    @Test
+    public void testSetBatteryModeConfigOverride_thenOptInBatteryMode() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90:mode=3,downscaleFactor=0.1,fps=30";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsDisabledNoOptInFromXml();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        startUser(gameManagerService, USER_ID_1);
+
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+
+        gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, 3, "40",
+                "0.2");
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+        // override will enable the interventions
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+
+        mockInterventionsDisabledAllOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+
+        assertTrue(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+        // opt-in is still false for battery mode as override exists
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+    }
+
     /**
      * Override device config for performance mode exists and is valid.
      */
@@ -1050,7 +1133,7 @@
         gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_PERFORMANCE,
                 gameManagerService.getGameMode(mPackageName, USER_ID_1));
-        mockInterventionsEnabledFromXml();
+        mockInterventionsEnabledNoOptInFromXml();
         checkLoadingBoost(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
     }
 
@@ -1058,7 +1141,7 @@
     public void testGameModeConfigAllowFpsTrue() throws Exception {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
-        mockInterventionsEnabledFromXml();
+        mockInterventionsEnabledNoOptInFromXml();
         GameManagerService gameManagerService = new GameManagerService(mMockContext,
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
@@ -1073,7 +1156,7 @@
     public void testGameModeConfigAllowFpsFalse() throws Exception {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
-        mockInterventionsDisabledFromXml();
+        mockInterventionsDisabledNoOptInFromXml();
         GameManagerService gameManagerService = new GameManagerService(mMockContext,
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
@@ -1424,6 +1507,39 @@
     }
 
     @Test
+    public void testSwitchUser() {
+        mockManageUsersGranted();
+        mockModifyGameModeGranted();
+
+        mockDeviceConfigBattery();
+        final Context context = InstrumentationRegistry.getContext();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper(), context.getFilesDir());
+        startUser(gameManagerService, USER_ID_1);
+        startUser(gameManagerService, USER_ID_2);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1);
+        checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD,
+                GameManager.GAME_MODE_BATTERY);
+        assertEquals(gameManagerService.getGameMode(mPackageName, USER_ID_1),
+                GameManager.GAME_MODE_BATTERY);
+
+        mockDeviceConfigAll();
+        switchUser(gameManagerService, USER_ID_1, USER_ID_2);
+        assertEquals(gameManagerService.getGameMode(mPackageName, USER_ID_2),
+                GameManager.GAME_MODE_STANDARD);
+        checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD,
+                GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_2);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1);
+
+        switchUser(gameManagerService, USER_ID_2, USER_ID_1);
+        checkReportedModes(gameManagerService, GameManager.GAME_MODE_STANDARD,
+                GameManager.GAME_MODE_BATTERY, GameManager.GAME_MODE_PERFORMANCE);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_2);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_BATTERY, USER_ID_1);
+    }
+
+    @Test
     public void testUpdateResolutionScalingFactor() {
         mockModifyGameModeGranted();
         mockDeviceConfigBattery();
@@ -1551,6 +1667,82 @@
         assertFalse(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1));
     }
 
+    @Test
+    public void testResetInterventions_onDeviceConfigReset() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        String configStringAfter = "";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringAfter);
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+    }
+
+    @Test
+    public void testResetInterventions_onInterventionsDisabled() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        mockInterventionsDisabledNoOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+    }
+
+    @Test
+    public void testResetInterventions_onGameModeOptedIn() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        mockInterventionsEnabledAllOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+    }
+
     private static void deleteFolder(File folder) {
         File[] files = folder.listFiles();
         if (files != null) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index e1713b0..98e895a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -22,6 +22,7 @@
 import static android.app.AppOpsManager.OP_CAMERA;
 import static android.app.AppOpsManager.OP_COARSE_LOCATION;
 import static android.app.AppOpsManager.OP_FINE_LOCATION;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_WIFI_SCAN;
 import static android.app.AppOpsManager.UID_STATE_BACKGROUND;
@@ -127,6 +128,8 @@
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -137,6 +140,8 @@
                 .update();
 
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
 
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -151,6 +156,8 @@
                 .update();
 
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
 
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
@@ -169,6 +176,8 @@
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -183,6 +192,8 @@
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -197,6 +208,8 @@
 
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -211,6 +224,8 @@
 
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -314,6 +329,8 @@
                 .update();
 
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -328,6 +345,8 @@
                 .update();
 
         assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+        assertEquals(MODE_IGNORED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -403,6 +422,8 @@
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -418,6 +439,8 @@
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
@@ -433,6 +456,8 @@
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
         assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+        assertEquals(MODE_ALLOWED,
+                mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
index 7111047..e08a715 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
@@ -36,13 +36,14 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java
new file mode 100644
index 0000000..f535997
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAndRestoreFeatureFlagsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.backup;
+
+import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.modules.utils.testing.TestableDeviceConfig;
+
+import static com.google.common.truth.Truth.assertThat;
+
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BackupAndRestoreFeatureFlagsTest {
+    @Rule
+    public TestableDeviceConfig.TestableDeviceConfigRule
+            mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
+
+    @Test
+    public void getBackupTransportFutureTimeoutMillis_notSet_returnsDefault() {
+        assertThat(
+                BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis()).isEqualTo(
+                600000);
+    }
+
+    @Test
+    public void getBackupTransportFutureTimeoutMillis_set_returnsSetValue() {
+        DeviceConfig.setProperty("backup_and_restore", "backup_transport_future_timeout_millis",
+                "1234", false);
+
+        assertThat(
+                BackupAndRestoreFeatureFlags.getBackupTransportFutureTimeoutMillis()).isEqualTo(
+                1234);
+    }
+
+    @Test
+    public void getBackupTransportCallbackTimeoutMillis_notSet_returnsDefault() {
+        assertThat(
+                BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis()).isEqualTo(
+                300000);
+    }
+
+    @Test
+    public void getBackupTransportCallbackTimeoutMillis_set_returnsSetValue() {
+        DeviceConfig.setProperty("backup_and_restore", "backup_transport_callback_timeout_millis",
+                "5678", false);
+
+        assertThat(
+                BackupAndRestoreFeatureFlags.getBackupTransportCallbackTimeoutMillis()).isEqualTo(
+                5678);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS b/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS
new file mode 100644
index 0000000..d99779e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/OWNERS
@@ -0,0 +1 @@
+include /services/backup/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index 20af02e..4c28c51 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -33,6 +33,7 @@
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.test.TestLooper;
 import android.util.FloatProperty;
@@ -135,6 +136,16 @@
                     DisplayPowerCallbacks displayPowerCallbacks) {
                 return mWakelockController;
             }
+
+            @Override
+            DisplayPowerProximityStateController getDisplayPowerProximityStateController(
+                    WakelockController wakelockController, DisplayDeviceConfig displayDeviceConfig,
+                    Looper looper, Runnable nudgeUpdatePowerState, int displayId,
+                    SensorManager sensorManager) {
+                return new DisplayPowerProximityStateController(wakelockController,
+                        displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
+                        sensorManager, /* injector= */ null);
+            }
         };
 
         addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
new file mode 100644
index 0000000..6e91b24
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
@@ -0,0 +1,407 @@
+/*
+ * 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.display;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.test.TestLooper;
+import android.view.Display;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class DisplayPowerProximityStateControllerTest {
+    @Mock
+    WakelockController mWakelockController;
+
+    @Mock
+    DisplayDeviceConfig mDisplayDeviceConfig;
+
+    @Mock
+    Runnable mNudgeUpdatePowerState;
+
+    @Mock
+    SensorManager mSensorManager;
+
+    private Sensor mProximitySensor;
+    private OffsettableClock mClock;
+    private TestLooper mTestLooper;
+    private SensorEventListener mSensorEventListener;
+    private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
+
+    @Before
+    public void before() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mClock = new OffsettableClock.Stopped();
+        mTestLooper = new TestLooper(mClock::now);
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData() {
+                    {
+                        type = Sensor.STRING_TYPE_PROXIMITY;
+                        // This is kept null because currently there is no way to define a sensor
+                        // name in TestUtils
+                        name = null;
+                    }
+                });
+        setUpProxSensor();
+        DisplayPowerProximityStateController.Injector injector =
+                new DisplayPowerProximityStateController.Injector() {
+                    @Override
+                    DisplayPowerProximityStateController.Clock createClock() {
+                        return new DisplayPowerProximityStateController.Clock() {
+                            @Override
+                            public long uptimeMillis() {
+                                return mClock.now();
+                            }
+                        };
+                    }
+                };
+        mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
+                mNudgeUpdatePowerState, 0,
+                mSensorManager, injector);
+        mSensorEventListener = mDisplayPowerProximityStateController.getProximitySensorListener();
+    }
+
+    @Test
+    public void updatePendingProximityRequestsWorksAsExpectedWhenPending() {
+        // Set the system to pending wait for proximity
+        assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(
+                true));
+        assertTrue(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+
+        // Update the pending proximity wait request
+        mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
+        assertTrue(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+    }
+
+    @Test
+    public void updatePendingProximityRequestsWorksAsExpectedWhenNotPending() {
+        // Will not wait or be in the pending wait state of not already pending
+        mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+    }
+
+    @Test
+    public void updatePendingProximityRequestsWorksAsExpectedWhenPendingAndProximityIgnored()
+            throws Exception {
+        // Set the system to the state where it will ignore proximity unless changed
+        enableProximitySensor();
+        emitAndValidatePositiveProximityEvent();
+        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChangedInternal();
+        advanceTime(1);
+        assertTrue(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        verify(mNudgeUpdatePowerState, times(2)).run();
+
+        // Do not set the system to pending wait for proximity
+        mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+
+        // Set the system to pending wait for proximity. But because the proximity is being
+        // ignored, it will not wait or not set the pending wait
+        assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(
+                true));
+        mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+    }
+
+    @Test
+    public void cleanupDisablesTheProximitySensor() {
+        enableProximitySensor();
+        mDisplayPowerProximityStateController.cleanup();
+        verify(mSensorManager).unregisterListener(
+                mSensorEventListener);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertEquals(mDisplayPowerProximityStateController.getProximity(),
+                DisplayPowerProximityStateController.PROXIMITY_UNKNOWN);
+        when(mWakelockController.releaseWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true);
+        assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1);
+    }
+
+    @Test
+    public void isProximitySensorAvailableReturnsTrueWhenAvailable() {
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
+    public void isProximitySensorAvailableReturnsFalseWhenNotAvailable() {
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData() {
+                    {
+                        type = null;
+                        name = null;
+                    }
+                });
+        mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
+                mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
+                mNudgeUpdatePowerState, 1,
+                mSensorManager, null);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
+    public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception {
+        DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class);
+        when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData() {
+                    {
+                        type = Sensor.STRING_TYPE_PROXIMITY;
+                        name = null;
+                    }
+                });
+        Sensor newProxSensor = TestUtils.createSensor(
+                Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f);
+        when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
+                .thenReturn(List.of(newProxSensor));
+        mDisplayPowerProximityStateController.notifyDisplayDeviceChanged(
+                updatedDisplayDeviceConfig);
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorAvailable());
+    }
+
+    @Test
+    public void setPendingWaitForNegativeProximityLockedWorksAsExpected() {
+        // Doesn't do anything not asked to wait
+        assertFalse(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(
+                false));
+        assertFalse(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+
+        // Sets pending wait negative proximity if not already waiting
+        assertTrue(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(
+                true));
+        assertTrue(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+
+        // Will not set pending wait negative proximity if already waiting
+        assertFalse(mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(
+                true));
+        assertTrue(
+                mDisplayPowerProximityStateController.getPendingWaitForNegativeProximityLocked());
+
+    }
+
+    @Test
+    public void evaluateProximityStateWhenRequestedUseOfProximitySensor() throws Exception {
+        // Enable the proximity sensor
+        enableProximitySensor();
+
+        // Emit a positive proximity event to move the system to a state to mimic a scenario
+        // where the system is in positive proximity
+        emitAndValidatePositiveProximityEvent();
+
+        // Again evaluate the proximity state, with system having positive proximity
+        setScreenOffBecauseOfPositiveProximityState();
+    }
+
+    @Test
+    public void evaluateProximityStateWhenScreenOffBecauseOfPositiveProximity() throws Exception {
+        // Enable the proximity sensor
+        enableProximitySensor();
+
+        // Emit a positive proximity event to move the system to a state to mimic a scenario
+        // where the system is in positive proximity
+        emitAndValidatePositiveProximityEvent();
+
+        // Again evaluate the proximity state, with system having positive proximity
+        setScreenOffBecauseOfPositiveProximityState();
+
+        // Set the system to pending wait for proximity
+        mDisplayPowerProximityStateController.setPendingWaitForNegativeProximityLocked(true);
+        // Update the pending proximity wait request
+        mDisplayPowerProximityStateController.updatePendingProximityRequestsLocked();
+
+        // Start ignoring proximity sensor
+        mDisplayPowerProximityStateController.ignoreProximitySensorUntilChangedInternal();
+        // Re-evaluate the proximity state, such that the system is detecting the positive
+        // proximity, and screen is off because of that
+        when(mWakelockController.getOnProximityNegativeRunnable()).thenReturn(mock(Runnable.class));
+        mDisplayPowerProximityStateController.updateProximityState(mock(
+                DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_ON);
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity());
+        assertTrue(
+                mDisplayPowerProximityStateController
+                        .shouldSkipRampBecauseOfProximityChangeToNegative());
+        verify(mWakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE);
+    }
+
+    @Test
+    public void evaluateProximityStateWhenDisplayIsTurningOff() throws Exception {
+        // Enable the proximity sensor
+        enableProximitySensor();
+
+        // Emit a positive proximity event to move the system to a state to mimic a scenario
+        // where the system is in positive proximity
+        emitAndValidatePositiveProximityEvent();
+
+        // Again evaluate the proximity state, with system having positive proximity
+        setScreenOffBecauseOfPositiveProximityState();
+
+        // Re-evaluate the proximity state, such that the system is detecting the positive
+        // proximity, and screen is off because of that
+        mDisplayPowerProximityStateController.updateProximityState(mock(
+                DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_OFF);
+        verify(mSensorManager).unregisterListener(
+                mSensorEventListener);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertEquals(mDisplayPowerProximityStateController.getProximity(),
+                DisplayPowerProximityStateController.PROXIMITY_UNKNOWN);
+        when(mWakelockController.releaseWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true);
+        assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1);
+    }
+
+    @Test
+    public void evaluateProximityStateNotWaitingForNegativeProximityAndNotUsingProxSensor()
+            throws Exception {
+        // Enable the proximity sensor
+        enableProximitySensor();
+
+        // Emit a positive proximity event to move the system to a state to mimic a scenario
+        // where the system is in positive proximity
+        emitAndValidatePositiveProximityEvent();
+
+        // Re-evaluate the proximity state, such that the system is detecting the positive
+        // proximity, and screen is off because of that
+        mDisplayPowerProximityStateController.updateProximityState(mock(
+                DisplayManagerInternal.DisplayPowerRequest.class), Display.STATE_ON);
+        verify(mSensorManager).unregisterListener(
+                mSensorEventListener);
+        assertFalse(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.getWaitingForNegativeProximity());
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertEquals(mDisplayPowerProximityStateController.getProximity(),
+                DisplayPowerProximityStateController.PROXIMITY_UNKNOWN);
+        when(mWakelockController.releaseWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true);
+        assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1);
+    }
+
+    private void advanceTime(long timeMs) {
+        mClock.fastForward(timeMs);
+        mTestLooper.dispatchAll();
+    }
+
+    private void setUpProxSensor() throws Exception {
+        mProximitySensor = TestUtils.createSensor(
+                Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 5.0f);
+        when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
+                .thenReturn(List.of(mProximitySensor));
+    }
+
+    private void emitAndValidatePositiveProximityEvent() throws Exception {
+        // Emit a positive proximity event to move the system to a state to mimic a scenario
+        // where the system is in positive proximity
+        when(mWakelockController.releaseWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE)).thenReturn(true);
+        mSensorEventListener.onSensorChanged(TestUtils.createSensorEvent(mProximitySensor, 4));
+        verify(mSensorManager).registerListener(mSensorEventListener,
+                mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL,
+                mDisplayPowerProximityStateController.getHandler());
+        verify(mWakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
+        assertEquals(mDisplayPowerProximityStateController.getPendingProximity(),
+                DisplayPowerProximityStateController.PROXIMITY_POSITIVE);
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertEquals(mDisplayPowerProximityStateController.getProximity(),
+                DisplayPowerProximityStateController.PROXIMITY_POSITIVE);
+        verify(mNudgeUpdatePowerState).run();
+        assertEquals(mDisplayPowerProximityStateController.getPendingProximityDebounceTime(), -1);
+    }
+
+    // Call evaluateProximityState with the request for using the proximity sensor. This will
+    // register the proximity sensor listener, which will be needed for mocking positive
+    // proximity scenarios.
+    private void enableProximitySensor() {
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+        displayPowerRequest.useProximitySensor = true;
+        mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest,
+                Display.STATE_ON);
+        verify(mSensorManager).registerListener(
+                mSensorEventListener,
+                mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL,
+                mDisplayPowerProximityStateController.getHandler());
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertFalse(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity());
+        verifyZeroInteractions(mWakelockController);
+    }
+
+    private void setScreenOffBecauseOfPositiveProximityState() {
+        // Prepare a request to indicate that the proximity sensor is to be used
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
+                DisplayManagerInternal.DisplayPowerRequest.class);
+        displayPowerRequest.useProximitySensor = true;
+
+        Runnable onProximityPositiveRunnable = mock(Runnable.class);
+        when(mWakelockController.getOnProximityPositiveRunnable()).thenReturn(
+                onProximityPositiveRunnable);
+
+        mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest,
+                Display.STATE_ON);
+        verify(mSensorManager).registerListener(
+                mSensorEventListener,
+                mProximitySensor, SensorManager.SENSOR_DELAY_NORMAL,
+                mDisplayPowerProximityStateController.getHandler());
+        assertTrue(mDisplayPowerProximityStateController.isProximitySensorEnabled());
+        assertFalse(mDisplayPowerProximityStateController.shouldIgnoreProximityUntilChanged());
+        assertTrue(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity());
+        verify(mWakelockController).acquireWakelock(
+                WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 3866da3..d41ac70 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -35,7 +35,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -43,6 +42,8 @@
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -75,6 +76,11 @@
     private static final int PORT_A = 0;
     private static final int PORT_B = 0x80;
     private static final int PORT_C = 0xFF;
+    private static final float REFRESH_RATE = 60f;
+    private static final RefreshRateRange REFRESH_RATE_RANGE =
+            new RefreshRateRange(REFRESH_RATE, REFRESH_RATE);
+    private static final RefreshRateRanges REFRESH_RATE_RANGES =
+            new RefreshRateRanges(REFRESH_RATE_RANGE, REFRESH_RATE_RANGE);
 
     private static final long HANDLER_WAIT_MS = 100;
 
@@ -697,16 +703,14 @@
                 new DisplayModeDirector.DesiredDisplayModeSpecs(
                         /*baseModeId*/ baseModeId,
                         /*allowGroupSwitching*/ false,
-                        new RefreshRateRange(60f, 60f),
-                        new RefreshRateRange(60f, 60f)
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
         waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
         verify(mSurfaceControlProxy).setDesiredDisplayModeSpecs(display.token,
                 new SurfaceControl.DesiredDisplayModeSpecs(
                         /* baseModeId */ 0,
                         /* allowGroupSwitching */ false,
-                        /* primaryRange */ 60f, 60f,
-                        /* appRange */ 60f, 60f
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
 
         // Change the display
@@ -732,8 +736,7 @@
                 new DisplayModeDirector.DesiredDisplayModeSpecs(
                         /*baseModeId*/ baseModeId,
                         /*allowGroupSwitching*/ false,
-                        new RefreshRateRange(60f, 60f),
-                        new RefreshRateRange(60f, 60f)
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
 
         waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
@@ -743,8 +746,7 @@
                 new SurfaceControl.DesiredDisplayModeSpecs(
                         /* baseModeId */ 2,
                         /* allowGroupSwitching */ false,
-                        /* primaryRange */ 60f, 60f,
-                        /* appRange */ 60f, 60f
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
     }
 
@@ -922,12 +924,11 @@
         }
 
         public SurfaceControl.DesiredDisplayModeSpecs desiredDisplayModeSpecs =
-                new SurfaceControl.DesiredDisplayModeSpecs(/* defaultMode */ 0,
-                    /* allowGroupSwitching */ false,
-                    /* primaryRefreshRateMin */ 60.f,
-                    /* primaryRefreshRateMax */ 60.f,
-                    /* appRefreshRateMin */ 60.f,
-                    /* appRefreshRateMax */60.f);
+                new SurfaceControl.DesiredDisplayModeSpecs(
+                        /* defaultMode */ 0,
+                        /* allowGroupSwitching */ false,
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
+                );
 
         private FakeDisplay(int port) {
             address = createDisplayAddress(port);
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..6e4d214 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -16,38 +16,13 @@
 
 package com.android.server.job;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+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 junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.anyLong;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.annotation.Nullable;
-import android.app.ActivityManagerInternal;
-import android.app.AppGlobals;
-import android.app.job.JobInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.IPackageManager;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.provider.DeviceConfig;
-import android.util.ArraySet;
-
-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;
@@ -56,6 +31,41 @@
 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;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AppGlobals;
+import android.app.IActivityManager;
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
 import com.android.internal.R;
 import com.android.internal.app.IBatteryStats;
 import com.android.server.LocalServices;
@@ -73,6 +83,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;
@@ -97,12 +108,23 @@
     @Mock
     private IPackageManager mIPackageManager;
 
-    static class InjectorForTest extends JobConcurrencyManager.Injector {
+    private static class InjectorForTest extends JobConcurrencyManager.Injector {
+        public final ArrayMap<JobServiceContext, JobStatus> contexts = new ArrayMap<>();
+
         @Override
         JobServiceContext createJobServiceContext(JobSchedulerService service,
                 JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
                 JobPackageTracker tracker, Looper looper) {
-            return mock(JobServiceContext.class);
+            final JobServiceContext context = mock(JobServiceContext.class);
+            doAnswer((Answer<Boolean>) invocationOnMock -> {
+                Object[] args = invocationOnMock.getArguments();
+                final JobStatus job = (JobStatus) args[0];
+                contexts.put(context, job);
+                doReturn(job).when(context).getRunningJobLocked();
+                return true;
+            }).when(context).executeRunnableJob(any(), anyInt());
+            contexts.put(context, null);
+            return context;
         }
     }
 
@@ -124,6 +146,7 @@
         mMockingSession = mockitoSession()
                 .initMocks(this)
                 .mockStatic(AppGlobals.class)
+                .spyStatic(DeviceConfig.class)
                 .strictness(Strictness.LENIENT)
                 .startMocking();
         final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class);
@@ -133,11 +156,22 @@
                 R.bool.config_jobSchedulerRestrictBackgroundUser);
         when(mContext.getResources()).thenReturn(mResources);
         doReturn(mContext).when(jobSchedulerService).getTestableContext();
+        doReturn(jobSchedulerService).when(jobSchedulerService).getLock();
         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);
+        doReturn(mock(PowerManager.class)).when(mContext).getSystemService(PowerManager.class);
         mInjector = new InjectorForTest();
+        doAnswer((Answer<Long>) invocationOnMock -> {
+            Object[] args = invocationOnMock.getArguments();
+            final JobStatus job = (JobStatus) args[0];
+            return job.shouldTreatAsExpeditedJob()
+                    ? JobSchedulerService.Constants.DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS
+                    : JobSchedulerService.Constants.DEFAULT_RUNTIME_MIN_GUARANTEE_MS;
+        }).when(jobSchedulerService).getMinJobExecutionGuaranteeMs(any());
         mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService, mInjector);
         mGracePeriodObserver = mock(GracePeriodObserver.class);
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -146,6 +180,16 @@
         createCurrentUser(true);
         mNextUserId = 10;
         mJobConcurrencyManager.mGracePeriodObserver = mGracePeriodObserver;
+
+        IActivityManager activityManager = ActivityManager.getService();
+        spyOn(activityManager);
+        try {
+            doNothing().when(activityManager).registerUserSwitchObserver(any(), anyString());
+        } catch (RemoteException e) {
+            fail("registerUserSwitchObserver threw exception: " + e.getMessage());
+        }
+
+        mJobConcurrencyManager.onSystemReady();
     }
 
     @After
@@ -163,32 +207,93 @@
         final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
         final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
         final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
-        mJobConcurrencyManager
-                .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
 
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
         assertEquals(0, preferredUidOnly.size());
         assertEquals(0, stoppable.size());
+        assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(0, assignmentInfo.numRunningTopEj);
     }
 
     @Test
     public void testPrepareForAssignmentDetermination_onlyPendingJobs() {
-        final ArraySet<JobStatus> jobs = new ArraySet<>();
         for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
             JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
             mPendingJobQueue.add(job);
-            jobs.add(job);
         }
 
         final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
         final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
         final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
-        mJobConcurrencyManager
-                .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
 
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
         assertEquals(0, preferredUidOnly.size());
         assertEquals(0, stoppable.size());
+        assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(0, assignmentInfo.numRunningTopEj);
+    }
+
+    @Test
+    public void testPrepareForAssignmentDetermination_onlyPreferredUidOnly() {
+        for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
+            JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
+            mJobConcurrencyManager.addRunningJobForTesting(job);
+        }
+
+        for (int i = 0; i < mInjector.contexts.size(); ++i) {
+            doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime();
+        }
+
+        final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+        final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+        final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
+
+        assertEquals(0, idle.size());
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+        assertEquals(0, stoppable.size());
+        assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(0, assignmentInfo.numRunningTopEj);
+    }
+
+    @Test
+    public void testPrepareForAssignmentDetermination_onlyRunningTopEjs() {
+        for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
+            JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
+            job.startedAsExpeditedJob = true;
+            job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
+            mJobConcurrencyManager.addRunningJobForTesting(job);
+        }
+
+        for (int i = 0; i < mInjector.contexts.size(); ++i) {
+            doReturn(i % 2 == 0).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime();
+        }
+
+        final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+        final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+        final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
+
+        assertEquals(0, idle.size());
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, preferredUidOnly.size());
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, stoppable.size());
+        assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
+                assignmentInfo.numRunningTopEj);
     }
 
     @Test
@@ -209,10 +314,13 @@
         final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
         final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
         final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
         mJobConcurrencyManager
-                .prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
-        mJobConcurrencyManager
-                .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable);
+                .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable,
+                        assignmentInfo);
 
         assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size());
         for (int i = changed.size() - 1; i >= 0; --i) {
@@ -222,6 +330,175 @@
     }
 
     @Test
+    public void testDetermineAssignments_allPreferredUidOnly_shortTimeLeft() throws Exception {
+        mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true);
+        setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
+                new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT));
+        for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) {
+            final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i;
+            final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid);
+            setPackageUid(sourcePkgName, uid);
+            final JobStatus job = createJob(uid, sourcePkgName);
+            spyOn(job);
+            doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob();
+            if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) {
+                mJobConcurrencyManager.addRunningJobForTesting(job);
+            } else {
+                mPendingJobQueue.add(job);
+            }
+        }
+
+        // Waiting time is too short, so we shouldn't create any extra contexts.
+        final long remainingTimeMs = JobConcurrencyManager.DEFAULT_MAX_WAIT_EJ_MS / 2;
+        for (int i = 0; i < mInjector.contexts.size(); ++i) {
+            doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime();
+            doReturn(remainingTimeMs)
+                    .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong());
+        }
+
+        final ArraySet<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
+        final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+        final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+        final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
+        assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+
+        mJobConcurrencyManager
+                .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable,
+                        assignmentInfo);
+
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+        assertEquals(0, changed.size());
+    }
+
+    @Test
+    public void testDetermineAssignments_allPreferredUidOnly_mediumTimeLeft() throws Exception {
+        mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true);
+        setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
+                new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT));
+        final ArraySet<JobStatus> jobs = new ArraySet<>();
+        for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) {
+            final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i;
+            final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid);
+            setPackageUid(sourcePkgName, uid);
+            final JobStatus job = createJob(uid, sourcePkgName);
+            spyOn(job);
+            doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob();
+            if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) {
+                mJobConcurrencyManager.addRunningJobForTesting(job);
+            } else {
+                mPendingJobQueue.add(job);
+                jobs.add(job);
+            }
+        }
+
+        // Waiting time is longer than the EJ waiting time, but shorter than regular job waiting
+        // time, so we should only create an extra context for an EJ.
+        final long remainingTimeMs = (JobConcurrencyManager.DEFAULT_MAX_WAIT_EJ_MS
+                + JobConcurrencyManager.DEFAULT_MAX_WAIT_REGULAR_MS) / 2;
+        for (int i = 0; i < mInjector.contexts.size(); ++i) {
+            doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime();
+            doReturn(remainingTimeMs)
+                    .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong());
+        }
+
+        final ArraySet<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
+        final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+        final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+        final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
+        assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+
+        mJobConcurrencyManager
+                .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable,
+                        assignmentInfo);
+
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+        for (int i = changed.size() - 1; i >= 0; --i) {
+            jobs.remove(changed.valueAt(i).newJob);
+        }
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - 1, jobs.size());
+        assertEquals(1, changed.size());
+        JobStatus assignedJob = changed.valueAt(0).newJob;
+        assertTrue(assignedJob.shouldTreatAsExpeditedJob());
+    }
+
+    @Test
+    public void testDetermineAssignments_allPreferredUidOnly_longTimeLeft() throws Exception {
+        mConfigBuilder.setBoolean(JobConcurrencyManager.KEY_ENABLE_MAX_WAIT_TIME_BYPASS, true);
+        setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
+                new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT));
+        final ArraySet<JobStatus> jobs = new ArraySet<>();
+        for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT * 2; ++i) {
+            final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i;
+            final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid);
+            setPackageUid(sourcePkgName, uid);
+            final JobStatus job = createJob(uid, sourcePkgName);
+            spyOn(job);
+            doReturn(i % 2 == 0).when(job).shouldTreatAsExpeditedJob();
+            if (i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT) {
+                mJobConcurrencyManager.addRunningJobForTesting(job);
+            } else {
+                mPendingJobQueue.add(job);
+                jobs.add(job);
+            }
+        }
+
+        // Waiting time is longer than even the regular job waiting time, so we should
+        // create an extra context for an EJ, and potentially one for a regular job.
+        final long remainingTimeMs = 2 * JobConcurrencyManager.DEFAULT_MAX_WAIT_REGULAR_MS;
+        for (int i = 0; i < mInjector.contexts.size(); ++i) {
+            doReturn(true).when(mInjector.contexts.keyAt(i)).isWithinExecutionGuaranteeTime();
+            doReturn(remainingTimeMs)
+                    .when(mInjector.contexts.keyAt(i)).getRemainingGuaranteedTimeMs(anyLong());
+        }
+
+        final ArraySet<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
+        final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
+        final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
+        final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
+        final JobConcurrencyManager.AssignmentInfo assignmentInfo =
+                new JobConcurrencyManager.AssignmentInfo();
+
+        mJobConcurrencyManager.prepareForAssignmentDeterminationLocked(
+                idle, preferredUidOnly, stoppable, assignmentInfo);
+        assertEquals(remainingTimeMs, assignmentInfo.minPreferredUidOnlyWaitingTimeMs);
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+
+        mJobConcurrencyManager
+                .determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable,
+                        assignmentInfo);
+
+        assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size());
+        // Depending on iteration order, we may create 1 or 2 contexts.
+        final long numAssignedJobs = changed.size();
+        assertTrue(numAssignedJobs > 0);
+        assertTrue(numAssignedJobs <= 2);
+        for (int i = 0; i < numAssignedJobs; ++i) {
+            jobs.remove(changed.valueAt(i).newJob);
+        }
+        assertEquals(numAssignedJobs,
+                JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT - jobs.size());
+        JobStatus firstAssignedJob = changed.valueAt(0).newJob;
+        if (!firstAssignedJob.shouldTreatAsExpeditedJob()) {
+            assertEquals(2, numAssignedJobs);
+            assertTrue(changed.valueAt(1).newJob.shouldTreatAsExpeditedJob());
+        } else if (numAssignedJobs == 2) {
+            assertFalse(changed.valueAt(1).newJob.shouldTreatAsExpeditedJob());
+        }
+    }
+
+    @Test
     public void testIsPkgConcurrencyLimited_top() {
         final JobStatus topJob = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0);
         topJob.lastEvaluatedBias = JobInfo.BIAS_TOP_APP;
@@ -595,7 +872,6 @@
     }
 
     private void updateDeviceConfig() throws Exception {
-        DeviceConfig.setProperties(mConfigBuilder.build());
         mJobConcurrencyManager.updateConfigLocked();
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 1753fc7..fc737d0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -41,6 +41,7 @@
 import android.app.IActivityManager;
 import android.app.UiModeManager;
 import android.app.job.JobInfo;
+import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
@@ -235,6 +236,59 @@
                 mService.getMinJobExecutionGuaranteeMs(jobDef));
     }
 
+
+    /**
+     * Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)}
+     * returns a job with the correct delay and deadline constraints.
+     */
+    @Test
+    public void testGetRescheduleJobForFailure() {
+        final long nowElapsed = sElapsedRealtimeClock.millis();
+        final long initialBackoffMs = MINUTE_IN_MILLIS;
+        mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO = 3;
+
+        JobStatus originalJob = createJobStatus("testGetRescheduleJobForFailure",
+                createJobInfo()
+                        .setBackoffCriteria(initialBackoffMs, JobInfo.BACKOFF_POLICY_LINEAR));
+        assertEquals(JobStatus.NO_EARLIEST_RUNTIME, originalJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, originalJob.getLatestRunTimeElapsed());
+
+        // failure = 0, systemStop = 1
+        JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob,
+                JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
+        assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
+
+        // failure = 0, systemStop = 2
+        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
+                JobParameters.INTERNAL_STOP_REASON_PREEMPT);
+        // failure = 0, systemStop = 3
+        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
+                JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED);
+        assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
+
+        // failure = 0, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO
+        for (int i = 0; i < mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO; ++i) {
+            rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
+                    JobParameters.INTERNAL_STOP_REASON_RTC_UPDATED);
+        }
+        assertEquals(nowElapsed + 2 * initialBackoffMs, rescheduledJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
+
+        // failure = 1, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO
+        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
+                JobParameters.INTERNAL_STOP_REASON_TIMEOUT);
+        assertEquals(nowElapsed + 3 * initialBackoffMs, rescheduledJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
+
+        // failure = 2, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO
+        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
+        assertEquals(nowElapsed + 4 * initialBackoffMs, rescheduledJob.getEarliestRunTime());
+        assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed());
+    }
+
     /**
      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
      * with the correct delay and deadline constraints if the periodic job is scheduled with the
@@ -544,14 +598,16 @@
         final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob",
                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
-        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
+        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
 
         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
         advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes
-        failedJob = mService.getRescheduleJobForFailureLocked(job);
+        failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
@@ -559,7 +615,8 @@
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
         advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes
-        failedJob = mService.getRescheduleJobForFailureLocked(job);
+        failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
@@ -569,7 +626,8 @@
         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
 
         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes
-        failedJob = mService.getRescheduleJobForFailureLocked(job);
+        failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes
 
         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
@@ -665,7 +723,8 @@
     public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() {
         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob",
                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
-        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
+        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         long now = sElapsedRealtimeClock.millis();
         long nextWindowStartTime = now + HOUR_IN_MILLIS;
         long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
@@ -701,7 +760,8 @@
         JobStatus job = createJobStatus(
                 "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob",
                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
-        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
+        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         // First window starts 30 minutes from now.
         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
         long now = sElapsedRealtimeClock.millis();
@@ -742,7 +802,8 @@
         JobStatus job = createJobStatus(
                 "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob_longPeriod",
                 createJobInfo().setPeriodic(7 * DAY_IN_MILLIS, 9 * HOUR_IN_MILLIS));
-        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
+        JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job,
+                JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
         // First window starts 6.625 days from now.
         advanceElapsedClock(6 * DAY_IN_MILLIS + 15 * HOUR_IN_MILLIS);
         long now = sElapsedRealtimeClock.millis();
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index 59cb43f..7c435be 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -432,10 +432,10 @@
         assertFalse(topStartedJobs.contains(unrelatedJob));
 
         // Job cleanup
-        mBatteryController.maybeStopTrackingJobLocked(batteryJob, null, false);
-        mBatteryController.maybeStopTrackingJobLocked(chargingJob, null, false);
-        mBatteryController.maybeStopTrackingJobLocked(bothPowerJob, null, false);
-        mBatteryController.maybeStopTrackingJobLocked(unrelatedJob, null, false);
+        mBatteryController.maybeStopTrackingJobLocked(batteryJob, null);
+        mBatteryController.maybeStopTrackingJobLocked(chargingJob, null);
+        mBatteryController.maybeStopTrackingJobLocked(bothPowerJob, null);
+        mBatteryController.maybeStopTrackingJobLocked(unrelatedJob, null);
         assertFalse(trackedJobs.contains(batteryJob));
         assertFalse(trackedJobs.contains(chargingJob));
         assertFalse(trackedJobs.contains(bothPowerJob));
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 674e500..3bee687 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -489,19 +489,22 @@
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(1000L);
         JobStatus js = createJobStatus("time", jb);
         js = new JobStatus(
-                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, FROZEN_TIME, FROZEN_TIME);
+                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 0,
+                FROZEN_TIME, FROZEN_TIME);
 
         assertEquals(mFcConfig.RESCHEDULED_JOB_DEADLINE_MS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
 
         js = new JobStatus(
-                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 3, FROZEN_TIME, FROZEN_TIME);
+                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 1,
+                FROZEN_TIME, FROZEN_TIME);
 
         assertEquals(2 * mFcConfig.RESCHEDULED_JOB_DEADLINE_MS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
 
         js = new JobStatus(
-                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 10, FROZEN_TIME, FROZEN_TIME);
+                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 10,
+                FROZEN_TIME, FROZEN_TIME);
         assertEquals(mFcConfig.MAX_RESCHEDULED_DEADLINE_MS,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0));
     }
@@ -637,7 +640,12 @@
         JobInfo.Builder jb = createJob(0);
         JobStatus js = createJobStatus("time", jb);
         js = new JobStatus(
-                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 1, FROZEN_TIME, FROZEN_TIME);
+                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 1, /* numSystemStops */ 0,
+                FROZEN_TIME, FROZEN_TIME);
+        assertFalse(js.hasFlexibilityConstraint());
+        js = new JobStatus(
+                js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 1,
+                FROZEN_TIME, FROZEN_TIME);
         assertFalse(js.hasFlexibilityConstraint());
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 149ae0b..7f522b0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -248,20 +248,32 @@
 
         // Less than 2 failures, priority shouldn't be affected.
         assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority());
-        int backoffAttempt = 1;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        int numFailures = 1;
+        int numSystemStops = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority());
 
         // 2+ failures, priority should be lowered as much as possible.
-        backoffAttempt = 2;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 2;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
-        backoffAttempt = 5;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 5;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
-        backoffAttempt = 8;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 8;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
+
+        // System stops shouldn't factor in the downgrade.
+        numSystemStops = 10;
+        numFailures = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
+        assertEquals(JobInfo.PRIORITY_MAX, job.getEffectivePriority());
     }
 
     @Test
@@ -274,33 +286,48 @@
 
         // Less than 2 failures, priority shouldn't be affected.
         assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
-        int backoffAttempt = 1;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        int numFailures = 1;
+        int numSystemStops = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
 
         // Failures in [2,4), priority should be lowered slightly.
-        backoffAttempt = 2;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 2;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority());
-        backoffAttempt = 3;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 3;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_DEFAULT, job.getEffectivePriority());
 
         // Failures in [4,6), priority should be lowered more.
-        backoffAttempt = 4;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 4;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
-        backoffAttempt = 5;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 5;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
 
         // 6+ failures, priority should be lowered as much as possible.
-        backoffAttempt = 6;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 6;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
-        backoffAttempt = 12;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 12;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
+
+        // System stops shouldn't factor in the downgrade.
+        numSystemStops = 10;
+        numFailures = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
+        assertEquals(JobInfo.PRIORITY_HIGH, job.getEffectivePriority());
     }
 
     /**
@@ -317,23 +344,36 @@
 
         // Less than 6 failures, priority shouldn't be affected.
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
-        int backoffAttempt = 1;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        int numFailures = 1;
+        int numSystemStops = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
-        backoffAttempt = 4;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 4;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
-        backoffAttempt = 5;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 5;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
 
         // 6+ failures, priority should be lowered as much as possible.
-        backoffAttempt = 6;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 6;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
-        backoffAttempt = 12;
-        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, backoffAttempt, 0, 0);
+        numFailures = 12;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
         assertEquals(JobInfo.PRIORITY_MIN, job.getEffectivePriority());
+
+        // System stops shouldn't factor in the downgrade.
+        numSystemStops = 10;
+        numFailures = 0;
+        job = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, numFailures,
+                numSystemStops, 0, 0);
+        assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority());
     }
 
     /**
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index bb477b1..b949b3b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -47,6 +47,7 @@
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.Process;
 import android.os.SystemClock;
@@ -276,13 +277,13 @@
         inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
                 .setWindow(
                         anyInt(), eq(sElapsedRealtimeClock.millis() + 4 * HOUR_IN_MILLIS),
-                        anyLong(), eq(TAG_PREFETCH), any(), any());
+                        anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class));
 
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 3 * HOUR_IN_MILLIS);
         inOrder.verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
                 .setWindow(
                         anyInt(), eq(sElapsedRealtimeClock.millis() + 8 * HOUR_IN_MILLIS),
-                        anyLong(), eq(TAG_PREFETCH), any(), any());
+                        anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class));
     }
 
     @Test
@@ -414,7 +415,7 @@
         verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
                 .setWindow(
                         anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS),
-                        anyLong(), eq(TAG_PREFETCH), any(), any());
+                        anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class));
         assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
         assertFalse(jobStatus.isReady());
     }
@@ -464,7 +465,7 @@
         verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
                 .setWindow(
                         anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS),
-                        anyLong(), eq(TAG_PREFETCH), any(), any());
+                        anyLong(), eq(TAG_PREFETCH), any(), any(Handler.class));
         assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
         assertFalse(jobStatus.isReady());
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 9407968..17822c6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -1356,7 +1356,7 @@
         synchronized (mQuotaController.mLock) {
             assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
-            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job, null);
         }
 
         setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
@@ -1441,7 +1441,7 @@
         synchronized (mQuotaController.mLock) {
             assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
-            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job, null);
         }
 
         setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
@@ -1474,7 +1474,7 @@
         synchronized (mQuotaController.mLock) {
             assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
                     mQuotaController.getMaxJobExecutionTimeMsLocked(job));
-            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job, null);
         }
 
         setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
@@ -2017,7 +2017,7 @@
             setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
         }
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
 
         advanceElapsedClock(15 * SECOND_IN_MILLIS);
@@ -2032,7 +2032,7 @@
             setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
         }
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
 
         advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS);
@@ -2090,7 +2090,7 @@
             setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid);
         }
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null);
         }
 
         advanceElapsedClock(15 * SECOND_IN_MILLIS);
@@ -2105,9 +2105,9 @@
             setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING, fgChangerUid);
         }
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(fgStateChanger, null);
 
-            mQuotaController.maybeStopTrackingJobLocked(unaffected, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(unaffected, null);
 
             assertTrue(mQuotaController.isWithinQuotaLocked(unaffected));
             assertTrue(unaffected.isReady());
@@ -2226,8 +2226,8 @@
 
         advanceElapsedClock(MINUTE_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job1, null);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
             assertFalse(mQuotaController
                     .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
             assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
@@ -2312,8 +2312,8 @@
 
         advanceElapsedClock(MINUTE_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job1, null);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
             assertFalse(mQuotaController
                     .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
             assertEquals(12, stats.jobCountLimit);
@@ -2390,7 +2390,7 @@
         advanceElapsedClock(MINUTE_IN_MILLIS);
         mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job1, null);
             assertTrue(mQuotaController
                     .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
             assertEquals(4, stats.sessionCountLimit);
@@ -2404,7 +2404,7 @@
 
         advanceElapsedClock(MINUTE_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
             assertFalse(mQuotaController
                     .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
             assertEquals(4, stats.sessionCountLimit);
@@ -2708,7 +2708,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
         // Test with timing sessions out of window but still under max execution limit.
@@ -2725,7 +2725,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                 createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1), false);
@@ -2734,7 +2734,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         synchronized (mQuotaController.mLock) {
             mQuotaController.prepareForExecutionLocked(jobStatus);
@@ -2749,7 +2749,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     @Test
@@ -2771,7 +2772,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2782,7 +2783,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
@@ -2796,7 +2797,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2808,7 +2809,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2818,7 +2819,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
@@ -2826,7 +2828,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     @Test
@@ -2850,7 +2853,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2861,7 +2864,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -2873,7 +2876,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2885,7 +2888,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2895,7 +2898,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
@@ -2903,7 +2907,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     /**
@@ -2932,7 +2937,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2943,7 +2948,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -2955,7 +2960,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2967,7 +2972,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2977,7 +2982,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
@@ -2985,7 +2991,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     @Test
@@ -3013,7 +3020,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -3023,7 +3030,7 @@
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -3039,7 +3046,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -3051,7 +3058,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -3061,7 +3068,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
@@ -3069,7 +3077,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
@@ -3108,7 +3117,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
 
@@ -3123,7 +3133,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         final long expectedFrequentAlarmTime =
                 outOfQuotaTime + (8 * HOUR_IN_MILLIS)
@@ -3135,7 +3145,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         final long expectedRareAlarmTime =
                 outOfQuotaTime + (24 * HOUR_IN_MILLIS)
@@ -3146,7 +3156,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // And back up again.
         setStandbyBucket(FREQUENT_INDEX, jobStatus);
@@ -3156,7 +3167,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(WORKING_INDEX, jobStatus);
         synchronized (mQuotaController.mLock) {
@@ -3165,7 +3176,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(ACTIVE_INDEX, jobStatus);
         synchronized (mQuotaController.mLock) {
@@ -3173,7 +3184,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
     }
@@ -3210,7 +3222,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Valid time in the future, so the count should be used.
         stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2;
@@ -3221,7 +3233,7 @@
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
     }
 
     /**
@@ -3318,7 +3330,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() {
@@ -3355,7 +3368,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     @Test
@@ -3711,7 +3725,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
@@ -3737,7 +3751,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3766,7 +3780,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
@@ -3774,11 +3788,11 @@
         }
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3819,7 +3833,7 @@
         long start = JobSchedulerService.sElapsedRealtimeClock.millis();
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3850,18 +3864,18 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         setDischarging();
         start = JobSchedulerService.sElapsedRealtimeClock.millis();
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3879,7 +3893,7 @@
         setCharging();
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
@@ -3905,7 +3919,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3934,7 +3948,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
@@ -3942,11 +3956,11 @@
         }
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -3973,7 +3987,7 @@
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
@@ -4005,7 +4019,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -4030,11 +4044,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
@@ -4063,7 +4077,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
         start = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -4073,11 +4087,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -4116,11 +4130,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg2, null);
         }
         assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
@@ -4155,11 +4169,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
 
         assertEquals(2, stats.jobCountInRateLimitingWindow);
@@ -4193,7 +4207,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -4218,11 +4232,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobTop, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
@@ -4253,7 +4267,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
@@ -4270,12 +4284,12 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobTop, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
-            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
         assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -4312,7 +4326,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, job1, true);
+            mQuotaController.maybeStopTrackingJobLocked(job1, job1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -4329,7 +4343,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -4348,7 +4362,7 @@
         long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS;
         advanceElapsedClock(elapsedGracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job3, null);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + elapsedGracePeriodMs, 1));
         assertEquals(expected,
@@ -4373,7 +4387,7 @@
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, remainingGracePeriod + 10 * SECOND_IN_MILLIS, 1));
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job4, job4, true);
+            mQuotaController.maybeStopTrackingJobLocked(job4, job4);
         }
         assertEquals(expected,
                 mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -4387,7 +4401,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job5, job5, true);
+            mQuotaController.maybeStopTrackingJobLocked(job5, job5);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -4611,7 +4625,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
         for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
@@ -4624,13 +4638,13 @@
             }
             advanceElapsedClock(SECOND_IN_MILLIS);
             synchronized (mQuotaController.mLock) {
-                mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+                mQuotaController.maybeStopTrackingJobLocked(job, null);
             }
             advanceElapsedClock(SECOND_IN_MILLIS);
         }
         // Start alarm shouldn't have been scheduled since the app was in quota up until this point.
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // The app is now out of job count quota
         JobStatus throttledJob = createJobStatus(
@@ -4649,7 +4663,7 @@
         final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
     }
 
     /**
@@ -4680,7 +4694,7 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
         for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
@@ -4695,13 +4709,13 @@
             }
             advanceElapsedClock(SECOND_IN_MILLIS);
             synchronized (mQuotaController.mLock) {
-                mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+                mQuotaController.maybeStopTrackingJobLocked(job, null);
             }
             advanceElapsedClock(SECOND_IN_MILLIS);
         }
         // Start alarm shouldn't have been scheduled since the app was in quota up until this point.
         verify(mAlarmManager, timeout(1000).times(0)).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // The app is now out of session count quota
         JobStatus throttledJob = createJobStatus(
@@ -4721,7 +4735,7 @@
         final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed;
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
     }
 
     @Test
@@ -5185,7 +5199,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -5196,7 +5211,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long end = now - (22 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
@@ -5208,7 +5224,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -5220,7 +5237,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -5230,7 +5248,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
@@ -5238,7 +5257,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
@@ -5282,7 +5302,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
 
@@ -5297,7 +5318,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(FREQUENT_INDEX);
         final long expectedFrequentAlarmTime =
@@ -5308,7 +5329,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(RARE_INDEX);
         final long expectedRareAlarmTime =
@@ -5319,7 +5340,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // And back up again.
         setStandbyBucket(FREQUENT_INDEX);
@@ -5329,7 +5351,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(WORKING_INDEX);
         synchronized (mQuotaController.mLock) {
@@ -5338,7 +5360,7 @@
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
                 anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                eq(TAG_QUOTA_CHECK), any(), any());
+                eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         setStandbyBucket(ACTIVE_INDEX);
         synchronized (mQuotaController.mLock) {
@@ -5346,7 +5368,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
     }
@@ -5388,7 +5411,8 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
         }
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     /** Tests that TimingSessions aren't saved when the device is charging. */
@@ -5408,7 +5432,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
@@ -5434,7 +5458,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5464,7 +5488,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
@@ -5472,11 +5496,11 @@
         }
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
         assertEquals(expected,
@@ -5521,7 +5545,7 @@
         long start = JobSchedulerService.sElapsedRealtimeClock.millis();
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5553,18 +5577,18 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         setDischarging();
         start = JobSchedulerService.sElapsedRealtimeClock.millis();
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5583,7 +5607,7 @@
         setCharging();
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -5610,7 +5634,7 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5640,7 +5664,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
@@ -5648,11 +5672,11 @@
         }
         advanceElapsedClock(20 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null);
         }
         expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
         assertEquals(expected,
@@ -5680,7 +5704,7 @@
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
         }
         assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
@@ -5715,7 +5739,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5741,11 +5765,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -5775,7 +5799,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS); // UID "inactive" now
         start = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -5785,11 +5809,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg3, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
         assertEquals(expected,
@@ -5825,7 +5849,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5851,11 +5875,11 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobTop, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -5887,7 +5911,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
@@ -5904,12 +5928,12 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobTop, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobTop, null);
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
-            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobBg2, null);
+            mQuotaController.maybeStopTrackingJobLocked(jobFg1, null);
         }
         expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
         assertEquals(expected,
@@ -5946,7 +5970,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, job1, true);
+            mQuotaController.maybeStopTrackingJobLocked(job1, job1);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -5963,7 +5987,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -5981,7 +6005,7 @@
         long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS;
         advanceElapsedClock(elapsedGracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job3, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job3, null);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -6005,7 +6029,7 @@
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job4, job4, true);
+            mQuotaController.maybeStopTrackingJobLocked(job4, job4);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -6019,7 +6043,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job5, job5, true);
+            mQuotaController.maybeStopTrackingJobLocked(job5, job5);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6049,7 +6073,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+            mQuotaController.maybeStopTrackingJobLocked(job, job);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6066,7 +6090,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job, null);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6085,7 +6109,7 @@
         long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS;
         advanceElapsedClock(elapsedGracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job, null);
         }
         expected.add(createTimingSession(start, 12 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6110,7 +6134,7 @@
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + remainingGracePeriod, 1));
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+            mQuotaController.maybeStopTrackingJobLocked(job, job);
         }
         assertEquals(expected,
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
@@ -6124,7 +6148,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+            mQuotaController.maybeStopTrackingJobLocked(job, job);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6169,7 +6193,7 @@
         // Wait for the grace period to expire so the handler can process the message.
         Thread.sleep(gracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job1, job1, true);
+            mQuotaController.maybeStopTrackingJobLocked(job1, job1);
         }
         assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
@@ -6189,7 +6213,7 @@
         // Wait for the grace period to expire so the handler can process the message.
         Thread.sleep(gracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null);
         }
         assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
 
@@ -6215,7 +6239,7 @@
         Thread.sleep(2 * gracePeriodMs);
         advanceElapsedClock(gracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job3, job3, true);
+            mQuotaController.maybeStopTrackingJobLocked(job3, job3);
         }
         expected.add(createTimingSession(start, gracePeriodMs, 1));
         assertEquals(expected,
@@ -6243,7 +6267,7 @@
         Thread.sleep(2 * gracePeriodMs);
         advanceElapsedClock(gracePeriodMs);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job4, job4, true);
+            mQuotaController.maybeStopTrackingJobLocked(job4, job4);
         }
         expected.add(createTimingSession(start, gracePeriodMs, 1));
         assertEquals(expected,
@@ -6270,7 +6294,7 @@
         Thread.sleep(2 * gracePeriodMs);
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(job5, job5, true);
+            mQuotaController.maybeStopTrackingJobLocked(job5, job5);
         }
         expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expected,
@@ -6424,7 +6448,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobReg1, jobReg1, true);
+            mQuotaController.maybeStopTrackingJobLocked(jobReg1, jobReg1);
         }
         expectedRegular.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expectedRegular,
@@ -6440,7 +6464,7 @@
         }
         advanceElapsedClock(10 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobEJ1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobEJ1, null);
         }
         expectedEJ.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         assertEquals(expectedRegular,
@@ -6461,12 +6485,12 @@
         }
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobEJ2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobEJ2, null);
         }
         expectedEJ.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
         advanceElapsedClock(5 * SECOND_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(jobReg2, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(jobReg2, null);
         }
         expectedRegular.add(
                 createTimingSession(start + 5 * SECOND_IN_MILLIS, 10 * SECOND_IN_MILLIS, 1));
@@ -6556,7 +6580,7 @@
         }
         advanceElapsedClock(5000);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(regJob, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(regJob, null);
         }
         assertEquals(0, debit.getTallyLocked());
         assertEquals(10 * MINUTE_IN_MILLIS,
@@ -6570,7 +6594,7 @@
         }
         advanceElapsedClock(5 * MINUTE_IN_MILLIS);
         synchronized (mQuotaController.mLock) {
-            mQuotaController.maybeStopTrackingJobLocked(eJob, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(eJob, null);
         }
         assertEquals(5 * MINUTE_IN_MILLIS, debit.getTallyLocked());
         assertEquals(5 * MINUTE_IN_MILLIS,
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
index 612e906..51d641b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
@@ -81,8 +81,7 @@
         public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
         }
 
-        public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
-                boolean forUpdate) {
+        public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
         }
 
         public void dumpControllerStateLocked(IndentingPrintWriter pw,
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/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
new file mode 100644
index 0000000..aabec22
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -0,0 +1,572 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.apex.ApexInfo;
+import android.apex.ApexSessionInfo;
+import android.apex.ApexSessionParams;
+import android.apex.IApexService;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.pm.parsing.PackageParser2;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Objects;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+
+public class ApexManagerTest {
+
+    @Rule
+    public final MockSystemRule mMockSystem = new MockSystemRule();
+
+    private static final String TEST_APEX_PKG = "com.android.apex.test";
+    private static final String TEST_APEX_FILE_NAME = "apex.test.apex";
+    private static final int TEST_SESSION_ID = 99999999;
+    private static final int[] TEST_CHILD_SESSION_ID = {8888, 7777};
+    private ApexManager mApexManager;
+    private PackageParser2 mPackageParser2;
+
+    private IApexService mApexService = mock(IApexService.class);
+
+    private PackageManagerService mPmService;
+
+    private InstallPackageHelper mInstallPackageHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        ApexManager.ApexManagerImpl managerImpl = spy(new ApexManager.ApexManagerImpl());
+        doReturn(mApexService).when(managerImpl).waitForApexService();
+        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[0]);
+        mApexManager = managerImpl;
+        mPackageParser2 = new PackageParser2(null, null, null, new PackageParser2.Callback() {
+            @Override
+            public boolean isChangeEnabled(long changeId, @NonNull ApplicationInfo appInfo) {
+                return true;
+            }
+
+            @Override
+            public boolean hasFeature(String feature) {
+                return true;
+            }
+        });
+
+        mMockSystem.system().stageNominalSystemState();
+        mPmService = new PackageManagerService(mMockSystem.mocks().getInjector(),
+                false /*factoryTest*/,
+                MockSystem.Companion.getDEFAULT_VERSION_INFO().fingerprint,
+                false /*isEngBuild*/,
+                false /*isUserDebugBuild*/,
+                Build.VERSION_CODES.CUR_DEVELOPMENT,
+                Build.VERSION.INCREMENTAL);
+        mMockSystem.system().validateFinalState();
+        mInstallPackageHelper = new InstallPackageHelper(mPmService, mock(AppDataHelper.class));
+    }
+
+    @NonNull
+    private List<ApexManager.ScanResult> scanApexInfos(ApexInfo[] apexInfos) {
+        return mInstallPackageHelper.scanApexPackages(apexInfos,
+                ParsingPackageUtils.PARSE_IS_SYSTEM_DIR,
+                PackageManagerService.SCAN_AS_SYSTEM, mPackageParser2,
+                ParallelPackageParser.makeExecutorService());
+    }
+
+    @Nullable
+    private ApexManager.ScanResult findActive(@NonNull List<ApexManager.ScanResult> results) {
+        return results.stream()
+                .filter(it -> it.apexInfo.isActive)
+                .filter(it -> Objects.equals(it.packageName, TEST_APEX_PKG))
+                .findFirst()
+                .orElse(null);
+    }
+
+    @Nullable
+    private ApexManager.ScanResult findFactory(@NonNull List<ApexManager.ScanResult> results,
+            @NonNull String packageName) {
+        return results.stream()
+                .filter(it -> it.apexInfo.isFactory)
+                .filter(it -> Objects.equals(it.packageName, packageName))
+                .findFirst()
+                .orElse(null);
+    }
+
+    @NonNull
+    private AndroidPackage mockParsePackage(@NonNull PackageParser2 parser,
+            @NonNull ApexInfo apexInfo) {
+        var flags = PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES;
+        try {
+            var parsedPackage = parser.parsePackage(new File(apexInfo.modulePath), flags,
+                    /* useCaches= */ false);
+            ScanPackageUtils.applyPolicy(parsedPackage,
+                    PackageManagerService.SCAN_AS_APEX | PackageManagerService.SCAN_AS_SYSTEM,
+                    mPmService.getPlatformPackage(), /* isUpdatedSystemApp */ false);
+            // isUpdatedSystemApp is ignoreable above, only used for shared library adjustment
+            return parsedPackage.hideAsFinal();
+        } catch (PackageManagerException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testScanActivePackage() {
+        var apexInfos = createApexInfoForTestPkg(true, false);
+        var results = scanApexInfos(apexInfos);
+        var active = findActive(results);
+        var factory = findFactory(results, TEST_APEX_PKG);
+
+        assertThat(active).isNotNull();
+        assertThat(active.packageName).isEqualTo(TEST_APEX_PKG);
+
+        assertThat(factory).isNull();
+    }
+
+    @Test
+    public void testScanFactoryPackage() {
+        var apexInfos = createApexInfoForTestPkg(false, true);
+        var results = scanApexInfos(apexInfos);
+        var active = findActive(results);
+        var factory = findFactory(results, TEST_APEX_PKG);
+
+        assertThat(factory).isNotNull();
+        assertThat(factory.packageName).contains(TEST_APEX_PKG);
+
+        assertThat(active).isNull();
+    }
+
+    @Test
+    public void testGetApexSystemServices() {
+        ApexInfo[] apexInfo = new ApexInfo[]{
+                createApexInfoForTestPkg(false, true, 1),
+                // only active apex reports apex-system-service
+                createApexInfoForTestPkg(true, false, 2),
+        };
+
+        List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo);
+        mApexManager.notifyScanResult(scanResults);
+
+        List<ApexSystemServiceInfo> services = mApexManager.getApexSystemServices();
+        assertThat(services).hasSize(1);
+        assertThat(services.stream().map(ApexSystemServiceInfo::getName).findFirst().orElse(null))
+                .matches("com.android.apex.test.ApexSystemService");
+    }
+
+    @Test
+    public void testIsApexPackage() {
+        var apexInfos = createApexInfoForTestPkg(false, true);
+        var results = scanApexInfos(apexInfos);
+        var factory = findFactory(results, TEST_APEX_PKG);
+        assertThat(factory.pkg.isApex()).isTrue();
+    }
+
+    @Test
+    public void testIsApexSupported() {
+        assertThat(mApexManager.isApexSupported()).isTrue();
+    }
+
+    @Test
+    public void testGetStagedSessionInfo() throws RemoteException {
+        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
+                getFakeStagedSessionInfo());
+
+        mApexManager.getStagedSessionInfo(TEST_SESSION_ID);
+        verify(mApexService, times(1)).getStagedSessionInfo(TEST_SESSION_ID);
+    }
+
+    @Test
+    public void testGetStagedSessionInfo_unKnownStagedSessionId() throws RemoteException {
+        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
+                getFakeUnknownSessionInfo());
+
+        assertThat(mApexManager.getStagedSessionInfo(TEST_SESSION_ID)).isNull();
+    }
+
+    @Test
+    public void testSubmitStagedSession_throwPackageManagerException() throws RemoteException {
+        doAnswer(invocation -> {
+            throw new Exception();
+        }).when(mApexService).submitStagedSession(any(), any());
+
+        assertThrows(PackageManagerException.class,
+                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
+    }
+
+    @Test
+    public void testSubmitStagedSession_throwRunTimeException() throws RemoteException {
+        doThrow(RemoteException.class).when(mApexService).submitStagedSession(any(), any());
+
+        assertThrows(RuntimeException.class,
+                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
+    }
+
+    @Test
+    public void testGetStagedApexInfos_throwRunTimeException() throws RemoteException {
+        doThrow(RemoteException.class).when(mApexService).getStagedApexInfos(any());
+
+        assertThrows(RuntimeException.class,
+                () -> mApexManager.getStagedApexInfos(testParamsWithChildren()));
+    }
+
+    @Test
+    public void testGetStagedApexInfos_returnsEmptyArrayOnError() throws RemoteException {
+        doThrow(ServiceSpecificException.class).when(mApexService).getStagedApexInfos(any());
+
+        assertThat(mApexManager.getStagedApexInfos(testParamsWithChildren())).hasLength(0);
+    }
+
+    @Test
+    public void testMarkStagedSessionReady_throwPackageManagerException() throws RemoteException {
+        doAnswer(invocation -> {
+            throw new Exception();
+        }).when(mApexService).markStagedSessionReady(anyInt());
+
+        assertThrows(PackageManagerException.class,
+                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void testMarkStagedSessionReady_throwRunTimeException() throws RemoteException {
+        doThrow(RemoteException.class).when(mApexService).markStagedSessionReady(anyInt());
+
+        assertThrows(RuntimeException.class,
+                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void testRevertActiveSessions_remoteException() throws RemoteException {
+        doThrow(RemoteException.class).when(mApexService).revertActiveSessions();
+
+        try {
+            assertThat(mApexManager.revertActiveSessions()).isFalse();
+        } catch (Exception e) {
+            throw new AssertionError("ApexManager should not raise Exception");
+        }
+    }
+
+    @Test
+    public void testMarkStagedSessionSuccessful_throwRemoteException() throws RemoteException {
+        doThrow(RemoteException.class).when(mApexService).markStagedSessionSuccessful(anyInt());
+
+        assertThrows(RuntimeException.class,
+                () -> mApexManager.markStagedSessionSuccessful(TEST_SESSION_ID));
+    }
+
+    @Test
+    public void testUninstallApex_throwException_returnFalse() throws RemoteException {
+        doAnswer(invocation -> {
+            throw new Exception();
+        }).when(mApexService).unstagePackages(any());
+
+        assertThat(mApexManager.uninstallApex(TEST_APEX_PKG)).isFalse();
+    }
+
+    @Test
+    public void testReportErrorWithApkInApex() throws RemoteException {
+        when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true));
+        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
+        assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG);
+
+        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, true);
+        List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo);
+        mApexManager.notifyScanResult(scanResults);
+
+        assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName)).isNull();
+        mApexManager.reportErrorWithApkInApex(activeApex.apexDirectory.getAbsolutePath(),
+                "Some random error");
+        assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName))
+                .isEqualTo("Some random error");
+    }
+
+    /**
+     * registerApkInApex method checks if the prefix of base apk path contains the apex package
+     * name. When an apex package name is a prefix of another apex package name, e.g,
+     * com.android.media and com.android.mediaprovider, then we need to ensure apk inside apex
+     * mediaprovider does not get registered under apex media.
+     */
+    @Test
+    public void testRegisterApkInApexDoesNotRegisterSimilarPrefix() throws RemoteException {
+        when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true));
+        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
+        assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG);
+
+        AndroidPackage fakeApkInApex = mock(AndroidPackage.class);
+        when(fakeApkInApex.getBaseApkPath()).thenReturn("/apex/" + TEST_APEX_PKG + "randomSuffix");
+        when(fakeApkInApex.getPackageName()).thenReturn("randomPackageName");
+
+        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, true);
+        List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo);
+        mApexManager.notifyScanResult(scanResults);
+
+        assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty();
+        mApexManager.registerApkInApex(fakeApkInApex);
+        assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty();
+    }
+
+    @Test
+    public void testInstallPackage_activeOnSystem() throws Exception {
+        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
+                /* isFactory= */ true, extractResource("test.apex_rebootless_v1",
+                        "test.rebootless_apex_v1.apex"));
+        ApexInfo[] apexInfo = new ApexInfo[]{activeApexInfo};
+        var results = scanApexInfos(apexInfo);
+
+        File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
+        ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
+                /* isFactory= */ false, finalApex);
+        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
+
+        File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
+        newApexInfo = mApexManager.installPackage(installedApex);
+
+        var newPkg = mockParsePackage(mPackageParser2, newApexInfo);
+        assertThat(newPkg.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
+        assertThat(newPkg.getLongVersionCode()).isEqualTo(2);
+
+        var factoryPkg = mockParsePackage(mPackageParser2,
+                findFactory(results, "test.apex.rebootless").apexInfo);
+        assertThat(factoryPkg.getBaseApkPath()).isEqualTo(activeApexInfo.modulePath);
+        assertThat(factoryPkg.getLongVersionCode()).isEqualTo(1);
+        assertThat(factoryPkg.isSystem()).isTrue();
+    }
+
+    @Test
+    public void testInstallPackage_activeOnData() throws Exception {
+        ApexInfo factoryApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ false,
+                /* isFactory= */ true, extractResource("test.apex_rebootless_v1",
+                        "test.rebootless_apex_v1.apex"));
+        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
+                /* isFactory= */ false, extractResource("test.apex.rebootless@1",
+                        "test.rebootless_apex_v1.apex"));
+        ApexInfo[] apexInfo = new ApexInfo[]{factoryApexInfo, activeApexInfo};
+        var results = scanApexInfos(apexInfo);
+
+        File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
+        ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
+                /* isFactory= */ false, finalApex);
+        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
+
+        File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
+        newApexInfo = mApexManager.installPackage(installedApex);
+
+        var newPkg = mockParsePackage(mPackageParser2, newApexInfo);
+        assertThat(newPkg.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
+        assertThat(newPkg.getLongVersionCode()).isEqualTo(2);
+
+        var factoryPkg = mockParsePackage(mPackageParser2,
+                findFactory(results, "test.apex.rebootless").apexInfo);
+        assertThat(factoryPkg.getBaseApkPath()).isEqualTo(factoryApexInfo.modulePath);
+        assertThat(factoryPkg.getLongVersionCode()).isEqualTo(1);
+        assertThat(factoryPkg.isSystem()).isTrue();
+    }
+
+    @Test
+    public void testInstallPackageBinderCallFails() throws Exception {
+        when(mApexService.installAndActivatePackage(anyString())).thenThrow(
+                new RuntimeException("install failed :("));
+
+        File installedApex = extractResource("test.apex_rebootless_v1",
+                "test.rebootless_apex_v1.apex");
+        assertThrows(PackageManagerException.class,
+                () -> mApexManager.installPackage(installedApex));
+    }
+
+    @Test
+    public void testGetActivePackageNameForApexModuleName() {
+        final String moduleName = "com.android.module_name";
+
+        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, false);
+        apexInfo[0].moduleName = moduleName;
+        List<ApexManager.ScanResult> scanResults = scanApexInfos(apexInfo);
+        mApexManager.notifyScanResult(scanResults);
+
+        assertThat(mApexManager.getActivePackageNameForApexModuleName(moduleName))
+                .isEqualTo(TEST_APEX_PKG);
+    }
+
+    @Test
+    public void testGetBackingApexFiles() throws Exception {
+        final ApexInfo apex = createApexInfoForTestPkg(true, true, 37);
+        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex});
+
+        final File backingApexFile = mApexManager.getBackingApexFile(
+                new File(mMockSystem.system().getApexDirectory(),
+                        TEST_APEX_PKG + "/apk/App/App.apk"));
+        assertThat(backingApexFile.getAbsolutePath()).isEqualTo(apex.modulePath);
+    }
+
+    @Test
+    public void testGetBackingApexFile_fileNotOnApexMountPoint_returnsNull() {
+        File result = mApexManager.getBackingApexFile(
+                new File("/data/local/tmp/whatever/does-not-matter"));
+        assertThat(result).isNull();
+    }
+
+    @Test
+    public void testGetBackingApexFiles_unknownApex_returnsNull() throws Exception {
+        final ApexInfo apex = createApexInfoForTestPkg(true, true, 37);
+        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex});
+
+        final File backingApexFile = mApexManager.getBackingApexFile(
+                new File(mMockSystem.system().getApexDirectory(), "com.wrong.apex/apk/App"));
+        assertThat(backingApexFile).isNull();
+    }
+
+    @Test
+    public void testGetBackingApexFiles_topLevelApexDir_returnsNull() {
+        assertThat(mApexManager.getBackingApexFile(Environment.getApexDirectory())).isNull();
+        assertThat(mApexManager.getBackingApexFile(new File("/apex/"))).isNull();
+        assertThat(mApexManager.getBackingApexFile(new File("/apex//"))).isNull();
+    }
+
+    @Test
+    public void testGetBackingApexFiles_flattenedApex() {
+        ApexManager flattenedApexManager = new ApexManager.ApexManagerFlattenedApex();
+        final File backingApexFile = flattenedApexManager.getBackingApexFile(
+                new File(mMockSystem.system().getApexDirectory(),
+                        "com.android.apex.cts.shim/app/CtsShim/CtsShim.apk"));
+        assertThat(backingApexFile).isNull();
+    }
+
+    @Test
+    public void testActiveApexChanged() throws RemoteException {
+        ApexInfo apex1 = createApexInfo(
+                "com.apex1", 37, true, true, new File("/data/apex/active/com.apex@37.apex"));
+        apex1.activeApexChanged = true;
+        apex1.preinstalledModulePath = apex1.modulePath;
+        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex1});
+        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
+        assertThat(activeApex.apexModuleName).isEqualTo("com.apex1");
+        assertThat(activeApex.activeApexChanged).isTrue();
+    }
+
+    private ApexInfo createApexInfoForTestPkg(boolean isActive, boolean isFactory, int version) {
+        File apexFile = extractResource(TEST_APEX_PKG, TEST_APEX_FILE_NAME);
+        ApexInfo apexInfo = new ApexInfo();
+        apexInfo.isActive = isActive;
+        apexInfo.isFactory = isFactory;
+        apexInfo.moduleName = TEST_APEX_PKG;
+        apexInfo.modulePath = apexFile.getPath();
+        apexInfo.versionCode = version;
+        apexInfo.preinstalledModulePath = apexFile.getPath();
+        return apexInfo;
+    }
+
+    private ApexInfo[] createApexInfoForTestPkg(boolean isActive, boolean isFactory) {
+        return new ApexInfo[]{createApexInfoForTestPkg(isActive, isFactory, 191000070)};
+    }
+
+    private ApexInfo createApexInfo(String moduleName, int versionCode, boolean isActive,
+            boolean isFactory, File apexFile) {
+        ApexInfo apexInfo = new ApexInfo();
+        apexInfo.moduleName = moduleName;
+        apexInfo.versionCode = versionCode;
+        apexInfo.isActive = isActive;
+        apexInfo.isFactory = isFactory;
+        apexInfo.modulePath = apexFile.getPath();
+        return apexInfo;
+    }
+
+    private ApexSessionInfo getFakeStagedSessionInfo() {
+        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
+        stagedSessionInfo.sessionId = TEST_SESSION_ID;
+        stagedSessionInfo.isStaged = true;
+
+        return stagedSessionInfo;
+    }
+
+    private ApexSessionInfo getFakeUnknownSessionInfo() {
+        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
+        stagedSessionInfo.sessionId = TEST_SESSION_ID;
+        stagedSessionInfo.isUnknown = true;
+
+        return stagedSessionInfo;
+    }
+
+    private static ApexSessionParams testParamsWithChildren() {
+        ApexSessionParams params = new ApexSessionParams();
+        params.sessionId = TEST_SESSION_ID;
+        params.childSessionIds = TEST_CHILD_SESSION_ID;
+        return params;
+    }
+
+    // Extracts the binary data from a resource and writes it to a temp file
+    private static File extractResource(String baseName, String fullResourceName) {
+        File file;
+        try {
+            file = File.createTempFile(baseName, ".apex");
+        } catch (IOException e) {
+            throw new AssertionError("CreateTempFile IOException" + e);
+        }
+
+        try (
+                InputStream in = ApexManager.class.getClassLoader()
+                        .getResourceAsStream(fullResourceName);
+                OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
+            if (in == null) {
+                throw new IllegalArgumentException("Resource not found: " + fullResourceName);
+            }
+            byte[] buf = new byte[65536];
+            int chunkSize;
+            while ((chunkSize = in.read(buf)) != -1) {
+                out.write(buf, 0, chunkSize);
+            }
+            return file;
+        } catch (IOException e) {
+            throw new AssertionError("Exception while converting stream to file" + e);
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
index 47f449c..1be7e2e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
@@ -223,7 +223,7 @@
                 /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
-                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
     }
 
@@ -241,7 +241,7 @@
         assertThat(getFailedPackageNamesSecondary()).isEmpty();
 
         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
-                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
                 /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
 
         assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
@@ -256,7 +256,7 @@
         mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
 
         runFullJob(mJobServiceForIdle, mJobParametersForIdle,
-                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
                 /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null);
 
         assertThat(getFailedPackageNamesPrimary()).isEmpty();
@@ -393,7 +393,7 @@
         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
 
         // Always reschedule for periodic job
-        verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true);
+        verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false);
         verifyLastControlDexOptBlockingCall(false);
     }
 
@@ -421,7 +421,7 @@
         mCancelThread.join(TEST_WAIT_TIMEOUT_MS);
 
         // Always reschedule for periodic job
-        verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true);
+        verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false);
         verify(mDexOptHelper, never()).controlDexOptBlocking(true);
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
index 7ccd6d9..e0662c4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
@@ -51,6 +51,7 @@
 
         mUserManagerInternal = rule.mocks().injector.userManagerInternal
         whenever(mUserManagerInternal.getUserIds()).thenReturn(intArrayOf(0, 1))
+        whenever(mUserManagerInternal.getUserTypesForStatsd(any())).thenReturn(intArrayOf(1, 1))
 
         mPms = createPackageManagerService()
         doAnswer { false }.`when`(mPms).isPackageDeviceAdmin(any(), any())
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/InitAppsHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/InitAppsHelperTest.kt
index 2165301..15b4975 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/InitAppsHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/InitAppsHelperTest.kt
@@ -83,7 +83,7 @@
         val pms = createPackageManagerService()
         assertThat(pms.isFirstBoot).isEqualTo(true)
         assertThat(pms.isDeviceUpgrading).isEqualTo(false)
-        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null, null,
+        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null,
             listOf<ScanPartition>())
         assertThat(
             initAppsHelper.systemScanFlags and PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE)
@@ -98,7 +98,7 @@
         val pms = createPackageManagerService()
         assertThat(pms.isFirstBoot).isEqualTo(false)
         assertThat(pms.isDeviceUpgrading).isEqualTo(true)
-        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null, null,
+        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null,
             listOf<ScanPartition>())
         assertThat(
             initAppsHelper.systemScanFlags and PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE)
@@ -112,7 +112,7 @@
         val pms = createPackageManagerService()
         assertThat(pms.isFirstBoot).isEqualTo(false)
         assertThat(pms.isDeviceUpgrading).isEqualTo(false)
-        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null, null,
+        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null,
             listOf<ScanPartition>())
         assertThat(
             initAppsHelper.systemScanFlags and PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE)
@@ -126,7 +126,7 @@
         val pms = createPackageManagerService()
         assertThat(pms.isFirstBoot).isEqualTo(false)
         assertThat(pms.isDeviceUpgrading).isEqualTo(true)
-        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null, null,
+        val initAppsHelper = InitAppsHelper(pms, rule.mocks().apexManager, null,
             listOf<ScanPartition>())
         assertThat(
             initAppsHelper.systemScanFlags and PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE)
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index dc7bcd6..dd6c733 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -78,14 +78,6 @@
 import com.android.server.testutils.nullable
 import com.android.server.testutils.whenever
 import com.android.server.utils.WatchedArrayMap
-import java.io.File
-import java.io.IOException
-import java.nio.file.Files
-import java.security.PublicKey
-import java.security.cert.CertificateException
-import java.util.Arrays
-import java.util.Random
-import java.util.concurrent.FutureTask
 import libcore.util.HexEncoding
 import org.junit.Assert
 import org.junit.rules.TestRule
@@ -94,6 +86,14 @@
 import org.mockito.AdditionalMatchers.or
 import org.mockito.Mockito
 import org.mockito.quality.Strictness
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.security.PublicKey
+import java.security.cert.CertificateException
+import java.util.Arrays
+import java.util.Random
+import java.util.concurrent.FutureTask
 
 /**
  * A utility for mocking behavior of the system and dependencies when testing PackageManagerService
@@ -104,6 +104,9 @@
 class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) {
     private val random = Random()
     val mocks = Mocks()
+
+    // TODO: getBackingApexFile does not handle paths that aren't /apex
+    val apexDirectory = File("/apex")
     val packageCacheDirectory: File =
             Files.createTempDirectory("packageCache").toFile()
     val rootDirectory: File =
@@ -297,7 +300,9 @@
         whenever(mocks.systemConfig.sharedLibraries).thenReturn(DEFAULT_SHARED_LIBRARIES_LIST)
         whenever(mocks.systemConfig.defaultVrComponents).thenReturn(ArraySet())
         whenever(mocks.systemConfig.hiddenApiWhitelistedApps).thenReturn(ArraySet())
+        wheneverStatic { SystemProperties.set(anyString(), anyString()) }.thenDoNothing()
         wheneverStatic { SystemProperties.getBoolean("fw.free_cache_v2", true) }.thenReturn(true)
+        wheneverStatic { Environment.getApexDirectory() }.thenReturn(apexDirectory)
         wheneverStatic { Environment.getPackageCacheDirectory() }.thenReturn(packageCacheDirectory)
         wheneverStatic { SystemProperties.digestOf("ro.build.fingerprint") }.thenReturn("cacheName")
         wheneverStatic { Environment.getRootDirectory() }.thenReturn(rootDirectory)
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
index e28d331..28c78b2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
@@ -22,6 +22,7 @@
 import android.content.pm.VersionedPackage
 import android.os.Build
 import android.os.storage.StorageManager
+import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.PackageUtils
 import com.android.server.SystemConfig.SharedLibraryEntry
@@ -222,10 +223,11 @@
         val parsedPackage = pair.second as ParsedPackage
         val scanRequest = ScanRequest(parsedPackage, null, null, null, null,
             null, null, null, 0, 0, false, null, null)
-        val scanResult = ScanResult(scanRequest, true, null, null, false, 0, null, null, null)
+        val scanResult = ScanResult(scanRequest, null, null, false, 0, null, null, null)
+        var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult)
 
         val latestInfoSetting =
-            mSharedLibrariesImpl.getStaticSharedLibLatestVersionSetting(scanResult)!!
+            mSharedLibrariesImpl.getStaticSharedLibLatestVersionSetting(installRequest)!!
 
         assertThat(latestInfoSetting).isNotNull()
         assertThat(latestInfoSetting.packageName).isEqualTo(STATIC_LIB_PACKAGE_NAME)
@@ -305,10 +307,11 @@
     @Test
     fun getAllowedSharedLibInfos_withStaticSharedLibInfo() {
         val testInfo = libOfStatic(TEST_LIB_PACKAGE_NAME, TEST_LIB_NAME, 1L)
-        val scanResult = ScanResult(mock(), true, null, null,
+        val scanResult = ScanResult(mock(), null, null,
             false, 0, null, testInfo, null)
+        var installRequest = InstallRequest(mock(), 0, 0, UserHandle(0), scanResult)
 
-        val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(scanResult)
+        val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest)
 
         assertThat(allowedInfos).hasSize(1)
         assertThat(allowedInfos[0].name).isEqualTo(TEST_LIB_NAME)
@@ -327,10 +330,11 @@
             .setPkgFlags(ApplicationInfo.FLAG_SYSTEM).build()
         val scanRequest = ScanRequest(parsedPackage, null, null, null, null,
             null, null, null, 0, 0, false, null, null)
-        val scanResult = ScanResult(scanRequest, true, packageSetting, null,
+        val scanResult = ScanResult(scanRequest, packageSetting, null,
             false, 0, null, null, listOf(testInfo))
+        var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult)
 
-        val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(scanResult)
+        val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest)
 
         assertThat(allowedInfos).hasSize(1)
         assertThat(allowedInfos[0].name).isEqualTo(TEST_LIB_NAME)
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..9be370f
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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, USER_SYSTEM, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_invalidDisplay() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, INVALID_DISPLAY));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID));
+
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_startedProfileOfCurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_displayAvailable() {
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_displayAlreadyAssigned() {
+        mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID);
+
+        IllegalStateException e = assertThrows(IllegalStateException.class, () -> mMediator
+                .assignUserToDisplay(OTHER_USER_ID, 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, USER_ID, SECONDARY_DISPLAY_ID);
+
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, 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() {
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() {
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() {
+        mMediator.assignUserToDisplay(PARENT_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, DEFAULT_DISPLAY));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    // TODO(b/244644281): when start & assign are merged, rename tests above and also call
+    // stopUserAndAssertState() at the end of them
+
+    @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() {
+        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() {
+        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() {
+        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);
+    }
+
+    // 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..7abdd9e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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 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);
+    }
+
+    // TODO(b/244644281): when start & assign are merged, rename tests below and also call
+    // stopUserAndAssertState() at the end of them
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, USER_ID, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_startProfileOfcurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+
+        assertThrows(UnsupportedOperationException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_stoppedProfileOfcurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertThrows(UnsupportedOperationException.class, () -> mMediator
+                .assignUserToDisplay(PROFILE_USER_ID, PARENT_USER_ID, SECONDARY_DISPLAY_ID));
+    }
+}
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..e8be97d
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -0,0 +1,533 @@
+/*
+ * 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.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
+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.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString;
+import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.UserIdInt;
+import android.util.Log;
+
+import com.android.server.ExtendedMockitoTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 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 ExtendedMockitoTestCase {
+
+    private static final String TAG = UserVisibilityMediatorTestCase.class.getSimpleName();
+
+    /**
+     * Id for a simple user (that doesn't have profiles).
+     */
+    protected static final int USER_ID = 600;
+
+    /**
+     * Id for another simple user.
+     */
+    protected static final int OTHER_USER_ID = 666;
+
+    /**
+     * Id for a user that has one profile (whose id is {@link #PROFILE_USER_ID}.
+     *
+     * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service.
+     */
+    protected static final int PARENT_USER_ID = 642;
+
+    /**
+     * Id for a profile whose parent is {@link #PARENTUSER_ID}.
+     *
+     * <p>You can use {@link #addDefaultProfileAndParent()} to add both of this user to the service.
+     */
+    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 static final boolean FG = true;
+    private static final boolean BG = false;
+
+    private final boolean mUsersOnSecondaryDisplaysEnabled;
+
+    protected UserVisibilityMediator mMediator;
+
+    protected UserVisibilityMediatorTestCase(boolean usersOnSecondaryDisplaysEnabled) {
+        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
+    }
+
+    @Before
+    public final void setMediator() {
+        mMediator = new UserVisibilityMediator(mUsersOnSecondaryDisplaysEnabled);
+        mDumpableDumperRule.addDumpable(mMediator);
+    }
+
+    @Test
+    public final void testStartUser_currentUser() {
+        int result = mMediator.startOnly(USER_ID, USER_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        assertCurrentUser(USER_ID);
+        assertIsCurrentUserOrRunningProfileOfCurrentUser(USER_ID);
+        assertStartedProfileGroupIdOf(USER_ID, USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_currentUserSecondaryDisplay() {
+        int result = mMediator.startOnly(USER_ID, USER_ID, FG, SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(USER_ID);
+        assertStartedProfileGroupIdOf(USER_ID, NO_PROFILE_GROUP_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_parentStarted() {
+        mockCurrentUser(PARENT_USER_ID);
+
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        assertCurrentUser(PARENT_USER_ID);
+        assertIsCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+        assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID);
+        assertProfileIsStarted(PROFILE_USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_parentNotStarted() {
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+        assertStartedProfileGroupIdOf(PROFILE_USER_ID, PARENT_USER_ID);
+        assertProfileIsStarted(PROFILE_USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileBg_secondaryDisplay() {
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileFg() {
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+        assertIsNotCurrentUserOrRunningProfileOfCurrentUser(PROFILE_USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testStartUser_profileFgSecondaryDisplay() {
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, FG, SECONDARY_DISPLAY_ID);
+
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+        assertCurrentUser(INITIAL_CURRENT_USER_ID);
+
+        stopUserAndAssertState(USER_ID);
+    }
+
+    @Test
+    public final void testGetStartedProfileGroupId_whenStartedWithNoProfileGroupId() {
+        int result = mMediator.startOnly(USER_ID, NO_PROFILE_GROUP_ID, FG, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        assertWithMessage("shit").that(mMediator.getStartedProfileGroupId(USER_ID))
+                .isEqualTo(USER_ID);
+    }
+
+    @Test
+    public final void testAssignUserToDisplay_defaultDisplayIgnored() {
+        mMediator.assignUserToDisplay(USER_ID, 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() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisible_stoppedProfileOfcurrentUser() {
+        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() {
+        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() {
+        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() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() {
+        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() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        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() {
+        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() {
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID))
+                .isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public final void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() {
+        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);
+    }
+
+    /**
+     * Stops the given user and assert the proper state is set.
+     *
+     * <p>This method should be called at the end of tests that starts a user, so it can test
+     * {@code stopUser()} as well (technically speaking, {@code stopUser()} should be tested on its
+     * own methods, but it depends on the user being started at first place, so pragmatically
+     * speaking, it's better to "reuse" such tests for both (start and stop)
+     */
+    private void stopUserAndAssertState(@UserIdInt int userId) {
+        mMediator.stopUser(userId);
+
+        assertUserIsStopped(userId);
+        assertNoUserAssignedToDisplay();
+    }
+
+    // TODO(b/244644281): remove if start & assign are merged; if they aren't, add a note explaining
+    // it's not meant to be used to test startUser() itself.
+    protected void mockCurrentUser(@UserIdInt int userId) {
+        Log.d(TAG, "mockCurrentUser(" + userId + ")");
+        int result = mMediator.startOnly(userId, userId, FG, DEFAULT_DISPLAY);
+        if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) {
+            throw new IllegalStateException("Failed to mock current user " + userId
+                    + ": mediator returned " + userAssignmentResultToString(result));
+        }
+    }
+
+    // TODO(b/244644281): remove when start & assign are merged; or add a note explaining
+    // it's not meant to be used to test startUser() itself.
+    protected void startDefaultProfile() {
+        mockCurrentUser(PARENT_USER_ID);
+        Log.d(TAG, "starting default profile (" + PROFILE_USER_ID + ") in background after starting"
+                + " its parent (" + PARENT_USER_ID + ") on foreground");
+
+        int result = mMediator.startOnly(PROFILE_USER_ID, PARENT_USER_ID, BG, DEFAULT_DISPLAY);
+        if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) {
+            throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID
+                    + ": mediator returned " + userAssignmentResultToString(result));
+        }
+    }
+
+    // TODO(b/244644281): remove when start & assign are merged; or add a note explaining
+    // it's not meant to be used to test stopUser() itself.
+    protected void stopDefaultProfile() {
+        Log.d(TAG, "stopping default profile");
+        mMediator.stopUser(PROFILE_USER_ID);
+    }
+
+    // TODO(b/244644281): remove when start & assign are merged; or add a note explaining
+    // it's not meant to be used to test assignUserToDisplay() itself.
+    protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) {
+        Log.d(TAG, "assignUserToDisplay(" + userId + ", " + displayId + ")");
+        int result = mMediator.startOnly(userId, userId, BG, displayId);
+        if (result != USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE) {
+            throw new IllegalStateException("Failed to startuser " + userId
+                    + " on background: mediator returned " + userAssignmentResultToString(result));
+        }
+        mMediator.assignUserToDisplay(userId, userId, displayId);
+
+    }
+
+    // TODO(b/244644281): remove when start & assign are merged; or rename to
+    // assertNoUserAssignedToSecondaryDisplays
+    protected final void assertNoUserAssignedToDisplay() {
+        assertWithMessage("users on secondary displays")
+                .that(mMediator.getUsersOnSecondaryDisplays())
+                .isEmpty();
+    }
+
+    // TODO(b/244644281): remove when start & assign are merged; or rename to
+    // assertUserAssignedToSecondaryDisplay
+    protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
+        assertWithMessage("users on secondary displays")
+                .that(mMediator.getUsersOnSecondaryDisplays())
+                .containsExactly(userId, displayId);
+    }
+
+    private void assertCurrentUser(@UserIdInt int userId) {
+        assertWithMessage("mediator.getCurrentUserId()").that(mMediator.getCurrentUserId())
+                .isEqualTo(userId);
+        if (userId != INITIAL_CURRENT_USER_ID) {
+            assertUserIsStarted(userId);
+        }
+    }
+
+    private void assertUserIsStarted(@UserIdInt int userId) {
+        assertWithMessage("mediator.isStarted(%s)", userId).that(mMediator.isStartedUser(userId))
+                .isTrue();
+    }
+
+    private void assertUserIsStopped(@UserIdInt int userId) {
+        assertWithMessage("mediator.isStarted(%s)", userId).that(mMediator.isStartedUser(userId))
+                .isFalse();
+    }
+
+    private void assertProfileIsStarted(@UserIdInt int userId) {
+        assertWithMessage("mediator.isStartedProfile(%s)", userId)
+                .that(mMediator.isStartedProfile(userId))
+                .isTrue();
+        assertUserIsStarted(userId);
+    }
+
+    private void assertStartedProfileGroupIdOf(@UserIdInt int userId,
+            @UserIdInt int profileGroupId) {
+        assertWithMessage("mediator.getStartedProfileGroupId(%s)", userId)
+                .that(mMediator.getStartedProfileGroupId(userId))
+                .isEqualTo(profileGroupId);
+    }
+
+    private void assertIsCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
+        assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId)
+                .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId))
+                .isTrue();
+        if (mMediator.getCurrentUserId() == userId) {
+            assertUserIsStarted(userId);
+        } else {
+            assertProfileIsStarted(userId);
+        }
+    }
+
+    private void assertIsNotCurrentUserOrRunningProfileOfCurrentUser(int userId) {
+        assertWithMessage("mediator.isCurrentUserOrRunningProfileOfCurrentUser(%s)", userId)
+                .that(mMediator.isCurrentUserOrRunningProfileOfCurrentUser(userId))
+                .isFalse();
+    }
+
+    private void assertStartUserResult(int actualResult, int expectedResult) {
+        assertWithMessage("startUser() result (where %s=%s and %s=%s)",
+                actualResult, userAssignmentResultToString(actualResult),
+                expectedResult, userAssignmentResultToString(expectedResult))
+                        .that(actualResult).isEqualTo(expectedResult);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
index 2fac31e..d477cb6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java
@@ -73,7 +73,12 @@
         }
 
         @Override
-        long getHardSatiatedConsumptionLimit() {
+        long getMinSatiatedConsumptionLimit() {
+            return 0;
+        }
+
+        @Override
+        long getMaxSatiatedConsumptionLimit() {
             return 0;
         }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java
index fb3e8f2..84a61c7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AlarmManagerEconomicPolicyTest.java
@@ -132,8 +132,10 @@
     public void testDefaults() {
         assertEquals(EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
                 mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES,
-                mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -150,13 +152,15 @@
     @Test
     public void testConstantsUpdating_ValidValues() {
         setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(5));
-        setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(25));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(3));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(25));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(10));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(9));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(7));
 
         assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(3), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(25), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -171,13 +175,15 @@
     public void testConstantsUpdating_InvalidValues() {
         // Test negatives.
         setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(-5));
-        setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(-5));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(-5));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(-5));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(-1));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(-2));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(-3));
 
         assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(1), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -188,14 +194,16 @@
         assertEquals(arcToCake(0), mEconomicPolicy.getMinSatiatedBalance(0, "com.any.other.app"));
 
         // Test min+max reversed.
-        setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(5));
-        setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(3));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(5));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(4));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(3));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(10));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(11));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(13));
 
         assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(5), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
         assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app"));
         assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted));
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java
index 6da4ab7..cad608f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/CompleteEconomicPolicyTest.java
@@ -155,9 +155,12 @@
         assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES
                 + EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
                 mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES
-                + EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES,
-                mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES
+                + EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES
+                + EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -178,8 +181,10 @@
     public void testConstantsUpdated() {
         setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(4));
         setDeviceConfigCakes(EconomyManager.KEY_AM_INITIAL_CONSUMPTION_LIMIT, arcToCake(6));
-        setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(24));
-        setDeviceConfigCakes(EconomyManager.KEY_AM_HARD_CONSUMPTION_LIMIT, arcToCake(26));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(2));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_CONSUMPTION_LIMIT, arcToCake(3));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(24));
+        setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_CONSUMPTION_LIMIT, arcToCake(26));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(9));
         setDeviceConfigCakes(EconomyManager.KEY_AM_MAX_SATIATED_BALANCE, arcToCake(11));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(8));
@@ -188,7 +193,8 @@
         setDeviceConfigCakes(EconomyManager.KEY_AM_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(2));
 
         assertEquals(arcToCake(10), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(50), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(50), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -206,8 +212,10 @@
         setDeviceConfigBoolean(EconomyManager.KEY_ENABLE_POLICY_JOB_SCHEDULER, false);
         assertEquals(EconomyManager.DEFAULT_AM_INITIAL_CONSUMPTION_LIMIT_CAKES,
                 mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(EconomyManager.DEFAULT_AM_HARD_CONSUMPTION_LIMIT_CAKES,
-                mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_AM_MIN_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_AM_MAX_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -229,8 +237,10 @@
         setDeviceConfigBoolean(EconomyManager.KEY_ENABLE_POLICY_JOB_SCHEDULER, true);
         assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES,
                 mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES,
-                mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(0, mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
         assertEquals(EconomyManager.DEFAULT_JS_MAX_SATIATED_BALANCE_CAKES,
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java
index b7bbcd75..ebf760c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/JobSchedulerEconomicPolicyTest.java
@@ -132,8 +132,10 @@
     public void testDefaults() {
         assertEquals(EconomyManager.DEFAULT_JS_INITIAL_CONSUMPTION_LIMIT_CAKES,
                 mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(EconomyManager.DEFAULT_JS_HARD_CONSUMPTION_LIMIT_CAKES,
-                mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MIN_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(EconomyManager.DEFAULT_JS_MAX_CONSUMPTION_LIMIT_CAKES,
+                mEconomicPolicy.getMaxSatiatedConsumptionLimit());
 
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
@@ -171,7 +173,8 @@
     @Test
     public void testConstantsUpdating_ValidValues() {
         setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5));
-        setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(25));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(2));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(25));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(10));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(6));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(4));
@@ -179,7 +182,8 @@
                 arcToCake(1));
 
         assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(25), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(2), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(25), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -198,7 +202,8 @@
     public void testConstantsUpdating_InvalidValues() {
         // Test negatives.
         setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(-5));
-        setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(-5));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(-5));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(-5));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(-1));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(-2));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(-3));
@@ -206,7 +211,8 @@
                 arcToCake(-4));
 
         assertEquals(arcToCake(1), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(1), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(1), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(1), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         final String pkgRestricted = "com.pkg.restricted";
         when(mIrs.isPackageRestricted(anyInt(), eq(pkgRestricted))).thenReturn(true);
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
@@ -221,14 +227,16 @@
                 mEconomicPolicy.getMinSatiatedBalance(0, pkgUpdater));
 
         // Test min+max reversed.
-        setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(5));
-        setDeviceConfigCakes(EconomyManager.KEY_JS_HARD_CONSUMPTION_LIMIT, arcToCake(3));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_CONSUMPTION_LIMIT, arcToCake(5));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_INITIAL_CONSUMPTION_LIMIT, arcToCake(4));
+        setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_CONSUMPTION_LIMIT, arcToCake(3));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MAX_SATIATED_BALANCE, arcToCake(10));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_EXEMPTED, arcToCake(11));
         setDeviceConfigCakes(EconomyManager.KEY_JS_MIN_SATIATED_BALANCE_OTHER_APP, arcToCake(13));
 
         assertEquals(arcToCake(5), mEconomicPolicy.getInitialSatiatedConsumptionLimit());
-        assertEquals(arcToCake(5), mEconomicPolicy.getHardSatiatedConsumptionLimit());
+        assertEquals(arcToCake(5), mEconomicPolicy.getMinSatiatedConsumptionLimit());
+        assertEquals(arcToCake(5), mEconomicPolicy.getMaxSatiatedConsumptionLimit());
         assertEquals(arcToCake(0), mEconomicPolicy.getMaxSatiatedBalance(0, pkgRestricted));
         assertEquals(arcToCake(13), mEconomicPolicy.getMaxSatiatedBalance(0, "com.any.other.app"));
         assertEquals(arcToCake(13), mEconomicPolicy.getMinSatiatedBalance(0, pkgExempted));
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
index 00d7541..a3a49d70 100644
--- a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
@@ -35,6 +35,7 @@
 
 import android.app.AlarmManager;
 import android.content.Context;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
 import android.util.ArraySet;
@@ -222,7 +223,8 @@
 
         alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS);
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any());
+                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any(
+                        Handler.class));
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
index 608b64e..0d14c9f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
@@ -589,14 +589,14 @@
         // No sessions saved yet.
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, never()).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions out of window.
         final long now = mInjector.getElapsedRealtime();
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, never()).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -604,25 +604,27 @@
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, never()).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Add some more sessions, but still in quota.
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1);
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, never()).setWindow(
-                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // Test when out of quota.
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, timeout(1000).times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         verify(mAlarmManager, times(1)).setWindow(
-                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                any(Handler.class));
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's category is changed. */
@@ -656,7 +658,8 @@
         mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, never())
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
         inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
 
         // And down from there.
@@ -665,41 +668,42 @@
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                        eq(TAG_QUOTA_CHECK), any(), any());
+                        eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS);
         mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                        eq(TAG_QUOTA_CHECK), any(), any());
+                        eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS);
         mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(),
-                        eq(TAG_QUOTA_CHECK), any(), any());
+                        eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         // And back up again.
         mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
-                        eq(TAG_QUOTA_CHECK), any(), any());
+                        eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
-                        eq(TAG_QUOTA_CHECK), any(), any());
+                        eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
 
         mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
+                        any(Handler.class));
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 5fb3a4e..7fd1ddb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -69,8 +69,6 @@
 import android.testing.TestableContext;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -80,6 +78,8 @@
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.internal.R;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.wallpaper.WallpaperManagerService.WallpaperData;
 import com.android.server.wm.WindowManagerInternal;
diff --git a/services/tests/mockingservicestests/utils-mockito/com/android/server/extendedtestutils/ExtendedMockitoUtils.kt b/services/tests/mockingservicestests/utils-mockito/com/android/server/extendedtestutils/ExtendedMockitoUtils.kt
index 72ae77e..a0a67439 100644
--- a/services/tests/mockingservicestests/utils-mockito/com/android/server/extendedtestutils/ExtendedMockitoUtils.kt
+++ b/services/tests/mockingservicestests/utils-mockito/com/android/server/extendedtestutils/ExtendedMockitoUtils.kt
@@ -33,9 +33,14 @@
     override fun thenReturn(value: T) {
         ExtendedMockito.doReturn(value).wheneverStatic(mockedMethod)
     }
+
+    override fun thenDoNothing() {
+        ExtendedMockito.doNothing().wheneverStatic(mockedMethod)
+    }
 }
 
 interface CustomStaticStubber<T> {
     fun thenAnswer(answer: Answer<T>)
     fun thenReturn(value: T)
+    fun thenDoNothing()
 }
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index a09d994..9386a23 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -123,9 +123,7 @@
         ":PackageParserTestApp3",
         ":PackageParserTestApp4",
         ":PackageParserTestApp5",
-        ":apex.test",
-        ":test.rebootless_apex_v1",
-        ":test.rebootless_apex_v2",
+        ":PackageParserTestApp6",
         ":com.android.apex.cts.shim.v1_prebuilt",
         ":com.android.apex.cts.shim.v2_different_certificate_prebuilt",
         ":com.android.apex.cts.shim.v2_unsigned_apk_container_prebuilt",
@@ -206,10 +204,10 @@
         ":FrameworksServicesTests_install_uses_sdk_q0",
         ":FrameworksServicesTests_install_uses_sdk_q0_r0",
         ":FrameworksServicesTests_install_uses_sdk_r0",
-        ":FrameworksServicesTests_install_uses_sdk_r5",
+        ":FrameworksServicesTests_install_uses_sdk_r1000",
         ":FrameworksServicesTests_install_uses_sdk_r_none",
         ":FrameworksServicesTests_install_uses_sdk_r0_s0",
-        ":FrameworksServicesTests_install_uses_sdk_r0_s5",
+        ":FrameworksServicesTests_install_uses_sdk_r0_s1000",
         ":FrameworksServicesTests_keyset_permdef_sa_unone",
         ":FrameworksServicesTests_keyset_permuse_sa_ua_ub",
         ":FrameworksServicesTests_keyset_permuse_sb_ua_ub",
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 6551bde..6349b21 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -107,6 +107,9 @@
 
     <queries>
         <package android:name="com.android.servicestests.apps.suspendtestapp" />
+        <intent>
+            <action android:name="android.media.browse.MediaBrowserService" />
+        </intent>
     </queries>
 
     <!-- Uses API introduced in O (26) -->
diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml
index 9052f58..9c7ce83 100644
--- a/services/tests/servicestests/AndroidTest.xml
+++ b/services/tests/servicestests/AndroidTest.xml
@@ -33,6 +33,7 @@
         <option name="test-file-name" value="SimpleServiceTestApp1.apk" />
         <option name="test-file-name" value="SimpleServiceTestApp2.apk" />
         <option name="test-file-name" value="SimpleServiceTestApp3.apk" />
+        <option name="test-file-name" value="FakeMediaApp.apk" />
     </target_preparer>
 
     <!-- Create place to store apks -->
diff --git a/services/tests/servicestests/apks/install_uses_sdk/Android.bp b/services/tests/servicestests/apks/install_uses_sdk/Android.bp
index a51293d..2894395 100644
--- a/services/tests/servicestests/apks/install_uses_sdk/Android.bp
+++ b/services/tests/servicestests/apks/install_uses_sdk/Android.bp
@@ -32,9 +32,9 @@
 }
 
 android_test_helper_app {
-    name: "FrameworksServicesTests_install_uses_sdk_r5",
+    name: "FrameworksServicesTests_install_uses_sdk_r1000",
     defaults: ["FrameworksServicesTests_apks_defaults"],
-    manifest: "AndroidManifest-r5.xml",
+    manifest: "AndroidManifest-r1000.xml",
 }
 
 android_test_helper_app {
@@ -44,9 +44,9 @@
 }
 
 android_test_helper_app {
-    name: "FrameworksServicesTests_install_uses_sdk_r0_s5",
+    name: "FrameworksServicesTests_install_uses_sdk_r0_s1000",
     defaults: ["FrameworksServicesTests_apks_defaults"],
-    manifest: "AndroidManifest-r0-s5.xml",
+    manifest: "AndroidManifest-r0-s1000.xml",
 }
 
 android_test_helper_app {
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
new file mode 100644
index 0000000..25743b8
--- /dev/null
+++ b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s1000.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.servicestests.install_uses_sdk">
+
+    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
+        <!-- This fails because 31 is not version 5 -->
+        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="0" />
+        <extension-sdk android:sdkVersion="31" android:minExtensionVersion="1000" />
+    </uses-sdk>
+
+    <application>
+    </application>
+</manifest>
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml
deleted file mode 100644
index bafe4c4..0000000
--- a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r0-s5.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 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.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.frameworks.servicestests.install_uses_sdk">
-
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
-        <!-- This fails because 31 is not version 5 -->
-        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="0" />
-        <extension-sdk android:sdkVersion="31" android:minExtensionVersion="5" />
-    </uses-sdk>
-
-    <application>
-    </application>
-</manifest>
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml
new file mode 100644
index 0000000..9bf9254
--- /dev/null
+++ b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r1000.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.servicestests.install_uses_sdk">
+
+    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
+        <!-- This will fail to install, because minExtensionVersion is not met -->
+        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="1000" />
+    </uses-sdk>
+
+    <application>
+    </application>
+</manifest>
diff --git a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml b/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml
deleted file mode 100644
index 7723d05..0000000
--- a/services/tests/servicestests/apks/install_uses_sdk/AndroidManifest-r5.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 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.
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.frameworks.servicestests.install_uses_sdk">
-
-    <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="29">
-        <!-- This will fail to install, because minExtensionVersion is not met -->
-        <extension-sdk android:sdkVersion="30" android:minExtensionVersion="5" />
-    </uses-sdk>
-
-    <application>
-    </application>
-</manifest>
diff --git a/services/tests/servicestests/res/xml/usertypes_test_full.xml b/services/tests/servicestests/res/xml/usertypes_test_full.xml
index 099ccbe..9568143 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_full.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_full.xml
@@ -16,7 +16,8 @@
 <user-types>
     <full-type
         name='android.test.1'
-        max-allowed-per-parent='12' >
+        max-allowed-per-parent='12'
+        max-allowed='17' >
         <default-restrictions no_remove_user='true' no_bluetooth='true' />
         <badge-colors>
             <item res='@*android:color/profile_badge_1' />
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index b27f49d..1a6dae37 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -33,6 +33,7 @@
         <user-properties
             showInLauncher='2020'
             startWithParent='false'
+            useParentsContacts='false'
         />
     </profile-type>
     <profile-type name='custom.test.1' max-allowed-per-parent='14' />
diff --git a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
index 42c4129..653ed1a 100644
--- a/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/BinaryTransparencyServiceTest.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.SystemProperties;
@@ -36,8 +37,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.FileDescriptor;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
 public class BinaryTransparencyServiceTest {
@@ -96,7 +96,7 @@
     @Test
     public void getApexInfo_postInitialize_returnsValidEntries() throws RemoteException {
         prepApexInfo();
-        Map result = mTestInterface.getApexInfo();
+        List result = mTestInterface.getApexInfo();
         Assert.assertNotNull("Apex info map should not be null", result);
         Assert.assertFalse("Apex info map should not be empty", result.isEmpty());
     }
@@ -105,13 +105,18 @@
     public void getApexInfo_postInitialize_returnsActualApexs()
             throws RemoteException, PackageManager.NameNotFoundException {
         prepApexInfo();
-        Map result = mTestInterface.getApexInfo();
+        List resultList = mTestInterface.getApexInfo();
 
         PackageManager pm = mContext.getPackageManager();
         Assert.assertNotNull(pm);
-        HashMap<PackageInfo, String> castedResult = (HashMap<PackageInfo, String>) result;
-        for (PackageInfo packageInfo : castedResult.keySet()) {
-            Assert.assertTrue(packageInfo.packageName + "is not an APEX!", packageInfo.isApex);
+        List<Bundle> castedResult = (List<Bundle>) resultList;
+        for (Bundle resultBundle : castedResult) {
+            PackageInfo resultPackageInfo = resultBundle.getParcelable(
+                    BinaryTransparencyService.BUNDLE_PACKAGE_INFO, PackageInfo.class);
+            Assert.assertNotNull("PackageInfo for APEX should not be null",
+                    resultPackageInfo);
+            Assert.assertTrue(resultPackageInfo.packageName + "is not an APEX!",
+                    resultPackageInfo.isApex);
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index c15f6a9..bbcf77b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -326,7 +326,7 @@
 
         void register(Context context) {
             if (!mRegistered) {
-                context.registerReceiver(this, mFilter);
+                context.registerReceiver(this, mFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
                 mRegistered = true;
             }
         }
diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
index f28ad79..e54a48b 100644
--- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
@@ -3470,7 +3470,8 @@
 
         @Override
         public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
-            return mMockContext.registerReceiver(receiver, filter);
+            return mMockContext.registerReceiver(receiver, filter,
+                    Context.RECEIVER_EXPORTED_UNAUDITED);
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
index 0b84a60..e6ab73a 100644
--- a/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/AnrHelperTest.java
@@ -49,6 +49,7 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -62,6 +63,7 @@
     private AnrHelper mAnrHelper;
 
     private ProcessRecord mAnrApp;
+    private ExecutorService mExecutorService;
 
     @Rule
     public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
@@ -88,7 +90,9 @@
                         return mServiceThreadRule.getThread().getThreadHandler();
                     }
                 }, mServiceThreadRule.getThread());
-            mAnrHelper = new AnrHelper(service);
+            mExecutorService = mock(ExecutorService.class);
+
+            mAnrHelper = new AnrHelper(service, mExecutorService);
         });
     }
 
@@ -119,7 +123,7 @@
 
         verify(mAnrApp.mErrorState, timeout(TIMEOUT_MS)).appNotResponding(
                 eq(activityShortComponentName), eq(appInfo), eq(parentShortComponentName),
-                eq(parentProcess), eq(aboveSystem), eq(timeoutRecord),
+                eq(parentProcess), eq(aboveSystem), eq(timeoutRecord), eq(mExecutorService),
                 eq(false) /* onlyDumpSelf */);
     }
 
@@ -133,7 +137,7 @@
             processingLatch.await();
             return null;
         }).when(mAnrApp.mErrorState).appNotResponding(anyString(), any(), any(), any(),
-                anyBoolean(), any(), anyBoolean());
+                anyBoolean(), any(), any(), anyBoolean());
         final ApplicationInfo appInfo = new ApplicationInfo();
         final TimeoutRecord timeoutRecord = TimeoutRecord.forInputDispatchWindowUnresponsive(
                 "annotation");
@@ -155,6 +159,7 @@
         processingLatch.countDown();
         // There is only one ANR reported.
         verify(mAnrApp.mErrorState, timeout(TIMEOUT_MS).only()).appNotResponding(
-                anyString(), any(), any(), any(), anyBoolean(), any(), anyBoolean());
+                anyString(), any(), any(), any(), anyBoolean(), any(), eq(mExecutorService),
+                anyBoolean());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/am/ProcessRecordTests.java b/services/tests/servicestests/src/com/android/server/am/ProcessRecordTests.java
index 70519e4..9cada91 100644
--- a/services/tests/servicestests/src/com/android/server/am/ProcessRecordTests.java
+++ b/services/tests/servicestests/src/com/android/server/am/ProcessRecordTests.java
@@ -45,6 +45,7 @@
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.concurrent.ExecutorService;
 
 /**
  * Build/Install/Run:
@@ -58,6 +59,7 @@
 
     private ProcessRecord mProcessRecord;
     private ProcessErrorStateRecord mProcessErrorState;
+    private ExecutorService mExecutorService;
 
     @BeforeClass
     public static void setUpOnce() throws Exception {
@@ -109,6 +111,7 @@
         runWithDexmakerShareClassLoader(() -> {
             mProcessRecord = new ProcessRecord(sService, sContext.getApplicationInfo(),
                     "name", 12345);
+            mExecutorService = mock(ExecutorService.class);
             mProcessErrorState = spy(mProcessRecord.mErrorState);
             doNothing().when(mProcessErrorState).startAppProblemLSP();
             doReturn(false).when(mProcessErrorState).isSilentAnr();
@@ -194,11 +197,11 @@
         assertTrue(mProcessRecord.isKilled());
     }
 
-    private static void appNotResponding(ProcessErrorStateRecord processErrorState,
+    private void appNotResponding(ProcessErrorStateRecord processErrorState,
             String annotation) {
         TimeoutRecord timeoutRecord = TimeoutRecord.forInputDispatchNoFocusedWindow(annotation);
         processErrorState.appNotResponding(null /* activityShortComponentName */, null /* aInfo */,
                 null /* parentShortComponentName */, null /* parentProcess */,
-                false /* aboveSystem */, timeoutRecord, false /* onlyDumpSelf */);
+                false /* aboveSystem */, timeoutRecord, mExecutorService, false /* onlyDumpSelf */);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 0b776a3..a49214f 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -38,6 +38,9 @@
 import static com.android.server.am.UserController.USER_CURRENT_MSG;
 import static com.android.server.am.UserController.USER_START_MSG;
 import static com.android.server.am.UserController.USER_SWITCH_TIMEOUT_MSG;
+import static com.android.server.am.UserController.USER_VISIBILITY_CHANGED_MSG;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
 
 import static com.google.android.collect.Lists.newArrayList;
 import static com.google.android.collect.Sets.newHashSet;
@@ -99,6 +102,7 @@
 import com.android.server.SystemService;
 import com.android.server.am.UserState.KeyEvictedCallback;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.UserManagerInternal.UserAssignmentResult;
 import com.android.server.pm.UserManagerService;
 import com.android.server.wm.WindowManagerService;
 
@@ -158,12 +162,18 @@
             REPORT_USER_SWITCH_MSG,
             USER_SWITCH_TIMEOUT_MSG,
             USER_START_MSG,
+            USER_VISIBILITY_CHANGED_MSG,
             USER_CURRENT_MSG);
 
-    private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
+    private static final Set<Integer> START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
             USER_START_MSG,
             REPORT_LOCKED_BOOT_COMPLETE_MSG);
 
+    private static final Set<Integer> START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
+            USER_START_MSG,
+            USER_VISIBILITY_CHANGED_MSG,
+            REPORT_LOCKED_BOOT_COMPLETE_MSG);
+
     @Before
     public void setUp() throws Exception {
         runWithDexmakerShareClassLoader(() -> {
@@ -182,6 +192,12 @@
             mockIsUsersOnSecondaryDisplaysEnabled(false);
             // All UserController params are set to default.
 
+            // Starts with a generic assumption that the user starts visible, but on tests where
+            // that's not the case, the test should call mockAssignUserToMainDisplay()
+            doReturn(UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE)
+                    .when(mInjector.mUserManagerInternalMock)
+                    .assignUserToDisplayOnStart(anyInt(), anyInt(), anyBoolean(), anyInt());
+
             mUserController = new UserController(mInjector);
             mUserController.setAllowUserUnlocking(true);
             setUpUser(TEST_USER_ID, NO_USERINFO_FLAGS);
@@ -209,16 +225,29 @@
 
     @Test
     public void testStartUser_background() {
+        mockAssignUserToMainDisplay(TEST_USER_ID, /* foreground= */ false,
+                USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
         boolean started = mUserController.startUser(TEST_USER_ID, /* foreground= */ false);
         assertWithMessage("startUser(%s, foreground=false)", TEST_USER_ID).that(started).isTrue();
         verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         verify(mInjector, never()).clearAllLockedTasks(anyString());
-        startBackgroundUserAssertions();
+        startBackgroundUserAssertions(/*visible= */ false);
         verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY);
     }
 
     @Test
+    public void testStartUser_displayAssignmentFailed() {
+        doReturn(UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE)
+                .when(mInjector.mUserManagerInternalMock)
+                .assignUserToDisplayOnStart(eq(TEST_USER_ID), anyInt(), eq(true), anyInt());
+
+        boolean started = mUserController.startUser(TEST_USER_ID, /* foreground= */ true);
+
+        assertWithMessage("startUser(%s, foreground=true)", TEST_USER_ID).that(started).isFalse();
+    }
+
+    @Test
     public void testStartUserOnSecondaryDisplay_defaultDisplay() {
         assertThrows(IllegalArgumentException.class, () -> mUserController
                 .startUserOnSecondaryDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY));
@@ -238,7 +267,7 @@
         verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         verify(mInjector, never()).clearAllLockedTasks(anyString());
-        startBackgroundUserAssertions();
+        startBackgroundUserAssertions(/*visible= */ true);
     }
 
     @Test
@@ -246,7 +275,7 @@
         mUserController.setInitialConfig(/* userSwitchUiEnabled= */ false,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
 
-        mUserController.startUser(TEST_USER_ID, true /* foreground */);
+        mUserController.startUser(TEST_USER_ID, /* foreground= */ true);
         verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
         verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
@@ -258,10 +287,14 @@
         assertFalse(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ true));
         // Make sure no intents have been fired for pre-created users.
         assertTrue(mInjector.mSentIntents.isEmpty());
+
+        verifyUserNeverAssignedToDisplay();
     }
 
     @Test
     public void testStartPreCreatedUser_background() throws Exception {
+        mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false,
+                USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
         assertTrue(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ false));
         // Make sure no intents have been fired for pre-created users.
         assertTrue(mInjector.mSentIntents.isEmpty());
@@ -289,8 +322,10 @@
         assertEquals("Unexpected message sent", expectedMessageCodes, actualCodes);
     }
 
-    private void startBackgroundUserAssertions() {
-        startUserAssertions(START_BACKGROUND_USER_ACTIONS, START_BACKGROUND_USER_MESSAGE_CODES);
+    private void startBackgroundUserAssertions(boolean visible) {
+        startUserAssertions(START_BACKGROUND_USER_ACTIONS,
+                visible ? START_VISIBLE_BACKGROUND_USER_MESSAGE_CODES
+                        : START_INVISIBLE_BACKGROUND_USER_MESSAGE_CODES);
     }
 
     private void startForegroundUserAssertions() {
@@ -303,6 +338,7 @@
         assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state);
         assertEquals("Unexpected old user id", 0, reportMsg.arg1);
         assertEquals("Unexpected new user id", TEST_USER_ID, reportMsg.arg2);
+        verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY);
     }
 
     @Test
@@ -313,6 +349,8 @@
         mUserController.startUserInForeground(NONEXIST_USER_ID);
         verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
         verify(mInjector.getWindowManager()).setSwitchingUser(false);
+
+        verifyUserNeverAssignedToDisplay();
     }
 
     @Test
@@ -395,6 +433,7 @@
         verify(mInjector, times(0)).dismissKeyguard(any(), anyString());
         verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
         continueUserSwitchAssertions(TEST_USER_ID, false);
+        verifySystemUserVisibilityChangedNotified(/* visible= */ false);
     }
 
     @Test
@@ -403,7 +442,7 @@
         mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
         // Start user -- this will update state of mUserController
-        mUserController.startUser(TEST_USER_ID, true);
+        mUserController.startUser(TEST_USER_ID, /* foreground=*/ true);
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -415,6 +454,7 @@
         verify(mInjector, times(1)).dismissKeyguard(any(), anyString());
         verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
         continueUserSwitchAssertions(TEST_USER_ID, false);
+        verifySystemUserVisibilityChangedNotified(/* visible= */ false);
     }
 
     @Test
@@ -423,7 +463,7 @@
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
 
         // Start user -- this will update state of mUserController
-        mUserController.startUser(TEST_USER_ID, true);
+        mUserController.startUser(TEST_USER_ID, /* foreground=*/ true);
         Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
         assertNotNull(reportMsg);
         UserState userState = (UserState) reportMsg.obj;
@@ -521,6 +561,7 @@
         assertFalse(mUserController.canStartMoreUsers());
         assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}),
                 mUserController.getRunningUsersLU());
+        verifySystemUserVisibilityChangedNotified(/* visible= */ false);
     }
 
     /**
@@ -530,7 +571,7 @@
      */
     @Test
     public void testUserLockingFromUserSwitchingForMultipleUsersDelayedLockingMode()
-            throws InterruptedException, RemoteException {
+            throws Exception {
         mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true);
 
@@ -645,6 +686,8 @@
         setUpUser(TEST_USER_ID1, 0);
         assertThrows(IllegalArgumentException.class,
                 () -> mUserController.startProfile(TEST_USER_ID1));
+
+        verifyUserNeverAssignedToDisplay();
     }
 
     @Test
@@ -660,23 +703,30 @@
         setUpUser(TEST_USER_ID1, UserInfo.FLAG_PROFILE | UserInfo.FLAG_DISABLED, /* preCreated= */
                 false, UserManager.USER_TYPE_PROFILE_MANAGED);
         assertThat(mUserController.startProfile(TEST_USER_ID1)).isFalse();
+
+        verifyUserNeverAssignedToDisplay();
     }
 
     @Test
     public void testStartProfile() throws Exception {
+        mockAssignUserToMainDisplay(TEST_PRE_CREATED_USER_ID, /* foreground= */ false,
+                USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
         setUpAndStartProfileInBackground(TEST_USER_ID1);
 
-        startBackgroundUserAssertions();
+        startBackgroundUserAssertions(/*visible= */ true);
         verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY);
     }
 
     @Test
     public void testStartProfile_whenUsersOnSecondaryDisplaysIsEnabled() throws Exception {
+        mockAssignUserToMainDisplay(TEST_USER_ID1, /* foreground= */ false,
+                USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
         mockIsUsersOnSecondaryDisplaysEnabled(true);
 
         setUpAndStartProfileInBackground(TEST_USER_ID1);
 
-        startBackgroundUserAssertions();
+        startBackgroundUserAssertions(/*visible= */ true);
         verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY);
     }
 
@@ -933,20 +983,33 @@
         when(mInjector.isUsersOnSecondaryDisplaysEnabled()).thenReturn(value);
     }
 
+    private void mockAssignUserToMainDisplay(@UserIdInt int userId, boolean foreground,
+            @UserAssignmentResult int result) {
+        when(mInjector.mUserManagerInternalMock.assignUserToDisplayOnStart(eq(userId),
+                /* profileGroupId= */ anyInt(), eq(foreground), eq(Display.DEFAULT_DISPLAY)))
+                        .thenReturn(result);
+    }
+
     private void verifyUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
-        verify(mInjector.getUserManagerInternal()).assignUserToDisplay(userId, displayId);
+        verify(mInjector.getUserManagerInternal()).assignUserToDisplayOnStart(eq(userId), anyInt(),
+                anyBoolean(), eq(displayId));
     }
 
     private void verifyUserNeverAssignedToDisplay() {
-        verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplay(anyInt(), anyInt());
+        verify(mInjector.getUserManagerInternal(), never()).assignUserToDisplayOnStart(anyInt(),
+                anyInt(), anyBoolean(), anyInt());
     }
 
     private void verifyUserUnassignedFromDisplay(@UserIdInt int userId) {
-        verify(mInjector.getUserManagerInternal()).unassignUserFromDisplay(userId);
+        verify(mInjector.getUserManagerInternal()).unassignUserFromDisplayOnStop(userId);
     }
 
     private void verifyUserUnassignedFromDisplayNeverCalled(@UserIdInt int userId) {
-        verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplay(userId);
+        verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplayOnStop(userId);
+    }
+
+    private void verifySystemUserVisibilityChangedNotified(boolean visible) {
+        verify(mInjector).onUserVisibilityChanged(UserHandle.USER_SYSTEM, visible);
     }
 
     // Should be public to allow mocking
@@ -1084,6 +1147,16 @@
         protected LockPatternUtils getLockPatternUtils() {
             return mLockPatternUtilsMock;
         }
+
+        @Override
+        void onUserStarting(@UserIdInt int userId) {
+            Log.i(TAG, "onUserStarting(" + userId + ")");
+        }
+
+        @Override
+        void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+            Log.i(TAG, "onUserVisibilityChanged(" + userId + ", " + visible + ")");
+        }
     }
 
     private static class TestHandler extends Handler {
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
index b33e22f..9acc4bd 100644
--- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
@@ -49,13 +49,13 @@
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.AtomicFile;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.widget.RemoteViews;
 
 import com.android.frameworks.servicestests.R;
 import com.android.internal.appwidget.IAppWidgetHost;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.mockito.ArgumentCaptor;
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
index 581a2a7..2d7d46f 100644
--- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.fail;
 
 import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -254,6 +255,9 @@
             ITransportStatusCallback c) throws RemoteException {}
         @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {}
         @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {}
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture)
+                throws RemoteException {}
         @Override public IBinder asBinder() {
             return null;
         }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 85d8aba..0d6d1a2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -64,6 +64,7 @@
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
+import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.fingerprint.FingerprintManager;
 import android.os.Binder;
 import android.os.IBinder;
@@ -75,7 +76,10 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.internal.statusbar.ISessionListener;
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.server.biometrics.log.BiometricContextProvider;
+import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.LockoutTracker;
 
 import org.junit.Before;
@@ -129,6 +133,16 @@
     ITrustManager mTrustManager;
     @Mock
     DevicePolicyManager mDevicePolicyManager;
+    @Mock
+    private IStatusBarService mStatusBarService;
+    @Mock
+    private ISessionListener mSessionListener;
+    @Mock
+    private AmbientDisplayConfiguration mAmbientDisplayConfiguration;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
+
+    BiometricContextProvider mBiometricContextProvider;
 
     @Before
     public void setUp() {
@@ -160,6 +174,11 @@
         when(mResources.getString(R.string.biometric_error_user_canceled))
                 .thenReturn(ERROR_USER_CANCELED);
 
+        when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true);
+        mBiometricContextProvider = new BiometricContextProvider(mAmbientDisplayConfiguration,
+                mStatusBarService, null /* handler */, mAuthSessionCoordinator);
+        when(mInjector.getBiometricContext(any())).thenReturn(mBiometricContextProvider);
+
         final String[] config = {
                 "0:2:15",  // ID0:Fingerprint:Strong
                 "1:8:15",  // ID1:Face:Strong
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index 68c9ce4..bb00634 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -125,12 +125,9 @@
         mProbe.destroy();
         mProbe.enable();
 
-        AtomicInteger lux = new AtomicInteger(10);
-        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
-
         verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
         verifyNoMoreInteractions(mSensorManager);
-        assertThat(lux.get()).isLessThan(0);
+        assertThat(mProbe.getMostRecentLux()).isLessThan(0);
     }
 
     @Test
@@ -178,6 +175,23 @@
     }
 
     @Test
+    public void testWatchDogCompletesAwait() {
+        mProbe.enable();
+
+        AtomicInteger lux = new AtomicInteger(-9);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        moveTimeBy(TIMEOUT_MS);
+
+        assertThat(lux.get()).isEqualTo(-1);
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
     public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
         testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
     }
@@ -306,15 +320,27 @@
     }
 
     @Test
-    public void testNoNextLuxWhenDestroyed() {
+    public void testDestroyAllowsAwaitLuxExactlyOnce() {
+        final float lastValue = 5.5f;
         mProbe.destroy();
 
-        AtomicInteger lux = new AtomicInteger(-20);
+        AtomicInteger lux = new AtomicInteger(10);
         mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
 
-        assertThat(lux.get()).isEqualTo(-1);
-        verify(mSensorManager, never()).registerListener(
+        verify(mSensorManager).registerListener(
                 mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{lastValue}));
+
+        assertThat(lux.get()).isEqualTo(Math.round(lastValue));
+        verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
+
+        lux.set(22);
+        mProbe.enable();
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+        mProbe.enable();
+
+        assertThat(lux.get()).isEqualTo(Math.round(lastValue));
         verifyNoMoreInteractions(mSensorManager);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
index c5a8557..ebf7fd8 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java
@@ -61,11 +61,11 @@
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
                 AUTHENTICATOR_DEFAULT);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED);
+                AUTHENTICATOR_DEFAULT);
     }
 
     @Test
-    public void testLockout() {
+    public void testConvenientLockout() {
         mAuthResultCoordinator.lockedOutFor(
                 BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
 
@@ -80,7 +80,7 @@
     }
 
     @Test
-    public void testConvenientLockout() {
+    public void testConvenientUnlock() {
         mAuthResultCoordinator.authenticatedFor(
                 BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
 
@@ -91,11 +91,26 @@
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
                 AUTHENTICATOR_DEFAULT);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED);
+                AUTHENTICATOR_DEFAULT);
     }
 
     @Test
     public void testWeakLockout() {
+        mAuthResultCoordinator.lockedOutFor(
+                BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE);
+
+        Map<Integer, Integer> authMap = mAuthResultCoordinator.getResult();
+
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_STRONG)).isEqualTo(
+                AUTHENTICATOR_DEFAULT);
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
+                AUTHENTICATOR_DEFAULT);
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
+                AUTHENTICATOR_LOCKED);
+    }
+
+    @Test
+    public void testWeakUnlock() {
         mAuthResultCoordinator.authenticatedFor(
                 BiometricManager.Authenticators.BIOMETRIC_WEAK);
 
@@ -104,13 +119,29 @@
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_STRONG)).isEqualTo(
                 AUTHENTICATOR_DEFAULT);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED);
+                AUTHENTICATOR_DEFAULT);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED);
+                AUTHENTICATOR_DEFAULT);
     }
 
     @Test
     public void testStrongLockout() {
+        mAuthResultCoordinator.lockedOutFor(
+                BiometricManager.Authenticators.BIOMETRIC_STRONG);
+
+        Map<Integer, Integer> authMap = mAuthResultCoordinator.getResult();
+
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_STRONG)).isEqualTo(
+                AUTHENTICATOR_LOCKED);
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
+                AUTHENTICATOR_LOCKED);
+        assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
+                AUTHENTICATOR_LOCKED);
+    }
+
+
+    @Test
+    public void testStrongUnlock() {
         mAuthResultCoordinator.authenticatedFor(
                 BiometricManager.Authenticators.BIOMETRIC_STRONG);
 
@@ -136,9 +167,9 @@
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_STRONG)).isEqualTo(
                 AUTHENTICATOR_DEFAULT);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_WEAK)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED | AUTHENTICATOR_LOCKED);
+                AUTHENTICATOR_LOCKED);
         assertThat(authMap.get(BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE)).isEqualTo(
-                AUTHENTICATOR_UNLOCKED | AUTHENTICATOR_LOCKED);
+                AUTHENTICATOR_LOCKED);
 
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
index 6e44875..c3b9cb1 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
@@ -53,22 +53,28 @@
     }
 
     @Test
-    public void testUserUnlocked() {
+    public void testUserUnlockedWithWeak() {
         mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
         mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_STRONG, 1 /* sensorId */,
                 0 /* requestId */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
 
         mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
-        mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1 /* sensorId */,
-                0 /* requestId */);
+        mCoordinator.authEndedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1 /* sensorId */,
+                0 /* requestId */, true /* wasSuccessful */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
@@ -77,38 +83,79 @@
         mCoordinator.authStartedFor(PRIMARY_USER, 2 /* sensorId */, 0 /* requestId */);
         mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_STRONG, 1 /* sensorId */,
                 0 /* requestId */);
-        mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 2 /* sensorId */,
-                0 /* requestId */);
+        mCoordinator.authEndedFor(PRIMARY_USER, BIOMETRIC_WEAK, 2 /* sensorId */,
+                0 /* requestId */, false /* wasSuccessful */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
 
         mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
-        mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1 /* sensorId */,
-                0 /* requestId */);
+        mCoordinator.authEndedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1 /* sensorId */,
+                0 /* requestId */, false /* wasSuccessful */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+    }
+
+    @Test
+    public void testWeakAndConvenientCannotResetLockout() {
+        mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
+        mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_STRONG, 1 /* sensorId */,
+                0 /* requestId */);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+
+        mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_WEAK, 0 /* requestId */);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+
+        mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE, 0 /* requestId */);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
     public void testUserCanAuthDuringLockoutOfSameSession() {
         mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG, 0 /* requestId */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
         mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
         mCoordinator.authStartedFor(PRIMARY_USER, 2 /* sensorId */, 0 /* requestId */);
         mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2 /* sensorId */,
                 0 /* requestId */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     @Test
@@ -123,25 +170,39 @@
 
         mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG, 0 /* requestId */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(
+                mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
 
         mCoordinator.authStartedFor(PRIMARY_USER, 1 /* sensorId */, 0 /* requestId */);
         mCoordinator.authStartedFor(PRIMARY_USER, 2 /* sensorId */, 0 /* requestId */);
         mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2 /* sensorId */,
                 0 /* requestId */);
 
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse();
-        assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse();
+        assertThat(
+                mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mCoordinator.getLockoutStateFor(SECONDARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
index 8e6d90c..3a9c0f0 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java
@@ -107,7 +107,7 @@
         assertThat(mClientMonitor.getRequestId()).isEqualTo(id);
     }
 
-    private class TestClientMonitor extends BaseClientMonitor implements Interruptable {
+    private class TestClientMonitor extends BaseClientMonitor {
         public boolean mCanceled = false;
 
         TestClientMonitor() {
@@ -129,5 +129,10 @@
         public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) {
             mCanceled = true;
         }
+
+        @Override
+        public boolean isInterruptable() {
+            return true;
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index 9e9d703..3c77a35 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -57,11 +57,16 @@
 
     public interface FakeHal {}
     public abstract static class InterruptableMonitor<T>
-            extends HalClientMonitor<T> implements  Interruptable {
+            extends HalClientMonitor<T> {
         public InterruptableMonitor() {
             super(null, null, null, null, 0, null, 0, 0,
                     mock(BiometricLogger.class), mock(BiometricContext.class));
         }
+
+        @Override
+        public boolean isInterruptable() {
+            return true;
+        }
     }
 
     @Rule
@@ -293,7 +298,6 @@
 
         assertThat(mOperation.isCanceling()).isTrue();
         verify(mClientMonitor).cancel();
-        verify(mClientMonitor, never()).cancelWithoutStarting(any());
         verify(mClientMonitor, never()).destroy();
 
         mStartedCallbackCaptor.getValue().onClientFinished(mClientMonitor, true);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index eb131419..4c898b0 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -16,7 +16,8 @@
 
 package com.android.server.biometrics.sensors;
 
-import static android.testing.TestableLooper.RunWithLooper;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_SUCCESS;
 
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
@@ -24,17 +25,19 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricService;
 import android.os.Binder;
@@ -63,32 +66,37 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.function.Supplier;
 
 @Presubmit
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class BiometricSchedulerTest {
 
     private static final String TAG = "BiometricSchedulerTest";
     private static final int TEST_SENSOR_ID = 1;
     private static final int LOG_NUM_RECENT_OPERATIONS = 2;
-
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            InstrumentationRegistry.getContext(), null);
     private BiometricScheduler mScheduler;
     private IBinder mToken;
-
     @Mock
     private IBiometricService mBiometricService;
-
-    @Rule
-    public final TestableContext mContext =
-            new TestableContext(InstrumentationRegistry.getContext(), null);
+    @Mock
+    private BiometricContext mBiometricContext;
+    @Mock
+    private AuthSessionCoordinator mAuthSessionCoordinator;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mToken = new Binder();
+        when(mAuthSessionCoordinator.getLockoutStateFor(anyInt(), anyInt())).thenReturn(
+                BIOMETRIC_SUCCESS);
+        when(mBiometricContext.getAuthSessionCoordinator()).thenReturn(mAuthSessionCoordinator);
         mScheduler = new BiometricScheduler(TAG, new Handler(TestableLooper.get(this).getLooper()),
                 BiometricScheduler.SENSOR_TYPE_UNKNOWN, null /* gestureAvailabilityTracker */,
                 mBiometricService, LOG_NUM_RECENT_OPERATIONS);
@@ -98,10 +106,10 @@
     public void testClientDuplicateFinish_ignoredBySchedulerAndDoesNotCrash() {
         final Supplier<Object> nonNullDaemon = () -> mock(Object.class);
 
-        final HalClientMonitor<Object> client1 =
-                new TestHalClientMonitor(mContext, mToken, nonNullDaemon);
-        final HalClientMonitor<Object> client2 =
-                new TestHalClientMonitor(mContext, mToken, nonNullDaemon);
+        final HalClientMonitor<Object> client1 = new TestHalClientMonitor(mContext, mToken,
+                nonNullDaemon);
+        final HalClientMonitor<Object> client2 = new TestHalClientMonitor(mContext, mToken,
+                nonNullDaemon);
         mScheduler.scheduleClientMonitor(client1);
         mScheduler.scheduleClientMonitor(client2);
 
@@ -112,10 +120,9 @@
     @Test
     public void testRemovesPendingOperations_whenNullHal_andNotBiometricPrompt() {
         // Even if second client has a non-null daemon, it needs to be canceled.
-        final TestHalClientMonitor client1 = new TestHalClientMonitor(
-                mContext, mToken, () -> null);
-        final TestHalClientMonitor client2 = new TestHalClientMonitor(
-                mContext, mToken, () -> mock(Object.class));
+        final TestHalClientMonitor client1 = new TestHalClientMonitor(mContext, mToken, () -> null);
+        final TestHalClientMonitor client2 = new TestHalClientMonitor(mContext, mToken,
+                () -> mock(Object.class));
 
         final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
         final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
@@ -150,10 +157,10 @@
 
         final ClientMonitorCallbackConverter listener1 = mock(ClientMonitorCallbackConverter.class);
 
-        final TestAuthenticationClient client1 =
-                new TestAuthenticationClient(mContext, () -> null, mToken, listener1);
-        final TestHalClientMonitor client2 =
-                new TestHalClientMonitor(mContext, mToken, () -> daemon2);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, () -> null,
+                mToken, listener1, mBiometricContext);
+        final TestHalClientMonitor client2 = new TestHalClientMonitor(mContext, mToken,
+                () -> daemon2);
 
         final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
         final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
@@ -188,15 +195,15 @@
     @Test
     public void testCancelNotInvoked_whenOperationWaitingForCookie() {
         final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
-        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
-                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class));
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, lazyDaemon1,
+                mToken, mock(ClientMonitorCallbackConverter.class), mBiometricContext);
         final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
 
         // Schedule a BiometricPrompt authentication request
         mScheduler.scheduleClientMonitor(client1, callback1);
 
-        assertNotEquals(0, mScheduler.mCurrentOperation.isReadyToStart(
-                mock(ClientMonitorCallback.class)));
+        assertNotEquals(0,
+                mScheduler.mCurrentOperation.isReadyToStart(mock(ClientMonitorCallback.class)));
         assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
         assertEquals(0, mScheduler.mPendingOperations.size());
 
@@ -304,7 +311,7 @@
         final TestHalClientMonitor client1 = new TestHalClientMonitor(mContext, mToken, lazyDaemon);
         final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
         final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, lazyDaemon,
-                mToken, callback);
+                mToken, callback, mBiometricContext);
 
         // Add a non-cancellable client, then add the auth client
         mScheduler.scheduleClientMonitor(client1);
@@ -323,7 +330,7 @@
         client1.getCallback().onClientFinished(client1, true /* success */);
         waitForIdle();
         verify(callback).onError(anyInt(), anyInt(),
-                eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
+                eq(BIOMETRIC_ERROR_CANCELED),
                 eq(0) /* vendorCode */);
         assertNull(mScheduler.getCurrentClient());
         assertTrue(client1.isAlreadyDone());
@@ -367,7 +374,8 @@
         final Supplier<Object> lazyDaemon = () -> mock(Object.class);
         final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
         testCancelsWhenRequestId(requestId, cancelRequestId, started,
-                new TestAuthenticationClient(mContext, lazyDaemon, mToken, callback));
+                new TestAuthenticationClient(mContext, lazyDaemon, mToken, callback,
+                        mBiometricContext));
     }
 
     @Test
@@ -448,11 +456,11 @@
         final long requestId2 = 20;
         final Supplier<Object> lazyDaemon = () -> mock(Object.class);
         final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class);
-        final TestAuthenticationClient client1 = new TestAuthenticationClient(
-                mContext, lazyDaemon, mToken, callback);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext, lazyDaemon,
+                mToken, callback, mBiometricContext);
         client1.setRequestId(requestId1);
-        final TestAuthenticationClient client2 = new TestAuthenticationClient(
-                mContext, lazyDaemon, mToken, callback);
+        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, lazyDaemon,
+                mToken, callback, mBiometricContext);
         client2.setRequestId(requestId2);
 
         mScheduler.scheduleClientMonitor(client1);
@@ -474,8 +482,8 @@
 
     @Test
     public void testInterruptPrecedingClients_whenExpected() {
-        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class,
-                withSettings().extraInterfaces(Interruptable.class));
+        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class);
+        when(interruptableMonitor.isInterruptable()).thenReturn(true);
 
         final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
         when(interrupter.interruptsPrecedingClients()).thenReturn(true);
@@ -484,14 +492,14 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor).cancel();
+        verify(interruptableMonitor).cancel();
         mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */);
     }
 
     @Test
     public void testDoesNotInterruptPrecedingClients_whenNotExpected() {
-        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class,
-                withSettings().extraInterfaces(Interruptable.class));
+        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class);
+        when(interruptableMonitor.isInterruptable()).thenReturn(true);
 
         final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
         when(interrupter.interruptsPrecedingClients()).thenReturn(false);
@@ -500,39 +508,205 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor, never()).cancel();
+        verify(interruptableMonitor, never()).cancel();
     }
 
     @Test
     public void testClientDestroyed_afterFinish() {
         final Supplier<Object> nonNullDaemon = () -> mock(Object.class);
-        final TestHalClientMonitor client =
-                new TestHalClientMonitor(mContext, mToken, nonNullDaemon);
+        final TestHalClientMonitor client = new TestHalClientMonitor(mContext, mToken,
+                nonNullDaemon);
         mScheduler.scheduleClientMonitor(client);
         client.mCallback.onClientFinished(client, true /* success */);
         waitForIdle();
         assertTrue(client.mDestroyed);
     }
 
+    @Test
+    public void testClearBiometricQueue_clearsHungAuthOperation() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */,
+                mBiometricContext);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        waitForIdle();
+
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        //Checking client is hung
+        verify(callback1).onClientStarted(client1);
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        assertNotNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+    }
+
+    @Test
+    public void testAuthWorks_afterClearBiometricQueue() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */,
+                mBiometricContext);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Checking client is hung
+        waitForIdle();
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+        assertNull(mScheduler.mCurrentOperation);
+
+
+        //Run additional auth client
+        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */,
+                mBiometricContext);
+        final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client2, callback2);
+
+        assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        //Ensure auth client passes
+        verify(callback2).onClientStarted(client2);
+        client2.getCallback().onClientFinished(client2, true);
+        waitForIdle();
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Asserting auth client passes
+        assertTrue(client2.isAlreadyDone());
+        assertNotNull(mScheduler.mCurrentOperation);
+    }
+
+    @Test
+    public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() {
+        //Creating clients
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */,
+                mBiometricContext);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(2, mScheduler.getCurrentPendingCount());
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        verify(callback1).onClientStarted(client1);
+
+        //Client finishes successfully
+        client1.getCallback().onClientFinished(client1, true);
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Watchdog does not clear pending operations
+        assertEquals(1, mScheduler.getCurrentPendingCount());
+        assertNotNull(mScheduler.mCurrentOperation);
+
+    }
+
     private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
         return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
     }
 
+    private void waitForIdle() {
+        TestableLooper.get(this).processAllMessages();
+    }
+
     private static class TestAuthenticationClient extends AuthenticationClient<Object> {
         boolean mStartedHal = false;
         boolean mStoppedHal = false;
         boolean mDestroyed = false;
         int mNumCancels = 0;
+        boolean mAuthenticateCalled = false;
+        boolean mOnErrorCalled = false;
 
-        public TestAuthenticationClient(@NonNull Context context,
+        TestAuthenticationClient(@NonNull Context context,
                 @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
-                @NonNull ClientMonitorCallbackConverter listener) {
+                @NonNull ClientMonitorCallbackConverter listener,
+                BiometricContext biometricContext) {
+            this(context, lazyDaemon, token, listener, 1 /* cookie */, biometricContext);
+        }
+
+        TestAuthenticationClient(@NonNull Context context,
+                @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
+                @NonNull ClientMonitorCallbackConverter listener, int cookie,
+                @NonNull BiometricContext biometricContext) {
             super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
-                    false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
-                    TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class),
+                    false /* restricted */, TAG, cookie, false /* requireConfirmation */,
+                    TEST_SENSOR_ID, mock(BiometricLogger.class), biometricContext,
                     true /* isStrongBiometric */, null /* taskStackListener */,
-                    mock(LockoutTracker.class), false /* isKeyguard */,
-                    true /* shouldVibrate */, false /* isKeyguardBypassEnabled */);
+                    null /* lockoutTracker */, false /* isKeyguard */,
+                    true /* shouldVibrate */, false /* isKeyguardBypassEnabled */,
+                    0 /* sensorStrength */);
         }
 
         @Override
@@ -546,7 +720,19 @@
         }
 
         @Override
-        protected void handleLifecycleAfterAuth(boolean authenticated) {}
+        protected void handleLifecycleAfterAuth(boolean authenticated) {
+        }
+
+        @Override
+        public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
+                boolean authenticated, ArrayList<Byte> hardwareAuthToken) {
+            mAuthenticateCalled = true;
+        }
+
+        @Override
+        protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) {
+            mOnErrorCalled = true;
+        }
 
         @Override
         public boolean wasUserDetected() {
@@ -571,14 +757,12 @@
         boolean mStoppedHal = false;
         int mNumCancels = 0;
 
-        TestEnrollClient(@NonNull Context context,
-                @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
-                @NonNull ClientMonitorCallbackConverter listener) {
+        TestEnrollClient(@NonNull Context context, @NonNull Supplier<Object> lazyDaemon,
+                @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener) {
             super(context, lazyDaemon, token, listener, 0 /* userId */, new byte[69],
-                    "test" /* owner */, mock(BiometricUtils.class),
-                    5 /* timeoutSec */, TEST_SENSOR_ID,
-                    true /* shouldVibrate */,
-                    mock(BiometricLogger.class), mock(BiometricContext.class));
+                    "test" /* owner */, mock(BiometricUtils.class), 5 /* timeoutSec */,
+                    TEST_SENSOR_ID, true /* shouldVibrate */, mock(BiometricLogger.class),
+                    mock(BiometricContext.class));
         }
 
         @Override
@@ -616,9 +800,9 @@
 
         TestHalClientMonitor(@NonNull Context context, @NonNull IBinder token,
                 @NonNull Supplier<Object> lazyDaemon, int cookie, int protoEnum) {
-            super(context, lazyDaemon, token /* token */, null /* listener */, 0 /* userId */,
-                    TAG, cookie, TEST_SENSOR_ID,
-                    mock(BiometricLogger.class), mock(BiometricContext.class));
+            super(context, lazyDaemon, token /* token */, null /* listener */, 0 /* userId */, TAG,
+                    cookie, TEST_SENSOR_ID, mock(BiometricLogger.class),
+                    mock(BiometricContext.class));
             mProtoEnum = protoEnum;
         }
 
@@ -651,8 +835,4 @@
             mDestroyed = true;
         }
     }
-
-    private void waitForIdle() {
-        TestableLooper.get(this).processAllMessages();
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
index 0b10a7b..968844e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java
@@ -50,16 +50,22 @@
 
     private static void unlockAllBiometrics(MultiBiometricLockoutState lockoutState, int userId) {
         lockoutState.setAuthenticatorTo(userId, BIOMETRIC_STRONG, true /* canAuthenticate */);
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isTrue();
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isTrue();
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     private static void lockoutAllBiometrics(MultiBiometricLockoutState lockoutState, int userId) {
         lockoutState.setAuthenticatorTo(userId, BIOMETRIC_STRONG, false /* canAuthenticate */);
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isFalse();
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isFalse();
-        assertThat(lockoutState.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isFalse();
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(lockoutState.getLockoutState(userId, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     private void unlockAllBiometrics() {
@@ -79,9 +85,12 @@
 
     @Test
     public void testInitialStateLockedOut() {
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     @Test
@@ -89,20 +98,26 @@
         unlockAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_CONVENIENCE,
                 false /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
         assertThat(
-                mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+                mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
     public void testWeakLockout() {
         unlockAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_WEAK, false /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(
-                mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+                mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
@@ -110,10 +125,13 @@
         lockoutAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_STRONG,
                 false /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(
-                mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+                mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
@@ -121,18 +139,24 @@
         lockoutAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_CONVENIENCE,
                 true /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     @Test
     public void testWeakUnlock() {
         lockoutAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_WEAK, true /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     @Test
@@ -140,9 +164,12 @@
         lockoutAllBiometrics();
         mLockoutState.setAuthenticatorTo(PRIMARY_USER, BIOMETRIC_STRONG,
                 true /* canAuthenticate */);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 
     @Test
@@ -154,45 +181,66 @@
         lockoutAllBiometrics(lockoutState, userTwo);
 
         lockoutState.setAuthenticatorTo(userOne, BIOMETRIC_WEAK, true /* canAuthenticate */);
-        assertThat(lockoutState.canUserAuthenticate(userOne, BIOMETRIC_STRONG)).isFalse();
-        assertThat(lockoutState.canUserAuthenticate(userOne, BIOMETRIC_WEAK)).isTrue();
-        assertThat(lockoutState.canUserAuthenticate(userOne, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(lockoutState.getLockoutState(userOne, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(lockoutState.getLockoutState(userOne, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(lockoutState.getLockoutState(userOne, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
-        assertThat(lockoutState.canUserAuthenticate(userTwo, BIOMETRIC_STRONG)).isFalse();
-        assertThat(lockoutState.canUserAuthenticate(userTwo, BIOMETRIC_WEAK)).isFalse();
-        assertThat(lockoutState.canUserAuthenticate(userTwo, BIOMETRIC_CONVENIENCE)).isFalse();
+        assertThat(lockoutState.getLockoutState(userTwo, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(lockoutState.getLockoutState(userTwo, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
+        assertThat(lockoutState.getLockoutState(userTwo, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_PERMANENT);
     }
 
     @Test
     public void testTimedLockout() {
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
         mLockoutState.increaseLockoutTime(PRIMARY_USER, BIOMETRIC_STRONG,
                 System.currentTimeMillis() + 1);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
         assertThat(
-                mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+                mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
     }
 
     @Test
     public void testTimedLockoutAfterDuration() {
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
 
         when(mClock.millis()).thenReturn(0L);
         mLockoutState.increaseLockoutTime(PRIMARY_USER, BIOMETRIC_STRONG, 1);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
         assertThat(
-                mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse();
+                mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_TIMED);
 
         when(mClock.millis()).thenReturn(2L);
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue();
-        assertThat(mLockoutState.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue();
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
+        assertThat(mLockoutState.getLockoutState(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
+                LockoutTracker.LOCKOUT_NONE);
     }
 }
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/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index 2dc3583..d10d7d4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -47,7 +47,6 @@
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.face.UsageStats;
 
 import org.junit.Before;
@@ -85,8 +84,6 @@
     @Mock
     private BiometricContext mBiometricContext;
     @Mock
-    private LockoutCache mLockoutCache;
-    @Mock
     private UsageStats mUsageStats;
     @Mock
     private ClientMonitorCallback mCallback;
@@ -161,7 +158,7 @@
                 false /* restricted */, "test-owner", 4 /* cookie */,
                 false /* requireConfirmation */, 9 /* sensorId */,
                 mBiometricLogger, mBiometricContext, true /* isStrongBiometric */,
-                mUsageStats, mLockoutCache, false /* allowBackgroundAuthentication */,
+                mUsageStats, null /* mLockoutCache */, false /* allowBackgroundAuthentication */,
                 false /* isKeyguardBypassEnabled */, null /* sensorPrivacyManager */,
                 0 /* biometricStrength */) {
             @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 12b8264..41f7433 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -39,6 +39,7 @@
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.HalClientMonitor;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
@@ -63,6 +64,8 @@
     private IFace mDaemon;
     @Mock
     private BiometricContext mBiometricContext;
+    @Mock
+    private BiometricStateCallback mBiometricStateCallback;
 
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -91,8 +94,8 @@
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG,
-                mLockoutResetDispatcher, mBiometricContext);
+        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback,
+                mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
     }
 
     @SuppressWarnings("rawtypes")
@@ -140,11 +143,13 @@
 
         TestableFaceProvider(@NonNull IFace daemon,
                 @NonNull Context context,
+                @NonNull BiometricStateCallback biometricStateCallback,
                 @NonNull SensorProps[] props,
                 @NonNull String halInstanceName,
                 @NonNull LockoutResetDispatcher lockoutResetDispatcher,
                 @NonNull BiometricContext biometricContext) {
-            super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext);
+            super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
+                    biometricContext);
             mDaemon = daemon;
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
index 2afc4d7..1f29bec 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
@@ -17,6 +17,7 @@
 package com.android.server.biometrics.sensors.face.aidl;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
@@ -25,7 +26,10 @@
 
 import android.content.Context;
 import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
+import android.hardware.biometrics.face.SensorProps;
 import android.os.Handler;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -36,6 +40,7 @@
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.LockoutCache;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
@@ -74,12 +79,18 @@
     private BiometricContext mBiometricContext;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private IFace mDaemon;
+    @Mock
+    private BiometricStateCallback mBiometricStateCallback;
 
     private final TestLooper mLooper = new TestLooper();
     private final LockoutCache mLockoutCache = new LockoutCache();
 
     private UserAwareBiometricScheduler mScheduler;
     private Sensor.HalSessionCallback mHalCallback;
+    private FaceProvider mFaceProvider;
+    private SensorProps[] mSensorProps;
 
     @Before
     public void setUp() {
@@ -99,6 +110,16 @@
         mHalCallback = new Sensor.HalSessionCallback(mContext, new Handler(mLooper.getLooper()),
                 TAG, mScheduler, SENSOR_ID,
                 USER_ID, mLockoutCache, mLockoutResetDispatcher, mHalSessionCallback);
+
+        final SensorProps sensor1 = new SensorProps();
+        sensor1.commonProps = new CommonProps();
+        sensor1.commonProps.sensorId = 0;
+        final SensorProps sensor2 = new SensorProps();
+        sensor2.commonProps = new CommonProps();
+        sensor2.commonProps.sensorId = 1;
+        mSensorProps = new SensorProps[]{sensor1, sensor2};
+        mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
+                mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
     }
 
     @Test
@@ -128,6 +149,18 @@
         verifyNotLocked();
     }
 
+    @Test
+    public void onBinderDied_noErrorOnNullClient() {
+        mScheduler.reset();
+        assertNull(mScheduler.getCurrentClient());
+        mFaceProvider.binderDied();
+
+        for (int i = 0; i < mFaceProvider.mSensors.size(); i++) {
+            final Sensor sensor = mFaceProvider.mSensors.valueAt(i);
+            assertNull(sensor.getSessionForUser(USER_ID));
+        }
+    }
+
     private void verifyNotLocked() {
         assertEquals(LockoutTracker.LOCKOUT_NONE, mLockoutCache.getLockoutModeForUser(USER_ID));
         verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index 116d2d5..a2cade7 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -43,6 +43,7 @@
 
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 
 import org.junit.Before;
@@ -73,6 +74,8 @@
     private BiometricScheduler mScheduler;
     @Mock
     private BiometricContext mBiometricContext;
+    @Mock
+    private BiometricStateCallback mBiometricStateCallback;
 
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -103,8 +106,8 @@
                 resetLockoutRequiresChallenge);
 
         Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST"));
-        mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler,
-                mBiometricContext);
+        mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps,
+                mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
         mBinder = new Binder();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 73548a3..5e5b48d 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -41,6 +41,7 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -62,7 +63,6 @@
 import com.android.server.biometrics.sensors.AuthSessionCoordinator;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.LockoutCache;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -74,6 +74,7 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
@@ -112,8 +113,6 @@
     @Mock
     private BiometricManager mBiometricManager;
     @Mock
-    private LockoutCache mLockoutCache;
-    @Mock
     private IUdfpsOverlayController mUdfpsOverlayController;
     @Mock
     private ISidefpsController mSideFpsController;
@@ -131,6 +130,8 @@
     private Probe mLuxProbe;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private Clock mClock;
     @Captor
     private ArgumentCaptor<OperationContext> mOperationContextCaptor;
     @Captor
@@ -372,6 +373,7 @@
     @Test
     public void fingerprintPowerIgnoresAuthInWindow() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
 
         final FingerprintAuthenticationClient client = createClient(1);
         client.start(mCallback);
@@ -382,11 +384,13 @@
         mLooper.dispatchAll();
 
         verify(mCallback).onClientFinished(any(), eq(false));
+        verify(mCancellationSignal).cancel();
     }
 
     @Test
     public void fingerprintAuthIgnoredWaitingForPower() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
 
         final FingerprintAuthenticationClient client = createClient(1);
         client.start(mCallback);
@@ -397,11 +401,13 @@
         mLooper.dispatchAll();
 
         verify(mCallback).onClientFinished(any(), eq(false));
+        verify(mCancellationSignal).cancel();
     }
 
     @Test
-    public void fingerprintAuthSucceedsAfterPowerWindow() throws Exception {
+    public void fingerprintAuthFailsWhenAuthAfterPower() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal);
 
         final FingerprintAuthenticationClient client = createClient(1);
         client.start(mCallback);
@@ -415,7 +421,9 @@
         mLooper.moveTimeForward(1000);
         mLooper.dispatchAll();
 
-        verify(mCallback).onClientFinished(any(), eq(true));
+        verify(mCallback, never()).onClientFinished(any(), eq(true));
+        verify(mCallback).onClientFinished(any(), eq(false));
+        when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal);
     }
 
     @Test
@@ -451,6 +459,52 @@
     }
 
     @Test
+    public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
+        mLooper.dispatchAll();
+
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+    }
+
+    @Test
     public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
 
@@ -497,6 +551,93 @@
         verify(mCallback).onClientFinished(any(), eq(true));
     }
 
+    @Test
+    public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
+        final int powerWindow = 500;
+        final long authStart = 300;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth occurs at time = 300
+        when(mClock.millis()).thenReturn(authStart);
+        // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        // After waiting 200 milliseconds, auth should succeed.
+        mLooper.moveTimeForward(powerWindow - authStart);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
+        final int powerWindow = 500;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth reject occurs at time = 300ms
+        when(mClock.millis()).thenReturn(300L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                false /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(300);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        when(mClock.millis()).thenReturn(1300L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // If code is correct, the new acquired start timestamp should be used
+        // and the code should only have to wait 500 - (1500-1300)ms.
+        when(mClock.millis()).thenReturn(1500L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(299);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        mLooper.moveTimeForward(1);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFpsPowerPressCancelsIsntantly() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+
+        client.onPowerPressed();
+        mLooper.dispatchAll();
+
+        verify(mCallback, never()).onClientFinished(any(), eq(true));
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
@@ -521,10 +662,10 @@
                 false /* requireConfirmation */,
                 9 /* sensorId */, mBiometricLogger, mBiometricContext,
                 true /* isStrongBiometric */,
-                null /* taskStackListener */, mLockoutCache,
-                mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
+                null /* taskStackListener */, null /* lockoutCache */,
+                mUdfpsOverlayController, mSideFpsController, null, allowBackgroundAuthentication,
                 mSensorProps,
-                new Handler(mLooper.getLooper()), 0 /* biometricStrength */) {
+                new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) {
             @Override
             protected ActivityTaskManager getActivityTaskManager() {
                 return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/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/camera/CameraServiceProxyTest.java b/services/tests/servicestests/src/com/android/server/camera/CameraServiceProxyTest.java
index ea746d1..faad961 100644
--- a/services/tests/servicestests/src/com/android/server/camera/CameraServiceProxyTest.java
+++ b/services/tests/servicestests/src/com/android/server/camera/CameraServiceProxyTest.java
@@ -30,7 +30,7 @@
 import android.view.Display;
 import android.view.Surface;
 
-import java.util.HashMap;
+import java.util.Map;
 
 @RunWith(JUnit4.class)
 public class CameraServiceProxyTest {
@@ -75,24 +75,22 @@
                 /*ignoreResizableAndSdkCheck*/true)).isEqualTo(
                 CameraMetadata.SCALER_ROTATE_AND_CROP_NONE);
         // Check rotation and lens facing combinations
-        HashMap<Integer, Integer> backFacingMap = new HashMap<Integer, Integer>() {{
-            put(Surface.ROTATION_0, CameraMetadata.SCALER_ROTATE_AND_CROP_NONE);
-            put(Surface.ROTATION_90, CameraMetadata.SCALER_ROTATE_AND_CROP_90);
-            put(Surface.ROTATION_270, CameraMetadata.SCALER_ROTATE_AND_CROP_270);
-            put(Surface.ROTATION_180, CameraMetadata.SCALER_ROTATE_AND_CROP_180);
-        }};
+        Map<Integer, Integer> backFacingMap = Map.of(
+                Surface.ROTATION_0, CameraMetadata.SCALER_ROTATE_AND_CROP_NONE,
+                Surface.ROTATION_90, CameraMetadata.SCALER_ROTATE_AND_CROP_90,
+                Surface.ROTATION_270, CameraMetadata.SCALER_ROTATE_AND_CROP_270,
+                Surface.ROTATION_180, CameraMetadata.SCALER_ROTATE_AND_CROP_180);
         taskInfo.isFixedOrientationPortrait = true;
         backFacingMap.forEach((key, value) -> {
             assertThat(CameraServiceProxy.getCropRotateScale(ctx, ctx.getPackageName(), taskInfo,
                     key, CameraCharacteristics.LENS_FACING_BACK,
                     /*ignoreResizableAndSdkCheck*/true)).isEqualTo(value);
         });
-        HashMap<Integer, Integer> frontFacingMap = new HashMap<Integer, Integer>() {{
-            put(Surface.ROTATION_0, CameraMetadata.SCALER_ROTATE_AND_CROP_NONE);
-            put(Surface.ROTATION_90, CameraMetadata.SCALER_ROTATE_AND_CROP_270);
-            put(Surface.ROTATION_270, CameraMetadata.SCALER_ROTATE_AND_CROP_90);
-            put(Surface.ROTATION_180, CameraMetadata.SCALER_ROTATE_AND_CROP_180);
-        }};
+        Map<Integer, Integer> frontFacingMap = Map.of(
+                Surface.ROTATION_0, CameraMetadata.SCALER_ROTATE_AND_CROP_NONE,
+                Surface.ROTATION_90, CameraMetadata.SCALER_ROTATE_AND_CROP_270,
+                Surface.ROTATION_270, CameraMetadata.SCALER_ROTATE_AND_CROP_90,
+                Surface.ROTATION_180, CameraMetadata.SCALER_ROTATE_AND_CROP_180);
         frontFacingMap.forEach((key, value) -> {
             assertThat(CameraServiceProxy.getCropRotateScale(ctx, ctx.getPackageName(), taskInfo,
                     key, CameraCharacteristics.LENS_FACING_FRONT,
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 6b8c26d..d2f2af1 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.companion.virtual;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
 
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -88,6 +91,30 @@
     }
 
     @Test
+    public void registerInputDevice_deviceCreation_hasDeviceId() {
+        final IBinder device1Token = new Binder("device1");
+        mInputController.createMouse("mouse", /*vendorId= */ 1, /*productId= */ 1, device1Token,
+                /* displayId= */ 1);
+        int device1Id = mInputController.getInputDeviceId(device1Token);
+
+        final IBinder device2Token = new Binder("device2");
+        mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2,
+                device2Token, 2);
+        int device2Id = mInputController.getInputDeviceId(device2Token);
+
+        assertWithMessage("Different devices should have different id").that(
+                device1Id).isNotEqualTo(device2Id);
+
+
+        int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+        assertWithMessage("InputManager's deviceIds list should contain id of device 1").that(
+                deviceIds).asList().contains(device1Id);
+        assertWithMessage("InputManager's deviceIds list should contain id of device 2").that(
+                deviceIds).asList().contains(device2Id);
+
+    }
+
+    @Test
     public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() {
         final IBinder deviceToken = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
@@ -115,4 +142,5 @@
         mInputController.unregisterInputDevice(deviceToken);
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
     }
+
 }
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 9c5d1a5..c715a21 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
@@ -16,6 +16,9 @@
 
 package com.android.server.companion.virtual;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
 import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -44,6 +47,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.companion.AssociationInfo;
 import android.companion.virtual.IVirtualDeviceActivityListener;
+import android.companion.virtual.VirtualDeviceManager;
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.audio.IAudioConfigChangedCallback;
 import android.companion.virtual.audio.IAudioRoutingCallback;
@@ -121,6 +125,7 @@
     private static final int VENDOR_ID = 5;
     private static final String UNIQUE_ID = "uniqueid";
     private static final String PHYS = "phys";
+    private static final int DEVICE_ID = 42;
     private static final int HEIGHT = 1800;
     private static final int WIDTH = 900;
     private static final Binder BINDER = new Binder("binder");
@@ -239,6 +244,55 @@
                 mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
                 mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
                 mActivityListener, mRunningAppsChangedCallback, params);
+        mVdms.addVirtualDevice(mDeviceImpl);
+    }
+
+    @Test
+    public void getDevicePolicy_invalidDeviceId_returnsDefault() {
+        assertThat(
+                mLocalService.getDevicePolicy(
+                        VirtualDeviceManager.INVALID_DEVICE_ID, POLICY_TYPE_SENSORS))
+                .isEqualTo(DEVICE_POLICY_DEFAULT);
+    }
+
+    @Test
+    public void getDevicePolicy_defaultDeviceId_returnsDefault() {
+        assertThat(
+                mLocalService.getDevicePolicy(
+                        VirtualDeviceManager.DEFAULT_DEVICE_ID, POLICY_TYPE_SENSORS))
+                .isEqualTo(DEVICE_POLICY_DEFAULT);
+    }
+
+    @Test
+    public void getDevicePolicy_nonExistentDeviceId_returnsDefault() {
+        assertThat(
+                mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId() + 1, POLICY_TYPE_SENSORS))
+                .isEqualTo(DEVICE_POLICY_DEFAULT);
+    }
+
+    @Test
+    public void getDevicePolicy_unspecifiedPolicy_returnsDefault() {
+        assertThat(
+                mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId(), POLICY_TYPE_SENSORS))
+                .isEqualTo(DEVICE_POLICY_DEFAULT);
+    }
+
+    @Test
+    public void getDevicePolicy_returnsCustom() {
+        VirtualDeviceParams params = new VirtualDeviceParams
+                .Builder()
+                .setBlockedActivities(getBlockedActivities())
+                .addDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
+                .build();
+        mDeviceImpl = new VirtualDeviceImpl(mContext,
+                mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1,
+                mInputController, (int associationId) -> {}, mPendingTrampolineCallback,
+                mActivityListener, mRunningAppsChangedCallback, params);
+        mVdms.addVirtualDevice(mDeviceImpl);
+
+        assertThat(
+                mLocalService.getDevicePolicy(mDeviceImpl.getDeviceId(), POLICY_TYPE_SENSORS))
+                .isEqualTo(DEVICE_POLICY_CUSTOM);
     }
 
     @Test
@@ -530,6 +584,16 @@
     }
 
     @Test
+    public void createVirtualKeyboard_inputDeviceId_obtainFromInputController() {
+        final int fd = 1;
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */ 1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+        assertWithMessage(
+                "InputController should return device id from InputDeviceDescriptor").that(
+                mInputController.getInputDeviceId(BINDER)).isEqualTo(DEVICE_ID);
+    }
+
+    @Test
     public void onAudioSessionStarting_hasVirtualAudioController() {
         mDeviceImpl.onVirtualDisplayCreatedLocked(
                 mDeviceImpl.createWindowPolicyController(), DISPLAY_ID);
@@ -576,9 +640,9 @@
         final int fd = 1;
         final int keyCode = KeyEvent.KEYCODE_A;
         final int action = VirtualKeyEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 1,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+
         mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode)
                 .setAction(action).build());
         verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action);
@@ -601,9 +665,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
                 .setButtonCode(buttonCode)
@@ -616,9 +679,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -642,9 +704,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
                 .setRelativeX(x).setRelativeY(y).build());
@@ -656,9 +717,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -683,9 +743,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
                 .setXAxisMovement(x)
@@ -698,9 +757,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -731,9 +789,8 @@
         final float x = 100.5f;
         final float y = 200.5f;
         final int action = VirtualTouchEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build());
         verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
@@ -750,9 +807,8 @@
         final int action = VirtualTouchEvent.ACTION_UP;
         final float pressure = 1.0f;
         final float majorAxisSize = 10.0f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType)
                 .setPressure(pressure).setMajorAxisSize(majorAxisSize).build());
@@ -888,4 +944,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/companion/virtual/VirtualDeviceParamsTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
index 77f1e24..036b6df 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceParamsTest.java
@@ -37,6 +37,8 @@
         VirtualDeviceParams originalParams = new VirtualDeviceParams.Builder()
                 .setLockState(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED)
                 .setUsersWithMatchingAccounts(Set.of(UserHandle.of(123), UserHandle.of(456)))
+                .addDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS,
+                        VirtualDeviceParams.DEVICE_POLICY_CUSTOM)
                 .build();
         Parcel parcel = Parcel.obtain();
         originalParams.writeToParcel(parcel, 0);
@@ -47,5 +49,7 @@
         assertThat(params.getLockState()).isEqualTo(VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED);
         assertThat(params.getUsersWithMatchingAccounts())
                 .containsExactly(UserHandle.of(123), UserHandle.of(456));
+        assertThat(params.getDevicePolicy(VirtualDeviceParams.POLICY_TYPE_SENSORS))
+                .isEqualTo(VirtualDeviceParams.DEVICE_POLICY_CUSTOM);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 4c939f0..0262f56 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -82,6 +82,7 @@
                 /* blockedActivities= */ new ArraySet<>(),
                 VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED,
                 /* activityListener= */ null,
+                /* pipBlockedCallback= */ null,
                 /* activityBlockedCallback= */ null,
                 /* secureWindowCallback= */ null,
                 /* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index ddb3049..8e669f0 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -140,7 +140,7 @@
 import android.security.keystore.AttestationUtils;
 import android.telephony.TelephonyManager;
 import android.telephony.data.ApnSetting;
-import android.test.MoreAsserts; // TODO(b/171932723): replace by Truth
+import android.test.MoreAsserts;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
@@ -5087,7 +5087,7 @@
     }
 
     @Test
-    public void testWipeDataDeviceOwner() throws Exception {
+    public void testWipeDevice_DeviceOwner() throws Exception {
         setDeviceOwner();
         when(getServices().userManager.getUserRestrictionSource(
                 UserManager.DISALLOW_FACTORY_RESET,
@@ -5096,7 +5096,7 @@
         when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
                 thenReturn("Just a test string.");
 
-        dpm.wipeData(0);
+        dpm.wipeDevice(0);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ false);
     }
@@ -5111,13 +5111,13 @@
         when(mContext.getResources().getString(R.string.work_profile_deleted_description_dpm_wipe)).
                 thenReturn("Just a test string.");
 
-        dpm.wipeData(WIPE_EUICC);
+        dpm.wipeDevice(WIPE_EUICC);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ true);
     }
 
     @Test
-    public void testWipeDataDeviceOwnerDisallowed() throws Exception {
+    public void testWipeDevice_DeviceOwnerDisallowed() throws Exception {
         setDeviceOwner();
         when(getServices().userManager.getUserRestrictionSource(
                 UserManager.DISALLOW_FACTORY_RESET,
@@ -5128,7 +5128,7 @@
         // The DO is not allowed to wipe the device if the user restriction was set
         // by the system
         assertExpectException(SecurityException.class, /* messageRegex= */ null,
-                () -> dpm.wipeData(0));
+                () -> dpm.wipeDevice(0));
     }
 
     @Test
@@ -7986,7 +7986,7 @@
     }
 
     @Test
-    public void testWipeData_financeDo_success() throws Exception {
+    public void testWipeDevice_financeDo_success() throws Exception {
         setDeviceOwner();
         dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED);
         when(getServices().userManager.getUserRestrictionSource(
@@ -7997,7 +7997,7 @@
                 .getString(R.string.work_profile_deleted_description_dpm_wipe))
                 .thenReturn("Test string");
 
-        dpm.wipeData(0);
+        dpm.wipeDevice(0);
 
         verifyRebootWipeUserData(/* wipeEuicc= */ false);
     }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index e991ec6..ac1667d 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -442,7 +442,8 @@
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
         mMockSystemServices.registerReceiver(receiver, filter, null);
-        return spiedContext.registerReceiver(receiver, filter);
+        return spiedContext.registerReceiver(receiver, filter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
index d58d71f..dc46ff8 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
@@ -23,13 +23,13 @@
 
 import android.app.admin.FactoryResetProtectionPolicy;
 import android.os.Parcel;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
index 72fac55..d540734 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
@@ -33,12 +33,12 @@
 import android.os.IpcDataCache;
 import android.os.Parcel;
 import android.os.UserHandle;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.JournaledFile;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.SystemService;
 
 import com.google.common.io.Files;
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
index 1308a3e..7588c79 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
@@ -29,13 +29,13 @@
 import android.app.admin.FreezePeriod;
 import android.app.admin.SystemUpdatePolicy;
 import android.os.Parcel;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
index df672c9..2c4fe53 100644
--- a/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
@@ -424,7 +424,7 @@
 
         @Override
         public LocalDate getLocalDate() {
-            return LocalDate.from(mLocalDate);
+            return mLocalDate;
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 6860abf..062bde8 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 
 import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
 
@@ -660,6 +661,117 @@
                 firstDisplayId);
     }
 
+    /** Tests that the virtual device is created in a device display group. */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. This should be added to the previously created display
+        // group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertEquals(
+                "Both displays should be added to the same displayGroup.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
+    /**
+     * Tests that the virtual display is not added to the device display group when
+     * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
+     */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
+        // the display should not be added to the previously created display group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertNotEquals(
+                "Display 1 should be in the device display group and display 2 in its own display"
+                        + " group.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
     @Test
     public void testGetDisplayIdToMirror() throws Exception {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 18dd264..9e61cab 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -57,8 +57,7 @@
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
-import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
 import android.os.Handler;
 import android.os.IThermalEventListener;
 import android.os.IThermalService;
@@ -71,10 +70,11 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.Preconditions;
@@ -105,8 +105,11 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnitParamsRunner.class)
 public class DisplayModeDirectorTest {
     // The tolerance within which we consider something approximately equals.
     private static final String TAG = "DisplayModeDirectorTest";
@@ -154,8 +157,6 @@
 
     private DisplayModeDirector createDirectorFromRefreshRateArray(
             float[] refreshRates, int baseModeId, float defaultRefreshRate) {
-        DisplayModeDirector director =
-                new DisplayModeDirector(mContext, mHandler, mInjector);
         Display.Mode[] modes = new Display.Mode[refreshRates.length];
         Display.Mode defaultMode = null;
         for (int i = 0; i < refreshRates.length; i++) {
@@ -194,13 +195,24 @@
     }
 
     @Test
-    public void testDisplayModeVoting() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDisplayModeVoting(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // With no votes present, DisplayModeDirector should allow any refresh rate.
-        DesiredDisplayModeSpecs modeSpecs =
-                createDirectorFromFpsRange(60, 90).getDesiredDisplayModeSpecs(DISPLAY_ID);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        DesiredDisplayModeSpecs modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(modeSpecs.baseModeId).isEqualTo(60);
-        assertThat(modeSpecs.primaryRefreshRateRange.min).isEqualTo(0f);
-        assertThat(modeSpecs.primaryRefreshRateRange.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.primary.physical.min).isEqualTo(0f);
+        assertThat(modeSpecs.primary.physical.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.primary.render.min).isEqualTo(0f);
+        assertThat(modeSpecs.primary.render.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.appRequest.physical.min).isEqualTo(0f);
+        assertThat(modeSpecs.appRequest.physical.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.appRequest.render.min).isEqualTo(0f);
+        assertThat(modeSpecs.appRequest.render.max).isEqualTo(Float.POSITIVE_INFINITY);
 
         int numPriorities =
                 DisplayModeDirector.Vote.MAX_PRIORITY - DisplayModeDirector.Vote.MIN_PRIORITY + 1;
@@ -210,21 +222,49 @@
         {
             int minFps = 60;
             int maxFps = 90;
-            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+            director = createDirectorFromFpsRange(60, 90);
             assertTrue(2 * numPriorities < maxFps - minFps + 1);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(DISPLAY_ID, votes);
             for (int i = 0; i < numPriorities; i++) {
                 int priority = Vote.MIN_PRIORITY + i;
-                votes.put(priority, Vote.forRefreshRates(minFps + i, maxFps - i));
+                votes.put(priority, Vote.forPhysicalRefreshRates(minFps + i, maxFps - i));
                 director.injectVotesByDisplay(votesByDisplay);
                 modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
                 assertThat(modeSpecs.baseModeId).isEqualTo(minFps + i);
-                assertThat(modeSpecs.primaryRefreshRateRange.min)
+                assertThat(modeSpecs.primary.physical.min)
                         .isEqualTo((float) (minFps + i));
-                assertThat(modeSpecs.primaryRefreshRateRange.max)
+                assertThat(modeSpecs.primary.physical.max)
                         .isEqualTo((float) (maxFps - i));
+                if (frameRateIsRefreshRate) {
+                    assertThat(modeSpecs.primary.render.min)
+                            .isEqualTo((float) (minFps + i));
+                } else {
+                    assertThat(modeSpecs.primary.render.min).isZero();
+                }
+                assertThat(modeSpecs.primary.render.max)
+                        .isEqualTo((float) (maxFps - i));
+                if (priority >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF) {
+                    assertThat(modeSpecs.appRequest.physical.min)
+                            .isEqualTo((float) (minFps + i));
+                    assertThat(modeSpecs.appRequest.physical.max)
+                            .isEqualTo((float) (maxFps - i));
+                    if (frameRateIsRefreshRate) {
+                        assertThat(modeSpecs.appRequest.render.min).isEqualTo(
+                                (float) (minFps + i));
+                    } else {
+                        assertThat(modeSpecs.appRequest.render.min).isZero();
+                    }
+                    assertThat(modeSpecs.appRequest.render.max).isEqualTo(
+                            (float) (maxFps - i));
+                } else {
+                    assertThat(modeSpecs.appRequest.physical.min).isZero();
+                    assertThat(modeSpecs.appRequest.physical.max).isPositiveInfinity();
+                    assertThat(modeSpecs.appRequest.render.min).isZero();
+                    assertThat(modeSpecs.appRequest.render.max).isPositiveInfinity();
+                }
+
             }
         }
 
@@ -232,42 +272,53 @@
         // presence of higher priority votes.
         {
             assertTrue(numPriorities >= 2);
-            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+            director = createDirectorFromFpsRange(60, 90);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(DISPLAY_ID, votes);
-            votes.put(Vote.MAX_PRIORITY, Vote.forRefreshRates(65, 85));
-            votes.put(Vote.MIN_PRIORITY, Vote.forRefreshRates(70, 80));
+            votes.put(Vote.MAX_PRIORITY, Vote.forPhysicalRefreshRates(65, 85));
+            votes.put(Vote.MIN_PRIORITY, Vote.forPhysicalRefreshRates(70, 80));
             director.injectVotesByDisplay(votesByDisplay);
             modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
             assertThat(modeSpecs.baseModeId).isEqualTo(70);
-            assertThat(modeSpecs.primaryRefreshRateRange.min).isEqualTo(70f);
-            assertThat(modeSpecs.primaryRefreshRateRange.max).isEqualTo(80f);
+            assertThat(modeSpecs.primary.physical.min).isEqualTo(70f);
+            assertThat(modeSpecs.primary.physical.max).isEqualTo(80f);
         }
     }
 
     @Test
-    public void testVotingWithFloatingPointErrors() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithFloatingPointErrors(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         float error = FLOAT_TOLERANCE / 4;
-        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(0, 60));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE,
-                Vote.forRefreshRates(60 + error, 60 + error));
+                Vote.forPhysicalRefreshRates(60 + error, 60 + error));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
-                Vote.forRefreshRates(60 - error, 60 - error));
+                Vote.forPhysicalRefreshRates(60 - error, 60 - error));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
     }
 
     @Test
-    public void testFlickerHasLowerPriorityThanUserAndRangeIsSingle() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testFlickerHasLowerPriorityThanUserAndRangeIsSingle(
+            boolean frameRateIsRefreshRate) {
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
                 < Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
@@ -276,6 +327,7 @@
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH
                 > Vote.PRIORITY_LOW_POWER_MODE);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -295,14 +347,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -310,14 +362,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(90, 90));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -325,14 +377,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[1];
@@ -340,22 +392,28 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(90, 90));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
     }
 
     @Test
-    public void testLPMHasHigherPriorityThanUser() {
-        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
-        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_SIZE);
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testLPMHasHigherPriorityThanUser(boolean frameRateIsRefreshRate) {
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE
+                > Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE
+                > Vote.PRIORITY_APP_REQUEST_SIZE);
 
-
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -375,12 +433,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -388,12 +452,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -401,12 +471,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         appRequestedMode = modes[1];
@@ -414,26 +490,37 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90);
     }
 
     @Test
-    public void testAppRequestRefreshRateRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestRefreshRateRange(boolean frameRateIsRefreshRate) {
         // Confirm that the app request range doesn't include flicker or min refresh rate settings,
         // but does include everything else.
         assertTrue(
                 Vote.PRIORITY_FLICKER_REFRESH_RATE
                         < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
-        assertTrue(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE
                 < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
-        assertTrue(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
@@ -446,25 +533,25 @@
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
         Display.Mode appRequestedMode = modes[1];
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
@@ -474,25 +561,28 @@
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
     }
 
     void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps,
-            float peakFps, float defaultFps, float primaryMin, float primaryMax,
-            float appRequestMin, float appRequestMax) {
+            float peakFps, float defaultFps, RefreshRateRanges primary,
+            RefreshRateRanges appRequest) {
         DesiredDisplayModeSpecs specs = director.getDesiredDisplayModeSpecsWithInjectedFpsSettings(
                 minFps, peakFps, defaultFps);
-        assertThat(specs.primaryRefreshRateRange.min).isEqualTo(primaryMin);
-        assertThat(specs.primaryRefreshRateRange.max).isEqualTo(primaryMax);
-        assertThat(specs.appRequestRefreshRateRange.min).isEqualTo(appRequestMin);
-        assertThat(specs.appRequestRefreshRateRange.max).isEqualTo(appRequestMax);
+        assertThat(specs.primary).isEqualTo(primary);
+        assertThat(specs.appRequest).isEqualTo(appRequest);
     }
 
     @Test
-    public void testSpecsFromRefreshRateSettings() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testSpecsFromRefreshRateSettings(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // Confirm that, with varying settings for min, peak, and default refresh rate,
         // DesiredDisplayModeSpecs is calculated correctly.
         float[] refreshRates = {30.f, 60.f, 90.f, 120.f, 150.f};
@@ -500,17 +590,56 @@
                 createDirectorFromRefreshRateArray(refreshRates, /*baseModeId=*/0);
 
         float inf = Float.POSITIVE_INFINITY;
-        verifySpecsWithRefreshRateSettings(director, 0, 0, 0, 0, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 0, 0, 90, 0, 90, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 0, 0, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 60, 0, 60, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 120, 0, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 0, 90, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 120, 90, 120, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 60, 90, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 120, 0, 90, 120, 0, 120);
-        verifySpecsWithRefreshRateSettings(director, 90, 60, 0, 90, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 60, 120, 90, 60, 90, 0, 120);
+        RefreshRateRange rangeAll = new RefreshRateRange(0, inf);
+        RefreshRateRange range0to60 = new RefreshRateRange(0, 60);
+        RefreshRateRange range0to90 = new RefreshRateRange(0, 90);
+        RefreshRateRange range0to120 = new RefreshRateRange(0, 120);
+        RefreshRateRange range60to90 = new RefreshRateRange(60, 90);
+        RefreshRateRange range90to90 = new RefreshRateRange(90, 90);
+        RefreshRateRange range90to120 = new RefreshRateRange(90, 120);
+        RefreshRateRange range60toInf = new RefreshRateRange(60, inf);
+        RefreshRateRange range90toInf = new RefreshRateRange(90, inf);
+
+        RefreshRateRanges frameRateAll = new RefreshRateRanges(rangeAll, rangeAll);
+        RefreshRateRanges frameRate90toInf = new RefreshRateRanges(range90toInf, range90toInf);
+        RefreshRateRanges frameRate0to60;
+        RefreshRateRanges frameRate0to90;
+        RefreshRateRanges frameRate0to120;
+        RefreshRateRanges frameRate60to90;
+        RefreshRateRanges frameRate90to90;
+        RefreshRateRanges frameRate90to120;
+        if (frameRateIsRefreshRate) {
+            frameRate0to60 = new RefreshRateRanges(range0to60, range0to60);
+            frameRate0to90 = new RefreshRateRanges(range0to90, range0to90);
+            frameRate0to120 = new RefreshRateRanges(range0to120, range0to120);
+            frameRate60to90 = new RefreshRateRanges(range60to90, range60to90);
+            frameRate90to90 = new RefreshRateRanges(range90to90, range90to90);
+            frameRate90to120 = new RefreshRateRanges(range90to120, range90to120);
+        } else {
+            frameRate0to60 = new RefreshRateRanges(rangeAll, range0to60);
+            frameRate0to90 = new RefreshRateRanges(rangeAll, range0to90);
+            frameRate0to120 = new RefreshRateRanges(rangeAll, range0to120);
+            frameRate60to90 = new RefreshRateRanges(range60toInf, range60to90);
+            frameRate90to90 = new RefreshRateRanges(range90toInf, range90to90);
+            frameRate90to120 = new RefreshRateRanges(range90toInf, range90to120);
+        }
+
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 0, frameRateAll, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 90, frameRate0to90, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 0, frameRate0to90, frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 60, frameRate0to60, frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 120, frameRate0to90,
+                frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 0, frameRate90toInf, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 120, frameRate90to120,
+                frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 60, frameRate90toInf, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 120, 0, frameRate90to120,
+                frameRate0to120);
+        verifySpecsWithRefreshRateSettings(director, 90, 60, 0, frameRate90to90,
+                frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 60, 120, 90, frameRate60to90,
+                frameRate0to120);
     }
 
     void verifyBrightnessObserverCall(DisplayModeDirector director, float minFps, float peakFps,
@@ -523,7 +652,12 @@
     }
 
     @Test
-    public void testBrightnessObserverCallWithRefreshRateSettings() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testBrightnessObserverCallWithRefreshRateSettings(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // Confirm that, with varying settings for min, peak, and default refresh rate, we make the
         // correct call to the brightness observer.
         float[] refreshRates = {60.f, 90.f, 120.f};
@@ -538,7 +672,12 @@
     }
 
     @Test
-    public void testVotingWithAlwaysRespectAppRequest() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithAlwaysRespectAppRequest(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/50, /*width=*/1000, /*height=*/1000, 50);
@@ -549,61 +688,94 @@
 
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
 
-
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(0, 60));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE, Vote.forRefreshRates(60, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 90));
         Display.Mode appRequestedMode = modes[2];
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
-        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(60, 60));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
 
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isFalse();
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
 
         director.setShouldAlwaysRespectAppRequestedMode(true);
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isTrue();
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(50);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(50);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.render.min).isAtMost(50);
+        assertThat(desiredSpecs.primary.render.max).isAtLeast(90);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
 
         director.setShouldAlwaysRespectAppRequestedMode(false);
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isFalse();
 
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
     }
 
     @Test
-    public void testVotingWithSwitchingTypeNone() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithSwitchingTypeNone(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(0, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE, Vote.forRefreshRates(30, 90));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
-
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(30, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
 
         director.injectVotesByDisplay(votesByDisplay);
         assertThat(director.getModeSwitchingType())
                 .isNotEqualTo(DisplayManager.SWITCHING_TYPE_NONE);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(
+                    60);
+        } else {
+            assertThat(desiredSpecs.appRequest.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(30);
 
         director.setModeSwitchingType(DisplayManager.SWITCHING_TYPE_NONE);
@@ -611,10 +783,79 @@
                 .isEqualTo(DisplayManager.SWITCHING_TYPE_NONE);
 
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(30);
+    }
+
+    @Test
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithSwitchingTypeRenderFrameRateOnly(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
+        DisplayModeDirector director = createDirectorFromFpsRange(0, 90);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(30, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
+
+        director.injectVotesByDisplay(votesByDisplay);
+        assertThat(director.getModeSwitchingType())
+                .isNotEqualTo(DisplayManager.SWITCHING_TYPE_NONE);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(
+                    60);
+        } else {
+            assertThat(desiredSpecs.appRequest.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(30);
+
+        director.setModeSwitchingType(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+        assertThat(director.getModeSwitchingType())
+                .isEqualTo(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(30);
+        } else {
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(30);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+
         assertThat(desiredSpecs.baseModeId).isEqualTo(30);
     }
 
@@ -641,7 +882,12 @@
     }
 
     @Test
-    public void testDefaultDisplayModeIsSelectedIfAvailable() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDefaultDisplayModeIsSelectedIfAvailable(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         final float[] refreshRates = new float[]{24f, 25f, 30f, 60f, 90f};
         final int defaultModeId = 3;
         DisplayModeDirector director = createDirectorFromRefreshRateArray(
@@ -778,7 +1024,7 @@
         sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 20 /*lux*/));
 
         Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
-        assertVoteForRefreshRate(vote, 90 /*fps*/);
+        assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
         assertThat(vote.disableRefreshRateSwitching).isTrue();
@@ -847,7 +1093,7 @@
         sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 9000));
 
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
-        assertVoteForRefreshRate(vote, 60 /*fps*/);
+        assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
         assertThat(vote.disableRefreshRateSwitching).isTrue();
@@ -900,10 +1146,10 @@
     public void testUdfpsListenerGetsRegistered() {
         DisplayModeDirector director =
                 createDirectorFromRefreshRateArray(new float[] {60.f, 90.f, 110.f}, 0);
-        verify(mStatusBarMock, never()).setUdfpsHbmListener(any());
+        verify(mStatusBarMock, never()).setUdfpsRefreshRateCallback(any());
 
         director.onBootCompleted();
-        verify(mStatusBarMock).setUdfpsHbmListener(eq(director.getUdpfsObserver()));
+        verify(mStatusBarMock).setUdfpsRefreshRateCallback(eq(director.getUdpfsObserver()));
     }
 
     @Test
@@ -912,10 +1158,9 @@
                 createDirectorFromRefreshRateArray(new float[] {60.f, 90.f, 110.f}, 0);
         director.start(createMockSensorManager());
         director.onBootCompleted();
-        ArgumentCaptor<IUdfpsHbmListener> captor =
-                ArgumentCaptor.forClass(IUdfpsHbmListener.class);
-        verify(mStatusBarMock).setUdfpsHbmListener(captor.capture());
-        IUdfpsHbmListener hbmListener = captor.getValue();
+        ArgumentCaptor<IUdfpsRefreshRateRequestCallback> captor =
+                ArgumentCaptor.forClass(IUdfpsRefreshRateRequestCallback.class);
+        verify(mStatusBarMock).setUdfpsRefreshRateCallback(captor.capture());
 
         // Should be no vote initially
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
@@ -923,12 +1168,17 @@
     }
 
     @Test
-    public void testAppRequestMinRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestMinRefreshRate(boolean frameRateIsRefreshRate) {
         // Confirm that the app min request range doesn't include flicker or min refresh rate
         // settings but does include everything else.
-        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE
+        assertTrue(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
@@ -942,38 +1192,42 @@
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE,
-                Vote.forRefreshRates(75, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(75, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
     }
 
     @Test
-    public void testAppRequestMaxRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestMaxRefreshRate(boolean frameRateIsRefreshRate) {
         // Confirm that the app max request range doesn't include flicker or min refresh rate
         // settings but does include everything else.
-        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE
+        assertTrue(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
         Display.Mode[] modes = new Display.Mode[3];
@@ -984,63 +1238,104 @@
         modes[2] = new Display.Mode(
                 /*modeId=*/90, /*width=*/1000, /*height=*/1000, 90);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[1]);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.render.min).isZero();
+        }
+        assertThat(desiredSpecs.primary.render.max).isAtMost(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isAtMost(60f);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.render.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.render.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 75));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 75));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isZero();
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(75);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(75);
+        } else {
+            assertThat(desiredSpecs.primary.render.min).isZero();
+        }
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(
+                    75);
+        } else {
+            assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        }
+        assertThat(desiredSpecs.appRequest.render.min).isZero();
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(75);
     }
 
     @Test
-    public void testAppRequestObserver_modeId() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_modeId(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestSize.baseModeRefreshRate).isZero();
+        assertThat(appRequestSize.appRequestBaseModeRefreshRate).isZero();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
 
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0);
@@ -1048,27 +1343,37 @@
         appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
     }
 
     @Test
-    public void testAppRequestObserver_minRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_minRefreshRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60, 0);
         Vote appRequestRefreshRate =
@@ -1079,11 +1384,20 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min)
+                    .isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max).isAtLeast(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
 
@@ -1096,17 +1410,32 @@
         assertNull(appRequestSize);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isWithin(
+                    FLOAT_TOLERANCE).of(90);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max).isAtLeast(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
-    public void testAppRequestObserver_maxRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_maxRefreshRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 90);
         Vote appRequestRefreshRate =
@@ -1117,10 +1446,19 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
@@ -1134,10 +1472,19 @@
         assertNull(appRequestSize);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
@@ -1155,46 +1502,71 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
     }
 
     @Test
-    public void testAppRequestObserver_modeIdAndRefreshRateRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_modeIdAndRefreshRateRange(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         Vote appRequestSize =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
-    public void testAppRequestsIsTheDefaultMode() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestsIsTheDefaultMode(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[2];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -1204,8 +1576,8 @@
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(1);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(60);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
 
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
@@ -1214,105 +1586,148 @@
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
-                        appRequestedMode.getPhysicalHeight()));
+                appRequestedMode.getPhysicalHeight()));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(60);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
     }
 
     @Test
-    public void testDisableRefreshRateSwitchingVote() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDisableRefreshRateSwitchingVote(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(50);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(50);
         assertThat(desiredSpecs.baseModeId).isEqualTo(50);
 
         votes.clear();
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
-                Vote.forRefreshRates(70, Float.POSITIVE_INFINITY));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+                Vote.forPhysicalRefreshRates(70, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(80, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 90));
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(80);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(80);
         assertThat(desiredSpecs.baseModeId).isEqualTo(80);
 
         votes.clear();
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+                Vote.forPhysicalRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(80, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 90));
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
     }
 
     @Test
-    public void testBaseModeIdInPrimaryRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testBaseModeIdInPrimaryRange(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(70));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+            assertThat(desiredSpecs.baseModeId).isEqualTo(70);
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 52));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 52));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(52);
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 58));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 58));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(58);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(58);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(58);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
     }
 
     @Test
-    public void testStaleAppVote() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testStaleAppVote(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -1358,9 +1773,124 @@
     }
 
     @Test
-    public void testProximitySensorVoting() throws Exception {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testRefreshRateIsSubsetOfFrameRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+
+
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(
+                    120);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(
+                    120);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(140, 140));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+    }
+
+    @Test
+    public void testRenderFrameRateIsAchievableByPhysicalRefreshRate() {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(false);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+
+
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(120, 120));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.appRequest.render.min).isZero();
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+    }
+
+    @Test
+    public void testRenderFrameRateIsDroppedIfLowerPriorityThenBaseModeRefreshRate() {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(false);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(120, 120));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(90));
+        votes.put(Vote.PRIORITY_PROXIMITY, Vote.forPhysicalRefreshRates(60, 120));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(90);
+    }
+
+    @Test
+    public void testProximitySensorVoting() {
         DisplayModeDirector director =
-                createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0);
+                createDirectorFromRefreshRateArray(new float[]{60.f, 90.f}, 0);
         director.start(createMockSensorManager());
 
         ArgumentCaptor<ProximityActiveListener> ProximityCaptor =
@@ -1389,7 +1919,7 @@
         // Set the proximity to active and verify that we added a vote.
         proximityListener.onProximityActive(true);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Set the display state to doze and verify that the vote is gone
         when(mInjector.isDozeState(any(Display.class))).thenReturn(true);
@@ -1401,7 +1931,7 @@
         when(mInjector.isDozeState(any(Display.class))).thenReturn(false);
         displayListener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Set the display state to doze and verify that the vote is gone
         when(mInjector.isDozeState(any(Display.class))).thenReturn(true);
@@ -1412,7 +1942,7 @@
         // Remove the display to cause the doze state to be removed
         displayListener.onDisplayRemoved(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Turn prox off and verify vote is gone.
         proximityListener.onProximityActive(false);
@@ -1456,7 +1986,7 @@
                     BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, hbmRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, hbmRefreshRate);
 
         // Turn on HBM, with brightness below the HBM range
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1483,7 +2013,7 @@
                     BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, hbmRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, hbmRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1579,7 +2109,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, initialRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, initialRefreshRate);
 
         // Change refresh rate vote value through DeviceConfig, ensure it takes precedence
         final int updatedRefreshRate = 90;
@@ -1589,7 +2119,7 @@
         assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight())
                 .isEqualTo(updatedRefreshRate);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, updatedRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, updatedRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1605,7 +2135,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, updatedRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, updatedRefreshRate);
 
         // Reset DeviceConfig refresh rate, ensure vote falls back to the initial value
         mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(0);
@@ -1613,7 +2143,7 @@
         waitForIdleSync();
         assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()).isEqualTo(0);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, initialRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, initialRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1693,7 +2223,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, 60.0f);
+        assertVoteForPhysicalRefreshRate(vote, 60.0f);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1741,7 +2271,7 @@
         if (Float.isNaN(rr)) {
             assertNull(vote);
         } else {
-            assertVoteForRefreshRate(vote, rr);
+            assertVoteForPhysicalRefreshRate(vote, rr);
         }
     }
 
@@ -1817,7 +2347,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Turn off HBM
         listener.onDisplayRemoved(DISPLAY_ID);
@@ -1845,7 +2375,7 @@
         // Set the skin temperature to critical and verify that we added a vote.
         listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_CRITICAL));
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertVoteForRefreshRateRange(vote, 0f, 60.f);
+        assertVoteForRenderFrameRateRange(vote, 0f, 60.f);
 
         // Set the skin temperature to severe and verify that the vote is gone.
         listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_SEVERE));
@@ -1871,18 +2401,18 @@
         return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
     }
 
-    private void assertVoteForRefreshRate(Vote vote, float refreshRate) {
+    private void assertVoteForPhysicalRefreshRate(Vote vote, float refreshRate) {
         assertThat(vote).isNotNull();
         final RefreshRateRange expectedRange = new RefreshRateRange(refreshRate, refreshRate);
-        assertThat(vote.refreshRateRange).isEqualTo(expectedRange);
+        assertThat(vote.refreshRateRanges.physical).isEqualTo(expectedRange);
     }
 
-    private void assertVoteForRefreshRateRange(
-            Vote vote, float refreshRateLow, float refreshRateHigh) {
+    private void assertVoteForRenderFrameRateRange(
+            Vote vote, float frameRateLow, float frameRateHigh) {
         assertThat(vote).isNotNull();
         final RefreshRateRange expectedRange =
-                new RefreshRateRange(refreshRateLow, refreshRateHigh);
-        assertThat(vote.refreshRateRange).isEqualTo(expectedRange);
+                new RefreshRateRange(frameRateLow, frameRateHigh);
+        assertThat(vote.refreshRateRanges.render).isEqualTo(expectedRange);
     }
 
     public static class FakeDeviceConfig extends FakeDeviceConfigInterface {
@@ -2083,6 +2613,11 @@
             return null;
         }
 
+        @Override
+        public boolean renderFrameRateIsPhysicalRefreshRate() {
+            return true;
+        }
+
         void notifyPeakRefreshRateChanged() {
             if (mPeakRefreshRateObserver != null) {
                 mPeakRefreshRateObserver.dispatchChange(false /*selfChange*/,
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 0b33c30..657bda6 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -369,6 +369,98 @@
     }
 
     @Test
+    public void testDevicesAreAddedToDeviceDisplayGroups() {
+        // Create the default internal display of the device.
+        LogicalDisplay defaultDisplay =
+                add(
+                        createDisplayDevice(
+                                Display.TYPE_INTERNAL,
+                                600,
+                                800,
+                                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+
+        // Create 3 virtual displays associated with a first virtual device.
+        int deviceId1 = 1;
+        TestDisplayDevice display1 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display1, deviceId1);
+        LogicalDisplay virtualDevice1Display1 = add(display1);
+
+        TestDisplayDevice display2 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display2, deviceId1);
+        LogicalDisplay virtualDevice1Display2 = add(display2);
+
+        TestDisplayDevice display3 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display3", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display3, deviceId1);
+        LogicalDisplay virtualDevice1Display3 = add(display3);
+
+        // Create another 3 virtual displays associated with a second virtual device.
+        int deviceId2 = 2;
+        TestDisplayDevice display4 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display4, deviceId2);
+        LogicalDisplay virtualDevice2Display1 = add(display4);
+
+        TestDisplayDevice display5 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display5, deviceId2);
+        LogicalDisplay virtualDevice2Display2 = add(display5);
+
+        // The final display is created with FLAG_OWN_DISPLAY_GROUP set.
+        TestDisplayDevice display6 =
+                createDisplayDevice(
+                        Display.TYPE_VIRTUAL,
+                        "virtualDevice2Display3",
+                        600,
+                        800,
+                        DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display6, deviceId2);
+        LogicalDisplay virtualDevice2Display3 = add(display6);
+
+        // Verify that the internal display is in the default display group.
+        assertEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(defaultDisplay)));
+
+        // Verify that all the displays for virtual device 1 are in the same (non-default) display
+        // group.
+        int virtualDevice1DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice1DisplayGroupId);
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display2)));
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display3)));
+
+        // The first 2 displays for virtual device 2 should be in the same non-default group.
+        int virtualDevice2DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice2DisplayGroupId);
+        assertEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display2)));
+        // virtualDevice2Display3 was created with FLAG_OWN_DISPLAY_GROUP and shouldn't be grouped
+        // with other displays of this device or be in the default display group.
+        assertNotEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+        assertNotEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+    }
+
+    @Test
     public void testDeviceShouldBeWoken() {
         assertTrue(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
                 DEVICE_STATE_CLOSED,
@@ -416,14 +508,22 @@
     /////////////////
 
     private TestDisplayDevice createDisplayDevice(int type, int width, int height, int flags) {
-        return createDisplayDevice(new TestUtils.TestDisplayAddress(), type, width, height, flags);
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), /*  uniqueId */ "", type, width, height, flags);
     }
 
     private TestDisplayDevice createDisplayDevice(
-            DisplayAddress address, int type, int width, int height, int flags) {
+            int type, String uniqueId, int width, int height, int flags) {
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), uniqueId, type, width, height, flags);
+    }
+
+    private TestDisplayDevice createDisplayDevice(
+            DisplayAddress address, String uniqueId, int type, int width, int height, int flags) {
         TestDisplayDevice device = new TestDisplayDevice();
         DisplayDeviceInfo displayDeviceInfo = device.getSourceInfo();
         displayDeviceInfo.type = type;
+        displayDeviceInfo.uniqueId = uniqueId;
         displayDeviceInfo.width = width;
         displayDeviceInfo.height = height;
         displayDeviceInfo.flags = flags;
diff --git a/services/tests/servicestests/src/com/android/server/display/TestUtils.java b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
index 0454587..a419b3f 100644
--- a/services/tests/servicestests/src/com/android/server/display/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
@@ -51,6 +51,12 @@
         }
     }
 
+    public static void setMaximumRange(Sensor sensor, float maximumRange) throws Exception {
+        Method setter = Sensor.class.getDeclaredMethod("setRange", Float.TYPE, Float.TYPE);
+        setter.setAccessible(true);
+        setter.invoke(sensor, maximumRange, 1);
+    }
+
     public static Sensor createSensor(int type, String strType) throws Exception {
         Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
         constr.setAccessible(true);
@@ -59,6 +65,16 @@
         return sensor;
     }
 
+    public static Sensor createSensor(int type, String strType, float maximumRange)
+            throws Exception {
+        Constructor<Sensor> constr = Sensor.class.getDeclaredConstructor();
+        constr.setAccessible(true);
+        Sensor sensor = constr.newInstance();
+        setSensorType(sensor, type, strType);
+        setMaximumRange(sensor, maximumRange);
+        return sensor;
+    }
+
     /**
      * Create a custom {@link DisplayAddress} to ensure we're not relying on any specific
      * display-address implementation in our code. Intentionally uses default object (reference)
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
index fabf535..d332b30 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
@@ -39,8 +39,6 @@
                 getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER));
         mBrightnessEvent.setPhysicalDisplayId("test");
         mBrightnessEvent.setLux(100.0f);
-        mBrightnessEvent.setFastAmbientLux(90.0f);
-        mBrightnessEvent.setSlowAmbientLux(85.0f);
         mBrightnessEvent.setPreThresholdLux(150.0f);
         mBrightnessEvent.setTime(System.currentTimeMillis());
         mBrightnessEvent.setInitialBrightness(25.0f);
@@ -50,6 +48,7 @@
         mBrightnessEvent.setRbcStrength(-1);
         mBrightnessEvent.setThermalMax(0.65f);
         mBrightnessEvent.setPowerFactor(0.2f);
+        mBrightnessEvent.setWasShortTermModelActive(true);
         mBrightnessEvent.setHbmMode(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
         mBrightnessEvent.setFlags(0);
         mBrightnessEvent.setAdjustmentFlags(0);
@@ -69,9 +68,9 @@
         String actualString = mBrightnessEvent.toString(false);
         String expectedString =
                 "BrightnessEvent: disp=1, physDisp=test, brt=0.6, initBrt=25.0, rcmdBrt=0.6,"
-                + " preBrt=NaN, lux=100.0, fastLux=90.0, slowLux=85.0, preLux=150.0, hbmMax=0.62,"
-                + " hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2, flags=, reason=doze"
-                + " [ low_pwr ], autoBrightness=true";
+                + " preBrt=NaN, lux=100.0, preLux=150.0, hbmMax=0.62, hbmMode=off, rbcStrength=-1,"
+                + " thrmMax=0.65, powerFactor=0.2, wasShortTermModelActive=true, flags=,"
+                + " reason=doze [ low_pwr ], autoBrightness=true";
         assertEquals(expectedString, actualString);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
new file mode 100644
index 0000000..303a370
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.dreams;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.service.dreams.IDreamService;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamControllerTest {
+    @Mock
+    private DreamController.Listener mListener;
+    @Mock
+    private Context mContext;
+    @Mock
+    private IBinder mIBinder;
+    @Mock
+    private IDreamService mIDreamService;
+
+    @Captor
+    private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor;
+    @Captor
+    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;
+
+    private final TestLooper mLooper = new TestLooper();
+    private final Handler mHandler = new Handler(mLooper.getLooper());
+
+    private DreamController mDreamController;
+
+    private Binder mToken;
+    private ComponentName mDreamName;
+    private ComponentName mOverlayName;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mIDreamService.asBinder()).thenReturn(mIBinder);
+        when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService);
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+
+        mToken = new Binder();
+        mDreamName = ComponentName.unflattenFromString("dream");
+        mOverlayName = ComponentName.unflattenFromString("dream_overlay");
+        mDreamController = new DreamController(mContext, mHandler, mListener);
+    }
+
+    @Test
+    public void startDream_attachOnServiceConnected() throws RemoteException {
+        // Call dream controller to start dreaming.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        // Mock service connected.
+        final ServiceConnection serviceConnection = captureServiceConnection();
+        serviceConnection.onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Verify that dream service is called to attach.
+        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any());
+    }
+
+    @Test
+    public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted()
+            throws RemoteException {
+        // Start first dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+        clearInvocations(mContext);
+
+        // Set up second dream.
+        final Binder newToken = new Binder();
+        final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream");
+        final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay");
+        final IDreamService newDreamService = mock(IDreamService.class);
+        final IBinder newBinder = mock(IBinder.class);
+        when(newDreamService.asBinder()).thenReturn(newBinder);
+        when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService);
+
+        // Start second dream.
+        mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(newDreamName, newBinder);
+        mLooper.dispatchAll();
+
+        // Mock second dream started.
+        verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/,
+                mRemoteCallbackCaptor.capture());
+        mRemoteCallbackCaptor.getValue().sendResult(null /*data*/);
+        mLooper.dispatchAll();
+
+        // Verify that the first dream is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    @Test
+    public void stopDream_detachFromService() throws RemoteException {
+        // Start dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Stop dream.
+        mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/);
+
+        // Verify that dream service is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    private ServiceConnection captureServiceConnection() {
+        verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
+                any());
+        return mServiceConnectionACaptor.getValue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index f9b8373..9672085 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -108,7 +108,7 @@
         }
 
         @Override
-        public boolean hasFsverity(String path) {
+        public boolean isFromTrustedProvider(String path, byte[] signature) {
             return mHasFsverityPaths.contains(path);
         }
 
@@ -291,6 +291,32 @@
     }
 
     @Test
+    public void construct_missingSignatureFile() throws Exception {
+        UpdatableFontDir dirForPreparation = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
+                mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
+        dirForPreparation.loadFontFileMap();
+        dirForPreparation.update(Arrays.asList(
+                newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE)));
+        assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
+
+        // Remove signature file next to the font file.
+        File fontDir = dirForPreparation.getPostScriptMap().get("foo");
+        File sigFile = new File(fontDir.getParentFile(), "font.fsv_sig");
+        assertThat(sigFile.exists()).isTrue();
+        sigFile.delete();
+
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
+                mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
+        dir.loadFontFileMap();
+        // The font file should be removed and should not be loaded.
+        assertThat(dir.getPostScriptMap()).isEmpty();
+        assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
+        assertThat(dir.getFontFamilyMap()).isEmpty();
+    }
+
+    @Test
     public void construct_olderThanPreinstalledFont() throws Exception {
         Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
             FontConfig.Font fooFont = new FontConfig.Font(
@@ -782,8 +808,8 @@
         UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() {
 
             @Override
-            public boolean hasFsverity(String path) {
-                return mFakeFsverityUtil.hasFsverity(path);
+            public boolean isFromTrustedProvider(String path, byte[] signature) {
+                return mFakeFsverityUtil.isFromTrustedProvider(path, signature);
             }
 
             @Override
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 545f318..3a57db9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -19,7 +19,6 @@
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_TV;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -47,7 +46,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /** Tests for {@link DevicePowerStatusAction} */
@@ -65,7 +63,6 @@
     private FakePowerManagerWrapper mPowerManager;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
 
     private DevicePowerStatusAction mDevicePowerStatusAction;
@@ -79,7 +76,8 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -117,11 +115,8 @@
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
-        mPlaybackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        mPlaybackDevice.init();
-        mLocalDevices.add(mPlaybackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+        mPlaybackDevice = mHdmiControlService.playback();
         mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV,
                 mCallbackMock);
         mTestLooper.dispatchAll();
@@ -213,7 +208,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
         mTestLooper.dispatchAll();
 
@@ -238,7 +232,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder
                 .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
@@ -263,7 +256,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder
                 .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
@@ -293,6 +285,12 @@
 
     @Test
     public void pendingActionDoesNotBlockSendingStandby() throws Exception {
+        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(
+                mPlaybackDevice.getDeviceInfo().getLogicalAddress(),
+                mPhysicalAddress);
+        assertThat(mPlaybackDevice.handleActiveSource(message))
+                .isEqualTo(Constants.HANDLED);
+
         mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index eb7a761..7df0078 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -27,7 +27,6 @@
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE;
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_DEVICE_POWER_ON;
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_REPORT_POWER_STATUS;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -86,7 +85,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     private int mPlaybackLogicalAddress1;
     private int mPlaybackLogicalAddress2;
@@ -101,7 +99,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -119,8 +118,6 @@
                 };
 
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -135,16 +132,14 @@
                 mHdmiCecController, mHdmiMhlControllerStub);
         mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork);
 
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
-
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
         // The addresses depend on local device's LA.
         // This help the tests to pass with every local device LA.
         mPlaybackLogicalAddress1 =
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 72d36b0..ac57834 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -26,7 +26,6 @@
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_DEVICE_POWER_ON;
 import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_REPORT_POWER_STATUS;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -101,7 +100,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     @Before
     public void setUp() {
@@ -110,7 +108,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -127,8 +126,7 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
+
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -136,7 +134,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_PLAYBACK_1,
@@ -149,12 +146,12 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_1);
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_2);
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
     }
 
     private static class TestActionTimer implements ActionTimer {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
index 559a2c0..29eccd4 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
@@ -118,7 +118,13 @@
     }
 
     @Override
-    public void nativeSetOption(int flag, boolean enabled) {}
+    public void enableWakeupByOtp(boolean enabled) {}
+
+    @Override
+    public void enableCec(boolean enabled) {}
+
+    @Override
+    public void enableSystemCecControl(boolean enabled) {}
 
     @Override
     public void nativeSetLanguage(String language) {}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index 9f744f9..d2fe6da 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -19,7 +19,6 @@
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.Constants.PATH_RELATIONSHIP_ANCESTOR;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -35,6 +34,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.os.Binder;
@@ -55,7 +55,6 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /**
@@ -68,7 +67,6 @@
     private HdmiCecAtomWriter mHdmiCecAtomWriterSpy;
     private HdmiControlService mHdmiControlServiceSpy;
     private HdmiCecController mHdmiCecController;
-    private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback;
     private HdmiMhlControllerStub mHdmiMhlControllerStub;
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
@@ -77,7 +75,6 @@
     private Context mContextSpy;
     private TestLooper mTestLooper = new TestLooper();
     private int mPhysicalAddress = 0x1110;
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiPortInfo[] mHdmiPortInfo;
 
     @Before
@@ -89,7 +86,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -123,14 +121,9 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
-        mHdmiCecLocalDevicePlayback.init();
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
-
         mHdmiControlServiceSpy.initService();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 91d265c..08d0e90 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -48,7 +48,6 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
-import java.util.Collections;
 
 @SmallTest
 @Presubmit
@@ -80,15 +79,19 @@
     private HdmiDeviceInfo mDeviceInfo;
     private boolean mArcSupport;
     private HdmiPortInfo[] mHdmiPortInfo;
+    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
 
     @Before
     public void setUp() {
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
 
         mHdmiControlService =
             new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                    Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                    mLocalDeviceTypes,
+                    new FakeAudioDeviceVolumeManagerWrapper()) {
                 @Override
                 AudioManager getAudioManager() {
                     return new AudioManager() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index fe9e0b6..75c4d92 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -47,6 +47,7 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
@@ -78,7 +79,6 @@
     private TestLooper mTestLooper = new TestLooper();
     private FakePowerManagerWrapper mPowerManager;
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
-    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
     private int mPlaybackPhysicalAddress;
     private int mPlaybackLogicalAddress;
     private boolean mWokenUp;
@@ -91,10 +91,10 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
-        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
 
                     @Override
                     void wakeUp() {
@@ -128,8 +128,6 @@
                     }
                 };
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setIoLooper(mMyLooper);
         mNativeWrapper = new FakeNativeWrapper();
@@ -137,7 +135,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
@@ -148,10 +145,11 @@
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPlaybackPhysicalAddress = 0x2000;
         mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
+        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mPlaybackLogicalAddress = mHdmiCecLocalDevicePlayback.getDeviceInfo().getLogicalAddress();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         mNativeWrapper.clearResultMessages();
@@ -1108,7 +1106,11 @@
                 HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
                 HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
         mPowerManager.setInteractive(true);
-        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                mPlaybackPhysicalAddress);
+        assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message))
+                .isEqualTo(Constants.HANDLED);
+        message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
         assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message))
                 .isEqualTo(Constants.HANDLED);
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 8112ca8..82c3401 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -128,7 +128,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -165,8 +166,6 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -174,7 +173,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
@@ -185,11 +183,12 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTvPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
+        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         for (String sad : SADS_NOT_TO_QUERY) {
             mHdmiControlService.getHdmiCecConfig().setIntValue(
                     sad, HdmiControlManager.QUERY_SAD_DISABLED);
@@ -591,11 +590,15 @@
 
     @Test
     public void handleReportAudioStatus_SamOnArcOff_setStreamVolumeNotCalled() {
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+                HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
         // Emulate Audio device on port 0x1000 (does not support ARC)
         mNativeWrapper.setPortConnectionStatus(1, true);
         HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                 ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
         mNativeWrapper.onCecMessage(hdmiCecMessage);
+        mTestLooper.dispatchAll();
 
         HdmiCecFeatureAction systemAudioAutoInitiationAction =
                 new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
@@ -822,6 +825,10 @@
 
     @Test
     public void receiveSetAudioVolumeLevel_samActivated_respondsFeatureAbort_noVolumeChange() {
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+                HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode(
                 ADDR_AUDIO_SYSTEM, ADDR_TV, true));
         mTestLooper.dispatchAll();
@@ -842,4 +849,53 @@
         verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
                 anyInt());
     }
+
+    @Test
+    public void tvSendRequestArcTerminationOnSleep() {
+        // Emulate Audio device on port 0x2000 (supports ARC)
+
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true);
+        mTestLooper.dispatchAll();
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+        HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination);
+
+        mNativeWrapper.onCecMessage(initiateArc);
+        mTestLooper.dispatchAll();
+
+        // Finish querying SADs
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        // ARC should be established after RequestSadAction is finished
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        mTestLooper.dispatchAll();
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+    }
+
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index b94deed..a08e398 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -16,7 +16,6 @@
 package com.android.server.hdmi;
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,6 +24,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.os.Looper;
 import android.os.test.TestLooper;
@@ -40,7 +40,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 @SmallTest
@@ -57,7 +56,6 @@
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiControlService mHdmiControlService;
     private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback;
 
@@ -66,7 +64,8 @@
         Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(contextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             boolean isControlEnabled() {
@@ -90,9 +89,6 @@
         };
         mHdmiControlService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setIoLooper(myLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(contextSpy));
         mNativeWrapper = new FakeNativeWrapper();
@@ -100,7 +96,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
@@ -111,10 +106,9 @@
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.getHdmiCecNetwork().initPortInfo();
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
-
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
         mHdmiCecPowerStatusController = new HdmiCecPowerStatusController(mHdmiControlService);
         mNativeWrapper.clearResultMessages();
     }
@@ -254,7 +248,6 @@
     private void setCecVersion(int version) {
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, version);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 674e471..8f6bee1 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -39,6 +39,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
@@ -61,7 +62,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Optional;
 
 /**
@@ -84,14 +84,16 @@
     private TestLooper mTestLooper = new TestLooper();
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiPortInfo[] mHdmiPortInfo;
+    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
 
     @Before
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes,
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -228,8 +230,6 @@
 
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
         mNativeWrapper.clearResultMessages();
-
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         assertThat(mHdmiControlServiceSpy.getInitialPowerStatus()).isEqualTo(
@@ -461,7 +461,6 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_1_4_B);
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 1fa3871..9b8cedf 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -88,7 +88,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -142,11 +143,7 @@
     public void succeedWithUnknownTvDevice() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
@@ -191,11 +188,7 @@
     public void succeedAfterGettingPowerStatusOn_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -244,11 +237,7 @@
     public void succeedAfterGettingTransientPowerStatus_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -310,11 +299,7 @@
     public void timeOut_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -359,11 +344,7 @@
     @Test
     public void succeedIfPowerStatusOn_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -399,11 +380,8 @@
     @Test
     public void succeedIfPowerStatusUnknown_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -453,11 +431,7 @@
     @Test
     public void succeedIfPowerStatusStandby_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -510,11 +484,6 @@
 
         assertThat(mHdmiControlService.isAddressAllocated()).isFalse();
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-
         TestCallback callback = new TestCallback();
 
         mHdmiControlService.oneTouchPlay(callback);
@@ -524,9 +493,8 @@
         mNativeWrapper.clearResultMessages();
 
         setHdmiControlEnabled(true);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
-
         mTestLooper.dispatchAll();
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
 
         HdmiCecMessage reportPowerStatusMessage =
                 HdmiCecMessageBuilder.buildReportPowerStatus(
@@ -554,12 +522,7 @@
     public void succeedWithAddressAllocated_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
         assertThat(mHdmiControlService.isAddressAllocated()).isTrue();
 
@@ -632,11 +595,7 @@
     public void noWakeUpOnReportPowerStatus() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index e5058be..f72ac71 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -20,7 +20,6 @@
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -44,7 +43,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
@@ -60,7 +58,6 @@
     private FakePowerManagerWrapper mPowerManager;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
     private HdmiCecLocalDeviceTv mTvDevice;
 
@@ -101,9 +98,6 @@
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mTvDevice = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mTvDevice.init();
-        mLocalDevices.add(mTvDevice);
         mTestLooper.dispatchAll();
         HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[2];
         hdmiPortInfo[0] =
@@ -117,8 +111,8 @@
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+        mTvDevice = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index c2519caa..c07d4be 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -18,12 +18,12 @@
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -69,7 +69,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mTvLogicalAddress;
     private List<byte[]> mSupportedSads;
     private RequestSadCallback mCallback =
@@ -97,7 +96,7 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlService =
-                new HdmiControlService(context, Collections.emptyList(),
+                new HdmiControlService(context, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
                         new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
@@ -115,8 +114,6 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -124,14 +121,13 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
         mNativeWrapper.clearResultMessages();
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 566a7e0..f5bf30b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -25,7 +25,6 @@
 import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
 import static com.android.server.hdmi.Constants.MESSAGE_ACTIVE_SOURCE;
 import static com.android.server.hdmi.Constants.MESSAGE_ROUTING_INFORMATION;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTING_INFORMATION;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -134,7 +133,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     private static RoutingControlAction createRoutingControlAction(HdmiCecLocalDeviceTv localDevice,
             TestInputSelectCallback callback) {
@@ -150,7 +148,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -172,15 +171,12 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mNativeWrapper = new FakeNativeWrapper();
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_AVR,
@@ -190,9 +186,9 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index dadf815..e3c8939 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -21,7 +21,6 @@
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -67,7 +66,6 @@
     private Context mContextSpy;
     private TestLooper mTestLooper = new TestLooper();
     private int mPhysicalAddress = 0x1100;
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPlaybackLogicalAddress;
 
     private TestCallback mTestCallback;
@@ -82,7 +80,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -104,21 +103,16 @@
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
 
-        mPlaybackDevice = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
-        mPlaybackDevice.init();
-        mLocalDevices.add(mPlaybackDevice);
-
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
         mTestLooper.dispatchAll();
 
+        mPlaybackDevice = mHdmiControlServiceSpy.playback();
         mPlaybackLogicalAddress = mPlaybackDevice.getDeviceInfo().getLogicalAddress();
 
         // Setup specific to these tests
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                 Constants.ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV));
         mTestLooper.dispatchAll();
-
         mTestCallback = new TestCallback();
         mAction = new SetAudioVolumeLevelDiscoveryAction(mPlaybackDevice,
                 Constants.ADDR_TV, mTestCallback);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 1644252..e7557fe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -19,7 +19,6 @@
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -28,6 +27,7 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.media.AudioManager;
 import android.os.Looper;
@@ -42,7 +42,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /**
@@ -61,7 +60,6 @@
     private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
 
     @Before
@@ -70,7 +68,8 @@
 
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -94,15 +93,12 @@
             }
         };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(myLooper);
         mNativeWrapper = new FakeNativeWrapper();
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
@@ -113,10 +109,10 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mPhysicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
         mNativeWrapper.clearResultMessages();
     }
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 65076a3..6590a2b 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.ContextWrapper
 import android.hardware.BatteryState.STATUS_CHARGING
+import android.hardware.BatteryState.STATUS_DISCHARGING
 import android.hardware.BatteryState.STATUS_FULL
 import android.hardware.BatteryState.STATUS_UNKNOWN
 import android.hardware.input.IInputDeviceBatteryListener
@@ -32,8 +33,10 @@
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
 import androidx.test.InstrumentationRegistry
+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
@@ -42,6 +45,8 @@
 import org.hamcrest.core.IsEqual.equalTo
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
@@ -63,14 +68,20 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.verification.VerificationMode
 
-private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
+private fun createInputDevice(
+    deviceId: Int,
+    hasBattery: Boolean = true,
+    supportsUsi: Boolean = false,
+    generation: Int = -1,
+): InputDevice =
     InputDevice.Builder()
         .setId(deviceId)
         .setName("Device $deviceId")
         .setDescriptor("descriptor $deviceId")
         .setExternal(true)
         .setHasBattery(hasBattery)
-        .setGeneration(0)
+        .setSupportsUsi(supportsUsi)
+        .setGeneration(generation)
         .build()
 
 // Returns a matcher that helps match member variables of a class.
@@ -118,7 +129,10 @@
     return Matchers.allOf(batteryStateMatchers)
 }
 
-// Helper used to verify interactions with a mocked battery listener.
+private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> =
+    matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN)
+
+// Helpers used to verify interactions with a mocked battery listener.
 private fun IInputDeviceBatteryListener.verifyNotified(
     deviceId: Int,
     mode: VerificationMode = times(1),
@@ -127,8 +141,21 @@
     capacity: Float? = null,
     eventTime: Long? = null
 ) {
-    verify(this, mode).onBatteryStateChanged(
-        MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime)))
+    verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode)
+}
+
+private fun IInputDeviceBatteryListener.verifyNotified(
+    matcher: Matcher<IInputDeviceBatteryState>,
+    mode: VerificationMode = times(1)
+) {
+    verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher))
+}
+
+private fun createMockListener(): IInputDeviceBatteryListener {
+    val listener = mock(IInputDeviceBatteryListener::class.java)
+    val binder = mock(Binder::class.java)
+    `when`(listener.asBinder()).thenReturn(binder)
+    return listener
 }
 
 /**
@@ -143,6 +170,8 @@
         const val PID = 42
         const val DEVICE_ID = 13
         const val SECOND_DEVICE_ID = 11
+        const val USI_DEVICE_ID = 101
+        const val SECOND_USI_DEVICE_ID = 102
         const val TIMESTAMP = 123456789L
     }
 
@@ -168,10 +197,11 @@
         testLooper = TestLooper()
         val inputManager = InputManager.resetInstance(iInputManager)
         `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
-        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID))
-        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
-            .thenReturn(createInputDevice(SECOND_DEVICE_ID))
+        `when`(iInputManager.inputDeviceIds).then {
+            deviceGenerationMap.keys.toIntArray()
+        }
+        addInputDevice(DEVICE_ID)
+        addInputDevice(SECOND_DEVICE_ID)
 
         batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
         batteryController.systemRunning()
@@ -180,10 +210,30 @@
         devicesChangedListener = listenerCaptor.value
     }
 
-    private fun notifyDeviceChanged(deviceId: Int) {
-        deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
+    private fun notifyDeviceChanged(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        val generation = deviceGenerationMap[deviceId]?.plus(1)
+            ?: throw IllegalArgumentException("Device $deviceId was never added!")
+        deviceGenerationMap[deviceId] = generation
+
+        `when`(iInputManager.getInputDevice(deviceId))
+            .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation))
         val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
-        devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        if (::devicesChangedListener.isInitialized) {
+            devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        }
+    }
+
+    private fun addInputDevice(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        deviceGenerationMap[deviceId] = 0
+        notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
     }
 
     @After
@@ -191,13 +241,6 @@
         InputManager.clearInstance()
     }
 
-    private fun createMockListener(): IInputDeviceBatteryListener {
-        val listener = mock(IInputDeviceBatteryListener::class.java)
-        val binder = mock(Binder::class.java)
-        `when`(listener.asBinder()).thenReturn(binder)
-        return listener
-    }
-
     @Test
     fun testRegisterAndUnregisterBinderLifecycle() {
         val listener = createMockListener()
@@ -303,19 +346,14 @@
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)
 
         // If the battery presence for the InputDevice changes, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = false)
         testLooper.dispatchNext()
-        listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN,
-            capacity = Float.NaN)
+        listener.verifyNotified(isInvalidBatteryState(DEVICE_ID))
         // Since the battery is no longer present, the UEventListener should be removed.
         verify(uEventManager).removeListener(uEventListener.value)
 
         // If the battery becomes present again, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = true)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING,
             capacity = 0.78f)
@@ -340,9 +378,17 @@
 
         // Move the time forward so that the polling period has elapsed.
         // The listener should be notified.
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.80f)
+
+        // Move the time forward so that another polling period has elapsed.
+        // The battery should still be polled, but there is no change so listeners are not notified.
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
+        testLooper.dispatchNext()
+        listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f)
     }
 
     @Test
@@ -357,7 +403,8 @@
         // The battery state changed, but we should not be polling for battery changes when the
         // device is not interactive.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchAll()
         listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f)
 
@@ -368,7 +415,8 @@
 
         // Ensure that we continue to poll for battery changes.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.90f)
     }
@@ -398,4 +446,192 @@
             matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f))
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)
     }
+
+    @Test
+    fun testUsiDeviceIsMonitoredPersistently() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        // Even though there is no listener added for this device, it is being monitored.
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // Add and remove a listener for the device.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        // The device is still being monitored.
+        verify(uEventManager, never()).removeListener(uEventListener.value)
+    }
+
+    @Test
+    fun testNoPollingWhenUsiDevicesAreMonitored() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2")
+        addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+
+        // Add a listener.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+    }
+
+    @Test
+    fun testExpectedFlowForUsiBattery() {
+        `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"))
+
+        // A USI device's battery state is not valid until the first UEvent notification.
+        // Add a listener, and ensure it is notified that the battery state is not present.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
+
+        // Ensure that querying for battery state also returns the same invalid result.
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+
+        // There is a UEvent signaling a battery change. The battery state is now valid.
+        uEventListener.value!!.onBatteryUEvent(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))
+
+        // There is another UEvent notification. The battery state is now updated.
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64)
+        uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1)
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
+
+        // The battery state is still valid after a millisecond.
+        testLooper.moveTimeForward(1)
+        testLooper.dispatchAll()
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
+
+        // The battery is no longer present after the timeout expires.
+        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/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 9092ec3..0884b78 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -367,6 +367,7 @@
         assertFalse(item_en_us_allcaps.mIsSystemLocale);
     }
 
+    @SuppressWarnings("SelfComparison")
     @Test
     public void testImeSubtypeListComparator() throws Exception {
         final ComponentName imeX1 = new ComponentName("com.example.imeX", "Ime1");
diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
index d91a748..f5029ec 100644
--- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java
@@ -71,7 +71,7 @@
     private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp";
     private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity";
     private static final long POLL_INTERVAL = 500;
-    private static final long DEFAULT_WAIT_TIMEOUT = 5000;
+    private static final long DEFAULT_WAIT_TIMEOUT = 10_000;
 
     private Context mContext;
     private AppOpsManager mAppOpsManager;
@@ -115,7 +115,8 @@
         final IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(ACTION_JOB_STARTED);
         intentFilter.addAction(ACTION_JOB_STOPPED);
-        mContext.registerReceiver(mJobStateChangeReceiver, intentFilter);
+        mContext.registerReceiver(mJobStateChangeReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
         setAppOpsModeAllowed(true);
         setPowerExemption(false);
     }
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index f138311..dc47b5e 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -8,7 +8,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -20,8 +19,6 @@
 import android.content.pm.PackageManagerInternal;
 import android.net.NetworkRequest;
 import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.os.SystemClock;
 import android.test.RenamingDelegatingContext;
@@ -32,7 +29,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.util.HexDump;
 import com.android.server.LocalServices;
 import com.android.server.job.JobStore.JobSet;
 import com.android.server.job.controllers.JobStatus;
@@ -44,7 +40,6 @@
 
 import java.time.Clock;
 import java.time.ZoneOffset;
-import java.util.Arrays;
 import java.util.Iterator;
 
 /**
@@ -143,19 +138,23 @@
 
         assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
         final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0);
-        assertTasksEqual(task, loadedTaskStatus.getJob());
+        assertJobsEqual(ts, loadedTaskStatus);
         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts));
-        assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid());
-        assertEquals(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION,
-                loadedTaskStatus.getInternalFlags());
-        compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
-                ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
-        compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
-                ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
     }
 
     @Test
-    public void testWritingTwoFilesToDisk() throws Exception {
+    public void testWritingTwoJobsToDisk_singleFile() throws Exception {
+        mTaskStoreUnderTest.setUseSplitFiles(false);
+        runWritingTwoJobsToDisk();
+    }
+
+    @Test
+    public void testWritingTwoJobsToDisk_splitFiles() throws Exception {
+        mTaskStoreUnderTest.setUseSplitFiles(true);
+        runWritingTwoJobsToDisk();
+    }
+
+    private void runWritingTwoJobsToDisk() throws Exception {
         final JobInfo task1 = new Builder(8, mComponent)
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(10000L)
@@ -169,8 +168,10 @@
                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
                 .setPersisted(true)
                 .build();
-        final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, SOME_UID, null, -1, null);
-        final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, SOME_UID, null, -1, null);
+        final int uid1 = SOME_UID;
+        final int uid2 = uid1 + 1;
+        final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
+        final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
         mTaskStoreUnderTest.add(taskStatus1);
         mTaskStoreUnderTest.add(taskStatus2);
         waitForPendingIo();
@@ -189,19 +190,10 @@
             loaded2 = tmp;
         }
 
-        assertTasksEqual(task1, loaded1.getJob());
-        assertTasksEqual(task2, loaded2.getJob());
+        assertJobsEqual(taskStatus1, loaded1);
+        assertJobsEqual(taskStatus2, loaded2);
         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1));
         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2));
-        // Check that the loaded task has the correct runtimes.
-        compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
-                taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
-        compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
-                taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
-        compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
-                taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
-        compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
-                taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
     }
 
     @Test
@@ -227,7 +219,7 @@
         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
-        assertTasksEqual(task, loaded.getJob());
+        assertJobsEqual(taskStatus, loaded);
     }
 
     @Test
@@ -414,6 +406,35 @@
     }
 
     @Test
+    public void testEstimatedNetworkBytes() throws Exception {
+        assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+                .setPersisted(true)
+                .setRequiredNetwork(new NetworkRequest.Builder().build())
+                .setEstimatedNetworkBytes(
+                        JobInfo.NETWORK_BYTES_UNKNOWN, JobInfo.NETWORK_BYTES_UNKNOWN)
+                .build());
+        assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+                .setPersisted(true)
+                .setRequiredNetwork(new NetworkRequest.Builder().build())
+                .setEstimatedNetworkBytes(5, 15)
+                .build());
+    }
+
+    @Test
+    public void testMinimumNetworkChunkBytes() throws Exception {
+        assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+                .setPersisted(true)
+                .setRequiredNetwork(new NetworkRequest.Builder().build())
+                .setMinimumNetworkChunkBytes(JobInfo.NETWORK_BYTES_UNKNOWN)
+                .build());
+        assertPersistedEquals(new JobInfo.Builder(0, mComponent)
+                .setPersisted(true)
+                .setRequiredNetwork(new NetworkRequest.Builder().build())
+                .setMinimumNetworkChunkBytes(42)
+                .build());
+    }
+
+    @Test
     public void testPersistedIdleConstraint() throws Exception {
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresDeviceIdle(true)
@@ -502,62 +523,30 @@
         final JobSet jobStatusSet = new JobSet();
         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
         final JobStatus second = jobStatusSet.getAllJobs().iterator().next();
-        assertTasksEqual(first.getJob(), second.getJob());
+        assertJobsEqual(first, second);
     }
 
     /**
-     * Helper function to throw an error if the provided task and TaskStatus objects are not equal.
+     * Helper function to throw an error if the provided JobStatus objects are not equal.
      */
-    private void assertTasksEqual(JobInfo first, JobInfo second) {
-        assertEquals("Different task ids.", first.getId(), second.getId());
-        assertEquals("Different components.", first.getService(), second.getService());
-        assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic());
-        assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis());
-        assertEquals("Different inital backoff.", first.getInitialBackoffMillis(),
-                second.getInitialBackoffMillis());
-        assertEquals("Different backoff policy.", first.getBackoffPolicy(),
-                second.getBackoffPolicy());
+    private void assertJobsEqual(JobStatus expected, JobStatus actual) {
+        assertEquals(expected.getJob(), actual.getJob());
 
-        assertEquals("Invalid charging constraint.", first.isRequireCharging(),
-                second.isRequireCharging());
-        assertEquals("Invalid battery not low constraint.", first.isRequireBatteryNotLow(),
-                second.isRequireBatteryNotLow());
-        assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
-                second.isRequireDeviceIdle());
-        assertEquals("Invalid network type.",
-                first.getNetworkType(), second.getNetworkType());
-        assertEquals("Invalid network.",
-                first.getRequiredNetwork(), second.getRequiredNetwork());
-        assertEquals("Invalid deadline constraint.",
-                first.hasLateConstraint(),
-                second.hasLateConstraint());
-        assertEquals("Invalid delay constraint.",
-                first.hasEarlyConstraint(),
-                second.hasEarlyConstraint());
-        assertEquals("Extras don't match",
-                first.getExtras().toString(), second.getExtras().toString());
-        assertEquals("Transient xtras don't match",
-                first.getTransientExtras().toString(), second.getTransientExtras().toString());
+        // Source UID isn't persisted, but the rest of the app info is.
+        assertEquals("Source package not equal",
+                expected.getSourcePackageName(), actual.getSourcePackageName());
+        assertEquals("Source user not equal", expected.getSourceUserId(), actual.getSourceUserId());
+        assertEquals("Calling UID not equal", expected.getUid(), actual.getUid());
+        assertEquals("Calling user not equal", expected.getUserId(), actual.getUserId());
 
-        // Since people can forget to add tests here for new fields, do one last
-        // validity check based on bits-on-wire equality.
-        final byte[] firstBytes = marshall(first);
-        final byte[] secondBytes = marshall(second);
-        if (!Arrays.equals(firstBytes, secondBytes)) {
-            Log.w(TAG, "First: " + HexDump.dumpHexString(firstBytes));
-            Log.w(TAG, "Second: " + HexDump.dumpHexString(secondBytes));
-            fail("Raw JobInfo aren't equal; see logs for details");
-        }
-    }
+        assertEquals("Internal flags not equal",
+                expected.getInternalFlags(), actual.getInternalFlags());
 
-    private static byte[] marshall(Parcelable p) {
-        final Parcel parcel = Parcel.obtain();
-        try {
-            p.writeToParcel(parcel, 0);
-            return parcel.marshall();
-        } finally {
-            parcel.recycle();
-        }
+        // Check that the loaded task has the correct runtimes.
+        compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
+                expected.getEarliestRunTime(), actual.getEarliestRunTime());
+        compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
+                expected.getLatestRunTimeElapsed(), actual.getLatestRunTimeElapsed());
     }
 
     /**
@@ -572,5 +561,4 @@
     }
 
     private static class StubClass {}
-
 }
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/AppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/AppUpdateTrackerTest.java
new file mode 100644
index 0000000..2c5d97d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locales/AppUpdateTrackerTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.locales;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.LocaleList;
+import android.util.ArraySet;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link AppUpdateTracker}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppUpdateTrackerTest {
+    private static final String DEFAULT_PACKAGE_NAME = "com.android.myapp";
+    private static final int DEFAULT_UID = Binder.getCallingUid() + 100;
+    private static final int DEFAULT_USER_ID = 0;
+    private static final String DEFAULT_LOCALE_TAGS = "en-XC,ar-XB";
+    private static final LocaleList DEFAULT_LOCALES = LocaleList.forLanguageTags(
+            DEFAULT_LOCALE_TAGS);
+    private AppUpdateTracker mAppUpdateTracker;
+
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private LocaleManagerService mMockLocaleManagerService;
+    @Mock
+    private ShadowLocaleManagerBackupHelper mMockBackupHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockContext = mock(Context.class);
+        mMockLocaleManagerService = mock(LocaleManagerService.class);
+        mMockBackupHelper = mock(ShadowLocaleManagerBackupHelper.class);
+        mAppUpdateTracker = spy(
+                new AppUpdateTracker(mMockContext, mMockLocaleManagerService, mMockBackupHelper));
+    }
+
+    @Test
+    public void testPackageUpgraded_localeEmpty_doNothing() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+        setUpPackageLocaleConfig(null, DEFAULT_PACKAGE_NAME);
+        setUpAppLocalesOptIn(true);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verifyNoLocalesCleared();
+    }
+
+    @Test
+    public void testPackageUpgraded_pkgNotInSp_doNothing() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        String pkgNameA = "com.android.myAppA";
+        String pkgNameB = "com.android.myAppB";
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB)));
+        setUpPackageLocaleConfig(null, DEFAULT_PACKAGE_NAME);
+        setUpAppLocalesOptIn(true);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verifyNoLocalesCleared();
+    }
+
+    @Test
+    public void testPackageUpgraded_appLocalesSupported_doNothing() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+        setUpPackageLocaleConfig(DEFAULT_LOCALES, DEFAULT_PACKAGE_NAME);
+
+        setUpAppLocalesOptIn(true);
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verifyNoLocalesCleared();
+
+        setUpAppLocalesOptIn(false);
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verifyNoLocalesCleared();
+
+        setUpAppLocalesOptIn(false);
+        setUpPackageLocaleConfig(null, DEFAULT_PACKAGE_NAME);
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verifyNoLocalesCleared();
+    }
+
+    @Test
+    public void testPackageUpgraded_appLocalesNotSupported_clearAppLocale() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+        setUpPackageLocaleConfig(null, DEFAULT_PACKAGE_NAME);
+        setUpAppLocalesOptIn(true);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, LocaleList.forLanguageTags(""), false);
+
+        setUpPackageLocaleConfig(LocaleList.getEmptyLocaleList(), DEFAULT_PACKAGE_NAME);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verify(mMockLocaleManagerService, times(2)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, LocaleList.forLanguageTags(""), false);
+
+        setUpAppLocalesOptIn(false);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verify(mMockLocaleManagerService, times(3)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, LocaleList.forLanguageTags(""), false);
+    }
+
+    @Test
+    public void testPackageUpgraded_appLocalesNotInLocaleConfig_clearAppLocale() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+        setUpPackageLocaleConfig(LocaleList.forLanguageTags("hi,fr"), DEFAULT_PACKAGE_NAME);
+        setUpAppLocalesOptIn(true);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, LocaleList.forLanguageTags(""), false);
+
+        setUpAppLocalesOptIn(false);
+
+        mAppUpdateTracker.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        verify(mMockLocaleManagerService, times(2)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, LocaleList.forLanguageTags(""), false);
+    }
+
+    private void setUpLocalesForPackage(String packageName, LocaleList locales) throws Exception {
+        doReturn(locales).when(mMockLocaleManagerService).getApplicationLocales(eq(packageName),
+                anyInt());
+    }
+
+    private void setUpPackageNamesForSp(Set<String> packageNames) {
+        SharedPreferences mockSharedPreference = mock(SharedPreferences.class);
+        doReturn(mockSharedPreference).when(mMockBackupHelper).getPersistedInfo();
+        doReturn(packageNames).when(mockSharedPreference).getStringSet(anyString(), any());
+    }
+
+    private void setUpPackageLocaleConfig(LocaleList locales, String packageName) {
+        doReturn(locales).when(mAppUpdateTracker).getPackageLocales(eq(packageName), anyInt());
+    }
+
+    private void setUpAppLocalesOptIn(boolean optIn) {
+        doReturn(optIn).when(mAppUpdateTracker).isSettingsAppLocalesOptIn();
+    }
+
+    /**
+     * Verifies that no app locales needs to be cleared for any package.
+     *
+     * <p>If {@link LocaleManagerService#setApplicationLocales} is not invoked when receiving the
+     * callback of package upgraded, we can conclude that no app locales needs to be cleared.
+     */
+    private void verifyNoLocalesCleared() throws Exception {
+        verify(mMockLocaleManagerService, times(0)).setApplicationLocales(anyString(), anyInt(),
+                any(), anyBoolean());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index c735d18..4d42afa 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -18,9 +18,11 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -35,6 +37,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -44,15 +47,17 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SimpleClock;
+import android.util.ArraySet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.locales.LocaleManagerBackupHelper.LocalesInfo;
 
 import org.junit.After;
 import org.junit.Before;
@@ -69,9 +74,12 @@
 import java.time.Clock;
 import java.time.Duration;
 import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Unit tests for the {@link LocaleManagerInternal}.
@@ -89,8 +97,8 @@
     private static final Duration RETENTION_PERIOD = Duration.ofDays(3);
     private static final LocaleList DEFAULT_LOCALES =
             LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS);
-    private static final Map<String, String> DEFAULT_PACKAGE_LOCALES_MAP = Map.of(
-            DEFAULT_PACKAGE_NAME, DEFAULT_LOCALE_TAGS);
+    private static final Map<String, LocalesInfo> DEFAULT_PACKAGE_LOCALES_INFO_MAP = Map.of(
+            DEFAULT_PACKAGE_NAME, new LocalesInfo(DEFAULT_LOCALE_TAGS, false));
     private static final SparseArray<LocaleManagerBackupHelper.StagedData> STAGE_DATA =
             new SparseArray<>();
 
@@ -103,6 +111,10 @@
     private PackageManager mMockPackageManager;
     @Mock
     private LocaleManagerService mMockLocaleManagerService;
+    @Mock
+    private SharedPreferences mMockDelegateAppLocalePackages;
+    @Mock
+    private SharedPreferences.Editor mMockSpEditor;
 
     BroadcastReceiver mUserMonitor;
     PackageMonitor mPackageMonitor;
@@ -127,9 +139,13 @@
         mMockContext = mock(Context.class);
         mMockPackageManager = mock(PackageManager.class);
         mMockLocaleManagerService = mock(LocaleManagerService.class);
+        mMockDelegateAppLocalePackages = mock(SharedPreferences.class);
+        mMockSpEditor = mock(SharedPreferences.Editor.class);
         SystemAppUpdateTracker systemAppUpdateTracker = mock(SystemAppUpdateTracker.class);
+        AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
 
         doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
+        doReturn(mMockSpEditor).when(mMockDelegateAppLocalePackages).edit();
 
         HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
                 Process.THREAD_PRIORITY_BACKGROUND);
@@ -137,12 +153,12 @@
 
         mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mMockContext,
                 mMockLocaleManagerService, mMockPackageManager, mClock, STAGE_DATA,
-                broadcastHandlerThread));
+                broadcastHandlerThread, mMockDelegateAppLocalePackages));
         doNothing().when(mBackupHelper).notifyBackupManager();
 
         mUserMonitor = mBackupHelper.getUserMonitor();
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-            systemAppUpdateTracker);
+            systemAppUpdateTracker, appUpdateTracker);
         setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS);
     }
 
@@ -171,9 +187,23 @@
     public void testBackupPayload_appLocalesSet_returnsNonNullBlob() throws Exception {
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
         setUpDummyAppForPackageManager(DEFAULT_PACKAGE_NAME);
+        setUpPackageNamesForSp(Collections.<String>emptySet());
 
         byte[] payload = mBackupHelper.getBackupPayload(DEFAULT_USER_ID);
-        verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_MAP, payload);
+        verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_INFO_MAP, payload);
+    }
+
+    @Test
+    public void testBackupPayload_appLocalesSet_fromDelegateSelector() throws Exception {
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        setUpDummyAppForPackageManager(DEFAULT_PACKAGE_NAME);
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+        Map<String, LocalesInfo> expectPackageLocalePack = Map.of(DEFAULT_PACKAGE_NAME,
+                new LocalesInfo(DEFAULT_LOCALE_TAGS, true));
+
+        byte[] payload = mBackupHelper.getBackupPayload(DEFAULT_USER_ID);
+
+        verifyPayloadForAppLocales(expectPackageLocalePack, payload);
     }
 
     @Test
@@ -195,14 +225,15 @@
         anotherAppInfo.packageName = "com.android.anotherapp";
         doReturn(List.of(defaultAppInfo, anotherAppInfo)).when(mMockPackageManager)
                 .getInstalledApplicationsAsUser(any(), anyInt());
-
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
+        setUpPackageNamesForSp(Collections.<String>emptySet());
         // Exception when getting locales for anotherApp.
         doThrow(new RemoteException("mock")).when(mMockLocaleManagerService).getApplicationLocales(
                 eq(anotherAppInfo.packageName), anyInt());
 
         byte[] payload = mBackupHelper.getBackupPayload(DEFAULT_USER_ID);
-        verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_MAP, payload);
+
+        verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_INFO_MAP, payload);
     }
 
     @Test
@@ -226,8 +257,7 @@
     @Test
     public void testRestore_allAppsInstalled_noStageDataCreated() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
 
@@ -235,38 +265,104 @@
 
         // Locales were restored
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
-                DEFAULT_USER_ID, DEFAULT_LOCALES);
-
+                DEFAULT_USER_ID, DEFAULT_LOCALES, false);
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
     }
 
     @Test
+    public void testRestore_allAppsInstalled_nothingToSp() throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
+        setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>());
+
+        mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+        // Locales were restored
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, DEFAULT_LOCALES, false);
+        checkStageDataDoesNotExist(DEFAULT_USER_ID);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, false,
+                false);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+    }
+
+    @Test
+    public void testRestore_allAppsInstalled_storeInfoToSp() throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Map<String, LocalesInfo> pkgLocalesMap = Map.of(DEFAULT_PACKAGE_NAME,
+                new LocalesInfo(DEFAULT_LOCALE_TAGS, true));
+        writeTestPayload(out, pkgLocalesMap);
+        setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>());
+
+        mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+        // Locales were restored
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID, DEFAULT_LOCALES, true);
+        checkStageDataDoesNotExist(DEFAULT_USER_ID);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true,
+                false);
+
+        verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+                new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+    }
+
+    @Test
+    public void testRestore_allAppsInstalled_InfoHasExistedInSp() throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        Map<String, LocalesInfo> pkgLocalesMap = Map.of(DEFAULT_PACKAGE_NAME,
+                new LocalesInfo(DEFAULT_LOCALE_TAGS, true));
+        writeTestPayload(out, pkgLocalesMap);
+        setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(DEFAULT_PACKAGE_NAME)));
+
+        mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+        // Locales were restored
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, DEFAULT_LOCALES, true);
+        checkStageDataDoesNotExist(DEFAULT_USER_ID);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true,
+                false);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+    }
+
+    @Test
     public void testRestore_noAppsInstalled_everythingStaged() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         verifyNothingRestored();
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
     }
 
     @Test
     public void testRestore_someAppsInstalled_partiallyStaged() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        HashMap<String, String> pkgLocalesMap = new HashMap<>();
-
+        HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
         String pkgNameA = "com.android.myAppA";
         String pkgNameB = "com.android.myAppB";
         String langTagsA = "ru";
         String langTagsB = "hi,fr";
-        pkgLocalesMap.put(pkgNameA, langTagsA);
-        pkgLocalesMap.put(pkgNameB, langTagsB);
+        LocalesInfo localesInfoA = new LocalesInfo(langTagsA, true);
+        LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+        pkgLocalesMap.put(pkgNameA, localesInfoA);
+        pkgLocalesMap.put(pkgNameB, localesInfoB);
         writeTestPayload(out, pkgLocalesMap);
-
         setUpPackageInstalled(pkgNameA);
         setUpPackageNotInstalled(pkgNameB);
         setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
@@ -274,9 +370,10 @@
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
-                LocaleList.forLanguageTags(langTagsA));
+                LocaleList.forLanguageTags(langTagsA), true);
 
         pkgLocalesMap.remove(pkgNameA);
+
         verifyStageDataForUser(pkgLocalesMap,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
     }
@@ -284,8 +381,7 @@
     @Test
     public void testRestore_appLocalesAlreadySet_nothingRestoredAndNoStageData() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.forLanguageTags("hi,mr"));
 
@@ -300,19 +396,20 @@
     public void testRestore_appLocalesSetForSomeApps_restoresOnlyForAppsHavingNoLocalesSet()
             throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        HashMap<String, String> pkgLocalesMap = new HashMap<>();
-
+        HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
         String pkgNameA = "com.android.myAppA";
         String pkgNameB = "com.android.myAppB";
         String pkgNameC = "com.android.myAppC";
         String langTagsA = "ru";
         String langTagsB = "hi,fr";
         String langTagsC = "zh,es";
-        pkgLocalesMap.put(pkgNameA, langTagsA);
-        pkgLocalesMap.put(pkgNameB, langTagsB);
-        pkgLocalesMap.put(pkgNameC, langTagsC);
+        LocalesInfo localesInfoA = new LocalesInfo(langTagsA, true);
+        LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+        LocalesInfo localesInfoC = new LocalesInfo(langTagsC, true);
+        pkgLocalesMap.put(pkgNameA, localesInfoA);
+        pkgLocalesMap.put(pkgNameB, localesInfoB);
+        pkgLocalesMap.put(pkgNameC, localesInfoC);
         writeTestPayload(out, pkgLocalesMap);
-
         // Both app A & B are installed on the device but A has locales already set.
         setUpPackageInstalled(pkgNameA);
         setUpPackageInstalled(pkgNameB);
@@ -320,20 +417,22 @@
         setUpLocalesForPackage(pkgNameA, LocaleList.forLanguageTags("mr,fr"));
         setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
         setUpLocalesForPackage(pkgNameC, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB, pkgNameC)));
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         // Restore locales only for myAppB.
         verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameA), anyInt(),
-                any());
+                any(), anyBoolean());
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
-                LocaleList.forLanguageTags(langTagsB));
+                LocaleList.forLanguageTags(langTagsB), true);
         verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameC), anyInt(),
-                any());
+                any(), anyBoolean());
 
         // App C is staged.
         pkgLocalesMap.remove(pkgNameA);
         pkgLocalesMap.remove(pkgNameB);
+
         verifyStageDataForUser(pkgLocalesMap,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
     }
@@ -341,40 +440,41 @@
     @Test
     public void testRestore_restoreInvokedAgain_creationTimeChanged() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
 
         final long newCreationTime = DEFAULT_CREATION_TIME_MILLIS + 100;
         setCurrentTimeMillis(newCreationTime);
+
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 newCreationTime, DEFAULT_USER_ID);
     }
 
     @Test
     public void testRestore_appInstalledAfterSUW_restoresFromStage() throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        HashMap<String, String> pkgLocalesMap = new HashMap<>();
-
+        HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
         String pkgNameA = "com.android.myAppA";
         String pkgNameB = "com.android.myAppB";
         String langTagsA = "ru";
         String langTagsB = "hi,fr";
-        pkgLocalesMap.put(pkgNameA, langTagsA);
-        pkgLocalesMap.put(pkgNameB, langTagsB);
+        LocalesInfo localesInfoA = new LocalesInfo(langTagsA, false);
+        LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+        pkgLocalesMap.put(pkgNameA, localesInfoA);
+        pkgLocalesMap.put(pkgNameB, localesInfoB);
         writeTestPayload(out, pkgLocalesMap);
-
         setUpPackageNotInstalled(pkgNameA);
         setUpPackageNotInstalled(pkgNameB);
         setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
         setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>());
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
@@ -385,9 +485,14 @@
         mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
-                LocaleList.forLanguageTags(langTagsA));
+                LocaleList.forLanguageTags(langTagsA), false);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
 
         pkgLocalesMap.remove(pkgNameA);
+
         verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
 
         setUpPackageInstalled(pkgNameB);
@@ -395,7 +500,12 @@
         mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
-                LocaleList.forLanguageTags(langTagsB));
+                LocaleList.forLanguageTags(langTagsB), true);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false);
+
+        verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+                new ArraySet<>(Arrays.asList(pkgNameB)));
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
     }
 
@@ -403,15 +513,14 @@
     public void testRestore_appInstalledAfterSUWAndLocalesAlreadySet_restoresNothing()
             throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         // Package is not present on the device when the SUW restore is going on.
         setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         verifyNothingRestored();
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
 
         // App is installed later (post SUW).
@@ -429,14 +538,13 @@
     public void testStageDataDeletion_backupPassRunAfterRetentionPeriod_stageDataDeleted()
             throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
 
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         verifyNothingRestored();
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
 
         // Retention period has not elapsed.
@@ -444,8 +552,8 @@
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
         doReturn(List.of()).when(mMockPackageManager)
                 .getInstalledApplicationsAsUser(any(), anyInt());
-        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
+        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
         checkStageDataExists(DEFAULT_USER_ID);
 
         // Exactly RETENTION_PERIOD amount of time has passed so stage data should still not be
@@ -453,8 +561,8 @@
         setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.toMillis());
         doReturn(List.of()).when(mMockPackageManager)
                 .getInstalledApplicationsAsUser(any(), anyInt());
-        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
+        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
         checkStageDataExists(DEFAULT_USER_ID);
 
         // Retention period has now expired, stage data should be deleted.
@@ -462,8 +570,8 @@
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
         doReturn(List.of()).when(mMockPackageManager)
                 .getInstalledApplicationsAsUser(any(), anyInt());
-        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
+        assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
     }
 
@@ -471,16 +579,16 @@
     public void testStageDataDeletion_lazyRestoreAfterRetentionPeriod_stageDataDeleted()
             throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        HashMap<String, String> pkgLocalesMap = new HashMap<>();
-
+        HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
         String pkgNameA = "com.android.myAppA";
         String pkgNameB = "com.android.myAppB";
         String langTagsA = "ru";
         String langTagsB = "hi,fr";
-        pkgLocalesMap.put(pkgNameA, langTagsA);
-        pkgLocalesMap.put(pkgNameB, langTagsB);
+        LocalesInfo localesInfoA = new LocalesInfo(langTagsA, false);
+        LocalesInfo localesInfoB = new LocalesInfo(langTagsB, false);
+        pkgLocalesMap.put(pkgNameA, localesInfoA);
+        pkgLocalesMap.put(pkgNameB, localesInfoB);
         writeTestPayload(out, pkgLocalesMap);
-
         setUpPackageNotInstalled(pkgNameA);
         setUpPackageNotInstalled(pkgNameB);
         setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
@@ -494,40 +602,40 @@
         // Retention period has not elapsed.
         setCurrentTimeMillis(
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
-
         setUpPackageInstalled(pkgNameA);
+
         mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
 
-        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
-                LocaleList.forLanguageTags(langTagsA));
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(
+                pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false);
 
         pkgLocalesMap.remove(pkgNameA);
+
         verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
 
         // Retention period has now expired, stage data should be deleted.
         setCurrentTimeMillis(
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
         setUpPackageInstalled(pkgNameB);
+
         mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
 
         verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(),
-                any());
-
+                any(), anyBoolean());
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
     }
 
     @Test
     public void testUserRemoval_userRemoved_stageDataDeleted() throws Exception {
         final ByteArrayOutputStream outDefault = new ByteArrayOutputStream();
-        writeTestPayload(outDefault, DEFAULT_PACKAGE_LOCALES_MAP);
-
+        writeTestPayload(outDefault, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
         final ByteArrayOutputStream outWorkProfile = new ByteArrayOutputStream();
         String anotherPackage = "com.android.anotherapp";
         String anotherLangTags = "mr,zh";
-        HashMap<String, String> pkgLocalesMapWorkProfile = new HashMap<>();
-        pkgLocalesMapWorkProfile.put(anotherPackage, anotherLangTags);
+        LocalesInfo localesInfo = new LocalesInfo(anotherLangTags, false);
+        HashMap<String, LocalesInfo> pkgLocalesMapWorkProfile = new HashMap<>();
+        pkgLocalesMapWorkProfile.put(anotherPackage, localesInfo);
         writeTestPayload(outWorkProfile, pkgLocalesMapWorkProfile);
-
         // DEFAULT_PACKAGE_NAME is NOT installed on the device.
         setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
         setUpPackageNotInstalled(anotherPackage);
@@ -537,8 +645,7 @@
                 WORK_PROFILE_USER_ID);
 
         verifyNothingRestored();
-
-        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
                 DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
         verifyStageDataForUser(pkgLocalesMapWorkProfile,
                 DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID);
@@ -554,6 +661,45 @@
                 DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID);
     }
 
+    @Test
+    public void testPackageRemoved_noInfoInSp() throws Exception {
+        String pkgNameA = "com.android.myAppA";
+        String pkgNameB = "com.android.myAppB";
+        setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB)));
+
+        mPackageMonitor.onPackageRemoved(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+    }
+
+    @Test
+    public void testPackageRemoved_removeInfoFromSp() throws Exception {
+        String pkgNameA = "com.android.myAppA";
+        String pkgNameB = "com.android.myAppB";
+        Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
+        setUpPackageNamesForSp(pkgNames);
+
+        mPackageMonitor.onPackageRemoved(pkgNameA, DEFAULT_UID);
+        pkgNames.remove(pkgNameA);
+
+        verify(mMockSpEditor, times(1)).putStringSet(
+                Integer.toString(DEFAULT_USER_ID), pkgNames);
+    }
+
+    @Test
+    public void testPackageDataCleared_removeInfoFromSp() throws Exception {
+        String pkgNameA = "com.android.myAppA";
+        String pkgNameB = "com.android.myAppB";
+        Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
+        setUpPackageNamesForSp(pkgNames);
+
+        mPackageMonitor.onPackageDataCleared(pkgNameB, DEFAULT_UID);
+        pkgNames.remove(pkgNameB);
+
+        verify(mMockSpEditor, times(1)).putStringSet(
+                Integer.toString(DEFAULT_USER_ID), pkgNames);
+    }
+
     private void setUpPackageInstalled(String packageName) throws Exception {
         doReturn(new PackageInfo()).when(mMockPackageManager).getPackageInfoAsUser(
                 eq(packageName), anyInt(), anyInt());
@@ -576,6 +722,11 @@
                 .getInstalledApplicationsAsUser(any(), anyInt());
     }
 
+    private void setUpPackageNamesForSp(Set<String> packageNames) {
+        doReturn(packageNames).when(mMockDelegateAppLocalePackages).getStringSet(anyString(),
+                any());
+    }
+
     /**
      * Verifies that nothing was restored for any package.
      *
@@ -584,31 +735,34 @@
      */
     private void verifyNothingRestored() throws Exception {
         verify(mMockLocaleManagerService, times(0)).setApplicationLocales(anyString(), anyInt(),
-                any());
+                any(), anyBoolean());
     }
 
-    private static void verifyPayloadForAppLocales(Map<String, String> expectedPkgLocalesMap,
+    private static void verifyPayloadForAppLocales(Map<String, LocalesInfo> expectedPkgLocalesMap,
             byte[] payload)
             throws IOException, XmlPullParserException {
         final ByteArrayInputStream stream = new ByteArrayInputStream(payload);
         final TypedXmlPullParser parser = Xml.newFastPullParser();
         parser.setInput(stream, StandardCharsets.UTF_8.name());
 
-        Map<String, String> backupDataMap = new HashMap<>();
+        Map<String, LocalesInfo> backupDataMap = new HashMap<>();
         XmlUtils.beginDocument(parser, TEST_LOCALES_XML_TAG);
         int depth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, depth)) {
             if (parser.getName().equals("package")) {
                 String packageName = parser.getAttributeValue(null, "name");
                 String languageTags = parser.getAttributeValue(null, "locales");
-                backupDataMap.put(packageName, languageTags);
+                boolean delegateSelector = parser.getAttributeBoolean(null, "delegate_selector");
+                LocalesInfo localesInfo = new LocalesInfo(languageTags, delegateSelector);
+                backupDataMap.put(packageName, localesInfo);
             }
         }
 
-        assertEquals(expectedPkgLocalesMap, backupDataMap);
+        verifyStageData(expectedPkgLocalesMap, backupDataMap);
     }
 
-    private static void writeTestPayload(OutputStream stream, Map<String, String> pkgLocalesMap)
+    private static void writeTestPayload(OutputStream stream,
+            Map<String, LocalesInfo> pkgLocalesMap)
             throws IOException {
         if (pkgLocalesMap.isEmpty()) {
             return;
@@ -622,7 +776,9 @@
         for (String pkg : pkgLocalesMap.keySet()) {
             out.startTag(/* namespace= */ null, "package");
             out.attribute(/* namespace= */ null, "name", pkg);
-            out.attribute(/* namespace= */ null, "locales", pkgLocalesMap.get(pkg));
+            out.attribute(/* namespace= */ null, "locales", pkgLocalesMap.get(pkg).mLocales);
+            out.attributeBoolean(/* namespace= */ null, "delegate_selector",
+                    pkgLocalesMap.get(pkg).mSetFromDelegate);
             out.endTag(/*namespace= */ null, "package");
         }
 
@@ -630,12 +786,23 @@
         out.endDocument();
     }
 
-    private void verifyStageDataForUser(Map<String, String> expectedPkgLocalesMap,
+    private void verifyStageDataForUser(Map<String, LocalesInfo> expectedPkgLocalesMap,
             long expectedCreationTimeMillis, int userId) {
         LocaleManagerBackupHelper.StagedData stagedDataForUser = STAGE_DATA.get(userId);
         assertNotNull(stagedDataForUser);
         assertEquals(expectedCreationTimeMillis, stagedDataForUser.mCreationTimeMillis);
-        assertEquals(expectedPkgLocalesMap, stagedDataForUser.mPackageStates);
+        verifyStageData(expectedPkgLocalesMap, stagedDataForUser.mPackageStates);
+    }
+
+    private static void verifyStageData(Map<String, LocalesInfo> expectedPkgLocalesMap,
+            Map<String, LocalesInfo> stageData) {
+        assertEquals(expectedPkgLocalesMap.size(), stageData.size());
+        for (String pkg : expectedPkgLocalesMap.keySet()) {
+            assertTrue(stageData.containsKey(pkg));
+            assertEquals(expectedPkgLocalesMap.get(pkg).mLocales, stageData.get(pkg).mLocales);
+            assertEquals(expectedPkgLocalesMap.get(pkg).mSetFromDelegate,
+                    stageData.get(pkg).mSetFromDelegate);
+        }
     }
 
     private static void checkStageDataExists(int userId) {
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 1dcdbac..79ed7d1 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.locales;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.fail;
@@ -35,13 +37,16 @@
 
 import android.Manifest;
 import android.app.ActivityManagerInternal;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.LocaleList;
+import android.provider.Settings;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.content.PackageMonitor;
@@ -111,6 +116,8 @@
         doReturn(DEFAULT_USER_ID).when(mMockActivityManager)
                 .handleIncomingUser(anyInt(), anyInt(), eq(DEFAULT_USER_ID), anyBoolean(), anyInt(),
                         anyString(), anyString());
+        doReturn(InstrumentationRegistry.getContext().getContentResolver())
+                .when(mMockContext).getContentResolver();
 
         mMockBackupHelper = mock(ShadowLocaleManagerBackupHelper.class);
         mLocaleManagerService = new LocaleManagerService(mMockContext, mMockActivityTaskManager,
@@ -126,7 +133,7 @@
 
         try {
             mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
-                    LocaleList.getEmptyLocaleList());
+                    LocaleList.getEmptyLocaleList(), false);
             fail("Expected SecurityException");
         } finally {
             verify(mMockContext).enforceCallingOrSelfPermission(
@@ -141,7 +148,7 @@
     public void testSetApplicationLocales_nullPackageName_fails() throws Exception {
         try {
             mLocaleManagerService.setApplicationLocales(/* appPackageName = */ null,
-                    DEFAULT_USER_ID, LocaleList.getEmptyLocaleList());
+                    DEFAULT_USER_ID, LocaleList.getEmptyLocaleList(), false);
             fail("Expected NullPointerException");
         } finally {
             verify(mMockBackupHelper, times(0)).notifyBackupManager();
@@ -155,7 +162,7 @@
 
         try {
             mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
-                    /* locales = */ null);
+                    /* locales = */ null, false);
             fail("Expected NullPointerException");
         } finally {
             verify(mMockBackupHelper, times(0)).notifyBackupManager();
@@ -173,7 +180,7 @@
         setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
 
         mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
-                DEFAULT_LOCALES);
+                DEFAULT_LOCALES, true);
 
         assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales());
         verify(mMockBackupHelper, times(1)).notifyBackupManager();
@@ -186,7 +193,7 @@
                 .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
 
         mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
-                DEFAULT_LOCALES);
+                DEFAULT_LOCALES, false);
 
         assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales());
         verify(mMockBackupHelper, times(1)).notifyBackupManager();
@@ -198,7 +205,7 @@
                 .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
         try {
             mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
-                    LocaleList.getEmptyLocaleList());
+                    LocaleList.getEmptyLocaleList(), false);
             fail("Expected IllegalArgumentException");
         } finally {
             assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales());
@@ -222,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 {
@@ -299,6 +329,26 @@
         assertEquals(DEFAULT_LOCALES, locales);
     }
 
+    @Test
+    public void testGetApplicationLocales_currentImeQueryForegroundAppLocales_returnsLocales()
+            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(true).when(mMockActivityManager).isAppForeground(anyInt());
+
+        LocaleList locales =
+                mLocaleManagerService.getApplicationLocales(
+                        DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        verify(mMockContext, never()).enforceCallingOrSelfPermission(any(), any());
+        assertEquals(DEFAULT_LOCALES, locales);
+    }
+
     private static void assertNoLocalesStored(LocaleList locales) {
         assertNull(locales);
     }
@@ -311,4 +361,13 @@
     private void setUpPassingPermissionCheckFor(String permission) {
         doNothing().when(mMockContext).enforceCallingOrSelfPermission(eq(permission), any());
     }
+
+    private String getCurrentInputMethodPackageName() {
+        String im = Settings.Secure.getString(
+                InstrumentationRegistry.getContext().getContentResolver(),
+                Settings.Secure.DEFAULT_INPUT_METHOD);
+        ComponentName cn = ComponentName.unflattenFromString(im);
+        assertThat(cn).isNotNull();
+        return cn.getPackageName();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
index e403c87..9f7cbe3 100644
--- a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
+++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
@@ -17,6 +17,7 @@
 package com.android.server.locales;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.os.HandlerThread;
 import android.util.SparseArray;
@@ -33,8 +34,8 @@
             LocaleManagerService localeManagerService,
             PackageManager packageManager, Clock clock,
             SparseArray<LocaleManagerBackupHelper.StagedData> stagedData,
-            HandlerThread broadcastHandlerThread) {
+            HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
         super(context, localeManagerService, packageManager, clock, stagedData,
-                broadcastHandlerThread);
+                broadcastHandlerThread, delegateAppLocalePackages);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index 808b74e..dc0740a 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -44,11 +44,13 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.AtomicFile;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import androidx.test.InstrumentationRegistry;
+
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import org.junit.After;
@@ -124,6 +126,8 @@
         doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mMockPackageManager)
                 .getInstallSourceInfo(anyString());
         doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
+        doReturn(InstrumentationRegistry.getContext().getContentResolver())
+                .when(mMockContext).getContentResolver();
 
         mStoragefile = new AtomicFile(new File(
                 Environment.getExternalStorageDirectory(), "systemUpdateUnitTests.xml"));
@@ -131,8 +135,9 @@
         mSystemAppUpdateTracker = new SystemAppUpdateTracker(mMockContext,
             mLocaleManagerService, mStoragefile);
 
-        mPackageMonitor = new LocaleManagerServicePackageMonitor(
-                mockLocaleManagerBackupHelper, mSystemAppUpdateTracker);
+        AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
+        mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper,
+                mSystemAppUpdateTracker, appUpdateTracker);
     }
 
     @After
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
new file mode 100644
index 0000000..fb1a8f8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.location.contexthub;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.location.ContextHubInfo;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+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;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class ContextHubServiceTest {
+    private static final int CONTEXT_HUB_ID = 3;
+    private static final String CONTEXT_HUB_STRING = "Context Hub Info Test";
+
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    @Mock private IContextHubWrapper mMockContextHubWrapper;
+    @Mock private ContextHubInfo mMockContextHubInfo;
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Before
+    public void setUp() throws RemoteException {
+        Pair<List<ContextHubInfo>, List<String>> hubInfo =
+                new Pair<>(Arrays.asList(mMockContextHubInfo), Arrays.asList(""));
+        when(mMockContextHubInfo.getId()).thenReturn(CONTEXT_HUB_ID);
+        when(mMockContextHubInfo.toString()).thenReturn(CONTEXT_HUB_STRING);
+        when(mMockContextHubWrapper.getHubs()).thenReturn(hubInfo);
+
+        when(mMockContextHubWrapper.supportsLocationSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(true);
+        when(mMockContextHubWrapper.supportsAirplaneModeSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsMicrophoneSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsBtSettingNotifications()).thenReturn(true);
+    }
+
+// TODO (b/254290317): These existing tests are to setup the testing infra for the ContextHub
+//                     service and verify the constructor correctly registers a context hub.
+//                     We need to augment these tests to cover the full behavior of the
+//                     ContextHub service
+
+    @Test
+    public void testConstructorRegistersContextHub() throws RemoteException {
+        ContextHubService service = new ContextHubService(mContext, mMockContextHubWrapper);
+        assertThat(service.getContextHubInfo(CONTEXT_HUB_ID)).isEqualTo(mMockContextHubInfo);
+    }
+
+    @Test
+    public void testConstructorRegistersNotifications() {
+        new ContextHubService(mContext, mMockContextHubWrapper);
+        verify(mMockContextHubWrapper).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiMainSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onMicrophoneSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onBtScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onBtMainSettingChanged(anyBoolean());
+    }
+
+    @Test
+    public void testConstructorRegistersNotificationsAndHandlesSettings() {
+        when(mMockContextHubWrapper.supportsLocationSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(false);
+        when(mMockContextHubWrapper.supportsAirplaneModeSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsMicrophoneSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsBtSettingNotifications()).thenReturn(false);
+
+        new ContextHubService(mContext, mMockContextHubWrapper);
+        verify(mMockContextHubWrapper, never()).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiMainSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onMicrophoneSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onBtScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onBtMainSettingChanged(anyBoolean());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java
index 1e855a9..1eb4fa5 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/PasswordSlotManagerTestable.java
@@ -58,4 +58,4 @@
         } catch (Exception e) {
         }
     }
-};
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
index ea3c5fa..d9ebb4c 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
@@ -62,7 +62,7 @@
                 context.unregisterReceiver(this);
                 latch.countDown();
             }
-        }, new IntentFilter(TEST_INTENT_ACTION));
+        }, new IntentFilter(TEST_INTENT_ACTION), Context.RECEIVER_EXPORTED_UNAUDITED);
 
         mStorage.setSnapshotListener(recoveryAgentUid, intent);
 
@@ -83,7 +83,8 @@
                 latch.countDown();
             }
         };
-        context.registerReceiver(broadcastReceiver, new IntentFilter(TEST_INTENT_ACTION));
+        context.registerReceiver(broadcastReceiver, new IntentFilter(TEST_INTENT_ACTION),
+                Context.RECEIVER_EXPORTED_UNAUDITED);
 
         mStorage.setSnapshotListener(recoveryAgentUid, intent);
         mStorage.setSnapshotListener(recoveryAgentUid, intent);
diff --git a/services/tests/servicestests/src/com/android/server/media/MediaButtonReceiverHolderTest.java b/services/tests/servicestests/src/com/android/server/media/MediaButtonReceiverHolderTest.java
new file mode 100644
index 0000000..1c4ee69
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/MediaButtonReceiverHolderTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.media;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaButtonReceiverHolderTest {
+
+    @Test
+    public void createMediaButtonReceiverHolder_resolvesNullComponentName() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        PendingIntent pi = PendingIntent.getBroadcast(context, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+        MediaButtonReceiverHolder a = MediaButtonReceiverHolder.create(/* userId= */ 0, pi,
+                context.getPackageName());
+        Truth.assertWithMessage("Component name must match PendingIntent creator package.").that(
+                a.getComponentName()).isNull();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/media/OWNERS b/services/tests/servicestests/src/com/android/server/media/OWNERS
new file mode 100644
index 0000000..55ffde2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 137631
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3bcde6a..b7f90d4 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -113,8 +113,10 @@
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
@@ -134,6 +136,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiInfo;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.INetworkManagementService;
 import android.os.PersistableBundle;
@@ -152,6 +155,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.DataUnit;
 import android.util.Log;
 import android.util.Pair;
@@ -171,11 +175,12 @@
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.usage.AppStandbyInternal;
 
-import libcore.io.Streams;
-
 import com.google.common.util.concurrent.AbstractFuture;
 
+import libcore.io.Streams;
+
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -286,6 +291,8 @@
     private NetworkPolicyListenerAnswer mPolicyListener;
     private NetworkPolicyManagerService mService;
 
+    private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>();
+
     /**
      * In some of the tests while initializing NetworkPolicyManagerService,
      * ACTION_RESTRICT_BACKGROUND_CHANGED is broadcasted. This is for capturing that broadcast.
@@ -437,6 +444,21 @@
             public void enforceCallingOrSelfPermission(String permission, String message) {
                 // Assume that we're AID_SYSTEM
             }
+
+            @Override
+            public Intent registerReceiver(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
+            }
+
+            @Override
+            public Intent registerReceiverForAllUsers(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiverForAllUsers(receiver, filter, broadcastPermission,
+                        scheduler);
+            }
         };
 
         setNetpolicyXml(context);
@@ -557,6 +579,13 @@
         RecurrenceRule.sClock = Clock.systemDefaultZone();
     }
 
+    @After
+    public void unregisterReceivers() throws Exception {
+        for (BroadcastReceiver receiver : mRegisteredReceivers) {
+            mServiceContext.unregisterReceiver(receiver);
+        }
+    }
+
     @Test
     public void testTurnRestrictBackgroundOn() throws Exception {
         assertRestrictBackgroundOff();
@@ -2033,6 +2062,9 @@
 
     @Test
     public void testNormalizeTemplate_duplicatedMergedImsiList() {
+        // This test leads to a Log.wtf, so skip it on eng builds. Otherwise, Log.wtf() would
+        // result in this process getting killed.
+        Assume.assumeFalse(Build.IS_ENG);
         final NetworkTemplate template = new NetworkTemplate.Builder(MATCH_CARRIER)
                 .setSubscriberIds(Set.of(TEST_IMSI)).build();
         final String[] mergedImsiGroup = new String[] {TEST_IMSI, TEST_IMSI};
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
index 94e67d1..ec61b87 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
@@ -16,16 +16,17 @@
 
 package com.android.server.om;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
+import android.content.pm.UserPackage;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,6 +44,9 @@
     private static final String OVERLAY2 = OVERLAY + "2";
     private static final OverlayIdentifier IDENTIFIER2 = new OverlayIdentifier(OVERLAY2);
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     @Test
     public void alwaysInitializeAllPackages() {
         final OverlayManagerServiceImpl impl = getImpl();
@@ -51,13 +55,11 @@
         addPackage(target(otherTarget), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER),
-                        new PackageAndUser(otherTarget, USER),
-                        new PackageAndUser(OVERLAY, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        // The result should be the same for every time
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
     }
 
     @Test
@@ -66,29 +68,31 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertFalse(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertTrue(o2.isEnabled());
-        assertFalse(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isTrue();
+        expect.that(o2.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertFalse(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isFalse();
     }
 
     @Test
@@ -98,28 +102,30 @@
         addPackage(overlay(OVERLAY, TARGET), USER);
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertTrue(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertFalse(o2.isEnabled());
-        assertTrue(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isFalse();
+        expect.that(o2.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertTrue(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isTrue();
     }
 
     @Test
@@ -128,17 +134,17 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
         final Consumer<ConfigState> setOverlay = (state -> {
             configureSystemOverlay(OVERLAY, state, 0 /* priority */);
-            assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+            expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
             final OverlayInfo o = impl.getOverlayInfo(IDENTIFIER, USER);
-            assertNotNull(o);
-            assertEquals(o.isEnabled(), state == ConfigState.IMMUTABLE_ENABLED
+            expect.that(o).isNotNull();
+            assertThat(expect.hasFailures()).isFalse();
+            expect.that(o.isEnabled()).isEqualTo(state == ConfigState.IMMUTABLE_ENABLED
                     || state == ConfigState.MUTABLE_ENABLED);
-            assertEquals(o.isMutable, state == ConfigState.MUTABLE_DISABLED
+            expect.that(o.isMutable).isEqualTo(state == ConfigState.MUTABLE_DISABLED
                     || state == ConfigState.MUTABLE_ENABLED);
         });
 
@@ -180,20 +186,20 @@
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertFalse(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isFalse();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertFalse(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isFalse();
 
         // Overlay priority changing between reboots should not affect enable state of mutable
         // overlays.
@@ -202,16 +208,18 @@
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertFalse(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isFalse();
     }
 
     @Test
@@ -223,33 +231,35 @@
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertTrue(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isTrue();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertTrue(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isTrue();
 
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertTrue(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isTrue();
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
index f69141d..ab52928 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
@@ -22,7 +22,6 @@
 import static android.os.OverlayablePolicy.CONFIG_SIGNATURE;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -30,7 +29,7 @@
 
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
-import android.util.Pair;
+import android.content.pm.UserPackage;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -66,7 +65,7 @@
     @Test
     public void testGetOverlayInfo() throws Exception {
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final OverlayManagerServiceImpl impl = getImpl();
         final OverlayInfo oi = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -79,11 +78,11 @@
     @Test
     public void testGetOverlayInfosForTarget() throws Exception {
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY2, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY3, TARGET), USER2,
-                Set.of(new PackageAndUser(OVERLAY3, USER2), new PackageAndUser(TARGET, USER2)));
+                Set.of(UserPackage.of(USER2, OVERLAY3), UserPackage.of(USER2, TARGET)));
 
         final OverlayManagerServiceImpl impl = getImpl();
         final List<OverlayInfo> ois = impl.getOverlayInfosForTarget(TARGET, USER);
@@ -107,13 +106,13 @@
     @Test
     public void testGetOverlayInfosForUser() throws Exception {
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY2, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY3, TARGET2), USER,
-                Set.of(new PackageAndUser(OVERLAY3, USER), new PackageAndUser(TARGET2, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY3), UserPackage.of(USER, TARGET2)));
 
         final OverlayManagerServiceImpl impl = getImpl();
         final Map<String, List<OverlayInfo>> everything = impl.getOverlaysForUser(USER);
@@ -138,11 +137,11 @@
     @Test
     public void testPriority() throws Exception {
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY2, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY3, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY3, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY3), UserPackage.of(USER, TARGET)));
 
         final OverlayManagerServiceImpl impl = getImpl();
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -152,15 +151,15 @@
         assertOverlayInfoForTarget(TARGET, USER, o1, o2, o3);
 
         assertEquals(impl.setLowestPriority(IDENTIFIER3, USER),
-                Optional.of(new PackageAndUser(TARGET, USER)));
+                Optional.of(UserPackage.of(USER, TARGET)));
         assertOverlayInfoForTarget(TARGET, USER, o3, o1, o2);
 
         assertEquals(impl.setHighestPriority(IDENTIFIER3, USER),
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         assertOverlayInfoForTarget(TARGET, USER, o1, o2, o3);
 
         assertEquals(impl.setPriority(IDENTIFIER, IDENTIFIER2, USER),
-                Optional.of(new PackageAndUser(TARGET, USER)));
+                Optional.of(UserPackage.of(USER, TARGET)));
         assertOverlayInfoForTarget(TARGET, USER, o2, o1, o3);
     }
 
@@ -170,47 +169,47 @@
         assertNull(impl.getOverlayInfo(IDENTIFIER, USER));
 
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
         assertState(STATE_MISSING_TARGET, IDENTIFIER, USER);
 
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         assertState(STATE_DISABLED, IDENTIFIER, USER);
 
         assertEquals(impl.setEnabled(IDENTIFIER, true, USER),
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         assertState(STATE_ENABLED, IDENTIFIER, USER);
 
         // target upgrades do not change the state of the overlay
         upgradeAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)),
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)),
+                Set.of(UserPackage.of(USER, TARGET)));
         assertState(STATE_ENABLED, IDENTIFIER, USER);
 
         uninstallAndAssert(TARGET, USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         assertState(STATE_MISSING_TARGET, IDENTIFIER, USER);
 
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         assertState(STATE_ENABLED, IDENTIFIER, USER);
     }
 
     @Test
     public void testOnOverlayPackageUpgraded() throws Exception {
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
         upgradeAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)),
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)),
+                Set.of(UserPackage.of(USER, TARGET)));
 
         // upgrade to a version where the overlay has changed its target
         upgradeAndAssert(overlay(OVERLAY, TARGET2), USER,
-                Set.of(new PackageAndUser(TARGET, USER)),
-                Set.of(new PackageAndUser(TARGET, USER),
-                        new PackageAndUser(TARGET2, USER)));
+                Set.of(UserPackage.of(USER, TARGET)),
+                Set.of(UserPackage.of(USER, TARGET),
+                        UserPackage.of(USER, TARGET2)));
     }
 
     @Test
@@ -222,10 +221,10 @@
         // request succeeded, and there was a change that needs to be
         // propagated to the rest of the system
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
-        assertEquals(Set.of(new PackageAndUser(TARGET, USER)),
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
+        assertEquals(Set.of(UserPackage.of(USER, TARGET)),
                 impl.setEnabled(IDENTIFIER, true, USER));
 
         // request succeeded, but nothing changed
@@ -239,9 +238,9 @@
 
         addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_OK), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final FakeIdmapDaemon idmapd = getIdmapd();
         final FakeDeviceState state = getState();
@@ -259,9 +258,9 @@
 
         addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final FakeIdmapDaemon idmapd = getIdmapd();
         final FakeDeviceState state = getState();
@@ -276,9 +275,9 @@
     public void testConfigSignaturePolicyNoConfig() throws Exception {
         addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final FakeIdmapDaemon idmapd = getIdmapd();
         final FakeDeviceState state = getState();
@@ -292,9 +291,9 @@
     @Test
     public void testConfigSignaturePolicyNoRefPkg() throws Exception {
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final FakeIdmapDaemon idmapd = getIdmapd();
         final FakeDeviceState state = getState();
@@ -312,9 +311,9 @@
 
         addPackage(app(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
         installAndAssert(target(TARGET), USER,
-                Set.of(new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, TARGET)));
         installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
-                Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+                Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
 
         final FakeIdmapDaemon idmapd = getIdmapd();
         final FakeDeviceState state = getState();
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
index 301697d..bba7669 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
@@ -28,6 +28,7 @@
 import android.content.om.OverlayInfo;
 import android.content.om.OverlayInfo.State;
 import android.content.om.OverlayableInfo;
+import android.content.pm.UserPackage;
 import android.os.FabricatedOverlayInfo;
 import android.os.FabricatedOverlayInternal;
 import android.text.TextUtils;
@@ -164,7 +165,7 @@
      * @throws IllegalStateException if the package is currently installed
      */
     void installAndAssert(@NonNull FakeDeviceState.PackageBuilder pkg, int userId,
-            @NonNull Set<PackageAndUser> onAddedUpdatedPackages)
+            @NonNull Set<UserPackage> onAddedUpdatedPackages)
             throws OperationFailedException {
         if (mState.select(pkg.packageName, userId) != null) {
             throw new IllegalStateException("package " + pkg.packageName + " already installed");
@@ -185,8 +186,8 @@
      * @throws IllegalStateException if the package is not currently installed
      */
     void upgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId,
-            @NonNull Set<PackageAndUser> onReplacingUpdatedPackages,
-            @NonNull Set<PackageAndUser> onReplacedUpdatedPackages)
+            @NonNull Set<UserPackage> onReplacingUpdatedPackages,
+            @NonNull Set<UserPackage> onReplacedUpdatedPackages)
             throws OperationFailedException {
         final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId);
         if (replacedPackage == null) {
@@ -207,7 +208,7 @@
      * @throws IllegalStateException if the package is not currently installed
      */
     void uninstallAndAssert(@NonNull String packageName, int userId,
-            @NonNull Set<PackageAndUser> onRemovedUpdatedPackages) {
+            @NonNull Set<UserPackage> onRemovedUpdatedPackages) {
         final FakeDeviceState.Package pkg = mState.select(packageName, userId);
         if (pkg == null) {
             throw new IllegalStateException("package " + packageName + " not installed");
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
index 0a26f27..3f7eac7 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
@@ -29,12 +29,13 @@
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.annotation.NonNull;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
deleted file mode 100644
index a7739ed..0000000
--- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
+++ /dev/null
@@ -1,599 +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.server.pm;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.testng.Assert.assertThrows;
-
-import android.apex.ApexInfo;
-import android.apex.ApexSessionInfo;
-import android.apex.ApexSessionParams;
-import android.apex.IApexService;
-import android.content.Context;
-import android.os.Environment;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-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.server.pm.parsing.PackageParser2;
-import com.android.server.pm.parsing.TestPackageParser2;
-import com.android.server.pm.pkg.AndroidPackage;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.List;
-
-@SmallTest
-@Presubmit
-@RunWith(AndroidJUnit4.class)
-
-public class ApexManagerTest {
-    private static final String TEST_APEX_PKG = "com.android.apex.test";
-    private static final String TEST_APEX_FILE_NAME = "apex.test.apex";
-    private static final int TEST_SESSION_ID = 99999999;
-    private static final int[] TEST_CHILD_SESSION_ID = {8888, 7777};
-    private ApexManager mApexManager;
-    private Context mContext;
-    private PackageParser2 mPackageParser2;
-
-    private IApexService mApexService = mock(IApexService.class);
-
-    @Before
-    public void setUp() throws RemoteException {
-        mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        ApexManager.ApexManagerImpl managerImpl = spy(new ApexManager.ApexManagerImpl());
-        doReturn(mApexService).when(managerImpl).waitForApexService();
-        mApexManager = managerImpl;
-        mPackageParser2 = new TestPackageParser2();
-    }
-
-    @Test
-    public void testGetPackageInfo_setFlagsMatchActivePackage() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, false);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        final var activePair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
-                ApexManager.MATCH_ACTIVE_PACKAGE);
-
-        assertThat(activePair).isNotNull();
-        assertThat(activePair.second.getPackageName()).contains(TEST_APEX_PKG);
-
-        final var factoryPair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
-                ApexManager.MATCH_FACTORY_PACKAGE);
-
-        assertThat(factoryPair).isNull();
-    }
-
-    @Test
-    public void testGetPackageInfo_setFlagsMatchFactoryPackage() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        var factoryPair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
-                ApexManager.MATCH_FACTORY_PACKAGE);
-
-        assertThat(factoryPair).isNotNull();
-        assertThat(factoryPair.second.getPackageName()).contains(TEST_APEX_PKG);
-
-        final var activePair = apexPackageInfo.getPackageInfo(TEST_APEX_PKG,
-                ApexManager.MATCH_ACTIVE_PACKAGE);
-
-        assertThat(activePair).isNull();
-    }
-
-    @Test
-    public void testGetPackageInfo_setFlagsNone() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getPackageInfo(TEST_APEX_PKG, 0)).isNull();
-    }
-
-    @Test
-    public void testGetApexSystemServices() throws RemoteException {
-        ApexInfo[] apexInfo = new ApexInfo[] {
-                createApexInfoForTestPkg(false, true, 1),
-                // only active apex reports apex-system-service
-                createApexInfoForTestPkg(true, false, 2),
-        };
-
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        List<ApexManager.ScanResult> scanResults = apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        mApexManager.notifyScanResult(scanResults);
-
-        List<ApexSystemServiceInfo> services = mApexManager.getApexSystemServices();
-        assertThat(services).hasSize(1);
-        assertThat(services.stream().map(ApexSystemServiceInfo::getName).findFirst().orElse(null))
-                .matches("com.android.apex.test.ApexSystemService");
-    }
-
-    @Test
-    public void testGetActivePackages() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getActivePackages()).isNotEmpty();
-    }
-
-    @Test
-    public void testGetActivePackages_noneActivePackages() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getActivePackages()).isEmpty();
-    }
-
-    @Test
-    public void testGetFactoryPackages() throws RemoteException {
-        ApexInfo [] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getFactoryPackages()).isNotEmpty();
-    }
-
-    @Test
-    public void testGetFactoryPackages_noneFactoryPackages() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, false);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getFactoryPackages()).isEmpty();
-    }
-
-    @Test
-    public void testGetInactivePackages() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getInactivePackages()).isNotEmpty();
-    }
-
-    @Test
-    public void testGetInactivePackages_noneInactivePackages() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, false);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.getInactivePackages()).isEmpty();
-    }
-
-    @Test
-    public void testIsApexPackage() throws RemoteException {
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(false, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        assertThat(apexPackageInfo.isApexPackage(TEST_APEX_PKG)).isTrue();
-    }
-
-    @Test
-    public void testIsApexSupported() {
-        assertThat(mApexManager.isApexSupported()).isTrue();
-    }
-
-    @Test
-    public void testGetStagedSessionInfo() throws RemoteException {
-        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
-                getFakeStagedSessionInfo());
-
-        mApexManager.getStagedSessionInfo(TEST_SESSION_ID);
-        verify(mApexService, times(1)).getStagedSessionInfo(TEST_SESSION_ID);
-    }
-
-    @Test
-    public void testGetStagedSessionInfo_unKnownStagedSessionId() throws RemoteException {
-        when(mApexService.getStagedSessionInfo(anyInt())).thenReturn(
-                getFakeUnknownSessionInfo());
-
-        assertThat(mApexManager.getStagedSessionInfo(TEST_SESSION_ID)).isNull();
-    }
-
-    @Test
-    public void testSubmitStagedSession_throwPackageManagerException() throws RemoteException {
-        doAnswer(invocation -> {
-            throw new Exception();
-        }).when(mApexService).submitStagedSession(any(), any());
-
-        assertThrows(PackageManagerException.class,
-                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
-    }
-
-    @Test
-    public void testSubmitStagedSession_throwRunTimeException() throws RemoteException {
-        doThrow(RemoteException.class).when(mApexService).submitStagedSession(any(), any());
-
-        assertThrows(RuntimeException.class,
-                () -> mApexManager.submitStagedSession(testParamsWithChildren()));
-    }
-
-    @Test
-    public void testGetStagedApexInfos_throwRunTimeException() throws RemoteException {
-        doThrow(RemoteException.class).when(mApexService).getStagedApexInfos(any());
-
-        assertThrows(RuntimeException.class,
-                () -> mApexManager.getStagedApexInfos(testParamsWithChildren()));
-    }
-
-    @Test
-    public void testGetStagedApexInfos_returnsEmptyArrayOnError() throws RemoteException {
-        doThrow(ServiceSpecificException.class).when(mApexService).getStagedApexInfos(any());
-
-        assertThat(mApexManager.getStagedApexInfos(testParamsWithChildren())).hasLength(0);
-    }
-
-    @Test
-    public void testMarkStagedSessionReady_throwPackageManagerException() throws RemoteException {
-        doAnswer(invocation -> {
-            throw new Exception();
-        }).when(mApexService).markStagedSessionReady(anyInt());
-
-        assertThrows(PackageManagerException.class,
-                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
-    }
-
-    @Test
-    public void testMarkStagedSessionReady_throwRunTimeException() throws RemoteException {
-        doThrow(RemoteException.class).when(mApexService).markStagedSessionReady(anyInt());
-
-        assertThrows(RuntimeException.class,
-                () -> mApexManager.markStagedSessionReady(TEST_SESSION_ID));
-    }
-
-    @Test
-    public void testRevertActiveSessions_remoteException() throws RemoteException {
-        doThrow(RemoteException.class).when(mApexService).revertActiveSessions();
-
-        try {
-            assertThat(mApexManager.revertActiveSessions()).isFalse();
-        } catch (Exception e) {
-            throw new AssertionError("ApexManager should not raise Exception");
-        }
-    }
-
-    @Test
-    public void testMarkStagedSessionSuccessful_throwRemoteException() throws RemoteException {
-        doThrow(RemoteException.class).when(mApexService).markStagedSessionSuccessful(anyInt());
-
-        assertThrows(RuntimeException.class,
-                () -> mApexManager.markStagedSessionSuccessful(TEST_SESSION_ID));
-    }
-
-    @Test
-    public void testUninstallApex_throwException_returnFalse() throws RemoteException {
-        doAnswer(invocation -> {
-            throw new Exception();
-        }).when(mApexService).unstagePackages(any());
-
-        assertThat(mApexManager.uninstallApex(TEST_APEX_PKG)).isFalse();
-    }
-
-    @Test
-    public void testReportErrorWithApkInApex() throws RemoteException {
-        when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true));
-        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
-        assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG);
-
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        List<ApexManager.ScanResult> scanResults = apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        mApexManager.notifyScanResult(scanResults);
-
-        assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName)).isNull();
-        mApexManager.reportErrorWithApkInApex(activeApex.apexDirectory.getAbsolutePath(),
-                "Some random error");
-        assertThat(mApexManager.getApkInApexInstallError(activeApex.apexModuleName))
-            .isEqualTo("Some random error");
-    }
-
-    /**
-     * registerApkInApex method checks if the prefix of base apk path contains the apex package
-     * name. When an apex package name is a prefix of another apex package name, e.g,
-     * com.android.media and com.android.mediaprovider, then we need to ensure apk inside apex
-     * mediaprovider does not get registered under apex media.
-     */
-    @Test
-    public void testRegisterApkInApexDoesNotRegisterSimilarPrefix() throws RemoteException {
-        when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true));
-        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
-        assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG);
-
-        AndroidPackage fakeApkInApex = mock(AndroidPackage.class);
-        when(fakeApkInApex.getBaseApkPath()).thenReturn("/apex/" + TEST_APEX_PKG + "randomSuffix");
-        when(fakeApkInApex.getPackageName()).thenReturn("randomPackageName");
-
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, true);
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        List<ApexManager.ScanResult> scanResults = apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        mApexManager.notifyScanResult(scanResults);
-
-        assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty();
-        mApexManager.registerApkInApex(fakeApkInApex);
-        assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty();
-    }
-
-    @Test
-    public void testInstallPackage_activeOnSystem() throws Exception {
-        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
-                /* isFactory= */ true, extractResource("test.apex_rebootless_v1",
-                  "test.rebootless_apex_v1.apex"));
-        ApexInfo[] apexInfo = new ApexInfo[]{activeApexInfo};
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
-        ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
-                /* isFactory= */ false, finalApex);
-        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
-
-        File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
-        newApexInfo = mApexManager.installPackage(installedApex);
-        apexPackageInfo.notifyPackageInstalled(newApexInfo, mPackageParser2);
-
-        var newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
-                ApexManager.MATCH_ACTIVE_PACKAGE);
-        assertThat(newInfo.second.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
-        assertThat(newInfo.second.getLongVersionCode()).isEqualTo(2);
-
-        var factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
-                ApexManager.MATCH_FACTORY_PACKAGE);
-        assertThat(factoryInfo.second.getBaseApkPath()).isEqualTo(activeApexInfo.modulePath);
-        assertThat(factoryInfo.second.getLongVersionCode()).isEqualTo(1);
-        assertThat(factoryInfo.second.isSystem()).isTrue();
-    }
-
-    @Test
-    public void testInstallPackage_activeOnData() throws Exception {
-        ApexInfo factoryApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ false,
-                /* isFactory= */ true, extractResource("test.apex_rebootless_v1",
-                  "test.rebootless_apex_v1.apex"));
-        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
-                /* isFactory= */ false, extractResource("test.apex.rebootless@1",
-                  "test.rebootless_apex_v1.apex"));
-        ApexInfo[] apexInfo = new ApexInfo[]{factoryApexInfo, activeApexInfo};
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex");
-        ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
-                /* isFactory= */ false, finalApex);
-        when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo);
-
-        File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex");
-        newApexInfo = mApexManager.installPackage(installedApex);
-        apexPackageInfo.notifyPackageInstalled(newApexInfo, mPackageParser2);
-
-        var newInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
-                ApexManager.MATCH_ACTIVE_PACKAGE);
-        assertThat(newInfo.second.getBaseApkPath()).isEqualTo(finalApex.getAbsolutePath());
-        assertThat(newInfo.second.getLongVersionCode()).isEqualTo(2);
-
-        var factoryInfo = apexPackageInfo.getPackageInfo("test.apex.rebootless",
-                ApexManager.MATCH_FACTORY_PACKAGE);
-        assertThat(factoryInfo.second.getBaseApkPath()).isEqualTo(factoryApexInfo.modulePath);
-        assertThat(factoryInfo.second.getLongVersionCode()).isEqualTo(1);
-        assertThat(factoryInfo.second.isSystem()).isTrue();
-    }
-
-    @Test
-    public void testInstallPackageBinderCallFails() throws Exception {
-        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
-                /* isFactory= */ false, extractResource("test.apex_rebootless_v1",
-                  "test.rebootless_apex_v1.apex"));
-        ApexInfo[] apexInfo = new ApexInfo[]{activeApexInfo};
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-
-        when(mApexService.installAndActivatePackage(anyString())).thenThrow(
-                new RuntimeException("install failed :("));
-
-        File installedApex = extractResource("test.apex_rebootless_v1",
-                "test.rebootless_apex_v1.apex");
-        assertThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(installedApex));
-    }
-
-    @Test
-    public void testGetActivePackageNameForApexModuleName() throws Exception {
-        final String moduleName = "com.android.module_name";
-
-        ApexInfo[] apexInfo = createApexInfoForTestPkg(true, false);
-        apexInfo[0].moduleName = moduleName;
-        ApexPackageInfo apexPackageInfo = new ApexPackageInfo();
-        List<ApexManager.ScanResult> scanResults = apexPackageInfo.scanApexPackages(
-                apexInfo, mPackageParser2, ParallelPackageParser.makeExecutorService());
-        mApexManager.notifyScanResult(scanResults);
-
-        assertThat(mApexManager.getActivePackageNameForApexModuleName(moduleName))
-                .isEqualTo(TEST_APEX_PKG);
-    }
-
-    @Test
-    public void testGetBackingApexFiles() throws Exception {
-        final ApexInfo apex = createApexInfoForTestPkg(true, true, 37);
-        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex});
-
-        final File backingApexFile = mApexManager.getBackingApexFile(
-                new File("/apex/" + TEST_APEX_PKG + "/apk/App/App.apk"));
-        assertThat(backingApexFile.getAbsolutePath()).isEqualTo(apex.modulePath);
-    }
-
-    @Test
-    public void testGetBackingApexFile_fileNotOnApexMountPoint_returnsNull() throws Exception {
-        File result = mApexManager.getBackingApexFile(
-                new File("/data/local/tmp/whatever/does-not-matter"));
-        assertThat(result).isNull();
-    }
-
-    @Test
-    public void testGetBackingApexFiles_unknownApex_returnsNull() throws Exception {
-        final ApexInfo apex = createApexInfoForTestPkg(true, true, 37);
-        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex});
-
-        final File backingApexFile = mApexManager.getBackingApexFile(
-                new File("/apex/com.wrong.apex/apk/App"));
-        assertThat(backingApexFile).isNull();
-    }
-
-    @Test
-    public void testGetBackingApexFiles_topLevelApexDir_returnsNull() throws Exception {
-        assertThat(mApexManager.getBackingApexFile(Environment.getApexDirectory())).isNull();
-        assertThat(mApexManager.getBackingApexFile(new File("/apex/"))).isNull();
-        assertThat(mApexManager.getBackingApexFile(new File("/apex//"))).isNull();
-    }
-
-    @Test
-    public void testGetBackingApexFiles_flattenedApex() throws Exception {
-        ApexManager flattenedApexManager = new ApexManager.ApexManagerFlattenedApex();
-        final File backingApexFile = flattenedApexManager.getBackingApexFile(
-                new File("/apex/com.android.apex.cts.shim/app/CtsShim/CtsShim.apk"));
-        assertThat(backingApexFile).isNull();
-    }
-
-    @Test
-    public void testActiveApexChanged() throws RemoteException {
-        ApexInfo apex1 = createApexInfo(
-                "com.apex1", 37, true, true, new File("/data/apex/active/com.apex@37.apex"));
-        apex1.activeApexChanged = true;
-        apex1.preinstalledModulePath = apex1.modulePath;
-        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex1});
-        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
-        assertThat(activeApex.apexModuleName).isEqualTo("com.apex1");
-        assertThat(activeApex.activeApexChanged).isTrue();
-    }
-
-    private ApexInfo createApexInfoForTestPkg(boolean isActive, boolean isFactory, int version) {
-        File apexFile = extractResource(TEST_APEX_PKG,  TEST_APEX_FILE_NAME);
-        ApexInfo apexInfo = new ApexInfo();
-        apexInfo.isActive = isActive;
-        apexInfo.isFactory = isFactory;
-        apexInfo.moduleName = TEST_APEX_PKG;
-        apexInfo.modulePath = apexFile.getPath();
-        apexInfo.versionCode = version;
-        apexInfo.preinstalledModulePath = apexFile.getPath();
-        return apexInfo;
-    }
-
-    private ApexInfo[] createApexInfoForTestPkg(boolean isActive, boolean isFactory) {
-        return new ApexInfo[]{createApexInfoForTestPkg(isActive, isFactory, 191000070)};
-    }
-
-    private ApexInfo createApexInfo(String moduleName, int versionCode, boolean isActive,
-            boolean isFactory, File apexFile) {
-        ApexInfo apexInfo = new ApexInfo();
-        apexInfo.moduleName = moduleName;
-        apexInfo.versionCode = versionCode;
-        apexInfo.isActive = isActive;
-        apexInfo.isFactory = isFactory;
-        apexInfo.modulePath = apexFile.getPath();
-        return apexInfo;
-    }
-
-    private ApexSessionInfo getFakeStagedSessionInfo() {
-        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
-        stagedSessionInfo.sessionId = TEST_SESSION_ID;
-        stagedSessionInfo.isStaged = true;
-
-        return stagedSessionInfo;
-    }
-
-    private ApexSessionInfo getFakeUnknownSessionInfo() {
-        ApexSessionInfo stagedSessionInfo = new ApexSessionInfo();
-        stagedSessionInfo.sessionId = TEST_SESSION_ID;
-        stagedSessionInfo.isUnknown = true;
-
-        return stagedSessionInfo;
-    }
-
-    private static ApexSessionParams testParamsWithChildren() {
-        ApexSessionParams params = new ApexSessionParams();
-        params.sessionId = TEST_SESSION_ID;
-        params.childSessionIds = TEST_CHILD_SESSION_ID;
-        return params;
-    }
-
-    // Extracts the binary data from a resource and writes it to a temp file
-    private static File extractResource(String baseName, String fullResourceName) {
-        File file;
-        try {
-            file = File.createTempFile(baseName, ".apex");
-        } catch (IOException e) {
-            throw new AssertionError("CreateTempFile IOException" + e);
-        }
-
-        try (
-                InputStream in = ApexManager.class.getClassLoader()
-                        .getResourceAsStream(fullResourceName);
-                OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
-            if (in == null) {
-                throw new IllegalArgumentException("Resource not found: " + fullResourceName);
-            }
-            byte[] buf = new byte[65536];
-            int chunkSize;
-            while ((chunkSize = in.read(buf)) != -1) {
-                out.write(buf, 0, chunkSize);
-            }
-            return file;
-        } catch (IOException e) {
-            throw new AssertionError("Exception while converting stream to file" + e);
-        }
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
index c321639..1a8ef9e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
@@ -387,7 +387,7 @@
 
         // delete user
         when(mSnapshot.getUserInfos()).thenReturn(USER_INFO_LIST);
-        appsFilter.onUserDeleted(ADDED_USER);
+        appsFilter.onUserDeleted(mSnapshot, ADDED_USER);
 
         for (int subjectUserId : USER_ARRAY) {
             for (int otherUserId : USER_ARRAY) {
@@ -925,7 +925,7 @@
         assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_OVERLAY_APPID,
                 overlaySetting, actorSetting, SYSTEM_USER));
 
-        appsFilter.removePackage(mSnapshot, targetSetting, false /* isReplace */);
+        appsFilter.removePackage(mSnapshot, targetSetting);
 
         // Actor loses visibility to the overlay via removal of the target
         assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_ACTOR_APPID, actorSetting,
@@ -1267,7 +1267,7 @@
         watcher.verifyNoChangeReported("get");
 
         // remove a package
-        appsFilter.removePackage(mSnapshot, seesNothing, false /* isReplace */);
+        appsFilter.removePackage(mSnapshot, seesNothing);
         watcher.verifyChangeReported("removePackage");
     }
 
@@ -1337,7 +1337,7 @@
                 target.getPackageName()));
 
         // New changes don't affect the snapshot
-        appsFilter.removePackage(mSnapshot, target, false);
+        appsFilter.removePackage(mSnapshot, target);
         assertTrue(
                 appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, instrumentation,
                         target,
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index fdf9354..0805485 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -74,6 +74,7 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningInfo;
 import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Icon;
@@ -97,7 +98,6 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.pm.LauncherAppsService.LauncherAppsImpl;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
 import com.android.server.uri.UriGrantsManagerInternal;
 import com.android.server.uri.UriPermissionOwner;
 import com.android.server.wm.ActivityTaskManagerInternal;
@@ -692,9 +692,9 @@
 
     protected Map<String, PackageInfo> mInjectedPackages;
 
-    protected Set<PackageWithUser> mUninstalledPackages;
-    protected Set<PackageWithUser> mDisabledPackages;
-    protected Set<PackageWithUser> mEphemeralPackages;
+    protected Set<UserPackage> mUninstalledPackages;
+    protected Set<UserPackage> mDisabledPackages;
+    protected Set<UserPackage> mEphemeralPackages;
     protected Set<String> mSystemPackages;
 
     protected PackageManager mMockPackageManager;
@@ -1200,28 +1200,28 @@
         if (ENABLE_DUMP) {
             Log.v(TAG, "Uninstall package " + packageName + " / " + userId);
         }
-        mUninstalledPackages.add(PackageWithUser.of(userId, packageName));
+        mUninstalledPackages.add(UserPackage.of(userId, packageName));
     }
 
     protected void installPackage(int userId, String packageName) {
         if (ENABLE_DUMP) {
             Log.v(TAG, "Install package " + packageName + " / " + userId);
         }
-        mUninstalledPackages.remove(PackageWithUser.of(userId, packageName));
+        mUninstalledPackages.remove(UserPackage.of(userId, packageName));
     }
 
     protected void disablePackage(int userId, String packageName) {
         if (ENABLE_DUMP) {
             Log.v(TAG, "Disable package " + packageName + " / " + userId);
         }
-        mDisabledPackages.add(PackageWithUser.of(userId, packageName));
+        mDisabledPackages.add(UserPackage.of(userId, packageName));
     }
 
     protected void enablePackage(int userId, String packageName) {
         if (ENABLE_DUMP) {
             Log.v(TAG, "Enable package " + packageName + " / " + userId);
         }
-        mDisabledPackages.remove(PackageWithUser.of(userId, packageName));
+        mDisabledPackages.remove(UserPackage.of(userId, packageName));
     }
 
     PackageInfo getInjectedPackageInfo(String packageName, @UserIdInt int userId,
@@ -1239,17 +1239,17 @@
         ret.applicationInfo.uid = UserHandle.getUid(userId, pi.applicationInfo.uid);
         ret.applicationInfo.packageName = pi.packageName;
 
-        if (mUninstalledPackages.contains(PackageWithUser.of(userId, packageName))) {
+        if (mUninstalledPackages.contains(UserPackage.of(userId, packageName))) {
             ret.applicationInfo.flags &= ~ApplicationInfo.FLAG_INSTALLED;
         }
-        if (mEphemeralPackages.contains(PackageWithUser.of(userId, packageName))) {
+        if (mEphemeralPackages.contains(UserPackage.of(userId, packageName))) {
             ret.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_INSTANT;
         }
         if (mSystemPackages.contains(packageName)) {
             ret.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
         }
         ret.applicationInfo.enabled =
-                !mDisabledPackages.contains(PackageWithUser.of(userId, packageName));
+                !mDisabledPackages.contains(UserPackage.of(userId, packageName));
 
         if (getSignatures) {
             ret.signatures = null;
diff --git a/services/tests/servicestests/src/com/android/server/pm/CompatibilityModeTest.java b/services/tests/servicestests/src/com/android/server/pm/CompatibilityModeTest.java
index eaa0e9b..f0d389b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/CompatibilityModeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/CompatibilityModeTest.java
@@ -34,6 +34,7 @@
 
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.pkg.PackageImpl;
+import com.android.server.pm.pkg.PackageStateUnserialized;
 import com.android.server.pm.pkg.PackageUserStateImpl;
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 
@@ -46,12 +47,16 @@
 
     private boolean mCompatibilityModeEnabled;;
     private PackageImpl mMockAndroidPackage;
+    private PackageSetting mMockPackageState;
     private PackageUserStateImpl mMockUserState;
 
     @Before
     public void setUp() {
         mCompatibilityModeEnabled = ParsingPackageUtils.sCompatibilityModeEnabled;
         mMockAndroidPackage = mock(PackageImpl.class);
+        mMockPackageState = mock(PackageSetting.class);
+        when(mMockPackageState.getTransientState())
+                .thenReturn(new PackageStateUnserialized(mMockPackageState));
         mMockUserState = new PackageUserStateImpl();
         mMockUserState.setInstalled(true);
     }
@@ -221,7 +226,7 @@
         info.flags |= flags;
         when(mMockAndroidPackage.toAppInfoWithoutState()).thenReturn(info);
         return PackageInfoUtils.generateApplicationInfo(mMockAndroidPackage,
-                0 /*flags*/, mMockUserState, 0 /*userId*/, null);
+                0 /*flags*/, mMockUserState, 0 /*userId*/, mMockPackageState);
     }
 
     private void setGlobalCompatibilityMode(boolean enabled) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
index a545b1f..648f895 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
@@ -32,13 +32,13 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.BackgroundThread;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
index 68310f4..59f27ec 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
@@ -127,6 +128,7 @@
     private static final String TEST_APP3_APK = "PackageParserTestApp3.apk";
     private static final String TEST_APP4_APK = "PackageParserTestApp4.apk";
     private static final String TEST_APP5_APK = "PackageParserTestApp5.apk";
+    private static final String TEST_APP6_APK = "PackageParserTestApp6.apk";
     private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp";
 
     @Before
@@ -331,6 +333,46 @@
         }
     }
 
+    @Test
+    public void testParseActivityTargetDisplayCategoryValid() throws Exception {
+        final File testFile = extractFile(TEST_APP4_APK);
+        String actualDisplayCategory = null;
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            final List<ParsedActivity> activities = pkg.getActivities();
+            for (ParsedActivity activity : activities) {
+                if ((PACKAGE_NAME + ".MyActivity").equals(activity.getName())) {
+                    actualDisplayCategory = activity.getTargetDisplayCategory();
+                }
+            }
+        } finally {
+            testFile.delete();
+        }
+        assertEquals("automotive", actualDisplayCategory);
+    }
+
+    @Test
+    public void testParseActivityTargetDisplayCategoryInvalid() throws Exception {
+        final File testFile = extractFile(TEST_APP6_APK);
+        String actualDisplayCategory = null;
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            final List<ParsedActivity> activities = pkg.getActivities();
+            for (ParsedActivity activity : activities) {
+                if ((PACKAGE_NAME + ".MyActivity").equals(activity.getName())) {
+                    actualDisplayCategory = activity.getTargetDisplayCategory();
+                }
+            }
+        } catch (PackageManagerException e) {
+            assertThat(e.getMessage()).contains(
+                    "targetDisplayCategory attribute can only consists"
+                            + " of alphanumeric characters, '_', and '.'");
+        } finally {
+            testFile.delete();
+        }
+        assertNotEquals("$automotive", actualDisplayCategory);
+    }
+
     private static final int PROPERTY_TYPE_BOOLEAN = 1;
     private static final int PROPERTY_TYPE_FLOAT = 2;
     private static final int PROPERTY_TYPE_INTEGER = 3;
@@ -634,27 +676,32 @@
         final File testFile = extractFile(TEST_APP4_APK);
         try {
             final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            var pkgSetting = mockPkgSetting(pkg);
             ApplicationInfo appInfo = PackageInfoUtils.generateApplicationInfo(pkg, 0,
-                    PackageUserStateInternal.DEFAULT, 0, null);
+                    PackageUserStateInternal.DEFAULT, 0, pkgSetting);
             for (ParsedActivity activity : pkg.getActivities()) {
                 assertNotNull(activity.getMetaData());
                 assertNull(PackageInfoUtils.generateActivityInfo(pkg, activity, 0,
-                        PackageUserStateInternal.DEFAULT, appInfo, 0, null).metaData);
+                        PackageUserStateInternal.DEFAULT, appInfo, 0, pkgSetting)
+                        .metaData);
             }
             for (ParsedProvider provider : pkg.getProviders()) {
                 assertNotNull(provider.getMetaData());
                 assertNull(PackageInfoUtils.generateProviderInfo(pkg, provider, 0,
-                        PackageUserStateInternal.DEFAULT, appInfo, 0, null).metaData);
+                        PackageUserStateInternal.DEFAULT, appInfo, 0, pkgSetting)
+                        .metaData);
             }
             for (ParsedActivity receiver : pkg.getReceivers()) {
                 assertNotNull(receiver.getMetaData());
                 assertNull(PackageInfoUtils.generateActivityInfo(pkg, receiver, 0,
-                        PackageUserStateInternal.DEFAULT, appInfo, 0, null).metaData);
+                        PackageUserStateInternal.DEFAULT, appInfo, 0, pkgSetting)
+                        .metaData);
             }
             for (ParsedService service : pkg.getServices()) {
                 assertNotNull(service.getMetaData());
                 assertNull(PackageInfoUtils.generateServiceInfo(pkg, service, 0,
-                        PackageUserStateInternal.DEFAULT, appInfo, 0, null).metaData);
+                        PackageUserStateInternal.DEFAULT, appInfo, 0, pkgSetting)
+                        .metaData);
             }
         } finally {
             testFile.delete();
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
index 7e4474f..47f75a5 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
@@ -25,13 +25,13 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.HexDump;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/pm/ScanTests.java b/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
index 4d03749..48d6d90 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
@@ -568,7 +568,6 @@
 
     private static void assertBasicPackageScanResult(
             ScanResult scanResult, String packageName, boolean isInstant) {
-        assertThat(scanResult.mSuccess, is(true));
 
         final PackageSetting pkgSetting = scanResult.mPkgSetting;
         assertBasicPackageSetting(scanResult, packageName, isInstant, pkgSetting);
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 867890f..b20c63c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -83,6 +83,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
+import android.content.pm.UserPackage;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.CompressFormat;
 import android.graphics.BitmapFactory;
@@ -98,16 +99,15 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.frameworks.servicestests.R;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.ConfigConstants;
 import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
 
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
@@ -4081,12 +4081,12 @@
         assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_10, LAUNCHER_1),
-                        PackageWithUser.of(USER_10, LAUNCHER_2)),
+                set(UserPackage.of(USER_10, LAUNCHER_1),
+                        UserPackage.of(USER_10, LAUNCHER_2)),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
         assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
                 "s0_1", "s0_2");
@@ -4113,12 +4113,12 @@
         assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_10, LAUNCHER_1),
-                        PackageWithUser.of(USER_10, LAUNCHER_2)),
+                set(UserPackage.of(USER_10, LAUNCHER_1),
+                        UserPackage.of(USER_10, LAUNCHER_2)),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
         assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
                 "s0_1", "s0_2");
@@ -4145,12 +4145,12 @@
         assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_10, LAUNCHER_1),
-                        PackageWithUser.of(USER_10, LAUNCHER_2)),
+                set(UserPackage.of(USER_10, LAUNCHER_1),
+                        UserPackage.of(USER_10, LAUNCHER_2)),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
         assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
                 "s0_2");
@@ -4176,11 +4176,11 @@
         assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+                set(UserPackage.of(USER_10, LAUNCHER_2)),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
         assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
                 "s0_2");
@@ -4205,11 +4205,11 @@
         assertEquals(set(CALLING_PACKAGE_1),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+                set(UserPackage.of(USER_10, LAUNCHER_2)),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
         assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
                 "s0_2");
@@ -4234,8 +4234,8 @@
         assertEquals(set(CALLING_PACKAGE_1),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(
                 set(),
@@ -4263,8 +4263,8 @@
         assertEquals(set(),
                 hashSet(user10.getAllPackagesForTest().keySet()));
         assertEquals(
-                set(PackageWithUser.of(USER_0, LAUNCHER_1),
-                        PackageWithUser.of(USER_0, LAUNCHER_2)),
+                set(UserPackage.of(USER_0, LAUNCHER_1),
+                        UserPackage.of(USER_0, LAUNCHER_2)),
                 hashSet(user0.getAllLaunchersForTest().keySet()));
         assertEquals(set(),
                 hashSet(user10.getAllLaunchersForTest().keySet()));
@@ -5584,12 +5584,12 @@
 
         assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_3));
         assertExistsAndShadow(user0.getAllLaunchersForTest().get(
-                PackageWithUser.of(USER_0, LAUNCHER_1)));
+                UserPackage.of(USER_0, LAUNCHER_1)));
         assertExistsAndShadow(user0.getAllLaunchersForTest().get(
-                PackageWithUser.of(USER_0, LAUNCHER_2)));
+                UserPackage.of(USER_0, LAUNCHER_2)));
 
-        assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_3)));
-        assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_P0, LAUNCHER_1)));
+        assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_0, LAUNCHER_3)));
+        assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_P0, LAUNCHER_1)));
 
         doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(any(byte[].class),
                 anyString());
@@ -6146,12 +6146,12 @@
         assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_2));
         assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_3));
         assertExistsAndShadow(user0.getAllLaunchersForTest().get(
-                PackageWithUser.of(USER_0, LAUNCHER_1)));
+                UserPackage.of(USER_0, LAUNCHER_1)));
         assertExistsAndShadow(user0.getAllLaunchersForTest().get(
-                PackageWithUser.of(USER_0, LAUNCHER_2)));
+                UserPackage.of(USER_0, LAUNCHER_2)));
 
-        assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_3)));
-        assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_P0, LAUNCHER_1)));
+        assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_0, LAUNCHER_3)));
+        assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_P0, LAUNCHER_1)));
 
         doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(any(byte[].class),
                 anyString());
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index c786784..15fd73c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -39,6 +39,7 @@
 import android.content.pm.Capability;
 import android.content.pm.CapabilityParams;
 import android.content.pm.ShortcutInfo;
+import android.content.pm.UserPackage;
 import android.content.res.Resources;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
@@ -51,7 +52,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.frameworks.servicestests.R;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
 
 import java.io.File;
 import java.io.FileWriter;
@@ -2413,7 +2413,7 @@
             assertWith(mManager.getDynamicShortcuts()).isEmpty();
         });
         // Make package 1 ephemeral.
-        mEphemeralPackages.add(PackageWithUser.of(USER_0, CALLING_PACKAGE_1));
+        mEphemeralPackages.add(UserPackage.of(USER_0, CALLING_PACKAGE_1));
 
         runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
             assertExpectException(IllegalStateException.class, "Ephemeral apps", () -> {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 96707fd..00aa520 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -176,6 +176,13 @@
     }
 
     @Test
+    public void testHasUserRestriction_NonExistentUserReturnsFalse() {
+        int nonExistentUserId = UserHandle.USER_NULL;
+        assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, nonExistentUserId))
+                .isFalse();
+    }
+
+    @Test
     public void testSetUserRestrictionWithIncorrectID() throws Exception {
         int incorrectId = 1;
         while (mUserManagerService.userExists(incorrectId)) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 13a7a3e..1f952c4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -23,13 +23,14 @@
 import android.content.pm.UserProperties;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -58,9 +59,15 @@
         final UserProperties defaultProps = new UserProperties.Builder()
                 .setShowInLauncher(21)
                 .setStartWithParent(false)
+                .setShowInSettings(45)
+                .setInheritDevicePolicy(67)
+                .setUseParentsContacts(false)
                 .build();
         final UserProperties actualProps = new UserProperties(defaultProps);
         actualProps.setShowInLauncher(14);
+        actualProps.setShowInSettings(32);
+        actualProps.setInheritDevicePolicy(51);
+        actualProps.setUseParentsContacts(true);
 
         // Write the properties to xml.
         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -98,10 +105,14 @@
         final UserProperties defaultProps = new UserProperties.Builder()
                 .setShowInLauncher(2145)
                 .setStartWithParent(true)
+                .setShowInSettings(3452)
+                .setInheritDevicePolicy(1732)
                 .build();
         final UserProperties orig = new UserProperties(defaultProps);
         orig.setShowInLauncher(2841);
         orig.setStartWithParent(false);
+        orig.setShowInSettings(1437);
+        orig.setInheritDevicePolicy(9456);
 
         // Test every permission level. (Currently, it's linear so it's easy.)
         for (int permLevel = 0; permLevel < 4; permLevel++) {
@@ -137,12 +148,20 @@
 
         // Items requiring exposeAll.
         assertEqualGetterOrThrows(orig::getStartWithParent, copy::getStartWithParent, exposeAll);
+        assertEqualGetterOrThrows(orig::getInheritDevicePolicy,
+                copy::getInheritDevicePolicy, exposeAll);
 
         // Items requiring hasManagePermission - put them here using hasManagePermission.
+        assertEqualGetterOrThrows(orig::getShowInSettings, copy::getShowInSettings,
+                hasManagePermission);
+        assertEqualGetterOrThrows(orig::getUseParentsContacts,
+                copy::getUseParentsContacts, hasManagePermission);
+
         // Items requiring hasQueryPermission - put them here using hasQueryPermission.
 
         // Items with no permission requirements.
         assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+
     }
 
     /**
@@ -181,5 +200,8 @@
         assertThat(expected.getPropertiesPresent()).isEqualTo(actual.getPropertiesPresent());
         assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
         assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
+        assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
+        assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
+        assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 5f48004..d7c1e37 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -83,7 +83,8 @@
                 /* flags= */0,
                 /* letsPersonalDataIntoProfile= */false).build());
         final UserProperties.Builder userProps = new UserProperties.Builder()
-                .setShowInLauncher(17);
+                .setShowInLauncher(17)
+                .setUseParentsContacts(true);
         final UserTypeDetails type = new UserTypeDetails.Builder()
                 .setName("a.name")
                 .setEnabled(1)
@@ -140,6 +141,7 @@
         }
 
         assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertTrue(type.getDefaultUserPropertiesReference().getUseParentsContacts());
 
         assertEquals(23, type.getBadgeLabel(0));
         assertEquals(24, type.getBadgeLabel(1));
@@ -182,6 +184,7 @@
         final UserProperties props = type.getDefaultUserPropertiesReference();
         assertNotNull(props);
         assertFalse(props.getStartWithParent());
+        assertFalse(props.getUseParentsContacts());
         assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
 
         assertFalse(type.hasBadge());
@@ -263,7 +266,8 @@
         final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
         final UserProperties.Builder props = new UserProperties.Builder()
                 .setShowInLauncher(19)
-                .setStartWithParent(true);
+                .setStartWithParent(true)
+                .setUseParentsContacts(true);
         final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
         builders.put(userTypeAosp1, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
@@ -289,7 +293,9 @@
         assertEquals(Resources.ID_NULL, aospType.getIconBadge());
         assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
         assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
-        assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertTrue(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertTrue(aospType.getDefaultUserPropertiesReference()
+                .getUseParentsContacts());
 
         // userTypeAosp2 should be modified.
         aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -319,7 +325,9 @@
                 makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
                 aospType.getDefaultRestrictions()));
         assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
-        assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertFalse(aospType.getDefaultUserPropertiesReference().getStartWithParent());
+        assertFalse(aospType.getDefaultUserPropertiesReference()
+                .getUseParentsContacts());
 
         // userTypeOem1 should be created.
         UserTypeDetails.Builder customType = builders.get(userTypeOem1);
@@ -347,6 +355,7 @@
         UserTypeDetails details = builders.get(userTypeFull).createUserTypeDetails();
         assertEquals(UNLIMITED_NUMBER_OF_USERS, details.getMaxAllowedPerParent());
         assertFalse(details.isEnabled());
+        assertEquals(17, details.getMaxAllowed());
         assertTrue(UserRestrictionsUtils.areEqual(
                 makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
                 details.getDefaultRestrictions()));
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index c1e778d..2e7e583 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 import static org.testng.Assert.assertThrows;
@@ -164,6 +165,14 @@
 
     @Test
     public void testCloneUser() throws Exception {
+
+        // Get the default properties for clone user type.
+        final UserTypeDetails userTypeDetails =
+                UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_CLONE);
+        assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_CLONE)
+                .that(userTypeDetails).isNotNull();
+        final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
         // Test that only one clone user can be created
         final int primaryUserId = mUserManager.getPrimaryUser().id;
         UserInfo userInfo = createProfileForUser("Clone user1",
@@ -187,6 +196,16 @@
                 .collect(Collectors.toList());
         assertThat(cloneUsers.size()).isEqualTo(1);
 
+        // Check that the new clone user has the expected properties (relative to the defaults)
+        // provided that the test caller has the necessary permissions.
+        UserProperties cloneUserProperties =
+                mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+        assertThat(typeProps.getUseParentsContacts())
+                .isEqualTo(cloneUserProperties.getUseParentsContacts());
+        assertThat(typeProps.getShowInLauncher())
+                .isEqualTo(cloneUserProperties.getShowInLauncher());
+        assertThrows(SecurityException.class, cloneUserProperties::getStartWithParent);
+
         // Verify clone user parent
         assertThat(mUserManager.getProfileParent(primaryUserId)).isNull();
         UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
@@ -599,7 +618,10 @@
         // Check that this new user has the expected properties (relative to the defaults)
         // provided that the test caller has the necessary permissions.
         assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
+        assertThat(userProps.getShowInSettings()).isEqualTo(typeProps.getShowInSettings());
+        assertFalse(userProps.getUseParentsContacts());
         assertThrows(SecurityException.class, userProps::getStartWithParent);
+        assertThrows(SecurityException.class, userProps::getInheritDevicePolicy);
     }
 
     // Make sure only max managed profiles can be created
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
index d5893c8..77d542a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
@@ -52,7 +52,6 @@
         assertFalse(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertFalse(opt.isForce());
         assertFalse(opt.isDexoptIdleBackgroundJob());
@@ -67,7 +66,6 @@
                 DexoptOptions.DEXOPT_BOOT_COMPLETE |
                 DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES |
                 DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX |
-                DexoptOptions.DEXOPT_ONLY_SHARED_DEX |
                 DexoptOptions.DEXOPT_DOWNGRADE  |
                 DexoptOptions.DEXOPT_AS_SHARED_LIBRARY |
                 DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB |
@@ -81,7 +79,6 @@
         assertTrue(opt.isBootComplete());
         assertTrue(opt.isCheckForProfileUpdates());
         assertTrue(opt.isDexoptOnlySecondaryDex());
-        assertTrue(opt.isDexoptOnlySharedDex());
         assertTrue(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertTrue(opt.isDexoptAsSharedLibrary());
@@ -113,7 +110,6 @@
             assertTrue(opt.isBootComplete());
             assertTrue(opt.isCheckForProfileUpdates());
             assertFalse(opt.isDexoptOnlySecondaryDex());
-            assertFalse(opt.isDexoptOnlySharedDex());
             assertFalse(opt.isDowngrade());
             assertTrue(opt.isForce());
             assertFalse(opt.isDexoptAsSharedLibrary());
@@ -131,7 +127,6 @@
         assertTrue(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertFalse(opt.isDexoptAsSharedLibrary());
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
index 9cd97ff3..8e6c014 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
@@ -20,21 +20,16 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.apex.ApexInfo;
 import android.content.Context;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PermissionInfo;
-import android.content.pm.SigningDetails;
 import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.content.pm.parsing.result.ParseResult;
 import android.content.pm.parsing.result.ParseTypeImpl;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.FileUtils;
-import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
 import android.util.SparseIntArray;
@@ -53,7 +48,6 @@
 import com.android.server.pm.pkg.component.ParsedIntentInfo;
 import com.android.server.pm.pkg.component.ParsedPermission;
 import com.android.server.pm.pkg.component.ParsedPermissionUtils;
-import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 
 import com.google.common.truth.Expect;
 
@@ -63,7 +57,6 @@
 
 import java.io.File;
 import java.io.InputStream;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -567,54 +560,6 @@
     }
 
     @Test
-    public void testApexPackageInfoGeneration() throws Exception {
-        String apexModuleName = "com.android.tzdata.apex";
-        File apexFile = copyRawResourceToFile(apexModuleName,
-                R.raw.com_android_tzdata);
-        ApexInfo apexInfo = new ApexInfo();
-        apexInfo.isActive = true;
-        apexInfo.isFactory = false;
-        apexInfo.moduleName = apexModuleName;
-        apexInfo.modulePath = apexFile.getPath();
-        apexInfo.versionCode = 191000070;
-        int flags = PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES;
-
-        ParseResult<ParsedPackage> result = ParsingPackageUtils.parseDefaultOneTime(apexFile,
-                flags, Collections.emptyList(), false /*collectCertificates*/);
-        if (result.isError()) {
-            throw new IllegalStateException(result.getErrorMessage(), result.getException());
-        }
-
-        ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
-        ParsedPackage pkg = result.getResult();
-        ParseResult<SigningDetails> ret = ParsingPackageUtils.getSigningDetails(
-                input, pkg, false /*skipVerify*/);
-        if (ret.isError()) {
-            throw new IllegalStateException(ret.getErrorMessage(), ret.getException());
-        }
-        pkg.setSigningDetails(ret.getResult());
-        PackageInfo pi = PackageInfoUtils.generate(pkg.setApex(true).hideAsFinal(), apexInfo,
-                flags, null, UserHandle.USER_SYSTEM);
-
-        assertEquals("com.google.android.tzdata", pi.applicationInfo.packageName);
-        assertTrue(pi.applicationInfo.enabled);
-        assertEquals(28, pi.applicationInfo.targetSdkVersion);
-        assertEquals(191000070, pi.applicationInfo.longVersionCode);
-        assertNotNull(pi.applicationInfo.metaData);
-        assertEquals(apexFile.getPath(), pi.applicationInfo.sourceDir);
-        assertEquals("Bundle[{com.android.vending.derived.apk.id=1}]",
-                pi.applicationInfo.metaData.toString());
-
-        assertEquals("com.google.android.tzdata", pi.packageName);
-        assertEquals(191000070, pi.getLongVersionCode());
-        assertNotNull(pi.signingInfo);
-        assertTrue(pi.signingInfo.getApkContentsSigners().length > 0);
-        assertTrue(pi.isApex);
-        assertTrue((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
-        assertTrue((pi.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0);
-    }
-
-    @Test
     public void testUsesSdk() throws Exception {
         ParsedPackage pkg;
         SparseIntArray minExtVers;
@@ -630,10 +575,11 @@
         assertEquals(0, minExtVers.get(31, -1));
 
         Map<Pair<String, Integer>, Integer> appToError = new HashMap<>();
-        appToError.put(Pair.create("install_uses_sdk.apk_r5", R.raw.install_uses_sdk_r5),
+        appToError.put(Pair.create("install_uses_sdk.apk_r1000", R.raw.install_uses_sdk_r1000),
                        PackageManager.INSTALL_FAILED_OLDER_SDK);
-        appToError.put(Pair.create("install_uses_sdk.apk_r0_s5", R.raw.install_uses_sdk_r0_s5),
-                       PackageManager.INSTALL_FAILED_OLDER_SDK);
+        appToError.put(
+                Pair.create("install_uses_sdk.apk_r0_s1000", R.raw.install_uses_sdk_r0_s1000),
+                PackageManager.INSTALL_FAILED_OLDER_SDK);
 
         appToError.put(Pair.create("install_uses_sdk.apk_q0", R.raw.install_uses_sdk_q0),
                        PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED);
@@ -650,7 +596,7 @@
             int result = entry.getValue();
             try {
                 parsePackage(filename, resId, x -> x);
-                expect.withMessage("Expected parsing error %d from %s", result, filename).fail();
+                expect.withMessage("Expected parsing error %s from %s", result, filename).fail();
             } catch (PackageManagerException expected) {
                 expect.that(expected.error).isEqualTo(result);
             }
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index fe4db3a..db2630e2 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -87,7 +87,6 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.power.PowerManagerService.BatteryReceiver;
@@ -147,7 +146,6 @@
     @Mock private SystemPropertiesWrapper mSystemPropertiesMock;
     @Mock private AppOpsManager mAppOpsManagerMock;
     @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock;
-    @Mock private PlatformCompat mPlatformCompat;
 
     @Mock
     private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@@ -321,11 +319,6 @@
             AppOpsManager createAppOpsManager(Context context) {
                 return mAppOpsManagerMock;
             }
-
-            @Override
-            PlatformCompat createPlatformCompat(Context context) {
-                return mPlatformCompat;
-            }
         });
         return mService;
     }
@@ -505,9 +498,6 @@
         String packageName = "pkg.name";
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_GRANTED);
@@ -532,23 +522,6 @@
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
         assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
-
-        // Verify that on older platforms only the appOp is necessary and the permission isn't
-        // checked
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(false);
-        when(mContextSpy.checkCallingOrSelfPermission(
-                android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-        forceSleep();
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
-
-        flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
-        mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
-                null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
-        mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
     }
 
     @Test
@@ -568,7 +541,7 @@
         int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
@@ -577,9 +550,6 @@
 
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_DENIED);
@@ -589,7 +559,7 @@
         flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 397770b..dcbdcdc 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -42,6 +42,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.IHintSession;
+import android.os.PerformanceHintManager;
 import android.os.Process;
 
 import com.android.server.FgThread;
@@ -250,6 +251,32 @@
     }
 
     @Test
+    public void testSendHint() throws Exception {
+        HintManagerService service = createService();
+        IBinder token = new Binder();
+
+        AppHintSession a = (AppHintSession) service.getBinderServiceInstance()
+                .createHintSession(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION);
+
+        a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
+        verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(),
+                eq(PerformanceHintManager.Session.CPU_LOAD_RESET));
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            a.sendHint(-1);
+        });
+
+        reset(mNativeWrapperMock);
+        // Set session to background, then the duration would not be updated.
+        service.mUidObserver.onUidStateChanged(
+                a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);
+        FgThread.getHandler().runWithScissors(() -> { }, 500);
+        assertFalse(a.updateHintAllowed());
+        a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET);
+        verify(mNativeWrapperMock, never()).halSendHint(anyLong(), anyInt());
+    }
+
+    @Test
     public void testDoHintInBackground() throws Exception {
         HintManagerService service = createService();
         IBinder token = new Binder();
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
index 1049274..970020f 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
@@ -27,13 +27,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.PowerProfile;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index 067f4e2..e603ea5 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -37,13 +37,14 @@
 import android.os.Parcel;
 import android.os.UidBatteryConsumer;
 import android.os.UserBatteryConsumer;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java
index 7731a32..c2556e9 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/CpuWakeupStatsTest.java
@@ -44,7 +44,9 @@
 public class CpuWakeupStatsTest {
     private static final String KERNEL_REASON_ALARM_IRQ = "120 test.alarm.device";
     private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
-    private static final String KERNEL_REASON_UNKNOWN = "unsupported-free-form-reason";
+    private static final String KERNEL_REASON_UNKNOWN = "free-form-reason test.alarm.device";
+    private static final String KERNEL_REASON_UNSUPPORTED = "-1 test.alarm.device";
+    private static final String KERNEL_REASON_ABORT = "Abort: due to test.alarm.device";
 
     private static final int TEST_UID_1 = 13239823;
     private static final int TEST_UID_2 = 25268423;
@@ -57,6 +59,7 @@
 
     @Test
     public void removesOldWakeups() {
+        // The xml resource doesn't matter for this test.
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_1);
 
         final Set<Long> timestamps = new HashSet<>();
@@ -165,11 +168,36 @@
 
         obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNKNOWN);
 
-        // Unrelated subsystems, should be ignored.
+        // Should be ignored as this type of wakeup is unsupported.
         obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
         obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4);
 
         // There should be nothing in the attribution map.
         assertThat(obj.mWakeupAttribution.size()).isEqualTo(0);
     }
+
+    @Test
+    public void unsupportedAttribution() {
+        final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3);
+
+        long wakeupTime = 970934;
+        obj.noteWakeupTimeAndReason(wakeupTime, 34, KERNEL_REASON_UNSUPPORTED);
+
+        // Should be ignored as this type of wakeup is unsupported.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 3, TEST_UID_4);
+
+        // There should be nothing in the attribution map.
+        assertThat(obj.mWakeupAttribution.size()).isEqualTo(0);
+
+        wakeupTime = 883124;
+        obj.noteWakeupTimeAndReason(wakeupTime, 3, KERNEL_REASON_ABORT);
+
+        // Should be ignored as this type of wakeup is unsupported.
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 2, TEST_UID_1, TEST_UID_4);
+        obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime - 5, TEST_UID_3);
+
+        // There should be nothing in the attribution map.
+        assertThat(obj.mWakeupAttribution.size()).isEqualTo(0);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
index 83139b0..5a482fc 100644
--- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java
@@ -44,6 +44,7 @@
 import android.content.Intent;
 import android.content.om.IOverlayManager;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
@@ -669,7 +670,10 @@
     }
 
     @Test
-    public void testSetNavBarMode_setsModeKids() throws RemoteException {
+    public void testSetNavBarMode_setsModeKids() throws Exception {
+        mContext.setMockPackageManager(mPackageManager);
+        when(mPackageManager.getPackageInfo(anyString(),
+                any(PackageManager.PackageInfoFlags.class))).thenReturn(new PackageInfo());
         int navBarModeKids = StatusBarManager.NAV_BAR_MODE_KIDS;
 
         mStatusBarManagerService.setNavBarMode(navBarModeKids);
diff --git a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
index 7ac4938..9c61d95 100644
--- a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
@@ -21,10 +21,10 @@
 import android.app.usage.CacheQuotaHint;
 import android.test.AndroidTestCase;
 import android.util.Pair;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/FakeServiceConfigAccessor.java b/services/tests/servicestests/src/com/android/server/timedetector/FakeServiceConfigAccessor.java
index a98a43b..93464cd 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/FakeServiceConfigAccessor.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/FakeServiceConfigAccessor.java
@@ -25,7 +25,7 @@
 import android.app.time.TimeCapabilitiesAndConfig;
 import android.app.time.TimeConfiguration;
 
-import com.android.server.timezonedetector.ConfigurationChangeListener;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -33,17 +33,17 @@
 /** A partially implemented, fake implementation of ServiceConfigAccessor for tests. */
 public class FakeServiceConfigAccessor implements ServiceConfigAccessor {
 
-    private final List<ConfigurationChangeListener> mConfigurationInternalChangeListeners =
+    private final List<StateChangeListener> mConfigurationInternalChangeListeners =
             new ArrayList<>();
     private ConfigurationInternal mConfigurationInternal;
 
     @Override
-    public void addConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
+    public void addConfigurationInternalChangeListener(StateChangeListener listener) {
         mConfigurationInternalChangeListeners.add(listener);
     }
 
     @Override
-    public void removeConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
+    public void removeConfigurationInternalChangeListener(StateChangeListener listener) {
         mConfigurationInternalChangeListeners.remove(listener);
     }
 
@@ -86,7 +86,7 @@
     }
 
     void simulateConfigurationChangeForTests() {
-        for (ConfigurationChangeListener listener : mConfigurationInternalChangeListeners) {
+        for (StateChangeListener listener : mConfigurationInternalChangeListeners) {
             listener.onChange();
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
index 62dae48..caef494 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -40,7 +40,7 @@
 
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timedetector.TimeDetectorStrategy.Origin;
-import com.android.server.timezonedetector.ConfigurationChangeListener;
+import com.android.server.timezonedetector.StateChangeListener;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -1821,7 +1821,7 @@
         private long mElapsedRealtimeMillis;
         private long mSystemClockMillis;
         private int mSystemClockConfidence = TIME_CONFIDENCE_LOW;
-        private ConfigurationChangeListener mConfigurationInternalChangeListener;
+        private StateChangeListener mConfigurationInternalChangeListener;
 
         // Tracking operations.
         private boolean mSystemClockWasSet;
@@ -1837,7 +1837,7 @@
         }
 
         @Override
-        public void setConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
+        public void setConfigurationInternalChangeListener(StateChangeListener listener) {
             mConfigurationInternalChangeListener = Objects.requireNonNull(listener);
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java
index 7140097..153d746 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java
@@ -57,7 +57,8 @@
     @Parameters({ "true,true", "true,false", "false,true", "false,false" })
     public void test_autoDetectionSupported_capabilitiesAndConfiguration(
             boolean userConfigAllowed, boolean bypassUserPolicyChecks) {
-        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(userConfigAllowed)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
@@ -82,10 +83,7 @@
             assertTrue(autoOnConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_GEO, autoOnConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOnConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities = autoOnConfig.asCapabilities(bypassUserPolicyChecks);
             if (userRestrictionsExpected) {
                 assertEquals(CAPABILITY_NOT_ALLOWED,
                         capabilities.getConfigureAutoDetectionEnabledCapability());
@@ -101,7 +99,7 @@
             assertEquals(CAPABILITY_POSSESSED,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOnConfig.asConfiguration();
             assertTrue(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -117,10 +115,8 @@
             assertFalse(autoOffConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_MANUAL, autoOffConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOffConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities =
+                    autoOffConfig.asCapabilities(bypassUserPolicyChecks);
             if (userRestrictionsExpected) {
                 assertEquals(CAPABILITY_NOT_ALLOWED,
                         capabilities.getConfigureAutoDetectionEnabledCapability());
@@ -136,7 +132,7 @@
             assertEquals(CAPABILITY_NOT_APPLICABLE,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOffConfig.asConfiguration();
             assertFalse(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -150,7 +146,8 @@
     @Parameters({ "true,true", "true,false", "false,true", "false,false" })
     public void test_autoDetectNotSupported_capabilitiesAndConfiguration(
             boolean userConfigAllowed, boolean bypassUserPolicyChecks) {
-        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(userConfigAllowed)
                 .setTelephonyDetectionFeatureSupported(false)
                 .setGeoDetectionFeatureSupported(false)
@@ -175,10 +172,7 @@
             assertFalse(autoOnConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_MANUAL, autoOnConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOnConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities = autoOnConfig.asCapabilities(bypassUserPolicyChecks);
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureAutoDetectionEnabledCapability());
             if (userRestrictionsExpected) {
@@ -189,7 +183,7 @@
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOnConfig.asConfiguration();
             assertTrue(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -205,10 +199,8 @@
             assertFalse(autoOffConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_MANUAL, autoOffConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOffConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities =
+                    autoOffConfig.asCapabilities(bypassUserPolicyChecks);
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureAutoDetectionEnabledCapability());
             if (userRestrictionsExpected) {
@@ -219,7 +211,7 @@
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOffConfig.asConfiguration();
             assertFalse(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -233,7 +225,8 @@
     @Parameters({ "true,true", "true,false", "false,true", "false,false" })
     public void test_geoDetectNotSupported_capabilitiesAndConfiguration(
             boolean userConfigAllowed, boolean bypassUserPolicyChecks) {
-        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(userConfigAllowed)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(false)
@@ -258,10 +251,7 @@
             assertFalse(autoOnConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_TELEPHONY, autoOnConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOnConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities = autoOnConfig.asCapabilities(bypassUserPolicyChecks);
             if (userRestrictionsExpected) {
                 assertEquals(CAPABILITY_NOT_ALLOWED,
                         capabilities.getConfigureAutoDetectionEnabledCapability());
@@ -276,7 +266,7 @@
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOnConfig.asConfiguration();
             assertTrue(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -292,10 +282,8 @@
             assertFalse(autoOffConfig.isGeoDetectionExecutionEnabled());
             assertEquals(DETECTION_MODE_MANUAL, autoOffConfig.getDetectionMode());
 
-            TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                    autoOffConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-
-            TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
+            TimeZoneCapabilities capabilities =
+                    autoOffConfig.asCapabilities(bypassUserPolicyChecks);
             if (userRestrictionsExpected) {
                 assertEquals(CAPABILITY_NOT_ALLOWED,
                         capabilities.getConfigureAutoDetectionEnabledCapability());
@@ -308,7 +296,7 @@
             assertEquals(CAPABILITY_NOT_SUPPORTED,
                     capabilities.getConfigureGeoDetectionEnabledCapability());
 
-            TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+            TimeZoneConfiguration configuration = autoOffConfig.asConfiguration();
             assertFalse(configuration.isAutoDetectionEnabled());
             assertTrue(configuration.isGeoDetectionEnabled());
         }
@@ -316,7 +304,8 @@
 
     @Test
     public void test_telephonyFallbackSupported() {
-        ConfigurationInternal config = new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        ConfigurationInternal config = new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(true)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(false)
@@ -331,7 +320,8 @@
     /** Tests when {@link ConfigurationInternal#getGeoDetectionRunInBackgroundEnabled()} is true. */
     @Test
     public void test_geoDetectionRunInBackgroundEnabled() {
-        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        ConfigurationInternal baseConfig = new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(true)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
index fdee86e..fc6afe4 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java
@@ -16,14 +16,11 @@
 
 package com.android.server.timezonedetector;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.fail;
 
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.app.time.TimeZoneCapabilities;
-import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
 
 import java.time.Duration;
@@ -31,76 +28,104 @@
 import java.util.List;
 import java.util.Optional;
 
-/** A partially implemented, fake implementation of ServiceConfigAccessor for tests. */
+/**
+ * A partially implemented, fake implementation of ServiceConfigAccessor for tests.
+ *
+ * <p>This class has rudamentary support for multiple users, but unlike the real thing, it doesn't
+ * simulate that some settings are global and shared between users. It also delivers config updates
+ * synchronously.
+ */
 public class FakeServiceConfigAccessor implements ServiceConfigAccessor {
 
-    private final List<ConfigurationChangeListener> mConfigurationInternalChangeListeners =
+    private final List<StateChangeListener> mConfigurationInternalChangeListeners =
             new ArrayList<>();
-    private ConfigurationInternal mConfigurationInternal;
+    private ConfigurationInternal mCurrentUserConfigurationInternal;
+    private ConfigurationInternal mOtherUserConfigurationInternal;
 
     @Override
-    public void addConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
+    public void addConfigurationInternalChangeListener(StateChangeListener listener) {
         mConfigurationInternalChangeListeners.add(listener);
     }
 
     @Override
-    public void removeConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
+    public void removeConfigurationInternalChangeListener(StateChangeListener listener) {
         mConfigurationInternalChangeListeners.remove(listener);
     }
 
     @Override
     public ConfigurationInternal getCurrentUserConfigurationInternal() {
-        return mConfigurationInternal;
+        return getConfigurationInternal(mCurrentUserConfigurationInternal.getUserId());
     }
 
     @Override
     public boolean updateConfiguration(
-            @UserIdInt int userID, @NonNull TimeZoneConfiguration requestedChanges,
+            @UserIdInt int userId, @NonNull TimeZoneConfiguration requestedChanges,
             boolean bypassUserPolicyChecks) {
-        assertNotNull(mConfigurationInternal);
+        assertNotNull(mCurrentUserConfigurationInternal);
         assertNotNull(requestedChanges);
 
+        ConfigurationInternal toUpdate = getConfigurationInternal(userId);
+
         // Simulate the real strategy's behavior: the new configuration will be updated to be the
-        // old configuration merged with the new if the user has the capability to up the settings.
-        // Then, if the configuration changed, the change listener is invoked.
-        TimeZoneCapabilitiesAndConfig capabilitiesAndConfig =
-                mConfigurationInternal.createCapabilitiesAndConfig(bypassUserPolicyChecks);
-        TimeZoneCapabilities capabilities = capabilitiesAndConfig.getCapabilities();
-        TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration();
+        // old configuration merged with the new if the user has the capability to update the
+        // settings. Then, if the configuration changed, the change listener is invoked.
+        TimeZoneCapabilities capabilities = toUpdate.asCapabilities(bypassUserPolicyChecks);
+        TimeZoneConfiguration configuration = toUpdate.asConfiguration();
         TimeZoneConfiguration newConfiguration =
                 capabilities.tryApplyConfigChanges(configuration, requestedChanges);
         if (newConfiguration == null) {
             return false;
         }
 
-        if (!newConfiguration.equals(capabilitiesAndConfig.getConfiguration())) {
-            mConfigurationInternal = mConfigurationInternal.merge(newConfiguration);
-
+        if (!newConfiguration.equals(configuration)) {
+            ConfigurationInternal updatedConfiguration = toUpdate.merge(newConfiguration);
+            if (updatedConfiguration.getUserId() == mCurrentUserConfigurationInternal.getUserId()) {
+                mCurrentUserConfigurationInternal = updatedConfiguration;
+            } else if (mOtherUserConfigurationInternal != null
+                    && updatedConfiguration.getUserId()
+                    == mOtherUserConfigurationInternal.getUserId()) {
+                mOtherUserConfigurationInternal = updatedConfiguration;
+            }
             // Note: Unlike the real strategy, the listeners are invoked synchronously.
-            simulateConfigurationChangeForTests();
+            notifyConfigurationChange();
         }
         return true;
     }
 
-    void initializeConfiguration(ConfigurationInternal configurationInternal) {
-        mConfigurationInternal = configurationInternal;
+    void initializeCurrentUserConfiguration(ConfigurationInternal configurationInternal) {
+        mCurrentUserConfigurationInternal = configurationInternal;
     }
 
-    void simulateConfigurationChangeForTests() {
-        for (ConfigurationChangeListener listener : mConfigurationInternalChangeListeners) {
-            listener.onChange();
-        }
+    void initializeOtherUserConfiguration(ConfigurationInternal configurationInternal) {
+        mOtherUserConfigurationInternal = configurationInternal;
+    }
+
+    void simulateCurrentUserConfigurationInternalChange(
+            ConfigurationInternal configurationInternal) {
+        mCurrentUserConfigurationInternal = configurationInternal;
+        // Note: Unlike the real strategy, the listeners are invoked synchronously.
+        notifyConfigurationChange();
+    }
+
+    void simulateOtherUserConfigurationInternalChange(ConfigurationInternal configurationInternal) {
+        mOtherUserConfigurationInternal = configurationInternal;
+        // Note: Unlike the real strategy, the listeners are invoked synchronously.
+        notifyConfigurationChange();
     }
 
     @Override
     public ConfigurationInternal getConfigurationInternal(int userId) {
-        assertEquals("Multi-user testing not supported currently",
-                userId, mConfigurationInternal.getUserId());
-        return mConfigurationInternal;
+        if (userId == mCurrentUserConfigurationInternal.getUserId()) {
+            return mCurrentUserConfigurationInternal;
+        } else if (mOtherUserConfigurationInternal != null
+                    && userId == mOtherUserConfigurationInternal.getUserId()) {
+            return mOtherUserConfigurationInternal;
+        }
+        throw new AssertionError("userId not known: " + userId);
     }
 
     @Override
-    public void addLocationTimeZoneManagerConfigListener(ConfigurationChangeListener listener) {
+    public void addLocationTimeZoneManagerConfigListener(StateChangeListener listener) {
         failUnimplemented();
     }
 
@@ -206,9 +231,14 @@
         failUnimplemented();
     }
 
+    private void notifyConfigurationChange() {
+        for (StateChangeListener listener : mConfigurationInternalChangeListeners) {
+            listener.onChange();
+        }
+    }
+
     @SuppressWarnings("UnusedReturnValue")
     private static <T> T failUnimplemented() {
-        fail("Unimplemented");
-        return null;
+        throw new AssertionError("Unimplemented");
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
index 228dc95..bcdc65c 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java
@@ -15,16 +15,39 @@
  */
 package com.android.server.timezonedetector;
 
+import static org.junit.Assert.assertEquals;
+
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
+import android.app.time.TimeZoneCapabilitiesAndConfig;
+import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.util.IndentingPrintWriter;
 
+import java.util.ArrayList;
+import java.util.Objects;
+
 public class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
 
+    private final FakeServiceConfigAccessor mFakeServiceConfigAccessor =
+            new FakeServiceConfigAccessor();
+    private final ArrayList<StateChangeListener> mListeners = new ArrayList<>();
     private TimeZoneState mTimeZoneState;
+    private TimeZoneDetectorStatus mStatus;
+
+    public FakeTimeZoneDetectorStrategy() {
+        mFakeServiceConfigAccessor.addConfigurationInternalChangeListener(
+                this::notifyChangeListeners);
+    }
+
+    public void initializeConfigurationAndStatus(
+            ConfigurationInternal configuration, TimeZoneDetectorStatus status) {
+        mFakeServiceConfigAccessor.initializeCurrentUserConfiguration(configuration);
+        mStatus = Objects.requireNonNull(status);
+    }
 
     @Override
     public boolean confirmTimeZone(String timeZoneId) {
@@ -32,6 +55,37 @@
     }
 
     @Override
+    public TimeZoneCapabilitiesAndConfig getCapabilitiesAndConfig(int userId,
+            boolean bypassUserPolicyChecks) {
+        ConfigurationInternal configurationInternal =
+                mFakeServiceConfigAccessor.getCurrentUserConfigurationInternal();
+        assertEquals("Multi-user testing not supported",
+                configurationInternal.getUserId(), userId);
+        return new TimeZoneCapabilitiesAndConfig(
+                mStatus,
+                configurationInternal.asCapabilities(bypassUserPolicyChecks),
+                configurationInternal.asConfiguration());
+    }
+
+    @Override
+    public boolean updateConfiguration(int userId, TimeZoneConfiguration requestedChanges,
+            boolean bypassUserPolicyChecks) {
+        return mFakeServiceConfigAccessor.updateConfiguration(
+                userId, requestedChanges, bypassUserPolicyChecks);
+    }
+
+    @Override
+    public void addChangeListener(StateChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    private void notifyChangeListeners() {
+        for (StateChangeListener listener : mListeners) {
+            listener.onChange();
+        }
+    }
+
+    @Override
     public TimeZoneState getTimeZoneState() {
         return mTimeZoneState;
     }
@@ -42,7 +96,7 @@
     }
 
     @Override
-    public void suggestGeolocationTimeZone(GeolocationTimeZoneSuggestion timeZoneSuggestion) {
+    public void handleLocationAlgorithmEvent(LocationAlgorithmEvent locationAlgorithmEvent) {
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java
index 0f667b3..602842a 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/GeolocationTimeZoneSuggestionTest.java
@@ -16,13 +16,8 @@
 
 package com.android.server.timezonedetector;
 
-import static com.android.server.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNull;
-
-import android.os.ShellCommand;
 
 import org.junit.Test;
 
@@ -49,11 +44,6 @@
         assertEquals(certain1v1, certain1v2);
         assertEquals(certain1v2, certain1v1);
 
-        // DebugInfo must not be considered in equals().
-        certain1v1.addDebugInfo("Debug info 1");
-        certain1v2.addDebugInfo("Debug info 2");
-        assertEquals(certain1v1, certain1v2);
-
         long time2 = 2222L;
         GeolocationTimeZoneSuggestion certain2 =
                 GeolocationTimeZoneSuggestion.createCertainSuggestion(time2, ARBITRARY_ZONE_IDS1);
@@ -71,40 +61,4 @@
         assertNotEquals(certain1v1, certain3);
         assertNotEquals(certain3, certain1v1);
     }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testParseCommandLineArg_noZoneIdsArg() {
-        ShellCommand testShellCommand =
-                createShellCommandWithArgsAndOptions(Collections.emptyList());
-        GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand);
-    }
-
-    @Test
-    public void testParseCommandLineArg_zoneIdsUncertain() {
-        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
-                "--zone_ids UNCERTAIN");
-        assertNull(GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand)
-                .getZoneIds());
-    }
-
-    @Test
-    public void testParseCommandLineArg_zoneIdsEmpty() {
-        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions("--zone_ids EMPTY");
-        assertEquals(Collections.emptyList(),
-                GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand).getZoneIds());
-    }
-
-    @Test
-    public void testParseCommandLineArg_zoneIdsPresent() {
-        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
-                "--zone_ids Europe/London,Europe/Paris");
-        assertEquals(Arrays.asList("Europe/London", "Europe/Paris"),
-                GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand).getZoneIds());
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testParseCommandLineArg_unknownArgument() {
-        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions("--bad_arg 0");
-        GeolocationTimeZoneSuggestion.parseCommandLineArg(testShellCommand);
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java
new file mode 100644
index 0000000..4c14014
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/LocationAlgorithmEventTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.timezonedetector;
+
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+
+import static com.android.server.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.os.ShellCommand;
+import android.service.timezone.TimeZoneProviderStatus;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class LocationAlgorithmEventTest {
+
+    public static final TimeZoneProviderStatus ARBITRARY_PROVIDER_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_NOT_APPLICABLE)
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                    .build();
+
+    public static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_ALGORITHM_STATUS =
+            new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                    PROVIDER_STATUS_IS_CERTAIN, ARBITRARY_PROVIDER_STATUS,
+                    PROVIDER_STATUS_NOT_PRESENT, null);
+
+    @Test
+    public void testEquals() {
+        GeolocationTimeZoneSuggestion suggestion1 =
+                GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L);
+        LocationTimeZoneAlgorithmStatus status1 = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_NOT_PRESENT, null, PROVIDER_STATUS_NOT_PRESENT, null);
+        LocationAlgorithmEvent event1v1 = new LocationAlgorithmEvent(status1, suggestion1);
+        assertEqualsAndHashCode(event1v1, event1v1);
+
+        LocationAlgorithmEvent event1v2 = new LocationAlgorithmEvent(status1, suggestion1);
+        assertEqualsAndHashCode(event1v1, event1v2);
+
+        GeolocationTimeZoneSuggestion suggestion2 =
+                GeolocationTimeZoneSuggestion.createUncertainSuggestion(2222L);
+        LocationAlgorithmEvent event2 = new LocationAlgorithmEvent(status1, suggestion2);
+        assertNotEquals(event1v1, event2);
+
+        LocationTimeZoneAlgorithmStatus status2 = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING,
+                PROVIDER_STATUS_NOT_PRESENT, null, PROVIDER_STATUS_NOT_READY, null);
+        LocationAlgorithmEvent event3 = new LocationAlgorithmEvent(status2, suggestion1);
+        assertNotEquals(event1v1, event3);
+
+        // DebugInfo must not be considered in equals().
+        event1v1.addDebugInfo("Debug info 1");
+        event1v2.addDebugInfo("Debug info 2");
+        assertEquals(event1v1, event1v2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noStatus() {
+        GeolocationTimeZoneSuggestion suggestion =
+                GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L);
+        ShellCommand testShellCommand =
+                createShellCommandWithArgsAndOptions(
+                        Arrays.asList("--suggestion", suggestion.toString()));
+
+        LocationAlgorithmEvent.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_noSuggestion() {
+        GeolocationTimeZoneSuggestion suggestion = null;
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion);
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                Arrays.asList("--status", event.getAlgorithmStatus().toString()));
+
+        assertEquals(event, LocationAlgorithmEvent.parseCommandLineArg(testShellCommand));
+    }
+
+    @Test
+    public void testParseCommandLineArg_suggestionUncertain() {
+        GeolocationTimeZoneSuggestion suggestion =
+                GeolocationTimeZoneSuggestion.createUncertainSuggestion(1111L);
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion);
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                Arrays.asList("--status", event.getAlgorithmStatus().toString(),
+                        "--suggestion", "UNCERTAIN"));
+
+        LocationAlgorithmEvent parsedEvent =
+                LocationAlgorithmEvent.parseCommandLineArg(testShellCommand);
+        assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus());
+        assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds());
+    }
+
+    @Test
+    public void testParseCommandLineArg_suggestionEmpty() {
+        GeolocationTimeZoneSuggestion suggestion =
+                GeolocationTimeZoneSuggestion.createCertainSuggestion(
+                        1111L, Collections.emptyList());
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion);
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                Arrays.asList("--status", event.getAlgorithmStatus().toString(),
+                        "--suggestion", "EMPTY"));
+
+        LocationAlgorithmEvent parsedEvent =
+                LocationAlgorithmEvent.parseCommandLineArg(testShellCommand);
+        assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus());
+        assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds());
+    }
+
+    @Test
+    public void testParseCommandLineArg_suggestionPresent() {
+        GeolocationTimeZoneSuggestion suggestion =
+                GeolocationTimeZoneSuggestion.createCertainSuggestion(
+                        1111L, Arrays.asList("Europe/London", "Europe/Paris"));
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion);
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                Arrays.asList("--status", event.getAlgorithmStatus().toString(),
+                        "--suggestion", "Europe/London,Europe/Paris"));
+
+        LocationAlgorithmEvent parsedEvent =
+                LocationAlgorithmEvent.parseCommandLineArg(testShellCommand);
+        assertEquals(event.getAlgorithmStatus(), parsedEvent.getAlgorithmStatus());
+        assertEquals(event.getSuggestion().getZoneIds(), parsedEvent.getSuggestion().getZoneIds());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        GeolocationTimeZoneSuggestion suggestion =
+                GeolocationTimeZoneSuggestion.createCertainSuggestion(
+                        1111L, Arrays.asList("Europe/London", "Europe/Paris"));
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_ALGORITHM_STATUS, suggestion);
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                Arrays.asList("--status", event.getAlgorithmStatus().toString(),
+                        "--suggestion", "Europe/London,Europe/Paris", "--bad_arg"));
+        LocationAlgorithmEvent.parseCommandLineArg(testShellCommand);
+    }
+
+    private static void assertEqualsAndHashCode(Object one, Object two) {
+        assertEquals(one, two);
+        assertEquals(two, one);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java
index 782eebf..ea801e8 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/MetricsTimeZoneDetectorStateTest.java
@@ -16,6 +16,10 @@
 
 package com.android.server.timezonedetector;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+
 import static com.android.server.timezonedetector.MetricsTimeZoneDetectorState.DETECTION_MODE_GEO;
 
 import static org.junit.Assert.assertEquals;
@@ -23,6 +27,7 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.UserIdInt;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 
@@ -31,6 +36,7 @@
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.List;
 import java.util.function.Function;
 
 /** Tests for {@link MetricsTimeZoneDetectorState}. */
@@ -38,6 +44,9 @@
 
     private static final @UserIdInt int ARBITRARY_USER_ID = 1;
     private static final @ElapsedRealtimeLong long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L;
+    private static final LocationTimeZoneAlgorithmStatus ARBITRARY_CERTAIN_STATUS =
+            new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                    PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null);
     private static final String DEVICE_TIME_ZONE_ID = "DeviceTimeZoneId";
 
     private static final ManualTimeZoneSuggestion MANUAL_TIME_ZONE_SUGGESTION =
@@ -50,11 +59,14 @@
                     .setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE)
                     .build();
 
-    private static final GeolocationTimeZoneSuggestion GEOLOCATION_TIME_ZONE_SUGGESTION =
+    public static final GeolocationTimeZoneSuggestion GEOLOCATION_SUGGESTION_CERTAIN =
             GeolocationTimeZoneSuggestion.createCertainSuggestion(
                     ARBITRARY_ELAPSED_REALTIME_MILLIS,
                     Arrays.asList("GeoTimeZoneId1", "GeoTimeZoneId2"));
 
+    private static final LocationAlgorithmEvent LOCATION_ALGORITHM_EVENT =
+            new LocationAlgorithmEvent(ARBITRARY_CERTAIN_STATUS, GEOLOCATION_SUGGESTION_CERTAIN);
+
     private final OrdinalGenerator<String> mOrdinalGenerator =
             new OrdinalGenerator<>(Function.identity());
 
@@ -68,7 +80,7 @@
         MetricsTimeZoneDetectorState metricsTimeZoneDetectorState =
                 MetricsTimeZoneDetectorState.create(mOrdinalGenerator, configurationInternal,
                         DEVICE_TIME_ZONE_ID, MANUAL_TIME_ZONE_SUGGESTION,
-                        TELEPHONY_TIME_ZONE_SUGGESTION, GEOLOCATION_TIME_ZONE_SUGGESTION);
+                        TELEPHONY_TIME_ZONE_SUGGESTION, LOCATION_ALGORITHM_EVENT);
 
         // Assert the content.
         assertCommonConfiguration(configurationInternal, metricsTimeZoneDetectorState);
@@ -88,9 +100,10 @@
         assertEquals(expectedTelephonySuggestion,
                 metricsTimeZoneDetectorState.getLatestTelephonySuggestion());
 
+        List<String> expectedZoneIds = LOCATION_ALGORITHM_EVENT.getSuggestion().getZoneIds();
         MetricsTimeZoneSuggestion expectedGeoSuggestion =
                 MetricsTimeZoneSuggestion.createCertain(
-                        GEOLOCATION_TIME_ZONE_SUGGESTION.getZoneIds().toArray(new String[0]),
+                        expectedZoneIds.toArray(new String[0]),
                         new int[] { 3, 4 });
         assertEquals(expectedGeoSuggestion,
                 metricsTimeZoneDetectorState.getLatestGeolocationSuggestion());
@@ -106,7 +119,7 @@
         MetricsTimeZoneDetectorState metricsTimeZoneDetectorState =
                 MetricsTimeZoneDetectorState.create(mOrdinalGenerator, configurationInternal,
                         DEVICE_TIME_ZONE_ID, MANUAL_TIME_ZONE_SUGGESTION,
-                        TELEPHONY_TIME_ZONE_SUGGESTION, GEOLOCATION_TIME_ZONE_SUGGESTION);
+                        TELEPHONY_TIME_ZONE_SUGGESTION, LOCATION_ALGORITHM_EVENT);
 
         // Assert the content.
         assertCommonConfiguration(configurationInternal, metricsTimeZoneDetectorState);
@@ -161,7 +174,8 @@
 
     private static ConfigurationInternal createConfigurationInternal(
             boolean enhancedMetricsCollectionEnabled) {
-        return new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        return new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setUserConfigAllowed(true)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java
index 21c9685..eb6f00c 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TestHandler.java
@@ -66,10 +66,14 @@
     /**
      * Waits for all enqueued work to be completed before returning.
      */
-    public void waitForMessagesToBeProcessed() throws InterruptedException {
+    public void waitForMessagesToBeProcessed() {
         synchronized (mMonitor) {
             if (mMessagesSent != mMessagesProcessed) {
-                mMonitor.wait();
+                try {
+                    mMonitor.wait();
+                } catch (InterruptedException e) {
+                    throw new AssertionError("Unexpected exception", e);
+                }
             }
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java
index 276fdb9..a02c8ca 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java
@@ -16,14 +16,22 @@
 
 package com.android.server.timezonedetector;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.TelephonyTimeZoneAlgorithmStatus;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.content.Context;
 import android.os.HandlerThread;
@@ -41,6 +49,15 @@
 @RunWith(AndroidJUnit4.class)
 public class TimeZoneDetectorInternalImplTest {
 
+    private static final TelephonyTimeZoneAlgorithmStatus ARBITRARY_TELEPHONY_STATUS =
+            new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING);
+    private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_CERTAIN_STATUS =
+            new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                    PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null);
+    private static final TimeZoneDetectorStatus ARBITRARY_DETECTOR_STATUS =
+            new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING, ARBITRARY_TELEPHONY_STATUS,
+                    ARBITRARY_LOCATION_CERTAIN_STATUS);
+
     private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L;
     private static final String ARBITRARY_ZONE_ID = "TestZoneId";
     private static final List<String> ARBITRARY_ZONE_IDS = Arrays.asList(ARBITRARY_ZONE_ID);
@@ -50,7 +67,6 @@
     private HandlerThread mHandlerThread;
     private TestHandler mTestHandler;
     private TestCurrentUserIdentityInjector mTestCurrentUserIdentityInjector;
-    private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private FakeTimeZoneDetectorStrategy mFakeTimeZoneDetectorStrategySpy;
 
     private TimeZoneDetectorInternalImpl mTimeZoneDetectorInternal;
@@ -65,12 +81,11 @@
         mTestHandler = new TestHandler(mHandlerThread.getLooper());
         mTestCurrentUserIdentityInjector = new TestCurrentUserIdentityInjector();
         mTestCurrentUserIdentityInjector.initializeCurrentUserId(ARBITRARY_USER_ID);
-        mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor());
         mFakeTimeZoneDetectorStrategySpy = spy(new FakeTimeZoneDetectorStrategy());
 
         mTimeZoneDetectorInternal = new TimeZoneDetectorInternalImpl(
                 mMockContext, mTestHandler, mTestCurrentUserIdentityInjector,
-                mFakeServiceConfigAccessorSpy, mFakeTimeZoneDetectorStrategySpy);
+                mFakeTimeZoneDetectorStrategySpy);
     }
 
     @After
@@ -83,17 +98,22 @@
     public void testGetCapabilitiesAndConfigForDpm() throws Exception {
         final boolean autoDetectionEnabled = true;
         ConfigurationInternal testConfig = createConfigurationInternal(autoDetectionEnabled);
-        mFakeServiceConfigAccessorSpy.initializeConfiguration(testConfig);
+        TimeZoneDetectorStatus testStatus = ARBITRARY_DETECTOR_STATUS;
+        mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(testConfig, testStatus);
 
         TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig =
                 mTimeZoneDetectorInternal.getCapabilitiesAndConfigForDpm();
 
         int expectedUserId = mTestCurrentUserIdentityInjector.getCurrentUserId();
-        verify(mFakeServiceConfigAccessorSpy).getConfigurationInternal(expectedUserId);
+        final boolean expectedBypassUserPolicyChecks = true;
+        verify(mFakeTimeZoneDetectorStrategySpy).getCapabilitiesAndConfig(
+                expectedUserId, expectedBypassUserPolicyChecks);
 
-        final boolean bypassUserPolicyChecks = true;
         TimeZoneCapabilitiesAndConfig expectedCapabilitiesAndConfig =
-                testConfig.createCapabilitiesAndConfig(bypassUserPolicyChecks);
+                new TimeZoneCapabilitiesAndConfig(
+                        testStatus,
+                        testConfig.asCapabilities(expectedBypassUserPolicyChecks),
+                        testConfig.asConfiguration());
         assertEquals(expectedCapabilitiesAndConfig, actualCapabilitiesAndConfig);
     }
 
@@ -102,7 +122,9 @@
         final boolean autoDetectionEnabled = false;
         ConfigurationInternal initialConfigurationInternal =
                 createConfigurationInternal(autoDetectionEnabled);
-        mFakeServiceConfigAccessorSpy.initializeConfiguration(initialConfigurationInternal);
+        TimeZoneDetectorStatus testStatus = ARBITRARY_DETECTOR_STATUS;
+        mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(
+                initialConfigurationInternal, testStatus);
 
         TimeZoneConfiguration timeConfiguration = new TimeZoneConfiguration.Builder()
                 .setAutoDetectionEnabled(true)
@@ -110,7 +132,7 @@
         assertTrue(mTimeZoneDetectorInternal.updateConfigurationForDpm(timeConfiguration));
 
         final boolean expectedBypassUserPolicyChecks = true;
-        verify(mFakeServiceConfigAccessorSpy).updateConfiguration(
+        verify(mFakeTimeZoneDetectorStrategySpy).updateConfiguration(
                 mTestCurrentUserIdentityInjector.getCurrentUserId(),
                 timeConfiguration,
                 expectedBypassUserPolicyChecks);
@@ -130,13 +152,15 @@
     }
 
     @Test
-    public void testSuggestGeolocationTimeZone() throws Exception {
+    public void testHandleLocationAlgorithmEvent() throws Exception {
         GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion();
-        mTimeZoneDetectorInternal.suggestGeolocationTimeZone(timeZoneSuggestion);
+        LocationAlgorithmEvent suggestionEvent = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion);
+        mTimeZoneDetectorInternal.handleLocationAlgorithmEvent(suggestionEvent);
         mTestHandler.assertTotalMessagesEnqueued(1);
 
         mTestHandler.waitForMessagesToBeProcessed();
-        verify(mFakeTimeZoneDetectorStrategySpy).suggestGeolocationTimeZone(timeZoneSuggestion);
+        verify(mFakeTimeZoneDetectorStrategySpy).handleLocationAlgorithmEvent(suggestionEvent);
     }
     private static ManualTimeZoneSuggestion createManualTimeZoneSuggestion() {
         return new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID);
@@ -148,7 +172,8 @@
     }
 
     private static ConfigurationInternal createConfigurationInternal(boolean autoDetectionEnabled) {
-        return new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        return new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
                 .setTelephonyFallbackSupported(false)
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java
index bb9d564..d9d8053 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java
@@ -16,6 +16,11 @@
 
 package com.android.server.timezonedetector;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -34,8 +39,11 @@
 import static org.mockito.Mockito.when;
 
 import android.app.time.ITimeZoneDetectorListener;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.TelephonyTimeZoneAlgorithmStatus;
 import android.app.time.TimeZoneCapabilitiesAndConfig;
 import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
@@ -59,6 +67,13 @@
 @RunWith(AndroidJUnit4.class)
 public class TimeZoneDetectorServiceTest {
 
+    private static final LocationTimeZoneAlgorithmStatus ARBITRARY_LOCATION_CERTAIN_STATUS =
+            new LocationTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING,
+                    PROVIDER_STATUS_IS_CERTAIN, null, PROVIDER_STATUS_NOT_PRESENT, null);
+    private static final TimeZoneDetectorStatus ARBITRARY_DETECTOR_STATUS =
+            new TimeZoneDetectorStatus(DETECTOR_STATUS_RUNNING,
+                    new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING),
+                    ARBITRARY_LOCATION_CERTAIN_STATUS);
     private static final int ARBITRARY_USER_ID = 9999;
     private static final List<String> ARBITRARY_TIME_ZONE_IDS = Arrays.asList("TestZoneId");
     private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234L;
@@ -69,7 +84,6 @@
     private HandlerThread mHandlerThread;
     private TestHandler mTestHandler;
     private TestCallerIdentityInjector mTestCallerIdentityInjector;
-    private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private FakeTimeZoneDetectorStrategy mFakeTimeZoneDetectorStrategySpy;
 
 
@@ -85,12 +99,11 @@
         mTestCallerIdentityInjector = new TestCallerIdentityInjector();
         mTestCallerIdentityInjector.initializeCallingUserId(ARBITRARY_USER_ID);
 
-        mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor());
         mFakeTimeZoneDetectorStrategySpy = spy(new FakeTimeZoneDetectorStrategy());
 
         mTimeZoneDetectorService = new TimeZoneDetectorService(
                 mMockContext, mTestHandler, mTestCallerIdentityInjector,
-                mFakeServiceConfigAccessorSpy, mFakeTimeZoneDetectorStrategySpy);
+                mFakeTimeZoneDetectorStrategySpy);
     }
 
     @After
@@ -115,7 +128,8 @@
 
         ConfigurationInternal configuration =
                 createConfigurationInternal(true /* autoDetectionEnabled*/);
-        mFakeServiceConfigAccessorSpy.initializeConfiguration(configuration);
+        mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(configuration,
+                ARBITRARY_DETECTOR_STATUS);
 
         TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig =
                 mTimeZoneDetectorService.getCapabilitiesAndConfig();
@@ -124,11 +138,15 @@
                 eq(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION), anyString());
 
         int expectedUserId = mTestCallerIdentityInjector.getCallingUserId();
-        verify(mFakeServiceConfigAccessorSpy).getConfigurationInternal(expectedUserId);
-
         boolean expectedBypassUserPolicyChecks = false;
+        verify(mFakeTimeZoneDetectorStrategySpy)
+                .getCapabilitiesAndConfig(expectedUserId, expectedBypassUserPolicyChecks);
+
         TimeZoneCapabilitiesAndConfig expectedCapabilitiesAndConfig =
-                configuration.createCapabilitiesAndConfig(expectedBypassUserPolicyChecks);
+                new TimeZoneCapabilitiesAndConfig(
+                        ARBITRARY_DETECTOR_STATUS,
+                        configuration.asCapabilities(expectedBypassUserPolicyChecks),
+                        configuration.asConfiguration());
         assertEquals(expectedCapabilitiesAndConfig, actualCapabilitiesAndConfig);
     }
 
@@ -160,7 +178,9 @@
     public void testListenerRegistrationAndCallbacks() throws Exception {
         ConfigurationInternal initialConfiguration =
                 createConfigurationInternal(false /* autoDetectionEnabled */);
-        mFakeServiceConfigAccessorSpy.initializeConfiguration(initialConfiguration);
+
+        mFakeTimeZoneDetectorStrategySpy.initializeConfigurationAndStatus(
+                initialConfiguration, ARBITRARY_DETECTOR_STATUS);
 
         IBinder mockListenerBinder = mock(IBinder.class);
         ITimeZoneDetectorListener mockListener = mock(ITimeZoneDetectorListener.class);
@@ -230,31 +250,35 @@
     }
 
     @Test
-    public void testSuggestGeolocationTimeZone_withoutPermission() {
+    public void testHandleLocationAlgorithmEvent_withoutPermission() {
         doThrow(new SecurityException("Mock"))
                 .when(mMockContext).enforceCallingPermission(anyString(), any());
         GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion();
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion);
 
         assertThrows(SecurityException.class,
-                () -> mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion));
+                () -> mTimeZoneDetectorService.handleLocationAlgorithmEvent(event));
         verify(mMockContext).enforceCallingPermission(
                 eq(android.Manifest.permission.SET_TIME_ZONE), anyString());
     }
 
     @Test
-    public void testSuggestGeolocationTimeZone() throws Exception {
+    public void testHandleLocationAlgorithmEvent() throws Exception {
         doNothing().when(mMockContext).enforceCallingPermission(anyString(), any());
 
         GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion();
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(
+                ARBITRARY_LOCATION_CERTAIN_STATUS, timeZoneSuggestion);
 
-        mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion);
+        mTimeZoneDetectorService.handleLocationAlgorithmEvent(event);
         mTestHandler.assertTotalMessagesEnqueued(1);
 
         verify(mMockContext).enforceCallingPermission(
                 eq(android.Manifest.permission.SET_TIME_ZONE), anyString());
 
         mTestHandler.waitForMessagesToBeProcessed();
-        verify(mFakeTimeZoneDetectorStrategySpy).suggestGeolocationTimeZone(timeZoneSuggestion);
+        verify(mFakeTimeZoneDetectorStrategySpy).handleLocationAlgorithmEvent(event);
     }
 
     @Test
@@ -455,7 +479,8 @@
         // Default geo detection settings from auto detection settings - they are not important to
         // the tests.
         final boolean geoDetectionEnabled = autoDetectionEnabled;
-        return new ConfigurationInternal.Builder(ARBITRARY_USER_ID)
+        return new ConfigurationInternal.Builder()
+                .setUserId(ARBITRARY_USER_ID)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
                 .setTelephonyFallbackSupported(false)
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
index d0a7c92..b991c5a 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java
@@ -16,6 +16,12 @@
 
 package com.android.server.timezonedetector;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_CERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_IS_UNCERTAIN;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT;
+import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_READY;
 import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID;
 import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET;
 import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY;
@@ -35,22 +41,35 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
+import android.app.time.LocationTimeZoneAlgorithmStatus;
+import android.app.time.TelephonyTimeZoneAlgorithmStatus;
+import android.app.time.TimeZoneCapabilitiesAndConfig;
+import android.app.time.TimeZoneConfiguration;
+import android.app.time.TimeZoneDetectorStatus;
 import android.app.time.TimeZoneState;
 import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType;
 import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality;
+import android.os.HandlerThread;
 
 import com.android.server.SystemTimeZone.TimeZoneConfidence;
 import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -97,7 +116,8 @@
     };
 
     private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(true)
                     .setGeoDetectionFeatureSupported(true)
                     .setTelephonyFallbackSupported(false)
@@ -110,7 +130,8 @@
                     .build();
 
     private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(true)
                     .setGeoDetectionFeatureSupported(true)
                     .setTelephonyFallbackSupported(false)
@@ -123,7 +144,8 @@
                     .build();
 
     private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(false)
                     .setGeoDetectionFeatureSupported(false)
                     .setTelephonyFallbackSupported(false)
@@ -136,7 +158,8 @@
                     .build();
 
     private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(true)
                     .setGeoDetectionFeatureSupported(true)
                     .setTelephonyFallbackSupported(false)
@@ -149,7 +172,8 @@
                     .build();
 
     private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(true)
                     .setGeoDetectionFeatureSupported(true)
                     .setTelephonyFallbackSupported(false)
@@ -162,7 +186,8 @@
                     .build();
 
     private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED =
-            new ConfigurationInternal.Builder(USER_ID)
+            new ConfigurationInternal.Builder()
+                    .setUserId(USER_ID)
                     .setTelephonyDetectionFeatureSupported(true)
                     .setGeoDetectionFeatureSupported(true)
                     .setTelephonyFallbackSupported(false)
@@ -174,14 +199,216 @@
                     .setGeoDetectionEnabledSetting(true)
                     .build();
 
-    private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
+    private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS =
+            new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING);
+
+    private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private FakeEnvironment mFakeEnvironment;
+    private HandlerThread mHandlerThread;
+    private TestHandler mTestHandler;
+
+    private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy;
 
     @Before
     public void setUp() {
         mFakeEnvironment = new FakeEnvironment();
-        mFakeEnvironment.initializeConfig(CONFIG_AUTO_DISABLED_GEO_DISABLED);
-        mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(mFakeEnvironment);
+        mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor());
+        mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration(
+                CONFIG_AUTO_DISABLED_GEO_DISABLED);
+
+        // Create a thread + handler for processing the work that the strategy posts.
+        mHandlerThread = new HandlerThread("TimeZoneDetectorStrategyImplTest");
+        mHandlerThread.start();
+        mTestHandler = new TestHandler(mHandlerThread.getLooper());
+        mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(
+                mFakeServiceConfigAccessorSpy, mTestHandler, mFakeEnvironment);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+        mHandlerThread.join();
+    }
+
+    @Test
+    public void testChangeListenerBehavior_currentUser() throws Exception {
+        ConfigurationInternal currentUserConfig = CONFIG_AUTO_DISABLED_GEO_DISABLED;
+        // The strategy initializes itself with the current user's config during construction.
+        assertEquals(currentUserConfig,
+                mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
+        mTimeZoneDetectorStrategy.addChangeListener(stateChangeListener);
+
+        boolean bypassUserPolicyChecks = false;
+
+        // Report a config change, but not one that actually changes anything.
+        {
+            mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange(
+                    CONFIG_AUTO_DISABLED_GEO_DISABLED);
+            assertStateChangeNotificationsSent(stateChangeListener, 0);
+            assertEquals(CONFIG_AUTO_DISABLED_GEO_DISABLED,
+                    mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+        }
+
+        // Report a config change that actually changes something.
+        {
+            mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange(
+                    CONFIG_AUTO_ENABLED_GEO_ENABLED);
+            assertStateChangeNotificationsSent(stateChangeListener, 1);
+            assertEquals(CONFIG_AUTO_ENABLED_GEO_ENABLED,
+                    mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+        }
+
+        // Perform a (current user) update via the strategy.
+        {
+            TimeZoneConfiguration requestedChanges =
+                    new TimeZoneConfiguration.Builder().setGeoDetectionEnabled(false).build();
+            mTimeZoneDetectorStrategy.updateConfiguration(
+                    USER_ID, requestedChanges, bypassUserPolicyChecks);
+            assertStateChangeNotificationsSent(stateChangeListener, 1);
+        }
+    }
+
+    // Perform a (not current user) update via the strategy. There's no listener behavior for
+    // updates to "other" users.
+    @Test
+    public void testChangeListenerBehavior_otherUser() throws Exception {
+        ConfigurationInternal currentUserConfig = CONFIG_AUTO_DISABLED_GEO_DISABLED;
+        // The strategy initializes itself with the current user's config during construction.
+        assertEquals(currentUserConfig,
+                mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
+        mTimeZoneDetectorStrategy.addChangeListener(stateChangeListener);
+
+        boolean bypassUserPolicyChecks = false;
+
+        int otherUserId = currentUserConfig.getUserId() + 1;
+        ConfigurationInternal otherUserConfig = new ConfigurationInternal.Builder(currentUserConfig)
+                .setUserId(otherUserId)
+                .setGeoDetectionEnabledSetting(true)
+                .build();
+        mFakeServiceConfigAccessorSpy.initializeOtherUserConfiguration(otherUserConfig);
+
+        TimeZoneConfiguration requestedChanges =
+                new TimeZoneConfiguration.Builder().setGeoDetectionEnabled(false).build();
+        mTimeZoneDetectorStrategy.updateConfiguration(
+                otherUserId, requestedChanges, bypassUserPolicyChecks);
+
+        // Only changes to the current user's config are notified.
+        assertStateChangeNotificationsSent(stateChangeListener, 0);
+    }
+
+    // Current user behavior: the strategy caches and returns the latest configuration.
+    @Test
+    public void testReadAndWriteConfiguration_currentUser() throws Exception {
+        ConfigurationInternal currentUserConfig = CONFIG_AUTO_ENABLED_GEO_DISABLED;
+        mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange(
+                currentUserConfig);
+
+        int otherUserId = currentUserConfig.getUserId() + 1;
+        ConfigurationInternal otherUserConfig = new ConfigurationInternal.Builder(currentUserConfig)
+                .setUserId(otherUserId)
+                .setGeoDetectionEnabledSetting(true)
+                .build();
+        mFakeServiceConfigAccessorSpy.simulateOtherUserConfigurationInternalChange(otherUserConfig);
+        reset(mFakeServiceConfigAccessorSpy);
+
+        final boolean bypassUserPolicyChecks = false;
+
+        ConfigurationInternal cachedConfigurationInternal =
+                mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests();
+        assertEquals(currentUserConfig, cachedConfigurationInternal);
+
+        // Confirm getCapabilitiesAndConfig() does not call through to the ServiceConfigAccessor.
+        {
+            reset(mFakeServiceConfigAccessorSpy);
+            TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig =
+                    mTimeZoneDetectorStrategy.getCapabilitiesAndConfig(
+                            currentUserConfig.getUserId(), bypassUserPolicyChecks);
+            verify(mFakeServiceConfigAccessorSpy, never()).getConfigurationInternal(
+                    currentUserConfig.getUserId());
+
+            assertEquals(currentUserConfig.asCapabilities(bypassUserPolicyChecks),
+                    actualCapabilitiesAndConfig.getCapabilities());
+            assertEquals(currentUserConfig.asConfiguration(),
+                    actualCapabilitiesAndConfig.getConfiguration());
+        }
+
+        // Confirm updateConfiguration() calls through to the ServiceConfigAccessor and updates
+        // the cached copy.
+        {
+            boolean newGeoDetectionEnabled =
+                    !cachedConfigurationInternal.asConfiguration().isGeoDetectionEnabled();
+            TimeZoneConfiguration requestedChanges = new TimeZoneConfiguration.Builder()
+                    .setGeoDetectionEnabled(newGeoDetectionEnabled)
+                    .build();
+            ConfigurationInternal expectedConfigAfterChange =
+                    new ConfigurationInternal.Builder(cachedConfigurationInternal)
+                            .setGeoDetectionEnabledSetting(newGeoDetectionEnabled)
+                            .build();
+
+            reset(mFakeServiceConfigAccessorSpy);
+            mTimeZoneDetectorStrategy.updateConfiguration(
+                    currentUserConfig.getUserId(), requestedChanges, bypassUserPolicyChecks);
+            verify(mFakeServiceConfigAccessorSpy, times(1)).updateConfiguration(
+                    currentUserConfig.getUserId(), requestedChanges, bypassUserPolicyChecks);
+            assertEquals(expectedConfigAfterChange,
+                    mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+        }
+    }
+
+    // Not current user behavior: the strategy reads from the ServiceConfigAccessor.
+    @Test
+    public void testReadAndWriteConfiguration_otherUser() throws Exception {
+        ConfigurationInternal currentUserConfig = CONFIG_AUTO_ENABLED_GEO_DISABLED;
+        mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange(
+                currentUserConfig);
+
+        int otherUserId = currentUserConfig.getUserId() + 1;
+        ConfigurationInternal otherUserConfig = new ConfigurationInternal.Builder(currentUserConfig)
+                .setUserId(otherUserId)
+                .setGeoDetectionEnabledSetting(true)
+                .build();
+        mFakeServiceConfigAccessorSpy.simulateOtherUserConfigurationInternalChange(otherUserConfig);
+        reset(mFakeServiceConfigAccessorSpy);
+
+        final boolean bypassUserPolicyChecks = false;
+
+        // Confirm getCapabilitiesAndConfig() does not call through to the ServiceConfigAccessor.
+        {
+            reset(mFakeServiceConfigAccessorSpy);
+            TimeZoneCapabilitiesAndConfig actualCapabilitiesAndConfig =
+                    mTimeZoneDetectorStrategy.getCapabilitiesAndConfig(
+                            otherUserId, bypassUserPolicyChecks);
+            verify(mFakeServiceConfigAccessorSpy, times(1)).getConfigurationInternal(otherUserId);
+
+            assertEquals(otherUserConfig.asCapabilities(bypassUserPolicyChecks),
+                    actualCapabilitiesAndConfig.getCapabilities());
+            assertEquals(otherUserConfig.asConfiguration(),
+                    actualCapabilitiesAndConfig.getConfiguration());
+        }
+
+        // Confirm updateConfiguration() calls through to the ServiceConfigAccessor and doesn't
+        // touch the cached copy.
+        {
+            ConfigurationInternal cachedConfigBeforeChange =
+                    mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests();
+            boolean newGeoDetectionEnabled =
+                    !otherUserConfig.asConfiguration().isGeoDetectionEnabled();
+            TimeZoneConfiguration requestedChanges = new TimeZoneConfiguration.Builder()
+                    .setGeoDetectionEnabled(newGeoDetectionEnabled)
+                    .build();
+
+            reset(mFakeServiceConfigAccessorSpy);
+            mTimeZoneDetectorStrategy.updateConfiguration(
+                    currentUserConfig.getUserId(), requestedChanges, bypassUserPolicyChecks);
+            verify(mFakeServiceConfigAccessorSpy, times(1)).updateConfiguration(
+                    currentUserConfig.getUserId(), requestedChanges, bypassUserPolicyChecks);
+            assertEquals(cachedConfigBeforeChange,
+                    mTimeZoneDetectorStrategy.getCachedCapabilitiesAndConfigForTests());
+        }
     }
 
     @Test
@@ -202,9 +429,9 @@
         QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex1ScoredSuggestion =
                 new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion,
                         TELEPHONY_SCORE_NONE);
-        assertEquals(expectedSlotIndex1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-        assertNull(mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+        script.verifyLatestQualifiedTelephonySuggestionReceived(
+                SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion)
+                .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null);
         assertEquals(expectedSlotIndex1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
 
@@ -215,10 +442,10 @@
         QualifiedTelephonyTimeZoneSuggestion expectedSlotIndex2ScoredSuggestion =
                 new QualifiedTelephonyTimeZoneSuggestion(slotIndex2TimeZoneSuggestion,
                         TELEPHONY_SCORE_NONE);
-        assertEquals(expectedSlotIndex1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-        assertEquals(expectedSlotIndex2ScoredSuggestion,
-                mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+        script.verifyLatestQualifiedTelephonySuggestionReceived(
+                        SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion)
+                .verifyLatestQualifiedTelephonySuggestionReceived(
+                        SLOT_INDEX2, expectedSlotIndex2ScoredSuggestion);
         // SlotIndex1 should always beat slotIndex2, all other things being equal.
         assertEquals(expectedSlotIndex1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
@@ -253,8 +480,8 @@
             QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
                     new QualifiedTelephonyTimeZoneSuggestion(
                             lowQualitySuggestion, testCase.expectedScore);
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
         }
@@ -270,8 +497,8 @@
             QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
                     new QualifiedTelephonyTimeZoneSuggestion(
                             goodQualitySuggestion, testCase2.expectedScore);
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
         }
@@ -287,8 +514,8 @@
             QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
                     new QualifiedTelephonyTimeZoneSuggestion(
                             lowQualitySuggestion2, testCase.expectedScore);
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
         }
@@ -319,8 +546,8 @@
             // Assert internal service state.
             QualifiedTelephonyTimeZoneSuggestion expectedScoredSuggestion =
                     new QualifiedTelephonyTimeZoneSuggestion(suggestion, testCase.expectedScore);
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
 
@@ -336,8 +563,8 @@
             }
 
             // Assert internal service state.
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
 
@@ -346,8 +573,8 @@
                     .verifyTimeZoneNotChanged();
 
             // Assert internal service state.
-            assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedScoredSuggestion);
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
         }
@@ -398,8 +625,8 @@
         }
 
         // Assert internal service state.
-        assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
+        script.verifyLatestQualifiedTelephonySuggestionReceived(
+                SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion);
         assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
     }
@@ -453,10 +680,10 @@
             }
 
             // Assert internal service state.
-            assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-            assertEquals(expectedEmptySlotIndex2ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                    SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion)
+                    .verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion);
             assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
 
@@ -466,10 +693,10 @@
             script.verifyTimeZoneNotChanged();
 
             // Assert internal service state.
-            assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-            assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion)
+                    .verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX2, expectedZoneSlotIndex2ScoredSuggestion);
             // SlotIndex1 should always beat slotIndex2, all other things being equal.
             assertEquals(expectedZoneSlotIndex1ScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
@@ -485,20 +712,20 @@
             }
 
             // Assert internal service state.
-            assertEquals(expectedEmptySlotIndex1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-            assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX1, expectedEmptySlotIndex1ScoredSuggestion)
+                    .verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX2, expectedZoneSlotIndex2ScoredSuggestion);
             assertEquals(expectedZoneSlotIndex2ScoredSuggestion,
                     mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests());
 
             // Reset the state for the next loop.
             script.simulateTelephonyTimeZoneSuggestion(emptySlotIndex2Suggestion)
                     .verifyTimeZoneNotChanged();
-            assertEquals(expectedEmptySlotIndex1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1));
-            assertEquals(expectedEmptySlotIndex2ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX2));
+            script.verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX1, expectedEmptySlotIndex1ScoredSuggestion)
+                    .verifyLatestQualifiedTelephonySuggestionReceived(
+                            SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion);
         }
     }
 
@@ -642,53 +869,185 @@
     }
 
     @Test
-    public void testGeoSuggestion_uncertain() {
+    public void testLocationAlgorithmEvent_statusChangesOnly() {
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
         Script script = new Script()
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
                 .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
-                .resetConfigurationTracking();
+                .resetConfigurationTracking()
+                .registerStateChangeListener(stateChangeListener);
 
-        GeolocationTimeZoneSuggestion uncertainSuggestion = createUncertainGeolocationSuggestion();
+        TimeZoneDetectorStatus expectedInitialDetectorStatus = new TimeZoneDetectorStatus(
+                DETECTOR_STATUS_RUNNING,
+                TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                LocationTimeZoneAlgorithmStatus.UNKNOWN);
+        script.verifyCachedDetectorStatus(expectedInitialDetectorStatus);
 
-        script.simulateGeolocationTimeZoneSuggestion(uncertainSuggestion)
-                .verifyTimeZoneNotChanged();
+        LocationTimeZoneAlgorithmStatus algorithmStatus1 = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_NOT_READY, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        LocationTimeZoneAlgorithmStatus algorithmStatus2 = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_NOT_PRESENT, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        assertNotEquals(algorithmStatus1, algorithmStatus2);
 
-        // Assert internal service state.
-        assertEquals(uncertainSuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        {
+            LocationAlgorithmEvent locationAlgorithmEvent =
+                    new LocationAlgorithmEvent(algorithmStatus1, null);
+            script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneNotChanged();
+
+            assertStateChangeNotificationsSent(stateChangeListener, 1);
+
+            // Assert internal service state.
+            TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus(
+                    DETECTOR_STATUS_RUNNING,
+                    TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                    algorithmStatus1);
+            script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                    .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+            // Repeat the event to demonstrate the state change notifier is not triggered.
+            script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneNotChanged();
+
+            assertStateChangeNotificationsSent(stateChangeListener, 0);
+
+            // Assert internal service state.
+            script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                    .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+        }
+
+        {
+            LocationAlgorithmEvent locationAlgorithmEvent =
+                    new LocationAlgorithmEvent(algorithmStatus2, null);
+            script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneNotChanged();
+
+            assertStateChangeNotificationsSent(stateChangeListener, 1);
+
+            // Assert internal service state.
+            TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus(
+                    DETECTOR_STATUS_RUNNING,
+                    TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                    algorithmStatus2);
+            script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                    .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+            // Repeat the event to demonstrate the state change notifier is not triggered.
+            script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneNotChanged();
+
+            assertStateChangeNotificationsSent(stateChangeListener, 0);
+
+            // Assert internal service state.
+            script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                    .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+        }
     }
 
     @Test
-    public void testGeoSuggestion_noZones() {
+    public void testLocationAlgorithmEvent_uncertain() {
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
         Script script = new Script()
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
                 .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
-                .resetConfigurationTracking();
+                .resetConfigurationTracking()
+                .registerStateChangeListener(stateChangeListener);
 
-        GeolocationTimeZoneSuggestion noZonesSuggestion = createCertainGeolocationSuggestion();
-
-        script.simulateGeolocationTimeZoneSuggestion(noZonesSuggestion)
+        LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                 .verifyTimeZoneNotChanged();
 
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
+
         // Assert internal service state.
-        assertEquals(noZonesSuggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus(
+                DETECTOR_STATUS_RUNNING,
+                TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                locationAlgorithmEvent.getAlgorithmStatus());
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        // Repeat the event to demonstrate the state change notifier is not triggered.
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneNotChanged();
+
+        // Detector remains running and location algorithm is still uncertain so nothing to report.
+        assertStateChangeNotificationsSent(stateChangeListener, 0);
+
+        // Assert internal service state.
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
     }
 
     @Test
-    public void testGeoSuggestion_oneZone() {
-        GeolocationTimeZoneSuggestion suggestion =
-                createCertainGeolocationSuggestion("Europe/London");
-
+    public void testLocationAlgorithmEvent_noZones() {
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
         Script script = new Script()
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
                 .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
-                .resetConfigurationTracking();
+                .resetConfigurationTracking()
+                .registerStateChangeListener(stateChangeListener);
 
-        script.simulateGeolocationTimeZoneSuggestion(suggestion)
-                .verifyTimeZoneChangedAndReset(suggestion);
+        LocationAlgorithmEvent locationAlgorithmEvent = createCertainLocationAlgorithmEvent();
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneNotChanged();
+
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
 
         // Assert internal service state.
-        assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus(
+                DETECTOR_STATUS_RUNNING,
+                TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                locationAlgorithmEvent.getAlgorithmStatus());
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        // Repeat the event to demonstrate the state change notifier is not triggered.
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneNotChanged();
+
+        assertStateChangeNotificationsSent(stateChangeListener, 0);
+
+        // Assert internal service state.
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+    }
+
+    @Test
+    public void testLocationAlgorithmEvent_oneZone() {
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
+        Script script = new Script()
+                .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
+                .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
+                .resetConfigurationTracking()
+                .registerStateChangeListener(stateChangeListener);
+
+        LocationAlgorithmEvent locationAlgorithmEvent =
+                createCertainLocationAlgorithmEvent("Europe/London");
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneChangedAndReset(locationAlgorithmEvent);
+
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
+
+        // Assert internal service state.
+        TimeZoneDetectorStatus expectedDetectorStatus = new TimeZoneDetectorStatus(
+                DETECTOR_STATUS_RUNNING,
+                TELEPHONY_ALGORITHM_RUNNING_STATUS,
+                locationAlgorithmEvent.getAlgorithmStatus());
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
+
+        // Repeat the event to demonstrate the state change notifier is not triggered.
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneNotChanged();
+
+        assertStateChangeNotificationsSent(stateChangeListener, 0);
+
+        // Assert internal service state.
+        script.verifyCachedDetectorStatus(expectedDetectorStatus)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
     }
 
     /**
@@ -697,41 +1056,35 @@
      * set to until that unambiguously can't be correct.
      */
     @Test
-    public void testGeoSuggestion_multiZone() {
-        GeolocationTimeZoneSuggestion londonOnlySuggestion =
-                createCertainGeolocationSuggestion("Europe/London");
-        GeolocationTimeZoneSuggestion londonOrParisSuggestion =
-                createCertainGeolocationSuggestion("Europe/Paris", "Europe/London");
-        GeolocationTimeZoneSuggestion parisOnlySuggestion =
-                createCertainGeolocationSuggestion("Europe/Paris");
-
+    public void testLocationAlgorithmEvent_multiZone() {
         Script script = new Script()
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
                 .simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED_GEO_ENABLED)
                 .resetConfigurationTracking();
 
-        script.simulateGeolocationTimeZoneSuggestion(londonOnlySuggestion)
-                .verifyTimeZoneChangedAndReset(londonOnlySuggestion);
-        assertEquals(londonOnlySuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        LocationAlgorithmEvent londonOnlyEvent =
+                createCertainLocationAlgorithmEvent("Europe/London");
+        script.simulateLocationAlgorithmEvent(londonOnlyEvent)
+                .verifyTimeZoneChangedAndReset(londonOnlyEvent)
+                .verifyLatestLocationAlgorithmEventReceived(londonOnlyEvent);
 
         // Confirm bias towards the current device zone when there's multiple zones to choose from.
-        script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion)
-                .verifyTimeZoneNotChanged();
-        assertEquals(londonOrParisSuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        LocationAlgorithmEvent londonOrParisEvent =
+                createCertainLocationAlgorithmEvent("Europe/Paris", "Europe/London");
+        script.simulateLocationAlgorithmEvent(londonOrParisEvent)
+                .verifyTimeZoneNotChanged()
+                .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent);
 
-        script.simulateGeolocationTimeZoneSuggestion(parisOnlySuggestion)
-                .verifyTimeZoneChangedAndReset(parisOnlySuggestion);
-        assertEquals(parisOnlySuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        LocationAlgorithmEvent parisOnlyEvent = createCertainLocationAlgorithmEvent("Europe/Paris");
+        script.simulateLocationAlgorithmEvent(parisOnlyEvent)
+                .verifyTimeZoneChangedAndReset(parisOnlyEvent)
+                .verifyLatestLocationAlgorithmEventReceived(parisOnlyEvent);
 
         // Now the suggestion that previously left the device on Europe/London will leave the device
         // on Europe/Paris.
-        script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion)
-                .verifyTimeZoneNotChanged();
-        assertEquals(londonOrParisSuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        script.simulateLocationAlgorithmEvent(londonOrParisEvent)
+                .verifyTimeZoneNotChanged()
+                .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent);
     }
 
     /**
@@ -740,8 +1093,9 @@
      */
     @Test
     public void testChangingGeoDetectionEnabled() {
-        GeolocationTimeZoneSuggestion geolocationSuggestion =
-                createCertainGeolocationSuggestion("Europe/London");
+        TestStateChangeListener stateChangeListener = new TestStateChangeListener();
+        LocationAlgorithmEvent locationAlgorithmEvent =
+                createCertainLocationAlgorithmEvent("Europe/London");
         TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion(
                 SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
                 "Europe/Paris");
@@ -749,20 +1103,22 @@
         Script script = new Script()
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW)
                 .simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED)
-                .resetConfigurationTracking();
+                .resetConfigurationTracking()
+                .registerStateChangeListener(stateChangeListener);
 
         // Add suggestions. Nothing should happen as time zone detection is disabled.
-        script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
-                .verifyTimeZoneNotChanged();
+        script.simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                .verifyTimeZoneNotChanged()
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
 
-        assertEquals(geolocationSuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        // A detector status change is considered a "state change".
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
 
         script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
-                .verifyTimeZoneNotChanged();
+                .verifyTimeZoneNotChanged()
+                .verifyLatestTelephonySuggestionReceived(SLOT_INDEX1, telephonySuggestion);
 
-        assertEquals(telephonySuggestion,
-                mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1).suggestion);
+        assertStateChangeNotificationsSent(stateChangeListener, 0);
 
         // Toggling the time zone detection enabled setting on should cause the device setting to be
         // set from the telephony signal, as we've started with geolocation time zone detection
@@ -770,18 +1126,25 @@
         script.simulateSetAutoMode(true)
                 .verifyTimeZoneChangedAndReset(telephonySuggestion);
 
+        // A configuration change is considered a "state change".
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
+
         // Changing the detection to enable geo detection will cause the device tz setting to
         // change to use the latest geolocation suggestion.
         script.simulateSetGeoDetectionEnabled(true)
-                .verifyTimeZoneChangedAndReset(geolocationSuggestion);
+                .verifyTimeZoneChangedAndReset(locationAlgorithmEvent);
+
+        // A configuration change is considered a "state change".
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
 
         // Changing the detection to disable geo detection should cause the device tz setting to
         // change to the telephony suggestion.
         script.simulateSetGeoDetectionEnabled(false)
-                .verifyTimeZoneChangedAndReset(telephonySuggestion);
+                .verifyTimeZoneChangedAndReset(telephonySuggestion)
+                .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent);
 
-        assertEquals(geolocationSuggestion,
-                mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion());
+        // A configuration change is considered a "state change".
+        assertStateChangeNotificationsSent(stateChangeListener, 1);
     }
 
     @Test
@@ -815,21 +1178,20 @@
 
         // Receiving an "uncertain" geolocation suggestion should have no effect.
         {
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
         }
 
         // Receiving a "certain" geolocation suggestion should disable telephony fallback mode.
         {
-            GeolocationTimeZoneSuggestion geolocationSuggestion =
-                    createCertainGeolocationSuggestion("Europe/London");
+            LocationAlgorithmEvent locationAlgorithmEvent =
+                    createCertainLocationAlgorithmEvent("Europe/London");
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
-                    .verifyTimeZoneChangedAndReset(geolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
         }
 
@@ -852,22 +1214,22 @@
         // Geolocation suggestions should continue to be used as normal (previous telephony
         // suggestions are not used, even when the geolocation suggestion is uncertain).
         {
-            GeolocationTimeZoneSuggestion geolocationSuggestion =
-                    createCertainGeolocationSuggestion("Europe/Rome");
+            LocationAlgorithmEvent certainLocationAlgorithmEvent =
+                    createCertainLocationAlgorithmEvent("Europe/Rome");
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
-                    .verifyTimeZoneChangedAndReset(geolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(certainLocationAlgorithmEvent)
+                    .verifyTimeZoneChangedAndReset(certainLocationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
 
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent uncertainLocationAlgorithmEvent =
+                    createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(uncertainLocationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
 
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(certainLocationAlgorithmEvent)
                     // No change needed, device will already be set to Europe/Rome.
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
@@ -884,21 +1246,20 @@
 
         // Make the geolocation algorithm uncertain.
         {
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneChangedAndReset(lastTelephonySuggestion)
                     .verifyTelephonyFallbackIsEnabled(true);
         }
 
         // Make the geolocation algorithm certain, disabling telephony fallback.
         {
-            GeolocationTimeZoneSuggestion geolocationSuggestion =
-                    createCertainGeolocationSuggestion("Europe/Lisbon");
+            LocationAlgorithmEvent locationAlgorithmEvent =
+                    createCertainLocationAlgorithmEvent("Europe/Lisbon");
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
-                    .verifyTimeZoneChangedAndReset(geolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
+                    .verifyTimeZoneChangedAndReset(locationAlgorithmEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
 
         }
@@ -906,10 +1267,9 @@
         // Demonstrate what happens when geolocation is uncertain when telephony fallback is
         // enabled.
         {
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false)
                     .simulateEnableTelephonyFallback()
@@ -937,10 +1297,9 @@
 
         // Receiving an "uncertain" geolocation suggestion should have no effect.
         {
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
         }
@@ -948,10 +1307,9 @@
         // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back
         // to
         {
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent locationAlgorithmEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(true);
         }
@@ -961,17 +1319,16 @@
         // Geolocation suggestions should continue to be used as normal (previous telephony
         // suggestions are not used, even when the geolocation suggestion is uncertain).
         {
-            GeolocationTimeZoneSuggestion geolocationSuggestion =
-                    createCertainGeolocationSuggestion("Europe/Rome");
+            LocationAlgorithmEvent certainEvent =
+                    createCertainLocationAlgorithmEvent("Europe/Rome");
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion)
-                    .verifyTimeZoneChangedAndReset(geolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(certainEvent)
+                    .verifyTimeZoneChangedAndReset(certainEvent)
                     .verifyTelephonyFallbackIsEnabled(false);
 
-            GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion =
-                    createUncertainGeolocationSuggestion();
+            LocationAlgorithmEvent uncertainEvent = createUncertainLocationAlgorithmEvent();
             script.simulateIncrementClock()
-                    .simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion)
+                    .simulateLocationAlgorithmEvent(uncertainEvent)
                     .verifyTimeZoneNotChanged()
                     .verifyTelephonyFallbackIsEnabled(false);
 
@@ -1095,15 +1452,15 @@
         TelephonyTimeZoneSuggestion telephonySuggestion =
                 createTelephonySuggestion(0 /* slotIndex */, MATCH_TYPE_NETWORK_COUNTRY_ONLY,
                         QUALITY_SINGLE_ZONE, "Zone2");
-        GeolocationTimeZoneSuggestion geolocationTimeZoneSuggestion =
-                createCertainGeolocationSuggestion("Zone3", "Zone2");
+        LocationAlgorithmEvent locationAlgorithmEvent =
+                createCertainLocationAlgorithmEvent("Zone3", "Zone2");
         script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion)
                 .verifyTimeZoneNotChanged()
-                .simulateGeolocationTimeZoneSuggestion(geolocationTimeZoneSuggestion)
+                .simulateLocationAlgorithmEvent(locationAlgorithmEvent)
                 .verifyTimeZoneNotChanged();
 
         assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
-                manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion,
+                manualSuggestion, telephonySuggestion, locationAlgorithmEvent,
                 MetricsTimeZoneDetectorState.DETECTION_MODE_MANUAL);
 
         // Update the config and confirm that the config metrics state updates also.
@@ -1112,11 +1469,11 @@
                 .setGeoDetectionEnabledSetting(true)
                 .build();
 
-        expectedDeviceTimeZoneId = geolocationTimeZoneSuggestion.getZoneIds().get(0);
+        expectedDeviceTimeZoneId = locationAlgorithmEvent.getSuggestion().getZoneIds().get(0);
         script.simulateConfigurationInternalChange(expectedInternalConfig)
                 .verifyTimeZoneChangedAndReset(expectedDeviceTimeZoneId, TIME_ZONE_CONFIDENCE_HIGH);
         assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId,
-                manualSuggestion, telephonySuggestion, geolocationTimeZoneSuggestion,
+                manualSuggestion, telephonySuggestion, locationAlgorithmEvent,
                 MetricsTimeZoneDetectorState.DETECTION_MODE_GEO);
     }
 
@@ -1128,7 +1485,7 @@
             ConfigurationInternal expectedInternalConfig,
             String expectedDeviceTimeZoneId, ManualTimeZoneSuggestion expectedManualSuggestion,
             TelephonyTimeZoneSuggestion expectedTelephonySuggestion,
-            GeolocationTimeZoneSuggestion expectedGeolocationTimeZoneSuggestion,
+            LocationAlgorithmEvent expectedLocationAlgorithmEvent,
             int expectedDetectionMode) {
 
         MetricsTimeZoneDetectorState actualState = mTimeZoneDetectorStrategy.generateMetricsState();
@@ -1141,7 +1498,7 @@
                 MetricsTimeZoneDetectorState.create(
                         tzIdOrdinalGenerator, expectedInternalConfig, expectedDeviceTimeZoneId,
                         expectedManualSuggestion, expectedTelephonySuggestion,
-                        expectedGeolocationTimeZoneSuggestion);
+                        expectedLocationAlgorithmEvent);
         // Rely on MetricsTimeZoneDetectorState.equals() for time zone ID / ID ordinal comparisons.
         assertEquals(expectedState, actualState);
     }
@@ -1181,39 +1538,50 @@
         return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX2).build();
     }
 
+    private LocationAlgorithmEvent createCertainLocationAlgorithmEvent(@NonNull String... zoneIds) {
+        GeolocationTimeZoneSuggestion suggestion = createCertainGeolocationSuggestion(zoneIds);
+        LocationTimeZoneAlgorithmStatus algorithmStatus = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_IS_CERTAIN, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion);
+        event.addDebugInfo("Test certain event");
+        return event;
+    }
+
+    private LocationAlgorithmEvent createUncertainLocationAlgorithmEvent() {
+        GeolocationTimeZoneSuggestion suggestion = createUncertainGeolocationSuggestion();
+        LocationTimeZoneAlgorithmStatus algorithmStatus = new LocationTimeZoneAlgorithmStatus(
+                DETECTION_ALGORITHM_STATUS_RUNNING, PROVIDER_STATUS_IS_UNCERTAIN, null,
+                PROVIDER_STATUS_NOT_PRESENT, null);
+        LocationAlgorithmEvent event = new LocationAlgorithmEvent(algorithmStatus, suggestion);
+        event.addDebugInfo("Test uncertain event");
+        return event;
+    }
+
     private GeolocationTimeZoneSuggestion createUncertainGeolocationSuggestion() {
-        return GeolocationTimeZoneSuggestion.createCertainSuggestion(
-                mFakeEnvironment.elapsedRealtimeMillis(), null);
+        return GeolocationTimeZoneSuggestion.createUncertainSuggestion(
+                mFakeEnvironment.elapsedRealtimeMillis());
     }
 
     private GeolocationTimeZoneSuggestion createCertainGeolocationSuggestion(
             @NonNull String... zoneIds) {
         assertNotNull(zoneIds);
 
-        GeolocationTimeZoneSuggestion suggestion =
-                GeolocationTimeZoneSuggestion.createCertainSuggestion(
-                        mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds));
-        suggestion.addDebugInfo("Test suggestion");
-        return suggestion;
+        return GeolocationTimeZoneSuggestion.createCertainSuggestion(
+                mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds));
     }
 
     static class FakeEnvironment implements TimeZoneDetectorStrategyImpl.Environment {
 
         private final TestState<String> mTimeZoneId = new TestState<>();
         private final TestState<Integer> mTimeZoneConfidence = new TestState<>();
-        private ConfigurationInternal mConfigurationInternal;
         private @ElapsedRealtimeLong long mElapsedRealtimeMillis;
-        private ConfigurationChangeListener mConfigurationInternalChangeListener;
 
         FakeEnvironment() {
             // Ensure the fake environment starts with the defaults a fresh device would.
             initializeTimeZoneSetting("", TIME_ZONE_CONFIDENCE_LOW);
         }
 
-        void initializeConfig(ConfigurationInternal configurationInternal) {
-            mConfigurationInternal = configurationInternal;
-        }
-
         void initializeClock(@ElapsedRealtimeLong long elapsedRealtimeMillis) {
             mElapsedRealtimeMillis = elapsedRealtimeMillis;
         }
@@ -1228,16 +1596,6 @@
         }
 
         @Override
-        public void setConfigurationInternalChangeListener(ConfigurationChangeListener listener) {
-            mConfigurationInternalChangeListener = listener;
-        }
-
-        @Override
-        public ConfigurationInternal getCurrentUserConfigurationInternal() {
-            return mConfigurationInternal;
-        }
-
-        @Override
         public String getDeviceTimeZone() {
             return mTimeZoneId.getLatest();
         }
@@ -1254,11 +1612,6 @@
             mTimeZoneConfidence.set(confidence);
         }
 
-        void simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
-            mConfigurationInternal = configurationInternal;
-            mConfigurationInternalChangeListener.onChange();
-        }
-
         void assertTimeZoneNotChanged() {
             mTimeZoneId.assertHasNotBeenSet();
             mTimeZoneConfidence.assertHasNotBeenSet();
@@ -1296,6 +1649,14 @@
         }
     }
 
+    private void assertStateChangeNotificationsSent(
+            TestStateChangeListener stateChangeListener, int expectedCount) {
+        // State change notifications are asynchronous, so we have to wait.
+        mTestHandler.waitForMessagesToBeProcessed();
+
+        stateChangeListener.assertNotificationsReceivedAndReset(expectedCount);
+    }
+
     /**
      * A "fluent" class allows reuse of code in tests: initialization, simulation and verification
      * logic.
@@ -1313,6 +1674,11 @@
             return this;
         }
 
+        Script registerStateChangeListener(StateChangeListener stateChangeListener) {
+            mTimeZoneDetectorStrategy.addChangeListener(stateChangeListener);
+            return this;
+        }
+
         Script simulateIncrementClock() {
             mFakeEnvironment.incrementClock();
             return this;
@@ -1322,7 +1688,8 @@
          * Simulates the user / user's configuration changing.
          */
         Script simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) {
-            mFakeEnvironment.simulateConfigurationInternalChange(configurationInternal);
+            mFakeServiceConfigAccessorSpy.simulateCurrentUserConfigurationInternalChange(
+                    configurationInternal);
             return this;
         }
 
@@ -1331,7 +1698,7 @@
          */
         Script simulateSetAutoMode(boolean autoDetectionEnabled) {
             ConfigurationInternal newConfig = new ConfigurationInternal.Builder(
-                    mFakeEnvironment.getCurrentUserConfigurationInternal())
+                    mFakeServiceConfigAccessorSpy.getCurrentUserConfigurationInternal())
                     .setAutoDetectionEnabledSetting(autoDetectionEnabled)
                     .build();
             simulateConfigurationInternalChange(newConfig);
@@ -1343,7 +1710,7 @@
          */
         Script simulateSetGeoDetectionEnabled(boolean geoDetectionEnabled) {
             ConfigurationInternal newConfig = new ConfigurationInternal.Builder(
-                    mFakeEnvironment.getCurrentUserConfigurationInternal())
+                    mFakeServiceConfigAccessorSpy.getCurrentUserConfigurationInternal())
                     .setGeoDetectionEnabledSetting(geoDetectionEnabled)
                     .build();
             simulateConfigurationInternalChange(newConfig);
@@ -1351,11 +1718,10 @@
         }
 
         /**
-         * Simulates the time zone detection strategy receiving a geolocation-originated
-         * suggestion.
+         * Simulates the time zone detection strategy receiving a location algorithm event.
          */
-        Script simulateGeolocationTimeZoneSuggestion(GeolocationTimeZoneSuggestion suggestion) {
-            mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(suggestion);
+        Script simulateLocationAlgorithmEvent(LocationAlgorithmEvent event) {
+            mTimeZoneDetectorStrategy.handleLocationAlgorithmEvent(event);
             return this;
         }
 
@@ -1412,7 +1778,9 @@
             return this;
         }
 
-        Script verifyTimeZoneChangedAndReset(GeolocationTimeZoneSuggestion suggestion) {
+        Script verifyTimeZoneChangedAndReset(LocationAlgorithmEvent event) {
+            GeolocationTimeZoneSuggestion suggestion = event.getSuggestion();
+            assertNotNull("Only events with suggestions can change the time zone", suggestion);
             assertEquals("Only use this method with unambiguous geo suggestions",
                     1, suggestion.getZoneIds().size());
             verifyTimeZoneChangedAndReset(
@@ -1427,6 +1795,32 @@
             return this;
         }
 
+        Script verifyCachedDetectorStatus(TimeZoneDetectorStatus expectedStatus) {
+            assertEquals(expectedStatus,
+                    mTimeZoneDetectorStrategy.getCachedDetectorStatusForTests());
+            return this;
+        }
+
+        Script verifyLatestLocationAlgorithmEventReceived(LocationAlgorithmEvent expectedEvent) {
+            assertEquals(expectedEvent,
+                    mTimeZoneDetectorStrategy.getLatestLocationAlgorithmEvent());
+            return this;
+        }
+
+        Script verifyLatestTelephonySuggestionReceived(int slotIndex,
+                TelephonyTimeZoneSuggestion expectedSuggestion) {
+            assertEquals(expectedSuggestion,
+                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(slotIndex).suggestion);
+            return this;
+        }
+
+        Script verifyLatestQualifiedTelephonySuggestionReceived(int slotIndex,
+                QualifiedTelephonyTimeZoneSuggestion expectedQualifiedSuggestion) {
+            assertEquals(expectedQualifiedSuggestion,
+                    mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(slotIndex));
+            return this;
+        }
+
         Script resetConfigurationTracking() {
             mFakeEnvironment.commitAllChanges();
             return this;
@@ -1457,4 +1851,27 @@
             @MatchType int matchType, @Quality int quality, int expectedScore) {
         return new TelephonyTestCase(matchType, quality, expectedScore);
     }
+
+    private static class TestStateChangeListener implements StateChangeListener {
+
+        private int mNotificationsReceived;
+
+        @Override
+        public void onChange() {
+            mNotificationsReceived++;
+        }
+
+        public void assertNotificationsReceivedAndReset(int expectedCount) {
+            assertNotificationsReceived(expectedCount);
+            resetNotificationsReceivedCount();
+        }
+
+        private void resetNotificationsReceivedCount() {
+            mNotificationsReceived = 0;
+        }
+
+        private void assertNotificationsReceived(int expectedCount) {
+            assertEquals(expectedCount, mNotificationsReceived);
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
index 52e9d3a..f8d169b 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
@@ -17,6 +17,7 @@
 package com.android.server.timezonedetector.location;
 
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 
 /**
  * Fake implementation of {@link TimeZoneProviderEventPreProcessor} which assumes that all events
@@ -30,8 +31,9 @@
     @Override
     public TimeZoneProviderEvent preProcess(TimeZoneProviderEvent timeZoneProviderEvent) {
         if (mIsUncertain) {
+            TimeZoneProviderStatus timeZoneProviderStatus = null;
             return TimeZoneProviderEvent.createUncertainEvent(
-                    timeZoneProviderEvent.getCreationElapsedMillis());
+                    timeZoneProviderEvent.getCreationElapsedMillis(), timeZoneProviderStatus);
         }
         return timeZoneProviderEvent;
     }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
index 0257ce0..7b1db95 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
@@ -15,6 +15,14 @@
  */
 package com.android.server.timezonedetector.location;
 
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_NOT_RUNNING;
+import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_UNKNOWN;
+
 import static com.android.server.timezonedetector.ConfigurationInternal.DETECTION_MODE_MANUAL;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
@@ -36,6 +44,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -45,14 +54,17 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.time.DetectorStatusTypes.DetectionAlgorithmStatus;
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
 import com.android.server.timezonedetector.ConfigurationInternal;
 import com.android.server.timezonedetector.GeolocationTimeZoneSuggestion;
+import com.android.server.timezonedetector.LocationAlgorithmEvent;
 import com.android.server.timezonedetector.TestState;
 import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderMetricsLogger;
 import com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.ProviderStateEnum;
@@ -78,8 +90,15 @@
             createSuggestionEvent(asList("Europe/London"));
     private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 =
             createSuggestionEvent(asList("Europe/Paris"));
+    private static final TimeZoneProviderStatus UNCERTAIN_PROVIDER_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE)
+                    .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_UNKNOWN)
+                    .build();
     private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT =
-            TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_TIME_MILLIS);
+            TimeZoneProviderEvent.createUncertainEvent(
+                    ARBITRARY_TIME_MILLIS, UNCERTAIN_PROVIDER_STATUS);
     private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT =
             TimeZoneProviderEvent.createPermanentFailureEvent(ARBITRARY_TIME_MILLIS, "Test");
 
@@ -127,7 +146,7 @@
         mTestPrimaryLocationTimeZoneProvider.setFailDuringInitialization(true);
 
         // Initialize. After initialization the providers must be initialized and one should be
-        // started.
+        // started. They should report their status change via the callback.
         controller.initialize(testEnvironment, mTestCallback);
 
         mTestPrimaryLocationTimeZoneProvider.assertInitialized();
@@ -140,7 +159,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertInitializationTimeoutSet(expectedInitTimeout);
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -170,7 +190,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -197,7 +218,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING, STATE_FAILED);
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -225,7 +247,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -248,7 +271,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -268,7 +292,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate time passing with no provider event being received from the primary.
@@ -282,7 +307,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate time passing with no provider event being received from either the primary or
@@ -297,7 +322,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Finally, the uncertainty timeout should cause the controller to make an uncertain
@@ -310,7 +335,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN);
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestCallback.assertEventWithUncertainSuggestionReportedAndCommit();
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -331,7 +356,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a location event being received from the primary provider. This should cause a
@@ -344,7 +370,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -366,7 +392,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate time passing with no provider event being received from the primary.
@@ -378,7 +405,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate a location event being received from the primary provider. This should cause a
@@ -391,7 +418,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -413,7 +440,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate time passing with no provider event being received from the primary.
@@ -425,7 +453,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate a location event being received from the secondary provider. This should cause a
@@ -439,7 +467,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -461,7 +489,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a location event being received from the primary provider. This should cause a
@@ -474,7 +503,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -487,7 +516,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // And a third, different event should cause another suggestion.
@@ -499,7 +528,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -521,7 +550,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate time passing with no provider event being received from the primary.
@@ -533,7 +563,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate a location event being received from the secondary provider. This should cause a
@@ -547,7 +577,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -561,7 +591,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // And a third, different event should cause another suggestion.
@@ -574,7 +604,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -596,7 +626,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a location event being received from the primary provider. This should cause a
@@ -609,7 +640,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -625,7 +656,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate a location event being received from the secondary provider. This should cause a
@@ -640,7 +671,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -656,7 +687,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate time passing. This means the uncertainty timeout should fire and the uncertain
@@ -669,7 +700,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN);
-        mTestCallback.assertUncertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithUncertainSuggestionReportedAndCommit(
                 USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -691,7 +722,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a location event being received from the primary provider. This should cause a
@@ -704,7 +736,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -719,7 +751,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // And a success event from the primary provider should cause the controller to make another
@@ -733,7 +765,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
@@ -753,7 +785,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is enabled.
@@ -764,7 +797,8 @@
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is disabled.
@@ -774,7 +808,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -793,7 +828,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_PROVIDERS_INITIALIZING, STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is enabled.
@@ -804,7 +840,8 @@
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a success event being received from the primary provider.
@@ -816,7 +853,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -829,8 +866,9 @@
         assertControllerState(controller, STATE_STOPPED);
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
-        mTestMetricsLogger.assertStateChangesAndCommit(STATE_UNCERTAIN, STATE_STOPPED);
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED);
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -851,7 +889,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate the primary provider suggesting a time zone.
@@ -865,7 +904,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -883,9 +922,9 @@
         mTestPrimaryLocationTimeZoneProvider.assertStateEnumAndConfig(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER2_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
-        mTestMetricsLogger.assertStateChangesAndCommit(
-                STATE_UNCERTAIN, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED, STATE_INITIALIZING);
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -906,7 +945,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a failure location event being received from the primary provider. This should
@@ -919,7 +959,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate uncertainty from the secondary.
@@ -931,7 +971,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // And a success event from the secondary provider should cause the controller to make
@@ -944,7 +984,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -957,7 +997,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
     }
 
@@ -978,7 +1018,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a failure location event being received from the primary provider. This should
@@ -991,7 +1032,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is disabled.
@@ -1001,7 +1042,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is enabled.
@@ -1012,7 +1054,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -1033,7 +1076,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate an uncertain event from the primary. This will start the secondary, which will
@@ -1048,7 +1092,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate failure event from the secondary. This should just affect the secondary's state.
@@ -1060,7 +1104,7 @@
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // And a success event from the primary provider should cause the controller to make
@@ -1073,7 +1117,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -1086,7 +1130,7 @@
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
     }
 
@@ -1107,7 +1151,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate an uncertain event from the primary. This will start the secondary, which will
@@ -1122,7 +1167,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Simulate failure event from the secondary. This should just affect the secondary's state.
@@ -1134,7 +1179,7 @@
                 PROVIDER_STATE_STARTED_UNCERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertUncertaintyTimeoutSet(testEnvironment, controller);
 
         // Now signal a config change so that geo detection is disabled.
@@ -1144,7 +1189,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Now signal a config change so that geo detection is enabled. Only the primary can be
@@ -1156,7 +1202,8 @@
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -1177,7 +1224,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate a failure event from the primary. This will start the secondary.
@@ -1189,7 +1237,7 @@
         mTestSecondaryLocationTimeZoneProvider.assertStateEnumAndConfigAndCommit(
                 PROVIDER_STATE_STARTED_INITIALIZING, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestMetricsLogger.assertStateChangesAndCommit();
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertNoEventReported();
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate failure event from the secondary.
@@ -1200,7 +1248,8 @@
         mTestPrimaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestSecondaryLocationTimeZoneProvider.assertIsPermFailedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_FAILED);
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
     }
 
@@ -1219,7 +1268,7 @@
         {
             LocationTimeZoneManagerServiceState state = controller.getStateForTests();
             assertEquals(STATE_INITIALIZING, state.getControllerState());
-            assertNull(state.getLastSuggestion());
+            assertNull(state.getLastEvent().getSuggestion());
             assertControllerRecordedStates(state,
                     STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
             assertProviderStates(state.getPrimaryProviderStates(),
@@ -1237,7 +1286,7 @@
         {
             LocationTimeZoneManagerServiceState state = controller.getStateForTests();
             assertEquals(STATE_INITIALIZING, state.getControllerState());
-            assertNull(state.getLastSuggestion());
+            assertNull(state.getLastEvent().getSuggestion());
             assertControllerRecordedStates(state);
             assertProviderStates(
                     state.getPrimaryProviderStates(), PROVIDER_STATE_STARTED_UNCERTAIN);
@@ -1254,7 +1303,7 @@
             LocationTimeZoneManagerServiceState state = controller.getStateForTests();
             assertEquals(STATE_CERTAIN, state.getControllerState());
             assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(),
-                    state.getLastSuggestion().getZoneIds());
+                    state.getLastEvent().getSuggestion().getZoneIds());
             assertControllerRecordedStates(state, STATE_CERTAIN);
             assertProviderStates(state.getPrimaryProviderStates());
             assertProviderStates(
@@ -1266,7 +1315,7 @@
             LocationTimeZoneManagerServiceState state = controller.getStateForTests();
             assertEquals(STATE_CERTAIN, state.getControllerState());
             assertEquals(USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1.getSuggestion().getTimeZoneIds(),
-                    state.getLastSuggestion().getZoneIds());
+                    state.getLastEvent().getSuggestion().getZoneIds());
             assertControllerRecordedStates(state);
             assertProviderStates(state.getPrimaryProviderStates());
             assertProviderStates(state.getSecondaryProviderStates());
@@ -1299,7 +1348,8 @@
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(
                 STATE_PROVIDERS_INITIALIZING, STATE_STOPPED, STATE_INITIALIZING);
-        mTestCallback.assertNoSuggestionMade();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_RUNNING);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
         // Simulate the primary provider suggesting a time zone.
@@ -1313,7 +1363,7 @@
                 PROVIDER_STATE_STARTED_CERTAIN, USER1_CONFIG_GEO_DETECTION_ENABLED);
         mTestSecondaryLocationTimeZoneProvider.assertIsStoppedAndCommit();
         mTestMetricsLogger.assertStateChangesAndCommit(STATE_CERTAIN);
-        mTestCallback.assertCertainSuggestionMadeFromEventAndCommit(
+        mTestCallback.assertEventWithCertainSuggestionReportedAndCommit(
                 USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT1);
         assertFalse(controller.isUncertaintyTimeoutSet());
 
@@ -1321,11 +1371,11 @@
         controller.destroy();
 
         assertControllerState(controller, STATE_DESTROYED);
-        mTestMetricsLogger.assertStateChangesAndCommit(
-                STATE_UNCERTAIN, STATE_STOPPED, STATE_DESTROYED);
+        mTestMetricsLogger.assertStateChangesAndCommit(STATE_STOPPED, STATE_DESTROYED);
 
         // Confirm that the previous suggestion was overridden.
-        mTestCallback.assertUncertainSuggestionMadeAndCommit();
+        mTestCallback.assertEventWithNoSuggestionReportedAndCommit(
+                DETECTION_ALGORITHM_STATUS_NOT_RUNNING);
 
         mTestPrimaryLocationTimeZoneProvider.assertStateChangesAndCommit(
                 PROVIDER_STATE_STOPPED, PROVIDER_STATE_DESTROYED);
@@ -1390,12 +1440,17 @@
     }
 
     private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List<String> timeZoneIds) {
+        TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_NOT_APPLICABLE)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_NOT_APPLICABLE)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
+                .setTimeZoneIds(timeZoneIds)
+                .build();
         return TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_TIME_MILLIS,
-                new TimeZoneProviderSuggestion.Builder()
-                        .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
-                        .setTimeZoneIds(timeZoneIds)
-                        .build());
+                ARBITRARY_TIME_MILLIS, suggestion, providerStatus);
     }
 
     private static void assertControllerState(LocationTimeZoneProviderController controller,
@@ -1498,63 +1553,101 @@
 
     private static class TestCallback extends LocationTimeZoneProviderController.Callback {
 
-        private TestState<GeolocationTimeZoneSuggestion> mLatestSuggestion = new TestState<>();
+        private TestState<LocationAlgorithmEvent> mLatestEvent = new TestState<>();
 
         TestCallback(ThreadingDomain threadingDomain) {
             super(threadingDomain);
         }
 
         @Override
-        void suggest(GeolocationTimeZoneSuggestion suggestion) {
-            mLatestSuggestion.set(suggestion);
+        void sendEvent(LocationAlgorithmEvent event) {
+            mLatestEvent.set(event);
         }
 
-        void assertCertainSuggestionMadeFromEventAndCommit(TimeZoneProviderEvent event) {
+        void assertNoEventReported() {
+            mLatestEvent.assertHasNotBeenSet();
+        }
+
+        /**
+         * Asserts one or more events have been reported, and the most recent does not contain a
+         * suggestion.
+         */
+        void assertEventWithNoSuggestionReportedAndCommit(
+                @DetectionAlgorithmStatus int expectedAlgorithmStatus) {
+            mLatestEvent.assertHasBeenSet();
+
+            LocationAlgorithmEvent latest = mLatestEvent.getLatest();
+            assertEquals(expectedAlgorithmStatus, latest.getAlgorithmStatus().getStatus());
+            assertNull(latest.getSuggestion());
+            mLatestEvent.commitLatest();
+        }
+
+        void assertEventWithCertainSuggestionReportedAndCommit(TimeZoneProviderEvent event) {
             // Test coding error if this fails.
             assertEquals(TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION, event.getType());
 
+            // By definition, the algorithm has to be running to report a suggestion.
+            @DetectionAlgorithmStatus int expectedAlgorithmStatus =
+                    DETECTION_ALGORITHM_STATUS_RUNNING;
             TimeZoneProviderSuggestion suggestion = event.getSuggestion();
-            assertSuggestionMadeAndCommit(
+            assertEventWithSuggestionReportedAndCommit(
+                    expectedAlgorithmStatus,
                     suggestion.getElapsedRealtimeMillis(),
                     suggestion.getTimeZoneIds());
         }
 
-        void assertNoSuggestionMade() {
-            mLatestSuggestion.assertHasNotBeenSet();
-        }
-
-        /** Asserts that an uncertain suggestion has been made from the supplied event. */
-        void assertUncertainSuggestionMadeFromEventAndCommit(TimeZoneProviderEvent event) {
+        /**
+         * Asserts that one or more events have been reported, and the most recent contains an
+         * uncertain suggestion matching select details from the supplied provider event.
+         */
+        void assertEventWithUncertainSuggestionReportedAndCommit(TimeZoneProviderEvent event) {
             // Test coding error if this fails.
             assertEquals(TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN, event.getType());
 
-            assertSuggestionMadeAndCommit(event.getCreationElapsedMillis(), null);
+            // By definition, the algorithm has to be running to report a suggestion.
+            @DetectionAlgorithmStatus int expectedAlgorithmStatus =
+                    DETECTION_ALGORITHM_STATUS_RUNNING;
+            assertEventWithSuggestionReportedAndCommit(
+                    expectedAlgorithmStatus, event.getCreationElapsedMillis(), null);
         }
 
         /**
-         * Asserts that an uncertain suggestion has been made.
-         * Ignores the suggestion's effectiveFromElapsedMillis.
+         * Asserts that one or more events have been reported, and the most recent contains an
+         * uncertain suggestion. Ignores the suggestion's effectiveFromElapsedMillis.
          */
-        void assertUncertainSuggestionMadeAndCommit() {
+        void assertEventWithUncertainSuggestionReportedAndCommit() {
+            // By definition, the algorithm has to be running to report a suggestion.
+            @DetectionAlgorithmStatus int expectedAlgorithmStatus =
+                    DETECTION_ALGORITHM_STATUS_RUNNING;
+
             // An "uncertain" suggestion has null time zone IDs.
-            assertSuggestionMadeAndCommit(null, null);
+            assertEventWithSuggestionReportedAndCommit(expectedAlgorithmStatus, null, null);
         }
 
         /**
-         * Asserts that a suggestion has been made and some properties of that suggestion.
-         * When expectedEffectiveFromElapsedMillis is null then its value isn't checked.
+         * Asserts that an event has been reported containing a suggestion and some properties of
+         * that suggestion. When expectedEffectiveFromElapsedMillis is null then its value isn't
+         * checked.
          */
-        private void assertSuggestionMadeAndCommit(
+        private void assertEventWithSuggestionReportedAndCommit(
+                @DetectionAlgorithmStatus int expectedAlgorithmStatus,
                 @Nullable @ElapsedRealtimeLong Long expectedEffectiveFromElapsedMillis,
                 @Nullable List<String> expectedZoneIds) {
-            mLatestSuggestion.assertHasBeenSet();
+            mLatestEvent.assertHasBeenSet();
+
+            LocationAlgorithmEvent latestEvent = mLatestEvent.getLatest();
+            assertEquals(expectedAlgorithmStatus, latestEvent.getAlgorithmStatus().getStatus());
+
+            GeolocationTimeZoneSuggestion suggestion = latestEvent.getSuggestion();
+            assertNotNull("Latest event doesn't contain a suggestion: event=" + latestEvent,
+                    suggestion);
+
             if (expectedEffectiveFromElapsedMillis != null) {
-                assertEquals(
-                        expectedEffectiveFromElapsedMillis.longValue(),
-                        mLatestSuggestion.getLatest().getEffectiveFromElapsedMillis());
+                assertEquals(expectedEffectiveFromElapsedMillis.longValue(),
+                        suggestion.getEffectiveFromElapsedMillis());
             }
-            assertEquals(expectedZoneIds, mLatestSuggestion.getLatest().getZoneIds());
-            mLatestSuggestion.commitLatest();
+            assertEquals(expectedZoneIds, suggestion.getZoneIds());
+            mLatestEvent.commitLatest();
         }
     }
 
@@ -1579,11 +1672,9 @@
         }
 
         @Override
-        void onInitialize() {
+        boolean onInitialize() {
             mInitialized = true;
-            if (mFailDuringInitialization) {
-                throw new RuntimeException("Simulated initialization failure");
-            }
+            return !mFailDuringInitialization;
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
index cb2905d..1ae74c6 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_CERTAIN;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
@@ -33,6 +36,7 @@
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
@@ -56,6 +60,12 @@
 public class LocationTimeZoneProviderTest {
 
     private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 123456789L;
+    private static final TimeZoneProviderStatus ARBITRARY_PROVIDER_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                    .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                    .build();
 
     private TestThreadingDomain mTestThreadingDomain;
     private TestProviderListener mProviderListener;
@@ -79,89 +89,108 @@
                 mTimeZoneProviderEventPreProcessor);
 
         // initialize()
-        provider.initialize(mProviderListener);
-        provider.assertOnInitializeCalled();
+        {
+            provider.initialize(mProviderListener);
+            provider.assertOnInitializeCalled();
 
-        ProviderState currentState = assertAndReturnProviderState(
-                provider, providerMetricsLogger, PROVIDER_STATE_STOPPED);
-        assertNull(currentState.currentUserConfiguration);
-        assertSame(provider, currentState.provider);
-        mTestThreadingDomain.assertQueueEmpty();
+            ProviderState currentState = assertAndReturnProviderState(
+                    provider, providerMetricsLogger, PROVIDER_STATE_STOPPED,
+                    /*expectedReportedStatus=*/null);
+            assertNull(currentState.currentUserConfiguration);
+            assertSame(provider, currentState.provider);
+            mTestThreadingDomain.assertQueueEmpty();
+        }
+
+        ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
 
         // startUpdates()
-        ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
-        Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
-        Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
-        Duration arbitraryEventFilteringAgeThreshold = Duration.ofMinutes(3);
-        provider.startUpdates(config, arbitraryInitializationTimeout,
-                arbitraryInitializationTimeoutFuzz, arbitraryEventFilteringAgeThreshold);
+        {
+            Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
+            Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
+            Duration arbitraryEventFilteringAgeThreshold = Duration.ofMinutes(3);
+            provider.startUpdates(config, arbitraryInitializationTimeout,
+                    arbitraryInitializationTimeoutFuzz, arbitraryEventFilteringAgeThreshold);
 
-        provider.assertOnStartCalled(
-                arbitraryInitializationTimeout, arbitraryEventFilteringAgeThreshold);
+            provider.assertOnStartCalled(
+                    arbitraryInitializationTimeout, arbitraryEventFilteringAgeThreshold);
 
-        currentState = assertAndReturnProviderState(
-                provider, providerMetricsLogger, PROVIDER_STATE_STARTED_INITIALIZING);
-        assertSame(provider, currentState.provider);
-        assertEquals(config, currentState.currentUserConfiguration);
-        assertNull(currentState.event);
-        // The initialization timeout should be queued.
-        Duration expectedInitializationTimeout =
-                arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz);
-        mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout);
-        // We don't intend to trigger the timeout, so clear it.
-        mTestThreadingDomain.removeAllQueuedRunnables();
+            ProviderState currentState = assertAndReturnProviderState(
+                    provider, providerMetricsLogger, PROVIDER_STATE_STARTED_INITIALIZING,
+                    /*expectedReportedStatus=*/null);
+            assertSame(provider, currentState.provider);
+            assertEquals(config, currentState.currentUserConfiguration);
+            assertNull(currentState.event);
+            // The initialization timeout should be queued.
+            Duration expectedInitializationTimeout =
+                    arbitraryInitializationTimeout.plus(arbitraryInitializationTimeoutFuzz);
+            mTestThreadingDomain.assertSingleDelayedQueueItem(expectedInitializationTimeout);
+            // We don't intend to trigger the timeout, so clear it.
+            mTestThreadingDomain.removeAllQueuedRunnables();
 
-        // Entering started does not trigger an onProviderStateChanged() as it is requested by the
-        // controller.
-        mProviderListener.assertProviderChangeNotReported();
+            // Entering started does not trigger an onProviderStateChanged() as it is requested by
+            // the controller.
+            mProviderListener.assertProviderChangeNotReported();
+        }
 
         // Simulate a suggestion event being received.
-        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
-                .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
-                .setTimeZoneIds(Arrays.asList("Europe/London"))
-                .build();
-        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
-        provider.simulateProviderEventReceived(event);
+        {
+            TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                    .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
+                    .setTimeZoneIds(Arrays.asList("Europe/London"))
+                    .build();
+            TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                    ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, ARBITRARY_PROVIDER_STATUS);
+            provider.simulateProviderEventReceived(event);
 
-        currentState = assertAndReturnProviderState(
-                provider, providerMetricsLogger, PROVIDER_STATE_STARTED_CERTAIN);
-        assertSame(provider, currentState.provider);
-        assertEquals(event, currentState.event);
-        assertEquals(config, currentState.currentUserConfiguration);
-        mTestThreadingDomain.assertQueueEmpty();
-        mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN);
+            ProviderState currentState = assertAndReturnProviderState(
+                    provider, providerMetricsLogger, PROVIDER_STATE_STARTED_CERTAIN,
+                    ARBITRARY_PROVIDER_STATUS);
+            assertSame(provider, currentState.provider);
+            assertEquals(event, currentState.event);
+            assertEquals(config, currentState.currentUserConfiguration);
+            mTestThreadingDomain.assertQueueEmpty();
+            mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN);
+        }
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
-        provider.simulateProviderEventReceived(event);
+        {
+            TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(
+                    ARBITRARY_ELAPSED_REALTIME_MILLIS, ARBITRARY_PROVIDER_STATUS);
+            provider.simulateProviderEventReceived(event);
 
-        currentState = assertAndReturnProviderState(
-                provider, providerMetricsLogger, PROVIDER_STATE_STARTED_UNCERTAIN);
-        assertSame(provider, currentState.provider);
-        assertEquals(event, currentState.event);
-        assertEquals(config, currentState.currentUserConfiguration);
-        mTestThreadingDomain.assertQueueEmpty();
-        mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN);
+            ProviderState currentState = assertAndReturnProviderState(
+                    provider, providerMetricsLogger, PROVIDER_STATE_STARTED_UNCERTAIN,
+                    ARBITRARY_PROVIDER_STATUS);
+            assertSame(provider, currentState.provider);
+            assertEquals(event, currentState.event);
+            assertEquals(config, currentState.currentUserConfiguration);
+            mTestThreadingDomain.assertQueueEmpty();
+            mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_UNCERTAIN);
+        }
 
         // stopUpdates()
-        provider.stopUpdates();
-        provider.assertOnStopUpdatesCalled();
+        {
+            provider.stopUpdates();
+            provider.assertOnStopUpdatesCalled();
 
-        currentState = assertAndReturnProviderState(
-                provider, providerMetricsLogger, PROVIDER_STATE_STOPPED);
-        assertSame(provider, currentState.provider);
-        assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
-        assertNull(currentState.event);
-        assertNull(currentState.currentUserConfiguration);
-        mTestThreadingDomain.assertQueueEmpty();
-        // Entering stopped does not trigger an onProviderStateChanged() as it is requested by the
-        // controller.
-        mProviderListener.assertProviderChangeNotReported();
+            ProviderState currentState = assertAndReturnProviderState(
+                    provider, providerMetricsLogger, PROVIDER_STATE_STOPPED,
+                    /*expectedReportedStatus=*/null);
+            assertSame(provider, currentState.provider);
+            assertEquals(PROVIDER_STATE_STOPPED, currentState.stateEnum);
+            assertNull(currentState.event);
+            assertNull(currentState.currentUserConfiguration);
+            mTestThreadingDomain.assertQueueEmpty();
+            // Entering stopped does not trigger an onProviderStateChanged() as it is requested by
+            // the controller.
+            mProviderListener.assertProviderChangeNotReported();
+        }
 
         // destroy()
-        provider.destroy();
-        provider.assertOnDestroyCalled();
+        {
+            provider.destroy();
+            provider.assertOnDestroyCalled();
+        }
     }
 
     @Test
@@ -193,12 +222,12 @@
                 .setTimeZoneIds(Arrays.asList("Europe/London"))
                 .build();
         TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, null);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN);
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS, null);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
 
@@ -235,8 +264,9 @@
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(invalidTimeZoneIds)
                 .build();
+        TimeZoneProviderStatus providerStatus = null;
         TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion);
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion, providerStatus);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
     }
@@ -270,9 +300,11 @@
      */
     private static ProviderState assertAndReturnProviderState(
             TestLocationTimeZoneProvider provider,
-            RecordingProviderMetricsLogger providerMetricsLogger, int expectedStateEnum) {
+            RecordingProviderMetricsLogger providerMetricsLogger, int expectedStateEnum,
+            TimeZoneProviderStatus expectedReportedStatus) {
         ProviderState currentState = provider.getCurrentState();
         assertEquals(expectedStateEnum, currentState.stateEnum);
+        assertEquals(expectedReportedStatus, currentState.getReportedStatus());
         providerMetricsLogger.assertChangeLoggedAndRemove(expectedStateEnum);
         providerMetricsLogger.assertNoMoreLogEntries();
         return currentState;
@@ -298,8 +330,9 @@
         }
 
         @Override
-        void onInitialize() {
+        boolean onInitialize() {
             mOnInitializeCalled = true;
+            return true;
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java
index 2c3a7c4..042e3ef8 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/TestSupport.java
@@ -42,7 +42,8 @@
 
     private static ConfigurationInternal createUserConfig(
             @UserIdInt int userId, boolean geoDetectionEnabledSetting) {
-        return new ConfigurationInternal.Builder(userId)
+        return new ConfigurationInternal.Builder()
+                .setUserId(userId)
                 .setUserConfigAllowed(true)
                 .setTelephonyDetectionFeatureSupported(true)
                 .setGeoDetectionFeatureSupported(true)
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
index ab4fe29..f3440f7 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
@@ -16,10 +16,15 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_OK;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_OK;
+
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 
 import org.junit.Test;
@@ -54,8 +59,14 @@
         for (String timeZone : nonExistingTimeZones) {
             TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone);
 
+            TimeZoneProviderStatus expectedProviderStatus =
+                    new TimeZoneProviderStatus.Builder(event.getTimeZoneProviderStatus())
+                            .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_FAILED)
+                            .build();
+
             TimeZoneProviderEvent expectedResultEvent =
-                    TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
+                    TimeZoneProviderEvent.createUncertainEvent(
+                            event.getCreationElapsedMillis(), expectedProviderStatus);
             assertWithMessage(timeZone + " is not a valid time zone")
                     .that(mPreProcessor.preProcess(event))
                     .isEqualTo(expectedResultEvent);
@@ -63,12 +74,17 @@
     }
 
     private static TimeZoneProviderEvent timeZoneProviderEvent(String... timeZoneIds) {
+        TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setConnectivityDependencyStatus(DEPENDENCY_STATUS_OK)
+                .setTimeZoneResolutionOperationStatus(OPERATION_STATUS_OK)
+                .build();
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(Arrays.asList(timeZoneIds))
+                .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
+                .build();
         return TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_TIME_MILLIS,
-                new TimeZoneProviderSuggestion.Builder()
-                        .setTimeZoneIds(Arrays.asList(timeZoneIds))
-                        .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
-                .build());
+                ARBITRARY_TIME_MILLIS, suggestion, providerStatus);
     }
 
 }
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..aafc16d 100644
--- a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
@@ -29,8 +29,10 @@
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 
 @SmallTest
 @RunWith(Enclosed.class)
@@ -51,17 +53,25 @@
         private StringWriter mTestStringWriter;
         private PrintWriter mTestPrintWriter;
 
+        private TestDumpSink mTestConsumer;
         private EventLogger mEventLogger;
 
         @Before
         public void setUp() {
             mTestStringWriter = new StringWriter();
             mTestPrintWriter = new PrintWriter(mTestStringWriter);
+            mTestConsumer = new TestDumpSink();
             mEventLogger = new EventLogger(EVENTS_LOGGER_SIZE, EVENTS_LOGGER_TAG);
         }
 
         @Test
-        public void testThatConsumeOfEmptyLoggerProducesEmptyList() {
+        public void testThatConsumerProducesEmptyListFromEmptyLog() {
+            mEventLogger.dump(mTestConsumer);
+            assertThat(mTestConsumer.getLastKnownConsumedEvents()).isEmpty();
+        }
+
+        @Test
+        public void testThatPrintWriterProducesEmptyListFromEmptyLog() {
             mEventLogger.dump(mTestPrintWriter);
             assertThat(mTestStringWriter.toString()).isEmpty();
         }
@@ -102,10 +112,12 @@
             });
         }
 
+        private TestDumpSink mTestConsumer;
         private EventLogger mEventLogger;
 
         private final StringWriter mTestStringWriter;
         private final PrintWriter mTestPrintWriter;
+
         private final EventLogger.Event[] mEventsToInsert;
         private final EventLogger.Event[] mExpectedEvents;
 
@@ -119,13 +131,27 @@
 
         @Before
         public void setUp() {
+            mTestConsumer = new TestDumpSink();
             mEventLogger = new EventLogger(EVENTS_LOGGER_SIZE, EVENTS_LOGGER_TAG);
         }
 
         @Test
-        public void testThatLoggingWorksAsExpected() {
+        public void testThatConsumerDumpsEventsAsExpected() {
             for (EventLogger.Event event: mEventsToInsert) {
-                mEventLogger.log(event);
+                mEventLogger.enqueue(event);
+            }
+
+            mEventLogger.dump(mTestConsumer);
+
+            assertThat(mTestConsumer.getLastKnownConsumedEvents())
+                    .containsExactlyElementsIn(mExpectedEvents);
+        }
+
+
+        @Test
+        public void testThatPrintWriterDumpsEventsAsExpected() {
+            for (EventLogger.Event event: mEventsToInsert) {
+                mEventLogger.enqueue(event);
             }
 
             mEventLogger.dump(mTestPrintWriter);
@@ -149,11 +175,27 @@
         return stringWriter.toString();
     }
 
-    private static class TestEvent extends EventLogger.Event {
+
+    private static final class TestEvent extends EventLogger.Event {
 
         @Override
         public String eventToString() {
             return getClass().getName() + "@" + Integer.toHexString(hashCode());
         }
     }
+
+    private static final class TestDumpSink implements EventLogger.DumpSink {
+
+        private final ArrayList<EventLogger.Event> mEvents = new ArrayList<>();
+
+        @Override
+        public void sink(String tag, List<EventLogger.Event> events) {
+            mEvents.clear();
+            mEvents.addAll(events);
+        }
+
+        public ArrayList<EventLogger.Event> getLastKnownConsumedEvents() {
+            return new ArrayList<>(mEvents);
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 235849c..c484f45 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -53,7 +53,8 @@
 
     private boolean mIsAvailable = true;
     private boolean mIsInfoLoadSuccessful = true;
-    private long mLatency;
+    private long mOnLatency;
+    private long mOffLatency;
     private int mOffCount;
 
     private int mCapabilities;
@@ -97,7 +98,7 @@
         public long on(long milliseconds, long vibrationId) {
             recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE,
                     /* frequencyHz= */ 0, (int) milliseconds));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(milliseconds, vibrationId);
             return milliseconds;
         }
@@ -105,12 +106,13 @@
         @Override
         public void off() {
             mOffCount++;
+            applyLatency(mOffLatency);
         }
 
         @Override
         public void setAmplitude(float amplitude) {
             mAmplitudes.add(amplitude);
-            applyLatency();
+            applyLatency(mOnLatency);
         }
 
         @Override
@@ -121,7 +123,7 @@
             }
             recordEffectSegment(vibrationId,
                     new PrebakedSegment((int) effect, false, (int) strength));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(EFFECT_DURATION, vibrationId);
             return EFFECT_DURATION;
         }
@@ -141,7 +143,7 @@
                 duration += EFFECT_DURATION + primitive.getDelay();
                 recordEffectSegment(vibrationId, primitive);
             }
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -154,7 +156,7 @@
                 recordEffectSegment(vibrationId, primitive);
             }
             recordBraking(vibrationId, braking);
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -193,10 +195,10 @@
             return mIsInfoLoadSuccessful;
         }
 
-        private void applyLatency() {
+        private void applyLatency(long latencyMillis) {
             try {
-                if (mLatency > 0) {
-                    Thread.sleep(mLatency);
+                if (latencyMillis > 0) {
+                    Thread.sleep(latencyMillis);
                 }
             } catch (InterruptedException e) {
             }
@@ -240,10 +242,15 @@
 
     /**
      * Sets the latency this controller should fake for turning the vibrator hardware on or setting
-     * it's vibration amplitude.
+     * the vibration amplitude.
      */
-    public void setLatency(long millis) {
-        mLatency = millis;
+    public void setOnLatency(long millis) {
+        mOnLatency = millis;
+    }
+
+    /** Sets the latency this controller should fake for turning the vibrator off. */
+    public void setOffLatency(long millis) {
+        mOffLatency = millis;
     }
 
     /** Set the capabilities of the fake vibrator hardware. */
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index a15e4b0..fc830a9 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -1159,7 +1159,7 @@
 
         // 25% of the first waveform step will be spent on the native on() call.
         // 25% of each waveform step will be spent on the native setAmplitude() call..
-        mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4);
+        mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4);
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         int stepCount = totalDuration / stepDuration;
@@ -1190,7 +1190,7 @@
         fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
 
         long latency = 5_000; // 5s
-        fakeVibrator.setLatency(latency);
+        fakeVibrator.setOnLatency(latency);
 
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
@@ -1204,8 +1204,7 @@
         // fail at waitForCompletion(cancellingThread).
         Thread cancellingThread = new Thread(
                 () -> conductor.notifyCancelled(
-                        new Vibration.EndInfo(
-                                Vibration.Status.CANCELLED_BY_USER),
+                        new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER),
                         /* immediate= */ false));
         cancellingThread.start();
 
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index c46fecd..c83afb7 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -826,13 +826,40 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // No segment played is the prebaked CLICK from the second vibration.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up repeating effect.
     }
 
     @Test
+    public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled()
+            throws Exception {
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow.
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+
+        VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{255, 0}, 1);
+        vibrate(service, repeatingEffect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, wait until the off waveform step.
+        assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+
+        // Cancel vibration right before requesting a new one.
+        // This should trigger slow IVibrator.off before setting the vibration status to cancelled.
+        cancelVibrate(service);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                ALARM_ATTRS);
+
+        // Check that second vibration was played.
+        assertTrue(fakeVibrator.getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
+    }
+
+    @Test
     public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -880,10 +907,8 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // The second vibration shouldn't have played any prebaked segment.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
-
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up long effect.
     }
 
diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/Android.bp b/services/tests/servicestests/test-apps/FakeMediaApp/Android.bp
new file mode 100644
index 0000000..a4041b7
--- /dev/null
+++ b/services/tests/servicestests/test-apps/FakeMediaApp/Android.bp
@@ -0,0 +1,37 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+    name: "FakeMediaApp",
+
+    sdk_version: "current",
+
+    srcs: ["**/*.java"],
+
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/FakeMediaApp/AndroidManifest.xml
new file mode 100644
index 0000000..c08ee7a
--- /dev/null
+++ b/services/tests/servicestests/test-apps/FakeMediaApp/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.servicestests.apps.fakemediaapp">
+
+    <application>
+        <receiver
+            android:name=".FakeMediaButtonBroadcastReceiver"
+            android:enabled="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_BUTTON" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>
diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS b/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS
new file mode 100644
index 0000000..55ffde2
--- /dev/null
+++ b/services/tests/servicestests/test-apps/FakeMediaApp/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 137631
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java b/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java
new file mode 100644
index 0000000..41f0cf5
--- /dev/null
+++ b/services/tests/servicestests/test-apps/FakeMediaApp/src/FakeMediaButtonBroadcastReceiver.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.servicestests.apps.fakemediaapp;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class FakeMediaButtonBroadcastReceiver extends BroadcastReceiver {
+
+    private static final String TAG = "FakeMediaButtonBroadcastReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.v(TAG, "onReceive not expected");
+    }
+}
diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
index 3e79407..b8585f2 100644
--- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
+++ b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java
@@ -34,7 +34,8 @@
     public boolean onStartJob(JobParameters params) {
         Log.i(TAG, "Test job executing: " + params.getJobId());
         Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED);
-        reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         sendBroadcast(reportJobStartIntent);
         return true;
     }
@@ -43,7 +44,8 @@
     public boolean onStopJob(JobParameters params) {
         Log.i(TAG, "Test job stopped executing: " + params.getJobId());
         Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED);
-        reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params);
+        reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         sendBroadcast(reportJobStopIntent);
         // Deadline constraint is dropped on reschedule, so it's more reliable to use a new job.
         return false;
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
index c611e38..3e78f9a 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
+++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
@@ -88,3 +88,17 @@
     resource_dirs: ["res"],
     manifest: "AndroidManifestApp5.xml",
 }
+
+android_test_helper_app {
+    name: "PackageParserTestApp6",
+    sdk_version: "current",
+    srcs: ["**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    resource_dirs: ["res"],
+    manifest: "AndroidManifestApp6.xml",
+}
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
index 70fd28d..4dcb442 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
+++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
@@ -31,7 +31,8 @@
         <property android:name="android.cts.PROPERTY_STRING_VIA_RESOURCE" android:value="@string/string_property" />
 
 	    <activity android:name="com.android.servicestests.apps.packageparserapp.MyActivity"
-	              android:exported="true" >
+	              android:exported="true"
+	              android:targetDisplayCategory="automotive">
 	        <property android:name="android.cts.PROPERTY_ACTIVITY" android:value="@integer/integer_property" />
 	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@integer/integer_property" />
 	        <property android:name="android.cts.PROPERTY_STRING" android:value="koala activity" />
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml
new file mode 100644
index 0000000..8e694e1
--- /dev/null
+++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.servicestests.apps.packageparserapp" >
+    <application>
+        <activity android:name="com.android.servicestests.apps.packageparserapp.MyActivity"
+                  android:exported="true"
+                  android:targetDisplayCategory="$automotive">
+        </activity>
+    </application>
+</manifest>
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 91c2fe0..8e81e2d 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -1371,6 +1371,39 @@
         verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
     private void triggerDockIntent() {
         final Intent dockedIntent =
                 new Intent(Intent.ACTION_DOCK_EVENT)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 7986043..582e744 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -62,11 +62,11 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import com.google.android.collect.Lists;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 4b93e35..9c68ddc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -46,10 +46,10 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IntArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.function.TriPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.NotificationManagerService.NotificationAssistants;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 1e94577..581cf43 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.notification;
 
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -30,9 +31,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.intThat;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -49,6 +52,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.pm.VersionedPackage;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerFilter;
@@ -59,16 +63,17 @@
 import android.testing.TestableContext;
 import android.util.ArraySet;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import com.google.common.collect.ImmutableList;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.internal.util.reflection.FieldSetter;
@@ -77,6 +82,7 @@
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
 import java.util.List;
 
 public class NotificationListenersTest extends UiServiceTestCase {
@@ -85,6 +91,8 @@
     private PackageManager mPm;
     @Mock
     private IPackageManager miPm;
+    @Mock
+    private Resources mResources;
 
     @Mock
     NotificationManagerService mNm;
@@ -96,7 +104,8 @@
 
     private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp");
     private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2");
-
+    private ComponentName mUninstalledComponent = new ComponentName("pkg3",
+            "pkg3.NotificationListenerService");
 
     @Before
     public void setUp() throws Exception {
@@ -111,7 +120,7 @@
 
     @Test
     public void testReadExtraTag() throws Exception {
-        String xml = "<" + TAG_REQUESTED_LISTENERS+ ">"
+        String xml = "<" + TAG_REQUESTED_LISTENERS + ">"
                 + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">"
                 + "<allowed types=\"7\" />"
                 + "</listener>"
@@ -131,11 +140,55 @@
     }
 
     @Test
+    public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception {
+        // setup with headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ true));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent);
+    }
+
+    @Test
+    public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled()
+            throws Exception {
+        // setup without headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ false));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent);
+    }
+
+    private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) {
+        ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName));
+        when(mResources
+                .getString(
+                        com.android.internal.R.string.config_defaultListenerAccessPackages))
+                .thenReturn(componentName.getPackageName());
+        when(mContext.getResources()).thenReturn(mResources);
+        doReturn(components).when(mListeners).queryPackageForServices(
+                eq(componentName.getPackageName()),
+                intThat(hasIntBitFlag(MATCH_ANY_USER)),
+                anyInt());
+    }
+
+    public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) {
+        return arg -> arg != null && ((arg & flag) == flag);
+    }
+
+    @Test
     public void testWriteExtraTag() throws Exception {
         NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
         VersionedPackage a1 = new VersionedPackage("pkg1", 243);
         NotificationListenerFilter nlf2 =
-                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
+                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1}));
         mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
         mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 92761427..afec085 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -193,8 +193,6 @@
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.widget.RemoteViews;
 
@@ -206,6 +204,8 @@
 import com.android.internal.logging.InstanceIdSequenceFake;
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -1101,6 +1101,10 @@
                 new NotificationChannel("id", "name", IMPORTANCE_HIGH);
         mBinderService.updateNotificationChannelForPackage(PKG, mUid, updatedChannel);
 
+        // pretend only this following part is called by the app (system permissions are required to
+        // update the notification channel on behalf of the user above)
+        mService.isSystemUid = false;
+
         // Recreating with a lower importance leaves channel unchanged.
         final NotificationChannel dupeChannel =
                 new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_LOW);
@@ -1126,6 +1130,46 @@
     }
 
     @Test
+    public void testCreateNotificationChannels_fromAppCannotSetFields() throws Exception {
+        // Confirm that when createNotificationChannels is called from the relevant app and not
+        // system, then it cannot set fields that can't be set by apps
+        mService.isSystemUid = false;
+
+        final NotificationChannel channel =
+                new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+        channel.setBypassDnd(true);
+        channel.setAllowBubbles(true);
+
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(channel)));
+
+        final NotificationChannel createdChannel =
+                mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
+        assertFalse(createdChannel.canBypassDnd());
+        assertFalse(createdChannel.canBubble());
+    }
+
+    @Test
+    public void testCreateNotificationChannels_fromSystemCanSetFields() throws Exception {
+        // Confirm that when createNotificationChannels is called from system,
+        // then it can set fields that can't be set by apps
+        mService.isSystemUid = true;
+
+        final NotificationChannel channel =
+                new NotificationChannel("id", "name", IMPORTANCE_DEFAULT);
+        channel.setBypassDnd(true);
+        channel.setAllowBubbles(true);
+
+        mBinderService.createNotificationChannels(PKG,
+                new ParceledListSlice(Arrays.asList(channel)));
+
+        final NotificationChannel createdChannel =
+                mBinderService.getNotificationChannel(PKG, mContext.getUserId(), PKG, "id");
+        assertTrue(createdChannel.canBypassDnd());
+        assertTrue(createdChannel.canBubble());
+    }
+
+    @Test
     public void testBlockedNotifications_suspended() throws Exception {
         when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(true);
 
@@ -3088,6 +3132,8 @@
 
     @Test
     public void testDeleteChannelGroupChecksForFgses() throws Exception {
+        // the setup for this test requires it to seem like it's coming from the app
+        mService.isSystemUid = false;
         when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid)))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
         CountDownLatch latch = new CountDownLatch(2);
@@ -3100,7 +3146,7 @@
             ParceledListSlice<NotificationChannel> pls =
                     new ParceledListSlice(ImmutableList.of(notificationChannel));
             try {
-                mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
+                mBinderService.createNotificationChannels(PKG, pls);
             } catch (RemoteException e) {
                 throw new RuntimeException(e);
             }
@@ -3119,8 +3165,10 @@
                 ParceledListSlice<NotificationChannel> pls =
                         new ParceledListSlice(ImmutableList.of(notificationChannel));
                 try {
-                mBinderService.createNotificationChannelsForPackage(PKG, mUid, pls);
-                mBinderService.deleteNotificationChannelGroup(PKG, "group");
+                    // Because existing channels won't have their groups overwritten when the call
+                    // is from the app, this call won't take the channel out of the group
+                    mBinderService.createNotificationChannels(PKG, pls);
+                    mBinderService.deleteNotificationChannelGroup(PKG, "group");
                 } catch (RemoteException e) {
                     throw new RuntimeException(e);
                 }
@@ -7623,8 +7671,30 @@
     }
 
     @Test
+    public void testAddAutomaticZenRule_systemAppIdCallTakesPackageFromOwner() throws Exception {
+        // The multi-user case: where the calling uid doesn't match the system uid, but the calling
+        // *appid* is the system.
+        mService.isSystemUid = false;
+        mService.isSystemAppId = true;
+        ZenModeHelper mockZenModeHelper = mock(ZenModeHelper.class);
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.setZenHelper(mockZenModeHelper);
+        ComponentName owner = new ComponentName("android", "ProviderName");
+        ZenPolicy zenPolicy = new ZenPolicy.Builder().allowAlarms(true).build();
+        boolean isEnabled = true;
+        AutomaticZenRule rule = new AutomaticZenRule("test", owner, owner, mock(Uri.class),
+                zenPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, isEnabled);
+        mBinderService.addAutomaticZenRule(rule, "com.android.settings");
+
+        // verify that zen mode helper gets passed in a package name of "android"
+        verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString());
+    }
+
+    @Test
     public void testAddAutomaticZenRule_nonSystemCallTakesPackageFromArg() throws Exception {
         mService.isSystemUid = false;
+        mService.isSystemAppId = false;
         ZenModeHelper mockZenModeHelper = mock(ZenModeHelper.class);
         when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
                 .thenReturn(true);
@@ -8659,7 +8729,7 @@
         assertEquals("friend", friendChannel.getConversationId());
         assertEquals(null, original.getConversationId());
         assertEquals(original.canShowBadge(), friendChannel.canShowBadge());
-        assertFalse(friendChannel.canBubble()); // can't be modified by app
+        assertEquals(original.canBubble(), friendChannel.canBubble()); // called by system
         assertFalse(original.getId().equals(friendChannel.getId()));
         assertNotNull(friendChannel.getId());
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
deleted file mode 100644
index d765042..0000000
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
+++ /dev/null
@@ -1,551 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.notification;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.app.RemoteInput;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.StyleSpan;
-import android.util.Pair;
-import android.widget.RemoteViews;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.UiServiceTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class NotificationTest extends UiServiceTestCase {
-
-    @Mock
-    ActivityManager mAm;
-
-    @Mock
-    Resources mResources;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testDoesNotStripsExtenders() {
-        Notification.Builder nb = new Notification.Builder(mContext, "channel");
-        nb.extend(new Notification.CarExtender().setColor(Color.RED));
-        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
-        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
-        Notification before = nb.build();
-        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
-
-        assertTrue(before == after);
-
-        assertEquals("different channel", new Notification.TvExtender(before).getChannelId());
-        assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
-        assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyles() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_changeStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle());
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testInboxTextChange() {
-        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
-        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
-    }
-
-    @Test
-    public void testBigTextTextChange() {
-        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("something"));
-        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("else"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
-    }
-
-    @Test
-    public void testBigPictureChange() {
-        Bitmap bitA = mock(Bitmap.class);
-        when(bitA.getGenerationId()).thenReturn(100);
-        Bitmap bitB = mock(Bitmap.class);
-        when(bitB.getGenerationId()).thenReturn(200);
-
-        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
-        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
-    }
-
-    @Test
-    public void testMessagingChange_text() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class)))
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "b", 100, mock(Person.class)))
-                );
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_data() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))
-                                .setData("text", mock(Uri.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_sender() {
-        Person a = mock(Person.class);
-        when(a.getName()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getName()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_key() {
-        Person a = mock(Person.class);
-        when(a.getKey()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getKey()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_ignoreTimeChange() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 1000, mock(Person.class)))
-                );
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testRemoteViews_nullChange() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test");
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(189);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(2);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(1);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferent_null() {
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSame() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentText() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSpannables() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon,
-                        new SpannableStringBuilder().append("test1",
-                                new StyleSpan(Typeface.BOLD),
-                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
-                        intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentNumber() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentIntent() {
-        PendingIntent intent1 = mock(PendingIntent.class);
-        PendingIntent intent2 = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsIgnoresRemoteInputs() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"i", "m"})
-                                .build())
-                        .build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"t", "m"})
-                                .build())
-                        .build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_noRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(false));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
-
-        Notification.Action actionWithRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification.Action actionWithoutRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutRemoteInput)
-                .addAction(actionWithRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(false);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(remoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(
-                                new RemoteInput.Builder("a")
-                                        .setAllowFreeFormInput(false).build())
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(true));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput =
-                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
-        RemoteInput freeformRemoteInput =
-                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
-
-        Notification.Action actionWithFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(freeformRemoteInput)
-                        .build();
-
-        Notification.Action actionWithoutFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutFreeformRemoteInput)
-                .addAction(actionWithFreeformRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(true);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(freeformRemoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
-    }
-}
-
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 598a22b..b64b281 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -110,14 +110,14 @@
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.os.AtomsProto.PackageNotificationPreferences;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.PermissionHelper.PackagePermission;
@@ -2727,7 +2727,7 @@
 
     @Test
     public void testCreateChannel_addToGroup() {
-        NotificationChannelGroup group = new NotificationChannelGroup("group", "");
+        NotificationChannelGroup group = new NotificationChannelGroup("group", "group");
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true);
         NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT);
         assertTrue(mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false));
@@ -3177,8 +3177,8 @@
 
     @Test
     public void testGetNotificationChannelGroupWithChannels() throws Exception {
-        NotificationChannelGroup group = new NotificationChannelGroup("group", "");
-        NotificationChannelGroup other = new NotificationChannelGroup("something else", "");
+        NotificationChannelGroup group = new NotificationChannelGroup("group", "group");
+        NotificationChannelGroup other = new NotificationChannelGroup("something else", "name");
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true);
         mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, other, true);
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 7817e81..a03a1b4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -44,12 +44,12 @@
 import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.IntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 8cf74fb..61a6985 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -32,6 +32,7 @@
 public class TestableNotificationManagerService extends NotificationManagerService {
     int countSystemChecks = 0;
     boolean isSystemUid = true;
+    boolean isSystemAppId = true;
     int countLogSmartSuggestionsVisible = 0;
     Set<Integer> mChannelToastsSent = new HashSet<>();
 
@@ -58,6 +59,12 @@
     }
 
     @Override
+    protected boolean isCallingAppIdSystem() {
+        countSystemChecks++;
+        return isSystemUid || isSystemAppId;
+    }
+
+    @Override
     protected boolean isCallerSystemOrPhone() {
         countSystemChecks++;
         return isSystemUid;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 949455a1..2b6db14 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -33,12 +33,12 @@
 import android.service.notification.ZenModeConfig.EventInfo;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Test;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 2ccdcaa..49edde5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -105,12 +105,12 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.ManagedServices.UserProfiles;
 
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/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 376399a..85c4975 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -324,7 +324,7 @@
 
         // The activity reports fully drawn before windows drawn, then the fully drawn event will
         // be pending (see {@link WindowingModeTransitionInfo#pendingFullyDrawn}).
-        mActivityMetricsLogger.logAppTransitionReportedDrawn(mTopActivity, false);
+        mActivityMetricsLogger.notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */);
         notifyTransitionStarting(mTopActivity);
         // The pending fully drawn event should send when the actual windows drawn event occurs.
         final ActivityMetricsLogger.TransitionInfoSnapshot info = notifyWindowsDrawn(mTopActivity);
@@ -337,7 +337,7 @@
         verifyNoMoreInteractions(mLaunchObserver);
 
         final ActivityMetricsLogger.TransitionInfoSnapshot fullyDrawnInfo = mActivityMetricsLogger
-                .logAppTransitionReportedDrawn(mTopActivity, false /* restoredFromBundle */);
+                .notifyFullyDrawn(mTopActivity, false /* restoredFromBundle */);
         assertWithMessage("Invisible event must be dropped").that(fullyDrawnInfo).isNull();
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 8a18912..079897b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -49,6 +49,7 @@
 import static android.os.Process.NOBODY_UID;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InsetsState.ITYPE_IME;
+import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -146,7 +147,6 @@
 import android.view.IWindowSession;
 import android.view.InsetsSource;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.Surface;
@@ -2002,7 +2002,7 @@
             doReturn(WindowManagerGlobal.ADD_STARTING_NOT_NEEDED).when(session).addToDisplay(
                     any() /* window */,  any() /* attrs */,
                     anyInt() /* viewVisibility */, anyInt() /* displayId */,
-                    any() /* requestedVisibilities */, any() /* outInputChannel */,
+                    anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
                     any() /* outInsetsState */, any() /* outActiveControls */,
                     any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
             mAtm.mWindowManager.mStartingSurfaceController
@@ -2284,8 +2284,7 @@
         doReturn(false).when(mAtm).shouldDisableNonVrUiLocked();
 
         spyOn(mDisplayContent.mDwpcHelper);
-        doReturn(false).when(mDisplayContent.mDwpcHelper).isWindowingModeSupported(
-                WINDOWING_MODE_PINNED);
+        doReturn(false).when(mDisplayContent.mDwpcHelper).isEnteringPipAllowed(anyInt());
 
         assertFalse(activity.checkEnterPictureInPictureState("TEST", false /* beforeStopping */));
     }
@@ -2807,7 +2806,7 @@
         final Task task = activity.getTask();
         final ActivityRecord topActivity = new ActivityBuilder(mAtm).setTask(task).build();
         topActivity.setVisible(false);
-        task.positionChildAt(topActivity, POSITION_TOP);
+        task.positionChildAt(POSITION_TOP, topActivity, false /* includeParents */);
         activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false,
                 true, false, false, false);
         waitUntilHandlersIdle();
@@ -2884,6 +2883,7 @@
         fragmentSetup.accept(taskFragment1, new Rect(0, 0, width / 2, height));
         task.addChild(taskFragment1, POSITION_TOP);
         assertEquals(task, activity1.mStartingData.mAssociatedTask);
+        assertEquals(activity1.mStartingData, task.mSharedStartingData);
 
         final TaskFragment taskFragment2 = new TaskFragment(
                 mAtm, null /* fragmentToken */, false /* createdByOrganizer */);
@@ -2903,7 +2903,6 @@
 
         verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl),
                 eq(task.mSurfaceControl));
-        assertEquals(activity1.mStartingData, startingWindow.mStartingData);
         assertEquals(task.mSurfaceControl, startingWindow.getAnimationLeashParent());
         assertEquals(taskFragment1.getBounds(), activity1.getBounds());
         // The activity was resized by task fragment, but starting window must still cover the task.
@@ -2914,6 +2913,7 @@
         activity1.onFirstWindowDrawn(activityWindow);
         activity2.onFirstWindowDrawn(activityWindow);
         assertNull(activity1.mStartingWindow);
+        assertNull(task.mSharedStartingData);
     }
 
     @Test
@@ -2989,10 +2989,10 @@
         final WindowManager.LayoutParams attrs =
                 new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING);
         final TestWindowState startingWindow = createWindowState(attrs, activity);
-        activity.startingDisplayed = true;
+        activity.mStartingData = mock(StartingData.class);
         activity.addWindow(startingWindow);
         assertTrue("Starting window should be present", activity.hasStartingWindow());
-        activity.startingDisplayed = false;
+        activity.mStartingData = null;
         assertTrue("Starting window should be present", activity.hasStartingWindow());
 
         activity.removeChild(startingWindow);
@@ -3233,9 +3233,7 @@
         app2.mActivityRecord.commitVisibility(false, false);
 
         // app1 requests IME visible.
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_IME, true);
-        app1.setRequestedVisibilities(requestedVisibilities);
+        app1.setRequestedVisibleTypes(ime(), ime());
         mDisplayContent.getInsetsStateController().onInsetsModified(app1);
 
         // Verify app1's IME insets is visible and app2's IME insets frozen flag set.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2b0e76c..fc1989e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.app.Activity.RESULT_CANCELED;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
 import static android.app.ActivityManager.PROCESS_STATE_TOP;
 import static android.app.ActivityManager.START_ABORTED;
 import static android.app.ActivityManager.START_CANCELED;
@@ -98,6 +99,7 @@
 import android.util.Pair;
 import android.util.Size;
 import android.view.Gravity;
+import android.view.RemoteAnimationAdapter;
 import android.window.TaskFragmentOrganizerToken;
 
 import androidx.test.filters.SmallTest;
@@ -608,10 +610,9 @@
     @Test
     public void testBackgroundActivityStartsAllowed_noStartsAborted() {
         doReturn(true).when(mAtm).isBackgroundActivityStartsEnabled();
-
         runAndVerifyBackgroundActivityStartsSubtest("allowed_noStartsAborted", false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
     }
 
@@ -620,97 +621,220 @@
      * disallowed.
      */
     @Test
-    public void testBackgroundActivityStartsDisallowed_unsupportedStartsAborted() {
+    public void testBackgroundActivityStartsDisallowed_unsupportedUsecaseAborted() {
         doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
-
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_unsupportedUsecase_aborted", true,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that unsupported usecases are aborted when background starts are
+     * disallowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callingUidProcessStateTopAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callingUidProcessStateTop_aborted", true,
                 UNIMPORTANT_UID, false, PROCESS_STATE_TOP,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that unsupported usecases are aborted when background starts are
+     * disallowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_realCallingUidProcessStateTopAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_realCallingUidProcessStateTop_aborted", true,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
                 UNIMPORTANT_UID2, false, PROCESS_STATE_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that unsupported usecases are aborted when background starts are
+     * disallowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_hasForegroundActivitiesAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_hasForegroundActivities_aborted", true,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 true, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that unsupported usecases are aborted when background starts are
+     * disallowed.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_pinnedSingleInstanceAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_pinned_singleinstance_aborted", true,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false, true);
-
     }
 
     /**
      * This test ensures that supported usecases aren't aborted when background starts are
-     * disallowed.
-     * The scenarios each have only one condition that makes them supported.
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the calling process runs as ROOT_UID.
      */
     @Test
-    public void testBackgroundActivityStartsDisallowed_supportedStartsNotAborted() {
+    public void testBackgroundActivityStartsDisallowed_rootUidNotAborted() {
         doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
-
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_rootUid_notAborted", false,
-                Process.ROOT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                Process.ROOT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the calling process is running as SYSTEM_UID.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_systemUidNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_systemUid_notAborted", false,
-                Process.SYSTEM_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                Process.SYSTEM_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the calling process is running as NFC_UID.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_nfcUidNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest("disallowed_nfcUid_notAborted", false,
-                Process.NFC_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                Process.NFC_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the calling process has a visible window.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callingUidHasVisibleWindowNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callingUidHasVisibleWindow_notAborted", false,
-                UNIMPORTANT_UID, true, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, true, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the real calling process (pending intent) has a visible window.
+     */
+    @Test
+    public void
+            testBackgroundActivityStartsDisallowed_realCallingUidHasVisibleWindowNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_realCallingUidHasVisibleWindow_notAborted", false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, true, PROCESS_STATE_TOP + 1,
-                false, false, false, false, false);
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, true, PROCESS_STATE_BOUND_TOP,
+                false, false, false, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller is in the recent activity list.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callerIsRecentsNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callerIsRecents_notAborted", false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, true, false, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller is temporarily (10s) allowed to start.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callerIsAllowedNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callerIsAllowed_notAborted", false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, true, false, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller explicitly has background activity start privilege.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callerIsInstrumentingWithBASPnotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callerIsInstrumentingWithBackgroundActivityStartPrivileges_notAborted",
                 false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, true, false);
+    }
+
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller is a device owner.
+     */
+    @Test
+    public void
+            testBackgroundActivityStartsDisallowed_callingPackageNameIsDeviceOwnerNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callingPackageNameIsDeviceOwner_notAborted", false,
-                UNIMPORTANT_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, true);
+    }
 
+    /**
+     * This test ensures that supported usecases aren't aborted when background starts are
+     * disallowed. Each scenarios tests one condition that makes them supported in isolation. In
+     * this case the caller is an IME.
+     */
+    @Test
+    public void testBackgroundActivityStartsDisallowed_callingPackageNameIsImeNotAborted() {
+        doReturn(false).when(mAtm).isBackgroundActivityStartsEnabled();
         setupImeWindow();
         runAndVerifyBackgroundActivityStartsSubtest(
                 "disallowed_callingPackageNameIsIme_notAborted", false,
-                CURRENT_IME_UID, false, PROCESS_STATE_TOP + 1,
-                UNIMPORTANT_UID2, false, PROCESS_STATE_TOP + 1,
+                CURRENT_IME_UID, false, PROCESS_STATE_BOUND_TOP,
+                UNIMPORTANT_UID2, false, PROCESS_STATE_BOUND_TOP,
                 false, false, false, false, false);
-
     }
 
     private void runAndVerifyBackgroundActivityStartsSubtest(String name, boolean shouldHaveAborted,
@@ -1138,6 +1262,26 @@
     }
 
     @Test
+    public void testRecycleTaskWakeUpWhenDreaming() {
+        doNothing().when(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString());
+        doReturn(true).when(mWm.mAtmService).isDreaming();
+        final ActivityStarter starter = prepareStarter(0 /* flags */);
+        final ActivityRecord target = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        starter.mStartActivity = target;
+        target.mVisibleRequested = false;
+        target.setTurnScreenOn(true);
+        // Assume the flag was consumed by relayout.
+        target.setCurrentLaunchCanTurnScreenOn(false);
+        startActivityInner(starter, target, null /* source */, null /* options */,
+                null /* inTask */, null /* inTaskFragment */);
+        // The flag should be set again when resuming (from recycleTask) the target as top.
+        assertTrue(target.currentLaunchCanTurnScreenOn());
+        // In real case, dream activity has a higher priority (TaskDisplayArea#getPriority) that
+        // will be put at a higher z-order. So it relies on wakeUp() to be dismissed.
+        verify(mWm.mAtmService.mTaskSupervisor).wakeUp(anyString());
+    }
+
+    @Test
     public void testTargetTaskInSplitScreen() {
         final ActivityStarter starter =
                 prepareStarter(FLAG_ACTIVITY_LAUNCH_ADJACENT, false /* mockGetRootTask */);
@@ -1323,6 +1467,32 @@
     }
 
     @Test
+    public void testRemoteAnimation_appliesToExistingTask() {
+        final ActivityStarter starter = prepareStarter(0, false);
+
+        // Put an activity on default display as the top focused activity.
+        ActivityRecord r = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Intent intent = new Intent();
+        intent.setComponent(ActivityBuilder.getDefaultComponent());
+        starter.setReason("testRemoteAnimation_newTask")
+                .setIntent(intent)
+                .execute();
+
+        assertNull(mRootWindowContainer.topRunningActivity().mPendingRemoteAnimation);
+
+        // Relaunch the activity with remote animation indicated in options.
+        final RemoteAnimationAdapter adaptor = mock(RemoteAnimationAdapter.class);
+        final ActivityOptions options = ActivityOptions.makeRemoteAnimation(adaptor);
+        starter.setReason("testRemoteAnimation_existingTask")
+                .setIntent(intent)
+                .setActivityOptions(options.toBundle())
+                .execute();
+
+        // Verify the remote animation is updated.
+        assertEquals(adaptor, mRootWindowContainer.topRunningActivity().mPendingRemoteAnimation);
+    }
+
+    @Test
     public void testStartLaunchIntoPipActivity() {
         final ActivityStarter starter = prepareStarter(0, false);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index d5e336b..eed32d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -40,14 +40,18 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 
+import android.app.ActivityOptions;
 import android.app.WaitResult;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.os.Binder;
 import android.os.ConditionVariable;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
@@ -308,4 +312,40 @@
         waitHandlerIdle(mAtm.mH);
         verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked");
     }
+
+    /** Verifies that launch from recents sets the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        IBinder launchCookie = new Binder("test_launch_cookie");
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchCookie(launchCookie);
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isEqualTo(launchCookie);
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
+
+    /** Verifies that launch from recents doesn't set the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withoutLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(
+                ActivityOptions.makeBasic().toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isNull();
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 513791d..0332c4b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -1300,6 +1300,8 @@
         activity.allDrawn = true;
         // Skip manipulate the SurfaceControl.
         doNothing().when(activity).setDropInputMode(anyInt());
+        // Assume the activity contains a window.
+        doReturn(true).when(activity).hasChild();
         // Make sure activity can create remote animation target.
         doReturn(mock(RemoteAnimationTarget.class)).when(activity).createRemoteAnimationTarget(
                 any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index f61effa..32c95fa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -16,8 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -27,7 +25,6 @@
 import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_NONE;
-import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
@@ -321,7 +318,6 @@
         final ActivityRecord activity2 = createActivityRecord(dc2);
 
         activity1.allDrawn = true;
-        activity1.startingDisplayed = true;
         activity1.startingMoved = true;
 
         // Simulate activity resume / finish flows to prepare app transition & set visibility,
@@ -412,50 +408,38 @@
     }
 
     @Test
-    public void testExcludeLauncher() {
+    public void testDelayWhileRecents() {
         final DisplayContent dc = createNewDisplay(Display.STATE_ON);
         doReturn(false).when(dc).onDescendantOrientationChanged(any());
         final Task task = createTask(dc);
 
-        // Simulate activity1 launches activity2
+        // Simulate activity1 launches activity2.
         final ActivityRecord activity1 = createActivityRecord(task);
         activity1.setVisible(true);
         activity1.mVisibleRequested = false;
         activity1.allDrawn = true;
-        dc.mClosingApps.add(activity1);
         final ActivityRecord activity2 = createActivityRecord(task);
         activity2.setVisible(false);
         activity2.mVisibleRequested = true;
         activity2.allDrawn = true;
+
+        dc.mClosingApps.add(activity1);
         dc.mOpeningApps.add(activity2);
         dc.prepareAppTransition(TRANSIT_OPEN);
-
-        // Simulate start recents
-        final ActivityRecord homeActivity = createActivityRecord(dc, WINDOWING_MODE_FULLSCREEN,
-                ACTIVITY_TYPE_HOME);
-        homeActivity.setVisible(false);
-        homeActivity.mVisibleRequested = true;
-        homeActivity.allDrawn = true;
-        dc.mOpeningApps.add(homeActivity);
-        dc.prepareAppTransition(TRANSIT_NONE);
-        doReturn(true).when(task)
-                .isSelfAnimating(anyInt(), eq(ANIMATION_TYPE_RECENTS));
+        assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_OPEN));
 
         // Wait until everything in animation handler get executed to prevent the exiting window
         // from being removed during WindowSurfacePlacer Traversal.
         waitUntilHandlersIdle();
 
+        // Start recents
+        doReturn(true).when(task)
+                .isSelfAnimating(anyInt(), eq(ANIMATION_TYPE_RECENTS));
+
         dc.mAppTransitionController.handleAppTransitionReady();
 
-        verify(activity1).commitVisibility(eq(false), anyBoolean(), anyBoolean());
-        verify(activity1).applyAnimation(any(), eq(TRANSIT_OLD_ACTIVITY_OPEN), eq(false),
-                anyBoolean(), any());
-        verify(activity2).commitVisibility(eq(true), anyBoolean(), anyBoolean());
-        verify(activity2).applyAnimation(any(), eq(TRANSIT_OLD_ACTIVITY_OPEN), eq(true),
-                anyBoolean(), any());
-        verify(homeActivity).commitVisibility(eq(true), anyBoolean(), anyBoolean());
-        verify(homeActivity, never()).applyAnimation(any(), anyInt(), anyBoolean(), anyBoolean(),
-                any());
+        verify(activity1, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
+        verify(activity2, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c3d49e1..f3f56e0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -98,28 +98,31 @@
     @Test
     public void backTypeCrossTaskWhenBackToPreviousTask() {
         Task taskA = createTask(mDefaultDisplay);
-        createActivityRecord(taskA);
+        ActivityRecord recordA = createActivityRecord(taskA);
+        Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());
+
         withSystemCallback(createTopTaskWithActivity());
         BackNavigationInfo backNavigationInfo = startBackNavigation();
         assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
         assertThat(typeToString(backNavigationInfo.getType()))
                 .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_TASK));
+
+        // verify if back animation would start.
+        verify(mBackNavigationController).scheduleAnimationLocked(
+                eq(BackNavigationInfo.TYPE_CROSS_TASK), any(), eq(mBackAnimationAdapter),
+                any());
     }
 
     @Test
     public void backTypeCrossActivityWhenBackToPreviousActivity() {
-        Task task = createTopTaskWithActivity();
-        WindowState window = createAppWindow(task, FIRST_APPLICATION_WINDOW, "window");
-        addToWindowMap(window, true);
-        IOnBackInvokedCallback callback = createOnBackInvokedCallback();
-        window.setOnBackInvokedCallbackInfo(
-                new OnBackInvokedCallbackInfo(callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM));
+        CrossActivityTestCase testCase = createTopTaskWithTwoActivities();
+        IOnBackInvokedCallback callback = withSystemCallback(testCase.task);
+
         BackNavigationInfo backNavigationInfo = startBackNavigation();
         assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
+        assertThat(backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
         assertThat(typeToString(backNavigationInfo.getType()))
                 .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_ACTIVITY));
-        assertWithMessage("Activity callback").that(
-                backNavigationInfo.getOnBackInvokedCallback()).isEqualTo(callback);
     }
 
     @Test
@@ -242,7 +245,7 @@
     private IOnBackInvokedCallback createOnBackInvokedCallback() {
         return new IOnBackInvokedCallback.Stub() {
             @Override
-            public void onBackStarted() {
+            public void onBackStarted(BackEvent backEvent) {
             }
 
             @Override
@@ -295,6 +298,34 @@
         return task;
     }
 
+    @NonNull
+    private CrossActivityTestCase createTopTaskWithTwoActivities() {
+        Task task = createTask(mDefaultDisplay);
+        ActivityRecord record1 = createActivityRecord(task);
+        ActivityRecord record2 = createActivityRecord(task);
+        // enable OnBackInvokedCallbacks
+        record2.info.applicationInfo.privateFlagsExt |=
+                PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK;
+        WindowState window1 = createWindow(null, FIRST_APPLICATION_WINDOW, record1, "window1");
+        WindowState window2 = createWindow(null, FIRST_APPLICATION_WINDOW, record2, "window2");
+        when(task.mSurfaceControl.isValid()).thenReturn(true);
+        when(record1.mSurfaceControl.isValid()).thenReturn(true);
+        when(record2.mSurfaceControl.isValid()).thenReturn(true);
+        Mockito.doNothing().when(task).reparentSurfaceControl(any(), any());
+        Mockito.doNothing().when(record1).reparentSurfaceControl(any(), any());
+        Mockito.doNothing().when(record2).reparentSurfaceControl(any(), any());
+        mAtm.setFocusedTask(task.mTaskId, record1);
+        mAtm.setFocusedTask(task.mTaskId, record2);
+        addToWindowMap(window1, true);
+        addToWindowMap(window2, true);
+
+        CrossActivityTestCase testCase = new CrossActivityTestCase();
+        testCase.task = task;
+        testCase.recordBack = record1;
+        testCase.recordFront = record2;
+        return testCase;
+    }
+
     private void addToWindowMap(WindowState window, boolean focus) {
         mWm.mWindowMap.put(window.mClient.asBinder(), window);
         if (focus) {
@@ -303,4 +334,10 @@
             doReturn(window).when(mWm).getFocusedWindowLocked();
         }
     }
+
+    private class CrossActivityTestCase {
+        public Task task;
+        public ActivityRecord recordBack;
+        public ActivityRecord recordFront;
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index ef84a4b..e85b574 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -24,7 +24,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -139,34 +138,12 @@
     }
 
     @Test
-    public void testDimAboveNoChildCreatesSurface() {
-        final float alpha = 0.8f;
-        mDimmer.dimAbove(mTransaction, alpha);
-
-        SurfaceControl dimLayer = getDimLayer();
-
-        assertNotNull("Dimmer should have created a surface", dimLayer);
-
-        verify(mTransaction).setAlpha(dimLayer, alpha);
-        verify(mTransaction).setLayer(dimLayer, Integer.MAX_VALUE);
-    }
-
-    @Test
-    public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() {
-        float alpha = 0.8f;
-        mDimmer.dimAbove(mTransaction, alpha);
-        final SurfaceControl firstSurface = getDimLayer();
-
-        alpha = 0.9f;
-        mDimmer.dimAbove(mTransaction, alpha);
-
-        assertEquals(firstSurface, getDimLayer());
-        verify(mTransaction).setAlpha(firstSurface, 0.9f);
-    }
-
-    @Test
     public void testUpdateDimsAppliesCrop() {
-        mDimmer.dimAbove(mTransaction, 0.8f);
+        TestWindowContainer child = new TestWindowContainer(mWm);
+        mHost.addChild(child, 0);
+
+        final float alpha = 0.8f;
+        mDimmer.dimAbove(mTransaction, child, alpha);
 
         int width = 100;
         int height = 300;
@@ -178,17 +155,6 @@
     }
 
     @Test
-    public void testDimAboveNoChildNotReset() {
-        mDimmer.dimAbove(mTransaction, 0.8f);
-        SurfaceControl dimLayer = getDimLayer();
-        mDimmer.resetDimStates();
-
-        mDimmer.updateDims(mTransaction, new Rect());
-        verify(mTransaction).show(getDimLayer());
-        verify(mTransaction, never()).remove(dimLayer);
-    }
-
-    @Test
     public void testDimAboveWithChildCreatesSurfaceAboveChild() {
         TestWindowContainer child = new TestWindowContainer(mWm);
         mHost.addChild(child, 0);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 11ae5d4..37ab9a0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -34,13 +34,14 @@
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
 import static android.view.DisplayCutout.fromBoundingRect;
-import static android.view.InsetsState.ITYPE_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
@@ -130,7 +131,6 @@
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -1399,10 +1399,7 @@
         win.getAttrs().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
         win.getAttrs().privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION;
         win.getAttrs().insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false);
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        win.setRequestedVisibilities(requestedVisibilities);
+        win.setRequestedVisibleTypes(0, navigationBars() | statusBars());
         win.mActivityRecord.mTargetSdk = P;
 
         performLayout(dc);
@@ -2314,6 +2311,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_NORMAL,
+                config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
@@ -2355,6 +2354,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_LARGE, testDisplayContent
+                .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
@@ -2482,7 +2483,7 @@
                 createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
         mDisplayContent.setImeLayeringTarget(imeAppTarget);
         spyOn(imeAppTarget);
-        doReturn(true).when(imeAppTarget).getRequestedVisibility(ITYPE_IME);
+        doReturn(true).when(imeAppTarget).isRequestedVisible(ime());
         assertEquals(imeChildWindow, mDisplayContent.findFocusedWindow());
 
         // Verify imeChildWindow doesn't be focused window if the next IME target does not
@@ -2507,7 +2508,7 @@
                 createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget");
         mDisplayContent.setImeLayeringTarget(imeAppTarget);
         spyOn(imeAppTarget);
-        doReturn(true).when(imeAppTarget).getRequestedVisibility(ITYPE_IME);
+        doReturn(true).when(imeAppTarget).isRequestedVisible(ime());
         assertEquals(imeMenuDialog, mDisplayContent.findFocusedWindow());
 
         // Verify imeMenuDialog doesn't be focused window if the next IME target is closing.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index a980765..d99946f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -45,6 +45,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
@@ -371,14 +372,14 @@
         final InsetsControlTarget controlTarget = mock(InsetsControlTarget.class);
         when(provider.getControlTarget()).thenReturn(controlTarget);
         when(windowState.getControllableInsetProvider()).thenReturn(provider);
-        when(controlTarget.getRequestedVisibility(anyInt())).thenReturn(true);
+        when(controlTarget.isRequestedVisible(anyInt())).thenReturn(true);
 
         displayPolicy.setCanSystemBarsBeShownByUser(false);
         displayPolicy.requestTransientBars(windowState, true);
-        verify(controlTarget, never()).showInsets(anyInt(), anyBoolean());
+        verify(controlTarget, never()).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
 
         displayPolicy.setCanSystemBarsBeShownByUser(true);
         displayPolicy.requestTransientBars(windowState, true);
-        verify(controlTarget).showInsets(anyInt(), anyBoolean());
+        verify(controlTarget).showInsets(anyInt(), anyBoolean(), any() /* statsToken */);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index 21197ba..db1d15a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -246,5 +246,10 @@
         public boolean canShowTasksInRecents() {
             return true;
         }
+
+        @Override
+        public boolean isEnteringPipAllowed(int uid) {
+            return true;
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 18a1caa..9d839fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -29,7 +29,6 @@
 
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayAddress;
@@ -37,6 +36,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
 
 import org.junit.After;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index ac3d0f0..75c5b6e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -213,7 +213,7 @@
         assertThat(newTaskBounds).isEqualTo(newDagBounds);
 
         // Activity config bounds is unchanged, size compat bounds is (860x[860x860/1200=616])
-        assertThat(mFirstActivity.getSizeCompatScale()).isLessThan(1f);
+        assertThat(mFirstActivity.getCompatScale()).isLessThan(1f);
         assertThat(activityConfigBounds.width()).isEqualTo(activityBounds.width());
         assertThat(activityConfigBounds.height()).isEqualTo(activityBounds.height());
         assertThat(activitySizeCompatBounds.height()).isEqualTo(newTaskBounds.height());
diff --git a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
index 13ebc93..0568b38 100644
--- a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
@@ -25,6 +25,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -36,6 +37,8 @@
 import android.view.Surface;
 import android.view.SurfaceControl;
 
+import com.android.server.wm.RefreshRatePolicy.FrameRateVote;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -50,12 +53,18 @@
 @Presubmit
 @RunWith(WindowTestRunner.class)
 public class FrameRateSelectionPriorityTests extends WindowTestsBase {
-    private static final float FLOAT_TOLERANCE = 0.01f;
     private static final int LOW_MODE_ID = 3;
 
     private DisplayPolicy mDisplayPolicy = mock(DisplayPolicy.class);
     private RefreshRatePolicy mRefreshRatePolicy;
     private HighRefreshRateDenylist mDenylist = mock(HighRefreshRateDenylist.class);
+    private FrameRateVote mTempFrameRateVote = new FrameRateVote();
+
+    private static final FrameRateVote FRAME_RATE_VOTE_NONE = new FrameRateVote();
+    private static final FrameRateVote FRAME_RATE_VOTE_60_EXACT =
+            new FrameRateVote(60, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+    private static final FrameRateVote FRAME_RATE_VOTE_60_PREFERRED =
+            new FrameRateVote(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
 
     WindowState createWindow(String name) {
         WindowState window = createWindow(null, TYPE_APPLICATION, name);
@@ -85,12 +94,12 @@
         assertNotNull("Window state is created", appWindow);
 
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority doesn't change.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Call the function a few times.
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
@@ -109,16 +118,15 @@
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
         assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy()
                 .getPreferredModeId(appWindow), 0);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
-        assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy()
-                .getPreferredRefreshRate(appWindow), 0, FLOAT_TOLERANCE);
-
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
+        assertFalse(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy()
+                .updateFrameRateVote(appWindow));
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority stays MAX_VALUE.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
 
@@ -127,7 +135,7 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes to 1.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), 1);
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
@@ -138,27 +146,27 @@
     public void testApplicationInFocusWithModeId() {
         final WindowState appWindow = createWindow("appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Application is in focus.
         appWindow.mToken.mDisplayContent.mCurrentFocus = appWindow;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
         // Update the mode ID to a requested number.
         appWindow.mAttrs.preferredDisplayModeId = 1;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 0);
-        assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_60_EXACT);
 
         // Remove the mode ID request.
         appWindow.mAttrs.preferredDisplayModeId = 0;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Verify we called actions on Transactions correctly.
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
@@ -175,7 +183,7 @@
     public void testApplicationNotInFocusWithModeId() {
         final WindowState appWindow = createWindow("appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         final WindowState inFocusWindow = createWindow("inFocus");
         appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow;
@@ -183,14 +191,14 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // The window is not in focus.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Update the mode ID to a requested number.
         appWindow.mAttrs.preferredDisplayModeId = 1;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 2);
-        assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_60_EXACT);
 
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
@@ -204,7 +212,7 @@
     public void testApplicationNotInFocusWithoutModeId() {
         final WindowState appWindow = createWindow("appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         final WindowState inFocusWindow = createWindow("inFocus");
         appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow;
@@ -212,14 +220,14 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // The window is not in focus.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Make sure that the mode ID is not set.
         appWindow.mAttrs.preferredDisplayModeId = 0;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority doesn't change.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
@@ -237,11 +245,10 @@
         when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
 
         assertEquals(0, mRefreshRatePolicy.getPreferredModeId(appWindow));
-        assertEquals(60, mRefreshRatePolicy.getPreferredRefreshRate(appWindow), FLOAT_TOLERANCE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         assertEquals(RefreshRatePolicy.LAYER_PRIORITY_UNSET, appWindow.mFrameRateSelectionPriority);
-        assertEquals(60, appWindow.mAppPreferredFrameRate, FLOAT_TOLERANCE);
+        assertEquals(FRAME_RATE_VOTE_60_EXACT, appWindow.mFrameRateVote);
 
         // Call the function a few times.
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
@@ -262,19 +269,19 @@
                 .thenReturn(DisplayManager.SWITCHING_TYPE_NONE);
 
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Update the mode ID to a requested number.
         appWindow.mAttrs.preferredDisplayModeId = 1;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
 
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         // Remove the mode ID request.
         appWindow.mAttrs.preferredDisplayModeId = 0;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
 
-        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mFrameRateVote, FRAME_RATE_VOTE_NONE);
 
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
@@ -292,11 +299,10 @@
         appWindow.mAttrs.preferredRefreshRate = 60;
 
         assertEquals(0, mRefreshRatePolicy.getPreferredModeId(appWindow));
-        assertEquals(60, mRefreshRatePolicy.getPreferredRefreshRate(appWindow), FLOAT_TOLERANCE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         assertEquals(RefreshRatePolicy.LAYER_PRIORITY_UNSET, appWindow.mFrameRateSelectionPriority);
-        assertEquals(60, appWindow.mAppPreferredFrameRate, FLOAT_TOLERANCE);
+        assertEquals(FRAME_RATE_VOTE_60_PREFERRED, appWindow.mFrameRateVote);
 
         // Call the function a few times.
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
@@ -307,6 +313,6 @@
                 any(SurfaceControl.class), anyInt());
         verify(appWindow.getPendingTransaction(), times(1)).setFrameRate(
                 appWindow.getSurfaceControl(), 60,
-                Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS);
+                Surface.FRAME_RATE_COMPATIBILITY_DEFAULT, Surface.CHANGE_FRAME_RATE_ALWAYS);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
index c839d12..a26cad9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ImeInsetsSourceProviderTest.java
@@ -55,7 +55,7 @@
         mDisplayContent.setImeControlTarget(popup);
         mDisplayContent.setImeLayeringTarget(appWin);
         popup.mAttrs.format = PixelFormat.TRANSPARENT;
-        mImeProvider.scheduleShowImePostLayout(appWin);
+        mImeProvider.scheduleShowImePostLayout(appWin, null /* statsToken */);
         assertTrue(mImeProvider.isReadyToShowIme());
     }
 
@@ -64,7 +64,7 @@
         WindowState target = createWindow(null, TYPE_APPLICATION, "app");
         mDisplayContent.setImeLayeringTarget(target);
         mDisplayContent.updateImeInputAndControlTarget(target);
-        mImeProvider.scheduleShowImePostLayout(target);
+        mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
         assertTrue(mImeProvider.isReadyToShowIme());
     }
 
@@ -78,11 +78,33 @@
         mDisplayContent.setImeLayeringTarget(target);
         mDisplayContent.setImeControlTarget(target);
 
-        mImeProvider.scheduleShowImePostLayout(target);
+        mImeProvider.scheduleShowImePostLayout(target, null /* statsToken */);
         assertFalse(mImeProvider.isImeShowing());
         mImeProvider.checkShowImePostLayout();
         assertTrue(mImeProvider.isImeShowing());
         mImeProvider.setImeShowing(false);
         assertFalse(mImeProvider.isImeShowing());
     }
+
+    @Test
+    public void testSetFrozen() {
+        WindowState ime = createWindow(null, TYPE_INPUT_METHOD, "ime");
+        makeWindowVisibleAndDrawn(ime);
+        mImeProvider.setWindowContainer(ime, null, null);
+        mImeProvider.setServerVisible(true);
+        mImeProvider.setClientVisible(true);
+        mImeProvider.updateVisibility();
+        assertTrue(mImeProvider.getSource().isVisible());
+
+        // Freezing IME states and set the server visible as false.
+        mImeProvider.setFrozen(true);
+        mImeProvider.setServerVisible(false);
+        // Expect the IME insets visible won't be changed.
+        assertTrue(mImeProvider.getSource().isVisible());
+
+        // Unfreeze IME states and expect the IME insets became invisible due to pending IME
+        // visible state updated.
+        mImeProvider.setFrozen(false);
+        assertFalse(mImeProvider.getSource().isVisible());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index f2bc47d..fd2a1d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -19,11 +19,15 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES;
+import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES;
 import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
@@ -51,7 +55,7 @@
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
+import android.view.WindowInsets;
 
 import androidx.test.filters.SmallTest;
 
@@ -74,7 +78,7 @@
     @Test
     public void testControlsForDispatch_regular() {
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
 
@@ -86,7 +90,7 @@
     @Test
     public void testControlsForDispatch_multiWindowTaskVisible() {
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final WindowState win = createWindow(null, WINDOWING_MODE_MULTI_WINDOW,
                 ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app");
@@ -99,7 +103,7 @@
     @Test
     public void testControlsForDispatch_freeformTaskVisible() {
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final WindowState win = createWindow(null, WINDOWING_MODE_FREEFORM,
                 ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app");
@@ -112,7 +116,7 @@
     @Test
     public void testControlsForDispatch_forceStatusBarVisible() {
         addStatusBar().mAttrs.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
 
@@ -126,7 +130,7 @@
         addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade").mAttrs.privateFlags |=
                 PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
 
@@ -139,7 +143,7 @@
     public void testControlsForDispatch_statusBarForceShowNavigation_butFocusedAnyways() {
         WindowState notifShade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade");
         notifShade.mAttrs.privateFlags |= PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(notifShade);
         InsetsSourceControl[] controls
@@ -155,7 +159,7 @@
         mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController());
         mDisplayContent.getInsetsPolicy().setRemoteInsetsControllerControlsSystemBars(true);
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
 
@@ -166,13 +170,11 @@
     @Test
     public void testControlsForDispatch_topAppHidesStatusBar() {
         addStatusBar();
-        addWindow(TYPE_NAVIGATION_BAR, "navBar");
+        addNavigationBar();
 
         // Add a fullscreen (MATCH_PARENT x MATCH_PARENT) app window which hides status bar.
         final WindowState fullscreenApp = addWindow(TYPE_APPLICATION, "fullscreenApp");
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        fullscreenApp.setRequestedVisibilities(requestedVisibilities);
+        fullscreenApp.setRequestedVisibleTypes(0, WindowInsets.Type.statusBars());
 
         // Add a non-fullscreen dialog window.
         final WindowState dialog = addWindow(TYPE_APPLICATION, "dialog");
@@ -205,9 +207,8 @@
 
         // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't.
         final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp");
-        final InsetsVisibilities newRequestedVisibilities = new InsetsVisibilities();
-        newRequestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true);
-        newFocusedFullscreenApp.setRequestedVisibilities(newRequestedVisibilities);
+        newFocusedFullscreenApp.setRequestedVisibleTypes(
+                WindowInsets.Type.statusBars(), WindowInsets.Type.statusBars());
         // Make sure status bar is hidden by previous insets state.
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(fullscreenApp);
 
@@ -261,17 +262,15 @@
         final WindowState statusBar = addStatusBar();
         statusBar.setHasSurface(true);
         statusBar.getControllableInsetProvider().setServerVisible(true);
-        final WindowState navBar = addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar");
+        final WindowState navBar = addNavigationBar();
         navBar.setHasSurface(true);
         navBar.getControllableInsetProvider().setServerVisible(true);
         final InsetsPolicy policy = spy(mDisplayContent.getInsetsPolicy());
         doNothing().when(policy).startAnimation(anyBoolean(), any());
 
         // Make both system bars invisible.
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false);
-        mAppWindow.setRequestedVisibilities(requestedVisibilities);
+        mAppWindow.setRequestedVisibleTypes(
+                0, navigationBars() | statusBars());
         policy.updateBarControlTarget(mAppWindow);
         waitUntilWindowAnimatorIdle();
         assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState()
@@ -301,8 +300,7 @@
     @Test
     public void testShowTransientBars_statusBarCanBeTransient_appGetsStatusBarFakeControl() {
         addStatusBar().getControllableInsetProvider().getSource().setVisible(false);
-        addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar")
-                .getControllableInsetProvider().setServerVisible(true);
+        addNavigationBar().getControllableInsetProvider().setServerVisible(true);
 
         final InsetsPolicy policy = spy(mDisplayContent.getInsetsPolicy());
         doNothing().when(policy).startAnimation(anyBoolean(), any());
@@ -331,8 +329,8 @@
     public void testAbortTransientBars_bothCanBeAborted_appGetsBothRealControls() {
         final InsetsSource statusBarSource =
                 addStatusBar().getControllableInsetProvider().getSource();
-        final InsetsSource navBarSource = addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar")
-                .getControllableInsetProvider().getSource();
+        final InsetsSource navBarSource =
+                addNavigationBar().getControllableInsetProvider().getSource();
         statusBarSource.setVisible(false);
         navBarSource.setVisible(false);
         mAppWindow.mAboveInsetsState.addSource(navBarSource);
@@ -364,10 +362,8 @@
         assertTrue(state.getSource(ITYPE_STATUS_BAR).isVisible());
         assertTrue(state.getSource(ITYPE_NAVIGATION_BAR).isVisible());
 
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true);
-        requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, true);
-        mAppWindow.setRequestedVisibilities(requestedVisibilities);
+        mAppWindow.setRequestedVisibleTypes(
+                navigationBars() | statusBars(), navigationBars() | statusBars());
         policy.onInsetsModified(mAppWindow);
         waitUntilWindowAnimatorIdle();
 
@@ -383,8 +379,7 @@
     @Test
     public void testShowTransientBars_abortsWhenControlTargetChanges() {
         addStatusBar().getControllableInsetProvider().getSource().setVisible(false);
-        addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar")
-                .getControllableInsetProvider().getSource().setVisible(false);
+        addNavigationBar().getControllableInsetProvider().getSource().setVisible(false);
         final WindowState app = addWindow(TYPE_APPLICATION, "app");
         final WindowState app2 = addWindow(TYPE_APPLICATION, "app");
 
@@ -400,9 +395,15 @@
         assertFalse(policy.isTransient(ITYPE_NAVIGATION_BAR));
     }
 
-    private WindowState addNonFocusableWindow(int type, String name) {
-        WindowState win = addWindow(type, name);
+    private WindowState addNavigationBar() {
+        final WindowState win = createWindow(null, TYPE_NAVIGATION_BAR, "navBar");
         win.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
+        win.mAttrs.providedInsets = new InsetsFrameProvider[] {
+                new InsetsFrameProvider(ITYPE_NAVIGATION_BAR),
+                new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES),
+                new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT)
+        };
+        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
         return win;
     }
 
@@ -429,6 +430,10 @@
     }
 
     private InsetsSourceControl[] addWindowAndGetControlsForDispatch(WindowState win) {
+        mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs);
+        // Force update the focus in DisplayPolicy here. Otherwise, without server side focus
+        // update, the policy relying on windowing type will never get updated.
+        mDisplayContent.getDisplayPolicy().focusChangedLw(null, win);
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
         return mDisplayContent.getInsetsStateController().getControlsForDispatch(win);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index fe14d8e..c898119 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -24,6 +24,7 @@
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -48,7 +49,6 @@
 import android.util.SparseArray;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
@@ -187,10 +187,7 @@
         getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null);
         getController().onImeControlTargetChanged(
                 mDisplayContent.getImeInputTarget().getWindowState());
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_IME, true);
-        mDisplayContent.getImeInputTarget().getWindowState()
-                .setRequestedVisibilities(requestedVisibilities);
+        mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime());
         getController().onInsetsModified(mDisplayContent.getImeInputTarget().getWindowState());
 
         // Send our spy window (app) into the system so that we can detect the invocation.
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
new file mode 100644
index 0000000..1246d1e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.util.AtomicFile;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationPersisterTest {
+
+    private static final long TIMEOUT = 2000L; // 2 secs
+
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private Context mContext;
+    private PersisterQueue mPersisterQueue;
+    private QueueState mQueueState;
+    private PersisterQueue.Listener mQueueListener;
+    private File mConfigFolder;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = getInstrumentation().getTargetContext();
+        mConfigFolder = mContext.getFilesDir();
+        mPersisterQueue = new PersisterQueue();
+        mQueueState = new QueueState();
+        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext,
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability),
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability),
+                mConfigFolder, mPersisterQueue, mQueueState);
+        mQueueListener = queueEmpty -> mQueueState.onItemAdded();
+        mPersisterQueue.addListener(mQueueListener);
+        mLetterboxConfigurationPersister.start();
+    }
+
+    public void tearDown() throws InterruptedException {
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        mPersisterQueue.removeListener(mQueueListener);
+        stopPersisterSafe(mPersisterQueue);
+    }
+
+    @Test
+    public void test_whenStoreIsCreated_valuesAreDefaults() {
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreWritten() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
+        final PersisterQueue firstPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
+                mQueueState);
+        firstPersister.start();
+        firstPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        firstPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(firstPersisterQueue);
+        stopPersisterSafe(firstPersisterQueue);
+        final PersisterQueue secondPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
+                mQueueState);
+        secondPersister.start();
+        final int newPositionForHorizontalReachability =
+                secondPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                secondPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(secondPersister, secondPersisterQueue);
+        waitForCompletion(secondPersisterQueue);
+        stopPersisterSafe(secondPersisterQueue);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    private void stopPersisterSafe(PersisterQueue persisterQueue) {
+        try {
+            persisterQueue.stopPersisting();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void waitForCompletion(PersisterQueue persisterQueue) {
+        final long endTime = System.currentTimeMillis() + TIMEOUT;
+        // The queue could be empty but the last item still processing and not completed. For this
+        // reason the completion happens when there are not more items to process and the last one
+        // has completed.
+        while (System.currentTimeMillis() < endTime && (!isQueueEmpty(persisterQueue)
+                || !hasLastItemCompleted())) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ie) { /* Nope */}
+        }
+    }
+
+    private boolean isQueueEmpty(PersisterQueue persisterQueue) {
+        return persisterQueue.findLastItem(
+                writeQueueItem -> true, PersisterQueue.WriteQueueItem.class) != null;
+    }
+
+    private boolean hasLastItemCompleted() {
+        return mQueueState.isEmpty();
+    }
+
+    private void deleteConfiguration(LetterboxConfigurationPersister persister,
+            PersisterQueue persisterQueue) {
+        final AtomicFile fileToDelete = new AtomicFile(
+                new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME));
+        persisterQueue.addItem(
+                new DeleteFileCommand(fileToDelete, mQueueState.andThen(
+                        s -> persister.useDefaultValue())), true);
+    }
+
+    private static class DeleteFileCommand implements
+            PersisterQueue.WriteQueueItem<DeleteFileCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToDelete;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+        DeleteFileCommand(@NonNull AtomicFile fileToDelete, Consumer<String> onComplete) {
+            mFileToDelete = fileToDelete;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            mFileToDelete.delete();
+            if (mOnComplete != null) {
+                mOnComplete.accept("DeleteFileCommand");
+            }
+        }
+    }
+
+    // Contains the current length of the persister queue
+    private static class QueueState implements Consumer<String> {
+
+        // The current number of commands in the queue
+        @VisibleForTesting
+        private final AtomicInteger mCounter = new AtomicInteger(0);
+
+        @Override
+        public void accept(String s) {
+            mCounter.decrementAndGet();
+        }
+
+        void onItemAdded() {
+            mCounter.incrementAndGet();
+        }
+
+        boolean isEmpty() {
+            return mCounter.get() == 0;
+        }
+
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
new file mode 100644
index 0000000..c927f9e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationTest {
+
+    private LetterboxConfiguration mLetterboxConfiguration;
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = getInstrumentation().getTargetContext();
+        mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class);
+        mLetterboxConfiguration = new LetterboxConfiguration(context,
+                mLetterboxConfigurationPersister);
+    }
+
+    @Test
+    public void test_whenReadingValues_storeIsInvoked() {
+        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
+        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+    }
+
+    @Test
+    public void test_whenSettingValues_updateConfigurationIsInvoked() {
+        mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+                anyInt());
+        mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+                anyInt());
+    }
+
+    @Test
+    public void test_whenMovedHorizontally_updatePositionAccordingly() {
+        // Starting from center
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from left
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from right
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+    }
+
+    @Test
+    public void test_whenMovedVertically_updatePositionAccordingly() {
+        // Starting from center
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from top
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from bottom
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+    }
+
+    private void assertForHorizontalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+                expected);
+    }
+
+    private void assertForVerticalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForVerticalReachability(
+                expected);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 9d2eb26..bcaf886 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -16,22 +16,29 @@
 
 package com.android.server.wm;
 
+import static android.view.SurfaceControl.RefreshRateRange.FLOAT_TOLERANCE;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.hardware.display.DisplayManager;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display.Mode;
+import android.view.Surface;
 import android.view.WindowManager.LayoutParams;
 
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.wm.RefreshRatePolicy.FrameRateVote;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,7 +52,6 @@
 @RunWith(WindowTestRunner.class)
 @FlakyTest
 public class RefreshRatePolicyTest extends WindowTestsBase {
-    private static final float FLOAT_TOLERANCE = 0.01f;
     private static final int HI_MODE_ID = 1;
     private static final float HI_REFRESH_RATE = 90;
 
@@ -57,6 +63,19 @@
 
     private RefreshRatePolicy mPolicy;
     private HighRefreshRateDenylist mDenylist = mock(HighRefreshRateDenylist.class);
+    private FrameRateVote mTempFrameRateVote = new FrameRateVote();
+
+    private static final FrameRateVote FRAME_RATE_VOTE_NONE = new FrameRateVote();
+    private static final FrameRateVote FRAME_RATE_VOTE_DENY_LIST =
+            new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+    private static final FrameRateVote FRAME_RATE_VOTE_LOW_EXACT =
+            new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+    private static final FrameRateVote FRAME_RATE_VOTE_HI_EXACT =
+            new FrameRateVote(HI_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+    private static final FrameRateVote FRAME_RATE_VOTE_LOW_PREFERRED =
+            new FrameRateVote(LOW_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
+    private static final FrameRateVote FRAME_RATE_VOTE_HI_PREFERRED =
+            new FrameRateVote(HI_REFRESH_RATE, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
 
     // Parcel and Unparcel the LayoutParams in the window state to test the path the object
     // travels from the app's process to system server
@@ -89,6 +108,8 @@
     WindowState createWindow(String name) {
         WindowState window = createWindow(null, TYPE_BASE_APPLICATION, name);
         when(window.getDisplayInfo()).thenReturn(mDisplayInfo);
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
         return window;
     }
 
@@ -98,20 +119,23 @@
         cameraUsingWindow.mAttrs.packageName = "com.android.test";
         parcelLayoutParams(cameraUsingWindow);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE, LOW_REFRESH_RATE);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.removeRefreshRateRangeForPackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
     }
@@ -122,20 +146,23 @@
         cameraUsingWindow.mAttrs.packageName = "com.android.test";
         parcelLayoutParams(cameraUsingWindow);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE, MID_REFRESH_RATE);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(MID_REFRESH_RATE,
                 mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.removeRefreshRateRangeForPackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
     }
@@ -146,20 +173,23 @@
         cameraUsingWindow.mAttrs.packageName = "com.android.test";
         parcelLayoutParams(cameraUsingWindow);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE - 10, HI_REFRESH_RATE + 10);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(HI_REFRESH_RATE,
                 mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.removeRefreshRateRangeForPackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
     }
@@ -171,8 +201,8 @@
         parcelLayoutParams(denylistedWindow);
         when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
         assertEquals(0, mPolicy.getPreferredModeId(denylistedWindow));
-        assertEquals(LOW_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(denylistedWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(denylistedWindow));
+        assertEquals(FRAME_RATE_VOTE_DENY_LIST, denylistedWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(denylistedWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(denylistedWindow), FLOAT_TOLERANCE);
     }
@@ -185,8 +215,8 @@
         parcelLayoutParams(overrideWindow);
         when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
         assertEquals(HI_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(HI_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_HI_EXACT, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
@@ -199,8 +229,8 @@
         parcelLayoutParams(overrideWindow);
         when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(HI_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
@@ -214,8 +244,8 @@
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE, LOW_REFRESH_RATE);
         assertEquals(HI_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(HI_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_HI_EXACT, overrideWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(LOW_REFRESH_RATE,
@@ -231,8 +261,8 @@
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE, LOW_REFRESH_RATE);
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(HI_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, overrideWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(LOW_REFRESH_RATE,
@@ -246,8 +276,8 @@
         overrideWindow.mAttrs.preferredDisplayModeId = LOW_MODE_ID;
         parcelLayoutParams(overrideWindow);
         assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(LOW_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_LOW_EXACT, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
 
@@ -255,7 +285,8 @@
                 overrideWindow.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
@@ -267,8 +298,8 @@
         overrideWindow.mAttrs.preferredRefreshRate = LOW_REFRESH_RATE;
         parcelLayoutParams(overrideWindow);
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(LOW_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_LOW_PREFERRED, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
 
@@ -276,7 +307,8 @@
                 overrideWindow.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
@@ -288,8 +320,8 @@
         parcelLayoutParams(window);
         when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(LOW_REFRESH_RATE,
-                mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_DENY_LIST, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
 
@@ -297,7 +329,8 @@
                 window.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
@@ -311,7 +344,8 @@
         mPolicy.addRefreshRateRangeForPackage("com.android.test",
                 LOW_REFRESH_RATE, LOW_REFRESH_RATE);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE,
                 mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(LOW_REFRESH_RATE,
@@ -321,7 +355,8 @@
                 cameraUsingWindow.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(cameraUsingWindow));
+        assertEquals(FRAME_RATE_VOTE_NONE, cameraUsingWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
     }
@@ -332,7 +367,8 @@
         window.mAttrs.preferredMaxDisplayRefreshRate = LOW_REFRESH_RATE;
         parcelLayoutParams(window);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
 
@@ -340,7 +376,8 @@
                 window.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
@@ -351,7 +388,8 @@
         window.mAttrs.preferredMinDisplayRefreshRate = LOW_REFRESH_RATE;
         parcelLayoutParams(window);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
         assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
 
@@ -359,7 +397,8 @@
                 window.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
@@ -370,8 +409,92 @@
         window.mAttrs.preferredRefreshRate = LOW_REFRESH_RATE;
         parcelLayoutParams(window);
         assertEquals(0, mPolicy.getPreferredModeId(window));
-        assertEquals(LOW_REFRESH_RATE, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_LOW_PREFERRED, window.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
+
+    @Test
+    public void testSwitchingTypeForExactVote() {
+        final WindowState window = createWindow("window");
+        window.mAttrs.preferredDisplayModeId = HI_MODE_ID;
+        parcelLayoutParams(window);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_NONE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_HI_EXACT, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_HI_EXACT, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
+    }
+
+    @Test
+    public void testSwitchingTypeForPreferredVote() {
+        final WindowState window = createWindow("window");
+        window.mAttrs.preferredRefreshRate = HI_REFRESH_RATE;
+        parcelLayoutParams(window);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_NONE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_HI_PREFERRED, window.mFrameRateVote);
+    }
+
+    @Test
+    public void testSwitchingTypeForDenylist() {
+        when(mDenylist.isDenylisted("com.android.test")).thenReturn(true);
+
+        final WindowState window = createWindow("window");
+        window.mAttrs.packageName = "com.android.test";
+        parcelLayoutParams(window);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_NONE);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_LOW_EXACT, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS);
+        assertFalse(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_LOW_EXACT, window.mFrameRateVote);
+
+        when(window.mWmService.mDisplayManagerInternal.getRefreshRateSwitchingType())
+                .thenReturn(DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
+        assertTrue(mPolicy.updateFrameRateVote(window));
+        assertEquals(FRAME_RATE_VOTE_NONE, window.mFrameRateVote);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
index b1acae2..eab2e15 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RunningTasksTest.java
@@ -42,7 +42,6 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
-import java.util.List;
 
 /**
  * Build/Install/Run:
@@ -66,55 +65,6 @@
     }
 
     @Test
-    public void testCollectTasksByLastActiveTime() {
-        // Create a number of stacks with tasks (of incrementing active time)
-        final ArrayList<DisplayContent> displays = new ArrayList<>();
-        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2500).build();
-        displays.add(display);
-
-        final int numStacks = 2;
-        for (int stackIndex = 0; stackIndex < numStacks; stackIndex++) {
-            final Task stack = new TaskBuilder(mSupervisor)
-                    .setDisplay(display)
-                    .setOnTop(false)
-                    .build();
-        }
-
-        final int numTasks = 10;
-        int activeTime = 0;
-        final List<Task> rootTasks = new ArrayList<>();
-        display.getDefaultTaskDisplayArea().forAllRootTasks(task -> {
-            rootTasks.add(task);
-        }, false /* traverseTopToBottom */);
-        for (int i = 0; i < numTasks; i++) {
-            final Task task =
-                    createTask(rootTasks.get(i % numStacks), ".Task" + i, i, activeTime++, null);
-            doReturn(false).when(task).isVisible();
-        }
-
-        // Ensure that the latest tasks were returned in order of decreasing last active time,
-        // collected from all tasks across all the stacks
-        final int numFetchTasks = 5;
-        ArrayList<RunningTaskInfo> tasks = new ArrayList<>();
-        mRunningTasks.getTasks(5, tasks, FLAG_ALLOWED | FLAG_CROSS_USERS,
-                mAtm.getRecentTasks(), mRootWindowContainer, -1 /* callingUid */, PROFILE_IDS);
-        assertThat(tasks).hasSize(numFetchTasks);
-        for (int i = 0; i < numFetchTasks; i++) {
-            assertEquals(numTasks - i - 1, tasks.get(i).id);
-        }
-
-        // Ensure that requesting more than the total number of tasks only returns the subset
-        // and does not crash
-        tasks.clear();
-        mRunningTasks.getTasks(100, tasks, FLAG_ALLOWED | FLAG_CROSS_USERS,
-                mAtm.getRecentTasks(), mRootWindowContainer, -1 /* callingUid */, PROFILE_IDS);
-        assertThat(tasks).hasSize(numTasks);
-        for (int i = 0; i < numTasks; i++) {
-            assertEquals(numTasks - i - 1, tasks.get(i).id);
-        }
-    }
-
-    @Test
     public void testTaskInfo_expectNoExtrasByDefault() {
         final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2500).build();
         final int numTasks = 10;
@@ -125,7 +75,7 @@
                     .build();
             final Bundle data = new Bundle();
             data.putInt("key", 100);
-            createTask(stack, ".Task" + i, i, i, data);
+            createTask(stack, ".Task" + i, i, data);
         }
 
         final int numFetchTasks = 5;
@@ -150,7 +100,7 @@
                     .build();
             final Bundle data = new Bundle();
             data.putInt("key", 100);
-            createTask(stack, ".Task" + i, i, i, data);
+            createTask(stack, ".Task" + i, i, data);
         }
 
         final int numFetchTasks = 5;
@@ -167,46 +117,63 @@
     }
 
     @Test
-    public void testUpdateLastActiveTimeOfVisibleTasks() {
+    public void testGetTasksSortByFocusAndVisibility() {
         final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2500).build();
+        final Task stack = new TaskBuilder(mSupervisor)
+                .setDisplay(display)
+                .setOnTop(true)
+                .build();
+
         final int numTasks = 10;
         final ArrayList<Task> tasks = new ArrayList<>();
         for (int i = 0; i < numTasks; i++) {
-            final Task task = createTask(null, ".Task" + i, i, i, null);
+            final Task task = createTask(stack, ".Task" + i, i, null);
             doReturn(false).when(task).isVisible();
             tasks.add(task);
         }
 
-        final Task visibleTask = tasks.get(0);
-        doReturn(true).when(visibleTask).isVisible();
-
-        final Task focusedTask = tasks.get(1);
+        final Task focusedTask = tasks.get(numTasks - 1);
         doReturn(true).when(focusedTask).isVisible();
-        doReturn(true).when(focusedTask).isFocused();
+        display.mFocusedApp = focusedTask.getTopNonFinishingActivity();
 
-        // Ensure that the last active time of visible tasks were updated while the focused one had
-        // the largest last active time.
+        final Task visibleTaskTop = tasks.get(numTasks - 2);
+        doReturn(true).when(visibleTaskTop).isVisible();
+
+        final Task visibleTaskBottom = tasks.get(numTasks - 3);
+        doReturn(true).when(visibleTaskBottom).isVisible();
+
+        // Ensure that the focused Task is on top, visible tasks below, then invisible tasks.
         final int numFetchTasks = 5;
         final ArrayList<RunningTaskInfo> fetchTasks = new ArrayList<>();
         mRunningTasks.getTasks(numFetchTasks, fetchTasks,
                 FLAG_ALLOWED | FLAG_CROSS_USERS | FLAG_KEEP_INTENT_EXTRA,
                 mAtm.getRecentTasks(), mRootWindowContainer, -1 /* callingUid */, PROFILE_IDS);
         assertThat(fetchTasks).hasSize(numFetchTasks);
-        assertEquals(fetchTasks.get(0).id, focusedTask.mTaskId);
-        assertEquals(fetchTasks.get(1).id, visibleTask.mTaskId);
+        for (int i = 0; i < numFetchTasks; i++) {
+            assertEquals(numTasks - i - 1, fetchTasks.get(i).id);
+        }
+
+        // Ensure that requesting more than the total number of tasks only returns the subset
+        // and does not crash
+        fetchTasks.clear();
+        mRunningTasks.getTasks(100, fetchTasks,
+                FLAG_ALLOWED | FLAG_CROSS_USERS | FLAG_KEEP_INTENT_EXTRA,
+                mAtm.getRecentTasks(), mRootWindowContainer, -1 /* callingUid */, PROFILE_IDS);
+        assertThat(fetchTasks).hasSize(numTasks);
+        for (int i = 0; i < numTasks; i++) {
+            assertEquals(numTasks - i - 1, fetchTasks.get(i).id);
+        }
     }
 
     /**
-     * Create a task with a single activity in it, with the given last active time.
+     * Create a task with a single activity in it.
      */
-    private Task createTask(Task stack, String className, int taskId,
-            int lastActiveTime, Bundle extras) {
+    private Task createTask(Task stack, String className, int taskId, Bundle extras) {
         final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
                 .setComponent(new ComponentName(mContext.getPackageName(), className))
                 .setTaskId(taskId)
                 .setParentTaskFragment(stack)
                 .build();
-        task.lastActiveTime = lastActiveTime;
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setTask(task)
                 .setComponent(new ComponentName(mContext.getPackageName(), ".TaskActivity"))
@@ -227,7 +194,7 @@
                     .setDisplay(i % 2 == 0 ? display0 : display1)
                     .setOnTop(true)
                     .build();
-            final Task task = createTask(stack, ".Task" + i, i, i, null);
+            final Task task = createTask(stack, ".Task" + i, i, null);
             tasks.add(task);
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index d59fce0..e65610f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -25,6 +25,7 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
+import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES;
 import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT;
@@ -71,6 +72,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.times;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -87,7 +89,7 @@
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
 import android.view.InsetsFrameProvider;
-import android.view.InsetsVisibilities;
+import android.view.InsetsSource;
 import android.view.WindowManager;
 
 import androidx.test.filters.MediumTest;
@@ -106,6 +108,9 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.List;
 
 /**
  * Tests for Size Compatibility mode.
@@ -2302,8 +2307,7 @@
         // We should get a null LetterboxDetails object as there is no letterboxed activity, so
         // nothing will get passed to SysUI
         verify(statusBar, never()).onSystemBarAttributesChanged(anyInt(), anyInt(),
-                any(), anyBoolean(), anyInt(),
-                any(InsetsVisibilities.class), isNull(), isNull());
+                any(), anyBoolean(), anyInt(), anyInt(), isNull(), isNull());
 
     }
 
@@ -2331,8 +2335,7 @@
         // Check that letterboxDetails actually gets passed to SysUI
         StatusBarManagerInternal statusBar = displayPolicy.getStatusBarManagerInternal();
         verify(statusBar).onSystemBarAttributesChanged(anyInt(), anyInt(),
-                any(), anyBoolean(), anyInt(),
-                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+                any(), anyBoolean(), anyInt(), anyInt(), isNull(), eq(expectedLetterboxDetails));
     }
 
     @Test
@@ -2367,8 +2370,49 @@
         // Check that letterboxDetails actually gets passed to SysUI
         StatusBarManagerInternal statusBarManager = displayPolicy.getStatusBarManagerInternal();
         verify(statusBarManager).onSystemBarAttributesChanged(anyInt(), anyInt(),
-                any(), anyBoolean(), anyInt(),
-                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+                any(), anyBoolean(), anyInt(), anyInt(), isNull(), eq(expectedLetterboxDetails));
+    }
+
+    @Test
+    public void testLetterboxDetailsForTaskBar_letterboxNotOverlappingTaskBar() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        final int screenHeight = 2200;
+        final int screenWidth = 1400;
+        final int taskbarHeight = 200;
+        setUpDisplaySizeWithApp(screenWidth, screenHeight);
+
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Move first activity to split screen which takes half of the screen.
+        organizer.mPrimary.setBounds(0, screenHeight / 2, screenWidth, screenHeight);
+        organizer.putTaskToPrimary(mTask, true);
+
+        final InsetsSource navSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR);
+        navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
+
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
+
+        final WindowState w1 = addWindowToActivity(mActivity);
+        w1.mAboveInsetsState.addSource(navSource);
+
+        // Prepare unresizable activity with max aspect ratio
+        prepareUnresizable(mActivity, /* maxAspect */ 1.1f, SCREEN_ORIENTATION_UNSPECIFIED);
+
+        // Refresh the letterboxes
+        mActivity.mRootWindowContainer.performSurfacePlacement();
+
+        final ArgumentCaptor<Rect> cropCapturer = ArgumentCaptor.forClass(Rect.class);
+        verify(mTransaction, times(2)).setWindowCrop(
+                eq(w1.getSurfaceControl()),
+                cropCapturer.capture()
+        );
+        final List<Rect> capturedCrops = cropCapturer.getAllValues();
+
+        final int expectedHeight = screenHeight / 2 - taskbarHeight;
+        assertEquals(2, capturedCrops.size());
+        assertEquals(expectedHeight, capturedCrops.get(0).bottom);
+        assertEquals(expectedHeight, capturedCrops.get(1).bottom);
     }
 
     @Test
@@ -2420,8 +2464,7 @@
         // Check that letterboxDetails actually gets passed to SysUI
         StatusBarManagerInternal statusBar = displayPolicy.getStatusBarManagerInternal();
         verify(statusBar).onSystemBarAttributesChanged(anyInt(), anyInt(),
-                any(), anyBoolean(), anyInt(),
-                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+                any(), anyBoolean(), anyInt(), anyInt(), isNull(), eq(expectedLetterboxDetails));
     }
 
     private void recomputeNaturalConfigurationOfUnresizableActivity() {
@@ -3165,7 +3208,7 @@
     /** Asserts that the size of activity is larger than its parent so it is scaling. */
     private void assertScaled() {
         assertTrue(mActivity.inSizeCompatMode());
-        assertNotEquals(1f, mActivity.getSizeCompatScale(), 0.0001f /* delta */);
+        assertNotEquals(1f, mActivity.getCompatScale(), 0.0001f /* delta */);
     }
 
     /** Asserts that the activity is best fitted in the parent. */
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
index 846a506..5e1fae0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java
@@ -192,16 +192,17 @@
     }
 
     private static class SyncTarget implements SurfaceSyncGroup.SyncTarget {
-        private SurfaceSyncGroup.SyncBufferCallback mSyncBufferCallback;
+        private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback;
 
         @Override
-        public void onReadyToSync(SurfaceSyncGroup.SyncBufferCallback syncBufferCallback) {
-            mSyncBufferCallback = syncBufferCallback;
+        public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup,
+                SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
+            mTransactionReadyCallback = transactionReadyCallback;
         }
 
         void onBufferReady() {
             SurfaceControl.Transaction t = new StubTransaction();
-            mSyncBufferCallback.onBufferReady(t);
+            mTransactionReadyCallback.onTransactionReady(t);
         }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 0b23359..4202f46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -56,6 +56,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -91,6 +92,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
 import java.util.List;
 
@@ -762,6 +764,50 @@
     }
 
     @Test
+    public void testOrganizerRemovedWithPendingEvents() {
+        final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        assertTrue(tf0.isOrganizedTaskFragment());
+        assertTrue(tf1.isOrganizedTaskFragment());
+        assertTrue(tf0.isAttached());
+        assertTrue(tf0.isAttached());
+
+        // Mock the behavior that remove TaskFragment can trigger event dispatch.
+        final Answer<Void> removeImmediately = invocation -> {
+            invocation.callRealMethod();
+            mController.dispatchPendingEvents();
+            return null;
+        };
+        doAnswer(removeImmediately).when(tf0).removeImmediately();
+        doAnswer(removeImmediately).when(tf1).removeImmediately();
+
+        // Add pending events.
+        mController.onTaskFragmentAppeared(mIOrganizer, tf0);
+        mController.onTaskFragmentAppeared(mIOrganizer, tf1);
+
+        // Remove organizer.
+        mController.unregisterOrganizer(mIOrganizer);
+        mController.dispatchPendingEvents();
+
+        // Nothing should happen after the organizer is removed.
+        verify(mOrganizer, never()).onTransactionReady(any());
+
+        // TaskFragments should be removed.
+        assertFalse(tf0.isOrganizedTaskFragment());
+        assertFalse(tf1.isOrganizedTaskFragment());
+        assertFalse(tf0.isAttached());
+        assertFalse(tf0.isAttached());
+    }
+
+    @Test
     public void testTaskFragmentInPip_startActivityInTaskFragment() {
         setupTaskFragmentInPip();
         final ActivityRecord activity = mTaskFragment.getTopMostActivity();
@@ -874,29 +920,87 @@
 
     @Test
     public void testDeferPendingTaskFragmentEventsOfInvisibleTask() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
                 .setOrganizer(mOrganizer)
                 .setFragmentToken(mFragmentToken)
                 .build();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify that events were not sent when the Task is in background.
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
+        mController.onTaskFragmentParentInfoChanged(mIOrganizer, task);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
+
+        // Verify that the events were sent when the Task becomes visible.
+        doReturn(true).when(task).shouldBeVisible(any());
+        task.lastActiveTime++;
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+    }
+
+    @Test
+    public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() {
+        // Invisible Task.
+        final Task invisibleTask = createTask(mDisplayContent);
+        final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(invisibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        doReturn(false).when(invisibleTask).shouldBeVisible(any());
+
+        // Visible Task.
+        final IBinder fragmentToken = new Binder();
+        final Task visibleTask = createTask(mDisplayContent);
+        final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(visibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(fragmentToken)
+                .build();
+        doReturn(true).when(invisibleTask).shouldBeVisible(any());
+
+        // Sending events
+        invisibleTaskFragment.mTaskFragmentAppearedSent = true;
+        visibleTaskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment);
+        mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment);
+        mController.dispatchPendingEvents();
+
+        // Verify that both events are sent.
+        verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
+        final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
+        final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+
+        // There should be two Task info changed with two TaskFragment info changed.
+        assertEquals(4, changes.size());
+        // Invisible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType());
+        assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId());
+        // Invisible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType());
+        assertEquals(invisibleTaskFragment.getFragmentToken(),
+                changes.get(1).getTaskFragmentToken());
+        // Visible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType());
+        assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId());
+        // Visible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType());
+        assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken());
     }
 
     @Test
     public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
@@ -905,24 +1009,26 @@
                 .createActivityCount(1)
                 .build();
         final ActivityRecord activity = taskFragment.getTopMostActivity();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(null, "test");
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify the info changed event is not sent because the Task is invisible
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Mock the task becomes visible, and activity resumed
+        // Mock the task becomes visible, and activity resumed. Verify the info changed event is
+        // sent.
         doReturn(true).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(activity, "test");
-
-        // Verifies that event is sent.
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
@@ -977,25 +1083,24 @@
         final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
         // Add another activity in the Task so that it always contains a non-finishing activity.
         createActivityRecord(task);
-        assertTrue(task.shouldBeVisible(null));
+        doReturn(false).when(task).shouldBeVisible(any());
 
-        // Dispatch pending info changed event from creating the activity
-        taskFragment.mTaskFragmentAppearedSent = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
 
-        // Verify the info changed callback is not called when the task is invisible
+        // Verify the info changed event is not sent because the Task is invisible
         clearInvocations(mOrganizer);
-        doReturn(false).when(task).shouldBeVisible(any());
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Finish the embedded activity, and verify the info changed callback is called because the
+        // Finish the embedded activity, and verify the info changed event is sent because the
         // TaskFragment is becoming empty.
         embeddedActivity.finishing = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 83f1789..3ff2c0e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -118,10 +118,13 @@
         doReturn(true).when(mTaskFragment).isVisibleRequested();
 
         clearInvocations(mTransaction);
+        mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate();
         mTaskFragment.setBounds(endBounds);
+        assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds));
+        mTaskFragment.initializeChangeTransition(startBounds);
+        mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
 
         // Surface reset when prepare transition.
-        verify(mTaskFragment).initializeChangeTransition(startBounds);
         verify(mTransaction).setPosition(mLeash, 0, 0);
         verify(mTransaction).setWindowCrop(mLeash, 0, 0);
 
@@ -166,7 +169,7 @@
 
         mTaskFragment.setBounds(endBounds);
 
-        verify(mTaskFragment, never()).initializeChangeTransition(any());
+        assertFalse(mTaskFragment.shouldStartChangeTransition(startBounds));
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java
index 7abe369..d535677 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskPositionerTests.java
@@ -88,7 +88,7 @@
 
     @After
     public void tearDown() {
-        mPositioner = null;
+        TaskPositioner.setFactory(null);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 92c9e80..d52c34b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -36,6 +36,7 @@
 import static android.view.Surface.ROTATION_90;
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
@@ -77,14 +78,16 @@
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.DisplayMetrics;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.window.TaskFragmentOrganizer;
 
 import androidx.test.filters.MediumTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -432,6 +435,24 @@
     }
 
     @Test
+    public void testPropagateFocusedStateToRootTask() {
+        final Task rootTask = createTask(mDefaultDisplay);
+        final Task leafTask = createTaskInRootTask(rootTask, 0 /* userId */);
+
+        final ActivityRecord activity = createActivityRecord(leafTask);
+
+        leafTask.getDisplayContent().setFocusedApp(activity);
+
+        assertTrue(leafTask.getTaskInfo().isFocused);
+        assertTrue(rootTask.getTaskInfo().isFocused);
+
+        leafTask.getDisplayContent().setFocusedApp(null);
+
+        assertFalse(leafTask.getTaskInfo().isFocused);
+        assertFalse(rootTask.getTaskInfo().isFocused);
+    }
+
+    @Test
     public void testReturnsToHomeRootTask() throws Exception {
         final Task task = createTask(1);
         spyOn(task);
@@ -1470,6 +1491,26 @@
                 tf0, parentTask.getTaskFragment(TaskFragment::isOrganizedTaskFragment));
     }
 
+    @Test
+    public void testReorderActivityToFront() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task =  new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+        doNothing().when(task).onActivityVisibleRequestedChanged();
+        final ActivityRecord activity = task.getTopMostActivity();
+
+        final TaskFragment fragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord embeddedActivity = fragment.getTopMostActivity();
+        task.moveActivityToFront(activity);
+        assertEquals("Activity must be moved to front", activity, task.getTopMostActivity());
+
+        doNothing().when(fragment).sendTaskFragmentInfoChanged();
+        task.moveActivityToFront(embeddedActivity);
+        assertEquals("Activity must be moved to front", embeddedActivity,
+                task.getTopMostActivity());
+        assertEquals("Activity must not be embedded", embeddedActivity,
+                task.getTopChild());
+    }
+
     private Task getTestTask() {
         final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
         return task.getBottomMostTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index bb5aceb..6e72bf3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import android.annotation.Nullable;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -26,6 +27,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.ScrollCaptureResponse;
+import android.view.inputmethod.ImeTracker;
 import android.window.ClientWindowFrames;
 
 import com.android.internal.os.IResultReceiver;
@@ -117,10 +119,12 @@
     }
 
     @Override
-    public void showInsets(int types, boolean fromIme) throws RemoteException {
+    public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+            throws RemoteException {
     }
 
     @Override
-    public void hideInsets(int types, boolean fromIme) throws RemoteException {
+    public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
+            throws RemoteException {
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 29a514c..35b9710 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -60,7 +60,9 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.app.ActivityManager;
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
@@ -80,6 +82,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.graphics.ColorUtils;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -1387,6 +1391,50 @@
     }
 
     @Test
+    public void testChangeSetBackgroundColor() {
+        final Transition transition = createTestTransition(TRANSIT_CHANGE);
+        final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        // Test background color for Activity and embedded TaskFragment.
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        final ActivityManager.TaskDescription taskDescription =
+                new ActivityManager.TaskDescription.Builder()
+                        .setBackgroundColor(Color.YELLOW)
+                        .build();
+        task.setTaskDescription(taskDescription);
+
+        // Start states:
+        embeddedActivity.mVisibleRequested = true;
+        nonEmbeddedActivity.mVisibleRequested = false;
+        changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf));
+        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity));
+        // End states:
+        embeddedActivity.mVisibleRequested = false;
+        nonEmbeddedActivity.mVisibleRequested = true;
+
+        participants.add(embeddedTf);
+        participants.add(nonEmbeddedActivity);
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                participants, changes);
+        final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType,
+                0 /* flags */, targets, changes, mMockT);
+
+        // Background color should be set on both Activity and embedded TaskFragment.
+        final int expectedBackgroundColor = ColorUtils.setAlphaComponent(
+                taskDescription.getBackgroundColor(), 255);
+        assertEquals(2, info.getChanges().size());
+        assertEquals(expectedBackgroundColor, info.getChanges().get(0).getBackgroundColor());
+        assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor());
+    }
+
+    @Test
     public void testTransitionVisibleChange() {
         registerTestTransitionPlayer();
         final ActivityRecord app = createActivityRecord(mDisplayContent);
@@ -1472,6 +1520,29 @@
         transition.abort();
     }
 
+    @Test
+    public void testCollectReparentChange() {
+        registerTestTransitionPlayer();
+
+        // Reparent activity in transition.
+        final Task lastParent = createTask(mDisplayContent);
+        final Task newParent = createTask(mDisplayContent);
+        final ActivityRecord activity = createActivityRecord(lastParent);
+        doReturn(true).when(lastParent).shouldRemoveSelfOnLastChildRemoval();
+        doNothing().when(activity).setDropInputMode(anyInt());
+        activity.mVisibleRequested = true;
+
+        final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */,
+                activity.mTransitionController, mWm.mSyncEngine);
+        activity.mTransitionController.moveToCollecting(transition);
+        transition.collect(activity);
+        activity.reparent(newParent, POSITION_TOP);
+
+        // ChangeInfo#mCommonAncestor should be set after reparent.
+        final Transition.ChangeInfo change = transition.mChanges.get(activity);
+        assertEquals(newParent.getDisplayArea(), change.mCommonAncestor);
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
index e824f3d..383722a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java
@@ -18,6 +18,7 @@
 
 import static android.view.InsetsState.ITYPE_IME;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 
@@ -31,7 +32,6 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSource;
-import android.view.InsetsVisibilities;
 
 import androidx.test.filters.SmallTest;
 
@@ -207,9 +207,7 @@
         statusBar.getFrame().set(0, 0, 500, 100);
         mProvider.setWindowContainer(statusBar, null, null);
         mProvider.updateControlForTarget(target, false /* force */);
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        target.setRequestedVisibilities(requestedVisibilities);
+        target.setRequestedVisibleTypes(0, statusBars());
         mProvider.updateClientVisibility(target);
         assertFalse(mSource.isVisible());
     }
@@ -220,9 +218,7 @@
         final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
         statusBar.getFrame().set(0, 0, 500, 100);
         mProvider.setWindowContainer(statusBar, null, null);
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        target.setRequestedVisibilities(requestedVisibilities);
+        target.setRequestedVisibleTypes(0, statusBars());
         mProvider.updateClientVisibility(target);
         assertTrue(mSource.isVisible());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
index 739e783..56c59cc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
@@ -41,7 +41,6 @@
 import android.view.DisplayCutout;
 import android.view.Gravity;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.WindowInsets;
 import android.view.WindowLayout;
 import android.view.WindowManager;
@@ -81,7 +80,7 @@
     private int mWindowingMode;
     private int mRequestedWidth;
     private int mRequestedHeight;
-    private InsetsVisibilities mRequestedVisibilities;
+    private int mRequestedVisibleTypes;
     private float mCompatScale;
 
     @Before
@@ -98,14 +97,14 @@
         mWindowingMode = WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
         mRequestedWidth = DISPLAY_WIDTH;
         mRequestedHeight = DISPLAY_HEIGHT;
-        mRequestedVisibilities = new InsetsVisibilities();
+        mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
         mCompatScale = 1f;
         mFrames.attachedFrame = null;
     }
 
     private void computeFrames() {
         mWindowLayout.computeFrames(mAttrs, mState, mDisplayCutoutSafe, mWindowBounds,
-                mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibilities,
+                mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibleTypes,
                 mCompatScale, mFrames);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index cf24ff2..4429aef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -71,10 +71,10 @@
 import android.view.IWindowSessionCallback;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.window.ClientWindowFrames;
 import android.window.ScreenCapture;
@@ -338,7 +338,7 @@
                 .getWindowType(eq(windowContextToken));
 
         mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY,
-                UserHandle.USER_SYSTEM, new InsetsVisibilities(), null, new InsetsState(),
+                UserHandle.USER_SYSTEM, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
                 new InsetsSourceControl[0], new Rect(), new float[1]);
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(),
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 3b64c51..690c2aa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1176,7 +1176,7 @@
         assertTrue(rootTask2.isOrganized());
 
         // Verify a back pressed does not call the organizer
-        mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token,
+        mWm.mAtmService.mActivityClientController.onBackPressed(activity.token,
                 new IRequestFinishCallback.Default());
         // Ensure events dispatch to organizer.
         mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
@@ -1187,7 +1187,7 @@
                 rootTask.mRemoteToken.toWindowContainerToken(), true);
 
         // Verify now that the back press does call the organizer
-        mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token,
+        mWm.mAtmService.mActivityClientController.onBackPressed(activity.token,
                 new IRequestFinishCallback.Default());
         // Ensure events dispatch to organizer.
         mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
@@ -1198,7 +1198,7 @@
                 rootTask.mRemoteToken.toWindowContainerToken(), false);
 
         // Verify now that the back press no longer calls the organizer
-        mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(activity.token,
+        mWm.mAtmService.mActivityClientController.onBackPressed(activity.token,
                 new IRequestFinishCallback.Default());
         // Ensure events dispatch to organizer.
         mWm.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
@@ -1404,7 +1404,7 @@
         mWm.mWindowPlacerLocked.deferLayout();
 
         rootTask.removeImmediately();
-        mWm.mAtmService.mActivityClientController.onBackPressedOnTaskRoot(record.token,
+        mWm.mAtmService.mActivityClientController.onBackPressed(record.token,
                 new IRequestFinishCallback.Default());
         waitUntilHandlersIdle();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 1636667..1b888f6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -26,6 +26,7 @@
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
@@ -92,9 +93,10 @@
 import android.view.InputWindowHandle;
 import android.view.InsetsSource;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
+import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentOrganizer;
 
 import androidx.test.filters.SmallTest;
 
@@ -430,9 +432,7 @@
                         null /* imeFrameProvider */);
         mDisplayContent.getInsetsStateController().onBarControlTargetChanged(
                 app, null /* fakeTopControlling */, app, null /* fakeNavControlling */);
-        final InsetsVisibilities requestedVisibilities = new InsetsVisibilities();
-        requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false);
-        app.setRequestedVisibilities(requestedVisibilities);
+        app.setRequestedVisibleTypes(0, statusBars());
         mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR)
                 .updateClientVisibility(app);
         waitUntilHandlersIdle();
@@ -802,6 +802,39 @@
     }
 
     @Test
+    public void testEmbeddedActivityResizing_clearAllDrawn() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
+        final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
+                "App window");
+        doReturn(true).when(embeddedActivity).isVisible();
+        embeddedActivity.mVisibleRequested = true;
+        makeWindowVisible(win);
+        win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
+        // Set the bounds twice:
+        // 1. To make sure there is no orientation change after #reportResized, which can also cause
+        // #clearAllDrawn.
+        // 2. Make #isLastConfigReportedToClient to be false after #reportResized, so it can process
+        // to check if we need redraw.
+        embeddedTf.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        embeddedTf.setBounds(0, 0, 1000, 2000);
+        win.reportResized();
+        embeddedTf.setBounds(500, 0, 1000, 2000);
+
+        // Clear all drawn when the embedded TaskFragment is in mDisplayContent.mChangingContainers.
+        win.updateResizingWindowIfNeeded();
+        verify(embeddedActivity, never()).clearAllDrawn();
+
+        mDisplayContent.mChangingContainers.add(embeddedTf);
+        win.updateResizingWindowIfNeeded();
+        verify(embeddedActivity).clearAllDrawn();
+    }
+
+    @Test
     public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
         final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
         win0.mActivityRecord.mVisibleRequested = false;
@@ -1002,7 +1035,7 @@
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
         assertTrue(mDisplayContent.shouldImeAttachedToApp());
-        controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+        controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
         controller.getImeSourceProvider().getSource().setVisible(true);
         controller.updateAboveInsetsState(false);
 
@@ -1039,7 +1072,7 @@
         mDisplayContent.setImeLayeringTarget(app);
         mDisplayContent.setImeInputTarget(app);
         assertTrue(mDisplayContent.shouldImeAttachedToApp());
-        controller.getImeSourceProvider().scheduleShowImePostLayout(app);
+        controller.getImeSourceProvider().scheduleShowImePostLayout(app, null /* statsToken */);
         controller.getImeSourceProvider().getSource().setVisible(true);
         controller.updateAboveInsetsState(false);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 40326e9..ab042d1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -26,6 +26,8 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.os.Process.SYSTEM_UID;
+import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES;
+import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES;
@@ -92,13 +94,13 @@
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.InsetsVisibilities;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
+import android.view.inputmethod.ImeTracker;
 import android.window.ITransitionPlayer;
 import android.window.ScreenCapture;
 import android.window.StartingWindowInfo;
@@ -347,6 +349,11 @@
                     LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
             mNavBarWindow.mAttrs.privateFlags |=
                     WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
+            mNavBarWindow.mAttrs.providedInsets = new InsetsFrameProvider[] {
+                    new InsetsFrameProvider(ITYPE_NAVIGATION_BAR),
+                    new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES),
+                    new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT)
+            };
             for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
                 mNavBarWindow.mAttrs.paramsForRotation[rot] =
                         getNavBarLayoutParamsForRotation(rot);
@@ -400,6 +407,11 @@
         lp.privateFlags |=
                 WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        lp.providedInsets = new InsetsFrameProvider[] {
+                new InsetsFrameProvider(ITYPE_NAVIGATION_BAR),
+                new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES),
+                new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT)
+        };
         return lp;
     }
 
@@ -837,16 +849,18 @@
             }
 
             @Override
-            public void showInsets(int i, boolean b) throws RemoteException {
+            public void showInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+                    throws RemoteException {
             }
 
             @Override
-            public void hideInsets(int i, boolean b) throws RemoteException {
+            public void hideInsets(int i, boolean b, @Nullable ImeTracker.Token t)
+                    throws RemoteException {
             }
 
             @Override
             public void topFocusedWindowChanged(ComponentName component,
-                    InsetsVisibilities requestedVisibilities) {
+                    int requestedVisibleTypes) {
             }
         };
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
index fc3962b..cd4d65d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
@@ -26,8 +26,10 @@
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
-import android.view.Surface;
 import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+
+import com.android.internal.policy.TransitionAnimation;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,7 +54,8 @@
     public void blackLuma() {
         Bitmap swBitmap = createBitmap(0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
+
         assertEquals(0, borderLuma, 0);
     }
 
@@ -60,7 +63,7 @@
     public void whiteLuma() {
         Bitmap swBitmap = createBitmap(1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -68,7 +71,7 @@
     public void unevenBitmapDimens() {
         Bitmap swBitmap = createBitmap(1, BITMAP_WIDTH + 1, BITMAP_HEIGHT + 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -77,7 +80,7 @@
         Bitmap swBitmap = createBitmap(1);
         setBorderLuma(swBitmap, 0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(0, borderLuma, 0);
     }
 
@@ -86,7 +89,7 @@
         Bitmap swBitmap = createBitmap(0);
         setBorderLuma(swBitmap, 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 7f5beb1..4fd2b78 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2392,6 +2392,8 @@
         @Override
         public void setAppStandbyBucket(String packageName, int bucket, int userId) {
 
+            super.setAppStandbyBucket_enforcePermission();
+
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final long token = Binder.clearCallingIdentity();
@@ -2442,6 +2444,8 @@
         @Override
         public void setAppStandbyBuckets(ParceledListSlice appBuckets, int userId) {
 
+            super.setAppStandbyBuckets_enforcePermission();
+
             final int callingUid = Binder.getCallingUid();
             final int callingPid = Binder.getCallingPid();
             final long token = Binder.clearCallingIdentity();
@@ -2493,6 +2497,8 @@
         public void setEstimatedLaunchTime(String packageName, long estimatedLaunchTime,
                 int userId) {
 
+            super.setEstimatedLaunchTime_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 UsageStatsService.this
@@ -2506,6 +2512,8 @@
         @Override
         public void setEstimatedLaunchTimes(ParceledListSlice estimatedLaunchTimes, int userId) {
 
+            super.setEstimatedLaunchTimes_enforcePermission();
+
             final long token = Binder.clearCallingIdentity();
             try {
                 UsageStatsService.this
diff --git a/services/usb/java/com/android/server/usb/DualOutputStreamDumpSink.java b/services/usb/java/com/android/server/usb/DualOutputStreamDumpSink.java
new file mode 100644
index 0000000..cea3d87
--- /dev/null
+++ b/services/usb/java/com/android/server/usb/DualOutputStreamDumpSink.java
@@ -0,0 +1,50 @@
+/*
+ * 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.usb;
+
+import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.server.utils.EventLogger;
+
+import java.util.List;
+
+/**
+ * Writes logs to {@link DualDumpOutputStream}.
+ *
+ * @see EventLogger.DumpSink
+ * @see DualDumpOutputStream
+ */
+final class DualOutputStreamDumpSink implements EventLogger.DumpSink {
+
+    private final long mId;
+    private final DualDumpOutputStream mDumpOutputStream;
+
+    /* package */ DualOutputStreamDumpSink(DualDumpOutputStream dualDumpOutputStream, long id) {
+        mDumpOutputStream = dualDumpOutputStream;
+        mId = id;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sink(String tag, List<EventLogger.Event> events) {
+        mDumpOutputStream.write("USB Event Log", mId, tag);
+        for (EventLogger.Event evt: events) {
+            mDumpOutputStream.write("USB Event", mId, evt.toString());
+        }
+    }
+}
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceLogger.java b/services/usb/java/com/android/server/usb/UsbDeviceLogger.java
deleted file mode 100644
index fab00bc..0000000
--- a/services/usb/java/com/android/server/usb/UsbDeviceLogger.java
+++ /dev/null
@@ -1,143 +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.
- */
-package com.android.server.usb;
-
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.dump.DualDumpOutputStream;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.LinkedList;
-
-/**
-* Constructor UsbDeviceLogger class
-*/
-public class UsbDeviceLogger {
-    private final Object mLock = new Object();
-
-    // ring buffer of events to log.
-    @GuardedBy("mLock")
-    private final LinkedList<Event> mEvents;
-
-    private final String mTitle;
-
-    // the maximum number of events to keep in log
-    private final int mMemSize;
-
-    /**
-     * Constructor for Event class.
-     */
-    public abstract static class Event {
-        // formatter for timestamps
-        private static final SimpleDateFormat sFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
-
-        private final Calendar mCalendar;
-
-        Event() {
-            mCalendar = Calendar.getInstance();
-        }
-
-    /**
-     * Convert event to String
-     * @return StringBuilder
-     */
-        public String toString() {
-            return (new StringBuilder(String.format("%tm-%td %tH:%tM:%tS.%tL",
-                    mCalendar, mCalendar, mCalendar, mCalendar, mCalendar, mCalendar)))
-                    .append(" ").append(eventToString()).toString();
-        }
-
-        /**
-         * Causes the string message for the event to appear in the logcat.
-         * Here is an example of how to create a new event (a StringEvent), adding it to the logger
-         * (an instance of UsbDeviceLoggerr) while also making it show in the logcat:
-         * <pre>
-         *     myLogger.log(
-         *         (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
-         * </pre>
-         * @param tag the tag for the android.util.Log.v
-         * @return the same instance of the event
-         */
-        public Event printLog(String tag) {
-            Log.i(tag, eventToString());
-            return this;
-        }
-
-        /**
-         * Convert event to String.
-         * This method is only called when the logger history is about to the dumped,
-         * so this method is where expensive String conversions should be made, not when the Event
-         * subclass is created.
-         * Timestamp information will be automatically added, do not include it.
-         * @return a string representation of the event that occurred.
-         */
-        public abstract String eventToString();
-    }
-
-    /**
-    * Constructor StringEvent class
-    */
-    public static class StringEvent extends Event {
-        private final String mMsg;
-
-        public StringEvent(String msg) {
-            mMsg = msg;
-        }
-
-        @Override
-        public String eventToString() {
-            return mMsg;
-        }
-    }
-
-    /**
-     * Constructor for logger.
-     * @param size the maximum number of events to keep in log
-     * @param title the string displayed before the recorded log
-     */
-    public UsbDeviceLogger(int size, String title) {
-        mEvents = new LinkedList<Event>();
-        mMemSize = size;
-        mTitle = title;
-    }
-
-    /**
-     * Constructor for logger.
-     * @param evt the maximum number of events to keep in log
-     */
-    public synchronized void log(Event evt) {
-        synchronized (mLock) {
-            if (mEvents.size() >= mMemSize) {
-                mEvents.removeFirst();
-            }
-            mEvents.add(evt);
-        }
-    }
-
-    /**
-     * Constructor for logger.
-     * @param dump the maximum number of events to keep in log
-     * @param id the category of events
-     */
-    public synchronized void dump(DualDumpOutputStream dump, long id) {
-        dump.write("USB Event Log", id, mTitle);
-        for (Event evt : mEvents) {
-            dump.write("USB Event", id, evt.toString());
-        }
-    }
-}
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index e90a376..1c081c1 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -91,6 +91,7 @@
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
+import com.android.server.utils.EventLogger;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.io.File;
@@ -213,7 +214,7 @@
     private static Set<Integer> sDenyInterfaces;
     private HashMap<Long, FileDescriptor> mControlFds;
 
-    private static UsbDeviceLogger sEventLogger;
+    private static EventLogger sEventLogger;
 
     static {
         sDenyInterfaces = new HashSet<>();
@@ -238,7 +239,7 @@
         public void onUEvent(UEventObserver.UEvent event) {
             if (DEBUG) Slog.v(TAG, "USB UEVENT: " + event.toString());
             if (sEventLogger != null) {
-                sEventLogger.log(new UsbDeviceLogger.StringEvent("USB UEVENT: "
+                sEventLogger.enqueue(new EventLogger.StringEvent("USB UEVENT: "
                         + event.toString()));
             } else {
                 if (DEBUG) Slog.d(TAG, "sEventLogger == null");
@@ -395,7 +396,7 @@
         mUEventObserver.startObserving(USB_STATE_MATCH);
         mUEventObserver.startObserving(ACCESSORY_START_MATCH);
 
-        sEventLogger = new UsbDeviceLogger(DUMPSYS_LOG_BUFFER, "UsbDeviceManager activity");
+        sEventLogger = new EventLogger(DUMPSYS_LOG_BUFFER, "UsbDeviceManager activity");
     }
 
     UsbProfileGroupSettingsManager getCurrentSettings() {
@@ -837,7 +838,7 @@
 
         protected void sendStickyBroadcast(Intent intent) {
             mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
-            sEventLogger.log(new UsbDeviceLogger.StringEvent("USB intent: " + intent));
+            sEventLogger.enqueue(new EventLogger.StringEvent("USB intent: " + intent));
         }
 
         private void updateUsbFunctions() {
@@ -2350,7 +2351,7 @@
 
         if (mHandler != null) {
             mHandler.dump(dump, "handler", UsbDeviceManagerProto.HANDLER);
-            sEventLogger.dump(dump, UsbHandlerProto.UEVENT);
+            sEventLogger.dump(new DualOutputStreamDumpSink(dump, UsbHandlerProto.UEVENT));
         }
 
         dump.end(token);
diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
index 2ae328b..394d6e7 100644
--- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.usb.UsbConfiguration;
+import android.hardware.usb.UsbConstants;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
 import android.hardware.usb.UsbEndpoint;
@@ -76,10 +77,10 @@
     // event schedulers for each input port of the physical device
     private MidiEventScheduler[] mEventSchedulers;
 
-    // Arbitrary number for timeout to not continue sending to
-    // an inactive device. This number tries to balances the number
-    // of cycles and not being permanently stuck.
-    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10;
+    // Timeout for sending a packet to a device.
+    // If bulkTransfer times out, retry sending the packet up to 20 times.
+    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50;
+    private static final int BULK_TRANSFER_NUMBER_OF_RETRIES = 20;
 
     // Arbitrary number for timeout when closing a thread
     private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200;
@@ -386,10 +387,15 @@
                                     break;
                                 }
                                 final UsbRequest response = connectionFinal.requestWait();
-                                if (response != request) {
-                                    Log.w(TAG, "Unexpected response");
+                                if (response == null) {
+                                    Log.w(TAG, "Response is null");
                                     break;
                                 }
+                                if (request != response) {
+                                    Log.w(TAG, "Skipping response");
+                                    continue;
+                                }
+
                                 int bytesRead = byteBuffer.position();
 
                                 if (bytesRead > 0) {
@@ -513,9 +519,47 @@
                                             convertedArray.length);
                                 }
 
-                                connectionFinal.bulkTransfer(endpointFinal, convertedArray,
-                                        convertedArray.length,
-                                        BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                boolean isInterrupted = false;
+                                // Split the packet into multiple if they are greater than the
+                                // endpoint's max packet size.
+                                for (int curPacketStart = 0;
+                                        curPacketStart < convertedArray.length &&
+                                        isInterrupted == false;
+                                        curPacketStart += endpointFinal.getMaxPacketSize()) {
+                                    int transferResult = -1;
+                                    int retryCount = 0;
+                                    int curPacketSize = Math.min(endpointFinal.getMaxPacketSize(),
+                                            convertedArray.length - curPacketStart);
+
+                                    // Keep trying to send the packet until the result is
+                                    // successful or until the retry limit is reached.
+                                    while (transferResult < 0 && retryCount <=
+                                            BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                        transferResult = connectionFinal.bulkTransfer(
+                                                endpointFinal,
+                                                convertedArray,
+                                                curPacketStart,
+                                                curPacketSize,
+                                                BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                        retryCount++;
+
+                                        if (Thread.currentThread().interrupted()) {
+                                            Log.w(TAG, "output thread interrupted after send");
+                                            isInterrupted = true;
+                                            break;
+                                        }
+                                        if (transferResult < 0) {
+                                            Log.d(TAG, "retrying packet. retryCount = "
+                                                    + retryCount + " result = " + transferResult);
+                                            if (retryCount > BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                                Log.w(TAG, "Skipping packet because timeout");
+                                            }
+                                        }
+                                    }
+                                }
+                                if (isInterrupted == true) {
+                                    break;
+                                }
                                 eventSchedulerFinal.addEventToPool(event);
                             }
                         } catch (NullPointerException e) {
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index bb0c4e9..f916660 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -53,8 +53,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -62,6 +60,9 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.utils.EventLogger;
 
 import libcore.io.IoUtils;
 
@@ -130,7 +131,7 @@
     @GuardedBy("mLock")
     private boolean mIsWriteSettingsScheduled;
 
-    private static UsbDeviceLogger sEventLogger;
+    private static EventLogger sEventLogger;
 
     /**
      * A package of a user.
@@ -263,7 +264,7 @@
 
         mUsbHandlerManager = usbResolveActivityManager;
 
-        sEventLogger = new UsbDeviceLogger(DUMPSYS_LOG_BUFFER,
+        sEventLogger = new EventLogger(DUMPSYS_LOG_BUFFER,
                 "UsbProfileGroupSettingsManager activity");
     }
 
@@ -970,7 +971,7 @@
                     matches, mAccessoryPreferenceMap.get(new AccessoryFilter(accessory)));
         }
 
-        sEventLogger.log(new UsbDeviceLogger.StringEvent("accessoryAttached: " + intent));
+        sEventLogger.enqueue(new EventLogger.StringEvent("accessoryAttached: " + intent));
         resolveActivity(intent, matches, defaultActivity, null, accessory);
     }
 
@@ -1524,7 +1525,8 @@
             }
         }
 
-        sEventLogger.dump(dump, UsbProfileGroupSettingsManagerProto.INTENT);
+        sEventLogger.dump(new DualOutputStreamDumpSink(dump,
+                UsbProfileGroupSettingsManagerProto.INTENT));
         dump.end(token);
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java
index 86f877f..72f6cc3 100644
--- a/services/usb/java/com/android/server/usb/UsbService.java
+++ b/services/usb/java/com/android/server/usb/UsbService.java
@@ -504,6 +504,15 @@
     }
 
     @Override
+    public boolean hasDevicePermissionWithIdentity(UsbDevice device, String packageName,
+            int pid, int uid) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
+
+        final int userId = UserHandle.getUserId(uid);
+        return getPermissionsForUser(userId).hasPermission(device, packageName, pid, uid);
+    }
+
+    @Override
     public boolean hasAccessoryPermission(UsbAccessory accessory) {
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
@@ -518,6 +527,14 @@
     }
 
     @Override
+    public boolean hasAccessoryPermissionWithIdentity(UsbAccessory accessory, int pid, int uid) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
+
+        final int userId = UserHandle.getUserId(uid);
+        return getPermissionsForUser(userId).hasPermission(accessory, pid, uid);
+    }
+
+    @Override
     public void requestDevicePermission(UsbDevice device, String packageName, PendingIntent pi) {
         final int uid = Binder.getCallingUid();
         final int pid = Binder.getCallingPid();
diff --git a/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java b/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
index dd5f153..f39cb39 100644
--- a/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
+++ b/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
@@ -48,13 +48,13 @@
 import android.util.EventLog;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbVCInputTerminal.java b/services/usb/java/com/android/server/usb/descriptors/UsbVCInputTerminal.java
index df63795..7a41b50 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbVCInputTerminal.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbVCInputTerminal.java
@@ -46,4 +46,4 @@
         // TODO Add reporting specific to this descriptor
         super.report(canvas);
     }
-};
+}
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbVCOutputTerminal.java b/services/usb/java/com/android/server/usb/descriptors/UsbVCOutputTerminal.java
index 4aa8ca2..32275a6 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbVCOutputTerminal.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbVCOutputTerminal.java
@@ -46,4 +46,4 @@
         super.report(canvas);
         // TODO Add reporting specific to this descriptor
     }
-};
+}
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbVCProcessingUnit.java b/services/usb/java/com/android/server/usb/descriptors/UsbVCProcessingUnit.java
index 5ce842e..0692066 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbVCProcessingUnit.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbVCProcessingUnit.java
@@ -47,4 +47,4 @@
         super.report(canvas);
         // TODO Add reporting specific to this descriptor
     }
-};
+}
diff --git a/services/usb/java/com/android/server/usb/descriptors/UsbVCSelectorUnit.java b/services/usb/java/com/android/server/usb/descriptors/UsbVCSelectorUnit.java
index 8e9b0d8..604dd66 100644
--- a/services/usb/java/com/android/server/usb/descriptors/UsbVCSelectorUnit.java
+++ b/services/usb/java/com/android/server/usb/descriptors/UsbVCSelectorUnit.java
@@ -47,4 +47,4 @@
         super.report(canvas);
         // TODO Add reporting specific to this descriptor
     }
-};
+}
diff --git a/services/voiceinteraction/TEST_MAPPING b/services/voiceinteraction/TEST_MAPPING
index c083e90..af67637 100644
--- a/services/voiceinteraction/TEST_MAPPING
+++ b/services/voiceinteraction/TEST_MAPPING
@@ -23,6 +23,14 @@
           "exclude-annotation": "androidx.test.filters.FlakyTest"
         }
       ]
+    },
+    {
+      "name": "CtsLocalVoiceInteraction",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
     }
   ]
 }
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java
deleted file mode 100644
index 73b4ce7..0000000
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerLogger.java
+++ /dev/null
@@ -1,136 +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.server.soundtrigger;
-
-import android.util.Log;
-
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.LinkedList;
-
-/**
-* Constructor SoundTriggerLogger class
-*/
-public class SoundTriggerLogger {
-
-    // ring buffer of events to log.
-    private final LinkedList<Event> mEvents;
-
-    private final String mTitle;
-
-    // the maximum number of events to keep in log
-    private final int mMemSize;
-
-    /**
-     * Constructor for Event class.
-     */
-    public abstract static class Event {
-        // formatter for timestamps
-        private static final SimpleDateFormat sFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
-
-        private final long mTimestamp;
-
-        Event() {
-            mTimestamp = System.currentTimeMillis();
-        }
-
-    /**
-     * Convert event to String
-     * @return StringBuilder
-     */
-        public String toString() {
-            return (new StringBuilder(sFormat.format(new Date(mTimestamp))))
-                    .append(" ").append(eventToString()).toString();
-        }
-
-        /**
-         * Causes the string message for the event to appear in the logcat.
-         * Here is an example of how to create a new event (a StringEvent), adding it to the logger
-         * (an instance of SoundTriggerLogger) while also making it show in the logcat:
-         * <pre>
-         *     myLogger.log(
-         *         (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
-         * </pre>
-         * @param tag the tag for the android.util.Log.v
-         * @return the same instance of the event
-         */
-        public Event printLog(String tag) {
-            Log.i(tag, eventToString());
-            return this;
-        }
-
-        /**
-         * Convert event to String.
-         * This method is only called when the logger history is about to the dumped,
-         * so this method is where expensive String conversions should be made, not when the Event
-         * subclass is created.
-         * Timestamp information will be automatically added, do not include it.
-         * @return a string representation of the event that occurred.
-         */
-        public abstract String eventToString();
-    }
-
-    /**
-    * Constructor StringEvent class
-    */
-    public static class StringEvent extends Event {
-        private final String mMsg;
-
-        public StringEvent(String msg) {
-            mMsg = msg;
-        }
-
-        @Override
-        public String eventToString() {
-            return mMsg;
-        }
-    }
-
-    /**
-     * Constructor for logger.
-     * @param size the maximum number of events to keep in log
-     * @param title the string displayed before the recorded log
-     */
-    public SoundTriggerLogger(int size, String title) {
-        mEvents = new LinkedList<Event>();
-        mMemSize = size;
-        mTitle = title;
-    }
-
-    /**
-     * Constructor for logger.
-     * @param evt the maximum number of events to keep in log
-     */
-    public synchronized void log(Event evt) {
-        if (mEvents.size() >= mMemSize) {
-            mEvents.removeFirst();
-        }
-        mEvents.add(evt);
-    }
-
-    /**
-     * Constructor for logger.
-     * @param pw the maximum number of events to keep in log
-     */
-    public synchronized void dump(PrintWriter pw) {
-        pw.println("ST Event log: " + mTitle);
-        for (Event evt : mEvents) {
-            pw.println(evt.toString());
-        }
-    }
-}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 5183e5b..81717f4 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -84,6 +84,7 @@
 import com.android.internal.app.ISoundTriggerService;
 import com.android.internal.app.ISoundTriggerSession;
 import com.android.server.SystemService;
+import com.android.server.utils.EventLogger;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -309,14 +310,14 @@
                     Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("startRecognition(): Uuid : "
-                        + parcelUuid));
+                sEventLogger.enqueue(new EventLogger.StringEvent(
+                        "startRecognition(): Uuid : " + parcelUuid));
 
                 GenericSoundModel model = getSoundModel(parcelUuid);
                 if (model == null) {
                     Slog.w(TAG, "Null model in database for id: " + parcelUuid);
 
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    sEventLogger.enqueue(new EventLogger.StringEvent(
                             "startRecognition(): Null model in database for id: " + parcelUuid));
 
                     return STATUS_ERROR;
@@ -339,7 +340,7 @@
                     Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("stopRecognition(): Uuid : "
+                sEventLogger.enqueue(new EventLogger.StringEvent("stopRecognition(): Uuid : "
                         + parcelUuid));
 
                 int ret = mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(),
@@ -359,7 +360,7 @@
                     Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("getSoundModel(): id = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("getSoundModel(): id = "
                         + soundModelId));
 
                 SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
@@ -376,7 +377,7 @@
                     Slog.i(TAG, "updateSoundModel(): model = " + soundModel);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("updateSoundModel(): model = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("updateSoundModel(): model = "
                         + soundModel));
 
                 mDbHelper.updateGenericSoundModel(soundModel);
@@ -391,7 +392,7 @@
                     Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("deleteSoundModel(): id = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("deleteSoundModel(): id = "
                         + soundModelId));
 
                 // Unload the model if it is loaded.
@@ -411,7 +412,7 @@
                 if (soundModel == null || soundModel.getUuid() == null) {
                     Slog.w(TAG, "Invalid sound model");
 
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    sEventLogger.enqueue(new EventLogger.StringEvent(
                             "loadGenericSoundModel(): Invalid sound model"));
 
                     return STATUS_ERROR;
@@ -420,7 +421,7 @@
                     Slog.i(TAG, "loadGenericSoundModel(): id = " + soundModel.getUuid());
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("loadGenericSoundModel(): id = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("loadGenericSoundModel(): id = "
                         + soundModel.getUuid()));
 
                 synchronized (mLock) {
@@ -447,7 +448,7 @@
                 if (soundModel == null || soundModel.getUuid() == null) {
                     Slog.w(TAG, "Invalid sound model");
 
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    sEventLogger.enqueue(new EventLogger.StringEvent(
                             "loadKeyphraseSoundModel(): Invalid sound model"));
 
                     return STATUS_ERROR;
@@ -455,7 +456,7 @@
                 if (soundModel.getKeyphrases() == null || soundModel.getKeyphrases().length != 1) {
                     Slog.w(TAG, "Only one keyphrase per model is currently supported.");
 
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    sEventLogger.enqueue(new EventLogger.StringEvent(
                             "loadKeyphraseSoundModel(): Only one keyphrase per model"
                                     + " is currently supported."));
 
@@ -465,8 +466,8 @@
                     Slog.i(TAG, "loadKeyphraseSoundModel(): id = " + soundModel.getUuid());
                 }
 
-                sEventLogger.log(
-                        new SoundTriggerLogger.StringEvent("loadKeyphraseSoundModel(): id = "
+                sEventLogger.enqueue(
+                        new EventLogger.StringEvent("loadKeyphraseSoundModel(): id = "
                                 + soundModel.getUuid()));
 
                 synchronized (mLock) {
@@ -503,7 +504,7 @@
                     Slog.i(TAG, "startRecognition(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "startRecognitionForService(): id = " + soundModelId));
 
                 IRecognitionStatusCallback callback =
@@ -515,7 +516,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "startRecognitionForService():" + soundModelId + " is not loaded"));
 
                         return STATUS_ERROR;
@@ -527,7 +528,7 @@
                     if (existingCallback != null) {
                         Slog.w(TAG, soundModelId + " is already running");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "startRecognitionForService():"
                                         + soundModelId + " is already running"));
 
@@ -542,7 +543,7 @@
                         default:
                             Slog.e(TAG, "Unknown model type");
 
-                            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            sEventLogger.enqueue(new EventLogger.StringEvent(
                                     "startRecognitionForService(): Unknown model type"));
 
                             return STATUS_ERROR;
@@ -551,7 +552,7 @@
                     if (ret != STATUS_OK) {
                         Slog.e(TAG, "Failed to start model: " + ret);
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "startRecognitionForService(): Failed to start model:"));
 
                         return ret;
@@ -574,7 +575,7 @@
                     Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "stopRecognitionForService(): id = " + soundModelId));
 
                 synchronized (mLock) {
@@ -582,7 +583,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "stopRecognitionForService(): " + soundModelId
                                         + " is not loaded"));
 
@@ -595,7 +596,7 @@
                     if (callback == null) {
                         Slog.w(TAG, soundModelId + " is not running");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "stopRecognitionForService(): " + soundModelId
                                         + " is not running"));
 
@@ -610,7 +611,7 @@
                         default:
                             Slog.e(TAG, "Unknown model type");
 
-                            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            sEventLogger.enqueue(new EventLogger.StringEvent(
                                     "stopRecognitionForService(): Unknown model type"));
 
                             return STATUS_ERROR;
@@ -619,7 +620,7 @@
                     if (ret != STATUS_OK) {
                         Slog.e(TAG, "Failed to stop model: " + ret);
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "stopRecognitionForService(): Failed to stop model: " + ret));
 
                         return ret;
@@ -642,7 +643,7 @@
                     Slog.i(TAG, "unloadSoundModel(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("unloadSoundModel(): id = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("unloadSoundModel(): id = "
                         + soundModelId));
 
                 synchronized (mLock) {
@@ -650,7 +651,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "unloadSoundModel(): " + soundModelId + " is not loaded"));
 
                         return STATUS_ERROR;
@@ -667,7 +668,7 @@
                         default:
                             Slog.e(TAG, "Unknown model type");
 
-                            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            sEventLogger.enqueue(new EventLogger.StringEvent(
                                     "unloadSoundModel(): Unknown model type"));
 
                             return STATUS_ERROR;
@@ -675,7 +676,7 @@
                     if (ret != STATUS_OK) {
                         Slog.e(TAG, "Failed to unload model");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "unloadSoundModel(): Failed to unload model"));
 
                         return ret;
@@ -709,7 +710,7 @@
                     Slog.i(TAG, "getModelState(): id = " + soundModelId);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): id = "
+                sEventLogger.enqueue(new EventLogger.StringEvent("getModelState(): id = "
                         + soundModelId));
 
                 synchronized (mLock) {
@@ -717,7 +718,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): "
+                        sEventLogger.enqueue(new EventLogger.StringEvent("getModelState(): "
                                 + soundModelId + " is not loaded"));
 
                         return ret;
@@ -729,7 +730,7 @@
                         default:
                             // SoundModel.TYPE_KEYPHRASE is not supported to increase privacy.
                             Slog.e(TAG, "Unsupported model type, " + soundModel.getType());
-                            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                            sEventLogger.enqueue(new EventLogger.StringEvent(
                                     "getModelState(): Unsupported model type, "
                                             + soundModel.getType()));
                             break;
@@ -751,7 +752,7 @@
 
                 synchronized (mLock) {
                     ModuleProperties properties = mSoundTriggerHelper.getModuleProperties();
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                    sEventLogger.enqueue(new EventLogger.StringEvent(
                             "getModuleProperties(): " + properties));
                     return properties;
                 }
@@ -769,7 +770,7 @@
                             + ", value=" + value);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "setParameter(): id=" + soundModelId
                                 + ", param=" + modelParam
                                 + ", value=" + value));
@@ -780,7 +781,7 @@
                         Slog.w(TAG, soundModelId + " is not loaded. Loaded models: "
                                 + mLoadedModels.toString());
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent("setParameter(): "
+                        sEventLogger.enqueue(new EventLogger.StringEvent("setParameter(): "
                                 + soundModelId + " is not loaded"));
 
                         return STATUS_BAD_VALUE;
@@ -803,7 +804,7 @@
                             + ", param=" + modelParam);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "getParameter(): id=" + soundModelId
                                 + ", param=" + modelParam));
 
@@ -812,7 +813,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent("getParameter(): "
+                        sEventLogger.enqueue(new EventLogger.StringEvent("getParameter(): "
                                 + soundModelId + " is not loaded"));
 
                         throw new IllegalArgumentException("sound model is not loaded");
@@ -834,7 +835,7 @@
                             + ", param=" + modelParam);
                 }
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "queryParameter(): id=" + soundModelId
                                 + ", param=" + modelParam));
 
@@ -843,7 +844,7 @@
                     if (soundModel == null) {
                         Slog.w(TAG, soundModelId + " is not loaded");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "queryParameter(): "
                                         + soundModelId + " is not loaded"));
 
@@ -857,7 +858,7 @@
 
         private void clientDied() {
             Slog.w(TAG, "Client died, cleaning up session.");
-            sEventLogger.log(new SoundTriggerLogger.StringEvent(
+            sEventLogger.enqueue(new EventLogger.StringEvent(
                     "Client died, cleaning up session."));
             mSoundTriggerHelper.detach();
         }
@@ -1027,7 +1028,7 @@
                     } catch (Exception e) {
                         Slog.e(TAG, mPuuid + ": Cannot remove client", e);
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                        sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                 + ": Cannot remove client"));
 
                     }
@@ -1052,7 +1053,7 @@
             private void destroy() {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + ": destroy"));
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid + ": destroy"));
 
                 synchronized (mRemoteServiceLock) {
                     disconnectLocked();
@@ -1086,7 +1087,7 @@
                                 Slog.e(TAG, mPuuid + ": Could not stop operation "
                                         + mRunningOpIds.valueAt(i), e);
 
-                                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                         + ": Could not stop operation " + mRunningOpIds.valueAt(
                                         i)));
 
@@ -1116,7 +1117,7 @@
                     if (ri == null) {
                         Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                        sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                 + ": " + mServiceName + " not found"));
 
                         return;
@@ -1127,7 +1128,7 @@
                         Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
                                 + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                        sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                 + ": " + mServiceName + " does not require "
                                 + BIND_SOUND_TRIGGER_DETECTION_SERVICE));
 
@@ -1143,7 +1144,7 @@
                     } else {
                         Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                        sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                 + ": Could not bind to " + mServiceName));
 
                     }
@@ -1165,7 +1166,7 @@
                                 mPuuid + ": Dropped operation as already destroyed or marked for "
                                         + "destruction");
 
-                        sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                        sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                 + ":Dropped operation as already destroyed or marked for "
                                 + "destruction"));
 
@@ -1197,7 +1198,7 @@
                                             mPuuid + ": Dropped operation as too many operations "
                                                     + "were run in last 24 hours");
 
-                                    sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                    sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                             + ": Dropped operation as too many operations "
                                             + "were run in last 24 hours"));
 
@@ -1207,7 +1208,7 @@
                             } catch (Exception e) {
                                 Slog.e(TAG, mPuuid + ": Could not drop operation", e);
 
-                                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                         + ": Could not drop operation"));
 
                             }
@@ -1224,7 +1225,7 @@
                             try {
                                 if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
 
-                                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                         + ": runOp " + opId));
 
                                 op.run(opId, mService);
@@ -1232,7 +1233,7 @@
                             } catch (Exception e) {
                                 Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
 
-                                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                                         + ": Could not run operation " + opId));
 
                             }
@@ -1265,7 +1266,7 @@
                 Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
                         + ")");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + "->" + mServiceName
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid + "->" + mServiceName
                         + ": IGNORED onKeyphraseDetected(" + event + ")"));
             }
 
@@ -1282,9 +1283,9 @@
                 attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
                 AudioAttributes attributes = attributesBuilder.build();
 
-                    AudioFormat originalFormat = event.getCaptureFormat();
+                AudioFormat originalFormat = event.getCaptureFormat();
 
-                    sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
+                sEventLogger.enqueue(new EventLogger.StringEvent("createAudioRecordForEvent"));
 
                 return (new AudioRecord.Builder())
                             .setAudioAttributes(attributes)
@@ -1301,7 +1302,7 @@
             public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + ": Generic sound trigger event: " + event));
 
                 runOrAddOperation(new Operation(
@@ -1336,7 +1337,7 @@
             public void onError(int status) {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + ": onError: " + status));
 
                 runOrAddOperation(
@@ -1359,7 +1360,7 @@
             public void onRecognitionPaused() {
                 Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + "->" + mServiceName + ": IGNORED onRecognitionPaused"));
 
             }
@@ -1368,7 +1369,7 @@
             public void onRecognitionResumed() {
                 Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + "->" + mServiceName + ": IGNORED onRecognitionResumed"));
 
             }
@@ -1377,7 +1378,7 @@
             public void onServiceConnected(ComponentName name, IBinder service) {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + ": onServiceConnected(" + service + ")"));
 
                 synchronized (mRemoteServiceLock) {
@@ -1400,7 +1401,7 @@
             public void onServiceDisconnected(ComponentName name) {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + ": onServiceDisconnected"));
 
                 synchronized (mRemoteServiceLock) {
@@ -1412,7 +1413,7 @@
             public void onBindingDied(ComponentName name) {
                 if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
+                sEventLogger.enqueue(new EventLogger.StringEvent(mPuuid
                         + ": onBindingDied"));
 
                 synchronized (mRemoteServiceLock) {
@@ -1424,7 +1425,7 @@
             public void onNullBinding(ComponentName name) {
                 Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
 
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(name + " for model "
+                sEventLogger.enqueue(new EventLogger.StringEvent(name + " for model "
                         + mPuuid + " returned a null binding"));
 
                 synchronized (mRemoteServiceLock) {
@@ -1610,7 +1611,7 @@
 
             private void clientDied() {
                 Slog.w(TAG, "Client died, cleaning up session.");
-                sEventLogger.log(new SoundTriggerLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "Client died, cleaning up session."));
                 mSoundTriggerHelper.detach();
             }
@@ -1637,7 +1638,7 @@
     //=================================================================
     // For logging
 
-    private static final SoundTriggerLogger sEventLogger = new SoundTriggerLogger(200,
+    private static final EventLogger sEventLogger = new EventLogger(200,
             "SoundTrigger activity");
 
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java
new file mode 100644
index 0000000..d5eea1f
--- /dev/null
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java
@@ -0,0 +1,233 @@
+/*
+ * 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.voiceinteraction;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+
+import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
+import android.media.permission.Identity;
+import android.os.ParcelFileDescriptor;
+import android.service.voice.HotwordAudioStream;
+import android.service.voice.HotwordDetectedResult;
+import android.util.Pair;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+final class HotwordAudioStreamManager {
+
+    private static final String TAG = "HotwordAudioStreamManager";
+    private static final String OP_MESSAGE = "Streaming hotword audio to VoiceInteractionService";
+    private static final String TASK_ID_PREFIX = "HotwordDetectedResult@";
+    private static final String THREAD_NAME_PREFIX = "Copy-";
+
+    private final AppOpsManager mAppOpsManager;
+    private final Identity mVoiceInteractorIdentity;
+    private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
+
+    HotwordAudioStreamManager(@NonNull AppOpsManager appOpsManager,
+            @NonNull Identity voiceInteractorIdentity) {
+        mAppOpsManager = appOpsManager;
+        mVoiceInteractorIdentity = voiceInteractorIdentity;
+    }
+
+    /**
+     * Starts copying the audio streams in the given {@link HotwordDetectedResult}.
+     * <p>
+     * The returned {@link HotwordDetectedResult} is identical the one that was passed in, except
+     * that the {@link ParcelFileDescriptor}s within {@link HotwordDetectedResult#getAudioStreams()}
+     * are replaced with descriptors from pipes managed by {@link HotwordAudioStreamManager}. The
+     * returned value should be passed on to the client (i.e., the voice interactor).
+     * </p>
+     *
+     * @throws IOException If there was an error creating the managed pipe.
+     */
+    @NonNull
+    public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result)
+            throws IOException {
+        List<HotwordAudioStream> audioStreams = result.getAudioStreams();
+        if (audioStreams.isEmpty()) {
+            return result;
+        }
+
+        List<HotwordAudioStream> newAudioStreams = new ArrayList<>(audioStreams.size());
+        List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> sourcesAndSinks = new ArrayList<>(
+                audioStreams.size());
+        for (HotwordAudioStream audioStream : audioStreams) {
+            ParcelFileDescriptor[] clientPipe = ParcelFileDescriptor.createReliablePipe();
+            ParcelFileDescriptor clientAudioSource = clientPipe[0];
+            ParcelFileDescriptor clientAudioSink = clientPipe[1];
+            HotwordAudioStream newAudioStream =
+                    audioStream.buildUpon().setAudioStreamParcelFileDescriptor(
+                            clientAudioSource).build();
+            newAudioStreams.add(newAudioStream);
+
+            ParcelFileDescriptor serviceAudioSource =
+                    audioStream.getAudioStreamParcelFileDescriptor();
+            sourcesAndSinks.add(new Pair<>(serviceAudioSource, clientAudioSink));
+        }
+
+        String resultTaskId = TASK_ID_PREFIX + System.identityHashCode(result);
+        mExecutorService.execute(new HotwordDetectedResultCopyTask(resultTaskId, sourcesAndSinks));
+
+        return result.buildUpon().setAudioStreams(newAudioStreams).build();
+    }
+
+    private class HotwordDetectedResultCopyTask implements Runnable {
+        private final String mResultTaskId;
+        private final List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> mSourcesAndSinks;
+        private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
+
+        HotwordDetectedResultCopyTask(String resultTaskId,
+                List<Pair<ParcelFileDescriptor, ParcelFileDescriptor>> sourcesAndSinks) {
+            mResultTaskId = resultTaskId;
+            mSourcesAndSinks = sourcesAndSinks;
+        }
+
+        @Override
+        public void run() {
+            Thread.currentThread().setName(THREAD_NAME_PREFIX + mResultTaskId);
+            int size = mSourcesAndSinks.size();
+            List<SingleAudioStreamCopyTask> tasks = new ArrayList<>(size);
+            for (int i = 0; i < size; i++) {
+                Pair<ParcelFileDescriptor, ParcelFileDescriptor> sourceAndSink =
+                        mSourcesAndSinks.get(i);
+                ParcelFileDescriptor serviceAudioSource = sourceAndSink.first;
+                ParcelFileDescriptor clientAudioSink = sourceAndSink.second;
+                String streamTaskId = mResultTaskId + "@" + i;
+                tasks.add(new SingleAudioStreamCopyTask(streamTaskId, serviceAudioSource,
+                        clientAudioSink));
+            }
+
+            if (mAppOpsManager.startOpNoThrow(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
+                    mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+                    mVoiceInteractorIdentity.attributionTag, OP_MESSAGE) == MODE_ALLOWED) {
+                try {
+                    // TODO(b/244599891): Set timeout, close after inactivity
+                    mExecutorService.invokeAll(tasks);
+                } catch (InterruptedException e) {
+                    Slog.e(TAG, mResultTaskId + ": Task was interrupted", e);
+                    bestEffortPropagateError(e.getMessage());
+                } finally {
+                    mAppOpsManager.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
+                            mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+                            mVoiceInteractorIdentity.attributionTag);
+                }
+            } else {
+                bestEffortPropagateError(
+                        "Failed to obtain RECORD_AUDIO_HOTWORD permission for "
+                                + SoundTriggerSessionPermissionsDecorator.toString(
+                                mVoiceInteractorIdentity));
+            }
+        }
+
+        private void bestEffortPropagateError(@NonNull String errorMessage) {
+            try {
+                for (Pair<ParcelFileDescriptor, ParcelFileDescriptor> sourceAndSink :
+                        mSourcesAndSinks) {
+                    ParcelFileDescriptor serviceAudioSource = sourceAndSink.first;
+                    ParcelFileDescriptor clientAudioSink = sourceAndSink.second;
+                    serviceAudioSource.closeWithError(errorMessage);
+                    clientAudioSink.closeWithError(errorMessage);
+                }
+            } catch (IOException e) {
+                Slog.e(TAG, mResultTaskId + ": Failed to propagate error", e);
+            }
+        }
+    }
+
+    private static class SingleAudioStreamCopyTask implements Callable<Void> {
+        // TODO: Make this buffer size customizable from updateState()
+        private static final int COPY_BUFFER_LENGTH = 2_560;
+
+        private final String mStreamTaskId;
+        private final ParcelFileDescriptor mAudioSource;
+        private final ParcelFileDescriptor mAudioSink;
+
+        SingleAudioStreamCopyTask(String streamTaskId, ParcelFileDescriptor audioSource,
+                ParcelFileDescriptor audioSink) {
+            mStreamTaskId = streamTaskId;
+            mAudioSource = audioSource;
+            mAudioSink = audioSink;
+        }
+
+        @Override
+        public Void call() throws Exception {
+            Thread.currentThread().setName(THREAD_NAME_PREFIX + mStreamTaskId);
+
+            // Note: We are intentionally NOT using try-with-resources here. If we did,
+            // the ParcelFileDescriptors will be automatically closed WITHOUT errors before we go
+            // into the IOException-catch block. We want to propagate the error while closing the
+            // PFDs.
+            InputStream fis = null;
+            OutputStream fos = null;
+            try {
+                fis = new ParcelFileDescriptor.AutoCloseInputStream(mAudioSource);
+                fos = new ParcelFileDescriptor.AutoCloseOutputStream(mAudioSink);
+                byte[] buffer = new byte[COPY_BUFFER_LENGTH];
+                while (true) {
+                    if (Thread.interrupted()) {
+                        Slog.e(TAG,
+                                mStreamTaskId + ": SingleAudioStreamCopyTask task was interrupted");
+                        break;
+                    }
+
+                    int bytesRead = fis.read(buffer);
+                    if (bytesRead < 0) {
+                        Slog.i(TAG, mStreamTaskId + ": Reached end of audio stream");
+                        break;
+                    }
+                    if (bytesRead > 0) {
+                        if (DEBUG) {
+                            // TODO(b/244599440): Add proper logging
+                            Slog.d(TAG, mStreamTaskId + ": Copied " + bytesRead
+                                    + " bytes from audio stream. First 20 bytes=" + Arrays.toString(
+                                    Arrays.copyOfRange(buffer, 0, 20)));
+                        }
+                        fos.write(buffer, 0, bytesRead);
+                    }
+                    // TODO(b/244599891): Close PFDs after inactivity
+                }
+            } catch (IOException e) {
+                mAudioSource.closeWithError(e.getMessage());
+                mAudioSink.closeWithError(e.getMessage());
+                Slog.e(TAG, mStreamTaskId + ": Failed to copy audio stream", e);
+            } finally {
+                if (fis != null) {
+                    fis.close();
+                }
+                if (fos != null) {
+                    fos.close();
+                }
+            }
+
+            return null;
+        }
+    }
+
+}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 921f6e2..3e49aed 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -19,7 +19,6 @@
 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
 import static android.Manifest.permission.RECORD_AUDIO;
 import static android.service.attention.AttentionService.PROXIMITY_UNKNOWN;
-import static android.service.voice.HotwordDetectedResult.EXTRA_PROXIMITY_METERS;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
 import static android.service.voice.HotwordDetectionService.ENABLE_PROXIMITY_RESULT;
@@ -35,7 +34,12 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__APP_REQUEST_UPDATE_STATE;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_ERROR_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_PROCESS_RESTARTED_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_REJECTED_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_STATUS_REPORTED_EXCEPTION;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_UPDATE_STATE_AFTER_TIMEOUT;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALL_UPDATE_STATE_EXCEPTION;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECTED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED;
@@ -129,7 +133,10 @@
 
     private static final String KEY_RESTART_PERIOD_IN_SECONDS = "restart_period_in_seconds";
     // TODO: These constants need to be refined.
-    private static final long VALIDATION_TIMEOUT_MILLIS = 4000;
+    // The validation timeout value is 3 seconds for onDetect of DSP trigger event.
+    private static final long VALIDATION_TIMEOUT_MILLIS = 3000;
+    // Write the onDetect timeout metric when it takes more time than MAX_VALIDATION_TIMEOUT_MILLIS.
+    private static final long MAX_VALIDATION_TIMEOUT_MILLIS = 4000;
     private static final long MAX_UPDATE_TIMEOUT_MILLIS = 30000;
     private static final long EXTERNAL_HOTWORD_CLEANUP_MILLIS = 2000;
     private static final Duration MAX_UPDATE_TIMEOUT_DURATION =
@@ -141,6 +148,7 @@
     private static final int HOTWORD_DETECTION_SERVICE_DIED = -1;
     private static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2;
     private static final int CALLBACK_DETECT_TIMEOUT = -3;
+    private static final int CALLBACK_ONDETECTED_STREAM_COPY_ERROR = -4;
 
     // Hotword metrics
     private static final int METRICS_INIT_UNKNOWN_TIMEOUT =
@@ -167,11 +175,15 @@
             HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED;
     private static final int METRICS_EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION =
             HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION;
+    private static final int METRICS_CALLBACK_ON_STATUS_REPORTED_EXCEPTION =
+            HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_STATUS_REPORTED_EXCEPTION;
 
     private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
     // TODO: This may need to be a Handler(looper)
     private final ScheduledExecutorService mScheduledExecutorService =
             Executors.newSingleThreadScheduledExecutor();
+    private final AppOpsManager mAppOpsManager;
+    private final HotwordAudioStreamManager mHotwordAudioStreamManager;
     @Nullable private final ScheduledFuture<?> mCancellationTaskFuture;
     private final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false);
     private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied;
@@ -193,7 +205,7 @@
     @Nullable AttentionManagerInternal mAttentionManagerInternal = null;
 
     final AttentionManagerInternal.ProximityUpdateCallbackInternal mProximityCallbackInternal =
-            this::setProximityMeters;
+            this::setProximityValue;
 
 
     volatile HotwordDetectionServiceIdentity mIdentity;
@@ -232,6 +244,9 @@
         mContext = context;
         mVoiceInteractionServiceUid = voiceInteractionServiceUid;
         mVoiceInteractorIdentity = voiceInteractorIdentity;
+        mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        mHotwordAudioStreamManager = new HotwordAudioStreamManager(mAppOpsManager,
+                mVoiceInteractorIdentity);
         mDetectionComponentName = serviceName;
         mUser = userId;
         mCallback = callback;
@@ -333,11 +348,10 @@
                         HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
                                 initResultMetricsResult);
                     } catch (RemoteException e) {
-                        // TODO: Add a new atom for RemoteException case, the error doesn't very
-                        // correct here
                         Slog.w(TAG, "Failed to report initialization status: " + e);
-                        HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
-                                METRICS_INIT_CALLBACK_STATE_ERROR);
+                        HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                                METRICS_CALLBACK_ON_STATUS_REPORTED_EXCEPTION,
+                                mVoiceInteractionServiceUid);
                     }
                 }
             };
@@ -349,6 +363,9 @@
             } catch (RemoteException e) {
                 // TODO: (b/181842909) Report an error to voice interactor
                 Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e);
+                HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                        HOTWORD_DETECTOR_EVENTS__EVENT__CALL_UPDATE_STATE_EXCEPTION,
+                        mVoiceInteractionServiceUid);
             }
             return future.orTimeout(MAX_UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         }).whenComplete((res, err) -> {
@@ -363,8 +380,9 @@
                             METRICS_INIT_UNKNOWN_TIMEOUT);
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to report initialization status UNKNOWN", e);
-                    HotwordMetricsLogger.writeServiceInitResultEvent(mDetectorType,
-                            METRICS_INIT_CALLBACK_STATE_ERROR);
+                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                            METRICS_CALLBACK_ON_STATUS_REPORTED_EXCEPTION,
+                            mVoiceInteractionServiceUid);
                 }
             } else if (err != null) {
                 Slog.w(TAG, "Failed to update state: " + err);
@@ -485,14 +503,20 @@
                         mSoftwareCallback.onError();
                         return;
                     }
-                    saveProximityMetersToBundle(result);
-                    mSoftwareCallback.onDetected(result, null, null);
-                    if (result != null) {
-                        Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
-                                + " bits from hotword trusted process");
-                        if (mDebugHotwordLogging) {
-                            Slog.i(TAG, "Egressed detected result: " + result);
-                        }
+                    saveProximityValueToBundle(result);
+                    HotwordDetectedResult newResult;
+                    try {
+                        newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
+                    } catch (IOException e) {
+                        // TODO: Write event
+                        mSoftwareCallback.onError();
+                        return;
+                    }
+                    mSoftwareCallback.onDetected(newResult, null, null);
+                    Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult)
+                            + " bits from hotword trusted process");
+                    if (mDebugHotwordLogging) {
+                        Slog.i(TAG, "Egressed detected result: " + newResult);
                     }
                 }
             }
@@ -607,20 +631,27 @@
                         enforcePermissionsForDataDelivery();
                         enforceExtraKeyphraseIdNotLeaked(result, recognitionEvent);
                     } catch (SecurityException e) {
+                        Slog.i(TAG, "Ignoring #onDetected due to a SecurityException", e);
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 mDetectorType,
                                 METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
                         externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
                         return;
                     }
-                    saveProximityMetersToBundle(result);
-                    externalCallback.onKeyphraseDetected(recognitionEvent, result);
-                    if (result != null) {
-                        Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
-                                + " bits from hotword trusted process");
-                        if (mDebugHotwordLogging) {
-                            Slog.i(TAG, "Egressed detected result: " + result);
-                        }
+                    saveProximityValueToBundle(result);
+                    HotwordDetectedResult newResult;
+                    try {
+                        newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
+                    } catch (IOException e) {
+                        // TODO: Write event
+                        externalCallback.onError(CALLBACK_ONDETECTED_STREAM_COPY_ERROR);
+                        return;
+                    }
+                    externalCallback.onKeyphraseDetected(recognitionEvent, newResult);
+                    Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult)
+                            + " bits from hotword trusted process");
+                    if (mDebugHotwordLogging) {
+                        Slog.i(TAG, "Egressed detected result: " + newResult);
                     }
                 }
             }
@@ -659,6 +690,10 @@
         synchronized (mLock) {
             mValidatingDspTrigger = true;
             mRemoteHotwordDetectionService.run(service -> {
+                // We use the VALIDATION_TIMEOUT_MILLIS to inform that the client needs to invoke
+                // the callback before timeout value. In order to reduce the latency impact between
+                // server side and client side, we need to use another timeout value
+                // MAX_VALIDATION_TIMEOUT_MILLIS to monitor it.
                 mCancellationKeyPhraseDetectionFuture = mScheduledExecutorService.schedule(
                         () -> {
                             // TODO: avoid allocate every time
@@ -671,9 +706,12 @@
                                 externalCallback.onError(CALLBACK_DETECT_TIMEOUT);
                             } catch (RemoteException e) {
                                 Slog.w(TAG, "Failed to report onError status: ", e);
+                                HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                                        HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_ERROR_EXCEPTION,
+                                        mVoiceInteractionServiceUid);
                             }
                         },
-                        VALIDATION_TIMEOUT_MILLIS,
+                        MAX_VALIDATION_TIMEOUT_MILLIS,
                         TimeUnit.MILLISECONDS);
                 service.detectFromDspSource(
                         recognitionEvent,
@@ -715,6 +753,7 @@
     }
 
     private void restartProcessLocked() {
+        // TODO(b/244598068): Check HotwordAudioStreamManager first
         Slog.v(TAG, "Restarting hotword detection process");
         ServiceConnection oldConnection = mRemoteHotwordDetectionService;
         HotwordDetectionServiceIdentity previousIdentity = mIdentity;
@@ -730,6 +769,9 @@
                         HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to call #rejected");
+                HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                        HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_REJECTED_EXCEPTION,
+                        mVoiceInteractionServiceUid);
             }
             mValidatingDspTrigger = false;
         }
@@ -745,6 +787,9 @@
             mCallback.onProcessRestarted();
         } catch (RemoteException e) {
             Slog.w(TAG, "Failed to communicate #onProcessRestarted", e);
+            HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                    HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_PROCESS_RESTARTED_EXCEPTION,
+                    mVoiceInteractionServiceUid);
         }
 
         // Restart listening from microphone if the hotword process has been restarted.
@@ -884,6 +929,9 @@
                     callback.onError();
                 } catch (RemoteException ex) {
                     Slog.w(TAG, "Failed to report onError status: " + ex);
+                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                            HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_ERROR_EXCEPTION,
+                            mVoiceInteractionServiceUid);
                 }
             } finally {
                 synchronized (mLock) {
@@ -949,16 +997,24 @@
                                         callback.onError();
                                         return;
                                     }
-                                    callback.onDetected(triggerResult, null /* audioFormat */,
+                                    HotwordDetectedResult newResult;
+                                    try {
+                                        newResult =
+                                                mHotwordAudioStreamManager.startCopyingAudioStreams(
+                                                        triggerResult);
+                                    } catch (IOException e) {
+                                        // TODO: Write event
+                                        callback.onError();
+                                        return;
+                                    }
+                                    callback.onDetected(newResult, null /* audioFormat */,
                                             null /* audioStream */);
-                                    if (triggerResult != null) {
-                                        Slog.i(TAG, "Egressed "
-                                                + HotwordDetectedResult.getUsageSize(triggerResult)
-                                                + " bits from hotword trusted process");
-                                        if (mDebugHotwordLogging) {
-                                            Slog.i(TAG,
-                                                    "Egressed detected result: " + triggerResult);
-                                        }
+                                    Slog.i(TAG, "Egressed "
+                                            + HotwordDetectedResult.getUsageSize(newResult)
+                                            + " bits from hotword trusted process");
+                                    if (mDebugHotwordLogging) {
+                                        Slog.i(TAG,
+                                                "Egressed detected result: " + newResult);
                                     }
                                 }
                             });
@@ -1063,6 +1119,9 @@
                     mCallback.onError(HOTWORD_DETECTION_SERVICE_DIED);
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to report onError status: " + e);
+                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                            HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_ON_ERROR_EXCEPTION,
+                            mVoiceInteractionServiceUid);
                 }
             }
         }
@@ -1182,15 +1241,15 @@
         });
     }
 
-    private void saveProximityMetersToBundle(HotwordDetectedResult result) {
+    private void saveProximityValueToBundle(HotwordDetectedResult result) {
         synchronized (mLock) {
             if (result != null && mProximityMeters != PROXIMITY_UNKNOWN) {
-                result.getExtras().putDouble(EXTRA_PROXIMITY_METERS, mProximityMeters);
+                result.setProximity(mProximityMeters);
             }
         }
     }
 
-    private void setProximityMeters(double proximityMeters) {
+    private void setProximityValue(double proximityMeters) {
         synchronized (mLock) {
             mProximityMeters = proximityMeters;
         }
@@ -1217,7 +1276,7 @@
         Binder.withCleanCallingIdentity(() -> {
             enforcePermissionForPreflight(mContext, mVoiceInteractorIdentity, RECORD_AUDIO);
             int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
-            mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(hotwordOp,
+            mAppOpsManager.noteOpNoThrow(hotwordOp,
                     mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
                     mVoiceInteractorIdentity.attributionTag, OP_MESSAGE);
             enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
@@ -1262,4 +1321,4 @@
 
     private static final String OP_MESSAGE =
             "Providing hotword detection result to VoiceInteractionService";
-};
+}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 151ff80..7207e373 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -1174,6 +1174,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public void setDisabled(boolean disabled) {
+            super.setDisabled_enforcePermission();
+
             synchronized (this) {
                 if (mTemporarilyDisabled == disabled) {
                     if (DEBUG) Slog.d(TAG, "setDisabled(): already " + disabled);
@@ -1244,6 +1246,8 @@
         public void updateState(
                 @Nullable PersistableBundle options,
                 @Nullable SharedMemory sharedMemory) {
+            super.updateState_enforcePermission();
+
             synchronized (this) {
                 enforceIsCurrentVoiceInteractionService();
 
@@ -1260,6 +1264,8 @@
                 @Nullable SharedMemory sharedMemory,
                 IHotwordRecognitionStatusCallback callback,
                 int detectorType) {
+            super.initAndVerifyDetector_enforcePermission();
+
             synchronized (this) {
                 enforceIsCurrentVoiceInteractionService();
 
@@ -1711,6 +1717,8 @@
                 @Nullable String attributionTag,
                 @Nullable IVoiceInteractionSessionShowCallback showCallback,
                 @Nullable IBinder activityToken) {
+            super.showSessionForActiveService_enforcePermission();
+
             if (DEBUG_USER) Slog.d(TAG, "showSessionForActiveService()");
 
             synchronized (this) {
@@ -1742,6 +1750,8 @@
         @Override
         public void hideCurrentSession() throws RemoteException {
 
+            super.hideCurrentSession_enforcePermission();
+
             if (mImpl == null) {
                 return;
             }
@@ -1762,6 +1772,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public void launchVoiceAssistFromKeyguard() {
+            super.launchVoiceAssistFromKeyguard_enforcePermission();
+
             synchronized (this) {
                 if (mImpl == null) {
                     Slog.w(TAG, "launchVoiceAssistFromKeyguard without running voice interaction"
@@ -1780,6 +1792,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public boolean isSessionRunning() {
+            super.isSessionRunning_enforcePermission();
+
             synchronized (this) {
                 return mImpl != null && mImpl.mActiveSession != null;
             }
@@ -1788,6 +1802,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public boolean activeServiceSupportsAssist() {
+            super.activeServiceSupportsAssist_enforcePermission();
+
             synchronized (this) {
                 return mImpl != null && mImpl.mInfo != null && mImpl.mInfo.getSupportsAssist();
             }
@@ -1796,6 +1812,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public boolean activeServiceSupportsLaunchFromKeyguard() throws RemoteException {
+            super.activeServiceSupportsLaunchFromKeyguard_enforcePermission();
+
             synchronized (this) {
                 return mImpl != null && mImpl.mInfo != null
                         && mImpl.mInfo.getSupportsLaunchFromKeyguard();
@@ -1805,6 +1823,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE)
         @Override
         public void onLockscreenShown() {
+            super.onLockscreenShown_enforcePermission();
+
             synchronized (this) {
                 if (mImpl == null) {
                     return;
@@ -1828,6 +1848,8 @@
         @Override
         public void registerVoiceInteractionSessionListener(
                 IVoiceInteractionSessionListener listener) {
+            super.registerVoiceInteractionSessionListener_enforcePermission();
+
             synchronized (this) {
                 mVoiceInteractionSessionListeners.register(listener);
             }
@@ -1837,6 +1859,8 @@
         @Override
         public void getActiveServiceSupportedActions(List<String> voiceActions,
                 IVoiceActionCheckCallback callback) {
+            super.getActiveServiceSupportedActions_enforcePermission();
+
             synchronized (this) {
                 if (mImpl == null) {
                     try {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 5d1901d..0a660b0 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -639,7 +639,7 @@
     private void logDetectorCreateEventIfNeeded(IHotwordRecognitionStatusCallback callback,
             int detectorType, boolean isCreated, int voiceInteractionServiceUid) {
         if (callback != null) {
-            HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, true,
+            HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, isCreated,
                     voiceInteractionServiceUid);
         }
 
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index f8bc499..763024f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -913,4 +913,4 @@
             }
         }
     };
-};
+}
diff --git a/telecomm/OWNERS b/telecomm/OWNERS
index eb0c432..dcaf858 100644
--- a/telecomm/OWNERS
+++ b/telecomm/OWNERS
@@ -4,3 +4,7 @@
 tgunn@google.com
 xiaotonj@google.com
 rgreenwalt@google.com
+chinmayd@google.com
+grantmenke@google.com
+pmadapurmath@google.com
+tjstuart@google.com
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 432af3a..5cef2cb 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -168,6 +168,18 @@
     public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
 
     /**
+     * Extra key intended for {@link InCallService}s that notify the user of an incoming call. When
+     * EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB returns true, the {@link InCallService} should not
+     * interrupt the user of the incoming call because the call is being suppressed by Do Not
+     * Disturb settings.
+     *
+     * This extra will be removed from the {@link Call} object for {@link InCallService}s that do
+     * not hold the {@link android.Manifest.permission#READ_CONTACTS} permission.
+     */
+    public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB =
+            "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
+
+    /**
      * Key for extra used to pass along a list of {@link PhoneAccountSuggestion}s to the in-call
      * UI when a call enters the {@link #STATE_SELECT_PHONE_ACCOUNT} state. The list included here
      * will have the same length and be in the same order as the list passed with
diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.java b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
index ff87ab0..a69dfb0b 100644
--- a/telecomm/java/android/telecom/ParcelableCallAnalytics.java
+++ b/telecomm/java/android/telecom/ParcelableCallAnalytics.java
@@ -111,6 +111,8 @@
         public static final int FILTERING_INITIATED = 106;
         public static final int FILTERING_COMPLETED = 107;
         public static final int FILTERING_TIMED_OUT = 108;
+        public static final int DND_CHECK_INITIATED = 109;
+        public static final int DND_CHECK_COMPLETED = 110;
 
         public static final int SKIP_RINGING = 200;
         public static final int SILENCE = 201;
@@ -195,6 +197,7 @@
         public static final int BLOCK_CHECK_FINISHED_TIMING = 9;
         public static final int FILTERING_COMPLETED_TIMING = 10;
         public static final int FILTERING_TIMED_OUT_TIMING = 11;
+        public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12;
         /** {@hide} */
         public static final int START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING = 12;
 
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index ccec67e..af37ed5 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1851,7 +1851,7 @@
         ITelecomService service = getTelecomService();
         if (service != null) {
             try {
-                return service.getCallStateUsingPackage(mContext.getPackageName(),
+                return service.getCallStateUsingPackage(mContext.getOpPackageName(),
                         mContext.getAttributionTag());
             } catch (RemoteException e) {
                 Log.d(TAG, "RemoteException calling getCallState().", e);
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index 5179bab..76d2b7d 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -206,6 +206,8 @@
                 return "DATA_ON_NON_DEFAULT_DURING_VOICE_CALL";
             case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
                 return "MMS_ALWAYS_ALLOWED";
+            case TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH:
+                return "AUTO_DATA_SWITCH";
             default:
                 return "UNKNOWN(" + mobileDataPolicy + ")";
         }
diff --git a/telephony/common/com/google/android/mms/pdu/EncodedStringValue.java b/telephony/common/com/google/android/mms/pdu/EncodedStringValue.java
index 8b01cb3..2787d83 100644
--- a/telephony/common/com/google/android/mms/pdu/EncodedStringValue.java
+++ b/telephony/common/com/google/android/mms/pdu/EncodedStringValue.java
@@ -199,7 +199,6 @@
      */
     @Override
     public Object clone() throws CloneNotSupportedException {
-        super.clone();
         int len = mData.length;
         byte[] dstBytes = new byte[len];
         System.arraycopy(mData, 0, dstBytes, 0, len);
diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java
index dc695d6..e19117b 100644
--- a/telephony/java/android/service/euicc/EuiccService.java
+++ b/telephony/java/android/service/euicc/EuiccService.java
@@ -730,6 +730,25 @@
     }
 
     /**
+     * Result code to string
+     *
+     * @param result The result code.
+     * @return The result code in string format.
+     *
+     * @hide
+     */
+    public static String resultToString(@Result int result) {
+        switch (result) {
+            case RESULT_OK: return "OK";
+            case RESULT_MUST_DEACTIVATE_SIM : return "MUST_DEACTIVATE_SIM";
+            case RESULT_RESOLVABLE_ERRORS: return "RESOLVABLE_ERRORS";
+            case RESULT_FIRST_USER: return "FIRST_USER";
+            default:
+            return "UNKNOWN(" + result + ")";
+        }
+    }
+
+    /**
      * Wrapper around IEuiccService that forwards calls to implementations of {@link EuiccService}.
      */
     private class IEuiccServiceWrapper extends IEuiccService.Stub {
diff --git a/telephony/java/android/service/euicc/GetEuiccProfileInfoListResult.java b/telephony/java/android/service/euicc/GetEuiccProfileInfoListResult.java
index 9add38e..46a049c 100644
--- a/telephony/java/android/service/euicc/GetEuiccProfileInfoListResult.java
+++ b/telephony/java/android/service/euicc/GetEuiccProfileInfoListResult.java
@@ -123,4 +123,16 @@
     public int describeContents() {
         return 0;
     }
+
+    /**
+     * @hide
+     *
+     * @return String representation of {@link GetEuiccProfileInfoListResult}
+     */
+    @Override
+    public String toString() {
+        return "[GetEuiccProfileInfoListResult: result=" + EuiccService.resultToString(result)
+                + ", isRemovable=" + mIsRemovable + ", mProfiles=" + Arrays.toString(mProfiles)
+                + "]";
+    }
 }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f43f0a5..ed96a9b 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1147,8 +1147,12 @@
             "carrier_data_call_apn_retry_after_disconnect_long";
 
     /**
-     * Data call setup permanent failure causes by the carrier
+     * Data call setup permanent failure causes by the carrier.
+     *
+     * @deprecated This API key was added in mistake and is not used anymore by the telephony data
+     * frameworks.
      */
+    @Deprecated
     public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS =
             "carrier_data_call_permanent_failure_strings";
 
@@ -2103,6 +2107,16 @@
      * is immediately closed (disabling keep-alive).
      */
     public static final String KEY_MMS_CLOSE_CONNECTION_BOOL = "mmsCloseConnection";
+    /**
+     * Waiting time in milliseconds used before releasing an MMS data call. Not tearing down an MMS
+     * data connection immediately helps to reduce the message delivering latency if messaging
+     * continues between all parties in the conversation since the same data connection can be
+     * reused for further messages.
+     *
+     * This timer will control how long the data call will be kept alive before being torn down.
+     */
+    public static final String KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT =
+            "mms_network_release_timeout_millis_int";
 
     /**
      * The flatten {@link android.content.ComponentName componentName} of the activity that can
@@ -7081,6 +7095,274 @@
         public static final String KEY_REFRESH_GEOLOCATION_TIMEOUT_MILLIS_INT =
                 KEY_PREFIX + "refresh_geolocation_timeout_millis_int";
 
+        /**
+         * List of 3GPP access network technologies where e911 over IMS is supported
+         * in the home network and domestic 3rd-party networks. The order in the list represents
+         * the preference. The domain selection service shall scan the network type in the order
+         * of the preference.
+         *
+         * <p>Possible values are,
+         * {@link AccessNetworkConstants.AccessNetworkType#NGRAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#EUTRAN}
+         *
+         * The default value for this key is
+         * {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN},
+         * @hide
+         */
+        public static final String
+                KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+                        + "emergency_over_ims_supported_3gpp_network_types_int_array";
+
+        /**
+         * List of 3GPP access network technologies where e911 over IMS is supported
+         * in the roaming network and non-domestic 3rd-party networks. The order in the list
+         * represents the preference. The domain selection service shall scan the network type
+         * in the order of the preference.
+         *
+         * <p>Possible values are,
+         * {@link AccessNetworkConstants.AccessNetworkType#NGRAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#EUTRAN}
+         *
+         * The default value for this key is
+         * {{@link AccessNetworkConstants.AccessNetworkType#EUTRAN},
+         * @hide
+         */
+        public static final String
+                KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+                        + "emergency_over_ims_roaming_supported_3gpp_network_types_int_array";
+
+        /**
+         * List of CS access network technologies where circuit-switched emergency calls are
+         * supported in the home network and domestic 3rd-party networks. The order in the list
+         * represents the preference. The domain selection service shall scan the network type
+         * in the order of the preference.
+         *
+         * <p>Possible values are,
+         * {@link AccessNetworkConstants.AccessNetworkType#GERAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#UTRAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#CDMA2000}
+         *
+         * The default value for this key is
+         * {{@link AccessNetworkConstants.AccessNetworkType#UTRAN},
+         * {@link AccessNetworkConstants.AccessNetworkType#GERAN}}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY =
+                KEY_PREFIX + "emergency_over_cs_supported_access_network_types_int_array";
+
+        /**
+         * List of CS access network technologies where circuit-switched emergency calls are
+         * supported in the roaming network and non-domestic 3rd-party networks. The order
+         * in the list represents the preference. The domain selection service shall scan
+         * the network type in the order of the preference.
+         *
+         * <p>Possible values are,
+         * {@link AccessNetworkConstants.AccessNetworkType#GERAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#UTRAN}
+         * {@link AccessNetworkConstants.AccessNetworkType#CDMA2000}
+         *
+         * The default value for this key is
+         * {{@link AccessNetworkConstants.AccessNetworkType#UTRAN},
+         * {@link AccessNetworkConstants.AccessNetworkType#GERAN}}.
+         * @hide
+         */
+        public static final String
+                KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY = KEY_PREFIX
+                        + "emergency_over_cs_roaming_supported_access_network_types_int_array";
+
+        /** @hide */
+        @IntDef({
+            DOMAIN_CS,
+            DOMAIN_PS_3GPP,
+            DOMAIN_PS_NON_3GPP
+        })
+        public @interface EmergencyDomain {}
+
+        /**
+         * Circuit switched domain.
+         * @hide
+         */
+        public static final int DOMAIN_CS = 1;
+
+        /**
+         * Packet switched domain over 3GPP networks.
+         * @hide
+         */
+        public static final int DOMAIN_PS_3GPP = 2;
+
+        /**
+         * Packet switched domain over non-3GPP networks such as Wi-Fi.
+         * @hide
+         */
+        public static final int DOMAIN_PS_NON_3GPP = 3;
+
+        /**
+         * Specifies the emergency call domain preference for the home network.
+         * The domain selection service shall choose the domain in the order
+         * for attempting the emergency call
+         *
+         * <p>Possible values are,
+         * {@link #DOMAIN_CS}
+         * {@link #DOMAIN_PS_3GPP}
+         * {@link #DOMAIN_PS_NON_3GPP}.
+         *
+         * The default value for this key is
+         * {{@link #DOMAIN_PS_3GPP},
+         * {@link #DOMAIN_CS},
+         * {@link #DOMAIN_PS_NON_3GPP}}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY =
+                KEY_PREFIX + "emergency_domain_preference_int_array";
+
+        /**
+         * Specifies the emergency call domain preference for the roaming network.
+         * The domain selection service shall choose the domain in the order
+         * for attempting the emergency call.
+         *
+         * <p>Possible values are,
+         * {@link #DOMAIN_CS}
+         * {@link #DOMAIN_PS_3GPP}
+         * {@link #DOMAIN_PS_NON_3GPP}.
+         *
+         * The default value for this key is
+         * {{@link #DOMAIN_PS_3GPP},
+         * {@link #DOMAIN_CS},
+         * {@link #DOMAIN_PS_NON_3GPP}}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY =
+                KEY_PREFIX + "emergency_domain_preference_roaming_int_array";
+
+        /**
+         * Specifies if emergency call shall be attempted on IMS, if PS is attached even though IMS
+         * is not registered and normal calls fallback to the CS networks.
+         *
+         * The default value for this key is {@code false}.
+         * @hide
+         */
+        public static final String KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL =
+                KEY_PREFIX + "prefer_ims_emergency_when_voice_calls_on_cs_bool";
+
+        /**
+         * Specifies maximum number of emergency call retries over Wi-Fi.
+         * This is valid only when {@link #DOMAIN_PS_NON_3GPP} is included in
+         * {@link #KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY} or
+         * {@link #KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY}.
+         *
+         * The default value for this key is 1.
+         * @hide
+         */
+        public static final String KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT =
+                KEY_PREFIX + "maximum_number_of_emergency_tries_over_vowifi_int";
+
+        /**
+         * Emergency scan timer to wait for scan results from radio before attempting the call
+         * over Wi-Fi. On timer expiry, if emergency call on Wi-Fi is allowed and possible,
+         * telephony shall cancel the scan and place the call on Wi-Fi. If emergency call on Wi-Fi
+         * is not possible, then domain seleciton continues to wait for the scan result from the
+         * radio. If an emergency scan result is received before the timer expires, the timer shall
+         * be stopped and no dialing over Wi-Fi will be tried. If this value is set to 0, then
+         * the timer is never started and domain selection waits for the scan result from the radio.
+         *
+         * The default value for the timer is 10 seconds.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_SCAN_TIMER_SEC_INT =
+                KEY_PREFIX + "emergency_scan_timer_sec_int";
+
+        /** @hide */
+        @IntDef(prefix = "SCAN_TYPE_",
+            value = {
+                SCAN_TYPE_NO_PREFERENCE,
+                SCAN_TYPE_FULL_SERVICE,
+                SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE})
+        public @interface EmergencyScanType {}
+
+        /**
+         * No specific preference given to the modem. Modem can return an emergency
+         * capable network either with limited service or full service.
+         * @hide
+         */
+        public static final int SCAN_TYPE_NO_PREFERENCE = 0;
+
+        /**
+         * Modem will attempt to camp on a network with full service only.
+         * @hide
+         */
+        public static final int SCAN_TYPE_FULL_SERVICE = 1;
+
+        /**
+         * Telephony shall attempt full service scan first.
+         * If a full service network is not found, telephony shall attempt a limited service scan.
+         * @hide
+         */
+        public static final int SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE = 2;
+
+        /**
+         * Specifies the preferred emergency network scanning type.
+         *
+         * <p>Possible values are,
+         * {@link #SCAN_TYPE_NO_PREFERENCE}
+         * {@link #SCAN_TYPE_FULL_SERVICE}
+         * {@link #SCAN_TYPE_FULL_SERVICE_FOLLOWED_BY_LIMITED_SERVICE}
+         *
+         * The default value for this key is {@link #SCAN_TYPE_NO_PREFERENCE}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT =
+                KEY_PREFIX + "emergency_network_scan_type_int";
+
+        /**
+         * Specifies the time by which a call should be set up on the current network
+         * once the call is routed on the network. If the call cannot be set up by timer expiry,
+         * call shall be re-dialed on the next available network.
+         * If this value is set to 0, the timer shall be disabled.
+         *
+         * The default value for this key is 0.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT =
+                KEY_PREFIX + "emergency_call_setup_timer_on_current_network_sec_int";
+
+        /**
+         * Specifies if emergency call shall be attempted on IMS only when IMS is registered.
+         * This is applicable only for the case PS is in service.
+         *
+         * The default value for this key is {@code false}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL =
+                KEY_PREFIX + "emergency_requires_ims_registration_bool";
+
+        /**
+         * Specifies if LTE is preferred when re-scanning networks after the failure of dialing
+         * over NR. If not, CS will be preferred.
+         *
+         * The default value for this key is {@code false}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL =
+                KEY_PREFIX + "emergency_lte_preferred_after_nr_failed_bool";
+
+        /**
+         * Specifies the numbers to be dialed over CDMA network in case of dialing over CS network.
+         *
+         * The default value for this key is an empty string array.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY =
+                KEY_PREFIX + "emergency_cdma_preferred_numbers_string_array";
+
+        /**
+         * Specifies if emergency call shall be attempted on IMS only when VoLTE is enabled.
+         *
+         * The default value for this key is {@code false}.
+         * @hide
+         */
+        public static final String KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL =
+                KEY_PREFIX + "emergency_requires_volte_enabled_bool";
+
         private static PersistableBundle getDefaults() {
             PersistableBundle defaults = new PersistableBundle();
             defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false);
@@ -7097,6 +7379,56 @@
             defaults.putInt(KEY_EMERGENCY_REGISTRATION_TIMER_MILLIS_INT, 10000);
             defaults.putInt(KEY_REFRESH_GEOLOCATION_TIMEOUT_MILLIS_INT, 5000);
 
+            defaults.putIntArray(
+                    KEY_EMERGENCY_OVER_IMS_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+                    new int[] {
+                        AccessNetworkType.EUTRAN,
+                    });
+
+            defaults.putIntArray(
+                    KEY_EMERGENCY_OVER_IMS_ROAMING_SUPPORTED_3GPP_NETWORK_TYPES_INT_ARRAY,
+                    new int[] {
+                        AccessNetworkType.EUTRAN,
+                    });
+
+            defaults.putIntArray(
+                    KEY_EMERGENCY_OVER_CS_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+                    new int[] {
+                        AccessNetworkType.UTRAN,
+                        AccessNetworkType.GERAN,
+                    });
+
+            defaults.putIntArray(
+                    KEY_EMERGENCY_OVER_CS_ROAMING_SUPPORTED_ACCESS_NETWORK_TYPES_INT_ARRAY,
+                    new int[] {
+                        AccessNetworkType.UTRAN,
+                        AccessNetworkType.GERAN,
+                    });
+
+            defaults.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_INT_ARRAY,
+                    new int[] {
+                        DOMAIN_PS_3GPP,
+                        DOMAIN_CS,
+                        DOMAIN_PS_NON_3GPP
+                    });
+            defaults.putIntArray(KEY_EMERGENCY_DOMAIN_PREFERENCE_ROAMING_INT_ARRAY,
+                    new int[] {
+                        DOMAIN_PS_3GPP,
+                        DOMAIN_CS,
+                        DOMAIN_PS_NON_3GPP
+                    });
+
+            defaults.putBoolean(KEY_PREFER_IMS_EMERGENCY_WHEN_VOICE_CALLS_ON_CS_BOOL, false);
+            defaults.putInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT, 1);
+            defaults.putInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT, 10);
+            defaults.putInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT, SCAN_TYPE_NO_PREFERENCE);
+            defaults.putInt(KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT, 0);
+            defaults.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, false);
+            defaults.putBoolean(KEY_EMERGENCY_LTE_PREFERRED_AFTER_NR_FAILED_BOOL, false);
+            defaults.putBoolean(KEY_EMERGENCY_REQUIRES_VOLTE_ENABLED_BOOL, false);
+            defaults.putStringArray(KEY_EMERGENCY_CDMA_PREFERRED_NUMBERS_STRING_ARRAY,
+                    new String[] {});
+
             return defaults;
         }
     }
@@ -7906,7 +8238,8 @@
                     KEY_XCAP_OVER_UT_SUPPORTED_RATS_INT_ARRAY,
                     new int[] {
                         AccessNetworkType.EUTRAN,
-                        AccessNetworkType.IWLAN
+                        AccessNetworkType.IWLAN,
+                        AccessNetworkType.NGRAN
                     });
             defaults.putString(KEY_UT_AS_SERVER_FQDN_STRING, "");
             defaults.putBoolean(KEY_TERMINAL_BASED_CALL_WAITING_DEFAULT_ENABLED_BOOL, true);
@@ -8436,7 +8769,8 @@
      *
      * The syntax of the retry rule:
      * 1. Retry based on {@link NetworkCapabilities}. Note that only APN-type network capabilities
-     *    are supported.
+     *    are supported. If the capabilities are not specified, then the retry rule only applies
+     *    to the current failed APN used in setup data call request.
      * "capabilities=[netCaps1|netCaps2|...], [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]"
      *
      * 2. Retry based on {@link DataFailCause}
@@ -8447,15 +8781,16 @@
      * "capabilities=[netCaps1|netCaps2|...], fail_causes=[cause1|cause2|cause3|...],
      *     [retry_interval=n1|n2|n3|n4...], [maximum_retries=n]"
      *
+     * 4. Permanent fail causes (no timer-based retry) on the current failed APN. Retry interval
+     *    is specified for retrying the next available APN.
+     * "permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|65543|65547|
+     *     2252|2253|2254, retry_interval=2500"
+     *
      * For example,
      * "capabilities=eims, retry_interval=1000, maximum_retries=20" means if the attached
      * network request is emergency, then retry data network setup every 1 second for up to 20
      * times.
      *
-     * "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2253|2254
-     * , maximum_retries=0" means for those fail causes, never retry with timers. Note that
-     * when environment changes, retry can still happen.
-     *
      * "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|"
      * "5000|10000|15000|20000|40000|60000|120000|240000|600000|1200000|1800000"
      * "1800000, maximum_retries=20" means for those capabilities, retry happens in 2.5s, 3s, 5s,
@@ -8599,7 +8934,12 @@
      *
      * Used to trade privacy/security against potentially reduced carrier coverage for some
      * carriers.
+     *
+     * @deprecated Future versions of Android will disallow carriers from hiding this toggle
+     * because disabling 2g is a security feature that users should always have access to at
+     * their discretion.
      */
+    @Deprecated
     public static final String KEY_HIDE_ENABLE_2G = "hide_enable_2g_bool";
 
     /**
@@ -8658,11 +8998,12 @@
 
     /**
      * Boolean indicating if the VoNR setting is visible in the Call Settings menu.
-     * If true, the VoNR setting menu will be visible. If false, the menu will be gone.
+     * If this flag is set and VoNR is enabled for this carrier (see {@link #KEY_VONR_ENABLED_BOOL})
+     * the VoNR setting menu will be visible. If {@link #KEY_VONR_ENABLED_BOOL} or
+     * this setting is false, the menu will be gone.
      *
-     * Disabled by default.
+     * Enabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
 
@@ -8672,11 +9013,19 @@
      *
      * Disabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
 
     /**
+     * Boolean indicating the default VoNR user preference setting.
+     * If true, the VoNR setting will be enabled. If false, it will be disabled initially.
+     *
+     * Enabled by default.
+     *
+     */
+    public static final String KEY_VONR_ON_BY_DEFAULT_BOOL = "vonr_on_by_default_bool";
+
+    /**
      * Determine whether unthrottle data retry when tracking area code (TAC/LAC) from cell changes
      *
      * @hide
@@ -8715,6 +9064,9 @@
      * premium capabilities should be blocked when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
+     * The maximum number of network boost notifications to show the user are defined in
+     * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT} and
+     * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT}.
      *
      * The default value is 30 minutes.
      *
@@ -8726,6 +9078,34 @@
             "premium_capability_notification_backoff_hysteresis_time_millis_long";
 
     /**
+     * The maximum number of times in a day that we display the notification for a network boost
+     * via premium capabilities when
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * returns a failure due to user action or timeout.
+     *
+     * The default value is 2 times.
+     *
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT =
+            "premium_capability_maximum_daily_notification_count_int";
+
+    /**
+     * The maximum number of times in a month that we display the notification for a network boost
+     * via premium capabilities when
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * returns a failure due to user action or timeout.
+     *
+     * The default value is 10 times.
+     *
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT =
+            "premium_capability_maximum_monthly_notification_count_int";
+
+    /**
      * The amount of time in milliseconds that the purchase request should be throttled when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to the carrier.
@@ -8733,13 +9113,29 @@
      * The default value is 30 minutes.
      *
      * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR
-     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED
      */
     public static final String
             KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG =
             "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
 
     /**
+     * The amount of time in milliseconds within which the network must set up a slicing
+     * configuration for the premium capability after
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * returns {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS}.
+     * During the setup time, calls to
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} will return
+     * {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}.
+     * If the network fails to set up a slicing configuration for the premium capability within the
+     * setup time, subsequent purchase requests will be allowed to go through again.
+     *
+     * The default value is 5 minutes.
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG =
+            "premium_capability_network_setup_time_millis_long";
+
+    /**
      * The URL to redirect to when the user clicks on the notification for a network boost via
      * premium capabilities after applications call
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}.
@@ -8752,6 +9148,20 @@
             "premium_capability_purchase_url_string";
 
     /**
+     * Whether to allow premium capabilities to be purchased when the device is connected to LTE.
+     * If this is {@code true}, applications can call
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * when connected to {@link TelephonyManager#NETWORK_TYPE_LTE} to purchase and use
+     * premium capabilities.
+     * If this is {@code false}, applications can only purchase and use premium capabilities when
+     * connected to {@link TelephonyManager#NETWORK_TYPE_NR}.
+     *
+     * This is {@code false} by default.
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL =
+            "premium_capability_supported_on_lte_bool";
+
+    /**
      * IWLAN handover rules that determine whether handover is allowed or disallowed between
      * cellular and IWLAN.
      *
@@ -9037,6 +9447,7 @@
         sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_INT, -1);
         sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_THRESHOLD_INT, -1);
         sDefaults.putInt(KEY_MMS_SUBJECT_MAX_LENGTH_INT, 40);
+        sDefaults.putInt(KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT, 5 * 1000);
         sDefaults.putString(KEY_MMS_EMAIL_GATEWAY_NUMBER_STRING, "");
         sDefaults.putString(KEY_MMS_HTTP_PARAMS_STRING, "");
         sDefaults.putString(KEY_MMS_NAI_SUFFIX_STRING, "");
@@ -9400,8 +9811,13 @@
         sDefaults.putStringArray(
                 KEY_TELEPHONY_DATA_SETUP_RETRY_RULES_STRING_ARRAY, new String[] {
                         "capabilities=eims, retry_interval=1000, maximum_retries=20",
-                        "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2252|"
-                                + "2253|2254, maximum_retries=0", // No retry for those causes
+                        // Permanent fail causes. When setup data call fails with the following
+                        // fail causes, telephony data frameworks will stop timer-based retry on
+                        // the failed APN until power cycle, APM, or some special events. Note that
+                        // even timer-based retry is not performed, condition-based (RAT changes,
+                        // registration state changes) retry can still happen.
+                        "permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|"
+                                + "-3|65543|65547|2252|2253|2254, retry_interval=2500",
                         "capabilities=mms|supl|cbs, retry_interval=2000",
                         "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|"
                                 + "5000|10000|15000|20000|40000|60000|120000|240000|"
@@ -9432,15 +9848,21 @@
         sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false);
         sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false);
-        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[]{});
+        sDefaults.putBoolean(KEY_VONR_ON_BY_DEFAULT_BOOL, true);
+        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {});
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
+        sDefaults.putInt(KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT, 2);
+        sDefaults.putInt(KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT, 10);
         sDefaults.putLong(
                 KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
+        sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG,
+                TimeUnit.MINUTES.toMillis(5));
         sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null);
+        sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false);
         sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
                 "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, "
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
diff --git a/telephony/java/android/telephony/CellIdentity.java b/telephony/java/android/telephony/CellIdentity.java
index 06cfd67..6e3cfac 100644
--- a/telephony/java/android/telephony/CellIdentity.java
+++ b/telephony/java/android/telephony/CellIdentity.java
@@ -107,7 +107,7 @@
 
         if ((mMccStr != null && mMncStr == null) || (mMccStr == null && mMncStr != null)) {
             AnomalyReporter.reportAnomaly(
-                    UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
+                    UUID.fromString("e257ae06-ac0a-44c0-ba63-823b9f07b3e4"),
                     "CellIdentity Missing Half of PLMN ID");
         }
 
diff --git a/telephony/java/android/telephony/DataFailCause.java b/telephony/java/android/telephony/DataFailCause.java
index 23835a7..b83b400 100644
--- a/telephony/java/android/telephony/DataFailCause.java
+++ b/telephony/java/android/telephony/DataFailCause.java
@@ -1620,29 +1620,26 @@
                 // If we are not able to find the configuration from carrier config, use the default
                 // ones.
                 if (permanentFailureSet == null) {
-                    permanentFailureSet = new HashSet<Integer>() {
-                        {
-                            add(OPERATOR_BARRED);
-                            add(MISSING_UNKNOWN_APN);
-                            add(UNKNOWN_PDP_ADDRESS_TYPE);
-                            add(USER_AUTHENTICATION);
-                            add(ACTIVATION_REJECT_GGSN);
-                            add(SERVICE_OPTION_NOT_SUPPORTED);
-                            add(SERVICE_OPTION_NOT_SUBSCRIBED);
-                            add(NSAPI_IN_USE);
-                            add(ONLY_IPV4_ALLOWED);
-                            add(ONLY_IPV6_ALLOWED);
-                            add(PROTOCOL_ERRORS);
-                            add(RADIO_POWER_OFF);
-                            add(TETHERED_CALL_ACTIVE);
-                            add(RADIO_NOT_AVAILABLE);
-                            add(UNACCEPTABLE_NETWORK_PARAMETER);
-                            add(SIGNAL_LOST);
-                            add(DUPLICATE_CID);
-                            add(MATCH_ALL_RULE_NOT_ALLOWED);
-                            add(ALL_MATCHING_RULES_FAILED);
-                        }
-                    };
+                    permanentFailureSet = new HashSet<Integer>();
+                    permanentFailureSet.add(OPERATOR_BARRED);
+                    permanentFailureSet.add(MISSING_UNKNOWN_APN);
+                    permanentFailureSet.add(UNKNOWN_PDP_ADDRESS_TYPE);
+                    permanentFailureSet.add(USER_AUTHENTICATION);
+                    permanentFailureSet.add(ACTIVATION_REJECT_GGSN);
+                    permanentFailureSet.add(SERVICE_OPTION_NOT_SUPPORTED);
+                    permanentFailureSet.add(SERVICE_OPTION_NOT_SUBSCRIBED);
+                    permanentFailureSet.add(NSAPI_IN_USE);
+                    permanentFailureSet.add(ONLY_IPV4_ALLOWED);
+                    permanentFailureSet.add(ONLY_IPV6_ALLOWED);
+                    permanentFailureSet.add(PROTOCOL_ERRORS);
+                    permanentFailureSet.add(RADIO_POWER_OFF);
+                    permanentFailureSet.add(TETHERED_CALL_ACTIVE);
+                    permanentFailureSet.add(RADIO_NOT_AVAILABLE);
+                    permanentFailureSet.add(UNACCEPTABLE_NETWORK_PARAMETER);
+                    permanentFailureSet.add(SIGNAL_LOST);
+                    permanentFailureSet.add(DUPLICATE_CID);
+                    permanentFailureSet.add(MATCH_ALL_RULE_NOT_ALLOWED);
+                    permanentFailureSet.add(ALL_MATCHING_RULES_FAILED);
                 }
 
                 permanentFailureSet.add(NO_RETRY_FAILURE);
diff --git a/telephony/java/android/telephony/DomainSelectionService.aidl b/telephony/java/android/telephony/DomainSelectionService.aidl
new file mode 100644
index 0000000..b9d2ba8
--- /dev/null
+++ b/telephony/java/android/telephony/DomainSelectionService.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony;
+
+parcelable DomainSelectionService.SelectionAttributes;
diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java
new file mode 100644
index 0000000..c352f2b
--- /dev/null
+++ b/telephony/java/android/telephony/DomainSelectionService.java
@@ -0,0 +1,863 @@
+/*
+ * 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.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.telephony.Annotation.DisconnectCauses;
+import android.telephony.Annotation.PreciseDisconnectCauses;
+import android.telephony.ims.ImsReasonInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.IDomainSelectionServiceController;
+import com.android.internal.telephony.IDomainSelector;
+import com.android.internal.telephony.ITransportSelectorCallback;
+import com.android.internal.telephony.ITransportSelectorResultCallback;
+import com.android.internal.telephony.IWwanSelectorCallback;
+import com.android.internal.telephony.IWwanSelectorResultCallback;
+import com.android.internal.telephony.util.TelephonyUtils;
+import com.android.telephony.Rlog;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Main domain selection implementation for various telephony features.
+ *
+ * The telephony framework will bind to the {@link DomainSelectionService}.
+ *
+ * @hide
+ */
+public class DomainSelectionService extends Service {
+
+    private static final String LOG_TAG = "DomainSelectionService";
+
+    /**
+     * The intent that must be defined as an intent-filter in the AndroidManifest of the
+     * {@link DomainSelectionService}.
+     *
+     * @hide
+     */
+    public static final String SERVICE_INTERFACE = "android.telephony.DomainSelectionService";
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "SELECTOR_TYPE_",
+            value = {
+                    SELECTOR_TYPE_CALLING,
+                    SELECTOR_TYPE_SMS,
+                    SELECTOR_TYPE_UT})
+    public @interface SelectorType {}
+
+    /** Indicates the domain selector type for calling. */
+    public static final int SELECTOR_TYPE_CALLING = 1;
+    /** Indicates the domain selector type for sms. */
+    public static final int SELECTOR_TYPE_SMS = 2;
+    /** Indicates the domain selector type for supplementary services. */
+    public static final int SELECTOR_TYPE_UT = 3;
+
+    /** Indicates that the modem can scan for emergency service as per modem’s implementation. */
+    public static final int SCAN_TYPE_NO_PREFERENCE = 0;
+
+    /** Indicates that the modem will scan for emergency service in limited service mode. */
+    public static final int SCAN_TYPE_LIMITED_SERVICE = 1;
+
+    /** Indicates that the modem will scan for emergency service in full service mode. */
+    public static final int SCAN_TYPE_FULL_SERVICE = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "SCAN_TYPE_",
+            value = {
+                    SCAN_TYPE_NO_PREFERENCE,
+                    SCAN_TYPE_LIMITED_SERVICE,
+                    SCAN_TYPE_FULL_SERVICE})
+    public @interface EmergencyScanType {}
+
+    /**
+     * Contains attributes required to determine the domain for a telephony service.
+     */
+    public static final class SelectionAttributes implements Parcelable {
+
+        private static final String TAG = "SelectionAttributes";
+
+        private int mSlotId;
+        private int mSubId;
+        private @Nullable String mCallId;
+        private @Nullable String mNumber;
+        private @SelectorType int mSelectorType;
+        private boolean mIsVideoCall;
+        private boolean mIsEmergency;
+        private boolean mIsExitedFromAirplaneMode;
+        //private @Nullable UtAttributes mUtAttributes;
+        private @Nullable ImsReasonInfo mImsReasonInfo;
+        private @PreciseDisconnectCauses int mCause;
+        private @Nullable EmergencyRegResult mEmergencyRegResult;
+
+        /**
+         * @param slotId The slot identifier.
+         * @param subId The subscription identifier.
+         * @param callId The call identifier.
+         * @param number The dialed number.
+         * @param selectorType Indicates the requested domain selector type.
+         * @param video Indicates it's a video call.
+         * @param emergency Indicates it's emergency service.
+         * @param exited {@code true} if the request caused the device to move out of airplane mode.
+         * @param imsReasonInfo The reason why the last PS attempt failed.
+         * @param cause The reason why the last CS attempt failed.
+         * @param regResult The current registration result for emergency services.
+         */
+        private SelectionAttributes(int slotId, int subId, @Nullable String callId,
+                @Nullable String number, @SelectorType int selectorType,
+                boolean video, boolean emergency, boolean exited,
+                /*UtAttributes attr,*/
+                @Nullable ImsReasonInfo imsReasonInfo, @PreciseDisconnectCauses int cause,
+                @Nullable EmergencyRegResult regResult) {
+            mSlotId = slotId;
+            mSubId = subId;
+            mCallId = callId;
+            mNumber = number;
+            mSelectorType = selectorType;
+            mIsVideoCall = video;
+            mIsEmergency = emergency;
+            mIsExitedFromAirplaneMode = exited;
+            //mUtAttributes = attr;
+            mImsReasonInfo = imsReasonInfo;
+            mCause = cause;
+            mEmergencyRegResult = regResult;
+        }
+
+        /**
+         * Copy constructor.
+         *
+         * @param s Source selection attributes.
+         * @hide
+         */
+        public SelectionAttributes(@NonNull SelectionAttributes s) {
+            mSlotId = s.mSlotId;
+            mSubId = s.mSubId;
+            mCallId = s.mCallId;
+            mNumber = s.mNumber;
+            mSelectorType = s.mSelectorType;
+            mIsEmergency = s.mIsEmergency;
+            mIsExitedFromAirplaneMode = s.mIsExitedFromAirplaneMode;
+            //mUtAttributes = s.mUtAttributes;
+            mImsReasonInfo = s.mImsReasonInfo;
+            mCause = s.mCause;
+            mEmergencyRegResult = s.mEmergencyRegResult;
+        }
+
+        /**
+         * Constructs a SelectionAttributes object from the given parcel.
+         */
+        private SelectionAttributes(@NonNull Parcel in) {
+            readFromParcel(in);
+        }
+
+        /**
+         * @return The slot identifier.
+         */
+        public int getSlotId() {
+            return mSlotId;
+        }
+
+        /**
+         * @return The subscription identifier.
+         */
+        public int getSubId() {
+            return mSubId;
+        }
+
+        /**
+         * @return The call identifier.
+         */
+        public @Nullable String getCallId() {
+            return mCallId;
+        }
+
+        /**
+         * @return The dialed number.
+         */
+        public @Nullable String getNumber() {
+            return mNumber;
+        }
+
+        /**
+         * @return The domain selector type.
+         */
+        public @SelectorType int getSelectorType() {
+            return mSelectorType;
+        }
+
+        /**
+         * @return {@code true} if the request is for a video call.
+         */
+        public boolean isVideoCall() {
+            return mIsVideoCall;
+        }
+
+        /**
+         * @return {@code true} if the request is for emergency services.
+         */
+        public boolean isEmergency() {
+            return mIsEmergency;
+        }
+
+        /**
+         * @return {@code true} if the request caused the device to move out of airplane mode.
+         */
+        public boolean isExitedFromAirplaneMode() {
+            return mIsExitedFromAirplaneMode;
+        }
+
+        /*
+        public @Nullable UtAttributes getUtAttributes();
+            return mUtAttributes;
+        }
+        */
+
+        /**
+         * @return The PS disconnect cause if trying over PS resulted in a failure and
+         *         reselection is required.
+         */
+        public @Nullable ImsReasonInfo getPsDisconnectCause() {
+            return mImsReasonInfo;
+        }
+
+        /**
+         * @return The CS disconnect cause if trying over CS resulted in a failure and
+         *         reselection is required.
+         */
+        public @PreciseDisconnectCauses int getCsDisconnectCause() {
+            return mCause;
+        }
+
+        /**
+         * @return The current registration state of cellular network.
+         */
+        public @Nullable EmergencyRegResult getEmergencyRegResult() {
+            return mEmergencyRegResult;
+        }
+
+        @Override
+        public @NonNull String toString() {
+            return "{ slotId=" + mSlotId
+                    + ", subId=" + mSubId
+                    + ", callId=" + mCallId
+                    + ", number=" + (Build.IS_DEBUGGABLE ? mNumber : "***")
+                    + ", type=" + mSelectorType
+                    + ", videoCall=" + mIsVideoCall
+                    + ", emergency=" + mIsEmergency
+                    + ", airplaneMode=" + mIsExitedFromAirplaneMode
+                    + ", reasonInfo=" + mImsReasonInfo
+                    + ", cause=" + mCause
+                    + ", regResult=" + mEmergencyRegResult
+                    + " }";
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            SelectionAttributes that = (SelectionAttributes) o;
+            return mSlotId == that.mSlotId && mSubId == that.mSubId
+                    && TextUtils.equals(mCallId, that.mCallId)
+                    && TextUtils.equals(mNumber, that.mNumber)
+                    && mSelectorType == that.mSelectorType && mIsVideoCall == that.mIsVideoCall
+                    && mIsEmergency == that.mIsEmergency
+                    && mIsExitedFromAirplaneMode == that.mIsExitedFromAirplaneMode
+                    //&& equalsHandlesNulls(mUtAttributes, that.mUtAttributes)
+                    && equalsHandlesNulls(mImsReasonInfo, that.mImsReasonInfo)
+                    && mCause == that.mCause
+                    && equalsHandlesNulls(mEmergencyRegResult, that.mEmergencyRegResult);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mCallId, mNumber, mImsReasonInfo,
+                    mIsVideoCall, mIsEmergency, mIsExitedFromAirplaneMode, mEmergencyRegResult,
+                    mSlotId, mSubId, mSelectorType, mCause);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel out, int flags) {
+            out.writeInt(mSlotId);
+            out.writeInt(mSubId);
+            out.writeString8(mCallId);
+            out.writeString8(mNumber);
+            out.writeInt(mSelectorType);
+            out.writeBoolean(mIsVideoCall);
+            out.writeBoolean(mIsEmergency);
+            out.writeBoolean(mIsExitedFromAirplaneMode);
+            //out.writeParcelable(mUtAttributes, 0);
+            out.writeParcelable(mImsReasonInfo, 0);
+            out.writeInt(mCause);
+            out.writeParcelable(mEmergencyRegResult, 0);
+        }
+
+        private void readFromParcel(@NonNull Parcel in) {
+            mSlotId = in.readInt();
+            mSubId = in.readInt();
+            mCallId = in.readString8();
+            mNumber = in.readString8();
+            mSelectorType = in.readInt();
+            mIsVideoCall = in.readBoolean();
+            mIsEmergency = in.readBoolean();
+            mIsExitedFromAirplaneMode = in.readBoolean();
+            //mUtAttributes = s.mUtAttributes;
+            mImsReasonInfo = in.readParcelable(ImsReasonInfo.class.getClassLoader(),
+                    android.telephony.ims.ImsReasonInfo.class);
+            mCause = in.readInt();
+            mEmergencyRegResult = in.readParcelable(EmergencyRegResult.class.getClassLoader(),
+                    EmergencyRegResult.class);
+        }
+
+        public static final @NonNull Creator<SelectionAttributes> CREATOR =
+                new Creator<SelectionAttributes>() {
+            @Override
+            public SelectionAttributes createFromParcel(@NonNull Parcel in) {
+                return new SelectionAttributes(in);
+            }
+
+            @Override
+            public SelectionAttributes[] newArray(int size) {
+                return new SelectionAttributes[size];
+            }
+        };
+
+        private static boolean equalsHandlesNulls(Object a, Object b) {
+            return (a == null) ? (b == null) : a.equals(b);
+        }
+
+        /**
+         * Builder class creating a new instance.
+         */
+        public static final class Builder {
+            private final int mSlotId;
+            private final int mSubId;
+            private @Nullable String mCallId;
+            private @Nullable String mNumber;
+            private final @SelectorType int mSelectorType;
+            private boolean mIsVideoCall;
+            private boolean mIsEmergency;
+            private boolean mIsExitedFromAirplaneMode;
+            //private @Nullable UtAttributes mUtAttributes;
+            private @Nullable ImsReasonInfo mImsReasonInfo;
+            private @PreciseDisconnectCauses int mCause;
+            private @Nullable EmergencyRegResult mEmergencyRegResult;
+
+            /**
+             * Default constructor for Builder.
+             */
+            public Builder(int slotId, int subId, @SelectorType int selectorType) {
+                mSlotId = slotId;
+                mSubId = subId;
+                mSelectorType = selectorType;
+            }
+
+            /**
+             * Sets the call identifier.
+             *
+             * @param callId The call identifier.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setCallId(@NonNull String callId) {
+                mCallId = callId;
+                return this;
+            }
+
+            /**
+             * Sets the dialed number.
+             *
+             * @param number The dialed number.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setNumber(@NonNull String number) {
+                mNumber = number;
+                return this;
+            }
+
+            /**
+             * Sets whether it's a video call or not.
+             *
+             * @param video Indicates it's a video call.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setVideoCall(boolean video) {
+                mIsVideoCall = video;
+                return this;
+            }
+
+            /**
+             * Sets whether it's an emergency service or not.
+             *
+             * @param emergency Indicates it's emergency service.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setEmergency(boolean emergency) {
+                mIsEmergency = emergency;
+                return this;
+            }
+
+            /**
+             * Sets whether the request caused the device to move out of airplane mode.
+             *
+             * @param exited {@code true} if the request caused the device to move out of
+             *        airplane mode.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setExitedFromAirplaneMode(boolean exited) {
+                mIsExitedFromAirplaneMode = exited;
+                return this;
+            }
+
+            /**
+             * Sets the Ut service attributes.
+             * Only applicable for SELECTOR_TYPE_UT
+             *
+             * @param attr Ut services attributes.
+             * @return The same instance of the builder.
+             */
+            /*
+            public @NonNull Builder setUtAttributes(@NonNull UtAttributes attr);
+                mUtAttributes = attr;
+                return this;
+            }
+            */
+
+            /**
+             * Sets an optional reason why the last PS attempt failed.
+             *
+             * @param info The reason why the last PS attempt failed.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setPsDisconnectCause(@NonNull ImsReasonInfo info) {
+                mImsReasonInfo = info;
+                return this;
+            }
+
+            /**
+             * Sets an optional reason why the last CS attempt failed.
+             *
+             * @param cause The reason why the last CS attempt failed.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setCsDisconnectCause(@PreciseDisconnectCauses int cause) {
+                mCause = cause;
+                return this;
+            }
+
+            /**
+             * Sets the current registration result for emergency services.
+             *
+             * @param regResult The current registration result for emergency services.
+             * @return The same instance of the builder.
+             */
+            public @NonNull Builder setEmergencyRegResult(@NonNull EmergencyRegResult regResult) {
+                mEmergencyRegResult = regResult;
+                return this;
+            }
+
+            /**
+             * Build the SelectionAttributes.
+             * @return The SelectionAttributes object.
+             */
+            public @NonNull SelectionAttributes build() {
+                return new SelectionAttributes(mSlotId, mSubId, mCallId, mNumber, mSelectorType,
+                        mIsVideoCall, mIsEmergency, mIsExitedFromAirplaneMode, /*mUtAttributes,*/
+                        mImsReasonInfo, mCause, mEmergencyRegResult);
+            }
+        }
+    }
+
+    /**
+     * A wrapper class for ITransportSelectorCallback interface.
+     */
+    private final class TransportSelectorCallbackWrapper implements TransportSelectorCallback {
+        private static final String TAG = "TransportSelectorCallbackWrapper";
+
+        private final @NonNull ITransportSelectorCallback mCallback;
+        private final @NonNull Executor mExecutor;
+
+        private @Nullable ITransportSelectorResultCallbackAdapter mResultCallback;
+        private @Nullable DomainSelectorWrapper mSelectorWrapper;
+
+        TransportSelectorCallbackWrapper(@NonNull ITransportSelectorCallback cb,
+                @NonNull Executor executor) {
+            mCallback = cb;
+            mExecutor = executor;
+        }
+
+        @Override
+        public void onCreated(@NonNull DomainSelector selector) {
+            try {
+                mSelectorWrapper = new DomainSelectorWrapper(selector, mExecutor);
+                mCallback.onCreated(mSelectorWrapper.getCallbackBinder());
+            } catch (Exception e) {
+                Rlog.e(TAG, "onCreated e=" + e);
+            }
+        }
+
+        @Override
+        public void onWlanSelected() {
+            try {
+                mCallback.onWlanSelected();
+            } catch (Exception e) {
+                Rlog.e(TAG, "onWlanSelected e=" + e);
+            }
+        }
+
+        @Override
+        public @NonNull WwanSelectorCallback onWwanSelected() {
+            WwanSelectorCallback callback = null;
+            try {
+                IWwanSelectorCallback cb = mCallback.onWwanSelected();
+                callback = new WwanSelectorCallbackWrapper(cb, mExecutor);
+            } catch (Exception e) {
+                Rlog.e(TAG, "onWwanSelected e=" + e);
+            }
+
+            return callback;
+        }
+
+        @Override
+        public void onWwanSelected(Consumer<WwanSelectorCallback> consumer) {
+            try {
+                mResultCallback = new ITransportSelectorResultCallbackAdapter(consumer, mExecutor);
+                mCallback.onWwanSelectedAsync(mResultCallback);
+            } catch (Exception e) {
+                Rlog.e(TAG, "onWwanSelected e=" + e);
+                executeMethodAsyncNoException(mExecutor,
+                        () -> consumer.accept(null), TAG, "onWwanSelectedAsync-Exception");
+            }
+        }
+
+        @Override
+        public void onSelectionTerminated(@DisconnectCauses int cause) {
+            try {
+                mCallback.onSelectionTerminated(cause);
+                mSelectorWrapper = null;
+            } catch (Exception e) {
+                Rlog.e(TAG, "onSelectionTerminated e=" + e);
+            }
+        }
+
+        private class ITransportSelectorResultCallbackAdapter
+                extends ITransportSelectorResultCallback.Stub {
+            private final @NonNull Consumer<WwanSelectorCallback> mConsumer;
+            private final @NonNull Executor mExecutor;
+
+            ITransportSelectorResultCallbackAdapter(
+                    @NonNull Consumer<WwanSelectorCallback> consumer,
+                    @NonNull Executor executor) {
+                mConsumer = consumer;
+                mExecutor = executor;
+            }
+
+            @Override
+            public void onCompleted(@NonNull IWwanSelectorCallback cb) {
+                if (mConsumer == null) return;
+
+                WwanSelectorCallback callback = new WwanSelectorCallbackWrapper(cb, mExecutor);
+                executeMethodAsyncNoException(mExecutor,
+                        () -> mConsumer.accept(callback), TAG, "onWwanSelectedAsync-Completed");
+            }
+        }
+    }
+
+    /**
+     * A wrapper class for IDomainSelector interface.
+     */
+    private final class DomainSelectorWrapper {
+        private static final String TAG = "DomainSelectorWrapper";
+
+        private @NonNull IDomainSelector mCallbackBinder;
+
+        DomainSelectorWrapper(@NonNull DomainSelector cb, @NonNull Executor executor) {
+            mCallbackBinder = new IDomainSelectorAdapter(cb, executor);
+        }
+
+        private class IDomainSelectorAdapter extends IDomainSelector.Stub {
+            private final @NonNull WeakReference<DomainSelector> mDomainSelectorWeakRef;
+            private final @NonNull Executor mExecutor;
+
+            IDomainSelectorAdapter(@NonNull DomainSelector domainSelector,
+                    @NonNull Executor executor) {
+                mDomainSelectorWeakRef =
+                        new WeakReference<DomainSelector>(domainSelector);
+                mExecutor = executor;
+            }
+
+            @Override
+            public void cancelSelection() {
+                final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
+                if (domainSelector == null) return;
+
+                executeMethodAsyncNoException(mExecutor,
+                        () -> domainSelector.cancelSelection(), TAG, "cancelSelection");
+            }
+
+            @Override
+            public void reselectDomain(@NonNull SelectionAttributes attr) {
+                final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
+                if (domainSelector == null) return;
+
+                executeMethodAsyncNoException(mExecutor,
+                        () -> domainSelector.reselectDomain(attr), TAG, "reselectDomain");
+            }
+
+            @Override
+            public void finishSelection() {
+                final DomainSelector domainSelector = mDomainSelectorWeakRef.get();
+                if (domainSelector == null) return;
+
+                executeMethodAsyncNoException(mExecutor,
+                        () -> domainSelector.finishSelection(), TAG, "finishSelection");
+            }
+        }
+
+        public @NonNull IDomainSelector getCallbackBinder() {
+            return mCallbackBinder;
+        }
+    }
+
+    /**
+     * A wrapper class for IWwanSelectorCallback and IWwanSelectorResultCallback.
+     */
+    private final class WwanSelectorCallbackWrapper
+            implements WwanSelectorCallback, CancellationSignal.OnCancelListener {
+        private static final String TAG = "WwanSelectorCallbackWrapper";
+
+        private final @NonNull IWwanSelectorCallback mCallback;
+        private final @NonNull Executor mExecutor;
+
+        private @Nullable IWwanSelectorResultCallbackAdapter mResultCallback;
+
+        WwanSelectorCallbackWrapper(@NonNull IWwanSelectorCallback cb,
+                @NonNull Executor executor) {
+            mCallback = cb;
+            mExecutor = executor;
+        }
+
+        @Override
+        public void onCancel() {
+            try {
+                mCallback.onCancel();
+            } catch (Exception e) {
+                Rlog.e(TAG, "onCancel e=" + e);
+            }
+        }
+
+        @Override
+        public void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
+                @EmergencyScanType int scanType, @NonNull CancellationSignal signal,
+                @NonNull Consumer<EmergencyRegResult> consumer) {
+            try {
+                if (signal != null) signal.setOnCancelListener(this);
+                mResultCallback = new IWwanSelectorResultCallbackAdapter(consumer, mExecutor);
+                mCallback.onRequestEmergencyNetworkScan(
+                        preferredNetworks.stream().mapToInt(Integer::intValue).toArray(),
+                        scanType, mResultCallback);
+            } catch (Exception e) {
+                Rlog.e(TAG, "onRequestEmergencyNetworkScan e=" + e);
+            }
+        }
+
+        @Override
+        public void onDomainSelected(@NetworkRegistrationInfo.Domain int domain) {
+            try {
+                mCallback.onDomainSelected(domain);
+            } catch (Exception e) {
+                Rlog.e(TAG, "onDomainSelected e=" + e);
+            }
+        }
+
+        private class IWwanSelectorResultCallbackAdapter
+                extends IWwanSelectorResultCallback.Stub {
+            private final @NonNull Consumer<EmergencyRegResult> mConsumer;
+            private final @NonNull Executor mExecutor;
+
+            IWwanSelectorResultCallbackAdapter(@NonNull Consumer<EmergencyRegResult> consumer,
+                    @NonNull Executor executor) {
+                mConsumer = consumer;
+                mExecutor = executor;
+            }
+
+            @Override
+            public void onComplete(@NonNull EmergencyRegResult result) {
+                if (mConsumer == null) return;
+
+                executeMethodAsyncNoException(mExecutor,
+                        () -> mConsumer.accept(result), TAG, "onScanComplete");
+            }
+        }
+    }
+
+    private final Object mExecutorLock = new Object();
+
+    /** Executor used to execute methods called remotely by the framework. */
+    private @NonNull Executor mExecutor;
+
+    /**
+     * Selects a domain for the given operation.
+     *
+     * @param attr Required to determine the domain.
+     * @param callback The callback instance being registered.
+     */
+    public void onDomainSelection(@NonNull SelectionAttributes attr,
+            @NonNull TransportSelectorCallback callback) {
+    }
+
+    /**
+     * Notifies the change in {@link ServiceState} for a specific slot.
+     *
+     * @param slotId For which the state changed.
+     * @param subId For which the state changed.
+     * @param serviceState Updated {@link ServiceState}.
+     */
+    public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) {
+    }
+
+    /**
+     * Notifies the change in {@link BarringInfo} for a specific slot.
+     *
+     * @param slotId For which the state changed.
+     * @param subId For which the state changed.
+     * @param info Updated {@link BarringInfo}.
+     */
+    public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo info) {
+    }
+
+    private final IBinder mDomainSelectionServiceController =
+            new IDomainSelectionServiceController.Stub() {
+        @Override
+        public void selectDomain(@NonNull SelectionAttributes attr,
+                @NonNull ITransportSelectorCallback callback)  throws RemoteException {
+            executeMethodAsync(getCachedExecutor(),
+                    () -> DomainSelectionService.this.onDomainSelection(attr,
+                            new TransportSelectorCallbackWrapper(callback, getCachedExecutor())),
+                    LOG_TAG, "onDomainSelection");
+        }
+
+        @Override
+        public void updateServiceState(int slotId, int subId, @NonNull ServiceState serviceState) {
+            executeMethodAsyncNoException(getCachedExecutor(),
+                    () -> DomainSelectionService.this.onServiceStateUpdated(slotId,
+                            subId, serviceState), LOG_TAG, "onServiceStateUpdated");
+        }
+
+        @Override
+        public void updateBarringInfo(int slotId, int subId, @NonNull BarringInfo info) {
+            executeMethodAsyncNoException(getCachedExecutor(),
+                    () -> DomainSelectionService.this.onBarringInfoUpdated(slotId, subId, info),
+                    LOG_TAG, "onBarringInfoUpdated");
+        }
+    };
+
+    private static void executeMethodAsync(@NonNull Executor executor, @NonNull Runnable r,
+            @NonNull String tag, @NonNull String errorLogName) throws RemoteException {
+        try {
+            CompletableFuture.runAsync(
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+        } catch (CancellationException | CompletionException e) {
+            Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
+            throw new RemoteException(e.getMessage());
+        }
+    }
+
+    private void executeMethodAsyncNoException(@NonNull Executor executor, @NonNull Runnable r,
+            @NonNull String tag, @NonNull String errorLogName) {
+        try {
+            CompletableFuture.runAsync(
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+        } catch (CancellationException | CompletionException e) {
+            Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
+        }
+    }
+
+    /** @hide */
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (SERVICE_INTERFACE.equals(intent.getAction())) {
+            Log.i(LOG_TAG, "DomainSelectionService Bound.");
+            return mDomainSelectionServiceController;
+        }
+        return null;
+    }
+
+    /**
+     * The DomainSelectionService will be able to define an {@link Executor} that the service
+     * can use to execute the methods. It has set the default executor as Runnable::run,
+     *
+     * @return An {@link Executor} to be used.
+     */
+    @SuppressLint("OnNameExpected")
+    public @NonNull Executor getExecutor() {
+        return Runnable::run;
+    }
+
+    /**
+     * Gets the {@link Executor} which executes methods of this service.
+     * This method should be private when this service is implemented in a separated process
+     * other than telephony framework.
+     * @return {@link Executor} instance.
+     * @hide
+     */
+    public @NonNull Executor getCachedExecutor() {
+        synchronized (mExecutorLock) {
+            if (mExecutor == null) {
+                Executor e = getExecutor();
+                mExecutor = (e != null) ? e : Runnable::run;
+            }
+            return mExecutor;
+        }
+    }
+
+    /**
+     * Returns a string representation of the domain.
+     * @param domain The domain.
+     * @return The name of the domain.
+     * @hide
+     */
+    public static @NonNull String getDomainName(@NetworkRegistrationInfo.Domain int domain) {
+        return NetworkRegistrationInfo.domainToString(domain);
+    }
+}
diff --git a/telephony/java/android/telephony/DomainSelector.java b/telephony/java/android/telephony/DomainSelector.java
new file mode 100644
index 0000000..0871831
--- /dev/null
+++ b/telephony/java/android/telephony/DomainSelector.java
@@ -0,0 +1,45 @@
+/*
+ * 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.telephony;
+
+import android.annotation.NonNull;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+
+/**
+ * Implemented as part of the {@link DomainSelectionService} to implement domain selection
+ * for a specific use case.
+ * @hide
+ */
+public interface DomainSelector {
+    /**
+     * Cancel an ongoing selection operation. It is up to the DomainSelectionService
+     * to clean up all ongoing operations with the framework.
+     */
+    void cancelSelection();
+
+    /**
+     * Reselect a domain due to the call not setting up properly.
+     *
+     * @param attr attributes required to select the domain.
+     */
+    void reselectDomain(@NonNull SelectionAttributes attr);
+
+    /**
+     * Finish the selection procedure and clean everything up.
+     */
+    void finishSelection();
+}
diff --git a/telephony/java/android/telephony/EmergencyRegResult.aidl b/telephony/java/android/telephony/EmergencyRegResult.aidl
new file mode 100644
index 0000000..f722962
--- /dev/null
+++ b/telephony/java/android/telephony/EmergencyRegResult.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.telephony;
+
+parcelable EmergencyRegResult;
diff --git a/telephony/java/android/telephony/EmergencyRegResult.java b/telephony/java/android/telephony/EmergencyRegResult.java
new file mode 100644
index 0000000..5aed412
--- /dev/null
+++ b/telephony/java/android/telephony/EmergencyRegResult.java
@@ -0,0 +1,318 @@
+/*
+ * 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.telephony;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Contains attributes required to determine the domain for a telephony service
+ * @hide
+ */
+public final class EmergencyRegResult implements Parcelable {
+
+    /**
+     * Indicates the cellular network type of the acquired system.
+     */
+    private @AccessNetworkConstants.RadioAccessNetworkType int mAccessNetworkType;
+
+    /**
+     * Registration state of the acquired system.
+     */
+    private @NetworkRegistrationInfo.RegistrationState int mRegState;
+
+    /**
+     * EMC domain indicates the current domain of the acquired system.
+     */
+    private @NetworkRegistrationInfo.Domain int mDomain;
+
+    /**
+     * Indicates whether the network supports voice over PS network.
+     */
+    private boolean mIsVopsSupported;
+
+    /**
+     * This indicates if camped network support VoLTE emergency bearers.
+     * This should only be set if the UE is in LTE mode.
+     */
+    private boolean mIsEmcBearerSupported;
+
+    /**
+     * The value of the network provided EMC in 5G Registration ACCEPT.
+     * This should be set only if the UE is in 5G mode.
+     */
+    private int mNwProvidedEmc;
+
+    /**
+     * The value of the network provided EMF(EPS Fallback) in 5G Registration ACCEPT.
+     * This should be set only if the UE is in 5G mode.
+     */
+    private int mNwProvidedEmf;
+
+    /** 3-digit Mobile Country Code, 000..999, empty string if unknown. */
+    private @NonNull String mMcc;
+
+    /** 2 or 3-digit Mobile Network Code, 00..999, empty string if unknown. */
+    private @NonNull String mMnc;
+
+    /**
+     * The ISO-3166-1 alpha-2 country code equivalent for the network's country code,
+     * empty string if unknown.
+     */
+    private @NonNull String mIso;
+
+    /**
+     * Constructor
+     * @param accessNetwork Indicates the network type of the acquired system.
+     * @param regState Indicates the registration state of the acquired system.
+     * @param domain Indicates the current domain of the acquired system.
+     * @param isVopsSupported Indicates whether the network supports voice over PS network.
+     * @param isEmcBearerSupported  Indicates if camped network support VoLTE emergency bearers.
+     * @param emc The value of the network provided EMC in 5G Registration ACCEPT.
+     * @param emf The value of the network provided EMF(EPS Fallback) in 5G Registration ACCEPT.
+     * @param mcc Mobile country code, empty string if unknown.
+     * @param mnc Mobile network code, empty string if unknown.
+     * @param iso The ISO-3166-1 alpha-2 country code equivalent, empty string if unknown.
+     * @hide
+     */
+    public EmergencyRegResult(
+            @AccessNetworkConstants.RadioAccessNetworkType int accessNetwork,
+            @NetworkRegistrationInfo.RegistrationState int regState,
+            @NetworkRegistrationInfo.Domain int domain,
+            boolean isVopsSupported, boolean isEmcBearerSupported, int emc, int emf,
+            @NonNull String mcc, @NonNull String mnc, @NonNull String iso) {
+        mAccessNetworkType = accessNetwork;
+        mRegState = regState;
+        mDomain = domain;
+        mIsVopsSupported = isVopsSupported;
+        mIsEmcBearerSupported = isEmcBearerSupported;
+        mNwProvidedEmc = emc;
+        mNwProvidedEmf = emf;
+        mMcc = mcc;
+        mMnc = mnc;
+        mIso = iso;
+    }
+
+    /**
+     * Copy constructors
+     *
+     * @param s Source emergency scan result
+     * @hide
+     */
+    public EmergencyRegResult(@NonNull EmergencyRegResult s) {
+        mAccessNetworkType = s.mAccessNetworkType;
+        mRegState = s.mRegState;
+        mDomain = s.mDomain;
+        mIsVopsSupported = s.mIsVopsSupported;
+        mIsEmcBearerSupported = s.mIsEmcBearerSupported;
+        mNwProvidedEmc = s.mNwProvidedEmc;
+        mNwProvidedEmf = s.mNwProvidedEmf;
+        mMcc = s.mMcc;
+        mMnc = s.mMnc;
+        mIso = s.mIso;
+    }
+
+    /**
+     * Construct a EmergencyRegResult object from the given parcel.
+     */
+    private EmergencyRegResult(@NonNull Parcel in) {
+        readFromParcel(in);
+    }
+
+    /**
+     * Returns the cellular access network type of the acquired system.
+     *
+     * @return the cellular network type.
+     */
+    public @AccessNetworkConstants.RadioAccessNetworkType int getAccessNetwork() {
+        return mAccessNetworkType;
+    }
+
+    /**
+     * Returns the registration state of the acquired system.
+     *
+     * @return the registration state.
+     */
+    public @NetworkRegistrationInfo.RegistrationState int getRegState() {
+        return mRegState;
+    }
+
+    /**
+     * Returns the current domain of the acquired system.
+     *
+     * @return the current domain.
+     */
+    public @NetworkRegistrationInfo.Domain int getDomain() {
+        return mDomain;
+    }
+
+    /**
+     * Returns whether the network supports voice over PS network.
+     *
+     * @return {@code true} if the network supports voice over PS network.
+     */
+    public boolean isVopsSupported() {
+        return mIsVopsSupported;
+    }
+
+    /**
+     * Returns whether camped network support VoLTE emergency bearers.
+     * This is not valid if the UE is not in LTE mode.
+     *
+     * @return {@code true} if the network supports VoLTE emergency bearers.
+     */
+    public boolean isEmcBearerSupported() {
+        return mIsEmcBearerSupported;
+    }
+
+    /**
+     * Returns the value of the network provided EMC in 5G Registration ACCEPT.
+     * This is not valid if UE is not in 5G mode.
+     *
+     * @return the value of the network provided EMC.
+     */
+    public int getNwProvidedEmc() {
+        return mNwProvidedEmc;
+    }
+
+    /**
+     * Returns the value of the network provided EMF(EPS Fallback) in 5G Registration ACCEPT.
+     * This is not valid if UE is not in 5G mode.
+     *
+     * @return the value of the network provided EMF.
+     */
+    public int getNwProvidedEmf() {
+        return mNwProvidedEmf;
+    }
+
+    /**
+     * Returns 3-digit Mobile Country Code.
+     *
+     * @return Mobile Country Code.
+     */
+    public @NonNull String getMcc() {
+        return mMcc;
+    }
+
+    /**
+     * Returns 2 or 3-digit Mobile Network Code.
+     *
+     * @return Mobile Network Code.
+     */
+    public @NonNull String getMnc() {
+        return mMnc;
+    }
+
+    /**
+     * Returns the ISO-3166-1 alpha-2 country code is provided in lowercase 2 character format.
+     *
+     * @return Country code.
+     */
+    public @NonNull String getIso() {
+        return mIso;
+    }
+
+    @Override
+    public @NonNull String toString() {
+        return "{ accessNetwork="
+                + AccessNetworkConstants.AccessNetworkType.toString(mAccessNetworkType)
+                + ", regState=" + NetworkRegistrationInfo.registrationStateToString(mRegState)
+                + ", domain=" + NetworkRegistrationInfo.domainToString(mDomain)
+                + ", vops=" + mIsVopsSupported
+                + ", emcBearer=" + mIsEmcBearerSupported
+                + ", emc=" + mNwProvidedEmc
+                + ", emf=" + mNwProvidedEmf
+                + ", mcc=" + mMcc
+                + ", mnc=" + mMnc
+                + ", iso=" + mIso
+                + " }";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        EmergencyRegResult that = (EmergencyRegResult) o;
+        return mAccessNetworkType == that.mAccessNetworkType
+                && mRegState == that.mRegState
+                && mDomain == that.mDomain
+                && mIsVopsSupported == that.mIsVopsSupported
+                && mIsEmcBearerSupported == that.mIsEmcBearerSupported
+                && mNwProvidedEmc == that.mNwProvidedEmc
+                && mNwProvidedEmf == that.mNwProvidedEmf
+                && TextUtils.equals(mMcc, that.mMcc)
+                && TextUtils.equals(mMnc, that.mMnc)
+                && TextUtils.equals(mIso, that.mIso);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAccessNetworkType, mRegState, mDomain,
+                mIsVopsSupported, mIsEmcBearerSupported,
+                mNwProvidedEmc, mNwProvidedEmf,
+                mMcc, mMnc, mIso);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(mAccessNetworkType);
+        out.writeInt(mRegState);
+        out.writeInt(mDomain);
+        out.writeBoolean(mIsVopsSupported);
+        out.writeBoolean(mIsEmcBearerSupported);
+        out.writeInt(mNwProvidedEmc);
+        out.writeInt(mNwProvidedEmf);
+        out.writeString8(mMcc);
+        out.writeString8(mMnc);
+        out.writeString8(mIso);
+    }
+
+    private void readFromParcel(@NonNull Parcel in) {
+        mAccessNetworkType = in.readInt();
+        mRegState = in.readInt();
+        mDomain = in.readInt();
+        mIsVopsSupported = in.readBoolean();
+        mIsEmcBearerSupported = in.readBoolean();
+        mNwProvidedEmc = in.readInt();
+        mNwProvidedEmf = in.readInt();
+        mMcc = in.readString8();
+        mMnc = in.readString8();
+        mIso = in.readString8();
+    }
+
+    public static final @NonNull Creator<EmergencyRegResult> CREATOR =
+            new Creator<EmergencyRegResult>() {
+        @Override
+        public EmergencyRegResult createFromParcel(@NonNull Parcel in) {
+            return new EmergencyRegResult(in);
+        }
+
+        @Override
+        public EmergencyRegResult[] newArray(int size) {
+            return new EmergencyRegResult[size];
+        }
+    };
+}
diff --git a/telephony/java/android/telephony/PhoneCapability.java b/telephony/java/android/telephony/PhoneCapability.java
index 63e3468..48170df 100644
--- a/telephony/java/android/telephony/PhoneCapability.java
+++ b/telephony/java/android/telephony/PhoneCapability.java
@@ -90,7 +90,9 @@
     /**
      * mMaxActiveVoiceSubscriptions defines the maximum subscriptions that can support
      * simultaneous voice calls. For a dual sim dual standby (DSDS) device it would be one, but
-     * for a dual sim dual active device it would be 2.
+     * for a dual sim dual active (DSDA) device, or a DSDS device that supports "virtual DSDA" (
+     * using the data line of 1 SIM to temporarily provide IMS voice connectivity to the other SIM)
+     * it would be 2.
      *
      * @hide
      */
@@ -99,7 +101,7 @@
     /**
      * mMaxActiveDataSubscriptions defines the maximum subscriptions that can support
      * simultaneous data connections.
-     * For example, for L+L device it should be 2.
+     * For example, for dual sim dual active L+L device it should be 2.
      *
      * @hide
      */
@@ -114,14 +116,20 @@
      */
     private final boolean mNetworkValidationBeforeSwitchSupported;
 
-    /** @hide */
-    private final List<ModemInfo> mLogicalModemList;
-
     /**
      * List of logical modem information.
      *
      * @hide
      */
+    @NonNull
+    private final List<ModemInfo> mLogicalModemList;
+
+    /**
+     * Device NR capabilities.
+     *
+     * @hide
+     */
+    @NonNull
     private final int[] mDeviceNrCapabilities;
 
     /** @hide */
@@ -136,6 +144,18 @@
         this.mDeviceNrCapabilities = deviceNrCapabilities;
     }
 
+    private PhoneCapability(@NonNull Builder builder) {
+        this.mMaxActiveVoiceSubscriptions = builder.mMaxActiveVoiceSubscriptions;
+        this.mMaxActiveDataSubscriptions = builder.mMaxActiveDataSubscriptions;
+        // Make sure it's not null.
+        this.mLogicalModemList = builder.mLogicalModemList == null ? new ArrayList<>()
+                : builder.mLogicalModemList;
+        this.mNetworkValidationBeforeSwitchSupported =
+                builder.mNetworkValidationBeforeSwitchSupported;
+        this.mDeviceNrCapabilities = builder.mDeviceNrCapabilities;
+
+    }
+
     @Override
     public String toString() {
         return "mMaxActiveVoiceSubscriptions=" + mMaxActiveVoiceSubscriptions
@@ -264,4 +284,121 @@
     public @NonNull @DeviceNrCapability int[] getDeviceNrCapabilities() {
         return mDeviceNrCapabilities == null ? (new int[0]) : mDeviceNrCapabilities;
     }
+
+
+    /**
+     * Builder for {@link PhoneCapability}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        /**
+         * mMaxActiveVoiceSubscriptions defines the maximum subscriptions that can support
+         * simultaneous voice calls. For a dual sim dual standby (DSDS) device it would be one, but
+         * for a dual sim dual active (DSDA) device, or a DSDS device that supports "virtual DSDA"
+         * (using the data line of 1 SIM to temporarily provide IMS voice connectivity to the other
+         * SIM) it would be 2.
+         *
+         * @hide
+         */
+        private int mMaxActiveVoiceSubscriptions = 0;
+
+        /**
+         * mMaxActiveDataSubscriptions defines the maximum subscriptions that can support
+         * simultaneous data connections. For example, for L+L device it should be 2.
+         *
+         * @hide
+         */
+        private int mMaxActiveDataSubscriptions = 0;
+
+        /**
+         * Whether modem supports both internet PDN up so that we can do ping test before tearing
+         * down the other one.
+         *
+         * @hide
+         */
+        private boolean mNetworkValidationBeforeSwitchSupported = false;
+
+        /**
+         * List of logical modem information.
+         *
+         * @hide
+         */
+        @NonNull
+        private List<ModemInfo> mLogicalModemList = new ArrayList<>();
+
+        /**
+         * Device NR capabilities.
+         *
+         * @hide
+         */
+        @NonNull
+        private int[] mDeviceNrCapabilities = new int[0];
+
+        /**
+         * Default constructor.
+         */
+        public Builder() {
+        }
+
+        public Builder(@NonNull PhoneCapability phoneCapability) {
+            mMaxActiveVoiceSubscriptions = phoneCapability.mMaxActiveVoiceSubscriptions;
+            mMaxActiveDataSubscriptions = phoneCapability.mMaxActiveDataSubscriptions;
+            mNetworkValidationBeforeSwitchSupported =
+                    phoneCapability.mNetworkValidationBeforeSwitchSupported;
+            mLogicalModemList = phoneCapability.mLogicalModemList;
+            mDeviceNrCapabilities = phoneCapability.mDeviceNrCapabilities;
+        }
+
+        /**
+         * Sets the max active voice subscriptions supported by the device.
+         */
+        public Builder setMaxActiveVoiceSubscriptions(int maxActiveVoiceSubscriptions) {
+            mMaxActiveVoiceSubscriptions = maxActiveVoiceSubscriptions;
+            return this;
+        }
+
+        /**
+         * Sets the max active voice subscriptions supported by the device.
+         */
+        public Builder setMaxActiveDataSubscriptions(int maxActiveDataSubscriptions) {
+            mMaxActiveDataSubscriptions = maxActiveDataSubscriptions;
+            return this;
+        }
+
+        /**
+         * Sets the max active data subscriptions supported by the device. Can be fewer than the
+         * active voice subscriptions.
+         */
+        public Builder setNetworkValidationBeforeSwitchSupported(
+                boolean networkValidationBeforeSwitchSupported) {
+            mNetworkValidationBeforeSwitchSupported = networkValidationBeforeSwitchSupported;
+            return this;
+        }
+
+        /**
+         * Sets the logical modem list of the device.
+         */
+        public Builder setLogicalModemList(@NonNull List<ModemInfo> logicalModemList) {
+            mLogicalModemList = logicalModemList;
+            return this;
+        }
+
+        /**
+         * Sets the NR capabilities supported by the device.
+         */
+        public Builder setDeviceNrCapabilities(@NonNull int[] deviceNrCapabilities) {
+            mDeviceNrCapabilities = deviceNrCapabilities;
+            return this;
+        }
+
+        /**
+         * Build the {@link PhoneCapability}.
+         *
+         * @return The {@link PhoneCapability} instance.
+         */
+        public PhoneCapability build() {
+            return new PhoneCapability(this);
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/PreciseCallState.java b/telephony/java/android/telephony/PreciseCallState.java
index 98eeacf..f433609 100644
--- a/telephony/java/android/telephony/PreciseCallState.java
+++ b/telephony/java/android/telephony/PreciseCallState.java
@@ -66,6 +66,11 @@
     public static final int PRECISE_CALL_STATE_DISCONNECTED =   7;
     /** Call state: Disconnecting. */
     public static final int PRECISE_CALL_STATE_DISCONNECTING =  8;
+    /**
+     * Call state: Incoming in pre-alerting state i.e. prior to entering
+     * {@link #PRECISE_CALL_STATE_INCOMING}.
+     */
+    public static final int PRECISE_CALL_STATE_INCOMING_SETUP = 9;
 
     private @PreciseCallStates int mRingingCallState = PRECISE_CALL_STATE_NOT_VALID;
     private @PreciseCallStates int mForegroundCallState = PRECISE_CALL_STATE_NOT_VALID;
diff --git a/telephony/java/android/telephony/RadioAccessSpecifier.java b/telephony/java/android/telephony/RadioAccessSpecifier.java
index a403095..9511db6 100644
--- a/telephony/java/android/telephony/RadioAccessSpecifier.java
+++ b/telephony/java/android/telephony/RadioAccessSpecifier.java
@@ -161,9 +161,17 @@
     }
 
     @Override
-    public int hashCode () {
+    public int hashCode() {
         return ((mRadioAccessNetwork * 31)
                 + (Arrays.hashCode(mBands) * 37)
                 + (Arrays.hashCode(mChannels)) * 39);
     }
+
+    @Override
+    public String toString() {
+        return "RadioAccessSpecifier[mRadioAccessNetwork="
+                + AccessNetworkConstants.AccessNetworkType.toString(mRadioAccessNetwork)
+                + ", mBands=" + Arrays.toString(mBands)
+                + ", mChannels=" + Arrays.toString(mChannels) + "]";
+    }
 }
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index bfa60ba..6be2f77 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -138,13 +138,6 @@
      */
     public static final int FREQUENCY_RANGE_MMWAVE = 4;
 
-    private static final List<Integer> FREQUENCY_RANGE_ORDER = Arrays.asList(
-            FREQUENCY_RANGE_UNKNOWN,
-            FREQUENCY_RANGE_LOW,
-            FREQUENCY_RANGE_MID,
-            FREQUENCY_RANGE_HIGH,
-            FREQUENCY_RANGE_MMWAVE);
-
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = "DUPLEX_MODE_",
@@ -2108,15 +2101,6 @@
     }
 
     /**
-     * @hide
-     */
-    public static final int getBetterNRFrequencyRange(int range1, int range2) {
-        return FREQUENCY_RANGE_ORDER.indexOf(range1) > FREQUENCY_RANGE_ORDER.indexOf(range2)
-                ? range1
-                : range2;
-    }
-
-    /**
      * Returns a copy of self with location-identifying information removed.
      * Always clears the NetworkRegistrationInfo's CellIdentity fields, but if removeCoarseLocation
      * is true, clears other info as well.
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index d670e55..1f301c1 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -2268,7 +2268,21 @@
             RESULT_RIL_SIM_ABSENT,
             RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED,
             RESULT_RIL_ACCESS_BARRED,
-            RESULT_RIL_BLOCKED_DUE_TO_CALL
+            RESULT_RIL_BLOCKED_DUE_TO_CALL,
+            RESULT_RIL_GENERIC_ERROR,
+            RESULT_RIL_INVALID_RESPONSE,
+            RESULT_RIL_SIM_PIN2,
+            RESULT_RIL_SIM_PUK2,
+            RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE,
+            RESULT_RIL_SIM_ERROR,
+            RESULT_RIL_INVALID_SIM_STATE,
+            RESULT_RIL_NO_SMS_TO_ACK,
+            RESULT_RIL_SIM_BUSY,
+            RESULT_RIL_SIM_FULL,
+            RESULT_RIL_NO_SUBSCRIPTION,
+            RESULT_RIL_NO_NETWORK_FOUND,
+            RESULT_RIL_DEVICE_IN_USE,
+            RESULT_RIL_ABORTED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Result {}
@@ -2534,7 +2548,7 @@
     public static final int RESULT_RIL_SIM_ABSENT = 120;
 
     /**
-     * 1X voice and SMS are not allowed simulteneously.
+     * 1X voice and SMS are not allowed simultaneously.
      */
     public static final int RESULT_RIL_SIMULTANEOUS_SMS_AND_CALL_NOT_ALLOWED = 121;
 
@@ -2553,6 +2567,73 @@
      */
     public static final int RESULT_RIL_GENERIC_ERROR = 124;
 
+    /**
+     * A RIL internal error when one of the RIL layers receives an unrecognized response from a
+     * lower layer.
+     */
+    public static final int RESULT_RIL_INVALID_RESPONSE = 125;
+
+    /**
+     * Operation requires SIM PIN2 to be entered
+     */
+    public static final int RESULT_RIL_SIM_PIN2 = 126;
+
+    /**
+     * Operation requires SIM PUK2 to be entered
+     */
+    public static final int RESULT_RIL_SIM_PUK2 = 127;
+
+    /**
+     * Fail to find CDMA subscription from specified location
+     */
+    public static final int RESULT_RIL_SUBSCRIPTION_NOT_AVAILABLE = 128;
+
+    /**
+     * Received error from SIM card
+     */
+    public static final int RESULT_RIL_SIM_ERROR = 129;
+
+    /**
+     * Cannot process the request in current SIM state
+     */
+    public static final int RESULT_RIL_INVALID_SIM_STATE = 130;
+
+    /**
+     * ACK received when there is no SMS to ack
+     */
+    public static final int RESULT_RIL_NO_SMS_TO_ACK = 131;
+
+    /**
+     * SIM is busy
+     */
+    public static final int RESULT_RIL_SIM_BUSY = 132;
+
+    /**
+     * The target EF is full
+     */
+    public static final int RESULT_RIL_SIM_FULL = 133;
+
+    /**
+     * Device does not have subscription
+     */
+    public static final int RESULT_RIL_NO_SUBSCRIPTION = 134;
+
+    /**
+     * Network cannot be found
+     */
+    public static final int RESULT_RIL_NO_NETWORK_FOUND = 135;
+
+    /**
+     * Operation cannot be performed because the device is currently in use
+     */
+    public static final int RESULT_RIL_DEVICE_IN_USE = 136;
+
+    /**
+     * Operation aborted
+     */
+    public static final int RESULT_RIL_ABORTED = 137;
+
+
     // SMS receiving results sent as a "result" extra in {@link Intents.SMS_REJECTED_ACTION}
 
     /**
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index eb3affc..76a145c 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -54,6 +54,7 @@
 import android.os.ParcelUuid;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.provider.Telephony.SimInfo;
 import android.telephony.euicc.EuiccManager;
 import android.telephony.ims.ImsMmTelManager;
@@ -3941,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.
@@ -3997,6 +4002,10 @@
      * cautiously, for example, after formatting the number to a consistent format with
      * {@link android.telephony.PhoneNumberUtils#formatNumberToE164(String, String)}.
      *
+     * <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.
      * @return the phone number, or an empty string if not available.
@@ -4154,4 +4163,79 @@
                 return "UNKNOWN(" + usageSetting + ")";
         }
     }
+
+    /**
+     * Set userHandle for a subscription.
+     *
+     * Used to set an association between a subscription and a user on the device so that voice
+     * calling and SMS from that subscription can be associated with that user.
+     * Data services are always shared between users on the device.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @param userHandle the userHandle associated with the subscription.
+     * Pass {@code null} user handle to clear the association.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public void setSubscriptionUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[setSubscriptionUserHandle]: "
+                    + "Invalid subscriptionId: " + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                iSub.setSubscriptionUserHandle(userHandle, subscriptionId);
+            } else {
+                throw new IllegalStateException("[setSubscriptionUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Get UserHandle of this subscription.
+     *
+     * Used to get user handle associated with this subscription.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @return userHandle associated with this subscription
+     * or {@code null} if subscription is not associated with any user.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public @Nullable UserHandle getSubscriptionUserHandle(int subscriptionId) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[getSubscriptionUserHandle]: "
+                    + "Invalid subscriptionId: " + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                return iSub.getSubscriptionUserHandle(subscriptionId);
+            } else {
+                throw new IllegalStateException("[getSubscriptionUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+        return null;
+    }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index f3d48a8..8c3ef67 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -54,6 +54,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -2974,7 +2975,11 @@
     public static final int NETWORK_TYPE_HSUPA = TelephonyProtoEnums.NETWORK_TYPE_HSUPA; // = 9.
     /** Current network is HSPA */
     public static final int NETWORK_TYPE_HSPA = TelephonyProtoEnums.NETWORK_TYPE_HSPA; // = 10.
-    /** Current network is iDen */
+    /**
+     * Current network is iDen
+     * @deprecated Legacy network type no longer being used.
+     */
+    @Deprecated
     public static final int NETWORK_TYPE_IDEN = TelephonyProtoEnums.NETWORK_TYPE_IDEN; // = 11.
     /** Current network is EVDO revision B*/
     public static final int NETWORK_TYPE_EVDO_B = TelephonyProtoEnums.NETWORK_TYPE_EVDO_B; // = 12.
@@ -3322,15 +3327,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;
         }
@@ -8294,16 +8298,23 @@
     /** Authentication type for UICC challenge is EAP AKA. See RFC 4187 for details. */
     public static final int AUTHTYPE_EAP_AKA = PhoneConstants.AUTH_CONTEXT_EAP_AKA;
     /**
-     * Authentication type for GBA Bootstrap Challenge is GBA_BOOTSTRAP.
-     * See 3GPP 33.220 Section 5.3.2.
-     * @hide
+     * Authentication type for GBA Bootstrap Challenge.
+     * Pass this authentication type into the {@link #getIccAuthentication} API to perform a GBA
+     * Bootstrap challenge (BSF), with {@code data} (generated according to the procedure defined in
+     * 3GPP 33.220 Section 5.3.2 step.4) in base64 encoding.
+     * This method will return the Bootstrapping response in base64 encoding when ICC authentication
+     * is completed.
+     * Ref 3GPP 33.220 Section 5.3.2.
      */
     public static final int AUTHTYPE_GBA_BOOTSTRAP = PhoneConstants.AUTH_CONTEXT_GBA_BOOTSTRAP;
     /**
-     * Authentication type for GBA Network Application Functions (NAF) key
-     * External Challenge is AUTHTYPE_GBA_NAF_KEY_EXTERNAL.
-     * See 3GPP 33.220 Section 5.3.2.
-     * @hide
+     * Authentication type for GBA Network Application Functions (NAF) key External Challenge.
+     * Pass this authentication type into the {@link #getIccAuthentication} API to perform a GBA
+     * Network Applications Functions (NAF) key External challenge using the NAF_ID parameter
+     * as the {@code data} in base64 encoding.
+     * This method will return the Ks_Ext_Naf key in base64 encoding when ICC authentication
+     * is completed.
+     * Ref 3GPP 33.220 Section 5.3.2.
      */
     public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL =
             PhoneConstants.AUTHTYPE_GBA_NAF_KEY_EXTERNAL;
@@ -8332,7 +8343,8 @@
      *
      * @param appType the icc application type, like {@link #APPTYPE_USIM}
      * @param authType the authentication type, any one of {@link #AUTHTYPE_EAP_AKA} or
-     * {@link #AUTHTYPE_EAP_SIM}
+     * {@link #AUTHTYPE_EAP_SIM} or {@link #AUTHTYPE_GBA_BOOTSTRAP} or
+     * {@link #AUTHTYPE_GBA_NAF_KEY_EXTERNAL}
      * @param data authentication challenge data, base64 encoded.
      * See 3GPP TS 31.102 7.1.2 for more details.
      * @return the response of authentication. This value will be null in the following cases:
@@ -8360,7 +8372,8 @@
      * @param subId subscription ID used for authentication
      * @param appType the icc application type, like {@link #APPTYPE_USIM}
      * @param authType the authentication type, any one of {@link #AUTHTYPE_EAP_AKA} or
-     * {@link #AUTHTYPE_EAP_SIM}
+     * {@link #AUTHTYPE_EAP_SIM} or {@link #AUTHTYPE_GBA_BOOTSTRAP} or
+     * {@link #AUTHTYPE_GBA_NAF_KEY_EXTERNAL}
      * @param data authentication challenge data, base64 encoded.
      * See 3GPP TS 31.102 7.1.2 for more details.
      * @return the response of authentication. This value will be null in the following cases only
@@ -9476,7 +9489,8 @@
             ALLOWED_NETWORK_TYPES_REASON_USER,
             ALLOWED_NETWORK_TYPES_REASON_POWER,
             ALLOWED_NETWORK_TYPES_REASON_CARRIER,
-            ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G
+            ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G,
+            ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AllowedNetworkTypesReason {
@@ -9515,14 +9529,24 @@
     public static final int ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G = 3;
 
     /**
+     * To indicate allowed network type change is requested by an update to the
+     * {@link android.os.UserManager.DISALLOW_CELLULAR_2G} user restriction.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS = 4;
+
+    /**
      * Set the allowed network types of the device and provide the reason triggering the allowed
      * network change.
-     * <p>Requires permission: android.Manifest.MODIFY_PHONE_STATE or
+     * <p>Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or
      * that the calling app has carrier privileges (see {@link #hasCarrierPrivileges}).
      *
-     * This can be called for following reasons
+     * This can be called for following reasons:
      * <ol>
-     * <li>Allowed network types control by USER {@link #ALLOWED_NETWORK_TYPES_REASON_USER}
+     * <li>Allowed network types control by USER
+     * {@link TelephonyManager#ALLOWED_NETWORK_TYPES_REASON_USER}
      * <li>Allowed network types control by carrier {@link #ALLOWED_NETWORK_TYPES_REASON_CARRIER}
      * </ol>
      * This API will result in allowing an intersection of allowed network types for all reasons,
@@ -9532,7 +9556,13 @@
      * @param allowedNetworkTypes The bitmask of allowed network type
      * @throws IllegalStateException if the Telephony process is not currently available.
      * @throws IllegalArgumentException if invalid AllowedNetworkTypesReason is passed.
-     * @throws SecurityException if the caller does not have the required privileges
+     * @throws SecurityException if the caller does not have the required privileges or if the
+     * caller tries to use one of the following security-based reasons without
+     * {@link android.Manifest.permission#MODIFY_PHONE_STATE} permissions.
+     * <ol>
+     *     <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G}</li>
+     *     <li>{@code TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS}</li>
+     * </ol>
      */
     @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
     @RequiresFeature(
@@ -9606,6 +9636,7 @@
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_POWER:
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_CARRIER:
             case TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G:
+            case ALLOWED_NETWORK_TYPES_REASON_USER_RESTRICTIONS:
                 return true;
         }
         return false;
@@ -12515,7 +12546,7 @@
             Log.e(TAG, "Error calling ITelephony#getServiceStateForSubscriber", e);
         } catch (NullPointerException e) {
             AnomalyReporter.reportAnomaly(
-                    UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
+                    UUID.fromString("e2bed88e-def9-476e-bd71-3e572a8de6d1"),
                     "getServiceStateForSubscriber " + subId + " NPE");
         }
         return null;
@@ -13910,7 +13941,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 {}
 
@@ -13970,6 +14002,11 @@
      */
     public static final long NETWORK_TYPE_BITMASK_HSPA = (1 << (NETWORK_TYPE_HSPA -1));
     /**
+     * network type bitmask indicating the support of radio tech iDen.
+     * @hide
+     */
+    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));
@@ -13987,12 +14024,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));
 
     /**
@@ -14935,21 +14973,132 @@
      * @return a Pair of (major version, minor version) or (-1,-1) if unknown.
      *
      * @hide
+     *
+     * @deprecated Use {@link #getHalVersion} instead.
      */
+    @Deprecated
     @UnsupportedAppUsage
     @TestApi
     public Pair<Integer, Integer> getRadioHalVersion() {
+        return getHalVersion(HAL_SERVICE_RADIO);
+    }
+
+    /** @hide */
+    public static final int HAL_SERVICE_RADIO = 0;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioData
+     * {@link RadioDataProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_DATA = 1;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioMessaging
+     * {@link RadioMessagingProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_MESSAGING = 2;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioModem
+     * {@link RadioModemProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_MODEM = 3;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioNetwork
+     * {@link RadioNetworkProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_NETWORK = 4;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioSim
+     * {@link RadioSimProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_SIM = 5;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioVoice
+     * {@link RadioVoiceProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_VOICE = 6;
+
+    /**
+     * HAL service type that supports the HAL APIs implementation of IRadioIms
+     * {@link RadioImsProxy}
+     * @hide
+     */
+    @TestApi
+    public static final int HAL_SERVICE_IMS = 7;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"HAL_SERVICE_"},
+            value = {
+                    HAL_SERVICE_RADIO,
+                    HAL_SERVICE_DATA,
+                    HAL_SERVICE_MESSAGING,
+                    HAL_SERVICE_MODEM,
+                    HAL_SERVICE_NETWORK,
+                    HAL_SERVICE_SIM,
+                    HAL_SERVICE_VOICE,
+                    HAL_SERVICE_IMS,
+            })
+    public @interface HalService {}
+
+    /**
+     * The HAL Version indicating that the version is unknown or invalid.
+     * @hide
+     */
+    @TestApi
+    public static final Pair HAL_VERSION_UNKNOWN = new Pair(-1, -1);
+
+    /**
+     * The HAL Version indicating that the version is unsupported.
+     * @hide
+     */
+    @TestApi
+    public static final Pair HAL_VERSION_UNSUPPORTED = new Pair(-2, -2);
+
+    /**
+     * Retrieve the HAL Version of a specific service for this device.
+     *
+     * Get the HAL version for a specific HAL interface for test purposes.
+     *
+     * @param halService the service id to query.
+     * @return a Pair of (major version, minor version), HAL_VERSION_UNKNOWN if unknown
+     * or HAL_VERSION_UNSUPPORTED if unsupported.
+     *
+     * @hide
+     */
+    @TestApi
+    public @NonNull Pair<Integer, Integer> getHalVersion(@HalService int halService) {
         try {
             ITelephony service = getITelephony();
             if (service != null) {
-                int version = service.getRadioHalVersion();
-                if (version == -1) return new Pair<Integer, Integer>(-1, -1);
-                return new Pair<Integer, Integer>(version / 100, version % 100);
+                int version = service.getHalVersion(halService);
+                if (version != -1) {
+                    return new Pair<Integer, Integer>(version / 100, version % 100);
+                }
+            } else {
+                throw new IllegalStateException("telephony service is null.");
             }
         } catch (RemoteException e) {
-            Log.e(TAG, "getRadioHalVersion() RemoteException", e);
+            Log.e(TAG, "getHalVersion() RemoteException", e);
+            e.rethrowAsRuntimeException();
         }
-        return new Pair<Integer, Integer>(-1, -1);
+        return HAL_VERSION_UNKNOWN;
     }
 
     /**
@@ -15626,11 +15775,29 @@
     public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2;
 
     /**
+     * Allow switching mobile data to the non-default SIM if the non-default SIM has better
+     * availability.
+     *
+     * This is used for temporarily allowing data on the non-default data SIM when on-default SIM
+     * has better availability on DSDS devices, where better availability means strong
+     * signal/connectivity.
+     * If this policy is enabled, data will be temporarily enabled on the non-default data SIM,
+     * including during any voice calls(equivalent to enabling
+     * {@link #MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL}).
+     *
+     * This policy can be enabled and disabled via {@link #setMobileDataPolicyEnabled}.
+     * @hide
+     */
+    @SystemApi
+    public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3;
+
+    /**
      * @hide
      */
     @IntDef(prefix = { "MOBILE_DATA_POLICY_" }, value = {
             MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
             MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
+            MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface MobileDataPolicy { }
@@ -17115,11 +17282,12 @@
     }
 
     /**
-     * A premium capability boosting the network to allow real-time interactive traffic.
-     * Corresponds to NetworkCapabilities#NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC.
+     * A premium capability that boosts the network to allow for real-time interactive traffic
+     * by prioritizing low latency communication.
+     * Corresponds to {@link NetworkCapabilities#NET_CAPABILITY_PRIORITIZE_LATENCY}.
      */
-    // TODO(b/245748544): add @link once NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC is defined.
-    public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1;
+    public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY =
+            NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 
     /**
      * Purchasable premium capabilities.
@@ -17127,7 +17295,7 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "PREMIUM_CAPABILITY_" }, value = {
-            PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC})
+            PREMIUM_CAPABILITY_PRIORITIZE_LATENCY})
     public @interface PremiumCapability {}
 
     /**
@@ -17139,8 +17307,8 @@
      */
     public static String convertPremiumCapabilityToString(@PremiumCapability int capability) {
         switch (capability) {
-            case PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC:
-                return "REALTIME_INTERACTIVE_TRAFFIC";
+            case PREMIUM_CAPABILITY_PRIORITIZE_LATENCY:
+                return "PRIORITIZE_LATENCY";
             default:
                 return "UNKNOWN (" + capability + ")";
         }
@@ -17170,7 +17338,13 @@
     }
 
     /**
-     * Purchase premium capability request was successful. Subsequent attempts will return
+     * Purchase premium capability request was successful.
+     * Once the purchase result is successful, the network must set up a slicing configuration
+     * for the purchased premium capability within the timeout specified by
+     * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG}.
+     * During the setup time, subsequent attempts will return
+     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}.
+     * After setup is complete, subsequent attempts will return
      * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} until the booster expires.
      * The expiry time is determined by the type or duration of boost purchased from the carrier,
      * provided at {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING}.
@@ -17178,11 +17352,19 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1;
 
     /**
-     * Purchase premium capability failed because the request is throttled for the amount of time
+     * Purchase premium capability failed because the request is throttled.
+     * If purchasing premium capabilities is throttled, it will be for the amount of time
      * specified by {@link CarrierConfigManager
-     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
-     * or {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
+     * If displaying the network boost notification is throttled, it will be for the amount of time
+     * specified by {@link CarrierConfigManager
+     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
+     * If a foreground application requests premium capabilities, the network boost notification
+     * will be displayed to the user regardless of the throttled status.
+     * We will show the network boost notification to the user up to the daily and monthly maximum
+     * number of times specified by
+     * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_MAXIMUM_DAILY_NOTIFICATION_COUNT_INT} and
+     * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_MAXIMUM_MONTHLY_NOTIFICATION_COUNT_INT}.
      * Subsequent attempts will return the same error until the request is no longer throttled
      * or throttling conditions change.
      */
@@ -17202,10 +17384,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4;
 
     /**
-     * Purchase premium capability failed because the user disabled the feature.
-     * Subsequent attempts will return the same error until the user re-enables the feature.
+     * Purchase premium capability failed because a foreground application requested the same
+     * capability. The notification for the current application will be dismissed and a new
+     * notification will be displayed to the user for the foreground application.
+     * Subsequent attempts will return
+     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS} until the foreground
+     * application's request is completed.
      */
-    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5;
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5;
 
     /**
      * Purchase premium capability failed because the user canceled the operation.
@@ -17252,7 +17438,8 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10;
 
     /**
-     * Purchase premium capability failed because the telephony service is down or unavailable.
+     * Purchase premium capability failed because the telephony service is unavailable
+     * or there was an error in the phone process.
      * Subsequent attempts will return the same error until request conditions are satisfied.
      */
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11;
@@ -17264,14 +17451,32 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12;
 
     /**
-     * Purchase premium capability failed because the network is congested.
+     * Purchase premium capability failed because the entitlement check failed.
      * Subsequent attempts will be throttled for the amount of time specified by
      * {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
      * and return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED}.
      * Throttling will be reevaluated when the network is no longer congested.
      */
-    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13;
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED = 13;
+
+    /**
+     * Purchase premium capability failed because the request was not made on the default data
+     * subscription, indicated by {@link SubscriptionManager#getDefaultDataSubscriptionId()}.
+     * Subsequent attempts will return the same error until the request is made on the default
+     * data subscription.
+     */
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION = 14;
+
+    /**
+     * Purchase premium capability was successful and is waiting for the network to setup the
+     * slicing configuration. If the setup is complete within the time specified by
+     * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG},
+     * subsequent requests will return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED}
+     * until the purchase expires. If the setup is not complete within the time specified above,
+     * applications can request the premium capability again.
+     */
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15;
 
     /**
      * Results of the purchase premium capability request.
@@ -17283,14 +17488,16 @@
             PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED})
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
     public @interface PurchasePremiumCapabilityResult {}
 
     /**
@@ -17311,8 +17518,8 @@
                 return "ALREADY_PURCHASED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS:
                 return "ALREADY_IN_PROGRESS";
-            case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED:
-                return "USER_DISABLED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN:
+                return "OVERRIDDEN";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED:
                 return "USER_CANCELED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED:
@@ -17327,8 +17534,12 @@
                 return "REQUEST_FAILED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE:
                 return "NETWORK_NOT_AVAILABLE";
-            case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED:
-                return "NETWORK_CONGESTED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED:
+                return "ENTITLEMENT_CHECK_FAILED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION:
+                return "NOT_DEFAULT_DATA_SUBSCRIPTION";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP:
+                return "PENDING_NETWORK_SETUP";
             default:
                 return "UNKNOWN (" + result + ")";
         }
@@ -17346,7 +17557,7 @@
      * @param callback The result of the purchase request.
      *                 One of {@link PurchasePremiumCapabilityResult}.
      * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE.
-     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid
+     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid.
      */
     @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE)
     public void purchasePremiumCapability(@PremiumCapability int capability,
diff --git a/telephony/java/android/telephony/TransportSelectorCallback.java b/telephony/java/android/telephony/TransportSelectorCallback.java
new file mode 100644
index 0000000..d396790
--- /dev/null
+++ b/telephony/java/android/telephony/TransportSelectorCallback.java
@@ -0,0 +1,60 @@
+/*
+ * 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.telephony;
+
+import android.annotation.NonNull;
+import android.telephony.Annotation.DisconnectCauses;
+
+import java.util.function.Consumer;
+
+/**
+ * A callback class used to receive the transport selection result.
+ * @hide
+ */
+public interface TransportSelectorCallback {
+    /**
+     * Notify that {@link DomainSelector} instance has been created for the selection request.
+     *
+     * @param selector the {@link DomainSelector} instance created.
+     */
+    void onCreated(@NonNull DomainSelector selector);
+
+    /**
+     * Notify that WLAN transport has been selected.
+     */
+    void onWlanSelected();
+
+    /**
+     * Notify that WWAN transport has been selected.
+     */
+    @NonNull WwanSelectorCallback onWwanSelected();
+
+    /**
+     * Notify that WWAN transport has been selected.
+     *
+     * @param consumer The callback to receive the result.
+     */
+    void onWwanSelected(Consumer<WwanSelectorCallback> consumer);
+
+    /**
+     * Notify that selection has terminated because there is no decision that can be made
+     * or a timeout has occurred. The call should be terminated when this method is called.
+     *
+     * @param cause indicates the reason.
+     */
+    void onSelectionTerminated(@DisconnectCauses int cause);
+}
diff --git a/telephony/java/android/telephony/WwanSelectorCallback.java b/telephony/java/android/telephony/WwanSelectorCallback.java
new file mode 100644
index 0000000..b3682ca
--- /dev/null
+++ b/telephony/java/android/telephony/WwanSelectorCallback.java
@@ -0,0 +1,51 @@
+/*
+ * 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.telephony;
+
+import android.annotation.NonNull;
+import android.os.CancellationSignal;
+import android.telephony.DomainSelectionService.EmergencyScanType;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A callback class used to receive the domain selection result.
+ * @hide
+ */
+public interface WwanSelectorCallback {
+    /**
+     * Notify the framework that the {@link DomainSelectionService} has requested an emergency
+     * network scan as part of selection.
+     *
+     * @param preferredNetworks the ordered list of preferred networks to scan.
+     * @param scanType indicates the scan preference, such as full service or limited service.
+     * @param signal notifies when the operation is canceled.
+     * @param consumer the handler of the response.
+     */
+    void onRequestEmergencyNetworkScan(@NonNull List<Integer> preferredNetworks,
+            @EmergencyScanType int scanType,
+            @NonNull CancellationSignal signal, @NonNull Consumer<EmergencyRegResult> consumer);
+
+    /**
+     * Notifies the FW that the domain has been selected. After this method is called,
+     * this interface can be discarded.
+     *
+     * @param domain The selected domain.
+     */
+    void onDomainSelected(@NetworkRegistrationInfo.Domain int domain);
+}
diff --git a/telephony/java/android/telephony/data/DataCallResponse.java b/telephony/java/android/telephony/data/DataCallResponse.java
index 73aff43..a834e2bb 100644
--- a/telephony/java/android/telephony/data/DataCallResponse.java
+++ b/telephony/java/android/telephony/data/DataCallResponse.java
@@ -468,14 +468,14 @@
         final boolean isQosBearerSessionsSame =
                 (mQosBearerSessions == null || other.mQosBearerSessions == null)
                 ? mQosBearerSessions == other.mQosBearerSessions
-                : mQosBearerSessions.size() == other.mQosBearerSessions.size()
-                && mQosBearerSessions.containsAll(other.mQosBearerSessions);
+                : (mQosBearerSessions.size() == other.mQosBearerSessions.size()
+                        && mQosBearerSessions.containsAll(other.mQosBearerSessions));
 
         final boolean isTrafficDescriptorsSame =
                 (mTrafficDescriptors == null || other.mTrafficDescriptors == null)
                 ? mTrafficDescriptors == other.mTrafficDescriptors
-                : mTrafficDescriptors.size() == other.mTrafficDescriptors.size()
-                && mTrafficDescriptors.containsAll(other.mTrafficDescriptors);
+                : (mTrafficDescriptors.size() == other.mTrafficDescriptors.size()
+                        && mTrafficDescriptors.containsAll(other.mTrafficDescriptors));
 
         return mCause == other.mCause
                 && mSuggestedRetryTime == other.mSuggestedRetryTime
@@ -504,10 +504,35 @@
 
     @Override
     public int hashCode() {
+        // Generate order-independent hashes for lists
+        int addressesHash = mAddresses.stream()
+                .map(LinkAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int dnsAddressesHash = mDnsAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int gatewayAddressesHash = mGatewayAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int pcscfAddressesHash = mPcscfAddresses.stream()
+                .map(InetAddress::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int qosBearerSessionsHash = mQosBearerSessions.stream()
+                .map(QosBearerSession::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
+        int trafficDescriptorsHash = mTrafficDescriptors.stream()
+                .map(TrafficDescriptor::hashCode)
+                .mapToInt(Integer::intValue)
+                .sum();
         return Objects.hash(mCause, mSuggestedRetryTime, mId, mLinkStatus, mProtocolType,
-                mInterfaceName, mAddresses, mDnsAddresses, mGatewayAddresses, mPcscfAddresses,
-                mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId, mDefaultQos,
-                mQosBearerSessions, mSliceInfo, mTrafficDescriptors);
+                mInterfaceName, addressesHash, dnsAddressesHash, gatewayAddressesHash,
+                pcscfAddressesHash, mMtu, mMtuV4, mMtuV6, mHandoverFailureMode, mPduSessionId,
+                mDefaultQos, qosBearerSessionsHash, mSliceInfo, trafficDescriptorsHash);
     }
 
     @Override
@@ -816,8 +841,8 @@
         /**
          * Set pdu session id.
          * <p/>
-         * The id must be between 1 and 15 when linked to a pdu session.  If no pdu session
-         * exists for the current data call, the id must be set to {@link PDU_SESSION_ID_NOT_SET}.
+         * The id must be between 1 and 15 when linked to a pdu session. If no pdu session
+         * exists for the current data call, the id must be set to {@link #PDU_SESSION_ID_NOT_SET}.
          *
          * @param pduSessionId Pdu Session Id of the data call.
          * @return The same instance of the builder.
@@ -858,6 +883,7 @@
          */
         public @NonNull Builder setQosBearerSessions(
                 @NonNull List<QosBearerSession> qosBearerSessions) {
+            Objects.requireNonNull(qosBearerSessions);
             mQosBearerSessions = qosBearerSessions;
             return this;
         }
@@ -891,6 +917,7 @@
          */
         public @NonNull Builder setTrafficDescriptors(
                 @NonNull List<TrafficDescriptor> trafficDescriptors) {
+            Objects.requireNonNull(trafficDescriptors);
             mTrafficDescriptors = trafficDescriptors;
             return this;
         }
diff --git a/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl b/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
index ba2b62d..8e27077 100644
--- a/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
+++ b/telephony/java/android/telephony/data/IQualifiedNetworksService.aidl
@@ -27,4 +27,5 @@
     oneway void createNetworkAvailabilityProvider(int slotId, IQualifiedNetworksServiceCallback callback);
     oneway void removeNetworkAvailabilityProvider(int slotId);
     oneway void reportThrottleStatusChanged(int slotId, in List<ThrottleStatus> statuses);
+    oneway void reportEmergencyDataNetworkPreferredTransportChanged (int slotId, int transportType);
 }
diff --git a/telephony/java/android/telephony/data/QosBearerFilter.java b/telephony/java/android/telephony/data/QosBearerFilter.java
index 0ab7b61..a0d9c1bd 100644
--- a/telephony/java/android/telephony/data/QosBearerFilter.java
+++ b/telephony/java/android/telephony/data/QosBearerFilter.java
@@ -130,6 +130,10 @@
         return precedence;
     }
 
+    public int getProtocol() {
+        return protocol;
+    }
+
     public static class PortRange implements Parcelable {
         int start;
         int end;
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index fb97336..56f0f9f 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -68,6 +68,7 @@
     private static final int QNS_REMOVE_ALL_NETWORK_AVAILABILITY_PROVIDERS          = 3;
     private static final int QNS_UPDATE_QUALIFIED_NETWORKS                          = 4;
     private static final int QNS_APN_THROTTLE_STATUS_CHANGED                        = 5;
+    private static final int QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED = 6;
 
     private final HandlerThread mHandlerThread;
 
@@ -193,6 +194,20 @@
         }
 
         /**
+         * The framework calls this method when the preferred transport type used to set up
+         * emergency data network is changed.
+         *
+         * This method is meant to be overridden.
+         *
+         * @param transportType transport type changed to be preferred
+         */
+        public void reportEmergencyDataNetworkPreferredTransportChanged(
+                @AccessNetworkConstants.TransportType int transportType) {
+            Log.d(TAG, "reportEmergencyDataNetworkPreferredTransportChanged: "
+                    + AccessNetworkConstants.transportTypeToString(transportType));
+        }
+
+        /**
          * Called when the qualified networks provider is removed. The extended class should
          * implement this method to perform cleanup works.
          */
@@ -237,6 +252,13 @@
                     }
                     break;
 
+                case QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED:
+                    if (provider != null) {
+                        int transportType = (int) message.arg2;
+                        provider.reportEmergencyDataNetworkPreferredTransportChanged(transportType);
+                    }
+                    break;
+
                 case QNS_REMOVE_NETWORK_AVAILABILITY_PROVIDER:
                     if (provider != null) {
                         provider.close();
@@ -332,6 +354,14 @@
             mHandler.obtainMessage(QNS_APN_THROTTLE_STATUS_CHANGED, slotIndex, 0, statuses)
                     .sendToTarget();
         }
+
+        @Override
+        public void reportEmergencyDataNetworkPreferredTransportChanged(int slotIndex,
+                @AccessNetworkConstants.TransportType int transportType) {
+            mHandler.obtainMessage(
+                    QNS_EMERGENCY_DATA_NETWORK_PREFERRED_TRANSPORT_CHANGED,
+                            slotIndex, transportType).sendToTarget();
+        }
     }
 
     private void log(String s) {
diff --git a/telephony/java/android/telephony/emergency/EmergencyNumber.java b/telephony/java/android/telephony/emergency/EmergencyNumber.java
index d9d5c14..e78a1e1 100644
--- a/telephony/java/android/telephony/emergency/EmergencyNumber.java
+++ b/telephony/java/android/telephony/emergency/EmergencyNumber.java
@@ -660,9 +660,6 @@
         if (!first.getEmergencyUrns().equals(second.getEmergencyUrns())) {
             return false;
         }
-        if (first.getEmergencyCallRouting() != second.getEmergencyCallRouting()) {
-            return false;
-        }
         // Never merge two numbers if one of them is from test mode but the other one is not;
         // This supports to remove a number from the test mode.
         if (first.isFromSources(EMERGENCY_NUMBER_SOURCE_TEST)
@@ -685,12 +682,18 @@
     public static EmergencyNumber mergeSameEmergencyNumbers(@NonNull EmergencyNumber first,
                                                             @NonNull EmergencyNumber second) {
         if (areSameEmergencyNumbers(first, second)) {
+            int routing = first.getEmergencyCallRouting();
+
+            if (second.isFromSources(EMERGENCY_NUMBER_SOURCE_DATABASE)) {
+                routing = second.getEmergencyCallRouting();
+            }
+
             return new EmergencyNumber(first.getNumber(), first.getCountryIso(), first.getMnc(),
                     first.getEmergencyServiceCategoryBitmask(),
                     first.getEmergencyUrns(),
                     first.getEmergencyNumberSourceBitmask()
                             | second.getEmergencyNumberSourceBitmask(),
-                    first.getEmergencyCallRouting());
+                    routing);
         }
         return null;
     }
diff --git a/telephony/java/android/telephony/euicc/EuiccCardManager.java b/telephony/java/android/telephony/euicc/EuiccCardManager.java
index 0a2bb3d..e61d1e6 100644
--- a/telephony/java/android/telephony/euicc/EuiccCardManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccCardManager.java
@@ -118,7 +118,10 @@
     /** Resets the default SM-DP+ address. */
     public static final int RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS = 1 << 2;
 
-    /** Result code when the requested profile is not found */
+    /** Result code when the requested profile is not found
+     * @deprecated use {@link #RESULT_PROFILE_DOES_NOT_EXIST}
+     **/
+    @Deprecated
     public static final int RESULT_PROFILE_NOT_FOUND = 1;
 
     /** Result code of execution with no error. */
@@ -133,6 +136,9 @@
     /** Result code indicating the caller is not the active LPA. */
     public static final int RESULT_CALLER_NOT_ALLOWED = -3;
 
+    /** Result code when the requested profile does not exist */
+    public static final int RESULT_PROFILE_DOES_NOT_EXIST = -4;
+
     /**
      * Callback to receive the result of an eUICC card API.
      *
@@ -224,7 +230,7 @@
 
     /**
      * Requests the enabled profile for a given port on an eUicc. Callback with result code
-     * {@link RESULT_PROFILE_NOT_FOUND} and {@code NULL} EuiccProfile if there is no enabled
+     * {@link RESULT_PROFILE_DOES_NOT_EXIST} and {@code NULL} EuiccProfile if there is no enabled
      * profile on the target port.
      *
      * @param cardId    The Id of the eUICC.
diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java
index f040153..33c86d8 100644
--- a/telephony/java/android/telephony/ims/ImsService.java
+++ b/telephony/java/android/telephony/ims/ImsService.java
@@ -50,7 +50,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.concurrent.CancellationException;
@@ -179,10 +178,9 @@
      * Used for logging purposes, see {@link #getCapabilitiesString(long)}
      * @hide
      */
-    private static final Map<Long, String> CAPABILITIES_LOG_MAP = new HashMap<Long, String>() {{
-            put(CAPABILITY_EMERGENCY_OVER_MMTEL, "EMERGENCY_OVER_MMTEL");
-            put(CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION");
-        }};
+    private static final Map<Long, String> CAPABILITIES_LOG_MAP = Map.of(
+            CAPABILITY_EMERGENCY_OVER_MMTEL, "EMERGENCY_OVER_MMTEL",
+            CAPABILITY_SIP_DELEGATE_CREATION, "SIP_DELEGATE_CREATION");
 
     /**
      * The intent that must be defined as an intent-filter in the AndroidManifest of the ImsService.
@@ -204,6 +202,7 @@
 
     private IImsServiceControllerListener mListener;
     private final Object mListenerLock = new Object();
+    private final Object mExecutorLock = new Object();
     private Executor mExecutor;
 
     /**
@@ -214,10 +213,6 @@
      * vendor use Runnable::run.
      */
     public ImsService() {
-        mExecutor = ImsService.this.getExecutor();
-        if (mExecutor == null) {
-            mExecutor = Runnable::run;
-        }
     }
 
     /**
@@ -356,7 +351,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 +365,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 +378,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 +422,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 +448,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.");
@@ -451,10 +456,10 @@
         }
     }
 
-    private IImsRcsFeature createRcsFeatureInternal(int slotId, int subI) {
-        RcsFeature f = createRcsFeatureForSubscription(slotId, subI);
+    private IImsRcsFeature createRcsFeatureInternal(int slotId, int subId) {
+        RcsFeature f = createRcsFeatureForSubscription(slotId, subId);
         if (f != null) {
-            f.setDefaultExecutor(mExecutor);
+            f.setDefaultExecutor(getCachedExecutor());
             setupFeature(f, slotId, ImsFeature.FEATURE_RCS);
             return f.getBinder();
         } else {
@@ -609,7 +614,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 +624,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/android/telephony/ims/RegistrationManager.java b/telephony/java/android/telephony/ims/RegistrationManager.java
index 090d413..9996b86 100644
--- a/telephony/java/android/telephony/ims/RegistrationManager.java
+++ b/telephony/java/android/telephony/ims/RegistrationManager.java
@@ -78,24 +78,22 @@
     /**@hide*/
     // Translate ImsRegistrationImplBase API to new AccessNetworkConstant because WLAN
     // and WWAN are more accurate constants.
-    Map<Integer, Integer> IMS_REG_TO_ACCESS_TYPE_MAP =
-            new HashMap<Integer, Integer>() {{
-                // Map NONE to -1 to make sure that we handle the REGISTRATION_TECH_NONE
-                // case, since it is defined.
-                put(ImsRegistrationImplBase.REGISTRATION_TECH_NONE,
-                        AccessNetworkConstants.TRANSPORT_TYPE_INVALID);
-                put(ImsRegistrationImplBase.REGISTRATION_TECH_LTE,
-                        AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
-                put(ImsRegistrationImplBase.REGISTRATION_TECH_NR,
-                        AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
-                put(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
-                        AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
-                /* As the cross sim will be using ePDG tunnel over internet, it behaves
-                   like IWLAN in most cases. Hence setting the access type as IWLAN
-                 */
-                put(ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM,
-                        AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
-            }};
+    Map<Integer, Integer> IMS_REG_TO_ACCESS_TYPE_MAP = Map.of(
+            // Map NONE to -1 to make sure that we handle the REGISTRATION_TECH_NONE
+            // case, since it is defined.
+            ImsRegistrationImplBase.REGISTRATION_TECH_NONE,
+                    AccessNetworkConstants.TRANSPORT_TYPE_INVALID,
+            ImsRegistrationImplBase.REGISTRATION_TECH_LTE,
+                    AccessNetworkConstants.TRANSPORT_TYPE_WWAN,
+            ImsRegistrationImplBase.REGISTRATION_TECH_NR,
+                    AccessNetworkConstants.TRANSPORT_TYPE_WWAN,
+            ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
+                    AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
+            /* As the cross sim will be using ePDG tunnel over internet, it behaves
+               like IWLAN in most cases. Hence setting the access type as IWLAN
+             */
+            ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM,
+                    AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
 
     /** @hide */
     @NonNull
diff --git a/telephony/java/android/telephony/ims/SrvccCall.aidl b/telephony/java/android/telephony/ims/SrvccCall.aidl
new file mode 100644
index 0000000..0f0a079
--- /dev/null
+++ b/telephony/java/android/telephony/ims/SrvccCall.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.ims;
+
+parcelable SrvccCall;
diff --git a/telephony/java/android/telephony/ims/SrvccCall.java b/telephony/java/android/telephony/ims/SrvccCall.java
new file mode 100644
index 0000000..cdc271e
--- /dev/null
+++ b/telephony/java/android/telephony/ims/SrvccCall.java
@@ -0,0 +1,153 @@
+/*
+ * 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.telephony.ims;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.Annotation.PreciseCallStates;
+
+import java.util.Objects;
+
+/**
+ * A Parcelable object to represent the current state of an IMS call that is being tracked
+ * in the ImsService when an SRVCC begins. This information will be delivered to modem.
+ * @see SrvccStartedCallback
+ *
+ * @hide
+ */
+@SystemApi
+public final class SrvccCall implements Parcelable {
+    private static final String TAG = "SrvccCall";
+
+    /** The IMS call profile */
+    private ImsCallProfile mImsCallProfile;
+
+    /** The IMS call id */
+    private String mCallId;
+
+    /** The call state */
+    private @PreciseCallStates int mCallState;
+
+    private SrvccCall(Parcel in) {
+        readFromParcel(in);
+    }
+
+    /**
+     * Constructs an instance of SrvccCall.
+     *
+     * @param callId the call ID associated with the IMS call
+     * @param callState the state of this IMS call
+     * @param imsCallProfile the profile associated with this IMS call
+     * @throws IllegalArgumentException if the callId or the imsCallProfile is null
+     */
+    public SrvccCall(@NonNull String callId, @PreciseCallStates int callState,
+            @NonNull ImsCallProfile imsCallProfile) {
+        if (callId == null) throw new IllegalArgumentException("callId is null");
+        if (imsCallProfile == null) throw new IllegalArgumentException("imsCallProfile is null");
+
+        mCallId = callId;
+        mCallState = callState;
+        mImsCallProfile = imsCallProfile;
+    }
+
+    /**
+     * @return the {@link ImsCallProfile} associated with this IMS call,
+     * which will be used to get the address, the name, and the audio direction
+     * including the call in pre-alerting state.
+     */
+    @NonNull
+    public ImsCallProfile getImsCallProfile() {
+        return mImsCallProfile;
+    }
+
+    /**
+     * @return the call ID associated with this IMS call.
+     *
+     * @see android.telephony.ims.stub.ImsCallSessionImplBase#getCallId().
+     */
+    @NonNull
+    public String getCallId() {
+        return mCallId;
+    }
+
+    /**
+     * @return the call state of the associated IMS call.
+     */
+    public @PreciseCallStates int getPreciseCallState() {
+        return mCallState;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "{ callId=" + mCallId
+                + ", callState=" + mCallState
+                + ", imsCallProfile=" + mImsCallProfile
+                + " }";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SrvccCall that = (SrvccCall) o;
+        return mImsCallProfile.equals(that.mImsCallProfile)
+                && mCallId.equals(that.mCallId)
+                && mCallState == that.mCallState;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(mImsCallProfile, mCallId);
+        result = 31 * result + mCallState;
+        return result;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(mCallId);
+        out.writeInt(mCallState);
+        out.writeParcelable(mImsCallProfile, 0);
+    }
+
+    private void readFromParcel(Parcel in) {
+        mCallId = in.readString();
+        mCallState = in.readInt();
+        mImsCallProfile = in.readParcelable(ImsCallProfile.class.getClassLoader(),
+                android.telephony.ims.ImsCallProfile.class);
+    }
+
+    public static final @android.annotation.NonNull Creator<SrvccCall> CREATOR =
+            new Creator<SrvccCall>() {
+        @Override
+        public SrvccCall createFromParcel(Parcel in) {
+            return new SrvccCall(in);
+        }
+
+        @Override
+        public SrvccCall[] newArray(int size) {
+            return new SrvccCall[size];
+        }
+    };
+}
diff --git a/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl b/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
index 801b81c..8519173 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelFeature.aidl
@@ -20,6 +20,7 @@
 import android.telephony.ims.aidl.IImsMmTelListener;
 import android.telephony.ims.aidl.IImsSmsListener;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.ISrvccStartedCallback;
 import android.telephony.ims.feature.CapabilityChangeRequest;
 import android.telephony.ims.RtpHeaderExtensionType;
 
@@ -55,6 +56,10 @@
             IImsCapabilityCallback c);
     oneway void queryCapabilityConfiguration(int capability, int radioTech,
             IImsCapabilityCallback c);
+    oneway void notifySrvccStarted(in ISrvccStartedCallback cb);
+    oneway void notifySrvccCompleted();
+    oneway void notifySrvccFailed();
+    oneway void notifySrvccCanceled();
     // SMS APIs
     void setSmsListener(IImsSmsListener l);
     oneway void sendSms(in int token, int messageRef, String format, String smsc, boolean retry,
diff --git a/telephony/java/android/telephony/ims/aidl/ISrvccStartedCallback.aidl b/telephony/java/android/telephony/ims/aidl/ISrvccStartedCallback.aidl
new file mode 100644
index 0000000..a173abf
--- /dev/null
+++ b/telephony/java/android/telephony/ims/aidl/ISrvccStartedCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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.telephony.ims.aidl;
+
+import android.telephony.ims.SrvccCall;
+
+import java.util.List;
+
+/**
+ * {@hide}
+ */
+oneway interface ISrvccStartedCallback {
+    void onSrvccCallNotified(in List<SrvccCall> profiles);
+}
diff --git a/telephony/java/android/telephony/ims/feature/ImsFeature.java b/telephony/java/android/telephony/ims/feature/ImsFeature.java
index f5b158f..174675f 100644
--- a/telephony/java/android/telephony/ims/feature/ImsFeature.java
+++ b/telephony/java/android/telephony/ims/feature/ImsFeature.java
@@ -34,7 +34,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -85,11 +84,10 @@
      * Used for logging purposes.
      * @hide
      */
-    public static final Map<Integer, String> FEATURE_LOG_MAP = new HashMap<Integer, String>() {{
-            put(FEATURE_EMERGENCY_MMTEL, "EMERGENCY_MMTEL");
-            put(FEATURE_MMTEL, "MMTEL");
-            put(FEATURE_RCS, "RCS");
-        }};
+    public static final Map<Integer, String> FEATURE_LOG_MAP = Map.of(
+            FEATURE_EMERGENCY_MMTEL, "EMERGENCY_MMTEL",
+            FEATURE_MMTEL, "MMTEL",
+            FEATURE_RCS, "RCS");
 
     /**
      * Integer values defining IMS features that are supported in ImsFeature.
@@ -145,11 +143,10 @@
      * Used for logging purposes.
      * @hide
      */
-    public static final Map<Integer, String> STATE_LOG_MAP = new HashMap<Integer, String>() {{
-            put(STATE_UNAVAILABLE, "UNAVAILABLE");
-            put(STATE_INITIALIZING, "INITIALIZING");
-            put(STATE_READY, "READY");
-        }};
+    public static final Map<Integer, String> STATE_LOG_MAP = Map.of(
+            STATE_UNAVAILABLE, "UNAVAILABLE",
+            STATE_INITIALIZING, "INITIALIZING",
+            STATE_READY, "READY");
 
     /**
      * Integer values defining the result codes that should be returned from
@@ -394,10 +391,12 @@
     @VisibleForTesting
     public void addImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) {
         try {
-            // If we have just connected, send queued status.
-            c.notifyImsFeatureStatus(getFeatureState());
-            // Add the callback if the callback completes successfully without a RemoteException.
-            mStatusCallbacks.register(c);
+            synchronized (mStatusCallbacks) {
+                // Add the callback if the callback completes successfully without a RemoteException
+                mStatusCallbacks.register(c);
+                // If we have just connected, send queued status.
+                c.notifyImsFeatureStatus(getFeatureState());
+            }
         } catch (RemoteException e) {
             Log.w(LOG_TAG, "Couldn't notify feature state: " + e.getMessage());
         }
@@ -409,7 +408,9 @@
      */
     @VisibleForTesting
     public void removeImsFeatureStatusCallback(@NonNull IImsFeatureStatusCallback c) {
-        mStatusCallbacks.unregister(c);
+        synchronized (mStatusCallbacks) {
+            mStatusCallbacks.unregister(c);
+        }
     }
 
     /**
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index c0ff12e..8147759 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -31,10 +31,12 @@
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsService;
 import android.telephony.ims.RtpHeaderExtensionType;
+import android.telephony.ims.SrvccCall;
 import android.telephony.ims.aidl.IImsCapabilityCallback;
 import android.telephony.ims.aidl.IImsMmTelFeature;
 import android.telephony.ims.aidl.IImsMmTelListener;
 import android.telephony.ims.aidl.IImsSmsListener;
+import android.telephony.ims.aidl.ISrvccStartedCallback;
 import android.telephony.ims.stub.ImsCallSessionImplBase;
 import android.telephony.ims.stub.ImsEcbmImplBase;
 import android.telephony.ims.stub.ImsMultiEndpointImplBase;
@@ -60,6 +62,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
@@ -289,6 +292,38 @@
                     "onSmsReady");
         }
 
+        @Override
+        public void notifySrvccStarted(final ISrvccStartedCallback cb) {
+            executeMethodAsyncNoException(
+                    () -> MmTelFeature.this.notifySrvccStarted(
+                            (profiles) -> {
+                                try {
+                                    cb.onSrvccCallNotified(profiles);
+                                } catch (Exception e) {
+                                    Log.e(LOG_TAG, "onSrvccCallNotified e=" + e);
+                                }
+                            }),
+                    "notifySrvccStarted");
+        }
+
+        @Override
+        public void notifySrvccCompleted() {
+            executeMethodAsyncNoException(
+                    () -> MmTelFeature.this.notifySrvccCompleted(), "notifySrvccCompleted");
+        }
+
+        @Override
+        public void notifySrvccFailed() {
+            executeMethodAsyncNoException(
+                    () -> MmTelFeature.this.notifySrvccFailed(), "notifySrvccFailed");
+        }
+
+        @Override
+        public void notifySrvccCanceled() {
+            executeMethodAsyncNoException(
+                    () -> MmTelFeature.this.notifySrvccCanceled(), "notifySrvccCanceled");
+        }
+
         // Call the methods with a clean calling identity on the executor and wait indefinitely for
         // the future to return.
         private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException {
@@ -969,6 +1004,75 @@
                 "Not implemented on device.");
     }
 
+    /**
+     * Notifies the MmTelFeature that the network has initiated an SRVCC (Single radio voice
+     * call continuity) for all IMS calls. When the network initiates an SRVCC, calls from
+     * the LTE domain are handed over to the legacy circuit switched domain. The modem requires
+     * knowledge of ongoing calls in the IMS domain in order to complete the SRVCC operation.
+     * <p>
+     * @param consumer The callback used to notify the framework of the list of IMS calls and their
+     * state at the time of the SRVCC.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void notifySrvccStarted(@NonNull Consumer<List<SrvccCall>> consumer) {
+        // Base Implementation - Should be overridden by IMS service
+    }
+
+    /**
+     * Notifies the MmTelFeature that the SRVCC is completed and the calls have been moved
+     * over to the circuit-switched domain.
+     * {@link android.telephony.CarrierConfigManager.ImsVoice#KEY_SRVCC_TYPE_INT_ARRAY}
+     * specifies the calls can be moved. Other calls will be disconnected.
+     * <p>
+     * The MmTelFeature may now release all resources related to the IMS calls.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void notifySrvccCompleted() {
+        // Base Implementation - Should be overridden by IMS service
+    }
+
+    /**
+     * Notifies the MmTelFeature that the SRVCC has failed.
+     *
+     * The handover can fail by encountering a failure at the radio level
+     * or temporary MSC server internal errors in handover procedure.
+     * Refer to 3GPP TS 23.216 section 8 Handover Failure.
+     * <p>
+     * IMS service will recover and continue calls over IMS.
+     * Per TS 24.237 12.2.4.2, UE shall send SIP UPDATE request containing the reason-text
+     * set to "failure to transition to CS domain".
+     *
+     * @hide
+     */
+    @SystemApi
+    public void notifySrvccFailed() {
+        // Base Implementation - Should be overridden by IMS service
+    }
+
+    /**
+     * Notifies the MmTelFeature that the SRVCC has been canceled.
+     *
+     * Since the state of network can be changed, the network can decide to terminate
+     * the handover procedure before its completion and to return to its state before the handover
+     * procedure was triggered.
+     * Refer to 3GPP TS 23.216 section 8.1.3 Handover Cancellation.
+     *
+     * <p>
+     * IMS service will recover and continue calls over IMS.
+     * Per TS 24.237 12.2.4.2, UE shall send SIP UPDATE request containing the reason-text
+     * set to "handover canceled".
+     *
+     * @hide
+     */
+    @SystemApi
+    public void notifySrvccCanceled() {
+        // Base Implementation - Should be overridden by IMS service
+    }
+
     private void setSmsListener(IImsSmsListener listener) {
         getSmsImplementation().registerSmsListener(listener);
     }
diff --git a/telephony/java/com/android/internal/telephony/IDomainSelectionServiceController.aidl b/telephony/java/com/android/internal/telephony/IDomainSelectionServiceController.aidl
new file mode 100644
index 0000000..8ad79ed
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IDomainSelectionServiceController.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.telephony;
+
+import android.telephony.BarringInfo;
+import android.telephony.DomainSelectionService.SelectionAttributes;
+import android.telephony.ServiceState;
+
+import com.android.internal.telephony.ITransportSelectorCallback;
+
+oneway interface IDomainSelectionServiceController {
+    void selectDomain(in SelectionAttributes attr, in ITransportSelectorCallback callback);
+    void updateServiceState(int slotId, int subId, in ServiceState serviceState);
+    void updateBarringInfo(int slotId, int subId, in BarringInfo info);
+}
diff --git a/telephony/java/com/android/internal/telephony/IDomainSelector.aidl b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl
new file mode 100644
index 0000000..d94840b
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IDomainSelector.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.telephony;
+
+import android.telephony.DomainSelectionService.SelectionAttributes;
+
+oneway interface IDomainSelector {
+    void cancelSelection();
+    void reselectDomain(in SelectionAttributes attr);
+    void finishSelection();
+}
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 917f35b..4752cca 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -18,6 +18,7 @@
 
 import android.telephony.SubscriptionInfo;
 import android.os.ParcelUuid;
+import android.os.UserHandle;
 import com.android.internal.telephony.ISetOpportunisticDataCallback;
 
 interface ISub {
@@ -316,4 +317,26 @@
      * @throws SecurityException if doesn't have MODIFY_PHONE_STATE or Carrier Privileges
      */
     int setUsageSetting(int usageSetting, int subId, String callingPackage);
+
+     /**
+      * Set userHandle for this subscription.
+      *
+      * @param userHandle the user handle for this subscription
+      * @param subId the unique SubscriptionInfo index in database
+      *
+      * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
+      * @throws IllegalArgumentException if subId is invalid.
+      */
+    int setSubscriptionUserHandle(in UserHandle userHandle, int subId);
+
+    /**
+     * Get UserHandle for this subscription
+     *
+     * @param subId the unique SubscriptionInfo index in database
+     * @return userHandle associated with this subscription.
+     *
+     * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
+     * @throws IllegalArgumentException if subId is invalid.
+     */
+     UserHandle getSubscriptionUserHandle(int subId);
 }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 648866b..abf4cde 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2159,6 +2159,12 @@
     int getRadioHalVersion();
 
     /**
+     * Get the HAL Version of a specific service
+     * encoded as 100 * MAJOR_VERSION + MINOR_VERSION or -1 if unknown
+     */
+    int getHalVersion(int service);
+
+    /**
      * Get the current calling package name.
      */
     String getCurrentPackageName();
diff --git a/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl b/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl
new file mode 100644
index 0000000..aca83f4
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/ITransportSelectorCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.telephony;
+
+import com.android.internal.telephony.IDomainSelector;
+import com.android.internal.telephony.ITransportSelectorResultCallback;
+import com.android.internal.telephony.IWwanSelectorCallback;
+
+interface ITransportSelectorCallback {
+    oneway void onCreated(in IDomainSelector selector);
+    oneway void onWlanSelected();
+    IWwanSelectorCallback onWwanSelected();
+    oneway void onWwanSelectedAsync(in ITransportSelectorResultCallback cb);
+    oneway void onSelectionTerminated(int cause);
+}
diff --git a/telephony/java/com/android/internal/telephony/ITransportSelectorResultCallback.aidl b/telephony/java/com/android/internal/telephony/ITransportSelectorResultCallback.aidl
new file mode 100644
index 0000000..1460b45
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/ITransportSelectorResultCallback.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony;
+
+import com.android.internal.telephony.IWwanSelectorCallback;
+
+oneway interface ITransportSelectorResultCallback {
+    void onCompleted(in IWwanSelectorCallback cb);
+}
diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl
new file mode 100644
index 0000000..339fbee
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IWwanSelectorCallback.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.telephony;
+
+import com.android.internal.telephony.IWwanSelectorResultCallback;
+
+oneway interface IWwanSelectorCallback {
+    void onRequestEmergencyNetworkScan(in int[] preferredNetworks,
+            int scanType, in IWwanSelectorResultCallback cb);
+    void onDomainSelected(int domain);
+    void onCancel();
+}
diff --git a/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl
new file mode 100644
index 0000000..0d61fcb
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/IWwanSelectorResultCallback.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony;
+
+import android.telephony.EmergencyRegResult;
+
+oneway interface IWwanSelectorResultCallback {
+    void onComplete(in EmergencyRegResult result);
+}
diff --git a/telephony/java/com/android/internal/telephony/PhoneConstants.java b/telephony/java/com/android/internal/telephony/PhoneConstants.java
index b2f7be6..0f83a05 100644
--- a/telephony/java/com/android/internal/telephony/PhoneConstants.java
+++ b/telephony/java/com/android/internal/telephony/PhoneConstants.java
@@ -239,4 +239,10 @@
     public static final int CELL_OFF_FLAG = 0;
     public static final int CELL_ON_FLAG = 1;
     public static final int CELL_OFF_DUE_TO_AIRPLANE_MODE_FLAG = 2;
+
+    /** The key to specify the selected domain for dialing calls. */
+    public static final String EXTRA_DIAL_DOMAIN = "dial_domain";
+
+    /** The key to specify the emergency service category */
+    public static final String EXTRA_EMERGENCY_SERVICE_CATEGORY = "emergency_service_category";
 }
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index 9892671..0c14dba 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -536,6 +536,16 @@
     int RIL_REQUEST_TRIGGER_EMERGENCY_NETWORK_SCAN = 230;
     int RIL_REQUEST_CANCEL_EMERGENCY_NETWORK_SCAN = 231;
     int RIL_REQUEST_EXIT_EMERGENCY_MODE = 232;
+    int RIL_REQUEST_SET_SRVCC_CALL_INFO = 233;
+    int RIL_REQUEST_UPDATE_IMS_REGISTRATION_INFO = 234;
+    int RIL_REQUEST_START_IMS_TRAFFIC = 235;
+    int RIL_REQUEST_STOP_IMS_TRAFFIC = 236;
+    int RIL_REQUEST_SEND_ANBR_QUERY = 237;
+    int RIL_REQUEST_TRIGGER_EPS_FALLBACK = 238;
+    int RIL_REQUEST_SET_NULL_CIPHER_AND_INTEGRITY_ENABLED = 239;
+    int RIL_REQUEST_UPDATE_IMS_CALL_STATUS = 240;
+    int RIL_REQUEST_SET_N1_MODE_ENABLED = 241;
+    int RIL_REQUEST_IS_N1_MODE_ENABLED = 242;
 
     /* Responses begin */
     int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
@@ -607,4 +617,7 @@
     int RIL_UNSOL_REGISTRATION_FAILED = 1104;
     int RIL_UNSOL_BARRING_INFO_CHANGED = 1105;
     int RIL_UNSOL_EMERGENCY_NETWORK_SCAN_RESULT = 1106;
+    int RIL_UNSOL_TRIGGER_IMS_DEREGISTRATION = 1107;
+    int RIL_UNSOL_CONNECTION_SETUP_FAILURE = 1108;
+    int RIL_UNSOL_NOTIFY_ANBR = 1109;
 }
diff --git a/tests/CanvasCompare/src/com/android/test/hwuicompare/DisplayModifier.java b/tests/CanvasCompare/src/com/android/test/hwuicompare/DisplayModifier.java
index 0f4e122..4bcf5a4 100644
--- a/tests/CanvasCompare/src/com/android/test/hwuicompare/DisplayModifier.java
+++ b/tests/CanvasCompare/src/com/android/test/hwuicompare/DisplayModifier.java
@@ -16,14 +16,16 @@
 
 package com.android.test.hwuicompare;
 
-import java.util.LinkedHashMap;
-import java.util.Map.Entry;
+import static java.util.Map.entry;
 
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.RectF;
 import android.util.Log;
 
+import java.util.Map;
+import java.util.Map.Entry;
+
 public abstract class DisplayModifier {
 
     // automated tests ignore any combination of operations that don't together return TOTAL_MASK
@@ -76,41 +78,36 @@
     };
 
     @SuppressWarnings("serial")
-    private static final LinkedHashMap<String, LinkedHashMap<String, DisplayModifier>> gMaps = new LinkedHashMap<String, LinkedHashMap<String, DisplayModifier>>() {
-        {
-            put("aa", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("true", new DisplayModifier() {
+    private static final Map<String, Map<String, DisplayModifier>> gMaps = Map.of(
+            "aa", Map.of(
+                    "true", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setAntiAlias(true);
                         }
-                    });
-                    put("false", new DisplayModifier() {
+                    },
+                    "false", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setAntiAlias(false);
                         }
-                    });
-                }
-            });
-            put("style", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("fill", new DisplayModifier() {
+                    }),
+            "style", Map.of(
+                    "fill", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStyle(Paint.Style.FILL);
                         }
-                    });
-                    put("stroke", new DisplayModifier() {
+                    },
+                    "stroke", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStyle(Paint.Style.STROKE);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_WIDTH_BIT; }
-                    });
-                    put("fillAndStroke", new DisplayModifier() {
+                    },
+                    "fillAndStroke", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStyle(Paint.Style.FILL_AND_STROKE);
@@ -118,131 +115,118 @@
 
                         @Override
                         protected int mask() { return SWEEP_STROKE_WIDTH_BIT; }
-                    });
-                }
-            });
-            put("strokeWidth", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("hair", new DisplayModifier() {
+                    }),
+            "strokeWidth", Map.of(
+                    "hair", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeWidth(0);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_WIDTH_BIT; }
-                    });
-                    put("0.3", new DisplayModifier() {
+                    },
+                    "0.3", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeWidth(0.3f);
                         }
-                    });
-                    put("1", new DisplayModifier() {
+                    },
+                    "1", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeWidth(1);
                         }
-                    });
-                    put("5", new DisplayModifier() {
+                    },
+                    "5", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeWidth(5);
                         }
-                    });
-                    put("30", new DisplayModifier() {
+                    },
+                    "30", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeWidth(30);
                         }
-                    });
-                }
-            });
-            put("strokeCap", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("butt", new DisplayModifier() {
+                    }),
+            "strokeCap", Map.of(
+                    "butt", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeCap(Paint.Cap.BUTT);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_CAP_BIT; }
-                    });
-                    put("round", new DisplayModifier() {
+                    },
+                    "round", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeCap(Paint.Cap.ROUND);
                         }
-                    });
-                    put("square", new DisplayModifier() {
+                    },
+                    "square", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeCap(Paint.Cap.SQUARE);
                         }
-                    });
-                }
-            });
-            put("strokeJoin", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("bevel", new DisplayModifier() {
+                    }),
+            "strokeJoin", Map.of(
+                    "bevel", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeJoin(Paint.Join.BEVEL);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_JOIN_BIT; }
-                    });
-                    put("round", new DisplayModifier() {
+                    },
+                    "round", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeJoin(Paint.Join.ROUND);
                         }
-                    });
-                    put("miter", new DisplayModifier() {
+                    },
+                    "miter", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setStrokeJoin(Paint.Join.MITER);
                         }
-                    });
+                    }),
                     // TODO: add miter0, miter1 etc to test miter distances
-                }
-            });
-
-            put("transform", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("noTransform", new DisplayModifier() {
+            "transform", Map.of(
+                    "noTransform", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {}
                         @Override
                         protected int mask() { return SWEEP_TRANSFORM_BIT; };
-                    });
-                    put("rotate5", new DisplayModifier() {
+                    },
+                    "rotate5", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.rotate(5);
                         }
-                    });
-                    put("rotate45", new DisplayModifier() {
+                    },
+                    "rotate45", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.rotate(45);
                         }
-                    });
-                    put("rotate90", new DisplayModifier() {
+                    },
+                    "rotate90", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.rotate(90);
                             canvas.translate(0, -200);
                         }
-                    });
-                    put("scale2x2", new DisplayModifier() {
+                    },
+                    "scale2x2", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.scale(2, 2);
                         }
                         @Override
                         protected int mask() { return SWEEP_TRANSFORM_BIT; };
-                    });
-                    put("rot20scl1x4", new DisplayModifier() {
+                    },
+                    "rot20scl1x4", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.rotate(20);
@@ -250,180 +234,167 @@
                         }
                         @Override
                         protected int mask() { return SWEEP_TRANSFORM_BIT; };
-                    });
-                }
-            });
-
-            put("shader", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("noShader", new DisplayModifier() {
+                    }),
+            "shader", Map.ofEntries(
+                    entry("noShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {}
                         @Override
                         protected int mask() { return SWEEP_SHADER_BIT; };
-                    });
-                    put("repeatShader", new DisplayModifier() {
+                    }),
+                    entry("repeatShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mRepeatShader);
                         }
                         @Override
                         protected int mask() { return SWEEP_SHADER_BIT; };
-                    });
-                    put("translatedShader", new DisplayModifier() {
+                    }),
+                    entry("translatedShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mTranslatedShader);
                         }
-                    });
-                    put("scaledShader", new DisplayModifier() {
+                    }),
+                    entry("scaledShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mScaledShader);
                         }
-                    });
-                    put("horGradient", new DisplayModifier() {
+                    }),
+                    entry("horGradient", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mHorGradient);
                         }
-                    });
-                    put("diagGradient", new DisplayModifier() {
+                    }),
+                    entry("diagGradient", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mDiagGradient);
                         }
                         @Override
                         protected int mask() { return SWEEP_SHADER_BIT; };
-                    });
-                    put("vertGradient", new DisplayModifier() {
+                    }),
+                    entry("vertGradient", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mVertGradient);
                         }
-                    });
-                    put("radGradient", new DisplayModifier() {
+                    }),
+                    entry("radGradient", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mRadGradient);
                         }
-                    });
-                    put("sweepGradient", new DisplayModifier() {
+                    }),
+                    entry("sweepGradient", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mSweepGradient);
                         }
-                    });
-                    put("composeShader", new DisplayModifier() {
+                    }),
+                    entry("composeShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mComposeShader);
                         }
-                    });
-                    put("bad composeShader", new DisplayModifier() {
+                    }),
+                    entry("bad composeShader", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mBadComposeShader);
                         }
-                    });
-                    put("bad composeShader 2", new DisplayModifier() {
+                    }),
+                    entry("bad composeShader 2", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setShader(ResourceModifiers.instance().mAnotherBadComposeShader);
                         }
-                    });
-                }
-            });
-
-            // FINAL MAP: DOES ACTUAL DRAWING
-            put("drawing", new LinkedHashMap<String, DisplayModifier>() {
-                {
-                    put("roundRect", new DisplayModifier() {
+                    })),
+            "drawing", Map.ofEntries(
+                    entry("roundRect", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawRoundRect(gRect, 20, 20, paint);
                         }
-                    });
-                    put("rect", new DisplayModifier() {
+                    }),
+                    entry("rect", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawRect(gRect, paint);
                         }
                         @Override
                         protected int mask() { return SWEEP_SHADER_BIT | SWEEP_STROKE_CAP_BIT; };
-                    });
-                    put("circle", new DisplayModifier() {
+                    }),
+                    entry("circle", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawCircle(100, 100, 75, paint);
                         }
-                    });
-                    put("oval", new DisplayModifier() {
+                    }),
+                    entry("oval", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawOval(gRect, paint);
                         }
-                    });
-                    put("lines", new DisplayModifier() {
+                    }),
+                    entry("lines", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawLines(gLinePts, paint);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_CAP_BIT; };
-                    });
-                    put("plusPoints", new DisplayModifier() {
+                    }),
+                    entry("plusPoints", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawPoints(gPts, paint);
                         }
-                    });
-                    put("text", new DisplayModifier() {
+                    }),
+                    entry("text", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setTextSize(36);
                             canvas.drawText("TEXTTEST", 0, 50, paint);
                         }
-                    });
-                    put("shadowtext", new DisplayModifier() {
+                    }),
+                    entry("shadowtext", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             paint.setTextSize(36);
                             paint.setShadowLayer(3.0f, 0.0f, 3.0f, 0xffff00ff);
                             canvas.drawText("TEXTTEST", 0, 50, paint);
                         }
-                    });
-                    put("bitmapMesh", new DisplayModifier() {
+                    }),
+                    entry("bitmapMesh", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawBitmapMesh(ResourceModifiers.instance().mBitmap, 3, 3,
                                     ResourceModifiers.instance().mBitmapVertices, 0, null, 0, null);
                         }
-                    });
-                    put("arc", new DisplayModifier() {
+                    }),
+                    entry("arc", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawArc(gRect, 260, 285, false, paint);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_CAP_BIT; };
-                    });
-                    put("arcFromCenter", new DisplayModifier() {
+                    }),
+                    entry("arcFromCenter", new DisplayModifier() {
                         @Override
                         public void modifyDrawing(Paint paint, Canvas canvas) {
                             canvas.drawArc(gRect, 260, 285, true, paint);
                         }
                         @Override
                         protected int mask() { return SWEEP_STROKE_JOIN_BIT; };
-                    });
-                }
-            });
+                    })));
             // WARNING: DON'T PUT MORE MAPS BELOW THIS
-        }
-    };
 
-    private static LinkedHashMap<String, DisplayModifier> getMapAtIndex(int index) {
-        for (LinkedHashMap<String, DisplayModifier> map : gMaps.values()) {
+    private static Map<String, DisplayModifier> getMapAtIndex(int index) {
+        for (Map<String, DisplayModifier> map : gMaps.values()) {
             if (index == 0) {
                 return map;
             }
@@ -439,7 +410,7 @@
     private static boolean stepInternal(boolean forward) {
         int modifierMapIndex = gMaps.size() - 1;
         while (modifierMapIndex >= 0) {
-            LinkedHashMap<String, DisplayModifier> map = getMapAtIndex(modifierMapIndex);
+            Map<String, DisplayModifier> map = getMapAtIndex(modifierMapIndex);
             mIndices[modifierMapIndex] += (forward ? 1 : -1);
 
             if (mIndices[modifierMapIndex] >= 0 && mIndices[modifierMapIndex] < map.size()) {
@@ -471,7 +442,7 @@
     private static boolean checkModificationStateMask() {
         int operatorMask = 0x0;
         int mapIndex = 0;
-        for (LinkedHashMap<String, DisplayModifier> map : gMaps.values()) {
+        for (Map<String, DisplayModifier> map : gMaps.values()) {
             int displayModifierIndex = mIndices[mapIndex];
             for (Entry<String, DisplayModifier> modifierEntry : map.entrySet()) {
                 if (displayModifierIndex == 0) {
@@ -488,7 +459,7 @@
 
     public static void apply(Paint paint, Canvas canvas) {
         int mapIndex = 0;
-        for (LinkedHashMap<String, DisplayModifier> map : gMaps.values()) {
+        for (Map<String, DisplayModifier> map : gMaps.values()) {
             int displayModifierIndex = mIndices[mapIndex];
             for (Entry<String, DisplayModifier> modifierEntry : map.entrySet()) {
                 if (displayModifierIndex == 0) {
@@ -510,7 +481,7 @@
         String[][] keys = new String[gMaps.size()][];
 
         int i = 0;
-        for (LinkedHashMap<String, DisplayModifier> map : gMaps.values()) {
+        for (Map<String, DisplayModifier> map : gMaps.values()) {
             keys[i] = new String[map.size()];
             int j = 0;
             for (String key : map.keySet()) {
diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml
index d91aa1e..a7d6a01 100644
--- a/tests/FlickerTests/AndroidTest.xml
+++ b/tests/FlickerTests/AndroidTest.xml
@@ -15,6 +15,8 @@
         <option name="run-command" value="cmd window tracing frame" />
         <!-- ensure lock screen mode is swipe -->
         <option name="run-command" value="locksettings set-disabled false" />
+        <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+        <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
         <!-- restart launcher to activate TAPL -->
         <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
     </target_preparer>
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)
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
index 0837c00..5686965 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt
@@ -44,7 +44,7 @@
     OpenAppAfterCameraTest(testSpec) {
     @Before
     override fun before() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
+        Assume.assumeTrue(isShellTransitionsEnabled)
     }
 
     @FlakyTest
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
index 73e6d22..5e6fc21 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt
@@ -19,11 +19,11 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.RequiresDevice
 import android.view.Surface
+import android.view.WindowManagerPolicyConstants
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -68,7 +68,6 @@
                     tapl.setExpectedRotation(Surface.ROTATION_0)
                 }
                 RemoveAllTasksButHomeRule.removeAllTasksButHome()
-                this.setRotation(testSpec.startRotation)
             }
             transitions {
                 tapl
@@ -187,7 +186,13 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams(): Collection<FlickerTestParameter> {
-            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests()
+            return FlickerTestParameterFactory.getInstance()
+                // TAPL fails on landscape mode b/240916028
+                .getConfigNonRotationTests(
+                    supportedNavigationModes = listOf(
+                        WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY
+                    )
+                )
         }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
index 09d7637..0edbc86 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
@@ -97,8 +97,8 @@
         super.statusBarLayerPositionAtEnd()
 
     /** {@inheritDoc} */
-    @Postsubmit
     @Test
+    @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
index c10b993..4ee1283 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.wm.flicker.launch
 
+import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
@@ -71,7 +72,7 @@
         }
 
     @Test
-    @Postsubmit
+    @FlakyTest(bugId = 227143265)
     fun showWhenLockedAppWindowBecomesVisible() {
         testSpec.assertWm {
             this.hasNoVisibleAppWindow()
@@ -83,7 +84,7 @@
     }
 
     @Test
-    @Postsubmit
+    @FlakyTest(bugId = 227143265)
     fun showWhenLockedAppLayerBecomesVisible() {
         testSpec.assertLayers {
             this.isInvisible(showWhenLockedApp)
@@ -98,11 +99,17 @@
     @Presubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible()
 
     /** {@inheritDoc} */
-    @Postsubmit
+    @FlakyTest(bugId = 227143265)
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 209599395)
+    @Test
+    override fun navBarLayerIsVisibleAtStartAndEnd() =
+        super.navBarLayerIsVisibleAtStartAndEnd()
+
     companion object {
         /**
          * Creates the test configurations.
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 6e935d1..83823ea 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -254,6 +254,8 @@
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
+            <meta-data android:name="android.app.shortcuts"
+                       android:resource="@xml/shortcuts" />
         </activity>
         <activity android:name=".SendNotificationActivity"
                   android:taskAffinity="com.android.server.wm.flicker.testapp.SendNotificationActivity"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/strings.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/strings.xml
new file mode 100644
index 0000000..24830de
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Label for split screen shortcut-->
+    <string name="split_screen_shortcut_label">Split Screen Secondary Activity</string>
+</resources>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/xml/shortcuts.xml b/tests/FlickerTests/test-apps/flickerapp/res/xml/shortcuts.xml
new file mode 100644
index 0000000..804ec99
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/xml/shortcuts.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+    <shortcut
+        android:shortcutId="split_screen_shortcut"
+        android:shortcutShortLabel="@string/split_screen_shortcut_label">
+        <intent
+            android:action="android.intent.action.VIEW"
+            android:targetPackage="com.android.server.wm.flicker.testapp"
+            android:targetClass="com.android.server.wm.flicker.testapp.SplitScreenSecondaryActivity" />
+    </shortcut>
+</shortcuts>
\ No newline at end of file
diff --git a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java
index 2ad0da9..8b9c020 100644
--- a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java
+++ b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java
@@ -58,7 +58,7 @@
         Object hash = getProperty(props, "__hash__");
 
         if (name instanceof String && hash instanceof Integer) {
-            return String.format(Locale.US, "%s@%x", name, hash);
+            return String.format(Locale.US, "%s@%x", name, (Integer) hash);
         } else {
             return null;
         }
diff --git a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java
new file mode 100644
index 0000000..0f96634
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.os;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.platform.test.annotations.Presubmit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TimeoutRecord}. */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class TimeoutRecordTest {
+
+    @Test
+    public void forBroadcastReceiver_returnsCorrectTimeoutRecord() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass"));
+
+        TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent);
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER);
+        assertEquals(record.mReason,
+                "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example"
+                        + ".app/ExampleClass }");
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forBroadcastReceiver_withTimeoutDurationMs_returnsCorrectTimeoutRecord() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass"));
+
+        TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent, 1000L);
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER);
+        assertEquals(record.mReason,
+                "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example"
+                        + ".app/ExampleClass }, waited 1000ms");
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forInputDispatchNoFocusedWindow_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forInputDispatchNoFocusedWindow("Test ANR reason");
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.INPUT_DISPATCH_NO_FOCUSED_WINDOW);
+        assertEquals(record.mReason,
+                "Test ANR reason");
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forInputDispatchWindowUnresponsive_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forInputDispatchWindowUnresponsive("Test ANR reason");
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.INPUT_DISPATCH_WINDOW_UNRESPONSIVE);
+        assertEquals(record.mReason, "Test ANR reason");
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forServiceExec_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forServiceExec("Test ANR reason");
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.SERVICE_EXEC);
+        assertEquals(record.mReason, "Test ANR reason");
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forServiceStartWithEndTime_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forServiceStartWithEndTime("Test ANR reason", 1000L);
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.SERVICE_START);
+        assertEquals(record.mReason, "Test ANR reason");
+        assertEquals(record.mEndUptimeMillis, 1000L);
+        assertTrue(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forContentProvider_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forContentProvider("Test ANR reason");
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.CONTENT_PROVIDER);
+        assertEquals(record.mReason, "Test ANR reason");
+        assertFalse(record.mEndTakenBeforeLocks);
+    }
+
+    @Test
+    public void forApp_returnsCorrectTimeoutRecord() {
+        TimeoutRecord record = TimeoutRecord.forApp("Test ANR reason");
+
+        assertNotNull(record);
+        assertEquals(record.mKind, TimeoutRecord.TimeoutKind.APP_REGISTERED);
+        assertEquals(record.mReason, "Test ANR reason");
+        assertFalse(record.mEndTakenBeforeLocks);
+    }
+}
diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java
index 4de51fb..43dc9de 100644
--- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java
+++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/HomeActivity.java
@@ -140,9 +140,9 @@
         handleNextBenchmark();
     }
 
+    @SuppressWarnings("MissingSuperCall") // TODO: Fix me
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-
     }
 
     private void handleNextBenchmark() {
diff --git a/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java b/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java
index c16efbd..d015a56 100644
--- a/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java
+++ b/tests/JankBench/app/src/main/java/com/android/benchmark/app/RunLocalBenchmarksActivity.java
@@ -367,6 +367,7 @@
         }
     }
 
+    @SuppressWarnings("MissingSuperCall") // TODO: Fix me
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
diff --git a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
index dd9b294..afaeca1 100644
--- a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
+++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
@@ -15,7 +15,6 @@
  */
 package com.android.frameworks.perftests.job;
 
-
 import android.app.job.JobInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -46,7 +45,8 @@
 public class JobStorePerfTests {
     private static final String SOURCE_PACKAGE = "com.android.frameworks.perftests.job";
     private static final int SOURCE_USER_ID = 0;
-    private static final int CALLING_UID = 10079;
+    private static final int BASE_CALLING_UID = 10079;
+    private static final int MAX_UID_COUNT = 10;
 
     private static Context sContext;
     private static File sTestDir;
@@ -65,10 +65,10 @@
         sJobStore = JobStore.initAndGetForTesting(sContext, sTestDir);
 
         for (int i = 0; i < 50; i++) {
-            sFewJobs.add(createJobStatus("fewJobs", i));
+            sFewJobs.add(createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT)));
         }
         for (int i = 0; i < 500; i++) {
-            sManyJobs.add(createJobStatus("manyJobs", i));
+            sManyJobs.add(createJobStatus("manyJobs", i, BASE_CALLING_UID + (i % MAX_UID_COUNT)));
         }
     }
 
@@ -104,6 +104,64 @@
         runPersistedJobWriting(sManyJobs);
     }
 
+    private void runPersistedJobWriting_delta(List<JobStatus> jobList,
+            List<JobStatus> jobAdditions, List<JobStatus> jobRemovals) {
+        final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
+
+        long elapsedTimeNs = 0;
+        while (benchmarkState.keepRunning(elapsedTimeNs)) {
+            sJobStore.clearForTesting();
+            for (JobStatus job : jobList) {
+                sJobStore.addForTesting(job);
+            }
+            sJobStore.writeStatusToDiskForTesting();
+
+            for (JobStatus job : jobAdditions) {
+                sJobStore.addForTesting(job);
+            }
+            for (JobStatus job : jobRemovals) {
+                sJobStore.removeForTesting(job);
+            }
+
+            final long startTime = SystemClock.elapsedRealtimeNanos();
+            sJobStore.writeStatusToDiskForTesting();
+            final long endTime = SystemClock.elapsedRealtimeNanos();
+            elapsedTimeNs = endTime - startTime;
+        }
+    }
+
+    @Test
+    public void testPersistedJobWriting_delta_fewJobs() {
+        List<JobStatus> additions = new ArrayList<>();
+        List<JobStatus> removals = new ArrayList<>();
+        final int numModifiedUids = MAX_UID_COUNT / 2;
+        for (int i = 0; i < sFewJobs.size() / 3; ++i) {
+            JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids));
+            if (i % 2 == 0) {
+                additions.add(job);
+            } else {
+                removals.add(job);
+            }
+        }
+        runPersistedJobWriting_delta(sFewJobs, additions, removals);
+    }
+
+    @Test
+    public void testPersistedJobWriting_delta_manyJobs() {
+        List<JobStatus> additions = new ArrayList<>();
+        List<JobStatus> removals = new ArrayList<>();
+        final int numModifiedUids = MAX_UID_COUNT / 2;
+        for (int i = 0; i < sManyJobs.size() / 3; ++i) {
+            JobStatus job = createJobStatus("fewJobs", i, BASE_CALLING_UID + (i % numModifiedUids));
+            if (i % 2 == 0) {
+                additions.add(job);
+            } else {
+                removals.add(job);
+            }
+        }
+        runPersistedJobWriting_delta(sManyJobs, additions, removals);
+    }
+
     private void runPersistedJobReading(List<JobStatus> jobList, boolean rtcIsGood) {
         final ManualBenchmarkState benchmarkState = mPerfManualStatusReporter.getBenchmarkState();
 
@@ -144,12 +202,12 @@
         runPersistedJobReading(sManyJobs, false);
     }
 
-    private static JobStatus createJobStatus(String testTag, int jobId) {
+    private static JobStatus createJobStatus(String testTag, int jobId, int callingUid) {
         JobInfo jobInfo = new JobInfo.Builder(jobId,
                 new ComponentName(sContext, "JobStorePerfTestJobService"))
                 .setPersisted(true)
                 .build();
         return JobStatus.createFromJobInfo(
-                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
     }
 }
diff --git a/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java b/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java
index 8afe841..17fa210 100644
--- a/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java
+++ b/tests/MirrorSurfaceTest/src/com/google/android/test/mirrorsurface/MirrorSurfaceActivity.java
@@ -295,8 +295,8 @@
     private void updateMirror(Rect displayFrame, float scale) {
         if (displayFrame.isEmpty()) {
             Rect bounds = mWindowBounds;
-            int defaultCropW = Math.round(bounds.width() / 2);
-            int defaultCropH = Math.round(bounds.height() / 2);
+            int defaultCropW = bounds.width() / 2;
+            int defaultCropH = bounds.height() / 2;
             displayFrame.set(0, 0, defaultCropW, defaultCropH);
         }
 
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 96bbf82..f8d885a 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -44,14 +44,14 @@
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
 import android.util.LongArrayQueue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.PackageWatchdog.HealthCheckState;
 import com.android.server.PackageWatchdog.MonitoredPackage;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
diff --git a/tests/RenderThreadTest/src/com/example/renderthread/MainActivity.java b/tests/RenderThreadTest/src/com/example/renderthread/MainActivity.java
index 241206d..65b7549 100644
--- a/tests/RenderThreadTest/src/com/example/renderthread/MainActivity.java
+++ b/tests/RenderThreadTest/src/com/example/renderthread/MainActivity.java
@@ -24,18 +24,14 @@
     static final String KEY_NAME = "name";
     static final String KEY_CLASS = "clazz";
 
-    static Map<String,?> make(String name) {
-        Map<String,Object> ret = new HashMap<String,Object>();
-        ret.put(KEY_NAME, name);
-        return ret;
-    }
-
-    @SuppressWarnings("serial")
-    static final ArrayList<Map<String,?>> SAMPLES = new ArrayList<Map<String,?>>() {{
+    static final ArrayList<Map<String, ?>> SAMPLES = new ArrayList<>();
+    static {
         for (int i = 1; i < 25; i++) {
-            add(make("List Item: " + i));
+            Map<String, Object> sample = new HashMap<String, Object>();
+            sample.put(KEY_NAME, "List Item: " + i);
+            SAMPLES.add(sample);
         }
-    }};
+    }
 
     Handler mHandler = new Handler();
 
diff --git a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
index 35859fe..ec1709c 100644
--- a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
+++ b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
@@ -18,12 +18,14 @@
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,6 +37,7 @@
  */
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class MultiUserRollbackTest extends BaseHostJUnit4Test {
+    private boolean mSupportMultiUsers;
     // The user that was running originally when the test starts.
     private int mOriginalUserId;
     private int mSecondaryUserId = -1;
@@ -46,14 +49,20 @@
 
     @After
     public void tearDown() throws Exception {
-        removeSecondaryUserIfNecessary();
-        runPhaseForUsers("cleanUp", mOriginalUserId);
-        uninstallPackage("com.android.cts.install.lib.testapp.A");
-        uninstallPackage("com.android.cts.install.lib.testapp.B");
+        if (mSupportMultiUsers) {
+            removeSecondaryUserIfNecessary();
+            runPhaseForUsers("cleanUp", mOriginalUserId);
+            uninstallPackage("com.android.cts.install.lib.testapp.A");
+            uninstallPackage("com.android.cts.install.lib.testapp.B");
+        }
     }
 
     @Before
     public void setup() throws Exception {
+        assumeTrue("Device does not support multiple users",
+                getDevice().isMultiUserSupported());
+
+        mSupportMultiUsers = true;
         mOriginalUserId = getDevice().getCurrentUser();
         createAndStartSecondaryUser();
         installPackage("RollbackTest.apk", "--user all");
@@ -90,6 +99,7 @@
     }
 
     @Test
+    @Ignore
     public void testBadUpdateRollback() throws Exception {
         // Need to switch user in order to send broadcasts in device tests
         assertTrue(getDevice().switchUser(mSecondaryUserId));
diff --git a/tests/RollbackTest/SampleRollbackApp/Android.bp b/tests/RollbackTest/SampleRollbackApp/Android.bp
index a18488d..074c7bc 100644
--- a/tests/RollbackTest/SampleRollbackApp/Android.bp
+++ b/tests/RollbackTest/SampleRollbackApp/Android.bp
@@ -29,4 +29,5 @@
     resource_dirs: ["res"],
     certificate: "platform",
     sdk_version: "system_current",
+    min_sdk_version: "29",
 }
diff --git a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
index 5a135c9..7fe4bae 100644
--- a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
+++ b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
@@ -16,7 +16,7 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.sample.rollbackapp" >
-    <uses-permission android:name="android.permission.TEST_MANAGE_ROLLBACKS" />
+    <uses-permission android:name="android.permission.MANAGE_ROLLBACKS" />
     <application
         android:label="@string/title_activity_main">
         <activity
@@ -28,4 +28,4 @@
             </intent-filter>
         </activity>
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
index 916551a..79a2f1f 100644
--- a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
+++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
@@ -75,6 +75,7 @@
                         String rollbackStatus = "FAILED";
                         if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) {
                             rollbackStatus = "SUCCESS";
+                            mTriggerRollbackButton.setClickable(false);
                         }
                         makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus);
                     }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED);
diff --git a/tests/SmokeTestApps/src/com/android/smoketest/triggers/CrashyApp.java b/tests/SmokeTestApps/src/com/android/smoketest/triggers/CrashyApp.java
index c11b0f3..f85fb0f 100644
--- a/tests/SmokeTestApps/src/com/android/smoketest/triggers/CrashyApp.java
+++ b/tests/SmokeTestApps/src/com/android/smoketest/triggers/CrashyApp.java
@@ -30,6 +30,7 @@
         setContentView(tv);
     }
 
+    @SuppressWarnings("ReturnValueIgnored")
     @Override
     public void onResume() {
         ((String) null).length();
diff --git a/tests/TouchLatency/Android.bp b/tests/TouchLatency/Android.bp
index 3a9e240..4ef1ead 100644
--- a/tests/TouchLatency/Android.bp
+++ b/tests/TouchLatency/Android.bp
@@ -12,6 +12,7 @@
     manifest: "app/src/main/AndroidManifest.xml",
     // omit gradle 'build' dir
     srcs: ["app/src/main/java/**/*.java"],
+    static_libs: ["com.google.android.material_material"],
     resource_dirs: ["app/src/main/res"],
     aaptflags: ["--auto-add-overlay"],
     sdk_version: "current",
diff --git a/tests/TouchLatency/app/build.gradle b/tests/TouchLatency/app/build.gradle
index f5ae6f4..129baab 100644
--- a/tests/TouchLatency/app/build.gradle
+++ b/tests/TouchLatency/app/build.gradle
@@ -6,7 +6,7 @@
 
     defaultConfig {
         applicationId "com.prefabulated.touchlatency"
-        minSdkVersion 28
+        minSdkVersion 30
         targetSdkVersion 33
         versionCode 1
         versionName "1.0"
@@ -17,4 +17,9 @@
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }
+
+    dependencies {
+        implementation 'androidx.appcompat:appcompat:1.5.1'
+        implementation 'com.google.android.material:material:1.6.0'
+    }
 }
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
index 6ab3b3e..2e93c87 100644
--- a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
@@ -16,7 +16,6 @@
 
 package com.prefabulated.touchlatency;
 
-import android.app.Activity;
 import android.app.ActivityOptions;
 import android.content.Intent;
 import android.hardware.display.DisplayManager;
@@ -30,25 +29,49 @@
 import android.view.Window;
 import android.view.WindowManager;
 
-public class TouchLatencyActivity extends Activity {
-    private Mode mDisplayModes[];
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.slider.RangeSlider;
+import com.google.android.material.slider.RangeSlider.OnChangeListener;
+
+public class TouchLatencyActivity extends AppCompatActivity {
+    private static final int REFRESH_RATE_SLIDER_MIN = 20;
+    private static final int REFRESH_RATE_SLIDER_STEP = 5;
+
+    private Menu mMenu;
+    private Mode[] mDisplayModes;
     private int mCurrentModeIndex;
+    private float mSliderPreferredRefreshRate;
     private DisplayManager mDisplayManager;
+
     private final DisplayManager.DisplayListener mDisplayListener =
             new DisplayManager.DisplayListener() {
         @Override
         public void onDisplayAdded(int i) {
-            invalidateOptionsMenu();
+            updateOptionsMenu();
         }
 
         @Override
         public void onDisplayRemoved(int i) {
-            invalidateOptionsMenu();
+            updateOptionsMenu();
         }
 
         @Override
         public void onDisplayChanged(int i) {
-            invalidateOptionsMenu();
+            updateOptionsMenu();
+        }
+    };
+
+    private final RangeSlider.OnChangeListener mRefreshRateSliderListener = new OnChangeListener() {
+        @Override
+        public void onValueChange(@NonNull RangeSlider slider, float value, boolean fromUser) {
+            if (value == mSliderPreferredRefreshRate) return;
+
+            mSliderPreferredRefreshRate = value;
+            WindowManager.LayoutParams w = getWindow().getAttributes();
+            w.preferredRefreshRate = mSliderPreferredRefreshRate;
+            getWindow().setAttributes(w);
         }
     };
 
@@ -75,17 +98,23 @@
         Trace.endSection();
     }
 
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu");
-        // Inflate the menu; this adds items to the action bar if it is present.
-        getMenuInflater().inflate(R.menu.menu_touch_latency, menu);
+    public void updateOptionsMenu() {
         if (mDisplayModes.length > 1) {
-            MenuItem menuItem = menu.findItem(R.id.display_mode);
+            MenuItem menuItem = mMenu.findItem(R.id.display_mode);
             Mode currentMode = getWindowManager().getDefaultDisplay().getMode();
             updateDisplayMode(menuItem, currentMode);
         }
-        updateMultiDisplayMenu(menu.findItem(R.id.multi_display));
+        updateRefreshRateMenu(mMenu.findItem(R.id.frame_rate));
+        updateMultiDisplayMenu(mMenu.findItem(R.id.multi_display));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu");
+        mMenu = menu;
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_touch_latency, mMenu);
+        updateOptionsMenu();
         Trace.endSection();
         return true;
     }
@@ -96,6 +125,32 @@
         menuItem.setVisible(true);
     }
 
+    private float getHighestRefreshRate() {
+        float maxRefreshRate = 0;
+        for (Display.Mode mode : getDisplay().getSupportedModes()) {
+            if (sameSizeMode(mode) && mode.getRefreshRate() > maxRefreshRate) {
+                maxRefreshRate = mode.getRefreshRate();
+            }
+        }
+        return maxRefreshRate;
+    }
+
+    private void updateRefreshRateMenu(MenuItem item) {
+        item.setActionView(R.layout.refresh_rate_layout);
+        RangeSlider slider = item.getActionView().findViewById(R.id.slider_from_layout);
+        slider.addOnChangeListener(mRefreshRateSliderListener);
+
+        float highestRefreshRate = getHighestRefreshRate();
+        slider.setValueFrom(REFRESH_RATE_SLIDER_MIN);
+        slider.setValueTo(highestRefreshRate);
+        slider.setStepSize(REFRESH_RATE_SLIDER_STEP);
+        if (mSliderPreferredRefreshRate < REFRESH_RATE_SLIDER_MIN
+                || mSliderPreferredRefreshRate > highestRefreshRate) {
+            mSliderPreferredRefreshRate = highestRefreshRate;
+        }
+        slider.setValues(mSliderPreferredRefreshRate);
+    }
+
     private void updateMultiDisplayMenu(MenuItem item) {
         item.setVisible(mDisplayManager.getDisplays().length > 1);
     }
@@ -105,6 +160,12 @@
         mDisplayManager.registerDisplayListener(mDisplayListener, new Handler());
     }
 
+    private boolean sameSizeMode(Display.Mode mode) {
+        Mode currentMode = mDisplayModes[mCurrentModeIndex];
+        return currentMode.getPhysicalHeight() == mode.getPhysicalHeight()
+            && currentMode.getPhysicalWidth() == mode.getPhysicalWidth();
+    }
+
     public void changeDisplayMode(MenuItem item) {
         Window w = getWindow();
         WindowManager.LayoutParams params = w.getAttributes();
@@ -112,10 +173,7 @@
         int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length;
         while (modeIndex != mCurrentModeIndex) {
             // skip modes with different resolutions
-            Mode currentMode = mDisplayModes[mCurrentModeIndex];
-            Mode nextMode = mDisplayModes[modeIndex];
-            if (currentMode.getPhysicalHeight() == nextMode.getPhysicalHeight()
-                    && currentMode.getPhysicalWidth() == nextMode.getPhysicalWidth()) {
+            if (sameSizeMode(mDisplayModes[modeIndex])) {
                 break;
             }
             modeIndex = (modeIndex + 1) % mDisplayModes.length;
diff --git a/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml b/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml
new file mode 100644
index 0000000..bb9ce60
--- /dev/null
+++ b/tests/TouchLatency/app/src/main/res/layout/refresh_rate_layout.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+        <com.google.android.material.slider.RangeSlider
+            android:id="@+id/slider_from_layout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:tickColor="@color/cardview_light_background"
+            app:trackColor="@color/cardview_light_background"
+            app:thumbColor="@color/cardview_dark_background"
+            android:visibility="visible"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
index abc7fd5..7169021 100644
--- a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
+++ b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
@@ -14,21 +14,25 @@
      limitations under the License.
 -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools" tools:context=".TouchLatencyActivity">
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="101"
-        android:showAsAction="always"
-        android:title="@string/mode"/>
+        android:title="@string/mode"
+        app:showAsAction="always" />
+    <item
+        android:id="@+id/frame_rate"
+        android:title="@string/frame_rate"
+        app:showAsAction="collapseActionView" />
     <item
         android:id="@+id/display_mode"
-        android:showAsAction="ifRoom"
         android:title="@string/display_mode"
-        android:visible="false"/>
-
+        android:visible="false"
+        app:showAsAction="always" />
     <item
         android:id="@+id/multi_display"
-        android:showAsAction="ifRoom"
         android:title="@string/multi_display"
-        android:visible="false"/>
+        android:visible="false"
+        app:showAsAction="ifRoom" />
 </menu>
diff --git a/tests/TouchLatency/app/src/main/res/values/strings.xml b/tests/TouchLatency/app/src/main/res/values/strings.xml
index 5ee86d8..cad2df7 100644
--- a/tests/TouchLatency/app/src/main/res/values/strings.xml
+++ b/tests/TouchLatency/app/src/main/res/values/strings.xml
@@ -18,5 +18,6 @@
 
     <string name="mode">Touch</string>
     <string name="display_mode">Mode</string>
+    <string name="frame_rate">Frame Rate</string>
     <string name="multi_display">multi-display</string>
 </resources>
diff --git a/tests/TouchLatency/app/src/main/res/values/styles.xml b/tests/TouchLatency/app/src/main/res/values/styles.xml
index 22da7c1..b23a87e 100644
--- a/tests/TouchLatency/app/src/main/res/values/styles.xml
+++ b/tests/TouchLatency/app/src/main/res/values/styles.xml
@@ -16,7 +16,7 @@
 <resources>
 
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar">
+    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
         <!-- Customize your theme here. -->
     </style>
 
diff --git a/tests/TouchLatency/gradle.properties b/tests/TouchLatency/gradle.properties
index 1d3591c..ccd5dda 100644
--- a/tests/TouchLatency/gradle.properties
+++ b/tests/TouchLatency/gradle.properties
@@ -15,4 +15,5 @@
 # When configured, Gradle will run in incubating parallel mode.
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
\ No newline at end of file
+# org.gradle.parallel=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
index cbe13d9..650686f 100644
--- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -373,6 +373,10 @@
         try (InputStream is = new FileInputStream(certPath)) {
             result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
         }
+        // /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts
+        final String copiedCert = "/data/fonts/debug_cert.der";
+        runShellCommand("cp " + certPath + " " + copiedCert, null);
+        runShellCommand("cmd font install-debug-cert " + copiedCert, null);
         // Assert that there are no errors.
         assertThat(result.second).isEmpty();
         String keyId = result.first.trim();
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
index 3da8b46..133c176 100644
--- a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
@@ -147,12 +147,39 @@
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
-        return registerReceiver(receiver, filter, null, null);
+        return registerReceiver(receiver, filter, null, null, 0);
+    }
+
+    /**
+     * Registers the specified {@code receiver} to listen for broadcasts that match the {@code
+     * filter} in the current process.
+     *
+     * <p>Since this method only listens for broadcasts in the current process, the provided {@code
+     * flags} are ignored; this method is primarily intended to allow receivers that register with
+     * flags to register in the current process during tests.
+     */
+    @Override
+    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+        return registerReceiver(receiver, filter, null, null, flags);
     }
 
     @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler) {
+        return registerReceiver(receiver, filter, broadcastPermission, scheduler, 0);
+    }
+
+    /**
+     * Registers the specified {@code receiver} to listen for broadcasts that match the {@code
+     * filter} to run in the context of the specified {@code scheduler} in the current process.
+     *
+     * <p>Since this method only listens for broadcasts in the current process, the provided {@code
+     * flags} are ignored; this method is primarily intended to allow receivers that register with
+     * flags to register in the current process during tests.
+     */
+    @Override
+    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+            String broadcastPermission, Handler scheduler, int flags) {
         synchronized (mInterceptors) {
             mInterceptors.add(new BroadcastInterceptor(receiver, filter, scheduler));
         }
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index f924b2e..ad06830 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -637,8 +637,7 @@
         final BroadcastReceiver receiver = getPackageChangeReceiver();
 
         verify(mMockContext).registerReceiver(any(), argThat(filter -> {
-            return filter.hasAction(Intent.ACTION_PACKAGE_REMOVED)
-                    && filter.hasAction(Intent.ACTION_PACKAGE_REMOVED);
+            return filter.hasAction(Intent.ACTION_PACKAGE_REMOVED);
         }), any(), any());
 
         receiver.onReceive(mMockContext, new Intent(Intent.ACTION_PACKAGE_REMOVED));
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 2a450ba..1d7fd1d 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -46,6 +46,13 @@
   string version = 2;
 }
 
+// References to non local resources
+message DynamicRefTable {
+  PackageId package_id = 1;
+  string package_name = 2;
+}
+
+
 // Top level message representing a resource table.
 message ResourceTable {
   // The string pool containing source paths referenced throughout the resource table. This does
@@ -60,6 +67,8 @@
 
   // The version fingerprints of the tools that built the resource table.
   repeated ToolFingerprint tool_fingerprint = 4;
+
+  repeated DynamicRefTable dynamic_ref_table = 5;
 }
 
 // A package ID in the range [0x00, 0xff].
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index aeedf8b..52e113e 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -21,7 +21,9 @@
 #include "Diagnostics.h"
 #include "LoadedApk.h"
 #include "ValueVisitor.h"
+#include "android-base/file.h"
 #include "android-base/macros.h"
+#include "android-base/stringprintf.h"
 #include "androidfw/StringPiece.h"
 #include "cmd/Util.h"
 #include "format/binary/TableFlattener.h"
@@ -353,6 +355,27 @@
   return 0;
 }
 
+bool ExtractResourceConfig(const std::string& path, IAaptContext* context,
+                           TableFlattenerOptions& out_options) {
+  std::string content;
+  if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) {
+    context->GetDiagnostics()->Error(android::DiagMessage(path) << "failed reading config file");
+    return false;
+  }
+  std::unordered_set<ResourceName> resources_exclude_list;
+  bool result = ParseResourceConfig(content, context, resources_exclude_list,
+                                    out_options.name_collapse_exemptions);
+  if (!result) {
+    return false;
+  }
+  if (!resources_exclude_list.empty()) {
+    context->GetDiagnostics()->Error(android::DiagMessage(path)
+                                     << "Unsupported '#remove' directive in resource config.");
+    return false;
+  }
+  return true;
+}
+
 const char* ConvertCommand::kOutputFormatProto = "proto";
 const char* ConvertCommand::kOutputFormatBinary = "binary";
 
@@ -401,6 +424,11 @@
   if (force_sparse_encoding_) {
     table_flattener_options_.sparse_entries = SparseEntriesMode::Forced;
   }
+  if (resources_config_path_) {
+    if (!ExtractResourceConfig(*resources_config_path_, &context, table_flattener_options_)) {
+      return 1;
+    }
+  }
 
   return Convert(&context, apk.get(), writer.get(), format, table_flattener_options_,
                  xml_flattener_options_);
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 6c09649..15fe11f 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -50,6 +50,25 @@
         android::base::StringPrintf("Preserve raw attribute values in xml files when using the"
             " '%s' output format", kOutputFormatBinary),
         &xml_flattener_options_.keep_raw_values);
+    AddOptionalFlag("--resources-config-path",
+                    "Path to the resources.cfg file containing the list of resources and \n"
+                    "directives to each resource. \n"
+                    "Format: type/resource_name#[directive][,directive]",
+                    &resources_config_path_);
+    AddOptionalSwitch(
+        "--collapse-resource-names",
+        "Collapses resource names to a single value in the key string pool. Resources can \n"
+        "be exempted using the \"no_collapse\" directive in a file specified by "
+        "--resources-config-path.",
+        &table_flattener_options_.collapse_key_stringpool);
+    AddOptionalSwitch(
+        "--deduplicate-entry-values",
+        "Whether to deduplicate pairs of resource entry and value for simple resources.\n"
+        "This is recommended to be used together with '--collapse-resource-names' flag or for\n"
+        "APKs where resource names are manually collapsed. For such APKs this flag allows to\n"
+        "store the same resource value only once in resource table which decreases APK size.\n"
+        "Has no effect on APKs where resource names are kept.",
+        &table_flattener_options_.deduplicate_entry_values);
     AddOptionalSwitch("-v", "Enables verbose logging", &verbose_);
   }
 
@@ -66,6 +85,7 @@
   bool verbose_ = false;
   bool enable_sparse_encoding_ = false;
   bool force_sparse_encoding_ = false;
+  std::optional<std::string> resources_config_path_;
 };
 
 int Convert(IAaptContext* context, LoadedApk* input, IArchiveWriter* output_writer,
diff --git a/tools/aapt2/cmd/Convert_test.cpp b/tools/aapt2/cmd/Convert_test.cpp
index 27df8c1..2c9388b 100644
--- a/tools/aapt2/cmd/Convert_test.cpp
+++ b/tools/aapt2/cmd/Convert_test.cpp
@@ -17,13 +17,18 @@
 #include "Convert.h"
 
 #include "LoadedApk.h"
+#include "test/Common.h"
 #include "test/Test.h"
 #include "ziparchive/zip_archive.h"
 
+using testing::AnyOfArray;
 using testing::Eq;
 using testing::Ne;
+using testing::Not;
+using testing::SizeIs;
 
 namespace aapt {
+using namespace aapt::test;
 
 using ConvertTest = CommandTestFixture;
 
@@ -145,4 +150,76 @@
   EXPECT_THAT(count, Eq(1));
 }
 
+TEST_F(ConvertTest, ConvertWithResourceNameCollapsing) {
+  StdErrDiagnostics diag;
+  const std::string compiled_files_dir = GetTestPath("compiled");
+  ASSERT_TRUE(CompileFile(GetTestPath("res/values/values.xml"),
+                          R"(<resources>
+                               <string name="first">string</string>
+                               <string name="second">string</string>
+                               <string name="third">another string</string>
+
+                               <bool name="bool1">true</bool>
+                               <bool name="bool2">true</bool>
+                               <bool name="bool3">true</bool>
+
+                               <integer name="int1">10</integer>
+                               <integer name="int2">10</integer>
+                             </resources>)",
+                          compiled_files_dir, &diag));
+  std::string resource_config_path = GetTestPath("resource-config");
+  WriteFile(resource_config_path, "integer/int1#no_collapse\ninteger/int2#no_collapse");
+
+  const std::string proto_apk = GetTestPath("proto.apk");
+  std::vector<std::string> link_args = {
+      "--proto-format", "--manifest", GetDefaultManifest(kDefaultPackageName), "-o", proto_apk,
+  };
+  ASSERT_TRUE(Link(link_args, compiled_files_dir, &diag));
+
+  const std::string binary_apk = GetTestPath("binary.apk");
+  std::vector<android::StringPiece> convert_args = {"-o",
+                                                    binary_apk,
+                                                    "--output-format",
+                                                    "binary",
+                                                    "--collapse-resource-names",
+                                                    "--deduplicate-entry-values",
+                                                    "--resources-config-path",
+                                                    resource_config_path,
+                                                    proto_apk};
+  ASSERT_THAT(ConvertCommand().Execute(convert_args, &std::cerr), Eq(0));
+
+  std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(binary_apk, &diag);
+  for (const auto& package : apk->GetResourceTable()->packages) {
+    for (const auto& type : package->types) {
+      switch (type->named_type.type) {
+        case ResourceType::kBool:
+          EXPECT_THAT(type->entries, SizeIs(3));
+          for (const auto& entry : type->entries) {
+            auto value = ValueCast<BinaryPrimitive>(entry->FindValue({})->value.get())->value;
+            EXPECT_THAT(value.data, Eq(0xffffffffu));
+          }
+          break;
+        case ResourceType::kString:
+          EXPECT_THAT(type->entries, SizeIs(3));
+          for (const auto& entry : type->entries) {
+            auto value = ValueCast<String>(entry->FindValue({})->value.get())->value;
+            EXPECT_THAT(entry->name, Not(AnyOfArray({"first", "second", "third"})));
+            EXPECT_THAT(*value, AnyOfArray({"string", "another string"}));
+          }
+          break;
+        case ResourceType::kInteger:
+          EXPECT_THAT(type->entries, SizeIs(2));
+          for (const auto& entry : type->entries) {
+            auto value = ValueCast<BinaryPrimitive>(entry->FindValue({})->value.get())->value;
+            EXPECT_THAT(entry->name, AnyOfArray({"int1", "int2"}));
+            EXPECT_THAT(value.data, Eq(10));
+          }
+          break;
+        default:
+          break;
+      }
+    }
+  }
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 116dcd6..a8d2299 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1085,6 +1085,10 @@
       const auto localeconfig_entry =
           ResolveTableEntry(context_, &final_table_, localeconfig_reference);
       if (!localeconfig_entry) {
+        // If locale config is resolved from external symbols - skip validation.
+        if (context_->GetExternalSymbols()->FindByReference(*localeconfig_reference)) {
+          return true;
+        }
         context_->GetDiagnostics()->Error(
             android::DiagMessage(localeConfig->compiled_value->GetSource())
             << "no localeConfig entry");
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 254f3a5..28fcc1a 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -840,6 +840,43 @@
   ASSERT_TRUE(Link(link1_args, &diag));
 }
 
+TEST_F(LinkTest, LocaleConfigVerificationExternalSymbol) {
+  StdErrDiagnostics diag;
+  const std::string base_files_dir = GetTestPath("base");
+  ASSERT_TRUE(CompileFile(GetTestPath("res/xml/locales_config.xml"), R"(
+    <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+      <locale android:name="en-US"/>
+      <locale android:name="pt"/>
+      <locale android:name="es-419"/>
+      <locale android:name="zh-Hans-SG"/>
+    </locale-config>)",
+                          base_files_dir, &diag));
+  const std::string base_apk = GetTestPath("base.apk");
+  std::vector<std::string> link_args = {
+      "--manifest",
+      GetDefaultManifest("com.aapt2.app"),
+      "-o",
+      base_apk,
+  };
+  ASSERT_TRUE(Link(link_args, base_files_dir, &diag));
+
+  const std::string localeconfig_manifest = GetTestPath("localeconfig_manifest.xml");
+  const std::string out_apk = GetTestPath("out.apk");
+  WriteFile(localeconfig_manifest, android::base::StringPrintf(R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.aapt2.app">
+
+      <application
+        android:localeConfig="@xml/locales_config">
+      </application>
+    </manifest>)"));
+  link_args = LinkCommandBuilder(this)
+                  .SetManifestFile(localeconfig_manifest)
+                  .AddParameter("-I", base_apk)
+                  .Build(out_apk);
+  ASSERT_TRUE(Link(link_args, &diag));
+}
+
 TEST_F(LinkTest, LocaleConfigWrongTag) {
   StdErrDiagnostics diag;
   const std::string compiled_files_dir = GetTestPath("compiled");
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 9feaf52..042926c 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -305,51 +305,14 @@
   OptimizeContext* context_;
 };
 
-bool ParseConfig(const std::string& content, IAaptContext* context, OptimizeOptions* options) {
-  size_t line_no = 0;
-  for (StringPiece line : util::Tokenize(content, '\n')) {
-    line_no++;
-    line = util::TrimWhitespace(line);
-    if (line.empty()) {
-      continue;
-    }
-
-    auto split_line = util::Split(line, '#');
-    if (split_line.size() < 2) {
-      context->GetDiagnostics()->Error(android::DiagMessage(line) << "No # found in line");
-      return false;
-    }
-    StringPiece resource_string = split_line[0];
-    StringPiece directives = split_line[1];
-    ResourceNameRef resource_name;
-    if (!ResourceUtils::ParseResourceName(resource_string, &resource_name)) {
-      context->GetDiagnostics()->Error(android::DiagMessage(line) << "Malformed resource name");
-      return false;
-    }
-    if (!resource_name.package.empty()) {
-      context->GetDiagnostics()->Error(android::DiagMessage(line)
-                                       << "Package set for resource. Only use type/name");
-      return false;
-    }
-    for (StringPiece directive : util::Tokenize(directives, ',')) {
-      if (directive == "remove") {
-        options->resources_exclude_list.insert(resource_name.ToResourceName());
-      } else if (directive == "no_collapse" || directive == "no_obfuscate") {
-        options->table_flattener_options.name_collapse_exemptions.insert(
-            resource_name.ToResourceName());
-      }
-    }
-  }
-  return true;
-}
-
 bool ExtractConfig(const std::string& path, IAaptContext* context, OptimizeOptions* options) {
   std::string content;
   if (!android::base::ReadFileToString(path, &content, true /*follow_symlinks*/)) {
     context->GetDiagnostics()->Error(android::DiagMessage(path) << "failed reading config file");
     return false;
   }
-  return ParseConfig(content, context, options);
+  return ParseResourceConfig(content, context, options->resources_exclude_list,
+                             options->table_flattener_options.name_collapse_exemptions);
 }
 
 bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index 790bb74..794a87b 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -123,6 +123,14 @@
     AddOptionalFlag("--resource-path-shortening-map",
         "Path to output the map of old resource paths to shortened paths.",
         &options_.shortened_paths_map_path);
+    AddOptionalSwitch(
+        "--deduplicate-entry-values",
+        "Whether to deduplicate pairs of resource entry and value for simple resources.\n"
+        "This is recommended to be used together with '--collapse-resource-names' flag or for\n"
+        "APKs where resource names are manually collapsed. For such APKs this flag allows to\n"
+        "store the same resource value only once in resource table which decreases APK size.\n"
+        "Has no effect on APKs where resource names are kept.",
+        &options_.table_flattener_options.deduplicate_entry_values);
     AddOptionalSwitch("-v", "Enables verbose logging", &verbose_);
   }
 
diff --git a/tools/aapt2/cmd/Optimize_test.cpp b/tools/aapt2/cmd/Optimize_test.cpp
deleted file mode 100644
index d180c87..0000000
--- a/tools/aapt2/cmd/Optimize_test.cpp
+++ /dev/null
@@ -1,68 +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.
- */
-
-#include "Optimize.h"
-
-#include "AppInfo.h"
-#include "LoadedApk.h"
-#include "Resource.h"
-#include "androidfw/IDiagnostics.h"
-#include "test/Test.h"
-
-using testing::Contains;
-using testing::Eq;
-
-namespace aapt {
-
-bool ParseConfig(const std::string&, IAaptContext*, OptimizeOptions*);
-
-using OptimizeTest = CommandTestFixture;
-
-TEST_F(OptimizeTest, ParseConfigWithNoCollapseExemptions) {
-  const std::string& content = R"(
-string/foo#no_collapse
-dimen/bar#no_collapse
-)";
-  aapt::test::Context context;
-  OptimizeOptions options;
-  ParseConfig(content, &context, &options);
-
-  const std::set<ResourceName>& name_collapse_exemptions =
-      options.table_flattener_options.name_collapse_exemptions;
-
-  ASSERT_THAT(name_collapse_exemptions.size(), Eq(2));
-  EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo")));
-  EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar")));
-}
-
-TEST_F(OptimizeTest, ParseConfigWithNoObfuscateExemptions) {
-  const std::string& content = R"(
-string/foo#no_obfuscate
-dimen/bar#no_obfuscate
-)";
-  aapt::test::Context context;
-  OptimizeOptions options;
-  ParseConfig(content, &context, &options);
-
-  const std::set<ResourceName>& name_collapse_exemptions =
-      options.table_flattener_options.name_collapse_exemptions;
-
-  ASSERT_THAT(name_collapse_exemptions.size(), Eq(2));
-  EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kString, "foo")));
-  EXPECT_THAT(name_collapse_exemptions, Contains(ResourceName({}, ResourceType::kDimen, "bar")));
-}
-
-}  // namespace aapt
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index c3a6ed1..56e2f52 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -447,4 +447,41 @@
   return case_insensitive;
 }
 
+bool ParseResourceConfig(const std::string& content, IAaptContext* context,
+                         std::unordered_set<ResourceName>& out_resource_exclude_list,
+                         std::set<ResourceName>& out_name_collapse_exemptions) {
+  for (StringPiece line : util::Tokenize(content, '\n')) {
+    line = util::TrimWhitespace(line);
+    if (line.empty()) {
+      continue;
+    }
+
+    auto split_line = util::Split(line, '#');
+    if (split_line.size() < 2) {
+      context->GetDiagnostics()->Error(android::DiagMessage(line) << "No # found in line");
+      return false;
+    }
+    StringPiece resource_string = split_line[0];
+    StringPiece directives = split_line[1];
+    ResourceNameRef resource_name;
+    if (!ResourceUtils::ParseResourceName(resource_string, &resource_name)) {
+      context->GetDiagnostics()->Error(android::DiagMessage(line) << "Malformed resource name");
+      return false;
+    }
+    if (!resource_name.package.empty()) {
+      context->GetDiagnostics()->Error(android::DiagMessage(line)
+                                       << "Package set for resource. Only use type/name");
+      return false;
+    }
+    for (StringPiece directive : util::Tokenize(directives, ',')) {
+      if (directive == "remove") {
+        out_resource_exclude_list.insert(resource_name.ToResourceName());
+      } else if (directive == "no_collapse" || directive == "no_obfuscate") {
+        out_name_collapse_exemptions.insert(resource_name.ToResourceName());
+      }
+    }
+  }
+  return true;
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 7af27f5..3d4ca24 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -18,12 +18,15 @@
 #define AAPT_SPLIT_UTIL_H
 
 #include <regex>
+#include <set>
+#include <unordered_set>
 
 #include "AppInfo.h"
 #include "SdkConstants.h"
 #include "androidfw/IDiagnostics.h"
 #include "androidfw/StringPiece.h"
 #include "filter/ConfigFilter.h"
+#include "process/IResourceTableConsumer.h"
 #include "split/TableSplitter.h"
 #include "xml/XmlDom.h"
 
@@ -76,6 +79,10 @@
 // Returns a case insensitive regular expression based on the input.
 std::regex GetRegularExpression(const std::string &input);
 
+bool ParseResourceConfig(const std::string& content, IAaptContext* context,
+                         std::unordered_set<ResourceName>& out_resource_exclude_list,
+                         std::set<ResourceName>& out_name_collapse_exemptions);
+
 }  // namespace aapt
 
 #endif /* AAPT_SPLIT_UTIL_H */
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 91accfe..28a6de8 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -25,6 +25,7 @@
 #include "util/Files.h"
 
 using ::android::ConfigDescription;
+using testing::UnorderedElementsAre;
 
 namespace aapt {
 
@@ -411,4 +412,61 @@
   EXPECT_FALSE(std::regex_search("file.koncowka", expression));
 }
 
+TEST(UtilTest, ParseConfigWithDirectives) {
+  const std::string& content = R"(
+bool/remove_me#remove
+bool/keep_name#no_collapse
+string/foo#no_obfuscate
+dimen/bar#no_obfuscate
+)";
+  aapt::test::Context context;
+  std::unordered_set<ResourceName> resource_exclusion;
+  std::set<ResourceName> name_collapse_exemptions;
+
+  EXPECT_TRUE(ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+
+  EXPECT_THAT(name_collapse_exemptions,
+              UnorderedElementsAre(ResourceName({}, ResourceType::kString, "foo"),
+                                   ResourceName({}, ResourceType::kDimen, "bar"),
+                                   ResourceName({}, ResourceType::kBool, "keep_name")));
+  EXPECT_THAT(resource_exclusion,
+              UnorderedElementsAre(ResourceName({}, ResourceType::kBool, "remove_me")));
+}
+
+TEST(UtilTest, ParseConfigResourceWithPackage) {
+  const std::string& content = R"(
+package:bool/remove_me#remove
+)";
+  aapt::test::Context context;
+  std::unordered_set<ResourceName> resource_exclusion;
+  std::set<ResourceName> name_collapse_exemptions;
+
+  EXPECT_FALSE(
+      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+}
+
+TEST(UtilTest, ParseConfigInvalidName) {
+  const std::string& content = R"(
+package:bool/1231#remove
+)";
+  aapt::test::Context context;
+  std::unordered_set<ResourceName> resource_exclusion;
+  std::set<ResourceName> name_collapse_exemptions;
+
+  EXPECT_FALSE(
+      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+}
+
+TEST(UtilTest, ParseConfigNoHash) {
+  const std::string& content = R"(
+package:bool/my_bool
+)";
+  aapt::test::Context context;
+  std::unordered_set<ResourceName> resource_exclusion;
+  std::set<ResourceName> name_collapse_exemptions;
+
+  EXPECT_FALSE(
+      ParseResourceConfig(content, &context, resource_exclusion, name_collapse_exemptions));
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 6a1e8c1..e39f327 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -562,6 +562,11 @@
     }
   }
 
+  for (const pb::DynamicRefTable& dynamic_ref : pb_table.dynamic_ref_table()) {
+    out_table->included_packages_.insert(
+        {dynamic_ref.package_id().id(), dynamic_ref.package_name()});
+  }
+
   // Deserialize the overlayable groups of the table
   std::vector<std::shared_ptr<Overlayable>> overlayables;
   for (const pb::Overlayable& pb_overlayable : pb_table.overlayable()) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 163a60a..a6d58fd 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -345,7 +345,11 @@
   pb::ToolFingerprint* pb_fingerprint = out_table->add_tool_fingerprint();
   pb_fingerprint->set_tool(util::GetToolName());
   pb_fingerprint->set_version(util::GetToolFingerprint());
-
+  for (auto it = table.included_packages_.begin(); it != table.included_packages_.end(); ++it) {
+    pb::DynamicRefTable* pb_dynamic_ref = out_table->add_dynamic_ref_table();
+    pb_dynamic_ref->mutable_package_id()->set_id(it->first);
+    pb_dynamic_ref->set_package_name(it->second);
+  }
   std::vector<Overlayable*> overlayables;
   auto table_view = table.GetPartitionedView();
   for (const auto& package : table_view.packages) {
diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
index 692fa42..5adc5e6 100644
--- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp
@@ -1024,4 +1024,28 @@
   EXPECT_THAT(*(custom_layout->path), Eq("res/layout/bar.xml"));
 }
 
+TEST(ProtoSerializeTest, SerializeDynamicRef) {
+  std::unique_ptr<IAaptContext> context =
+      test::ContextBuilder().SetCompilationPackage("app").SetPackageId(0x7f).Build();
+  std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build();
+  table->included_packages_.insert({20, "foobar"});
+  table->included_packages_.insert({30, "barfoo"});
+
+  ResourceTable new_table;
+  pb::ResourceTable pb_table;
+  MockFileCollection files;
+  std::string error;
+  SerializeTableToPb(*table, &pb_table, context->GetDiagnostics());
+  ASSERT_TRUE(DeserializeTableFromPb(pb_table, &files, &new_table, &error));
+  EXPECT_THAT(error, IsEmpty());
+
+  int result = new_table.included_packages_.size();
+  EXPECT_THAT(result, Eq(2));
+  auto it = new_table.included_packages_.begin();
+  EXPECT_THAT(it->first, Eq(20));
+  EXPECT_THAT(it->second, Eq("foobar"));
+  it++;
+  EXPECT_THAT(it->first, Eq(30));
+  EXPECT_THAT(it->second, Eq("barfoo"));
+}
 }  // namespace aapt
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 5cee17e..d0850b8 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -30,6 +30,94 @@
 
 namespace aapt {
 
+// This is to detect whether an <intent-filter> contains deeplink.
+// See https://developer.android.com/training/app-links/deep-linking.
+static bool HasDeepLink(xml::Element* intent_filter_el) {
+  xml::Element* action_el = intent_filter_el->FindChild({}, "action");
+  xml::Element* category_el = intent_filter_el->FindChild({}, "category");
+  xml::Element* data_el = intent_filter_el->FindChild({}, "data");
+  if (action_el == nullptr || category_el == nullptr || data_el == nullptr) {
+    return false;
+  }
+
+  // Deeplinks must specify the ACTION_VIEW intent action.
+  constexpr const char* action_view = "android.intent.action.VIEW";
+  if (intent_filter_el->FindChildWithAttribute({}, "action", xml::kSchemaAndroid, "name",
+                                               action_view) == nullptr) {
+    return false;
+  }
+
+  // Deeplinks must have scheme included in <data> tag.
+  xml::Attribute* data_scheme_attr = data_el->FindAttribute(xml::kSchemaAndroid, "scheme");
+  if (data_scheme_attr == nullptr || data_scheme_attr->value.empty()) {
+    return false;
+  }
+
+  // Deeplinks must include BROWSABLE category.
+  constexpr const char* category_browsable = "android.intent.category.BROWSABLE";
+  if (intent_filter_el->FindChildWithAttribute({}, "category", xml::kSchemaAndroid, "name",
+                                               category_browsable) == nullptr) {
+    return false;
+  }
+  return true;
+}
+
+static bool VerifyDeeplinkPathAttribute(xml::Element* data_el, android::SourcePathDiagnostics* diag,
+                                        const std::string& attr_name) {
+  xml::Attribute* attr = data_el->FindAttribute(xml::kSchemaAndroid, attr_name);
+  if (attr != nullptr && !attr->value.empty()) {
+    StringPiece attr_value = attr->value;
+    const char* startChar = attr_value.begin();
+    if (attr_name == "pathPattern") {
+      // pathPattern starts with '.' or '*' does not need leading slash.
+      // Reference starts with @ does not need leading slash.
+      if (*startChar == '/' || *startChar == '.' || *startChar == '*' || *startChar == '@') {
+        return true;
+      } else {
+        diag->Error(android::DiagMessage(data_el->line_number)
+                    << "attribute 'android:" << attr_name << "' in <" << data_el->name
+                    << "> tag has value of '" << attr_value
+                    << "', it must be in a pattern start with '.' or '*', otherwise must start "
+                       "with a leading slash '/'");
+        return false;
+      }
+    } else {
+      // Reference starts with @ does not need leading slash.
+      if (*startChar == '/' || *startChar == '@') {
+        return true;
+      } else {
+        diag->Error(android::DiagMessage(data_el->line_number)
+                    << "attribute 'android:" << attr_name << "' in <" << data_el->name
+                    << "> tag has value of '" << attr_value
+                    << "', it must start with a leading slash '/'");
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+static bool VerifyDeepLinkIntentAction(xml::Element* intent_filter_el,
+                                       android::SourcePathDiagnostics* diag) {
+  if (!HasDeepLink(intent_filter_el)) {
+    return true;
+  }
+
+  xml::Element* data_el = intent_filter_el->FindChild({}, "data");
+  if (data_el != nullptr) {
+    if (!VerifyDeeplinkPathAttribute(data_el, diag, "path")) {
+      return false;
+    }
+    if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPrefix")) {
+      return false;
+    }
+    if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPattern")) {
+      return false;
+    }
+  }
+  return true;
+}
+
 static bool RequiredNameIsNotEmpty(xml::Element* el, android::SourcePathDiagnostics* diag) {
   xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name");
   if (attr == nullptr) {
@@ -323,6 +411,7 @@
 
   // Common <intent-filter> actions.
   xml::XmlNodeAction intent_filter_action;
+  intent_filter_action.Action(VerifyDeepLinkIntentAction);
   intent_filter_action["action"].Action(RequiredNameIsNotEmpty);
   intent_filter_action["category"].Action(RequiredNameIsNotEmpty);
   intent_filter_action["data"];
diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp
index 098d0be..8d1a647 100644
--- a/tools/aapt2/link/ManifestFixer_test.cpp
+++ b/tools/aapt2/link/ManifestFixer_test.cpp
@@ -1068,4 +1068,364 @@
       </manifest>)";
   EXPECT_THAT(Verify(input), NotNull());
 }
+
+TEST_F(ManifestFixerTest, IntentFilterActionMustHaveNonEmptyName) {
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+             package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.MAIN" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+}
+
+TEST_F(ManifestFixerTest, IntentFilterCategoryMustHaveNonEmptyName) {
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <category android:name="" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+             package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <category />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <category android:name="android.intent.category.LAUNCHER" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+}
+
+TEST_F(ManifestFixerTest, IntentFilterPathMustStartWithLeadingSlashOnDeepLinks) {
+  // No DeepLink.
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+             package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <data />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // No DeepLink, missing ACTION_VIEW.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink, missing DEFAULT category while DEFAULT is recommended but not required.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  // No DeepLink, missing BROWSABLE category.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // No DeepLink, missing 'android:scheme' in <data> tag.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:host="www.example.com"
+                          android:pathPrefix="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // No DeepLink, <action> is ACTION_MAIN not ACTION_VIEW.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.MAIN" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with no leading slash in android:path.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:path="path" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  // DeepLink with leading slash in android:path.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:path="/path" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with no leading slash in android:pathPrefix.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="pathPrefix" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  // DeepLink with leading slash in android:pathPrefix.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPrefix="/pathPrefix" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with no leading slash in android:pathPattern.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPattern="pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), IsNull());
+
+  // DeepLink with leading slash in android:pathPattern.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPattern="/pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with '.' start in pathPattern.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPattern=".*\\.pathPattern" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with '*' start in pathPattern.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:pathPattern="*" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+
+  // DeepLink with string reference as a path.
+  input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity">
+          <intent-filter>
+            <action android:name="android.intent.action.VIEW" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <category android:name="android.intent.category.BROWSABLE" />
+            <data android:scheme="http"
+                          android:host="www.example.com"
+                          android:path="@string/startup_uri" />
+          </intent-filter>
+        </activity>
+      </application>
+    </manifest>)";
+  EXPECT_THAT(Verify(input), NotNull());
+}
 }  // namespace aapt
diff --git a/tools/fonts/fontchain_linter.py b/tools/fonts/fontchain_linter.py
index 35a0ce6..87b4c68 100755
--- a/tools/fonts/fontchain_linter.py
+++ b/tools/fonts/fontchain_linter.py
@@ -381,62 +381,14 @@
         return tuple(f"{s:X}" for s in sequence)
     return hex(sequence)
 
-def check_plausible_compat_pua(coverage, all_emoji, equivalent_emoji):
-    # A PUA should point to every RGI emoji and that PUA should be unique to the
-    # set of equivalent sequences for the emoji.
-    problems = []
-    for seq in all_emoji:
-        # We're looking to match not-PUA with PUA so filter out existing PUA
-        if contains_pua(seq):
-            continue
-
-        # Filter out non-RGI things that end up in all_emoji
-        if only_tags(seq) or seq in {ZWJ, COMBINING_KEYCAP, EMPTY_FLAG_SEQUENCE}:
-            continue
-
-        equivalents = [seq]
-        if seq in equivalent_emoji:
-            equivalents.append(equivalent_emoji[seq])
-
-        # If there are problems the hex code is much more useful
-        log_equivalents = [hex_strs(s) for s in equivalents]
-
-        # The system compat font should NOT include regional indicators as these have been split out
-        if contains_regional_indicator(seq):
-            assert not any(s in coverage for s in equivalents), f"Regional indicators not expected in compat font, found {log_equivalents}"
-            continue
-
-        glyph = {coverage[e] for e in equivalents}
-        if len(glyph) != 1:
-            problems.append(f"{log_equivalents} should all point to the same glyph")
-            continue
-        glyph = next(iter(glyph))
-
-        pua = {s for s, g in coverage.items() if contains_pua(s) and g == glyph}
-        if not pua:
-            problems.append(f"Expected PUA for {log_equivalents} but none exist")
-            continue
-
-    assert not problems, "\n".join(sorted(problems)) + f"\n{len(problems)} PUA problems"
-
-def check_emoji_compat(all_emoji, equivalent_emoji):
+def check_emoji_not_compat(all_emoji, equivalent_emoji):
     compat_psnames = set()
     for emoji_font in get_emoji_fonts():
         ttf = open_font(emoji_font)
         psname = get_psname(ttf)
 
-        is_compat_font = "meta" in ttf and 'Emji' in ttf["meta"].data
-        if not is_compat_font:
-            continue
-        compat_psnames.add(psname)
-
-        # If the font has compat metadata it should have PUAs for emoji sequences
-        coverage = get_emoji_map(emoji_font)
-        check_plausible_compat_pua(coverage, all_emoji, equivalent_emoji)
-
-
-    # NotoColorEmoji must be a Compat font.
-    assert 'NotoColorEmoji' in compat_psnames, 'NotoColorEmoji MUST be a compat font'
+        if "meta" in ttf:
+            assert 'Emji' not in ttf["meta"].data, 'NotoColorEmoji MUST be a compat font'
 
 
 def check_emoji_font_coverage(emoji_fonts, all_emoji, equivalent_emoji):
@@ -847,7 +799,7 @@
         ucd_path = sys.argv[3]
         parse_ucd(ucd_path)
         all_emoji, default_emoji, equivalent_emoji = compute_expected_emoji()
-        check_emoji_compat(all_emoji, equivalent_emoji)
+        check_emoji_not_compat(all_emoji, equivalent_emoji)
         check_emoji_coverage(all_emoji, equivalent_emoji)
         check_emoji_defaults(default_emoji)
 
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
index 4d69d26..741655b 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -20,6 +20,7 @@
 import com.android.tools.lint.client.api.Vendor
 import com.android.tools.lint.detector.api.CURRENT_API
 import com.google.android.lint.aidl.EnforcePermissionDetector
+import com.google.android.lint.aidl.EnforcePermissionHelperDetector
 import com.google.android.lint.aidl.ManualPermissionCheckDetector
 import com.google.android.lint.parcel.SaferParcelChecker
 import com.google.auto.service.AutoService
@@ -38,6 +39,7 @@
         CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
         EnforcePermissionDetector.ISSUE_MISSING_ENFORCE_PERMISSION,
         EnforcePermissionDetector.ISSUE_MISMATCHING_ENFORCE_PERMISSION,
+        EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER,
         ManualPermissionCheckDetector.ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
         SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
         PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
index 68a450d..1b0f035 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -26,6 +26,7 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.aidl.hasPermissionMethodAnnotation
 import com.intellij.psi.PsiType
 import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UBlockExpression
@@ -149,11 +150,6 @@
             enabledByDefault = false
         )
 
-        private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
-            .any {
-                it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
-            }
-
         private fun isPermissionMethodReturnType(method: UMethod): Boolean =
             listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
 
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
index 5106111..d120e1d 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -18,37 +18,54 @@
 
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Location
-import com.intellij.psi.PsiVariable
+import com.android.tools.lint.detector.api.getUMethod
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.ULiteralExpression
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.asRecursiveLogString
+import org.jetbrains.uast.evaluateString
+import org.jetbrains.uast.visitor.AbstractUastVisitor
 
 /**
- * Helper ADT class that facilitates the creation of lint auto fixes
+ * Helper class that facilitates the creation of lint auto fixes
  *
  * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks
  * that should be migrated to @EnforcePermission(allOf={...})
  *
  * TODO: handle anyOf style annotations
  */
-sealed class EnforcePermissionFix {
-    abstract fun locations(): List<Location>
-    abstract fun javaAnnotationParameter(): String
-
-    fun javaAnnotation(): String = "@$ANNOTATION_ENFORCE_PERMISSION(${javaAnnotationParameter()})"
+data class EnforcePermissionFix(
+    val locations: List<Location>,
+    val permissionNames: List<String>
+) {
+    val annotation: String
+        get() {
+            val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
+            val annotationParameter =
+                if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions
+            return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
+        }
 
     companion object {
-        fun fromCallExpression(callExpression: UCallExpression, context: JavaContext): SingleFix =
-            SingleFix(
-                getPermissionCheckLocation(context, callExpression),
-                getPermissionCheckArgumentValue(callExpression)
-            )
+        /**
+         * conditionally constructs EnforcePermissionFix from a UCallExpression
+         * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
+         */
+        fun fromCallExpression(
+            context: JavaContext,
+            callExpression: UCallExpression
+        ): EnforcePermissionFix? =
+            if (isPermissionMethodCall(callExpression)) {
+                EnforcePermissionFix(
+                    listOf(getPermissionCheckLocation(context, callExpression)),
+                    getPermissionCheckValues(callExpression)
+                )
+            } else null
 
-        fun maybeAddManifestPrefix(permissionName: String): String =
-            if (permissionName.contains(".")) permissionName
-            else "android.Manifest.permission.$permissionName"
+
+        fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix =
+            EnforcePermissionFix(
+                individuals.flatMap { it.locations },
+                individuals.flatMap { it.permissionNames }
+            )
 
         /**
          * Given a permission check, get its proper location
@@ -70,49 +87,51 @@
         }
 
         /**
-         * Given a permission check and an argument,
-         * pull out the permission value that is being used
+         * Given a @PermissionMethod, find arguments annotated with @PermissionName
+         * and pull out the permission value(s) being used.  Also evaluates nested calls
+         * to @PermissionMethod(s) in the given method's body.
          */
-        private fun getPermissionCheckArgumentValue(
-            callExpression: UCallExpression,
-            argumentPosition: Int = 0
-        ): String {
+        private fun getPermissionCheckValues(
+            callExpression: UCallExpression
+        ): List<String> {
+            if (!isPermissionMethodCall(callExpression)) return emptyList()
 
-            val identifier = when (
-                val argument = callExpression.valueArguments.getOrNull(argumentPosition)
-            ) {
-                is UQualifiedReferenceExpression -> when (val selector = argument.selector) {
-                    is USimpleNameReferenceExpression ->
-                        ((selector.resolve() as PsiVariable).computeConstantValue() as String)
+            val result = mutableSetOf<String>() // protect against duplicate permission values
+            val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
+            val bfsQueue = ArrayDeque(listOf(callExpression))
 
-                    else -> throw RuntimeException(
-                        "Couldn't resolve argument: ${selector.asRecursiveLogString()}"
-                    )
-                }
+            // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available
+            // source code for @PermissionName(s).
+            while (bfsQueue.isNotEmpty()) {
+                val current = bfsQueue.removeFirst()
+                visitedCalls.add(current)
+                result.addAll(findPermissions(current))
 
-                is USimpleNameReferenceExpression -> (
-                        (argument.resolve() as PsiVariable).computeConstantValue() as String)
-
-                is ULiteralExpression -> argument.value as String
-
-                else -> throw RuntimeException(
-                    "Couldn't resolve argument: ${argument?.asRecursiveLogString()}"
-                )
+                current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() {
+                    override fun visitCallExpression(node: UCallExpression): Boolean {
+                        if (isPermissionMethodCall(node) && node !in visitedCalls) {
+                            bfsQueue.add(node)
+                        }
+                        return false
+                    }
+                })
             }
 
-            return identifier.substringAfterLast(".")
+            return result.toList()
+        }
+
+        private fun findPermissions(
+            callExpression: UCallExpression,
+        ): List<String> {
+            val indices = callExpression.resolve()?.getUMethod()
+                ?.uastParameters
+                ?.filter(::hasPermissionNameAnnotation)
+                ?.mapNotNull { it.sourcePsi?.parameterIndex() }
+                ?: emptyList()
+
+            return indices.mapNotNull {
+                callExpression.getArgumentForParameter(it)?.evaluateString()
+            }
         }
     }
 }
-
-data class SingleFix(val location: Location, val permissionName: String) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = listOf(this.location)
-    override fun javaAnnotationParameter(): String = maybeAddManifestPrefix(this.permissionName)
-}
-data class AllOfFix(val checks: List<SingleFix>) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = this.checks.map { it.location }
-    override fun javaAnnotationParameter(): String =
-        "allOf={${
-            this.checks.joinToString(", ") { maybeAddManifestPrefix(it.permissionName) }
-        }}"
-}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt
new file mode 100644
index 0000000..3c2ea1d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt
@@ -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.google.android.lint.aidl
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiElement
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+
+class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+            listOf(UMethod::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler = AidlStubHandler(context)
+
+    private inner class AidlStubHandler(val context: JavaContext) : UElementHandler() {
+        override fun visitMethod(node: UMethod) {
+            if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return
+
+            val targetExpression = "super.${node.name}$HELPER_SUFFIX()"
+
+            val body = node.uastBody as? UBlockExpression
+            if (body == null) {
+                context.report(
+                        ISSUE_ENFORCE_PERMISSION_HELPER,
+                        context.getLocation(node),
+                        "Method must start with $targetExpression",
+                )
+                return
+            }
+
+            val firstExpression = body.expressions.firstOrNull()
+            if (firstExpression == null) {
+                context.report(
+                    ISSUE_ENFORCE_PERMISSION_HELPER,
+                    context.getLocation(node),
+                    "Method must start with $targetExpression",
+                )
+                return
+            }
+
+            val firstExpressionSource = firstExpression.asSourceString()
+                    .filterNot(Char::isWhitespace)
+
+            if (firstExpressionSource != targetExpression) {
+                val locationTarget = getLocationTarget(firstExpression)
+                val expressionLocation = context.getLocation(locationTarget)
+                val indent = " ".repeat(expressionLocation.start?.column ?: 0)
+
+                val fix = fix()
+                    .replace()
+                    .range(expressionLocation)
+                    .beginning()
+                    .with("$targetExpression;\n\n$indent")
+                    .reformat(true)
+                    .autoFix()
+                    .build()
+
+                context.report(
+                    ISSUE_ENFORCE_PERMISSION_HELPER,
+                    context.getLocation(node),
+                    "Method must start with $targetExpression",
+                    fix
+                )
+            }
+        }
+    }
+
+    companion object {
+        private const val HELPER_SUFFIX = "_enforcePermission"
+
+        private const val EXPLANATION = """
+            When @EnforcePermission is applied, the AIDL compiler generates a Stub method to do the
+            permission check called yourMethodName$HELPER_SUFFIX.
+
+            You must call this method as the first expression in your implementation.
+            """
+
+        val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create(
+                id = "MissingEnforcePermissionHelper",
+                briefDescription = """Missing permission-enforcing method call in AIDL method 
+                    |annotated with @EnforcePermission""".trimMargin(),
+                explanation = EXPLANATION,
+                category = Category.SECURITY,
+                priority = 6,
+                severity = Severity.ERROR,
+                implementation = Implementation(
+                        EnforcePermissionHelperDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                )
+        )
+
+        /**
+         * handles an edge case with UDeclarationsExpression, where sourcePsi is null,
+         * resulting in an incorrect Location if used directly
+         */
+        private fun getLocationTarget(firstExpression: UExpression): PsiElement? {
+            if (firstExpression.sourcePsi != null) return firstExpression.sourcePsi
+            if (firstExpression is UDeclarationsExpression) {
+                return firstExpression.declarations.firstOrNull()?.sourcePsi
+            }
+            return null
+        }
+    }
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
new file mode 100644
index 0000000..edbdd8d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.ANNOTATION_PERMISSION_METHOD
+import com.google.android.lint.ANNOTATION_PERMISSION_NAME
+import com.google.android.lint.CLASS_STUB
+import com.intellij.psi.PsiAnonymousClass
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+
+/**
+ * Given a UMethod, determine if this method is
+ * an entrypoint to an interface generated by AIDL,
+ * returning the interface name if so
+ */
+fun getContainingAidlInterface(node: UMethod): String? {
+    if (!isInClassCalledStub(node)) return null
+    for (superMethod in node.findSuperMethods()) {
+        for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
+            ?: continue) {
+            if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
+                return superMethod.containingClass?.name
+            }
+        }
+    }
+    return null
+}
+
+private fun isInClassCalledStub(node: UMethod): Boolean {
+    (node.containingClass as? PsiAnonymousClass)?.let {
+        return it.baseClassReference.referenceName == CLASS_STUB
+    }
+    return node.containingClass?.extendsList?.referenceElements?.any {
+        it.referenceName == CLASS_STUB
+    } ?: false
+}
+
+fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
+    val method = callExpression.resolve()?.getUMethod() ?: return false
+    return hasPermissionMethodAnnotation(method)
+}
+
+fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+    .any {
+        it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+    }
+
+fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
+    it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
index 2cea394..2c53f39 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
@@ -25,9 +25,6 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.google.android.lint.CLASS_STUB
-import com.google.android.lint.ENFORCE_PERMISSION_METHODS
-import com.intellij.psi.PsiAnonymousClass
 import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
@@ -56,7 +53,7 @@
             val body = (node.uastBody as? UBlockExpression) ?: return
             val fix = accumulateSimplePermissionCheckFixes(body) ?: return
 
-            val javaRemoveFixes = fix.locations().map {
+            val javaRemoveFixes = fix.locations.map {
                 fix()
                     .replace()
                     .reformat(true)
@@ -67,7 +64,7 @@
             }
 
             val javaAnnotateFix = fix()
-                .annotate(fix.javaAnnotation())
+                .annotate(fix.annotation)
                 .range(context.getLocation(node))
                 .autoFix()
                 .build()
@@ -77,7 +74,7 @@
 
             context.report(
                 ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
-                fix.locations().last(),
+                fix.locations.last(),
                 message,
                 fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix)
             )
@@ -97,14 +94,14 @@
          */
         private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression):
                 EnforcePermissionFix? {
-            val singleFixes = mutableListOf<SingleFix>()
+            val singleFixes = mutableListOf<EnforcePermissionFix>()
             for (expression in methodBody.expressions) {
                 singleFixes.add(getPermissionCheckFix(expression) ?: break)
             }
             return when (singleFixes.size) {
                 0 -> null
                 1 -> singleFixes[0]
-                else -> AllOfFix(singleFixes)
+                else -> EnforcePermissionFix.compose(singleFixes)
             }
         }
 
@@ -113,7 +110,7 @@
          * the helper for creating a lint auto fix to @EnforcePermission
          */
         private fun getPermissionCheckFix(startingExpression: UElement?):
-                SingleFix? {
+                EnforcePermissionFix? {
             return when (startingExpression) {
                 is UQualifiedReferenceExpression -> getPermissionCheckFix(
                     startingExpression.selector
@@ -121,11 +118,8 @@
 
                 is UIfExpression -> getPermissionCheckFix(startingExpression.condition)
 
-                is UCallExpression -> {
-                    return if (isPermissionCheck(startingExpression))
-                        EnforcePermissionFix.fromCallExpression(startingExpression, context)
-                    else null
-                }
+                is UCallExpression -> return EnforcePermissionFix
+                            .fromCallExpression(context, startingExpression)
 
                 else -> null
             }
@@ -160,40 +154,5 @@
             ),
             enabledByDefault = false, // TODO: enable once b/241171714 is resolved
         )
-
-        private fun isPermissionCheck(callExpression: UCallExpression): Boolean {
-            val method = callExpression.resolve() ?: return false
-            val className = method.containingClass?.qualifiedName
-            return ENFORCE_PERMISSION_METHODS.any {
-                it.clazz == className && it.name == method.name
-            }
-        }
-
-        /**
-         * given a UMethod, determine if this method is
-         * an entrypoint to an interface generated by AIDL,
-         * returning the interface name if so
-         */
-        fun getContainingAidlInterface(node: UMethod): String? {
-            if (!isInClassCalledStub(node)) return null
-            for (superMethod in node.findSuperMethods()) {
-                for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
-                    ?: continue) {
-                    if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
-                        return superMethod.containingClass?.name
-                    }
-                }
-            }
-            return null
-        }
-
-        private fun isInClassCalledStub(node: UMethod): Boolean {
-            (node.containingClass as? PsiAnonymousClass)?.let {
-                return it.baseClassReference.referenceName == CLASS_STUB
-            }
-            return node.containingClass?.extendsList?.referenceElements?.any {
-                it.referenceName == CLASS_STUB
-            } ?: false
-        }
     }
 }
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt
new file mode 100644
index 0000000..31e4846
--- /dev/null
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt
@@ -0,0 +1,159 @@
+/*
+* Copyright (C) 2022 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+
+class EnforcePermissionHelperDetectorTest : LintDetectorTest() {
+    override fun getDetector() = EnforcePermissionHelperDetector()
+    override fun getIssues() = listOf(
+        EnforcePermissionHelperDetector.ISSUE_ENFORCE_PERMISSION_HELPER)
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk()
+
+    fun testFirstExpressionIsFunctionCall() {
+        lint().files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+                        @Override
+                        @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+                        public void test() throws android.os.RemoteException {
+                            Binder.getCallingUid();
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper]
+                    @Override
+                    ^
+                1 errors, 0 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...:
+                @@ -8 +8
+                +         super.test_enforcePermission();
+                +
+                """
+            )
+    }
+
+    fun testFirstExpressionIsVariableDeclaration() {
+        lint().files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+                    @Override
+                    @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+                    public void test() throws android.os.RemoteException {
+                        String foo = "bar";
+                        Binder.getCallingUid();
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper]
+                    @Override
+                    ^
+                1 errors, 0 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Autofix for src/Foo.java line 5: Replace with super.test_enforcePermission();...:
+                @@ -8 +8
+                +         super.test_enforcePermission();
+                +
+                """
+            )
+    }
+
+    fun testMethodIsEmpty() {
+        lint().files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+                    @Override
+                    @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+                    public void test() throws android.os.RemoteException {}
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper]
+                    @Override
+                    ^
+                1 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testOkay() {
+        lint().files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+                    @Override
+                    @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS")
+                    public void test() throws android.os.RemoteException {
+                        super.test_enforcePermission();
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    companion object {
+        val stubs = arrayOf(aidlStub, contextStub, binderStub)
+    }
+}
+
+
+
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
index 1a1c6bc..d4a3497 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
@@ -17,8 +17,8 @@
 package com.google.android.lint.aidl
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 
@@ -42,7 +42,7 @@
                         private Context mContext;
                         @Override
                         public void test() throws android.os.RemoteException {
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -53,8 +53,8 @@
             .expect(
                 """
                 src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
-                        mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
-                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 0 errors, 1 warnings
                 """
             )
@@ -62,9 +62,9 @@
                 """
                 Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
                 @@ -5 +5
-                +     @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -7 +8
-                -         mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                -         mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                 """
             )
     }
@@ -81,7 +81,7 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                             }
                         };
                     }
@@ -102,10 +102,49 @@
                 """
                 Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
                 @@ -6 +6
-                +         @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +         @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
+                """
+            )
+    }
+
+    fun testConstantEvaluation() {
+        lint().files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs,
+            manifestStub
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+                @@ -6 +6
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -8 +9
+                -         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
                 """
             )
     }
@@ -122,9 +161,9 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                                    "android.permission.WRITE_CONTACTS", "foo");
                             }
                         };
                     }
@@ -144,13 +183,13 @@
             .expectFixDiffs(
                 """
                 Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
-                @@ -6 +6                                                                                                                                                                                                       
-                +         @android.annotation.EnforcePermission(allOf={android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS})
+                @@ -6 +6
+                +         @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                -                 "android.permission.WRITE_CONTACTS", "foo");
                 """
             )
     }
@@ -166,7 +205,7 @@
                         @Override
                         public void test() throws android.os.RemoteException {
                             long uid = Binder.getCallingUid();
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -177,35 +216,156 @@
             .expectClean()
     }
 
+    fun testPermissionHelper() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+
+                        @android.content.pm.PermissionMethod
+                        private void helper() {
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        }
+
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            helper();
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helper();
+                        ~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
+                @@ -12 +12
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -14 +15
+                -         helper();
+                """
+            )
+    }
+
+    fun testPermissionHelperAllOf() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helper() {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helper();
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 16: Annotate with @EnforcePermission:
+                @@ -13 +13
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"})
+                @@ -15 +16
+                -         helper();
+                -         mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                """
+            )
+    }
+
+
+    fun testPermissionHelperNested() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helperHelper() {
+                        helper("android.permission.WRITE_CONTACTS");
+                    }
+
+                    @android.content.pm.PermissionMethod
+                    private void helper(@android.content.pm.PermissionName String extraPermission) {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helperHelper();
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helperHelper();
+                        ~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 19: Annotate with @EnforcePermission:
+                @@ -17 +17
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"})
+                @@ -19 +20
+                -         helperHelper();
+                """
+            )
+    }
+
+
+
     companion object {
-        private val aidlStub: TestFile = java(
-            """
-               package android.test;
-               public interface ITest extends android.os.IInterface {
-                    public static abstract class Stub extends android.os.Binder implements android.test.ITest {}
-                    public void test() throws android.os.RemoteException;
-               }
-            """
-        ).indented()
-
-        private val contextStub: TestFile = java(
-            """
-                package android.content;
-                public class Context {
-                    public void enforceCallingOrSelfPermission(String permission, String message) {}
-                }
-            """
-        ).indented()
-
-        private val binderStub: TestFile = java(
-            """
-                package android.os;
-                public class Binder {
-                    public static int getCallingUid() {}
-                }
-            """
-        ).indented()
-
-        val stubs = arrayOf(aidlStub, contextStub, binderStub)
+        val stubs = arrayOf(
+            aidlStub,
+            contextStub,
+            binderStub,
+            permissionMethodStub,
+            permissionNameStub
+        )
     }
 }
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
new file mode 100644
index 0000000..bd6b195
--- /dev/null
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt
@@ -0,0 +1,80 @@
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFile
+
+val aidlStub: TestFile = java(
+    """
+        package android.test;
+        public interface ITest extends android.os.IInterface {
+            public static abstract class Stub extends android.os.Binder implements android.test.ITest {}
+            public void test() throws android.os.RemoteException;
+        }
+    """
+).indented()
+
+val contextStub: TestFile = java(
+    """
+        package android.content;
+        public class Context {
+            @android.content.pm.PermissionMethod
+            public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {}
+        }
+    """
+).indented()
+
+val binderStub: TestFile = java(
+    """
+        package android.os;
+        public class Binder {
+            public static int getCallingUid() {}
+        }
+    """
+).indented()
+
+val permissionMethodStub: TestFile = java(
+"""
+        package android.content.pm;
+
+        import static java.lang.annotation.ElementType.METHOD;
+        import static java.lang.annotation.RetentionPolicy.CLASS;
+
+        import java.lang.annotation.Retention;
+        import java.lang.annotation.Target;
+
+        @Retention(CLASS)
+        @Target({METHOD})
+        public @interface PermissionMethod {}
+    """
+).indented()
+
+val permissionNameStub: TestFile = java(
+"""
+        package android.content.pm;
+
+        import static java.lang.annotation.ElementType.FIELD;
+        import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+        import static java.lang.annotation.ElementType.METHOD;
+        import static java.lang.annotation.ElementType.PARAMETER;
+        import static java.lang.annotation.RetentionPolicy.CLASS;
+
+        import java.lang.annotation.Retention;
+        import java.lang.annotation.Target;
+
+        @Retention(CLASS)
+        @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+        public @interface PermissionName {}
+    """
+).indented()
+
+val manifestStub: TestFile = java(
+    """
+        package android;
+
+        public final class Manifest {
+            public static final class permission {
+                public static final String READ_CONTACTS="android.permission.READ_CONTACTS";
+            }
+        }
+    """.trimIndent()
+)
\ No newline at end of file
diff --git a/tools/locked_region_code_injection/Android.bp b/tools/locked_region_code_injection/Android.bp
index 6efd1f6..ff1f8e2 100644
--- a/tools/locked_region_code_injection/Android.bp
+++ b/tools/locked_region_code_injection/Android.bp
@@ -16,6 +16,6 @@
         "asm-commons-9.2",
         "asm-tree-9.2",
         "asm-analysis-9.2",
-        "guava-21.0",
+        "guava",
     ],
 }
diff --git a/tools/traceinjection/Android.bp b/tools/traceinjection/Android.bp
index 39d1b1c..bb32df6 100644
--- a/tools/traceinjection/Android.bp
+++ b/tools/traceinjection/Android.bp
@@ -16,7 +16,7 @@
         "asm-commons-9.2",
         "asm-tree-9.2",
         "asm-analysis-9.2",
-        "guava-21.0",
+        "guava",
     ],
 }
 
diff --git a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
index a750696..5012622 100644
--- a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java
@@ -117,13 +117,12 @@
     private static final byte[] TEST_PSK =
             new byte[]{'T', 'e', 's', 't'};
 
-    private static final Set<Integer> SCAN_FREQ_SET =
-            new HashSet<Integer>() {{
-                add(2410);
-                add(2450);
-                add(5050);
-                add(5200);
-            }};
+    private static final Set<Integer> SCAN_FREQ_SET = Set.of(
+            2410,
+            2450,
+            5050,
+            5200);
+
     private static final String TEST_QUOTED_SSID_1 = "\"testSsid1\"";
     private static final String TEST_QUOTED_SSID_2 = "\"testSsid2\"";
     private static final int[] TEST_FREQUENCIES_1 = {};
@@ -131,13 +130,11 @@
     private static final MacAddress TEST_RAW_MAC_BYTES = MacAddress.fromBytes(
             new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05});
 
-    private static final List<byte[]> SCAN_HIDDEN_NETWORK_SSID_LIST =
-            new ArrayList<byte[]>() {{
-                add(LocalNativeUtil.byteArrayFromArrayList(
-                        LocalNativeUtil.decodeSsid(TEST_QUOTED_SSID_1)));
-                add(LocalNativeUtil.byteArrayFromArrayList(
-                        LocalNativeUtil.decodeSsid(TEST_QUOTED_SSID_2)));
-            }};
+    private static final List<byte[]> SCAN_HIDDEN_NETWORK_SSID_LIST = List.of(
+            LocalNativeUtil.byteArrayFromArrayList(
+                    LocalNativeUtil.decodeSsid(TEST_QUOTED_SSID_1)),
+            LocalNativeUtil.byteArrayFromArrayList(
+                    LocalNativeUtil.decodeSsid(TEST_QUOTED_SSID_2)));
 
     private static final PnoSettings TEST_PNO_SETTINGS = new PnoSettings();
     static {